@vit-foundation/ui 0.12.0 → 0.13.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/chrome.d.ts +1 -0
- package/dist/components/account/NewsletterSignup.svelte +19 -3
- package/dist/components/contact/ContactForm.svelte +25 -7
- package/dist/components/jobs/JobList.svelte +2 -0
- package/dist/components/layout/Footer.svelte +54 -5
- package/dist/components/layout/Footer.svelte.d.ts +6 -1
- package/dist/components/layout/Nav.svelte +64 -8
- package/dist/components/layout/Nav.svelte.d.ts +6 -1
- package/dist/components/layout/site-link-edit.d.ts +17 -0
- package/dist/components/layout/site-link-edit.js +1 -0
- package/dist/components/projects/ProjectCard.svelte +2 -0
- package/dist/components/timeline/TimelineMilestone.svelte +3 -1
- package/dist/components/ui/CardMedia.svelte +66 -3
- package/dist/components/ui/CardMedia.svelte.d.ts +6 -2
- package/dist/components/weeklies/CommentSection.svelte +20 -3
- package/dist/components/weeklies/WeeklieCard.svelte +2 -0
- package/dist/config/edit-messages.d.ts +2 -0
- package/dist/config/edit-messages.js +1 -0
- package/dist/config/types.d.ts +4 -0
- package/dist/content/types.d.ts +8 -0
- package/dist/edit/chrome/DraftBadge.svelte +30 -0
- package/dist/edit/chrome/DraftBadge.svelte.d.ts +18 -0
- package/dist/edit/types.d.ts +1 -1
- package/package.json +1 -1
package/dist/chrome.d.ts
CHANGED
|
@@ -4,4 +4,5 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export { default as Footer } from './components/layout/Footer.svelte';
|
|
6
6
|
export { default as Nav } from './components/layout/Nav.svelte';
|
|
7
|
+
export type { SiteLinkEditMap } from './components/layout/site-link-edit.js';
|
|
7
8
|
export { default as PageShell } from './components/layout/PageShell.svelte';
|
|
@@ -44,9 +44,23 @@
|
|
|
44
44
|
>
|
|
45
45
|
{#snippet children(text, attrs)}<span {...attrs}>{text}</span>{/snippet}
|
|
46
46
|
</Editable>
|
|
47
|
-
<
|
|
47
|
+
<ActionLabel
|
|
48
|
+
edit={config.messageEdit?.('comments_signupLink')}
|
|
49
|
+
value={msg.comments_signupLink()}
|
|
50
|
+
>
|
|
51
|
+
{#snippet control()}<Link href={withNewsletterIntent('/signup')}
|
|
52
|
+
>{msg.comments_signupLink()}</Link
|
|
53
|
+
>{/snippet}
|
|
54
|
+
</ActionLabel>
|
|
48
55
|
·
|
|
49
|
-
<
|
|
56
|
+
<ActionLabel
|
|
57
|
+
edit={config.messageEdit?.('comments_loginLink')}
|
|
58
|
+
value={msg.comments_loginLink()}
|
|
59
|
+
>
|
|
60
|
+
{#snippet control()}<Link href={withNewsletterIntent('/login')}
|
|
61
|
+
>{msg.comments_loginLink()}</Link
|
|
62
|
+
>{/snippet}
|
|
63
|
+
</ActionLabel>
|
|
50
64
|
</p>
|
|
51
65
|
{:else if account.newsletterSubscribed}
|
|
52
66
|
<p class="prompt">
|
|
@@ -56,7 +70,9 @@
|
|
|
56
70
|
>
|
|
57
71
|
{#snippet children(text, attrs)}<span {...attrs}>{text}</span>{/snippet}
|
|
58
72
|
</Editable>
|
|
59
|
-
<
|
|
73
|
+
<ActionLabel edit={config.messageEdit?.('account_navLabel')} value={msg.account_navLabel()}>
|
|
74
|
+
{#snippet control()}<Link href="/account">{msg.account_navLabel()}</Link>{/snippet}
|
|
75
|
+
</ActionLabel>
|
|
60
76
|
</p>
|
|
61
77
|
{:else}
|
|
62
78
|
<form {...f}>
|
|
@@ -62,13 +62,31 @@
|
|
|
62
62
|
*/
|
|
63
63
|
const categoryRows = $derived(
|
|
64
64
|
config.messageEdit
|
|
65
|
-
?
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
? [
|
|
66
|
+
...categories.map((category) => ({
|
|
67
|
+
descriptor: chromeProperty(`contact_category_${category}`, {
|
|
68
|
+
type: 'text' as const,
|
|
69
|
+
label: contactCategoryLabel(category, msg)
|
|
70
|
+
}),
|
|
71
|
+
value: contactCategoryLabel(category, msg)
|
|
72
|
+
})),
|
|
73
|
+
// Post-submit feedback never shows while editing (the mirror's
|
|
74
|
+
// forms are inert), so these two edit here or nowhere in place.
|
|
75
|
+
{
|
|
76
|
+
descriptor: chromeProperty('contact_success', {
|
|
77
|
+
type: 'text' as const,
|
|
78
|
+
label: msg.contact_success()
|
|
79
|
+
}),
|
|
80
|
+
value: msg.contact_success()
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
descriptor: chromeProperty('contact_invalid', {
|
|
84
|
+
type: 'text' as const,
|
|
85
|
+
label: msg.contact_invalid()
|
|
86
|
+
}),
|
|
87
|
+
value: msg.contact_invalid()
|
|
88
|
+
}
|
|
89
|
+
]
|
|
72
90
|
: []
|
|
73
91
|
);
|
|
74
92
|
const categoryFrameSpec = $derived(
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { getUiConfig } from '../../config/context.js';
|
|
21
21
|
import type { JobOpeningData } from '../../content/types.js';
|
|
22
22
|
import AddSlot from '../../edit/chrome/AddSlot.svelte';
|
|
23
|
+
import DraftBadge from '../../edit/chrome/DraftBadge.svelte';
|
|
23
24
|
import EditFrame from '../../edit/chrome/EditFrame.svelte';
|
|
24
25
|
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
25
26
|
import { getEditAdapter } from '../../edit/context.js';
|
|
@@ -94,6 +95,7 @@
|
|
|
94
95
|
<Editable edit={edit?.title} value={job.title}>
|
|
95
96
|
{#snippet children(text, attrs)}<h3 {...attrs}>{text}</h3>{/snippet}
|
|
96
97
|
</Editable>
|
|
98
|
+
{#if job.draft}<DraftBadge />{/if}
|
|
97
99
|
<DateText value={job.postedOn} />
|
|
98
100
|
{#if job.description}
|
|
99
101
|
<Editable edit={edit?.description} value={job.description}>
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
import { getUiConfig } from '../../config/context.js';
|
|
3
3
|
import type { SiteLink } from '../../config/types.js';
|
|
4
4
|
import ActionLabel from '../../edit/ActionLabel.svelte';
|
|
5
|
+
import AddSlot from '../../edit/chrome/AddSlot.svelte';
|
|
6
|
+
import EditFrame from '../../edit/chrome/EditFrame.svelte';
|
|
7
|
+
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
8
|
+
import { getEditAdapter } from '../../edit/context.js';
|
|
5
9
|
import Editable from '../../edit/Editable.svelte';
|
|
6
|
-
import type { EditDescriptor } from '../../edit/types.js';
|
|
10
|
+
import type { CollectionRef, EditDescriptor } from '../../edit/types.js';
|
|
7
11
|
import Link from '../ui/Link.svelte';
|
|
12
|
+
import type { SiteLinkEditMap } from './site-link-edit.js';
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
15
|
* The links are a prop rather than a package constant: the host app owns
|
|
@@ -15,11 +20,34 @@
|
|
|
15
20
|
links: SiteLink[];
|
|
16
21
|
/** Edit descriptors for the link labels — see Nav. */
|
|
17
22
|
editFor?: (link: SiteLink) => EditDescriptor | undefined;
|
|
23
|
+
/** Panel rows (href, order) per link — see Nav. */
|
|
24
|
+
propertiesFor?: (link: SiteLink) => SiteLinkEditMap | undefined;
|
|
25
|
+
/** Names the links' collection; with `applyOp`, turns on add/remove. */
|
|
26
|
+
collection?: CollectionRef;
|
|
18
27
|
}
|
|
19
28
|
|
|
20
|
-
let { links, editFor = undefined }: Props = $props();
|
|
29
|
+
let { links, editFor = undefined, propertiesFor = undefined, collection }: Props = $props();
|
|
21
30
|
|
|
22
31
|
const config = getUiConfig();
|
|
32
|
+
const adapter = getEditAdapter();
|
|
33
|
+
|
|
34
|
+
const structural = $derived(
|
|
35
|
+
collection !== undefined && (adapter?.isEditing ?? false) && adapter?.applyOp !== undefined
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
/** The map the host gave, plus removal — the list owns identity. */
|
|
39
|
+
function editMapFor(link: SiteLink): SiteLinkEditMap | undefined {
|
|
40
|
+
const map = propertiesFor?.(link);
|
|
41
|
+
if (!structural || !collection || link.id === undefined) return map;
|
|
42
|
+
return { ...map, removeOp: { kind: 'remove', collection, id: link.id } };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rowsFor(link: SiteLink, map: SiteLinkEditMap | undefined) {
|
|
46
|
+
return [
|
|
47
|
+
map?.href && { descriptor: map.href, value: link.href },
|
|
48
|
+
map?.order && { descriptor: map.order, value: String(link.order ?? 0) }
|
|
49
|
+
].filter((row) => row !== undefined);
|
|
50
|
+
}
|
|
23
51
|
|
|
24
52
|
const year = new Date().getFullYear();
|
|
25
53
|
</script>
|
|
@@ -31,12 +59,33 @@
|
|
|
31
59
|
</span>
|
|
32
60
|
<ul>
|
|
33
61
|
{#each links as link (link.href)}
|
|
62
|
+
{@const map = editMapFor(link)}
|
|
63
|
+
{@const rows = rowsFor(link, map)}
|
|
34
64
|
<li>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
65
|
+
<!-- Inside the li, so the list's flex layout never gains a child. -->
|
|
66
|
+
<EditFrame
|
|
67
|
+
spec={map && (rows.length > 0 || map.removeOp)
|
|
68
|
+
? {
|
|
69
|
+
label: map.label ?? link.label,
|
|
70
|
+
hasPanel: rows.length > 0,
|
|
71
|
+
removeOp: map.removeOp
|
|
72
|
+
}
|
|
73
|
+
: undefined}
|
|
74
|
+
>
|
|
75
|
+
{#snippet panel()}
|
|
76
|
+
<EditPanel {rows} />
|
|
77
|
+
{/snippet}
|
|
78
|
+
<ActionLabel edit={editFor?.(link)} value={link.label}>
|
|
79
|
+
{#snippet control()}<Link href={link.href}>{link.label}</Link>{/snippet}
|
|
80
|
+
</ActionLabel>
|
|
81
|
+
</EditFrame>
|
|
38
82
|
</li>
|
|
39
83
|
{/each}
|
|
84
|
+
{#if structural && collection}
|
|
85
|
+
<li>
|
|
86
|
+
<AddSlot op={{ kind: 'create', collection }} />
|
|
87
|
+
</li>
|
|
88
|
+
{/if}
|
|
40
89
|
</ul>
|
|
41
90
|
<p class="rights">
|
|
42
91
|
© {year}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SiteLink } from '../../config/types.js';
|
|
2
|
-
import type { EditDescriptor } from '../../edit/types.js';
|
|
2
|
+
import type { CollectionRef, EditDescriptor } from '../../edit/types.js';
|
|
3
|
+
import type { SiteLinkEditMap } from './site-link-edit.js';
|
|
3
4
|
/**
|
|
4
5
|
* The links are a prop rather than a package constant: the host app owns
|
|
5
6
|
* its routes, and Nav and Footer render the same list — the foundation
|
|
@@ -9,6 +10,10 @@ interface Props {
|
|
|
9
10
|
links: SiteLink[];
|
|
10
11
|
/** Edit descriptors for the link labels — see Nav. */
|
|
11
12
|
editFor?: (link: SiteLink) => EditDescriptor | undefined;
|
|
13
|
+
/** Panel rows (href, order) per link — see Nav. */
|
|
14
|
+
propertiesFor?: (link: SiteLink) => SiteLinkEditMap | undefined;
|
|
15
|
+
/** Names the links' collection; with `applyOp`, turns on add/remove. */
|
|
16
|
+
collection?: CollectionRef;
|
|
12
17
|
}
|
|
13
18
|
declare const Footer: import("svelte").Component<Props, {}, "">;
|
|
14
19
|
type Footer = ReturnType<typeof Footer>;
|
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
import { getUiConfig } from '../../config/context.js';
|
|
3
3
|
import type { SiteLink } from '../../config/types.js';
|
|
4
4
|
import ActionLabel from '../../edit/ActionLabel.svelte';
|
|
5
|
-
import
|
|
5
|
+
import AddSlot from '../../edit/chrome/AddSlot.svelte';
|
|
6
|
+
import EditFrame from '../../edit/chrome/EditFrame.svelte';
|
|
7
|
+
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
8
|
+
import { getEditAdapter } from '../../edit/context.js';
|
|
9
|
+
import type { CollectionRef, EditDescriptor } from '../../edit/types.js';
|
|
6
10
|
import { isPathUnder } from '../../utils/paths.js';
|
|
7
11
|
import GhostButton from '../ui/GhostButton.svelte';
|
|
8
12
|
import Link from '../ui/Link.svelte';
|
|
13
|
+
import type { SiteLinkEditMap } from './site-link-edit.js';
|
|
9
14
|
|
|
10
15
|
interface Props {
|
|
11
16
|
/**
|
|
@@ -25,11 +30,41 @@
|
|
|
25
30
|
* text instead of an anchor (see ActionLabel).
|
|
26
31
|
*/
|
|
27
32
|
editFor?: (link: SiteLink) => EditDescriptor | undefined;
|
|
33
|
+
/** Panel rows (href, order) per link — the label edits via `editFor`. */
|
|
34
|
+
propertiesFor?: (link: SiteLink) => SiteLinkEditMap | undefined;
|
|
35
|
+
/** Names the links' collection; with `applyOp`, turns on add/remove. */
|
|
36
|
+
collection?: CollectionRef;
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
let {
|
|
39
|
+
let {
|
|
40
|
+
links,
|
|
41
|
+
account = null,
|
|
42
|
+
url = undefined,
|
|
43
|
+
editFor = undefined,
|
|
44
|
+
propertiesFor = undefined,
|
|
45
|
+
collection
|
|
46
|
+
}: Props = $props();
|
|
31
47
|
|
|
32
48
|
const config = getUiConfig();
|
|
49
|
+
const adapter = getEditAdapter();
|
|
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(link: SiteLink): SiteLinkEditMap | undefined {
|
|
57
|
+
const map = propertiesFor?.(link);
|
|
58
|
+
if (!structural || !collection || link.id === undefined) return map;
|
|
59
|
+
return { ...map, removeOp: { kind: 'remove', collection, id: link.id } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function rowsFor(link: SiteLink, map: SiteLinkEditMap | undefined) {
|
|
63
|
+
return [
|
|
64
|
+
map?.href && { descriptor: map.href, value: link.href },
|
|
65
|
+
map?.order && { descriptor: map.order, value: String(link.order ?? 0) }
|
|
66
|
+
].filter((row) => row !== undefined);
|
|
67
|
+
}
|
|
33
68
|
const msg = $derived(config.messages);
|
|
34
69
|
|
|
35
70
|
let isMenuOpen = $state(false);
|
|
@@ -85,16 +120,37 @@
|
|
|
85
120
|
<div class="menu" id="site-menu" class:open={isMenuOpen}>
|
|
86
121
|
<ul class="links">
|
|
87
122
|
{#each links as link (link.href)}
|
|
123
|
+
{@const map = editMapFor(link)}
|
|
124
|
+
{@const rows = rowsFor(link, map)}
|
|
88
125
|
<li>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
126
|
+
<!-- Inside the li, so the row's layout never gains a child. -->
|
|
127
|
+
<EditFrame
|
|
128
|
+
spec={map && (rows.length > 0 || map.removeOp)
|
|
129
|
+
? {
|
|
130
|
+
label: map.label ?? link.label,
|
|
131
|
+
hasPanel: rows.length > 0,
|
|
132
|
+
removeOp: map.removeOp
|
|
133
|
+
}
|
|
134
|
+
: undefined}
|
|
135
|
+
>
|
|
136
|
+
{#snippet panel()}
|
|
137
|
+
<EditPanel {rows} />
|
|
94
138
|
{/snippet}
|
|
95
|
-
|
|
139
|
+
<ActionLabel edit={editFor?.(link)} value={link.label}>
|
|
140
|
+
{#snippet control()}
|
|
141
|
+
<Link href={link.href} aria-current={isCurrent(link.href) ? 'page' : undefined}>
|
|
142
|
+
{link.label}
|
|
143
|
+
</Link>
|
|
144
|
+
{/snippet}
|
|
145
|
+
</ActionLabel>
|
|
146
|
+
</EditFrame>
|
|
96
147
|
</li>
|
|
97
148
|
{/each}
|
|
149
|
+
{#if structural && collection}
|
|
150
|
+
<li>
|
|
151
|
+
<AddSlot op={{ kind: 'create', collection }} />
|
|
152
|
+
</li>
|
|
153
|
+
{/if}
|
|
98
154
|
</ul>
|
|
99
155
|
|
|
100
156
|
{#if account}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SiteLink } from '../../config/types.js';
|
|
2
|
-
import type { EditDescriptor } from '../../edit/types.js';
|
|
2
|
+
import type { CollectionRef, EditDescriptor } from '../../edit/types.js';
|
|
3
|
+
import type { SiteLinkEditMap } from './site-link-edit.js';
|
|
3
4
|
interface Props {
|
|
4
5
|
/**
|
|
5
6
|
* Primary navigation entries. A prop rather than a package constant:
|
|
@@ -20,6 +21,10 @@ interface Props {
|
|
|
20
21
|
* text instead of an anchor (see ActionLabel).
|
|
21
22
|
*/
|
|
22
23
|
editFor?: (link: SiteLink) => EditDescriptor | undefined;
|
|
24
|
+
/** Panel rows (href, order) per link — the label edits via `editFor`. */
|
|
25
|
+
propertiesFor?: (link: SiteLink) => SiteLinkEditMap | undefined;
|
|
26
|
+
/** Names the links' collection; with `applyOp`, turns on add/remove. */
|
|
27
|
+
collection?: CollectionRef;
|
|
23
28
|
}
|
|
24
29
|
declare const Nav: import("svelte").Component<Props, {}, "">;
|
|
25
30
|
type Nav = ReturnType<typeof Nav>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EntityOp, PropertyDescriptor } from '../../edit/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* What the CMS may edit on one nav/footer link beyond its LABEL (which edits
|
|
4
|
+
* through the existing `editFor` ActionLabel swap): the href and the display
|
|
5
|
+
* order are panel rows, and removal arrives injected by the list from its
|
|
6
|
+
* `collection` — the Timeline/CollaboratorList pattern.
|
|
7
|
+
*/
|
|
8
|
+
export interface SiteLinkEditMap {
|
|
9
|
+
href?: PropertyDescriptor;
|
|
10
|
+
order?: PropertyDescriptor;
|
|
11
|
+
/** Accessible name for the frame, defaulting to the link's label. */
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Set by the list from its `collection` — removal of this row. */
|
|
14
|
+
removeOp?: Extract<EntityOp, {
|
|
15
|
+
kind: 'remove';
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import EditFrame from '../../edit/chrome/EditFrame.svelte';
|
|
27
27
|
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
28
28
|
import CardMedia from '../ui/CardMedia.svelte';
|
|
29
|
+
import DraftBadge from '../../edit/chrome/DraftBadge.svelte';
|
|
29
30
|
import CardTitle from '../ui/CardTitle.svelte';
|
|
30
31
|
import DateText from '../ui/DateText.svelte';
|
|
31
32
|
import Link from '../ui/Link.svelte';
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
{/snippet}
|
|
85
86
|
<CardMedia src={project.imageUrl} alt="" ratio="16 / 9" width="1200" height="675" />
|
|
86
87
|
<div class="text">
|
|
88
|
+
{#if project.draft}<DraftBadge />{/if}
|
|
87
89
|
<DateText value={project.publishedOn} />
|
|
88
90
|
<CardTitle>
|
|
89
91
|
{#if titleEditing}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import Editable from '../../edit/Editable.svelte';
|
|
30
30
|
import { MILESTONE_CATEGORY_COLOR, milestoneCategoryLabel } from '../../utils/milestones.js';
|
|
31
31
|
import CardMedia from '../ui/CardMedia.svelte';
|
|
32
|
+
import DraftBadge from '../../edit/chrome/DraftBadge.svelte';
|
|
32
33
|
import DateText from '../ui/DateText.svelte';
|
|
33
34
|
import Link from '../ui/Link.svelte';
|
|
34
35
|
|
|
@@ -110,6 +111,7 @@
|
|
|
110
111
|
{#snippet children(text, attrs)}<p class="category" {...attrs}>{text}</p>{/snippet}
|
|
111
112
|
</Editable>
|
|
112
113
|
<DateText value={milestone.occurredOn} />
|
|
114
|
+
{#if milestone.draft}<DraftBadge />{/if}
|
|
113
115
|
<Editable edit={edit?.title} value={milestone.title}>
|
|
114
116
|
{#snippet children(text, attrs)}<h3 {...attrs}>{text}</h3>{/snippet}
|
|
115
117
|
</Editable>
|
|
@@ -119,7 +121,7 @@
|
|
|
119
121
|
</Editable>
|
|
120
122
|
{/if}
|
|
121
123
|
{#if milestone.imageUrls.length > 0}
|
|
122
|
-
<CardMedia src={milestone.imageUrls[0]} alt="" width="1200" height="675" />
|
|
124
|
+
<CardMedia src={milestone.imageUrls[0] ?? null} alt="" width="1200" height="675" />
|
|
123
125
|
{/if}
|
|
124
126
|
{#if milestone.linkUrl}
|
|
125
127
|
<p class="more">
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
3
3
|
|
|
4
|
-
interface Props extends HTMLImgAttributes {
|
|
5
|
-
|
|
4
|
+
interface Props extends Omit<HTMLImgAttributes, 'src'> {
|
|
5
|
+
/**
|
|
6
|
+
* Null/undefined renders the placeholder directly — a row may simply
|
|
7
|
+
* have no image yet (the CMS creates rows before anyone uploads one).
|
|
8
|
+
*/
|
|
9
|
+
src: string | null | undefined;
|
|
6
10
|
/** Empty string for decorative images (the surrounding card carries the text). */
|
|
7
11
|
alt: string;
|
|
8
12
|
/** CSS aspect-ratio (e.g. '16 / 9'); omit to keep the image's natural height. */
|
|
@@ -10,9 +14,54 @@
|
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
let { src, alt, ratio, ...rest }: Props = $props();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The src that 404'd, so a broken path degrades to the placeholder instead
|
|
20
|
+
* of the browser's broken-image box. Keyed by value, not a boolean: a NEW
|
|
21
|
+
* src (an editor fixing the path) gets a fresh attempt.
|
|
22
|
+
*/
|
|
23
|
+
let failedSrc = $state<string | null>(null);
|
|
24
|
+
const showImage = $derived(Boolean(src) && src !== failedSrc);
|
|
25
|
+
|
|
26
|
+
// The placeholder must hold the image's footprint: the ratio prop when
|
|
27
|
+
// given, else the intrinsic width/height the caller passed, else square.
|
|
28
|
+
const placeholderRatio = $derived(
|
|
29
|
+
ratio ?? (rest.width && rest.height ? `${rest.width} / ${rest.height}` : '1 / 1')
|
|
30
|
+
);
|
|
13
31
|
</script>
|
|
14
32
|
|
|
15
|
-
|
|
33
|
+
{#if showImage}
|
|
34
|
+
<img
|
|
35
|
+
{src}
|
|
36
|
+
{alt}
|
|
37
|
+
loading="lazy"
|
|
38
|
+
style:aspect-ratio={ratio}
|
|
39
|
+
onerror={() => (failedSrc = src ?? null)}
|
|
40
|
+
{...rest}
|
|
41
|
+
/>
|
|
42
|
+
{:else}
|
|
43
|
+
<!-- Same accessibility contract as the img: named when alt has text,
|
|
44
|
+
invisible to readers when the image was decorative. -->
|
|
45
|
+
<div
|
|
46
|
+
class="placeholder"
|
|
47
|
+
style:aspect-ratio={placeholderRatio}
|
|
48
|
+
role={alt ? 'img' : undefined}
|
|
49
|
+
aria-label={alt || undefined}
|
|
50
|
+
aria-hidden={alt ? undefined : true}
|
|
51
|
+
>
|
|
52
|
+
<svg
|
|
53
|
+
viewBox="0 0 24 24"
|
|
54
|
+
fill="none"
|
|
55
|
+
stroke="currentColor"
|
|
56
|
+
stroke-width="1.5"
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
>
|
|
59
|
+
<rect x="3" y="4" width="18" height="16" rx="2" />
|
|
60
|
+
<circle cx="9" cy="10" r="1.8" />
|
|
61
|
+
<path d="M3.5 17.5 9 13l4 3.2 4-4.2 3.5 4" />
|
|
62
|
+
</svg>
|
|
63
|
+
</div>
|
|
64
|
+
{/if}
|
|
16
65
|
|
|
17
66
|
<style>
|
|
18
67
|
img {
|
|
@@ -20,4 +69,18 @@
|
|
|
20
69
|
background: var(--color-band-grey);
|
|
21
70
|
object-fit: cover;
|
|
22
71
|
}
|
|
72
|
+
|
|
73
|
+
.placeholder {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
width: 100%;
|
|
78
|
+
border-radius: var(--radius);
|
|
79
|
+
background: var(--color-band-grey);
|
|
80
|
+
color: var(--color-ink-muted);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.placeholder svg {
|
|
84
|
+
width: clamp(1.5rem, 18%, 3rem);
|
|
85
|
+
}
|
|
23
86
|
</style>
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
2
|
-
interface Props extends HTMLImgAttributes {
|
|
3
|
-
|
|
2
|
+
interface Props extends Omit<HTMLImgAttributes, 'src'> {
|
|
3
|
+
/**
|
|
4
|
+
* Null/undefined renders the placeholder directly — a row may simply
|
|
5
|
+
* have no image yet (the CMS creates rows before anyone uploads one).
|
|
6
|
+
*/
|
|
7
|
+
src: string | null | undefined;
|
|
4
8
|
/** Empty string for decorative images (the surrounding card carries the text). */
|
|
5
9
|
alt: string;
|
|
6
10
|
/** CSS aspect-ratio (e.g. '16 / 9'); omit to keep the image's natural height. */
|
|
@@ -97,7 +97,14 @@
|
|
|
97
97
|
{@const reply = replyFormFor(String(thread.id))}
|
|
98
98
|
<!-- Native disclosure: the reply form opens without JS. -->
|
|
99
99
|
<details class="reply">
|
|
100
|
-
<summary>
|
|
100
|
+
<summary>
|
|
101
|
+
<ActionLabel
|
|
102
|
+
edit={config.messageEdit?.('comments_replyLabel')}
|
|
103
|
+
value={msg.comments_replyLabel()}
|
|
104
|
+
>
|
|
105
|
+
{#snippet control()}{msg.comments_replyLabel()}{/snippet}
|
|
106
|
+
</ActionLabel>
|
|
107
|
+
</summary>
|
|
101
108
|
<form class="form-stack" {...reply}>
|
|
102
109
|
<Field
|
|
103
110
|
id="reply-body-{thread.id}"
|
|
@@ -173,9 +180,19 @@
|
|
|
173
180
|
>
|
|
174
181
|
{#snippet children(text, attrs)}<span {...attrs}>{text}</span>{/snippet}
|
|
175
182
|
</Editable>
|
|
176
|
-
<
|
|
183
|
+
<ActionLabel
|
|
184
|
+
edit={config.messageEdit?.('comments_loginLink')}
|
|
185
|
+
value={msg.comments_loginLink()}
|
|
186
|
+
>
|
|
187
|
+
{#snippet control()}<Link href="/login">{msg.comments_loginLink()}</Link>{/snippet}
|
|
188
|
+
</ActionLabel>
|
|
177
189
|
·
|
|
178
|
-
<
|
|
190
|
+
<ActionLabel
|
|
191
|
+
edit={config.messageEdit?.('comments_signupLink')}
|
|
192
|
+
value={msg.comments_signupLink()}
|
|
193
|
+
>
|
|
194
|
+
{#snippet control()}<Link href="/signup">{msg.comments_signupLink()}</Link>{/snippet}
|
|
195
|
+
</ActionLabel>
|
|
179
196
|
</p>
|
|
180
197
|
{/if}
|
|
181
198
|
</section>
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import EditFrame from '../../edit/chrome/EditFrame.svelte';
|
|
24
24
|
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
25
25
|
import CardMedia from '../ui/CardMedia.svelte';
|
|
26
|
+
import DraftBadge from '../../edit/chrome/DraftBadge.svelte';
|
|
26
27
|
import CardTitle from '../ui/CardTitle.svelte';
|
|
27
28
|
import DateText from '../ui/DateText.svelte';
|
|
28
29
|
import Link from '../ui/Link.svelte';
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
{/snippet}
|
|
63
64
|
<header>
|
|
64
65
|
<span class="number">{config.messages.weeklie_number({ number: weekly.number })}</span>
|
|
66
|
+
{#if weekly.draft}<DraftBadge />{/if}
|
|
65
67
|
<DateText value={weekly.publishedOn} />
|
|
66
68
|
</header>
|
|
67
69
|
<CardMedia src={weekly.imageUrl} alt="" ratio="1 / 1" width="600" height="600" />
|
|
@@ -36,6 +36,8 @@ export interface EditMessages {
|
|
|
36
36
|
edit_addFailed(): string;
|
|
37
37
|
/** Property rows. */
|
|
38
38
|
edit_uploadImage(): string;
|
|
39
|
+
/** The chip a card wears while its row is unpublished. */
|
|
40
|
+
edit_draftBadge(): string;
|
|
39
41
|
edit_clearValue(): string;
|
|
40
42
|
edit_emptyRequired(): string;
|
|
41
43
|
}
|
|
@@ -19,6 +19,7 @@ export const defaultEditMessages = {
|
|
|
19
19
|
edit_add: ({ label }) => `Afegeix ${label}`,
|
|
20
20
|
edit_addFailed: () => 'No s’ha pogut afegir',
|
|
21
21
|
edit_uploadImage: () => 'Puja una imatge',
|
|
22
|
+
edit_draftBadge: () => 'Esborrany',
|
|
22
23
|
edit_clearValue: () => 'Buida el valor',
|
|
23
24
|
edit_emptyRequired: () => 'Aquest camp no pot quedar buit'
|
|
24
25
|
};
|
package/dist/config/types.d.ts
CHANGED
|
@@ -12,8 +12,12 @@ export type Locale = (typeof LOCALES)[number];
|
|
|
12
12
|
export declare const BASE_LOCALE: Locale;
|
|
13
13
|
/** One primary-navigation entry, rendered by Nav and repeated by Footer. */
|
|
14
14
|
export interface SiteLink {
|
|
15
|
+
/** Present only where the nav is editable — structural ops need identity. */
|
|
16
|
+
id?: string | number;
|
|
15
17
|
href: string;
|
|
16
18
|
label: string;
|
|
19
|
+
/** The row's sort_order, for the CMS's order panel row. */
|
|
20
|
+
order?: number;
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* The strings the components render, as message *functions* so the app's
|
package/dist/content/types.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type SortDirection = 'asc' | 'desc';
|
|
|
18
18
|
/** A weekly as its listing card shows it. */
|
|
19
19
|
export interface WeeklyCardData {
|
|
20
20
|
id: number;
|
|
21
|
+
/** CMS-only: true renders the «Esborrany» badge. The public site never sets it. */
|
|
22
|
+
draft?: boolean;
|
|
21
23
|
number: number;
|
|
22
24
|
slug: string;
|
|
23
25
|
/** ISO date (2026-08-30). */
|
|
@@ -30,6 +32,8 @@ export type ProjectKind = 'collaboration' | 'passion';
|
|
|
30
32
|
/** A project as its card shows it. */
|
|
31
33
|
export interface ProjectCardData {
|
|
32
34
|
id: number;
|
|
35
|
+
/** CMS-only: true renders the «Esborrany» badge. The public site never sets it. */
|
|
36
|
+
draft?: boolean;
|
|
33
37
|
slug: string;
|
|
34
38
|
kind: ProjectKind;
|
|
35
39
|
publishedOn: string;
|
|
@@ -43,6 +47,8 @@ export type MilestoneCategory = 'education' | 'lab' | 'foundation' | 'collaborat
|
|
|
43
47
|
/** One timeline milestone. */
|
|
44
48
|
export interface MilestoneData {
|
|
45
49
|
id: number;
|
|
50
|
+
/** CMS-only: true renders the «Esborrany» badge. The public site never sets it. */
|
|
51
|
+
draft?: boolean;
|
|
46
52
|
occurredOn: string;
|
|
47
53
|
category: MilestoneCategory;
|
|
48
54
|
title: string;
|
|
@@ -68,6 +74,8 @@ export interface CollaboratorData {
|
|
|
68
74
|
export interface JobOpeningData {
|
|
69
75
|
/** Present only where the list is editable — remove ops need an identity. */
|
|
70
76
|
id?: string | number;
|
|
77
|
+
/** CMS-only: true renders the «Esborrany» badge. The public site never sets it. */
|
|
78
|
+
draft?: boolean;
|
|
71
79
|
slug: string;
|
|
72
80
|
title: string;
|
|
73
81
|
description: string | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getUiConfig } from '../../config/context.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The «Esborrany» chip a card wears while its row is unpublished. The flag
|
|
6
|
+
* arrives on the DATA (`draft?: boolean`), set only by a CMS host — the
|
|
7
|
+
* public site never marks anything draft, so its cards render without this
|
|
8
|
+
* component ever mounting. Wording comes from editMessages for the same
|
|
9
|
+
* reason: it is editor-facing, never catalog copy.
|
|
10
|
+
*/
|
|
11
|
+
const config = getUiConfig();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<span class="draft">{config.editMessages.edit_draftBadge()}</span>
|
|
15
|
+
|
|
16
|
+
<style>
|
|
17
|
+
.draft {
|
|
18
|
+
display: inline-block;
|
|
19
|
+
align-self: start;
|
|
20
|
+
border: 1px solid var(--color-ink-muted);
|
|
21
|
+
border-radius: 999px;
|
|
22
|
+
padding: 0.1em 0.7em;
|
|
23
|
+
font-size: var(--text-sm);
|
|
24
|
+
font-weight: 600;
|
|
25
|
+
letter-spacing: 0.04em;
|
|
26
|
+
text-transform: uppercase;
|
|
27
|
+
color: var(--color-ink-muted);
|
|
28
|
+
background: var(--color-surface);
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const DraftBadge: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type DraftBadge = InstanceType<typeof DraftBadge>;
|
|
18
|
+
export default DraftBadge;
|
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' | 'collaborators';
|
|
18
|
+
export type EditableEntity = 'weeklies' | 'projects' | 'milestones' | 'team_members' | 'job_openings' | 'themes' | 'collaborators' | 'site_links';
|
|
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
|