@eifi1/ui-kit 0.14.1 → 0.14.2

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.
Files changed (53) hide show
  1. package/README.md +4 -3
  2. package/dist/components/amount-input.d.ts +8 -0
  3. package/dist/components/amount-input.js +10 -4
  4. package/dist/components/amount-input.js.map +1 -1
  5. package/dist/components/error-boundary.js +2 -1
  6. package/dist/components/error-boundary.js.map +1 -1
  7. package/dist/components/money-field.d.ts +92 -0
  8. package/dist/components/money-field.js +40 -0
  9. package/dist/components/money-field.js.map +1 -0
  10. package/dist/components/number-field.d.ts +12 -1
  11. package/dist/components/number-field.js +4 -0
  12. package/dist/components/number-field.js.map +1 -1
  13. package/dist/components/page-header.js +29 -3
  14. package/dist/components/page-header.js.map +1 -1
  15. package/dist/components/text-link.js +2 -1
  16. package/dist/components/text-link.js.map +1 -1
  17. package/dist/feedback/feedback-dialog.d.ts +10 -1
  18. package/dist/feedback/feedback-dialog.js +13 -3
  19. package/dist/feedback/feedback-dialog.js.map +1 -1
  20. package/dist/i18n/locales/de.js +1 -0
  21. package/dist/i18n/locales/de.js.map +1 -1
  22. package/dist/i18n/locales/es.js +1 -0
  23. package/dist/i18n/locales/es.js.map +1 -1
  24. package/dist/i18n/locales/fr.js +1 -0
  25. package/dist/i18n/locales/fr.js.map +1 -1
  26. package/dist/i18n/locales/hu.js +1 -0
  27. package/dist/i18n/locales/hu.js.map +1 -1
  28. package/dist/i18n/locales/it.js +1 -0
  29. package/dist/i18n/locales/it.js.map +1 -1
  30. package/dist/i18n/locales/zh.js +1 -0
  31. package/dist/i18n/locales/zh.js.map +1 -1
  32. package/dist/index.d.ts +2 -1
  33. package/dist/index.js +1 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/lib/document-navigation.d.ts +15 -0
  36. package/dist/lib/document-navigation.js +13 -0
  37. package/dist/lib/document-navigation.js.map +1 -0
  38. package/package.json +7 -4
  39. package/src/components/amount-input.tsx +18 -4
  40. package/src/components/error-boundary.tsx +2 -1
  41. package/src/components/money-field.tsx +75 -0
  42. package/src/components/number-field.tsx +14 -0
  43. package/src/components/page-header.tsx +38 -3
  44. package/src/components/text-link.tsx +2 -1
  45. package/src/feedback/feedback-dialog.tsx +20 -2
  46. package/src/i18n/locales/de.ts +1 -0
  47. package/src/i18n/locales/es.ts +1 -0
  48. package/src/i18n/locales/fr.ts +1 -0
  49. package/src/i18n/locales/hu.ts +1 -0
  50. package/src/i18n/locales/it.ts +1 -0
  51. package/src/i18n/locales/zh.ts +1 -0
  52. package/src/index.ts +1 -0
  53. package/src/lib/document-navigation.ts +17 -0
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// @eifi1/ui-kit — shared design-system barrel.\n//\n// Domain-free UI + theme surface shared across apps (lead app: Keksdose). Consumers import\n// components/theme from \"@eifi1/ui-kit\" and the token stylesheet from \"@eifi1/ui-kit/tokens.css\".\n\n// ── lib ────────────────────────────────────────────────────────────────────\nexport * from \"./lib/calc\";\nexport { cn } from \"./lib/cn\";\nexport { logger, setStoreLog } from \"./lib/logger\";\n// Intl number / money / percent / date / relative-time formatting, and `useKitFormat()`\n// for the same bound to the provider's locale (kastlan's formatters.ts, English \"5m ago\").\nexport * from \"./lib/format\";\n// localStorage that never throws (Safari private mode, blocked site data).\nexport { readStored, writeStored } from \"./lib/safe-storage\";\n// Date helpers are also available via the \"@eifi1/ui-kit/dates\" subpath.\n\n// ── hooks ────────────────────────────────────────────────────────────────────\nexport { useMediaQuery } from \"./hooks/use-media-query\";\nexport { useBodyScrollLock } from \"./hooks/use-body-scroll-lock\";\nexport { useAnchoredRect } from \"./hooks/use-anchored-rect\";\nexport type { AnchorRect } from \"./hooks/use-anchored-rect\";\nexport { useAnchoredPanel, anchoredPanelPlacement, useVisualViewport } from \"./hooks/use-anchored-panel\";\nexport type {\n AnchoredPanel,\n AnchoredPanelOptions,\n ViewportBox,\n} from \"./hooks/use-anchored-panel\";\nexport { useEscapeKey, useOutsideClick } from \"./hooks/use-dismiss\";\n// Focus containment for an overlay, and a live region for a change that moves no focus.\n// Both were extracted from the two components that already did them correctly (Modal and\n// CommandPalette) so the rest of the kit stops being the exception.\nexport { useFocusTrap } from \"./hooks/use-focus-trap\";\nexport type { FocusTrapOptions } from \"./hooks/use-focus-trap\";\nexport { useAnnounce } from \"./hooks/use-announce\";\nexport type { UseAnnounceReturn, UseAnnounceOptions, AnnounceRegionProps } from \"./hooks/use-announce\";\nexport { useOverlayHistory } from \"./hooks/use-overlay-history\";\nexport { useCloseTransition, OVERLAY_EXIT_MS } from \"./hooks/use-close-transition\";\nexport { useRowSwipe } from \"./hooks/use-row-swipe\";\nexport type { SwipeStage, RowSwipeOptions, RowSwipeReturn } from \"./hooks/use-row-swipe\";\n// Any element as a file drop target, screened like FileButton (kastlan).\nexport { useFileDrop, dragHasFiles } from \"./hooks/use-file-drop\";\nexport type { UseFileDropOptions, UseFileDropReturn, FileDropProps } from \"./hooks/use-file-drop\";\n// kastlan's search boxes each carried a hand-rolled copy, two without the cleanup.\nexport { useDebounce, useDebouncedCallback } from \"./hooks/use-debounce\";\nexport type { DebouncedCallbackOptions, DebouncedFunction } from \"./hooks/use-debounce\";\n// One search param as state, the active tab in `?tab=`, and a dialog's open state in the\n// URL (Modal's `urlParam`) — kastlan's and keksdose's use-search-param-state, in the kit.\nexport { useSearchParamState, useTabParam, useDialogParam } from \"./hooks/use-search-param-state\";\nexport type { SearchParamStateOptions, DialogParam } from \"./hooks/use-search-param-state\";\n// Copy that reports whether it worked — keksdose's copy buttons said \"Copied\" when it had not.\nexport { useCopyToClipboard, copyToClipboard } from \"./hooks/use-copy-to-clipboard\";\nexport type { CopyState, UseCopyToClipboardOptions, UseCopyToClipboardReturn } from \"./hooks/use-copy-to-clipboard\";\n// Keyboard shortcuts (\"Mod+K\", \"Ctrl+Shift+F\") that leave typing in fields alone.\nexport { useHotkey, parseHotkey, matchesHotkey } from \"./hooks/use-hotkey\";\nexport type { Hotkey, ParsedHotkey, UseHotkeyOptions } from \"./hooks/use-hotkey\";\n// A protected file as an object URL, fetched with the app's own auth (kastlan's AuthedImage).\nexport { useAuthedSrc } from \"./hooks/use-authed-src\";\nexport type { AuthedFetcher, AuthedSrcStatus, UseAuthedSrcOptions, UseAuthedSrcResult } from \"./hooks/use-authed-src\";\n\n// ── theme / palettes ─────────────────────────────────────────────────────────\nexport * from \"./theme/chart-palette\";\n// Colour maths (sRGB ↔ OKLCH, WCAG contrast, CVD simulation) and the palette deriver\n// that builds a whole, contrast-solved palette from one brand colour plus any colours\n// pinned by hand. `auditPalette` / `auditChartRamp` measure a palette rather than\n// trusting a comment about it.\nexport * from \"./theme/color\";\nexport * from \"./theme/palette-derive\";\nexport * from \"./theme/palette-presets\";\nexport * from \"./theme/theme-store\";\nexport * from \"./theme/palette-store\";\n\n// ── components ───────────────────────────────────────────────────────────────\nexport * from \"./components/ui\";\n// The label-ABOVE field group (label, hint, error, required), inside wizards or out.\nexport * from \"./components/field\";\nexport * from \"./components/search-field\";\nexport * from \"./components/dropdown\";\nexport * from \"./components/popover\";\nexport { SwipeableRow } from \"./components/swipeable-row\";\nexport type { SwipeAction, SwipeableRowProps } from \"./components/swipeable-row\";\nexport * from \"./components/calculator\";\nexport * from \"./components/numpad-sheet\";\nexport * from \"./components/number-input\";\nexport * from \"./components/currency-select\";\nexport * from \"./components/amount-input\";\nexport * from \"./components/combobox\";\nexport * from \"./components/picker-sheet\";\nexport * from \"./components/entity-combobox\";\nexport * from \"./components/multi-entity-combobox\";\nexport * from \"./components/multi-select\";\nexport * from \"./components/tooltip\";\nexport * from \"./components/user-avatar\";\nexport * from \"./components/settings-fields\";\n// Sync state for a database-backed field: the engine (`useFieldSync`) and the\n// affordance (`FieldSyncIndicator`), kept separate because the state is useful\n// without the dot — a form can gate its submit on any field being `pending`.\nexport * from \"./components/field-sync\";\nexport * from \"./components/month-picker\";\nexport * from \"./components/checkbox\";\nexport * from \"./components/switch\";\nexport * from \"./components/slider\";\nexport * from \"./components/time-input\";\nexport * from \"./components/number-field\";\nexport * from \"./components/sparkline\";\nexport * from \"./components/stat-tile\";\nexport * from \"./components/signature-pad\";\nexport * from \"./components/password-strength\";\nexport * from \"./components/page-contents\";\nexport * from \"./components/disclosure\";\nexport * from \"./components/dialog-frame\";\nexport * from \"./components/danger-confirm\";\n// `useConfirm()` — the promise-based replacement for `window.confirm`, one host per app.\nexport * from \"./components/confirm-dialog\";\n// A non-modal corner panel and its round trigger (keksdose's assistant launcher).\nexport * from \"./components/floating-panel\";\n// The count / clear / actions bar a selection brings up (keksdose's three copies).\nexport * from \"./components/bulk-action-bar\";\nexport * from \"./components/swatch-picker\";\nexport * from \"./components/icon-picker\";\nexport * from \"./components/choice-card\";\nexport * from \"./components/autocomplete\";\nexport * from \"./components/measured-grid\";\nexport { useWindowedRows } from \"./hooks/use-windowed-rows\";\nexport type { WindowedRows } from \"./hooks/use-windowed-rows\";\n// Named, not `export *`: file-button.tsx also holds the screening helpers the\n// dropzone shares, which are internal.\nexport { FileButton, useFilePicker, matchesAccept, DEFAULT_FILE_PICKER_LABELS } from \"./components/file-button\";\nexport type {\n FileButtonProps,\n UseFilePickerOptions,\n UseFilePickerReturn,\n FilePickerLabels,\n FileRejection,\n FileRejectionReason,\n FileScreenOptions,\n} from \"./components/file-button\";\nexport * from \"./components/treemap\";\nexport * from \"./components/series-chart\";\nexport * from \"./components/chart-zoom\";\nexport * from \"./components/toggle-legend\";\nexport * from \"./components/facing-pair\";\nexport * from \"./components/series-chart-labels\";\nexport * from \"./components/pie-chart\";\nexport * from \"./components/pie-chart-labels\";\nexport * from \"./components/account-settings\";\n// 0.12.0: the fourth account section, and the QR code TwoFactorSetting draws its\n// otpauth URI with (encoder in lib/qr-encode.ts — no dependency, see there).\nexport * from \"./components/passkeys-setting\";\nexport * from \"./components/qr-code\";\nexport { encodeQr } from \"./lib/qr-encode\";\nexport type { QrEncodeOptions, QrErrorCorrection, QrMatrix } from \"./lib/qr-encode\";\nexport * from \"./components/alert-banner\";\nexport * from \"./components/toggle-group\";\n// A wrapping set of links/buttons marked `aria-current` — Tabs-like looks, not tabs.\nexport * from \"./components/nav-pills\";\n// A pill carrying one VALUE — inert, a link, or a toggle, depending on which prop it is\n// given — and the list field built from it. Distinct from Button on purpose: a row of\n// buttons reads as \"choose an action\", a row of chips as \"here are the things\".\nexport * from \"./components/chip\";\nexport * from \"./components/wizard-stepper\";\nexport * from \"./components/hover-menu\";\nexport * from \"./components/modal\";\nexport * from \"./components/full-bleed-dialog\";\nexport * from \"./components/grouped-picker\";\nexport * from \"./components/file-dropzone\";\nexport * from \"./components/mini-calendar\";\nexport * from \"./components/calendar-heatmap\";\nexport * from \"./components/date-picker\";\nexport * from \"./components/tree-view\";\nexport * from \"./components/chart\";\n// 0.8.0 layout and feedback primitives the apps hand-rolled or took from shadcn/Radix.\nexport * from \"./components/description-list\";\n// The Save / Cancel row forms and dialogs end in, and the editable line-item repeater\n// (journal lines, lease components, invoice positions). 0.12.0.\nexport * from \"./components/form-actions\";\nexport * from \"./components/line-items\";\nexport * from \"./components/progress-bar\";\n// Named: skeleton.tsx also holds SKELETON_CLASS, the look StatTile shares — internal.\nexport { Skeleton } from \"./components/skeleton\";\nexport type { SkeletonProps, SkeletonShape } from \"./components/skeleton\";\n// 0.12.0: a centred spinner with words, signed figures and deltas, a render-error fallback.\nexport * from \"./components/loading-state\";\nexport * from \"./components/signed-amount\";\nexport * from \"./components/error-boundary\";\nexport * from \"./components/copy-button\";\nexport { AuthedImage, DEFAULT_AUTHED_IMAGE_LABELS } from \"./components/authed-image\";\nexport type { AuthedImageProps, AuthedImageLabels } from \"./components/authed-image\";\nexport * from \"./components/image-grid\";\nexport * from \"./components/lightbox\";\nexport * from \"./components/button-group\";\nexport * from \"./components/table\";\nexport * from \"./components/separator\";\n// Named: scroll-area.tsx also holds the overflow hook and scrollbar class Table shares.\nexport { ScrollArea } from \"./components/scroll-area\";\nexport type { ScrollAreaProps } from \"./components/scroll-area\";\n// 0.10.0 rows, menu rows, text primitives and page chrome the apps drew by hand.\nexport * from \"./components/list\";\nexport * from \"./components/menu-item\";\nexport * from \"./components/text\";\nexport * from \"./components/status-dot\";\nexport * from \"./components/page-header\";\nexport * from \"./components/breadcrumbs\";\n// The one inline link (kastlan's EntityLink / CellLink / legal and source links), through\n// `<UiKitProvider linkComponent>`. Named: the file also holds the kit's internal link picker.\nexport { TextLink } from \"./components/text-link\";\nexport type {\n TextLinkProps,\n TextLinkRenderProps,\n TextLinkTone,\n TextLinkUnderline,\n TextLinkCurrent,\n} from \"./components/text-link\";\n// The toast layer over sonner (an OPTIONAL peer, loaded lazily): `toast` mirrors sonner's\n// API so apps migrate by swapping the import; `<Toaster>` carries the placement, theme,\n// tones and z-index both apps had wired by hand.\nexport { toast, Toaster, DEFAULT_TOAST_LABELS, TOAST_ACTION_DURATION } from \"./components/toast\";\nexport type {\n ToastId,\n ToastAction,\n ToastOptions,\n ToastPromiseOptions,\n ToastUndoOptions,\n ToastRedoOptions,\n ToastLabels,\n ToastPosition,\n ToastSwipeDirection,\n ToasterOffset,\n ToasterProps,\n} from \"./components/toast\";\n\n// ── data-table suite ─────────────────────────────────────────────────────────\nexport * from \"./components/data-table-labels\";\nexport * from \"./components/data-table-sort\";\nexport * from \"./components/data-table-filters\";\nexport * from \"./components/data-table-pagination\";\nexport { FilterPopover } from \"./components/data-table-filter-popover\";\n// data-table re-exports SortState internally; export the rest explicitly to\n// avoid a duplicate SortState star-export (it comes from data-table-sort).\nexport { DataTable } from \"./components/data-table\";\nexport type {\n DataTableColumn,\n DataTableCellProps,\n DataTableHeadProps,\n DataTableProps,\n ServerPagination,\n FilterState,\n MobileSwipeActions,\n DataTableDensity,\n DataTableChrome,\n DataTableRowAction,\n} from \"./components/data-table\";\n// 0.12.0: the yes/no cell, and a server table's state held by its owner, URL-synced.\nexport * from \"./components/data-table-cells\";\nexport { useTableUrlState, readTableUrlState } from \"./components/use-table-state\";\nexport type {\n TableUrlState,\n TableUrlSync,\n TableUrlStateProps,\n UseTableUrlStateOptions,\n UseTableUrlStateReturn,\n} from \"./components/use-table-state\";\n\n// ── shell (composable app chrome) ────────────────────────────────────────────\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\nexport * from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/role-switcher\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n\n// ── feedback ─────────────────────────────────────────────────────────────────\n// The form somebody files a report with, and the parts an inbox is built from:\n// the status vocabulary both apps share value for value, the policy about what a\n// row may move to from where it stands, and the look of a status control, a\n// category badge and an opened report. The app still wires its own API, columns\n// and strings — see the note at the top of `feedback-inbox.tsx` for what is\n// deliberately left to it.\nexport * from \"./feedback/feedback-attachment\";\nexport * from \"./feedback/feedback-dialog\";\nexport * from \"./feedback/feedback-inbox\";\nexport * from \"./feedback/feedback-thread\";\n\n// ── multi-step wizard ────────────────────────────────────────────────────────\n// The engine (step machine, validators, collected data), the chrome around it\n// (indicator + nav bar + cancel confirm) and the review step it ends on.\n// `components/wizard-stepper` next door is a different, much smaller thing: a bare\n// step indicator for a two-step import flow, with no engine.\n//\n// Two modules were removed here rather than kept as stock. `wizard-field` held a\n// `WizardField`/`WizardSelectField` pair that had never been rendered in any app —\n// a field group nothing was built from, which this comment used to advertise. And\n// `use-rhf-wizard-step` was the only thing in the package that touched\n// react-hook-form, for a hook neither consumer called: keeping it meant an optional\n// peer, a dev dependency and a type import in every consumer's typecheck, bought\n// for nobody. A design system may carry stock; untested stock nothing has ever\n// rendered is not stock, it is a liability with an export.\nexport * from \"./wizard/types\";\nexport * from \"./wizard/use-wizard\";\nexport * from \"./wizard/wizard-context\";\nexport * from \"./wizard/validation\";\nexport * from \"./wizard/wizard-step\";\nexport * from \"./wizard/wizard-summary\";\nexport * from \"./wizard/stepper-nav\";\n\n// ── guided tours ─────────────────────────────────────────────────────────────\nexport * from \"./tour/tour\";\n\n// ── command palette / global search ──────────────────────────────────────────\nexport * from \"./search/command-palette\";\nexport * from \"./search/search-index\";\nexport * from \"./search/global-search\";\n\n// ── i18n: one label tree, one optional provider ──────────────────────────────\n// `UiKitLabels` names every string the kit renders; `<UiKitProvider labels locale>`\n// hands a translation to every component below it. Precedence: a component's own\n// prop > the provider > the English default. See src/i18n/kit-labels.tsx.\nexport * from \"./i18n/kit-labels\";\nexport * from \"./i18n/defaults\";\n"],"mappings":";AAMA,cAAc;AACd,SAAS,UAAU;AACnB,SAAS,QAAQ,mBAAmB;AAGpC,cAAc;AAEd,SAAS,YAAY,mBAAmB;AAIxC,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAEhC,SAAS,kBAAkB,wBAAwB,yBAAyB;AAM5E,SAAS,cAAc,uBAAuB;AAI9C,SAAS,oBAAoB;AAE7B,SAAS,mBAAmB;AAE5B,SAAS,yBAAyB;AAClC,SAAS,oBAAoB,uBAAuB;AACpD,SAAS,mBAAmB;AAG5B,SAAS,aAAa,oBAAoB;AAG1C,SAAS,aAAa,4BAA4B;AAIlD,SAAS,qBAAqB,aAAa,sBAAsB;AAGjE,SAAS,oBAAoB,uBAAuB;AAGpD,SAAS,WAAW,aAAa,qBAAqB;AAGtD,SAAS,oBAAoB;AAI7B,cAAc;AAKd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AAEd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,oBAAoB;AAE7B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAId,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,cAAc;AAEd,cAAc;AAEd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,uBAAuB;AAIhC,SAAS,YAAY,eAAe,eAAe,kCAAkC;AAUrF,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AACd,cAAc;AACd,SAAS,gBAAgB;AAEzB,cAAc;AACd,cAAc;AAEd,cAAc;AAId,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,gBAAgB;AAGzB,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,aAAa,mCAAmC;AAEzD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,kBAAkB;AAG3B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,SAAS,gBAAgB;AAWzB,SAAS,OAAO,SAAS,sBAAsB,6BAA6B;AAgB5E,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,qBAAqB;AAG9B,SAAS,iBAAiB;AAc1B,cAAc;AACd,SAAS,kBAAkB,yBAAyB;AAUpD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AASd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAgBd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AAMd,cAAc;AACd,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// @eifi1/ui-kit — shared design-system barrel.\n//\n// Domain-free UI + theme surface shared across apps (lead app: Keksdose). Consumers import\n// components/theme from \"@eifi1/ui-kit\" and the token stylesheet from \"@eifi1/ui-kit/tokens.css\".\n\n// ── lib ────────────────────────────────────────────────────────────────────\nexport * from \"./lib/calc\";\nexport { cn } from \"./lib/cn\";\nexport { logger, setStoreLog } from \"./lib/logger\";\n// Intl number / money / percent / date / relative-time formatting, and `useKitFormat()`\n// for the same bound to the provider's locale (kastlan's formatters.ts, English \"5m ago\").\nexport * from \"./lib/format\";\n// localStorage that never throws (Safari private mode, blocked site data).\nexport { readStored, writeStored } from \"./lib/safe-storage\";\n// Date helpers are also available via the \"@eifi1/ui-kit/dates\" subpath.\n\n// ── hooks ────────────────────────────────────────────────────────────────────\nexport { useMediaQuery } from \"./hooks/use-media-query\";\nexport { useBodyScrollLock } from \"./hooks/use-body-scroll-lock\";\nexport { useAnchoredRect } from \"./hooks/use-anchored-rect\";\nexport type { AnchorRect } from \"./hooks/use-anchored-rect\";\nexport { useAnchoredPanel, anchoredPanelPlacement, useVisualViewport } from \"./hooks/use-anchored-panel\";\nexport type {\n AnchoredPanel,\n AnchoredPanelOptions,\n ViewportBox,\n} from \"./hooks/use-anchored-panel\";\nexport { useEscapeKey, useOutsideClick } from \"./hooks/use-dismiss\";\n// Focus containment for an overlay, and a live region for a change that moves no focus.\n// Both were extracted from the two components that already did them correctly (Modal and\n// CommandPalette) so the rest of the kit stops being the exception.\nexport { useFocusTrap } from \"./hooks/use-focus-trap\";\nexport type { FocusTrapOptions } from \"./hooks/use-focus-trap\";\nexport { useAnnounce } from \"./hooks/use-announce\";\nexport type { UseAnnounceReturn, UseAnnounceOptions, AnnounceRegionProps } from \"./hooks/use-announce\";\nexport { useOverlayHistory } from \"./hooks/use-overlay-history\";\nexport { useCloseTransition, OVERLAY_EXIT_MS } from \"./hooks/use-close-transition\";\nexport { useRowSwipe } from \"./hooks/use-row-swipe\";\nexport type { SwipeStage, RowSwipeOptions, RowSwipeReturn } from \"./hooks/use-row-swipe\";\n// Any element as a file drop target, screened like FileButton (kastlan).\nexport { useFileDrop, dragHasFiles } from \"./hooks/use-file-drop\";\nexport type { UseFileDropOptions, UseFileDropReturn, FileDropProps } from \"./hooks/use-file-drop\";\n// kastlan's search boxes each carried a hand-rolled copy, two without the cleanup.\nexport { useDebounce, useDebouncedCallback } from \"./hooks/use-debounce\";\nexport type { DebouncedCallbackOptions, DebouncedFunction } from \"./hooks/use-debounce\";\n// One search param as state, the active tab in `?tab=`, and a dialog's open state in the\n// URL (Modal's `urlParam`) — kastlan's and keksdose's use-search-param-state, in the kit.\nexport { useSearchParamState, useTabParam, useDialogParam } from \"./hooks/use-search-param-state\";\nexport type { SearchParamStateOptions, DialogParam } from \"./hooks/use-search-param-state\";\n// Copy that reports whether it worked — keksdose's copy buttons said \"Copied\" when it had not.\nexport { useCopyToClipboard, copyToClipboard } from \"./hooks/use-copy-to-clipboard\";\nexport type { CopyState, UseCopyToClipboardOptions, UseCopyToClipboardReturn } from \"./hooks/use-copy-to-clipboard\";\n// Keyboard shortcuts (\"Mod+K\", \"Ctrl+Shift+F\") that leave typing in fields alone.\nexport { useHotkey, parseHotkey, matchesHotkey } from \"./hooks/use-hotkey\";\nexport type { Hotkey, ParsedHotkey, UseHotkeyOptions } from \"./hooks/use-hotkey\";\n// A protected file as an object URL, fetched with the app's own auth (kastlan's AuthedImage).\nexport { useAuthedSrc } from \"./hooks/use-authed-src\";\nexport type { AuthedFetcher, AuthedSrcStatus, UseAuthedSrcOptions, UseAuthedSrcResult } from \"./hooks/use-authed-src\";\n\n// ── theme / palettes ─────────────────────────────────────────────────────────\nexport * from \"./theme/chart-palette\";\n// Colour maths (sRGB ↔ OKLCH, WCAG contrast, CVD simulation) and the palette deriver\n// that builds a whole, contrast-solved palette from one brand colour plus any colours\n// pinned by hand. `auditPalette` / `auditChartRamp` measure a palette rather than\n// trusting a comment about it.\nexport * from \"./theme/color\";\nexport * from \"./theme/palette-derive\";\nexport * from \"./theme/palette-presets\";\nexport * from \"./theme/theme-store\";\nexport * from \"./theme/palette-store\";\n\n// ── components ───────────────────────────────────────────────────────────────\nexport * from \"./components/ui\";\n// The label-ABOVE field group (label, hint, error, required), inside wizards or out.\nexport * from \"./components/field\";\nexport * from \"./components/search-field\";\nexport * from \"./components/dropdown\";\nexport * from \"./components/popover\";\nexport { SwipeableRow } from \"./components/swipeable-row\";\nexport type { SwipeAction, SwipeableRowProps } from \"./components/swipeable-row\";\nexport * from \"./components/calculator\";\nexport * from \"./components/numpad-sheet\";\nexport * from \"./components/number-input\";\nexport * from \"./components/currency-select\";\nexport * from \"./components/amount-input\";\nexport * from \"./components/money-field\";\nexport * from \"./components/combobox\";\nexport * from \"./components/picker-sheet\";\nexport * from \"./components/entity-combobox\";\nexport * from \"./components/multi-entity-combobox\";\nexport * from \"./components/multi-select\";\nexport * from \"./components/tooltip\";\nexport * from \"./components/user-avatar\";\nexport * from \"./components/settings-fields\";\n// Sync state for a database-backed field: the engine (`useFieldSync`) and the\n// affordance (`FieldSyncIndicator`), kept separate because the state is useful\n// without the dot — a form can gate its submit on any field being `pending`.\nexport * from \"./components/field-sync\";\nexport * from \"./components/month-picker\";\nexport * from \"./components/checkbox\";\nexport * from \"./components/switch\";\nexport * from \"./components/slider\";\nexport * from \"./components/time-input\";\nexport * from \"./components/number-field\";\nexport * from \"./components/sparkline\";\nexport * from \"./components/stat-tile\";\nexport * from \"./components/signature-pad\";\nexport * from \"./components/password-strength\";\nexport * from \"./components/page-contents\";\nexport * from \"./components/disclosure\";\nexport * from \"./components/dialog-frame\";\nexport * from \"./components/danger-confirm\";\n// `useConfirm()` — the promise-based replacement for `window.confirm`, one host per app.\nexport * from \"./components/confirm-dialog\";\n// A non-modal corner panel and its round trigger (keksdose's assistant launcher).\nexport * from \"./components/floating-panel\";\n// The count / clear / actions bar a selection brings up (keksdose's three copies).\nexport * from \"./components/bulk-action-bar\";\nexport * from \"./components/swatch-picker\";\nexport * from \"./components/icon-picker\";\nexport * from \"./components/choice-card\";\nexport * from \"./components/autocomplete\";\nexport * from \"./components/measured-grid\";\nexport { useWindowedRows } from \"./hooks/use-windowed-rows\";\nexport type { WindowedRows } from \"./hooks/use-windowed-rows\";\n// Named, not `export *`: file-button.tsx also holds the screening helpers the\n// dropzone shares, which are internal.\nexport { FileButton, useFilePicker, matchesAccept, DEFAULT_FILE_PICKER_LABELS } from \"./components/file-button\";\nexport type {\n FileButtonProps,\n UseFilePickerOptions,\n UseFilePickerReturn,\n FilePickerLabels,\n FileRejection,\n FileRejectionReason,\n FileScreenOptions,\n} from \"./components/file-button\";\nexport * from \"./components/treemap\";\nexport * from \"./components/series-chart\";\nexport * from \"./components/chart-zoom\";\nexport * from \"./components/toggle-legend\";\nexport * from \"./components/facing-pair\";\nexport * from \"./components/series-chart-labels\";\nexport * from \"./components/pie-chart\";\nexport * from \"./components/pie-chart-labels\";\nexport * from \"./components/account-settings\";\n// 0.12.0: the fourth account section, and the QR code TwoFactorSetting draws its\n// otpauth URI with (encoder in lib/qr-encode.ts — no dependency, see there).\nexport * from \"./components/passkeys-setting\";\nexport * from \"./components/qr-code\";\nexport { encodeQr } from \"./lib/qr-encode\";\nexport type { QrEncodeOptions, QrErrorCorrection, QrMatrix } from \"./lib/qr-encode\";\nexport * from \"./components/alert-banner\";\nexport * from \"./components/toggle-group\";\n// A wrapping set of links/buttons marked `aria-current` — Tabs-like looks, not tabs.\nexport * from \"./components/nav-pills\";\n// A pill carrying one VALUE — inert, a link, or a toggle, depending on which prop it is\n// given — and the list field built from it. Distinct from Button on purpose: a row of\n// buttons reads as \"choose an action\", a row of chips as \"here are the things\".\nexport * from \"./components/chip\";\nexport * from \"./components/wizard-stepper\";\nexport * from \"./components/hover-menu\";\nexport * from \"./components/modal\";\nexport * from \"./components/full-bleed-dialog\";\nexport * from \"./components/grouped-picker\";\nexport * from \"./components/file-dropzone\";\nexport * from \"./components/mini-calendar\";\nexport * from \"./components/calendar-heatmap\";\nexport * from \"./components/date-picker\";\nexport * from \"./components/tree-view\";\nexport * from \"./components/chart\";\n// 0.8.0 layout and feedback primitives the apps hand-rolled or took from shadcn/Radix.\nexport * from \"./components/description-list\";\n// The Save / Cancel row forms and dialogs end in, and the editable line-item repeater\n// (journal lines, lease components, invoice positions). 0.12.0.\nexport * from \"./components/form-actions\";\nexport * from \"./components/line-items\";\nexport * from \"./components/progress-bar\";\n// Named: skeleton.tsx also holds SKELETON_CLASS, the look StatTile shares — internal.\nexport { Skeleton } from \"./components/skeleton\";\nexport type { SkeletonProps, SkeletonShape } from \"./components/skeleton\";\n// 0.12.0: a centred spinner with words, signed figures and deltas, a render-error fallback.\nexport * from \"./components/loading-state\";\nexport * from \"./components/signed-amount\";\nexport * from \"./components/error-boundary\";\nexport * from \"./components/copy-button\";\nexport { AuthedImage, DEFAULT_AUTHED_IMAGE_LABELS } from \"./components/authed-image\";\nexport type { AuthedImageProps, AuthedImageLabels } from \"./components/authed-image\";\nexport * from \"./components/image-grid\";\nexport * from \"./components/lightbox\";\nexport * from \"./components/button-group\";\nexport * from \"./components/table\";\nexport * from \"./components/separator\";\n// Named: scroll-area.tsx also holds the overflow hook and scrollbar class Table shares.\nexport { ScrollArea } from \"./components/scroll-area\";\nexport type { ScrollAreaProps } from \"./components/scroll-area\";\n// 0.10.0 rows, menu rows, text primitives and page chrome the apps drew by hand.\nexport * from \"./components/list\";\nexport * from \"./components/menu-item\";\nexport * from \"./components/text\";\nexport * from \"./components/status-dot\";\nexport * from \"./components/page-header\";\nexport * from \"./components/breadcrumbs\";\n// The one inline link (kastlan's EntityLink / CellLink / legal and source links), through\n// `<UiKitProvider linkComponent>`. Named: the file also holds the kit's internal link picker.\nexport { TextLink } from \"./components/text-link\";\nexport type {\n TextLinkProps,\n TextLinkRenderProps,\n TextLinkTone,\n TextLinkUnderline,\n TextLinkCurrent,\n} from \"./components/text-link\";\n// The toast layer over sonner (an OPTIONAL peer, loaded lazily): `toast` mirrors sonner's\n// API so apps migrate by swapping the import; `<Toaster>` carries the placement, theme,\n// tones and z-index both apps had wired by hand.\nexport { toast, Toaster, DEFAULT_TOAST_LABELS, TOAST_ACTION_DURATION } from \"./components/toast\";\nexport type {\n ToastId,\n ToastAction,\n ToastOptions,\n ToastPromiseOptions,\n ToastUndoOptions,\n ToastRedoOptions,\n ToastLabels,\n ToastPosition,\n ToastSwipeDirection,\n ToasterOffset,\n ToasterProps,\n} from \"./components/toast\";\n\n// ── data-table suite ─────────────────────────────────────────────────────────\nexport * from \"./components/data-table-labels\";\nexport * from \"./components/data-table-sort\";\nexport * from \"./components/data-table-filters\";\nexport * from \"./components/data-table-pagination\";\nexport { FilterPopover } from \"./components/data-table-filter-popover\";\n// data-table re-exports SortState internally; export the rest explicitly to\n// avoid a duplicate SortState star-export (it comes from data-table-sort).\nexport { DataTable } from \"./components/data-table\";\nexport type {\n DataTableColumn,\n DataTableCellProps,\n DataTableHeadProps,\n DataTableProps,\n ServerPagination,\n FilterState,\n MobileSwipeActions,\n DataTableDensity,\n DataTableChrome,\n DataTableRowAction,\n} from \"./components/data-table\";\n// 0.12.0: the yes/no cell, and a server table's state held by its owner, URL-synced.\nexport * from \"./components/data-table-cells\";\nexport { useTableUrlState, readTableUrlState } from \"./components/use-table-state\";\nexport type {\n TableUrlState,\n TableUrlSync,\n TableUrlStateProps,\n UseTableUrlStateOptions,\n UseTableUrlStateReturn,\n} from \"./components/use-table-state\";\n\n// ── shell (composable app chrome) ────────────────────────────────────────────\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\nexport * from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/role-switcher\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n\n// ── feedback ─────────────────────────────────────────────────────────────────\n// The form somebody files a report with, and the parts an inbox is built from:\n// the status vocabulary both apps share value for value, the policy about what a\n// row may move to from where it stands, and the look of a status control, a\n// category badge and an opened report. The app still wires its own API, columns\n// and strings — see the note at the top of `feedback-inbox.tsx` for what is\n// deliberately left to it.\nexport * from \"./feedback/feedback-attachment\";\nexport * from \"./feedback/feedback-dialog\";\nexport * from \"./feedback/feedback-inbox\";\nexport * from \"./feedback/feedback-thread\";\n\n// ── multi-step wizard ────────────────────────────────────────────────────────\n// The engine (step machine, validators, collected data), the chrome around it\n// (indicator + nav bar + cancel confirm) and the review step it ends on.\n// `components/wizard-stepper` next door is a different, much smaller thing: a bare\n// step indicator for a two-step import flow, with no engine.\n//\n// Two modules were removed here rather than kept as stock. `wizard-field` held a\n// `WizardField`/`WizardSelectField` pair that had never been rendered in any app —\n// a field group nothing was built from, which this comment used to advertise. And\n// `use-rhf-wizard-step` was the only thing in the package that touched\n// react-hook-form, for a hook neither consumer called: keeping it meant an optional\n// peer, a dev dependency and a type import in every consumer's typecheck, bought\n// for nobody. A design system may carry stock; untested stock nothing has ever\n// rendered is not stock, it is a liability with an export.\nexport * from \"./wizard/types\";\nexport * from \"./wizard/use-wizard\";\nexport * from \"./wizard/wizard-context\";\nexport * from \"./wizard/validation\";\nexport * from \"./wizard/wizard-step\";\nexport * from \"./wizard/wizard-summary\";\nexport * from \"./wizard/stepper-nav\";\n\n// ── guided tours ─────────────────────────────────────────────────────────────\nexport * from \"./tour/tour\";\n\n// ── command palette / global search ──────────────────────────────────────────\nexport * from \"./search/command-palette\";\nexport * from \"./search/search-index\";\nexport * from \"./search/global-search\";\n\n// ── i18n: one label tree, one optional provider ──────────────────────────────\n// `UiKitLabels` names every string the kit renders; `<UiKitProvider labels locale>`\n// hands a translation to every component below it. Precedence: a component's own\n// prop > the provider > the English default. See src/i18n/kit-labels.tsx.\nexport * from \"./i18n/kit-labels\";\nexport * from \"./i18n/defaults\";\n"],"mappings":";AAMA,cAAc;AACd,SAAS,UAAU;AACnB,SAAS,QAAQ,mBAAmB;AAGpC,cAAc;AAEd,SAAS,YAAY,mBAAmB;AAIxC,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAEhC,SAAS,kBAAkB,wBAAwB,yBAAyB;AAM5E,SAAS,cAAc,uBAAuB;AAI9C,SAAS,oBAAoB;AAE7B,SAAS,mBAAmB;AAE5B,SAAS,yBAAyB;AAClC,SAAS,oBAAoB,uBAAuB;AACpD,SAAS,mBAAmB;AAG5B,SAAS,aAAa,oBAAoB;AAG1C,SAAS,aAAa,4BAA4B;AAIlD,SAAS,qBAAqB,aAAa,sBAAsB;AAGjE,SAAS,oBAAoB,uBAAuB;AAGpD,SAAS,WAAW,aAAa,qBAAqB;AAGtD,SAAS,oBAAoB;AAI7B,cAAc;AAKd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AAEd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,oBAAoB;AAE7B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAId,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,cAAc;AAEd,cAAc;AAEd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,uBAAuB;AAIhC,SAAS,YAAY,eAAe,eAAe,kCAAkC;AAUrF,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AACd,cAAc;AACd,SAAS,gBAAgB;AAEzB,cAAc;AACd,cAAc;AAEd,cAAc;AAId,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,gBAAgB;AAGzB,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,aAAa,mCAAmC;AAEzD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,kBAAkB;AAG3B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,SAAS,gBAAgB;AAWzB,SAAS,OAAO,SAAS,sBAAsB,6BAA6B;AAgB5E,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,qBAAqB;AAG9B,SAAS,iBAAiB;AAc1B,cAAc;AACd,SAAS,kBAAkB,yBAAyB;AAUpD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AASd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAgBd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AAMd,cAAc;AACd,cAAc;","names":[]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The full-document navigations the kit makes, behind one object the tests can spy on.
3
+ *
4
+ * jsdom implements neither `location.reload` nor `location.replace`, and its
5
+ * `window.location` is unforgeable: a test cannot stub it by redefining the property
6
+ * once the suite runs each file in its own VM context (`pool: "vmThreads"`, which took
7
+ * the suite from ~3 minutes to ~1). `vi.spyOn(documentNavigation, "reload")` works in
8
+ * every pool. Internal, not part of the package's surface.
9
+ */
10
+ declare const documentNavigation: {
11
+ reload(): void;
12
+ replace(href: string): void;
13
+ };
14
+
15
+ export { documentNavigation };
@@ -0,0 +1,13 @@
1
+ "use client";
2
+ const documentNavigation = {
3
+ reload() {
4
+ window.location.reload();
5
+ },
6
+ replace(href) {
7
+ window.location.replace(href);
8
+ }
9
+ };
10
+ export {
11
+ documentNavigation
12
+ };
13
+ //# sourceMappingURL=document-navigation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/document-navigation.ts"],"sourcesContent":["/**\n * The full-document navigations the kit makes, behind one object the tests can spy on.\n *\n * jsdom implements neither `location.reload` nor `location.replace`, and its\n * `window.location` is unforgeable: a test cannot stub it by redefining the property\n * once the suite runs each file in its own VM context (`pool: \"vmThreads\"`, which took\n * the suite from ~3 minutes to ~1). `vi.spyOn(documentNavigation, \"reload\")` works in\n * every pool. Internal, not part of the package's surface.\n */\nexport const documentNavigation = {\n reload(): void {\n window.location.reload();\n },\n replace(href: string): void {\n window.location.replace(href);\n },\n};\n"],"mappings":";AASO,MAAM,qBAAqB;AAAA,EAChC,SAAe;AACb,WAAO,SAAS,OAAO;AAAA,EACzB;AAAA,EACA,QAAQ,MAAoB;AAC1B,WAAO,SAAS,QAAQ,IAAI;AAAA,EAC9B;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eifi1/ui-kit",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
4
4
  "type": "module",
