@ewanc26/pds-landing 1.0.0 → 2.0.2

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 (55) hide show
  1. package/README.md +122 -14
  2. package/dist/components/ContactSection.svelte +59 -0
  3. package/dist/components/ContactSection.svelte.d.ts +17 -0
  4. package/dist/components/Divider.svelte +9 -0
  5. package/dist/components/Divider.svelte.d.ts +26 -0
  6. package/dist/components/KVGrid.svelte +87 -0
  7. package/dist/components/KVGrid.svelte.d.ts +13 -0
  8. package/dist/components/LinkList.svelte +60 -0
  9. package/dist/components/LinkList.svelte.d.ts +16 -0
  10. package/dist/components/PDSFooter.svelte +43 -0
  11. package/dist/components/PDSFooter.svelte.d.ts +9 -0
  12. package/dist/components/PDSPage.svelte +172 -0
  13. package/dist/components/PDSPage.svelte.d.ts +32 -0
  14. package/dist/components/PromptLine.svelte +48 -0
  15. package/dist/components/PromptLine.svelte.d.ts +11 -0
  16. package/dist/components/SectionLabel.svelte +25 -0
  17. package/dist/components/SectionLabel.svelte.d.ts +8 -0
  18. package/dist/components/StatusGrid.svelte +92 -0
  19. package/dist/components/StatusGrid.svelte.d.ts +10 -0
  20. package/dist/components/Tagline.svelte +24 -0
  21. package/dist/components/Tagline.svelte.d.ts +8 -0
  22. package/dist/components/TerminalCard.svelte +77 -0
  23. package/dist/components/TerminalCard.svelte.d.ts +10 -0
  24. package/dist/index.d.ts +15 -0
  25. package/dist/index.js +17 -0
  26. package/dist/utils/fetchPDSStatus.d.ts +34 -0
  27. package/dist/utils/fetchPDSStatus.js +65 -0
  28. package/package.json +68 -31
  29. package/LICENSE +0 -661
  30. package/dist/assets/icon/android-icon-192x192.png +0 -0
  31. package/dist/assets/icon/apple-icon-114x114.png +0 -0
  32. package/dist/assets/icon/apple-icon-120x120.png +0 -0
  33. package/dist/assets/icon/apple-icon-144x144.png +0 -0
  34. package/dist/assets/icon/apple-icon-152x152.png +0 -0
  35. package/dist/assets/icon/apple-icon-180x180.png +0 -0
  36. package/dist/assets/icon/apple-icon-57x57.png +0 -0
  37. package/dist/assets/icon/apple-icon-60x60.png +0 -0
  38. package/dist/assets/icon/apple-icon-72x72.png +0 -0
  39. package/dist/assets/icon/apple-icon-76x76.png +0 -0
  40. package/dist/assets/icon/browserconfig.xml +0 -2
  41. package/dist/assets/icon/favicon-16x16.png +0 -0
  42. package/dist/assets/icon/favicon-256x256.png +0 -0
  43. package/dist/assets/icon/favicon-32x32.png +0 -0
  44. package/dist/assets/icon/favicon-96x96.png +0 -0
  45. package/dist/assets/icon/favicon.ico +0 -0
  46. package/dist/assets/icon/instructions.txt +0 -22
  47. package/dist/assets/icon/manifest.json +0 -41
  48. package/dist/assets/icon/ms-icon-144x144.png +0 -0
  49. package/dist/assets/icon/ms-icon-150x150.png +0 -0
  50. package/dist/assets/icon/ms-icon-310x310.png +0 -0
  51. package/dist/assets/icon/ms-icon-70x70.png +0 -0
  52. package/dist/assets/thumb.svg +0 -78
  53. package/dist/index.html +0 -231
  54. package/dist/script.js +0 -100
  55. package/dist/style.css +0 -1
package/README.md CHANGED
@@ -1,24 +1,132 @@
1
- # pds-landing
1
+ # @ewanc26/pds-landing
2
2
 
