kuinetic 0.1.3 → 0.2.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 (125) hide show
  1. package/README.md +93 -10
  2. package/dist/esm/advanced/index.mjs +4727 -0
  3. package/dist/esm/chunk-AEQGC2KM.mjs +189 -0
  4. package/dist/esm/chunk-MQSNMYRP.mjs +9332 -0
  5. package/dist/esm/chunk-RVTYN6NZ.mjs +3446 -0
  6. package/dist/esm/chunk-U43BX5O2.mjs +2984 -0
  7. package/dist/esm/core/index.mjs +16 -9
  8. package/dist/esm/effects/index.mjs +19 -4
  9. package/dist/esm/index.mjs +17 -10
  10. package/dist/kuinetic.advanced.js +5070 -0
  11. package/dist/kuinetic.advanced.min.js +749 -0
  12. package/dist/kuinetic.all.js +15218 -5595
  13. package/dist/kuinetic.all.min.js +2 -0
  14. package/dist/kuinetic.css +2900 -360
  15. package/dist/kuinetic.js +15217 -5593
  16. package/dist/kuinetic.min.css +1 -0
  17. package/dist/kuinetic.min.js +7 -0
  18. package/dist/types/3d/angles.d.ts +30 -0
  19. package/dist/types/3d/flattening.d.ts +49 -0
  20. package/dist/types/3d/index.d.ts +36 -0
  21. package/dist/types/3d/model-3d.d.ts +115 -0
  22. package/dist/types/3d/register.d.ts +26 -0
  23. package/dist/types/3d/webgl.d.ts +24 -0
  24. package/dist/types/advanced/audio.d.ts +322 -0
  25. package/dist/types/advanced/base.d.ts +233 -0
  26. package/dist/types/advanced/camera-3d.d.ts +236 -0
  27. package/dist/types/advanced/fluid-cursor.d.ts +149 -0
  28. package/dist/types/advanced/gl-utils.d.ts +52 -0
  29. package/dist/types/advanced/glsl.d.ts +60 -0
  30. package/dist/types/advanced/index.d.ts +23 -0
  31. package/dist/types/advanced/particles.d.ts +108 -0
  32. package/dist/types/advanced/scenes.d.ts +207 -0
  33. package/dist/types/advanced/shaders.d.ts +972 -0
  34. package/dist/types/browser/boot.d.ts +72 -0
  35. package/dist/types/core/activation.d.ts +227 -2
  36. package/dist/types/core/animator.d.ts +446 -11
  37. package/dist/types/core/attrs.d.ts +9 -0
  38. package/dist/types/core/breakpoints.d.ts +260 -0
  39. package/dist/types/core/bundles.d.ts +71 -0
  40. package/dist/types/core/callback.d.ts +64 -0
  41. package/dist/types/core/capabilities.d.ts +47 -0
  42. package/dist/types/core/channels.d.ts +120 -2
  43. package/dist/types/core/cloak-selectors.d.ts +47 -0
  44. package/dist/types/core/compile.d.ts +207 -2
  45. package/dist/types/core/control.d.ts +218 -0
  46. package/dist/types/core/declarations.d.ts +128 -0
  47. package/dist/types/core/derived/aggregate.d.ts +43 -0
  48. package/dist/types/core/derived/book.d.ts +30 -0
  49. package/dist/types/core/derived/diagnostics.d.ts +51 -0
  50. package/dist/types/core/derived/group-gate.d.ts +62 -0
  51. package/dist/types/core/derived/install.d.ts +110 -0
  52. package/dist/types/core/derived/late-matches.d.ts +19 -0
  53. package/dist/types/core/derived/types.d.ts +108 -0
  54. package/dist/types/core/easing.d.ts +57 -0
  55. package/dist/types/core/element-config.d.ts +47 -6
  56. package/dist/types/core/element-size.d.ts +23 -0
  57. package/dist/types/core/event-sources.d.ts +39 -0
  58. package/dist/types/core/flip.d.ts +34 -1
  59. package/dist/types/core/gesture.d.ts +12 -0
  60. package/dist/types/core/host-facts.d.ts +155 -0
  61. package/dist/types/core/index.d.ts +7 -3
  62. package/dist/types/core/owned-styles.d.ts +129 -2
  63. package/dist/types/core/params.d.ts +48 -0
  64. package/dist/types/core/parse.d.ts +28 -1
  65. package/dist/types/core/register-into.d.ts +29 -0
  66. package/dist/types/core/registry.d.ts +10 -3
  67. package/dist/types/core/repeat.d.ts +91 -0
  68. package/dist/types/core/sequence.d.ts +126 -0
  69. package/dist/types/core/stagger-config.d.ts +199 -0
  70. package/dist/types/core/stagger-keys.d.ts +63 -0
  71. package/dist/types/core/stagger.d.ts +187 -6
  72. package/dist/types/core/style-plan.d.ts +21 -16
  73. package/dist/types/core/target.d.ts +128 -0
  74. package/dist/types/core/threshold-reachability.d.ts +45 -0
  75. package/dist/types/core/time-scale.d.ts +9 -0
  76. package/dist/types/core/toggle-actions.d.ts +141 -0
  77. package/dist/types/core/travel.d.ts +59 -0
  78. package/dist/types/core/types.d.ts +705 -13
  79. package/dist/types/core/unquoted-selectors.d.ts +53 -0
  80. package/dist/types/effects/carousel/drag.d.ts +80 -0
  81. package/dist/types/effects/carousel/index.d.ts +149 -0
  82. package/dist/types/effects/catalog/background-media.d.ts +152 -0
  83. package/dist/types/effects/catalog/discrete.d.ts +13 -0
  84. package/dist/types/effects/catalog/interaction-proximity.d.ts +24 -0
  85. package/dist/types/effects/catalog/interaction-reveal.d.ts +248 -0
  86. package/dist/types/effects/catalog/interaction-shared.d.ts +42 -0
  87. package/dist/types/effects/catalog/interaction-states.d.ts +68 -0
  88. package/dist/types/effects/catalog/materials.d.ts +67 -0
  89. package/dist/types/effects/catalog/media-shared.d.ts +3 -2
  90. package/dist/types/effects/catalog/numbers-shared.d.ts +8 -5
  91. package/dist/types/effects/catalog/shared.d.ts +2 -2
  92. package/dist/types/effects/catalog/text-shared.d.ts +92 -10
  93. package/dist/types/effects/catalog/transforms.d.ts +13 -0
  94. package/dist/types/effects/catalog/view-transitions.d.ts +13 -0
  95. package/dist/types/effects/forms/primitives.d.ts +15 -4
  96. package/dist/types/effects/gestures/index.d.ts +53 -0
  97. package/dist/types/effects/index.d.ts +5 -0
  98. package/dist/types/effects/layout/presets.d.ts +35 -0
  99. package/dist/types/effects/motion-path/index.d.ts +35 -0
  100. package/dist/types/effects/scroll-mechanics/params.d.ts +23 -0
  101. package/dist/types/effects/scroll-mechanics/presets.d.ts +51 -0
  102. package/dist/types/effects/scroll-mechanics/section-index.d.ts +47 -0
  103. package/dist/types/effects/shared.d.ts +162 -2
  104. package/dist/types/effects/step-index.d.ts +121 -0
  105. package/dist/types/effects/step-marking.d.ts +41 -33
  106. package/dist/types/effects/svg/icon-parts.d.ts +23 -0
  107. package/dist/types/effects/three-d/flip-parts.d.ts +46 -0
  108. package/dist/types/effects/tween/index.d.ts +57 -0
  109. package/dist/types/effects/tween/properties.d.ts +107 -0
  110. package/dist/types/effects/tween/waypoints.d.ts +106 -0
  111. package/dist/types/showcase/compare.d.ts +4 -0
  112. package/dist/types/showcase/device-frame.d.ts +3 -0
  113. package/dist/types/showcase/hotspots.d.ts +3 -0
  114. package/dist/types/showcase/index.d.ts +30 -0
  115. package/dist/types/showcase/lightbox.d.ts +3 -0
  116. package/dist/types/showcase/media-source.d.ts +13 -0
  117. package/dist/types/showcase/modal-shell.d.ts +22 -0
  118. package/dist/types/showcase/scroll-story.d.ts +4 -0
  119. package/dist/types/showcase/shared.d.ts +47 -0
  120. package/dist/types/showcase/slideshow.d.ts +3 -0
  121. package/dist/types/showcase/slow-mo.d.ts +5 -0
  122. package/package.json +14 -7
  123. package/dist/esm/chunk-5DON3UFQ.mjs +0 -4380
  124. package/dist/esm/chunk-JT4PZL3A.mjs +0 -773
  125. package/dist/esm/chunk-QUVFODSQ.mjs +0 -1278
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The facts that belong to the *element* rather than to any one effect on it.
3
+ *
4
+ * Only `reducedMotion` is still a true single-element fact after the plan's D-B.4 reversal: `rm:`
5
+ * is one author decision (or one folded default) that applies however many `target:` groups the
6
+ * attribute compiles into, so {@link mergeHostFacts} still folds the strictest declared policy
7
+ * across every group and writes the merged answer back onto all of them.
8
+ *
9
+ * `supportedActivations`/`supportedTimelines`/`channels`/`defaultActivation` are **not** folded
10
+ * here any more — a `target:` group is a derived host in its own right (each match will get its
11
+ * own trigger, timeline and events once Phase 2 lands), so pretending its capabilities are the
12
+ * *document's* capabilities was the bug: `parallax-scale target:video timeline:view` alongside a
13
+ * `flip-card` host used to intersect the two primitives' `supportedTimelines` down to `[]` and warn
14
+ * a mismatch that was never really there. `compile.ts`'s `compileTargets` now resolves each of
15
+ * those per group, from that group's own composed entries, and writes them straight onto that
16
+ * group's own plan — see this plan's design doc, section D-B.4, for the full reasoning.
17
+ *
18
+ * Split out of `compile.ts` when that file reached its own 400-line lint ceiling, the same way
19
+ * `test/css-composition-invariants.test.ts` was split out of `css-invariants.test.ts`. The seam is
20
+ * not arbitrary: everything here folds a list of already-computed facts down to one value, and
21
+ * nothing here knows what an effect, a primitive or a registry is. `resolveDefaultActivation` takes
22
+ * {@link ActivationClaim}s rather than compiled entries for exactly that reason — the same
23
+ * structural-input argument `channels.ts` makes for `ChannelClaim`, and it is what keeps the
24
+ * type-only import of `CompiledPlan` below from becoming a runtime cycle.
25
+ */
26
+ import type { CompiledPlan } from './compile.js';
27
+ import type { Activation, EffectPhase, ReducedMotionPolicy } from './types.js';
28
+ /** What {@link resolveDefaultActivation} needs to know about one composed effect. */
29
+ export interface ActivationClaim {
30
+ /** When this effect holds its channels — `compile.ts`'s `phaseOf`, already derived. */
31
+ phase?: EffectPhase;
32
+ /** The activation its primitive prefers, or absent when it expresses no preference. */
33
+ defaultActivation?: Activation;
34
+ }
35
+ /**
36
+ * Which activation the composed effects prefer, when the author named none.
37
+ *
38
+ * An element has exactly one activation binding — `animator.ts`'s `resolveActivation` returns a
39
+ * single `Activation` and every effect on the element is wired to it — so a comma list whose halves
40
+ * want different triggers cannot have both. What this decides is which one loses.
41
+ *
42
+ * It used to be first-wins: `plan.defaultActivation ??= primitive.defaultActivation`, folded in
43
+ * authoring order in `buildPlan` and again across every `target:` group's already-folded answer in
44
+ * `mergeHostFacts` (the D1 model — one document-wide answer written onto every group, since D-B.4
45
+ * reversed that: each group now calls this function once, over only its own composed entries, and
46
+ * keeps its own answer). `??=` is the right operator for "a default fills in when nothing is set"
47
+ * and the wrong one for a *merge*, because there is no sense in which the first name in a comma
48
+ * list is the authoritative one. The consequence was that composing an entrance with a behaviour
49
+ * changed **when the entrance itself fired**: `lift` declares `defaultActivation: 'load'`, so
50
+ * `data-kui="fade-up, lift"` — the pair the whole phase axis was added to allow — bound the element
51
+ * on `load` and fired the reveal before it was ever scrolled to. `back-in-down, drag` did the same.
52
+ * Measured across the catalog, 4,545 composing pairs took their activation from the non-entrance
53
+ * half.
54
+ *
55
+ * The rule is that **an entrance names the trigger**, and it is the lesser of two unavoidable harms
56
+ * rather than a preference. Either choice compromises something:
57
+ *
58
+ * - Binding on the behaviour's `load` spends the reveal on a below-the-fold element while nobody is
59
+ * looking. The entrance is destroyed outright and cannot be recovered.
60
+ * - Binding on the entrance's `enter` wires the behaviour up when the element scrolls into view
61
+ * instead of at load. It is late, and it still works — nobody drags, hovers or clicks an element
62
+ * they cannot see.
63
+ *
64
+ * `?? 'enter'` on the found entrance, not its raw declaration: almost no entrance declares an
65
+ * activation (the catalog's entrances leave it undefined and let `element-config.ts`'s `'enter'`
66
+ * fall through), so returning `undefined` here would hand the decision straight back to the
67
+ * first-wins fallback and change nothing at all. Materialising `'enter'` is what makes the
68
+ * entrance's silence a position. It is materialised *only* when something else declared a value to
69
+ * beat — a lone `fade-up` still compiles `defaultActivation: undefined`, exactly as before.
70
+ *
71
+ * ## No warning, and that was measured rather than assumed
72
+ *
73
+ * Silently picking one is what caused this, so a diagnostic is the obvious accompaniment. Two
74
+ * definitions were tried against the catalog and both are noise, not signal:
75
+ *
76
+ * - "the resolved activation is not in some composed primitive's `supportedActivations`" — 6,098
77
+ * pairs, including `fade-up, lift`, because `lift` declares `supportedActivations: ['load']`.
78
+ * - "two entries each declare an activation and the values differ" — 4,882 pairs, again including
79
+ * the flagship pairs.
80
+ *
81
+ * Disagreement is the *normal* state of this catalog: 120 of 290 names declare `load`, 21 declare
82
+ * `enter`, 124 declare nothing. A warning that fires on one composing pair in seven trains authors
83
+ * to ignore the reporter, which costs more than the thing it reports. `on:` remains the spelling
84
+ * for an author who wants the other answer, and it still wins outright — `animator.ts` only reads
85
+ * this field when `config.activationAuthored` is false.
86
+ *
87
+ * @param claims - Every effect that survived composition, for one `target:` group (`compile.ts`'s
88
+ * `compileTargets` calls this once per group, not once over the whole document — see D-B.4).
89
+ * Survivors rather than authored segments: an effect the resolver dropped is not going to run, so
90
+ * letting it name the trigger would bind the element for a corpse.
91
+ * @complexity O(n) time in the claim count; O(n) space.
92
+ * @overallScore 100
93
+ */
94
+ export declare function resolveDefaultActivation(claims: ActivationClaim[]): Activation | undefined;
95
+ /**
96
+ * Fold the strictest declared `reducedMotion` across every `target:` group and write the merged
97
+ * answer back onto every group's plan.
98
+ *
99
+ * `rm:` is one author decision (or one folded default among the composed primitives) for the whole
100
+ * attribute, even when its effects are split across several `target:` groups — `fade-up target:h1
101
+ * rm:disable, pin target:.x` cannot honour two different reduced-motion policies on one page load
102
+ * decided from two different subsets of the same author's intent. Every other cross-group fact this
103
+ * function used to merge (`supportedActivations`/`supportedTimelines`/`channels`/
104
+ * `defaultActivation`) is decided per group instead now — see this module's own doc comment for why
105
+ * — so this is the one field left.
106
+ *
107
+ * Mutates the plans in place rather than returning a new list: `buildPlan` already built each one,
108
+ * and threading a copy through here for one field write would cost more than it clarifies.
109
+ *
110
+ * @complexity O(g) time in the group count; O(1) space.
111
+ * @overallScore 100
112
+ */
113
+ export declare function mergeHostFacts(targets: {
114
+ plan: CompiledPlan;
115
+ }[]): void;
116
+ /**
117
+ * Fold an authored `rm:` into the policy the composed primitives declared.
118
+ *
119
+ * `rm:` is the one thing an author can say about reduced motion, and it is deliberately a
120
+ * *one-way ratchet*: it can only make the policy stricter, never weaker.
121
+ *
122
+ * The rule is not invented for this key — it is {@link strictestPolicy}'s, applied one more time.
123
+ * That function already encodes "if any effect must not run, none of the list should", and an
124
+ * author key that could overrule it would make the whole fold advisory: `parallax` declares
125
+ * `disable` because parallax is a documented vestibular trigger, not because the library is being
126
+ * cautious on the author's behalf, and `rm:shorten` on it would hand a visitor who has explicitly
127
+ * asked their operating system for less motion exactly the motion they asked not to receive. The
128
+ * useful direction is the other one and it stays open: `rm:disable` on a spinning logo whose
129
+ * primitive only claims `shorten` is a real request the library previously had no spelling for.
130
+ *
131
+ * A weakening attempt warns by name rather than being ignored, because the author wrote a value
132
+ * and is otherwise owed an explanation for why the page does not behave as they asked.
133
+ *
134
+ * @param declared - Strictest policy among the composed primitives.
135
+ * @param authored - The hoisted `rm:` value, already validated by `parse.ts`.
136
+ * @complexity O(1) time and space.
137
+ * @overallScore 100
138
+ */
139
+ export declare function resolvedPolicy(declared: ReducedMotionPolicy, authored: ReducedMotionPolicy | undefined, warnings: string[]): ReducedMotionPolicy;
140
+ /**
141
+ * Narrow a running capability intersection by one more primitive's support list.
142
+ *
143
+ * `undefined` means no primitive has contributed yet; `[]` means the composed primitives share
144
+ * nothing. Collapsing those two states into "is the array empty?" — which is what the previous
145
+ * `length ? filter : copy` form did — made an intersection that had legitimately emptied out
146
+ * repopulate from the next effect: `fade-up, parallax-scale, scroll-progress-ring timeline:view`
147
+ * emptied on the second effect and came back as `['scroll', 'view']` on the third, so
148
+ * `style-plan.ts` applied `view()` to `fade-up`, the exact mismatch `supportedTimelines` was
149
+ * added to prevent. Emptiness is a real answer here and must survive the rest of the list.
150
+ *
151
+ * @complexity O(a * b) time in the two list lengths — both are single-digit; O(a) space.
152
+ * @overallScore 100
153
+ */
154
+ export declare function intersect<T>(accumulated: T[] | undefined, supported: T[]): T[];
155
+ export declare function strictestPolicy(a: ReducedMotionPolicy, b: ReducedMotionPolicy): ReducedMotionPolicy;
@@ -1,9 +1,13 @@
1
- export { createActivationBinder } from './activation.js';
2
- export type { ActivationBinder, ActivationBinderOptions } from './activation.js';
1
+ export { createActivationBinder, resolveActivationSpec } from './activation.js';
2
+ export type { ActivationBinder, ActivationBinderOptions, ActivationRequest, ActivationSpec, ActivationTrigger, } from './activation.js';
3
3
  export { Animator, createAnimator, ATTR } from './animator.js';