5
5
  "description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
6
6
  "_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
@@ -68,8 +68,8 @@
68
68
  "LICENSE"
69
69
  ],
70
70
  "scripts": {
71
- "typecheck": "tsc --noEmit && tsc -p tsconfig.node.json",
72
- "lint": "eslint . --max-warnings 0",
71
+ "typecheck": "tsc --noEmit --incremental --tsBuildInfoFile node_modules/.cache/tsc/app.tsbuildinfo && tsc -p tsconfig.node.json --incremental --tsBuildInfoFile node_modules/.cache/tsc/node.tsbuildinfo",
72
+ "lint": "eslint . --max-warnings 0 --cache --cache-location node_modules/.cache/eslint/",
73
73
  "check:tokens": "node scripts/check-token-discipline.mjs",
74
74
  "test:coverage": "vitest run --coverage",
75
75
  "check:graph": "node scripts/check-module-graph.mjs",
@@ -77,7 +77,10 @@
77
77
  "check:tailwind": "node scripts/check-tailwind-scan.mjs",
78
78
  "check:security": "npm audit --audit-level=high",
79
79
  "check:commits": "node scripts/check-commit-range.cjs",
80
- "check": "npm run typecheck && npm run lint && npm run check:tokens && npm run test:coverage && npm run build && npm run check:graph && npm run check:package && npm run build:showcase && npm run check:tailwind && npm run check:security && npm run check:commits",
80
+ "check:static": "npm run typecheck && npm run lint && npm run check:tokens && npm run check:security && npm run check:commits",
81
+ "check:test": "npm run test:coverage",
82
+ "check:build": "npm run build && npm run check:graph && npm run check:package && npm run build:showcase && npm run check:tailwind",
83
+ "check": "node scripts/check.mjs",
81
84
  "test": "vitest run",
82
85
  "test:watch": "vitest",
83
86
  "build": "node scripts/tsup-quiet.mjs && node scripts/fix-esm-extensions.mjs",
@@ -172,6 +172,14 @@ interface AmountInputProps {
172
172
  /** The largest amount the field settles on (a deposit release capped at the
173
173
  * balance). A higher figure is lowered to it when it settles. */
174
174
  max?: number;
175
+ /**
176
+ * The SETTLED figure, dot-decimal, sign included — fired on blur, on Enter, on the
177
+ * numpad's "=" and on every result the calculator writes back, never on a keystroke
178
+ * ("1200+" is not a figure yet). `onChange` still reports every keystroke; this is
179
+ * for a host that stores a number and must not see drafts (kastlan 52; see
180
+ * {@link MoneyField}, which is built on it).
181
+ */
182
+ onCommit?: (value: string) => void;
175
183
  }
176
184
 
177
185
  /** The currency's minor unit (CHF 2, JPY 0), or `undefined` for no currency or a code
@@ -295,7 +303,7 @@ function isResultOf(previous: string, text: string, settle: (text: string) => st
295
303
  }
296
304
 
297
305
  export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
298
- ({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid: invalidProp, className, id, ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames, digits: digitsProp, min, max }, ref) => {
306
+ ({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid: invalidProp, className, id, ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames, digits: digitsProp, min, max, onCommit }, ref) => {
299
307
  const generatedId = useId();
300
308
  const fieldId = id ?? generatedId;
301
309
  const invalid = Boolean(invalidProp) || ariaInvalid === true || ariaInvalid === "true";
@@ -399,7 +407,9 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
399
407
  };
400
408
  const commit = () => {
401
409
  setDraft(null);
402
- handleText(settle(commitExpression(shown)));
410
+ const settled = settle(commitExpression(shown));
411
+ handleText(settled);
412
+ onCommit?.(settled);
403
413
  };
404
414
  const typed = (raw: string) => {
405
415
  setDraft(raw);
@@ -533,7 +543,9 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
533
543
  value={display}
534
544
  onChange={(result, expression) => {
535
545
  setDraft(null);
536
- handleText(settle(result), expression);
546
+ const settled = settle(result);
547
+ handleText(settled, expression);
548
+ onCommit?.(settled);
537
549
  }}
538
550
  className="px-1.5"
539
551
  ariaLabel={labels?.calculatorTrigger}
@@ -622,7 +634,9 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
622
634
  onChange={typed}
623
635
  onResult={(result) => {
624
636
  setDraft(null);
625
- handleText(result);
637
+ const settled = settle(result);
638
+ handleText(settled);
639
+ onCommit?.(settled);
626
640
  }}
627
641
  onDone={() => innerRef.current?.blur()}
628
642
  label={label}
@@ -5,6 +5,7 @@ import { useKitLabels } from "../i18n/kit-labels";
5
5
  import { Button, EmptyState } from "./ui";
6
6
  import { CopyButton } from "./copy-button";
7
7
  import { Disclosure } from "./disclosure";
8
+ import { documentNavigation } from "../lib/document-navigation";
8
9
 
9
10
  /** The words of the default fallback. */
10
11
  export interface ErrorBoundaryLabels {
@@ -277,7 +278,7 @@ function isOffline(): boolean {
277
278
 
278
279
  function reloadPage(): void {
279
280
  try {
280
- window.location.reload();
281
+ documentNavigation.reload();
281
282
  } catch {
282
283
  /* nothing left to do from here */
283
284
  }
@@ -0,0 +1,75 @@
1
+ import { useState, type ComponentProps, type Ref } from "react";
2
+ import { AmountInput } from "./amount-input";
3
+
4
+ type AmountInputProps = ComponentProps<typeof AmountInput>;
5
+
6
+ export interface MoneyFieldProps
7
+ extends Omit<AmountInputProps, "value" | "onChange" | "onCommit" | "negative" | "onNegativeChange" | "ref"> {
8
+ /** The amount, or `null` for an empty field. */
9
+ value: number | null;
10
+ /**
11
+ * The settled amount — on blur, on Enter, on the numpad's "=" and on a calculator
12
+ * result; never a half-typed "1200+". Rounded to the currency's minor unit (or
13
+ * `digits`) and clamped to `min` / `max`, like {@link AmountInput}. `null` when the
14
+ * field was emptied. Not fired when the settled amount equals `value`, so an
15
+ * Enter-then-blur saves once.
16
+ */
17
+ onCommit: (value: number | null) => void;
18
+ ref?: Ref<HTMLInputElement>;
19
+ }
20
+
21
+ /** The number a settled text reads as: `null` for empty, `undefined` for no number
22
+ * (a lone "-" or "."). */
23
+ function parseSettled(text: string): number | null | undefined {
24
+ const t = text.trim();
25
+ if (t === "") return null;
26
+ const n = Number(t);
27
+ return Number.isFinite(n) ? n : undefined;
28
+ }
29
+
30
+ /**
31
+ * {@link AmountInput} with a NUMBER in and a number out, for money edited outside a
32
+ * form — a budget row, a table cell (kastlan 52: new-budget-page edited a CHF amount per
33
+ * row with `NumberField` because AmountInput is string-valued and reports every
34
+ * keystroke, "1200+" included, so a number-state row needed its own parse-and-settle
35
+ * glue). This is that glue, the same one `RhfMoneyField` has: the text being typed
36
+ * lives here, and only a settled figure reaches `onCommit`.
37
+ *
38
+ * The currency chip, the calculator, the numpad, the locale's decimal mark, the
39
+ * rounding to the currency's minor unit and `min` / `max` are AmountInput's. A caller-
40
+ * owned sign (`negative`) is not offered: the figure here carries its own minus.
41
+ *
42
+ * ```tsx
43
+ * <MoneyField ariaLabel="Amount" currency="CHF" value={row.amount}
44
+ * onCommit={(amount) => updateRow(row.id, { amount })} />
45
+ * ```
46
+ */
47
+ export function MoneyField({ value, onCommit, ref, ...props }: MoneyFieldProps) {
48
+ const external = value === null ? "" : String(value);
49
+ // The draft is re-read from `value` only when `value` moves away from what the draft
50
+ // already says (a reset, another row's save), so a commit's own echo keeps the
51
+ // typist's spelling ("12.50" stays "12.50" when 12.5 comes back).
52
+ const [draft, setDraft] = useState(external);
53
+ const [seen, setSeen] = useState(value);
54
+ if (seen !== value) {
55
+ setSeen(value);
56
+ if (parseSettled(draft) !== value) setDraft(external);
57
+ }
58
+ return (
59
+ <AmountInput
60
+ {...props}
61
+ ref={ref}
62
+ value={draft}
63
+ onChange={setDraft}
64
+ onCommit={(text) => {
65
+ const next = parseSettled(text);
66
+ // No number ("-" alone): back to the stored amount rather than a guess.
67
+ if (next === undefined) {
68
+ setDraft(external);
69
+ return;
70
+ }
71
+ if (next !== value) onCommit(next);
72
+ }}
73
+ />
74
+ );
75
+ }
@@ -304,3 +304,17 @@ export function NumberField({
304
304
  </div>
305
305
  );
306
306
  }
307
+
308
+ /**
309
+ * A {@link NumberField} for whole numbers: `digits={0}` and `calculator={false}` as
310
+ * defaults, each overridable — the preset `RhfIntegerField` (`@eifi1/ui-kit/rhf`) applies to a
311
+ * form-bound field, for the plain one (kastlan 51: a building's construction year in a
312
+ * wizard's local state, an entity id in the document upload spelled the two props out
313
+ * by hand).
314
+ *
315
+ * `??` rather than a spread over the defaults, so a wrapper forwarding its own optional
316
+ * `digits={props.digits}` still gets the preset.
317
+ */
318
+ export function IntegerField(props: NumberFieldProps) {
319
+ return <NumberField {...props} digits={props.digits ?? 0} calculator={props.calculator ?? false} />;
320
+ }
@@ -1,3 +1,4 @@
1
+ import { useLayoutEffect, useRef, useState } from "react";
1
2
  import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";
2
3
  import { cn } from "../lib/cn";
3
4
  import { SECTION_LABEL_CLASS } from "./text";
@@ -168,6 +169,37 @@ export function PageHeader({
168
169
  // wraps instead: actions that do not fit go to a line of their own, as the app's old
169
170
  // `flex-wrap` header did.
170
171
  const keepsWords = mobileLayout === "inline" && !truncateTitle;
172
+ // keksdose, after H1: once the actions have wrapped below the title, the phone's
173
+ // header was THREE rows — title / actions / secondary — where the app's old header
174
+ // shared the second row between the month nav and the toggles (~48px of phone
175
+ // height). So the secondary group's full-width basis (G6b: a row of its own) is
176
+ // dropped exactly when the actions already sit below the title; then it joins their
177
+ // row if it fits, and wraps under them if not. While the actions share the title's
178
+ // row, nothing changes: the secondary group never climbs onto the title's row.
179
+ //
180
+ // CSS cannot see "the previous item wrapped", so it is measured — by a
181
+ // ResizeObserver, whose first callback runs on `observe`. It is stable: the
182
+ // secondary group comes after the actions, so its basis cannot move them. No
183
+ // ResizeObserver (jsdom) or no layout keeps the basis, the 0.14.0 layout.
184
+ const measuresWrap = keepsWords && hasSecondary && actions != null;
185
+ const titleRef = useRef<HTMLDivElement>(null);
186
+ const actionsRef = useRef<HTMLDivElement>(null);
187
+ const [actionsWrapped, setActionsWrapped] = useState(false);
188
+ useLayoutEffect(() => {
189
+ const titleBox = titleRef.current;
190
+ const actionsBox = actionsRef.current;
191
+ if (!measuresWrap || !titleBox || !actionsBox || typeof ResizeObserver === "undefined") return;
192
+ const observer = new ResizeObserver(() => {
193
+ const t = titleBox.getBoundingClientRect();
194
+ const a = actionsBox.getBoundingClientRect();
195
+ // Unmeasured (display: none, no layout): keep the full-width basis.
196
+ setActionsWrapped(t.height > 0 && a.height > 0 && a.top >= t.bottom - 1);
197
+ });
198
+ observer.observe(titleBox);
199
+ observer.observe(actionsBox);
200
+ if (titleBox.parentElement) observer.observe(titleBox.parentElement);
201
+ return () => observer.disconnect();
202
+ }, [measuresWrap]);
171
203
  return (
172
204
  <div {...rest} className={cn("flex min-w-0 flex-col gap-2", className)}>
173
205
  {breadcrumbs}
@@ -184,7 +216,7 @@ export function PageHeader({
184
216
  align,
185
217
  )}
186
218
  >
187
- <div className={cn("flex-1", keepsWords ? "min-w-min" : "min-w-0")}>
219
+ <div ref={titleRef} className={cn("flex-1", keepsWords ? "min-w-min" : "min-w-0")}>
188
220
  {eyebrow != null && <p className={cn(SECTION_LABEL_CLASS.xs, "mb-1")}>{eyebrow}</p>}
189
221
  <Heading
190
222
  className={cn(
@@ -198,7 +230,10 @@ export function PageHeader({
198
230
  {description != null && <p className="mt-1 text-sm text-[var(--text-muted)]">{description}</p>}
199
231
  </div>
200
232
  {actions != null && (
201
- <div className={cn("flex shrink-0 flex-wrap items-center gap-2", hasSecondary && "sm:order-2")}>
233
+ <div
234
+ ref={actionsRef}
235
+ className={cn("flex shrink-0 flex-wrap items-center gap-2", hasSecondary && "sm:order-2")}
236
+ >
202
237
  {actions}
203
238
  </div>
204
239
  )}
@@ -206,7 +241,7 @@ export function PageHeader({
206
241
  <div
207
242
  className={cn(
208
243
  "flex flex-wrap items-center gap-2 sm:order-1 sm:shrink-0",
209
- wrapsSecondary && "basis-full sm:basis-auto",
244
+ wrapsSecondary && (measuresWrap && actionsWrapped ? "sm:basis-auto" : "basis-full sm:basis-auto"),
210
245
  )}
211
246
  >
212
247
  {secondaryActions}
@@ -4,6 +4,7 @@ import type { LucideIcon } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from "../i18n/kit-labels";
6
6
  import type { KitLinkComponent } from "../i18n/kit-labels";
7
+ import { documentNavigation } from "../lib/document-navigation";
7
8
 
8
9
  /* ── Choosing what draws a link ──────────────────────────────────────────── */
9
10
 
@@ -98,7 +99,7 @@ export function replacingClick(
98
99
  onClick?.(event);
99
100
  if (!isPlainNavigationClick(event)) return;
100
101
  event.preventDefault();
101
- window.location.replace(href);
102
+ documentNavigation.replace(href);
102
103
  };
103
104
  }
104
105
 
@@ -53,6 +53,9 @@ export interface FeedbackDialogTextLabels {
53
53
  category: string;
54
54
  subject: string;
55
55
  body: string;
56
+ /** The body's label when `requireBody={false}`: "What happened? (optional)".
57
+ * Optional, so a complete `UiKitLabels` typed before 0.14.2 still compiles. */
58
+ bodyOptional?: string;
56
59
  /** The heading over the attachment buttons. */
57
60
  attachment: string;
58
61
  /** The line beside the buttons naming the shortcut. */
@@ -66,6 +69,7 @@ export const DEFAULT_FEEDBACK_DIALOG_LABELS: FeedbackDialogTextLabels = {
66
69
  category: "Category",
67
70
  subject: "Subject",
68
71
  body: "What happened?",
72
+ bodyOptional: "What happened? (optional)",
69
73
  attachment: "Screenshot",
70
74
  submitHint: "Ctrl/⌘ + Enter to send",
71
75
  cancel: "Cancel",
@@ -108,6 +112,7 @@ export function FeedbackDialog({
108
112
  maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
109
113
  onAttachmentError,
110
114
  onCaptureScreenshot,
115
+ requireBody = true,
111
116
  }: {
112
117
  open: boolean;
113
118
  onClose: () => void;
@@ -129,6 +134,12 @@ export function FeedbackDialog({
129
134
  * attachment"; the returned file is fed through the same validation + preview.
130
135
  */
131
136
  onCaptureScreenshot?: () => Promise<File | null>;
137
+ /**
138
+ * `false`: a report with only a subject can be sent, and the body's label says it is
139
+ * optional (`bodyOptional`, unless `labels.body` is passed). Default `true`, the
140
+ * dialog's behaviour until 0.14.2 (keksdose K1: its backend takes title-only reports).
141
+ */
142
+ requireBody?: boolean;
132
143
  }) {
133
144
  const [title, setTitle] = useState("");
134
145
  const [body, setBody] = useState("");
@@ -151,7 +162,9 @@ export function FeedbackDialog({
151
162
  }
152
163
  }
153
164
 
154
- const canSubmit = !!title && !!body && !submitting;
165
+ // Trimmed: a subject or body of spaces is empty (keksdose's API answers a blank title
166
+ // with a 422, and a blank report is no report).
167
+ const canSubmit = !!title.trim() && (!requireBody || !!body.trim()) && !submitting;
155
168
  const trySubmit = () => {
156
169
  if (canSubmit) void onSubmit({ title, body, category, attachment });
157
170
  };
@@ -174,7 +187,12 @@ export function FeedbackDialog({
174
187
  <Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant="display" />
175
188
  );
176
189
  const bodyField = (
177
- <Textarea rows={5} label={labels.body} value={body} onChange={(e) => setBody(e.target.value)} />
190
+ <Textarea
191
+ rows={5}
192
+ label={requireBody ? labels.body : (labelsProp?.body ?? labels.bodyOptional ?? labels.body)}
193
+ value={body}
194
+ onChange={(e) => setBody(e.target.value)}
195
+ />
178
196
  );
179
197
  const attachmentField = (
180
198
  <FeedbackAttachmentField
@@ -26,6 +26,7 @@ export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
26
26
  category: "Kategorie",
27
27
  subject: "Betreff",
28
28
  body: "Was ist passiert?",
29
+ bodyOptional: "Was ist passiert? (optional)",
29
30
  attachment: "Screenshot",
30
31
  submitHint: "Strg/⌘ + Enter zum Senden",
31
32
  cancel: "Abbrechen",
@@ -27,6 +27,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
27
27
  category: "Categoría",
28
28
  subject: "Asunto",
29
29
  body: "¿Qué ha pasado?",
30
+ bodyOptional: "¿Qué ha pasado? (opcional)",
30
31
  attachment: "Captura de pantalla",
31
32
  submitHint: "Ctrl/⌘ + Intro para enviar",
32
33
  cancel: "Cancelar",
@@ -27,6 +27,7 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
27
27
  category: "Catégorie",
28
28
  subject: "Objet",
29
29
  body: "Que s’est-il passé\u202f?",
30
+ bodyOptional: "Que s’est-il passé\u202f? (facultatif)",
30
31
  attachment: "Capture d’écran",
31
32
  submitHint: "Ctrl/⌘ + Entrée pour envoyer",
32
33
  cancel: "Annuler",
@@ -26,6 +26,7 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
26
26
  category: "Kategória",
27
27
  subject: "Tárgy",
28
28
  body: "Mi történt?",
29
+ bodyOptional: "Mi történt? (nem kötelező)",
29
30
  attachment: "Képernyőkép",
30
31
  submitHint: "Ctrl/⌘ + Enter a küldéshez",
31
32
  cancel: "Mégse",
@@ -27,6 +27,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
27
27
  category: "Categoria",
28
28
  subject: "Oggetto",
29
29
  body: "Cosa è successo?",
30
+ bodyOptional: "Cosa è successo? (facoltativo)",
30
31
  attachment: "Screenshot",
31
32
  submitHint: "Ctrl/⌘ + Invio per inviare",
32
33
  cancel: "Annulla",
@@ -26,6 +26,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
26
26
  category: "类别",
27
27
  subject: "主题",
28
28
  body: "发生了什么?",
29
+ bodyOptional: "发生了什么?(可选)",
29
30
  attachment: "截图",
30
31
  submitHint: "Ctrl/⌘ + Enter 发送",
31
32
  cancel: "取消",
package/src/index.ts CHANGED
@@ -83,6 +83,7 @@ export * from "./components/numpad-sheet";
83
83
  export * from "./components/number-input";
84
84
  export * from "./components/currency-select";
85
85
  export * from "./components/amount-input";
86
+ export * from "./components/money-field";
86
87
  export * from "./components/combobox";
87
88
  export * from "./components/picker-sheet";
88
89
  export * from "./components/entity-combobox";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The full-document navigations the kit makes, behind one object the tests can spy on.
3
+ *
4
+ * jsdom implements neither `location.reload` nor `location.replace`, and its
5
+ * `window.location` is unforgeable: a test cannot stub it by redefining the property
6
+ * once the suite runs each file in its own VM context (`pool: "vmThreads"`, which took
7
+ * the suite from ~3 minutes to ~1). `vi.spyOn(documentNavigation, "reload")` works in
8
+ * every pool. Internal, not part of the package's surface.
9
+ */
10
+ export const documentNavigation = {
11
+ reload(): void {
12
+ window.location.reload();
13
+ },
14
+ replace(href: string): void {
15
+ window.location.replace(href);
16
+ },
17
+ };