@ewanc26/pds-landing 0.1.2 → 2.0.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/README.md +122 -12
- package/dist/components/ContactSection.svelte +59 -0
- package/dist/components/ContactSection.svelte.d.ts +17 -0
- package/dist/components/Divider.svelte +9 -0
- package/dist/components/Divider.svelte.d.ts +26 -0
- package/dist/components/KVGrid.svelte +87 -0
- package/dist/components/KVGrid.svelte.d.ts +13 -0
- package/dist/components/LinkList.svelte +60 -0
- package/dist/components/LinkList.svelte.d.ts +16 -0
- package/dist/components/PDSFooter.svelte +43 -0
- package/dist/components/PDSFooter.svelte.d.ts +9 -0
- package/dist/components/PDSPage.svelte +172 -0
- package/dist/components/PDSPage.svelte.d.ts +32 -0
- package/dist/components/PromptLine.svelte +48 -0
- package/dist/components/PromptLine.svelte.d.ts +11 -0
- package/dist/components/SectionLabel.svelte +25 -0
- package/dist/components/SectionLabel.svelte.d.ts +8 -0
- package/dist/components/StatusGrid.svelte +92 -0
- package/dist/components/StatusGrid.svelte.d.ts +10 -0
- package/dist/components/Tagline.svelte +24 -0
- package/dist/components/Tagline.svelte.d.ts +8 -0
- package/dist/components/TerminalCard.svelte +77 -0
- package/dist/components/TerminalCard.svelte.d.ts +10 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/utils/fetchPDSStatus.d.ts +34 -0
- package/dist/utils/fetchPDSStatus.js +65 -0
- package/package.json +41 -17
package/README.md
CHANGED
|
@@ -1,22 +1,132 @@
|
|
|
1
|
-
# pds-landing
|
|
1
|
+
# @ewanc26/pds-landing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Composable Svelte 5 components for an ATProto PDS landing page — terminal aesthetic, live status fetching, zero config to drop in.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
65
|
+
<SectionLabel label="links" />
|
|
66
|
+
<LinkList links={[
|
|
67
|
+
{ href: 'https://bsky.app', label: 'Bluesky' },
|
|
68
|
+
{ href: 'https://atproto.com', label: 'ATProto docs' },
|
|
69
|
+
]} />
|
|
11
70
|
|
|
12
|
-
|
|
13
|
-
|
|
71
|
+
<Divider />
|
|
72
|
+
|
|
73
|
+
<SectionLabel label="contact" />
|
|
74
|
+
<ContactSection blueskyHandle="you.bsky.social" />
|
|
75
|
+
</TerminalCard>
|
|
76
|
+
|
|
77
|
+
<PDSFooter />
|
|
78
|
+
```
|
|
79
|
+
|
|
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';
|
|
86
|
+
|
|
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>
|
|
93
|
+
|
|
94
|
+
<KVGrid {items} />
|
|
14
95
|
```
|
|
15
96
|
|
|
16
|
-
|
|
97
|
+
### Fetch status yourself
|
|
17
98
|
|
|
18
|
-
|
|
99
|
+
```ts
|
|
100
|
+
import { fetchPDSStatus } from '@ewanc26/pds-landing';
|
|
19
101
|
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
22
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,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
|
+
·
|
|
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>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { LinkItem } from './LinkList.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Text shown in the terminal titlebar. */
|
|
4
|
+
cardTitle?: string;
|
|
5
|
+
promptUser?: string;
|
|
6
|
+
promptHost?: string;
|
|
7
|
+
promptPath?: string;
|
|
8
|
+
tagline?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Origin prepended to `/xrpc/…` calls.
|
|
11
|
+
* Leave empty (`''`) to use the current page's origin (default).
|
|
12
|
+
*/
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
showStatus?: boolean;
|
|
15
|
+
showEndpoints?: boolean;
|
|
16
|
+
showLinks?: boolean;
|
|
17
|
+
showContact?: boolean;
|
|
18
|
+
showFooter?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Static links always shown in the Links section.
|
|
21
|
+
* Dynamic links (privacy policy / ToS) from `describeServer` are
|
|
22
|
+
* appended automatically.
|
|
23
|
+
*/
|
|
24
|
+
staticLinks?: LinkItem[];
|
|
25
|
+
blueskyHandle?: string;
|
|
26
|
+
blueskyClientUrl?: string;
|
|
27
|
+
showNixpkg?: boolean;
|
|
28
|
+
showAtproto?: boolean;
|
|
29
|
+
}
|
|
30
|
+
declare const PDSPage: import("svelte").Component<Props, {}, "">;
|
|
31
|
+
type PDSPage = ReturnType<typeof PDSPage>;
|
|
32
|
+
export default PDSPage;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Username portion (before the @). */
|
|
4
|
+
user: string;
|
|
5
|
+
/** Hostname (after the @). */
|
|
6
|
+
host: string;
|
|
7
|
+
/** Shell path shown after the colon. Defaults to `~`. */
|
|
8
|
+
path?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { user, host, path = '~' }: Props = $props();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div class="pds-prompt-line">
|
|
15
|
+
<span class="pds-user-marker">{user}@{host}</span><span class="pds-prompt-path">:{path}</span
|
|
16
|
+
><span class="pds-prompt-char" aria-hidden="true"> $</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
.pds-prompt-line {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: baseline;
|
|
23
|
+
margin-bottom: 0.3rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.pds-user-marker {
|
|
27
|
+
color: var(--pds-color-green);
|
|
28
|
+
font-weight: 700;
|
|
29
|
+
word-break: break-all;
|
|
30
|
+
font-size: clamp(0.95em, 4vw, 1.15em);
|
|
31
|
+
letter-spacing: -0.01em;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.pds-prompt-path {
|
|
35
|
+
color: var(--pds-color-subtext-0);
|
|
36
|
+
font-weight: 700;
|
|
37
|
+
font-size: clamp(0.95em, 4vw, 1.15em);
|
|
38
|
+
opacity: 0.6;
|
|
39
|
+
user-select: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.pds-prompt-char {
|
|
43
|
+
color: var(--pds-color-subtext-0);
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
font-size: clamp(0.95em, 4vw, 1.15em);
|
|
46
|
+
user-select: none;
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Username portion (before the @). */
|
|
3
|
+
user: string;
|
|
4
|
+
/** Hostname (after the @). */
|
|
5
|
+
host: string;
|
|
6
|
+
/** Shell path shown after the colon. Defaults to `~`. */
|
|
7
|
+
path?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const PromptLine: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type PromptLine = ReturnType<typeof PromptLine>;
|
|
11
|
+
export default PromptLine;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
label?: string;
|
|
6
|
+
children?: Snippet;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { label, children }: Props = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div class="pds-section-label">
|
|
13
|
+
{#if children}{@render children()}{:else}{label ?? ''}{/if}
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<style>
|
|
17
|
+
.pds-section-label {
|
|
18
|
+
font-weight: 700;
|
|
19
|
+
text-transform: uppercase;
|
|
20
|
+
font-size: 0.72em;
|
|
21
|
+
letter-spacing: 0.12em;
|
|
22
|
+
color: var(--pds-color-green);
|
|
23
|
+
margin-bottom: 0.7rem;
|
|
24
|
+
}
|
|
25
|
+
</style>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import KVGrid from './KVGrid.svelte';
|
|
4
|
+
import type { KVItem } from './KVGrid.svelte';
|
|
5
|
+
import { fetchPDSStatus, type PDSHealth, type PDSDescription } from '../utils/fetchPDSStatus.js';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/**
|
|
9
|
+
* Base URL to prepend to `/xrpc/…` calls.
|
|
10
|
+
* Defaults to `''` (same origin — correct for in-browser use).
|
|
11
|
+
*/
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { baseUrl = '' }: Props = $props();
|
|
16
|
+
|
|
17
|
+
const LOADING_ITEMS: KVItem[] = [
|
|
18
|
+
{ key: 'reachable', value: '…', status: 'loading' },
|
|
19
|
+
{ key: 'version', value: '…', status: 'loading' },
|
|
20
|
+
{ key: 'did', value: '…', status: 'loading' },
|
|
21
|
+
{ key: 'accounts', value: '…', status: 'loading' },
|
|
22
|
+
{ key: 'invite required', value: '…', status: 'loading' }
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
let items: KVItem[] = $state([...LOADING_ITEMS]);
|
|
26
|
+
|
|
27
|
+
function buildItems(
|
|
28
|
+
health: PDSHealth,
|
|
29
|
+
description: PDSDescription,
|
|
30
|
+
accountCount: number
|
|
31
|
+
): KVItem[] {
|
|
32
|
+
const result: KVItem[] = [
|
|
33
|
+
{
|
|
34
|
+
key: 'reachable',
|
|
35
|
+
value: health.reachable ? '✓ online' : '✗ unreachable',
|
|
36
|
+
status: health.reachable ? 'ok' : 'err'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: 'version',
|
|
40
|
+
value: health.version ?? (health.reachable ? 'unknown' : '—'),
|
|
41
|
+
status: health.reachable ? undefined : 'err'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: 'did',
|
|
45
|
+
value: description.did ?? '—'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: 'accounts',
|
|
49
|
+
value: accountCount >= 0 ? accountCount.toString() : '—'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'invite required',
|
|
53
|
+
value:
|
|
54
|
+
description.inviteCodeRequired === null
|
|
55
|
+
? '—'
|
|
56
|
+
: description.inviteCodeRequired
|
|
57
|
+
? 'yes'
|
|
58
|
+
: 'no',
|
|
59
|
+
status:
|
|
60
|
+
description.inviteCodeRequired === null
|
|
61
|
+
? undefined
|
|
62
|
+
: description.inviteCodeRequired
|
|
63
|
+
? 'warn'
|
|
64
|
+
: 'ok'
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
if (description.phoneVerificationRequired !== null) {
|
|
69
|
+
result.push({
|
|
70
|
+
key: 'phone verify',
|
|
71
|
+
value: description.phoneVerificationRequired ? 'yes' : 'no',
|
|
72
|
+
status: description.phoneVerificationRequired ? 'warn' : 'ok'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (description.availableUserDomains.length > 0) {
|
|
77
|
+
result.push({
|
|
78
|
+
key: 'user domains',
|
|
79
|
+
value: description.availableUserDomains.join(', ')
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onMount(async () => {
|
|
87
|
+
const { health, description, accountCount } = await fetchPDSStatus(baseUrl);
|
|
88
|
+
items = buildItems(health, description, accountCount);
|
|
89
|
+
});
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<KVGrid {items} />
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/**
|
|
3
|
+
* Base URL to prepend to `/xrpc/…` calls.
|
|
4
|
+
* Defaults to `''` (same origin — correct for in-browser use).
|
|
5
|
+
*/
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const StatusGrid: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type StatusGrid = ReturnType<typeof StatusGrid>;
|
|
10
|
+
export default StatusGrid;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
text?: string;
|
|
6
|
+
children?: Snippet;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { text, children }: Props = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<p class="pds-tagline">
|
|
13
|
+
{#if children}{@render children()}{:else}{text ?? ''}{/if}
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<style>
|
|
17
|
+
.pds-tagline {
|
|
18
|
+
color: var(--pds-color-overlay-0);
|
|
19
|
+
font-size: 0.82em;
|
|
20
|
+
margin-top: 0.2rem;
|
|
21
|
+
margin-bottom: 1.4rem;
|
|
22
|
+
line-height: 1.5;
|
|
23
|
+
}
|
|
24
|
+
</style>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Text shown in the titlebar after the traffic-light dots. */
|
|
6
|
+
title?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { title = 'terminal', class: cls = '', children }: Props = $props();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div class="pds-card {cls}">
|
|
15
|
+
<div class="pds-card-titlebar">
|
|
16
|
+
<span class="pds-dot" aria-hidden="true"></span>
|
|
17
|
+
<span class="pds-dot" aria-hidden="true"></span>
|
|
18
|
+
<span class="pds-dot" aria-hidden="true"></span>
|
|
19
|
+
<span class="pds-card-title">{title}</span>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="pds-card-body">
|
|
22
|
+
{#if children}{@render children()}{/if}
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
.pds-card {
|
|
28
|
+
width: 100%;
|
|
29
|
+
max-width: 680px;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
border-radius: 0.5rem;
|
|
32
|
+
background-color: var(--pds-color-mantle);
|
|
33
|
+
border: 1px solid var(--pds-color-surface-1);
|
|
34
|
+
box-shadow:
|
|
35
|
+
0 0 0 1px color-mix(in srgb, var(--pds-color-green) 6%, transparent),
|
|
36
|
+
0 8px 32px color-mix(in srgb, var(--pds-color-shadow) 50%, transparent);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.pds-card-titlebar {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 0.5rem;
|
|
43
|
+
padding: 0.55rem 1rem;
|
|
44
|
+
background-color: var(--pds-color-surface-0);
|
|
45
|
+
color: var(--pds-color-green);
|
|
46
|
+
border-bottom: 1px solid color-mix(in srgb, var(--pds-color-green) 15%, transparent);
|
|
47
|
+
font-size: 0.75rem;
|
|
48
|
+
min-width: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.pds-card-title {
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
text-overflow: ellipsis;
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
min-width: 0;
|
|
56
|
+
margin-left: 0.4rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.pds-dot {
|
|
60
|
+
width: 10px;
|
|
61
|
+
height: 10px;
|
|
62
|
+
border-radius: 50%;
|
|
63
|
+
flex-shrink: 0;
|
|
64
|
+
background-color: color-mix(in srgb, var(--pds-color-green) 25%, transparent);
|
|
65
|
+
border: 1px solid color-mix(in srgb, var(--pds-color-green) 40%, transparent);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.pds-card-body {
|
|
69
|
+
padding: 1.4rem 1.6rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@media (max-width: 440px) {
|
|
73
|
+
.pds-card-body {
|
|
74
|
+
padding: 1.1rem;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Text shown in the titlebar after the traffic-light dots. */
|
|
4
|
+
title?: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
children?: Snippet;
|
|
7
|
+
}
|
|
8
|
+
declare const TerminalCard: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type TerminalCard = ReturnType<typeof TerminalCard>;
|
|
10
|
+
export default TerminalCard;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { fetchPDSStatus } from './utils/fetchPDSStatus.js';
|
|
2
|
+
export type { PDSHealth, PDSDescription, PDSStatusResult } from './utils/fetchPDSStatus.js';
|
|
3
|
+
export { default as TerminalCard } from './components/TerminalCard.svelte';
|
|
4
|
+
export { default as PromptLine } from './components/PromptLine.svelte';
|
|
5
|
+
export { default as Tagline } from './components/Tagline.svelte';
|
|
6
|
+
export { default as SectionLabel } from './components/SectionLabel.svelte';
|
|
7
|
+
export { default as Divider } from './components/Divider.svelte';
|
|
8
|
+
export { default as KVGrid } from './components/KVGrid.svelte';
|
|
9
|
+
export type { KVItem } from './components/KVGrid.svelte';
|
|
10
|
+
export { default as LinkList } from './components/LinkList.svelte';
|
|
11
|
+
export type { LinkItem } from './components/LinkList.svelte';
|
|
12
|
+
export { default as StatusGrid } from './components/StatusGrid.svelte';
|
|
13
|
+
export { default as ContactSection } from './components/ContactSection.svelte';
|
|
14
|
+
export { default as PDSFooter } from './components/PDSFooter.svelte';
|
|
15
|
+
export { default as PDSPage } from './components/PDSPage.svelte';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ─── Utilities ────────────────────────────────────────────────────────────────
|
|
2
|
+
export { fetchPDSStatus } from './utils/fetchPDSStatus.js';
|
|
3
|
+
// ─── Primitive components ─────────────────────────────────────────────────────
|
|
4
|
+
export { default as TerminalCard } from './components/TerminalCard.svelte';
|
|
5
|
+
export { default as PromptLine } from './components/PromptLine.svelte';
|
|
6
|
+
export { default as Tagline } from './components/Tagline.svelte';
|
|
7
|
+
export { default as SectionLabel } from './components/SectionLabel.svelte';
|
|
8
|
+
export { default as Divider } from './components/Divider.svelte';
|
|
9
|
+
export { default as KVGrid } from './components/KVGrid.svelte';
|
|
10
|
+
export { default as LinkList } from './components/LinkList.svelte';
|
|
11
|
+
// ─── Smart / data-fetching components ────────────────────────────────────────
|
|
12
|
+
export { default as StatusGrid } from './components/StatusGrid.svelte';
|
|
13
|
+
// ─── Compound / section components ───────────────────────────────────────────
|
|
14
|
+
export { default as ContactSection } from './components/ContactSection.svelte';
|
|
15
|
+
export { default as PDSFooter } from './components/PDSFooter.svelte';
|
|
16
|
+
// ─── Full-page convenience component ─────────────────────────────────────────
|
|
17
|
+
export { default as PDSPage } from './components/PDSPage.svelte';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types and utilities for fetching live status from an ATProto PDS.
|
|
3
|
+
*/
|
|
4
|
+
export interface PDSHealth {
|
|
5
|
+
reachable: boolean;
|
|
6
|
+
/** Reported software version, or null if unreachable / not exposed. */
|
|
7
|
+
version: string | null;
|
|
8
|
+
}
|
|
9
|
+
export interface PDSDescription {
|
|
10
|
+
did: string | null;
|
|
11
|
+
inviteCodeRequired: boolean | null;
|
|
12
|
+
phoneVerificationRequired: boolean | null;
|
|
13
|
+
availableUserDomains: string[];
|
|
14
|
+
links: {
|
|
15
|
+
privacyPolicy?: string;
|
|
16
|
+
termsOfService?: string;
|
|
17
|
+
} | null;
|
|
18
|
+
contact: {
|
|
19
|
+
email?: string;
|
|
20
|
+
} | null;
|
|
21
|
+
}
|
|
22
|
+
export interface PDSStatusResult {
|
|
23
|
+
health: PDSHealth;
|
|
24
|
+
description: PDSDescription;
|
|
25
|
+
/** Total repo count, or -1 if the fetch failed. */
|
|
26
|
+
accountCount: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Fetch live status from a PDS.
|
|
30
|
+
*
|
|
31
|
+
* @param baseUrl - Origin to prepend to `/xrpc/…` paths.
|
|
32
|
+
* Defaults to `''` (same origin, works in-browser).
|
|
33
|
+
*/
|
|
34
|
+
export declare function fetchPDSStatus(baseUrl?: string): Promise<PDSStatusResult>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types and utilities for fetching live status from an ATProto PDS.
|
|
3
|
+
*/
|
|
4
|
+
async function fetchJSON(url) {
|
|
5
|
+
const r = await fetch(url);
|
|
6
|
+
if (!r.ok)
|
|
7
|
+
throw new Error(`HTTP ${r.status}`);
|
|
8
|
+
return r.json();
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Fetch live status from a PDS.
|
|
12
|
+
*
|
|
13
|
+
* @param baseUrl - Origin to prepend to `/xrpc/…` paths.
|
|
14
|
+
* Defaults to `''` (same origin, works in-browser).
|
|
15
|
+
*/
|
|
16
|
+
export async function fetchPDSStatus(baseUrl = '') {
|
|
17
|
+
// ── health ────────────────────────────────────────────────────────────────
|
|
18
|
+
let health = { reachable: false, version: null };
|
|
19
|
+
try {
|
|
20
|
+
const h = (await fetchJSON(`${baseUrl}/xrpc/_health`));
|
|
21
|
+
health = { reachable: true, version: h.version ?? null };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// leave defaults
|
|
25
|
+
}
|
|
26
|
+
// ── description ───────────────────────────────────────────────────────────
|
|
27
|
+
let description = {
|
|
28
|
+
did: null,
|
|
29
|
+
inviteCodeRequired: null,
|
|
30
|
+
phoneVerificationRequired: null,
|
|
31
|
+
availableUserDomains: [],
|
|
32
|
+
links: null,
|
|
33
|
+
contact: null
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
const d = (await fetchJSON(`${baseUrl}/xrpc/com.atproto.server.describeServer`));
|
|
37
|
+
description = {
|
|
38
|
+
did: d.did ?? null,
|
|
39
|
+
inviteCodeRequired: d.inviteCodeRequired ?? null,
|
|
40
|
+
phoneVerificationRequired: d.phoneVerificationRequired ?? null,
|
|
41
|
+
availableUserDomains: d.availableUserDomains ?? [],
|
|
42
|
+
links: d.links ?? null,
|
|
43
|
+
contact: d.contact ?? null
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// leave defaults
|
|
48
|
+
}
|
|
49
|
+
// ── account count (paginated) ─────────────────────────────────────────────
|
|
50
|
+
let accountCount = 0;
|
|
51
|
+
try {
|
|
52
|
+
let cursor;
|
|
53
|
+
do {
|
|
54
|
+
const url = `${baseUrl}/xrpc/com.atproto.sync.listRepos?limit=1000` +
|
|
55
|
+
(cursor ? `&cursor=${encodeURIComponent(cursor)}` : '');
|
|
56
|
+
const r = (await fetchJSON(url));
|
|
57
|
+
accountCount += (r.repos ?? []).length;
|
|
58
|
+
cursor = r.cursor;
|
|
59
|
+
} while (cursor);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
accountCount = -1;
|
|
63
|
+
}
|
|
64
|
+
return { health, description, accountCount };
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,45 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ewanc26/pds-landing",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"author": "Ewan Croft",
|
|
6
|
-
"license": "AGPL-3.0-only",
|
|
7
|
-
"private": false,
|
|
8
|
-
"publishConfig": {
|
|
9
|
-
"access": "public"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"build",
|
|
13
|
-
"README.md"
|
|
14
|
-
],
|
|
15
|
-
"type": "module",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Composable Svelte components for an ATProto PDS landing page — terminal-aesthetic UI with live status fetching.",
|
|
16
5
|
"scripts": {
|
|
17
6
|
"dev": "vite dev",
|
|
18
|
-
"build": "vite build",
|
|
7
|
+
"build": "vite build && npm run prepack",
|
|
19
8
|
"preview": "vite preview",
|
|
20
9
|
"prepare": "svelte-kit sync || echo ''",
|
|
10
|
+
"prepack": "svelte-kit sync && svelte-package && publint",
|
|
21
11
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
22
12
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
23
13
|
"lint": "prettier --check .",
|
|
24
|
-
"format": "prettier --write ."
|
|
25
|
-
|
|
14
|
+
"format": "prettier --write ."
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"!dist/**/*.test.*",
|
|
19
|
+
"!dist/**/*.spec.*"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": [
|
|
22
|
+
"**/*.css"
|
|
23
|
+
],
|
|
24
|
+
"svelte": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"svelte": "./dist/index.js",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@sveltejs/kit": ">=2.0.0",
|
|
36
|
+
"svelte": "^5.0.0",
|
|
37
|
+
"tailwindcss": ">=4.0.0"
|
|
26
38
|
},
|
|
27
39
|
"dependencies": {
|
|
28
40
|
"@ewanc26/ui": "workspace:*"
|
|
29
41
|
},
|
|
30
42
|
"devDependencies": {
|
|
31
|
-
"@sveltejs/adapter-
|
|
43
|
+
"@sveltejs/adapter-static": "^3.0.10",
|
|
32
44
|
"@sveltejs/kit": "^2.50.2",
|
|
45
|
+
"@sveltejs/package": "^2.5.7",
|
|
33
46
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
34
47
|
"@tailwindcss/typography": "^0.5.19",
|
|
35
48
|
"@tailwindcss/vite": "^4.1.18",
|
|
36
49
|
"prettier": "^3.8.1",
|
|
37
50
|
"prettier-plugin-svelte": "^3.4.1",
|
|
38
51
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
52
|
+
"publint": "^0.3.17",
|
|
39
53
|
"svelte": "^5.51.0",
|
|
40
54
|
"svelte-check": "^4.4.2",
|
|
41
55
|
"tailwindcss": "^4.1.18",
|
|
42
56
|
"typescript": "^5.9.3",
|
|
43
57
|
"vite": "^7.3.1"
|
|
58
|
+
},
|
|
59
|
+
"keywords": [
|
|
60
|
+
"svelte",
|
|
61
|
+
"atproto",
|
|
62
|
+
"bluesky",
|
|
63
|
+
"pds",
|
|
64
|
+
"landing-page"
|
|
65
|
+
],
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
44
68
|
}
|
|
45
69
|
}
|