diff --git a/.changeset/6172-markdown-kanban-one-authority.md b/.changeset/6172-markdown-kanban-one-authority.md index cbb6e6a6dd..8c8daafe52 100644 --- a/.changeset/6172-markdown-kanban-one-authority.md +++ b/.changeset/6172-markdown-kanban-one-authority.md @@ -36,7 +36,12 @@ extended, so it was always inherited rather than added by the plugin. one.** `KanbanImpl.tsx` and `KanbanEnhanced.tsx` each redeclared both names. A TypeScript-AST comparison found them strict-SUBSET copies of `./types` with nothing typed differently, so their extra members moved onto the one -declaration and both files now re-point at it. +declaration and, at this change, both files re-pointed at it. + +⚠️ **Dated note, 2026-09-25 — `KanbanEnhanced.tsx` has since been deleted — +objectui#8932.** Later in this same release that module left the package, so +`KanbanImpl.tsx` is the one file of the two that remains. The convergence above +is unaffected; the objectui#8932 entry states what ships. Additive for consumers: `KanbanCard` gains `cardSubtitle`, `cardFieldCells` and `coverImage`; `KanbanColumn` gains `collapsed`. All four are optional, so every diff --git a/.changeset/8802-8257-8008-kanban-gantt-family-retirement.md b/.changeset/8802-8257-8008-kanban-gantt-family-retirement.md index 51518cd0e2..7555488cf7 100644 --- a/.changeset/8802-8257-8008-kanban-gantt-family-retirement.md +++ b/.changeset/8802-8257-8008-kanban-gantt-family-retirement.md @@ -138,10 +138,17 @@ is a different case, and the earlier draft of this note stated it wrongly: this package's `exports` map has exactly two entries — `.` and `./style.css` — and the barrel never re-exported the component, so `@object-ui/plugin-kanban/KanbanEnhanced` has never been a resolvable specifier -for a consumer. With `kanban-enhanced` unregistered, `KanbanEnhanced.tsx` has -zero non-test importers. ⛔ The file is deliberately left in place: deleting -published-but-unreachable source is a further narrowing and needs its own -maintainer ruling, which this change does not have. +for a consumer. At this change, with `kanban-enhanced` unregistered, +`KanbanEnhanced.tsx` had zero non-test importers, and the file was deliberately +left in place: deleting published-but-unreachable source is a further narrowing +and needed its own maintainer ruling, which this change did not have. + +⚠️ **Dated note, 2026-09-25 — that ruling came, and the file has since been +deleted — objectui#8932.** Later in this same release +`packages/plugin-kanban/src/KanbanEnhanced.tsx` was deleted, and with it the +`dist/KanbanEnhanced.d.ts` typings this package shipped for it. The rest of this +entry is kept as the reading of this change; the objectui#8932 entry states what +ships. **⚠️ `@object-ui/sdui-parser`: `QUICK_ADD_HOST_TYPES` loses `kanban` with the registration.** The `inert-quick-add` diagnostic (objectui#8285) named the two diff --git a/.changeset/8913-object-kanban-columns-declared.md b/.changeset/8913-object-kanban-columns-declared.md index 204704b3c4..1bc7ddff96 100644 --- a/.changeset/8913-object-kanban-columns-declared.md +++ b/.changeset/8913-object-kanban-columns-declared.md @@ -35,16 +35,21 @@ strings**. Both arms are admitted whole — `columns` is `string[] | Lane[]`, a union of two ARRAY shapes rather than an array of a per-element union — and on the lane arm **`cards` is optional**. The alternative — reusing `KanbanColumn`, whose `cards` is required — was measured and rejected: it is the RUNTIME lane -(`bucketCardsIntoColumns` fills `cards` before either board implementation sees -one), and as the authoring element it would have refused the protocol's own +(at this change, `bucketCardsIntoColumns` filled `cards` before either board +implementation saw one), and as the authoring element it would have refused the protocol's own gate-validated `{ id, title }` example, the lanes the renderer materializes from a picklist, and this repository's own typed board fixtures. **What is judged again.** A lane has `id` (**string**) and `title`, optionally carrying `cards`, `limit`, `className` and `collapsed` — exactly the members the -two board implementations read. When a lane carries `cards`, each card is judged -by the one card authority (`KanbanCardSchema`), which is what restores -objectui#6939's finding: **a card with no `title` is refused again**. +two board implementations read at this change. When a lane carries `cards`, each +card is judged by the one card authority (`KanbanCardSchema`), which is what +restores objectui#6939's finding: **a card with no `title` is refused again**. + +⚠️ **Dated note, 2026-09-25 — one board implementation remains — +objectui#8932.** Later in this same release `KanbanEnhanced`, the second of the +two, was deleted. `KanbanImpl` alone reads all six lane members, so the judged set +above is unchanged. The objectui#8932 entry states what ships. **Two shapes the protocol's literal `z.unknown()` would take are refused, because the renderer mishandles them.** A declaration that admits a shape nothing can diff --git a/.changeset/8932-kanban-enhanced-types-jsdoc.md b/.changeset/8932-kanban-enhanced-types-jsdoc.md new file mode 100644 index 0000000000..b6ab17815e --- /dev/null +++ b/.changeset/8932-kanban-enhanced-types-jsdoc.md @@ -0,0 +1,5 @@ +--- +'@object-ui/types': patch +--- + +The JSDoc on `KanbanCard`, `KanbanColumn.cards` and `ObjectKanbanSchema.columns` no longer names `KanbanEnhanced` as a board that reads these members. `KanbanImpl` has been the one kanban board since objectui#8932 deleted that module, and the older counts that included it now carry a date. Only comments change; no type moves. diff --git a/.changeset/8932-retire-kanban-enhanced.md b/.changeset/8932-retire-kanban-enhanced.md new file mode 100644 index 0000000000..5ea1a40b2a --- /dev/null +++ b/.changeset/8932-retire-kanban-enhanced.md @@ -0,0 +1,44 @@ +--- +'@object-ui/plugin-kanban': patch +--- + +Delete `packages/plugin-kanban/src/KanbanEnhanced.tsx` (objectui#8932, ruling of +2026-09-11, ratified by the maintainer 2026-09-24). + +**How the component left the bundle.** Through 17.6.0 the `KanbanEnhanced` +component shipped inlined in `dist/index.js` and `dist/index.umd.cjs`, reachable +through the `kanban-enhanced` registry key and the `kanbanComponents` map. It +leaves the bundle in this release together with that key (objectui#8257 / +objectui#8802, the kanban and gantt family retirement entry). After that +retirement, only two test files still referred to the source file. + +**What this change removes:** + +- the source file; +- `dist/KanbanEnhanced.d.ts` and `dist/KanbanEnhanced.d.ts.map`, the typings the + build still emitted for it; +- the `@tanstack/react-virtual` dependency, whose only consumer in this package + was that file. + +**No supported import changes.** Neither the 17.6.0 `exports` map (`.` only) nor +this release's (`.` and `./style.css`) has a `./KanbanEnhanced` entry. Neither +entry point exports the component by name. `@object-ui/plugin-kanban/KanbanEnhanced` +is not a resolvable specifier. + +**If you deep-imported it** through a path into +`node_modules/@object-ui/plugin-kanban/dist/` that the `exports` map does not +allow, that path reached `dist/KanbanEnhanced.d.ts` in 17.6.0. Those typings had +no `dist/KanbanEnhanced.js` beside them, and now the path reaches nothing. Render +the one registered board, `object-kanban`, instead. `KanbanCard` and +`KanbanColumn`, which the module re-exported, are still exported from +`@object-ui/plugin-kanban`. The card-formatting rule type it aliased is +`KanbanConditionalFormattingRule` in `@object-ui/types`. `KanbanEnhancedProps` is +gone along with the component. + +**If you import `@tanstack/react-virtual` yourself** and got it only through +`@object-ui/plugin-kanban`, declare it in your own `package.json`. + +**`style.css`.** This package's stylesheet, `@object-ui/plugin-kanban/style.css`, +first ships in this release (objectui#4929). It is built after this deletion, so +it does not carry the three utility rules only that module used: +`border-border/50`, `text-yellow-500` and `shadow-xl`. diff --git a/packages/plugin-kanban/package.json b/packages/plugin-kanban/package.json index f22f7bff86..d36da81807 100644 --- a/packages/plugin-kanban/package.json +++ b/packages/plugin-kanban/package.json @@ -43,7 +43,6 @@ "@object-ui/plugin-detail": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@tanstack/react-virtual": "^3.14.9", "lucide-react": "^1.31.0" }, "peerDependencies": { diff --git a/packages/plugin-kanban/src/KanbanEnhanced.tsx b/packages/plugin-kanban/src/KanbanEnhanced.tsx deleted file mode 100644 index c582011172..0000000000 --- a/packages/plugin-kanban/src/KanbanEnhanced.tsx +++ /dev/null @@ -1,499 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import * as React from "react" -import { useVirtualizer } from "@tanstack/react-virtual" -import { - DndContext, - DragEndEvent, - DragOverlay, - DragStartEvent, - PointerSensor, - useSensor, - useSensors, - closestCorners, -} from "@dnd-kit/core" -import { - SortableContext, - arrayMove, - useSortable, - verticalListSortingStrategy, -} from "@dnd-kit/sortable" -import { CSS } from "@dnd-kit/utilities" -import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Button, Input } from "@object-ui/components" -import { resolveConditionalFormatting } from "@object-ui/core" -import { usePredicateScope } from "@object-ui/react" -import type { KanbanConditionalFormattingRule } from "@object-ui/types" -import type { KanbanCard, KanbanColumn } from './types' -import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react" - -const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ') - -// One authority for these two names in this package: `./types` (objectui#6172 / #6155). -// This file's former local copies added `coverImage` (card) and `collapsed` -// (column); both now live on the canonical declaration as optional members, so -// this module sees exactly the shape it declared before. The re-export -// preserves the export surface; it is not a second declaration. -export type { KanbanCard, KanbanColumn } from './types' - -// Card formatting accepts the native `{ field, operator, value }` shape and the -// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types. -export type ConditionalFormattingRule = KanbanConditionalFormattingRule - -export interface KanbanEnhancedProps { - columns: KanbanColumn[] - onCardMove?: (cardId: string, fromColumnId: string, toColumnId: string, newIndex: number) => void - onColumnToggle?: (columnId: string, collapsed: boolean) => void - enableVirtualScrolling?: boolean - virtualScrollThreshold?: number - className?: string - quickAdd?: boolean - onQuickAdd?: (columnId: string, title: string) => void - conditionalFormatting?: ConditionalFormattingRule[] -} - -// Card conditional formatting now delegates to the shared CEL evaluator -// (issue #1584 / ADR-0058) so kanban cards, list rows, and grid rows reach the -// identical verdict. Beyond the native `{ field, operator, value }` rules the -// kanban schema declares, this also accepts spec `{ condition, style }` rules. -// The host predicate scope is bound alongside the card so `features.*` / -// `current_user.*` conditions resolve here exactly as they do on grid rows. -function getCardStyles( - card: KanbanCard, - rules?: ConditionalFormattingRule[], - scope?: Record, -): React.CSSProperties { - return resolveConditionalFormatting(card as Record, rules as any, scope) as React.CSSProperties -} - -function SortableCard({ card, conditionalFormatting }: { card: KanbanCard; conditionalFormatting?: ConditionalFormattingRule[] }) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: card.id }) - - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : undefined, - } - - const predicateScope = usePredicateScope() - const cardStyles = getCardStyles(card, conditionalFormatting, predicateScope) - - return ( -
- - {card.coverImage && ( -
- -
- )} - - {card.title} - {card.description && ( - - {card.description} - - )} - - {card.badges && card.badges.length > 0 && ( - -
- {card.badges.map((badge, index) => ( - - {badge.label} - - ))} -
-
- )} -
-
- ) -} - -function VirtualizedCardList({ cards, parentRef, conditionalFormatting }: { cards: KanbanCard[]; parentRef: React.RefObject; conditionalFormatting?: ConditionalFormattingRule[] }) { - const rowVirtualizer = useVirtualizer({ - count: cards.length, - getScrollElement: () => parentRef.current, - estimateSize: () => 120, - overscan: 5, - }) - - return ( -
- {rowVirtualizer.getVirtualItems().map((virtualItem) => { - const card = cards[virtualItem.index] - return ( -
- -
- ) - })} -
- ) -} - -function QuickAddForm({ columnId, onAdd }: { columnId: string; onAdd: (columnId: string, title: string) => void }) { - const [isAdding, setIsAdding] = React.useState(false) - const [title, setTitle] = React.useState('') - const inputRef = React.useRef(null) - - const handleSubmit = () => { - const trimmed = title.trim() - if (trimmed) { - onAdd(columnId, trimmed) - setTitle('') - } - setIsAdding(false) - } - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - handleSubmit() - } else if (e.key === 'Escape') { - setTitle('') - setIsAdding(false) - } - } - - if (!isAdding) { - return ( - - ) - } - - return ( -
- setTitle(e.target.value)} - onKeyDown={handleKeyDown} - onBlur={handleSubmit} - placeholder="Enter card title..." - className="text-sm" - autoFocus - /> -
- ) -} - -function KanbanColumnEnhanced({ - column, - cards, - onToggle, - enableVirtual, - quickAdd, - onQuickAdd, - conditionalFormatting, -}: { - column: KanbanColumn - cards: KanbanCard[] - onToggle: (collapsed: boolean) => void - enableVirtual: boolean - quickAdd?: boolean - onQuickAdd?: (columnId: string, title: string) => void - conditionalFormatting?: ConditionalFormattingRule[] -}) { - const safeCards = cards || [] - const scrollRef = React.useRef(null) - const { setNodeRef } = useSortable({ - id: column.id, - data: { - type: "column", - }, - }) - - const isLimitExceeded = column.limit && safeCards.length >= column.limit - const isNearLimit = column.limit && safeCards.length >= column.limit * 0.8 - - return ( -
-
-
- - {!column.collapsed && ( - <> -

- {column.title} -

-
- - {safeCards.length} - {column.limit && ` / ${column.limit}`} - - {isLimitExceeded && ( - - Full - - )} - {isNearLimit && !isLimitExceeded && ( - - )} -
- - )} -
- {column.collapsed && ( -
- - {column.title} - - - {safeCards.length} - -
- )} -
- {!column.collapsed && ( -
- c.id)} - strategy={verticalListSortingStrategy} - > - {enableVirtual ? ( - - ) : ( -
- {safeCards.map((card) => ( - - ))} -
- )} -
- {quickAdd && onQuickAdd && ( - - )} -
- )} -
- ) -} - -export function KanbanEnhanced({ - columns, - onCardMove, - onColumnToggle, - enableVirtualScrolling = false, - virtualScrollThreshold = 50, - className, - quickAdd, - onQuickAdd, - conditionalFormatting, -}: KanbanEnhancedProps) { - const [activeCard, setActiveCard] = React.useState(null) - - const safeColumns = React.useMemo(() => { - return (columns || []).map(col => ({ - ...col, - cards: col.cards || [] - })); - }, [columns]); - - const [boardColumns, setBoardColumns] = React.useState(safeColumns) - - React.useEffect(() => { - setBoardColumns(safeColumns) - }, [safeColumns]) - - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 8, - }, - }) - ) - - const handleDragStart = (event: DragStartEvent) => { - const { active } = event - const card = findCard(active.id as string) - setActiveCard(card) - } - - const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event - setActiveCard(null) - - if (!over) return - - const activeId = active.id as string - const overId = over.id as string - - if (activeId === overId) return - - const activeColumn = findColumnByCardId(activeId) - const overColumn = findColumnByCardId(overId) || findColumnById(overId) - - if (!activeColumn || !overColumn) return - - if (activeColumn.id === overColumn.id) { - const cards = [...activeColumn.cards] - const oldIndex = cards.findIndex((c) => c.id === activeId) - const newIndex = cards.findIndex((c) => c.id === overId) - - const newCards = arrayMove(cards, oldIndex, newIndex) - setBoardColumns((prev) => - prev.map((col) => - col.id === activeColumn.id ? { ...col, cards: newCards } : col - ) - ) - } else { - const activeCards = [...activeColumn.cards] - const overCards = [...overColumn.cards] - const activeIndex = activeCards.findIndex((c) => c.id === activeId) - - const isDroppingOnColumn = overId === overColumn.id - const overIndex = isDroppingOnColumn - ? overCards.length - : overCards.findIndex((c) => c.id === overId) - - const [movedCard] = activeCards.splice(activeIndex, 1) - overCards.splice(overIndex, 0, movedCard) - - setBoardColumns((prev) => - prev.map((col) => { - if (col.id === activeColumn.id) { - return { ...col, cards: activeCards } - } - if (col.id === overColumn.id) { - return { ...col, cards: overCards } - } - return col - }) - ) - - if (onCardMove) { - onCardMove(activeId, activeColumn.id, overColumn.id, overIndex) - } - } - } - - const findCard = React.useCallback( - (cardId: string): KanbanCard | null => { - for (const column of boardColumns) { - const card = column.cards.find((c) => c.id === cardId) - if (card) return card - } - return null - }, - [boardColumns] - ) - - const findColumnByCardId = React.useCallback( - (cardId: string): KanbanColumn | null => { - return boardColumns.find((col) => col.cards.some((c) => c.id === cardId)) || null - }, - [boardColumns] - ) - - const findColumnById = React.useCallback( - (columnId: string): KanbanColumn | null => { - return boardColumns.find((col) => col.id === columnId) || null - }, - [boardColumns] - ) - - const handleColumnToggle = React.useCallback((columnId: string, collapsed: boolean) => { - setBoardColumns(prev => - prev.map(col => col.id === columnId ? { ...col, collapsed } : col) - ) - onColumnToggle?.(columnId, collapsed) - }, [onColumnToggle]) - - return ( - -
- {boardColumns.map((column) => { - const shouldUseVirtual = enableVirtualScrolling && column.cards.length > virtualScrollThreshold - return ( - handleColumnToggle(column.id, collapsed)} - enableVirtual={shouldUseVirtual} - quickAdd={quickAdd} - onQuickAdd={onQuickAdd} - conditionalFormatting={conditionalFormatting} - /> - ) - })} -
- - {activeCard ? : null} - -
- ) -} - -export default KanbanEnhanced; diff --git a/packages/plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx b/packages/plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx index fd608943ad..479da75953 100644 --- a/packages/plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx +++ b/packages/plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx @@ -59,9 +59,15 @@ * the DERIVATION from the read site, which is the thing a re-key cannot hold * constant. * + * ## What objectui#8932 removed from this file + * + * The prop recorder for the `KanbanEnhanced` chunk, deleted with that module. + * No leg read it once `kanban-enhanced` retired: every probe below records the + * `KanbanImpl` chunk, so no assertion moved or went. + * * ## Suite 1 — runtime reachability, per registration * - * The two lazy board chunks are replaced by prop recorders; three spies are + * The lazy board chunk is replaced by a prop recorder; three spies are * authored on the schema; the question is which of them reach the board. * * - `KanbanRenderer` rendered directly (`../index.tsx`): all three arrive BY @@ -128,26 +134,19 @@ import { ObjectKanbanSchema as ObjectKanbanZod } from '@object-ui/types/zod'; import { KanbanRenderer } from '../index'; import '../index'; -/** Every props object either board implementation was rendered with, in order. */ +/** Every props object the board implementation was rendered with, in order. */ const recorded = vi.hoisted(() => ({ impl: [] as Array>, - enhanced: [] as Array>, })); -// Both lazy chunks are replaced by prop recorders: the question this file asks -// is what reaches the board's props, not what the board draws with them. +// The lazy board chunk is replaced by a prop recorder: the question this file +// asks is what reaches the board's props, not what the board draws with them. vi.mock('../KanbanImpl', () => ({ default: (props: Record) => { recorded.impl.push(props); return null; }, })); -vi.mock('../KanbanEnhanced', () => ({ - default: (props: Record) => { - recorded.enhanced.push(props); - return null; - }, -})); const STATIC_COLUMNS = [{ id: 'todo', title: 'To Do', cards: [{ id: '1', title: 'One' }] }]; @@ -156,7 +155,7 @@ function authored() { } /** Wait for one more board render to be recorded, then return the props it got. */ -async function lastBoardProps(log: 'impl' | 'enhanced', before: number, unmount: () => void) { +async function lastBoardProps(log: keyof typeof recorded, before: number, unmount: () => void) { await waitFor(() => expect(recorded[log].length).toBeGreaterThan(before)); const received = recorded[log][recorded[log].length - 1]; unmount(); @@ -171,7 +170,7 @@ async function lastBoardProps(log: 'impl' | 'enhanced', before: number, unmount: * lanes statically and nothing here fetches. (The prop is required and typed * `any`, so the value has to be spelled rather than omitted.) */ -async function boardPropsFor(type: string, log: 'impl' | 'enhanced', schemaKeys: Record) { +async function boardPropsFor(type: string, log: keyof typeof recorded, schemaKeys: Record) { const Renderer = ComponentRegistry.get(type) as React.ComponentType>; expect(Renderer, `\`${type}\` is not registered`).toBeDefined(); const before = recorded[log].length; diff --git a/packages/plugin-kanban/src/__tests__/kanbanEnhancedRetired-8932.test.ts b/packages/plugin-kanban/src/__tests__/kanbanEnhancedRetired-8932.test.ts new file mode 100644 index 0000000000..6b9d2027e2 --- /dev/null +++ b/packages/plugin-kanban/src/__tests__/kanbanEnhancedRetired-8932.test.ts @@ -0,0 +1,80 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#8932 — `KanbanEnhanced.tsx` is DELETED from `@object-ui/plugin-kanban`. + * + * The module was reachable through no entry of this package's `exports` map + * (`.` and `./style.css` only) and re-exported by no barrel, and once the + * `kanban-enhanced` registry key retired (objectui#8257) nothing but two test + * files referred to it. Ruled delete (2026-09-11, ratified by the maintainer + * 2026-09-24): published source that nothing can reach is removed, rather than + * given an export nobody asked for or left for the next reader to re-measure. + * ⛔ If a richer board is ever wanted, it comes back as a feature of the one + * registered board (`KanbanImpl`), not as a second implementation beside it. + * + * Two halves, each with a firing control on the same instrument: + * + * - the file is absent from `src/`, while `KanbanImpl.tsx` in the same + * directory reads present; + * - no file under `src/` names the retired module as a module specifier — a + * static or dynamic import, a re-export, a `require`, or a `vi.mock` / + * `vi.doMock` target — while the same pattern over the same walk finds the + * live board's lazy import in `index.tsx`. + * + * ⚠️ Mock targets count on purpose. A mock registered against a path that no + * longer exists fails nothing, so it would outlive the module silently; one of + * the two references this deletion found was exactly that shape. + */ + +import { describe, expect, it } from 'vitest'; +import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { dirname, join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** `packages/plugin-kanban/src` — one level up from `src/__tests__`. */ +const SRC_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); + +/** The deleted module, and the live board that is the control for it. */ +const RETIRED = 'KanbanEnhanced'; +const LIVE = 'KanbanImpl'; + +/** Every `.ts` / `.tsx` file under `src/`, recursively, as a path relative to `src/`. */ +function sourceFiles(dir: string = SRC_DIR): string[] { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const full = join(dir, entry.name); + if (entry.isDirectory()) return sourceFiles(full); + return /\.tsx?$/.test(entry.name) ? [relative(SRC_DIR, full)] : []; + }); +} + +/** The files under `src/` that name `/MODULE` (extension optional) as a module specifier. */ +function filesReferencing(moduleName: string): string[] { + const specifier = new RegExp( + String.raw`(?:\bfrom|\bimport|\brequire|\bvi\.(?:do)?[mM]ock)\s*\(?\s*['"][^'"]*/${moduleName}(?:\.tsx?)?['"]`, + ); + return sourceFiles() + .filter((rel) => specifier.test(readFileSync(join(SRC_DIR, rel), 'utf8'))) + .sort(); +} + +describe('@object-ui/plugin-kanban — KanbanEnhanced.tsx is deleted (objectui#8932)', () => { + it('the module file is gone from src/, while the live board beside it reads present', () => { + expect({ + retired: existsSync(join(SRC_DIR, `${RETIRED}.tsx`)), + live: existsSync(join(SRC_DIR, `${LIVE}.tsx`)), + }).toEqual({ retired: false, live: true }); + }); + + it('no file under src/ names the deleted module as a specifier, while the same pattern finds the live board', () => { + // Firing control first: the walk and the pattern are live, so the empty + // list below is a reading and not a probe that matches nothing. + expect(filesReferencing(LIVE)).toContain('index.tsx'); + expect(filesReferencing(RETIRED)).toEqual([]); + }); +}); diff --git a/packages/plugin-kanban/src/cardPredicateScope.test.tsx b/packages/plugin-kanban/src/cardPredicateScope.test.tsx index 5e311a603f..bab0fcfa08 100644 --- a/packages/plugin-kanban/src/cardPredicateScope.test.tsx +++ b/packages/plugin-kanban/src/cardPredicateScope.test.tsx @@ -13,13 +13,19 @@ * identical verdict on grid rows and kanban cards. Before this suite, kanban * evaluated cards without the ambient scope, so such a condition silently * never matched here while working on the grid. + * + * objectui#8932 deleted `KanbanEnhanced`, the second board this suite used to + * drive. Its positive leg went with it — the `KanbanBoard` leg pins the same + * verdict on the one registered board. The two legs that only ever ran on it + * now run on `KanbanBoard`: the fail-soft reading outside the provider, which + * is the negative control that makes the live positive leg a reading, and the + * objectui#5741 bare-field canon. */ import { describe, it, expect, afterEach } from 'vitest'; import * as React from 'react'; import { render, cleanup } from '@testing-library/react'; import { PredicateScopeProvider } from '@object-ui/react'; -import { KanbanEnhanced } from './KanbanEnhanced'; import KanbanBoard from './KanbanImpl'; // happy-dom may lack ResizeObserver (KanbanBoard's container-aware column @@ -51,15 +57,6 @@ function findByBg(container: HTMLElement, bg: string): HTMLElement | undefined { afterEach(cleanup); describe('kanban card conditional formatting · host predicate scope (ADR-0058)', () => { - it('KanbanEnhanced binds the ambient predicate scope alongside the card', () => { - const { container } = render( - - - , - ); - expect(findByBg(container, HOT_BG)).toBeTruthy(); - }); - it('KanbanBoard (impl) binds the ambient predicate scope alongside the card', () => { const { container } = render( @@ -71,7 +68,7 @@ describe('kanban card conditional formatting · host predicate scope (ADR-0058)' it('a scope-gated condition fails SOFT (no style) outside the provider', () => { const { container } = render( - , + , ); expect(findByBg(container, HOT_BG)).toBeUndefined(); }); @@ -82,11 +79,11 @@ describe('kanban card conditional formatting · host predicate scope (ADR-0058)' // to "no style". The canonical spelling on the same card is the control // that proves the rule was consulted at all. const bareRules = [{ condition: 'id == "c1"', style: { backgroundColor: HOT_BG } }] as any; - const bare = render(); + const bare = render(); expect(findByBg(bare.container, HOT_BG)).toBeUndefined(); cleanup(); const canonRules = [{ condition: 'record.id == "c1"', style: { backgroundColor: HOT_BG } }] as any; - const canon = render(); + const canon = render(); expect(findByBg(canon.container, HOT_BG)).toBeTruthy(); }); }); diff --git a/packages/plugin-kanban/src/index.tsx b/packages/plugin-kanban/src/index.tsx index 7e09f2d928..42af46a957 100644 --- a/packages/plugin-kanban/src/index.tsx +++ b/packages/plugin-kanban/src/index.tsx @@ -452,20 +452,25 @@ export const kanbanComponents = { * the same firing control (`object-grid`, 2 JSON / 128 TS) and silent control * (`zzz-not-a-type`, 0) the `kanban-ui` note above cites. * - * `KanbanEnhanced.tsx` itself is untouched on disk. ⛔ It is NOT, and never - * was, reachable from outside this package: `package.json` `exports` publishes - * exactly two entries — `.` and `./style.css` — and this barrel does not - * re-export the component, so `@object-ui/plugin-kanban/KanbanEnhanced` has - * never been a resolvable specifier for a consumer. (An earlier revision of - * this note claimed it was; that claim was wrong and is corrected here rather - * than deleted, because it is what a reader would otherwise copy.) What this - * card removes is the registry key and the `React.lazy` wrapper that existed - * only to serve it; what it leaves behind is a module with zero non-test - * importers — `cardPredicateScope.test.tsx` reaches it by relative path. - * ⛔ Deleting the file is a FURTHER narrowing of published source and needs its - * own maintainer ruling, which this card does not carry, so it stays. + * `KanbanEnhanced` was NOT, and never had been, reachable from outside this + * package: `package.json` `exports` publishes exactly two entries — `.` and + * `./style.css` — and this barrel never re-exported the component, so + * `@object-ui/plugin-kanban/KanbanEnhanced` was never a resolvable specifier + * for a consumer. (An earlier revision of this note claimed it was; that claim + * was wrong and is corrected here rather than deleted, because it is what a + * reader would otherwise copy.) What this card removed is the registry key and + * the `React.lazy` wrapper that existed only to serve it. It left the module + * itself on disk, with zero non-test importers, because deleting published + * source is a further narrowing that needed its own maintainer ruling. + * + * ⚠️ That ruling came: objectui#8932 (2026-09-11, ratified 2026-09-24) deleted + * `KanbanEnhanced.tsx`, the two test references it had left, and with them the + * `dist/KanbanEnhanced.d.ts` typings the package still emitted for it. (Through + * 17.6.0 the component itself was also bundled into `dist/index.js`, behind the + * `kanban-enhanced` key; it left the bundle with this card's retirement.) * - * Pinned in `src/__tests__/kanban-family-registry-keys-retired-8257.test.ts`. + * Pinned in `src/__tests__/kanban-family-registry-keys-retired-8257.test.ts` + * (the key) and `src/__tests__/kanbanEnhancedRetired-8932.test.ts` (the file). */ /** diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index 2e4964f0be..109c75cdf8 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -57,7 +57,8 @@ import type { DASHBOARD_SPEC_EXCLUDED } from './zod/complex.zod.js'; * Every member below was carried from the plugin's declaration verbatim. The * runtime-computed members (`cardSubtitle`, `cardFieldCells`, `coverImage`, * a badge's `colorStyle`) are what `ObjectKanban` writes onto the cards it - * hands the board and what `KanbanImpl` / `KanbanEnhanced` read back; the Zod + * hands the board and what `KanbanImpl` reads back (the one board since + * objectui#8932 deleted `KanbanEnhanced`); the Zod * mirror (`zod/complex.zod.ts`) passes those through the way it passes * `TableColumn.headerIcon` through (objectui#6424). * @@ -128,9 +129,10 @@ export interface KanbanColumn { /** * Cards in this column. * - * Named `cards` because that is what every board reads and every authored - * document writes: `KanbanImpl` (12 lines), `KanbanEnhanced` (8) and - * `bucketCardsIntoColumns` all read `column.cards`, and the two catalog + * Named `cards` because that is what the board reads and every authored + * document writes: `KanbanImpl` and `bucketCardsIntoColumns` read + * `column.cards` (measured at objectui#6939: 12 lines in `KanbanImpl`, and 8 + * in `KanbanEnhanced` until objectui#8932 deleted it), and the two catalog * entries, the plugin docs and `content/docs/api/schema-reference.md` all * author it. The retired declarative face spelled this `items` until * objectui#6939 — a spelling with zero read sites, which made every authored diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index efe2595f4a..dba0fbefd1 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -3725,8 +3725,9 @@ export interface ObjectKanbanSchema extends BaseSchema { * ## ⛔ Why this is NOT `KanbanColumn[]`, which is what the card proposed * * {@link KanbanColumn} requires `cards`. It is the RUNTIME lane shape — what - * `bucketCardsIntoColumns` produces and what `KanbanImpl` / `KanbanEnhanced` - * consume, with `cards` always filled — and using it as the AUTHORING + * `bucketCardsIntoColumns` produces and what `KanbanImpl` consumes (the one + * board since objectui#8932 deleted `KanbanEnhanced`), with `cards` always + * filled — and using it as the AUTHORING * element would refuse three live shapes, each measured rather than argued: * * 1. the protocol's own gate-validated example @@ -3747,11 +3748,12 @@ export interface ObjectKanbanSchema extends BaseSchema { * ## The member set is the READ set * * `id` / `title` / `cards` / `limit` / `className` / `collapsed` are exactly - * the lane members the two board implementations read, counted off - * `KanbanImpl`, `KanbanEnhanced`, `useCrossSwimlaneMove`, + * the lane members the board reads, counted when there were two board + * implementations off `KanbanImpl`, `KanbanEnhanced`, `useCrossSwimlaneMove`, * `useQuickAddReorder` and `bucketCardsIntoColumns`. Their VALUE types come * from those read sites too — see {@link id} for the one this card had to - * correct. + * correct. objectui#8932 later deleted `KanbanEnhanced`; `KanbanImpl` alone + * still reads all six members. * * ## ⭐ The bare-string arm is REACHABLE since objectui#8990 * diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d105086a6..2a0cccf07d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2204,9 +2204,6 @@ importers: '@object-ui/types': specifier: workspace:* version: link:../types - '@tanstack/react-virtual': - specifier: ^3.14.9 - version: 3.14.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) lucide-react: specifier: ^1.31.0 version: 1.35.0(react@19.2.8) diff --git a/scripts/__tests__/one-authority-per-exported-name-6273.test.ts b/scripts/__tests__/one-authority-per-exported-name-6273.test.ts index ef937e811a..0a8b7933d9 100644 --- a/scripts/__tests__/one-authority-per-exported-name-6273.test.ts +++ b/scripts/__tests__/one-authority-per-exported-name-6273.test.ts @@ -394,7 +394,9 @@ const KNOWN_COLLISIONS: ReadonlyMap = new Map([ // a derived declaration is still an authority. That contrast is why #6298 // took the re-export route rather than the derive route. ['ComponentMeta', ['packages/core/src/registry/Registry.ts', 'packages/types/src/base.ts']], - ['ConditionalFormattingRule', ['packages/plugin-kanban/src/KanbanEnhanced.tsx', 'packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/types/src/objectql.ts']], + // `ConditionalFormattingRule` had a THIRD site, `packages/plugin-kanban/src/KanbanEnhanced.tsx`, + // which left the tree when objectui#8932 deleted that module; the two below still collide. + ['ConditionalFormattingRule', ['packages/plugin-kanban/src/KanbanImpl.tsx', 'packages/types/src/objectql.ts']], ['ConfirmDialogState', ['packages/app-shell/src/views/ActionConfirmDialog.tsx', 'packages/plugin-designer/src/hooks/useConfirmDialog.ts']], ['ConnectionState', ['packages/collaboration/src/useRealtimeSubscription.ts', 'packages/data-objectstack/src/index.ts']], ['DataSource', ['packages/app-shell/src/types.ts', 'packages/types/src/data.ts']],