@entifix/react-controls 0.18.2 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +5 -5
  2. package/dist/i18n/catalog/en.d.ts.map +1 -1
  3. package/dist/i18n/catalog/en.js +13 -1
  4. package/dist/i18n/catalog/en.js.map +1 -1
  5. package/dist/i18n/catalog/es.d.ts +12 -0
  6. package/dist/i18n/catalog/es.d.ts.map +1 -1
  7. package/dist/i18n/catalog/es.js +13 -1
  8. package/dist/i18n/catalog/es.js.map +1 -1
  9. package/dist/i18n/catalog/index.d.ts +24 -0
  10. package/dist/i18n/catalog/index.d.ts.map +1 -1
  11. package/dist/index.d.ts +7 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +7 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/primitives.d.ts +7 -0
  16. package/dist/primitives.d.ts.map +1 -1
  17. package/dist/primitives.js +7 -0
  18. package/dist/primitives.js.map +1 -1
  19. package/dist/theme/theme-context.d.ts +23 -7
  20. package/dist/theme/theme-context.d.ts.map +1 -1
  21. package/dist/theme/theme-context.js +76 -14
  22. package/dist/theme/theme-context.js.map +1 -1
  23. package/dist/theme/theme-script.d.ts +4 -3
  24. package/dist/theme/theme-script.d.ts.map +1 -1
  25. package/dist/theme/theme-script.js +8 -3
  26. package/dist/theme/theme-script.js.map +1 -1
  27. package/dist/theme/types.d.ts +19 -3
  28. package/dist/theme/types.d.ts.map +1 -1
  29. package/dist/theme/types.js.map +1 -1
  30. package/dist/ui/atoms/alert/alert-dismiss.d.ts +9 -0
  31. package/dist/ui/atoms/alert/alert-dismiss.d.ts.map +1 -0
  32. package/dist/ui/atoms/alert/alert-dismiss.js +28 -0
  33. package/dist/ui/atoms/alert/alert-dismiss.js.map +1 -0
  34. package/dist/ui/atoms/alert/alert.d.ts +24 -0
  35. package/dist/ui/atoms/alert/alert.d.ts.map +1 -0
  36. package/dist/ui/atoms/alert/alert.js +50 -0
  37. package/dist/ui/atoms/alert/alert.js.map +1 -0
  38. package/dist/ui/atoms/alert/index.d.ts +3 -0
  39. package/dist/ui/atoms/alert/index.d.ts.map +1 -0
  40. package/dist/ui/atoms/alert/index.js +4 -0
  41. package/dist/ui/atoms/alert/index.js.map +1 -0
  42. package/dist/ui/atoms/alert/tone.d.ts +8 -0
  43. package/dist/ui/atoms/alert/tone.d.ts.map +1 -0
  44. package/dist/ui/atoms/alert/tone.js +11 -0
  45. package/dist/ui/atoms/alert/tone.js.map +1 -0
  46. package/dist/ui/atoms/button/button-recipe.js +1 -1
  47. package/dist/ui/atoms/button/button-recipe.js.map +1 -1
  48. package/dist/ui/atoms/button/index.d.ts +1 -0
  49. package/dist/ui/atoms/button/index.d.ts.map +1 -1
  50. package/dist/ui/atoms/button/index.js +1 -0
  51. package/dist/ui/atoms/button/index.js.map +1 -1
  52. package/dist/ui/atoms/button/submit-button.d.ts +13 -0
  53. package/dist/ui/atoms/button/submit-button.d.ts.map +1 -0
  54. package/dist/ui/atoms/button/submit-button.js +29 -0
  55. package/dist/ui/atoms/button/submit-button.js.map +1 -0
  56. package/dist/ui/atoms/cell-value/cell-value.d.ts +6 -1
  57. package/dist/ui/atoms/cell-value/cell-value.d.ts.map +1 -1
  58. package/dist/ui/atoms/cell-value/cell-value.js +20 -2
  59. package/dist/ui/atoms/cell-value/cell-value.js.map +1 -1
  60. package/dist/ui/atoms/field/checkbox.d.ts.map +1 -1
  61. package/dist/ui/atoms/field/checkbox.js +21 -1
  62. package/dist/ui/atoms/field/checkbox.js.map +1 -1
  63. package/dist/ui/atoms/field/field.js +1 -1
  64. package/dist/ui/atoms/field/field.js.map +1 -1
  65. package/dist/ui/atoms/field-control/field-control.d.ts +6 -1
  66. package/dist/ui/atoms/field-control/field-control.d.ts.map +1 -1
  67. package/dist/ui/atoms/field-control/field-control.js +23 -3
  68. package/dist/ui/atoms/field-control/field-control.js.map +1 -1
  69. package/dist/ui/atoms/money/index.d.ts +4 -0
  70. package/dist/ui/atoms/money/index.d.ts.map +1 -0
  71. package/dist/ui/atoms/money/index.js +5 -0
  72. package/dist/ui/atoms/money/index.js.map +1 -0
  73. package/dist/ui/atoms/money/money-input.d.ts +19 -0
  74. package/dist/ui/atoms/money/money-input.d.ts.map +1 -0
  75. package/dist/ui/atoms/money/money-input.js +75 -0
  76. package/dist/ui/atoms/money/money-input.js.map +1 -0
  77. package/dist/ui/atoms/money/money-math.d.ts +21 -0
  78. package/dist/ui/atoms/money/money-math.d.ts.map +1 -0
  79. package/dist/ui/atoms/money/money-math.js +36 -0
  80. package/dist/ui/atoms/money/money-math.js.map +1 -0
  81. package/dist/ui/atoms/money/money.d.ts +19 -0
  82. package/dist/ui/atoms/money/money.d.ts.map +1 -0
  83. package/dist/ui/atoms/money/money.js +26 -0
  84. package/dist/ui/atoms/money/money.js.map +1 -0
  85. package/dist/ui/atoms/number-input/index.d.ts +3 -0
  86. package/dist/ui/atoms/number-input/index.d.ts.map +1 -0
  87. package/dist/ui/atoms/number-input/index.js +4 -0
  88. package/dist/ui/atoms/number-input/index.js.map +1 -0
  89. package/dist/ui/atoms/number-input/number-input.d.ts +10 -0
  90. package/dist/ui/atoms/number-input/number-input.d.ts.map +1 -0
  91. package/dist/ui/atoms/number-input/number-input.js +23 -0
  92. package/dist/ui/atoms/number-input/number-input.js.map +1 -0
  93. package/dist/ui/atoms/number-input/quantity-input.d.ts +22 -0
  94. package/dist/ui/atoms/number-input/quantity-input.d.ts.map +1 -0
  95. package/dist/ui/atoms/number-input/quantity-input.js +99 -0
  96. package/dist/ui/atoms/number-input/quantity-input.js.map +1 -0
  97. package/dist/ui/atoms/skeleton/skeleton.js +1 -1
  98. package/dist/ui/atoms/skeleton/skeleton.js.map +1 -1
  99. package/dist/ui/atoms/status-tag/index.d.ts +2 -0
  100. package/dist/ui/atoms/status-tag/index.d.ts.map +1 -0
  101. package/dist/ui/atoms/status-tag/index.js +3 -0
  102. package/dist/ui/atoms/status-tag/index.js.map +1 -0
  103. package/dist/ui/atoms/status-tag/status-tag.d.ts +17 -0
  104. package/dist/ui/atoms/status-tag/status-tag.d.ts.map +1 -0
  105. package/dist/ui/atoms/status-tag/status-tag.js +37 -0
  106. package/dist/ui/atoms/status-tag/status-tag.js.map +1 -0
  107. package/dist/ui/atoms/switch/switch.d.ts.map +1 -1
  108. package/dist/ui/atoms/switch/switch.js +6 -3
  109. package/dist/ui/atoms/switch/switch.js.map +1 -1
  110. package/dist/ui/molecules/breadcrumbs/breadcrumbs.d.ts +11 -2
  111. package/dist/ui/molecules/breadcrumbs/breadcrumbs.d.ts.map +1 -1
  112. package/dist/ui/molecules/breadcrumbs/breadcrumbs.js +9 -7
  113. package/dist/ui/molecules/breadcrumbs/breadcrumbs.js.map +1 -1
  114. package/dist/ui/molecules/column-settings/column-settings.js +1 -1
  115. package/dist/ui/molecules/column-settings/column-settings.js.map +1 -1
  116. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js +2 -2
  117. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js.map +1 -1
  118. package/dist/ui/molecules/description-list/description-list.d.ts +17 -0
  119. package/dist/ui/molecules/description-list/description-list.d.ts.map +1 -0
  120. package/dist/ui/molecules/description-list/description-list.js +36 -0
  121. package/dist/ui/molecules/description-list/description-list.js.map +1 -0
  122. package/dist/ui/molecules/description-list/index.d.ts +2 -0
  123. package/dist/ui/molecules/description-list/index.d.ts.map +1 -0
  124. package/dist/ui/molecules/description-list/index.js +3 -0
  125. package/dist/ui/molecules/description-list/index.js.map +1 -0
  126. package/dist/ui/molecules/drawer/drawer.js +3 -3
  127. package/dist/ui/molecules/drawer/drawer.js.map +1 -1
  128. package/dist/ui/molecules/empty-state/empty-state.d.ts +15 -0
  129. package/dist/ui/molecules/empty-state/empty-state.d.ts.map +1 -0
  130. package/dist/ui/molecules/empty-state/empty-state.js +29 -0
  131. package/dist/ui/molecules/empty-state/empty-state.js.map +1 -0
  132. package/dist/ui/molecules/empty-state/index.d.ts +2 -0
  133. package/dist/ui/molecules/empty-state/index.d.ts.map +1 -0
  134. package/dist/ui/molecules/empty-state/index.js +3 -0
  135. package/dist/ui/molecules/empty-state/index.js.map +1 -0
  136. package/dist/ui/molecules/entity-link-input/entity-link-input.d.ts.map +1 -1
  137. package/dist/ui/molecules/entity-link-input/entity-link-input.js +7 -4
  138. package/dist/ui/molecules/entity-link-input/entity-link-input.js.map +1 -1
  139. package/dist/ui/molecules/entity-link-picker/entity-link-picker.js +2 -2
  140. package/dist/ui/molecules/entity-link-picker/entity-link-picker.js.map +1 -1
  141. package/dist/ui/molecules/media-gallery/index.d.ts +2 -0
  142. package/dist/ui/molecules/media-gallery/index.d.ts.map +1 -0
  143. package/dist/ui/molecules/media-gallery/index.js +3 -0
  144. package/dist/ui/molecules/media-gallery/index.js.map +1 -0
  145. package/dist/ui/molecules/media-gallery/media-gallery.d.ts +31 -0
  146. package/dist/ui/molecules/media-gallery/media-gallery.d.ts.map +1 -0
  147. package/dist/ui/molecules/media-gallery/media-gallery.js +64 -0
  148. package/dist/ui/molecules/media-gallery/media-gallery.js.map +1 -0
  149. package/dist/ui/molecules/menu/menu.d.ts.map +1 -1
  150. package/dist/ui/molecules/menu/menu.js +6 -4
  151. package/dist/ui/molecules/menu/menu.js.map +1 -1
  152. package/dist/ui/molecules/nav-menu/nav-menu.js +4 -4
  153. package/dist/ui/molecules/nav-menu/nav-menu.js.map +1 -1
  154. package/dist/ui/molecules/segmented-nav/segment-classes.d.ts +3 -3
  155. package/dist/ui/molecules/segmented-nav/segment-classes.d.ts.map +1 -1
  156. package/dist/ui/molecules/segmented-nav/segment-classes.js +2 -2
  157. package/dist/ui/molecules/segmented-nav/segment-classes.js.map +1 -1
  158. package/dist/ui/molecules/segmented-nav/segmented-nav.d.ts +14 -4
  159. package/dist/ui/molecules/segmented-nav/segmented-nav.d.ts.map +1 -1
  160. package/dist/ui/molecules/segmented-nav/segmented-nav.js +6 -5
  161. package/dist/ui/molecules/segmented-nav/segmented-nav.js.map +1 -1
  162. package/dist/ui/molecules/split-view/split-view.js +3 -3
  163. package/dist/ui/molecules/split-view/split-view.js.map +1 -1
  164. package/dist/ui/molecules/tab-strip/tab-strip.d.ts +51 -12
  165. package/dist/ui/molecules/tab-strip/tab-strip.d.ts.map +1 -1
  166. package/dist/ui/molecules/tab-strip/tab-strip.js +125 -34
  167. package/dist/ui/molecules/tab-strip/tab-strip.js.map +1 -1
  168. package/dist/ui/molecules/toast-region/toast-region.js +1 -1
  169. package/dist/ui/molecules/toast-region/toast-region.js.map +1 -1
  170. package/dist/ui/organisms/command-palette/command-palette.js +6 -6
  171. package/dist/ui/organisms/command-palette/command-palette.js.map +1 -1
  172. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.d.ts.map +1 -1
  173. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js +8 -2
  174. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js.map +1 -1
  175. package/dist/ui/organisms/entity-form/entity-form.js +4 -2
  176. package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
  177. package/dist/ui/organisms/entity-table/entity-table.d.ts.map +1 -1
  178. package/dist/ui/organisms/entity-table/entity-table.js +2 -1
  179. package/dist/ui/organisms/entity-table/entity-table.js.map +1 -1
  180. package/package.json +5 -5
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The segmented look, shared by `SegmentedNav` (links) and `SegmentedControl`
3
3
  * (a choice in the page): one bordered track, the chosen segment filled.
