meno-astro 0.1.2 → 0.1.3

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.
@@ -3,7 +3,7 @@ import {
3
3
  extractLocaleFromPath,
4
4
  isI18nValue,
5
5
  resolveI18nValue
6
- } from "./chunk-OT56NX4T.js";
6
+ } from "./chunk-I57UVB4P.js";
7
7
  import {
8
8
  __require
9
9
  } from "./chunk-ZYQNHI3W.js";
@@ -88,4 +88,4 @@ export {
88
88
  deriveLocale,
89
89
  createLocaleMiddleware
90
90
  };
91
- //# sourceMappingURL=chunk-DY7HYV2D.js.map
91
+ //# sourceMappingURL=chunk-54OHWVG3.js.map
@@ -0,0 +1,80 @@
1
+ import {
2
+ isStyleMapping,
3
+ responsiveStylesToClasses,
4
+ shortHash
5
+ } from "./chunk-I57UVB4P.js";
6
+
7
+ // lib/runtime/style.ts
8
+ var collected = /* @__PURE__ */ new Map();
9
+ function flushCollectedStyles() {
10
+ return Array.from(collected.values()).filter(Boolean).join("\n");
11
+ }
12
+ function resetStyleCollector() {
13
+ collected.clear();
14
+ }
15
+ function resolveMappingValue(mapping, props) {
16
+ if (!props) return void 0;
17
+ const propValue = props[mapping.prop];
18
+ if (propValue === void 0 || propValue === null) return void 0;
19
+ const resolved = mapping.values[String(propValue)];
20
+ return resolved === void 0 ? void 0 : resolved;
21
+ }
22
+ function resolveMappingsInFlat(style2, props) {
23
+ const out = {};
24
+ for (const [prop, value] of Object.entries(style2)) {
25
+ if (isStyleMapping(value)) {
26
+ const resolved = resolveMappingValue(value, props);
27
+ if (resolved !== void 0) out[prop] = resolved;
28
+ continue;
29
+ }
30
+ out[prop] = value;
31
+ }
32
+ return out;
33
+ }
34
+ function isResponsive(style2) {
35
+ return typeof style2 === "object" && style2 !== null && ("base" in style2 || "tablet" in style2 || "mobile" in style2);
36
+ }
37
+ function resolveMappingsInStyle(style2, props) {
38
+ if (isResponsive(style2)) {
39
+ const out = {};
40
+ for (const [bp, bpStyle] of Object.entries(style2)) {
41
+ if (!bpStyle) continue;
42
+ out[bp] = resolveMappingsInFlat(bpStyle, props);
43
+ }
44
+ return out;
45
+ }
46
+ return resolveMappingsInFlat(style2, props);
47
+ }
48
+ function sanitizeLabel(label) {
49
+ return label.toLowerCase().replace(/[^a-z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
50
+ }
51
+ function computeClassName(resolvedBase, resolvedInteractive, label) {
52
+ const fingerprint = JSON.stringify({ b: resolvedBase, i: resolvedInteractive });
53
+ const hash = shortHash(fingerprint);
54
+ const prefix = label ? sanitizeLabel(label) : "";
55
+ return prefix ? `m_${prefix}_${hash}` : `m_${hash}`;
56
+ }
57
+ function style(styleObject, props, meta) {
58
+ const base = styleObject ?? {};
59
+ const resolvedBase = resolveMappingsInStyle(base, props);
60
+ const classes = responsiveStylesToClasses(resolvedBase);
61
+ const resolvedInteractive = (meta?.interactive ?? []).map(
62
+ (rule) => ({
63
+ ...rule,
64
+ style: resolveMappingsInStyle(rule.style, props)
65
+ })
66
+ );
67
+ if (resolvedInteractive.length > 0) {
68
+ classes.push(computeClassName({}, resolvedInteractive, meta?.label));
69
+ }
70
+ return classes.join(" ");
71
+ }
72
+
73
+ export {
74
+ flushCollectedStyles,
75
+ resetStyleCollector,
76
+ resolveMappingsInStyle,
77
+ computeClassName,
78
+ style
79
+ };
80
+ //# sourceMappingURL=chunk-6VWPGRPL.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../lib/runtime/style.ts"],
4
+ "sourcesContent": ["/**\n * meno-astro \u2014 `style()` runtime resolver + CSS collector (proof-of-concept).\n *\n * Emitted `.astro` markup styles every node with `class={style(styleObject[, meta])}`\n * (see `dialect/emit/emitNode.ts:105-124`). The `styleObject` is a Meno\n * `StyleObject` / `ResponsiveStyleObject` \u2014 `{ base, tablet, mobile }` where any value\n * may be a prop-binding `{ _mapping: true, prop, values }`. The optional `meta` carries\n * `interactive` (hover/etc. state rules), `label`, and `genClass`.\n *\n * This module is the runtime side of that contract:\n * 1. `style()` returns the element's class name (the `class={...}` value), and\n * 2. side-effects the generated CSS into a module-level collector that a future\n * `BaseLayout`/integration will flush into a `<style>` tag.\n *\n * \u2500\u2500 Why a props argument (the \"option A\" design being proven here) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * A `_mapping` value resolves against the *host component's prop values*, but the\n * emitted `style(styleObject, meta)` call does NOT carry them. Option A threads a\n * `props` scope into the call \u2014 `style(styleObject, props, meta)` \u2014 and resolves each\n * `_mapping` to a concrete CSS value *before* generating CSS. This file implements and\n * unit-tests exactly that. (The emitter change that actually passes `props` at the call\n * site is the *next* step, gated on this PoC \u2014 it is intentionally NOT done here.)\n *\n * \u2500\u2500 Reuse, not reinvention \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * All CSS generation is meno-core's, the same code the JSON runtime's SSR uses:\n * - `generateInteractiveCSS` \u2014 turns a list of selector rules (base + `:hover` + \u2026)\n * into CSS, with full responsive `base`/`tablet`/`mobile` \u2192 `@media` handling and\n * the `styleObjectToCSS` property serializer. We model the base style as one rule\n * with an empty postfix (`.<class> { \u2026 }`) and append the interactive rules, so a\n * single call covers base + responsive + interactive, all scoped to one class.\n * - `DEFAULT_BREAKPOINTS` \u2014 the project's breakpoint media-query thresholds\n * (tablet 1024px, mobile 540px).\n * - `shortHash` \u2014 meno-core's deterministic djb2 hash, so identical styleObjects\n * dedupe to the same class name.\n * - `isStyleMapping` \u2014 the `_mapping` type guard; mapping *resolution* mirrors\n * meno-core's `resolveExtractedMappings` (`mapping.values[String(propValue)]`).\n *\n * \u2500\u2500 Collector design + the per-render-isolation caveat \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * The collector is a module-level `Map` keyed by class name (so repeated `style()`\n * calls for the same class are naturally de-duplicated within a render). `style()`\n * appends; `flushCollectedStyles()` concatenates; `resetStyleCollector()` clears.\n *\n * PRODUCTION HARDENING (out of scope for this PoC): SSR is concurrent, so a module-\n * level sink races across overlapping renders. The production form scopes the\n * collector to the async call tree with `AsyncLocalStorage` (node:async_hooks) \u2014 the\n * exact pattern already used for the locale context in `runtime/i18n.ts`'s\n * `runWithLocale`. A `BaseLayout` would `runWithStyleCollector(() => \u2026)` around the\n * render and flush the per-render sink into a `<style>`. The module-level + resettable\n * collector here is sufficient to *prove the resolver* under unit tests.\n */\n\n// Narrow `meno-core/shared/*` subpath imports (the same convention `runtime/i18n.ts`\n// uses) rather than the broad `meno-core` barrel \u2014 the barrel re-exports the whole\n// shared surface, dragging unrelated modules (and their latent type errors) into this\n// package's type-check graph. We only need the style pipeline.\nimport {\n DEFAULT_BREAKPOINTS,\n type BreakpointConfig,\n} from 'meno-core/shared';\nimport { generateInteractiveCSS } from 'meno-core/shared';\nimport { isStyleMapping } from 'meno-core/shared';\nimport { shortHash } from 'meno-core/shared';\n// The SAME forward mapper meno-core's JSON runtime (ComponentBuilder) uses \u2014 so the\n// emitted class names are byte-identical to Meno core. The utility CSS itself is\n// generated at BUILD time by the meno() integration (Astro renders <head> before\n// <body>, so a runtime collector would always be empty); style() only returns names.\nimport { responsiveStylesToClasses } from 'meno-core/shared';\nimport type {\n StyleObject,\n StyleValue,\n ResponsiveStyleObject,\n InteractiveStyles,\n InteractiveStyleRule,\n} from 'meno-core/shared/types';\n\n// ---------------------------------------------------------------------------\n// `meta` \u2014 the second/third argument shape emitted alongside the style object.\n// Mirrors `emitClassAttr` in dialect/emit/emitNode.ts.\n// ---------------------------------------------------------------------------\n\n/** The `meta` payload carried by `class={style(styleObject, meta)}`. */\nexport interface StyleMeta {\n /** Hover/focus/state rules (`.element:hover { \u2026 }`, etc.). */\n interactive?: InteractiveStyles;\n /** The node's editor label \u2014 folded into the (still content-derived) class name. */\n label?: string;\n /** The node's `generateElementClass` flag (carried for parity; not load-bearing here). */\n genClass?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Module-level CSS collector. See the per-render-isolation caveat in the header.\n// Keyed by class name so duplicate `style()` calls within a render emit once.\n// ---------------------------------------------------------------------------\n\nconst collected = new Map<string, string>();\n\n/** Drain the collector, returning all accumulated CSS as a single string (insertion order). */\nexport function flushCollectedStyles(): string {\n return Array.from(collected.values()).filter(Boolean).join('\\n');\n}\n\n/** Clear the collector. Call between renders (the PoC stand-in for per-render isolation). */\nexport function resetStyleCollector(): void {\n collected.clear();\n}\n\n// ---------------------------------------------------------------------------\n// `_mapping` (prop-binding) resolution \u2014 the option-A proof.\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a single `_mapping` value against `props`, returning the concrete CSS value\n * or `undefined` when it cannot be resolved (no props, prop unset, or value not in the\n * mapping's table). Semantics mirror meno-core's `resolveExtractedMappings`:\n * `mapping.values[String(props[mapping.prop])]`.\n */\nfunction resolveMappingValue(\n mapping: { prop: string; values: Record<string, string | number> },\n props: Record<string, unknown> | undefined,\n): string | number | undefined {\n if (!props) return undefined;\n const propValue = props[mapping.prop];\n if (propValue === undefined || propValue === null) return undefined;\n const resolved = mapping.values[String(propValue)];\n return resolved === undefined ? undefined : resolved;\n}\n\n/**\n * Return a copy of a flat StyleObject with every `_mapping` value replaced by its\n * prop-resolved concrete value. Unresolvable mappings are dropped (graceful\n * degradation \u2014 a missing prop must never throw, and an unresolved property simply\n * isn't emitted, falling back to the cascade/UA default). Plain values pass through.\n */\nfunction resolveMappingsInFlat(\n style: StyleObject,\n props: Record<string, unknown> | undefined,\n): StyleObject {\n const out: StyleObject = {};\n for (const [prop, value] of Object.entries(style)) {\n if (isStyleMapping(value)) {\n const resolved = resolveMappingValue(value, props);\n if (resolved !== undefined) out[prop] = resolved;\n // else: drop \u2014 unresolved mapping, no rule emitted.\n continue;\n }\n out[prop] = value;\n }\n return out;\n}\n\n/** True when a StyleValue is the responsive `{ base/tablet/mobile }` shape. */\nfunction isResponsive(style: StyleValue): style is ResponsiveStyleObject {\n return (\n typeof style === 'object' &&\n style !== null &&\n ('base' in style || 'tablet' in style || 'mobile' in style)\n );\n}\n\n/**\n * Resolve `_mapping` values across a whole StyleValue (flat or responsive), per\n * breakpoint, against `props`. The result is a plain StyleValue with no `_mapping`\n * objects remaining \u2014 ready for meno-core's CSS generator.\n */\nexport function resolveMappingsInStyle(\n style: StyleValue,\n props: Record<string, unknown> | undefined,\n): StyleValue {\n if (isResponsive(style)) {\n const out: ResponsiveStyleObject = {};\n for (const [bp, bpStyle] of Object.entries(style)) {\n if (!bpStyle) continue;\n out[bp] = resolveMappingsInFlat(bpStyle, props);\n }\n return out;\n }\n return resolveMappingsInFlat(style as StyleObject, props);\n}\n\n// ---------------------------------------------------------------------------\n// Deterministic class name.\n// ---------------------------------------------------------------------------\n\n/** Lowercase + CSS-safe a label fragment for use in a class name. */\nfunction sanitizeLabel(label: string): string {\n return label\n .toLowerCase()\n .replace(/[^a-z0-9_-]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '');\n}\n\n/**\n * Compute a deterministic class name from the *resolved* style payload (base style +\n * interactive rules) plus the optional label. Identical inputs \u21D2 identical class, so\n * two `style()` calls with the same styleObject dedupe to one CSS rule. The hash uses\n * meno-core's `shortHash` (djb2). A label prefix is added for human-readable selectors\n * but the hash still keys dedup, so distinct styles never collide on label alone.\n */\nexport function computeClassName(\n resolvedBase: StyleValue,\n resolvedInteractive: InteractiveStyles,\n label: string | undefined,\n): string {\n const fingerprint = JSON.stringify({ b: resolvedBase, i: resolvedInteractive });\n const hash = shortHash(fingerprint);\n const prefix = label ? sanitizeLabel(label) : '';\n return prefix ? `m_${prefix}_${hash}` : `m_${hash}`;\n}\n\n// ---------------------------------------------------------------------------\n// style() \u2014 the emitter-facing resolver.\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a Meno style payload to a CSS class name, side-effecting the generated CSS\n * into the module-level collector.\n *\n * @param styleObject The node's style \u2014 flat `StyleObject` or responsive\n * `{ base, tablet, mobile }`. Values may be `_mapping` prop bindings.\n * @param props The host component's resolved prop values, used to resolve\n * `_mapping` bindings (the option-A contract). Optional: a style\n * with a `_mapping` but no `props` degrades gracefully (the bound\n * property is omitted rather than throwing).\n * @param meta Optional `{ interactive, label, genClass }` \u2014 interactive state\n * rules and label, as emitted.\n * @returns The element's CSS class name (the `class={style(...)}` value).\n */\nexport function style(\n styleObject: StyleValue | null | undefined,\n props?: Record<string, unknown>,\n meta?: StyleMeta,\n): string {\n const base = styleObject ?? {};\n\n // 1. Resolve prop-bound `_mapping` values to concrete CSS values using the host\n // component's props (the same resolution meno-core does at render).\n const resolvedBase = resolveMappingsInStyle(base, props);\n\n // 2. Base styles \u2192 meno-core utility classes (byte-identical to the JSON runtime).\n const classes = responsiveStylesToClasses(resolvedBase);\n\n // 3. Interactive (`:hover`, \u2026) styles aren't expressible as utility classes (they're\n // states), so meno-core scopes them to an element-specific class. Resolve their\n // `_mapping`s the same way and emit a DETERMINISTIC class so the build-time CSS\n // scan generates the matching `.<class>:hover { \u2026 }` rule. No interactive \u21D2 no\n // extra class.\n const resolvedInteractive: InteractiveStyles = (meta?.interactive ?? []).map(\n (rule): InteractiveStyleRule => ({\n ...rule,\n style: resolveMappingsInStyle(rule.style, props),\n }),\n );\n if (resolvedInteractive.length > 0) {\n classes.push(computeClassName({}, resolvedInteractive, meta?.label));\n }\n\n return classes.join(' ');\n}\n"],
5
+ "mappings": ";;;;;;;AA8FA,IAAM,YAAY,oBAAI,IAAoB;AAGnC,SAAS,uBAA+B;AAC7C,SAAO,MAAM,KAAK,UAAU,OAAO,CAAC,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AACjE;AAGO,SAAS,sBAA4B;AAC1C,YAAU,MAAM;AAClB;AAYA,SAAS,oBACP,SACA,OAC6B;AAC7B,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,YAAY,MAAM,QAAQ,IAAI;AACpC,MAAI,cAAc,UAAa,cAAc,KAAM,QAAO;AAC1D,QAAM,WAAW,QAAQ,OAAO,OAAO,SAAS,CAAC;AACjD,SAAO,aAAa,SAAY,SAAY;AAC9C;AAQA,SAAS,sBACPA,QACA,OACa;AACb,QAAM,MAAmB,CAAC;AAC1B,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQA,MAAK,GAAG;AACjD,QAAI,eAAe,KAAK,GAAG;AACzB,YAAM,WAAW,oBAAoB,OAAO,KAAK;AACjD,UAAI,aAAa,OAAW,KAAI,IAAI,IAAI;AAExC;AAAA,IACF;AACA,QAAI,IAAI,IAAI;AAAA,EACd;AACA,SAAO;AACT;AAGA,SAAS,aAAaA,QAAmD;AACvE,SACE,OAAOA,WAAU,YACjBA,WAAU,SACT,UAAUA,UAAS,YAAYA,UAAS,YAAYA;AAEzD;AAOO,SAAS,uBACdA,QACA,OACY;AACZ,MAAI,aAAaA,MAAK,GAAG;AACvB,UAAM,MAA6B,CAAC;AACpC,eAAW,CAAC,IAAI,OAAO,KAAK,OAAO,QAAQA,MAAK,GAAG;AACjD,UAAI,CAAC,QAAS;AACd,UAAI,EAAE,IAAI,sBAAsB,SAAS,KAAK;AAAA,IAChD;AACA,WAAO;AAAA,EACT;AACA,SAAO,sBAAsBA,QAAsB,KAAK;AAC1D;AAOA,SAAS,cAAc,OAAuB;AAC5C,SAAO,MACJ,YAAY,EACZ,QAAQ,gBAAgB,GAAG,EAC3B,QAAQ,OAAO,GAAG,EAClB,QAAQ,UAAU,EAAE;AACzB;AASO,SAAS,iBACd,cACA,qBACA,OACQ;AACR,QAAM,cAAc,KAAK,UAAU,EAAE,GAAG,cAAc,GAAG,oBAAoB,CAAC;AAC9E,QAAM,OAAO,UAAU,WAAW;AAClC,QAAM,SAAS,QAAQ,cAAc,KAAK,IAAI;AAC9C,SAAO,SAAS,KAAK,MAAM,IAAI,IAAI,KAAK,KAAK,IAAI;AACnD;AAoBO,SAAS,MACd,aACA,OACA,MACQ;AACR,QAAM,OAAO,eAAe,CAAC;AAI7B,QAAM,eAAe,uBAAuB,MAAM,KAAK;AAGvD,QAAM,UAAU,0BAA0B,YAAY;AAOtD,QAAM,uBAA0C,MAAM,eAAe,CAAC,GAAG;AAAA,IACvE,CAAC,UAAgC;AAAA,MAC/B,GAAG;AAAA,MACH,OAAO,uBAAuB,KAAK,OAAO,KAAK;AAAA,IACjD;AAAA,EACF;AACA,MAAI,oBAAoB,SAAS,GAAG;AAClC,YAAQ,KAAK,iBAAiB,CAAC,GAAG,qBAAqB,MAAM,KAAK,CAAC;AAAA,EACrE;AAEA,SAAO,QAAQ,KAAK,GAAG;AACzB;",
6
+ "names": ["style"]
7
+ }