@alfakit/react 0.1.2 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/components/actions/action-modals.js +4 -0
- 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/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/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/form/form-block.js +1 -1
- package/dist/components/form/form-block.js.map +1 -1
- package/dist/components/form/form-context.js +2 -2
- 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/section.js +2 -2
- package/dist/components/layout/section.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/index.d.ts +2 -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 +6 -2
- 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/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/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.css +7 -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 +20 -4
- package/dist/starter-kit/widgets-loaded.js.map +1 -1
- package/dist/starter-kit/widgets-triggers.js +34 -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 +6 -1
- package/package.json +2 -2
- package/src/components/actions/action-modals.tsx +10 -0
- 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/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/repeater.tsx +2 -3
- package/src/components/fields/rich-editor.tsx +3 -0
- package/src/components/form/form-block.tsx +2 -1
- package/src/components/form/form-context.tsx +2 -2
- package/src/components/kanban/kanban.tsx +16 -3
- package/src/components/layout/section.tsx +2 -2
- package/src/components/panel/app-sidebar.tsx +2 -38
- package/src/components/panel/brand.tsx +46 -0
- package/src/components/realtime/live-records-pill.tsx +4 -3
- package/src/components/table/cells.tsx +9 -2
- package/src/components/table/column-manager.tsx +12 -10
- package/src/components/table/content-layout.tsx +5 -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/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 +22 -0
- package/src/starter-kit/widgets-triggers.tsx +36 -1
- package/src/starter-kit/widgets.tsx +95 -20
- package/src/styles.css +15 -2
|
@@ -41,50 +41,14 @@ import {
|
|
|
41
41
|
SidebarMenuSubItem,
|
|
42
42
|
SidebarRail,
|
|
43
43
|
} from '../ui/sidebar'
|
|
44
|
+
import { Brand } from './brand'
|
|
44
45
|
import { GlobalSearch } from './global-search'
|
|
45
46
|
import { NavLabel } from './nav-label'
|
|
46
47
|
import { Slot } from './slot'
|
|
47
48
|
import { TenantMenu } from './tenant-menu'
|
|
48
49
|
import { UserMenu } from './user-menu'
|
|
49
50
|
|
|
50
|
-
export
|
|
51
|
-
const panel = usePanel()
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<Link
|
|
55
|
-
href={panel.url ?? '/'}
|
|
56
|
-
className={cn('flex items-center gap-2 overflow-hidden', className)}
|
|
57
|
-
>
|
|
58
|
-
{panel.brandLogo ? (
|
|
59
|
-
<>
|
|
60
|
-
<img
|
|
61
|
-
src={panel.brandLogo}
|
|
62
|
-
alt={panel.brandName}
|
|
63
|
-
style={{ height: panel.brandLogoHeight ?? '1.75rem' }}
|
|
64
|
-
className={cn('max-w-full object-contain', panel.darkModeBrandLogo && 'dark:hidden')}
|
|
65
|
-
/>
|
|
66
|
-
{panel.darkModeBrandLogo && (
|
|
67
|
-
<img
|
|
68
|
-
src={panel.darkModeBrandLogo}
|
|
69
|
-
alt={panel.brandName}
|
|
70
|
-
style={{ height: panel.brandLogoHeight ?? '1.75rem' }}
|
|
71
|
-
className="hidden max-w-full object-contain dark:block"
|
|
72
|
-
/>
|
|
73
|
-
)}
|
|
74
|
-
</>
|
|
75
|
-
) : (
|
|
76
|
-
<>
|
|
77
|
-
<span className="bg-primary text-primary-foreground flex size-8 shrink-0 items-center justify-center rounded-lg text-sm font-bold">
|
|
78
|
-
{panel.brandName.charAt(0)}
|
|
79
|
-
</span>
|
|
80
|
-
<span className="truncate text-sm font-semibold group-data-[collapsible=icon]:hidden">
|
|
81
|
-
{panel.brandName}
|
|
82
|
-
</span>
|
|
83
|
-
</>
|
|
84
|
-
)}
|
|
85
|
-
</Link>
|
|
86
|
-
)
|
|
87
|
-
}
|
|
51
|
+
export { Brand } from './brand'
|
|
88
52
|
|
|
89
53
|
function NavBadge({ item }: { item: NavigationItemSchema }) {
|
|
90
54
|
const t = useT()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Link } from '@inertiajs/react'
|
|
2
|
+
import { usePanel } from '../../context'
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The panel's logo (or initial) and name, linking to the panel home. Its own module, so the
|
|
7
|
+
* auth layout doesn't import the sidebar.
|
|
8
|
+
*/
|
|
9
|
+
export function Brand({ className }: { className?: string }) {
|
|
10
|
+
const panel = usePanel()
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Link
|
|
14
|
+
href={panel.url ?? '/'}
|
|
15
|
+
className={cn('flex items-center gap-2 overflow-hidden', className)}
|
|
16
|
+
>
|
|
17
|
+
{panel.brandLogo ? (
|
|
18
|
+
<>
|
|
19
|
+
<img
|
|
20
|
+
src={panel.brandLogo}
|
|
21
|
+
alt={panel.brandName}
|
|
22
|
+
style={{ height: panel.brandLogoHeight ?? '1.75rem' }}
|
|
23
|
+
className={cn('max-w-full object-contain', panel.darkModeBrandLogo && 'dark:hidden')}
|
|
24
|
+
/>
|
|
25
|
+
{panel.darkModeBrandLogo && (
|
|
26
|
+
<img
|
|
27
|
+
src={panel.darkModeBrandLogo}
|
|
28
|
+
alt={panel.brandName}
|
|
29
|
+
style={{ height: panel.brandLogoHeight ?? '1.75rem' }}
|
|
30
|
+
className="hidden max-w-full object-contain dark:block"
|
|
31
|
+
/>
|
|
32
|
+
)}
|
|
33
|
+
</>
|
|
34
|
+
) : (
|
|
35
|
+
<>
|
|
36
|
+
<span className="bg-primary text-primary-foreground flex size-8 shrink-0 items-center justify-center rounded-lg text-sm font-bold">
|
|
37
|
+
{panel.brandName.charAt(0)}
|
|
38
|
+
</span>
|
|
39
|
+
<span className="truncate text-sm font-semibold group-data-[collapsible=icon]:hidden">
|
|
40
|
+
{panel.brandName}
|
|
41
|
+
</span>
|
|
42
|
+
</>
|
|
43
|
+
)}
|
|
44
|
+
</Link>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LiveUpdates, shownRecordKeys, type LiveUpdatesSchema, type RecordKey } from '@alfakit/core'
|
|
2
2
|
import { ArrowUpIcon } from 'lucide-react'
|
|
3
3
|
import { useEffect, useMemo } from 'react'
|
|
4
|
-
import { useChoice, usePollComponent } from '../../context'
|
|
4
|
+
import { useChoice, usePollComponent, useRefreshComponents } from '../../context'
|
|
5
5
|
import { useStore } from '../../hooks/use-store'
|
|
6
6
|
import { Button } from '../ui/button'
|
|
7
7
|
|
|
@@ -16,15 +16,16 @@ export function useLiveUpdates(
|
|
|
16
16
|
options: { polling?: boolean; records?: ReadonlyArray<{ key?: RecordKey }> | null } = {},
|
|
17
17
|
): LiveUpdates | null {
|
|
18
18
|
const poll = usePollComponent()
|
|
19
|
+
const refresh = useRefreshComponents()
|
|
19
20
|
const key = live ? JSON.stringify(live) : ''
|
|
20
21
|
const polling = options.polling
|
|
21
22
|
const records = options.records
|
|
22
23
|
|
|
23
24
|
const controller = useMemo(
|
|
24
|
-
() => (live ? new LiveUpdates({ live, target, polling, poll }) : null),
|
|
25
|
+
() => (live ? new LiveUpdates({ live, target, polling, poll, refresh }) : null),
|
|
25
26
|
// `live` is compared by value (`key`): a reload brings a new object with the same settings.
|
|
26
27
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
27
|
-
[key, target, polling, poll],
|
|
28
|
+
[key, target, polling, poll, refresh],
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
// Updates of records on other pages don't refresh the table.
|
|
@@ -60,15 +60,22 @@ export interface CellProps {
|
|
|
60
60
|
recordKey: RecordKey
|
|
61
61
|
token: string
|
|
62
62
|
component: string
|
|
63
|
+
/**
|
|
64
|
+
* Set when this renderer was picked from the registry (`column:<type>`): it then renders
|
|
65
|
+
* the built-in cell instead of looking itself up again, so an ejected `TableCellContent`
|
|
66
|
+
* registered for a column type doesn't render itself forever.
|
|
67
|
+
*/
|
|
68
|
+
fromRegistry?: boolean
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
export function TableCellContent(props: CellProps) {
|
|
66
72
|
const { column } = props
|
|
73
|
+
const registry = useRegistry()
|
|
67
74
|
// A component registered as `column:<type>` replaces the built-in cell renderer.
|
|
68
|
-
const Custom =
|
|
75
|
+
const Custom = props.fromRegistry ? undefined : registry.resolve(columnRegistryKey(column))
|
|
69
76
|
|
|
70
77
|
if (Custom) {
|
|
71
|
-
return createElement(Custom, props)
|
|
78
|
+
return createElement(Custom, { ...props, fromRegistry: true })
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
switch (column.type) {
|
|
@@ -193,24 +193,26 @@ export function ColumnManager({
|
|
|
193
193
|
|
|
194
194
|
const orderChanged = Boolean(orderDraft && columnOrderChanged(appliedNames, orderDraft))
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
visitTable(table.identifier, { columns: draft }, ['page'])
|
|
201
|
-
}
|
|
196
|
+
// The order is saved before the visibility visit: both requests share the session, and a
|
|
197
|
+
// visit running alongside the save would write the old order back when it finishes.
|
|
198
|
+
const apply = async () => {
|
|
199
|
+
const columns = draft && columnDraftChanged(table.visibleColumns, draft) ? draft : null
|
|
202
200
|
|
|
203
201
|
setDraft(null)
|
|
204
202
|
setOrderDraft(null)
|
|
205
203
|
setAnnouncement('')
|
|
206
204
|
setOpen(false)
|
|
205
|
+
|
|
206
|
+
if (orderDraft && orderChanged) await save(orderDraft)
|
|
207
|
+
|
|
208
|
+
if (columns) visitTable(table.identifier, { columns }, ['page'])
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
// Reset: the defined column order and, for toggleable columns, the default visibility.
|
|
210
|
-
const reset = () => {
|
|
212
|
+
const reset = async () => {
|
|
211
213
|
if (reorderable) {
|
|
212
214
|
setOrderDraft(null)
|
|
213
|
-
|
|
215
|
+
await save([])
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
if (toggleable) {
|
|
@@ -237,7 +239,7 @@ export function ColumnManager({
|
|
|
237
239
|
disabled={saving}
|
|
238
240
|
data-slot="column-manager-reset"
|
|
239
241
|
className={cn(!applyView && 'w-full')}
|
|
240
|
-
onClick={reset}
|
|
242
|
+
onClick={() => void reset()}
|
|
241
243
|
/>
|
|
242
244
|
) : null
|
|
243
245
|
const inHeader = table.columnManagerResetActionPosition === 'header'
|
|
@@ -394,7 +396,7 @@ export function ColumnManager({
|
|
|
394
396
|
disabled={
|
|
395
397
|
!orderChanged && (!draft || !columnDraftChanged(table.visibleColumns, draft))
|
|
396
398
|
}
|
|
397
|
-
onClick={apply}
|
|
399
|
+
onClick={() => void apply()}
|
|
398
400
|
/>
|
|
399
401
|
)}
|
|
400
402
|
</div>
|
|
@@ -313,13 +313,17 @@ export function ContentLayoutSort({
|
|
|
313
313
|
if (sortable.length === 0) return null
|
|
314
314
|
|
|
315
315
|
const direction = table.sort?.direction ?? 'asc'
|
|
316
|
+
// A default sort on a column the select doesn't list (`created_at`) shows the placeholder.
|
|
317
|
+
const selected = sortable.some((column) => column.name === table.sort?.column)
|
|
318
|
+
? (table.sort?.column ?? '')
|
|
319
|
+
: ''
|
|
316
320
|
|
|
317
321
|
return (
|
|
318
322
|
<div className="flex items-center gap-1">
|
|
319
323
|
<label className="flex items-center gap-2 text-sm">
|
|
320
324
|
<span className="sr-only">{t('table.sort_by', {}, 'Sort by')}</span>
|
|
321
325
|
<select
|
|
322
|
-
value={
|
|
326
|
+
value={selected}
|
|
323
327
|
onChange={(event) =>
|
|
324
328
|
visitTable(
|
|
325
329
|
table.identifier,
|
|
@@ -14,12 +14,13 @@ import {
|
|
|
14
14
|
ChevronsRightIcon,
|
|
15
15
|
} from 'lucide-react'
|
|
16
16
|
import { useEffect, useRef, useState } from 'react'
|
|
17
|
-
import { useT } from '../../context'
|
|
17
|
+
import { useFormatter, useT } from '../../context'
|
|
18
18
|
import { Icon } from '../../lib/icons'
|
|
19
19
|
import { Button } from '../ui/button'
|
|
20
20
|
|
|
21
21
|
export function TablePagination({ table }: { table: TableSchema }) {
|
|
22
22
|
const t = useT()
|
|
23
|
+
const format = useFormatter()
|
|
23
24
|
const pagination = table.pagination
|
|
24
25
|
|
|
25
26
|
if (!pagination) {
|
|
@@ -41,13 +42,17 @@ export function TablePagination({ table }: { table: TableSchema }) {
|
|
|
41
42
|
const summary = numbered
|
|
42
43
|
? t(
|
|
43
44
|
'table.showing',
|
|
44
|
-
{
|
|
45
|
-
|
|
45
|
+
{
|
|
46
|
+
first: format.number(pagination.from),
|
|
47
|
+
last: format.number(pagination.to),
|
|
48
|
+
total: format.number(pagination.total),
|
|
49
|
+
},
|
|
50
|
+
'Showing :first–:last of :total',
|
|
46
51
|
)
|
|
47
52
|
: t(
|
|
48
53
|
'table.showing_range',
|
|
49
|
-
{ first: pagination.from, last: pagination.to },
|
|
50
|
-
|
|
54
|
+
{ first: format.number(pagination.from), last: format.number(pagination.to) },
|
|
55
|
+
'Showing :first–:last',
|
|
51
56
|
)
|
|
52
57
|
|
|
53
58
|
return (
|
|
@@ -114,7 +119,7 @@ export function TablePagination({ table }: { table: TableSchema }) {
|
|
|
114
119
|
onClick={() => visitTable(table.identifier, { page }, ['page'])}
|
|
115
120
|
aria-current={page === pagination.currentPage ? 'page' : undefined}
|
|
116
121
|
>
|
|
117
|
-
{page}
|
|
122
|
+
{format.number(page)}
|
|
118
123
|
</Button>
|
|
119
124
|
),
|
|
120
125
|
)}
|
|
@@ -169,6 +174,7 @@ function PerPageSelect({
|
|
|
169
174
|
pagination: AnyPaginationSchema
|
|
170
175
|
}) {
|
|
171
176
|
const t = useT()
|
|
177
|
+
const format = useFormatter()
|
|
172
178
|
|
|
173
179
|
if (pagination.pageOptions.length <= 1) return null
|
|
174
180
|
|
|
@@ -188,7 +194,7 @@ function PerPageSelect({
|
|
|
188
194
|
>
|
|
189
195
|
{pagination.pageOptions.map((option) => (
|
|
190
196
|
<option key={option} value={String(option)}>
|
|
191
|
-
{option === 'all' ? t('table.all', {}, 'All') : option}
|
|
197
|
+
{option === 'all' ? t('table.all', {}, 'All') : format.number(option)}
|
|
192
198
|
</option>
|
|
193
199
|
))}
|
|
194
200
|
</select>
|
|
@@ -210,6 +216,7 @@ function AppendingPagination({
|
|
|
210
216
|
pagination: Extract<AnyPaginationSchema, { mode: 'load-more' | 'infinite' }>
|
|
211
217
|
}) {
|
|
212
218
|
const t = useT()
|
|
219
|
+
const format = useFormatter()
|
|
213
220
|
const sentinel = useRef<HTMLDivElement>(null)
|
|
214
221
|
const [loading, setLoading] = useState(false)
|
|
215
222
|
const [failed, setFailed] = useState(false)
|
|
@@ -290,8 +297,8 @@ function AppendingPagination({
|
|
|
290
297
|
<span>
|
|
291
298
|
{t(
|
|
292
299
|
'table.showing_range',
|
|
293
|
-
{ first: pagination.from, last: pagination.to },
|
|
294
|
-
|
|
300
|
+
{ first: format.number(pagination.from), last: format.number(pagination.to) },
|
|
301
|
+
'Showing :first–:last',
|
|
295
302
|
)}
|
|
296
303
|
</span>
|
|
297
304
|
<PerPageSelect table={table} pagination={pagination} />
|
|
@@ -202,6 +202,20 @@ export function DataTable({
|
|
|
202
202
|
[table.identifier, table.searchDebounce],
|
|
203
203
|
)
|
|
204
204
|
|
|
205
|
+
// Column searches (`searchable(isIndividual: true)`) apply after the same pause as the table
|
|
206
|
+
// search; Enter applies at once. Callers pass every column's search (the others unchanged).
|
|
207
|
+
const pushColumnSearch = useMemo(
|
|
208
|
+
() =>
|
|
209
|
+
debounce(
|
|
210
|
+
(columnSearch: Record<string, string>) =>
|
|
211
|
+
visitTable(table.identifier, { columnSearch }, ['page']),
|
|
212
|
+
table.searchDebounce,
|
|
213
|
+
),
|
|
214
|
+
[table.identifier, table.searchDebounce],
|
|
215
|
+
)
|
|
216
|
+
const searchColumn = (name: string, value: string) =>
|
|
217
|
+
pushColumnSearch({ ...(table.columnSearches ?? {}), [name]: value })
|
|
218
|
+
|
|
205
219
|
// Polls refresh only this table (a partial reload of its own schema), share one request
|
|
206
220
|
// with other components on the same interval and pause while the tab is hidden.
|
|
207
221
|
const pollComponent = usePollComponent()
|
|
@@ -619,19 +633,18 @@ export function DataTable({
|
|
|
619
633
|
placeholder={t('search', {}, 'Search')}
|
|
620
634
|
aria-label={`${t('search', {}, 'Search')} ${column.label}`}
|
|
621
635
|
className="h-7 text-xs font-normal"
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
columnSearch: {
|
|
628
|
-
...(table.columnSearches ?? {}),
|
|
629
|
-
[column.name]: (event.target as HTMLInputElement).value,
|
|
630
|
-
},
|
|
631
|
-
},
|
|
632
|
-
['page'],
|
|
633
|
-
)
|
|
636
|
+
onChange={(event) => {
|
|
637
|
+
if (!table.searchOnBlur) searchColumn(column.name, event.target.value)
|
|
638
|
+
}}
|
|
639
|
+
onBlur={(event) =>
|
|
640
|
+
table.searchOnBlur && searchColumn(column.name, event.target.value)
|
|
634
641
|
}
|
|
642
|
+
onKeyDown={(event) => {
|
|
643
|
+
if (event.key !== 'Enter') return
|
|
644
|
+
|
|
645
|
+
searchColumn(column.name, (event.target as HTMLInputElement).value)
|
|
646
|
+
pushColumnSearch.flush()
|
|
647
|
+
}}
|
|
635
648
|
/>
|
|
636
649
|
)}
|
|
637
650
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { XIcon } from 'lucide-react'
|
|
2
|
-
import { Dialog as DialogPrimitive } from 'radix-ui'
|
|
2
|
+
import { Dialog as DialogPrimitive, Direction } from 'radix-ui'
|
|
3
3
|
import type { ComponentProps } from 'react'
|
|
4
4
|
import { useT } from '../../context'
|
|
5
5
|
import { Icon } from '../../lib/icons'
|
|
@@ -40,11 +40,15 @@ function DialogContent({
|
|
|
40
40
|
showCloseButton = true,
|
|
41
41
|
...props
|
|
42
42
|
}: ComponentProps<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }) {
|
|
43
|
+
// Portaled outside Alfakit's root: carry the reading direction (starter kit pages and
|
|
44
|
+
// standalone components leave <html dir> to the host).
|
|
45
|
+
const dir = Direction.useDirection()
|
|
43
46
|
const t = useT()
|
|
44
47
|
return (
|
|
45
48
|
<DialogPortal data-slot="dialog-portal">
|
|
46
49
|
<DialogOverlay />
|
|
47
50
|
<DialogPrimitive.Content
|
|
51
|
+
dir={dir}
|
|
48
52
|
data-slot="dialog-content"
|
|
49
53
|
className={cn(
|
|
50
54
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Popover as PopoverPrimitive } from 'radix-ui'
|
|
1
|
+
import { Direction, Popover as PopoverPrimitive } from 'radix-ui'
|
|
2
2
|
import type { ComponentProps } from 'react'
|
|
3
3
|
import { cn } from '../../lib/utils'
|
|
4
4
|
|
|
@@ -16,9 +16,13 @@ function PopoverContent({
|
|
|
16
16
|
sideOffset = 4,
|
|
17
17
|
...props
|
|
18
18
|
}: ComponentProps<typeof PopoverPrimitive.Content>) {
|
|
19
|
+
// Portaled outside Alfakit's root: carry the reading direction (starter kit pages and
|
|
20
|
+
// standalone components leave <html dir> to the host).
|
|
21
|
+
const dir = Direction.useDirection()
|
|
19
22
|
return (
|
|
20
23
|
<PopoverPrimitive.Portal>
|
|
21
24
|
<PopoverPrimitive.Content
|
|
25
|
+
dir={dir}
|
|
22
26
|
data-slot="popover-content"
|
|
23
27
|
align={align}
|
|
24
28
|
sideOffset={sideOffset}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { XIcon } from 'lucide-react'
|
|
2
|
-
import { Dialog as SheetPrimitive } from 'radix-ui'
|
|
2
|
+
import { Direction, Dialog as SheetPrimitive } from 'radix-ui'
|
|
3
3
|
import type { ComponentProps } from 'react'
|
|
4
4
|
import { useT } from '../../context'
|
|
5
5
|
import { Icon } from '../../lib/icons'
|
|
@@ -44,11 +44,15 @@ function SheetContent({
|
|
|
44
44
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
45
45
|
showCloseButton?: boolean
|
|
46
46
|
}) {
|
|
47
|
+
// Portaled outside Alfakit's root: carry the reading direction (starter kit pages and
|
|
48
|
+
// standalone components leave <html dir> to the host).
|
|
49
|
+
const dir = Direction.useDirection()
|
|
47
50
|
const t = useT()
|
|
48
51
|
return (
|
|
49
52
|
<SheetPortal>
|
|
50
53
|
<SheetOverlay />
|
|
51
54
|
<SheetPrimitive.Content
|
|
55
|
+
dir={dir}
|
|
52
56
|
data-slot="sheet-content"
|
|
53
57
|
className={cn(
|
|
54
58
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Tooltip as TooltipPrimitive } from 'radix-ui'
|
|
1
|
+
import { Direction, Tooltip as TooltipPrimitive } from 'radix-ui'
|
|
2
2
|
import type { ComponentProps } from 'react'
|
|
3
3
|
import { cn } from '../../lib/utils'
|
|
4
4
|
|
|
@@ -29,9 +29,13 @@ function TooltipContent({
|
|
|
29
29
|
children,
|
|
30
30
|
...props
|
|
31
31
|
}: ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
32
|
+
// Portaled outside Alfakit's root: carry the reading direction (starter kit pages and
|
|
33
|
+
// standalone components leave <html dir> to the host).
|
|
34
|
+
const dir = Direction.useDirection()
|
|
32
35
|
return (
|
|
33
36
|
<TooltipPrimitive.Portal>
|
|
34
37
|
<TooltipPrimitive.Content
|
|
38
|
+
dir={dir}
|
|
35
39
|
data-slot="tooltip-content"
|
|
36
40
|
sideOffset={sideOffset}
|
|
37
41
|
className={cn(
|
package/src/context.tsx
CHANGED
|
@@ -28,6 +28,11 @@ export interface AlfakitContextValue {
|
|
|
28
28
|
* their own schemas by token.
|
|
29
29
|
*/
|
|
30
30
|
poll?: (target: string, interval: string | number | null | undefined) => () => void
|
|
31
|
+
/**
|
|
32
|
+
* Fetches fresh schemas of components now (`live()` tables and widgets after a record
|
|
33
|
+
* change). Panels leave it out (a partial reload); components outside panels fetch theirs.
|
|
34
|
+
*/
|
|
35
|
+
refresh?: (targets: string[]) => Promise<void>
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
const AlfakitContext = createContext<AlfakitContextValue | null>(null)
|
|
@@ -126,6 +131,11 @@ export function usePollComponent(): NonNullable<AlfakitContextValue['poll']> {
|
|
|
126
131
|
return useContext(AlfakitContext)?.poll ?? pollComponent
|
|
127
132
|
}
|
|
128
133
|
|
|
134
|
+
/** How `live()` components refresh: their own fetch outside panels, else a partial reload. */
|
|
135
|
+
export function useRefreshComponents(): AlfakitContextValue['refresh'] {
|
|
136
|
+
return useContext(AlfakitContext)?.refresh
|
|
137
|
+
}
|
|
138
|
+
|
|
129
139
|
export function useAlfakitProps(): AlfakitPageProps {
|
|
130
140
|
return useOptionalPageProps()
|
|
131
141
|
}
|
package/src/create-alfakit.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
configureCharts,
|
|
8
8
|
configureRealtime,
|
|
9
9
|
debugRecorder,
|
|
10
|
+
documentNonce,
|
|
10
11
|
effectiveErrorNotifications,
|
|
11
12
|
initializeAppearance,
|
|
12
13
|
installErrorNotifications,
|
|
@@ -43,6 +44,7 @@ import { SchemaContractGuard } from './components/panel/schema-contract-guard'
|
|
|
43
44
|
import { Toaster } from './components/ui/sonner'
|
|
44
45
|
import { TooltipProvider } from './components/ui/tooltip'
|
|
45
46
|
import { AlfakitProvider, useAlfakit, type AlfakitContextValue, type AnyComponent } from './context'
|
|
47
|
+
import { panelLayouts } from './layouts/panel-layouts'
|
|
46
48
|
import { discoverComponents } from './lib/discovery'
|
|
47
49
|
import { preloadIcons } from './lib/icons'
|
|
48
50
|
import { AlfakitPage } from './pages/page'
|
|
@@ -72,9 +74,13 @@ export function AlfakitRoot({
|
|
|
72
74
|
translator.setLocale(localization?.tag ?? 'en')
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
// Starter kit mode: the host app owns `<html lang dir>` (its own pages and layout are not
|
|
78
|
+
// mirrored); `AlfakitStarterKitRoot` puts the direction on Alfakit's own root element.
|
|
79
|
+
const ownsDocument = panel?.starterKit !== true
|
|
80
|
+
|
|
75
81
|
useLayoutEffect(() => {
|
|
76
|
-
applyDocumentLocale(localization)
|
|
77
|
-
}, [localization])
|
|
82
|
+
if (ownsDocument) applyDocumentLocale(localization)
|
|
83
|
+
}, [localization, ownsDocument])
|
|
78
84
|
|
|
79
85
|
useLayoutEffect(() => {
|
|
80
86
|
applyPanelTheme(panel)
|
|
@@ -133,6 +139,7 @@ export function bootAlfakit(
|
|
|
133
139
|
// Auto-registered files sit between the built-ins and the explicit `components` / `pages`.
|
|
134
140
|
const discovered = discoverComponents(options.discover)
|
|
135
141
|
const registry = createDefaultRegistry()
|
|
142
|
+
.registerMany(panelLayouts)
|
|
136
143
|
.registerMany(discovered.components)
|
|
137
144
|
.registerMany(options.components ?? {})
|
|
138
145
|
const pages: Record<string, AnyComponent> = {
|
|
@@ -237,6 +244,8 @@ export function bootAlfakit(
|
|
|
237
244
|
return createInertiaApp<AlfakitPageProps>({
|
|
238
245
|
title: (title) => title,
|
|
239
246
|
progress: options.progress === undefined ? { color: 'var(--primary)' } : options.progress,
|
|
247
|
+
// The page's CSP nonce, so the progress bar's injected <style> passes a nonce-based policy.
|
|
248
|
+
nonce: documentNonce(),
|
|
240
249
|
resolve: (name, page) => {
|
|
241
250
|
const component = pages[name]
|
|
242
251
|
|
package/src/index.ts
CHANGED
|
@@ -99,6 +99,8 @@ export { discoverComponents, discoveredComponent, type DiscoveredComponents } fr
|
|
|
99
99
|
export { Icon } from './lib/icons'
|
|
100
100
|
export { cn } from './lib/utils'
|
|
101
101
|
export { AlfakitPage, ContentBlockRenderer, PageContent, PageHeader, PageShell } from './pages/page'
|
|
102
|
+
// `createAlfakit()` registers these; a registry built by hand for `AlfakitPage` needs them too.
|
|
103
|
+
export { panelLayouts } from './layouts/panel-layouts'
|
|
102
104
|
export { createDefaultRegistry } from './registry'
|
|
103
105
|
export * from './standalone'
|
|
104
106
|
export * from './starter-kit'
|
|
@@ -15,36 +15,12 @@ import { Topbar } from '../components/panel/topbar'
|
|
|
15
15
|
import { SidebarInset, SidebarProvider, SidebarTrigger } from '../components/ui/sidebar'
|
|
16
16
|
import { useAlfakitProps, useRegistry } from '../context'
|
|
17
17
|
import { cn } from '../lib/utils'
|
|
18
|
+
import { MAX_WIDTHS } from './max-widths'
|
|
18
19
|
import { TopNavLayout } from './top-nav-layout'
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
xs: 'max-w-xs',
|
|
24
|
-
sm: 'max-w-sm',
|
|
25
|
-
md: 'max-w-md',
|
|
26
|
-
lg: 'max-w-lg',
|
|
27
|
-
xl: 'max-w-xl',
|
|
28
|
-
'2xl': 'max-w-2xl',
|
|
29
|
-
'3xl': 'max-w-3xl',
|
|
30
|
-
'4xl': 'max-w-4xl',
|
|
31
|
-
'5xl': 'max-w-5xl',
|
|
32
|
-
'6xl': 'max-w-6xl',
|
|
33
|
-
'7xl': 'max-w-7xl',
|
|
34
|
-
none: 'max-w-none',
|
|
35
|
-
min: 'max-w-min',
|
|
36
|
-
max: 'max-w-max',
|
|
37
|
-
fit: 'max-w-fit',
|
|
38
|
-
prose: 'max-w-prose',
|
|
39
|
-
container: 'container',
|
|
40
|
-
'screen-sm': 'max-w-(--breakpoint-sm)',
|
|
41
|
-
'screen-md': 'max-w-(--breakpoint-md)',
|
|
42
|
-
'screen-lg': 'max-w-(--breakpoint-lg)',
|
|
43
|
-
'screen-xl': 'max-w-(--breakpoint-xl)',
|
|
44
|
-
'screen-2xl': 'max-w-(--breakpoint-2xl)',
|
|
45
|
-
full: 'max-w-full',
|
|
46
|
-
screen: 'max-w-screen',
|
|
47
|
-
}
|
|
21
|
+
// Re-exported for code that imported it from here; the map lives in `./max-widths` so the
|
|
22
|
+
// starter kit's content layout doesn't pull the panel chrome in.
|
|
23
|
+
export { MAX_WIDTHS } from './max-widths'
|
|
48
24
|
|
|
49
25
|
export interface AppLayoutProps {
|
|
50
26
|
children: ReactNode
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
|
-
import { Brand } from '../components/panel/
|
|
2
|
+
import { Brand } from '../components/panel/brand'
|
|
3
3
|
import { LocaleSwitcher } from '../components/panel/locale-switcher'
|
|
4
4
|
import { Slot } from '../components/panel/slot'
|
|
5
5
|
import { ThemeToggle } from '../components/panel/theme-toggle'
|
|
6
6
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card'
|
|
7
7
|
import { usePanel } from '../context'
|
|
8
8
|
import { cn } from '../lib/utils'
|
|
9
|
-
import { MAX_WIDTHS } from './
|
|
9
|
+
import { MAX_WIDTHS } from './max-widths'
|
|
10
10
|
|
|
11
11
|
export interface AuthLayoutProps {
|
|
12
12
|
children: ReactNode
|
|
@@ -3,7 +3,7 @@ import { Link } from '@inertiajs/react'
|
|
|
3
3
|
import { LogOutIcon } from 'lucide-react'
|
|
4
4
|
import type { ReactNode } from 'react'
|
|
5
5
|
import { NotificationsMenu } from '../components/notifications/notifications-menu'
|
|
6
|
-
import { Brand } from '../components/panel/
|
|
6
|
+
import { Brand } from '../components/panel/brand'
|
|
7
7
|
import { DebugPanelSlot } from '../components/panel/debug-panel-slot'
|
|
8
8
|
import { TopbarLocaleSwitcher } from '../components/panel/locale-switcher'
|
|
9
9
|
import { Slot } from '../components/panel/slot'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { lazyComponents } from '@alfakit/core'
|
|
2
|
+
import type { AnyComponent } from '../context'
|
|
3
|
+
import { discoveredComponent } from '../lib/discovery'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The fullscreen layout (`$layout = 'fullscreen'`), loaded on demand by starter kit pages:
|
|
7
|
+
* they render in the host's layout, so the page chunk never imports the panel's header,
|
|
8
|
+
* menus and notifications for a layout few pages use. Only the starter kit registers it;
|
|
9
|
+
* `createAlfakit()` registers the layouts themselves up front (`panelLayouts`), so a panel's
|
|
10
|
+
* bundle has no extra lazy entry points.
|
|
11
|
+
*/
|
|
12
|
+
export const lazyPanelLayouts: Record<string, AnyComponent> = lazyComponents<AnyComponent>({
|
|
13
|
+
'layout:fullscreen': () => import('./fullscreen-layout').then((m) => m.FullscreenLayout),
|
|
14
|
+
}).map(discoveredComponent).components
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Tailwind classes for the `maxContentWidth` setting of the panel and its pages. */
|
|
2
|
+
export const MAX_WIDTHS: Record<string, string> = {
|
|
3
|
+
'3xs': 'max-w-3xs',
|
|
4
|
+
'2xs': 'max-w-2xs',
|
|
5
|
+
xs: 'max-w-xs',
|
|
6
|
+
sm: 'max-w-sm',
|
|
7
|
+
md: 'max-w-md',
|
|
8
|
+
lg: 'max-w-lg',
|
|
9
|
+
xl: 'max-w-xl',
|
|
10
|
+
'2xl': 'max-w-2xl',
|
|
11
|
+
'3xl': 'max-w-3xl',
|
|
12
|
+
'4xl': 'max-w-4xl',
|
|
13
|
+
'5xl': 'max-w-5xl',
|
|
14
|
+
'6xl': 'max-w-6xl',
|
|
15
|
+
'7xl': 'max-w-7xl',
|
|
16
|
+
none: 'max-w-none',
|
|
17
|
+
min: 'max-w-min',
|
|
18
|
+
max: 'max-w-max',
|
|
19
|
+
fit: 'max-w-fit',
|
|
20
|
+
prose: 'max-w-prose',
|
|
21
|
+
container: 'container',
|
|
22
|
+
'screen-sm': 'max-w-(--breakpoint-sm)',
|
|
23
|
+
'screen-md': 'max-w-(--breakpoint-md)',
|
|
24
|
+
'screen-lg': 'max-w-(--breakpoint-lg)',
|
|
25
|
+
'screen-xl': 'max-w-(--breakpoint-xl)',
|
|
26
|
+
'screen-2xl': 'max-w-(--breakpoint-2xl)',
|
|
27
|
+
full: 'max-w-full',
|
|
28
|
+
screen: 'max-w-screen',
|
|
29
|
+
}
|