@commonpub/layer 0.21.22 → 0.22.1
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/components/admin/theme/AdminThemeFamilyCard.vue +277 -0
- package/components/admin/theme/AdminThemeOverridesPanel.vue +222 -0
- package/components/admin/theme/AdminThemePreviewPane.vue +218 -0
- package/components/admin/theme/AdminThemeSceneAdmin.vue +189 -0
- package/components/admin/theme/AdminThemeSceneGallery.vue +353 -0
- package/components/admin/theme/AdminThemeSceneProse.vue +140 -0
- package/components/admin/theme/AdminThemeTokenGroup.vue +98 -0
- package/components/admin/theme/AdminThemeTokenInput.vue +278 -0
- package/composables/useTheme.ts +24 -14
- package/composables/useThemeAdmin.ts +167 -0
- package/package.json +7 -7
- package/pages/admin/theme/edit/[id].vue +595 -0
- package/pages/admin/theme/index.vue +449 -0
- package/plugins/theme.ts +25 -7
- package/server/api/admin/themes/[id].delete.ts +40 -0
- package/server/api/admin/themes/[id].get.ts +20 -0
- package/server/api/admin/themes/[id].put.ts +45 -0
- package/server/api/admin/themes/discover.get.ts +22 -0
- package/server/api/admin/themes/index.get.ts +40 -0
- package/server/api/admin/themes/index.post.ts +46 -0
- package/server/api/profile/theme.put.ts +2 -1
- package/server/middleware/theme.ts +23 -9
- package/server/utils/instanceTheme.ts +145 -25
- package/types/theme.ts +54 -0
- package/utils/themeDiscovery.ts +67 -0
- package/utils/themeIO.ts +79 -0
- package/utils/themeIds.ts +25 -0
- package/pages/admin/theme.vue +0 -502
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Component gallery preview. Renders representative UI primitives —
|
|
4
|
+
* buttons, cards, forms, badges, alerts, code — so every category of
|
|
5
|
+
* token has something visible to land on. Self-contained: all markup
|
|
6
|
+
* uses `var(--*)` only so the parent's inline-style scope drives the look.
|
|
7
|
+
*
|
|
8
|
+
* Designed to be REPLACEABLE later — when the layout-builder lands, this
|
|
9
|
+
* scene becomes one option among many in AdminThemePreviewPane's picker.
|
|
10
|
+
*/
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div class="scene-gallery">
|
|
15
|
+
<!-- TYPOGRAPHY -->
|
|
16
|
+
<section class="scene-block">
|
|
17
|
+
<h2 class="scene-section-label">Typography</h2>
|
|
18
|
+
<div class="scene-card">
|
|
19
|
+
<h1 class="scene-h1">Display heading H1</h1>
|
|
20
|
+
<h2 class="scene-h2">Section heading H2</h2>
|
|
21
|
+
<h3 class="scene-h3">Subsection heading H3</h3>
|
|
22
|
+
<p class="scene-body">
|
|
23
|
+
Body copy uses <code class="scene-inline-code">var(--font-body)</code> at
|
|
24
|
+
<code class="scene-inline-code">var(--text-base)</code>. The quick brown fox
|
|
25
|
+
jumps over the lazy dog.
|
|
26
|
+
</p>
|
|
27
|
+
<p class="scene-muted">Secondary text — note the contrast against background.</p>
|
|
28
|
+
<p class="scene-faint">Tertiary text — used for placeholders and faint metadata.</p>
|
|
29
|
+
<p class="scene-mono-label">Mono label — uppercase letter-spaced</p>
|
|
30
|
+
</div>
|
|
31
|
+
</section>
|
|
32
|
+
|
|
33
|
+
<!-- BUTTONS -->
|
|
34
|
+
<section class="scene-block">
|
|
35
|
+
<h2 class="scene-section-label">Buttons</h2>
|
|
36
|
+
<div class="scene-card scene-card-row">
|
|
37
|
+
<button class="scene-btn scene-btn-primary">Primary action</button>
|
|
38
|
+
<button class="scene-btn">Secondary</button>
|
|
39
|
+
<button class="scene-btn scene-btn-ghost">Ghost</button>
|
|
40
|
+
<button class="scene-btn scene-btn-danger">Destructive</button>
|
|
41
|
+
<button class="scene-btn" disabled>Disabled</button>
|
|
42
|
+
</div>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<!-- BADGES + SEMANTIC -->
|
|
46
|
+
<section class="scene-block">
|
|
47
|
+
<h2 class="scene-section-label">Badges & semantic colors</h2>
|
|
48
|
+
<div class="scene-card scene-card-row">
|
|
49
|
+
<span class="scene-badge tone-accent">Accent</span>
|
|
50
|
+
<span class="scene-badge tone-green">Success</span>
|
|
51
|
+
<span class="scene-badge tone-yellow">Warning</span>
|
|
52
|
+
<span class="scene-badge tone-red">Error</span>
|
|
53
|
+
<span class="scene-badge tone-purple">Info</span>
|
|
54
|
+
<span class="scene-badge tone-teal">Teal</span>
|
|
55
|
+
<span class="scene-badge tone-pink">Pink</span>
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
<!-- FORMS -->
|
|
60
|
+
<section class="scene-block">
|
|
61
|
+
<h2 class="scene-section-label">Forms</h2>
|
|
62
|
+
<div class="scene-card">
|
|
63
|
+
<label class="scene-field">
|
|
64
|
+
<span class="scene-field-label">Email address</span>
|
|
65
|
+
<input class="scene-input" type="email" placeholder="you@example.com" value="federation@deveco.io" />
|
|
66
|
+
</label>
|
|
67
|
+
<label class="scene-field">
|
|
68
|
+
<span class="scene-field-label">Bio</span>
|
|
69
|
+
<textarea class="scene-input scene-textarea" rows="3">Building open community tooling.</textarea>
|
|
70
|
+
</label>
|
|
71
|
+
<label class="scene-field">
|
|
72
|
+
<span class="scene-field-label">Visibility</span>
|
|
73
|
+
<select class="scene-input">
|
|
74
|
+
<option>Public</option>
|
|
75
|
+
<option>Members only</option>
|
|
76
|
+
<option>Private</option>
|
|
77
|
+
</select>
|
|
78
|
+
</label>
|
|
79
|
+
<div class="scene-checkrow">
|
|
80
|
+
<label class="scene-check"><input type="checkbox" checked /> Allow follows</label>
|
|
81
|
+
<label class="scene-check"><input type="checkbox" /> Email digest</label>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<!-- CONTENT CARDS -->
|
|
87
|
+
<section class="scene-block">
|
|
88
|
+
<h2 class="scene-section-label">Content cards</h2>
|
|
89
|
+
<div class="scene-card-grid">
|
|
90
|
+
<article class="scene-content-card">
|
|
91
|
+
<div class="scene-content-image" aria-hidden="true">
|
|
92
|
+
<span class="scene-content-type">PROJECT</span>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="scene-content-body">
|
|
95
|
+
<h3 class="scene-content-title">Tiny mesh router for off-grid relays</h3>
|
|
96
|
+
<p class="scene-content-excerpt">Solar-powered LoRa nodes that auto-form a mesh and route AP federation activity over store-and-forward links.</p>
|
|
97
|
+
<div class="scene-content-meta">
|
|
98
|
+
<span class="scene-avatar" aria-hidden="true">M</span>
|
|
99
|
+
<span class="scene-content-author">moheeb</span>
|
|
100
|
+
<span class="scene-content-dot">·</span>
|
|
101
|
+
<span class="scene-content-date">3 days ago</span>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</article>
|
|
105
|
+
<article class="scene-content-card">
|
|
106
|
+
<div class="scene-content-image scene-content-image-alt" aria-hidden="true">
|
|
107
|
+
<span class="scene-content-type tone-explainer">EXPLAINER</span>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="scene-content-body">
|
|
110
|
+
<h3 class="scene-content-title">How ActivityPub delivery actually works</h3>
|
|
111
|
+
<p class="scene-content-excerpt">An interactive walkthrough of the inbox / outbox dance, signatures, and the recovery dead-letter queue.</p>
|
|
112
|
+
<div class="scene-content-meta">
|
|
113
|
+
<span class="scene-avatar" aria-hidden="true">C</span>
|
|
114
|
+
<span class="scene-content-author">commonpub</span>
|
|
115
|
+
<span class="scene-content-dot">·</span>
|
|
116
|
+
<span class="scene-content-date">Last week</span>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</article>
|
|
120
|
+
</div>
|
|
121
|
+
</section>
|
|
122
|
+
|
|
123
|
+
<!-- ALERTS -->
|
|
124
|
+
<section class="scene-block">
|
|
125
|
+
<h2 class="scene-section-label">Alerts</h2>
|
|
126
|
+
<div class="scene-stack">
|
|
127
|
+
<div class="scene-alert tone-accent">
|
|
128
|
+
<i class="fa-solid fa-circle-info" aria-hidden="true" />
|
|
129
|
+
<div>
|
|
130
|
+
<strong>Federation enabled.</strong>
|
|
131
|
+
Your instance is sending and receiving ActivityPub messages with 3 peers.
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="scene-alert tone-green">
|
|
135
|
+
<i class="fa-solid fa-check" aria-hidden="true" />
|
|
136
|
+
<div><strong>Saved.</strong> Theme tokens are applied to your live site.</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="scene-alert tone-yellow">
|
|
139
|
+
<i class="fa-solid fa-triangle-exclamation" aria-hidden="true" />
|
|
140
|
+
<div><strong>Heads up:</strong> two tokens reference colors that fail WCAG AA contrast.</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</section>
|
|
144
|
+
|
|
145
|
+
<!-- CODE -->
|
|
146
|
+
<section class="scene-block">
|
|
147
|
+
<h2 class="scene-section-label">Code block</h2>
|
|
148
|
+
<div class="scene-code">
|
|
149
|
+
<div class="scene-code-header">
|
|
150
|
+
<span>useFederationDigest.ts</span>
|
|
151
|
+
<button class="scene-code-copy" type="button">copy</button>
|
|
152
|
+
</div>
|
|
153
|
+
<pre class="scene-code-body"><code>// Computes the rfc-3230 digest header for an inbox POST.
|
|
154
|
+
export function digestFor(body: string): string {
|
|
155
|
+
const buf = new TextEncoder().encode(body);
|
|
156
|
+
const hash = crypto.subtle.digestSync('SHA-256', buf);
|
|
157
|
+
return 'SHA-256=' + base64(hash);
|
|
158
|
+
}</code></pre>
|
|
159
|
+
</div>
|
|
160
|
+
</section>
|
|
161
|
+
|
|
162
|
+
<!-- SHADOWS / SHAPES -->
|
|
163
|
+
<section class="scene-block">
|
|
164
|
+
<h2 class="scene-section-label">Shadows & shape</h2>
|
|
165
|
+
<div class="scene-shadow-grid">
|
|
166
|
+
<div class="scene-shadow-box" style="box-shadow: var(--shadow-sm)">sm</div>
|
|
167
|
+
<div class="scene-shadow-box" style="box-shadow: var(--shadow-md)">md</div>
|
|
168
|
+
<div class="scene-shadow-box" style="box-shadow: var(--shadow-lg)">lg</div>
|
|
169
|
+
<div class="scene-shadow-box" style="box-shadow: var(--shadow-xl)">xl</div>
|
|
170
|
+
<div class="scene-shadow-box" style="box-shadow: var(--shadow-accent)">accent</div>
|
|
171
|
+
</div>
|
|
172
|
+
</section>
|
|
173
|
+
</div>
|
|
174
|
+
</template>
|
|
175
|
+
|
|
176
|
+
<style scoped>
|
|
177
|
+
.scene-gallery { display: flex; flex-direction: column; gap: var(--space-6); max-width: 720px; margin: 0 auto; }
|
|
178
|
+
|
|
179
|
+
.scene-block { display: flex; flex-direction: column; gap: var(--space-2); }
|
|
180
|
+
|
|
181
|
+
.scene-section-label {
|
|
182
|
+
font-family: var(--font-mono);
|
|
183
|
+
font-size: var(--text-label);
|
|
184
|
+
letter-spacing: var(--tracking-wide);
|
|
185
|
+
text-transform: uppercase;
|
|
186
|
+
color: var(--text-dim);
|
|
187
|
+
margin: 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.scene-card {
|
|
191
|
+
background: var(--surface);
|
|
192
|
+
border: var(--border-width-default) solid var(--border);
|
|
193
|
+
padding: var(--space-4);
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-direction: column;
|
|
196
|
+
gap: var(--space-3);
|
|
197
|
+
}
|
|
198
|
+
.scene-card-row { flex-direction: row; flex-wrap: wrap; align-items: center; }
|
|
199
|
+
|
|
200
|
+
/* Typography */
|
|
201
|
+
.scene-h1 { font-family: var(--font-heading); font-size: var(--text-3xl); font-weight: var(--font-weight-bold); margin: 0; color: var(--text); letter-spacing: var(--tracking-tight); line-height: var(--leading-tight); }
|
|
202
|
+
.scene-h2 { font-family: var(--font-heading); font-size: var(--text-xl); font-weight: var(--font-weight-semibold); margin: 0; color: var(--text); }
|
|
203
|
+
.scene-h3 { font-family: var(--font-heading); font-size: var(--text-md); font-weight: var(--font-weight-semibold); margin: 0; color: var(--text); }
|
|
204
|
+
.scene-body { font-size: var(--text-base); line-height: var(--leading-normal); color: var(--text); margin: 0; }
|
|
205
|
+
.scene-muted { font-size: var(--text-sm); color: var(--text-dim); margin: 0; }
|
|
206
|
+
.scene-faint { font-size: var(--text-sm); color: var(--text-faint); margin: 0; }
|
|
207
|
+
.scene-mono-label { font-family: var(--font-mono); font-size: var(--text-label); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--text-dim); margin: 0; }
|
|
208
|
+
.scene-inline-code { font-family: var(--font-mono); font-size: 0.9em; padding: 1px 6px; background: var(--surface2); color: var(--accent); border: var(--border-width-thin) solid var(--border2); }
|
|
209
|
+
|
|
210
|
+
/* Buttons */
|
|
211
|
+
.scene-btn {
|
|
212
|
+
display: inline-flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
gap: 6px;
|
|
215
|
+
padding: 8px 14px;
|
|
216
|
+
font-family: var(--font-mono);
|
|
217
|
+
font-size: var(--text-sm);
|
|
218
|
+
font-weight: var(--font-weight-semibold);
|
|
219
|
+
letter-spacing: var(--tracking-wide);
|
|
220
|
+
background: var(--surface);
|
|
221
|
+
color: var(--text);
|
|
222
|
+
border: var(--border-width-default) solid var(--border);
|
|
223
|
+
cursor: pointer;
|
|
224
|
+
box-shadow: var(--shadow-sm);
|
|
225
|
+
transition: transform var(--transition-fast);
|
|
226
|
+
}
|
|
227
|
+
.scene-btn:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow-md); }
|
|
228
|
+
.scene-btn:active { transform: translate(0, 0); box-shadow: none; }
|
|
229
|
+
.scene-btn:disabled { opacity: 0.55; cursor: not-allowed; box-shadow: none; transform: none; }
|
|
230
|
+
.scene-btn-primary { background: var(--accent); color: var(--color-on-accent); border-color: var(--border); }
|
|
231
|
+
.scene-btn-ghost { background: transparent; box-shadow: none; }
|
|
232
|
+
.scene-btn-danger { background: var(--red); color: var(--color-text-inverse); border-color: var(--border); }
|
|
233
|
+
|
|
234
|
+
/* Badges */
|
|
235
|
+
.scene-badge {
|
|
236
|
+
display: inline-flex;
|
|
237
|
+
align-items: center;
|
|
238
|
+
gap: 4px;
|
|
239
|
+
padding: 2px 8px;
|
|
240
|
+
font-family: var(--font-mono);
|
|
241
|
+
font-size: var(--text-label);
|
|
242
|
+
letter-spacing: var(--tracking-wide);
|
|
243
|
+
text-transform: uppercase;
|
|
244
|
+
border: var(--border-width-thin) solid var(--border2);
|
|
245
|
+
}
|
|
246
|
+
.tone-accent { color: var(--accent); background: var(--accent-bg); border-color: var(--accent-border); }
|
|
247
|
+
.tone-green { color: var(--green); background: var(--green-bg); border-color: var(--green-border); }
|
|
248
|
+
.tone-yellow { color: var(--yellow); background: var(--yellow-bg); border-color: var(--yellow-border); }
|
|
249
|
+
.tone-red { color: var(--red); background: var(--red-bg); border-color: var(--red-border); }
|
|
250
|
+
.tone-purple { color: var(--purple); background: var(--purple-bg); border-color: var(--purple-border); }
|
|
251
|
+
.tone-teal { color: var(--teal); background: var(--teal-bg); border-color: var(--teal-border); }
|
|
252
|
+
.tone-pink { color: var(--pink); background: var(--pink-bg); border-color: var(--pink-border); }
|
|
253
|
+
.tone-explainer { color: var(--teal); background: var(--teal-bg); border-color: var(--teal-border); }
|
|
254
|
+
|
|
255
|
+
/* Forms */
|
|
256
|
+
.scene-field { display: flex; flex-direction: column; gap: 6px; }
|
|
257
|
+
.scene-field-label { font-family: var(--font-mono); font-size: var(--text-label); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--text-dim); }
|
|
258
|
+
.scene-input {
|
|
259
|
+
background: var(--surface2);
|
|
260
|
+
color: var(--text);
|
|
261
|
+
border: var(--border-width-default) solid var(--border);
|
|
262
|
+
padding: 8px 10px;
|
|
263
|
+
font-size: var(--text-sm);
|
|
264
|
+
font-family: var(--font-body);
|
|
265
|
+
width: 100%;
|
|
266
|
+
}
|
|
267
|
+
.scene-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
268
|
+
.scene-textarea { resize: vertical; min-height: 60px; font-family: var(--font-body); }
|
|
269
|
+
.scene-checkrow { display: flex; gap: var(--space-4); flex-wrap: wrap; }
|
|
270
|
+
.scene-check { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-sm); color: var(--text); }
|
|
271
|
+
|
|
272
|
+
/* Content cards */
|
|
273
|
+
.scene-card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-4); }
|
|
274
|
+
.scene-content-card { background: var(--surface); border: var(--border-width-default) solid var(--border); display: flex; flex-direction: column; box-shadow: var(--shadow-sm); }
|
|
275
|
+
.scene-content-image { position: relative; height: 120px; background: linear-gradient(135deg, var(--accent-bg) 0%, var(--accent-bg-strong, var(--accent-bg)) 100%); border-bottom: var(--border-width-default) solid var(--border); }
|
|
276
|
+
.scene-content-image-alt { background: linear-gradient(135deg, var(--teal-bg) 0%, var(--purple-bg) 100%); }
|
|
277
|
+
.scene-content-type { position: absolute; top: 8px; left: 8px; font-family: var(--font-mono); font-size: 10px; letter-spacing: var(--tracking-wide); padding: 2px 6px; background: var(--color-badge-overlay, rgba(0,0,0,0.7)); color: #fff; }
|
|
278
|
+
.scene-content-body { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); }
|
|
279
|
+
.scene-content-title { font-size: var(--text-md); font-weight: var(--font-weight-semibold); margin: 0; color: var(--text); }
|
|
280
|
+
.scene-content-excerpt { font-size: var(--text-sm); color: var(--text-dim); margin: 0; line-height: var(--leading-snug); }
|
|
281
|
+
.scene-content-meta { display: flex; align-items: center; gap: 6px; font-size: var(--text-sm); color: var(--text-dim); }
|
|
282
|
+
.scene-avatar { width: 22px; height: 22px; border-radius: var(--radius-full); background: var(--accent); color: var(--color-on-accent); display: inline-flex; align-items: center; justify-content: center; font-size: 11px; font-weight: var(--font-weight-bold); }
|
|
283
|
+
.scene-content-author { color: var(--text); font-weight: var(--font-weight-medium); }
|
|
284
|
+
.scene-content-dot { color: var(--text-faint); }
|
|
285
|
+
|
|
286
|
+
/* Alerts */
|
|
287
|
+
.scene-stack { display: flex; flex-direction: column; gap: var(--space-2); }
|
|
288
|
+
.scene-alert {
|
|
289
|
+
display: flex;
|
|
290
|
+
gap: var(--space-3);
|
|
291
|
+
padding: var(--space-3) var(--space-4);
|
|
292
|
+
background: var(--surface);
|
|
293
|
+
border: var(--border-width-default) solid var(--border);
|
|
294
|
+
border-left-width: 4px;
|
|
295
|
+
font-size: var(--text-sm);
|
|
296
|
+
color: var(--text);
|
|
297
|
+
}
|
|
298
|
+
.scene-alert i { padding-top: 2px; flex-shrink: 0; }
|
|
299
|
+
.scene-alert.tone-accent { border-left-color: var(--accent); background: var(--accent-bg); }
|
|
300
|
+
.scene-alert.tone-accent i { color: var(--accent); }
|
|
301
|
+
.scene-alert.tone-green { border-left-color: var(--green); background: var(--green-bg); }
|
|
302
|
+
.scene-alert.tone-green i { color: var(--green); }
|
|
303
|
+
.scene-alert.tone-yellow { border-left-color: var(--yellow); background: var(--yellow-bg); }
|
|
304
|
+
.scene-alert.tone-yellow i { color: var(--yellow); }
|
|
305
|
+
|
|
306
|
+
/* Code */
|
|
307
|
+
.scene-code {
|
|
308
|
+
border: var(--border-width-default) solid var(--code-border, var(--border));
|
|
309
|
+
background: var(--code-bg);
|
|
310
|
+
color: var(--code-text);
|
|
311
|
+
font-family: var(--font-mono);
|
|
312
|
+
overflow: hidden;
|
|
313
|
+
border-radius: 0;
|
|
314
|
+
}
|
|
315
|
+
.scene-code-header {
|
|
316
|
+
display: flex;
|
|
317
|
+
justify-content: space-between;
|
|
318
|
+
align-items: center;
|
|
319
|
+
padding: var(--space-2) var(--space-3);
|
|
320
|
+
background: var(--code-header-bg);
|
|
321
|
+
color: var(--code-muted);
|
|
322
|
+
font-size: var(--text-label);
|
|
323
|
+
letter-spacing: var(--tracking-wide);
|
|
324
|
+
text-transform: uppercase;
|
|
325
|
+
border-bottom: var(--border-width-thin) solid var(--code-border, var(--border));
|
|
326
|
+
}
|
|
327
|
+
.scene-code-copy {
|
|
328
|
+
background: none;
|
|
329
|
+
border: var(--border-width-thin) solid var(--code-border, var(--border));
|
|
330
|
+
color: var(--code-muted);
|
|
331
|
+
font-family: var(--font-mono);
|
|
332
|
+
font-size: var(--text-label);
|
|
333
|
+
padding: 2px 8px;
|
|
334
|
+
cursor: pointer;
|
|
335
|
+
}
|
|
336
|
+
.scene-code-body { margin: 0; padding: var(--space-3) var(--space-4); font-size: var(--text-sm); white-space: pre-wrap; overflow: auto; }
|
|
337
|
+
|
|
338
|
+
/* Shadows */
|
|
339
|
+
.scene-shadow-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: var(--space-6); padding: var(--space-4); }
|
|
340
|
+
.scene-shadow-box {
|
|
341
|
+
height: 60px;
|
|
342
|
+
background: var(--surface);
|
|
343
|
+
border: var(--border-width-default) solid var(--border);
|
|
344
|
+
display: flex;
|
|
345
|
+
align-items: center;
|
|
346
|
+
justify-content: center;
|
|
347
|
+
font-family: var(--font-mono);
|
|
348
|
+
font-size: var(--text-label);
|
|
349
|
+
text-transform: uppercase;
|
|
350
|
+
letter-spacing: var(--tracking-wide);
|
|
351
|
+
color: var(--text-dim);
|
|
352
|
+
}
|
|
353
|
+
</style>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Article-style preview. Mimics the long-form prose layout used by
|
|
4
|
+
* blogs, projects, explainers, and docs pages. Useful for tuning
|
|
5
|
+
* typography, link colors, blockquote, list, table, and prose code.
|
|
6
|
+
*/
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<article class="scene-prose">
|
|
11
|
+
<header class="scene-prose-header">
|
|
12
|
+
<span class="scene-prose-eyebrow">PROJECT · OPEN</span>
|
|
13
|
+
<h1 class="scene-prose-title">Building a federated maker community without a platform</h1>
|
|
14
|
+
<p class="scene-prose-deck">
|
|
15
|
+
How CommonPub instances stay sovereign while still talking to Mastodon, Lemmy,
|
|
16
|
+
and each other — and what we learned shipping the first three live sites.
|
|
17
|
+
</p>
|
|
18
|
+
<div class="scene-prose-byline">
|
|
19
|
+
<span class="scene-prose-avatar">M</span>
|
|
20
|
+
<span><strong>moheeb</strong> · <a href="#" class="scene-prose-link">deveco.io</a></span>
|
|
21
|
+
<span class="scene-prose-dot">·</span>
|
|
22
|
+
<span>8 min read</span>
|
|
23
|
+
</div>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<p>
|
|
27
|
+
Most maker communities live on someone else's platform. The platform owns the
|
|
28
|
+
identity, the content, the moderation, and the moment the platform changes
|
|
29
|
+
direction, the community goes with it. That's the failure mode CommonPub is
|
|
30
|
+
built around — every instance is a complete site, federation is opt-in, and
|
|
31
|
+
moving your community is a database export, not a migration ticket.
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<h2 class="scene-prose-h2">Three sites, one codebase</h2>
|
|
35
|
+
<p>
|
|
36
|
+
The reference deployment runs <a href="#" class="scene-prose-link">commonpub.io</a>,
|
|
37
|
+
<a href="#" class="scene-prose-link">deveco.io</a>, and
|
|
38
|
+
<a href="#" class="scene-prose-link">heatsynclabs.io</a> off the same Nuxt layer.
|
|
39
|
+
Each one extends <code class="scene-prose-code-inline">@commonpub/layer</code> and
|
|
40
|
+
overrides only what's specific to the community.
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<blockquote class="scene-prose-quote">
|
|
44
|
+
The schema is the work — everything else follows from it.
|
|
45
|
+
</blockquote>
|
|
46
|
+
|
|
47
|
+
<h3 class="scene-prose-h3">What the layer ships</h3>
|
|
48
|
+
<ul class="scene-prose-list">
|
|
49
|
+
<li>Content types: project, blog, explainer, video, doc page</li>
|
|
50
|
+
<li>Federation: ActivityPub via Fedify, instance + actor signing</li>
|
|
51
|
+
<li>Hubs: local-first groups, with a Group-actor escape hatch</li>
|
|
52
|
+
<li>Admin panel: feature flags, theme picker, audit log, mirror config</li>
|
|
53
|
+
</ul>
|
|
54
|
+
|
|
55
|
+
<h3 class="scene-prose-h3">Sample query</h3>
|
|
56
|
+
<p>The hub feed is built from this Drizzle query:</p>
|
|
57
|
+
<pre class="scene-prose-pre"><code>const items = await db
|
|
58
|
+
.select()
|
|
59
|
+
.from(content)
|
|
60
|
+
.where(eq(content.hubId, hub.id))
|
|
61
|
+
.orderBy(desc(content.publishedAt))
|
|
62
|
+
.limit(20);</code></pre>
|
|
63
|
+
|
|
64
|
+
<h3 class="scene-prose-h3">Deploy targets</h3>
|
|
65
|
+
<table class="scene-prose-table">
|
|
66
|
+
<thead>
|
|
67
|
+
<tr>
|
|
68
|
+
<th>Site</th>
|
|
69
|
+
<th>Federation</th>
|
|
70
|
+
<th>Custom theme</th>
|
|
71
|
+
</tr>
|
|
72
|
+
</thead>
|
|
73
|
+
<tbody>
|
|
74
|
+
<tr><td>commonpub.io</td><td>Yes</td><td>Classic</td></tr>
|
|
75
|
+
<tr><td>deveco.io</td><td>Yes</td><td>devEco brand</td></tr>
|
|
76
|
+
<tr><td>heatsynclabs.io</td><td>Soon</td><td>Hacker green</td></tr>
|
|
77
|
+
</tbody>
|
|
78
|
+
</table>
|
|
79
|
+
|
|
80
|
+
<hr class="scene-prose-hr" />
|
|
81
|
+
<p class="scene-prose-foot">Last updated 2026-05-26 · 8-minute read · Tagged <a href="#" class="scene-prose-link">#federation</a> <a href="#" class="scene-prose-link">#open</a></p>
|
|
82
|
+
</article>
|
|
83
|
+
</template>
|
|
84
|
+
|
|
85
|
+
<style scoped>
|
|
86
|
+
.scene-prose {
|
|
87
|
+
max-width: 640px;
|
|
88
|
+
margin: 0 auto;
|
|
89
|
+
font-family: var(--font-body);
|
|
90
|
+
font-size: var(--text-base);
|
|
91
|
+
line-height: var(--leading-normal);
|
|
92
|
+
color: var(--text);
|
|
93
|
+
}
|
|
94
|
+
.scene-prose-header { margin-bottom: var(--space-8); }
|
|
95
|
+
.scene-prose-eyebrow {
|
|
96
|
+
font-family: var(--font-mono);
|
|
97
|
+
font-size: var(--text-label);
|
|
98
|
+
letter-spacing: var(--tracking-widest);
|
|
99
|
+
text-transform: uppercase;
|
|
100
|
+
color: var(--accent);
|
|
101
|
+
}
|
|
102
|
+
.scene-prose-title { font-family: var(--font-heading); font-size: var(--text-3xl); font-weight: var(--font-weight-bold); letter-spacing: var(--tracking-tight); line-height: var(--leading-tight); color: var(--text); margin: var(--space-3) 0 var(--space-3); }
|
|
103
|
+
.scene-prose-deck { font-size: var(--text-md); color: var(--text-dim); line-height: var(--leading-snug); margin: 0 0 var(--space-4); }
|
|
104
|
+
.scene-prose-byline { display: flex; align-items: center; gap: 8px; color: var(--text-dim); font-size: var(--text-sm); }
|
|
105
|
+
.scene-prose-avatar { width: 28px; height: 28px; border-radius: var(--radius-full); background: var(--accent); color: var(--color-on-accent); display: inline-flex; align-items: center; justify-content: center; font-weight: var(--font-weight-bold); font-size: 12px; }
|
|
106
|
+
.scene-prose-dot { color: var(--text-faint); }
|
|
107
|
+
.scene-prose-h2 { font-family: var(--font-heading); font-size: var(--text-2xl); font-weight: var(--font-weight-semibold); color: var(--text); margin: var(--space-8) 0 var(--space-3); letter-spacing: var(--tracking-tight); }
|
|
108
|
+
.scene-prose-h3 { font-family: var(--font-heading); font-size: var(--text-lg); font-weight: var(--font-weight-semibold); color: var(--text); margin: var(--space-6) 0 var(--space-2); }
|
|
109
|
+
.scene-prose p { margin: 0 0 var(--space-4); }
|
|
110
|
+
.scene-prose-link { color: var(--color-link); text-decoration: underline; text-underline-offset: 2px; }
|
|
111
|
+
.scene-prose-link:hover { color: var(--color-link-hover); }
|
|
112
|
+
.scene-prose-quote {
|
|
113
|
+
border-left: 4px solid var(--accent);
|
|
114
|
+
margin: var(--space-6) 0;
|
|
115
|
+
padding: var(--space-2) var(--space-5);
|
|
116
|
+
font-family: var(--font-heading);
|
|
117
|
+
font-size: var(--text-lg);
|
|
118
|
+
font-style: italic;
|
|
119
|
+
color: var(--text-dim);
|
|
120
|
+
background: var(--accent-bg);
|
|
121
|
+
}
|
|
122
|
+
.scene-prose-code-inline { font-family: var(--font-mono); font-size: 0.9em; padding: 1px 6px; background: var(--surface2); border: var(--border-width-thin) solid var(--border2); color: var(--accent); }
|
|
123
|
+
.scene-prose-pre {
|
|
124
|
+
background: var(--code-bg);
|
|
125
|
+
color: var(--code-text);
|
|
126
|
+
padding: var(--space-4);
|
|
127
|
+
margin: var(--space-4) 0 var(--space-6);
|
|
128
|
+
font-family: var(--font-mono);
|
|
129
|
+
font-size: var(--text-sm);
|
|
130
|
+
overflow: auto;
|
|
131
|
+
border: var(--border-width-thin) solid var(--code-border);
|
|
132
|
+
}
|
|
133
|
+
.scene-prose-list { padding-left: var(--space-5); margin: 0 0 var(--space-4); }
|
|
134
|
+
.scene-prose-list li { margin-bottom: var(--space-2); }
|
|
135
|
+
.scene-prose-table { width: 100%; border-collapse: collapse; margin: var(--space-4) 0 var(--space-6); }
|
|
136
|
+
.scene-prose-table th, .scene-prose-table td { padding: var(--space-2) var(--space-3); border-bottom: var(--border-width-thin) solid var(--border2); text-align: left; font-size: var(--text-sm); }
|
|
137
|
+
.scene-prose-table th { font-family: var(--font-mono); font-size: var(--text-label); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--text-dim); background: var(--surface2); border-bottom: var(--border-width-default) solid var(--border); }
|
|
138
|
+
.scene-prose-hr { border: 0; border-top: var(--border-width-default) solid var(--border2); margin: var(--space-8) 0; }
|
|
139
|
+
.scene-prose-foot { color: var(--text-faint); font-size: var(--text-sm); }
|
|
140
|
+
</style>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Collapsible group of token rows. Renders the group header + a list of
|
|
4
|
+
* AdminThemeTokenInput inside an open <details>. The "modified" count
|
|
5
|
+
* badge on the header tells the user which groups they've touched.
|
|
6
|
+
*/
|
|
7
|
+
import { computed } from 'vue';
|
|
8
|
+
import type { TokenSpec, TokenGroup } from '@commonpub/ui';
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
group: TokenGroup;
|
|
12
|
+
label: string;
|
|
13
|
+
icon: string;
|
|
14
|
+
description: string;
|
|
15
|
+
specs: TokenSpec[];
|
|
16
|
+
tokens: Record<string, string>;
|
|
17
|
+
/** Default open state. Surfaces (top group) opens by default. */
|
|
18
|
+
open?: boolean;
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
const emit = defineEmits<{
|
|
22
|
+
update: [key: string, value: string];
|
|
23
|
+
reset: [key: string];
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
const modifiedCount = computed(() =>
|
|
27
|
+
props.specs.reduce((acc, s) => acc + (props.tokens[s.key] && props.tokens[s.key] !== s.default ? 1 : 0), 0),
|
|
28
|
+
);
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<details class="token-group" :open="open">
|
|
33
|
+
<summary class="token-group-header">
|
|
34
|
+
<i :class="['fa-solid', icon, 'token-group-icon']" aria-hidden="true" />
|
|
35
|
+
<div class="token-group-meta">
|
|
36
|
+
<span class="token-group-label">{{ label }}</span>
|
|
37
|
+
<span class="token-group-desc">{{ description }}</span>
|
|
38
|
+
</div>
|
|
39
|
+
<span v-if="modifiedCount > 0" class="token-group-count">{{ modifiedCount }}</span>
|
|
40
|
+
<i class="fa-solid fa-chevron-right token-group-chevron" aria-hidden="true" />
|
|
41
|
+
</summary>
|
|
42
|
+
|
|
43
|
+
<div class="token-group-body">
|
|
44
|
+
<AdminThemeTokenInput
|
|
45
|
+
v-for="spec in specs"
|
|
46
|
+
:key="spec.key"
|
|
47
|
+
:spec="spec"
|
|
48
|
+
:value="tokens[spec.key] ?? ''"
|
|
49
|
+
@update="(v) => emit('update', spec.key, v)"
|
|
50
|
+
@reset="emit('reset', spec.key)"
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
</details>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<style scoped>
|
|
57
|
+
.token-group {
|
|
58
|
+
border-bottom: var(--border-width-default) solid var(--border2);
|
|
59
|
+
}
|
|
60
|
+
.token-group:last-of-type { border-bottom: 0; }
|
|
61
|
+
|
|
62
|
+
.token-group-header {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
gap: var(--space-3);
|
|
66
|
+
padding: var(--space-3) var(--space-3);
|
|
67
|
+
background: var(--surface);
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
user-select: none;
|
|
70
|
+
list-style: none;
|
|
71
|
+
}
|
|
72
|
+
.token-group-header::-webkit-details-marker { display: none; }
|
|
73
|
+
.token-group-header:hover { background: var(--surface2); }
|
|
74
|
+
|
|
75
|
+
.token-group-icon { color: var(--text-dim); font-size: 14px; width: 16px; text-align: center; flex-shrink: 0; }
|
|
76
|
+
.token-group-meta { flex: 1; display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
|
77
|
+
.token-group-label { font-size: var(--text-sm); font-weight: var(--font-weight-semibold); color: var(--text); }
|
|
78
|
+
.token-group-desc { font-size: var(--text-xs); color: var(--text-faint); line-height: var(--leading-snug); }
|
|
79
|
+
|
|
80
|
+
.token-group-count {
|
|
81
|
+
font-family: var(--font-mono);
|
|
82
|
+
font-size: 10px;
|
|
83
|
+
letter-spacing: var(--tracking-wide);
|
|
84
|
+
padding: 1px 6px;
|
|
85
|
+
background: var(--accent-bg);
|
|
86
|
+
color: var(--accent);
|
|
87
|
+
border: var(--border-width-thin) solid var(--accent-border);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.token-group-chevron {
|
|
91
|
+
font-size: 11px;
|
|
92
|
+
color: var(--text-faint);
|
|
93
|
+
transition: transform var(--transition-fast);
|
|
94
|
+
}
|
|
95
|
+
[open] > .token-group-header .token-group-chevron { transform: rotate(90deg); }
|
|
96
|
+
|
|
97
|
+
.token-group-body { background: var(--bg); }
|
|
98
|
+
</style>
|