@commonpub/layer 0.71.2 → 0.72.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/ContentCard.vue +6 -0
- package/components/ContentPicker.vue +2 -0
- package/components/ImportUrlModal.vue +2 -0
- package/components/PublishErrorsModal.vue +2 -0
- package/components/RemoteFollowDialog.vue +2 -0
- package/components/ShareToHubModal.vue +2 -0
- package/components/VideoCard.vue +3 -0
- package/components/admin/theme/studio/AdminThemeStudio.vue +31 -1
- package/components/editors/MarkdownImportDialog.vue +2 -0
- package/components/hub/HubHero.vue +3 -0
- package/layouts/default.vue +5 -2
- package/package.json +6 -6
- package/server/utils/instanceTheme.ts +18 -3
- package/theme/base.css +10 -0
- package/theme/components.css +4 -0
- package/theme/layouts.css +3 -0
- package/utils/themeDiscovery.ts +21 -1
|
@@ -141,6 +141,8 @@ function formatCount(n: number | undefined): string {
|
|
|
141
141
|
border: var(--border-width-default) solid var(--border);
|
|
142
142
|
overflow: hidden;
|
|
143
143
|
transition: transform 0.15s, box-shadow 0.15s;
|
|
144
|
+
-webkit-backdrop-filter: var(--surface-backdrop, none);
|
|
145
|
+
backdrop-filter: var(--surface-backdrop, none);
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
.cpub-cc:hover {
|
|
@@ -164,6 +166,10 @@ function formatCount(n: number | undefined): string {
|
|
|
164
166
|
align-items: center;
|
|
165
167
|
justify-content: center;
|
|
166
168
|
overflow: hidden;
|
|
169
|
+
/* Edge-spanning section inside the card's overflow:hidden — the container
|
|
170
|
+
clips the outer corners; rounding HERE creates wedge gaps on rounded
|
|
171
|
+
themes (universal radius leak). */
|
|
172
|
+
border-radius: 0;
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
.cpub-cc-cover {
|
|
@@ -114,6 +114,8 @@ useFocusTrap(dialogRef, () => props.open, close);
|
|
|
114
114
|
background: var(--surface);
|
|
115
115
|
border: var(--border-width-default) solid var(--border);
|
|
116
116
|
box-shadow: var(--shadow-xl);
|
|
117
|
+
-webkit-backdrop-filter: var(--surface-backdrop, none);
|
|
118
|
+
backdrop-filter: var(--surface-backdrop, none);
|
|
117
119
|
width: 520px;
|
|
118
120
|
max-width: 90vw;
|
|
119
121
|
max-height: 70vh;
|
|
@@ -193,6 +193,8 @@ useFocusTrap(dialogRef, () => props.show, handleClose);
|
|
|
193
193
|
display: flex;
|
|
194
194
|
flex-direction: column;
|
|
195
195
|
max-height: 80vh;
|
|
196
|
+
-webkit-backdrop-filter: var(--surface-backdrop, none);
|
|
197
|
+
backdrop-filter: var(--surface-backdrop, none);
|
|
196
198
|
}
|
|
197
199
|
|
|
198
200
|
.cpub-import-header {
|
|
@@ -52,6 +52,8 @@ useFocusTrap(dialogRef, () => props.show, () => emit('dismiss'));
|
|
|
52
52
|
max-width: 420px;
|
|
53
53
|
width: 100%;
|
|
54
54
|
box-shadow: var(--shadow-md);
|
|
55
|
+
-webkit-backdrop-filter: var(--surface-backdrop, none);
|
|
56
|
+
backdrop-filter: var(--surface-backdrop, none);
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
.cpub-publish-errors-title {
|
|
@@ -87,6 +87,8 @@ useFocusTrap(dialogRef, () => open.value, close);
|
|
|
87
87
|
background: var(--bg); border: var(--border-width-default) solid var(--border);
|
|
88
88
|
width: 100%; max-width: 420px; padding: 24px;
|
|
89
89
|
box-shadow: var(--shadow-md);
|
|
90
|
+
-webkit-backdrop-filter: var(--surface-backdrop, none);
|
|
91
|
+
backdrop-filter: var(--surface-backdrop, none);
|
|
90
92
|
}
|
|
91
93
|
.cpub-rfd-header {
|
|
92
94
|
display: flex; align-items: center; justify-content: space-between;
|
|
@@ -100,6 +100,8 @@ async function handleShare(): Promise<void> {
|
|
|
100
100
|
background: var(--surface);
|
|
101
101
|
border: var(--border-width-default) solid var(--border);
|
|
102
102
|
box-shadow: var(--shadow-lg);
|
|
103
|
+
-webkit-backdrop-filter: var(--surface-backdrop, none);
|
|
104
|
+
backdrop-filter: var(--surface-backdrop, none);
|
|
103
105
|
padding: 24px;
|
|
104
106
|
max-width: 420px;
|
|
105
107
|
width: 90vw;
|
package/components/VideoCard.vue
CHANGED
|
@@ -55,6 +55,9 @@ function formatDuration(seconds: number | null | undefined): string {
|
|
|
55
55
|
background: var(--surface2);
|
|
56
56
|
border-bottom: var(--border-width-default) solid var(--border);
|
|
57
57
|
overflow: hidden;
|
|
58
|
+
/* Edge-spanning section inside the card's overflow:hidden — rounding here
|
|
59
|
+
creates wedge gaps on rounded themes; the container clips the corners. */
|
|
60
|
+
border-radius: 0;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
.cpub-video-thumb img {
|
|
@@ -83,8 +83,30 @@ const SCHEMES: { val: HarmonyScheme; label: string }[] = [
|
|
|
83
83
|
// --- Design-ethos archetype (Phase 3) ----------------------------------
|
|
84
84
|
// Applies a whole structural preset (shape/shadow/border/type/density/texture)
|
|
85
85
|
// while preserving the user's chosen color + mode, then tags the recipe.
|
|
86
|
+
// `treatment` is REPLACED, not merged: switching Glass → Brutalist must clear
|
|
87
|
+
// the translucency, otherwise the ethos switch is incoherent.
|
|
86
88
|
function applyArchetype(a: DesignArchetype): void {
|
|
87
|
-
|
|
89
|
+
const { treatment, ...rest } = a.patch;
|
|
90
|
+
recipe.value = { ...recipe.value, ...rest, treatment, archetype: a.k };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- Surface treatment (glass + page gradient) --------------------------
|
|
94
|
+
// Normalized so "all off" stores `undefined` (keeps legacy recipes and the
|
|
95
|
+
// no-treatment projection byte-identical).
|
|
96
|
+
const glassPct = computed(() => Math.round((recipe.value.treatment?.glass ?? 0) * 100));
|
|
97
|
+
function setTreatment(patch: { glass?: number; bgGradient?: boolean }): void {
|
|
98
|
+
const next = {
|
|
99
|
+
glass: recipe.value.treatment?.glass ?? 0,
|
|
100
|
+
bgGradient: recipe.value.treatment?.bgGradient ?? false,
|
|
101
|
+
...patch,
|
|
102
|
+
};
|
|
103
|
+
recipe.value.treatment =
|
|
104
|
+
next.glass > 0 || next.bgGradient
|
|
105
|
+
? {
|
|
106
|
+
...(next.glass > 0 ? { glass: next.glass } : {}),
|
|
107
|
+
...(next.bgGradient ? { bgGradient: true } : {}),
|
|
108
|
+
}
|
|
109
|
+
: undefined;
|
|
88
110
|
}
|
|
89
111
|
|
|
90
112
|
// --- Neutral temperature (Phase 2) -------------------------------------
|
|
@@ -529,6 +551,14 @@ function finishWith(apply: boolean): void {
|
|
|
529
551
|
<span class="cpub-studio-lbl">Grain <span class="cpub-studio-val">{{ Math.round(recipe.texture * 100) }}%</span></span>
|
|
530
552
|
<input type="range" min="0" max="12" :value="Math.round(recipe.texture * 100)" class="cpub-studio-range" @input="recipe.texture = Number(($event.target as HTMLInputElement).value) / 100" />
|
|
531
553
|
</label>
|
|
554
|
+
<label class="cpub-studio-field">
|
|
555
|
+
<span class="cpub-studio-lbl">Glass <span class="cpub-studio-val">{{ glassPct }}%</span></span>
|
|
556
|
+
<input type="range" min="0" max="30" :value="glassPct" class="cpub-studio-range" aria-label="Glass strength" @input="setTreatment({ glass: Number(($event.target as HTMLInputElement).value) / 100 })" />
|
|
557
|
+
</label>
|
|
558
|
+
<div class="cpub-studio-toggle-line">
|
|
559
|
+
<span class="cpub-studio-lbl">Background gradient <span class="cpub-studio-hint">tints toward accent</span></span>
|
|
560
|
+
<button type="button" class="cpub-studio-switch" :class="{ on: recipe.treatment?.bgGradient }" :aria-pressed="Boolean(recipe.treatment?.bgGradient)" aria-label="Toggle background gradient" @click="setTreatment({ bgGradient: !recipe.treatment?.bgGradient })" />
|
|
561
|
+
</div>
|
|
532
562
|
<label class="cpub-studio-field">
|
|
533
563
|
<span class="cpub-studio-lbl">Motion</span>
|
|
534
564
|
<span class="cpub-studio-seg">
|
|
@@ -150,6 +150,8 @@ async function readFile(file: File): Promise<void> {
|
|
|
150
150
|
box-shadow: var(--shadow-xl);
|
|
151
151
|
display: flex; flex-direction: column;
|
|
152
152
|
max-height: 80vh;
|
|
153
|
+
-webkit-backdrop-filter: var(--surface-backdrop, none);
|
|
154
|
+
backdrop-filter: var(--surface-backdrop, none);
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
.md-import-header {
|
|
@@ -67,6 +67,9 @@ const isCompanyHub = computed(() => hubType.value === 'company');
|
|
|
67
67
|
position: relative;
|
|
68
68
|
overflow: hidden;
|
|
69
69
|
border-bottom: var(--border-width-default) solid var(--border);
|
|
70
|
+
/* Edge-spanning band inside the hero's overflow:hidden — rounding here
|
|
71
|
+
creates wedge gaps on rounded themes; the container clips the corners. */
|
|
72
|
+
border-radius: 0;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
.cpub-hub-banner-pattern {
|
package/layouts/default.vue
CHANGED
|
@@ -281,6 +281,7 @@ const userUsername = computed(() => user.value?.username ?? '');
|
|
|
281
281
|
border-bottom-left-radius: var(--cpub-topbar-radius, 0);
|
|
282
282
|
border-bottom-right-radius: var(--cpub-topbar-radius, 0);
|
|
283
283
|
box-shadow: var(--cpub-topbar-shadow, none);
|
|
284
|
+
-webkit-backdrop-filter: var(--cpub-topbar-blur, none);
|
|
284
285
|
backdrop-filter: var(--cpub-topbar-blur, none);
|
|
285
286
|
display: flex; align-items: center;
|
|
286
287
|
padding: 0 var(--cpub-topbar-padding-x, 20px); gap: 0; z-index: 100;
|
|
@@ -322,6 +323,8 @@ const userUsername = computed(() => user.value?.username ?? '');
|
|
|
322
323
|
background: var(--surface); border: var(--border-width-default) solid var(--border);
|
|
323
324
|
box-shadow: var(--shadow-md); z-index: 200; display: flex; flex-direction: column; padding: 4px 0;
|
|
324
325
|
margin-top: 4px;
|
|
326
|
+
-webkit-backdrop-filter: var(--surface-backdrop, none);
|
|
327
|
+
backdrop-filter: var(--surface-backdrop, none);
|
|
325
328
|
}
|
|
326
329
|
:deep(.cpub-nav-panel-item) {
|
|
327
330
|
display: flex; align-items: center; gap: 8px; padding: 8px 14px;
|
|
@@ -362,7 +365,7 @@ const userUsername = computed(() => user.value?.username ?? '');
|
|
|
362
365
|
.cpub-user-avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--purple-bg); border: var(--border-width-default) solid var(--purple); display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; color: var(--purple); font-family: var(--font-mono); overflow: hidden; }
|
|
363
366
|
.cpub-user-avatar-img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
|
|
364
367
|
.cpub-user-menu-wrapper { position: relative; }
|
|
365
|
-
.cpub-user-dropdown { position: absolute; top: calc(100% + 6px); right: 0; min-width: 180px; background: var(--surface); border: var(--border-width-default) solid var(--border); box-shadow: var(--shadow-md); z-index: 200; display: flex; flex-direction: column; padding: 4px 0; }
|
|
368
|
+
.cpub-user-dropdown { position: absolute; top: calc(100% + 6px); right: 0; min-width: 180px; background: var(--surface); border: var(--border-width-default) solid var(--border); box-shadow: var(--shadow-md); z-index: 200; display: flex; flex-direction: column; padding: 4px 0; -webkit-backdrop-filter: var(--surface-backdrop, none); backdrop-filter: var(--surface-backdrop, none); }
|
|
366
369
|
.cpub-dropdown-item { display: flex; align-items: center; gap: 8px; padding: 8px 16px; font-size: 12px; color: var(--text-dim); text-decoration: none; background: none; border: none; cursor: pointer; font-family: inherit; width: 100%; text-align: left; transition: all 0.15s; }
|
|
367
370
|
.cpub-dropdown-item:hover { background: var(--surface2); color: var(--text); }
|
|
368
371
|
.cpub-dropdown-item i { width: 14px; text-align: center; font-size: 11px; }
|
|
@@ -374,7 +377,7 @@ const userUsername = computed(() => user.value?.username ?? '');
|
|
|
374
377
|
|
|
375
378
|
.cpub-mobile-toggle { display: none; width: 32px; height: 32px; background: none; border: var(--border-width-default) solid transparent; color: var(--text-dim); font-size: 16px; cursor: pointer; align-items: center; justify-content: center; }
|
|
376
379
|
.cpub-mobile-menu { display: none; position: fixed; inset: 0; top: var(--cpub-topbar-height, 48px); z-index: 99; background: var(--color-surface-overlay-light); }
|
|
377
|
-
:deep(.cpub-mobile-nav) { background: var(--surface); border-bottom: var(--border-width-default) solid var(--border); padding: 8px 0; display: flex; flex-direction: column; box-shadow: var(--shadow-md); }
|
|
380
|
+
:deep(.cpub-mobile-nav) { background: var(--surface); border-bottom: var(--border-width-default) solid var(--border); padding: 8px 0; display: flex; flex-direction: column; box-shadow: var(--shadow-md); -webkit-backdrop-filter: var(--surface-backdrop, none); backdrop-filter: var(--surface-backdrop, none); }
|
|
378
381
|
:deep(.cpub-mobile-link) { display: flex; align-items: center; gap: 10px; padding: 10px 20px; font-size: 13px; color: var(--text-dim); text-decoration: none; transition: background 0.1s; }
|
|
379
382
|
:deep(.cpub-mobile-link:hover) { background: var(--surface2); color: var(--text); }
|
|
380
383
|
:deep(.cpub-mobile-link i) { width: 16px; text-align: center; font-size: 12px; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -57,13 +57,13 @@
|
|
|
57
57
|
"@commonpub/config": "0.21.0",
|
|
58
58
|
"@commonpub/docs": "0.6.3",
|
|
59
59
|
"@commonpub/editor": "0.7.11",
|
|
60
|
-
"@commonpub/explainer": "0.7.15",
|
|
61
|
-
"@commonpub/learning": "0.5.2",
|
|
62
60
|
"@commonpub/protocol": "0.13.0",
|
|
63
|
-
"@commonpub/schema": "0.
|
|
61
|
+
"@commonpub/schema": "0.40.0",
|
|
62
|
+
"@commonpub/theme-studio": "0.6.1",
|
|
63
|
+
"@commonpub/ui": "0.13.1",
|
|
64
|
+
"@commonpub/explainer": "0.7.15",
|
|
64
65
|
"@commonpub/server": "2.84.1",
|
|
65
|
-
"@commonpub/
|
|
66
|
-
"@commonpub/ui": "0.12.2"
|
|
66
|
+
"@commonpub/learning": "0.5.2"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Users only toggle light/dark within that family.
|
|
4
4
|
|
|
5
5
|
import { eq } from 'drizzle-orm';
|
|
6
|
-
import { instanceSettings } from '@commonpub/schema';
|
|
6
|
+
import { instanceSettings, isSafeBgImageValue } from '@commonpub/schema';
|
|
7
7
|
import {
|
|
8
8
|
getCustomTokenOverrides,
|
|
9
9
|
listCustomThemes,
|
|
@@ -15,6 +15,21 @@ import { THEME_TO_FAMILY, FAMILY_VARIANTS, IS_DARK, VALID_THEME_IDS } from '../.
|
|
|
15
15
|
|
|
16
16
|
const CACHE_TTL = 60_000; // 1 minute, admin changes propagate fast
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Sink-side guard for the one token whose VALUE can fetch when rendered:
|
|
20
|
+
* `--bg-image` feeds `background-image`, so a `url(...)` is a beacon/exfil
|
|
21
|
+
* channel. The themes POST/PUT already reject unsafe values, but the token
|
|
22
|
+
* map has other write paths (the generic admin settings route writes
|
|
23
|
+
* `instance_settings` keys wholesale), so the render sink enforces the same
|
|
24
|
+
* allowlist: a non-gradient bg-image is dropped, never injected.
|
|
25
|
+
*/
|
|
26
|
+
export function sanitizeRenderTokens(tokens: Record<string, string>): Record<string, string> {
|
|
27
|
+
const v = tokens['bg-image'];
|
|
28
|
+
if (v === undefined || isSafeBgImageValue(v)) return tokens;
|
|
29
|
+
const { 'bg-image': _dropped, ...rest } = tokens;
|
|
30
|
+
return rest;
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
interface CachedThemeState {
|
|
19
34
|
/** The admin's chosen default theme (built-in id, custom data-attr, or registered id) */
|
|
20
35
|
defaultTheme: string;
|
|
@@ -138,7 +153,7 @@ export async function resolveThemeContext(
|
|
|
138
153
|
const sib = state.customByAttr.get(sibAttr);
|
|
139
154
|
if (sib) members.push({ attr: sibAttr, rec: sib });
|
|
140
155
|
}
|
|
141
|
-
themeVariants = members.map((m) => ({ attr: m.attr, tokens: m.rec.tokens }));
|
|
156
|
+
themeVariants = members.map((m) => ({ attr: m.attr, tokens: sanitizeRenderTokens(m.rec.tokens) }));
|
|
142
157
|
const lightM = members.find((m) => !m.rec.isDark);
|
|
143
158
|
const darkM = members.find((m) => m.rec.isDark);
|
|
144
159
|
if (members.length === 2 && lightM && darkM) {
|
|
@@ -167,7 +182,7 @@ export async function resolveThemeContext(
|
|
|
167
182
|
instanceTheme: admin,
|
|
168
183
|
isDark,
|
|
169
184
|
themeVariants,
|
|
170
|
-
overrides: { ...state.tokenOverrides },
|
|
185
|
+
overrides: sanitizeRenderTokens({ ...state.tokenOverrides }),
|
|
171
186
|
pair,
|
|
172
187
|
fontHref,
|
|
173
188
|
};
|
package/theme/base.css
CHANGED
|
@@ -216,6 +216,15 @@
|
|
|
216
216
|
--shadow-block: 4px 4px 0 var(--border);
|
|
217
217
|
--shadow-block-sm: 2px 2px 0 var(--border);
|
|
218
218
|
|
|
219
|
+
/* === TREATMENTS ===
|
|
220
|
+
Surface effects. Defaults are TRUE no-ops: `none`, never `blur(0)` — any
|
|
221
|
+
non-none backdrop-filter creates a stacking context and becomes the
|
|
222
|
+
containing block for fixed/absolute descendants, which would move
|
|
223
|
+
dropdowns/modals on every existing theme. Built-in themes don't set
|
|
224
|
+
these; Theme Studio emits them for glass recipes. */
|
|
225
|
+
--surface-backdrop: none; /* backdrop-filter for cards/panels (glass) */
|
|
226
|
+
--bg-image: none; /* page background gradient (gradients only) */
|
|
227
|
+
|
|
219
228
|
/* === TRANSITIONS === */
|
|
220
229
|
--transition-fast: 0.1s ease;
|
|
221
230
|
--transition-default: 0.15s ease;
|
|
@@ -298,6 +307,7 @@ body {
|
|
|
298
307
|
line-height: var(--leading-normal);
|
|
299
308
|
color: var(--text);
|
|
300
309
|
background-color: var(--bg);
|
|
310
|
+
background-image: var(--bg-image, none);
|
|
301
311
|
-webkit-font-smoothing: antialiased;
|
|
302
312
|
-moz-osx-font-smoothing: grayscale;
|
|
303
313
|
}
|
package/theme/components.css
CHANGED
|
@@ -90,6 +90,10 @@
|
|
|
90
90
|
padding: 16px;
|
|
91
91
|
margin-bottom: 12px;
|
|
92
92
|
box-shadow: var(--shadow-block);
|
|
93
|
+
/* Glass treatment hook — `none` by default (a true no-op; non-none values
|
|
94
|
+
create a stacking context, so only opted-in themes pay that cost). */
|
|
95
|
+
-webkit-backdrop-filter: var(--surface-backdrop, none);
|
|
96
|
+
backdrop-filter: var(--surface-backdrop, none);
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
.cpub-sb-title {
|
package/theme/layouts.css
CHANGED
|
@@ -283,6 +283,9 @@
|
|
|
283
283
|
border: var(--border-width-default) solid var(--border);
|
|
284
284
|
overflow: hidden;
|
|
285
285
|
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
|
|
286
|
+
/* Glass treatment hook — `none` default is a true no-op. */
|
|
287
|
+
-webkit-backdrop-filter: var(--surface-backdrop, none);
|
|
288
|
+
backdrop-filter: var(--surface-backdrop, none);
|
|
286
289
|
}
|
|
287
290
|
|
|
288
291
|
.cpub-card:hover {
|
package/utils/themeDiscovery.ts
CHANGED
|
@@ -15,6 +15,25 @@ export interface DiscoveredTheme {
|
|
|
15
15
|
isDark: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Substitute `var(--x)` / `var(--x, fallback)` references in a default
|
|
20
|
+
* value using a property getter (computed style). getComputedStyle returns
|
|
21
|
+
* custom properties with their var() references ALREADY substituted, so a
|
|
22
|
+
* spec default like `var(--surface)` must be resolved the same way before
|
|
23
|
+
* diffing — otherwise every var()-defaulted token (font-heading, the chrome
|
|
24
|
+
* family) reads as "overridden" on a stock site.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveVarRefs(get: (name: string) => string, value: string): string {
|
|
27
|
+
let out = value;
|
|
28
|
+
for (let i = 0; i < 4 && out.includes('var('); i++) {
|
|
29
|
+
out = out.replace(/var\((--[a-zA-Z0-9_-]+)(?:,\s*([^()]*))?\)/g, (_, name: string, fb?: string) => {
|
|
30
|
+
const v = get(name).trim();
|
|
31
|
+
return v || fb || '';
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
18
37
|
/**
|
|
19
38
|
* Read `getComputedStyle(:root)` for every canonical token and return
|
|
20
39
|
* the subset that differs from `TOKEN_SPECS[i].default`.
|
|
@@ -23,11 +42,12 @@ export function detectAppliedOverrides(): DiscoveredTheme {
|
|
|
23
42
|
if (typeof window === 'undefined') return { count: 0, tokens: {}, isDark: false };
|
|
24
43
|
const root = document.documentElement;
|
|
25
44
|
const cs = getComputedStyle(root);
|
|
45
|
+
const get = (name: string): string => cs.getPropertyValue(name);
|
|
26
46
|
const overrides: Record<string, string> = {};
|
|
27
47
|
for (const spec of TOKEN_SPECS) {
|
|
28
48
|
const actual = cs.getPropertyValue(`--${spec.key}`).trim();
|
|
29
49
|
if (!actual) continue;
|
|
30
|
-
if (normalize(actual) !== normalize(spec.default)) {
|
|
50
|
+
if (normalize(actual) !== normalize(resolveVarRefs(get, spec.default))) {
|
|
31
51
|
overrides[spec.key] = actual;
|
|
32
52
|
}
|
|
33
53
|
}
|