@velastack/cms 0.4.0 → 0.5.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 (127) hide show
  1. package/README.md +107 -18
  2. package/dist/backend/index.d.ts +1 -1
  3. package/dist/backend/routes/seed.d.ts +6 -0
  4. package/dist/backend/routes/seed.js +96 -0
  5. package/dist/backend/routes/table.js +3 -0
  6. package/dist/backend/store/queries.d.ts +6 -1
  7. package/dist/backend/store/queries.js +89 -0
  8. package/dist/cms.js +13 -16
  9. package/dist/components/admin-bar/admin-bar-internal.svelte +4 -4
  10. package/dist/components/admin-bar/admin-bar.css +1 -1
  11. package/dist/components/admin-bar/field-input.svelte +220 -0
  12. package/dist/components/admin-bar/field-input.svelte.d.ts +29 -0
  13. package/dist/components/admin-bar/icons/pencil.svelte +19 -0
  14. package/dist/components/admin-bar/icons/pencil.svelte.d.ts +3 -0
  15. package/dist/components/admin-bar/keyboard-shortcuts-dialog.svelte +1 -1
  16. package/dist/components/admin-bar/locales-panel.svelte +42 -1
  17. package/dist/components/admin-bar/page-config.d.ts +8 -1
  18. package/dist/components/admin-bar/page-config.js +15 -2
  19. package/dist/components/admin-bar/reorder-buttons.svelte +52 -0
  20. package/dist/components/admin-bar/reorder-buttons.svelte.d.ts +10 -0
  21. package/dist/components/admin-bar/seo-panel.svelte +144 -189
  22. package/dist/components/admin-bar/site-options-panel.svelte +115 -0
  23. package/dist/components/admin-bar/site-options-panel.svelte.d.ts +7 -0
  24. package/dist/components/admin-bar/structured-editor-popover.svelte +250 -0
  25. package/dist/components/admin-bar/structured-editor-popover.svelte.d.ts +48 -0
  26. package/dist/components/admin-bar/ui/checkbox/checkbox.svelte +36 -0
  27. package/dist/components/admin-bar/ui/checkbox/checkbox.svelte.d.ts +5 -0
  28. package/dist/components/admin-bar/ui/checkbox/index.d.ts +2 -0
  29. package/dist/components/admin-bar/ui/checkbox/index.js +2 -0
  30. package/dist/components/admin-bar/ui/popover/index.d.ts +5 -0
  31. package/dist/components/admin-bar/ui/popover/index.js +7 -0
  32. package/dist/components/admin-bar/ui/popover/popover-close.svelte +7 -0
  33. package/dist/components/admin-bar/ui/popover/popover-close.svelte.d.ts +4 -0
  34. package/dist/components/admin-bar/ui/popover/popover-content.svelte +35 -0
  35. package/dist/components/admin-bar/ui/popover/popover-content.svelte.d.ts +10 -0
  36. package/dist/components/admin-bar/ui/popover/popover-trigger.svelte +7 -0
  37. package/dist/components/admin-bar/ui/popover/popover-trigger.svelte.d.ts +4 -0
  38. package/dist/components/admin-bar/ui/popover/popover.svelte +7 -0
  39. package/dist/components/admin-bar/ui/popover/popover.svelte.d.ts +3 -0
  40. package/dist/components/admin-bar/ui/sheet/index.d.ts +10 -0
  41. package/dist/components/admin-bar/ui/sheet/index.js +12 -0
  42. package/dist/components/admin-bar/ui/sheet/sheet-close.svelte +7 -0
  43. package/dist/components/admin-bar/ui/sheet/sheet-close.svelte.d.ts +4 -0
  44. package/dist/components/admin-bar/ui/sheet/sheet-content.svelte +64 -0
  45. package/dist/components/admin-bar/ui/sheet/sheet-content.svelte.d.ts +17 -0
  46. package/dist/components/admin-bar/ui/sheet/sheet-description.svelte +17 -0
  47. package/dist/components/admin-bar/ui/sheet/sheet-description.svelte.d.ts +4 -0
  48. package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte +23 -0
  49. package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  50. package/dist/components/admin-bar/ui/sheet/sheet-header.svelte +20 -0
  51. package/dist/components/admin-bar/ui/sheet/sheet-header.svelte.d.ts +5 -0
  52. package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte +20 -0
  53. package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
  54. package/dist/components/admin-bar/ui/sheet/sheet-title.svelte +17 -0
  55. package/dist/components/admin-bar/ui/sheet/sheet-title.svelte.d.ts +4 -0
  56. package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte +7 -0
  57. package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
  58. package/dist/components/admin-bar/ui/sheet/sheet.svelte +7 -0
  59. package/dist/components/admin-bar/ui/sheet/sheet.svelte.d.ts +3 -0
  60. package/dist/components/admin-bar/ui/switch/index.d.ts +2 -0
  61. package/dist/components/admin-bar/ui/switch/index.js +2 -0
  62. package/dist/components/admin-bar/ui/switch/switch.svelte +27 -0
  63. package/dist/components/admin-bar/ui/switch/switch.svelte.d.ts +5 -0
  64. package/dist/components/admin-bar/ui/tabs/index.d.ts +5 -0
  65. package/dist/components/admin-bar/ui/tabs/index.js +7 -0
  66. package/dist/components/admin-bar/ui/tabs/tabs-content.svelte +17 -0
  67. package/dist/components/admin-bar/ui/tabs/tabs-content.svelte.d.ts +4 -0
  68. package/dist/components/admin-bar/ui/tabs/tabs-list.svelte +16 -0
  69. package/dist/components/admin-bar/ui/tabs/tabs-list.svelte.d.ts +4 -0
  70. package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte +20 -0
  71. package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  72. package/dist/components/admin-bar/ui/tabs/tabs.svelte +19 -0
  73. package/dist/components/admin-bar/ui/tabs/tabs.svelte.d.ts +4 -0
  74. package/dist/components/cms/cms-boolean.svelte +9 -18
  75. package/dist/components/cms/cms-boolean.svelte.d.ts +1 -0
  76. package/dist/components/cms/cms-date-time-editable.svelte +3 -3
  77. package/dist/components/cms/cms-date-time.svelte +17 -17
  78. package/dist/components/cms/cms-date-time.svelte.d.ts +2 -1
  79. package/dist/components/cms/cms-image-editable.svelte +13 -4
  80. package/dist/components/cms/cms-image.svelte +37 -30
  81. package/dist/components/cms/cms-image.svelte.d.ts +11 -4
  82. package/dist/components/cms/cms-link.svelte +26 -19
  83. package/dist/components/cms/cms-link.svelte.d.ts +2 -0
  84. package/dist/components/cms/cms-markdown.svelte +15 -16
  85. package/dist/components/cms/cms-markdown.svelte.d.ts +1 -0
  86. package/dist/components/cms/cms-number-editable.svelte +4 -2
  87. package/dist/components/cms/cms-number.svelte +18 -17
  88. package/dist/components/cms/cms-number.svelte.d.ts +2 -1
  89. package/dist/components/cms/cms-repeater.svelte +15 -18
  90. package/dist/components/cms/cms-repeater.svelte.d.ts +1 -0
  91. package/dist/components/cms/cms-rich-text.svelte +17 -22
  92. package/dist/components/cms/cms-rich-text.svelte.d.ts +1 -0
  93. package/dist/components/cms/cms-store.svelte.d.ts +14 -0
  94. package/dist/components/cms/cms-store.svelte.js +39 -2
  95. package/dist/components/cms/cms-text.svelte +23 -30
  96. package/dist/components/cms/cms-text.svelte.d.ts +3 -1
  97. package/dist/components/cms/scope.d.ts +25 -5
  98. package/dist/components/cms/structured-registry.d.ts +10 -0
  99. package/dist/components/cms/structured-registry.js +6 -0
  100. package/dist/components/cms/use-cms-field.svelte.d.ts +54 -0
  101. package/dist/components/cms/use-cms-field.svelte.js +83 -0
  102. package/dist/core/metadata.d.ts +44 -0
  103. package/dist/core/metadata.js +59 -0
  104. package/dist/core/structured.d.ts +108 -0
  105. package/dist/core/structured.js +184 -0
  106. package/dist/core/wire.d.ts +31 -1
  107. package/dist/editor.d.ts +22 -0
  108. package/dist/editor.js +21 -0
  109. package/dist/index.d.ts +12 -4
  110. package/dist/index.js +13 -3
  111. package/dist/server/load-cms.js +1 -0
  112. package/package.json +12 -3
  113. package/plugin/README.md +22 -2
  114. package/plugin/dist/build.d.ts +74 -0
  115. package/plugin/dist/build.js +210 -0
  116. package/plugin/dist/build.js.map +1 -0
  117. package/plugin/dist/index.d.ts +10 -1
  118. package/plugin/dist/index.js +63 -4
  119. package/plugin/dist/index.js.map +1 -1
  120. package/plugin/dist/manifest.d.ts +31 -0
  121. package/plugin/dist/manifest.js +146 -41
  122. package/plugin/dist/manifest.js.map +1 -1
  123. package/plugin/dist/parse-svelte.d.ts +15 -0
  124. package/plugin/dist/parse-svelte.js +26 -16
  125. package/plugin/dist/parse-svelte.js.map +1 -1
  126. package/dist/components/admin-bar/site-settings-panel.svelte +0 -225
  127. package/dist/components/admin-bar/site-settings-panel.svelte.d.ts +0 -7
