@eifi1/ui-kit 0.46.0 → 0.47.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 (148) hide show
  1. package/README.md +12 -11
  2. package/dist/admin/user-roster.d.ts +6 -2
  3. package/dist/admin/user-roster.js +7 -3
  4. package/dist/admin/user-roster.js.map +1 -1
  5. package/dist/components/data-table-row-actions.d.ts +22 -5
  6. package/dist/components/data-table-row-actions.js +14 -5
  7. package/dist/components/data-table-row-actions.js.map +1 -1
  8. package/dist/components/data-table.d.ts +16 -1
  9. package/dist/components/data-table.js +20 -1
  10. package/dist/components/data-table.js.map +1 -1
  11. package/dist/components/row-actions.d.ts +37 -5
  12. package/dist/components/row-actions.js +22 -5
  13. package/dist/components/row-actions.js.map +1 -1
  14. package/dist/components/tabs.d.ts +24 -16
  15. package/dist/components/tabs.js +37 -31
  16. package/dist/components/tabs.js.map +1 -1
  17. package/dist/components/translation-review.js +3 -1
  18. package/dist/components/translation-review.js.map +1 -1
  19. package/dist/components/ui-labels.js +1 -1
  20. package/dist/components/ui-labels.js.map +1 -1
  21. package/dist/data-table.d.ts +1 -0
  22. package/dist/data-table.js +2 -0
  23. package/dist/data-table.js.map +1 -1
  24. package/dist/eslint/index.js +1 -1
  25. package/dist/feedback/feedback-attachment-download.d.ts +19 -0
  26. package/dist/feedback/feedback-attachment-download.js +59 -0
  27. package/dist/feedback/feedback-attachment-download.js.map +1 -0
  28. package/dist/feedback/feedback-comment-thread.d.ts +17 -6
  29. package/dist/feedback/feedback-comment-thread.js +21 -2
  30. package/dist/feedback/feedback-comment-thread.js.map +1 -1
  31. package/dist/feedback/feedback-entry-file.d.ts +61 -0
  32. package/dist/feedback/feedback-entry-file.js +76 -0
  33. package/dist/feedback/feedback-entry-file.js.map +1 -0
  34. package/dist/feedback/feedback-public-list.d.ts +18 -5
  35. package/dist/feedback/feedback-public-list.js +8 -0
  36. package/dist/feedback/feedback-public-list.js.map +1 -1
  37. package/dist/feedback/feedback-public-types.d.ts +7 -2
  38. package/dist/feedback/feedback-replies.d.ts +26 -9
  39. package/dist/feedback/feedback-replies.js +20 -10
  40. package/dist/feedback/feedback-replies.js.map +1 -1
  41. package/dist/feedback/feedback-row-detail-labels.js +0 -1
  42. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  43. package/dist/feedback/feedback-row-detail.d.ts +23 -10
  44. package/dist/feedback/feedback-row-detail.js +10 -54
  45. package/dist/feedback/feedback-row-detail.js.map +1 -1
  46. package/dist/feedback.d.ts +1 -0
  47. package/dist/feedback.js +1 -0
  48. package/dist/feedback.js.map +1 -1
  49. package/dist/i18n/german.js +1 -6
  50. package/dist/i18n/german.js.map +1 -1
  51. package/dist/i18n/locales/en.js +1 -6
  52. package/dist/i18n/locales/en.js.map +2 -2
  53. package/dist/i18n/locales/es.js +1 -6
  54. package/dist/i18n/locales/es.js.map +1 -1
  55. package/dist/i18n/locales/fr.js +1 -6
  56. package/dist/i18n/locales/fr.js.map +1 -1
  57. package/dist/i18n/locales/hu.js +1 -6
  58. package/dist/i18n/locales/hu.js.map +1 -1
  59. package/dist/i18n/locales/it.js +1 -6
  60. package/dist/i18n/locales/it.js.map +1 -1
  61. package/dist/i18n/locales/zh.js +1 -6
  62. package/dist/i18n/locales/zh.js.map +1 -1
  63. package/dist/i18n/review.js +0 -1
  64. package/dist/i18n/review.js.map +1 -1
  65. package/dist/index.d.ts +5 -7
  66. package/dist/index.js +14 -5
  67. package/dist/index.js.map +1 -1
  68. package/dist/notifications/device-zone-report.d.ts +7 -0
  69. package/dist/notifications/device-zone-report.js +8 -1
  70. package/dist/notifications/device-zone-report.js.map +1 -1
  71. package/dist/notifications/feedback-notification-types.d.ts +14 -1
  72. package/dist/notifications/feedback-notification-types.js +5 -1
  73. package/dist/notifications/feedback-notification-types.js.map +1 -1
  74. package/dist/notifications/notification-bell.d.ts +1 -1
  75. package/dist/notifications/notification-bell.js.map +1 -1
  76. package/dist/notifications/notification-inbox.d.ts +12 -3
  77. package/dist/notifications/notification-inbox.js +2 -2
  78. package/dist/notifications/notification-inbox.js.map +1 -1
  79. package/dist/notifications/notification-types.d.ts +0 -3
  80. package/dist/notifications/notifications-labels.d.ts +2 -13
  81. package/dist/notifications/notifications-labels.js +0 -4
  82. package/dist/notifications/notifications-labels.js.map +1 -1
  83. package/dist/notifications/quiet-hours-setting.d.ts +3 -8
  84. package/dist/notifications/quiet-hours-setting.js +8 -3
  85. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  86. package/dist/offline/local-first.d.ts +9 -0
  87. package/dist/offline/local-first.js +14 -0
  88. package/dist/offline/local-first.js.map +1 -1
  89. package/dist/offline.d.ts +1 -1
  90. package/dist/offline.js +2 -1
  91. package/dist/offline.js.map +1 -1
  92. package/dist/sync/db.d.ts +17 -0
  93. package/dist/sync/db.js +10 -1
  94. package/dist/sync/db.js.map +1 -1
  95. package/dist/sync/engine.d.ts +11 -9
  96. package/dist/sync/engine.js.map +1 -1
  97. package/dist/sync/hooks.d.ts +5 -0
  98. package/dist/sync/hooks.js.map +1 -1
  99. package/dist/sync.d.ts +2 -2
  100. package/dist/sync.js +5 -2
  101. package/dist/sync.js.map +1 -1
  102. package/dist/theme/theme-store.d.ts +4 -4
  103. package/dist/theme/theme-store.js +1 -0
  104. package/dist/theme/theme-store.js.map +1 -1
  105. package/package.json +2 -2
  106. package/src/admin/user-roster.tsx +12 -3
  107. package/src/components/data-table-row-actions.tsx +33 -9
  108. package/src/components/data-table.tsx +46 -3
  109. package/src/components/row-actions.tsx +73 -10
  110. package/src/components/tabs.tsx +66 -44
  111. package/src/components/translation-review.tsx +3 -1
  112. package/src/components/ui-labels.ts +1 -1
  113. package/src/data-table.ts +3 -0
  114. package/src/feedback/feedback-attachment-download.tsx +77 -0
  115. package/src/feedback/feedback-comment-thread.tsx +42 -7
  116. package/src/feedback/feedback-entry-file.tsx +150 -0
  117. package/src/feedback/feedback-public-list.tsx +25 -4
  118. package/src/feedback/feedback-public-types.ts +7 -2
  119. package/src/feedback/feedback-replies.tsx +46 -19
  120. package/src/feedback/feedback-row-detail-labels.ts +0 -1
  121. package/src/feedback/feedback-row-detail.tsx +35 -86
  122. package/src/feedback.ts +1 -0
  123. package/src/i18n/german.ts +1 -6
  124. package/src/i18n/locales/es.ts +1 -6
  125. package/src/i18n/locales/fr.ts +1 -6
  126. package/src/i18n/locales/hu.ts +1 -6
  127. package/src/i18n/locales/it.ts +1 -6
  128. package/src/i18n/locales/zh.ts +1 -6
  129. package/src/i18n/review.ts +0 -1
  130. package/src/index.ts +19 -5
  131. package/src/notifications/device-zone-report.ts +14 -1
  132. package/src/notifications/feedback-notification-types.ts +19 -1
  133. package/src/notifications/notification-bell.tsx +1 -1
  134. package/src/notifications/notification-inbox.tsx +15 -5
  135. package/src/notifications/notification-types.ts +0 -3
  136. package/src/notifications/notifications-labels.ts +2 -17
  137. package/src/notifications/quiet-hours-setting.tsx +16 -11
  138. package/src/offline/local-first.ts +24 -0
  139. package/src/offline.ts +1 -1
  140. package/src/sync/db.ts +22 -0
  141. package/src/sync/engine.ts +11 -9
  142. package/src/sync/hooks.ts +5 -0
  143. package/src/sync.ts +2 -2
  144. package/src/theme/theme-store.ts +13 -5
  145. package/dist/notifications/time-zone-select.d.ts +0 -53
  146. package/dist/notifications/time-zone-select.js +0 -76
  147. package/dist/notifications/time-zone-select.js.map +0 -1
  148. package/src/notifications/time-zone-select.tsx +0 -129
@@ -5,6 +5,7 @@ import { type CommitScope } from "./write-lock.js";
5
5
  import type { IconButtonGlyphSize, IconButtonSize } from "./icon-button.js";
6
6
  import type { TooltipSide } from "./tooltip.js";
7
7
  import type { DataTableColumn } from "./data-table.js";