4
- */ /** The joined segments; scrolls sideways rather than wrapping. */ export const LIST = 'm-0 flex w-max max-w-full list-none overflow-x-auto rounded-md border border-border p-[0.1875rem] [scrollbar-width:none]';
5
- const SEGMENT = 'block rounded-[calc(var(--radius-md,0.375rem)-0.125rem)] px-xs py-[0.3125rem] text-step-sm whitespace-nowrap no-underline transition-colors duration-(--duration-fast,150ms) motion-reduce:transition-none';
4
+ */ /** The joined segments; scrolls sideways rather than wrapping. */ export const LIST = 'm-0 flex w-max max-w-full list-none overflow-x-auto rounded-md border border-border p-0.75 [scrollbar-width:none]';
5
+ const SEGMENT = 'block rounded-[calc(var(--radius-md,0.375rem)-0.125rem)] px-xs py-1.25 text-step-sm whitespace-nowrap no-underline transition-colors duration-(--duration-fast,150ms) motion-reduce:transition-none';
6
6
  export const OTHER = `${SEGMENT} focus-ring text-content hover:bg-[color-mix(in_srgb,var(--color-content)_6%,transparent)] hover:text-primary`;
7
7
  export const CURRENT = `${SEGMENT} bg-primary font-medium text-primary-content`;
8
8
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/segmented-nav/segment-classes.ts"],"sourcesContent":["/**\n * The segmented look, shared by `SegmentedNav` (links) and `SegmentedControl`\n * (a choice in the page): one bordered track, the chosen segment filled.\n */\n/** The joined segments; scrolls sideways rather than wrapping. */\nexport const LIST =\n 'm-0 flex w-max max-w-full list-none overflow-x-auto rounded-md border border-border p-[0.1875rem] [scrollbar-width:none]';\n\nconst SEGMENT =\n 'block rounded-[calc(var(--radius-md,0.375rem)-0.125rem)] px-xs py-[0.3125rem] text-step-sm whitespace-nowrap no-underline transition-colors duration-(--duration-fast,150ms) motion-reduce:transition-none';\n\nexport const OTHER = `${SEGMENT} focus-ring text-content hover:bg-[color-mix(in_srgb,var(--color-content)_6%,transparent)] hover:text-primary`;\n\nexport const CURRENT = `${SEGMENT} bg-primary font-medium text-primary-content`;\n"],"names":["LIST","SEGMENT","OTHER","CURRENT"],"mappings":"AAAA;;;CAGC,GACD,gEAAgE,GAChE,OAAO,MAAMA,OACX,2HAA2H;AAE7H,MAAMC,UACJ;AAEF,OAAO,MAAMC,QAAQ,GAAGD,QAAQ,6GAA6G,CAAC,CAAC;AAE/I,OAAO,MAAME,UAAU,GAAGF,QAAQ,4CAA4C,CAAC,CAAC"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/segmented-nav/segment-classes.ts"],"sourcesContent":["/**\n * The segmented look, shared by `SegmentedNav` (links) and `SegmentedControl`\n * (a choice in the page): one bordered track, the chosen segment filled.\n */\n/** The joined segments; scrolls sideways rather than wrapping. */\nexport const LIST =\n 'm-0 flex w-max max-w-full list-none overflow-x-auto rounded-md border border-border p-0.75 [scrollbar-width:none]';\n\nconst SEGMENT =\n 'block rounded-[calc(var(--radius-md,0.375rem)-0.125rem)] px-xs py-1.25 text-step-sm whitespace-nowrap no-underline transition-colors duration-(--duration-fast,150ms) motion-reduce:transition-none';\n\nexport const OTHER = `${SEGMENT} focus-ring text-content hover:bg-[color-mix(in_srgb,var(--color-content)_6%,transparent)] hover:text-primary`;\n\nexport const CURRENT = `${SEGMENT} bg-primary font-medium text-primary-content`;\n"],"names":["LIST","SEGMENT","OTHER","CURRENT"],"mappings":"AAAA;;;CAGC,GACD,gEAAgE,GAChE,OAAO,MAAMA,OACX,oHAAoH;AAEtH,MAAMC,UACJ;AAEF,OAAO,MAAMC,QAAQ,GAAGD,QAAQ,6GAA6G,CAAC,CAAC;AAE/I,OAAO,MAAME,UAAU,GAAGF,QAAQ,4CAA4C,CAAC,CAAC"}
@@ -13,18 +13,28 @@ export type SegmentedNavLink = ComponentType<{
13
13
  children: ReactNode;
14
14
  }>;
15
15
  /**
16
- * A choice between pages, drawn as one segmented control: each segment is a
17
- * link, the current page's is a marked span. A visible label names the
18
- * choice; the `<nav>` carries it for assistive technology.
16
+ * What the segments choose between. `pages` are separate pages, and the
17
+ * current one is `aria-current="page"`; `sort` orders one page's results, so
18
+ * every segment is the same page and the current one is only the current
19
+ * choice (`aria-current="true"`) — calling it the page would tell a screen
20
+ * reader the visitor is somewhere they are not.
21
+ */
22
+ export type SegmentedNavKind = 'pages' | 'sort';
23
+ /**
24
+ * A choice between pages, or between orderings of one page, drawn as one
25
+ * segmented control: each segment is a link, the current one a marked span.
26
+ * A visible label names the choice; the `<nav>` carries it for assistive
27
+ * technology.
19
28
  *
20
29
  * No hooks, so a server component renders it, and every segment works with
21
30
  * scripting off. Its parts carry `data-slot` (`segmented-nav`,
22
31
  * `segmented-nav-label`, `segmented-nav-list`, `segmented-nav-item`).
23
32
  */
24
- export declare function SegmentedNav({ label, items, link: Link, className, }: {
33
+ export declare function SegmentedNav({ label, items, link: Link, kind, className, }: {
25
34
  label: string;
26
35
  items: readonly SegmentedNavItem[];
27
36
  link?: SegmentedNavLink;
37
+ kind?: SegmentedNavKind;
28
38
  className?: string;
29
39
  }): import("react").JSX.Element;
30
40
  //# sourceMappingURL=segmented-nav.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"segmented-nav.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/segmented-nav/segmented-nav.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAItD,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,iFAAiF;AACjF,MAAM,MAAM,gBAAgB,GAAG,aAAa,CAAC;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC,CAAC;AAMH;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,KAAK,EACL,IAAI,EAAE,IAAa,EACnB,SAAS,GACV,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACnC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,+BA+BA"}
1
+ {"version":3,"file":"segmented-nav.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/segmented-nav/segmented-nav.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAItD,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,iFAAiF;AACjF,MAAM,MAAM,gBAAgB,GAAG,aAAa,CAAC;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC,CAAC;AAMH;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,CAAC;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,KAAK,EACL,IAAI,EAAE,IAAa,EACnB,IAAc,EACd,SAAS,GACV,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACnC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,+BAkCA"}
@@ -11,14 +11,15 @@ const ANCHOR = (_0)=>{
11
11
  }));
12
12
  };
