@vit-foundation/ui 0.24.2 → 0.25.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.
Files changed (32) hide show
  1. package/README.md +11 -11
  2. package/dist/components/pages/GetInvolvedPage.svelte +139 -0
  3. package/dist/components/pages/GetInvolvedPage.svelte.d.ts +36 -0
  4. package/dist/components/pages/HomePage.svelte +256 -0
  5. package/dist/components/pages/HomePage.svelte.d.ts +36 -0
  6. package/dist/components/pages/LegalPage.svelte +39 -0
  7. package/dist/components/pages/LegalPage.svelte.d.ts +14 -0
  8. package/dist/components/pages/ProjectPage.svelte +128 -0
  9. package/dist/components/pages/ProjectPage.svelte.d.ts +17 -0
  10. package/dist/components/pages/TransparencyPage.svelte +164 -0
  11. package/dist/components/pages/TransparencyPage.svelte.d.ts +40 -0
  12. package/dist/components/pages/WeekliesPage.svelte +184 -0
  13. package/dist/components/pages/WeekliesPage.svelte.d.ts +46 -0
  14. package/dist/components/pages/WeeklyPage.svelte +203 -0
  15. package/dist/components/pages/WeeklyPage.svelte.d.ts +35 -0
  16. package/dist/components/pages/WhatWeDoPage.svelte +107 -0
  17. package/dist/components/pages/WhatWeDoPage.svelte.d.ts +27 -0
  18. package/dist/components/pages/WhoWeArePage.svelte +108 -0
  19. package/dist/components/pages/WhoWeArePage.svelte.d.ts +32 -0
  20. package/dist/config/messages.js +22 -0
  21. package/dist/config/types.d.ts +36 -0
  22. package/dist/content/index.d.ts +3 -1
  23. package/dist/content/index.js +1 -0
  24. package/dist/content/pages.d.ts +64 -0
  25. package/dist/content/pages.js +71 -0
  26. package/dist/content/types.d.ts +45 -0
  27. package/dist/content/types.js +0 -5
  28. package/dist/content-components.d.ts +18 -0
  29. package/dist/content-components.js +12 -0
  30. package/dist/contract.d.ts +3 -0
  31. package/dist/contract.js +4 -0
  32. package/package.json +1 -1
