@vit-foundation/ui 0.1.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/LICENSE +201 -0
- package/README.md +129 -0
- package/dist/components/jobs/JobList.svelte +88 -0
- package/dist/components/jobs/JobList.svelte.d.ts +18 -0
- package/dist/components/jobs/index.d.ts +2 -0
- package/dist/components/jobs/index.js +1 -0
- package/dist/components/layout/Footer.svelte +79 -0
- package/dist/components/layout/Footer.svelte.d.ts +12 -0
- package/dist/components/layout/Nav.svelte +229 -0
- package/dist/components/layout/Nav.svelte.d.ts +18 -0
- package/dist/components/layout/PageShell.svelte +107 -0
- package/dist/components/layout/PageShell.svelte.d.ts +42 -0
- package/dist/components/layout/index.d.ts +3 -0
- package/dist/components/layout/index.js +3 -0
- package/dist/components/projects/ProjectCard.svelte +107 -0
- package/dist/components/projects/ProjectCard.svelte.d.ts +17 -0
- package/dist/components/projects/index.d.ts +2 -0
- package/dist/components/projects/index.js +1 -0
- package/dist/components/team/CollaboratorList.svelte +37 -0
- package/dist/components/team/CollaboratorList.svelte.d.ts +7 -0
- package/dist/components/team/TeamMemberCard.svelte +78 -0
- package/dist/components/team/TeamMemberCard.svelte.d.ts +21 -0
- package/dist/components/team/index.d.ts +3 -0
- package/dist/components/team/index.js +2 -0
- package/dist/components/timeline/Timeline.svelte +116 -0
- package/dist/components/timeline/Timeline.svelte.d.ts +12 -0
- package/dist/components/timeline/TimelineMilestone.svelte +127 -0
- package/dist/components/timeline/TimelineMilestone.svelte.d.ts +15 -0
- package/dist/components/timeline/index.d.ts +3 -0
- package/dist/components/timeline/index.js +2 -0
- package/dist/components/ui/Button.svelte +53 -0
- package/dist/components/ui/Button.svelte.d.ts +21 -0
- package/dist/components/ui/CardMedia.svelte +23 -0
- package/dist/components/ui/CardMedia.svelte.d.ts +11 -0
- package/dist/components/ui/CardTitle.svelte +28 -0
- package/dist/components/ui/CardTitle.svelte.d.ts +8 -0
- package/dist/components/ui/CopyIntro.svelte +40 -0
- package/dist/components/ui/CopyIntro.svelte.d.ts +24 -0
- package/dist/components/ui/DateText.svelte +43 -0
- package/dist/components/ui/DateText.svelte.d.ts +28 -0
- package/dist/components/ui/DecorShapes.svelte +36 -0
- package/dist/components/ui/DecorShapes.svelte.d.ts +7 -0
- package/dist/components/ui/Field.svelte +89 -0
- package/dist/components/ui/Field.svelte.d.ts +55 -0
- package/dist/components/ui/FilterChips.svelte +43 -0
- package/dist/components/ui/FilterChips.svelte.d.ts +15 -0
- package/dist/components/ui/FormErrorFeedback.svelte +55 -0
- package/dist/components/ui/FormErrorFeedback.svelte.d.ts +33 -0
- package/dist/components/ui/FormFeedback.svelte +65 -0
- package/dist/components/ui/FormFeedback.svelte.d.ts +30 -0
- package/dist/components/ui/FormResultSlot.svelte +33 -0
- package/dist/components/ui/FormResultSlot.svelte.d.ts +26 -0
- package/dist/components/ui/GhostButton.svelte +46 -0
- package/dist/components/ui/GhostButton.svelte.d.ts +10 -0
- package/dist/components/ui/Link.svelte +30 -0
- package/dist/components/ui/Link.svelte.d.ts +21 -0
- package/dist/components/ui/Pagination.svelte +77 -0
- package/dist/components/ui/Pagination.svelte.d.ts +18 -0
- package/dist/components/ui/RichText.svelte +160 -0
- package/dist/components/ui/RichText.svelte.d.ts +18 -0
- package/dist/components/ui/SearchInput.svelte +90 -0
- package/dist/components/ui/SearchInput.svelte.d.ts +13 -0
- package/dist/components/ui/ShareRow.svelte +95 -0
- package/dist/components/ui/ShareRow.svelte.d.ts +19 -0
- package/dist/components/ui/index.d.ts +17 -0
- package/dist/components/ui/index.js +17 -0
- package/dist/components/weeklies/SortSelect.svelte +42 -0
- package/dist/components/weeklies/SortSelect.svelte.d.ts +8 -0
- package/dist/components/weeklies/WeeklieCard.svelte +82 -0
- package/dist/components/weeklies/WeeklieCard.svelte.d.ts +15 -0
- package/dist/components/weeklies/index.d.ts +3 -0
- package/dist/components/weeklies/index.js +2 -0
- package/dist/config/UiProvider.svelte +21 -0
- package/dist/config/UiProvider.svelte.d.ts +14 -0
- package/dist/config/context.d.ts +17 -0
- package/dist/config/context.js +47 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.js +4 -0
- package/dist/config/messages.d.ts +7 -0
- package/dist/config/messages.js +38 -0
- package/dist/config/types.d.ts +99 -0
- package/dist/config/types.js +9 -0
- package/dist/content/index.d.ts +3 -0
- package/dist/content/index.js +1 -0
- package/dist/content/richtext.d.ts +10 -0
- package/dist/content/richtext.js +14 -0
- package/dist/content/types.d.ts +71 -0
- package/dist/content/types.js +6 -0
- package/dist/edit/Editable.svelte +197 -0
- package/dist/edit/Editable.svelte.d.ts +52 -0
- package/dist/edit/context.d.ts +10 -0
- package/dist/edit/context.js +15 -0
- package/dist/edit/helpers.d.ts +18 -0
- package/dist/edit/helpers.js +14 -0
- package/dist/edit/index.d.ts +5 -0
- package/dist/edit/index.js +4 -0
- package/dist/edit/types.d.ts +64 -0
- package/dist/edit/types.js +4 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +17 -0
- package/dist/styles/base.css +67 -0
- package/dist/styles/tokens.css +57 -0
- package/dist/utils/dates.d.ts +14 -0
- package/dist/utils/dates.js +34 -0
- package/dist/utils/milestones.d.ts +11 -0
- package/dist/utils/milestones.js +28 -0
- package/dist/utils/paths.d.ts +2 -0
- package/dist/utils/paths.js +4 -0
- package/package.json +139 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getUiConfig } from '../../config/context.js';
|
|
3
|
+
import type { MilestoneData } from '../../content/types.js';
|
|
4
|
+
import type { MilestoneEditMap } from './TimelineMilestone.svelte';
|
|
5
|
+
import { yearOf } from '../../utils/dates.js';
|
|
6
|
+
import TimelineMilestone from './TimelineMilestone.svelte';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
milestones: MilestoneData[];
|
|
10
|
+
/** 'compact' = homepage slice; 'full' = transparency history with year markers. */
|
|
11
|
+
variant?: 'compact' | 'full';
|
|
12
|
+
/** Edit descriptors for one milestone's fields; see TimelineMilestone. */
|
|
13
|
+
editFor?: (milestone: MilestoneData) => MilestoneEditMap | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { milestones, variant = 'compact', editFor }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const config = getUiConfig();
|
|
19
|
+
|
|
20
|
+
interface TimelineEntry {
|
|
21
|
+
milestone: MilestoneData;
|
|
22
|
+
/** Set on the first milestone of each year (full variant only). */
|
|
23
|
+
yearMarker: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const entries = $derived(
|
|
27
|
+
milestones.map((milestone, index): TimelineEntry => {
|
|
28
|
+
const year = yearOf(milestone.occurredOn);
|
|
29
|
+
const previousYear = index > 0 ? yearOf(milestones[index - 1].occurredOn) : null;
|
|
30
|
+
return {
|
|
31
|
+
milestone,
|
|
32
|
+
yearMarker: variant === 'full' && year !== previousYear ? year : null
|
|
33
|
+
};
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<!-- Scrollable overflow region: tabindex="0" is required so keyboard users
|
|
39
|
+
can focus it and scroll horizontally (axe: scrollable-region-focusable). -->
|
|
40
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
41
|
+
<div
|
|
42
|
+
class={`timeline ${variant}`}
|
|
43
|
+
role="region"
|
|
44
|
+
aria-label={config.messages.timeline_label()}
|
|
45
|
+
tabindex="0"
|
|
46
|
+
>
|
|
47
|
+
<ol>
|
|
48
|
+
{#each entries as entry (entry.milestone.id)}
|
|
49
|
+
<li>
|
|
50
|
+
{#if entry.yearMarker}
|
|
51
|
+
<span class="year">{entry.yearMarker}</span>
|
|
52
|
+
{/if}
|
|
53
|
+
<TimelineMilestone milestone={entry.milestone} edit={editFor?.(entry.milestone)} />
|
|
54
|
+
</li>
|
|
55
|
+
{/each}
|
|
56
|
+
</ol>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<style>
|
|
60
|
+
.timeline {
|
|
61
|
+
overflow-x: auto;
|
|
62
|
+
padding-block: var(--space-3);
|
|
63
|
+
scrollbar-width: thin;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ol {
|
|
67
|
+
list-style: none;
|
|
68
|
+
margin: 0;
|
|
69
|
+
padding: var(--space-2) 0 0;
|
|
70
|
+
display: flex;
|
|
71
|
+
gap: var(--space-5);
|
|
72
|
+
position: relative;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* The track line the milestone dots sit on. */
|
|
76
|
+
ol::before {
|
|
77
|
+
content: '';
|
|
78
|
+
position: absolute;
|
|
79
|
+
top: 0;
|
|
80
|
+
left: 0;
|
|
81
|
+
right: 0;
|
|
82
|
+
height: 2px;
|
|
83
|
+
background: var(--color-ink);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
li {
|
|
87
|
+
position: relative;
|
|
88
|
+
flex: 0 0 16rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.full li {
|
|
92
|
+
flex-basis: 18rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.year {
|
|
96
|
+
position: absolute;
|
|
97
|
+
top: -2.4rem;
|
|
98
|
+
left: 0;
|
|
99
|
+
font-size: var(--text-lg);
|
|
100
|
+
font-weight: 700;
|
|
101
|
+
color: var(--color-ink-muted);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.full {
|
|
105
|
+
padding-top: var(--space-5);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.timeline:focus-visible {
|
|
109
|
+
outline: 2px solid var(--color-brand);
|
|
110
|
+
outline-offset: 4px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.compact {
|
|
114
|
+
overflow-x: auto;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MilestoneData } from '../../content/types.js';
|
|
2
|
+
import type { MilestoneEditMap } from './TimelineMilestone.svelte';
|
|
3
|
+
interface Props {
|
|
4
|
+
milestones: MilestoneData[];
|
|
5
|
+
/** 'compact' = homepage slice; 'full' = transparency history with year markers. */
|
|
6
|
+
variant?: 'compact' | 'full';
|
|
7
|
+
/** Edit descriptors for one milestone's fields; see TimelineMilestone. */
|
|
8
|
+
editFor?: (milestone: MilestoneData) => MilestoneEditMap | undefined;
|
|
9
|
+
}
|
|
10
|
+
declare const Timeline: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type Timeline = ReturnType<typeof Timeline>;
|
|
12
|
+
export default Timeline;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { EditDescriptor } from '../../edit/types.js';
|
|
3
|
+
|
|
4
|
+
/** Which of the milestone's localized fields are editable at this render site. */
|
|
5
|
+
export interface MilestoneEditMap {
|
|
6
|
+
title?: EditDescriptor;
|
|
7
|
+
body?: EditDescriptor;
|
|
8
|
+
}
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { getUiConfig } from '../../config/context.js';
|
|
13
|
+
import type { MilestoneData } from '../../content/types.js';
|
|
14
|
+
import Editable from '../../edit/Editable.svelte';
|
|
15
|
+
import { MILESTONE_CATEGORY_COLOR, milestoneCategoryLabel } from '../../utils/milestones.js';
|
|
16
|
+
import CardMedia from '../ui/CardMedia.svelte';
|
|
17
|
+
import DateText from '../ui/DateText.svelte';
|
|
18
|
+
import Link from '../ui/Link.svelte';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
milestone: MilestoneData;
|
|
22
|
+
/** Marks fields editable where an edit adapter is active. */
|
|
23
|
+
edit?: MilestoneEditMap;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let { milestone, edit }: Props = $props();
|
|
27
|
+
|
|
28
|
+
const config = getUiConfig();
|
|
29
|
+
|
|
30
|
+
const color = $derived(MILESTONE_CATEGORY_COLOR[milestone.category]);
|
|
31
|
+
/**
|
|
32
|
+
* `link_url` is an unconstrained editable column, and the `press` category
|
|
33
|
+
* exists for coverage that lives on someone else's site. Link is for
|
|
34
|
+
* internal paths: it runs its href through the app's resolver, which may
|
|
35
|
+
* splice a locale segment into a foreign path, and drops the `rel` an
|
|
36
|
+
* outbound link needs. The failure is silent: the anchor renders and only
|
|
37
|
+
* breaks on click.
|
|
38
|
+
*
|
|
39
|
+
* This is the only place the rule is stated, and the only column that needs
|
|
40
|
+
* it. A project's `external_url` and a collaborator's `url` are external by
|
|
41
|
+
* definition, so their render sites emit `rel="external noopener"` with no
|
|
42
|
+
* predicate — they never route the value through Link, so nothing splices a
|
|
43
|
+
* locale into it.
|
|
44
|
+
*/
|
|
45
|
+
const isExternal = $derived(/^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(milestone.linkUrl ?? ''));
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<article style={`--milestone-color: ${color}`}>
|
|
49
|
+
<span class="dot" aria-hidden="true"></span>
|
|
50
|
+
<p class="category">{milestoneCategoryLabel(milestone.category, config.messages)}</p>
|
|
51
|
+
<DateText value={milestone.occurredOn} />
|
|
52
|
+
<Editable edit={edit?.title} value={milestone.title}>
|
|
53
|
+
{#snippet children(text, attrs)}<h3 {...attrs}>{text}</h3>{/snippet}
|
|
54
|
+
</Editable>
|
|
55
|
+
{#if milestone.body}
|
|
56
|
+
<Editable edit={edit?.body} value={milestone.body}>
|
|
57
|
+
{#snippet children(text, attrs)}<p class="body" {...attrs}>{text}</p>{/snippet}
|
|
58
|
+
</Editable>
|
|
59
|
+
{/if}
|
|
60
|
+
{#if milestone.imageUrls.length > 0}
|
|
61
|
+
<CardMedia src={milestone.imageUrls[0]} alt="" width="1200" height="675" />
|
|
62
|
+
{/if}
|
|
63
|
+
{#if milestone.linkUrl}
|
|
64
|
+
<p class="more">
|
|
65
|
+
{#if isExternal}
|
|
66
|
+
<a href={milestone.linkUrl} rel="external noopener">{config.messages.common_readMore()}</a>
|
|
67
|
+
{:else}
|
|
68
|
+
<Link href={milestone.linkUrl}>{config.messages.common_readMore()}</Link>
|
|
69
|
+
{/if}
|
|
70
|
+
</p>
|
|
71
|
+
{/if}
|
|
72
|
+
</article>
|
|
73
|
+
|
|
74
|
+
<style>
|
|
75
|
+
article {
|
|
76
|
+
position: relative;
|
|
77
|
+
padding-top: var(--space-4);
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
gap: var(--space-1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Sits on the track line drawn by the parent Timeline. */
|
|
84
|
+
.dot {
|
|
85
|
+
position: absolute;
|
|
86
|
+
top: -7px;
|
|
87
|
+
left: 0;
|
|
88
|
+
width: 14px;
|
|
89
|
+
height: 14px;
|
|
90
|
+
border-radius: 50%;
|
|
91
|
+
background: var(--milestone-color);
|
|
92
|
+
border: 2px solid var(--color-surface);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.category {
|
|
96
|
+
margin: 0;
|
|
97
|
+
font-size: var(--text-sm);
|
|
98
|
+
font-weight: 700;
|
|
99
|
+
text-transform: uppercase;
|
|
100
|
+
letter-spacing: 0.08em;
|
|
101
|
+
color: var(--milestone-color);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
h3 {
|
|
105
|
+
margin: 0;
|
|
106
|
+
font-size: var(--text-base);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.body {
|
|
110
|
+
margin: 0;
|
|
111
|
+
font-size: var(--text-sm);
|
|
112
|
+
color: var(--color-ink-secondary);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
article > :global(img) {
|
|
116
|
+
margin-top: var(--space-2);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.more {
|
|
120
|
+
margin: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.more :global(a) {
|
|
124
|
+
color: var(--color-brand);
|
|
125
|
+
font-weight: 600;
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EditDescriptor } from '../../edit/types.js';
|
|
2
|
+
/** Which of the milestone's localized fields are editable at this render site. */
|
|
3
|
+
export interface MilestoneEditMap {
|
|
4
|
+
title?: EditDescriptor;
|
|
5
|
+
body?: EditDescriptor;
|
|
6
|
+
}
|
|
7
|
+
import type { MilestoneData } from '../../content/types.js';
|
|
8
|
+
interface Props {
|
|
9
|
+
milestone: MilestoneData;
|
|
10
|
+
/** Marks fields editable where an edit adapter is active. */
|
|
11
|
+
edit?: MilestoneEditMap;
|
|
12
|
+
}
|
|
13
|
+
declare const TimelineMilestone: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type TimelineMilestone = ReturnType<typeof TimelineMilestone>;
|
|
15
|
+
export default TimelineMilestone;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
interface Props extends HTMLButtonAttributes {
|
|
6
|
+
/**
|
|
7
|
+
* The submitting count of the remote form this button posts, or `null`
|
|
8
|
+
* for a control that genuinely has no pending state — the classic
|
|
9
|
+
* `action="?/…"` post, which has no count to read.
|
|
10
|
+
*
|
|
11
|
+
* Required and explicitly nullable, for the reason **field constraint**
|
|
12
|
+
* gives: optional made "this control cannot be pending" and "nobody
|
|
13
|
+
* thought about it" one value, and two submits had already taken the
|
|
14
|
+
* second — both logout, both double-submittable. A count rather than a
|
|
15
|
+
* boolean because that is what kit hands back, and `> 0` was the rule
|
|
16
|
+
* twelve call sites re-derived. It belongs here, once.
|
|
17
|
+
*/
|
|
18
|
+
pending: number | null;
|
|
19
|
+
children: Snippet;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { pending, children, type = 'button', disabled = false, ...rest }: Props = $props();
|
|
23
|
+
|
|
24
|
+
// `disabled` stays a separate prop: a button can be unavailable for a reason
|
|
25
|
+
// of its own — the Google button before consent — as well as while posting.
|
|
26
|
+
const isPending = $derived((pending ?? 0) > 0);
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<button {type} {...rest} disabled={disabled || isPending} aria-busy={isPending || undefined}>
|
|
30
|
+
{@render children()}
|
|
31
|
+
</button>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
button {
|
|
35
|
+
padding: var(--space-2) var(--space-4);
|
|
36
|
+
border: none;
|
|
37
|
+
border-radius: var(--radius);
|
|
38
|
+
background: var(--color-brand);
|
|
39
|
+
color: var(--color-surface);
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
transition: background var(--transition-fast);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
button:hover {
|
|
46
|
+
background: var(--color-navy);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
button:disabled {
|
|
50
|
+
opacity: 0.6;
|
|
51
|
+
cursor: default;
|
|
52
|
+
}
|
|
53
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
+
interface Props extends HTMLButtonAttributes {
|
|
4
|
+
/**
|
|
5
|
+
* The submitting count of the remote form this button posts, or `null`
|
|
6
|
+
* for a control that genuinely has no pending state — the classic
|
|
7
|
+
* `action="?/…"` post, which has no count to read.
|
|
8
|
+
*
|
|
9
|
+
* Required and explicitly nullable, for the reason **field constraint**
|
|
10
|
+
* gives: optional made "this control cannot be pending" and "nobody
|
|
11
|
+
* thought about it" one value, and two submits had already taken the
|
|
12
|
+
* second — both logout, both double-submittable. A count rather than a
|
|
13
|
+
* boolean because that is what kit hands back, and `> 0` was the rule
|
|
14
|
+
* twelve call sites re-derived. It belongs here, once.
|
|
15
|
+
*/
|
|
16
|
+
pending: number | null;
|
|
17
|
+
children: Snippet;
|
|
18
|
+
}
|
|
19
|
+
declare const Button: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type Button = ReturnType<typeof Button>;
|
|
21
|
+
export default Button;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
interface Props extends HTMLImgAttributes {
|
|
5
|
+
src: string;
|
|
6
|
+
/** Empty string for decorative images (the surrounding card carries the text). */
|
|
7
|
+
alt: string;
|
|
8
|
+
/** CSS aspect-ratio (e.g. '16 / 9'); omit to keep the image's natural height. */
|
|
9
|
+
ratio?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { src, alt, ratio, ...rest }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<img {src} {alt} loading="lazy" style:aspect-ratio={ratio} {...rest} />
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
img {
|
|
19
|
+
border-radius: var(--radius);
|
|
20
|
+
background: var(--color-band-grey);
|
|
21
|
+
object-fit: cover;
|
|
22
|
+
}
|
|
23
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props extends HTMLImgAttributes {
|
|
3
|
+
src: string;
|
|
4
|
+
/** Empty string for decorative images (the surrounding card carries the text). */
|
|
5
|
+
alt: string;
|
|
6
|
+
/** CSS aspect-ratio (e.g. '16 / 9'); omit to keep the image's natural height. */
|
|
7
|
+
ratio?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const CardMedia: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type CardMedia = ReturnType<typeof CardMedia>;
|
|
11
|
+
export default CardMedia;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** The title content — plain text or a Link/anchor around it. */
|
|
6
|
+
children: Snippet;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { children }: Props = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<h3>{@render children()}</h3>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
h3 {
|
|
16
|
+
margin: 0;
|
|
17
|
+
font-size: var(--text-lg);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
h3 :global(a) {
|
|
21
|
+
text-decoration: none;
|
|
22
|
+
color: var(--color-ink);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
h3 :global(a:hover) {
|
|
26
|
+
text-decoration: underline;
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The title content — plain text or a Link/anchor around it. */
|
|
4
|
+
children: Snippet;
|
|
5
|
+
}
|
|
6
|
+
declare const CardTitle: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type CardTitle = ReturnType<typeof CardTitle>;
|
|
8
|
+
export default CardTitle;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Editable from '../../edit/Editable.svelte';
|
|
3
|
+
import type { EditDescriptor } from '../../edit/types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An editorial intro paragraph.
|
|
7
|
+
*
|
|
8
|
+
* Owns the intro's element and styling, which five route style blocks
|
|
9
|
+
* repeated. It deliberately does NOT decide what an empty block means: the
|
|
10
|
+
* foundation site's repository integration test asserts that every key a
|
|
11
|
+
* page declares resolves to non-empty copy, so `''` is a failing suite
|
|
12
|
+
* rather than a state to render around. This component used to guard for
|
|
13
|
+
* it, and that guard taught every new render site that it had a decision
|
|
14
|
+
* to make here.
|
|
15
|
+
*
|
|
16
|
+
* Not for a hero subtitle or a band lede: those are different visual roles
|
|
17
|
+
* with their own styling, not editorial intros.
|
|
18
|
+
*/
|
|
19
|
+
interface Props {
|
|
20
|
+
/** Copy for the block. Asserted non-empty; see the note above. */
|
|
21
|
+
text: string;
|
|
22
|
+
/** Marks the copy editable where an edit adapter is active. */
|
|
23
|
+
edit?: EditDescriptor;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let { text, edit }: Props = $props();
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<Editable {edit} value={text}>
|
|
30
|
+
{#snippet children(value, attrs)}
|
|
31
|
+
<p class="intro" {...attrs}>{value}</p>
|
|
32
|
+
{/snippet}
|
|
33
|
+
</Editable>
|
|
34
|
+
|
|
35
|
+
<style>
|
|
36
|
+
.intro {
|
|
37
|
+
color: var(--color-ink-secondary);
|
|
38
|
+
max-width: 60ch;
|
|
39
|
+
}
|
|
40
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EditDescriptor } from '../../edit/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* An editorial intro paragraph.
|
|
4
|
+
*
|
|
5
|
+
* Owns the intro's element and styling, which five route style blocks
|
|
6
|
+
* repeated. It deliberately does NOT decide what an empty block means: the
|
|
7
|
+
* foundation site's repository integration test asserts that every key a
|
|
8
|
+
* page declares resolves to non-empty copy, so `''` is a failing suite
|
|
9
|
+
* rather than a state to render around. This component used to guard for
|
|
10
|
+
* it, and that guard taught every new render site that it had a decision
|
|
11
|
+
* to make here.
|
|
12
|
+
*
|
|
13
|
+
* Not for a hero subtitle or a band lede: those are different visual roles
|
|
14
|
+
* with their own styling, not editorial intros.
|
|
15
|
+
*/
|
|
16
|
+
interface Props {
|
|
17
|
+
/** Copy for the block. Asserted non-empty; see the note above. */
|
|
18
|
+
text: string;
|
|
19
|
+
/** Marks the copy editable where an edit adapter is active. */
|
|
20
|
+
edit?: EditDescriptor;
|
|
21
|
+
}
|
|
22
|
+
declare const CopyIntro: import("svelte").Component<Props, {}, "">;
|
|
23
|
+
type CopyIntro = ReturnType<typeof CopyIntro>;
|
|
24
|
+
export default CopyIntro;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getUiConfig } from '../../config/context.js';
|
|
3
|
+
import { formatDate } from '../../utils/dates.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A published, posted or occurred date: the machine-readable value and the
|
|
7
|
+
* human string, from one module.
|
|
8
|
+
*
|
|
9
|
+
* `formatDate` owned the string, but the element around it owned nothing —
|
|
10
|
+
* seven render sites restated the `datetime` attribute, the locale
|
|
11
|
+
* read and the muted styling, and five read the locale for that one
|
|
12
|
+
* line and nothing else. A `<time>` that loses its `datetime` renders
|
|
13
|
+
* identically, so the defect is invisible on screen and costs only
|
|
14
|
+
* assistive technology and search engines: the same silent-failure shape
|
|
15
|
+
* LocaleField owns for forms.
|
|
16
|
+
*
|
|
17
|
+
* Timestamps are accepted as well as dates. The attribute carries the whole
|
|
18
|
+
* ISO value while the text shows the day, which is what a comment's
|
|
19
|
+
* `createdAt` needs and what its call site used to slice by hand.
|
|
20
|
+
*
|
|
21
|
+
* Resolving a timestamp to a day belongs to `formatDate`, not here: slicing
|
|
22
|
+
* the first ten characters takes the *UTC* day and then renders it as if it
|
|
23
|
+
* were a local one, so a comment posted just after midnight in Barcelona
|
|
24
|
+
* showed the previous date.
|
|
25
|
+
*/
|
|
26
|
+
interface Props {
|
|
27
|
+
/** An ISO date (`2026-08-28`) or timestamp; both render as a day. */
|
|
28
|
+
value: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let { value }: Props = $props();
|
|
32
|
+
|
|
33
|
+
const config = getUiConfig();
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<time datetime={value}>{formatDate(value, config.locale())}</time>
|
|
37
|
+
|
|
38
|
+
<style>
|
|
39
|
+
time {
|
|
40
|
+
font-size: var(--text-sm);
|
|
41
|
+
color: var(--color-ink-muted);
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A published, posted or occurred date: the machine-readable value and the
|
|
3
|
+
* human string, from one module.
|
|
4
|
+
*
|
|
5
|
+
* `formatDate` owned the string, but the element around it owned nothing —
|
|
6
|
+
* seven render sites restated the `datetime` attribute, the locale
|
|
7
|
+
* read and the muted styling, and five read the locale for that one
|
|
8
|
+
* line and nothing else. A `<time>` that loses its `datetime` renders
|
|
9
|
+
* identically, so the defect is invisible on screen and costs only
|
|
10
|
+
* assistive technology and search engines: the same silent-failure shape
|
|
11
|
+
* LocaleField owns for forms.
|
|
12
|
+
*
|
|
13
|
+
* Timestamps are accepted as well as dates. The attribute carries the whole
|
|
14
|
+
* ISO value while the text shows the day, which is what a comment's
|
|
15
|
+
* `createdAt` needs and what its call site used to slice by hand.
|
|
16
|
+
*
|
|
17
|
+
* Resolving a timestamp to a day belongs to `formatDate`, not here: slicing
|
|
18
|
+
* the first ten characters takes the *UTC* day and then renders it as if it
|
|
19
|
+
* were a local one, so a comment posted just after midnight in Barcelona
|
|
20
|
+
* showed the previous date.
|
|
21
|
+
*/
|
|
22
|
+
interface Props {
|
|
23
|
+
/** An ISO date (`2026-08-28`) or timestamp; both render as a day. */
|
|
24
|
+
value: string;
|
|
25
|
+
}
|
|
26
|
+
declare const DateText: import("svelte").Component<Props, {}, "">;
|
|
27
|
+
type DateText = ReturnType<typeof DateText>;
|
|
28
|
+
export default DateText;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Mirrors the composition horizontally (wireframes place shapes on both page corners). */
|
|
4
|
+
flip?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let { flip = false }: Props = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<svg
|
|
11
|
+
class="shapes"
|
|
12
|
+
class:flip
|
|
13
|
+
viewBox="0 0 240 160"
|
|
14
|
+
width="240"
|
|
15
|
+
height="160"
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
focusable="false"
|
|
18
|
+
>
|
|
19
|
+
<rect x="0" y="80" width="80" height="80" fill="var(--color-navy)" />
|
|
20
|
+
<path d="M80 80 L160 80 L80 160 Z" fill="var(--color-magenta)" />
|
|
21
|
+
<rect x="160" y="80" width="80" height="80" fill="var(--color-cream)" />
|
|
22
|
+
<path d="M160 0 A80 80 0 0 1 240 80 L160 80 Z" fill="var(--color-brand-light)" />
|
|
23
|
+
<rect x="80" y="20" width="60" height="60" fill="var(--color-navy)" opacity="0.15" />
|
|
24
|
+
</svg>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
.shapes {
|
|
28
|
+
display: block;
|
|
29
|
+
max-width: 100%;
|
|
30
|
+
height: auto;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.flip {
|
|
34
|
+
transform: scaleX(-1);
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Mirrors the composition horizontally (wireframes place shapes on both page corners). */
|
|
3
|
+
flip?: boolean;
|
|
4
|
+
}
|
|
5
|
+
declare const DecorShapes: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type DecorShapes = ReturnType<typeof DecorShapes>;
|
|
7
|
+
export default DecorShapes;
|