@jungherz-de/glasskit 1.31.0 → 1.32.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 +107 -1
- package/README.md +125 -20
- package/SKILL.md +496 -20
- package/glasskit-droplet.js +70 -32
- package/glasskit-styles.js +2 -2
- package/glasskit.css +783 -24
- package/glasskit.min.css +1 -1
- package/glasskit.min.css.map +1 -1
- package/package.json +2 -2
- package/theme-override.css +87 -0
package/glasskit-droplet.js
CHANGED
|
@@ -25,6 +25,13 @@
|
|
|
25
25
|
// and data-droplet-lift while the drop travels, data-droplet-drag while a
|
|
26
26
|
// finger holds it, and .is-under-droplet on the item below it.
|
|
27
27
|
//
|
|
28
|
+
// A row laid out as a column — a vertical tab bar, a sidebar — runs the drop
|
|
29
|
+
// along y (since 1.32.0): data-droplet-axis="y" on the row, the directions
|
|
30
|
+
// "down" and "up", the drag on the vertical. The axis comes from the row's
|
|
31
|
+
// aria-orientation="vertical" or its flex-direction. The timing comes from
|
|
32
|
+
// the --gl-droplet-* tokens, which the module reads as well: the lift and how
|
|
33
|
+
// long the lens follows the drop.
|
|
34
|
+
//
|
|
28
35
|
// Options of attachDroplet, for rows whose items live elsewhere (GlassKit
|
|
29
36
|
// Elements uses them): items() returns the items, isChosen(item) tells the
|
|
30
37
|
// chosen one, choose(item) picks one, lens(item, on) marks the item below
|
|
@@ -36,6 +43,13 @@ const ROWS = '.glass-tab-bar--droplet, .glass-segmented--droplet';
|
|
|
36
43
|
const ITEMS = '.glass-tab-bar__item, .glass-segmented__item';
|
|
37
44
|
const attached = new WeakMap();
|
|
38
45
|
|
|
46
|
+
// A duration token as the browser hands it over, "0.3s" or "280ms".
|
|
47
|
+
const ms = (value, fallback) => {
|
|
48
|
+
const v = String(value).trim(), n = parseFloat(v);
|
|
49
|
+
if (Number.isNaN(n)) return fallback;
|
|
50
|
+
return v.endsWith('ms') ? n : v.endsWith('s') ? n * 1000 : fallback;
|
|
51
|
+
};
|
|
52
|
+
|
|
39
53
|
export function attachDroplet(row, options = {}) {
|
|
40
54
|
if (attached.has(row)) return attached.get(row);
|
|
41
55
|
|
|
@@ -45,12 +59,17 @@ export function attachDroplet(row, options = {}) {
|
|
|
45
59
|
const choose = options.choose || (it => it.click());
|
|
46
60
|
const mark = options.lens || ((it, on) => it.classList.toggle('is-under-droplet', on));
|
|
47
61
|
const usable = it => !it.disabled && it.getAttribute('aria-disabled') !== 'true';
|
|
48
|
-
const scrolls = () => row.scrollWidth > row.clientWidth + 1;
|
|
49
62
|
const motion = matchMedia('(prefers-reduced-motion: no-preference)');
|
|
50
63
|
const set = (k, px) => row.style.setProperty(`--gl-droplet-${k}`, `${px}px`);
|
|
64
|
+
const token = (name, fallback) => ms(getComputedStyle(row).getPropertyValue(`--gl-droplet-${name}`), fallback);
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
// pos is where the drop starts along the axis: left in a row, top in a
|
|
67
|
+
// column. The axis is read again whenever the drop is placed.
|
|
68
|
+
let chosen = null, pos = 0, placed = null, liftTimer = 0, lensFrame = 0, edgeFrame = 0;
|
|
69
|
+
let drag = null, eatClick = false, under = null, axisY = false;
|
|
70
|
+
const vertical = () => row.getAttribute('aria-orientation') === 'vertical'
|
|
71
|
+
|| getComputedStyle(row).flexDirection.startsWith('column');
|
|
72
|
+
const scrolls = () => axisY ? row.scrollHeight > row.clientHeight + 1 : row.scrollWidth > row.clientWidth + 1;
|
|
54
73
|
|
|
55
74
|
// The row on screen and its scale there. Rects are viewport pixels, scaled
|
|
56
75
|
// by every transform on an ancestor — an app that zooms in on start, a
|
|
@@ -77,13 +96,14 @@ export function attachDroplet(row, options = {}) {
|
|
|
77
96
|
const l = (r.left - f.n.left) / f.sx - row.clientLeft + row.scrollLeft;
|
|
78
97
|
const t = (r.top - f.n.top) / f.sy - row.clientTop + row.scrollTop;
|
|
79
98
|
const height = r.height / f.sy;
|
|
80
|
-
return { left: l, width: r.width / f.sx, top: t, bottom: row.clientHeight - t - height };
|
|
99
|
+
return { left: l, width: r.width / f.sx, top: t, height, bottom: row.clientHeight - t - height };
|
|
81
100
|
};
|
|
82
|
-
const put =
|
|
83
|
-
|
|
84
|
-
set('left',
|
|
85
|
-
set('top', b.top); set('bottom', b.
|
|
101
|
+
const put = b => {
|
|
102
|
+
pos = axisY ? b.top : b.left;
|
|
103
|
+
set('left', b.left); set('right', row.clientWidth - b.left - b.width);
|
|
104
|
+
set('top', b.top); set('bottom', row.clientHeight - b.top - b.height);
|
|
86
105
|
};
|
|
106
|
+
const centreOf = b => axisY ? b.top + b.height / 2 : b.left + b.width / 2;
|
|
87
107
|
|
|
88
108
|
// The usable item nearest a point of the row lights up as under a lens.
|
|
89
109
|
const lens = centre => {
|
|
@@ -91,7 +111,7 @@ export function attachDroplet(row, options = {}) {
|
|
|
91
111
|
const f = centre == null ? null : frame();
|
|
92
112
|
if (f) for (const it of items()) {
|
|
93
113
|
if (!usable(it)) continue;
|
|
94
|
-
const
|
|
114
|
+
const d = Math.abs(centreOf(boxOf(it, f)) - centre);
|
|
95
115
|
if (d < best) { best = d; below = it; }
|
|
96
116
|
}
|
|
97
117
|
if (below !== under) {
|
|
@@ -101,33 +121,43 @@ export function attachDroplet(row, options = {}) {
|
|
|
101
121
|
}
|
|
102
122
|
return below;
|
|
103
123
|
};
|
|
104
|
-
// While the drop travels, the lens follows its centre
|
|
124
|
+
// While the drop travels, the lens follows its centre — as long as its
|
|
125
|
+
// edges travel by the tokens, the trailing one the longer (620 ms unset).
|
|
105
126
|
const follow = () => {
|
|
106
127
|
cancelAnimationFrame(lensFrame);
|
|
107
128
|
const start = performance.now();
|
|
129
|
+
const length = Math.max(token('duration', 300), token('trail-delay', 40) + token('trail-duration', 500)) + 80;
|
|
108
130
|
const step = now => {
|
|
109
|
-
if (now - start >
|
|
131
|
+
if (now - start > length) { lens(null); return; }
|
|
110
132
|
const cs = getComputedStyle(row, '::before');
|
|
111
|
-
|
|
112
|
-
|
|
133
|
+
if (axisY) {
|
|
134
|
+
const t = parseFloat(cs.top), b = parseFloat(cs.bottom);
|
|
135
|
+
lens(t + (row.clientHeight - t - b) / 2);
|
|
136
|
+
} else {
|
|
137
|
+
const l = parseFloat(cs.left), r = parseFloat(cs.right);
|
|
138
|
+
lens(l + (row.clientWidth - l - r) / 2);
|
|
139
|
+
}
|
|
113
140
|
lensFrame = requestAnimationFrame(step);
|
|
114
141
|
};
|
|
115
142
|
lensFrame = requestAnimationFrame(step);
|
|
116
143
|
};
|
|
117
144
|
|
|
118
145
|
const place = (it, travel) => {
|
|
119
|
-
|
|
146
|
+
axisY = vertical();
|
|
147
|
+
if (axisY) row.dataset.dropletAxis = 'y';
|
|
148
|
+
else delete row.dataset.dropletAxis;
|
|
149
|
+
const b = boxOf(it), to = axisY ? b.top : b.left;
|
|
120
150
|
row.dataset.droplet = '';
|
|
121
|
-
if (travel && motion.matches && Math.abs(
|
|
122
|
-
row.dataset.dropletDir =
|
|
151
|
+
if (travel && motion.matches && Math.abs(to - pos) > 1) {
|
|
152
|
+
row.dataset.dropletDir = axisY ? (to > pos ? 'down' : 'up') : (to > pos ? 'right' : 'left');
|
|
123
153
|
row.dataset.dropletLift = '';
|
|
124
154
|
clearTimeout(liftTimer);
|
|
125
|
-
liftTimer = setTimeout(() => { delete row.dataset.dropletLift; }, 280);
|
|
155
|
+
liftTimer = setTimeout(() => { delete row.dataset.dropletLift; }, token('lift-duration', 280));
|
|
126
156
|
follow();
|
|
127
157
|
} else {
|
|
128
158
|
delete row.dataset.dropletDir;
|
|
129
159
|
}
|
|
130
|
-
put(b
|
|
160
|
+
put(b);
|
|
131
161
|
placed = { ...b, rowWidth: row.clientWidth, rowHeight: row.clientHeight };
|
|
132
162
|
chosen = it;
|
|
133
163
|
};
|
|
@@ -155,34 +185,42 @@ export function attachDroplet(row, options = {}) {
|
|
|
155
185
|
|
|
156
186
|
// Dragging: past 8px the drop slides under the finger and grows into a
|
|
157
187
|
// lens; let go, the item below it is chosen and the drop settles on it.
|
|
158
|
-
|
|
188
|
+
// Along the row's axis: x in a row, y in a column.
|
|
189
|
+
const at = client => {
|
|
159
190
|
const f = frame();
|
|
160
|
-
const
|
|
191
|
+
const p = axisY
|
|
192
|
+
? (client - f.n.top) / f.sy - row.clientTop + row.scrollTop
|
|
193
|
+
: (client - f.n.left) / f.sx - row.clientLeft + row.scrollLeft;
|
|
161
194
|
const boxes = items().map(it => boxOf(it, f));
|
|
162
|
-
const min = Math.min(...boxes.map(b => b.left));
|
|
163
|
-
const max = Math.max(...boxes.map(b => b.left + b.width)) - drag.
|
|
164
|
-
return Math.min(max, Math.max(min,
|
|
195
|
+
const min = Math.min(...boxes.map(b => axisY ? b.top : b.left));
|
|
196
|
+
const max = Math.max(...boxes.map(b => axisY ? b.top + b.height : b.left + b.width)) - drag.size;
|
|
197
|
+
return Math.min(max, Math.max(min, p - drag.size / 2));
|
|
165
198
|
};
|
|
166
199
|
const track = () => {
|
|
167
|
-
const
|
|
168
|
-
put(
|
|
169
|
-
drag.below = lens(
|
|
200
|
+
const p = at(drag.client), b = boxOf(chosen);
|
|
201
|
+
put(axisY ? { ...b, top: p, height: drag.size } : { ...b, left: p, width: drag.size });
|
|
202
|
+
drag.below = lens(p + drag.size / 2);
|
|
170
203
|
cancelAnimationFrame(edgeFrame);
|
|
171
204
|
if (!scrolls()) return;
|
|
172
205
|
const n = row.getBoundingClientRect(), zone = 32;
|
|
173
|
-
const
|
|
174
|
-
|
|
206
|
+
const lo = axisY ? n.top : n.left, hi = axisY ? n.bottom : n.right;
|
|
207
|
+
const v = drag.client < lo + zone ? -1 : drag.client > hi - zone ? 1 : 0;
|
|
208
|
+
if (v) edgeFrame = requestAnimationFrame(() => {
|
|
209
|
+
if (axisY) row.scrollTop += v * 6; else row.scrollLeft += v * 6;
|
|
210
|
+
if (drag?.moved) track();
|
|
211
|
+
});
|
|
175
212
|
};
|
|
176
213
|
const down = e => {
|
|
177
214
|
if (!chosen || drag || !usable(chosen) || (e.pointerType === 'mouse' && e.button !== 0)) return;
|
|
178
215
|
if (scrolls() && !e.composedPath().includes(chosen)) return;
|
|
179
|
-
|
|
216
|
+
const b = boxOf(chosen), client = axisY ? e.clientY : e.clientX;
|
|
217
|
+
drag = { id: e.pointerId, start: client, client, size: axisY ? b.height : b.width, moved: false, below: null };
|
|
180
218
|
};
|
|
181
219
|
const move = e => {
|
|
182
220
|
if (!drag || e.pointerId !== drag.id) return;
|
|
183
|
-
drag.
|
|
221
|
+
drag.client = axisY ? e.clientY : e.clientX;
|
|
184
222
|
if (!drag.moved) {
|
|
185
|
-
if (Math.abs(
|
|
223
|
+
if (Math.abs(drag.client - drag.start) < 8) return;
|
|
186
224
|
drag.moved = true;
|
|
187
225
|
row.setPointerCapture(e.pointerId);
|
|
188
226
|
delete row.dataset.dropletDir;
|
|
@@ -226,7 +264,7 @@ export function attachDroplet(row, options = {}) {
|
|
|
226
264
|
row.removeEventListener('pointercancel', up);
|
|
227
265
|
row.removeEventListener('click', click, true);
|
|
228
266
|
lens(null);
|
|
229
|
-
for (const k of ['droplet', 'dropletDir', 'dropletLift', 'dropletDrag']) delete row.dataset[k];
|
|
267
|
+
for (const k of ['droplet', 'dropletDir', 'dropletLift', 'dropletDrag', 'dropletAxis']) delete row.dataset[k];
|
|
230
268
|
for (const k of ['left', 'right', 'top', 'bottom']) row.style.removeProperty(`--gl-droplet-${k}`);
|
|
231
269
|
attached.delete(row);
|
|
232
270
|
},
|