@entifix/react-controls 0.19.0 → 0.20.1

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 (203) hide show
  1. package/README.md +8 -0
  2. package/dist/i18n/catalog/en.d.ts.map +1 -1
  3. package/dist/i18n/catalog/en.js +55 -9
  4. package/dist/i18n/catalog/en.js.map +1 -1
  5. package/dist/i18n/catalog/es.d.ts +64 -5
  6. package/dist/i18n/catalog/es.d.ts.map +1 -1
  7. package/dist/i18n/catalog/es.js +61 -9
  8. package/dist/i18n/catalog/es.js.map +1 -1
  9. package/dist/i18n/catalog/index.d.ts +102 -10
  10. package/dist/i18n/catalog/index.d.ts.map +1 -1
  11. package/dist/i18n/entity-labels.d.ts +14 -0
  12. package/dist/i18n/entity-labels.d.ts.map +1 -1
  13. package/dist/i18n/entity-labels.js +15 -0
  14. package/dist/i18n/entity-labels.js.map +1 -1
  15. package/dist/i18n/i18n-context.js +2 -2
  16. package/dist/i18n/i18n-context.js.map +1 -1
  17. package/dist/index.d.ts +6 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +6 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/primitives.d.ts +0 -1
  22. package/dist/primitives.d.ts.map +1 -1
  23. package/dist/primitives.js +0 -1
  24. package/dist/primitives.js.map +1 -1
  25. package/dist/theme/index.d.ts +1 -0
  26. package/dist/theme/index.d.ts.map +1 -1
  27. package/dist/theme/index.js +1 -0
  28. package/dist/theme/index.js.map +1 -1
  29. package/dist/theme/theme-context.d.ts +15 -3
  30. package/dist/theme/theme-context.d.ts.map +1 -1
  31. package/dist/theme/theme-context.js +49 -23
  32. package/dist/theme/theme-context.js.map +1 -1
  33. package/dist/theme/theme-cookie.d.ts +9 -0
  34. package/dist/theme/theme-cookie.d.ts.map +1 -0
  35. package/dist/theme/theme-cookie.js +8 -0
  36. package/dist/theme/theme-cookie.js.map +1 -0
  37. package/dist/theme/theme-script.d.ts +8 -6
  38. package/dist/theme/theme-script.d.ts.map +1 -1
  39. package/dist/theme/theme-script.js +21 -12
  40. package/dist/theme/theme-script.js.map +1 -1
  41. package/dist/ui/actions/action-surfaces.d.ts +1 -1
  42. package/dist/ui/actions/action-surfaces.js +1 -1
  43. package/dist/ui/actions/action-surfaces.js.map +1 -1
  44. package/dist/ui/actions/error-actions.d.ts +32 -0
  45. package/dist/ui/actions/error-actions.d.ts.map +1 -0
  46. package/dist/ui/actions/error-actions.js +40 -0
  47. package/dist/ui/actions/error-actions.js.map +1 -0
  48. package/dist/ui/actions/index.d.ts +3 -0
  49. package/dist/ui/actions/index.d.ts.map +1 -1
  50. package/dist/ui/actions/index.js +3 -0
  51. package/dist/ui/actions/index.js.map +1 -1
  52. package/dist/ui/actions/shortcut.d.ts +13 -0
  53. package/dist/ui/actions/shortcut.d.ts.map +1 -0
  54. package/dist/ui/actions/shortcut.js +18 -0
  55. package/dist/ui/actions/shortcut.js.map +1 -0
  56. package/dist/ui/actions/use-verb-invoker.d.ts +55 -0
  57. package/dist/ui/actions/use-verb-invoker.d.ts.map +1 -0
  58. package/dist/ui/actions/use-verb-invoker.js +98 -0
  59. package/dist/ui/actions/use-verb-invoker.js.map +1 -0
  60. package/dist/ui/atoms/cell-value/cell-value.d.ts.map +1 -1
  61. package/dist/ui/atoms/cell-value/cell-value.js +28 -14
  62. package/dist/ui/atoms/cell-value/cell-value.js.map +1 -1
  63. package/dist/ui/atoms/field-control/field-control.d.ts +1 -0
  64. package/dist/ui/atoms/field-control/field-control.d.ts.map +1 -1
  65. package/dist/ui/atoms/field-control/field-control.js +2 -0
  66. package/dist/ui/atoms/field-control/field-control.js.map +1 -1
  67. package/dist/ui/atoms/number-input/quantity-input.d.ts +8 -1
  68. package/dist/ui/atoms/number-input/quantity-input.d.ts.map +1 -1
  69. package/dist/ui/atoms/number-input/quantity-input.js +2 -1
  70. package/dist/ui/atoms/number-input/quantity-input.js.map +1 -1
  71. package/dist/ui/atoms/table/table.d.ts +12 -3
  72. package/dist/ui/atoms/table/table.d.ts.map +1 -1
  73. package/dist/ui/atoms/table/table.js +59 -10
  74. package/dist/ui/atoms/table/table.js.map +1 -1
  75. package/dist/ui/atoms/text/text.d.ts +2 -2
  76. package/dist/ui/atoms/text/text.d.ts.map +1 -1
  77. package/dist/ui/atoms/text/text.js.map +1 -1
  78. package/dist/ui/molecules/action-bar/action-bar.d.ts +76 -0
  79. package/dist/ui/molecules/action-bar/action-bar.d.ts.map +1 -0
  80. package/dist/ui/molecules/action-bar/action-bar.js +218 -0
  81. package/dist/ui/molecules/action-bar/action-bar.js.map +1 -0
  82. package/dist/ui/molecules/action-bar/index.d.ts +2 -0
  83. package/dist/ui/molecules/action-bar/index.d.ts.map +1 -0
  84. package/dist/ui/molecules/action-bar/index.js +3 -0
  85. package/dist/ui/molecules/action-bar/index.js.map +1 -0
  86. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.d.ts.map +1 -1
  87. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js +8 -10
  88. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js.map +1 -1
  89. package/dist/ui/molecules/bulk-result/bulk-result.d.ts +3 -3
  90. package/dist/ui/molecules/bulk-result/bulk-result.js +4 -4
  91. package/dist/ui/molecules/bulk-result/bulk-result.js.map +1 -1
  92. package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts +24 -9
  93. package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts.map +1 -1
  94. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js +36 -11
  95. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js.map +1 -1
  96. package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts +33 -20
  97. package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts.map +1 -1
  98. package/dist/ui/molecules/entity-record-card/entity-record-card.js +53 -20
  99. package/dist/ui/molecules/entity-record-card/entity-record-card.js.map +1 -1
  100. package/dist/ui/molecules/filter-builder/filter-builder.d.ts +8 -2
  101. package/dist/ui/molecules/filter-builder/filter-builder.d.ts.map +1 -1
  102. package/dist/ui/molecules/filter-builder/filter-builder.js +14 -5
  103. package/dist/ui/molecules/filter-builder/filter-builder.js.map +1 -1
  104. package/dist/ui/molecules/filter-chips/filter-chips.d.ts +16 -0
  105. package/dist/ui/molecules/filter-chips/filter-chips.d.ts.map +1 -0
  106. package/dist/ui/molecules/filter-chips/filter-chips.js +73 -0
  107. package/dist/ui/molecules/filter-chips/filter-chips.js.map +1 -0
  108. package/dist/ui/molecules/filter-chips/index.d.ts +2 -0
  109. package/dist/ui/molecules/filter-chips/index.d.ts.map +1 -0
  110. package/dist/ui/molecules/filter-chips/index.js +3 -0
  111. package/dist/ui/molecules/filter-chips/index.js.map +1 -0
  112. package/dist/ui/molecules/menu/menu.d.ts +11 -1
  113. package/dist/ui/molecules/menu/menu.d.ts.map +1 -1
  114. package/dist/ui/molecules/menu/menu.js +62 -2
  115. package/dist/ui/molecules/menu/menu.js.map +1 -1
  116. package/dist/ui/molecules/pagination/pagination.d.ts +5 -0
  117. package/dist/ui/molecules/pagination/pagination.d.ts.map +1 -1
  118. package/dist/ui/molecules/pagination/pagination.js +35 -18
  119. package/dist/ui/molecules/pagination/pagination.js.map +1 -1
  120. package/dist/ui/molecules/region-state/index.d.ts +2 -0
  121. package/dist/ui/molecules/region-state/index.d.ts.map +1 -0
  122. package/dist/ui/molecules/region-state/index.js +3 -0
  123. package/dist/ui/molecules/region-state/index.js.map +1 -0
  124. package/dist/ui/molecules/region-state/region-state.d.ts +42 -0
  125. package/dist/ui/molecules/region-state/region-state.d.ts.map +1 -0
  126. package/dist/ui/molecules/region-state/region-state.js +83 -0
  127. package/dist/ui/molecules/region-state/region-state.js.map +1 -0
  128. package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts +3 -1
  129. package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts.map +1 -1
  130. package/dist/ui/molecules/table-toolbar/table-toolbar.js +3 -2
  131. package/dist/ui/molecules/table-toolbar/table-toolbar.js.map +1 -1
  132. package/dist/ui/molecules/toast-region/toast-region.d.ts +63 -19
  133. package/dist/ui/molecules/toast-region/toast-region.d.ts.map +1 -1
  134. package/dist/ui/molecules/toast-region/toast-region.js +129 -42
  135. package/dist/ui/molecules/toast-region/toast-region.js.map +1 -1
  136. package/dist/ui/organisms/command-palette/command-palette.d.ts.map +1 -1
  137. package/dist/ui/organisms/command-palette/command-palette.js +19 -12
  138. package/dist/ui/organisms/command-palette/command-palette.js.map +1 -1
  139. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.d.ts.map +1 -1
  140. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js +0 -1
  141. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js.map +1 -1
  142. package/dist/ui/organisms/entity-form/entity-form.d.ts +1 -1
  143. package/dist/ui/organisms/entity-form/entity-form.d.ts.map +1 -1
  144. package/dist/ui/organisms/entity-form/entity-form.js +322 -214
  145. package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
  146. package/dist/ui/organisms/entity-form/entity-form.types.d.ts +16 -0
  147. package/dist/ui/organisms/entity-form/entity-form.types.d.ts.map +1 -1
  148. package/dist/ui/organisms/entity-form/entity-form.types.js.map +1 -1
  149. package/dist/ui/organisms/entity-table/entity-table.d.ts +1 -1
  150. package/dist/ui/organisms/entity-table/entity-table.d.ts.map +1 -1
  151. package/dist/ui/organisms/entity-table/entity-table.js +332 -197
  152. package/dist/ui/organisms/entity-table/entity-table.js.map +1 -1
  153. package/dist/ui/organisms/entity-table/entity-table.types.d.ts +19 -4
  154. package/dist/ui/organisms/entity-table/entity-table.types.d.ts.map +1 -1
  155. package/dist/ui/organisms/entity-table/entity-table.types.js.map +1 -1
  156. package/dist/ui/organisms/entity-table/header-sort.d.ts +18 -0
  157. package/dist/ui/organisms/entity-table/header-sort.d.ts.map +1 -0
  158. package/dist/ui/organisms/entity-table/header-sort.js +54 -0
  159. package/dist/ui/organisms/entity-table/header-sort.js.map +1 -0
  160. package/dist/ui/organisms/entity-table/pivot.d.ts +25 -0
  161. package/dist/ui/organisms/entity-table/pivot.d.ts.map +1 -0
  162. package/dist/ui/organisms/entity-table/pivot.js +106 -0
  163. package/dist/ui/organisms/entity-table/pivot.js.map +1 -0
  164. package/dist/ui/organisms/entity-table/use-result-announcement.d.ts +13 -0
  165. package/dist/ui/organisms/entity-table/use-result-announcement.d.ts.map +1 -0
  166. package/dist/ui/organisms/entity-table/use-result-announcement.js +31 -0
  167. package/dist/ui/organisms/entity-table/use-result-announcement.js.map +1 -0
  168. package/dist/ui/organisms/top-bar/live-indicator.d.ts +11 -0
  169. package/dist/ui/organisms/top-bar/live-indicator.d.ts.map +1 -0
  170. package/dist/ui/organisms/top-bar/live-indicator.js +46 -0
  171. package/dist/ui/organisms/top-bar/live-indicator.js.map +1 -0
  172. package/dist/ui/organisms/top-bar/top-bar.d.ts +11 -2
  173. package/dist/ui/organisms/top-bar/top-bar.d.ts.map +1 -1
  174. package/dist/ui/organisms/top-bar/top-bar.js +14 -5
  175. package/dist/ui/organisms/top-bar/top-bar.js.map +1 -1
  176. package/dist/ui/organisms/wizard/wizard.d.ts +1 -1
  177. package/dist/ui/organisms/wizard/wizard.d.ts.map +1 -1
  178. package/dist/ui/organisms/wizard/wizard.js +7 -1
  179. package/dist/ui/organisms/wizard/wizard.js.map +1 -1
  180. package/dist/ui/organisms/wizard/wizard.types.d.ts +8 -0
  181. package/dist/ui/organisms/wizard/wizard.types.d.ts.map +1 -1
  182. package/dist/ui/organisms/wizard/wizard.types.js.map +1 -1
  183. package/dist/ui/utils/live-updates.d.ts +24 -0
  184. package/dist/ui/utils/live-updates.d.ts.map +1 -0
  185. package/dist/ui/utils/live-updates.js +20 -0
  186. package/dist/ui/utils/live-updates.js.map +1 -0
  187. package/dist/ui/utils/use-focus-on-arrival.d.ts +14 -0
  188. package/dist/ui/utils/use-focus-on-arrival.d.ts.map +1 -0
  189. package/dist/ui/utils/use-focus-on-arrival.js +42 -0
  190. package/dist/ui/utils/use-focus-on-arrival.js.map +1 -0
  191. package/dist/ui/utils/use-hotkey.d.ts +44 -25
  192. package/dist/ui/utils/use-hotkey.d.ts.map +1 -1
  193. package/dist/ui/utils/use-hotkey.js +80 -48
  194. package/dist/ui/utils/use-hotkey.js.map +1 -1
  195. package/package.json +5 -5
  196. package/dist/ui/molecules/split-button/index.d.ts +0 -2
  197. package/dist/ui/molecules/split-button/index.d.ts.map +0 -1
  198. package/dist/ui/molecules/split-button/index.js +0 -3
  199. package/dist/ui/molecules/split-button/index.js.map +0 -1
  200. package/dist/ui/molecules/split-button/split-button.d.ts +0 -21
  201. package/dist/ui/molecules/split-button/split-button.d.ts.map +0 -1
  202. package/dist/ui/molecules/split-button/split-button.js +0 -35
  203. package/dist/ui/molecules/split-button/split-button.js.map +0 -1