@@ -0,0 +1,5 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import { type WithElementRef } from '../../utils.js';
3
+ declare const SheetHeader: import("svelte").Component<WithElementRef<HTMLAttributes<HTMLDivElement>>, {}, "ref">;
4
+ type SheetHeader = ReturnType<typeof SheetHeader>;
5
+ export default SheetHeader;
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ import { Dialog as SheetPrimitive } from 'bits-ui';
3
+ import { cn } from '../../utils.js';
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ ...restProps
9
+ }: SheetPrimitive.OverlayProps = $props();
10
+ </script>
11
+
12
+ <SheetPrimitive.Overlay
13
+ bind:ref
14
+ data-slot="sheet-overlay"
15
+ class={cn(
16
+ 'vela:fixed vela:inset-0 vela:z-[9998] vela:bg-black/50 vela:data-[state=open]:animate-in vela:data-[state=closed]:animate-out vela:data-[state=closed]:fade-out-0 vela:data-[state=open]:fade-in-0',
17
+ className
18
+ )}
19
+ {...restProps}
20
+ />
@@ -0,0 +1,4 @@
1
+ import { Dialog as SheetPrimitive } from 'bits-ui';
2
+ declare const SheetOverlay: import("svelte").Component<SheetPrimitive.OverlayProps, {}, "ref">;
3
+ type SheetOverlay = ReturnType<typeof SheetOverlay>;
4
+ export default SheetOverlay;
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ import { Dialog as SheetPrimitive } from 'bits-ui';
3
+ import { cn } from '../../utils.js';
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ ...restProps
9
+ }: SheetPrimitive.TitleProps = $props();
10
+ </script>
11
+
12
+ <SheetPrimitive.Title
13
+ bind:ref
14
+ data-slot="sheet-title"
15
+ class={cn('vela:text-[15px] vela:font-semibold vela:m-0', className)}
16
+ {...restProps}
17
+ />
@@ -0,0 +1,4 @@
1
+ import { Dialog as SheetPrimitive } from 'bits-ui';
2
+ declare const SheetTitle: import("svelte").Component<SheetPrimitive.TitleProps, {}, "ref">;
3
+ type SheetTitle = ReturnType<typeof SheetTitle>;
4
+ export default SheetTitle;
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ import { Dialog as SheetPrimitive } from 'bits-ui';
3
+
4
+ let { ref = $bindable(null), ...restProps }: SheetPrimitive.TriggerProps = $props();
5
+ </script>
6
+
7
+ <SheetPrimitive.Trigger bind:ref data-slot="sheet-trigger" {...restProps} />
@@ -0,0 +1,4 @@
1
+ import { Dialog as SheetPrimitive } from 'bits-ui';
2
+ declare const SheetTrigger: import("svelte").Component<SheetPrimitive.TriggerProps, {}, "ref">;
3
+ type SheetTrigger = ReturnType<typeof SheetTrigger>;
4
+ export default SheetTrigger;
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ import { Dialog as SheetPrimitive } from 'bits-ui';
3
+
4
+ let { open = $bindable(false), ...restProps }: SheetPrimitive.RootProps = $props();
5
+ </script>
6
+
7
+ <SheetPrimitive.Root bind:open {...restProps} />
@@ -0,0 +1,3 @@
1
+ declare const Sheet: import("svelte").Component<import("bits-ui").AlertDialogRootPropsWithoutHTML, {}, "open">;
2
+ type Sheet = ReturnType<typeof Sheet>;
3
+ export default Sheet;
@@ -0,0 +1,2 @@
1
+ import Root from './switch.svelte';
2
+ export { Root, Root as Switch };
@@ -0,0 +1,2 @@
1
+ import Root from './switch.svelte';
2
+ export { Root, Root as Switch };
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import { Switch as SwitchPrimitive } from 'bits-ui';
3
+ import { cn, type WithoutChildrenOrChild } from '../../utils.js';
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ checked = $bindable(false),
8
+ class: className,
9
+ ...restProps
10
+ }: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
11
+ </script>
12
+
13
+ <SwitchPrimitive.Root
14
+ bind:ref
15
+ bind:checked
16
+ data-slot="switch"
17
+ class={cn(
18
+ 'vela:peer vela:inline-flex vela:h-5 vela:w-9 vela:shrink-0 vela:items-center vela:rounded-full vela:border vela:border-transparent vela:transition-colors vela:cursor-pointer vela:focus-visible:outline-none vela:focus-visible:ring-2 vela:focus-visible:ring-[var(--cms-accent)] vela:disabled:cursor-not-allowed vela:disabled:opacity-50 vela:data-[state=checked]:bg-[var(--cms-accent)] vela:data-[state=unchecked]:bg-[var(--cms-bar-divider)]',
19
+ className
20
+ )}
21
+ {...restProps}
22
+ >
23
+ <SwitchPrimitive.Thumb
24
+ data-slot="switch-thumb"
25
+ class="vela:pointer-events-none vela:block vela:size-4 vela:rounded-full vela:bg-white vela:shadow vela:transition-transform vela:data-[state=checked]:translate-x-4 vela:data-[state=unchecked]:translate-x-0.5"
26
+ />
27
+ </SwitchPrimitive.Root>
@@ -0,0 +1,5 @@
1
+ import { Switch as SwitchPrimitive } from 'bits-ui';
2
+ import { type WithoutChildrenOrChild } from '../../utils.js';
3
+ declare const Switch: import("svelte").Component<WithoutChildrenOrChild<SwitchPrimitive.RootProps>, {}, "ref" | "checked">;
4
+ type Switch = ReturnType<typeof Switch>;
5
+ export default Switch;
@@ -0,0 +1,5 @@
1
+ import Root from './tabs.svelte';
2
+ import Content from './tabs-content.svelte';
3
+ import List from './tabs-list.svelte';
4
+ import Trigger from './tabs-trigger.svelte';
5
+ export { Root, Content, List, Trigger, Root as Tabs, Content as TabsContent, List as TabsList, Trigger as TabsTrigger };
@@ -0,0 +1,7 @@
1
+ import Root from './tabs.svelte';
2
+ import Content from './tabs-content.svelte';
3
+ import List from './tabs-list.svelte';
4
+ import Trigger from './tabs-trigger.svelte';
5
+ export { Root, Content, List, Trigger,
6
+ //
7
+ Root as Tabs, Content as TabsContent, List as TabsList, Trigger as TabsTrigger };
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ import { Tabs as TabsPrimitive } from 'bits-ui';
3
+ import { cn } from '../../utils.js';
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ ...restProps
9
+ }: TabsPrimitive.ContentProps = $props();
10
+ </script>
11
+
12
+ <TabsPrimitive.Content
13
+ bind:ref
14
+ data-slot="tabs-content"
15
+ class={cn('vela:flex-1 vela:outline-none', className)}
16
+ {...restProps}
17
+ />
@@ -0,0 +1,4 @@
1
+ import { Tabs as TabsPrimitive } from 'bits-ui';
2
+ declare const TabsContent: import("svelte").Component<TabsPrimitive.ContentProps, {}, "ref">;
3
+ type TabsContent = ReturnType<typeof TabsContent>;
4
+ export default TabsContent;
@@ -0,0 +1,16 @@
1
+ <script lang="ts">
2
+ import { Tabs as TabsPrimitive } from 'bits-ui';
3
+ import { cn } from '../../utils.js';
4
+
5
+ let { ref = $bindable(null), class: className, ...restProps }: TabsPrimitive.ListProps = $props();
6
+ </script>
7
+
8
+ <TabsPrimitive.List
9
+ bind:ref
10
+ data-slot="tabs-list"
11
+ class={cn(
12
+ 'vela:inline-flex vela:self-start vela:items-center vela:justify-center vela:p-0.5 vela:rounded-md vela:bg-[var(--cms-bar-bg-hover)] vela:border vela:border-[var(--cms-bar-divider)]',
13
+ className
14
+ )}
15
+ {...restProps}
16
+ />
@@ -0,0 +1,4 @@
1
+ import { Tabs as TabsPrimitive } from 'bits-ui';
2
+ declare const TabsList: import("svelte").Component<TabsPrimitive.ListProps, {}, "ref">;
3
+ type TabsList = ReturnType<typeof TabsList>;
4
+ export default TabsList;
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ import { Tabs as TabsPrimitive } from 'bits-ui';
3
+ import { cn } from '../../utils.js';
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ ...restProps
9
+ }: TabsPrimitive.TriggerProps = $props();
10
+ </script>
11
+
12
+ <TabsPrimitive.Trigger
13
+ bind:ref
14
+ data-slot="tabs-trigger"
15
+ class={cn(
16
+ 'vela:inline-flex vela:items-center vela:justify-center vela:gap-1.5 vela:px-2.5 vela:py-1 vela:rounded vela:text-[11px] vela:font-medium vela:whitespace-nowrap vela:cursor-pointer vela:transition-colors vela:text-bar-text-secondary vela:hover:text-bar-text vela:data-[state=active]:bg-bar-bg vela:data-[state=active]:text-bar-text vela:focus-visible:outline-none vela:focus-visible:ring-2 vela:focus-visible:ring-[var(--cms-accent)] vela:disabled:pointer-events-none vela:disabled:opacity-50',
17
+ className
18
+ )}
19
+ {...restProps}
20
+ />
@@ -0,0 +1,4 @@
1
+ import { Tabs as TabsPrimitive } from 'bits-ui';
2
+ declare const TabsTrigger: import("svelte").Component<TabsPrimitive.TriggerProps, {}, "ref">;
3
+ type TabsTrigger = ReturnType<typeof TabsTrigger>;
4
+ export default TabsTrigger;
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import { Tabs as TabsPrimitive } from 'bits-ui';
3
+ import { cn } from '../../utils.js';
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ value = $bindable(),
8
+ class: className,
9
+ ...restProps
10
+ }: TabsPrimitive.RootProps = $props();
11
+ </script>
12
+
13
+ <TabsPrimitive.Root
14
+ bind:ref
15
+ bind:value
16
+ data-slot="tabs"
17
+ class={cn('vela:flex vela:flex-col vela:gap-2', className)}
18
+ {...restProps}
19
+ />
@@ -0,0 +1,4 @@
1
+ import { Tabs as TabsPrimitive } from 'bits-ui';
2
+ declare const Tabs: import("svelte").Component<TabsPrimitive.RootProps, {}, "value" | "ref">;
3
+ type Tabs = ReturnType<typeof Tabs>;
4
+ export default Tabs;
@@ -1,35 +1,26 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import { getCmsScope } from './scope.js';
4
- import { cmsStore, type CmsScopeRef } from './cms-store.svelte.js';
3
+ import { useCmsField } from './use-cms-field.svelte.js';
5
4
 
