@pyreon/unistyle 0.33.0 → 0.35.0

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/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
- import { nativeCompat, provide } from "@pyreon/core";
2
- import { ThemeContext } from "@pyreon/styler";
1
+ import { h, nativeCompat, provide } from "@pyreon/core";
2
+ import { ThemeContext, css, normalizeCSS, sheet, useThemeAccessor } from "@pyreon/styler";
3
3
  import { Provider as Provider$1, config, context, isEmpty, set } from "@pyreon/ui-core";
4
+ import { renderEffect } from "@pyreon/reactivity";
4
5
 
5
6
  //#region src/responsive/breakpoints.ts
6
7
  const breakpoints = {
@@ -26,10 +27,10 @@ const createMediaQueries = ((props) => {
26
27
  else if (breakpointValue != null) {
27
28
  const emSize = breakpointValue / rootSize;
28
29
  acc[key] = (...args) => css`
29
- @media only screen and (min-width: ${emSize}em) {
30
- ${css(...args)};
31
- }
32
- `;
30
+ @media only screen and (min-width: ${emSize}em) {
31
+ ${css(...args)};
32
+ }
33
+ `;
33
34
  }
34
35
  }
35
36
  return acc;
@@ -150,6 +151,7 @@ const parse = (css) => {
150
151
  depth--;
151
152
  if (depth === 0) {
152
153
  const raw = css.slice(segmentStart, i + 1).trim();
154
+ /* v8 ignore next */
153
155
  if (raw) entries.push({
154
156
  kind: "block",
155
157
  raw
@@ -210,9 +212,11 @@ const optimizeBreakpointDeltas = (cssStrings) => {
210
212
  //#endregion
211
213
  //#region src/responsive/optimizeTheme.ts
212
214
  const shallowEqual = (a, b) => {
213
- /* v8 ignore next 2 — defensive identity + null guards; both arms structurally covered */
215
+ /* v8 ignore start — defensive identity + null guards; a/b are always present,
216
+ distinct breakpoint objects in real usage */
214
217
  if (a === b) return true;
215
218
  if (!a || !b) return false;
219
+ /* v8 ignore stop */
216
220
  let aCount = 0;
217
221
  for (const key in a) {
218
222
  aCount++;
@@ -389,6 +393,7 @@ const makeItResponsive = ({ theme: customTheme, key = "", css, styles, normalize
389
393
  `;
390
394
  });
391
395
  const cacheEntry = themeCache.get(internalTheme);
396
+ /* v8 ignore else */
392
397
  if (cacheEntry) {
393
398
  if (!cacheEntry.rendered) cacheEntry.rendered = /* @__PURE__ */ new WeakMap();
394
399
  cacheEntry.rendered.set(theme, result);
@@ -735,54 +740,114 @@ function resolveVarPass(s, registry) {
735
740
  }
736
741
 
737
742
  //#endregion
738
- //#region src/styles/alignContent.ts
739
- const ALIGN_CONTENT_MAP_SHARED = {
740
- center: "center",
741
- spaceBetween: "space-between",
742
- spaceAround: "space-around",
743
- block: "stretch"
744
- };
745
- const ALIGN_CONTENT_MAP_X = {
746
- left: "flex-start",
747
- right: "flex-end",
748
- ...ALIGN_CONTENT_MAP_SHARED
749
- };
750
- const ALIGN_CONTENT_MAP_Y = {
751
- top: "flex-start",
752
- bottom: "flex-end",
753
- ...ALIGN_CONTENT_MAP_SHARED
754
- };
755
- const ALIGN_CONTENT_DIRECTION = {
756
- inline: "row",
757
- reverseInline: "row-reverse",
758
- rows: "column",
759
- reverseRows: "column-reverse"
760
- };
761
- const alignContent = (attrs) => {
762
- const { direction, alignX, alignY } = attrs;
763
- if (isEmpty(attrs) || !direction || !alignX || !alignY) return null;
764
- const isReverted = direction === "inline" || direction === "reverseInline";
765
- const dir = ALIGN_CONTENT_DIRECTION[direction];
766
- const x = ALIGN_CONTENT_MAP_X[alignX];
767
- const y = ALIGN_CONTENT_MAP_Y[alignY];
768
- return `flex-direction: ${dir}; align-items: ${isReverted ? y : x}; justify-content: ${isReverted ? x : y};`;
769
- };
770
-
771
- //#endregion
772
- //#region src/styles/extendCss.ts
773
- const simpleCss = (strings, ...values) => {
774
- let result = "";
775
- for (let i = 0; i < strings.length; i++) {
776
- result += strings[i];
777
- if (i < values.length) result += String(values[i] ?? "");
743
+ //#region src/cpse.ts
744
+ /**
745
+ * Custom-Property Style Extraction (CPSE) — Phase 0 proof-of-concept primitive.
746
+ *
747
+ * See `.claude/audits/custom-property-style-extraction-2026-06-22.md` for the
748
+ * full RFC. The thesis: decouple a style prop's **CSS-rule identity** from its
749
+ * **value identity**. Instead of baking the resolved value into the rule —
750
+ *
751
+ * gap: 2.25rem // value-DEPENDENT rule → a new rule + a
752
+ * // `styler.resolve` per distinct value
753
+ * // (cost O(distinct value tuples), proven by
754
+ * // styler/__tests__/static-styler-resolve-cost)
755
+ *
756
+ * — emit a value-AGNOSTIC rule that reads a custom property, and deliver the
757
+ * value per-instance as an inline custom property —
758
+ *
759
+ * gap: var(--u-<hash>) // ONE shared rule, resolved ONCE per
760
+ * // definition; cost O(component definitions)
761
+ * style="--u-<hash>: 2.25rem" // per-instance, no resolve / no hash /
762
+ * // no new rule
763
+ *
764
+ * This makes styling cost **flat in app cardinality** and gives **dynamic
765
+ * (signal-driven) values for free** (update the inline custom property — no
766
+ * `styler.resolve`, no rule churn).
767
+ *
768
+ * Phase 0 scope: a single declaration / single property, to PROVE the
769
+ * mechanism (1 rule + 1 resolve for N distinct values; computed-style parity
770
+ * with the value-baked path; nesting-safe). Phases 1-4 (RFC §5) generalize
771
+ * across the 170+ unistyle property mappings, responsive arrays (per-breakpoint
772
+ * vars + media queries), the dynamic path, and rocketstyle integration.
773
+ *
774
+ * Self-contained: depends only on unistyle's own `value()` conversion + an
775
+ * inline FNV-1a. Does NOT import `@pyreon/styler` (keeps the primitive
776
+ * layer-pure; only the measurement TESTS reach for the styler counters).
777
+ */
778
+ const fnv1a = (s) => {
779
+ let h = 2166136261;
780
+ for (let i = 0; i < s.length; i++) {
781
+ h ^= s.charCodeAt(i);
782
+ h = Math.imul(h, 16777619);
778
783
  }
779
- return result;
780
- };
781
- const extendCss = (styles) => {
782
- if (!styles) return "";
783
- if (typeof styles === "function") return styles(simpleCss);
784
- return styles;
784
+ return (h >>> 0).toString(36);
785
785
  };
786
+ /**
787
+ * Extract one style declaration into a value-agnostic rule + a per-instance
788
+ * custom-property value.
789
+ *
790
+ * @param property CSS property name (already in CSS-spec form, e.g. `gap`).
791
+ * @param rawValue The author-supplied value (`36`, `"1rem"`, `"var(--x)"`, …).
792
+ * @param rootSize px→rem base (defaults to 16, matching `value()`).
793
+ */
794
+ function extractStyleVar(property, rawValue, rootSize = 16) {
795
+ const varName = cpseVarName(property);
796
+ const converted = value(rawValue, rootSize);
797
+ return {
798
+ rule: `${property}:var(${varName})`,
799
+ varName,
800
+ varValue: converted == null ? null : String(converted)
801
+ };
802
+ }
803
+ /**
804
+ * Canonical CPSE custom-property name for a CSS property (+ optional
805
+ * breakpoint suffix for the responsive path). Stable + collision-free across
806
+ * distinct property names; shared across components for the same property
807
+ * (intended — every instance sets its own inline value, §nesting test).
808
+ */
809
+ function cpseVarName(property, breakpoint) {
810
+ return breakpoint ? `--u-${fnv1a(property)}-${breakpoint}` : `--u-${fnv1a(property)}`;
811
+ }
812
+ const STRUCTURAL = /[{}&@]|url\(/;
813
+ /**
814
+ * Rewrite every flat `prop: value;` declaration in a resolved CSS fragment to
815
+ * the value-agnostic `prop: var(--u-<hash>[-bp]);` form, writing each value
816
+ * into `varsOut`. Returns the rewritten fragment. Fragments carrying any
817
+ * structure (selectors, at-rules, nesting, url()) are returned UNCHANGED.
818
+ *
819
+ * This operates on `processDescriptor`'s ALREADY-RESOLVED output, so it
820
+ * inherits every unit-conversion / shorthand correctness for free and stays
821
+ * general across the whole property map.
822
+ *
823
+ * @param frag a resolved CSS fragment, e.g. `"gap: 2.25rem;"` or
824
+ * `"margin: 1rem 2rem;"` (possibly several `;`-separated).
825
+ * @param varsOut sink: `varName → value` for the per-instance inline props.
826
+ * @param breakpoint optional suffix so per-breakpoint values get distinct vars.
827
+ */
828
+ function cpseRewrite(frag, varsOut, breakpoint) {
829
+ if (!frag || STRUCTURAL.test(frag)) return frag;
830
+ let out = "";
831
+ for (const decl of frag.split(";")) {
832
+ const trimmed = decl.trim();
833
+ if (!trimmed) continue;
834
+ const colon = trimmed.indexOf(":");
835
+ if (colon < 1) {
836
+ out += `${trimmed};`;
837
+ continue;
838
+ }
839
+ const prop = trimmed.slice(0, colon).trim();
840
+ const val = trimmed.slice(colon + 1).trim();
841
+ if (!val || val.startsWith("var(") || !/^[a-z][a-z-]*$/.test(prop)) {
842
+ out += `${trimmed};`;
843
+ continue;
844
+ }
845
+ const varName = cpseVarName(prop, breakpoint);
846
+ varsOut[varName] = val;
847
+ out += `${prop}:var(${varName});`;
848
+ }
849
+ return out;
850
+ }
786
851
 
787
852
  //#endregion
788
853
  //#region src/styles/shorthands/borderRadius.ts
@@ -2387,7 +2452,7 @@ for (let i = 0; i < propertyMap.length; i++) {
2387
2452
  * pseudo-selectors, and @layer wrapping.
2388
2453
  */
2389
2454
  const _seen = /* @__PURE__ */ new Set();
2390
- const styles = ({ theme: t, css, rootSize }) => {
2455
+ const styles = ({ theme: t, css, rootSize, extractVars, breakpoint }) => {
2391
2456
  if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("unistyle.styles");
2392
2457
  const calc = (...params) => values(params, rootSize);
2393
2458
  const shorthand = edge(rootSize);
@@ -2401,14 +2466,18 @@ const styles = ({ theme: t, css, rootSize }) => {
2401
2466
  if (_seen.has(idx)) continue;
2402
2467
  _seen.add(idx);
2403
2468
  if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("unistyle.descriptor");
2404
- fragments.push(processDescriptor(propertyMap[idx], t, css, calc, shorthand, borderRadiusFn));
2469
+ let frag = processDescriptor(propertyMap[idx], t, css, calc, shorthand, borderRadiusFn);
2470
+ if (extractVars && typeof frag === "string") frag = cpseRewrite(frag, extractVars, breakpoint);
2471
+ fragments.push(frag);
2405
2472
  }
2406
2473
  }
2407
2474
  if (fragments.length === 0 && Object.keys(t).length > 0) {
2408
2475
  if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("unistyle.descriptor.fallback-scan");
2409
2476
  for (const d of propertyMap) {
2410
2477
  if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("unistyle.descriptor");
2411
- fragments.push(processDescriptor(d, t, css, calc, shorthand, borderRadiusFn));
2478
+ let frag = processDescriptor(d, t, css, calc, shorthand, borderRadiusFn);
2479
+ if (extractVars && typeof frag === "string") frag = cpseRewrite(frag, extractVars, breakpoint);
2480
+ fragments.push(frag);
2412
2481
  }
2413
2482
  }
2414
2483
  return css`
@@ -2417,5 +2486,205 @@ const styles = ({ theme: t, css, rootSize }) => {
2417
2486
  };
2418
2487
 
2419
2488
  //#endregion
2420
- export { ALIGN_CONTENT_DIRECTION, ALIGN_CONTENT_MAP_X, ALIGN_CONTENT_MAP_Y, CSS_VARS_DEFAULT_EXCLUDE, Provider, alignContent, breakpoints, context, createMediaQueries, enrichTheme, extendCss, makeItResponsive, normalizeTheme, resolveCssVarReferences, sortBreakpoints, stripUnit, styles, themeToCssVars, transformTheme, value, values };
2489
+ //#region src/cpse-styled.tsx
2490
+ const RESERVED = /* @__PURE__ */ new Set([
2491
+ "styles",
2492
+ "rootSize",
2493
+ "breakpoints",
2494
+ "class",
2495
+ "ref",
2496
+ "children"
2497
+ ]);
2498
+ const DEFAULT_BREAKPOINTS = {
2499
+ xs: 0,
2500
+ sm: 576,
2501
+ md: 768,
2502
+ lg: 992,
2503
+ xl: 1200
2504
+ };
2505
+ /** A value is responsive if it's a mobile-first array or a breakpoint object
2506
+ * (a `var(--x)` / plain string is NOT — strings are scalar). */
2507
+ const isResponsiveValue = (v) => Array.isArray(v) || v != null && typeof v === "object";
2508
+ const isResponsiveTheme = (t) => {
2509
+ for (const k in t) if (isResponsiveValue(t[k])) return true;
2510
+ return false;
2511
+ };
2512
+ /** Expand a responsive theme into per-breakpoint flat themes (only the
2513
+ * breakpoints that actually carry a value). Mobile-first arrays map index→
2514
+ * sorted-breakpoint; breakpoint objects map by key; scalars land on the base. */
2515
+ function expandResponsive(t, sorted) {
2516
+ const perBp = /* @__PURE__ */ new Map();
2517
+ const put = (bp, prop, val) => {
2518
+ let m = perBp.get(bp);
2519
+ if (!m) {
2520
+ m = {};
2521
+ perBp.set(bp, m);
2522
+ }
2523
+ m[prop] = val;
2524
+ };
2525
+ const rec = t;
2526
+ for (const prop in rec) {
2527
+ const v = rec[prop];
2528
+ if (Array.isArray(v)) {
2529
+ for (let i = 0; i < v.length && i < sorted.length; i++) if (v[i] != null) put(sorted[i], prop, v[i]);
2530
+ } else if (v != null && typeof v === "object") for (const bp in v) {
2531
+ const bv = v[bp];
2532
+ if (sorted.includes(bp) && bv != null) put(bp, prop, bv);
2533
+ }
2534
+ else put(sorted[0], prop, v);
2535
+ }
2536
+ return perBp;
2537
+ }
2538
+ function cpseStyled(tag) {
2539
+ const classByShape = /* @__PURE__ */ new Map();
2540
+ /**
2541
+ * Compute the per-instance var map (always) and the value-agnostic className
2542
+ * (resolved + inserted only on the first sighting of a shape). Returns both.
2543
+ * `styles({ extractVars })` writes vars synchronously while building fragments,
2544
+ * so vars are ready without the expensive `String(...)` (→ `styler.resolve`)
2545
+ * + insert — those run only on a cache miss.
2546
+ */
2547
+ const resolve = (styleTheme, rootSize, breakpoints) => {
2548
+ const vars = {};
2549
+ if (!isResponsiveTheme(styleTheme)) {
2550
+ const result = styles({
2551
+ theme: styleTheme,
2552
+ css,
2553
+ rootSize,
2554
+ extractVars: vars
2555
+ });
2556
+ const shapeKey = `flat:${Object.keys(styleTheme).sort().join("|")}`;
2557
+ let className = classByShape.get(shapeKey);
2558
+ if (className === void 0) {
2559
+ const text = normalizeCSS(String(result));
2560
+ className = text.length > 0 ? sheet.insert(text) : "";
2561
+ classByShape.set(shapeKey, className);
2562
+ }
2563
+ return {
2564
+ className,
2565
+ vars
2566
+ };
2567
+ }
2568
+ const sorted = sortBreakpoints(breakpoints);
2569
+ const perBp = expandResponsive(styleTheme, sorted);
2570
+ const mq = createMediaQueries({
2571
+ breakpoints,
2572
+ rootSize,
2573
+ css
2574
+ });
2575
+ const frags = [];
2576
+ const shapeParts = [];
2577
+ for (const bp of sorted) {
2578
+ const flat = perBp.get(bp);
2579
+ if (!flat || Object.keys(flat).length === 0) continue;
2580
+ const decl = String(styles({
2581
+ theme: flat,
2582
+ css,
2583
+ rootSize,
2584
+ extractVars: vars,
2585
+ breakpoint: bp
2586
+ }));
2587
+ frags.push(String(mq[bp]`${decl}`));
2588
+ shapeParts.push(`${bp}:${Object.keys(flat).sort().join(",")}`);
2589
+ }
2590
+ const shapeKey = `resp:${shapeParts.join("|")}`;
2591
+ let className = classByShape.get(shapeKey);
2592
+ if (className === void 0) {
2593
+ const text = normalizeCSS(frags.join("\n"));
2594
+ className = text.length > 0 ? sheet.insert(text) : "";
2595
+ classByShape.set(shapeKey, className);
2596
+ }
2597
+ return {
2598
+ className,
2599
+ vars
2600
+ };
2601
+ };
2602
+ return (props) => {
2603
+ const theme = useThemeAccessor()() ?? {};
2604
+ const rootSize = props.rootSize ?? theme.rootSize ?? 16;
2605
+ const breakpoints = props.breakpoints ?? theme.breakpoints ?? DEFAULT_BREAKPOINTS;
2606
+ const stylesProp = props.styles;
2607
+ const isDynamic = typeof stylesProp === "function";
2608
+ const getTheme = () => isDynamic ? stylesProp() : stylesProp ?? {};
2609
+ const first = resolve(getTheme(), rootSize, breakpoints);
2610
+ const className = props.class ? `${first.className} ${props.class}` : first.className;
2611
+ const rest = {};
2612
+ for (const k in props) if (!RESERVED.has(k)) rest[k] = props[k];
2613
+ let el = null;
2614
+ const applyVars = (vars) => {
2615
+ if (!el) return;
2616
+ for (const k in vars) el.style.setProperty(k, vars[k]);
2617
+ };
2618
+ const ref = (node) => {
2619
+ el = node;
2620
+ if (el) applyVars(first.vars);
2621
+ const userRef = props.ref;
2622
+ if (typeof userRef === "function") userRef(node);
2623
+ else if (userRef && typeof userRef === "object") userRef.current = node;
2624
+ };
2625
+ if (isDynamic) renderEffect(() => {
2626
+ applyVars(resolve(getTheme(), rootSize, breakpoints).vars);
2627
+ });
2628
+ const children = props.children != null ? [props.children] : [];
2629
+ return h(tag, {
2630
+ class: className,
2631
+ style: first.vars,
2632
+ ref,
2633
+ ...rest
2634
+ }, ...children);
2635
+ };
2636
+ }
2637
+
2638
+ //#endregion
2639
+ //#region src/styles/alignContent.ts
2640
+ const ALIGN_CONTENT_MAP_SHARED = {
2641
+ center: "center",
2642
+ spaceBetween: "space-between",
2643
+ spaceAround: "space-around",
2644
+ block: "stretch"
2645
+ };
2646
+ const ALIGN_CONTENT_MAP_X = {
2647
+ left: "flex-start",
2648
+ right: "flex-end",
2649
+ ...ALIGN_CONTENT_MAP_SHARED
2650
+ };
2651
+ const ALIGN_CONTENT_MAP_Y = {
2652
+ top: "flex-start",
2653
+ bottom: "flex-end",
2654
+ ...ALIGN_CONTENT_MAP_SHARED
2655
+ };
2656
+ const ALIGN_CONTENT_DIRECTION = {
2657
+ inline: "row",
2658
+ reverseInline: "row-reverse",
2659
+ rows: "column",
2660
+ reverseRows: "column-reverse"
2661
+ };
2662
+ const alignContent = (attrs) => {
2663
+ const { direction, alignX, alignY } = attrs;
2664
+ if (isEmpty(attrs) || !direction || !alignX || !alignY) return null;
2665
+ const isReverted = direction === "inline" || direction === "reverseInline";
2666
+ const dir = ALIGN_CONTENT_DIRECTION[direction];
2667
+ const x = ALIGN_CONTENT_MAP_X[alignX];
2668
+ const y = ALIGN_CONTENT_MAP_Y[alignY];
2669
+ return `flex-direction: ${dir}; align-items: ${isReverted ? y : x}; justify-content: ${isReverted ? x : y};`;
2670
+ };
2671
+
2672
+ //#endregion
2673
+ //#region src/styles/extendCss.ts
2674
+ const simpleCss = (strings, ...values) => {
2675
+ let result = "";
2676
+ for (let i = 0; i < strings.length; i++) {
2677
+ result += strings[i];
2678
+ if (i < values.length) result += String(values[i] ?? "");
2679
+ }
2680
+ return result;
2681
+ };
2682
+ const extendCss = (styles) => {
2683
+ if (!styles) return "";
2684
+ if (typeof styles === "function") return styles(simpleCss);
2685
+ return styles;
2686
+ };
2687
+
2688
+ //#endregion
2689
+ export { ALIGN_CONTENT_DIRECTION, ALIGN_CONTENT_MAP_X, ALIGN_CONTENT_MAP_Y, CSS_VARS_DEFAULT_EXCLUDE, Provider, alignContent, breakpoints, context, cpseRewrite, cpseStyled, cpseVarName, createMediaQueries, enrichTheme, extendCss, extractStyleVar, makeItResponsive, normalizeTheme, resolveCssVarReferences, sortBreakpoints, stripUnit, styles, themeToCssVars, transformTheme, value, values };
2421
2690
  //# sourceMappingURL=index.js.map