@alfakit/react 0.1.1 → 0.1.6

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 (219) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/components/actions/action-button.d.ts.map +1 -1
  3. package/dist/components/actions/action-button.js +5 -4
  4. package/dist/components/actions/action-button.js.map +1 -1
  5. package/dist/components/actions/action-modals.d.ts.map +1 -1
  6. package/dist/components/actions/action-modals.js +6 -5
  7. package/dist/components/actions/action-modals.js.map +1 -1
  8. package/dist/components/ai/copilot.d.ts +3 -1
  9. package/dist/components/ai/copilot.d.ts.map +1 -1
  10. package/dist/components/ai/copilot.js +4 -1
  11. package/dist/components/ai/copilot.js.map +1 -1
  12. package/dist/components/cms/live-preview.js +3 -2
  13. package/dist/components/cms/live-preview.js.map +1 -1
  14. package/dist/components/cms/page.js +1 -1
  15. package/dist/components/fields/builder.js +15 -6
  16. package/dist/components/fields/builder.js.map +1 -1
  17. package/dist/components/fields/color-picker.js +1 -0
  18. package/dist/components/fields/color-picker.js.map +1 -1
  19. package/dist/components/fields/file-upload.js +11 -10
  20. package/dist/components/fields/file-upload.js.map +1 -1
  21. package/dist/components/fields/image-editor-dialog.js +4 -2
  22. package/dist/components/fields/image-editor-dialog.js.map +1 -1
  23. package/dist/components/fields/item-actions.js +10 -10
  24. package/dist/components/fields/item-actions.js.map +1 -1
  25. package/dist/components/fields/key-value.d.ts.map +1 -1
  26. package/dist/components/fields/key-value.js +3 -3
  27. package/dist/components/fields/key-value.js.map +1 -1
  28. package/dist/components/fields/modal-table-select.js +6 -5
  29. package/dist/components/fields/modal-table-select.js.map +1 -1
  30. package/dist/components/fields/one-time-code-input.js +1 -1
  31. package/dist/components/fields/one-time-code-input.js.map +1 -1
  32. package/dist/components/fields/repeater-table.js +12 -1
  33. package/dist/components/fields/repeater-table.js.map +1 -1
  34. package/dist/components/fields/repeater.js +2 -2
  35. package/dist/components/fields/repeater.js.map +1 -1
  36. package/dist/components/fields/rich-editor.js +2 -1
  37. package/dist/components/fields/rich-editor.js.map +1 -1
  38. package/dist/components/fields/select.js +3 -3
  39. package/dist/components/fields/select.js.map +1 -1
  40. package/dist/components/file-manager/file-manager.js +1 -1
  41. package/dist/components/file-manager/file-manager.js.map +1 -1
  42. package/dist/components/form/form-block.d.ts.map +1 -1
  43. package/dist/components/form/form-block.js +14 -2
  44. package/dist/components/form/form-block.js.map +1 -1
  45. package/dist/components/form/form-context.js +7 -3
  46. package/dist/components/form/form-context.js.map +1 -1
  47. package/dist/components/kanban/kanban.js +4 -3
  48. package/dist/components/kanban/kanban.js.map +1 -1
  49. package/dist/components/layout/fused-group.js +1 -0
  50. package/dist/components/layout/fused-group.js.map +1 -1
  51. package/dist/components/layout/section.js +2 -2
  52. package/dist/components/layout/section.js.map +1 -1
  53. package/dist/components/layout/tabs.d.ts.map +1 -1
  54. package/dist/components/layout/tabs.js +54 -15
  55. package/dist/components/layout/tabs.js.map +1 -1
  56. package/dist/components/panel/app-sidebar.d.ts +2 -3
  57. package/dist/components/panel/app-sidebar.d.ts.map +1 -1
  58. package/dist/components/panel/app-sidebar.js +1 -24
  59. package/dist/components/panel/app-sidebar.js.map +1 -1
  60. package/dist/components/panel/brand.d.ts +10 -0
  61. package/dist/components/panel/brand.d.ts.map +1 -0
  62. package/dist/components/panel/brand.js +37 -0
  63. package/dist/components/panel/brand.js.map +1 -0
  64. package/dist/components/panel/global-search.js +1 -0
  65. package/dist/components/panel/global-search.js.map +1 -1
  66. package/dist/components/panel/index.d.ts +2 -1
  67. package/dist/components/panel/shortcut-keys.js +1 -0
  68. package/dist/components/panel/shortcut-keys.js.map +1 -1
  69. package/dist/components/panel/tenant-menu.js +2 -2
  70. package/dist/components/panel/tenant-menu.js.map +1 -1
  71. package/dist/components/panel/topbar.js +1 -1
  72. package/dist/components/realtime/live-records-pill.d.ts.map +1 -1
  73. package/dist/components/realtime/live-records-pill.js +6 -3
  74. package/dist/components/realtime/live-records-pill.js.map +1 -1
  75. package/dist/components/table/cells.d.ts +6 -0
  76. package/dist/components/table/cells.d.ts.map +1 -1
  77. package/dist/components/table/cells.js +15 -3
  78. package/dist/components/table/cells.js.map +1 -1
  79. package/dist/components/table/column-manager.js +8 -7
  80. package/dist/components/table/column-manager.js.map +1 -1
  81. package/dist/components/table/content-layout.js +2 -1
  82. package/dist/components/table/content-layout.js.map +1 -1
  83. package/dist/components/table/grouping.js +11 -1
  84. package/dist/components/table/grouping.js.map +1 -1
  85. package/dist/components/table/pagination.js +16 -13
  86. package/dist/components/table/pagination.js.map +1 -1
  87. package/dist/components/table/table-block.js +14 -4
  88. package/dist/components/table/table-block.js.map +1 -1
  89. package/dist/components/ui/dialog.d.ts.map +1 -1
  90. package/dist/components/ui/dialog.js +3 -1
  91. package/dist/components/ui/dialog.js.map +1 -1
  92. package/dist/components/ui/popover.d.ts.map +1 -1
  93. package/dist/components/ui/popover.js +3 -1
  94. package/dist/components/ui/popover.js.map +1 -1
  95. package/dist/components/ui/sheet.d.ts.map +1 -1
  96. package/dist/components/ui/sheet.js +3 -1
  97. package/dist/components/ui/sheet.js.map +1 -1
  98. package/dist/components/ui/tooltip.js +3 -1
  99. package/dist/components/ui/tooltip.js.map +1 -1
  100. package/dist/components.css +347 -0
  101. package/dist/context.d.ts +5 -0
  102. package/dist/context.d.ts.map +1 -1
  103. package/dist/context.js +5 -1
  104. package/dist/context.js.map +1 -1
  105. package/dist/create-alfakit.d.ts.map +1 -1
  106. package/dist/create-alfakit.js +8 -5
  107. package/dist/create-alfakit.js.map +1 -1
  108. package/dist/eject-manifest.json +16 -3
  109. package/dist/index.d.ts +6 -3
  110. package/dist/index.js +24 -21
  111. package/dist/layouts/app-layout.d.ts.map +1 -1
  112. package/dist/layouts/app-layout.js +2 -29
  113. package/dist/layouts/app-layout.js.map +1 -1
  114. package/dist/layouts/auth-layout.js +2 -2
  115. package/dist/layouts/auth-layout.js.map +1 -1
  116. package/dist/layouts/fullscreen-layout.js +1 -1
  117. package/dist/layouts/fullscreen-layout.js.map +1 -1
  118. package/dist/layouts/lazy-layouts.js +15 -0
  119. package/dist/layouts/lazy-layouts.js.map +1 -0
  120. package/dist/layouts/max-widths.js +34 -0
  121. package/dist/layouts/max-widths.js.map +1 -0
  122. package/dist/layouts/panel-layouts.d.ts +10 -0
  123. package/dist/layouts/panel-layouts.d.ts.map +1 -0
  124. package/dist/layouts/panel-layouts.js +20 -0
  125. package/dist/layouts/panel-layouts.js.map +1 -0
  126. package/dist/layouts/top-nav-layout.js +2 -2
  127. package/dist/layouts/top-nav-layout.js.map +1 -1
  128. package/dist/pages/page.d.ts.map +1 -1
  129. package/dist/pages/page.js +12 -7
  130. package/dist/pages/page.js.map +1 -1
  131. package/dist/standalone/alfakit-component.js +11 -6
  132. package/dist/standalone/alfakit-component.js.map +1 -1
  133. package/dist/standalone/alfakit-toaster.d.ts.map +1 -1
  134. package/dist/standalone/alfakit-toaster.js +8 -1
  135. package/dist/standalone/alfakit-toaster.js.map +1 -1
  136. package/dist/standalone.css +54 -0
  137. package/dist/starter-kit/dashboard-widgets.d.ts +33 -0
  138. package/dist/starter-kit/dashboard-widgets.d.ts.map +1 -0
  139. package/dist/starter-kit/dashboard-widgets.js +53 -0
  140. package/dist/starter-kit/dashboard-widgets.js.map +1 -0
  141. package/dist/starter-kit/impersonation-banner.js +34 -0
  142. package/dist/starter-kit/impersonation-banner.js.map +1 -0
  143. package/dist/starter-kit/index.d.ts +3 -2
  144. package/dist/starter-kit/index.js +3 -2
  145. package/dist/starter-kit/starter-kit.d.ts +6 -0
  146. package/dist/starter-kit/starter-kit.d.ts.map +1 -1
  147. package/dist/starter-kit/starter-kit.js +18 -7
  148. package/dist/starter-kit/starter-kit.js.map +1 -1
  149. package/dist/starter-kit/widgets-loaded.js +21 -5
  150. package/dist/starter-kit/widgets-loaded.js.map +1 -1
  151. package/dist/starter-kit/widgets-triggers.js +35 -3
  152. package/dist/starter-kit/widgets-triggers.js.map +1 -1
  153. package/dist/starter-kit/widgets.d.ts +13 -5
  154. package/dist/starter-kit/widgets.d.ts.map +1 -1
  155. package/dist/starter-kit/widgets.js +56 -18
  156. package/dist/starter-kit/widgets.js.map +1 -1
  157. package/dist/styles.css +15 -2
  158. package/dist/themes.css +8 -335
  159. package/package.json +2 -2
  160. package/src/components/actions/action-button.tsx +7 -4
  161. package/src/components/actions/action-modals.tsx +13 -1
  162. package/src/components/ai/copilot.tsx +14 -1
  163. package/src/components/cms/live-preview.tsx +5 -2
  164. package/src/components/fields/builder.tsx +14 -5
  165. package/src/components/fields/color-picker.tsx +1 -0
  166. package/src/components/fields/file-upload.tsx +21 -10
  167. package/src/components/fields/image-editor-dialog.tsx +7 -2
  168. package/src/components/fields/item-actions.tsx +32 -9
  169. package/src/components/fields/key-value.tsx +3 -2
  170. package/src/components/fields/modal-table-select.tsx +8 -3
  171. package/src/components/fields/one-time-code-input.tsx +1 -1
  172. package/src/components/fields/repeater-table.tsx +12 -1
  173. package/src/components/fields/repeater.tsx +2 -3
  174. package/src/components/fields/rich-editor.tsx +3 -0
  175. package/src/components/fields/select.tsx +7 -3
  176. package/src/components/file-manager/file-manager.tsx +1 -1
  177. package/src/components/form/form-block.tsx +26 -1
  178. package/src/components/form/form-context.tsx +3 -6
  179. package/src/components/kanban/kanban.tsx +16 -3
  180. package/src/components/layout/fused-group.tsx +5 -1
  181. package/src/components/layout/section.tsx +2 -2
  182. package/src/components/layout/tabs.tsx +72 -17
  183. package/src/components/panel/app-sidebar.tsx +2 -38
  184. package/src/components/panel/brand.tsx +46 -0
  185. package/src/components/panel/global-search.tsx +1 -1
  186. package/src/components/panel/shortcut-keys.tsx +4 -1
  187. package/src/components/panel/tenant-menu.tsx +2 -2
  188. package/src/components/realtime/live-records-pill.tsx +4 -3
  189. package/src/components/table/cells.tsx +27 -5
  190. package/src/components/table/column-manager.tsx +12 -10
  191. package/src/components/table/content-layout.tsx +5 -1
  192. package/src/components/table/grouping.tsx +15 -1
  193. package/src/components/table/pagination.tsx +16 -9
  194. package/src/components/table/table-block.tsx +25 -12
  195. package/src/components/ui/dialog.tsx +5 -1
  196. package/src/components/ui/popover.tsx +5 -1
  197. package/src/components/ui/sheet.tsx +5 -1
  198. package/src/components/ui/tooltip.tsx +5 -1
  199. package/src/context.tsx +10 -0
  200. package/src/create-alfakit.tsx +11 -2
  201. package/src/index.ts +2 -0
  202. package/src/layouts/app-layout.tsx +4 -28
  203. package/src/layouts/auth-layout.tsx +2 -2
  204. package/src/layouts/fullscreen-layout.tsx +1 -1
  205. package/src/layouts/lazy-layouts.ts +14 -0
  206. package/src/layouts/max-widths.ts +29 -0
  207. package/src/layouts/panel-layouts.ts +17 -0
  208. package/src/layouts/top-nav-layout.tsx +2 -2
  209. package/src/pages/page.tsx +21 -12
  210. package/src/standalone/alfakit-component.tsx +18 -7
  211. package/src/standalone/alfakit-toaster.tsx +21 -2
  212. package/src/starter-kit/dashboard-widgets.tsx +78 -0
  213. package/src/starter-kit/impersonation-banner.tsx +48 -0
  214. package/src/starter-kit/index.ts +2 -0
  215. package/src/starter-kit/starter-kit.tsx +31 -4
  216. package/src/starter-kit/widgets-loaded.tsx +23 -1
  217. package/src/starter-kit/widgets-triggers.tsx +37 -2
  218. package/src/starter-kit/widgets.tsx +95 -20
  219. package/src/styles.css +15 -2
