@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.
@@ -0,0 +1,218 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Live preview pane for the theme editor. Hosts the scene picker, the
4
+ * light/dark mode toggle, and the scrollable scene surface that renders
5
+ * the in-progress theme tokens applied to representative components.
6
+ *
7
+ * The scene system is pluggable: each scene is a Vue component rendered
8
+ * inside the token-scoped wrapper. Add scenes by registering them in
9
+ * PREVIEW_SCENES and dropping an `AdminThemeScene*.vue` in this directory.
10
+ *
11
+ * Future scenes the architecture is built to absorb:
12
+ * - 'iframe-route' — render an actual site route with the in-progress theme
13
+ * - 'page-layout' — full landing-page mockup with editable section list
14
+ * - 'layout-builder' — drag-and-drop section composer
15
+ */
16
+ import { computed, ref } from 'vue';
17
+
18
+ const props = defineProps<{
19
+ tokens: Record<string, string>;
20
+ /** The base theme whose CSS file provides inherited defaults (via data-theme). */
21
+ parentTheme: string;
22
+ isDark: boolean;
23
+ }>();
24
+
25
+ interface SceneOption {
26
+ id: 'gallery' | 'prose' | 'admin';
27
+ label: string;
28
+ description: string;
29
+ icon: string;
30
+ }
31
+
32
+ const PREVIEW_SCENES: SceneOption[] = [
33
+ { id: 'gallery', label: 'Components', description: 'Buttons, cards, forms, badges, prose, code', icon: 'fa-th-large' },
34
+ { id: 'prose', label: 'Article', description: 'Headings, paragraphs, quote, code block, list', icon: 'fa-file-lines' },
35
+ { id: 'admin', label: 'Admin shell', description: 'Topbar, sidebar, table, stat cards', icon: 'fa-gauge' },
36
+ ];
37
+
38
+ const activeScene = ref<SceneOption['id']>('gallery');
39
+ const previewMode = ref<'light' | 'dark'>(props.isDark ? 'dark' : 'light');
40
+
41
+ /**
42
+ * Map every parent-theme id to its family's light + dark variant. Mirrors
43
+ * `layers/base/utils/themeConfig.ts` THEME_TO_FAMILY + FAMILY_VARIANTS,
44
+ * inlined here so the preview pane doesn't need to import the SSR-side
45
+ * utils. Custom-theme parents (`cpub-custom-*`) and any unknown id fall
46
+ * back to the classic family — the user's tokens override on top regardless.
47
+ */
48
+ const FAMILY_VARIANT_OF: Record<string, { light: string; dark: string }> = {
49
+ base: { light: 'base', dark: 'dark' },
50
+ dark: { light: 'base', dark: 'dark' },
51
+ agora: { light: 'agora', dark: 'agora-dark' },
52
+ 'agora-dark': { light: 'agora', dark: 'agora-dark' },
53
+ generics: { light: 'generics', dark: 'generics' },
54
+ };
55
+
56
+ /**
57
+ * The actual `data-theme` attribute applied to the preview surface,
58
+ * resolved from `parentTheme` + `previewMode`. Returns `undefined` for the
59
+ * base/light case (no attribute = `:root` rules apply natively, matching
60
+ * the convention used by `applyThemeToElement` elsewhere).
61
+ *
62
+ * **Bug fix from 0.22.0**: previously `:data-theme="parentTheme"` was
63
+ * hardcoded, so the Light/Dark toggle updated a ref but never re-rendered
64
+ * the preview. Now the toggle actually swaps the rendered theme.
65
+ */
66
+ const effectiveDataTheme = computed<string | undefined>(() => {
67
+ const variants = FAMILY_VARIANT_OF[props.parentTheme] ?? FAMILY_VARIANT_OF.base!;
68
+ const v = previewMode.value === 'dark' ? variants.dark : variants.light;
69
+ return v === 'base' ? undefined : v;
70
+ });
71
+
72
+ /**
73
+ * Build the inline style string scoped to the preview surface. We apply
74
+ * tokens to the wrapper element only — that scopes the in-progress theme
75
+ * to the preview and avoids leaking it into the surrounding admin UI.
76
+ */
77
+ const previewStyle = computed(() => {
78
+ const lines: string[] = [];
79
+ for (const [k, v] of Object.entries(props.tokens)) {
80
+ if (typeof v !== 'string') continue;
81
+ const safeKey = k.replace(/[^a-zA-Z0-9_-]/g, '');
82
+ const safeVal = v.replace(/[\r\n;]/g, ' ');
83
+ if (!safeKey) continue;
84
+ lines.push(`--${safeKey}: ${safeVal}`);
85
+ }
86
+ return lines.join('; ');
87
+ });
88
+ </script>
89
+
90
+ <template>
91
+ <div class="theme-preview-pane">
92
+ <header class="theme-preview-header">
93
+ <div class="theme-preview-scene-picker" role="tablist" aria-label="Preview scene">
94
+ <button
95
+ v-for="scene in PREVIEW_SCENES"
96
+ :key="scene.id"
97
+ type="button"
98
+ role="tab"
99
+ :aria-selected="activeScene === scene.id"
100
+ class="theme-preview-scene-tab"
101
+ :class="{ active: activeScene === scene.id }"
102
+ :title="scene.description"
103
+ @click="activeScene = scene.id"
104
+ >
105
+ <i :class="['fa-solid', scene.icon]" aria-hidden="true" />
106
+ <span>{{ scene.label }}</span>
107
+ </button>
108
+ </div>
109
+
110
+ <div class="theme-preview-mode-toggle" role="radiogroup" aria-label="Preview mode">
111
+ <button
112
+ type="button"
113
+ role="radio"
114
+ :aria-checked="previewMode === 'light'"
115
+ class="theme-preview-mode-btn"
116
+ :class="{ active: previewMode === 'light' }"
117
+ @click="previewMode = 'light'"
118
+ >
119
+ <i class="fa-solid fa-sun" aria-hidden="true" /> Light
120
+ </button>
121
+ <button
122
+ type="button"
123
+ role="radio"
124
+ :aria-checked="previewMode === 'dark'"
125
+ class="theme-preview-mode-btn"
126
+ :class="{ active: previewMode === 'dark' }"
127
+ @click="previewMode = 'dark'"
128
+ >
129
+ <i class="fa-solid fa-moon" aria-hidden="true" /> Dark
130
+ </button>
131
+ </div>
132
+ </header>
133
+
134
+ <div
135
+ class="theme-preview-surface"
136
+ :data-theme="effectiveDataTheme"
137
+ :style="previewStyle"
138
+ :data-preview-mode="previewMode"
139
+ >
140
+ <AdminThemeSceneGallery v-if="activeScene === 'gallery'" />
141
+ <AdminThemeSceneProse v-else-if="activeScene === 'prose'" />
142
+ <AdminThemeSceneAdmin v-else-if="activeScene === 'admin'" />
143
+ </div>
144
+ </div>
145
+ </template>
146
+
147
+ <style scoped>
148
+ .theme-preview-pane {
149
+ display: flex;
150
+ flex-direction: column;
151
+ height: 100%;
152
+ min-height: 0;
153
+ background: var(--surface2);
154
+ border-left: var(--border-width-default) solid var(--border);
155
+ }
156
+
157
+ .theme-preview-header {
158
+ display: flex;
159
+ justify-content: space-between;
160
+ align-items: center;
161
+ padding: var(--space-2) var(--space-3);
162
+ background: var(--surface);
163
+ border-bottom: var(--border-width-default) solid var(--border);
164
+ flex-wrap: wrap;
165
+ gap: var(--space-2);
166
+ }
167
+
168
+ .theme-preview-scene-picker,
169
+ .theme-preview-mode-toggle {
170
+ display: flex;
171
+ gap: 2px;
172
+ background: var(--surface2);
173
+ padding: 2px;
174
+ border: var(--border-width-thin) solid var(--border2);
175
+ }
176
+
177
+ .theme-preview-scene-tab,
178
+ .theme-preview-mode-btn {
179
+ display: inline-flex;
180
+ align-items: center;
181
+ gap: 6px;
182
+ padding: 6px 10px;
183
+ background: none;
184
+ border: 0;
185
+ font-family: var(--font-mono);
186
+ font-size: 11px;
187
+ letter-spacing: var(--tracking-wide);
188
+ text-transform: uppercase;
189
+ color: var(--text-dim);
190
+ cursor: pointer;
191
+ border-radius: 0;
192
+ }
193
+ .theme-preview-scene-tab:hover,
194
+ .theme-preview-mode-btn:hover { color: var(--text); }
195
+ .theme-preview-scene-tab.active {
196
+ background: var(--surface);
197
+ color: var(--text);
198
+ box-shadow: inset 0 -2px 0 var(--accent);
199
+ }
200
+ .theme-preview-mode-btn.active {
201
+ background: var(--surface);
202
+ color: var(--accent);
203
+ }
204
+ .theme-preview-scene-tab i { font-size: 10px; }
205
+
206
+ .theme-preview-surface {
207
+ flex: 1;
208
+ overflow: auto;
209
+ padding: var(--space-4);
210
+ background-color: var(--bg);
211
+ color: var(--text);
212
+ font-family: var(--font-body);
213
+ /* Token re-application happens via inline style on this element. The
214
+ `data-theme` attr seeds inherited defaults; inline style overrides
215
+ each token the editor has changed. */
216
+ min-height: 0;
217
+ }
218
+ </style>
@@ -0,0 +1,189 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Admin-shell preview. Renders a fake topbar + sidebar + stat dashboard
4
+ * + table so admins can preview what the panel they're using right now
5
+ * will look like with the in-progress theme applied.
6
+ */
7
+ </script>
8
+
9
+ <template>
10
+ <div class="scene-admin">
11
+ <header class="scene-admin-top">
12
+ <span class="scene-admin-brand">{{ '{ Instance Name }' }}</span>
13
+ <span class="scene-admin-badge">Admin</span>
14
+ <span class="scene-admin-back">← Back to site</span>
15
+ </header>
16
+
17
+ <div class="scene-admin-body">
18
+ <nav class="scene-admin-side">
19
+ <a class="scene-admin-link"><i class="fa-solid fa-gauge" aria-hidden="true" />Dashboard</a>
20
+ <a class="scene-admin-link"><i class="fa-solid fa-users" aria-hidden="true" />Users</a>
21
+ <a class="scene-admin-link"><i class="fa-solid fa-newspaper" aria-hidden="true" />Content</a>
22
+ <a class="scene-admin-link active"><i class="fa-solid fa-palette" aria-hidden="true" />Theme</a>
23
+ <a class="scene-admin-link"><i class="fa-solid fa-globe" aria-hidden="true" />Federation</a>
24
+ <a class="scene-admin-link"><i class="fa-solid fa-toggle-on" aria-hidden="true" />Features</a>
25
+ </nav>
26
+
27
+ <main class="scene-admin-main">
28
+ <h1 class="scene-admin-title">Instance overview</h1>
29
+ <p class="scene-admin-sub">Snapshot of activity across this CommonPub instance.</p>
30
+
31
+ <div class="scene-admin-stats">
32
+ <div class="scene-admin-stat">
33
+ <span class="scene-admin-stat-label">Members</span>
34
+ <span class="scene-admin-stat-value">3,124</span>
35
+ <span class="scene-admin-stat-delta">+18 this week</span>
36
+ </div>
37
+ <div class="scene-admin-stat">
38
+ <span class="scene-admin-stat-label">Published</span>
39
+ <span class="scene-admin-stat-value">512</span>
40
+ <span class="scene-admin-stat-delta">+24 this week</span>
41
+ </div>
42
+ <div class="scene-admin-stat">
43
+ <span class="scene-admin-stat-label">Federated</span>
44
+ <span class="scene-admin-stat-value">87 peers</span>
45
+ <span class="scene-admin-stat-delta tone-warn">2 retrying</span>
46
+ </div>
47
+ <div class="scene-admin-stat">
48
+ <span class="scene-admin-stat-label">Storage</span>
49
+ <span class="scene-admin-stat-value">8.2 GB</span>
50
+ <span class="scene-admin-stat-delta">of 50 GB</span>
51
+ </div>
52
+ </div>
53
+
54
+ <h2 class="scene-admin-h2">Recent reports</h2>
55
+ <div class="scene-admin-table-wrap">
56
+ <table class="scene-admin-table">
57
+ <thead>
58
+ <tr><th>Target</th><th>Reason</th><th>Reporter</th><th>Status</th></tr>
59
+ </thead>
60
+ <tbody>
61
+ <tr>
62
+ <td><strong>@spam-actor@example.fed</strong></td>
63
+ <td>Spam</td>
64
+ <td>moheeb</td>
65
+ <td><span class="scene-admin-pill tone-yellow">Open</span></td>
66
+ </tr>
67
+ <tr>
68
+ <td>Project: "Buy followers fast"</td>
69
+ <td>Spam</td>
70
+ <td>aly</td>
71
+ <td><span class="scene-admin-pill tone-green">Resolved</span></td>
72
+ </tr>
73
+ <tr>
74
+ <td>Comment on "Edge AI nodes"</td>
75
+ <td>Harassment</td>
76
+ <td>kim</td>
77
+ <td><span class="scene-admin-pill tone-red">Escalated</span></td>
78
+ </tr>
79
+ </tbody>
80
+ </table>
81
+ </div>
82
+ </main>
83
+ </div>
84
+ </div>
85
+ </template>
86
+
87
+ <style scoped>
88
+ .scene-admin {
89
+ background: var(--bg);
90
+ border: var(--border-width-default) solid var(--border);
91
+ min-height: 480px;
92
+ overflow: hidden;
93
+ display: flex;
94
+ flex-direction: column;
95
+ }
96
+
97
+ .scene-admin-top {
98
+ height: 44px;
99
+ display: flex;
100
+ align-items: center;
101
+ gap: var(--space-3);
102
+ padding: 0 var(--space-4);
103
+ background: var(--surface);
104
+ border-bottom: var(--border-width-default) solid var(--border);
105
+ }
106
+ .scene-admin-brand { font-weight: var(--font-weight-bold); color: var(--text); }
107
+ .scene-admin-badge {
108
+ padding: 2px 8px;
109
+ background: var(--accent);
110
+ color: var(--color-on-accent);
111
+ font-family: var(--font-mono);
112
+ font-size: 10px;
113
+ letter-spacing: var(--tracking-wide);
114
+ text-transform: uppercase;
115
+ }
116
+ .scene-admin-back { margin-left: auto; color: var(--text-dim); font-size: var(--text-sm); }
117
+
118
+ .scene-admin-body { display: flex; flex: 1; min-height: 0; }
119
+ .scene-admin-side {
120
+ width: 180px;
121
+ padding: var(--space-3) var(--space-2);
122
+ background: var(--surface);
123
+ border-right: var(--border-width-default) solid var(--border);
124
+ display: flex;
125
+ flex-direction: column;
126
+ gap: 2px;
127
+ }
128
+ .scene-admin-link {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 10px;
132
+ padding: 6px var(--space-3);
133
+ color: var(--text-dim);
134
+ font-size: var(--text-sm);
135
+ cursor: pointer;
136
+ text-decoration: none;
137
+ }
138
+ .scene-admin-link i { width: 14px; text-align: center; font-size: 11px; }
139
+ .scene-admin-link:hover { color: var(--text); background: var(--surface2); }
140
+ .scene-admin-link.active { color: var(--accent); background: var(--accent-bg); font-weight: var(--font-weight-semibold); }
141
+
142
+ .scene-admin-main { flex: 1; padding: var(--space-5); min-width: 0; }
143
+ .scene-admin-title { font-size: var(--text-xl); font-weight: var(--font-weight-bold); color: var(--text); margin: 0 0 var(--space-1); }
144
+ .scene-admin-sub { color: var(--text-dim); margin: 0 0 var(--space-5); }
145
+
146
+ .scene-admin-stats {
147
+ display: grid;
148
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
149
+ gap: var(--space-3);
150
+ margin-bottom: var(--space-6);
151
+ }
152
+ .scene-admin-stat {
153
+ background: var(--surface);
154
+ border: var(--border-width-default) solid var(--border);
155
+ padding: var(--space-3) var(--space-4);
156
+ display: flex;
157
+ flex-direction: column;
158
+ gap: 2px;
159
+ box-shadow: var(--shadow-sm);
160
+ }
161
+ .scene-admin-stat-label { font-family: var(--font-mono); font-size: var(--text-label); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--text-dim); }
162
+ .scene-admin-stat-value { font-size: var(--text-xl); font-weight: var(--font-weight-bold); color: var(--text); }
163
+ .scene-admin-stat-delta { font-size: var(--text-sm); color: var(--green); }
164
+ .scene-admin-stat-delta.tone-warn { color: var(--yellow); }
165
+
166
+ .scene-admin-h2 { font-size: var(--text-md); font-weight: var(--font-weight-semibold); color: var(--text); margin: 0 0 var(--space-2); }
167
+ .scene-admin-table-wrap { background: var(--surface); border: var(--border-width-default) solid var(--border); overflow: hidden; }
168
+ .scene-admin-table { width: 100%; border-collapse: collapse; }
169
+ .scene-admin-table th, .scene-admin-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); color: var(--text); }
170
+ .scene-admin-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); }
171
+ .scene-admin-table tr:last-child td { border-bottom: 0; }
172
+ .scene-admin-pill {
173
+ display: inline-block;
174
+ padding: 1px 8px;
175
+ font-family: var(--font-mono);
176
+ font-size: 10px;
177
+ letter-spacing: var(--tracking-wide);
178
+ text-transform: uppercase;
179
+ border: var(--border-width-thin) solid var(--border2);
180
+ }
181
+ .scene-admin-pill.tone-green { color: var(--green); background: var(--green-bg); border-color: var(--green-border); }
182
+ .scene-admin-pill.tone-yellow { color: var(--yellow); background: var(--yellow-bg); border-color: var(--yellow-border); }
183
+ .scene-admin-pill.tone-red { color: var(--red); background: var(--red-bg); border-color: var(--red-border); }
184
+
185
+ @media (max-width: 540px) {
186
+ .scene-admin-side { width: 50px; }
187
+ .scene-admin-link span { display: none; }
188
+ }
189
+ </style>