@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.
- package/dist/components/contact/ContactForm.svelte +44 -14
- package/dist/components/jobs/JobList.svelte +73 -12
- package/dist/components/jobs/JobList.svelte.d.ts +19 -2
- package/dist/components/projects/ProjectCard.svelte +78 -33
- package/dist/components/projects/ProjectCard.svelte.d.ts +11 -2
- package/dist/components/team/CollaboratorList.svelte +87 -9
- package/dist/components/team/CollaboratorList.svelte.d.ts +26 -0
- package/dist/components/team/TeamMemberCard.svelte +41 -19
- package/dist/components/team/TeamMemberCard.svelte.d.ts +8 -4
- package/dist/components/timeline/Timeline.svelte +47 -2
- package/dist/components/timeline/Timeline.svelte.d.ts +8 -0
- package/dist/components/timeline/TimelineMilestone.svelte +94 -38
- package/dist/components/timeline/TimelineMilestone.svelte.d.ts +16 -2
- package/dist/components/ui/RichText.svelte +12 -6
- package/dist/components/ui/SearchInput.svelte +31 -10
- package/dist/components/ui/SearchInput.svelte.d.ts +7 -0
- package/dist/components/weeklies/SortSelect.svelte +41 -11
- package/dist/components/weeklies/SortSelect.svelte.d.ts +8 -0
- package/dist/components/weeklies/WeeklieCard.svelte +44 -19
- package/dist/components/weeklies/WeeklieCard.svelte.d.ts +9 -2
- package/dist/config/context.js +6 -1
- package/dist/config/edit-messages.d.ts +47 -0
- package/dist/config/edit-messages.js +24 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +1 -0
- package/dist/config/types.d.ts +8 -0
- package/dist/content/types.d.ts +4 -0
- package/dist/edit/Editable.svelte +5 -3
- package/dist/edit/chrome/AddSlot.svelte +89 -0
- package/dist/edit/chrome/AddSlot.svelte.d.ts +17 -0
- package/dist/edit/chrome/ConfirmDialog.svelte +65 -0
- package/dist/edit/chrome/ConfirmDialog.svelte.d.ts +18 -0
- package/dist/edit/chrome/EditFrame.svelte +171 -0
- package/dist/edit/chrome/EditFrame.svelte.d.ts +33 -0
- package/dist/edit/chrome/EditPanel.svelte +29 -0
- package/dist/edit/chrome/EditPanel.svelte.d.ts +15 -0
- package/dist/edit/chrome/EditPopover.svelte +57 -0
- package/dist/edit/chrome/EditPopover.svelte.d.ts +17 -0
- package/dist/edit/chrome/PropertyRow.svelte +241 -0
- package/dist/edit/chrome/PropertyRow.svelte.d.ts +17 -0
- package/dist/edit/helpers.d.ts +19 -1
- package/dist/edit/helpers.js +21 -0
- package/dist/edit/index.d.ts +8 -2
- package/dist/edit/index.js +6 -1
- package/dist/edit/types.d.ts +82 -2
- 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={
|
|
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
|
-
/**
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
>
|
|
75
|
-
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
/**
|
|
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 ??
|
|
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()}>
|
|
86
|
-
|
|
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">
|
|
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=
|
|
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"
|
|
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
|
-
|
|
74
|
-
<
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
<
|
|
23
|
-
{#snippet
|
|
24
|
-
<
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
/**
|
|
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 {
|
package/dist/config/context.js
CHANGED
|
@@ -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)
|