@@ -0,0 +1,46 @@
1
+ import { Link } from '@inertiajs/react'
2
+ import { usePanel } from '../../context'
3
+ import { cn } from '../../lib/utils'
4
+
5
+ /**
6
+ * The panel's logo (or initial) and name, linking to the panel home. Its own module, so the
7
+ * auth layout doesn't import the sidebar.
8
+ */
9
+ export function Brand({ className }: { className?: string }) {
10
+ const panel = usePanel()
11
+
12
+ return (
13
+ <Link
14
+ href={panel.url ?? '/'}
15
+ className={cn('flex items-center gap-2 overflow-hidden', className)}
16
+ >
17
+ {panel.brandLogo ? (
18
+ <>
19
+ <img
20
+ src={panel.brandLogo}
21
+ alt={panel.brandName}
22
+ style={{ height: panel.brandLogoHeight ?? '1.75rem' }}
23
+ className={cn('max-w-full object-contain', panel.darkModeBrandLogo && 'dark:hidden')}
24
+ />
25
+ {panel.darkModeBrandLogo && (
26
+ <img
27
+ src={panel.darkModeBrandLogo}
28
+ alt={panel.brandName}
29
+ style={{ height: panel.brandLogoHeight ?? '1.75rem' }}
30
+ className="hidden max-w-full object-contain dark:block"
31
+ />
32
+ )}
33
+ </>
34
+ ) : (
35
+ <>
36
+ <span className="bg-primary text-primary-foreground flex size-8 shrink-0 items-center justify-center rounded-lg text-sm font-bold">
37
+ {panel.brandName.charAt(0)}
38
+ </span>
39
+ <span className="truncate text-sm font-semibold group-data-[collapsible=icon]:hidden">
40
+ {panel.brandName}
41
+ </span>
42
+ </>
43
+ )}
44
+ </Link>
45
+ )
46
+ }
@@ -133,7 +133,7 @@ export function GlobalSearch({ variant = 'topbar' }: { variant?: 'topbar' | 'sid
133
133
  </span>
134
134
  )}
