kuinetic 0.2.2 → 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 +2 -2
- package/dist/esm/{chunk-K2M7O6W5.mjs → chunk-FASAEQDR.mjs} +32 -1
- package/dist/esm/{chunk-XDDJVANU.mjs → chunk-FYMZW5N2.mjs} +2301 -528
- package/dist/esm/{chunk-BBQ22D7A.mjs → chunk-MZQG2VMG.mjs} +202 -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.all.js +2502 -501
- package/dist/kuinetic.all.min.js +2 -2
- package/dist/kuinetic.css +238 -65
- package/dist/kuinetic.js +2501 -500
- package/dist/kuinetic.min.css +1 -1
- package/dist/kuinetic.min.js +1 -1
- package/dist/kuinetic.notes.js +1 -0
- package/dist/types/3d/flattening.d.ts +8 -7
- package/dist/types/core/deck-viewer.d.ts +87 -0
- package/dist/types/core/describe.d.ts +174 -0
- package/dist/types/core/gesture.d.ts +10 -0
- package/dist/types/core/index.d.ts +6 -2
- 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 +7 -3
|
@@ -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
|
|
@@ -4660,6 +5354,17 @@ function recognise(el, handlers, options = {}, deps = defaultGestureDeps()) {
|
|
|
4660
5354
|
longPressTimer = null;
|
|
4661
5355
|
}
|
|
4662
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
|
+
}
|
|
4663
5368
|
origin = sampleOf(event);
|
|
4664
5369
|
samples = [origin];
|
|
4665
5370
|
active = false;
|
|
@@ -4743,7 +5448,9 @@ function recognise(el, handlers, options = {}, deps = defaultGestureDeps()) {
|
|
|
4743
5448
|
stopListeningOnDocument();
|
|
4744
5449
|
releaseCapture(event.pointerId);
|
|
4745
5450
|
if (!origin) return;
|
|
4746
|
-
|
|
5451
|
+
finish(sampleOf(event));
|
|
5452
|
+
}
|
|
5453
|
+
function finish(sample) {
|
|
4747
5454
|
samples.push(sample);
|
|
4748
5455
|
reportEnd(vectorNow(sample), sample);
|
|
4749
5456
|
origin = null;
|
|
@@ -4752,6 +5459,7 @@ function recognise(el, handlers, options = {}, deps = defaultGestureDeps()) {
|
|
|
4752
5459
|
samples = [];
|
|
4753
5460
|
}
|
|
4754
5461
|
function onLostCapture(event) {
|
|
5462
|
+
if (event.target !== el) return;
|
|
4755
5463
|
if (!captured || event.pointerId !== pointerId) return;
|
|
4756
5464
|
captured = false;
|
|
4757
5465
|
onUp(event);
|
|
@@ -4906,16 +5614,19 @@ function settle(runners, offset, vector, mode) {
|
|
|
4906
5614
|
runners.x.to(offset.x + vector.vx * carry);
|
|
4907
5615
|
runners.y.to(offset.y + vector.vy * carry);
|
|
4908
5616
|
}
|
|
4909
|
-
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`);
|
|
4910
5620
|
const stop = recognise(
|
|
4911
5621
|
el,
|
|
4912
5622
|
{
|
|
4913
5623
|
onSwipe(direction) {
|
|
4914
5624
|
el.setAttribute("data-kui-swipe", direction);
|
|
5625
|
+
announceSwipe(ctx.win, el, direction);
|
|
4915
5626
|
}
|
|
4916
5627
|
},
|
|
4917
5628
|
{
|
|
4918
|
-
axis
|
|
5629
|
+
axis,
|
|
4919
5630
|
swipeVelocity: params.num("velocity", 300),
|
|
4920
5631
|
// This primitive moves nothing, so it must not hold the pointer from `pointerdown` — that
|
|
4921
5632
|
// retargets the following `click` at this element and kills every interactive child (a
|
|
@@ -4981,7 +5692,7 @@ function prepareMagnetic(el, params, ctx) {
|
|
|
4981
5692
|
var GESTURE_PRIMITIVES = [
|
|
4982
5693
|
gesturePrimitive(
|
|
4983
5694
|
"draggable",
|
|
4984
|
-
["translate"],
|
|
5695
|
+
["translate", attributeChannel("data-kui-dragging")],
|
|
4985
5696
|
{
|
|
4986
5697
|
...springParams2,
|
|
4987
5698
|
axis: { type: "keyword", default: "both", cssProperty: "--kui-axis", keywords: ["x", "y", "both"] },
|
|
@@ -5012,7 +5723,7 @@ var GESTURE_PRIMITIVES = [
|
|
|
5012
5723
|
),
|
|
5013
5724
|
gesturePrimitive(
|
|
5014
5725
|
"swipeable",
|
|
5015
|
-
["
|
|
5726
|
+
[attributeChannel("data-kui-swipe")],
|
|
5016
5727
|
{
|
|
5017
5728
|
axis: { type: "keyword", default: "both", cssProperty: "--kui-axis", keywords: ["x", "y", "both"] },
|
|
5018
5729
|
velocity: { type: "number", default: "300", cssProperty: "--kui-velocity" }
|
|
@@ -5021,7 +5732,7 @@ var GESTURE_PRIMITIVES = [
|
|
|
5021
5732
|
),
|
|
5022
5733
|
gesturePrimitive(
|
|
5023
5734
|
"pressable",
|
|
5024
|
-
["
|
|
5735
|
+
[attributeChannel("data-kui-pressed")],
|
|
5025
5736
|
{ duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" } },
|
|
5026
5737
|
// `duration` here is the hold threshold, not a span of motion — `long-press 800ms` means
|
|
5027
5738
|
// "count it once the finger has been down for 800ms". It is read (see `preparePressable`), so
|
|
@@ -5072,6 +5783,7 @@ var GESTURE_PRESETS = [
|
|
|
5072
5783
|
},
|
|
5073
5784
|
{ name: "swipe", primitive: "swipeable", phase: "state" },
|
|
5074
5785
|
{ name: "swipe-x", primitive: "swipeable", params: { axis: "x" }, phase: "state" },
|
|
5786
|
+
{ name: "swipe-y", primitive: "swipeable", params: { axis: "y" }, phase: "state" },
|
|
5075
5787
|
{ name: "long-press", primitive: "pressable", phase: "state" },
|
|
5076
5788
|
{ name: "magnetic", primitive: "magnetic", phase: "state" },
|
|
5077
5789
|
{
|
|
@@ -5367,13 +6079,15 @@ var LAYOUT_PRIMITIVES = [
|
|
|
5367
6079
|
),
|
|
5368
6080
|
layoutPrimitive(
|
|
5369
6081
|
"auto-height",
|
|
5370
|
-
|
|
6082
|
+
// The two properties it writes on the panel: `overflow: hidden`, and the `height` it animates.
|
|
6083
|
+
["overflow", "height"],
|
|
5371
6084
|
{ attribute: { type: "text", default: "data-open", cssProperty: "--kui-attribute" } },
|
|
5372
6085
|
deferPrepare(prepareAutoHeight)
|
|
5373
6086
|
),
|
|
5374
6087
|
layoutPrimitive(
|
|
5375
6088
|
"flip-indicator",
|
|
5376
|
-
|
|
6089
|
+
// The bar's landing spot (`translate`) and its `width`, matched to the selected tab.
|
|
6090
|
+
["translate", "width"],
|
|
5377
6091
|
{
|
|
5378
6092
|
follow: { type: "text", default: "", cssProperty: "--kui-follow" },
|
|
5379
6093
|
attribute: { type: "text", default: "aria-selected", cssProperty: "--kui-attribute" }
|
|
@@ -5622,26 +6336,25 @@ function prepareBackToTop(el, params, ctx) {
|
|
|
5622
6336
|
var NAV_JS_PRIMITIVES = [
|
|
5623
6337
|
navPrimitive(
|
|
5624
6338
|
"header-shrink",
|
|
5625
|
-
//
|
|
5626
|
-
//
|
|
5627
|
-
//
|
|
5628
|
-
//
|
|
5629
|
-
//
|
|
5630
|
-
//
|
|
5631
|
-
|
|
5632
|
-
["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")],
|
|
5633
6346
|
{ offset: { type: "number", default: "120", cssProperty: "--kui-offset" } },
|
|
5634
6347
|
deferPrepare(prepareHeaderShrink)
|
|
5635
6348
|
),
|
|
5636
6349
|
navPrimitive(
|
|
5637
6350
|
"header-hide-on-scroll",
|
|
5638
|
-
[CHANNEL.translate],
|
|
6351
|
+
[CHANNEL.translate, attributeChannel("data-kui-hidden")],
|
|
5639
6352
|
{ offset: { type: "number", default: "8", cssProperty: "--kui-offset" } },
|
|
5640
6353
|
deferPrepare(prepareHeaderHide)
|
|
5641
6354
|
),
|
|
5642
6355
|
navPrimitive(
|
|
5643
6356
|
"back-to-top-fade",
|
|
5644
|
-
[CHANNEL.opacity, CHANNEL.translate],
|
|
6357
|
+
[CHANNEL.opacity, CHANNEL.translate, attributeChannel("data-kui-visible")],
|
|
5645
6358
|
{ offset: { type: "number", default: "400", cssProperty: "--kui-offset" } },
|
|
5646
6359
|
deferPrepare(prepareBackToTop)
|
|
5647
6360
|
)
|
|
@@ -6204,7 +6917,10 @@ function installSnapContainer(axis, ctx) {
|
|
|
6204
6917
|
var SCROLL_PRIMITIVES = [
|
|
6205
6918
|
scrollPrimitive({
|
|
6206
6919
|
id: "pin",
|
|
6207
|
-
|
|
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")],
|
|
6208
6924
|
parameters: {
|
|
6209
6925
|
...distanceParam2,
|
|
6210
6926
|
...stickyParams
|
|
@@ -6214,7 +6930,13 @@ var SCROLL_PRIMITIVES = [
|
|
|
6214
6930
|
}),
|
|
6215
6931
|
scrollPrimitive({
|
|
6216
6932
|
id: "scroll-progress",
|
|
6217
|
-
|
|
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")],
|
|
6218
6940
|
parameters: {
|
|
6219
6941
|
...distanceParam2,
|
|
6220
6942
|
steps: { type: "number", default: "0", cssProperty: "--kui-steps" },
|
|
@@ -6250,7 +6972,8 @@ var SCROLL_PRIMITIVES = [
|
|
|
6250
6972
|
}),
|
|
6251
6973
|
scrollPrimitive({
|
|
6252
6974
|
id: "media-scrub",
|
|
6253
|
-
|
|
6975
|
+
// `position`: it holds its host the way a pin does (`position: sticky` and its `top`).
|
|
6976
|
+
channels: ["media", "progress", "position"],
|
|
6254
6977
|
parameters: {
|
|
6255
6978
|
...distanceParam2,
|
|
6256
6979
|
// A scrub is a hold, so it needs the same two knobs a pin does. Declaring them here is what
|
|
@@ -6269,7 +6992,7 @@ var SCROLL_PRIMITIVES = [
|
|
|
6269
6992
|
}),
|
|
6270
6993
|
scrollPrimitive({
|
|
6271
6994
|
id: "scroll-spy",
|
|
6272
|
-
channels: ["
|
|
6995
|
+
channels: [attributeChannel("data-kui-active")],
|
|
6273
6996
|
parameters: {
|
|
6274
6997
|
// `distance`: the per-section form only. `offset-top`: the container form only. Each is a
|
|
6275
6998
|
// no-op — warned, not silent — in the other; see `prepareScrollSpySingle` and
|
|
@@ -6295,8 +7018,8 @@ var SCROLL_PRIMITIVES = [
|
|
|
6295
7018
|
scrollPrimitive({
|
|
6296
7019
|
id: "smooth-scroll",
|
|
6297
7020
|
/*
|
|
6298
|
-
* Its own channel, not the `'layout'` it used to share with `pin`, `stacking-cards` and
|
|
6299
|
-
* `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
|
|
6300
7023
|
* property, and this one writes exactly `scroll-behavior` — a property that describes how a
|
|
6301
7024
|
* *user-or-script-initiated* scroll is performed, and that no other primitive touches.
|
|
6302
7025
|
*
|
|
@@ -6317,7 +7040,11 @@ var SCROLL_PRIMITIVES = [
|
|
|
6317
7040
|
}),
|
|
6318
7041
|
scrollPrimitive({
|
|
6319
7042
|
id: "scroll-snap",
|
|
6320
|
-
|
|
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],
|
|
6321
7048
|
parameters: {
|
|
6322
7049
|
axis: { type: "keyword", default: "y", cssProperty: "--kui-axis", keywords: ["x", "y"] },
|
|
6323
7050
|
strictness: {
|
|
@@ -6659,7 +7386,7 @@ function findBars(el) {
|
|
|
6659
7386
|
return children.filter((child) => child.textContent.trim() === "");
|
|
6660
7387
|
}
|
|
6661
7388
|
function withIconParts(inner) {
|
|
6662
|
-
return (el, params, ctx) => {
|
|
7389
|
+
return inheritTimingContract(inner, (el, params, ctx) => {
|
|
6663
7390
|
if (!el.querySelector(".kui-bar")) {
|
|
6664
7391
|
const bars = findBars(el);
|
|
6665
7392
|
if (bars.length > 0) {
|
|
@@ -6678,7 +7405,7 @@ function withIconParts(inner) {
|
|
|
6678
7405
|
}
|
|
6679
7406
|
}
|
|
6680
7407
|
return inner(el, params, ctx);
|
|
6681
|
-
};
|
|
7408
|
+
});
|
|
6682
7409
|
}
|
|
6683
7410
|
|
|
6684
7411
|
// src/effects/svg/index.ts
|
|
@@ -6717,18 +7444,18 @@ function prepareMorph(el, params, ctx) {
|
|
|
6717
7444
|
};
|
|
6718
7445
|
let current = 0;
|
|
6719
7446
|
const enter = () => drive(1);
|
|
6720
|
-
const
|
|
7447
|
+
const leave2 = () => drive(0);
|
|
6721
7448
|
el.addEventListener("pointerenter", enter, { passive: true });
|
|
6722
7449
|
el.addEventListener("focusin", enter, { passive: true });
|
|
6723
|
-
el.addEventListener("pointerleave",
|
|
6724
|
-
el.addEventListener("focusout",
|
|
7450
|
+
el.addEventListener("pointerleave", leave2, { passive: true });
|
|
7451
|
+
el.addEventListener("focusout", leave2, { passive: true });
|
|
6725
7452
|
return () => {
|
|
6726
7453
|
cancelled = true;
|
|
6727
7454
|
cancelAnimationFrame(frame);
|
|
6728
7455
|
el.removeEventListener("pointerenter", enter);
|
|
6729
7456
|
el.removeEventListener("focusin", enter);
|
|
6730
|
-
el.removeEventListener("pointerleave",
|
|
6731
|
-
el.removeEventListener("focusout",
|
|
7457
|
+
el.removeEventListener("pointerleave", leave2);
|
|
7458
|
+
el.removeEventListener("focusout", leave2);
|
|
6732
7459
|
path2.setAttribute("d", startPath);
|
|
6733
7460
|
};
|
|
6734
7461
|
}
|
|
@@ -6749,7 +7476,16 @@ var LOGO_BUILD_PRIMITIVE = cssPrimitive("logo-assemble", [
|
|
|
6749
7476
|
var ICON_TOGGLE_PRIMITIVE = {
|
|
6750
7477
|
id: "icon-toggle",
|
|
6751
7478
|
renderer: "javascript",
|
|
6752
|
-
|
|
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
|
+
],
|
|
6753
7489
|
parameters: {
|
|
6754
7490
|
duration: { type: "time", default: "260ms", cssProperty: "--kui-duration" },
|
|
6755
7491
|
// The state flip is the start moment: `aria-expanded` goes true and the bars begin to move.
|
|
@@ -6947,8 +7683,10 @@ var CARD_TOGGLE_PRIMITIVE = {
|
|
|
6947
7683
|
// `'discrete'` alongside `rotate`: the unconditional `[data-kui-fx~='flip-card']` rule pins
|
|
6948
7684
|
// `display: grid` so the two faces stack in one cell instead of flowing as normal block
|
|
6949
7685
|
// siblings — unrelated to `catalog/discrete.ts`'s show/hide use of the same physical property,
|
|
6950
|
-
// but `display` is tracked as one channel regardless of the value written into it.
|
|
6951
|
-
|
|
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],
|
|
6952
7690
|
parameters: {
|
|
6953
7691
|
duration: { type: "time", default: "700ms", cssProperty: "--kui-duration" },
|
|
6954
7692
|
// The turn has a start moment — the click, or the pointer arriving — so a delay before it is
|
|
@@ -7086,8 +7824,8 @@ function projectRelease(position, velocityPxPerSec, travelPx) {
|
|
|
7086
7824
|
function snapTo(projected) {
|
|
7087
7825
|
return Math.round(projected) + 0;
|
|
7088
7826
|
}
|
|
7089
|
-
function wrapPlace(
|
|
7090
|
-
return total > 0 ? (
|
|
7827
|
+
function wrapPlace(place2, total) {
|
|
7828
|
+
return total > 0 ? (place2 % total + total) % total : 0;
|
|
7091
7829
|
}
|
|
7092
7830
|
var KEY_DELTAS = {
|
|
7093
7831
|
ArrowRight: 1,
|
|
@@ -7099,8 +7837,26 @@ var KEY_DELTAS = {
|
|
|
7099
7837
|
Home: null,
|
|
7100
7838
|
End: null
|
|
7101
7839
|
};
|
|
7102
|
-
function
|
|
7103
|
-
|
|
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) {
|
|
7104
7860
|
let suppressClick = false;
|
|
7105
7861
|
const onClickCapture = (event) => {
|
|
7106
7862
|
if (!suppressClick) return;
|
|
@@ -7108,19 +7864,49 @@ function createRingDrag(request) {
|
|
|
7108
7864
|
event.stopPropagation();
|
|
7109
7865
|
event.preventDefault();
|
|
7110
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);
|
|
7111
7893
|
const stopRecognising = enabled ? recognise(
|
|
7112
7894
|
el,
|
|
7113
7895
|
{
|
|
7114
7896
|
onStart() {
|
|
7897
|
+
dragFrom = positionOf();
|
|
7898
|
+
sign = request.travelSign();
|
|
7899
|
+
selection.hold();
|
|
7115
7900
|
setDragging(true);
|
|
7116
7901
|
},
|
|
7117
7902
|
onMove(vector) {
|
|
7118
|
-
moveTo(
|
|
7903
|
+
moveTo(dragFrom - sign * vector.dx / travelPx);
|
|
7119
7904
|
},
|
|
7120
7905
|
onEnd(vector) {
|
|
7906
|
+
selection.release();
|
|
7121
7907
|
setDragging(false);
|
|
7122
|
-
|
|
7123
|
-
const settled = snapTo(projectRelease(positionOf(), vector.vx, travelPx));
|
|
7908
|
+
clicks.arm();
|
|
7909
|
+
const settled = snapTo(projectRelease(positionOf(), sign * vector.vx, travelPx));
|
|
7124
7910
|
moveTo(wrapPlace(settled, total()));
|
|
7125
7911
|
}
|
|
7126
7912
|
},
|
|
@@ -7129,7 +7915,15 @@ function createRingDrag(request) {
|
|
|
7129
7915
|
// threshold than `recognise`'s own default of 4px is what buys that — a ring occupies a
|
|
7130
7916
|
// large slab of a phone screen, and a scroll that begins with a two-pixel horizontal wobble
|
|
7131
7917
|
// should not capture the pointer away from the page.
|
|
7132
|
-
|
|
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" }
|
|
7133
7927
|
) : () => {
|
|
7134
7928
|
};
|
|
7135
7929
|
if (enabled) ctx.style.set("touch-action", "pan-y");
|
|
@@ -7148,216 +7942,213 @@ function createRingDrag(request) {
|
|
|
7148
7942
|
const grantedFocus = el.getAttribute("tabindex") === null;
|
|
7149
7943
|
if (grantedFocus) el.setAttribute("tabindex", "0");
|
|
7150
7944
|
el.addEventListener("keydown", onKeyDown);
|
|
7151
|
-
el.addEventListener("click", onClickCapture, true);
|
|
7152
7945
|
return () => {
|
|
7153
7946
|
stopRecognising();
|
|
7947
|
+
selection.release();
|
|
7154
7948
|
el.removeEventListener("keydown", onKeyDown);
|
|
7155
|
-
|
|
7949
|
+
clicks.release();
|
|
7156
7950
|
if (grantedFocus) el.removeAttribute("tabindex");
|
|
7157
7951
|
};
|
|
7158
7952
|
}
|
|
7159
7953
|
|
|
7160
|
-
// src/effects/carousel/
|
|
7954
|
+
// src/effects/carousel/deck.ts
|
|
7161
7955
|
var DRAGGING_ATTR = "data-kui-ring-dragging";
|
|
7162
|
-
var
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
["opacity", (value) => value !== "" && Number(value) < 1],
|
|
7168
|
-
["filter", (value) => value !== "" && value !== "none"],
|
|
7169
|
-
["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")
|
|
7170
7961
|
];
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
if (found) {
|
|
7177
|
-
ctx.warn(
|
|
7178
|
-
`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.`
|
|
7179
|
-
);
|
|
7180
|
-
return;
|
|
7181
|
-
}
|
|
7182
|
-
node = node.parentElement;
|
|
7183
|
-
}
|
|
7184
|
-
}
|
|
7185
|
-
function flatteningDeclaration(node, ctx) {
|
|
7186
|
-
const style = ctx.win.getComputedStyle?.(node);
|
|
7187
|
-
if (!style) return null;
|
|
7188
|
-
for (const [property2, flattens] of FLATTENING) {
|
|
7189
|
-
const value = style.getPropertyValue(property2);
|
|
7190
|
-
if (flattens(value)) return { property: property2, value };
|
|
7191
|
-
}
|
|
7192
|
-
return null;
|
|
7193
|
-
}
|
|
7194
|
-
function ledgersFor(store, node) {
|
|
7195
|
-
let attributes = store.attributes.get(node);
|
|
7196
|
-
if (!attributes) {
|
|
7197
|
-
attributes = createAttributeLedger(node);
|
|
7198
|
-
store.attributes.set(node, attributes);
|
|
7199
|
-
}
|
|
7200
|
-
let styles = store.styles.get(node);
|
|
7201
|
-
if (!styles) {
|
|
7202
|
-
styles = createStyleLedger(node);
|
|
7203
|
-
store.styles.set(node, styles);
|
|
7204
|
-
}
|
|
7205
|
-
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;
|
|
7206
7967
|
}
|
|
7207
|
-
function
|
|
7208
|
-
|
|
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;
|
|
7209
7972
|
}
|
|
7210
|
-
function
|
|
7211
|
-
|
|
7212
|
-
return wrapped > 180 ? wrapped - 360 : wrapped;
|
|
7973
|
+
function offsetWrapped(previous, next, count) {
|
|
7974
|
+
return Math.abs(next - previous) > count / 2;
|
|
7213
7975
|
}
|
|
7214
7976
|
function snapPosition(position, total) {
|
|
7215
7977
|
if (total <= 0) return { step: 0, drift: 0 };
|
|
7216
7978
|
const nearest = Math.round(position);
|
|
7217
7979
|
return { step: (nearest % total + total) % total, drift: position - nearest };
|
|
7218
7980
|
}
|
|
7219
|
-
var
|
|
7981
|
+
var DECK_PARAMETERS = {
|
|
7220
7982
|
duration: { type: "time", default: "620ms", cssProperty: "--kui-duration" },
|
|
7221
7983
|
...TRIGGER_DELAY_PARAM,
|
|
7222
7984
|
ease: { type: "easing", default: "cubic-bezier(0.22, 1, 0.36, 1)", cssProperty: "--kui-ease" },
|
|
7223
|
-
|
|
7224
|
-
* A real CSS `<angle>`, not a normalised scalar.
|
|
7225
|
-
*
|
|
7226
|
-
* A designer specifies fifteen degrees; nobody specifies 0.375 of an unstated maximum. A
|
|
7227
|
-
* normalised knob also bakes taste into a coordinate — the maximum *is* an opinion about how much
|
|
7228
|
-
* tilt is too much, expressed as a unit — and it cannot be typed in the spelling the rest of the
|
|
7229
|
-
* attribute grammar uses (`0.05turn` is a legal angle and means something exact). Legibility is
|
|
7230
|
-
* still bounded, but the bound is applied to the angle, in `carousel.css`, where it is one
|
|
7231
|
-
* `clamp()` an author can see in devtools rather than a hidden rescale.
|
|
7232
|
-
*
|
|
7233
|
-
* Sign convention: **positive tilts the camera up and over the ring**, looking down at it. That
|
|
7234
|
-
* is the opposite sign from the `rotateX()` it compiles to — see `carousel.css` for the
|
|
7235
|
-
* derivation — and the inversion is deliberate: "18 degrees above" is the sentence a designer
|
|
7236
|
-
* says, and it should not be spelled `-18deg` because of which way a matrix happens to turn.
|
|
7237
|
-
*/
|
|
7238
|
-
tilt: { type: "angle", default: "0deg", cssProperty: "--kui-tilt" },
|
|
7239
|
-
/*
|
|
7240
|
-
* How far the ring spreads. `360deg` is a full ring; anything less is a slice.
|
|
7241
|
-
*
|
|
7242
|
-
* This is the parameter the concave name exists around. From the centre of a *full* ring most of
|
|
7243
|
-
* it is behind your head, so a 360-degree spread there is not a design choice but a way of hiding
|
|
7244
|
-
* two thirds of the content — the inside name defaults to a slice for that reason.
|
|
7245
|
-
*/
|
|
7246
|
-
arc: { type: "angle", default: "360deg", cssProperty: "--kui-arc" },
|
|
7247
|
-
/*
|
|
7248
|
-
* The ring's radius, or unset for one derived from the content.
|
|
7249
|
-
*
|
|
7250
|
-
* Empty default, not a number, and for the same reason `step-progress`'s `steps` is empty:
|
|
7251
|
-
* `readParams` fills every declared default in unconditionally, so an empty one is the only way
|
|
7252
|
-
* to tell "the author said nothing" from "the author said 320px". Unset, `carousel.css` computes
|
|
7253
|
-
* the circumradius of a regular polygon with this many sides and this much side length —
|
|
7254
|
-
* `(width + gap) / (2 * tan(step / 2))` — which is the arrangement where neighbours just touch.
|
|
7255
|
-
* Every consumer solving that by hand was the alternative, and they would all have solved it
|
|
7256
|
-
* differently.
|
|
7257
|
-
*
|
|
7258
|
-
* The width half of that has to come from JavaScript: CSS can compute with a measurement but
|
|
7259
|
-
* cannot take one. See `measureItemWidth`.
|
|
7260
|
-
*/
|
|
7261
|
-
radius: { type: "length", default: "", cssProperty: "--kui-radius" },
|
|
7262
|
-
/** Breathing room between neighbours, spent by the derived radius above. */
|
|
7263
|
-
gap: { type: "length", default: "24px", cssProperty: "--kui-gap" },
|
|
7264
|
-
/** Camera distance. Shorter is a wider lens: more foreshortening, more drama, more distortion. */
|
|
7265
|
-
perspective: { type: "length", default: "1600px", cssProperty: "--kui-perspective" },
|
|
7266
|
-
/*
|
|
7267
|
-
* Whether a slot keeps its own outward orientation, or turns to keep facing the viewer.
|
|
7268
|
-
*
|
|
7269
|
-
* `radial` is the ring's natural state and needs nothing: a slot placed by
|
|
7270
|
-
* `rotateY(θ) translateZ(R)` already faces outward along its own radius, which is why the live
|
|
7271
|
-
* one faces you. It is what you want for a cover-flow, and it is what makes the far side of the
|
|
7272
|
-
* ring read as the far side.
|
|
7273
|
-
*
|
|
7274
|
-
* `camera` counter-rotates each slot's *child* — never the slot itself; the placement transform
|
|
7275
|
-
* and the billboard have to stay on separate boxes or every later change to one silently
|
|
7276
|
-
* multiplies into the other — so text stays readable at every position, which is what a raised
|
|
7277
|
-
* camera needs. Costs one element of markup per slide; see `docs/catalog.md`.
|
|
7278
|
-
*/
|
|
7279
|
-
facing: {
|
|
7280
|
-
type: "keyword",
|
|
7281
|
-
default: "radial",
|
|
7282
|
-
cssProperty: "--kui-facing",
|
|
7283
|
-
keywords: ["radial", "camera"]
|
|
7284
|
-
},
|
|
7285
|
-
/** Whether a pointer can spin the ring by hand. */
|
|
7985
|
+
/** Whether a pointer can move the deck by hand. */
|
|
7286
7986
|
grab: { type: "keyword", default: "true", cssProperty: "--kui-grab", keywords: ["true", "false"] },
|
|
7287
7987
|
/*
|
|
7288
|
-
* How far the pointer travels, in pixels, to move the
|
|
7988
|
+
* How far the pointer travels, in pixels, to move the deck one place.
|
|
7289
7989
|
*
|
|
7290
7990
|
* A pixels-per-step mapping rather than pixels-per-degree, because a step is the unit everything
|
|
7291
7991
|
* else here is in — the index, the offset, the snap — and a degree is not: the same drag would
|
|
7292
7992
|
* move a six-item ring one place and a sixty-item ring ten, purely because the spacing changed.
|
|
7293
7993
|
*/
|
|
7294
7994
|
travel: { type: "number", default: "220", cssProperty: "--kui-travel", finite: true, minimum: 1 },
|
|
7295
|
-
/** Which elements
|
|
7995
|
+
/** Which elements are the slides. Unset means this element's own children. */
|
|
7296
7996
|
target: { type: "text", default: "", cssProperty: "--kui-target" },
|
|
7297
7997
|
/** Optional controls, resolved exactly as `step-progress` resolves its own. */
|
|
7298
7998
|
next: { type: "text", default: "", cssProperty: "--kui-next" },
|
|
7299
7999
|
prev: { type: "text", default: "", cssProperty: "--kui-prev" },
|
|
7300
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,
|
|
8005
|
+
/*
|
|
8006
|
+
* Continuous rotation: how long one full cycle of the deck takes. `0s` is off.
|
|
8007
|
+
*
|
|
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.
|
|
8014
|
+
*/
|
|
8015
|
+
spin: { type: "time", default: "0s", cssProperty: "--kui-spin" },
|
|
8016
|
+
/*
|
|
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`).
|
|
8019
|
+
*/
|
|
8020
|
+
autoplay: AUTOPLAY_PARAM,
|
|
8021
|
+
/*
|
|
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.
|
|
8026
|
+
*/
|
|
8027
|
+
lightbox: DECK_LIGHTBOX_PARAM,
|
|
7301
8028
|
scope: SCOPE_PARAM
|
|
7302
8029
|
};
|
|
7303
|
-
function
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
}
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
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);
|
|
7314
8048
|
const scope = scopeParam(params, "self");
|
|
7315
8049
|
const resolveSlots = () => selector ? queryScoped(el, ctx, selector, scope) : el.children;
|
|
7316
|
-
const marker = createStepMarker(resolveSlots, (message) => ctx.warn(
|
|
8050
|
+
const marker = createStepMarker(resolveSlots, (message) => ctx.warn(`${label} ${message}`));
|
|
7317
8051
|
const total = () => countSteps(params, resolveSlots);
|
|
7318
8052
|
const hostAttributes = createAttributeLedger(el);
|
|
7319
8053
|
const hostStyles = createStyleLedger(el);
|
|
7320
|
-
|
|
7321
|
-
const slots = { attributes: /* @__PURE__ */ new Map()
|
|
7322
|
-
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);
|
|
7323
8057
|
let position = 0;
|
|
7324
|
-
|
|
7325
|
-
const
|
|
7326
|
-
|
|
7327
|
-
const
|
|
7328
|
-
|
|
7329
|
-
|
|
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) => {
|
|
7330
8081
|
hostStyles.set("--kui-step-position", (step + drift2).toFixed(4));
|
|
8082
|
+
};
|
|
8083
|
+
const render = () => {
|
|
8084
|
+
const count = total();
|
|
8085
|
+
const { step, drift: drift2 } = snapPosition(position, count);
|
|
8086
|
+
hostAttributes.set("data-kui-step", String(step));
|
|
8087
|
+
hostStyles.set("--kui-step", String(step));
|
|
8088
|
+
hostStyles.set("--kui-item-count", String(count));
|
|
8089
|
+
publishPosition(step, drift2);
|
|
7331
8090
|
marker.mark(step);
|
|
7332
|
-
const
|
|
7333
|
-
if (
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
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;
|
|
7339
8103
|
}
|
|
8104
|
+
publishPosition(step, drift2);
|
|
8105
|
+
markFaces(rendered.nodes, rendered.count, drift2);
|
|
7340
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
|
+
});
|
|
7341
8127
|
const goTo = (next) => {
|
|
8128
|
+
motion.interrupt();
|
|
7342
8129
|
position = next;
|
|
7343
8130
|
render();
|
|
7344
8131
|
};
|
|
7345
|
-
const
|
|
7346
|
-
const
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
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);
|
|
7351
8149
|
}
|
|
7352
|
-
|
|
7353
|
-
return true;
|
|
8150
|
+
return travelSign(centres.get(0) ?? 0, centres.get(1) ?? null, centres.get(-1) ?? null);
|
|
7354
8151
|
};
|
|
7355
|
-
bindControl("next", () => goTo(nextStep(snapPosition(position, total()).step, total())));
|
|
7356
|
-
bindControl("prev", () => goTo(prevStep(snapPosition(position, total()).step, total())));
|
|
7357
|
-
bindControl("jump", (_node, at) => {
|
|
7358
|
-
if (at >= 0) goTo(at);
|
|
7359
|
-
});
|
|
7360
|
-
const releaseControls = delegateControls({ el, ctx, scope, groups });
|
|
7361
8152
|
const releaseDrag = createRingDrag({
|
|
7362
8153
|
el,
|
|
7363
8154
|
ctx,
|
|
@@ -7365,22 +8156,405 @@ function prepareSpatialRing(el, params, ctx) {
|
|
|
7365
8156
|
travelPx: params.num("travel", 220),
|
|
7366
8157
|
total,
|
|
7367
8158
|
positionOf: () => position,
|
|
8159
|
+
travelSign: screenTravel,
|
|
7368
8160
|
moveTo: goTo,
|
|
7369
|
-
setDragging: (dragging) =>
|
|
8161
|
+
setDragging: (dragging) => {
|
|
8162
|
+
hostAttributes.set(DRAGGING_ATTR, String(dragging));
|
|
8163
|
+
motion.hold(dragging);
|
|
8164
|
+
}
|
|
7370
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
|
+
);
|
|
7371
8171
|
render();
|
|
7372
8172
|
return () => {
|
|
8173
|
+
releaseViewer();
|
|
8174
|
+
releaseMotion();
|
|
7373
8175
|
releaseDrag();
|
|
7374
8176
|
releaseControls();
|
|
7375
8177
|
marker.restore();
|
|
7376
8178
|
for (const ledger of slots.attributes.values()) ledger.restore();
|
|
7377
|
-
for (const ledger of slots.styles.values()) ledger.restore();
|
|
7378
8179
|
slots.attributes.clear();
|
|
7379
|
-
|
|
8180
|
+
attachment?.release();
|
|
7380
8181
|
hostAttributes.restore();
|
|
7381
8182
|
hostStyles.restore();
|
|
7382
8183
|
};
|
|
7383
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
|
+
}
|
|
7384
8558
|
function degreesOf(value, fallbackDeg) {
|
|
7385
8559
|
const match = /^(-?[\d.]+)(deg|rad|turn|grad)?$/i.exec(value.trim());
|
|
7386
8560
|
if (!match) return fallbackDeg;
|
|
@@ -7400,7 +8574,15 @@ function degreesOf(value, fallbackDeg) {
|
|
|
7400
8574
|
var SPATIAL_RING_PRIMITIVE = {
|
|
7401
8575
|
id: "spatial-ring",
|
|
7402
8576
|
renderer: "javascript",
|
|
7403
|
-
|
|
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
|
+
],
|
|
7404
8586
|
parameters: RING_PARAMETERS,
|
|
7405
8587
|
supportedTimelines: ["time"],
|
|
7406
8588
|
// `load`, not `enter`: a carousel that only wires its arrows once scrolled into view is broken,
|
|
@@ -7411,7 +8593,7 @@ var SPATIAL_RING_PRIMITIVE = {
|
|
|
7411
8593
|
reducedMotion: "shorten",
|
|
7412
8594
|
prepare: deferPrepare(prepareSpatialRing)
|
|
7413
8595
|
};
|
|
7414
|
-
var CAROUSEL_PRIMITIVES = [SPATIAL_RING_PRIMITIVE];
|
|
8596
|
+
var CAROUSEL_PRIMITIVES = [SPATIAL_RING_PRIMITIVE, SPATIAL_STACK_PRIMITIVE];
|
|
7415
8597
|
var CAROUSEL_PRESETS = [
|
|
7416
8598
|
{
|
|
7417
8599
|
name: "carousel-3d",
|
|
@@ -7448,10 +8630,31 @@ var CAROUSEL_PRESETS = [
|
|
|
7448
8630
|
params: { arc: "120deg", facing: "camera", perspective: "900px" },
|
|
7449
8631
|
requiresOwnSubtree: true,
|
|
7450
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"
|
|
7451
8654
|
}
|
|
7452
8655
|
];
|
|
7453
8656
|
function registerCarousel(registry) {
|
|
7454
|
-
return registry.registerPrimitives(CAROUSEL_PRIMITIVES).registerPresets(CAROUSEL_PRESETS);
|
|
8657
|
+
return registry.registerPrimitives(CAROUSEL_PRIMITIVES).registerPresets(CAROUSEL_PRESETS).registerPresets(STACK_PRESETS);
|
|
7455
8658
|
}
|
|
7456
8659
|
|
|
7457
8660
|
// src/effects/tween/properties.ts
|
|
@@ -8007,7 +9210,10 @@ var deviceFrameParams = {
|
|
|
8007
9210
|
keywords: [...DEVICE_FRAME_KINDS]
|
|
8008
9211
|
},
|
|
8009
9212
|
color: { type: "color", default: "#111", cssProperty: "--kui-device-color" },
|
|
8010
|
-
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" }
|
|
8011
9217
|
};
|
|
8012
9218
|
function prepareDeviceFrame(el, params) {
|
|
8013
9219
|
const state = createAttributeLedger(el);
|
|
@@ -8020,8 +9226,9 @@ var DEVICE_FRAME_PRIMITIVE = widgetPrimitive(
|
|
|
8020
9226
|
// Exclusive ownership of the frame chrome. Its static box-model declarations are outside
|
|
8021
9227
|
// the channel-property audit, as they are for the existing catalog. The chrome also reaches
|
|
8022
9228
|
// into the direct media child (`object-fit: cover` etc.); that is a `requiresOwnSubtree`
|
|
8023
|
-
// question, as with the other showcase widgets.
|
|
8024
|
-
|
|
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")],
|
|
8025
9232
|
parameters: deviceFrameParams,
|
|
8026
9233
|
// `'layout'`: the bezel's `border`+`padding` are box-model properties added once, at
|
|
8027
9234
|
// activation, which is a real (if one-time) layout — not the "transform/opacity only" budget
|
|
@@ -8056,10 +9263,12 @@ function lock(state) {
|
|
|
8056
9263
|
state.lock = ledger;
|
|
8057
9264
|
}
|
|
8058
9265
|
function clearContent(state) {
|
|
8059
|
-
state.current
|
|
9266
|
+
const closing = state.current;
|
|
9267
|
+
closing?.onClose?.();
|
|
8060
9268
|
state.current = void 0;
|
|
8061
9269
|
state.currentOwner = void 0;
|
|
8062
9270
|
state.slot?.replaceChildren();
|
|
9271
|
+
if (closing?.opener) announceViewer(state.doc, closing.opener, false);
|
|
8063
9272
|
}
|
|
8064
9273
|
function onClose(state) {
|
|
8065
9274
|
state.dialog?.classList.remove("is-open");
|
|
@@ -8078,11 +9287,9 @@ function dismiss(state) {
|
|
|
8078
9287
|
}, state.current.reducedMotion ? 0 : state.current.duration);
|
|
8079
9288
|
}
|
|
8080
9289
|
function clickedOutside(state, target) {
|
|
8081
|
-
const
|
|
8082
|
-
if (
|
|
8083
|
-
|
|
8084
|
-
}
|
|
8085
|
-
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");
|
|
8086
9293
|
}
|
|
8087
9294
|
function forceReflow(element) {
|
|
8088
9295
|
return element.offsetWidth;
|
|
@@ -8132,6 +9339,7 @@ function open(state, content, owner) {
|
|
|
8132
9339
|
dialog.scrollTop = 0;
|
|
8133
9340
|
forceReflow(dialog);
|
|
8134
9341
|
dialog.classList.add("is-open");
|
|
9342
|
+
if (content.opener) announceViewer(state.doc, content.opener, true);
|
|
8135
9343
|
}
|
|
8136
9344
|
function closeNow(state) {
|
|
8137
9345
|
if (state.timer !== void 0) clearTimeout(state.timer);
|
|
@@ -8211,25 +9419,31 @@ function resolveMediaSource(href) {
|
|
|
8211
9419
|
}
|
|
8212
9420
|
|
|
8213
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)";
|
|
8214
9425
|
var paramsSchema = {
|
|
8215
|
-
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"] },
|
|
8216
9427
|
target: { type: "text", default: "", cssProperty: "--kui-target" },
|
|
8217
9428
|
scope: SCOPE_PARAM,
|
|
8218
|
-
scale: { type: "number", default:
|
|
8219
|
-
duration: { type: "time", default:
|
|
8220
|
-
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" },
|
|
8221
9432
|
loop: { type: "keyword", default: "true", cssProperty: "--kui-lightbox-loop", keywords: ["true", "false"] },
|
|
8222
9433
|
aspect: { type: "keyword", default: "", cssProperty: "--kui-lightbox-aspect", keywords: ["wide", "tall", "square"] },
|
|
8223
9434
|
caption: { type: "keyword", default: "figcaption", cssProperty: "--kui-lightbox-caption", keywords: ["figcaption", "alt", "title", "none"] }
|
|
8224
9435
|
};
|
|
8225
9436
|
function defaultTargets(el, media) {
|
|
8226
|
-
if (el.matches("a[href]") || media
|
|
9437
|
+
if (el.matches("a[href]") || media !== "video" && el.matches("img")) return [el];
|
|
8227
9438
|
return [...el.querySelectorAll(media === "video" ? ":scope a[href]" : ":scope a[href], :scope img")];
|
|
8228
9439
|
}
|
|
9440
|
+
function effectName(media) {
|
|
9441
|
+
return media === "video" ? "video-lightbox" : "lightbox";
|
|
9442
|
+
}
|
|
8229
9443
|
function targets(el, params, ctx, media) {
|
|
8230
9444
|
const authored = params.text("target");
|
|
8231
9445
|
if (!authored) return defaultTargets(el, media);
|
|
8232
|
-
const selector = resolveTarget(authored, ctx, media
|
|
9446
|
+
const selector = resolveTarget(authored, ctx, effectName(media));
|
|
8233
9447
|
return selector ? queryScoped(el, ctx, selector, scopeParam(params, "self")) : [];
|
|
8234
9448
|
}
|
|
8235
9449
|
function imageTrigger(target) {
|
|
@@ -8240,52 +9454,137 @@ function imageIn(trigger) {
|
|
|
8240
9454
|
}
|
|
8241
9455
|
function captionFor(trigger, image, source) {
|
|
8242
9456
|
if (source === "none") return "";
|
|
8243
|
-
|
|
8244
|
-
if (source === "
|
|
8245
|
-
|
|
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();
|
|
8246
9464
|
}
|
|
8247
9465
|
function imageItem(trigger, captionSource) {
|
|
8248
9466
|
const image = imageIn(trigger);
|
|
8249
9467
|
if (!image) return null;
|
|
8250
9468
|
const link = trigger instanceof HTMLAnchorElement ? trigger : null;
|
|
8251
9469
|
return {
|
|
9470
|
+
kind: "image",
|
|
8252
9471
|
trigger,
|
|
8253
9472
|
image,
|
|
8254
9473
|
src: link?.href ?? (image.currentSrc || image.src),
|
|
8255
9474
|
caption: captionFor(trigger, image, captionSource)
|
|
8256
9475
|
};
|
|
8257
9476
|
}
|
|
8258
|
-
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) {
|
|
8259
9494
|
const seen = /* @__PURE__ */ new Set();
|
|
8260
|
-
const
|
|
8261
|
-
for (const target of
|
|
8262
|
-
const trigger = imageTrigger(target);
|
|
9495
|
+
const triggers = [];
|
|
9496
|
+
for (const target of found) {
|
|
9497
|
+
const trigger = media === "video" ? target : imageTrigger(target);
|
|
8263
9498
|
if (seen.has(trigger)) continue;
|
|
8264
|
-
|
|
8265
|
-
if (!item) continue;
|
|
9499
|
+
if (!usable(trigger, media)) continue;
|
|
8266
9500
|
seen.add(trigger);
|
|
8267
|
-
|
|
9501
|
+
triggers.push(trigger);
|
|
8268
9502
|
}
|
|
8269
|
-
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;
|
|
8270
9510
|
}
|
|
8271
9511
|
function modalOptions(params, ctx) {
|
|
8272
9512
|
return {
|
|
8273
|
-
duration: params.timing.durationMs ?? params.ms("duration",
|
|
8274
|
-
scale: params.text("scale",
|
|
8275
|
-
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),
|
|
8276
9516
|
reducedMotion: ctx.reducedMotion
|
|
8277
9517
|
};
|
|
8278
9518
|
}
|
|
8279
|
-
function showImage(item, view
|
|
9519
|
+
function showImage(item, view) {
|
|
8280
9520
|
view.src = item.src;
|
|
8281
9521
|
view.alt = item.image.alt;
|
|
8282
|
-
caption.textContent = item.caption;
|
|
8283
|
-
caption.hidden = item.caption === "";
|
|
8284
9522
|
const ratio = item.image.naturalWidth ? item.image.naturalHeight / item.image.naturalWidth : 1;
|
|
8285
9523
|
view.className = ratio > 1.5 ? "is-tall" : "is-fit";
|
|
8286
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
|
+
}
|
|
8287
9578
|
function renderGallery(state, focus) {
|
|
8288
|
-
|
|
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
|
+
}
|
|
8289
9588
|
state.counter.textContent = `${state.index + 1} of ${state.items.length}`;
|
|
8290
9589
|
state.previous.disabled = !state.loop && state.index === 0;
|
|
8291
9590
|
state.next.disabled = !state.loop && state.index === state.items.length - 1;
|
|
@@ -8303,6 +9602,7 @@ function clickGallery(state, delta, button2) {
|
|
|
8303
9602
|
}
|
|
8304
9603
|
function galleryKey(state, event) {
|
|
8305
9604
|
if (state.items.length < 2) return;
|
|
9605
|
+
if (event.target.closest("video")) return;
|
|
8306
9606
|
if (event.key === "ArrowLeft") moveGallery(state, -1, true);
|
|
8307
9607
|
else if (event.key === "ArrowRight") moveGallery(state, 1, true);
|
|
8308
9608
|
else if (event.key === "Home") {
|
|
@@ -8314,8 +9614,40 @@ function galleryKey(state, event) {
|
|
|
8314
9614
|
} else return;
|
|
8315
9615
|
event.preventDefault();
|
|
8316
9616
|
}
|
|
8317
|
-
function
|
|
8318
|
-
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;
|
|
8319
9651
|
const box = doc.createElement("div");
|
|
8320
9652
|
box.className = "kui-lightbox-gallery";
|
|
8321
9653
|
const figure = doc.createElement("figure");
|
|
@@ -8323,7 +9655,6 @@ function galleryContent(items, initial, loop2, options) {
|
|
|
8323
9655
|
const view = doc.createElement("img");
|
|
8324
9656
|
view.decoding = "async";
|
|
8325
9657
|
const caption = doc.createElement("figcaption");
|
|
8326
|
-
figure.append(view, caption);
|
|
8327
9658
|
box.append(figure);
|
|
8328
9659
|
const counter = doc.createElement("span");
|
|
8329
9660
|
counter.className = "kui-lightbox-counter";
|
|
@@ -8333,149 +9664,244 @@ function galleryContent(items, initial, loop2, options) {
|
|
|
8333
9664
|
previous.type = next.type = "button";
|
|
8334
9665
|
previous.textContent = "\u2039";
|
|
8335
9666
|
next.textContent = "\u203A";
|
|
8336
|
-
|
|
8337
|
-
|
|
9667
|
+
const { noun, label } = vocabulary(items);
|
|
9668
|
+
previous.setAttribute("aria-label", `Previous ${noun}`);
|
|
9669
|
+
next.setAttribute("aria-label", `Next ${noun}`);
|
|
8338
9670
|
previous.className = "kui-lightbox-prev";
|
|
8339
9671
|
next.className = "kui-lightbox-next";
|
|
8340
|
-
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
|
+
};
|
|
8341
9685
|
if (items.length > 1) {
|
|
8342
9686
|
box.append(previous, next, counter);
|
|
8343
9687
|
previous.addEventListener("click", () => clickGallery(state, -1, previous));
|
|
8344
9688
|
next.addEventListener("click", () => clickGallery(state, 1, next));
|
|
8345
9689
|
}
|
|
8346
9690
|
renderGallery(state, false);
|
|
9691
|
+
const unswipe = items.length > 1 ? swipeGallery(state, box) : void 0;
|
|
9692
|
+
const { duration, scale, ease, reducedMotion } = options;
|
|
8347
9693
|
return {
|
|
8348
|
-
|
|
9694
|
+
duration,
|
|
9695
|
+
scale,
|
|
9696
|
+
ease,
|
|
9697
|
+
reducedMotion,
|
|
8349
9698
|
node: box,
|
|
8350
|
-
label
|
|
8351
|
-
|
|
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
|
+
},
|
|
8352
9706
|
onKey: (event) => galleryKey(state, event)
|
|
8353
9707
|
};
|
|
8354
9708
|
}
|
|
8355
|
-
function stopMedia(frame) {
|
|
8356
|
-
const video = frame.querySelector("video");
|
|
8357
|
-
if (video) {
|
|
8358
|
-
if (video.readyState > 0) video.pause();
|
|
8359
|
-
video.removeAttribute("src");
|
|
8360
|
-
}
|
|
8361
|
-
frame.replaceChildren();
|
|
8362
|
-
}
|
|
8363
|
-
function videoContent(link, source, options, aspect) {
|
|
8364
|
-
const doc = link.ownerDocument;
|
|
8365
|
-
const frame = doc.createElement("div");
|
|
8366
|
-
frame.className = `kui-lightbox-frame kui-lightbox-frame--${aspect}`;
|
|
8367
|
-
const poster = link.querySelector("img");
|
|
8368
|
-
const title = link.title || poster?.getAttribute("alt") || link.getAttribute("aria-label")?.trim() || link.textContent?.trim() || "Video";
|
|
8369
|
-
if (source.kind === "file") {
|
|
8370
|
-
const video = doc.createElement("video");
|
|
8371
|
-
video.src = source.embedUrl;
|
|
8372
|
-
video.controls = true;
|
|
8373
|
-
video.autoplay = true;
|
|
8374
|
-
video.playsInline = true;
|
|
8375
|
-
video.setAttribute("aria-label", title);
|
|
8376
|
-
frame.append(video);
|
|
8377
|
-
} else {
|
|
8378
|
-
const iframe = doc.createElement("iframe");
|
|
8379
|
-
iframe.src = source.embedUrl;
|
|
8380
|
-
iframe.title = title;
|
|
8381
|
-
iframe.allow = "autoplay; encrypted-media; picture-in-picture; fullscreen";
|
|
8382
|
-
iframe.allowFullscreen = true;
|
|
8383
|
-
frame.append(iframe);
|
|
8384
|
-
}
|
|
8385
|
-
return { ...options, node: frame, label: "Video viewer", onClose: () => stopMedia(frame) };
|
|
8386
|
-
}
|
|
8387
9709
|
function primaryClick(event) {
|
|
8388
9710
|
return event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey;
|
|
8389
9711
|
}
|
|
8390
|
-
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) {
|
|
8391
9731
|
const restore = [];
|
|
8392
|
-
|
|
8393
|
-
const bare =
|
|
9732
|
+
for (const trigger of triggers) {
|
|
9733
|
+
const bare = trigger instanceof HTMLImageElement;
|
|
8394
9734
|
if (bare) {
|
|
8395
|
-
const attrs = createAttributeLedger(
|
|
9735
|
+
const attrs = createAttributeLedger(trigger);
|
|
8396
9736
|
attrs.set("role", "button");
|
|
8397
9737
|
attrs.set("tabindex", "0");
|
|
8398
|
-
attrs.set("aria-label",
|
|
9738
|
+
attrs.set("aria-label", trigger.alt ? `Open larger image: ${trigger.alt}` : "Open larger image");
|
|
8399
9739
|
restore.push(() => attrs.restore());
|
|
8400
|
-
|
|
9740
|
+
trigger.addEventListener("keydown", (event) => {
|
|
8401
9741
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
8402
9742
|
event.preventDefault();
|
|
8403
|
-
|
|
9743
|
+
openAt(triggers, trigger, wiring);
|
|
8404
9744
|
}, { signal: wiring.signal });
|
|
8405
9745
|
}
|
|
8406
|
-
|
|
9746
|
+
trigger.addEventListener("click", (event) => {
|
|
8407
9747
|
if (!primaryClick(event)) return;
|
|
8408
|
-
if (
|
|
8409
|
-
wiring.shell.open(galleryContent(items, index, loop2, wiring.options));
|
|
9748
|
+
if (openAt(triggers, trigger, wiring)) event.preventDefault();
|
|
8410
9749
|
}, { signal: wiring.signal });
|
|
8411
|
-
}
|
|
9750
|
+
}
|
|
8412
9751
|
return restore;
|
|
8413
9752
|
}
|
|
8414
|
-
function
|
|
8415
|
-
const
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
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
|
+
};
|
|
8431
9822
|
}
|
|
8432
9823
|
function prepareLightbox(el, params, ctx) {
|
|
8433
9824
|
const media = params.text("media", "image");
|
|
8434
|
-
const
|
|
8435
|
-
if (
|
|
9825
|
+
const triggers = triggersFor(targets(el, params, ctx, media), media);
|
|
9826
|
+
if (triggers.length === 0) return () => {
|
|
8436
9827
|
};
|
|
8437
9828
|
const controller = new AbortController();
|
|
8438
9829
|
const shell = acquireModalShell(ctx.doc);
|
|
8439
|
-
const
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
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
|
+
});
|
|
8447
9840
|
return continuousSetup(() => {
|
|
8448
9841
|
controller.abort();
|
|
8449
9842
|
for (const undo of restore) undo();
|
|
8450
9843
|
shell.release();
|
|
8451
9844
|
});
|
|
8452
9845
|
}
|
|
9846
|
+
var TRIGGERS_CHANNEL = "lightbox:triggers";
|
|
8453
9847
|
var LIGHTBOX_PRIMITIVE = widgetPrimitive(
|
|
8454
9848
|
"lightbox",
|
|
8455
|
-
{ channels: [
|
|
9849
|
+
{ channels: [TRIGGERS_CHANNEL], parameters: paramsSchema, perfClass: "paint" },
|
|
8456
9850
|
withTimingContract("lightbox", { honours: ["duration", "ease"], because: "the dialog opens immediately, so it has no delay phase" }, deferPrepare(prepareLightbox))
|
|
8457
9851
|
);
|
|
8458
9852
|
var LIGHTBOX_PRESETS = [
|
|
8459
9853
|
{ name: "lightbox", primitive: "lightbox", params: { media: "image" } },
|
|
8460
|
-
|
|
9854
|
+
// A video row has never shown captions; `caption:figcaption` opts one in.
|
|
9855
|
+
{ name: "video-lightbox", primitive: "lightbox", params: { media: "video", caption: "none" } }
|
|
8461
9856
|
];
|
|
8462
9857
|
|
|
8463
9858
|
// src/showcase/compare.ts
|
|
8464
9859
|
var COMPARE_AXES = ["x", "y"];
|
|
8465
9860
|
var COMPARE_AXIS_ATTR = "data-kui-compare-axis";
|
|
9861
|
+
var COMPARE_DIVIDERS_ATTR = "data-kui-compare-dividers";
|
|
8466
9862
|
var compareParams = {
|
|
8467
9863
|
position: {
|
|
8468
9864
|
type: "percentage",
|
|
8469
9865
|
default: "50%",
|
|
8470
9866
|
cssProperty: "--kui-compare"
|
|
8471
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" },
|
|
8472
9875
|
axis: {
|
|
8473
9876
|
type: "keyword",
|
|
8474
9877
|
default: "x",
|
|
8475
9878
|
cssProperty: "--kui-compare-axis",
|
|
8476
9879
|
keywords: [...COMPARE_AXES]
|
|
8477
|
-
}
|
|
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"] }
|
|
8478
9902
|
};
|
|
9903
|
+
var MIN_SWEEP_MS = 250;
|
|
9904
|
+
var MAX_FRAME_MS2 = 100;
|
|
8479
9905
|
var VERTICAL_KEY_DELTA = { ArrowUp: -1, ArrowDown: 1 };
|
|
8480
9906
|
function getMediaName(item, fallback) {
|
|
8481
9907
|
const alt = item.localName === "picture" ? item.querySelector("img")?.getAttribute("alt") : item.getAttribute("alt");
|
|
@@ -8491,76 +9917,402 @@ function resolveCompareLabel(el, media) {
|
|
|
8491
9917
|
}
|
|
8492
9918
|
return "Compare";
|
|
8493
9919
|
}
|
|
8494
|
-
function
|
|
9920
|
+
function createRange(doc, label, value, axis) {
|
|
8495
9921
|
const range = doc.createElement("input");
|
|
8496
9922
|
range.type = "range";
|
|
8497
9923
|
range.className = "kui-compare-range";
|
|
8498
9924
|
range.min = "0";
|
|
8499
9925
|
range.max = "100";
|
|
8500
9926
|
range.step = "1";
|
|
8501
|
-
range.value = String(
|
|
8502
|
-
range.setAttribute("aria-label", label);
|
|
8503
|
-
range.setAttribute("aria-valuetext",
|
|
9927
|
+
range.value = String(value);
|
|
9928
|
+
range.setAttribute("aria-label", label.name);
|
|
9929
|
+
range.setAttribute("aria-valuetext", label.valueText);
|
|
8504
9930
|
if (axis === "y") range.setAttribute("aria-orientation", "vertical");
|
|
9931
|
+
return range;
|
|
9932
|
+
}
|
|
9933
|
+
function createHandle(doc) {
|
|
8505
9934
|
const handle = doc.createElement("span");
|
|
8506
9935
|
handle.className = "kui-compare-handle";
|
|
8507
9936
|
handle.setAttribute("aria-hidden", "true");
|
|
8508
|
-
return
|
|
9937
|
+
return handle;
|
|
8509
9938
|
}
|
|
8510
|
-
function
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
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);
|
|
8514
9970
|
}
|
|
8515
|
-
const
|
|
8516
|
-
|
|
8517
|
-
|
|
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 }) {
|
|
8518
10063
|
const initialPercent = Math.round(params.num("position", 0.5) * 100);
|
|
8519
10064
|
const style = createStyleLedger(el);
|
|
8520
10065
|
style.set("--kui-compare", params.text("position", "50%"));
|
|
8521
|
-
const
|
|
8522
|
-
|
|
8523
|
-
|
|
10066
|
+
const doc = el.ownerDocument;
|
|
10067
|
+
const range = createRange(
|
|
10068
|
+
doc,
|
|
10069
|
+
{ name: resolveCompareLabel(el, media), valueText: `${initialPercent}% after` },
|
|
8524
10070
|
initialPercent,
|
|
8525
10071
|
axis
|
|
8526
10072
|
);
|
|
10073
|
+
const handle = createHandle(doc);
|
|
8527
10074
|
const onInput = () => {
|
|
8528
10075
|
const val = range.value;
|
|
8529
10076
|
style.set("--kui-compare", `${val}%`);
|
|
8530
10077
|
range.setAttribute("aria-valuetext", `${val}% after`);
|
|
8531
10078
|
};
|
|
8532
|
-
const
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
if (delta < 0) range.stepDown();
|
|
8537
|
-
else range.stepUp();
|
|
8538
|
-
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`);
|
|
8539
10083
|
};
|
|
10084
|
+
const onKeyDown = verticalKeys(range, axis, onInput);
|
|
8540
10085
|
range.addEventListener("input", onInput);
|
|
8541
10086
|
range.addEventListener("keydown", onKeyDown);
|
|
8542
10087
|
el.appendChild(range);
|
|
8543
10088
|
el.appendChild(handle);
|
|
8544
|
-
|
|
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();
|
|
8545
10100
|
range.removeEventListener("input", onInput);
|
|
8546
10101
|
range.removeEventListener("keydown", onKeyDown);
|
|
8547
10102
|
range.remove();
|
|
8548
10103
|
handle.remove();
|
|
8549
|
-
attrs.restore();
|
|
8550
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();
|
|
8551
10299
|
});
|
|
8552
10300
|
}
|
|
8553
10301
|
var COMPARE_PRIMITIVE = widgetPrimitive(
|
|
8554
10302
|
"compare",
|
|
8555
10303
|
{
|
|
8556
|
-
|
|
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)],
|
|
8557
10308
|
parameters: compareParams,
|
|
8558
10309
|
perfClass: "paint"
|
|
8559
10310
|
},
|
|
8560
10311
|
withTimingContract(
|
|
8561
10312
|
"compare",
|
|
8562
10313
|
{
|
|
8563
|
-
|
|
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"
|
|
8564
10316
|
},
|
|
8565
10317
|
deferPrepare(prepareCompare)
|
|
8566
10318
|
)
|
|
@@ -8734,7 +10486,8 @@ function prepareHotspots(el, params, ctx) {
|
|
|
8734
10486
|
var HOTSPOTS_PRIMITIVE = widgetPrimitive(
|
|
8735
10487
|
"hotspots",
|
|
8736
10488
|
{
|
|
8737
|
-
|
|
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")],
|
|
8738
10491
|
parameters: hotspotsParams,
|
|
8739
10492
|
perfClass: "layout"
|
|
8740
10493
|
},
|
|
@@ -8938,7 +10691,7 @@ function bindAutoplay(options) {
|
|
|
8938
10691
|
hovered = true;
|
|
8939
10692
|
update();
|
|
8940
10693
|
};
|
|
8941
|
-
const
|
|
10694
|
+
const leave2 = () => {
|
|
8942
10695
|
hovered = false;
|
|
8943
10696
|
update();
|
|
8944
10697
|
};
|
|
@@ -8955,7 +10708,7 @@ function bindAutoplay(options) {
|
|
|
8955
10708
|
update();
|
|
8956
10709
|
};
|
|
8957
10710
|
el.addEventListener("pointerenter", enter);
|
|
8958
|
-
el.addEventListener("pointerleave",
|
|
10711
|
+
el.addEventListener("pointerleave", leave2);
|
|
8959
10712
|
el.addEventListener("focusin", focus);
|
|
8960
10713
|
el.addEventListener("focusout", blur);
|
|
8961
10714
|
ctx.doc.addEventListener("visibilitychange", update);
|
|
@@ -8964,7 +10717,7 @@ function bindAutoplay(options) {
|
|
|
8964
10717
|
return () => {
|
|
8965
10718
|
if (timer !== null) clearTimeout(timer);
|
|
8966
10719
|
el.removeEventListener("pointerenter", enter);
|
|
8967
|
-
el.removeEventListener("pointerleave",
|
|
10720
|
+
el.removeEventListener("pointerleave", leave2);
|
|
8968
10721
|
el.removeEventListener("focusin", focus);
|
|
8969
10722
|
el.removeEventListener("focusout", blur);
|
|
8970
10723
|
ctx.doc.removeEventListener("visibilitychange", update);
|
|
@@ -9030,7 +10783,6 @@ function prepareSlideshow(el, params, ctx) {
|
|
|
9030
10783
|
el,
|
|
9031
10784
|
params,
|
|
9032
10785
|
ctx,
|
|
9033
|
-
scope,
|
|
9034
10786
|
resolveSteps,
|
|
9035
10787
|
name: "slideshow",
|
|
9036
10788
|
clickFallback: false,
|
|
@@ -9058,7 +10810,19 @@ function prepareSlideshow(el, params, ctx) {
|
|
|
9058
10810
|
}
|
|
9059
10811
|
var SLIDESHOW_PRIMITIVE = widgetPrimitive(
|
|
9060
10812
|
"slideshow",
|
|
9061
|
-
|
|
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
|
+
},
|
|
9062
10826
|
withTimingContract(
|
|
9063
10827
|
"slideshow",
|
|
9064
10828
|
{ honours: ["duration", "ease"], because: "the slideshow has no delayed start" },
|
|
@@ -9220,7 +10984,14 @@ function prepareScrollStory(el, params, ctx) {
|
|
|
9220
10984
|
}
|
|
9221
10985
|
var SCROLL_STORY_PRIMITIVE = widgetPrimitive(
|
|
9222
10986
|
"scroll-story",
|
|
9223
|
-
|
|
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
|
+
},
|
|
9224
10995
|
withTimingContract(
|
|
9225
10996
|
"scroll-story",
|
|
9226
10997
|
{ because: "scroll position" },
|
|
@@ -9323,7 +11094,8 @@ function prepareSlowMo(el, params) {
|
|
|
9323
11094
|
}
|
|
9324
11095
|
var SLOW_MO_PRIMITIVE = widgetPrimitive(
|
|
9325
11096
|
"slow-mo",
|
|
9326
|
-
|
|
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" },
|
|
9327
11099
|
withTimingContract(
|
|
9328
11100
|
"slow-mo",
|
|
9329
11101
|
{ because: "it changes playback rate rather than defining an animation timeline" },
|
|
@@ -9352,6 +11124,7 @@ var SHOWCASE_PRESETS = [
|
|
|
9352
11124
|
...SLOW_MO_PRESETS
|
|
9353
11125
|
];
|
|
9354
11126
|
function registerShowcase(target) {
|
|
11127
|
+
provideDeckViewer(attachDeckLightbox);
|
|
9355
11128
|
return registerInto(target, SHOWCASE_PRIMITIVES, SHOWCASE_PRESETS, "Showcase");
|
|
9356
11129
|
}
|
|
9357
11130
|
|