meno-core 1.1.4 → 1.1.5
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/dist/chunks/{chunk-ZEDLSHYQ.js → chunk-H6EOTPJA.js} +2 -2
- package/dist/chunks/{chunk-UOF4MCAD.js → chunk-RLBV2R6J.js} +540 -16
- package/dist/chunks/{chunk-UOF4MCAD.js.map → chunk-RLBV2R6J.js.map} +3 -3
- package/dist/chunks/{chunk-FQBIC2OB.js → chunk-YMBUSHII.js} +1 -1
- package/dist/chunks/{chunk-FQBIC2OB.js.map → chunk-YMBUSHII.js.map} +1 -1
- package/dist/lib/client/index.js +2 -2
- package/dist/lib/server/index.js +39 -10
- package/dist/lib/server/index.js.map +2 -2
- package/dist/lib/shared/index.js +2 -2
- package/lib/server/fileWatcher.test.ts +90 -0
- package/lib/server/fileWatcher.ts +36 -0
- package/lib/server/services/configService.ts +0 -5
- package/lib/server/ssr/htmlGenerator.test.ts +0 -1
- package/lib/server/ssr/htmlGenerator.ts +3 -11
- package/lib/shared/cssGeneration.test.ts +36 -0
- package/lib/shared/cssGeneration.ts +6 -1
- package/lib/shared/tailwindThemeScale.ts +1 -0
- package/lib/shared/types/comment.ts +9 -3
- package/lib/shared/utilityClassConfig.ts +469 -10
- package/lib/shared/utilityClassMapper.test.ts +120 -2
- package/lib/shared/utilityClassMapper.ts +37 -0
- package/lib/shared/utilityClassNames.ts +90 -0
- package/package.json +1 -1
- /package/dist/chunks/{chunk-ZEDLSHYQ.js.map → chunk-H6EOTPJA.js.map} +0 -0
package/dist/lib/shared/index.js
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
setLogSink,
|
|
52
52
|
slugify,
|
|
53
53
|
translatePath
|
|
54
|
-
} from "../../chunks/chunk-
|
|
54
|
+
} from "../../chunks/chunk-YMBUSHII.js";
|
|
55
55
|
import {
|
|
56
56
|
NETLIFY_LOCALE_404_BEGIN,
|
|
57
57
|
NETLIFY_LOCALE_404_END,
|
|
@@ -294,7 +294,7 @@ import {
|
|
|
294
294
|
validatePath,
|
|
295
295
|
validatePropDefinition,
|
|
296
296
|
validateStructuredComponentDefinition
|
|
297
|
-
} from "../../chunks/chunk-
|
|
297
|
+
} from "../../chunks/chunk-RLBV2R6J.js";
|
|
298
298
|
import {
|
|
299
299
|
DEFAULT_I18N_CONFIG,
|
|
300
300
|
buildLocalizedPath,
|
|
@@ -243,6 +243,96 @@ describe('FileWatcher astro CMS template pages', () => {
|
|
|
243
243
|
});
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
+
describe('FileWatcher theme.css', () => {
|
|
247
|
+
let projectRoot: string;
|
|
248
|
+
let stylesDir: string;
|
|
249
|
+
|
|
250
|
+
beforeEach(() => {
|
|
251
|
+
projectRoot = join(tmpdir(), `meno-fw-theme-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
252
|
+
stylesDir = join(projectRoot, 'src', 'styles');
|
|
253
|
+
mkdirSync(join(projectRoot, 'src'), { recursive: true });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
afterEach(() => {
|
|
257
|
+
try {
|
|
258
|
+
rmSync(projectRoot, { recursive: true, force: true });
|
|
259
|
+
} catch {
|
|
260
|
+
/* ignore */
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// theme.css holds BOTH token halves (colors + variables), so one edit must
|
|
265
|
+
// invalidate both service caches — an external write (e.g. an AI tool adding
|
|
266
|
+
// a token) previously stayed invisible until a dev-server restart.
|
|
267
|
+
test('fires onColorsChange AND onVariablesChange when theme.css is written', async () => {
|
|
268
|
+
let colorChanges = 0;
|
|
269
|
+
let variableChanges = 0;
|
|
270
|
+
const watcher = new FileWatcher({
|
|
271
|
+
onColorsChange: async () => {
|
|
272
|
+
colorChanges++;
|
|
273
|
+
},
|
|
274
|
+
onVariablesChange: async () => {
|
|
275
|
+
variableChanges++;
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
mkdirSync(stylesDir, { recursive: true });
|
|
280
|
+
writeFileSync(join(stylesDir, 'theme.css'), ':root { --bg: #fff; }');
|
|
281
|
+
watcher.watchThemeCss(stylesDir);
|
|
282
|
+
await settle(50);
|
|
283
|
+
|
|
284
|
+
writeFileSync(join(stylesDir, 'theme.css'), ':root { --bg: #fff; --accent: #f00; }');
|
|
285
|
+
await waitFor(() => colorChanges >= 1 && variableChanges >= 1);
|
|
286
|
+
|
|
287
|
+
watcher.stopAll();
|
|
288
|
+
expect(colorChanges).toBeGreaterThanOrEqual(1);
|
|
289
|
+
expect(variableChanges).toBeGreaterThanOrEqual(1);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// src/styles may not exist at startup (blank project — theme.css lands later
|
|
293
|
+
// via the JSON migration or an external tool), so the watcher must defer-attach.
|
|
294
|
+
test('fires after src/styles is created post-startup', async () => {
|
|
295
|
+
let colorChanges = 0;
|
|
296
|
+
const watcher = new FileWatcher({
|
|
297
|
+
onColorsChange: async () => {
|
|
298
|
+
colorChanges++;
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
watcher.watchThemeCss(stylesDir);
|
|
303
|
+
await settle(50);
|
|
304
|
+
mkdirSync(stylesDir, { recursive: true });
|
|
305
|
+
await settle(100);
|
|
306
|
+
writeFileSync(join(stylesDir, 'theme.css'), ':root { --bg: #fff; }');
|
|
307
|
+
await waitFor(() => colorChanges >= 1);
|
|
308
|
+
|
|
309
|
+
watcher.stopAll();
|
|
310
|
+
expect(colorChanges).toBeGreaterThanOrEqual(1);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('ignores writes to other files in src/styles', async () => {
|
|
314
|
+
let fired = 0;
|
|
315
|
+
const watcher = new FileWatcher({
|
|
316
|
+
onColorsChange: async () => {
|
|
317
|
+
fired++;
|
|
318
|
+
},
|
|
319
|
+
onVariablesChange: async () => {
|
|
320
|
+
fired++;
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
mkdirSync(stylesDir, { recursive: true });
|
|
325
|
+
watcher.watchThemeCss(stylesDir);
|
|
326
|
+
await settle(50);
|
|
327
|
+
|
|
328
|
+
writeFileSync(join(stylesDir, 'global.css'), 'body { margin: 0; }');
|
|
329
|
+
await settle(200);
|
|
330
|
+
|
|
331
|
+
watcher.stopAll();
|
|
332
|
+
expect(fired).toBe(0);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
246
336
|
describe('FileWatcher project.config.json', () => {
|
|
247
337
|
let projectRoot: string;
|
|
248
338
|
let prevRoot: string;
|
|
@@ -152,6 +152,7 @@ export class FileWatcher {
|
|
|
152
152
|
private templatesWatcher: FSWatcher | null = null;
|
|
153
153
|
private colorsWatcher: FSWatcher | null = null;
|
|
154
154
|
private variablesWatcher: FSWatcher | null = null;
|
|
155
|
+
private themeCssWatcher: FSWatcher | null = null;
|
|
155
156
|
private enumsWatcher: FSWatcher | null = null;
|
|
156
157
|
private cmsWatcher: FSWatcher | null = null;
|
|
157
158
|
private imagesWatcher: FSWatcher | null = null;
|
|
@@ -261,6 +262,35 @@ export class FileWatcher {
|
|
|
261
262
|
});
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Start watching `src/styles/theme.css` — the token source of truth for
|
|
267
|
+
* astro-format projects, where colors AND variables share the one file
|
|
268
|
+
* (colors.json / variables.json were migrated away, so the two watchers
|
|
269
|
+
* above never fire there). An external edit (e.g. an AI tool adding tokens)
|
|
270
|
+
* must invalidate both service caches and refresh connected clients, or the
|
|
271
|
+
* new tokens only appear after a dev-server restart. Deferred attach:
|
|
272
|
+
* `src/styles` may not exist yet (theme.css lands later via migration or an
|
|
273
|
+
* external tool). No-op in legacy JSON projects (no `src/`).
|
|
274
|
+
*/
|
|
275
|
+
watchThemeCss(dirPath: string = join(getProjectRoot(), 'src', 'styles')): void {
|
|
276
|
+
attachWhenDirExists(
|
|
277
|
+
dirPath,
|
|
278
|
+
() =>
|
|
279
|
+
watch(dirPath, { recursive: false }, async (_event, filename) => {
|
|
280
|
+
if (filename !== 'theme.css') return;
|
|
281
|
+
if (this.callbacks.onColorsChange) {
|
|
282
|
+
await this.callbacks.onColorsChange();
|
|
283
|
+
}
|
|
284
|
+
if (this.callbacks.onVariablesChange) {
|
|
285
|
+
await this.callbacks.onVariablesChange();
|
|
286
|
+
}
|
|
287
|
+
}),
|
|
288
|
+
(w) => {
|
|
289
|
+
this.themeCssWatcher = w;
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
264
294
|
/**
|
|
265
295
|
* Start watching enums.json file
|
|
266
296
|
*/
|
|
@@ -425,6 +455,7 @@ export class FileWatcher {
|
|
|
425
455
|
this.watchTemplates();
|
|
426
456
|
this.watchColors();
|
|
427
457
|
this.watchVariables();
|
|
458
|
+
this.watchThemeCss();
|
|
428
459
|
this.watchEnums();
|
|
429
460
|
this.watchCMS();
|
|
430
461
|
this.watchImages();
|
|
@@ -465,6 +496,11 @@ export class FileWatcher {
|
|
|
465
496
|
this.variablesWatcher = null;
|
|
466
497
|
}
|
|
467
498
|
|
|
499
|
+
if (this.themeCssWatcher) {
|
|
500
|
+
this.themeCssWatcher.close();
|
|
501
|
+
this.themeCssWatcher = null;
|
|
502
|
+
}
|
|
503
|
+
|
|
468
504
|
if (this.enumsWatcher) {
|
|
469
505
|
this.enumsWatcher.close();
|
|
470
506
|
this.enumsWatcher = null;
|
|
@@ -66,7 +66,6 @@ interface RawProjectConfig {
|
|
|
66
66
|
imageFormat?: ImageFormat;
|
|
67
67
|
remConversion?: Partial<RemConversionConfig>;
|
|
68
68
|
customCode?: CustomCodeConfig;
|
|
69
|
-
showMenoBadge?: boolean;
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
/**
|
|
@@ -343,10 +342,6 @@ export class ConfigService {
|
|
|
343
342
|
return this.config.customCode;
|
|
344
343
|
}
|
|
345
344
|
|
|
346
|
-
getShowMenoBadge(): boolean {
|
|
347
|
-
return this.config?.showMenoBadge === true;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
345
|
getImageFormat(): ImageFormat {
|
|
351
346
|
if (this.config?.imageFormat === 'avif') return 'avif';
|
|
352
347
|
return 'webp';
|
|
@@ -93,7 +93,6 @@ const mockConfigService = {
|
|
|
93
93
|
getLibraries: mock(() => undefined),
|
|
94
94
|
getResponsiveScales: mock(() => ({ enabled: false, baseReference: 16 })),
|
|
95
95
|
getCustomCode: mock(() => ({})),
|
|
96
|
-
getShowMenoBadge: mock(() => false),
|
|
97
96
|
getRemConversion: mock(() => ({ enabled: false, baseFontSize: 16 })),
|
|
98
97
|
};
|
|
99
98
|
|
|
@@ -394,18 +394,10 @@ export async function generateSSRHTML(
|
|
|
394
394
|
await configService.load();
|
|
395
395
|
const globalLibraries = configService.getLibraries() || { js: [], css: [] };
|
|
396
396
|
const globalCustomCode = configService.getCustomCode();
|
|
397
|
-
// The Meno badge previously used inline event handlers (onmouseenter /
|
|
398
|
-
// onmouseleave) to drive its opacity hover, which CSP with `'nonce-…'` and
|
|
399
|
-
// no `'unsafe-inline'` rejects. Move the hover to a stylesheet rule using a
|
|
400
|
-
// dedicated class so nonces aren't needed for this purely-presentational
|
|
401
|
-
// effect.
|
|
402
|
-
const menoBadgeHtml = configService.getShowMenoBadge()
|
|
403
|
-
? `<style${nonceAttr}>.meno-badge{position:fixed;bottom:12px;left:12px;z-index:9999;background:#000;color:#fff;padding:4px 10px;border-radius:6px;font-size:12px;font-family:system-ui,sans-serif;text-decoration:none;opacity:0.8;transition:opacity 0.2s}.meno-badge:hover,.meno-badge:focus{opacity:1}</style><a class="meno-badge" href="https://meno.so" target="_blank" rel="noopener">Made in Meno</a>`
|
|
404
|
-
: '';
|
|
405
397
|
const mergedCustomCode = {
|
|
406
398
|
head: [globalCustomCode.head, pageCustomCode?.head].filter(Boolean).join('\n'),
|
|
407
399
|
bodyStart: [globalCustomCode.bodyStart, pageCustomCode?.bodyStart].filter(Boolean).join('\n'),
|
|
408
|
-
bodyEnd: [globalCustomCode.bodyEnd, pageCustomCode?.bodyEnd
|
|
400
|
+
bodyEnd: [globalCustomCode.bodyEnd, pageCustomCode?.bodyEnd].filter(Boolean).join('\n'),
|
|
409
401
|
};
|
|
410
402
|
const componentLibraries = collectComponentLibraries(components, pageData.components || {});
|
|
411
403
|
// Merge global + component first (component libraries always extend)
|
|
@@ -735,12 +727,12 @@ picture {
|
|
|
735
727
|
// HTML. Without it, nonce-only `script-src` would drop the recreated script
|
|
736
728
|
// and CMS hot-reload (updated `window.__MENO_CMS__` data) would silently fail.
|
|
737
729
|
const liveReloadScript = injectLiveReload
|
|
738
|
-
? `<script${nonceAttr}>(function(){var ws,timer,gen=0,lastSrvRoot=null,dn=document.querySelector('meta[name="csp-nonce"]'),docNonce=dn?dn.getAttribute('content'):'';function strip(s){return s?s.replace(/[?&]_r=\\d+/,''):''}function classList(el){return (el.getAttribute('class')||'').split(/\\s+/).filter(Boolean)}function syncEl(cur,srv,old){var cc=classList(cur),sc=classList(srv),oc=old?new Set(classList(old)):new Set();var rt=cc.filter(function(c){return !oc.has(c)});var seen=new Set(),fin=[];sc.concat(rt).forEach(function(c){if(!seen.has(c)){seen.add(c);fin.push(c)}});var fs=fin.join(' ');if((cur.getAttribute('class')||'')!==fs){if(fs)cur.setAttribute('class',fs);else cur.removeAttribute('class')}for(var i=0;i<srv.attributes.length;i++){var a=srv.attributes[i];if(a.name==='class')continue;var ov=old?old.getAttribute(a.name):null;if(a.value!==ov){if(cur.getAttribute(a.name)!==a.value)cur.setAttribute(a.name,a.value)}}if(old){for(var i=0;i<old.attributes.length;i++){var a=old.attributes[i];if(a.name==='class')continue;if(!srv.hasAttribute(a.name)&&cur.hasAttribute(a.name))cur.removeAttribute(a.name)}}}function syncText(cur,srv){var cc=cur.childNodes,sc=srv.childNodes;for(var i=0;i<sc.length;i++){var s=sc[i],c=cc[i];if(s.nodeType===3&&c&&c.nodeType===3){if(c.textContent!==s.textContent)c.textContent=s.textContent}}}function ek(el){var p=el.getAttribute('data-element-path'),ci=el.getAttribute('data-cms-item-index');return ci?p+'|'+ci:p}function structKey(root){var e=root.querySelectorAll('[data-element-path]'),k=[];for(var i=0;i<e.length;i++)k.push(ek(e[i]));return k.sort().join('~')}function softSync(curR,srvR,oldR){var ce=curR.querySelectorAll('[data-element-path]');var sbp={},se=srvR.querySelectorAll('[data-element-path]');for(var i=0;i<se.length;i++)sbp[ek(se[i])]=se[i];var obp={};if(oldR){var oe=oldR.querySelectorAll('[data-element-path]');for(var i=0;i<oe.length;i++)obp[ek(oe[i])]=oe[i]}for(var i=0;i<ce.length;i++){var c=ce[i],p=ek(c),s=sbp[p];if(!s)continue;syncEl(c,s,obp[p]);syncText(c,s)}syncText(curR,srvR)}function connect(){ws=new WebSocket(${wsUrl});ws.onmessage=function(e){var d=JSON.parse(e.data);if(d.type==='hmr:libraries-update'){location.reload()}else if(d.type==='hmr:update'||d.type==='hmr:cms-update'||d.type==='hmr:colors-update'||d.type==='hmr:variables-update')hotReload()};ws.onclose=function(){clearTimeout(timer);timer=setTimeout(connect,1000)}}function hotReload(){var g=++gen;var sx=window.scrollX,sy=window.scrollY;fetch(location.href,{cache:'no-store'}).then(function(r){return r.text()}).then(function(html){if(g!==gen)return;var p=new DOMParser();var d=p.parseFromString(html,'text/html');var or=document.getElementById('root'),nr=d.getElementById('root');var oscr=document.querySelector('script[src^="/_scripts/"]'),nscr=d.querySelector('script[src^="/_scripts/"]');var oss=oscr?strip(oscr.getAttribute('src')):'',nss=nscr?strip(nscr.getAttribute('src')):'';var hardReset=(oss!==nss)||!lastSrvRoot||!or||!nr||structKey(lastSrvRoot)!==structKey(nr);if(or&&nr){if(hardReset){if(or.innerHTML!==nr.innerHTML)or.innerHTML=nr.innerHTML}else{softSync(or,nr,lastSrvRoot)}}if(nr)lastSrvRoot=nr.cloneNode(true);var os=document.getElementById('meno-styles'),ns=d.getElementById('meno-styles');if(os&&ns&&os.textContent!==ns.textContent)os.textContent=ns.textContent;var nh=d.documentElement;if(nh){var nl=nh.getAttribute('lang')||'en',nt=nh.getAttribute('theme')||'light';if(document.documentElement.getAttribute('lang')!==nl)document.documentElement.setAttribute('lang',nl);if(document.documentElement.getAttribute('theme')!==nt)document.documentElement.setAttribute('theme',nt)}var ocms=document.querySelectorAll('script[id^="meno-cms-"]'),ncms=d.querySelectorAll('script[id^="meno-cms-"]');var ock=JSON.stringify(Array.prototype.map.call(ocms,function(s){return [s.id,s.textContent]}));var nck=JSON.stringify(Array.prototype.map.call(ncms,function(s){return [s.id,s.textContent]}));if(ock!==nck){ocms.forEach(function(s){s.remove()});ncms.forEach(function(s){var c=document.createElement('script');c.type=s.type;c.id=s.id;if(docNonce)c.nonce=docNonce;c.textContent=s.textContent;document.head.appendChild(c)})}window.__menoHotReload=true;var olib=document.querySelectorAll('body > script[src^="/libraries/"]'),nlib=d.querySelectorAll('body > script[src^="/libraries/"]');var olk=JSON.stringify(Array.prototype.map.call(olib,function(s){return strip(s.getAttribute('src'))}).sort());var nlk=JSON.stringify(Array.prototype.map.call(nlib,function(s){return strip(s.getAttribute('src'))}).sort());if(olk!==nlk){olib.forEach(function(o){o.remove()});nlib.forEach(function(n){var src=n.getAttribute('src');var ls=document.createElement('script');ls.src=src+(src.indexOf('?')>-1?'&':'?')+'_r='+Date.now();document.body.appendChild(ls)})}if(!hardReset){
|
|
730
|
+
? `<script${nonceAttr}>(function(){var ws,timer,gen=0,lastSrvRoot=null,dn=document.querySelector('meta[name="csp-nonce"]'),docNonce=dn?dn.getAttribute('content'):'';function strip(s){return s?s.replace(/[?&]_r=\\d+/,''):''}function iScroll(x,y){var de=document.documentElement,b=document.body,pd=de.style.scrollBehavior,pb=b?b.style.scrollBehavior:'';de.style.scrollBehavior='auto';if(b)b.style.scrollBehavior='auto';try{window.scrollTo(x,y)}catch(e){}de.style.scrollBehavior=pd;if(b)b.style.scrollBehavior=pb}function classList(el){return (el.getAttribute('class')||'').split(/\\s+/).filter(Boolean)}function syncEl(cur,srv,old){var cc=classList(cur),sc=classList(srv),oc=old?new Set(classList(old)):new Set();var rt=cc.filter(function(c){return !oc.has(c)});var seen=new Set(),fin=[];sc.concat(rt).forEach(function(c){if(!seen.has(c)){seen.add(c);fin.push(c)}});var fs=fin.join(' ');if((cur.getAttribute('class')||'')!==fs){if(fs)cur.setAttribute('class',fs);else cur.removeAttribute('class')}for(var i=0;i<srv.attributes.length;i++){var a=srv.attributes[i];if(a.name==='class')continue;var ov=old?old.getAttribute(a.name):null;if(a.value!==ov){if(cur.getAttribute(a.name)!==a.value)cur.setAttribute(a.name,a.value)}}if(old){for(var i=0;i<old.attributes.length;i++){var a=old.attributes[i];if(a.name==='class')continue;if(!srv.hasAttribute(a.name)&&cur.hasAttribute(a.name))cur.removeAttribute(a.name)}}}function syncText(cur,srv){var cc=cur.childNodes,sc=srv.childNodes;for(var i=0;i<sc.length;i++){var s=sc[i],c=cc[i];if(s.nodeType===3&&c&&c.nodeType===3){if(c.textContent!==s.textContent)c.textContent=s.textContent}}}function ek(el){var p=el.getAttribute('data-element-path'),ci=el.getAttribute('data-cms-item-index');return ci?p+'|'+ci:p}function structKey(root){var e=root.querySelectorAll('[data-element-path]'),k=[];for(var i=0;i<e.length;i++)k.push(ek(e[i]));return k.sort().join('~')}function softSync(curR,srvR,oldR){var ce=curR.querySelectorAll('[data-element-path]');var sbp={},se=srvR.querySelectorAll('[data-element-path]');for(var i=0;i<se.length;i++)sbp[ek(se[i])]=se[i];var obp={};if(oldR){var oe=oldR.querySelectorAll('[data-element-path]');for(var i=0;i<oe.length;i++)obp[ek(oe[i])]=oe[i]}for(var i=0;i<ce.length;i++){var c=ce[i],p=ek(c),s=sbp[p];if(!s)continue;syncEl(c,s,obp[p]);syncText(c,s)}syncText(curR,srvR)}function connect(){ws=new WebSocket(${wsUrl});ws.onmessage=function(e){var d=JSON.parse(e.data);if(d.type==='hmr:libraries-update'){location.reload()}else if(d.type==='hmr:update'||d.type==='hmr:cms-update'||d.type==='hmr:colors-update'||d.type==='hmr:variables-update')hotReload()};ws.onclose=function(){clearTimeout(timer);timer=setTimeout(connect,1000)}}function hotReload(){var g=++gen;var sx=window.scrollX,sy=window.scrollY;fetch(location.href,{cache:'no-store'}).then(function(r){return r.text()}).then(function(html){if(g!==gen)return;var p=new DOMParser();var d=p.parseFromString(html,'text/html');var or=document.getElementById('root'),nr=d.getElementById('root');var oscr=document.querySelector('script[src^="/_scripts/"]'),nscr=d.querySelector('script[src^="/_scripts/"]');var oss=oscr?strip(oscr.getAttribute('src')):'',nss=nscr?strip(nscr.getAttribute('src')):'';var hardReset=(oss!==nss)||!lastSrvRoot||!or||!nr||structKey(lastSrvRoot)!==structKey(nr);if(or&&nr){if(hardReset){if(or.innerHTML!==nr.innerHTML)or.innerHTML=nr.innerHTML}else{softSync(or,nr,lastSrvRoot)}}if(nr)lastSrvRoot=nr.cloneNode(true);var os=document.getElementById('meno-styles'),ns=d.getElementById('meno-styles');if(os&&ns&&os.textContent!==ns.textContent)os.textContent=ns.textContent;var nh=d.documentElement;if(nh){var nl=nh.getAttribute('lang')||'en',nt=nh.getAttribute('theme')||'light';if(document.documentElement.getAttribute('lang')!==nl)document.documentElement.setAttribute('lang',nl);if(document.documentElement.getAttribute('theme')!==nt)document.documentElement.setAttribute('theme',nt)}var ocms=document.querySelectorAll('script[id^="meno-cms-"]'),ncms=d.querySelectorAll('script[id^="meno-cms-"]');var ock=JSON.stringify(Array.prototype.map.call(ocms,function(s){return [s.id,s.textContent]}));var nck=JSON.stringify(Array.prototype.map.call(ncms,function(s){return [s.id,s.textContent]}));if(ock!==nck){ocms.forEach(function(s){s.remove()});ncms.forEach(function(s){var c=document.createElement('script');c.type=s.type;c.id=s.id;if(docNonce)c.nonce=docNonce;c.textContent=s.textContent;document.head.appendChild(c)})}window.__menoHotReload=true;var olib=document.querySelectorAll('body > script[src^="/libraries/"]'),nlib=d.querySelectorAll('body > script[src^="/libraries/"]');var olk=JSON.stringify(Array.prototype.map.call(olib,function(s){return strip(s.getAttribute('src'))}).sort());var nlk=JSON.stringify(Array.prototype.map.call(nlib,function(s){return strip(s.getAttribute('src'))}).sort());if(olk!==nlk){olib.forEach(function(o){o.remove()});nlib.forEach(function(n){var src=n.getAttribute('src');var ls=document.createElement('script');ls.src=src+(src.indexOf('?')>-1?'&':'?')+'_r='+Date.now();document.body.appendChild(ls)})}if(!hardReset){iScroll(sx,sy)}else{if(oscr)oscr.remove();if(nscr){var src=nscr.getAttribute('src');var s=document.createElement('script');s.src=src+(src.indexOf('?')>-1?'&':'?')+'_r='+Date.now();s.onload=function(){document.dispatchEvent(new Event('DOMContentLoaded'));iScroll(sx,sy)};s.onerror=function(){iScroll(sx,sy)};document.body.appendChild(s)}else{document.dispatchEvent(new Event('DOMContentLoaded'));iScroll(sx,sy)}}}).catch(function(){location.reload()})}var iR=document.getElementById('root');if(iR)lastSrvRoot=iR.cloneNode(true);connect()})()</script>`
|
|
739
731
|
: '';
|
|
740
732
|
|
|
741
733
|
// Scroll position handlers for preview mode iframe switching
|
|
742
734
|
const scrollHandlerScript = injectLiveReload
|
|
743
|
-
? `<script${nonceAttr}>(function(){window.addEventListener('message',function(e){if(e.data.type==='GET_SCROLL_POSITION'){window.parent.postMessage({type:'SCROLL_POSITION_RESPONSE',scrollX:window.scrollX,scrollY:window.scrollY},'*')}else if(e.data.type==='SET_SCROLL_POSITION'){window.scrollTo(e.data.scrollX,e.data.scrollY)}})})()</script>`
|
|
735
|
+
? `<script${nonceAttr}>(function(){window.addEventListener('message',function(e){if(e.data.type==='GET_SCROLL_POSITION'){window.parent.postMessage({type:'SCROLL_POSITION_RESPONSE',scrollX:window.scrollX,scrollY:window.scrollY},'*')}else if(e.data.type==='SET_SCROLL_POSITION'){var de=document.documentElement,b=document.body,pd=de.style.scrollBehavior,pb=b?b.style.scrollBehavior:'';de.style.scrollBehavior='auto';if(b)b.style.scrollBehavior='auto';window.scrollTo(e.data.scrollX,e.data.scrollY);de.style.scrollBehavior=pd;if(b)b.style.scrollBehavior=pb}})})()</script>`
|
|
744
736
|
: '';
|
|
745
737
|
|
|
746
738
|
// In production, output minified CSS on single line; in dev, preserve formatting
|
|
@@ -384,6 +384,42 @@ describe('cssGeneration', () => {
|
|
|
384
384
|
expect(generateRuleForClass('[text-overflow:ellipsis]')).toBe('text-overflow: ellipsis;');
|
|
385
385
|
expect(generateRuleForClass('[clip-path:circle(50%)]')).toBe('clip-path: circle(50%);');
|
|
386
386
|
});
|
|
387
|
+
|
|
388
|
+
test('filter functions wrap the value in their function', () => {
|
|
389
|
+
expect(generateRuleForClass('blur-[70px]')).toBe('filter: blur(70px);');
|
|
390
|
+
expect(generateRuleForClass('backdrop-blur-[8px]')).toBe('backdrop-filter: blur(8px);');
|
|
391
|
+
expect(generateRuleForClass('blur-(--glow)')).toBe('filter: blur(var(--glow));');
|
|
392
|
+
expect(generateRuleForClass('brightness-50')).toBe('filter: brightness(0.5);');
|
|
393
|
+
expect(generateRuleForClass('grayscale')).toBe('filter: grayscale(100%);');
|
|
394
|
+
expect(generateRuleForClass('hue-rotate-90')).toBe('filter: hue-rotate(90deg);');
|
|
395
|
+
expect(generateRuleForClass('drop-shadow-[0_4px_6px_#0003]')).toBe('filter: drop-shadow(0 4px 6px #0003);');
|
|
396
|
+
expect(generateRuleForClass('backdrop-opacity-50')).toBe('backdrop-filter: opacity(0.5);');
|
|
397
|
+
// Length named scale is deliberately not absorbed (no blur theme tokens).
|
|
398
|
+
expect(generateRuleForClass('blur-sm')).toBeNull();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('per-axis transforms and side radius expand correctly', () => {
|
|
402
|
+
expect(generateRuleForClass('skew-x-[10deg]')).toBe('transform: skewX(10deg);');
|
|
403
|
+
expect(generateRuleForClass('scale-y-95')).toBe('scale: 1 0.95;');
|
|
404
|
+
expect(generateRuleForClass('rounded-t-lg')).toBe(
|
|
405
|
+
'border-top-left-radius: var(--radius-lg); border-top-right-radius: var(--radius-lg);',
|
|
406
|
+
);
|
|
407
|
+
expect(generateRuleForClass('rounded-l-[8px]')).toBe(
|
|
408
|
+
'border-top-left-radius: 8px; border-bottom-left-radius: 8px;',
|
|
409
|
+
);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('new statics and viewport keywords', () => {
|
|
413
|
+
expect(generateRuleForClass('italic')).toBe('font-style: italic;');
|
|
414
|
+
expect(generateRuleForClass('text-nowrap')).toBe('white-space: nowrap;');
|
|
415
|
+
expect(generateRuleForClass('break-words')).toBe('overflow-wrap: break-word;');
|
|
416
|
+
expect(generateRuleForClass('isolate')).toBe('isolation: isolate;');
|
|
417
|
+
expect(generateRuleForClass('snap-x')).toBe('scroll-snap-type: x mandatory;');
|
|
418
|
+
expect(generateRuleForClass('mix-blend-multiply')).toBe('mix-blend-mode: multiply;');
|
|
419
|
+
expect(generateRuleForClass('columns-3')).toBe('columns: 3;');
|
|
420
|
+
expect(generateRuleForClass('h-dvh')).toBe('height: 100dvh;');
|
|
421
|
+
expect(generateRuleForClass('shadow-inner')).toBe('box-shadow: var(--shadow-inner);');
|
|
422
|
+
});
|
|
387
423
|
});
|
|
388
424
|
});
|
|
389
425
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Used by both client and server to ensure consistent CSS output
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { propertyOrder, staticUtilityReverse, presetClassReverse } from './utilityClassConfig';
|
|
7
|
+
import { propertyOrder, staticUtilityReverse, presetClassReverse, ROUNDED_SIDE_CORNERS } from './utilityClassConfig';
|
|
8
8
|
import {
|
|
9
9
|
parseUtilityClass,
|
|
10
10
|
splitVariantPrefix,
|
|
@@ -217,6 +217,11 @@ export function generateRuleForClass(className: string, knownTokens?: ReadonlySe
|
|
|
217
217
|
if (root === 'size') {
|
|
218
218
|
return `width: ${value}; height: ${value};`;
|
|
219
219
|
}
|
|
220
|
+
// Side border-radius: no CSS per-side shorthand — emit both corner longhands.
|
|
221
|
+
const sideCorners = root ? ROUNDED_SIDE_CORNERS[root] : undefined;
|
|
222
|
+
if (sideCorners) {
|
|
223
|
+
return `${sideCorners[0]}: ${value}; ${sideCorners[1]}: ${value};`;
|
|
224
|
+
}
|
|
220
225
|
if (root === 'px') {
|
|
221
226
|
return `padding-left: ${value}; padding-right: ${value};`;
|
|
222
227
|
}
|
|
@@ -137,6 +137,7 @@ const SPECS: ScaleSpec[] = [
|
|
|
137
137
|
['lg', '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)'],
|
|
138
138
|
['xl', '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)'],
|
|
139
139
|
['2xl', '0 25px 50px -12px rgb(0 0 0 / 0.25)'],
|
|
140
|
+
['inner', 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)'],
|
|
140
141
|
],
|
|
141
142
|
},
|
|
142
143
|
{
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Comments are Figma-style pins placed on components in the studio canvas.
|
|
5
5
|
* One file per pin (thread embedded). Storage layout (status as top folder):
|
|
6
|
-
* {projectRoot}/comments/<status>/<pageSlug>--<seq>--<titleSlug>.json
|
|
7
|
-
* e.g. comments/open/blog__post--3--hero-is-too-dark.json
|
|
6
|
+
* {projectRoot}/comments/<status>/<pageSlug>--<seq>--<titleSlug>--<idFrag>.json
|
|
7
|
+
* e.g. comments/open/blog__post--3--hero-is-too-dark--550e8400.json
|
|
8
8
|
*
|
|
9
9
|
* Pins are an authoring-time concept — they live in studio only.
|
|
10
10
|
*/
|
|
@@ -119,7 +119,13 @@ export interface Comment {
|
|
|
119
119
|
_filename: string;
|
|
120
120
|
/** Page path the pin lives on (e.g. `"blog/post"`). */
|
|
121
121
|
_pagePath: string;
|
|
122
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Best-effort creation counter (server stamps `max+1` at create time), used
|
|
124
|
+
* as the human-scannable middle segment of the filename. NOT collision-proof
|
|
125
|
+
* under concurrent/offline creation and NOT the badge shown to users — the pin
|
|
126
|
+
* badge is derived client-side from the full set (see `assignDisplaySeq`), so a
|
|
127
|
+
* repeated `_seq` here is harmless.
|
|
128
|
+
*/
|
|
123
129
|
_seq: number;
|
|
124
130
|
/** ISO timestamps. */
|
|
125
131
|
_createdAt: string;
|