4
4
  export type { AnimatorOptions } from './animator.js';
5
5
  export { detect } from './capabilities.js';
6
6
  export type { Capabilities } from './capabilities.js';
7
+ export { control } from './control.js';
8
+ export type { ControlHandle } from './control.js';
9
+ export { KUI_EVENT } from './control.js';
10
+ export type { LifecycleDetail, LifecycleEvent, LifecycleEventType, LifecycleReason, } from './control.js';
7
11
  export { play, resolveTargets, toAttributeValue } from './play.js';
8
12
  export type { PlaybackHandle, PlayOptions, Target } from './play.js';
9
13
  export { Registry } from './registry.js';
@@ -11,4 +15,4 @@ export type { ResolvedEffect } from './registry.js';
11
15
  export { collectingReporter, consoleReporter, silentReporter } from './reporter.js';
12
16
  export type { CollectingReporter, Reporter } from './reporter.js';
13
17
  export { CHANNEL, inertInstance } from './types.js';
14
- export type { Activation, Channel, Cleanup, EffectInstance, EffectParams, ParamSpec, ParameterSchema, PerfClass, PrepareContext, Preset, Primitive, ReducedMotionPolicy, Timeline, } from './types.js';
18
+ export type { Activation, Channel, Cleanup, EffectInstance, EffectParams, EffectVariant, InstanceControl, NamedActivation, ParamSpec, ParameterSchema, PerfClass, PlaybackState, PrepareContext, Preset, Primitive, ReducedMotionPolicy, Timeline, } from './types.js';
@@ -20,6 +20,19 @@ export interface StyleLedger {
20
20
  restore(): void;
21
21
  /** Properties currently owned. Diagnostics and leak assertions. */
22
22
  owned(): string[];
23
+ /**
24
+ * The value this ledger will restore `property` to — whatever was there immediately before its
25
+ * first `set()` or `claim()` — or `undefined` if this ledger has never captured that property.
26
+ *
27
+ * `restore()` can put a value back, but until now nothing could ask what it remembered without
28
+ * writing it: a caller that needs the *value itself* mid-effect (a "resting state" to fall back
29
+ * to when its own live input settles at a neutral point, say) had to keep a private snapshot of
30
+ * its own, taken at its own moment, which is exactly the two-sources-of-truth bug this ledger
31
+ * exists to prevent everywhere else. `undefined` here means only "never captured" — a property
32
+ * that *was* captured but held no value beforehand reads back as `''`, matching what a live
33
+ * `getPropertyValue` call would have returned for it at that same instant.
34
+ */
35
+ peek(property: string): string | undefined;
23
36
  }
