@velastack/cms 0.5.0 → 0.6.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 (134) hide show
  1. package/README.md +104 -47
  2. package/dist/backend/index.d.ts +7 -5
  3. package/dist/backend/index.js +7 -5
  4. package/dist/backend/types.d.ts +1 -1
  5. package/dist/components/admin-bar/admin-bar-internal.svelte +54 -59
  6. package/dist/components/admin-bar/admin-bar.css +1 -1
  7. package/dist/components/admin-bar/admin-bar.svelte +11 -7
  8. package/dist/components/admin-bar/field-input.svelte +20 -1
  9. package/dist/components/admin-bar/field-input.svelte.d.ts +2 -1
  10. package/dist/components/admin-bar/fields-form.svelte +218 -0
  11. package/dist/components/admin-bar/fields-form.svelte.d.ts +16 -0
  12. package/dist/components/admin-bar/history-panel.svelte +4 -4
  13. package/dist/components/admin-bar/link-input.svelte +102 -0
  14. package/dist/components/admin-bar/link-input.svelte.d.ts +9 -0
  15. package/dist/components/admin-bar/list-field.svelte +93 -0
  16. package/dist/components/admin-bar/list-field.svelte.d.ts +20 -0
  17. package/dist/components/admin-bar/locales-panel.svelte +1 -1
  18. package/dist/components/admin-bar/page-options.d.ts +24 -0
  19. package/dist/components/admin-bar/page-options.js +53 -0
  20. package/dist/components/admin-bar/pages-panel.svelte +1 -1
  21. package/dist/components/admin-bar/rich-text-input.svelte +92 -0
  22. package/dist/components/admin-bar/rich-text-input.svelte.d.ts +9 -0
  23. package/dist/components/admin-bar/structured-editor-popover.svelte +15 -6
  24. package/dist/components/admin-bar/theme.svelte.js +1 -1
  25. package/dist/components/cms/cms-collection.svelte +46 -0
  26. package/dist/components/cms/cms-collection.svelte.d.ts +25 -0
  27. package/dist/components/cms/cms-contact.svelte +26 -0
  28. package/dist/components/cms/cms-contact.svelte.d.ts +14 -0
  29. package/dist/components/cms/cms-faq.svelte +22 -0
  30. package/dist/components/cms/cms-faq.svelte.d.ts +15 -0
  31. package/dist/components/cms/cms-gallery.svelte +31 -0
  32. package/dist/components/cms/cms-gallery.svelte.d.ts +16 -0
  33. package/dist/components/cms/cms-hours-editable.svelte +160 -0
  34. package/dist/components/cms/cms-hours-editable.svelte.d.ts +17 -0
  35. package/dist/components/cms/cms-hours.svelte +42 -0
  36. package/dist/components/cms/cms-hours.svelte.d.ts +19 -0
  37. package/dist/components/cms/cms-image.svelte +3 -25
  38. package/dist/components/cms/cms-image.svelte.d.ts +3 -11
  39. package/dist/components/cms/cms-link-editable.svelte +4 -60
  40. package/dist/components/cms/cms-link.svelte +3 -23
  41. package/dist/components/cms/cms-link.svelte.d.ts +3 -10
  42. package/dist/components/cms/cms-logos.svelte +29 -0
  43. package/dist/components/cms/cms-logos.svelte.d.ts +16 -0
  44. package/dist/components/cms/cms-nav.svelte +22 -0
  45. package/dist/components/cms/cms-nav.svelte.d.ts +15 -0
  46. package/dist/components/cms/cms-pricing.svelte +36 -0
  47. package/dist/components/cms/cms-pricing.svelte.d.ts +14 -0
  48. package/dist/components/cms/cms-schedule.svelte +35 -0
  49. package/dist/components/cms/cms-schedule.svelte.d.ts +16 -0
  50. package/dist/components/cms/cms-social-links.svelte +35 -0
  51. package/dist/components/cms/cms-social-links.svelte.d.ts +15 -0
  52. package/dist/components/cms/cms-stats.svelte +31 -0
  53. package/dist/components/cms/cms-stats.svelte.d.ts +16 -0
  54. package/dist/components/cms/cms-steps.svelte +31 -0
  55. package/dist/components/cms/cms-steps.svelte.d.ts +16 -0
  56. package/dist/components/cms/cms-store.svelte.js +8 -5
  57. package/dist/components/cms/cms-structured-editable.svelte +29 -0
  58. package/dist/components/cms/{cms-repeater-editable.svelte.d.ts → cms-structured-editable.svelte.d.ts} +12 -8
  59. package/dist/components/cms/cms-structured.svelte +50 -0
  60. package/dist/components/cms/cms-structured.svelte.d.ts +49 -0
  61. package/dist/components/cms/cms-team.svelte +22 -0
  62. package/dist/components/cms/cms-team.svelte.d.ts +15 -0
  63. package/dist/components/cms/cms-testimonials.svelte +36 -0
  64. package/dist/components/cms/cms-testimonials.svelte.d.ts +15 -0
  65. package/dist/components/cms/cms-timeline.svelte +35 -0
  66. package/dist/components/cms/cms-timeline.svelte.d.ts +16 -0
  67. package/dist/components/cms/index.d.ts +15 -1
  68. package/dist/components/cms/index.js +15 -1
  69. package/dist/core/cms-fetch.d.ts +16 -0
  70. package/dist/core/cms-fetch.js +24 -0
  71. package/dist/core/shapes/collection.d.ts +60 -0
  72. package/dist/core/shapes/collection.js +194 -0
  73. package/dist/core/shapes/contact.d.ts +70 -0
  74. package/dist/core/shapes/contact.js +214 -0
  75. package/dist/core/shapes/faq.d.ts +12 -0
  76. package/dist/core/shapes/faq.js +48 -0
  77. package/dist/core/shapes/form.d.ts +46 -0
  78. package/dist/core/shapes/form.js +7 -0
  79. package/dist/core/shapes/hours.d.ts +91 -0
  80. package/dist/core/shapes/hours.js +325 -0
  81. package/dist/core/shapes/image.d.ts +15 -0
  82. package/dist/core/shapes/image.js +27 -0
  83. package/dist/core/shapes/index.d.ts +31 -0
  84. package/dist/core/shapes/index.js +13 -0
  85. package/dist/core/shapes/link.d.ts +15 -0
  86. package/dist/core/shapes/link.js +42 -0
  87. package/dist/core/shapes/nav.d.ts +21 -0
  88. package/dist/core/shapes/nav.js +71 -0
  89. package/dist/core/shapes/presets.d.ts +65 -0
  90. package/dist/core/shapes/presets.js +152 -0
  91. package/dist/core/shapes/pricing.d.ts +40 -0
  92. package/dist/core/shapes/pricing.js +115 -0
  93. package/dist/core/shapes/social-links.d.ts +19 -0
  94. package/dist/core/shapes/social-links.js +83 -0
  95. package/dist/core/shapes/team.d.ts +22 -0
  96. package/dist/core/shapes/team.js +70 -0
  97. package/dist/core/shapes/testimonials.d.ts +24 -0
  98. package/dist/core/shapes/testimonials.js +86 -0
  99. package/dist/{components/cms → core}/structured-registry.d.ts +2 -2
  100. package/dist/core/structured.d.ts +33 -10
  101. package/dist/core/structured.js +98 -47
  102. package/dist/editor.d.ts +4 -0
  103. package/dist/editor.js +4 -0
  104. package/dist/index.d.ts +22 -8
  105. package/dist/index.js +24 -5
  106. package/dist/server/README.md +1 -1
  107. package/dist/server/api-adapter.js +1 -1
  108. package/dist/server/cms.d.ts +1 -1
  109. package/dist/server/cms.js +1 -1
  110. package/dist/server/load-cms.js +1 -1
  111. package/package.json +15 -11
  112. package/plugin/README.md +34 -4
  113. package/plugin/dist/adapter.d.ts +5 -5
  114. package/plugin/dist/adapter.js +21 -7
  115. package/plugin/dist/adapter.js.map +1 -1
  116. package/plugin/dist/build-state.d.ts +1 -0
  117. package/plugin/dist/build-state.js +10 -1
  118. package/plugin/dist/build-state.js.map +1 -1
  119. package/plugin/dist/build.d.ts +2 -2
  120. package/plugin/dist/build.js +8 -7
  121. package/plugin/dist/build.js.map +1 -1
  122. package/plugin/dist/index.js +5 -1
  123. package/plugin/dist/index.js.map +1 -1
  124. package/plugin/dist/manifest.d.ts +18 -0
  125. package/plugin/dist/manifest.js +78 -10
  126. package/plugin/dist/manifest.js.map +1 -1
  127. package/plugin/dist/transforms.d.ts +4 -4
  128. package/plugin/dist/transforms.js +4 -4
  129. package/dist/cms.d.ts +0 -6
  130. package/dist/cms.js +0 -27
  131. package/dist/components/cms/cms-repeater-editable.svelte +0 -137
  132. package/dist/components/cms/cms-repeater.svelte +0 -52
  133. package/dist/components/cms/cms-repeater.svelte.d.ts +0 -31
  134. /package/dist/{components/cms → core}/structured-registry.js +0 -0