3
- Static landing page for [pds.ewancroft.uk](https://pds.ewancroft.uk)a personal ATProto PDS.
3
+ Composable Svelte 5 components for an ATProto PDS landing page terminal aesthetic, live status fetching, zero config to drop in.
4
4
 
5
- Displays live PDS status by querying `/xrpc/_health` and
6
- `/xrpc/com.atproto.server.describeServer` on load.
5
+ ## Install
7
6
 
8
- ## Build
7
+ ```bash
8
+ pnpm add @ewanc26/pds-landing @ewanc26/ui
9
+ ```
10
+
11
+ ## Quick start — full page
12
+
13
+ ```svelte
14
+ <script>
15
+ import { PDSPage } from '@ewanc26/pds-landing';
16
+ </script>
17
+
18
+ <!-- Drop-in: renders the complete terminal landing page -->
19
+ <PDSPage
20
+ cardTitle="ewan's pds"
21
+ promptUser="server"
22
+ promptHost="pds.ewancroft.uk"
23
+ tagline="Bluesky-compatible ATProto PDS · personal instance"
24
+ blueskyHandle="ewancroft.uk"
25
+ />
26
+ ```
27
+
28
+ Import the PDS design tokens and base styles once in your layout:
29
+
30
+ ```css
31
+ /* app.css / layout.css */
32
+ @import '@ewanc26/ui/styles/pds-tokens.css';
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Mix-and-match — primitives
38
+
39
+ All primitives are exported individually so you can compose custom layouts.
40
+
41
+ ```svelte
42
+ <script>
43
+ import {
44
+ TerminalCard,
45
+ PromptLine,
46
+ Tagline,
47
+ SectionLabel,
48
+ Divider,
49
+ StatusGrid,
50
+ LinkList,
51
+ ContactSection,
52
+ PDSFooter,
53
+ } from '@ewanc26/pds-landing';
54
+ </script>
55
+
56
+ <TerminalCard title="my pds">
57
+ <PromptLine user="server" host="pds.example.com" />
58
+ <Tagline text="My custom tagline" />
59
+
60
+ <SectionLabel label="status" />
61
+ <StatusGrid /> <!-- fetches live from same origin -->
62
+
63
+ <Divider />
9
64
 
10
- ```sh
11
- nix build
65
+ <SectionLabel label="links" />
66
+ <LinkList links={[
67
+ { href: 'https://bsky.app', label: 'Bluesky' },
68
+ { href: 'https://atproto.com', label: 'ATProto docs' },
69
+ ]} />
70
+
71
+ <Divider />
72
+
73
+ <SectionLabel label="contact" />
74
+ <ContactSection blueskyHandle="you.bsky.social" />
75
+ </TerminalCard>
76
+
77
+ <PDSFooter />
12
78
  ```
13
79
 
14
- Output is a directory of static files suitable for serving directly with Caddy
15
- or any other file server.
80
+ ### Use raw KV data
81
+
82
+ ```svelte
83
+ <script>
84
+ import { KVGrid } from '@ewanc26/pds-landing';
85
+ import type { KVItem } from '@ewanc26/pds-landing';
16
86
 
17
- ## Usage in nix-config
87
+ const items: KVItem[] = [
88
+ { key: 'status', value: '✓ online', status: 'ok' },
89
+ { key: 'region', value: 'eu-west-1' },
90
+ { key: 'invite', value: 'required', status: 'warn' },
91
+ ];
92
+ </script>
18
93
 
19
- Referenced via git subtree at `modules/server/pds-landing`. To pull upstream
20
- changes into nix-config:
94
+ <KVGrid {items} />
95
+ ```
96
+
97
+ ### Fetch status yourself
21
98
 
22
- ```sh
23
- git subtree pull --prefix modules/server/pds-landing pkgs pds-landing-split --squash
99
+ ```ts
100
+ import { fetchPDSStatus } from '@ewanc26/pds-landing';
101
+
102
+ const { health, description, accountCount } = await fetchPDSStatus('https://pds.example.com');
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Components
108
+
109
+ | Component | Description |
110
+ |---|---|
111
+ | `PDSPage` | Full assembled landing page (convenience) |
112
+ | `TerminalCard` | Terminal window shell with traffic-light dots titlebar |
113
+ | `PromptLine` | `user@host:path $` bash prompt header |
114
+ | `Tagline` | Dimmed subtitle beneath the prompt |
115
+ | `SectionLabel` | Uppercase section heading |
116
+ | `Divider` | Thin green-tinted `<hr>` |
117
+ | `KVGrid` | Key-value grid with ok/warn/err/loading states |
118
+ | `StatusGrid` | Live-fetching PDS status grid (wraps `KVGrid`) |
119
+ | `LinkList` | `→ link` list |
120
+ | `ContactSection` | Bluesky mention + optional email |
121
+ | `PDSFooter` | Footer with nixpkgs / atproto links |
122
+
123
+ ## Design tokens
124
+
125
+ All components consume CSS custom properties from `@ewanc26/ui/styles/pds-tokens.css`:
126
+
127
+ ```
128
+ --pds-font-mono
129
+ --pds-color-crust / mantle / base / surface-0 / surface-1 / overlay-0
130
+ --pds-color-text / subtext-0
131
+ --pds-color-green / red / yellow / shadow
24
132
  ```
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ /**
4
+ * Bluesky handle (without @) to link to.
5
+ * Example: `'ewancroft.uk'`
6
+ */
7
+ blueskyHandle?: string;
8
+ /**
9
+ * Base URL of the Bluesky web client used to build the profile link.
10
+ * Defaults to `'https://bsky.app'`.
11
+ */
12
+ blueskyClientUrl?: string;
13
+ /** Contact email address. Rendered only when provided. */
14
+ email?: string;
15
+ }
16
+
17
+ let {
18
+ blueskyHandle,
19
+ blueskyClientUrl = 'https://bsky.app',
20
+ email
21
+ }: Props = $props();
22
+
23
+ let profileUrl = $derived(
24
+ blueskyHandle ? `${blueskyClientUrl}/profile/${blueskyHandle}` : null
25
+ );
26
+ </script>
27
+
28
+ {#if blueskyHandle && profileUrl}
29
+ <p class="pds-contact-note">
30
+ Send a mention on Bluesky to
31
+ <a href={profileUrl} target="_blank" rel="noopener">@{blueskyHandle}</a>
32
+ </p>
33
+ {/if}
34
+
35
+ {#if email}
36
+ <p class="pds-contact-note pds-contact-email-row">
37
+ Email: <a href="mailto:{email}">{email}</a>
38
+ </p>
39
+ {/if}
40
+
41
+ <style>
42
+ .pds-contact-note {
43
+ font-size: 0.88em;
44
+ color: var(--pds-color-subtext-0);
45
+ }
46
+
47
+ .pds-contact-email-row {
48
+ margin-top: 0.4rem;
49
+ }
50
+
51
+ .pds-contact-note a {
52
+ text-decoration: none;
53
+ color: var(--pds-color-green);
54
+ }
55
+
56
+ .pds-contact-note a:hover {
57
+ text-decoration: underline;
58
+ }
59
+ </style>
@@ -0,0 +1,17 @@
1
+ interface Props {
2
+ /**
3
+ * Bluesky handle (without @) to link to.
4
+ * Example: `'ewancroft.uk'`
5
+ */
6
+ blueskyHandle?: string;
7
+ /**
8
+ * Base URL of the Bluesky web client used to build the profile link.
9
+ * Defaults to `'https://bsky.app'`.
10
+ */
11
+ blueskyClientUrl?: string;
12
+ /** Contact email address. Rendered only when provided. */
13
+ email?: string;
14
+ }
15
+ declare const ContactSection: import("svelte").Component<Props, {}, "">;
16
+ type ContactSection = ReturnType<typeof ContactSection>;
17
+ export default ContactSection;
@@ -0,0 +1,9 @@
1
+ <hr class="pds-divider" />
2
+
3
+ <style>
4
+ .pds-divider {
5
+ border: none;
6
+ border-top: 1px solid color-mix(in srgb, var(--pds-color-green) 12%, transparent);
7
+ margin: 1.2rem 0;
8
+ }
9
+ </style>
@@ -0,0 +1,26 @@
1
+ export default Divider;
2
+ type Divider = SvelteComponent<{
3
+ [x: string]: never;
4
+ }, {
5
+ [evt: string]: CustomEvent<any>;
6
+ }, {}> & {
7
+ $$bindings?: string | undefined;
8
+ };
9
+ declare const Divider: $$__sveltets_2_IsomorphicComponent<{
10
+ [x: string]: never;
11
+ }, {
12
+ [evt: string]: CustomEvent<any>;
13
+ }, {}, {}, string>;
14
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
15
+ new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
16
+ $$bindings?: Bindings;
17
+ } & Exports;
18
+ (internal: unknown, props: {
19
+ $$events?: Events;
20
+ $$slots?: Slots;
21
+ }): Exports & {
22
+ $set?: any;
23
+ $on?: any;
24
+ };
25
+ z_$$bindings?: Bindings;
26
+ }
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ /** A single row in the key-value status grid. */
3
+ export interface KVItem {
4
+ key: string;
5
+ value: string;
6
+ /** Visual state applied to the value cell. */
7
+ status?: 'ok' | 'warn' | 'err' | 'loading';
8
+ }
9
+
10
+ interface Props {
11
+ items: KVItem[];
12
+ }
13
+
14
+ let { items }: Props = $props();
15
+ </script>
16
+
17
+ <div class="pds-kv-grid" role="list">
18
+ {#each items as item (item.key)}
19
+ <span class="pds-kv-key" role="term">{item.key}</span>
20
+ <span class="pds-kv-val {item.status ?? ''}" role="definition">{item.value}</span>
21
+ {/each}
22
+ </div>
23
+
24
+ <style>
25
+ .pds-kv-grid {
26
+ display: grid;
27
+ grid-template-columns: max-content 1fr;
28
+ gap: 0.25rem 1.2rem;
29
+ font-size: 0.88em;
30
+ margin-bottom: 1.4rem;
31
+ }
32
+
33
+ .pds-kv-key {
34
+ color: var(--pds-color-green);
35
+ opacity: 0.6;
36
+ white-space: nowrap;
37
+ }
38
+
39
+ .pds-kv-val {
40
+ color: var(--pds-color-text);
41
+ word-break: break-all;
42
+ min-width: 0;
43
+ }
44
+
45
+ .pds-kv-val.ok {
46
+ color: var(--pds-color-green);
47
+ }
48
+ .pds-kv-val.warn {
49
+ color: var(--pds-color-yellow);
50
+ }
51
+ .pds-kv-val.err {
52
+ color: var(--pds-color-red);
53
+ }
54
+ .pds-kv-val.loading {
55
+ color: var(--pds-color-surface-1);
56
+ animation: pds-kv-pulse 1.2s ease-in-out infinite;
57
+ }
58
+
59
+ @keyframes pds-kv-pulse {
60
+ 0%,
61
+ 100% {
62
+ opacity: 0.4;
63
+ }
64
+ 50% {
65
+ opacity: 1;
66
+ }
67
+ }
68
+
69
+ @media (max-width: 440px) {
70
+ .pds-kv-grid {
71
+ grid-template-columns: 1fr;
72
+ gap: 0;
73
+ }
74
+
75
+ .pds-kv-key {
76
+ text-transform: uppercase;
77
+ opacity: 1;
78
+ font-size: 0.7em;
79
+ letter-spacing: 0.1em;
80
+ margin-top: 0.7rem;
81
+ }
82
+
83
+ .pds-kv-key:first-child {
84
+ margin-top: 0;
85
+ }
86
+ }
87
+ </style>
@@ -0,0 +1,13 @@
1
+ /** A single row in the key-value status grid. */
2
+ export interface KVItem {
3
+ key: string;
4
+ value: string;
5
+ /** Visual state applied to the value cell. */
6
+ status?: 'ok' | 'warn' | 'err' | 'loading';
7
+ }
8
+ interface Props {
9
+ items: KVItem[];
10
+ }
11
+ declare const KVGrid: import("svelte").Component<Props, {}, "">;
12
+ type KVGrid = ReturnType<typeof KVGrid>;
13
+ export default KVGrid;
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ /** A single entry in a link list. */
3
+ export interface LinkItem {
4
+ href: string;
5
+ label: string;
6
+ /**
7
+ * Whether to open in a new tab.
8
+ * Defaults to `true` — set to `false` for internal navigation.
9
+ */
10
+ external?: boolean;
11
+ }
12
+
13
+ interface Props {
14
+ links: LinkItem[];
15
+ }
16
+
17
+ let { links }: Props = $props();
18
+ </script>
19
+
20
+ <ul class="pds-link-list">
21
+ {#each links as link (link.href)}
22
+ <li>
23
+ <a
24
+ href={link.href}
25
+ target={link.external !== false ? '_blank' : undefined}
26
+ rel={link.external !== false ? 'noopener' : undefined}
27
+ >
28
+ {link.label}
29
+ </a>
30
+ </li>
31
+ {/each}
32
+ </ul>
33
+
34
+ <style>
35
+ .pds-link-list {
36
+ list-style: none;
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 0.35rem;
40
+ font-size: 0.88em;
41
+ padding: 0;
42
+ margin: 0;
43
+ }
44
+
45
+ .pds-link-list li::before {
46
+ content: '→ ';
47
+ color: var(--pds-color-green);
48
+ }
49
+
50
+ .pds-link-list a {
51
+ text-decoration: none;
52
+ color: var(--pds-color-green);
53
+ opacity: 0.85;
54
+ transition: opacity 0.15s;
55
+ }
56
+
57
+ .pds-link-list a:hover {
58
+ opacity: 1;
59
+ }
60
+ </style>
@@ -0,0 +1,16 @@
1
+ /** A single entry in a link list. */
2
+ export interface LinkItem {
3
+ href: string;
4
+ label: string;
5
+ /**
6
+ * Whether to open in a new tab.
7
+ * Defaults to `true` — set to `false` for internal navigation.
8
+ */
9
+ external?: boolean;
10
+ }
11
+ interface Props {
12
+ links: LinkItem[];
13
+ }
14
+ declare const LinkList: import("svelte").Component<Props, {}, "">;
15
+ type LinkList = ReturnType<typeof LinkList>;
16
+ export default LinkList;
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ /** Show the `nixpkgs#bluesky-pds` credit. Defaults to `true`. */
4
+ showNixpkg?: boolean;
5
+ /** Show the `atproto.com` link. Defaults to `true`. */
6
+ showAtproto?: boolean;
7
+ }
8
+
9
+ let { showNixpkg = true, showAtproto = true }: Props = $props();
10
+ </script>
11
+
12
+ <footer class="pds-footer">
13
+ {#if showNixpkg}
14
+ powered by
15
+ <a href="https://search.nixos.org/packages?show=bluesky-pds" target="_blank" rel="noopener"
16
+ >nixpkgs#bluesky-pds</a
17
+ >
18
+ {/if}
19
+ {#if showNixpkg && showAtproto}
20
+ &nbsp;·&nbsp;
21
+ {/if}
22
+ {#if showAtproto}
23
+ <a href="https://atproto.com" target="_blank" rel="noopener">atproto.com</a>
24
+ {/if}
25
+ </footer>
26
+
27
+ <style>
28
+ .pds-footer {
29
+ text-align: center;
30
+ color: color-mix(in srgb, var(--pds-color-green) 35%, transparent);
31
+ font-size: 0.75em;
32
+ }
33
+
34
+ .pds-footer a {
35
+ text-decoration: underline;
36
+ color: color-mix(in srgb, var(--pds-color-green) 35%, transparent);
37
+ transition: color 0.15s;
38
+ }
39
+
40
+ .pds-footer a:hover {
41
+ color: var(--pds-color-green);
42
+ }
43
+ </style>
@@ -0,0 +1,9 @@
1
+ interface Props {
2
+ /** Show the `nixpkgs#bluesky-pds` credit. Defaults to `true`. */
3
+ showNixpkg?: boolean;
4
+ /** Show the `atproto.com` link. Defaults to `true`. */
5
+ showAtproto?: boolean;
6
+ }
7
+ declare const PDSFooter: import("svelte").Component<Props, {}, "">;
8
+ type PDSFooter = ReturnType<typeof PDSFooter>;
9
+ export default PDSFooter;
@@ -0,0 +1,172 @@
1
+ <script lang="ts">
2
+ /**
3
+ * PDSPage — a fully assembled ATProto PDS landing page.
4
+ *
5
+ * All sections (status, endpoints, links, contact) are included by default.
6
+ * Each section can be individually disabled via boolean props, and the
7
+ * individual primitives (TerminalCard, StatusGrid, LinkList, …) can be
8
+ * composed manually for bespoke layouts.
9
+ */
10
+ import { onMount } from 'svelte';
11
+ import TerminalCard from './TerminalCard.svelte';
12
+ import PromptLine from './PromptLine.svelte';
13
+ import Tagline from './Tagline.svelte';
14
+ import SectionLabel from './SectionLabel.svelte';
15
+ import Divider from './Divider.svelte';
16
+ import StatusGrid from './StatusGrid.svelte';
17
+ import LinkList from './LinkList.svelte';
18
+ import ContactSection from './ContactSection.svelte';
19
+ import PDSFooter from './PDSFooter.svelte';
20
+ import type { LinkItem } from './LinkList.svelte';
21
+ import { fetchPDSStatus } from '../utils/fetchPDSStatus.js';
22
+
23
+ interface Props {
24
+ // ── Card chrome ──────────────────────────────────────────────────────
25
+ /** Text shown in the terminal titlebar. */
26
+ cardTitle?: string;
27
+
28
+ // ── Prompt ───────────────────────────────────────────────────────────
29
+ promptUser?: string;
30
+ promptHost?: string;
31
+ promptPath?: string;
32
+ tagline?: string;
33
+
34
+ // ── API ──────────────────────────────────────────────────────────────
35
+ /**
36
+ * Origin prepended to `/xrpc/…` calls.
37
+ * Leave empty (`''`) to use the current page's origin (default).
38
+ */
39
+ baseUrl?: string;
40
+
41
+ // ── Sections ─────────────────────────────────────────────────────────
42
+ showStatus?: boolean;
43
+ showEndpoints?: boolean;
44
+ showLinks?: boolean;
45
+ showContact?: boolean;
46
+ showFooter?: boolean;
47
+
48
+ // ── Links section ────────────────────────────────────────────────────
49
+ /**
50
+ * Static links always shown in the Links section.
51
+ * Dynamic links (privacy policy / ToS) from `describeServer` are
52
+ * appended automatically.
53
+ */
54
+ staticLinks?: LinkItem[];
55
+
56
+ // ── Contact section ──────────────────────────────────────────────────
57
+ blueskyHandle?: string;
58
+ blueskyClientUrl?: string;
59
+
60
+ // ── Footer ───────────────────────────────────────────────────────────
61
+ showNixpkg?: boolean;
62
+ showAtproto?: boolean;
63
+ }
64
+
65
+ let {
66
+ cardTitle = "ewan's pds",
67
+ promptUser = 'server',
68
+ promptHost = 'pds.ewancroft.uk',
69
+ promptPath = '~',
70
+ tagline = 'Bluesky-compatible ATProto PDS · personal instance',
71
+ baseUrl = '',
72
+ showStatus = true,
73
+ showEndpoints = true,
74
+ showLinks = true,
75
+ showContact = true,
76
+ showFooter = true,
77
+ staticLinks = [{ href: 'https://witchsky.app', label: 'Witchsky Web Client' }],
78
+ blueskyHandle = 'ewancroft.uk',
79
+ blueskyClientUrl = 'https://witchsky.app',
80
+ showNixpkg = true,
81
+ showAtproto = true
82
+ }: Props = $props();
83
+
84
+ const ENDPOINT_LINKS: LinkItem[] = [
85
+ { href: 'https://github.com/bluesky-social/atproto', label: 'atproto source code' },
86
+ { href: 'https://github.com/bluesky-social/pds', label: 'self-hosting guide' },
87
+ { href: 'https://atproto.com', label: 'protocol docs' }
88
+ ];
89
+
90
+ // Dynamic state populated from describeServer
91
+ let extraLinks: LinkItem[] = $state([]);
92
+ let dynamicLinks: LinkItem[] = $derived([...staticLinks, ...extraLinks]);
93
+ let contactEmail: string | null = $state(null);
94
+
95
+ onMount(async () => {
96
+ try {
97
+ const { description } = await fetchPDSStatus(baseUrl);
98
+ const extras: LinkItem[] = [];
99
+ if (description.links?.privacyPolicy) {
100
+ extras.push({ href: description.links.privacyPolicy, label: 'Privacy Policy' });
101
+ }
102
+ if (description.links?.termsOfService) {
103
+ extras.push({ href: description.links.termsOfService, label: 'Terms of Service' });
104
+ }
105
+ if (extras.length) extraLinks = extras;
106
+ if (description.contact?.email) contactEmail = description.contact.email;
107
+ } catch {
108
+ // silently fall back to static defaults
109
+ }
110
+ });
111
+ </script>
112
+
113
+ <div class="pds-page">
114
+ <TerminalCard title={cardTitle}>
115
+ <PromptLine user={promptUser} host={promptHost} path={promptPath} />
116
+ <Tagline text={tagline} />
117
+
118
+ {#if showStatus}
119
+ <SectionLabel label="status" />
120
+ <StatusGrid {baseUrl} />
121
+ {/if}
122
+
123
+ {#if showEndpoints}
124
+ <Divider />
125
+ <SectionLabel label="endpoints" />
126
+ <p class="pds-endpoints-note">
127
+ Most API routes are under <span class="pds-highlight">/xrpc/</span>
128
+ </p>
129
+ <LinkList links={ENDPOINT_LINKS} />
130
+ {/if}
131
+
132
+ {#if showLinks}
133
+ <Divider />
134
+ <SectionLabel label="links" />
135
+ <LinkList links={dynamicLinks} />
136
+ {/if}
137
+
138
+ {#if showContact}
139
+ <Divider />
140
+ <SectionLabel label="contact" />
141
+ <ContactSection
142
+ {blueskyHandle}
143
+ {blueskyClientUrl}
144
+ email={contactEmail ?? undefined}
145
+ />
146
+ {/if}
147
+ </TerminalCard>
148
+
149
+ {#if showFooter}
150
+ <PDSFooter {showNixpkg} {showAtproto} />
151
+ {/if}
152
+ </div>
153
+
154
+ <style>
155
+ .pds-page {
156
+ display: flex;
157
+ flex-direction: column;
158
+ align-items: center;
159
+ gap: 2rem;
160
+ width: 100%;
161
+ }
162
+
163
+ .pds-endpoints-note {
164
+ font-size: 0.88em;
165
+ color: var(--pds-color-subtext-0);
166
+ margin-bottom: 0.7rem;
167
+ }
168
+
169
+ .pds-highlight {
170
+ color: var(--pds-color-green);
171
+ }
172
+ </style>