@alfakit/react 0.1.2 → 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 (171) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/components/actions/action-modals.js +4 -0
  3. package/dist/components/actions/action-modals.js.map +1 -1
  4. package/dist/components/ai/copilot.d.ts +3 -1
  5. package/dist/components/ai/copilot.d.ts.map +1 -1
  6. package/dist/components/ai/copilot.js +4 -1
  7. package/dist/components/ai/copilot.js.map +1 -1
  8. package/dist/components/cms/live-preview.js +3 -2
  9. package/dist/components/cms/live-preview.js.map +1 -1
  10. package/dist/components/cms/page.js +1 -1
  11. package/dist/components/fields/builder.js +15 -6
  12. package/dist/components/fields/builder.js.map +1 -1
  13. package/dist/components/fields/image-editor-dialog.js +4 -2
  14. package/dist/components/fields/image-editor-dialog.js.map +1 -1
  15. package/dist/components/fields/item-actions.js +10 -10
  16. package/dist/components/fields/item-actions.js.map +1 -1
  17. package/dist/components/fields/key-value.d.ts.map +1 -1
  18. package/dist/components/fields/key-value.js +3 -3
  19. package/dist/components/fields/key-value.js.map +1 -1
  20. package/dist/components/fields/modal-table-select.js +6 -5
  21. package/dist/components/fields/modal-table-select.js.map +1 -1
  22. package/dist/components/fields/repeater.js +2 -2
  23. package/dist/components/fields/repeater.js.map +1 -1
  24. package/dist/components/fields/rich-editor.js +2 -1
  25. package/dist/components/fields/rich-editor.js.map +1 -1
  26. package/dist/components/form/form-block.js +1 -1
  27. package/dist/components/form/form-block.js.map +1 -1
  28. package/dist/components/form/form-context.js +2 -2
  29. package/dist/components/form/form-context.js.map +1 -1
  30. package/dist/components/kanban/kanban.js +4 -3
  31. package/dist/components/kanban/kanban.js.map +1 -1
  32. package/dist/components/layout/section.js +2 -2
  33. package/dist/components/layout/section.js.map +1 -1
  34. package/dist/components/panel/app-sidebar.d.ts +2 -3
  35. package/dist/components/panel/app-sidebar.d.ts.map +1 -1
  36. package/dist/components/panel/app-sidebar.js +1 -24
  37. package/dist/components/panel/app-sidebar.js.map +1 -1
  38. package/dist/components/panel/brand.d.ts +10 -0
  39. package/dist/components/panel/brand.d.ts.map +1 -0
  40. package/dist/components/panel/brand.js +37 -0
  41. package/dist/components/panel/brand.js.map +1 -0
  42. package/dist/components/panel/index.d.ts +2 -1
  43. package/dist/components/panel/topbar.js +1 -1
  44. package/dist/components/realtime/live-records-pill.d.ts.map +1 -1
  45. package/dist/components/realtime/live-records-pill.js +6 -3
  46. package/dist/components/realtime/live-records-pill.js.map +1 -1
  47. package/dist/components/table/cells.d.ts +6 -0
  48. package/dist/components/table/cells.d.ts.map +1 -1
  49. package/dist/components/table/cells.js +6 -2
  50. package/dist/components/table/cells.js.map +1 -1
  51. package/dist/components/table/column-manager.js +8 -7
  52. package/dist/components/table/column-manager.js.map +1 -1
  53. package/dist/components/table/content-layout.js +2 -1
  54. package/dist/components/table/content-layout.js.map +1 -1
  55. package/dist/components/table/pagination.js +16 -13
  56. package/dist/components/table/pagination.js.map +1 -1
  57. package/dist/components/table/table-block.js +14 -4
  58. package/dist/components/table/table-block.js.map +1 -1
  59. package/dist/components/ui/dialog.d.ts.map +1 -1
  60. package/dist/components/ui/dialog.js +3 -1
  61. package/dist/components/ui/dialog.js.map +1 -1
  62. package/dist/components/ui/popover.d.ts.map +1 -1
  63. package/dist/components/ui/popover.js +3 -1
  64. package/dist/components/ui/popover.js.map +1 -1
  65. package/dist/components/ui/sheet.d.ts.map +1 -1
  66. package/dist/components/ui/sheet.js +3 -1
  67. package/dist/components/ui/sheet.js.map +1 -1
  68. package/dist/components/ui/tooltip.js +3 -1
  69. package/dist/components/ui/tooltip.js.map +1 -1
  70. package/dist/context.d.ts +5 -0
  71. package/dist/context.d.ts.map +1 -1
  72. package/dist/context.js +5 -1
  73. package/dist/context.js.map +1 -1
  74. package/dist/create-alfakit.d.ts.map +1 -1
  75. package/dist/create-alfakit.js +8 -5
  76. package/dist/create-alfakit.js.map +1 -1
  77. package/dist/eject-manifest.json +16 -3
  78. package/dist/index.d.ts +6 -3
  79. package/dist/index.js +24 -21
  80. package/dist/layouts/app-layout.d.ts.map +1 -1
  81. package/dist/layouts/app-layout.js +2 -29
  82. package/dist/layouts/app-layout.js.map +1 -1
  83. package/dist/layouts/auth-layout.js +2 -2
  84. package/dist/layouts/auth-layout.js.map +1 -1
  85. package/dist/layouts/fullscreen-layout.js +1 -1
  86. package/dist/layouts/fullscreen-layout.js.map +1 -1
  87. package/dist/layouts/lazy-layouts.js +15 -0
  88. package/dist/layouts/lazy-layouts.js.map +1 -0
  89. package/dist/layouts/max-widths.js +34 -0
  90. package/dist/layouts/max-widths.js.map +1 -0
  91. package/dist/layouts/panel-layouts.d.ts +10 -0
  92. package/dist/layouts/panel-layouts.d.ts.map +1 -0
  93. package/dist/layouts/panel-layouts.js +20 -0
  94. package/dist/layouts/panel-layouts.js.map +1 -0
  95. package/dist/layouts/top-nav-layout.js +2 -2
  96. package/dist/layouts/top-nav-layout.js.map +1 -1
  97. package/dist/pages/page.d.ts.map +1 -1
  98. package/dist/pages/page.js +12 -7
  99. package/dist/pages/page.js.map +1 -1
  100. package/dist/standalone/alfakit-component.js +11 -6
  101. package/dist/standalone/alfakit-component.js.map +1 -1
  102. package/dist/standalone.css +7 -0
  103. package/dist/starter-kit/dashboard-widgets.d.ts +33 -0
  104. package/dist/starter-kit/dashboard-widgets.d.ts.map +1 -0
  105. package/dist/starter-kit/dashboard-widgets.js +53 -0
  106. package/dist/starter-kit/dashboard-widgets.js.map +1 -0
  107. package/dist/starter-kit/impersonation-banner.js +34 -0
  108. package/dist/starter-kit/impersonation-banner.js.map +1 -0
  109. package/dist/starter-kit/index.d.ts +3 -2
  110. package/dist/starter-kit/index.js +3 -2
  111. package/dist/starter-kit/starter-kit.d.ts +6 -0
  112. package/dist/starter-kit/starter-kit.d.ts.map +1 -1
  113. package/dist/starter-kit/starter-kit.js +18 -7
  114. package/dist/starter-kit/starter-kit.js.map +1 -1
  115. package/dist/starter-kit/widgets-loaded.js +20 -4
  116. package/dist/starter-kit/widgets-loaded.js.map +1 -1
  117. package/dist/starter-kit/widgets-triggers.js +34 -3
  118. package/dist/starter-kit/widgets-triggers.js.map +1 -1
  119. package/dist/starter-kit/widgets.d.ts +13 -5
  120. package/dist/starter-kit/widgets.d.ts.map +1 -1
  121. package/dist/starter-kit/widgets.js +56 -18
  122. package/dist/starter-kit/widgets.js.map +1 -1
  123. package/dist/styles.css +15 -2
  124. package/dist/themes.css +6 -1
  125. package/package.json +2 -2
  126. package/src/components/actions/action-modals.tsx +10 -0
  127. package/src/components/ai/copilot.tsx +14 -1
  128. package/src/components/cms/live-preview.tsx +5 -2
  129. package/src/components/fields/builder.tsx +14 -5
  130. package/src/components/fields/image-editor-dialog.tsx +7 -2
  131. package/src/components/fields/item-actions.tsx +32 -9
  132. package/src/components/fields/key-value.tsx +3 -2
  133. package/src/components/fields/modal-table-select.tsx +8 -3
  134. package/src/components/fields/repeater.tsx +2 -3
  135. package/src/components/fields/rich-editor.tsx +3 -0
  136. package/src/components/form/form-block.tsx +2 -1
  137. package/src/components/form/form-context.tsx +2 -2
  138. package/src/components/kanban/kanban.tsx +16 -3
  139. package/src/components/layout/section.tsx +2 -2
  140. package/src/components/panel/app-sidebar.tsx +2 -38
  141. package/src/components/panel/brand.tsx +46 -0
  142. package/src/components/realtime/live-records-pill.tsx +4 -3
  143. package/src/components/table/cells.tsx +9 -2
  144. package/src/components/table/column-manager.tsx +12 -10
  145. package/src/components/table/content-layout.tsx +5 -1
  146. package/src/components/table/pagination.tsx +16 -9
  147. package/src/components/table/table-block.tsx +25 -12
  148. package/src/components/ui/dialog.tsx +5 -1
  149. package/src/components/ui/popover.tsx +5 -1
  150. package/src/components/ui/sheet.tsx +5 -1
  151. package/src/components/ui/tooltip.tsx +5 -1
  152. package/src/context.tsx +10 -0
  153. package/src/create-alfakit.tsx +11 -2
  154. package/src/index.ts +2 -0
  155. package/src/layouts/app-layout.tsx +4 -28
  156. package/src/layouts/auth-layout.tsx +2 -2
  157. package/src/layouts/fullscreen-layout.tsx +1 -1
  158. package/src/layouts/lazy-layouts.ts +14 -0
  159. package/src/layouts/max-widths.ts +29 -0
  160. package/src/layouts/panel-layouts.ts +17 -0
  161. package/src/layouts/top-nav-layout.tsx +2 -2
  162. package/src/pages/page.tsx +21 -12
  163. package/src/standalone/alfakit-component.tsx +18 -7
  164. package/src/starter-kit/dashboard-widgets.tsx +78 -0
  165. package/src/starter-kit/impersonation-banner.tsx +48 -0
  166. package/src/starter-kit/index.ts +2 -0
  167. package/src/starter-kit/starter-kit.tsx +31 -4
  168. package/src/starter-kit/widgets-loaded.tsx +22 -0
  169. package/src/starter-kit/widgets-triggers.tsx +36 -1
  170. package/src/starter-kit/widgets.tsx +95 -20
  171. package/src/styles.css +15 -2