24
37
  /**
25
38
  * Create a ledger over one element's inline style.
@@ -36,9 +49,123 @@ export interface AttributeLedger {
36
49
  }
37
50
  /**
38
51
  * The same discipline for attributes, so a library-stamped attribute never clobbers an authored
39
- * one permanently.
52
+ * one permanently. The concrete ledger also exposes `remove`, which records the original value
53
+ * before clearing a boolean attribute such as a slideshow slide's `inert`.
40
54
  *
41
55
  * @complexity O(1) per write; O(n) to restore.
42
56
  * @overallScore 100
43
57
  */
44
- export declare function createAttributeLedger(el: Element): AttributeLedger;
58
+ export declare function createAttributeLedger(el: Element): AttributeLedger & {
59
+ remove(name: string): void;
60
+ };
61
+ /**
62
+ * One owner's claim on the shared per-element ledgers.
63
+ *
64
+ * Deliberately not "the registry" — there is exactly one registry, module-wide, and a claim is a
65
+ * handle onto it. Two claims over one element see one capture and two refcounts.
66
+ */
67
+ export interface StyleClaim {
68
+ /**
69
+ * This element's shared ledger, claimed for this owner.
70
+ *
71
+ * The object handed back is this claim's own view of it: `set`/`claim`/`owned`/`peek` read and
72
+ * write the shared ledger, and `restore()` means *"this owner is done"* rather than *"unwind the
73
+ * element now"*. That distinction is what lets a caller holding only a `StyleLedger` — every
74
+ * primitive, via `PrepareContext.style` — participate in the refcount without knowing it exists.
75
+ */
76
+ style(el: Element): StyleLedger;
77
+ /** Drop this owner's claim on `el`, restoring it when no other owner holds it. */
78
+ release(el: Element): void;
79
+ /** Elements this claim currently holds, in the order it first asked for them. A snapshot. */
80
+ elements(): Element[];
81
+ }
82
+ /**
83
+ * Open one owner's claim on the shared inline-style ledgers.
84
+ *
85
+ * **Writing is claiming; reading is not.** A handle outlives its own `restore()` — a primitive is
86
+ * handed `PrepareContext.style` once at prepare and keeps it for the element's whole life, a
87
+ * controller cancels and re-activates, a `LedgerSet` is restored and the element is entered again
88
+ * — so a handle that closed over one entry would, after release, write straight into a ledger the
89
+ * registry had already deleted and restored. The value landed on the element, the claim was empty,
90
+ * and nothing anywhere would ever put it back. So `set` and `claim` rejoin. `owned` and `peek`
91
+ * deliberately do not: asking what this owner will restore must not make it an owner, and a
92
+ * released handle honestly answers "nothing".
93
+ *
94
+ * @returns A claim that hands out ref-counted handles and releases them one element at a time.
95
+ * @complexity O(1) per lookup and per release; O(p) in properties written, on the last release.
96
+ * @overallScore 100
97
+ */
98
+ export declare function createStyleClaim(): StyleClaim;
99
+ /**
100
+ * One owner's claim on the shared per-element *attribute* ledgers.
101
+ *
102
+ * The same model and the same reason. `animator.ts`'s `installMatch` stamps `data-kui-fx` and
103
+ * `data-kui-rm` onto every element a group's `target:` resolves to, and under `scope:page` two
104
+ * authored hosts can resolve to the same element — at which point the second `LedgerSet` captured
105
+ * the first one's stamp as the author's attribute and put *that* back on teardown.
106
+ */
107
+ export interface AttributeClaim {
108
+ /** This element's shared attribute ledger, claimed for this owner. */
109
+ attributes(el: Element): AttributeLedger;
110
+ /** Drop this owner's claim on `el`, restoring it when no other owner holds it. */
111
+ release(el: Element): void;
112
+ /** Elements this claim currently holds, in the order it first asked for them. A snapshot. */
113
+ elements(): Element[];
114
+ }
115
+ /**
116
+ * Open one owner's claim on the shared attribute ledgers.
117
+ *
118
+ * @complexity O(1) per lookup and per release; O(n) in attributes written, on the last release.
119
+ * @overallScore 100
120
+ */
121
+ export declare function createAttributeClaim(): AttributeClaim;
122
+ /**
123
+ * Every element one authored `data-kui` wrote to, and the ledgers that unwind them.
124
+ *
125
+ * The host owns the lifecycle — one `InstanceState`, one gate, one event stream — but the *writes*
126
+ * do not all land on the host. `target:` names elements the effect animates instead of the element
127
+ * the attribute sits on, and each of those needs its own pair of ledgers: `createStyleLedger`
128
+ * closes over one element's `style` object and one snapshot of whether that element had a `style`
129
+ * attribute to begin with, so it cannot be shared and cannot be reconstructed later.
130
+ *
131
+ * A set rather than an array of pairs because the same element is reached repeatedly — once per
132
+ * effect in a composed attribute, once more by the stagger pass — and a second ledger over an
133
+ * element the first has already written to would snapshot *this library's* values as the author's
134
+ * own and restore to them. Memoising per element is what makes "what was there before" mean before
135
+ * this instance existed, rather than before this particular call.
136
+ *
137
+ * Both kinds go one level further out than that, through {@link createStyleClaim} and
138
+ * {@link createAttributeClaim}: memoising *within* one set is not enough once two sets reach the
139
+ * same element — two authored hosts whose `target:` lands on one node, a stagger group and the
140
+ * child's own state, a core effect and an advanced controller. The set is one owner of a shared,
141
+ * ref-counted capture, so `restore()` here means "this host is finished with the element", and the
142
+ * element is unwound by whichever owner is last.
143
+ */
144
+ export interface LedgerSet {
145
+ /** This element's inline-style ledger, created on first ask. */
146
+ style(el: Element): StyleLedger;
147
+ /** This element's attribute ledger, created on first ask. */
148
+ attributes(el: Element): AttributeLedger;
149
+ /** Unwind every element this set ever handed out a ledger for. Host last — see below. */
150
+ restore(): void;
151
+ }
152
+ /**
153
+ * Open a ledger set over one authored host.
154
+ *
155
+ * **Restore is host-last, and that ordering is load-bearing.** The host carries `data-kui-state`,
156
+ * which is the cloak layer's per-element release key: `html[data-kui-cloak] [data-kui][data-kui-reveal]:not([data-kui-state])`
157
+ * holds a subtree at `opacity: 0`, and the attribute's presence is what lets go. Restoring the host
158
+ * first removes that key while the elements underneath still carry library styles, reopening the
159
+ * cloak over a half-unwound subtree. Doing it last means the page is already back to the author's
160
+ * markup at the instant it becomes visible again.
161
+ *
162
+ * Style before attributes within each element, matching the order `release()` has always used: a
163
+ * primitive's teardown may write styles, and the attributes are what CSS keys on to decide whether
164
+ * those styles mean anything.
165
+ *
166
+ * @param host - The authored element. Always present in the set, always restored last.
167
+ * @returns A set that hands out memoised ledgers and unwinds all of them together.
168
+ * @complexity O(1) per lookup; O(n) space and O(n) time to restore, in elements written to.
169
+ * @overallScore 100
170
+ */
171
+ export declare function createLedgerSet(host: Element): LedgerSet;
@@ -15,6 +15,54 @@ export interface ValidationResult {
15
15
  * @overallScore 100
16
16
  */
17
17
  export declare function validate(raw: string, spec: ParamSpec): ValidationResult;
18
+ /**
19
+ * Expand decimal notation lexically, optionally shifting the decimal point.
20
+ *
21
+ * Lexical rather than arithmetic on purpose. `Number('1.1') / 100` is `0.011000000000000001` — a
22
+ * binary floating-point artefact that would be written verbatim into a custom property and shown
23
+ * to anyone inspecting the element, making a correct value look broken. Shifting the digits
24
+ * instead is exact for every input, and it neither rounds a tiny scale to zero nor rounds a large
25
+ * integer to its nearest double.
26
+ *
27
+ * The length cap matches the core value budget and bounds the allocation even for `1e999999999`,
28
+ * which would otherwise ask for a gigabyte of zeroes. An unrepresentable result returns
29
+ * `undefined` and leaves the caller holding the original input for ordinary validation to reject.
30
+ *
31
+ * Lives in the core because two callers need the same answer: this module's `'number|percentage'`
32
+ * normalisation, and `effects/tween/properties.ts`'s `withImpliedUnit`, which has accepted
33
+ * `opacity:50%` on the tween since before the union type existed. Two copies would be two places
34
+ * for `50%` to stop meaning `0.5`.
35
+ *
36
+ * @param raw - A candidate number in CSS's spelling, including `+` and exponent forms.
37
+ * @param shift - Decimal places to move the point by; `-2` divides by one hundred.
38
+ * @returns The plain decimal spelling, or `undefined` when the input is not a number or the
39
+ * result would exceed the value budget.
40
+ * @complexity O(n) time and space in the digit count, bounded by the cap.
41
+ * @overallScore 100
42
+ */
43
+ export declare function decimalNumber(raw: string, shift?: number): string | undefined;
44
+ /**
45
+ * The escape screen on its own, for a value that has no type to be validated against.
46
+ *
47
+ * Every authored value normally reaches `style.setProperty` through {@link validate}, which runs
48
+ * this screen and then a type match. One value cannot take the second half: `data-kui-stagger`'s
49
+ * step (and its `cascade:` spelling inside `data-kui`) has always been passed through verbatim so
50
+ * that `var(--speed)` and `calc(90ms * 2)` work, and narrowing it to a `<time>` literal now would
51
+ * break every group written against that promise. That left it as the one authored string in the
52
+ * library with no screen at all — and `data-kui` is explicitly not assumed to be site-owner text
53
+ * (see the module doc above; a CMS field or a comment can reach it).
54
+ *
55
+ * So the half that costs the expression forms nothing is available separately. It is deliberately
56
+ * *not* a type check: anything shaped like a length, a time, a `calc()` or a `var()` passes, and
57
+ * only the characters and functions that can escape a declaration or reach the network are
58
+ * refused.
59
+ *
60
+ * @param value - Author-supplied text bound for a custom property.
61
+ * @returns Whether it is safe to write as-is.
62
+ * @complexity O(n) time in value length; O(1) space.
63
+ * @overallScore 100
64
+ */
65
+ export declare function isSafeCssValue(value: string): boolean;
18
66
  /**
19
67
  * Whether an authored value stays on the page's own origin wherever a `src`-shaped `text`
20
68
  * parameter is actually turned into a network request.
@@ -1,4 +1,4 @@
1
- import type { ParsedValue } from './types.js';
1
+ import type { Activation, ParsedValue } from './types.js';
2
2
  /**
3
3
  * Split on a delimiter, ignoring delimiters nested inside parentheses or quotes.
4
4
  *
@@ -11,6 +11,25 @@ import type { ParsedValue } from './types.js';
11
11
  * @overallScore 100
12
12
  */
13
13
  export declare function splitTopLevel(input: string, delimiter: ',' | ' ', warnings?: string[]): string[];
14
+ /** The parsed longhand activation and its optional foreign event source. */
15
+ export interface ParsedActivationAttribute {
16
+ activation?: Activation;
17
+ from?: string;
18
+ warnings: string[];
19
+ }
20
+ /**
21
+ * Parse the `data-kui-on` grammar: an activation followed by optional `key:value` refinements.
22
+ *
23
+ * `from:` deliberately lives only in this longhand grammar. It cannot be a general `data-kui`
24
+ * hoist because `from:` remains a per-effect parameter on existing primitives such as `count-up`.
25
+ * Quoting keeps a selector containing whitespace in one token, e.g.
26
+ * `submit from:"#signup form"`.
27
+ *
28
+ * @param input - Raw `data-kui-on` attribute value.
29
+ * @returns The activation/source pair, or no pair when any token is malformed.
30
+ * @complexity O(n) time in input length; O(n) space for tokenisation.
31
+ */
32
+ export declare function parseActivationAttribute(input: string | null): ParsedActivationAttribute;
14
33
  /**
15
34
  * Parse a `data-kui` attribute value.
16
35
  *
@@ -20,3 +39,11 @@ export declare function splitTopLevel(input: string, delimiter: ',' | ' ', warni
20
39
  * @overallScore 100
21
40
  */
22
41
  export declare function parse(input: string): ParsedValue;
42
+ /**
43
+ * Write a hoisted value once. A second, differing value across segments is an authoring mistake;
44
+ * first one wins so behaviour stays deterministic.
45
+ *
46
+ * @complexity O(1) time, O(1) space.
47
+ * @overallScore 100
48
+ */
49
+ export declare function assignOnce<K extends 'activation' | 'timeline' | 'threshold' | 'actions' | 'cascade' | 'spread' | 'order' | 'cols' | 'along' | 'rm' | 'func'>(result: ParsedValue, key: K, value: ParsedValue[K], label: string): void;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * How the showcase module is handed a registry.
3
+ *
4
+ * A near-copy of `registerInto`/`asRegistry`/`hasRegistrarShape` from `src/3d/register.ts`
5
+ * (itself a copy of `src/advanced/base.ts:519-568`) — deliberately copied rather than imported.
6
+ * `src/showcase/` ships in core today but is designed to split into its own `<script>` bundle and
7
+ * subpath later (see `.claude/showcase-run/showcase-plan.md`'s "Exact steps to split"), and importing a
8
+ * sibling tier's helper would mean the split could not happen without also shipping whichever tier
9
+ * that helper lived in. This file is the third copy of the same nine lines; the two originals stay
10
+ * the reference implementation, and if the shape gate's rule ever changes, all three move together.
11
+ * `test/showcase-seam.test.ts` covers this copy the same way `test/3d-register-gate.test.ts` mirrors
12
+ * `src/advanced/__tests__/register-gate.test.ts`, so a drift shows up as a failing test rather than
13
+ * a browser bug.
14
+ */
15
+ import type { Preset, Primitive } from './types.js';
16
+ import type { Registry } from './registry.js';
17
+ import type { Animator } from './animator.js';
18
+ /**
19
+ * Register one module's primitives and presets onto a `Registry`, an `Animator`, or anything
20
+ * carrying either shape.
21
+ *
22
+ * @param target - The registry, the animator, or a host exposing one as `.registry`.
23
+ * @param primitives - One primitive or a list of them.
24
+ * @param presets - One preset or a list of them.
25
+ * @param name - Spelled into the thrown message, so it names the function the consumer called.
26
+ * @returns `target`, so callers can chain.
27
+ * @complexity O(n) in primitives plus presets.
28
+ */
29
+ export declare function registerInto(target: unknown, primitives: Primitive | Primitive[], presets: Preset | Preset[], name?: string): Registry | Animator;
@@ -6,9 +6,16 @@ export interface ResolvedEffect {
6
6
  /**
7
7
  * Name → primitive alias table.
8
8
  *
9
- * The catalog's ~237 names come from 29 primitives; 48 of them are one primitive with different
10
- * parameter defaults. Presets are therefore data rows, not code, and adding a name in a later
11
- * release costs a table entry. See docs/catalog.md.
9
+ * The catalog's 302 names come from 159 primitives; 143 of the names are aliases — the
10
+ * same primitive with different parameter defaults. Presets are therefore data rows, not
11
+ * code, and adding a name in a later release costs a table entry. See docs/catalog.md.
12
+ *
13
+ * Those two numbers are derived, never incremented by hand: `createRegistry().names().length`,
14
+ * and the count of distinct `resolve(name).primitive.id` across them. Do not add to a
15
+ * remembered total — the figures above were stale by 31 names and 21 primitives when last
16
+ * checked, because someone did. Note they count *primitives*, which is not the same thing as
17
+ * the "primitive families" docs/catalog.md tallies; that grouping is curatorial and has no
18
+ * registry equivalent.
12
19
  */
13
20
  export declare class Registry {
14
21
  private primitives;
@@ -0,0 +1,91 @@
1
+ import type { EffectSpec, Renderer, Timeline } from './types.js';
2
+ export type PlaybackKey = 'repeat' | 'yoyo';
3
+ /**
4
+ * Whether a `key:value` key is one this module owns, so `parse.ts` can dispatch with one lookup
5
+ * rather than a growing `||` chain against its own complexity ceiling.
6
+ *
7
+ * @complexity O(1) time and space.
8
+ * @overallScore 100
9
+ */
10
+ export declare function isPlaybackKey(key: string): key is PlaybackKey;
11
+ /** Where a playback token came from, so the diagnostics can quote it back. */
12
+ export interface PlaybackContext {
13
+ segment: string;
14
+ warn: (message: string) => void;
15
+ }
16
+ /**
17
+ * Validate and lift one `repeat:`/`yoyo:` token onto the spec being built.
18
+ *
19
+ * Validated here, at parse time, for the reason `rm:` is: both are closed value sets with no
20
+ * expression forms, so there is nothing a later stage could know that this one does not, and the
21
+ * author is owed the diagnostic next to every other grammar diagnostic. The *timeline*-dependent
22
+ * refusals are the opposite case and live in {@link resolvePlayback}, which runs where the
23
+ * element's timeline and renderer are known.
24
+ *
25
+ * Every rejection is a warning plus no value, never a warning plus a half-applied one — the same
26
+ * fail-open `applyGate` uses. A mistyped count leaves the effect playing exactly once, which is
27
+ * what it did before the author added the key.
28
+ *
29
+ * @complexity O(n) time in the value's length; O(1) space.
30
+ * @overallScore 100
31
+ */
32
+ export declare function applyPlayback(spec: EffectSpec, key: PlaybackKey, value: string, context: PlaybackContext): void;
33
+ /** What {@link resolvePlayback} needs to know about one compiled segment. */
34
+ export interface PlaybackInput {
35
+ /** Preset name. Only ever used in warnings, which have to name what the author wrote. */
36
+ name: string;
37
+ renderer: Renderer;
38
+ /** Whether the preset's from-state is one the visitor must not see. See `Preset.cloak`. */
39
+ cloak?: boolean;
40
+ timeline: Timeline;
41
+ repeat?: string;
42
+ yoyo?: boolean;
43
+ }
44
+ /** The playback settings that survived, plus anything the author needs told. */
45
+ export interface PlaybackResolution {
46
+ repeat?: string;
47
+ yoyo?: boolean;
48
+ warnings: string[];
49
+ }
50
+ /**
51
+ * Decide what an authored `repeat:`/`yoyo:` actually compiles to, given the renderer and timeline.
52
+ *
53
+ * Refusals here **drop the modifier and keep the effect**, which is the same fail-open
54
+ * `refuseContainerGate` uses one function away in `compile.ts`: the effect still runs, it simply
55
+ * runs the way it would have without the key, and the author is told by name. It is the right
56
+ * direction for this feature specifically because the failure mode of *keeping* an impossible
57
+ * repeat is not "nothing happens" but a visibly broken element — see the progress-timeline case.
58
+ *
59
+ * @complexity O(1) time; O(1) space beyond the returned warnings.
60
+ * @overallScore 100
61
+ */
62
+ export declare function resolvePlayback(input: PlaybackInput): PlaybackResolution;
63
+ /**
64
+ * The finite number of plays a validated `repeat:` asks for.
65
+ *
66
+ * @returns The count, or `undefined` for `infinite` and for an unwritten `repeat:` — the two cases
67
+ * where no caller may do arithmetic with it.
68
+ * @complexity O(n) time in the value's length; O(1) space.
69
+ * @overallScore 100
70
+ */
71
+ export declare function repeatCount(repeat: string | undefined): number | undefined;
72
+ /** Whether a validated `repeat:` asks for a loop with no end. */
73
+ export declare function isInfiniteRepeat(repeat: string | undefined): boolean;
74
+ /** The value a track's `animation-direction` takes. `normal` is the CSS initial value. */
75
+ export declare function directionValue(yoyo: boolean | undefined): string;
76
+ /**
77
+ * A CSS time expression scaled from one iteration to the whole playback.
78
+ *
79
+ * Used by both consumers of a repeat that has to do arithmetic — the `at:` sequencer's "after the
80
+ * previous one ends", and the pin scrub head in `declarations.ts` — so the two cannot disagree
81
+ * about how long a repeated effect takes.
82
+ *
83
+ * Returns `duration` untouched for an unwritten or single repeat, which is what makes every
84
+ * attribute written before this parameter existed compile byte-for-byte identically. Never called
85
+ * with `infinite`: both callers refuse that case by name first, because `calc(600ms * infinite)`
86
+ * is not an expression and a browser drops the whole declaration rather than reporting it.
87
+ *
88
+ * @complexity O(n) time in the expression's length; O(1) space.
89
+ * @overallScore 100
90
+ */
91
+ export declare function playbackExpression(duration: string, repeat: string | undefined): string;