6
5
  type Props = {
7
6
  name: string;
7
+ scope?: string;
8
8
  initial?: boolean;
9
9
  value?: unknown;
10
10
  children?: Snippet<[boolean]>;
11
11
  };
12
- let { name, initial = false, value, children }: Props = $props();
12
+ let { name, scope, initial = false, value, children }: Props = $props();
13
13
 
14
- const scope = getCmsScope();
15
- const ref = $derived<CmsScopeRef | null>(
16
- scope ? { scopeId: scope.scopeId, routeId: scope.routeId, params: scope.params } : null
14
+ const field = useCmsField(
15
+ () => ({ name, scope, value }),
16
+ (raw): boolean => (typeof raw === 'boolean' ? raw : raw === null ? false : initial)
17
17
  );
18
18
 
19
- const resolved = $derived.by(() => {
20
- if (value !== undefined) return value;
21
- return ref ? cmsStore.getValue(ref, name) : undefined;
22
- });
23
-
24
- const current = $derived(typeof resolved === 'boolean' ? resolved : initial);
25
- const editable = $derived(cmsStore.isEditing && value === undefined && !!ref);
26
-
27
- const toggle = () => {
28
- if (ref) cmsStore.setValue(ref, name, !current);
29
- };
19
+ const current = $derived(field.current);
20
+ const toggle = () => field.set(!current);
30
21
  </script>
31
22
 
32
- {#if editable && ref}
23
+ {#if field.editable && field.ref}
33
24
  <span class="cms-boolean-edit">
34
25
  {@render children?.(current)}
35
26
  <button
@@ -1,6 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type Props = {
3
3
  name: string;
4
+ scope?: string;
4
5
  initial?: boolean;
5
6
  value?: unknown;
6
7
  children?: Snippet<[boolean]>;
@@ -16,7 +16,7 @@
16
16
  );
17
17
 
18
18
  const live = $derived(cmsStore.getValue(scope, name));
19
- const display = $derived(typeof live === 'string' ? live : (initial ?? ''));
19
+ const display = $derived(typeof live === 'string' ? live : live === null ? '' : (initial ?? ''));
20
20
 
21
21
  let el = $state<HTMLInputElement>();
22
22
 
@@ -29,8 +29,8 @@
29
29
 
30
30
  const onInput = (e: Event) => {
31
31
  const raw = (e.currentTarget as HTMLInputElement).value;
32
- if (raw === '') return;
33
- cmsStore.setValue(scope, name, raw);
32
+ // An emptied input clears the field explicitly (`null`).
33
+ cmsStore.setValue(scope, name, raw === '' ? null : raw);
34
34
  };
35
35
  </script>
36
36
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  export type CmsDateTimeProps = {
7
7
  name: string;
8
+ scope?: string;
8
9
  /**
9
10
  * `'datetime'` (default) → `<input type="datetime-local">`, stores
10
11
  * `YYYY-MM-DDTHH:MM`. `'date'` → `<input type="date">`, stores
@@ -13,44 +14,43 @@
13
14
  */
14
15
  mode?: CmsDateTimeMode;
15
16
  fallback?: string;
16
- /** Per-item override used by `CmsRepeater`. */
17
+ /** Per-item override used by structured components. */
17
18
  value?: unknown;
18
19
  children?: Snippet<[string]>;
19
20
  };
20
21
  </script>
21
22
 
22
23
  <script lang="ts">
23
- import { getCmsScope } from './scope.js';
24
- import { cmsStore, type CmsScopeRef } from './cms-store.svelte.js';
24
+ import { useCmsField } from './use-cms-field.svelte.js';
25
25
 
26
- let { name, mode = 'datetime', fallback, value, children }: CmsDateTimeProps = $props();
26
+ let { name, scope, mode = 'datetime', fallback, value, children }: CmsDateTimeProps = $props();
27
27
 
28
- const scope = getCmsScope();
29
- const ref = $derived<CmsScopeRef | null>(
30
- scope ? { scopeId: scope.scopeId, routeId: scope.routeId, params: scope.params } : null
28
+ const field = useCmsField(
29
+ () => ({ name, scope, value }),
30
+ (raw): string | undefined => {
31
+ if (raw === undefined) return fallback;
32
+ return typeof raw === 'string' ? raw : '';
33
+ }
31
34
  );
32
35
 
33
- const resolved = $derived.by(() => {
34
- if (value !== undefined) return value;
35
- return ref ? cmsStore.getValue(ref, name) : undefined;
36
- });
37
-
38
- const current = $derived(typeof resolved === 'string' ? resolved : fallback);
39
- const editable = $derived(cmsStore.isEditing && value === undefined && !!ref);
36
+ const current = $derived(field.current);
40
37
  </script>
41
38
 
42
- {#if editable && ref}
39
+ {#if field.editable && field.ref}
43
40
  {#await import('./cms-date-time-editable.svelte') then { default: Editable }}
44
- <Editable scope={ref} {name} {mode} initial={current} />
41
+ <Editable scope={field.ref} {name} {mode} initial={current} />
45
42
  {/await}
46
43
  {:else if current && children}
47
44
  {@render children(current)}
48
45
  {:else if current}
49
46
  {current}
47
+ {:else if field.raw === null}
48
+ <!-- cleared by the editor -->
50
49
  {:else if children}
51
50
  {@render children('')}
52
51
  {:else}
53
- <span class="cms-missing" data-cms-name={name} data-cms-scope={scope?.scopeId ?? '?'}>{name}</span
52
+ <span class="cms-missing" data-cms-name={name} data-cms-scope={field.ref?.scopeId ?? '?'}
53
+ >{name}</span
54
54
  >
55
55
  {/if}
56
56
 
@@ -2,6 +2,7 @@ import type { Snippet } from 'svelte';
2
2
  export type CmsDateTimeMode = 'datetime' | 'date' | 'time';
3
3
  export type CmsDateTimeProps = {
4
4
  name: string;
5
+ scope?: string;
5
6
  /**
6
7
  * `'datetime'` (default) → `<input type="datetime-local">`, stores
7
8
  * `YYYY-MM-DDTHH:MM`. `'date'` → `<input type="date">`, stores
@@ -10,7 +11,7 @@ export type CmsDateTimeProps = {
10
11
  */
11
12
  mode?: CmsDateTimeMode;
12
13
  fallback?: string;
13
- /** Per-item override used by `CmsRepeater`. */
14
+ /** Per-item override used by structured components. */
14
15
  value?: unknown;
15
16
  children?: Snippet<[string]>;
16
17
  };
@@ -29,13 +29,17 @@
29
29
  // Read leaves through `getValue` so live drafts reflect immediately. Fall
30
30
  // back to the snapshot the display passed in when nothing's been written.
31
31
  const currentUrl = $derived.by(() => {
32
- const v = pathGet(cmsStore.getValue(scope, name), 'url');
32
+ const stored = cmsStore.getValue(scope, name);
33
+ const v = pathGet(stored, 'url');
33
34
  if (typeof v === 'string') return v;
35
+ if (v === null || stored === null) return '';
34
36
  return initial.url ?? '';
35
37
  });
36
38
  const currentAlt = $derived.by(() => {
37
- const v = pathGet(cmsStore.getValue(scope, name), 'alt');
39
+ const stored = cmsStore.getValue(scope, name);
40
+ const v = pathGet(stored, 'alt');
38
41
  if (typeof v === 'string') return v;
42
+ if (v === null || stored === null) return '';
39
43
  return initial.alt ?? '';
40
44
  });
41
45
 
@@ -55,8 +59,13 @@
55
59
  let uploading = $state(false);
56
60
  let error: string | null = $state(null);
57
61
 
58
- const writeUrl = (v: string) => cmsStore.setValue(scope, `${name}.url`, v);
59
- const writeAlt = (v: string) => cmsStore.setValue(scope, `${name}.alt`, v);
62
+ // Whole-object writes (see `core/structured.ts`): a cleared image is
63
+ // `{ url: null }`, which merges over the published tree as "cleared"
64
+ // instead of falling back to the template's demo image.
65
+ const writeUrl = (v: string) =>
66
+ cmsStore.setValue(scope, name, { url: v || null, alt: currentAlt || null });
67
+ const writeAlt = (v: string) =>
68
+ cmsStore.setValue(scope, name, { url: currentUrl || null, alt: v || null });
60
69
 
61
70
  const upload = async (file: File) => {
62
71
  if (!file.type.startsWith('image/')) {
@@ -1,62 +1,69 @@
1
1
  <script lang="ts" module>
2
+ /**
3
+ * Stored shape of an image slot. Seeds and fallbacks may pass a bare URL
4
+ * string; it is normalized to `{ url }`. A cleared slot stores `url: null`.
5
+ */
2
6
  export type CmsImageValue = {
3
- url?: string;
4
- alt?: string;
7
+ url?: string | null;
8
+ alt?: string | null;
5
9
  width?: number;
6
10
  height?: number;
7
11
  };
12
+
13
+ export const normalizeImage = (raw: unknown): CmsImageValue => {
14
+ if (raw == null) return {};
15
+ if (typeof raw === 'string') return { url: raw };
16
+ if (typeof raw === 'object' && !Array.isArray(raw)) {
17
+ const r = raw as Record<string, unknown>;
18
+ const out: CmsImageValue = {};
19
+ if (typeof r.url === 'string') out.url = r.url;
20
+ if (typeof r.alt === 'string') out.alt = r.alt;
21
+ if (typeof r.width === 'number') out.width = r.width;
22
+ if (typeof r.height === 'number') out.height = r.height;
23
+ return out;
24
+ }
25
+ return {};
26
+ };
8
27
  </script>
9
28
 
10
29
  <script lang="ts">
11
30
  import type { Snippet } from 'svelte';
12
- import { getCmsScope } from './scope.js';
13
- import { cmsStore, type CmsScopeRef } from './cms-store.svelte.js';
31
+ import { useCmsField } from './use-cms-field.svelte.js';
14
32
 
15
33
  type Props = {
16
34
  name: string;
17
- /** Fallback alt text used when the branch has no `alt` leaf. */
35
+ scope?: string;
36
+ /** Fallback alt text used when the stored value has no `alt`. */
18
37
  alt?: string;
19
- fallback?: string;
38
+ /** A URL, or a full `{ url, alt }` value. */
39
+ fallback?: string | CmsImageValue;
20
40
  value?: unknown;
21
41
  children?: Snippet;
22
42
  };
23
43
 
24
- let { name, alt = '', fallback, value, children }: Props = $props();
44
+ let { name, scope, alt = '', fallback, value, children }: Props = $props();
25
45
 
26
- const scope = getCmsScope();
27
- const ref = $derived<CmsScopeRef | null>(
28
- scope ? { scopeId: scope.scopeId, routeId: scope.routeId, params: scope.params } : null
46
+ const field = useCmsField(
47
+ () => ({ name, scope, value }),
48
+ (raw): CmsImageValue => (raw === undefined ? normalizeImage(fallback) : normalizeImage(raw))
29
49
  );
30
50
 
31
- const resolved = $derived.by(() => {
32
- if (value !== undefined) return value;
33
- return ref ? cmsStore.getValue(ref, name) : undefined;
34
- });
35
-
36
- // `<CmsImage>` owns a `{ url, alt, width?, height? }` branch. Accept a bare
37
- // string as the URL leaf for legacy/value-prop callers.
38
- const branch = $derived.by((): CmsImageValue | null => {
39
- if (resolved == null) return null;
40
- if (typeof resolved === 'string') return { url: resolved };
41
- if (typeof resolved === 'object' && !Array.isArray(resolved)) return resolved as CmsImageValue;
42
- return null;
43
- });
44
-
45
- const src = $derived(branch?.url ?? fallback ?? '');
46
- const altText = $derived(branch?.alt ?? alt ?? '');
47
- const editable = $derived(cmsStore.isEditing && value === undefined && !!ref);
51
+ const src = $derived(field.current.url ?? '');
52
+ const altText = $derived(field.current.alt ?? alt);
48
53
  </script>
49
54
 
50
- {#if editable && ref}
55
+ {#if field.editable && field.ref}
51
56
  {#await import('./cms-image-editable.svelte') then { default: Editable }}
52
- <Editable scope={ref} {name} initial={branch ?? {}} fallbackAlt={alt} />
57
+ <Editable scope={field.ref} {name} initial={field.current} fallbackAlt={alt} />
53
58
  {/await}
54
59
  {:else if src}
55
60
  <img class="cms-image" {src} alt={altText} />
61
+ {:else if field.raw === null}
62
+ <!-- cleared by the editor -->
56
63
  {:else if children}
57
64
  {@render children()}
58
65
  {:else}
59
- <span class="cms-missing" data-cms-name={name} data-cms-scope={scope?.scopeId ?? '?'}>
66
+ <span class="cms-missing" data-cms-name={name} data-cms-scope={field.ref?.scopeId ?? '?'}>
60
67
  {name}
61
68
  </span>
62
69
  {/if}
@@ -1,15 +1,22 @@
1
+ /**
2
+ * Stored shape of an image slot. Seeds and fallbacks may pass a bare URL
3
+ * string; it is normalized to `{ url }`. A cleared slot stores `url: null`.
4
+ */
1
5
  export type CmsImageValue = {
2
- url?: string;
3
- alt?: string;
6
+ url?: string | null;
7
+ alt?: string | null;
4
8
  width?: number;
5
9
  height?: number;
6
10
  };
11
+ export declare const normalizeImage: (raw: unknown) => CmsImageValue;
7
12
  import type { Snippet } from 'svelte';
8
13
  type Props = {
9
14
  name: string;
10
- /** Fallback alt text used when the branch has no `alt` leaf. */
15
+ scope?: string;
16
+ /** Fallback alt text used when the stored value has no `alt`. */
11
17
  alt?: string;
12
- fallback?: string;
18
+ /** A URL, or a full `{ url, alt }` value. */
19
+ fallback?: string | CmsImageValue;
13
20
  value?: unknown;
14
21
  children?: Snippet;
15
22
  };