@yewdiel21/motion-kit 0.15.0 → 0.16.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/README.md +4 -0
- package/dist/auto/index.d.ts +3 -3
- package/dist/auto/index.js +2 -2
- package/dist/{chunk-6IRJYXJU.js → chunk-NFZIX3IA.js} +2 -2
- package/dist/{chunk-JXRS3OEA.js → chunk-RBEKR5UZ.js} +29 -7
- package/dist/chunk-RBEKR5UZ.js.map +1 -0
- package/dist/{chunk-A6QQCF6V.js → chunk-SDQ776LG.js} +129 -17
- package/dist/chunk-SDQ776LG.js.map +1 -0
- package/dist/css/components/chart.css +7 -0
- package/dist/css/components/heatmap-chart.css +7 -0
- package/dist/css/components/pie-chart.css +7 -0
- package/dist/css/components/radar-chart.css +7 -0
- package/dist/css/components/radial-chart.css +16 -2
- package/dist/css/components/sankey-chart.css +7 -0
- package/dist/css/components/scroll-island.css +3 -0
- package/dist/elements/index.d.ts +3 -3
- package/dist/elements/index.js +11 -6
- package/dist/elements/index.js.map +1 -1
- package/dist/index.css +19 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/react/index.d.ts +9 -9
- package/dist/react/index.js +16 -10
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-6X_bns60.d.ts → reduced-DQwVXQYb.d.ts} +1 -1
- package/dist/{reveal-BfyKKsw4.d.ts → reveal-DRpNO4KP.d.ts} +12 -0
- package/dist/{store-CA2Qcun_.d.ts → store-Da3zSh8Z.d.ts} +5 -1
- package/package.json +1 -1
- package/src/components/add-to-cart/README.md +49 -34
- package/src/components/add-to-cart/react.tsx +4 -0
- package/src/components/chart/README.md +49 -34
- package/src/components/chart/chart.css +7 -0
- package/src/components/chart/element.ts +2 -1
- package/src/components/chart/engine.ts +105 -13
- package/src/components/chart/react.tsx +2 -2
- package/src/components/radial-chart/README.md +10 -0
- package/src/components/radial-chart/element.ts +2 -2
- package/src/components/radial-chart/engine.ts +33 -6
- package/src/components/radial-chart/radial-chart.css +9 -2
- package/src/components/radial-chart/react.tsx +2 -2
- package/src/components/scroll-island/README.md +44 -34
- package/src/components/scroll-island/element.ts +1 -1
- package/src/components/scroll-island/engine.ts +25 -3
- package/src/components/scroll-island/react.tsx +3 -3
- package/src/components/scroll-island/scroll-island.css +3 -0
- package/dist/chunk-A6QQCF6V.js.map +0 -1
- package/dist/chunk-JXRS3OEA.js.map +0 -1
- /package/dist/{chunk-6IRJYXJU.js.map → chunk-NFZIX3IA.js.map} +0 -0
|
@@ -3,7 +3,7 @@ import { reduced } from '../../core/reduced';
|
|
|
3
3
|
import { createScope } from '../../core/scope';
|
|
4
4
|
import { inert, type Instance } from '../../core/types';
|
|
5
5
|
import { blurIn, rollDigits } from '../../motion/micro';
|
|
6
|
-
import { chartFrame, finite, formatNumber, onWidth, polar, round, slotColor, svg } from '../chart/parts';
|
|
6
|
+
import { arcPath, chartFrame, finite, formatNumber, onWidth, polar, round, slotColor, svg } from '../chart/parts';
|
|
7
7
|
|
|
8
8
|
/*
|
|
9
9
|
* Radial chart: totals as rings, one inside the other, each filling its track
|
|
@@ -33,6 +33,10 @@ export interface RadialChartOptions {
|
|
|
33
33
|
size?: number;
|
|
34
34
|
/** Ring thickness in px (default: what fits, up to 16). */
|
|
35
35
|
thickness?: number;
|
|
36
|
+
/** Discrete sectors per ring (0: continuous, default). */
|
|
37
|
+
segments?: number;
|
|
38
|
+
/** Show values (default) or progress toward the goals in the middle. */
|
|
39
|
+
center?: 'value' | 'percent';
|
|
36
40
|
label?: string;
|
|
37
41
|
/** The middle when nothing is chosen (default 'Total'). */
|
|
38
42
|
totalLabel?: string;
|
|
@@ -90,6 +94,7 @@ export function createRadialChart(root: HTMLElement | null, initial: RadialChart
|
|
|
90
94
|
const percent = (ring: { value: number; max: number }) => new Intl.NumberFormat(undefined, { style: 'percent', maximumFractionDigits: 0 }).format(ring.value / ring.max);
|
|
91
95
|
const sweep = () => (Math.min(360, Math.max(90, finite(options.sweep) ? options.sweep : 360)) * Math.PI) / 180;
|
|
92
96
|
const startAngle = () => -sweep() / 2 + (sweep() >= Math.PI * 2 - 1e-6 ? Math.PI : 0);
|
|
97
|
+
const segments = () => finite(options.segments) ? Math.max(0, Math.min(80, Math.round(options.segments))) : 0;
|
|
93
98
|
|
|
94
99
|
let geometry = { size: 240, cx: 120, cy: 120, radii: [] as number[], thickness: 12 };
|
|
95
100
|
let arcs: SVGPathElement[] = [];
|
|
@@ -109,17 +114,35 @@ export function createRadialChart(root: HTMLElement | null, initial: RadialChart
|
|
|
109
114
|
const thickness = finite(options.thickness) ? options.thickness : Math.max(4, Math.min(16, fit));
|
|
110
115
|
const spacing = thickness * 1.35;
|
|
111
116
|
geometry = { size, cx: size / 2, cy: size / 2, thickness, radii: model.map((_, i) => outer - thickness / 2 - i * spacing) };
|
|
112
|
-
|
|
117
|
+
const half = sweep() <= Math.PI + 1e-6;
|
|
118
|
+
const height = half ? size / 2 + 54 : size;
|
|
119
|
+
canvas.setAttribute('viewBox', `0 0 ${size} ${height}`);
|
|
113
120
|
canvas.setAttribute('width', String(size));
|
|
114
|
-
canvas.setAttribute('height', String(
|
|
121
|
+
canvas.setAttribute('height', String(height));
|
|
115
122
|
plot.style.setProperty('--mk-radial-size', `${size}px`);
|
|
116
123
|
plot.style.setProperty('--mk-radial-thickness', `${thickness}px`);
|
|
124
|
+
plot.style.setProperty('--mk-radial-center-y', half ? `${size / 2}px` : '50%');
|
|
125
|
+
plot.style.setProperty('--mk-radial-center-width', n === 1 ? '0.68' : '0.36');
|
|
117
126
|
};
|
|
118
127
|
scope.add(() => {
|
|
119
128
|
plot.style.removeProperty('--mk-radial-size');
|
|
120
129
|
plot.style.removeProperty('--mk-radial-thickness');
|
|
130
|
+
plot.style.removeProperty('--mk-radial-center-y');
|
|
131
|
+
plot.style.removeProperty('--mk-radial-center-width');
|
|
121
132
|
});
|
|
122
133
|
|
|
134
|
+
const segmentedPath = (radius: number, share: number) => {
|
|
135
|
+
const count = segments(), step = sweep() / count, a0 = startAngle();
|
|
136
|
+
const gap = Math.min(step * 0.3, 4 / Math.max(1, radius));
|
|
137
|
+
const limit = a0 + sweep() * Math.min(1, Math.max(0, share));
|
|
138
|
+
const outer = radius + geometry.thickness / 2 - 1, inner = Math.max(0, radius - geometry.thickness / 2 + 1);
|
|
139
|
+
return Array.from({ length: count }, (_, index) => {
|
|
140
|
+
const from = a0 + index * step + gap / 2;
|
|
141
|
+
const to = Math.min(a0 + (index + 1) * step - gap / 2, limit);
|
|
142
|
+
return to > from ? arcPath(geometry.cx, geometry.cy, outer, inner, from, to) : '';
|
|
143
|
+
}).join('');
|
|
144
|
+
};
|
|
145
|
+
|
|
123
146
|
const build = () => {
|
|
124
147
|
canvas.replaceChildren();
|
|
125
148
|
const { cx, cy, radii } = geometry;
|
|
@@ -127,13 +150,14 @@ export function createRadialChart(root: HTMLElement | null, initial: RadialChart
|
|
|
127
150
|
const back = svg('g', { 'data-mk-part': 'tracks' });
|
|
128
151
|
const front = svg('g', { 'data-mk-part': 'series' });
|
|
129
152
|
tracks = model.map((_, i) => {
|
|
130
|
-
const track = svg('path', { d: strokeArc(cx, cy, radii[i]!, a0, a0 + sweep()), 'data-mk-index': i });
|
|
153
|
+
const track = svg('path', { d: segments() ? segmentedPath(radii[i]!, 1) : strokeArc(cx, cy, radii[i]!, a0, a0 + sweep()), 'data-mk-index': i });
|
|
131
154
|
back.append(track);
|
|
132
155
|
return track;
|
|
133
156
|
});
|
|
134
157
|
arcs = model.map((ring, i) => {
|
|
135
158
|
const arc = svg('path', { 'data-mk-index': i, 'data-mk-part': 'ring' });
|
|
136
159
|
arc.style.stroke = ring.color;
|
|
160
|
+
if (segments()) arc.style.fill = ring.color;
|
|
137
161
|
front.append(arc);
|
|
138
162
|
return arc;
|
|
139
163
|
});
|
|
@@ -146,14 +170,16 @@ export function createRadialChart(root: HTMLElement | null, initial: RadialChart
|
|
|
146
170
|
const a0 = startAngle();
|
|
147
171
|
arcs.forEach((arc, i) => {
|
|
148
172
|
const share = Math.min(1, Math.max(0, (values[i] ?? 0) / model[i]!.max));
|
|
149
|
-
arc.setAttribute('d', share > 0.001 ? strokeArc(cx, cy, radii[i]!, a0, a0 + sweep() * share) : '');
|
|
173
|
+
arc.setAttribute('d', share > 0.001 ? segments() ? segmentedPath(radii[i]!, share) : strokeArc(cx, cy, radii[i]!, a0, a0 + sweep() * share) : '');
|
|
150
174
|
});
|
|
151
175
|
};
|
|
152
176
|
|
|
153
177
|
const paintCenter = (animate: boolean) => {
|
|
154
178
|
center.hidden = !model.length;
|
|
155
179
|
const ring = selected === null ? null : model[selected];
|
|
156
|
-
const
|
|
180
|
+
const denominator = ring ? ring.max : model.reduce((sum, each) => sum + each.max, 0);
|
|
181
|
+
const value = ring ? ring.value : total();
|
|
182
|
+
const text = options.center === 'percent' ? new Intl.NumberFormat(undefined, { style: 'percent', maximumFractionDigits: 1 }).format(value / (denominator || 1)) : format(value);
|
|
157
183
|
const caption = ring ? `${ring.label} · ${percent(ring)}` : (options.totalLabel ?? 'Total');
|
|
158
184
|
const changed = centerLabel.textContent !== caption;
|
|
159
185
|
if (centerValue.textContent !== text) {
|
|
@@ -178,6 +204,7 @@ export function createRadialChart(root: HTMLElement | null, initial: RadialChart
|
|
|
178
204
|
|
|
179
205
|
const draw = (animate: boolean) => {
|
|
180
206
|
glide?.kill();
|
|
207
|
+
scope.attr(root, 'data-mk-segmented', segments() ? '' : null);
|
|
181
208
|
const name = options.label ?? 'Gráfica radial';
|
|
182
209
|
canvas.setAttribute('aria-label', model.length ? `${name}. ${model.length} anillos, total ${format(total())}. Usa las flechas para recorrerlos.` : `${name}: ${options.emptyLabel ?? 'sin datos'}`);
|
|
183
210
|
frame.fillTable(name, ['Anillo', 'Valor', 'Meta', 'Avance'], model.map((ring) => [ring.label, format(ring.value), format(ring.max), percent(ring)]));
|
|
@@ -32,6 +32,13 @@ mk-radial-chart {
|
|
|
32
32
|
stroke: var(--mk-chart-grid);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
[data-mk-chart-type='radial'][data-mk-segmented] :is([data-mk-part='tracks'], [data-mk-part='series']) path {
|
|
36
|
+
stroke-width: 1px;
|
|
37
|
+
stroke-linejoin: round;
|
|
38
|
+
}
|
|
39
|
+
[data-mk-chart-type='radial'][data-mk-segmented] [data-mk-part='tracks'] path { fill: var(--mk-chart-track); stroke: var(--mk-chart-track); }
|
|
40
|
+
[data-mk-chart-type='radial'][data-mk-segmented] [data-mk-part='series'] path[data-mk-on] { filter: brightness(1.12); stroke-width: 2px; }
|
|
41
|
+
|
|
35
42
|
[data-mk-chart-type='radial'] :is([data-mk-part='tracks'], [data-mk-part='series']) path[data-mk-on] {
|
|
36
43
|
stroke-width: calc(var(--mk-radial-thickness, 12px) + 4px);
|
|
37
44
|
}
|
|
@@ -42,12 +49,12 @@ mk-radial-chart {
|
|
|
42
49
|
|
|
43
50
|
[data-mk-radial-center] {
|
|
44
51
|
position: absolute;
|
|
45
|
-
top: 50
|
|
52
|
+
top: var(--mk-radial-center-y, 50%);
|
|
46
53
|
left: 50%;
|
|
47
54
|
display: grid;
|
|
48
55
|
justify-items: center;
|
|
49
56
|
gap: 0.15rem;
|
|
50
|
-
max-width: calc(var(--mk-radial-size, 240px) * 0.36);
|
|
57
|
+
max-width: calc(var(--mk-radial-size, 240px) * var(--mk-radial-center-width, 0.36));
|
|
51
58
|
transform: translate(-50%, -50%);
|
|
52
59
|
text-align: center;
|
|
53
60
|
pointer-events: none;
|
|
@@ -8,7 +8,7 @@ export interface RadialChartProps extends Omit<HTMLAttributes<HTMLDivElement>, '
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/** <RadialChart label="Metas" data={[{ label: 'Pasos', value: 7400, max: 10000 }, …]} /> */
|
|
11
|
-
export function RadialChart({ data, max, sweep, size, thickness, label, totalLabel, legend, emptyLabel, formatValue, onSelect, instanceRef, className, ...props }: RadialChartProps) {
|
|
12
|
-
const ref = useChart(createRadialChart, { data, max, sweep, size, thickness, label, totalLabel, legend, emptyLabel } as RadialChartOptions, { formatValue, onSelect }, instanceRef);
|
|
11
|
+
export function RadialChart({ data, max, sweep, size, thickness, segments, center, label, totalLabel, legend, emptyLabel, formatValue, onSelect, instanceRef, className, ...props }: RadialChartProps) {
|
|
12
|
+
const ref = useChart(createRadialChart, { data, max, sweep, size, thickness, segments, center, label, totalLabel, legend, emptyLabel } as RadialChartOptions, { formatValue, onSelect }, instanceRef);
|
|
13
13
|
return <div {...props} ref={ref} className={['mk-chart mk-radial-chart', className].filter(Boolean).join(' ')} />;
|
|
14
14
|
}
|
|
@@ -1,34 +1,44 @@
|
|
|
1
|
-
# scroll-island
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
</
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
# scroll-island
|
|
2
|
+
|
|
3
|
+
Una barra fina que se transforma en una píldora con el porcentaje y la sección actual al acercar el puntero al borde derecho. Arrastra la píldora o pulsa su carril para desplazarte.
|
|
4
|
+
|
|
5
|
+
## React
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import '@yewdiel21/motion-kit/css';
|
|
9
|
+
import { ScrollIsland } from '@yewdiel21/motion-kit/react';
|
|
10
|
+
|
|
11
|
+
<ScrollIsland persistent style={{ height: 360, overflow: 'auto' }}
|
|
12
|
+
tabIndex={0} role="region" aria-label="Guía de diseño">
|
|
13
|
+
<section style={{ minHeight: 400 }}><h3>Introducción</h3>Contenido…</section>
|
|
14
|
+
<section style={{ minHeight: 400 }}><h3>Detalles</h3>Contenido…</section>
|
|
15
|
+
</ScrollIsland>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**El contenedor debe tener altura limitada, overflow y contenido suficiente para desplazarse.** Si todo cabe, la barra se oculta. `persistent` deja visible la barra fina mientras está en reposo; por defecto aparece al desplazarse y desaparece después.
|
|
19
|
+
|
|
20
|
+
## HTML / Astro
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<mk-scroll-island persistent style="display:block;height:360px;overflow:auto"
|
|
24
|
+
tabindex="0" role="region" aria-label="Guía">
|
|
25
|
+
<section style="min-height:400px"><h3>Introducción</h3>Contenido…</section>
|
|
26
|
+
<section style="min-height:400px"><h3>Detalles</h3>Contenido…</section>
|
|
27
|
+
</mk-scroll-island>
|
|
28
|
+
<script>
|
|
29
|
+
import { defineElements } from '@yewdiel21/motion-kit/elements';
|
|
30
|
+
defineElements(['mk-scroll-island']);
|
|
31
|
+
</script>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Opciones
|
|
35
|
+
|
|
36
|
+
- `page`: sigue el scroll de la página en vez del contenedor. En React `<ScrollIsland page />` no dibuja contenedor propio.
|
|
37
|
+
- `sections`: selector de encabezados, `h2, h3` por defecto; `false` omite el nombre de sección en la factory/React.
|
|
38
|
+
- `reach`: distancia del borde que activa la píldora, 56 px.
|
|
39
|
+
- `idle`: demora para ocultarse, 900 ms.
|
|
40
|
+
- `persistent`: mantiene la barra fina visible, false por defecto.
|
|
41
|
+
- `onSection(section)`: cambio de sección, también `mk:section` con `detail.section`.
|
|
42
|
+
- Factory `createScrollIsland(root, options)`; instancia `refresh()` y `destroy()`.
|
|
43
|
+
|
|
44
|
+
El overlay decorativo usa `aria-hidden`; el contenido mantiene su desplazamiento nativo por teclado y tacto. Dale `tabindex="0"` y un nombre al panel. Respeta movimiento reducido, se adapta al tamaño del contenedor y elimina el overlay al desmontarse.
|
|
@@ -6,7 +6,7 @@ import { createScrollIsland, type ScrollIslandInstance } from './engine';
|
|
|
6
6
|
* or <mk-scroll-island page></mk-scroll-island> for the page. Emits mk:section ({section}).
|
|
7
7
|
*/
|
|
8
8
|
export class MkScrollIslandElement extends MkElement<ScrollIslandInstance> {
|
|
9
|
-
static options: Record<string, OptionType> = { sections: 'string', reach: 'number', idle: 'number', page: 'boolean' };
|
|
9
|
+
static options: Record<string, OptionType> = { sections: 'string', reach: 'number', idle: 'number', persistent: 'boolean', page: 'boolean' };
|
|
10
10
|
|
|
11
11
|
protected create(options: Record<string, unknown>) {
|
|
12
12
|
return createScrollIsland(options.page ? document.documentElement : this, options);
|
|
@@ -23,6 +23,8 @@ export interface ScrollIslandOptions {
|
|
|
23
23
|
reach?: number;
|
|
24
24
|
/** ms the bar stays after scrolling stops (default 900). */
|
|
25
25
|
idle?: number;
|
|
26
|
+
/** Keep the thin bar visible while idle (default false). */
|
|
27
|
+
persistent?: boolean;
|
|
26
28
|
/** Follow the page's scroll (default: when the root is <html> or <body>). */
|
|
27
29
|
page?: boolean;
|
|
28
30
|
/** The current section changed (its heading's text, or null before the first). */
|
|
@@ -52,6 +54,7 @@ export function createScrollIsland(root: HTMLElement | null, options: ScrollIsla
|
|
|
52
54
|
const marked = page ? document.documentElement : root;
|
|
53
55
|
// The scroller's own bar hides (scroll-island.css).
|
|
54
56
|
scope.attr(marked, 'data-mk-scroll-island-host', '');
|
|
57
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
55
58
|
|
|
56
59
|
const island = document.createElement('div');
|
|
57
60
|
island.dataset.mkScrollIsland = '';
|
|
@@ -99,6 +102,16 @@ export function createScrollIsland(root: HTMLElement | null, options: ScrollIsla
|
|
|
99
102
|
let section: string | null | undefined;
|
|
100
103
|
let hideTimer: ReturnType<typeof setTimeout> | undefined;
|
|
101
104
|
let dragging: { id: number; offset: number } | null = null;
|
|
105
|
+
let scrollBehavior: { value: string; priority: string; hadStyle: boolean } | null = null;
|
|
106
|
+
const restoreBehavior = () => {
|
|
107
|
+
if (!scrollBehavior) return;
|
|
108
|
+
const previous = scrollBehavior;
|
|
109
|
+
if (previous.value) scroller.style.setProperty('scroll-behavior', previous.value, previous.priority);
|
|
110
|
+
else scroller.style.removeProperty('scroll-behavior');
|
|
111
|
+
if (!previous.hadStyle && !scroller.getAttribute('style')) scroller.removeAttribute('style');
|
|
112
|
+
scrollBehavior = null;
|
|
113
|
+
};
|
|
114
|
+
scope.add(restoreBehavior);
|
|
102
115
|
scope.add(() => clearTimeout(hideTimer));
|
|
103
116
|
|
|
104
117
|
const headings = () => (options.sections === false ? [] : [...(page ? document.body : root).querySelectorAll<HTMLElement>(options.sections ?? 'h2, h3')]);
|
|
@@ -116,7 +129,7 @@ export function createScrollIsland(root: HTMLElement | null, options: ScrollIsla
|
|
|
116
129
|
const size = as === 'island' ? ISLAND_HEIGHT : Math.max(MIN_BAR, Math.min(height, height * (scroller.clientHeight / Math.max(1, scroller.scrollHeight))));
|
|
117
130
|
const top = (height - size) * progress();
|
|
118
131
|
if (as !== 'island') return { top, height: size, width: BAR_WIDTH, borderRadius: BAR_WIDTH / 2 };
|
|
119
|
-
return { top, height: size, width: Math.max(ISLAND_HEIGHT, content.scrollWidth + 28), borderRadius: size / 2 };
|
|
132
|
+
return { top, height: size, width: Math.min(Math.max(ISLAND_HEIGHT, track().right - track().left - INSET * 2), Math.max(ISLAND_HEIGHT, content.scrollWidth + 28)), borderRadius: size / 2 };
|
|
120
133
|
};
|
|
121
134
|
|
|
122
135
|
const place = () => {
|
|
@@ -158,7 +171,7 @@ export function createScrollIsland(root: HTMLElement | null, options: ScrollIsla
|
|
|
158
171
|
const settleLater = () => {
|
|
159
172
|
clearTimeout(hideTimer);
|
|
160
173
|
hideTimer = setTimeout(() => {
|
|
161
|
-
if (state === 'bar' && !dragging) go('hidden');
|
|
174
|
+
if (state === 'bar' && !dragging && !options.persistent) go('hidden');
|
|
162
175
|
}, idle);
|
|
163
176
|
};
|
|
164
177
|
|
|
@@ -213,6 +226,9 @@ export function createScrollIsland(root: HTMLElement | null, options: ScrollIsla
|
|
|
213
226
|
if (state !== 'island' || down.button !== 0) return;
|
|
214
227
|
down.preventDefault();
|
|
215
228
|
down.stopPropagation();
|
|
229
|
+
if (!scrollBehavior) scrollBehavior = { value: scroller.style.getPropertyValue('scroll-behavior'), priority: scroller.style.getPropertyPriority('scroll-behavior'), hadStyle: scroller.hasAttribute('style') };
|
|
230
|
+
// A drag follows the pointer directly even when the app uses smooth scrolling.
|
|
231
|
+
scroller.style.setProperty('scroll-behavior', 'auto', 'important');
|
|
216
232
|
const box = thumb.getBoundingClientRect();
|
|
217
233
|
dragging = { id: down.pointerId, offset: down.clientY - box.top || ISLAND_HEIGHT / 2 };
|
|
218
234
|
island.setAttribute('data-mk-dragging', '');
|
|
@@ -225,6 +241,7 @@ export function createScrollIsland(root: HTMLElement | null, options: ScrollIsla
|
|
|
225
241
|
const release = (event: Event) => {
|
|
226
242
|
if (!dragging || (event as PointerEvent).pointerId !== dragging.id) return;
|
|
227
243
|
dragging = null;
|
|
244
|
+
restoreBehavior();
|
|
228
245
|
island.removeAttribute('data-mk-dragging');
|
|
229
246
|
};
|
|
230
247
|
scope.on(window, 'pointerup', release);
|
|
@@ -240,7 +257,12 @@ export function createScrollIsland(root: HTMLElement | null, options: ScrollIsla
|
|
|
240
257
|
});
|
|
241
258
|
|
|
242
259
|
paintText();
|
|
243
|
-
go('hidden');
|
|
260
|
+
go(options.persistent ? 'bar' : 'hidden');
|
|
261
|
+
if (typeof ResizeObserver === 'function') {
|
|
262
|
+
const observer = scope.observe(new ResizeObserver(() => { paintText(); go(state, true); }));
|
|
263
|
+
observer.observe(root);
|
|
264
|
+
for (const child of root.children) observer.observe(child);
|
|
265
|
+
}
|
|
244
266
|
|
|
245
267
|
return {
|
|
246
268
|
refresh: () => {
|
|
@@ -9,14 +9,14 @@ export interface ScrollIslandProps extends HTMLAttributes<HTMLDivElement>, Scrol
|
|
|
9
9
|
* <ScrollIsland style={{ height: 400, overflow: 'auto' }}>…</ScrollIsland> scrolls its children with the island;
|
|
10
10
|
* <ScrollIsland page /> puts it on the page (it renders nothing of its own then).
|
|
11
11
|
*/
|
|
12
|
-
export function ScrollIsland({ sections, reach, idle, page, onSection, className, children, ...props }: ScrollIslandProps) {
|
|
12
|
+
export function ScrollIsland({ sections, reach, idle, persistent, page, onSection, className, children, ...props }: ScrollIslandProps) {
|
|
13
13
|
const ref = useRef<HTMLDivElement>(null);
|
|
14
14
|
const section = useLatest(onSection);
|
|
15
15
|
useIsomorphicLayoutEffect(() => {
|
|
16
|
-
const created = createScrollIsland(page ? document.documentElement : ref.current, { sections, reach, idle, page, onSection: (name) => section.current?.(name) });
|
|
16
|
+
const created = createScrollIsland(page ? document.documentElement : ref.current, { sections, reach, idle, persistent, page, onSection: (name) => section.current?.(name) });
|
|
17
17
|
return () => created.destroy();
|
|
18
18
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
19
|
-
}, [sections, reach, idle, page]);
|
|
19
|
+
}, [sections, reach, idle, persistent, page]);
|
|
20
20
|
if (page) return null;
|
|
21
21
|
return (
|
|
22
22
|
<div {...props} ref={ref} className={['mk-scroll-island', className].filter(Boolean).join(' ')}>
|
|
@@ -73,6 +73,8 @@ mk-scroll-island {
|
|
|
73
73
|
[data-mk-island-content] {
|
|
74
74
|
display: inline-flex;
|
|
75
75
|
flex: none;
|
|
76
|
+
max-width: 100%;
|
|
77
|
+
min-width: 0;
|
|
76
78
|
align-items: center;
|
|
77
79
|
gap: 0.5rem;
|
|
78
80
|
padding: 0 14px;
|
|
@@ -91,6 +93,7 @@ mk-scroll-island {
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
[data-mk-island-section] {
|
|
96
|
+
min-width: 0;
|
|
94
97
|
max-width: 12rem;
|
|
95
98
|
overflow: hidden;
|
|
96
99
|
color: rgb(255 255 255 / 0.68);
|