@hanzo/ui 8.1.18 → 8.1.20
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/dist/dots.cjs +208 -0
- package/dist/dots.d.ts +42 -0
- package/dist/dots.d.ts.map +1 -0
- package/dist/dots.js +205 -0
- package/dist/dots.js.map +1 -0
- package/dist/gallery.html +141 -1
- package/dist/styles/motion.css +140 -0
- package/dist/styles.css +140 -0
- package/package.json +6 -1
package/dist/dots.cjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use client';"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.SWELL = exports.Dots = void 0;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
/**
|
|
7
|
+
* Dots — anything, sampled onto a grid of dots.
|
|
8
|
+
*
|
|
9
|
+
* A halftone screen: divide a surface into cells, measure how bright the source
|
|
10
|
+
* is at each cell, and draw a dot whose radius carries that measurement. It is
|
|
11
|
+
* the oldest trick in reproduction and it still reads as considered, because the
|
|
12
|
+
* grid is regular and only the ink varies.
|
|
13
|
+
*
|
|
14
|
+
* Three sources, one mechanism. Whatever it is gets rasterised once into an
|
|
15
|
+
* offscreen canvas, then read back per cell — so an image, a word and a moving
|
|
16
|
+
* field all take the same path and there is one place the sampling happens:
|
|
17
|
+
*
|
|
18
|
+
* <Dots src="/hero.png" /> a picture
|
|
19
|
+
* <Dots text="Ship Apps." /> type
|
|
20
|
+
* <Dots field={(x, y, t) => …} /> a function of space and time
|
|
21
|
+
*
|
|
22
|
+
* With none of them it draws the default field, a slow diagonal swell, which is
|
|
23
|
+
* the ambient case a page usually wants behind a headline.
|
|
24
|
+
*
|
|
25
|
+
* WEB ONLY, and off the barrel deliberately. `2d` canvas has no counterpart on
|
|
26
|
+
* React Native, and this package's promise is that a root import runs on native
|
|
27
|
+
* — so putting this in the barrel would make that false for every consumer. It
|
|
28
|
+
* lives at `@hanzo/ui/dots`, the same quarantine `product/theme-toggle-next`
|
|
29
|
+
* uses for its Next dependency: a caller reaches for it on purpose.
|
|
30
|
+
*
|
|
31
|
+
* It renders the canvas during SSR and paints in an effect, so a static export
|
|
32
|
+
* ships a correctly-sized element and no layout moves when the paint lands.
|
|
33
|
+
*/
|
|
34
|
+
const react_1 = require("react");
|
|
35
|
+
/** The default: a diagonal swell, slow enough to read as atmosphere. */
|
|
36
|
+
const SWELL = (x, y, t) => {
|
|
37
|
+
const d = (x + y) * 2.2 - t * 0.18;
|
|
38
|
+
// Two waves at different rates so the pattern does not visibly repeat.
|
|
39
|
+
const v = Math.sin(d * Math.PI) * 0.5 + Math.sin(d * Math.PI * 0.37 + 1.7) * 0.5;
|
|
40
|
+
return Math.max(0, v) ** 1.6;
|
|
41
|
+
};
|
|
42
|
+
exports.SWELL = SWELL;
|
|
43
|
+
/** The two gradients a `fade` compiles to — one per axis, intersected. */
|
|
44
|
+
const dissolve = (fade) => {
|
|
45
|
+
if (!fade)
|
|
46
|
+
return undefined;
|
|
47
|
+
const f = fade === true ? { top: 0.2, bottom: 0.2, left: 0.2, right: 0.2 } : fade;
|
|
48
|
+
const stop = (n) => Math.max(0, Math.min(0.5, n ?? 0));
|
|
49
|
+
const axis = (a, b, dir) => a || b
|
|
50
|
+
? `linear-gradient(to ${dir}, ${a ? 'transparent' : '#fff'}, #fff ${(a * 100).toFixed(1)}%, ` +
|
|
51
|
+
`#fff ${((1 - b) * 100).toFixed(1)}%, ${b ? 'transparent' : '#fff'})`
|
|
52
|
+
: null;
|
|
53
|
+
return [axis(stop(f.top), stop(f.bottom), 'bottom'), axis(stop(f.left), stop(f.right), 'right')]
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.join(', ');
|
|
56
|
+
};
|
|
57
|
+
const Dots = ({ src, text, field, cell = 6, color, animate = true, fade, className, style, }) => {
|
|
58
|
+
const ref = (0, react_1.useRef)(null);
|
|
59
|
+
(0, react_1.useEffect)(() => {
|
|
60
|
+
const canvas = ref.current;
|
|
61
|
+
if (!canvas)
|
|
62
|
+
return;
|
|
63
|
+
const ctx = canvas.getContext('2d');
|
|
64
|
+
if (!ctx)
|
|
65
|
+
return;
|
|
66
|
+
// The ink follows the cascade unless told otherwise, so a Dots inside a
|
|
67
|
+
// themed section is that theme's colour without anyone passing it down.
|
|
68
|
+
const ink = color ?? getComputedStyle(canvas).color;
|
|
69
|
+
// Motion is a preference before it is a prop. A field that animates against
|
|
70
|
+
// an explicit "reduce" setting is the kind of thing nobody notices shipping.
|
|
71
|
+
const still = !animate ||
|
|
72
|
+
(typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
73
|
+
// The offscreen surface the source is rasterised into, at CELL resolution —
|
|
74
|
+
// one pixel per cell. Sampling a full-resolution bitmap per cell would read
|
|
75
|
+
// one arbitrary pixel out of many; letting the browser downscale into this
|
|
76
|
+
// buffer averages the cell for free, which is what a halftone wants.
|
|
77
|
+
const grid = document.createElement('canvas');
|
|
78
|
+
const gctx = grid.getContext('2d', { willReadFrequently: true });
|
|
79
|
+
if (!gctx)
|
|
80
|
+
return;
|
|
81
|
+
let raf = 0;
|
|
82
|
+
let stop = false;
|
|
83
|
+
let image = null;
|
|
84
|
+
const started = performance.now();
|
|
85
|
+
/** Rasterise the source into `grid` at cols×rows, then draw dots from it. */
|
|
86
|
+
const paint = (t) => {
|
|
87
|
+
const dpr = Math.min(2, devicePixelRatio || 1);
|
|
88
|
+
const w = canvas.clientWidth;
|
|
89
|
+
const h = canvas.clientHeight;
|
|
90
|
+
if (w < 1 || h < 1)
|
|
91
|
+
return;
|
|
92
|
+
if (canvas.width !== Math.round(w * dpr)) {
|
|
93
|
+
canvas.width = Math.round(w * dpr);
|
|
94
|
+
canvas.height = Math.round(h * dpr);
|
|
95
|
+
}
|
|
96
|
+
const cols = Math.max(1, Math.floor(w / cell));
|
|
97
|
+
const rows = Math.max(1, Math.floor(h / cell));
|
|
98
|
+
grid.width = cols;
|
|
99
|
+
grid.height = rows;
|
|
100
|
+
if (image) {
|
|
101
|
+
// `drawImage` into a cols×rows buffer IS the downscale — cover-fit so a
|
|
102
|
+
// source of any ratio fills without distorting.
|
|
103
|
+
const s = Math.max(cols / image.width, rows / image.height);
|
|
104
|
+
const iw = image.width * s;
|
|
105
|
+
const ih = image.height * s;
|
|
106
|
+
gctx.clearRect(0, 0, cols, rows);
|
|
107
|
+
gctx.drawImage(image, (cols - iw) / 2, (rows - ih) / 2, iw, ih);
|
|
108
|
+
}
|
|
109
|
+
else if (text) {
|
|
110
|
+
gctx.clearRect(0, 0, cols, rows);
|
|
111
|
+
gctx.fillStyle = '#fff';
|
|
112
|
+
gctx.textAlign = 'center';
|
|
113
|
+
gctx.textBaseline = 'middle';
|
|
114
|
+
// Fit the word to the buffer: measure at a known size, then scale.
|
|
115
|
+
const probe = 64;
|
|
116
|
+
gctx.font = `700 ${probe}px ${getComputedStyle(canvas).fontFamily}`;
|
|
117
|
+
const width = gctx.measureText(text).width || 1;
|
|
118
|
+
const size = Math.min((probe * cols * 0.92) / width, rows * 0.8);
|
|
119
|
+
gctx.font = `700 ${size}px ${getComputedStyle(canvas).fontFamily}`;
|
|
120
|
+
gctx.fillText(text, cols / 2, rows / 2);
|
|
121
|
+
}
|
|
122
|
+
const sampled = image || text ? gctx.getImageData(0, 0, cols, rows).data : null;
|
|
123
|
+
const fn = field ?? (sampled ? null : SWELL);
|
|
124
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
125
|
+
ctx.clearRect(0, 0, w, h);
|
|
126
|
+
ctx.fillStyle = ink;
|
|
127
|
+
const max = cell * 0.5;
|
|
128
|
+
for (let row = 0; row < rows; row++) {
|
|
129
|
+
for (let col = 0; col < cols; col++) {
|
|
130
|
+
let v;
|
|
131
|
+
if (sampled) {
|
|
132
|
+
const i = (row * cols + col) * 4;
|
|
133
|
+
// Luminance, weighted by alpha so transparent areas carry no ink.
|
|
134
|
+
const a = sampled[i + 3] / 255;
|
|
135
|
+
v =
|
|
136
|
+
((sampled[i] * 0.2126 + sampled[i + 1] * 0.7152 + sampled[i + 2] * 0.0722) / 255) * a;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
v = fn(col / cols, row / rows, t);
|
|
140
|
+
}
|
|
141
|
+
if (v <= 0.02)
|
|
142
|
+
continue;
|
|
143
|
+
const r = Math.min(max, max * Math.min(1, v));
|
|
144
|
+
ctx.beginPath();
|
|
145
|
+
ctx.arc(col * cell + cell / 2, row * cell + cell / 2, r, 0, Math.PI * 2);
|
|
146
|
+
ctx.fill();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const frame = () => {
|
|
151
|
+
if (stop)
|
|
152
|
+
return;
|
|
153
|
+
paint((performance.now() - started) / 1000);
|
|
154
|
+
raf = requestAnimationFrame(frame);
|
|
155
|
+
};
|
|
156
|
+
const start = () => {
|
|
157
|
+
// A still source is painted once. Only a field needs a loop, and only when
|
|
158
|
+
// motion is allowed — an rAF that redraws an unchanging image is a battery
|
|
159
|
+
// cost with nothing on screen to show for it.
|
|
160
|
+
if (!still && !image && !text)
|
|
161
|
+
frame();
|
|
162
|
+
else
|
|
163
|
+
paint(0);
|
|
164
|
+
};
|
|
165
|
+
if (src) {
|
|
166
|
+
image = new Image();
|
|
167
|
+
image.crossOrigin = 'anonymous';
|
|
168
|
+
image.onload = start;
|
|
169
|
+
image.src = src;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
start();
|
|
173
|
+
}
|
|
174
|
+
// Re-rasterise on resize: the cell count changes with the box, so a fixed
|
|
175
|
+
// buffer would stretch rather than re-sample.
|
|
176
|
+
const ro = new ResizeObserver(() => {
|
|
177
|
+
if (still || image || text)
|
|
178
|
+
paint(0);
|
|
179
|
+
});
|
|
180
|
+
ro.observe(canvas);
|
|
181
|
+
return () => {
|
|
182
|
+
stop = true;
|
|
183
|
+
cancelAnimationFrame(raf);
|
|
184
|
+
ro.disconnect();
|
|
185
|
+
if (image)
|
|
186
|
+
image.onload = null;
|
|
187
|
+
};
|
|
188
|
+
}, [src, text, field, cell, color, animate]);
|
|
189
|
+
const mask = dissolve(fade);
|
|
190
|
+
return ((0, jsx_runtime_1.jsx)("canvas", { ref: ref, "data-slot": "dots", className: className, style: {
|
|
191
|
+
display: 'block',
|
|
192
|
+
width: '100%',
|
|
193
|
+
height: '100%',
|
|
194
|
+
// Both spellings: WebKit still wants the prefix for mask-composite, and
|
|
195
|
+
// a single unprefixed declaration silently drops the second gradient on
|
|
196
|
+
// Safari — which reads as "the fade only works on one axis".
|
|
197
|
+
...(mask
|
|
198
|
+
? {
|
|
199
|
+
WebkitMaskImage: mask,
|
|
200
|
+
maskImage: mask,
|
|
201
|
+
WebkitMaskComposite: 'source-in',
|
|
202
|
+
maskComposite: 'intersect',
|
|
203
|
+
}
|
|
204
|
+
: null),
|
|
205
|
+
...style,
|
|
206
|
+
} }));
|
|
207
|
+
};
|
|
208
|
+
exports.Dots = Dots;
|
package/dist/dots.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Brightness at a point, 0 (dark, no dot) to 1 (full dot). `t` is seconds. */
|
|
2
|
+
export type Field = (x: number, y: number, t: number) => number;
|
|
3
|
+
export interface DotsProps {
|
|
4
|
+
/** An image to sample. Same-origin or CORS-enabled — a tainted canvas cannot be read back. */
|
|
5
|
+
src?: string;
|
|
6
|
+
/** Type to sample. Uses the page's own sans stack, so it matches the brand. */
|
|
7
|
+
text?: string;
|
|
8
|
+
/** A function of space and time. `x` and `y` are 0..1; `t` is seconds. */
|
|
9
|
+
field?: Field;
|
|
10
|
+
/** Cell pitch in CSS pixels. Smaller is finer and costs more. */
|
|
11
|
+
cell?: number;
|
|
12
|
+
/** The ink. Any CSS colour; defaults to the current text colour. */
|
|
13
|
+
color?: string;
|
|
14
|
+
/** Animate. A `field` moves; an image or text holds still, so this is ignored for them. */
|
|
15
|
+
animate?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Dissolve the field at its edges instead of ending it on a line.
|
|
18
|
+
*
|
|
19
|
+
* `true` fades all four sides. An object fades only the sides named, each
|
|
20
|
+
* value being the fraction of that axis the fade occupies (`0.2` = the outer
|
|
21
|
+
* fifth). A field that stops abruptly reads as a rectangle someone pasted on;
|
|
22
|
+
* one that dissolves reads as part of the page.
|
|
23
|
+
*
|
|
24
|
+
* This is a mask, not a second overlaid element, so nothing has to be kept in
|
|
25
|
+
* sync with the canvas and the page background shows through exactly.
|
|
26
|
+
* Asymmetric values are the useful case: hold the field back where type sits
|
|
27
|
+
* and let it run where the page is empty.
|
|
28
|
+
*/
|
|
29
|
+
fade?: boolean | {
|
|
30
|
+
top?: number;
|
|
31
|
+
bottom?: number;
|
|
32
|
+
left?: number;
|
|
33
|
+
right?: number;
|
|
34
|
+
};
|
|
35
|
+
className?: string;
|
|
36
|
+
style?: React.CSSProperties;
|
|
37
|
+
}
|
|
38
|
+
/** The default: a diagonal swell, slow enough to read as atmosphere. */
|
|
39
|
+
declare const SWELL: Field;
|
|
40
|
+
declare const Dots: ({ src, text, field, cell, color, animate, fade, className, style, }: DotsProps) => import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
export { Dots, SWELL };
|
|
42
|
+
//# sourceMappingURL=dots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dots.d.ts","sourceRoot":"","sources":["../src/dots.tsx"],"names":[],"mappings":"AAgCA,+EAA+E;AAC/E,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;AAE/D,MAAM,WAAW,SAAS;IACxB,8FAA8F;IAC9F,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2FAA2F;IAC3F,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACjF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;CAC5B;AAED,wEAAwE;AACxE,QAAA,MAAM,KAAK,EAAE,KAKZ,CAAA;AAiBD,QAAA,MAAM,IAAI,GAAI,qEAUX,SAAS,4CAkKX,CAAA;AAED,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA"}
|
package/dist/dots.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Dots — anything, sampled onto a grid of dots.
|
|
5
|
+
*
|
|
6
|
+
* A halftone screen: divide a surface into cells, measure how bright the source
|
|
7
|
+
* is at each cell, and draw a dot whose radius carries that measurement. It is
|
|
8
|
+
* the oldest trick in reproduction and it still reads as considered, because the
|
|
9
|
+
* grid is regular and only the ink varies.
|
|
10
|
+
*
|
|
11
|
+
* Three sources, one mechanism. Whatever it is gets rasterised once into an
|
|
12
|
+
* offscreen canvas, then read back per cell — so an image, a word and a moving
|
|
13
|
+
* field all take the same path and there is one place the sampling happens:
|
|
14
|
+
*
|
|
15
|
+
* <Dots src="/hero.png" /> a picture
|
|
16
|
+
* <Dots text="Ship Apps." /> type
|
|
17
|
+
* <Dots field={(x, y, t) => …} /> a function of space and time
|
|
18
|
+
*
|
|
19
|
+
* With none of them it draws the default field, a slow diagonal swell, which is
|
|
20
|
+
* the ambient case a page usually wants behind a headline.
|
|
21
|
+
*
|
|
22
|
+
* WEB ONLY, and off the barrel deliberately. `2d` canvas has no counterpart on
|
|
23
|
+
* React Native, and this package's promise is that a root import runs on native
|
|
24
|
+
* — so putting this in the barrel would make that false for every consumer. It
|
|
25
|
+
* lives at `@hanzo/ui/dots`, the same quarantine `product/theme-toggle-next`
|
|
26
|
+
* uses for its Next dependency: a caller reaches for it on purpose.
|
|
27
|
+
*
|
|
28
|
+
* It renders the canvas during SSR and paints in an effect, so a static export
|
|
29
|
+
* ships a correctly-sized element and no layout moves when the paint lands.
|
|
30
|
+
*/
|
|
31
|
+
import { useEffect, useRef } from 'react';
|
|
32
|
+
/** The default: a diagonal swell, slow enough to read as atmosphere. */
|
|
33
|
+
const SWELL = (x, y, t) => {
|
|
34
|
+
const d = (x + y) * 2.2 - t * 0.18;
|
|
35
|
+
// Two waves at different rates so the pattern does not visibly repeat.
|
|
36
|
+
const v = Math.sin(d * Math.PI) * 0.5 + Math.sin(d * Math.PI * 0.37 + 1.7) * 0.5;
|
|
37
|
+
return Math.max(0, v) ** 1.6;
|
|
38
|
+
};
|
|
39
|
+
/** The two gradients a `fade` compiles to — one per axis, intersected. */
|
|
40
|
+
const dissolve = (fade) => {
|
|
41
|
+
if (!fade)
|
|
42
|
+
return undefined;
|
|
43
|
+
const f = fade === true ? { top: 0.2, bottom: 0.2, left: 0.2, right: 0.2 } : fade;
|
|
44
|
+
const stop = (n) => Math.max(0, Math.min(0.5, n ?? 0));
|
|
45
|
+
const axis = (a, b, dir) => a || b
|
|
46
|
+
? `linear-gradient(to ${dir}, ${a ? 'transparent' : '#fff'}, #fff ${(a * 100).toFixed(1)}%, ` +
|
|
47
|
+
`#fff ${((1 - b) * 100).toFixed(1)}%, ${b ? 'transparent' : '#fff'})`
|
|
48
|
+
: null;
|
|
49
|
+
return [axis(stop(f.top), stop(f.bottom), 'bottom'), axis(stop(f.left), stop(f.right), 'right')]
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.join(', ');
|
|
52
|
+
};
|
|
53
|
+
const Dots = ({ src, text, field, cell = 6, color, animate = true, fade, className, style, }) => {
|
|
54
|
+
const ref = useRef(null);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const canvas = ref.current;
|
|
57
|
+
if (!canvas)
|
|
58
|
+
return;
|
|
59
|
+
const ctx = canvas.getContext('2d');
|
|
60
|
+
if (!ctx)
|
|
61
|
+
return;
|
|
62
|
+
// The ink follows the cascade unless told otherwise, so a Dots inside a
|
|
63
|
+
// themed section is that theme's colour without anyone passing it down.
|
|
64
|
+
const ink = color ?? getComputedStyle(canvas).color;
|
|
65
|
+
// Motion is a preference before it is a prop. A field that animates against
|
|
66
|
+
// an explicit "reduce" setting is the kind of thing nobody notices shipping.
|
|
67
|
+
const still = !animate ||
|
|
68
|
+
(typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
69
|
+
// The offscreen surface the source is rasterised into, at CELL resolution —
|
|
70
|
+
// one pixel per cell. Sampling a full-resolution bitmap per cell would read
|
|
71
|
+
// one arbitrary pixel out of many; letting the browser downscale into this
|
|
72
|
+
// buffer averages the cell for free, which is what a halftone wants.
|
|
73
|
+
const grid = document.createElement('canvas');
|
|
74
|
+
const gctx = grid.getContext('2d', { willReadFrequently: true });
|
|
75
|
+
if (!gctx)
|
|
76
|
+
return;
|
|
77
|
+
let raf = 0;
|
|
78
|
+
let stop = false;
|
|
79
|
+
let image = null;
|
|
80
|
+
const started = performance.now();
|
|
81
|
+
/** Rasterise the source into `grid` at cols×rows, then draw dots from it. */
|
|
82
|
+
const paint = (t) => {
|
|
83
|
+
const dpr = Math.min(2, devicePixelRatio || 1);
|
|
84
|
+
const w = canvas.clientWidth;
|
|
85
|
+
const h = canvas.clientHeight;
|
|
86
|
+
if (w < 1 || h < 1)
|
|
87
|
+
return;
|
|
88
|
+
if (canvas.width !== Math.round(w * dpr)) {
|
|
89
|
+
canvas.width = Math.round(w * dpr);
|
|
90
|
+
canvas.height = Math.round(h * dpr);
|
|
91
|
+
}
|
|
92
|
+
const cols = Math.max(1, Math.floor(w / cell));
|
|
93
|
+
const rows = Math.max(1, Math.floor(h / cell));
|
|
94
|
+
grid.width = cols;
|
|
95
|
+
grid.height = rows;
|
|
96
|
+
if (image) {
|
|
97
|
+
// `drawImage` into a cols×rows buffer IS the downscale — cover-fit so a
|
|
98
|
+
// source of any ratio fills without distorting.
|
|
99
|
+
const s = Math.max(cols / image.width, rows / image.height);
|
|
100
|
+
const iw = image.width * s;
|
|
101
|
+
const ih = image.height * s;
|
|
102
|
+
gctx.clearRect(0, 0, cols, rows);
|
|
103
|
+
gctx.drawImage(image, (cols - iw) / 2, (rows - ih) / 2, iw, ih);
|
|
104
|
+
}
|
|
105
|
+
else if (text) {
|
|
106
|
+
gctx.clearRect(0, 0, cols, rows);
|
|
107
|
+
gctx.fillStyle = '#fff';
|
|
108
|
+
gctx.textAlign = 'center';
|
|
109
|
+
gctx.textBaseline = 'middle';
|
|
110
|
+
// Fit the word to the buffer: measure at a known size, then scale.
|
|
111
|
+
const probe = 64;
|
|
112
|
+
gctx.font = `700 ${probe}px ${getComputedStyle(canvas).fontFamily}`;
|
|
113
|
+
const width = gctx.measureText(text).width || 1;
|
|
114
|
+
const size = Math.min((probe * cols * 0.92) / width, rows * 0.8);
|
|
115
|
+
gctx.font = `700 ${size}px ${getComputedStyle(canvas).fontFamily}`;
|
|
116
|
+
gctx.fillText(text, cols / 2, rows / 2);
|
|
117
|
+
}
|
|
118
|
+
const sampled = image || text ? gctx.getImageData(0, 0, cols, rows).data : null;
|
|
119
|
+
const fn = field ?? (sampled ? null : SWELL);
|
|
120
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
121
|
+
ctx.clearRect(0, 0, w, h);
|
|
122
|
+
ctx.fillStyle = ink;
|
|
123
|
+
const max = cell * 0.5;
|
|
124
|
+
for (let row = 0; row < rows; row++) {
|
|
125
|
+
for (let col = 0; col < cols; col++) {
|
|
126
|
+
let v;
|
|
127
|
+
if (sampled) {
|
|
128
|
+
const i = (row * cols + col) * 4;
|
|
129
|
+
// Luminance, weighted by alpha so transparent areas carry no ink.
|
|
130
|
+
const a = sampled[i + 3] / 255;
|
|
131
|
+
v =
|
|
132
|
+
((sampled[i] * 0.2126 + sampled[i + 1] * 0.7152 + sampled[i + 2] * 0.0722) / 255) * a;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
v = fn(col / cols, row / rows, t);
|
|
136
|
+
}
|
|
137
|
+
if (v <= 0.02)
|
|
138
|
+
continue;
|
|
139
|
+
const r = Math.min(max, max * Math.min(1, v));
|
|
140
|
+
ctx.beginPath();
|
|
141
|
+
ctx.arc(col * cell + cell / 2, row * cell + cell / 2, r, 0, Math.PI * 2);
|
|
142
|
+
ctx.fill();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const frame = () => {
|
|
147
|
+
if (stop)
|
|
148
|
+
return;
|
|
149
|
+
paint((performance.now() - started) / 1000);
|
|
150
|
+
raf = requestAnimationFrame(frame);
|
|
151
|
+
};
|
|
152
|
+
const start = () => {
|
|
153
|
+
// A still source is painted once. Only a field needs a loop, and only when
|
|
154
|
+
// motion is allowed — an rAF that redraws an unchanging image is a battery
|
|
155
|
+
// cost with nothing on screen to show for it.
|
|
156
|
+
if (!still && !image && !text)
|
|
157
|
+
frame();
|
|
158
|
+
else
|
|
159
|
+
paint(0);
|
|
160
|
+
};
|
|
161
|
+
if (src) {
|
|
162
|
+
image = new Image();
|
|
163
|
+
image.crossOrigin = 'anonymous';
|
|
164
|
+
image.onload = start;
|
|
165
|
+
image.src = src;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
start();
|
|
169
|
+
}
|
|
170
|
+
// Re-rasterise on resize: the cell count changes with the box, so a fixed
|
|
171
|
+
// buffer would stretch rather than re-sample.
|
|
172
|
+
const ro = new ResizeObserver(() => {
|
|
173
|
+
if (still || image || text)
|
|
174
|
+
paint(0);
|
|
175
|
+
});
|
|
176
|
+
ro.observe(canvas);
|
|
177
|
+
return () => {
|
|
178
|
+
stop = true;
|
|
179
|
+
cancelAnimationFrame(raf);
|
|
180
|
+
ro.disconnect();
|
|
181
|
+
if (image)
|
|
182
|
+
image.onload = null;
|
|
183
|
+
};
|
|
184
|
+
}, [src, text, field, cell, color, animate]);
|
|
185
|
+
const mask = dissolve(fade);
|
|
186
|
+
return (_jsx("canvas", { ref: ref, "data-slot": "dots", className: className, style: {
|
|
187
|
+
display: 'block',
|
|
188
|
+
width: '100%',
|
|
189
|
+
height: '100%',
|
|
190
|
+
// Both spellings: WebKit still wants the prefix for mask-composite, and
|
|
191
|
+
// a single unprefixed declaration silently drops the second gradient on
|
|
192
|
+
// Safari — which reads as "the fade only works on one axis".
|
|
193
|
+
...(mask
|
|
194
|
+
? {
|
|
195
|
+
WebkitMaskImage: mask,
|
|
196
|
+
maskImage: mask,
|
|
197
|
+
WebkitMaskComposite: 'source-in',
|
|
198
|
+
maskComposite: 'intersect',
|
|
199
|
+
}
|
|
200
|
+
: null),
|
|
201
|
+
...style,
|
|
202
|
+
} }));
|
|
203
|
+
};
|
|
204
|
+
export { Dots, SWELL };
|
|
205
|
+
//# sourceMappingURL=dots.js.map
|
package/dist/dots.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dots.js","sourceRoot":"","sources":["../src/dots.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAoCzC,wEAAwE;AACxE,MAAM,KAAK,GAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE;IAC/B,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,CAAA;IAClC,uEAAuE;IACvE,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,GAAG,CAAC,GAAG,GAAG,CAAA;IAChF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,GAAG,CAAA;AAC9B,CAAC,CAAA;AAED,0EAA0E;AAC1E,MAAM,QAAQ,GAAG,CAAC,IAAuB,EAAsB,EAAE;IAC/D,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAA;IAC3B,MAAM,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IACjF,MAAM,IAAI,GAAG,CAAC,CAAU,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC/D,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,CAAS,EAAE,GAAW,EAAE,EAAE,CACjD,CAAC,IAAI,CAAC;QACJ,CAAC,CAAC,sBAAsB,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,UAAU,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK;YAC3F,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,GAAG;QACvE,CAAC,CAAC,IAAI,CAAA;IACV,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;SAC7F,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,IAAI,CAAC,CAAA;AACf,CAAC,CAAA;AAED,MAAM,IAAI,GAAG,CAAC,EACZ,GAAG,EACH,IAAI,EACJ,KAAK,EACL,IAAI,GAAG,CAAC,EACR,KAAK,EACL,OAAO,GAAG,IAAI,EACd,IAAI,EACJ,SAAS,EACT,KAAK,GACK,EAAE,EAAE;IACd,MAAM,GAAG,GAAG,MAAM,CAA2B,IAAI,CAAC,CAAA;IAElD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAA;QAC1B,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;QACnC,IAAI,CAAC,GAAG;YAAE,OAAM;QAEhB,wEAAwE;QACxE,wEAAwE;QACxE,MAAM,GAAG,GAAG,KAAK,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAC,KAAK,CAAA;QAEnD,4EAA4E;QAC5E,6EAA6E;QAC7E,MAAM,KAAK,GACT,CAAC,OAAO;YACR,CAAC,OAAO,UAAU,KAAK,UAAU,IAAI,UAAU,CAAC,kCAAkC,CAAC,CAAC,OAAO,CAAC,CAAA;QAE9F,4EAA4E;QAC5E,4EAA4E;QAC5E,2EAA2E;QAC3E,qEAAqE;QACrE,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAA;QAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAA;QAChE,IAAI,CAAC,IAAI;YAAE,OAAM;QAEjB,IAAI,GAAG,GAAG,CAAC,CAAA;QACX,IAAI,IAAI,GAAG,KAAK,CAAA;QAChB,IAAI,KAAK,GAA4B,IAAI,CAAA;QACzC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAA;QAEjC,6EAA6E;QAC7E,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,EAAE;YAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,gBAAgB,IAAI,CAAC,CAAC,CAAA;YAC9C,MAAM,CAAC,GAAG,MAAM,CAAC,WAAW,CAAA;YAC5B,MAAM,CAAC,GAAG,MAAM,CAAC,YAAY,CAAA;YAC7B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC;gBAAE,OAAM;YAE1B,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;gBACzC,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,CAAA;gBAClC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,CAAA;YACrC,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAA;YAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAA;YAC9C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;YACjB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;YAElB,IAAI,KAAK,EAAE,CAAC;gBACV,wEAAwE;gBACxE,gDAAgD;gBAChD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAA;gBAC3D,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,CAAA;gBAC1B,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;gBAC3B,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;gBAChC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;YACjE,CAAC;iBAAM,IAAI,IAAI,EAAE,CAAC;gBAChB,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;gBAChC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAA;gBACvB,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAA;gBACzB,IAAI,CAAC,YAAY,GAAG,QAAQ,CAAA;gBAC5B,mEAAmE;gBACnE,MAAM,KAAK,GAAG,EAAE,CAAA;gBAChB,IAAI,CAAC,IAAI,GAAG,OAAO,KAAK,MAAM,gBAAgB,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,CAAA;gBACnE,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAA;gBAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,IAAI,GAAG,IAAI,CAAC,GAAG,KAAK,EAAE,IAAI,GAAG,GAAG,CAAC,CAAA;gBAChE,IAAI,CAAC,IAAI,GAAG,OAAO,IAAI,MAAM,gBAAgB,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,CAAA;gBAClE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,CAAA;YACzC,CAAC;YAED,MAAM,OAAO,GAAG,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;YAC/E,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;YAE5C,GAAG,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;YACtC,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;YACzB,GAAG,CAAC,SAAS,GAAG,GAAG,CAAA;YAEnB,MAAM,GAAG,GAAG,IAAI,GAAG,GAAG,CAAA;YACtB,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC;gBACpC,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC;oBACpC,IAAI,CAAS,CAAA;oBACb,IAAI,OAAO,EAAE,CAAC;wBACZ,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;wBAChC,kEAAkE;wBAClE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAA;wBAC9B,CAAC;4BACC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;oBACzF,CAAC;yBAAM,CAAC;wBACN,CAAC,GAAG,EAAG,CAAC,GAAG,GAAG,IAAI,EAAE,GAAG,GAAG,IAAI,EAAE,CAAC,CAAC,CAAA;oBACpC,CAAC;oBACD,IAAI,CAAC,IAAI,IAAI;wBAAE,SAAQ;oBACvB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;oBAC7C,GAAG,CAAC,SAAS,EAAE,CAAA;oBACf,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,CAAC,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;oBACxE,GAAG,CAAC,IAAI,EAAE,CAAA;gBACZ,CAAC;YACH,CAAC;QACH,CAAC,CAAA;QAED,MAAM,KAAK,GAAG,GAAG,EAAE;YACjB,IAAI,IAAI;gBAAE,OAAM;YAChB,KAAK,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,CAAA;YAC3C,GAAG,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAA;QACpC,CAAC,CAAA;QAED,MAAM,KAAK,GAAG,GAAG,EAAE;YACjB,2EAA2E;YAC3E,2EAA2E;YAC3E,8CAA8C;YAC9C,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI;gBAAE,KAAK,EAAE,CAAA;;gBACjC,KAAK,CAAC,CAAC,CAAC,CAAA;QACf,CAAC,CAAA;QAED,IAAI,GAAG,EAAE,CAAC;YACR,KAAK,GAAG,IAAI,KAAK,EAAE,CAAA;YACnB,KAAK,CAAC,WAAW,GAAG,WAAW,CAAA;YAC/B,KAAK,CAAC,MAAM,GAAG,KAAK,CAAA;YACpB,KAAK,CAAC,GAAG,GAAG,GAAG,CAAA;QACjB,CAAC;aAAM,CAAC;YACN,KAAK,EAAE,CAAA;QACT,CAAC;QAED,0EAA0E;QAC1E,8CAA8C;QAC9C,MAAM,EAAE,GAAG,IAAI,cAAc,CAAC,GAAG,EAAE;YACjC,IAAI,KAAK,IAAI,KAAK,IAAI,IAAI;gBAAE,KAAK,CAAC,CAAC,CAAC,CAAA;QACtC,CAAC,CAAC,CAAA;QACF,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QAElB,OAAO,GAAG,EAAE;YACV,IAAI,GAAG,IAAI,CAAA;YACX,oBAAoB,CAAC,GAAG,CAAC,CAAA;YACzB,EAAE,CAAC,UAAU,EAAE,CAAA;YACf,IAAI,KAAK;gBAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAA;QAChC,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAA;IAE5C,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC3B,OAAO,CACL,iBACE,GAAG,EAAE,GAAG,eACE,MAAM,EAChB,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE;YACL,OAAO,EAAE,OAAO;YAChB,KAAK,EAAE,MAAM;YACb,MAAM,EAAE,MAAM;YACd,wEAAwE;YACxE,wEAAwE;YACxE,6DAA6D;YAC7D,GAAG,CAAC,IAAI;gBACN,CAAC,CAAC;oBACE,eAAe,EAAE,IAAI;oBACrB,SAAS,EAAE,IAAI;oBACf,mBAAmB,EAAE,WAAW;oBAChC,aAAa,EAAE,WAAW;iBAC3B;gBACH,CAAC,CAAC,IAAI,CAAC;YACT,GAAG,KAAK;SACT,GACD,CACH,CAAA;AACH,CAAC,CAAA;AAED,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA"}
|
package/dist/gallery.html
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!doctype html><html lang="en" class="t_dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>@hanzo/ui 8.1.
|
|
1
|
+
<!doctype html><html lang="en" class="t_dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>@hanzo/ui 8.1.20</title><style>/* @hanzo/ui — generated by scripts/gen-css.mjs. Do not edit. */
|
|
2
2
|
:root { --hanzo-ui-styles: 1; }
|
|
3
3
|
/* Hanzo Design System — the entry point. Import THIS one file.
|
|
4
4
|
*
|
|
@@ -1953,6 +1953,146 @@
|
|
|
1953
1953
|
display: block;
|
|
1954
1954
|
}
|
|
1955
1955
|
|
|
1956
|
+
|
|
1957
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
1958
|
+
Scrollbars, on the surfaces this package scrolls.
|
|
1959
|
+
|
|
1960
|
+
A scroll region is a component decision, so its scrollbar is one too. Left to
|
|
1961
|
+
the platform it is not neutral: on Linux/GTK Chrome draws a wide track with
|
|
1962
|
+
STEPPER ARROWS at both ends, and a dark product grows two grey buttons per
|
|
1963
|
+
scroll region — visible in chat's thread, its source strip and the composer at
|
|
1964
|
+
the same time. Windows draws a wide light track. macOS overlays a thin one and
|
|
1965
|
+
hides it, which is the behaviour the other two should have had.
|
|
1966
|
+
|
|
1967
|
+
So the package states it, and states it ONCE, for the five slots that actually
|
|
1968
|
+
scroll (measured, not guessed: textarea, fill, thread, sources-strip,
|
|
1969
|
+
composer-field). Scoping to our own slots is the point — a component library
|
|
1970
|
+
restyling the HOST's page scrollbars is taking a decision that is not its own.
|
|
1971
|
+
|
|
1972
|
+
`::-webkit-scrollbar-button { display: none }` is the line that removes the
|
|
1973
|
+
steppers; without it the arrows survive every other rule here. The thumb sits
|
|
1974
|
+
on the same alpha ladder as every other edge, so it lifts with its surface
|
|
1975
|
+
instead of being a fixed grey that only suits one theme.
|
|
1976
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
1977
|
+
:where(
|
|
1978
|
+
[data-slot='fill'],
|
|
1979
|
+
[data-slot='thread'],
|
|
1980
|
+
[data-slot='sources-strip'],
|
|
1981
|
+
[data-slot='sidebar-scroll'],
|
|
1982
|
+
[data-slot='scroll-area-viewport'],
|
|
1983
|
+
[data-slot='textarea'],
|
|
1984
|
+
[data-slot='composer-field']
|
|
1985
|
+
) {
|
|
1986
|
+
/* Firefox — no pseudo-elements, two properties, same intent. */
|
|
1987
|
+
scrollbar-width: thin;
|
|
1988
|
+
scrollbar-color: rgb(255 255 255 / 0.18) transparent;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
:where(
|
|
1992
|
+
[data-slot='fill'],
|
|
1993
|
+
[data-slot='thread'],
|
|
1994
|
+
[data-slot='sources-strip'],
|
|
1995
|
+
[data-slot='sidebar-scroll'],
|
|
1996
|
+
[data-slot='scroll-area-viewport'],
|
|
1997
|
+
[data-slot='textarea'],
|
|
1998
|
+
[data-slot='composer-field']
|
|
1999
|
+
)::-webkit-scrollbar {
|
|
2000
|
+
width: 8px;
|
|
2001
|
+
height: 8px;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
/* The steppers. This is the rule that removes the arrows. */
|
|
2005
|
+
:where(
|
|
2006
|
+
[data-slot='fill'],
|
|
2007
|
+
[data-slot='thread'],
|
|
2008
|
+
[data-slot='sources-strip'],
|
|
2009
|
+
[data-slot='sidebar-scroll'],
|
|
2010
|
+
[data-slot='scroll-area-viewport'],
|
|
2011
|
+
[data-slot='textarea'],
|
|
2012
|
+
[data-slot='composer-field']
|
|
2013
|
+
)::-webkit-scrollbar-button {
|
|
2014
|
+
display: none;
|
|
2015
|
+
width: 0;
|
|
2016
|
+
height: 0;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
:where(
|
|
2020
|
+
[data-slot='fill'],
|
|
2021
|
+
[data-slot='thread'],
|
|
2022
|
+
[data-slot='sources-strip'],
|
|
2023
|
+
[data-slot='sidebar-scroll'],
|
|
2024
|
+
[data-slot='scroll-area-viewport'],
|
|
2025
|
+
[data-slot='textarea'],
|
|
2026
|
+
[data-slot='composer-field']
|
|
2027
|
+
)::-webkit-scrollbar-track {
|
|
2028
|
+
background: transparent;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
:where(
|
|
2032
|
+
[data-slot='fill'],
|
|
2033
|
+
[data-slot='thread'],
|
|
2034
|
+
[data-slot='sources-strip'],
|
|
2035
|
+
[data-slot='sidebar-scroll'],
|
|
2036
|
+
[data-slot='scroll-area-viewport'],
|
|
2037
|
+
[data-slot='textarea'],
|
|
2038
|
+
[data-slot='composer-field']
|
|
2039
|
+
)::-webkit-scrollbar-thumb {
|
|
2040
|
+
background: rgb(255 255 255 / 0.18);
|
|
2041
|
+
border-radius: 999px;
|
|
2042
|
+
/* A transparent border with background-clip keeps the thumb off the very edge
|
|
2043
|
+
without a track colour doing it, which is what makes it read as an overlay
|
|
2044
|
+
rather than a channel. */
|
|
2045
|
+
border: 2px solid transparent;
|
|
2046
|
+
background-clip: content-box;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
:where(
|
|
2050
|
+
[data-slot='fill'],
|
|
2051
|
+
[data-slot='thread'],
|
|
2052
|
+
[data-slot='sources-strip'],
|
|
2053
|
+
[data-slot='sidebar-scroll'],
|
|
2054
|
+
[data-slot='scroll-area-viewport'],
|
|
2055
|
+
[data-slot='textarea'],
|
|
2056
|
+
[data-slot='composer-field']
|
|
2057
|
+
)::-webkit-scrollbar-thumb:hover {
|
|
2058
|
+
background: rgb(255 255 255 / 0.30);
|
|
2059
|
+
background-clip: content-box;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
/* The light theme reads the same ladder from the other side. */
|
|
2063
|
+
.t_light :where(
|
|
2064
|
+
[data-slot='fill'],
|
|
2065
|
+
[data-slot='thread'],
|
|
2066
|
+
[data-slot='sources-strip'],
|
|
2067
|
+
[data-slot='sidebar-scroll'],
|
|
2068
|
+
[data-slot='scroll-area-viewport'],
|
|
2069
|
+
[data-slot='textarea'],
|
|
2070
|
+
[data-slot='composer-field']
|
|
2071
|
+
) {
|
|
2072
|
+
scrollbar-color: rgb(0 0 0 / 0.20) transparent;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
.t_light :where(
|
|
2076
|
+
[data-slot='fill'],
|
|
2077
|
+
[data-slot='thread'],
|
|
2078
|
+
[data-slot='sources-strip'],
|
|
2079
|
+
[data-slot='sidebar-scroll'],
|
|
2080
|
+
[data-slot='scroll-area-viewport'],
|
|
2081
|
+
[data-slot='textarea'],
|
|
2082
|
+
[data-slot='composer-field']
|
|
2083
|
+
)::-webkit-scrollbar-thumb {
|
|
2084
|
+
background: rgb(0 0 0 / 0.20);
|
|
2085
|
+
background-clip: content-box;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
/* The textarea's resize grip is the other platform artefact in the same family:
|
|
2089
|
+
a diagonal hatch in the corner of every Textarea and composer, drawn by the UA
|
|
2090
|
+
and matching nothing. A composer that grows with its content does not need a
|
|
2091
|
+
drag handle, and one that does not grow should not offer a broken one. */
|
|
2092
|
+
:where([data-slot='textarea'], [data-slot='composer-field']) {
|
|
2093
|
+
resize: none;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
1956
2096
|
._ovs-contain {overscroll-behavior:contain;}
|
|
1957
2097
|
.t_unmounted .is_View, .t_unmounted .is_Text { transition: none !important; }
|
|
1958
2098
|
.is_View { display: flex; align-items: stretch; flex-direction: column; flex-basis: auto; box-sizing: border-box; min-height: 0; min-width: 0; flex-shrink: 0; }
|
package/dist/styles/motion.css
CHANGED
|
@@ -264,3 +264,143 @@
|
|
|
264
264
|
object-fit: cover;
|
|
265
265
|
display: block;
|
|
266
266
|
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
270
|
+
Scrollbars, on the surfaces this package scrolls.
|
|
271
|
+
|
|
272
|
+
A scroll region is a component decision, so its scrollbar is one too. Left to
|
|
273
|
+
the platform it is not neutral: on Linux/GTK Chrome draws a wide track with
|
|
274
|
+
STEPPER ARROWS at both ends, and a dark product grows two grey buttons per
|
|
275
|
+
scroll region — visible in chat's thread, its source strip and the composer at
|
|
276
|
+
the same time. Windows draws a wide light track. macOS overlays a thin one and
|
|
277
|
+
hides it, which is the behaviour the other two should have had.
|
|
278
|
+
|
|
279
|
+
So the package states it, and states it ONCE, for the five slots that actually
|
|
280
|
+
scroll (measured, not guessed: textarea, fill, thread, sources-strip,
|
|
281
|
+
composer-field). Scoping to our own slots is the point — a component library
|
|
282
|
+
restyling the HOST's page scrollbars is taking a decision that is not its own.
|
|
283
|
+
|
|
284
|
+
`::-webkit-scrollbar-button { display: none }` is the line that removes the
|
|
285
|
+
steppers; without it the arrows survive every other rule here. The thumb sits
|
|
286
|
+
on the same alpha ladder as every other edge, so it lifts with its surface
|
|
287
|
+
instead of being a fixed grey that only suits one theme.
|
|
288
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
289
|
+
:where(
|
|
290
|
+
[data-slot='fill'],
|
|
291
|
+
[data-slot='thread'],
|
|
292
|
+
[data-slot='sources-strip'],
|
|
293
|
+
[data-slot='sidebar-scroll'],
|
|
294
|
+
[data-slot='scroll-area-viewport'],
|
|
295
|
+
[data-slot='textarea'],
|
|
296
|
+
[data-slot='composer-field']
|
|
297
|
+
) {
|
|
298
|
+
/* Firefox — no pseudo-elements, two properties, same intent. */
|
|
299
|
+
scrollbar-width: thin;
|
|
300
|
+
scrollbar-color: rgb(255 255 255 / 0.18) transparent;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
:where(
|
|
304
|
+
[data-slot='fill'],
|
|
305
|
+
[data-slot='thread'],
|
|
306
|
+
[data-slot='sources-strip'],
|
|
307
|
+
[data-slot='sidebar-scroll'],
|
|
308
|
+
[data-slot='scroll-area-viewport'],
|
|
309
|
+
[data-slot='textarea'],
|
|
310
|
+
[data-slot='composer-field']
|
|
311
|
+
)::-webkit-scrollbar {
|
|
312
|
+
width: 8px;
|
|
313
|
+
height: 8px;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* The steppers. This is the rule that removes the arrows. */
|
|
317
|
+
:where(
|
|
318
|
+
[data-slot='fill'],
|
|
319
|
+
[data-slot='thread'],
|
|
320
|
+
[data-slot='sources-strip'],
|
|
321
|
+
[data-slot='sidebar-scroll'],
|
|
322
|
+
[data-slot='scroll-area-viewport'],
|
|
323
|
+
[data-slot='textarea'],
|
|
324
|
+
[data-slot='composer-field']
|
|
325
|
+
)::-webkit-scrollbar-button {
|
|
326
|
+
display: none;
|
|
327
|
+
width: 0;
|
|
328
|
+
height: 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
:where(
|
|
332
|
+
[data-slot='fill'],
|
|
333
|
+
[data-slot='thread'],
|
|
334
|
+
[data-slot='sources-strip'],
|
|
335
|
+
[data-slot='sidebar-scroll'],
|
|
336
|
+
[data-slot='scroll-area-viewport'],
|
|
337
|
+
[data-slot='textarea'],
|
|
338
|
+
[data-slot='composer-field']
|
|
339
|
+
)::-webkit-scrollbar-track {
|
|
340
|
+
background: transparent;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
:where(
|
|
344
|
+
[data-slot='fill'],
|
|
345
|
+
[data-slot='thread'],
|
|
346
|
+
[data-slot='sources-strip'],
|
|
347
|
+
[data-slot='sidebar-scroll'],
|
|
348
|
+
[data-slot='scroll-area-viewport'],
|
|
349
|
+
[data-slot='textarea'],
|
|
350
|
+
[data-slot='composer-field']
|
|
351
|
+
)::-webkit-scrollbar-thumb {
|
|
352
|
+
background: rgb(255 255 255 / 0.18);
|
|
353
|
+
border-radius: 999px;
|
|
354
|
+
/* A transparent border with background-clip keeps the thumb off the very edge
|
|
355
|
+
without a track colour doing it, which is what makes it read as an overlay
|
|
356
|
+
rather than a channel. */
|
|
357
|
+
border: 2px solid transparent;
|
|
358
|
+
background-clip: content-box;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
:where(
|
|
362
|
+
[data-slot='fill'],
|
|
363
|
+
[data-slot='thread'],
|
|
364
|
+
[data-slot='sources-strip'],
|
|
365
|
+
[data-slot='sidebar-scroll'],
|
|
366
|
+
[data-slot='scroll-area-viewport'],
|
|
367
|
+
[data-slot='textarea'],
|
|
368
|
+
[data-slot='composer-field']
|
|
369
|
+
)::-webkit-scrollbar-thumb:hover {
|
|
370
|
+
background: rgb(255 255 255 / 0.30);
|
|
371
|
+
background-clip: content-box;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* The light theme reads the same ladder from the other side. */
|
|
375
|
+
.t_light :where(
|
|
376
|
+
[data-slot='fill'],
|
|
377
|
+
[data-slot='thread'],
|
|
378
|
+
[data-slot='sources-strip'],
|
|
379
|
+
[data-slot='sidebar-scroll'],
|
|
380
|
+
[data-slot='scroll-area-viewport'],
|
|
381
|
+
[data-slot='textarea'],
|
|
382
|
+
[data-slot='composer-field']
|
|
383
|
+
) {
|
|
384
|
+
scrollbar-color: rgb(0 0 0 / 0.20) transparent;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.t_light :where(
|
|
388
|
+
[data-slot='fill'],
|
|
389
|
+
[data-slot='thread'],
|
|
390
|
+
[data-slot='sources-strip'],
|
|
391
|
+
[data-slot='sidebar-scroll'],
|
|
392
|
+
[data-slot='scroll-area-viewport'],
|
|
393
|
+
[data-slot='textarea'],
|
|
394
|
+
[data-slot='composer-field']
|
|
395
|
+
)::-webkit-scrollbar-thumb {
|
|
396
|
+
background: rgb(0 0 0 / 0.20);
|
|
397
|
+
background-clip: content-box;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* The textarea's resize grip is the other platform artefact in the same family:
|
|
401
|
+
a diagonal hatch in the corner of every Textarea and composer, drawn by the UA
|
|
402
|
+
and matching nothing. A composer that grows with its content does not need a
|
|
403
|
+
drag handle, and one that does not grow should not offer a broken one. */
|
|
404
|
+
:where([data-slot='textarea'], [data-slot='composer-field']) {
|
|
405
|
+
resize: none;
|
|
406
|
+
}
|
package/dist/styles.css
CHANGED
|
@@ -1953,6 +1953,146 @@
|
|
|
1953
1953
|
display: block;
|
|
1954
1954
|
}
|
|
1955
1955
|
|
|
1956
|
+
|
|
1957
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
1958
|
+
Scrollbars, on the surfaces this package scrolls.
|
|
1959
|
+
|
|
1960
|
+
A scroll region is a component decision, so its scrollbar is one too. Left to
|
|
1961
|
+
the platform it is not neutral: on Linux/GTK Chrome draws a wide track with
|
|
1962
|
+
STEPPER ARROWS at both ends, and a dark product grows two grey buttons per
|
|
1963
|
+
scroll region — visible in chat's thread, its source strip and the composer at
|
|
1964
|
+
the same time. Windows draws a wide light track. macOS overlays a thin one and
|
|
1965
|
+
hides it, which is the behaviour the other two should have had.
|
|
1966
|
+
|
|
1967
|
+
So the package states it, and states it ONCE, for the five slots that actually
|
|
1968
|
+
scroll (measured, not guessed: textarea, fill, thread, sources-strip,
|
|
1969
|
+
composer-field). Scoping to our own slots is the point — a component library
|
|
1970
|
+
restyling the HOST's page scrollbars is taking a decision that is not its own.
|
|
1971
|
+
|
|
1972
|
+
`::-webkit-scrollbar-button { display: none }` is the line that removes the
|
|
1973
|
+
steppers; without it the arrows survive every other rule here. The thumb sits
|
|
1974
|
+
on the same alpha ladder as every other edge, so it lifts with its surface
|
|
1975
|
+
instead of being a fixed grey that only suits one theme.
|
|
1976
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
1977
|
+
:where(
|
|
1978
|
+
[data-slot='fill'],
|
|
1979
|
+
[data-slot='thread'],
|
|
1980
|
+
[data-slot='sources-strip'],
|
|
1981
|
+
[data-slot='sidebar-scroll'],
|
|
1982
|
+
[data-slot='scroll-area-viewport'],
|
|
1983
|
+
[data-slot='textarea'],
|
|
1984
|
+
[data-slot='composer-field']
|
|
1985
|
+
) {
|
|
1986
|
+
/* Firefox — no pseudo-elements, two properties, same intent. */
|
|
1987
|
+
scrollbar-width: thin;
|
|
1988
|
+
scrollbar-color: rgb(255 255 255 / 0.18) transparent;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
:where(
|
|
1992
|
+
[data-slot='fill'],
|
|
1993
|
+
[data-slot='thread'],
|
|
1994
|
+
[data-slot='sources-strip'],
|
|
1995
|
+
[data-slot='sidebar-scroll'],
|
|
1996
|
+
[data-slot='scroll-area-viewport'],
|
|
1997
|
+
[data-slot='textarea'],
|
|
1998
|
+
[data-slot='composer-field']
|
|
1999
|
+
)::-webkit-scrollbar {
|
|
2000
|
+
width: 8px;
|
|
2001
|
+
height: 8px;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
/* The steppers. This is the rule that removes the arrows. */
|
|
2005
|
+
:where(
|
|
2006
|
+
[data-slot='fill'],
|
|
2007
|
+
[data-slot='thread'],
|
|
2008
|
+
[data-slot='sources-strip'],
|
|
2009
|
+
[data-slot='sidebar-scroll'],
|
|
2010
|
+
[data-slot='scroll-area-viewport'],
|
|
2011
|
+
[data-slot='textarea'],
|
|
2012
|
+
[data-slot='composer-field']
|
|
2013
|
+
)::-webkit-scrollbar-button {
|
|
2014
|
+
display: none;
|
|
2015
|
+
width: 0;
|
|
2016
|
+
height: 0;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
:where(
|
|
2020
|
+
[data-slot='fill'],
|
|
2021
|
+
[data-slot='thread'],
|
|
2022
|
+
[data-slot='sources-strip'],
|
|
2023
|
+
[data-slot='sidebar-scroll'],
|
|
2024
|
+
[data-slot='scroll-area-viewport'],
|
|
2025
|
+
[data-slot='textarea'],
|
|
2026
|
+
[data-slot='composer-field']
|
|
2027
|
+
)::-webkit-scrollbar-track {
|
|
2028
|
+
background: transparent;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
:where(
|
|
2032
|
+
[data-slot='fill'],
|
|
2033
|
+
[data-slot='thread'],
|
|
2034
|
+
[data-slot='sources-strip'],
|
|
2035
|
+
[data-slot='sidebar-scroll'],
|
|
2036
|
+
[data-slot='scroll-area-viewport'],
|
|
2037
|
+
[data-slot='textarea'],
|
|
2038
|
+
[data-slot='composer-field']
|
|
2039
|
+
)::-webkit-scrollbar-thumb {
|
|
2040
|
+
background: rgb(255 255 255 / 0.18);
|
|
2041
|
+
border-radius: 999px;
|
|
2042
|
+
/* A transparent border with background-clip keeps the thumb off the very edge
|
|
2043
|
+
without a track colour doing it, which is what makes it read as an overlay
|
|
2044
|
+
rather than a channel. */
|
|
2045
|
+
border: 2px solid transparent;
|
|
2046
|
+
background-clip: content-box;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
:where(
|
|
2050
|
+
[data-slot='fill'],
|
|
2051
|
+
[data-slot='thread'],
|
|
2052
|
+
[data-slot='sources-strip'],
|
|
2053
|
+
[data-slot='sidebar-scroll'],
|
|
2054
|
+
[data-slot='scroll-area-viewport'],
|
|
2055
|
+
[data-slot='textarea'],
|
|
2056
|
+
[data-slot='composer-field']
|
|
2057
|
+
)::-webkit-scrollbar-thumb:hover {
|
|
2058
|
+
background: rgb(255 255 255 / 0.30);
|
|
2059
|
+
background-clip: content-box;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
/* The light theme reads the same ladder from the other side. */
|
|
2063
|
+
.t_light :where(
|
|
2064
|
+
[data-slot='fill'],
|
|
2065
|
+
[data-slot='thread'],
|
|
2066
|
+
[data-slot='sources-strip'],
|
|
2067
|
+
[data-slot='sidebar-scroll'],
|
|
2068
|
+
[data-slot='scroll-area-viewport'],
|
|
2069
|
+
[data-slot='textarea'],
|
|
2070
|
+
[data-slot='composer-field']
|
|
2071
|
+
) {
|
|
2072
|
+
scrollbar-color: rgb(0 0 0 / 0.20) transparent;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
.t_light :where(
|
|
2076
|
+
[data-slot='fill'],
|
|
2077
|
+
[data-slot='thread'],
|
|
2078
|
+
[data-slot='sources-strip'],
|
|
2079
|
+
[data-slot='sidebar-scroll'],
|
|
2080
|
+
[data-slot='scroll-area-viewport'],
|
|
2081
|
+
[data-slot='textarea'],
|
|
2082
|
+
[data-slot='composer-field']
|
|
2083
|
+
)::-webkit-scrollbar-thumb {
|
|
2084
|
+
background: rgb(0 0 0 / 0.20);
|
|
2085
|
+
background-clip: content-box;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
/* The textarea's resize grip is the other platform artefact in the same family:
|
|
2089
|
+
a diagonal hatch in the corner of every Textarea and composer, drawn by the UA
|
|
2090
|
+
and matching nothing. A composer that grows with its content does not need a
|
|
2091
|
+
drag handle, and one that does not grow should not offer a broken one. */
|
|
2092
|
+
:where([data-slot='textarea'], [data-slot='composer-field']) {
|
|
2093
|
+
resize: none;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
1956
2096
|
._ovs-contain {overscroll-behavior:contain;}
|
|
1957
2097
|
.t_unmounted .is_View, .t_unmounted .is_Text { transition: none !important; }
|
|
1958
2098
|
.is_View { display: flex; align-items: stretch; flex-direction: column; flex-basis: auto; box-sizing: border-box; min-height: 0; min-width: 0; flex-shrink: 0; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/ui",
|
|
3
|
-
"version": "8.1.
|
|
3
|
+
"version": "8.1.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hanzo UI — the one canonical Hanzo component library, on @hanzo/gui + @hanzo/design. ONE substrate: every component renders through @hanzo/gui primitives, so the same import works on web, native (expo) and desktop (Tauri). Self-contained (theme.css), presentational, host-agnostic, clean-room.",
|
|
6
6
|
"exports": {
|
|
@@ -183,6 +183,11 @@
|
|
|
183
183
|
"import": "./dist/tw.js",
|
|
184
184
|
"require": "./dist/tw.cjs",
|
|
185
185
|
"default": "./dist/tw.js"
|
|
186
|
+
},
|
|
187
|
+
"./dots": {
|
|
188
|
+
"types": "./dist/dots.d.ts",
|
|
189
|
+
"import": "./dist/dots.js",
|
|
190
|
+
"require": "./dist/dots.cjs"
|
|
186
191
|
}
|
|
187
192
|
},
|
|
188
193
|
"main": "./dist/index.cjs",
|