@yahoo/uds-create-config 5.25.2 → 5.26.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.
Files changed (44) hide show
  1. package/dist/configs/system.js +2 -0
  2. package/dist/entities/system/Component.d.ts +35 -2
  3. package/dist/entities/system/Component.js +224 -9
  4. package/dist/entities/system/Settings.js +1 -1
  5. package/dist/entities/system/StyleProperty.js +1 -1
  6. package/dist/entities/system/component-module.d.ts +17 -1
  7. package/dist/entities/system/component-module.js +54 -2
  8. package/dist/entities/system/defineComponent.d.ts +53 -13
  9. package/dist/entities/system/defineComponent.js +1 -0
  10. package/dist/entities/system/forward-route-invariants.js +1 -1
  11. package/dist/entities/system/icon-domain-invariants.js +1 -1
  12. package/dist/entities/system/root-layer-invariants.js +1 -1
  13. package/dist/entities/system/token-values.js +1 -1
  14. package/dist/framework/Config.d.ts +1 -1
  15. package/dist/framework/Config.js +1 -1
  16. package/dist/framework/class-names.d.ts +27 -1
  17. package/dist/framework/class-names.js +56 -3
  18. package/dist/framework/component-states.d.ts +40 -0
  19. package/dist/framework/component-states.js +53 -0
  20. package/dist/framework/interaction-conditions.d.ts +48 -0
  21. package/dist/framework/interaction-conditions.js +104 -0
  22. package/dist/framework/layer-styles.d.ts +5 -1
  23. package/dist/framework/layer-styles.js +18 -4
  24. package/dist/framework/projections.js +44 -2
  25. package/dist/framework/render-spec.js +14 -4
  26. package/dist/framework/schema-version.d.ts +1 -1
  27. package/dist/framework/schema-version.js +5 -5
  28. package/dist/framework/validate-spec.js +14 -0
  29. package/dist/index.d.ts +5 -3
  30. package/dist/index.js +6 -4
  31. package/dist/migrations/20261013154037_component_states.d.ts +19 -0
  32. package/dist/migrations/20261013154037_component_states.js +74 -0
  33. package/dist/renderer/componentEntry.d.ts +7 -1
  34. package/dist/renderer/componentEntry.js +30 -1
  35. package/dist/renderer/forced-stamp.d.ts +26 -0
  36. package/dist/renderer/forced-stamp.js +50 -0
  37. package/dist/renderer/index.d.ts +3 -1
  38. package/dist/renderer/index.js +3 -1
  39. package/dist/renderer/use-forced-stamp.d.ts +11 -0
  40. package/dist/renderer/use-forced-stamp.js +50 -0
  41. package/dist/spec/specToJsx.d.ts +6 -0
  42. package/dist/spec/specToJsx.js +5 -0
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +2 -2
@@ -627,8 +627,35 @@ type DefaultsOf<P extends PropMap> = Partial<DeclaredProps<P>> & {
627
627
  * `as` clause drops exactly the named keys and keeps the rest as declared.
628
628
  */
629
629
  type Without<T, K extends PropertyKey> = string extends K ? T : { [Key in keyof T as Key extends K ? never : Key]: T[Key] };
630
+ /** One state a component declares in `.states()` — what its render knows and a style rule keys on.
631
+ * A boolean (`filled`), or a closed set of names with an optional default (`phase`). */
632
+ type StateDecl = {
633
+ readonly type: 'boolean';
634
+ } | {
635
+ readonly type: 'variant';
636
+ readonly value: readonly string[];
637
+ readonly default?: string;
638
+ };
639
+ /** A component's states, by name. */
640
+ type StateMap = Readonly<Record<string, StateDecl>>;
641
+ /** The value each state of `S` takes: a boolean's `true`/`false`, a variant's names. */
642
+ type StateValues<S> = { [K in keyof S]?: S[K] extends {
643
+ type: 'variant';
644
+ value: readonly (infer V)[];
645
+ } ? V : boolean };
646
+ /**
647
+ * How a render reports its states — `const layers = states(props, { filled: value !== '' })` — and
648
+ * spreads the bundles it hands back: `<Box {...layers.root}>`.
649
+ *
650
+ * A pure call, not a hook, so it can sit under a condition. It takes the render's `props` (the
651
+ * bundles at rest, or as Studio or a test forced them) and returns the bundles BUILT FOR those
652
+ * states, a fresh set: every rule naming a state, a prop a rule sets on a layer, the root's
653
+ * `data-<name>` attribute, a portal layer and a nested instance all follow the values given. A
654
+ * forced value still wins over them.
655
+ */
656
+ type StatesHelper<S extends StateMap, L extends LayerMap = LayerMap> = (props: Record<keyof L & string, Record<string, unknown>>, values: StateValues<S>) => Record<keyof L & string, Record<string, unknown>>;
630
657
  /** What a render receives: the generated per-layer bundle, plus the props the component declared. */
631
- type RenderArgs<L extends LayerMap, P extends PropMap> = {
658
+ type RenderArgs<L extends LayerMap, P extends PropMap, S extends StateMap = Record<never, never>> = {
632
659
  readonly props: Record<keyof L & string, Record<string, unknown>>;
633
660
  /**
634
661
  * The ref the emitted module forwards to the render, for the element it chooses to attach it to.
@@ -637,7 +664,9 @@ type RenderArgs<L extends LayerMap, P extends PropMap> = {
637
664
  * `<input ref={ref}>` in another — and which element is the render's decision.
638
665
  */
639
666
  readonly ref?: Ref<any>;
640
- } & DeclaredProps<P, L, unknown, 'render'> & Record<string, unknown>;
667
+ } & ([keyof S] extends [never] ? Record<never, never> : {
668
+ readonly states: StatesHelper<S, L>;
669
+ }) & DeclaredProps<P, L, unknown, 'render'> & Record<string, unknown>;
641
670
  interface ComponentBuilder<L extends LayerMap, P extends PropMap,
642
671
  /** Props the RENDER takes that the config doesn't describe — React handlers, mostly (`onPress`).
643
672
  * They're part of the component's surface but not of its data, which is why they're declared at
@@ -650,7 +679,7 @@ D = Record<never, never>,
650
679
  * `.forwards()` so `ComponentPropsOf` can union each forwarded layer's element props — `root` is
651
680
  * already covered structurally by {@link ElementOf}, so for a root-only forward this changes no
652
681
  * inferred type, which is what makes replacing `inherits` a no-op at every call site. */
653
- F extends string = never> extends Authored {
682
+ F extends string = never, /** The states `.states()` declared — what the render knows and reports, never a caller's prop. */S extends StateMap = Record<never, never>> extends Authored {
654
683
  /**
655
684
  * The phantom call signature — the authored value is never rendered (the build substitutes the
656
685
  * emitted module), but a call site is checked against it. Generic over the element a call site
@@ -671,7 +700,7 @@ F extends string = never> extends Authored {
671
700
  /** The authored body, readable so codegen can pair it with the render found in the source. */
672
701
  readonly body: Record<string, unknown>;
673
702
  readonly path?: string;
674
- layers<const L2 extends LayerMap>(layers: L2): ComponentBuilder<L2, P, E, D, F>;
703
+ layers<const L2 extends LayerMap>(layers: L2): ComponentBuilder<L2, P, E, D, F, S>;
675
704
  /**
676
705
  * This component's props.
677
706
  *
@@ -680,8 +709,19 @@ F extends string = never> extends Authored {
680
709
  * provide. The callback and its `layer`/`bind` helpers are gone — they only ever split a path string
681
710
  * that the parameter's own type now checks.
682
711
  */
683
- props<const P2 extends PropMap<LayerPath<L>>>(props: P2 & NoInfer<{ [K in keyof P2]: Exact<P2[K], AuthoredProp<LayerPath<L>>> }>): ComponentBuilder<L, P2, E, D, F>;
684
- styles(rules: readonly StyleRuleInput<L>[]): ComponentBuilder<L, P, E, D, F>;
712
+ props<const P2 extends PropMap<LayerPath<L>>>(props: P2 & NoInfer<{ [K in keyof P2]: Exact<P2[K], AuthoredProp<LayerPath<L>>> }>): ComponentBuilder<L, P2, E, D, F, S>;
713
+ /**
714
+ * What the render knows that a style rule keys on — the field has a value, the image loaded, the
715
+ * indicator is sliding: `{ filled: { type: 'boolean' } }`, `{ phase: { type: 'variant', value:
716
+ * ['idle', 'sliding'], default: 'idle' } }`.
717
+ *
718
+ * Not props, so not part of the component's API: the render passes its `props` and the values it
719
+ * knows to `const layers = states(props, { filled })` and spreads those layers in `props`' place;
720
+ * a rule naming a state in `when` follows the values given. A value a caller controls is a prop;
721
+ * the render reports the state from it.
722
+ */
723
+ states<const S2 extends StateMap>(states: S2): ComponentBuilder<L, P, E, D, F, S2>;
724
+ styles(rules: readonly StyleRuleInput<L>[]): ComponentBuilder<L, P, E, D, F, S>;
685
725
  /**
686
726
  * What this component applies with nothing set.
687
727
  *
@@ -692,7 +732,7 @@ F extends string = never> extends Authored {
692
732
  * `const`, so an `as` keeps its literal type: without it `{ as: 'input' }` widens to `string` and
693
733
  * there's no tag left to resolve the element's props against.
694
734
  */
695
- defaultProps<const D2 extends DefaultsOf<P>>(defaults: D2): ComponentBuilder<L, P, E, D2, F>;
735
+ defaultProps<const D2 extends DefaultsOf<P>>(defaults: D2): ComponentBuilder<L, P, E, D2, F, S>;
696
736
  /**
697
737
  * The layers whose element props this component ALSO exposes, keyed by layer.
698
738
  *
@@ -704,7 +744,7 @@ F extends string = never> extends Authored {
704
744
  * source FROM stored config — every keyed collection there is one call with a record literal, and a
705
745
  * second authored form would make generated source drift from what a person writes.
706
746
  */
707
- forwards<const F2 extends Readonly<Partial<Record<keyof L & string, Forwarded>>>>(forwards: F2): ComponentBuilder<L, P, E, D, F | (keyof F2 & string)>;
747
+ forwards<const F2 extends Readonly<Partial<Record<keyof L & string, Forwarded>>>>(forwards: F2): ComponentBuilder<L, P, E, D, F | (keyof F2 & string), S>;
708
748
  /**
709
749
  * What a person is told about this component: the label a picker shows, and what it's for.
710
750
  * `guidance` is for a model composing with it, in the imperative voice of a rule; the guidance
@@ -717,7 +757,7 @@ F extends string = never> extends Authored {
717
757
  guidance?: string;
718
758
  hidden?: boolean;
719
759
  deprecated?: boolean;
720
- }): ComponentBuilder<L, P, E, D, F>;
760
+ }): ComponentBuilder<L, P, E, D, F, S>;
721
761
  /**
722
762
  * The anatomy: which elements this component renders, in what order and nesting.
723
763
  *
@@ -726,7 +766,7 @@ F extends string = never> extends Authored {
726
766
  * it from the source and copies it verbatim, so what it returns is irrelevant; declaring it is
727
767
  * what puts it in the file.
728
768
  */
729
- render<E2 = E>(fn: (args: Without<RenderArgs<L, P>, keyof E2> & E2) => unknown): ComponentBuilder<L, P, E2, D, F>;
769
+ render<E2 = E>(fn: (args: Without<RenderArgs<L, P, S>, keyof E2> & E2) => unknown): ComponentBuilder<L, P, E2, D, F, S>;
730
770
  /**
731
771
  * An optional browser-canvas anatomy for a native component.
732
772
  *
@@ -734,7 +774,7 @@ F extends string = never> extends Authored {
734
774
  * Use this only when the phone anatomy is a poor fit for a browser canvas (a full-screen safe-area
735
775
  * wrapper, for example). Like `render`, it is source-read build input and stores no config data.
736
776
  */
737
- renderCanvas<E2 extends E = E>(fn: (args: Without<RenderArgs<L, P>, keyof E2> & E2) => unknown): ComponentBuilder<L, P, E, D, F>;
777
+ renderCanvas<E2 extends E = E>(fn: (args: Without<RenderArgs<L, P, S>, keyof E2> & E2) => unknown): ComponentBuilder<L, P, E, D, F, S>;
738
778
  /**
739
779
  * What a preview of this component STARTS from — the props a single tile is rendered with.
740
780
  *
@@ -755,7 +795,7 @@ F extends string = never> extends Authored {
755
795
  * so a preview can say a boolean is worth a column the way a rule would.
756
796
  */
757
797
  when?: readonly PreviewCondition<P>[];
758
- }): ComponentBuilder<L, P, E, D, F>;
798
+ }): ComponentBuilder<L, P, E, D, F, S>;
759
799
  toStored(): Record<string, unknown>;
