@alfakit/react 0.1.1 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/components/actions/action-button.d.ts.map +1 -1
- package/dist/components/actions/action-button.js +5 -4
- package/dist/components/actions/action-button.js.map +1 -1
- package/dist/components/actions/action-modals.d.ts.map +1 -1
- package/dist/components/actions/action-modals.js +6 -5
- package/dist/components/actions/action-modals.js.map +1 -1
- package/dist/components/ai/copilot.d.ts +3 -1
- package/dist/components/ai/copilot.d.ts.map +1 -1
- package/dist/components/ai/copilot.js +4 -1
- package/dist/components/ai/copilot.js.map +1 -1
- package/dist/components/cms/live-preview.js +3 -2
- package/dist/components/cms/live-preview.js.map +1 -1
- package/dist/components/cms/page.js +1 -1
- package/dist/components/fields/builder.js +15 -6
- package/dist/components/fields/builder.js.map +1 -1
- package/dist/components/fields/color-picker.js +1 -0
- package/dist/components/fields/color-picker.js.map +1 -1
- package/dist/components/fields/file-upload.js +11 -10
- package/dist/components/fields/file-upload.js.map +1 -1
- package/dist/components/fields/image-editor-dialog.js +4 -2
- package/dist/components/fields/image-editor-dialog.js.map +1 -1
- package/dist/components/fields/item-actions.js +10 -10
- package/dist/components/fields/item-actions.js.map +1 -1
- package/dist/components/fields/key-value.d.ts.map +1 -1
- package/dist/components/fields/key-value.js +3 -3
- package/dist/components/fields/key-value.js.map +1 -1
- package/dist/components/fields/modal-table-select.js +6 -5
- package/dist/components/fields/modal-table-select.js.map +1 -1
- package/dist/components/fields/one-time-code-input.js +1 -1
- package/dist/components/fields/one-time-code-input.js.map +1 -1
- package/dist/components/fields/repeater-table.js +12 -1
- package/dist/components/fields/repeater-table.js.map +1 -1
- package/dist/components/fields/repeater.js +2 -2
- package/dist/components/fields/repeater.js.map +1 -1
- package/dist/components/fields/rich-editor.js +2 -1
- package/dist/components/fields/rich-editor.js.map +1 -1
- package/dist/components/fields/select.js +3 -3
- package/dist/components/fields/select.js.map +1 -1
- package/dist/components/file-manager/file-manager.js +1 -1
- package/dist/components/file-manager/file-manager.js.map +1 -1
- package/dist/components/form/form-block.d.ts.map +1 -1
- package/dist/components/form/form-block.js +14 -2
- package/dist/components/form/form-block.js.map +1 -1
- package/dist/components/form/form-context.js +7 -3
- package/dist/components/form/form-context.js.map +1 -1
- package/dist/components/kanban/kanban.js +4 -3
- package/dist/components/kanban/kanban.js.map +1 -1
- package/dist/components/layout/fused-group.js +1 -0
- package/dist/components/layout/fused-group.js.map +1 -1
- package/dist/components/layout/section.js +2 -2
- package/dist/components/layout/section.js.map +1 -1
- package/dist/components/layout/tabs.d.ts.map +1 -1
- package/dist/components/layout/tabs.js +54 -15
- package/dist/components/layout/tabs.js.map +1 -1
- package/dist/components/panel/app-sidebar.d.ts +2 -3
- package/dist/components/panel/app-sidebar.d.ts.map +1 -1
- package/dist/components/panel/app-sidebar.js +1 -24
- package/dist/components/panel/app-sidebar.js.map +1 -1
- package/dist/components/panel/brand.d.ts +10 -0
- package/dist/components/panel/brand.d.ts.map +1 -0
- package/dist/components/panel/brand.js +37 -0
- package/dist/components/panel/brand.js.map +1 -0
- package/dist/components/panel/global-search.js +1 -0
- package/dist/components/panel/global-search.js.map +1 -1
- package/dist/components/panel/index.d.ts +2 -1
- package/dist/components/panel/shortcut-keys.js +1 -0
- package/dist/components/panel/shortcut-keys.js.map +1 -1
- package/dist/components/panel/tenant-menu.js +2 -2
- package/dist/components/panel/tenant-menu.js.map +1 -1
- package/dist/components/panel/topbar.js +1 -1
- package/dist/components/realtime/live-records-pill.d.ts.map +1 -1
- package/dist/components/realtime/live-records-pill.js +6 -3
- package/dist/components/realtime/live-records-pill.js.map +1 -1
- package/dist/components/table/cells.d.ts +6 -0
- package/dist/components/table/cells.d.ts.map +1 -1
- package/dist/components/table/cells.js +15 -3
- package/dist/components/table/cells.js.map +1 -1
- package/dist/components/table/column-manager.js +8 -7
- package/dist/components/table/column-manager.js.map +1 -1
- package/dist/components/table/content-layout.js +2 -1
- package/dist/components/table/content-layout.js.map +1 -1
- package/dist/components/table/grouping.js +11 -1
- package/dist/components/table/grouping.js.map +1 -1
- package/dist/components/table/pagination.js +16 -13
- package/dist/components/table/pagination.js.map +1 -1
- package/dist/components/table/table-block.js +14 -4
- package/dist/components/table/table-block.js.map +1 -1
- package/dist/components/ui/dialog.d.ts.map +1 -1
- package/dist/components/ui/dialog.js +3 -1
- package/dist/components/ui/dialog.js.map +1 -1
- package/dist/components/ui/popover.d.ts.map +1 -1
- package/dist/components/ui/popover.js +3 -1
- package/dist/components/ui/popover.js.map +1 -1
- package/dist/components/ui/sheet.d.ts.map +1 -1
- package/dist/components/ui/sheet.js +3 -1
- package/dist/components/ui/sheet.js.map +1 -1
- package/dist/components/ui/tooltip.js +3 -1
- package/dist/components/ui/tooltip.js.map +1 -1
- package/dist/components.css +347 -0
- package/dist/context.d.ts +5 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +5 -1
- package/dist/context.js.map +1 -1
- package/dist/create-alfakit.d.ts.map +1 -1
- package/dist/create-alfakit.js +8 -5
- package/dist/create-alfakit.js.map +1 -1
- package/dist/eject-manifest.json +16 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +24 -21
- package/dist/layouts/app-layout.d.ts.map +1 -1
- package/dist/layouts/app-layout.js +2 -29
- package/dist/layouts/app-layout.js.map +1 -1
- package/dist/layouts/auth-layout.js +2 -2
- package/dist/layouts/auth-layout.js.map +1 -1
- package/dist/layouts/fullscreen-layout.js +1 -1
- package/dist/layouts/fullscreen-layout.js.map +1 -1
- package/dist/layouts/lazy-layouts.js +15 -0
- package/dist/layouts/lazy-layouts.js.map +1 -0
- package/dist/layouts/max-widths.js +34 -0
- package/dist/layouts/max-widths.js.map +1 -0
- package/dist/layouts/panel-layouts.d.ts +10 -0
- package/dist/layouts/panel-layouts.d.ts.map +1 -0
- package/dist/layouts/panel-layouts.js +20 -0
- package/dist/layouts/panel-layouts.js.map +1 -0
- package/dist/layouts/top-nav-layout.js +2 -2
- package/dist/layouts/top-nav-layout.js.map +1 -1
- package/dist/pages/page.d.ts.map +1 -1
- package/dist/pages/page.js +12 -7
- package/dist/pages/page.js.map +1 -1
- package/dist/standalone/alfakit-component.js +11 -6
- package/dist/standalone/alfakit-component.js.map +1 -1
- package/dist/standalone/alfakit-toaster.d.ts.map +1 -1
- package/dist/standalone/alfakit-toaster.js +8 -1
- package/dist/standalone/alfakit-toaster.js.map +1 -1
- package/dist/standalone.css +54 -0
- package/dist/starter-kit/dashboard-widgets.d.ts +33 -0
- package/dist/starter-kit/dashboard-widgets.d.ts.map +1 -0
- package/dist/starter-kit/dashboard-widgets.js +53 -0
- package/dist/starter-kit/dashboard-widgets.js.map +1 -0
- package/dist/starter-kit/impersonation-banner.js +34 -0
- package/dist/starter-kit/impersonation-banner.js.map +1 -0
- package/dist/starter-kit/index.d.ts +3 -2
- package/dist/starter-kit/index.js +3 -2
- package/dist/starter-kit/starter-kit.d.ts +6 -0
- package/dist/starter-kit/starter-kit.d.ts.map +1 -1
- package/dist/starter-kit/starter-kit.js +18 -7
- package/dist/starter-kit/starter-kit.js.map +1 -1
- package/dist/starter-kit/widgets-loaded.js +21 -5
- package/dist/starter-kit/widgets-loaded.js.map +1 -1
- package/dist/starter-kit/widgets-triggers.js +35 -3
- package/dist/starter-kit/widgets-triggers.js.map +1 -1
- package/dist/starter-kit/widgets.d.ts +13 -5
- package/dist/starter-kit/widgets.d.ts.map +1 -1
- package/dist/starter-kit/widgets.js +56 -18
- package/dist/starter-kit/widgets.js.map +1 -1
- package/dist/styles.css +15 -2
- package/dist/themes.css +8 -335
- package/package.json +2 -2
- package/src/components/actions/action-button.tsx +7 -4
- package/src/components/actions/action-modals.tsx +13 -1
- package/src/components/ai/copilot.tsx +14 -1
- package/src/components/cms/live-preview.tsx +5 -2
- package/src/components/fields/builder.tsx +14 -5
- package/src/components/fields/color-picker.tsx +1 -0
- package/src/components/fields/file-upload.tsx +21 -10
- package/src/components/fields/image-editor-dialog.tsx +7 -2
- package/src/components/fields/item-actions.tsx +32 -9
- package/src/components/fields/key-value.tsx +3 -2
- package/src/components/fields/modal-table-select.tsx +8 -3
- package/src/components/fields/one-time-code-input.tsx +1 -1
- package/src/components/fields/repeater-table.tsx +12 -1
- package/src/components/fields/repeater.tsx +2 -3
- package/src/components/fields/rich-editor.tsx +3 -0
- package/src/components/fields/select.tsx +7 -3
- package/src/components/file-manager/file-manager.tsx +1 -1
- package/src/components/form/form-block.tsx +26 -1
- package/src/components/form/form-context.tsx +3 -6
- package/src/components/kanban/kanban.tsx +16 -3
- package/src/components/layout/fused-group.tsx +5 -1
- package/src/components/layout/section.tsx +2 -2
- package/src/components/layout/tabs.tsx +72 -17
- package/src/components/panel/app-sidebar.tsx +2 -38
- package/src/components/panel/brand.tsx +46 -0
- package/src/components/panel/global-search.tsx +1 -1
- package/src/components/panel/shortcut-keys.tsx +4 -1
- package/src/components/panel/tenant-menu.tsx +2 -2
- package/src/components/realtime/live-records-pill.tsx +4 -3
- package/src/components/table/cells.tsx +27 -5
- package/src/components/table/column-manager.tsx +12 -10
- package/src/components/table/content-layout.tsx +5 -1
- package/src/components/table/grouping.tsx +15 -1
- package/src/components/table/pagination.tsx +16 -9
- package/src/components/table/table-block.tsx +25 -12
- package/src/components/ui/dialog.tsx +5 -1
- package/src/components/ui/popover.tsx +5 -1
- package/src/components/ui/sheet.tsx +5 -1
- package/src/components/ui/tooltip.tsx +5 -1
- package/src/context.tsx +10 -0
- package/src/create-alfakit.tsx +11 -2
- package/src/index.ts +2 -0
- package/src/layouts/app-layout.tsx +4 -28
- package/src/layouts/auth-layout.tsx +2 -2
- package/src/layouts/fullscreen-layout.tsx +1 -1
- package/src/layouts/lazy-layouts.ts +14 -0
- package/src/layouts/max-widths.ts +29 -0
- package/src/layouts/panel-layouts.ts +17 -0
- package/src/layouts/top-nav-layout.tsx +2 -2
- package/src/pages/page.tsx +21 -12
- package/src/standalone/alfakit-component.tsx +18 -7
- package/src/standalone/alfakit-toaster.tsx +21 -2
- package/src/starter-kit/dashboard-widgets.tsx +78 -0
- package/src/starter-kit/impersonation-banner.tsx +48 -0
- package/src/starter-kit/index.ts +2 -0
- package/src/starter-kit/starter-kit.tsx +31 -4
- package/src/starter-kit/widgets-loaded.tsx +23 -1
- package/src/starter-kit/widgets-triggers.tsx +37 -2
- package/src/starter-kit/widgets.tsx +95 -20
- package/src/styles.css +15 -2
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
ImageEditorData,
|
|
5
5
|
ImageEditorDragMode,
|
|
6
6
|
} from '@alfakit/core'
|
|
7
|
-
import { ImageEditor, loadEditorImage } from '@alfakit/core'
|
|
7
|
+
import { ImageEditor, imageEditorOptionsKey, loadEditorImage } from '@alfakit/core'
|
|
8
8
|
import {
|
|
9
9
|
FlipHorizontal2Icon,
|
|
10
10
|
FlipVertical2Icon,
|
|
@@ -63,9 +63,14 @@ export function ImageEditorDialog({
|
|
|
63
63
|
const [saving, setSaving] = useState(false)
|
|
64
64
|
const hintId = useId()
|
|
65
65
|
|
|
66
|
+
// Restart only for another image or other settings: the schema object itself is new on
|
|
67
|
+
// every render of a template item (a new builder block), which kept restarting the load.
|
|
68
|
+
const optionsKey = imageEditorOptionsKey(options)
|
|
69
|
+
|
|
66
70
|
useEffect(() => {
|
|
67
71
|
if (!request) return
|
|
68
72
|
|
|
73
|
+
const options = JSON.parse(optionsKey) as FileUploadImageEditorSchema
|
|
69
74
|
let cancelled = false
|
|
70
75
|
|
|
71
76
|
loadEditorImage(request.file)
|
|
@@ -97,7 +102,7 @@ export function ImageEditorDialog({
|
|
|
97
102
|
return null
|
|
98
103
|
})
|
|
99
104
|
}
|
|
100
|
-
}, [request,
|
|
105
|
+
}, [request, optionsKey])
|
|
101
106
|
|
|
102
107
|
const finish = (file: File | null) => {
|
|
103
108
|
request?.resolve(file)
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ItemMoveTracker,
|
|
13
13
|
addActionJustify,
|
|
14
14
|
itemActionContext,
|
|
15
|
+
itemActionIcon,
|
|
15
16
|
resolveItemAction,
|
|
16
17
|
withItemActionState,
|
|
17
18
|
} from '@alfakit/core'
|
|
@@ -154,19 +155,41 @@ export function useRunItemAction(context: ActionContext) {
|
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
/** Default looks of the built-in actions, for payloads without `itemActions
|
|
158
|
-
export function useItemActionFallbacks(
|
|
158
|
+
/** Default looks of the built-in actions, for payloads without `itemActions` (icons follow their `alfakit::` aliases). */
|
|
159
|
+
export function useItemActionFallbacks(
|
|
160
|
+
addLabel = '',
|
|
161
|
+
kind: 'repeater' | 'builder' = 'repeater',
|
|
162
|
+
): Record<ItemActionName, ItemActionFallback> {
|
|
159
163
|
const t = useT()
|
|
160
164
|
|
|
161
165
|
return {
|
|
162
166
|
add: { label: addLabel, icon: 'plus', variant: 'outline' },
|
|
163
|
-
delete: {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
delete: {
|
|
168
|
+
label: t('form.remove_item', {}, 'Remove'),
|
|
169
|
+
icon: itemActionIcon(kind, 'delete', 'trash-2'),
|
|
170
|
+
color: 'danger',
|
|
171
|
+
},
|
|
172
|
+
clone: { label: t('form.clone', {}, 'Clone'), icon: itemActionIcon(kind, 'clone', 'copy') },
|
|
173
|
+
reorder: {
|
|
174
|
+
label: t('form.reorder_item', {}, 'Reorder'),
|
|
175
|
+
icon: itemActionIcon(kind, 'reorder', 'grip-vertical'),
|
|
176
|
+
},
|
|
177
|
+
moveUp: {
|
|
178
|
+
label: t('form.move_up', {}, 'Move up'),
|
|
179
|
+
icon: itemActionIcon(kind, 'move-up', 'chevron-up'),
|
|
180
|
+
},
|
|
181
|
+
moveDown: {
|
|
182
|
+
label: t('form.move_down', {}, 'Move down'),
|
|
183
|
+
icon: itemActionIcon(kind, 'move-down', 'chevron-down'),
|
|
184
|
+
},
|
|
185
|
+
collapse: {
|
|
186
|
+
label: t('form.collapse_item', {}, 'Collapse'),
|
|
187
|
+
icon: itemActionIcon(kind, 'collapse', 'chevron-up'),
|
|
188
|
+
},
|
|
189
|
+
expand: {
|
|
190
|
+
label: t('form.expand_item', {}, 'Expand'),
|
|
191
|
+
icon: itemActionIcon(kind, 'expand', 'chevron-down'),
|
|
192
|
+
},
|
|
170
193
|
collapseAll: {
|
|
171
194
|
label: t('form.collapse_all', {}, 'Collapse all'),
|
|
172
195
|
icon: 'chevrons-down-up',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fieldIds,
|
|
3
|
+
itemActionIcon,
|
|
3
4
|
moveRepeaterItem,
|
|
4
5
|
reorderedItemKeys,
|
|
5
6
|
resolveItemAction,
|
|
@@ -51,11 +52,11 @@ export function KeyValueField({ component }: { component: KeyValueSchema }) {
|
|
|
51
52
|
})
|
|
52
53
|
const deleteAction = resolveItemAction('delete', component, undefined, {
|
|
53
54
|
label: t('form.remove_row', {}, 'Remove row'),
|
|
54
|
-
icon: 'trash-2',
|
|
55
|
+
icon: itemActionIcon('key-value', 'delete', 'trash-2'),
|
|
55
56
|
})
|
|
56
57
|
const reorderAction = resolveItemAction('reorder', component, undefined, {
|
|
57
58
|
label: t('form.reorder_item', {}, 'Reorder'),
|
|
58
|
-
icon: 'grip-vertical',
|
|
59
|
+
icon: itemActionIcon('key-value', 'reorder', 'grip-vertical'),
|
|
59
60
|
})
|
|
60
61
|
|
|
61
62
|
const update = (next: Row[]) => setValue(next)
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
XIcon,
|
|
21
21
|
} from 'lucide-react'
|
|
22
22
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
23
|
-
import { useChoice, useT } from '../../context'
|
|
23
|
+
import { useChoice, useFormatter, useT } from '../../context'
|
|
24
24
|
import { useStore } from '../../hooks/use-store'
|
|
25
25
|
import { Icon } from '../../lib/icons'
|
|
26
26
|
import { cn } from '../../lib/utils'
|
|
@@ -135,6 +135,7 @@ interface PickerProps {
|
|
|
135
135
|
|
|
136
136
|
function ModalTablePicker({ component, initial, onClose, onConfirm }: PickerProps) {
|
|
137
137
|
const t = useT()
|
|
138
|
+
const format = useFormatter()
|
|
138
139
|
const choice = useChoice()
|
|
139
140
|
const form = useFormStore()
|
|
140
141
|
const multiple = Boolean(component.multiple)
|
|
@@ -399,8 +400,12 @@ function ModalTablePicker({ component, initial, onClose, onConfirm }: PickerProp
|
|
|
399
400
|
{paging.total !== undefined
|
|
400
401
|
? t(
|
|
401
402
|
'table.showing',
|
|
402
|
-
{
|
|
403
|
-
|
|
403
|
+
{
|
|
404
|
+
first: format.number(paging.from),
|
|
405
|
+
last: format.number(paging.to),
|
|
406
|
+
total: format.number(paging.total),
|
|
407
|
+
},
|
|
408
|
+
'Showing :first–:last of :total',
|
|
404
409
|
)
|
|
405
410
|
: `${paging.from}–${paging.to}`}
|
|
406
411
|
</span>
|
|
@@ -25,7 +25,6 @@ export function OneTimeCodeInputField({ component }: { component: OneTimeCodeInp
|
|
|
25
25
|
inputMode="numeric"
|
|
26
26
|
autoComplete="one-time-code"
|
|
27
27
|
pattern="[0-9]*"
|
|
28
|
-
maxLength={component.length}
|
|
29
28
|
value={value ?? ''}
|
|
30
29
|
onChange={(event) => {
|
|
31
30
|
const code = oneTimeCodeDigits(event.target.value, component.length)
|
|
@@ -45,6 +44,7 @@ export function OneTimeCodeInputField({ component }: { component: OneTimeCodeInp
|
|
|
45
44
|
required={component.required}
|
|
46
45
|
autoFocus={component.autofocus}
|
|
47
46
|
data-slot="one-time-code-input"
|
|
47
|
+
data-input=""
|
|
48
48
|
{...aria}
|
|
49
49
|
{...extraProps(
|
|
50
50
|
component.extraInputAttributes,
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
TableColumnSchema,
|
|
8
8
|
} from '@alfakit/core'
|
|
9
9
|
import { resolveItemAction, tableColumnAlignmentClass, tableColumnCells } from '@alfakit/core'
|
|
10
|
+
import type { CSSProperties } from 'react'
|
|
10
11
|
import { useT } from '../../context'
|
|
11
12
|
import type { VirtualListBinding } from '../../hooks/use-virtual-list'
|
|
12
13
|
import { cn } from '../../lib/utils'
|
|
@@ -41,6 +42,16 @@ export interface RepeaterTableProps {
|
|
|
41
42
|
extraActionsFor?: (index: number) => ActionSchema[] | undefined
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* A fixed `width()` (`9rem`, `120px`) is also the column's minimum, so the table scrolls
|
|
47
|
+
* sideways instead of squeezing its fields; percentages stay flexible.
|
|
48
|
+
*/
|
|
49
|
+
function columnWidthStyle(width?: string | null): CSSProperties | undefined {
|
|
50
|
+
if (!width) return undefined
|
|
51
|
+
|
|
52
|
+
return width.trim().endsWith('%') ? { width } : { width, minWidth: width }
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
/** Header cells shared by the Repeater and RepeatableEntry tables. */
|
|
45
56
|
export function TableColumnHeaders({ columns }: { columns: TableColumnSchema[] }) {
|
|
46
57
|
return (
|
|
@@ -49,7 +60,7 @@ export function TableColumnHeaders({ columns }: { columns: TableColumnSchema[] }
|
|
|
49
60
|
<th
|
|
50
61
|
key={`${column.name ?? 'column'}-${index}`}
|
|
51
62
|
scope="col"
|
|
52
|
-
style={column.width
|
|
63
|
+
style={columnWidthStyle(column.width)}
|
|
53
64
|
className={cn(
|
|
54
65
|
'text-muted-foreground px-3 py-2 text-xs font-medium',
|
|
55
66
|
tableColumnAlignmentClass(column),
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
removeIndexedState,
|
|
14
14
|
removeRepeaterItem,
|
|
15
15
|
reorderedItemKeys,
|
|
16
|
-
|
|
16
|
+
templateForIndex,
|
|
17
17
|
virtualizeOptions,
|
|
18
18
|
} from '@alfakit/core'
|
|
19
19
|
import { useState } from 'react'
|
|
@@ -125,8 +125,7 @@ function RepeaterFieldContent({ component }: { component: RepeaterSchema }) {
|
|
|
125
125
|
)
|
|
126
126
|
|
|
127
127
|
const schemaFor = (index: number): SchemaComponent[] =>
|
|
128
|
-
component.items[index]?.schema ??
|
|
129
|
-
replacePlaceholder(component.template, '__INDEX__', String(index))
|
|
128
|
+
component.items[index]?.schema ?? templateForIndex(component.template, index)
|
|
130
129
|
|
|
131
130
|
const canAdd =
|
|
132
131
|
component.addable &&
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
attachToolbarKeyboard,
|
|
10
10
|
commandLabel,
|
|
11
11
|
detailsNodeConfigs,
|
|
12
|
+
documentNonce,
|
|
12
13
|
EditorAttachments,
|
|
13
14
|
fieldIds,
|
|
14
15
|
fileAttachmentError,
|
|
@@ -403,6 +404,8 @@ function RichEditorInput({
|
|
|
403
404
|
useEffect(() => () => attachments.destroy(), [attachments])
|
|
404
405
|
|
|
405
406
|
const editor = useEditor({
|
|
407
|
+
// TipTap injects its base CSS; with the page's CSP nonce a nonce-based policy allows it.
|
|
408
|
+
injectNonce: documentNonce(),
|
|
406
409
|
extensions: [
|
|
407
410
|
StarterKit.configure({ link: false }),
|
|
408
411
|
Link.configure(linkExtensionOptions(component.linkProtocols)),
|
|
@@ -65,7 +65,9 @@ export function SelectField({ component }: { component: SelectSchema }) {
|
|
|
65
65
|
className="border-input dark:bg-input/30 focus-visible:ring-ring/50 h-9 w-full rounded-md border bg-transparent px-3 text-sm shadow-xs outline-none focus-visible:ring-[3px]"
|
|
66
66
|
>
|
|
67
67
|
{component.selectablePlaceholder && (
|
|
68
|
-
<option value="">
|
|
68
|
+
<option value="">
|
|
69
|
+
{component.placeholder ?? t('select_option', {}, 'Select an option')}
|
|
70
|
+
</option>
|
|
69
71
|
)}
|
|
70
72
|
{component.options.map((option) =>
|
|
71
73
|
isOptionGroup(option) ? (
|
|
@@ -103,7 +105,9 @@ export function SelectField({ component }: { component: SelectSchema }) {
|
|
|
103
105
|
aria-readonly={readOnly || undefined}
|
|
104
106
|
className="w-full"
|
|
105
107
|
>
|
|
106
|
-
<SelectValue
|
|
108
|
+
<SelectValue
|
|
109
|
+
placeholder={component.placeholder ?? t('select_option', {}, 'Select an option')}
|
|
110
|
+
/>
|
|
107
111
|
</SelectTrigger>
|
|
108
112
|
<SelectContent>
|
|
109
113
|
{component.selectablePlaceholder &&
|
|
@@ -411,7 +415,7 @@ function ComboboxSelect({
|
|
|
411
415
|
error && 'border-destructive',
|
|
412
416
|
)}
|
|
413
417
|
>
|
|
414
|
-
<span className="flex flex-1 flex-wrap items-center gap-1 text-start">
|
|
418
|
+
<span className="flex min-w-0 flex-1 flex-wrap items-center gap-1 text-start">
|
|
415
419
|
{selected.length === 0 && (
|
|
416
420
|
<span className="text-muted-foreground">
|
|
417
421
|
{component.placeholder ?? t('select_option', {}, 'Select an option')}
|
|
@@ -322,7 +322,7 @@ export function FileManagerView({ schema, onChoose, onCancel }: FileManagerViewP
|
|
|
322
322
|
)}
|
|
323
323
|
<nav
|
|
324
324
|
aria-label={tt('breadcrumb', {}, 'Folder path')}
|
|
325
|
-
className="min-w-0 flex-
|
|
325
|
+
className="min-w-0 flex-[1_1_12rem]"
|
|
326
326
|
data-slot="file-manager-breadcrumb"
|
|
327
327
|
>
|
|
328
328
|
<ol className="flex min-w-0 flex-wrap items-center gap-1 text-sm">
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
registerFormSubmitter,
|
|
14
14
|
registerPageForm,
|
|
15
15
|
resolvePartialProps,
|
|
16
|
+
shouldResyncForm,
|
|
16
17
|
wizardProgressStore,
|
|
17
18
|
} from '@alfakit/core'
|
|
18
19
|
import { router } from '@inertiajs/react'
|
|
@@ -93,6 +94,10 @@ export function FormBlock({ block }: { block: FormBlockSchema }) {
|
|
|
93
94
|
const latestBlock = useRef(block)
|
|
94
95
|
const submittedBlock = useRef<FormBlockSchema | null>(null)
|
|
95
96
|
const refillPending = useRef(false)
|
|
97
|
+
// Another action succeeded and its new block has not arrived yet.
|
|
98
|
+
const resyncPending = useRef(false)
|
|
99
|
+
// The block when the latest non-GET visit started, to tell whether its response arrived.
|
|
100
|
+
const visitBlock = useRef<FormBlockSchema | null>(null)
|
|
96
101
|
|
|
97
102
|
useEffect(() => {
|
|
98
103
|
latestBlock.current = block
|
|
@@ -101,6 +106,12 @@ export function FormBlock({ block }: { block: FormBlockSchema }) {
|
|
|
101
106
|
refillPending.current = false
|
|
102
107
|
store.fill(block)
|
|
103
108
|
}
|
|
109
|
+
|
|
110
|
+
if (resyncPending.current) {
|
|
111
|
+
resyncPending.current = false
|
|
112
|
+
|
|
113
|
+
if (!store.isDirty) store.fill(block)
|
|
114
|
+
}
|
|
104
115
|
}, [block, store])
|
|
105
116
|
|
|
106
117
|
const actions: ActionSchema[] = useMemo(
|
|
@@ -117,7 +128,8 @@ export function FormBlock({ block }: { block: FormBlockSchema }) {
|
|
|
117
128
|
submitting.current = true
|
|
118
129
|
submittedBlock.current = latestBlock.current
|
|
119
130
|
store.setProcessing(true)
|
|
120
|
-
|
|
131
|
+
// A click that blurred a live field started a refresh: send the state it returns.
|
|
132
|
+
store.whenSettled(() => runner.execute(action, ctx, store.state.values))
|
|
121
133
|
},
|
|
122
134
|
[runner, store],
|
|
123
135
|
)
|
|
@@ -152,11 +164,24 @@ export function FormBlock({ block }: { block: FormBlockSchema }) {
|
|
|
152
164
|
if (submitting.current && !submitVisit.current && visit.method !== 'get') {
|
|
153
165
|
submitVisit.current = visit.id
|
|
154
166
|
}
|
|
167
|
+
|
|
168
|
+
if (visit.method !== 'get') visitBlock.current = latestBlock.current
|
|
155
169
|
})
|
|
156
170
|
|
|
157
171
|
const offSuccess = router.on('success', (event) => {
|
|
158
172
|
const id = submitVisit.current
|
|
159
173
|
|
|
174
|
+
// Another action changed the record: show the server's new form unless it holds edits.
|
|
175
|
+
if (!id && shouldResyncForm(pageFlash(event.detail.page), store)) {
|
|
176
|
+
if (latestBlock.current !== visitBlock.current) {
|
|
177
|
+
store.fill(latestBlock.current)
|
|
178
|
+
} else {
|
|
179
|
+
resyncPending.current = true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
160
185
|
if (!id || (event.detail.visitId ?? id) !== id) return
|
|
161
186
|
|
|
162
187
|
// A halted action (rate limit, `halt()`) saved nothing: keep what the user typed.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BaseField, FieldAriaAttributes, FormState, FormStore } from '@alfakit/core'
|
|
2
|
-
import { fieldAria } from '@alfakit/core'
|
|
2
|
+
import { fieldAria, fieldDomId, fieldError } from '@alfakit/core'
|
|
3
3
|
import { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react'
|
|
4
4
|
import { useStore, useStoreSelector } from '../../hooks/use-store'
|
|
5
5
|
|
|
@@ -60,15 +60,12 @@ export function useField<T = unknown>(field: BaseField): FieldBinding<T> {
|
|
|
60
60
|
return current as T
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
const error = useStoreSelector(store.store, (state) => state.errors
|
|
63
|
+
const error = useStoreSelector(store.store, (state) => fieldError(state.errors, path))
|
|
64
64
|
|
|
65
65
|
const setValue = useCallback((next: T) => store.setValue(path, next), [store, path])
|
|
66
66
|
const blur = useCallback(() => store.blur(path), [store, path])
|
|
67
67
|
|
|
68
|
-
const id = useMemo(
|
|
69
|
-
() => field.id ?? `alfakit-${path.replace(/[^a-zA-Z0-9_-]/g, '-')}`,
|
|
70
|
-
[field.id, path],
|
|
71
|
-
)
|
|
68
|
+
const id = useMemo(() => field.id ?? fieldDomId(store.idPrefix, path), [field.id, store, path])
|
|
72
69
|
|
|
73
70
|
const aria = useMemo(
|
|
74
71
|
() =>
|
|
@@ -28,7 +28,14 @@ import {
|
|
|
28
28
|
SearchIcon,
|
|
29
29
|
} from 'lucide-react'
|
|
30
30
|
import { Fragment, useEffect, useId, useRef, useState, type KeyboardEvent } from 'react'
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
useActionRunner,
|
|
33
|
+
useChoice,
|
|
34
|
+
useDirection,
|
|
35
|
+
useFormatter,
|
|
36
|
+
usePanel,
|
|
37
|
+
useT,
|
|
38
|
+
} from '../../context'
|
|
32
39
|
import { useStore } from '../../hooks/use-store'
|
|
33
40
|
import { badgeColorClasses, colorClass, cssColor } from '../../lib/colors'
|
|
34
41
|
import { Icon } from '../../lib/icons'
|
|
@@ -424,6 +431,7 @@ function ColumnHeader({
|
|
|
424
431
|
onCreate: () => void
|
|
425
432
|
t: Translate
|
|
426
433
|
}) {
|
|
434
|
+
const choice = useChoice()
|
|
427
435
|
const wip = kanbanWipState(column)
|
|
428
436
|
const shown =
|
|
429
437
|
column.wipLimit !== undefined ? column.count : filtered ? column.total : column.count
|
|
@@ -436,7 +444,7 @@ function ColumnHeader({
|
|
|
436
444
|
{ shown: column.total, count: column.count },
|
|
437
445
|
':shown of :count cards',
|
|
438
446
|
)
|
|
439
|
-
:
|
|
447
|
+
: choice('kanban.count', column.count, {}, '{1} :count card|[0,*] :count cards')
|
|
440
448
|
const style = column.color ? { borderTopColor: cssColor(column.color) } : undefined
|
|
441
449
|
const toggleLabel = collapsed
|
|
442
450
|
? t('kanban.expand', { column: column.label }, 'Expand :column')
|
|
@@ -532,7 +540,12 @@ function ColumnHeader({
|
|
|
532
540
|
)}
|
|
533
541
|
{wip === 'over' && (
|
|
534
542
|
<p className="text-destructive text-xs">
|
|
535
|
-
{
|
|
543
|
+
{choice(
|
|
544
|
+
'kanban.wip_over',
|
|
545
|
+
column.wipLimit ?? 0,
|
|
546
|
+
{ limit: column.wipLimit ?? 0 },
|
|
547
|
+
'{1} Over the limit of :limit card|[0,*] Over the limit of :limit cards',
|
|
548
|
+
)}
|
|
536
549
|
</p>
|
|
537
550
|
)}
|
|
538
551
|
{wip === 'full' && column.wipStrict && (
|
|
@@ -25,7 +25,11 @@ export function FusedGroupComponent({ component }: { component: FusedGroupSchema
|
|
|
25
25
|
{...extraProps(component.extraAttributes, 'flex flex-col gap-1.5')}
|
|
26
26
|
>
|
|
27
27
|
{component.label && (
|
|
28
|
-
<span
|
|
28
|
+
<span
|
|
29
|
+
id={labelId}
|
|
30
|
+
data-slot="label"
|
|
31
|
+
className="text-sm leading-none font-medium select-none"
|
|
32
|
+
>
|
|
29
33
|
{component.label}
|
|
30
34
|
</span>
|
|
31
35
|
)}
|
|
@@ -76,11 +76,11 @@ export function SectionComponent({ component }: { component: SectionSchema }) {
|
|
|
76
76
|
{component.heading &&
|
|
77
77
|
createElement(
|
|
78
78
|
headingTag(component.headingLevel),
|
|
79
|
-
{ className: 'text-base leading-6 font-semibold' },
|
|
79
|
+
{ className: 'text-base leading-6 font-semibold break-words' },
|
|
80
80
|
component.heading,
|
|
81
81
|
)}
|
|
82
82
|
{component.description && (
|
|
83
|
-
<p className="text-muted-foreground mt-1 text-sm">{component.description}</p>
|
|
83
|
+
<p className="text-muted-foreground mt-1 text-sm break-words">{component.description}</p>
|
|
84
84
|
)}
|
|
85
85
|
</div>
|
|
86
86
|
{component.headerActions && component.headerActions.length > 0 && (
|
|
@@ -1,30 +1,51 @@
|
|
|
1
1
|
import type { TabSchema, TabsSchema } from '@alfakit/core'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
componentHasErrors,
|
|
4
|
+
readPersisted,
|
|
5
|
+
schemaGapClass,
|
|
6
|
+
tabFromQuery,
|
|
7
|
+
tabQueryValue,
|
|
8
|
+
tabToReveal,
|
|
9
|
+
writePersisted,
|
|
10
|
+
} from '@alfakit/core'
|
|
3
11
|
import { usePage } from '@inertiajs/react'
|
|
4
12
|
import { useEffect, useState } from 'react'
|
|
13
|
+
import { useT } from '../../context'
|
|
5
14
|
import { useHydrated } from '../../hooks/use-hydrated'
|
|
15
|
+
import { useStoreSelector } from '../../hooks/use-store'
|
|
6
16
|
import { extraProps } from '../../lib/attributes'
|
|
7
17
|
import { badgeColorClasses, colorClass } from '../../lib/colors'
|
|
8
18
|
import { Icon } from '../../lib/icons'
|
|
9
19
|
import { cn } from '../../lib/utils'
|
|
20
|
+
import { useOptionalFormStore } from '../form/form-context'
|
|
10
21
|
import { SchemaRenderer } from '../schema/schema-renderer'
|
|
11
22
|
import { Badge } from '../ui/badge'
|
|
12
23
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'
|
|
13
24
|
import { DeferredSchema, useDeferredBadge } from './deferred'
|
|
14
25
|
|
|
26
|
+
// Used when tabs render outside a form. Module-level so the snapshot stays stable.
|
|
27
|
+
const NO_ERRORS: Record<string, string[]> = {}
|
|
28
|
+
const NO_FORM_STORE = {
|
|
29
|
+
get: () => ({ errors: NO_ERRORS }),
|
|
30
|
+
subscribe: () => () => {},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const TAB_INVALID_CLASSES =
|
|
34
|
+
'text-destructive data-[state=active]:text-destructive dark:text-destructive dark:data-[state=active]:text-destructive'
|
|
35
|
+
|
|
15
36
|
export function TabsComponent({ component }: { component: TabsSchema }) {
|
|
16
37
|
const tabs = component.schema ?? []
|
|
17
38
|
// The page URL (not `window.location`) so the server renders the same tab as the browser.
|
|
18
39
|
const url = usePage().url
|
|
40
|
+
// The tab's readable value (`?tab=billing-address`), or its key in links from older versions.
|
|
19
41
|
const fromQuery = component.queryStringKey
|
|
20
|
-
?
|
|
42
|
+
? tabFromQuery(
|
|
43
|
+
tabs,
|
|
44
|
+
new URL(url, 'http://localhost').searchParams.get(component.queryStringKey),
|
|
45
|
+
)
|
|
21
46
|
: null
|
|
22
47
|
const [initial] = useState<string>(
|
|
23
|
-
() =>
|
|
24
|
-
tabs.find((tab) => tab.key === fromQuery)?.key ??
|
|
25
|
-
tabs[Math.max(0, component.activeTab - 1)]?.key ??
|
|
26
|
-
tabs[0]?.key ??
|
|
27
|
-
'',
|
|
48
|
+
() => fromQuery ?? tabs[Math.max(0, component.activeTab - 1)]?.key ?? tabs[0]?.key ?? '',
|
|
28
49
|
)
|
|
29
50
|
|
|
30
51
|
const persist = component.persistTab
|
|
@@ -36,6 +57,21 @@ export function TabsComponent({ component }: { component: TabsSchema }) {
|
|
|
36
57
|
hydrated && persist && !fromQuery ? readPersisted(persist.key, persist.storage) : null
|
|
37
58
|
const active = chosen ?? (stored && tabs.some((tab) => tab.key === stored) ? stored : initial)
|
|
38
59
|
|
|
60
|
+
// Tabs holding a field with a validation error are marked, and when a submit brings new
|
|
61
|
+
// errors while the open tab has none, the first tab with an error opens.
|
|
62
|
+
const t = useT()
|
|
63
|
+
const store = useOptionalFormStore()
|
|
64
|
+
const errors = useStoreSelector(store?.store ?? NO_FORM_STORE, (state) => state.errors)
|
|
65
|
+
const [seenErrors, setSeenErrors] = useState<Record<string, string[]>>({})
|
|
66
|
+
|
|
67
|
+
if (seenErrors !== errors) {
|
|
68
|
+
setSeenErrors(errors)
|
|
69
|
+
|
|
70
|
+
const reveal = tabToReveal(tabs, active, seenErrors, errors)
|
|
71
|
+
|
|
72
|
+
if (reveal !== null) setChosen(reveal)
|
|
73
|
+
}
|
|
74
|
+
|
|
39
75
|
const changeTab = (value: string) => {
|
|
40
76
|
setChosen(value)
|
|
41
77
|
|
|
@@ -45,10 +81,11 @@ export function TabsComponent({ component }: { component: TabsSchema }) {
|
|
|
45
81
|
useEffect(() => {
|
|
46
82
|
if (!component.queryStringKey || typeof window === 'undefined') return
|
|
47
83
|
|
|
84
|
+
const tab = tabs.find((item) => item.key === active)
|
|
48
85
|
const url = new URL(window.location.href)
|
|
49
|
-
url.searchParams.set(component.queryStringKey, active)
|
|
86
|
+
url.searchParams.set(component.queryStringKey, tab ? tabQueryValue(tab) : active)
|
|
50
87
|
window.history.replaceState(window.history.state, '', url)
|
|
51
|
-
}, [active, component.queryStringKey])
|
|
88
|
+
}, [active, component.queryStringKey, tabs])
|
|
52
89
|
|
|
53
90
|
return (
|
|
54
91
|
<Tabs
|
|
@@ -76,14 +113,32 @@ export function TabsComponent({ component }: { component: TabsSchema }) {
|
|
|
76
113
|
: 'relative max-w-full overflow-x-auto',
|
|
77
114
|
)}
|
|
78
115
|
>
|
|
79
|
-
{tabs.map((tab) =>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
116
|
+
{tabs.map((tab) => {
|
|
117
|
+
const invalid = componentHasErrors(tab, errors)
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<TabsTrigger
|
|
121
|
+
key={tab.key}
|
|
122
|
+
value={tab.key}
|
|
123
|
+
data-invalid={invalid || undefined}
|
|
124
|
+
className={invalid ? TAB_INVALID_CLASSES : undefined}
|
|
125
|
+
>
|
|
126
|
+
{tab.icon && tab.iconPosition !== 'after' && <Icon name={tab.icon} />}
|
|
127
|
+
{tab.label}
|
|
128
|
+
{tab.icon && tab.iconPosition === 'after' && <Icon name={tab.icon} />}
|
|
129
|
+
<TabBadge tab={tab} />
|
|
130
|
+
{invalid && (
|
|
131
|
+
<>
|
|
132
|
+
<span
|
|
133
|
+
aria-hidden="true"
|
|
134
|
+
className="bg-destructive size-1.5 shrink-0 rounded-full"
|
|
135
|
+
/>
|
|
136
|
+
<span className="sr-only">({t('translatable.has_errors')})</span>
|
|
137
|
+
</>
|
|
138
|
+
)}
|
|
139
|
+
</TabsTrigger>
|
|
140
|
+
)
|
|
141
|
+
})}
|
|
87
142
|
</TabsList>
|
|
88
143
|
{tabs.map((tab) => (
|
|
89
144
|
<TabsContent
|
|
@@ -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
|
|
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()
|