@@ -0,0 +1,102 @@
1
+ <script lang="ts">
2
+ /**
3
+ * A link field inside a structured editor: a page from the site or an
4
+ * external URL, plus "open in a new tab". `value` is a `CmsLinkValue`
5
+ * or `null`; `onChange(null)` clears it.
6
+ */
7
+ import { untrack } from 'svelte';
8
+ import { page } from '$app/state';
9
+ import type { CmsLinkValue } from '../../core/shapes/link.js';
10
+ import { fetchPageOptions, pageOptionValue, type PageOption } from './page-options.js';
11
+ import { Input } from './ui/input/index.js';
12
+ import { Checkbox } from './ui/checkbox/index.js';
13
+
14
+ type Props = {
15
+ value: CmsLinkValue | null;
16
+ onChange: (next: CmsLinkValue | null) => void;
17
+ label?: string;
18
+ };
19
+ let { value, onChange, label }: Props = $props();
20
+
21
+ const endpoint = $derived(page.data.cms?.endpoint ?? '/api/cms');
22
+ const selectClass =
23
+ 'vela:flex vela:h-9 vela:w-full vela:rounded-md vela:border vela:border-[var(--cms-bar-divider)]' +
24
+ ' vela:bg-[var(--cms-bar-bg-hover)] vela:px-3 vela:py-1 vela:text-[13px] vela:text-bar-text' +
25
+ ' vela:focus:outline-none vela:focus:ring-2 vela:focus:ring-[var(--cms-accent)]';
26
+
27
+ let mode = $state<'page' | 'url'>(
28
+ untrack(() => (value?.href && !value.routeId ? 'url' : 'page'))
29
+ );
30
+ let options = $state<PageOption[] | null>(null);
31
+ $effect(() => {
32
+ if (mode === 'page' && options === null) {
33
+ void fetchPageOptions(endpoint).then((o) => (options = o));
34
+ }
35
+ });
36
+
37
+ const selected = $derived(
38
+ value?.routeId ? pageOptionValue(value.routeId, value.params ?? {}) : ''
39
+ );
40
+ const newTab = $derived(value?.newTab === true);
41
+
42
+ const emit = (next: CmsLinkValue) => {
43
+ onChange(next.href || next.routeId ? next : null);
44
+ };
45
+ const choosePage = (key: string) => {
46
+ const opt = options?.find((o) => o.value === key);
47
+ if (!opt) return emit({ newTab });
48
+ emit({ routeId: opt.routeId, params: opt.params, newTab });
49
+ };
50
+ const setUrl = (href: string) => emit({ href: href.trim(), newTab });
51
+ const setNewTab = (checked: boolean) => {
52
+ if (value) emit({ ...value, newTab: checked });
53
+ };
54
+ </script>
55
+
56
+ <div class="vela:flex vela:flex-col vela:gap-1.5">
57
+ <div class="vela:flex vela:items-center vela:justify-between">
58
+ <span class="vela:text-[12px] vela:text-bar-text-secondary">{label ?? 'Link'}</span>
59
+ <span
60
+ class="vela:inline-flex vela:rounded-md vela:border vela:border-[var(--cms-bar-divider)] vela:p-0.5 vela:text-[11px]"
61
+ role="radiogroup"
62
+ aria-label="Link type"
63
+ >
64
+ {#each [['page', 'Page'], ['url', 'URL']] as const as [key, name] (key)}
65
+ <button
66
+ type="button"
67
+ role="radio"
68
+ aria-checked={mode === key}
69
+ class="vela:rounded vela:px-2 vela:py-0.5 vela:cursor-pointer vela:text-bar-text-secondary aria-checked:vela:bg-[var(--cms-bar-bg-hover)] aria-checked:vela:text-bar-text"
70
+ onclick={() => (mode = key)}
71
+ >
72
+ {name}
73
+ </button>
74
+ {/each}
75
+ </span>
76
+ </div>
77
+ {#if mode === 'page'}
78
+ <select
79
+ class={selectClass}
80
+ value={selected}
81
+ onchange={(e) => choosePage(e.currentTarget.value)}
82
+ >
83
+ <option value="">{options === null ? 'Loading…' : '— No link —'}</option>
84
+ {#each options ?? [] as opt (opt.value)}
85
+ <option value={opt.value}>{opt.label}</option>
86
+ {/each}
87
+ </select>
88
+ {:else}
89
+ <Input
90
+ type="url"
91
+ value={value?.href ?? ''}
92
+ placeholder="https://…"
93
+ oninput={(e) => setUrl(e.currentTarget.value)}
94
+ />
95
+ {/if}
96
+ <label
97
+ class="vela:flex vela:items-center vela:gap-2 vela:text-[12px] vela:text-bar-text-secondary"
98
+ >
99
+ <Checkbox checked={newTab} onCheckedChange={(c) => setNewTab(c === true)} disabled={!value} />
100
+ Open in a new tab
101
+ </label>
102
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { CmsLinkValue } from '../../core/shapes/link.js';
2
+ type Props = {
3
+ value: CmsLinkValue | null;
4
+ onChange: (next: CmsLinkValue | null) => void;
5
+ label?: string;
6
+ };
7
+ declare const LinkInput: import("svelte").Component<Props, {}, "">;
8
+ type LinkInput = ReturnType<typeof LinkInput>;
9
+ export default LinkInput;
@@ -0,0 +1,93 @@
1
+ <script lang="ts">
2
+ /**
3
+ * A list of items inside a structured form: one collapsible card per
4
+ * item, up/down/remove buttons, and an add button. Every item carries a
5
+ * stable `id` (new ones get a random one), so translations and reorders
6
+ * never lose track of an item.
7
+ */
8
+ import type { FormField } from '../../core/shapes/form.js';
9
+ import type { Tree } from '../../core/path.js';
10
+ import { moveItem, newItemId } from '../../core/structured.js';
11
+ import FieldsForm from './fields-form.svelte';
12
+ import ReorderButtons from './reorder-buttons.svelte';
13
+ import ChevronDownIcon from './icons/chevron-down.svelte';
14
+ import ChevronRightIcon from './icons/chevron-right.svelte';
15
+ import { Button } from './ui/button/index.js';
16
+
17
+ type Props = {
18
+ label: string;
19
+ itemLabel: string;
20
+ titleKey?: string;
21
+ fields: readonly FormField[];
22
+ blank: () => Tree;
23
+ items: Tree[];
24
+ onChange: (items: Tree[]) => void;
25
+ };
26
+ let { label, itemLabel, titleKey, fields, blank, items, onChange }: Props = $props();
27
+
28
+ let open = $state(new Set<string>());
29
+ const toggle = (id: string) => {
30
+ const next = new Set(open);
31
+ if (next.has(id)) next.delete(id);
32
+ else next.add(id);
33
+ open = next;
34
+ };
35
+
36
+ const idOf = (item: Tree, i: number): string =>
37
+ typeof item.id === 'string' && item.id !== '' ? item.id : `item-${i}`;
38
+
39
+ const titleOf = (item: Tree, i: number): string => {
40
+ const t = titleKey ? item[titleKey] : undefined;
41
+ if (typeof t === 'string' && t.trim() !== '') return t;
42
+ return `${itemLabel[0].toUpperCase()}${itemLabel.slice(1)} ${i + 1}`;
43
+ };
44
+
45
+ const add = () => {
46
+ const id = newItemId();
47
+ onChange([...items, { id, ...blank() }]);
48
+ open = new Set([...open, id]);
49
+ };
50
+ const replace = (i: number, next: Tree) => onChange(items.map((it, j) => (j === i ? next : it)));
51
+ </script>
52
+
53
+ <div class="vela:flex vela:flex-col vela:gap-2">
54
+ <span class="vela:text-[12px] vela:text-bar-text-secondary">{label}</span>
55
+ {#each items as item, i (idOf(item, i))}
56
+ {@const id = idOf(item, i)}
57
+ {@const expanded = open.has(id)}
58
+ <div class="vela:rounded-md vela:border vela:border-[var(--cms-bar-divider)]">
59
+ <div class="vela:flex vela:items-center vela:gap-1 vela:pl-1 vela:pr-1.5 vela:py-1">
60
+ <button
61
+ type="button"
62
+ class="vela:flex vela:flex-1 vela:min-w-0 vela:items-center vela:gap-1.5 vela:cursor-pointer vela:text-left vela:text-[13px] vela:text-bar-text"
63
+ aria-expanded={expanded}
64
+ onclick={() => toggle(id)}
65
+ >
66
+ {#if expanded}
67
+ <ChevronDownIcon class="vela:size-3.5 vela:shrink-0 vela:text-bar-text-tertiary" />
68
+ {:else}
69
+ <ChevronRightIcon class="vela:size-3.5 vela:shrink-0 vela:text-bar-text-tertiary" />
70
+ {/if}
71
+ <span class="vela:truncate">{titleOf(item, i)}</span>
72
+ </button>
73
+ <ReorderButtons
74
+ index={i}
75
+ count={items.length}
76
+ label={itemLabel}
77
+ onMove={(from, to) => onChange(moveItem(items, from, to))}
78
+ onRemove={(idx) => onChange(items.filter((_, j) => j !== idx))}
79
+ />
80
+ </div>
81
+ {#if expanded}
82
+ <div
83
+ class="vela:flex vela:flex-col vela:gap-3 vela:border-t vela:border-[var(--cms-bar-divider)] vela:p-3"
84
+ >
85
+ <FieldsForm {fields} value={item} onChange={(next) => replace(i, next)} />
86
+ </div>
87
+ {/if}
88
+ </div>
89
+ {/each}
90
+ <Button variant="outline" size="pill" class="vela:self-start" onclick={add}>
91
+ + Add {itemLabel}
92
+ </Button>
93
+ </div>
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A list of items inside a structured form: one collapsible card per
3
+ * item, up/down/remove buttons, and an add button. Every item carries a
4
+ * stable `id` (new ones get a random one), so translations and reorders
5
+ * never lose track of an item.
6
+ */
7
+ import type { FormField } from '../../core/shapes/form.js';
8
+ import type { Tree } from '../../core/path.js';
9
+ type Props = {
10
+ label: string;
11
+ itemLabel: string;
12
+ titleKey?: string;
13
+ fields: readonly FormField[];
14
+ blank: () => Tree;
15
+ items: Tree[];
16
+ onChange: (items: Tree[]) => void;
17
+ };
18
+ declare const ListField: import("svelte").Component<Props, {}, "">;
19
+ type ListField = ReturnType<typeof ListField>;
20
+ export default ListField;
@@ -2,7 +2,7 @@
2
2
  import { page } from '$app/state';
3
3
  import { cmsStore, type ReleaseItem } from '../cms/cms-store.svelte.js';
4
4
  import type { CmsPayload } from '../cms/scope.js';
5
- import { getStructured } from '../cms/structured-registry.js';
5
+ import { getStructured } from '../../core/structured-registry.js';
6
6
  import { countTranslations, extractTranslations } from '../../core/structured.js';
7
7
  import PanelFooter from './panel-footer.svelte';
8
8
  import PanelHeader from './panel-header.svelte';
@@ -0,0 +1,24 @@
1
+ type PageEntry = {
2
+ params: Record<string, string>;
3
+ isDraft: boolean;
4
+ isDeletePending: boolean;
5
+ redirectTo?: string;
6
+ gone?: boolean;
7
+ };
8
+ type PageRoute = {
9
+ routeId: string;
10
+ ownedParams: string[];
11
+ entries: PageEntry[];
12
+ };
13
+ export type PageOption = {
14
+ /** `routeId::{params}` — stable key for a select. */
15
+ value: string;
16
+ /** The resolved URL, shown to the editor. */
17
+ label: string;
18
+ routeId: string;
19
+ params: Record<string, string>;
20
+ };
21
+ export declare const pageOptionValue: (routeId: string, params: Record<string, string>) => string;
22
+ export declare const toPageOptions: (routes: PageRoute[]) => PageOption[];
23
+ export declare const fetchPageOptions: (endpoint: string) => Promise<PageOption[]>;
24
+ export {};
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The site's pages as link targets, for the page picker in link editors.
3
+ * Fetched once per endpoint and shared by every picker on the page.
4
+ */
5
+ import { resolveRouteUrl } from './resolve-route.js';
6
+ export const pageOptionValue = (routeId, params) => `${routeId}::${JSON.stringify(params)}`;
7
+ export const toPageOptions = (routes) => {
8
+ const opts = [];
9
+ for (const r of routes) {
10
+ const entries = r.entries.length > 0
11
+ ? r.entries
12
+ : [{ params: {}, isDraft: false, isDeletePending: false }];
13
+ for (const ent of entries) {
14
+ // Tombstones (redirected / gone) aren't real link targets.
15
+ if (ent.redirectTo || ent.gone)
16
+ continue;
17
+ let url;
18
+ try {
19
+ url = resolveRouteUrl(r.routeId, ent.params);
20
+ }
21
+ catch {
22
+ continue;
23
+ }
24
+ opts.push({
25
+ value: pageOptionValue(r.routeId, ent.params),
26
+ label: url,
27
+ routeId: r.routeId,
28
+ params: ent.params
29
+ });
30
+ }
31
+ }
32
+ opts.sort((a, b) => a.label.localeCompare(b.label));
33
+ return opts;
34
+ };
35
+ const cache = new Map();
36
+ export const fetchPageOptions = (endpoint) => {
37
+ let p = cache.get(endpoint);
38
+ if (!p) {
39
+ p = fetch(`${endpoint}/pages`, { credentials: 'include' })
40
+ .then(async (res) => {
41
+ if (!res.ok)
42
+ return [];
43
+ const data = (await res.json());
44
+ return toPageOptions(data.routes);
45
+ })
46
+ .catch(() => {
47
+ cache.delete(endpoint);
48
+ return [];
49
+ });
50
+ cache.set(endpoint, p);
51
+ }
52
+ return p;
53
+ };
@@ -219,7 +219,7 @@
219
219
  const key = cmsStore.openRelease?.preview_key;
220
220
  if (key) url.searchParams.set('preview', key);
221
221
  // onClose();
222
- await goto(url, { keepFocus: true, noScroll: true });
222
+ await goto(url, { reset: false });
223
223
  };
224
224
 
225
225
  const onView = async (routeId: string, params: Record<string, string>) => {
@@ -0,0 +1,92 @@
1
+ <script lang="ts">
2
+ /**
3
+ * A rich text field inside a structured editor: the same TipTap setup as
4
+ * the inline `CmsRichText` editor, bound to a local value instead of a
5
+ * CMS path.
6
+ */
7
+ import { onDestroy, onMount, untrack } from 'svelte';
8
+ import { Editor } from '@tiptap/core';
9
+ import StarterKit from '@tiptap/starter-kit';
10
+ import Toolbar from '../cms/cms-rich-text-toolbar.svelte';
11
+
12
+ type Props = {
13
+ value: string;
14
+ onChange: (html: string) => void;
15
+ label?: string;
16
+ placeholder?: string;
17
+ };
18
+ let { value, onChange, label }: Props = $props();
19
+
20
+ let host = $state<HTMLDivElement>();
21
+ let wrap = $state<HTMLDivElement>();
22
+ let editor = $state<Editor>();
23
+ let focused = $state(false);
24
+ let tick = $state(0);
25
+
26
+ onMount(() => {
27
+ editor = new Editor({
28
+ element: host,
29
+ extensions: [StarterKit.configure({ heading: { levels: [2, 3] } })],
30
+ content: value,
31
+ editorProps: { attributes: { class: 'cms-rich-text-input-pm' } },
32
+ onUpdate: ({ editor }) => onChange(editor.isEmpty ? '' : editor.getHTML()),
33
+ onTransaction: () => {
34
+ tick++;
35
+ }
36
+ });
37
+ });
38
+ onDestroy(() => editor?.destroy());
39
+
40
+ $effect(() => {
41
+ if (!editor || focused) return;
42
+ const next = value;
43
+ untrack(() => {
44
+ if (editor && editor.getHTML() !== next) {
45
+ editor.commands.setContent(next, { emitUpdate: false });
46
+ }
47
+ });
48
+ });
49
+ </script>
50
+
51
+ <div class="vela:flex vela:flex-col vela:gap-1.5">
52
+ {#if label}<span class="vela:text-[12px] vela:text-bar-text-secondary">{label}</span>{/if}
53
+ <div
54
+ class="cms-rich-text-input vela:relative vela:rounded-md vela:border vela:border-[var(--cms-bar-divider)] vela:px-3 vela:py-2 vela:text-[13px] vela:text-bar-text"
55
+ class:focused
56
+ bind:this={wrap}
57
+ onfocusin={() => (focused = true)}
58
+ onfocusout={(e) => {
59
+ const next = e.relatedTarget as Node | null;
60
+ if (next && wrap?.contains(next)) return;
61
+ focused = false;
62
+ }}
63
+ >
64
+ {#if editor && focused}
65
+ <div class="vela:mb-2">
66
+ <Toolbar {editor} {tick} />
67
+ </div>
68
+ {/if}
69
+ <div bind:this={host}></div>
70
+ </div>
71
+ </div>
72
+
73
+ <style>
74
+ .cms-rich-text-input.focused {
75
+ border-color: var(--cms-accent, #534ab7);
76
+ }
77
+ .cms-rich-text-input :global(.cms-rich-text-input-pm) {
78
+ outline: none;
79
+ min-height: 3em;
80
+ line-height: 1.5;
81
+ }
82
+ .cms-rich-text-input :global(.cms-rich-text-input-pm > :first-child) {
83
+ margin-top: 0;
84
+ }
85
+ .cms-rich-text-input :global(.cms-rich-text-input-pm > :last-child) {
86
+ margin-bottom: 0;
87
+ }
88
+ .cms-rich-text-input :global(.cms-rich-text-input-pm ul),
89
+ .cms-rich-text-input :global(.cms-rich-text-input-pm ol) {
90
+ padding-left: 1.25em;
91
+ }
92
+ </style>
@@ -0,0 +1,9 @@
1
+ type Props = {
2
+ value: string;
3
+ onChange: (html: string) => void;
4
+ label?: string;
5
+ placeholder?: string;
6
+ };
7
+ declare const RichTextInput: import("svelte").Component<Props, {}, "">;
8
+ type RichTextInput = ReturnType<typeof RichTextInput>;
9
+ export default RichTextInput;
@@ -24,6 +24,7 @@
24
24
  import PencilIcon from './icons/pencil.svelte';
25
25
  import { Button } from './ui/button/index.js';
26
26
  import { Input } from './ui/input/index.js';
27
+ import { Textarea } from './ui/textarea/index.js';
27
28
  import * as Popover from './ui/popover/index.js';
28
29
  import * as Sheet from './ui/sheet/index.js';
29
30
  import * as Tabs from './ui/tabs/index.js';
@@ -155,12 +156,20 @@
155
156
  <span class="vela:text-[12px] vela:text-bar-text-secondary vela:truncate">
156
157
  {f.source}
157
158
  </span>
158
- <Input
159
- type="text"
160
- value={translationValue(tab, f)}
161
- placeholder={f.source}
162
- oninput={(e) => setTranslation(tab, f, e.currentTarget.value)}
163
- />
159
+ {#if f.source.length > 80 || f.source.includes('<')}
160
+ <Textarea
161
+ value={translationValue(tab, f)}
162
+ placeholder={f.source}
163
+ oninput={(e) => setTranslation(tab, f, e.currentTarget.value)}
164
+ />
165
+ {:else}
166
+ <Input
167
+ type="text"
168
+ value={translationValue(tab, f)}
169
+ placeholder={f.source}
170
+ oninput={(e) => setTranslation(tab, f, e.currentTarget.value)}
171
+ />
172
+ {/if}
164
173
  </label>
165
174
  {/each}
166
175
  {/if}
@@ -5,7 +5,7 @@
5
5
  * component's `<CssRoot>`, and the system-preference listener all read from
6
6
  * the same singleton so theme changes propagate everywhere immediately.
7
7
  */
8
- import { browser } from '$app/environment';
8
+ import { browser } from '$app/env';
9
9
  const STORAGE_KEY = 'cms.theme';
10
10
  const readStoredTheme = () => {
11
11
  if (typeof localStorage === 'undefined')
@@ -0,0 +1,46 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * `CmsCollection`: services, rooms, programmes, tours, projects. One list,
4
+ * rendered whole on its page and sliced (`limit`, `featured`, `tag`) on
5
+ * the home page through `scope="root"` or the list page's scope id.
6
+ */
7
+ export * from '../../core/shapes/collection.js';
8
+ </script>
9
+
10
+ <script lang="ts">
11
+ import type { Snippet } from 'svelte';
12
+ import Structured from './cms-structured.svelte';
13
+ import {
14
+ cmsCollection,
15
+ collectionView,
16
+ type CmsCollectionValue,
17
+ type CollectionItem,
18
+ type CollectionItemView
19
+ } from '../../core/shapes/collection.js';
20
+
21
+ type Props = {
22
+ name: string;
23
+ scope?: string;
24
+ /** The full value, or a bare item list. */
25
+ fallback?: Partial<CmsCollectionValue> | Partial<CollectionItem>[];
26
+ value?: unknown;
27
+ /** Render at most this many items. */
28
+ limit?: number;
29
+ /** Only featured items. */
30
+ featured?: boolean;
31
+ /** Only items carrying this tag. */
32
+ tag?: string;
33
+ children: Snippet<[CollectionItemView[]]>;
34
+ };
35
+ let { name, scope, fallback, value, limit, featured, tag, children }: Props = $props();
36
+ </script>
37
+
38
+ <Structured
39
+ schema={cmsCollection}
40
+ {name}
41
+ {scope}
42
+ {fallback}
43
+ {value}
44
+ view={(v: CmsCollectionValue) => collectionView(v, { limit, featured, tag })}
45
+ {children}
46
+ />
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `CmsCollection`: services, rooms, programmes, tours, projects. One list,
3
+ * rendered whole on its page and sliced (`limit`, `featured`, `tag`) on
4
+ * the home page through `scope="root"` or the list page's scope id.
5
+ */
6
+ export * from '../../core/shapes/collection.js';
7
+ import type { Snippet } from 'svelte';
8
+ import { type CmsCollectionValue, type CollectionItem, type CollectionItemView } from '../../core/shapes/collection.js';
9
+ type Props = {
10
+ name: string;
11
+ scope?: string;
12
+ /** The full value, or a bare item list. */
13
+ fallback?: Partial<CmsCollectionValue> | Partial<CollectionItem>[];
14
+ value?: unknown;
15
+ /** Render at most this many items. */
16
+ limit?: number;
17
+ /** Only featured items. */
18
+ featured?: boolean;
19
+ /** Only items carrying this tag. */
20
+ tag?: string;
21
+ children: Snippet<[CollectionItemView[]]>;
22
+ };
23
+ declare const CmsCollection: import("svelte").Component<Props, {}, "">;
24
+ type CmsCollection = ReturnType<typeof CmsCollection>;
25
+ export default CmsCollection;
@@ -0,0 +1,26 @@
1
+ <script lang="ts" module>
2
+ /** `CmsContact`: address, phones, emails and the links a template renders from them. */
3
+ export * from '../../core/shapes/contact.js';
4
+ </script>
5
+
6
+ <script lang="ts">
7
+ import type { Snippet } from 'svelte';
8
+ import Structured from './cms-structured.svelte';
9
+ import {
10
+ cmsContact,
11
+ contactView,
12
+ type CmsContactValue,
13
+ type ContactView
14
+ } from '../../core/shapes/contact.js';
15
+
16
+ type Props = {
17
+ name: string;
18
+ scope?: string;
19
+ fallback?: Partial<CmsContactValue>;
20
+ value?: unknown;
21
+ children: Snippet<[ContactView]>;
22
+ };
23
+ let { name, scope, fallback, value, children }: Props = $props();
24
+ </script>
25
+
26
+ <Structured schema={cmsContact} {name} {scope} {fallback} {value} view={contactView} {children} />
@@ -0,0 +1,14 @@
1
+ /** `CmsContact`: address, phones, emails and the links a template renders from them. */
2
+ export * from '../../core/shapes/contact.js';
3
+ import type { Snippet } from 'svelte';
4
+ import { type CmsContactValue, type ContactView } from '../../core/shapes/contact.js';
5
+ type Props = {
6
+ name: string;
7
+ scope?: string;
8
+ fallback?: Partial<CmsContactValue>;
9
+ value?: unknown;
10
+ children: Snippet<[ContactView]>;
11
+ };
12
+ declare const CmsContact: import("svelte").Component<Props, {}, "">;
13
+ type CmsContact = ReturnType<typeof CmsContact>;
14
+ export default CmsContact;
@@ -0,0 +1,22 @@
1
+ <script lang="ts" module>
2
+ /** `CmsFaq`: questions and rich answers. */
3
+ export * from '../../core/shapes/faq.js';
4
+ </script>
5
+
6
+ <script lang="ts">
7
+ import type { Snippet } from 'svelte';
8
+ import Structured from './cms-structured.svelte';
9
+ import { cmsFaq, faqView, type CmsFaqValue, type FaqItem } from '../../core/shapes/faq.js';
10
+
11
+ type Props = {
12
+ name: string;
13
+ scope?: string;
14
+ /** The full value, or a bare item list. */
15
+ fallback?: Partial<CmsFaqValue> | Array<Partial<CmsFaqValue['items'][number]>>;
16
+ value?: unknown;
17
+ children: Snippet<[FaqItem[]]>;
18
+ };
19
+ let { name, scope, fallback, value, children }: Props = $props();
20
+ </script>
21
+
22
+ <Structured schema={cmsFaq} {name} {scope} {fallback} {value} view={faqView} {children} />
@@ -0,0 +1,15 @@
1
+ /** `CmsFaq`: questions and rich answers. */
2
+ export * from '../../core/shapes/faq.js';
3
+ import type { Snippet } from 'svelte';
4
+ import { type CmsFaqValue, type FaqItem } from '../../core/shapes/faq.js';
5
+ type Props = {
6
+ name: string;
7
+ scope?: string;
8
+ /** The full value, or a bare item list. */
9
+ fallback?: Partial<CmsFaqValue> | Array<Partial<CmsFaqValue['items'][number]>>;
10
+ value?: unknown;
11
+ children: Snippet<[FaqItem[]]>;
12
+ };
13
+ declare const CmsFaq: import("svelte").Component<Props, {}, "">;
14
+ type CmsFaq = ReturnType<typeof CmsFaq>;
15
+ export default CmsFaq;
@@ -0,0 +1,31 @@
1
+ <script lang="ts" module>
2
+ /** `CmsGallery` preset: image and caption. */
3
+ export type { CmsGalleryValue, GalleryItem } from '../../core/shapes/presets.js';
4
+ export { cmsGallery } from '../../core/shapes/presets.js';
5
+ </script>
6
+
7
+ <script lang="ts">
8
+ import type { Snippet } from 'svelte';
9
+ import Structured from './cms-structured.svelte';
10
+ import { cmsGallery, type CmsGalleryValue, type GalleryItem } from '../../core/shapes/presets.js';
11
+
12
+ type Props = {
13
+ name: string;
14
+ scope?: string;
15
+ /** The full value, or a bare item list. */
16
+ fallback?: Partial<CmsGalleryValue> | Partial<GalleryItem>[];
17
+ value?: unknown;
18
+ children: Snippet<[GalleryItem[]]>;
19
+ };
20
+ let { name, scope, fallback, value, children }: Props = $props();
21
+ </script>
22
+
23
+ <Structured
24
+ schema={cmsGallery}
25
+ {name}
26
+ {scope}
27
+ {fallback}
28
+ {value}
29
+ view={(v: CmsGalleryValue) => v.items.filter((i) => i.image !== null)}
30
+ {children}
31
+ />