@alfakit/react 0.1.10 → 0.1.12

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 (91) 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 +2 -2
  5. package/dist/components/dev-tools/dev-tools-inspector.js.map +1 -1
  6. package/dist/components/entries/other-entries.d.ts.map +1 -1
  7. package/dist/components/entries/other-entries.js +1 -1
  8. package/dist/components/entries/other-entries.js.map +1 -1
  9. package/dist/components/fields/builder.d.ts.map +1 -1
  10. package/dist/components/fields/builder.js +3 -3
  11. package/dist/components/fields/builder.js.map +1 -1
  12. package/dist/components/fields/file-upload.d.ts.map +1 -1
  13. package/dist/components/fields/file-upload.js +2 -5
  14. package/dist/components/fields/file-upload.js.map +1 -1
  15. package/dist/components/fields/money-input.d.ts +5 -0
  16. package/dist/components/fields/money-input.d.ts.map +1 -1
  17. package/dist/components/fields/money-input.js +2 -5
  18. package/dist/components/fields/money-input.js.map +1 -1
  19. package/dist/components/fields/repeater.d.ts.map +1 -1
  20. package/dist/components/fields/repeater.js +3 -3
  21. package/dist/components/fields/repeater.js.map +1 -1
  22. package/dist/components/fields/rich-editor-extensions.js +5 -2
  23. package/dist/components/fields/rich-editor-extensions.js.map +1 -1
  24. package/dist/components/layout/wizard.d.ts.map +1 -1
  25. package/dist/components/layout/wizard.js +4 -1
  26. package/dist/components/layout/wizard.js.map +1 -1
  27. package/dist/components/notifications/flash-toasts.d.ts.map +1 -1
  28. package/dist/components/notifications/flash-toasts.js +11 -1
  29. package/dist/components/notifications/flash-toasts.js.map +1 -1
  30. package/dist/components/notifications/notifications-menu.js +3 -3
  31. package/dist/components/notifications/notifications-menu.js.map +1 -1
  32. package/dist/components/panel/lazy-placeholder.d.ts +1 -0
  33. package/dist/components/panel/lazy-placeholder.d.ts.map +1 -1
  34. package/dist/components/panel/lazy-placeholder.js +38 -4
  35. package/dist/components/panel/lazy-placeholder.js.map +1 -1
  36. package/dist/components/panel/pwa.d.ts.map +1 -1
  37. package/dist/components/panel/pwa.js +8 -7
  38. package/dist/components/panel/pwa.js.map +1 -1
  39. package/dist/components/panel/shortcuts-overlay.js +1 -1
  40. package/dist/components/panel/shortcuts-overlay.js.map +1 -1
  41. package/dist/components/table/cells.js +2 -2
  42. package/dist/components/table/cells.js.map +1 -1
  43. package/dist/components/ui/calendar.d.ts.map +1 -1
  44. package/dist/components/ui/calendar.js +7 -0
  45. package/dist/components/ui/calendar.js.map +1 -1
  46. package/dist/components/widgets/dashboard-customizer.js +2 -2
  47. package/dist/components/widgets/dashboard-customizer.js.map +1 -1
  48. package/dist/components/widgets/widgets-block.js +2 -2
  49. package/dist/components/widgets/widgets-block.js.map +1 -1
  50. package/dist/components.css +19 -0
  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.js +1 -1
  58. package/dist/pages/page.js.map +1 -1
  59. package/dist/standalone/alfakit-component.js +2 -2
  60. package/dist/standalone/alfakit-component.js.map +1 -1
  61. package/dist/standalone.css +21 -0
  62. package/dist/starter-kit/index.d.ts +2 -2
  63. package/dist/starter-kit/index.js +2 -2
  64. package/dist/starter-kit/starter-kit.d.ts +9 -0
  65. package/dist/starter-kit/starter-kit.d.ts.map +1 -1
  66. package/dist/starter-kit/starter-kit.js +21 -2
  67. package/dist/starter-kit/starter-kit.js.map +1 -1
  68. package/package.json +2 -2
  69. package/src/components/actions/action-button.tsx +15 -1
  70. package/src/components/dev-tools/dev-tools-inspector.tsx +6 -1
  71. package/src/components/entries/other-entries.tsx +2 -0
  72. package/src/components/fields/builder.tsx +7 -2
  73. package/src/components/fields/file-upload.tsx +2 -7
  74. package/src/components/fields/money-input.tsx +3 -14
  75. package/src/components/fields/repeater.tsx +6 -3
  76. package/src/components/fields/rich-editor-extensions.tsx +9 -5
  77. package/src/components/layout/wizard.tsx +5 -1
  78. package/src/components/notifications/flash-toasts.tsx +8 -0
  79. package/src/components/notifications/notifications-menu.tsx +3 -3
  80. package/src/components/panel/lazy-placeholder.tsx +41 -1
  81. package/src/components/panel/pwa.tsx +23 -9
  82. package/src/components/panel/shortcuts-overlay.tsx +1 -1
  83. package/src/components/table/cells.tsx +6 -2
  84. package/src/components/ui/calendar.tsx +8 -0
  85. package/src/components/widgets/dashboard-customizer.tsx +2 -2
  86. package/src/components/widgets/widgets-block.tsx +2 -2
  87. package/src/create-alfakit.tsx +9 -1
  88. package/src/pages/page.tsx +1 -1
  89. package/src/standalone/alfakit-component.tsx +2 -2
  90. package/src/starter-kit/index.ts +1 -0
  91. package/src/starter-kit/starter-kit.tsx +22 -0
