@entifix/react-controls 0.18.2 → 0.20.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/README.md +13 -5
- package/dist/i18n/catalog/en.d.ts.map +1 -1
- package/dist/i18n/catalog/en.js +68 -10
- package/dist/i18n/catalog/en.js.map +1 -1
- package/dist/i18n/catalog/es.d.ts +76 -5
- package/dist/i18n/catalog/es.d.ts.map +1 -1
- package/dist/i18n/catalog/es.js +74 -10
- package/dist/i18n/catalog/es.js.map +1 -1
- package/dist/i18n/catalog/index.d.ts +126 -10
- package/dist/i18n/catalog/index.d.ts.map +1 -1
- package/dist/i18n/entity-labels.d.ts +14 -0
- package/dist/i18n/entity-labels.d.ts.map +1 -1
- package/dist/i18n/entity-labels.js +15 -0
- package/dist/i18n/entity-labels.js.map +1 -1
- package/dist/i18n/i18n-context.js +2 -2
- package/dist/i18n/i18n-context.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/primitives.d.ts +7 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +7 -1
- package/dist/primitives.js.map +1 -1
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +1 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/theme-context.d.ts +35 -7
- package/dist/theme/theme-context.d.ts.map +1 -1
- package/dist/theme/theme-context.js +109 -21
- package/dist/theme/theme-context.js.map +1 -1
- package/dist/theme/theme-cookie.d.ts +9 -0
- package/dist/theme/theme-cookie.d.ts.map +1 -0
- package/dist/theme/theme-cookie.js +8 -0
- package/dist/theme/theme-cookie.js.map +1 -0
- package/dist/theme/theme-script.d.ts +8 -5
- package/dist/theme/theme-script.d.ts.map +1 -1
- package/dist/theme/theme-script.js +24 -10
- package/dist/theme/theme-script.js.map +1 -1
- package/dist/theme/types.d.ts +19 -3
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/theme/types.js.map +1 -1
- package/dist/ui/actions/action-surfaces.d.ts +1 -1
- package/dist/ui/actions/action-surfaces.js +1 -1
- package/dist/ui/actions/action-surfaces.js.map +1 -1
- package/dist/ui/actions/error-actions.d.ts +32 -0
- package/dist/ui/actions/error-actions.d.ts.map +1 -0
- package/dist/ui/actions/error-actions.js +40 -0
- package/dist/ui/actions/error-actions.js.map +1 -0
- package/dist/ui/actions/index.d.ts +2 -0
- package/dist/ui/actions/index.d.ts.map +1 -1
- package/dist/ui/actions/index.js +2 -0
- package/dist/ui/actions/index.js.map +1 -1
- package/dist/ui/actions/shortcut.d.ts +13 -0
- package/dist/ui/actions/shortcut.d.ts.map +1 -0
- package/dist/ui/actions/shortcut.js +18 -0
- package/dist/ui/actions/shortcut.js.map +1 -0
- package/dist/ui/actions/use-verb-invoker.d.ts +50 -0
- package/dist/ui/actions/use-verb-invoker.d.ts.map +1 -0
- package/dist/ui/actions/use-verb-invoker.js +93 -0
- package/dist/ui/actions/use-verb-invoker.js.map +1 -0
- package/dist/ui/atoms/alert/alert-dismiss.d.ts +9 -0
- package/dist/ui/atoms/alert/alert-dismiss.d.ts.map +1 -0
- package/dist/ui/atoms/alert/alert-dismiss.js +28 -0
- package/dist/ui/atoms/alert/alert-dismiss.js.map +1 -0
- package/dist/ui/atoms/alert/alert.d.ts +24 -0
- package/dist/ui/atoms/alert/alert.d.ts.map +1 -0
- package/dist/ui/atoms/alert/alert.js +50 -0
- package/dist/ui/atoms/alert/alert.js.map +1 -0
- package/dist/ui/atoms/alert/index.d.ts +3 -0
- package/dist/ui/atoms/alert/index.d.ts.map +1 -0
- package/dist/ui/atoms/alert/index.js +4 -0
- package/dist/ui/atoms/alert/index.js.map +1 -0
- package/dist/ui/atoms/alert/tone.d.ts +8 -0
- package/dist/ui/atoms/alert/tone.d.ts.map +1 -0
- package/dist/ui/atoms/alert/tone.js +11 -0
- package/dist/ui/atoms/alert/tone.js.map +1 -0
- package/dist/ui/atoms/button/button-recipe.js +1 -1
- package/dist/ui/atoms/button/button-recipe.js.map +1 -1
- package/dist/ui/atoms/button/index.d.ts +1 -0
- package/dist/ui/atoms/button/index.d.ts.map +1 -1
- package/dist/ui/atoms/button/index.js +1 -0
- package/dist/ui/atoms/button/index.js.map +1 -1
- package/dist/ui/atoms/button/submit-button.d.ts +13 -0
- package/dist/ui/atoms/button/submit-button.d.ts.map +1 -0
- package/dist/ui/atoms/button/submit-button.js +29 -0
- package/dist/ui/atoms/button/submit-button.js.map +1 -0
- package/dist/ui/atoms/cell-value/cell-value.d.ts +6 -1
- package/dist/ui/atoms/cell-value/cell-value.d.ts.map +1 -1
- package/dist/ui/atoms/cell-value/cell-value.js +48 -16
- package/dist/ui/atoms/cell-value/cell-value.js.map +1 -1
- package/dist/ui/atoms/field/checkbox.d.ts.map +1 -1
- package/dist/ui/atoms/field/checkbox.js +21 -1
- package/dist/ui/atoms/field/checkbox.js.map +1 -1
- package/dist/ui/atoms/field/field.js +1 -1
- package/dist/ui/atoms/field/field.js.map +1 -1
- package/dist/ui/atoms/field-control/field-control.d.ts +7 -1
- package/dist/ui/atoms/field-control/field-control.d.ts.map +1 -1
- package/dist/ui/atoms/field-control/field-control.js +25 -3
- package/dist/ui/atoms/field-control/field-control.js.map +1 -1
- package/dist/ui/atoms/money/index.d.ts +4 -0
- package/dist/ui/atoms/money/index.d.ts.map +1 -0
- package/dist/ui/atoms/money/index.js +5 -0
- package/dist/ui/atoms/money/index.js.map +1 -0
- package/dist/ui/atoms/money/money-input.d.ts +19 -0
- package/dist/ui/atoms/money/money-input.d.ts.map +1 -0
- package/dist/ui/atoms/money/money-input.js +75 -0
- package/dist/ui/atoms/money/money-input.js.map +1 -0
- package/dist/ui/atoms/money/money-math.d.ts +21 -0
- package/dist/ui/atoms/money/money-math.d.ts.map +1 -0
- package/dist/ui/atoms/money/money-math.js +36 -0
- package/dist/ui/atoms/money/money-math.js.map +1 -0
- package/dist/ui/atoms/money/money.d.ts +19 -0
- package/dist/ui/atoms/money/money.d.ts.map +1 -0
- package/dist/ui/atoms/money/money.js +26 -0
- package/dist/ui/atoms/money/money.js.map +1 -0
- package/dist/ui/atoms/number-input/index.d.ts +3 -0
- package/dist/ui/atoms/number-input/index.d.ts.map +1 -0
- package/dist/ui/atoms/number-input/index.js +4 -0
- package/dist/ui/atoms/number-input/index.js.map +1 -0
- package/dist/ui/atoms/number-input/number-input.d.ts +10 -0
- package/dist/ui/atoms/number-input/number-input.d.ts.map +1 -0
- package/dist/ui/atoms/number-input/number-input.js +23 -0
- package/dist/ui/atoms/number-input/number-input.js.map +1 -0
- package/dist/ui/atoms/number-input/quantity-input.d.ts +29 -0
- package/dist/ui/atoms/number-input/quantity-input.d.ts.map +1 -0
- package/dist/ui/atoms/number-input/quantity-input.js +100 -0
- package/dist/ui/atoms/number-input/quantity-input.js.map +1 -0
- package/dist/ui/atoms/skeleton/skeleton.js +1 -1
- package/dist/ui/atoms/skeleton/skeleton.js.map +1 -1
- package/dist/ui/atoms/status-tag/index.d.ts +2 -0
- package/dist/ui/atoms/status-tag/index.d.ts.map +1 -0
- package/dist/ui/atoms/status-tag/index.js +3 -0
- package/dist/ui/atoms/status-tag/index.js.map +1 -0
- package/dist/ui/atoms/status-tag/status-tag.d.ts +17 -0
- package/dist/ui/atoms/status-tag/status-tag.d.ts.map +1 -0
- package/dist/ui/atoms/status-tag/status-tag.js +37 -0
- package/dist/ui/atoms/status-tag/status-tag.js.map +1 -0
- package/dist/ui/atoms/switch/switch.d.ts.map +1 -1
- package/dist/ui/atoms/switch/switch.js +6 -3
- package/dist/ui/atoms/switch/switch.js.map +1 -1
- package/dist/ui/atoms/table/table.d.ts +12 -3
- package/dist/ui/atoms/table/table.d.ts.map +1 -1
- package/dist/ui/atoms/table/table.js +59 -10
- package/dist/ui/atoms/table/table.js.map +1 -1
- package/dist/ui/atoms/text/text.d.ts +2 -2
- package/dist/ui/atoms/text/text.d.ts.map +1 -1
- package/dist/ui/atoms/text/text.js.map +1 -1
- package/dist/ui/molecules/action-bar/action-bar.d.ts +76 -0
- package/dist/ui/molecules/action-bar/action-bar.d.ts.map +1 -0
- package/dist/ui/molecules/action-bar/action-bar.js +218 -0
- package/dist/ui/molecules/action-bar/action-bar.js.map +1 -0
- package/dist/ui/molecules/action-bar/index.d.ts +2 -0
- package/dist/ui/molecules/action-bar/index.d.ts.map +1 -0
- package/dist/ui/molecules/action-bar/index.js +3 -0
- package/dist/ui/molecules/action-bar/index.js.map +1 -0
- package/dist/ui/molecules/breadcrumbs/breadcrumbs.d.ts +11 -2
- package/dist/ui/molecules/breadcrumbs/breadcrumbs.d.ts.map +1 -1
- package/dist/ui/molecules/breadcrumbs/breadcrumbs.js +9 -7
- package/dist/ui/molecules/breadcrumbs/breadcrumbs.js.map +1 -1
- package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.d.ts.map +1 -1
- package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js +8 -10
- package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js.map +1 -1
- package/dist/ui/molecules/bulk-result/bulk-result.d.ts +3 -3
- package/dist/ui/molecules/bulk-result/bulk-result.js +4 -4
- package/dist/ui/molecules/bulk-result/bulk-result.js.map +1 -1
- package/dist/ui/molecules/column-settings/column-settings.js +1 -1
- package/dist/ui/molecules/column-settings/column-settings.js.map +1 -1
- package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts +24 -9
- package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts.map +1 -1
- package/dist/ui/molecules/confirm-dialog/confirm-dialog.js +38 -13
- package/dist/ui/molecules/confirm-dialog/confirm-dialog.js.map +1 -1
- package/dist/ui/molecules/description-list/description-list.d.ts +17 -0
- package/dist/ui/molecules/description-list/description-list.d.ts.map +1 -0
- package/dist/ui/molecules/description-list/description-list.js +36 -0
- package/dist/ui/molecules/description-list/description-list.js.map +1 -0
- package/dist/ui/molecules/description-list/index.d.ts +2 -0
- package/dist/ui/molecules/description-list/index.d.ts.map +1 -0
- package/dist/ui/molecules/description-list/index.js +3 -0
- package/dist/ui/molecules/description-list/index.js.map +1 -0
- package/dist/ui/molecules/drawer/drawer.js +3 -3
- package/dist/ui/molecules/drawer/drawer.js.map +1 -1
- package/dist/ui/molecules/empty-state/empty-state.d.ts +15 -0
- package/dist/ui/molecules/empty-state/empty-state.d.ts.map +1 -0
- package/dist/ui/molecules/empty-state/empty-state.js +29 -0
- package/dist/ui/molecules/empty-state/empty-state.js.map +1 -0
- package/dist/ui/molecules/empty-state/index.d.ts +2 -0
- package/dist/ui/molecules/empty-state/index.d.ts.map +1 -0
- package/dist/ui/molecules/empty-state/index.js +3 -0
- package/dist/ui/molecules/empty-state/index.js.map +1 -0
- package/dist/ui/molecules/entity-link-input/entity-link-input.d.ts.map +1 -1
- package/dist/ui/molecules/entity-link-input/entity-link-input.js +7 -4
- package/dist/ui/molecules/entity-link-input/entity-link-input.js.map +1 -1
- package/dist/ui/molecules/entity-link-picker/entity-link-picker.js +2 -2
- package/dist/ui/molecules/entity-link-picker/entity-link-picker.js.map +1 -1
- package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts +33 -20
- package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts.map +1 -1
- package/dist/ui/molecules/entity-record-card/entity-record-card.js +53 -20
- package/dist/ui/molecules/entity-record-card/entity-record-card.js.map +1 -1
- package/dist/ui/molecules/filter-builder/filter-builder.d.ts +8 -2
- package/dist/ui/molecules/filter-builder/filter-builder.d.ts.map +1 -1
- package/dist/ui/molecules/filter-builder/filter-builder.js +14 -5
- package/dist/ui/molecules/filter-builder/filter-builder.js.map +1 -1
- package/dist/ui/molecules/filter-chips/filter-chips.d.ts +16 -0
- package/dist/ui/molecules/filter-chips/filter-chips.d.ts.map +1 -0
- package/dist/ui/molecules/filter-chips/filter-chips.js +73 -0
- package/dist/ui/molecules/filter-chips/filter-chips.js.map +1 -0
- package/dist/ui/molecules/filter-chips/index.d.ts +2 -0
- package/dist/ui/molecules/filter-chips/index.d.ts.map +1 -0
- package/dist/ui/molecules/filter-chips/index.js +3 -0
- package/dist/ui/molecules/filter-chips/index.js.map +1 -0
- package/dist/ui/molecules/media-gallery/index.d.ts +2 -0
- package/dist/ui/molecules/media-gallery/index.d.ts.map +1 -0
- package/dist/ui/molecules/media-gallery/index.js +3 -0
- package/dist/ui/molecules/media-gallery/index.js.map +1 -0
- package/dist/ui/molecules/media-gallery/media-gallery.d.ts +31 -0
- package/dist/ui/molecules/media-gallery/media-gallery.d.ts.map +1 -0
- package/dist/ui/molecules/media-gallery/media-gallery.js +64 -0
- package/dist/ui/molecules/media-gallery/media-gallery.js.map +1 -0
- package/dist/ui/molecules/menu/menu.d.ts +11 -1
- package/dist/ui/molecules/menu/menu.d.ts.map +1 -1
- package/dist/ui/molecules/menu/menu.js +68 -6
- package/dist/ui/molecules/menu/menu.js.map +1 -1
- package/dist/ui/molecules/nav-menu/nav-menu.js +4 -4
- package/dist/ui/molecules/nav-menu/nav-menu.js.map +1 -1
- package/dist/ui/molecules/pagination/pagination.d.ts +5 -0
- package/dist/ui/molecules/pagination/pagination.d.ts.map +1 -1
- package/dist/ui/molecules/pagination/pagination.js +35 -18
- package/dist/ui/molecules/pagination/pagination.js.map +1 -1
- package/dist/ui/molecules/region-state/index.d.ts +2 -0
- package/dist/ui/molecules/region-state/index.d.ts.map +1 -0
- package/dist/ui/molecules/region-state/index.js +3 -0
- package/dist/ui/molecules/region-state/index.js.map +1 -0
- package/dist/ui/molecules/region-state/region-state.d.ts +42 -0
- package/dist/ui/molecules/region-state/region-state.d.ts.map +1 -0
- package/dist/ui/molecules/region-state/region-state.js +83 -0
- package/dist/ui/molecules/region-state/region-state.js.map +1 -0
- package/dist/ui/molecules/segmented-nav/segment-classes.d.ts +3 -3
- package/dist/ui/molecules/segmented-nav/segment-classes.d.ts.map +1 -1
- package/dist/ui/molecules/segmented-nav/segment-classes.js +2 -2
- package/dist/ui/molecules/segmented-nav/segment-classes.js.map +1 -1
- package/dist/ui/molecules/segmented-nav/segmented-nav.d.ts +14 -4
- package/dist/ui/molecules/segmented-nav/segmented-nav.d.ts.map +1 -1
- package/dist/ui/molecules/segmented-nav/segmented-nav.js +6 -5
- package/dist/ui/molecules/segmented-nav/segmented-nav.js.map +1 -1
- package/dist/ui/molecules/split-view/split-view.js +3 -3
- package/dist/ui/molecules/split-view/split-view.js.map +1 -1
- package/dist/ui/molecules/tab-strip/tab-strip.d.ts +51 -12
- package/dist/ui/molecules/tab-strip/tab-strip.d.ts.map +1 -1
- package/dist/ui/molecules/tab-strip/tab-strip.js +125 -34
- package/dist/ui/molecules/tab-strip/tab-strip.js.map +1 -1
- package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts +3 -1
- package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts.map +1 -1
- package/dist/ui/molecules/table-toolbar/table-toolbar.js +3 -2
- package/dist/ui/molecules/table-toolbar/table-toolbar.js.map +1 -1
- package/dist/ui/molecules/toast-region/toast-region.d.ts +63 -19
- package/dist/ui/molecules/toast-region/toast-region.d.ts.map +1 -1
- package/dist/ui/molecules/toast-region/toast-region.js +129 -42
- package/dist/ui/molecules/toast-region/toast-region.js.map +1 -1
- package/dist/ui/organisms/command-palette/command-palette.d.ts.map +1 -1
- package/dist/ui/organisms/command-palette/command-palette.js +25 -18
- package/dist/ui/organisms/command-palette/command-palette.js.map +1 -1
- package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.d.ts.map +1 -1
- package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js +8 -3
- package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.d.ts +1 -1
- package/dist/ui/organisms/entity-form/entity-form.d.ts.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.js +325 -215
- package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.types.d.ts +8 -0
- package/dist/ui/organisms/entity-form/entity-form.types.d.ts.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.types.js.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table.d.ts +1 -1
- package/dist/ui/organisms/entity-table/entity-table.d.ts.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table.js +334 -198
- package/dist/ui/organisms/entity-table/entity-table.js.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table.types.d.ts +19 -4
- package/dist/ui/organisms/entity-table/entity-table.types.d.ts.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table.types.js.map +1 -1
- package/dist/ui/organisms/entity-table/header-sort.d.ts +18 -0
- package/dist/ui/organisms/entity-table/header-sort.d.ts.map +1 -0
- package/dist/ui/organisms/entity-table/header-sort.js +54 -0
- package/dist/ui/organisms/entity-table/header-sort.js.map +1 -0
- package/dist/ui/organisms/entity-table/pivot.d.ts +25 -0
- package/dist/ui/organisms/entity-table/pivot.d.ts.map +1 -0
- package/dist/ui/organisms/entity-table/pivot.js +106 -0
- package/dist/ui/organisms/entity-table/pivot.js.map +1 -0
- package/dist/ui/organisms/entity-table/use-result-announcement.d.ts +13 -0
- package/dist/ui/organisms/entity-table/use-result-announcement.d.ts.map +1 -0
- package/dist/ui/organisms/entity-table/use-result-announcement.js +31 -0
- package/dist/ui/organisms/entity-table/use-result-announcement.js.map +1 -0
- package/dist/ui/organisms/top-bar/live-indicator.d.ts +11 -0
- package/dist/ui/organisms/top-bar/live-indicator.d.ts.map +1 -0
- package/dist/ui/organisms/top-bar/live-indicator.js +46 -0
- package/dist/ui/organisms/top-bar/live-indicator.js.map +1 -0
- package/dist/ui/organisms/top-bar/top-bar.d.ts +11 -2
- package/dist/ui/organisms/top-bar/top-bar.d.ts.map +1 -1
- package/dist/ui/organisms/top-bar/top-bar.js +14 -5
- package/dist/ui/organisms/top-bar/top-bar.js.map +1 -1
- package/dist/ui/organisms/wizard/wizard.d.ts +1 -1
- package/dist/ui/organisms/wizard/wizard.d.ts.map +1 -1
- package/dist/ui/organisms/wizard/wizard.js +7 -1
- package/dist/ui/organisms/wizard/wizard.js.map +1 -1
- package/dist/ui/organisms/wizard/wizard.types.d.ts +8 -0
- package/dist/ui/organisms/wizard/wizard.types.d.ts.map +1 -1
- package/dist/ui/organisms/wizard/wizard.types.js.map +1 -1
- package/dist/ui/utils/live-updates.d.ts +24 -0
- package/dist/ui/utils/live-updates.d.ts.map +1 -0
- package/dist/ui/utils/live-updates.js +20 -0
- package/dist/ui/utils/live-updates.js.map +1 -0
- package/dist/ui/utils/use-focus-on-arrival.d.ts +14 -0
- package/dist/ui/utils/use-focus-on-arrival.d.ts.map +1 -0
- package/dist/ui/utils/use-focus-on-arrival.js +42 -0
- package/dist/ui/utils/use-focus-on-arrival.js.map +1 -0
- package/dist/ui/utils/use-hotkey.d.ts +44 -25
- package/dist/ui/utils/use-hotkey.d.ts.map +1 -1
- package/dist/ui/utils/use-hotkey.js +80 -48
- package/dist/ui/utils/use-hotkey.js.map +1 -1
- package/package.json +5 -5
- package/dist/ui/molecules/split-button/index.d.ts +0 -2
- package/dist/ui/molecules/split-button/index.d.ts.map +0 -1
- package/dist/ui/molecules/split-button/index.js +0 -3
- package/dist/ui/molecules/split-button/index.js.map +0 -1
- package/dist/ui/molecules/split-button/split-button.d.ts +0 -21
- package/dist/ui/molecules/split-button/split-button.d.ts.map +0 -1
- package/dist/ui/molecules/split-button/split-button.js +0 -35
- package/dist/ui/molecules/split-button/split-button.js.map +0 -1
|
@@ -1,37 +1,64 @@
|
|
|
1
1
|
'use client';
|
|
2
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
3
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
5
|
import { useErrorMessage, useT } from '../../../i18n/index.js';
|
|
6
|
+
import { useErrorAction } from '../../actions/error-actions.js';
|
|
7
|
+
import { Alert } from '../../atoms/alert/index.js';
|
|
5
8
|
import { Button } from '../../atoms/button/index.js';
|
|
9
|
+
import { cn } from '../../utils/cn.js';
|
|
10
|
+
/**
|
|
11
|
+
* How long a toast that is neither a failure nor offers an action stays, in
|
|
12
|
+
* milliseconds. A failure stays until it is dismissed: it reports something
|
|
13
|
+
* that already happened, often after the screen that started it was left, and
|
|
14
|
+
* a message that times out is one a slow reader never gets. A toast with an
|
|
15
|
+
* action stays too, for the same reader.
|
|
16
|
+
*/ export const TOAST_TIMEOUT_MS = 8000;
|
|
6
17
|
const ToastContext = /*#__PURE__*/ createContext(undefined);
|
|
7
18
|
/**
|
|
8
19
|
* Holds the toasts one host shows. Mounted once, above everything that can
|
|
9
20
|
* fail asynchronously, with a {@link ToastRegion} somewhere inside it.
|
|
10
|
-
*/ export function ToastProvider({ children }) {
|
|
11
|
-
const [
|
|
21
|
+
*/ export function ToastProvider({ children, timeoutMs = TOAST_TIMEOUT_MS }) {
|
|
22
|
+
const [toasts, setToasts] = useState([]);
|
|
12
23
|
const next = useRef(0);
|
|
13
|
-
const
|
|
24
|
+
const timers = useRef(new Map());
|
|
25
|
+
const dismiss = useCallback((id)=>{
|
|
26
|
+
clearTimeout(timers.current.get(id));
|
|
27
|
+
timers.current.delete(id);
|
|
28
|
+
setToasts((shown)=>shown.filter((toast)=>toast.id !== id));
|
|
29
|
+
}, []);
|
|
30
|
+
const show = useCallback((toast)=>{
|
|
14
31
|
next.current += 1;
|
|
15
32
|
const id = next.current;
|
|
16
|
-
|
|
33
|
+
setToasts((shown)=>[
|
|
17
34
|
...shown,
|
|
18
|
-
{
|
|
19
|
-
id
|
|
20
|
-
|
|
21
|
-
}
|
|
35
|
+
_extends({}, toast, {
|
|
36
|
+
id
|
|
37
|
+
})
|
|
22
38
|
]);
|
|
39
|
+
// A failure stays until it is dismissed, and so does a toast that
|
|
40
|
+
// offers an action: Undo on a timer is a time limit on the reader's
|
|
41
|
+
// only way back (WCAG 2.2.1). The rest say their piece and go.
|
|
42
|
+
if (toast.tone !== 'failure' && toast.action === undefined) timers.current.set(id, setTimeout(()=>dismiss(id), timeoutMs));
|
|
23
43
|
return id;
|
|
24
|
-
}, [
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
}, [
|
|
45
|
+
dismiss,
|
|
46
|
+
timeoutMs
|
|
47
|
+
]);
|
|
48
|
+
useEffect(()=>{
|
|
49
|
+
const pending = timers.current;
|
|
50
|
+
return ()=>{
|
|
51
|
+
for (const timer of pending.values())clearTimeout(timer);
|
|
52
|
+
pending.clear();
|
|
53
|
+
};
|
|
27
54
|
}, []);
|
|
28
55
|
const value = useMemo(()=>({
|
|
29
|
-
|
|
30
|
-
|
|
56
|
+
toasts,
|
|
57
|
+
show,
|
|
31
58
|
dismiss
|
|
32
59
|
}), [
|
|
33
|
-
|
|
34
|
-
|
|
60
|
+
toasts,
|
|
61
|
+
show,
|
|
35
62
|
dismiss
|
|
36
63
|
]);
|
|
37
64
|
return /*#__PURE__*/ _jsx(ToastContext.Provider, {
|
|
@@ -41,41 +68,101 @@ const ToastContext = /*#__PURE__*/ createContext(undefined);
|
|
|
41
68
|
}
|
|
42
69
|
/**
|
|
43
70
|
* The {@link ToastProvider} above, or `undefined` outside one: a caller with
|
|
44
|
-
* no region to show a
|
|
45
|
-
* vanish.
|
|
71
|
+
* no region to show a toast in keeps what it has to say where it can, rather
|
|
72
|
+
* than having it vanish.
|
|
46
73
|
*/ export const useToasts = ()=>useContext(ToastContext);
|
|
74
|
+
const ALERT_TONE = {
|
|
75
|
+
success: 'success',
|
|
76
|
+
undo: 'info',
|
|
77
|
+
info: 'info',
|
|
78
|
+
failure: 'danger'
|
|
79
|
+
};
|
|
80
|
+
function ToastItem({ toast, onDismiss }) {
|
|
81
|
+
var _toast_action;
|
|
82
|
+
const t = useT();
|
|
83
|
+
const message = useErrorMessage();
|
|
84
|
+
const errorAction = useErrorAction();
|
|
85
|
+
// A failure with no action of its own offers the host's next step for its
|
|
86
|
+
// code, when there is one.
|
|
87
|
+
const next = toast.tone === 'failure' ? errorAction(toast.failure) : undefined;
|
|
88
|
+
const action = (_toast_action = toast.action) != null ? _toast_action : next === undefined ? undefined : {
|
|
89
|
+
label: next.label,
|
|
90
|
+
onAction: next.onAction
|
|
91
|
+
};
|
|
92
|
+
return /*#__PURE__*/ _jsx(Alert, {
|
|
93
|
+
tone: ALERT_TONE[toast.tone],
|
|
94
|
+
"data-toast-tone": toast.tone,
|
|
95
|
+
className: "pointer-events-auto bg-surface shadow-overlay",
|
|
96
|
+
action: /*#__PURE__*/ _jsxs("div", {
|
|
97
|
+
className: "flex gap-3xs",
|
|
98
|
+
children: [
|
|
99
|
+
action !== undefined && /*#__PURE__*/ _jsx(Button, {
|
|
100
|
+
type: "button",
|
|
101
|
+
variant: "secondary",
|
|
102
|
+
onClick: ()=>{
|
|
103
|
+
action.onAction();
|
|
104
|
+
onDismiss();
|
|
105
|
+
},
|
|
106
|
+
children: action.label
|
|
107
|
+
}),
|
|
108
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
109
|
+
type: "button",
|
|
110
|
+
variant: "ghost",
|
|
111
|
+
onClick: onDismiss,
|
|
112
|
+
children: t('toast.dismiss')
|
|
113
|
+
})
|
|
114
|
+
]
|
|
115
|
+
}),
|
|
116
|
+
children: toast.tone === 'failure' ? message(toast.failure) : toast.message
|
|
117
|
+
});
|
|
118
|
+
}
|
|
47
119
|
/**
|
|
48
|
-
* Where a host's toasts render
|
|
49
|
-
*
|
|
50
|
-
* asynchronous failure reads exactly as a synchronous one would.
|
|
120
|
+
* Where a host's toasts render, each through {@link Alert}: a failure is
|
|
121
|
+
* `role="alert"` and every other tone `role="status"`.
|
|
51
122
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
123
|
+
* ⚠️ **An empty region takes no room and no clicks.** It stays mounted, so a
|
|
124
|
+
* toast lands in a region that already exists, but at zero size and with
|
|
125
|
+
* `pointer-events: none` — a fixed box that kept its padding once covered two
|
|
126
|
+
* focused links and swallowed clicks (WCAG 2.4.11). While toasts show, the
|
|
127
|
+
* document's bottom scroll padding is the region's height, so a control
|
|
128
|
+
* focused by keyboard scrolls clear of it. F6 moves focus into the region.
|
|
55
129
|
*/ export function ToastRegion() {
|
|
130
|
+
var _ref;
|
|
56
131
|
const toasts = useToasts();
|
|
57
132
|
const t = useT();
|
|
58
|
-
const
|
|
133
|
+
const region = useRef(null);
|
|
134
|
+
const shown = (_ref = toasts == null ? void 0 : toasts.toasts) != null ? _ref : [];
|
|
135
|
+
const count = shown.length;
|
|
136
|
+
useEffect(()=>{
|
|
137
|
+
const root = document.documentElement;
|
|
138
|
+
if (count === 0) {
|
|
139
|
+
root.style.removeProperty('scroll-padding-bottom');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// The region renders whenever a toast can: the ref is set by now.
|
|
143
|
+
const node = region.current;
|
|
144
|
+
root.style.setProperty('scroll-padding-bottom', `${node.offsetHeight}px`);
|
|
145
|
+
const onKeyDown = (event)=>{
|
|
146
|
+
if (event.key !== 'F6') return;
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
node.focus();
|
|
149
|
+
};
|
|
150
|
+
document.addEventListener('keydown', onKeyDown);
|
|
151
|
+
return ()=>document.removeEventListener('keydown', onKeyDown);
|
|
152
|
+
}, [
|
|
153
|
+
count
|
|
154
|
+
]);
|
|
59
155
|
if (toasts === undefined) return null;
|
|
60
156
|
return /*#__PURE__*/ _jsx("section", {
|
|
157
|
+
ref: region,
|
|
61
158
|
"aria-label": t('toast.region'),
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
children: message(failure)
|
|
70
|
-
}),
|
|
71
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
72
|
-
type: "button",
|
|
73
|
-
variant: "ghost",
|
|
74
|
-
onClick: ()=>toasts.dismiss(id),
|
|
75
|
-
children: t('toast.dismiss')
|
|
76
|
-
})
|
|
77
|
-
]
|
|
78
|
-
}, id))
|
|
159
|
+
tabIndex: -1,
|
|
160
|
+
"data-testid": "toast-region",
|
|
161
|
+
className: cn('bottom-0 right-0 pointer-events-none fixed z-(--z-toast) flex flex-col gap-xs focus-ring', count === 0 ? 'size-0 overflow-hidden' : 'max-w-md w-full p-s'),
|
|
162
|
+
children: shown.map((toast)=>/*#__PURE__*/ _jsx(ToastItem, {
|
|
163
|
+
toast: toast,
|
|
164
|
+
onDismiss: ()=>toasts.dismiss(toast.id)
|
|
165
|
+
}, toast.id))
|
|
79
166
|
});
|
|
80
167
|
}
|
|
81
168
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/ui/molecules/toast-region/toast-region.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { useErrorMessage, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\n\n/**\n * A failure as {@link useErrorMessage} renders it: a typed failure carries its\n * `code`, and anything else that reached the user keeps its `message`.\n */\nexport type ToastFailure = Parameters<ReturnType<typeof useErrorMessage>>[0];\n\n/** One failure waiting in the region, under the id it was shown with. */\nexport interface FailureToast {\n readonly id: number;\n readonly failure: ToastFailure;\n}\n\nexport interface Toasts {\n /** The failures shown, oldest first. */\n readonly failures: readonly FailureToast[];\n /** Shows a failure until it is dismissed, and answers its id. */\n readonly showFailure: (failure: ToastFailure) => number;\n readonly dismiss: (id: number) => void;\n}\n\nconst ToastContext = createContext<Toasts | undefined>(undefined);\n\n/**\n * Holds the toasts one host shows. Mounted once, above everything that can\n * fail asynchronously, with a {@link ToastRegion} somewhere inside it.\n */\nexport function ToastProvider({ children }: { readonly children: ReactNode }) {\n const [failures, setFailures] = useState<readonly FailureToast[]>([]);\n const next = useRef(0);\n\n const showFailure = useCallback((failure: ToastFailure) => {\n next.current += 1;\n const id = next.current;\n setFailures(shown => [...shown, { id, failure }]);\n return id;\n }, []);\n const dismiss = useCallback((id: number) => {\n setFailures(shown => shown.filter(toast => toast.id !== id));\n }, []);\n\n const value = useMemo(\n () => ({ failures, showFailure, dismiss }),\n [failures, showFailure, dismiss],\n );\n return (\n <ToastContext.Provider value={value}>{children}</ToastContext.Provider>\n );\n}\n\n/**\n * The {@link ToastProvider} above, or `undefined` outside one: a caller with\n * no region to show a failure in keeps it where it can, rather than having it\n * vanish.\n */\nexport const useToasts = (): Toasts | undefined => useContext(ToastContext);\n\n/**\n * Where a host's toasts render: each failure in the reader's language,\n * through the same {@link useErrorMessage} a form's error slot uses, so an\n * asynchronous failure reads exactly as a synchronous one would.\n *\n * A failure stays until it is dismissed. It reports something that already\n * happened, often after the screen that started it was left, and a message\n * that times out is one a slow reader never gets.\n */\nexport function ToastRegion() {\n const toasts = useToasts();\n const t = useT();\n const message = useErrorMessage();\n\n if (toasts === undefined) return null;\n return (\n <section\n aria-label={t('toast.region')}\n className=\"bottom-0 right-0 max-w-md fixed z-50 flex w-full flex-col gap-xs p-s\"\n >\n {toasts.failures.map(({ id, failure }) => (\n <div\n key={id}\n role=\"alert\"\n className=\"flex items-start gap-xs rounded-lg border border-danger bg-surface p-xs shadow-overlay\"\n >\n <p className=\"flex-1 text-step-sm text-danger\">{message(failure)}</p>\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => toasts.dismiss(id)}\n >\n {t('toast.dismiss')}\n </Button>\n </div>\n ))}\n </section>\n );\n}\n"],"names":["createContext","useCallback","useContext","useMemo","useRef","useState","useErrorMessage","useT","Button","ToastContext","undefined","ToastProvider","children","failures","setFailures","next","showFailure","failure","current","id","shown","dismiss","filter","toast","value","Provider","useToasts","ToastRegion","toasts","t","message","section","aria-label","className","map","div","role","p","type","variant","onClick"],"mappings":"AAAA;;AAEA,SACEA,aAAa,EAEbC,WAAW,EACXC,UAAU,EACVC,OAAO,EACPC,MAAM,EACNC,QAAQ,QACH,QAAQ;AAEf,SAASC,eAAe,EAAEC,IAAI,QAAQ,yBAAyB;AAC/D,SAASC,MAAM,QAAQ,8BAA8B;AAsBrD,MAAMC,6BAAeT,cAAkCU;AAEvD;;;CAGC,GACD,OAAO,SAASC,cAAc,EAAEC,QAAQ,EAAoC;IAC1E,MAAM,CAACC,UAAUC,YAAY,GAAGT,SAAkC,EAAE;IACpE,MAAMU,OAAOX,OAAO;IAEpB,MAAMY,cAAcf,YAAY,CAACgB;QAC/BF,KAAKG,OAAO,IAAI;QAChB,MAAMC,KAAKJ,KAAKG,OAAO;QACvBJ,YAAYM,CAAAA,QAAS;mBAAIA;gBAAO;oBAAED;oBAAIF;gBAAQ;aAAE;QAChD,OAAOE;IACT,GAAG,EAAE;IACL,MAAME,UAAUpB,YAAY,CAACkB;QAC3BL,YAAYM,CAAAA,QAASA,MAAME,MAAM,CAACC,CAAAA,QAASA,MAAMJ,EAAE,KAAKA;IAC1D,GAAG,EAAE;IAEL,MAAMK,QAAQrB,QACZ,IAAO,CAAA;YAAEU;YAAUG;YAAaK;QAAQ,CAAA,GACxC;QAACR;QAAUG;QAAaK;KAAQ;IAElC,qBACE,KAACZ,aAAagB,QAAQ;QAACD,OAAOA;kBAAQZ;;AAE1C;AAEA;;;;CAIC,GACD,OAAO,MAAMc,YAAY,IAA0BxB,WAAWO,cAAc;AAE5E;;;;;;;;CAQC,GACD,OAAO,SAASkB;IACd,MAAMC,SAASF;IACf,MAAMG,IAAItB;IACV,MAAMuB,UAAUxB;IAEhB,IAAIsB,WAAWlB,WAAW,OAAO;IACjC,qBACE,KAACqB;QACCC,cAAYH,EAAE;QACdI,WAAU;kBAETL,OAAOf,QAAQ,CAACqB,GAAG,CAAC,CAAC,EAAEf,EAAE,EAAEF,OAAO,EAAE,iBACnC,MAACkB;gBAECC,MAAK;gBACLH,WAAU;;kCAEV,KAACI;wBAAEJ,WAAU;kCAAmCH,QAAQb;;kCACxD,KAACT;wBACC8B,MAAK;wBACLC,SAAQ;wBACRC,SAAS,IAAMZ,OAAOP,OAAO,CAACF;kCAE7BU,EAAE;;;eAVAV;;AAgBf"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/ui/molecules/toast-region/toast-region.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { useErrorMessage, useT } from '../../../i18n/index.js';\nimport { useErrorAction } from '../../actions/error-actions.js';\nimport { Alert, type AlertTone } from '../../atoms/alert/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { cn } from '../../utils/cn.js';\n\n/**\n * A failure as {@link useErrorMessage} renders it: a typed failure carries its\n * `code`, and anything else that reached the user keeps its `message`.\n */\nexport type ToastFailure = Parameters<ReturnType<typeof useErrorMessage>>[0];\n\n/**\n * What a toast reports\n * ([ADR 0028](../../../../../../../docs/adr/0028-states-an-organism-must-render.md)):\n * a write that worked, one that can still be taken back, something worth\n * knowing, or a failure. Only a failure interrupts.\n */\nexport type ToastTone = 'success' | 'undo' | 'info' | 'failure';\n\n/** The one thing a toast offers to do about what it reports. */\nexport interface ToastAction {\n /** The button's text, already in the reader's language. */\n readonly label: string;\n readonly onAction: () => void;\n}\n\n/**\n * A toast to show. A failure carries the failure itself, rendered through the\n * same {@link useErrorMessage} a form's error slot uses; every other tone\n * carries a sentence already in the reader's language. An `undo` toast without\n * its Undo would be a success toast, so the type asks for the action.\n */\nexport type ToastInput =\n | {\n readonly tone: 'success' | 'info';\n readonly message: string;\n readonly action?: ToastAction;\n }\n | {\n readonly tone: 'undo';\n readonly message: string;\n readonly action: ToastAction;\n }\n | {\n readonly tone: 'failure';\n readonly failure: ToastFailure;\n readonly action?: ToastAction;\n };\n\n/** One toast waiting in the region, under the id it was shown with. */\nexport type Toast = ToastInput & { readonly id: number };\n\nexport interface Toasts {\n /** The toasts shown, oldest first. */\n readonly toasts: readonly Toast[];\n /** Shows a toast, and answers its id. */\n readonly show: (toast: ToastInput) => number;\n readonly dismiss: (id: number) => void;\n}\n\n/**\n * How long a toast that is neither a failure nor offers an action stays, in\n * milliseconds. A failure stays until it is dismissed: it reports something\n * that already happened, often after the screen that started it was left, and\n * a message that times out is one a slow reader never gets. A toast with an\n * action stays too, for the same reader.\n */\nexport const TOAST_TIMEOUT_MS = 8000;\n\nconst ToastContext = createContext<Toasts | undefined>(undefined);\n\nexport interface ToastProviderProps {\n readonly children: ReactNode;\n /** How long a success, undo or info toast stays. */\n readonly timeoutMs?: number;\n}\n\n/**\n * Holds the toasts one host shows. Mounted once, above everything that can\n * fail asynchronously, with a {@link ToastRegion} somewhere inside it.\n */\nexport function ToastProvider({\n children,\n timeoutMs = TOAST_TIMEOUT_MS,\n}: ToastProviderProps) {\n const [toasts, setToasts] = useState<readonly Toast[]>([]);\n const next = useRef(0);\n const timers = useRef(new Map<number, ReturnType<typeof setTimeout>>());\n\n const dismiss = useCallback((id: number) => {\n clearTimeout(timers.current.get(id));\n timers.current.delete(id);\n setToasts(shown => shown.filter(toast => toast.id !== id));\n }, []);\n\n const show = useCallback(\n (toast: ToastInput) => {\n next.current += 1;\n const id = next.current;\n setToasts(shown => [...shown, { ...toast, id }]);\n // A failure stays until it is dismissed, and so does a toast that\n // offers an action: Undo on a timer is a time limit on the reader's\n // only way back (WCAG 2.2.1). The rest say their piece and go.\n if (toast.tone !== 'failure' && toast.action === undefined)\n timers.current.set(\n id,\n setTimeout(() => dismiss(id), timeoutMs),\n );\n return id;\n },\n [dismiss, timeoutMs],\n );\n\n useEffect(() => {\n const pending = timers.current;\n return () => {\n for (const timer of pending.values()) clearTimeout(timer);\n pending.clear();\n };\n }, []);\n\n const value = useMemo(\n () => ({ toasts, show, dismiss }),\n [toasts, show, dismiss],\n );\n return (\n <ToastContext.Provider value={value}>{children}</ToastContext.Provider>\n );\n}\n\n/**\n * The {@link ToastProvider} above, or `undefined` outside one: a caller with\n * no region to show a toast in keeps what it has to say where it can, rather\n * than having it vanish.\n */\nexport const useToasts = (): Toasts | undefined => useContext(ToastContext);\n\nconst ALERT_TONE: Readonly<Record<ToastTone, AlertTone>> = {\n success: 'success',\n undo: 'info',\n info: 'info',\n failure: 'danger',\n};\n\nfunction ToastItem({\n toast,\n onDismiss,\n}: {\n readonly toast: Toast;\n readonly onDismiss: () => void;\n}) {\n const t = useT();\n const message = useErrorMessage();\n const errorAction = useErrorAction();\n // A failure with no action of its own offers the host's next step for its\n // code, when there is one.\n const next =\n toast.tone === 'failure' ? errorAction(toast.failure) : undefined;\n const action =\n toast.action ??\n (next === undefined\n ? undefined\n : { label: next.label, onAction: next.onAction });\n return (\n <Alert\n tone={ALERT_TONE[toast.tone]}\n data-toast-tone={toast.tone}\n className=\"pointer-events-auto bg-surface shadow-overlay\"\n action={\n <div className=\"flex gap-3xs\">\n {action !== undefined && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={() => {\n action.onAction();\n onDismiss();\n }}\n >\n {action.label}\n </Button>\n )}\n <Button type=\"button\" variant=\"ghost\" onClick={onDismiss}>\n {t('toast.dismiss')}\n </Button>\n </div>\n }\n >\n {toast.tone === 'failure' ? message(toast.failure) : toast.message}\n </Alert>\n );\n}\n\n/**\n * Where a host's toasts render, each through {@link Alert}: a failure is\n * `role=\"alert\"` and every other tone `role=\"status\"`.\n *\n * ⚠️ **An empty region takes no room and no clicks.** It stays mounted, so a\n * toast lands in a region that already exists, but at zero size and with\n * `pointer-events: none` — a fixed box that kept its padding once covered two\n * focused links and swallowed clicks (WCAG 2.4.11). While toasts show, the\n * document's bottom scroll padding is the region's height, so a control\n * focused by keyboard scrolls clear of it. F6 moves focus into the region.\n */\nexport function ToastRegion() {\n const toasts = useToasts();\n const t = useT();\n const region = useRef<HTMLElement>(null);\n const shown = toasts?.toasts ?? [];\n const count = shown.length;\n\n useEffect(() => {\n const root = document.documentElement;\n if (count === 0) {\n root.style.removeProperty('scroll-padding-bottom');\n return;\n }\n // The region renders whenever a toast can: the ref is set by now.\n const node = region.current as HTMLElement;\n root.style.setProperty('scroll-padding-bottom', `${node.offsetHeight}px`);\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'F6') return;\n event.preventDefault();\n node.focus();\n };\n document.addEventListener('keydown', onKeyDown);\n return () => document.removeEventListener('keydown', onKeyDown);\n }, [count]);\n\n if (toasts === undefined) return null;\n return (\n <section\n ref={region}\n aria-label={t('toast.region')}\n tabIndex={-1}\n data-testid=\"toast-region\"\n className={cn(\n 'bottom-0 right-0 pointer-events-none fixed z-(--z-toast) flex flex-col gap-xs focus-ring',\n count === 0 ? 'size-0 overflow-hidden' : 'max-w-md w-full p-s',\n )}\n >\n {shown.map(toast => (\n <ToastItem\n key={toast.id}\n toast={toast}\n onDismiss={() => toasts.dismiss(toast.id)}\n />\n ))}\n </section>\n );\n}\n"],"names":["createContext","useCallback","useContext","useEffect","useMemo","useRef","useState","useErrorMessage","useT","useErrorAction","Alert","Button","cn","TOAST_TIMEOUT_MS","ToastContext","undefined","ToastProvider","children","timeoutMs","toasts","setToasts","next","timers","Map","dismiss","id","clearTimeout","current","get","delete","shown","filter","toast","show","tone","action","set","setTimeout","pending","timer","values","clear","value","Provider","useToasts","ALERT_TONE","success","undo","info","failure","ToastItem","onDismiss","t","message","errorAction","label","onAction","data-toast-tone","className","div","type","variant","onClick","ToastRegion","region","count","length","root","document","documentElement","style","removeProperty","node","setProperty","offsetHeight","onKeyDown","event","key","preventDefault","focus","addEventListener","removeEventListener","section","ref","aria-label","tabIndex","data-testid","map"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/command-palette/command-palette.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,eAAe,CAAC;AAevB,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,+DAA+D;IAC/D,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9B;;;;;;;OAOG;IACH,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,aAAa,EAAE,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;;OAGG;IACH,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,MAAM,EAAE,oBAAoB,CAAC;CAC9B;
|
|
1
|
+
{"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/command-palette/command-palette.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,eAAe,CAAC;AAevB,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,+DAA+D;IAC/D,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9B;;;;;;;OAOG;IACH,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,aAAa,EAAE,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;;OAGG;IACH,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,MAAM,EAAE,oBAAoB,CAAC;CAC9B;AA2GD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,OAAO,EACP,KAAK,EACL,KAAK,EACL,aAAa,EACb,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,MAAM,GACP,EAAE,mBAAmB,+BAqJrB"}
|
|
@@ -18,7 +18,7 @@ function GroupHeading({ label }) {
|
|
|
18
18
|
function CommandRow({ option }) {
|
|
19
19
|
return /*#__PURE__*/ _jsxs(ComboboxOption, {
|
|
20
20
|
value: option,
|
|
21
|
-
className: cn('flex cursor-pointer items-baseline gap-2xs rounded-md px-2xs py-3xs', 'text-step-sm text-content', 'data-focus:bg-surface'),
|
|
21
|
+
className: cn('flex min-h-(--target-min) cursor-pointer items-baseline gap-2xs rounded-md px-2xs py-3xs', 'text-step-sm text-content', 'focus-ring data-focus:bg-surface data-focus:-outline-offset-2'),
|
|
22
22
|
children: [
|
|
23
23
|
/*#__PURE__*/ _jsx("span", {
|
|
24
24
|
className: "truncate",
|
|
@@ -43,18 +43,14 @@ function Group({ group, labels }) {
|
|
|
43
43
|
/*#__PURE__*/ _jsx(GroupHeading, {
|
|
44
44
|
label: group.label
|
|
45
45
|
}),
|
|
46
|
-
group.isLoading &&
|
|
46
|
+
group.isLoading && // The wait is said once, by the palette's status region: a listbox
|
|
47
|
+
// holds options, and text inside it is read as none of them.
|
|
48
|
+
/*#__PURE__*/ _jsx("div", {
|
|
47
49
|
role: "presentation",
|
|
48
50
|
className: "px-2xs py-3xs",
|
|
49
|
-
children:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
children: labels.loading
|
|
53
|
-
}),
|
|
54
|
-
/*#__PURE__*/ _jsx(SkeletonText, {
|
|
55
|
-
lines: 2
|
|
56
|
-
})
|
|
57
|
-
]
|
|
51
|
+
children: /*#__PURE__*/ _jsx(SkeletonText, {
|
|
52
|
+
lines: 2
|
|
53
|
+
})
|
|
58
54
|
}),
|
|
59
55
|
group.options.map((option)=>/*#__PURE__*/ _jsx(CommandRow, {
|
|
60
56
|
option: option
|
|
@@ -110,6 +106,7 @@ function Group({ group, labels }) {
|
|
|
110
106
|
// that fires after the bug is reported.
|
|
111
107
|
groups.forEach((group)=>group.options.forEach(commandEffectOf));
|
|
112
108
|
const isEmpty = groups.every(groupIsEmpty);
|
|
109
|
+
const isAnswering = groups.some((group)=>group.isLoading);
|
|
113
110
|
const pop = ()=>{
|
|
114
111
|
if (stack.length === 0) {
|
|
115
112
|
onClose();
|
|
@@ -163,15 +160,15 @@ function Group({ group, labels }) {
|
|
|
163
160
|
// Headless UI routes Escape here. Popping rather than closing is what
|
|
164
161
|
// makes the stack feel like a stack; at the root the two are the same.
|
|
165
162
|
onClose: pop,
|
|
166
|
-
className: "inset-0 fixed z-
|
|
163
|
+
className: "inset-0 fixed z-(--z-palette) flex items-start justify-center overflow-y-auto p-s pt-(--overlay-offset-block)",
|
|
167
164
|
children: [
|
|
168
165
|
/*#__PURE__*/ _jsx("div", {
|
|
169
166
|
"aria-hidden": true,
|
|
170
|
-
className: "inset-0 bg-
|
|
167
|
+
className: "inset-0 fixed bg-scrim"
|
|
171
168
|
}),
|
|
172
169
|
/*#__PURE__*/ _jsxs(DialogPanel, {
|
|
173
170
|
"data-testid": "command-palette",
|
|
174
|
-
className: "relative w-full max-w-xl overflow-hidden rounded-lg border border-border bg-surface-elevated shadow-overlay",
|
|
171
|
+
className: "relative w-full max-w-(--container-xl) overflow-hidden rounded-lg border border-border bg-surface-elevated shadow-overlay",
|
|
175
172
|
children: [
|
|
176
173
|
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
177
174
|
className: "sr-only",
|
|
@@ -185,9 +182,11 @@ function Group({ group, labels }) {
|
|
|
185
182
|
children: [
|
|
186
183
|
page.title !== undefined && /*#__PURE__*/ _jsx("button", {
|
|
187
184
|
type: "button",
|
|
188
|
-
"
|
|
185
|
+
// Its name holds the text it shows (WCAG 2.5.3): "Volver" is
|
|
186
|
+
// said, and "Nuevo" is both seen and said.
|
|
187
|
+
"aria-label": `${labels.back} ${page.title}`,
|
|
189
188
|
onClick: pop,
|
|
190
|
-
className: "shrink-0 rounded-md bg-surface px-2xs py-3xs text-step-xs text-content-muted focus-ring",
|
|
189
|
+
className: "inline-flex min-h-(--target-min) shrink-0 items-center rounded-md bg-surface px-2xs py-3xs text-step-xs text-content-muted focus-ring",
|
|
191
190
|
children: page.title
|
|
192
191
|
}),
|
|
193
192
|
/*#__PURE__*/ _jsx(ComboboxInput, {
|
|
@@ -197,17 +196,25 @@ function Group({ group, labels }) {
|
|
|
197
196
|
value: term,
|
|
198
197
|
onChange: (event)=>onTermChange(event.currentTarget.value),
|
|
199
198
|
onKeyDown: handleKeyDown,
|
|
200
|
-
className: "flex-1 bg-transparent py-3xs text-step-1 text-content placeholder:text-content-muted
|
|
199
|
+
className: "flex-1 rounded-sm bg-transparent py-3xs text-step-1 text-content focus-ring placeholder:text-content-muted"
|
|
201
200
|
})
|
|
202
201
|
]
|
|
203
202
|
}),
|
|
203
|
+
/*#__PURE__*/ _jsx("span", {
|
|
204
|
+
role: "status",
|
|
205
|
+
className: "sr-only",
|
|
206
|
+
children: isEmpty ? labels.empty : isAnswering ? labels.loading : ''
|
|
207
|
+
}),
|
|
204
208
|
/*#__PURE__*/ _jsx(ComboboxOptions, {
|
|
205
209
|
static: true,
|
|
206
|
-
|
|
210
|
+
// Still filling: a listbox with no options yet is not a broken one.
|
|
211
|
+
"aria-busy": isAnswering || undefined,
|
|
212
|
+
className: cn('max-h-(--overlay-max-block) overflow-y-auto p-3xs', // The panel is a focus-managed CONTAINER, not a target: the ring
|
|
207
213
|
// belongs on the row inside it.
|
|
208
214
|
'focus:outline-none'),
|
|
209
215
|
children: isEmpty ? /*#__PURE__*/ _jsx("div", {
|
|
210
216
|
role: "presentation",
|
|
217
|
+
"aria-hidden": "true",
|
|
211
218
|
className: "px-2xs py-s text-step-sm text-content-muted",
|
|
212
219
|
children: labels.empty
|
|
213
220
|
}) : groups.filter((group)=>!groupIsEmpty(group)).map((group)=>/*#__PURE__*/ _jsx(Group, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/ui/organisms/command-palette/command-palette.tsx"],"sourcesContent":["'use client';\n\nimport {\n commandEffectOf,\n type CommandGroup,\n type CommandOption,\n type CommandPage,\n} from '@entifix/core';\nimport {\n Combobox,\n ComboboxInput,\n ComboboxOption,\n ComboboxOptions,\n Dialog,\n DialogPanel,\n DialogTitle,\n} from '@headlessui/react';\nimport type { KeyboardEvent } from 'react';\n\nimport { SkeletonText } from '../../atoms/skeleton/index.js';\nimport { cn } from '../../utils/cn.js';\n\n/** Every string the palette renders. Resolved — this control does not translate. */\nexport interface CommandPaletteLabels {\n /** Names the dialog. A dialog with no accessible name is announced as \"dialog\". */\n readonly title: string;\n /** Nothing matched, and every source said so. */\n readonly empty: string;\n /** Announced while a group is still answering. */\n readonly loading: string;\n /** \"N más\" — a function, so the plural rule stays in the catalog. */\n readonly more: (remaining: number) => string;\n /** Leaves a pushed page. Also the chip's accessible name. */\n readonly back: string;\n}\n\nexport interface CommandPaletteProps {\n open: boolean;\n onClose: () => void;\n /** Every page this palette can show. The root is the first. */\n pages: readonly CommandPage[];\n /**\n * The pushed page ids, outermost first — empty means the root.\n *\n * Controlled, so the host can empty it in the same handler that opens the\n * palette. An internal stack would have to be reset from an effect keyed on\n * `open`, which is setting state in a passive effect for something that is a\n * fact about the event that opened it.\n */\n stack: readonly string[];\n onStackChange: (stack: readonly string[]) => void;\n term: string;\n onTermChange: (term: string) => void;\n /**\n * An option was chosen. The host runs it — this control has no router and\n * cannot navigate, which is the `hrefFor` rule.\n */\n onSelect: (option: CommandOption) => void;\n labels: CommandPaletteLabels;\n}\n\nconst groupIsEmpty = (group: CommandGroup): boolean =>\n group.options.length === 0 &&\n !group.isLoading &&\n group.unavailable === undefined;\n\nfunction GroupHeading({ label }: { label: string }) {\n return (\n // `presentation`, not a heading: everything inside a `listbox` that is not\n // an `option` must be presentational, or assistive tech counts the heading\n // as a selectable row and the option count it announces is wrong.\n <div\n role=\"presentation\"\n className=\"tracking-wide px-2xs pt-2xs pb-3xs text-step-xs font-semibold text-content-muted uppercase\"\n >\n {label}\n </div>\n );\n}\n\nfunction CommandRow({ option }: { option: CommandOption }) {\n return (\n <ComboboxOption\n value={option}\n className={cn(\n 'flex cursor-pointer items-baseline gap-2xs rounded-md px-2xs py-3xs',\n 'text-step-sm text-content',\n 'data-focus:bg-surface',\n )}\n >\n <span className=\"truncate\">{option.label}</span>\n {option.sublabel !== undefined && (\n <span className=\"truncate text-step-xs text-content-muted\">\n {option.sublabel}\n </span>\n )}\n {option.hint !== undefined && (\n <span className=\"ml-auto shrink-0 text-step-xs text-content-muted\">\n {option.hint}\n </span>\n )}\n </ComboboxOption>\n );\n}\n\nfunction Group({\n group,\n labels,\n}: {\n group: CommandGroup;\n labels: CommandPaletteLabels;\n}) {\n const remaining =\n group.total === undefined ? 0 : group.total - group.options.length;\n\n return (\n <div data-testid={`command-group-${group.key}`}>\n <GroupHeading label={group.label} />\n\n {group.isLoading && (\n <div role=\"presentation\" className=\"px-2xs py-3xs\">\n {/* The skeleton is `aria-hidden`, so the wait needs its own text or a\n screen reader hears a group that simply has no rows. */}\n <span className=\"sr-only\">{labels.loading}</span>\n <SkeletonText lines={2} />\n </div>\n )}\n\n {group.options.map(option => (\n <CommandRow key={option.id} option={option} />\n ))}\n\n {remaining > 0 && (\n <div\n role=\"presentation\"\n className=\"px-2xs py-3xs text-step-xs text-content-muted\"\n >\n {labels.more(remaining)}\n </div>\n )}\n\n {/*\n A degraded source is NAMED, never a missing group: an absent group reads\n as \"nothing matched\", which nobody can honestly claim about a source they\n could not reach. The two severities look different on purpose — `scope`\n is the ordinary state for an operator with no organization, and painting\n that as a warning on every keystroke teaches people to ignore the one\n that means something is broken.\n */}\n {group.unavailable !== undefined && (\n <div\n role=\"presentation\"\n data-testid={`command-unavailable-${group.key}`}\n className={cn(\n 'px-2xs py-3xs text-step-xs',\n group.unavailable.severity === 'reachability'\n ? 'text-danger'\n : 'text-content-muted',\n )}\n >\n {group.unavailable.message}\n </div>\n )}\n </div>\n );\n}\n\n/**\n * A floating command bar: search destinations, records and commands, and run\n * them from the keyboard.\n *\n * Presentational, and strictly so. It holds no index, performs no fetch and\n * knows no router — every group arrives already filtered, already authorized and\n * already translated, from hooks in the shell layer that meet this component at\n * the framework-free `CommandSource` port in core. That is the same arrangement\n * `EntityLinkSource` uses, and it exists because `@entifix/react-controls` may\n * not import `@entifix/react-integration`.\n *\n * **The sources filter, not this component.** A record group is filtered by the\n * service that answered it; a command group is filtered by the hook that built\n * it. Filtering again here would mean the palette had to know which groups were\n * pre-filtered, and a group that answered a *different* term would be silently\n * emptied rather than shown as stale.\n *\n * Depth is a **stack of pages**: selecting an option carrying `push` makes the\n * palette become that page. `Escape` pops one level and closes at the root;\n * `Backspace` on an empty term pops too, which is the gesture Raycast trained\n * people on and costs nothing when there is nothing to pop.\n *\n * Built on Headless UI's `Dialog` + `Combobox`, so the focus trap, focus\n * restoration, `role=\"combobox\"`, `aria-expanded`, `aria-activedescendant` and\n * the arrow-key contract are the library's rather than seven hand-written\n * behaviours that each have to be right.\n */\nexport function CommandPalette({\n open,\n onClose,\n pages,\n stack,\n onStackChange,\n term,\n onTermChange,\n onSelect,\n labels,\n}: CommandPaletteProps) {\n const activeId = stack[stack.length - 1];\n const page =\n (activeId === undefined\n ? pages[0]\n : pages.find(candidate => candidate.id === activeId)) ?? pages[0];\n\n const groups = page.sources.flatMap(source => source.groups);\n // Runs on every render of the palette, not on the click: an option with no\n // effect — or with two — is a line that looks selectable and misbehaves, and\n // the check that only fires when someone already selected it is the check\n // that fires after the bug is reported.\n groups.forEach(group => group.options.forEach(commandEffectOf));\n\n const isEmpty = groups.every(groupIsEmpty);\n\n const pop = () => {\n if (stack.length === 0) {\n onClose();\n return;\n }\n onStackChange(stack.slice(0, -1));\n onTermChange('');\n };\n\n const handleSelect = (option: CommandOption | null) => {\n // Headless UI types `onChange` as nullable — it reports `null` when a search\n // is abandoned rather than chosen, and acting on that would run whatever\n // happened to be highlighted.\n //\n // ⚠️ Unreachable *here*, and ignored for coverage rather than contorted\n // around: this combobox is driven entirely from the outside (`value` is the\n // caller's `term`, there is no `displayValue`) and Escape is handled on the\n // input above, so the library never produces the null it admits. The guard\n // stays because the type is the library's contract, not ours to assume away.\n /* v8 ignore next 2 */\n if (option === null) return;\n if (option.push !== undefined) {\n onStackChange([...stack, option.push]);\n onTermChange('');\n return;\n }\n onSelect(option);\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n // ⚠️ Escape is handled here rather than left to the `Dialog`, because\n // `Combobox` consumes it first once a term has been typed — it treats the\n // key as \"close the suggestion list\", which for a `static` list means\n // nothing at all, and the palette then refuses to close for exactly the\n // person who has typed something. Measured, not defensive.\n if (event.key === 'Escape') {\n event.preventDefault();\n event.stopPropagation();\n pop();\n return;\n }\n // Only when there is nothing to delete, so the key keeps its ordinary\n // meaning for as long as the person is still editing a term.\n if (event.key === 'Backspace' && term === '' && stack.length > 0) {\n event.preventDefault();\n pop();\n }\n };\n\n return (\n <Dialog\n open={open}\n // Headless UI routes Escape here. Popping rather than closing is what\n // makes the stack feel like a stack; at the root the two are the same.\n onClose={pop}\n className=\"inset-0 fixed z-50 flex items-start justify-center overflow-y-auto p-s pt-[12vh]\"\n >\n <div aria-hidden className=\"inset-0 bg-black/40 fixed\" />\n <DialogPanel\n data-testid=\"command-palette\"\n className=\"relative w-full max-w-xl overflow-hidden rounded-lg border border-border bg-surface-elevated shadow-overlay\"\n >\n <DialogTitle className=\"sr-only\">{labels.title}</DialogTitle>\n\n {/*\n Uncontrolled: this combobox has no value worth holding — an option is\n an act, not a stored choice. Every render rebuilds its options from the\n sources, so the identities differ and selecting the same row twice\n still fires `onChange` both times.\n */}\n <Combobox<CommandOption> onChange={handleSelect}>\n <div className=\"flex items-center gap-2xs border-b border-border px-s py-2xs\">\n {page.title !== undefined && (\n <button\n type=\"button\"\n aria-label={labels.back}\n onClick={pop}\n className=\"shrink-0 rounded-md bg-surface px-2xs py-3xs text-step-xs text-content-muted focus-ring\"\n >\n {page.title}\n </button>\n )}\n <ComboboxInput\n data-autofocus\n aria-label={labels.title}\n placeholder={page.placeholder}\n value={term}\n onChange={event => onTermChange(event.currentTarget.value)}\n onKeyDown={handleKeyDown}\n className=\"flex-1 bg-transparent py-3xs text-step-1 text-content placeholder:text-content-muted focus:outline-none\"\n />\n </div>\n\n <ComboboxOptions\n static\n className={cn(\n 'max-h-[60vh] overflow-y-auto p-3xs',\n // The panel is a focus-managed CONTAINER, not a target: the ring\n // belongs on the row inside it.\n 'focus:outline-none',\n )}\n >\n {isEmpty ? (\n <div\n role=\"presentation\"\n className=\"px-2xs py-s text-step-sm text-content-muted\"\n >\n {labels.empty}\n </div>\n ) : (\n groups\n .filter(group => !groupIsEmpty(group))\n .map(group => (\n <Group key={group.key} group={group} labels={labels} />\n ))\n )}\n </ComboboxOptions>\n </Combobox>\n </DialogPanel>\n </Dialog>\n );\n}\n"],"names":["commandEffectOf","Combobox","ComboboxInput","ComboboxOption","ComboboxOptions","Dialog","DialogPanel","DialogTitle","SkeletonText","cn","groupIsEmpty","group","options","length","isLoading","unavailable","undefined","GroupHeading","label","div","role","className","CommandRow","option","value","span","sublabel","hint","Group","labels","remaining","total","data-testid","key","loading","lines","map","id","more","severity","message","CommandPalette","open","onClose","pages","stack","onStackChange","term","onTermChange","onSelect","activeId","page","find","candidate","groups","sources","flatMap","source","forEach","isEmpty","every","pop","slice","handleSelect","push","handleKeyDown","event","preventDefault","stopPropagation","aria-hidden","title","onChange","button","type","aria-label","back","onClick","data-autofocus","placeholder","currentTarget","onKeyDown","static","empty","filter"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/ui/organisms/command-palette/command-palette.tsx"],"sourcesContent":["'use client';\n\nimport {\n commandEffectOf,\n type CommandGroup,\n type CommandOption,\n type CommandPage,\n} from '@entifix/core';\nimport {\n Combobox,\n ComboboxInput,\n ComboboxOption,\n ComboboxOptions,\n Dialog,\n DialogPanel,\n DialogTitle,\n} from '@headlessui/react';\nimport type { KeyboardEvent } from 'react';\n\nimport { SkeletonText } from '../../atoms/skeleton/index.js';\nimport { cn } from '../../utils/cn.js';\n\n/** Every string the palette renders. Resolved — this control does not translate. */\nexport interface CommandPaletteLabels {\n /** Names the dialog. A dialog with no accessible name is announced as \"dialog\". */\n readonly title: string;\n /** Nothing matched, and every source said so. */\n readonly empty: string;\n /** Announced while a group is still answering. */\n readonly loading: string;\n /** \"N más\" — a function, so the plural rule stays in the catalog. */\n readonly more: (remaining: number) => string;\n /** Leaves a pushed page. Also the chip's accessible name. */\n readonly back: string;\n}\n\nexport interface CommandPaletteProps {\n open: boolean;\n onClose: () => void;\n /** Every page this palette can show. The root is the first. */\n pages: readonly CommandPage[];\n /**\n * The pushed page ids, outermost first — empty means the root.\n *\n * Controlled, so the host can empty it in the same handler that opens the\n * palette. An internal stack would have to be reset from an effect keyed on\n * `open`, which is setting state in a passive effect for something that is a\n * fact about the event that opened it.\n */\n stack: readonly string[];\n onStackChange: (stack: readonly string[]) => void;\n term: string;\n onTermChange: (term: string) => void;\n /**\n * An option was chosen. The host runs it — this control has no router and\n * cannot navigate, which is the `hrefFor` rule.\n */\n onSelect: (option: CommandOption) => void;\n labels: CommandPaletteLabels;\n}\n\nconst groupIsEmpty = (group: CommandGroup): boolean =>\n group.options.length === 0 &&\n !group.isLoading &&\n group.unavailable === undefined;\n\nfunction GroupHeading({ label }: { label: string }) {\n return (\n // `presentation`, not a heading: everything inside a `listbox` that is not\n // an `option` must be presentational, or assistive tech counts the heading\n // as a selectable row and the option count it announces is wrong.\n <div\n role=\"presentation\"\n className=\"tracking-wide px-2xs pt-2xs pb-3xs text-step-xs font-semibold text-content-muted uppercase\"\n >\n {label}\n </div>\n );\n}\n\nfunction CommandRow({ option }: { option: CommandOption }) {\n return (\n <ComboboxOption\n value={option}\n className={cn(\n 'flex min-h-(--target-min) cursor-pointer items-baseline gap-2xs rounded-md px-2xs py-3xs',\n 'text-step-sm text-content',\n 'focus-ring data-focus:bg-surface data-focus:-outline-offset-2',\n )}\n >\n <span className=\"truncate\">{option.label}</span>\n {option.sublabel !== undefined && (\n <span className=\"truncate text-step-xs text-content-muted\">\n {option.sublabel}\n </span>\n )}\n {option.hint !== undefined && (\n <span className=\"ml-auto shrink-0 text-step-xs text-content-muted\">\n {option.hint}\n </span>\n )}\n </ComboboxOption>\n );\n}\n\nfunction Group({\n group,\n labels,\n}: {\n group: CommandGroup;\n labels: CommandPaletteLabels;\n}) {\n const remaining =\n group.total === undefined ? 0 : group.total - group.options.length;\n\n return (\n <div data-testid={`command-group-${group.key}`}>\n <GroupHeading label={group.label} />\n\n {group.isLoading && (\n // The wait is said once, by the palette's status region: a listbox\n // holds options, and text inside it is read as none of them.\n <div role=\"presentation\" className=\"px-2xs py-3xs\">\n <SkeletonText lines={2} />\n </div>\n )}\n\n {group.options.map(option => (\n <CommandRow key={option.id} option={option} />\n ))}\n\n {remaining > 0 && (\n <div\n role=\"presentation\"\n className=\"px-2xs py-3xs text-step-xs text-content-muted\"\n >\n {labels.more(remaining)}\n </div>\n )}\n\n {/*\n A degraded source is NAMED, never a missing group: an absent group reads\n as \"nothing matched\", which nobody can honestly claim about a source they\n could not reach. The two severities look different on purpose — `scope`\n is the ordinary state for an operator with no organization, and painting\n that as a warning on every keystroke teaches people to ignore the one\n that means something is broken.\n */}\n {group.unavailable !== undefined && (\n <div\n role=\"presentation\"\n data-testid={`command-unavailable-${group.key}`}\n className={cn(\n 'px-2xs py-3xs text-step-xs',\n group.unavailable.severity === 'reachability'\n ? 'text-danger'\n : 'text-content-muted',\n )}\n >\n {group.unavailable.message}\n </div>\n )}\n </div>\n );\n}\n\n/**\n * A floating command bar: search destinations, records and commands, and run\n * them from the keyboard.\n *\n * Presentational, and strictly so. It holds no index, performs no fetch and\n * knows no router — every group arrives already filtered, already authorized and\n * already translated, from hooks in the shell layer that meet this component at\n * the framework-free `CommandSource` port in core. That is the same arrangement\n * `EntityLinkSource` uses, and it exists because `@entifix/react-controls` may\n * not import `@entifix/react-integration`.\n *\n * **The sources filter, not this component.** A record group is filtered by the\n * service that answered it; a command group is filtered by the hook that built\n * it. Filtering again here would mean the palette had to know which groups were\n * pre-filtered, and a group that answered a *different* term would be silently\n * emptied rather than shown as stale.\n *\n * Depth is a **stack of pages**: selecting an option carrying `push` makes the\n * palette become that page. `Escape` pops one level and closes at the root;\n * `Backspace` on an empty term pops too, which is the gesture Raycast trained\n * people on and costs nothing when there is nothing to pop.\n *\n * Built on Headless UI's `Dialog` + `Combobox`, so the focus trap, focus\n * restoration, `role=\"combobox\"`, `aria-expanded`, `aria-activedescendant` and\n * the arrow-key contract are the library's rather than seven hand-written\n * behaviours that each have to be right.\n */\nexport function CommandPalette({\n open,\n onClose,\n pages,\n stack,\n onStackChange,\n term,\n onTermChange,\n onSelect,\n labels,\n}: CommandPaletteProps) {\n const activeId = stack[stack.length - 1];\n const page =\n (activeId === undefined\n ? pages[0]\n : pages.find(candidate => candidate.id === activeId)) ?? pages[0];\n\n const groups = page.sources.flatMap(source => source.groups);\n // Runs on every render of the palette, not on the click: an option with no\n // effect — or with two — is a line that looks selectable and misbehaves, and\n // the check that only fires when someone already selected it is the check\n // that fires after the bug is reported.\n groups.forEach(group => group.options.forEach(commandEffectOf));\n\n const isEmpty = groups.every(groupIsEmpty);\n const isAnswering = groups.some(group => group.isLoading);\n\n const pop = () => {\n if (stack.length === 0) {\n onClose();\n return;\n }\n onStackChange(stack.slice(0, -1));\n onTermChange('');\n };\n\n const handleSelect = (option: CommandOption | null) => {\n // Headless UI types `onChange` as nullable — it reports `null` when a search\n // is abandoned rather than chosen, and acting on that would run whatever\n // happened to be highlighted.\n //\n // ⚠️ Unreachable *here*, and ignored for coverage rather than contorted\n // around: this combobox is driven entirely from the outside (`value` is the\n // caller's `term`, there is no `displayValue`) and Escape is handled on the\n // input above, so the library never produces the null it admits. The guard\n // stays because the type is the library's contract, not ours to assume away.\n /* v8 ignore next 2 */\n if (option === null) return;\n if (option.push !== undefined) {\n onStackChange([...stack, option.push]);\n onTermChange('');\n return;\n }\n onSelect(option);\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n // ⚠️ Escape is handled here rather than left to the `Dialog`, because\n // `Combobox` consumes it first once a term has been typed — it treats the\n // key as \"close the suggestion list\", which for a `static` list means\n // nothing at all, and the palette then refuses to close for exactly the\n // person who has typed something. Measured, not defensive.\n if (event.key === 'Escape') {\n event.preventDefault();\n event.stopPropagation();\n pop();\n return;\n }\n // Only when there is nothing to delete, so the key keeps its ordinary\n // meaning for as long as the person is still editing a term.\n if (event.key === 'Backspace' && term === '' && stack.length > 0) {\n event.preventDefault();\n pop();\n }\n };\n\n return (\n <Dialog\n open={open}\n // Headless UI routes Escape here. Popping rather than closing is what\n // makes the stack feel like a stack; at the root the two are the same.\n onClose={pop}\n className=\"inset-0 fixed z-(--z-palette) flex items-start justify-center overflow-y-auto p-s pt-(--overlay-offset-block)\"\n >\n <div aria-hidden className=\"inset-0 fixed bg-scrim\" />\n <DialogPanel\n data-testid=\"command-palette\"\n className=\"relative w-full max-w-(--container-xl) overflow-hidden rounded-lg border border-border bg-surface-elevated shadow-overlay\"\n >\n <DialogTitle className=\"sr-only\">{labels.title}</DialogTitle>\n\n {/*\n Uncontrolled: this combobox has no value worth holding — an option is\n an act, not a stored choice. Every render rebuilds its options from the\n sources, so the identities differ and selecting the same row twice\n still fires `onChange` both times.\n */}\n <Combobox<CommandOption> onChange={handleSelect}>\n <div className=\"flex items-center gap-2xs border-b border-border px-s py-2xs\">\n {page.title !== undefined && (\n <button\n type=\"button\"\n // Its name holds the text it shows (WCAG 2.5.3): \"Volver\" is\n // said, and \"Nuevo\" is both seen and said.\n aria-label={`${labels.back} ${page.title}`}\n onClick={pop}\n className=\"inline-flex min-h-(--target-min) shrink-0 items-center rounded-md bg-surface px-2xs py-3xs text-step-xs text-content-muted focus-ring\"\n >\n {page.title}\n </button>\n )}\n <ComboboxInput\n data-autofocus\n aria-label={labels.title}\n placeholder={page.placeholder}\n value={term}\n onChange={event => onTermChange(event.currentTarget.value)}\n onKeyDown={handleKeyDown}\n className=\"flex-1 rounded-sm bg-transparent py-3xs text-step-1 text-content focus-ring placeholder:text-content-muted\"\n />\n </div>\n\n {/* Outside the listbox, which may hold only options: said when the\n palette finds nothing, and while a group is still answering. */}\n <span role=\"status\" className=\"sr-only\">\n {isEmpty ? labels.empty : isAnswering ? labels.loading : ''}\n </span>\n\n <ComboboxOptions\n static\n // Still filling: a listbox with no options yet is not a broken one.\n aria-busy={isAnswering || undefined}\n className={cn(\n 'max-h-(--overlay-max-block) overflow-y-auto p-3xs',\n // The panel is a focus-managed CONTAINER, not a target: the ring\n // belongs on the row inside it.\n 'focus:outline-none',\n )}\n >\n {isEmpty ? (\n <div\n role=\"presentation\"\n aria-hidden=\"true\"\n className=\"px-2xs py-s text-step-sm text-content-muted\"\n >\n {labels.empty}\n </div>\n ) : (\n groups\n .filter(group => !groupIsEmpty(group))\n .map(group => (\n <Group key={group.key} group={group} labels={labels} />\n ))\n )}\n </ComboboxOptions>\n </Combobox>\n </DialogPanel>\n </Dialog>\n );\n}\n"],"names":["commandEffectOf","Combobox","ComboboxInput","ComboboxOption","ComboboxOptions","Dialog","DialogPanel","DialogTitle","SkeletonText","cn","groupIsEmpty","group","options","length","isLoading","unavailable","undefined","GroupHeading","label","div","role","className","CommandRow","option","value","span","sublabel","hint","Group","labels","remaining","total","data-testid","key","lines","map","id","more","severity","message","CommandPalette","open","onClose","pages","stack","onStackChange","term","onTermChange","onSelect","activeId","page","find","candidate","groups","sources","flatMap","source","forEach","isEmpty","every","isAnswering","some","pop","slice","handleSelect","push","handleKeyDown","event","preventDefault","stopPropagation","aria-hidden","title","onChange","button","type","aria-label","back","onClick","data-autofocus","placeholder","currentTarget","onKeyDown","empty","loading","static","aria-busy","filter"],"mappings":"AAAA;;AAEA,SACEA,eAAe,QAIV,gBAAgB;AACvB,SACEC,QAAQ,EACRC,aAAa,EACbC,cAAc,EACdC,eAAe,EACfC,MAAM,EACNC,WAAW,EACXC,WAAW,QACN,oBAAoB;AAG3B,SAASC,YAAY,QAAQ,gCAAgC;AAC7D,SAASC,EAAE,QAAQ,oBAAoB;AAyCvC,MAAMC,eAAe,CAACC,QACpBA,MAAMC,OAAO,CAACC,MAAM,KAAK,KACzB,CAACF,MAAMG,SAAS,IAChBH,MAAMI,WAAW,KAAKC;AAExB,SAASC,aAAa,EAAEC,KAAK,EAAqB;IAChD,OACE,2EAA2E;IAC3E,2EAA2E;IAC3E,kEAAkE;kBAClE,KAACC;QACCC,MAAK;QACLC,WAAU;kBAETH;;AAGP;AAEA,SAASI,WAAW,EAAEC,MAAM,EAA6B;IACvD,qBACE,MAACpB;QACCqB,OAAOD;QACPF,WAAWZ,GACT,4FACA,6BACA;;0BAGF,KAACgB;gBAAKJ,WAAU;0BAAYE,OAAOL,KAAK;;YACvCK,OAAOG,QAAQ,KAAKV,2BACnB,KAACS;gBAAKJ,WAAU;0BACbE,OAAOG,QAAQ;;YAGnBH,OAAOI,IAAI,KAAKX,2BACf,KAACS;gBAAKJ,WAAU;0BACbE,OAAOI,IAAI;;;;AAKtB;AAEA,SAASC,MAAM,EACbjB,KAAK,EACLkB,MAAM,EAIP;IACC,MAAMC,YACJnB,MAAMoB,KAAK,KAAKf,YAAY,IAAIL,MAAMoB,KAAK,GAAGpB,MAAMC,OAAO,CAACC,MAAM;IAEpE,qBACE,MAACM;QAAIa,eAAa,CAAC,cAAc,EAAErB,MAAMsB,GAAG,EAAE;;0BAC5C,KAAChB;gBAAaC,OAAOP,MAAMO,KAAK;;YAE/BP,MAAMG,SAAS,IACd,mEAAmE;YACnE,6DAA6D;0BAC7D,KAACK;gBAAIC,MAAK;gBAAeC,WAAU;0BACjC,cAAA,KAACb;oBAAa0B,OAAO;;;YAIxBvB,MAAMC,OAAO,CAACuB,GAAG,CAACZ,CAAAA,uBACjB,KAACD;oBAA2BC,QAAQA;mBAAnBA,OAAOa,EAAE;YAG3BN,YAAY,mBACX,KAACX;gBACCC,MAAK;gBACLC,WAAU;0BAETQ,OAAOQ,IAAI,CAACP;;YAYhBnB,MAAMI,WAAW,KAAKC,2BACrB,KAACG;gBACCC,MAAK;gBACLY,eAAa,CAAC,oBAAoB,EAAErB,MAAMsB,GAAG,EAAE;gBAC/CZ,WAAWZ,GACT,8BACAE,MAAMI,WAAW,CAACuB,QAAQ,KAAK,iBAC3B,gBACA;0BAGL3B,MAAMI,WAAW,CAACwB,OAAO;;;;AAKpC;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,SAASC,eAAe,EAC7BC,IAAI,EACJC,OAAO,EACPC,KAAK,EACLC,KAAK,EACLC,aAAa,EACbC,IAAI,EACJC,YAAY,EACZC,QAAQ,EACRnB,MAAM,EACc;QAGjBoB;IAFH,MAAMA,WAAWL,KAAK,CAACA,MAAM/B,MAAM,GAAG,EAAE;IACxC,MAAMqC,QACHD,OAAAA,aAAajC,YACV2B,KAAK,CAAC,EAAE,GACRA,MAAMQ,IAAI,CAACC,CAAAA,YAAaA,UAAUhB,EAAE,KAAKa,qBAF5CA,OAE0DN,KAAK,CAAC,EAAE;IAErE,MAAMU,SAASH,KAAKI,OAAO,CAACC,OAAO,CAACC,CAAAA,SAAUA,OAAOH,MAAM;IAC3D,2EAA2E;IAC3E,6EAA6E;IAC7E,0EAA0E;IAC1E,wCAAwC;IACxCA,OAAOI,OAAO,CAAC9C,CAAAA,QAASA,MAAMC,OAAO,CAAC6C,OAAO,CAACzD;IAE9C,MAAM0D,UAAUL,OAAOM,KAAK,CAACjD;IAC7B,MAAMkD,cAAcP,OAAOQ,IAAI,CAAClD,CAAAA,QAASA,MAAMG,SAAS;IAExD,MAAMgD,MAAM;QACV,IAAIlB,MAAM/B,MAAM,KAAK,GAAG;YACtB6B;YACA;QACF;QACAG,cAAcD,MAAMmB,KAAK,CAAC,GAAG,CAAC;QAC9BhB,aAAa;IACf;IAEA,MAAMiB,eAAe,CAACzC;QACpB,6EAA6E;QAC7E,yEAAyE;QACzE,8BAA8B;QAC9B,EAAE;QACF,wEAAwE;QACxE,4EAA4E;QAC5E,4EAA4E;QAC5E,2EAA2E;QAC3E,6EAA6E;QAC7E,oBAAoB,GACpB,IAAIA,WAAW,MAAM;QACrB,IAAIA,OAAO0C,IAAI,KAAKjD,WAAW;YAC7B6B,cAAc;mBAAID;gBAAOrB,OAAO0C,IAAI;aAAC;YACrClB,aAAa;YACb;QACF;QACAC,SAASzB;IACX;IAEA,MAAM2C,gBAAgB,CAACC;QACrB,sEAAsE;QACtE,0EAA0E;QAC1E,sEAAsE;QACtE,wEAAwE;QACxE,2DAA2D;QAC3D,IAAIA,MAAMlC,GAAG,KAAK,UAAU;YAC1BkC,MAAMC,cAAc;YACpBD,MAAME,eAAe;YACrBP;YACA;QACF;QACA,sEAAsE;QACtE,6DAA6D;QAC7D,IAAIK,MAAMlC,GAAG,KAAK,eAAea,SAAS,MAAMF,MAAM/B,MAAM,GAAG,GAAG;YAChEsD,MAAMC,cAAc;YACpBN;QACF;IACF;IAEA,qBACE,MAACzD;QACCoC,MAAMA;QACN,sEAAsE;QACtE,uEAAuE;QACvEC,SAASoB;QACTzC,WAAU;;0BAEV,KAACF;gBAAImD,aAAW;gBAACjD,WAAU;;0BAC3B,MAACf;gBACC0B,eAAY;gBACZX,WAAU;;kCAEV,KAACd;wBAAYc,WAAU;kCAAWQ,OAAO0C,KAAK;;kCAQ9C,MAACtE;wBAAwBuE,UAAUR;;0CACjC,MAAC7C;gCAAIE,WAAU;;oCACZ6B,KAAKqB,KAAK,KAAKvD,2BACd,KAACyD;wCACCC,MAAK;wCACL,6DAA6D;wCAC7D,2CAA2C;wCAC3CC,cAAY,GAAG9C,OAAO+C,IAAI,CAAC,CAAC,EAAE1B,KAAKqB,KAAK,EAAE;wCAC1CM,SAASf;wCACTzC,WAAU;kDAET6B,KAAKqB,KAAK;;kDAGf,KAACrE;wCACC4E,gBAAc;wCACdH,cAAY9C,OAAO0C,KAAK;wCACxBQ,aAAa7B,KAAK6B,WAAW;wCAC7BvD,OAAOsB;wCACP0B,UAAUL,CAAAA,QAASpB,aAAaoB,MAAMa,aAAa,CAACxD,KAAK;wCACzDyD,WAAWf;wCACX7C,WAAU;;;;0CAMd,KAACI;gCAAKL,MAAK;gCAASC,WAAU;0CAC3BqC,UAAU7B,OAAOqD,KAAK,GAAGtB,cAAc/B,OAAOsD,OAAO,GAAG;;0CAG3D,KAAC/E;gCACCgF,MAAM;gCACN,oEAAoE;gCACpEC,aAAWzB,eAAe5C;gCAC1BK,WAAWZ,GACT,qDACA,iEAAiE;gCACjE,gCAAgC;gCAChC;0CAGDiD,wBACC,KAACvC;oCACCC,MAAK;oCACLkD,eAAY;oCACZjD,WAAU;8CAETQ,OAAOqD,KAAK;qCAGf7B,OACGiC,MAAM,CAAC3E,CAAAA,QAAS,CAACD,aAAaC,QAC9BwB,GAAG,CAACxB,CAAAA,sBACH,KAACiB;wCAAsBjB,OAAOA;wCAAOkB,QAAQA;uCAAjClB,MAAMsB,GAAG;;;;;;;;AAQvC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-detail-grid.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-detail-grid/entity-detail-grid.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"entity-detail-grid.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-detail-grid/entity-detail-grid.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAK3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,OAAe,EACf,SAAiB,EACjB,QAAe,EACf,MAAM,EACN,SAAS,GACV,EAAE,qBAAqB,+BA4MvB"}
|
|
@@ -5,6 +5,7 @@ import { describeChildColumns, editableChildColumns, newRowKey, parseRowFieldPat
|
|
|
5
5
|
import { useId, useMemo } from 'react';
|
|
6
6
|
import { useLocalizedDescriptors, useT } from '../../../i18n/index.js';
|
|
7
7
|
import { Button } from '../../atoms/button/index.js';
|
|
8
|
+
import { CellValue } from '../../atoms/cell-value/index.js';
|
|
8
9
|
import { FieldControl } from '../../atoms/field-control/index.js';
|
|
9
10
|
import { Skeleton } from '../../atoms/skeleton/index.js';
|
|
10
11
|
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableMessageRow, TableRow } from '../../atoms/table/index.js';
|
|
@@ -140,7 +141,6 @@ import { cn } from '../../utils/cn.js';
|
|
|
140
141
|
/*#__PURE__*/ _jsxs(Table, {
|
|
141
142
|
"aria-label": descriptor.label,
|
|
142
143
|
"aria-busy": isLoading || undefined,
|
|
143
|
-
"aria-rowcount": rows.length,
|
|
144
144
|
children: [
|
|
145
145
|
/*#__PURE__*/ _jsx(TableHead, {
|
|
146
146
|
children: /*#__PURE__*/ _jsxs(TableRow, {
|
|
@@ -180,9 +180,14 @@ import { cn } from '../../utils/cn.js';
|
|
|
180
180
|
"aria-labelledby": `${gridId}-${column.name}`,
|
|
181
181
|
"aria-invalid": error !== undefined || undefined,
|
|
182
182
|
"aria-describedby": error === undefined ? undefined : `${cellId}-error`,
|
|
183
|
-
onChange: (next)=>setRow(index, column.name, next)
|
|
184
|
-
|
|
183
|
+
onChange: (next)=>setRow(index, column.name, next),
|
|
184
|
+
record: row
|
|
185
|
+
}) : column.money === undefined ? /*#__PURE__*/ _jsx("span", {
|
|
185
186
|
children: (_row_column_name1 = row[column.name]) != null ? _row_column_name1 : ''
|
|
187
|
+
}) : /*#__PURE__*/ _jsx(CellValue, {
|
|
188
|
+
value: row[column.name],
|
|
189
|
+
descriptor: column,
|
|
190
|
+
record: row
|
|
186
191
|
}),
|
|
187
192
|
editing && error !== undefined && /*#__PURE__*/ _jsx("span", {
|
|
188
193
|
id: `${cellId}-error`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/ui/organisms/entity-detail-grid/entity-detail-grid.tsx"],"sourcesContent":["'use client';\n\nimport {\n describeChildColumns,\n editableChildColumns,\n type EntityFieldDescriptor,\n type EntityRowDraft,\n newRowKey,\n parseRowFieldPath,\n ROW_KEY,\n rowFieldPath,\n} from '@entifix/core';\nimport { type KeyboardEvent, useId, useMemo } from 'react';\n\nimport { useLocalizedDescriptors, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { FieldControl } from '../../atoms/field-control/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 { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.js';\nimport type { EntityDetailGridProps } from './entity-detail-grid.types.js';\n\n/** Rows the placeholder stands in for. Enough to read as a grid, not a page. */\nconst SKELETON_ROW_COUNT = 3;\n\n/**\n * The rows a record **owns**, edited in place.\n *\n * A `composition` — an order and its lines — where the rows have no life outside\n * the master and go out in the same write\n *. Not\n * a `linkCollection`, which is association to records that exist on their own\n * and save separately; that editor is still #26.\n *\n * ## Why this is not `EntityTable`\n *\n * `EntityTable` is a *server* listing: pages, RSQL filtering and sorting,\n * column personalization, a selection model and a bulk bar. A detail grid has\n * none of those and cannot have them — its rows are local, unsaved, and\n * unqueryable by construction, since a collection member is never `filterable`.\n * Sharing the organism would mean every one of those features growing a\n * \"detail\" branch. It shares the `Table` **atoms** instead, which is the level\n * the two actually have in common.\n *\n * ## Inline cells, and why Enter is free\n *\n * Each cell is a {@link FieldControl}, the same descriptor→input map the form\n * uses, so a child's `enum` gets a select and its `date` a date input with no\n * code here. `Tab` walks the row natively and crosses into the next; `Enter` on\n * the last row appends and focuses the new row's first cell.\n *\n * That last binding is available only because `EntityForm` renders a `<Card>`\n * with a `<button onClick>` Save rather than a `<form>` — inside a real form,\n * `Enter` in a text input submits, and appending a row would fight the browser\n * for it. Wrapping the form in a `<form>` element later would break this, which\n * is the one thing worth knowing before doing it.\n *\n * ## Errors\n *\n * A cell carries `aria-invalid` and points at its own message; the grid carries\n * **one** summary above it, which is the only `role=\"alert\"` in the component.\n * A live region per cell would announce the whole grid on every keystroke, and\n * a message rendered only in the cell is unreachable when the failing row is\n * scrolled out — the case a long grid always meets.\n */\nexport function EntityDetailGrid({\n descriptor,\n rows,\n onRowsChange,\n errors,\n editing = false,\n isLoading = false,\n skeleton = true,\n footer,\n className,\n}: EntityDetailGridProps) {\n const t = useT();\n const gridId = useId();\n\n const described = useMemo(\n () =>\n descriptor.childType === undefined\n ? []\n : editableChildColumns(describeChildColumns(descriptor.childType)),\n [descriptor.childType],\n );\n const columns = useLocalizedDescriptors(described);\n\n // Only the messages that name *this* member. The form hands over its whole\n // error map so a record with two owned collections needs no bookkeeping at\n // the call site, and a grid that filtered nothing would show the other's.\n const rowErrors = useMemo(\n () => readRowErrors(descriptor.name, errors),\n [descriptor.name, errors],\n );\n\n const showSkeleton = isLoading && skeleton !== false;\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n // One more than the columns: every row carries its remove control.\n const columnCount = columns.length + (editing ? 1 : 0);\n\n const setRow = (index: number, member: string, value: string) => {\n onRowsChange(\n rows.map((row, position) =>\n position === index ? { ...row, [member]: value } : row,\n ),\n );\n };\n\n const firstColumn = columns[0]?.name;\n\n const appendRow = () => {\n onRowsChange([...rows, seedEmptyRow(columns)]);\n focusCell(gridId, rows.length, firstColumn);\n };\n\n const removeRow = (index: number) => {\n onRowsChange(rows.filter((_row, position) => position !== index));\n // Focus the row that slid into this position, or the one above it when the\n // removed row was last — never nothing, which drops the user to the page.\n focusCell(gridId, Math.min(index, rows.length - 2), firstColumn);\n };\n\n /**\n * `Enter` anywhere in the **last row** appends the next one.\n *\n * Bound to the row rather than to its final cell, which is what it looked\n * like it should be: the last *column* is whatever the child declared last,\n * and a read-only member there renders a disabled input that never receives a\n * key at all — so the binding would exist and be unreachable, on exactly the\n * entities that have a computed column. The row is the unit the user is\n * finishing anyway.\n *\n * Only from a text input: a `<select>` opens on Enter and a checkbox toggles\n * on Space, so taking the key from those would break a control's own\n * behaviour to add a row the user did not ask for. A read-only column needs\n * no test of its own — its input is `disabled`, so it never takes focus and\n * never raises a key here at all.\n */\n const onLastRowKeyDown = (event: KeyboardEvent<HTMLTableRowElement>) => {\n if (event.key !== 'Enter') return;\n if (!(event.target instanceof HTMLInputElement)) return;\n event.preventDefault();\n appendRow();\n };\n\n return (\n <Stack gap=\"2xs\" className={className}>\n <div className=\"flex items-center justify-between gap-s\">\n <span className=\"text-step-sm font-medium text-content-muted\">\n {descriptor.label}\n </span>\n {editing && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={appendRow}\n disabled={isLoading}\n >\n {t('detail.addRow')}\n </Button>\n )}\n </div>\n\n {editing && rowErrors.size > 0 && (\n <div role=\"alert\" className=\"text-step-sm text-danger\">\n {t('detail.errorSummary', { count: rowErrors.size })}\n </div>\n )}\n\n <Table\n aria-label={descriptor.label}\n aria-busy={isLoading || undefined}\n aria-rowcount={rows.length}\n >\n <TableHead>\n <TableRow>\n {columns.map(column => (\n <TableHeaderCell\n key={column.name}\n id={`${gridId}-${column.name}`}\n >\n {column.label}\n </TableHeaderCell>\n ))}\n {editing && (\n <TableHeaderCell align=\"end\">\n <span className=\"sr-only\">{t('detail.rowActions')}</span>\n </TableHeaderCell>\n )}\n </TableRow>\n </TableHead>\n\n <TableBody>\n {showSkeleton\n ? (customSkeleton ?? (\n <SkeletonRows\n columns={columns.length}\n extra={editing ? 1 : 0}\n />\n ))\n : rows.map((row, index) => (\n <TableRow\n key={row[ROW_KEY]}\n onKeyDown={\n editing && index === rows.length - 1\n ? onLastRowKeyDown\n : undefined\n }\n >\n {columns.map(column => {\n const error = rowErrors.get(\n rowFieldPath(descriptor.name, index, column.name),\n );\n const cellId = `${gridId}-${index}-${column.name}`;\n return (\n <TableCell key={column.name}>\n {editing ? (\n <FieldControl\n descriptor={column}\n value={row[column.name] ?? ''}\n id={cellId}\n aria-labelledby={`${gridId}-${column.name}`}\n aria-invalid={error !== undefined || undefined}\n aria-describedby={\n error === undefined\n ? undefined\n : `${cellId}-error`\n }\n onChange={next => setRow(index, column.name, next)}\n />\n ) : (\n <span>{row[column.name] ?? ''}</span>\n )}\n {editing && error !== undefined && (\n <span\n id={`${cellId}-error`}\n className=\"mt-3xs block text-step-sm text-danger\"\n >\n {error}\n </span>\n )}\n </TableCell>\n );\n })}\n {editing && (\n <TableCell align=\"end\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => removeRow(index)}\n >\n {t('detail.removeRow', { row: index + 1 })}\n </Button>\n </TableCell>\n )}\n </TableRow>\n ))}\n\n {!showSkeleton && rows.length === 0 && (\n <TableMessageRow colSpan={Math.max(columnCount, 1)}>\n {t('detail.empty')}\n </TableMessageRow>\n )}\n </TableBody>\n </Table>\n\n {footer !== undefined && !showSkeleton && (\n <div className={cn('flex justify-end text-step-sm text-content')}>\n {footer(rows)}\n </div>\n )}\n </Stack>\n );\n}\n\n/** The placeholder, shaped like the grid it stands in for. */\nfunction SkeletonRows({ columns, extra }: { columns: number; extra: number }) {\n return (\n <>\n {Array.from({ length: SKELETON_ROW_COUNT }, (_row, index) => (\n <TableRow key={index}>\n {Array.from({ length: columns + extra }, (_cell, cellIndex) => (\n <TableCell key={cellIndex}>\n <Skeleton className=\"h-8 w-full\" />\n </TableCell>\n ))}\n </TableRow>\n ))}\n </>\n );\n}\n\n/**\n * A blank row, seeded with every column the grid edits.\n *\n * Every member is present and empty rather than absent, for the reason a form's\n * seed carries every key: a control handed `undefined` flips from controlled to\n * uncontrolled the moment the user types into it.\n */\nfunction seedEmptyRow(\n columns: readonly EntityFieldDescriptor[],\n): EntityRowDraft {\n const row: EntityRowDraft = { [ROW_KEY]: newRowKey() };\n for (const column of columns) row[column.name] = '';\n return row;\n}\n\n/**\n * Moves focus into a cell after the list changed.\n *\n * By element id rather than through a ref into the `<tbody>`, because the row\n * being focused does not exist yet when this is called — it is the row the\n * caller has just asked React to render. Deferring to a microtask lets that\n * render commit first; `getElementById` then finds the input the same way the\n * cell's own `aria-describedby` does.\n *\n * A row index that names nothing simply focuses nothing, which is the right\n * outcome for removing the only row: there is no cell to land in, and stealing\n * focus somewhere arbitrary is worse than leaving it where the user put it.\n */\nfunction focusCell(gridId: string, index: number, column?: string): void {\n if (column === undefined || index < 0) return;\n\n queueMicrotask(() => {\n document.getElementById(`${gridId}-${index}-${column}`)?.focus();\n });\n}\n\n/** The messages naming a row of `member`, keyed by their full path. */\nfunction readRowErrors(\n member: string,\n errors: Record<string, string> | undefined,\n): Map<string, string> {\n const found = new Map<string, string>();\n if (errors === undefined) return found;\n\n for (const [path, message] of Object.entries(errors)) {\n if (parseRowFieldPath(path)?.member === member) found.set(path, message);\n }\n return found;\n}\n"],"names":["describeChildColumns","editableChildColumns","newRowKey","parseRowFieldPath","ROW_KEY","rowFieldPath","useId","useMemo","useLocalizedDescriptors","useT","Button","FieldControl","Skeleton","Table","TableBody","TableCell","TableHead","TableHeaderCell","TableMessageRow","TableRow","Stack","cn","SKELETON_ROW_COUNT","EntityDetailGrid","descriptor","rows","onRowsChange","errors","editing","isLoading","skeleton","footer","className","columns","t","gridId","described","childType","undefined","rowErrors","readRowErrors","name","showSkeleton","customSkeleton","columnCount","length","setRow","index","member","value","map","row","position","firstColumn","appendRow","seedEmptyRow","focusCell","removeRow","filter","_row","Math","min","onLastRowKeyDown","event","key","target","HTMLInputElement","preventDefault","gap","div","span","label","type","variant","size","onClick","disabled","role","count","aria-label","aria-busy","aria-rowcount","column","id","align","SkeletonRows","extra","onKeyDown","error","get","cellId","aria-labelledby","aria-invalid","aria-describedby","onChange","next","colSpan","max","Array","from","_cell","cellIndex","queueMicrotask","document","getElementById","focus","found","Map","path","message","Object","entries","set"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/ui/organisms/entity-detail-grid/entity-detail-grid.tsx"],"sourcesContent":["'use client';\n\nimport {\n describeChildColumns,\n editableChildColumns,\n type EntityFieldDescriptor,\n type EntityRowDraft,\n newRowKey,\n parseRowFieldPath,\n ROW_KEY,\n rowFieldPath,\n} from '@entifix/core';\nimport { type KeyboardEvent, useId, useMemo } from 'react';\n\nimport { useLocalizedDescriptors, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { CellValue } from '../../atoms/cell-value/index.js';\nimport { FieldControl } from '../../atoms/field-control/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 { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.js';\nimport type { EntityDetailGridProps } from './entity-detail-grid.types.js';\n\n/** Rows the placeholder stands in for. Enough to read as a grid, not a page. */\nconst SKELETON_ROW_COUNT = 3;\n\n/**\n * The rows a record **owns**, edited in place.\n *\n * A `composition` — an order and its lines — where the rows have no life outside\n * the master and go out in the same write\n *. Not\n * a `linkCollection`, which is association to records that exist on their own\n * and save separately; that editor is still #26.\n *\n * ## Why this is not `EntityTable`\n *\n * `EntityTable` is a *server* listing: pages, RSQL filtering and sorting,\n * column personalization, a selection model and a bulk bar. A detail grid has\n * none of those and cannot have them — its rows are local, unsaved, and\n * unqueryable by construction, since a collection member is never `filterable`.\n * Sharing the organism would mean every one of those features growing a\n * \"detail\" branch. It shares the `Table` **atoms** instead, which is the level\n * the two actually have in common.\n *\n * ## Inline cells, and why Enter is free\n *\n * Each cell is a {@link FieldControl}, the same descriptor→input map the form\n * uses, so a child's `enum` gets a select and its `date` a date input with no\n * code here. `Tab` walks the row natively and crosses into the next; `Enter` on\n * the last row appends and focuses the new row's first cell.\n *\n * That last binding is available only because `EntityForm` renders a `<Card>`\n * with a `<button onClick>` Save rather than a `<form>` — inside a real form,\n * `Enter` in a text input submits, and appending a row would fight the browser\n * for it. Wrapping the form in a `<form>` element later would break this, which\n * is the one thing worth knowing before doing it.\n *\n * ## Errors\n *\n * A cell carries `aria-invalid` and points at its own message; the grid carries\n * **one** summary above it, which is the only `role=\"alert\"` in the component.\n * A live region per cell would announce the whole grid on every keystroke, and\n * a message rendered only in the cell is unreachable when the failing row is\n * scrolled out — the case a long grid always meets.\n */\nexport function EntityDetailGrid({\n descriptor,\n rows,\n onRowsChange,\n errors,\n editing = false,\n isLoading = false,\n skeleton = true,\n footer,\n className,\n}: EntityDetailGridProps) {\n const t = useT();\n const gridId = useId();\n\n const described = useMemo(\n () =>\n descriptor.childType === undefined\n ? []\n : editableChildColumns(describeChildColumns(descriptor.childType)),\n [descriptor.childType],\n );\n const columns = useLocalizedDescriptors(described);\n\n // Only the messages that name *this* member. The form hands over its whole\n // error map so a record with two owned collections needs no bookkeeping at\n // the call site, and a grid that filtered nothing would show the other's.\n const rowErrors = useMemo(\n () => readRowErrors(descriptor.name, errors),\n [descriptor.name, errors],\n );\n\n const showSkeleton = isLoading && skeleton !== false;\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n // One more than the columns: every row carries its remove control.\n const columnCount = columns.length + (editing ? 1 : 0);\n\n const setRow = (index: number, member: string, value: string) => {\n onRowsChange(\n rows.map((row, position) =>\n position === index ? { ...row, [member]: value } : row,\n ),\n );\n };\n\n const firstColumn = columns[0]?.name;\n\n const appendRow = () => {\n onRowsChange([...rows, seedEmptyRow(columns)]);\n focusCell(gridId, rows.length, firstColumn);\n };\n\n const removeRow = (index: number) => {\n onRowsChange(rows.filter((_row, position) => position !== index));\n // Focus the row that slid into this position, or the one above it when the\n // removed row was last — never nothing, which drops the user to the page.\n focusCell(gridId, Math.min(index, rows.length - 2), firstColumn);\n };\n\n /**\n * `Enter` anywhere in the **last row** appends the next one.\n *\n * Bound to the row rather than to its final cell, which is what it looked\n * like it should be: the last *column* is whatever the child declared last,\n * and a read-only member there renders a disabled input that never receives a\n * key at all — so the binding would exist and be unreachable, on exactly the\n * entities that have a computed column. The row is the unit the user is\n * finishing anyway.\n *\n * Only from a text input: a `<select>` opens on Enter and a checkbox toggles\n * on Space, so taking the key from those would break a control's own\n * behaviour to add a row the user did not ask for. A read-only column needs\n * no test of its own — its input is `disabled`, so it never takes focus and\n * never raises a key here at all.\n */\n const onLastRowKeyDown = (event: KeyboardEvent<HTMLTableRowElement>) => {\n if (event.key !== 'Enter') return;\n if (!(event.target instanceof HTMLInputElement)) return;\n event.preventDefault();\n appendRow();\n };\n\n return (\n <Stack gap=\"2xs\" className={className}>\n <div className=\"flex items-center justify-between gap-s\">\n <span className=\"text-step-sm font-medium text-content-muted\">\n {descriptor.label}\n </span>\n {editing && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={appendRow}\n disabled={isLoading}\n >\n {t('detail.addRow')}\n </Button>\n )}\n </div>\n\n {editing && rowErrors.size > 0 && (\n <div role=\"alert\" className=\"text-step-sm text-danger\">\n {t('detail.errorSummary', { count: rowErrors.size })}\n </div>\n )}\n\n <Table aria-label={descriptor.label} aria-busy={isLoading || undefined}>\n <TableHead>\n <TableRow>\n {columns.map(column => (\n <TableHeaderCell\n key={column.name}\n id={`${gridId}-${column.name}`}\n >\n {column.label}\n </TableHeaderCell>\n ))}\n {editing && (\n <TableHeaderCell align=\"end\">\n <span className=\"sr-only\">{t('detail.rowActions')}</span>\n </TableHeaderCell>\n )}\n </TableRow>\n </TableHead>\n\n <TableBody>\n {showSkeleton\n ? (customSkeleton ?? (\n <SkeletonRows\n columns={columns.length}\n extra={editing ? 1 : 0}\n />\n ))\n : rows.map((row, index) => (\n <TableRow\n key={row[ROW_KEY]}\n onKeyDown={\n editing && index === rows.length - 1\n ? onLastRowKeyDown\n : undefined\n }\n >\n {columns.map(column => {\n const error = rowErrors.get(\n rowFieldPath(descriptor.name, index, column.name),\n );\n const cellId = `${gridId}-${index}-${column.name}`;\n return (\n <TableCell key={column.name}>\n {editing ? (\n <FieldControl\n descriptor={column}\n value={row[column.name] ?? ''}\n id={cellId}\n aria-labelledby={`${gridId}-${column.name}`}\n aria-invalid={error !== undefined || undefined}\n aria-describedby={\n error === undefined\n ? undefined\n : `${cellId}-error`\n }\n onChange={next => setRow(index, column.name, next)}\n record={row}\n />\n ) : column.money === undefined ? (\n <span>{row[column.name] ?? ''}</span>\n ) : (\n <CellValue\n value={row[column.name]}\n descriptor={column}\n record={row}\n />\n )}\n {editing && error !== undefined && (\n <span\n id={`${cellId}-error`}\n className=\"mt-3xs block text-step-sm text-danger\"\n >\n {error}\n </span>\n )}\n </TableCell>\n );\n })}\n {editing && (\n <TableCell align=\"end\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => removeRow(index)}\n >\n {t('detail.removeRow', { row: index + 1 })}\n </Button>\n </TableCell>\n )}\n </TableRow>\n ))}\n\n {!showSkeleton && rows.length === 0 && (\n <TableMessageRow colSpan={Math.max(columnCount, 1)}>\n {t('detail.empty')}\n </TableMessageRow>\n )}\n </TableBody>\n </Table>\n\n {footer !== undefined && !showSkeleton && (\n <div className={cn('flex justify-end text-step-sm text-content')}>\n {footer(rows)}\n </div>\n )}\n </Stack>\n );\n}\n\n/** The placeholder, shaped like the grid it stands in for. */\nfunction SkeletonRows({ columns, extra }: { columns: number; extra: number }) {\n return (\n <>\n {Array.from({ length: SKELETON_ROW_COUNT }, (_row, index) => (\n <TableRow key={index}>\n {Array.from({ length: columns + extra }, (_cell, cellIndex) => (\n <TableCell key={cellIndex}>\n <Skeleton className=\"h-8 w-full\" />\n </TableCell>\n ))}\n </TableRow>\n ))}\n </>\n );\n}\n\n/**\n * A blank row, seeded with every column the grid edits.\n *\n * Every member is present and empty rather than absent, for the reason a form's\n * seed carries every key: a control handed `undefined` flips from controlled to\n * uncontrolled the moment the user types into it.\n */\nfunction seedEmptyRow(\n columns: readonly EntityFieldDescriptor[],\n): EntityRowDraft {\n const row: EntityRowDraft = { [ROW_KEY]: newRowKey() };\n for (const column of columns) row[column.name] = '';\n return row;\n}\n\n/**\n * Moves focus into a cell after the list changed.\n *\n * By element id rather than through a ref into the `<tbody>`, because the row\n * being focused does not exist yet when this is called — it is the row the\n * caller has just asked React to render. Deferring to a microtask lets that\n * render commit first; `getElementById` then finds the input the same way the\n * cell's own `aria-describedby` does.\n *\n * A row index that names nothing simply focuses nothing, which is the right\n * outcome for removing the only row: there is no cell to land in, and stealing\n * focus somewhere arbitrary is worse than leaving it where the user put it.\n */\nfunction focusCell(gridId: string, index: number, column?: string): void {\n if (column === undefined || index < 0) return;\n\n queueMicrotask(() => {\n document.getElementById(`${gridId}-${index}-${column}`)?.focus();\n });\n}\n\n/** The messages naming a row of `member`, keyed by their full path. */\nfunction readRowErrors(\n member: string,\n errors: Record<string, string> | undefined,\n): Map<string, string> {\n const found = new Map<string, string>();\n if (errors === undefined) return found;\n\n for (const [path, message] of Object.entries(errors)) {\n if (parseRowFieldPath(path)?.member === member) found.set(path, message);\n }\n return found;\n}\n"],"names":["describeChildColumns","editableChildColumns","newRowKey","parseRowFieldPath","ROW_KEY","rowFieldPath","useId","useMemo","useLocalizedDescriptors","useT","Button","CellValue","FieldControl","Skeleton","Table","TableBody","TableCell","TableHead","TableHeaderCell","TableMessageRow","TableRow","Stack","cn","SKELETON_ROW_COUNT","EntityDetailGrid","descriptor","rows","onRowsChange","errors","editing","isLoading","skeleton","footer","className","columns","t","gridId","described","childType","undefined","rowErrors","readRowErrors","name","showSkeleton","customSkeleton","columnCount","length","setRow","index","member","value","map","row","position","firstColumn","appendRow","seedEmptyRow","focusCell","removeRow","filter","_row","Math","min","onLastRowKeyDown","event","key","target","HTMLInputElement","preventDefault","gap","div","span","label","type","variant","size","onClick","disabled","role","count","aria-label","aria-busy","column","id","align","SkeletonRows","extra","onKeyDown","error","get","cellId","aria-labelledby","aria-invalid","aria-describedby","onChange","next","record","money","colSpan","max","Array","from","_cell","cellIndex","queueMicrotask","document","getElementById","focus","found","Map","path","message","Object","entries","set"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type Entity } from '@entifix/core';
|
|
2
2
|
import type { EntityFormProps } from './entity-form.types.js';
|
|
3
|
-
export declare function EntityForm<TEntity extends Entity>({ entityConstructor, entity, values, onFieldChange, linkSources, onLinkChange, mode: modeProp, defaultMode, onModeChange, errors, formError, onSubmit, onDelete, embedded, isLoading, skeleton, isSaving, isPending, isDeleting, error, backHref, renderLink, title, metadata, isMetadataLoading, onUseCase, onClone, children, }: EntityFormProps<TEntity>): import("react").JSX.Element;
|
|
3
|
+
export declare function EntityForm<TEntity extends Entity>({ entityConstructor, entity, values, onFieldChange, linkSources, onLinkChange, mode: modeProp, defaultMode, onModeChange, errors, formError, onSubmit, onDelete, deletePolicy, embedded, isLoading, skeleton, isSaving, isPending, isDeleting, error, onRetry, backHref, renderLink, title, metadata, isMetadataLoading, onUseCase, onClone, children, }: EntityFormProps<TEntity>): import("react").JSX.Element;
|
|
4
4
|
//# sourceMappingURL=entity-form.d.ts.map
|