8
+ import { type TableHeaderCellProps } from "./table.js";
8
9
  import { DEFAULT_ROW_ACTIONS_LABELS } from "./row-actions-labels.js";
9
10
  /**
10
11
  * A row's actions — inline icons at Normal, one "⋯" menu at Large and above
@@ -183,6 +184,30 @@ export interface RowActionsProps {
183
184
  * dialog then returns it.
184
185
  */
185
186
  export declare function RowActions({ actions, name, collapse, size, menuWidth, className, glyphSize, tooltipSide, labels: labelsProp, }: RowActionsProps): import("react").JSX.Element | null;
187
+ /** The actions cell's classes — `rowActionsColumn`'s `className`: the column hugs its
188
+ * buttons, on one line, at the row's end. A hand-built table's `<TableCell>` takes them
189
+ * around its {@link RowActions} (0.47, keksdose N17). */
190
+ export declare const ROW_ACTIONS_CELL_CLASS = "w-px whitespace-nowrap text-end";
191
+ /** The actions heading's classes — `rowActionsColumn`'s `headClassName`, which
192
+ * {@link RowActionsHead} draws: hugging, end-aligned over the buttons (0.47, N17). */
193
+ export declare const ROW_ACTIONS_HEAD_CLASS = "w-px text-end";
194
+ export interface RowActionsHeadProps extends Omit<TableHeaderCellProps, "children"> {
195
+ /** The heading. Default: the provider's `rowActions.actions` ("Actions"). */
196
+ children?: ReactNode;
197
+ /** Named for a screen reader only — `rowActionsColumn`'s `header: false`. */
198
+ hideLabel?: boolean;
199
+ labels?: LabelOverride<RowActionsLabels>;
200
+ }
201
+ /**
202
+ * A hand-built table's actions heading (0.47, keksdose N17): the `<th>` `rowActionsColumn`
203
+ * gives a DataTable — "Actions" on screen in the provider's words, end-aligned over the
204
+ * buttons, hugging them. Pair it with `<TableCell className={ROW_ACTIONS_CELL_CLASS}>`
205
+ * around each row's {@link RowActions}. Every other `TableHeaderCell` prop passes on
206
+ * (`size`, the app's `className`, merged last).
207
+ *
208
+ * <RowActionsHead size="sm" className={TH} />
209
+ */
210
+ export declare function RowActionsHead({ children, hideLabel, labels, className, ...rest }: RowActionsHeadProps): import("react").JSX.Element;
186
211
  export interface RowActionsColumnOptions<T> {
187
212
  /** The column's key. Default `"actions"`. */
188
213
  key?: string;
@@ -201,10 +226,17 @@ export interface RowActionsColumnOptions<T> {
201
226
  /**
202
227
  * The column's name as plain text — the column-settings checklist and the phone
203
228
  * card's label read it (`DataTableColumn.headerText`). Pass the app's own word; left
204
- * out, the header shows the provider's `rowActions.actions` to a screen reader and
205
- * the plain-text places fall back to the column's `key`.
229
+ * out, the heading shows the provider's `rowActions.actions` and the plain-text
230
+ * places fall back to the column's `key`.
206
231
  */
207
232
  headerText?: string;
233
+ /**
234
+ * The heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE "Actions" heading on
235
+ * every table with an actions column). Default: `headerText`, else the provider's
236
+ * `rowActions.actions` ("Actions"), shown, end-aligned over the buttons. A node
237
+ * replaces it; `false` keeps the pre-0.46.1 heading, named for a screen reader only.
238
+ */
239
+ header?: ReactNode | false;
208
240
  labels?: LabelOverride<RowActionsLabels>;
209
241
  /** Anything else for the column (`mobileHidden`, `cellProps`…), merged last. */
210
242
  column?: Partial<DataTableColumn<T>>;
@@ -215,10 +247,10 @@ export interface RowActionsColumnOptions<T> {
215
247
  *
216
248
  * columns={[...columns, rowActionsColumn({ actions: (r) => [...], name: (r) => r.name })]}
217
249
  *
218
- * The header is visually hidden (the icons say what the column is), the column hugs its
219
- * content at the row's end, and `noRowLink` keeps a `rowHref` anchor off it, so the
250
+ * The heading says "Actions" on screen (0.46.1; `header` changes or hides it), the
251
+ * column hugs its content at the row's end, and `noRowLink` keeps a `rowHref` anchor off it, so the
220
252
  * buttons keep their own clicks. On a phone it is a line of the card like any column;
221
253
  * set `column: { mobileHidden: true }` to drop it there (with `mobileDetailsInRow` it
222
254
  * then shows in the opened row instead).
223
255
  */
224
- export declare function rowActionsColumn<T>({ key, actions, name, collapse, size, glyphSize, tooltipSide, headerText, labels, column, }: RowActionsColumnOptions<T>): DataTableColumn<T>;
256
+ export declare function rowActionsColumn<T>({ key, actions, name, collapse, size, glyphSize, tooltipSide, headerText, header, labels, column, }: RowActionsColumnOptions<T>): DataTableColumn<T>;
@@ -10,6 +10,7 @@ import { Button } from "./button.js";
10
10
  import { CompactControls } from "./field-parts.js";
11
11
  import { IconButton } from "./icon-button.js";
12
12
  import { useCommitReason } from "./write-lock.js";
13
+ import { TableHeaderCell } from "./table.js";
13
14
  import { DEFAULT_ROW_ACTIONS_LABELS } from "./row-actions-labels.js";
14
15
  function visibleActions(actions) {
15
16
  return actions.filter((a) => Boolean(a) && !a.hidden);
@@ -276,9 +277,19 @@ function InlineAction({
276
277
  }
277
278
  );
278
279
  }
279
- function RowActionsHeader({ text, labels }) {
280
+ function RowActionsHeader({
281
+ text,
282
+ labels,
283
+ hidden
284
+ }) {
280
285
  const resolved = useKitLabels("rowActions", DEFAULT_ROW_ACTIONS_LABELS, labels);
281
- return /* @__PURE__ */ jsx("span", { className: "sr-only", children: text ?? resolved.actions });
286
+ const word = text ?? resolved.actions;
287
+ return hidden ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: word }) : /* @__PURE__ */ jsx(Fragment, { children: word });
288
+ }
289
+ const ROW_ACTIONS_CELL_CLASS = "w-px whitespace-nowrap text-end";
290
+ const ROW_ACTIONS_HEAD_CLASS = "w-px text-end";
291
+ function RowActionsHead({ children, hideLabel, labels, className, ...rest }) {
292
+ return /* @__PURE__ */ jsx(TableHeaderCell, { align: "end", ...rest, className: cn(ROW_ACTIONS_HEAD_CLASS, className), children: typeof children === "string" || children == null ? /* @__PURE__ */ jsx(RowActionsHeader, { text: children ?? void 0, labels, hidden: hideLabel }) : children });
282
293
  }
283
294
  function rowActionsColumn({
284
295
  key = "actions",
@@ -289,12 +300,13 @@ function rowActionsColumn({
289
300
  glyphSize,
290
301
  tooltipSide,
291
302
  headerText,
303
+ header,
292
304
  labels,
293
305
  column
294
306
  }) {
295
307
  return {
296
308
  key,
297
- header: /* @__PURE__ */ jsx(RowActionsHeader, { text: headerText, labels }),
309
+ header: header === false || header == null ? /* @__PURE__ */ jsx(RowActionsHeader, { text: headerText, labels, hidden: header === false }) : header,
298
310
  headerText,
299
311
  cell: (row) => /* @__PURE__ */ jsx(
300
312
  RowActions,
@@ -308,15 +320,20 @@ function rowActionsColumn({
308
320
  labels
309
321
  }
310
322
  ),
311
- className: "w-px whitespace-nowrap text-end",
312
- headClassName: "w-px",
323
+ className: ROW_ACTIONS_CELL_CLASS,
324
+ // The heading over the buttons, at the row's end (0.46.1); a hand-built table's
325
+ // `RowActionsHead` (0.47).
326
+ headClassName: ROW_ACTIONS_HEAD_CLASS,
313
327
  noRowLink: true,
314
328
  ...column
315
329
  };
316
330
  }
317
331
  export {
318
332
  DEFAULT_ROW_ACTIONS_LABELS,
333
+ ROW_ACTIONS_CELL_CLASS,
334
+ ROW_ACTIONS_HEAD_CLASS,
319
335
  RowActions,
336
+ RowActionsHead,
320
337
  rowActionsColumn
321
338
  };
322
339
  //# sourceMappingURL=row-actions.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/row-actions.tsx"],"sourcesContent":["import { isValidElement } from \"react\";\nimport type { KeyboardEvent, MouseEvent, ReactElement, ReactNode } from \"react\";\nimport { MoreHorizontal } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useLargeText } from \"../hooks/use-large-text\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { Popover } from \"./popover\";\nimport { Button } from \"./button\";\nimport { CompactControls } from \"./field-parts\";\nimport { IconButton } from \"./icon-button\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport type { IconButtonGlyphSize, IconButtonSize } from \"./icon-button\";\nimport type { TooltipSide } from \"./tooltip\";\nimport type { DataTableColumn } from \"./data-table\";\nimport { DEFAULT_ROW_ACTIONS_LABELS } from \"./row-actions-labels\";\n\n/**\n * A row's actions — inline icons at Normal, one \"⋯\" menu at Large and above\n * (docs/text-size-harmonization.md §10.8).\n *\n * At Large an IconButton shows its label as text (§4). In a dense row — a register's\n * action column, a list row's end — every label at once does not fit: Kurvenschmiede\n * has 44 IconButtons in rows, kastlan 119. So a row's actions collapse instead, into\n * the menu the user roster has used since 0.31 (`UserRowActions`, now built on this):\n * one \"⋯\" button, and a menu with each action's icon AND words, where a locked one stays\n * listed and says why.\n *\n * The data is the roster's `UserRowActionList`, generalised: entries, and `false` /\n * `null` for the ones a condition left out (`row.locked || {…}`).\n *\n * 0.33 (§10.16): an action carries its state at both sizes — a toggle (`pressed`), a\n * disclosure (`expanded`, `controls`), a tone, a tour anchor (`dataTour`) — so a row\n * with an \"on\" share button or a tour step no longer keeps hand-rolled IconButtons at\n * Normal and this only at Large (keksdose k15).\n */\n\n/** An action's colour: IconButton's tone inline; in the menu the glyph takes it and the\n * words keep the text colour, except `danger`, whose words are the danger text. */\nexport type RowActionTone = \"default\" | \"muted\" | \"info\" | \"warning\" | \"danger\";\n\n/** One action of a row. */\nexport interface RowAction {\n /** React key. Default: the label. */\n key?: string;\n /** The action's name: the inline icon's name and tooltip, the menu entry's words. */\n label: string;\n /**\n * Runs it. The kit sends nothing — the app opens its confirmation or calls its API.\n * Optional only beside `href`; an action has one or the other (or both: `onSelect`\n * then runs as the link is followed).\n */\n onSelect?: () => void;\n /**\n * The action NAVIGATES — an edit page, a mail to compose (0.32.1, kastlan's 0.32\n * report): a real link inline and in the menu, so open in a new tab, a middle click\n * and the URL on hover work, which `onSelect` + `navigate` lost. The kit's link rule\n * applies ({@link Button}'s `href`): in-app through the provider's `linkComponent`, an\n * external or `#anchor` one a plain `<a>`, `mailto:` included.\n *\n * A link cannot carry a reason or a spinner, so an action with a `disabledReason`\n * (or under a write lock, with `commit`) or `pending` stays a button and says why.\n */\n href?: string;\n /** With `href`: leaves the app — a new tab, and a screen reader is told so. */\n external?: boolean;\n /**\n * The glyph: a Lucide icon (`Pencil`), or an element (`<Pencil />`, an app's own\n * svg) — DataTable's `rowActions` hand their `icon` node through. Inline, an action\n * without one is a small text button; in the menu it is the words alone.\n */\n icon?: LucideIcon | ReactElement;\n /**\n * `danger` for delete, revoke, erase: the quiet red of IconButton's `tone=\"danger\"`\n * inline, the danger text tone in the menu. `muted` (an open or add that should not\n * compete), `info` (a notice-worthy, harmless action: keksdose's reconcile) and\n * `warning` (a flag that wants attention): IconButton's tone inline; in the menu the\n * glyph takes it and the words keep the text colour, so `muted` looks like `default`\n * there (0.33).\n */\n tone?: RowActionTone;\n /** Why it is not available for this row. It stays listed, focusable, and says so — an\n * action that silently vanished would leave the reader looking for it. */\n disabledReason?: ReactNode;\n /**\n * This action COMMITS — it saves, sends, deletes: under a locked\n * {@link WriteLockProvider} it is refused with the lock's reason, as {@link Button}'s\n * `commit` is, inline and in the menu alike. No provider, or an unlocked one: no effect.\n */\n commit?: CommitScope;\n /**\n * In flight — the request it started has not answered. Inline, the icon turns into\n * IconButton's spinner (`pending`); collapsed, the \"⋯\" button does, since the menu\n * closed when the action was chosen. Either way nothing in the row can be pressed again\n * until it settles: the \"⋯\" opens no menu while busy.\n */\n pending?: boolean;\n /**\n * Not available for a moment, with nothing to explain — another action of the list\n * is in flight (an admin panel's `busy`). The plain `disabled`; a lasting refusal is a\n * `disabledReason`, which says why.\n */\n disabled?: boolean;\n /** Leave it out for this row. */\n hidden?: boolean;\n /**\n * A toggle (0.33): `aria-pressed` and IconButton's \"on\" look inline, `aria-pressed` on\n * the menu entry. Keep the label the same in both states (\"Share budget\"): the pressed\n * state already says whether it is on. Not on an `href` action — a link is not a\n * toggle — nor with `expanded`.\n */\n pressed?: boolean;\n /**\n * Opens or closes something IN THE PAGE (0.33) — a share card under the row, an\n * add-category row: `aria-expanded`, inline and on the menu entry. Not with `pressed`:\n * a disclosure says open, not on. Not on an `href` action.\n */\n expanded?: boolean;\n /** The `id` of what `expanded` opens: `aria-controls`, inline and on the menu entry. */\n controls?: string;\n /**\n * A tour anchor (0.33, §10.18): `data-tour` on ONE element per action at a time.\n * Inline, on the action's own control (the IconButton, the small text button, the\n * link). Collapsed, on an `aria-hidden` box laid over the \"⋯\" button — `[data-tour=x]`\n * resolves to a box the size of the \"⋯\", whose `data-slot=\"row-actions\"` box holds the\n * button, two anchored actions in one row don't fight over one attribute, and a tap\n * still reaches the button. Never on the entry in the open menu: the kit's tour takes\n * the first visible match and the menu is portalled after the row, so a copy there\n * would never be found, only make the selector match twice. The kit's name for it, as\n * `AppShellNavItem.dataTour`.\n */\n dataTour?: string;\n}\n\n/** What a row's actions may be: entries, and `false` / `null` / `undefined` for the ones\n * a condition left out. */\nexport type RowActionList = readonly (RowAction | false | null | undefined)[];\n\nexport interface RowActionsLabels {\n /** The menu button's name when the row has no name of its own; also the actions\n * column's (hidden) header in {@link rowActionsColumn}. */\n actions: string;\n /** The menu button's name, given the row's: \"Actions for Ada Example\". */\n actionsFor: (name: string) => string;\n}\n\n// Defined in ./row-actions-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ROW_ACTIONS_LABELS };\n\n/**\n * - `\"auto\"` (default): the icons inline at Normal; the \"⋯\" menu at Large and Extra\n * large once there are two or more (§10.8). A single action stays inline at every\n * size, with its label as text at Large — one label fits a row, and a menu of one is\n * a detour.\n * - `\"menu\"`: the menu at every size — the roster's choice, whose actions grow (§2.4\n * there) and do not fit a phone card as a strip.\n * - `\"inline\"`: the icons at every size — for a row the app knows has room; at Large\n * each then shows its label (IconButton's `labelVisible`).\n */\nexport type RowActionsCollapse = \"auto\" | \"menu\" | \"inline\";\n\n/** The sizes a row action comes in: IconButton's row steps. `2xs` is DataTable's\n * compact density. At Large every one is at least a 48 px target (IconButton). */\nexport type RowActionsSize = Extract<IconButtonSize, \"md\" | \"sm\" | \"xs\" | \"2xs\">;\n\nexport interface RowActionsProps {\n actions: RowActionList;\n /** The row's name, for the menu button's name — \"Actions for Ada Example\". Without it\n * (or `null`, as API fields often are) the button is \"Actions\". */\n name?: string | null;\n /** When the actions collapse into the menu — see {@link RowActionsCollapse}. */\n collapse?: RowActionsCollapse;\n /** The inline icons' and the \"⋯\" button's size. Default `xs`, a list row's action. */\n size?: RowActionsSize;\n /** The menu's width. Default `\"15rem\"`, in rem so it grows with the words (§3.2). */\n menuWidth?: `${number}rem`;\n /** On the wrapper: the inline strip, or the box round the \"⋯\" button. */\n className?: string;\n /** The inline glyphs' size, IconButton's `glyphSize` — e.g. 14 in a dense header strip\n * (keksdose's category group header). The \"⋯\" button keeps its box's own. */\n glyphSize?: IconButtonGlyphSize;\n /** The inline tooltips' side, IconButton's `tooltipSide` — `\"start\"` for a table's last\n * column, so a bubble does not run off the row's end. Per row: every inline action. */\n tooltipSide?: TooltipSide;\n labels?: LabelOverride<RowActionsLabels>;\n}\n\nfunction visibleActions(actions: RowActionList): RowAction[] {\n return actions.filter((a): a is RowAction => Boolean(a) && !(a as RowAction).hidden);\n}\n\n/** An action's glyph as a node: a component is rendered, an element is used as is. */\nfunction glyphOf(icon: RowAction[\"icon\"]): ReactNode {\n if (icon === undefined) return null;\n if (isValidElement(icon)) return icon;\n const Icon = icon as LucideIcon;\n return <Icon />;\n}\n\n/** The menu glyph's tint per tone (0.33): the words keep the text colour, so the glyph\n * carries `info` and `warning` — IconButton's toned colours. `danger` colours the words\n * through Button's tone, and `muted` is the default look in a menu. */\nconst MENU_GLYPH_TONE: Partial<Record<RowActionTone, string>> = {\n info: \"text-info\",\n warning: \"text-warning\",\n};\n\n/** The state an action's control says, inline and in the menu alike: `aria-expanded` and\n * `aria-controls` for a disclosure (a toggle's `aria-pressed` goes through the button's\n * own `pressed`, which also draws the \"on\" look). */\nfunction stateAttrs(action: RowAction) {\n return {\n \"aria-expanded\": action.expanded,\n \"aria-controls\": action.expanded === undefined ? undefined : action.controls,\n };\n}\n\n/** An action's tone on its inline control, for a caller's CSS and a test: `data-tone`,\n * left off for the default look. */\nfunction toneAttr(tone: RowActionTone | undefined) {\n return { \"data-tone\": tone === undefined || tone === \"default\" ? undefined : tone };\n}\n\n/**\n * A row's actions, inline or as a \"⋯\" menu — see {@link RowActionsCollapse}. Every\n * control stops its click (and the Enter/Space it stands for) from reaching the row, so\n * a clickable row or a DataTable `rowLink` does not open as well; in a DataTable column\n * also set `noRowLink: true`, as {@link rowActionsColumn} does.\n *\n * Inline, the icons sit in a {@link CompactControls} region at Normal: their labels\n * stay the names and the tooltips, and a locked one keeps its reason in the tooltip — a\n * line under one icon of a row would break the row. In the menu a locked action's\n * reason is a line under its words at Large and on touch (Button's\n * `disabledReasonDisplay`).\n *\n * The menu is portalled, so a table's scroller cannot clip it; Escape and an outside\n * press close it, and focus goes back to the \"⋯\" button, which is where a confirmation\n * dialog then returns it.\n */\nexport function RowActions({\n actions,\n name,\n collapse = \"auto\",\n size = \"xs\",\n menuWidth = \"15rem\",\n className,\n glyphSize,\n tooltipSide,\n labels: labelsProp,\n}: RowActionsProps) {\n const labels = useKitLabels(\"rowActions\", DEFAULT_ROW_ACTIONS_LABELS, labelsProp);\n const large = useLargeText();\n const entries = visibleActions(actions);\n if (entries.length === 0) return null;\n const asMenu = collapse === \"menu\" || (collapse === \"auto\" && large && entries.length > 1);\n if (!asMenu) {\n const strip = (\n <div data-slot=\"row-actions\" className={cn(\"inline-flex items-center gap-0.5\", className)}>\n {entries.map((action) => (\n <InlineAction\n key={action.key ?? action.label}\n action={action}\n size={size}\n glyphSize={glyphSize}\n tooltipSide={tooltipSide}\n />\n ))}\n </div>\n );\n // Compact at Normal (no room for a reason line in a row); at Large an inline action\n // is the app's choice (\"inline\") or the row's only one, and shows its words.\n return large ? strip : <CompactControls>{strip}</CompactControls>;\n }\n const title = name ? labels.actionsFor(name) : labels.actions;\n // The menu closed when the action was chosen, so what is in flight shows on \"⋯\".\n const busy = entries.some((action) => action.pending);\n const anchored = entries.filter((action) => action.dataTour);\n return (\n // The \"⋯\" in a box of its own, the inline strip's slot: the tour anchors of the\n // collapsed actions lie over the button in it (see `dataTour`), and `className`\n // places the box as it places the strip.\n <div data-slot=\"row-actions\" className={cn(\"relative inline-flex\", className)}>\n <Popover\n width={menuWidth}\n aria-label={title}\n className=\"p-1\"\n trigger={({ open, toggle, ref }) => (\n <IconButton\n ref={ref}\n size={size}\n label={title}\n // \"⋯\" is read by everyone as \"more actions\"; the menu it opens is the words.\n labelVisible={false}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n pending={busy}\n stopPropagation\n onClick={toggle}\n >\n <MoreHorizontal />\n </IconButton>\n )}\n // React carries an event out of a portal along the COMPONENT tree, so a click in\n // the menu reached a clickable row or a DataTable `onRowClick` the menu was opened\n // from, though the panel is nowhere near it in the page — only the \"⋯\" trigger\n // stopped its own (keksdose's 0.32 report). Stopped on the PANEL itself, its padding\n // and border included (0.32.2: 0.32.1 stopped it on an inner box, and a click on\n // the panel's `p-1` still went through): clicks, and the Enter / Space a row also\n // listens for. Escape goes on, to the popover's own handler that closes it.\n onClick={stopClick}\n onKeyDown={stopActivationKeys}\n >\n {(close) => (\n <ul data-slot=\"row-actions-menu\" className=\"space-y-0.5\">\n {entries.map((action) => (\n <li key={action.key ?? action.label}>\n <MenuAction action={action} close={close} />\n </li>\n ))}\n </ul>\n )}\n </Popover>\n {anchored.map((action) => (\n // The More cell's TourAnchor: decoration over the button, which keeps every tap.\n <span\n key={action.key ?? action.label}\n aria-hidden\n data-tour={action.dataTour}\n className=\"pointer-events-none absolute inset-0\"\n />\n ))}\n </div>\n );\n}\n\nfunction stopClick(e: MouseEvent) {\n e.stopPropagation();\n}\n\nfunction stopActivationKeys(e: KeyboardEvent) {\n if (e.key === \"Enter\" || e.key === \" \") e.stopPropagation();\n}\n\n/**\n * The `href` an action renders as a link with, or `undefined` for a button: a link\n * cannot carry a reason or a spinner (Button's link form has neither), so a refused\n * action — its own `disabledReason`, or a write lock on a `commit` — and a pending one\n * stay buttons and say so.\n */\nfunction useActionHref(action: RowAction): string | undefined {\n const reason = useCommitReason(action.commit, action.disabledReason);\n const refused = reason !== undefined && reason !== null && reason !== false && reason !== \"\";\n return action.href !== undefined && !action.pending && !refused ? action.href : undefined;\n}\n\n/** One entry of the \"⋯\" menu: the words, as a link for an `href`, else a button. */\nfunction MenuAction({ action, close }: { action: RowAction; close: () => void }) {\n const href = useActionHref(action);\n const glyph = glyphOf(action.icon);\n const glyphTone = action.tone === undefined ? undefined : MENU_GLYPH_TONE[action.tone];\n const words = (\n <>\n {glyph && (\n <span\n aria-hidden\n data-tone={glyphTone ? action.tone : undefined}\n className={cn(\"flex shrink-0 [&_svg]:size-4\", glyphTone)}\n >\n {glyph}\n </span>\n )}\n {action.label}\n </>\n );\n const tone = action.tone === \"danger\" ? \"danger\" : undefined;\n const select = () => {\n close();\n action.onSelect?.();\n };\n if (href !== undefined) {\n return (\n <Button\n href={href}\n external={action.external}\n variant=\"ghost\"\n size=\"sm\"\n stretch\n tone={tone}\n disabled={action.disabled}\n className=\"w-full justify-start text-start\"\n onClick={select}\n >\n {words}\n </Button>\n );\n }\n return (\n <Button\n {...stateAttrs(action)}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n stretch\n tone={tone}\n pressed={action.pressed}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n className=\"w-full justify-start text-start\"\n onClick={select}\n >\n {words}\n </Button>\n );\n}\n\n/** One inline action: an IconButton with its icon, else a small text button — each a\n * link for an `href` (see {@link useActionHref}). The control carries the action's\n * state, its tone and its tour anchor. */\nfunction InlineAction({\n action,\n size,\n glyphSize,\n tooltipSide,\n}: {\n action: RowAction;\n size: RowActionsSize;\n glyphSize?: IconButtonGlyphSize;\n tooltipSide?: TooltipSide;\n}) {\n const href = useActionHref(action);\n const glyph = glyphOf(action.icon);\n const iconTone = action.tone === \"default\" ? undefined : action.tone;\n // A text button has Button's two tones; `info` and `warning` are a glyph's colours.\n const textTone = action.tone === \"danger\" || action.tone === \"muted\" ? action.tone : undefined;\n const marks = { \"data-tour\": action.dataTour, ...toneAttr(action.tone) };\n if (glyph) {\n if (href !== undefined) {\n return (\n <IconButton\n {...marks}\n href={href}\n external={action.external}\n size={size}\n glyphSize={glyphSize}\n label={action.label}\n tooltipSide={tooltipSide}\n tone={iconTone}\n disabled={action.disabled}\n stopPropagation\n onClick={() => action.onSelect?.()}\n >\n {glyph}\n </IconButton>\n );\n }\n return (\n <IconButton\n {...marks}\n {...stateAttrs(action)}\n size={size}\n glyphSize={glyphSize}\n label={action.label}\n tooltipSide={tooltipSide}\n tone={iconTone}\n pressed={action.pressed}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n stopPropagation\n onClick={() => action.onSelect?.()}\n >\n {glyph}\n </IconButton>\n );\n }\n // A row listening for clicks must not open as well (IconButton's stopPropagation).\n const stop = {\n onClick: (e: MouseEvent) => {\n e.stopPropagation();\n action.onSelect?.();\n },\n onKeyDown: (e: KeyboardEvent) => {\n if (e.key === \"Enter\" || e.key === \" \") e.stopPropagation();\n },\n };\n if (href !== undefined) {\n return (\n <Button\n {...marks}\n href={href}\n external={action.external}\n variant=\"ghost\"\n size=\"sm\"\n tone={textTone}\n disabled={action.disabled}\n {...stop}\n >\n {action.label}\n </Button>\n );\n }\n return (\n <Button\n {...marks}\n {...stateAttrs(action)}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n tone={textTone}\n pressed={action.pressed}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n {...stop}\n >\n {action.label}\n </Button>\n );\n}\n\n/** The actions column's hidden header, in the provider's words. */\nfunction RowActionsHeader({ text, labels }: { text?: string; labels?: LabelOverride<RowActionsLabels> }) {\n const resolved = useKitLabels(\"rowActions\", DEFAULT_ROW_ACTIONS_LABELS, labels);\n return <span className=\"sr-only\">{text ?? resolved.actions}</span>;\n}\n\nexport interface RowActionsColumnOptions<T> {\n /** The column's key. Default `\"actions\"`. */\n key?: string;\n /** The row's actions — `false` / `null` for the ones a condition leaves out. */\n actions: (row: T) => RowActionList;\n /** The row's name, for the \"⋯\" button's name (\"Actions for Ada Example\"). `null` and\n * `undefined` both mean \"Actions\". */\n name?: (row: T) => string | null | undefined;\n collapse?: RowActionsCollapse;\n /** Default `xs`. */\n size?: RowActionsSize;\n /** See {@link RowActionsProps.glyphSize}. */\n glyphSize?: IconButtonGlyphSize;\n /** See {@link RowActionsProps.tooltipSide} — `\"start\"` in a table's last column. */\n tooltipSide?: TooltipSide;\n /**\n * The column's name as plain text — the column-settings checklist and the phone\n * card's label read it (`DataTableColumn.headerText`). Pass the app's own word; left\n * out, the header shows the provider's `rowActions.actions` to a screen reader and\n * the plain-text places fall back to the column's `key`.\n */\n headerText?: string;\n labels?: LabelOverride<RowActionsLabels>;\n /** Anything else for the column (`mobileHidden`, `cellProps`…), merged last. */\n column?: Partial<DataTableColumn<T>>;\n}\n\n/**\n * A DataTable column of {@link RowActions} — an app's action column in one line\n * (§10.8): inline icons at Normal, the \"⋯\" menu at Large with two or more actions.\n *\n * columns={[...columns, rowActionsColumn({ actions: (r) => [...], name: (r) => r.name })]}\n *\n * The header is visually hidden (the icons say what the column is), the column hugs its\n * content at the row's end, and `noRowLink` keeps a `rowHref` anchor off it, so the\n * buttons keep their own clicks. On a phone it is a line of the card like any column;\n * set `column: { mobileHidden: true }` to drop it there (with `mobileDetailsInRow` it\n * then shows in the opened row instead).\n */\nexport function rowActionsColumn<T>({\n key = \"actions\",\n actions,\n name,\n collapse,\n size,\n glyphSize,\n tooltipSide,\n headerText,\n labels,\n column,\n}: RowActionsColumnOptions<T>): DataTableColumn<T> {\n return {\n key,\n header: <RowActionsHeader text={headerText} labels={labels} />,\n headerText,\n cell: (row) => (\n <RowActions\n actions={actions(row)}\n name={name?.(row)}\n collapse={collapse}\n size={size}\n glyphSize={glyphSize}\n tooltipSide={tooltipSide}\n labels={labels}\n />\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px\",\n noRowLink: true,\n ...column,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/row-actions.tsx"],"sourcesContent":["import { isValidElement } from \"react\";\nimport type { KeyboardEvent, MouseEvent, ReactElement, ReactNode } from \"react\";\nimport { MoreHorizontal } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useLargeText } from \"../hooks/use-large-text\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { Popover } from \"./popover\";\nimport { Button } from \"./button\";\nimport { CompactControls } from \"./field-parts\";\nimport { IconButton } from \"./icon-button\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport type { IconButtonGlyphSize, IconButtonSize } from \"./icon-button\";\nimport type { TooltipSide } from \"./tooltip\";\nimport type { DataTableColumn } from \"./data-table\";\nimport { TableHeaderCell, type TableHeaderCellProps } from \"./table\";\nimport { DEFAULT_ROW_ACTIONS_LABELS } from \"./row-actions-labels\";\n\n/**\n * A row's actions — inline icons at Normal, one \"⋯\" menu at Large and above\n * (docs/text-size-harmonization.md §10.8).\n *\n * At Large an IconButton shows its label as text (§4). In a dense row — a register's\n * action column, a list row's end — every label at once does not fit: Kurvenschmiede\n * has 44 IconButtons in rows, kastlan 119. So a row's actions collapse instead, into\n * the menu the user roster has used since 0.31 (`UserRowActions`, now built on this):\n * one \"⋯\" button, and a menu with each action's icon AND words, where a locked one stays\n * listed and says why.\n *\n * The data is the roster's `UserRowActionList`, generalised: entries, and `false` /\n * `null` for the ones a condition left out (`row.locked || {…}`).\n *\n * 0.33 (§10.16): an action carries its state at both sizes — a toggle (`pressed`), a\n * disclosure (`expanded`, `controls`), a tone, a tour anchor (`dataTour`) — so a row\n * with an \"on\" share button or a tour step no longer keeps hand-rolled IconButtons at\n * Normal and this only at Large (keksdose k15).\n */\n\n/** An action's colour: IconButton's tone inline; in the menu the glyph takes it and the\n * words keep the text colour, except `danger`, whose words are the danger text. */\nexport type RowActionTone = \"default\" | \"muted\" | \"info\" | \"warning\" | \"danger\";\n\n/** One action of a row. */\nexport interface RowAction {\n /** React key. Default: the label. */\n key?: string;\n /** The action's name: the inline icon's name and tooltip, the menu entry's words. */\n label: string;\n /**\n * Runs it. The kit sends nothing — the app opens its confirmation or calls its API.\n * Optional only beside `href`; an action has one or the other (or both: `onSelect`\n * then runs as the link is followed).\n */\n onSelect?: () => void;\n /**\n * The action NAVIGATES — an edit page, a mail to compose (0.32.1, kastlan's 0.32\n * report): a real link inline and in the menu, so open in a new tab, a middle click\n * and the URL on hover work, which `onSelect` + `navigate` lost. The kit's link rule\n * applies ({@link Button}'s `href`): in-app through the provider's `linkComponent`, an\n * external or `#anchor` one a plain `<a>`, `mailto:` included.\n *\n * A link cannot carry a reason or a spinner, so an action with a `disabledReason`\n * (or under a write lock, with `commit`) or `pending` stays a button and says why.\n */\n href?: string;\n /** With `href`: leaves the app — a new tab, and a screen reader is told so. */\n external?: boolean;\n /**\n * The glyph: a Lucide icon (`Pencil`), or an element (`<Pencil />`, an app's own\n * svg) — DataTable's `rowActions` hand their `icon` node through. Inline, an action\n * without one is a small text button; in the menu it is the words alone.\n */\n icon?: LucideIcon | ReactElement;\n /**\n * `danger` for delete, revoke, erase: the quiet red of IconButton's `tone=\"danger\"`\n * inline, the danger text tone in the menu. `muted` (an open or add that should not\n * compete), `info` (a notice-worthy, harmless action: keksdose's reconcile) and\n * `warning` (a flag that wants attention): IconButton's tone inline; in the menu the\n * glyph takes it and the words keep the text colour, so `muted` looks like `default`\n * there (0.33).\n */\n tone?: RowActionTone;\n /** Why it is not available for this row. It stays listed, focusable, and says so — an\n * action that silently vanished would leave the reader looking for it. */\n disabledReason?: ReactNode;\n /**\n * This action COMMITS — it saves, sends, deletes: under a locked\n * {@link WriteLockProvider} it is refused with the lock's reason, as {@link Button}'s\n * `commit` is, inline and in the menu alike. No provider, or an unlocked one: no effect.\n */\n commit?: CommitScope;\n /**\n * In flight — the request it started has not answered. Inline, the icon turns into\n * IconButton's spinner (`pending`); collapsed, the \"⋯\" button does, since the menu\n * closed when the action was chosen. Either way nothing in the row can be pressed again\n * until it settles: the \"⋯\" opens no menu while busy.\n */\n pending?: boolean;\n /**\n * Not available for a moment, with nothing to explain — another action of the list\n * is in flight (an admin panel's `busy`). The plain `disabled`; a lasting refusal is a\n * `disabledReason`, which says why.\n */\n disabled?: boolean;\n /** Leave it out for this row. */\n hidden?: boolean;\n /**\n * A toggle (0.33): `aria-pressed` and IconButton's \"on\" look inline, `aria-pressed` on\n * the menu entry. Keep the label the same in both states (\"Share budget\"): the pressed\n * state already says whether it is on. Not on an `href` action — a link is not a\n * toggle — nor with `expanded`.\n */\n pressed?: boolean;\n /**\n * Opens or closes something IN THE PAGE (0.33) — a share card under the row, an\n * add-category row: `aria-expanded`, inline and on the menu entry. Not with `pressed`:\n * a disclosure says open, not on. Not on an `href` action.\n */\n expanded?: boolean;\n /** The `id` of what `expanded` opens: `aria-controls`, inline and on the menu entry. */\n controls?: string;\n /**\n * A tour anchor (0.33, §10.18): `data-tour` on ONE element per action at a time.\n * Inline, on the action's own control (the IconButton, the small text button, the\n * link). Collapsed, on an `aria-hidden` box laid over the \"⋯\" button — `[data-tour=x]`\n * resolves to a box the size of the \"⋯\", whose `data-slot=\"row-actions\"` box holds the\n * button, two anchored actions in one row don't fight over one attribute, and a tap\n * still reaches the button. Never on the entry in the open menu: the kit's tour takes\n * the first visible match and the menu is portalled after the row, so a copy there\n * would never be found, only make the selector match twice. The kit's name for it, as\n * `AppShellNavItem.dataTour`.\n */\n dataTour?: string;\n}\n\n/** What a row's actions may be: entries, and `false` / `null` / `undefined` for the ones\n * a condition left out. */\nexport type RowActionList = readonly (RowAction | false | null | undefined)[];\n\nexport interface RowActionsLabels {\n /** The menu button's name when the row has no name of its own; also the actions\n * column's (hidden) header in {@link rowActionsColumn}. */\n actions: string;\n /** The menu button's name, given the row's: \"Actions for Ada Example\". */\n actionsFor: (name: string) => string;\n}\n\n// Defined in ./row-actions-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_ROW_ACTIONS_LABELS };\n\n/**\n * - `\"auto\"` (default): the icons inline at Normal; the \"⋯\" menu at Large and Extra\n * large once there are two or more (§10.8). A single action stays inline at every\n * size, with its label as text at Large — one label fits a row, and a menu of one is\n * a detour.\n * - `\"menu\"`: the menu at every size — the roster's choice, whose actions grow (§2.4\n * there) and do not fit a phone card as a strip.\n * - `\"inline\"`: the icons at every size — for a row the app knows has room; at Large\n * each then shows its label (IconButton's `labelVisible`).\n */\nexport type RowActionsCollapse = \"auto\" | \"menu\" | \"inline\";\n\n/** The sizes a row action comes in: IconButton's row steps. `2xs` is DataTable's\n * compact density. At Large every one is at least a 48 px target (IconButton). */\nexport type RowActionsSize = Extract<IconButtonSize, \"md\" | \"sm\" | \"xs\" | \"2xs\">;\n\nexport interface RowActionsProps {\n actions: RowActionList;\n /** The row's name, for the menu button's name — \"Actions for Ada Example\". Without it\n * (or `null`, as API fields often are) the button is \"Actions\". */\n name?: string | null;\n /** When the actions collapse into the menu — see {@link RowActionsCollapse}. */\n collapse?: RowActionsCollapse;\n /** The inline icons' and the \"⋯\" button's size. Default `xs`, a list row's action. */\n size?: RowActionsSize;\n /** The menu's width. Default `\"15rem\"`, in rem so it grows with the words (§3.2). */\n menuWidth?: `${number}rem`;\n /** On the wrapper: the inline strip, or the box round the \"⋯\" button. */\n className?: string;\n /** The inline glyphs' size, IconButton's `glyphSize` — e.g. 14 in a dense header strip\n * (keksdose's category group header). The \"⋯\" button keeps its box's own. */\n glyphSize?: IconButtonGlyphSize;\n /** The inline tooltips' side, IconButton's `tooltipSide` — `\"start\"` for a table's last\n * column, so a bubble does not run off the row's end. Per row: every inline action. */\n tooltipSide?: TooltipSide;\n labels?: LabelOverride<RowActionsLabels>;\n}\n\nfunction visibleActions(actions: RowActionList): RowAction[] {\n return actions.filter((a): a is RowAction => Boolean(a) && !(a as RowAction).hidden);\n}\n\n/** An action's glyph as a node: a component is rendered, an element is used as is. */\nfunction glyphOf(icon: RowAction[\"icon\"]): ReactNode {\n if (icon === undefined) return null;\n if (isValidElement(icon)) return icon;\n const Icon = icon as LucideIcon;\n return <Icon />;\n}\n\n/** The menu glyph's tint per tone (0.33): the words keep the text colour, so the glyph\n * carries `info` and `warning` — IconButton's toned colours. `danger` colours the words\n * through Button's tone, and `muted` is the default look in a menu. */\nconst MENU_GLYPH_TONE: Partial<Record<RowActionTone, string>> = {\n info: \"text-info\",\n warning: \"text-warning\",\n};\n\n/** The state an action's control says, inline and in the menu alike: `aria-expanded` and\n * `aria-controls` for a disclosure (a toggle's `aria-pressed` goes through the button's\n * own `pressed`, which also draws the \"on\" look). */\nfunction stateAttrs(action: RowAction) {\n return {\n \"aria-expanded\": action.expanded,\n \"aria-controls\": action.expanded === undefined ? undefined : action.controls,\n };\n}\n\n/** An action's tone on its inline control, for a caller's CSS and a test: `data-tone`,\n * left off for the default look. */\nfunction toneAttr(tone: RowActionTone | undefined) {\n return { \"data-tone\": tone === undefined || tone === \"default\" ? undefined : tone };\n}\n\n/**\n * A row's actions, inline or as a \"⋯\" menu — see {@link RowActionsCollapse}. Every\n * control stops its click (and the Enter/Space it stands for) from reaching the row, so\n * a clickable row or a DataTable `rowLink` does not open as well; in a DataTable column\n * also set `noRowLink: true`, as {@link rowActionsColumn} does.\n *\n * Inline, the icons sit in a {@link CompactControls} region at Normal: their labels\n * stay the names and the tooltips, and a locked one keeps its reason in the tooltip — a\n * line under one icon of a row would break the row. In the menu a locked action's\n * reason is a line under its words at Large and on touch (Button's\n * `disabledReasonDisplay`).\n *\n * The menu is portalled, so a table's scroller cannot clip it; Escape and an outside\n * press close it, and focus goes back to the \"⋯\" button, which is where a confirmation\n * dialog then returns it.\n */\nexport function RowActions({\n actions,\n name,\n collapse = \"auto\",\n size = \"xs\",\n menuWidth = \"15rem\",\n className,\n glyphSize,\n tooltipSide,\n labels: labelsProp,\n}: RowActionsProps) {\n const labels = useKitLabels(\"rowActions\", DEFAULT_ROW_ACTIONS_LABELS, labelsProp);\n const large = useLargeText();\n const entries = visibleActions(actions);\n if (entries.length === 0) return null;\n const asMenu = collapse === \"menu\" || (collapse === \"auto\" && large && entries.length > 1);\n if (!asMenu) {\n const strip = (\n <div data-slot=\"row-actions\" className={cn(\"inline-flex items-center gap-0.5\", className)}>\n {entries.map((action) => (\n <InlineAction\n key={action.key ?? action.label}\n action={action}\n size={size}\n glyphSize={glyphSize}\n tooltipSide={tooltipSide}\n />\n ))}\n </div>\n );\n // Compact at Normal (no room for a reason line in a row); at Large an inline action\n // is the app's choice (\"inline\") or the row's only one, and shows its words.\n return large ? strip : <CompactControls>{strip}</CompactControls>;\n }\n const title = name ? labels.actionsFor(name) : labels.actions;\n // The menu closed when the action was chosen, so what is in flight shows on \"⋯\".\n const busy = entries.some((action) => action.pending);\n const anchored = entries.filter((action) => action.dataTour);\n return (\n // The \"⋯\" in a box of its own, the inline strip's slot: the tour anchors of the\n // collapsed actions lie over the button in it (see `dataTour`), and `className`\n // places the box as it places the strip.\n <div data-slot=\"row-actions\" className={cn(\"relative inline-flex\", className)}>\n <Popover\n width={menuWidth}\n aria-label={title}\n className=\"p-1\"\n trigger={({ open, toggle, ref }) => (\n <IconButton\n ref={ref}\n size={size}\n label={title}\n // \"⋯\" is read by everyone as \"more actions\"; the menu it opens is the words.\n labelVisible={false}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n pending={busy}\n stopPropagation\n onClick={toggle}\n >\n <MoreHorizontal />\n </IconButton>\n )}\n // React carries an event out of a portal along the COMPONENT tree, so a click in\n // the menu reached a clickable row or a DataTable `onRowClick` the menu was opened\n // from, though the panel is nowhere near it in the page — only the \"⋯\" trigger\n // stopped its own (keksdose's 0.32 report). Stopped on the PANEL itself, its padding\n // and border included (0.32.2: 0.32.1 stopped it on an inner box, and a click on\n // the panel's `p-1` still went through): clicks, and the Enter / Space a row also\n // listens for. Escape goes on, to the popover's own handler that closes it.\n onClick={stopClick}\n onKeyDown={stopActivationKeys}\n >\n {(close) => (\n <ul data-slot=\"row-actions-menu\" className=\"space-y-0.5\">\n {entries.map((action) => (\n <li key={action.key ?? action.label}>\n <MenuAction action={action} close={close} />\n </li>\n ))}\n </ul>\n )}\n </Popover>\n {anchored.map((action) => (\n // The More cell's TourAnchor: decoration over the button, which keeps every tap.\n <span\n key={action.key ?? action.label}\n aria-hidden\n data-tour={action.dataTour}\n className=\"pointer-events-none absolute inset-0\"\n />\n ))}\n </div>\n );\n}\n\nfunction stopClick(e: MouseEvent) {\n e.stopPropagation();\n}\n\nfunction stopActivationKeys(e: KeyboardEvent) {\n if (e.key === \"Enter\" || e.key === \" \") e.stopPropagation();\n}\n\n/**\n * The `href` an action renders as a link with, or `undefined` for a button: a link\n * cannot carry a reason or a spinner (Button's link form has neither), so a refused\n * action — its own `disabledReason`, or a write lock on a `commit` — and a pending one\n * stay buttons and say so.\n */\nfunction useActionHref(action: RowAction): string | undefined {\n const reason = useCommitReason(action.commit, action.disabledReason);\n const refused = reason !== undefined && reason !== null && reason !== false && reason !== \"\";\n return action.href !== undefined && !action.pending && !refused ? action.href : undefined;\n}\n\n/** One entry of the \"⋯\" menu: the words, as a link for an `href`, else a button. */\nfunction MenuAction({ action, close }: { action: RowAction; close: () => void }) {\n const href = useActionHref(action);\n const glyph = glyphOf(action.icon);\n const glyphTone = action.tone === undefined ? undefined : MENU_GLYPH_TONE[action.tone];\n const words = (\n <>\n {glyph && (\n <span\n aria-hidden\n data-tone={glyphTone ? action.tone : undefined}\n className={cn(\"flex shrink-0 [&_svg]:size-4\", glyphTone)}\n >\n {glyph}\n </span>\n )}\n {action.label}\n </>\n );\n const tone = action.tone === \"danger\" ? \"danger\" : undefined;\n const select = () => {\n close();\n action.onSelect?.();\n };\n if (href !== undefined) {\n return (\n <Button\n href={href}\n external={action.external}\n variant=\"ghost\"\n size=\"sm\"\n stretch\n tone={tone}\n disabled={action.disabled}\n className=\"w-full justify-start text-start\"\n onClick={select}\n >\n {words}\n </Button>\n );\n }\n return (\n <Button\n {...stateAttrs(action)}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n stretch\n tone={tone}\n pressed={action.pressed}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n className=\"w-full justify-start text-start\"\n onClick={select}\n >\n {words}\n </Button>\n );\n}\n\n/** One inline action: an IconButton with its icon, else a small text button — each a\n * link for an `href` (see {@link useActionHref}). The control carries the action's\n * state, its tone and its tour anchor. */\nfunction InlineAction({\n action,\n size,\n glyphSize,\n tooltipSide,\n}: {\n action: RowAction;\n size: RowActionsSize;\n glyphSize?: IconButtonGlyphSize;\n tooltipSide?: TooltipSide;\n}) {\n const href = useActionHref(action);\n const glyph = glyphOf(action.icon);\n const iconTone = action.tone === \"default\" ? undefined : action.tone;\n // A text button has Button's two tones; `info` and `warning` are a glyph's colours.\n const textTone = action.tone === \"danger\" || action.tone === \"muted\" ? action.tone : undefined;\n const marks = { \"data-tour\": action.dataTour, ...toneAttr(action.tone) };\n if (glyph) {\n if (href !== undefined) {\n return (\n <IconButton\n {...marks}\n href={href}\n external={action.external}\n size={size}\n glyphSize={glyphSize}\n label={action.label}\n tooltipSide={tooltipSide}\n tone={iconTone}\n disabled={action.disabled}\n stopPropagation\n onClick={() => action.onSelect?.()}\n >\n {glyph}\n </IconButton>\n );\n }\n return (\n <IconButton\n {...marks}\n {...stateAttrs(action)}\n size={size}\n glyphSize={glyphSize}\n label={action.label}\n tooltipSide={tooltipSide}\n tone={iconTone}\n pressed={action.pressed}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n stopPropagation\n onClick={() => action.onSelect?.()}\n >\n {glyph}\n </IconButton>\n );\n }\n // A row listening for clicks must not open as well (IconButton's stopPropagation).\n const stop = {\n onClick: (e: MouseEvent) => {\n e.stopPropagation();\n action.onSelect?.();\n },\n onKeyDown: (e: KeyboardEvent) => {\n if (e.key === \"Enter\" || e.key === \" \") e.stopPropagation();\n },\n };\n if (href !== undefined) {\n return (\n <Button\n {...marks}\n href={href}\n external={action.external}\n variant=\"ghost\"\n size=\"sm\"\n tone={textTone}\n disabled={action.disabled}\n {...stop}\n >\n {action.label}\n </Button>\n );\n }\n return (\n <Button\n {...marks}\n {...stateAttrs(action)}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n tone={textTone}\n pressed={action.pressed}\n disabledReason={action.disabledReason}\n commit={action.commit}\n pending={action.pending}\n disabled={action.disabled}\n {...stop}\n >\n {action.label}\n </Button>\n );\n}\n\n/** The actions column's heading, in the provider's words: on screen since 0.46.1 (keksdose\n * dev#598 rework), a screen reader's name only with `hidden`. */\nfunction RowActionsHeader({\n text,\n labels,\n hidden,\n}: {\n text?: string;\n labels?: LabelOverride<RowActionsLabels>;\n hidden?: boolean;\n}) {\n const resolved = useKitLabels(\"rowActions\", DEFAULT_ROW_ACTIONS_LABELS, labels);\n const word = text ?? resolved.actions;\n return hidden ? <span className=\"sr-only\">{word}</span> : <>{word}</>;\n}\n\n/** The actions cell's classes — `rowActionsColumn`'s `className`: the column hugs its\n * buttons, on one line, at the row's end. A hand-built table's `<TableCell>` takes them\n * around its {@link RowActions} (0.47, keksdose N17). */\nexport const ROW_ACTIONS_CELL_CLASS = \"w-px whitespace-nowrap text-end\";\n/** The actions heading's classes — `rowActionsColumn`'s `headClassName`, which\n * {@link RowActionsHead} draws: hugging, end-aligned over the buttons (0.47, N17). */\nexport const ROW_ACTIONS_HEAD_CLASS = \"w-px text-end\";\n\nexport interface RowActionsHeadProps extends Omit<TableHeaderCellProps, \"children\"> {\n /** The heading. Default: the provider's `rowActions.actions` (\"Actions\"). */\n children?: ReactNode;\n /** Named for a screen reader only — `rowActionsColumn`'s `header: false`. */\n hideLabel?: boolean;\n labels?: LabelOverride<RowActionsLabels>;\n}\n\n/**\n * A hand-built table's actions heading (0.47, keksdose N17): the `<th>` `rowActionsColumn`\n * gives a DataTable — \"Actions\" on screen in the provider's words, end-aligned over the\n * buttons, hugging them. Pair it with `<TableCell className={ROW_ACTIONS_CELL_CLASS}>`\n * around each row's {@link RowActions}. Every other `TableHeaderCell` prop passes on\n * (`size`, the app's `className`, merged last).\n *\n * <RowActionsHead size=\"sm\" className={TH} />\n */\nexport function RowActionsHead({ children, hideLabel, labels, className, ...rest }: RowActionsHeadProps) {\n return (\n <TableHeaderCell align=\"end\" {...rest} className={cn(ROW_ACTIONS_HEAD_CLASS, className)}>\n {typeof children === \"string\" || children == null ? (\n <RowActionsHeader text={children ?? undefined} labels={labels} hidden={hideLabel} />\n ) : (\n children\n )}\n </TableHeaderCell>\n );\n}\n\nexport interface RowActionsColumnOptions<T> {\n /** The column's key. Default `\"actions\"`. */\n key?: string;\n /** The row's actions — `false` / `null` for the ones a condition leaves out. */\n actions: (row: T) => RowActionList;\n /** The row's name, for the \"⋯\" button's name (\"Actions for Ada Example\"). `null` and\n * `undefined` both mean \"Actions\". */\n name?: (row: T) => string | null | undefined;\n collapse?: RowActionsCollapse;\n /** Default `xs`. */\n size?: RowActionsSize;\n /** See {@link RowActionsProps.glyphSize}. */\n glyphSize?: IconButtonGlyphSize;\n /** See {@link RowActionsProps.tooltipSide} — `\"start\"` in a table's last column. */\n tooltipSide?: TooltipSide;\n /**\n * The column's name as plain text — the column-settings checklist and the phone\n * card's label read it (`DataTableColumn.headerText`). Pass the app's own word; left\n * out, the heading shows the provider's `rowActions.actions` and the plain-text\n * places fall back to the column's `key`.\n */\n headerText?: string;\n /**\n * The heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE \"Actions\" heading on\n * every table with an actions column). Default: `headerText`, else the provider's\n * `rowActions.actions` (\"Actions\"), shown, end-aligned over the buttons. A node\n * replaces it; `false` keeps the pre-0.46.1 heading, named for a screen reader only.\n */\n header?: ReactNode | false;\n labels?: LabelOverride<RowActionsLabels>;\n /** Anything else for the column (`mobileHidden`, `cellProps`…), merged last. */\n column?: Partial<DataTableColumn<T>>;\n}\n\n/**\n * A DataTable column of {@link RowActions} — an app's action column in one line\n * (§10.8): inline icons at Normal, the \"⋯\" menu at Large with two or more actions.\n *\n * columns={[...columns, rowActionsColumn({ actions: (r) => [...], name: (r) => r.name })]}\n *\n * The heading says \"Actions\" on screen (0.46.1; `header` changes or hides it), the\n * column hugs its content at the row's end, and `noRowLink` keeps a `rowHref` anchor off it, so the\n * buttons keep their own clicks. On a phone it is a line of the card like any column;\n * set `column: { mobileHidden: true }` to drop it there (with `mobileDetailsInRow` it\n * then shows in the opened row instead).\n */\nexport function rowActionsColumn<T>({\n key = \"actions\",\n actions,\n name,\n collapse,\n size,\n glyphSize,\n tooltipSide,\n headerText,\n header,\n labels,\n column,\n}: RowActionsColumnOptions<T>): DataTableColumn<T> {\n return {\n key,\n header:\n header === false || header == null ? (\n <RowActionsHeader text={headerText} labels={labels} hidden={header === false} />\n ) : (\n header\n ),\n headerText,\n cell: (row) => (\n <RowActions\n actions={actions(row)}\n name={name?.(row)}\n collapse={collapse}\n size={size}\n glyphSize={glyphSize}\n tooltipSide={tooltipSide}\n labels={labels}\n />\n ),\n className: ROW_ACTIONS_CELL_CLASS,\n // The heading over the buttons, at the row's end (0.46.1); a hand-built table's\n // `RowActionsHead` (0.47).\n headClassName: ROW_ACTIONS_HEAD_CLASS,\n noRowLink: true,\n ...column,\n };\n}\n"],"mappings":"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","names":[]}
@@ -36,10 +36,16 @@ export interface TabsLabels {
36
36
  add: string;
37
37
  /** Accessible name of a tab's × — "Remove <tab>". */
38
38
  remove: (tab: string) => string;
39
- /** The `overflowCue` caret while tabs lie past the strip's end: "More tabs". */
39
+ /** The `overflowCue` › over the strip's end, while tabs lie past it: "More tabs". */
40
40
  more: string;
41
- /** The `overflowCue` caret at the strip's end, which goes back to the first tab. */
42
- backToFirst: string;
41
+ /** The `overflowCue` ‹ over the strip's start, while tabs lie before it: "Previous tabs". */
42
+ previous: string;
43
+ /**
44
+ * @deprecated 0.47: not drawn any more — the caret that turned into "back to the
45
+ * first tab" at the strip's end is now a ‹ of its own (`previous`). Accepted and
46
+ * ignored; removed in 0.48.
47
+ */
48
+ backToFirst?: string;
43
49
  }
44
50
  export { DEFAULT_TABS_LABELS };
45
51
  /**
@@ -62,20 +68,22 @@ export interface TabsProps<T extends string> extends Omit<ComponentPropsWithoutR
62
68
  onChange: (id: T) => void;
63
69
  className?: string;
64
70
  /**
65
- * The caret beside a SCROLLING strip (0.40, kk run 79 gap 1): a › while
66
- * tabs lie past the strip's end — a tap scrolls it on by three quarters of its width —
67
- * turning into a ‹ at the end, which goes back to the first. Shown only while the strip
68
- * actually overflows.
71
+ * Carets over the ends of a SCROLLING strip (0.40, kk run 79 gap 1; 0.47, keksdose
72
+ * live #410 round 4): a › over the strip's end while tabs lie past it, and a ‹ over
73
+ * its start while tabs lie before it — each there only while something is hidden
74
+ * past ITS edge (1 px slack, so a few cut-off pixels count), each a step of three
75
+ * quarters of the strip's width toward its own side. Neither ever changes direction:
76
+ * until 0.46 one caret turned round into "back to the first" at the end, and read as
77
+ * an arrow pointing the wrong way. Start and end follow the reading direction.
69
78
  *
70
- * The edge fade alone could read as the end of the row: at the default scroll position
71
- * the last visible tab can finish exactly where the fade begins, so nothing looks cut
72
- * off (keksdose live #410, whose `tab-strip-more.tsx` this replaces). One button that
73
- * turns round rather than two that come and go, so the strip's width never changes
74
- * under the thumb. Out of the tab order — the keyboard walks the strip with the arrow
75
- * keys, which bring the focused tab into view. At every width since 0.46 (keksdose live
76
- * #410 round 3: eleven report tabs overflow a laptop too); before, `md:hidden`. Ignored on
77
- * a vertical strip (a side nav is a column, not a row). Names:
78
- * `tabs.more` / `tabs.backToFirst`.
79
+ * The edge fade alone could read as the end of the row: the last visible tab can
80
+ * finish exactly where the fade begins, so nothing looks cut off (keksdose live #410).
81
+ * The carets sit OVER the strip's ends (positioned, on the page's surface), not beside
82
+ * it, so the strip's width never changes as they come and go. Out of the tab order —
83
+ * the keyboard walks the strip with the arrow keys, which bring the focused tab into
84
+ * view. At every width. Ignored on a vertical strip (a side nav is a column, not a
85
+ * row). Names: `tabs.more` / `tabs.previous`. keksdose's `scroll-edge-cues.tsx` is
86
+ * the reference this replaces.
79
87
  */
80
88
  overflowCue?: boolean;
81
89
  /**
@@ -48,35 +48,38 @@ function Tabs({
48
48
  const stripRef = useRef(null);
49
49
  const setStrip = useMemo(() => mergeRefs(stripRef, ref), [ref]);
50
50
  const fade = useStripFade(stripRef);
51
- const cue = overflowCue && !vertical && fade.overflow !== void 0;
52
- const cueAtEnd = fade.overflow === "start";
53
- const onCue = () => {
51
+ const cue = overflowCue && !vertical;
52
+ const cueStart = cue && (fade.overflow === "start" || fade.overflow === "both");
53
+ const cueEnd = cue && (fade.overflow === "end" || fade.overflow === "both");
54
+ const onCue = (side) => {
54
55
  const strip = stripRef.current;
55
56
  if (!strip) return;
56
57
  const behavior = prefersReducedMotion() ? "auto" : "smooth";
57
- if (cueAtEnd) {
58
- strip.scrollTo({ left: 0, behavior });
59
- return;
60
- }
61
58
  const rtl = getComputedStyle(strip).direction === "rtl";
62
- strip.scrollBy({ left: (rtl ? -1 : 1) * Math.round(strip.clientWidth * 0.75), behavior });
59
+ const toward = (side === "end" ? 1 : -1) * (rtl ? -1 : 1);
60
+ strip.scrollBy({ left: toward * Math.round(strip.clientWidth * 0.75), behavior });
63
61
  };
64
- const cueButton = cue ? (
65
- // The strip's bottom rule continues under the caret, so the row reads as one.
66
- /* @__PURE__ */ jsx("div", { "data-tabs-overflow-cue": "", className: "flex shrink-0 items-center self-stretch border-b border-subtle", children: /* @__PURE__ */ jsx(
67
- IconButton,
68
- {
69
- size: "sm",
70
- variant: "ghost",
71
- tone: "muted",
72
- tabIndex: -1,
73
- type: "button",
74
- "aria-label": cueAtEnd ? text.backToFirst : text.more,
75
- onClick: onCue,
76
- children: cueAtEnd ? /* @__PURE__ */ jsx(ChevronLeft, { className: "rtl:-scale-x-100" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "rtl:-scale-x-100" })
77
- }
78
- ) })
79
- ) : null;
62
+ const cueAt = (side) => /* @__PURE__ */ jsx(
63
+ "div",
64
+ {
65
+ "data-tabs-overflow-cue": side,
66
+ className: cn("absolute top-1/2 z-10 flex -translate-y-1/2", side === "start" ? "start-0" : "end-0"),
67
+ children: /* @__PURE__ */ jsx(
68
+ IconButton,
69
+ {
70
+ size: "sm",
71
+ variant: "secondary",
72
+ className: "bg-surface shadow-sm",
73
+ tabIndex: -1,
74
+ type: "button",
75
+ label: side === "start" ? text.previous : text.more,
76
+ labelVisible: false,
77
+ onClick: () => onCue(side),
78
+ children: side === "start" ? /* @__PURE__ */ jsx(ChevronLeft, { className: "rtl:-scale-x-100" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "rtl:-scale-x-100" })
79
+ }
80
+ )
81
+ }
82
+ );
80
83
  useLayoutEffect(() => {
81
84
  const strip = stripRef.current;
82
85
  const tab = strip?.querySelector('[role="tab"][aria-selected="true"]');
@@ -137,7 +140,7 @@ function Tabs({
137
140
  "data-overflow": fade.overflow,
138
141
  className: cn(
139
142
  vertical ? TABLIST_VERTICAL_CLASSES : TABLIST_CLASSES,
140
- onAdd ? "min-w-0" : overflowCue && !vertical ? "min-w-0 flex-1" : className
143
+ cue ? "min-w-0 flex-1" : onAdd ? "min-w-0" : className
141
144
  ),
142
145
  children: tabs.map((tab) => {
143
146
  const isActive = tab.id === active;
@@ -260,11 +263,15 @@ function Tabs({
260
263
  children: text.remove(nameOf(activeTab))
261
264
  }
262
265
  ) : null;
263
- if (!onAdd && overflowCue && !vertical) {
266
+ const stripBox = cue ? /* @__PURE__ */ jsxs("div", { className: cn("relative flex min-w-0", !onAdd && "flex-1"), children: [
267
+ tablist,
268
+ cueStart && cueAt("start"),
269
+ cueEnd && cueAt("end")
270
+ ] }) : tablist;
271
+ if (!onAdd && cue) {
264
272
  return /* @__PURE__ */ jsxs("div", { className: cn("flex items-end", className), children: [
265
- tablist,
266
- removeButton,
267
- cueButton
273
+ stripBox,
274
+ removeButton
268
275
  ] });
269
276
  }
270
277
  if (!onAdd) {
@@ -284,9 +291,8 @@ function Tabs({
284
291
  ] });
285
292
  }
286
293
  return /* @__PURE__ */ jsxs("div", { className: cn("flex items-end", className), children: [
287
- tablist,
294
+ stripBox,
288
295
  removeButton,
289
- cueButton,
290
296
  /* @__PURE__ */ jsx(
291
297
  "div",
292
298
  {