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.
@@ -51,7 +51,7 @@ import {
51
51
  setLogSink,
52
52
  slugify,
53
53
  translatePath
54
- } from "../../chunks/chunk-FQBIC2OB.js";
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-UOF4MCAD.js";
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, menoBadgeHtml].filter(Boolean).join('\n'),
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){window.scrollTo(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'));window.scrollTo(sx,sy)};s.onerror=function(){window.scrollTo(sx,sy)};document.body.appendChild(s)}else{document.dispatchEvent(new Event('DOMContentLoaded'));window.scrollTo(sx,sy)}}}).catch(function(){location.reload()})}var iR=document.getElementById('root');if(iR)lastSrvRoot=iR.cloneNode(true);connect()})()</script>`
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
- /** Stable per-page badge number. Server assigns at create time. */
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;