@alfakit/react 0.1.13 → 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.
Files changed (78) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/components/cms/blocks.d.ts.map +1 -1
  3. package/dist/components/cms/blocks.js +2 -2
  4. package/dist/components/cms/blocks.js.map +1 -1
  5. package/dist/components/cms/head.d.ts +6 -4
  6. package/dist/components/cms/head.d.ts.map +1 -1
  7. package/dist/components/cms/head.js +10 -4
  8. package/dist/components/cms/head.js.map +1 -1
  9. package/dist/components/cms/page.d.ts.map +1 -1
  10. package/dist/components/cms/page.js +16 -3
  11. package/dist/components/cms/page.js.map +1 -1
  12. package/dist/components/fields/file-upload.js +4 -4
  13. package/dist/components/fields/file-upload.js.map +1 -1
  14. package/dist/components/fields/rich-editor.js +4 -2
  15. package/dist/components/fields/rich-editor.js.map +1 -1
  16. package/dist/components/fields/slider.js +1 -1
  17. package/dist/components/fields/slider.js.map +1 -1
  18. package/dist/components/helpdesk/chat-widget.js +1 -1
  19. package/dist/components/layout/section.js +1 -1
  20. package/dist/components/layout/tabs.js +1 -1
  21. package/dist/components/panel/app-sidebar.d.ts.map +1 -1
  22. package/dist/components/panel/app-sidebar.js +27 -5
  23. package/dist/components/panel/app-sidebar.js.map +1 -1
  24. package/dist/components/panel/global-search.js +2 -2
  25. package/dist/components/panel/global-search.js.map +1 -1
  26. package/dist/components/panel/shortcut-keys.js +1 -1
  27. package/dist/components/panel/shortcut-keys.js.map +1 -1
  28. package/dist/components/table/list-tabs.js +1 -1
  29. package/dist/components/table/list-tabs.js.map +1 -1
  30. package/dist/components/table/table-views.js +1 -1
  31. package/dist/components/table/table-views.js.map +1 -1
  32. package/dist/components/versions/record-versions.js +1 -1
  33. package/dist/context.d.ts +8 -0
  34. package/dist/context.d.ts.map +1 -1
  35. package/dist/context.js +27 -2
  36. package/dist/context.js.map +1 -1
  37. package/dist/create-alfakit.d.ts.map +1 -1
  38. package/dist/create-alfakit.js +3 -2
  39. package/dist/create-alfakit.js.map +1 -1
  40. package/dist/eject-manifest.json +2 -2
  41. package/dist/index.js +1 -1
  42. package/dist/layouts/app-layout.d.ts.map +1 -1
  43. package/dist/layouts/app-layout.js +13 -3
  44. package/dist/layouts/app-layout.js.map +1 -1
  45. package/dist/lib/discovery.d.ts +4 -0
  46. package/dist/lib/discovery.d.ts.map +1 -1
  47. package/dist/lib/discovery.js +17 -7
  48. package/dist/lib/discovery.js.map +1 -1
  49. package/dist/lib/icons.d.ts +2 -1
  50. package/dist/lib/icons.d.ts.map +1 -1
  51. package/dist/lib/icons.js +55 -19
  52. package/dist/lib/icons.js.map +1 -1
  53. package/dist/pages/page.d.ts +1 -1
  54. package/dist/pages/page.d.ts.map +1 -1
  55. package/dist/pages/page.js +47 -10
  56. package/dist/pages/page.js.map +1 -1
  57. package/dist/starter-kit/widgets-triggers.js +1 -1
  58. package/dist/starter-kit/widgets-triggers.js.map +1 -1
  59. package/dist/themes.css +92 -0
  60. package/package.json +2 -2
  61. package/src/components/cms/blocks.tsx +7 -1
  62. package/src/components/cms/head.tsx +19 -4
  63. package/src/components/cms/page.tsx +17 -2
  64. package/src/components/fields/file-upload.tsx +5 -4
  65. package/src/components/fields/rich-editor.tsx +7 -2
  66. package/src/components/fields/slider.tsx +3 -1
  67. package/src/components/panel/app-sidebar.tsx +28 -7
  68. package/src/components/panel/global-search.tsx +4 -1
  69. package/src/components/panel/shortcut-keys.tsx +1 -1
  70. package/src/components/table/list-tabs.tsx +3 -1
  71. package/src/components/table/table-views.tsx +3 -1
  72. package/src/context.tsx +46 -1
  73. package/src/create-alfakit.tsx +7 -2
  74. package/src/layouts/app-layout.tsx +16 -3
  75. package/src/lib/discovery.tsx +23 -8
  76. package/src/lib/icons.tsx +90 -24
  77. package/src/pages/page.tsx +85 -27
  78. package/src/starter-kit/widgets-triggers.tsx +4 -1
@@ -1,29 +1,39 @@
1
+ import { useOptionalAlfakit } from "../context.js";
1
2
  import { ComponentDiscovery } from "@alfakit/core";
2
- import { createElement, useEffect, useState } from "react";
3
+ import { createElement, useEffect, useState, useSyncExternalStore } from "react";
3
4
  //#region src/lib/discovery.tsx
5
+ const subscribe = () => () => {};
4
6
  /**
5
7
  * A discovered component: rendered straight away once its code has loaded (the pages that
6
8
  * use it preload it, see `ComponentDiscovery.preloadFor()`), else as soon as it arrives
7
9
  * (through state, not `<Suspense>`, whose reveal React throttles by up to 300 ms).
8
10
  * Each mount keeps the path it started with, so loading never remounts a field.
11
+ *
12
+ * Where pages are not preloaded (starter kits, standalone components), the server render
13
+ * and hydration leave it out even when its code is loaded: a long-lived SSR server keeps
14
+ * what earlier requests loaded, a fresh browser has none of it (OM-B06).
9
15
  */