@@ -41,50 +41,14 @@ import {
41
41
  SidebarMenuSubItem,
42
42
  SidebarRail,
43
43
  } from '../ui/sidebar'
44
+ import { Brand } from './brand'
44
45
  import { GlobalSearch } from './global-search'
45
46
  import { NavLabel } from './nav-label'
46
47
  import { Slot } from './slot'
47
48
  import { TenantMenu } from './tenant-menu'
48
49
  import { UserMenu } from './user-menu'
49
50
 
50
- export function Brand({ className }: { className?: string }) {
51
- const panel = usePanel()
52
-
53
- return (
54
- <Link
55
- href={panel.url ?? '/'}
56
- className={cn('flex items-center gap-2 overflow-hidden', className)}
57
- >
58
- {panel.brandLogo ? (
59
- <>
60
- <img
61
- src={panel.brandLogo}
62
- alt={panel.brandName}
63
- style={{ height: panel.brandLogoHeight ?? '1.75rem' }}
64
- className={cn('max-w-full object-contain', panel.darkModeBrandLogo && 'dark:hidden')}
65
- />
66
- {panel.darkModeBrandLogo && (
67
- <img
68
- src={panel.darkModeBrandLogo}
69
- alt={panel.brandName}
70
- style={{ height: panel.brandLogoHeight ?? '1.75rem' }}
71
- className="hidden max-w-full object-contain dark:block"
72
- />
73
- )}
74
- </>
75
- ) : (
76
- <>
77
- <span className="bg-primary text-primary-foreground flex size-8 shrink-0 items-center justify-center rounded-lg text-sm font-bold">
78
- {panel.brandName.charAt(0)}
79
- </span>
80
- <span className="truncate text-sm font-semibold group-data-[collapsible=icon]:hidden">
81
- {panel.brandName}
82
- </span>
83
- </>
84
- )}
85
- </Link>
86
- )
87
- }
51
+ export { Brand } from './brand'
88
52
 
