@vit-foundation/ui 0.11.0 → 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/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/content/types.d.ts +4 -0
- package/dist/edit/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
<script lang="ts">
|
|
21
21
|
import { getUiConfig } from '../../config/context.js';
|
|
22
22
|
import ActionLabel from '../../edit/ActionLabel.svelte';
|
|
23
|
+
import EditFrame from '../../edit/chrome/EditFrame.svelte';
|
|
24
|
+
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
25
|
+
import { chromeProperty } from '../../edit/helpers.js';
|
|
23
26
|
import { CONTACT_MESSAGE, CONTACT_NAME, EMAIL } from '../../forms/constraints.js';
|
|
24
27
|
import { CONTACT_CATEGORIES } from '../../content/types.js';
|
|
25
28
|
import { contactCategoryLabel } from '../../utils/contact.js';
|
|
@@ -49,27 +52,54 @@
|
|
|
49
52
|
// what order (the host schema derives its enum from CONTACT_CATEGORIES),
|
|
50
53
|
// and the messages own the copy for each.
|
|
51
54
|
const categories = CONTACT_CATEGORIES;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An <option> cannot hold a caret, so the category labels edit through
|
|
58
|
+
* the frame's panel — one text row per category, over the SAME chrome
|
|
59
|
+
* keys the options render. Gated the way the form's other wording is:
|
|
60
|
+
* only where the host exposes `messageEdit` (the CMS mirror); the frame
|
|
61
|
+
* itself further requires an editing adapter with `saveProperty`.
|
|
62
|
+
*/
|
|
63
|
+
const categoryRows = $derived(
|
|
64
|
+
config.messageEdit
|
|
65
|
+
? categories.map((category) => ({
|
|
66
|
+
descriptor: chromeProperty(`contact_category_${category}`, {
|
|
67
|
+
type: 'text' as const,
|
|
68
|
+
label: contactCategoryLabel(category, msg)
|
|
69
|
+
}),
|
|
70
|
+
value: contactCategoryLabel(category, msg)
|
|
71
|
+
}))
|
|
72
|
+
: []
|
|
73
|
+
);
|
|
74
|
+
const categoryFrameSpec = $derived(
|
|
75
|
+
categoryRows.length > 0 ? { label: msg.contact_categoryLabel(), hasPanel: true } : undefined
|
|
76
|
+
);
|
|
52
77
|
</script>
|
|
53
78
|
|
|
54
79
|
{#if f.result?.ok}
|
|
55
80
|
<FormFeedback kind="success">{msg.contact_success()}</FormFeedback>
|
|
56
81
|
{:else}
|
|
57
82
|
<form class="form-stack" {...f}>
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
labelEdit={config.messageEdit?.('contact_categoryLabel')}
|
|
62
|
-
field={f.fields.category}
|
|
63
|
-
constraint={null}
|
|
64
|
-
>
|
|
65
|
-
{#snippet children(attrs)}
|
|
66
|
-
<select class="control" {...attrs} {...f.fields.category.as('select')}>
|
|
67
|
-
{#each categories as category (category)}
|
|
68
|
-
<option value={category}>{contactCategoryLabel(category, msg)}</option>
|
|
69
|
-
{/each}
|
|
70
|
-
</select>
|
|
83
|
+
<EditFrame spec={categoryFrameSpec}>
|
|
84
|
+
{#snippet panel()}
|
|
85
|
+
<EditPanel rows={categoryRows} />
|
|
71
86
|
{/snippet}
|
|
72
|
-
|
|
87
|
+
<Field
|
|
88
|
+
id="contact-category"
|
|
89
|
+
label={msg.contact_categoryLabel()}
|
|
90
|
+
labelEdit={config.messageEdit?.('contact_categoryLabel')}
|
|
91
|
+
field={f.fields.category}
|
|
92
|
+
constraint={null}
|
|
93
|
+
>
|
|
94
|
+
{#snippet children(attrs)}
|
|
95
|
+
<select class="control" {...attrs} {...f.fields.category.as('select')}>
|
|
96
|
+
{#each categories as category (category)}
|
|
97
|
+
<option value={category}>{contactCategoryLabel(category, msg)}</option>
|
|
98
|
+
{/each}
|
|
99
|
+
</select>
|
|
100
|
+
{/snippet}
|
|
101
|
+
</Field>
|
|
102
|
+
</EditFrame>
|
|
73
103
|
|
|
74
104
|
<Field
|
|
75
105
|
id="contact-name"
|
|
@@ -1,17 +1,30 @@
|
|
|
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 one opening's fields are editable at this render site:
|
|
6
|
+
* localized text inline, the posting date through the frame's panel.
|
|
7
|
+
*/
|
|
5
8
|
export interface JobEditMap {
|
|
6
9
|
title?: EditDescriptor;
|
|
7
10
|
description?: EditDescriptor;
|
|
11
|
+
postedOn?: PropertyDescriptor;
|
|
12
|
+
/** Accessible name for the frame — usually the opening's title. */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Set by the list from its `collection` — removal of this opening. */
|
|
15
|
+
removeOp?: Extract<EntityOp, { kind: 'remove' }>;
|
|
8
16
|
}
|
|
9
17
|
</script>
|
|
10
18
|
|
|
11
19
|
<script lang="ts">
|
|
12
20
|
import { getUiConfig } from '../../config/context.js';
|
|
13
21
|
import type { JobOpeningData } from '../../content/types.js';
|
|
22
|
+
import AddSlot from '../../edit/chrome/AddSlot.svelte';
|
|
23
|
+
import EditFrame from '../../edit/chrome/EditFrame.svelte';
|
|
24
|
+
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
25
|
+
import { getEditAdapter } from '../../edit/context.js';
|
|
14
26
|
import Editable from '../../edit/Editable.svelte';
|
|
27
|
+
import type { CollectionRef } from '../../edit/types.js';
|
|
15
28
|
import DateText from '../ui/DateText.svelte';
|
|
16
29
|
|
|
17
30
|
interface Props {
|
|
@@ -21,37 +34,80 @@
|
|
|
21
34
|
* own identity. Returning undefined leaves that row read-only.
|
|
22
35
|
*/
|
|
23
36
|
editFor?: (job: JobOpeningData) => JobEditMap | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Names the collection the openings belong to. Set alongside an adapter
|
|
39
|
+
* with `applyOp`, it turns on the add slot and each frame's remove —
|
|
40
|
+
* for rows that carry an `id` (removal needs an identity).
|
|
41
|
+
*/
|
|
42
|
+
collection?: CollectionRef;
|
|
24
43
|
}
|
|
25
44
|
|
|
26
|
-
let { jobs, editFor }: Props = $props();
|
|
45
|
+
let { jobs, editFor, collection }: Props = $props();
|
|
27
46
|
|
|
28
47
|
const config = getUiConfig();
|
|
48
|
+
const adapter = getEditAdapter();
|
|
29
49
|
const msg = $derived(config.messages);
|
|
50
|
+
|
|
51
|
+
const structural = $derived(
|
|
52
|
+
collection !== undefined && (adapter?.isEditing ?? false) && adapter?.applyOp !== undefined
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/** The map the host gave, plus removal — the list owns identity. */
|
|
56
|
+
function editMapFor(job: JobOpeningData): JobEditMap | undefined {
|
|
57
|
+
const map = editFor?.(job);
|
|
58
|
+
if (!structural || !collection || job.id === undefined) return map;
|
|
59
|
+
return { ...map, removeOp: { kind: 'remove', collection, id: job.id } };
|
|
60
|
+
}
|
|
30
61
|
</script>
|
|
31
62
|
|
|
32
63
|
{#if jobs.length === 0}
|
|
33
64
|
<Editable edit={config.messageEdit?.('jobs_empty')} value={msg.jobs_empty()}>
|
|
34
65
|
{#snippet children(text, attrs)}<p class="empty" {...attrs}>{text}</p>{/snippet}
|
|
35
66
|
</Editable>
|
|
67
|
+
{#if structural && collection}
|
|
68
|
+
<AddSlot op={{ kind: 'create', collection }} />
|
|
69
|
+
{/if}
|
|
36
70
|
<Editable edit={config.messageEdit?.('jobs_newsletterNudge')} value={msg.jobs_newsletterNudge()}>
|
|
37
71
|
{#snippet children(text, attrs)}<p class="nudge" {...attrs}>{text}</p>{/snippet}
|
|
38
72
|
</Editable>
|
|
39
73
|
{:else}
|
|
40
74
|
<ul>
|
|
41
75
|
{#each jobs as job (job.slug)}
|
|
42
|
-
{@const edit =
|
|
76
|
+
{@const edit = editMapFor(job)}
|
|
77
|
+
{@const rows = [edit?.postedOn && { descriptor: edit.postedOn, value: job.postedOn }].filter(
|
|
78
|
+
(row) => row !== undefined
|
|
79
|
+
)}
|
|
43
80
|
<li>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
81
|
+
<!-- Inside the li — see CollaboratorList. -->
|
|
82
|
+
<EditFrame
|
|
83
|
+
spec={edit && (rows.length > 0 || edit.removeOp)
|
|
84
|
+
? {
|
|
85
|
+
label: edit.label ?? job.title,
|
|
86
|
+
hasPanel: rows.length > 0,
|
|
87
|
+
removeOp: edit.removeOp
|
|
88
|
+
}
|
|
89
|
+
: undefined}
|
|
90
|
+
>
|
|
91
|
+
{#snippet panel()}
|
|
92
|
+
<EditPanel {rows} />
|
|
93
|
+
{/snippet}
|
|
94
|
+
<Editable edit={edit?.title} value={job.title}>
|
|
95
|
+
{#snippet children(text, attrs)}<h3 {...attrs}>{text}</h3>{/snippet}
|
|
51
96
|
</Editable>
|
|
52
|
-
|
|
97
|
+
<DateText value={job.postedOn} />
|
|
98
|
+
{#if job.description}
|
|
99
|
+
<Editable edit={edit?.description} value={job.description}>
|
|
100
|
+
{#snippet children(text, attrs)}<p {...attrs}>{text}</p>{/snippet}
|
|
101
|
+
</Editable>
|
|
102
|
+
{/if}
|
|
103
|
+
</EditFrame>
|
|
53
104
|
</li>
|
|
54
105
|
{/each}
|
|
106
|
+
{#if structural && collection}
|
|
107
|
+
<li class="add-slot">
|
|
108
|
+
<AddSlot op={{ kind: 'create', collection }} />
|
|
109
|
+
</li>
|
|
110
|
+
{/if}
|
|
55
111
|
</ul>
|
|
56
112
|
<Editable edit={config.messageEdit?.('jobs_newsletterNudge')} value={msg.jobs_newsletterNudge()}>
|
|
57
113
|
{#snippet children(text, attrs)}<p class="nudge" {...attrs}>{text}</p>{/snippet}
|
|
@@ -91,4 +147,9 @@
|
|
|
91
147
|
.nudge {
|
|
92
148
|
color: var(--color-ink-secondary);
|
|
93
149
|
}
|
|
150
|
+
|
|
151
|
+
li.add-slot {
|
|
152
|
+
background: none;
|
|
153
|
+
padding: 0;
|
|
154
|
+
}
|
|
94
155
|
</style>
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import type { EditDescriptor } from '../../edit/types.js';
|
|
2
|
-
/**
|
|
1
|
+
import type { EditDescriptor, EntityOp, PropertyDescriptor } from '../../edit/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which of one opening's fields are editable at this render site:
|
|
4
|
+
* localized text inline, the posting date through the frame's panel.
|
|
5
|
+
*/
|
|
3
6
|
export interface JobEditMap {
|
|
4
7
|
title?: EditDescriptor;
|
|
5
8
|
description?: EditDescriptor;
|
|
9
|
+
postedOn?: PropertyDescriptor;
|
|
10
|
+
/** Accessible name for the frame — usually the opening's title. */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Set by the list from its `collection` — removal of this opening. */
|
|
13
|
+
removeOp?: Extract<EntityOp, {
|
|
14
|
+
kind: 'remove';
|
|
15
|
+
}>;
|
|
6
16
|
}
|
|
7
17
|
import type { JobOpeningData } from '../../content/types.js';
|
|
18
|
+
import type { CollectionRef } from '../../edit/types.js';
|
|
8
19
|
interface Props {
|
|
9
20
|
jobs: JobOpeningData[];
|
|
10
21
|
/**
|
|
@@ -12,6 +23,12 @@ interface Props {
|
|
|
12
23
|
* own identity. Returning undefined leaves that row read-only.
|
|
13
24
|
*/
|
|
14
25
|
editFor?: (job: JobOpeningData) => JobEditMap | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Names the collection the openings belong to. Set alongside an adapter
|
|
28
|
+
* with `applyOp`, it turns on the add slot and each frame's remove —
|
|
29
|
+
* for rows that carry an `id` (removal needs an identity).
|
|
30
|
+
*/
|
|
31
|
+
collection?: CollectionRef;
|
|
15
32
|
}
|
|
16
33
|
declare const JobList: import("svelte").Component<Props, {}, "">;
|
|
17
34
|
type JobList = ReturnType<typeof JobList>;
|
|
@@ -1,10 +1,19 @@
|
|
|
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 record-shaped scalars through the frame's panel.
|
|
7
|
+
*/
|
|
5
8
|
export interface ProjectEditMap {
|
|
6
9
|
title?: EditDescriptor;
|
|
7
10
|
excerpt?: EditDescriptor;
|
|
11
|
+
kind?: PropertyDescriptor;
|
|
12
|
+
publishedOn?: PropertyDescriptor;
|
|
13
|
+
externalUrl?: PropertyDescriptor;
|
|
14
|
+
image?: PropertyDescriptor;
|
|
15
|
+
/** Accessible name for the frame, e.g. the project's title. */
|
|
16
|
+
label?: string;
|
|
8
17
|
}
|
|
9
18
|
</script>
|
|
10
19
|
|
|
@@ -14,6 +23,8 @@
|
|
|
14
23
|
import type { ProjectCardData } from '../../content/types.js';
|
|
15
24
|
import { getEditAdapter } from '../../edit/context.js';
|
|
16
25
|
import Editable from '../../edit/Editable.svelte';
|
|
26
|
+
import EditFrame from '../../edit/chrome/EditFrame.svelte';
|
|
27
|
+
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
17
28
|
import CardMedia from '../ui/CardMedia.svelte';
|
|
18
29
|
import CardTitle from '../ui/CardTitle.svelte';
|
|
19
30
|
import DateText from '../ui/DateText.svelte';
|
|
@@ -35,41 +46,75 @@
|
|
|
35
46
|
// See WeeklieCard: an editing title renders as plain text, never inside
|
|
36
47
|
// the story link or the external anchor.
|
|
37
48
|
const titleEditing = $derived(edit?.title !== undefined && (adapter?.isEditing ?? false));
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Default kind options are the raw union members: no message keys exist
|
|
52
|
+
* for them, and a host wanting worded labels passes `options` in its
|
|
53
|
+
* descriptor, which wins over these (same spread rule as the milestone
|
|
54
|
+
* category select).
|
|
55
|
+
*/
|
|
56
|
+
const KIND_OPTIONS = [
|
|
57
|
+
{ value: 'collaboration', label: 'Collaboration' },
|
|
58
|
+
{ value: 'passion', label: 'Passion' }
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const panelRows = $derived(
|
|
62
|
+
[
|
|
63
|
+
edit?.kind && {
|
|
64
|
+
descriptor: { options: KIND_OPTIONS, ...edit.kind },
|
|
65
|
+
value: project.kind as string
|
|
66
|
+
},
|
|
67
|
+
edit?.publishedOn && { descriptor: edit.publishedOn, value: project.publishedOn },
|
|
68
|
+
edit?.externalUrl && { descriptor: edit.externalUrl, value: project.externalUrl },
|
|
69
|
+
edit?.image && { descriptor: edit.image, value: project.imageUrl }
|
|
70
|
+
].filter((row) => row !== undefined)
|
|
71
|
+
);
|
|
72
|
+
const frameSpec = $derived(
|
|
73
|
+
edit && panelRows.length > 0
|
|
74
|
+
? { label: edit.label ?? project.title, hasPanel: true }
|
|
75
|
+
: undefined
|
|
76
|
+
);
|
|
38
77
|
</script>
|
|
39
78
|
|
|
40
79
|
<article class={variant}>
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
{project.
|
|
80
|
+
<!-- Inside the article — see WeeklieCard. -->
|
|
81
|
+
<EditFrame spec={frameSpec}>
|
|
82
|
+
{#snippet panel()}
|
|
83
|
+
<EditPanel rows={panelRows} />
|
|
84
|
+
{/snippet}
|
|
85
|
+
<CardMedia src={project.imageUrl} alt="" ratio="16 / 9" width="1200" height="675" />
|
|
86
|
+
<div class="text">
|
|
87
|
+
<DateText value={project.publishedOn} />
|
|
88
|
+
<CardTitle>
|
|
89
|
+
{#if titleEditing}
|
|
90
|
+
<Editable edit={edit?.title} value={project.title}>
|
|
91
|
+
{#snippet children(text, attrs)}<span {...attrs}>{text}</span>{/snippet}
|
|
92
|
+
</Editable>
|
|
93
|
+
{:else if project.hasStory}
|
|
94
|
+
<Link href={`/what-we-do/${project.slug}`}>{project.title}</Link>
|
|
95
|
+
{:else if project.externalUrl}
|
|
96
|
+
<a href={project.externalUrl} rel="external noopener">{project.title}</a>
|
|
97
|
+
{:else}
|
|
98
|
+
{project.title}
|
|
99
|
+
{/if}
|
|
100
|
+
</CardTitle>
|
|
101
|
+
<Editable edit={edit?.excerpt} value={project.excerpt}>
|
|
102
|
+
{#snippet children(text, attrs)}<p {...attrs}>{text}</p>{/snippet}
|
|
103
|
+
</Editable>
|
|
104
|
+
{#if project.hasStory}
|
|
105
|
+
<ActionLabel
|
|
106
|
+
edit={config.messageEdit?.('common_readMore')}
|
|
107
|
+
value={config.messages.common_readMore()}
|
|
108
|
+
>
|
|
109
|
+
{#snippet control()}
|
|
110
|
+
<Link href={`/what-we-do/${project.slug}`} class="more">
|
|
111
|
+
{config.messages.common_readMore()}
|
|
112
|
+
</Link>
|
|
113
|
+
{/snippet}
|
|
114
|
+
</ActionLabel>
|
|
55
115
|
{/if}
|
|
56
|
-
</
|
|
57
|
-
|
|
58
|
-
{#snippet children(text, attrs)}<p {...attrs}>{text}</p>{/snippet}
|
|
59
|
-
</Editable>
|
|
60
|
-
{#if project.hasStory}
|
|
61
|
-
<ActionLabel
|
|
62
|
-
edit={config.messageEdit?.('common_readMore')}
|
|
63
|
-
value={config.messages.common_readMore()}
|
|
64
|
-
>
|
|
65
|
-
{#snippet control()}
|
|
66
|
-
<Link href={`/what-we-do/${project.slug}`} class="more">
|
|
67
|
-
{config.messages.common_readMore()}
|
|
68
|
-
</Link>
|
|
69
|
-
{/snippet}
|
|
70
|
-
</ActionLabel>
|
|
71
|
-
{/if}
|
|
72
|
-
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</EditFrame>
|
|
73
118
|
</article>
|
|
74
119
|
|
|
75
120
|
<style>
|
|
@@ -1,8 +1,17 @@
|
|
|
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 record-shaped scalars through the frame's panel.
|
|
5
|
+
*/
|
|
3
6
|
export interface ProjectEditMap {
|
|
4
7
|
title?: EditDescriptor;
|
|
5
8
|
excerpt?: EditDescriptor;
|
|
9
|
+
kind?: PropertyDescriptor;
|
|
10
|
+
publishedOn?: PropertyDescriptor;
|
|
11
|
+
externalUrl?: PropertyDescriptor;
|
|
12
|
+
image?: PropertyDescriptor;
|
|
13
|
+
/** Accessible name for the frame, e.g. the project's title. */
|
|
14
|
+
label?: string;
|
|
6
15
|
}
|
|
7
16
|
import type { ProjectCardData } from '../../content/types.js';
|
|
8
17
|
interface Props {
|
|
@@ -1,25 +1,99 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { EntityOp, PropertyDescriptor } from '../../edit/types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A collaborator is three plain (non-localized) columns, so everything
|
|
6
|
+
* edits through the frame's PANEL — the per-locale inline contract does
|
|
7
|
+
* not apply to any of it.
|
|
8
|
+
*/
|
|
9
|
+
export interface CollaboratorEditMap {
|
|
10
|
+
personName?: PropertyDescriptor;
|
|
11
|
+
affiliation?: PropertyDescriptor;
|
|
12
|
+
url?: PropertyDescriptor;
|
|
13
|
+
/** Accessible name for the frame — usually the person's name. */
|
|
14
|
+
label?: string;
|
|
15
|
+
/** Set by the list from its `collection` — removal of this row. */
|
|
16
|
+
removeOp?: Extract<EntityOp, { kind: 'remove' }>;
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
1
20
|
<script lang="ts">
|
|
2
21
|
import type { CollaboratorData } from '../../content/types.js';
|
|
22
|
+
import AddSlot from '../../edit/chrome/AddSlot.svelte';
|
|
23
|
+
import EditFrame from '../../edit/chrome/EditFrame.svelte';
|
|
24
|
+
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
25
|
+
import { getEditAdapter } from '../../edit/context.js';
|
|
26
|
+
import type { CollectionRef } from '../../edit/types.js';
|
|
3
27
|
|
|
4
28
|
interface Props {
|
|
5
29
|
collaborators: CollaboratorData[];
|
|
30
|
+
/** Edit descriptors per row; returning undefined leaves it read-only. */
|
|
31
|
+
editFor?: (collaborator: CollaboratorData) => CollaboratorEditMap | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Names the collection these rows belong to. Set alongside an adapter
|
|
34
|
+
* with `applyOp`, it turns on the add slot and each frame's remove —
|
|
35
|
+
* for rows that carry an `id` (removal needs an identity).
|
|
36
|
+
*/
|
|
37
|
+
collection?: CollectionRef;
|
|
6
38
|
}
|
|
7
39
|
|
|
8
|
-
let { collaborators }: Props = $props();
|
|
40
|
+
let { collaborators, editFor, collection }: Props = $props();
|
|
41
|
+
|
|
42
|
+
const adapter = getEditAdapter();
|
|
43
|
+
|
|
44
|
+
const structural = $derived(
|
|
45
|
+
collection !== undefined && (adapter?.isEditing ?? false) && adapter?.applyOp !== undefined
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/** The map the host gave, plus removal — the list owns identity. */
|
|
49
|
+
function editMapFor(collaborator: CollaboratorData): CollaboratorEditMap | undefined {
|
|
50
|
+
const map = editFor?.(collaborator);
|
|
51
|
+
if (!structural || !collection || collaborator.id === undefined) return map;
|
|
52
|
+
return { ...map, removeOp: { kind: 'remove', collection, id: collaborator.id } };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function rowsFor(collaborator: CollaboratorData, map: CollaboratorEditMap | undefined) {
|
|
56
|
+
return [
|
|
57
|
+
map?.personName && { descriptor: map.personName, value: collaborator.personName },
|
|
58
|
+
map?.affiliation && { descriptor: map.affiliation, value: collaborator.affiliation },
|
|
59
|
+
map?.url && { descriptor: map.url, value: collaborator.url }
|
|
60
|
+
].filter((row) => row !== undefined);
|
|
61
|
+
}
|
|
9
62
|
</script>
|
|
10
63
|
|
|
11
64
|
<ul>
|
|
12
|
-
{#each collaborators as collaborator (collaborator.personName + collaborator.affiliation)}
|
|
65
|
+
{#each collaborators as collaborator (collaborator.id ?? collaborator.personName + collaborator.affiliation)}
|
|
66
|
+
{@const map = editMapFor(collaborator)}
|
|
67
|
+
{@const rows = rowsFor(collaborator, map)}
|
|
13
68
|
<li>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
69
|
+
<!-- Inside the li, so the list's own layout never gains a child. -->
|
|
70
|
+
<EditFrame
|
|
71
|
+
spec={map && (rows.length > 0 || map.removeOp)
|
|
72
|
+
? {
|
|
73
|
+
label: map.label ?? collaborator.personName,
|
|
74
|
+
hasPanel: rows.length > 0,
|
|
75
|
+
removeOp: map.removeOp
|
|
76
|
+
}
|
|
77
|
+
: undefined}
|
|
78
|
+
>
|
|
79
|
+
{#snippet panel()}
|
|
80
|
+
<EditPanel {rows} />
|
|
81
|
+
{/snippet}
|
|
82
|
+
<strong>{collaborator.personName}</strong>
|
|
83
|
+
·
|
|
84
|
+
{#if collaborator.url}
|
|
85
|
+
<a href={collaborator.url} rel="external noopener">{collaborator.affiliation}</a>
|
|
86
|
+
{:else}
|
|
87
|
+
{collaborator.affiliation}
|
|
88
|
+
{/if}
|
|
89
|
+
</EditFrame>
|
|
21
90
|
</li>
|
|
22
91
|
{/each}
|
|
92
|
+
{#if structural && collection}
|
|
93
|
+
<li class="add-slot">
|
|
94
|
+
<AddSlot op={{ kind: 'create', collection }} />
|
|
95
|
+
</li>
|
|
96
|
+
{/if}
|
|
23
97
|
</ul>
|
|
24
98
|
|
|
25
99
|
<style>
|
|
@@ -34,4 +108,8 @@
|
|
|
34
108
|
a {
|
|
35
109
|
color: var(--color-brand);
|
|
36
110
|
}
|
|
111
|
+
|
|
112
|
+
li.add-slot {
|
|
113
|
+
list-style: none;
|
|
114
|
+
}
|
|
37
115
|
</style>
|
|
@@ -1,6 +1,32 @@
|
|
|
1
|
+
import type { EntityOp, PropertyDescriptor } from '../../edit/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A collaborator is three plain (non-localized) columns, so everything
|
|
4
|
+
* edits through the frame's PANEL — the per-locale inline contract does
|
|
5
|
+
* not apply to any of it.
|
|
6
|
+
*/
|
|
7
|
+
export interface CollaboratorEditMap {
|
|
8
|
+
personName?: PropertyDescriptor;
|
|
9
|
+
affiliation?: PropertyDescriptor;
|
|
10
|
+
url?: PropertyDescriptor;
|
|
11
|
+
/** Accessible name for the frame — usually the person's name. */
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Set by the list from its `collection` — removal of this row. */
|
|
14
|
+
removeOp?: Extract<EntityOp, {
|
|
15
|
+
kind: 'remove';
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
1
18
|
import type { CollaboratorData } from '../../content/types.js';
|
|
19
|
+
import type { CollectionRef } from '../../edit/types.js';
|
|
2
20
|
interface Props {
|
|
3
21
|
collaborators: CollaboratorData[];
|
|
22
|
+
/** Edit descriptors per row; returning undefined leaves it read-only. */
|
|
23
|
+
editFor?: (collaborator: CollaboratorData) => CollaboratorEditMap | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Names the collection these rows belong to. Set alongside an adapter
|
|
26
|
+
* with `applyOp`, it turns on the add slot and each frame's remove —
|
|
27
|
+
* for rows that carry an `id` (removal needs an identity).
|
|
28
|
+
*/
|
|
29
|
+
collection?: CollectionRef;
|
|
4
30
|
}
|
|
5
31
|
declare const CollaboratorList: import("svelte").Component<Props, {}, "">;
|
|
6
32
|
type CollaboratorList = ReturnType<typeof CollaboratorList>;
|
|
@@ -1,20 +1,26 @@
|
|
|
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
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
5
|
+
* Which of the card's fields are editable at this render site. `name` and
|
|
6
|
+
* `photo` are plain (non-localized) columns, so they edit through the
|
|
7
|
+
* frame's PANEL — the per-locale inline contract does not apply to them.
|
|
8
8
|
*/
|
|
9
9
|
export interface TeamMemberEditMap {
|
|
10
10
|
role?: EditDescriptor;
|
|
11
11
|
bio?: EditDescriptor;
|
|
12
|
+
name?: PropertyDescriptor;
|
|
13
|
+
photo?: PropertyDescriptor;
|
|
14
|
+
/** Accessible name for the frame — usually the member's name. */
|
|
15
|
+
label?: string;
|
|
12
16
|
}
|
|
13
17
|
</script>
|
|
14
18
|
|
|
15
19
|
<script lang="ts">
|
|
16
20
|
import type { TeamMemberData } from '../../content/types.js';
|
|
17
21
|
import Editable from '../../edit/Editable.svelte';
|
|
22
|
+
import EditFrame from '../../edit/chrome/EditFrame.svelte';
|
|
23
|
+
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
18
24
|
import CardMedia from '../ui/CardMedia.svelte';
|
|
19
25
|
|
|
20
26
|
interface Props {
|
|
@@ -26,25 +32,41 @@
|
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
let { member, variant = 'board', edit }: Props = $props();
|
|
35
|
+
|
|
36
|
+
const panelRows = $derived(
|
|
37
|
+
[
|
|
38
|
+
edit?.name && { descriptor: edit.name, value: member.name },
|
|
39
|
+
edit?.photo && { descriptor: edit.photo, value: member.photoUrl }
|
|
40
|
+
].filter((row) => row !== undefined)
|
|
41
|
+
);
|
|
42
|
+
const frameSpec = $derived(
|
|
43
|
+
edit && panelRows.length > 0 ? { label: edit.label ?? member.name, hasPanel: true } : undefined
|
|
44
|
+
);
|
|
29
45
|
</script>
|
|
30
46
|
|
|
31
47
|
<article class={variant}>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
48
|
+
<!-- Inside the article — see WeeklieCard. -->
|
|
49
|
+
<EditFrame spec={frameSpec}>
|
|
50
|
+
{#snippet panel()}
|
|
51
|
+
<EditPanel rows={panelRows} />
|
|
52
|
+
{/snippet}
|
|
53
|
+
<CardMedia
|
|
54
|
+
src={member.photoUrl}
|
|
55
|
+
alt={member.name}
|
|
56
|
+
ratio={variant === 'featured' ? '4 / 5' : '1 / 1'}
|
|
57
|
+
width="400"
|
|
58
|
+
height="500"
|
|
59
|
+
/>
|
|
60
|
+
<h3>{member.name}</h3>
|
|
61
|
+
<Editable edit={edit?.role} value={member.role}>
|
|
62
|
+
{#snippet children(text, attrs)}<p class="role" {...attrs}>{text}</p>{/snippet}
|
|
46
63
|
</Editable>
|
|
47
|
-
|
|
64
|
+
{#if member.bio}
|
|
65
|
+
<Editable edit={edit?.bio} value={member.bio}>
|
|
66
|
+
{#snippet children(text, attrs)}<p class="bio" {...attrs}>{text}</p>{/snippet}
|
|
67
|
+
</Editable>
|
|
68
|
+
{/if}
|
|
69
|
+
</EditFrame>
|
|
48
70
|
</article>
|
|
49
71
|
|
|
50
72
|
<style>
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import type { EditDescriptor } from '../../edit/types.js';
|
|
1
|
+
import type { EditDescriptor, PropertyDescriptor } from '../../edit/types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Which of the card's
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
3
|
+
* Which of the card's fields are editable at this render site. `name` and
|
|
4
|
+
* `photo` are plain (non-localized) columns, so they edit through the
|
|
5
|
+
* frame's PANEL — the per-locale inline contract does not apply to them.
|
|
6
6
|
*/
|
|
7
7
|
export interface TeamMemberEditMap {
|
|
8
8
|
role?: EditDescriptor;
|
|
9
9
|
bio?: EditDescriptor;
|
|
10
|
+
name?: PropertyDescriptor;
|
|
11
|
+
photo?: PropertyDescriptor;
|
|
12
|
+
/** Accessible name for the frame — usually the member's name. */
|
|
13
|
+
label?: string;
|
|
10
14
|
}
|
|
11
15
|
import type { TeamMemberData } from '../../content/types.js';
|
|
12
16
|
interface Props {
|
|
@@ -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/content/types.d.ts
CHANGED
|
@@ -59,11 +59,15 @@ export interface TeamMemberData {
|
|
|
59
59
|
isBoard: boolean;
|
|
60
60
|
}
|
|
61
61
|
export interface CollaboratorData {
|
|
62
|
+
/** Present only where the list is editable — remove ops need an identity. */
|
|
63
|
+
id?: string | number;
|
|
62
64
|
personName: string;
|
|
63
65
|
affiliation: string;
|
|
64
66
|
url: string | null;
|
|
65
67
|
}
|
|
66
68
|
export interface JobOpeningData {
|
|
69
|
+
/** Present only where the list is editable — remove ops need an identity. */
|
|
70
|
+
id?: string | number;
|
|
67
71
|
slug: string;
|
|
68
72
|
title: string;
|
|
69
73
|
description: string | null;
|
package/dist/edit/types.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare function localize(value: LocalizedText, locale: Locale): string;
|
|
|
15
15
|
* The entities whose fields the components can mark editable. Names match
|
|
16
16
|
* the content tables so an adapter can route a save without a mapping layer.
|
|
17
17
|
*/
|
|
18
|
-
export type EditableEntity = 'weeklies' | 'projects' | 'milestones' | 'team_members' | 'job_openings' | 'themes';
|
|
18
|
+
export type EditableEntity = 'weeklies' | 'projects' | 'milestones' | 'team_members' | 'job_openings' | 'themes' | 'collaborators';
|
|
19
19
|
/**
|
|
20
20
|
* The identity of one editable string in the content model: a page copy
|
|
21
21
|
* block (`page_content` row), one localized field of a domain entity, or one
|