@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.
- package/CHANGELOG.md +12 -0
- package/dist/components/actions/action-button.js +13 -1
- package/dist/components/actions/action-button.js.map +1 -1
- package/dist/components/dev-tools/dev-tools-inspector.js +2 -2
- package/dist/components/dev-tools/dev-tools-inspector.js.map +1 -1
- package/dist/components/entries/other-entries.d.ts.map +1 -1
- package/dist/components/entries/other-entries.js +1 -1
- package/dist/components/entries/other-entries.js.map +1 -1
- package/dist/components/fields/builder.d.ts.map +1 -1
- package/dist/components/fields/builder.js +3 -3
- package/dist/components/fields/builder.js.map +1 -1
- package/dist/components/fields/file-upload.d.ts.map +1 -1
- package/dist/components/fields/file-upload.js +2 -5
- package/dist/components/fields/file-upload.js.map +1 -1
- package/dist/components/fields/money-input.d.ts +5 -0
- package/dist/components/fields/money-input.d.ts.map +1 -1
- package/dist/components/fields/money-input.js +2 -5
- package/dist/components/fields/money-input.js.map +1 -1
- package/dist/components/fields/repeater.d.ts.map +1 -1
- package/dist/components/fields/repeater.js +3 -3
- package/dist/components/fields/repeater.js.map +1 -1
- package/dist/components/fields/rich-editor-extensions.js +5 -2
- package/dist/components/fields/rich-editor-extensions.js.map +1 -1
- package/dist/components/layout/wizard.d.ts.map +1 -1
- package/dist/components/layout/wizard.js +4 -1
- package/dist/components/layout/wizard.js.map +1 -1
- package/dist/components/notifications/flash-toasts.d.ts.map +1 -1
- package/dist/components/notifications/flash-toasts.js +11 -1
- package/dist/components/notifications/flash-toasts.js.map +1 -1
- package/dist/components/notifications/notifications-menu.js +3 -3
- package/dist/components/notifications/notifications-menu.js.map +1 -1
- package/dist/components/panel/lazy-placeholder.d.ts +1 -0
- package/dist/components/panel/lazy-placeholder.d.ts.map +1 -1
- package/dist/components/panel/lazy-placeholder.js +38 -4
- package/dist/components/panel/lazy-placeholder.js.map +1 -1
- package/dist/components/panel/pwa.d.ts.map +1 -1
- package/dist/components/panel/pwa.js +8 -7
- package/dist/components/panel/pwa.js.map +1 -1
- package/dist/components/panel/shortcuts-overlay.js +1 -1
- package/dist/components/panel/shortcuts-overlay.js.map +1 -1
- package/dist/components/table/cells.js +2 -2
- package/dist/components/table/cells.js.map +1 -1
- package/dist/components/ui/calendar.d.ts.map +1 -1
- package/dist/components/ui/calendar.js +7 -0
- package/dist/components/ui/calendar.js.map +1 -1
- package/dist/components/widgets/dashboard-customizer.js +2 -2
- package/dist/components/widgets/dashboard-customizer.js.map +1 -1
- package/dist/components/widgets/widgets-block.js +2 -2
- package/dist/components/widgets/widgets-block.js.map +1 -1
- package/dist/components.css +19 -0
- package/dist/create-alfakit.d.ts.map +1 -1
- package/dist/create-alfakit.js +3 -2
- package/dist/create-alfakit.js.map +1 -1
- package/dist/eject-manifest.json +4 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/pages/page.js +1 -1
- package/dist/pages/page.js.map +1 -1
- package/dist/standalone/alfakit-component.js +2 -2
- package/dist/standalone/alfakit-component.js.map +1 -1
- package/dist/standalone.css +21 -0
- package/dist/starter-kit/index.d.ts +2 -2
- package/dist/starter-kit/index.js +2 -2
- package/dist/starter-kit/starter-kit.d.ts +9 -0
- package/dist/starter-kit/starter-kit.d.ts.map +1 -1
- package/dist/starter-kit/starter-kit.js +21 -2
- package/dist/starter-kit/starter-kit.js.map +1 -1
- package/package.json +2 -2
- package/src/components/actions/action-button.tsx +15 -1
- package/src/components/dev-tools/dev-tools-inspector.tsx +6 -1
- package/src/components/entries/other-entries.tsx +2 -0
- package/src/components/fields/builder.tsx +7 -2
- package/src/components/fields/file-upload.tsx +2 -7
- package/src/components/fields/money-input.tsx +3 -14
- package/src/components/fields/repeater.tsx +6 -3
- package/src/components/fields/rich-editor-extensions.tsx +9 -5
- package/src/components/layout/wizard.tsx +5 -1
- package/src/components/notifications/flash-toasts.tsx +8 -0
- package/src/components/notifications/notifications-menu.tsx +3 -3
- package/src/components/panel/lazy-placeholder.tsx +41 -1
- package/src/components/panel/pwa.tsx +23 -9
- package/src/components/panel/shortcuts-overlay.tsx +1 -1
- package/src/components/table/cells.tsx +6 -2
- package/src/components/ui/calendar.tsx +8 -0
- package/src/components/widgets/dashboard-customizer.tsx +2 -2
- package/src/components/widgets/widgets-block.tsx +2 -2
- package/src/create-alfakit.tsx +9 -1
- package/src/pages/page.tsx +1 -1
- package/src/standalone/alfakit-component.tsx +2 -2
- package/src/starter-kit/index.ts +1 -0
- 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
|
|
50
|
-
//
|
|
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]
|
|
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={
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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={
|
|
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 {
|
|
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(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
<
|
|
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 {
|
|
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
|
-
<
|
|
52
|
+
<LazyWidgetPlaceholder widget={widget} />
|
|
53
53
|
) : (
|
|
54
54
|
<WidgetRenderer widget={widget} />
|
|
55
55
|
)}
|
package/src/create-alfakit.tsx
CHANGED
|
@@ -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(
|
|
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}
|
package/src/pages/page.tsx
CHANGED
|
@@ -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
|
|
209
|
+
className="max-w-full min-w-0"
|
|
210
210
|
/>
|
|
211
211
|
)}
|
|
212
212
|
</header>
|
package/src/starter-kit/index.ts
CHANGED
|
@@ -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()
|