@entifix/react-controls 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/i18n/catalog/en.d.ts.map +1 -1
- package/dist/i18n/catalog/en.js +3 -0
- package/dist/i18n/catalog/en.js.map +1 -1
- package/dist/i18n/catalog/es.d.ts +4 -1
- package/dist/i18n/catalog/es.d.ts.map +1 -1
- package/dist/i18n/catalog/es.js +4 -1
- package/dist/i18n/catalog/es.js.map +1 -1
- package/dist/i18n/catalog/index.d.ts +6 -0
- package/dist/i18n/catalog/index.d.ts.map +1 -1
- package/dist/preferences/use-viewport-mode.d.ts +0 -1
- package/dist/preferences/use-viewport-mode.d.ts.map +1 -1
- package/dist/preferences/use-viewport-mode.js +0 -1
- package/dist/preferences/use-viewport-mode.js.map +1 -1
- package/dist/ui/actions/action-surfaces.d.ts +3 -3
- package/dist/ui/actions/action-surfaces.js +4 -4
- package/dist/ui/actions/action-surfaces.js.map +1 -1
- package/dist/ui/actions/render-link.d.ts +8 -4
- package/dist/ui/actions/render-link.d.ts.map +1 -1
- package/dist/ui/actions/render-link.js.map +1 -1
- package/dist/ui/atoms/field-control/field-control.d.ts +1 -1
- package/dist/ui/atoms/field-control/field-control.js +1 -1
- package/dist/ui/atoms/field-control/field-control.js.map +1 -1
- package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.d.ts +2 -2
- package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js.map +1 -1
- package/dist/ui/molecules/bulk-result/bulk-result.js.map +1 -1
- package/dist/ui/molecules/loading-boundary/loading-boundary.d.ts +1 -1
- package/dist/ui/molecules/loading-boundary/loading-boundary.js +1 -1
- package/dist/ui/molecules/loading-boundary/loading-boundary.js.map +1 -1
- package/dist/ui/molecules/pagination/index.d.ts +2 -0
- package/dist/ui/molecules/pagination/index.d.ts.map +1 -1
- package/dist/ui/molecules/pagination/index.js +2 -0
- package/dist/ui/molecules/pagination/index.js.map +1 -1
- package/dist/ui/molecules/pagination/page-math.d.ts +23 -0
- package/dist/ui/molecules/pagination/page-math.d.ts.map +1 -0
- package/dist/ui/molecules/pagination/page-math.js +47 -0
- package/dist/ui/molecules/pagination/page-math.js.map +1 -0
- package/dist/ui/molecules/pagination/pagination-links.d.ts +33 -0
- package/dist/ui/molecules/pagination/pagination-links.d.ts.map +1 -0
- package/dist/ui/molecules/pagination/pagination-links.js +94 -0
- package/dist/ui/molecules/pagination/pagination-links.js.map +1 -0
- package/dist/ui/molecules/pagination/pagination.d.ts +6 -1
- package/dist/ui/molecules/pagination/pagination.d.ts.map +1 -1
- package/dist/ui/molecules/pagination/pagination.js +8 -2
- package/dist/ui/molecules/pagination/pagination.js.map +1 -1
- package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.d.ts +2 -2
- package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js +2 -2
- package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.js +4 -4
- package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.types.d.ts +2 -2
- package/dist/ui/organisms/entity-form/entity-form.types.js.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table-tree.d.ts +33 -0
- package/dist/ui/organisms/entity-table/entity-table-tree.d.ts.map +1 -0
- package/dist/ui/organisms/entity-table/entity-table-tree.js +80 -0
- package/dist/ui/organisms/entity-table/entity-table-tree.js.map +1 -0
- package/dist/ui/organisms/entity-table/entity-table.d.ts.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table.js +41 -9
- package/dist/ui/organisms/entity-table/entity-table.js.map +1 -1
- package/dist/ui/organisms/wizard/wizard.d.ts +1 -2
- package/dist/ui/organisms/wizard/wizard.d.ts.map +1 -1
- package/dist/ui/organisms/wizard/wizard.js +2 -3
- package/dist/ui/organisms/wizard/wizard.js.map +1 -1
- package/dist/ui/organisms/wizard/wizard.types.d.ts +2 -2
- package/dist/ui/organisms/wizard/wizard.types.js.map +1 -1
- package/dist/ui/utils/active-section/active-section.d.ts +1 -1
- package/dist/ui/utils/active-section/active-section.js +1 -1
- package/dist/ui/utils/active-section/active-section.js.map +1 -1
- package/dist/ui/utils/keep-section/keep-section.d.ts +1 -1
- package/dist/ui/utils/keep-section/keep-section.js +1 -1
- package/dist/ui/utils/keep-section/keep-section.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/ui/organisms/entity-table/entity-table.tsx"],"sourcesContent":["'use client';\n\nimport {\n emptySelection,\n EntifixLogicError,\n type Entity,\n type EntityId,\n type EntitySelection,\n type EntitySorting,\n extractMetaEntity,\n type FilterGroup,\n isSelected,\n selectionSize,\n toggleSelected,\n type UseCaseDescriptor,\n} from '@entifix/core';\nimport { Fragment, type ReactNode, useRef, useState } from 'react';\n\nimport { useErrorMessage, useT, useTranslateKey } from '../../../i18n/index.js';\nimport {\n OVERFLOW_GLYPH,\n renderAnchor,\n useCasesForSurface,\n} from '../../actions/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { CellValue } from '../../atoms/cell-value/index.js';\nimport { Checkbox } from '../../atoms/field/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeaderCell,\n TableMessageRow,\n TableRow,\n} from '../../atoms/table/index.js';\nimport { linkClassName } from '../../atoms/text/index.js';\nimport { BulkActionBar } from '../../molecules/bulk-action-bar/index.js';\nimport { BulkResult } from '../../molecules/bulk-result/index.js';\nimport { ColumnSettings } from '../../molecules/column-settings/index.js';\nimport { ConfirmDialog } from '../../molecules/confirm-dialog/index.js';\nimport { EntityRecordCard } from '../../molecules/entity-record-card/index.js';\nimport { FilterBuilder } from '../../molecules/filter-builder/index.js';\nimport { LoadingBoundary } from '../../molecules/loading-boundary/index.js';\nimport { Menu } from '../../molecules/menu/index.js';\nimport { Pagination } from '../../molecules/pagination/index.js';\nimport { SortBuilder } from '../../molecules/sort-builder/index.js';\nimport { TableToolbar } from '../../molecules/table-toolbar/index.js';\nimport type {\n EntityTableColumn,\n EntityTablePivotBreakpoint,\n EntityTableProps,\n} from './entity-table.types.js';\nimport { readEntityTableSlots } from './entity-table-slots.js';\nimport { useEntityTableColumns } from './use-entity-table-columns.js';\n\n/**\n * The grid and the card list are both rendered, and CSS picks one. A JS\n * breakpoint hook would have to guess during SSR and correct after mount —\n * a hydration mismatch on every page load. Classes are written out in full\n * because Tailwind cannot see a class name that is concatenated at runtime.\n */\nconst PIVOT_CLASS: Record<\n EntityTablePivotBreakpoint,\n { grid: string; cards: string }\n> = {\n sm: { grid: 'hidden sm:block', cards: 'sm:hidden' },\n md: { grid: 'hidden md:block', cards: 'md:hidden' },\n lg: { grid: 'hidden lg:block', cards: 'lg:hidden' },\n};\n\ntype Panel = 'none' | 'filters' | 'sorting';\n\n/**\n * How many placeholder rows the built-in skeleton draws.\n *\n * The column count is taken from the real geometry, because that is the axis a\n * mismatch shifts; the row count is capped instead of tracking `pageSize`, since\n * a fifty-row page would otherwise paint fifty shimmer rows to stand in for\n * content that arrives below the fold anyway.\n */\nconst SKELETON_ROW_CAP = 5;\n\n/**\n * A table that builds itself from an entity's metadata: columns, labels, value\n * formatting and the filter/sort controls all come from `@accessor()`\n * declarations, so listing a new entity needs no bespoke table.\n *\n * Three things layer on top of that default:\n * - **personalization** — column order and visibility, persisted through the\n * UI-preferences port (so it survives reloads and can later move server-side);\n * - **responsiveness** — below `pivotBreakpoint` rows pivot into label/value\n * cards instead of scrolling sideways;\n * - **slots** — `<EntityColumn>` / `<EntityTableHeader>` / `<EntityTableRow>` /\n * `<EntityTableToolbar>` children override any part of the default rendering.\n */\n/**\n * What a pending verb will act on once it is confirmed.\n *\n * A form has one record; a table has a row or a selection, and those reach two\n * different handlers with two different payloads. Carrying the selection itself\n * rather than reading it back at confirm time also removes a branch that could\n * not happen: the bulk bar only renders inside a truthy selection.\n */\ntype PendingTarget<TEntity extends Entity> =\n | { kind: 'row'; item: TEntity }\n | { kind: 'selection'; selection: EntitySelection<TEntity> };\n\nexport function EntityTable<TEntity extends Entity>({\n entityConstructor,\n isLoading,\n skeleton = true,\n error,\n items,\n totalItems,\n currentPage,\n pageSize,\n onPageChange,\n onPageSizeChange,\n hrefFor,\n newHref,\n renderLink = renderAnchor,\n onSelect,\n preferencesKey,\n showControls = true,\n pivotBreakpoint = 'md',\n filtering,\n sorting,\n onFilteringChange,\n onSortingChange,\n selection,\n onSelectionChange,\n metadata,\n isMetadataLoading = false,\n onBulkUseCase,\n onUseCase,\n bulkOutcomes,\n onBulkDismiss,\n onBulkRetry,\n isBulkRunning = false,\n children,\n}: EntityTableProps<TEntity>) {\n const t = useT();\n // A descriptor's label is a runtime catalog key — the typed gate cannot see\n // it, which is why `@r10c/i18n-check` is what catches a typo.\n const translateKey = useTranslateKey();\n const errorMessage = useErrorMessage();\n const metaEntity = extractMetaEntity(entityConstructor);\n const slots = readEntityTableSlots<TEntity>(children);\n const [panel, setPanel] = useState<Panel>('none');\n\n /**\n * A verb waiting to be confirmed, and what it will act on.\n *\n * ⚠️ **The table had no confirmation flow at all** until #216 moved a\n * destructive verb onto it. It read `confirm.tone` — only to pick a tone for\n * a button or a menu item — and then fired on the first click. `unpublish` is\n * declared `destructive`, so a verb reachable from a row menu without this\n * would have been a one-click unpublish with a red label explaining what had\n * already happened.\n *\n * The state holds the confirmation itself rather than the descriptor, so the\n * dialog cannot open for a verb that never asked for one — the same shape\n * `EntityForm` uses, and for the same reason.\n *\n * It carries the **target** too, which the form's does not need: a form has\n * one record and a table has a row or a selection, and those run through two\n * different handlers with two different payloads.\n */\n const [pending, setPending] = useState<\n | {\n key: string;\n labelKey: string;\n confirm: NonNullable<UseCaseDescriptor['confirm']>;\n target: PendingTarget<TEntity>;\n }\n | undefined\n >(undefined);\n\n const { columns, visibleColumns, hidden, setPersonalization } =\n useEntityTableColumns<TEntity>(\n entityConstructor,\n slots.columns,\n items[0],\n `entity-table:${preferencesKey ?? metaEntity.key ?? metaEntity.name}`,\n );\n\n const pivot = PIVOT_CLASS[pivotBreakpoint];\n\n // A picker's table is choosing *one* value; a multi-selection there would\n // offer a set to a form member that holds a single reference, and the two\n // controls would fight over the same row. This is a wiring mistake rather\n // than a state, so it costs the render — the same call `EntityForm` makes\n // for a link source aimed at a member no picker can edit.\n if (onSelect !== undefined && selection !== undefined) {\n throw new EntifixLogicError(\n 'EntityTable was given both `onSelect` and `selection`. A picker picks one ' +\n 'row and a selection accumulates many, so the row control cannot be both. ' +\n 'Drop `selection` inside a picker, or drop `onSelect` outside one.',\n );\n }\n\n /**\n * The selection, or `undefined` when the table is not selectable at all.\n *\n * One value rather than a `selection` prop beside a `selectable` boolean:\n * two of them means every render site has to re-test both, and the second\n * test is unreachable — which reads as a defensive check and is really dead\n * code the coverage gate then cannot close.\n */\n const selectionState =\n selection !== undefined && onSelectionChange !== undefined\n ? selection\n : undefined;\n const selectable = selectionState !== undefined;\n const rowUseCases = useCasesForSurface('row-menu', metadata?.useCases);\n const bulkUseCases = useCasesForSurface('bulk-bar', metadata?.useCases);\n const toolbarUseCases = useCasesForSurface(\n 'table-toolbar',\n metadata?.useCases,\n );\n const hasRowMenu = rowUseCases.length > 0 && onUseCase !== undefined;\n\n /**\n * What a `context-independent` collection verb acts on: everything the\n * current filter matches.\n *\n * Not an empty `ids` selection, which would say \"no rows\" — the verb is\n * available *without* a selection precisely because its subject is the\n * collection, so the honest payload is the same filter the listing is\n * showing, and the same one a \"select all matching\" escalation would send.\n */\n const everythingMatching: EntitySelection<TEntity> = {\n mode: 'matching',\n filtering,\n total: totalItems,\n excluded: new Set<EntityId>(),\n };\n\n const hasRowAction =\n onSelect !== undefined || hrefFor !== undefined || hasRowMenu;\n const columnCount =\n visibleColumns.length + (hasRowAction ? 1 : 0) + (selectable ? 1 : 0);\n\n /**\n * The first load has nothing to show and is held with a skeleton; a refetch\n * already has rows, so it keeps them and only marks the region busy. Splitting\n * the two is what stops every pagination click flashing grey.\n */\n const isFirstLoad = isLoading && items.length === 0;\n const isRefetching = isLoading && items.length > 0;\n\n /** `true`/omitted → the built-in default; a node → that node; `false` → none. */\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n const showSkeleton = isFirstLoad && skeleton !== false;\n const skeletonRows = Array.from(\n { length: Math.min(pageSize, SKELETON_ROW_CAP) },\n (_, index) => index,\n );\n\n /**\n * The refetch hint, and it is deliberately the only one: rows stay readable\n * and simply dim, because a second skeleton over live data would be two\n * loading signals in one region.\n */\n const busyClass = isRefetching\n ? 'opacity-60 transition-opacity duration-200 ease-smooth'\n : '';\n\n const renderCell = (\n column: EntityTableColumn<TEntity>,\n item: TEntity,\n ): ReactNode =>\n column.render ? (\n column.render(item)\n ) : (\n <CellValue\n value={(item as Record<string, unknown>)[column.name]}\n descriptor={column}\n />\n );\n\n /**\n * The row a shift-click ranges *from* — the last box the user actually\n * toggled. Held in a ref rather than state because it changes nothing on\n * screen, and re-rendering the whole table on every click to remember an\n * anchor is a cost with no visible return.\n */\n const anchorIndex = useRef<number | undefined>(undefined);\n\n const setSelection = (next: EntitySelection<TEntity>) =>\n onSelectionChange?.(next);\n\n const toggleRow = (\n index: number,\n checked: boolean,\n shiftKey: boolean,\n current: EntitySelection<TEntity>,\n ) => {\n // A shift-click extends the last toggle across the rows *on this page*, in\n // the direction the user dragged, and applies the new state to all of them.\n const from = shiftKey ? (anchorIndex.current ?? index) : index;\n const [start, end] = from <= index ? [from, index] : [index, from];\n\n let next = current;\n for (const row of items.slice(start, end + 1)) {\n next = toggleSelected(next, row.id, checked);\n }\n\n anchorIndex.current = index;\n setSelection(next);\n };\n\n /**\n * The header box, and it has three states rather than two: none of this\n * page's rows selected, all of them, or some. Without the third the box\n * reads as \"nothing is selected\" while a bulk bar sits below it saying\n * otherwise.\n */\n const pageIds = items.map(item => item.id);\n const selectedOnPage = selectionState\n ? pageIds.filter(id => isSelected(selectionState, id)).length\n : 0;\n const allOnPageSelected =\n pageIds.length > 0 && selectedOnPage === pageIds.length;\n const someOnPageSelected = selectedOnPage > 0 && !allOnPageSelected;\n\n const toggleAllOnPage = (\n checked: boolean,\n current: EntitySelection<TEntity>,\n ) => {\n let next = current;\n for (const id of pageIds) next = toggleSelected(next, id, checked);\n anchorIndex.current = undefined;\n setSelection(next);\n };\n\n /**\n * How a row names itself to a screen reader.\n *\n * The first visible column's value, because that is what the user reads the\n * row *as* — \"Seleccionar Acme\" rather than \"Seleccionar fila 3\", which tells\n * them the position they can already see and not the thing they cannot.\n */\n const rowLabel = (item: TEntity): string => {\n // The first non-identifier column **that this row actually filled in**.\n //\n // An id names a row the way \"row 3\" does: it repeats what the user can\n // already see. But skipping only the id is not enough — measured against\n // the seeded catalog, where `code` is the first non-id column and is empty\n // on every brand, so the label fell straight through to the id and never\n // reached `name`. A column that is blank on this row names it no better\n // than its position does, so the search continues past it.\n //\n // The id remains the last resort, because a row with nothing rendered\n // still has to announce itself.\n const named = visibleColumns.find(column => {\n if (column.type === 'id') return false;\n const value = (item as Record<string, unknown>)[column.name];\n return value != null && String(value) !== '';\n });\n\n return named\n ? String((item as Record<string, unknown>)[named.name])\n : String(item.id);\n };\n\n const selectionCell = (\n item: TEntity,\n index: number,\n // Passed in rather than closed over: this is only ever called under\n // `selectable`, which already proves the selection exists, and reading the\n // optional prop here would add a fallback arm that nothing can reach.\n current: EntitySelection<TEntity>,\n ) => (\n <Checkbox\n // Deliberately no `id`: both pivots are always in the DOM, so an id here\n // would be emitted twice per row. `aria-label` names the control instead,\n // and it names the *record* rather than the position.\n aria-label={t('table.selectRow', { record: rowLabel(item) })}\n checked={isSelected(current, item.id)}\n disabled={isBulkRunning}\n onChange={event =>\n toggleRow(\n index,\n event.currentTarget.checked,\n (event.nativeEvent as MouseEvent).shiftKey === true,\n current,\n )\n }\n />\n );\n\n const runTarget = (key: string, target: PendingTarget<TEntity>) => {\n if (target.kind === 'row') onUseCase?.(key, target.item);\n else onBulkUseCase?.(key, target.selection);\n };\n\n const invoke = (\n useCase: UseCaseDescriptor,\n target: PendingTarget<TEntity>,\n ) => {\n const confirm = useCase.confirm;\n if (confirm) {\n setPending({\n key: useCase.key,\n labelKey: useCase.labelKey,\n confirm,\n target,\n });\n return;\n }\n runTarget(useCase.key, target);\n };\n\n const rowMenu = (item: TEntity) =>\n hasRowMenu ? (\n <Menu>\n <Menu.Trigger aria-label={t('table.actions')}>\n {OVERFLOW_GLYPH}\n </Menu.Trigger>\n <Menu.Items>\n {rowUseCases.map(useCase => (\n <Menu.Item\n key={useCase.key}\n tone={\n useCase.confirm?.tone === 'destructive'\n ? 'destructive'\n : 'neutral'\n }\n onClick={() => invoke(useCase, { kind: 'row', item })}\n >\n {translateKey(useCase.labelKey)}\n </Menu.Item>\n ))}\n </Menu.Items>\n </Menu>\n ) : undefined;\n\n /**\n * Selection wins over navigation: a picker's row must set the value, not send\n * the user away from the form they came from.\n *\n * The row menu sits *beside* whichever of the two applies rather than\n * replacing it — \"open this record\" and \"run a verb on it\" are both true at\n * once, and folding the first into the menu would bury the action every row\n * of a list exists for.\n */\n const recordAction = (item: TEntity) => {\n const primary = onSelect ? (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => onSelect(item)}\n >\n {t('table.select')}\n </Button>\n ) : hrefFor ? (\n renderLink({\n href: hrefFor(item.id),\n className: linkClassName,\n children: t('table.open'),\n intent: { kind: 'open', id: item.id },\n })\n ) : undefined;\n\n const menu = rowMenu(item);\n if (!menu) return primary;\n\n return (\n <span className=\"flex items-center gap-2xs\">\n {primary}\n {menu}\n </span>\n );\n };\n\n /**\n * \"No records\" is a statement about the data; after a failed load it would be\n * a lie about it. The distinction matters most to the user who is deciding\n * whether their catalog is empty or their backend is down.\n */\n const emptyMessage = error ? t('table.error') : t('table.empty');\n\n return (\n <div className=\"flex flex-col gap-s\">\n {/*\n One announcement for the whole table, not one per layout. Both the grid\n and the card list are always in the DOM — CSS picks which is visible —\n so a live region inside each meant assistive tech was told twice that\n the table was loading. The shimmer itself is aria-hidden, so this is the\n only thing that carries the news.\n */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {isFirstLoad ? t('table.loading') : ''}\n </span>\n\n {showControls && (\n <TableToolbar\n start={\n <>\n {newHref &&\n renderLink({\n href: newHref,\n className: linkClassName,\n children: t('table.new'),\n intent: { kind: 'new' },\n })}\n {/* Collection-bound verbs that need no selection. They sit with\n `New` rather than in the bulk bar, because the bulk bar only\n exists while rows are ticked and these are always available. */}\n <LoadingBoundary isLoading={isMetadataLoading} lines={0}>\n <>\n {toolbarUseCases.map(useCase => (\n <Button\n key={useCase.key}\n type=\"button\"\n variant={\n useCase.confirm?.tone === 'destructive'\n ? 'destructive'\n : 'secondary'\n }\n size=\"sm\"\n disabled={isBulkRunning}\n onClick={() =>\n onBulkUseCase?.(useCase.key, everythingMatching)\n }\n >\n {translateKey(useCase.labelKey)}\n </Button>\n ))}\n </>\n </LoadingBoundary>\n {slots.toolbar}\n </>\n }\n end={\n <>\n <Button\n type=\"button\"\n variant={panel === 'filters' ? 'primary' : 'secondary'}\n size=\"sm\"\n onClick={() =>\n setPanel(current =>\n current === 'filters' ? 'none' : 'filters',\n )\n }\n >\n {t('table.filters')}\n </Button>\n <Button\n type=\"button\"\n variant={panel === 'sorting' ? 'primary' : 'secondary'}\n size=\"sm\"\n onClick={() =>\n setPanel(current =>\n current === 'sorting' ? 'none' : 'sorting',\n )\n }\n >\n {t('table.sorting')}\n </Button>\n <ColumnSettings\n columns={columns}\n hidden={hidden}\n onChange={setPersonalization}\n />\n </>\n }\n panel={\n panel === 'filters' ? (\n <FilterBuilder<TEntity>\n descriptors={columns.filter(column => column.filterable)}\n value={filtering}\n onChange={(applied: FilterGroup<TEntity>) =>\n onFilteringChange?.(applied)\n }\n />\n ) : panel === 'sorting' ? (\n <SortBuilder<TEntity>\n descriptors={columns.filter(column => column.sortable)}\n value={sorting}\n onChange={(applied: EntitySorting<TEntity>) =>\n onSortingChange?.(applied)\n }\n />\n ) : undefined\n }\n />\n )}\n\n {/*\n The bar appears when there is something selected and something to do\n with it. It sits above the rows rather than floating over them: a fixed\n overlay covers the last row of a short page, which is the row the user\n most likely just ticked.\n */}\n {selectionState && selectionSize(selectionState) > 0 && (\n <BulkActionBar\n count={selectionSize(selectionState)}\n useCases={bulkUseCases}\n busy={isBulkRunning}\n // Offered only when this page is fully ticked and the store holds\n // more than the page — the escalation has somewhere to escalate to.\n matchingTotal={\n selectionState.mode === 'ids' &&\n allOnPageSelected &&\n totalItems > items.length\n ? totalItems\n : undefined\n }\n onSelectAllMatching={() => setSelection(everythingMatching)}\n onClear={() => setSelection(emptySelection<TEntity>())}\n onUseCase={useCase =>\n invoke(useCase, { kind: 'selection', selection: selectionState })\n }\n />\n )}\n\n {pending && (\n <ConfirmDialog\n open\n tone={pending.confirm.tone}\n title={translateKey(pending.labelKey)}\n message={translateKey(pending.confirm.messageKey)}\n busy={isBulkRunning}\n onCancel={() => setPending(undefined)}\n onConfirm={() => {\n const { key, target } = pending;\n setPending(undefined);\n runTarget(key, target);\n }}\n />\n )}\n\n {bulkOutcomes && bulkOutcomes.length > 0 && onBulkDismiss && (\n <BulkResult\n outcomes={bulkOutcomes}\n busy={isBulkRunning}\n labelFor={id => {\n const row = items.find(item => item.id === id);\n return row ? rowLabel(row) : undefined;\n }}\n onRetryFailed={onBulkRetry}\n onDismiss={onBulkDismiss}\n />\n )}\n\n {/*\n Rendered once, outside both layouts, and independently of whether there\n are rows: a refetch that fails leaves the previous page on screen, and\n without this the user would read stale data as current.\n */}\n {error && (\n <p\n role=\"alert\"\n data-testid=\"entity-table-error\"\n className=\"rounded-sm border border-danger bg-danger-subtle px-s py-2xs text-step-sm text-danger\"\n >\n {errorMessage(error)}\n </p>\n )}\n\n {/* Wide viewports: a grid. */}\n <div className={`${pivot.grid} ${busyClass}`}>\n <Table>\n <TableHead>\n {slots.header ? (\n slots.header.render(visibleColumns)\n ) : (\n <tr>\n {selectable && (\n <TableHeaderCell>\n <Checkbox\n aria-label={t('table.selectAllOnPage')}\n checked={allOnPageSelected}\n indeterminate={someOnPageSelected}\n disabled={isBulkRunning || items.length === 0}\n onChange={event =>\n toggleAllOnPage(\n event.currentTarget.checked,\n selectionState,\n )\n }\n />\n </TableHeaderCell>\n )}\n {visibleColumns.map(column => (\n <TableHeaderCell key={column.name}>\n {column.header ?? column.label}\n </TableHeaderCell>\n ))}\n {hasRowAction && (\n <TableHeaderCell>{t('table.actions')}</TableHeaderCell>\n )}\n </tr>\n )}\n </TableHead>\n <TableBody aria-busy={isLoading || undefined}>\n {/* Skeleton rows rather than the word \"Loading\", and one per column\n rather than a single full-width blob: the placeholder occupies\n the real grid, so the swap to rows shifts nothing sideways.\n `TableMessageRow` is deliberately bypassed — it is one centred\n full-span cell and cannot express the geometry. */}\n {showSkeleton &&\n (customSkeleton ? (\n <TableMessageRow colSpan={columnCount}>\n {customSkeleton}\n </TableMessageRow>\n ) : (\n skeletonRows.map(row => (\n <TableRow key={row}>\n {selectable && (\n <TableCell>\n <Skeleton shape=\"line\" className=\"w-4\" />\n </TableCell>\n )}\n {visibleColumns.map(column => (\n <TableCell key={column.name}>\n <Skeleton shape=\"line\" className=\"w-full\" />\n </TableCell>\n ))}\n {hasRowAction && (\n <TableCell>\n <Skeleton shape=\"line\" className=\"w-16\" />\n </TableCell>\n )}\n </TableRow>\n ))\n ))}\n {!isLoading && items.length === 0 && (\n <TableMessageRow colSpan={columnCount}>\n {emptyMessage}\n </TableMessageRow>\n )}\n {items.map((item, index) =>\n slots.row ? (\n // A Fragment carries the key without wrapping the custom row in\n // an element `<tbody>` would reject.\n <Fragment key={String(item.id ?? index)}>\n {slots.row.render(item, visibleColumns)}\n </Fragment>\n ) : (\n <TableRow key={String(item.id ?? index)}>\n {selectionState && (\n <TableCell>\n {selectionCell(item, index, selectionState)}\n </TableCell>\n )}\n {visibleColumns.map(column => (\n <TableCell key={column.name}>\n {renderCell(column, item)}\n </TableCell>\n ))}\n {hasRowAction && <TableCell>{recordAction(item)}</TableCell>}\n </TableRow>\n ),\n )}\n </TableBody>\n </Table>\n </div>\n\n {/* Narrow viewports: the same columns pivoted into cards. */}\n <div\n className={`${pivot.cards} flex flex-col gap-2xs ${busyClass}`}\n aria-busy={isLoading || undefined}\n >\n {/* The card pivot gets card-shaped placeholders, not a bare shimmer\n block: the border, radius and padding are the geometry here. */}\n {showSkeleton &&\n (customSkeleton ??\n skeletonRows.map(row => (\n <EntityRecordCard\n key={row}\n columns={visibleColumns}\n renderCell={() => <Skeleton shape=\"line\" className=\"w-24\" />}\n />\n )))}\n {!isLoading && items.length === 0 && (\n <p className=\"text-step-sm text-content-muted\">{emptyMessage}</p>\n )}\n {items.map((item, index) => (\n <EntityRecordCard\n key={String(item.id ?? index)}\n columns={visibleColumns}\n // The card is handed the resolved columns themselves, so the\n // narrow layout goes through the exact same renderers as the grid.\n renderCell={column =>\n renderCell(column as EntityTableColumn<TEntity>, item)\n }\n leading={\n selectionState\n ? selectionCell(item, index, selectionState)\n : undefined\n }\n actions={recordAction(item)}\n />\n ))}\n </div>\n\n <Pagination\n currentPage={currentPage}\n pageSize={pageSize}\n totalItems={totalItems}\n onPageChange={onPageChange}\n onPageSizeChange={onPageSizeChange}\n />\n\n {slots.rest}\n </div>\n );\n}\n"],"names":["emptySelection","EntifixLogicError","extractMetaEntity","isSelected","selectionSize","toggleSelected","Fragment","useRef","useState","useErrorMessage","useT","useTranslateKey","OVERFLOW_GLYPH","renderAnchor","useCasesForSurface","Button","CellValue","Checkbox","Skeleton","Table","TableBody","TableCell","TableHead","TableHeaderCell","TableMessageRow","TableRow","linkClassName","BulkActionBar","BulkResult","ColumnSettings","ConfirmDialog","EntityRecordCard","FilterBuilder","LoadingBoundary","Menu","Pagination","SortBuilder","TableToolbar","readEntityTableSlots","useEntityTableColumns","PIVOT_CLASS","sm","grid","cards","md","lg","SKELETON_ROW_CAP","EntityTable","entityConstructor","isLoading","skeleton","error","items","totalItems","currentPage","pageSize","onPageChange","onPageSizeChange","hrefFor","newHref","renderLink","onSelect","preferencesKey","showControls","pivotBreakpoint","filtering","sorting","onFilteringChange","onSortingChange","selection","onSelectionChange","metadata","isMetadataLoading","onBulkUseCase","onUseCase","bulkOutcomes","onBulkDismiss","onBulkRetry","isBulkRunning","children","t","translateKey","errorMessage","metaEntity","slots","panel","setPanel","pending","setPending","undefined","columns","visibleColumns","hidden","setPersonalization","key","name","pivot","selectionState","selectable","rowUseCases","useCases","bulkUseCases","toolbarUseCases","hasRowMenu","length","everythingMatching","mode","total","excluded","Set","hasRowAction","columnCount","isFirstLoad","isRefetching","customSkeleton","showSkeleton","skeletonRows","Array","from","Math","min","_","index","busyClass","renderCell","column","item","render","value","descriptor","anchorIndex","setSelection","next","toggleRow","checked","shiftKey","current","start","end","row","slice","id","pageIds","map","selectedOnPage","filter","allOnPageSelected","someOnPageSelected","toggleAllOnPage","rowLabel","named","find","type","String","selectionCell","aria-label","record","disabled","onChange","event","currentTarget","nativeEvent","runTarget","target","kind","invoke","useCase","confirm","labelKey","rowMenu","Trigger","Items","Item","tone","onClick","recordAction","primary","variant","size","href","className","intent","menu","span","emptyMessage","div","role","aria-live","lines","toolbar","descriptors","filterable","applied","sortable","count","busy","matchingTotal","onSelectAllMatching","onClear","open","title","message","messageKey","onCancel","onConfirm","outcomes","labelFor","onRetryFailed","onDismiss","p","data-testid","header","tr","indeterminate","label","aria-busy","colSpan","shape","leading","actions","rest"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/ui/organisms/entity-table/entity-table.tsx"],"sourcesContent":["'use client';\n\nimport {\n emptySelection,\n EntifixLogicError,\n type Entity,\n type EntityId,\n type EntitySelection,\n type EntitySorting,\n extractMetaEntity,\n type FilterGroup,\n hierarchyOf,\n isSelected,\n selectionSize,\n toggleSelected,\n type UseCaseDescriptor,\n} from '@entifix/core';\nimport { Fragment, type ReactNode, useMemo, useRef, useState } from 'react';\n\nimport { useErrorMessage, useT, useTranslateKey } from '../../../i18n/index.js';\nimport {\n OVERFLOW_GLYPH,\n renderAnchor,\n useCasesForSurface,\n} from '../../actions/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { CellValue } from '../../atoms/cell-value/index.js';\nimport { Checkbox } from '../../atoms/field/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeaderCell,\n TableMessageRow,\n TableRow,\n} from '../../atoms/table/index.js';\nimport { linkClassName } from '../../atoms/text/index.js';\nimport { BulkActionBar } from '../../molecules/bulk-action-bar/index.js';\nimport { BulkResult } from '../../molecules/bulk-result/index.js';\nimport { ColumnSettings } from '../../molecules/column-settings/index.js';\nimport { ConfirmDialog } from '../../molecules/confirm-dialog/index.js';\nimport { EntityRecordCard } from '../../molecules/entity-record-card/index.js';\nimport { FilterBuilder } from '../../molecules/filter-builder/index.js';\nimport { LoadingBoundary } from '../../molecules/loading-boundary/index.js';\nimport { Menu } from '../../molecules/menu/index.js';\nimport { Pagination } from '../../molecules/pagination/index.js';\nimport { SortBuilder } from '../../molecules/sort-builder/index.js';\nimport { TableToolbar } from '../../molecules/table-toolbar/index.js';\nimport type {\n EntityTableColumn,\n EntityTablePivotBreakpoint,\n EntityTableProps,\n} from './entity-table.types.js';\nimport { readEntityTableSlots } from './entity-table-slots.js';\nimport { shownTreeRows, TreeCell, treeRows } from './entity-table-tree.js';\nimport { useEntityTableColumns } from './use-entity-table-columns.js';\n\n/**\n * The grid and the card list are both rendered, and CSS picks one. A JS\n * breakpoint hook would have to guess during SSR and correct after mount —\n * a hydration mismatch on every page load. Classes are written out in full\n * because Tailwind cannot see a class name that is concatenated at runtime.\n */\nconst PIVOT_CLASS: Record<\n EntityTablePivotBreakpoint,\n { grid: string; cards: string }\n> = {\n sm: { grid: 'hidden sm:block', cards: 'sm:hidden' },\n md: { grid: 'hidden md:block', cards: 'md:hidden' },\n lg: { grid: 'hidden lg:block', cards: 'lg:hidden' },\n};\n\ntype Panel = 'none' | 'filters' | 'sorting';\n\n/**\n * How many placeholder rows the built-in skeleton draws.\n *\n * The column count is taken from the real geometry, because that is the axis a\n * mismatch shifts; the row count is capped instead of tracking `pageSize`, since\n * a fifty-row page would otherwise paint fifty shimmer rows to stand in for\n * content that arrives below the fold anyway.\n */\nconst SKELETON_ROW_CAP = 5;\n\n/**\n * A table that builds itself from an entity's metadata: columns, labels, value\n * formatting and the filter/sort controls all come from `@accessor()`\n * declarations, so listing a new entity needs no bespoke table.\n *\n * Three things layer on top of that default:\n * - **personalization** — column order and visibility, persisted through the\n * UI-preferences port (so it survives reloads and can later move server-side);\n * - **responsiveness** — below `pivotBreakpoint` rows pivot into label/value\n * cards instead of scrolling sideways;\n * - **slots** — `<EntityColumn>` / `<EntityTableHeader>` / `<EntityTableRow>` /\n * `<EntityTableToolbar>` children override any part of the default rendering.\n */\n/**\n * What a pending verb will act on once it is confirmed.\n *\n * A form has one record; a table has a row or a selection, and those reach two\n * different handlers with two different payloads. Carrying the selection itself\n * rather than reading it back at confirm time also removes a branch that could\n * not happen: the bulk bar only renders inside a truthy selection.\n */\ntype PendingTarget<TEntity extends Entity> =\n | { kind: 'row'; item: TEntity }\n | { kind: 'selection'; selection: EntitySelection<TEntity> };\n\nexport function EntityTable<TEntity extends Entity>({\n entityConstructor,\n isLoading,\n skeleton = true,\n error,\n items,\n totalItems,\n currentPage,\n pageSize,\n onPageChange,\n onPageSizeChange,\n hrefFor,\n newHref,\n renderLink = renderAnchor,\n onSelect,\n preferencesKey,\n showControls = true,\n pivotBreakpoint = 'md',\n filtering,\n sorting,\n onFilteringChange,\n onSortingChange,\n selection,\n onSelectionChange,\n metadata,\n isMetadataLoading = false,\n onBulkUseCase,\n onUseCase,\n bulkOutcomes,\n onBulkDismiss,\n onBulkRetry,\n isBulkRunning = false,\n children,\n}: EntityTableProps<TEntity>) {\n const t = useT();\n // A descriptor's label is a runtime catalog key — the typed gate cannot see\n // it, which is why a host's catalog check is what catches a typo.\n const translateKey = useTranslateKey();\n const errorMessage = useErrorMessage();\n const metaEntity = extractMetaEntity(entityConstructor);\n const slots = readEntityTableSlots<TEntity>(children);\n const [panel, setPanel] = useState<Panel>('none');\n\n /**\n * A verb waiting to be confirmed, and what it will act on.\n *\n * ⚠️ **The table had no confirmation flow at all** until a change moved a\n * destructive verb onto it. It read `confirm.tone` — only to pick a tone for\n * a button or a menu item — and then fired on the first click. `unpublish` is\n * declared `destructive`, so a verb reachable from a row menu without this\n * would have been a one-click unpublish with a red label explaining what had\n * already happened.\n *\n * The state holds the confirmation itself rather than the descriptor, so the\n * dialog cannot open for a verb that never asked for one — the same shape\n * `EntityForm` uses, and for the same reason.\n *\n * It carries the **target** too, which the form's does not need: a form has\n * one record and a table has a row or a selection, and those run through two\n * different handlers with two different payloads.\n */\n const [pending, setPending] = useState<\n | {\n key: string;\n labelKey: string;\n confirm: NonNullable<UseCaseDescriptor['confirm']>;\n target: PendingTarget<TEntity>;\n }\n | undefined\n >(undefined);\n\n const { columns, visibleColumns, hidden, setPersonalization } =\n useEntityTableColumns<TEntity>(\n entityConstructor,\n slots.columns,\n items[0],\n `entity-table:${preferencesKey ?? metaEntity.key ?? metaEntity.name}`,\n );\n\n const pivot = PIVOT_CLASS[pivotBreakpoint];\n\n // A picker's table is choosing *one* value; a multi-selection there would\n // offer a set to a form member that holds a single reference, and the two\n // controls would fight over the same row. This is a wiring mistake rather\n // than a state, so it costs the render — the same call `EntityForm` makes\n // for a link source aimed at a member no picker can edit.\n if (onSelect !== undefined && selection !== undefined) {\n throw new EntifixLogicError(\n 'EntityTable was given both `onSelect` and `selection`. A picker picks one ' +\n 'row and a selection accumulates many, so the row control cannot be both. ' +\n 'Drop `selection` inside a picker, or drop `onSelect` outside one.',\n );\n }\n\n /**\n * The selection, or `undefined` when the table is not selectable at all.\n *\n * One value rather than a `selection` prop beside a `selectable` boolean:\n * two of them means every render site has to re-test both, and the second\n * test is unreachable — which reads as a defensive check and is really dead\n * code the coverage gate then cannot close.\n */\n const selectionState =\n selection !== undefined && onSelectionChange !== undefined\n ? selection\n : undefined;\n const selectable = selectionState !== undefined;\n const rowUseCases = useCasesForSurface('row-menu', metadata?.useCases);\n const bulkUseCases = useCasesForSurface('bulk-bar', metadata?.useCases);\n const toolbarUseCases = useCasesForSurface(\n 'table-toolbar',\n metadata?.useCases,\n );\n const hasRowMenu = rowUseCases.length > 0 && onUseCase !== undefined;\n\n /**\n * What a `context-independent` collection verb acts on: everything the\n * current filter matches.\n *\n * Not an empty `ids` selection, which would say \"no rows\" — the verb is\n * available *without* a selection precisely because its subject is the\n * collection, so the honest payload is the same filter the listing is\n * showing, and the same one a \"select all matching\" escalation would send.\n */\n const everythingMatching: EntitySelection<TEntity> = {\n mode: 'matching',\n filtering,\n total: totalItems,\n excluded: new Set<EntityId>(),\n };\n\n const hasRowAction =\n onSelect !== undefined || hrefFor !== undefined || hasRowMenu;\n const columnCount =\n visibleColumns.length + (hasRowAction ? 1 : 0) + (selectable ? 1 : 0);\n\n /**\n * The first load has nothing to show and is held with a skeleton; a refetch\n * already has rows, so it keeps them and only marks the region busy. Splitting\n * the two is what stops every pagination click flashing grey.\n */\n const isFirstLoad = isLoading && items.length === 0;\n const isRefetching = isLoading && items.length > 0;\n\n /** `true`/omitted → the built-in default; a node → that node; `false` → none. */\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n const showSkeleton = isFirstLoad && skeleton !== false;\n const skeletonRows = Array.from(\n { length: Math.min(pageSize, SKELETON_ROW_CAP) },\n (_, index) => index,\n );\n\n /**\n * The refetch hint, and it is deliberately the only one: rows stay readable\n * and simply dim, because a second skeleton over live data would be two\n * loading signals in one region.\n */\n const busyClass = isRefetching\n ? 'opacity-60 transition-opacity duration-200 ease-smooth'\n : '';\n\n const renderCell = (\n column: EntityTableColumn<TEntity>,\n item: TEntity,\n ): ReactNode =>\n column.render ? (\n column.render(item)\n ) : (\n <CellValue\n value={(item as Record<string, unknown>)[column.name]}\n descriptor={column}\n />\n );\n\n /**\n * The row a shift-click ranges *from* — the last box the user actually\n * toggled. Held in a ref rather than state because it changes nothing on\n * screen, and re-rendering the whole table on every click to remember an\n * anchor is a cost with no visible return.\n */\n const anchorIndex = useRef<number | undefined>(undefined);\n\n /**\n * An entity that declares a parent renders as a tree: the page arranged\n * depth first, its first column indented, and a subtree hidden while its\n * record is collapsed. Collapsing is view state, so it lives here and is\n * forgotten with the table, like the shift-click anchor above.\n */\n const treeKeys = useMemo(\n () => hierarchyOf(entityConstructor),\n [entityConstructor],\n );\n const [collapsed, setCollapsed] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n const rows = useMemo(() => treeRows(items, treeKeys), [items, treeKeys]);\n const shown = shownTreeRows(rows, collapsed);\n const shownItems = shown.map(row => row.item);\n const toggleCollapsed = (key: string) =>\n setCollapsed(current => {\n const next = new Set(current);\n if (next.has(key)) next.delete(key);\n else next.add(key);\n return next;\n });\n\n const setSelection = (next: EntitySelection<TEntity>) =>\n onSelectionChange?.(next);\n\n const toggleRow = (\n index: number,\n checked: boolean,\n shiftKey: boolean,\n current: EntitySelection<TEntity>,\n ) => {\n // A shift-click extends the last toggle across the rows *on this page*, in\n // the direction the user dragged, and applies the new state to all of them.\n const from = shiftKey ? (anchorIndex.current ?? index) : index;\n const [start, end] = from <= index ? [from, index] : [index, from];\n\n let next = current;\n // The rows as shown: in a tree the order on screen is not the load's.\n for (const row of shownItems.slice(start, end + 1)) {\n next = toggleSelected(next, row.id, checked);\n }\n\n anchorIndex.current = index;\n setSelection(next);\n };\n\n /**\n * The header box, and it has three states rather than two: none of this\n * page's rows selected, all of them, or some. Without the third the box\n * reads as \"nothing is selected\" while a bulk bar sits below it saying\n * otherwise.\n */\n const pageIds = items.map(item => item.id);\n const selectedOnPage = selectionState\n ? pageIds.filter(id => isSelected(selectionState, id)).length\n : 0;\n const allOnPageSelected =\n pageIds.length > 0 && selectedOnPage === pageIds.length;\n const someOnPageSelected = selectedOnPage > 0 && !allOnPageSelected;\n\n const toggleAllOnPage = (\n checked: boolean,\n current: EntitySelection<TEntity>,\n ) => {\n let next = current;\n for (const id of pageIds) next = toggleSelected(next, id, checked);\n anchorIndex.current = undefined;\n setSelection(next);\n };\n\n /**\n * How a row names itself to a screen reader.\n *\n * The first visible column's value, because that is what the user reads the\n * row *as* — \"Seleccionar Acme\" rather than \"Seleccionar fila 3\", which tells\n * them the position they can already see and not the thing they cannot.\n */\n const rowLabel = (item: TEntity): string => {\n // The first non-identifier column **that this row actually filled in**.\n //\n // An id names a row the way \"row 3\" does: it repeats what the user can\n // already see. But skipping only the id is not enough — measured against\n // the seeded catalog, where `code` is the first non-id column and is empty\n // on every brand, so the label fell straight through to the id and never\n // reached `name`. A column that is blank on this row names it no better\n // than its position does, so the search continues past it.\n //\n // The id remains the last resort, because a row with nothing rendered\n // still has to announce itself.\n const named = visibleColumns.find(column => {\n if (column.type === 'id') return false;\n const value = (item as Record<string, unknown>)[column.name];\n return value != null && String(value) !== '';\n });\n\n return named\n ? String((item as Record<string, unknown>)[named.name])\n : String(item.id);\n };\n\n const selectionCell = (\n item: TEntity,\n index: number,\n // Passed in rather than closed over: this is only ever called under\n // `selectable`, which already proves the selection exists, and reading the\n // optional prop here would add a fallback arm that nothing can reach.\n current: EntitySelection<TEntity>,\n ) => (\n <Checkbox\n // Deliberately no `id`: both pivots are always in the DOM, so an id here\n // would be emitted twice per row. `aria-label` names the control instead,\n // and it names the *record* rather than the position.\n aria-label={t('table.selectRow', { record: rowLabel(item) })}\n checked={isSelected(current, item.id)}\n disabled={isBulkRunning}\n onChange={event =>\n toggleRow(\n index,\n event.currentTarget.checked,\n (event.nativeEvent as MouseEvent).shiftKey === true,\n current,\n )\n }\n />\n );\n\n const runTarget = (key: string, target: PendingTarget<TEntity>) => {\n if (target.kind === 'row') onUseCase?.(key, target.item);\n else onBulkUseCase?.(key, target.selection);\n };\n\n const invoke = (\n useCase: UseCaseDescriptor,\n target: PendingTarget<TEntity>,\n ) => {\n const confirm = useCase.confirm;\n if (confirm) {\n setPending({\n key: useCase.key,\n labelKey: useCase.labelKey,\n confirm,\n target,\n });\n return;\n }\n runTarget(useCase.key, target);\n };\n\n const rowMenu = (item: TEntity) =>\n hasRowMenu ? (\n <Menu>\n <Menu.Trigger aria-label={t('table.actions')}>\n {OVERFLOW_GLYPH}\n </Menu.Trigger>\n <Menu.Items>\n {rowUseCases.map(useCase => (\n <Menu.Item\n key={useCase.key}\n tone={\n useCase.confirm?.tone === 'destructive'\n ? 'destructive'\n : 'neutral'\n }\n onClick={() => invoke(useCase, { kind: 'row', item })}\n >\n {translateKey(useCase.labelKey)}\n </Menu.Item>\n ))}\n </Menu.Items>\n </Menu>\n ) : undefined;\n\n /**\n * Selection wins over navigation: a picker's row must set the value, not send\n * the user away from the form they came from.\n *\n * The row menu sits *beside* whichever of the two applies rather than\n * replacing it — \"open this record\" and \"run a verb on it\" are both true at\n * once, and folding the first into the menu would bury the action every row\n * of a list exists for.\n */\n const recordAction = (item: TEntity) => {\n const primary = onSelect ? (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => onSelect(item)}\n >\n {t('table.select')}\n </Button>\n ) : hrefFor ? (\n renderLink({\n href: hrefFor(item.id),\n className: linkClassName,\n children: t('table.open'),\n intent: { kind: 'open', id: item.id },\n })\n ) : undefined;\n\n const menu = rowMenu(item);\n if (!menu) return primary;\n\n return (\n <span className=\"flex items-center gap-2xs\">\n {primary}\n {menu}\n </span>\n );\n };\n\n /**\n * \"No records\" is a statement about the data; after a failed load it would be\n * a lie about it. The distinction matters most to the user who is deciding\n * whether their catalog is empty or their backend is down.\n */\n const emptyMessage = error ? t('table.error') : t('table.empty');\n\n return (\n <div className=\"flex flex-col gap-s\">\n {/*\n One announcement for the whole table, not one per layout. Both the grid\n and the card list are always in the DOM — CSS picks which is visible —\n so a live region inside each meant assistive tech was told twice that\n the table was loading. The shimmer itself is aria-hidden, so this is the\n only thing that carries the news.\n */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {isFirstLoad ? t('table.loading') : ''}\n </span>\n\n {showControls && (\n <TableToolbar\n start={\n <>\n {newHref &&\n renderLink({\n href: newHref,\n className: linkClassName,\n children: t('table.new'),\n intent: { kind: 'new' },\n })}\n {/* Collection-bound verbs that need no selection. They sit with\n `New` rather than in the bulk bar, because the bulk bar only\n exists while rows are ticked and these are always available. */}\n <LoadingBoundary isLoading={isMetadataLoading} lines={0}>\n <>\n {toolbarUseCases.map(useCase => (\n <Button\n key={useCase.key}\n type=\"button\"\n variant={\n useCase.confirm?.tone === 'destructive'\n ? 'destructive'\n : 'secondary'\n }\n size=\"sm\"\n disabled={isBulkRunning}\n onClick={() =>\n onBulkUseCase?.(useCase.key, everythingMatching)\n }\n >\n {translateKey(useCase.labelKey)}\n </Button>\n ))}\n </>\n </LoadingBoundary>\n {slots.toolbar}\n </>\n }\n end={\n <>\n <Button\n type=\"button\"\n variant={panel === 'filters' ? 'primary' : 'secondary'}\n size=\"sm\"\n onClick={() =>\n setPanel(current =>\n current === 'filters' ? 'none' : 'filters',\n )\n }\n >\n {t('table.filters')}\n </Button>\n <Button\n type=\"button\"\n variant={panel === 'sorting' ? 'primary' : 'secondary'}\n size=\"sm\"\n onClick={() =>\n setPanel(current =>\n current === 'sorting' ? 'none' : 'sorting',\n )\n }\n >\n {t('table.sorting')}\n </Button>\n <ColumnSettings\n columns={columns}\n hidden={hidden}\n onChange={setPersonalization}\n />\n </>\n }\n panel={\n panel === 'filters' ? (\n <FilterBuilder<TEntity>\n descriptors={columns.filter(column => column.filterable)}\n value={filtering}\n onChange={(applied: FilterGroup<TEntity>) =>\n onFilteringChange?.(applied)\n }\n />\n ) : panel === 'sorting' ? (\n <SortBuilder<TEntity>\n descriptors={columns.filter(column => column.sortable)}\n value={sorting}\n onChange={(applied: EntitySorting<TEntity>) =>\n onSortingChange?.(applied)\n }\n />\n ) : undefined\n }\n />\n )}\n\n {/*\n The bar appears when there is something selected and something to do\n with it. It sits above the rows rather than floating over them: a fixed\n overlay covers the last row of a short page, which is the row the user\n most likely just ticked.\n */}\n {selectionState && selectionSize(selectionState) > 0 && (\n <BulkActionBar\n count={selectionSize(selectionState)}\n useCases={bulkUseCases}\n busy={isBulkRunning}\n // Offered only when this page is fully ticked and the store holds\n // more than the page — the escalation has somewhere to escalate to.\n matchingTotal={\n selectionState.mode === 'ids' &&\n allOnPageSelected &&\n totalItems > items.length\n ? totalItems\n : undefined\n }\n onSelectAllMatching={() => setSelection(everythingMatching)}\n onClear={() => setSelection(emptySelection<TEntity>())}\n onUseCase={useCase =>\n invoke(useCase, { kind: 'selection', selection: selectionState })\n }\n />\n )}\n\n {pending && (\n <ConfirmDialog\n open\n tone={pending.confirm.tone}\n title={translateKey(pending.labelKey)}\n message={translateKey(pending.confirm.messageKey)}\n busy={isBulkRunning}\n onCancel={() => setPending(undefined)}\n onConfirm={() => {\n const { key, target } = pending;\n setPending(undefined);\n runTarget(key, target);\n }}\n />\n )}\n\n {bulkOutcomes && bulkOutcomes.length > 0 && onBulkDismiss && (\n <BulkResult\n outcomes={bulkOutcomes}\n busy={isBulkRunning}\n labelFor={id => {\n const row = items.find(item => item.id === id);\n return row ? rowLabel(row) : undefined;\n }}\n onRetryFailed={onBulkRetry}\n onDismiss={onBulkDismiss}\n />\n )}\n\n {/*\n Rendered once, outside both layouts, and independently of whether there\n are rows: a refetch that fails leaves the previous page on screen, and\n without this the user would read stale data as current.\n */}\n {error && (\n <p\n role=\"alert\"\n data-testid=\"entity-table-error\"\n className=\"rounded-sm border border-danger bg-danger-subtle px-s py-2xs text-step-sm text-danger\"\n >\n {errorMessage(error)}\n </p>\n )}\n\n {/* Wide viewports: a grid. */}\n <div className={`${pivot.grid} ${busyClass}`}>\n <Table>\n <TableHead>\n {slots.header ? (\n slots.header.render(visibleColumns)\n ) : (\n <tr>\n {selectable && (\n <TableHeaderCell>\n <Checkbox\n aria-label={t('table.selectAllOnPage')}\n checked={allOnPageSelected}\n indeterminate={someOnPageSelected}\n disabled={isBulkRunning || items.length === 0}\n onChange={event =>\n toggleAllOnPage(\n event.currentTarget.checked,\n selectionState,\n )\n }\n />\n </TableHeaderCell>\n )}\n {visibleColumns.map(column => (\n <TableHeaderCell key={column.name}>\n {column.header ?? column.label}\n </TableHeaderCell>\n ))}\n {hasRowAction && (\n <TableHeaderCell>{t('table.actions')}</TableHeaderCell>\n )}\n </tr>\n )}\n </TableHead>\n <TableBody aria-busy={isLoading || undefined}>\n {/* Skeleton rows rather than the word \"Loading\", and one per column\n rather than a single full-width blob: the placeholder occupies\n the real grid, so the swap to rows shifts nothing sideways.\n `TableMessageRow` is deliberately bypassed — it is one centred\n full-span cell and cannot express the geometry. */}\n {showSkeleton &&\n (customSkeleton ? (\n <TableMessageRow colSpan={columnCount}>\n {customSkeleton}\n </TableMessageRow>\n ) : (\n skeletonRows.map(row => (\n <TableRow key={row}>\n {selectable && (\n <TableCell>\n <Skeleton shape=\"line\" className=\"w-4\" />\n </TableCell>\n )}\n {visibleColumns.map(column => (\n <TableCell key={column.name}>\n <Skeleton shape=\"line\" className=\"w-full\" />\n </TableCell>\n ))}\n {hasRowAction && (\n <TableCell>\n <Skeleton shape=\"line\" className=\"w-16\" />\n </TableCell>\n )}\n </TableRow>\n ))\n ))}\n {!isLoading && items.length === 0 && (\n <TableMessageRow colSpan={columnCount}>\n {emptyMessage}\n </TableMessageRow>\n )}\n {shown.map(({ item, depth, hasChildren }, index) =>\n slots.row ? (\n // A Fragment carries the key without wrapping the custom row in\n // an element `<tbody>` would reject.\n <Fragment key={String(item.id ?? index)}>\n {slots.row.render(item, visibleColumns)}\n </Fragment>\n ) : (\n <TableRow key={String(item.id ?? index)}>\n {selectionState && (\n <TableCell>\n {selectionCell(item, index, selectionState)}\n </TableCell>\n )}\n {visibleColumns.map((column, columnIndex) => (\n <TableCell key={column.name}>\n {treeKeys !== undefined && columnIndex === 0 ? (\n <TreeCell\n depth={depth}\n hasChildren={hasChildren}\n expanded={!collapsed.has(treeKeys.idOf(item))}\n label={t(\n collapsed.has(treeKeys.idOf(item))\n ? 'table.expandRow'\n : 'table.collapseRow',\n { record: rowLabel(item) },\n )}\n onToggle={() => toggleCollapsed(treeKeys.idOf(item))}\n >\n {renderCell(column, item)}\n </TreeCell>\n ) : (\n renderCell(column, item)\n )}\n </TableCell>\n ))}\n {hasRowAction && <TableCell>{recordAction(item)}</TableCell>}\n </TableRow>\n ),\n )}\n </TableBody>\n </Table>\n </div>\n\n {/* Narrow viewports: the same columns pivoted into cards. */}\n <div\n className={`${pivot.cards} flex flex-col gap-2xs ${busyClass}`}\n aria-busy={isLoading || undefined}\n >\n {/* The card pivot gets card-shaped placeholders, not a bare shimmer\n block: the border, radius and padding are the geometry here. */}\n {showSkeleton &&\n (customSkeleton ??\n skeletonRows.map(row => (\n <EntityRecordCard\n key={row}\n columns={visibleColumns}\n renderCell={() => <Skeleton shape=\"line\" className=\"w-24\" />}\n />\n )))}\n {!isLoading && items.length === 0 && (\n <p className=\"text-step-sm text-content-muted\">{emptyMessage}</p>\n )}\n {shown.map(({ item }, index) => (\n <EntityRecordCard\n key={String(item.id ?? index)}\n columns={visibleColumns}\n // The card is handed the resolved columns themselves, so the\n // narrow layout goes through the exact same renderers as the grid.\n renderCell={column =>\n renderCell(column as EntityTableColumn<TEntity>, item)\n }\n leading={\n selectionState\n ? selectionCell(item, index, selectionState)\n : undefined\n }\n actions={recordAction(item)}\n />\n ))}\n </div>\n\n <Pagination\n currentPage={currentPage}\n pageSize={pageSize}\n totalItems={totalItems}\n onPageChange={onPageChange}\n onPageSizeChange={onPageSizeChange}\n />\n\n {slots.rest}\n </div>\n );\n}\n"],"names":["emptySelection","EntifixLogicError","extractMetaEntity","hierarchyOf","isSelected","selectionSize","toggleSelected","Fragment","useMemo","useRef","useState","useErrorMessage","useT","useTranslateKey","OVERFLOW_GLYPH","renderAnchor","useCasesForSurface","Button","CellValue","Checkbox","Skeleton","Table","TableBody","TableCell","TableHead","TableHeaderCell","TableMessageRow","TableRow","linkClassName","BulkActionBar","BulkResult","ColumnSettings","ConfirmDialog","EntityRecordCard","FilterBuilder","LoadingBoundary","Menu","Pagination","SortBuilder","TableToolbar","readEntityTableSlots","shownTreeRows","TreeCell","treeRows","useEntityTableColumns","PIVOT_CLASS","sm","grid","cards","md","lg","SKELETON_ROW_CAP","EntityTable","entityConstructor","isLoading","skeleton","error","items","totalItems","currentPage","pageSize","onPageChange","onPageSizeChange","hrefFor","newHref","renderLink","onSelect","preferencesKey","showControls","pivotBreakpoint","filtering","sorting","onFilteringChange","onSortingChange","selection","onSelectionChange","metadata","isMetadataLoading","onBulkUseCase","onUseCase","bulkOutcomes","onBulkDismiss","onBulkRetry","isBulkRunning","children","t","translateKey","errorMessage","metaEntity","slots","panel","setPanel","pending","setPending","undefined","columns","visibleColumns","hidden","setPersonalization","key","name","pivot","selectionState","selectable","rowUseCases","useCases","bulkUseCases","toolbarUseCases","hasRowMenu","length","everythingMatching","mode","total","excluded","Set","hasRowAction","columnCount","isFirstLoad","isRefetching","customSkeleton","showSkeleton","skeletonRows","Array","from","Math","min","_","index","busyClass","renderCell","column","item","render","value","descriptor","anchorIndex","treeKeys","collapsed","setCollapsed","rows","shown","shownItems","map","row","toggleCollapsed","current","next","has","delete","add","setSelection","toggleRow","checked","shiftKey","start","end","slice","id","pageIds","selectedOnPage","filter","allOnPageSelected","someOnPageSelected","toggleAllOnPage","rowLabel","named","find","type","String","selectionCell","aria-label","record","disabled","onChange","event","currentTarget","nativeEvent","runTarget","target","kind","invoke","useCase","confirm","labelKey","rowMenu","Trigger","Items","Item","tone","onClick","recordAction","primary","variant","size","href","className","intent","menu","span","emptyMessage","div","role","aria-live","lines","toolbar","descriptors","filterable","applied","sortable","count","busy","matchingTotal","onSelectAllMatching","onClear","open","title","message","messageKey","onCancel","onConfirm","outcomes","labelFor","onRetryFailed","onDismiss","p","data-testid","header","tr","indeterminate","label","aria-busy","colSpan","shape","depth","hasChildren","columnIndex","expanded","idOf","onToggle","leading","actions","rest"],"mappings":"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"}
|
|
@@ -7,8 +7,7 @@ import type { WizardProps } from './wizard.types.js';
|
|
|
7
7
|
* draft and no router — `activeStep` is a prop and `onStepChange` reports a
|
|
8
8
|
* click, the same rule that made `hrefFor` a prop, so URL synchronisation stays
|
|
9
9
|
* the shell's job and this control can be told to render step four by a route,
|
|
10
|
-
* a workspace tab, or a Storybook arg without knowing which
|
|
11
|
-
* ([ADR 0045](../../../../../../../docs/adr/0045-the-wizard-a-step-graph-and-a-submit-that-hands-off.md)).
|
|
10
|
+
* a workspace tab, or a Storybook arg without knowing which.
|
|
12
11
|
*
|
|
13
12
|
* ⚠️ **The stepper is an ordered list, not a tablist.** `TabStrip` is the
|
|
14
13
|
* nearest-looking thing in this package and is the wrong precedent: a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wizard.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/wizard/wizard.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAkB,MAAM,mBAAmB,CAAC;AAkCrE
|
|
1
|
+
{"version":3,"file":"wizard.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/wizard/wizard.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAkB,MAAM,mBAAmB,CAAC;AAkCrE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,MAAM,CAAC,EACrB,KAAK,EACL,UAAU,EACV,KAAK,EACL,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,SAAiB,EACjB,UAAiB,EACjB,YAAoB,EACpB,KAAK,EACL,cAAc,EACd,SAAiB,EACjB,QAAe,EACf,SAAS,GACV,EAAE,WAAW,+BA+Gb"}
|
|
@@ -15,7 +15,7 @@ import { cn } from '../../utils/cn.js';
|
|
|
15
15
|
*
|
|
16
16
|
* `t(\`wizard.status.${status}\`)` would work and would be untyped — the
|
|
17
17
|
* augmentation cannot see a template literal, so the only thing left checking
|
|
18
|
-
* it would be
|
|
18
|
+
* it would be a host's catalog check, which typically does not read `.tsx`. Four literals
|
|
19
19
|
* cost nothing and stay compile-checked.
|
|
20
20
|
*/ const STATUS_KEY = {
|
|
21
21
|
pending: 'wizard.status.pending',
|
|
@@ -43,8 +43,7 @@ const LABEL_CLASS = {
|
|
|
43
43
|
* draft and no router — `activeStep` is a prop and `onStepChange` reports a
|
|
44
44
|
* click, the same rule that made `hrefFor` a prop, so URL synchronisation stays
|
|
45
45
|
* the shell's job and this control can be told to render step four by a route,
|
|
46
|
-
* a workspace tab, or a Storybook arg without knowing which
|
|
47
|
-
* ([ADR 0045](../../../../../../../docs/adr/0045-the-wizard-a-step-graph-and-a-submit-that-hands-off.md)).
|
|
46
|
+
* a workspace tab, or a Storybook arg without knowing which.
|
|
48
47
|
*
|
|
49
48
|
* ⚠️ **The stepper is an ordered list, not a tablist.** `TabStrip` is the
|
|
50
49
|
* nearest-looking thing in this package and is the wrong precedent: a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/ui/organisms/wizard/wizard.tsx"],"sourcesContent":["'use client';\n\nimport type { WizardStepStatus } from '@entifix/core';\nimport { type ReactNode, useEffect, useRef } from 'react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport { Text } from '../../atoms/text/index.js';\nimport { Cluster } from '../../layout/cluster/index.js';\nimport { Card } from '../../molecules/card/index.js';\nimport { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.js';\nimport type { WizardProps, WizardStepView } from './wizard.types.js';\n\n/** How many placeholder steps stand in when the caller has none yet. */\nconst SKELETON_STEP_COUNT = 4;\n\n/**\n * The status catalog keys, written out rather than composed.\n *\n * `t(\\`wizard.status.${status}\\`)` would work and would be untyped — the\n * augmentation cannot see a template literal, so the only thing left checking\n * it would be `@r10c/i18n-check`, which does not read `.tsx`. Four literals\n * cost nothing and stay compile-checked.\n */\nconst STATUS_KEY = {\n pending: 'wizard.status.pending',\n active: 'wizard.status.active',\n complete: 'wizard.status.complete',\n error: 'wizard.status.error',\n} as const satisfies Record<WizardStepStatus, string>;\n\nconst MARKER_CLASS: Record<WizardStepStatus, string> = {\n pending: 'border-border bg-surface text-content-muted',\n active: 'border-accent bg-accent text-primary-content',\n complete: 'border-accent bg-surface text-accent',\n error: 'border-danger bg-danger-subtle text-danger',\n};\n\nconst LABEL_CLASS: Record<WizardStepStatus, string> = {\n pending: 'text-content-muted',\n active: 'font-medium text-content',\n complete: 'text-content',\n error: 'font-medium text-danger',\n};\n\n/**\n * A guided multi-step flow: the stepper, the active step's body, and the\n * controls that move between them.\n *\n * **Presentational, and controlled from above.** It holds no step state, no\n * draft and no router — `activeStep` is a prop and `onStepChange` reports a\n * click, the same rule that made `hrefFor` a prop, so URL synchronisation stays\n * the shell's job and this control can be told to render step four by a route,\n * a workspace tab, or a Storybook arg without knowing which\n * ([ADR 0045](../../../../../../../docs/adr/0045-the-wizard-a-step-graph-and-a-submit-that-hands-off.md)).\n *\n * ⚠️ **The stepper is an ordered list, not a tablist.** `TabStrip` is the\n * nearest-looking thing in this package and is the wrong precedent: a\n * `role=\"tablist\"` tells assistive technology that the panels are siblings the\n * user may choose between, and a wizard's steps are ordered and gated. So this\n * is `<ol>`/`<li>` with `aria-current=\"step\"`, and each step's status is\n * announced rather than carried by colour alone.\n */\nexport function Wizard({\n steps,\n activeStep,\n title,\n children,\n onNext,\n onFinish,\n onPrevious,\n onStepChange,\n canFinish = false,\n canAdvance = true,\n isSubmitting = false,\n recap,\n onDismissRecap,\n isLoading = false,\n skeleton = true,\n className,\n}: WizardProps) {\n const t = useT();\n const heading = useRef<HTMLHeadingElement>(null);\n const focused = useRef(activeStep);\n\n // Focus moves to the new step's heading, and **only on a change**: doing it on\n // mount would take focus from whatever opened the wizard, which on a route is\n // the link the operator just used.\n useEffect(() => {\n if (focused.current === activeStep) return;\n focused.current = activeStep;\n heading.current?.focus();\n }, [activeStep]);\n\n if (isLoading && skeleton !== false) {\n return (\n <WizardSkeleton\n stepCount={steps.length > 0 ? steps.length : SKELETON_STEP_COUNT}\n placeholder={skeleton === true ? undefined : skeleton}\n className={className}\n />\n );\n }\n\n const position = steps.findIndex(step => step.id === activeStep) + 1;\n\n return (\n <Stack gap=\"m\" className={className}>\n <Stepper\n steps={steps}\n activeStep={activeStep}\n onStepChange={onStepChange}\n />\n\n {recap && (\n <Card data-testid=\"wizard-recap\" className=\"border-l-4 border-l-accent\">\n <Stack gap=\"2xs\">\n <Text as=\"h3\" step={0} weight=\"semibold\">\n {t('wizard.resume.title')}\n </Text>\n {recap}\n {onDismissRecap && (\n <Cluster justify=\"end\">\n <Button variant=\"ghost\" size=\"sm\" onClick={onDismissRecap}>\n {t('wizard.resume.dismiss')}\n </Button>\n </Cluster>\n )}\n </Stack>\n </Card>\n )}\n\n {/* No `Card` around the body: a step brings its own surface — an embedded\n `EntityForm` is already a card — and nesting one inside another reads\n as two panels for one step. */}\n <Stack gap=\"s\">\n <Stack gap=\"3xs\">\n {/*\n A plain heading with a real ref, and `tabIndex={-1}` so script can\n focus it without putting it in the tab order — the target of the\n advance above.\n */}\n <h2\n ref={heading}\n tabIndex={-1}\n className=\"rounded-sm text-step-1 font-semibold text-content focus-ring outline-none\"\n >\n {title}\n </h2>\n {position > 0 && (\n <Text step={-1} tone=\"muted\">\n {t('wizard.stepOf', {\n current: position,\n total: steps.length,\n })}\n </Text>\n )}\n </Stack>\n\n {children}\n </Stack>\n\n <Cluster justify=\"between\">\n <div>\n {onPrevious && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n data-testid=\"wizard-previous\"\n onClick={onPrevious}\n disabled={isSubmitting}\n >\n {t('wizard.previous')}\n </Button>\n )}\n </div>\n {/* Named for the tests and the e2e, because a step may bring controls\n of its own — a table step's pager sits in the same view. */}\n <Button\n type=\"button\"\n data-testid=\"wizard-next\"\n onClick={canFinish ? onFinish : onNext}\n disabled={isSubmitting || !canAdvance}\n >\n {isSubmitting && t('wizard.submitting')}\n {!isSubmitting && canFinish && t('wizard.finish')}\n {!isSubmitting && !canFinish && t('wizard.next')}\n </Button>\n </Cluster>\n </Stack>\n );\n}\n\ninterface StepperProps {\n steps: readonly WizardStepView[];\n activeStep: string;\n onStepChange?: (stepId: string) => void;\n}\n\nfunction Stepper({ steps, activeStep, onStepChange }: StepperProps) {\n const t = useT();\n\n return (\n <ol\n aria-label={t('wizard.steps')}\n className=\"flex flex-wrap items-center gap-x-s gap-y-2xs\"\n >\n {steps.map((step, index) => (\n <li\n key={step.id}\n aria-current={step.id === activeStep ? 'step' : undefined}\n className=\"flex items-center gap-2xs\"\n >\n <StepMarker step={step} index={index} onStepChange={onStepChange} />\n {index < steps.length - 1 && (\n <span aria-hidden=\"true\" className=\"h-px w-s shrink-0 bg-border\" />\n )}\n </li>\n ))}\n </ol>\n );\n}\n\ninterface StepMarkerProps {\n step: WizardStepView;\n index: number;\n onStepChange?: (stepId: string) => void;\n}\n\n/**\n * One step's dot and label.\n *\n * A completed step is a `<button>` and every other step is not, rather than a\n * disabled button throughout: a disabled control is still announced as a\n * control, so a pending step would read as something the operator may activate\n * and cannot, when in truth there is nothing there to activate yet.\n */\nfunction StepMarker({ step, index, onStepChange }: StepMarkerProps) {\n const t = useT();\n const clickable = onStepChange !== undefined && step.status === 'complete';\n\n const body = (\n <>\n <span\n aria-hidden=\"true\"\n className={cn(\n 'size-6 flex shrink-0 items-center justify-center rounded-full border',\n 'text-step-xs font-medium transition duration-200 ease-smooth',\n MARKER_CLASS[step.status],\n )}\n >\n {index + 1}\n </span>\n <span className={cn('text-step-sm', LABEL_CLASS[step.status])}>\n {step.label}\n </span>\n {/*\n Status is announced as well as coloured. A screen reader walks this list\n without seeing the ring, and \"completed\" is the whole point of a stepper.\n */}\n <span className=\"sr-only\">{t(STATUS_KEY[step.status])}</span>\n </>\n );\n\n if (!clickable) {\n return <span className=\"flex items-center gap-3xs\">{body}</span>;\n }\n\n return (\n <button\n type=\"button\"\n onClick={() => onStepChange(step.id)}\n aria-label={t('wizard.goToStep', { label: step.label })}\n className=\"flex items-center gap-3xs rounded-md p-3xs focus-ring hover:bg-surface-elevated\"\n >\n {body}\n </button>\n );\n}\n\ninterface WizardSkeletonProps {\n stepCount: number;\n placeholder?: ReactNode;\n className?: string;\n}\n\n/**\n * The wizard's shape while it is in flight.\n *\n * Sized from the step count it was handed, so the swap to the real stepper\n * shifts nothing — the geometry rule every skeleton in this package follows.\n */\nfunction WizardSkeleton({\n stepCount,\n placeholder,\n className,\n}: WizardSkeletonProps) {\n return (\n <Stack\n gap=\"m\"\n className={className}\n role=\"status\"\n aria-busy=\"true\"\n data-testid=\"wizard-skeleton\"\n >\n <Cluster gap=\"s\">\n {Array.from({ length: stepCount }, (_, index) => (\n <Skeleton key={index} shape=\"line\" className=\"h-6 w-24\" />\n ))}\n </Cluster>\n {placeholder ?? <Skeleton className=\"h-40 w-full\" />}\n </Stack>\n );\n}\n"],"names":["useEffect","useRef","useT","Button","Skeleton","Text","Cluster","Card","Stack","cn","SKELETON_STEP_COUNT","STATUS_KEY","pending","active","complete","error","MARKER_CLASS","LABEL_CLASS","Wizard","steps","activeStep","title","children","onNext","onFinish","onPrevious","onStepChange","canFinish","canAdvance","isSubmitting","recap","onDismissRecap","isLoading","skeleton","className","t","heading","focused","current","focus","WizardSkeleton","stepCount","length","placeholder","undefined","position","findIndex","step","id","gap","Stepper","data-testid","as","weight","justify","variant","size","onClick","h2","ref","tabIndex","tone","total","div","type","disabled","ol","aria-label","map","index","li","aria-current","StepMarker","span","aria-hidden","clickable","status","body","label","button","role","aria-busy","Array","from","_","shape"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/ui/organisms/wizard/wizard.tsx"],"sourcesContent":["'use client';\n\nimport type { WizardStepStatus } from '@entifix/core';\nimport { type ReactNode, useEffect, useRef } from 'react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport { Text } from '../../atoms/text/index.js';\nimport { Cluster } from '../../layout/cluster/index.js';\nimport { Card } from '../../molecules/card/index.js';\nimport { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.js';\nimport type { WizardProps, WizardStepView } from './wizard.types.js';\n\n/** How many placeholder steps stand in when the caller has none yet. */\nconst SKELETON_STEP_COUNT = 4;\n\n/**\n * The status catalog keys, written out rather than composed.\n *\n * `t(\\`wizard.status.${status}\\`)` would work and would be untyped — the\n * augmentation cannot see a template literal, so the only thing left checking\n * it would be a host's catalog check, which typically does not read `.tsx`. Four literals\n * cost nothing and stay compile-checked.\n */\nconst STATUS_KEY = {\n pending: 'wizard.status.pending',\n active: 'wizard.status.active',\n complete: 'wizard.status.complete',\n error: 'wizard.status.error',\n} as const satisfies Record<WizardStepStatus, string>;\n\nconst MARKER_CLASS: Record<WizardStepStatus, string> = {\n pending: 'border-border bg-surface text-content-muted',\n active: 'border-accent bg-accent text-primary-content',\n complete: 'border-accent bg-surface text-accent',\n error: 'border-danger bg-danger-subtle text-danger',\n};\n\nconst LABEL_CLASS: Record<WizardStepStatus, string> = {\n pending: 'text-content-muted',\n active: 'font-medium text-content',\n complete: 'text-content',\n error: 'font-medium text-danger',\n};\n\n/**\n * A guided multi-step flow: the stepper, the active step's body, and the\n * controls that move between them.\n *\n * **Presentational, and controlled from above.** It holds no step state, no\n * draft and no router — `activeStep` is a prop and `onStepChange` reports a\n * click, the same rule that made `hrefFor` a prop, so URL synchronisation stays\n * the shell's job and this control can be told to render step four by a route,\n * a workspace tab, or a Storybook arg without knowing which.\n *\n * ⚠️ **The stepper is an ordered list, not a tablist.** `TabStrip` is the\n * nearest-looking thing in this package and is the wrong precedent: a\n * `role=\"tablist\"` tells assistive technology that the panels are siblings the\n * user may choose between, and a wizard's steps are ordered and gated. So this\n * is `<ol>`/`<li>` with `aria-current=\"step\"`, and each step's status is\n * announced rather than carried by colour alone.\n */\nexport function Wizard({\n steps,\n activeStep,\n title,\n children,\n onNext,\n onFinish,\n onPrevious,\n onStepChange,\n canFinish = false,\n canAdvance = true,\n isSubmitting = false,\n recap,\n onDismissRecap,\n isLoading = false,\n skeleton = true,\n className,\n}: WizardProps) {\n const t = useT();\n const heading = useRef<HTMLHeadingElement>(null);\n const focused = useRef(activeStep);\n\n // Focus moves to the new step's heading, and **only on a change**: doing it on\n // mount would take focus from whatever opened the wizard, which on a route is\n // the link the operator just used.\n useEffect(() => {\n if (focused.current === activeStep) return;\n focused.current = activeStep;\n heading.current?.focus();\n }, [activeStep]);\n\n if (isLoading && skeleton !== false) {\n return (\n <WizardSkeleton\n stepCount={steps.length > 0 ? steps.length : SKELETON_STEP_COUNT}\n placeholder={skeleton === true ? undefined : skeleton}\n className={className}\n />\n );\n }\n\n const position = steps.findIndex(step => step.id === activeStep) + 1;\n\n return (\n <Stack gap=\"m\" className={className}>\n <Stepper\n steps={steps}\n activeStep={activeStep}\n onStepChange={onStepChange}\n />\n\n {recap && (\n <Card data-testid=\"wizard-recap\" className=\"border-l-4 border-l-accent\">\n <Stack gap=\"2xs\">\n <Text as=\"h3\" step={0} weight=\"semibold\">\n {t('wizard.resume.title')}\n </Text>\n {recap}\n {onDismissRecap && (\n <Cluster justify=\"end\">\n <Button variant=\"ghost\" size=\"sm\" onClick={onDismissRecap}>\n {t('wizard.resume.dismiss')}\n </Button>\n </Cluster>\n )}\n </Stack>\n </Card>\n )}\n\n {/* No `Card` around the body: a step brings its own surface — an embedded\n `EntityForm` is already a card — and nesting one inside another reads\n as two panels for one step. */}\n <Stack gap=\"s\">\n <Stack gap=\"3xs\">\n {/*\n A plain heading with a real ref, and `tabIndex={-1}` so script can\n focus it without putting it in the tab order — the target of the\n advance above.\n */}\n <h2\n ref={heading}\n tabIndex={-1}\n className=\"rounded-sm text-step-1 font-semibold text-content focus-ring outline-none\"\n >\n {title}\n </h2>\n {position > 0 && (\n <Text step={-1} tone=\"muted\">\n {t('wizard.stepOf', {\n current: position,\n total: steps.length,\n })}\n </Text>\n )}\n </Stack>\n\n {children}\n </Stack>\n\n <Cluster justify=\"between\">\n <div>\n {onPrevious && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n data-testid=\"wizard-previous\"\n onClick={onPrevious}\n disabled={isSubmitting}\n >\n {t('wizard.previous')}\n </Button>\n )}\n </div>\n {/* Named for the tests and the e2e, because a step may bring controls\n of its own — a table step's pager sits in the same view. */}\n <Button\n type=\"button\"\n data-testid=\"wizard-next\"\n onClick={canFinish ? onFinish : onNext}\n disabled={isSubmitting || !canAdvance}\n >\n {isSubmitting && t('wizard.submitting')}\n {!isSubmitting && canFinish && t('wizard.finish')}\n {!isSubmitting && !canFinish && t('wizard.next')}\n </Button>\n </Cluster>\n </Stack>\n );\n}\n\ninterface StepperProps {\n steps: readonly WizardStepView[];\n activeStep: string;\n onStepChange?: (stepId: string) => void;\n}\n\nfunction Stepper({ steps, activeStep, onStepChange }: StepperProps) {\n const t = useT();\n\n return (\n <ol\n aria-label={t('wizard.steps')}\n className=\"flex flex-wrap items-center gap-x-s gap-y-2xs\"\n >\n {steps.map((step, index) => (\n <li\n key={step.id}\n aria-current={step.id === activeStep ? 'step' : undefined}\n className=\"flex items-center gap-2xs\"\n >\n <StepMarker step={step} index={index} onStepChange={onStepChange} />\n {index < steps.length - 1 && (\n <span aria-hidden=\"true\" className=\"h-px w-s shrink-0 bg-border\" />\n )}\n </li>\n ))}\n </ol>\n );\n}\n\ninterface StepMarkerProps {\n step: WizardStepView;\n index: number;\n onStepChange?: (stepId: string) => void;\n}\n\n/**\n * One step's dot and label.\n *\n * A completed step is a `<button>` and every other step is not, rather than a\n * disabled button throughout: a disabled control is still announced as a\n * control, so a pending step would read as something the operator may activate\n * and cannot, when in truth there is nothing there to activate yet.\n */\nfunction StepMarker({ step, index, onStepChange }: StepMarkerProps) {\n const t = useT();\n const clickable = onStepChange !== undefined && step.status === 'complete';\n\n const body = (\n <>\n <span\n aria-hidden=\"true\"\n className={cn(\n 'size-6 flex shrink-0 items-center justify-center rounded-full border',\n 'text-step-xs font-medium transition duration-200 ease-smooth',\n MARKER_CLASS[step.status],\n )}\n >\n {index + 1}\n </span>\n <span className={cn('text-step-sm', LABEL_CLASS[step.status])}>\n {step.label}\n </span>\n {/*\n Status is announced as well as coloured. A screen reader walks this list\n without seeing the ring, and \"completed\" is the whole point of a stepper.\n */}\n <span className=\"sr-only\">{t(STATUS_KEY[step.status])}</span>\n </>\n );\n\n if (!clickable) {\n return <span className=\"flex items-center gap-3xs\">{body}</span>;\n }\n\n return (\n <button\n type=\"button\"\n onClick={() => onStepChange(step.id)}\n aria-label={t('wizard.goToStep', { label: step.label })}\n className=\"flex items-center gap-3xs rounded-md p-3xs focus-ring hover:bg-surface-elevated\"\n >\n {body}\n </button>\n );\n}\n\ninterface WizardSkeletonProps {\n stepCount: number;\n placeholder?: ReactNode;\n className?: string;\n}\n\n/**\n * The wizard's shape while it is in flight.\n *\n * Sized from the step count it was handed, so the swap to the real stepper\n * shifts nothing — the geometry rule every skeleton in this package follows.\n */\nfunction WizardSkeleton({\n stepCount,\n placeholder,\n className,\n}: WizardSkeletonProps) {\n return (\n <Stack\n gap=\"m\"\n className={className}\n role=\"status\"\n aria-busy=\"true\"\n data-testid=\"wizard-skeleton\"\n >\n <Cluster gap=\"s\">\n {Array.from({ length: stepCount }, (_, index) => (\n <Skeleton key={index} shape=\"line\" className=\"h-6 w-24\" />\n ))}\n </Cluster>\n {placeholder ?? <Skeleton className=\"h-40 w-full\" />}\n </Stack>\n );\n}\n"],"names":["useEffect","useRef","useT","Button","Skeleton","Text","Cluster","Card","Stack","cn","SKELETON_STEP_COUNT","STATUS_KEY","pending","active","complete","error","MARKER_CLASS","LABEL_CLASS","Wizard","steps","activeStep","title","children","onNext","onFinish","onPrevious","onStepChange","canFinish","canAdvance","isSubmitting","recap","onDismissRecap","isLoading","skeleton","className","t","heading","focused","current","focus","WizardSkeleton","stepCount","length","placeholder","undefined","position","findIndex","step","id","gap","Stepper","data-testid","as","weight","justify","variant","size","onClick","h2","ref","tabIndex","tone","total","div","type","disabled","ol","aria-label","map","index","li","aria-current","StepMarker","span","aria-hidden","clickable","status","body","label","button","role","aria-busy","Array","from","_","shape"],"mappings":"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"}
|
|
@@ -54,7 +54,7 @@ export interface WizardProps {
|
|
|
54
54
|
* The submit is in flight.
|
|
55
55
|
*
|
|
56
56
|
* It stays in flight: `onFinish` hands off and returns before the write is
|
|
57
|
-
* terminal
|
|
57
|
+
* terminal,
|
|
58
58
|
* so what this disables is a second submit — it is not a spinner waiting for
|
|
59
59
|
* a resolution the caller never gets.
|
|
60
60
|
*/
|
|
@@ -64,7 +64,7 @@ export interface WizardProps {
|
|
|
64
64
|
*
|
|
65
65
|
* Persistence gives "continue" for free; what it does not give is remembering
|
|
66
66
|
* what you had answered, which is the known failure of a long flow restored
|
|
67
|
-
* mid-way
|
|
67
|
+
* mid-way.
|
|
68
68
|
*/
|
|
69
69
|
recap?: ReactNode;
|
|
70
70
|
/** Dismiss the recap. Omit and it has no dismiss control. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/ui/organisms/wizard/wizard.types.ts"],"sourcesContent":["import type { WizardStepStatus } from '@entifix/core';\nimport type { ReactNode } from 'react';\n\n/**\n * One step as the stepper shows it.\n *\n * `label` is **already-resolved copy**, not a catalog key, which is the rule\n * `ConfirmDialog`'s `title`/`message` already follow: a step's name belongs to\n * the domain that declared the wizard, so its copy is `shell:`-namespaced and\n * this package cannot type-check the key.\n */\nexport interface WizardStepView {\n readonly id: string;\n readonly label: string;\n readonly status: WizardStepStatus;\n}\n\nexport interface WizardProps {\n /**\n * The steps on the path this flow is taking, in order — the projection\n * `stepStatuses` returns, not the definition's whole step list.\n *\n * A graph's declared steps include branches not taken, so rendering all of\n * them promises an operator who chose to start blank a step they will never\n * see.\n */\n steps: readonly WizardStepView[];\n\n /** Which step's body `children` is. Controlled; the shell owns the URL. */\n activeStep: string;\n\n /**\n * The active step's heading.\n *\n * It is also the **focus target on advance**: moving focus to the top of the\n * new step is what makes a keyboard or screen-reader user land somewhere\n * meaningful instead of wherever the previous step's Next button was.\n */\n title: string;\n\n /** The active step's body. */\n children: ReactNode;\n\n onNext: () => void;\n onFinish: () => void;\n /** Absent at the entry step, where there is nowhere to go back to. */\n onPrevious?: () => void;\n /**\n * A completed step was clicked. Omit and the stepper is read-only.\n *\n * Only `complete` steps are clickable; a `pending` one would skip the per-step\n * submit that validates everything in between.\n */\n onStepChange?: (stepId: string) => void;\n\n /** The active step ends the flow, so the primary button reads Finalizar. */\n canFinish?: boolean;\n /** Advancing is blocked — a required answer is missing on this step. */\n canAdvance?: boolean;\n\n /**\n * The submit is in flight.\n *\n * It stays in flight: `onFinish` hands off and returns before the write is\n * terminal
|
|
1
|
+
{"version":3,"sources":["../../../../src/ui/organisms/wizard/wizard.types.ts"],"sourcesContent":["import type { WizardStepStatus } from '@entifix/core';\nimport type { ReactNode } from 'react';\n\n/**\n * One step as the stepper shows it.\n *\n * `label` is **already-resolved copy**, not a catalog key, which is the rule\n * `ConfirmDialog`'s `title`/`message` already follow: a step's name belongs to\n * the domain that declared the wizard, so its copy is `shell:`-namespaced and\n * this package cannot type-check the key.\n */\nexport interface WizardStepView {\n readonly id: string;\n readonly label: string;\n readonly status: WizardStepStatus;\n}\n\nexport interface WizardProps {\n /**\n * The steps on the path this flow is taking, in order — the projection\n * `stepStatuses` returns, not the definition's whole step list.\n *\n * A graph's declared steps include branches not taken, so rendering all of\n * them promises an operator who chose to start blank a step they will never\n * see.\n */\n steps: readonly WizardStepView[];\n\n /** Which step's body `children` is. Controlled; the shell owns the URL. */\n activeStep: string;\n\n /**\n * The active step's heading.\n *\n * It is also the **focus target on advance**: moving focus to the top of the\n * new step is what makes a keyboard or screen-reader user land somewhere\n * meaningful instead of wherever the previous step's Next button was.\n */\n title: string;\n\n /** The active step's body. */\n children: ReactNode;\n\n onNext: () => void;\n onFinish: () => void;\n /** Absent at the entry step, where there is nowhere to go back to. */\n onPrevious?: () => void;\n /**\n * A completed step was clicked. Omit and the stepper is read-only.\n *\n * Only `complete` steps are clickable; a `pending` one would skip the per-step\n * submit that validates everything in between.\n */\n onStepChange?: (stepId: string) => void;\n\n /** The active step ends the flow, so the primary button reads Finalizar. */\n canFinish?: boolean;\n /** Advancing is blocked — a required answer is missing on this step. */\n canAdvance?: boolean;\n\n /**\n * The submit is in flight.\n *\n * It stays in flight: `onFinish` hands off and returns before the write is\n * terminal,\n * so what this disables is a second submit — it is not a spinner waiting for\n * a resolution the caller never gets.\n */\n isSubmitting?: boolean;\n\n /**\n * What was already decided, shown when a half-finished wizard is resumed.\n *\n * Persistence gives \"continue\" for free; what it does not give is remembering\n * what you had answered, which is the known failure of a long flow restored\n * mid-way.\n */\n recap?: ReactNode;\n /** Dismiss the recap. Omit and it has no dismiss control. */\n onDismissRecap?: () => void;\n\n isLoading?: boolean;\n /**\n * What holds the wizard's shape while it is in flight.\n *\n * `true` (the default) renders the built-in placeholder, sized from the step\n * count it was given so the swap to the real stepper shifts nothing. A node\n * replaces that default; `false` renders no placeholder at all.\n */\n skeleton?: boolean | ReactNode;\n\n className?: string;\n}\n"],"names":[],"mappings":"AAiBA,WA2EC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Marks the nav anchor of the section in view with `aria-current`: an
|
|
3
|
-
* enhancement over anchors that already work
|
|
3
|
+
* enhancement over anchors that already work. Renders nothing.
|
|
4
4
|
*
|
|
5
5
|
* A section counts as in view while it crosses the band just above the
|
|
6
6
|
* viewport's middle; over the hero, none does.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
3
|
/**
|
|
4
4
|
* Marks the nav anchor of the section in view with `aria-current`: an
|
|
5
|
-
* enhancement over anchors that already work
|
|
5
|
+
* enhancement over anchors that already work. Renders nothing.
|
|
6
6
|
*
|
|
7
7
|
* A section counts as in view while it crosses the band just above the
|
|
8
8
|
* viewport's middle; over the hero, none does.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/ui/utils/active-section/active-section.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect } from 'react';\n\n/**\n * Marks the nav anchor of the section in view with `aria-current`: an\n * enhancement over anchors that already work
|
|
1
|
+
{"version":3,"sources":["../../../../src/ui/utils/active-section/active-section.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect } from 'react';\n\n/**\n * Marks the nav anchor of the section in view with `aria-current`: an\n * enhancement over anchors that already work. Renders nothing.\n *\n * A section counts as in view while it crosses the band just above the\n * viewport's middle; over the hero, none does.\n */\nexport function ActiveSection({ sections }: { sections: readonly string[] }) {\n useEffect(() => {\n const links = new Map(\n sections.map(id => [\n id,\n document.querySelectorAll<HTMLAnchorElement>(`a[data-section=\"${id}\"]`),\n ]),\n );\n const visible = new Set<string>();\n const mark = () => {\n const current = sections.find(id => visible.has(id));\n for (const [id, anchors] of links) {\n for (const anchor of anchors) {\n if (id === current) anchor.setAttribute('aria-current', 'location');\n else anchor.removeAttribute('aria-current');\n }\n }\n };\n const observer = new IntersectionObserver(\n entries => {\n for (const entry of entries) {\n if (entry.isIntersecting) visible.add(entry.target.id);\n else visible.delete(entry.target.id);\n }\n mark();\n },\n { rootMargin: '-40% 0px -55% 0px' },\n );\n for (const id of sections) {\n const section = document.getElementById(id);\n if (section) observer.observe(section);\n }\n return () => observer.disconnect();\n }, [sections]);\n return null;\n}\n"],"names":["useEffect","ActiveSection","sections","links","Map","map","id","document","querySelectorAll","visible","Set","mark","current","find","has","anchors","anchor","setAttribute","removeAttribute","observer","IntersectionObserver","entries","entry","isIntersecting","add","target","delete","rootMargin","section","getElementById","observe","disconnect"],"mappings":"AAAA;AAEA,SAASA,SAAS,QAAQ,QAAQ;AAElC;;;;;;CAMC,GACD,OAAO,SAASC,cAAc,EAAEC,QAAQ,EAAmC;IACzEF,UAAU;QACR,MAAMG,QAAQ,IAAIC,IAChBF,SAASG,GAAG,CAACC,CAAAA,KAAM;gBACjBA;gBACAC,SAASC,gBAAgB,CAAoB,CAAC,gBAAgB,EAAEF,GAAG,EAAE,CAAC;aACvE;QAEH,MAAMG,UAAU,IAAIC;QACpB,MAAMC,OAAO;YACX,MAAMC,UAAUV,SAASW,IAAI,CAACP,CAAAA,KAAMG,QAAQK,GAAG,CAACR;YAChD,KAAK,MAAM,CAACA,IAAIS,QAAQ,IAAIZ,MAAO;gBACjC,KAAK,MAAMa,UAAUD,QAAS;oBAC5B,IAAIT,OAAOM,SAASI,OAAOC,YAAY,CAAC,gBAAgB;yBACnDD,OAAOE,eAAe,CAAC;gBAC9B;YACF;QACF;QACA,MAAMC,WAAW,IAAIC,qBACnBC,CAAAA;YACE,KAAK,MAAMC,SAASD,QAAS;gBAC3B,IAAIC,MAAMC,cAAc,EAAEd,QAAQe,GAAG,CAACF,MAAMG,MAAM,CAACnB,EAAE;qBAChDG,QAAQiB,MAAM,CAACJ,MAAMG,MAAM,CAACnB,EAAE;YACrC;YACAK;QACF,GACA;YAAEgB,YAAY;QAAoB;QAEpC,KAAK,MAAMrB,MAAMJ,SAAU;YACzB,MAAM0B,UAAUrB,SAASsB,cAAc,CAACvB;YACxC,IAAIsB,SAAST,SAASW,OAAO,CAACF;QAChC;QACA,OAAO,IAAMT,SAASY,UAAU;IAClC,GAAG;QAAC7B;KAAS;IACb,OAAO;AACT"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Carries the section in view across the language switch
|
|
2
|
+
* Carries the section in view across the language switch: a
|
|
3
3
|
* fragment never reaches a server, so the switch's `href` is given one as it
|
|
4
4
|
* is followed. Without scripting the visitor lands at the top of the other
|
|
5
5
|
* locale. Renders nothing.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
3
|
/**
|
|
4
|
-
* Carries the section in view across the language switch
|
|
4
|
+
* Carries the section in view across the language switch: a
|
|
5
5
|
* fragment never reaches a server, so the switch's `href` is given one as it
|
|
6
6
|
* is followed. Without scripting the visitor lands at the top of the other
|
|
7
7
|
* locale. Renders nothing.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/ui/utils/keep-section/keep-section.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect } from 'react';\n\n/**\n * Carries the section in view across the language switch
|
|
1
|
+
{"version":3,"sources":["../../../../src/ui/utils/keep-section/keep-section.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect } from 'react';\n\n/**\n * Carries the section in view across the language switch: a\n * fragment never reaches a server, so the switch's `href` is given one as it\n * is followed. Without scripting the visitor lands at the top of the other\n * locale. Renders nothing.\n */\nexport function KeepSection() {\n useEffect(() => {\n const follow = (event: MouseEvent) => {\n const link =\n event.target instanceof Element\n ? event.target.closest<HTMLAnchorElement>('a[data-keep-section]')\n : null;\n if (!link) return;\n const current = document.querySelector('a[data-section][aria-current]');\n const section = current?.getAttribute('data-section');\n link.hash = section ? section : window.location.hash;\n };\n // Capture: the `href` must be set before the browser follows it.\n document.addEventListener('click', follow, true);\n return () => document.removeEventListener('click', follow, true);\n }, []);\n return null;\n}\n"],"names":["useEffect","KeepSection","follow","event","link","target","Element","closest","current","document","querySelector","section","getAttribute","hash","window","location","addEventListener","removeEventListener"],"mappings":"AAAA;AAEA,SAASA,SAAS,QAAQ,QAAQ;AAElC;;;;;CAKC,GACD,OAAO,SAASC;IACdD,UAAU;QACR,MAAME,SAAS,CAACC;YACd,MAAMC,OACJD,MAAME,MAAM,YAAYC,UACpBH,MAAME,MAAM,CAACE,OAAO,CAAoB,0BACxC;YACN,IAAI,CAACH,MAAM;YACX,MAAMI,UAAUC,SAASC,aAAa,CAAC;YACvC,MAAMC,UAAUH,2BAAAA,QAASI,YAAY,CAAC;YACtCR,KAAKS,IAAI,GAAGF,UAAUA,UAAUG,OAAOC,QAAQ,CAACF,IAAI;QACtD;QACA,iEAAiE;QACjEJ,SAASO,gBAAgB,CAAC,SAASd,QAAQ;QAC3C,OAAO,IAAMO,SAASQ,mBAAmB,CAAC,SAASf,QAAQ;IAC7D,GAAG,EAAE;IACL,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entifix/react-controls",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Entity-agnostic React UI — table, form, filters, wizard, command palette — rendered from entity metadata, plus layout primitives.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"@headlessui/react": "^2.2.10",
|
|
20
20
|
"class-variance-authority": "^0.7.1",
|
|
21
21
|
"idb": "^8.0.4",
|
|
22
|
-
"@entifix/core": "0.
|
|
22
|
+
"@entifix/core": "0.7.0"
|
|
23
23
|
},
|
|
24
24
|
"exports": {
|
|
25
25
|
"./package.json": "./package.json",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"react": "19.3.1",
|
|
54
54
|
"react-i18next": "17.1.0",
|
|
55
55
|
"storybook": "^10.6.2",
|
|
56
|
-
"@entifix/i18n": "0.
|
|
57
|
-
"@entifix/style": "0.
|
|
56
|
+
"@entifix/i18n": "0.7.0",
|
|
57
|
+
"@entifix/style": "0.7.0"
|
|
58
58
|
},
|
|
59
59
|
"nx": {
|
|
60
60
|
"tags": [
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"i18next": ">=26 <27",
|
|
98
98
|
"react": ">=19 <20",
|
|
99
99
|
"react-i18next": ">=17 <18",
|
|
100
|
-
"@entifix/i18n": "0.
|
|
100
|
+
"@entifix/i18n": "0.7.0"
|
|
101
101
|
},
|
|
102
102
|
"peerDependenciesMeta": {
|
|
103
103
|
"@entifix/i18n": {
|