@vit-foundation/ui 0.10.1 → 0.12.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 (46) hide show
  1. package/dist/components/contact/ContactForm.svelte +44 -14
  2. package/dist/components/jobs/JobList.svelte +73 -12
  3. package/dist/components/jobs/JobList.svelte.d.ts +19 -2
  4. package/dist/components/projects/ProjectCard.svelte +78 -33
  5. package/dist/components/projects/ProjectCard.svelte.d.ts +11 -2
  6. package/dist/components/team/CollaboratorList.svelte +87 -9
  7. package/dist/components/team/CollaboratorList.svelte.d.ts +26 -0
  8. package/dist/components/team/TeamMemberCard.svelte +41 -19
  9. package/dist/components/team/TeamMemberCard.svelte.d.ts +8 -4
  10. package/dist/components/timeline/Timeline.svelte +47 -2
  11. package/dist/components/timeline/Timeline.svelte.d.ts +8 -0
  12. package/dist/components/timeline/TimelineMilestone.svelte +94 -38
  13. package/dist/components/timeline/TimelineMilestone.svelte.d.ts +16 -2
  14. package/dist/components/ui/RichText.svelte +12 -6
  15. package/dist/components/ui/SearchInput.svelte +31 -10
  16. package/dist/components/ui/SearchInput.svelte.d.ts +7 -0
  17. package/dist/components/weeklies/SortSelect.svelte +41 -11
  18. package/dist/components/weeklies/SortSelect.svelte.d.ts +8 -0
  19. package/dist/components/weeklies/WeeklieCard.svelte +44 -19
  20. package/dist/components/weeklies/WeeklieCard.svelte.d.ts +9 -2
  21. package/dist/config/context.js +6 -1
  22. package/dist/config/edit-messages.d.ts +47 -0
  23. package/dist/config/edit-messages.js +24 -0
  24. package/dist/config/index.d.ts +2 -0
  25. package/dist/config/index.js +1 -0
  26. package/dist/config/types.d.ts +8 -0
  27. package/dist/content/types.d.ts +4 -0
  28. package/dist/edit/Editable.svelte +5 -3
  29. package/dist/edit/chrome/AddSlot.svelte +89 -0
  30. package/dist/edit/chrome/AddSlot.svelte.d.ts +17 -0
  31. package/dist/edit/chrome/ConfirmDialog.svelte +65 -0
  32. package/dist/edit/chrome/ConfirmDialog.svelte.d.ts +18 -0
  33. package/dist/edit/chrome/EditFrame.svelte +171 -0
  34. package/dist/edit/chrome/EditFrame.svelte.d.ts +33 -0
  35. package/dist/edit/chrome/EditPanel.svelte +29 -0
  36. package/dist/edit/chrome/EditPanel.svelte.d.ts +15 -0
  37. package/dist/edit/chrome/EditPopover.svelte +57 -0
  38. package/dist/edit/chrome/EditPopover.svelte.d.ts +17 -0
  39. package/dist/edit/chrome/PropertyRow.svelte +241 -0
  40. package/dist/edit/chrome/PropertyRow.svelte.d.ts +17 -0
  41. package/dist/edit/helpers.d.ts +19 -1
  42. package/dist/edit/helpers.js +21 -0
  43. package/dist/edit/index.d.ts +8 -2
  44. package/dist/edit/index.js +6 -1
  45. package/dist/edit/types.d.ts +82 -2
  46. package/package.json +1 -1
@@ -2,6 +2,9 @@
2
2
  import { getUiConfig } from '../../config/context.js';
3
3
  import type { MilestoneData } from '../../content/types.js';
4
4
  import type { MilestoneEditMap } from './TimelineMilestone.svelte';
5
+ import AddSlot from '../../edit/chrome/AddSlot.svelte';
6
+ import { getEditAdapter } from '../../edit/context.js';
7
+ import type { CollectionRef } from '../../edit/types.js';
5
8
  import { yearOf } from '../../utils/dates.js';
6
9
  import TimelineMilestone from './TimelineMilestone.svelte';
7
10
 
@@ -11,11 +14,30 @@
11
14
  variant?: 'compact' | 'full';
12
15
  /** Edit descriptors for one milestone's fields; see TimelineMilestone. */
13
16
  editFor?: (milestone: MilestoneData) => MilestoneEditMap | undefined;