10
16
  function discoveredComponent(entry) {
11
17
  function Discovered(props) {
12
- const [state, setState] = useState(() => ({ component: entry.current }));
18
+ const preloaded = useOptionalAlfakit()?.preloadsPageComponents === true;
19
+ const hydrated = useSyncExternalStore(subscribe, () => true, () => false);
20
+ const ready = preloaded || hydrated;
21
+ const [state, setState] = useState(() => ({ component: ready ? entry.current : void 0 }));
22
+ const component = state.component ?? (ready ? entry.current : void 0);
13
23
  useEffect(() => {
14
- if (state.component) return;
24
+ if (component) return;
15
25
  let active = true;
16
- entry.load().then((component) => {
17
- if (active) setState({ component });
26
+ entry.load().then((loaded) => {
27
+ if (active) setState({ component: loaded });
18
28
  }, (error) => {
19
29
  if (active) setState({ error: error ?? /* @__PURE__ */ new Error(`Failed to load ${entry.key}`) });
20
30
  });
21
31
  return () => {
22
32
  active = false;
23
33
  };
24
- }, [state.component]);
34
+ }, [component]);
25
35
  if (state.error !== void 0) throw state.error;
26
- return state.component ? createElement(state.component, props) : null;
36
+ return component ? createElement(component, props) : null;
27
37
  }
28
38
  Discovered.displayName = `Discovered(${entry.key})`;
29
39
  return Discovered;
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","names":[],"sources":["../../src/lib/discovery.tsx"],"sourcesContent":["import {\n ComponentDiscovery,\n type DiscoveredEntry,\n type DiscoveryModules,\n type DiscoveryOptions,\n} from '@alfakit/core'\nimport { createElement, useEffect, useState } from 'react'\nimport type { AnyComponent } from '../context'\n\n/**\n * A discovered component: rendered straight away once its code has loaded (the pages that\n * use it preload it, see `ComponentDiscovery.preloadFor()`), else as soon as it arrives\n * (through state, not `<Suspense>`, whose reveal React throttles by up to 300 ms).\n * Each mount keeps the path it started with, so loading never remounts a field.\n */\nexport function discoveredComponent(entry: DiscoveredEntry<AnyComponent>): AnyComponent {\n function Discovered(props: Record<string, unknown>) {\n const [state, setState] = useState<{ component?: AnyComponent; error?: unknown }>(() => ({\n component: entry.current,\n }))\n\n useEffect(() => {\n if (state.component) return\n\n let active = true\n\n entry.load().then(\n (component) => {\n if (active) setState({ component })\n },\n (error: unknown) => {\n if (active) setState({ error: error ?? new Error(`Failed to load ${entry.key}`) })\n },\n )\n\n return () => {\n active = false\n }\n }, [state.component])\n\n if (state.error !== undefined) throw state.error\n\n return state.component ? createElement(state.component, props) : null\n }\n\n Discovered.displayName = `Discovered(${entry.key})`\n\n return Discovered\n}\n\nexport interface DiscoveredComponents {\n discovery: ComponentDiscovery<AnyComponent>\n components: Record<string, AnyComponent>\n pages: Record<string, AnyComponent>\n}\n\n/**\n * Registry entries for `createAlfakit({ discover: import.meta.glob(…) })`, also usable on\n * their own: `registry.registerMany(discoverComponents(modules).components)`.\n */\nexport function discoverComponents(\n modules: DiscoveryModules | undefined,\n options: DiscoveryOptions = {},\n): DiscoveredComponents {\n const discovery = new ComponentDiscovery<AnyComponent>(modules ?? {}, options)\n\n return { discovery, ...discovery.map(discoveredComponent) }\n}\n"],"mappings":";;;;;;;;;AAeA,SAAgB,oBAAoB,OAAoD;CACtF,SAAS,WAAW,OAAgC;EAClD,MAAM,CAAC,OAAO,YAAY,gBAA+D,EACvF,WAAW,MAAM,QACnB,EAAE;EAEF,gBAAgB;GACd,IAAI,MAAM,WAAW;GAErB,IAAI,SAAS;GAEb,MAAM,KAAK,CAAC,CAAC,MACV,cAAc;IACb,IAAI,QAAQ,SAAS,EAAE,UAAU,CAAC;GACpC,IACC,UAAmB;IAClB,IAAI,QAAQ,SAAS,EAAE,OAAO,yBAAS,IAAI,MAAM,kBAAkB,MAAM,KAAK,EAAE,CAAC;GACnF,CACF;GAEA,aAAa;IACX,SAAS;GACX;EACF,GAAG,CAAC,MAAM,SAAS,CAAC;EAEpB,IAAI,MAAM,UAAU,KAAA,GAAW,MAAM,MAAM;EAE3C,OAAO,MAAM,YAAY,cAAc,MAAM,WAAW,KAAK,IAAI;CACnE;CAEA,WAAW,cAAc,cAAc,MAAM,IAAI;CAEjD,OAAO;AACT;;;;;AAYA,SAAgB,mBACd,SACA,UAA4B,CAAC,GACP;CACtB,MAAM,YAAY,IAAI,mBAAiC,WAAW,CAAC,GAAG,OAAO;CAE7E,OAAO;EAAE;EAAW,GAAG,UAAU,IAAI,mBAAmB;CAAE;AAC5D"}
1
+ {"version":3,"file":"discovery.js","names":[],"sources":["../../src/lib/discovery.tsx"],"sourcesContent":["import {\n ComponentDiscovery,\n type DiscoveredEntry,\n type DiscoveryModules,\n type DiscoveryOptions,\n} from '@alfakit/core'\nimport { createElement, useEffect, useState, useSyncExternalStore } from 'react'\nimport { useOptionalAlfakit, type AnyComponent } from '../context'\n\nconst subscribe = () => () => {}\n\n/**\n * A discovered component: rendered straight away once its code has loaded (the pages that\n * use it preload it, see `ComponentDiscovery.preloadFor()`), else as soon as it arrives\n * (through state, not `<Suspense>`, whose reveal React throttles by up to 300 ms).\n * Each mount keeps the path it started with, so loading never remounts a field.\n *\n * Where pages are not preloaded (starter kits, standalone components), the server render\n * and hydration leave it out even when its code is loaded: a long-lived SSR server keeps\n * what earlier requests loaded, a fresh browser has none of it (OM-B06).\n */\nexport function discoveredComponent(entry: DiscoveredEntry<AnyComponent>): AnyComponent {\n function Discovered(props: Record<string, unknown>) {\n const preloaded = useOptionalAlfakit()?.preloadsPageComponents === true\n // False on the server and while hydrating, true for every later render and mount.\n const hydrated = useSyncExternalStore(\n subscribe,\n () => true,\n () => false,\n )\n const ready = preloaded || hydrated\n const [state, setState] = useState<{ component?: AnyComponent; error?: unknown }>(() => ({\n component: ready ? entry.current : undefined,\n }))\n const component = state.component ?? (ready ? entry.current : undefined)\n\n useEffect(() => {\n if (component) return\n\n let active = true\n\n entry.load().then(\n (loaded) => {\n if (active) setState({ component: loaded })\n },\n (error: unknown) => {\n if (active) setState({ error: error ?? new Error(`Failed to load ${entry.key}`) })\n },\n )\n\n return () => {\n active = false\n }\n }, [component])\n\n if (state.error !== undefined) throw state.error\n\n return component ? createElement(component, props) : null\n }\n\n Discovered.displayName = `Discovered(${entry.key})`\n\n return Discovered\n}\n\nexport interface DiscoveredComponents {\n discovery: ComponentDiscovery<AnyComponent>\n components: Record<string, AnyComponent>\n pages: Record<string, AnyComponent>\n}\n\n/**\n * Registry entries for `createAlfakit({ discover: import.meta.glob(…) })`, also usable on\n * their own: `registry.registerMany(discoverComponents(modules).components)`.\n */\nexport function discoverComponents(\n modules: DiscoveryModules | undefined,\n options: DiscoveryOptions = {},\n): DiscoveredComponents {\n const discovery = new ComponentDiscovery<AnyComponent>(modules ?? {}, options)\n\n return { discovery, ...discovery.map(discoveredComponent) }\n}\n"],"mappings":";;;;AASA,MAAM,wBAAwB,CAAC;;;;;;;;;;;AAY/B,SAAgB,oBAAoB,OAAoD;CACtF,SAAS,WAAW,OAAgC;EAClD,MAAM,YAAY,mBAAmB,CAAC,EAAE,2BAA2B;EAEnE,MAAM,WAAW,qBACf,iBACM,YACA,KACR;EACA,MAAM,QAAQ,aAAa;EAC3B,MAAM,CAAC,OAAO,YAAY,gBAA+D,EACvF,WAAW,QAAQ,MAAM,UAAU,KAAA,EACrC,EAAE;EACF,MAAM,YAAY,MAAM,cAAc,QAAQ,MAAM,UAAU,KAAA;EAE9D,gBAAgB;GACd,IAAI,WAAW;GAEf,IAAI,SAAS;GAEb,MAAM,KAAK,CAAC,CAAC,MACV,WAAW;IACV,IAAI,QAAQ,SAAS,EAAE,WAAW,OAAO,CAAC;GAC5C,IACC,UAAmB;IAClB,IAAI,QAAQ,SAAS,EAAE,OAAO,yBAAS,IAAI,MAAM,kBAAkB,MAAM,KAAK,EAAE,CAAC;GACnF,CACF;GAEA,aAAa;IACX,SAAS;GACX;EACF,GAAG,CAAC,SAAS,CAAC;EAEd,IAAI,MAAM,UAAU,KAAA,GAAW,MAAM,MAAM;EAE3C,OAAO,YAAY,cAAc,WAAW,KAAK,IAAI;CACvD;CAEA,WAAW,cAAc,cAAc,MAAM,IAAI;CAEjD,OAAO;AACT;;;;;AAYA,SAAgB,mBACd,SACA,UAA4B,CAAC,GACP;CACtB,MAAM,YAAY,IAAI,mBAAiC,WAAW,CAAC,GAAG,OAAO;CAE7E,OAAO;EAAE;EAAW,GAAG,UAAU,IAAI,mBAAmB;CAAE;AAC5D"}
@@ -15,7 +15,8 @@ export type IconProps = Omit<ComponentProps<DynamicIconComponent>, 'name' | 'fal
15
15
  * app's replacement (`Icons::register()`) or `fallback`. Unknown Lucide names fall back to
16
16
  * `fallback`, if any. `fallback` may also be a statically imported glyph
17
17
  * (`fallback={SearchIcon}`): renderer chrome uses it so an unreplaced alias draws at once,
18
- * without loading anything. Register a `panel:icon` component
18
+ * without loading anything. Named icons load on demand; once loaded, later mounts draw them
19
+ * synchronously. Register a `panel:icon` component
19
20
  * (`createAlfakit({ components: { 'panel:icon': MyIcon } })`) to render every named icon
20
21
  * with another icon set; it receives `name`, `fallback` and `className`.
21
22
  */
@@ -1 +1 @@
1
- {"version":3,"file":"icons.d.ts","names":["LucideIconName"],"sources":["../../src/lib/icons.tsx"],"mappings":";;;;KAOK,8BAA8B;;YAsDvB,YAAY,cAAc;YAE1B,YAAY,KAAK,eAAe;EAC1C;EACA,WAAWA,WAAiB;;;;;;;;;;;;wBAad,OAAO,MAAM,UAAU,cAAc,cAAc,SAAS,4BAAS,IAAA"}
1
+ {"version":3,"file":"icons.d.ts","names":["LucideIconName"],"sources":["../../src/lib/icons.tsx"],"mappings":";;;;KAeK,8BAA8B;;YA+GvB,YAAY,cAAc;YAE1B,YAAY,KAAK,eAAe;EAC1C;EACA,WAAWA,WAAiB;;;;;;;;;;;;;wBAcd,OAAO,MAAM,UAAU,cAAc,cAAc,SAAS,4BAAS,IAAA"}
package/dist/lib/icons.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { useOptionalRegistry } from "../context.js";
2
2
  import { cn } from "./utils.js";
3
+ import { useHydrated } from "../hooks/use-hydrated.js";
3
4
  import { iconAlias, iconAliasOverride, iconSource, isIconAlias } from "@alfakit/core";
4
5
  import { createElement, useEffect, useState } from "react";
5
6
  import { jsx } from "react/jsx-runtime";
@@ -8,34 +9,68 @@ import { jsx } from "react/jsx-runtime";
8
9
  * `lucide-react/dynamic` carries a lazy import for every Lucide icon (~2,000 entries), so it
9
10
  * is not part of any entry chunk: panels (`createAlfakit()`, starter kit pages) start loading
10
11
  * it while they boot, since every page draws named icons, and elsewhere (a host's sidebar)
11
- * it loads with the first icon. Named icons render asynchronously either way.
12
+ * it loads with the first icon.
12
13
  */
13
- let dynamicIcon;
14
- let pendingDynamicIcon;
15
- function loadDynamicIcon() {
16
- pendingDynamicIcon ??= import("lucide-react/dynamic.mjs").then((module) => dynamicIcon = module.DynamicIcon, (error) => {
17
- pendingDynamicIcon = void 0;
14
+ let pendingImports;
15
+ function loadIconImports() {
16
+ pendingImports ??= import("lucide-react/dynamic.mjs").then((module) => module.dynamicIconImports, (error) => {
17
+ pendingImports = void 0;
18
18
  throw error;
19
19
  });
20
- return pendingDynamicIcon;
20
+ return pendingImports;
21
+ }
22
+ /**
23
+ * Loaded icons by name (`null`: not a Lucide icon), shared by every `<Icon>`: an icon that
24
+ * loaded once draws synchronously in every later mount (the page after a visit, new table
25
+ * rows), instead of rendering empty for a frame on each mount as `DynamicIcon` does.
26
+ */
27
+ const loadedIcons = /* @__PURE__ */ new Map();
28
+ const pendingIcons = /* @__PURE__ */ new Map();
29
+ function loadIcon(name) {
30
+ const loaded = loadedIcons.get(name);
31
+ if (loaded !== void 0) return Promise.resolve(loaded);
32
+ let pending = pendingIcons.get(name);
33
+ if (!pending) {
34
+ pending = loadIconImports().then((imports) => {
35
+ const load = Object.prototype.hasOwnProperty.call(imports, name) ? imports[name] : void 0;
36
+ return load ? load().then((module) => module.default) : null;
37
+ }).then((icon) => {
38
+ loadedIcons.set(name, icon);
39
+ pendingIcons.delete(name);
40
+ return icon;
41
+ }, (error) => {
42
+ pendingIcons.delete(name);
43
+ throw error;
44
+ });
45
+ pendingIcons.set(name, pending);
46
+ }
47
+ return pending;
21
48
  }
22
49
  /** Start loading `lucide-react/dynamic` ahead of the first named icon (browser only). */
23
50
  function preloadIcons() {
24
- if (typeof window !== "undefined") loadDynamicIcon().catch(() => void 0);
51
+ if (typeof window !== "undefined") loadIconImports().catch(() => void 0);
25
52
  }
26
- /** `DynamicIcon`, rendering its `fallback` (or nothing) until `lucide-react/dynamic` loads. */
27
- function LazyDynamicIcon(props) {
28
- const [Dynamic, setDynamic] = useState(() => dynamicIcon);
53
+ /**
54
+ * A Lucide icon by name, rendering `fallback` (or nothing) while it loads and for unknown
55
+ * names. An icon that has loaded renders at once, except during the server render and
56
+ * hydration: the server draws no named icons, so the hydrated markup matches it.
57
+ */
58
+ function LucideIcon({ name, fallback, ...props }) {
59
+ const hydrated = useHydrated();
60
+ const [, setLoaded] = useState(0);
61
+ const Loaded = hydrated ? loadedIcons.get(name) : void 0;
29
62
  useEffect(() => {
30
- if (Dynamic) return;
63
+ if (loadedIcons.has(name)) return;
31
64
  let active = true;
32
- loadDynamicIcon().then((component) => active && setDynamic(() => component), () => void 0);
65
+ loadIcon(name).then(() => {
66
+ if (active) setLoaded((count) => count + 1);
67
+ }, (error) => console.error(error));
33
68
  return () => {
34
69
  active = false;
35
70
  };
36
- }, [Dynamic]);
37
- if (Dynamic) return /* @__PURE__ */ jsx(Dynamic, { ...props });
38
- return props.fallback ? createElement(props.fallback) : null;
71
+ }, [name]);
72
+ if (Loaded) return createElement(Loaded, props);
73
+ return fallback ? fallback() : null;
39
74
  }
40
75
  /**
41
76
  * Renders an icon resolved by the server (`Icons::resolve()`): a Lucide name (`users`,
@@ -43,7 +78,8 @@ function LazyDynamicIcon(props) {
43
78
  * app's replacement (`Icons::register()`) or `fallback`. Unknown Lucide names fall back to
44
79
  * `fallback`, if any. `fallback` may also be a statically imported glyph
45
80
  * (`fallback={SearchIcon}`): renderer chrome uses it so an unreplaced alias draws at once,
46
- * without loading anything. Register a `panel:icon` component
81
+ * without loading anything. Named icons load on demand; once loaded, later mounts draw them
82
+ * synchronously. Register a `panel:icon` component
47
83
  * (`createAlfakit({ components: { 'panel:icon': MyIcon } })`) to render every named icon
48
84
  * with another icon set; it receives `name`, `fallback` and `className`.
49
85
  */
@@ -77,10 +113,10 @@ function Icon({ name, fallback: fallbackProp, className, ...props }) {
77
113
  className: classes,
78
114
  ...props
79
115
  });
80
- return /* @__PURE__ */ jsx(LazyDynamicIcon, {
116
+ return /* @__PURE__ */ jsx(LucideIcon, {
81
117
  name: source.name,
82
118
  className: classes,
83
- fallback: Glyph ? () => /* @__PURE__ */ jsx(Glyph, { className: classes }) : fallback ? () => /* @__PURE__ */ jsx(LazyDynamicIcon, {
119
+ fallback: Glyph ? () => /* @__PURE__ */ jsx(Glyph, { className: classes }) : fallback ? () => /* @__PURE__ */ jsx(LucideIcon, {
84
120
  name: fallback,
85
121
  className: classes
86
122
  }) : void 0,
@@ -1 +1 @@
1
- {"version":3,"file":"icons.js","names":[],"sources":["../../src/lib/icons.tsx"],"sourcesContent":["import { iconAlias, iconAliasOverride, iconSource, isIconAlias } from '@alfakit/core'\nimport type { LucideProps } from 'lucide-react'\nimport type { DynamicIcon, IconName as LucideIconName } from 'lucide-react/dynamic.mjs'\nimport { createElement, useEffect, useState, type ComponentProps, type ComponentType } from 'react'\nimport { useOptionalRegistry } from '../context'\nimport { cn } from './utils'\n\ntype DynamicIconComponent = typeof DynamicIcon\n\n/**\n * `lucide-react/dynamic` carries a lazy import for every Lucide icon (~2,000 entries), so it\n * is not part of any entry chunk: panels (`createAlfakit()`, starter kit pages) start loading\n * it while they boot, since every page draws named icons, and elsewhere (a host's sidebar)\n * it loads with the first icon. Named icons render asynchronously either way.\n */\nlet dynamicIcon: DynamicIconComponent | undefined\nlet pendingDynamicIcon: Promise<DynamicIconComponent> | undefined\n\nfunction loadDynamicIcon(): Promise<DynamicIconComponent> {\n pendingDynamicIcon ??= import('lucide-react/dynamic.mjs').then(\n (module) => (dynamicIcon = module.DynamicIcon),\n (error: unknown) => {\n pendingDynamicIcon = undefined\n\n throw error\n },\n )\n\n return pendingDynamicIcon\n}\n\n/** Start loading `lucide-react/dynamic` ahead of the first named icon (browser only). */\nexport function preloadIcons(): void {\n if (typeof window !== 'undefined') loadDynamicIcon().catch(() => undefined)\n}\n\n/** `DynamicIcon`, rendering its `fallback` (or nothing) until `lucide-react/dynamic` loads. */\nfunction LazyDynamicIcon(props: ComponentProps<DynamicIconComponent>) {\n const [Dynamic, setDynamic] = useState(() => dynamicIcon)\n\n useEffect(() => {\n if (Dynamic) return\n\n let active = true\n\n loadDynamicIcon().then(\n (component) => active && setDynamic(() => component),\n () => undefined,\n )\n\n return () => {\n active = false\n }\n }, [Dynamic])\n\n if (Dynamic) return <Dynamic {...props} />\n\n return props.fallback ? createElement(props.fallback) : null\n}\n\n/** A statically imported glyph (`SearchIcon` from `lucide-react`). */\nexport type IconGlyph = ComponentType<LucideProps>\n\nexport type IconProps = Omit<ComponentProps<DynamicIconComponent>, 'name' | 'fallback'> & {\n name?: string | null\n fallback?: LucideIconName | IconGlyph\n}\n\n/**\n * Renders an icon resolved by the server (`Icons::resolve()`): a Lucide name (`users`,\n * `shopping-cart`), an image URL or sanitised `<svg>` markup. `alfakit::` aliases use the\n * app's replacement (`Icons::register()`) or `fallback`. Unknown Lucide names fall back to\n * `fallback`, if any. `fallback` may also be a statically imported glyph\n * (`fallback={SearchIcon}`): renderer chrome uses it so an unreplaced alias draws at once,\n * without loading anything. Register a `panel:icon` component\n * (`createAlfakit({ components: { 'panel:icon': MyIcon } })`) to render every named icon\n * with another icon set; it receives `name`, `fallback` and `className`.\n */\nexport function Icon({ name, fallback: fallbackProp, className, ...props }: IconProps) {\n const Custom = useOptionalRegistry()?.resolve('panel:icon')\n\n if (!name) {\n return null\n }\n\n const classes = cn('size-4 shrink-0', className)\n const Glyph = typeof fallbackProp === 'string' ? undefined : fallbackProp\n const fallback = typeof fallbackProp === 'string' ? fallbackProp : undefined\n\n // Chrome icons (`<Icon name=\"alfakit::…\" fallback={SearchIcon} />`) draw their bundled\n // glyph synchronously unless the app replaced the alias with `Icons::register()`.\n if (Glyph && isIconAlias(name) && iconAliasOverride(name) === undefined) {\n return <Glyph className={classes} {...props} />\n }\n\n const source = iconSource(iconAlias(name, fallback))\n\n if (source.type === 'svg') {\n return (\n <span\n aria-hidden=\"true\"\n data-slot=\"icon\"\n className={cn('inline-flex [&>svg]:size-full', classes)}\n // Sanitised on the server (SvgSanitizer): drawing elements only.\n dangerouslySetInnerHTML={{ __html: source.svg }}\n />\n )\n }\n\n if (source.type === 'url') {\n return (\n <img\n src={source.url}\n alt=\"\"\n aria-hidden=\"true\"\n data-slot=\"icon\"\n className={cn('object-contain', classes)}\n />\n )\n }\n\n if (Custom) {\n return createElement(Custom, {\n name: source.name,\n fallback,\n className: classes,\n ...props,\n })\n }\n\n return (\n <LazyDynamicIcon\n name={source.name as LucideIconName}\n className={classes}\n fallback={\n Glyph\n ? () => <Glyph className={classes} />\n : fallback\n ? () => <LazyDynamicIcon name={fallback} className={classes} />\n : undefined\n }\n {...props}\n />\n )\n}\n"],"mappings":";;;;;;;;;;;;AAeA,IAAI;AACJ,IAAI;AAEJ,SAAS,kBAAiD;CACxD,uBAAuB,OAAO,2BAA2B,CAAC,MACvD,WAAY,cAAc,OAAO,cACjC,UAAmB;EAClB,qBAAqB,KAAA;EAErB,MAAM;CACR,CACF;CAEA,OAAO;AACT;;AAGA,SAAgB,eAAqB;CACnC,IAAI,OAAO,WAAW,aAAa,gBAAgB,CAAC,CAAC,YAAY,KAAA,CAAS;AAC5E;;AAGA,SAAS,gBAAgB,OAA6C;CACpE,MAAM,CAAC,SAAS,cAAc,eAAe,WAAW;CAExD,gBAAgB;EACd,IAAI,SAAS;EAEb,IAAI,SAAS;EAEb,gBAAgB,CAAC,CAAC,MACf,cAAc,UAAU,iBAAiB,SAAS,SAC7C,KAAA,CACR;EAEA,aAAa;GACX,SAAS;EACX;CACF,GAAG,CAAC,OAAO,CAAC;CAEZ,IAAI,SAAS,OAAO,oBAAC,SAAD,EAAS,GAAI,MAAQ,CAAA;CAEzC,OAAO,MAAM,WAAW,cAAc,MAAM,QAAQ,IAAI;AAC1D;;;;;;;;;;;AAoBA,SAAgB,KAAK,EAAE,MAAM,UAAU,cAAc,WAAW,GAAG,SAAoB;CACrF,MAAM,SAAS,oBAAoB,CAAC,EAAE,QAAQ,YAAY;CAE1D,IAAI,CAAC,MACH,OAAO;CAGT,MAAM,UAAU,GAAG,mBAAmB,SAAS;CAC/C,MAAM,QAAQ,OAAO,iBAAiB,WAAW,KAAA,IAAY;CAC7D,MAAM,WAAW,OAAO,iBAAiB,WAAW,eAAe,KAAA;CAInE,IAAI,SAAS,YAAY,IAAI,KAAK,kBAAkB,IAAI,MAAM,KAAA,GAC5D,OAAO,oBAAC,OAAD;EAAO,WAAW;EAAS,GAAI;CAAQ,CAAA;CAGhD,MAAM,SAAS,WAAW,UAAU,MAAM,QAAQ,CAAC;CAEnD,IAAI,OAAO,SAAS,OAClB,OACE,oBAAC,QAAD;EACE,eAAY;EACZ,aAAU;EACV,WAAW,GAAG,iCAAiC,OAAO;EAEtD,yBAAyB,EAAE,QAAQ,OAAO,IAAI;CAC/C,CAAA;CAIL,IAAI,OAAO,SAAS,OAClB,OACE,oBAAC,OAAD;EACE,KAAK,OAAO;EACZ,KAAI;EACJ,eAAY;EACZ,aAAU;EACV,WAAW,GAAG,kBAAkB,OAAO;CACxC,CAAA;CAIL,IAAI,QACF,OAAO,cAAc,QAAQ;EAC3B,MAAM,OAAO;EACb;EACA,WAAW;EACX,GAAG;CACL,CAAC;CAGH,OACE,oBAAC,iBAAD;EACE,MAAM,OAAO;EACb,WAAW;EACX,UACE,cACU,oBAAC,OAAD,EAAO,WAAW,QAAU,CAAA,IAClC,iBACQ,oBAAC,iBAAD;GAAiB,MAAM;GAAU,WAAW;EAAU,CAAA,IAC5D,KAAA;EAER,GAAI;CACL,CAAA;AAEL"}
1
+ {"version":3,"file":"icons.js","names":[],"sources":["../../src/lib/icons.tsx"],"sourcesContent":["import { iconAlias, iconAliasOverride, iconSource, isIconAlias } from '@alfakit/core'\nimport type { LucideProps } from 'lucide-react'\nimport type { DynamicIcon, IconName as LucideIconName } from 'lucide-react/dynamic.mjs'\nimport {\n createElement,\n useEffect,\n useState,\n type ComponentProps,\n type ComponentType,\n type ReactNode,\n} from 'react'\nimport { useOptionalRegistry } from '../context'\nimport { useHydrated } from '../hooks/use-hydrated'\nimport { cn } from './utils'\n\ntype DynamicIconComponent = typeof DynamicIcon\ntype LucideGlyph = ComponentType<LucideProps>\ntype IconImports = Record<string, () => Promise<{ default: LucideGlyph }>>\n\n/**\n * `lucide-react/dynamic` carries a lazy import for every Lucide icon (~2,000 entries), so it\n * is not part of any entry chunk: panels (`createAlfakit()`, starter kit pages) start loading\n * it while they boot, since every page draws named icons, and elsewhere (a host's sidebar)\n * it loads with the first icon.\n */\nlet pendingImports: Promise<IconImports> | undefined\n\nfunction loadIconImports(): Promise<IconImports> {\n pendingImports ??= import('lucide-react/dynamic.mjs').then(\n (module) => module.dynamicIconImports as unknown as IconImports,\n (error: unknown) => {\n pendingImports = undefined\n\n throw error\n },\n )\n\n return pendingImports\n}\n\n/**\n * Loaded icons by name (`null`: not a Lucide icon), shared by every `<Icon>`: an icon that\n * loaded once draws synchronously in every later mount (the page after a visit, new table\n * rows), instead of rendering empty for a frame on each mount as `DynamicIcon` does.\n */\nconst loadedIcons = new Map<string, LucideGlyph | null>()\nconst pendingIcons = new Map<string, Promise<LucideGlyph | null>>()\n\nfunction loadIcon(name: string): Promise<LucideGlyph | null> {\n const loaded = loadedIcons.get(name)\n\n if (loaded !== undefined) return Promise.resolve(loaded)\n\n let pending = pendingIcons.get(name)\n\n if (!pending) {\n pending = loadIconImports()\n .then((imports) => {\n const load = Object.prototype.hasOwnProperty.call(imports, name) ? imports[name] : undefined\n\n return load ? load().then((module) => module.default) : null\n })\n .then(\n (icon) => {\n loadedIcons.set(name, icon)\n pendingIcons.delete(name)\n\n return icon\n },\n (error: unknown) => {\n // A failed chunk is tried again by the next mount.\n pendingIcons.delete(name)\n\n throw error\n },\n )\n pendingIcons.set(name, pending)\n }\n\n return pending\n}\n\n/** Start loading `lucide-react/dynamic` ahead of the first named icon (browser only). */\nexport function preloadIcons(): void {\n if (typeof window !== 'undefined') loadIconImports().catch(() => undefined)\n}\n\ntype LucideIconProps = Omit<ComponentProps<DynamicIconComponent>, 'name' | 'fallback'> & {\n name: string\n fallback?: () => ReactNode\n}\n\n/**\n * A Lucide icon by name, rendering `fallback` (or nothing) while it loads and for unknown\n * names. An icon that has loaded renders at once, except during the server render and\n * hydration: the server draws no named icons, so the hydrated markup matches it.\n */\nfunction LucideIcon({ name, fallback, ...props }: LucideIconProps) {\n const hydrated = useHydrated()\n const [, setLoaded] = useState(0)\n const Loaded = hydrated ? loadedIcons.get(name) : undefined\n\n useEffect(() => {\n if (loadedIcons.has(name)) return\n\n let active = true\n\n loadIcon(name).then(\n () => {\n if (active) setLoaded((count) => count + 1)\n },\n (error: unknown) => console.error(error),\n )\n\n return () => {\n active = false\n }\n }, [name])\n\n // A module-level cache entry (one component per icon name), not created during render.\n if (Loaded) return createElement(Loaded, props as LucideProps)\n\n return fallback ? fallback() : null\n}\n\n/** A statically imported glyph (`SearchIcon` from `lucide-react`). */\nexport type IconGlyph = ComponentType<LucideProps>\n\nexport type IconProps = Omit<ComponentProps<DynamicIconComponent>, 'name' | 'fallback'> & {\n name?: string | null\n fallback?: LucideIconName | IconGlyph\n}\n\n/**\n * Renders an icon resolved by the server (`Icons::resolve()`): a Lucide name (`users`,\n * `shopping-cart`), an image URL or sanitised `<svg>` markup. `alfakit::` aliases use the\n * app's replacement (`Icons::register()`) or `fallback`. Unknown Lucide names fall back to\n * `fallback`, if any. `fallback` may also be a statically imported glyph\n * (`fallback={SearchIcon}`): renderer chrome uses it so an unreplaced alias draws at once,\n * without loading anything. Named icons load on demand; once loaded, later mounts draw them\n * synchronously. Register a `panel:icon` component\n * (`createAlfakit({ components: { 'panel:icon': MyIcon } })`) to render every named icon\n * with another icon set; it receives `name`, `fallback` and `className`.\n */\nexport function Icon({ name, fallback: fallbackProp, className, ...props }: IconProps) {\n const Custom = useOptionalRegistry()?.resolve('panel:icon')\n\n if (!name) {\n return null\n }\n\n const classes = cn('size-4 shrink-0', className)\n const Glyph = typeof fallbackProp === 'string' ? undefined : fallbackProp\n const fallback = typeof fallbackProp === 'string' ? fallbackProp : undefined\n\n // Chrome icons (`<Icon name=\"alfakit::…\" fallback={SearchIcon} />`) draw their bundled\n // glyph synchronously unless the app replaced the alias with `Icons::register()`.\n if (Glyph && isIconAlias(name) && iconAliasOverride(name) === undefined) {\n return <Glyph className={classes} {...props} />\n }\n\n const source = iconSource(iconAlias(name, fallback))\n\n if (source.type === 'svg') {\n return (\n <span\n aria-hidden=\"true\"\n data-slot=\"icon\"\n className={cn('inline-flex [&>svg]:size-full', classes)}\n // Sanitised on the server (SvgSanitizer): drawing elements only.\n dangerouslySetInnerHTML={{ __html: source.svg }}\n />\n )\n }\n\n if (source.type === 'url') {\n return (\n <img\n src={source.url}\n alt=\"\"\n aria-hidden=\"true\"\n data-slot=\"icon\"\n className={cn('object-contain', classes)}\n />\n )\n }\n\n if (Custom) {\n return createElement(Custom, {\n name: source.name,\n fallback,\n className: classes,\n ...props,\n })\n }\n\n return (\n <LucideIcon\n name={source.name}\n className={classes}\n fallback={\n Glyph\n ? () => <Glyph className={classes} />\n : fallback\n ? () => <LucideIcon name={fallback} className={classes} />\n : undefined\n }\n {...props}\n />\n )\n}\n"],"mappings":"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"}
@@ -9,7 +9,7 @@ export declare function AlfakitPage(): import("react").JSX.Element | null;
9
9
  export declare function PageShell({ page, children }: {
10
10
  page: PageSchema;
11
11
  children: ReactNode;
12
- }): import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
12
+ }): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
13
13
  export declare function PageHeader({ page }: {
14
14
  page: PageSchema;
15
15
  }): import("react").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"page.d.ts","names":[],"sources":["../../src/pages/page.tsx"],"mappings":";;;;;;;wBAmEgB,+BAAW,IAAA;wBA0FX,YAAY,MAAM;EAAc,MAAM;EAAY,UAAU;oBAAW,2CAAA;wBA2CvE,aAAa;EAAU,MAAM;oBAAY,IAAA;wBA4DzC,cAAc;EAAU,MAAM;oBAAY,IAAA;wBAU1C,uBAAuB;EAAW,OAAO;oBAAc,IAAA"}
1
+ {"version":3,"file":"page.d.ts","names":[],"sources":["../../src/pages/page.tsx"],"mappings":";;;;;;;wBAsEgB,+BAAW,IAAA;wBAwJX,YAAY,MAAM;EAAc,MAAM;EAAY,UAAU;yCAAW,SAAA,aAAA,6DAAA,8BAAA,+CAAA,8BAAA,SAAA,iDAAA,IAAA;wBAoCvE,aAAa;EAAU,MAAM;oBAAY,IAAA;wBA4DzC,cAAc;EAAU,MAAM;oBAAY,IAAA;wBAU1C,uBAAuB;EAAW,OAAO;oBAAc,IAAA"}
@@ -20,7 +20,7 @@ import { WidgetGrid } from "../components/widgets/widgets-block.js";
20
20
  import { BlankLayout } from "../layouts/blank-layout.js";
21
21
  import { RELATION_QUERY_KEY, applyBodyAttributes, customPageProps, hasRelationManagerTabs, isLazyPlaceholder, openDefaultAction, relationManagerTabs, relationTabFromQuery, relationTabSlug, resolveLazyComponents } from "@alfakit/core";
22
22
  import { Head, Link, usePage } from "@inertiajs/react";
23
- import { Fragment, createElement, useEffect, useId, useMemo, useRef, useState } from "react";
23
+ import { Fragment, createContext, createElement, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
24
24
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
25
25
  //#region src/pages/page.tsx
26
26
  /**
@@ -88,21 +88,53 @@ function useBodyAttributes(attributes) {
88
88
  function layout(registry, key) {
89
89
  return registry.resolve(key) ?? BlankLayout;
90
90
  }
91
- function PageShell({ page, children }) {
91
+ function layoutKind(page) {
92
+ return page.layout === "auth" || page.layout === "blank" || page.layout === "fullscreen" ? page.layout : "app";
93
+ }
94
+ /** The layout `PanelLayoutHost` already renders around the page, if any. */
95
+ const HostedLayoutContext = createContext(null);
96
+ /** The page's registered layout (`layout:app`, `layout:auth`…) around `children`. */
97
+ function PanelLayout({ page, children }) {
92
98
  const { registry } = useAlfakit();
93
- useBodyAttributes(page.extraBodyAttributes);
94
- if (page.layout === "auth") return createElement(layout(registry, "layout:auth"), {
99
+ const kind = layoutKind(page);
100
+ if (kind === "auth") return createElement(layout(registry, "layout:auth"), {
95
101
  heading: page.heading ?? page.title,
96
102
  subheading: page.subheading,
97
103
  page
98
104
  }, children);
99
- if (page.layout === "blank") return createElement(layout(registry, "layout:blank"), { page }, children);
100
- if (page.layout === "fullscreen") return createElement(layout(registry, "layout:fullscreen"), { page }, children);
101
- return createElement(layout(registry, "layout:app"), {
105
+ if (kind === "app") return createElement(layout(registry, "layout:app"), {
102
106
  breadcrumbs: page.breadcrumbs,
103
107
  maxContentWidth: page.maxContentWidth,
104
108
  page
105
- }, /* @__PURE__ */ jsxs(Fragment$1, { children: [
109
+ }, children);
110
+ return createElement(layout(registry, `layout:${kind}`), { page }, children);
111
+ }
112
+ /**
113
+ * Keeps the panel layout (sidebar, top bar) mounted while the user moves between Alfakit
114
+ * pages. Inertia remounts the page component on every visit; this sits in the persistent
115
+ * Inertia layout (`AlfakitRoot`) and renders the open page's layout from its props, so only
116
+ * the page part below it (heading, actions, content) is replaced. The `PageShell` inside
117
+ * the page then renders just that part. Custom page components (their own Inertia page or
118
+ * `page.view`) and starter kit pages render their layout through `PageShell` as before.
119
+ */
120
+ function PanelLayoutHost({ children }) {
121
+ const { component, props } = usePage();
122
+ const page = useMemo(() => props.page ? resolveLazyComponents(props.page, props.alfakitLazy) : void 0, [props.page, props.alfakitLazy]);
123
+ const hosted = component === "Alfakit/Page" && page && !page.view && props.alfakit?.starterKit !== true ? page : void 0;
124
+ return /* @__PURE__ */ jsx(HostedLayoutContext.Provider, {
125
+ value: hosted ? layoutKind(hosted) : null,
126
+ children: hosted ? /* @__PURE__ */ jsx(PanelLayout, {
127
+ page: hosted,
128
+ children
129
+ }) : children
130
+ });
131
+ }
132
+ function PageShell({ page, children }) {
133
+ const { registry } = useAlfakit();
134
+ const hosted = useContext(HostedLayoutContext);
135
+ const kind = layoutKind(page);
136
+ useBodyAttributes(page.extraBodyAttributes);
137
+ const content = kind === "app" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
106
138
  /* @__PURE__ */ jsx(KeyboardShortcuts, {}),
107
139
  /* @__PURE__ */ jsx(Slot, { name: "page.start" }),
108
140
  /* @__PURE__ */ jsx(PageHeader, { page }),
@@ -112,7 +144,12 @@ function PageShell({ page, children }) {
112
144
  children
113
145
  }) : children,
114
146
  /* @__PURE__ */ jsx(Slot, { name: "page.end" })
115
- ] }));
147
+ ] }) : children;
148
+ if (hosted === kind) return content;
149
+ return /* @__PURE__ */ jsx(PanelLayout, {
150
+ page,
151
+ children: content
152
+ });
116
153
  }
117
154
  function PageHeader({ page }) {
118
155
  const { registry } = useAlfakit();
@@ -325,6 +362,6 @@ function RelationManagers({ block }) {
325
362
  });
326
363
  }
327
364
  //#endregion
328
- export { AlfakitPage, ContentBlockRenderer, PageContent, PageHeader, PageShell, useBodyAttributes, useDefaultAction };
365
+ export { AlfakitPage, ContentBlockRenderer, PageContent, PageHeader, PageShell, PanelLayoutHost, useBodyAttributes, useDefaultAction };
329
366
 
330
367
  //# sourceMappingURL=page.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"page.js","names":["FormBlock"],"sources":["../../src/pages/page.tsx"],"sourcesContent":["import type {\n ContentBlock,\n FormBlock as FormBlockSchema,\n InfolistBlock,\n LinksBlock,\n PageSchema,\n RelationManagersBlock,\n SchemaBlock,\n TableBlock as TableBlockSchema,\n WidgetsBlock as WidgetsBlockSchema,\n} from '@alfakit/core'\nimport {\n applyBodyAttributes,\n customPageProps,\n hasRelationManagerTabs,\n isLazyPlaceholder,\n openDefaultAction,\n RELATION_QUERY_KEY,\n relationManagerTabs,\n relationTabFromQuery,\n relationTabSlug,\n resolveLazyComponents,\n type DefaultActionOpener,\n} from '@alfakit/core'\nimport { Head, Link, usePage } from '@inertiajs/react'\nimport {\n createElement,\n Fragment,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react'\nimport { PageActionProvider } from '../components/actions/action-context'\nimport { ActionList } from '../components/actions/action-list'\nimport { FormBlock } from '../components/form/form-block'\nimport { KeyboardShortcuts } from '../components/panel/keyboard-shortcuts'\nimport { LazyPlaceholder } from '../components/panel/lazy-placeholder'\nimport { Slot } from '../components/panel/slot'\nimport { SubNavigationLayout } from '../components/panel/sub-navigation'\nimport { ConflictBanner } from '../components/realtime/conflict-banner'\nimport { RecordPresence } from '../components/realtime/record-presence'\nimport { SchemaRenderer } from '../components/schema/schema-renderer'\nimport { ListTabs, ListTabsPanel } from '../components/table/list-tabs'\nimport { DataTable } from '../components/table/table-block'\nimport { Badge } from '../components/ui/badge'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs'\nimport { WidgetGrid } from '../components/widgets/widgets-block'\nimport {\n useActionRunner,\n useAlfakit,\n useAlfakitProps,\n usePanel,\n useT,\n type AnyComponent,\n} from '../context'\nimport { BlankLayout } from '../layouts/blank-layout'\nimport { badgeColorClasses, colorClass } from '../lib/colors'\nimport { Icon } from '../lib/icons'\nimport { cn } from '../lib/utils'\n\n/**\n * `Alfakit/Page`: renders any schema-driven page. Custom pages (`Page::$component`) are\n * resolved from the `pages` map passed to `createAlfakit()`.\n */\nexport function AlfakitPage() {\n const props = useAlfakitProps()\n const panel = usePanel()\n const t = useT()\n const { pages } = useAlfakit()\n // Lazy widgets and relation managers arrive in a deferred prop after the first response.\n const page = useMemo(\n () => (props.page ? resolveLazyComponents(props.page, props.alfakitLazy) : undefined),\n [props.page, props.alfakitLazy],\n )\n\n useDefaultAction(page)\n\n if (!page) {\n return null\n }\n\n // Starter kit mode: the host's `title` callback adds the application name.\n const title = panel.starterKit ? page.title : `${page.title} - ${panel.brandName}`\n\n if (page.view) {\n const Custom = pages[page.view.component]\n\n if (!Custom) {\n return (\n <div className=\"text-destructive p-6\">\n {t(\n 'page_not_registered',\n { component: page.view.component },\n `Page component ${page.view.component} is not registered in createAlfakit({ pages }).`,\n )}\n </div>\n )\n }\n\n return (\n <PageActionProvider value={{ token: page.token, component: 'page' }}>\n <Head title={title} />\n {/* The component brings its own PageShell (as in panel mode, where it is the Inertia\n page itself) and takes the live props, so `reloadProps()` reaches it. */}\n <Custom\n {...customPageProps(page.view.props, props as Record<string, unknown>)}\n page={page}\n />\n </PageActionProvider>\n )\n }\n\n return (\n <PageActionProvider value={{ token: page.token, component: 'page' }}>\n <Head title={title} />\n <PageShell page={page}>\n <PageContent page={page} />\n </PageShell>\n </PageActionProvider>\n )\n}\n\n/**\n * Opens the page's default action (`$defaultAction`, `?tableAction=`) once the page has\n * hydrated, and once per page: re-renders after the action ran don't open it again.\n */\nexport function useDefaultAction(page: PageSchema | undefined) {\n const runner = useActionRunner()\n const opener = useRef<DefaultActionOpener>({ opened: null })\n\n useEffect(() => {\n openDefaultAction(page, opener.current, (action, context) => runner.trigger(action, context))\n }, [page, runner])\n}\n\n/** Sets the page's `$extraBodyAttributes` on `<body>` while it is open. */\nexport function useBodyAttributes(attributes: Record<string, string> | undefined) {\n const key = attributes ? JSON.stringify(attributes) : ''\n\n useEffect(\n () => applyBodyAttributes(key ? (JSON.parse(key) as Record<string, string>) : undefined),\n [key],\n )\n}\n\n/**\n * The registered layout: `createAlfakit()` registers the panel's (`panelLayouts`), the starter\n * kit its content layout. Without one the page renders bare: importing the panel layouts here\n * would put the sidebar and top bar in every starter kit page.\n */\nfunction layout(registry: { resolve(key: string): AnyComponent | undefined }, key: string) {\n return registry.resolve(key) ?? BlankLayout\n}\n\nexport function PageShell({ page, children }: { page: PageSchema; children: ReactNode }) {\n const { registry } = useAlfakit()\n\n useBodyAttributes(page.extraBodyAttributes)\n\n // Each layout can be replaced: createAlfakit({ components: { 'layout:app': MyLayout } }).\n if (page.layout === 'auth') {\n return createElement(\n layout(registry, 'layout:auth'),\n { heading: page.heading ?? page.title, subheading: page.subheading, page },\n children,\n )\n }\n\n if (page.layout === 'blank') {\n return createElement(layout(registry, 'layout:blank'), { page }, children)\n }\n\n if (page.layout === 'fullscreen') {\n return createElement(layout(registry, 'layout:fullscreen'), { page }, children)\n }\n\n return createElement(\n layout(registry, 'layout:app'),\n { breadcrumbs: page.breadcrumbs, maxContentWidth: page.maxContentWidth, page },\n <>\n {/* `?` overlay, `/`, `g d` and panel command shortcuts (`keyboardShortcuts()`). */}\n <KeyboardShortcuts />\n <Slot name=\"page.start\" />\n <PageHeader page={page} />\n {/* Edit pages: somebody else saved the record (`detectConflicts()`, `Panel::realtime()`). */}\n {page.conflicts &&\n createElement(registry.resolve('page:conflict-banner') ?? ConflictBanner, { page })}\n {page.subNavigation ? (\n <SubNavigationLayout navigation={page.subNavigation}>{children}</SubNavigationLayout>\n ) : (\n children\n )}\n <Slot name=\"page.end\" />\n </>,\n )\n}\n\nexport function PageHeader({ page }: { page: PageSchema }) {\n const { registry } = useAlfakit()\n const actions = page.headerActions ?? []\n // `$headerActionsAlignment`: the default (end) sits beside the heading; the others\n // stack the actions under it.\n const alignment = page.headerActionsAlignment\n\n return (\n <>\n <Slot name=\"page.header.before\" />\n <header\n data-slot=\"page-header\"\n data-actions-alignment={alignment}\n className={cn(\n 'flex flex-col gap-4',\n !alignment && 'sm:flex-row sm:items-start sm:justify-between',\n )}\n >\n <div className=\"min-w-0\">\n <Slot name=\"page.heading.before\" />\n <h1 className=\"text-2xl font-semibold tracking-tight\">{page.heading ?? page.title}</h1>\n {page.subheading && (\n <p className=\"text-muted-foreground mt-1 text-sm\">{page.subheading}</p>\n )}\n <Slot name=\"page.heading.after\" />\n </div>\n <div\n className={cn(\n 'flex shrink-0 flex-wrap items-center gap-2',\n alignment === 'start'\n ? 'justify-start'\n : alignment === 'center'\n ? 'justify-center'\n : alignment === 'between' || alignment === 'justify'\n ? 'justify-between'\n : 'justify-end',\n )}\n >\n {/* Record pages with `Panel::realtime()`: who else is on the record. */}\n {page.presence &&\n createElement(registry.resolve('page:presence') ?? RecordPresence, {\n presence: page.presence,\n })}\n <Slot name=\"page.actions.before\" />\n {actions.length > 0 && (\n <ActionList\n actions={actions}\n context={{ token: page.token, component: 'page' }}\n align=\"end\"\n className=\"max-w-full shrink-0\"\n />\n )}\n <Slot name=\"page.actions.after\" />\n </div>\n </header>\n <Slot name=\"page.header.after\" />\n </>\n )\n}\n\nexport function PageContent({ page }: { page: PageSchema }) {\n return (\n <div className=\"flex flex-col gap-6\">\n {page.content.map((block) => (\n <ContentBlockRenderer key={block.key} block={block} />\n ))}\n </div>\n )\n}\n\nexport function ContentBlockRenderer({ block }: { block: ContentBlock }) {\n const { registry } = useAlfakit()\n // Links list tabs to the table they control.\n const tabsIdBase = `alfakit-list-tabs-${useId().replace(/[^A-Za-z0-9_-]/g, '')}`\n\n switch (block.type) {\n case 'Form':\n return <FormBlock block={block as FormBlockSchema} />\n case 'Infolist':\n return (\n <div data-slot=\"infolist\" className=\"contents\">\n <SchemaRenderer\n components={(block as InfolistBlock).components}\n columns={(block as InfolistBlock).columns ?? { default: 1, lg: 2 }}\n />\n </div>\n )\n case 'Schema':\n return (\n <SchemaRenderer components={(block as SchemaBlock).components} columns={{ default: 1 }} />\n )\n case 'Table': {\n const table = block as TableBlockSchema\n const content = table.tabs?.length ? (\n <div className=\"flex flex-col gap-4\">\n <ListTabs\n tabs={table.tabs}\n activeTab={table.activeTab}\n tabsQueryKey={table.tabsQueryKey}\n label={table.pluralModelLabel}\n idBase={tabsIdBase}\n />\n <ListTabsPanel idBase={tabsIdBase} activeTab={table.activeTab}>\n <DataTable table={table} component={table.component} />\n </ListTabsPanel>\n </div>\n ) : (\n <DataTable table={table} component={table.component} />\n )\n\n if (block.key !== 'table') {\n return content\n }\n\n // The page's main table: the server sends these slots on List / ManageRelatedRecords pages only.\n return (\n <>\n <Slot name=\"resource.list-records.table.before\" />\n <Slot name=\"resource.manage-related-records.table.before\" />\n {content}\n <Slot name=\"resource.manage-related-records.table.after\" />\n <Slot name=\"resource.list-records.table.after\" />\n </>\n )\n }\n case 'RelationManagers':\n return <RelationManagers block={block as RelationManagersBlock} />\n case 'Widgets':\n return block.key === 'headerWidgets' ? (\n <>\n <Slot name=\"page.header-widgets.before\" />\n <WidgetGrid block={block as WidgetsBlockSchema} />\n <Slot name=\"page.header-widgets.after\" />\n </>\n ) : block.key === 'footerWidgets' ? (\n <>\n <Slot name=\"page.footer-widgets.before\" />\n <WidgetGrid block={block as WidgetsBlockSchema} />\n <Slot name=\"page.footer-widgets.after\" />\n </>\n ) : (\n <WidgetGrid block={block as WidgetsBlockSchema} />\n )\n case 'Links':\n return (\n <div className=\"flex flex-col items-center gap-1 text-center text-sm\">\n {(block as LinksBlock).links.map((link) => (\n <Link\n key={link.url}\n href={link.url}\n className=\"text-primary underline-offset-4 hover:underline\"\n >\n {link.label}\n </Link>\n ))}\n </div>\n )\n default: {\n const Custom = registry.resolve(block.type)\n\n return Custom ? createElement(Custom, { block }) : null\n }\n }\n}\n\nfunction RelationManagers({ block }: { block: RelationManagersBlock }) {\n const tabs = useMemo(() => relationManagerTabs(block), [block])\n // `?relation=`: a key in `getRelations()` or a position. The page URL (not\n // `window.location`) so the server renders the same tab as the browser.\n const url = usePage().url\n const [active, setActive] = useState(\n () =>\n relationTabFromQuery(\n tabs,\n new URL(url, 'http://localhost').searchParams.get(RELATION_QUERY_KEY),\n )?.key ??\n tabs[0]?.key ??\n '',\n )\n\n const changeTab = (key: string) => {\n setActive(key)\n\n if (typeof window === 'undefined') return\n\n const next = new URL(window.location.href)\n const slug = relationTabSlug(tabs, key)\n\n if (slug === null) next.searchParams.delete(RELATION_QUERY_KEY)\n else next.searchParams.set(RELATION_QUERY_KEY, slug)\n\n window.history.replaceState(window.history.state, '', next)\n }\n\n if (block.managers.length === 0) {\n return null\n }\n\n if (!hasRelationManagerTabs(block)) {\n const manager = block.managers[0]!\n\n return (\n <section className=\"flex flex-col gap-3\">\n <Slot name=\"resource.relation-manager.before\" scope={manager.key} />\n {isLazyPlaceholder(manager) ? (\n <LazyPlaceholder variant=\"table\" />\n ) : (\n <DataTable\n table={{ ...manager.table, heading: manager.table.heading ?? manager.title }}\n component={manager.component}\n />\n )}\n <Slot name=\"resource.relation-manager.after\" scope={manager.key} />\n </section>\n )\n }\n\n return (\n <Tabs value={active} onValueChange={changeTab}>\n <TabsList>\n <Slot name=\"resource.tabs.start\" />\n {tabs.map((tab) => (\n <TabsTrigger key={tab.key} value={tab.key}>\n {tab.icon && <Icon name={tab.icon} />}\n {tab.title}\n {tab.badge !== undefined && tab.badge !== null && tab.badge !== '' && (\n <Badge\n variant=\"secondary\"\n title={tab.badgeTooltip}\n className={cn(\n 'ms-1 px-1.5 py-0',\n tab.badgeColor && colorClass(badgeColorClasses, tab.badgeColor, 'gray'),\n )}\n >\n {tab.badge}\n {tab.badgeTooltip && <span className=\"sr-only\">{` (${tab.badgeTooltip})`}</span>}\n </Badge>\n )}\n </TabsTrigger>\n ))}\n <Slot name=\"resource.tabs.end\" />\n </TabsList>\n {tabs.map((tab) => (\n <TabsContent\n key={tab.key}\n value={tab.key}\n className=\"flex flex-col gap-6 pt-2 data-[state=inactive]:hidden\"\n data-slot={tab.group ? 'relation-group' : tab.content ? 'relation-content' : undefined}\n // The form keeps its unsaved state while another tab is open.\n forceMount={tab.content ? true : undefined}\n >\n {/* The page's form / infolist (`hasCombinedRelationManagerTabsWithContent()`). */}\n {tab.content && <ContentBlockRenderer block={tab.content as ContentBlock} />}\n {tab.managers.map((manager) => (\n <Fragment key={manager.key}>\n <Slot name=\"resource.relation-manager.before\" scope={manager.key} />\n {isLazyPlaceholder(manager) ? (\n <LazyPlaceholder variant=\"table\" />\n ) : (\n <DataTable\n // Inside a group each table keeps its own heading; a lone manager's is the tab.\n table={{\n ...manager.table,\n heading: tab.group ? (manager.table.heading ?? manager.title) : undefined,\n }}\n component={manager.component}\n />\n )}\n <Slot name=\"resource.relation-manager.after\" scope={manager.key} />\n </Fragment>\n ))}\n </TabsContent>\n ))}\n </Tabs>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"page.js","names":["FormBlock"],"sources":["../../src/pages/page.tsx"],"sourcesContent":["import type {\n AlfakitPageProps,\n ContentBlock,\n FormBlock as FormBlockSchema,\n InfolistBlock,\n LinksBlock,\n PageSchema,\n RelationManagersBlock,\n SchemaBlock,\n TableBlock as TableBlockSchema,\n WidgetsBlock as WidgetsBlockSchema,\n} from '@alfakit/core'\nimport {\n applyBodyAttributes,\n customPageProps,\n hasRelationManagerTabs,\n isLazyPlaceholder,\n openDefaultAction,\n RELATION_QUERY_KEY,\n relationManagerTabs,\n relationTabFromQuery,\n relationTabSlug,\n resolveLazyComponents,\n type DefaultActionOpener,\n} from '@alfakit/core'\nimport { Head, Link, usePage } from '@inertiajs/react'\nimport {\n createContext,\n createElement,\n Fragment,\n useContext,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react'\nimport { PageActionProvider } from '../components/actions/action-context'\nimport { ActionList } from '../components/actions/action-list'\nimport { FormBlock } from '../components/form/form-block'\nimport { KeyboardShortcuts } from '../components/panel/keyboard-shortcuts'\nimport { LazyPlaceholder } from '../components/panel/lazy-placeholder'\nimport { Slot } from '../components/panel/slot'\nimport { SubNavigationLayout } from '../components/panel/sub-navigation'\nimport { ConflictBanner } from '../components/realtime/conflict-banner'\nimport { RecordPresence } from '../components/realtime/record-presence'\nimport { SchemaRenderer } from '../components/schema/schema-renderer'\nimport { ListTabs, ListTabsPanel } from '../components/table/list-tabs'\nimport { DataTable } from '../components/table/table-block'\nimport { Badge } from '../components/ui/badge'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs'\nimport { WidgetGrid } from '../components/widgets/widgets-block'\nimport {\n useActionRunner,\n useAlfakit,\n useAlfakitProps,\n usePanel,\n useT,\n type AnyComponent,\n} from '../context'\nimport { BlankLayout } from '../layouts/blank-layout'\nimport { badgeColorClasses, colorClass } from '../lib/colors'\nimport { Icon } from '../lib/icons'\nimport { cn } from '../lib/utils'\n\n/**\n * `Alfakit/Page`: renders any schema-driven page. Custom pages (`Page::$component`) are\n * resolved from the `pages` map passed to `createAlfakit()`.\n */\nexport function AlfakitPage() {\n const props = useAlfakitProps()\n const panel = usePanel()\n const t = useT()\n const { pages } = useAlfakit()\n // Lazy widgets and relation managers arrive in a deferred prop after the first response.\n const page = useMemo(\n () => (props.page ? resolveLazyComponents(props.page, props.alfakitLazy) : undefined),\n [props.page, props.alfakitLazy],\n )\n\n useDefaultAction(page)\n\n if (!page) {\n return null\n }\n\n // Starter kit mode: the host's `title` callback adds the application name.\n const title = panel.starterKit ? page.title : `${page.title} - ${panel.brandName}`\n\n if (page.view) {\n const Custom = pages[page.view.component]\n\n if (!Custom) {\n return (\n <div className=\"text-destructive p-6\">\n {t(\n 'page_not_registered',\n { component: page.view.component },\n `Page component ${page.view.component} is not registered in createAlfakit({ pages }).`,\n )}\n </div>\n )\n }\n\n return (\n <PageActionProvider value={{ token: page.token, component: 'page' }}>\n <Head title={title} />\n {/* The component brings its own PageShell (as in panel mode, where it is the Inertia\n page itself) and takes the live props, so `reloadProps()` reaches it. */}\n <Custom\n {...customPageProps(page.view.props, props as Record<string, unknown>)}\n page={page}\n />\n </PageActionProvider>\n )\n }\n\n return (\n <PageActionProvider value={{ token: page.token, component: 'page' }}>\n <Head title={title} />\n <PageShell page={page}>\n <PageContent page={page} />\n </PageShell>\n </PageActionProvider>\n )\n}\n\n/**\n * Opens the page's default action (`$defaultAction`, `?tableAction=`) once the page has\n * hydrated, and once per page: re-renders after the action ran don't open it again.\n */\nexport function useDefaultAction(page: PageSchema | undefined) {\n const runner = useActionRunner()\n const opener = useRef<DefaultActionOpener>({ opened: null })\n\n useEffect(() => {\n openDefaultAction(page, opener.current, (action, context) => runner.trigger(action, context))\n }, [page, runner])\n}\n\n/** Sets the page's `$extraBodyAttributes` on `<body>` while it is open. */\nexport function useBodyAttributes(attributes: Record<string, string> | undefined) {\n const key = attributes ? JSON.stringify(attributes) : ''\n\n useEffect(\n () => applyBodyAttributes(key ? (JSON.parse(key) as Record<string, string>) : undefined),\n [key],\n )\n}\n\n/**\n * The registered layout: `createAlfakit()` registers the panel's (`panelLayouts`), the starter\n * kit its content layout. Without one the page renders bare: importing the panel layouts here\n * would put the sidebar and top bar in every starter kit page.\n */\nfunction layout(registry: { resolve(key: string): AnyComponent | undefined }, key: string) {\n return registry.resolve(key) ?? BlankLayout\n}\n\ntype LayoutKind = 'app' | 'auth' | 'blank' | 'fullscreen'\n\nfunction layoutKind(page: PageSchema): LayoutKind {\n return page.layout === 'auth' || page.layout === 'blank' || page.layout === 'fullscreen'\n ? page.layout\n : 'app'\n}\n\n/** The layout `PanelLayoutHost` already renders around the page, if any. */\nconst HostedLayoutContext = createContext<LayoutKind | null>(null)\n\n/** The page's registered layout (`layout:app`, `layout:auth`…) around `children`. */\nfunction PanelLayout({ page, children }: { page: PageSchema; children: ReactNode }) {\n const { registry } = useAlfakit()\n const kind = layoutKind(page)\n\n // Each layout can be replaced: createAlfakit({ components: { 'layout:app': MyLayout } }).\n if (kind === 'auth') {\n return createElement(\n layout(registry, 'layout:auth'),\n { heading: page.heading ?? page.title, subheading: page.subheading, page },\n children,\n )\n }\n\n if (kind === 'app') {\n return createElement(\n layout(registry, 'layout:app'),\n { breadcrumbs: page.breadcrumbs, maxContentWidth: page.maxContentWidth, page },\n children,\n )\n }\n\n return createElement(layout(registry, `layout:${kind}`), { page }, children)\n}\n\n/**\n * Keeps the panel layout (sidebar, top bar) mounted while the user moves between Alfakit\n * pages. Inertia remounts the page component on every visit; this sits in the persistent\n * Inertia layout (`AlfakitRoot`) and renders the open page's layout from its props, so only\n * the page part below it (heading, actions, content) is replaced. The `PageShell` inside\n * the page then renders just that part. Custom page components (their own Inertia page or\n * `page.view`) and starter kit pages render their layout through `PageShell` as before.\n */\nexport function PanelLayoutHost({ children }: { children: ReactNode }) {\n const { component, props } = usePage<AlfakitPageProps>()\n const page = useMemo(\n () => (props.page ? resolveLazyComponents(props.page, props.alfakitLazy) : undefined),\n [props.page, props.alfakitLazy],\n )\n const hosted =\n component === 'Alfakit/Page' && page && !page.view && props.alfakit?.starterKit !== true\n ? page\n : undefined\n\n return (\n <HostedLayoutContext.Provider value={hosted ? layoutKind(hosted) : null}>\n {hosted ? <PanelLayout page={hosted}>{children}</PanelLayout> : children}\n </HostedLayoutContext.Provider>\n )\n}\n\nexport function PageShell({ page, children }: { page: PageSchema; children: ReactNode }) {\n const { registry } = useAlfakit()\n const hosted = useContext(HostedLayoutContext)\n const kind = layoutKind(page)\n\n useBodyAttributes(page.extraBodyAttributes)\n\n const content =\n kind === 'app' ? (\n <>\n {/* `?` overlay, `/`, `g d` and panel command shortcuts (`keyboardShortcuts()`). */}\n <KeyboardShortcuts />\n <Slot name=\"page.start\" />\n <PageHeader page={page} />\n {/* Edit pages: somebody else saved the record (`detectConflicts()`, `Panel::realtime()`). */}\n {page.conflicts &&\n createElement(registry.resolve('page:conflict-banner') ?? ConflictBanner, { page })}\n {page.subNavigation ? (\n <SubNavigationLayout navigation={page.subNavigation}>{children}</SubNavigationLayout>\n ) : (\n children\n )}\n <Slot name=\"page.end\" />\n </>\n ) : (\n children\n )\n\n // The persistent layout around the page already draws this layout.\n if (hosted === kind) {\n return content\n }\n\n return <PanelLayout page={page}>{content}</PanelLayout>\n}\n\nexport function PageHeader({ page }: { page: PageSchema }) {\n const { registry } = useAlfakit()\n const actions = page.headerActions ?? []\n // `$headerActionsAlignment`: the default (end) sits beside the heading; the others\n // stack the actions under it.\n const alignment = page.headerActionsAlignment\n\n return (\n <>\n <Slot name=\"page.header.before\" />\n <header\n data-slot=\"page-header\"\n data-actions-alignment={alignment}\n className={cn(\n 'flex flex-col gap-4',\n !alignment && 'sm:flex-row sm:items-start sm:justify-between',\n )}\n >\n <div className=\"min-w-0\">\n <Slot name=\"page.heading.before\" />\n <h1 className=\"text-2xl font-semibold tracking-tight\">{page.heading ?? page.title}</h1>\n {page.subheading && (\n <p className=\"text-muted-foreground mt-1 text-sm\">{page.subheading}</p>\n )}\n <Slot name=\"page.heading.after\" />\n </div>\n <div\n className={cn(\n 'flex shrink-0 flex-wrap items-center gap-2',\n alignment === 'start'\n ? 'justify-start'\n : alignment === 'center'\n ? 'justify-center'\n : alignment === 'between' || alignment === 'justify'\n ? 'justify-between'\n : 'justify-end',\n )}\n >\n {/* Record pages with `Panel::realtime()`: who else is on the record. */}\n {page.presence &&\n createElement(registry.resolve('page:presence') ?? RecordPresence, {\n presence: page.presence,\n })}\n <Slot name=\"page.actions.before\" />\n {actions.length > 0 && (\n <ActionList\n actions={actions}\n context={{ token: page.token, component: 'page' }}\n align=\"end\"\n className=\"max-w-full shrink-0\"\n />\n )}\n <Slot name=\"page.actions.after\" />\n </div>\n </header>\n <Slot name=\"page.header.after\" />\n </>\n )\n}\n\nexport function PageContent({ page }: { page: PageSchema }) {\n return (\n <div className=\"flex flex-col gap-6\">\n {page.content.map((block) => (\n <ContentBlockRenderer key={block.key} block={block} />\n ))}\n </div>\n )\n}\n\nexport function ContentBlockRenderer({ block }: { block: ContentBlock }) {\n const { registry } = useAlfakit()\n // Links list tabs to the table they control.\n const tabsIdBase = `alfakit-list-tabs-${useId().replace(/[^A-Za-z0-9_-]/g, '')}`\n\n switch (block.type) {\n case 'Form':\n return <FormBlock block={block as FormBlockSchema} />\n case 'Infolist':\n return (\n <div data-slot=\"infolist\" className=\"contents\">\n <SchemaRenderer\n components={(block as InfolistBlock).components}\n columns={(block as InfolistBlock).columns ?? { default: 1, lg: 2 }}\n />\n </div>\n )\n case 'Schema':\n return (\n <SchemaRenderer components={(block as SchemaBlock).components} columns={{ default: 1 }} />\n )\n case 'Table': {\n const table = block as TableBlockSchema\n const content = table.tabs?.length ? (\n <div className=\"flex flex-col gap-4\">\n <ListTabs\n tabs={table.tabs}\n activeTab={table.activeTab}\n tabsQueryKey={table.tabsQueryKey}\n label={table.pluralModelLabel}\n idBase={tabsIdBase}\n />\n <ListTabsPanel idBase={tabsIdBase} activeTab={table.activeTab}>\n <DataTable table={table} component={table.component} />\n </ListTabsPanel>\n </div>\n ) : (\n <DataTable table={table} component={table.component} />\n )\n\n if (block.key !== 'table') {\n return content\n }\n\n // The page's main table: the server sends these slots on List / ManageRelatedRecords pages only.\n return (\n <>\n <Slot name=\"resource.list-records.table.before\" />\n <Slot name=\"resource.manage-related-records.table.before\" />\n {content}\n <Slot name=\"resource.manage-related-records.table.after\" />\n <Slot name=\"resource.list-records.table.after\" />\n </>\n )\n }\n case 'RelationManagers':\n return <RelationManagers block={block as RelationManagersBlock} />\n case 'Widgets':\n return block.key === 'headerWidgets' ? (\n <>\n <Slot name=\"page.header-widgets.before\" />\n <WidgetGrid block={block as WidgetsBlockSchema} />\n <Slot name=\"page.header-widgets.after\" />\n </>\n ) : block.key === 'footerWidgets' ? (\n <>\n <Slot name=\"page.footer-widgets.before\" />\n <WidgetGrid block={block as WidgetsBlockSchema} />\n <Slot name=\"page.footer-widgets.after\" />\n </>\n ) : (\n <WidgetGrid block={block as WidgetsBlockSchema} />\n )\n case 'Links':\n return (\n <div className=\"flex flex-col items-center gap-1 text-center text-sm\">\n {(block as LinksBlock).links.map((link) => (\n <Link\n key={link.url}\n href={link.url}\n className=\"text-primary underline-offset-4 hover:underline\"\n >\n {link.label}\n </Link>\n ))}\n </div>\n )\n default: {\n const Custom = registry.resolve(block.type)\n\n return Custom ? createElement(Custom, { block }) : null\n }\n }\n}\n\nfunction RelationManagers({ block }: { block: RelationManagersBlock }) {\n const tabs = useMemo(() => relationManagerTabs(block), [block])\n // `?relation=`: a key in `getRelations()` or a position. The page URL (not\n // `window.location`) so the server renders the same tab as the browser.\n const url = usePage().url\n const [active, setActive] = useState(\n () =>\n relationTabFromQuery(\n tabs,\n new URL(url, 'http://localhost').searchParams.get(RELATION_QUERY_KEY),\n )?.key ??\n tabs[0]?.key ??\n '',\n )\n\n const changeTab = (key: string) => {\n setActive(key)\n\n if (typeof window === 'undefined') return\n\n const next = new URL(window.location.href)\n const slug = relationTabSlug(tabs, key)\n\n if (slug === null) next.searchParams.delete(RELATION_QUERY_KEY)\n else next.searchParams.set(RELATION_QUERY_KEY, slug)\n\n window.history.replaceState(window.history.state, '', next)\n }\n\n if (block.managers.length === 0) {\n return null\n }\n\n if (!hasRelationManagerTabs(block)) {\n const manager = block.managers[0]!\n\n return (\n <section className=\"flex flex-col gap-3\">\n <Slot name=\"resource.relation-manager.before\" scope={manager.key} />\n {isLazyPlaceholder(manager) ? (\n <LazyPlaceholder variant=\"table\" />\n ) : (\n <DataTable\n table={{ ...manager.table, heading: manager.table.heading ?? manager.title }}\n component={manager.component}\n />\n )}\n <Slot name=\"resource.relation-manager.after\" scope={manager.key} />\n </section>\n )\n }\n\n return (\n <Tabs value={active} onValueChange={changeTab}>\n <TabsList>\n <Slot name=\"resource.tabs.start\" />\n {tabs.map((tab) => (\n <TabsTrigger key={tab.key} value={tab.key}>\n {tab.icon && <Icon name={tab.icon} />}\n {tab.title}\n {tab.badge !== undefined && tab.badge !== null && tab.badge !== '' && (\n <Badge\n variant=\"secondary\"\n title={tab.badgeTooltip}\n className={cn(\n 'ms-1 px-1.5 py-0',\n tab.badgeColor && colorClass(badgeColorClasses, tab.badgeColor, 'gray'),\n )}\n >\n {tab.badge}\n {tab.badgeTooltip && <span className=\"sr-only\">{` (${tab.badgeTooltip})`}</span>}\n </Badge>\n )}\n </TabsTrigger>\n ))}\n <Slot name=\"resource.tabs.end\" />\n </TabsList>\n {tabs.map((tab) => (\n <TabsContent\n key={tab.key}\n value={tab.key}\n className=\"flex flex-col gap-6 pt-2 data-[state=inactive]:hidden\"\n data-slot={tab.group ? 'relation-group' : tab.content ? 'relation-content' : undefined}\n // The form keeps its unsaved state while another tab is open.\n forceMount={tab.content ? true : undefined}\n >\n {/* The page's form / infolist (`hasCombinedRelationManagerTabsWithContent()`). */}\n {tab.content && <ContentBlockRenderer block={tab.content as ContentBlock} />}\n {tab.managers.map((manager) => (\n <Fragment key={manager.key}>\n <Slot name=\"resource.relation-manager.before\" scope={manager.key} />\n {isLazyPlaceholder(manager) ? (\n <LazyPlaceholder variant=\"table\" />\n ) : (\n <DataTable\n // Inside a group each table keeps its own heading; a lone manager's is the tab.\n table={{\n ...manager.table,\n heading: tab.group ? (manager.table.heading ?? manager.title) : undefined,\n }}\n component={manager.component}\n />\n )}\n <Slot name=\"resource.relation-manager.after\" scope={manager.key} />\n </Fragment>\n ))}\n </TabsContent>\n ))}\n </Tabs>\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 dir=\"ltr\" className=\"bg-muted hidden rounded px-1 font-mono text-[10px] sm:inline\">\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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"}
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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"}