@dumiflex/wp-kit 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +5 -0
- package/package.json +4 -3
- package/src/Avatar.vue +1 -1
- package/src/Banner.vue +4 -4
- package/src/Button.vue +73 -44
- package/src/Checkbox.vue +201 -63
- package/src/Chip.vue +5 -2
- package/src/CodeBlock.vue +33 -6
- package/src/ColorField.vue +60 -6
- package/src/Combobox.vue +10 -14
- package/src/ConfirmModal.vue +12 -22
- package/src/ContextMenu.vue +11 -30
- package/src/CopyButton.vue +1 -1
- package/src/DataTable.vue +3 -4
- package/src/DatePicker.vue +1 -1
- package/src/Disclosure.vue +2 -1
- package/src/Drawer.vue +2 -2
- package/src/DropZone.vue +2 -2
- package/src/HsvPicker.vue +2 -2
- package/src/IconPicker.vue +1 -1
- package/src/ImageGrid.vue +17 -15
- package/src/Lightbox.vue +6 -3
- package/src/LoadingMark.vue +93 -51
- package/src/Modal.vue +109 -44
- package/src/ModalShell.vue +26 -5
- package/src/Pagination.vue +4 -4
- package/src/Popover.vue +68 -12
- package/src/RadioGroup.vue +5 -4
- package/src/SearchInput.vue +1 -1
- package/src/Select.vue +53 -158
- package/src/Skeleton.vue +21 -5
- package/src/Slider.vue +166 -55
- package/src/SortableList.vue +1 -1
- package/src/TagInput.vue +1 -1
- package/src/Timeline.vue +2 -2
- package/src/WorkingLine.vue +320 -0
- package/src/WpIconButton.vue +3 -2
- package/src/WpImageZoom.vue +7 -7
- package/src/WpSegmented.vue +3 -1
- package/src/WpSwitch.vue +1 -1
- package/src/WpTabs.vue +1 -1
- package/src/_modal-motion.css +1 -1
- package/src/button.css +8 -6
- package/src/floating.ts +5 -0
- package/src/highlight.ts +199 -0
- package/src/hsv.ts +1 -1
- package/src/loading-mark-motion.ts +249 -0
- package/src/menu.css +51 -0
- package/src/motion.css +73 -0
- package/src/slider-math.ts +45 -1
- package/src/theme/looks.css +6 -19
- package/src/theme/tokens.css +12 -1108
- package/src/tokens.ts +8 -3
- package/src/tooltip.css +8 -5
- package/src/useDialogFocus.ts +11 -2
- package/src/CloseButton.vue +0 -69
- package/src/ColorPicker.vue +0 -201
- package/src/ConfirmDialog.vue +0 -165
- package/src/RangeSlider.vue +0 -183
- package/src/Toggle.vue +0 -36
- package/src/WpButton.vue +0 -75
- package/src/WpCheck.vue +0 -104
- package/src/_skeleton.css +0 -50
package/src/hsv.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* lowercase. Functions are tolerant of out-of-range input — they clamp
|
|
6
6
|
* rather than throw, which keeps the picker UI robust under fast drags.
|
|
7
7
|
*
|
|
8
|
-
* Lives in shared/ so both the
|
|
8
|
+
* Lives in shared/ so both the ColorField / TweaksPanel
|
|
9
9
|
* AND the canvas-side bundle modal can drive the same HsvPicker
|
|
10
10
|
* without a cross-package import.
|
|
11
11
|
*/
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoadingMark's motion, drawn on a canvas.
|
|
3
|
+
*
|
|
4
|
+
* The mark used to move its SVG paths with CSS. A keyframed `transform` /
|
|
5
|
+
* `opacity` goes to the compositor, which blurs the path and snaps it to whole
|
|
6
|
+
* pixels as it starts; animating registered custom properties fixed that in
|
|
7
|
+
* Chromium, but Firefox still lays each frame of an SVG transform on the pixel
|
|
8
|
+
* grid, so the rings stepped and twitched as they slid. A canvas puts every
|
|
9
|
+
* shape at its exact fractional position each frame, sharp and smooth in both.
|
|
10
|
+
*
|
|
11
|
+
* One `requestAnimationFrame` loop draws every mark on the page. Each mark
|
|
12
|
+
* keeps its own clock, so a host can slow or pause the marks inside it with
|
|
13
|
+
* `data-wp-motion-speed` (a number) and `data-wp-motion-paused` on an
|
|
14
|
+
* ancestor, as the kit site's Animations section does.
|
|
15
|
+
*/
|
|
16
|
+
import { RING_A, RING_B, STAR_A, STAR_B, STAR_BIG } from "./logo-paths";
|
|
17
|
+
|
|
18
|
+
/** One loop of the logo, in ms; the sparkle's swap takes two. */
|
|
19
|
+
export const LOOP_MS = 2400;
|
|
20
|
+
/** Room around the 1024 box for the rings as they slide apart (70 units)
|
|
21
|
+
* and for anti-aliasing at the edge. */
|
|
22
|
+
export const PAD = 82;
|
|
23
|
+
export const BOX = 1024 + 2 * PAD;
|
|
24
|
+
|
|
25
|
+
/** CSS `cubic-bezier(x1, y1, x2, y2)` as a function of progress. */
|
|
26
|
+
export function cubicBezier(x1: number, y1: number, x2: number, y2: number): (x: number) => number {
|
|
27
|
+
return (x) => {
|
|
28
|
+
if (x <= 0) return 0;
|
|
29
|
+
if (x >= 1) return 1;
|
|
30
|
+
let lo = 0;
|
|
31
|
+
let hi = 1;
|
|
32
|
+
let t = x;
|
|
33
|
+
for (let k = 0; k < 30; k++) {
|
|
34
|
+
t = (lo + hi) / 2;
|
|
35
|
+
const cx = 3 * x1 * t * (1 - t) ** 2 + 3 * x2 * t * t * (1 - t) + t ** 3;
|
|
36
|
+
if (cx < x) lo = t;
|
|
37
|
+
else hi = t;
|
|
38
|
+
}
|
|
39
|
+
return 3 * y1 * t * (1 - t) ** 2 + 3 * y2 * t * t * (1 - t) + t ** 3;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SMOOTH = cubicBezier(0.65, 0, 0.35, 1);
|
|
44
|
+
const EASE_IN_OUT = cubicBezier(0.42, 0, 0.58, 1);
|
|
45
|
+
|
|
46
|
+
type Stop = readonly [at: number, values: readonly number[]];
|
|
47
|
+
|
|
48
|
+
/** Value of a keyframe track at progress `p` (0..1), eased per segment the
|
|
49
|
+
* way CSS eases between two keyframes. */
|
|
50
|
+
export function track(stops: readonly Stop[], p: number, ease: (x: number) => number): number[] {
|
|
51
|
+
for (let k = 0; k < stops.length - 1; k++) {
|
|
52
|
+
const a = stops[k];
|
|
53
|
+
const b = stops[k + 1];
|
|
54
|
+
if (!a || !b) break;
|
|
55
|
+
if (p >= a[0] && p <= b[0]) {
|
|
56
|
+
if (b[0] === a[0]) return [...b[1]];
|
|
57
|
+
const e = ease((p - a[0]) / (b[0] - a[0]));
|
|
58
|
+
return a[1].map((v, j) => v + ((b[1][j] ?? v) - v) * e);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...(stops[stops.length - 1]?.[1] ?? [])];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The rings' slide apart (units of the 1024 box). */
|
|
65
|
+
const SPLIT: readonly Stop[] = [[0, [0]], [0.15, [0]], [0.4, [70]], [0.6, [70]], [0.85, [0]], [1, [0]]];
|
|
66
|
+
/** The big star's dip: [scale, turn in degrees]. */
|
|
67
|
+
const DIP: readonly Stop[] = [[0, [1, 0]], [0.15, [1, 0]], [0.32, [0.7, 45]], [0.5, [1, 90]], [1, [1, 90]]];
|
|
68
|
+
/** The small stars' twinkle: [opacity, scale]. */
|
|
69
|
+
const TWINKLE: readonly Stop[] = [[0, [1, 1]], [0.5, [0.35, 0.6]], [1, [1, 1]]];
|
|
70
|
+
/** The sparkle's small stars trading places (degrees, over two loops). */
|
|
71
|
+
const SWAP: readonly Stop[] = [[0, [0]], [0.15, [0]], [0.5, [180]], [0.65, [180]], [1, [360]]];
|
|
72
|
+
|
|
73
|
+
export interface Pose {
|
|
74
|
+
split: number;
|
|
75
|
+
bigScale: number;
|
|
76
|
+
bigTurn: number;
|
|
77
|
+
twinkleAlpha: number;
|
|
78
|
+
twinkleScale: number;
|
|
79
|
+
swap: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Where every part is `ms` into the animation. */
|
|
83
|
+
export function poseAt(ms: number): Pose {
|
|
84
|
+
const p = (((ms % LOOP_MS) + LOOP_MS) % LOOP_MS) / LOOP_MS;
|
|
85
|
+
const p2 = (((ms % (2 * LOOP_MS)) + 2 * LOOP_MS) % (2 * LOOP_MS)) / (2 * LOOP_MS);
|
|
86
|
+
const [split = 0] = track(SPLIT, p, SMOOTH);
|
|
87
|
+
const [bigScale = 1, bigTurn = 0] = track(DIP, p, SMOOTH);
|
|
88
|
+
const [twinkleAlpha = 1, twinkleScale = 1] = track(TWINKLE, p, EASE_IN_OUT);
|
|
89
|
+
const [swap = 0] = track(SWAP, p2, SMOOTH);
|
|
90
|
+
return { split, bigScale, bigTurn, twinkleAlpha, twinkleScale, swap };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Centres of the stars in the 1024 box (from their bounding boxes). */
|
|
94
|
+
const C_BIG = [512, 512] as const;
|
|
95
|
+
const C_A = [360, 664] as const;
|
|
96
|
+
const C_B = [664, 360] as const;
|
|
97
|
+
|
|
98
|
+
let paths: Record<"ra" | "rb" | "big" | "a" | "b", Path2D> | null = null;
|
|
99
|
+
function shapes(): Record<"ra" | "rb" | "big" | "a" | "b", Path2D> {
|
|
100
|
+
paths ??= { ra: new Path2D(RING_A), rb: new Path2D(RING_B), big: new Path2D(STAR_BIG), a: new Path2D(STAR_A), b: new Path2D(STAR_B) };
|
|
101
|
+
return paths;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface Look {
|
|
105
|
+
variant: "logo" | "sparkle";
|
|
106
|
+
/** [violet, indigo, teal], or three times the text colour for `mono`. */
|
|
107
|
+
colours: readonly [string, string, string];
|
|
108
|
+
/** Flat colours (mono): no ring gradients. */
|
|
109
|
+
flat: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function around(ctx: CanvasRenderingContext2D, [x, y]: readonly [number, number], scale: number, turn: number, path: Path2D, fill: string, alpha: number): void {
|
|
113
|
+
ctx.save();
|
|
114
|
+
ctx.translate(x, y);
|
|
115
|
+
ctx.rotate((turn * Math.PI) / 180);
|
|
116
|
+
ctx.scale(scale, scale);
|
|
117
|
+
ctx.translate(-x, -y);
|
|
118
|
+
ctx.globalAlpha = alpha;
|
|
119
|
+
ctx.fillStyle = fill;
|
|
120
|
+
ctx.fill(path);
|
|
121
|
+
ctx.restore();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Draws one frame on a canvas `BOX` units square (scaled to its pixels). */
|
|
125
|
+
export function drawPose(ctx: CanvasRenderingContext2D, width: number, look: Look, pose: Pose): void {
|
|
126
|
+
const k = width / BOX;
|
|
127
|
+
const [violet, indigo, teal] = look.colours;
|
|
128
|
+
const p = shapes();
|
|
129
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
130
|
+
ctx.clearRect(0, 0, width, width);
|
|
131
|
+
ctx.setTransform(k, 0, 0, k, PAD * k, PAD * k);
|
|
132
|
+
if (look.variant === "logo") {
|
|
133
|
+
const ringA = (() => {
|
|
134
|
+
if (look.flat) return violet;
|
|
135
|
+
const g = ctx.createLinearGradient(312.56, 897.19, 312.56, 2);
|
|
136
|
+
g.addColorStop(0, violet);
|
|
137
|
+
g.addColorStop(1, indigo);
|
|
138
|
+
return g;
|
|
139
|
+
})();
|
|
140
|
+
const ringB = (() => {
|
|
141
|
+
if (look.flat) return violet;
|
|
142
|
+
const g = ctx.createLinearGradient(711.44, 126.81, 711.44, 1022);
|
|
143
|
+
g.addColorStop(0, indigo);
|
|
144
|
+
g.addColorStop(1, teal);
|
|
145
|
+
return g;
|
|
146
|
+
})();
|
|
147
|
+
ctx.save();
|
|
148
|
+
ctx.translate(-pose.split, -pose.split);
|
|
149
|
+
ctx.fillStyle = ringA;
|
|
150
|
+
ctx.fill(p.ra);
|
|
151
|
+
ctx.restore();
|
|
152
|
+
ctx.save();
|
|
153
|
+
ctx.translate(pose.split, pose.split);
|
|
154
|
+
ctx.fillStyle = ringB;
|
|
155
|
+
ctx.fill(p.rb);
|
|
156
|
+
ctx.restore();
|
|
157
|
+
around(ctx, C_BIG, pose.bigScale, pose.bigTurn, p.big, violet, 1);
|
|
158
|
+
around(ctx, C_A, pose.twinkleScale, 0, p.a, indigo, pose.twinkleAlpha);
|
|
159
|
+
around(ctx, C_B, pose.twinkleScale, 0, p.b, teal, pose.twinkleAlpha);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
ctx.translate(512, 512);
|
|
163
|
+
ctx.scale(1.45, 1.45);
|
|
164
|
+
ctx.translate(-512, -512);
|
|
165
|
+
around(ctx, C_BIG, pose.bigScale, pose.bigTurn, p.big, violet, 1);
|
|
166
|
+
ctx.save();
|
|
167
|
+
ctx.translate(512, 512);
|
|
168
|
+
ctx.rotate((pose.swap * Math.PI) / 180);
|
|
169
|
+
ctx.translate(-512, -512);
|
|
170
|
+
ctx.fillStyle = indigo;
|
|
171
|
+
ctx.fill(p.a);
|
|
172
|
+
ctx.fillStyle = teal;
|
|
173
|
+
ctx.fill(p.b);
|
|
174
|
+
ctx.restore();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** A mark the loop draws. */
|
|
178
|
+
export interface Animated {
|
|
179
|
+
canvas: HTMLCanvasElement;
|
|
180
|
+
look: () => Look;
|
|
181
|
+
/** ms of animation shown so far, advanced by the loop. */
|
|
182
|
+
clock: number;
|
|
183
|
+
/** False while off screen: the clock still runs, nothing is drawn. */
|
|
184
|
+
visible: boolean;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const marks = new Set<Animated>();
|
|
188
|
+
let frame = 0;
|
|
189
|
+
let last = 0;
|
|
190
|
+
|
|
191
|
+
/** Speed and pause from the nearest host that sets them. */
|
|
192
|
+
export function motionOf(el: Element): { speed: number; paused: boolean } {
|
|
193
|
+
const host = el.closest("[data-wp-motion-speed], [data-wp-motion-paused]");
|
|
194
|
+
if (!host) return { speed: 1, paused: false };
|
|
195
|
+
const paused = host.closest("[data-wp-motion-paused]") !== null;
|
|
196
|
+
const speedHost = host.closest("[data-wp-motion-speed]");
|
|
197
|
+
const speed = Number(speedHost?.getAttribute("data-wp-motion-speed") ?? "1");
|
|
198
|
+
return { speed: Number.isFinite(speed) && speed >= 0 ? speed : 1, paused };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function fit(c: HTMLCanvasElement): void {
|
|
202
|
+
const css = c.clientWidth;
|
|
203
|
+
const want = Math.max(1, Math.round(css * (window.devicePixelRatio || 1)));
|
|
204
|
+
if (c.width !== want) {
|
|
205
|
+
c.width = want;
|
|
206
|
+
c.height = want;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function tick(now: number): void {
|
|
211
|
+
const dt = last ? Math.min(now - last, 100) : 0;
|
|
212
|
+
last = now;
|
|
213
|
+
for (const m of marks) {
|
|
214
|
+
const { speed, paused } = motionOf(m.canvas);
|
|
215
|
+
if (!paused) m.clock += dt * speed;
|
|
216
|
+
if (!m.visible) continue;
|
|
217
|
+
const ctx = m.canvas.getContext("2d");
|
|
218
|
+
if (!ctx) continue;
|
|
219
|
+
fit(m.canvas);
|
|
220
|
+
drawPose(ctx, m.canvas.width, m.look(), poseAt(m.clock));
|
|
221
|
+
}
|
|
222
|
+
frame = marks.size ? requestAnimationFrame(tick) : 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Starts drawing a mark; returns the function that stops it. */
|
|
226
|
+
export function animate(mark: Animated): () => void {
|
|
227
|
+
marks.add(mark);
|
|
228
|
+
if (!frame) {
|
|
229
|
+
last = 0;
|
|
230
|
+
frame = requestAnimationFrame(tick);
|
|
231
|
+
}
|
|
232
|
+
return () => {
|
|
233
|
+
marks.delete(mark);
|
|
234
|
+
if (!marks.size && frame) {
|
|
235
|
+
cancelAnimationFrame(frame);
|
|
236
|
+
frame = 0;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Draws one still frame right away (the first frame, before the loop runs,
|
|
242
|
+
* so the mark never shows blank). */
|
|
243
|
+
export function drawNow(mark: Animated): boolean {
|
|
244
|
+
const ctx = mark.canvas.getContext("2d");
|
|
245
|
+
if (!ctx) return false;
|
|
246
|
+
fit(mark.canvas);
|
|
247
|
+
drawPose(ctx, mark.canvas.width, mark.look(), poseAt(mark.clock));
|
|
248
|
+
return true;
|
|
249
|
+
}
|
package/src/menu.css
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* Menu panel: the one look of every floating list in the kit (Select's list,
|
|
2
|
+
* ContextMenu / DropdownMenu and Combobox's list). Pulled in scoped by
|
|
3
|
+
* Popover (whose `variant="menu"` panel is the menu), ContextMenu, Select and
|
|
4
|
+
* Combobox. It reads only the SHARED_TOKENS names, so a menu wears the
|
|
5
|
+
* manager colours on a page and the canvas colours in a node.
|
|
6
|
+
*
|
|
7
|
+
* <div class="wp-menu"> the panel
|
|
8
|
+
* <li class="wp-menu__item" data-active="true" aria-selected="true">
|
|
9
|
+
*
|
|
10
|
+
* Item states: hover and `[data-active="true"]` (the keyboard's row) share one
|
|
11
|
+
* tint; `[aria-selected="true"]` is a lighter one; `[aria-disabled="true"]` is
|
|
12
|
+
* dim and takes no hover. A piece keeps its own row layout (icons, meta,
|
|
13
|
+
* subtitles, ticks); the sheet sets everything you see around it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
.wp-menu {
|
|
17
|
+
z-index: 10020; /* above every modal overlay (they run up to 10010) */
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 4px;
|
|
21
|
+
list-style: none;
|
|
22
|
+
background: var(--wp-bg-2);
|
|
23
|
+
color: var(--wp-text);
|
|
24
|
+
border: 1px solid var(--wp-border-strong);
|
|
25
|
+
border-radius: min(8px, var(--wp-radius-cap, 999px));
|
|
26
|
+
box-shadow: var(--wp-shadow-md);
|
|
27
|
+
font-family: var(--wp-font-sans, sans-serif);
|
|
28
|
+
font-size: 12px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.wp-menu__item {
|
|
32
|
+
padding: 6px 8px;
|
|
33
|
+
border-radius: min(6px, var(--wp-radius-cap, 999px));
|
|
34
|
+
color: var(--wp-text);
|
|
35
|
+
font-size: 12px;
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
user-select: none;
|
|
38
|
+
}
|
|
39
|
+
.wp-menu__item[aria-selected="true"] {
|
|
40
|
+
background: color-mix(in oklab, var(--wp-accent) 12%, transparent);
|
|
41
|
+
}
|
|
42
|
+
.wp-menu__item:hover,
|
|
43
|
+
.wp-menu__item[data-active="true"] {
|
|
44
|
+
background: var(--wp-accent-glow);
|
|
45
|
+
color: var(--wp-text);
|
|
46
|
+
}
|
|
47
|
+
.wp-menu__item[aria-disabled="true"] {
|
|
48
|
+
color: var(--wp-text-dim);
|
|
49
|
+
background: transparent;
|
|
50
|
+
cursor: not-allowed;
|
|
51
|
+
}
|
package/src/motion.css
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Entrances and nudges for things that appear while you watch: a streamed
|
|
3
|
+
* reply's new words, a reaction, a chip by the selection, a question that
|
|
4
|
+
* needs an answer. Add the class to the element as it mounts.
|
|
5
|
+
*
|
|
6
|
+
* .wp-enter-fade fades in (150 ms): each new run of streamed text.
|
|
7
|
+
* .wp-enter-pop pops in with a small overshoot (320 ms): a reaction.
|
|
8
|
+
* .wp-enter-rise rises 4 px into place (140 ms): a chip above a
|
|
9
|
+
* selection; add data-from="below" when it sits under it.
|
|
10
|
+
* .wp-enter-drop drops 6 px into place (500 ms): a panel that asks for
|
|
11
|
+
* something.
|
|
12
|
+
* .wp-attention a soft accent ring that pulses three times (after
|
|
13
|
+
* .wp-enter-drop when both are set).
|
|
14
|
+
*
|
|
15
|
+
* The moving element keeps its own position: set --wp-enter-x for an extra
|
|
16
|
+
* horizontal shift (a chip centred with -50%), and it goes into the same
|
|
17
|
+
* transform.
|
|
18
|
+
*
|
|
19
|
+
* Every entrance animates registered custom properties, never `opacity` or
|
|
20
|
+
* `transform` themselves: those go to the compositor, which lifts the element
|
|
21
|
+
* onto its own layer while it moves and drops it after, so text went soft
|
|
22
|
+
* during the motion and snapped half a pixel at its end.
|
|
23
|
+
*/
|
|
24
|
+
@property --wp-enter-o { syntax: "<number>"; inherits: false; initial-value: 1; }
|
|
25
|
+
@property --wp-enter-s { syntax: "<number>"; inherits: false; initial-value: 1; }
|
|
26
|
+
@property --wp-enter-y { syntax: "<length>"; inherits: false; initial-value: 0px; }
|
|
27
|
+
|
|
28
|
+
@layer wp-extension {
|
|
29
|
+
.wp-enter-fade {
|
|
30
|
+
opacity: var(--wp-enter-o);
|
|
31
|
+
animation: wp-enter-fade 150ms ease-out;
|
|
32
|
+
}
|
|
33
|
+
.wp-enter-pop {
|
|
34
|
+
opacity: var(--wp-enter-o);
|
|
35
|
+
transform: translateX(var(--wp-enter-x, 0px)) scale(var(--wp-enter-s));
|
|
36
|
+
animation: wp-enter-pop 0.32s cubic-bezier(0.3, 1.6, 0.5, 1) both;
|
|
37
|
+
}
|
|
38
|
+
.wp-enter-rise {
|
|
39
|
+
opacity: var(--wp-enter-o);
|
|
40
|
+
transform: translate(var(--wp-enter-x, 0px), var(--wp-enter-y));
|
|
41
|
+
animation: wp-enter-rise 0.14s ease-out both;
|
|
42
|
+
}
|
|
43
|
+
.wp-enter-rise[data-from="below"] { animation-name: wp-enter-rise-below; }
|
|
44
|
+
.wp-enter-drop {
|
|
45
|
+
opacity: var(--wp-enter-o);
|
|
46
|
+
transform: translate(var(--wp-enter-x, 0px), var(--wp-enter-y));
|
|
47
|
+
animation: wp-enter-drop 0.5s ease-out;
|
|
48
|
+
}
|
|
49
|
+
.wp-attention { animation: wp-attention 2.4s ease-in-out 3; }
|
|
50
|
+
.wp-enter-drop.wp-attention {
|
|
51
|
+
animation: wp-enter-drop 0.5s ease-out, wp-attention 2.4s ease-in-out 0.5s 3;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@keyframes wp-enter-fade { from { --wp-enter-o: 0; } to { --wp-enter-o: 1; } }
|
|
55
|
+
@keyframes wp-enter-pop {
|
|
56
|
+
from { --wp-enter-s: 0.3; --wp-enter-o: 0; }
|
|
57
|
+
to { --wp-enter-s: 1; --wp-enter-o: 1; }
|
|
58
|
+
}
|
|
59
|
+
@keyframes wp-enter-rise {
|
|
60
|
+
from { --wp-enter-o: 0; --wp-enter-y: 4px; }
|
|
61
|
+
to { --wp-enter-o: 1; --wp-enter-y: 0px; }
|
|
62
|
+
}
|
|
63
|
+
@keyframes wp-enter-rise-below {
|
|
64
|
+
from { --wp-enter-o: 0; --wp-enter-y: -4px; }
|
|
65
|
+
to { --wp-enter-o: 1; --wp-enter-y: 0px; }
|
|
66
|
+
}
|
|
67
|
+
@keyframes wp-enter-drop { from { --wp-enter-o: 0; --wp-enter-y: -6px; } }
|
|
68
|
+
@keyframes wp-attention { 50% { box-shadow: 0 0 0 6px var(--wp-accent-glow); } }
|
|
69
|
+
|
|
70
|
+
@media (prefers-reduced-motion: reduce) {
|
|
71
|
+
.wp-enter-fade, .wp-enter-pop, .wp-enter-rise, .wp-enter-drop, .wp-attention { animation: none; }
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/slider-math.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Number helpers for
|
|
2
|
+
* Number helpers for Slider (one thumb or a pair): clamp
|
|
3
3
|
* to the ends, snap to the step counted from `min`, and turn a pointer
|
|
4
4
|
* position into a value. Steps like 0.1 snap without float fuzz.
|
|
5
5
|
*/
|
|
@@ -43,3 +43,47 @@ export function keyStep(key: string, v: number, min: number, max: number, step:
|
|
|
43
43
|
default: return null;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Mark snapping. `snap: true` keeps the value on a mark (the nearest one);
|
|
49
|
+
* a number pulls it onto a mark only within that distance, so the thumb
|
|
50
|
+
* moves freely by its step elsewhere and "clicks" into a mark near one.
|
|
51
|
+
* With no marks, or snap off, the value comes back as it was.
|
|
52
|
+
*/
|
|
53
|
+
export function snapToMarks(v: number, marks: readonly number[], snapTo: boolean | number | undefined): number {
|
|
54
|
+
if (!snapTo || !marks.length) return v;
|
|
55
|
+
let best = marks[0] ?? v;
|
|
56
|
+
for (const m of marks) if (Math.abs(m - v) < Math.abs(best - v)) best = m;
|
|
57
|
+
if (snapTo === true) return best;
|
|
58
|
+
return Math.abs(best - v) <= snapTo ? best : v;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A key press on a slider that only takes marks (`snap: true`): arrows go to
|
|
63
|
+
* the next mark up or down, PageUp/PageDown jump `big` marks, Home/End the
|
|
64
|
+
* first and last. Null for a key the slider ignores.
|
|
65
|
+
*/
|
|
66
|
+
export function keyToMark(key: string, v: number, marks: readonly number[], big = 3): number | null {
|
|
67
|
+
const sorted = [...new Set(marks)].sort((a, b) => a - b);
|
|
68
|
+
if (!sorted.length) return null;
|
|
69
|
+
// Where v sits: the index of the mark at it, or between two marks.
|
|
70
|
+
const at = sorted.findIndex((m) => m >= v);
|
|
71
|
+
const exact = at >= 0 && sorted[at] === v;
|
|
72
|
+
const up = (n: number): number => {
|
|
73
|
+
const from = at < 0 ? sorted.length : exact ? at : at - 1;
|
|
74
|
+
return sorted[Math.min(sorted.length - 1, from + n)] ?? v;
|
|
75
|
+
};
|
|
76
|
+
const down = (n: number): number => {
|
|
77
|
+
const from = at < 0 ? sorted.length : at;
|
|
78
|
+
return sorted[Math.max(0, from - n)] ?? v;
|
|
79
|
+
};
|
|
80
|
+
switch (key) {
|
|
81
|
+
case "ArrowRight": case "ArrowUp": return up(1);
|
|
82
|
+
case "ArrowLeft": case "ArrowDown": return down(1);
|
|
83
|
+
case "PageUp": return up(big);
|
|
84
|
+
case "PageDown": return down(big);
|
|
85
|
+
case "Home": return sorted[0] ?? v;
|
|
86
|
+
case "End": return sorted[sorted.length - 1] ?? v;
|
|
87
|
+
default: return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/theme/looks.css
CHANGED
|
@@ -47,8 +47,10 @@
|
|
|
47
47
|
/* --wp-radius-cap: the roundest a corner may get in this look. Fixed radii
|
|
48
48
|
in components are written min(Npx, var(--wp-radius-cap, 999px)) and
|
|
49
49
|
pills var(--wp-radius-cap, 999px), so Orbit draws them as authored and a
|
|
50
|
-
square look squares them without a rule per component. Circles
|
|
51
|
-
|
|
50
|
+
square look squares them without a rule per component. Circles are
|
|
51
|
+
min(50%, var(--wp-radius-cap, 999px)): round in Orbit, softened in Crisp,
|
|
52
|
+
square in Terminal and TUI (dots, radios, avatars, the timeline).
|
|
53
|
+
src/__tests__/corners.test.ts fails on a raw radius. The theme audit (scripts/theme-audit) fails on a corner
|
|
52
54
|
rounder than the cap. */
|
|
53
55
|
html[data-wp-look="crisp"] { --wp-radius-cap: 4px; }
|
|
54
56
|
html[data-wp-look="terminal"],
|
|
@@ -362,10 +364,8 @@ html[data-wp-look="tui"] .wp-check:is([aria-checked="true"], [data-checked="true
|
|
|
362
364
|
html[data-wp-look="tui"] .wp-check:is([aria-checked="mixed"], [data-indeterminate="true"]):not([data-checked="true"])::before { content: "[-]"; color: var(--wp-accent-text); }
|
|
363
365
|
html[data-wp-look="tui"] .wp-page__title { font-size: 16px; font-weight: 700; letter-spacing: 0; }
|
|
364
366
|
/* Radios read ( ) / (*) beside the [ ] / [x] checkboxes; the drawn dot
|
|
365
|
-
hides. RadioGroup marks the picked option on the row (data-checked)
|
|
366
|
-
|
|
367
|
-
html[data-wp-look="tui"] .wp-radio-group__opt .wp-radio-group__dot,
|
|
368
|
-
html[data-wp-look="tui"] .wp-radio {
|
|
367
|
+
hides. RadioGroup marks the picked option on the row (data-checked). */
|
|
368
|
+
html[data-wp-look="tui"] .wp-radio-group__opt .wp-radio-group__dot {
|
|
369
369
|
width: auto;
|
|
370
370
|
min-width: 3ch;
|
|
371
371
|
height: auto;
|
|
@@ -381,19 +381,6 @@ html[data-wp-look="tui"] .wp-radio-group__opt[data-checked] .wp-radio-group__dot
|
|
|
381
381
|
html[data-wp-look="tui"] .wp-radio-group__dot::before { content: "( )"; }
|
|
382
382
|
html[data-wp-look="tui"] .wp-radio-group__opt[data-checked] .wp-radio-group__dot::before { content: "(*)"; color: var(--wp-accent-text); }
|
|
383
383
|
html[data-wp-look="tui"] .wp-radio-group__opt[aria-disabled="true"][data-checked] .wp-radio-group__dot::before { color: var(--wp-text-dim); }
|
|
384
|
-
html[data-wp-look="tui"] .wp-radio::before,
|
|
385
|
-
html[data-wp-look="tui"] .wp-radio:disabled::before {
|
|
386
|
-
content: "( )";
|
|
387
|
-
width: auto;
|
|
388
|
-
height: auto;
|
|
389
|
-
border-radius: 0;
|
|
390
|
-
background: transparent;
|
|
391
|
-
transform: none;
|
|
392
|
-
}
|
|
393
|
-
html[data-wp-look="tui"] .wp-radio:checked::before { content: "(*)"; color: var(--wp-accent-text); }
|
|
394
|
-
html[data-wp-look="tui"] .wp-radio:disabled:checked::before { content: "(*)"; color: var(--wp-text-dim); }
|
|
395
|
-
html[data-wp-look="tui"] .wp-radio:checked,
|
|
396
|
-
html[data-wp-look="tui"] .wp-radio:disabled:checked { background: transparent; }
|
|
397
384
|
|
|
398
385
|
/* ================= Canvas previews ignore the look =================
|
|
399
386
|
The canvas keeps ComfyUI's ground in every look (only the accent follows
|