760
800
  assignPath(path: string): void;
761
801
  }
@@ -787,4 +827,4 @@ interface DefineComponent {
787
827
  declare function definePropsFirst<const P extends PropMap<LayerPath>>(props: P & NoInfer<{ [K in keyof P]: Exact<P[K], AuthoredProp<LayerPath>> }>): ComponentBuilder<LayerMap, P>;
788
828
  declare const defineComponent: DefineComponent;
789
829
  //#endregion
790
- export { AsInput, AuthoredComponent, AuthoredProp, ComponentBuilder, ComponentPropsOf, DefaultsOf, GlyphComponent, GlyphValue, LayerElement, LayerMap, PhantomCallProps, PhantomReadProps, PropCondition, PropDecl, PropMap, RegisteredMembers, RenderArgs, SlotCodeForm, SlotDecl, StyleRuleInput, defineComponent };
830
+ export { AsInput, AuthoredComponent, AuthoredProp, ComponentBuilder, ComponentPropsOf, DefaultsOf, GlyphComponent, GlyphValue, LayerElement, LayerMap, PhantomCallProps, PhantomReadProps, PropCondition, PropDecl, PropMap, RegisteredMembers, RenderArgs, SlotCodeForm, SlotDecl, StateMap, StyleRuleInput, defineComponent };
@@ -15,6 +15,7 @@ function build(body) {
15
15
  path: void 0,
16
16
  layers: (layers) => next({ layers: withEmittedRefs(layers) }),
17
17
  props: (props) => next({ props }),
18
+ states: (states) => next({ states }),
18
19
  styles: (rules) => next({ styles: rules }),
19
20
  defaultProps: (defaults) => next({ defaultProps: defaults }),
20
21
  forwards: (forwards) => next({ forwards }),
@@ -1,6 +1,6 @@
1
1
  import { isRef, pathOf } from "../../framework/utils/refs.js";
2
- import { isPlainRecord } from "./style-bag.js";
3
2
  import { createSliceMemo } from "../../framework/memo.js";
3
+ import { isPlainRecord } from "./style-bag.js";
4
4
  import { stylePropEntries } from "../../framework/projections.js";
5
5
  import { forwardRoutes, intoName, routedPropIn, routesContent } from "../../framework/prop-surface.js";
6
6
  //#region src/entities/system/forward-route-invariants.ts
@@ -1,7 +1,7 @@
1
1
  import { isRef, kindOf, memberOf, pathOf } from "../../framework/utils/refs.js";
2
+ import { createSliceMemo } from "../../framework/memo.js";
2
3
  import { spellValues } from "../../framework/value-domain.js";
3
4
  import { collectBag, isPlainRecord } from "./style-bag.js";
4
- import { createSliceMemo } from "../../framework/memo.js";
5
5
  import { glyphComponentOf, glyphLibrariesOf, memberLeaves, propValueDomain, refListOf, resolveComponentProps } from "../../framework/projections.js";
6
6
  //#region src/entities/system/icon-domain-invariants.ts
7
7
  /**
@@ -1,5 +1,5 @@
1
- import { isPlainRecord } from "./style-bag.js";
2
1
  import { createSliceMemo } from "../../framework/memo.js";
2
+ import { isPlainRecord } from "./style-bag.js";
3
3
  //#region src/entities/system/root-layer-invariants.ts
4
4
  /**
5
5
  * A component's layers include one named `root`. The name is what the anatomy's edge list starts
@@ -1,6 +1,6 @@
1
1
  import { collectRefs, kindOf, pathOf, ref, splitRef } from "../../framework/utils/refs.js";
2
- import { parseRootFontSize } from "../../css/root-font-size.js";
3
2
  import { createSliceMemo } from "../../framework/memo.js";
3
+ import { parseRootFontSize } from "../../css/root-font-size.js";
4
4
  import { CSS_DEFAULTS, Settings } from "./Settings.js";
5
5
  import { cssVar, declaresCssVariable, resolveTokenValue } from "../../framework/projections.js";
6
6
  //#region src/entities/system/token-values.ts
@@ -73,7 +73,7 @@ interface RecordedSource {
73
73
  * The dense-counter `2` a short-lived scheme stamped before the chain existed reads as the mint
74
74
  * that replaced it (see `detectedWireVersion`).
75
75
  */
76
- declare const SERIALIZED_CONFIG_VERSION = 20261012200606;
76
+ declare const SERIALIZED_CONFIG_VERSION = 20261013154037;
77
77
  interface SerializedConfig {
78
78
  /** See {@link SERIALIZED_CONFIG_VERSION}. Always written; a stored artifact may lack it. */
79
79
  readonly version: number;
@@ -5749,7 +5749,7 @@ var Config = class Config {
5749
5749
  * links from the provided sources. A declared `linkedKind` with no provided source throws. */
5750
5750
  hydrateFrom(stored, options) {
5751
5751
  const claimed = schemaVersionOf(stored);
5752
- if (claimed > 20261012200606) throw new SchemaVersionTooNew(claimed);
5752
+ if (claimed > 20261013154037) throw new SchemaVersionTooNew(claimed);
5753
5753
  const wire = upgradeSerializedConfig(stored);
5754
5754
  if (!Array.isArray(wire.ownedKinds) || typeof wire.items !== "object" || wire.items === null) throw new ConfigFormatError(`Config "${stored.name ?? "system-config"}" cannot be hydrated: this is not a serialized config-v2 config (no "ownedKinds"/"items"). A pre-cutover build artifact converts through the registered cutover migration — import the config type's module (e.g. \`configs/system\`) before hydrating, or port the repo once with \`uds migrate\`.`);
5755
5755
  const leftover = wire.options;
@@ -1,4 +1,5 @@
1
1
  import { Config } from "./Config.js";
2
+ import { ComponentState, UDS_FORCE_STATES, componentStateDefaults, componentStates } from "./component-states.js";
2
3
 
3
4
  //#region src/framework/class-names.d.ts
4
5
  /** `HStack` → `hstack`, `AvatarItem` → `avatar-item`. A dash only between a lower/digit and an
@@ -13,6 +14,17 @@ declare function kebabComponent(name: string): string;
13
14
  * nothing when one side skips the kebab-casing.
14
15
  */
15
16
  declare function forceStateAttribute(modifier: string): string;
17
+ /**
18
+ * The selector a forced state's stylesheet twin matches: the force attribute naming `owner`, the
19
+ * component whose rule it is (`[data-create-force-hover~="Button"]`).
20
+ *
21
+ * The attribute's value is the list of components forced on that element, by path. A component
22
+ * whose root IS another component's instance shares that element, so a bare `[attr]` lit the inner
23
+ * component's hover in the outer one's Hover column; naming the owner keeps each column to the
24
+ * component it belongs to. Without an owner (a global style, a shared utility) any forced element
25
+ * matches.
26
+ */
27
+ declare function forceStateSelector(modifier: string, owner?: string): string;
16
28
  /**
17
29
  * The attribute that forces one mode group on an element and its subtree:
18
30
  * `data-create-force-mode-<group>="<option>"`. The group is lowercased and a nested group's slashes
@@ -75,6 +87,20 @@ declare function componentLayerClass(config: Config, path: string, layer: string
75
87
  * the cheaper problem.
76
88
  */
77
89
  declare function ruleCondition(when: Record<string, unknown> | null | undefined): string;
90
+ /**
91
+ * The attribute a state rides on the root — `filled` → `data-filled`, `isLoaded` → `data-is-loaded`.
92
+ * A boolean is present when true and absent otherwise; a variant holds its value. The emitted module
93
+ * spells it with the same transform, since a stylesheet keyed on one spelling matches nothing the
94
+ * render wrote in another.
95
+ */
96
+ declare function componentStateAttribute(name: string): string;
97
+ /**
98
+ * The attributes a root carries for its states' values — `{ filled: true }` → `{ 'data-filled': '' }`.
99
+ * A state given no value takes its declared default; one that is false or unset carries nothing.
100
+ * What a projection that has the values up front (the canvas) writes, and what the emitted module's
101
+ * `states` helper writes from the render's.
102
+ */
103
+ declare function componentStateAttributes(states: readonly ComponentState[], values: Readonly<Record<string, unknown>>): Record<string, string>;
78
104
  /** One layer's conditional class — the rule it comes from, and the props that have to hold. */
79
105
  interface ComponentRuleClass {
80
106
  /** The layer the class lands on. */
@@ -362,4 +388,4 @@ declare function modifierUtilitiesUsed(config: Config): Set<string>;
362
388
  */
363
389
  declare function splitVariants(cls: string): string[];
364
390
  //#endregion
365
- export { ComponentRuleClass, PickedComposite, SplitStyleProps, arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, inlineRuleComposites, isForceModeProp, kebabComponent, layerJsMotionPresets, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOffScaleLiteral, stylePropOwnClasses, styleRuleMotionClasses };
391
+ export { ComponentRuleClass, PickedComposite, SplitStyleProps, arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, componentStateAttribute, componentStateAttributes, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, forceStateSelector, inlineRuleComposites, isForceModeProp, kebabComponent, layerJsMotionPresets, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOffScaleLiteral, stylePropOwnClasses, styleRuleMotionClasses };
@@ -2,6 +2,7 @@ import { isRef, kindOf, memberOf, pathOf, refLeaf, refOfKey } from "./utils/refs
2
2
  import { forwardsOf, rootLayerOf } from "../entities/system/element.js";
3
3
  import { createSliceMemo } from "./memo.js";
4
4
  import { classPrefixOf, prefixed, varPrefixOf } from "../entities/system/Settings.js";
5
+ import "./component-states.js";
5
6
  import { storedPreview } from "./preview-store.js";
6
7
  import { authoredValue, className, isCustomPropertyKey, negatedCssValue, renderStyleValue, resolveComponentProps, stylePropEntries, stylePropLeafForToken, stylePropNumberLeaf, stylePropNumberUnit, stylePropValueLeaves, stylePropertyPathFor } from "./projections.js";
7
8
  import { inlineMotionId, isInlineMotion, jsMotionBody } from "../entities/system/motion-variants.js";
@@ -45,6 +46,31 @@ function forceStateAttribute(modifier) {
45
46
  return `${FORCE_ATTRIBUTE_PREFIX}${kebabComponent(name.slice(name.lastIndexOf("/") + 1))}`;
46
47
  }
47
48
  /**
49
+ * The selector a forced state's stylesheet twin matches: the force attribute naming `owner`, the
50
+ * component whose rule it is (`[data-create-force-hover~="Button"]`).
51
+ *
52
+ * The attribute's value is the list of components forced on that element, by path. A component
53
+ * whose root IS another component's instance shares that element, so a bare `[attr]` lit the inner
54
+ * component's hover in the outer one's Hover column; naming the owner keeps each column to the
55
+ * component it belongs to. Without an owner (a global style, a shared utility) any forced element
56
+ * matches.
57
+ */
58
+ function forceStateSelector(modifier, owner) {
59
+ const attribute = forceStateAttribute(modifier);
60
+ return owner === void 0 ? `[${attribute}]` : `[${attribute}~=${JSON.stringify(owner)}]`;
61
+ }
62
+ /**
63
+ * The force attributes a pin bag asks for: one per pseudo-state pinned on (`{ _active: true }` →
64
+ * `data-create-force-active`), each naming `owner`, the component it's forced on (see
65
+ * {@link forceStateSelector}). A component state in the same bag (`dragging`) is the render's, not
66
+ * the stylesheet's, so it's left out.
67
+ */
68
+ function forcedModifierAttributes(pins, owner = "") {
69
+ const attrs = {};
70
+ for (const [key, value] of Object.entries(pins ?? {})) if (key.startsWith("_") && !key.includes("@") && (value === true || value === "true")) attrs[forceStateAttribute(key)] = owner;
71
+ return attrs;
72
+ }
73
+ /**
48
74
  * The attribute that forces one mode group on an element and its subtree:
49
75
  * `data-create-force-mode-<group>="<option>"`. The group is lowercased and a nested group's slashes
50
76
  * become dashes, since an attribute name can hold neither. The emitter admits this attribute beside
@@ -137,6 +163,31 @@ function ruleCondition(when) {
137
163
  return Object.entries(when ?? {}).map(([prop, value]) => `${prop}-${String(value).replaceAll("/", "-").replaceAll(".", "_").replace(/\s+/g, "-")}`).join("-");
138
164
  }
139
165
  /**
166
+ * The attribute a state rides on the root — `filled` → `data-filled`, `isLoaded` → `data-is-loaded`.
167
+ * A boolean is present when true and absent otherwise; a variant holds its value. The emitted module
168
+ * spells it with the same transform, since a stylesheet keyed on one spelling matches nothing the
169
+ * render wrote in another.
170
+ */
171
+ function componentStateAttribute(name) {
172
+ return `data-${kebabComponent(name)}`;
173
+ }
174
+ /**
175
+ * The attributes a root carries for its states' values — `{ filled: true }` → `{ 'data-filled': '' }`.
176
+ * A state given no value takes its declared default; one that is false or unset carries nothing.
177
+ * What a projection that has the values up front (the canvas) writes, and what the emitted module's
178
+ * `states` helper writes from the render's.
179
+ */
180
+ function componentStateAttributes(states, values) {
181
+ const out = {};
182
+ for (const state of states) {
183
+ const value = values[state.name] ?? state.default;
184
+ if (value === void 0 || value === null || value === false) continue;
185
+ if (value === "false" && state.type === "boolean") continue;
186
+ out[componentStateAttribute(state.name)] = state.type === "boolean" ? "" : String(value);
187
+ }
188
+ return out;
189
+ }
190
+ /**
140
191
  * Every conditional class a component's style rules produce.
141
192
  *
142
193
  * Unconditional rules are absent by design: they land on the layer's own zero-specificity selector
@@ -171,15 +222,17 @@ function computeRuleClasses(config, path) {
171
222
  const base = componentClassBase(config, path);
172
223
  const out = [];
173
224
  for (const rule of Object.values(styles)) {
174
- const when = Object.fromEntries(Object.entries(rule.when ?? {}).map(([prop, value]) => [prop, refLeaf(value)]));
225
+ const when = Object.fromEntries(Object.entries(rule.when ?? {}).filter(([prop]) => !prop.startsWith("_")).map(([prop, value]) => [prop, refLeaf(value)]));
175
226
  const condition = ruleCondition(when);
176
227
  if (!condition) continue;
177
228
  for (const layer of Object.keys(rule.layers ?? {})) {
178
229
  if (!(layer in layers)) continue;
230
+ const className = `${base}-${layer}-${condition}`;
231
+ if (out.some((entry) => entry.className === className)) continue;
179
232
  out.push({
180
233
  layer,
181
234
  when,
182
- className: `${base}-${layer}-${condition}`
235
+ className
183
236
  });
184
237
  }
185
238
  }
@@ -1024,4 +1077,4 @@ function splitVariants(cls) {
1024
1077
  return segments;
1025
1078
  }
1026
1079
  //#endregion
1027
- export { arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, inlineRuleComposites, isForceModeProp, kebabComponent, layerJsMotionPresets, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOffScaleLiteral, stylePropOwnClasses, styleRuleMotionClasses };
1080
+ export { arbitraryClassName, arbitraryNeedsClass, classLeaf, classSelector, componentClassBase, componentCompositeClasses, componentLayerClass, componentMotionClasses, componentPropClasses, componentPropComposites, componentRuleClasses, componentStateAttribute, componentStateAttributes, forceModeAttribute, forceModeAttributeOf, forceModeProp, forceModePropGroup, forceModePropValue, forceModeProps, forceStateAttribute, forceStateSelector, forcedModifierAttributes, inlineRuleComposites, isForceModeProp, kebabComponent, layerJsMotionPresets, layerRoutedProps, modifierUtilitiesUsed, motionClassName, ruleCondition, splitStyleProps, splitVariants, stylePropClassBase, stylePropClassName, stylePropClasses, stylePropOffScaleLiteral, stylePropOwnClasses, styleRuleMotionClasses };
@@ -0,0 +1,40 @@
1
+ import { Config } from "./Config.js";
2
+
3
+ //#region src/framework/component-states.d.ts
4
+ /**
5
+ * One STATE a component declares (its `states`): what the render knows ("the field has a value",
6
+ * "the image loaded") and a style rule keys on. Not a prop — a caller never passes one.
7
+ *
8
+ * The render reports its states with the `states(…)` call it is handed, in its body before it reads a
9
+ * layer bundle; the module builds each bundle when the render first reads it, so a rule naming a
10
+ * state is tested like any prop, against the state's value: forced, else reported, else its rest
11
+ * value ({@link componentStateDefaults}). The root also carries each state as a `data-<name>`
12
+ * attribute ({@link componentStateAttribute}). Studio and tests force a state through the module's
13
+ * internal channel ({@link UDS_FORCE_STATES}), which wins over the render's value.
14
+ */
15
+ interface ComponentState {
16
+ readonly name: string;
17
+ readonly type: 'boolean' | 'variant';
18
+ /** A variant state's names, in declared order. */
19
+ readonly values?: readonly string[];
20
+ /** The value the root carries when neither the render nor a forced value gives one — a variant
21
+ * state's declared default. A boolean's rest is `false`, which is no attribute. */
22
+ readonly default?: string;
23
+ }
24
+ /**
25
+ * The one key a forced state reaches an emitted module under: `{ [UDS_FORCE_STATES]: { filled: true } }`.
26
+ * Internal on purpose: Studio's grid and canvas and a test force a state through it, and no caller's
27
+ * typed props mention it, so forcing a state is never part of a component's API.
28
+ */
29
+ declare const UDS_FORCE_STATES = "__udsForceStates";
30
+ /** The states a component declares, in declared order — see {@link ComponentState}. Its OWN: a
31
+ * state belongs to the render that sets it, so one a layer's component declares is that layer's. */
32
+ declare function componentStates(config: Config, path: string): readonly ComponentState[];
33
+ /**
34
+ * What each of a component's states reads at rest — a boolean's `false`, a variant's declared
35
+ * default — which every surface lays a reported or forced value over before testing a rule. A
36
+ * variant state with no default has no rest value, so a rule naming one of its values waits for one.
37
+ */
38
+ declare function componentStateDefaults(config: Config, path: string): Record<string, string | boolean>;
39
+ //#endregion
40
+ export { ComponentState, UDS_FORCE_STATES, componentStateDefaults, componentStates };
@@ -0,0 +1,53 @@
1
+ import { createSliceMemo } from "./memo.js";
2
+ //#region src/framework/component-states.ts
3
+ /**
4
+ * The one key a forced state reaches an emitted module under: `{ [UDS_FORCE_STATES]: { filled: true } }`.
5
+ * Internal on purpose: Studio's grid and canvas and a test force a state through it, and no caller's
6
+ * typed props mention it, so forcing a state is never part of a component's API.
7
+ */
8
+ const UDS_FORCE_STATES = "__udsForceStates";
9
+ /** The states a component declares, in declared order — see {@link ComponentState}. Its OWN: a
10
+ * state belongs to the render that sets it, so one a layer's component declares is that layer's. */
11
+ function componentStates(config, path) {
12
+ const cache = statesCache(config);
13
+ const hit = cache.get(path);
14
+ if (hit) return hit;
15
+ const states = computeStates(config, path);
16
+ cache.set(path, states);
17
+ return states;
18
+ }
19
+ /** Per component slice, the states of each component asked for so far — an expansion asks once per
20
+ * root it paints. */
21
+ const statesCache = createSliceMemo({
22
+ kinds: ["component"],
23
+ compute: (_config) => /* @__PURE__ */ new Map()
24
+ });
25
+ function computeStates(config, path) {
26
+ const body = config.resolve("component", path)?.toJSON();
27
+ if (!body?.states || typeof body.states !== "object") return [];
28
+ const out = [];
29
+ for (const [name, decl] of Object.entries(body.states)) if (decl?.type === "boolean") out.push({
30
+ name,
31
+ type: "boolean"
32
+ });
33
+ else if (decl?.type === "variant" && Array.isArray(decl.value)) out.push({
34
+ name,
35
+ type: "variant",
36
+ values: decl.value.map(String),
37
+ ...typeof decl.default === "string" ? { default: decl.default } : {}
38
+ });
39
+ return out;
40
+ }
41
+ /**
42
+ * What each of a component's states reads at rest — a boolean's `false`, a variant's declared
43
+ * default — which every surface lays a reported or forced value over before testing a rule. A
44
+ * variant state with no default has no rest value, so a rule naming one of its values waits for one.
45
+ */
46
+ function componentStateDefaults(config, path) {
47
+ const out = {};
48
+ for (const state of componentStates(config, path)) if (state.type === "boolean") out[state.name] = false;
49
+ else if (state.default !== void 0) out[state.name] = state.default;
50
+ return out;
51
+ }
52
+ //#endregion
53
+ export { UDS_FORCE_STATES, componentStateDefaults, componentStates };
@@ -0,0 +1,48 @@
1
+ import { Config } from "./Config.js";
2
+
3
+ //#region src/framework/interaction-conditions.d.ts
4
+ /** One interaction a rule keys on: the bag key, the modifier it resolves to, and whose it is. */
5
+ interface InteractionCondition {
6
+ /** As written in `when`: `_hover`. */
7
+ readonly key: string;
8
+ /** The modifier's path: `state/hover`. */
9
+ readonly modifier: string;
10
+ /** The layer whose interaction it is — the root for `true`. */
11
+ readonly layer: string;
12
+ }
13
+ /** Whether `modifier` (a path or leaf) can be a rule's interaction condition. */
14
+ declare function isInteractionModifier(config: Config, modifier: string): boolean;
15
+ /** The `when` key an interaction modifier is written under: its leaf, `_hover`. */
16
+ declare function interactionKey(modifier: string): string;
17
+ /** Every interaction modifier the config declares, by path. */
18
+ declare function interactionModifiers(config: Config): ReadonlySet<string>;
19
+ /**
20
+ * A rule's condition split in two: the props and states the render tests, and the interactions the
21
+ * stylesheet (or, for what CSS can't do, the module's own event tracking) holds it under.
22
+ */
23
+ declare function whenParts({
24
+ config,
25
+ when,
26
+ layers
27
+ }: {
28
+ config: Config;
29
+ when: Readonly<Record<string, unknown>> | null | undefined; /** The component's layers, for the root an interaction's `true` names. */
30
+ layers?: Readonly<Record<string, unknown>>;
31
+ }): {
32
+ props: Record<string, unknown>;
33
+ interactions: InteractionCondition[];
34
+ };
35
+ /** Whether `when` names any interaction — what tells the readers a rule holds only under one. */
36
+ declare function hasInteraction(config: Config, when: Readonly<Record<string, unknown>> | null | undefined): boolean;
37
+ /** One interaction a component's rules key on, with the selector that tells whether it holds. */
38
+ interface ComponentInteraction extends InteractionCondition {
39
+ /** The modifier's own selector, without its leading `&` (`:hover`): what `element.matches` asks. */
40
+ readonly selector: string;
41
+ }
42
+ /**
43
+ * Every interaction a component's rules key on, once per key and layer — what a surface that renders
44
+ * the component from its config, with no module to track them, watches on the live element.
45
+ */
46
+ declare function componentInteractions(config: Config, path: string): ComponentInteraction[];
47
+ //#endregion
48
+ export { ComponentInteraction, InteractionCondition, componentInteractions, hasInteraction, interactionKey, interactionModifiers, isInteractionModifier, whenParts };
@@ -0,0 +1,104 @@
1
+ import { modifierCategory } from "../entities/system/modifier-axis.js";
2
+ import { rootLayerOf } from "../entities/system/element.js";
3
+ import { createSliceMemo } from "./memo.js";
4
+ //#region src/framework/interaction-conditions.ts
5
+ /**
6
+ * A style rule's interaction conditions: `when: { _hover: true }`, "while the component is hovered".
7
+ *
8
+ * A rule's `when` names props and states, which the render knows. An interaction is the browser's —
9
+ * the pointer is over an element, it is pressed, it has focus — and a rule may key on one the same
10
+ * way, so the rule's whole reach (styles, an instance's props, content) applies while it holds. The
11
+ * value names whose interaction it is: `true` is the component's own root, and a layer name is that
12
+ * layer (`_hover: 'closeButton'`, "while the close button is hovered").
13
+ *
14
+ * Only an interaction modifier qualifies: a selector modifier that isn't a mode and names a state of
15
+ * the element itself (`:hover`, `[aria-checked="true"]`), never a pseudo-element (`::placeholder`)
16
+ * or a relation to another element (`[data-group]:hover &`).
17
+ */
18
+ /** Whether `modifier` (a path or leaf) can be a rule's interaction condition. */
19
+ function isInteractionModifier(config, modifier) {
20
+ return interactionModifiers(config).has(config.resolve("modifier", modifier)?.path ?? "");
21
+ }
22
+ /** The `when` key an interaction modifier is written under: its leaf, `_hover`. */
23
+ function interactionKey(modifier) {
24
+ return `_${modifier.slice(modifier.lastIndexOf("/") + 1)}`;
25
+ }
26
+ /** Every interaction modifier the config declares, by path. */
27
+ function interactionModifiers(config) {
28
+ return interactionMemo(config);
29
+ }
30
+ const interactionMemo = createSliceMemo({
31
+ kinds: ["modifier"],
32
+ compute: (config) => {
33
+ const paths = /* @__PURE__ */ new Set();
34
+ for (const modifier of config.list("modifier")) {
35
+ const { type, value } = modifier.toJSON() ?? {};
36
+ if (type !== "selector" || typeof value !== "string") continue;
37
+ if (modifierCategory(config, modifier.path) === "mode") continue;
38
+ const own = value.trim().replace(/^&/, "");
39
+ if (own.includes("::") || own.includes("&") || /\s/.test(own)) continue;
40
+ paths.add(modifier.path);
41
+ }
42
+ return paths;
43
+ }
44
+ });
45
+ /**
46
+ * A rule's condition split in two: the props and states the render tests, and the interactions the
47
+ * stylesheet (or, for what CSS can't do, the module's own event tracking) holds it under.
48
+ */
49
+ function whenParts({ config, when, layers }) {
50
+ const props = {};
51
+ const interactions = [];
52
+ const root = layers ? rootLayerOf(layers)[0] : "root";
53
+ for (const [key, value] of Object.entries(when ?? {})) {
54
+ const modifier = key.startsWith("_") ? config.resolve("modifier", key.slice(1)) : void 0;
55
+ if (modifier && interactionModifiers(config).has(modifier.path)) {
56
+ if (value === false || value === "false" || value == null) continue;
57
+ interactions.push({
58
+ key,
59
+ modifier: modifier.path,
60
+ layer: value === true || value === "true" ? root : String(value)
61
+ });
62
+ continue;
63
+ }
64
+ props[key] = value;
65
+ }
66
+ return {
67
+ props,
68
+ interactions
69
+ };
70
+ }
71
+ /** Whether `when` names any interaction — what tells the readers a rule holds only under one. */
72
+ function hasInteraction(config, when) {
73
+ return whenParts({
74
+ config,
75
+ when
76
+ }).interactions.length > 0;
77
+ }
78
+ /**
79
+ * Every interaction a component's rules key on, once per key and layer — what a surface that renders
80
+ * the component from its config, with no module to track them, watches on the live element.
81
+ */
82
+ function componentInteractions(config, path) {
83
+ const body = config.resolve("component", path)?.toJSON();
84
+ const out = [];
85
+ for (const rule of Object.values(body?.styles ?? {})) {
86
+ const { interactions } = whenParts({
87
+ config,
88
+ when: rule.when,
89
+ layers: body?.layers
90
+ });
91
+ for (const interaction of interactions) {
92
+ if (out.some((seen) => seen.key === interaction.key && seen.layer === interaction.layer)) continue;
93
+ const value = (config.resolve("modifier", interaction.modifier)?.toJSON())?.value;
94
+ if (typeof value !== "string") continue;
95
+ out.push({
96
+ ...interaction,
97
+ selector: value.trim().replace(/^&/, "")
98
+ });
99
+ }
100
+ }
101
+ return out;
102
+ }
103
+ //#endregion
104
+ export { componentInteractions, hasInteraction, interactionKey, interactionModifiers, isInteractionModifier, whenParts };
@@ -2,7 +2,11 @@ import { Config } from "./Config.js";
2
2
 
3
3
  //#region src/framework/layer-styles.d.ts
4
4
  /** Whether every prop a rule names holds the value it asks for. Compared as strings, because a
5
- * condition is canonicalized into the rule's key and a variant value is a string there. */
5
+ * condition is canonicalized into the rule's key and a variant value is a string there.
6
+ *
7
+ * An interaction (`_hover`, see `interaction-conditions`) holds only when the state says it does —
8
+ * `{ _hover: true }`, a surface forcing it — whichever layer the rule names: no prop bag is ever
9
+ * hovered, so at rest the rule is out. */
6
10
  declare function ruleApplies(when: Record<string, unknown> | null | undefined, state: Record<string, unknown>): boolean;
7
11
  interface LayerStyleOptions {
8
12
  /**