89
53
  function NavBadge({ item }: { item: NavigationItemSchema }) {
90
54
  const t = useT()
@@ -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
+ }
@@ -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.
@@ -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) {
@@ -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,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'
@@ -15,36 +15,12 @@ import { Topbar } from '../components/panel/topbar'
15
15
  import { SidebarInset, SidebarProvider, SidebarTrigger } from '../components/ui/sidebar'
16
16
  import { useAlfakitProps, useRegistry } from '../context'
17
17
  import { cn } from '../lib/utils'
18
+ import { MAX_WIDTHS } from './max-widths'
18
19
  import { TopNavLayout } from './top-nav-layout'
19
20
 
20
- export const MAX_WIDTHS: Record<string, string> = {
21
- '3xs': 'max-w-3xs',
22
- '2xs': 'max-w-2xs',
23
- xs: 'max-w-xs',
24
- sm: 'max-w-sm',
25
- md: 'max-w-md',
26
- lg: 'max-w-lg',
27
- xl: 'max-w-xl',
28
- '2xl': 'max-w-2xl',
29
- '3xl': 'max-w-3xl',
30
- '4xl': 'max-w-4xl',
31
- '5xl': 'max-w-5xl',
32
- '6xl': 'max-w-6xl',
33
- '7xl': 'max-w-7xl',
34
- none: 'max-w-none',
35
- min: 'max-w-min',
36
- max: 'max-w-max',
37
- fit: 'max-w-fit',
38
- prose: 'max-w-prose',
39
- container: 'container',
40
- 'screen-sm': 'max-w-(--breakpoint-sm)',
41
- 'screen-md': 'max-w-(--breakpoint-md)',
42
- 'screen-lg': 'max-w-(--breakpoint-lg)',
43
- 'screen-xl': 'max-w-(--breakpoint-xl)',
44
- 'screen-2xl': 'max-w-(--breakpoint-2xl)',
45
- full: 'max-w-full',
46
- screen: 'max-w-screen',
47
- }
21
+ // Re-exported for code that imported it from here; the map lives in `./max-widths` so the
22
+ // starter kit's content layout doesn't pull the panel chrome in.
23
+ export { MAX_WIDTHS } from './max-widths'
48
24
 
49
25
  export interface AppLayoutProps {
50
26
  children: ReactNode
@@ -1,12 +1,12 @@
1
1
  import type { ReactNode } from 'react'
2
- import { Brand } from '../components/panel/app-sidebar'
2
+ import { Brand } from '../components/panel/brand'
3
3
  import { LocaleSwitcher } from '../components/panel/locale-switcher'
4
4
  import { Slot } from '../components/panel/slot'
5
5
  import { ThemeToggle } from '../components/panel/theme-toggle'
6
6
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card'
7
7
  import { usePanel } from '../context'
8
8
  import { cn } from '../lib/utils'
9
- import { MAX_WIDTHS } from './app-layout'
9
+ import { MAX_WIDTHS } from './max-widths'
10
10
 
11
11
  export interface AuthLayoutProps {
12
12
  children: ReactNode
@@ -3,7 +3,7 @@ import { Link } from '@inertiajs/react'
3
3
  import { LogOutIcon } from 'lucide-react'
4
4
  import type { ReactNode } from 'react'
5
5
  import { NotificationsMenu } from '../components/notifications/notifications-menu'
6
- import { Brand } from '../components/panel/app-sidebar'
6
+ import { Brand } from '../components/panel/brand'
7
7
  import { DebugPanelSlot } from '../components/panel/debug-panel-slot'
8
8
  import { TopbarLocaleSwitcher } from '../components/panel/locale-switcher'
9
9
  import { Slot } from '../components/panel/slot'
@@ -0,0 +1,14 @@
1
+ import { lazyComponents } from '@alfakit/core'
2
+ import type { AnyComponent } from '../context'
3
+ import { discoveredComponent } from '../lib/discovery'
4
+
5
+ /**
6
+ * The fullscreen layout (`$layout = 'fullscreen'`), loaded on demand by starter kit pages:
7
+ * they render in the host's layout, so the page chunk never imports the panel's header,
8
+ * menus and notifications for a layout few pages use. Only the starter kit registers it;
9
+ * `createAlfakit()` registers the layouts themselves up front (`panelLayouts`), so a panel's
10
+ * bundle has no extra lazy entry points.
11
+ */
12
+ export const lazyPanelLayouts: Record<string, AnyComponent> = lazyComponents<AnyComponent>({
13
+ 'layout:fullscreen': () => import('./fullscreen-layout').then((m) => m.FullscreenLayout),
14
+ }).map(discoveredComponent).components
@@ -0,0 +1,29 @@
1
+ /** Tailwind classes for the `maxContentWidth` setting of the panel and its pages. */
2
+ export const MAX_WIDTHS: Record<string, string> = {
3
+ '3xs': 'max-w-3xs',
4
+ '2xs': 'max-w-2xs',
5
+ xs: 'max-w-xs',
6
+ sm: 'max-w-sm',
7
+ md: 'max-w-md',
8
+ lg: 'max-w-lg',
9
+ xl: 'max-w-xl',
10
+ '2xl': 'max-w-2xl',
11
+ '3xl': 'max-w-3xl',
12
+ '4xl': 'max-w-4xl',
13
+ '5xl': 'max-w-5xl',
14
+ '6xl': 'max-w-6xl',
15
+ '7xl': 'max-w-7xl',
16
+ none: 'max-w-none',
17
+ min: 'max-w-min',
18
+ max: 'max-w-max',
19
+ fit: 'max-w-fit',
20
+ prose: 'max-w-prose',
21
+ container: 'container',
22
+ 'screen-sm': 'max-w-(--breakpoint-sm)',
23
+ 'screen-md': 'max-w-(--breakpoint-md)',
24
+ 'screen-lg': 'max-w-(--breakpoint-lg)',
25
+ 'screen-xl': 'max-w-(--breakpoint-xl)',
26
+ 'screen-2xl': 'max-w-(--breakpoint-2xl)',
27
+ full: 'max-w-full',
28
+ screen: 'max-w-screen',
29
+ }