kuinetic 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/dist/esm/advanced/index.mjs +5 -5
- package/dist/esm/{chunk-U43BX5O2.mjs → chunk-FASAEQDR.mjs} +54 -7
- package/dist/esm/{chunk-BN2Y4UKB.mjs → chunk-FYMZW5N2.mjs} +2373 -541
- package/dist/esm/{chunk-RVTYN6NZ.mjs → chunk-MZQG2VMG.mjs} +208 -2
- package/dist/esm/core/index.mjs +21 -3
- package/dist/esm/effects/index.mjs +2 -2
- package/dist/esm/index.mjs +35 -4
- package/dist/esm/notes/index.mjs +1075 -0
- package/dist/kuinetic.advanced.js +63 -25
- package/dist/kuinetic.advanced.min.js +3 -3
- package/dist/kuinetic.all.js +2658 -538
- package/dist/kuinetic.all.min.js +2 -2
- package/dist/kuinetic.css +238 -65
- package/dist/kuinetic.js +2657 -537
- package/dist/kuinetic.min.css +1 -1
- package/dist/kuinetic.min.js +2 -2
- package/dist/kuinetic.notes.js +1 -0
- package/dist/types/3d/flattening.d.ts +8 -7
- package/dist/types/advanced/camera-3d.d.ts +0 -4
- package/dist/types/advanced/fluid-cursor.d.ts +0 -4
- package/dist/types/advanced/particles.d.ts +0 -4
- package/dist/types/advanced/scenes.d.ts +0 -4
- package/dist/types/browser/boot.d.ts +9 -4
- package/dist/types/core/activation.d.ts +11 -0
- package/dist/types/core/compile.d.ts +7 -0
- package/dist/types/core/deck-viewer.d.ts +87 -0
- package/dist/types/core/derived/types.d.ts +0 -1
- package/dist/types/core/describe.d.ts +174 -0
- package/dist/types/core/gesture.d.ts +16 -1
- package/dist/types/core/index.d.ts +6 -2
- package/dist/types/core/stagger.d.ts +1 -1
- package/dist/types/core/target.d.ts +28 -0
- package/dist/types/core/timing-contract.d.ts +61 -0
- package/dist/types/core/types.d.ts +59 -0
- package/dist/types/effects/auto-motion.d.ts +169 -0
- package/dist/types/effects/carousel/deck.d.ts +170 -0
- package/dist/types/effects/carousel/drag.d.ts +6 -0
- package/dist/types/effects/carousel/fit.d.ts +130 -0
- package/dist/types/effects/carousel/index.d.ts +6 -23
- package/dist/types/effects/carousel/stack.d.ts +77 -0
- package/dist/types/effects/catalog/interaction-reveal.d.ts +69 -9
- package/dist/types/effects/forms/primitives.d.ts +1 -2
- package/dist/types/effects/gestures/index.d.ts +3 -4
- package/dist/types/effects/shared.d.ts +3 -30
- package/dist/types/effects/step-index.d.ts +48 -10
- package/dist/types/effects/swipe-event.d.ts +75 -0
- package/dist/types/index.d.ts +26 -0
- package/dist/types/notes/catalog-core.d.ts +3 -0
- package/dist/types/notes/catalog.d.ts +3 -0
- package/dist/types/notes/index.d.ts +13 -0
- package/dist/types/notes/interaction-reveal.d.ts +3 -0
- package/dist/types/notes/interaction.d.ts +6 -0
- package/dist/types/notes/keys.d.ts +7 -0
- package/dist/types/notes/mechanics.d.ts +7 -0
- package/dist/types/notes/scroll-nav-forms.d.ts +6 -0
- package/dist/types/notes/shared.d.ts +6 -0
- package/dist/types/notes/showcase.d.ts +7 -0
- package/dist/types/notes/text.d.ts +3 -0
- package/dist/types/showcase/compare.d.ts +9 -0
- package/dist/types/showcase/index.d.ts +3 -0
- package/dist/types/showcase/lightbox.d.ts +23 -1
- package/dist/types/showcase/modal-shell.d.ts +11 -0
- package/package.json +10 -4
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ABSOLUTE_BASIS,
|
|
3
|
+
ALL_TIMING_TOKENS,
|
|
3
4
|
ATTR,
|
|
4
5
|
CHANNEL,
|
|
5
6
|
DEFAULT_SPRING,
|
|
6
7
|
Registry,
|
|
7
8
|
SCOPE_PARAM,
|
|
9
|
+
SUBTREE_CHANNEL,
|
|
8
10
|
TIME_SCALE_ATTR,
|
|
11
|
+
attributeChannel,
|
|
9
12
|
clamp01,
|
|
10
13
|
continuousSetup,
|
|
11
14
|
createMeasureCache,
|
|
12
15
|
createSpringRunner,
|
|
13
16
|
cssEasingValue,
|
|
14
17
|
decimalNumber,
|
|
18
|
+
declareTimingContract,
|
|
15
19
|
defaultSpringDeps,
|
|
16
20
|
deferPrepare,
|
|
17
21
|
deferredInstance,
|
|
18
22
|
effectDurationMs,
|
|
19
23
|
inertInstance,
|
|
24
|
+
inheritTimingContract,
|
|
20
25
|
isSameOriginPath,
|
|
26
|
+
queryControls,
|
|
21
27
|
queryScoped,
|
|
22
28
|
resolveTarget,
|
|
23
29
|
scopeParam,
|
|
@@ -26,7 +32,7 @@ import {
|
|
|
26
32
|
toPixels,
|
|
27
33
|
validate,
|
|
28
34
|
waapiEasingValue
|
|
29
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-FASAEQDR.mjs";
|
|
30
36
|
import {
|
|
31
37
|
createAttributeLedger,
|
|
32
38
|
createStyleLedger
|
|
@@ -36,7 +42,6 @@ import {
|
|
|
36
42
|
var TRIGGER_DELAY_PARAM = {
|
|
37
43
|
delay: { type: "time", default: "0ms", cssProperty: "--kui-delay" }
|
|
38
44
|
};
|
|
39
|
-
var ALL_TIMING_TOKENS = ["duration", "delay", "ease"];
|
|
40
45
|
function authoredTiming(params, token) {
|
|
41
46
|
if (token === "ease") return params.timing.easing;
|
|
42
47
|
const ms = token === "duration" ? params.timing.durationMs : params.timing.delayMs;
|
|
@@ -64,20 +69,20 @@ function mirrorTimingToCss(id, honours, params, ctx) {
|
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
71
|
function withTimingContract(id, contract, prepare) {
|
|
67
|
-
return (el, params, ctx) => {
|
|
72
|
+
return declareTimingContract((el, params, ctx) => {
|
|
68
73
|
warnUnhonouredTiming(id, contract, params, ctx.warn);
|
|
69
74
|
return prepare(el, params, ctx);
|
|
70
|
-
};
|
|
75
|
+
}, contract);
|
|
71
76
|
}
|
|
72
77
|
function stylesheetTimingPrepare(id, contract) {
|
|
73
|
-
return (el, params, ctx) => {
|
|
78
|
+
return declareTimingContract((el, params, ctx) => {
|
|
74
79
|
warnUnhonouredTiming(id, contract, params, ctx.warn);
|
|
75
80
|
return deferredInstance(() => {
|
|
76
81
|
mirrorTimingToCss(id, contract.honours ?? [], params, ctx);
|
|
77
82
|
return () => {
|
|
78
83
|
};
|
|
79
84
|
});
|
|
80
|
-
};
|
|
85
|
+
}, contract);
|
|
81
86
|
}
|
|
82
87
|
var TIMELINE_AGNOSTIC = ["time", "view", "scroll", "pin"];
|
|
83
88
|
var COMMON = {
|
|
@@ -154,12 +159,12 @@ var AMBIENT_PRIMITIVES = [
|
|
|
154
159
|
* What the ring rules actually write (`ambient.css`) beyond the gradient: `mask` +
|
|
155
160
|
* `mask-composite`, which subtract the element's own content box to leave a ring — the same
|
|
156
161
|
* physical property `media-mask` claims under the `'mask'` channel — and `position: relative`
|
|
157
|
-
* plus a `padding` that *is* the ring's thickness
|
|
158
|
-
*
|
|
159
|
-
* `
|
|
160
|
-
*
|
|
162
|
+
* plus a `padding` that *is* the ring's thickness — `position` as `pin` and `background-media`
|
|
163
|
+
* claim it, `padding` as `header-shrink` does. Declared only as `background`, a
|
|
164
|
+
* `gradient-border, pin-section` pair composed silently while both decided the host's
|
|
165
|
+
* `position`, and `gradient-border, mask-reveal` while both wrote `mask`.
|
|
161
166
|
*/
|
|
162
|
-
cssPrimitive("ambient-gradient-ring", [CHANNEL.background, "mask", "
|
|
167
|
+
cssPrimitive("ambient-gradient-ring", [CHANNEL.background, "mask", "position", "padding"], {
|
|
163
168
|
parameters: drift,
|
|
164
169
|
defaultActivation: "load",
|
|
165
170
|
reducedMotion: "disable",
|
|
@@ -837,22 +842,287 @@ var GROUP_PRESETS = [
|
|
|
837
842
|
var STATE_PRIMITIVES = [...PRESS_PRIMITIVES, ...GROUP_PRIMITIVES];
|
|
838
843
|
var STATE_PRESETS = [...PRESS_PRESETS, ...GROUP_PRESETS];
|
|
839
844
|
|
|
845
|
+
// src/core/element-size.ts
|
|
846
|
+
function watchElementSize(win, onChange) {
|
|
847
|
+
const ResizeObserverCtor = win.ResizeObserver;
|
|
848
|
+
const observer = ResizeObserverCtor ? new ResizeObserverCtor(onChange) : void 0;
|
|
849
|
+
const elements = /* @__PURE__ */ new Set();
|
|
850
|
+
win.addEventListener("resize", onChange, { passive: true });
|
|
851
|
+
return {
|
|
852
|
+
observe(element) {
|
|
853
|
+
if (elements.has(element)) return;
|
|
854
|
+
elements.add(element);
|
|
855
|
+
observer?.observe(element);
|
|
856
|
+
},
|
|
857
|
+
unobserve(element) {
|
|
858
|
+
elements.delete(element);
|
|
859
|
+
observer?.unobserve(element);
|
|
860
|
+
},
|
|
861
|
+
disconnect() {
|
|
862
|
+
win.removeEventListener("resize", onChange);
|
|
863
|
+
observer?.disconnect();
|
|
864
|
+
elements.clear();
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function frameScheduler(win, callback) {
|
|
869
|
+
let frame;
|
|
870
|
+
let timeout;
|
|
871
|
+
const run = () => {
|
|
872
|
+
frame = void 0;
|
|
873
|
+
timeout = void 0;
|
|
874
|
+
callback();
|
|
875
|
+
};
|
|
876
|
+
return {
|
|
877
|
+
request() {
|
|
878
|
+
if (frame !== void 0 || timeout !== void 0) return;
|
|
879
|
+
if (win.requestAnimationFrame) frame = win.requestAnimationFrame(run);
|
|
880
|
+
else timeout = win.setTimeout(run, 16);
|
|
881
|
+
},
|
|
882
|
+
cancel() {
|
|
883
|
+
if (frame !== void 0) win.cancelAnimationFrame(frame);
|
|
884
|
+
if (timeout !== void 0) win.clearTimeout(timeout);
|
|
885
|
+
frame = void 0;
|
|
886
|
+
timeout = void 0;
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
840
891
|
// src/effects/catalog/interaction-reveal.ts
|
|
841
|
-
function revealPrimitive(id, channels, parameters, perfClass = "compositor") {
|
|
892
|
+
function revealPrimitive(id, channels, parameters, { perfClass = "compositor", placed } = {}) {
|
|
893
|
+
const contract = {
|
|
894
|
+
honours: ALL_TIMING_TOKENS,
|
|
895
|
+
because: "interaction.css pins that value on this effect"
|
|
896
|
+
};
|
|
842
897
|
return {
|
|
843
898
|
id,
|
|
844
899
|
renderer: "javascript",
|
|
845
|
-
|
|
900
|
+
// A placed family publishes the resolved side, and a running tease, on its host.
|
|
901
|
+
channels: placed ? [...channels, attributeChannel(placed.attribute), attributeChannel(placed.tease)] : channels,
|
|
846
902
|
parameters,
|
|
847
903
|
supportedTimelines: ["time"],
|
|
848
904
|
supportedActivations: ["load"],
|
|
849
905
|
defaultActivation: "load",
|
|
850
906
|
perfClass,
|
|
851
907
|
reducedMotion: "shorten",
|
|
852
|
-
prepare:
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
908
|
+
prepare: placed ? withTimingContract(
|
|
909
|
+
id,
|
|
910
|
+
contract,
|
|
911
|
+
(el, params, ctx) => deferredInstance(() => {
|
|
912
|
+
mirrorTimingToCss(id, ALL_TIMING_TOKENS, params, ctx);
|
|
913
|
+
return prepareReveal(el, params, placed, ctx);
|
|
914
|
+
})
|
|
915
|
+
) : stylesheetTimingPrepare(id, contract)
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
var OPPOSITE = { top: "bottom", bottom: "top", left: "right", right: "left" };
|
|
919
|
+
function isSide(value) {
|
|
920
|
+
return value !== null && Object.hasOwn(OPPOSITE, value);
|
|
921
|
+
}
|
|
922
|
+
function chooseSide(preferred, room, extent) {
|
|
923
|
+
if (room[preferred] >= extent) return preferred;
|
|
924
|
+
const opposite = OPPOSITE[preferred];
|
|
925
|
+
return room[opposite] > room[preferred] ? opposite : preferred;
|
|
926
|
+
}
|
|
927
|
+
function extentOf(part, host, side) {
|
|
928
|
+
switch (side) {
|
|
929
|
+
case "top":
|
|
930
|
+
return -part.offsetTop;
|
|
931
|
+
case "bottom":
|
|
932
|
+
return part.offsetTop + part.offsetHeight - host.clientHeight;
|
|
933
|
+
case "left":
|
|
934
|
+
return -part.offsetLeft;
|
|
935
|
+
case "right":
|
|
936
|
+
return part.offsetLeft + part.offsetWidth - host.clientWidth;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function prepareReveal(el, params, spec, ctx) {
|
|
940
|
+
const release2 = claimPart(el, params, spec, ctx);
|
|
941
|
+
const attrs = createAttributeLedger(el);
|
|
942
|
+
const restore = () => {
|
|
943
|
+
attrs.restore();
|
|
944
|
+
release2();
|
|
945
|
+
};
|
|
946
|
+
const place2 = params.text("place");
|
|
947
|
+
const teaseMs = params.ms("tease", 0);
|
|
948
|
+
const auto = place2 === "auto";
|
|
949
|
+
if (!auto && isSide(place2)) attrs.set(spec.attribute, place2);
|
|
950
|
+
const placeSide = auto ? autoPlacer(el, spec, attrs, ctx.win) : void 0;
|
|
951
|
+
const shift = shifter(el, spec, ctx);
|
|
952
|
+
const measure = () => {
|
|
953
|
+
placeSide?.();
|
|
954
|
+
shift();
|
|
955
|
+
};
|
|
956
|
+
let endTease = () => {
|
|
957
|
+
};
|
|
958
|
+
const shown = whileShown(el, ctx, measure, () => endTease());
|
|
959
|
+
const tease = teaseMs > 0 ? teaseOnEnter(el, ctx, { ms: teaseMs, attribute: spec.tease, attrs, shown }) : void 0;
|
|
960
|
+
if (tease) endTease = tease.end;
|
|
961
|
+
return () => {
|
|
962
|
+
tease?.stop();
|
|
963
|
+
shown.stop();
|
|
964
|
+
restore();
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function claimPart(el, params, spec, ctx) {
|
|
968
|
+
const selector = resolveTarget(params.text("target"), ctx, spec.name);
|
|
969
|
+
if (!selector) return () => {
|
|
970
|
+
};
|
|
971
|
+
const matches = queryScoped(el, ctx, selector, "self");
|
|
972
|
+
const part = matches[0];
|
|
973
|
+
if (!part) {
|
|
974
|
+
ctx.warn(`${spec.name} target "${selector}" matched nothing inside this element`);
|
|
975
|
+
return () => {
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
if (matches.length > 1) {
|
|
979
|
+
ctx.warn(
|
|
980
|
+
`${spec.name} target "${selector}" matched ${matches.length} elements; only the first is used`
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
const ledger = createAttributeLedger(part);
|
|
984
|
+
if (!part.hasAttribute(spec.marker)) ledger.set(spec.marker, "");
|
|
985
|
+
return () => ledger.restore();
|
|
986
|
+
}
|
|
987
|
+
function autoPlacer(el, spec, attrs, win) {
|
|
988
|
+
const preferred = spec.preferred(el);
|
|
989
|
+
let side = preferred;
|
|
990
|
+
attrs.set(spec.attribute, side);
|
|
991
|
+
return () => {
|
|
992
|
+
const part = el.querySelector(`[${spec.marker}]`);
|
|
993
|
+
if (!part) return;
|
|
994
|
+
const rect = el.getBoundingClientRect();
|
|
995
|
+
const room = {
|
|
996
|
+
top: rect.top,
|
|
997
|
+
bottom: win.innerHeight - rect.bottom,
|
|
998
|
+
left: rect.left,
|
|
999
|
+
right: win.innerWidth - rect.right
|
|
1000
|
+
};
|
|
1001
|
+
const next = chooseSide(preferred, room, extentOf(part, el, side));
|
|
1002
|
+
if (next === side) return;
|
|
1003
|
+
side = next;
|
|
1004
|
+
attrs.set(spec.attribute, side);
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
var VIEWPORT_MARGIN = 8;
|
|
1008
|
+
function shiftWithin(start, size, viewport, margin = VIEWPORT_MARGIN) {
|
|
1009
|
+
const low = margin;
|
|
1010
|
+
const high = viewport - margin;
|
|
1011
|
+
if (start < low || size > high - low) return low - start;
|
|
1012
|
+
if (start + size > high) return high - start - size;
|
|
1013
|
+
return 0;
|
|
1014
|
+
}
|
|
1015
|
+
function shifter(el, spec, ctx) {
|
|
1016
|
+
let written = 0;
|
|
1017
|
+
return () => {
|
|
1018
|
+
const part = el.querySelector(`[${spec.marker}]`);
|
|
1019
|
+
if (!part) return;
|
|
1020
|
+
const stamped = el.getAttribute(spec.attribute);
|
|
1021
|
+
const side = isSide(stamped) ? stamped : spec.preferred(el);
|
|
1022
|
+
const rect = part.getBoundingClientRect();
|
|
1023
|
+
const hostRect = el.getBoundingClientRect();
|
|
1024
|
+
const [startKey, sizeKey, offsetKey, borderKey, layoutKey, viewportKey, windowKey] = side === "top" || side === "bottom" ? ["left", "width", "offsetLeft", "clientLeft", "offsetWidth", "clientWidth", "innerWidth"] : ["top", "height", "offsetTop", "clientTop", "offsetHeight", "clientHeight", "innerHeight"];
|
|
1025
|
+
const hostSize = el[layoutKey];
|
|
1026
|
+
const zoom = hostSize > 0 ? hostRect[sizeKey] / hostSize : 1;
|
|
1027
|
+
if (zoom <= 0) return;
|
|
1028
|
+
const entranceScale = parseFloat(ctx.win.getComputedStyle(part).scale) || 1;
|
|
1029
|
+
const size = rect[sizeKey] / entranceScale;
|
|
1030
|
+
const start = hostRect[startKey] + (el[borderKey] + part[offsetKey]) * zoom - size / 2;
|
|
1031
|
+
const viewport = ctx.doc.documentElement[viewportKey] || ctx.win[windowKey];
|
|
1032
|
+
const next = shiftWithin(start, size, viewport) / zoom;
|
|
1033
|
+
if (next === written) return;
|
|
1034
|
+
written = next;
|
|
1035
|
+
ctx.style.set(spec.shift, `${next}px`);
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function whileShown(el, ctx, measure, onUser) {
|
|
1039
|
+
const { win } = ctx;
|
|
1040
|
+
const frame = frameScheduler(win, measure);
|
|
1041
|
+
let hovered = false;
|
|
1042
|
+
let focused = false;
|
|
1043
|
+
let teasing = false;
|
|
1044
|
+
let watching;
|
|
1045
|
+
const sync = () => {
|
|
1046
|
+
const shown = hovered || focused || teasing;
|
|
1047
|
+
if (shown && !watching) {
|
|
1048
|
+
measure();
|
|
1049
|
+
watching = new AbortController();
|
|
1050
|
+
const options = { capture: true, passive: true, signal: watching.signal };
|
|
1051
|
+
win.addEventListener("scroll", frame.request, options);
|
|
1052
|
+
win.addEventListener("resize", frame.request, options);
|
|
1053
|
+
} else if (!shown && watching) {
|
|
1054
|
+
watching.abort();
|
|
1055
|
+
watching = void 0;
|
|
1056
|
+
frame.cancel();
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
const on = (type, update) => el.addEventListener(type, (event) => {
|
|
1060
|
+
update(event);
|
|
1061
|
+
sync();
|
|
1062
|
+
}, { signal: ctx.signal });
|
|
1063
|
+
on("pointerenter", () => {
|
|
1064
|
+
hovered = true;
|
|
1065
|
+
onUser();
|
|
1066
|
+
});
|
|
1067
|
+
on("pointerleave", () => hovered = false);
|
|
1068
|
+
on("focusin", () => {
|
|
1069
|
+
focused = true;
|
|
1070
|
+
onUser();
|
|
1071
|
+
});
|
|
1072
|
+
on("focusout", (event) => {
|
|
1073
|
+
focused = el.contains(event.relatedTarget);
|
|
1074
|
+
});
|
|
1075
|
+
return {
|
|
1076
|
+
setTeasing(on2) {
|
|
1077
|
+
teasing = on2;
|
|
1078
|
+
sync();
|
|
1079
|
+
},
|
|
1080
|
+
stop() {
|
|
1081
|
+
watching?.abort();
|
|
1082
|
+
frame.cancel();
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
function teaseOnEnter(el, ctx, {
|
|
1087
|
+
ms,
|
|
1088
|
+
attribute,
|
|
1089
|
+
attrs,
|
|
1090
|
+
shown
|
|
1091
|
+
}) {
|
|
1092
|
+
const { win } = ctx;
|
|
1093
|
+
let timer;
|
|
1094
|
+
let observer;
|
|
1095
|
+
const end = () => {
|
|
1096
|
+
if (timer === void 0) return;
|
|
1097
|
+
win.clearTimeout(timer);
|
|
1098
|
+
timer = void 0;
|
|
1099
|
+
attrs.remove(attribute);
|
|
1100
|
+
shown.setTeasing(false);
|
|
1101
|
+
};
|
|
1102
|
+
const start = () => {
|
|
1103
|
+
shown.setTeasing(true);
|
|
1104
|
+
attrs.set(attribute, "");
|
|
1105
|
+
timer = win.setTimeout(end, ms);
|
|
1106
|
+
};
|
|
1107
|
+
const Observer = win.IntersectionObserver;
|
|
1108
|
+
if (Observer) {
|
|
1109
|
+
observer = new Observer(
|
|
1110
|
+
(entries) => {
|
|
1111
|
+
if (!entries.some((entry) => entry.isIntersecting && entry.intersectionRatio >= 0.5)) return;
|
|
1112
|
+
observer.disconnect();
|
|
1113
|
+
start();
|
|
1114
|
+
},
|
|
1115
|
+
{ threshold: 0.5 }
|
|
1116
|
+
);
|
|
1117
|
+
observer.observe(el);
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
end,
|
|
1121
|
+
stop() {
|
|
1122
|
+
observer?.disconnect();
|
|
1123
|
+
if (timer !== void 0) win.clearTimeout(timer);
|
|
1124
|
+
timer = void 0;
|
|
1125
|
+
}
|
|
856
1126
|
};
|
|
857
1127
|
}
|
|
858
1128
|
var labelSwapParams = {
|
|
@@ -885,10 +1155,34 @@ var hoverIntentParams = {
|
|
|
885
1155
|
duration: { type: "time", default: "160ms", cssProperty: "--kui-duration" },
|
|
886
1156
|
delay: { type: "time", default: "1000ms", cssProperty: "--kui-delay" },
|
|
887
1157
|
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
|
|
888
|
-
distance: { type: "length", default: "4px", cssProperty: "--kui-hover-intent-distance" }
|
|
1158
|
+
distance: { type: "length", default: "4px", cssProperty: "--kui-hover-intent-distance" },
|
|
1159
|
+
color: { type: "color", default: "#f4f4f0", cssProperty: "--kui-hover-intent-color" },
|
|
1160
|
+
"bg-color": { type: "color", default: "#111111", cssProperty: "--kui-hover-intent-bg-color" },
|
|
1161
|
+
radius: { type: "length", default: "12px", cssProperty: "--kui-hover-intent-radius" },
|
|
1162
|
+
// `place` and `tease` are read by `prepareReveal`, not by any stylesheet — see
|
|
1163
|
+
// `ParamSpecBase.cssProperty`.
|
|
1164
|
+
place: {
|
|
1165
|
+
type: "keyword",
|
|
1166
|
+
default: "top",
|
|
1167
|
+
keywords: ["top", "bottom", "auto"],
|
|
1168
|
+
cssProperty: "--kui-hover-intent-place"
|
|
1169
|
+
},
|
|
1170
|
+
tease: { type: "time", default: "0ms", cssProperty: "--kui-hover-intent-tease" },
|
|
1171
|
+
// Names the hint, in place of a `data-kui-hint` in the markup — see `claimPart`. Declared, so
|
|
1172
|
+
// `compile.ts` leaves it here rather than moving the effect onto the hint.
|
|
1173
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" }
|
|
889
1174
|
};
|
|
890
1175
|
var HOVER_INTENT_PRIMITIVES = [
|
|
891
|
-
revealPrimitive("hover-intent", ["hint"], hoverIntentParams
|
|
1176
|
+
revealPrimitive("hover-intent", ["hint"], hoverIntentParams, {
|
|
1177
|
+
placed: {
|
|
1178
|
+
name: "hover-intent",
|
|
1179
|
+
marker: "data-kui-hint",
|
|
1180
|
+
attribute: "data-kui-hint-place",
|
|
1181
|
+
tease: "data-kui-hint-tease",
|
|
1182
|
+
shift: "--kui-hover-intent-shift",
|
|
1183
|
+
preferred: () => "top"
|
|
1184
|
+
}
|
|
1185
|
+
})
|
|
892
1186
|
];
|
|
893
1187
|
var HOVER_INTENT_PRESETS = [
|
|
894
1188
|
{ name: "hover-intent", phase: "state", primitive: "hover-intent" }
|
|
@@ -906,16 +1200,65 @@ var anchoredPreviewParams = {
|
|
|
906
1200
|
finite: true,
|
|
907
1201
|
minimum: 0,
|
|
908
1202
|
maximum: 1
|
|
909
|
-
}
|
|
1203
|
+
},
|
|
1204
|
+
color: { type: "color", default: "currentcolor", cssProperty: "--kui-anchored-preview-color" },
|
|
1205
|
+
"bg-color": {
|
|
1206
|
+
type: "color",
|
|
1207
|
+
default: "transparent",
|
|
1208
|
+
cssProperty: "--kui-anchored-preview-bg-color"
|
|
1209
|
+
},
|
|
1210
|
+
radius: { type: "length", default: "0px", cssProperty: "--kui-anchored-preview-radius" },
|
|
1211
|
+
// `place` and `tease` are read by `prepareReveal`, not by any stylesheet — see
|
|
1212
|
+
// `ParamSpecBase.cssProperty`. `tease` is `hover-intent`'s, unchanged: `0ms` is off.
|
|
1213
|
+
place: {
|
|
1214
|
+
type: "keyword",
|
|
1215
|
+
default: "top",
|
|
1216
|
+
keywords: ["top", "bottom", "left", "right", "auto"],
|
|
1217
|
+
cssProperty: "--kui-anchored-preview-place"
|
|
1218
|
+
},
|
|
1219
|
+
tease: { type: "time", default: "0ms", cssProperty: "--kui-anchored-preview-tease" },
|
|
1220
|
+
// Names the preview, in place of a `data-kui-preview` in the markup — see `claimPart`.
|
|
1221
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" }
|
|
910
1222
|
};
|
|
1223
|
+
function previewSideOf(el) {
|
|
1224
|
+
for (const token of (el.getAttribute(ATTR.normalized) ?? "").split(/\s+/)) {
|
|
1225
|
+
const side = /^anchored-preview(?:-(bottom|left|right))?$/.exec(token);
|
|
1226
|
+
if (side) return side[1] ?? "top";
|
|
1227
|
+
}
|
|
1228
|
+
return "top";
|
|
1229
|
+
}
|
|
911
1230
|
var ANCHORED_PREVIEW_PRIMITIVES = [
|
|
912
|
-
revealPrimitive("anchored-preview", ["preview"], anchoredPreviewParams
|
|
1231
|
+
revealPrimitive("anchored-preview", ["preview"], anchoredPreviewParams, {
|
|
1232
|
+
placed: {
|
|
1233
|
+
name: "anchored-preview",
|
|
1234
|
+
marker: "data-kui-preview",
|
|
1235
|
+
attribute: "data-kui-preview-place",
|
|
1236
|
+
tease: "data-kui-preview-tease",
|
|
1237
|
+
shift: "--kui-anchored-preview-shift",
|
|
1238
|
+
preferred: previewSideOf
|
|
1239
|
+
}
|
|
1240
|
+
})
|
|
913
1241
|
];
|
|
914
1242
|
var ANCHORED_PREVIEW_PRESETS = [
|
|
915
1243
|
{ name: "anchored-preview", phase: "state", primitive: "anchored-preview" },
|
|
916
|
-
{
|
|
917
|
-
|
|
918
|
-
|
|
1244
|
+
{
|
|
1245
|
+
name: "anchored-preview-bottom",
|
|
1246
|
+
phase: "state",
|
|
1247
|
+
primitive: "anchored-preview",
|
|
1248
|
+
params: { place: "bottom" }
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
name: "anchored-preview-left",
|
|
1252
|
+
phase: "state",
|
|
1253
|
+
primitive: "anchored-preview",
|
|
1254
|
+
params: { place: "left" }
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
name: "anchored-preview-right",
|
|
1258
|
+
phase: "state",
|
|
1259
|
+
primitive: "anchored-preview",
|
|
1260
|
+
params: { place: "right" }
|
|
1261
|
+
}
|
|
919
1262
|
];
|
|
920
1263
|
var searchExpandParams = {
|
|
921
1264
|
duration: { type: "time", default: "260ms", cssProperty: "--kui-duration" },
|
|
@@ -933,7 +1276,7 @@ var SEARCH_EXPAND_PRIMITIVES = [
|
|
|
933
1276
|
"search-expand",
|
|
934
1277
|
["expand", "discrete", "search-field"],
|
|
935
1278
|
searchExpandParams,
|
|
936
|
-
"layout"
|
|
1279
|
+
{ perfClass: "layout" }
|
|
937
1280
|
)
|
|
938
1281
|
];
|
|
939
1282
|
var SEARCH_EXPAND_PRESETS = [
|
|
@@ -1349,7 +1692,10 @@ var cursorDotParams = {
|
|
|
1349
1692
|
label: { type: "text", default: "", cssProperty: "--kui-label" }
|
|
1350
1693
|
};
|
|
1351
1694
|
var POINTER_PRIMITIVES = [
|
|
1352
|
-
|
|
1695
|
+
// `skew` is the `transform` shorthand's channel, which is what `prepareTilt3d` writes (a
|
|
1696
|
+
// perspective and two axis rotations) — never the `rotate` property — beside an inline
|
|
1697
|
+
// `transition` for its settle.
|
|
1698
|
+
pointerPrimitive("tilt-3d", ["skew", "transition"], tiltParams, deferPrepare(prepareTilt3d)),
|
|
1353
1699
|
pointerPrimitive("tilt-parallax", ["translate"], parallaxParams, deferPrepare(prepareTiltParallax)),
|
|
1354
1700
|
pointerPrimitive("cursor-follow", ["translate"], cursorDotParams, deferPrepare(prepareCursorFollow)),
|
|
1355
1701
|
pointerPrimitive("cursor-lag", ["translate"], cursorDotParams, deferPrepare(prepareCursorLag)),
|
|
@@ -1358,7 +1704,13 @@ var POINTER_PRIMITIVES = [
|
|
|
1358
1704
|
// `pseudo-before`: the glow overlay is a `::before` rule in `interaction.css`, so this primitive
|
|
1359
1705
|
// owns that box the same way the border family does. See `border-draw`'s comment in
|
|
1360
1706
|
// `HOVER_PRIMITIVES` for what the token means and why it was added here in the same change.
|
|
1361
|
-
|
|
1707
|
+
// `position`: the `relative` it gives an unpositioned host so that overlay has an anchor.
|
|
1708
|
+
pointerPrimitive(
|
|
1709
|
+
"cursor-spotlight",
|
|
1710
|
+
["spotlight", "pseudo-before", "position"],
|
|
1711
|
+
{},
|
|
1712
|
+
deferPrepare(prepareSpotlight)
|
|
1713
|
+
)
|
|
1362
1714
|
];
|
|
1363
1715
|
function prepareCursorFollow(el, params, ctx) {
|
|
1364
1716
|
return prepareCursorDot(el, params, ctx, "kui-cursor-dot-follow");
|
|
@@ -1838,52 +2190,6 @@ function installBackgroundMedia(el, ctx, options) {
|
|
|
1838
2190
|
};
|
|
1839
2191
|
}
|
|
1840
2192
|
|
|
1841
|
-
// src/core/element-size.ts
|
|
1842
|
-
function watchElementSize(win, onChange) {
|
|
1843
|
-
const ResizeObserverCtor = win.ResizeObserver;
|
|
1844
|
-
const observer = ResizeObserverCtor ? new ResizeObserverCtor(onChange) : void 0;
|
|
1845
|
-
const elements = /* @__PURE__ */ new Set();
|
|
1846
|
-
win.addEventListener("resize", onChange, { passive: true });
|
|
1847
|
-
return {
|
|
1848
|
-
observe(element) {
|
|
1849
|
-
if (elements.has(element)) return;
|
|
1850
|
-
elements.add(element);
|
|
1851
|
-
observer?.observe(element);
|
|
1852
|
-
},
|
|
1853
|
-
unobserve(element) {
|
|
1854
|
-
elements.delete(element);
|
|
1855
|
-
observer?.unobserve(element);
|
|
1856
|
-
},
|
|
1857
|
-
disconnect() {
|
|
1858
|
-
win.removeEventListener("resize", onChange);
|
|
1859
|
-
observer?.disconnect();
|
|
1860
|
-
elements.clear();
|
|
1861
|
-
}
|
|
1862
|
-
};
|
|
1863
|
-
}
|
|
1864
|
-
function frameScheduler(win, callback) {
|
|
1865
|
-
let frame;
|
|
1866
|
-
let timeout;
|
|
1867
|
-
const run = () => {
|
|
1868
|
-
frame = void 0;
|
|
1869
|
-
timeout = void 0;
|
|
1870
|
-
callback();
|
|
1871
|
-
};
|
|
1872
|
-
return {
|
|
1873
|
-
request() {
|
|
1874
|
-
if (frame !== void 0 || timeout !== void 0) return;
|
|
1875
|
-
if (win.requestAnimationFrame) frame = win.requestAnimationFrame(run);
|
|
1876
|
-
else timeout = win.setTimeout(run, 16);
|
|
1877
|
-
},
|
|
1878
|
-
cancel() {
|
|
1879
|
-
if (frame !== void 0) win.cancelAnimationFrame(frame);
|
|
1880
|
-
if (timeout !== void 0) win.clearTimeout(timeout);
|
|
1881
|
-
frame = void 0;
|
|
1882
|
-
timeout = void 0;
|
|
1883
|
-
}
|
|
1884
|
-
};
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
2193
|
// src/effects/catalog/media-shared.ts
|
|
1888
2194
|
var AXIS_DEGREES = { vertical: 0, horizontal: 90 };
|
|
1889
2195
|
var UNIT_DEGREES = { deg: 1, d: 1, grad: 0.9, rad: 180 / Math.PI, turn: 360 };
|
|
@@ -2242,14 +2548,14 @@ function prepareSlatAssemble(el, params, ctx) {
|
|
|
2242
2548
|
settle2 = resolve;
|
|
2243
2549
|
});
|
|
2244
2550
|
let landed = false;
|
|
2245
|
-
const
|
|
2551
|
+
const land2 = () => {
|
|
2246
2552
|
if (landed) return;
|
|
2247
2553
|
landed = true;
|
|
2248
2554
|
stage.classList.remove("kui-slat-animating");
|
|
2249
2555
|
built.restore();
|
|
2250
2556
|
settle2();
|
|
2251
2557
|
};
|
|
2252
|
-
const timer = ctx.win.setTimeout(
|
|
2558
|
+
const timer = ctx.win.setTimeout(land2, slatAssembleFinishMs(params, count));
|
|
2253
2559
|
return {
|
|
2254
2560
|
cleanup: () => {
|
|
2255
2561
|
ctx.win.clearTimeout(timer);
|
|
@@ -2260,7 +2566,7 @@ function prepareSlatAssemble(el, params, ctx) {
|
|
|
2260
2566
|
finished,
|
|
2261
2567
|
finish: () => {
|
|
2262
2568
|
ctx.win.clearTimeout(timer);
|
|
2263
|
-
|
|
2569
|
+
land2();
|
|
2264
2570
|
}
|
|
2265
2571
|
};
|
|
2266
2572
|
}
|
|
@@ -2395,7 +2701,9 @@ var MEDIA_JS_PRIMITIVES = [
|
|
|
2395
2701
|
{
|
|
2396
2702
|
id: "slat-assemble",
|
|
2397
2703
|
renderer: "javascript",
|
|
2398
|
-
|
|
2704
|
+
// `position` for the `relative` it gives an unpositioned host, and the subtree because
|
|
2705
|
+
// `installSlatStage` appends its slat stage there.
|
|
2706
|
+
channels: [CHANNEL.opacity, CHANNEL.translate, CHANNEL.rotate, "position", SUBTREE_CHANNEL],
|
|
2399
2707
|
parameters: slatParams,
|
|
2400
2708
|
supportedTimelines: ["time"],
|
|
2401
2709
|
supportedActivations: ["load", "enter", "hover", "focus", "click", "manual"],
|
|
@@ -2419,12 +2727,15 @@ var MEDIA_JS_PRIMITIVES = [
|
|
|
2419
2727
|
* and it is what makes `background-media, video-scrub` on one element a reported conflict
|
|
2420
2728
|
* rather than two effects silently fighting over the same picture.
|
|
2421
2729
|
*
|
|
2422
|
-
* `
|
|
2423
|
-
*
|
|
2424
|
-
*
|
|
2425
|
-
*
|
|
2730
|
+
* `position` and `isolation` are the two properties preparation writes on the *host*, a
|
|
2731
|
+
* stacking-context claim on someone else's element. Left undeclared, `background-media,
|
|
2732
|
+
* pin-section` composed silently while both decided what `position` the host has — the
|
|
2733
|
+
* conflict detector cannot report a claim it was never told about.
|
|
2734
|
+
*
|
|
2735
|
+
* And the subtree, because the layer is appended into the host (`installBackgroundMedia`): it
|
|
2736
|
+
* would become a deck's last slide, or `scroll-story`'s `:last-child` in place of its sections.
|
|
2426
2737
|
*/
|
|
2427
|
-
channels: ["media", "
|
|
2738
|
+
channels: ["media", "position", "isolation", SUBTREE_CHANNEL],
|
|
2428
2739
|
renderer: "javascript",
|
|
2429
2740
|
parameters: backgroundMediaParams,
|
|
2430
2741
|
// Not a claim to support four timelines — an abstention. A backdrop is not driven by progress
|
|
@@ -2953,7 +3264,9 @@ function countPrimitive(id, parameters, prepare) {
|
|
|
2953
3264
|
return {
|
|
2954
3265
|
id,
|
|
2955
3266
|
renderer: "javascript",
|
|
2956
|
-
|
|
3267
|
+
// The subtree too: both counters rebuild the host's children (the number, or the odometer's
|
|
3268
|
+
// digit columns), so a deck or a widget beside one would lose what it put there.
|
|
3269
|
+
channels: ["content", SUBTREE_CHANNEL],
|
|
2957
3270
|
parameters,
|
|
2958
3271
|
supportedTimelines: ["time"],
|
|
2959
3272
|
supportedActivations: ["load", "enter", "hover", "focus", "click", "manual"],
|
|
@@ -3341,7 +3654,9 @@ function jsTextPrimitive(id, channels, options) {
|
|
|
3341
3654
|
return {
|
|
3342
3655
|
id,
|
|
3343
3656
|
renderer: "javascript",
|
|
3344
|
-
|
|
3657
|
+
// Every one of these replaces the host's children with its own spans or layers, so it owns
|
|
3658
|
+
// them: beside a deck it would split the slides, beside `slow-mo` it would swallow the toggle.
|
|
3659
|
+
channels: [...channels, SUBTREE_CHANNEL],
|
|
3345
3660
|
parameters: options.parameters,
|
|
3346
3661
|
supportedTimelines: ["time"],
|
|
3347
3662
|
supportedActivations: ["load", "enter", "hover", "focus", "click", "manual"],
|
|
@@ -3471,21 +3786,21 @@ function lineCompletion(ctx, params, layers, count) {
|
|
|
3471
3786
|
const finished = new Promise((resolve) => {
|
|
3472
3787
|
settle2 = resolve;
|
|
3473
3788
|
});
|
|
3474
|
-
const
|
|
3789
|
+
const land2 = () => {
|
|
3475
3790
|
ctx.win.clearTimeout(timer);
|
|
3476
3791
|
timer = void 0;
|
|
3477
3792
|
layers.decorative.removeAttribute("aria-hidden");
|
|
3478
3793
|
layers.srOnly.remove();
|
|
3479
3794
|
settle2();
|
|
3480
3795
|
};
|
|
3481
|
-
let timer = ctx.win.setTimeout(
|
|
3796
|
+
let timer = ctx.win.setTimeout(land2, splitRevealFinishMs(params, count));
|
|
3482
3797
|
return {
|
|
3483
3798
|
finished,
|
|
3484
|
-
land,
|
|
3799
|
+
land: land2,
|
|
3485
3800
|
rearm(nextCount) {
|
|
3486
3801
|
if (timer === void 0) return;
|
|
3487
3802
|
ctx.win.clearTimeout(timer);
|
|
3488
|
-
timer = ctx.win.setTimeout(
|
|
3803
|
+
timer = ctx.win.setTimeout(land2, splitRevealFinishMs(params, nextCount));
|
|
3489
3804
|
},
|
|
3490
3805
|
cancel() {
|
|
3491
3806
|
if (timer !== void 0) ctx.win.clearTimeout(timer);
|
|
@@ -3724,7 +4039,8 @@ var TEXT_JS_PRIMITIVES = [
|
|
|
3724
4039
|
prepare: deferPrepare(prepareTypewriter),
|
|
3725
4040
|
perfClass: "continuous"
|
|
3726
4041
|
}),
|
|
3727
|
-
|
|
4042
|
+
// `min-width`/`min-height`: the rest box it pins while the glyphs churn (`prepareScramble`).
|
|
4043
|
+
jsTextPrimitive("scramble-text", ["content", "min-width", "min-height"], {
|
|
3728
4044
|
parameters: scrambleParams,
|
|
3729
4045
|
prepare: deferPrepare(prepareScramble),
|
|
3730
4046
|
perfClass: "continuous"
|
|
@@ -4133,6 +4449,36 @@ function registerCore(registry) {
|
|
|
4133
4449
|
return registry;
|
|
4134
4450
|
}
|
|
4135
4451
|
|
|
4452
|
+
// src/core/deck-viewer.ts
|
|
4453
|
+
var provided = null;
|
|
4454
|
+
function provideDeckViewer(viewer) {
|
|
4455
|
+
provided = viewer;
|
|
4456
|
+
}
|
|
4457
|
+
function attachDeckViewer(request) {
|
|
4458
|
+
return provided ? provided(request) : null;
|
|
4459
|
+
}
|
|
4460
|
+
var VIEWER_EVENT = "kui:viewer";
|
|
4461
|
+
function announceViewer(doc, opener, open2) {
|
|
4462
|
+
const Ctor = doc.defaultView?.CustomEvent ?? CustomEvent;
|
|
4463
|
+
const source = opener.isConnected ? opener : doc;
|
|
4464
|
+
source.dispatchEvent(new Ctor(VIEWER_EVENT, { bubbles: true, detail: { open: open2 } }));
|
|
4465
|
+
}
|
|
4466
|
+
var DECK_LIGHTBOX_PARAM = {
|
|
4467
|
+
type: "keyword",
|
|
4468
|
+
default: "false",
|
|
4469
|
+
cssProperty: "--kui-deck-lightbox",
|
|
4470
|
+
keywords: ["true", "false"]
|
|
4471
|
+
};
|
|
4472
|
+
function deckLightbox(enabled, request, warn) {
|
|
4473
|
+
if (!enabled) return () => {
|
|
4474
|
+
};
|
|
4475
|
+
const release2 = attachDeckViewer(request);
|
|
4476
|
+
if (release2) return release2;
|
|
4477
|
+
warn("lightbox:true needs the showcase module (the lightbox gallery), which is not loaded; cards will not open");
|
|
4478
|
+
return () => {
|
|
4479
|
+
};
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4136
4482
|
// src/effects/step-marking.ts
|
|
4137
4483
|
var STEP_STATE_ATTR = "data-kui-step-state";
|
|
4138
4484
|
var STEP_OFFSET_ATTR = "data-kui-step-offset";
|
|
@@ -4221,28 +4567,49 @@ function countSteps(params, resolveSteps) {
|
|
|
4221
4567
|
}
|
|
4222
4568
|
return Math.max(1, ...groups.values());
|
|
4223
4569
|
}
|
|
4570
|
+
function bindControl(request, param, run) {
|
|
4571
|
+
const { el, ctx, params, label } = request;
|
|
4572
|
+
const selector = resolveTarget(params.text(param), ctx, `${label} ${param}`);
|
|
4573
|
+
if (!selector) return null;
|
|
4574
|
+
if (queryControls(el, ctx, selector).length === 0) ctx.warn(`${label} ${param} "${selector}" matched nothing`);
|
|
4575
|
+
return { selector, run };
|
|
4576
|
+
}
|
|
4224
4577
|
function delegateControls(request) {
|
|
4225
|
-
const { el, ctx,
|
|
4578
|
+
const { el, ctx, groups } = request;
|
|
4226
4579
|
if (groups.length === 0) return () => {
|
|
4227
4580
|
};
|
|
4228
|
-
const
|
|
4229
|
-
const onClick = (event) => {
|
|
4230
|
-
const from = event.target;
|
|
4231
|
-
if (typeof from?.closest !== "function") return;
|
|
4581
|
+
const press = (from, inside) => {
|
|
4232
4582
|
for (const { selector, run } of groups) {
|
|
4233
|
-
const matches =
|
|
4583
|
+
const matches = queryControls(el, ctx, selector);
|
|
4234
4584
|
const matched = new Set(matches);
|
|
4585
|
+
const stop = inside ? el : null;
|
|
4235
4586
|
let node = from;
|
|
4236
|
-
while (node && !matched.has(node)) node = node.parentElement;
|
|
4237
|
-
if (!node) continue;
|
|
4587
|
+
while (node && node !== stop && !matched.has(node)) node = node.parentElement;
|
|
4588
|
+
if (!node || node === stop) continue;
|
|
4238
4589
|
run(node, matches.indexOf(node));
|
|
4239
4590
|
}
|
|
4240
4591
|
};
|
|
4241
|
-
|
|
4242
|
-
|
|
4592
|
+
const pressed = (event) => {
|
|
4593
|
+
const from = event.target;
|
|
4594
|
+
return typeof from?.closest === "function" ? from : null;
|
|
4595
|
+
};
|
|
4596
|
+
const onInside = (event) => {
|
|
4597
|
+
const from = pressed(event);
|
|
4598
|
+
if (from) press(from, true);
|
|
4599
|
+
};
|
|
4600
|
+
const onOutside = (event) => {
|
|
4601
|
+
const from = pressed(event);
|
|
4602
|
+
if (from && !el.contains(from)) press(from, false);
|
|
4603
|
+
};
|
|
4604
|
+
el.addEventListener("click", onInside);
|
|
4605
|
+
ctx.doc.addEventListener("click", onOutside);
|
|
4606
|
+
return () => {
|
|
4607
|
+
el.removeEventListener("click", onInside);
|
|
4608
|
+
ctx.doc.removeEventListener("click", onOutside);
|
|
4609
|
+
};
|
|
4243
4610
|
}
|
|
4244
4611
|
function createStepIndex(options) {
|
|
4245
|
-
const { el, params, ctx,
|
|
4612
|
+
const { el, params, ctx, resolveSteps, onRender } = options;
|
|
4246
4613
|
const name = options.name ?? "step-progress";
|
|
4247
4614
|
const marker = createStepMarker(resolveSteps, (message) => ctx.warn(`${name} ${message}`));
|
|
4248
4615
|
const self = createAttributeLedger(el);
|
|
@@ -4261,26 +4628,23 @@ function createStepIndex(options) {
|
|
|
4261
4628
|
};
|
|
4262
4629
|
const next = () => goTo(nextStep(step, total()));
|
|
4263
4630
|
const prev = () => goTo(prevStep(step, total()));
|
|
4264
|
-
const
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
if (!selector) return false;
|
|
4268
|
-
if (queryScoped(el, ctx, selector, scope).length === 0) {
|
|
4269
|
-
ctx.warn(`${name} ${param} "${selector}" matched nothing`);
|
|
4270
|
-
}
|
|
4271
|
-
groups.push({ selector, run });
|
|
4272
|
-
return true;
|
|
4631
|
+
const byHand = (move) => {
|
|
4632
|
+
options.onInput?.();
|
|
4633
|
+
move();
|
|
4273
4634
|
};
|
|
4274
|
-
const
|
|
4275
|
-
|
|
4276
|
-
bindControl("
|
|
4277
|
-
bindControl("
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
const
|
|
4635
|
+
const request = { el, ctx, params, label: name };
|
|
4636
|
+
const groups = [
|
|
4637
|
+
bindControl(request, "next", () => byHand(next)),
|
|
4638
|
+
bindControl(request, "prev", () => byHand(prev)),
|
|
4639
|
+
bindControl(request, "jump", (_node, position) => {
|
|
4640
|
+
if (position >= 0) byHand(() => goTo(position));
|
|
4641
|
+
}),
|
|
4642
|
+
...options.controls ?? []
|
|
4643
|
+
].filter((group) => group !== null);
|
|
4644
|
+
const fallback = options.clickFallback !== false && groups.length === 0;
|
|
4645
|
+
const onContainerClick = () => byHand(next);
|
|
4646
|
+
if (fallback) el.addEventListener("click", onContainerClick);
|
|
4647
|
+
const releaseControls = delegateControls({ el, ctx, groups });
|
|
4284
4648
|
render();
|
|
4285
4649
|
return {
|
|
4286
4650
|
goTo,
|
|
@@ -4289,7 +4653,7 @@ function createStepIndex(options) {
|
|
|
4289
4653
|
current: () => step,
|
|
4290
4654
|
total,
|
|
4291
4655
|
release: () => {
|
|
4292
|
-
if (fallback) el.removeEventListener("click",
|
|
4656
|
+
if (fallback) el.removeEventListener("click", onContainerClick);
|
|
4293
4657
|
releaseControls();
|
|
4294
4658
|
self.restore();
|
|
4295
4659
|
selfStyle.restore();
|
|
@@ -4298,51 +4662,327 @@ function createStepIndex(options) {
|
|
|
4298
4662
|
};
|
|
4299
4663
|
}
|
|
4300
4664
|
|
|
4301
|
-
// src/effects/
|
|
4302
|
-
var
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
// Native pseudo-classes drive these and the motion lands on a sibling, not on the control that
|
|
4320
|
-
// carries the attribute, so base.css's policy layer enforces this through its sibling
|
|
4321
|
-
// `transition-duration` rules rather than the `animation-*` ones.
|
|
4322
|
-
reducedMotion: "disable",
|
|
4323
|
-
prepare: withTimingContract("native-state", FORM_STATE_TIMING, () => inertInstance())
|
|
4324
|
-
};
|
|
4325
|
-
function prepareSiblingScale(cssProperty) {
|
|
4326
|
-
return (el, params) => {
|
|
4327
|
-
const sibling = el.nextElementSibling;
|
|
4328
|
-
sibling?.style.setProperty(cssProperty, String(params.num("scale", 1)));
|
|
4329
|
-
return () => sibling?.style.removeProperty(cssProperty);
|
|
4330
|
-
};
|
|
4665
|
+
// src/effects/auto-motion.ts
|
|
4666
|
+
var MIN_PERIOD_MS = 2e3;
|
|
4667
|
+
var MAX_FRAME_MS = 100;
|
|
4668
|
+
function clampPeriod(ms) {
|
|
4669
|
+
if (!Number.isFinite(ms) || ms === 0) return 0;
|
|
4670
|
+
return Math.sign(ms) * Math.max(MIN_PERIOD_MS, Math.abs(ms));
|
|
4671
|
+
}
|
|
4672
|
+
function cyclesFor(dtMs, periodMs) {
|
|
4673
|
+
return Math.min(Math.max(dtMs, 0), MAX_FRAME_MS) / periodMs;
|
|
4674
|
+
}
|
|
4675
|
+
function isKeyboardFocus(target) {
|
|
4676
|
+
const node = target;
|
|
4677
|
+
if (typeof node?.matches !== "function") return true;
|
|
4678
|
+
try {
|
|
4679
|
+
return node.matches(":focus-visible");
|
|
4680
|
+
} catch {
|
|
4681
|
+
return true;
|
|
4682
|
+
}
|
|
4331
4683
|
}
|
|
4332
|
-
function
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4684
|
+
function createAutoMotion(request) {
|
|
4685
|
+
const { el, ctx, spinMs, autoplayMs, settleMs, advance, step, setSpinning, onPausedChange } = request;
|
|
4686
|
+
const pauseOnHover = request.pauseOnHover ?? true;
|
|
4687
|
+
const enabled = spinMs !== 0 || autoplayMs !== 0;
|
|
4688
|
+
if (!enabled) {
|
|
4689
|
+
return {
|
|
4690
|
+
enabled,
|
|
4691
|
+
interrupt() {
|
|
4692
|
+
},
|
|
4693
|
+
hold() {
|
|
4694
|
+
},
|
|
4695
|
+
toggle() {
|
|
4696
|
+
},
|
|
4697
|
+
release() {
|
|
4698
|
+
}
|
|
4699
|
+
};
|
|
4700
|
+
}
|
|
4701
|
+
const win = ctx.win;
|
|
4702
|
+
let userPaused = ctx.reducedMotion;
|
|
4703
|
+
let hovered = false;
|
|
4704
|
+
let focused = false;
|
|
4705
|
+
let onscreen = true;
|
|
4706
|
+
let gesture = false;
|
|
4707
|
+
let held = false;
|
|
4708
|
+
let viewing = false;
|
|
4709
|
+
let viewerOpen = false;
|
|
4710
|
+
let focusBeforeViewer = false;
|
|
4711
|
+
let frame = 0;
|
|
4712
|
+
let lastFrameAt = null;
|
|
4713
|
+
let timer = null;
|
|
4714
|
+
let holdTimer = null;
|
|
4715
|
+
let spinning = false;
|
|
4716
|
+
const active = () => !userPaused && !hovered && !focused && onscreen && !gesture && !held && !viewing && !ctx.doc.hidden;
|
|
4717
|
+
const requestFrame = (callback) => typeof win.requestAnimationFrame === "function" ? win.requestAnimationFrame(callback) : win.setTimeout(() => callback(Date.now()), 16);
|
|
4718
|
+
const cancelFrame = (handle) => {
|
|
4719
|
+
if (typeof win.cancelAnimationFrame === "function") win.cancelAnimationFrame(handle);
|
|
4720
|
+
else win.clearTimeout(handle);
|
|
4721
|
+
};
|
|
4722
|
+
const publishSpinning = (next) => {
|
|
4723
|
+
if (next === spinning) return;
|
|
4724
|
+
spinning = next;
|
|
4725
|
+
setSpinning(next);
|
|
4726
|
+
};
|
|
4727
|
+
const tick = (now) => {
|
|
4728
|
+
frame = 0;
|
|
4729
|
+
if (!active()) return;
|
|
4730
|
+
if (lastFrameAt !== null) advance(cyclesFor(now - lastFrameAt, spinMs));
|
|
4731
|
+
lastFrameAt = now;
|
|
4732
|
+
frame = requestFrame(tick);
|
|
4733
|
+
};
|
|
4734
|
+
const startSpin = () => {
|
|
4735
|
+
if (frame !== 0) return;
|
|
4736
|
+
publishSpinning(true);
|
|
4737
|
+
lastFrameAt = null;
|
|
4738
|
+
frame = requestFrame(tick);
|
|
4739
|
+
};
|
|
4740
|
+
const stopSpin = () => {
|
|
4741
|
+
if (frame !== 0) cancelFrame(frame);
|
|
4742
|
+
frame = 0;
|
|
4743
|
+
publishSpinning(false);
|
|
4744
|
+
};
|
|
4745
|
+
const armAutoplay = () => {
|
|
4746
|
+
if (timer !== null) return;
|
|
4747
|
+
timer = win.setTimeout(() => {
|
|
4748
|
+
timer = null;
|
|
4749
|
+
step(autoplayMs > 0 ? 1 : -1);
|
|
4750
|
+
update();
|
|
4751
|
+
}, Math.abs(autoplayMs));
|
|
4344
4752
|
};
|
|
4345
|
-
|
|
4753
|
+
const clearAutoplay = () => {
|
|
4754
|
+
if (timer === null) return;
|
|
4755
|
+
win.clearTimeout(timer);
|
|
4756
|
+
timer = null;
|
|
4757
|
+
};
|
|
4758
|
+
function update() {
|
|
4759
|
+
const running = active();
|
|
4760
|
+
if (spinMs !== 0) (running ? startSpin : stopSpin)();
|
|
4761
|
+
if (autoplayMs !== 0) (running ? armAutoplay : clearAutoplay)();
|
|
4762
|
+
}
|
|
4763
|
+
const onEnter = () => {
|
|
4764
|
+
hovered = true;
|
|
4765
|
+
update();
|
|
4766
|
+
};
|
|
4767
|
+
const onLeave = () => {
|
|
4768
|
+
hovered = false;
|
|
4769
|
+
update();
|
|
4770
|
+
};
|
|
4771
|
+
const onFocusIn = (event) => {
|
|
4772
|
+
focused = (!viewerOpen || focusBeforeViewer) && isKeyboardFocus(event.target);
|
|
4773
|
+
update();
|
|
4774
|
+
};
|
|
4775
|
+
const onFocusOut = (event) => {
|
|
4776
|
+
if (!viewerOpen) focusBeforeViewer = focused;
|
|
4777
|
+
const next = event.relatedTarget;
|
|
4778
|
+
focused = next !== null && el.contains(next) && isKeyboardFocus(next);
|
|
4779
|
+
update();
|
|
4780
|
+
};
|
|
4781
|
+
const onVisibility = () => update();
|
|
4782
|
+
const onViewer = (event) => {
|
|
4783
|
+
const open2 = event.detail?.open === true;
|
|
4784
|
+
viewerOpen = open2;
|
|
4785
|
+
viewing = open2 && el.contains(event.target);
|
|
4786
|
+
update();
|
|
4787
|
+
};
|
|
4788
|
+
if (pauseOnHover) {
|
|
4789
|
+
el.addEventListener("pointerenter", onEnter);
|
|
4790
|
+
el.addEventListener("pointerleave", onLeave);
|
|
4791
|
+
}
|
|
4792
|
+
el.addEventListener("focusin", onFocusIn);
|
|
4793
|
+
el.addEventListener("focusout", onFocusOut);
|
|
4794
|
+
ctx.doc.addEventListener("visibilitychange", onVisibility);
|
|
4795
|
+
ctx.doc.addEventListener(VIEWER_EVENT, onViewer);
|
|
4796
|
+
const Observer = win.IntersectionObserver;
|
|
4797
|
+
const observer = Observer ? new Observer((entries) => {
|
|
4798
|
+
for (const entry of entries) onscreen = entry.isIntersecting;
|
|
4799
|
+
update();
|
|
4800
|
+
}) : null;
|
|
4801
|
+
observer?.observe(el);
|
|
4802
|
+
onPausedChange(userPaused);
|
|
4803
|
+
update();
|
|
4804
|
+
return {
|
|
4805
|
+
enabled,
|
|
4806
|
+
interrupt() {
|
|
4807
|
+
held = true;
|
|
4808
|
+
if (holdTimer !== null) win.clearTimeout(holdTimer);
|
|
4809
|
+
holdTimer = win.setTimeout(() => {
|
|
4810
|
+
holdTimer = null;
|
|
4811
|
+
held = false;
|
|
4812
|
+
update();
|
|
4813
|
+
}, Math.max(0, settleMs));
|
|
4814
|
+
update();
|
|
4815
|
+
},
|
|
4816
|
+
hold(holding) {
|
|
4817
|
+
gesture = holding;
|
|
4818
|
+
update();
|
|
4819
|
+
},
|
|
4820
|
+
toggle() {
|
|
4821
|
+
userPaused = !userPaused;
|
|
4822
|
+
onPausedChange(userPaused);
|
|
4823
|
+
update();
|
|
4824
|
+
},
|
|
4825
|
+
release() {
|
|
4826
|
+
if (frame !== 0) cancelFrame(frame);
|
|
4827
|
+
frame = 0;
|
|
4828
|
+
if (timer !== null) win.clearTimeout(timer);
|
|
4829
|
+
if (holdTimer !== null) win.clearTimeout(holdTimer);
|
|
4830
|
+
timer = null;
|
|
4831
|
+
holdTimer = null;
|
|
4832
|
+
observer?.disconnect();
|
|
4833
|
+
el.removeEventListener("pointerenter", onEnter);
|
|
4834
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
4835
|
+
el.removeEventListener("focusin", onFocusIn);
|
|
4836
|
+
el.removeEventListener("focusout", onFocusOut);
|
|
4837
|
+
ctx.doc.removeEventListener("visibilitychange", onVisibility);
|
|
4838
|
+
ctx.doc.removeEventListener(VIEWER_EVENT, onViewer);
|
|
4839
|
+
publishSpinning(false);
|
|
4840
|
+
}
|
|
4841
|
+
};
|
|
4842
|
+
}
|
|
4843
|
+
var AUTOPLAY_PARAM = { type: "time", default: "0s", cssProperty: "--kui-autoplay" };
|
|
4844
|
+
var PAUSE_PARAM = { type: "text", default: "", cssProperty: "--kui-pause" };
|
|
4845
|
+
var HOVER_PARAM = {
|
|
4846
|
+
type: "keyword",
|
|
4847
|
+
default: "pause",
|
|
4848
|
+
keywords: ["pause", "none"],
|
|
4849
|
+
cssProperty: "--kui-hover"
|
|
4850
|
+
};
|
|
4851
|
+
function motionPeriods(request) {
|
|
4852
|
+
const { params, ctx, label } = request;
|
|
4853
|
+
const spinRaw = request.spin ? params.ms("spin", 0) : 0;
|
|
4854
|
+
const autoplayRaw = params.ms("autoplay", 0);
|
|
4855
|
+
const spin2 = clampPeriod(spinRaw);
|
|
4856
|
+
const autoplay = clampPeriod(autoplayRaw);
|
|
4857
|
+
if (spin2 !== spinRaw) ctx.warn(`${label} spin: minimum is 2s; clamped to ${Math.abs(spin2) / 1e3}s`);
|
|
4858
|
+
if (autoplay !== autoplayRaw) {
|
|
4859
|
+
ctx.warn(`${label} autoplay: minimum is 2s; clamped to ${Math.abs(autoplay) / 1e3}s`);
|
|
4860
|
+
}
|
|
4861
|
+
if (spin2 !== 0 && autoplay !== 0) {
|
|
4862
|
+
ctx.warn(`${label}: spin: and autoplay: are both set; spin: wins`);
|
|
4863
|
+
return { spin: spin2, autoplay: 0 };
|
|
4864
|
+
}
|
|
4865
|
+
return { spin: spin2, autoplay };
|
|
4866
|
+
}
|
|
4867
|
+
function createDeckMotion(request) {
|
|
4868
|
+
const { el, ctx, params, label, settleMs, step } = request;
|
|
4869
|
+
const { spin: spin2, autoplay } = motionPeriods(request);
|
|
4870
|
+
const ledgers = /* @__PURE__ */ new Map();
|
|
4871
|
+
const pauseSelector = resolveTarget(params.text("pause"), ctx, `${label} pause`);
|
|
4872
|
+
const reflectPaused = (paused) => {
|
|
4873
|
+
if (!pauseSelector) return;
|
|
4874
|
+
for (const node of queryControls(el, ctx, pauseSelector)) {
|
|
4875
|
+
let ledger = ledgers.get(node);
|
|
4876
|
+
if (!ledger) {
|
|
4877
|
+
ledger = createAttributeLedger(node);
|
|
4878
|
+
ledgers.set(node, ledger);
|
|
4879
|
+
}
|
|
4880
|
+
ledger.set("aria-pressed", String(paused));
|
|
4881
|
+
}
|
|
4882
|
+
};
|
|
4883
|
+
const motion = createAutoMotion({
|
|
4884
|
+
el,
|
|
4885
|
+
ctx,
|
|
4886
|
+
spinMs: spin2,
|
|
4887
|
+
autoplayMs: autoplay,
|
|
4888
|
+
settleMs,
|
|
4889
|
+
pauseOnHover: !params.is("hover", "none"),
|
|
4890
|
+
advance: request.spin?.advance,
|
|
4891
|
+
step,
|
|
4892
|
+
setSpinning: request.spin?.setSpinning,
|
|
4893
|
+
onPausedChange: reflectPaused
|
|
4894
|
+
});
|
|
4895
|
+
let pause = null;
|
|
4896
|
+
if (pauseSelector && !motion.enabled) {
|
|
4897
|
+
ctx.warn(`${label} pause: has nothing to pause without ${request.spin ? "spin: or " : ""}autoplay:`);
|
|
4898
|
+
} else if (pauseSelector) {
|
|
4899
|
+
pause = bindControl({ el, ctx, params, label }, "pause", () => motion.toggle());
|
|
4900
|
+
}
|
|
4901
|
+
return {
|
|
4902
|
+
motion,
|
|
4903
|
+
pause,
|
|
4904
|
+
release() {
|
|
4905
|
+
motion.release();
|
|
4906
|
+
for (const ledger of ledgers.values()) ledger.restore();
|
|
4907
|
+
ledgers.clear();
|
|
4908
|
+
}
|
|
4909
|
+
};
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4912
|
+
// src/effects/swipe-event.ts
|
|
4913
|
+
var SWIPE_EVENT = "kui:swipe";
|
|
4914
|
+
var DIRECTIONS = /* @__PURE__ */ new Set(["left", "right", "up", "down"]);
|
|
4915
|
+
function announceSwipe(win, el, direction) {
|
|
4916
|
+
const Ctor = win.CustomEvent;
|
|
4917
|
+
el.dispatchEvent(new Ctor(SWIPE_EVENT, { bubbles: true, detail: { direction } }));
|
|
4918
|
+
}
|
|
4919
|
+
function swipeStep(direction) {
|
|
4920
|
+
return direction === "left" || direction === "up" ? 1 : -1;
|
|
4921
|
+
}
|
|
4922
|
+
function swipeReaches(source, el) {
|
|
4923
|
+
if (source === el) return true;
|
|
4924
|
+
if (!source.contains(el)) return false;
|
|
4925
|
+
for (let node = el.parentElement; node && node !== source; node = node.parentElement) {
|
|
4926
|
+
if (node.hasAttribute("data-kui-step")) return false;
|
|
4927
|
+
}
|
|
4928
|
+
return true;
|
|
4929
|
+
}
|
|
4930
|
+
function stepOnSwipe(doc, el, step) {
|
|
4931
|
+
const onSwipe = (event) => {
|
|
4932
|
+
const direction = event.detail?.direction;
|
|
4933
|
+
const source = event.target;
|
|
4934
|
+
if (!direction || !DIRECTIONS.has(direction) || source.nodeType !== 1) return;
|
|
4935
|
+
if (swipeReaches(source, el)) step(swipeStep(direction));
|
|
4936
|
+
};
|
|
4937
|
+
doc.addEventListener(SWIPE_EVENT, onSwipe);
|
|
4938
|
+
return () => doc.removeEventListener(SWIPE_EVENT, onSwipe);
|
|
4939
|
+
}
|
|
4940
|
+
|
|
4941
|
+
// src/effects/forms/primitives.ts
|
|
4942
|
+
var timing = {
|
|
4943
|
+
duration: { type: "time", default: "400ms", cssProperty: "--kui-duration" },
|
|
4944
|
+
...TRIGGER_DELAY_PARAM,
|
|
4945
|
+
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" }
|
|
4946
|
+
};
|
|
4947
|
+
var FORM_STATE_TIMING = {
|
|
4948
|
+
because: "forms.css pins its timing literally \u2014 the motion lands on a sibling that inline custom properties cannot reach"
|
|
4949
|
+
};
|
|
4950
|
+
var NATIVE_STATE_PRIMITIVE = {
|
|
4951
|
+
id: "native-state",
|
|
4952
|
+
renderer: "javascript",
|
|
4953
|
+
channels: [CHANNEL.translate, CHANNEL.scale, CHANNEL.opacity, CHANNEL.stroke, CHANNEL.color],
|
|
4954
|
+
parameters: {},
|
|
4955
|
+
supportedTimelines: ["time"],
|
|
4956
|
+
supportedActivations: ["load"],
|
|
4957
|
+
defaultActivation: "load",
|
|
4958
|
+
perfClass: "compositor",
|
|
4959
|
+
// Native pseudo-classes drive these and the motion lands on a sibling, not on the control that
|
|
4960
|
+
// carries the attribute, so base.css's policy layer enforces this through its sibling
|
|
4961
|
+
// `transition-duration` rules rather than the `animation-*` ones.
|
|
4962
|
+
reducedMotion: "disable",
|
|
4963
|
+
prepare: withTimingContract("native-state", FORM_STATE_TIMING, () => inertInstance())
|
|
4964
|
+
};
|
|
4965
|
+
function prepareSiblingScale(cssProperty) {
|
|
4966
|
+
return (el, params) => {
|
|
4967
|
+
const sibling = el.nextElementSibling;
|
|
4968
|
+
sibling?.style.setProperty(cssProperty, String(params.num("scale", 1)));
|
|
4969
|
+
return () => sibling?.style.removeProperty(cssProperty);
|
|
4970
|
+
};
|
|
4971
|
+
}
|
|
4972
|
+
function siblingScalePrimitive(id, cssProperty) {
|
|
4973
|
+
return {
|
|
4974
|
+
id,
|
|
4975
|
+
renderer: "javascript",
|
|
4976
|
+
channels: [CHANNEL.translate, CHANNEL.scale, CHANNEL.opacity, CHANNEL.stroke, CHANNEL.color],
|
|
4977
|
+
parameters: { scale: { type: "number", default: "1", cssProperty } },
|
|
4978
|
+
supportedTimelines: ["time"],
|
|
4979
|
+
supportedActivations: ["load"],
|
|
4980
|
+
defaultActivation: "load",
|
|
4981
|
+
perfClass: "compositor",
|
|
4982
|
+
reducedMotion: "disable",
|
|
4983
|
+
prepare: withTimingContract(id, FORM_STATE_TIMING, deferPrepare(prepareSiblingScale(cssProperty)))
|
|
4984
|
+
};
|
|
4985
|
+
}
|
|
4346
4986
|
var TOGGLE_MORPH_PRIMITIVE = siblingScalePrimitive("toggle-morph", "--kui-toggle-scale");
|
|
4347
4987
|
var RADIO_FILL_PRIMITIVE = siblingScalePrimitive("radio-fill", "--kui-radio-scale");
|
|
4348
4988
|
var FOCUS_RING_PRIMITIVE = cssPrimitive("focus-ring", ["shadow"], {
|
|
@@ -4418,7 +5058,7 @@ function prepareRangeFill(el, params, ctx) {
|
|
|
4418
5058
|
}
|
|
4419
5059
|
var STRENGTH_METER_PRIMITIVE = jsInputPrimitive(
|
|
4420
5060
|
"strength-meter",
|
|
4421
|
-
["meter"],
|
|
5061
|
+
["meter", attributeChannel("data-kui-strength-level")],
|
|
4422
5062
|
{},
|
|
4423
5063
|
deferPrepare(prepareStrengthMeter)
|
|
4424
5064
|
);
|
|
@@ -4429,14 +5069,55 @@ var RANGE_FILL_PRIMITIVE = jsInputPrimitive(
|
|
|
4429
5069
|
deferPrepare(prepareRangeFill)
|
|
4430
5070
|
);
|
|
4431
5071
|
function prepareStepProgress(el, params, ctx) {
|
|
4432
|
-
const
|
|
5072
|
+
const label = "step-progress";
|
|
5073
|
+
const selector = resolveTarget(params.text("target"), ctx, label);
|
|
4433
5074
|
const scope = scopeParam(params, "page");
|
|
4434
5075
|
const resolveSteps = () => selector ? queryScoped(el, ctx, selector, scope) : el.children;
|
|
4435
|
-
|
|
5076
|
+
const lightbox = params.is("lightbox");
|
|
5077
|
+
const { motion, pause, release: releaseMotion } = createDeckMotion({
|
|
5078
|
+
el,
|
|
5079
|
+
ctx,
|
|
5080
|
+
params,
|
|
5081
|
+
label,
|
|
5082
|
+
settleMs: effectDurationMs(params, 400),
|
|
5083
|
+
step: (direction) => direction > 0 ? index.next() : index.prev()
|
|
5084
|
+
});
|
|
5085
|
+
const index = createStepIndex({
|
|
5086
|
+
el,
|
|
5087
|
+
params,
|
|
5088
|
+
ctx,
|
|
5089
|
+
resolveSteps,
|
|
5090
|
+
controls: pause ? [pause] : [],
|
|
5091
|
+
onInput: () => motion.interrupt(),
|
|
5092
|
+
/*
|
|
5093
|
+
* With `lightbox:true` a click on a slide opens the viewer, so the container click cannot also
|
|
5094
|
+
* advance the deck: one press would both open the gallery and move the slide behind it. The
|
|
5095
|
+
* deck still moves on its arrows, dots, swipe and autoplay.
|
|
5096
|
+
*/
|
|
5097
|
+
clickFallback: !lightbox
|
|
5098
|
+
});
|
|
5099
|
+
const releaseViewer = deckLightbox(
|
|
5100
|
+
lightbox,
|
|
5101
|
+
{ host: el, cards: resolveSteps, doc: ctx.doc, reducedMotion: ctx.reducedMotion },
|
|
5102
|
+
(message) => ctx.warn(`${label} ${message}`)
|
|
5103
|
+
);
|
|
5104
|
+
const releaseSwipe = stepOnSwipe(ctx.doc, el, (direction) => {
|
|
5105
|
+
motion.interrupt();
|
|
5106
|
+
if (direction > 0) index.next();
|
|
5107
|
+
else index.prev();
|
|
5108
|
+
});
|
|
5109
|
+
return () => {
|
|
5110
|
+
releaseViewer();
|
|
5111
|
+
releaseSwipe();
|
|
5112
|
+
releaseMotion();
|
|
5113
|
+
index.release();
|
|
5114
|
+
};
|
|
4436
5115
|
}
|
|
4437
5116
|
var STEP_PROGRESS_BASE = jsInputPrimitive(
|
|
4438
5117
|
"step-progress",
|
|
4439
|
-
|
|
5118
|
+
// The host's children are its steps unless `target:` says otherwise, so a widget that inserts
|
|
5119
|
+
// one (`slow-mo`'s toggle) would become a step: the subtree is this deck's.
|
|
5120
|
+
[SUBTREE_CHANNEL, attributeChannel("data-kui-step")],
|
|
4440
5121
|
{
|
|
4441
5122
|
// Empty default, not '4': `readParams` fills every declared default in unconditionally and
|
|
4442
5123
|
// does not validate it, so an empty one is how `prepareStepProgress` tells "unauthored" from
|
|
@@ -4483,6 +5164,16 @@ var STEP_PROGRESS_BASE = jsInputPrimitive(
|
|
|
4483
5164
|
* at 390px and at 1440px, and the page keeps owning the box. Same reason `rest:` is a number.
|
|
4484
5165
|
*/
|
|
4485
5166
|
main: { type: "number", default: "1", cssProperty: "--kui-main", finite: true, minimum: 0 },
|
|
5167
|
+
/*
|
|
5168
|
+
* The deck steps itself every `autoplay:` (`0s`, the default, is off), and `pause:` names the
|
|
5169
|
+
* author's play/pause control, and `hover:none` stops a resting pointer pausing it. The spatial
|
|
5170
|
+
* decks' declarations and scheduler, shared — see `prepareStepProgress` and `effects/auto-motion.ts`.
|
|
5171
|
+
*/
|
|
5172
|
+
autoplay: AUTOPLAY_PARAM,
|
|
5173
|
+
pause: PAUSE_PARAM,
|
|
5174
|
+
hover: HOVER_PARAM,
|
|
5175
|
+
// A click on a slide opens every slide in the lightbox gallery. See `core/deck-viewer.ts`.
|
|
5176
|
+
lightbox: DECK_LIGHTBOX_PARAM,
|
|
4486
5177
|
// Which tree `target:` is searched in. Unset means this primitive's own historical answer —
|
|
4487
5178
|
// see `prepareStepProgress`. One declaration, shared: `effects/step-marking.ts`.
|
|
4488
5179
|
scope: SCOPE_PARAM
|
|
@@ -4529,7 +5220,7 @@ function prepareSubmitFlow(el, params, ctx) {
|
|
|
4529
5220
|
}
|
|
4530
5221
|
var SUBMIT_FLOW_PRIMITIVE = jsInputPrimitive(
|
|
4531
5222
|
"submit-flow",
|
|
4532
|
-
["
|
|
5223
|
+
[attributeChannel("data-kui-stage")],
|
|
4533
5224
|
{
|
|
4534
5225
|
load: { type: "time", default: "1200ms", cssProperty: "--kui-load" },
|
|
4535
5226
|
hold: { type: "time", default: "1500ms", cssProperty: "--kui-hold" }
|
|
@@ -4579,9 +5270,12 @@ var FORMS_PRESETS = [
|
|
|
4579
5270
|
* two readings is the stylesheet. `step-progress` was the wrong word to type on a deck of
|
|
4580
5271
|
* slides, which is the only reason this alias exists; nothing behaves differently under it.
|
|
4581
5272
|
*
|
|
4582
|
-
* It is still an *index*, not a carousel component: no ARIA, no roving focus
|
|
4583
|
-
*
|
|
4584
|
-
*
|
|
5273
|
+
* It is still an *index*, not a carousel component: no ARIA, no roving focus. That boundary is
|
|
5274
|
+
* the one section H states — the library animates elements you control and does not own the
|
|
5275
|
+
* widget — and naming this `carousel` does not move it. `autoplay:`/`pause:` do not cross it: they
|
|
5276
|
+
* press the same "next" a visitor's control does, on a timer, and the control is still the
|
|
5277
|
+
* author's markup (`effects/auto-motion.ts`, shared with the spatial decks). Likewise
|
|
5278
|
+
* `lightbox:true` hands a click on a slide to the showcase gallery (`core/deck-viewer.ts`).
|
|
4585
5279
|
*
|
|
4586
5280
|
* No `requiresOwnSubtree` here, unlike `step-progress` beside it. That flag means "this name's
|
|
4587
5281
|
* shipped CSS reaches past the element into its descendants, so the universal `target:` must
|
|
@@ -4643,6 +5337,10 @@ function recognise(el, handlers, options = {}, deps = defaultGestureDeps()) {
|
|
|
4643
5337
|
let active = false;
|
|
4644
5338
|
let longPressTimer = null;
|
|
4645
5339
|
let longPressFired = false;
|
|
5340
|
+
let captured = false;
|
|
5341
|
+
let pointerId = null;
|
|
5342
|
+
let lastHandled = null;
|
|
5343
|
+
let documentListening = null;
|
|
4646
5344
|
function sampleOf(event) {
|
|
4647
5345
|
return { x: event.clientX, y: event.clientY, time: deps.now() };
|
|
4648
5346
|
}
|
|
@@ -4656,11 +5354,24 @@ function recognise(el, handlers, options = {}, deps = defaultGestureDeps()) {
|
|
|
4656
5354
|
longPressTimer = null;
|
|
4657
5355
|
}
|
|
4658
5356
|
function onDown(event) {
|
|
5357
|
+
if (origin && pointerId !== null && event.pointerId !== pointerId) {
|
|
5358
|
+
clearLongPress();
|
|
5359
|
+
const last = samples[samples.length - 1];
|
|
5360
|
+
releaseCapture(pointerId);
|
|
5361
|
+
finish({ x: last.x, y: last.y, time: deps.now() });
|
|
5362
|
+
return;
|
|
5363
|
+
}
|
|
5364
|
+
if (options.accept && !options.accept(event)) {
|
|
5365
|
+
origin = null;
|
|
5366
|
+
return;
|
|
5367
|
+
}
|
|
4659
5368
|
origin = sampleOf(event);
|
|
4660
5369
|
samples = [origin];
|
|
4661
5370
|
active = false;
|
|
4662
5371
|
longPressFired = false;
|
|
4663
|
-
|
|
5372
|
+
pointerId = event.pointerId;
|
|
5373
|
+
captured = capturePointer === true && takeCapture(event.pointerId);
|
|
5374
|
+
if (!captured) listenOnDocument();
|
|
4664
5375
|
if (longPressMs > 0) {
|
|
4665
5376
|
longPressTimer = deps.setTimer(() => {
|
|
4666
5377
|
longPressFired = true;
|
|
@@ -4668,8 +5379,43 @@ function recognise(el, handlers, options = {}, deps = defaultGestureDeps()) {
|
|
|
4668
5379
|
}, longPressMs);
|
|
4669
5380
|
}
|
|
4670
5381
|
}
|
|
5382
|
+
function takeCapture(id) {
|
|
5383
|
+
if (typeof el.setPointerCapture !== "function") return false;
|
|
5384
|
+
try {
|
|
5385
|
+
el.setPointerCapture(id);
|
|
5386
|
+
return true;
|
|
5387
|
+
} catch {
|
|
5388
|
+
return false;
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
function captureOnDrag(event) {
|
|
5392
|
+
if (capturePointer !== "drag") return;
|
|
5393
|
+
captured = takeCapture(event.pointerId);
|
|
5394
|
+
if (captured) stopListeningOnDocument();
|
|
5395
|
+
}
|
|
5396
|
+
function listenOnDocument() {
|
|
5397
|
+
const doc = el.ownerDocument;
|
|
5398
|
+
if (!doc || documentListening) return;
|
|
5399
|
+
documentListening = doc;
|
|
5400
|
+
doc.addEventListener("pointermove", onMove, { passive: true });
|
|
5401
|
+
doc.addEventListener("pointerup", onUp, { passive: true });
|
|
5402
|
+
doc.addEventListener("pointercancel", onUp, { passive: true });
|
|
5403
|
+
}
|
|
5404
|
+
function stopListeningOnDocument() {
|
|
5405
|
+
const doc = documentListening;
|
|
5406
|
+
if (!doc) return;
|
|
5407
|
+
documentListening = null;
|
|
5408
|
+
doc.removeEventListener("pointermove", onMove);
|
|
5409
|
+
doc.removeEventListener("pointerup", onUp);
|
|
5410
|
+
doc.removeEventListener("pointercancel", onUp);
|
|
5411
|
+
}
|
|
5412
|
+
function firstHearing(event) {
|
|
5413
|
+
if (event === lastHandled) return false;
|
|
5414
|
+
lastHandled = event;
|
|
5415
|
+
return true;
|
|
5416
|
+
}
|
|
4671
5417
|
function onMove(event) {
|
|
4672
|
-
if (!origin) return;
|
|
5418
|
+
if (!origin || !firstHearing(event)) return;
|
|
4673
5419
|
const sample = sampleOf(event);
|
|
4674
5420
|
samples.push(sample);
|
|
4675
5421
|
if (samples.length > MAX_SAMPLES) samples.shift();
|
|
@@ -4678,17 +5424,17 @@ function recognise(el, handlers, options = {}, deps = defaultGestureDeps()) {
|
|
|
4678
5424
|
if (!active) {
|
|
4679
5425
|
active = true;
|
|
4680
5426
|
clearLongPress();
|
|
5427
|
+
captureOnDrag(event);
|
|
4681
5428
|
handlers.onStart?.(origin);
|
|
4682
5429
|
}
|
|
4683
5430
|
handlers.onMove?.(vector, sample);
|
|
4684
5431
|
}
|
|
4685
|
-
function
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
const vector = vectorNow(sample);
|
|
5432
|
+
function releaseCapture(id) {
|
|
5433
|
+
if (captured) el.releasePointerCapture?.(id);
|
|
5434
|
+
captured = false;
|
|
5435
|
+
pointerId = null;
|
|
5436
|
+
}
|
|
5437
|
+
function reportEnd(vector, sample) {
|
|
4692
5438
|
if (active) {
|
|
4693
5439
|
handlers.onEnd?.(vector, sample);
|
|
4694
5440
|
const direction = swipeDirection(vector, swipeVelocity);
|
|
@@ -4696,21 +5442,41 @@ function recognise(el, handlers, options = {}, deps = defaultGestureDeps()) {
|
|
|
4696
5442
|
} else if (longPressFired) {
|
|
4697
5443
|
handlers.onEnd?.(vector, sample);
|
|
4698
5444
|
}
|
|
5445
|
+
}
|
|
5446
|
+
function onUp(event) {
|
|
5447
|
+
clearLongPress();
|
|
5448
|
+
stopListeningOnDocument();
|
|
5449
|
+
releaseCapture(event.pointerId);
|
|
5450
|
+
if (!origin) return;
|
|
5451
|
+
finish(sampleOf(event));
|
|
5452
|
+
}
|
|
5453
|
+
function finish(sample) {
|
|
5454
|
+
samples.push(sample);
|
|
5455
|
+
reportEnd(vectorNow(sample), sample);
|
|
4699
5456
|
origin = null;
|
|
4700
5457
|
active = false;
|
|
4701
5458
|
longPressFired = false;
|
|
4702
5459
|
samples = [];
|
|
4703
5460
|
}
|
|
5461
|
+
function onLostCapture(event) {
|
|
5462
|
+
if (event.target !== el) return;
|
|
5463
|
+
if (!captured || event.pointerId !== pointerId) return;
|
|
5464
|
+
captured = false;
|
|
5465
|
+
onUp(event);
|
|
5466
|
+
}
|
|
4704
5467
|
el.addEventListener("pointerdown", onDown);
|
|
4705
5468
|
el.addEventListener("pointermove", onMove, { passive: true });
|
|
4706
5469
|
el.addEventListener("pointerup", onUp, { passive: true });
|
|
4707
5470
|
el.addEventListener("pointercancel", onUp, { passive: true });
|
|
5471
|
+
el.addEventListener("lostpointercapture", onLostCapture, { passive: true });
|
|
4708
5472
|
return () => {
|
|
4709
5473
|
clearLongPress();
|
|
5474
|
+
stopListeningOnDocument();
|
|
4710
5475
|
el.removeEventListener("pointerdown", onDown);
|
|
4711
5476
|
el.removeEventListener("pointermove", onMove);
|
|
4712
5477
|
el.removeEventListener("pointerup", onUp);
|
|
4713
5478
|
el.removeEventListener("pointercancel", onUp);
|
|
5479
|
+
el.removeEventListener("lostpointercapture", onLostCapture);
|
|
4714
5480
|
};
|
|
4715
5481
|
}
|
|
4716
5482
|
function defaultGestureDeps() {
|
|
@@ -4848,22 +5614,26 @@ function settle(runners, offset, vector, mode) {
|
|
|
4848
5614
|
runners.x.to(offset.x + vector.vx * carry);
|
|
4849
5615
|
runners.y.to(offset.y + vector.vy * carry);
|
|
4850
5616
|
}
|
|
4851
|
-
function prepareSwipeable(el, params) {
|
|
5617
|
+
function prepareSwipeable(el, params, ctx) {
|
|
5618
|
+
const axis = params.text("axis", "both");
|
|
5619
|
+
if (axis !== "both") ctx.style.set("touch-action", `${axis === "x" ? "pan-y" : "pan-x"} pinch-zoom`);
|
|
4852
5620
|
const stop = recognise(
|
|
4853
5621
|
el,
|
|
4854
5622
|
{
|
|
4855
5623
|
onSwipe(direction) {
|
|
4856
5624
|
el.setAttribute("data-kui-swipe", direction);
|
|
5625
|
+
announceSwipe(ctx.win, el, direction);
|
|
4857
5626
|
}
|
|
4858
5627
|
},
|
|
4859
5628
|
{
|
|
4860
|
-
axis
|
|
5629
|
+
axis,
|
|
4861
5630
|
swipeVelocity: params.num("velocity", 300),
|
|
4862
|
-
// This primitive
|
|
4863
|
-
//
|
|
4864
|
-
//
|
|
4865
|
-
//
|
|
4866
|
-
capturePointer
|
|
5631
|
+
// This primitive moves nothing, so it must not hold the pointer from `pointerdown` — that
|
|
5632
|
+
// retargets the following `click` at this element and kills every interactive child (a
|
|
5633
|
+
// `swipe-x` on a carousel shell killed its own dots and buttons). But a flick that leaves a
|
|
5634
|
+
// small element never delivers `pointerup` to it unless captured, so capture only once the
|
|
5635
|
+
// drag starts. See `capturePointer` in `core/gesture.ts`.
|
|
5636
|
+
capturePointer: "drag"
|
|
4867
5637
|
}
|
|
4868
5638
|
);
|
|
4869
5639
|
return () => {
|
|
@@ -4922,7 +5692,7 @@ function prepareMagnetic(el, params, ctx) {
|
|
|
4922
5692
|
var GESTURE_PRIMITIVES = [
|
|
4923
5693
|
gesturePrimitive(
|
|
4924
5694
|
"draggable",
|
|
4925
|
-
["translate"],
|
|
5695
|
+
["translate", attributeChannel("data-kui-dragging")],
|
|
4926
5696
|
{
|
|
4927
5697
|
...springParams2,
|
|
4928
5698
|
axis: { type: "keyword", default: "both", cssProperty: "--kui-axis", keywords: ["x", "y", "both"] },
|
|
@@ -4953,7 +5723,7 @@ var GESTURE_PRIMITIVES = [
|
|
|
4953
5723
|
),
|
|
4954
5724
|
gesturePrimitive(
|
|
4955
5725
|
"swipeable",
|
|
4956
|
-
["
|
|
5726
|
+
[attributeChannel("data-kui-swipe")],
|
|
4957
5727
|
{
|
|
4958
5728
|
axis: { type: "keyword", default: "both", cssProperty: "--kui-axis", keywords: ["x", "y", "both"] },
|
|
4959
5729
|
velocity: { type: "number", default: "300", cssProperty: "--kui-velocity" }
|
|
@@ -4962,7 +5732,7 @@ var GESTURE_PRIMITIVES = [
|
|
|
4962
5732
|
),
|
|
4963
5733
|
gesturePrimitive(
|
|
4964
5734
|
"pressable",
|
|
4965
|
-
["
|
|
5735
|
+
[attributeChannel("data-kui-pressed")],
|
|
4966
5736
|
{ duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" } },
|
|
4967
5737
|
// `duration` here is the hold threshold, not a span of motion — `long-press 800ms` means
|
|
4968
5738
|
// "count it once the finger has been down for 800ms". It is read (see `preparePressable`), so
|
|
@@ -5013,6 +5783,7 @@ var GESTURE_PRESETS = [
|
|
|
5013
5783
|
},
|
|
5014
5784
|
{ name: "swipe", primitive: "swipeable", phase: "state" },
|
|
5015
5785
|
{ name: "swipe-x", primitive: "swipeable", params: { axis: "x" }, phase: "state" },
|
|
5786
|
+
{ name: "swipe-y", primitive: "swipeable", params: { axis: "y" }, phase: "state" },
|
|
5016
5787
|
{ name: "long-press", primitive: "pressable", phase: "state" },
|
|
5017
5788
|
{ name: "magnetic", primitive: "magnetic", phase: "state" },
|
|
5018
5789
|
{
|
|
@@ -5308,13 +6079,15 @@ var LAYOUT_PRIMITIVES = [
|
|
|
5308
6079
|
),
|
|
5309
6080
|
layoutPrimitive(
|
|
5310
6081
|
"auto-height",
|
|
5311
|
-
|
|
6082
|
+
// The two properties it writes on the panel: `overflow: hidden`, and the `height` it animates.
|
|
6083
|
+
["overflow", "height"],
|
|
5312
6084
|
{ attribute: { type: "text", default: "data-open", cssProperty: "--kui-attribute" } },
|
|
5313
6085
|
deferPrepare(prepareAutoHeight)
|
|
5314
6086
|
),
|
|
5315
6087
|
layoutPrimitive(
|
|
5316
6088
|
"flip-indicator",
|
|
5317
|
-
|
|
6089
|
+
// The bar's landing spot (`translate`) and its `width`, matched to the selected tab.
|
|
6090
|
+
["translate", "width"],
|
|
5318
6091
|
{
|
|
5319
6092
|
follow: { type: "text", default: "", cssProperty: "--kui-follow" },
|
|
5320
6093
|
attribute: { type: "text", default: "aria-selected", cssProperty: "--kui-attribute" }
|
|
@@ -5563,26 +6336,25 @@ function prepareBackToTop(el, params, ctx) {
|
|
|
5563
6336
|
var NAV_JS_PRIMITIVES = [
|
|
5564
6337
|
navPrimitive(
|
|
5565
6338
|
"header-shrink",
|
|
5566
|
-
//
|
|
5567
|
-
//
|
|
5568
|
-
//
|
|
5569
|
-
//
|
|
5570
|
-
//
|
|
5571
|
-
//
|
|
5572
|
-
|
|
5573
|
-
["layout", "shadow"],
|
|
6339
|
+
// The three properties its host rule writes (navigation.css): the two it interpolates from
|
|
6340
|
+
// `--kui-shrink`, `padding-block` and `font-size`, each under its own name, and `box-shadow`
|
|
6341
|
+
// on the channel that already owns every other box-shadow writer (`lift-shadow`,
|
|
6342
|
+
// `border-glow`), so composing `header-shrink` with either of those is a refused conflict
|
|
6343
|
+
// instead of a silent clobber. The two used to sit in the shared `'layout'` bucket, which also
|
|
6344
|
+
// refused `header-shrink, pin-until` — a sticky header that shrinks — over nothing they share.
|
|
6345
|
+
["padding", "font-size", "shadow", attributeChannel("data-kui-shrunk")],
|
|
5574
6346
|
{ offset: { type: "number", default: "120", cssProperty: "--kui-offset" } },
|
|
5575
6347
|
deferPrepare(prepareHeaderShrink)
|
|
5576
6348
|
),
|
|
5577
6349
|
navPrimitive(
|
|
5578
6350
|
"header-hide-on-scroll",
|
|
5579
|
-
[CHANNEL.translate],
|
|
6351
|
+
[CHANNEL.translate, attributeChannel("data-kui-hidden")],
|
|
5580
6352
|
{ offset: { type: "number", default: "8", cssProperty: "--kui-offset" } },
|
|
5581
6353
|
deferPrepare(prepareHeaderHide)
|
|
5582
6354
|
),
|
|
5583
6355
|
navPrimitive(
|
|
5584
6356
|
"back-to-top-fade",
|
|
5585
|
-
[CHANNEL.opacity, CHANNEL.translate],
|
|
6357
|
+
[CHANNEL.opacity, CHANNEL.translate, attributeChannel("data-kui-visible")],
|
|
5586
6358
|
{ offset: { type: "number", default: "400", cssProperty: "--kui-offset" } },
|
|
5587
6359
|
deferPrepare(prepareBackToTop)
|
|
5588
6360
|
)
|
|
@@ -6145,7 +6917,10 @@ function installSnapContainer(axis, ctx) {
|
|
|
6145
6917
|
var SCROLL_PRIMITIVES = [
|
|
6146
6918
|
scrollPrimitive({
|
|
6147
6919
|
id: "pin",
|
|
6148
|
-
|
|
6920
|
+
// `position: sticky` and its `top` (`installSticky`): the host's position is what it holds. The
|
|
6921
|
+
// spacer it can insert is a sibling, not a child, so the host's subtree is not claimed. And the
|
|
6922
|
+
// `data-kui-pinned` flag it publishes while holding.
|
|
6923
|
+
channels: ["position", "progress", attributeChannel("data-kui-pinned")],
|
|
6149
6924
|
parameters: {
|
|
6150
6925
|
...distanceParam2,
|
|
6151
6926
|
...stickyParams
|
|
@@ -6155,7 +6930,13 @@ var SCROLL_PRIMITIVES = [
|
|
|
6155
6930
|
}),
|
|
6156
6931
|
scrollPrimitive({
|
|
6157
6932
|
id: "scroll-progress",
|
|
6158
|
-
|
|
6933
|
+
// `data-kui-step` beside `progress`: with `steps:` this publishes the same index attribute (and
|
|
6934
|
+
// its `--kui-step` twin) a `carousel` does, so the two on one element would overwrite each
|
|
6935
|
+
// other's step every frame. The claim is static, so it holds with `steps:0` too — a list can't
|
|
6936
|
+
// be composed or refused per param value. One claim covers both writes: every primitive that
|
|
6937
|
+
// writes the `--kui-step` index also writes the attribute, so a separate channel for the
|
|
6938
|
+
// property could never change a decision.
|
|
6939
|
+
channels: ["progress", attributeChannel("data-kui-step")],
|
|
6159
6940
|
parameters: {
|
|
6160
6941
|
...distanceParam2,
|
|
6161
6942
|
steps: { type: "number", default: "0", cssProperty: "--kui-steps" },
|
|
@@ -6191,7 +6972,8 @@ var SCROLL_PRIMITIVES = [
|
|
|
6191
6972
|
}),
|
|
6192
6973
|
scrollPrimitive({
|
|
6193
6974
|
id: "media-scrub",
|
|
6194
|
-
|
|
6975
|
+
// `position`: it holds its host the way a pin does (`position: sticky` and its `top`).
|
|
6976
|
+
channels: ["media", "progress", "position"],
|
|
6195
6977
|
parameters: {
|
|
6196
6978
|
...distanceParam2,
|
|
6197
6979
|
// A scrub is a hold, so it needs the same two knobs a pin does. Declaring them here is what
|
|
@@ -6210,7 +6992,7 @@ var SCROLL_PRIMITIVES = [
|
|
|
6210
6992
|
}),
|
|
6211
6993
|
scrollPrimitive({
|
|
6212
6994
|
id: "scroll-spy",
|
|
6213
|
-
channels: ["
|
|
6995
|
+
channels: [attributeChannel("data-kui-active")],
|
|
6214
6996
|
parameters: {
|
|
6215
6997
|
// `distance`: the per-section form only. `offset-top`: the container form only. Each is a
|
|
6216
6998
|
// no-op — warned, not silent — in the other; see `prepareScrollSpySingle` and
|
|
@@ -6236,8 +7018,8 @@ var SCROLL_PRIMITIVES = [
|
|
|
6236
7018
|
scrollPrimitive({
|
|
6237
7019
|
id: "smooth-scroll",
|
|
6238
7020
|
/*
|
|
6239
|
-
* Its own channel, not the `'layout'` it used to share with `pin`, `stacking-cards` and
|
|
6240
|
-
* `scroll-snap
|
|
7021
|
+
* Its own channel, not the `'layout'` bucket it used to share with `pin`, `stacking-cards` and
|
|
7022
|
+
* `scroll-snap` (a bucket since split into the properties each writes). The channel model exists to stop two effects fighting over the same CSS
|
|
6241
7023
|
* property, and this one writes exactly `scroll-behavior` — a property that describes how a
|
|
6242
7024
|
* *user-or-script-initiated* scroll is performed, and that no other primitive touches.
|
|
6243
7025
|
*
|
|
@@ -6258,7 +7040,11 @@ var SCROLL_PRIMITIVES = [
|
|
|
6258
7040
|
}),
|
|
6259
7041
|
scrollPrimitive({
|
|
6260
7042
|
id: "scroll-snap",
|
|
6261
|
-
|
|
7043
|
+
// `scroll-snap-type`, and with `target:` the container it makes of the host (`installSnapContainer`):
|
|
7044
|
+
// `overflow`, and `display: flex` on the x axis — `discrete`'s property. A claim is static, so
|
|
7045
|
+
// it holds without `target:` too. The subtree likewise: with no `target:` the host's children
|
|
7046
|
+
// are the snap items, each given a `scroll-snap-align`.
|
|
7047
|
+
channels: ["scroll-snap-type", "overflow", "discrete", SUBTREE_CHANNEL],
|
|
6262
7048
|
parameters: {
|
|
6263
7049
|
axis: { type: "keyword", default: "y", cssProperty: "--kui-axis", keywords: ["x", "y"] },
|
|
6264
7050
|
strictness: {
|
|
@@ -6600,7 +7386,7 @@ function findBars(el) {
|
|
|
6600
7386
|
return children.filter((child) => child.textContent.trim() === "");
|
|
6601
7387
|
}
|
|
6602
7388
|
function withIconParts(inner) {
|
|
6603
|
-
return (el, params, ctx) => {
|
|
7389
|
+
return inheritTimingContract(inner, (el, params, ctx) => {
|
|
6604
7390
|
if (!el.querySelector(".kui-bar")) {
|
|
6605
7391
|
const bars = findBars(el);
|
|
6606
7392
|
if (bars.length > 0) {
|
|
@@ -6619,7 +7405,7 @@ function withIconParts(inner) {
|
|
|
6619
7405
|
}
|
|
6620
7406
|
}
|
|
6621
7407
|
return inner(el, params, ctx);
|
|
6622
|
-
};
|
|
7408
|
+
});
|
|
6623
7409
|
}
|
|
6624
7410
|
|
|
6625
7411
|
// src/effects/svg/index.ts
|
|
@@ -6658,18 +7444,18 @@ function prepareMorph(el, params, ctx) {
|
|
|
6658
7444
|
};
|
|
6659
7445
|
let current = 0;
|
|
6660
7446
|
const enter = () => drive(1);
|
|
6661
|
-
const
|
|
7447
|
+
const leave2 = () => drive(0);
|
|
6662
7448
|
el.addEventListener("pointerenter", enter, { passive: true });
|
|
6663
7449
|
el.addEventListener("focusin", enter, { passive: true });
|
|
6664
|
-
el.addEventListener("pointerleave",
|
|
6665
|
-
el.addEventListener("focusout",
|
|
7450
|
+
el.addEventListener("pointerleave", leave2, { passive: true });
|
|
7451
|
+
el.addEventListener("focusout", leave2, { passive: true });
|
|
6666
7452
|
return () => {
|
|
6667
7453
|
cancelled = true;
|
|
6668
7454
|
cancelAnimationFrame(frame);
|
|
6669
7455
|
el.removeEventListener("pointerenter", enter);
|
|
6670
7456
|
el.removeEventListener("focusin", enter);
|
|
6671
|
-
el.removeEventListener("pointerleave",
|
|
6672
|
-
el.removeEventListener("focusout",
|
|
7457
|
+
el.removeEventListener("pointerleave", leave2);
|
|
7458
|
+
el.removeEventListener("focusout", leave2);
|
|
6673
7459
|
path2.setAttribute("d", startPath);
|
|
6674
7460
|
};
|
|
6675
7461
|
}
|
|
@@ -6690,7 +7476,16 @@ var LOGO_BUILD_PRIMITIVE = cssPrimitive("logo-assemble", [
|
|
|
6690
7476
|
var ICON_TOGGLE_PRIMITIVE = {
|
|
6691
7477
|
id: "icon-toggle",
|
|
6692
7478
|
renderer: "javascript",
|
|
6693
|
-
|
|
7479
|
+
// The subtree too: with no `.kui-bar` markup the host's own children are the bars
|
|
7480
|
+
// (`icon-parts.ts` stamps them), so a deck or widget beside it would have its children moved.
|
|
7481
|
+
channels: [
|
|
7482
|
+
CHANNEL.translate,
|
|
7483
|
+
CHANNEL.rotate,
|
|
7484
|
+
CHANNEL.scale,
|
|
7485
|
+
CHANNEL.opacity,
|
|
7486
|
+
CHANNEL.clip,
|
|
7487
|
+
SUBTREE_CHANNEL
|
|
7488
|
+
],
|
|
6694
7489
|
parameters: {
|
|
6695
7490
|
duration: { type: "time", default: "260ms", cssProperty: "--kui-duration" },
|
|
6696
7491
|
// The state flip is the start moment: `aria-expanded` goes true and the bars begin to move.
|
|
@@ -6888,8 +7683,10 @@ var CARD_TOGGLE_PRIMITIVE = {
|
|
|
6888
7683
|
// `'discrete'` alongside `rotate`: the unconditional `[data-kui-fx~='flip-card']` rule pins
|
|
6889
7684
|
// `display: grid` so the two faces stack in one cell instead of flowing as normal block
|
|
6890
7685
|
// siblings — unrelated to `catalog/discrete.ts`'s show/hide use of the same physical property,
|
|
6891
|
-
// but `display` is tracked as one channel regardless of the value written into it.
|
|
6892
|
-
|
|
7686
|
+
// but `display` is tracked as one channel regardless of the value written into it. The subtree
|
|
7687
|
+
// because the host's children are the two faces (`flip-parts.ts` marks them, and injects the
|
|
7688
|
+
// toggle button when none is authored).
|
|
7689
|
+
channels: [CHANNEL.rotate, "discrete", SUBTREE_CHANNEL],
|
|
6893
7690
|
parameters: {
|
|
6894
7691
|
duration: { type: "time", default: "700ms", cssProperty: "--kui-duration" },
|
|
6895
7692
|
// The turn has a start moment — the click, or the pointer arriving — so a delay before it is
|
|
@@ -7027,8 +7824,8 @@ function projectRelease(position, velocityPxPerSec, travelPx) {
|
|
|
7027
7824
|
function snapTo(projected) {
|
|
7028
7825
|
return Math.round(projected) + 0;
|
|
7029
7826
|
}
|
|
7030
|
-
function wrapPlace(
|
|
7031
|
-
return total > 0 ? (
|
|
7827
|
+
function wrapPlace(place2, total) {
|
|
7828
|
+
return total > 0 ? (place2 % total + total) % total : 0;
|
|
7032
7829
|
}
|
|
7033
7830
|
var KEY_DELTAS = {
|
|
7034
7831
|
ArrowRight: 1,
|
|
@@ -7040,8 +7837,26 @@ var KEY_DELTAS = {
|
|
|
7040
7837
|
Home: null,
|
|
7041
7838
|
End: null
|
|
7042
7839
|
};
|
|
7043
|
-
function
|
|
7044
|
-
|
|
7840
|
+
function holdSelection(doc) {
|
|
7841
|
+
let ledger;
|
|
7842
|
+
const release2 = () => {
|
|
7843
|
+
ledger?.restore();
|
|
7844
|
+
ledger = void 0;
|
|
7845
|
+
};
|
|
7846
|
+
return {
|
|
7847
|
+
hold() {
|
|
7848
|
+
release2();
|
|
7849
|
+
doc.getSelection()?.removeAllRanges();
|
|
7850
|
+
ledger = createStyleLedger(doc.documentElement);
|
|
7851
|
+
ledger.claim("user-select");
|
|
7852
|
+
ledger.claim("-webkit-user-select");
|
|
7853
|
+
ledger.set("user-select", "none");
|
|
7854
|
+
ledger.set("-webkit-user-select", "none");
|
|
7855
|
+
},
|
|
7856
|
+
release: release2
|
|
7857
|
+
};
|
|
7858
|
+
}
|
|
7859
|
+
function guardClicks(el, enabled) {
|
|
7045
7860
|
let suppressClick = false;
|
|
7046
7861
|
const onClickCapture = (event) => {
|
|
7047
7862
|
if (!suppressClick) return;
|
|
@@ -7049,19 +7864,49 @@ function createRingDrag(request) {
|
|
|
7049
7864
|
event.stopPropagation();
|
|
7050
7865
|
event.preventDefault();
|
|
7051
7866
|
};
|
|
7867
|
+
const onPressCapture = () => {
|
|
7868
|
+
suppressClick = false;
|
|
7869
|
+
};
|
|
7870
|
+
const onNativeDrag = (event) => {
|
|
7871
|
+
event.preventDefault();
|
|
7872
|
+
};
|
|
7873
|
+
el.addEventListener("click", onClickCapture, true);
|
|
7874
|
+
el.addEventListener("pointerdown", onPressCapture, true);
|
|
7875
|
+
if (enabled) el.addEventListener("dragstart", onNativeDrag);
|
|
7876
|
+
return {
|
|
7877
|
+
arm() {
|
|
7878
|
+
suppressClick = true;
|
|
7879
|
+
},
|
|
7880
|
+
release() {
|
|
7881
|
+
el.removeEventListener("click", onClickCapture, true);
|
|
7882
|
+
el.removeEventListener("pointerdown", onPressCapture, true);
|
|
7883
|
+
el.removeEventListener("dragstart", onNativeDrag);
|
|
7884
|
+
}
|
|
7885
|
+
};
|
|
7886
|
+
}
|
|
7887
|
+
function createRingDrag(request) {
|
|
7888
|
+
const { el, ctx, enabled, travelPx, total, positionOf, moveTo, setDragging } = request;
|
|
7889
|
+
const clicks = guardClicks(el, enabled);
|
|
7890
|
+
let dragFrom = 0;
|
|
7891
|
+
let sign = 1;
|
|
7892
|
+
const selection = holdSelection(el.ownerDocument);
|
|
7052
7893
|
const stopRecognising = enabled ? recognise(
|
|
7053
7894
|
el,
|
|
7054
7895
|
{
|
|
7055
7896
|
onStart() {
|
|
7897
|
+
dragFrom = positionOf();
|
|
7898
|
+
sign = request.travelSign();
|
|
7899
|
+
selection.hold();
|
|
7056
7900
|
setDragging(true);
|
|
7057
7901
|
},
|
|
7058
7902
|
onMove(vector) {
|
|
7059
|
-
moveTo(
|
|
7903
|
+
moveTo(dragFrom - sign * vector.dx / travelPx);
|
|
7060
7904
|
},
|
|
7061
7905
|
onEnd(vector) {
|
|
7906
|
+
selection.release();
|
|
7062
7907
|
setDragging(false);
|
|
7063
|
-
|
|
7064
|
-
const settled = snapTo(projectRelease(positionOf(), vector.vx, travelPx));
|
|
7908
|
+
clicks.arm();
|
|
7909
|
+
const settled = snapTo(projectRelease(positionOf(), sign * vector.vx, travelPx));
|
|
7065
7910
|
moveTo(wrapPlace(settled, total()));
|
|
7066
7911
|
}
|
|
7067
7912
|
},
|
|
@@ -7070,7 +7915,15 @@ function createRingDrag(request) {
|
|
|
7070
7915
|
// threshold than `recognise`'s own default of 4px is what buys that — a ring occupies a
|
|
7071
7916
|
// large slab of a phone screen, and a scroll that begins with a two-pixel horizontal wobble
|
|
7072
7917
|
// should not capture the pointer away from the page.
|
|
7073
|
-
|
|
7918
|
+
//
|
|
7919
|
+
// Capture only once the press becomes a drag (`'drag'`), never on `pointerdown`. Held from
|
|
7920
|
+
// the press, capture retargets the `click` that ends a plain tap at this host instead of the
|
|
7921
|
+
// card under the pointer — found by the `lightbox:true` browser tier, where a real click on
|
|
7922
|
+
// a card arrived as a click on the ring and opened nothing. The same retargeting made every
|
|
7923
|
+
// link and button inside a card dead in a real browser while jsdom (which has no capture)
|
|
7924
|
+
// stayed green. The host never moves under the pointer — its cards do — so the press has
|
|
7925
|
+
// nothing to stay captured for until it is a drag, and a drag's click is swallowed above.
|
|
7926
|
+
{ axis: "x", threshold: DRAG_THRESHOLD_PX, capturePointer: "drag" }
|
|
7074
7927
|
) : () => {
|
|
7075
7928
|
};
|
|
7076
7929
|
if (enabled) ctx.style.set("touch-action", "pan-y");
|
|
@@ -7089,216 +7942,213 @@ function createRingDrag(request) {
|
|
|
7089
7942
|
const grantedFocus = el.getAttribute("tabindex") === null;
|
|
7090
7943
|
if (grantedFocus) el.setAttribute("tabindex", "0");
|
|
7091
7944
|
el.addEventListener("keydown", onKeyDown);
|
|
7092
|
-
el.addEventListener("click", onClickCapture, true);
|
|
7093
7945
|
return () => {
|
|
7094
7946
|
stopRecognising();
|
|
7947
|
+
selection.release();
|
|
7095
7948
|
el.removeEventListener("keydown", onKeyDown);
|
|
7096
|
-
|
|
7949
|
+
clicks.release();
|
|
7097
7950
|
if (grantedFocus) el.removeAttribute("tabindex");
|
|
7098
7951
|
};
|
|
7099
7952
|
}
|
|
7100
7953
|
|
|
7101
|
-
// src/effects/carousel/
|
|
7954
|
+
// src/effects/carousel/deck.ts
|
|
7102
7955
|
var DRAGGING_ATTR = "data-kui-ring-dragging";
|
|
7103
|
-
var
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
["opacity", (value) => value !== "" && Number(value) < 1],
|
|
7109
|
-
["filter", (value) => value !== "" && value !== "none"],
|
|
7110
|
-
["backdrop-filter", (value) => value !== "" && value !== "none"]
|
|
7956
|
+
var SPATIAL_DECK_CHANNELS = [
|
|
7957
|
+
SUBTREE_CHANNEL,
|
|
7958
|
+
attributeChannel("data-kui-step"),
|
|
7959
|
+
attributeChannel(DRAGGING_ATTR),
|
|
7960
|
+
attributeChannel("tabindex")
|
|
7111
7961
|
];
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
if (found) {
|
|
7118
|
-
ctx.warn(
|
|
7119
|
-
`carousel: an ancestor <${node.localName}> has ${found.property}: ${found.value}, which flattens transform-style: preserve-3d \u2014 the ring will render as flat overlapping cards. Move the ring out of it, or drop that property on the ancestor.`
|
|
7120
|
-
);
|
|
7121
|
-
return;
|
|
7122
|
-
}
|
|
7123
|
-
node = node.parentElement;
|
|
7124
|
-
}
|
|
7125
|
-
}
|
|
7126
|
-
function flatteningDeclaration(node, ctx) {
|
|
7127
|
-
const style = ctx.win.getComputedStyle?.(node);
|
|
7128
|
-
if (!style) return null;
|
|
7129
|
-
for (const [property2, flattens] of FLATTENING) {
|
|
7130
|
-
const value = style.getPropertyValue(property2);
|
|
7131
|
-
if (flattens(value)) return { property: property2, value };
|
|
7132
|
-
}
|
|
7133
|
-
return null;
|
|
7134
|
-
}
|
|
7135
|
-
function ledgersFor(store, node) {
|
|
7136
|
-
let attributes = store.attributes.get(node);
|
|
7137
|
-
if (!attributes) {
|
|
7138
|
-
attributes = createAttributeLedger(node);
|
|
7139
|
-
store.attributes.set(node, attributes);
|
|
7140
|
-
}
|
|
7141
|
-
let styles = store.styles.get(node);
|
|
7142
|
-
if (!styles) {
|
|
7143
|
-
styles = createStyleLedger(node);
|
|
7144
|
-
store.styles.set(node, styles);
|
|
7145
|
-
}
|
|
7146
|
-
return { attributes, styles };
|
|
7962
|
+
var SPINNING_ATTR = "data-kui-ring-spinning";
|
|
7963
|
+
var FACE_ATTR = "data-kui-ring-face";
|
|
7964
|
+
var WRAP_ATTR = "data-kui-ring-wrap";
|
|
7965
|
+
function commitStyle(win, node) {
|
|
7966
|
+
return win.getComputedStyle?.(node).transform;
|
|
7147
7967
|
}
|
|
7148
|
-
function
|
|
7149
|
-
|
|
7968
|
+
function travelSign(liveX, nextX, previousX) {
|
|
7969
|
+
if (nextX !== null) return nextX < liveX ? -1 : 1;
|
|
7970
|
+
if (previousX !== null) return previousX > liveX ? -1 : 1;
|
|
7971
|
+
return 1;
|
|
7150
7972
|
}
|
|
7151
|
-
function
|
|
7152
|
-
|
|
7153
|
-
return wrapped > 180 ? wrapped - 360 : wrapped;
|
|
7973
|
+
function offsetWrapped(previous, next, count) {
|
|
7974
|
+
return Math.abs(next - previous) > count / 2;
|
|
7154
7975
|
}
|
|
7155
7976
|
function snapPosition(position, total) {
|
|
7156
7977
|
if (total <= 0) return { step: 0, drift: 0 };
|
|
7157
7978
|
const nearest = Math.round(position);
|
|
7158
7979
|
return { step: (nearest % total + total) % total, drift: position - nearest };
|
|
7159
7980
|
}
|
|
7160
|
-
var
|
|
7981
|
+
var DECK_PARAMETERS = {
|
|
7161
7982
|
duration: { type: "time", default: "620ms", cssProperty: "--kui-duration" },
|
|
7162
7983
|
...TRIGGER_DELAY_PARAM,
|
|
7163
7984
|
ease: { type: "easing", default: "cubic-bezier(0.22, 1, 0.36, 1)", cssProperty: "--kui-ease" },
|
|
7985
|
+
/** Whether a pointer can move the deck by hand. */
|
|
7986
|
+
grab: { type: "keyword", default: "true", cssProperty: "--kui-grab", keywords: ["true", "false"] },
|
|
7164
7987
|
/*
|
|
7165
|
-
*
|
|
7166
|
-
*
|
|
7167
|
-
* A designer specifies fifteen degrees; nobody specifies 0.375 of an unstated maximum. A
|
|
7168
|
-
* normalised knob also bakes taste into a coordinate — the maximum *is* an opinion about how much
|
|
7169
|
-
* tilt is too much, expressed as a unit — and it cannot be typed in the spelling the rest of the
|
|
7170
|
-
* attribute grammar uses (`0.05turn` is a legal angle and means something exact). Legibility is
|
|
7171
|
-
* still bounded, but the bound is applied to the angle, in `carousel.css`, where it is one
|
|
7172
|
-
* `clamp()` an author can see in devtools rather than a hidden rescale.
|
|
7988
|
+
* How far the pointer travels, in pixels, to move the deck one place.
|
|
7173
7989
|
*
|
|
7174
|
-
*
|
|
7175
|
-
* is the
|
|
7176
|
-
*
|
|
7177
|
-
* says, and it should not be spelled `-18deg` because of which way a matrix happens to turn.
|
|
7990
|
+
* A pixels-per-step mapping rather than pixels-per-degree, because a step is the unit everything
|
|
7991
|
+
* else here is in — the index, the offset, the snap — and a degree is not: the same drag would
|
|
7992
|
+
* move a six-item ring one place and a sixty-item ring ten, purely because the spacing changed.
|
|
7178
7993
|
*/
|
|
7179
|
-
|
|
7994
|
+
travel: { type: "number", default: "220", cssProperty: "--kui-travel", finite: true, minimum: 1 },
|
|
7995
|
+
/** Which elements are the slides. Unset means this element's own children. */
|
|
7996
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" },
|
|
7997
|
+
/** Optional controls, resolved exactly as `step-progress` resolves its own. */
|
|
7998
|
+
next: { type: "text", default: "", cssProperty: "--kui-next" },
|
|
7999
|
+
prev: { type: "text", default: "", cssProperty: "--kui-prev" },
|
|
8000
|
+
jump: { type: "text", default: "", cssProperty: "--kui-jump" },
|
|
8001
|
+
/** A control that pauses and resumes `spin:`/`autoplay:`. See `PAUSE_PARAM` (`auto-motion.ts`). */
|
|
8002
|
+
pause: PAUSE_PARAM,
|
|
8003
|
+
/** `hover:none`: a resting pointer no longer pauses `spin:`/`autoplay:`. See `HOVER_PARAM`. */
|
|
8004
|
+
hover: HOVER_PARAM,
|
|
7180
8005
|
/*
|
|
7181
|
-
*
|
|
8006
|
+
* Continuous rotation: how long one full cycle of the deck takes. `0s` is off.
|
|
7182
8007
|
*
|
|
7183
|
-
*
|
|
7184
|
-
*
|
|
7185
|
-
*
|
|
8008
|
+
* A period rather than a speed because a period is what a designer can picture ("one turn every
|
|
8009
|
+
* forty seconds") and it is a `time`, which the attribute grammar already reads, clamps and
|
|
8010
|
+
* documents. A full cycle is every slide passing once — one revolution of a full ring — so adding a
|
|
8011
|
+
* card does not change how fast the ring turns. **Negative runs the other way**: `spin:-40s`. A
|
|
8012
|
+
* signed value rather than a `direction:` keyword so one row can say everything about its motion
|
|
8013
|
+
* in one token, which is what three rows at three speeds on one page want.
|
|
7186
8014
|
*/
|
|
7187
|
-
|
|
8015
|
+
spin: { type: "time", default: "0s", cssProperty: "--kui-spin" },
|
|
7188
8016
|
/*
|
|
7189
|
-
*
|
|
7190
|
-
*
|
|
7191
|
-
* Empty default, not a number, and for the same reason `step-progress`'s `steps` is empty:
|
|
7192
|
-
* `readParams` fills every declared default in unconditionally, so an empty one is the only way
|
|
7193
|
-
* to tell "the author said nothing" from "the author said 320px". Unset, `carousel.css` computes
|
|
7194
|
-
* the circumradius of a regular polygon with this many sides and this much side length —
|
|
7195
|
-
* `(width + gap) / (2 * tan(step / 2))` — which is the arrangement where neighbours just touch.
|
|
7196
|
-
* Every consumer solving that by hand was the alternative, and they would all have solved it
|
|
7197
|
-
* differently.
|
|
7198
|
-
*
|
|
7199
|
-
* The width half of that has to come from JavaScript: CSS can compute with a measurement but
|
|
7200
|
-
* cannot take one. See `measureItemWidth`.
|
|
8017
|
+
* Stepped motion: how long the deck rests on each slide. When both are set `spin:` wins, because a
|
|
8018
|
+
* deck cannot be both gliding and resting. See `AUTOPLAY_PARAM` (`auto-motion.ts`).
|
|
7201
8019
|
*/
|
|
7202
|
-
|
|
7203
|
-
/** Breathing room between neighbours, spent by the derived radius above. */
|
|
7204
|
-
gap: { type: "length", default: "24px", cssProperty: "--kui-gap" },
|
|
7205
|
-
/** Camera distance. Shorter is a wider lens: more foreshortening, more drama, more distortion. */
|
|
7206
|
-
perspective: { type: "length", default: "1600px", cssProperty: "--kui-perspective" },
|
|
8020
|
+
autoplay: AUTOPLAY_PARAM,
|
|
7207
8021
|
/*
|
|
7208
|
-
*
|
|
7209
|
-
*
|
|
7210
|
-
*
|
|
7211
|
-
* `
|
|
7212
|
-
* one faces you. It is what you want for a cover-flow, and it is what makes the far side of the
|
|
7213
|
-
* ring read as the far side.
|
|
7214
|
-
*
|
|
7215
|
-
* `camera` counter-rotates each slot's *child* — never the slot itself; the placement transform
|
|
7216
|
-
* and the billboard have to stay on separate boxes or every later change to one silently
|
|
7217
|
-
* multiplies into the other — so text stays readable at every position, which is what a raised
|
|
7218
|
-
* camera needs. Costs one element of markup per slide; see `docs/catalog.md`.
|
|
8022
|
+
* `lightbox:true`: a click on a card opens every card of this deck in the shared lightbox gallery,
|
|
8023
|
+
* at the card clicked. A drag still only moves the deck — its click is swallowed before it can
|
|
8024
|
+
* reach a card (`drag.ts`). The gallery itself lives in the showcase module; see
|
|
8025
|
+
* `core/deck-viewer.ts` for how a deck reaches it without importing it.
|
|
7219
8026
|
*/
|
|
7220
|
-
|
|
7221
|
-
type: "keyword",
|
|
7222
|
-
default: "radial",
|
|
7223
|
-
cssProperty: "--kui-facing",
|
|
7224
|
-
keywords: ["radial", "camera"]
|
|
7225
|
-
},
|
|
7226
|
-
/** Whether a pointer can spin the ring by hand. */
|
|
7227
|
-
grab: { type: "keyword", default: "true", cssProperty: "--kui-grab", keywords: ["true", "false"] },
|
|
7228
|
-
/*
|
|
7229
|
-
* How far the pointer travels, in pixels, to move the ring one place.
|
|
7230
|
-
*
|
|
7231
|
-
* A pixels-per-step mapping rather than pixels-per-degree, because a step is the unit everything
|
|
7232
|
-
* else here is in — the index, the offset, the snap — and a degree is not: the same drag would
|
|
7233
|
-
* move a six-item ring one place and a sixty-item ring ten, purely because the spacing changed.
|
|
7234
|
-
*/
|
|
7235
|
-
travel: { type: "number", default: "220", cssProperty: "--kui-travel", finite: true, minimum: 1 },
|
|
7236
|
-
/** Which elements sit on the ring. Unset means this element's own children. */
|
|
7237
|
-
target: { type: "text", default: "", cssProperty: "--kui-target" },
|
|
7238
|
-
/** Optional controls, resolved exactly as `step-progress` resolves its own. */
|
|
7239
|
-
next: { type: "text", default: "", cssProperty: "--kui-next" },
|
|
7240
|
-
prev: { type: "text", default: "", cssProperty: "--kui-prev" },
|
|
7241
|
-
jump: { type: "text", default: "", cssProperty: "--kui-jump" },
|
|
8027
|
+
lightbox: DECK_LIGHTBOX_PARAM,
|
|
7242
8028
|
scope: SCOPE_PARAM
|
|
7243
8029
|
};
|
|
7244
|
-
function
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
}
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
8030
|
+
function ledgerFor(store, node) {
|
|
8031
|
+
let ledger = store.attributes.get(node);
|
|
8032
|
+
if (!ledger) {
|
|
8033
|
+
ledger = createAttributeLedger(node);
|
|
8034
|
+
store.attributes.set(node, ledger);
|
|
8035
|
+
}
|
|
8036
|
+
return ledger;
|
|
8037
|
+
}
|
|
8038
|
+
function measureItem(styles, slots) {
|
|
8039
|
+
const first = slots[0];
|
|
8040
|
+
if (!first) return;
|
|
8041
|
+
if (first.offsetWidth > 0) styles.set("--kui-item-width", `${first.offsetWidth}px`);
|
|
8042
|
+
if (first.offsetHeight > 0) styles.set("--kui-item-height", `${first.offsetHeight}px`);
|
|
8043
|
+
}
|
|
8044
|
+
function prepareSpatialDeck(el, params, ctx, layout) {
|
|
8045
|
+
const { label } = layout;
|
|
8046
|
+
mirrorTimingToCss(layout.id, ALL_TIMING_TOKENS, params, ctx);
|
|
8047
|
+
const selector = resolveTarget(params.text("target"), ctx, label);
|
|
7255
8048
|
const scope = scopeParam(params, "self");
|
|
7256
8049
|
const resolveSlots = () => selector ? queryScoped(el, ctx, selector, scope) : el.children;
|
|
7257
|
-
const marker = createStepMarker(resolveSlots, (message) => ctx.warn(
|
|
8050
|
+
const marker = createStepMarker(resolveSlots, (message) => ctx.warn(`${label} ${message}`));
|
|
7258
8051
|
const total = () => countSteps(params, resolveSlots);
|
|
7259
8052
|
const hostAttributes = createAttributeLedger(el);
|
|
7260
8053
|
const hostStyles = createStyleLedger(el);
|
|
7261
|
-
|
|
7262
|
-
const slots = { attributes: /* @__PURE__ */ new Map()
|
|
7263
|
-
const
|
|
8054
|
+
for (const [name, value] of Object.entries(layout.hostAttributes ?? {})) hostAttributes.set(name, value);
|
|
8055
|
+
const slots = { attributes: /* @__PURE__ */ new Map() };
|
|
8056
|
+
const attachment = layout.attach?.(hostStyles);
|
|
7264
8057
|
let position = 0;
|
|
7265
|
-
|
|
8058
|
+
let rendered = { step: -1, count: 1, nodes: [] };
|
|
8059
|
+
const lastOffsets = /* @__PURE__ */ new WeakMap();
|
|
8060
|
+
const settleWrapped = (nodes, count) => {
|
|
8061
|
+
for (const node of nodes) {
|
|
8062
|
+
const raw = Number(node.getAttribute("data-kui-step-offset"));
|
|
8063
|
+
const offset = layout.placeOf ? layout.placeOf(raw, count) : raw;
|
|
8064
|
+
const previous = lastOffsets.get(node);
|
|
8065
|
+
lastOffsets.set(node, offset);
|
|
8066
|
+
if (previous === void 0 || !offsetWrapped(previous, offset, count)) continue;
|
|
8067
|
+
const flag = createAttributeLedger(node);
|
|
8068
|
+
flag.set(WRAP_ATTR, "true");
|
|
8069
|
+
commitStyle(ctx.win, node);
|
|
8070
|
+
flag.restore();
|
|
8071
|
+
}
|
|
8072
|
+
};
|
|
8073
|
+
const markFaces = (nodes, count, drift2) => {
|
|
8074
|
+
for (const node of nodes) {
|
|
8075
|
+
const offset = Number(node.getAttribute("data-kui-step-offset"));
|
|
8076
|
+
const face = layout.faceOf(offset - drift2, offset, count);
|
|
8077
|
+
if (node.getAttribute(FACE_ATTR) !== face) ledgerFor(slots, node).set(FACE_ATTR, face);
|
|
8078
|
+
}
|
|
8079
|
+
};
|
|
8080
|
+
const publishPosition = (step, drift2) => {
|
|
8081
|
+
hostStyles.set("--kui-step-position", (step + drift2).toFixed(4));
|
|
8082
|
+
};
|
|
7266
8083
|
const render = () => {
|
|
7267
8084
|
const count = total();
|
|
7268
8085
|
const { step, drift: drift2 } = snapPosition(position, count);
|
|
7269
8086
|
hostAttributes.set("data-kui-step", String(step));
|
|
7270
8087
|
hostStyles.set("--kui-step", String(step));
|
|
7271
|
-
hostStyles.set("--kui-
|
|
8088
|
+
hostStyles.set("--kui-item-count", String(count));
|
|
8089
|
+
publishPosition(step, drift2);
|
|
7272
8090
|
marker.mark(step);
|
|
7273
|
-
const
|
|
7274
|
-
if (
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
8091
|
+
const nodes = [...resolveSlots()];
|
|
8092
|
+
if (layout.measure) measureItem(hostStyles, nodes);
|
|
8093
|
+
markFaces(nodes, count, drift2);
|
|
8094
|
+
settleWrapped(nodes, count);
|
|
8095
|
+
attachment?.render(nodes, count);
|
|
8096
|
+
rendered = { step, count, nodes };
|
|
8097
|
+
};
|
|
8098
|
+
const renderFrame = () => {
|
|
8099
|
+
const { step, drift: drift2 } = snapPosition(position, rendered.count);
|
|
8100
|
+
if (step !== rendered.step) {
|
|
8101
|
+
render();
|
|
8102
|
+
return;
|
|
7280
8103
|
}
|
|
8104
|
+
publishPosition(step, drift2);
|
|
8105
|
+
markFaces(rendered.nodes, rendered.count, drift2);
|
|
7281
8106
|
};
|
|
8107
|
+
const { motion, pause, release: releaseMotion } = createDeckMotion({
|
|
8108
|
+
el,
|
|
8109
|
+
ctx,
|
|
8110
|
+
params,
|
|
8111
|
+
label,
|
|
8112
|
+
settleMs: effectDurationMs(params, 620),
|
|
8113
|
+
step(direction) {
|
|
8114
|
+
const count = total();
|
|
8115
|
+
const { step } = snapPosition(position, count);
|
|
8116
|
+
position = direction > 0 ? nextStep(step, count) : prevStep(step, count);
|
|
8117
|
+
render();
|
|
8118
|
+
},
|
|
8119
|
+
spin: {
|
|
8120
|
+
advance(cycles) {
|
|
8121
|
+
position = wrapPlace(position + cycles * rendered.count, rendered.count);
|
|
8122
|
+
renderFrame();
|
|
8123
|
+
},
|
|
8124
|
+
setSpinning: (spinning) => hostAttributes.set(SPINNING_ATTR, String(spinning))
|
|
8125
|
+
}
|
|
8126
|
+
});
|
|
7282
8127
|
const goTo = (next) => {
|
|
8128
|
+
motion.interrupt();
|
|
7283
8129
|
position = next;
|
|
7284
8130
|
render();
|
|
7285
8131
|
};
|
|
7286
|
-
const
|
|
7287
|
-
const
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
8132
|
+
const request = { el, ctx, params, label };
|
|
8133
|
+
const groups = [
|
|
8134
|
+
bindControl(request, "next", () => goTo(nextStep(snapPosition(position, total()).step, total()))),
|
|
8135
|
+
bindControl(request, "prev", () => goTo(prevStep(snapPosition(position, total()).step, total()))),
|
|
8136
|
+
bindControl(request, "jump", (_node, at) => {
|
|
8137
|
+
if (at >= 0) goTo(at);
|
|
8138
|
+
}),
|
|
8139
|
+
pause
|
|
8140
|
+
].filter((group) => group !== null);
|
|
8141
|
+
const releaseControls = delegateControls({ el, ctx, groups });
|
|
8142
|
+
const screenTravel = () => {
|
|
8143
|
+
const centres = /* @__PURE__ */ new Map();
|
|
8144
|
+
for (const node of rendered.nodes) {
|
|
8145
|
+
const offset = Number(node.getAttribute("data-kui-step-offset"));
|
|
8146
|
+
if (Math.abs(offset) > 1) continue;
|
|
8147
|
+
const box = node.getBoundingClientRect();
|
|
8148
|
+
centres.set(offset, box.left + box.width / 2);
|
|
7292
8149
|
}
|
|
7293
|
-
|
|
7294
|
-
return true;
|
|
8150
|
+
return travelSign(centres.get(0) ?? 0, centres.get(1) ?? null, centres.get(-1) ?? null);
|
|
7295
8151
|
};
|
|
7296
|
-
bindControl("next", () => goTo(nextStep(snapPosition(position, total()).step, total())));
|
|
7297
|
-
bindControl("prev", () => goTo(prevStep(snapPosition(position, total()).step, total())));
|
|
7298
|
-
bindControl("jump", (_node, at) => {
|
|
7299
|
-
if (at >= 0) goTo(at);
|
|
7300
|
-
});
|
|
7301
|
-
const releaseControls = delegateControls({ el, ctx, scope, groups });
|
|
7302
8152
|
const releaseDrag = createRingDrag({
|
|
7303
8153
|
el,
|
|
7304
8154
|
ctx,
|
|
@@ -7306,22 +8156,405 @@ function prepareSpatialRing(el, params, ctx) {
|
|
|
7306
8156
|
travelPx: params.num("travel", 220),
|
|
7307
8157
|
total,
|
|
7308
8158
|
positionOf: () => position,
|
|
8159
|
+
travelSign: screenTravel,
|
|
7309
8160
|
moveTo: goTo,
|
|
7310
|
-
setDragging: (dragging) =>
|
|
8161
|
+
setDragging: (dragging) => {
|
|
8162
|
+
hostAttributes.set(DRAGGING_ATTR, String(dragging));
|
|
8163
|
+
motion.hold(dragging);
|
|
8164
|
+
}
|
|
7311
8165
|
});
|
|
8166
|
+
const releaseViewer = deckLightbox(
|
|
8167
|
+
params.is("lightbox"),
|
|
8168
|
+
{ host: el, cards: resolveSlots, doc: ctx.doc, reducedMotion: ctx.reducedMotion },
|
|
8169
|
+
(message) => ctx.warn(`${label} ${message}`)
|
|
8170
|
+
);
|
|
7312
8171
|
render();
|
|
7313
8172
|
return () => {
|
|
8173
|
+
releaseViewer();
|
|
8174
|
+
releaseMotion();
|
|
7314
8175
|
releaseDrag();
|
|
7315
8176
|
releaseControls();
|
|
7316
8177
|
marker.restore();
|
|
7317
8178
|
for (const ledger of slots.attributes.values()) ledger.restore();
|
|
7318
|
-
for (const ledger of slots.styles.values()) ledger.restore();
|
|
7319
8179
|
slots.attributes.clear();
|
|
7320
|
-
|
|
8180
|
+
attachment?.release();
|
|
7321
8181
|
hostAttributes.restore();
|
|
7322
8182
|
hostStyles.restore();
|
|
7323
8183
|
};
|
|
7324
8184
|
}
|
|
8185
|
+
|
|
8186
|
+
// src/effects/carousel/fit.ts
|
|
8187
|
+
var RING_FIT_PROPERTY = "--kui-ring-fit";
|
|
8188
|
+
var SAMPLE_DEG = 0.25;
|
|
8189
|
+
var SEARCH_CEILING_PX = 1e5;
|
|
8190
|
+
var SEARCH_STEPS = 34;
|
|
8191
|
+
var RAD = Math.PI / 180;
|
|
8192
|
+
function reachableAngles(arcDeg, count) {
|
|
8193
|
+
if (Math.abs(arcDeg) >= 360) return { fromDeg: -180, toDeg: 180 };
|
|
8194
|
+
const places = Math.max(1, count);
|
|
8195
|
+
const spacing = arcDeg / places;
|
|
8196
|
+
const high = Math.floor(places / 2);
|
|
8197
|
+
const low = high - places + 1;
|
|
8198
|
+
const ends = [(low - 0.5) * spacing, (high + 0.5) * spacing];
|
|
8199
|
+
return { fromDeg: Math.min(...ends), toDeg: Math.max(...ends) };
|
|
8200
|
+
}
|
|
8201
|
+
function place(point, { sinA, cosA, sinT, cosT }) {
|
|
8202
|
+
const [x, y, z] = point;
|
|
8203
|
+
const x1 = x * cosA + z * sinA;
|
|
8204
|
+
const z1 = -x * sinA + z * cosA;
|
|
8205
|
+
return [x1, y * cosT - z1 * sinT, y * sinT + z1 * cosT];
|
|
8206
|
+
}
|
|
8207
|
+
function cardFits(g, r, rotation) {
|
|
8208
|
+
const z0 = (g.inside ? -1 : 1) * r;
|
|
8209
|
+
const centre = place([0, 0, z0], rotation);
|
|
8210
|
+
const hw = g.width / 2;
|
|
8211
|
+
const hh = g.height / 2;
|
|
8212
|
+
for (const [cx, cy] of [[-hw, -hh], [hw, -hh], [-hw, hh], [hw, hh]]) {
|
|
8213
|
+
const [x, , z] = g.facing === "camera" ? [centre[0] + cx, centre[1] + cy, centre[2]] : place([cx, cy, z0], rotation);
|
|
8214
|
+
if (z >= g.perspective - 1) return false;
|
|
8215
|
+
const scale = Number.isFinite(g.perspective) ? g.perspective / (g.perspective - z) : 1;
|
|
8216
|
+
const screen = (g.originOffset + x) * scale;
|
|
8217
|
+
if (screen < g.minX || screen > g.maxX) return false;
|
|
8218
|
+
}
|
|
8219
|
+
return true;
|
|
8220
|
+
}
|
|
8221
|
+
function fitsAt(g, r, angles) {
|
|
8222
|
+
const sinT = Math.sin(g.tiltDeg * RAD);
|
|
8223
|
+
const cosT = Math.cos(g.tiltDeg * RAD);
|
|
8224
|
+
return angles.every((angle) => cardFits(g, r, { sinA: Math.sin(angle * RAD), cosA: Math.cos(angle * RAD), sinT, cosT }));
|
|
8225
|
+
}
|
|
8226
|
+
function sampleAngles(g) {
|
|
8227
|
+
const from = g.inside ? Math.max(g.fromDeg, -90) : g.fromDeg;
|
|
8228
|
+
const to = g.inside ? Math.min(g.toDeg, 90) : g.toDeg;
|
|
8229
|
+
const steps = Math.max(1, Math.ceil((to - from) / SAMPLE_DEG));
|
|
8230
|
+
return Array.from({ length: steps + 1 }, (_, i) => from + (to - from) * i / steps);
|
|
8231
|
+
}
|
|
8232
|
+
function solveRingFit(g) {
|
|
8233
|
+
const angles = sampleAngles(g);
|
|
8234
|
+
if (!fitsAt(g, 0, angles)) return 0;
|
|
8235
|
+
if (fitsAt(g, SEARCH_CEILING_PX, angles)) return Infinity;
|
|
8236
|
+
let low = 0;
|
|
8237
|
+
let high = SEARCH_CEILING_PX;
|
|
8238
|
+
for (let i = 0; i < SEARCH_STEPS; i++) {
|
|
8239
|
+
const mid = (low + high) / 2;
|
|
8240
|
+
if (fitsAt(g, mid, angles)) low = mid;
|
|
8241
|
+
else high = mid;
|
|
8242
|
+
}
|
|
8243
|
+
return Math.floor(low * 100) / 100;
|
|
8244
|
+
}
|
|
8245
|
+
function fitBounds(el, win) {
|
|
8246
|
+
const doc = el.ownerDocument;
|
|
8247
|
+
for (let node = el.parentElement; node && node !== doc.body && node !== doc.documentElement; node = node.parentElement) {
|
|
8248
|
+
const overflow = win.getComputedStyle(node).overflowX;
|
|
8249
|
+
if (overflow === "clip" || overflow === "hidden") return null;
|
|
8250
|
+
if (overflow === "auto" || overflow === "scroll") {
|
|
8251
|
+
const left2 = node.getBoundingClientRect().left + node.clientLeft;
|
|
8252
|
+
return { left: left2, right: left2 + node.clientWidth };
|
|
8253
|
+
}
|
|
8254
|
+
}
|
|
8255
|
+
const left = -win.scrollX;
|
|
8256
|
+
return { left, right: left + doc.documentElement.clientWidth };
|
|
8257
|
+
}
|
|
8258
|
+
function pixels(value, fallback) {
|
|
8259
|
+
const parsed = Number.parseFloat(value);
|
|
8260
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
8261
|
+
}
|
|
8262
|
+
function createRingFit(options) {
|
|
8263
|
+
const { el, ctx, styles } = options;
|
|
8264
|
+
if (typeof ctx.win.getComputedStyle !== "function" || typeof ctx.win.addEventListener !== "function") {
|
|
8265
|
+
return { render() {
|
|
8266
|
+
}, release() {
|
|
8267
|
+
} };
|
|
8268
|
+
}
|
|
8269
|
+
const tiltDeg = Math.min(80, Math.max(-80, -options.tiltParamDeg));
|
|
8270
|
+
let card;
|
|
8271
|
+
let count = 1;
|
|
8272
|
+
const solve = () => {
|
|
8273
|
+
const bounds = fitBounds(el, ctx.win);
|
|
8274
|
+
if (!bounds || !card || card.offsetWidth === 0) {
|
|
8275
|
+
styles.set(RING_FIT_PROPERTY, "");
|
|
8276
|
+
return;
|
|
8277
|
+
}
|
|
8278
|
+
const style = ctx.win.getComputedStyle(el);
|
|
8279
|
+
const host = el.getBoundingClientRect();
|
|
8280
|
+
const originX = host.left + pixels(style.perspectiveOrigin, host.width / 2);
|
|
8281
|
+
const fit = solveRingFit({
|
|
8282
|
+
width: card.offsetWidth,
|
|
8283
|
+
height: card.offsetHeight,
|
|
8284
|
+
perspective: pixels(style.perspective, Infinity),
|
|
8285
|
+
tiltDeg,
|
|
8286
|
+
// The stylesheet's own selector for the concave placement, read rather than threaded through
|
|
8287
|
+
// as a parameter: the name is what `carousel.css` branches on, so it is what this must too.
|
|
8288
|
+
inside: el.matches("[data-kui-fx~='carousel-3d-inside']"),
|
|
8289
|
+
// `facing:camera` counter-rotates the slot's element *children* (`carousel.css`); a slot with
|
|
8290
|
+
// none — bare text — is drawn at its radial angle, and is fitted as the radial card it is.
|
|
8291
|
+
facing: card.firstElementChild ? options.facing : "radial",
|
|
8292
|
+
...reachableAngles(options.arcDeg, count),
|
|
8293
|
+
originOffset: host.left + host.width / 2 - originX,
|
|
8294
|
+
minX: bounds.left - originX,
|
|
8295
|
+
maxX: bounds.right - originX
|
|
8296
|
+
});
|
|
8297
|
+
styles.set(RING_FIT_PROPERTY, Number.isFinite(fit) ? `${fit}px` : "");
|
|
8298
|
+
};
|
|
8299
|
+
const frame = frameScheduler(ctx.win, solve);
|
|
8300
|
+
const watch = watchElementSize(ctx.win, frame.request);
|
|
8301
|
+
watch.observe(el);
|
|
8302
|
+
watch.observe(el.ownerDocument.documentElement);
|
|
8303
|
+
return {
|
|
8304
|
+
render(nodes, total) {
|
|
8305
|
+
const first = nodes[0];
|
|
8306
|
+
if (first === card && total === count) return;
|
|
8307
|
+
card = first;
|
|
8308
|
+
count = total;
|
|
8309
|
+
solve();
|
|
8310
|
+
},
|
|
8311
|
+
release() {
|
|
8312
|
+
watch.disconnect();
|
|
8313
|
+
frame.cancel();
|
|
8314
|
+
}
|
|
8315
|
+
};
|
|
8316
|
+
}
|
|
8317
|
+
|
|
8318
|
+
// src/effects/carousel/stack.ts
|
|
8319
|
+
function stackFace(offset, drift2, count, depth) {
|
|
8320
|
+
const ahead = stackPlace(offset, count) - drift2;
|
|
8321
|
+
return ahead > -0.5 && ahead < depth + 1 ? "front" : "back";
|
|
8322
|
+
}
|
|
8323
|
+
function stackPlace(offset, count) {
|
|
8324
|
+
return wrapPlace(offset + 1, count) - 1;
|
|
8325
|
+
}
|
|
8326
|
+
var STACK_PARAMETERS = {
|
|
8327
|
+
...DECK_PARAMETERS,
|
|
8328
|
+
/*
|
|
8329
|
+
* How far each card behind the front one steps sideways. Positive is to the right, negative to
|
|
8330
|
+
* the left. A length rather than a percentage because the host reserves `depth × shift` of room
|
|
8331
|
+
* on that side (cards moved by a transform take no layout space) and a padding percentage would
|
|
8332
|
+
* resolve against a different box than a translate percentage does.
|
|
8333
|
+
*/
|
|
8334
|
+
shift: { type: "length", default: "48px", cssProperty: "--kui-stack-shift" },
|
|
8335
|
+
/** How far each card behind steps upward. Negative recedes downward instead. */
|
|
8336
|
+
rise: { type: "length", default: "36px", cssProperty: "--kui-stack-rise" },
|
|
8337
|
+
/** Scale lost per place behind the front: `0.08` makes the third card back 76% size. */
|
|
8338
|
+
shrink: { type: "number", default: "0.08", cssProperty: "--kui-stack-shrink", finite: true, minimum: 0, maximum: 0.3 },
|
|
8339
|
+
/** Blur added per place behind the front. The front card is always sharp. */
|
|
8340
|
+
blur: { type: "length", default: "2px", cssProperty: "--kui-stack-blur" },
|
|
8341
|
+
/** Opacity lost per place behind the front. */
|
|
8342
|
+
fade: { type: "number", default: "0.2", cssProperty: "--kui-stack-fade", finite: true, minimum: 0, maximum: 1 },
|
|
8343
|
+
/*
|
|
8344
|
+
* How many cards show behind the front one. The rest wait, hidden, at the back — `visibility`,
|
|
8345
|
+
* not only `opacity`, so a card nobody can see is also not a tab stop or a click target.
|
|
8346
|
+
*/
|
|
8347
|
+
depth: { type: "number", default: "3", cssProperty: "--kui-stack-depth", integer: true, minimum: 1, maximum: 8 }
|
|
8348
|
+
};
|
|
8349
|
+
function prepareSpatialStack(el, params, ctx) {
|
|
8350
|
+
const depth = params.num("depth", 3);
|
|
8351
|
+
return prepareSpatialDeck(el, params, ctx, {
|
|
8352
|
+
id: "spatial-stack",
|
|
8353
|
+
label: "carousel-stack",
|
|
8354
|
+
faceOf: (place2, offset, count) => stackFace(offset, offset - place2, count, depth),
|
|
8355
|
+
placeOf: stackPlace,
|
|
8356
|
+
measure: false
|
|
8357
|
+
});
|
|
8358
|
+
}
|
|
8359
|
+
var SPATIAL_STACK_PRIMITIVE = {
|
|
8360
|
+
id: "spatial-stack",
|
|
8361
|
+
renderer: "javascript",
|
|
8362
|
+
// What `deck.ts` writes on the host: the cards (the subtree), the step index, the drag.
|
|
8363
|
+
channels: [CHANNEL.skew, "discrete", CHANNEL.opacity, CHANNEL.filter, ...SPATIAL_DECK_CHANNELS],
|
|
8364
|
+
parameters: STACK_PARAMETERS,
|
|
8365
|
+
supportedTimelines: ["time"],
|
|
8366
|
+
supportedActivations: ["load", "enter", "click", "manual"],
|
|
8367
|
+
defaultActivation: "load",
|
|
8368
|
+
perfClass: "compositor",
|
|
8369
|
+
reducedMotion: "shorten",
|
|
8370
|
+
prepare: deferPrepare(prepareSpatialStack)
|
|
8371
|
+
};
|
|
8372
|
+
var STACK_PRESETS = [
|
|
8373
|
+
{
|
|
8374
|
+
name: "carousel-stack",
|
|
8375
|
+
primitive: "spatial-stack",
|
|
8376
|
+
requiresOwnSubtree: true,
|
|
8377
|
+
phase: "idle"
|
|
8378
|
+
}
|
|
8379
|
+
];
|
|
8380
|
+
|
|
8381
|
+
// src/effects/carousel/index.ts
|
|
8382
|
+
var QUARTER_TURN_DEG = 90;
|
|
8383
|
+
var FLATTENING = [
|
|
8384
|
+
["overflow", (value) => value !== "" && value !== "visible"],
|
|
8385
|
+
["clip-path", (value) => value !== "" && value !== "none"],
|
|
8386
|
+
["opacity", (value) => value !== "" && Number(value) < 1],
|
|
8387
|
+
["filter", (value) => value !== "" && value !== "none"],
|
|
8388
|
+
["backdrop-filter", (value) => value !== "" && value !== "none"],
|
|
8389
|
+
["isolation", (value) => value === "isolate"],
|
|
8390
|
+
["mix-blend-mode", (value) => value !== "" && value !== "normal"],
|
|
8391
|
+
["mask-image", (value) => value !== "" && value !== "none"]
|
|
8392
|
+
];
|
|
8393
|
+
function warnFlattenedHost(el, ctx) {
|
|
8394
|
+
const found = flatteningDeclaration(el, ctx);
|
|
8395
|
+
if (!found) return;
|
|
8396
|
+
ctx.warn(
|
|
8397
|
+
`carousel: the ring's own <${el.localName}> has ${found.property}: ${found.value}, which flattens transform-style: preserve-3d \u2014 the ring will render as flat overlapping cards. Move that property to a wrapper around the ring: an ancestor does not flatten it.`
|
|
8398
|
+
);
|
|
8399
|
+
}
|
|
8400
|
+
function flatteningDeclaration(node, ctx) {
|
|
8401
|
+
const style = ctx.win.getComputedStyle?.(node);
|
|
8402
|
+
if (!style) return null;
|
|
8403
|
+
for (const [property2, flattens] of FLATTENING) {
|
|
8404
|
+
const value = style.getPropertyValue(property2);
|
|
8405
|
+
if (flattens(value)) return { property: property2, value };
|
|
8406
|
+
}
|
|
8407
|
+
return null;
|
|
8408
|
+
}
|
|
8409
|
+
function faceAt(angleDeg) {
|
|
8410
|
+
return Math.abs(normaliseDegrees(angleDeg)) >= QUARTER_TURN_DEG ? "back" : "front";
|
|
8411
|
+
}
|
|
8412
|
+
function normaliseDegrees(angleDeg) {
|
|
8413
|
+
const wrapped = (angleDeg % 360 + 360) % 360;
|
|
8414
|
+
return wrapped > 180 ? wrapped - 360 : wrapped;
|
|
8415
|
+
}
|
|
8416
|
+
var RING_PARAMETERS = {
|
|
8417
|
+
...DECK_PARAMETERS,
|
|
8418
|
+
/*
|
|
8419
|
+
* A real CSS `<angle>`, not a normalised scalar.
|
|
8420
|
+
*
|
|
8421
|
+
* A designer specifies fifteen degrees; nobody specifies 0.375 of an unstated maximum. A
|
|
8422
|
+
* normalised knob also bakes taste into a coordinate — the maximum *is* an opinion about how much
|
|
8423
|
+
* tilt is too much, expressed as a unit — and it cannot be typed in the spelling the rest of the
|
|
8424
|
+
* attribute grammar uses (`0.05turn` is a legal angle and means something exact). Legibility is
|
|
8425
|
+
* still bounded, but the bound is applied to the angle, in `carousel.css`, where it is one
|
|
8426
|
+
* `clamp()` an author can see in devtools rather than a hidden rescale.
|
|
8427
|
+
*
|
|
8428
|
+
* Sign convention: **positive tilts the camera up and over the ring**, looking down at it. That
|
|
8429
|
+
* is the opposite sign from the `rotateX()` it compiles to — see `carousel.css` for the
|
|
8430
|
+
* derivation — and the inversion is deliberate: "18 degrees above" is the sentence a designer
|
|
8431
|
+
* says, and it should not be spelled `-18deg` because of which way a matrix happens to turn.
|
|
8432
|
+
*/
|
|
8433
|
+
tilt: { type: "angle", default: "0deg", cssProperty: "--kui-tilt" },
|
|
8434
|
+
/*
|
|
8435
|
+
* How far the ring spreads. `360deg` is a full ring; anything less is a slice.
|
|
8436
|
+
*
|
|
8437
|
+
* This is the parameter the concave name exists around. From the centre of a *full* ring most of
|
|
8438
|
+
* it is behind your head, so a 360-degree spread there is not a design choice but a way of hiding
|
|
8439
|
+
* two thirds of the content — the inside name defaults to a slice for that reason.
|
|
8440
|
+
*/
|
|
8441
|
+
arc: { type: "angle", default: "360deg", cssProperty: "--kui-arc" },
|
|
8442
|
+
/*
|
|
8443
|
+
* The ring's radius, or unset for one derived from the content.
|
|
8444
|
+
*
|
|
8445
|
+
* Empty default, not a number, and for the same reason `step-progress`'s `steps` is empty:
|
|
8446
|
+
* `readParams` fills every declared default in unconditionally, so an empty one is the only way
|
|
8447
|
+
* to tell "the author said nothing" from "the author said 320px". Unset, `carousel.css` computes
|
|
8448
|
+
* the circumradius of a regular polygon with this many sides and this much side length —
|
|
8449
|
+
* `(width + gap) / (2 * tan(step / 2))` — which is the arrangement where neighbours just touch.
|
|
8450
|
+
* Every consumer solving that by hand was the alternative, and they would all have solved it
|
|
8451
|
+
* differently.
|
|
8452
|
+
*
|
|
8453
|
+
* The width half of that has to come from JavaScript: CSS can compute with a measurement but
|
|
8454
|
+
* cannot take one. See `measureItem` in `deck.ts`.
|
|
8455
|
+
*
|
|
8456
|
+
* Authored or derived, a depth ring's radius is a ceiling: a ring that would push its page
|
|
8457
|
+
* sideways is pulled in exactly as far as it has to be (`fit.ts`, `--kui-ring-fit`), and one that
|
|
8458
|
+
* already fits — or sits inside an ancestor that clips it — keeps this radius to the pixel.
|
|
8459
|
+
*/
|
|
8460
|
+
radius: { type: "length", default: "", cssProperty: "--kui-radius" },
|
|
8461
|
+
/** Breathing room between neighbours, spent by the derived radius above. */
|
|
8462
|
+
gap: { type: "length", default: "24px", cssProperty: "--kui-gap" },
|
|
8463
|
+
/** Camera distance. Shorter is a wider lens: more foreshortening, more drama, more distortion. */
|
|
8464
|
+
perspective: { type: "length", default: "1600px", cssProperty: "--kui-perspective" },
|
|
8465
|
+
/*
|
|
8466
|
+
* Whether a slot keeps its own outward orientation, or turns to keep facing the viewer.
|
|
8467
|
+
*
|
|
8468
|
+
* `radial` is the ring's natural state and needs nothing: a slot placed by
|
|
8469
|
+
* `rotateY(θ) translateZ(R)` already faces outward along its own radius, which is why the live
|
|
8470
|
+
* one faces you. It is what you want for a cover-flow, and it is what makes the far side of the
|
|
8471
|
+
* ring read as the far side.
|
|
8472
|
+
*
|
|
8473
|
+
* `camera` counter-rotates each slot's *child* — never the slot itself; the placement transform
|
|
8474
|
+
* and the billboard have to stay on separate boxes or every later change to one silently
|
|
8475
|
+
* multiplies into the other — so text stays readable at every position, which is what a raised
|
|
8476
|
+
* camera needs. Costs one element of markup per slide; see `docs/catalog.md`.
|
|
8477
|
+
*/
|
|
8478
|
+
facing: {
|
|
8479
|
+
type: "keyword",
|
|
8480
|
+
default: "radial",
|
|
8481
|
+
cssProperty: "--kui-facing",
|
|
8482
|
+
keywords: ["radial", "camera"]
|
|
8483
|
+
},
|
|
8484
|
+
/*
|
|
8485
|
+
* Which plane the ring lies in: `depth` (the default — a carousel you look *at*, turning about a
|
|
8486
|
+
* vertical pole) or `screen` (a clock face, turning about the axis that points at you).
|
|
8487
|
+
*
|
|
8488
|
+
* ## Why a parameter of this primitive, and not a new one
|
|
8489
|
+
*
|
|
8490
|
+
* A flat ring of cards around a centred word is the same object as the 3D ring seen from its
|
|
8491
|
+
* pole: N slots, evenly spaced by `arc / count`, placed from `--kui-offset` and the shared drift,
|
|
8492
|
+
* advanced by the same drag, keys, controls and `spin:`. Every number this primitive publishes
|
|
8493
|
+
* means the same thing in both planes; only the three transform functions that turn a place into
|
|
8494
|
+
* a position differ, and those live in `carousel.css` anyway. A second primitive would have
|
|
8495
|
+
* duplicated the whole index to change one stylesheet rule and one face test.
|
|
8496
|
+
*
|
|
8497
|
+
* It is a parameter rather than only a name (unlike convex/concave, which differ in every
|
|
8498
|
+
* default and so earned two names) because the runtime has to know it: a flat slot is never
|
|
8499
|
+
* turned away from the viewer, so the quarter-turn face test that stops the back of a 3D ring
|
|
8500
|
+
* swallowing clicks would, on a clock face, disable every card in the bottom half. The name that
|
|
8501
|
+
* ships it, `carousel-orbit`, sets it; the stylesheet gates on the published attribute rather
|
|
8502
|
+
* than the name, so `carousel-3d plane:screen` is equally honoured — same rule as `facing:`.
|
|
8503
|
+
*
|
|
8504
|
+
* `facing:` keeps its meaning here. `radial` turns each card with the ring, top edge outward like
|
|
8505
|
+
* the numerals on a watch bezel; `camera` keeps every card upright. In the screen plane upright
|
|
8506
|
+
* needs no child counter-rotation at all — the slot is *translated* to its place on the circle
|
|
8507
|
+
* instead of rotated there — so it costs no extra markup, unlike the 3D billboard.
|
|
8508
|
+
*/
|
|
8509
|
+
plane: {
|
|
8510
|
+
type: "keyword",
|
|
8511
|
+
default: "depth",
|
|
8512
|
+
cssProperty: "--kui-ring-plane",
|
|
8513
|
+
keywords: ["depth", "screen"]
|
|
8514
|
+
}
|
|
8515
|
+
};
|
|
8516
|
+
function prepareSpatialRing(el, params, ctx) {
|
|
8517
|
+
const plane = params.text("plane", "depth");
|
|
8518
|
+
if (plane === "depth") warnFlattenedHost(el, ctx);
|
|
8519
|
+
const arcDeg = degreesOf(params.text("arc", "360deg"), 360);
|
|
8520
|
+
const faceOf = (place2, _offset, count) => plane === "screen" ? "front" : faceAt(place2 * (arcDeg / count));
|
|
8521
|
+
return prepareSpatialDeck(el, params, ctx, {
|
|
8522
|
+
id: "spatial-ring",
|
|
8523
|
+
label: "carousel",
|
|
8524
|
+
faceOf,
|
|
8525
|
+
/*
|
|
8526
|
+
* A derived radius needs a card width; a flat ring also needs a card's size to reserve its own
|
|
8527
|
+
* circle (the host is sized to it, since transformed slots take no layout space), so it measures
|
|
8528
|
+
* even when the author named a radius.
|
|
8529
|
+
*/
|
|
8530
|
+
measure: params.text("radius") === "" || plane === "screen",
|
|
8531
|
+
hostAttributes: {
|
|
8532
|
+
/*
|
|
8533
|
+
* `facing:` and `plane:` reach CSS as attributes, not only as the custom properties their
|
|
8534
|
+
* specs declare. A stylesheet cannot select on the *value* of a custom property — `@container
|
|
8535
|
+
* style(--kui-facing: camera)` can, and is not supported widely enough to hang a documented
|
|
8536
|
+
* parameter on — so the one thing a selector needs is published in the one form a selector
|
|
8537
|
+
* can read. Same pairing, for the same reason, as the number/attribute pair `step-marking.ts`
|
|
8538
|
+
* publishes for the ring place.
|
|
8539
|
+
*/
|
|
8540
|
+
"data-kui-ring-facing": params.text("facing", "radial"),
|
|
8541
|
+
"data-kui-ring-plane": plane
|
|
8542
|
+
},
|
|
8543
|
+
/*
|
|
8544
|
+
* The fit to the page (`fit.ts`). Depth plane only: a flat ring already reserves and clamps to
|
|
8545
|
+
* its own square (`carousel.css`, `[data-kui-ring-plane='screen']`), and its cards never leave
|
|
8546
|
+
* the plane that square is measured in.
|
|
8547
|
+
*/
|
|
8548
|
+
attach: plane === "depth" ? (styles) => createRingFit({
|
|
8549
|
+
el,
|
|
8550
|
+
ctx,
|
|
8551
|
+
styles,
|
|
8552
|
+
arcDeg,
|
|
8553
|
+
tiltParamDeg: degreesOf(params.text("tilt", "0deg"), 0),
|
|
8554
|
+
facing: params.text("facing", "radial") === "camera" ? "camera" : "radial"
|
|
8555
|
+
}) : void 0
|
|
8556
|
+
});
|
|
8557
|
+
}
|
|
7325
8558
|
function degreesOf(value, fallbackDeg) {
|
|
7326
8559
|
const match = /^(-?[\d.]+)(deg|rad|turn|grad)?$/i.exec(value.trim());
|
|
7327
8560
|
if (!match) return fallbackDeg;
|
|
@@ -7341,7 +8574,15 @@ function degreesOf(value, fallbackDeg) {
|
|
|
7341
8574
|
var SPATIAL_RING_PRIMITIVE = {
|
|
7342
8575
|
id: "spatial-ring",
|
|
7343
8576
|
renderer: "javascript",
|
|
7344
|
-
|
|
8577
|
+
// What `deck.ts` writes on the host (the cards, the step index, the drag), then the two
|
|
8578
|
+
// attributes only the ring's own layout publishes.
|
|
8579
|
+
channels: [
|
|
8580
|
+
CHANNEL.skew,
|
|
8581
|
+
"discrete",
|
|
8582
|
+
...SPATIAL_DECK_CHANNELS,
|
|
8583
|
+
attributeChannel("data-kui-ring-facing"),
|
|
8584
|
+
attributeChannel("data-kui-ring-plane")
|
|
8585
|
+
],
|
|
7345
8586
|
parameters: RING_PARAMETERS,
|
|
7346
8587
|
supportedTimelines: ["time"],
|
|
7347
8588
|
// `load`, not `enter`: a carousel that only wires its arrows once scrolled into view is broken,
|
|
@@ -7352,7 +8593,7 @@ var SPATIAL_RING_PRIMITIVE = {
|
|
|
7352
8593
|
reducedMotion: "shorten",
|
|
7353
8594
|
prepare: deferPrepare(prepareSpatialRing)
|
|
7354
8595
|
};
|
|
7355
|
-
var CAROUSEL_PRIMITIVES = [SPATIAL_RING_PRIMITIVE];
|
|
8596
|
+
var CAROUSEL_PRIMITIVES = [SPATIAL_RING_PRIMITIVE, SPATIAL_STACK_PRIMITIVE];
|
|
7356
8597
|
var CAROUSEL_PRESETS = [
|
|
7357
8598
|
{
|
|
7358
8599
|
name: "carousel-3d",
|
|
@@ -7389,10 +8630,31 @@ var CAROUSEL_PRESETS = [
|
|
|
7389
8630
|
params: { arc: "120deg", facing: "camera", perspective: "900px" },
|
|
7390
8631
|
requiresOwnSubtree: true,
|
|
7391
8632
|
phase: "idle"
|
|
8633
|
+
},
|
|
8634
|
+
/*
|
|
8635
|
+
* The flat ring: cards on a clock face around whatever sits in the middle of the host (a word, a
|
|
8636
|
+
* logo — anything the `target:` does not name stays centred and off the ring). `plane:screen` is
|
|
8637
|
+
* the whole difference; see the parameter's note for why that is a parameter of this primitive and
|
|
8638
|
+
* not a new one.
|
|
8639
|
+
*
|
|
8640
|
+
* Named for what it does rather than which way it goes — `orbit` is the motion, and it has no
|
|
8641
|
+
* direction to name (the directional-naming rule, "named by travel", is about entrances that
|
|
8642
|
+
* *have* one; a ring's direction is the sign of `spin:`). Not `carousel-3d-*`, because it is not
|
|
8643
|
+
* three-dimensional, and a name that promised depth would send people looking for a `tilt:`.
|
|
8644
|
+
*
|
|
8645
|
+
* `facing:camera` by default: upright cards read as a set of things; radially turned ones read as
|
|
8646
|
+
* a dial, which is the less common ask and one keyword away.
|
|
8647
|
+
*/
|
|
8648
|
+
{
|
|
8649
|
+
name: "carousel-orbit",
|
|
8650
|
+
primitive: "spatial-ring",
|
|
8651
|
+
params: { plane: "screen", facing: "camera" },
|
|
8652
|
+
requiresOwnSubtree: true,
|
|
8653
|
+
phase: "idle"
|
|
7392
8654
|
}
|
|
7393
8655
|
];
|
|
7394
8656
|
function registerCarousel(registry) {
|
|
7395
|
-
return registry.registerPrimitives(CAROUSEL_PRIMITIVES).registerPresets(CAROUSEL_PRESETS);
|
|
8657
|
+
return registry.registerPrimitives(CAROUSEL_PRIMITIVES).registerPresets(CAROUSEL_PRESETS).registerPresets(STACK_PRESETS);
|
|
7396
8658
|
}
|
|
7397
8659
|
|
|
7398
8660
|
// src/effects/tween/properties.ts
|
|
@@ -7948,7 +9210,10 @@ var deviceFrameParams = {
|
|
|
7948
9210
|
keywords: [...DEVICE_FRAME_KINDS]
|
|
7949
9211
|
},
|
|
7950
9212
|
color: { type: "color", default: "#111", cssProperty: "--kui-device-color" },
|
|
7951
|
-
radius: { type: "length", default: RADIUS_UNSET, cssProperty: "--kui-device-radius" }
|
|
9213
|
+
radius: { type: "length", default: RADIUS_UNSET, cssProperty: "--kui-device-radius" },
|
|
9214
|
+
// Its own knob rather than a reading of `radius:`: the frame's corner and the screen's are two
|
|
9215
|
+
// shapes, and the derived screen value already follows `radius:` when this is left unwritten.
|
|
9216
|
+
"screen-radius": { type: "length", default: RADIUS_UNSET, cssProperty: "--kui-device-screen-radius" }
|
|
7952
9217
|
};
|
|
7953
9218
|
function prepareDeviceFrame(el, params) {
|
|
7954
9219
|
const state = createAttributeLedger(el);
|
|
@@ -7961,8 +9226,9 @@ var DEVICE_FRAME_PRIMITIVE = widgetPrimitive(
|
|
|
7961
9226
|
// Exclusive ownership of the frame chrome. Its static box-model declarations are outside
|
|
7962
9227
|
// the channel-property audit, as they are for the existing catalog. The chrome also reaches
|
|
7963
9228
|
// into the direct media child (`object-fit: cover` etc.); that is a `requiresOwnSubtree`
|
|
7964
|
-
// question, as with the other showcase widgets.
|
|
7965
|
-
|
|
9229
|
+
// question, as with the other showcase widgets. The frame kind it stamps on the host is an
|
|
9230
|
+
// attribute it writes, so it claims that too.
|
|
9231
|
+
channels: ["frame", attributeChannel("data-kui-device")],
|
|
7966
9232
|
parameters: deviceFrameParams,
|
|
7967
9233
|
// `'layout'`: the bezel's `border`+`padding` are box-model properties added once, at
|
|
7968
9234
|
// activation, which is a real (if one-time) layout — not the "transform/opacity only" budget
|
|
@@ -7997,10 +9263,12 @@ function lock(state) {
|
|
|
7997
9263
|
state.lock = ledger;
|
|
7998
9264
|
}
|
|
7999
9265
|
function clearContent(state) {
|
|
8000
|
-
state.current
|
|
9266
|
+
const closing = state.current;
|
|
9267
|
+
closing?.onClose?.();
|
|
8001
9268
|
state.current = void 0;
|
|
8002
9269
|
state.currentOwner = void 0;
|
|
8003
9270
|
state.slot?.replaceChildren();
|
|
9271
|
+
if (closing?.opener) announceViewer(state.doc, closing.opener, false);
|
|
8004
9272
|
}
|
|
8005
9273
|
function onClose(state) {
|
|
8006
9274
|
state.dialog?.classList.remove("is-open");
|
|
@@ -8019,11 +9287,9 @@ function dismiss(state) {
|
|
|
8019
9287
|
}, state.current.reducedMotion ? 0 : state.current.duration);
|
|
8020
9288
|
}
|
|
8021
9289
|
function clickedOutside(state, target) {
|
|
8022
|
-
const
|
|
8023
|
-
if (
|
|
8024
|
-
|
|
8025
|
-
}
|
|
8026
|
-
return !content.contains(target) && !target.closest("button");
|
|
9290
|
+
const { node, inside } = state.current;
|
|
9291
|
+
if (inside) return !inside(target);
|
|
9292
|
+
return !node.contains(target) && !target.closest("button");
|
|
8027
9293
|
}
|
|
8028
9294
|
function forceReflow(element) {
|
|
8029
9295
|
return element.offsetWidth;
|
|
@@ -8073,6 +9339,7 @@ function open(state, content, owner) {
|
|
|
8073
9339
|
dialog.scrollTop = 0;
|
|
8074
9340
|
forceReflow(dialog);
|
|
8075
9341
|
dialog.classList.add("is-open");
|
|
9342
|
+
if (content.opener) announceViewer(state.doc, content.opener, true);
|
|
8076
9343
|
}
|
|
8077
9344
|
function closeNow(state) {
|
|
8078
9345
|
if (state.timer !== void 0) clearTimeout(state.timer);
|
|
@@ -8152,25 +9419,31 @@ function resolveMediaSource(href) {
|
|
|
8152
9419
|
}
|
|
8153
9420
|
|
|
8154
9421
|
// src/showcase/lightbox.ts
|
|
9422
|
+
var DEFAULT_DURATION_MS = 280;
|
|
9423
|
+
var DEFAULT_SCALE = "0.965";
|
|
9424
|
+
var DEFAULT_EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
8155
9425
|
var paramsSchema = {
|
|
8156
|
-
media: { type: "keyword", default: "image", cssProperty: "--kui-lightbox-media", keywords: ["image", "video"] },
|
|
9426
|
+
media: { type: "keyword", default: "image", cssProperty: "--kui-lightbox-media", keywords: ["image", "video", "mixed"] },
|
|
8157
9427
|
target: { type: "text", default: "", cssProperty: "--kui-target" },
|
|
8158
9428
|
scope: SCOPE_PARAM,
|
|
8159
|
-
scale: { type: "number", default:
|
|
8160
|
-
duration: { type: "time", default:
|
|
8161
|
-
ease: { type: "easing", default:
|
|
9429
|
+
scale: { type: "number", default: DEFAULT_SCALE, cssProperty: "--kui-from-scale", finite: true, minimum: 0 },
|
|
9430
|
+
duration: { type: "time", default: `${DEFAULT_DURATION_MS}ms`, cssProperty: "--kui-lightbox-duration" },
|
|
9431
|
+
ease: { type: "easing", default: DEFAULT_EASE, cssProperty: "--kui-lightbox-ease" },
|
|
8162
9432
|
loop: { type: "keyword", default: "true", cssProperty: "--kui-lightbox-loop", keywords: ["true", "false"] },
|
|
8163
9433
|
aspect: { type: "keyword", default: "", cssProperty: "--kui-lightbox-aspect", keywords: ["wide", "tall", "square"] },
|
|
8164
9434
|
caption: { type: "keyword", default: "figcaption", cssProperty: "--kui-lightbox-caption", keywords: ["figcaption", "alt", "title", "none"] }
|
|
8165
9435
|
};
|
|
8166
9436
|
function defaultTargets(el, media) {
|
|
8167
|
-
if (el.matches("a[href]") || media
|
|
9437
|
+
if (el.matches("a[href]") || media !== "video" && el.matches("img")) return [el];
|
|
8168
9438
|
return [...el.querySelectorAll(media === "video" ? ":scope a[href]" : ":scope a[href], :scope img")];
|
|
8169
9439
|
}
|
|
9440
|
+
function effectName(media) {
|
|
9441
|
+
return media === "video" ? "video-lightbox" : "lightbox";
|
|
9442
|
+
}
|
|
8170
9443
|
function targets(el, params, ctx, media) {
|
|
8171
9444
|
const authored = params.text("target");
|
|
8172
9445
|
if (!authored) return defaultTargets(el, media);
|
|
8173
|
-
const selector = resolveTarget(authored, ctx, media
|
|
9446
|
+
const selector = resolveTarget(authored, ctx, effectName(media));
|
|
8174
9447
|
return selector ? queryScoped(el, ctx, selector, scopeParam(params, "self")) : [];
|
|
8175
9448
|
}
|
|
8176
9449
|
function imageTrigger(target) {
|
|
@@ -8181,52 +9454,137 @@ function imageIn(trigger) {
|
|
|
8181
9454
|
}
|
|
8182
9455
|
function captionFor(trigger, image, source) {
|
|
8183
9456
|
if (source === "none") return "";
|
|
8184
|
-
|
|
8185
|
-
if (source === "
|
|
8186
|
-
|
|
9457
|
+
const alt = image?.alt ?? "";
|
|
9458
|
+
if (source === "alt") return alt;
|
|
9459
|
+
if (source === "title") return titleOf(trigger, image);
|
|
9460
|
+
return trigger.closest("figure")?.querySelector("figcaption")?.textContent?.trim() || alt;
|
|
9461
|
+
}
|
|
9462
|
+
function titleOf(trigger, image) {
|
|
9463
|
+
return (trigger.getAttribute("title") || image?.title || "").trim();
|
|
8187
9464
|
}
|
|
8188
9465
|
function imageItem(trigger, captionSource) {
|
|
8189
9466
|
const image = imageIn(trigger);
|
|
8190
9467
|
if (!image) return null;
|
|
8191
9468
|
const link = trigger instanceof HTMLAnchorElement ? trigger : null;
|
|
8192
9469
|
return {
|
|
9470
|
+
kind: "image",
|
|
8193
9471
|
trigger,
|
|
8194
9472
|
image,
|
|
8195
9473
|
src: link?.href ?? (image.currentSrc || image.src),
|
|
8196
9474
|
caption: captionFor(trigger, image, captionSource)
|
|
8197
9475
|
};
|
|
8198
9476
|
}
|
|
8199
|
-
function
|
|
9477
|
+
function videoTitle(link, poster) {
|
|
9478
|
+
return link.title || poster?.getAttribute("alt") || link.getAttribute("aria-label")?.trim() || link.textContent?.trim() || "Video";
|
|
9479
|
+
}
|
|
9480
|
+
function videoItem(link, captionSource) {
|
|
9481
|
+
const source = resolveMediaSource(link.getAttribute("href") ?? "");
|
|
9482
|
+
if (!source) return null;
|
|
9483
|
+
const poster = link.querySelector("img");
|
|
9484
|
+
return { kind: "video", trigger: link, source, title: videoTitle(link, poster), caption: captionFor(link, poster, captionSource) };
|
|
9485
|
+
}
|
|
9486
|
+
function itemFor(trigger, media, captionSource) {
|
|
9487
|
+
if (media !== "image" && trigger instanceof HTMLAnchorElement) {
|
|
9488
|
+
const video = videoItem(trigger, captionSource);
|
|
9489
|
+
if (video || media === "video") return video;
|
|
9490
|
+
}
|
|
9491
|
+
return imageItem(trigger, captionSource);
|
|
9492
|
+
}
|
|
9493
|
+
function triggersFor(found, media) {
|
|
8200
9494
|
const seen = /* @__PURE__ */ new Set();
|
|
8201
|
-
const
|
|
8202
|
-
for (const target of
|
|
8203
|
-
const trigger = imageTrigger(target);
|
|
9495
|
+
const triggers = [];
|
|
9496
|
+
for (const target of found) {
|
|
9497
|
+
const trigger = media === "video" ? target : imageTrigger(target);
|
|
8204
9498
|
if (seen.has(trigger)) continue;
|
|
8205
|
-
|
|
8206
|
-
if (!item) continue;
|
|
9499
|
+
if (!usable(trigger, media)) continue;
|
|
8207
9500
|
seen.add(trigger);
|
|
8208
|
-
|
|
9501
|
+
triggers.push(trigger);
|
|
8209
9502
|
}
|
|
8210
|
-
return
|
|
9503
|
+
return triggers;
|
|
9504
|
+
}
|
|
9505
|
+
function usable(trigger, media) {
|
|
9506
|
+
const link = trigger instanceof HTMLAnchorElement;
|
|
9507
|
+
if (media === "video") return link;
|
|
9508
|
+
const image = imageIn(trigger) !== null;
|
|
9509
|
+
return media === "image" ? image : link || image;
|
|
8211
9510
|
}
|
|
8212
9511
|
function modalOptions(params, ctx) {
|
|
8213
9512
|
return {
|
|
8214
|
-
duration: params.timing.durationMs ?? params.ms("duration",
|
|
8215
|
-
scale: params.text("scale",
|
|
8216
|
-
ease: params.timing.easing ?? params.text("ease",
|
|
9513
|
+
duration: params.timing.durationMs ?? params.ms("duration", DEFAULT_DURATION_MS),
|
|
9514
|
+
scale: params.text("scale", DEFAULT_SCALE),
|
|
9515
|
+
ease: params.timing.easing ?? params.text("ease", DEFAULT_EASE),
|
|
8217
9516
|
reducedMotion: ctx.reducedMotion
|
|
8218
9517
|
};
|
|
8219
9518
|
}
|
|
8220
|
-
function showImage(item, view
|
|
9519
|
+
function showImage(item, view) {
|
|
8221
9520
|
view.src = item.src;
|
|
8222
9521
|
view.alt = item.image.alt;
|
|
8223
|
-
caption.textContent = item.caption;
|
|
8224
|
-
caption.hidden = item.caption === "";
|
|
8225
9522
|
const ratio = item.image.naturalWidth ? item.image.naturalHeight / item.image.naturalWidth : 1;
|
|
8226
9523
|
view.className = ratio > 1.5 ? "is-tall" : "is-fit";
|
|
8227
9524
|
}
|
|
9525
|
+
function videoFrame(item, aspect, doc) {
|
|
9526
|
+
const frame = doc.createElement("div");
|
|
9527
|
+
frame.className = `kui-lightbox-frame kui-lightbox-frame--${aspect || item.source.aspect}`;
|
|
9528
|
+
if (item.source.kind === "file") {
|
|
9529
|
+
const video = doc.createElement("video");
|
|
9530
|
+
video.src = item.source.embedUrl;
|
|
9531
|
+
video.controls = true;
|
|
9532
|
+
video.autoplay = true;
|
|
9533
|
+
video.playsInline = true;
|
|
9534
|
+
video.setAttribute("aria-label", item.title);
|
|
9535
|
+
frame.append(video);
|
|
9536
|
+
} else {
|
|
9537
|
+
const iframe = doc.createElement("iframe");
|
|
9538
|
+
iframe.src = item.source.embedUrl;
|
|
9539
|
+
iframe.title = item.title;
|
|
9540
|
+
iframe.allow = "autoplay; encrypted-media; picture-in-picture; fullscreen";
|
|
9541
|
+
iframe.allowFullscreen = true;
|
|
9542
|
+
frame.append(iframe);
|
|
9543
|
+
}
|
|
9544
|
+
return frame;
|
|
9545
|
+
}
|
|
9546
|
+
function pauseVideo(video) {
|
|
9547
|
+
if (video.readyState > 0) video.pause();
|
|
9548
|
+
}
|
|
9549
|
+
function stopMedia(frame) {
|
|
9550
|
+
const video = frame.querySelector("video");
|
|
9551
|
+
if (video) {
|
|
9552
|
+
pauseVideo(video);
|
|
9553
|
+
video.removeAttribute("src");
|
|
9554
|
+
}
|
|
9555
|
+
frame.replaceChildren();
|
|
9556
|
+
}
|
|
9557
|
+
function leave(state, node) {
|
|
9558
|
+
if (node === state.view) return;
|
|
9559
|
+
const video = node.querySelector("video");
|
|
9560
|
+
if (video) pauseVideo(video);
|
|
9561
|
+
else node.replaceChildren();
|
|
9562
|
+
}
|
|
9563
|
+
function land(state, item) {
|
|
9564
|
+
if (item.kind === "image") {
|
|
9565
|
+
showImage(item, state.view);
|
|
9566
|
+
return state.view;
|
|
9567
|
+
}
|
|
9568
|
+
const cached = state.players.get(item);
|
|
9569
|
+
if (cached) {
|
|
9570
|
+
void cached.querySelector("video").play().catch(() => {
|
|
9571
|
+
});
|
|
9572
|
+
return cached;
|
|
9573
|
+
}
|
|
9574
|
+
const frame = videoFrame(item, state.aspect, state.figure.ownerDocument);
|
|
9575
|
+
if (item.source.kind === "file") state.players.set(item, frame);
|
|
9576
|
+
return frame;
|
|
9577
|
+
}
|
|
8228
9578
|
function renderGallery(state, focus) {
|
|
8229
|
-
|
|
9579
|
+
const item = state.items[state.index];
|
|
9580
|
+
if (state.shown?.item !== item) {
|
|
9581
|
+
if (state.shown) leave(state, state.shown.node);
|
|
9582
|
+
const node = land(state, item);
|
|
9583
|
+
state.shown = { item, node };
|
|
9584
|
+
state.caption.textContent = item.caption;
|
|
9585
|
+
state.caption.hidden = item.caption === "";
|
|
9586
|
+
state.figure.replaceChildren(node, state.caption);
|
|
9587
|
+
}
|
|
8230
9588
|
state.counter.textContent = `${state.index + 1} of ${state.items.length}`;
|
|
8231
9589
|
state.previous.disabled = !state.loop && state.index === 0;
|
|
8232
9590
|
state.next.disabled = !state.loop && state.index === state.items.length - 1;
|
|
@@ -8244,6 +9602,7 @@ function clickGallery(state, delta, button2) {
|
|
|
8244
9602
|
}
|
|
8245
9603
|
function galleryKey(state, event) {
|
|
8246
9604
|
if (state.items.length < 2) return;
|
|
9605
|
+
if (event.target.closest("video")) return;
|
|
8247
9606
|
if (event.key === "ArrowLeft") moveGallery(state, -1, true);
|
|
8248
9607
|
else if (event.key === "ArrowRight") moveGallery(state, 1, true);
|
|
8249
9608
|
else if (event.key === "Home") {
|
|
@@ -8255,8 +9614,40 @@ function galleryKey(state, event) {
|
|
|
8255
9614
|
} else return;
|
|
8256
9615
|
event.preventDefault();
|
|
8257
9616
|
}
|
|
8258
|
-
function
|
|
8259
|
-
const
|
|
9617
|
+
function swipeGallery(state, box) {
|
|
9618
|
+
const viewport = box.ownerDocument.defaultView?.visualViewport ?? null;
|
|
9619
|
+
const zoomed = () => (viewport?.scale ?? 1) > 1.01;
|
|
9620
|
+
const sync = () => {
|
|
9621
|
+
box.classList.toggle("is-swipeable", !zoomed());
|
|
9622
|
+
};
|
|
9623
|
+
sync();
|
|
9624
|
+
viewport?.addEventListener("resize", sync);
|
|
9625
|
+
const stop = recognise(box, {
|
|
9626
|
+
onSwipe(direction) {
|
|
9627
|
+
if (direction === "left" || direction === "right") moveGallery(state, swipeStep(direction), false);
|
|
9628
|
+
}
|
|
9629
|
+
}, {
|
|
9630
|
+
// A press on a button still clicks it: capture is taken only once the finger has moved.
|
|
9631
|
+
capturePointer: "drag",
|
|
9632
|
+
accept: (event) => event.pointerType !== "mouse" && !zoomed() && event.target.closest("video") === null
|
|
9633
|
+
});
|
|
9634
|
+
return () => {
|
|
9635
|
+
stop();
|
|
9636
|
+
viewport?.removeEventListener("resize", sync);
|
|
9637
|
+
};
|
|
9638
|
+
}
|
|
9639
|
+
function closeViewer(state) {
|
|
9640
|
+
state.view.removeAttribute("src");
|
|
9641
|
+
for (const frame of state.players.values()) stopMedia(frame);
|
|
9642
|
+
if (state.shown && state.shown.node !== state.view) stopMedia(state.shown.node);
|
|
9643
|
+
}
|
|
9644
|
+
function vocabulary(items) {
|
|
9645
|
+
const kinds = new Set(items.map((item) => item.kind));
|
|
9646
|
+
if (kinds.size > 1) return { noun: "item", label: "Media viewer" };
|
|
9647
|
+
return kinds.has("video") ? { noun: "video", label: "Video viewer" } : { noun: "image", label: "Image viewer" };
|
|
9648
|
+
}
|
|
9649
|
+
function galleryContent(items, initial, options) {
|
|
9650
|
+
const doc = items[initial].trigger.ownerDocument;
|
|
8260
9651
|
const box = doc.createElement("div");
|
|
8261
9652
|
box.className = "kui-lightbox-gallery";
|
|
8262
9653
|
const figure = doc.createElement("figure");
|
|
@@ -8264,7 +9655,6 @@ function galleryContent(items, initial, loop2, options) {
|
|
|
8264
9655
|
const view = doc.createElement("img");
|
|
8265
9656
|
view.decoding = "async";
|
|
8266
9657
|
const caption = doc.createElement("figcaption");
|
|
8267
|
-
figure.append(view, caption);
|
|
8268
9658
|
box.append(figure);
|
|
8269
9659
|
const counter = doc.createElement("span");
|
|
8270
9660
|
counter.className = "kui-lightbox-counter";
|
|
@@ -8274,149 +9664,244 @@ function galleryContent(items, initial, loop2, options) {
|
|
|
8274
9664
|
previous.type = next.type = "button";
|
|
8275
9665
|
previous.textContent = "\u2039";
|
|
8276
9666
|
next.textContent = "\u203A";
|
|
8277
|
-
|
|
8278
|
-
|
|
9667
|
+
const { noun, label } = vocabulary(items);
|
|
9668
|
+
previous.setAttribute("aria-label", `Previous ${noun}`);
|
|
9669
|
+
next.setAttribute("aria-label", `Next ${noun}`);
|
|
8279
9670
|
previous.className = "kui-lightbox-prev";
|
|
8280
9671
|
next.className = "kui-lightbox-next";
|
|
8281
|
-
const state = {
|
|
9672
|
+
const state = {
|
|
9673
|
+
items,
|
|
9674
|
+
index: initial,
|
|
9675
|
+
loop: options.loop,
|
|
9676
|
+
aspect: options.aspect,
|
|
9677
|
+
figure,
|
|
9678
|
+
view,
|
|
9679
|
+
caption,
|
|
9680
|
+
counter,
|
|
9681
|
+
previous,
|
|
9682
|
+
next,
|
|
9683
|
+
players: /* @__PURE__ */ new Map()
|
|
9684
|
+
};
|
|
8282
9685
|
if (items.length > 1) {
|
|
8283
9686
|
box.append(previous, next, counter);
|
|
8284
9687
|
previous.addEventListener("click", () => clickGallery(state, -1, previous));
|
|
8285
9688
|
next.addEventListener("click", () => clickGallery(state, 1, next));
|
|
8286
9689
|
}
|
|
8287
9690
|
renderGallery(state, false);
|
|
9691
|
+
const unswipe = items.length > 1 ? swipeGallery(state, box) : void 0;
|
|
9692
|
+
const { duration, scale, ease, reducedMotion } = options;
|
|
8288
9693
|
return {
|
|
8289
|
-
|
|
9694
|
+
duration,
|
|
9695
|
+
scale,
|
|
9696
|
+
ease,
|
|
9697
|
+
reducedMotion,
|
|
8290
9698
|
node: box,
|
|
8291
|
-
label
|
|
8292
|
-
|
|
9699
|
+
label,
|
|
9700
|
+
// The media and the controls keep the viewer open; the empty figure around them closes it.
|
|
9701
|
+
inside: (target) => box.contains(target) && target.closest("img, video, iframe, button") !== null,
|
|
9702
|
+
onClose: () => {
|
|
9703
|
+
unswipe?.();
|
|
9704
|
+
closeViewer(state);
|
|
9705
|
+
},
|
|
8293
9706
|
onKey: (event) => galleryKey(state, event)
|
|
8294
9707
|
};
|
|
8295
9708
|
}
|
|
8296
|
-
function stopMedia(frame) {
|
|
8297
|
-
const video = frame.querySelector("video");
|
|
8298
|
-
if (video) {
|
|
8299
|
-
if (video.readyState > 0) video.pause();
|
|
8300
|
-
video.removeAttribute("src");
|
|
8301
|
-
}
|
|
8302
|
-
frame.replaceChildren();
|
|
8303
|
-
}
|
|
8304
|
-
function videoContent(link, source, options, aspect) {
|
|
8305
|
-
const doc = link.ownerDocument;
|
|
8306
|
-
const frame = doc.createElement("div");
|
|
8307
|
-
frame.className = `kui-lightbox-frame kui-lightbox-frame--${aspect}`;
|
|
8308
|
-
const poster = link.querySelector("img");
|
|
8309
|
-
const title = link.title || poster?.getAttribute("alt") || link.getAttribute("aria-label")?.trim() || link.textContent?.trim() || "Video";
|
|
8310
|
-
if (source.kind === "file") {
|
|
8311
|
-
const video = doc.createElement("video");
|
|
8312
|
-
video.src = source.embedUrl;
|
|
8313
|
-
video.controls = true;
|
|
8314
|
-
video.autoplay = true;
|
|
8315
|
-
video.playsInline = true;
|
|
8316
|
-
video.setAttribute("aria-label", title);
|
|
8317
|
-
frame.append(video);
|
|
8318
|
-
} else {
|
|
8319
|
-
const iframe = doc.createElement("iframe");
|
|
8320
|
-
iframe.src = source.embedUrl;
|
|
8321
|
-
iframe.title = title;
|
|
8322
|
-
iframe.allow = "autoplay; encrypted-media; picture-in-picture; fullscreen";
|
|
8323
|
-
iframe.allowFullscreen = true;
|
|
8324
|
-
frame.append(iframe);
|
|
8325
|
-
}
|
|
8326
|
-
return { ...options, node: frame, label: "Video viewer", onClose: () => stopMedia(frame) };
|
|
8327
|
-
}
|
|
8328
9709
|
function primaryClick(event) {
|
|
8329
9710
|
return event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey;
|
|
8330
9711
|
}
|
|
8331
|
-
function
|
|
9712
|
+
function openAt(triggers, trigger, wiring) {
|
|
9713
|
+
const items = [];
|
|
9714
|
+
let initial = -1;
|
|
9715
|
+
for (const candidate of triggers) {
|
|
9716
|
+
const item = itemFor(candidate, wiring.media, wiring.captionSource);
|
|
9717
|
+
if (!item) continue;
|
|
9718
|
+
if (candidate === trigger) initial = items.length;
|
|
9719
|
+
items.push(item);
|
|
9720
|
+
}
|
|
9721
|
+
if (initial < 0) {
|
|
9722
|
+
const href = trigger.getAttribute("href") ?? "";
|
|
9723
|
+
if (!wiring.warned.has(href)) wiring.ctx.warn(`${effectName(wiring.media)} cannot embed "${href}"; the link will navigate normally`);
|
|
9724
|
+
wiring.warned.add(href);
|
|
9725
|
+
return false;
|
|
9726
|
+
}
|
|
9727
|
+
wiring.shell.open({ ...galleryContent(items, initial, wiring.options), opener: trigger });
|
|
9728
|
+
return true;
|
|
9729
|
+
}
|
|
9730
|
+
function wireTriggers(triggers, wiring) {
|
|
8332
9731
|
const restore = [];
|
|
8333
|
-
|
|
8334
|
-
const bare =
|
|
9732
|
+
for (const trigger of triggers) {
|
|
9733
|
+
const bare = trigger instanceof HTMLImageElement;
|
|
8335
9734
|
if (bare) {
|
|
8336
|
-
const attrs = createAttributeLedger(
|
|
9735
|
+
const attrs = createAttributeLedger(trigger);
|
|
8337
9736
|
attrs.set("role", "button");
|
|
8338
9737
|
attrs.set("tabindex", "0");
|
|
8339
|
-
attrs.set("aria-label",
|
|
9738
|
+
attrs.set("aria-label", trigger.alt ? `Open larger image: ${trigger.alt}` : "Open larger image");
|
|
8340
9739
|
restore.push(() => attrs.restore());
|
|
8341
|
-
|
|
9740
|
+
trigger.addEventListener("keydown", (event) => {
|
|
8342
9741
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
8343
9742
|
event.preventDefault();
|
|
8344
|
-
|
|
9743
|
+
openAt(triggers, trigger, wiring);
|
|
8345
9744
|
}, { signal: wiring.signal });
|
|
8346
9745
|
}
|
|
8347
|
-
|
|
9746
|
+
trigger.addEventListener("click", (event) => {
|
|
8348
9747
|
if (!primaryClick(event)) return;
|
|
8349
|
-
if (
|
|
8350
|
-
wiring.shell.open(galleryContent(items, index, loop2, wiring.options));
|
|
9748
|
+
if (openAt(triggers, trigger, wiring)) event.preventDefault();
|
|
8351
9749
|
}, { signal: wiring.signal });
|
|
8352
|
-
}
|
|
9750
|
+
}
|
|
8353
9751
|
return restore;
|
|
8354
9752
|
}
|
|
8355
|
-
function
|
|
8356
|
-
const
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
9753
|
+
function cardItem(card) {
|
|
9754
|
+
const link = card.matches("a[href]") ? card : card.querySelector("a[href]");
|
|
9755
|
+
return link && itemFor(link, "mixed", "figcaption") || itemFor(card, "mixed", "figcaption");
|
|
9756
|
+
}
|
|
9757
|
+
var OWN_CLICK = "button, input, select, textarea, label, summary, [contenteditable]";
|
|
9758
|
+
function attachDeckLightbox(request) {
|
|
9759
|
+
const { host, cards, doc, reducedMotion } = request;
|
|
9760
|
+
const shell = acquireModalShell(doc);
|
|
9761
|
+
const options = {
|
|
9762
|
+
duration: DEFAULT_DURATION_MS,
|
|
9763
|
+
scale: DEFAULT_SCALE,
|
|
9764
|
+
ease: DEFAULT_EASE,
|
|
9765
|
+
reducedMotion,
|
|
9766
|
+
loop: true,
|
|
9767
|
+
aspect: ""
|
|
9768
|
+
};
|
|
9769
|
+
const turnedAway = (card) => card.getAttribute("data-kui-ring-face") === "back";
|
|
9770
|
+
const openAt2 = (pick, faceChecked = false) => {
|
|
9771
|
+
const all = [...cards()];
|
|
9772
|
+
const card = all.find(pick);
|
|
9773
|
+
if (!card || !faceChecked && turnedAway(card)) return false;
|
|
9774
|
+
const items = [];
|
|
9775
|
+
let initial = -1;
|
|
9776
|
+
for (const candidate of all) {
|
|
9777
|
+
const item = cardItem(candidate);
|
|
9778
|
+
if (!item) continue;
|
|
9779
|
+
if (candidate === card) initial = items.length;
|
|
9780
|
+
items.push(item);
|
|
9781
|
+
}
|
|
9782
|
+
if (initial < 0) return false;
|
|
9783
|
+
shell.open({ ...galleryContent(items, initial, options), opener: card });
|
|
9784
|
+
return true;
|
|
9785
|
+
};
|
|
9786
|
+
let pressed = null;
|
|
9787
|
+
const cardUnder = (event) => {
|
|
9788
|
+
const all = [...cards()];
|
|
9789
|
+
const hits = typeof doc.elementsFromPoint === "function" ? doc.elementsFromPoint(event.clientX, event.clientY) : [];
|
|
9790
|
+
for (const node of [...hits, event.target]) {
|
|
9791
|
+
const card = all.find((candidate) => candidate.contains(node));
|
|
9792
|
+
if (card) return turnedAway(card) ? null : { card, node };
|
|
9793
|
+
}
|
|
9794
|
+
return null;
|
|
9795
|
+
};
|
|
9796
|
+
const onPress = (event) => {
|
|
9797
|
+
pressed = event.button === 0 ? cardUnder(event) : null;
|
|
9798
|
+
};
|
|
9799
|
+
const onClick = (event) => {
|
|
9800
|
+
const press = event.detail > 0 ? pressed : null;
|
|
9801
|
+
pressed = null;
|
|
9802
|
+
const target = event.target;
|
|
9803
|
+
if (event.defaultPrevented || !primaryClick(event) || target.closest(OWN_CLICK)) return;
|
|
9804
|
+
if (press?.node.closest(OWN_CLICK)) return;
|
|
9805
|
+
const opened = press ? openAt2((card) => card === press.card, true) : openAt2((card) => card.contains(target));
|
|
9806
|
+
if (opened) event.preventDefault();
|
|
9807
|
+
};
|
|
9808
|
+
const onKeyDown = (event) => {
|
|
9809
|
+
const key = event.key;
|
|
9810
|
+
if (event.target !== host || key !== "Enter" && key !== " ") return;
|
|
9811
|
+
if (openAt2((card) => card.getAttribute(STEP_STATE_ATTR) === "active")) event.preventDefault();
|
|
9812
|
+
};
|
|
9813
|
+
host.addEventListener("pointerdown", onPress);
|
|
9814
|
+
host.addEventListener("click", onClick);
|
|
9815
|
+
host.addEventListener("keydown", onKeyDown);
|
|
9816
|
+
return () => {
|
|
9817
|
+
host.removeEventListener("pointerdown", onPress);
|
|
9818
|
+
host.removeEventListener("click", onClick);
|
|
9819
|
+
host.removeEventListener("keydown", onKeyDown);
|
|
9820
|
+
shell.release();
|
|
9821
|
+
};
|
|
8372
9822
|
}
|
|
8373
9823
|
function prepareLightbox(el, params, ctx) {
|
|
8374
9824
|
const media = params.text("media", "image");
|
|
8375
|
-
const
|
|
8376
|
-
if (
|
|
9825
|
+
const triggers = triggersFor(targets(el, params, ctx, media), media);
|
|
9826
|
+
if (triggers.length === 0) return () => {
|
|
8377
9827
|
};
|
|
8378
9828
|
const controller = new AbortController();
|
|
8379
9829
|
const shell = acquireModalShell(ctx.doc);
|
|
8380
|
-
const
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
9830
|
+
const options = { ...modalOptions(params, ctx), loop: params.is("loop"), aspect: params.text("aspect") };
|
|
9831
|
+
const restore = wireTriggers(triggers, {
|
|
9832
|
+
shell,
|
|
9833
|
+
options,
|
|
9834
|
+
signal: controller.signal,
|
|
9835
|
+
media,
|
|
9836
|
+
captionSource: params.text("caption", "figcaption"),
|
|
9837
|
+
ctx,
|
|
9838
|
+
warned: /* @__PURE__ */ new Set()
|
|
9839
|
+
});
|
|
8388
9840
|
return continuousSetup(() => {
|
|
8389
9841
|
controller.abort();
|
|
8390
9842
|
for (const undo of restore) undo();
|
|
8391
9843
|
shell.release();
|
|
8392
9844
|
});
|
|
8393
9845
|
}
|
|
9846
|
+
var TRIGGERS_CHANNEL = "lightbox:triggers";
|
|
8394
9847
|
var LIGHTBOX_PRIMITIVE = widgetPrimitive(
|
|
8395
9848
|
"lightbox",
|
|
8396
|
-
{ channels: [
|
|
9849
|
+
{ channels: [TRIGGERS_CHANNEL], parameters: paramsSchema, perfClass: "paint" },
|
|
8397
9850
|
withTimingContract("lightbox", { honours: ["duration", "ease"], because: "the dialog opens immediately, so it has no delay phase" }, deferPrepare(prepareLightbox))
|
|
8398
9851
|
);
|
|
8399
9852
|
var LIGHTBOX_PRESETS = [
|
|
8400
9853
|
{ name: "lightbox", primitive: "lightbox", params: { media: "image" } },
|
|
8401
|
-
|
|
9854
|
+
// A video row has never shown captions; `caption:figcaption` opts one in.
|
|
9855
|
+
{ name: "video-lightbox", primitive: "lightbox", params: { media: "video", caption: "none" } }
|
|
8402
9856
|
];
|
|
8403
9857
|
|
|
8404
9858
|
// src/showcase/compare.ts
|
|
8405
9859
|
var COMPARE_AXES = ["x", "y"];
|
|
8406
9860
|
var COMPARE_AXIS_ATTR = "data-kui-compare-axis";
|
|
9861
|
+
var COMPARE_DIVIDERS_ATTR = "data-kui-compare-dividers";
|
|
8407
9862
|
var compareParams = {
|
|
8408
9863
|
position: {
|
|
8409
9864
|
type: "percentage",
|
|
8410
9865
|
default: "50%",
|
|
8411
9866
|
cssProperty: "--kui-compare"
|
|
8412
9867
|
},
|
|
9868
|
+
/*
|
|
9869
|
+
* Where each divider of a three-or-more frame starts, as one quoted, space-separated list —
|
|
9870
|
+
* `positions:'20% 45% 80%'` — because a comma would start a new step, not a new value. Default
|
|
9871
|
+
* (empty) is an even split. `position:` stays the two-media spelling: it is a single percentage
|
|
9872
|
+
* and every existing page relies on it validating as one.
|
|
9873
|
+
*/
|
|
9874
|
+
positions: { type: "text", default: "", cssProperty: "--kui-compare-positions" },
|
|
8413
9875
|
axis: {
|
|
8414
9876
|
type: "keyword",
|
|
8415
9877
|
default: "x",
|
|
8416
9878
|
cssProperty: "--kui-compare-axis",
|
|
8417
9879
|
keywords: [...COMPARE_AXES]
|
|
8418
|
-
}
|
|
9880
|
+
},
|
|
9881
|
+
/*
|
|
9882
|
+
* The auto-drag, in `background-media`'s words (`src/effects/catalog/media.ts`): `always` starts
|
|
9883
|
+
* at load, `in-view` the first time the slider is on screen, `never` (default) not at all.
|
|
9884
|
+
*
|
|
9885
|
+
* Not `on:load` / `on:enter`, because the activation grammar already has a job here: it is what
|
|
9886
|
+
* *builds* the slider, and a widget accepts only `on:load` (`src/showcase/shared.ts`) so the
|
|
9887
|
+
* control exists before a keyboard user tabs to it. `on:enter` would defer the widget, not the
|
|
9888
|
+
* motion — so the motion's start is its own parameter.
|
|
9889
|
+
*/
|
|
9890
|
+
autoplay: {
|
|
9891
|
+
type: "keyword",
|
|
9892
|
+
default: "never",
|
|
9893
|
+
cssProperty: "--kui-compare-autoplay",
|
|
9894
|
+
keywords: ["never", "in-view", "always"]
|
|
9895
|
+
},
|
|
9896
|
+
/** One edge-to-edge sweep. The positional spelling (`compare 4s`) means the same thing. */
|
|
9897
|
+
duration: { type: "time", default: "3s", cssProperty: "--kui-compare-duration" },
|
|
9898
|
+
/** Head toward 0% first instead of 100%. */
|
|
9899
|
+
reverse: { type: "keyword", default: "false", cssProperty: "--kui-compare-reverse", keywords: ["true", "false"] },
|
|
9900
|
+
/** Sweep back and forth until someone takes the handle, instead of one pass. */
|
|
9901
|
+
loop: { type: "keyword", default: "false", cssProperty: "--kui-compare-loop", keywords: ["true", "false"] }
|
|
8419
9902
|
};
|
|
9903
|
+
var MIN_SWEEP_MS = 250;
|
|
9904
|
+
var MAX_FRAME_MS2 = 100;
|
|
8420
9905
|
var VERTICAL_KEY_DELTA = { ArrowUp: -1, ArrowDown: 1 };
|
|
8421
9906
|
function getMediaName(item, fallback) {
|
|
8422
9907
|
const alt = item.localName === "picture" ? item.querySelector("img")?.getAttribute("alt") : item.getAttribute("alt");
|
|
@@ -8432,76 +9917,402 @@ function resolveCompareLabel(el, media) {
|
|
|
8432
9917
|
}
|
|
8433
9918
|
return "Compare";
|
|
8434
9919
|
}
|
|
8435
|
-
function
|
|
9920
|
+
function createRange(doc, label, value, axis) {
|
|
8436
9921
|
const range = doc.createElement("input");
|
|
8437
9922
|
range.type = "range";
|
|
8438
9923
|
range.className = "kui-compare-range";
|
|
8439
9924
|
range.min = "0";
|
|
8440
9925
|
range.max = "100";
|
|
8441
9926
|
range.step = "1";
|
|
8442
|
-
range.value = String(
|
|
8443
|
-
range.setAttribute("aria-label", label);
|
|
8444
|
-
range.setAttribute("aria-valuetext",
|
|
9927
|
+
range.value = String(value);
|
|
9928
|
+
range.setAttribute("aria-label", label.name);
|
|
9929
|
+
range.setAttribute("aria-valuetext", label.valueText);
|
|
8445
9930
|
if (axis === "y") range.setAttribute("aria-orientation", "vertical");
|
|
9931
|
+
return range;
|
|
9932
|
+
}
|
|
9933
|
+
function createHandle(doc) {
|
|
8446
9934
|
const handle = doc.createElement("span");
|
|
8447
9935
|
handle.className = "kui-compare-handle";
|
|
8448
9936
|
handle.setAttribute("aria-hidden", "true");
|
|
8449
|
-
return
|
|
9937
|
+
return handle;
|
|
8450
9938
|
}
|
|
8451
|
-
function
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
9939
|
+
function verticalKeys(range, axis, onInput) {
|
|
9940
|
+
return (event) => {
|
|
9941
|
+
const delta = VERTICAL_KEY_DELTA[event.key];
|
|
9942
|
+
if (axis !== "y" || delta === void 0) return;
|
|
9943
|
+
event.preventDefault();
|
|
9944
|
+
if (delta < 0) range.stepDown();
|
|
9945
|
+
else range.stepUp();
|
|
9946
|
+
onInput();
|
|
9947
|
+
};
|
|
9948
|
+
}
|
|
9949
|
+
function sweepPercent(phase) {
|
|
9950
|
+
return 50 - 50 * Math.cos(Math.PI * phase / 100);
|
|
9951
|
+
}
|
|
9952
|
+
function startPhase(percent, reverse) {
|
|
9953
|
+
const outbound = 100 / Math.PI * Math.acos(1 - percent / 50);
|
|
9954
|
+
return reverse ? 200 - outbound : outbound;
|
|
9955
|
+
}
|
|
9956
|
+
function sweepDuration(params, ctx) {
|
|
9957
|
+
const authored = params.timing.durationMs ?? params.ms("duration", 3e3);
|
|
9958
|
+
if (authored >= MIN_SWEEP_MS) return authored;
|
|
9959
|
+
ctx.warn(`compare duration: minimum is ${MIN_SWEEP_MS}ms; clamped to ${MIN_SWEEP_MS}ms`);
|
|
9960
|
+
return MIN_SWEEP_MS;
|
|
9961
|
+
}
|
|
9962
|
+
function bindAutoDrag(options) {
|
|
9963
|
+
const { host, grab, ctx, mode, loop: loop2 } = options;
|
|
9964
|
+
if (mode === "never" || ctx.reducedMotion) return () => {
|
|
9965
|
+
};
|
|
9966
|
+
const run = runAutoDrag(options, () => stop());
|
|
9967
|
+
let observer = null;
|
|
9968
|
+
if (mode === "in-view" || loop2) {
|
|
9969
|
+
observer = observeOnscreen(ctx.win, host, run.setOnscreen);
|
|
8455
9970
|
}
|
|
8456
|
-
const
|
|
8457
|
-
|
|
8458
|
-
|
|
9971
|
+
const stop = () => {
|
|
9972
|
+
run.stop();
|
|
9973
|
+
observer?.disconnect();
|
|
9974
|
+
observer = null;
|
|
9975
|
+
for (const target of grab) {
|
|
9976
|
+
target.removeEventListener("pointerdown", stop);
|
|
9977
|
+
target.removeEventListener("focus", stop);
|
|
9978
|
+
}
|
|
9979
|
+
};
|
|
9980
|
+
for (const target of grab) {
|
|
9981
|
+
target.addEventListener("pointerdown", stop);
|
|
9982
|
+
target.addEventListener("focus", stop);
|
|
9983
|
+
}
|
|
9984
|
+
run.setOnscreen(mode === "always" || observer === null);
|
|
9985
|
+
return stop;
|
|
9986
|
+
}
|
|
9987
|
+
function sweepPlan(options) {
|
|
9988
|
+
const { write, homes, bounds, durationMs, reverse, loop: loop2 } = options;
|
|
9989
|
+
const order = homes.map((_, index) => index);
|
|
9990
|
+
if (reverse) order.reverse();
|
|
9991
|
+
const cycleMs = 2 * durationMs;
|
|
9992
|
+
const totalMs = cycleMs * order.length;
|
|
9993
|
+
const phases = homes.map((home, index) => {
|
|
9994
|
+
const [lo, hi] = bounds[index];
|
|
9995
|
+
return hi > lo ? startPhase(100 * (home - lo) / (hi - lo), reverse) : 0;
|
|
9996
|
+
});
|
|
9997
|
+
const park = (slot2) => write(order[slot2], homes[order[slot2]]);
|
|
9998
|
+
let slot = 0;
|
|
9999
|
+
return {
|
|
10000
|
+
show(elapsed) {
|
|
10001
|
+
if (!loop2 && elapsed >= totalMs) {
|
|
10002
|
+
park(slot);
|
|
10003
|
+
return false;
|
|
10004
|
+
}
|
|
10005
|
+
const into = elapsed % totalMs;
|
|
10006
|
+
const next = Math.floor(into / cycleMs);
|
|
10007
|
+
if (next !== slot) park(slot);
|
|
10008
|
+
slot = next;
|
|
10009
|
+
const index = order[slot];
|
|
10010
|
+
const [lo, hi] = bounds[index];
|
|
10011
|
+
const phase = phases[index] + 100 * (into - slot * cycleMs) / durationMs;
|
|
10012
|
+
write(index, lo + (hi - lo) * sweepPercent(phase) / 100);
|
|
10013
|
+
return true;
|
|
10014
|
+
}
|
|
10015
|
+
};
|
|
10016
|
+
}
|
|
10017
|
+
function observeOnscreen(win, host, onChange) {
|
|
10018
|
+
const Observer = win.IntersectionObserver;
|
|
10019
|
+
if (!Observer) return null;
|
|
10020
|
+
const observer = new Observer((entries) => {
|
|
10021
|
+
for (const entry of entries) onChange(entry.isIntersecting);
|
|
10022
|
+
});
|
|
10023
|
+
observer.observe(host);
|
|
10024
|
+
return observer;
|
|
10025
|
+
}
|
|
10026
|
+
function runAutoDrag(options, finish) {
|
|
10027
|
+
const win = options.ctx.win;
|
|
10028
|
+
const plan = sweepPlan(options);
|
|
10029
|
+
let elapsed = 0;
|
|
10030
|
+
let lastFrameAt = null;
|
|
10031
|
+
let frame = 0;
|
|
10032
|
+
let done = false;
|
|
10033
|
+
let onscreen = false;
|
|
10034
|
+
const update = () => {
|
|
10035
|
+
const active = !done && onscreen;
|
|
10036
|
+
if (active && frame === 0) {
|
|
10037
|
+
lastFrameAt = null;
|
|
10038
|
+
frame = win.requestAnimationFrame(tick);
|
|
10039
|
+
} else if (!active && frame !== 0) {
|
|
10040
|
+
win.cancelAnimationFrame(frame);
|
|
10041
|
+
frame = 0;
|
|
10042
|
+
}
|
|
10043
|
+
};
|
|
10044
|
+
function tick(now) {
|
|
10045
|
+
frame = 0;
|
|
10046
|
+
if (lastFrameAt !== null) elapsed += Math.min(now - lastFrameAt, MAX_FRAME_MS2);
|
|
10047
|
+
lastFrameAt = now;
|
|
10048
|
+
if (plan.show(elapsed)) frame = win.requestAnimationFrame(tick);
|
|
10049
|
+
else finish();
|
|
10050
|
+
}
|
|
10051
|
+
return {
|
|
10052
|
+
setOnscreen(next) {
|
|
10053
|
+
onscreen = next;
|
|
10054
|
+
update();
|
|
10055
|
+
},
|
|
10056
|
+
stop() {
|
|
10057
|
+
done = true;
|
|
10058
|
+
update();
|
|
10059
|
+
}
|
|
10060
|
+
};
|
|
10061
|
+
}
|
|
10062
|
+
function buildPair({ el, media, params, ctx, axis, timing: timing3 }) {
|
|
8459
10063
|
const initialPercent = Math.round(params.num("position", 0.5) * 100);
|
|
8460
10064
|
const style = createStyleLedger(el);
|
|
8461
10065
|
style.set("--kui-compare", params.text("position", "50%"));
|
|
8462
|
-
const
|
|
8463
|
-
|
|
8464
|
-
|
|
10066
|
+
const doc = el.ownerDocument;
|
|
10067
|
+
const range = createRange(
|
|
10068
|
+
doc,
|
|
10069
|
+
{ name: resolveCompareLabel(el, media), valueText: `${initialPercent}% after` },
|
|
8465
10070
|
initialPercent,
|
|
8466
10071
|
axis
|
|
8467
10072
|
);
|
|
10073
|
+
const handle = createHandle(doc);
|
|
8468
10074
|
const onInput = () => {
|
|
8469
10075
|
const val = range.value;
|
|
8470
10076
|
style.set("--kui-compare", `${val}%`);
|
|
8471
10077
|
range.setAttribute("aria-valuetext", `${val}% after`);
|
|
8472
10078
|
};
|
|
8473
|
-
const
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
if (delta < 0) range.stepDown();
|
|
8478
|
-
else range.stepUp();
|
|
8479
|
-
onInput();
|
|
10079
|
+
const writeAuto = (_index, percent) => {
|
|
10080
|
+
range.value = String(Math.round(percent));
|
|
10081
|
+
style.set("--kui-compare", `${Number(percent.toFixed(3))}%`);
|
|
10082
|
+
range.setAttribute("aria-valuetext", `${range.value}% after`);
|
|
8480
10083
|
};
|
|
10084
|
+
const onKeyDown = verticalKeys(range, axis, onInput);
|
|
8481
10085
|
range.addEventListener("input", onInput);
|
|
8482
10086
|
range.addEventListener("keydown", onKeyDown);
|
|
8483
10087
|
el.appendChild(range);
|
|
8484
10088
|
el.appendChild(handle);
|
|
8485
|
-
|
|
10089
|
+
const stopAutoDrag = bindAutoDrag({
|
|
10090
|
+
...timing3,
|
|
10091
|
+
host: el,
|
|
10092
|
+
grab: [range],
|
|
10093
|
+
ctx,
|
|
10094
|
+
write: writeAuto,
|
|
10095
|
+
homes: [params.num("position", 0.5) * 100],
|
|
10096
|
+
bounds: [[0, 100]]
|
|
10097
|
+
});
|
|
10098
|
+
return () => {
|
|
10099
|
+
stopAutoDrag();
|
|
8486
10100
|
range.removeEventListener("input", onInput);
|
|
8487
10101
|
range.removeEventListener("keydown", onKeyDown);
|
|
8488
10102
|
range.remove();
|
|
8489
10103
|
handle.remove();
|
|
8490
|
-
attrs.restore();
|
|
8491
10104
|
style.restore();
|
|
10105
|
+
};
|
|
10106
|
+
}
|
|
10107
|
+
var PERCENT_TOKEN = /^(\d+(?:\.\d+)?)%$/;
|
|
10108
|
+
function resolvePositions(authored, count, warn) {
|
|
10109
|
+
const even = Array.from({ length: count }, (_, k) => Math.round(100 * (k + 1) / (count + 1)));
|
|
10110
|
+
if (authored.trim() === "") return even;
|
|
10111
|
+
const tokens = authored.trim().split(/\s+/);
|
|
10112
|
+
const values = tokens.map((token) => PERCENT_TOKEN.exec(token)?.[1]);
|
|
10113
|
+
const numbers = values.map(Number);
|
|
10114
|
+
const usable2 = tokens.length === count && values.every((value) => value !== void 0) && numbers.every((value, k) => value <= 100 && (k === 0 || value >= numbers[k - 1]));
|
|
10115
|
+
if (usable2) return numbers.map(Math.round);
|
|
10116
|
+
const example = even.map((value) => String(value) + "%").join(" ");
|
|
10117
|
+
warn(
|
|
10118
|
+
`compare positions: expected ${count} ascending percentages between 0% and 100% for ${count + 1} media (e.g. positions:'${example}'); using an even split`
|
|
10119
|
+
);
|
|
10120
|
+
return even;
|
|
10121
|
+
}
|
|
10122
|
+
function buildMulti({ el, media, params, ctx, axis, timing: timing3 }) {
|
|
10123
|
+
if (params.text("position", "50%") !== "50%") {
|
|
10124
|
+
ctx.warn(`compare position: sets the one divider of two media; with ${media.length}, use positions:`);
|
|
10125
|
+
}
|
|
10126
|
+
const positions = resolvePositions(params.text("positions", ""), media.length - 1, ctx.warn);
|
|
10127
|
+
const homes = [...positions];
|
|
10128
|
+
const attrs = createAttributeLedger(el);
|
|
10129
|
+
attrs.set(COMPARE_DIVIDERS_ATTR, String(homes.length));
|
|
10130
|
+
const dividers = createDividers(el, media, axis, positions);
|
|
10131
|
+
const surface = bindSurface(el, axis, dividers);
|
|
10132
|
+
const stopAutoDrag = bindAutoDrag({
|
|
10133
|
+
...timing3,
|
|
10134
|
+
host: el,
|
|
10135
|
+
grab: [surface.node, ...dividers.ranges],
|
|
10136
|
+
ctx,
|
|
10137
|
+
write: dividers.render,
|
|
10138
|
+
homes,
|
|
10139
|
+
bounds: homes.map((_, k) => boundsOf(homes, k))
|
|
10140
|
+
});
|
|
10141
|
+
return () => {
|
|
10142
|
+
stopAutoDrag();
|
|
10143
|
+
surface.destroy();
|
|
10144
|
+
dividers.destroy();
|
|
10145
|
+
attrs.restore();
|
|
10146
|
+
};
|
|
10147
|
+
}
|
|
10148
|
+
function boundsOf(positions, k) {
|
|
10149
|
+
return [k > 0 ? positions[k - 1] : 0, k < positions.length - 1 ? positions[k + 1] : 100];
|
|
10150
|
+
}
|
|
10151
|
+
function dividerLabel(el, media, k) {
|
|
10152
|
+
const count = media.length - 1;
|
|
10153
|
+
const authored = el.getAttribute("aria-label")?.trim();
|
|
10154
|
+
if (authored) return `${authored}: divider ${k + 1} of ${count}`;
|
|
10155
|
+
const before = getMediaName(media[k], `Image ${k + 1}`);
|
|
10156
|
+
const after = getMediaName(media[k + 1], `Image ${k + 2}`);
|
|
10157
|
+
return `Divider ${k + 1} of ${count}: ${before} / ${after}`;
|
|
10158
|
+
}
|
|
10159
|
+
function createDividers(el, media, axis, positions) {
|
|
10160
|
+
const doc = el.ownerDocument;
|
|
10161
|
+
const count = positions.length;
|
|
10162
|
+
const clip = axis === "y" ? "inset(var(--kui-compare) 0 0 0)" : "inset(0 0 0 var(--kui-compare))";
|
|
10163
|
+
const mediaStyles = media.slice(1).map((item) => createStyleLedger(item));
|
|
10164
|
+
for (const ledger of mediaStyles) ledger.set("clip-path", clip);
|
|
10165
|
+
const ranges = [];
|
|
10166
|
+
const handles = [];
|
|
10167
|
+
const cleanups = [];
|
|
10168
|
+
const render = (k, shown) => {
|
|
10169
|
+
positions[k] = Math.round(shown);
|
|
10170
|
+
const css = `${Number(shown.toFixed(3))}%`;
|
|
10171
|
+
handles[k].style.setProperty("--kui-compare", css);
|
|
10172
|
+
mediaStyles[k].set("--kui-compare", css);
|
|
10173
|
+
ranges[k].value = String(positions[k]);
|
|
10174
|
+
ranges[k].setAttribute("aria-valuetext", `${positions[k]}%`);
|
|
10175
|
+
if (k > 0) ranges[k - 1].max = String(positions[k]);
|
|
10176
|
+
if (k < count - 1) ranges[k + 1].min = String(positions[k]);
|
|
10177
|
+
};
|
|
10178
|
+
for (let k = 0; k < count; k++) {
|
|
10179
|
+
const range = createRange(doc, { name: dividerLabel(el, media, k), valueText: `${positions[k]}%` }, positions[k], axis);
|
|
10180
|
+
const handle = createHandle(doc);
|
|
10181
|
+
ranges.push(range);
|
|
10182
|
+
handles.push(handle);
|
|
10183
|
+
el.append(range, handle);
|
|
10184
|
+
const onInput = () => render(k, Number(range.value));
|
|
10185
|
+
const onKeyDown = verticalKeys(range, axis, onInput);
|
|
10186
|
+
range.addEventListener("input", onInput);
|
|
10187
|
+
range.addEventListener("keydown", onKeyDown);
|
|
10188
|
+
cleanups.push(() => {
|
|
10189
|
+
range.removeEventListener("input", onInput);
|
|
10190
|
+
range.removeEventListener("keydown", onKeyDown);
|
|
10191
|
+
});
|
|
10192
|
+
}
|
|
10193
|
+
for (let k = 0; k < count; k++) {
|
|
10194
|
+
const [lo, hi] = boundsOf(positions, k);
|
|
10195
|
+
ranges[k].min = String(lo);
|
|
10196
|
+
ranges[k].max = String(hi);
|
|
10197
|
+
render(k, positions[k]);
|
|
10198
|
+
}
|
|
10199
|
+
return {
|
|
10200
|
+
ranges,
|
|
10201
|
+
render,
|
|
10202
|
+
moveTo(k, percent) {
|
|
10203
|
+
const [lo, hi] = boundsOf(positions, k);
|
|
10204
|
+
render(k, Math.min(hi, Math.max(lo, percent)));
|
|
10205
|
+
},
|
|
10206
|
+
nearest: (percent) => nearestDivider(positions, percent),
|
|
10207
|
+
destroy() {
|
|
10208
|
+
for (const cleanup of cleanups) cleanup();
|
|
10209
|
+
for (const node of [...ranges, ...handles]) node.remove();
|
|
10210
|
+
for (const ledger of mediaStyles) ledger.restore();
|
|
10211
|
+
}
|
|
10212
|
+
};
|
|
10213
|
+
}
|
|
10214
|
+
function nearestDivider(positions, percent) {
|
|
10215
|
+
let best = 0;
|
|
10216
|
+
for (let k = 1; k < positions.length; k++) {
|
|
10217
|
+
const distance2 = Math.abs(positions[k] - percent);
|
|
10218
|
+
const bestDistance = Math.abs(positions[best] - percent);
|
|
10219
|
+
if (distance2 < bestDistance || distance2 === bestDistance && percent >= positions[k]) best = k;
|
|
10220
|
+
}
|
|
10221
|
+
return best;
|
|
10222
|
+
}
|
|
10223
|
+
function bindSurface(el, axis, dividers) {
|
|
10224
|
+
const surface = el.ownerDocument.createElement("div");
|
|
10225
|
+
surface.className = "kui-compare-surface";
|
|
10226
|
+
surface.setAttribute("aria-hidden", "true");
|
|
10227
|
+
el.appendChild(surface);
|
|
10228
|
+
let dragging = -1;
|
|
10229
|
+
let pointerId = null;
|
|
10230
|
+
const percentAt = (event) => {
|
|
10231
|
+
const rect = el.getBoundingClientRect();
|
|
10232
|
+
const along = axis === "y" ? event.clientY - rect.top : event.clientX - rect.left;
|
|
10233
|
+
const size = axis === "y" ? rect.height : rect.width;
|
|
10234
|
+
return size > 0 ? 100 * along / size : 0;
|
|
10235
|
+
};
|
|
10236
|
+
const onPointerDown = (event) => {
|
|
10237
|
+
if (event.button > 0 || pointerId !== null) return;
|
|
10238
|
+
pointerId = event.pointerId;
|
|
10239
|
+
const percent = percentAt(event);
|
|
10240
|
+
dragging = dividers.nearest(percent);
|
|
10241
|
+
event.preventDefault();
|
|
10242
|
+
surface.setPointerCapture?.(event.pointerId);
|
|
10243
|
+
dividers.ranges[dragging].focus({ preventScroll: true });
|
|
10244
|
+
dividers.moveTo(dragging, percent);
|
|
10245
|
+
};
|
|
10246
|
+
const onPointerMove = (event) => {
|
|
10247
|
+
if (dragging >= 0 && event.pointerId === pointerId) dividers.moveTo(dragging, percentAt(event));
|
|
10248
|
+
};
|
|
10249
|
+
const onPointerUp = (event) => {
|
|
10250
|
+
if (event.pointerId !== pointerId) return;
|
|
10251
|
+
dragging = -1;
|
|
10252
|
+
pointerId = null;
|
|
10253
|
+
};
|
|
10254
|
+
const onLostCapture = (event) => {
|
|
10255
|
+
if (event.target !== surface) return;
|
|
10256
|
+
onPointerUp(event);
|
|
10257
|
+
};
|
|
10258
|
+
const listeners = [
|
|
10259
|
+
["pointerdown", onPointerDown],
|
|
10260
|
+
["pointermove", onPointerMove],
|
|
10261
|
+
["pointerup", onPointerUp],
|
|
10262
|
+
["pointercancel", onPointerUp],
|
|
10263
|
+
["lostpointercapture", onLostCapture]
|
|
10264
|
+
];
|
|
10265
|
+
for (const [type, listener] of listeners) surface.addEventListener(type, listener);
|
|
10266
|
+
return {
|
|
10267
|
+
node: surface,
|
|
10268
|
+
destroy() {
|
|
10269
|
+
for (const [type, listener] of listeners) surface.removeEventListener(type, listener);
|
|
10270
|
+
surface.remove();
|
|
10271
|
+
}
|
|
10272
|
+
};
|
|
10273
|
+
}
|
|
10274
|
+
function prepareCompare(el, params, ctx) {
|
|
10275
|
+
const media = Array.from(el.querySelectorAll(":scope > :is(img, picture, video)"));
|
|
10276
|
+
if (media.length < 2) {
|
|
10277
|
+
ctx.warn("compare requires at least two media children (img, picture, or video)");
|
|
10278
|
+
}
|
|
10279
|
+
const axis = params.text("axis", "x");
|
|
10280
|
+
const attrs = createAttributeLedger(el);
|
|
10281
|
+
attrs.set(COMPARE_AXIS_ATTR, axis);
|
|
10282
|
+
const build2 = media.length > 2 ? buildMulti : buildPair;
|
|
10283
|
+
const teardown = build2({
|
|
10284
|
+
el,
|
|
10285
|
+
media,
|
|
10286
|
+
params,
|
|
10287
|
+
ctx,
|
|
10288
|
+
axis,
|
|
10289
|
+
timing: {
|
|
10290
|
+
mode: params.text("autoplay", "never"),
|
|
10291
|
+
durationMs: sweepDuration(params, ctx),
|
|
10292
|
+
reverse: params.is("reverse"),
|
|
10293
|
+
loop: params.is("loop")
|
|
10294
|
+
}
|
|
10295
|
+
});
|
|
10296
|
+
return continuousSetup(() => {
|
|
10297
|
+
teardown();
|
|
10298
|
+
attrs.restore();
|
|
8492
10299
|
});
|
|
8493
10300
|
}
|
|
8494
10301
|
var COMPARE_PRIMITIVE = widgetPrimitive(
|
|
8495
10302
|
"compare",
|
|
8496
10303
|
{
|
|
8497
|
-
|
|
10304
|
+
// The layers it stacks and the range, handle and surface it appends; the axis and divider
|
|
10305
|
+
// markers it writes on the host. Not `clip`: its `clip-path` is on the after layer, never the
|
|
10306
|
+
// host, so a `wipe-up` revealing the whole slider is not a fight over one property.
|
|
10307
|
+
channels: [SUBTREE_CHANNEL, attributeChannel(COMPARE_AXIS_ATTR), attributeChannel(COMPARE_DIVIDERS_ATTR)],
|
|
8498
10308
|
parameters: compareParams,
|
|
8499
10309
|
perfClass: "paint"
|
|
8500
10310
|
},
|
|
8501
10311
|
withTimingContract(
|
|
8502
10312
|
"compare",
|
|
8503
10313
|
{
|
|
8504
|
-
|
|
10314
|
+
honours: ["duration"],
|
|
10315
|
+
because: "its only motion is the autoplay: sweep, which starts on autoplay: rather than after a delay and eases into each edge on its own curve"
|
|
8505
10316
|
},
|
|
8506
10317
|
deferPrepare(prepareCompare)
|
|
8507
10318
|
)
|
|
@@ -8675,7 +10486,8 @@ function prepareHotspots(el, params, ctx) {
|
|
|
8675
10486
|
var HOTSPOTS_PRIMITIVE = widgetPrimitive(
|
|
8676
10487
|
"hotspots",
|
|
8677
10488
|
{
|
|
8678
|
-
|
|
10489
|
+
// The marker buttons it appends, placed in percent of the host, and the marker style it writes.
|
|
10490
|
+
channels: [SUBTREE_CHANNEL, attributeChannel("data-kui-hotspot-marker")],
|
|
8679
10491
|
parameters: hotspotsParams,
|
|
8680
10492
|
perfClass: "layout"
|
|
8681
10493
|
},
|
|
@@ -8879,7 +10691,7 @@ function bindAutoplay(options) {
|
|
|
8879
10691
|
hovered = true;
|
|
8880
10692
|
update();
|
|
8881
10693
|
};
|
|
8882
|
-
const
|
|
10694
|
+
const leave2 = () => {
|
|
8883
10695
|
hovered = false;
|
|
8884
10696
|
update();
|
|
8885
10697
|
};
|
|
@@ -8896,7 +10708,7 @@ function bindAutoplay(options) {
|
|
|
8896
10708
|
update();
|
|
8897
10709
|
};
|
|
8898
10710
|
el.addEventListener("pointerenter", enter);
|
|
8899
|
-
el.addEventListener("pointerleave",
|
|
10711
|
+
el.addEventListener("pointerleave", leave2);
|
|
8900
10712
|
el.addEventListener("focusin", focus);
|
|
8901
10713
|
el.addEventListener("focusout", blur);
|
|
8902
10714
|
ctx.doc.addEventListener("visibilitychange", update);
|
|
@@ -8905,7 +10717,7 @@ function bindAutoplay(options) {
|
|
|
8905
10717
|
return () => {
|
|
8906
10718
|
if (timer !== null) clearTimeout(timer);
|
|
8907
10719
|
el.removeEventListener("pointerenter", enter);
|
|
8908
|
-
el.removeEventListener("pointerleave",
|
|
10720
|
+
el.removeEventListener("pointerleave", leave2);
|
|
8909
10721
|
el.removeEventListener("focusin", focus);
|
|
8910
10722
|
el.removeEventListener("focusout", blur);
|
|
8911
10723
|
ctx.doc.removeEventListener("visibilitychange", update);
|
|
@@ -8937,7 +10749,7 @@ function bindSwipe(el, params, index) {
|
|
|
8937
10749
|
return recognise(el, { onSwipe: (direction) => {
|
|
8938
10750
|
if (direction === "left") index.next();
|
|
8939
10751
|
if (direction === "right") index.prev();
|
|
8940
|
-
} }, { axis: "x", capturePointer:
|
|
10752
|
+
} }, { axis: "x", capturePointer: "drag" });
|
|
8941
10753
|
}
|
|
8942
10754
|
function prepareSlideshow(el, params, ctx) {
|
|
8943
10755
|
const selector = resolveTarget(
|
|
@@ -8971,7 +10783,6 @@ function prepareSlideshow(el, params, ctx) {
|
|
|
8971
10783
|
el,
|
|
8972
10784
|
params,
|
|
8973
10785
|
ctx,
|
|
8974
|
-
scope,
|
|
8975
10786
|
resolveSteps,
|
|
8976
10787
|
name: "slideshow",
|
|
8977
10788
|
clickFallback: false,
|
|
@@ -8999,7 +10810,19 @@ function prepareSlideshow(el, params, ctx) {
|
|
|
8999
10810
|
}
|
|
9000
10811
|
var SLIDESHOW_PRIMITIVE = widgetPrimitive(
|
|
9001
10812
|
"slideshow",
|
|
9002
|
-
|
|
10813
|
+
// The controls it inserts, then what it writes on the host: `createStepIndex`'s `data-kui-step`,
|
|
10814
|
+
// as every step deck does, and `decorateHost`'s mode and carousel roles.
|
|
10815
|
+
{
|
|
10816
|
+
channels: [
|
|
10817
|
+
SUBTREE_CHANNEL,
|
|
10818
|
+
attributeChannel("data-kui-step"),
|
|
10819
|
+
attributeChannel(SLIDE_MODE_ATTR),
|
|
10820
|
+
attributeChannel("role"),
|
|
10821
|
+
attributeChannel("aria-roledescription")
|
|
10822
|
+
],
|
|
10823
|
+
parameters: slideshowParams,
|
|
10824
|
+
perfClass: "layout"
|
|
10825
|
+
},
|
|
9003
10826
|
withTimingContract(
|
|
9004
10827
|
"slideshow",
|
|
9005
10828
|
{ honours: ["duration", "ease"], because: "the slideshow has no delayed start" },
|
|
@@ -9161,7 +10984,14 @@ function prepareScrollStory(el, params, ctx) {
|
|
|
9161
10984
|
}
|
|
9162
10985
|
var SCROLL_STORY_PRIMITIVE = widgetPrimitive(
|
|
9163
10986
|
"scroll-story",
|
|
9164
|
-
|
|
10987
|
+
// Its stylesheet lays out the host's first and last child, so nothing else may insert one. The
|
|
10988
|
+
// step index on the host (`applyStep`) is claimed so a `carousel` or `scroll-progress` beside it
|
|
10989
|
+
// is refused rather than left to overwrite it.
|
|
10990
|
+
{
|
|
10991
|
+
channels: [SUBTREE_CHANNEL, attributeChannel("data-kui-step"), attributeChannel(STORY_SIDE_ATTR)],
|
|
10992
|
+
parameters: scrollStoryParams,
|
|
10993
|
+
perfClass: "layout"
|
|
10994
|
+
},
|
|
9165
10995
|
withTimingContract(
|
|
9166
10996
|
"scroll-story",
|
|
9167
10997
|
{ because: "scroll position" },
|
|
@@ -9264,7 +11094,8 @@ function prepareSlowMo(el, params) {
|
|
|
9264
11094
|
}
|
|
9265
11095
|
var SLOW_MO_PRIMITIVE = widgetPrimitive(
|
|
9266
11096
|
"slow-mo",
|
|
9267
|
-
|
|
11097
|
+
// The toggle it prepends into the host, and the time scale it writes there.
|
|
11098
|
+
{ channels: [SUBTREE_CHANNEL, attributeChannel(TIME_SCALE_ATTR)], parameters: slowMoParams, perfClass: "compositor" },
|
|
9268
11099
|
withTimingContract(
|
|
9269
11100
|
"slow-mo",
|
|
9270
11101
|
{ because: "it changes playback rate rather than defining an animation timeline" },
|
|
@@ -9293,6 +11124,7 @@ var SHOWCASE_PRESETS = [
|
|
|
9293
11124
|
...SLOW_MO_PRESETS
|
|
9294
11125
|
];
|
|
9295
11126
|
function registerShowcase(target) {
|
|
11127
|
+
provideDeckViewer(attachDeckLightbox);
|
|
9296
11128
|
return registerInto(target, SHOWCASE_PRIMITIVES, SHOWCASE_PRESETS, "Showcase");
|
|
9297
11129
|
}
|
|
9298
11130
|
|