@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,160 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { renderBody } from '../../content/richtext.js';
|
|
3
|
+
import { getEditAdapter } from '../../edit/context.js';
|
|
4
|
+
import type { EditDescriptor } from '../../edit/types.js';
|
|
5
|
+
import Button from './Button.svelte';
|
|
6
|
+
import GhostButton from './GhostButton.svelte';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Renders the block mini-format (`## ` subheadings, blank-line paragraphs)
|
|
10
|
+
* with real elements — never `{@html}`.
|
|
11
|
+
*
|
|
12
|
+
* With an `edit` descriptor and an active adapter it offers a source
|
|
13
|
+
* editor: the mini-format *is* the stored format, so a textarea over the
|
|
14
|
+
* raw body with a live preview underneath is the honest v1. A block-level
|
|
15
|
+
* WYSIWYG is future work; the save contract would not change.
|
|
16
|
+
*/
|
|
17
|
+
interface Props {
|
|
18
|
+
body: string;
|
|
19
|
+
/** Marks the body editable where an edit adapter is active. */
|
|
20
|
+
edit?: EditDescriptor;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let { body, edit }: Props = $props();
|
|
24
|
+
|
|
25
|
+
const adapter = getEditAdapter();
|
|
26
|
+
const canEdit = $derived(edit !== undefined && (adapter?.isEditing ?? false));
|
|
27
|
+
|
|
28
|
+
/** Last persisted body: what renders, and what Cancel·la returns to. */
|
|
29
|
+
// svelte-ignore state_referenced_locally
|
|
30
|
+
let savedBody = $state(body);
|
|
31
|
+
let draft = $state('');
|
|
32
|
+
let isOpen = $state(false);
|
|
33
|
+
let status = $state<'idle' | 'saving' | 'error'>('idle');
|
|
34
|
+
|
|
35
|
+
// Follow the prop when the app reloads content — and only then (after a
|
|
36
|
+
// save, savedBody has legitimately advanced past the prop). Never over an
|
|
37
|
+
// open editor.
|
|
38
|
+
// svelte-ignore state_referenced_locally
|
|
39
|
+
let lastPropBody = $state(body);
|
|
40
|
+
$effect(() => {
|
|
41
|
+
if (body !== lastPropBody) {
|
|
42
|
+
lastPropBody = body;
|
|
43
|
+
if (!isOpen) savedBody = body;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const blocks = $derived(renderBody(savedBody));
|
|
48
|
+
const preview = $derived(renderBody(draft));
|
|
49
|
+
|
|
50
|
+
function open(): void {
|
|
51
|
+
draft = savedBody;
|
|
52
|
+
status = 'idle';
|
|
53
|
+
isOpen = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function cancel(): void {
|
|
57
|
+
isOpen = false;
|
|
58
|
+
status = 'idle';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function save(): Promise<void> {
|
|
62
|
+
if (!edit || !adapter) return;
|
|
63
|
+
status = 'saving';
|
|
64
|
+
try {
|
|
65
|
+
await adapter.save(edit, draft);
|
|
66
|
+
savedBody = draft;
|
|
67
|
+
isOpen = false;
|
|
68
|
+
status = 'idle';
|
|
69
|
+
} catch {
|
|
70
|
+
// The draft stays in the textarea; the reader decides whether to retry.
|
|
71
|
+
status = 'error';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
{#if canEdit && isOpen}
|
|
77
|
+
<div class="editor">
|
|
78
|
+
<textarea
|
|
79
|
+
class="control"
|
|
80
|
+
bind:value={draft}
|
|
81
|
+
rows={Math.max(6, draft.split('\n').length + 1)}
|
|
82
|
+
aria-label={edit?.label ?? 'Edita el contingut'}
|
|
83
|
+
></textarea>
|
|
84
|
+
<div class="actions">
|
|
85
|
+
<Button pending={status === 'saving' ? 1 : 0} onclick={() => void save()}>Desa</Button>
|
|
86
|
+
<GhostButton onclick={cancel}>Cancel·la</GhostButton>
|
|
87
|
+
{#if status === 'error'}
|
|
88
|
+
<p class="error" role="alert">Error en desar. Torna-ho a provar.</p>
|
|
89
|
+
{/if}
|
|
90
|
+
</div>
|
|
91
|
+
<div class="richtext preview" aria-label="Vista prèvia">
|
|
92
|
+
{#each preview as block, index (index)}
|
|
93
|
+
{#if block.type === 'h2'}
|
|
94
|
+
<h2>{block.text}</h2>
|
|
95
|
+
{:else}
|
|
96
|
+
<p>{block.text}</p>
|
|
97
|
+
{/if}
|
|
98
|
+
{/each}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
{:else}
|
|
102
|
+
<div class="richtext">
|
|
103
|
+
{#each blocks as block, index (index)}
|
|
104
|
+
{#if block.type === 'h2'}
|
|
105
|
+
<h2>{block.text}</h2>
|
|
106
|
+
{:else}
|
|
107
|
+
<p>{block.text}</p>
|
|
108
|
+
{/if}
|
|
109
|
+
{/each}
|
|
110
|
+
{#if canEdit}
|
|
111
|
+
<p class="edit-row"><GhostButton onclick={open}>Edita</GhostButton></p>
|
|
112
|
+
{/if}
|
|
113
|
+
</div>
|
|
114
|
+
{/if}
|
|
115
|
+
|
|
116
|
+
<style>
|
|
117
|
+
.richtext {
|
|
118
|
+
max-width: 65ch;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
h2 {
|
|
122
|
+
font-size: var(--text-lg);
|
|
123
|
+
margin-block: var(--space-4) var(--space-2);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.editor {
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
gap: var(--space-3);
|
|
130
|
+
max-width: 65ch;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
textarea {
|
|
134
|
+
width: 100%;
|
|
135
|
+
padding: var(--space-2) var(--space-3);
|
|
136
|
+
font: inherit;
|
|
137
|
+
resize: vertical;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.actions {
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
gap: var(--space-2);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.error {
|
|
147
|
+
margin: 0;
|
|
148
|
+
color: var(--series-8);
|
|
149
|
+
font-size: var(--text-sm);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.preview {
|
|
153
|
+
border-top: 1px dashed var(--color-hairline);
|
|
154
|
+
padding-top: var(--space-3);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.edit-row {
|
|
158
|
+
margin: var(--space-2) 0 0;
|
|
159
|
+
}
|
|
160
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EditDescriptor } from '../../edit/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Renders the block mini-format (`## ` subheadings, blank-line paragraphs)
|
|
4
|
+
* with real elements — never `{@html}`.
|
|
5
|
+
*
|
|
6
|
+
* With an `edit` descriptor and an active adapter it offers a source
|
|
7
|
+
* editor: the mini-format *is* the stored format, so a textarea over the
|
|
8
|
+
* raw body with a live preview underneath is the honest v1. A block-level
|
|
9
|
+
* WYSIWYG is future work; the save contract would not change.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
body: string;
|
|
13
|
+
/** Marks the body editable where an edit adapter is active. */
|
|
14
|
+
edit?: EditDescriptor;
|
|
15
|
+
}
|
|
16
|
+
declare const RichText: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type RichText = ReturnType<typeof RichText>;
|
|
18
|
+
export default RichText;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Ties the label to the input; override when a page renders two. */
|
|
4
|
+
id?: string;
|
|
5
|
+
value?: string;
|
|
6
|
+
placeholder: string;
|
|
7
|
+
label: string;
|
|
8
|
+
/** Called after the debounce delay with the trimmed query. */
|
|
9
|
+
onsearch: (query: string) => void;
|
|
10
|
+
debounceMs?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_DEBOUNCE_MS = 300;
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
id = 'search-input',
|
|
17
|
+
value = '',
|
|
18
|
+
placeholder,
|
|
19
|
+
label,
|
|
20
|
+
onsearch,
|
|
21
|
+
debounceMs = DEFAULT_DEBOUNCE_MS
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
// svelte-ignore state_referenced_locally
|
|
25
|
+
let query = $state(value);
|
|
26
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The last query this input reported, so it can tell its own echo from a
|
|
30
|
+
* new value. The caller writes what we emit back into `value`, and on the
|
|
31
|
+
* weeklies index that round trip lands mid-debounce: adopting `value`
|
|
32
|
+
* unconditionally would overwrite the characters typed since.
|
|
33
|
+
*/
|
|
34
|
+
// svelte-ignore state_referenced_locally
|
|
35
|
+
let emitted = $state(value);
|
|
36
|
+
|
|
37
|
+
// `value` seeds the field and then follows it, but only when the caller
|
|
38
|
+
// reports something this input did not ask for — a navigation that changed
|
|
39
|
+
// the filters. Without this the box kept the previous page's search text
|
|
40
|
+
// while the results below it came back unfiltered.
|
|
41
|
+
$effect(() => {
|
|
42
|
+
if (value !== emitted) {
|
|
43
|
+
query = value;
|
|
44
|
+
emitted = value;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function emit(): void {
|
|
49
|
+
emitted = query.trim();
|
|
50
|
+
onsearch(emitted);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function handleInput(): void {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
timer = setTimeout(emit, debounceMs);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The pending timer outlives the component without this. Typing and then
|
|
59
|
+
// clicking a result within the debounce window fired `onsearch` from a
|
|
60
|
+
// destroyed input, and on the weeklies index that writes the query back to
|
|
61
|
+
// the URL — rewriting the address bar of whatever page the reader had just
|
|
62
|
+
// navigated to.
|
|
63
|
+
$effect(() => () => clearTimeout(timer));
|
|
64
|
+
|
|
65
|
+
function handleSubmit(event: SubmitEvent): void {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
emit();
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<form role="search" onsubmit={handleSubmit}>
|
|
73
|
+
<label class="visually-hidden" for={id}>{label}</label>
|
|
74
|
+
<input
|
|
75
|
+
{id}
|
|
76
|
+
class="control"
|
|
77
|
+
type="search"
|
|
78
|
+
{placeholder}
|
|
79
|
+
bind:value={query}
|
|
80
|
+
oninput={handleInput}
|
|
81
|
+
/>
|
|
82
|
+
</form>
|
|
83
|
+
|
|
84
|
+
<style>
|
|
85
|
+
input {
|
|
86
|
+
width: 100%;
|
|
87
|
+
max-width: 24rem;
|
|
88
|
+
padding: var(--space-2) var(--space-3);
|
|
89
|
+
}
|
|
90
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Ties the label to the input; override when a page renders two. */
|
|
3
|
+
id?: string;
|
|
4
|
+
value?: string;
|
|
5
|
+
placeholder: string;
|
|
6
|
+
label: string;
|
|
7
|
+
/** Called after the debounce delay with the trimmed query. */
|
|
8
|
+
onsearch: (query: string) => void;
|
|
9
|
+
debounceMs?: number;
|
|
10
|
+
}
|
|
11
|
+
declare const SearchInput: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type SearchInput = ReturnType<typeof SearchInput>;
|
|
13
|
+
export default SearchInput;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getUiConfig } from '../../config/context.js';
|
|
3
|
+
import GhostButton from './GhostButton.svelte';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The page URL and the clipboard are parameters, not module-scope reads.
|
|
7
|
+
* The remote forms already substitute this way (`form?: typeof defaultForm`);
|
|
8
|
+
* routing state never did, which is why the copy branches below — the reset
|
|
9
|
+
* timeout and the rejected write — had no test.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Title used in the share text. */
|
|
13
|
+
title: string;
|
|
14
|
+
/** The URL being shared. Only tests and stories pass one. */
|
|
15
|
+
url?: string;
|
|
16
|
+
/** Only tests and stories pass one. */
|
|
17
|
+
writeClipboard?: (text: string) => Promise<void>;
|
|
18
|
+
/** How long the "copied" confirmation stays up. */
|
|
19
|
+
copiedResetMs?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
title,
|
|
24
|
+
url = undefined,
|
|
25
|
+
writeClipboard = (text) => navigator.clipboard.writeText(text),
|
|
26
|
+
copiedResetMs = 2000
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
const config = getUiConfig();
|
|
30
|
+
const msg = $derived(config.messages);
|
|
31
|
+
|
|
32
|
+
let isCopied = $state(false);
|
|
33
|
+
|
|
34
|
+
const shareUrl = $derived(url ?? config.url()?.href ?? '');
|
|
35
|
+
const encodedUrl = $derived(encodeURIComponent(shareUrl));
|
|
36
|
+
const encodedTitle = $derived(encodeURIComponent(title));
|
|
37
|
+
|
|
38
|
+
async function copyLink(): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
await writeClipboard(shareUrl);
|
|
41
|
+
isCopied = true;
|
|
42
|
+
setTimeout(() => (isCopied = false), copiedResetMs);
|
|
43
|
+
} catch {
|
|
44
|
+
isCopied = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<div class="share">
|
|
50
|
+
<span class="label">{msg.share_label()}</span>
|
|
51
|
+
<a
|
|
52
|
+
href={`https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`}
|
|
53
|
+
rel="external noopener"
|
|
54
|
+
target="_blank"
|
|
55
|
+
>
|
|
56
|
+
X
|
|
57
|
+
</a>
|
|
58
|
+
<a
|
|
59
|
+
href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`}
|
|
60
|
+
rel="external noopener"
|
|
61
|
+
target="_blank"
|
|
62
|
+
>
|
|
63
|
+
LinkedIn
|
|
64
|
+
</a>
|
|
65
|
+
<GhostButton class="copy" onclick={copyLink}>
|
|
66
|
+
{isCopied ? msg.share_copied() : msg.share_copyLink()}
|
|
67
|
+
</GhostButton>
|
|
68
|
+
<span class="visually-hidden" role="status">{isCopied ? msg.share_copied() : ''}</span>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<style>
|
|
72
|
+
.share {
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
gap: var(--space-3);
|
|
76
|
+
padding-block: var(--space-3);
|
|
77
|
+
border-top: 1px solid var(--color-hairline);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.label {
|
|
81
|
+
font-weight: 700;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
a {
|
|
85
|
+
color: var(--color-ink-secondary);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.share :global(button.copy) {
|
|
89
|
+
color: var(--color-ink-secondary);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.share :global(button.copy:hover) {
|
|
93
|
+
color: var(--color-brand);
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page URL and the clipboard are parameters, not module-scope reads.
|
|
3
|
+
* The remote forms already substitute this way (`form?: typeof defaultForm`);
|
|
4
|
+
* routing state never did, which is why the copy branches below — the reset
|
|
5
|
+
* timeout and the rejected write — had no test.
|
|
6
|
+
*/
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Title used in the share text. */
|
|
9
|
+
title: string;
|
|
10
|
+
/** The URL being shared. Only tests and stories pass one. */
|
|
11
|
+
url?: string;
|
|
12
|
+
/** Only tests and stories pass one. */
|
|
13
|
+
writeClipboard?: (text: string) => Promise<void>;
|
|
14
|
+
/** How long the "copied" confirmation stays up. */
|
|
15
|
+
copiedResetMs?: number;
|
|
16
|
+
}
|
|
17
|
+
declare const ShareRow: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type ShareRow = ReturnType<typeof ShareRow>;
|
|
19
|
+
export default ShareRow;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { default as Button } from './Button.svelte';
|
|
2
|
+
export { default as CardMedia } from './CardMedia.svelte';
|
|
3
|
+
export { default as CardTitle } from './CardTitle.svelte';
|
|
4
|
+
export { default as CopyIntro } from './CopyIntro.svelte';
|
|
5
|
+
export { default as DateText } from './DateText.svelte';
|
|
6
|
+
export { default as DecorShapes } from './DecorShapes.svelte';
|
|
7
|
+
export { default as Field } from './Field.svelte';
|
|
8
|
+
export { default as FilterChips } from './FilterChips.svelte';
|
|
9
|
+
export { default as FormErrorFeedback } from './FormErrorFeedback.svelte';
|
|
10
|
+
export { default as FormFeedback } from './FormFeedback.svelte';
|
|
11
|
+
export { default as FormResultSlot } from './FormResultSlot.svelte';
|
|
12
|
+
export { default as GhostButton } from './GhostButton.svelte';
|
|
13
|
+
export { default as Link } from './Link.svelte';
|
|
14
|
+
export { default as Pagination } from './Pagination.svelte';
|
|
15
|
+
export { default as RichText } from './RichText.svelte';
|
|
16
|
+
export { default as SearchInput } from './SearchInput.svelte';
|
|
17
|
+
export { default as ShareRow } from './ShareRow.svelte';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { default as Button } from './Button.svelte';
|
|
2
|
+
export { default as CardMedia } from './CardMedia.svelte';
|
|
3
|
+
export { default as CardTitle } from './CardTitle.svelte';
|
|
4
|
+
export { default as CopyIntro } from './CopyIntro.svelte';
|
|
5
|
+
export { default as DateText } from './DateText.svelte';
|
|
6
|
+
export { default as DecorShapes } from './DecorShapes.svelte';
|
|
7
|
+
export { default as Field } from './Field.svelte';
|
|
8
|
+
export { default as FilterChips } from './FilterChips.svelte';
|
|
9
|
+
export { default as FormErrorFeedback } from './FormErrorFeedback.svelte';
|
|
10
|
+
export { default as FormFeedback } from './FormFeedback.svelte';
|
|
11
|
+
export { default as FormResultSlot } from './FormResultSlot.svelte';
|
|
12
|
+
export { default as GhostButton } from './GhostButton.svelte';
|
|
13
|
+
export { default as Link } from './Link.svelte';
|
|
14
|
+
export { default as Pagination } from './Pagination.svelte';
|
|
15
|
+
export { default as RichText } from './RichText.svelte';
|
|
16
|
+
export { default as SearchInput } from './SearchInput.svelte';
|
|
17
|
+
export { default as ShareRow } from './ShareRow.svelte';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getUiConfig } from '../../config/context.js';
|
|
3
|
+
import type { SortDirection } from '../../content/types.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
value: SortDirection;
|
|
7
|
+
onchange: (value: SortDirection) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { value, onchange }: Props = $props();
|
|
11
|
+
|
|
12
|
+
const config = getUiConfig();
|
|
13
|
+
const msg = $derived(config.messages);
|
|
14
|
+
|
|
15
|
+
function handleChange(event: Event): void {
|
|
16
|
+
const target = event.currentTarget as HTMLSelectElement;
|
|
17
|
+
onchange(target.value === 'asc' ? 'asc' : 'desc');
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<label>
|
|
22
|
+
<span>{msg.weeklies_sortLabel()}</span>
|
|
23
|
+
<select class="control" {value} onchange={handleChange}>
|
|
24
|
+
<option value="desc">{msg.weeklies_sortDesc()}</option>
|
|
25
|
+
<option value="asc">{msg.weeklies_sortAsc()}</option>
|
|
26
|
+
</select>
|
|
27
|
+
</label>
|
|
28
|
+
|
|
29
|
+
<style>
|
|
30
|
+
label {
|
|
31
|
+
display: inline-flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
gap: var(--space-1);
|
|
34
|
+
font-size: var(--text-sm);
|
|
35
|
+
color: var(--color-ink-secondary);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
select {
|
|
39
|
+
padding: var(--space-1) var(--space-2);
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SortDirection } from '../../content/types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
value: SortDirection;
|
|
4
|
+
onchange: (value: SortDirection) => void;
|
|
5
|
+
}
|
|
6
|
+
declare const SortSelect: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type SortSelect = ReturnType<typeof SortSelect>;
|
|
8
|
+
export default SortSelect;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { EditDescriptor } from '../../edit/types.js';
|
|
3
|
+
|
|
4
|
+
/** Which of the card's localized fields are editable at this render site. */
|
|
5
|
+
export interface WeeklyEditMap {
|
|
6
|
+
title?: EditDescriptor;
|
|
7
|
+
excerpt?: EditDescriptor;
|
|
8
|
+
}
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { getUiConfig } from '../../config/context.js';
|
|
13
|
+
import type { WeeklyCardData } from '../../content/types.js';
|
|
14
|
+
import { getEditAdapter } from '../../edit/context.js';
|
|
15
|
+
import Editable from '../../edit/Editable.svelte';
|
|
16
|
+
import CardMedia from '../ui/CardMedia.svelte';
|
|
17
|
+
import CardTitle from '../ui/CardTitle.svelte';
|
|
18
|
+
import DateText from '../ui/DateText.svelte';
|
|
19
|
+
import Link from '../ui/Link.svelte';
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
weekly: WeeklyCardData;
|
|
23
|
+
/** Marks fields editable where an edit adapter is active. */
|
|
24
|
+
edit?: WeeklyEditMap;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let { weekly, edit }: Props = $props();
|
|
28
|
+
|
|
29
|
+
const config = getUiConfig();
|
|
30
|
+
const adapter = getEditAdapter();
|
|
31
|
+
|
|
32
|
+
// While the title is being edited it renders as plain text, not a link:
|
|
33
|
+
// a contenteditable inside an anchor still navigates on click, which
|
|
34
|
+
// would throw the editor off the page mid-draft.
|
|
35
|
+
const titleEditing = $derived(edit?.title !== undefined && (adapter?.isEditing ?? false));
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<article>
|
|
39
|
+
<header>
|
|
40
|
+
<span class="number">{config.messages.weeklie_number({ number: weekly.number })}</span>
|
|
41
|
+
<DateText value={weekly.publishedOn} />
|
|
42
|
+
</header>
|
|
43
|
+
<CardMedia src={weekly.imageUrl} alt="" ratio="1 / 1" width="600" height="600" />
|
|
44
|
+
<CardTitle>
|
|
45
|
+
{#if titleEditing}
|
|
46
|
+
<Editable edit={edit?.title} value={weekly.title}>
|
|
47
|
+
{#snippet children(text, attrs)}<span {...attrs}>{text}</span>{/snippet}
|
|
48
|
+
</Editable>
|
|
49
|
+
{:else}
|
|
50
|
+
<Link href={`/weeklies/${weekly.slug}`}>{weekly.title}</Link>
|
|
51
|
+
{/if}
|
|
52
|
+
</CardTitle>
|
|
53
|
+
<Editable edit={edit?.excerpt} value={weekly.excerpt}>
|
|
54
|
+
{#snippet children(text, attrs)}<p {...attrs}>{text}</p>{/snippet}
|
|
55
|
+
</Editable>
|
|
56
|
+
</article>
|
|
57
|
+
|
|
58
|
+
<style>
|
|
59
|
+
article {
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: column;
|
|
62
|
+
gap: var(--space-2);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
header {
|
|
66
|
+
display: flex;
|
|
67
|
+
justify-content: space-between;
|
|
68
|
+
align-items: baseline;
|
|
69
|
+
font-size: var(--text-sm);
|
|
70
|
+
color: var(--color-ink-secondary);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.number {
|
|
74
|
+
font-weight: 700;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
p {
|
|
78
|
+
margin: 0;
|
|
79
|
+
color: var(--color-ink-secondary);
|
|
80
|
+
font-size: var(--text-sm);
|
|
81
|
+
}
|
|
82
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EditDescriptor } from '../../edit/types.js';
|
|
2
|
+
/** Which of the card's localized fields are editable at this render site. */
|
|
3
|
+
export interface WeeklyEditMap {
|
|
4
|
+
title?: EditDescriptor;
|
|
5
|
+
excerpt?: EditDescriptor;
|
|
6
|
+
}
|
|
7
|
+
import type { WeeklyCardData } from '../../content/types.js';
|
|
8
|
+
interface Props {
|
|
9
|
+
weekly: WeeklyCardData;
|
|
10
|
+
/** Marks fields editable where an edit adapter is active. */
|
|
11
|
+
edit?: WeeklyEditMap;
|
|
12
|
+
}
|
|
13
|
+
declare const WeeklieCard: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type WeeklieCard = ReturnType<typeof WeeklieCard>;
|
|
15
|
+
export default WeeklieCard;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { setUiConfig } from './context.js';
|
|
4
|
+
import type { UiConfigInput } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mounts the host app's UiConfig for everything beneath it — typically
|
|
8
|
+
* once, in the root layout. The config prop is read lazily on every
|
|
9
|
+
* access, so reactive inputs (current locale, current URL) stay live.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
config: UiConfigInput;
|
|
13
|
+
children: Snippet;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { config, children }: Props = $props();
|
|
17
|
+
|
|
18
|
+
setUiConfig(() => config);
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
{@render children()}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { UiConfigInput } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Mounts the host app's UiConfig for everything beneath it — typically
|
|
5
|
+
* once, in the root layout. The config prop is read lazily on every
|
|
6
|
+
* access, so reactive inputs (current locale, current URL) stay live.
|
|
7
|
+
*/
|
|
8
|
+
interface Props {
|
|
9
|
+
config: UiConfigInput;
|
|
10
|
+
children: Snippet;
|
|
11
|
+
}
|
|
12
|
+
declare const UiProvider: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type UiProvider = ReturnType<typeof UiProvider>;
|
|
14
|
+
export default UiProvider;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type UiConfig, type UiConfigInput } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The provider-less config: identity hrefs, Catalan, no router. Complete on
|
|
4
|
+
* purpose — every component must render without a provider, which is what
|
|
5
|
+
* keeps stories and component tests free of app wiring.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_UI_CONFIG: UiConfig;
|
|
8
|
+
/**
|
|
9
|
+
* Installs the app's config for the subtree. Takes a *thunk* so a provider
|
|
10
|
+
* whose input is a reactive prop stays live: the resolved object delegates
|
|
11
|
+
* every read back through `input()`, and Svelte's reactivity flows through
|
|
12
|
+
* the getters. Called by UiProvider; call it directly only from a layout-like
|
|
13
|
+
* component's init.
|
|
14
|
+
*/
|
|
15
|
+
export declare function setUiConfig(input: () => UiConfigInput): UiConfig;
|
|
16
|
+
/** The nearest provider's config, or the standalone defaults. */
|
|
17
|
+
export declare function getUiConfig(): UiConfig;
|