13
13
  /**
14
- * A choice between pages, drawn as one segmented control: each segment is a
15
- * link, the current page's is a marked span. A visible label names the
16
- * choice; the `<nav>` carries it for assistive technology.
14
+ * A choice between pages, or between orderings of one page, drawn as one
15
+ * segmented control: each segment is a link, the current one a marked span.
16
+ * A visible label names the choice; the `<nav>` carries it for assistive
17
+ * technology.
17
18
  *
18
19
  * No hooks, so a server component renders it, and every segment works with
19
20
  * scripting off. Its parts carry `data-slot` (`segmented-nav`,
20
21
  * `segmented-nav-label`, `segmented-nav-list`, `segmented-nav-item`).
21
- */ export function SegmentedNav({ label, items, link: Link = ANCHOR, className }) {
22
+ */ export function SegmentedNav({ label, items, link: Link = ANCHOR, kind = 'pages', className }) {
22
23
  return /*#__PURE__*/ _jsxs("nav", {
23
24
  "data-slot": "segmented-nav",
24
25
  "aria-label": label,
@@ -37,7 +38,7 @@ const ANCHOR = (_0)=>{
37
38
  "data-slot": "segmented-nav-item",
38
39
  className: "flex-none",
39
40
  children: item.current ? /*#__PURE__*/ _jsx("span", {
40
- "aria-current": "page",
41
+ "aria-current": kind === 'sort' ? 'true' : 'page',
41
42
  className: CURRENT,
42
43
  children: item.label
43
44
  }) : /*#__PURE__*/ _jsx(Link, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/segmented-nav/segmented-nav.tsx"],"sourcesContent":["import type { ComponentType, ReactNode } from 'react';\n\nimport { CURRENT, LIST, OTHER } from './segment-classes.js';\n\n/** One segment: a page of its own, the current one marked. */\nexport interface SegmentedNavItem {\n readonly key: string;\n readonly label: ReactNode;\n readonly href: string;\n readonly current: boolean;\n}\n\n/** What renders a segment that leads elsewhere: an `<a>`, or a router's link. */\nexport type SegmentedNavLink = ComponentType<{\n href: string;\n className?: string;\n children: ReactNode;\n}>;\n\nconst ANCHOR: SegmentedNavLink = ({ children, ...props }) => (\n <a {...props}>{children}</a>\n);\n\n/**\n * A choice between pages, drawn as one segmented control: each segment is a\n * link, the current page's is a marked span. A visible label names the\n * choice; the `<nav>` carries it for assistive technology.\n *\n * No hooks, so a server component renders it, and every segment works with\n * scripting off. Its parts carry `data-slot` (`segmented-nav`,\n * `segmented-nav-label`, `segmented-nav-list`, `segmented-nav-item`).\n */\nexport function SegmentedNav({\n label,\n items,\n link: Link = ANCHOR,\n className,\n}: {\n label: string;\n items: readonly SegmentedNavItem[];\n link?: SegmentedNavLink;\n className?: string;\n}) {\n return (\n <nav data-slot=\"segmented-nav\" aria-label={label} className={className}>\n <span\n data-slot=\"segmented-nav-label\"\n aria-hidden=\"true\"\n className=\"text-step-sm text-content-muted\"\n >\n {label}\n </span>\n <ul data-slot=\"segmented-nav-list\" className={LIST}>\n {items.map(item => (\n <li\n key={item.key}\n data-slot=\"segmented-nav-item\"\n className=\"flex-none\"\n >\n {item.current ? (\n <span aria-current=\"page\" className={CURRENT}>\n {item.label}\n </span>\n ) : (\n <Link href={item.href} className={OTHER}>\n {item.label}\n </Link>\n )}\n </li>\n ))}\n </ul>\n </nav>\n );\n}\n"],"names":["CURRENT","LIST","OTHER","ANCHOR","children","props","a","SegmentedNav","label","items","link","Link","className","nav","data-slot","aria-label","span","aria-hidden","ul","map","item","li","current","aria-current","href","key"],"mappings":";;;AAEA,SAASA,OAAO,EAAEC,IAAI,EAAEC,KAAK,QAAQ,uBAAuB;AAiB5D,MAAMC,SAA2B;QAAC,EAAEC,QAAQ,EAAY,OAAPC;;;yBAC/C,KAACC,kBAAMD;kBAAQD;;;AAGjB;;;;;;;;CAQC,GACD,OAAO,SAASG,aAAa,EAC3BC,KAAK,EACLC,KAAK,EACLC,MAAMC,OAAOR,MAAM,EACnBS,SAAS,EAMV;IACC,qBACE,MAACC;QAAIC,aAAU;QAAgBC,cAAYP;QAAOI,WAAWA;;0BAC3D,KAACI;gBACCF,aAAU;gBACVG,eAAY;gBACZL,WAAU;0BAETJ;;0BAEH,KAACU;gBAAGJ,aAAU;gBAAqBF,WAAWX;0BAC3CQ,MAAMU,GAAG,CAACC,CAAAA,qBACT,KAACC;wBAECP,aAAU;wBACVF,WAAU;kCAETQ,KAAKE,OAAO,iBACX,KAACN;4BAAKO,gBAAa;4BAAOX,WAAWZ;sCAClCoB,KAAKZ,KAAK;2CAGb,KAACG;4BAAKa,MAAMJ,KAAKI,IAAI;4BAAEZ,WAAWV;sCAC/BkB,KAAKZ,KAAK;;uBAVVY,KAAKK,GAAG;;;;AAkBzB"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/segmented-nav/segmented-nav.tsx"],"sourcesContent":["import type { ComponentType, ReactNode } from 'react';\n\nimport { CURRENT, LIST, OTHER } from './segment-classes.js';\n\n/** One segment: a page of its own, the current one marked. */\nexport interface SegmentedNavItem {\n readonly key: string;\n readonly label: ReactNode;\n readonly href: string;\n readonly current: boolean;\n}\n\n/** What renders a segment that leads elsewhere: an `<a>`, or a router's link. */\nexport type SegmentedNavLink = ComponentType<{\n href: string;\n className?: string;\n children: ReactNode;\n}>;\n\nconst ANCHOR: SegmentedNavLink = ({ children, ...props }) => (\n <a {...props}>{children}</a>\n);\n\n/**\n * What the segments choose between. `pages` are separate pages, and the\n * current one is `aria-current=\"page\"`; `sort` orders one page's results, so\n * every segment is the same page and the current one is only the current\n * choice (`aria-current=\"true\"`) — calling it the page would tell a screen\n * reader the visitor is somewhere they are not.\n */\nexport type SegmentedNavKind = 'pages' | 'sort';\n\n/**\n * A choice between pages, or between orderings of one page, drawn as one\n * segmented control: each segment is a link, the current one a marked span.\n * A visible label names the choice; the `<nav>` carries it for assistive\n * technology.\n *\n * No hooks, so a server component renders it, and every segment works with\n * scripting off. Its parts carry `data-slot` (`segmented-nav`,\n * `segmented-nav-label`, `segmented-nav-list`, `segmented-nav-item`).\n */\nexport function SegmentedNav({\n label,\n items,\n link: Link = ANCHOR,\n kind = 'pages',\n className,\n}: {\n label: string;\n items: readonly SegmentedNavItem[];\n link?: SegmentedNavLink;\n kind?: SegmentedNavKind;\n className?: string;\n}) {\n return (\n <nav data-slot=\"segmented-nav\" aria-label={label} className={className}>\n <span\n data-slot=\"segmented-nav-label\"\n aria-hidden=\"true\"\n className=\"text-step-sm text-content-muted\"\n >\n {label}\n </span>\n <ul data-slot=\"segmented-nav-list\" className={LIST}>\n {items.map(item => (\n <li\n key={item.key}\n data-slot=\"segmented-nav-item\"\n className=\"flex-none\"\n >\n {item.current ? (\n <span\n aria-current={kind === 'sort' ? 'true' : 'page'}\n className={CURRENT}\n >\n {item.label}\n </span>\n ) : (\n <Link href={item.href} className={OTHER}>\n {item.label}\n </Link>\n )}\n </li>\n ))}\n </ul>\n </nav>\n );\n}\n"],"names":["CURRENT","LIST","OTHER","ANCHOR","children","props","a","SegmentedNav","label","items","link","Link","kind","className","nav","data-slot","aria-label","span","aria-hidden","ul","map","item","li","current","aria-current","href","key"],"mappings":";;;AAEA,SAASA,OAAO,EAAEC,IAAI,EAAEC,KAAK,QAAQ,uBAAuB;AAiB5D,MAAMC,SAA2B;QAAC,EAAEC,QAAQ,EAAY,OAAPC;;;yBAC/C,KAACC,kBAAMD;kBAAQD;;;AAYjB;;;;;;;;;CASC,GACD,OAAO,SAASG,aAAa,EAC3BC,KAAK,EACLC,KAAK,EACLC,MAAMC,OAAOR,MAAM,EACnBS,OAAO,OAAO,EACdC,SAAS,EAOV;IACC,qBACE,MAACC;QAAIC,aAAU;QAAgBC,cAAYR;QAAOK,WAAWA;;0BAC3D,KAACI;gBACCF,aAAU;gBACVG,eAAY;gBACZL,WAAU;0BAETL;;0BAEH,KAACW;gBAAGJ,aAAU;gBAAqBF,WAAWZ;0BAC3CQ,MAAMW,GAAG,CAACC,CAAAA,qBACT,KAACC;wBAECP,aAAU;wBACVF,WAAU;kCAETQ,KAAKE,OAAO,iBACX,KAACN;4BACCO,gBAAcZ,SAAS,SAAS,SAAS;4BACzCC,WAAWb;sCAEVqB,KAAKb,KAAK;2CAGb,KAACG;4BAAKc,MAAMJ,KAAKI,IAAI;4BAAEZ,WAAWX;sCAC/BmB,KAAKb,KAAK;;uBAbVa,KAAKK,GAAG;;;;AAqBzB"}
@@ -44,11 +44,11 @@ import { cn } from '../../utils/cn.js';
44
44
  };
45
45
  return /*#__PURE__*/ _jsx("div", {
46
46
  "data-slot": "split-view",
47
- className: cn('lg:min-h-[28rem] relative', className),
47
+ className: cn('lg:min-h-112 relative', className),
48
48
  children: /*#__PURE__*/ _jsx("ol", {
49
49
  "data-slot": "split-list",
50
50
  "aria-label": label,
51
- className: "m-0 p-0 lg:max-h-[28rem] lg:w-[40%] lg:overflow-y-auto list-none",
51
+ className: "m-0 p-0 lg:max-h-112 lg:w-2/5 lg:overflow-y-auto list-none",
52
52
  children: items.map((item, at)=>{
53
53
  const chosen = at === index;
54
54
  return /*#__PURE__*/ _jsxs("li", {
@@ -66,7 +66,7 @@ import { cn } from '../../utils/cn.js';
66
66
  }),
67
67
  chosen && /*#__PURE__*/ _jsx("div", {
68
68
  "data-slot": "split-detail",
69
- className: "lg:absolute lg:top-0 lg:right-0 lg:max-h-[28rem] lg:w-[58%] lg:overflow-y-auto lg:p-0 px-xs py-s",
69
+ className: "lg:absolute lg:top-0 lg:right-0 lg:max-h-112 lg:w-7/12 lg:overflow-y-auto lg:p-0 px-xs py-s",
70
70
  children: item.detail
71
71
  })
72
72
  ]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/split-view/split-view.tsx"],"sourcesContent":["'use client';\n\nimport type { KeyboardEvent, MouseEvent, ReactNode } from 'react';\n\nimport { cn } from '../../utils/cn.js';\n\nexport interface SplitViewItem {\n readonly id: string;\n /** What the list shows of the item: its number and title, say. */\n readonly head: ReactNode;\n /** What the pane shows once the item is chosen. */\n readonly detail: ReactNode;\n /** Where the item lives on its own: followed without scripting. */\n readonly href: string;\n}\n\n/** The keys that move the choice, and where each moves it. */\nconst MOVES: Readonly<\n Record<string, (index: number, count: number) => number>\n> = {\n ArrowDown: (index, count) => Math.min(index + 1, count - 1),\n ArrowUp: index => Math.max(index - 1, 0),\n Home: () => 0,\n End: (_, count) => count - 1,\n};\n\n/**\n * A list beside a reading pane: the chosen item's detail shows next to the\n * list on a wide screen, and under its own row on a narrow one, where the\n * list reads as an accordion.\n *\n * One markup for both: the chosen row carries its detail. On a wide screen\n * the list is a column of fixed height that scrolls, and the detail is placed\n * against the whole view, so the list's overflow neither clips nor scrolls it.\n *\n * Each row is a link to the item's own page, which is what a visitor without\n * scripting follows. With it, a plain click chooses instead, and ↑, ↓, Home\n * and End move the choice, focus following it (one row in the tab order).\n * Parts carry `data-slot` (`split-view`, `split-list`, `split-row`,\n * `split-trigger`, `split-detail`) for a page to restyle.\n */\nexport function SplitView({\n label,\n items,\n selected,\n onSelect,\n className,\n}: {\n /** The list's accessible name. */\n label: string;\n items: readonly SplitViewItem[];\n /** The chosen item's id; an unknown one chooses nothing. */\n selected: string | undefined;\n onSelect: (id: string) => void;\n className?: string;\n}) {\n const index = items.findIndex(item => item.id === selected);\n const focusable = index === -1 ? 0 : index;\n\n const choose = (event: MouseEvent, id: string) => {\n // A new tab or window is the link's own business.\n if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey)\n return;\n event.preventDefault();\n onSelect(id);\n };\n\n // On each trigger rather than the list: a list is not interactive, so the\n // keys belong to the row that has focus.\n const move = (event: KeyboardEvent<HTMLAnchorElement>) => {\n const to = MOVES[event.key];\n if (to === undefined) return;\n event.preventDefault();\n const next = to(focusable, items.length);\n const triggers = event.currentTarget\n .closest('[data-slot=\"split-list\"]')\n ?.querySelectorAll<HTMLElement>('[data-slot=\"split-trigger\"]');\n triggers?.[next]?.focus();\n onSelect((items[next] as SplitViewItem).id);\n };\n\n return (\n <div\n data-slot=\"split-view\"\n className={cn('lg:min-h-[28rem] relative', className)}\n >\n <ol\n data-slot=\"split-list\"\n aria-label={label}\n className=\"m-0 p-0 lg:max-h-[28rem] lg:w-[40%] lg:overflow-y-auto list-none\"\n >\n {items.map((item, at) => {\n const chosen = at === index;\n return (\n <li key={item.id} data-slot=\"split-row\">\n <a\n href={item.href}\n data-slot=\"split-trigger\"\n aria-current={chosen ? 'true' : undefined}\n tabIndex={at === focusable ? 0 : -1}\n onClick={event => choose(event, item.id)}\n onKeyDown={move}\n className=\"block rounded-md px-xs py-2xs text-inherit no-underline focus-ring hover:bg-[color-mix(in_srgb,var(--color-content)_6%,transparent)] aria-[current]:bg-[color-mix(in_srgb,var(--color-primary)_14%,transparent)]\"\n >\n {item.head}\n </a>\n {chosen && (\n <div\n data-slot=\"split-detail\"\n className=\"lg:absolute lg:top-0 lg:right-0 lg:max-h-[28rem] lg:w-[58%] lg:overflow-y-auto lg:p-0 px-xs py-s\"\n >\n {item.detail}\n </div>\n )}\n </li>\n );\n })}\n </ol>\n </div>\n );\n}\n"],"names":["cn","MOVES","ArrowDown","index","count","Math","min","ArrowUp","max","Home","End","_","SplitView","label","items","selected","onSelect","className","findIndex","item","id","focusable","choose","event","button","metaKey","ctrlKey","shiftKey","preventDefault","move","triggers","to","key","undefined","next","length","currentTarget","closest","querySelectorAll","focus","div","data-slot","ol","aria-label","map","at","chosen","li","a","href","aria-current","tabIndex","onClick","onKeyDown","head","detail"],"mappings":"AAAA;;AAIA,SAASA,EAAE,QAAQ,oBAAoB;AAYvC,4DAA4D,GAC5D,MAAMC,QAEF;IACFC,WAAW,CAACC,OAAOC,QAAUC,KAAKC,GAAG,CAACH,QAAQ,GAAGC,QAAQ;IACzDG,SAASJ,CAAAA,QAASE,KAAKG,GAAG,CAACL,QAAQ,GAAG;IACtCM,MAAM,IAAM;IACZC,KAAK,CAACC,GAAGP,QAAUA,QAAQ;AAC7B;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASQ,UAAU,EACxBC,KAAK,EACLC,KAAK,EACLC,QAAQ,EACRC,QAAQ,EACRC,SAAS,EASV;IACC,MAAMd,QAAQW,MAAMI,SAAS,CAACC,CAAAA,OAAQA,KAAKC,EAAE,KAAKL;IAClD,MAAMM,YAAYlB,UAAU,CAAC,IAAI,IAAIA;IAErC,MAAMmB,SAAS,CAACC,OAAmBH;QACjC,kDAAkD;QAClD,IAAIG,MAAMC,MAAM,KAAK,KAAKD,MAAME,OAAO,IAAIF,MAAMG,OAAO,IAAIH,MAAMI,QAAQ,EACxE;QACFJ,MAAMK,cAAc;QACpBZ,SAASI;IACX;IAEA,0EAA0E;IAC1E,yCAAyC;IACzC,MAAMS,OAAO,CAACN;YAKKA,8BAGjBO;QAPA,MAAMC,KAAK9B,KAAK,CAACsB,MAAMS,GAAG,CAAC;QAC3B,IAAID,OAAOE,WAAW;QACtBV,MAAMK,cAAc;QACpB,MAAMM,OAAOH,GAAGV,WAAWP,MAAMqB,MAAM;QACvC,MAAML,YAAWP,+BAAAA,MAAMa,aAAa,CACjCC,OAAO,CAAC,gDADMd,6BAEbe,gBAAgB,CAAc;QAClCR,6BAAAA,iBAAAA,QAAU,CAACI,KAAK,qBAAhBJ,eAAkBS,KAAK;QACvBvB,SAAS,AAACF,KAAK,CAACoB,KAAK,CAAmBd,EAAE;IAC5C;IAEA,qBACE,KAACoB;QACCC,aAAU;QACVxB,WAAWjB,GAAG,6BAA6BiB;kBAE3C,cAAA,KAACyB;YACCD,aAAU;YACVE,cAAY9B;YACZI,WAAU;sBAETH,MAAM8B,GAAG,CAAC,CAACzB,MAAM0B;gBAChB,MAAMC,SAASD,OAAO1C;gBACtB,qBACE,MAAC4C;oBAAiBN,aAAU;;sCAC1B,KAACO;4BACCC,MAAM9B,KAAK8B,IAAI;4BACfR,aAAU;4BACVS,gBAAcJ,SAAS,SAASb;4BAChCkB,UAAUN,OAAOxB,YAAY,IAAI,CAAC;4BAClC+B,SAAS7B,CAAAA,QAASD,OAAOC,OAAOJ,KAAKC,EAAE;4BACvCiC,WAAWxB;4BACXZ,WAAU;sCAETE,KAAKmC,IAAI;;wBAEXR,wBACC,KAACN;4BACCC,aAAU;4BACVxB,WAAU;sCAETE,KAAKoC,MAAM;;;mBAjBTpC,KAAKC,EAAE;YAsBpB;;;AAIR"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/split-view/split-view.tsx"],"sourcesContent":["'use client';\n\nimport type { KeyboardEvent, MouseEvent, ReactNode } from 'react';\n\nimport { cn } from '../../utils/cn.js';\n\nexport interface SplitViewItem {\n readonly id: string;\n /** What the list shows of the item: its number and title, say. */\n readonly head: ReactNode;\n /** What the pane shows once the item is chosen. */\n readonly detail: ReactNode;\n /** Where the item lives on its own: followed without scripting. */\n readonly href: string;\n}\n\n/** The keys that move the choice, and where each moves it. */\nconst MOVES: Readonly<\n Record<string, (index: number, count: number) => number>\n> = {\n ArrowDown: (index, count) => Math.min(index + 1, count - 1),\n ArrowUp: index => Math.max(index - 1, 0),\n Home: () => 0,\n End: (_, count) => count - 1,\n};\n\n/**\n * A list beside a reading pane: the chosen item's detail shows next to the\n * list on a wide screen, and under its own row on a narrow one, where the\n * list reads as an accordion.\n *\n * One markup for both: the chosen row carries its detail. On a wide screen\n * the list is a column of fixed height that scrolls, and the detail is placed\n * against the whole view, so the list's overflow neither clips nor scrolls it.\n *\n * Each row is a link to the item's own page, which is what a visitor without\n * scripting follows. With it, a plain click chooses instead, and ↑, ↓, Home\n * and End move the choice, focus following it (one row in the tab order).\n * Parts carry `data-slot` (`split-view`, `split-list`, `split-row`,\n * `split-trigger`, `split-detail`) for a page to restyle.\n */\nexport function SplitView({\n label,\n items,\n selected,\n onSelect,\n className,\n}: {\n /** The list's accessible name. */\n label: string;\n items: readonly SplitViewItem[];\n /** The chosen item's id; an unknown one chooses nothing. */\n selected: string | undefined;\n onSelect: (id: string) => void;\n className?: string;\n}) {\n const index = items.findIndex(item => item.id === selected);\n const focusable = index === -1 ? 0 : index;\n\n const choose = (event: MouseEvent, id: string) => {\n // A new tab or window is the link's own business.\n if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey)\n return;\n event.preventDefault();\n onSelect(id);\n };\n\n // On each trigger rather than the list: a list is not interactive, so the\n // keys belong to the row that has focus.\n const move = (event: KeyboardEvent<HTMLAnchorElement>) => {\n const to = MOVES[event.key];\n if (to === undefined) return;\n event.preventDefault();\n const next = to(focusable, items.length);\n const triggers = event.currentTarget\n .closest('[data-slot=\"split-list\"]')\n ?.querySelectorAll<HTMLElement>('[data-slot=\"split-trigger\"]');\n triggers?.[next]?.focus();\n onSelect((items[next] as SplitViewItem).id);\n };\n\n return (\n <div\n data-slot=\"split-view\"\n className={cn('lg:min-h-112 relative', className)}\n >\n <ol\n data-slot=\"split-list\"\n aria-label={label}\n className=\"m-0 p-0 lg:max-h-112 lg:w-2/5 lg:overflow-y-auto list-none\"\n >\n {items.map((item, at) => {\n const chosen = at === index;\n return (\n <li key={item.id} data-slot=\"split-row\">\n <a\n href={item.href}\n data-slot=\"split-trigger\"\n aria-current={chosen ? 'true' : undefined}\n tabIndex={at === focusable ? 0 : -1}\n onClick={event => choose(event, item.id)}\n onKeyDown={move}\n className=\"block rounded-md px-xs py-2xs text-inherit no-underline focus-ring hover:bg-[color-mix(in_srgb,var(--color-content)_6%,transparent)] aria-[current]:bg-[color-mix(in_srgb,var(--color-primary)_14%,transparent)]\"\n >\n {item.head}\n </a>\n {chosen && (\n <div\n data-slot=\"split-detail\"\n className=\"lg:absolute lg:top-0 lg:right-0 lg:max-h-112 lg:w-7/12 lg:overflow-y-auto lg:p-0 px-xs py-s\"\n >\n {item.detail}\n </div>\n )}\n </li>\n );\n })}\n </ol>\n </div>\n );\n}\n"],"names":["cn","MOVES","ArrowDown","index","count","Math","min","ArrowUp","max","Home","End","_","SplitView","label","items","selected","onSelect","className","findIndex","item","id","focusable","choose","event","button","metaKey","ctrlKey","shiftKey","preventDefault","move","triggers","to","key","undefined","next","length","currentTarget","closest","querySelectorAll","focus","div","data-slot","ol","aria-label","map","at","chosen","li","a","href","aria-current","tabIndex","onClick","onKeyDown","head","detail"],"mappings":"AAAA;;AAIA,SAASA,EAAE,QAAQ,oBAAoB;AAYvC,4DAA4D,GAC5D,MAAMC,QAEF;IACFC,WAAW,CAACC,OAAOC,QAAUC,KAAKC,GAAG,CAACH,QAAQ,GAAGC,QAAQ;IACzDG,SAASJ,CAAAA,QAASE,KAAKG,GAAG,CAACL,QAAQ,GAAG;IACtCM,MAAM,IAAM;IACZC,KAAK,CAACC,GAAGP,QAAUA,QAAQ;AAC7B;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASQ,UAAU,EACxBC,KAAK,EACLC,KAAK,EACLC,QAAQ,EACRC,QAAQ,EACRC,SAAS,EASV;IACC,MAAMd,QAAQW,MAAMI,SAAS,CAACC,CAAAA,OAAQA,KAAKC,EAAE,KAAKL;IAClD,MAAMM,YAAYlB,UAAU,CAAC,IAAI,IAAIA;IAErC,MAAMmB,SAAS,CAACC,OAAmBH;QACjC,kDAAkD;QAClD,IAAIG,MAAMC,MAAM,KAAK,KAAKD,MAAME,OAAO,IAAIF,MAAMG,OAAO,IAAIH,MAAMI,QAAQ,EACxE;QACFJ,MAAMK,cAAc;QACpBZ,SAASI;IACX;IAEA,0EAA0E;IAC1E,yCAAyC;IACzC,MAAMS,OAAO,CAACN;YAKKA,8BAGjBO;QAPA,MAAMC,KAAK9B,KAAK,CAACsB,MAAMS,GAAG,CAAC;QAC3B,IAAID,OAAOE,WAAW;QACtBV,MAAMK,cAAc;QACpB,MAAMM,OAAOH,GAAGV,WAAWP,MAAMqB,MAAM;QACvC,MAAML,YAAWP,+BAAAA,MAAMa,aAAa,CACjCC,OAAO,CAAC,gDADMd,6BAEbe,gBAAgB,CAAc;QAClCR,6BAAAA,iBAAAA,QAAU,CAACI,KAAK,qBAAhBJ,eAAkBS,KAAK;QACvBvB,SAAS,AAACF,KAAK,CAACoB,KAAK,CAAmBd,EAAE;IAC5C;IAEA,qBACE,KAACoB;QACCC,aAAU;QACVxB,WAAWjB,GAAG,yBAAyBiB;kBAEvC,cAAA,KAACyB;YACCD,aAAU;YACVE,cAAY9B;YACZI,WAAU;sBAETH,MAAM8B,GAAG,CAAC,CAACzB,MAAM0B;gBAChB,MAAMC,SAASD,OAAO1C;gBACtB,qBACE,MAAC4C;oBAAiBN,aAAU;;sCAC1B,KAACO;4BACCC,MAAM9B,KAAK8B,IAAI;4BACfR,aAAU;4BACVS,gBAAcJ,SAAS,SAASb;4BAChCkB,UAAUN,OAAOxB,YAAY,IAAI,CAAC;4BAClC+B,SAAS7B,CAAAA,QAASD,OAAOC,OAAOJ,KAAKC,EAAE;4BACvCiC,WAAWxB;4BACXZ,WAAU;sCAETE,KAAKmC,IAAI;;wBAEXR,wBACC,KAACN;4BACCC,aAAU;4BACVxB,WAAU;sCAETE,KAAKoC,MAAM;;;mBAjBTpC,KAAKC,EAAE;YAsBpB;;;AAIR"}
@@ -1,29 +1,68 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
2
- export type TabStripProps = ComponentPropsWithoutRef<'div'>;
1
+ import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
+ export interface TabStripProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children'> {
3
+ /** The {@link Tab}s, in order. */
4
+ children?: ReactNode;
5
+ /** The tablist's accessible name; the controls catalog's `tabs.list` when omitted. */
6
+ label?: string;
7
+ /**
8
+ * What sits after the tabs — a {@link TabAddButton}. It is outside the
9
+ * tablist, because a tablist may own nothing but tabs.
10
+ */
11
+ actions?: ReactNode;
12
+ }
3
13
  /**
4
- * The workspace tab strip: a horizontally scrolling row of {@link Tab}s sitting
5
- * on the workspace surface. Presentational only — the open set, order, and
6
- * active tab come from the caller (the tabs store lives in the shell).
14
+ * The workspace tab strip, on the WAI-ARIA tabs pattern: one Tab stop for the
15
+ * whole list, Arrow Left/Right (wrapping), Home and End move between tabs, and
16
+ * Enter or Space activates the focused one — activation is manual, because a
17
+ * tab's body is a whole screen and rendering every one an arrow passes over is
18
+ * work nobody asked for. Delete closes a closable tab (see {@link Tab}).
19
+ *
20
+ * Presentational: the open set, the order and the active tab come from the
21
+ * caller. Each tab controls one {@link TabPanel}, which the caller renders.
22
+ * Below the width of its tabs the list scrolls on its own, so the strip
23
+ * reflows at 320px rather than widening the page.
7
24
  */
8
- export declare function TabStrip({ className, children, ...props }: TabStripProps): import("react").JSX.Element;
25
+ export declare function TabStrip({ className, children, label, actions, ...props }: TabStripProps): import("react").JSX.Element;
9
26
  /** `dirty` = unsaved draft; `saving` = autosave in flight; `error` = save failed. */
10
27
  export type TabState = 'idle' | 'dirty' | 'saving' | 'error';
11
28
  export interface TabProps {
29
+ /** The tab's element id, which its {@link TabPanel} is labelled by. */
30
+ id: string;
12
31
  label: string;
32
+ /** The id of the {@link TabPanel} this tab shows, when it is the active one. */
33
+ controls?: string;
13
34
  active?: boolean;
14
35
  state?: TabState;
15
36
  onSelect?: () => void;
37
+ /** Makes the tab closable: by the × beside it, or Delete while it has focus. */
16
38
  onClose?: () => void;
17
39
  className?: string;
40
+ /** Set by {@link TabStrip} on the first tab when none is active. */
41
+ tabStop?: boolean;
42
+ }
43
+ /**
44
+ * One tab. `role="tab"` sits on the button itself, so the tab is a single
45
+ * interactive element; the close × beside it is a pointer convenience only —
46
+ * hidden from assistive technology and out of the Tab order — and Delete on the
47
+ * focused tab is its keyboard equivalent, announced through `aria-keyshortcuts`.
48
+ *
49
+ * The active tab lifts onto the surface (`bg-surface-elevated` + `-mb-px`, so
50
+ * its base dissolves into the content plane below), and a non-idle tab shows
51
+ * the autosave dot and its name says the same thing in words, since a dot alone is
52
+ * colour carrying meaning (WCAG 1.4.1).
53
+ */
54
+ export declare function Tab({ id, label, controls, active, state, onSelect, onClose, className, tabStop, }: TabProps): import("react").JSX.Element;
55
+ export interface TabPanelProps extends ComponentPropsWithoutRef<'div'> {
56
+ id: string;
57
+ /** The id of the {@link Tab} that shows this panel. */
58
+ labelledBy?: string;
18
59
  }
19
60
  /**
20
- * One tab. The active tab lifts onto the surface (`bg-surface-elevated` +
21
- * `-mb-px` so its base dissolves into the content plane below — the seam), and
22
- * a non-idle tab shows the autosave dot: it breathes while `saving`, turns
23
- * `danger` on `error`. The one place the chrome gets loud, on purpose.
61
+ * The region a tab shows. Focusable, as the pattern asks, so a keyboard user
62
+ * can reach a panel whose content has nothing focusable of its own.
24
63
  */
25
- export declare function Tab({ label, active, state, onSelect, onClose, className, }: TabProps): import("react").JSX.Element;
64
+ export declare function TabPanel({ labelledBy, className, ...props }: TabPanelProps): import("react").JSX.Element;
26
65
  export type TabAddButtonProps = ComponentPropsWithoutRef<'button'>;
27
- /** The "+" that opens a new tab. */
66
+ /** The "+" that opens a new tab. Pass it as {@link TabStrip}'s `actions`. */
28
67
  export declare function TabAddButton({ className, ...props }: TabAddButtonProps): import("react").JSX.Element;
29
68
  //# sourceMappingURL=tab-strip.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tab-strip.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/tab-strip/tab-strip.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,OAAO,CAAC;AAKtD,MAAM,MAAM,aAAa,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;AAE5D;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,aAAa,+BAaxE;AAED,qFAAqF;AACrF,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE7D,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,EAClB,KAAK,EACL,MAAc,EACd,KAAc,EACd,QAAQ,EACR,OAAO,EACP,SAAS,GACV,EAAE,QAAQ,+BAyDV;AAED,MAAM,MAAM,iBAAiB,GAAG,wBAAwB,CAAC,QAAQ,CAAC,CAAC;AAEnE,oCAAoC;AACpC,wBAAgB,YAAY,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,iBAAiB,+BAkBtE"}
1
+ {"version":3,"file":"tab-strip.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/tab-strip/tab-strip.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,wBAAwB,EAI7B,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAKf,MAAM,WAAW,aAAc,SAAQ,IAAI,CACzC,wBAAwB,CAAC,KAAK,CAAC,EAC/B,UAAU,CACX;IACC,kCAAkC;IAClC,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB;AAuBD;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,EACvB,SAAS,EACT,QAAQ,EACR,KAAK,EACL,OAAO,EACP,GAAG,KAAK,EACT,EAAE,aAAa,+BAmCf;AAED,qFAAqF;AACrF,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE7D,MAAM,WAAW,QAAQ;IACvB,uEAAuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,GAAG,CAAC,EAClB,EAAE,EACF,KAAK,EACL,QAAQ,EACR,MAAc,EACd,KAAc,EACd,QAAQ,EACR,OAAO,EACP,SAAS,EACT,OAAgB,GACjB,EAAE,QAAQ,+BAgFV;AAED,MAAM,WAAW,aAAc,SAAQ,wBAAwB,CAAC,KAAK,CAAC;IACpE,EAAE,EAAE,MAAM,CAAC;IACX,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,aAAa,+BAY1E;AAED,MAAM,MAAM,iBAAiB,GAAG,wBAAwB,CAAC,QAAQ,CAAC,CAAC;AAEnE,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,iBAAiB,+BAkBtE"}
@@ -2,67 +2,158 @@
2
2
  import { _ as _extends } from "@swc/helpers/_/_extends";
3
3
  import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
4
4
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
+ import { Children, cloneElement, isValidElement } from 'react';
5
6
  import { useT } from '../../../i18n/index.js';
6
7
  import { cn } from '../../utils/cn.js';
7
8
  /**
8
- * The workspace tab strip: a horizontally scrolling row of {@link Tab}s sitting
9
- * on the workspace surface. Presentational only — the open set, order, and
10
- * active tab come from the caller (the tabs store lives in the shell).
9
+ * Arrow Left/Right (wrapping), Home and End move focus to a sibling tab in the
10
+ * same tablist. Each tab handles its own keys, so the tablist stays a plain
11
+ * container and the keyboard model holds for a Tab rendered anywhere.
12
+ */ function moveFocus(event) {
13
+ var _all_to;
14
+ const list = event.currentTarget.closest('[role="tablist"]');
15
+ if (!list) return false;
16
+ const all = [
17
+ ...list.querySelectorAll('[role="tab"]')
18
+ ];
19
+ const from = all.indexOf(event.currentTarget);
20
+ const to = {
21
+ ArrowRight: (from + 1) % all.length,
22
+ ArrowLeft: (from - 1 + all.length) % all.length,
23
+ Home: 0,
24
+ End: all.length - 1
25
+ }[event.key];
26
+ if (to === undefined) return false;
27
+ (_all_to = all[to]) == null ? void 0 : _all_to.focus();
28
+ return true;
29
+ }
30
+ /**
31
+ * The workspace tab strip, on the WAI-ARIA tabs pattern: one Tab stop for the
32
+ * whole list, Arrow Left/Right (wrapping), Home and End move between tabs, and
33
+ * Enter or Space activates the focused one — activation is manual, because a
34
+ * tab's body is a whole screen and rendering every one an arrow passes over is
35
+ * work nobody asked for. Delete closes a closable tab (see {@link Tab}).
36
+ *
37
+ * Presentational: the open set, the order and the active tab come from the
38
+ * caller. Each tab controls one {@link TabPanel}, which the caller renders.
39
+ * Below the width of its tabs the list scrolls on its own, so the strip
40
+ * reflows at 320px rather than widening the page.
11
41
  */ export function TabStrip(_0) {
12
- let { className, children } = _0, props = _object_without_properties_loose(_0, [
42
+ let { className, children, label, actions } = _0, props = _object_without_properties_loose(_0, [
13
43
  "className",
14
- "children"
44
+ "children",
45
+ "label",
46
+ "actions"
15
47
  ]);
16
- return /*#__PURE__*/ _jsx("div", _extends({
17
- role: "tablist",
18
- className: cn('flex items-end gap-3xs overflow-x-auto border-b border-border bg-surface px-2xs', className)
48
+ const t = useT();
49
+ const tabs = Children.toArray(children);
50
+ // Roving tabindex: the active tab is the list's Tab stop, or the first tab
51
+ // when none is active (a dead link shows no tab as its own).
52
+ const anyActive = tabs.some((tab)=>/*#__PURE__*/ isValidElement(tab) && tab.props.active === true);
53
+ const items = anyActive ? tabs : tabs.map((tab, index)=>index === 0 && /*#__PURE__*/ isValidElement(tab) ? /*#__PURE__*/ cloneElement(tab, {
54
+ tabStop: true
55
+ }) : tab);
56
+ return /*#__PURE__*/ _jsxs("div", _extends({
57
+ className: cn('min-w-0 flex w-full items-end gap-3xs border-b border-border bg-surface px-2xs', className)
19
58
  }, props, {
20
- children: children
59
+ children: [
60
+ /*#__PURE__*/ _jsx("div", {
61
+ role: "tablist",
62
+ "aria-label": label != null ? label : t('tabs.list'),
63
+ "aria-orientation": "horizontal",
64
+ className: "min-w-0 flex flex-1 items-end gap-3xs overflow-x-auto",
65
+ children: items
66
+ }),
67
+ actions
68
+ ]
21
69
  }));
22
70
  }
23
71
  /**
24
- * One tab. The active tab lifts onto the surface (`bg-surface-elevated` +
25
- * `-mb-px` so its base dissolves into the content plane below — the seam), and
26
- * a non-idle tab shows the autosave dot: it breathes while `saving`, turns
27
- * `danger` on `error`. The one place the chrome gets loud, on purpose.
28
- */ export function Tab({ label, active = false, state = 'idle', onSelect, onClose, className }) {
72
+ * One tab. `role="tab"` sits on the button itself, so the tab is a single
73
+ * interactive element; the close × beside it is a pointer convenience only —
74
+ * hidden from assistive technology and out of the Tab order — and Delete on the
75
+ * focused tab is its keyboard equivalent, announced through `aria-keyshortcuts`.
76
+ *
77
+ * The active tab lifts onto the surface (`bg-surface-elevated` + `-mb-px`, so
78
+ * its base dissolves into the content plane below), and a non-idle tab shows
79
+ * the autosave dot and its name says the same thing in words, since a dot alone is
80
+ * colour carrying meaning (WCAG 1.4.1).
81
+ */ export function Tab({ id, label, controls, active = false, state = 'idle', onSelect, onClose, className, tabStop = active }) {
29
82
  const t = useT();
30
83
  return /*#__PURE__*/ _jsxs("div", {
31
- role: "tab",
32
- "aria-selected": active,
33
- className: cn('group relative flex items-center gap-2xs rounded-t-md px-s py-2xs text-step-sm', 'transition duration-200 ease-smooth', active ? '-mb-px bg-surface-elevated font-medium text-content shadow-edge' : 'text-content-muted hover:bg-surface-elevated/60', state === 'error' && 'bg-danger-subtle text-danger', className),
84
+ role: "none",
85
+ className: cn('group relative flex shrink-0 items-center rounded-t-md text-step-sm', 'transition duration-200 ease-smooth', active ? '-mb-px bg-surface-elevated font-medium text-content shadow-edge' : 'text-content-muted hover:bg-surface-elevated/60', state === 'error' && 'bg-danger-subtle text-danger', className),
34
86
  children: [
35
87
  active && /*#__PURE__*/ _jsx("span", {
36
88
  "aria-hidden": "true",
37
- className: "inset-x-0 top-0 absolute h-[2px] rounded-full bg-accent"
89
+ className: "inset-x-0 top-0 h-0.5 pointer-events-none absolute rounded-full bg-accent"
38
90
  }),
39
- /*#__PURE__*/ _jsx("button", {
91
+ /*#__PURE__*/ _jsxs("button", {
40
92
  type: "button",
93
+ role: "tab",
94
+ id: id,
95
+ "aria-selected": active,
96
+ "aria-controls": active ? controls : undefined,
97
+ "aria-keyshortcuts": onClose ? 'Delete' : undefined,
98
+ tabIndex: tabStop ? 0 : -1,
41
99
  onClick: onSelect,
42
- className: "max-w-[12rem] truncate focus-ring",
43
- children: label
44
- }),
45
- state !== 'idle' && /*#__PURE__*/ _jsx("span", {
46
- "data-testid": "tab-indicator",
47
- "aria-hidden": "true",
48
- className: cn('size-[6px] shrink-0 rounded-full', state === 'error' ? 'bg-danger' : 'bg-accent', state === 'saving' && 'animate-pulse motion-reduce:animate-none')
100
+ "aria-label": state === 'idle' ? undefined : t('tabs.withState', {
101
+ label,
102
+ state: t(`tabs.${state}`)
103
+ }),
104
+ onKeyDown: (event)=>{
105
+ if (event.key === 'Delete' && onClose) {
106
+ event.preventDefault();
107
+ onClose();
108
+ } else if (moveFocus(event)) {
109
+ event.preventDefault();
110
+ }
111
+ },
112
+ className: cn('max-w-48 flex min-h-(--target-min) items-center gap-2xs rounded-t-md py-2xs pl-s focus-ring', onClose ? 'pr-3xs' : 'pr-s'),
113
+ children: [
114
+ /*#__PURE__*/ _jsx("span", {
115
+ className: "truncate",
116
+ children: label
117
+ }),
118
+ state !== 'idle' && /*#__PURE__*/ _jsx("span", {
119
+ "data-testid": "tab-indicator",
120
+ "aria-hidden": "true",
121
+ className: cn('size-1.5 shrink-0 rounded-full', state === 'error' ? 'bg-danger' : 'bg-accent', state === 'saving' && 'animate-pulse motion-reduce:animate-none')
122
+ })
123
+ ]
49
124
  }),
50
125
  onClose && /*#__PURE__*/ _jsx("button", {
51
126
  type: "button",
52
- "aria-label": t('tabs.close', {
127
+ "aria-hidden": "true",
128
+ tabIndex: -1,
129
+ title: t('tabs.close', {
53
130
  label
54
131
  }),
55
132
  onClick: onClose,
56
- className: cn('rounded ml-2xs p-3xs text-content-muted opacity-0 focus-ring transition', 'group-hover:opacity-100 hover:bg-surface hover:text-content focus:opacity-100', active && 'opacity-100'),
57
- children: /*#__PURE__*/ _jsx("span", {
58
- "aria-hidden": "true",
59
- children: "×"
60
- })
133
+ className: cn('rounded mr-3xs flex min-h-(--target-min) min-w-(--target-min) items-center justify-center text-content-muted opacity-0 transition', 'group-focus-within:opacity-100 group-hover:opacity-100 hover:bg-surface hover:text-content', active && 'opacity-100'),
134
+ children: "×"
61
135
  })
62
136
  ]
63
137
  });
64
138
  }
65
- /** The "+" that opens a new tab. */ export function TabAddButton(_0) {
139
+ /**
140
+ * The region a tab shows. Focusable, as the pattern asks, so a keyboard user
141
+ * can reach a panel whose content has nothing focusable of its own.
142
+ */ export function TabPanel(_0) {
143
+ let { labelledBy, className } = _0, props = _object_without_properties_loose(_0, [
144
+ "labelledBy",
145
+ "className"
146
+ ]);
147
+ return /*#__PURE__*/ _jsx("div", _extends({
148
+ role: "tabpanel",
149
+ // The tabs pattern makes the panel itself a Tab stop.
150
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
151
+ tabIndex: 0,
152
+ "aria-labelledby": labelledBy,
153
+ className: cn('focus-ring', className)
154
+ }, props));
155
+ }
156
+ /** The "+" that opens a new tab. Pass it as {@link TabStrip}'s `actions`. */ export function TabAddButton(_0) {
66
157
  let { className } = _0, props = _object_without_properties_loose(_0, [
67
158
  "className"
68
159
  ]);
@@ -70,7 +161,7 @@ import { cn } from '../../utils/cn.js';
70
161
  return /*#__PURE__*/ _jsx("button", _extends({
71
162
  type: "button",
72
163
  "aria-label": t('tabs.new'),
73
- className: cn('mb-3xs shrink-0 rounded-md px-2xs py-3xs text-step-sm text-content-muted', 'transition hover:bg-surface-elevated hover:text-content', 'focus-ring', className)
164
+ className: cn('mb-3xs flex min-h-(--target-min) min-w-(--target-min) shrink-0 items-center justify-center rounded-md px-2xs text-step-sm text-content-muted', 'transition hover:bg-surface-elevated hover:text-content', 'focus-ring', className)
74
165
  }, props, {
75
166
  children: /*#__PURE__*/ _jsx("span", {
76
167
  "aria-hidden": "true",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/tab-strip/tab-strip.tsx"],"sourcesContent":["'use client';\n\nimport type { ComponentPropsWithoutRef } from 'react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport type TabStripProps = ComponentPropsWithoutRef<'div'>;\n\n/**\n * The workspace tab strip: a horizontally scrolling row of {@link Tab}s sitting\n * on the workspace surface. Presentational only — the open set, order, and\n * active tab come from the caller (the tabs store lives in the shell).\n */\nexport function TabStrip({ className, children, ...props }: TabStripProps) {\n return (\n <div\n role=\"tablist\"\n className={cn(\n 'flex items-end gap-3xs overflow-x-auto border-b border-border bg-surface px-2xs',\n className,\n )}\n {...props}\n >\n {children}\n </div>\n );\n}\n\n/** `dirty` = unsaved draft; `saving` = autosave in flight; `error` = save failed. */\nexport type TabState = 'idle' | 'dirty' | 'saving' | 'error';\n\nexport interface TabProps {\n label: string;\n active?: boolean;\n state?: TabState;\n onSelect?: () => void;\n onClose?: () => void;\n className?: string;\n}\n\n/**\n * One tab. The active tab lifts onto the surface (`bg-surface-elevated` +\n * `-mb-px` so its base dissolves into the content plane below — the seam), and\n * a non-idle tab shows the autosave dot: it breathes while `saving`, turns\n * `danger` on `error`. The one place the chrome gets loud, on purpose.\n */\nexport function Tab({\n label,\n active = false,\n state = 'idle',\n onSelect,\n onClose,\n className,\n}: TabProps) {\n const t = useT();\n\n return (\n <div\n role=\"tab\"\n aria-selected={active}\n className={cn(\n 'group relative flex items-center gap-2xs rounded-t-md px-s py-2xs text-step-sm',\n 'transition duration-200 ease-smooth',\n active\n ? '-mb-px bg-surface-elevated font-medium text-content shadow-edge'\n : 'text-content-muted hover:bg-surface-elevated/60',\n state === 'error' && 'bg-danger-subtle text-danger',\n className,\n )}\n >\n {active && (\n <span\n aria-hidden=\"true\"\n className=\"inset-x-0 top-0 absolute h-[2px] rounded-full bg-accent\"\n />\n )}\n <button\n type=\"button\"\n onClick={onSelect}\n className=\"max-w-[12rem] truncate focus-ring\"\n >\n {label}\n </button>\n {state !== 'idle' && (\n <span\n data-testid=\"tab-indicator\"\n aria-hidden=\"true\"\n className={cn(\n 'size-[6px] shrink-0 rounded-full',\n state === 'error' ? 'bg-danger' : 'bg-accent',\n state === 'saving' && 'animate-pulse motion-reduce:animate-none',\n )}\n />\n )}\n {onClose && (\n <button\n type=\"button\"\n aria-label={t('tabs.close', { label })}\n onClick={onClose}\n className={cn(\n 'rounded ml-2xs p-3xs text-content-muted opacity-0 focus-ring transition',\n 'group-hover:opacity-100 hover:bg-surface hover:text-content focus:opacity-100',\n active && 'opacity-100',\n )}\n >\n <span aria-hidden=\"true\">×</span>\n </button>\n )}\n </div>\n );\n}\n\nexport type TabAddButtonProps = ComponentPropsWithoutRef<'button'>;\n\n/** The \"+\" that opens a new tab. */\nexport function TabAddButton({ className, ...props }: TabAddButtonProps) {\n const t = useT();\n\n return (\n <button\n type=\"button\"\n aria-label={t('tabs.new')}\n className={cn(\n 'mb-3xs shrink-0 rounded-md px-2xs py-3xs text-step-sm text-content-muted',\n 'transition hover:bg-surface-elevated hover:text-content',\n 'focus-ring',\n className,\n )}\n {...props}\n >\n <span aria-hidden=\"true\">+</span>\n </button>\n );\n}\n"],"names":["useT","cn","TabStrip","className","children","props","div","role","Tab","label","active","state","onSelect","onClose","t","aria-selected","span","aria-hidden","button","type","onClick","data-testid","aria-label","TabAddButton"],"mappings":"AAAA;;;;AAIA,SAASA,IAAI,QAAQ,yBAAyB;AAC9C,SAASC,EAAE,QAAQ,oBAAoB;AAIvC;;;;CAIC,GACD,OAAO,SAASC,SAAS,EAAgD;QAAhD,EAAEC,SAAS,EAAEC,QAAQ,EAA2B,OAAtBC;;;;IACjD,qBACE,KAACC;QACCC,MAAK;QACLJ,WAAWF,GACT,mFACAE;OAEEE;kBAEHD;;AAGP;AAcA;;;;;CAKC,GACD,OAAO,SAASI,IAAI,EAClBC,KAAK,EACLC,SAAS,KAAK,EACdC,QAAQ,MAAM,EACdC,QAAQ,EACRC,OAAO,EACPV,SAAS,EACA;IACT,MAAMW,IAAId;IAEV,qBACE,MAACM;QACCC,MAAK;QACLQ,iBAAeL;QACfP,WAAWF,GACT,kFACA,uCACAS,SACI,oEACA,mDACJC,UAAU,WAAW,gCACrBR;;YAGDO,wBACC,KAACM;gBACCC,eAAY;gBACZd,WAAU;;0BAGd,KAACe;gBACCC,MAAK;gBACLC,SAASR;gBACTT,WAAU;0BAETM;;YAEFE,UAAU,wBACT,KAACK;gBACCK,eAAY;gBACZJ,eAAY;gBACZd,WAAWF,GACT,oCACAU,UAAU,UAAU,cAAc,aAClCA,UAAU,YAAY;;YAI3BE,yBACC,KAACK;gBACCC,MAAK;gBACLG,cAAYR,EAAE,cAAc;oBAAEL;gBAAM;gBACpCW,SAASP;gBACTV,WAAWF,GACT,2EACA,iFACAS,UAAU;0BAGZ,cAAA,KAACM;oBAAKC,eAAY;8BAAO;;;;;AAKnC;AAIA,kCAAkC,GAClC,OAAO,SAASM,aAAa,EAA0C;QAA1C,EAAEpB,SAAS,EAA+B,OAA1BE;;;IAC3C,MAAMS,IAAId;IAEV,qBACE,KAACkB;QACCC,MAAK;QACLG,cAAYR,EAAE;QACdX,WAAWF,GACT,4EACA,2DACA,cACAE;OAEEE;kBAEJ,cAAA,KAACW;YAAKC,eAAY;sBAAO;;;AAG/B"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/tab-strip/tab-strip.tsx"],"sourcesContent":["'use client';\n\nimport {\n Children,\n cloneElement,\n type ComponentPropsWithoutRef,\n isValidElement,\n type KeyboardEvent,\n type ReactElement,\n type ReactNode,\n} from 'react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface TabStripProps extends Omit<\n ComponentPropsWithoutRef<'div'>,\n 'children'\n> {\n /** The {@link Tab}s, in order. */\n children?: ReactNode;\n /** The tablist's accessible name; the controls catalog's `tabs.list` when omitted. */\n label?: string;\n /**\n * What sits after the tabs — a {@link TabAddButton}. It is outside the\n * tablist, because a tablist may own nothing but tabs.\n */\n actions?: ReactNode;\n}\n\n/**\n * Arrow Left/Right (wrapping), Home and End move focus to a sibling tab in the\n * same tablist. Each tab handles its own keys, so the tablist stays a plain\n * container and the keyboard model holds for a Tab rendered anywhere.\n */\nfunction moveFocus(event: KeyboardEvent<HTMLButtonElement>): boolean {\n const list = event.currentTarget.closest('[role=\"tablist\"]');\n if (!list) return false;\n const all = [...list.querySelectorAll<HTMLElement>('[role=\"tab\"]')];\n const from = all.indexOf(event.currentTarget);\n const to = {\n ArrowRight: (from + 1) % all.length,\n ArrowLeft: (from - 1 + all.length) % all.length,\n Home: 0,\n End: all.length - 1,\n }[event.key];\n if (to === undefined) return false;\n all[to]?.focus();\n return true;\n}\n\n/**\n * The workspace tab strip, on the WAI-ARIA tabs pattern: one Tab stop for the\n * whole list, Arrow Left/Right (wrapping), Home and End move between tabs, and\n * Enter or Space activates the focused one — activation is manual, because a\n * tab's body is a whole screen and rendering every one an arrow passes over is\n * work nobody asked for. Delete closes a closable tab (see {@link Tab}).\n *\n * Presentational: the open set, the order and the active tab come from the\n * caller. Each tab controls one {@link TabPanel}, which the caller renders.\n * Below the width of its tabs the list scrolls on its own, so the strip\n * reflows at 320px rather than widening the page.\n */\nexport function TabStrip({\n className,\n children,\n label,\n actions,\n ...props\n}: TabStripProps) {\n const t = useT();\n const tabs = Children.toArray(children);\n // Roving tabindex: the active tab is the list's Tab stop, or the first tab\n // when none is active (a dead link shows no tab as its own).\n const anyActive = tabs.some(\n tab => isValidElement<TabProps>(tab) && tab.props.active === true,\n );\n const items = anyActive\n ? tabs\n : tabs.map((tab, index) =>\n index === 0 && isValidElement<TabProps>(tab)\n ? cloneElement(tab as ReactElement<TabProps>, { tabStop: true })\n : tab,\n );\n\n return (\n <div\n className={cn(\n 'min-w-0 flex w-full items-end gap-3xs border-b border-border bg-surface px-2xs',\n className,\n )}\n {...props}\n >\n <div\n role=\"tablist\"\n aria-label={label ?? t('tabs.list')}\n aria-orientation=\"horizontal\"\n className=\"min-w-0 flex flex-1 items-end gap-3xs overflow-x-auto\"\n >\n {items}\n </div>\n {actions}\n </div>\n );\n}\n\n/** `dirty` = unsaved draft; `saving` = autosave in flight; `error` = save failed. */\nexport type TabState = 'idle' | 'dirty' | 'saving' | 'error';\n\nexport interface TabProps {\n /** The tab's element id, which its {@link TabPanel} is labelled by. */\n id: string;\n label: string;\n /** The id of the {@link TabPanel} this tab shows, when it is the active one. */\n controls?: string;\n active?: boolean;\n state?: TabState;\n onSelect?: () => void;\n /** Makes the tab closable: by the × beside it, or Delete while it has focus. */\n onClose?: () => void;\n className?: string;\n /** Set by {@link TabStrip} on the first tab when none is active. */\n tabStop?: boolean;\n}\n\n/**\n * One tab. `role=\"tab\"` sits on the button itself, so the tab is a single\n * interactive element; the close × beside it is a pointer convenience only —\n * hidden from assistive technology and out of the Tab order — and Delete on the\n * focused tab is its keyboard equivalent, announced through `aria-keyshortcuts`.\n *\n * The active tab lifts onto the surface (`bg-surface-elevated` + `-mb-px`, so\n * its base dissolves into the content plane below), and a non-idle tab shows\n * the autosave dot and its name says the same thing in words, since a dot alone is\n * colour carrying meaning (WCAG 1.4.1).\n */\nexport function Tab({\n id,\n label,\n controls,\n active = false,\n state = 'idle',\n onSelect,\n onClose,\n className,\n tabStop = active,\n}: TabProps) {\n const t = useT();\n\n return (\n <div\n role=\"none\"\n className={cn(\n 'group relative flex shrink-0 items-center rounded-t-md text-step-sm',\n 'transition duration-200 ease-smooth',\n active\n ? '-mb-px bg-surface-elevated font-medium text-content shadow-edge'\n : 'text-content-muted hover:bg-surface-elevated/60',\n state === 'error' && 'bg-danger-subtle text-danger',\n className,\n )}\n >\n {active && (\n <span\n aria-hidden=\"true\"\n className=\"inset-x-0 top-0 h-0.5 pointer-events-none absolute rounded-full bg-accent\"\n />\n )}\n <button\n type=\"button\"\n role=\"tab\"\n id={id}\n aria-selected={active}\n aria-controls={active ? controls : undefined}\n aria-keyshortcuts={onClose ? 'Delete' : undefined}\n tabIndex={tabStop ? 0 : -1}\n onClick={onSelect}\n aria-label={\n state === 'idle'\n ? undefined\n : t('tabs.withState', { label, state: t(`tabs.${state}`) })\n }\n onKeyDown={event => {\n if (event.key === 'Delete' && onClose) {\n event.preventDefault();\n onClose();\n } else if (moveFocus(event)) {\n event.preventDefault();\n }\n }}\n className={cn(\n 'max-w-48 flex min-h-(--target-min) items-center gap-2xs rounded-t-md py-2xs pl-s focus-ring',\n onClose ? 'pr-3xs' : 'pr-s',\n )}\n >\n <span className=\"truncate\">{label}</span>\n {state !== 'idle' && (\n <span\n data-testid=\"tab-indicator\"\n aria-hidden=\"true\"\n className={cn(\n 'size-1.5 shrink-0 rounded-full',\n state === 'error' ? 'bg-danger' : 'bg-accent',\n state === 'saving' && 'animate-pulse motion-reduce:animate-none',\n )}\n />\n )}\n </button>\n {onClose && (\n <button\n type=\"button\"\n aria-hidden=\"true\"\n tabIndex={-1}\n title={t('tabs.close', { label })}\n onClick={onClose}\n className={cn(\n 'rounded mr-3xs flex min-h-(--target-min) min-w-(--target-min) items-center justify-center text-content-muted opacity-0 transition',\n 'group-focus-within:opacity-100 group-hover:opacity-100 hover:bg-surface hover:text-content',\n active && 'opacity-100',\n )}\n >\n ×\n </button>\n )}\n </div>\n );\n}\n\nexport interface TabPanelProps extends ComponentPropsWithoutRef<'div'> {\n id: string;\n /** The id of the {@link Tab} that shows this panel. */\n labelledBy?: string;\n}\n\n/**\n * The region a tab shows. Focusable, as the pattern asks, so a keyboard user\n * can reach a panel whose content has nothing focusable of its own.\n */\nexport function TabPanel({ labelledBy, className, ...props }: TabPanelProps) {\n return (\n <div\n role=\"tabpanel\"\n // The tabs pattern makes the panel itself a Tab stop.\n // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex\n tabIndex={0}\n aria-labelledby={labelledBy}\n className={cn('focus-ring', className)}\n {...props}\n />\n );\n}\n\nexport type TabAddButtonProps = ComponentPropsWithoutRef<'button'>;\n\n/** The \"+\" that opens a new tab. Pass it as {@link TabStrip}'s `actions`. */\nexport function TabAddButton({ className, ...props }: TabAddButtonProps) {\n const t = useT();\n\n return (\n <button\n type=\"button\"\n aria-label={t('tabs.new')}\n className={cn(\n 'mb-3xs flex min-h-(--target-min) min-w-(--target-min) shrink-0 items-center justify-center rounded-md px-2xs text-step-sm text-content-muted',\n 'transition hover:bg-surface-elevated hover:text-content',\n 'focus-ring',\n className,\n )}\n {...props}\n >\n <span aria-hidden=\"true\">+</span>\n </button>\n );\n}\n"],"names":["Children","cloneElement","isValidElement","useT","cn","moveFocus","event","all","list","currentTarget","closest","querySelectorAll","from","indexOf","to","ArrowRight","length","ArrowLeft","Home","End","key","undefined","focus","TabStrip","className","children","label","actions","props","t","tabs","toArray","anyActive","some","tab","active","items","map","index","tabStop","div","role","aria-label","aria-orientation","Tab","id","controls","state","onSelect","onClose","span","aria-hidden","button","type","aria-selected","aria-controls","aria-keyshortcuts","tabIndex","onClick","onKeyDown","preventDefault","data-testid","title","TabPanel","labelledBy","aria-labelledby","TabAddButton"],"mappings":"AAAA;;;;AAEA,SACEA,QAAQ,EACRC,YAAY,EAEZC,cAAc,QAIT,QAAQ;AAEf,SAASC,IAAI,QAAQ,yBAAyB;AAC9C,SAASC,EAAE,QAAQ,oBAAoB;AAiBvC;;;;CAIC,GACD,SAASC,UAAUC,KAAuC;QAYxDC;IAXA,MAAMC,OAAOF,MAAMG,aAAa,CAACC,OAAO,CAAC;IACzC,IAAI,CAACF,MAAM,OAAO;IAClB,MAAMD,MAAM;WAAIC,KAAKG,gBAAgB,CAAc;KAAgB;IACnE,MAAMC,OAAOL,IAAIM,OAAO,CAACP,MAAMG,aAAa;IAC5C,MAAMK,KAAK;QACTC,YAAY,AAACH,CAAAA,OAAO,CAAA,IAAKL,IAAIS,MAAM;QACnCC,WAAW,AAACL,CAAAA,OAAO,IAAIL,IAAIS,MAAM,AAAD,IAAKT,IAAIS,MAAM;QAC/CE,MAAM;QACNC,KAAKZ,IAAIS,MAAM,GAAG;IACpB,CAAC,CAACV,MAAMc,GAAG,CAAC;IACZ,IAAIN,OAAOO,WAAW,OAAO;KAC7Bd,UAAAA,GAAG,CAACO,GAAG,qBAAPP,QAASe,KAAK;IACd,OAAO;AACT;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,SAAS,EAMT;QANS,EACvBC,SAAS,EACTC,QAAQ,EACRC,KAAK,EACLC,OAAO,EAEO,OADXC;;;;;;IAEH,MAAMC,IAAI1B;IACV,MAAM2B,OAAO9B,SAAS+B,OAAO,CAACN;IAC9B,2EAA2E;IAC3E,6DAA6D;IAC7D,MAAMO,YAAYF,KAAKG,IAAI,CACzBC,CAAAA,oBAAOhC,eAAyBgC,QAAQA,IAAIN,KAAK,CAACO,MAAM,KAAK;IAE/D,MAAMC,QAAQJ,YACVF,OACAA,KAAKO,GAAG,CAAC,CAACH,KAAKI,QACbA,UAAU,mBAAKpC,eAAyBgC,qBACpCjC,aAAaiC,KAA+B;YAAEK,SAAS;QAAK,KAC5DL;IAGV,qBACE,MAACM;QACChB,WAAWpB,GACT,kFACAoB;OAEEI;;0BAEJ,KAACY;gBACCC,MAAK;gBACLC,YAAU,EAAEhB,gBAAAA,QAASG,EAAE;gBACvBc,oBAAiB;gBACjBnB,WAAU;0BAETY;;YAEFT;;;AAGP;AAqBA;;;;;;;;;;CAUC,GACD,OAAO,SAASiB,IAAI,EAClBC,EAAE,EACFnB,KAAK,EACLoB,QAAQ,EACRX,SAAS,KAAK,EACdY,QAAQ,MAAM,EACdC,QAAQ,EACRC,OAAO,EACPzB,SAAS,EACTe,UAAUJ,MAAM,EACP;IACT,MAAMN,IAAI1B;IAEV,qBACE,MAACqC;QACCC,MAAK;QACLjB,WAAWpB,GACT,uEACA,uCACA+B,SACI,oEACA,mDACJY,UAAU,WAAW,gCACrBvB;;YAGDW,wBACC,KAACe;gBACCC,eAAY;gBACZ3B,WAAU;;0BAGd,MAAC4B;gBACCC,MAAK;gBACLZ,MAAK;gBACLI,IAAIA;gBACJS,iBAAenB;gBACfoB,iBAAepB,SAASW,WAAWzB;gBACnCmC,qBAAmBP,UAAU,WAAW5B;gBACxCoC,UAAUlB,UAAU,IAAI,CAAC;gBACzBmB,SAASV;gBACTN,cACEK,UAAU,SACN1B,YACAQ,EAAE,kBAAkB;oBAAEH;oBAAOqB,OAAOlB,EAAE,CAAC,KAAK,EAAEkB,OAAO;gBAAE;gBAE7DY,WAAWrD,CAAAA;oBACT,IAAIA,MAAMc,GAAG,KAAK,YAAY6B,SAAS;wBACrC3C,MAAMsD,cAAc;wBACpBX;oBACF,OAAO,IAAI5C,UAAUC,QAAQ;wBAC3BA,MAAMsD,cAAc;oBACtB;gBACF;gBACApC,WAAWpB,GACT,+FACA6C,UAAU,WAAW;;kCAGvB,KAACC;wBAAK1B,WAAU;kCAAYE;;oBAC3BqB,UAAU,wBACT,KAACG;wBACCW,eAAY;wBACZV,eAAY;wBACZ3B,WAAWpB,GACT,kCACA2C,UAAU,UAAU,cAAc,aAClCA,UAAU,YAAY;;;;YAK7BE,yBACC,KAACG;gBACCC,MAAK;gBACLF,eAAY;gBACZM,UAAU,CAAC;gBACXK,OAAOjC,EAAE,cAAc;oBAAEH;gBAAM;gBAC/BgC,SAAST;gBACTzB,WAAWpB,GACT,qIACA,8FACA+B,UAAU;0BAEb;;;;AAMT;AAQA;;;CAGC,GACD,OAAO,SAAS4B,SAAS,EAAkD;QAAlD,EAAEC,UAAU,EAAExC,SAAS,EAA2B,OAAtBI;;;;IACnD,qBACE,KAACY;QACCC,MAAK;QACL,sDAAsD;QACtD,+DAA+D;QAC/DgB,UAAU;QACVQ,mBAAiBD;QACjBxC,WAAWpB,GAAG,cAAcoB;OACxBI;AAGV;AAIA,2EAA2E,GAC3E,OAAO,SAASsC,aAAa,EAA0C;QAA1C,EAAE1C,SAAS,EAA+B,OAA1BI;;;IAC3C,MAAMC,IAAI1B;IAEV,qBACE,KAACiD;QACCC,MAAK;QACLX,cAAYb,EAAE;QACdL,WAAWpB,GACT,gJACA,2DACA,cACAoB;OAEEI;kBAEJ,cAAA,KAACsB;YAAKC,eAAY;sBAAO;;;AAG/B"}
@@ -59,7 +59,7 @@ const ToastContext = /*#__PURE__*/ createContext(undefined);
59
59
  if (toasts === undefined) return null;
60
60
  return /*#__PURE__*/ _jsx("section", {
61
61
  "aria-label": t('toast.region'),
62
- className: "bottom-0 right-0 max-w-md fixed z-50 flex w-full flex-col gap-xs p-s",
62
+ className: "bottom-0 right-0 max-w-md fixed z-(--z-toast) flex w-full flex-col gap-xs p-s",
63
63
  children: toasts.failures.map(({ id, failure })=>/*#__PURE__*/ _jsxs("div", {
64
64
  role: "alert",
65
65
  className: "flex items-start gap-xs rounded-lg border border-danger bg-surface p-xs shadow-overlay",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/toast-region/toast-region.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { useErrorMessage, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\n\n/**\n * A failure as {@link useErrorMessage} renders it: a typed failure carries its\n * `code`, and anything else that reached the user keeps its `message`.\n */\nexport type ToastFailure = Parameters<ReturnType<typeof useErrorMessage>>[0];\n\n/** One failure waiting in the region, under the id it was shown with. */\nexport interface FailureToast {\n readonly id: number;\n readonly failure: ToastFailure;\n}\n\nexport interface Toasts {\n /** The failures shown, oldest first. */\n readonly failures: readonly FailureToast[];\n /** Shows a failure until it is dismissed, and answers its id. */\n readonly showFailure: (failure: ToastFailure) => number;\n readonly dismiss: (id: number) => void;\n}\n\nconst ToastContext = createContext<Toasts | undefined>(undefined);\n\n/**\n * Holds the toasts one host shows. Mounted once, above everything that can\n * fail asynchronously, with a {@link ToastRegion} somewhere inside it.\n */\nexport function ToastProvider({ children }: { readonly children: ReactNode }) {\n const [failures, setFailures] = useState<readonly FailureToast[]>([]);\n const next = useRef(0);\n\n const showFailure = useCallback((failure: ToastFailure) => {\n next.current += 1;\n const id = next.current;\n setFailures(shown => [...shown, { id, failure }]);\n return id;\n }, []);\n const dismiss = useCallback((id: number) => {\n setFailures(shown => shown.filter(toast => toast.id !== id));\n }, []);\n\n const value = useMemo(\n () => ({ failures, showFailure, dismiss }),\n [failures, showFailure, dismiss],\n );\n return (\n <ToastContext.Provider value={value}>{children}</ToastContext.Provider>\n );\n}\n\n/**\n * The {@link ToastProvider} above, or `undefined` outside one: a caller with\n * no region to show a failure in keeps it where it can, rather than having it\n * vanish.\n */\nexport const useToasts = (): Toasts | undefined => useContext(ToastContext);\n\n/**\n * Where a host's toasts render: each failure in the reader's language,\n * through the same {@link useErrorMessage} a form's error slot uses, so an\n * asynchronous failure reads exactly as a synchronous one would.\n *\n * A failure stays until it is dismissed. It reports something that already\n * happened, often after the screen that started it was left, and a message\n * that times out is one a slow reader never gets.\n */\nexport function ToastRegion() {\n const toasts = useToasts();\n const t = useT();\n const message = useErrorMessage();\n\n if (toasts === undefined) return null;\n return (\n <section\n aria-label={t('toast.region')}\n className=\"bottom-0 right-0 max-w-md fixed z-50 flex w-full flex-col gap-xs p-s\"\n >\n {toasts.failures.map(({ id, failure }) => (\n <div\n key={id}\n role=\"alert\"\n className=\"flex items-start gap-xs rounded-lg border border-danger bg-surface p-xs shadow-overlay\"\n >\n <p className=\"flex-1 text-step-sm text-danger\">{message(failure)}</p>\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => toasts.dismiss(id)}\n >\n {t('toast.dismiss')}\n </Button>\n </div>\n ))}\n </section>\n );\n}\n"],"names":["createContext","useCallback","useContext","useMemo","useRef","useState","useErrorMessage","useT","Button","ToastContext","undefined","ToastProvider","children","failures","setFailures","next","showFailure","failure","current","id","shown","dismiss","filter","toast","value","Provider","useToasts","ToastRegion","toasts","t","message","section","aria-label","className","map","div","role","p","type","variant","onClick"],"mappings":"AAAA;;AAEA,SACEA,aAAa,EAEbC,WAAW,EACXC,UAAU,EACVC,OAAO,EACPC,MAAM,EACNC,QAAQ,QACH,QAAQ;AAEf,SAASC,eAAe,EAAEC,IAAI,QAAQ,yBAAyB;AAC/D,SAASC,MAAM,QAAQ,8BAA8B;AAsBrD,MAAMC,6BAAeT,cAAkCU;AAEvD;;;CAGC,GACD,OAAO,SAASC,cAAc,EAAEC,QAAQ,EAAoC;IAC1E,MAAM,CAACC,UAAUC,YAAY,GAAGT,SAAkC,EAAE;IACpE,MAAMU,OAAOX,OAAO;IAEpB,MAAMY,cAAcf,YAAY,CAACgB;QAC/BF,KAAKG,OAAO,IAAI;QAChB,MAAMC,KAAKJ,KAAKG,OAAO;QACvBJ,YAAYM,CAAAA,QAAS;mBAAIA;gBAAO;oBAAED;oBAAIF;gBAAQ;aAAE;QAChD,OAAOE;IACT,GAAG,EAAE;IACL,MAAME,UAAUpB,YAAY,CAACkB;QAC3BL,YAAYM,CAAAA,QAASA,MAAME,MAAM,CAACC,CAAAA,QAASA,MAAMJ,EAAE,KAAKA;IAC1D,GAAG,EAAE;IAEL,MAAMK,QAAQrB,QACZ,IAAO,CAAA;YAAEU;YAAUG;YAAaK;QAAQ,CAAA,GACxC;QAACR;QAAUG;QAAaK;KAAQ;IAElC,qBACE,KAACZ,aAAagB,QAAQ;QAACD,OAAOA;kBAAQZ;;AAE1C;AAEA;;;;CAIC,GACD,OAAO,MAAMc,YAAY,IAA0BxB,WAAWO,cAAc;AAE5E;;;;;;;;CAQC,GACD,OAAO,SAASkB;IACd,MAAMC,SAASF;IACf,MAAMG,IAAItB;IACV,MAAMuB,UAAUxB;IAEhB,IAAIsB,WAAWlB,WAAW,OAAO;IACjC,qBACE,KAACqB;QACCC,cAAYH,EAAE;QACdI,WAAU;kBAETL,OAAOf,QAAQ,CAACqB,GAAG,CAAC,CAAC,EAAEf,EAAE,EAAEF,OAAO,EAAE,iBACnC,MAACkB;gBAECC,MAAK;gBACLH,WAAU;;kCAEV,KAACI;wBAAEJ,WAAU;kCAAmCH,QAAQb;;kCACxD,KAACT;wBACC8B,MAAK;wBACLC,SAAQ;wBACRC,SAAS,IAAMZ,OAAOP,OAAO,CAACF;kCAE7BU,EAAE;;;eAVAV;;AAgBf"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/toast-region/toast-region.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { useErrorMessage, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\n\n/**\n * A failure as {@link useErrorMessage} renders it: a typed failure carries its\n * `code`, and anything else that reached the user keeps its `message`.\n */\nexport type ToastFailure = Parameters<ReturnType<typeof useErrorMessage>>[0];\n\n/** One failure waiting in the region, under the id it was shown with. */\nexport interface FailureToast {\n readonly id: number;\n readonly failure: ToastFailure;\n}\n\nexport interface Toasts {\n /** The failures shown, oldest first. */\n readonly failures: readonly FailureToast[];\n /** Shows a failure until it is dismissed, and answers its id. */\n readonly showFailure: (failure: ToastFailure) => number;\n readonly dismiss: (id: number) => void;\n}\n\nconst ToastContext = createContext<Toasts | undefined>(undefined);\n\n/**\n * Holds the toasts one host shows. Mounted once, above everything that can\n * fail asynchronously, with a {@link ToastRegion} somewhere inside it.\n */\nexport function ToastProvider({ children }: { readonly children: ReactNode }) {\n const [failures, setFailures] = useState<readonly FailureToast[]>([]);\n const next = useRef(0);\n\n const showFailure = useCallback((failure: ToastFailure) => {\n next.current += 1;\n const id = next.current;\n setFailures(shown => [...shown, { id, failure }]);\n return id;\n }, []);\n const dismiss = useCallback((id: number) => {\n setFailures(shown => shown.filter(toast => toast.id !== id));\n }, []);\n\n const value = useMemo(\n () => ({ failures, showFailure, dismiss }),\n [failures, showFailure, dismiss],\n );\n return (\n <ToastContext.Provider value={value}>{children}</ToastContext.Provider>\n );\n}\n\n/**\n * The {@link ToastProvider} above, or `undefined` outside one: a caller with\n * no region to show a failure in keeps it where it can, rather than having it\n * vanish.\n */\nexport const useToasts = (): Toasts | undefined => useContext(ToastContext);\n\n/**\n * Where a host's toasts render: each failure in the reader's language,\n * through the same {@link useErrorMessage} a form's error slot uses, so an\n * asynchronous failure reads exactly as a synchronous one would.\n *\n * A failure stays until it is dismissed. It reports something that already\n * happened, often after the screen that started it was left, and a message\n * that times out is one a slow reader never gets.\n */\nexport function ToastRegion() {\n const toasts = useToasts();\n const t = useT();\n const message = useErrorMessage();\n\n if (toasts === undefined) return null;\n return (\n <section\n aria-label={t('toast.region')}\n className=\"bottom-0 right-0 max-w-md fixed z-(--z-toast) flex w-full flex-col gap-xs p-s\"\n >\n {toasts.failures.map(({ id, failure }) => (\n <div\n key={id}\n role=\"alert\"\n className=\"flex items-start gap-xs rounded-lg border border-danger bg-surface p-xs shadow-overlay\"\n >\n <p className=\"flex-1 text-step-sm text-danger\">{message(failure)}</p>\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => toasts.dismiss(id)}\n >\n {t('toast.dismiss')}\n </Button>\n </div>\n ))}\n </section>\n );\n}\n"],"names":["createContext","useCallback","useContext","useMemo","useRef","useState","useErrorMessage","useT","Button","ToastContext","undefined","ToastProvider","children","failures","setFailures","next","showFailure","failure","current","id","shown","dismiss","filter","toast","value","Provider","useToasts","ToastRegion","toasts","t","message","section","aria-label","className","map","div","role","p","type","variant","onClick"],"mappings":"AAAA;;AAEA,SACEA,aAAa,EAEbC,WAAW,EACXC,UAAU,EACVC,OAAO,EACPC,MAAM,EACNC,QAAQ,QACH,QAAQ;AAEf,SAASC,eAAe,EAAEC,IAAI,QAAQ,yBAAyB;AAC/D,SAASC,MAAM,QAAQ,8BAA8B;AAsBrD,MAAMC,6BAAeT,cAAkCU;AAEvD;;;CAGC,GACD,OAAO,SAASC,cAAc,EAAEC,QAAQ,EAAoC;IAC1E,MAAM,CAACC,UAAUC,YAAY,GAAGT,SAAkC,EAAE;IACpE,MAAMU,OAAOX,OAAO;IAEpB,MAAMY,cAAcf,YAAY,CAACgB;QAC/BF,KAAKG,OAAO,IAAI;QAChB,MAAMC,KAAKJ,KAAKG,OAAO;QACvBJ,YAAYM,CAAAA,QAAS;mBAAIA;gBAAO;oBAAED;oBAAIF;gBAAQ;aAAE;QAChD,OAAOE;IACT,GAAG,EAAE;IACL,MAAME,UAAUpB,YAAY,CAACkB;QAC3BL,YAAYM,CAAAA,QAASA,MAAME,MAAM,CAACC,CAAAA,QAASA,MAAMJ,EAAE,KAAKA;IAC1D,GAAG,EAAE;IAEL,MAAMK,QAAQrB,QACZ,IAAO,CAAA;YAAEU;YAAUG;YAAaK;QAAQ,CAAA,GACxC;QAACR;QAAUG;QAAaK;KAAQ;IAElC,qBACE,KAACZ,aAAagB,QAAQ;QAACD,OAAOA;kBAAQZ;;AAE1C;AAEA;;;;CAIC,GACD,OAAO,MAAMc,YAAY,IAA0BxB,WAAWO,cAAc;AAE5E;;;;;;;;CAQC,GACD,OAAO,SAASkB;IACd,MAAMC,SAASF;IACf,MAAMG,IAAItB;IACV,MAAMuB,UAAUxB;IAEhB,IAAIsB,WAAWlB,WAAW,OAAO;IACjC,qBACE,KAACqB;QACCC,cAAYH,EAAE;QACdI,WAAU;kBAETL,OAAOf,QAAQ,CAACqB,GAAG,CAAC,CAAC,EAAEf,EAAE,EAAEF,OAAO,EAAE,iBACnC,MAACkB;gBAECC,MAAK;gBACLH,WAAU;;kCAEV,KAACI;wBAAEJ,WAAU;kCAAmCH,QAAQb;;kCACxD,KAACT;wBACC8B,MAAK;wBACLC,SAAQ;wBACRC,SAAS,IAAMZ,OAAOP,OAAO,CAACF;kCAE7BU,EAAE;;;eAVAV;;AAgBf"}