@alfakit/react 0.1.8 → 0.1.10
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 +14 -0
- package/dist/components/actions/action-modals.js +1 -1
- package/dist/components/actions/action-modals.js.map +1 -1
- package/dist/components/dev-tools/dev-tools-inspector.js +1 -1
- package/dist/components/dev-tools/dev-tools-inspector.js.map +1 -1
- package/dist/components/fields/checkbox-list.js +2 -2
- package/dist/components/fields/checkbox-list.js.map +1 -1
- package/dist/components/fields/rich-editor-extensions.js +14 -4
- package/dist/components/fields/rich-editor-extensions.js.map +1 -1
- package/dist/components/file-manager/file-manager.js +2 -0
- 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 +6 -3
- package/dist/components/form/form-block.js.map +1 -1
- package/dist/components/layout/elements.d.ts.map +1 -1
- package/dist/components/layout/elements.js +16 -5
- package/dist/components/layout/elements.js.map +1 -1
- package/dist/components/layout/mixed-content.d.ts +6 -4
- package/dist/components/layout/mixed-content.d.ts.map +1 -1
- package/dist/components/layout/mixed-content.js +6 -4
- package/dist/components/layout/mixed-content.js.map +1 -1
- package/dist/components/layout/section.js +4 -2
- package/dist/components/layout/section.js.map +1 -1
- package/dist/components/notifications/notifications-menu.d.ts.map +1 -1
- package/dist/components/notifications/notifications-menu.js +45 -17
- package/dist/components/notifications/notifications-menu.js.map +1 -1
- package/dist/components/panel/app-sidebar.js +3 -3
- package/dist/components/panel/app-sidebar.js.map +1 -1
- package/dist/components/panel/bottom-nav.js +2 -2
- package/dist/components/panel/bottom-nav.js.map +1 -1
- package/dist/components/panel/keyboard-shortcuts.js +1 -1
- package/dist/components/panel/keyboard-shortcuts.js.map +1 -1
- package/dist/components/panel/shortcuts-overlay.js +13 -10
- package/dist/components/panel/shortcuts-overlay.js.map +1 -1
- package/dist/components/table/column-manager.js +4 -4
- package/dist/components/table/column-manager.js.map +1 -1
- package/dist/components/table/spreadsheet.js +8 -1
- package/dist/components/table/spreadsheet.js.map +1 -1
- package/dist/components/translation-manager/translation-manager.js +1 -1
- package/dist/components/translation-manager/translation-manager.js.map +1 -1
- package/dist/components/ui/tooltip.d.ts.map +1 -1
- package/dist/components/ui/tooltip.js +1 -1
- package/dist/components/ui/tooltip.js.map +1 -1
- package/dist/components/widgets/stats-overview.js +3 -3
- package/dist/components/widgets/stats-overview.js.map +1 -1
- package/dist/eject-manifest.json +2 -2
- package/dist/layouts/top-nav-layout.js +3 -3
- package/dist/layouts/top-nav-layout.js.map +1 -1
- package/dist/pages/page.d.ts.map +1 -1
- package/dist/pages/page.js +4 -7
- package/dist/pages/page.js.map +1 -1
- package/dist/standalone/alfakit-component.d.ts +0 -10
- package/dist/standalone/alfakit-component.d.ts.map +1 -1
- package/dist/standalone/alfakit-component.js +20 -9
- package/dist/standalone/alfakit-component.js.map +1 -1
- package/dist/standalone/runtime.js +2 -1
- package/dist/standalone/runtime.js.map +1 -1
- package/dist/standalone.css +12 -7
- package/dist/starter-kit/navigation.d.ts +6 -1
- package/dist/starter-kit/navigation.d.ts.map +1 -1
- package/dist/starter-kit/navigation.js +3 -2
- package/dist/starter-kit/navigation.js.map +1 -1
- package/dist/starter-kit/starter-kit.d.ts.map +1 -1
- package/dist/starter-kit/starter-kit.js +7 -2
- package/dist/starter-kit/starter-kit.js.map +1 -1
- package/dist/styles.css +2 -1
- package/dist/themes.css +4 -0
- package/package.json +2 -2
- package/src/components/actions/action-modals.tsx +2 -0
- package/src/components/dev-tools/dev-tools-inspector.tsx +1 -1
- package/src/components/fields/checkbox-list.tsx +2 -2
- package/src/components/fields/rich-editor-extensions.tsx +24 -2
- package/src/components/file-manager/file-manager.tsx +3 -1
- package/src/components/form/form-block.tsx +15 -2
- package/src/components/layout/elements.tsx +27 -4
- package/src/components/layout/mixed-content.tsx +8 -4
- package/src/components/layout/section.tsx +2 -2
- package/src/components/notifications/notifications-menu.tsx +42 -1
- package/src/components/panel/app-sidebar.tsx +3 -3
- package/src/components/panel/bottom-nav.tsx +2 -2
- package/src/components/panel/keyboard-shortcuts.tsx +1 -1
- package/src/components/panel/shortcuts-overlay.tsx +29 -24
- package/src/components/table/column-manager.tsx +6 -4
- package/src/components/table/spreadsheet.tsx +18 -7
- package/src/components/translation-manager/translation-manager.tsx +1 -1
- package/src/components/ui/tooltip.tsx +10 -1
- package/src/components/widgets/stats-overview.tsx +6 -3
- package/src/layouts/top-nav-layout.tsx +3 -3
- package/src/pages/page.tsx +7 -3
- package/src/standalone/alfakit-component.tsx +18 -3
- package/src/standalone/runtime.ts +3 -0
- package/src/starter-kit/navigation.ts +12 -1
- package/src/starter-kit/starter-kit.tsx +10 -1
- package/src/styles.css +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"starter-kit.js","names":[],"sources":["../../src/starter-kit/starter-kit.tsx"],"sourcesContent":["import {\n ActionRunner,\n STARTER_KIT_TOASTER_ID,\n Translator,\n configureCharts,\n installStarterKitRuntime,\n registerRichEditorExtensions,\n resolveRequestErrorNotification,\n starterKitBreadcrumbsFromProps,\n syncStarterKitPage,\n usesStarterKitHostLayout,\n type AlfakitOptions,\n type AlfakitPageProps,\n type StarterKitBreadcrumb,\n} from '@alfakit/core'\nimport { usePage } from '@inertiajs/react'\nimport { useEffect, useState, type ComponentType, type ReactNode } from 'react'\nimport { setToasterId, showToast } from '../components/notifications/flash-toasts'\nimport { Slot } from '../components/panel/slot'\nimport {\n AlfakitProvider,\n useAlfakitProps,\n type AlfakitContextValue,\n type AnyComponent,\n} from '../context'\nimport { AlfakitRoot } from '../create-alfakit'\nimport type { AppLayoutProps } from '../layouts/app-layout'\nimport { AuthLayout } from '../layouts/auth-layout'\nimport { lazyPanelLayouts } from '../layouts/lazy-layouts'\nimport { MAX_WIDTHS } from '../layouts/max-widths'\nimport { discoverComponents } from '../lib/discovery'\nimport { preloadIcons } from '../lib/icons'\nimport { cn } from '../lib/utils'\nimport { AlfakitPage } from '../pages/page'\nimport { createDefaultRegistry } from '../registry'\n\n/** The host's layout, as the starter kits' `AppLayout` takes its props. */\nexport type StarterKitLayout = ComponentType<{\n breadcrumbs?: StarterKitBreadcrumb[]\n children: ReactNode\n}>\n\nexport interface StarterKitPageOptions extends Pick<\n AlfakitOptions<AnyComponent>,\n 'components' | 'pages' | 'chartFormatters' | 'chartPlugins' | 'richEditorExtensions'\n> {\n /**\n * The host layout Alfakit pages render in, usually the starter kit's `AppLayout`\n * (`@/layouts/app-layout`). It receives the page's breadcrumbs as `{ title, href }`.\n * `null` renders the pages without a host layout.\n */\n layout?: StarterKitLayout | null\n /**\n * Auto-registered custom components, as for `createAlfakit({ discover })`: pass\n * `import.meta.glob('../../alfakit/{fields,columns,entries,widgets,pages,components,blocks}/**\\/*.tsx')`\n * from `resources/js/pages/Alfakit/Page.tsx` and every file there registers under its name.\n */\n discover?: AlfakitOptions<AnyComponent>['discover']\n}\n\n/**\n * The content area of an Alfakit page inside the host layout: what `layout:app` renders in\n * starter kit mode instead of Alfakit's sidebar and top bar. Replace it with\n * `createStarterKitPage({ components: { 'layout:app': MyContent } })`.\n */\nexport function StarterKitContentLayout({ children, maxContentWidth }: AppLayoutProps) {\n const props = useAlfakitProps()\n // The host's pages fill the content area, so starter kit pages default to the full width.\n const width = maxContentWidth ?? props.alfakit.maxContentWidth ?? 'full'\n\n return (\n <div\n data-slot=\"content\"\n data-alfakit-embedded=\"\"\n className={cn(\n 'mx-auto flex w-full min-w-0 flex-1 flex-col gap-6 p-4 md:p-6',\n MAX_WIDTHS[width] ?? 'max-w-full',\n )}\n >\n <Slot name=\"content.start\" />\n {children}\n <Slot name=\"content.end\" />\n </div>\n )\n}\n\n// One context for the browser session (it survives navigations between host and Alfakit\n// pages); a server render builds one per request so concurrent renders never share a locale.\nlet browserContext: AlfakitContextValue | null = null\n\nfunction createStarterKitContext(options: StarterKitPageOptions): AlfakitContextValue {\n // The host's layout replaces the panel's. Auth pages (invitations, tenant registration)\n // keep the panel's card; the fullscreen layout loads on demand.\n // Auto-registered files sit between the built-ins and the explicit `components` / `pages`.\n const discovered = discoverComponents(options.discover)\n const registry = createDefaultRegistry()\n .registerMany(lazyPanelLayouts)\n .register('layout:auth', AuthLayout)\n .register('layout:app', StarterKitContentLayout)\n .registerMany(discovered.components)\n .registerMany(options.components ?? {})\n const translator = new Translator()\n\n return {\n registry,\n pages: { 'Alfakit/Page': AlfakitPage, ...discovered.pages, ...options.pages },\n translator,\n actions: new ActionRunner({\n endpoints: { action: '', modal: '' },\n // A failed action request is a toast, never a silent no-op.\n onError: (error) => {\n const notification = resolveRequestErrorNotification(\n error,\n null,\n (key, replacements, fallback) => translator.t(key, replacements, fallback),\n )\n\n if (notification) showToast(notification)\n },\n }),\n }\n}\n\nfunction starterKitContext(options: StarterKitPageOptions): AlfakitContextValue {\n if (typeof window === 'undefined') return createStarterKitContext(options)\n\n browserContext ??= createStarterKitContext(options)\n\n return browserContext\n}\n\n/**\n * The Inertia layout around every Alfakit page in starter kit mode, inside the host layout:\n * Alfakit's context, modals, toasts and the reading direction. It stays mounted while the\n * user moves between Alfakit pages.\n */\nexport function AlfakitStarterKitRoot({\n options,\n children,\n}: {\n options?: StarterKitPageOptions\n children?: ReactNode\n}) {\n const [context] = useState(() => starterKitContext(options ?? {}))\n const props = usePage<AlfakitPageProps>().props\n const panel = props.alfakit\n\n // Endpoints and error notifications follow the open page.\n if (panel) {\n context.actions.setEndpoints({\n action: panel.endpoints.action ?? '',\n modal: panel.endpoints.modal ?? '',\n })\n }\n\n useEffect(() => {\n syncStarterKitPage(props)\n }, [props])\n\n useEffect(() => {\n installStarterKitRuntime(\n (notification) => showToast(notification),\n (key, replacements, fallback) => context.translator.t(key, replacements, fallback),\n )\n setToasterId(STARTER_KIT_TOASTER_ID)\n\n return () => {\n // A host page is open: its errors and toasts are the host's again.\n syncStarterKitPage(null)\n setToasterId(undefined)\n }\n }, [context])\n\n return (\n <AlfakitProvider value={context}>\n {/* The reading direction and language of Alfakit's content only: the host layout around\n it (sidebar, header) and the app's own pages keep the document's. */}\n <div\n data-slot=\"starter-kit-root\"\n dir={panel?.localization?.direction ?? 'ltr'}\n lang={panel?.localization?.tag}\n className=\"contents\"\n >\n <AlfakitRoot toasterId={STARTER_KIT_TOASTER_ID}>{children}</AlfakitRoot>\n </div>\n </AlfakitProvider>\n )\n}\n\n/**\n * The page component for `Alfakit/Page` in a starter kit app: the host's\n * `resources/js/pages/Alfakit/Page.tsx` (written by `alfakit:install --starter-kit`) exports it,\n * so Inertia's page resolution, the root view's page preload and SSR all find it like any\n * other host page.\n *\n * ```tsx\n * import { createStarterKitPage } from '@alfakit/react'\n * import AppLayout from '@/layouts/app-layout'\n *\n * export default createStarterKitPage({ layout: AppLayout })\n * ```\n */\nexport function createStarterKitPage(options: StarterKitPageOptions = {}) {\n // Panel pages draw named icons: fetch `lucide-react/dynamic` with the page.\n preloadIcons()\n // `'js:name'` chart option callbacks, Chart.js plugins and rich editor extensions.\n configureCharts(options)\n registerRichEditorExtensions(options.richEditorExtensions ?? {})\n\n function AlfakitStarterKitPage() {\n return <AlfakitPage />\n }\n\n // A layout callback: the host layout (with the page's breadcrumbs), then Alfakit's root.\n AlfakitStarterKitPage.layout = (props: Record<string, unknown>) => [\n ...(options.layout && usesStarterKitHostLayout(props)\n ? [[options.layout, { breadcrumbs: starterKitBreadcrumbsFromProps(props) }] as const]\n : []),\n [AlfakitStarterKitRoot, { options }] as const,\n ]\n\n return AlfakitStarterKitPage\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAiEA,SAAgB,wBAAwB,EAAE,UAAU,mBAAmC;CACrF,MAAM,QAAQ,gBAAgB;CAE9B,MAAM,QAAQ,mBAAmB,MAAM,QAAQ,mBAAmB;CAElE,OACE,qBAAC,OAAD;EACE,aAAU;EACV,yBAAsB;EACtB,WAAW,GACT,gEACA,WAAW,UAAU,YACvB;EANF,UAAA;GAQE,oBAAC,MAAD,EAAM,MAAK,gBAAiB,CAAA;GAC3B;GACD,oBAAC,MAAD,EAAM,MAAK,cAAe,CAAA;EACvB;;AAET;AAIA,IAAI,iBAA6C;AAEjD,SAAS,wBAAwB,SAAqD;CAIpF,MAAM,aAAa,mBAAmB,QAAQ,QAAQ;CACtD,MAAM,WAAW,sBAAsB,CAAC,CACrC,aAAa,gBAAgB,CAAC,CAC9B,SAAS,eAAe,UAAU,CAAC,CACnC,SAAS,cAAc,uBAAuB,CAAC,CAC/C,aAAa,WAAW,UAAU,CAAC,CACnC,aAAa,QAAQ,cAAc,CAAC,CAAC;CACxC,MAAM,aAAa,IAAI,WAAW;CAElC,OAAO;EACL;EACA,OAAO;GAAE,gBAAgB;GAAa,GAAG,WAAW;GAAO,GAAG,QAAQ;EAAM;EAC5E;EACA,SAAS,IAAI,aAAa;GACxB,WAAW;IAAE,QAAQ;IAAI,OAAO;GAAG;GAEnC,UAAU,UAAU;IAClB,MAAM,eAAe,gCACnB,OACA,OACC,KAAK,cAAc,aAAa,WAAW,EAAE,KAAK,cAAc,QAAQ,CAC3E;IAEA,IAAI,cAAc,UAAU,YAAY;GAC1C;EACF,CAAC;CACH;AACF;AAEA,SAAS,kBAAkB,SAAqD;CAC9E,IAAI,OAAO,WAAW,aAAa,OAAO,wBAAwB,OAAO;CAEzE,mBAAmB,wBAAwB,OAAO;CAElD,OAAO;AACT;;;;;;AAOA,SAAgB,sBAAsB,EACpC,SACA,YAIC;CACD,MAAM,CAAC,WAAW,eAAe,kBAAkB,WAAW,CAAC,CAAC,CAAC;CACjE,MAAM,QAAQ,QAA0B,CAAC,CAAC;CAC1C,MAAM,QAAQ,MAAM;CAGpB,IAAI,OACF,QAAQ,QAAQ,aAAa;EAC3B,QAAQ,MAAM,UAAU,UAAU;EAClC,OAAO,MAAM,UAAU,SAAS;CAClC,CAAC;CAGH,gBAAgB;EACd,mBAAmB,KAAK;CAC1B,GAAG,CAAC,KAAK,CAAC;CAEV,gBAAgB;EACd,0BACG,iBAAiB,UAAU,YAAY,IACvC,KAAK,cAAc,aAAa,QAAQ,WAAW,EAAE,KAAK,cAAc,QAAQ,CACnF;EACA,aAAa,sBAAsB;EAEnC,aAAa;GAEX,mBAAmB,IAAI;GACvB,aAAa,KAAA,CAAS;EACxB;CACF,GAAG,CAAC,OAAO,CAAC;CAEZ,OACE,oBAAC,iBAAD;EAAiB,OAAO;EAGtB,UAAA,oBAAC,OAAD;GACE,aAAU;GACV,KAAK,OAAO,cAAc,aAAa;GACvC,MAAM,OAAO,cAAc;GAC3B,WAAU;GAEV,UAAA,oBAAC,aAAD;IAAa,WAAW;IAAyB;GAAsB,CAAA;EACpE,CAAA;CACU,CAAA;AAErB;;;;;;;;;;;;;;AAeA,SAAgB,qBAAqB,UAAiC,CAAC,GAAG;CAExE,aAAa;CAEb,gBAAgB,OAAO;CACvB,6BAA6B,QAAQ,wBAAwB,CAAC,CAAC;CAE/D,SAAS,wBAAwB;EAC/B,OAAO,oBAAC,aAAD,CAAc,CAAA;CACvB;CAGA,sBAAsB,UAAU,UAAmC,CACjE,GAAI,QAAQ,UAAU,yBAAyB,KAAK,IAChD,CAAC,CAAC,QAAQ,QAAQ,EAAE,aAAa,+BAA+B,KAAK,EAAE,CAAC,CAAU,IAClF,CAAC,GACL,CAAC,uBAAuB,EAAE,QAAQ,CAAC,CACrC;CAEA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"starter-kit.js","names":[],"sources":["../../src/starter-kit/starter-kit.tsx"],"sourcesContent":["import {\n ActionRunner,\n STARTER_KIT_TOASTER_ID,\n Translator,\n configureCharts,\n installStarterKitRuntime,\n registerRichEditorExtensions,\n resolveRequestErrorNotification,\n starterKitBreadcrumbsFromProps,\n syncStarterKitPage,\n usesStarterKitHostLayout,\n type AlfakitOptions,\n type AlfakitPageProps,\n type StarterKitBreadcrumb,\n} from '@alfakit/core'\nimport { usePage } from '@inertiajs/react'\nimport { useEffect, useState, type ComponentType, type ReactNode } from 'react'\nimport { setToasterId, showToast } from '../components/notifications/flash-toasts'\nimport { DebugPanelSlot } from '../components/panel/debug-panel-slot'\nimport { Slot } from '../components/panel/slot'\nimport {\n AlfakitProvider,\n useAlfakitProps,\n type AlfakitContextValue,\n type AnyComponent,\n} from '../context'\nimport { AlfakitRoot } from '../create-alfakit'\nimport type { AppLayoutProps } from '../layouts/app-layout'\nimport { AuthLayout } from '../layouts/auth-layout'\nimport { lazyPanelLayouts } from '../layouts/lazy-layouts'\nimport { MAX_WIDTHS } from '../layouts/max-widths'\nimport { discoverComponents } from '../lib/discovery'\nimport { preloadIcons } from '../lib/icons'\nimport { cn } from '../lib/utils'\nimport { AlfakitPage } from '../pages/page'\nimport { createDefaultRegistry } from '../registry'\n\n/** The host's layout, as the starter kits' `AppLayout` takes its props. */\nexport type StarterKitLayout = ComponentType<{\n breadcrumbs?: StarterKitBreadcrumb[]\n children: ReactNode\n}>\n\nexport interface StarterKitPageOptions extends Pick<\n AlfakitOptions<AnyComponent>,\n 'components' | 'pages' | 'chartFormatters' | 'chartPlugins' | 'richEditorExtensions'\n> {\n /**\n * The host layout Alfakit pages render in, usually the starter kit's `AppLayout`\n * (`@/layouts/app-layout`). It receives the page's breadcrumbs as `{ title, href }`.\n * `null` renders the pages without a host layout.\n */\n layout?: StarterKitLayout | null\n /**\n * Auto-registered custom components, as for `createAlfakit({ discover })`: pass\n * `import.meta.glob('../../alfakit/{fields,columns,entries,widgets,pages,components,blocks}/**\\/*.tsx')`\n * from `resources/js/pages/Alfakit/Page.tsx` and every file there registers under its name.\n */\n discover?: AlfakitOptions<AnyComponent>['discover']\n}\n\n/**\n * The content area of an Alfakit page inside the host layout: what `layout:app` renders in\n * starter kit mode instead of Alfakit's sidebar and top bar. Replace it with\n * `createStarterKitPage({ components: { 'layout:app': MyContent } })`.\n */\nexport function StarterKitContentLayout({ children, maxContentWidth }: AppLayoutProps) {\n const props = useAlfakitProps()\n // The host's pages fill the content area, so starter kit pages default to the full width.\n const width = maxContentWidth ?? props.alfakit.maxContentWidth ?? 'full'\n\n return (\n <div\n data-slot=\"content\"\n data-alfakit-embedded=\"\"\n className={cn(\n 'mx-auto flex w-full min-w-0 flex-1 flex-col gap-6 p-4 md:p-6',\n MAX_WIDTHS[width] ?? 'max-w-full',\n )}\n >\n <Slot name=\"content.start\" />\n {children}\n <Slot name=\"content.end\" />\n </div>\n )\n}\n\n// One context for the browser session (it survives navigations between host and Alfakit\n// pages); a server render builds one per request so concurrent renders never share a locale.\nlet browserContext: AlfakitContextValue | null = null\n\nfunction createStarterKitContext(options: StarterKitPageOptions): AlfakitContextValue {\n // The host's layout replaces the panel's. Auth pages (invitations, tenant registration)\n // keep the panel's card; the fullscreen layout loads on demand.\n // Auto-registered files sit between the built-ins and the explicit `components` / `pages`.\n const discovered = discoverComponents(options.discover)\n const registry = createDefaultRegistry()\n .registerMany(lazyPanelLayouts)\n .register('layout:auth', AuthLayout)\n .register('layout:app', StarterKitContentLayout)\n .registerMany(discovered.components)\n .registerMany(options.components ?? {})\n const translator = new Translator()\n\n return {\n registry,\n pages: { 'Alfakit/Page': AlfakitPage, ...discovered.pages, ...options.pages },\n translator,\n actions: new ActionRunner({\n endpoints: { action: '', modal: '' },\n // A failed action request is a toast, never a silent no-op.\n onError: (error) => {\n const notification = resolveRequestErrorNotification(\n error,\n null,\n (key, replacements, fallback) => translator.t(key, replacements, fallback),\n )\n\n if (notification) showToast(notification)\n },\n }),\n }\n}\n\nfunction starterKitContext(options: StarterKitPageOptions): AlfakitContextValue {\n if (typeof window === 'undefined') return createStarterKitContext(options)\n\n browserContext ??= createStarterKitContext(options)\n\n return browserContext\n}\n\n/**\n * The Inertia layout around every Alfakit page in starter kit mode, inside the host layout:\n * Alfakit's context, modals, toasts and the reading direction. It stays mounted while the\n * user moves between Alfakit pages.\n */\nexport function AlfakitStarterKitRoot({\n options,\n children,\n}: {\n options?: StarterKitPageOptions\n children?: ReactNode\n}) {\n const [context] = useState(() => starterKitContext(options ?? {}))\n const props = usePage<AlfakitPageProps>().props\n const panel = props.alfakit\n\n // Endpoints and error notifications follow the open page.\n if (panel) {\n context.actions.setEndpoints({\n action: panel.endpoints.action ?? '',\n modal: panel.endpoints.modal ?? '',\n })\n }\n\n useEffect(() => {\n syncStarterKitPage(props)\n }, [props])\n\n useEffect(() => {\n installStarterKitRuntime(\n (notification) => showToast(notification),\n (key, replacements, fallback) => context.translator.t(key, replacements, fallback),\n )\n setToasterId(STARTER_KIT_TOASTER_ID)\n\n return () => {\n // A host page is open: its errors and toasts are the host's again.\n syncStarterKitPage(null)\n setToasterId(undefined)\n }\n }, [context])\n\n return (\n <AlfakitProvider value={context}>\n {/* The reading direction and language of Alfakit's content only: the host layout around\n it (sidebar, header) and the app's own pages keep the document's. */}\n <div\n data-slot=\"starter-kit-root\"\n dir={panel?.localization?.direction ?? 'ltr'}\n lang={panel?.localization?.tag}\n className=\"contents\"\n >\n <AlfakitRoot toasterId={STARTER_KIT_TOASTER_ID}>\n {children}\n {/* The debug panel's button lives in AlfaKit's top bar, which a starter kit doesn't\n render: float it in the corner instead (nothing without `alfakitDebug`). The corner is the\n document's right: the kit's sidebar stays on the left in a right-to-left panel language. */}\n <div data-slot=\"starter-kit-debug-panel\" className=\"fixed right-16 bottom-16 z-40\">\n <DebugPanelSlot />\n </div>\n </AlfakitRoot>\n </div>\n </AlfakitProvider>\n )\n}\n\n/**\n * The page component for `Alfakit/Page` in a starter kit app: the host's\n * `resources/js/pages/Alfakit/Page.tsx` (written by `alfakit:install --starter-kit`) exports it,\n * so Inertia's page resolution, the root view's page preload and SSR all find it like any\n * other host page.\n *\n * ```tsx\n * import { createStarterKitPage } from '@alfakit/react'\n * import AppLayout from '@/layouts/app-layout'\n *\n * export default createStarterKitPage({ layout: AppLayout })\n * ```\n */\nexport function createStarterKitPage(options: StarterKitPageOptions = {}) {\n // Panel pages draw named icons: fetch `lucide-react/dynamic` with the page.\n preloadIcons()\n // `'js:name'` chart option callbacks, Chart.js plugins and rich editor extensions.\n configureCharts(options)\n registerRichEditorExtensions(options.richEditorExtensions ?? {})\n\n function AlfakitStarterKitPage() {\n return <AlfakitPage />\n }\n\n // A layout callback: the host layout (with the page's breadcrumbs), then Alfakit's root.\n AlfakitStarterKitPage.layout = (props: Record<string, unknown>) => [\n ...(options.layout && usesStarterKitHostLayout(props)\n ? [[options.layout, { breadcrumbs: starterKitBreadcrumbsFromProps(props) }] as const]\n : []),\n [AlfakitStarterKitRoot, { options }] as const,\n ]\n\n return AlfakitStarterKitPage\n}\n"],"mappings":"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"}
|
package/dist/styles.css
CHANGED
|
@@ -289,7 +289,8 @@
|
|
|
289
289
|
}
|
|
290
290
|
/* Rich editor mentions, merge tags and custom blocks (RichEditor extensions) */
|
|
291
291
|
.alfakit-prose :where(.alfakit-mention):not(:where(.alfakit-not-prose *)) {
|
|
292
|
-
|
|
292
|
+
/* Full-contrast text on the tint (WCAG AA); the ring marks it as a mention. */
|
|
293
|
+
@apply bg-primary/10 text-foreground ring-primary/30 rounded px-1 font-medium ring-1;
|
|
293
294
|
}
|
|
294
295
|
.alfakit-prose :where(.alfakit-merge-tag):not(:where(.alfakit-not-prose *)) {
|
|
295
296
|
@apply bg-muted text-muted-foreground rounded px-1 font-mono text-xs;
|
package/dist/themes.css
CHANGED
|
@@ -742,6 +742,10 @@
|
|
|
742
742
|
max-height: none;
|
|
743
743
|
width: 100%;
|
|
744
744
|
max-width: 100%;
|
|
745
|
+
/* The shadcn centring utilities read these variables. Resetting them survives a
|
|
746
|
+
minifier that drops `translate: none` in favour of the utilities' `translate`. */
|
|
747
|
+
--tw-translate-x: 0;
|
|
748
|
+
--tw-translate-y: 0;
|
|
745
749
|
translate: none;
|
|
746
750
|
transform: none;
|
|
747
751
|
border-radius: 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfakit/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "React + shadcn/ui renderer for Alfakit, the Laravel admin panel builder by Alfa Six.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alfa Six (https://alfasixbh.com)",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"react-day-picker": "^10.0.2",
|
|
97
97
|
"sonner": "^2.0.8",
|
|
98
98
|
"tailwind-merge": "^3.7.0",
|
|
99
|
-
"@alfakit/core": "0.1.
|
|
99
|
+
"@alfakit/core": "0.1.10"
|
|
100
100
|
},
|
|
101
101
|
"peerDependencies": {
|
|
102
102
|
"@inertiajs/react": "^3.0.0",
|
|
@@ -469,6 +469,8 @@ function ActionModal({ modal, hidden = false }: { modal: OpenModal; hidden?: boo
|
|
|
469
469
|
// Pinned to the top of the scrolling dialog, edge to edge.
|
|
470
470
|
schema.stickyHeader &&
|
|
471
471
|
'bg-background sticky -top-6 z-10 -mx-6 -mt-6 rounded-t-lg border-b px-6 pt-6 pb-4',
|
|
472
|
+
// Room for the close button (X): a long heading would run under it.
|
|
473
|
+
schema.closeButton !== false && (schema.stickyHeader ? 'pe-12' : 'pe-6'),
|
|
472
474
|
)}
|
|
473
475
|
>
|
|
474
476
|
{header}
|
|
@@ -80,7 +80,7 @@ export function DevToolsInspector() {
|
|
|
80
80
|
size="icon"
|
|
81
81
|
aria-label={t('dev_tools.open', {}, 'Open schema inspector')}
|
|
82
82
|
data-slot="dev-tools-toggle"
|
|
83
|
-
className="fixed end-4 bottom-
|
|
83
|
+
className="fixed end-4 bottom-16 z-40 rounded-full shadow-md [[data-slot=starter-kit-root]_&]:end-auto [[data-slot=starter-kit-root]_&]:right-4"
|
|
84
84
|
>
|
|
85
85
|
<BracesIcon />
|
|
86
86
|
</Button>
|
|
@@ -66,7 +66,7 @@ export function CheckboxListField({ component }: { component: CheckboxListSchema
|
|
|
66
66
|
<FieldWrapper field={component} htmlFor={id} error={error}>
|
|
67
67
|
<div className="flex flex-col gap-2">
|
|
68
68
|
{(component.searchable || (component.bulkToggleable && !readOnly)) && (
|
|
69
|
-
<div className="flex items-center gap-2">
|
|
69
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
70
70
|
{component.searchable && (
|
|
71
71
|
<Input
|
|
72
72
|
value={search}
|
|
@@ -77,7 +77,7 @@ export function CheckboxListField({ component }: { component: CheckboxListSchema
|
|
|
77
77
|
/>
|
|
78
78
|
)}
|
|
79
79
|
{component.bulkToggleable && !readOnly && (
|
|
80
|
-
<div className="flex gap-1">
|
|
80
|
+
<div className="flex flex-wrap gap-1">
|
|
81
81
|
{!selectAll?.hidden && (
|
|
82
82
|
<Button
|
|
83
83
|
type="button"
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
customBlockNodeConfig,
|
|
12
12
|
EditorSuggestions,
|
|
13
13
|
fieldActionContext,
|
|
14
|
+
fixedContainingBlockOffset,
|
|
14
15
|
groupCustomBlocks,
|
|
15
16
|
groupTriggerButton,
|
|
16
17
|
isCustomBlockResult,
|
|
@@ -358,6 +359,9 @@ export function ToolbarGroupButton({
|
|
|
358
359
|
onRun: (name: string) => void
|
|
359
360
|
}) {
|
|
360
361
|
const buttons = group.buttons.filter((name) => commands[name])
|
|
362
|
+
// After a pick, typing goes on in the editor (the command focused it); the menu would
|
|
363
|
+
// otherwise return focus to its trigger. Closing without a pick (Escape) still does.
|
|
364
|
+
const picked = useRef(false)
|
|
361
365
|
|
|
362
366
|
if (buttons.length === 0) return null
|
|
363
367
|
|
|
@@ -385,6 +389,13 @@ export function ToolbarGroupButton({
|
|
|
385
389
|
align="start"
|
|
386
390
|
aria-label={group.label}
|
|
387
391
|
className={cn(!group.textual && 'flex min-w-0 flex-row gap-0.5')}
|
|
392
|
+
onCloseAutoFocus={(event) => {
|
|
393
|
+
if (!picked.current) return
|
|
394
|
+
|
|
395
|
+
picked.current = false
|
|
396
|
+
event.preventDefault()
|
|
397
|
+
editor.commands.focus()
|
|
398
|
+
}}
|
|
388
399
|
>
|
|
389
400
|
{buttons.map((name) => {
|
|
390
401
|
const label = commandLabel(name, commands, translate)
|
|
@@ -396,7 +407,10 @@ export function ToolbarGroupButton({
|
|
|
396
407
|
aria-label={group.textual ? undefined : label}
|
|
397
408
|
title={label}
|
|
398
409
|
className={cn(pressed && 'bg-accent text-accent-foreground')}
|
|
399
|
-
onSelect={() =>
|
|
410
|
+
onSelect={() => {
|
|
411
|
+
picked.current = true
|
|
412
|
+
onRun(name)
|
|
413
|
+
}}
|
|
400
414
|
>
|
|
401
415
|
{icons(name)}
|
|
402
416
|
{group.textual && <span>{label}</span>}
|
|
@@ -436,7 +450,15 @@ export function SuggestionPopup({
|
|
|
436
450
|
data-slot="rich-editor-suggestions"
|
|
437
451
|
className="bg-popover text-popover-foreground fixed z-50 min-w-48 overflow-hidden rounded-md border p-1 shadow-md"
|
|
438
452
|
// Runtime coordinates cannot be Tailwind classes.
|
|
439
|
-
|
|
453
|
+
// Inside a modal the dialog's transform is the containing block of this fixed list.
|
|
454
|
+
style={
|
|
455
|
+
state.coords
|
|
456
|
+
? {
|
|
457
|
+
left: state.coords.left - fixedContainingBlockOffset(editor.view.dom).x,
|
|
458
|
+
top: state.coords.bottom + 4 - fixedContainingBlockOffset(editor.view.dom).y,
|
|
459
|
+
}
|
|
460
|
+
: undefined
|
|
461
|
+
}
|
|
440
462
|
>
|
|
441
463
|
{state.loading && state.items.length === 0 ? (
|
|
442
464
|
<div className="text-muted-foreground px-2 py-1.5 text-sm">
|
|
@@ -1389,7 +1389,8 @@ function Dialogs({
|
|
|
1389
1389
|
open={dialog?.type === 'createFolder' || dialog?.type === 'rename'}
|
|
1390
1390
|
onOpenChange={(open) => !open && close()}
|
|
1391
1391
|
>
|
|
1392
|
-
|
|
1392
|
+
{/* The title and the labelled field say it all: no description. */}
|
|
1393
|
+
<DialogContent data-slot="file-manager-dialog" aria-describedby={undefined}>
|
|
1393
1394
|
<form
|
|
1394
1395
|
className="flex flex-col gap-4"
|
|
1395
1396
|
onSubmit={(event) => {
|
|
@@ -1763,6 +1764,7 @@ function Preview({
|
|
|
1763
1764
|
<DialogContent
|
|
1764
1765
|
className="flex max-h-[90vh] flex-col sm:max-w-4xl"
|
|
1765
1766
|
data-slot="file-manager-preview"
|
|
1767
|
+
aria-describedby={undefined}
|
|
1766
1768
|
onKeyDown={onKeyDown}
|
|
1767
1769
|
>
|
|
1768
1770
|
<DialogHeader>
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
FormStore,
|
|
4
4
|
applyFormRefresh,
|
|
5
5
|
formActionsVisible,
|
|
6
|
+
formIdPrefix,
|
|
6
7
|
formIdentity,
|
|
7
8
|
formRefreshFor,
|
|
8
9
|
isImplicitSubmit,
|
|
@@ -17,8 +18,15 @@ import {
|
|
|
17
18
|
wizardProgressStore,
|
|
18
19
|
} from '@alfakit/core'
|
|
19
20
|
import { router } from '@inertiajs/react'
|
|
20
|
-
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
21
|
-
import {
|
|
21
|
+
import { useCallback, useEffect, useId, useMemo, useRef } from 'react'
|
|
22
|
+
import {
|
|
23
|
+
useActionRunner,
|
|
24
|
+
useAlfakitProps,
|
|
25
|
+
usePageSchema,
|
|
26
|
+
usePanel,
|
|
27
|
+
useStandalone,
|
|
28
|
+
useT,
|
|
29
|
+
} from '../../context'
|
|
22
30
|
import { useStore } from '../../hooks/use-store'
|
|
23
31
|
import { ActionList } from '../actions/action-list'
|
|
24
32
|
import { showToast } from '../notifications/flash-toasts'
|
|
@@ -42,12 +50,17 @@ export function FormBlock({ block }: { block: FormBlockSchema }) {
|
|
|
42
50
|
// identity starts from server state again. The token is re-issued per response, so it
|
|
43
51
|
// is swapped in place instead of being part of the identity.
|
|
44
52
|
const identity = formIdentity(page, block)
|
|
53
|
+
// Several standalone components can share a page (and field names): each form gets its
|
|
54
|
+
// own field id prefix, so labels and errors point at their own inputs.
|
|
55
|
+
const standalone = useStandalone()
|
|
56
|
+
const instanceId = useId()
|
|
45
57
|
|
|
46
58
|
const store = useMemo(
|
|
47
59
|
() =>
|
|
48
60
|
new FormStore(block, {
|
|
49
61
|
token,
|
|
50
62
|
component: block.component,
|
|
63
|
+
idPrefix: standalone ? formIdPrefix(`form-${instanceId}`) : undefined,
|
|
51
64
|
refreshUrl: panel.endpoints.schemaRefresh,
|
|
52
65
|
optionsUrl: panel.endpoints.schemaOptions,
|
|
53
66
|
validateUrl: panel.endpoints.schemaValidate,
|
|
@@ -25,6 +25,8 @@ import { Icon } from '../../lib/icons'
|
|
|
25
25
|
import { cn } from '../../lib/utils'
|
|
26
26
|
import { usePageActionContext } from '../actions/action-context'
|
|
27
27
|
import { ActionList } from '../actions/action-list'
|
|
28
|
+
import { useOptionalFormStore } from '../form/form-context'
|
|
29
|
+
import { useFieldActionContext } from '../schema/field-actions'
|
|
28
30
|
import { RenderComponent } from '../schema/schema-renderer'
|
|
29
31
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
|
|
30
32
|
import { MixedContent } from './mixed-content'
|
|
@@ -67,7 +69,12 @@ const CALLOUT_ICON_SIZES: Record<string, string> = {
|
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
export function CalloutComponent({ component }: { component: CalloutSchema }) {
|
|
70
|
-
|
|
72
|
+
// Inside a form the actions run as field actions of this component, so `$get` / `$set`
|
|
73
|
+
// reach the form (like a section's header actions).
|
|
74
|
+
const form = useOptionalFormStore()
|
|
75
|
+
const pageContext = usePageActionContext()
|
|
76
|
+
const fieldContext = useFieldActionContext(component)
|
|
77
|
+
const context = form ? fieldContext : pageContext
|
|
71
78
|
const tone = component.color ?? component.status
|
|
72
79
|
// `iconColor()`, or the status colour once `color(null)` removed the background.
|
|
73
80
|
const iconColor = component.iconColor ?? (component.color ? undefined : component.status)
|
|
@@ -113,19 +120,30 @@ export function CalloutComponent({ component }: { component: CalloutSchema }) {
|
|
|
113
120
|
className={cn('mt-2', actionsAlignmentClass(component.footerActionsAlignment))}
|
|
114
121
|
/>
|
|
115
122
|
)}
|
|
116
|
-
<MixedContent
|
|
123
|
+
<MixedContent
|
|
124
|
+
items={component.footer}
|
|
125
|
+
name="callout-footer"
|
|
126
|
+
className="mt-2"
|
|
127
|
+
context={context}
|
|
128
|
+
/>
|
|
117
129
|
</div>
|
|
118
130
|
<MixedContent
|
|
119
131
|
items={component.controls}
|
|
120
132
|
name="callout-controls"
|
|
121
133
|
className="-me-1 -mt-1 shrink-0 gap-1"
|
|
134
|
+
context={context}
|
|
122
135
|
/>
|
|
123
136
|
</div>
|
|
124
137
|
)
|
|
125
138
|
}
|
|
126
139
|
|
|
127
140
|
export function EmptyStateComponent({ component }: { component: EmptyStateComponentSchema }) {
|
|
128
|
-
|
|
141
|
+
// Inside a form the actions run as field actions of this component, so `$get` / `$set`
|
|
142
|
+
// reach the form (like a section's header actions).
|
|
143
|
+
const form = useOptionalFormStore()
|
|
144
|
+
const pageContext = usePageActionContext()
|
|
145
|
+
const fieldContext = useFieldActionContext(component)
|
|
146
|
+
const context = form ? fieldContext : pageContext
|
|
129
147
|
|
|
130
148
|
return (
|
|
131
149
|
<div
|
|
@@ -158,7 +176,12 @@ export function EmptyStateComponent({ component }: { component: EmptyStateCompon
|
|
|
158
176
|
{component.actions && component.actions.length > 0 && (
|
|
159
177
|
<ActionList actions={component.actions} context={context} className="mt-2 justify-center" />
|
|
160
178
|
)}
|
|
161
|
-
<MixedContent
|
|
179
|
+
<MixedContent
|
|
180
|
+
items={component.footer}
|
|
181
|
+
name="empty-state-footer"
|
|
182
|
+
className="justify-center"
|
|
183
|
+
context={context}
|
|
184
|
+
/>
|
|
162
185
|
</div>
|
|
163
186
|
)
|
|
164
187
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyActionSchema, ContentSlotItem } from '@alfakit/core'
|
|
1
|
+
import type { ActionContext, AnyActionSchema, ContentSlotItem } from '@alfakit/core'
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
3
|
import { cn } from '../../lib/utils'
|
|
4
4
|
import { usePageActionContext } from '../actions/action-context'
|
|
@@ -7,19 +7,23 @@ import { RenderComponent } from '../schema/schema-renderer'
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Actions and components placed in a layout component's fixed spot (a section's
|
|
10
|
-
* `afterHeader()`, a callout's `controls()`). Actions run in the
|
|
11
|
-
*
|
|
10
|
+
* `afterHeader()`, a callout's `controls()`). Actions run in the owner's action context
|
|
11
|
+
* (`context`: inside a form, the owner's field context, so `$get` / `$set` work), else the
|
|
12
|
+
* page's.
|
|
12
13
|
*/
|
|
13
14
|
export function MixedContent({
|
|
14
15
|
items,
|
|
15
16
|
name,
|
|
16
17
|
className,
|
|
18
|
+
context: ownerContext,
|
|
17
19
|
}: {
|
|
18
20
|
items: ContentSlotItem[] | undefined
|
|
19
21
|
name: string
|
|
20
22
|
className?: string
|
|
23
|
+
context?: ActionContext
|
|
21
24
|
}) {
|
|
22
|
-
const
|
|
25
|
+
const pageContext = usePageActionContext()
|
|
26
|
+
const context = ownerContext ?? pageContext
|
|
23
27
|
|
|
24
28
|
if (!items?.length) {
|
|
25
29
|
return null
|
|
@@ -86,7 +86,7 @@ export function SectionComponent({ component }: { component: SectionSchema }) {
|
|
|
86
86
|
{component.headerActions && component.headerActions.length > 0 && (
|
|
87
87
|
<ActionList actions={component.headerActions} context={context} size="sm" />
|
|
88
88
|
)}
|
|
89
|
-
<MixedContent items={component.afterHeader} name="section-after-header" />
|
|
89
|
+
<MixedContent items={component.afterHeader} name="section-after-header" context={context} />
|
|
90
90
|
{component.collapsible && (
|
|
91
91
|
<CollapsibleTrigger asChild>
|
|
92
92
|
<Button
|
|
@@ -123,7 +123,7 @@ export function SectionComponent({ component }: { component: SectionSchema }) {
|
|
|
123
123
|
className={actionsAlignmentClass(component.footerActionsAlignment) || undefined}
|
|
124
124
|
/>
|
|
125
125
|
)}
|
|
126
|
-
<MixedContent items={component.footer} name="section-footer-content" />
|
|
126
|
+
<MixedContent items={component.footer} name="section-footer-content" context={context} />
|
|
127
127
|
</div>
|
|
128
128
|
) : null
|
|
129
129
|
|
|
@@ -3,9 +3,13 @@ import {
|
|
|
3
3
|
notificationTintStyles,
|
|
4
4
|
onDatabaseNotificationsSent,
|
|
5
5
|
onOpenDatabaseNotifications,
|
|
6
|
+
pwaStore,
|
|
7
|
+
startPwa,
|
|
8
|
+
togglePushNotifications,
|
|
6
9
|
} from '@alfakit/core'
|
|
7
|
-
import { BellIcon, CheckCheckIcon, Trash2Icon } from 'lucide-react'
|
|
10
|
+
import { BellIcon, BellOffIcon, BellRingIcon, CheckCheckIcon, Trash2Icon } from 'lucide-react'
|
|
8
11
|
import { useEffect, useMemo, useState } from 'react'
|
|
12
|
+
import { toast } from 'sonner'
|
|
9
13
|
import { useDirection, useFormatter, usePanel, useT } from '../../context'
|
|
10
14
|
import { useStore } from '../../hooks/use-store'
|
|
11
15
|
import { cssColor, textColorClasses } from '../../lib/colors'
|
|
@@ -86,6 +90,29 @@ export function NotificationsMenu({ variant = 'topbar' }: NotificationsMenuProps
|
|
|
86
90
|
[center],
|
|
87
91
|
)
|
|
88
92
|
|
|
93
|
+
// `pwa(push: true)` in a starter kit: the app's user menu has no AlfaKit entries, so Web
|
|
94
|
+
// Push is turned on or off from the bell's menu.
|
|
95
|
+
const pwa = useStore(pwaStore)
|
|
96
|
+
const kitPush = Boolean(panel.starterKit && panel.pwa?.push)
|
|
97
|
+
const pushToggle = kitPush && pwa.push !== 'unsupported'
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (kitPush) startPwa(panel.pwa)
|
|
101
|
+
}, [kitPush, panel.pwa])
|
|
102
|
+
|
|
103
|
+
const togglePush = async () => {
|
|
104
|
+
const { message } = await togglePushNotifications()
|
|
105
|
+
const text = t(message.key, {}, message.fallback)
|
|
106
|
+
|
|
107
|
+
if (message.error) toast.error(text)
|
|
108
|
+
else toast.success(text)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const pushLabel =
|
|
112
|
+
pwa.push === 'subscribed'
|
|
113
|
+
? t('pwa.notifications.disable', {}, 'Turn off notifications')
|
|
114
|
+
: t('pwa.notifications.enable', {}, 'Turn on notifications')
|
|
115
|
+
|
|
89
116
|
if (!config || !center) {
|
|
90
117
|
return null
|
|
91
118
|
}
|
|
@@ -145,6 +172,20 @@ export function NotificationsMenu({ variant = 'topbar' }: NotificationsMenuProps
|
|
|
145
172
|
<div className="flex items-center justify-between border-b px-3 py-2">
|
|
146
173
|
<span className="text-sm font-semibold">{t('notifications', {}, 'Notifications')}</span>
|
|
147
174
|
<div className="flex items-center gap-1">
|
|
175
|
+
{pushToggle && (
|
|
176
|
+
<Button
|
|
177
|
+
type="button"
|
|
178
|
+
variant="ghost"
|
|
179
|
+
size="icon-sm"
|
|
180
|
+
data-slot="pwa-push"
|
|
181
|
+
disabled={pwa.pushBusy || pwa.push === 'denied'}
|
|
182
|
+
title={pushLabel}
|
|
183
|
+
aria-label={pushLabel}
|
|
184
|
+
onClick={() => void togglePush()}
|
|
185
|
+
>
|
|
186
|
+
{pwa.push === 'subscribed' ? <BellOffIcon /> : <BellRingIcon />}
|
|
187
|
+
</Button>
|
|
188
|
+
)}
|
|
148
189
|
{state.unreadCount > 0 && (
|
|
149
190
|
<Button
|
|
150
191
|
type="button"
|
|
@@ -133,7 +133,7 @@ function NavSubItem({ item }: { item: NavigationItemSchema }) {
|
|
|
133
133
|
<CollapsibleContent>
|
|
134
134
|
<SidebarMenuSub>
|
|
135
135
|
{item.children.map((child) => (
|
|
136
|
-
<NavSubItem key={child.label} item={child} />
|
|
136
|
+
<NavSubItem key={child.url ?? child.label} item={child} />
|
|
137
137
|
))}
|
|
138
138
|
</SidebarMenuSub>
|
|
139
139
|
</CollapsibleContent>
|
|
@@ -175,7 +175,7 @@ function NavItem({ item }: { item: NavigationItemSchema }) {
|
|
|
175
175
|
<CollapsibleContent>
|
|
176
176
|
<SidebarMenuSub>
|
|
177
177
|
{item.children.map((child) => (
|
|
178
|
-
<NavSubItem key={child.label} item={child} />
|
|
178
|
+
<NavSubItem key={child.url ?? child.label} item={child} />
|
|
179
179
|
))}
|
|
180
180
|
</SidebarMenuSub>
|
|
181
181
|
</CollapsibleContent>
|
|
@@ -263,7 +263,7 @@ function NavGroup({ group }: { group: NavigationGroupSchema }) {
|
|
|
263
263
|
<SidebarGroupContent className={cn(flyout && 'group-data-[collapsible=icon]:hidden')}>
|
|
264
264
|
<SidebarMenu>
|
|
265
265
|
{group.items.map((item) => (
|
|
266
|
-
<NavItem key={item.label} item={item} />
|
|
266
|
+
<NavItem key={item.url ?? item.label} item={item} />
|
|
267
267
|
))}
|
|
268
268
|
</SidebarMenu>
|
|
269
269
|
</SidebarGroupContent>
|
|
@@ -81,7 +81,7 @@ export function BottomNav() {
|
|
|
81
81
|
className="md:hidden"
|
|
82
82
|
>
|
|
83
83
|
{primary.map((item) => (
|
|
84
|
-
<BottomNavLink key={item.label} item={item} />
|
|
84
|
+
<BottomNavLink key={item.url ?? item.label} item={item} />
|
|
85
85
|
))}
|
|
86
86
|
{overflow.length > 0 && (
|
|
87
87
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
@@ -109,7 +109,7 @@ export function BottomNav() {
|
|
|
109
109
|
.flatMap((item) => [item, ...(item.children ?? [])])
|
|
110
110
|
.map((item) => (
|
|
111
111
|
<Link
|
|
112
|
-
key={item.label}
|
|
112
|
+
key={item.url ?? item.label}
|
|
113
113
|
href={navigationItemUrl(item) ?? '#'}
|
|
114
114
|
onClick={() => setOpen(false)}
|
|
115
115
|
data-active={isNavigationItemActive(item) ? 'true' : undefined}
|
|
@@ -47,7 +47,7 @@ export function KeyboardShortcuts() {
|
|
|
47
47
|
const overrides = JSON.stringify(config ? (config.overrides ?? {}) : {})
|
|
48
48
|
const token = page?.token ?? ''
|
|
49
49
|
const commands = panel.commandPalette?.commands
|
|
50
|
-
const home = panel.url
|
|
50
|
+
const home = panel.dashboardUrl ?? panel.url
|
|
51
51
|
// Strings, so the effect below doesn't re-run on every render (`t` is a new function each time).
|
|
52
52
|
const overlayLabel = t('shortcuts.show_overlay', {}, 'Show keyboard shortcuts')
|
|
53
53
|
const searchLabel = t('shortcuts.focus_search', {}, 'Search the table')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ShortcutListGroup, ShortcutListItem } from '@alfakit/core'
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
ShortcutRecorder,
|
|
4
4
|
flattenActionGroup,
|
|
5
5
|
saveShortcutOverrides,
|
|
6
6
|
shortcutOverlayGroups,
|
|
@@ -52,50 +52,55 @@ export function ShortcutsOverlay({
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// While recording, the
|
|
56
|
-
//
|
|
55
|
+
// While recording, the keys pressed are the new binding: one key (with modifiers) or a
|
|
56
|
+
// sequence ("g o"), finished a second after the last key (Escape cancels). Listening in
|
|
57
|
+
// the capture phase keeps the keys from closing the dialog or running another shortcut.
|
|
57
58
|
useEffect(() => {
|
|
58
59
|
if (!recording) return
|
|
59
60
|
|
|
60
61
|
shortcuts.setPaused(true)
|
|
61
62
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
event.stopPropagation()
|
|
65
|
-
|
|
66
|
-
if (event.key === 'Escape') {
|
|
63
|
+
const recorder = new ShortcutRecorder({
|
|
64
|
+
onDone: (binding) => {
|
|
67
65
|
setRecording(null)
|
|
68
|
-
setMessage(t('shortcuts.cancelled', {}, 'Change cancelled.'))
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
}
|
|
67
|
+
const conflict = shortcuts.conflict([binding], recording)
|
|
72
68
|
|
|
73
|
-
|
|
69
|
+
if (conflict) {
|
|
70
|
+
setMessage(
|
|
71
|
+
t(
|
|
72
|
+
'shortcuts.conflict',
|
|
73
|
+
{ label: conflict.label },
|
|
74
|
+
`Already used by “${conflict.label}”.`,
|
|
75
|
+
),
|
|
76
|
+
)
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
return
|
|
79
|
+
}
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
void save(recording, [binding])
|
|
82
|
+
},
|
|
83
|
+
})
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
const listener = (event: KeyboardEvent) => {
|
|
86
|
+
event.preventDefault()
|
|
87
|
+
event.stopPropagation()
|
|
80
88
|
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
{ label: conflict.label },
|
|
86
|
-
`Already used by “${conflict.label}”.`,
|
|
87
|
-
),
|
|
88
|
-
)
|
|
89
|
+
if (event.key === 'Escape') {
|
|
90
|
+
recorder.cancel()
|
|
91
|
+
setRecording(null)
|
|
92
|
+
setMessage(t('shortcuts.cancelled', {}, 'Change cancelled.'))
|
|
89
93
|
|
|
90
94
|
return
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
|
|
97
|
+
recorder.press(event)
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
window.addEventListener('keydown', listener, true)
|
|
97
101
|
|
|
98
102
|
return () => {
|
|
103
|
+
recorder.cancel()
|
|
99
104
|
window.removeEventListener('keydown', listener, true)
|
|
100
105
|
shortcuts.setPaused(false)
|
|
101
106
|
}
|
|
@@ -114,13 +114,13 @@ export function ColumnManager({
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
const save = async (order: string[]): Promise<boolean> => {
|
|
117
|
+
const save = async (order: string[], options: { reload?: boolean } = {}): Promise<boolean> => {
|
|
118
118
|
if (!panel.endpoints.tableColumns) return false
|
|
119
119
|
|
|
120
120
|
setSaving(true)
|
|
121
121
|
|
|
122
122
|
try {
|
|
123
|
-
await saveColumnOrder(panel.endpoints.tableColumns, token, component, order)
|
|
123
|
+
await saveColumnOrder(panel.endpoints.tableColumns, token, component, order, options)
|
|
124
124
|
|
|
125
125
|
return true
|
|
126
126
|
} catch (error) {
|
|
@@ -203,7 +203,8 @@ export function ColumnManager({
|
|
|
203
203
|
setAnnouncement('')
|
|
204
204
|
setOpen(false)
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
// The visibility visit renders the new order too: no second reload racing it.
|
|
207
|
+
if (orderDraft && orderChanged) await save(orderDraft, { reload: !columns })
|
|
207
208
|
|
|
208
209
|
if (columns) visitTable(table.identifier, { columns }, ['page'])
|
|
209
210
|
}
|
|
@@ -212,7 +213,8 @@ export function ColumnManager({
|
|
|
212
213
|
const reset = async () => {
|
|
213
214
|
if (reorderable) {
|
|
214
215
|
setOrderDraft(null)
|
|
215
|
-
|
|
216
|
+
// The visibility visit below renders the defined order too.
|
|
217
|
+
await save([], { reload: !toggleable })
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
if (toggleable) {
|