@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,277 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * One row in the admin theme picker. Displays:
4
+ * • light + dark swatches (each clickable to pick that variant directly)
5
+ * • family name + description
6
+ * • active / current pill
7
+ * • action buttons: Edit (custom only), Duplicate, Export, Delete
8
+ *
9
+ * Stays presentational — all wiring (select / edit / duplicate / delete /
10
+ * export) is emitted up so the page owns the state machine.
11
+ */
12
+ import type { ThemeFamilyView } from '../../../types/theme';
13
+
14
+ defineProps<{
15
+ family: ThemeFamilyView;
16
+ active: boolean;
17
+ saving: boolean;
18
+ }>();
19
+
20
+ const emit = defineEmits<{
21
+ select: [themeId: string];
22
+ edit: [themeId: string];
23
+ duplicate: [themeId: string];
24
+ exportTheme: [themeId: string];
25
+ remove: [themeId: string];
26
+ }>();
27
+
28
+ function variantBoxStyle(v: { bg: string; surface: string; accent: string; text: string; border: string }) {
29
+ return {
30
+ backgroundColor: v.bg,
31
+ borderColor: v.border,
32
+ };
33
+ }
34
+
35
+ function variantInnerStyle(v: { bg: string; surface: string; accent: string; text: string; border: string }) {
36
+ return {
37
+ backgroundColor: v.surface,
38
+ borderColor: v.border,
39
+ boxShadow: `3px 3px 0 ${v.border}`,
40
+ };
41
+ }
42
+
43
+ function badge(family: ThemeFamilyView): { label: string; tone: 'builtin' | 'registered' | 'custom' } {
44
+ if (family.source === 'custom') return { label: 'Custom', tone: 'custom' };
45
+ if (family.source === 'registered') return { label: 'From code', tone: 'registered' };
46
+ return { label: 'Built-in', tone: 'builtin' };
47
+ }
48
+ </script>
49
+
50
+ <template>
51
+ <article class="theme-family-card" :class="{ active }">
52
+ <div class="theme-family-previews">
53
+ <button
54
+ v-if="family.light"
55
+ type="button"
56
+ class="theme-family-preview"
57
+ :style="variantBoxStyle(family.preview.light)"
58
+ :disabled="saving"
59
+ :aria-label="`Select ${family.name} light`"
60
+ @click="emit('select', family.light.id)"
61
+ >
62
+ <div class="theme-family-preview-card" :style="variantInnerStyle(family.preview.light)">
63
+ <div class="theme-family-preview-heading" :style="{ backgroundColor: family.preview.light.text, opacity: 0.85 }" />
64
+ <div class="theme-family-preview-text" :style="{ backgroundColor: family.preview.light.text, opacity: 0.35 }" />
65
+ <div class="theme-family-preview-text" :style="{ backgroundColor: family.preview.light.text, opacity: 0.35, width: '60%' }" />
66
+ <div class="theme-family-preview-accent" :style="{ backgroundColor: family.preview.light.accent }" />
67
+ </div>
68
+ <span class="theme-family-mode-label">Light</span>
69
+ </button>
70
+
71
+ <button
72
+ v-if="family.dark"
73
+ type="button"
74
+ class="theme-family-preview theme-family-preview-dark"
75
+ :style="variantBoxStyle(family.preview.dark)"
76
+ :disabled="saving"
77
+ :aria-label="`Select ${family.name} dark`"
78
+ @click="emit('select', family.dark.id)"
79
+ >
80
+ <div class="theme-family-preview-card" :style="variantInnerStyle(family.preview.dark)">
81
+ <div class="theme-family-preview-heading" :style="{ backgroundColor: family.preview.dark.text, opacity: 0.85 }" />
82
+ <div class="theme-family-preview-text" :style="{ backgroundColor: family.preview.dark.text, opacity: 0.35 }" />
83
+ <div class="theme-family-preview-text" :style="{ backgroundColor: family.preview.dark.text, opacity: 0.35, width: '60%' }" />
84
+ <div class="theme-family-preview-accent" :style="{ backgroundColor: family.preview.dark.accent }" />
85
+ </div>
86
+ <span class="theme-family-mode-label">Dark</span>
87
+ </button>
88
+ </div>
89
+
90
+ <div class="theme-family-meta">
91
+ <div class="theme-family-meta-head">
92
+ <h3 class="theme-family-name">{{ family.name }}</h3>
93
+ <span class="theme-family-tag" :class="`tag-${badge(family).tone}`">{{ badge(family).label }}</span>
94
+ <span v-if="active" class="theme-family-active">
95
+ <i class="fa-solid fa-check" aria-hidden="true" /> Active
96
+ </span>
97
+ </div>
98
+ <p class="theme-family-desc">{{ family.description }}</p>
99
+
100
+ <div class="theme-family-actions">
101
+ <button
102
+ v-if="family.source === 'custom'"
103
+ type="button"
104
+ class="cpub-btn cpub-btn-sm"
105
+ @click="emit('edit', family.light?.id ?? family.dark!.id)"
106
+ >
107
+ <i class="fa-solid fa-pen-to-square" aria-hidden="true" /> Edit
108
+ </button>
109
+ <button
110
+ type="button"
111
+ class="cpub-btn cpub-btn-sm"
112
+ :title="family.source === 'custom' ? 'Duplicate this theme' : 'Fork to a new editable custom theme'"
113
+ @click="emit('duplicate', family.light?.id ?? family.dark!.id)"
114
+ >
115
+ <i class="fa-solid fa-copy" aria-hidden="true" /> {{ family.source === 'custom' ? 'Duplicate' : 'Fork' }}
116
+ </button>
117
+ <button
118
+ v-if="family.source === 'custom'"
119
+ type="button"
120
+ class="cpub-btn cpub-btn-sm"
121
+ @click="emit('exportTheme', family.light?.id ?? family.dark!.id)"
122
+ >
123
+ <i class="fa-solid fa-file-export" aria-hidden="true" /> Export
124
+ </button>
125
+ <button
126
+ v-if="family.source === 'custom'"
127
+ type="button"
128
+ class="cpub-btn cpub-btn-sm theme-family-action-danger"
129
+ @click="emit('remove', family.light?.id ?? family.dark!.id)"
130
+ >
131
+ <i class="fa-solid fa-trash" aria-hidden="true" /> Delete
132
+ </button>
133
+ </div>
134
+ </div>
135
+ </article>
136
+ </template>
137
+
138
+ <style scoped>
139
+ .theme-family-card {
140
+ display: grid;
141
+ grid-template-columns: auto 1fr;
142
+ border: var(--border-width-default) solid var(--border2);
143
+ background: var(--surface);
144
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
145
+ }
146
+
147
+ .theme-family-card.active {
148
+ border-color: var(--accent);
149
+ box-shadow: var(--shadow-accent);
150
+ }
151
+
152
+ .theme-family-card:hover { border-color: var(--border); }
153
+
154
+ .theme-family-previews {
155
+ display: flex;
156
+ border-right: var(--border-width-default) solid var(--border2);
157
+ }
158
+
159
+ .theme-family-preview {
160
+ position: relative;
161
+ width: 130px;
162
+ height: 110px;
163
+ padding: var(--space-3);
164
+ display: flex;
165
+ flex-direction: column;
166
+ align-items: center;
167
+ justify-content: center;
168
+ border: 0;
169
+ border-right: var(--border-width-default) solid var(--border2);
170
+ cursor: pointer;
171
+ border-radius: 0;
172
+ }
173
+
174
+ .theme-family-preview:last-child { border-right: 0; }
175
+ .theme-family-preview:hover { filter: brightness(1.05); }
176
+ .theme-family-preview:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
177
+ .theme-family-preview:disabled { cursor: wait; opacity: 0.6; }
178
+
179
+ .theme-family-preview-card {
180
+ width: 80%;
181
+ padding: var(--space-2) var(--space-3);
182
+ border-width: 2px;
183
+ border-style: solid;
184
+ display: flex;
185
+ flex-direction: column;
186
+ gap: 4px;
187
+ border-radius: 0;
188
+ }
189
+
190
+ .theme-family-preview-heading { height: 5px; width: 55%; border-radius: 0; }
191
+ .theme-family-preview-text { height: 3px; width: 85%; border-radius: 0; }
192
+ .theme-family-preview-accent { height: 10px; width: 40%; margin-top: 4px; border-radius: 0; }
193
+
194
+ .theme-family-mode-label {
195
+ position: absolute;
196
+ bottom: 4px;
197
+ right: 6px;
198
+ font-family: var(--font-mono);
199
+ font-size: 9px;
200
+ letter-spacing: var(--tracking-wide);
201
+ text-transform: uppercase;
202
+ color: var(--text-faint);
203
+ opacity: 0.7;
204
+ }
205
+
206
+ .theme-family-meta {
207
+ padding: var(--space-4);
208
+ display: flex;
209
+ flex-direction: column;
210
+ gap: var(--space-2);
211
+ min-width: 0;
212
+ }
213
+
214
+ .theme-family-meta-head {
215
+ display: flex;
216
+ align-items: center;
217
+ gap: var(--space-2);
218
+ flex-wrap: wrap;
219
+ }
220
+
221
+ .theme-family-name {
222
+ font-size: var(--text-md);
223
+ font-weight: var(--font-weight-bold);
224
+ margin: 0;
225
+ }
226
+
227
+ .theme-family-tag {
228
+ font-family: var(--font-mono);
229
+ font-size: 10px;
230
+ letter-spacing: var(--tracking-wide);
231
+ text-transform: uppercase;
232
+ padding: 2px 6px;
233
+ border: var(--border-width-thin) solid var(--border2);
234
+ color: var(--text-dim);
235
+ }
236
+ .tag-custom { color: var(--accent); border-color: var(--accent-border); background: var(--accent-bg); }
237
+ .tag-registered { color: var(--purple); border-color: var(--purple-border); background: var(--purple-bg); }
238
+
239
+ .theme-family-active {
240
+ margin-left: auto;
241
+ display: inline-flex;
242
+ align-items: center;
243
+ gap: 4px;
244
+ font-family: var(--font-mono);
245
+ font-size: 10px;
246
+ letter-spacing: var(--tracking-wide);
247
+ text-transform: uppercase;
248
+ color: var(--accent);
249
+ font-weight: var(--font-weight-semibold);
250
+ }
251
+
252
+ .theme-family-desc {
253
+ font-size: var(--text-sm);
254
+ color: var(--text-dim);
255
+ margin: 0;
256
+ line-height: var(--leading-snug);
257
+ }
258
+
259
+ .theme-family-actions {
260
+ display: flex;
261
+ gap: var(--space-2);
262
+ flex-wrap: wrap;
263
+ margin-top: auto;
264
+ }
265
+
266
+ .theme-family-action-danger {
267
+ color: var(--red);
268
+ border-color: var(--red-border);
269
+ }
270
+ .theme-family-action-danger:hover { background: var(--red-bg); }
271
+
272
+ @media (max-width: 640px) {
273
+ .theme-family-card { grid-template-columns: 1fr; }
274
+ .theme-family-previews { border-right: 0; border-bottom: var(--border-width-default) solid var(--border2); }
275
+ .theme-family-preview { flex: 1; }
276
+ }
277
+ </style>
@@ -0,0 +1,222 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Legacy "token overrides" panel — applies on top of whichever theme is
4
+ * active. Useful for one-off tweaks ("flip the accent for our anniversary
5
+ * week") without authoring a full custom theme. For real edits, the
6
+ * editor at `/admin/theme/edit/[id]` is the better tool.
7
+ *
8
+ * Self-contained: takes the initial map from a prop, owns the in-progress
9
+ * draft, emits `save` with the final map. The parent persists via
10
+ * `/api/admin/settings` and decides when to refresh.
11
+ */
12
+ import { computed, ref, watch } from 'vue';
13
+
14
+ const props = defineProps<{
15
+ /** Initial map from `instance_settings.theme.token_overrides`. */
16
+ initial: Record<string, string>;
17
+ /** Disables save buttons while a parent-driven request is in flight. */
18
+ saving: boolean;
19
+ }>();
20
+
21
+ const emit = defineEmits<{
22
+ save: [overrides: Record<string, string>];
23
+ }>();
24
+
25
+ const draft = ref<Record<string, string>>({ ...props.initial });
26
+ const newKey = ref('');
27
+ const newValue = ref('');
28
+
29
+ watch(() => props.initial, (next) => {
30
+ // Reset draft when the parent reloads settings (e.g. after a save round-trip)
31
+ draft.value = { ...next };
32
+ });
33
+
34
+ const count = computed(() => Object.keys(draft.value).length);
35
+ const dirty = computed(() => JSON.stringify(draft.value) !== JSON.stringify(props.initial));
36
+
37
+ function addOverride(): void {
38
+ const k = newKey.value.trim().replace(/^--/, '');
39
+ const v = newValue.value.trim();
40
+ if (!k || !v) return;
41
+ draft.value = { ...draft.value, [k]: v };
42
+ newKey.value = '';
43
+ newValue.value = '';
44
+ }
45
+
46
+ function removeOverride(key: string): void {
47
+ const next = { ...draft.value };
48
+ delete next[key];
49
+ draft.value = next;
50
+ }
51
+
52
+ function looksLikeColor(value: string): boolean {
53
+ return value.startsWith('#') || value.startsWith('rgb');
54
+ }
55
+ </script>
56
+
57
+ <template>
58
+ <details class="admin-theme-overrides">
59
+ <summary class="admin-theme-overrides-summary">
60
+ <i class="fa-solid fa-sliders" aria-hidden="true" />
61
+ <span>Token overrides</span>
62
+ <span v-if="count > 0" class="admin-theme-overrides-count">{{ count }} active</span>
63
+ </summary>
64
+ <div class="admin-theme-overrides-body">
65
+ <p class="admin-theme-overrides-desc">
66
+ Ad-hoc overrides applied on top of whichever theme is active. Useful for
67
+ one-off tweaks without authoring a full custom theme. For real edits,
68
+ create or fork a theme above.
69
+ </p>
70
+
71
+ <div v-if="count > 0" class="admin-overrides-list">
72
+ <div v-for="(value, key) in draft" :key="key" class="admin-override-row">
73
+ <code class="admin-override-key">--{{ key }}</code>
74
+ <span class="admin-override-value">
75
+ <span
76
+ v-if="looksLikeColor(String(value))"
77
+ class="admin-override-swatch"
78
+ :style="{ backgroundColor: String(value) }"
79
+ />
80
+ {{ value }}
81
+ </span>
82
+ <button
83
+ class="cpub-btn cpub-btn-sm admin-override-remove"
84
+ :aria-label="`Remove override for ${key}`"
85
+ @click="removeOverride(key as string)"
86
+ >
87
+ <i class="fa-solid fa-xmark" aria-hidden="true" />
88
+ </button>
89
+ </div>
90
+ </div>
91
+
92
+ <div class="admin-override-add">
93
+ <input v-model="newKey" class="admin-override-input" placeholder="Token name (e.g. accent)" @keyup.enter="addOverride" />
94
+ <input v-model="newValue" class="admin-override-input" placeholder="Value (e.g. #ff6600)" @keyup.enter="addOverride" />
95
+ <button class="cpub-btn cpub-btn-sm" :disabled="!newKey.trim() || !newValue.trim()" @click="addOverride">Add</button>
96
+ </div>
97
+
98
+ <div class="admin-override-actions">
99
+ <button class="cpub-btn cpub-btn-primary" :disabled="saving || !dirty" @click="emit('save', draft)">
100
+ <i class="fa-solid fa-floppy-disk" aria-hidden="true" /> Save overrides
101
+ </button>
102
+ </div>
103
+ </div>
104
+ </details>
105
+ </template>
106
+
107
+ <style scoped>
108
+ .admin-theme-overrides {
109
+ margin-top: var(--space-6);
110
+ border-top: var(--border-width-default) solid var(--border);
111
+ padding-top: var(--space-4);
112
+ }
113
+
114
+ .admin-theme-overrides-summary {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: var(--space-2);
118
+ font-size: var(--text-md);
119
+ font-weight: var(--font-weight-semibold);
120
+ color: var(--text);
121
+ cursor: pointer;
122
+ padding: var(--space-2);
123
+ list-style: none;
124
+ }
125
+ .admin-theme-overrides-summary::-webkit-details-marker { display: none; }
126
+ .admin-theme-overrides-summary::before {
127
+ content: '\f054'; /* fa-chevron-right */
128
+ font-family: 'Font Awesome 6 Free';
129
+ font-weight: 900;
130
+ font-size: 12px;
131
+ color: var(--text-dim);
132
+ transition: transform var(--transition-fast);
133
+ }
134
+ [open] > .admin-theme-overrides-summary::before { transform: rotate(90deg); }
135
+
136
+ .admin-theme-overrides-count {
137
+ font-family: var(--font-mono);
138
+ font-size: 10px;
139
+ letter-spacing: var(--tracking-wide);
140
+ text-transform: uppercase;
141
+ padding: 1px 6px;
142
+ background: var(--accent-bg);
143
+ color: var(--accent);
144
+ border: var(--border-width-thin) solid var(--accent-border);
145
+ }
146
+
147
+ .admin-theme-overrides-body { padding: var(--space-3) 0; }
148
+ .admin-theme-overrides-desc {
149
+ font-size: var(--text-sm);
150
+ color: var(--text-dim);
151
+ margin: 0 0 var(--space-3);
152
+ max-width: 560px;
153
+ line-height: var(--leading-snug);
154
+ }
155
+
156
+ .admin-overrides-list {
157
+ border: var(--border-width-default) solid var(--border);
158
+ background: var(--surface);
159
+ margin-bottom: var(--space-3);
160
+ }
161
+ .admin-override-row {
162
+ display: flex;
163
+ align-items: center;
164
+ padding: var(--space-2) var(--space-3);
165
+ border-bottom: var(--border-width-thin) solid var(--border2);
166
+ gap: var(--space-3);
167
+ }
168
+ .admin-override-row:last-child { border-bottom: 0; }
169
+ .admin-override-key {
170
+ font-family: var(--font-mono);
171
+ font-size: var(--text-sm);
172
+ color: var(--accent);
173
+ flex-shrink: 0;
174
+ }
175
+ .admin-override-value {
176
+ font-family: var(--font-mono);
177
+ font-size: var(--text-sm);
178
+ color: var(--text-dim);
179
+ display: flex;
180
+ align-items: center;
181
+ gap: var(--space-2);
182
+ flex: 1;
183
+ min-width: 0;
184
+ overflow: hidden;
185
+ text-overflow: ellipsis;
186
+ white-space: nowrap;
187
+ }
188
+ .admin-override-swatch {
189
+ display: inline-block;
190
+ width: 14px;
191
+ height: 14px;
192
+ border: var(--border-width-thin) solid var(--border2);
193
+ flex-shrink: 0;
194
+ }
195
+ .admin-override-remove { flex-shrink: 0; padding: var(--space-1); color: var(--text-faint); }
196
+ .admin-override-remove:hover { color: var(--red); }
197
+
198
+ .admin-override-add {
199
+ display: flex;
200
+ gap: var(--space-2);
201
+ padding: var(--space-3);
202
+ border: 2px dashed var(--border2);
203
+ background: var(--surface);
204
+ margin-bottom: var(--space-3);
205
+ }
206
+ .admin-override-input {
207
+ font-size: var(--text-sm);
208
+ padding: var(--space-1) var(--space-2);
209
+ border: var(--border-width-default) solid var(--border);
210
+ background: var(--surface2);
211
+ color: var(--text);
212
+ font-family: var(--font-mono);
213
+ flex: 1;
214
+ min-width: 0;
215
+ }
216
+
217
+ .admin-override-actions { margin-top: var(--space-3); }
218
+
219
+ @media (max-width: 640px) {
220
+ .admin-override-add { flex-direction: column; }
221
+ }
222
+ </style>