@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
package/README.md CHANGED
@@ -559,20 +559,20 @@ columns, strings and permissions (see the note at the top of `src/feedback/feedb
559
559
 
560
560
  <!-- BEGIN GENERATED: exports — node scripts/gen-export-inventory.mjs -->
561
561
 
562
- **2133 names from 349 modules** — 985 values and 1148 types. _Italic_ is a type-only export.
562
+ **2143 names from 350 modules** — 990 values and 1153 types. _Italic_ is a type-only export.
563
563
 
564
564
  Generated from `dist/index.d.ts` by `node scripts/gen-export-inventory.mjs`; the count
565
565
  is pinned by `src/__tests__/public-surface.test.ts`. Do not edit between the markers.
566
566
 
567
567
  | Entry point | Names |
568
568
  |---|---|
569
- | `@eifi1/ui-kit` | 2133 |
569
+ | `@eifi1/ui-kit` | 2143 |
570
570
  | `@eifi1/ui-kit/chart` | 92 |
571
571
  | `@eifi1/ui-kit/shell` | 114 |
572
- | `@eifi1/ui-kit/data-table` | 52 |
572
+ | `@eifi1/ui-kit/data-table` | 53 |
573
573
  | `@eifi1/ui-kit/wizard` | 24 |
574
574
  | `@eifi1/ui-kit/tour` | 37 |
575
- | `@eifi1/ui-kit/feedback` | 206 |
575
+ | `@eifi1/ui-kit/feedback` | 212 |
576
576
  | `@eifi1/ui-kit/search` | 23 |
577
577
  | `@eifi1/ui-kit/dates` | 22 |
578
578
  | `@eifi1/ui-kit/table-text` | 14 |
@@ -580,8 +580,8 @@ is pinned by `src/__tests__/public-surface.test.ts`. Do not edit between the mar
580
580
  | `@eifi1/ui-kit/eslint` | 1 |
581
581
  | `@eifi1/ui-kit/testing` | 11 |
582
582
  | `@eifi1/ui-kit/rhf` | 48 |
583
- | `@eifi1/ui-kit/offline` | 32 |
584
- | `@eifi1/ui-kit/sync` | 35 |
583
+ | `@eifi1/ui-kit/offline` | 33 |
584
+ | `@eifi1/ui-kit/sync` | 38 |
585
585
  | `@eifi1/ui-kit/sync/testing` | 3 |
586
586
  | `@eifi1/ui-kit/push-worker` | 10 |
587
587
 
@@ -784,7 +784,7 @@ re-slicing of it, never a second API.
784
784
  | `components/legal-page` | `LEGAL_SKELETON`, `LegalAcceptCheckbox`, `LegalKitSection`, `LegalPage`, _`LegalAcceptCheckboxProps`_, _`LegalDisclaimerVariant`_, _`LegalKitSectionKey`_, _`LegalKitSectionProps`_, _`LegalPageProps`_, _`LegalSectionOwner`_, _`LegalSkeletonEntry`_, _`LegalSkeletonPage`_ |
785
785
  | `components/appearance-labels` | `DEFAULT_APPEARANCE_LABELS`, `useAppearanceLabels`, _`AppearanceLabels`_ |
786
786
  | `components/date-format-setting` | `DateFormatSetting`, _`DateFormatSettingProps`_ |
787
- | `components/row-actions` | `DEFAULT_ROW_ACTIONS_LABELS`, `RowActions`, `rowActionsColumn`, _`RowAction`_, _`RowActionList`_, _`RowActionsCollapse`_, _`RowActionsColumnOptions`_, _`RowActionsLabels`_, _`RowActionsProps`_, _`RowActionsSize`_, _`RowActionTone`_ |
787
+ | `components/row-actions` | `DEFAULT_ROW_ACTIONS_LABELS`, `ROW_ACTIONS_CELL_CLASS`, `ROW_ACTIONS_HEAD_CLASS`, `RowActions`, `rowActionsColumn`, `RowActionsHead`, _`RowAction`_, _`RowActionList`_, _`RowActionsCollapse`_, _`RowActionsColumnOptions`_, _`RowActionsHeadProps`_, _`RowActionsLabels`_, _`RowActionsProps`_, _`RowActionsSize`_, _`RowActionTone`_ |
788
788
  | `components/checkbox-group` | `CheckboxGroup`, _`CheckboxGroupLegendVisibility`_, _`CheckboxGroupOption`_, _`CheckboxGroupProps`_ |
789
789
  | `components/one-time-code-input` | `OneTimeCodeInput`, _`OneTimeCodeInputProps`_ |
790
790
  | `components/language-select` | `LanguageSelect`, _`LanguageSelectProps`_ |
@@ -802,6 +802,7 @@ re-slicing of it, never a second API.
802
802
  | `components/data-table-pagination` | `PAGE_SIZE_OPTIONS`, `Pagination` |
803
803
  | `components/data-table-filter-popover` | `FilterPopover` |
804
804
  | `components/data-table` | `DataTable`, `DEFAULT_DATA_TABLE_SORT_LABELS`, _`DataTableCellProps`_, _`DataTableChrome`_, _`DataTableColumn`_, _`DataTableDensity`_, _`DataTableDesktopSwipeActions`_, _`DataTableHeadProps`_, _`DataTableProps`_, _`DataTableRowAction`_, _`DataTableSortLabels`_, _`FilterState`_, _`MobileSwipeActions`_, _`ServerPagination`_ |
805
+ | `components/data-table-row-actions` | `swipePlanRowActions` |
805
806
  | `components/data-table-cells` | `booleanColumn`, `BooleanMark`, _`BooleanColumnOptions`_, _`BooleanMarkProps`_ |
806
807
  | `components/use-table-state` | `readTableUrlState`, `useTableUrlState`, _`TableUrlState`_, _`TableUrlStateProps`_, _`TableUrlSync`_, _`UseTableUrlStateOptions`_, _`UseTableUrlStateReturn`_ |
807
808
  | `components/translation-review-editor` | `TranslationReviewEditor`, _`TranslationReviewEditorProps`_ |
@@ -891,6 +892,7 @@ re-slicing of it, never a second API.
891
892
  | `feedback/feedback-vote-button` | `DEFAULT_FEEDBACK_VOTE_LABELS`, `FeedbackVoteButton`, _`FeedbackVoteButtonProps`_, _`FeedbackVoteLabels`_ |
892
893
  | `feedback/feedback-comment-thread` | `DEFAULT_FEEDBACK_COMMENTS_LABELS`, `FeedbackCommentThread`, _`FeedbackCommentsLabels`_, _`FeedbackCommentThreadProps`_ |
893
894
  | `feedback/feedback-replies` | `DEFAULT_FEEDBACK_REPLIES_LABELS`, `FeedbackReplyComposer`, `feedbackReplyNudge`, `FeedbackReplyTimeline`, `useFeedbackRepliesLabels`, _`FeedbackRepliesLabels`_, _`FeedbackReplyComposerProps`_, _`FeedbackReplyTimelineProps`_ |
895
+ | `feedback/feedback-entry-file` | `FeedbackEntryFile`, `useFeedbackEntryUpload`, _`FeedbackEntryFileProps`_, _`FeedbackEntryUpload`_, _`FeedbackEntryUploadOptions`_, _`FeedbackEntryUploadState`_ |
894
896
  | `feedback/feedback-vocabulary` | `FEEDBACK_CATEGORY_META`, `FEEDBACK_CATEGORY_ORDER`, `FEEDBACK_STATUS_META`, `FEEDBACK_STATUS_ORDER`, `feedbackCategoryRank`, `nextFeedbackStatus`, `selectableFeedbackStatuses`, `visibleFeedbackStatuses`, _`FeedbackCategory`_, _`FeedbackStatus`_ |
895
897
 
896
898
  ### wizard
@@ -1004,16 +1006,15 @@ re-slicing of it, never a second API.
1004
1006
  | `notifications/notification-types` | _`DigestCadence`_, _`NotificationChannel`_, _`NotificationSettingsPatch`_, _`NotificationSwitches`_, _`NotificationTypeEntry`_, _`NotificationTypeGroup`_, _`NotificationTypesValue`_, _`PushDeviceItem`_ |
1005
1007
  | `notifications/notification-types-setting` | `NotificationTypesSetting`, _`NotificationTypesSettingProps`_ |
1006
1008
  | `notifications/quiet-hours-setting` | `QuietHoursSetting`, _`QuietHoursSettingProps`_ |
1007
- | `notifications/time-zone-select` | `deviceTimeZone`, `timeZoneOptions`, `TimeZoneSelect`, _`TimeZoneSelectProps`_ |
1008
- | `notifications/notification-inbox` | `INBOX_BADGE_INTERVAL`, `NotificationInbox`, _`NotificationInboxItem`_, _`NotificationInboxProps`_ |
1009
+ | `notifications/notification-inbox` | `INBOX_BADGE_INTERVAL`, `NotificationInbox`, _`NotificationInboxAction`_, _`NotificationInboxItem`_, _`NotificationInboxProps`_ |
1009
1010
  | `notifications/notification-bell` | `NotificationBell`, _`NotificationBellProps`_ |
1010
- | `notifications/device-zone-report` | `useDeviceZoneReport`, _`DeviceZoneReportOptions`_ |
1011
+ | `notifications/device-zone-report` | `deviceTimeZone`, `useDeviceZoneReport`, _`DeviceZoneReportOptions`_ |
1011
1012
  | `notifications/use-push-device` | `notifyPushSubscriptionChanged`, `PUSH_SUBSCRIPTION_CHANGED`, `PushDeviceError`, `pushSupported`, `usePushDevice`, _`PushDevice`_, _`PushDeviceErrorCode`_, _`PushPermission`_, _`PushSubscriptionInput`_, _`UsePushDeviceOptions`_ |
1012
1013
  | `notifications/push-worker` | `handlePushSubscriptionChange`, `pushSubscriptionInput`, `resubscribeAfterChange`, _`PushReplaceInput`_, _`PushResubscribeOutcome`_, _`PushSubscriptionChangeEventLike`_, _`PushSubscriptionChangeOptions`_, _`PushWorkerScope`_ |
1013
1014
  | `notifications/push-device-setting` | `PushDeviceSetting`, _`PushDeviceAction`_, _`PushDeviceSettingProps`_ |
1014
1015
  | `notifications/push-devices-setting` | `describeUserAgent`, `PushDevicesSetting`, _`PushDevicesSettingProps`_ |
1015
1016
  | `notifications/unsubscribe-panel` | `UnsubscribePanel`, `unsubscribeTokenType`, _`UnsubscribePanelProps`_ |
1016
- | `notifications/feedback-notification-types` | `DEFAULT_FEEDBACK_NOTIFICATION_TYPES_LABELS`, `useFeedbackNotificationTypes`, _`FeedbackNotificationTypeId`_, _`FeedbackNotificationTypesLabels`_ |
1017
+ | `notifications/feedback-notification-types` | `DEFAULT_FEEDBACK_NOTIFICATION_TYPES_LABELS`, `useFeedbackNotificationTypes`, `useFeedbackNotificationTypesLabels`, _`FeedbackNotificationTypeId`_, _`FeedbackNotificationTypesLabels`_ |
1017
1018
 
1018
1019
  ### demo
1019
1020
 
@@ -180,6 +180,10 @@ export interface UserRosterColumnsOptions<T extends UserRosterRow, R extends str
180
180
  extra?: readonly DataTableColumn<T>[];
181
181
  /** The row's action menu (§4). Left out: no actions column. */
182
182
  actions?: (row: T) => UserRowActionList;
183
+ /** The actions column's heading (0.46.1, keksdose dev#598 rework — a VISIBLE "Actions"
184
+ * on every table with an actions column). Default: `labels.actions`, shown. A node
185
+ * replaces it; `false` keeps the pre-0.46.1 heading, a screen reader's name only. */
186
+ actionsHeader?: ReactNode | false;
183
187
  /**
184
188
  * Whether this row's role is a select rather than a chip, and with which reasons.
185
189
  * Left out, or `false`: a chip. kastlan's several roles are never a select here — they
@@ -216,10 +220,10 @@ export interface UserRosterColumnsOptions<T extends UserRosterRow, R extends str
216
220
  * Under `serverPagination` the `sortBy`s only make the headers clickable; the app sends
217
221
  * the sort ({@link userRosterSort}).
218
222
  */
219
- export declare function userRosterColumns<T extends UserRosterRow, R extends string = string>({ roles, stateOf, extra, actions, editableRole, onRoleChange, roleOf, emailColumn, labels: labelsProp, }: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[];
223
+ export declare function userRosterColumns<T extends UserRosterRow, R extends string = string>({ roles, stateOf, extra, actions, actionsHeader, editableRole, onRoleChange, roleOf, emailColumn, labels: labelsProp, }: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[];
220
224
  /**
221
225
  * {@link userRosterColumns} with the `userRoster` words from the provider, memoised on
222
226
  * the options — pass stable callbacks (`useCallback`) and a module-level vocabulary, or
223
227
  * the columns are rebuilt on every render.
224
228
  */
225
- export declare function useUserRosterColumns<T extends UserRosterRow, R extends string = string>({ roles, stateOf, extra, actions, editableRole, onRoleChange, roleOf, emailColumn, labels: labelsProp, }: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[];
229
+ export declare function useUserRosterColumns<T extends UserRosterRow, R extends string = string>({ roles, stateOf, extra, actions, actionsHeader, editableRole, onRoleChange, roleOf, emailColumn, labels: labelsProp, }: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[];
@@ -86,6 +86,7 @@ function userRosterColumns({
86
86
  stateOf = adminUserStates,
87
87
  extra = [],
88
88
  actions,
89
+ actionsHeader,
89
90
  editableRole,
90
91
  onRoleChange,
91
92
  roleOf,
@@ -183,13 +184,14 @@ function userRosterColumns({
183
184
  if (actions) {
184
185
  columns.push({
185
186
  key: "actions",
186
- header: /* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.actions }),
187
+ // On screen, as every actions column's heading is (0.46.1, keksdose dev#598 rework).
188
+ header: actionsHeader === false ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.actions }) : actionsHeader ?? labels.actions,
187
189
  headerText: labels.actions,
188
190
  // The caller's own words only: the menu reads the provider's itself, and the
189
191
  // English defaults merged in above must not shadow them.
190
192
  cell: (row) => /* @__PURE__ */ jsx(RowName, { row, children: (name) => /* @__PURE__ */ jsx(UserRowActions, { actions: actions(row), name, labels: labelsProp }) }),
191
193
  className: "w-px whitespace-nowrap text-end",
192
- headClassName: "w-px",
194
+ headClassName: "w-px text-end",
193
195
  // The menu button keeps its own click: a row link must not wrap it.
194
196
  noRowLink: true
195
197
  });
@@ -201,6 +203,7 @@ function useUserRosterColumns({
201
203
  stateOf,
202
204
  extra,
203
205
  actions,
206
+ actionsHeader,
204
207
  editableRole,
205
208
  onRoleChange,
206
209
  roleOf,
@@ -214,13 +217,14 @@ function useUserRosterColumns({
214
217
  stateOf,
215
218
  extra,
216
219
  actions,
220
+ actionsHeader,
217
221
  editableRole,
218
222
  onRoleChange,
219
223
  roleOf,
220
224
  emailColumn,
221
225
  labels
222
226
  }),
223
- [roles, stateOf, extra, actions, editableRole, onRoleChange, roleOf, emailColumn, labels]
227
+ [roles, stateOf, extra, actions, actionsHeader, editableRole, onRoleChange, roleOf, emailColumn, labels]
224
228
  );
225
229
  }
226
230
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/user-roster.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { DateInput } from \"../lib/format\";\nimport type { PersonName } from \"../lib/person-name\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { AccountStateChip, RoleChip, dateColumn } from \"../components/account-chips\";\nimport type { AccountState, RoleVocabulary } from \"../components/account-chips\";\nimport type { ChipTone } from \"../components/chip\";\nimport type { DataTableColumn } from \"../components/data-table\";\nimport { encodeSorts } from \"../components/data-table-sort\";\nimport type { SortState } from \"../components/data-table-sort\";\nimport { RowActions } from \"../components/row-actions\";\nimport type { RowAction, RowActionList } from \"../components/row-actions\";\nimport { UserAvatar } from \"../components/user-avatar\";\nimport { personLabel } from \"./admin-parts\";\nimport { RoleSelect } from \"./role-select\";\nimport type { RoleLock } from \"./role-select\";\nimport { DEFAULT_USER_ROSTER_LABELS } from \"./user-roster-labels\";\n\n/**\n * The admin's user list (docs/user-admin-harmonization.md §3): a column preset for the\n * kit's `DataTable`, not a table of its own.\n *\n * keksdose built its roster by hand (1150 lines, its own Badge), Kurvenschmiede on the\n * kit `DataTable` with the 0.18 chips, kastlan as a settings page. What the three share\n * is the row — name over address, a role, the account's states, created, last login, the\n * app's own columns, and a handful of actions — so that is what the kit draws, and the\n * table, its paging, its sort and its filters stay the app's `DataTable` wiring:\n *\n * ```tsx\n * const columns = useUserRosterColumns<AdminUserRow, Role>({ roles: ROLES, actions, extra });\n * <DataTable rows={page.users} columns={columns} rowKey={(r) => r.id}\n * serverPagination={{ page, pageSize, total: page.total, onPageChange }}\n * sorts={sorts} onSortsChange={setSorts} />\n * // GET /admin/users?sort=${userRosterSort(sorts)}&…\n * ```\n *\n * The column keys ARE the server's sort keys (§3.1): `name`, `email`, `role`, `created`,\n * `last_login` — so {@link userRosterSort} turns the table's sort state into the query\n * as it is. Phone cards come from the same columns: the identity is the card's headline,\n * the role, states, last login and the actions are its fields, created is left off.\n */\n\n/* ── Rows and states ────────────────────────────────────────────────────────── */\n\n/**\n * The fields of the contract's `AdminUserRow` the preset reads (§3.2, the auth\n * contract's `UserResponse` core). An app's row has more — its `extra` — and the\n * preset passes the whole row to every callback.\n */\nexport interface UserRosterRow extends AdminUserStateFields {\n id: string | number;\n email: string;\n first_name?: string | null;\n last_name?: string | null;\n /** The derived whole name — only the fallback when both parts are blank. */\n display_name?: string | null;\n /** keksdose, Kurvenschmiede: one role. */\n role?: string | null;\n /** kastlan: the account's roles in the current company. Wins over `role`. */\n roles?: readonly string[] | null;\n created_at?: DateInput;\n /** Account-wide; kastlan's comes from its session rows (§3.2). */\n last_login_at?: DateInput;\n}\n\n/** The state fields of the contract's row that {@link adminUserStates} maps (§3.2). */\nexport interface AdminUserStateFields {\n is_active?: boolean;\n email_verified?: boolean;\n password_change_required_at?: string | null;\n /** `after_days` mode: the day the account is erased (§6.4). */\n deletion_scheduled_at?: string | null;\n /** When the user asked. The only deletion field in kastlan's `operator` mode, where\n * nothing is scheduled. */\n deletion_requested_at?: string | null;\n}\n\n/** One state chip of a row: an {@link AccountState}, or one with its `date` (the\n * `deletion` day), a `tone` or the app's own words. */\nexport type RosterState =\n | AccountState\n | { state: AccountState; date?: DateInput; tone?: ChipTone; label?: ReactNode };\n\n/**\n * The contract's row as state chips (§3.2: \"map 1:1 to the kit's AccountStateChip\"):\n *\n * - marked for deletion → `deletion` with the scheduled day (none in `operator` mode),\n * IN PLACE of `inactive` — such an account is always deactivated (§2.1);\n * - else `active` / `inactive` from `is_active`;\n * - `unverified` when `email_verified` is `false` (not when it is missing);\n * - `passwordChange` while `password_change_required_at` is set.\n */\nexport function adminUserStates(row: AdminUserStateFields): RosterState[] {\n const out: RosterState[] = [];\n if (row.deletion_scheduled_at || row.deletion_requested_at) {\n out.push({ state: \"deletion\", date: row.deletion_scheduled_at ?? null });\n } else if (row.is_active !== undefined) {\n out.push(row.is_active ? \"active\" : \"inactive\");\n }\n if (row.email_verified === false) out.push(\"unverified\");\n if (row.password_change_required_at) out.push(\"passwordChange\");\n return out;\n}\n\n/** The server's sort keys, which are the preset's column keys (§3.1). */\nexport const USER_ROSTER_SORT_KEYS = [\"name\", \"email\", \"role\", \"created\", \"last_login\"] as const;\nexport type UserRosterSortKey = (typeof USER_ROSTER_SORT_KEYS)[number];\n\n/**\n * The table's sort state as the `sort` query parameter of `GET /admin/users` (§3.1), in\n * the DataTable's own encoding ({@link encodeSorts}: `\"last_login.desc\"`), which\n * server-kit 0.4 reads beside the `-key` form. Only the first criterion on one of the\n * server's keys: an app's own column (`owned`) is the client's to sort, and the query\n * takes one key — a shift-clicked second criterion is not sent. `undefined` when nothing\n * the server knows is sorted, so its default order applies.\n */\nexport function userRosterSort(sorts: readonly SortState[]): string | undefined {\n const first = sorts.find((s) => (USER_ROSTER_SORT_KEYS as readonly string[]).includes(s.key));\n return first ? (encodeSorts([first]) ?? undefined) : undefined;\n}\n\n/* ── Labels ─────────────────────────────────────────────────────────────────── */\n\n/** The `userRoster` namespace: the preset's headers and the action menu's words. */\nexport interface UserRosterLabels {\n /** The identity column — the name with the address under it. */\n name: string;\n /** The separate address column (`emailColumn`). */\n email: string;\n role: string;\n state: string;\n created: string;\n lastLogin: string;\n /** A last login that never happened. */\n never: string;\n /** The actions column's (hidden) header. */\n actions: string;\n /** The action menu's button and panel — \"Actions for Ada Example\". */\n actionsFor: (name: string) => string;\n}\n\n// Defined in ./user-roster-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_USER_ROSTER_LABELS };\n\n/* ── UserIdentityCell ───────────────────────────────────────────────────────── */\n\nexport interface UserIdentityCellProps {\n /** First and last name (auth §3.2), shown in the READER's order. */\n person?: PersonName | null;\n /** A whole name, for an API that still sends `display_name` only. */\n name?: string | null;\n email?: string | null;\n /** The initials avatar before the name. Default true. */\n avatar?: boolean;\n /** After the name, on its line — a \"You\" chip, a demo mark. */\n badge?: ReactNode;\n /** Over the provider's locale. */\n locale?: string;\n className?: string;\n}\n\n/**\n * Who an account is, in one cell (§3.3): the initials avatar, the name in the reader's\n * order ({@link formatPersonName} — \"Example Ada\" in Hungarian) and the address under it\n * — the cell both rosters drew by hand. An account with no name yet (a migrated row\n * before `CompleteNameDialog`, an invitee) shows its address once, as the name.\n *\n * Both lines carry `data-private`, so keksdose's demo blur reaches them, and both end\n * in \"…\" at Normal only: from Large up they wrap (`truncate-until-large`, 0.33,\n * docs/text-size-harmonization.md §10.17), since a cut name or address is a finding there.\n */\nexport function UserIdentityCell({\n person,\n name,\n email,\n avatar = true,\n badge,\n locale: localeProp,\n className,\n}: UserIdentityCellProps) {\n const locale = useKitLocale(localeProp);\n const named = personLabel({ first: person?.first, last: person?.last, name }, locale);\n const address = (email ?? \"\").trim();\n return (\n <div className={cn(\"flex min-w-0 items-center gap-2.5\", className)}>\n {avatar && <UserAvatar person={person} name={name} email={email} size=\"sm\" />}\n <div className=\"min-w-0\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span data-private className=\"truncate-until-large font-medium text-primary\">\n {named || address}\n </span>\n {badge}\n </div>\n {named && address && (\n <div data-private className=\"truncate-until-large text-xs text-muted\">\n {address}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n/* ── The row's actions ──────────────────────────────────────────────────────── */\n\n/** One entry of a row's action menu (§4) — the kit's {@link RowAction} since 0.32,\n * which generalised it for every dense row (docs/text-size-harmonization.md §10.8). */\nexport type UserRowAction = RowAction;\n\n/** What `actions(row)` may return: entries, and `false` / `null` for the ones a\n * condition left out (`row.is_active && {…}`). */\nexport type UserRowActionList = RowActionList;\n\nexport interface UserRowActionsProps {\n actions: UserRowActionList;\n /** The account's name, for the menu's name — \"Actions for Ada Example\". */\n name: string;\n labels?: LabelOverride<UserRosterLabels>;\n}\n\n/**\n * A row's actions as one \"…\" button and a menu of them: keksdose had a strip of icon\n * buttons in the expanded row, Kurvenschmiede five text buttons in a cell — neither fits\n * a phone card, and both grow with every action §2.4 adds. The menu is portalled, so a\n * table's scroller cannot clip it; Escape and an outside press close it, and focus goes\n * back to the button, which is where a confirmation dialog then returns it.\n *\n * Since 0.32 this is {@link RowActions} with `collapse=\"menu\"` — the menu at every size —\n * named by the roster's own `actionsFor`.\n */\nexport function UserRowActions({ actions, name, labels: labelsProp }: UserRowActionsProps) {\n const labels = useKitLabels(\"userRoster\", DEFAULT_USER_ROSTER_LABELS, labelsProp);\n return (\n <RowActions\n actions={actions}\n name={name}\n collapse=\"menu\"\n labels={{ actionsFor: labels.actionsFor }}\n />\n );\n}\n\n/* ── The preset ─────────────────────────────────────────────────────────────── */\n\n/** How a row's role may be changed inline: `true`, or with reasons. */\nexport interface RoleEditing<R extends string> {\n /** Why the whole select is locked for this row — `\"self\"`, `\"last_admin\"`, or words. */\n disabledReason?: RoleLock;\n /** Why one role cannot be chosen for this row (a string: it goes inside an option). */\n optionDisabledReason?: (role: R) => string | undefined;\n /** The roles that may be chosen. Default: the whole vocabulary. */\n offered?: readonly R[];\n}\n\nexport interface UserRosterColumnsOptions<T extends UserRosterRow, R extends string = string> {\n /** The app's roles, as `RoleChip` reads them. */\n roles: RoleVocabulary<R>;\n /** The row's state chips. Default {@link adminUserStates}, which reads the contract's\n * fields (`is_active`, `email_verified`, `password_change_required_at`,\n * `deletion_scheduled_at`, `deletion_requested_at`). */\n stateOf?: (row: T) => readonly RosterState[];\n /** The app's own columns — keksdose's plan and key custody, Kurvenschmiede's owned\n * counts — after the dates, before the actions. */\n extra?: readonly DataTableColumn<T>[];\n /** The row's action menu (§4). Left out: no actions column. */\n actions?: (row: T) => UserRowActionList;\n /**\n * Whether this row's role is a select rather than a chip, and with which reasons.\n * Left out, or `false`: a chip. kastlan's several roles are never a select here — they\n * are a \"Manage roles\" action that opens `RolesEditor`.\n */\n editableRole?: (row: T) => boolean | RoleEditing<R>;\n /** A role was picked in a row's select. The app confirms (`acknowledge`) and sends it. */\n onRoleChange?: (row: T, role: R) => void;\n /** The row's role(s). Default `row.roles ?? row.role`. */\n roleOf?: (row: T) => R | readonly R[] | null | undefined;\n /** A separate, sortable \"Email\" column (the server's `email` sort key) beside the\n * identity. Default false: the address is under the name already. */\n emailColumn?: boolean;\n /** The words, already resolved. Default English; {@link useUserRosterColumns} reads\n * the provider's `userRoster` namespace. */\n labels?: LabelOverride<UserRosterLabels>;\n}\n\nfunction rolesOf<T extends UserRosterRow, R extends string>(\n row: T,\n roleOf: UserRosterColumnsOptions<T, R>[\"roleOf\"],\n): R[] {\n const raw = roleOf ? roleOf(row) : ((row.roles ?? row.role) as R | readonly R[] | null | undefined);\n if (raw === null || raw === undefined || raw === \"\") return [];\n return typeof raw === \"string\" ? [raw as R] : [...(raw as readonly R[])];\n}\n\n/** The row's name in the reader's order — a component, because the order is the\n * READER's and the locale comes from the provider. */\nfunction RowName<T extends UserRosterRow>({ row, children }: { row: T; children: (name: string) => ReactNode }) {\n const locale = useKitLocale();\n const name = personLabel(\n { first: row.first_name, last: row.last_name, name: row.display_name, email: row.email },\n locale,\n );\n return <>{children(name)}</>;\n}\n\nfunction StateChips({ states }: { states: readonly RosterState[] }) {\n if (states.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\">\n {states.map((entry) => {\n const spec = typeof entry === \"string\" ? { state: entry } : entry;\n return (\n <AccountStateChip key={spec.state} state={spec.state} date={spec.date} tone={spec.tone} size=\"xs\">\n {spec.label}\n </AccountStateChip>\n );\n })}\n </div>\n );\n}\n\n/**\n * The roster's columns, in the contract's order (§3.3):\n *\n * | key | cell | sort | phone |\n * |---|---|---|---|\n * | `name` | {@link UserIdentityCell} | last, first | the card's headline |\n * | `email` | the address (`emailColumn` only) | address | hidden |\n * | `role` | `RoleChip`(s), or {@link RoleSelect} where `editableRole` | role | field |\n * | `state` | `AccountStateChip`s from `stateOf` | — | field |\n * | `created` | `dateColumn` | instant | hidden |\n * | `last_login` | `dateColumn`, relative, \"Never\" | instant | field |\n * | …`extra` | the app's | the app's | the app's |\n * | `actions` | {@link UserRowActions} | — | field |\n *\n * Pure: hand it resolved words, or use {@link useUserRosterColumns}. The keys are stable,\n * so an app may drop or move a column (`columns.filter((c) => c.key !== \"created\")`).\n * Under `serverPagination` the `sortBy`s only make the headers clickable; the app sends\n * the sort ({@link userRosterSort}).\n */\nexport function userRosterColumns<T extends UserRosterRow, R extends string = string>({\n roles,\n stateOf = adminUserStates,\n extra = [],\n actions,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn = false,\n labels: labelsProp,\n}: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[] {\n const labels: UserRosterLabels = { ...DEFAULT_USER_ROSTER_LABELS, ...labelsProp };\n const roleWords = (row: T) =>\n rolesOf(row, roleOf)\n .map((role) => roles[role]?.label ?? role)\n .join(\", \");\n const columns: DataTableColumn<T>[] = [\n {\n key: \"name\",\n header: labels.name,\n cell: (row) => (\n <UserIdentityCell\n person={{ first: row.first_name, last: row.last_name }}\n name={row.display_name}\n email={row.email}\n />\n ),\n // By last name, then first (auth §3.2), as the server sorts `name`.\n sortBy: (row) => `${row.last_name ?? \"\"} ${row.first_name ?? \"\"}`.trim().toLocaleLowerCase() || row.email,\n // Either part, in either order, or the address (auth §10.6).\n filterBy: (row) =>\n [row.first_name, row.last_name, row.last_name, row.first_name, row.display_name, row.email]\n .filter(Boolean)\n .join(\" \"),\n mobilePrimary: true,\n },\n ];\n if (emailColumn) {\n columns.push({\n key: \"email\",\n header: labels.email,\n className: \"whitespace-nowrap\",\n cell: (row) => (\n <span data-private className=\"text-secondary\">\n {row.email}\n </span>\n ),\n sortBy: (row) => row.email.toLowerCase(),\n filterBy: (row) => row.email,\n mobileHidden: true,\n });\n }\n columns.push(\n {\n key: \"role\",\n header: labels.role,\n cell: (row) => {\n const held = rolesOf(row, roleOf);\n const editing = held.length <= 1 ? editableRole?.(row) : false;\n if (editing && onRoleChange) {\n const rules: RoleEditing<R> = editing === true ? {} : editing;\n return (\n <RowName row={row}>\n {(name) => (\n <RoleSelect<R>\n size=\"sm\"\n name={name}\n value={held[0] ?? null}\n roles={roles}\n offered={rules.offered}\n disabledReason={rules.disabledReason}\n optionDisabledReason={rules.optionDisabledReason}\n onChange={(role) => onRoleChange(row, role)}\n />\n )}\n </RowName>\n );\n }\n if (held.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\">\n {held.map((role) => (\n <RoleChip<R> key={role} value={role} roles={roles} size=\"xs\" />\n ))}\n </div>\n );\n },\n sortBy: roleWords,\n filter: {\n type: \"select\",\n getValue: (row) => rolesOf(row, roleOf)[0] ?? \"\",\n options: (Object.keys(roles) as R[]).map((value) => ({ value, label: roles[value].label })),\n },\n },\n {\n key: \"state\",\n header: labels.state,\n cell: (row) => <StateChips states={stateOf(row)} />,\n },\n dateColumn<T>({\n key: \"created\",\n header: labels.created,\n value: (row) => row.created_at ?? null,\n column: { mobileHidden: true },\n }),\n // How long ago rather than a date — the question is \"recently?\" — with the exact\n // time beside it (keksdose's last-seen column, Kurvenschmiede's; 0.32 §4: never only\n // in a tooltip).\n dateColumn<T>({\n key: \"last_login\",\n header: labels.lastLogin,\n value: (row) => row.last_login_at ?? null,\n display: \"relative\",\n empty: labels.never,\n }),\n ...extra,\n );\n if (actions) {\n columns.push({\n key: \"actions\",\n header: <span className=\"sr-only\">{labels.actions}</span>,\n headerText: labels.actions,\n // The caller's own words only: the menu reads the provider's itself, and the\n // English defaults merged in above must not shadow them.\n cell: (row) => (\n <RowName row={row}>\n {(name) => <UserRowActions actions={actions(row)} name={name} labels={labelsProp} />}\n </RowName>\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px\",\n // The menu button keeps its own click: a row link must not wrap it.\n noRowLink: true,\n });\n }\n return columns;\n}\n\n/**\n * {@link userRosterColumns} with the `userRoster` words from the provider, memoised on\n * the options — pass stable callbacks (`useCallback`) and a module-level vocabulary, or\n * the columns are rebuilt on every render.\n */\nexport function useUserRosterColumns<T extends UserRosterRow, R extends string = string>({\n roles,\n stateOf,\n extra,\n actions,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels: labelsProp,\n}: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[] {\n const labels = useKitLabels(\"userRoster\", DEFAULT_USER_ROSTER_LABELS, labelsProp);\n return useMemo(\n () =>\n userRosterColumns<T, R>({\n roles,\n stateOf,\n extra,\n actions,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels,\n }),\n [roles, stateOf, extra, actions, editableRole, onRoleChange, roleOf, emailColumn, labels],\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/admin/user-roster.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { DateInput } from \"../lib/format\";\nimport type { PersonName } from \"../lib/person-name\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { AccountStateChip, RoleChip, dateColumn } from \"../components/account-chips\";\nimport type { AccountState, RoleVocabulary } from \"../components/account-chips\";\nimport type { ChipTone } from \"../components/chip\";\nimport type { DataTableColumn } from \"../components/data-table\";\nimport { encodeSorts } from \"../components/data-table-sort\";\nimport type { SortState } from \"../components/data-table-sort\";\nimport { RowActions } from \"../components/row-actions\";\nimport type { RowAction, RowActionList } from \"../components/row-actions\";\nimport { UserAvatar } from \"../components/user-avatar\";\nimport { personLabel } from \"./admin-parts\";\nimport { RoleSelect } from \"./role-select\";\nimport type { RoleLock } from \"./role-select\";\nimport { DEFAULT_USER_ROSTER_LABELS } from \"./user-roster-labels\";\n\n/**\n * The admin's user list (docs/user-admin-harmonization.md §3): a column preset for the\n * kit's `DataTable`, not a table of its own.\n *\n * keksdose built its roster by hand (1150 lines, its own Badge), Kurvenschmiede on the\n * kit `DataTable` with the 0.18 chips, kastlan as a settings page. What the three share\n * is the row — name over address, a role, the account's states, created, last login, the\n * app's own columns, and a handful of actions — so that is what the kit draws, and the\n * table, its paging, its sort and its filters stay the app's `DataTable` wiring:\n *\n * ```tsx\n * const columns = useUserRosterColumns<AdminUserRow, Role>({ roles: ROLES, actions, extra });\n * <DataTable rows={page.users} columns={columns} rowKey={(r) => r.id}\n * serverPagination={{ page, pageSize, total: page.total, onPageChange }}\n * sorts={sorts} onSortsChange={setSorts} />\n * // GET /admin/users?sort=${userRosterSort(sorts)}&…\n * ```\n *\n * The column keys ARE the server's sort keys (§3.1): `name`, `email`, `role`, `created`,\n * `last_login` — so {@link userRosterSort} turns the table's sort state into the query\n * as it is. Phone cards come from the same columns: the identity is the card's headline,\n * the role, states, last login and the actions are its fields, created is left off.\n */\n\n/* ── Rows and states ────────────────────────────────────────────────────────── */\n\n/**\n * The fields of the contract's `AdminUserRow` the preset reads (§3.2, the auth\n * contract's `UserResponse` core). An app's row has more — its `extra` — and the\n * preset passes the whole row to every callback.\n */\nexport interface UserRosterRow extends AdminUserStateFields {\n id: string | number;\n email: string;\n first_name?: string | null;\n last_name?: string | null;\n /** The derived whole name — only the fallback when both parts are blank. */\n display_name?: string | null;\n /** keksdose, Kurvenschmiede: one role. */\n role?: string | null;\n /** kastlan: the account's roles in the current company. Wins over `role`. */\n roles?: readonly string[] | null;\n created_at?: DateInput;\n /** Account-wide; kastlan's comes from its session rows (§3.2). */\n last_login_at?: DateInput;\n}\n\n/** The state fields of the contract's row that {@link adminUserStates} maps (§3.2). */\nexport interface AdminUserStateFields {\n is_active?: boolean;\n email_verified?: boolean;\n password_change_required_at?: string | null;\n /** `after_days` mode: the day the account is erased (§6.4). */\n deletion_scheduled_at?: string | null;\n /** When the user asked. The only deletion field in kastlan's `operator` mode, where\n * nothing is scheduled. */\n deletion_requested_at?: string | null;\n}\n\n/** One state chip of a row: an {@link AccountState}, or one with its `date` (the\n * `deletion` day), a `tone` or the app's own words. */\nexport type RosterState =\n | AccountState\n | { state: AccountState; date?: DateInput; tone?: ChipTone; label?: ReactNode };\n\n/**\n * The contract's row as state chips (§3.2: \"map 1:1 to the kit's AccountStateChip\"):\n *\n * - marked for deletion → `deletion` with the scheduled day (none in `operator` mode),\n * IN PLACE of `inactive` — such an account is always deactivated (§2.1);\n * - else `active` / `inactive` from `is_active`;\n * - `unverified` when `email_verified` is `false` (not when it is missing);\n * - `passwordChange` while `password_change_required_at` is set.\n */\nexport function adminUserStates(row: AdminUserStateFields): RosterState[] {\n const out: RosterState[] = [];\n if (row.deletion_scheduled_at || row.deletion_requested_at) {\n out.push({ state: \"deletion\", date: row.deletion_scheduled_at ?? null });\n } else if (row.is_active !== undefined) {\n out.push(row.is_active ? \"active\" : \"inactive\");\n }\n if (row.email_verified === false) out.push(\"unverified\");\n if (row.password_change_required_at) out.push(\"passwordChange\");\n return out;\n}\n\n/** The server's sort keys, which are the preset's column keys (§3.1). */\nexport const USER_ROSTER_SORT_KEYS = [\"name\", \"email\", \"role\", \"created\", \"last_login\"] as const;\nexport type UserRosterSortKey = (typeof USER_ROSTER_SORT_KEYS)[number];\n\n/**\n * The table's sort state as the `sort` query parameter of `GET /admin/users` (§3.1), in\n * the DataTable's own encoding ({@link encodeSorts}: `\"last_login.desc\"`), which\n * server-kit 0.4 reads beside the `-key` form. Only the first criterion on one of the\n * server's keys: an app's own column (`owned`) is the client's to sort, and the query\n * takes one key — a shift-clicked second criterion is not sent. `undefined` when nothing\n * the server knows is sorted, so its default order applies.\n */\nexport function userRosterSort(sorts: readonly SortState[]): string | undefined {\n const first = sorts.find((s) => (USER_ROSTER_SORT_KEYS as readonly string[]).includes(s.key));\n return first ? (encodeSorts([first]) ?? undefined) : undefined;\n}\n\n/* ── Labels ─────────────────────────────────────────────────────────────────── */\n\n/** The `userRoster` namespace: the preset's headers and the action menu's words. */\nexport interface UserRosterLabels {\n /** The identity column — the name with the address under it. */\n name: string;\n /** The separate address column (`emailColumn`). */\n email: string;\n role: string;\n state: string;\n created: string;\n lastLogin: string;\n /** A last login that never happened. */\n never: string;\n /** The actions column's (hidden) header. */\n actions: string;\n /** The action menu's button and panel — \"Actions for Ada Example\". */\n actionsFor: (name: string) => string;\n}\n\n// Defined in ./user-roster-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_USER_ROSTER_LABELS };\n\n/* ── UserIdentityCell ───────────────────────────────────────────────────────── */\n\nexport interface UserIdentityCellProps {\n /** First and last name (auth §3.2), shown in the READER's order. */\n person?: PersonName | null;\n /** A whole name, for an API that still sends `display_name` only. */\n name?: string | null;\n email?: string | null;\n /** The initials avatar before the name. Default true. */\n avatar?: boolean;\n /** After the name, on its line — a \"You\" chip, a demo mark. */\n badge?: ReactNode;\n /** Over the provider's locale. */\n locale?: string;\n className?: string;\n}\n\n/**\n * Who an account is, in one cell (§3.3): the initials avatar, the name in the reader's\n * order ({@link formatPersonName} — \"Example Ada\" in Hungarian) and the address under it\n * — the cell both rosters drew by hand. An account with no name yet (a migrated row\n * before `CompleteNameDialog`, an invitee) shows its address once, as the name.\n *\n * Both lines carry `data-private`, so keksdose's demo blur reaches them, and both end\n * in \"…\" at Normal only: from Large up they wrap (`truncate-until-large`, 0.33,\n * docs/text-size-harmonization.md §10.17), since a cut name or address is a finding there.\n */\nexport function UserIdentityCell({\n person,\n name,\n email,\n avatar = true,\n badge,\n locale: localeProp,\n className,\n}: UserIdentityCellProps) {\n const locale = useKitLocale(localeProp);\n const named = personLabel({ first: person?.first, last: person?.last, name }, locale);\n const address = (email ?? \"\").trim();\n return (\n <div className={cn(\"flex min-w-0 items-center gap-2.5\", className)}>\n {avatar && <UserAvatar person={person} name={name} email={email} size=\"sm\" />}\n <div className=\"min-w-0\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span data-private className=\"truncate-until-large font-medium text-primary\">\n {named || address}\n </span>\n {badge}\n </div>\n {named && address && (\n <div data-private className=\"truncate-until-large text-xs text-muted\">\n {address}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n/* ── The row's actions ──────────────────────────────────────────────────────── */\n\n/** One entry of a row's action menu (§4) — the kit's {@link RowAction} since 0.32,\n * which generalised it for every dense row (docs/text-size-harmonization.md §10.8). */\nexport type UserRowAction = RowAction;\n\n/** What `actions(row)` may return: entries, and `false` / `null` for the ones a\n * condition left out (`row.is_active && {…}`). */\nexport type UserRowActionList = RowActionList;\n\nexport interface UserRowActionsProps {\n actions: UserRowActionList;\n /** The account's name, for the menu's name — \"Actions for Ada Example\". */\n name: string;\n labels?: LabelOverride<UserRosterLabels>;\n}\n\n/**\n * A row's actions as one \"…\" button and a menu of them: keksdose had a strip of icon\n * buttons in the expanded row, Kurvenschmiede five text buttons in a cell — neither fits\n * a phone card, and both grow with every action §2.4 adds. The menu is portalled, so a\n * table's scroller cannot clip it; Escape and an outside press close it, and focus goes\n * back to the button, which is where a confirmation dialog then returns it.\n *\n * Since 0.32 this is {@link RowActions} with `collapse=\"menu\"` — the menu at every size —\n * named by the roster's own `actionsFor`.\n */\nexport function UserRowActions({ actions, name, labels: labelsProp }: UserRowActionsProps) {\n const labels = useKitLabels(\"userRoster\", DEFAULT_USER_ROSTER_LABELS, labelsProp);\n return (\n <RowActions\n actions={actions}\n name={name}\n collapse=\"menu\"\n labels={{ actionsFor: labels.actionsFor }}\n />\n );\n}\n\n/* ── The preset ─────────────────────────────────────────────────────────────── */\n\n/** How a row's role may be changed inline: `true`, or with reasons. */\nexport interface RoleEditing<R extends string> {\n /** Why the whole select is locked for this row — `\"self\"`, `\"last_admin\"`, or words. */\n disabledReason?: RoleLock;\n /** Why one role cannot be chosen for this row (a string: it goes inside an option). */\n optionDisabledReason?: (role: R) => string | undefined;\n /** The roles that may be chosen. Default: the whole vocabulary. */\n offered?: readonly R[];\n}\n\nexport interface UserRosterColumnsOptions<T extends UserRosterRow, R extends string = string> {\n /** The app's roles, as `RoleChip` reads them. */\n roles: RoleVocabulary<R>;\n /** The row's state chips. Default {@link adminUserStates}, which reads the contract's\n * fields (`is_active`, `email_verified`, `password_change_required_at`,\n * `deletion_scheduled_at`, `deletion_requested_at`). */\n stateOf?: (row: T) => readonly RosterState[];\n /** The app's own columns — keksdose's plan and key custody, Kurvenschmiede's owned\n * counts — after the dates, before the actions. */\n extra?: readonly DataTableColumn<T>[];\n /** The row's action menu (§4). Left out: no actions column. */\n actions?: (row: T) => UserRowActionList;\n /** The actions column's heading (0.46.1, keksdose dev#598 rework — a VISIBLE \"Actions\"\n * on every table with an actions column). Default: `labels.actions`, shown. A node\n * replaces it; `false` keeps the pre-0.46.1 heading, a screen reader's name only. */\n actionsHeader?: ReactNode | false;\n /**\n * Whether this row's role is a select rather than a chip, and with which reasons.\n * Left out, or `false`: a chip. kastlan's several roles are never a select here — they\n * are a \"Manage roles\" action that opens `RolesEditor`.\n */\n editableRole?: (row: T) => boolean | RoleEditing<R>;\n /** A role was picked in a row's select. The app confirms (`acknowledge`) and sends it. */\n onRoleChange?: (row: T, role: R) => void;\n /** The row's role(s). Default `row.roles ?? row.role`. */\n roleOf?: (row: T) => R | readonly R[] | null | undefined;\n /** A separate, sortable \"Email\" column (the server's `email` sort key) beside the\n * identity. Default false: the address is under the name already. */\n emailColumn?: boolean;\n /** The words, already resolved. Default English; {@link useUserRosterColumns} reads\n * the provider's `userRoster` namespace. */\n labels?: LabelOverride<UserRosterLabels>;\n}\n\nfunction rolesOf<T extends UserRosterRow, R extends string>(\n row: T,\n roleOf: UserRosterColumnsOptions<T, R>[\"roleOf\"],\n): R[] {\n const raw = roleOf ? roleOf(row) : ((row.roles ?? row.role) as R | readonly R[] | null | undefined);\n if (raw === null || raw === undefined || raw === \"\") return [];\n return typeof raw === \"string\" ? [raw as R] : [...(raw as readonly R[])];\n}\n\n/** The row's name in the reader's order — a component, because the order is the\n * READER's and the locale comes from the provider. */\nfunction RowName<T extends UserRosterRow>({ row, children }: { row: T; children: (name: string) => ReactNode }) {\n const locale = useKitLocale();\n const name = personLabel(\n { first: row.first_name, last: row.last_name, name: row.display_name, email: row.email },\n locale,\n );\n return <>{children(name)}</>;\n}\n\nfunction StateChips({ states }: { states: readonly RosterState[] }) {\n if (states.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\">\n {states.map((entry) => {\n const spec = typeof entry === \"string\" ? { state: entry } : entry;\n return (\n <AccountStateChip key={spec.state} state={spec.state} date={spec.date} tone={spec.tone} size=\"xs\">\n {spec.label}\n </AccountStateChip>\n );\n })}\n </div>\n );\n}\n\n/**\n * The roster's columns, in the contract's order (§3.3):\n *\n * | key | cell | sort | phone |\n * |---|---|---|---|\n * | `name` | {@link UserIdentityCell} | last, first | the card's headline |\n * | `email` | the address (`emailColumn` only) | address | hidden |\n * | `role` | `RoleChip`(s), or {@link RoleSelect} where `editableRole` | role | field |\n * | `state` | `AccountStateChip`s from `stateOf` | — | field |\n * | `created` | `dateColumn` | instant | hidden |\n * | `last_login` | `dateColumn`, relative, \"Never\" | instant | field |\n * | …`extra` | the app's | the app's | the app's |\n * | `actions` | {@link UserRowActions} | — | field |\n *\n * Pure: hand it resolved words, or use {@link useUserRosterColumns}. The keys are stable,\n * so an app may drop or move a column (`columns.filter((c) => c.key !== \"created\")`).\n * Under `serverPagination` the `sortBy`s only make the headers clickable; the app sends\n * the sort ({@link userRosterSort}).\n */\nexport function userRosterColumns<T extends UserRosterRow, R extends string = string>({\n roles,\n stateOf = adminUserStates,\n extra = [],\n actions,\n actionsHeader,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn = false,\n labels: labelsProp,\n}: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[] {\n const labels: UserRosterLabels = { ...DEFAULT_USER_ROSTER_LABELS, ...labelsProp };\n const roleWords = (row: T) =>\n rolesOf(row, roleOf)\n .map((role) => roles[role]?.label ?? role)\n .join(\", \");\n const columns: DataTableColumn<T>[] = [\n {\n key: \"name\",\n header: labels.name,\n cell: (row) => (\n <UserIdentityCell\n person={{ first: row.first_name, last: row.last_name }}\n name={row.display_name}\n email={row.email}\n />\n ),\n // By last name, then first (auth §3.2), as the server sorts `name`.\n sortBy: (row) => `${row.last_name ?? \"\"} ${row.first_name ?? \"\"}`.trim().toLocaleLowerCase() || row.email,\n // Either part, in either order, or the address (auth §10.6).\n filterBy: (row) =>\n [row.first_name, row.last_name, row.last_name, row.first_name, row.display_name, row.email]\n .filter(Boolean)\n .join(\" \"),\n mobilePrimary: true,\n },\n ];\n if (emailColumn) {\n columns.push({\n key: \"email\",\n header: labels.email,\n className: \"whitespace-nowrap\",\n cell: (row) => (\n <span data-private className=\"text-secondary\">\n {row.email}\n </span>\n ),\n sortBy: (row) => row.email.toLowerCase(),\n filterBy: (row) => row.email,\n mobileHidden: true,\n });\n }\n columns.push(\n {\n key: \"role\",\n header: labels.role,\n cell: (row) => {\n const held = rolesOf(row, roleOf);\n const editing = held.length <= 1 ? editableRole?.(row) : false;\n if (editing && onRoleChange) {\n const rules: RoleEditing<R> = editing === true ? {} : editing;\n return (\n <RowName row={row}>\n {(name) => (\n <RoleSelect<R>\n size=\"sm\"\n name={name}\n value={held[0] ?? null}\n roles={roles}\n offered={rules.offered}\n disabledReason={rules.disabledReason}\n optionDisabledReason={rules.optionDisabledReason}\n onChange={(role) => onRoleChange(row, role)}\n />\n )}\n </RowName>\n );\n }\n if (held.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\">\n {held.map((role) => (\n <RoleChip<R> key={role} value={role} roles={roles} size=\"xs\" />\n ))}\n </div>\n );\n },\n sortBy: roleWords,\n filter: {\n type: \"select\",\n getValue: (row) => rolesOf(row, roleOf)[0] ?? \"\",\n options: (Object.keys(roles) as R[]).map((value) => ({ value, label: roles[value].label })),\n },\n },\n {\n key: \"state\",\n header: labels.state,\n cell: (row) => <StateChips states={stateOf(row)} />,\n },\n dateColumn<T>({\n key: \"created\",\n header: labels.created,\n value: (row) => row.created_at ?? null,\n column: { mobileHidden: true },\n }),\n // How long ago rather than a date — the question is \"recently?\" — with the exact\n // time beside it (keksdose's last-seen column, Kurvenschmiede's; 0.32 §4: never only\n // in a tooltip).\n dateColumn<T>({\n key: \"last_login\",\n header: labels.lastLogin,\n value: (row) => row.last_login_at ?? null,\n display: \"relative\",\n empty: labels.never,\n }),\n ...extra,\n );\n if (actions) {\n columns.push({\n key: \"actions\",\n // On screen, as every actions column's heading is (0.46.1, keksdose dev#598 rework).\n header:\n actionsHeader === false ? <span className=\"sr-only\">{labels.actions}</span> : (actionsHeader ?? labels.actions),\n headerText: labels.actions,\n // The caller's own words only: the menu reads the provider's itself, and the\n // English defaults merged in above must not shadow them.\n cell: (row) => (\n <RowName row={row}>\n {(name) => <UserRowActions actions={actions(row)} name={name} labels={labelsProp} />}\n </RowName>\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px text-end\",\n // The menu button keeps its own click: a row link must not wrap it.\n noRowLink: true,\n });\n }\n return columns;\n}\n\n/**\n * {@link userRosterColumns} with the `userRoster` words from the provider, memoised on\n * the options — pass stable callbacks (`useCallback`) and a module-level vocabulary, or\n * the columns are rebuilt on every render.\n */\nexport function useUserRosterColumns<T extends UserRosterRow, R extends string = string>({\n roles,\n stateOf,\n extra,\n actions,\n actionsHeader,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels: labelsProp,\n}: UserRosterColumnsOptions<T, R>): DataTableColumn<T>[] {\n const labels = useKitLabels(\"userRoster\", DEFAULT_USER_ROSTER_LABELS, labelsProp);\n return useMemo(\n () =>\n userRosterColumns<T, R>({\n roles,\n stateOf,\n extra,\n actions,\n actionsHeader,\n editableRole,\n onRoleChange,\n roleOf,\n emailColumn,\n labels,\n }),\n [roles, stateOf, extra, actions, actionsHeader, editableRole, onRoleChange, roleOf, emailColumn, labels],\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from "react";
1
2
  import { type RowAction } from "./row-actions.js";
2
3
  import type { DataTableLabels } from "./data-table-labels.js";
3
4
  import type { DataTableColumn, DataTableDensity, DataTableDesktopSwipeActions, DataTableProps, MobileSwipeActions } from "./data-table.js";
@@ -11,10 +12,25 @@ export declare const SWIPE_ACTIONS_KEY = "__swipeActions";
11
12
  * never resized, never the totals row's header. */
12
13
  export declare function isTableOwnColumn(key: string): boolean;
13
14
  /**
14
- * A row's swipe plan as desktop row actions (dev#598): `end` then `start` — the physical
15
- * `right` / `left` only where the logical side is missing, as the card reads them — each
16
- * action once, told apart by its `id`, else by its label (two stages with one label would
17
- * be one button twice).
15
+ * An actions column's heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE
16
+ * "Actions" heading on every table with an actions column): the caller's node, else the
17
+ * word on screen; `false` is the pre-0.46.1 heading, a screen reader's name only.
18
+ */
19
+ export declare function actionsHeading(header: ReactNode | false | undefined, word: string): ReactNode;
20
+ /**
21
+ * A row's swipe plan as desktop row actions (dev#598) — what DataTable's
22
+ * `desktopSwipeActions` column draws, for an app that builds its own actions column (or
23
+ * a `RowActions` outside a table) from the same `mobileSwipeActions` plan, so the two
24
+ * cannot drift (public since 0.46.1, keksdose N3).
25
+ *
26
+ * `end` then `start` — the physical `right` / `left` only where the logical side is
27
+ * missing, as the card reads them — each action once, told apart by its `id`, else by its
28
+ * label (two stages with one label would be one button twice). Each button runs the
29
+ * swipe's own `onCommit`; a swipe's `danger` / `warning` / `info` tone carries over, any
30
+ * other reads `muted`. `options.omit` leaves an action out, `options.commit` gives it a
31
+ * `CommitScope` (see {@link DataTableDesktopSwipeActions}). A `null` plan is `[]`.
32
+ *
33
+ * <RowActions actions={swipePlanRowActions(plan(row), row, { omit })} name={row.title} />
18
34
  */
19
35
  export declare function swipePlanRowActions<T>(plan: MobileSwipeActions | null | undefined, row: T, options?: Pick<DataTableDesktopSwipeActions<T>, "omit" | "commit">): RowAction[];
20
36
  /**
@@ -26,9 +42,10 @@ export declare function swipePlanRowActions<T>(plan: MobileSwipeActions | null |
26
42
  * second special case beside the selection column: it then gets the cell padding,
27
43
  * the widths and the phone rules (`mobileHidden`) every other column gets.
28
44
  */
29
- export declare function useRowActionColumn<T>({ columns: columnsProp, rowActions, rowName, labels, density, mobileSwipeActions, desktopSwipeActions, }: {
45
+ export declare function useRowActionColumn<T>({ columns: columnsProp, rowActions, rowActionsHeader, rowName, labels, density, mobileSwipeActions, desktopSwipeActions, }: {
30
46
  columns: DataTableColumn<T>[];
31
47
  rowActions: DataTableProps<T>["rowActions"];
48
+ rowActionsHeader?: DataTableProps<T>["rowActionsHeader"];
32
49
  rowName: DataTableProps<T>["rowName"];
33
50
  labels: DataTableLabels;
34
51
  density: DataTableDensity;
@@ -8,6 +8,10 @@ const SWIPE_ACTIONS_KEY = "__swipeActions";
8
8
  function isTableOwnColumn(key) {
9
9
  return key === ROW_ACTIONS_KEY || key === SWIPE_ACTIONS_KEY;
10
10
  }
11
+ function actionsHeading(header, word) {
12
+ if (header === false) return /* @__PURE__ */ jsx("span", { className: "sr-only", children: word });
13
+ return header ?? word;
14
+ }
11
15
  function swipeTone(action) {
12
16
  switch (action.tone) {
13
17
  case "danger":
@@ -67,6 +71,7 @@ function toRowAction(a, row) {
67
71
  function useRowActionColumn({
68
72
  columns: columnsProp,
69
73
  rowActions,
74
+ rowActionsHeader,
70
75
  rowName,
71
76
  labels,
72
77
  density,
@@ -83,10 +88,11 @@ function useRowActionColumn({
83
88
  const swipeName = swipeOptions?.name;
84
89
  const swipeCollapse = swipeOptions?.collapse;
85
90
  const swipeCommit = swipeOptions?.commit;
91
+ const swipeHeader = swipeOptions?.header;
86
92
  const swipeColumn = useMemo(
87
93
  () => swipeColumnOn ? {
88
94
  key: SWIPE_ACTIONS_KEY,
89
- header: /* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.actions }),
95
+ header: actionsHeading(swipeHeader, labels.actions),
90
96
  cell: (row) => /* @__PURE__ */ jsx(
91
97
  RowActions,
92
98
  {
@@ -100,7 +106,8 @@ function useRowActionColumn({
100
106
  }
101
107
  ),
102
108
  className: "w-px whitespace-nowrap text-end",
103
- headClassName: "w-px",
109
+ // The heading over the buttons, at the row's end (0.46.1).
110
+ headClassName: "w-px text-end",
104
111
  // On a phone the same actions are the card's swipes.
105
112
  mobileHidden: true,
106
113
  noRowLink: true
@@ -113,6 +120,7 @@ function useRowActionColumn({
113
120
  swipeName,
114
121
  rowName,
115
122
  swipeCollapse,
123
+ swipeHeader,
116
124
  density,
117
125
  labels.actions
118
126
  ]
@@ -122,7 +130,7 @@ function useRowActionColumn({
122
130
  ...columnsProp,
123
131
  {
124
132
  key: ROW_ACTIONS_KEY,
125
- header: /* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.actions }),
133
+ header: actionsHeading(rowActionsHeader, labels.actions),
126
134
  // Through RowActions (0.32, §10.8): the icons inline at Normal, one "⋯"
127
135
  // menu at Large once there are two — every label at once does not fit a
128
136
  // row. Each control stops its own click: the row's handler already skips
@@ -139,13 +147,13 @@ function useRowActionColumn({
139
147
  }
140
148
  ),
141
149
  className: "w-px whitespace-nowrap text-end",
142
- headClassName: "w-px",
150
+ headClassName: "w-px text-end",
143
151
  // On a phone the same actions are the card's swipes.
144
152
  mobileHidden: true,
145
153
  noRowLink: true
146
154
  }
147
155
  ] : columnsProp,
148
- [columnsProp, rowActions, rowActionsFor, rowName, labels.actions, density]
156
+ [columnsProp, rowActions, rowActionsHeader, rowActionsFor, rowName, labels.actions, density]
149
157
  );
150
158
  const columns = useMemo(
151
159
  () => swipeColumn ? [...actionColumns, swipeColumn] : actionColumns,
@@ -172,6 +180,7 @@ function useRowActionColumn({
172
180
  export {
173
181
  ROW_ACTIONS_KEY,
174
182
  SWIPE_ACTIONS_KEY,
183
+ actionsHeading,
175
184
  isTableOwnColumn,
176
185
  swipePlanRowActions,
177
186
  useRowActionColumn
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-row-actions.tsx"],"sourcesContent":["import { isValidElement, useCallback, useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Pencil, Trash2 } from \"lucide-react\";\nimport { RowActions, type RowAction, type RowActionTone } from \"./row-actions\";\nimport type { SwipeAction } from \"./swipeable-row\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport type {\n DataTableColumn,\n DataTableDensity,\n DataTableDesktopSwipeActions,\n DataTableProps,\n DataTableRowAction,\n MobileSwipeActions,\n} from \"./data-table\";\n\n// DataTable's per-row actions (`rowActions`): the actions column on a wide screen and\n// the swipes on a phone card. Moved out of data-table.tsx whole (refactor audit\n// 2026-10, MOD-3 step 1); internal — the public types stay in data-table.tsx.\n\n/** The actions column's key. Not a column a caller declares, so it is kept out of the\n * column-settings panel. */\nexport const ROW_ACTIONS_KEY = \"__rowActions\";\n\n/** The key of the column derived from `mobileSwipeActions` (0.46, dev#598) — the\n * table's own too. */\nexport const SWIPE_ACTIONS_KEY = \"__swipeActions\";\n\n/** A column the TABLE adds, not the caller: out of the column settings and the count,\n * never resized, never the totals row's header. */\nexport function isTableOwnColumn(key: string): boolean {\n return key === ROW_ACTIONS_KEY || key === SWIPE_ACTIONS_KEY;\n}\n\n/** A curtain's tone, as an icon button's (keksdose's `toneOf`): only the tones that MEAN\n * something on a quiet icon carry over; a status colour, a hue or an app's `paint` stays\n * on the curtain and reads muted. */\nfunction swipeTone(action: SwipeAction): RowActionTone {\n switch (action.tone) {\n case \"danger\":\n case \"warning\":\n case \"info\":\n return action.tone;\n default:\n return \"muted\";\n }\n}\n\n/**\n * A row's swipe plan as desktop row actions (dev#598): `end` then `start` — the physical\n * `right` / `left` only where the logical side is missing, as the card reads them — each\n * action once, told apart by its `id`, else by its label (two stages with one label would\n * be one button twice).\n */\nexport function swipePlanRowActions<T>(\n plan: MobileSwipeActions | null | undefined,\n row: T,\n options: Pick<DataTableDesktopSwipeActions<T>, \"omit\" | \"commit\"> = {},\n): RowAction[] {\n if (!plan) return [];\n const out: RowAction[] = [];\n const seen = new Set<string>();\n for (const action of [...(plan.end ?? plan.right ?? []), ...(plan.start ?? plan.left ?? [])]) {\n const key = action.id ?? action.label;\n if (seen.has(key)) continue;\n seen.add(key);\n if (options.omit?.(action, row)) continue;\n const commit = typeof options.commit === \"function\" ? options.commit(action, row) : options.commit;\n out.push({\n key,\n label: action.label,\n icon: isValidElement(action.icon)\n ? action.icon\n : action.icon != null && action.icon !== false\n ? <>{action.icon}</>\n : undefined,\n tone: swipeTone(action),\n commit,\n onSelect: action.onCommit,\n });\n }\n return out;\n}\n\ninterface ResolvedRowAction<T> {\n key: string;\n label: string;\n icon: ReactNode;\n tone: \"default\" | \"danger\";\n swipe: \"start\" | \"end\" | false;\n action: DataTableRowAction<T>;\n}\n\nfunction resolveRowAction<T>(\n action: DataTableRowAction<T>,\n index: number,\n labels: DataTableLabels,\n): ResolvedRowAction<T> {\n const tone = action.tone ?? (action.kind === \"delete\" ? \"danger\" : \"default\");\n return {\n key: action.kind ?? `${index}:${action.label ?? \"\"}`,\n label: action.label ?? (action.kind === \"edit\" ? labels.edit : action.kind === \"delete\" ? labels.delete : \"\"),\n icon:\n action.icon ??\n (action.kind === \"edit\" ? <Pencil /> : action.kind === \"delete\" ? <Trash2 /> : null),\n tone,\n swipe: action.swipe ?? (tone === \"danger\" ? \"start\" : \"end\"),\n action,\n };\n}\n\n/** Ask, then act. */\nasync function runRowAction<T>(action: DataTableRowAction<T>, row: T): Promise<void> {\n if (action.confirm && !(await action.confirm(row))) return;\n action.onAction(row);\n}\n\n/** A resolved row action as {@link RowActions} takes it. Its `icon` is any node; a\n * non-element one (a string glyph) is wrapped, since RowAction wants an element. */\nfunction toRowAction<T>(a: ResolvedRowAction<T>, row: T): RowAction {\n return {\n key: a.key,\n label: a.label,\n icon: isValidElement(a.icon) ? a.icon : a.icon != null && a.icon !== false ? <>{a.icon}</> : undefined,\n tone: a.tone,\n disabledReason: a.action.disabledReason?.(row),\n onSelect: () => void runRowAction(a.action, row),\n };\n}\n\n/**\n * The caller's columns with the actions column appended, and the same actions as a\n * phone card's swipes (`swipesFromActions`; DataTable uses it unless the caller passes\n * `mobileSwipeActions`). Without `rowActions` the columns come back as given.\n *\n * The actions column is appended to the caller's columns rather than drawn as a\n * second special case beside the selection column: it then gets the cell padding,\n * the widths and the phone rules (`mobileHidden`) every other column gets.\n */\nexport function useRowActionColumn<T>({\n columns: columnsProp,\n rowActions,\n rowName,\n labels,\n density,\n mobileSwipeActions,\n desktopSwipeActions,\n}: {\n columns: DataTableColumn<T>[];\n rowActions: DataTableProps<T>[\"rowActions\"];\n rowName: DataTableProps<T>[\"rowName\"];\n labels: DataTableLabels;\n density: DataTableDensity;\n mobileSwipeActions?: DataTableProps<T>[\"mobileSwipeActions\"];\n desktopSwipeActions?: DataTableProps<T>[\"desktopSwipeActions\"];\n}): {\n columns: DataTableColumn<T>[];\n swipesFromActions: (row: T) => MobileSwipeActions | null;\n} {\n const rowActionsFor = useCallback(\n (row: T): ResolvedRowAction<T>[] =>\n rowActions\n ? (typeof rowActions === \"function\" ? rowActions(row) : rowActions)\n .filter((a) => !a.hidden?.(row))\n .map((a, i) => resolveRowAction(a, i, labels))\n : [],\n [rowActions, labels],\n );\n // dev#598: the desktop half of the caller's swipes. On by default, except beside\n // `rowActions`, whose column already is the desktop's (and whose swipes these usually\n // are). The options object is often a fresh literal per render; its parts are the deps.\n const swipeOptions = typeof desktopSwipeActions === \"object\" ? desktopSwipeActions : undefined;\n const swipeColumnOn =\n !!mobileSwipeActions && (desktopSwipeActions === undefined ? !rowActions : desktopSwipeActions !== false);\n const swipeOmit = swipeOptions?.omit;\n const swipeName = swipeOptions?.name;\n const swipeCollapse = swipeOptions?.collapse;\n const swipeCommit = swipeOptions?.commit;\n const swipeColumn = useMemo<DataTableColumn<T> | null>(\n () =>\n swipeColumnOn\n ? {\n key: SWIPE_ACTIONS_KEY,\n header: <span className=\"sr-only\">{labels.actions}</span>,\n cell: (row) => (\n <RowActions\n actions={swipePlanRowActions(mobileSwipeActions!(row), row, { omit: swipeOmit, commit: swipeCommit })}\n name={(swipeName ?? rowName)?.(row)}\n collapse={swipeCollapse}\n size={density === \"compact\" ? \"2xs\" : \"xs\"}\n // The last column's tooltips open toward the row's start.\n tooltipSide=\"start\"\n labels={{ actions: labels.actions }}\n className=\"justify-end\"\n />\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px\",\n // On a phone the same actions are the card's swipes.\n mobileHidden: true,\n noRowLink: true,\n }\n : null,\n [\n swipeColumnOn,\n mobileSwipeActions,\n swipeOmit,\n swipeCommit,\n swipeName,\n rowName,\n swipeCollapse,\n density,\n labels.actions,\n ],\n );\n const actionColumns = useMemo<DataTableColumn<T>[]>(\n () =>\n rowActions\n ? [\n ...columnsProp,\n {\n key: ROW_ACTIONS_KEY,\n header: <span className=\"sr-only\">{labels.actions}</span>,\n // Through RowActions (0.32, §10.8): the icons inline at Normal, one \"⋯\"\n // menu at Large once there are two — every label at once does not fit a\n // row. Each control stops its own click: the row's handler already skips\n // its controls, and this is for the caller who wraps the table in a\n // clickable element of their own.\n cell: (row) => (\n <RowActions\n actions={rowActionsFor(row).map((a) => toRowAction(a, row))}\n name={rowName?.(row)}\n size={density === \"compact\" ? \"2xs\" : \"xs\"}\n labels={{ actions: labels.actions }}\n className=\"justify-end\"\n />\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px\",\n // On a phone the same actions are the card's swipes.\n mobileHidden: true,\n noRowLink: true,\n },\n ]\n : columnsProp,\n [columnsProp, rowActions, rowActionsFor, rowName, labels.actions, density],\n );\n const columns = useMemo(\n () => (swipeColumn ? [...actionColumns, swipeColumn] : actionColumns),\n [actionColumns, swipeColumn],\n );\n const swipesFromActions = (row: T): MobileSwipeActions | null => {\n const swipes = rowActionsFor(row).filter((a) => a.swipe && !a.action.disabledReason?.(row));\n if (!swipes.length) return null;\n const toSwipe = (a: ResolvedRowAction<T>): SwipeAction => ({\n label: a.label,\n icon: a.icon,\n onCommit: () => void runRowAction(a.action, row),\n // The row action's tone, painted by SwipeableRow: its soft wash idle, its fill\n // and that fill's foreground armed (0.33).\n tone: a.tone === \"danger\" ? \"danger\" : \"brand\",\n });\n return {\n start: swipes.filter((a) => a.swipe === \"start\").map(toSwipe),\n end: swipes.filter((a) => a.swipe === \"end\").map(toSwipe),\n };\n };\n return { columns, swipesFromActions };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/data-table-row-actions.tsx"],"sourcesContent":["import { isValidElement, useCallback, useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Pencil, Trash2 } from \"lucide-react\";\nimport { RowActions, type RowAction, type RowActionTone } from \"./row-actions\";\nimport type { SwipeAction } from \"./swipeable-row\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport type {\n DataTableColumn,\n DataTableDensity,\n DataTableDesktopSwipeActions,\n DataTableProps,\n DataTableRowAction,\n MobileSwipeActions,\n} from \"./data-table\";\n\n// DataTable's per-row actions (`rowActions`): the actions column on a wide screen and\n// the swipes on a phone card. Moved out of data-table.tsx whole (refactor audit\n// 2026-10, MOD-3 step 1); internal — the public types stay in data-table.tsx.\n\n/** The actions column's key. Not a column a caller declares, so it is kept out of the\n * column-settings panel. */\nexport const ROW_ACTIONS_KEY = \"__rowActions\";\n\n/** The key of the column derived from `mobileSwipeActions` (0.46, dev#598) — the\n * table's own too. */\nexport const SWIPE_ACTIONS_KEY = \"__swipeActions\";\n\n/** A column the TABLE adds, not the caller: out of the column settings and the count,\n * never resized, never the totals row's header. */\nexport function isTableOwnColumn(key: string): boolean {\n return key === ROW_ACTIONS_KEY || key === SWIPE_ACTIONS_KEY;\n}\n\n/**\n * An actions column's heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE\n * \"Actions\" heading on every table with an actions column): the caller's node, else the\n * word on screen; `false` is the pre-0.46.1 heading, a screen reader's name only.\n */\nexport function actionsHeading(header: ReactNode | false | undefined, word: string): ReactNode {\n if (header === false) return <span className=\"sr-only\">{word}</span>;\n return header ?? word;\n}\n\n/** A curtain's tone, as an icon button's (keksdose's `toneOf`): only the tones that MEAN\n * something on a quiet icon carry over; a status colour, a hue or an app's `paint` stays\n * on the curtain and reads muted. */\nfunction swipeTone(action: SwipeAction): RowActionTone {\n switch (action.tone) {\n case \"danger\":\n case \"warning\":\n case \"info\":\n return action.tone;\n default:\n return \"muted\";\n }\n}\n\n/**\n * A row's swipe plan as desktop row actions (dev#598) — what DataTable's\n * `desktopSwipeActions` column draws, for an app that builds its own actions column (or\n * a `RowActions` outside a table) from the same `mobileSwipeActions` plan, so the two\n * cannot drift (public since 0.46.1, keksdose N3).\n *\n * `end` then `start` — the physical `right` / `left` only where the logical side is\n * missing, as the card reads them — each action once, told apart by its `id`, else by its\n * label (two stages with one label would be one button twice). Each button runs the\n * swipe's own `onCommit`; a swipe's `danger` / `warning` / `info` tone carries over, any\n * other reads `muted`. `options.omit` leaves an action out, `options.commit` gives it a\n * `CommitScope` (see {@link DataTableDesktopSwipeActions}). A `null` plan is `[]`.\n *\n * <RowActions actions={swipePlanRowActions(plan(row), row, { omit })} name={row.title} />\n */\nexport function swipePlanRowActions<T>(\n plan: MobileSwipeActions | null | undefined,\n row: T,\n options: Pick<DataTableDesktopSwipeActions<T>, \"omit\" | \"commit\"> = {},\n): RowAction[] {\n if (!plan) return [];\n const out: RowAction[] = [];\n const seen = new Set<string>();\n for (const action of [...(plan.end ?? plan.right ?? []), ...(plan.start ?? plan.left ?? [])]) {\n const key = action.id ?? action.label;\n if (seen.has(key)) continue;\n seen.add(key);\n if (options.omit?.(action, row)) continue;\n const commit = typeof options.commit === \"function\" ? options.commit(action, row) : options.commit;\n out.push({\n key,\n label: action.label,\n icon: isValidElement(action.icon)\n ? action.icon\n : action.icon != null && action.icon !== false\n ? <>{action.icon}</>\n : undefined,\n tone: swipeTone(action),\n commit,\n onSelect: action.onCommit,\n });\n }\n return out;\n}\n\ninterface ResolvedRowAction<T> {\n key: string;\n label: string;\n icon: ReactNode;\n tone: \"default\" | \"danger\";\n swipe: \"start\" | \"end\" | false;\n action: DataTableRowAction<T>;\n}\n\nfunction resolveRowAction<T>(\n action: DataTableRowAction<T>,\n index: number,\n labels: DataTableLabels,\n): ResolvedRowAction<T> {\n const tone = action.tone ?? (action.kind === \"delete\" ? \"danger\" : \"default\");\n return {\n key: action.kind ?? `${index}:${action.label ?? \"\"}`,\n label: action.label ?? (action.kind === \"edit\" ? labels.edit : action.kind === \"delete\" ? labels.delete : \"\"),\n icon:\n action.icon ??\n (action.kind === \"edit\" ? <Pencil /> : action.kind === \"delete\" ? <Trash2 /> : null),\n tone,\n swipe: action.swipe ?? (tone === \"danger\" ? \"start\" : \"end\"),\n action,\n };\n}\n\n/** Ask, then act. */\nasync function runRowAction<T>(action: DataTableRowAction<T>, row: T): Promise<void> {\n if (action.confirm && !(await action.confirm(row))) return;\n action.onAction(row);\n}\n\n/** A resolved row action as {@link RowActions} takes it. Its `icon` is any node; a\n * non-element one (a string glyph) is wrapped, since RowAction wants an element. */\nfunction toRowAction<T>(a: ResolvedRowAction<T>, row: T): RowAction {\n return {\n key: a.key,\n label: a.label,\n icon: isValidElement(a.icon) ? a.icon : a.icon != null && a.icon !== false ? <>{a.icon}</> : undefined,\n tone: a.tone,\n disabledReason: a.action.disabledReason?.(row),\n onSelect: () => void runRowAction(a.action, row),\n };\n}\n\n/**\n * The caller's columns with the actions column appended, and the same actions as a\n * phone card's swipes (`swipesFromActions`; DataTable uses it unless the caller passes\n * `mobileSwipeActions`). Without `rowActions` the columns come back as given.\n *\n * The actions column is appended to the caller's columns rather than drawn as a\n * second special case beside the selection column: it then gets the cell padding,\n * the widths and the phone rules (`mobileHidden`) every other column gets.\n */\nexport function useRowActionColumn<T>({\n columns: columnsProp,\n rowActions,\n rowActionsHeader,\n rowName,\n labels,\n density,\n mobileSwipeActions,\n desktopSwipeActions,\n}: {\n columns: DataTableColumn<T>[];\n rowActions: DataTableProps<T>[\"rowActions\"];\n rowActionsHeader?: DataTableProps<T>[\"rowActionsHeader\"];\n rowName: DataTableProps<T>[\"rowName\"];\n labels: DataTableLabels;\n density: DataTableDensity;\n mobileSwipeActions?: DataTableProps<T>[\"mobileSwipeActions\"];\n desktopSwipeActions?: DataTableProps<T>[\"desktopSwipeActions\"];\n}): {\n columns: DataTableColumn<T>[];\n swipesFromActions: (row: T) => MobileSwipeActions | null;\n} {\n const rowActionsFor = useCallback(\n (row: T): ResolvedRowAction<T>[] =>\n rowActions\n ? (typeof rowActions === \"function\" ? rowActions(row) : rowActions)\n .filter((a) => !a.hidden?.(row))\n .map((a, i) => resolveRowAction(a, i, labels))\n : [],\n [rowActions, labels],\n );\n // dev#598: the desktop half of the caller's swipes. On by default, except beside\n // `rowActions`, whose column already is the desktop's (and whose swipes these usually\n // are). The options object is often a fresh literal per render; its parts are the deps.\n const swipeOptions = typeof desktopSwipeActions === \"object\" ? desktopSwipeActions : undefined;\n const swipeColumnOn =\n !!mobileSwipeActions && (desktopSwipeActions === undefined ? !rowActions : desktopSwipeActions !== false);\n const swipeOmit = swipeOptions?.omit;\n const swipeName = swipeOptions?.name;\n const swipeCollapse = swipeOptions?.collapse;\n const swipeCommit = swipeOptions?.commit;\n const swipeHeader = swipeOptions?.header;\n const swipeColumn = useMemo<DataTableColumn<T> | null>(\n () =>\n swipeColumnOn\n ? {\n key: SWIPE_ACTIONS_KEY,\n header: actionsHeading(swipeHeader, labels.actions),\n cell: (row) => (\n <RowActions\n actions={swipePlanRowActions(mobileSwipeActions!(row), row, { omit: swipeOmit, commit: swipeCommit })}\n name={(swipeName ?? rowName)?.(row)}\n collapse={swipeCollapse}\n size={density === \"compact\" ? \"2xs\" : \"xs\"}\n // The last column's tooltips open toward the row's start.\n tooltipSide=\"start\"\n labels={{ actions: labels.actions }}\n className=\"justify-end\"\n />\n ),\n className: \"w-px whitespace-nowrap text-end\",\n // The heading over the buttons, at the row's end (0.46.1).\n headClassName: \"w-px text-end\",\n // On a phone the same actions are the card's swipes.\n mobileHidden: true,\n noRowLink: true,\n }\n : null,\n [\n swipeColumnOn,\n mobileSwipeActions,\n swipeOmit,\n swipeCommit,\n swipeName,\n rowName,\n swipeCollapse,\n swipeHeader,\n density,\n labels.actions,\n ],\n );\n const actionColumns = useMemo<DataTableColumn<T>[]>(\n () =>\n rowActions\n ? [\n ...columnsProp,\n {\n key: ROW_ACTIONS_KEY,\n header: actionsHeading(rowActionsHeader, labels.actions),\n // Through RowActions (0.32, §10.8): the icons inline at Normal, one \"⋯\"\n // menu at Large once there are two — every label at once does not fit a\n // row. Each control stops its own click: the row's handler already skips\n // its controls, and this is for the caller who wraps the table in a\n // clickable element of their own.\n cell: (row) => (\n <RowActions\n actions={rowActionsFor(row).map((a) => toRowAction(a, row))}\n name={rowName?.(row)}\n size={density === \"compact\" ? \"2xs\" : \"xs\"}\n labels={{ actions: labels.actions }}\n className=\"justify-end\"\n />\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px text-end\",\n // On a phone the same actions are the card's swipes.\n mobileHidden: true,\n noRowLink: true,\n },\n ]\n : columnsProp,\n [columnsProp, rowActions, rowActionsHeader, rowActionsFor, rowName, labels.actions, density],\n );\n const columns = useMemo(\n () => (swipeColumn ? [...actionColumns, swipeColumn] : actionColumns),\n [actionColumns, swipeColumn],\n );\n const swipesFromActions = (row: T): MobileSwipeActions | null => {\n const swipes = rowActionsFor(row).filter((a) => a.swipe && !a.action.disabledReason?.(row));\n if (!swipes.length) return null;\n const toSwipe = (a: ResolvedRowAction<T>): SwipeAction => ({\n label: a.label,\n icon: a.icon,\n onCommit: () => void runRowAction(a.action, row),\n // The row action's tone, painted by SwipeableRow: its soft wash idle, its fill\n // and that fill's foreground armed (0.33).\n tone: a.tone === \"danger\" ? \"danger\" : \"brand\",\n });\n return {\n start: swipes.filter((a) => a.swipe === \"start\").map(toSwipe),\n end: swipes.filter((a) => a.swipe === \"end\").map(toSwipe),\n };\n };\n return { columns, swipesFromActions };\n}\n"],"mappings":"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","names":[]}
@@ -568,6 +568,14 @@ export interface DataTableProps<T> {
568
568
  * `mobileSwipeActions` wins over the swipes derived from here.
569
569
  */
570
570
  rowActions?: DataTableRowAction<T>[] | ((row: T) => DataTableRowAction<T>[]);
571
+ /**
572
+ * The `rowActions` column's heading (0.46.1, keksdose dev#598 rework — Marcel: a
573
+ * VISIBLE "Actions" heading on every table with an actions column). Default: the
574
+ * provider's `dataTable.actions` ("Actions"), shown, end-aligned over the buttons. A
575
+ * node replaces it; `false` keeps the pre-0.46.1 heading, named for a screen reader
576
+ * only.
577
+ */
578
+ rowActionsHeader?: ReactNode | false;
571
579
  /**
572
580
  * The bar above the table — a search field, filter chips, a create button. Inside
573
581
  * the table's frame, above the header, on desktop and phone alike (resource-list
@@ -656,6 +664,13 @@ export interface DataTableDesktopSwipeActions<T> {
656
664
  * no commit scope (the plan decides).
657
665
  */
658
666
  commit?: CommitScope | ((action: SwipeAction, row: T) => CommitScope | undefined);
667
+ /**
668
+ * The column's heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE "Actions"
669
+ * heading on every table with an actions column). Default: the provider's
670
+ * `dataTable.actions` ("Actions"), shown, end-aligned over the buttons. A node
671
+ * replaces it; `false` keeps the 0.46.0 heading, named for a screen reader only.
672
+ */
673
+ header?: ReactNode | false;
659
674
  }
660
675
  /**
661
676
  * The phone sort control's strings (keksdose K10). They are `dataTable` strings —
@@ -678,4 +693,4 @@ export interface DataTableSortLabels {
678
693
  * (DataTableLabels extends DataTableSortLabels; one source for the English). */
679
694
  export declare const DEFAULT_DATA_TABLE_SORT_LABELS: DataTableSortLabels;
680
695
  import { type TableUrlSync } from "./use-table-state.js";
681
- export declare function DataTable<T>({ rows, columns: columnsProp, rowKey, defaultPageSize, onRowClick, rowHref, expandedRow, isExpanded, rowClassName, rowAttributes, paginated, empty, storageKey, urlSync, fillHeight, maxBodyHeight, serverPagination, filters: filtersProp, onFiltersChange, sorts: sortsProp, onSortsChange, pageSize: pageSizeProp, onPageSizeChange, selection, labels: labelsProp, locale: localeProp, storageKeyPrefix: storageKeyPrefixProp, mobileExpandAsDialog, mobileDetailsInRow, rowName, mobileDialogBackCloses, mobileGroupBy, mobileGroupLabel, mobileCard, mobileSort, mobileSwipeActions, desktopSwipeActions, leadingRow, density, sortCycle, chrome, columnSettings: columnSettingsProp, resizable: resizableProp, multiSort: multiSortProp, frame, edgeFade, className, rowActions, toolbar, footerLabel, footer: footerProp, }: DataTableProps<T>): import("react").JSX.Element;
696
+ export declare function DataTable<T>({ rows, columns: columnsProp, rowKey, defaultPageSize, onRowClick, rowHref, expandedRow, isExpanded, rowClassName, rowAttributes, paginated, empty, storageKey, urlSync, fillHeight, maxBodyHeight, serverPagination, filters: filtersProp, onFiltersChange, sorts: sortsProp, onSortsChange, pageSize: pageSizeProp, onPageSizeChange, selection, labels: labelsProp, locale: localeProp, storageKeyPrefix: storageKeyPrefixProp, mobileExpandAsDialog, mobileDetailsInRow, rowName, mobileDialogBackCloses, mobileGroupBy, mobileGroupLabel, mobileCard, mobileSort, mobileSwipeActions, desktopSwipeActions, leadingRow, density, sortCycle, chrome, columnSettings: columnSettingsProp, resizable: resizableProp, multiSort: multiSortProp, frame, edgeFade, className, rowActions, rowActionsHeader, toolbar, footerLabel, footer: footerProp, }: DataTableProps<T>): import("react").JSX.Element;
@@ -104,6 +104,7 @@ function DataTable({
104
104
  edgeFade = false,
105
105
  className,
106
106
  rowActions,
107
+ rowActionsHeader,
107
108
  toolbar,
108
109
  footerLabel,
109
110
  footer: footerProp
@@ -123,6 +124,7 @@ function DataTable({
123
124
  const { columns, swipesFromActions } = useRowActionColumn({
124
125
  columns: columnsProp,
125
126
  rowActions,
127
+ rowActionsHeader,
126
128
  rowName,
127
129
  labels,
128
130
  density,
@@ -321,6 +323,14 @@ function DataTable({
321
323
  const columnsCountLabel = labels.columnsCount(visibleCount - ownVisible, totalCount - ownColumns);
322
324
  const isMdUp = !usePhoneLayout();
323
325
  const desktopScroller = useRef(null);
326
+ const measuresVisibleWidth = isMdUp && !!expandedRow;
327
+ useEffect(() => {
328
+ const el = desktopScroller.current;
329
+ if (!measuresVisibleWidth || !el || typeof ResizeObserver === "undefined") return;
330
+ const ro = new ResizeObserver(() => el.style.setProperty("--dt-visible-w", `${el.clientWidth}px`));
331
+ ro.observe(el);
332
+ return () => ro.disconnect();
333
+ }, [measuresVisibleWidth]);
324
334
  const edgeFadeProps = useEdgeFade(desktopScroller, edgeFade && isMdUp);
325
335
  const mobileColumns = visibleColumns.filter((c) => !c.mobileHidden);
326
336
  const mobilePrimaryCol = mobileColumns.find((c) => c.mobilePrimary) ?? mobileColumns[0] ?? null;
@@ -801,7 +811,16 @@ function DataTable({
801
811
  ]
802
812
  }
803
813
  ),
804
- expansion && /* @__PURE__ */ jsx("tr", { id: disclosure ? detailId : void 0, className: "bg-surface-2 border-t border-subtle", children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0", children: /* @__PURE__ */ jsx("div", { className: cn("w-0 min-w-full", compact ? "px-2 py-2" : "px-3 py-3"), children: expansion }) }) })
814
+ expansion && /* @__PURE__ */ jsx("tr", { id: disclosure ? detailId : void 0, className: "bg-surface-2 border-t border-subtle", children: /* @__PURE__ */ jsx("td", { colSpan: totalColSpan, className: "p-0", children: /* @__PURE__ */ jsx("div", { className: "w-0 min-w-full", children: /* @__PURE__ */ jsx(
815
+ "div",
816
+ {
817
+ className: cn(
818
+ "sticky start-0 w-[var(--dt-visible-w,100%)] max-w-full",
819
+ compact ? "px-2 py-2" : "px-3 py-3"
820
+ ),
821
+ children: expansion
822
+ }
823
+ ) }) }) })
805
824
  ] }, key);
806
825
  }),
807
826
  slice.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(