@alfakit/react 0.1.9 → 0.1.11

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 (107) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/actions/action-button.js +13 -1
  3. package/dist/components/actions/action-button.js.map +1 -1
  4. package/dist/components/dev-tools/dev-tools-inspector.js +3 -3
  5. package/dist/components/dev-tools/dev-tools-inspector.js.map +1 -1
  6. package/dist/components/fields/checkbox-list.js +2 -2
  7. package/dist/components/fields/checkbox-list.js.map +1 -1
  8. package/dist/components/fields/money-input.d.ts +5 -0
  9. package/dist/components/fields/money-input.d.ts.map +1 -1
  10. package/dist/components/fields/money-input.js +2 -5
  11. package/dist/components/fields/money-input.js.map +1 -1
  12. package/dist/components/fields/rich-editor-extensions.js +14 -4
  13. package/dist/components/fields/rich-editor-extensions.js.map +1 -1
  14. package/dist/components/file-manager/file-manager.js +2 -0
  15. package/dist/components/file-manager/file-manager.js.map +1 -1
  16. package/dist/components/layout/elements.d.ts.map +1 -1
  17. package/dist/components/layout/elements.js +16 -5
  18. package/dist/components/layout/elements.js.map +1 -1
  19. package/dist/components/layout/mixed-content.d.ts +6 -4
  20. package/dist/components/layout/mixed-content.d.ts.map +1 -1
  21. package/dist/components/layout/mixed-content.js +6 -4
  22. package/dist/components/layout/mixed-content.js.map +1 -1
  23. package/dist/components/layout/section.js +4 -2
  24. package/dist/components/layout/section.js.map +1 -1
  25. package/dist/components/notifications/notifications-menu.d.ts.map +1 -1
  26. package/dist/components/notifications/notifications-menu.js +45 -17
  27. package/dist/components/notifications/notifications-menu.js.map +1 -1
  28. package/dist/components/panel/lazy-placeholder.d.ts +1 -0
  29. package/dist/components/panel/lazy-placeholder.d.ts.map +1 -1
  30. package/dist/components/panel/lazy-placeholder.js +38 -4
  31. package/dist/components/panel/lazy-placeholder.js.map +1 -1
  32. package/dist/components/panel/shortcuts-overlay.js +13 -10
  33. package/dist/components/panel/shortcuts-overlay.js.map +1 -1
  34. package/dist/components/table/cells.js +1 -1
  35. package/dist/components/table/cells.js.map +1 -1
  36. package/dist/components/table/column-manager.js +4 -4
  37. package/dist/components/table/column-manager.js.map +1 -1
  38. package/dist/components/table/spreadsheet.js +8 -1
  39. package/dist/components/table/spreadsheet.js.map +1 -1
  40. package/dist/components/translation-manager/translation-manager.js +1 -1
  41. package/dist/components/translation-manager/translation-manager.js.map +1 -1
  42. package/dist/components/ui/calendar.d.ts.map +1 -1
  43. package/dist/components/ui/calendar.js +7 -0
  44. package/dist/components/ui/calendar.js.map +1 -1
  45. package/dist/components/widgets/dashboard-customizer.js +2 -2
  46. package/dist/components/widgets/dashboard-customizer.js.map +1 -1
  47. package/dist/components/widgets/stats-overview.js +3 -3
  48. package/dist/components/widgets/stats-overview.js.map +1 -1
  49. package/dist/components/widgets/widgets-block.js +2 -2
  50. package/dist/components/widgets/widgets-block.js.map +1 -1
  51. package/dist/create-alfakit.d.ts.map +1 -1
  52. package/dist/create-alfakit.js +3 -2
  53. package/dist/create-alfakit.js.map +1 -1
  54. package/dist/eject-manifest.json +4 -2
  55. package/dist/index.d.ts +2 -2
  56. package/dist/index.js +2 -2
  57. package/dist/pages/page.d.ts.map +1 -1
  58. package/dist/pages/page.js +4 -7
  59. package/dist/pages/page.js.map +1 -1
  60. package/dist/standalone/alfakit-component.d.ts +0 -10
  61. package/dist/standalone/alfakit-component.d.ts.map +1 -1
  62. package/dist/standalone/alfakit-component.js +5 -4
  63. package/dist/standalone/alfakit-component.js.map +1 -1
  64. package/dist/standalone/runtime.js +2 -1
  65. package/dist/standalone/runtime.js.map +1 -1
  66. package/dist/standalone.css +23 -1
  67. package/dist/starter-kit/index.d.ts +2 -2
  68. package/dist/starter-kit/index.js +2 -2
  69. package/dist/starter-kit/navigation.d.ts +6 -1
  70. package/dist/starter-kit/navigation.d.ts.map +1 -1
  71. package/dist/starter-kit/navigation.js +3 -2
  72. package/dist/starter-kit/navigation.js.map +1 -1
  73. package/dist/starter-kit/starter-kit.d.ts +9 -0
  74. package/dist/starter-kit/starter-kit.d.ts.map +1 -1
  75. package/dist/starter-kit/starter-kit.js +22 -3
  76. package/dist/starter-kit/starter-kit.js.map +1 -1
  77. package/dist/styles.css +2 -1
  78. package/dist/themes.css +4 -0
  79. package/package.json +2 -2
  80. package/src/components/actions/action-button.tsx +15 -1
  81. package/src/components/dev-tools/dev-tools-inspector.tsx +7 -2
  82. package/src/components/fields/checkbox-list.tsx +2 -2
  83. package/src/components/fields/money-input.tsx +3 -14
  84. package/src/components/fields/rich-editor-extensions.tsx +24 -2
  85. package/src/components/file-manager/file-manager.tsx +3 -1
  86. package/src/components/layout/elements.tsx +27 -4
  87. package/src/components/layout/mixed-content.tsx +8 -4
  88. package/src/components/layout/section.tsx +2 -2
  89. package/src/components/notifications/notifications-menu.tsx +42 -1
  90. package/src/components/panel/lazy-placeholder.tsx +41 -1
  91. package/src/components/panel/shortcuts-overlay.tsx +29 -24
  92. package/src/components/table/cells.tsx +3 -1
  93. package/src/components/table/column-manager.tsx +6 -4
  94. package/src/components/table/spreadsheet.tsx +18 -7
  95. package/src/components/translation-manager/translation-manager.tsx +1 -1
  96. package/src/components/ui/calendar.tsx +8 -0
  97. package/src/components/widgets/dashboard-customizer.tsx +2 -2
  98. package/src/components/widgets/stats-overview.tsx +6 -3
  99. package/src/components/widgets/widgets-block.tsx +2 -2
  100. package/src/create-alfakit.tsx +9 -1
  101. package/src/pages/page.tsx +7 -3
  102. package/src/standalone/alfakit-component.tsx +9 -4
  103. package/src/standalone/runtime.ts +3 -0
  104. package/src/starter-kit/index.ts +1 -0
  105. package/src/starter-kit/navigation.ts +12 -1
  106. package/src/starter-kit/starter-kit.tsx +25 -2
  107. package/src/styles.css +2 -1
