@edoxen/browser 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.
Files changed (69) hide show
  1. package/README.md +45 -0
  2. package/dist/cli.js +4475 -0
  3. package/package.json +82 -0
  4. package/src/astro/components/ActionTypeBadge.astro +28 -0
  5. package/src/astro/components/AgendaTable.astro +49 -0
  6. package/src/astro/components/BodyBadge.astro +28 -0
  7. package/src/astro/components/CommitteeCard.astro +58 -0
  8. package/src/astro/components/CountryFlag.astro +34 -0
  9. package/src/astro/components/DecadeTimeline.astro +52 -0
  10. package/src/astro/components/DecisionList.astro +66 -0
  11. package/src/astro/components/EmptyState.astro +19 -0
  12. package/src/astro/components/LocaleTabs.astro +44 -0
  13. package/src/astro/components/MeetingList.astro +63 -0
  14. package/src/astro/components/MinutesSection.astro +59 -0
  15. package/src/astro/components/OfficerList.astro +49 -0
  16. package/src/astro/components/PageHero.astro +42 -0
  17. package/src/astro/components/PrevNextNav.astro +62 -0
  18. package/src/astro/components/ReferenceDocuments.astro +38 -0
  19. package/src/astro/components/UrnBar.astro +49 -0
  20. package/src/astro/layouts/BaseLayout.astro +79 -0
  21. package/src/astro/layouts/DecisionLayout.astro +21 -0
  22. package/src/astro/layouts/MeetingLayout.astro +21 -0
  23. package/src/astro/pages/404.astro +10 -0
  24. package/src/astro/pages/[lang]/about.astro +65 -0
  25. package/src/astro/pages/[lang]/decisions/[urn].astro +126 -0
  26. package/src/astro/pages/[lang]/decisions/index.astro +27 -0
  27. package/src/astro/pages/[lang]/index.astro +50 -0
  28. package/src/astro/pages/[lang]/meetings/[urn].astro +114 -0
  29. package/src/astro/pages/[lang]/meetings/index.astro +27 -0
  30. package/src/astro/pages/about.astro +65 -0
  31. package/src/astro/pages/decisions/[urn].astro +122 -0
  32. package/src/astro/pages/decisions/index.astro +67 -0
  33. package/src/astro/pages/index.astro +50 -0
  34. package/src/astro/pages/meetings/[urn].astro +110 -0
  35. package/src/astro/pages/meetings/index.astro +20 -0
  36. package/src/cli/index.ts +195 -0
  37. package/src/config/index.ts +33 -0
  38. package/src/config/paths.spec.ts +59 -0
  39. package/src/config/paths.ts +26 -0
  40. package/src/config/schema.spec.ts +263 -0
  41. package/src/config/schema.ts +132 -0
  42. package/src/config/tokens.spec.ts +57 -0
  43. package/src/config/tokens.ts +48 -0
  44. package/src/data/index.ts +36 -0
  45. package/src/data/lint.spec.ts +79 -0
  46. package/src/data/lint.ts +77 -0
  47. package/src/data/load.spec.ts +58 -0
  48. package/src/data/load.ts +50 -0
  49. package/src/data/prepare.spec.ts +137 -0
  50. package/src/data/prepare.ts +199 -0
  51. package/src/data/project.ts +11 -0
  52. package/src/data/validate.ts +38 -0
  53. package/src/errors.ts +25 -0
  54. package/src/i18n/index.spec.ts +121 -0
  55. package/src/i18n/index.ts +90 -0
  56. package/src/index.ts +74 -0
  57. package/src/integration.ts +233 -0
  58. package/src/islands/decade-scroller.ts +18 -0
  59. package/src/islands/print-button.ts +12 -0
  60. package/src/islands/search-filter-core.spec.ts +84 -0
  61. package/src/islands/search-filter-core.ts +89 -0
  62. package/src/islands/search-filter.ts +166 -0
  63. package/src/islands/section-tabs.ts +35 -0
  64. package/src/islands/theme-toggle.ts +33 -0
  65. package/src/islands/urn-copy.ts +29 -0
  66. package/src/seo/index.spec.ts +76 -0
  67. package/src/seo/index.ts +85 -0
  68. package/src/virtual.d.ts +11 -0
  69. package/styles/base.css +117 -0