17
+ /**
18
+ * Names the collection these milestones are rows of. Set alongside an
19
+ * adapter with `applyOp`, it turns on the add slots and each frame's
20
+ * remove. NO reorder: order derives from `occurredOn` — editing the
21
+ * date IS the reorder.
22
+ */
23
+ collection?: CollectionRef;
14
24
  }
15
25
 
16
- let { milestones, variant = 'compact', editFor }: Props = $props();
26
+ let { milestones, variant = 'compact', editFor, collection }: Props = $props();
17
27
 
18
28
  const config = getUiConfig();
29
+ const adapter = getEditAdapter();
30
+
31
+ const structural = $derived(
32
+ collection !== undefined && (adapter?.isEditing ?? false) && adapter?.applyOp !== undefined
33
+ );
34
+
35
+ /** The map the host gave, plus removal — the list owns identity and order. */
36
+ function editMapFor(milestone: MilestoneData): MilestoneEditMap | undefined {
37
+ const map = editFor?.(milestone);
38
+ if (!structural || !collection) return map;
39
+ return { ...map, removeOp: { kind: 'remove', collection, id: milestone.id } };
40
+ }
19
41
 
20
42
  interface TimelineEntry {
21
43
  milestone: MilestoneData;
@@ -46,13 +68,29 @@
46
68
  >
47
69
  <ol>
48
70
  {#each entries as entry (entry.milestone.id)}
71
+ {#if structural && collection}
72
+ <li class="add-slot">
73
+ <AddSlot
74
+ op={{
75
+ kind: 'create',
76
+ collection,
77
+ anchor: { id: entry.milestone.id, placement: 'before' }
78
+ }}
79
+ />
80
+ </li>
81
+ {/if}
49
82
  <li>
50
83
  {#if entry.yearMarker}
51
84
  <span class="year">{entry.yearMarker}</span>
52
85
  {/if}
53
- <TimelineMilestone milestone={entry.milestone} edit={editFor?.(entry.milestone)} />
86
+ <TimelineMilestone milestone={entry.milestone} edit={editMapFor(entry.milestone)} />
54
87
  </li>
55
88
  {/each}
89
+ {#if structural && collection}
90
+ <li class="add-slot">
91
+ <AddSlot op={{ kind: 'create', collection }} />
92
+ </li>
93
+ {/if}
56
94
  </ol>
57
95
  </div>
58
96
 
@@ -92,6 +130,13 @@
92
130
  flex-basis: 18rem;
93
131
  }
94
132
 
133
+ /* The add affordances sit between the cards without taking a card's slot. */
134
+ li.add-slot,
135
+ .full li.add-slot {
136
+ flex: 0 0 auto;
137
+ align-self: center;
138
+ }
139
+
95
140
  .year {
96
141
  position: absolute;
97
142
  top: -2.4rem;
@@ -1,11 +1,19 @@
1
1
  import type { MilestoneData } from '../../content/types.js';
2
2
  import type { MilestoneEditMap } from './TimelineMilestone.svelte';
3
+ import type { CollectionRef } from '../../edit/types.js';
3
4
  interface Props {
4
5
  milestones: MilestoneData[];
5
6
  /** 'compact' = homepage slice; 'full' = transparency history with year markers. */
6
7
  variant?: 'compact' | 'full';
7
8
  /** Edit descriptors for one milestone's fields; see TimelineMilestone. */
8
9
  editFor?: (milestone: MilestoneData) => MilestoneEditMap | undefined;
10
+ /**
11
+ * Names the collection these milestones are rows of. Set alongside an
12
+ * adapter with `applyOp`, it turns on the add slots and each frame's
13
+ * remove. NO reorder: order derives from `occurredOn` — editing the
14
+ * date IS the reorder.
15
+ */
16
+ collection?: CollectionRef;
9
17
  }
10
18
  declare const Timeline: import("svelte").Component<Props, {}, "">;
11
19
  type Timeline = ReturnType<typeof Timeline>;
@@ -1,17 +1,31 @@
1
1
  <script module lang="ts">
2
- import type { EditDescriptor } from '../../edit/types.js';
2
+ import type { EditDescriptor, EntityOp, PropertyDescriptor } from '../../edit/types.js';
3
3
 
4
- /** Which of the milestone's localized fields are editable at this render site. */
4
+ /**
5
+ * Which of the milestone's fields are editable at this render site:
6
+ * localized text inline (EditDescriptor), scalars through the frame's
7
+ * property panel (PropertyDescriptor), and removal from the collection.
8
+ */
5
9
  export interface MilestoneEditMap {
6
10
  title?: EditDescriptor;
7
11
  body?: EditDescriptor;
12
+ occurredOn?: PropertyDescriptor;
13
+ category?: PropertyDescriptor;
14
+ linkUrl?: PropertyDescriptor;
15
+ image?: PropertyDescriptor;
16
+ /** Accessible name for the frame, e.g. "Fita: Neix la fundació". */
17
+ label?: string;
18
+ /** Set by Timeline from its `collection` — removal of this milestone. */
19
+ removeOp?: Extract<EntityOp, { kind: 'remove' }>;
8
20
  }
9
21
  </script>
10
22
 
11
23
  <script lang="ts">
12
24
  import { getUiConfig } from '../../config/context.js';
13
25
  import ActionLabel from '../../edit/ActionLabel.svelte';
14
- import type { MilestoneData } from '../../content/types.js';
26
+ import type { MilestoneCategory, MilestoneData } from '../../content/types.js';
27
+ import EditFrame from '../../edit/chrome/EditFrame.svelte';
28
+ import EditPanel from '../../edit/chrome/EditPanel.svelte';
15
29
  import Editable from '../../edit/Editable.svelte';
16
30
  import { MILESTONE_CATEGORY_COLOR, milestoneCategoryLabel } from '../../utils/milestones.js';
17
31
  import CardMedia from '../ui/CardMedia.svelte';
@@ -28,6 +42,41 @@
28
42
 
29
43
  const config = getUiConfig();
30
44
 
45
+ const CATEGORIES = Object.keys(MILESTONE_CATEGORY_COLOR) as MilestoneCategory[];
46
+
47
+ /**
48
+ * The panel rows, zipped from the map's descriptors and this milestone's
49
+ * values. The category select's options come from the same labels the chip
50
+ * renders — filled here so the host never re-derives them.
51
+ */
52
+ const panelRows = $derived(
53
+ [
54
+ edit?.occurredOn && { descriptor: edit.occurredOn, value: milestone.occurredOn },
55
+ edit?.category && {
56
+ descriptor: {
57
+ options: CATEGORIES.map((category) => ({
58
+ value: category,
59
+ label: milestoneCategoryLabel(category, config.messages)
60
+ })),
61
+ ...edit.category
62
+ },
63
+ value: milestone.category
64
+ },
65
+ edit?.linkUrl && { descriptor: edit.linkUrl, value: milestone.linkUrl },
66
+ edit?.image && { descriptor: edit.image, value: milestone.imageUrls[0] ?? null }
67
+ ].filter((row) => row !== undefined)
68
+ );
69
+
70
+ const frameSpec = $derived(
71
+ edit && (panelRows.length > 0 || edit.removeOp)
72
+ ? {
73
+ label: edit.label ?? milestone.title,
74
+ hasPanel: panelRows.length > 0,
75
+ removeOp: edit.removeOp
76
+ }
77
+ : undefined
78
+ );
79
+
31
80
  const color = $derived(MILESTONE_CATEGORY_COLOR[milestone.category]);
32
81
  /**
33
82
  * `link_url` is an unconstrained editable column, and the `press` category
@@ -48,42 +97,49 @@
48
97
 
49
98
  <article style={`--milestone-color: ${color}`}>
50
99
  <span class="dot" aria-hidden="true"></span>
51
- <Editable
52
- edit={config.messageEdit?.(`category_${milestone.category}`)}
53
- value={milestoneCategoryLabel(milestone.category, config.messages)}
54
- >
55
- {#snippet children(text, attrs)}<p class="category" {...attrs}>{text}</p>{/snippet}
56
- </Editable>
57
- <DateText value={milestone.occurredOn} />
58
- <Editable edit={edit?.title} value={milestone.title}>
59
- {#snippet children(text, attrs)}<h3 {...attrs}>{text}</h3>{/snippet}
60
- </Editable>
61
- {#if milestone.body}
62
- <Editable edit={edit?.body} value={milestone.body}>
63
- {#snippet children(text, attrs)}<p class="body" {...attrs}>{text}</p>{/snippet}
100
+ <!-- The frame sits INSIDE the article so the timeline's flex row never
101
+ gains an unexpected child; inactive it renders the content alone. -->
102
+ <EditFrame spec={frameSpec}>
103
+ {#snippet panel()}
104
+ <EditPanel rows={panelRows} />
105
+ {/snippet}
106
+ <Editable
107
+ edit={config.messageEdit?.(`category_${milestone.category}`)}
108
+ value={milestoneCategoryLabel(milestone.category, config.messages)}
109
+ >
110
+ {#snippet children(text, attrs)}<p class="category" {...attrs}>{text}</p>{/snippet}
111
+ </Editable>
112
+ <DateText value={milestone.occurredOn} />
113
+ <Editable edit={edit?.title} value={milestone.title}>
114
+ {#snippet children(text, attrs)}<h3 {...attrs}>{text}</h3>{/snippet}
64
115
  </Editable>
65
- {/if}
66
- {#if milestone.imageUrls.length > 0}
67
- <CardMedia src={milestone.imageUrls[0]} alt="" width="1200" height="675" />
68
- {/if}
69
- {#if milestone.linkUrl}
70
- <p class="more">
71
- <ActionLabel
72
- edit={config.messageEdit?.('common_readMore')}
73
- value={config.messages.common_readMore()}
74
- >
75
- {#snippet control()}
76
- {#if isExternal}
77
- <a href={milestone.linkUrl} rel="external noopener"
78
- >{config.messages.common_readMore()}</a
79
- >
80
- {:else if milestone.linkUrl}
81
- <Link href={milestone.linkUrl}>{config.messages.common_readMore()}</Link>
82
- {/if}
83
- {/snippet}
84
- </ActionLabel>
85
- </p>
86
- {/if}
116
+ {#if milestone.body}
117
+ <Editable edit={edit?.body} value={milestone.body}>
118
+ {#snippet children(text, attrs)}<p class="body" {...attrs}>{text}</p>{/snippet}
119
+ </Editable>
120
+ {/if}
121
+ {#if milestone.imageUrls.length > 0}
122
+ <CardMedia src={milestone.imageUrls[0]} alt="" width="1200" height="675" />
123
+ {/if}
124
+ {#if milestone.linkUrl}
125
+ <p class="more">
126
+ <ActionLabel
127
+ edit={config.messageEdit?.('common_readMore')}
128
+ value={config.messages.common_readMore()}
129
+ >
130
+ {#snippet control()}
131
+ {#if isExternal}
132
+ <a href={milestone.linkUrl} rel="external noopener"
133
+ >{config.messages.common_readMore()}</a
134
+ >
135
+ {:else if milestone.linkUrl}
136
+ <Link href={milestone.linkUrl}>{config.messages.common_readMore()}</Link>
137
+ {/if}
138
+ {/snippet}
139
+ </ActionLabel>
140
+ </p>
141
+ {/if}
142
+ </EditFrame>
87
143
  </article>
88
144
 
89
145
  <style>
@@ -1,8 +1,22 @@
1
- import type { EditDescriptor } from '../../edit/types.js';
2
- /** Which of the milestone's localized fields are editable at this render site. */
1
+ import type { EditDescriptor, EntityOp, PropertyDescriptor } from '../../edit/types.js';
2
+ /**
3
+ * Which of the milestone's fields are editable at this render site:
4
+ * localized text inline (EditDescriptor), scalars through the frame's
5
+ * property panel (PropertyDescriptor), and removal from the collection.
6
+ */
3
7
  export interface MilestoneEditMap {
4
8
  title?: EditDescriptor;
5
9
  body?: EditDescriptor;
10
+ occurredOn?: PropertyDescriptor;
11
+ category?: PropertyDescriptor;
12
+ linkUrl?: PropertyDescriptor;
13
+ image?: PropertyDescriptor;
14
+ /** Accessible name for the frame, e.g. "Fita: Neix la fundació". */
15
+ label?: string;
16
+ /** Set by Timeline from its `collection` — removal of this milestone. */
17
+ removeOp?: Extract<EntityOp, {
18
+ kind: 'remove';
19
+ }>;
6
20
  }
7
21
  import type { MilestoneData } from '../../content/types.js';
8
22
  interface Props {
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { getUiConfig } from '../../config/context.js';
2
3
  import { renderBody } from '../../content/richtext.js';
3
4
  import { getEditAdapter } from '../../edit/context.js';
4
5
  import type { EditDescriptor } from '../../edit/types.js';
@@ -23,6 +24,7 @@
23
24
  let { body, edit }: Props = $props();
24
25
 
25
26
  const adapter = getEditAdapter();
27
+ const config = getUiConfig();
26
28
  const canEdit = $derived(edit !== undefined && (adapter?.isEditing ?? false));
27
29
 
28
30
  /** Last persisted body: what renders, and what Cancel·la returns to. */
@@ -79,16 +81,18 @@
79
81
  class="control"
80
82
  bind:value={draft}
81
83
  rows={Math.max(6, draft.split('\n').length + 1)}
82
- aria-label={edit?.label ?? 'Edita el contingut'}
84
+ aria-label={edit?.label ?? config.editMessages.edit_editBody()}
83
85
  ></textarea>
84
86
  <div class="actions">
85
- <Button pending={status === 'saving' ? 1 : 0} onclick={() => void save()}>Desa</Button>
86
- <GhostButton onclick={cancel}>Cancel·la</GhostButton>
87
+ <Button pending={status === 'saving' ? 1 : 0} onclick={() => void save()}>
88
+ {config.editMessages.edit_save()}
89
+ </Button>
90
+ <GhostButton onclick={cancel}>{config.editMessages.edit_cancel()}</GhostButton>
87
91
  {#if status === 'error'}
88
- <p class="error" role="alert">Error en desar. Torna-ho a provar.</p>
92
+ <p class="error" role="alert">{config.editMessages.edit_saveError()}. Torna-ho a provar.</p>
89
93
  {/if}
90
94
  </div>
91
- <div class="richtext preview" aria-label="Vista prèvia">
95
+ <div class="richtext preview" aria-label={config.editMessages.edit_preview()}>
92
96
  {#each preview as block, index (index)}
93
97
  {#if block.type === 'h2'}
94
98
  <h2>{block.text}</h2>
@@ -108,7 +112,9 @@
108
112
  {/if}
109
113
  {/each}
110
114
  {#if canEdit}
111
- <p class="edit-row"><GhostButton onclick={open}>Edita</GhostButton></p>
115
+ <p class="edit-row">
116
+ <GhostButton onclick={open}>{config.editMessages.edit_edit()}</GhostButton>
117
+ </p>
112
118
  {/if}
113
119
  </div>
114
120
  {/if}
@@ -1,4 +1,8 @@
1
1
  <script lang="ts">
2
+ import EditFrame from '../../edit/chrome/EditFrame.svelte';
3
+ import EditPanel from '../../edit/chrome/EditPanel.svelte';
4
+ import type { PropertyDescriptor } from '../../edit/types.js';
5
+
2
6
  interface Props {
3
7
  /** Ties the label to the input; override when a page renders two. */
4
8
  id?: string;
@@ -8,6 +12,12 @@
8
12
  /** Called after the debounce delay with the trimmed query. */
9
13
  onsearch: (query: string) => void;
10
14
  debounceMs?: number;
15
+ /**
16
+ * A placeholder cannot hold a caret, so it edits through the frame's
17
+ * panel: one text row over whatever the host's descriptor names
18
+ * (a chrome key on the foundation site). Inert without an adapter.
19
+ */
20
+ placeholderEdit?: PropertyDescriptor;
11
21
  }
12
22
 
13
23
  const DEFAULT_DEBOUNCE_MS = 300;
@@ -18,9 +28,12 @@
18
28
  placeholder,
19
29
  label,
20
30
  onsearch,
21
- debounceMs = DEFAULT_DEBOUNCE_MS
31
+ debounceMs = DEFAULT_DEBOUNCE_MS,
32
+ placeholderEdit
22
33
  }: Props = $props();
23
34
 
35
+ const frameSpec = $derived(placeholderEdit !== undefined ? { label, hasPanel: true } : undefined);
36
+
24
37
  // svelte-ignore state_referenced_locally
25
38
  let query = $state(value);
26
39
  let timer: ReturnType<typeof setTimeout> | undefined;
@@ -70,15 +83,23 @@
70
83
  </script>
71
84
 
72
85
  <form role="search" onsubmit={handleSubmit}>
73
- <label class="visually-hidden" for={id}>{label}</label>
74
- <input
75
- {id}
76
- class="control"
77
- type="search"
78
- {placeholder}
79
- bind:value={query}
80
- oninput={handleInput}
81
- />
86
+ <!-- Inside the form, so the host's toolbar layout never gains a child. -->
87
+ <EditFrame spec={frameSpec}>
88
+ {#snippet panel()}
89
+ {#if placeholderEdit}
90
+ <EditPanel rows={[{ descriptor: placeholderEdit, value: placeholder }]} />
91
+ {/if}
92
+ {/snippet}
93
+ <label class="visually-hidden" for={id}>{label}</label>
94
+ <input
95
+ {id}
96
+ class="control"
97
+ type="search"
98
+ {placeholder}
99
+ bind:value={query}
100
+ oninput={handleInput}
101
+ />
102
+ </EditFrame>
82
103
  </form>
83
104
 
84
105
  <style>
@@ -1,3 +1,4 @@
1
+ import type { PropertyDescriptor } from '../../edit/types.js';
1
2
  interface Props {
2
3
  /** Ties the label to the input; override when a page renders two. */
3
4
  id?: string;
@@ -7,6 +8,12 @@ interface Props {
7
8
  /** Called after the debounce delay with the trimmed query. */
8
9
  onsearch: (query: string) => void;
9
10
  debounceMs?: number;
11
+ /**
12
+ * A placeholder cannot hold a caret, so it edits through the frame's
13
+ * panel: one text row over whatever the host's descriptor names
14
+ * (a chrome key on the foundation site). Inert without an adapter.
15
+ */
16
+ placeholderEdit?: PropertyDescriptor;
10
17
  }
11
18
  declare const SearchInput: import("svelte").Component<Props, {}, "">;
12
19
  type SearchInput = ReturnType<typeof SearchInput>;
@@ -2,34 +2,64 @@
2
2
  import { getUiConfig } from '../../config/context.js';
3
3
  import type { SortDirection } from '../../content/types.js';
4
4
  import ActionLabel from '../../edit/ActionLabel.svelte';
5
+ import EditFrame from '../../edit/chrome/EditFrame.svelte';
6
+ import EditPanel from '../../edit/chrome/EditPanel.svelte';
7
+ import type { PropertyDescriptor } from '../../edit/types.js';
5
8
 
6
9
  interface Props {
7
10
  value: SortDirection;
8
11
  onchange: (value: SortDirection) => void;
12
+ /**
13
+ * An <option> cannot hold a caret, so the option labels edit through
14
+ * the frame's panel — one text row per direction, descriptors from the
15
+ * host (chrome keys on the foundation site). The visible label keeps
16
+ * its ActionLabel swap. Inert without an adapter.
17
+ */
18
+ optionsEdit?: (direction: SortDirection) => PropertyDescriptor | undefined;
9
19
  }
10
20
 
11
- let { value, onchange }: Props = $props();
21
+ let { value, onchange, optionsEdit }: Props = $props();
12
22
 
13
23
  const config = getUiConfig();
14
24
  const msg = $derived(config.messages);
15
25
 
26
+ const panelRows = $derived(
27
+ (
28
+ [
29
+ ['desc', msg.weeklies_sortDesc()],
30
+ ['asc', msg.weeklies_sortAsc()]
31
+ ] as const
32
+ ).flatMap(([direction, label]) => {
33
+ const descriptor = optionsEdit?.(direction);
34
+ return descriptor ? [{ descriptor, value: label }] : [];
35
+ })
36
+ );
37
+ const frameSpec = $derived(
38
+ panelRows.length > 0 ? { label: msg.weeklies_sortLabel(), hasPanel: true } : undefined
39
+ );
40
+
16
41
  function handleChange(event: Event): void {
17
42
  const target = event.currentTarget as HTMLSelectElement;
18
43
  onchange(target.value === 'asc' ? 'asc' : 'desc');
19
44
  }
20
45
  </script>
21
46
 
22
- <ActionLabel edit={config.messageEdit?.('weeklies_sortLabel')} value={msg.weeklies_sortLabel()}>
23
- {#snippet control()}
24
- <label>
25
- <span>{msg.weeklies_sortLabel()}</span>
26
- <select class="control" {value} onchange={handleChange}>
27
- <option value="desc">{msg.weeklies_sortDesc()}</option>
28
- <option value="asc">{msg.weeklies_sortAsc()}</option>
29
- </select>
30
- </label>
47
+ <EditFrame spec={frameSpec}>
48
+ {#snippet panel()}
49
+ <EditPanel rows={panelRows} />
31
50
  {/snippet}
32
- </ActionLabel>
51
+ <ActionLabel edit={config.messageEdit?.('weeklies_sortLabel')} value={msg.weeklies_sortLabel()}>
52
+ {#snippet control()}
53
+ <label>
54
+ <span>{msg.weeklies_sortLabel()}</span>
55
+ <select class="control" {value} onchange={handleChange}>
56
+ <option value="desc">{msg.weeklies_sortDesc()}</option>
57
+ <option value="asc">{msg.weeklies_sortAsc()}</option>
58
+ </select>
59
+ </label>
60
+ {/snippet}
61
+ </ActionLabel>
62
+ </EditFrame>
33
63
 
34
64
  <style>
35
65
  label {
@@ -1,7 +1,15 @@
1
1
  import type { SortDirection } from '../../content/types.js';
2
+ import type { PropertyDescriptor } from '../../edit/types.js';
2
3
  interface Props {
3
4
  value: SortDirection;
4
5
  onchange: (value: SortDirection) => void;
6
+ /**
7
+ * An <option> cannot hold a caret, so the option labels edit through
8
+ * the frame's panel — one text row per direction, descriptors from the
9
+ * host (chrome keys on the foundation site). The visible label keeps
10
+ * its ActionLabel swap. Inert without an adapter.
11
+ */
12
+ optionsEdit?: (direction: SortDirection) => PropertyDescriptor | undefined;
5
13
  }
6
14
  declare const SortSelect: import("svelte").Component<Props, {}, "">;
7
15
  type SortSelect = ReturnType<typeof SortSelect>;
@@ -1,10 +1,17 @@
1
1
  <script module lang="ts">
2
- import type { EditDescriptor } from '../../edit/types.js';
2
+ import type { EditDescriptor, PropertyDescriptor } from '../../edit/types.js';
3
3
 
4
- /** Which of the card's localized fields are editable at this render site. */
4
+ /**
5
+ * Which of the card's fields are editable at this render site: localized
6
+ * text inline, the cover image through the frame's panel. NO collection
7
+ * ops — weeklies keep their own authoring flow in the CMS.
8
+ */
5
9
  export interface WeeklyEditMap {
6
10
  title?: EditDescriptor;
7
11
  excerpt?: EditDescriptor;
12
+ image?: PropertyDescriptor;
13
+ /** Accessible name for the frame, e.g. "Weekly #12". */
14
+ label?: string;
8
15
  }
9
16
  </script>
10
17
 
@@ -13,6 +20,8 @@
13
20
  import type { WeeklyCardData } from '../../content/types.js';
14
21
  import { getEditAdapter } from '../../edit/context.js';
15
22
  import Editable from '../../edit/Editable.svelte';
23
+ import EditFrame from '../../edit/chrome/EditFrame.svelte';
24
+ import EditPanel from '../../edit/chrome/EditPanel.svelte';
16
25
  import CardMedia from '../ui/CardMedia.svelte';
17
26
  import CardTitle from '../ui/CardTitle.svelte';
18
27
  import DateText from '../ui/DateText.svelte';
@@ -33,26 +42,42 @@
33
42
  // a contenteditable inside an anchor still navigates on click, which
34
43
  // would throw the editor off the page mid-draft.
35
44
  const titleEditing = $derived(edit?.title !== undefined && (adapter?.isEditing ?? false));
45
+
46
+ const panelRows = $derived(
47
+ [edit?.image && { descriptor: edit.image, value: weekly.imageUrl }].filter(
48
+ (row) => row !== undefined
49
+ )
50
+ );
51
+ const frameSpec = $derived(
52
+ edit && panelRows.length > 0 ? { label: edit.label ?? weekly.title, hasPanel: true } : undefined
53
+ );
36
54
  </script>
37
55
 
38
56
  <article>
39
- <header>
40
- <span class="number">{config.messages.weeklie_number({ number: weekly.number })}</span>
41
- <DateText value={weekly.publishedOn} />
42
- </header>
43
- <CardMedia src={weekly.imageUrl} alt="" ratio="1 / 1" width="600" height="600" />
44
- <CardTitle>
45
- {#if titleEditing}
46
- <Editable edit={edit?.title} value={weekly.title}>
47
- {#snippet children(text, attrs)}<span {...attrs}>{text}</span>{/snippet}
48
- </Editable>
49
- {:else}
50
- <Link href={`/weeklies/${weekly.slug}`}>{weekly.title}</Link>
51
- {/if}
52
- </CardTitle>
53
- <Editable edit={edit?.excerpt} value={weekly.excerpt}>
54
- {#snippet children(text, attrs)}<p {...attrs}>{text}</p>{/snippet}
55
- </Editable>
57
+ <!-- Inside the article, so the grid the host lays cards into never gains
58
+ an unexpected child; inactive it renders the content alone. -->
59
+ <EditFrame spec={frameSpec}>
60
+ {#snippet panel()}
61
+ <EditPanel rows={panelRows} />
62
+ {/snippet}
63
+ <header>
64
+ <span class="number">{config.messages.weeklie_number({ number: weekly.number })}</span>
65
+ <DateText value={weekly.publishedOn} />
66
+ </header>
67
+ <CardMedia src={weekly.imageUrl} alt="" ratio="1 / 1" width="600" height="600" />
68
+ <CardTitle>
69
+ {#if titleEditing}
70
+ <Editable edit={edit?.title} value={weekly.title}>
71
+ {#snippet children(text, attrs)}<span {...attrs}>{text}</span>{/snippet}
72
+ </Editable>
73
+ {:else}
74
+ <Link href={`/weeklies/${weekly.slug}`}>{weekly.title}</Link>
75
+ {/if}
76
+ </CardTitle>
77
+ <Editable edit={edit?.excerpt} value={weekly.excerpt}>
78
+ {#snippet children(text, attrs)}<p {...attrs}>{text}</p>{/snippet}
79
+ </Editable>
80
+ </EditFrame>
56
81
  </article>
57
82
 
58
83
  <style>
@@ -1,8 +1,15 @@
1
- import type { EditDescriptor } from '../../edit/types.js';
2
- /** Which of the card's localized fields are editable at this render site. */
1
+ import type { EditDescriptor, PropertyDescriptor } from '../../edit/types.js';
2
+ /**
3
+ * Which of the card's fields are editable at this render site: localized
4
+ * text inline, the cover image through the frame's panel. NO collection
5
+ * ops — weeklies keep their own authoring flow in the CMS.
6
+ */
3
7
  export interface WeeklyEditMap {
4
8
  title?: EditDescriptor;
5
9
  excerpt?: EditDescriptor;
10
+ image?: PropertyDescriptor;
11
+ /** Accessible name for the frame, e.g. "Weekly #12". */
12
+ label?: string;
6
13
  }
7
14
  import type { WeeklyCardData } from '../../content/types.js';
8
15
  interface Props {
@@ -1,4 +1,5 @@
1
1
  import { getContext, setContext } from 'svelte';
2
+ import { defaultEditMessages } from './edit-messages.js';
2
3
  import { defaultMessages } from './messages.js';
3
4
  import { BASE_LOCALE, LOCALES } from './types.js';
4
5
  const KEY = Symbol.for('@vit-foundation/ui:config');
@@ -14,7 +15,8 @@ export const DEFAULT_UI_CONFIG = {
14
15
  url: () => undefined,
15
16
  canonicalPathname: (url) => url.pathname,
16
17
  siteName: 'ViT',
17
- messages: defaultMessages
18
+ messages: defaultMessages,
19
+ editMessages: defaultEditMessages
18
20
  };
19
21
  /**
20
22
  * Installs the app's config for the subtree. Takes a *thunk* so a provider
@@ -38,6 +40,9 @@ export function setUiConfig(input) {
38
40
  get messages() {
39
41
  return input().messages ?? defaultMessages;
40
42
  },
43
+ get editMessages() {
44
+ return input().editMessages ?? defaultEditMessages;
45
+ },
41
46
  // Undefined means inert: components render their message strings as
42
47
  // plain text, which is every app but the CMS.
43
48
  messageEdit: (key) => input().messageEdit?.(key)