135
135
  {hydrated && suffix?.type === 'keyBinding' && (
136
- <kbd className="bg-muted hidden rounded px-1 font-mono text-[10px] sm:inline">
136
+ <kbd dir="ltr" className="bg-muted hidden rounded px-1 font-mono text-[10px] sm:inline">
137
137
  {formatKeyBinding(suffix.keyBinding)}
138
138
  </kbd>
139
139
  )}
@@ -24,7 +24,10 @@ export function ShortcutKeys({ binding, className }: { binding: string; classNam
24
24
  {t('shortcuts.then', {}, 'then')}
25
25
  </span>
26
26
  )}
27
- <kbd className="bg-muted text-muted-foreground rounded border px-1.5 font-mono text-[10px] leading-5">
27
+ <kbd
28
+ dir="ltr"
29
+ className="bg-muted text-muted-foreground rounded border px-1.5 font-mono text-[10px] leading-5"
30
+ >
28
31
  {step}
29
32
  </kbd>
30
33
  </span>
@@ -151,7 +151,7 @@ export function TenantMenu({ variant = 'sidebar' }: { variant?: 'sidebar' | 'top
151
151
  <DropdownMenuItem asChild>
152
152
  <Link href={tenancy.profileUrl} className="w-full">
153
153
  <Icon name="alfakit::panels.tenant-menu.profile-button" fallback={SettingsIcon} />{' '}
154
- {t('tenancy.profile', {}, 'Settings')}
154
+ {tenancy.profileLabel ?? t('tenancy.profile', {}, 'Settings')}
155
155
  </Link>
156
156
  </DropdownMenuItem>
157
157
  )
@@ -164,7 +164,7 @@ export function TenantMenu({ variant = 'sidebar' }: { variant?: 'sidebar' | 'top
164
164
  <DropdownMenuItem asChild>
165
165
  <Link href={tenancy.registrationUrl} className="w-full">
166
166
  <Icon name="alfakit::panels.tenant-menu.registration-button" fallback={PlusIcon} />{' '}
167
- {t('tenancy.register', {}, 'New')}
167
+ {tenancy.registrationLabel ?? t('tenancy.register', {}, 'New')}
168
168
  </Link>
169
169
  </DropdownMenuItem>
170
170
  )
@@ -1,7 +1,7 @@
1
1
  import { LiveUpdates, shownRecordKeys, type LiveUpdatesSchema, type RecordKey } from '@alfakit/core'
2
2
  import { ArrowUpIcon } from 'lucide-react'
3
3
  import { useEffect, useMemo } from 'react'
4
- import { useChoice, usePollComponent } from '../../context'
4
+ import { useChoice, usePollComponent, useRefreshComponents } from '../../context'
5
5
  import { useStore } from '../../hooks/use-store'
6
6
  import { Button } from '../ui/button'
7
7
 
@@ -16,15 +16,16 @@ export function useLiveUpdates(
16
16
  options: { polling?: boolean; records?: ReadonlyArray<{ key?: RecordKey }> | null } = {},
17
17
  ): LiveUpdates | null {
18
18
  const poll = usePollComponent()
19
+ const refresh = useRefreshComponents()
19
20
  const key = live ? JSON.stringify(live) : ''
20
21
  const polling = options.polling
21
22
  const records = options.records
22
23
 
23
24
  const controller = useMemo(
24
- () => (live ? new LiveUpdates({ live, target, polling, poll }) : null),
25
+ () => (live ? new LiveUpdates({ live, target, polling, poll, refresh }) : null),
25
26
  // `live` is compared by value (`key`): a reload brings a new object with the same settings.
26
27
  // eslint-disable-next-line react-hooks/exhaustive-deps
27
- [key, target, polling, poll],
28
+ [key, target, polling, poll, refresh],
28
29
  )
29
30
 
30
31
  // Updates of records on other pages don't refresh the table.
@@ -26,7 +26,7 @@ import {
26
26
  } from '@alfakit/core'
27
27
  import { router } from '@inertiajs/react'
28
28
  import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'
29
- import { createElement, useEffect, useRef, useState } from 'react'
29
+ import { createElement, useEffect, useId, useRef, useState } from 'react'
30
30
  import { usePanel, useRegistry, useT } from '../../context'
31
31
  import { extraProps } from '../../lib/attributes'
32
32
  import {
@@ -60,15 +60,22 @@ export interface CellProps {
60
60
  recordKey: RecordKey
61
61
  token: string
62
62
  component: string
63
+ /**
64
+ * Set when this renderer was picked from the registry (`column:<type>`): it then renders
65
+ * the built-in cell instead of looking itself up again, so an ejected `TableCellContent`
66
+ * registered for a column type doesn't render itself forever.
67
+ */
68
+ fromRegistry?: boolean
63
69
  }
64
70
 
65
71
  export function TableCellContent(props: CellProps) {
66
72
  const { column } = props
73
+ const registry = useRegistry()
67
74
  // A component registered as `column:<type>` replaces the built-in cell renderer.
68
- const Custom = useRegistry().resolve(columnRegistryKey(column))
75
+ const Custom = props.fromRegistry ? undefined : registry.resolve(columnRegistryKey(column))
69
76
 
70
77
  if (Custom) {
71
- return createElement(Custom, props)
78
+ return createElement(Custom, { ...props, fromRegistry: true })
72
79
  }
73
80
 
74
81
  switch (column.type) {
@@ -587,6 +594,8 @@ function NativeSelectCell(props: CellProps) {
587
594
  cell.state === null || cell.state === undefined ? '' : String(cell.state),
588
595
  )
589
596
 
597
+ const errorId = useId()
598
+
590
599
  return (
591
600
  <span onClick={(event) => event.stopPropagation()}>
592
601
  <select
@@ -596,7 +605,9 @@ function NativeSelectCell(props: CellProps) {
596
605
  setValue(event.target.value)
597
606
  void update(event.target.value === '' ? null : event.target.value)
598
607
  }}
608
+ aria-label={column.label || column.name}
599
609
  aria-invalid={error ? true : undefined}
610
+ aria-describedby={error ? errorId : undefined}
600
611
  className={cn(
601
612
  'border-input dark:bg-input/30 focus-visible:ring-ring/50 h-8 min-w-32 rounded-md border bg-transparent px-2 text-sm shadow-xs outline-none focus-visible:ring-[3px]',
602
613
  error && 'border-destructive',
@@ -611,7 +622,11 @@ function NativeSelectCell(props: CellProps) {
611
622
  </option>
612
623
  ))}
613
624
  </select>
614
- {error && <span className="text-destructive block text-xs">{error}</span>}
625
+ {error && (
626
+ <span id={errorId} className="text-destructive block text-xs">
627
+ {error}
628
+ </span>
629
+ )}
615
630
  </span>
616
631
  )
617
632
  }
@@ -626,6 +641,7 @@ function TextInputCell(props: CellProps) {
626
641
  const initial = cell.state === null || cell.state === undefined ? '' : String(cell.state)
627
642
  // `prefix()` / `suffix()`: text affixes around the input, like the TextInput field.
628
643
  const grouped = Boolean(cell.prefix || cell.suffix)
644
+ const errorId = useId()
629
645
 
630
646
  const field = (
631
647
  <span className={cn('relative block', grouped && 'min-w-0 flex-1')}>
@@ -648,7 +664,9 @@ function TextInputCell(props: CellProps) {
648
664
  step={schema.step}
649
665
  inputMode={schema.inputMode as React.HTMLAttributes<HTMLInputElement>['inputMode']}
650
666
  disabled={Boolean(cell.disabled) || saving}
667
+ aria-label={column.label || column.name}
651
668
  aria-invalid={error ? true : undefined}
669
+ aria-describedby={error ? errorId : undefined}
652
670
  className={cn(
653
671
  'h-8 min-w-32',
654
672
  schema.prefixIcon && 'ps-8',
@@ -706,7 +724,11 @@ function TextInputCell(props: CellProps) {
706
724
  ) : (
707
725
  field
708
726
  )}
709
- {error && <span className="text-destructive block text-xs">{error}</span>}
727
+ {error && (
728
+ <span id={errorId} className="text-destructive block text-xs">
729
+ {error}
730
+ </span>
731
+ )}
710
732
  </span>
711
733
  )
712
734
  }
@@ -193,24 +193,26 @@ export function ColumnManager({
193
193
 
194
194
  const orderChanged = Boolean(orderDraft && columnOrderChanged(appliedNames, orderDraft))
195
195
 
196
- const apply = () => {
197
- if (orderDraft && orderChanged) void save(orderDraft)
198
-
199
- if (draft && columnDraftChanged(table.visibleColumns, draft)) {
200
- visitTable(table.identifier, { columns: draft }, ['page'])
201
- }
196
+ // The order is saved before the visibility visit: both requests share the session, and a
197
+ // visit running alongside the save would write the old order back when it finishes.
198
+ const apply = async () => {
199
+ const columns = draft && columnDraftChanged(table.visibleColumns, draft) ? draft : null
202
200
 
203
201
  setDraft(null)
204
202
  setOrderDraft(null)
205
203
  setAnnouncement('')
206
204
  setOpen(false)
205
+
206
+ if (orderDraft && orderChanged) await save(orderDraft)
207
+
208
+ if (columns) visitTable(table.identifier, { columns }, ['page'])
207
209
  }
208
210
 
209
211
  // Reset: the defined column order and, for toggleable columns, the default visibility.
210
- const reset = () => {
212
+ const reset = async () => {
211
213
  if (reorderable) {
212
214
  setOrderDraft(null)
213
- void save([])
215
+ await save([])
214
216
  }
215
217
 
216
218
  if (toggleable) {
@@ -237,7 +239,7 @@ export function ColumnManager({
237
239
  disabled={saving}
238
240
  data-slot="column-manager-reset"
239
241
  className={cn(!applyView && 'w-full')}
240
- onClick={reset}
242
+ onClick={() => void reset()}
241
243
  />
242
244
  ) : null
243
245
  const inHeader = table.columnManagerResetActionPosition === 'header'
@@ -394,7 +396,7 @@ export function ColumnManager({
394
396
  disabled={
395
397
  !orderChanged && (!draft || !columnDraftChanged(table.visibleColumns, draft))
396
398
  }
397
- onClick={apply}
399
+ onClick={() => void apply()}
398
400
  />
399
401
  )}
400
402
  </div>
@@ -313,13 +313,17 @@ export function ContentLayoutSort({
313
313
  if (sortable.length === 0) return null
314
314
 
315
315
  const direction = table.sort?.direction ?? 'asc'
316
+ // A default sort on a column the select doesn't list (`created_at`) shows the placeholder.
317
+ const selected = sortable.some((column) => column.name === table.sort?.column)
318
+ ? (table.sort?.column ?? '')
319
+ : ''
316
320
 
317
321
  return (
318
322
  <div className="flex items-center gap-1">
319
323
  <label className="flex items-center gap-2 text-sm">
320
324
  <span className="sr-only">{t('table.sort_by', {}, 'Sort by')}</span>
321
325
  <select
322
- value={table.sort?.column ?? ''}
326
+ value={selected}
323
327
  onChange={(event) =>
324
328
  visitTable(
325
329
  table.identifier,
@@ -14,6 +14,7 @@ import {
14
14
  import { ChevronDownIcon, ChevronRightIcon, LayersIcon } from 'lucide-react'
15
15
  import { useEffect, useState } from 'react'
16
16
  import { useT } from '../../context'
17
+ import { colorClass, textColorClasses } from '../../lib/colors'
17
18
  import { Icon } from '../../lib/icons'
18
19
  import { cn } from '../../lib/utils'
19
20
  import { Checkbox } from '../ui/checkbox'
@@ -183,7 +184,7 @@ export function GroupHeaderRow({
183
184
  selection?: { checked: boolean | 'indeterminate'; onChange: (checked: boolean) => void }
184
185
  }) {
185
186
  const t = useT()
186
- const content = (
187
+ const text = (
187
188
  <span className="flex flex-col items-start">
188
189
  <span className="font-medium">{group.title}</span>
189
190
  {group.description && (
@@ -191,6 +192,19 @@ export function GroupHeaderRow({
191
192
  )}
192
193
  </span>
193
194
  )
195
+ // An enum group shows its `HasIcon` icon, in its `HasColor` colour, before the title.
196
+ const content = group.icon ? (
197
+ <span data-slot="table-group-title" className="flex items-center gap-2">
198
+ <Icon
199
+ name={group.icon}
200
+ className={cn('size-4 shrink-0', colorClass(textColorClasses, group.color, 'gray'))}
201
+ aria-hidden="true"
202
+ />
203
+ {text}
204
+ </span>
205
+ ) : (
206
+ text
207
+ )
194
208
 
195
209
  return (
196
210
  <TableRow data-slot="table-group" className="bg-muted/50 hover:bg-muted/50">
@@ -14,12 +14,13 @@ import {
14
14
  ChevronsRightIcon,
15
15
  } from 'lucide-react'
16
16
  import { useEffect, useRef, useState } from 'react'
17
- import { useT } from '../../context'
17
+ import { useFormatter, useT } from '../../context'
18
18
  import { Icon } from '../../lib/icons'
19
19
  import { Button } from '../ui/button'
20
20
 
21
21
  export function TablePagination({ table }: { table: TableSchema }) {
22
22
  const t = useT()
23
+ const format = useFormatter()
23
24
  const pagination = table.pagination
24
25
 
25
26
  if (!pagination) {
@@ -41,13 +42,17 @@ export function TablePagination({ table }: { table: TableSchema }) {
41
42
  const summary = numbered
42
43
  ? t(
43
44
  'table.showing',
44
- { first: pagination.from, last: pagination.to, total: pagination.total },
45
- `Showing ${pagination.from}–${pagination.to} of ${pagination.total}`,
45
+ {
46
+ first: format.number(pagination.from),
47
+ last: format.number(pagination.to),
48
+ total: format.number(pagination.total),
49
+ },
50
+ 'Showing :first–:last of :total',
46
51
  )
47
52
  : t(
48
53
  'table.showing_range',
49
- { first: pagination.from, last: pagination.to },
50
- `Showing ${pagination.from}–${pagination.to}`,
54
+ { first: format.number(pagination.from), last: format.number(pagination.to) },
55
+ 'Showing :first–:last',
51
56
  )
52
57
 
53
58
  return (
@@ -114,7 +119,7 @@ export function TablePagination({ table }: { table: TableSchema }) {
114
119
  onClick={() => visitTable(table.identifier, { page }, ['page'])}
115
120
  aria-current={page === pagination.currentPage ? 'page' : undefined}
116
121
  >
117
- {page}
122
+ {format.number(page)}
118
123
  </Button>
119
124
  ),
120
125
  )}
@@ -169,6 +174,7 @@ function PerPageSelect({
169
174
  pagination: AnyPaginationSchema
170
175
  }) {
171
176
  const t = useT()
177
+ const format = useFormatter()
172
178
 
173
179
  if (pagination.pageOptions.length <= 1) return null
174
180
 
@@ -188,7 +194,7 @@ function PerPageSelect({
188
194
  >
189
195
  {pagination.pageOptions.map((option) => (
190
196
  <option key={option} value={String(option)}>
191
- {option === 'all' ? t('table.all', {}, 'All') : option}
197
+ {option === 'all' ? t('table.all', {}, 'All') : format.number(option)}
192
198
  </option>
193
199
  ))}
194
200
  </select>
@@ -210,6 +216,7 @@ function AppendingPagination({
210
216
  pagination: Extract<AnyPaginationSchema, { mode: 'load-more' | 'infinite' }>
211
217
  }) {
212
218
  const t = useT()
219
+ const format = useFormatter()
213
220
  const sentinel = useRef<HTMLDivElement>(null)
214
221
  const [loading, setLoading] = useState(false)
215
222
  const [failed, setFailed] = useState(false)
@@ -290,8 +297,8 @@ function AppendingPagination({
290
297
  <span>
291
298
  {t(
292
299
  'table.showing_range',
293
- { first: pagination.from, last: pagination.to },
294
- `Showing ${pagination.from}–${pagination.to}`,
300
+ { first: format.number(pagination.from), last: format.number(pagination.to) },
301
+ 'Showing :first–:last',
295
302
  )}
296
303
  </span>
297
304
  <PerPageSelect table={table} pagination={pagination} />
@@ -202,6 +202,20 @@ export function DataTable({
202
202
  [table.identifier, table.searchDebounce],
203
203
  )
204
204
 
205
+ // Column searches (`searchable(isIndividual: true)`) apply after the same pause as the table
206
+ // search; Enter applies at once. Callers pass every column's search (the others unchanged).
207
+ const pushColumnSearch = useMemo(
208
+ () =>
209
+ debounce(
210
+ (columnSearch: Record<string, string>) =>
211
+ visitTable(table.identifier, { columnSearch }, ['page']),
212
+ table.searchDebounce,
213
+ ),
214
+ [table.identifier, table.searchDebounce],
215
+ )
216
+ const searchColumn = (name: string, value: string) =>
217
+ pushColumnSearch({ ...(table.columnSearches ?? {}), [name]: value })
218
+
205
219
  // Polls refresh only this table (a partial reload of its own schema), share one request
206
220
  // with other components on the same interval and pause while the tab is hidden.
207
221
  const pollComponent = usePollComponent()
@@ -619,19 +633,18 @@ export function DataTable({
619
633
  placeholder={t('search', {}, 'Search')}
620
634
  aria-label={`${t('search', {}, 'Search')} ${column.label}`}
621
635
  className="h-7 text-xs font-normal"
622
- onKeyDown={(event) =>
623
- event.key === 'Enter' &&
624
- visitTable(
625
- table.identifier,
626
- {
627
- columnSearch: {
628
- ...(table.columnSearches ?? {}),
629
- [column.name]: (event.target as HTMLInputElement).value,
630
- },
631
- },
632
- ['page'],
633
- )
636
+ onChange={(event) => {
637
+ if (!table.searchOnBlur) searchColumn(column.name, event.target.value)
638
+ }}
639
+ onBlur={(event) =>
640
+ table.searchOnBlur && searchColumn(column.name, event.target.value)
634
641
  }
642
+ onKeyDown={(event) => {
643
+ if (event.key !== 'Enter') return
644
+
645
+ searchColumn(column.name, (event.target as HTMLInputElement).value)
646
+ pushColumnSearch.flush()
647
+ }}
635
648
  />
636
649
  )}
637
650
  </div>
@@ -1,5 +1,5 @@
1
1
  import { XIcon } from 'lucide-react'
2
- import { Dialog as DialogPrimitive } from 'radix-ui'
2
+ import { Dialog as DialogPrimitive, Direction } from 'radix-ui'
3
3
  import type { ComponentProps } from 'react'
4
4
  import { useT } from '../../context'
5
5
  import { Icon } from '../../lib/icons'
@@ -40,11 +40,15 @@ function DialogContent({
40
40
  showCloseButton = true,
41
41
  ...props
42
42
  }: ComponentProps<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }) {
43
+ // Portaled outside Alfakit's root: carry the reading direction (starter kit pages and
44
+ // standalone components leave <html dir> to the host).
45
+ const dir = Direction.useDirection()
43
46
  const t = useT()
44
47
  return (
45
48
  <DialogPortal data-slot="dialog-portal">
46
49
  <DialogOverlay />
47
50
  <DialogPrimitive.Content
51
+ dir={dir}
48
52
  data-slot="dialog-content"
49
53
  className={cn(
50
54
  'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
@@ -1,4 +1,4 @@
1
- import { Popover as PopoverPrimitive } from 'radix-ui'
1
+ import { Direction, Popover as PopoverPrimitive } from 'radix-ui'
2
2
  import type { ComponentProps } from 'react'
3
3
  import { cn } from '../../lib/utils'
4
4
 
@@ -16,9 +16,13 @@ function PopoverContent({
16
16
  sideOffset = 4,
17
17
  ...props
18
18
  }: ComponentProps<typeof PopoverPrimitive.Content>) {
19
+ // Portaled outside Alfakit's root: carry the reading direction (starter kit pages and
20
+ // standalone components leave <html dir> to the host).
21
+ const dir = Direction.useDirection()
19
22
  return (
20
23
  <PopoverPrimitive.Portal>
21
24
  <PopoverPrimitive.Content
25
+ dir={dir}
22
26
  data-slot="popover-content"
23
27
  align={align}
24
28
  sideOffset={sideOffset}
@@ -1,5 +1,5 @@
1
1
  import { XIcon } from 'lucide-react'
2
- import { Dialog as SheetPrimitive } from 'radix-ui'
2
+ import { Direction, Dialog as SheetPrimitive } from 'radix-ui'
3
3
  import type { ComponentProps } from 'react'
4
4
  import { useT } from '../../context'
5
5
  import { Icon } from '../../lib/icons'
@@ -44,11 +44,15 @@ function SheetContent({
44
44
  side?: 'top' | 'right' | 'bottom' | 'left'
45
45
  showCloseButton?: boolean
46
46
  }) {
47
+ // Portaled outside Alfakit's root: carry the reading direction (starter kit pages and
48
+ // standalone components leave <html dir> to the host).
49
+ const dir = Direction.useDirection()
47
50
  const t = useT()
48
51
  return (
49
52
  <SheetPortal>
50
53
  <SheetOverlay />
51
54
  <SheetPrimitive.Content
55
+ dir={dir}
52
56
  data-slot="sheet-content"
53
57
  className={cn(
54
58
  'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
@@ -1,4 +1,4 @@
1
- import { Tooltip as TooltipPrimitive } from 'radix-ui'
1
+ import { Direction, Tooltip as TooltipPrimitive } from 'radix-ui'
2
2
  import type { ComponentProps } from 'react'
3
3
  import { cn } from '../../lib/utils'
4
4
 
@@ -29,9 +29,13 @@ function TooltipContent({
29
29
  children,
30
30
  ...props
31
31
  }: ComponentProps<typeof TooltipPrimitive.Content>) {
32
+ // Portaled outside Alfakit's root: carry the reading direction (starter kit pages and
33
+ // standalone components leave <html dir> to the host).
34
+ const dir = Direction.useDirection()
32
35
  return (
33
36
  <TooltipPrimitive.Portal>
34
37
  <TooltipPrimitive.Content
38
+ dir={dir}
35
39
  data-slot="tooltip-content"
36
40
  sideOffset={sideOffset}
37
41
  className={cn(
package/src/context.tsx CHANGED
@@ -28,6 +28,11 @@ export interface AlfakitContextValue {
28
28
  * their own schemas by token.
29
29
  */
30
30
  poll?: (target: string, interval: string | number | null | undefined) => () => void
31
+ /**
32
+ * Fetches fresh schemas of components now (`live()` tables and widgets after a record
33
+ * change). Panels leave it out (a partial reload); components outside panels fetch theirs.
34
+ */
35
+ refresh?: (targets: string[]) => Promise<void>
31
36
  }
32
37
 
33
38
  const AlfakitContext = createContext<AlfakitContextValue | null>(null)
@@ -126,6 +131,11 @@ export function usePollComponent(): NonNullable<AlfakitContextValue['poll']> {
126
131
  return useContext(AlfakitContext)?.poll ?? pollComponent
127
132
  }
128
133
 
134
+ /** How `live()` components refresh: their own fetch outside panels, else a partial reload. */
135
+ export function useRefreshComponents(): AlfakitContextValue['refresh'] {
136
+ return useContext(AlfakitContext)?.refresh
137
+ }
138
+
129
139
  export function useAlfakitProps(): AlfakitPageProps {
130
140
  return useOptionalPageProps()
131
141
  }
@@ -7,6 +7,7 @@ import {
7
7
  configureCharts,
8
8
  configureRealtime,
9
9
  debugRecorder,
10
+ documentNonce,
10
11
  effectiveErrorNotifications,
11
12
  initializeAppearance,
12
13
  installErrorNotifications,
@@ -43,6 +44,7 @@ import { SchemaContractGuard } from './components/panel/schema-contract-guard'
43
44
  import { Toaster } from './components/ui/sonner'
44
45
  import { TooltipProvider } from './components/ui/tooltip'
45
46
  import { AlfakitProvider, useAlfakit, type AlfakitContextValue, type AnyComponent } from './context'
47
+ import { panelLayouts } from './layouts/panel-layouts'
46
48
  import { discoverComponents } from './lib/discovery'
47
49
  import { preloadIcons } from './lib/icons'
48
50
  import { AlfakitPage } from './pages/page'
@@ -72,9 +74,13 @@ export function AlfakitRoot({
72
74
  translator.setLocale(localization?.tag ?? 'en')
73
75
  }
74
76
 
77
+ // Starter kit mode: the host app owns `<html lang dir>` (its own pages and layout are not
78
+ // mirrored); `AlfakitStarterKitRoot` puts the direction on Alfakit's own root element.
79
+ const ownsDocument = panel?.starterKit !== true
80
+
75
81
  useLayoutEffect(() => {
76
- applyDocumentLocale(localization)
77
- }, [localization])
82
+ if (ownsDocument) applyDocumentLocale(localization)
83
+ }, [localization, ownsDocument])
78
84
 
79
85
  useLayoutEffect(() => {
80
86
  applyPanelTheme(panel)
@@ -133,6 +139,7 @@ export function bootAlfakit(
133
139
  // Auto-registered files sit between the built-ins and the explicit `components` / `pages`.
134
140
  const discovered = discoverComponents(options.discover)
135
141
  const registry = createDefaultRegistry()
142
+ .registerMany(panelLayouts)
136
143
  .registerMany(discovered.components)
137
144
  .registerMany(options.components ?? {})
138
145
  const pages: Record<string, AnyComponent> = {
@@ -237,6 +244,8 @@ export function bootAlfakit(
237
244
  return createInertiaApp<AlfakitPageProps>({
238
245
  title: (title) => title,
239
246
  progress: options.progress === undefined ? { color: 'var(--primary)' } : options.progress,
247
+ // The page's CSP nonce, so the progress bar's injected <style> passes a nonce-based policy.
248
+ nonce: documentNonce(),
240
249
  resolve: (name, page) => {
241
250
  const component = pages[name]
242
251
 
package/src/index.ts CHANGED
@@ -99,6 +99,8 @@ export { discoverComponents, discoveredComponent, type DiscoveredComponents } fr
99
99
  export { Icon } from './lib/icons'
100
100
  export { cn } from './lib/utils'
101
101
  export { AlfakitPage, ContentBlockRenderer, PageContent, PageHeader, PageShell } from './pages/page'
102
+ // `createAlfakit()` registers these; a registry built by hand for `AlfakitPage` needs them too.
103
+ export { panelLayouts } from './layouts/panel-layouts'
102
104
  export { createDefaultRegistry } from './registry'
103
105
  export * from './standalone'
104
106
  export * from './starter-kit'