@@ -0,0 +1,62 @@
1
+ ---
2
+ interface Props {
3
+ prev?: { urn: string; title: string }
4
+ next?: { urn: string; title: string }
5
+ basePath: string
6
+ }
7
+ const { prev, next, basePath } = Astro.props
8
+ ---
9
+
10
+ {(prev || next) && (
11
+ <nav class="edoxen-prev-next" aria-label="Adjacent items">
12
+ <div class="edoxen-prev-next__cell">
13
+ {prev && (
14
+ <a href={`${basePath}/${encodeURIComponent(prev.urn)}`} rel="prev">
15
+ <span class="edoxen-prev-next__label">← Previous</span>
16
+ <span class="edoxen-prev-next__title">{prev.title}</span>
17
+ </a>
18
+ )}
19
+ </div>
20
+ <div class="edoxen-prev-next__cell edoxen-prev-next__cell--right">
21
+ {next && (
22
+ <a href={`${basePath}/${encodeURIComponent(next.urn)}`} rel="next">
23
+ <span class="edoxen-prev-next__label">Next →</span>
24
+ <span class="edoxen-prev-next__title">{next.title}</span>
25
+ </a>
26
+ )}
27
+ </div>
28
+ </nav>
29
+ )}
30
+
31
+ <style>
32
+ .edoxen-prev-next {
33
+ display: grid;
34
+ grid-template-columns: 1fr 1fr;
35
+ gap: 1rem;
36
+ margin-top: 2rem;
37
+ padding-top: 1rem;
38
+ border-top: 1px solid var(--edoxen-color-border);
39
+ }
40
+ .edoxen-prev-next__cell--right {
41
+ text-align: right;
42
+ }
43
+ .edoxen-prev-next__cell a {
44
+ display: flex;
45
+ flex-direction: column;
46
+ text-decoration: none;
47
+ padding: 0.5rem 0.75rem;
48
+ border: 1px solid var(--edoxen-color-border);
49
+ border-radius: var(--edoxen-radius-sm);
50
+ background: var(--edoxen-color-surface);
51
+ }
52
+ .edoxen-prev-next__label {
53
+ font-size: 0.75rem;
54
+ color: var(--edoxen-color-muted);
55
+ text-transform: uppercase;
56
+ letter-spacing: 0.05em;
57
+ }
58
+ .edoxen-prev-next__title {
59
+ font-weight: 500;
60
+ color: var(--edoxen-color-text);
61
+ }
62
+ </style>
@@ -0,0 +1,38 @@
1
+ ---
2
+ import type { SourceUrl, Url } from '@edoxen/edoxen'
3
+
4
+ interface Props {
5
+ sources?: readonly SourceUrl[]
6
+ urls?: readonly Url[]
7
+ }
8
+ const { sources, urls } = Astro.props
9
+ const items: { href: string; label: string; kind?: string }[] = []
10
+ for (const s of sources ?? []) {
11
+ items.push({ href: s.ref, label: s.format ?? 'document', kind: s.kind })
12
+ }
13
+ for (const u of urls ?? []) {
14
+ items.push({ href: u.ref, label: u.kind ?? 'document', kind: u.kind })
15
+ }
16
+ ---
17
+
18
+ {items.length > 0 && (
19
+ <ul class="edoxen-reference-docs">
20
+ {items.map((item) => (
21
+ <li>
22
+ <a href={item.href} rel="noopener">
23
+ {item.label}{item.kind && item.kind !== 'document' ? ` (${item.kind})` : ''}
24
+ </a>
25
+ </li>
26
+ ))}
27
+ </ul>
28
+ )}
29
+
30
+ <style>
31
+ .edoxen-reference-docs {
32
+ list-style: square;
33
+ padding-left: 1.5rem;
34
+ }
35
+ .edoxen-reference-docs li {
36
+ margin: 0.25rem 0;
37
+ }
38
+ </style>
@@ -0,0 +1,49 @@
1
+ ---
2
+ interface Props {
3
+ urn: string
4
+ label?: string
5
+ copyable?: boolean
6
+ }
7
+ const { urn, label = 'URN', copyable = true } = Astro.props
8
+ ---
9
+
10
+ <div class="edoxen-urn-bar">
11
+ <span class="edoxen-urn-bar__label">{label}</span>
12
+ <code class="edoxen-urn-bar__value">{urn}</code>
13
+ {copyable && <urn-copy data-text={urn} data-label={label}></urn-copy>}
14
+ </div>
15
+
16
+ <style>
17
+ .edoxen-urn-bar {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ gap: 0.5rem;
21
+ padding: 0.4rem 0.75rem;
22
+ border: 1px solid var(--edoxen-color-border);
23
+ border-radius: var(--edoxen-radius-sm);
24
+ background: var(--edoxen-color-surface);
25
+ font-size: 0.85rem;
26
+ }
27
+ .edoxen-urn-bar__label {
28
+ color: var(--edoxen-color-muted);
29
+ text-transform: uppercase;
30
+ font-size: 0.7rem;
31
+ letter-spacing: 0.05em;
32
+ }
33
+ .edoxen-urn-bar__value {
34
+ font-family: var(--edoxen-font-mono, ui-monospace, monospace);
35
+ }
36
+ .edoxen-urn-bar :global(.edoxen-urn-copy__button) {
37
+ border: 1px solid var(--edoxen-color-border);
38
+ background: var(--edoxen-color-background);
39
+ color: var(--edoxen-color-text);
40
+ padding: 0.15rem 0.5rem;
41
+ border-radius: var(--edoxen-radius-sm);
42
+ font-size: 0.75rem;
43
+ cursor: pointer;
44
+ }
45
+ .edoxen-urn-bar :global(.edoxen-urn-copy__button:hover) {
46
+ background: var(--edoxen-color-accent);
47
+ color: #fff;
48
+ }
49
+ </style>
@@ -0,0 +1,79 @@
1
+ ---
2
+ import type { EdoxenConfig } from '../../config/index.js'
3
+ import { generateCssTokens } from '../../config/index.js'
4
+
5
+ interface Props {
6
+ config: EdoxenConfig
7
+ title?: string
8
+ description?: string
9
+ lang?: string
10
+ }
11
+
12
+ const { config, title, description, lang = config.site.locale } = Astro.props
13
+ const pageTitle = title ?? config.site.title
14
+ const pageDescription = description ?? config.site.description
15
+ const tokens = generateCssTokens(config.theme)
16
+ const canonical = new URL(Astro.url.pathname, config.site.url).toString()
17
+ ---
18
+
19
+ <!doctype html>
20
+ <html lang={lang} data-theme={config.features.darkMode ? 'auto' : 'light'}>
21
+ <head>
22
+ <meta charset="utf-8" />
23
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
24
+ <title>{pageTitle}</title>
25
+ <meta name="description" content={pageDescription} />
26
+ <link rel="canonical" href={canonical} />
27
+ {config.theme.logos.favicon && (
28
+ <link rel="icon" href={config.theme.logos.favicon} />
29
+ )}
30
+ <meta property="og:title" content={pageTitle} />
31
+ <meta property="og:description" content={pageDescription} />
32
+ <meta property="og:url" content={canonical} />
33
+ <meta name="twitter:card" content="summary" />
34
+ <meta name="twitter:title" content={pageTitle} />
35
+ <meta name="twitter:description" content={pageDescription} />
36
+ <style is:global set:html={tokens} />
37
+ <slot name="head" />
38
+ </head>
39
+ <body>
40
+ <header class="edoxen-header">
41
+ <a href="/" class="edoxen-header__brand">
42
+ {config.theme.logos.primary && (
43
+ <img src={config.theme.logos.primary} alt={config.site.title} />
44
+ )}
45
+ <span>{config.site.title}</span>
46
+ </a>
47
+ <nav class="edoxen-header__nav" aria-label="Primary">
48
+ {config.nav.map((item) => (
49
+ !item.locale || item.locale === lang
50
+ ? <a href={item.href}>{item.label}</a>
51
+ : null
52
+ ))}
53
+ </nav>
54
+ {config.features.darkMode && <theme-toggle></theme-toggle>}
55
+ </header>
56
+ <main class="edoxen-main">
57
+ <slot />
58
+ </main>
59
+ <footer class="edoxen-footer">
60
+ {config.theme.logos.footer && (
61
+ <img src={config.theme.logos.footer} alt="" />
62
+ )}
63
+ <ul class="edoxen-footer__social">
64
+ {config.social.map((s) => (
65
+ <li><a href={s.href}>{s.label}</a></li>
66
+ ))}
67
+ </ul>
68
+ <p>Powered by <a href="https://edoxen.github.io">Edoxen</a>.</p>
69
+ </footer>
70
+ <script>
71
+ import '../../islands/theme-toggle.ts'
72
+ import '../../islands/urn-copy.ts'
73
+ import '../../islands/search-filter.ts'
74
+ import '../../islands/section-tabs.ts'
75
+ import '../../islands/decade-scroller.ts'
76
+ import '../../islands/print-button.ts'
77
+ </script>
78
+ </body>
79
+ </html>
@@ -0,0 +1,21 @@
1
+ ---
2
+ import BaseLayout from './BaseLayout.astro'
3
+ import type { EdoxenConfig } from '../../config/index.js'
4
+ import type { DecisionListItem } from '../../data/index.js'
5
+
6
+ interface Props {
7
+ config: EdoxenConfig
8
+ item: DecisionListItem
9
+ }
10
+ const { config, item } = Astro.props
11
+ const title = `${item.title} — ${config.site.title}`
12
+ ---
13
+
14
+ <BaseLayout config={config} title={title} description={item.kind}>
15
+ <article class="edoxen-decision">
16
+ <a href="/decisions">← Decisions</a>
17
+ <h1>{item.title}</h1>
18
+ <p><code>{item.urn}</code></p>
19
+ <slot />
20
+ </article>
21
+ </BaseLayout>
@@ -0,0 +1,21 @@
1
+ ---
2
+ import BaseLayout from './BaseLayout.astro'
3
+ import type { EdoxenConfig } from '../../config/index.js'
4
+ import type { MeetingListItem } from '../../data/index.js'
5
+
6
+ interface Props {
7
+ config: EdoxenConfig
8
+ item: MeetingListItem
9
+ }
10
+ const { config, item } = Astro.props
11
+ const title = `${item.title} — ${config.site.title}`
12
+ ---
13
+
14
+ <BaseLayout config={config} title={title} description={item.identifier}>
15
+ <article class="edoxen-meeting">
16
+ <a href="/meetings">← Meetings</a>
17
+ <h1>{item.title}</h1>
18
+ {item.startDate && <p>{item.startDate}{item.endDate && ` – ${item.endDate}`}</p>}
19
+ <slot />
20
+ </article>
21
+ </BaseLayout>
@@ -0,0 +1,10 @@
1
+ ---
2
+ import cfg from 'virtual:edoxen-config'
3
+ import BaseLayout from '../layouts/BaseLayout.astro'
4
+ ---
5
+
6
+ <BaseLayout config={cfg} title={`Not found — ${cfg.site.title}`}>
7
+ <h1>404</h1>
8
+ <p>The page you requested does not exist.</p>
9
+ <p><a href="/">Return to {cfg.site.title}</a></p>
10
+ </BaseLayout>
@@ -0,0 +1,65 @@
1
+ ---
2
+ import cfg from 'virtual:edoxen-config'
3
+ import payloads from 'virtual:edoxen-payloads'
4
+ import BaseLayout from '../../layouts/BaseLayout.astro'
5
+
6
+ export function getStaticPaths() {
7
+ return cfg.locales
8
+ .filter((l) => (l.routePrefix ?? '') !== '')
9
+ .map((l) => ({ params: { lang: l.code } }))
10
+ }
11
+
12
+ const decisionCount = payloads.decisionsList.items.length
13
+ const meetingCount = payloads.meetingsList.items.length
14
+ ---
15
+
16
+ <BaseLayout config={cfg} title={`About — ${cfg.site.title}`}>
17
+ <h1>About</h1>
18
+
19
+ <section>
20
+ <h2>{cfg.site.title}</h2>
21
+ <p>{cfg.site.description}</p>
22
+ <dl class="edoxen-stats">
23
+ <div><dt>Decisions</dt><dd>{decisionCount}</dd></div>
24
+ <div><dt>Meetings</dt><dd>{meetingCount}</dd></div>
25
+ </dl>
26
+ </section>
27
+
28
+ <section>
29
+ <h2>Edoxen format</h2>
30
+ <p>
31
+ This site renders meeting and decision data using the
32
+ <a href="https://edoxen.github.io">Edoxen</a> information model — a
33
+ YAML-based schema for formal proceedings of standards bodies.
34
+ </p>
35
+ </section>
36
+
37
+ <section>
38
+ <h2>Using this site</h2>
39
+ <ul>
40
+ <li><strong>Decisions</strong> — browse the resolutions archive.</li>
41
+ <li><strong>Meetings</strong> — see meetings and their agendas, minutes, and adopted decisions.</li>
42
+ <li><strong>URNs</strong> — every entity has a stable URN for citation.</li>
43
+ </ul>
44
+ </section>
45
+ </BaseLayout>
46
+
47
+ <style>
48
+ .edoxen-stats {
49
+ display: flex;
50
+ flex-wrap: wrap;
51
+ gap: 1.5rem;
52
+ margin: 1rem 0 0;
53
+ }
54
+ .edoxen-stats dd {
55
+ margin: 0;
56
+ font-size: 1.5rem;
57
+ font-weight: 600;
58
+ }
59
+ .edoxen-stats dt {
60
+ font-size: 0.85rem;
61
+ color: var(--edoxen-color-muted);
62
+ text-transform: uppercase;
63
+ letter-spacing: 0.05em;
64
+ }
65
+ </style>
@@ -0,0 +1,126 @@
1
+ ---
2
+ import cfg from 'virtual:edoxen-config'
3
+ import payloads from 'virtual:edoxen-payloads'
4
+ import DecisionLayout from '../../../layouts/DecisionLayout.astro'
5
+ import UrnBar from '../../../components/UrnBar.astro'
6
+ import BodyBadge from '../../../components/BodyBadge.astro'
7
+ import LocaleTabs from '../../../components/LocaleTabs.astro'
8
+ import { decisionJsonLd } from '../../../../seo/index.js'
9
+ import { pickLocalizedValue, availableSpellings } from '../../../../i18n/index.js'
10
+
11
+ export function getStaticPaths() {
12
+ const urns = Object.keys(payloads.decisionByUrn)
13
+ const nonDefault = cfg.locales.filter((l) => (l.routePrefix ?? '') !== '')
14
+ return urns.flatMap((urn) =>
15
+ nonDefault.map((loc) => ({
16
+ params: { lang: loc.code, urn },
17
+ props: { decision: payloads.decisionByUrn[urn] },
18
+ })),
19
+ )
20
+ }
21
+
22
+ interface Props {
23
+ decision: typeof payloads.decisionByUrn[string]
24
+ }
25
+ const { decision } = Astro.props
26
+ const locale = Astro.params.lang ?? cfg.site.locale
27
+ const title = pickLocalizedValue(decision.title, locale, decision.urn ?? '')
28
+ const subject = pickLocalizedValue(decision.subject, locale)
29
+ const considering = pickLocalizedValue(decision.considering, locale)
30
+ const actions = decision.actions ?? []
31
+ const considerations = decision.considerations ?? []
32
+ const approvals = decision.approvals ?? []
33
+ const titleSpellings = availableSpellings(decision.title)
34
+ const jsonld = decisionJsonLd(decision, { siteUrl: cfg.site.url, siteTitle: cfg.site.title, defaultLocale: locale })
35
+ ---
36
+
37
+ <DecisionLayout
38
+ config={cfg}
39
+ item={{
40
+ urn: decision.urn ?? '',
41
+ identifier: (decision.identifier[0]?.prefix ?? '') + '-' + (decision.identifier[0]?.number ?? ''),
42
+ title,
43
+ kind: decision.kind ?? '',
44
+ dates: (decision.dates ?? []).map((d) => ({ type: d.type ?? '', date: d.date ?? '' })),
45
+ bodyType: decision.body_type,
46
+ }}
47
+ >
48
+ <script type="application/ld+json" set:html={JSON.stringify(jsonld)} is:inline />
49
+ <UrnBar urn={decision.urn ?? ''} />
50
+
51
+ {decision.body_type && <p><BodyBadge code={decision.body_type} /></p>}
52
+
53
+ {titleSpellings.length > 1 && <LocaleTabs spellings={titleSpellings} active={locale} field="title" />}
54
+
55
+ {subject && (
56
+ <section>
57
+ <h2>Subject</h2>
58
+ <p>{subject}</p>
59
+ </section>
60
+ )}
61
+
62
+ {considering && (
63
+ <section>
64
+ <h2>Considering</h2>
65
+ <p>{considering}</p>
66
+ </section>
67
+ )}
68
+
69
+ {considerations.length > 0 && (
70
+ <section>
71
+ <h2>Considerations</h2>
72
+ <ul>
73
+ {considerations.map((c) => (
74
+ <li>{c.kind}{c.references && c.references.length > 0 ? `: ${c.references.map((r) => r.urn).join(', ')}` : ''}</li>
75
+ ))}
76
+ </ul>
77
+ </section>
78
+ )}
79
+
80
+ {actions.length > 0 && (
81
+ <section>
82
+ <h2>Actions</h2>
83
+ <ul>
84
+ {actions.map((a) => (
85
+ <li>
86
+ <strong>{a.type}</strong>
87
+ {a.message && <div>{pickLocalizedValue(a.message, locale)}</div>}
88
+ </li>
89
+ ))}
90
+ </ul>
91
+ </section>
92
+ )}
93
+
94
+ {approvals.length > 0 && (
95
+ <section>
96
+ <h2>Approvals</h2>
97
+ <ul>
98
+ {approvals.map((a) => (
99
+ <li>{a.degree}{a.body ? ` — ${a.body}` : ''}{a.date ? ` (${a.date})` : ''}</li>
100
+ ))}
101
+ </ul>
102
+ </section>
103
+ )}
104
+
105
+ {(decision.dates ?? []).length > 0 && (
106
+ <section>
107
+ <h2>Dates</h2>
108
+ <ul>
109
+ {(decision.dates ?? []).map((d) => (
110
+ <li>{d.type}: {d.date}</li>
111
+ ))}
112
+ </ul>
113
+ </section>
114
+ )}
115
+
116
+ {(decision.urls ?? []).length > 0 && (
117
+ <section>
118
+ <h2>Reference documents</h2>
119
+ <ul>
120
+ {(decision.urls ?? []).map((u) => (
121
+ <li><a href={u.ref}>{u.kind ?? 'document'}</a></li>
122
+ ))}
123
+ </ul>
124
+ </section>
125
+ )}
126
+ </DecisionLayout>
@@ -0,0 +1,27 @@
1
+ ---
2
+ import cfg from 'virtual:edoxen-config'
3
+ import payloads from 'virtual:edoxen-payloads'
4
+ import BaseLayout from '../../../layouts/BaseLayout.astro'
5
+ import DecisionList from '../../../components/DecisionList.astro'
6
+
7
+ export function getStaticPaths() {
8
+ return cfg.locales
9
+ .filter((l) => (l.routePrefix ?? '') !== '')
10
+ .map((l) => ({ params: { lang: l.code } }))
11
+ }
12
+
13
+ const lang = Astro.params.lang ?? cfg.site.locale
14
+ const locEntry = cfg.locales.find((l) => l.code === lang)
15
+ const basePath = locEntry?.routePrefix ? `${locEntry.routePrefix}/decisions` : '/decisions'
16
+ const items = payloads.decisionsList.items
17
+ const endpoint = `${cfg.site.basePath}data/decisions.json`.replace(/\/+/g, '/')
18
+ ---
19
+
20
+ <BaseLayout config={cfg} lang={lang} title={`Decisions — ${cfg.site.title}`}>
21
+ <h1>Decisions</h1>
22
+
23
+ <search-filter data-endpoint={endpoint} data-base-path={basePath}></search-filter>
24
+ <noscript>
25
+ <DecisionList items={items} />
26
+ </noscript>
27
+ </BaseLayout>
@@ -0,0 +1,50 @@
1
+ ---
2
+ import cfg from 'virtual:edoxen-config'
3
+ import payloads from 'virtual:edoxen-payloads'
4
+ import BaseLayout from '../../layouts/BaseLayout.astro'
5
+
6
+ export function getStaticPaths() {
7
+ return cfg.locales
8
+ .filter((l) => (l.routePrefix ?? '') !== '')
9
+ .map((l) => ({ params: { lang: l.code } }))
10
+ }
11
+
12
+ const lang = Astro.params.lang ?? cfg.site.locale
13
+ const locEntry = cfg.locales.find((l) => l.code === lang)
14
+ const urlPrefix = locEntry?.routePrefix ?? ''
15
+ const recentDecisions = payloads.decisionsList.items.slice(0, 5)
16
+ const recentMeetings = payloads.meetingsList.items.slice(0, 3)
17
+ ---
18
+
19
+ <BaseLayout config={cfg} lang={lang}>
20
+ <section>
21
+ <h1>{cfg.site.title}</h1>
22
+ <p>{cfg.site.description}</p>
23
+ </section>
24
+
25
+ <section>
26
+ <h2>Latest decisions</h2>
27
+ <ul>
28
+ {recentDecisions.map((d) => (
29
+ <li>
30
+ <a href={`${urlPrefix}/decisions/${encodeURIComponent(d.urn)}`}>{d.title}</a>
31
+ {d.bodyType && <span class="edoxen-badge">{d.bodyType}</span>}
32
+ </li>
33
+ ))}
34
+ </ul>
35
+ </section>
36
+
37
+ {recentMeetings.length > 0 && (
38
+ <section>
39
+ <h2>Recent meetings</h2>
40
+ <ul>
41
+ {recentMeetings.map((m) => (
42
+ <li>
43
+ <a href={`${urlPrefix}/meetings/${encodeURIComponent(m.urn)}`}>{m.title}</a>
44
+ {m.startDate && <span> — {m.startDate}</span>}
45
+ </li>
46
+ ))}
47
+ </ul>
48
+ </section>
49
+ )}
50
+ </BaseLayout>
@@ -0,0 +1,114 @@
1
+ ---
2
+ import cfg from 'virtual:edoxen-config'
3
+ import payloads from 'virtual:edoxen-payloads'
4
+ import MeetingLayout from '../../../layouts/MeetingLayout.astro'
5
+ import UrnBar from '../../../components/UrnBar.astro'
6
+ import BodyBadge from '../../../components/BodyBadge.astro'
7
+ import AgendaTable from '../../../components/AgendaTable.astro'
8
+ import MinutesSection from '../../../components/MinutesSection.astro'
9
+ import { meetingJsonLd } from '../../../../seo/index.js'
10
+ import { pickLocalizedValue } from '../../../../i18n/index.js'
11
+
12
+ export function getStaticPaths() {
13
+ const urns = Object.keys(payloads.meetingByUrn)
14
+ const nonDefault = cfg.locales.filter((l) => (l.routePrefix ?? '') !== '')
15
+ return urns.flatMap((urn) =>
16
+ nonDefault.map((loc) => ({
17
+ params: { lang: loc.code, urn },
18
+ props: { meeting: payloads.meetingByUrn[urn] },
19
+ })),
20
+ )
21
+ }
22
+
23
+ interface Props {
24
+ meeting: typeof payloads.meetingByUrn[string]
25
+ }
26
+ const { meeting } = Astro.props
27
+ const locale = Astro.params.lang ?? cfg.site.locale
28
+ const title = pickLocalizedValue(meeting.title, locale, meeting.urn ?? '')
29
+ const officers = meeting.officers ?? []
30
+ const agenda = meeting.agenda
31
+ const minutes = meeting.minutes ?? []
32
+ const linkedDecisions = (meeting.decisions ?? []).map((id) =>
33
+ payloads.decisionsList.items.find((d) =>
34
+ d.identifier === `${id.prefix}-${id.number}`
35
+ )
36
+ ).filter((d): d is NonNullable<typeof d> => d !== undefined)
37
+ const jsonld = meetingJsonLd(meeting, { siteUrl: cfg.site.url, siteTitle: cfg.site.title, defaultLocale: locale })
38
+ ---
39
+
40
+ <MeetingLayout
41
+ config={cfg}
42
+ item={{
43
+ urn: meeting.urn ?? '',
44
+ identifier: (meeting.identifier[0]?.prefix ?? '') + '-' + (meeting.identifier[0]?.number ?? ''),
45
+ title,
46
+ startDate: meeting.date_range?.start,
47
+ endDate: meeting.date_range?.end,
48
+ bodyType: meeting.body_type,
49
+ city: meeting.city,
50
+ countryCode: meeting.country_code,
51
+ status: meeting.status,
52
+ }}
53
+ >
54
+ <script type="application/ld+json" set:html={JSON.stringify(jsonld)} is:inline />
55
+ <UrnBar urn={meeting.urn ?? ''} />
56
+
57
+ {meeting.body_type && <p><BodyBadge code={meeting.body_type} /></p>}
58
+
59
+ {meeting.city && (
60
+ <section>
61
+ <h2>Venue</h2>
62
+ <p>{meeting.city}{meeting.country_code ? `, ${meeting.country_code}` : ''}</p>
63
+ </section>
64
+ )}
65
+
66
+ {officers.length > 0 && (
67
+ <section>
68
+ <h2>Officers</h2>
69
+ <ul>
70
+ {officers.map((o) => (
71
+ <li>{o.role}{o.person?.name ? `: ${o.person.name}` : ''}</li>
72
+ ))}
73
+ </ul>
74
+ </section>
75
+ )}
76
+
77
+ {agenda && agenda.items && agenda.items.length > 0 && (
78
+ <section>
79
+ <h2>Agenda</h2>
80
+ <AgendaTable items={agenda.items} locale={locale} />
81
+ </section>
82
+ )}
83
+
84
+ {minutes.length > 0 && (
85
+ <section>
86
+ <h2>Minutes</h2>
87
+ {minutes.map((m) => (
88
+ <MinutesSection minutes={m} locale={locale} />
89
+ ))}
90
+ </section>
91
+ )}
92
+
93
+ {linkedDecisions.length > 0 && (
94
+ <section>
95
+ <h2>Adopted decisions</h2>
96
+ <ul>
97
+ {linkedDecisions.map((d) => (
98
+ <li><a href={`/decisions/${encodeURIComponent(d.urn)}`}>{d.title}</a></li>
99
+ ))}
100
+ </ul>
101
+ </section>
102
+ )}
103
+
104
+ {(meeting.source_urls ?? []).length > 0 && (
105
+ <section>
106
+ <h2>Source documents</h2>
107
+ <ul>
108
+ {(meeting.source_urls ?? []).map((s) => (
109
+ <li><a href={s.ref}>{s.format ?? 'document'}</a></li>
110
+ ))}
111
+ </ul>
112
+ </section>
113
+ )}
114
+ </MeetingLayout>