@@ -1,31 +1,50 @@
1
- /** One chord, described the way a person would say it. */
2
- export interface Hotkey {
3
- /** The `KeyboardEvent.key`, compared case-insensitively. */
4
- readonly key: string;
5
- /** ⌘ on a Mac, Ctrl elsewhere. Either satisfies it. */
6
- readonly mod?: boolean;
7
- readonly shift?: boolean;
1
+ /**
2
+ * Where a set of shortcuts applies: the page, or one workspace tab. A tab's
3
+ * shortcuts fire only while it is the active tab, so two tabs each holding a
4
+ * form can both bind Mod+S without one saving the other.
5
+ */
6
+ export interface HotkeyScope {
7
+ readonly id: string;
8
+ readonly active: boolean;
8
9
  }
10
+ /** The page: every shortcut outside a workspace tab. */
11
+ export declare const PAGE_SCOPE: HotkeyScope;
9
12
  /**
10
- * Fire `handler` when any of `hotkeys` is pressed, anywhere on the page.
11
- *
12
- * The repo's first global shortcut, so two things are decided here rather than
13
- * at each future call site.
14
- *
15
- * **It skips while focus is in an editable element**, because a page-wide
16
- * listener that does not would eat the chord a person meant for the field they
17
- * are typing in. The palette's own input is exempt in practice: the palette
18
- * unmounts its trigger's listener behaviour by simply not being open.
13
+ * Scopes the shortcuts bound below it — a workspace tab passes its own id and
14
+ * whether it is the active tab.
15
+ */
16
+ export declare const HotkeyScopeProvider: import("react").Provider<HotkeyScope>;
17
+ /** For specs: a page with nothing bound. */
18
+ export declare const resetHotkeysForTesting: () => void;
19
+ export interface HotkeyOptions {
20
+ /**
21
+ * Bind only if the chord is free, and stay silent if it is not. Without it
22
+ * a second binding of a chord in one scope throws at registration: two
23
+ * handlers for one keystroke is a bug, and whichever fired first would
24
+ * decide what the reader got. A built-in default — a form's Mod+S — takes
25
+ * this, so a host's own binding wins.
26
+ */
27
+ readonly ifFree?: boolean;
28
+ /** Bound only while true; a disabled control's shortcut does nothing. */
29
+ readonly enabled?: boolean;
30
+ }
31
+ /**
32
+ * Runs `handler` on any of `chords` — `"Mod+K"`, `"Mod+Shift+P"` — within
33
+ * the nearest {@link HotkeyScope} (ADR 0026).
19
34
  *
20
- * **It calls `preventDefault`**, which is what stops the browser acting on a
21
- * chord it also claims. ⚠️ That is not guaranteed to work: Chrome, Safari and
22
- * Edge dispatch these to the page and honour it, Firefox reserves some chords
23
- * and ignores `defaultPrevented` for them. So no single binding may be
24
- * load-bearing — bind more than one, and always leave a visible control that
25
- * does the same thing.
35
+ * - **Modifier chords only**: every one holds `Mod`, so no single key or
36
+ * Shift+key steals a character from a reader typing (WCAG 2.1.4), and no
37
+ * Alt+letter, which macOS Option composes into a character. The editing
38
+ * chords — Mod+A, C, V, X, Y, Z — stay the field's. Anything else throws
39
+ * when it is bound.
40
+ * - **They fire inside a text field**: a chord is never typing, and Mod+S
41
+ * is exactly what a reader presses with the caret in a field.
42
+ * - **A chord is held once per scope**, so a conflict is refused when it is
43
+ * bound rather than resolved by whoever listens first.
26
44
  *
27
- * `handler` is held in a ref so a caller may pass an inline closure without
28
- * re-subscribing the listener on every render.
45
+ * It calls `preventDefault`, which stops the browser's own action for the
46
+ * chord where the browser lets it; no binding should be the only way to do
47
+ * anything, so a visible control always does the same thing.
29
48
  */
30
- export declare function useHotkey(hotkeys: readonly Hotkey[], handler: (event: KeyboardEvent) => void): void;
49
+ export declare function useHotkey(chords: readonly string[], handler: (event: KeyboardEvent) => void, { ifFree, enabled }?: HotkeyOptions): void;
31
50
  //# sourceMappingURL=use-hotkey.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-hotkey.d.ts","sourceRoot":"","sources":["../../../src/ui/utils/use-hotkey.ts"],"names":[],"mappings":"AAIA,0DAA0D;AAC1D,MAAM,WAAW,MAAM;IACrB,4DAA4D;IAC5D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AA2BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GACtC,IAAI,CAwBN"}
1
+ {"version":3,"file":"use-hotkey.d.ts","sourceRoot":"","sources":["../../../src/ui/utils/use-hotkey.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,wDAAwD;AACxD,eAAO,MAAM,UAAU,EAAE,WAA0C,CAAC;AAIpE;;;GAGG;AACH,eAAO,MAAM,mBAAmB,uCAA8B,CAAC;AAgC/D,4CAA4C;AAC5C,eAAO,MAAM,sBAAsB,QAAO,IAAqB,CAAC;AAUhE,MAAM,WAAW,aAAa;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,yEAAyE;IACzE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,OAAO,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,EACvC,EAAE,MAAc,EAAE,OAAc,EAAE,GAAE,aAAkB,GACrD,IAAI,CAqCN"}
@@ -1,68 +1,100 @@
1
1
  'use client';
2
- import { useEffect, useRef } from 'react';
3
- const isEditable = (target)=>{
4
- if (!(target instanceof HTMLElement)) return false;
5
- const tag = target.tagName;
6
- if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
7
- // `closest`, not `isContentEditable`: the property is true for a *descendant*
8
- // of an editable host but is not implemented in jsdom, so the branch that
9
- // matters most for a rich editor would be the one no test could reach. The
10
- // selector covers the host and everything inside it, and the `:not` arm is
11
- // what keeps `contenteditable="false"` — the way a widget opts a subtree back
12
- // out — from reading as editable.
13
- return target.closest('[contenteditable]:not([contenteditable="false"])') !== null;
2
+ import { parseChord } from '@entifix/core';
3
+ import { createContext, useContext, useEffect, useRef } from 'react';
4
+ /** The page: every shortcut outside a workspace tab. */ export const PAGE_SCOPE = {
5
+ id: 'page',
6
+ active: true
14
7
  };
15
- const matches = (event, hotkey)=>{
16
- var _hotkey_mod, _hotkey_shift;
17
- if (event.key.toLowerCase() !== hotkey.key.toLowerCase()) return false;
18
- // ⌘ and Ctrl are the same intent on different platforms, so either satisfies
19
- // `mod` rather than the hook asking the caller to sniff the platform. Nothing
20
- // in the fleet binds a chord that means one thing with ⌘ and another with Ctrl.
21
- const mod = event.metaKey || event.ctrlKey;
22
- if (((_hotkey_mod = hotkey.mod) != null ? _hotkey_mod : false) !== mod) return false;
23
- return ((_hotkey_shift = hotkey.shift) != null ? _hotkey_shift : false) === event.shiftKey;
8
+ const HotkeyScopeContext = createContext(PAGE_SCOPE);
9
+ /**
10
+ * Scopes the shortcuts bound below it — a workspace tab passes its own id and
11
+ * whether it is the active tab.
12
+ */ export const HotkeyScopeProvider = HotkeyScopeContext.Provider;
13
+ /**
14
+ * Which chords each scope holds, for refusing a second binding of one. A
15
+ * chord is held at most once per scope — a second binding is refused or
16
+ * yields — so a set is enough.
17
+ */ const bound = new Map();
18
+ const holds = (scope, chord)=>{
19
+ var _bound_get;
20
+ return ((_bound_get = bound.get(scope)) == null ? void 0 : _bound_get.has(chord)) === true;
24
21
  };
25
22
  /**
26
- * Fire `handler` when any of `hotkeys` is pressed, anywhere on the page.
27
- *
28
- * The repo's first global shortcut, so two things are decided here rather than
29
- * at each future call site.
30
- *
31
- * **It skips while focus is in an editable element**, because a page-wide
32
- * listener that does not would eat the chord a person meant for the field they
33
- * are typing in. The palette's own input is exempt in practice: the palette
34
- * unmounts its trigger's listener behaviour by simply not being open.
23
+ * Whether `chord` is taken where `scope` would bind it: in the same scope, or
24
+ * — since the page's shortcuts apply inside every tab — between the page and
25
+ * a tab.
26
+ */ function isTaken(scope, chord) {
27
+ if (holds(scope, chord)) return true;
28
+ if (scope === PAGE_SCOPE.id) return [
29
+ ...bound.values()
30
+ ].some((chords)=>chords.has(chord));
31
+ return holds(PAGE_SCOPE.id, chord);
32
+ }
33
+ function bind(scope, chord) {
34
+ var _bound_get;
35
+ const chords = (_bound_get = bound.get(scope)) != null ? _bound_get : new Set();
36
+ bound.set(scope, chords.add(chord));
37
+ return ()=>{
38
+ chords.delete(chord);
39
+ };
40
+ }
41
+ /** For specs: a page with nothing bound. */ export const resetHotkeysForTesting = ()=>bound.clear();
42
+ const matches = (event, chord)=>event.key.toLowerCase() === chord.key && // ⌘ and Ctrl are the same intent on different platforms, so either one
43
+ // satisfies `Mod` rather than each caller sniffing the platform.
44
+ (event.metaKey || event.ctrlKey) && event.shiftKey === chord.shift && event.altKey === chord.alt;
45
+ /**
46
+ * Runs `handler` on any of `chords` — `"Mod+K"`, `"Mod+Shift+P"` — within
47
+ * the nearest {@link HotkeyScope} (ADR 0026).
35
48
  *
36
- * **It calls `preventDefault`**, which is what stops the browser acting on a
37
- * chord it also claims. ⚠️ That is not guaranteed to work: Chrome, Safari and
38
- * Edge dispatch these to the page and honour it, Firefox reserves some chords
39
- * and ignores `defaultPrevented` for them. So no single binding may be
40
- * load-bearing — bind more than one, and always leave a visible control that
41
- * does the same thing.
49
+ * - **Modifier chords only**: every one holds `Mod`, so no single key or
50
+ * Shift+key steals a character from a reader typing (WCAG 2.1.4), and no
51
+ * Alt+letter, which macOS Option composes into a character. The editing
52
+ * chords — Mod+A, C, V, X, Y, Z — stay the field's. Anything else throws
53
+ * when it is bound.
54
+ * - **They fire inside a text field**: a chord is never typing, and Mod+S
55
+ * is exactly what a reader presses with the caret in a field.
56
+ * - **A chord is held once per scope**, so a conflict is refused when it is
57
+ * bound rather than resolved by whoever listens first.
42
58
  *
43
- * `handler` is held in a ref so a caller may pass an inline closure without
44
- * re-subscribing the listener on every render.
45
- */ export function useHotkey(hotkeys, handler) {
59
+ * It calls `preventDefault`, which stops the browser's own action for the
60
+ * chord where the browser lets it; no binding should be the only way to do
61
+ * anything, so a visible control always does the same thing.
62
+ */ export function useHotkey(chords, handler, { ifFree = false, enabled = true } = {}) {
63
+ const scope = useContext(HotkeyScopeContext);
46
64
  const handlerRef = useRef(handler);
65
+ const activeRef = useRef(scope.active);
47
66
  useEffect(()=>{
48
67
  handlerRef.current = handler;
68
+ activeRef.current = scope.active;
49
69
  });
50
- // Serialized rather than passed by identity: callers build the array inline,
51
- // so the array itself is a new object every render and would re-subscribe the
52
- // listener on each one.
53
- const signature = JSON.stringify(hotkeys);
70
+ // Joined rather than passed by identity: callers write the list inline.
71
+ const signature = chords.join(' ');
54
72
  useEffect(()=>{
55
- const bindings = JSON.parse(signature);
73
+ if (!enabled || signature === '') return;
74
+ const parsed = signature.split(' ').map(parseChord);
75
+ const free = parsed.filter((chord)=>!isTaken(scope.id, chord.text));
76
+ if (free.length < parsed.length && !ifFree) {
77
+ const taken = parsed.find((chord)=>!free.includes(chord));
78
+ throw new Error(`${taken.text} is already bound here. A chord does one thing in a scope: unbind the other, or pass \`ifFree\` to the default that should yield.`);
79
+ }
80
+ if (free.length === 0) return;
81
+ const unbind = free.map((chord)=>bind(scope.id, chord.text));
56
82
  const onKeyDown = (event)=>{
57
- if (isEditable(event.target)) return;
58
- if (!bindings.some((hotkey)=>matches(event, hotkey))) return;
83
+ if (!activeRef.current) return;
84
+ if (!free.some((chord)=>matches(event, chord))) return;
59
85
  event.preventDefault();
60
86
  handlerRef.current(event);
61
87
  };
62
88
  document.addEventListener('keydown', onKeyDown);
63
- return ()=>document.removeEventListener('keydown', onKeyDown);
89
+ return ()=>{
90
+ document.removeEventListener('keydown', onKeyDown);
91
+ for (const release of unbind)release();
92
+ };
64
93
  }, [
65
- signature
94
+ signature,
95
+ scope.id,
96
+ ifFree,
97
+ enabled
66
98
  ]);
67
99
  }
68
100
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/utils/use-hotkey.ts"],"sourcesContent":["'use client';\n\nimport { useEffect, useRef } from 'react';\n\n/** One chord, described the way a person would say it. */\nexport interface Hotkey {\n /** The `KeyboardEvent.key`, compared case-insensitively. */\n readonly key: string;\n /** ⌘ on a Mac, Ctrl elsewhere. Either satisfies it. */\n readonly mod?: boolean;\n readonly shift?: boolean;\n}\n\nconst isEditable = (target: EventTarget | null): boolean => {\n if (!(target instanceof HTMLElement)) return false;\n const tag = target.tagName;\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;\n // `closest`, not `isContentEditable`: the property is true for a *descendant*\n // of an editable host but is not implemented in jsdom, so the branch that\n // matters most for a rich editor would be the one no test could reach. The\n // selector covers the host and everything inside it, and the `:not` arm is\n // what keeps `contenteditable=\"false\"` — the way a widget opts a subtree back\n // out — from reading as editable.\n return (\n target.closest('[contenteditable]:not([contenteditable=\"false\"])') !== null\n );\n};\n\nconst matches = (event: KeyboardEvent, hotkey: Hotkey): boolean => {\n if (event.key.toLowerCase() !== hotkey.key.toLowerCase()) return false;\n // ⌘ and Ctrl are the same intent on different platforms, so either satisfies\n // `mod` rather than the hook asking the caller to sniff the platform. Nothing\n // in the fleet binds a chord that means one thing with ⌘ and another with Ctrl.\n const mod = event.metaKey || event.ctrlKey;\n if ((hotkey.mod ?? false) !== mod) return false;\n return (hotkey.shift ?? false) === event.shiftKey;\n};\n\n/**\n * Fire `handler` when any of `hotkeys` is pressed, anywhere on the page.\n *\n * The repo's first global shortcut, so two things are decided here rather than\n * at each future call site.\n *\n * **It skips while focus is in an editable element**, because a page-wide\n * listener that does not would eat the chord a person meant for the field they\n * are typing in. The palette's own input is exempt in practice: the palette\n * unmounts its trigger's listener behaviour by simply not being open.\n *\n * **It calls `preventDefault`**, which is what stops the browser acting on a\n * chord it also claims. ⚠️ That is not guaranteed to work: Chrome, Safari and\n * Edge dispatch these to the page and honour it, Firefox reserves some chords\n * and ignores `defaultPrevented` for them. So no single binding may be\n * load-bearing — bind more than one, and always leave a visible control that\n * does the same thing.\n *\n * `handler` is held in a ref so a caller may pass an inline closure without\n * re-subscribing the listener on every render.\n */\nexport function useHotkey(\n hotkeys: readonly Hotkey[],\n handler: (event: KeyboardEvent) => void,\n): void {\n const handlerRef = useRef(handler);\n useEffect(() => {\n handlerRef.current = handler;\n });\n\n // Serialized rather than passed by identity: callers build the array inline,\n // so the array itself is a new object every render and would re-subscribe the\n // listener on each one.\n const signature = JSON.stringify(hotkeys);\n\n useEffect(() => {\n const bindings = JSON.parse(signature) as Hotkey[];\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (isEditable(event.target)) return;\n if (!bindings.some(hotkey => matches(event, hotkey))) return;\n event.preventDefault();\n handlerRef.current(event);\n };\n\n document.addEventListener('keydown', onKeyDown);\n return () => document.removeEventListener('keydown', onKeyDown);\n }, [signature]);\n}\n"],"names":["useEffect","useRef","isEditable","target","HTMLElement","tag","tagName","closest","matches","event","hotkey","key","toLowerCase","mod","metaKey","ctrlKey","shift","shiftKey","useHotkey","hotkeys","handler","handlerRef","current","signature","JSON","stringify","bindings","parse","onKeyDown","some","preventDefault","document","addEventListener","removeEventListener"],"mappings":"AAAA;AAEA,SAASA,SAAS,EAAEC,MAAM,QAAQ,QAAQ;AAW1C,MAAMC,aAAa,CAACC;IAClB,IAAI,CAAEA,CAAAA,kBAAkBC,WAAU,GAAI,OAAO;IAC7C,MAAMC,MAAMF,OAAOG,OAAO;IAC1B,IAAID,QAAQ,WAAWA,QAAQ,cAAcA,QAAQ,UAAU,OAAO;IACtE,8EAA8E;IAC9E,0EAA0E;IAC1E,2EAA2E;IAC3E,2EAA2E;IAC3E,8EAA8E;IAC9E,kCAAkC;IAClC,OACEF,OAAOI,OAAO,CAAC,wDAAwD;AAE3E;AAEA,MAAMC,UAAU,CAACC,OAAsBC;QAMhCA,aACGA;IANR,IAAID,MAAME,GAAG,CAACC,WAAW,OAAOF,OAAOC,GAAG,CAACC,WAAW,IAAI,OAAO;IACjE,6EAA6E;IAC7E,8EAA8E;IAC9E,gFAAgF;IAChF,MAAMC,MAAMJ,MAAMK,OAAO,IAAIL,MAAMM,OAAO;IAC1C,IAAI,EAACL,cAAAA,OAAOG,GAAG,YAAVH,cAAc,WAAWG,KAAK,OAAO;IAC1C,OAAO,EAACH,gBAAAA,OAAOM,KAAK,YAAZN,gBAAgB,WAAWD,MAAMQ,QAAQ;AACnD;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASC,UACdC,OAA0B,EAC1BC,OAAuC;IAEvC,MAAMC,aAAapB,OAAOmB;IAC1BpB,UAAU;QACRqB,WAAWC,OAAO,GAAGF;IACvB;IAEA,6EAA6E;IAC7E,8EAA8E;IAC9E,wBAAwB;IACxB,MAAMG,YAAYC,KAAKC,SAAS,CAACN;IAEjCnB,UAAU;QACR,MAAM0B,WAAWF,KAAKG,KAAK,CAACJ;QAE5B,MAAMK,YAAY,CAACnB;YACjB,IAAIP,WAAWO,MAAMN,MAAM,GAAG;YAC9B,IAAI,CAACuB,SAASG,IAAI,CAACnB,CAAAA,SAAUF,QAAQC,OAAOC,UAAU;YACtDD,MAAMqB,cAAc;YACpBT,WAAWC,OAAO,CAACb;QACrB;QAEAsB,SAASC,gBAAgB,CAAC,WAAWJ;QACrC,OAAO,IAAMG,SAASE,mBAAmB,CAAC,WAAWL;IACvD,GAAG;QAACL;KAAU;AAChB"}
1
+ {"version":3,"sources":["../../../src/ui/utils/use-hotkey.ts"],"sourcesContent":["'use client';\n\nimport { parseChord, type ParsedChord } from '@entifix/core';\nimport { createContext, useContext, useEffect, useRef } from 'react';\n\n/**\n * Where a set of shortcuts applies: the page, or one workspace tab. A tab's\n * shortcuts fire only while it is the active tab, so two tabs each holding a\n * form can both bind Mod+S without one saving the other.\n */\nexport interface HotkeyScope {\n readonly id: string;\n readonly active: boolean;\n}\n\n/** The page: every shortcut outside a workspace tab. */\nexport const PAGE_SCOPE: HotkeyScope = { id: 'page', active: true };\n\nconst HotkeyScopeContext = createContext<HotkeyScope>(PAGE_SCOPE);\n\n/**\n * Scopes the shortcuts bound below it — a workspace tab passes its own id and\n * whether it is the active tab.\n */\nexport const HotkeyScopeProvider = HotkeyScopeContext.Provider;\n\n/**\n * Which chords each scope holds, for refusing a second binding of one. A\n * chord is held at most once per scope — a second binding is refused or\n * yields — so a set is enough.\n */\nconst bound = new Map<string, Set<string>>();\n\nconst holds = (scope: string, chord: string): boolean =>\n bound.get(scope)?.has(chord) === true;\n\n/**\n * Whether `chord` is taken where `scope` would bind it: in the same scope, or\n * — since the page's shortcuts apply inside every tab — between the page and\n * a tab.\n */\nfunction isTaken(scope: string, chord: string): boolean {\n if (holds(scope, chord)) return true;\n if (scope === PAGE_SCOPE.id)\n return [...bound.values()].some(chords => chords.has(chord));\n return holds(PAGE_SCOPE.id, chord);\n}\n\nfunction bind(scope: string, chord: string): () => void {\n const chords = bound.get(scope) ?? new Set<string>();\n bound.set(scope, chords.add(chord));\n return () => {\n chords.delete(chord);\n };\n}\n\n/** For specs: a page with nothing bound. */\nexport const resetHotkeysForTesting = (): void => bound.clear();\n\nconst matches = (event: KeyboardEvent, chord: ParsedChord): boolean =>\n event.key.toLowerCase() === chord.key &&\n // ⌘ and Ctrl are the same intent on different platforms, so either one\n // satisfies `Mod` rather than each caller sniffing the platform.\n (event.metaKey || event.ctrlKey) &&\n event.shiftKey === chord.shift &&\n event.altKey === chord.alt;\n\nexport interface HotkeyOptions {\n /**\n * Bind only if the chord is free, and stay silent if it is not. Without it\n * a second binding of a chord in one scope throws at registration: two\n * handlers for one keystroke is a bug, and whichever fired first would\n * decide what the reader got. A built-in default — a form's Mod+S — takes\n * this, so a host's own binding wins.\n */\n readonly ifFree?: boolean;\n /** Bound only while true; a disabled control's shortcut does nothing. */\n readonly enabled?: boolean;\n}\n\n/**\n * Runs `handler` on any of `chords` — `\"Mod+K\"`, `\"Mod+Shift+P\"` — within\n * the nearest {@link HotkeyScope} (ADR 0026).\n *\n * - **Modifier chords only**: every one holds `Mod`, so no single key or\n * Shift+key steals a character from a reader typing (WCAG 2.1.4), and no\n * Alt+letter, which macOS Option composes into a character. The editing\n * chords — Mod+A, C, V, X, Y, Z — stay the field's. Anything else throws\n * when it is bound.\n * - **They fire inside a text field**: a chord is never typing, and Mod+S\n * is exactly what a reader presses with the caret in a field.\n * - **A chord is held once per scope**, so a conflict is refused when it is\n * bound rather than resolved by whoever listens first.\n *\n * It calls `preventDefault`, which stops the browser's own action for the\n * chord where the browser lets it; no binding should be the only way to do\n * anything, so a visible control always does the same thing.\n */\nexport function useHotkey(\n chords: readonly string[],\n handler: (event: KeyboardEvent) => void,\n { ifFree = false, enabled = true }: HotkeyOptions = {},\n): void {\n const scope = useContext(HotkeyScopeContext);\n const handlerRef = useRef(handler);\n const activeRef = useRef(scope.active);\n useEffect(() => {\n handlerRef.current = handler;\n activeRef.current = scope.active;\n });\n\n // Joined rather than passed by identity: callers write the list inline.\n const signature = chords.join(' ');\n\n useEffect(() => {\n if (!enabled || signature === '') return;\n const parsed = signature.split(' ').map(parseChord);\n const free = parsed.filter(chord => !isTaken(scope.id, chord.text));\n if (free.length < parsed.length && !ifFree) {\n const taken = parsed.find(chord => !free.includes(chord)) as ParsedChord;\n throw new Error(\n `${taken.text} is already bound here. A chord does one thing in a scope: unbind the other, or pass \\`ifFree\\` to the default that should yield.`,\n );\n }\n if (free.length === 0) return;\n const unbind = free.map(chord => bind(scope.id, chord.text));\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (!activeRef.current) return;\n if (!free.some(chord => matches(event, chord))) return;\n event.preventDefault();\n handlerRef.current(event);\n };\n document.addEventListener('keydown', onKeyDown);\n return () => {\n document.removeEventListener('keydown', onKeyDown);\n for (const release of unbind) release();\n };\n }, [signature, scope.id, ifFree, enabled]);\n}\n"],"names":["parseChord","createContext","useContext","useEffect","useRef","PAGE_SCOPE","id","active","HotkeyScopeContext","HotkeyScopeProvider","Provider","bound","Map","holds","scope","chord","get","has","isTaken","values","some","chords","bind","Set","set","add","delete","resetHotkeysForTesting","clear","matches","event","key","toLowerCase","metaKey","ctrlKey","shiftKey","shift","altKey","alt","useHotkey","handler","ifFree","enabled","handlerRef","activeRef","current","signature","join","parsed","split","map","free","filter","text","length","taken","find","includes","Error","unbind","onKeyDown","preventDefault","document","addEventListener","removeEventListener","release"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entifix/react-controls",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "Entity-agnostic React UI — table, form, filters, wizard, command palette — rendered from entity metadata, plus layout primitives.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,7 +19,7 @@
19
19
  "@headlessui/react": "^2.2.10",
20
20
  "class-variance-authority": "^0.7.1",
21
21
  "idb": "^8.0.4",
22
- "@entifix/core": "0.19.0"
22
+ "@entifix/core": "0.20.1"
23
23
  },
24
24
  "exports": {
25
25
  "./package.json": "./package.json",
@@ -55,8 +55,8 @@
55
55
  "react": "19.3.1",
56
56
  "react-i18next": "17.1.10",
57
57
  "storybook": "^10.6.5",
58
- "@entifix/i18n": "0.19.0",
59
- "@entifix/style": "0.19.0"
58
+ "@entifix/i18n": "0.20.1",
59
+ "@entifix/style": "0.20.1"
60
60
  },
61
61
  "nx": {
62
62
  "tags": [
@@ -111,7 +111,7 @@
111
111
  "i18next": ">=26 <27",
112
112
  "react": ">=19 <20",
113
113
  "react-i18next": ">=17 <18",
114
- "@entifix/i18n": "0.19.0"
114
+ "@entifix/i18n": "0.20.1"
115
115
  },
116
116
  "peerDependenciesMeta": {
117
117
  "@entifix/i18n": {
@@ -1,2 +0,0 @@
1
- export * from './split-button.js';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/split-button/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
@@ -1,3 +0,0 @@
1
- export * from './split-button.js';
2
-
3
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/split-button/index.ts"],"sourcesContent":["export * from './split-button.js';\n"],"names":[],"mappings":"AAAA,cAAc,oBAAoB"}
@@ -1,21 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- /**
3
- * One main action, and a menu of the related ones beside it: `[Download │ ▾]`.
4
- *
5
- * The caller renders the main action — a link or a button, styled as the
6
- * primary button — so it stays in the static HTML and keeps its own
7
- * attributes; without `children` it stands alone, as before scripting runs.
8
- * The menu is a `NavMenu`, so it opens with scripting off and the page's own
9
- * menu handling closes it. Parts carry `data-slot` (`split-button`, and the
10
- * menu's own).
11
- */
12
- export declare function SplitButton({ primary, menuLabel, children, className, }: {
13
- /** The main action, its corners squared on the side the menu joins. */
14
- primary: ReactNode;
15
- /** The accessible name of the ▾ trigger. */
16
- menuLabel: string;
17
- /** The menu's entries; none, and the main action stands alone. */
18
- children?: ReactNode;
19
- className?: string;
20
- }): import("react").JSX.Element;
21
- //# sourceMappingURL=split-button.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"split-button.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/split-button/split-button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAYvC;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,SAAS,EACT,QAAQ,EACR,SAAS,GACV,EAAE;IACD,uEAAuE;IACvE,OAAO,EAAE,SAAS,CAAC;IACnB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,+BAuBA"}
@@ -1,35 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { button } from '../../atoms/button/button-recipe.js';
3
- import { cn } from '../../utils/cn.js';
4
- import { NavMenu } from '../nav-menu/nav-menu.js';
5
- /** The ▾ segment: the primary button's look, joined to the action before it. */ const TRIGGER = cn(button({
6
- variant: 'primary',
7
- size: 'sm'
8
- }), 'h-auto cursor-pointer list-none self-stretch rounded-l-none border-l border-l-[color-mix(in_srgb,var(--color-primary-content)_25%,transparent)] px-2xs select-none [&_[data-slot=nav-menu-chevron]]:text-current [&::-webkit-details-marker]:hidden');
9
- /**
10
- * One main action, and a menu of the related ones beside it: `[Download │ ▾]`.
11
- *
12
- * The caller renders the main action — a link or a button, styled as the
13
- * primary button — so it stays in the static HTML and keeps its own
14
- * attributes; without `children` it stands alone, as before scripting runs.
15
- * The menu is a `NavMenu`, so it opens with scripting off and the page's own
16
- * menu handling closes it. Parts carry `data-slot` (`split-button`, and the
17
- * menu's own).
18
- */ export function SplitButton({ primary, menuLabel, children, className }) {
19
- return /*#__PURE__*/ _jsxs("div", {
20
- "data-slot": "split-button",
21
- className: cn('inline-flex items-stretch', children !== undefined && '[&>:first-child]:rounded-r-none', className),
22
- children: [
23
- primary,
24
- children !== undefined && /*#__PURE__*/ _jsx(NavMenu, {
25
- label: menuLabel,
26
- icon: null,
27
- className: "flex",
28
- triggerClassName: TRIGGER,
29
- children: children
30
- })
31
- ]
32
- });
33
- }
34
-
35
- //# sourceMappingURL=split-button.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/split-button/split-button.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\n\nimport { button } from '../../atoms/button/button-recipe.js';\nimport { cn } from '../../utils/cn.js';\nimport { NavMenu } from '../nav-menu/nav-menu.js';\n\n/** The ▾ segment: the primary button's look, joined to the action before it. */\nconst TRIGGER = cn(\n button({ variant: 'primary', size: 'sm' }),\n 'h-auto cursor-pointer list-none self-stretch rounded-l-none border-l border-l-[color-mix(in_srgb,var(--color-primary-content)_25%,transparent)] px-2xs select-none [&_[data-slot=nav-menu-chevron]]:text-current [&::-webkit-details-marker]:hidden',\n);\n\n/**\n * One main action, and a menu of the related ones beside it: `[Download │ ▾]`.\n *\n * The caller renders the main action — a link or a button, styled as the\n * primary button — so it stays in the static HTML and keeps its own\n * attributes; without `children` it stands alone, as before scripting runs.\n * The menu is a `NavMenu`, so it opens with scripting off and the page's own\n * menu handling closes it. Parts carry `data-slot` (`split-button`, and the\n * menu's own).\n */\nexport function SplitButton({\n primary,\n menuLabel,\n children,\n className,\n}: {\n /** The main action, its corners squared on the side the menu joins. */\n primary: ReactNode;\n /** The accessible name of the ▾ trigger. */\n menuLabel: string;\n /** The menu's entries; none, and the main action stands alone. */\n children?: ReactNode;\n className?: string;\n}) {\n return (\n <div\n data-slot=\"split-button\"\n className={cn(\n 'inline-flex items-stretch',\n children !== undefined && '[&>:first-child]:rounded-r-none',\n className,\n )}\n >\n {primary}\n {children !== undefined && (\n <NavMenu\n label={menuLabel}\n icon={null}\n className=\"flex\"\n triggerClassName={TRIGGER}\n >\n {children}\n </NavMenu>\n )}\n </div>\n );\n}\n"],"names":["button","cn","NavMenu","TRIGGER","variant","size","SplitButton","primary","menuLabel","children","className","div","data-slot","undefined","label","icon","triggerClassName"],"mappings":";AAEA,SAASA,MAAM,QAAQ,sCAAsC;AAC7D,SAASC,EAAE,QAAQ,oBAAoB;AACvC,SAASC,OAAO,QAAQ,0BAA0B;AAElD,8EAA8E,GAC9E,MAAMC,UAAUF,GACdD,OAAO;IAAEI,SAAS;IAAWC,MAAM;AAAK,IACxC;AAGF;;;;;;;;;CASC,GACD,OAAO,SAASC,YAAY,EAC1BC,OAAO,EACPC,SAAS,EACTC,QAAQ,EACRC,SAAS,EASV;IACC,qBACE,MAACC;QACCC,aAAU;QACVF,WAAWT,GACT,6BACAQ,aAAaI,aAAa,mCAC1BH;;YAGDH;YACAE,aAAaI,2BACZ,KAACX;gBACCY,OAAON;gBACPO,MAAM;gBACNL,WAAU;gBACVM,kBAAkBb;0BAEjBM;;;;AAKX"}