@@ -13,13 +13,6 @@ import { Input } from '../ui/input'
13
13
  * it, accepts the locale's separators and digits ("1.234,50", "١٢٫٥"), and rounds to
14
14
  * the currency's precision on blur. The form value is a plain decimal string ("1234.50").
15
15
  */
16
- export function clampAmount(amount: number, min?: number | null, max?: number | null): number {
17
- const lower = min ?? Number.NEGATIVE_INFINITY
18
- const upper = max ?? Number.POSITIVE_INFINITY
19
-
20
- return Math.min(upper, Math.max(lower, amount))
21
- }
22
-
23
16
  export function MoneyInputField({ component }: { component: MoneyInputSchema }) {
24
17
  const { value, setValue, blur, error, id, disabled, aria } = useField<string | number | null>(
25
18
  component,
@@ -46,13 +39,9 @@ export function MoneyInputField({ component }: { component: MoneyInputSchema })
46
39
 
47
40
  const parsed = format.parseNumber(text)
48
41
 
49
- // Unparseable input is kept as typed so server validation can explain the problem;
50
- // amounts are brought inside `minValue()` / `maxValue()`.
51
- setValue(
52
- parsed === null
53
- ? text
54
- : clampAmount(parsed, component.min, component.max).toFixed(component.decimals),
55
- )
42
+ // Unparseable input is kept as typed and out-of-range amounts are kept as well, so the
43
+ // server's `minValue()` / `maxValue()` rules report them (W-V3-11).
44
+ setValue(parsed === null ? text : parsed.toFixed(component.decimals))
56
45
  }
57
46
 
58
47
  const affix = (text: string, icon: string | undefined, side: 'start' | 'end') =>
@@ -1,6 +1,5 @@
1
1
  import type { RepeaterSchema, SchemaComponent } from '@alfakit/core'
2
2
  import {
3
- VIRTUAL_ESTIMATES,
4
3
  appendRepeaterItem,
5
4
  cloneIndexedState,
6
5
  cloneRepeaterItem,
@@ -8,12 +7,14 @@ import {
8
7
  fieldIds,
9
8
  gridColumnsClasses,
10
9
  isDeferredPlaceholder,
10
+ itemExtraActions,
11
11
  moveIndexedState,
12
12
  moveRepeaterItem,
13
13
  removeIndexedState,
14
14
  removeRepeaterItem,
15
15
  reorderedItemKeys,
16
16
  templateForIndex,
17
+ VIRTUAL_ESTIMATES,
17
18
  virtualizeOptions,
18
19
  } from '@alfakit/core'
19
20
  import { useState } from 'react'
@@ -165,7 +166,7 @@ function RepeaterFieldContent({ component }: { component: RepeaterSchema }) {
165
166
  virtual={virtual}
166
167
  context={context}
167
168
  resolve={(name, index) => resolve(name, component.items[index])}
168
- extraActionsFor={(index) => component.items[index]?.actions}
169
+ extraActionsFor={(index) => itemExtraActions(component, component.items[index])}
169
170
  />
170
171
  ) : (
171
172
  <>
@@ -245,7 +246,9 @@ function RepeaterFieldContent({ component }: { component: RepeaterSchema }) {
245
246
  index={index}
246
247
  count={items.length}
247
248
  resolve={(name) => resolve(name, serverItem)}
248
- extraActions={locked ? undefined : serverItem?.actions}
249
+ extraActions={
250
+ locked ? undefined : itemExtraActions(component, serverItem)
251
+ }
249
252
  buttons={buttons}
250
253
  canClone={Boolean(component.cloneable) && canAdd}
251
254
  canDelete={canDelete}
@@ -152,11 +152,15 @@ export function useRichEditorExtensions(
152
152
 
153
153
  if (!result || !editor || editor.isDestroyed) return
154
154
 
155
- editor
156
- .chain()
157
- .focus()
158
- .insertContent({ type: 'customBlock', attrs: { id: result.id, config: result.config } })
159
- .run()
155
+ const block = { type: 'customBlock', attrs: { id: result.id, config: result.config } }
156
+ // A block inserted just before is still selected (a node selection): insert after it
157
+ // instead of replacing it (W-S2-05).
158
+ const { selection } = editor.state
159
+ const chain = editor.chain().focus()
160
+ ;('node' in selection && selection.node
161
+ ? chain.insertContentAt(selection.to, block)
162
+ : chain.insertContent(block)
163
+ ).run()
160
164
  }
161
165
 
162
166
  return { extensions, suggestions, insertBlock, context }
@@ -21,6 +21,7 @@ import { cn } from '../../lib/utils'
21
21
  import { ActionButton } from '../actions/action-button'
22
22
  import { usePageActionContext } from '../actions/action-context'
23
23
  import { useOptionalFormStore } from '../form/form-context'
24
+ import { useFieldActionContext } from '../schema/field-actions'
24
25
  import { SchemaRenderer } from '../schema/schema-renderer'
25
26
  import { Button } from '../ui/button'
26
27
  import { DeferredSchema } from './deferred'
@@ -52,6 +53,9 @@ export function WizardComponent({ component }: { component: WizardSchema }) {
52
53
  if (stepQueryKey) persistWizardStep(current, stepQueryKey)
53
54
  }, [current, stepQueryKey])
54
55
  const store = useOptionalFormStore()
56
+ // Inside a form the submit action runs as a field action, so `$get` / `$set` work (W-S2-09).
57
+ const fieldContext = useFieldActionContext(component)
58
+ const submitContext = store ? fieldContext : actionContext
55
59
 
56
60
  // An action modal around this wizard shows its submit button on the last step only.
57
61
  useEffect(() => {
@@ -229,7 +233,7 @@ export function WizardComponent({ component }: { component: WizardSchema }) {
229
233
  </Button>
230
234
  )
231
235
  ) : component.submitAction ? (
232
- <ActionButton action={component.submitAction} context={actionContext} />
236
+ <ActionButton action={component.submitAction} context={submitContext} />
233
237
  ) : component.submitActionHtml ? (
234
238
  <div
235
239
  data-slot="wizard-submit"
@@ -43,6 +43,14 @@ export function setToasterId(id: string | undefined): void {
43
43
  toasterId = id
44
44
  }
45
45
 
46
+ /**
47
+ * Options for a direct `toast.*()` call: the starter kit toaster's id merged in, so PWA and
48
+ * notification messages reach AlfaKit's toaster in a starter kit too (W-S1-03).
49
+ */
50
+ export function toastOptions<T extends object>(options?: T): T & { toasterId?: string } {
51
+ return { ...(options ?? ({} as T)), toasterId }
52
+ }
53
+
46
54
  /** `closeNotification(id)` (or a `close-notification` event) dismisses that toast. */
47
55
  export function listenForClosedNotifications(): void {
48
56
  if (closeListening || typeof window === 'undefined') return
@@ -19,7 +19,7 @@ import { Button } from '../ui/button'
19
19
  import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'
20
20
  import { ScrollArea } from '../ui/scroll-area'
21
21
  import { Spinner } from '../ui/spinner'
22
- import { NotificationActionButton } from './flash-toasts'
22
+ import { NotificationActionButton, toastOptions } from './flash-toasts'
23
23
 
24
24
  export interface NotificationsMenuProps {
25
25
  /**
@@ -104,8 +104,8 @@ export function NotificationsMenu({ variant = 'topbar' }: NotificationsMenuProps
104
104
  const { message } = await togglePushNotifications()
105
105
  const text = t(message.key, {}, message.fallback)
106
106
 
107
- if (message.error) toast.error(text)
108
- else toast.success(text)
107
+ if (message.error) toast.error(text, toastOptions())
108
+ else toast.success(text, toastOptions())
109
109
  }
110
110
 
111
111
  const pushLabel =
@@ -1,5 +1,8 @@
1
- import { useT } from '../../context'
1
+ import { widgetPollTarget, type LazyWidgetSchema } from '@alfakit/core'
2
+ import { useState } from 'react'
3
+ import { useRefreshComponents, useT } from '../../context'
2
4
  import { cn } from '../../lib/utils'
5
+ import { Button } from '../ui/button'
3
6
  import { Skeleton } from '../ui/skeleton'
4
7
 
5
8
  /**
@@ -37,3 +40,40 @@ export function LazyPlaceholder({
37
40
  </div>
38
41
  )
39
42
  }
43
+
44
+ /**
45
+ * A lazy widget's cell: the skeleton while its schema loads, or, when the request failed
46
+ * (outside panels: a refused or failed fetch), a message with a retry.
47
+ */
48
+ export function LazyWidgetPlaceholder({ widget }: { widget: LazyWidgetSchema }) {
49
+ const t = useT()
50
+ const refresh = useRefreshComponents()
51
+ const [retrying, setRetrying] = useState(false)
52
+ const variant = widget.type === 'TableWidget' ? 'table' : 'widget'
53
+
54
+ if (!widget.failed) return <LazyPlaceholder variant={variant} />
55
+
56
+ const retry = () => {
57
+ if (!refresh) return
58
+
59
+ setRetrying(true)
60
+ void Promise.resolve(refresh([widgetPollTarget(widget)])).finally(() => setRetrying(false))
61
+ }
62
+
63
+ return (
64
+ <div
65
+ data-slot="lazy-placeholder"
66
+ data-variant={variant}
67
+ data-failed=""
68
+ role="alert"
69
+ className="bg-card text-muted-foreground flex min-h-28 flex-col items-center justify-center gap-3 rounded-xl border border-dashed p-6 text-center text-sm"
70
+ >
71
+ <p>{t('widget_failed', {}, 'This widget could not be loaded.')}</p>
72
+ {refresh && (
73
+ <Button type="button" variant="outline" size="sm" disabled={retrying} onClick={retry}>
74
+ {t('retry', {}, 'Retry')}
75
+ </Button>
76
+ )}
77
+ </div>
78
+ )
79
+ }
@@ -14,6 +14,7 @@ import { toast } from 'sonner'
14
14
  import { usePanel, useT } from '../../context'
15
15
  import { useStore } from '../../hooks/use-store'
16
16
  import { Icon } from '../../lib/icons'
17
+ import { toastOptions } from '../notifications/flash-toasts'
17
18
  import { DropdownMenuItem } from '../ui/dropdown-menu'
18
19
 
19
20
  const UPDATE_TOAST_ID = 'alfakit-pwa-update'
@@ -27,7 +28,7 @@ export async function installPwa(t: Translator['t']): Promise<void> {
27
28
  {},
28
29
  'To install this app, tap the Share button and then “Add to Home Screen”.',
29
30
  ),
30
- { duration: 10000 },
31
+ toastOptions({ duration: 10000 }),
31
32
  )
32
33
  }
33
34
  }
@@ -49,11 +50,14 @@ export function PwaManager() {
49
50
  useEffect(() => {
50
51
  if (!updateAvailable) return
51
52
 
52
- toast(t('pwa.update_available', {}, 'A new version is available'), {
53
- id: UPDATE_TOAST_ID,
54
- duration: Infinity,
55
- action: { label: t('pwa.reload', {}, 'Reload'), onClick: () => applyPwaUpdate() },
56
- })
53
+ toast(
54
+ t('pwa.update_available', {}, 'A new version is available'),
55
+ toastOptions({
56
+ id: UPDATE_TOAST_ID,
57
+ duration: Infinity,
58
+ action: { label: t('pwa.reload', {}, 'Reload'), onClick: () => applyPwaUpdate() },
59
+ }),
60
+ )
57
61
  }, [updateAvailable, t])
58
62
 
59
63
  return null
@@ -74,15 +78,25 @@ export function PwaMenuItems() {
74
78
  state.push === 'subscribed' ? await unsubscribeFromPush() : await subscribeToPush()
75
79
 
76
80
  if (outcome === 'subscribed') {
77
- toast.success(t('pwa.notifications.enabled', {}, 'Notifications are on for this device'))
81
+ toast.success(
82
+ t('pwa.notifications.enabled', {}, 'Notifications are on for this device'),
83
+ toastOptions(),
84
+ )
78
85
  } else if (outcome === 'unsubscribed') {
79
- toast.success(t('pwa.notifications.disabled', {}, 'Notifications are off for this device'))
86
+ toast.success(
87
+ t('pwa.notifications.disabled', {}, 'Notifications are off for this device'),
88
+ toastOptions(),
89
+ )
80
90
  } else if (outcome === 'denied') {
81
91
  toast.error(
82
92
  t('pwa.notifications.blocked', {}, 'Notifications are blocked in your browser settings'),
93
+ toastOptions(),
83
94
  )
84
95
  } else {
85
- toast.error(t('pwa.notifications.failed', {}, 'Couldn’t change notifications. Try again.'))
96
+ toast.error(
97
+ t('pwa.notifications.failed', {}, 'Couldn’t change notifications. Try again.'),
98
+ toastOptions(),
99
+ )
86
100
  }
87
101
  }
88
102
 
@@ -221,7 +221,7 @@ export function ShortcutsOverlay({
221
221
  aria-label={t(
222
222
  'shortcuts.reset_label',
223
223
  { label: item.label },
224
- `Restore the default shortcut for ${item.label}`,
224
+ `Reset the shortcut for ${item.label}`,
225
225
  )}
226
226
  onClick={() => void save(item.id, null)}
227
227
  >
@@ -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),
@@ -240,7 +242,9 @@ function ImageCell({ column, cell }: CellProps) {
240
242
  <span
241
243
  data-slot="image-remaining"
242
244
  className={cn(
243
- 'text-muted-foreground ms-1',
245
+ 'text-muted-foreground',
246
+ // The last image keeps the stack's negative end margin (W-S3-03).
247
+ schema.stacked && usesDefaultImageStack(schema) ? 'ms-3' : 'ms-1',
244
248
  remainingTextSizeClass(schema.limitedRemainingTextSize),
245
249
  )}
246
250
  >
@@ -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
  )
@@ -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}
@@ -281,7 +281,7 @@ export function ContentBlockRenderer({ block }: { block: ContentBlock }) {
281
281
  <div data-slot="infolist" className="contents">
282
282
  <SchemaRenderer
283
283
  components={(block as InfolistBlock).components}
284
- columns={{ default: 1, lg: 2 }}
284
+ columns={(block as InfolistBlock).columns ?? { default: 1, lg: 2 }}
285
285
  />
286
286
  </div>
287
287
  )
@@ -193,7 +193,7 @@ export function AlfakitComponent({
193
193
  data-slot="page-header"
194
194
  className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
195
195
  >
196
- <div className="min-w-0">
196
+ <div className="min-w-0 sm:max-w-[50%] sm:shrink-0">
197
197
  {page.heading && (
198
198
  <h2 className="text-xl font-semibold tracking-tight">{page.heading}</h2>
199
199
  )}
@@ -206,7 +206,7 @@ export function AlfakitComponent({
206
206
  actions={page.headerActions}
207
207
  context={actionContext}
208
208
  align="end"
209
- className="max-w-full shrink-0"
209
+ className="max-w-full min-w-0"
210
210
  />
211
211
  )}
212
212
  </header>
@@ -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,
@@ -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()