@@ -1,6 +1,6 @@
1
1
  import type { ShortcutListGroup, ShortcutListItem } from '@alfakit/core'
2
2
  import {
3
- bindingFromEvent,
3
+ ShortcutRecorder,
4
4
  flattenActionGroup,
5
5
  saveShortcutOverrides,
6
6
  shortcutOverlayGroups,
@@ -52,50 +52,55 @@ export function ShortcutsOverlay({
52
52
  }
53
53
  }
54
54
 
55
- // While recording, the next key press is the new binding (Escape cancels). Listening in the
56
- // capture phase keeps the key from closing the dialog or running another shortcut.
55
+ // While recording, the keys pressed are the new binding: one key (with modifiers) or a
56
+ // sequence ("g o"), finished a second after the last key (Escape cancels). Listening in
57
+ // the capture phase keeps the keys from closing the dialog or running another shortcut.
57
58
  useEffect(() => {
58
59
  if (!recording) return
59
60
 
60
61
  shortcuts.setPaused(true)
61
62
 
62
- const listener = (event: KeyboardEvent) => {
63
- event.preventDefault()
64
- event.stopPropagation()
65
-
66
- if (event.key === 'Escape') {
63
+ const recorder = new ShortcutRecorder({
64
+ onDone: (binding) => {
67
65
  setRecording(null)
68
- setMessage(t('shortcuts.cancelled', {}, 'Change cancelled.'))
69
66
 
70
- return
71
- }
67
+ const conflict = shortcuts.conflict([binding], recording)
72
68
 
73
- const binding = bindingFromEvent(event)
69
+ if (conflict) {
70
+ setMessage(
71
+ t(
72
+ 'shortcuts.conflict',
73
+ { label: conflict.label },
74
+ `Already used by “${conflict.label}”.`,
75
+ ),
76
+ )
74
77
 
75
- if (!binding) return
78
+ return
79
+ }
76
80
 
77
- const conflict = shortcuts.conflict([binding], recording)
81
+ void save(recording, [binding])
82
+ },
83
+ })
78
84
 
79
- setRecording(null)
85
+ const listener = (event: KeyboardEvent) => {
86
+ event.preventDefault()
87
+ event.stopPropagation()
80
88
 
81
- if (conflict) {
82
- setMessage(
83
- t(
84
- 'shortcuts.conflict',
85
- { label: conflict.label },
86
- `Already used by “${conflict.label}”.`,
87
- ),
88
- )
89
+ if (event.key === 'Escape') {
90
+ recorder.cancel()
91
+ setRecording(null)
92
+ setMessage(t('shortcuts.cancelled', {}, 'Change cancelled.'))
89
93
 
90
94
  return
91
95
  }
92
96
 
93
- void save(recording, [binding])
97
+ recorder.press(event)
94
98
  }
95
99
 
96
100
  window.addEventListener('keydown', listener, true)
97
101
 
98
102
  return () => {
103
+ recorder.cancel()
99
104
  window.removeEventListener('keydown', listener, true)
100
105
  shortcuts.setPaused(false)
101
106
  }
@@ -220,7 +220,9 @@ function ImageCell({ column, cell }: CellProps) {
220
220
  <img
221
221
  key={index}
222
222
  src={url}
223
- alt={cell.alt ?? ''}
223
+ // Without `alt()` the first image is named after the column, so a record link that
224
+ // wraps it has a name (W-V4-20).
225
+ alt={cell.alt ?? (index === 0 ? (column.label ?? '') : '')}
224
226
  style={{
225
227
  height: schema.height ?? size,
226
228
  width: schema.width ?? (schema.square || schema.circular ? size : undefined),
@@ -114,13 +114,13 @@ export function ColumnManager({
114
114
  }
115
115
  }
116
116
 
117
- const save = async (order: string[]): Promise<boolean> => {
117
+ const save = async (order: string[], options: { reload?: boolean } = {}): Promise<boolean> => {
118
118
  if (!panel.endpoints.tableColumns) return false
119
119
 
120
120
  setSaving(true)
121
121
 
122
122
  try {
123
- await saveColumnOrder(panel.endpoints.tableColumns, token, component, order)
123
+ await saveColumnOrder(panel.endpoints.tableColumns, token, component, order, options)
124
124
 
125
125
  return true
126
126
  } catch (error) {
@@ -203,7 +203,8 @@ export function ColumnManager({
203
203
  setAnnouncement('')
204
204
  setOpen(false)
205
205
 
206
- if (orderDraft && orderChanged) await save(orderDraft)
206
+ // The visibility visit renders the new order too: no second reload racing it.
207
+ if (orderDraft && orderChanged) await save(orderDraft, { reload: !columns })
207
208
 
208
209
  if (columns) visitTable(table.identifier, { columns }, ['page'])
209
210
  }
@@ -212,7 +213,8 @@ export function ColumnManager({
212
213
  const reset = async () => {
213
214
  if (reorderable) {
214
215
  setOrderDraft(null)
215
- await save([])
216
+ // The visibility visit below renders the defined order too.
217
+ await save([], { reload: !toggleable })
216
218
  }
217
219
 
218
220
  if (toggleable) {
@@ -539,14 +539,25 @@ export function Spreadsheet({ table, token, component, onClose }: SpreadsheetPro
539
539
  <DialogHeader>
540
540
  <DialogTitle>{t('table.spreadsheet.paste_title', {}, 'Paste cells')}</DialogTitle>
541
541
  <DialogDescription>
542
- {choice(
543
- 'table.spreadsheet.paste_summary',
544
- preview?.plan.changes.length ?? 0,
545
- {},
546
- ':count cell will change|:count cells will change',
542
+ {/* One line per count: translations can't join two sentences. */}
543
+ <span className="block">
544
+ {choice(
545
+ 'table.spreadsheet.paste_summary',
546
+ preview?.plan.changes.length ?? 0,
547
+ {},
548
+ ':count cell will change|:count cells will change',
549
+ )}
550
+ </span>
551
+ {(preview?.plan.skipped ?? 0) > 0 && (
552
+ <span className="block" data-slot="spreadsheet-paste-skipped">
553
+ {choice(
554
+ 'table.spreadsheet.paste_skipped',
555
+ preview?.plan.skipped ?? 0,
556
+ {},
557
+ ':count read-only cell is skipped|:count read-only cells are skipped',
558
+ )}
559
+ </span>
547
560
  )}
548
- {(preview?.plan.skipped ?? 0) > 0 &&
549
- ` ${choice('table.spreadsheet.paste_skipped', preview?.plan.skipped ?? 0, {}, ':count read-only cell is skipped|:count read-only cells are skipped')}`}
550
561
  </DialogDescription>
551
562
  </DialogHeader>
552
563
  <div data-slot="spreadsheet-paste" className="flex flex-col gap-3">
@@ -330,7 +330,7 @@ function TranslationManagerView({ schema }: { schema: TranslationManagerSchema }
330
330
  <tr>
331
331
  <th
332
332
  scope="col"
333
- className="bg-muted sticky start-0 z-10 min-w-56 border-b px-3 py-2 text-start font-medium"
333
+ className="bg-card sticky start-0 z-10 min-w-56 border-b [background-image:linear-gradient(var(--muted),var(--muted))] px-3 py-2 text-start font-medium"
334
334
  >
335
335
  {t('translation-manager.key', {}, 'Key')}
336
336
  </th>
@@ -1,6 +1,7 @@
1
1
  import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
2
2
  import type { ComponentProps } from 'react'
3
3
  import { DayPicker } from 'react-day-picker'
4
+ import { useT } from '../../context'
4
5
  import { cn } from '../../lib/utils'
5
6
  import { buttonVariants } from './button'
6
7
 
@@ -10,9 +11,16 @@ function Calendar({
10
11
  showOutsideDays = true,
11
12
  ...props
12
13
  }: ComponentProps<typeof DayPicker>) {
14
+ const t = useT()
15
+
13
16
  return (
14
17
  <DayPicker
15
18
  showOutsideDays={showOutsideDays}
19
+ labels={{
20
+ labelPrevious: () => t('form.previous_month', {}, 'Previous month'),
21
+ labelNext: () => t('form.next_month', {}, 'Next month'),
22
+ ...props.labels,
23
+ }}
16
24
  className={cn('p-3', className)}
17
25
  classNames={{
18
26
  months: 'flex flex-col sm:flex-row gap-2',
@@ -24,7 +24,7 @@ import { useEffect, useId, useMemo, type ReactNode } from 'react'
24
24
  import { useDirection, useT } from '../../context'
25
25
  import { useStore } from '../../hooks/use-store'
26
26
  import { cn } from '../../lib/utils'
27
- import { LazyPlaceholder } from '../panel/lazy-placeholder'
27
+ import { LazyWidgetPlaceholder } from '../panel/lazy-placeholder'
28
28
  import { Button } from '../ui/button'
29
29
 
30
30
  /**
@@ -150,7 +150,7 @@ export function CustomizableWidgetGrid({
150
150
 
151
151
  const content = widget ? (
152
152
  isLazyPlaceholder(widget) ? (
153
- <LazyPlaceholder variant={widget.type === 'TableWidget' ? 'table' : 'widget'} />
153
+ <LazyWidgetPlaceholder widget={widget} />
154
154
  ) : (
155
155
  renderWidget(widget)
156
156
  )
@@ -71,7 +71,9 @@ function StatCard({ stat }: { stat: StatSchema }) {
71
71
  >
72
72
  <div className="flex items-start justify-between gap-3">
73
73
  <div className="flex flex-col gap-1">
74
- <span className="text-muted-foreground text-sm font-medium">{stat.label}</span>
74
+ <span className="text-muted-foreground text-sm font-medium">
75
+ <bdi>{stat.label}</bdi>
76
+ </span>
75
77
  {(stat.value === null || stat.value === '') && stat.placeholder ? (
76
78
  <span
77
79
  className="text-muted-foreground text-2xl font-semibold tracking-tight"
@@ -86,7 +88,7 @@ function StatCard({ stat }: { stat: StatSchema }) {
86
88
  stat.color && textColorClasses[stat.color],
87
89
  )}
88
90
  >
89
- {stat.value}
91
+ <bdi>{stat.value}</bdi>
90
92
  </span>
91
93
  )}
92
94
  {stat.description && (
@@ -101,7 +103,8 @@ function StatCard({ stat }: { stat: StatSchema }) {
101
103
  {stat.descriptionIcon && stat.descriptionIconPosition !== 'after' && (
102
104
  <Icon name={stat.descriptionIcon} className="size-3.5" />
103
105
  )}
104
- {stat.description}
106
+ {/* Mixed-direction text (a Latin "-3% this month" in Arabic) keeps its order. */}
107
+ <bdi>{stat.description}</bdi>
105
108
  {stat.descriptionIcon && stat.descriptionIconPosition === 'after' && (
106
109
  <Icon name={stat.descriptionIcon} className="size-3.5" />
107
110
  )}
@@ -10,7 +10,7 @@ import { createElement, type ComponentProps } from 'react'
10
10
  import { useRegistry, useT } from '../../context'
11
11
  import { lazyComponent } from '../../lib/lazy'
12
12
  import { cn } from '../../lib/utils'
13
- import { LazyPlaceholder } from '../panel/lazy-placeholder'
13
+ import { LazyWidgetPlaceholder } from '../panel/lazy-placeholder'
14
14
  import { Slot } from '../panel/slot'
15
15
  import { DataTable } from '../table/table-block'
16
16
  import { ChartWidget } from './chart-lazy'
@@ -49,7 +49,7 @@ function StaticWidgetGrid({ block }: { block: WidgetsBlockSchema }) {
49
49
  {block.widgets.map((widget) => (
50
50
  <div key={widget.key} className={cn('min-w-0', columnSpanClasses(widget.columnSpan))}>
51
51
  {isLazyPlaceholder(widget) ? (
52
- <LazyPlaceholder variant={widget.type === 'TableWidget' ? 'table' : 'widget'} />
52
+ <LazyWidgetPlaceholder widget={widget} />
53
53
  ) : (
54
54
  <WidgetRenderer widget={widget} />
55
55
  )}
@@ -13,10 +13,12 @@ import {
13
13
  installErrorNotifications,
14
14
  installPrefetchInvalidation,
15
15
  installSpaUrlExceptions,
16
+ overlayPlacementDirection,
16
17
  registerRichEditorExtensions,
17
18
  reloadOnExpiredPage,
18
19
  resolveRequestErrorNotification,
19
20
  resolveToastPosition,
21
+ setIconAliases,
20
22
  setRequestErrorReporter,
21
23
  whenIdle,
22
24
  type AlfakitOptions,
@@ -72,6 +74,9 @@ export function AlfakitRoot({
72
74
  if (panel) {
73
75
  translator.setTranslations(panel.translations ?? {})
74
76
  translator.setLocale(localization?.tag ?? 'en')
77
+ // Chrome icons replaced with `Icons::register()`, also in a server render, so the server
78
+ // and the browser draw the same glyph (W-V3-28).
79
+ setIconAliases(panel.theme?.icons)
75
80
  }
76
81
 
77
82
  // Starter kit mode: the host app owns `<html lang dir>` (its own pages and layout are not
@@ -104,7 +109,10 @@ export function AlfakitRoot({
104
109
  })}
105
110
  <Toaster
106
111
  id={toasterId}
107
- position={resolveToastPosition(panel?.toastPosition, direction)}
112
+ position={resolveToastPosition(
113
+ panel?.toastPosition,
114
+ overlayPlacementDirection(direction, panel?.starterKit),
115
+ )}
108
116
  offset={PANEL_TOAST_OFFSET}
109
117
  mobileOffset={PANEL_TOAST_OFFSET}
110
118
  dir={direction}
@@ -11,6 +11,7 @@ import type {
11
11
  } from '@alfakit/core'
12
12
  import {
13
13
  applyBodyAttributes,
14
+ customPageProps,
14
15
  hasRelationManagerTabs,
15
16
  isLazyPlaceholder,
16
17
  openDefaultAction,
@@ -102,9 +103,12 @@ export function AlfakitPage() {
102
103
  return (
103
104
  <PageActionProvider value={{ token: page.token, component: 'page' }}>
104
105
  <Head title={title} />
105
- <PageShell page={page}>
106
- <Custom {...page.view.props} page={page} />
107
- </PageShell>
106
+ {/* The component brings its own PageShell (as in panel mode, where it is the Inertia
107
+ page itself) and takes the live props, so `reloadProps()` reaches it. */}
108
+ <Custom
109
+ {...customPageProps(page.view.props, props as Record<string, unknown>)}
110
+ page={page}
111
+ />
108
112
  </PageActionProvider>
109
113
  )
110
114
  }
@@ -8,7 +8,7 @@ import {
8
8
  } from '@alfakit/core'
9
9
  import { usePage } from '@inertiajs/react'
10
10
  import { Direction } from 'radix-ui'
11
- import { useEffect, useMemo, useState, type ReactNode } from 'react'
11
+ import { useEffect, useLayoutEffect, useMemo, useState, type ReactNode } from 'react'
12
12
  import { PageActionProvider } from '../components/actions/action-context'
13
13
  import { ActionList } from '../components/actions/action-list'
14
14
  import { ActionModals } from '../components/actions/action-modals'
@@ -55,6 +55,9 @@ export interface AlfakitComponentProps {
55
55
  * <AlfakitComponent component={orders} className="max-w-4xl" />
56
56
  * ```
57
57
  */
58
+ // `useLayoutEffect` warns during a server render, where nothing registers anyway.
59
+ const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect
60
+
58
61
  export function AlfakitComponent({
59
62
  component,
60
63
  components,
@@ -138,7 +141,9 @@ export function AlfakitComponent({
138
141
  // Partial reloads (sorting, paging, inline edits) request this component's own prop.
139
142
  const propName = findStandalonePropName(props, component)
140
143
 
141
- useEffect(() => registerStandaloneProp(propName ?? null), [propName])
144
+ // A layout effect runs before the children's effects: a `deferLoading()` table's first
145
+ // reload must already ask for this component's prop, not the panel `page` prop.
146
+ useIsomorphicLayoutEffect(() => registerStandaloneProp(propName ?? null), [propName])
142
147
 
143
148
  useEffect(() => {
144
149
  void state.loadLazy(component)
@@ -188,7 +193,7 @@ export function AlfakitComponent({
188
193
  data-slot="page-header"
189
194
  className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
190
195
  >
191
- <div className="min-w-0">
196
+ <div className="min-w-0 sm:max-w-[50%] sm:shrink-0">
192
197
  {page.heading && (
193
198
  <h2 className="text-xl font-semibold tracking-tight">{page.heading}</h2>
194
199
  )}
@@ -201,7 +206,7 @@ export function AlfakitComponent({
201
206
  actions={page.headerActions}
202
207
  context={actionContext}
203
208
  align="end"
204
- className="max-w-full shrink-0"
209
+ className="max-w-full min-w-0"
205
210
  />
206
211
  )}
207
212
  </header>
@@ -3,6 +3,7 @@ import {
3
3
  ComponentRegistry,
4
4
  FlashNotificationTracker,
5
5
  Translator,
6
+ installPrefetchInvalidation,
6
7
  reloadOnExpiredPage,
7
8
  resolveRequestErrorNotification,
8
9
  type NotificationSchema,
@@ -27,6 +28,8 @@ export function ensureStandaloneRuntime(): void {
27
28
 
28
29
  browserReady = true
29
30
  reloadOnExpiredPage()
31
+ // Pages restored by Back after a change are reloaded; shared with a surrounding panel.
32
+ installPrefetchInvalidation()
30
33
  }
31
34
 
32
35
  /**
@@ -3,6 +3,7 @@ export { useAlfakitNavigation, type AlfakitNavigation } from './navigation'
3
3
  export {
4
4
  AlfakitStarterKitRoot,
5
5
  StarterKitContentLayout,
6
+ StarterKitFullscreenLayout,
6
7
  createStarterKitPage,
7
8
  type StarterKitLayout,
8
9
  type StarterKitPageOptions,
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  starterKitNavigation,
3
3
  toStarterKitNavGroups,
4
+ toStarterKitUserMenuItems,
4
5
  type StarterKitNavGroup,
5
6
  type StarterKitNavigationSchema,
7
+ type StarterKitUserMenuItem,
6
8
  } from '@alfakit/core'
7
9
  import { usePage } from '@inertiajs/react'
8
10
  import { useMemo } from 'react'
@@ -12,6 +14,11 @@ export interface AlfakitNavigation {
12
14
  navigation: StarterKitNavigationSchema | null
13
15
  /** Its groups as plain items (`title`, `href`, `icon`, `isActive`, `badge`, `children`). */
14
16
  groups: StarterKitNavGroup[]
17
+ /**
18
+ * The panel's user menu links (`userMenuItems()`, Billing's entry…) for the kit's user
19
+ * menu: `<AlfakitUserMenuItems />` renders them with the app's dropdown items.
20
+ */
21
+ userMenuItems: StarterKitUserMenuItem[]
15
22
  }
16
23
 
17
24
  /**
@@ -28,7 +35,11 @@ export function useAlfakitNavigation(panelId?: string): AlfakitNavigation {
28
35
  const navigation = starterKitNavigation(props, panelId)
29
36
 
30
37
  return useMemo(
31
- () => ({ navigation, groups: toStarterKitNavGroups(navigation?.groups) }),
38
+ () => ({
39
+ navigation,
40
+ groups: toStarterKitNavGroups(navigation?.groups),
41
+ userMenuItems: toStarterKitUserMenuItems(navigation?.userMenu),
42
+ }),
32
43
  [navigation],
33
44
  )
34
45
  }
@@ -85,6 +85,27 @@ export function StarterKitContentLayout({ children, maxContentWidth }: AppLayout
85
85
  )
86
86
  }
87
87
 
88
+ /**
89
+ * A `$layout = 'fullscreen'` page inside the host layout: what `layout:fullscreen` renders in
90
+ * starter kit mode. The host layout is the chrome, so there is no AlfaKit header (brand, top
91
+ * bar slots, user menu, Exit); the screen gets the whole content area. Replace it with
92
+ * `{ components: { 'layout:fullscreen': MyScreen } }`.
93
+ */
94
+ export function StarterKitFullscreenLayout({ children }: { children: ReactNode }) {
95
+ return (
96
+ <div
97
+ data-slot="content"
98
+ data-layout="fullscreen"
99
+ data-alfakit-embedded=""
100
+ className="flex min-h-0 w-full min-w-0 flex-1 flex-col"
101
+ >
102
+ <Slot name="content.start" />
103
+ {children}
104
+ <Slot name="content.end" />
105
+ </div>
106
+ )
107
+ }
108
+
88
109
  // One context for the browser session (it survives navigations between host and Alfakit
89
110
  // pages); a server render builds one per request so concurrent renders never share a locale.
90
111
  let browserContext: AlfakitContextValue | null = null
@@ -98,6 +119,7 @@ function createStarterKitContext(options: StarterKitPageOptions): AlfakitContext
98
119
  .registerMany(lazyPanelLayouts)
99
120
  .register('layout:auth', AuthLayout)
100
121
  .register('layout:app', StarterKitContentLayout)
122
+ .register('layout:fullscreen', StarterKitFullscreenLayout)
101
123
  .registerMany(discovered.components)
102
124
  .registerMany(options.components ?? {})
103
125
  const translator = new Translator()
@@ -185,8 +207,9 @@ export function AlfakitStarterKitRoot({
185
207
  <AlfakitRoot toasterId={STARTER_KIT_TOASTER_ID}>
186
208
  {children}
187
209
  {/* The debug panel's button lives in AlfaKit's top bar, which a starter kit doesn't
188
- render: float it in the corner instead (nothing without `alfakitDebug`). */}
189
- <div data-slot="starter-kit-debug-panel" className="fixed end-16 bottom-4 z-40">
210
+ render: float it in the corner instead (nothing without `alfakitDebug`). The corner is the
211
+ document's right: the kit's sidebar stays on the left in a right-to-left panel language. */}
212
+ <div data-slot="starter-kit-debug-panel" className="fixed right-16 bottom-16 z-40">
190
213
  <DebugPanelSlot />
191
214
  </div>
192
215
  </AlfakitRoot>
package/src/styles.css CHANGED
@@ -289,7 +289,8 @@
289
289
  }
290
290
  /* Rich editor mentions, merge tags and custom blocks (RichEditor extensions) */
291
291
  .alfakit-prose :where(.alfakit-mention):not(:where(.alfakit-not-prose *)) {
292
- @apply bg-primary/10 text-primary rounded px-1 font-medium;
292
+ /* Full-contrast text on the tint (WCAG AA); the ring marks it as a mention. */
293
+ @apply bg-primary/10 text-foreground ring-primary/30 rounded px-1 font-medium ring-1;
293
294
  }
294
295
  .alfakit-prose :where(.alfakit-merge-tag):not(:where(.alfakit-not-prose *)) {
295
296
  @apply bg-muted text-muted-foreground rounded px-1 font-mono text-xs;