@alfakit/react 0.1.12 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/components/cms/blocks.d.ts.map +1 -1
- package/dist/components/cms/blocks.js +2 -2
- package/dist/components/cms/blocks.js.map +1 -1
- package/dist/components/cms/head.d.ts +6 -4
- package/dist/components/cms/head.d.ts.map +1 -1
- package/dist/components/cms/head.js +10 -4
- package/dist/components/cms/head.js.map +1 -1
- package/dist/components/cms/page.d.ts.map +1 -1
- package/dist/components/cms/page.js +16 -3
- package/dist/components/cms/page.js.map +1 -1
- package/dist/components/fields/file-upload.js +4 -4
- package/dist/components/fields/file-upload.js.map +1 -1
- package/dist/components/fields/rich-editor.js +4 -2
- package/dist/components/fields/rich-editor.js.map +1 -1
- package/dist/components/fields/slider.js +1 -1
- package/dist/components/fields/slider.js.map +1 -1
- package/dist/components/helpdesk/chat-widget.js +1 -1
- package/dist/components/layout/section.js +1 -1
- package/dist/components/layout/tabs.js +1 -1
- package/dist/components/panel/app-sidebar.d.ts.map +1 -1
- package/dist/components/panel/app-sidebar.js +27 -5
- package/dist/components/panel/app-sidebar.js.map +1 -1
- package/dist/components/panel/global-search.js +2 -2
- package/dist/components/panel/global-search.js.map +1 -1
- package/dist/components/panel/shortcut-keys.js +1 -1
- package/dist/components/panel/shortcut-keys.js.map +1 -1
- package/dist/components/table/list-tabs.js +1 -1
- package/dist/components/table/list-tabs.js.map +1 -1
- package/dist/components/table/table-views.js +1 -1
- package/dist/components/table/table-views.js.map +1 -1
- package/dist/components/versions/record-versions.js +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +27 -2
- package/dist/context.js.map +1 -1
- package/dist/create-alfakit.d.ts.map +1 -1
- package/dist/create-alfakit.js +6 -3
- package/dist/create-alfakit.js.map +1 -1
- package/dist/eject-manifest.json +2 -2
- package/dist/index.js +1 -1
- package/dist/layouts/app-layout.d.ts.map +1 -1
- package/dist/layouts/app-layout.js +13 -3
- package/dist/layouts/app-layout.js.map +1 -1
- package/dist/lib/discovery.d.ts +4 -0
- package/dist/lib/discovery.d.ts.map +1 -1
- package/dist/lib/discovery.js +17 -7
- package/dist/lib/discovery.js.map +1 -1
- package/dist/lib/icons.d.ts +2 -1
- package/dist/lib/icons.d.ts.map +1 -1
- package/dist/lib/icons.js +55 -19
- package/dist/lib/icons.js.map +1 -1
- package/dist/pages/page.d.ts +1 -1
- package/dist/pages/page.d.ts.map +1 -1
- package/dist/pages/page.js +47 -10
- package/dist/pages/page.js.map +1 -1
- package/dist/standalone/alfakit-component.js +2 -1
- package/dist/standalone/alfakit-component.js.map +1 -1
- package/dist/standalone/alfakit-toaster.js +2 -1
- package/dist/standalone/alfakit-toaster.js.map +1 -1
- package/dist/standalone/runtime.js +1 -11
- package/dist/standalone/runtime.js.map +1 -1
- package/dist/standalone/toaster-claims.js +16 -0
- package/dist/standalone/toaster-claims.js.map +1 -0
- package/dist/starter-kit/widgets-loaded.js +63 -6
- package/dist/starter-kit/widgets-loaded.js.map +1 -1
- package/dist/starter-kit/widgets-triggers.js +1 -1
- package/dist/starter-kit/widgets-triggers.js.map +1 -1
- package/dist/themes.css +92 -0
- package/package.json +2 -2
- package/src/components/cms/blocks.tsx +7 -1
- package/src/components/cms/head.tsx +19 -4
- package/src/components/cms/page.tsx +17 -2
- package/src/components/fields/file-upload.tsx +5 -4
- package/src/components/fields/rich-editor.tsx +7 -2
- package/src/components/fields/slider.tsx +3 -1
- package/src/components/panel/app-sidebar.tsx +28 -7
- package/src/components/panel/global-search.tsx +4 -1
- package/src/components/panel/shortcut-keys.tsx +1 -1
- package/src/components/table/list-tabs.tsx +3 -1
- package/src/components/table/table-views.tsx +3 -1
- package/src/context.tsx +46 -1
- package/src/create-alfakit.tsx +13 -2
- package/src/layouts/app-layout.tsx +16 -3
- package/src/lib/discovery.tsx +23 -8
- package/src/lib/icons.tsx +90 -24
- package/src/pages/page.tsx +85 -27
- package/src/standalone/runtime.ts +2 -14
- package/src/standalone/toaster-claims.ts +16 -0
- package/src/starter-kit/widgets-loaded.tsx +84 -1
- package/src/starter-kit/widgets-triggers.tsx +4 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createDefaultRegistry } from "../registry.js";
|
|
2
2
|
import { getStandaloneDefaults } from "./configure.js";
|
|
3
|
+
import { claimToaster, hasToaster } from "./toaster-claims.js";
|
|
3
4
|
import { showToast } from "../components/notifications/flash-toasts.js";
|
|
4
5
|
import { ActionRunner, ComponentRegistry, FlashNotificationTracker, Translator, installPrefetchInvalidation, reloadOnExpiredPage, resolveRequestErrorNotification } from "@alfakit/core";
|
|
5
6
|
import { router } from "@inertiajs/react";
|
|
@@ -48,17 +49,6 @@ function createStandaloneContext(endpoints, overrides, poll) {
|
|
|
48
49
|
poll
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
|
-
let toasters = 0;
|
|
52
|
-
/** An `<AlfakitToaster>` (or the component that took the role) is mounted; returns a release. */
|
|
53
|
-
function claimToaster() {
|
|
54
|
-
toasters++;
|
|
55
|
-
return () => {
|
|
56
|
-
toasters--;
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
function hasToaster() {
|
|
60
|
-
return toasters > 0;
|
|
61
|
-
}
|
|
62
52
|
const flashTracker = new FlashNotificationTracker();
|
|
63
53
|
let flashRegistry = null;
|
|
64
54
|
let flashListening = false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","names":[],"sources":["../../src/standalone/runtime.ts"],"sourcesContent":["import {\n ActionRunner,\n ComponentRegistry,\n FlashNotificationTracker,\n Translator,\n installPrefetchInvalidation,\n reloadOnExpiredPage,\n resolveRequestErrorNotification,\n type NotificationSchema,\n} from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { showToast } from '../components/notifications/flash-toasts'\nimport type { AlfakitContextValue, AnyComponent } from '../context'\nimport { createDefaultRegistry } from '../registry'\nimport { getStandaloneDefaults } from './configure'\n\nconst isServer = typeof window === 'undefined'\n\n// Translations are the same for every component of a page, so the browser shares one\n// translator; a server render gets its own so concurrent requests never share a locale.\nconst browserTranslator = isServer ? null : new Translator()\n\nlet browserReady = false\n\n/** Browser-wide listeners every panel app installs; once per page load. */\nexport function ensureStandaloneRuntime(): void {\n if (isServer || browserReady) return\n\n browserReady = true\n reloadOnExpiredPage()\n // Pages restored by Back after a change are reloaded; shared with a surrounding panel.\n installPrefetchInvalidation()\n}\n\n/**\n * The registry of a standalone component: the built-in renderers, the `configureAlfakit()`\n * defaults (or the surrounding panel's registry), then the component's own overrides.\n */\nexport function createStandaloneRegistry(\n overrides?: Record<string, AnyComponent>,\n base?: ComponentRegistry<AnyComponent>,\n): ComponentRegistry<AnyComponent> {\n if (!base) {\n return createDefaultRegistry()\n .registerMany(getStandaloneDefaults().components ?? {})\n .registerMany(overrides ?? {})\n }\n\n if (!overrides || Object.keys(overrides).length === 0) {\n return base\n }\n\n const registry = new ComponentRegistry<AnyComponent>()\n\n for (const type of base.types()) {\n const component = base.resolve(type)\n\n if (component) registry.register(type, component)\n }\n\n return registry.registerMany(overrides)\n}\n\n/**\n * The context value of one `<AlfakitComponent>` rendered outside `createAlfakit()`: its\n * own registry and action runner (so its modals are its own), the shared translator.\n */\nexport function createStandaloneContext(\n endpoints: { action: string; modal: string },\n overrides: Record<string, AnyComponent> | undefined,\n poll: AlfakitContextValue['poll'],\n): AlfakitContextValue {\n const translator = browserTranslator ?? new Translator()\n\n return {\n registry: createStandaloneRegistry(overrides),\n pages: {},\n translator,\n actions: new ActionRunner({\n endpoints: { ...endpoints },\n // Outside a panel there is no `errorNotifications` config: a generic toast.\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 poll,\n }\n}\n\n// ---------------------------------------------------------------- Toaster ownership\n\
|
|
1
|
+
{"version":3,"file":"runtime.js","names":[],"sources":["../../src/standalone/runtime.ts"],"sourcesContent":["import {\n ActionRunner,\n ComponentRegistry,\n FlashNotificationTracker,\n Translator,\n installPrefetchInvalidation,\n reloadOnExpiredPage,\n resolveRequestErrorNotification,\n type NotificationSchema,\n} from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { showToast } from '../components/notifications/flash-toasts'\nimport type { AlfakitContextValue, AnyComponent } from '../context'\nimport { createDefaultRegistry } from '../registry'\nimport { getStandaloneDefaults } from './configure'\n\nconst isServer = typeof window === 'undefined'\n\n// Translations are the same for every component of a page, so the browser shares one\n// translator; a server render gets its own so concurrent requests never share a locale.\nconst browserTranslator = isServer ? null : new Translator()\n\nlet browserReady = false\n\n/** Browser-wide listeners every panel app installs; once per page load. */\nexport function ensureStandaloneRuntime(): void {\n if (isServer || browserReady) return\n\n browserReady = true\n reloadOnExpiredPage()\n // Pages restored by Back after a change are reloaded; shared with a surrounding panel.\n installPrefetchInvalidation()\n}\n\n/**\n * The registry of a standalone component: the built-in renderers, the `configureAlfakit()`\n * defaults (or the surrounding panel's registry), then the component's own overrides.\n */\nexport function createStandaloneRegistry(\n overrides?: Record<string, AnyComponent>,\n base?: ComponentRegistry<AnyComponent>,\n): ComponentRegistry<AnyComponent> {\n if (!base) {\n return createDefaultRegistry()\n .registerMany(getStandaloneDefaults().components ?? {})\n .registerMany(overrides ?? {})\n }\n\n if (!overrides || Object.keys(overrides).length === 0) {\n return base\n }\n\n const registry = new ComponentRegistry<AnyComponent>()\n\n for (const type of base.types()) {\n const component = base.resolve(type)\n\n if (component) registry.register(type, component)\n }\n\n return registry.registerMany(overrides)\n}\n\n/**\n * The context value of one `<AlfakitComponent>` rendered outside `createAlfakit()`: its\n * own registry and action runner (so its modals are its own), the shared translator.\n */\nexport function createStandaloneContext(\n endpoints: { action: string; modal: string },\n overrides: Record<string, AnyComponent> | undefined,\n poll: AlfakitContextValue['poll'],\n): AlfakitContextValue {\n const translator = browserTranslator ?? new Translator()\n\n return {\n registry: createStandaloneRegistry(overrides),\n pages: {},\n translator,\n actions: new ActionRunner({\n endpoints: { ...endpoints },\n // Outside a panel there is no `errorNotifications` config: a generic toast.\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 poll,\n }\n}\n\n// ---------------------------------------------------------------- Toaster ownership\n\n// In a module of its own: the starter kit header components check it without loading this one.\nexport { claimToaster, hasToaster } from './toaster-claims'\n\n// ---------------------------------------------------------------- Flashed notifications\n\nconst flashTracker = new FlashNotificationTracker()\nlet flashRegistry: ComponentRegistry<AnyComponent> | null = null\nlet flashListening = false\n\n/**\n * Show the notifications the server flashed (`Notification::make()->send()`), from the\n * `flash` Inertia event after each visit and from the initial page (whose event fired\n * before the app mounted). One listener per page load, so it survives layouts that remount\n * between visits; the registry of the last mounted toaster renders custom toast views.\n */\nexport function ensureFlashToasts(registry: ComponentRegistry<AnyComponent>): void {\n flashRegistry = registry\n\n if (isServer || flashListening) return\n\n flashListening = true\n\n const show = (flash: unknown) => {\n const flashed = (flash as { alfakitNotifications?: NotificationSchema[] } | undefined)\n ?.alfakitNotifications\n\n for (const notification of flashTracker.fresh(flashed)) {\n showToast(notification, flashRegistry?.resolve(notification.view ?? ''))\n }\n }\n\n show(initialFlash())\n router.on('flash', (event) => show(event.detail.flash))\n}\n\n/** The initial page's flash data (fired as an event before the app mounted). */\nexport function initialFlash(): unknown {\n if (isServer) return undefined\n\n try {\n const page = document.querySelector<HTMLElement>('[data-page]')?.dataset.page\n\n return page ? (JSON.parse(page) as { flash?: unknown }).flash : undefined\n } catch {\n return undefined\n }\n}\n"],"mappings":";;;;;;;AAgBA,MAAM,WAAW,OAAO,WAAW;AAInC,MAAM,oBAAoB,WAAW,OAAO,IAAI,WAAW;AAE3D,IAAI,eAAe;;AAGnB,SAAgB,0BAAgC;CAC9C,IAAI,YAAY,cAAc;CAE9B,eAAe;CACf,oBAAoB;CAEpB,4BAA4B;AAC9B;;;;;AAMA,SAAgB,yBACd,WACA,MACiC;CACjC,IAAI,CAAC,MACH,OAAO,sBAAsB,CAAC,CAC3B,aAAa,sBAAsB,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CACtD,aAAa,aAAa,CAAC,CAAC;CAGjC,IAAI,CAAC,aAAa,OAAO,KAAK,SAAS,CAAC,CAAC,WAAW,GAClD,OAAO;CAGT,MAAM,WAAW,IAAI,kBAAgC;CAErD,KAAK,MAAM,QAAQ,KAAK,MAAM,GAAG;EAC/B,MAAM,YAAY,KAAK,QAAQ,IAAI;EAEnC,IAAI,WAAW,SAAS,SAAS,MAAM,SAAS;CAClD;CAEA,OAAO,SAAS,aAAa,SAAS;AACxC;;;;;AAMA,SAAgB,wBACd,WACA,WACA,MACqB;CACrB,MAAM,aAAa,qBAAqB,IAAI,WAAW;CAEvD,OAAO;EACL,UAAU,yBAAyB,SAAS;EAC5C,OAAO,CAAC;EACR;EACA,SAAS,IAAI,aAAa;GACxB,WAAW,EAAE,GAAG,UAAU;GAE1B,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;EACD;CACF;AACF;AASA,MAAM,eAAe,IAAI,yBAAyB;AAClD,IAAI,gBAAwD;AAC5D,IAAI,iBAAiB;;;;;;;AAQrB,SAAgB,kBAAkB,UAAiD;CACjF,gBAAgB;CAEhB,IAAI,YAAY,gBAAgB;CAEhC,iBAAiB;CAEjB,MAAM,QAAQ,UAAmB;EAC/B,MAAM,UAAW,OACb;EAEJ,KAAK,MAAM,gBAAgB,aAAa,MAAM,OAAO,GACnD,UAAU,cAAc,eAAe,QAAQ,aAAa,QAAQ,EAAE,CAAC;CAE3E;CAEA,KAAK,aAAa,CAAC;CACnB,OAAO,GAAG,UAAU,UAAU,KAAK,MAAM,OAAO,KAAK,CAAC;AACxD;;AAGA,SAAgB,eAAwB;CACtC,IAAI,UAAU,OAAO,KAAA;CAErB,IAAI;EACF,MAAM,OAAO,SAAS,cAA2B,aAAa,CAAC,EAAE,QAAQ;EAEzE,OAAO,OAAQ,KAAK,MAAM,IAAI,CAAC,CAAyB,QAAQ,KAAA;CAClE,QAAQ;EACN;CACF;AACF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/standalone/toaster-claims.ts
|
|
2
|
+
let toasters = 0;
|
|
3
|
+
/** An `<AlfakitToaster>` (or the component that took the role) is mounted; returns a release. */
|
|
4
|
+
function claimToaster() {
|
|
5
|
+
toasters++;
|
|
6
|
+
return () => {
|
|
7
|
+
toasters--;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function hasToaster() {
|
|
11
|
+
return toasters > 0;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { claimToaster, hasToaster };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=toaster-claims.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toaster-claims.js","names":[],"sources":["../../src/standalone/toaster-claims.ts"],"sourcesContent":["// Who renders AlfaKit's toaster on the page: `<AlfakitToaster>`, `AlfakitRoot`, the first\n// standalone component or the starter kit header components on the host's own pages.\nlet toasters = 0\n\n/** An `<AlfakitToaster>` (or the component that took the role) is mounted; returns a release. */\nexport function claimToaster(): () => void {\n toasters++\n\n return () => {\n toasters--\n }\n}\n\nexport function hasToaster(): boolean {\n return toasters > 0\n}\n"],"mappings":";AAEA,IAAI,WAAW;;AAGf,SAAgB,eAA2B;CACzC;CAEA,aAAa;EACX;CACF;AACF;AAEA,SAAgB,aAAsB;CACpC,OAAO,WAAW;AACpB"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { AlfakitProvider, StandaloneProvider, useLocalization, useT } from "../context.js";
|
|
1
|
+
import { AlfakitProvider, StandaloneProvider, useLocalization, useRegistry, useT } from "../context.js";
|
|
2
2
|
import { Icon } from "../lib/icons.js";
|
|
3
3
|
import { DropdownMenu as DropdownMenu$1, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../components/ui/dropdown-menu.js";
|
|
4
4
|
import { TooltipProvider } from "../components/ui/tooltip.js";
|
|
5
5
|
import { AiCopilot } from "../components/ai/copilot.js";
|
|
6
|
+
import { claimToaster, hasToaster } from "../standalone/toaster-claims.js";
|
|
7
|
+
import { ensureFlashToasts } from "../standalone/runtime.js";
|
|
8
|
+
import { setToasterId } from "../components/notifications/flash-toasts.js";
|
|
6
9
|
import { TenantName } from "../components/panel/nav-label.js";
|
|
7
10
|
import { Avatar as Avatar$1, AvatarFallback, AvatarImage } from "../components/ui/avatar.js";
|
|
8
11
|
import { NotificationsMenu } from "../components/notifications/notifications-menu.js";
|
|
@@ -10,13 +13,15 @@ import { RealtimeNotifications } from "../components/notifications/realtime-noti
|
|
|
10
13
|
import { GlobalSearch } from "../components/panel/global-search.js";
|
|
11
14
|
import { TenantMenu } from "../components/panel/tenant-menu.js";
|
|
12
15
|
import { LocaleOptions } from "../components/panel/locale-switcher.js";
|
|
16
|
+
import { Toaster } from "../components/ui/sonner.js";
|
|
13
17
|
import { CopilotTrigger, LanguageSwitcherTrigger, NotificationsTrigger, SearchTrigger, TenantSwitcherTrigger, languageTriggerClasses } from "./widgets-triggers.js";
|
|
14
|
-
import { ActionRunner, ComponentRegistry, Translator, initials, isAlfakitPage, loadStarterKitPanel, loadedStarterKitPanel, openRequestedStarterKitSearch, showsLanguageSwitcher, starterKitCopilot, starterKitPanelLocale, starterKitWidgetsPage, starterKitWidgetsPanel, takeRequestedStarterKitCopilot, takeRequestedStarterKitLanguages, tenantNameText } from "@alfakit/core";
|
|
18
|
+
import { ActionRunner, ComponentRegistry, STARTER_KIT_TOASTER_ID, Translator, initials, isAlfakitPage, loadStarterKitPanel, loadedStarterKitPanel, openRequestedStarterKitSearch, overlayPlacementDirection, resolveToastPosition, showsLanguageSwitcher, starterKitCopilot, starterKitPanelLocale, starterKitWidgetsPage, starterKitWidgetsPanel, takeRequestedStarterKitCopilot, takeRequestedStarterKitLanguages, tenantNameText } from "@alfakit/core";
|
|
15
19
|
import { router, usePage } from "@inertiajs/react";
|
|
16
20
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
17
21
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
18
22
|
import { Direction } from "radix-ui";
|
|
19
23
|
import { ChevronsUpDownIcon, LanguagesIcon, PlusIcon } from "lucide-react";
|
|
24
|
+
import { createPortal } from "react-dom";
|
|
20
25
|
//#region src/starter-kit/widgets-loaded.tsx
|
|
21
26
|
/**
|
|
22
27
|
* The loaded header components of starter kit mode: the panel's own search palette,
|
|
@@ -66,23 +71,75 @@ function useWidgetsPanel(source) {
|
|
|
66
71
|
widgets
|
|
67
72
|
]);
|
|
68
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* AlfaKit's toaster on a host page (as `<AlfakitToaster>`, without its standalone context):
|
|
76
|
+
* Alfakit's toasts go to it, and so do flashed notifications.
|
|
77
|
+
*/
|
|
78
|
+
function HostToaster({ position, dir }) {
|
|
79
|
+
const registry = useRegistry();
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
ensureFlashToasts(registry);
|
|
82
|
+
}, [registry]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
setToasterId(STARTER_KIT_TOASTER_ID);
|
|
85
|
+
return () => setToasterId(void 0);
|
|
86
|
+
}, []);
|
|
87
|
+
return /* @__PURE__ */ jsx(Toaster, {
|
|
88
|
+
id: STARTER_KIT_TOASTER_ID,
|
|
89
|
+
position: resolveToastPosition(position, dir),
|
|
90
|
+
dir,
|
|
91
|
+
richColors: true,
|
|
92
|
+
closeButton: true
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* On the host's own pages no AlfaKit toaster is mounted, so the first loaded header
|
|
97
|
+
* component renders one: the push toggle's and the PWA's messages show there too (W-S1-03).
|
|
98
|
+
* AlfaKit pages (`AlfakitRoot`) and standalone components claim it themselves; the check
|
|
99
|
+
* runs after every component's effects, and again after each visit.
|
|
100
|
+
*/
|
|
101
|
+
function useOwnsHostToaster() {
|
|
102
|
+
const alfakitPage = isAlfakitPage(usePage().props);
|
|
103
|
+
const [owns, setOwns] = useState(false);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (alfakitPage) return void 0;
|
|
106
|
+
let release = null;
|
|
107
|
+
let cancelled = false;
|
|
108
|
+
queueMicrotask(() => {
|
|
109
|
+
if (cancelled || hasToaster()) return;
|
|
110
|
+
release = claimToaster();
|
|
111
|
+
setOwns(true);
|
|
112
|
+
});
|
|
113
|
+
return () => {
|
|
114
|
+
cancelled = true;
|
|
115
|
+
release?.();
|
|
116
|
+
setOwns(false);
|
|
117
|
+
};
|
|
118
|
+
}, [alfakitPage]);
|
|
119
|
+
return owns && !alfakitPage;
|
|
120
|
+
}
|
|
69
121
|
function WidgetShell({ panel, children }) {
|
|
70
122
|
const context = useWidgetsContext(panel);
|
|
71
123
|
const page = useMemo(() => starterKitWidgetsPage(), []);
|
|
124
|
+
const ownsToaster = useOwnsHostToaster();
|
|
125
|
+
const direction = panel.localization?.direction ?? "ltr";
|
|
72
126
|
return /* @__PURE__ */ jsx(AlfakitProvider, {
|
|
73
127
|
value: context,
|
|
74
|
-
children: /* @__PURE__ */
|
|
128
|
+
children: /* @__PURE__ */ jsxs(StandaloneProvider, {
|
|
75
129
|
value: {
|
|
76
130
|
panel,
|
|
77
131
|
page
|
|
78
132
|
},
|
|
79
|
-
children: /* @__PURE__ */ jsx(Direction.Provider, {
|
|
80
|
-
dir:
|
|
133
|
+
children: [/* @__PURE__ */ jsx(Direction.Provider, {
|
|
134
|
+
dir: direction,
|
|
81
135
|
children: /* @__PURE__ */ jsx(TooltipProvider, {
|
|
82
136
|
delayDuration: 0,
|
|
83
137
|
children
|
|
84
138
|
})
|
|
85
|
-
})
|
|
139
|
+
}), ownsToaster && createPortal(/* @__PURE__ */ jsx(HostToaster, {
|
|
140
|
+
position: panel.toastPosition,
|
|
141
|
+
dir: overlayPlacementDirection(direction, true)
|
|
142
|
+
}), document.body)]
|
|
86
143
|
})
|
|
87
144
|
});
|
|
88
145
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"widgets-loaded.js","names":["DropdownMenu","Avatar"],"sources":["../../src/starter-kit/widgets-loaded.tsx"],"sourcesContent":["import {\n ActionRunner,\n ComponentRegistry,\n Translator,\n initials,\n isAlfakitPage,\n loadStarterKitPanel,\n loadedStarterKitPanel,\n openRequestedStarterKitSearch,\n showsLanguageSwitcher,\n starterKitCopilot,\n starterKitPanelLocale,\n starterKitWidgetsPage,\n starterKitWidgetsPanel,\n takeRequestedStarterKitCopilot,\n takeRequestedStarterKitLanguages,\n tenantNameText,\n type PanelSchema,\n type StarterKitWidgets,\n} from '@alfakit/core'\nimport { router, usePage } from '@inertiajs/react'\nimport { ChevronsUpDownIcon, LanguagesIcon, PlusIcon } from 'lucide-react'\nimport { Direction } from 'radix-ui'\nimport { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'\nimport { AiCopilot } from '../components/ai/copilot'\nimport { NotificationsMenu } from '../components/notifications/notifications-menu'\nimport { RealtimeNotifications } from '../components/notifications/realtime-notifications'\nimport { GlobalSearch } from '../components/panel/global-search'\nimport { LocaleOptions } from '../components/panel/locale-switcher'\nimport { TenantName } from '../components/panel/nav-label'\nimport { TenantMenu } from '../components/panel/tenant-menu'\nimport { Avatar, AvatarFallback, AvatarImage } from '../components/ui/avatar'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../components/ui/dropdown-menu'\nimport { TooltipProvider } from '../components/ui/tooltip'\nimport {\n AlfakitProvider,\n StandaloneProvider,\n useLocalization,\n useT,\n type AlfakitContextValue,\n type AnyComponent,\n} from '../context'\nimport { Icon } from '../lib/icons'\nimport {\n CopilotTrigger,\n LanguageSwitcherTrigger,\n NotificationsTrigger,\n SearchTrigger,\n TenantSwitcherTrigger,\n languageTriggerClasses,\n type WidgetProps,\n} from './widgets-triggers'\n\n/**\n * The loaded header components of starter kit mode: the panel's own search palette,\n * notifications menu, tenant menu and language menu, rendered outside Alfakit pages with\n * the panel data from `widgets.endpoint`. This module only loads in the browser.\n */\n\n// One context for the browser session: its own translator (the panel's strings), an empty\n// registry (only `panel:icon` overrides are looked up here) and an idle action runner.\nlet widgetsContext: AlfakitContextValue | null = null\n\nfunction useWidgetsContext(panel: PanelSchema): AlfakitContextValue {\n widgetsContext ??= {\n registry: new ComponentRegistry<AnyComponent>(),\n pages: {},\n translator: new Translator(),\n actions: new ActionRunner({ endpoints: { action: '', modal: '' } }),\n }\n\n widgetsContext.translator.setTranslations(panel.translations ?? {})\n widgetsContext.translator.setLocale(panel.localization?.tag ?? 'en')\n\n return widgetsContext\n}\n\n/**\n * The panel the components render with, once its data has loaded (null until then). It is\n * loaded per endpoint (a tenant switch) and language (a language switch), so the components\n * follow a switch instead of keeping the old language's strings.\n */\nfunction useWidgetsPanel(source: StarterKitWidgets): PanelSchema | null {\n const endpoint = source.widgets.endpoint\n const locale = starterKitPanelLocale(source)\n const [loaded, setLoaded] = useState(() => loadedStarterKitPanel(endpoint, locale))\n // Until the new language's data arrives, not the old language's.\n const current =\n loaded && (locale === undefined || loaded.localization?.code === locale) ? loaded : null\n const base = loadedStarterKitPanel(endpoint, locale) ?? current\n\n useEffect(() => {\n let active = true\n\n void loadStarterKitPanel(endpoint, locale).then(\n (panel) => active && setLoaded(panel),\n () => {},\n )\n\n return () => {\n active = false\n }\n }, [endpoint, locale])\n\n const { navigation, widgets } = source\n\n return useMemo(\n () => (base ? starterKitWidgetsPanel(base, { navigation, widgets }) : null),\n [base, navigation, widgets],\n )\n}\n\nfunction WidgetShell({ panel, children }: { panel: PanelSchema; children: ReactNode }) {\n const context = useWidgetsContext(panel)\n const page = useMemo(() => starterKitWidgetsPage(), [])\n\n return (\n <AlfakitProvider value={context}>\n <StandaloneProvider value={{ panel, page }}>\n <Direction.Provider dir={panel.localization?.direction ?? 'ltr'}>\n <TooltipProvider delayDuration={0}>{children}</TooltipProvider>\n </Direction.Provider>\n </StandaloneProvider>\n </AlfakitProvider>\n )\n}\n\n/** The panel's command palette and global search. */\nexport function LoadedSearch({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n\n // A click or the key bindings before the palette loaded open it now (after its own\n // effects registered the shortcut).\n useEffect(() => {\n if (panel) openRequestedStarterKitSearch()\n }, [panel])\n\n if (!panel) return <SearchTrigger source={source} />\n\n return (\n <WidgetShell panel={panel}>\n <GlobalSearch />\n </WidgetShell>\n )\n}\n\n/** The notifications bell, with realtime notifications on the host's own pages. */\nexport function LoadedNotifications({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n // Alfakit pages subscribe to realtime notifications themselves.\n const alfakitPage = isAlfakitPage(usePage().props)\n\n if (!panel) return <NotificationsTrigger source={source} />\n\n return (\n <WidgetShell panel={panel}>\n <NotificationsMenu />\n {!alfakitPage && <RealtimeNotifications />}\n </WidgetShell>\n )\n}\n\n/** The panel's tenant menu inside a tenant, a menu to choose one elsewhere. */\nexport function LoadedTenantSwitcher({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n\n if (!panel) return <TenantSwitcherTrigger source={source} />\n\n return (\n <WidgetShell panel={panel}>\n {panel.tenancy?.current ? <TenantMenu variant=\"topbar\" /> : <TenantChooser panel={panel} />}\n </WidgetShell>\n )\n}\n\n/** The AI copilot (the panel pages' `topbar.end` slot component). */\nexport function LoadedCopilot({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n // A click or the key bindings before the copilot loaded open it at once.\n const [autoOpen] = useState(() => takeRequestedStarterKitCopilot())\n const component = useMemo(() => (panel ? starterKitCopilot(panel) : null), [panel])\n\n if (!panel) return <CopilotTrigger source={source} />\n\n if (!component) return null\n\n return (\n <WidgetShell panel={panel}>\n <AiCopilot component={component} autoOpen={autoOpen || takeRequestedStarterKitCopilot()} />\n </WidgetShell>\n )\n}\n\n/**\n * The panel's language menu (its languages in their own names, `LocaleOptions`): picking\n * one posts to the panel's locale endpoint and reloads the page in it. Nothing without a\n * language switcher with two or more languages.\n */\nexport function LoadedLanguageSwitcher({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n\n if (!panel) return <LanguageSwitcherTrigger source={source} />\n\n if (!showsLanguageSwitcher(panel.localization, 'auth')) return null\n\n return (\n <WidgetShell panel={panel}>\n <LanguageMenu />\n </WidgetShell>\n )\n}\n\nfunction LanguageMenu() {\n const t = useT()\n const localization = useLocalization()\n const [open, setOpen] = useState(false)\n // Taken once per mount (effects run twice in StrictMode).\n const requested = useRef<boolean | null>(null)\n\n // A click on the trigger before the menu loaded opens it once mounted: a frame later, as\n // the menu closes again when it opens while focus is still on the replaced button.\n useEffect(() => {\n requested.current ??= takeRequestedStarterKitLanguages()\n\n if (!requested.current) return\n\n const frame = requestAnimationFrame(() => setOpen(true))\n\n return () => cancelAnimationFrame(frame)\n }, [])\n const label = t('change_language', {}, 'Change language')\n\n return (\n <DropdownMenu open={open} onOpenChange={setOpen}>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n data-slot=\"language-switcher-trigger\"\n aria-label={label}\n title={label}\n className={languageTriggerClasses}\n >\n <LanguagesIcon className=\"size-4\" aria-hidden=\"true\" />\n <span lang={localization.tag} dir={localization.direction} className=\"hidden sm:inline\">\n {localization.label}\n </span>\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"min-w-40\">\n <LocaleOptions />\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/** The user's tenants on a page outside the panel (no current tenant). */\nfunction TenantChooser({ panel }: { panel: PanelSchema }) {\n const t = useT()\n const tenancy = panel.tenancy\n\n if (!tenancy || (tenancy.tenants.length === 0 && !tenancy.registrationUrl)) return null\n\n const label = t('starter_kit.choose_tenant', {}, 'Choose a tenant')\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n data-slot=\"tenant-menu\"\n className=\"hover:bg-accent data-[state=open]:bg-accent inline-flex h-9 max-w-60 shrink-0 items-center gap-2 rounded-md px-2 text-sm font-medium\"\n >\n <span className=\"sr-only lg:not-sr-only lg:truncate\">{label}</span>\n <Icon\n name=\"alfakit::panels.tenant-menu.toggle-button\"\n fallback={ChevronsUpDownIcon}\n className=\"text-muted-foreground size-4 shrink-0\"\n />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"min-w-56 rounded-lg\" side=\"bottom\" align=\"end\" sideOffset={4}>\n {tenancy.tenants.length > 0 && (\n <DropdownMenuLabel className=\"text-muted-foreground text-xs\">{label}</DropdownMenuLabel>\n )}\n {tenancy.tenants.map((tenant) => (\n <DropdownMenuItem key={String(tenant.id)} onSelect={() => router.visit(tenant.url)}>\n <Avatar className=\"size-6 rounded-sm\">\n {tenant.avatar && <AvatarImage src={tenant.avatar} alt=\"\" />}\n <AvatarFallback className=\"rounded-sm text-[10px]\">\n {initials(tenantNameText(tenant))}\n </AvatarFallback>\n </Avatar>\n <TenantName tenant={tenant} className=\"flex-1 truncate\" />\n </DropdownMenuItem>\n ))}\n {tenancy.registrationUrl && (\n <>\n {tenancy.tenants.length > 0 && <DropdownMenuSeparator />}\n <DropdownMenuItem onSelect={() => router.visit(tenancy.registrationUrl!)}>\n <Icon name=\"alfakit::panels.tenant-menu.registration-button\" fallback={PlusIcon} />\n {tenancy.registrationLabel ?? t('tenancy.register', {}, 'New')}\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"widgets-loaded.js","names":["DropdownMenu","Avatar"],"sources":["../../src/starter-kit/widgets-loaded.tsx"],"sourcesContent":["import {\n ActionRunner,\n ComponentRegistry,\n STARTER_KIT_TOASTER_ID,\n Translator,\n initials,\n isAlfakitPage,\n loadStarterKitPanel,\n loadedStarterKitPanel,\n openRequestedStarterKitSearch,\n overlayPlacementDirection,\n resolveToastPosition,\n showsLanguageSwitcher,\n starterKitCopilot,\n starterKitPanelLocale,\n starterKitWidgetsPage,\n starterKitWidgetsPanel,\n takeRequestedStarterKitCopilot,\n takeRequestedStarterKitLanguages,\n tenantNameText,\n type PanelSchema,\n type StarterKitWidgets,\n type ToastPosition,\n} from '@alfakit/core'\nimport { router, usePage } from '@inertiajs/react'\nimport { ChevronsUpDownIcon, LanguagesIcon, PlusIcon } from 'lucide-react'\nimport { Direction } from 'radix-ui'\nimport { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'\nimport { createPortal } from 'react-dom'\nimport { AiCopilot } from '../components/ai/copilot'\nimport { setToasterId } from '../components/notifications/flash-toasts'\nimport { NotificationsMenu } from '../components/notifications/notifications-menu'\nimport { RealtimeNotifications } from '../components/notifications/realtime-notifications'\nimport { GlobalSearch } from '../components/panel/global-search'\nimport { LocaleOptions } from '../components/panel/locale-switcher'\nimport { TenantName } from '../components/panel/nav-label'\nimport { TenantMenu } from '../components/panel/tenant-menu'\nimport { Avatar, AvatarFallback, AvatarImage } from '../components/ui/avatar'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../components/ui/dropdown-menu'\nimport { Toaster } from '../components/ui/sonner'\nimport { TooltipProvider } from '../components/ui/tooltip'\nimport {\n AlfakitProvider,\n StandaloneProvider,\n useLocalization,\n useRegistry,\n useT,\n type AlfakitContextValue,\n type AnyComponent,\n} from '../context'\nimport { Icon } from '../lib/icons'\nimport { ensureFlashToasts } from '../standalone/runtime'\nimport { claimToaster, hasToaster } from '../standalone/toaster-claims'\nimport {\n CopilotTrigger,\n LanguageSwitcherTrigger,\n NotificationsTrigger,\n SearchTrigger,\n TenantSwitcherTrigger,\n languageTriggerClasses,\n type WidgetProps,\n} from './widgets-triggers'\n\n/**\n * The loaded header components of starter kit mode: the panel's own search palette,\n * notifications menu, tenant menu and language menu, rendered outside Alfakit pages with\n * the panel data from `widgets.endpoint`. This module only loads in the browser.\n */\n\n// One context for the browser session: its own translator (the panel's strings), an empty\n// registry (only `panel:icon` overrides are looked up here) and an idle action runner.\nlet widgetsContext: AlfakitContextValue | null = null\n\nfunction useWidgetsContext(panel: PanelSchema): AlfakitContextValue {\n widgetsContext ??= {\n registry: new ComponentRegistry<AnyComponent>(),\n pages: {},\n translator: new Translator(),\n actions: new ActionRunner({ endpoints: { action: '', modal: '' } }),\n }\n\n widgetsContext.translator.setTranslations(panel.translations ?? {})\n widgetsContext.translator.setLocale(panel.localization?.tag ?? 'en')\n\n return widgetsContext\n}\n\n/**\n * The panel the components render with, once its data has loaded (null until then). It is\n * loaded per endpoint (a tenant switch) and language (a language switch), so the components\n * follow a switch instead of keeping the old language's strings.\n */\nfunction useWidgetsPanel(source: StarterKitWidgets): PanelSchema | null {\n const endpoint = source.widgets.endpoint\n const locale = starterKitPanelLocale(source)\n const [loaded, setLoaded] = useState(() => loadedStarterKitPanel(endpoint, locale))\n // Until the new language's data arrives, not the old language's.\n const current =\n loaded && (locale === undefined || loaded.localization?.code === locale) ? loaded : null\n const base = loadedStarterKitPanel(endpoint, locale) ?? current\n\n useEffect(() => {\n let active = true\n\n void loadStarterKitPanel(endpoint, locale).then(\n (panel) => active && setLoaded(panel),\n () => {},\n )\n\n return () => {\n active = false\n }\n }, [endpoint, locale])\n\n const { navigation, widgets } = source\n\n return useMemo(\n () => (base ? starterKitWidgetsPanel(base, { navigation, widgets }) : null),\n [base, navigation, widgets],\n )\n}\n\n/**\n * AlfaKit's toaster on a host page (as `<AlfakitToaster>`, without its standalone context):\n * Alfakit's toasts go to it, and so do flashed notifications.\n */\nfunction HostToaster({ position, dir }: { position?: ToastPosition; dir: 'ltr' | 'rtl' }) {\n const registry = useRegistry()\n\n useEffect(() => {\n ensureFlashToasts(registry)\n }, [registry])\n\n useEffect(() => {\n setToasterId(STARTER_KIT_TOASTER_ID)\n\n return () => setToasterId(undefined)\n }, [])\n\n return (\n <Toaster\n id={STARTER_KIT_TOASTER_ID}\n position={resolveToastPosition(position, dir)}\n dir={dir}\n richColors\n closeButton\n />\n )\n}\n\n/**\n * On the host's own pages no AlfaKit toaster is mounted, so the first loaded header\n * component renders one: the push toggle's and the PWA's messages show there too (W-S1-03).\n * AlfaKit pages (`AlfakitRoot`) and standalone components claim it themselves; the check\n * runs after every component's effects, and again after each visit.\n */\nfunction useOwnsHostToaster(): boolean {\n const alfakitPage = isAlfakitPage(usePage().props)\n const [owns, setOwns] = useState(false)\n\n useEffect(() => {\n if (alfakitPage) return undefined\n\n let release: (() => void) | null = null\n let cancelled = false\n\n queueMicrotask(() => {\n if (cancelled || hasToaster()) return\n\n release = claimToaster()\n setOwns(true)\n })\n\n return () => {\n cancelled = true\n release?.()\n setOwns(false)\n }\n }, [alfakitPage])\n\n return owns && !alfakitPage\n}\n\nfunction WidgetShell({ panel, children }: { panel: PanelSchema; children: ReactNode }) {\n const context = useWidgetsContext(panel)\n const page = useMemo(() => starterKitWidgetsPage(), [])\n const ownsToaster = useOwnsHostToaster()\n const direction = panel.localization?.direction ?? 'ltr'\n\n return (\n <AlfakitProvider value={context}>\n <StandaloneProvider value={{ panel, page }}>\n <Direction.Provider dir={direction}>\n <TooltipProvider delayDuration={0}>{children}</TooltipProvider>\n </Direction.Provider>\n {/* In the body, not in the header (a header with a backdrop filter would hold its\n fixed position), placed in the document's direction as on AlfaKit pages. */}\n {ownsToaster &&\n createPortal(\n <HostToaster\n position={panel.toastPosition}\n dir={overlayPlacementDirection(direction, true)}\n />,\n document.body,\n )}\n </StandaloneProvider>\n </AlfakitProvider>\n )\n}\n\n/** The panel's command palette and global search. */\nexport function LoadedSearch({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n\n // A click or the key bindings before the palette loaded open it now (after its own\n // effects registered the shortcut).\n useEffect(() => {\n if (panel) openRequestedStarterKitSearch()\n }, [panel])\n\n if (!panel) return <SearchTrigger source={source} />\n\n return (\n <WidgetShell panel={panel}>\n <GlobalSearch />\n </WidgetShell>\n )\n}\n\n/** The notifications bell, with realtime notifications on the host's own pages. */\nexport function LoadedNotifications({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n // Alfakit pages subscribe to realtime notifications themselves.\n const alfakitPage = isAlfakitPage(usePage().props)\n\n if (!panel) return <NotificationsTrigger source={source} />\n\n return (\n <WidgetShell panel={panel}>\n <NotificationsMenu />\n {!alfakitPage && <RealtimeNotifications />}\n </WidgetShell>\n )\n}\n\n/** The panel's tenant menu inside a tenant, a menu to choose one elsewhere. */\nexport function LoadedTenantSwitcher({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n\n if (!panel) return <TenantSwitcherTrigger source={source} />\n\n return (\n <WidgetShell panel={panel}>\n {panel.tenancy?.current ? <TenantMenu variant=\"topbar\" /> : <TenantChooser panel={panel} />}\n </WidgetShell>\n )\n}\n\n/** The AI copilot (the panel pages' `topbar.end` slot component). */\nexport function LoadedCopilot({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n // A click or the key bindings before the copilot loaded open it at once.\n const [autoOpen] = useState(() => takeRequestedStarterKitCopilot())\n const component = useMemo(() => (panel ? starterKitCopilot(panel) : null), [panel])\n\n if (!panel) return <CopilotTrigger source={source} />\n\n if (!component) return null\n\n return (\n <WidgetShell panel={panel}>\n <AiCopilot component={component} autoOpen={autoOpen || takeRequestedStarterKitCopilot()} />\n </WidgetShell>\n )\n}\n\n/**\n * The panel's language menu (its languages in their own names, `LocaleOptions`): picking\n * one posts to the panel's locale endpoint and reloads the page in it. Nothing without a\n * language switcher with two or more languages.\n */\nexport function LoadedLanguageSwitcher({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n\n if (!panel) return <LanguageSwitcherTrigger source={source} />\n\n if (!showsLanguageSwitcher(panel.localization, 'auth')) return null\n\n return (\n <WidgetShell panel={panel}>\n <LanguageMenu />\n </WidgetShell>\n )\n}\n\nfunction LanguageMenu() {\n const t = useT()\n const localization = useLocalization()\n const [open, setOpen] = useState(false)\n // Taken once per mount (effects run twice in StrictMode).\n const requested = useRef<boolean | null>(null)\n\n // A click on the trigger before the menu loaded opens it once mounted: a frame later, as\n // the menu closes again when it opens while focus is still on the replaced button.\n useEffect(() => {\n requested.current ??= takeRequestedStarterKitLanguages()\n\n if (!requested.current) return\n\n const frame = requestAnimationFrame(() => setOpen(true))\n\n return () => cancelAnimationFrame(frame)\n }, [])\n const label = t('change_language', {}, 'Change language')\n\n return (\n <DropdownMenu open={open} onOpenChange={setOpen}>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n data-slot=\"language-switcher-trigger\"\n aria-label={label}\n title={label}\n className={languageTriggerClasses}\n >\n <LanguagesIcon className=\"size-4\" aria-hidden=\"true\" />\n <span lang={localization.tag} dir={localization.direction} className=\"hidden sm:inline\">\n {localization.label}\n </span>\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"min-w-40\">\n <LocaleOptions />\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/** The user's tenants on a page outside the panel (no current tenant). */\nfunction TenantChooser({ panel }: { panel: PanelSchema }) {\n const t = useT()\n const tenancy = panel.tenancy\n\n if (!tenancy || (tenancy.tenants.length === 0 && !tenancy.registrationUrl)) return null\n\n const label = t('starter_kit.choose_tenant', {}, 'Choose a tenant')\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n data-slot=\"tenant-menu\"\n className=\"hover:bg-accent data-[state=open]:bg-accent inline-flex h-9 max-w-60 shrink-0 items-center gap-2 rounded-md px-2 text-sm font-medium\"\n >\n <span className=\"sr-only lg:not-sr-only lg:truncate\">{label}</span>\n <Icon\n name=\"alfakit::panels.tenant-menu.toggle-button\"\n fallback={ChevronsUpDownIcon}\n className=\"text-muted-foreground size-4 shrink-0\"\n />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"min-w-56 rounded-lg\" side=\"bottom\" align=\"end\" sideOffset={4}>\n {tenancy.tenants.length > 0 && (\n <DropdownMenuLabel className=\"text-muted-foreground text-xs\">{label}</DropdownMenuLabel>\n )}\n {tenancy.tenants.map((tenant) => (\n <DropdownMenuItem key={String(tenant.id)} onSelect={() => router.visit(tenant.url)}>\n <Avatar className=\"size-6 rounded-sm\">\n {tenant.avatar && <AvatarImage src={tenant.avatar} alt=\"\" />}\n <AvatarFallback className=\"rounded-sm text-[10px]\">\n {initials(tenantNameText(tenant))}\n </AvatarFallback>\n </Avatar>\n <TenantName tenant={tenant} className=\"flex-1 truncate\" />\n </DropdownMenuItem>\n ))}\n {tenancy.registrationUrl && (\n <>\n {tenancy.tenants.length > 0 && <DropdownMenuSeparator />}\n <DropdownMenuItem onSelect={() => router.visit(tenancy.registrationUrl!)}>\n <Icon name=\"alfakit::panels.tenant-menu.registration-button\" fallback={PlusIcon} />\n {tenancy.registrationLabel ?? t('tenancy.register', {}, 'New')}\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"],"mappings":"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"}
|
|
@@ -36,7 +36,7 @@ function SearchTrigger({ source, onLoad }) {
|
|
|
36
36
|
}),
|
|
37
37
|
hydrated && binding && /* @__PURE__ */ jsx("kbd", {
|
|
38
38
|
dir: "ltr",
|
|
39
|
-
className: "bg-muted hidden rounded px-1 font-mono text-[10px] sm:inline",
|
|
39
|
+
className: "bg-muted text-foreground/70 hidden rounded px-1 font-mono text-[10px] sm:inline",
|
|
40
40
|
children: formatKeyBinding(binding)
|
|
41
41
|
})
|
|
42
42
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"widgets-triggers.js","names":[],"sources":["../../src/starter-kit/widgets-triggers.tsx"],"sourcesContent":["import {\n formatKeyBinding,\n initials,\n registerStarterKitCopilotShortcut,\n registerStarterKitSearchShortcut,\n requestStarterKitCopilot,\n requestStarterKitLanguages,\n requestStarterKitSearch,\n tenantNameText,\n type StarterKitWidgets,\n} from '@alfakit/core'\nimport { BellIcon, ChevronsUpDownIcon, LanguagesIcon, SearchIcon, SparklesIcon } from 'lucide-react'\nimport { useEffect } from 'react'\nimport { useHydrated } from '../hooks/use-hydrated'\n\n/**\n * What the starter kit header components show until their code and the panel data have\n * loaded: the same look as the loaded triggers, from the shared prop alone.\n */\nexport interface WidgetProps {\n source: StarterKitWidgets\n}\n\nconst triggerClasses =\n 'border-input bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 inline-flex h-8 shrink-0 items-center rounded-md border text-sm shadow-xs outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50'\n\n/** The search field until the palette has loaded: the same look, and it remembers a click. */\nexport function SearchTrigger({ source, onLoad }: WidgetProps & { onLoad?: () => void }) {\n const hydrated = useHydrated()\n const search = source.widgets.search\n const keys = JSON.stringify(search?.keyBindings ?? ['mod+k'])\n const placeholder = search?.placeholder ?? ''\n // `onLoad` is a stable function (it starts loading the palette's code).\n useEffect(\n () =>\n registerStarterKitSearchShortcut(JSON.parse(keys) as string[], placeholder, () => onLoad?.()),\n [keys, placeholder, onLoad],\n )\n\n const binding = search?.keyBindings[0]\n\n return (\n <button\n type=\"button\"\n className={`${triggerClasses} text-muted-foreground size-8 justify-center gap-2 px-0 font-normal sm:w-56 sm:justify-start sm:px-2.5`}\n onPointerEnter={() => onLoad?.()}\n onClick={() => {\n requestStarterKitSearch()\n onLoad?.()\n }}\n >\n <SearchIcon className=\"size-4\" aria-hidden=\"true\" />\n <span className=\"sr-only sm:not-sr-only sm:flex-1 sm:text-start\">{placeholder}</span>\n {hydrated && binding && (\n <kbd
|
|
1
|
+
{"version":3,"file":"widgets-triggers.js","names":[],"sources":["../../src/starter-kit/widgets-triggers.tsx"],"sourcesContent":["import {\n formatKeyBinding,\n initials,\n registerStarterKitCopilotShortcut,\n registerStarterKitSearchShortcut,\n requestStarterKitCopilot,\n requestStarterKitLanguages,\n requestStarterKitSearch,\n tenantNameText,\n type StarterKitWidgets,\n} from '@alfakit/core'\nimport { BellIcon, ChevronsUpDownIcon, LanguagesIcon, SearchIcon, SparklesIcon } from 'lucide-react'\nimport { useEffect } from 'react'\nimport { useHydrated } from '../hooks/use-hydrated'\n\n/**\n * What the starter kit header components show until their code and the panel data have\n * loaded: the same look as the loaded triggers, from the shared prop alone.\n */\nexport interface WidgetProps {\n source: StarterKitWidgets\n}\n\nconst triggerClasses =\n 'border-input bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 inline-flex h-8 shrink-0 items-center rounded-md border text-sm shadow-xs outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50'\n\n/** The search field until the palette has loaded: the same look, and it remembers a click. */\nexport function SearchTrigger({ source, onLoad }: WidgetProps & { onLoad?: () => void }) {\n const hydrated = useHydrated()\n const search = source.widgets.search\n const keys = JSON.stringify(search?.keyBindings ?? ['mod+k'])\n const placeholder = search?.placeholder ?? ''\n // `onLoad` is a stable function (it starts loading the palette's code).\n useEffect(\n () =>\n registerStarterKitSearchShortcut(JSON.parse(keys) as string[], placeholder, () => onLoad?.()),\n [keys, placeholder, onLoad],\n )\n\n const binding = search?.keyBindings[0]\n\n return (\n <button\n type=\"button\"\n className={`${triggerClasses} text-muted-foreground size-8 justify-center gap-2 px-0 font-normal sm:w-56 sm:justify-start sm:px-2.5`}\n onPointerEnter={() => onLoad?.()}\n onClick={() => {\n requestStarterKitSearch()\n onLoad?.()\n }}\n >\n <SearchIcon className=\"size-4\" aria-hidden=\"true\" />\n <span className=\"sr-only sm:not-sr-only sm:flex-1 sm:text-start\">{placeholder}</span>\n {hydrated && binding && (\n <kbd\n dir=\"ltr\"\n className=\"bg-muted text-foreground/70 hidden rounded px-1 font-mono text-[10px] sm:inline\"\n >\n {formatKeyBinding(binding)}\n </kbd>\n )}\n </button>\n )\n}\n\n/** The bell and its unread count until the menu has loaded. */\nexport function NotificationsTrigger({ source }: WidgetProps) {\n const notifications = source.widgets.notifications\n const count = notifications?.unreadCount ?? 0\n\n return (\n <button\n type=\"button\"\n data-slot=\"notifications-trigger\"\n aria-label={notifications?.label}\n className=\"hover:bg-accent hover:text-accent-foreground relative inline-flex size-9 items-center justify-center rounded-md\"\n >\n <BellIcon className=\"size-4\" aria-hidden=\"true\" />\n {count > 0 && (\n <span className=\"bg-primary text-primary-foreground absolute -end-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-medium\">\n {count > 99 ? '99+' : count}\n </span>\n )}\n </button>\n )\n}\n\n/** The current tenant (or an empty avatar) until the switcher has loaded: a placeholder. */\nexport function TenantSwitcherTrigger({ source }: WidgetProps) {\n const current = source.widgets.tenancy?.current\n\n return (\n <button\n type=\"button\"\n data-slot=\"tenant-menu\"\n aria-hidden=\"true\"\n tabIndex={-1}\n className=\"hover:bg-accent inline-flex h-9 max-w-60 shrink-0 items-center gap-2 rounded-md px-2 text-sm font-medium\"\n >\n <span className=\"bg-muted flex size-6 items-center justify-center rounded-md text-[10px]\">\n {current ? initials(tenantNameText(current)) : ''}\n </span>\n {current && (\n <span className=\"sr-only min-w-0 truncate lg:not-sr-only\">{tenantNameText(current)}</span>\n )}\n <ChevronsUpDownIcon className=\"text-muted-foreground size-4 shrink-0\" aria-hidden=\"true\" />\n </button>\n )\n}\n\n/**\n * The AI copilot's button until its code has loaded: the same look, and it remembers a\n * click or its key bindings (`mod+j`), so the copilot opens once loaded.\n */\nexport function CopilotTrigger({ source, onLoad }: WidgetProps & { onLoad?: () => void }) {\n const copilot = source.widgets.copilot\n const keys = JSON.stringify(copilot?.keyBindings ?? [])\n const label = copilot?.label ?? ''\n // `onLoad` is a stable function (it starts loading the copilot's code).\n useEffect(\n () => registerStarterKitCopilotShortcut(JSON.parse(keys) as string[], label, () => onLoad?.()),\n [keys, label, onLoad],\n )\n\n return (\n <button\n type=\"button\"\n data-slot=\"ai-copilot-trigger\"\n aria-label={label}\n aria-haspopup=\"dialog\"\n title={label}\n className=\"hover:bg-accent hover:text-accent-foreground inline-flex size-9 items-center justify-center rounded-md\"\n onPointerEnter={() => onLoad?.()}\n onClick={() => {\n requestStarterKitCopilot()\n onLoad?.()\n }}\n >\n <SparklesIcon className=\"size-4\" aria-hidden=\"true\" />\n </button>\n )\n}\n\n/** The language button's look, before and after its menu has loaded. */\nexport const languageTriggerClasses =\n 'hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent inline-flex h-9 min-w-9 shrink-0 items-center justify-center gap-1.5 rounded-md px-2 text-sm font-medium outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50'\n\n/**\n * The language button until its menu has loaded: the languages icon and the current\n * language in its own name (from `sm`). A click is remembered and opens the menu once loaded.\n */\nexport function LanguageSwitcherTrigger({ source, onLoad }: WidgetProps & { onLoad?: () => void }) {\n const language = source.widgets.language\n\n return (\n <button\n type=\"button\"\n data-slot=\"language-switcher-trigger\"\n aria-label={language?.label}\n aria-haspopup=\"menu\"\n title={language?.label}\n className={languageTriggerClasses}\n onPointerEnter={() => onLoad?.()}\n onClick={() => {\n requestStarterKitLanguages()\n onLoad?.()\n }}\n >\n <LanguagesIcon className=\"size-4\" aria-hidden=\"true\" />\n {language && (\n <span\n lang={language.current.tag}\n dir={language.current.direction}\n className=\"hidden sm:inline\"\n >\n {language.current.label}\n </span>\n )}\n </button>\n )\n}\n"],"mappings":"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"}
|
package/dist/themes.css
CHANGED
|
@@ -1015,3 +1015,95 @@
|
|
|
1015
1015
|
box-shadow: none;
|
|
1016
1016
|
outline: none;
|
|
1017
1017
|
}
|
|
1018
|
+
|
|
1019
|
+
/* ------------------------------------------------------------------ CMS site design */
|
|
1020
|
+
|
|
1021
|
+
/*
|
|
1022
|
+
* Public CMS pages (alfasix/alfakit-cms). **Site design** sets the theme variables on the
|
|
1023
|
+
* document root; these rules apply its fonts and each block's **Style** section
|
|
1024
|
+
* (`data-cms-background`, `data-cms-spacing`, `data-cms-visibility` on
|
|
1025
|
+
* `data-slot="cms-block"`). The page keeps copies of its colours so a coloured block can
|
|
1026
|
+
* swap them for its content.
|
|
1027
|
+
*/
|
|
1028
|
+
[data-slot='cms-page'] {
|
|
1029
|
+
--cms-primary: var(--primary);
|
|
1030
|
+
--cms-on-primary: var(--primary-foreground);
|
|
1031
|
+
--cms-secondary: var(--secondary);
|
|
1032
|
+
--cms-on-secondary: var(--secondary-foreground);
|
|
1033
|
+
font-family: var(--cms-font-body);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/* In-page links (`#pricing`) stop below the sticky site header. */
|
|
1037
|
+
[data-slot='cms-page'] [data-slot='cms-block'][id] {
|
|
1038
|
+
scroll-margin-top: 5rem;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
[data-slot='cms-page'] :is(h1, h2, h3, h4, h5, h6) {
|
|
1042
|
+
font-family: var(--cms-font-heading, var(--cms-font-body));
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
[data-slot='cms-block'][data-cms-background] {
|
|
1046
|
+
background-color: var(--background);
|
|
1047
|
+
color: var(--foreground);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
[data-slot='cms-block'][data-cms-background='muted'] {
|
|
1051
|
+
--background: var(--muted);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
[data-slot='cms-block'][data-cms-background='primary'],
|
|
1055
|
+
[data-slot='cms-block'][data-cms-background='secondary'],
|
|
1056
|
+
[data-slot='cms-block'][data-cms-background='dark'] {
|
|
1057
|
+
--muted: color-mix(in oklab, var(--foreground) 10%, var(--background));
|
|
1058
|
+
--muted-foreground: color-mix(in oklab, var(--foreground) 80%, var(--background));
|
|
1059
|
+
--border: color-mix(in oklab, var(--foreground) 22%, var(--background));
|
|
1060
|
+
--card: color-mix(in oklab, var(--foreground) 7%, var(--background));
|
|
1061
|
+
--card-foreground: var(--foreground);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
[data-slot='cms-block'][data-cms-background='primary'] {
|
|
1065
|
+
--background: var(--cms-primary);
|
|
1066
|
+
--foreground: var(--cms-on-primary);
|
|
1067
|
+
--primary: var(--cms-on-primary);
|
|
1068
|
+
--primary-foreground: var(--cms-primary);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
[data-slot='cms-block'][data-cms-background='secondary'] {
|
|
1072
|
+
--background: var(--cms-secondary);
|
|
1073
|
+
--foreground: var(--cms-on-secondary);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
[data-slot='cms-block'][data-cms-background='dark'] {
|
|
1077
|
+
--background: oklch(0.205 0 0);
|
|
1078
|
+
--foreground: oklch(0.985 0 0);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
[data-slot='cms-block'][data-cms-spacing='none'] > * {
|
|
1082
|
+
padding-block: 0;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
[data-slot='cms-block'][data-cms-spacing='small'] > * {
|
|
1086
|
+
padding-block: 1.5rem;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
[data-slot='cms-block'][data-cms-spacing='large'] > * {
|
|
1090
|
+
padding-block: 6rem;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
@media (min-width: 640px) {
|
|
1094
|
+
[data-slot='cms-block'][data-cms-spacing='large'] > * {
|
|
1095
|
+
padding-block: 8rem;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
@media (max-width: 767.98px) {
|
|
1100
|
+
[data-slot='cms-block'][data-cms-visibility='desktop'] {
|
|
1101
|
+
display: none;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
@media (min-width: 768px) {
|
|
1106
|
+
[data-slot='cms-block'][data-cms-visibility='mobile'] {
|
|
1107
|
+
display: none;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfakit/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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.
|
|
99
|
+
"@alfakit/core": "0.2.0"
|
|
100
100
|
},
|
|
101
101
|
"peerDependencies": {
|
|
102
102
|
"@inertiajs/react": "^3.0.0",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CMS_FORM_KEY,
|
|
3
3
|
cmsAspectClass,
|
|
4
|
+
cmsBlockAttributes,
|
|
4
5
|
cmsBlockKey,
|
|
5
6
|
cmsBlocks,
|
|
6
7
|
cmsColumnsClass,
|
|
@@ -464,7 +465,12 @@ export function CmsBlocks({
|
|
|
464
465
|
if (!Component) return null
|
|
465
466
|
|
|
466
467
|
return (
|
|
467
|
-
<div
|
|
468
|
+
<div
|
|
469
|
+
key={block.id}
|
|
470
|
+
{...cmsBlockAttributes(block)}
|
|
471
|
+
data-slot="cms-block"
|
|
472
|
+
data-cms-block={block.type}
|
|
473
|
+
>
|
|
468
474
|
<Component block={block} data={block.data} />
|
|
469
475
|
</div>
|
|
470
476
|
)
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import { cmsHeadTags, type CmsSeoSchema } from '@alfakit/core'
|
|
1
|
+
import { cmsDesignLinks, cmsHeadTags, type CmsSeoSchema, type CmsSiteDesign } from '@alfakit/core'
|
|
2
2
|
import { Head } from '@inertiajs/react'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* The page's `<title>`, meta, canonical, `hreflang` and JSON-LD tags,
|
|
6
|
-
* `<Head>` (server-rendered with SSR,
|
|
5
|
+
* The page's `<title>`, meta, canonical, `hreflang` and JSON-LD tags, plus the site
|
|
6
|
+
* design's favicon and fonts, through Inertia's `<Head>` (server-rendered with SSR,
|
|
7
|
+
* updated on every visit).
|
|
7
8
|
*/
|
|
8
|
-
export function CmsHead({
|
|
9
|
+
export function CmsHead({
|
|
10
|
+
seo,
|
|
11
|
+
design,
|
|
12
|
+
}: {
|
|
13
|
+
seo: CmsSeoSchema | null | undefined
|
|
14
|
+
design?: CmsSiteDesign | null
|
|
15
|
+
}) {
|
|
9
16
|
const tags = cmsHeadTags(seo)
|
|
10
17
|
|
|
11
18
|
return (
|
|
@@ -36,6 +43,14 @@ export function CmsHead({ seo }: { seo: CmsSeoSchema | null | undefined }) {
|
|
|
36
43
|
hrefLang={link.hreflang}
|
|
37
44
|
/>
|
|
38
45
|
))}
|
|
46
|
+
{cmsDesignLinks(design).map((link) => (
|
|
47
|
+
<link
|
|
48
|
+
key={`design:${link.rel}`}
|
|
49
|
+
head-key={`cms:design:${link.rel}`}
|
|
50
|
+
rel={link.rel}
|
|
51
|
+
href={link.href}
|
|
52
|
+
/>
|
|
53
|
+
))}
|
|
39
54
|
{/* Encoded on the server with `<` escaped, so it can't close the element. */}
|
|
40
55
|
{tags.jsonLd && (
|
|
41
56
|
<script head-key="cms:jsonld" type="application/ld+json">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CMS_FOOTER_KEY,
|
|
3
3
|
CMS_HEADER_KEY,
|
|
4
|
+
cmsDesignCss,
|
|
4
5
|
cmsPayload,
|
|
5
6
|
type CmsPageProps,
|
|
6
7
|
type CmsPayload,
|
|
@@ -42,7 +43,16 @@ export function CmsHeader({ cms }: { cms: CmsPayload }) {
|
|
|
42
43
|
href={cms.site.url}
|
|
43
44
|
className="focus-visible:ring-ring/50 rounded-md text-lg font-semibold tracking-tight outline-none focus-visible:ring-[3px]"
|
|
44
45
|
>
|
|
45
|
-
{cms.site.
|
|
46
|
+
{cms.site.design?.logo ? (
|
|
47
|
+
<img
|
|
48
|
+
src={cms.site.design.logo.url}
|
|
49
|
+
alt={cms.site.design.logo.alt}
|
|
50
|
+
data-slot="cms-logo"
|
|
51
|
+
className="h-9 w-auto max-w-48 object-contain"
|
|
52
|
+
/>
|
|
53
|
+
) : (
|
|
54
|
+
cms.site.name
|
|
55
|
+
)}
|
|
46
56
|
</a>
|
|
47
57
|
<div className="flex items-center gap-2">
|
|
48
58
|
<CmsNav menu={cms.menus.main}>
|
|
@@ -130,6 +140,7 @@ function CmsPageLayout({ cms, children }: { cms: CmsPayload; children?: ReactNod
|
|
|
130
140
|
const { t } = useCms()
|
|
131
141
|
const header: AnyComponent = registry?.resolve(CMS_HEADER_KEY) ?? CmsHeader
|
|
132
142
|
const footer: AnyComponent = registry?.resolve(CMS_FOOTER_KEY) ?? CmsFooter
|
|
143
|
+
const designCss = cmsDesignCss(cms.site.design)
|
|
133
144
|
|
|
134
145
|
return (
|
|
135
146
|
<div
|
|
@@ -145,6 +156,10 @@ function CmsPageLayout({ cms, children }: { cms: CmsPayload; children?: ReactNod
|
|
|
145
156
|
>
|
|
146
157
|
{t('cms.skip_to_content', {}, 'Skip to content')}
|
|
147
158
|
</a>
|
|
159
|
+
{/* Site design: built on the server from checked values, never contains `<`. */}
|
|
160
|
+
{designCss && (
|
|
161
|
+
<style data-slot="cms-design" dangerouslySetInnerHTML={{ __html: designCss }} />
|
|
162
|
+
)}
|
|
148
163
|
{cms.preview && <PreviewBanner />}
|
|
149
164
|
{createElement(header, { cms })}
|
|
150
165
|
<main
|
|
@@ -204,7 +219,7 @@ export function CmsPage({
|
|
|
204
219
|
|
|
205
220
|
return (
|
|
206
221
|
<CmsProvider payload={cms} components={components}>
|
|
207
|
-
<CmsHead seo={cms.page.seo} />
|
|
222
|
+
<CmsHead seo={cms.page.seo} design={cms.site.design} />
|
|
208
223
|
<CmsPageLayout cms={cms} />
|
|
209
224
|
</CmsProvider>
|
|
210
225
|
)
|
|
@@ -147,7 +147,8 @@ export function FileUploadField({ component }: { component: FileUploadSchema })
|
|
|
147
147
|
const local = pendingFile(file, position)
|
|
148
148
|
setUploading((state) => [...state, local])
|
|
149
149
|
|
|
150
|
-
queued
|
|
150
|
+
// Submits wait for queued and running uploads, so saving now doesn't drop the file.
|
|
151
|
+
queued.push(store.trackUpload(limit(() => send(endpoint, file, local))))
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
await Promise.all(queued)
|
|
@@ -254,7 +255,7 @@ export function FileUploadField({ component }: { component: FileUploadSchema })
|
|
|
254
255
|
|
|
255
256
|
const local = pendingFile(file, `edit:${existing.path}`)
|
|
256
257
|
setUploading((state) => [...state, local])
|
|
257
|
-
await send(endpoint, file, local, existing.path)
|
|
258
|
+
await store.trackUpload(send(endpoint, file, local, existing.path))
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
const send = async (endpoint: string, file: File, local: LocalFile, replacing?: string) => {
|
|
@@ -596,10 +597,10 @@ export function FileUploadField({ component }: { component: FileUploadSchema })
|
|
|
596
597
|
</span>
|
|
597
598
|
)}
|
|
598
599
|
{!circle &&
|
|
599
|
-
(component.
|
|
600
|
+
(component.acceptedFileTypesLabel || component.maxSize || component.minFiles) && (
|
|
600
601
|
<span className="text-muted-foreground text-xs">
|
|
601
602
|
{[
|
|
602
|
-
component.
|
|
603
|
+
component.acceptedFileTypesLabel,
|
|
603
604
|
component.maxSize
|
|
604
605
|
? `≤ ${fileManagerSize(component.maxSize * 1024, formatter)}`
|
|
605
606
|
: null,
|
|
@@ -110,7 +110,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } fro
|
|
|
110
110
|
import { useActionRunner, usePanel, useT } from '../../context'
|
|
111
111
|
import { Icon } from '../../lib/icons'
|
|
112
112
|
import { cn } from '../../lib/utils'
|
|
113
|
-
import { useField } from '../form/form-context'
|
|
113
|
+
import { useField, useOptionalFormStore } from '../form/form-context'
|
|
114
114
|
import { FieldWrapper } from '../schema/field-wrapper'
|
|
115
115
|
import { Button } from '../ui/button'
|
|
116
116
|
import { Separator } from '../ui/separator'
|
|
@@ -284,6 +284,7 @@ function RichEditorInput({
|
|
|
284
284
|
}) {
|
|
285
285
|
const t = useT()
|
|
286
286
|
const panel = usePanel()
|
|
287
|
+
const formStore = useOptionalFormStore()
|
|
287
288
|
const runner = useActionRunner()
|
|
288
289
|
const { value, setValue, blur, error, id, disabled, readOnly, aria } =
|
|
289
290
|
useField<unknown>(component)
|
|
@@ -516,7 +517,11 @@ function RichEditorInput({
|
|
|
516
517
|
|
|
517
518
|
const pickImage = () => fileInput.current?.click()
|
|
518
519
|
|
|
519
|
-
|
|
520
|
+
// Submits wait for the attachment's upload, so saving now doesn't drop the image.
|
|
521
|
+
const uploadImage = (file: File) =>
|
|
522
|
+
formStore ? formStore.trackUpload(sendImage(file)) : sendImage(file)
|
|
523
|
+
|
|
524
|
+
const sendImage = async (file: File) => {
|
|
520
525
|
if (!panel.endpoints.upload || !editor) return
|
|
521
526
|
|
|
522
527
|
// The server checks again when the form is saved.
|