@@ -0,0 +1,128 @@
1
+ <script module lang="ts">
2
+ import type { ArticleEdit } from '../../content/types.js';
3
+
4
+ /** What a CMS may open on a project's own page: title, excerpt, and the body as rich text. */
5
+ export type ProjectPageEdit = ArticleEdit;
6
+ </script>
7
+
8
+ <script lang="ts">
9
+ import { getUiConfig } from '../../config/context.js';
10
+ import type { ProjectArticleData } from '../../content/types.js';
11
+ import ActionLabel from '../../edit/ActionLabel.svelte';
12
+ import Editable from '../../edit/Editable.svelte';
13
+ import PageShell from '../layout/PageShell.svelte';
14
+ import DateText from '../ui/DateText.svelte';
15
+ import Link from '../ui/Link.svelte';
16
+ import RichText from '../ui/RichText.svelte';
17
+ import ShareRow from '../ui/ShareRow.svelte';
18
+
19
+ /**
20
+ * A project's story: date, title, excerpt, hero image, the rich-text body
21
+ * with a share row, and — when the project points somewhere — a preview
22
+ * card of the external destination. Whether a project HAS a page is the
23
+ * host's load's decision (a body-less project 404s there).
24
+ */
25
+ interface Props {
26
+ project: ProjectArticleData;
27
+ edit?: ProjectPageEdit;
28
+ }
29
+
30
+ let { project, edit }: Props = $props();
31
+
32
+ const config = getUiConfig();
33
+ const msg = $derived(config.messages);
34
+ </script>
35
+
36
+ <PageShell title={project.title} description={project.excerpt} variant="article">
37
+ <p class="back">
38
+ <ActionLabel edit={config.messageEdit?.('back_label')} value={msg.back_label()}>
39
+ {#snippet control()}
40
+ <Link href="/what-we-do">← {msg.back_label()}</Link>
41
+ {/snippet}
42
+ </ActionLabel>
43
+ </p>
44
+
45
+ <header>
46
+ <DateText value={project.publishedOn} />
47
+ <Editable edit={edit?.title} value={project.title}>
48
+ {#snippet children(text, attrs)}<h1 {...attrs}>{text}</h1>{/snippet}
49
+ </Editable>
50
+ <Editable edit={edit?.excerpt} value={project.excerpt}>
51
+ {#snippet children(text, attrs)}<p class="excerpt" {...attrs}>{text}</p>{/snippet}
52
+ </Editable>
53
+ </header>
54
+
55
+ <img class="hero" src={project.imageUrl} alt="" width="1200" height="675" />
56
+
57
+ <div class="layout">
58
+ <div class="body">
59
+ {#if project.body}
60
+ <RichText body={project.body} edit={edit?.body} />
61
+ {/if}
62
+ <ShareRow title={project.title} />
63
+ </div>
64
+
65
+ {#if project.externalUrl}
66
+ <aside>
67
+ <a class="preview" href={project.externalUrl} rel="external noopener">
68
+ {#if project.previewImageUrl}
69
+ <img src={project.previewImageUrl} alt="" width="900" height="300" />
70
+ {/if}
71
+ <span>{project.externalUrl.replace(/^https?:\/\//, '')} ↗</span>
72
+ </a>
73
+ </aside>
74
+ {/if}
75
+ </div>
76
+ </PageShell>
77
+
78
+ <style>
79
+ .back :global(a) {
80
+ text-decoration: none;
81
+ color: var(--color-ink-secondary);
82
+ }
83
+
84
+ h1 {
85
+ margin: var(--space-2) 0;
86
+ }
87
+
88
+ .excerpt {
89
+ font-size: var(--text-lg);
90
+ color: var(--color-ink-secondary);
91
+ max-width: 60ch;
92
+ }
93
+
94
+ .hero {
95
+ border-radius: var(--radius-lg);
96
+ margin-block: var(--space-4);
97
+ background: var(--color-band-grey);
98
+ }
99
+
100
+ .layout {
101
+ display: grid;
102
+ grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
103
+ gap: var(--space-5);
104
+ align-items: start;
105
+ }
106
+
107
+ .preview {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: var(--space-2);
111
+ border: 1px solid var(--color-hairline);
112
+ border-radius: var(--radius);
113
+ padding: var(--space-3);
114
+ text-decoration: none;
115
+ color: var(--color-brand);
116
+ font-weight: 600;
117
+ }
118
+
119
+ .preview img {
120
+ border-radius: var(--radius);
121
+ }
122
+
123
+ @media (max-width: 720px) {
124
+ .layout {
125
+ grid-template-columns: 1fr;
126
+ }
127
+ }
128
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { ArticleEdit } from '../../content/types.js';
2
+ /** What a CMS may open on a project's own page: title, excerpt, and the body as rich text. */
3
+ export type ProjectPageEdit = ArticleEdit;
4
+ import type { ProjectArticleData } from '../../content/types.js';
5
+ /**
6
+ * A project's story: date, title, excerpt, hero image, the rich-text body
7
+ * with a share row, and — when the project points somewhere — a preview
8
+ * card of the external destination. Whether a project HAS a page is the
9
+ * host's load's decision (a body-less project 404s there).
10
+ */
11
+ interface Props {
12
+ project: ProjectArticleData;
13
+ edit?: ProjectPageEdit;
14
+ }
15
+ declare const ProjectPage: import("svelte").Component<Props, {}, "">;
16
+ type ProjectPage = ReturnType<typeof ProjectPage>;
17
+ export default ProjectPage;
@@ -0,0 +1,164 @@
1
+ <script module lang="ts">
2
+ import type { CopyEditFor } from '../../content/pages.js';
3
+ import type { MilestoneData } from '../../content/types.js';
4
+ import type { CollectionRef } from '../../edit/types.js';
5
+ import type { MilestoneEditMap } from '../timeline/TimelineMilestone.svelte';
6
+
7
+ /**
8
+ * What a CMS may open on /transparency. `milestones` names the timeline's
9
+ * collection (add slots, removes); the category chips edit through
10
+ * `config.messageEdit` over their own `category_*` keys, and the search
11
+ * placeholder through the box's panel — neither needs a member here.
12
+ */
13
+ export interface TransparencyPageEdit {
14
+ copy?: CopyEditFor<'transparency'>;
15
+ milestoneFor?: (milestone: MilestoneData) => MilestoneEditMap | undefined;
16
+ milestones?: CollectionRef;
17
+ }
18
+ </script>
19
+
20
+ <script lang="ts">
21
+ import { getUiConfig } from '../../config/context.js';
22
+ import type { PageCopy } from '../../content/pages.js';
23
+ import { MILESTONE_CATEGORIES, type MilestoneCategory } from '../../content/types.js';
24
+ import Editable from '../../edit/Editable.svelte';
25
+ import { chromeProperty } from '../../edit/helpers.js';
26
+ import { matchesMilestoneFilter, milestoneCategoryLabel } from '../../utils/milestones.js';
27
+ import { createUrlFilters } from '../../utils/url-filters.svelte.js';
28
+ import PageShell from '../layout/PageShell.svelte';
29
+ import Timeline from '../timeline/Timeline.svelte';
30
+ import CopyIntro from '../ui/CopyIntro.svelte';
31
+ import FilterChips from '../ui/FilterChips.svelte';
32
+ import SearchInput from '../ui/SearchInput.svelte';
33
+
34
+ /** The two filters, as they ride the URL. An alias: createUrlFilters needs an index signature. */
35
+ type TimelineFilters = { q: string; category: MilestoneCategory | null };
36
+
37
+ /**
38
+ * The full milestone history with a search box and category chips. The
39
+ * whole timeline is already loaded, so filtering stays in the browser
40
+ * (`matchesMilestoneFilter`); only the URL is mirrored, through the
41
+ * host's shallow-routing write — the package has no router.
42
+ */
43
+ interface Props {
44
+ content: PageCopy<'transparency'>;
45
+ milestones: MilestoneData[];
46
+ /** The server-parsed filters of a deep link; invalid values already fell back. */
47
+ query: TimelineFilters;
48
+ /** Shallow-routing URL write — see UrlFiltersConfig.replaceUrl. */
49
+ replaceUrl: (path: string) => void;
50
+ edit?: TransparencyPageEdit;
51
+ }
52
+
53
+ let { content, milestones, query, replaceUrl, edit }: Props = $props();
54
+
55
+ const config = getUiConfig();
56
+ const msg = $derived(config.messages);
57
+
58
+ const filters = createUrlFilters<TimelineFilters>({
59
+ path: '/transparency',
60
+ initial: () => ({ q: query.q, category: query.category }),
61
+ toQuery: (values) => ({ q: values.q, category: values.category }),
62
+ replaceUrl: (path) => replaceUrl(path)
63
+ });
64
+
65
+ // $derived, not a const: the labels resolve to STRINGS through `msg`, and
66
+ // a CMS switching its editing locale (or saving a wording) repaints every
67
+ // other string on the page — the chips must follow.
68
+ const chips = $derived(
69
+ MILESTONE_CATEGORIES.map((value) => ({ value, label: milestoneCategoryLabel(value, msg) }))
70
+ );
71
+
72
+ const filtered = $derived(
73
+ milestones.filter((milestone) => matchesMilestoneFilter(milestone, filters.values))
74
+ );
75
+
76
+ const isCategory = (value: string | null): value is MilestoneCategory =>
77
+ value !== null && (MILESTONE_CATEGORIES as readonly string[]).includes(value);
78
+
79
+ function handleSearch(value: string): void {
80
+ filters.update({ q: value });
81
+ }
82
+
83
+ function handleCategory(value: string | null): void {
84
+ filters.update({ category: isCategory(value) ? value : null });
85
+ }
86
+
87
+ /** A chip's wording is a catalog key — `category_lab` for `lab`. */
88
+ const categoryKey = (category: MilestoneCategory) => `category_${category}` as const;
89
+
90
+ const placeholderProperty = $derived(
91
+ config.messageEdit
92
+ ? chromeProperty('timeline_searchPlaceholder', {
93
+ type: 'text',
94
+ locale: config.locale(),
95
+ label: msg.timeline_searchPlaceholder()
96
+ })
97
+ : undefined
98
+ );
99
+ </script>
100
+
101
+ <PageShell title={content.heading} variant="wide">
102
+ <header>
103
+ <Editable edit={edit?.copy?.('heading')} value={content.heading}>
104
+ {#snippet children(text, attrs)}<h1 {...attrs}>{text}</h1>{/snippet}
105
+ </Editable>
106
+ <CopyIntro text={content.intro} edit={edit?.copy?.('intro')} />
107
+ </header>
108
+
109
+ <div class="controls">
110
+ <SearchInput
111
+ value={filters.values.q}
112
+ placeholder={msg.timeline_searchPlaceholder()}
113
+ label={msg.timeline_searchPlaceholder()}
114
+ onsearch={handleSearch}
115
+ placeholderEdit={placeholderProperty}
116
+ />
117
+ <FilterChips
118
+ {chips}
119
+ selected={filters.values.category}
120
+ label={msg.timeline_filterLabel()}
121
+ onchange={handleCategory}
122
+ editFor={(chip) =>
123
+ isCategory(chip.value) ? config.messageEdit?.(categoryKey(chip.value)) : undefined}
124
+ />
125
+ </div>
126
+
127
+ <section aria-live="polite" aria-label={msg.timeline_label()}>
128
+ {#if filtered.length === 0}
129
+ <Editable edit={config.messageEdit?.('timeline_empty')} value={msg.timeline_empty()}>
130
+ {#snippet children(text, attrs)}<p class="empty" {...attrs}>{text}</p>{/snippet}
131
+ </Editable>
132
+ {:else}
133
+ <Timeline
134
+ milestones={filtered}
135
+ variant="full"
136
+ editFor={edit?.milestoneFor}
137
+ collection={edit?.milestones}
138
+ />
139
+ {/if}
140
+ </section>
141
+ </PageShell>
142
+
143
+ <style>
144
+ header,
145
+ .controls {
146
+ max-width: var(--content-max);
147
+ margin-inline: auto;
148
+ }
149
+
150
+ .controls {
151
+ display: flex;
152
+ flex-wrap: wrap;
153
+ gap: var(--space-4);
154
+ align-items: center;
155
+ margin-block: var(--space-4);
156
+ }
157
+
158
+ .empty {
159
+ max-width: var(--content-max);
160
+ margin-inline: auto;
161
+ color: var(--color-ink-secondary);
162
+ padding-block: var(--space-5);
163
+ }
164
+ </style>
@@ -0,0 +1,40 @@
1
+ import type { CopyEditFor } from '../../content/pages.js';
2
+ import type { MilestoneData } from '../../content/types.js';
3
+ import type { CollectionRef } from '../../edit/types.js';
4
+ import type { MilestoneEditMap } from '../timeline/TimelineMilestone.svelte';
5
+ /**
6
+ * What a CMS may open on /transparency. `milestones` names the timeline's
7
+ * collection (add slots, removes); the category chips edit through
8
+ * `config.messageEdit` over their own `category_*` keys, and the search
9
+ * placeholder through the box's panel — neither needs a member here.
10
+ */
11
+ export interface TransparencyPageEdit {
12
+ copy?: CopyEditFor<'transparency'>;
13
+ milestoneFor?: (milestone: MilestoneData) => MilestoneEditMap | undefined;
14
+ milestones?: CollectionRef;
15
+ }
16
+ import type { PageCopy } from '../../content/pages.js';
17
+ import { type MilestoneCategory } from '../../content/types.js';
18
+ /** The two filters, as they ride the URL. An alias: createUrlFilters needs an index signature. */
19
+ type TimelineFilters = {
20
+ q: string;
21
+ category: MilestoneCategory | null;
22
+ };
23
+ /**
24
+ * The full milestone history with a search box and category chips. The
25
+ * whole timeline is already loaded, so filtering stays in the browser
26
+ * (`matchesMilestoneFilter`); only the URL is mirrored, through the
27
+ * host's shallow-routing write — the package has no router.
28
+ */
29
+ interface Props {
30
+ content: PageCopy<'transparency'>;
31
+ milestones: MilestoneData[];
32
+ /** The server-parsed filters of a deep link; invalid values already fell back. */
33
+ query: TimelineFilters;
34
+ /** Shallow-routing URL write — see UrlFiltersConfig.replaceUrl. */
35
+ replaceUrl: (path: string) => void;
36
+ edit?: TransparencyPageEdit;
37
+ }
38
+ declare const TransparencyPage: import("svelte").Component<Props, {}, "">;
39
+ type TransparencyPage = ReturnType<typeof TransparencyPage>;
40
+ export default TransparencyPage;
@@ -0,0 +1,184 @@
1
+ <script module lang="ts">
2
+ import type { CopyEditFor } from '../../content/pages.js';
3
+ import type { ThemeData, WeeklyCardData } from '../../content/types.js';
4
+ import type { EditDescriptor } from '../../edit/types.js';
5
+ import type { WeeklyEditMap } from '../weeklies/WeeklieCard.svelte';
6
+
7
+ /**
8
+ * What a CMS may open on /weeklies. A theme chip's wording is a THEME's
9
+ * localized name — an entity, unlike the category chips — so the host
10
+ * answers `themeFor` per theme. The search placeholder and the sort
11
+ * options edit through their panels over `config.messageEdit`; the h1
12
+ * and the empty state inline over the same gate.
13
+ */
14
+ export interface WeekliesPageEdit {
15
+ copy?: CopyEditFor<'weeklies'>;
16
+ weeklyFor?: (weekly: WeeklyCardData) => WeeklyEditMap | undefined;
17
+ themeFor?: (theme: ThemeData) => EditDescriptor | undefined;
18
+ }
19
+ </script>
20
+
21
+ <script lang="ts">
22
+ import { getUiConfig } from '../../config/context.js';
23
+ import type { PageCopy } from '../../content/pages.js';
24
+ import type { SortDirection } from '../../content/types.js';
25
+ import Editable from '../../edit/Editable.svelte';
26
+ import { chromeProperty } from '../../edit/helpers.js';
27
+ import type { WeeklyListServerData } from '../../utils/weekly-list-contract.js';
28
+ import { createWeeklyList, type WeeklyListConfig } from '../../utils/weekly-list.svelte.js';
29
+ import PageShell from '../layout/PageShell.svelte';
30
+ import CopyIntro from '../ui/CopyIntro.svelte';
31
+ import FilterChips from '../ui/FilterChips.svelte';
32
+ import Pagination from '../ui/Pagination.svelte';
33
+ import SearchInput from '../ui/SearchInput.svelte';
34
+ import SortSelect from '../weeklies/SortSelect.svelte';
35
+ import WeeklieCard from '../weeklies/WeeklieCard.svelte';
36
+
37
+ /**
38
+ * The weeklies index: theme chips, search, sort, the grid, paging.
39
+ * Filters, paging, the refetch and the server/client reconciliation are
40
+ * `createWeeklyList`'s; this renders what it reports. The three things
41
+ * the package deliberately lacks — a backend, an i18n runtime and a
42
+ * router — are the host's `fetchPage`, the provider's locale, and
43
+ * `replaceUrl`.
44
+ *
45
+ * The paging links go to `Pagination` UNWRAPPED: it renders them through
46
+ * `Link`, which resolves every internal path through `UiConfig.href`, so
47
+ * a host that re-roots the site (vit-brain's `mirrorHref`) must NOT wrap
48
+ * `hrefFor` itself — that prefixed the path twice.
49
+ */
50
+ interface Props {
51
+ content: PageCopy<'weeklies'>;
52
+ themes: ThemeData[];
53
+ /** The server-rendered page — the website's route `data` satisfies it as is. */
54
+ server: WeeklyListServerData;
55
+ /** The host's remote query for the first page of a changed filter. */
56
+ fetchPage: WeeklyListConfig['fetchPage'];
57
+ /** Shallow-routing URL write — see UrlFiltersConfig.replaceUrl. */
58
+ replaceUrl: (path: string) => void;
59
+ edit?: WeekliesPageEdit;
60
+ }
61
+
62
+ let { content, themes, server, fetchPage, replaceUrl, edit }: Props = $props();
63
+
64
+ const config = getUiConfig();
65
+ const msg = $derived(config.messages);
66
+
67
+ const list = createWeeklyList({
68
+ server: () => server,
69
+ fetchPage: (input) => fetchPage(input),
70
+ locale: () => config.locale(),
71
+ replaceUrl: (path) => replaceUrl(path)
72
+ });
73
+
74
+ const chips = $derived(themes.map((theme) => ({ value: theme.slug, label: theme.name })));
75
+ const themeEdit = (chip: { value: string }) => {
76
+ const theme = themes.find((candidate) => candidate.slug === chip.value);
77
+ return theme ? edit?.themeFor?.(theme) : undefined;
78
+ };
79
+
80
+ // Wording that cannot hold a caret — the placeholder, the two <option>
81
+ // labels — edits through a panel, only where the host exposes `messageEdit`.
82
+ const placeholderProperty = $derived(
83
+ config.messageEdit
84
+ ? chromeProperty('weeklies_searchPlaceholder', {
85
+ type: 'text',
86
+ locale: config.locale(),
87
+ label: msg.weeklies_searchPlaceholder()
88
+ })
89
+ : undefined
90
+ );
91
+ const sortOptionProperty = (direction: SortDirection) =>
92
+ config.messageEdit
93
+ ? chromeProperty(direction === 'desc' ? 'weeklies_sortDesc' : 'weeklies_sortAsc', {
94
+ type: 'text',
95
+ locale: config.locale(),
96
+ label: direction === 'desc' ? msg.weeklies_sortDesc() : msg.weeklies_sortAsc()
97
+ })
98
+ : undefined;
99
+ </script>
100
+
101
+ <PageShell title={msg.nav_weeklies()}>
102
+ <header>
103
+ <Editable edit={config.messageEdit?.('nav_weeklies')} value={msg.nav_weeklies()}>
104
+ {#snippet children(text, attrs)}<h1 {...attrs}>{text}</h1>{/snippet}
105
+ </Editable>
106
+ <CopyIntro text={content.intro} edit={edit?.copy?.('intro')} />
107
+ </header>
108
+
109
+ <div class="controls">
110
+ <FilterChips
111
+ {chips}
112
+ selected={list.filters.theme}
113
+ label={msg.weeklies_filterLabel()}
114
+ onchange={(value) => list.update({ theme: value })}
115
+ editFor={themeEdit}
116
+ />
117
+ <SearchInput
118
+ value={list.filters.q}
119
+ placeholder={msg.weeklies_searchPlaceholder()}
120
+ label={msg.weeklies_searchPlaceholder()}
121
+ onsearch={(q) => list.update({ q })}
122
+ placeholderEdit={placeholderProperty}
123
+ />
124
+ <SortSelect
125
+ value={list.filters.sort}
126
+ onchange={(sort) => list.update({ sort })}
127
+ optionsEdit={sortOptionProperty}
128
+ />
129
+ </div>
130
+
131
+ <section
132
+ aria-live="polite"
133
+ aria-busy={list.isLoading}
134
+ aria-label={msg.weeklies_searchResultsLabel()}
135
+ >
136
+ {#if list.loadError}
137
+ <p class="load-error" role="alert">{msg.weeklies_loadError()}</p>
138
+ {/if}
139
+ {#if list.items.length === 0}
140
+ <Editable edit={config.messageEdit?.('weeklies_empty')} value={msg.weeklies_empty()}>
141
+ {#snippet children(text, attrs)}<p class="empty" {...attrs}>{text}</p>{/snippet}
142
+ </Editable>
143
+ {:else}
144
+ <div class="grid">
145
+ {#each list.items as weekly (weekly.slug)}
146
+ <WeeklieCard {weekly} edit={edit?.weeklyFor?.(weekly)} />
147
+ {/each}
148
+ </div>
149
+ {/if}
150
+ </section>
151
+
152
+ <Pagination
153
+ page={list.page}
154
+ total={list.total}
155
+ pageSize={server.pageSize}
156
+ href={(page) => list.hrefFor(page)}
157
+ />
158
+ </PageShell>
159
+
160
+ <style>
161
+ .controls {
162
+ display: flex;
163
+ flex-wrap: wrap;
164
+ align-items: center;
165
+ gap: var(--space-4);
166
+ margin-block: var(--space-4);
167
+ }
168
+
169
+ .grid {
170
+ display: grid;
171
+ grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
172
+ gap: var(--space-5) var(--space-4);
173
+ }
174
+
175
+ .empty {
176
+ color: var(--color-ink-secondary);
177
+ padding-block: var(--space-5);
178
+ }
179
+
180
+ .load-error {
181
+ color: var(--series-8);
182
+ font-weight: 600;
183
+ }
184
+ </style>
@@ -0,0 +1,46 @@
1
+ import type { CopyEditFor } from '../../content/pages.js';
2
+ import type { ThemeData, WeeklyCardData } from '../../content/types.js';
3
+ import type { EditDescriptor } from '../../edit/types.js';
4
+ import type { WeeklyEditMap } from '../weeklies/WeeklieCard.svelte';
5
+ /**
6
+ * What a CMS may open on /weeklies. A theme chip's wording is a THEME's
7
+ * localized name — an entity, unlike the category chips — so the host
8
+ * answers `themeFor` per theme. The search placeholder and the sort
9
+ * options edit through their panels over `config.messageEdit`; the h1
10
+ * and the empty state inline over the same gate.
11
+ */
12
+ export interface WeekliesPageEdit {
13
+ copy?: CopyEditFor<'weeklies'>;
14
+ weeklyFor?: (weekly: WeeklyCardData) => WeeklyEditMap | undefined;
15
+ themeFor?: (theme: ThemeData) => EditDescriptor | undefined;
16
+ }
17
+ import type { PageCopy } from '../../content/pages.js';
18
+ import type { WeeklyListServerData } from '../../utils/weekly-list-contract.js';
19
+ import { type WeeklyListConfig } from '../../utils/weekly-list.svelte.js';
20
+ /**
21
+ * The weeklies index: theme chips, search, sort, the grid, paging.
22
+ * Filters, paging, the refetch and the server/client reconciliation are
23
+ * `createWeeklyList`'s; this renders what it reports. The three things
24
+ * the package deliberately lacks — a backend, an i18n runtime and a
25
+ * router — are the host's `fetchPage`, the provider's locale, and
26
+ * `replaceUrl`.
27
+ *
28
+ * The paging links go to `Pagination` UNWRAPPED: it renders them through
29
+ * `Link`, which resolves every internal path through `UiConfig.href`, so
30
+ * a host that re-roots the site (vit-brain's `mirrorHref`) must NOT wrap
31
+ * `hrefFor` itself — that prefixed the path twice.
32
+ */
33
+ interface Props {
34
+ content: PageCopy<'weeklies'>;
35
+ themes: ThemeData[];
36
+ /** The server-rendered page — the website's route `data` satisfies it as is. */
37
+ server: WeeklyListServerData;
38
+ /** The host's remote query for the first page of a changed filter. */
39
+ fetchPage: WeeklyListConfig['fetchPage'];
40
+ /** Shallow-routing URL write — see UrlFiltersConfig.replaceUrl. */
41
+ replaceUrl: (path: string) => void;
42
+ edit?: WeekliesPageEdit;
43
+ }
44
+ declare const WeekliesPage: import("svelte").Component<Props, {}, "">;
45
+ type WeekliesPage = ReturnType<typeof WeekliesPage>;
46
+ export default WeekliesPage;