@hanzo/ui 8.1.19 → 8.1.21

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 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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dots.js","sourceRoot":"","sources":["../src/dots.tsx"],"names":[],"mappings":"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"}
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.19</title><style>/* @hanzo/ui — generated by scripts/gen-css.mjs. Do not edit. */
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.21</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
  *
@@ -1957,24 +1957,34 @@
1957
1957
  /* ─────────────────────────────────────────────────────────────────────────────
1958
1958
  Scrollbars, on the surfaces this package scrolls.
1959
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.
1960
+ Left to the platform this is not neutral: Linux/GTK Chrome draws a wide track
1961
+ with STEPPER ARROWS at both ends, so a dark product grows two grey buttons per
1962
+ scroll region. Windows draws a wide light track. macOS overlays a thin one,
1963
+ which is the behaviour the other two should have had.
1964
+
1965
+ THE TWO HALVES MUST NOT BE DECLARED TOGETHER, and that is the whole subtlety.
1966
+ Chromium supports both the standard properties and the `::-webkit-scrollbar`
1967
+ pseudo-elements — but the moment `scrollbar-width` or `scrollbar-color` is
1968
+ anything other than `auto`, it takes the standard path and DISCARDS the entire
1969
+ webkit block. Shipping both is how 8.1.19 removed nothing: measured headed on
1970
+ Linux, as-shipped gave a 10px gutter with arrows, while the authored webkit
1971
+ rule alone gives 8px with none. Isolated: webkit-only 8px no arrows;
1972
+ standard-only 10px arrows; both together 10px arrows.
1973
+
1974
+ Neither half is redundant, so neither can simply be deleted — Firefox
1975
+ implements ONLY the standard properties, Safari ONLY the webkit ones. They are
1976
+ separated by engine instead: the webkit block applies wherever the selector is
1977
+ understood, and the standard properties apply only where it is not.
1978
+
1979
+ And it is invisible headless. Chromium's headless shell paints OVERLAY
1980
+ scrollbars, so nothing is drawn, `offsetWidth - clientWidth` is 0 on a blank
1981
+ page, and `getComputedStyle(el, '::-webkit-scrollbar-button').display` happily
1982
+ reports `none` for a rule that never ran. This must be checked headed.
1976
1983
  ───────────────────────────────────────────────────────────────────────────── */
1977
- :where(
1984
+
1985
+ /* Firefox and anything else with no webkit pseudo-elements. */
1986
+ @supports not selector(::-webkit-scrollbar) {
1987
+ :where(
1978
1988
  [data-slot='fill'],
1979
1989
  [data-slot='thread'],
1980
1990
  [data-slot='sources-strip'],
@@ -1983,12 +1993,44 @@
1983
1993
  [data-slot='textarea'],
1984
1994
  [data-slot='composer-field']
1985
1995
  ) {
1986
- /* Firefox — no pseudo-elements, two properties, same intent. */
1987
- scrollbar-width: thin;
1988
- scrollbar-color: rgb(255 255 255 / 0.18) transparent;
1989
- }
1996
+ scrollbar-width: thin;
1997
+ scrollbar-color: rgb(255 255 255 / 0.18) transparent;
1998
+ }
1999
+ .t_light :where(
2000
+ [data-slot='fill'],
2001
+ [data-slot='thread'],
2002
+ [data-slot='sources-strip'],
2003
+ [data-slot='sidebar-scroll'],
2004
+ [data-slot='scroll-area-viewport'],
2005
+ [data-slot='textarea'],
2006
+ [data-slot='composer-field']
2007
+ ) { scrollbar-color: rgb(0 0 0 / 0.20) transparent; }
2008
+ }
2009
+
2010
+ /* Chromium and Safari.
2011
+ The first rule is the load-bearing one and it is a RESET, not a style.
2012
+ @hanzo/design sets `scrollbar-color` and `scrollbar-width` on `:where(html)`
2013
+ (theme.css), and both INHERIT — so every element in the document already
2014
+ carries a non-`auto` value, Chromium takes the standard path, and the whole
2015
+ webkit block below is discarded before it is read. Nothing declared here can
2016
+ win until the inherited value is put back to `auto` on the element itself.
2017
+ That is why the arrows survived 8.1.19: the competing declaration was one
2018
+ level up and inherited, not a rule anyone would find in this file. */
2019
+ @supports selector(::-webkit-scrollbar) {
2020
+ :where(
2021
+ [data-slot='fill'],
2022
+ [data-slot='thread'],
2023
+ [data-slot='sources-strip'],
2024
+ [data-slot='sidebar-scroll'],
2025
+ [data-slot='scroll-area-viewport'],
2026
+ [data-slot='textarea'],
2027
+ [data-slot='composer-field']
2028
+ ) {
2029
+ scrollbar-width: auto;
2030
+ scrollbar-color: auto;
2031
+ }
1990
2032
 
1991
- :where(
2033
+ :where(
1992
2034
  [data-slot='fill'],
1993
2035
  [data-slot='thread'],
1994
2036
  [data-slot='sources-strip'],
@@ -1996,13 +2038,11 @@
1996
2038
  [data-slot='scroll-area-viewport'],
1997
2039
  [data-slot='textarea'],
1998
2040
  [data-slot='composer-field']
1999
- )::-webkit-scrollbar {
2000
- width: 8px;
2001
- height: 8px;
2002
- }
2041
+ )::-webkit-scrollbar { width: 8px; height: 8px; }
2003
2042
 
2004
- /* The steppers. This is the rule that removes the arrows. */
2005
- :where(
2043
+ /* The steppers. This is the rule that removes the arrows, and the reason the
2044
+ two halves had to be separated at all. */
2045
+ :where(
2006
2046
  [data-slot='fill'],
2007
2047
  [data-slot='thread'],
2008
2048
  [data-slot='sources-strip'],
@@ -2010,13 +2050,9 @@
2010
2050
  [data-slot='scroll-area-viewport'],
2011
2051
  [data-slot='textarea'],
2012
2052
  [data-slot='composer-field']
2013
- )::-webkit-scrollbar-button {
2014
- display: none;
2015
- width: 0;
2016
- height: 0;
2017
- }
2053
+ )::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
2018
2054
 
2019
- :where(
2055
+ :where(
2020
2056
  [data-slot='fill'],
2021
2057
  [data-slot='thread'],
2022
2058
  [data-slot='sources-strip'],
@@ -2024,11 +2060,9 @@
2024
2060
  [data-slot='scroll-area-viewport'],
2025
2061
  [data-slot='textarea'],
2026
2062
  [data-slot='composer-field']
2027
- )::-webkit-scrollbar-track {
2028
- background: transparent;
2029
- }
2063
+ )::-webkit-scrollbar-track { background: transparent; }
2030
2064
 
2031
- :where(
2065
+ :where(
2032
2066
  [data-slot='fill'],
2033
2067
  [data-slot='thread'],
2034
2068
  [data-slot='sources-strip'],
@@ -2037,16 +2071,15 @@
2037
2071
  [data-slot='textarea'],
2038
2072
  [data-slot='composer-field']
2039
2073
  )::-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(
2074
+ background: rgb(255 255 255 / 0.18);
2075
+ border-radius: 999px;
2076
+ /* A transparent border with background-clip keeps the thumb off the very
2077
+ edge without a track colour doing it, so it reads as an overlay rather
2078
+ than a channel. */
2079
+ border: 2px solid transparent;
2080
+ background-clip: content-box;
2081
+ }
2082
+ :where(
2050
2083
  [data-slot='fill'],
2051
2084
  [data-slot='thread'],
2052
2085
  [data-slot='sources-strip'],
@@ -2055,12 +2088,10 @@
2055
2088
  [data-slot='textarea'],
2056
2089
  [data-slot='composer-field']
2057
2090
  )::-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(
2091
+ background: rgb(255 255 255 / 0.30);
2092
+ background-clip: content-box;
2093
+ }
2094
+ :where(
2064
2095
  [data-slot='fill'],
2065
2096
  [data-slot='thread'],
2066
2097
  [data-slot='sources-strip'],
@@ -2068,11 +2099,9 @@
2068
2099
  [data-slot='scroll-area-viewport'],
2069
2100
  [data-slot='textarea'],
2070
2101
  [data-slot='composer-field']
2071
- ) {
2072
- scrollbar-color: rgb(0 0 0 / 0.20) transparent;
2073
- }
2102
+ )::-webkit-scrollbar-corner { background: transparent; }
2074
2103
 
2075
- .t_light :where(
2104
+ .t_light :where(
2076
2105
  [data-slot='fill'],
2077
2106
  [data-slot='thread'],
2078
2107
  [data-slot='sources-strip'],
@@ -2081,17 +2110,15 @@
2081
2110
  [data-slot='textarea'],
2082
2111
  [data-slot='composer-field']
2083
2112
  )::-webkit-scrollbar-thumb {
2084
- background: rgb(0 0 0 / 0.20);
2085
- background-clip: content-box;
2113
+ background: rgb(0 0 0 / 0.20);
2114
+ background-clip: content-box;
2115
+ }
2086
2116
  }
2087
2117
 
2088
2118
  /* 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
- }
2119
+ a diagonal hatch drawn by the UA that matches nothing. A composer that grows
2120
+ with its content does not need one. */
2121
+ :where([data-slot='textarea'], [data-slot='composer-field']) { resize: none; }
2095
2122
 
2096
2123
  ._ovs-contain {overscroll-behavior:contain;}
2097
2124
  .t_unmounted .is_View, .t_unmounted .is_Text { transition: none !important; }
@@ -269,24 +269,34 @@
269
269
  /* ─────────────────────────────────────────────────────────────────────────────
270
270
  Scrollbars, on the surfaces this package scrolls.
271
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.
272
+ Left to the platform this is not neutral: Linux/GTK Chrome draws a wide track
273
+ with STEPPER ARROWS at both ends, so a dark product grows two grey buttons per
274
+ scroll region. Windows draws a wide light track. macOS overlays a thin one,
275
+ which is the behaviour the other two should have had.
276
+
277
+ THE TWO HALVES MUST NOT BE DECLARED TOGETHER, and that is the whole subtlety.
278
+ Chromium supports both the standard properties and the `::-webkit-scrollbar`
279
+ pseudo-elements — but the moment `scrollbar-width` or `scrollbar-color` is
280
+ anything other than `auto`, it takes the standard path and DISCARDS the entire
281
+ webkit block. Shipping both is how 8.1.19 removed nothing: measured headed on
282
+ Linux, as-shipped gave a 10px gutter with arrows, while the authored webkit
283
+ rule alone gives 8px with none. Isolated: webkit-only 8px no arrows;
284
+ standard-only 10px arrows; both together 10px arrows.
285
+
286
+ Neither half is redundant, so neither can simply be deleted — Firefox
287
+ implements ONLY the standard properties, Safari ONLY the webkit ones. They are
288
+ separated by engine instead: the webkit block applies wherever the selector is
289
+ understood, and the standard properties apply only where it is not.
290
+
291
+ And it is invisible headless. Chromium's headless shell paints OVERLAY
292
+ scrollbars, so nothing is drawn, `offsetWidth - clientWidth` is 0 on a blank
293
+ page, and `getComputedStyle(el, '::-webkit-scrollbar-button').display` happily
294
+ reports `none` for a rule that never ran. This must be checked headed.
288
295
  ───────────────────────────────────────────────────────────────────────────── */
289
- :where(
296
+
297
+ /* Firefox and anything else with no webkit pseudo-elements. */
298
+ @supports not selector(::-webkit-scrollbar) {
299
+ :where(
290
300
  [data-slot='fill'],
291
301
  [data-slot='thread'],
292
302
  [data-slot='sources-strip'],
@@ -295,12 +305,44 @@
295
305
  [data-slot='textarea'],
296
306
  [data-slot='composer-field']
297
307
  ) {
298
- /* Firefox — no pseudo-elements, two properties, same intent. */
299
- scrollbar-width: thin;
300
- scrollbar-color: rgb(255 255 255 / 0.18) transparent;
301
- }
308
+ scrollbar-width: thin;
309
+ scrollbar-color: rgb(255 255 255 / 0.18) transparent;
310
+ }
311
+ .t_light :where(
312
+ [data-slot='fill'],
313
+ [data-slot='thread'],
314
+ [data-slot='sources-strip'],
315
+ [data-slot='sidebar-scroll'],
316
+ [data-slot='scroll-area-viewport'],
317
+ [data-slot='textarea'],
318
+ [data-slot='composer-field']
319
+ ) { scrollbar-color: rgb(0 0 0 / 0.20) transparent; }
320
+ }
321
+
322
+ /* Chromium and Safari.
323
+ The first rule is the load-bearing one and it is a RESET, not a style.
324
+ @hanzo/design sets `scrollbar-color` and `scrollbar-width` on `:where(html)`
325
+ (theme.css), and both INHERIT — so every element in the document already
326
+ carries a non-`auto` value, Chromium takes the standard path, and the whole
327
+ webkit block below is discarded before it is read. Nothing declared here can
328
+ win until the inherited value is put back to `auto` on the element itself.
329
+ That is why the arrows survived 8.1.19: the competing declaration was one
330
+ level up and inherited, not a rule anyone would find in this file. */
331
+ @supports selector(::-webkit-scrollbar) {
332
+ :where(
333
+ [data-slot='fill'],
334
+ [data-slot='thread'],
335
+ [data-slot='sources-strip'],
336
+ [data-slot='sidebar-scroll'],
337
+ [data-slot='scroll-area-viewport'],
338
+ [data-slot='textarea'],
339
+ [data-slot='composer-field']
340
+ ) {
341
+ scrollbar-width: auto;
342
+ scrollbar-color: auto;
343
+ }
302
344
 
303
- :where(
345
+ :where(
304
346
  [data-slot='fill'],
305
347
  [data-slot='thread'],
306
348
  [data-slot='sources-strip'],
@@ -308,13 +350,11 @@
308
350
  [data-slot='scroll-area-viewport'],
309
351
  [data-slot='textarea'],
310
352
  [data-slot='composer-field']
311
- )::-webkit-scrollbar {
312
- width: 8px;
313
- height: 8px;
314
- }
353
+ )::-webkit-scrollbar { width: 8px; height: 8px; }
315
354
 
316
- /* The steppers. This is the rule that removes the arrows. */
317
- :where(
355
+ /* The steppers. This is the rule that removes the arrows, and the reason the
356
+ two halves had to be separated at all. */
357
+ :where(
318
358
  [data-slot='fill'],
319
359
  [data-slot='thread'],
320
360
  [data-slot='sources-strip'],
@@ -322,13 +362,9 @@
322
362
  [data-slot='scroll-area-viewport'],
323
363
  [data-slot='textarea'],
324
364
  [data-slot='composer-field']
325
- )::-webkit-scrollbar-button {
326
- display: none;
327
- width: 0;
328
- height: 0;
329
- }
365
+ )::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
330
366
 
331
- :where(
367
+ :where(
332
368
  [data-slot='fill'],
333
369
  [data-slot='thread'],
334
370
  [data-slot='sources-strip'],
@@ -336,11 +372,9 @@
336
372
  [data-slot='scroll-area-viewport'],
337
373
  [data-slot='textarea'],
338
374
  [data-slot='composer-field']
339
- )::-webkit-scrollbar-track {
340
- background: transparent;
341
- }
375
+ )::-webkit-scrollbar-track { background: transparent; }
342
376
 
343
- :where(
377
+ :where(
344
378
  [data-slot='fill'],
345
379
  [data-slot='thread'],
346
380
  [data-slot='sources-strip'],
@@ -349,16 +383,15 @@
349
383
  [data-slot='textarea'],
350
384
  [data-slot='composer-field']
351
385
  )::-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(
386
+ background: rgb(255 255 255 / 0.18);
387
+ border-radius: 999px;
388
+ /* A transparent border with background-clip keeps the thumb off the very
389
+ edge without a track colour doing it, so it reads as an overlay rather
390
+ than a channel. */
391
+ border: 2px solid transparent;
392
+ background-clip: content-box;
393
+ }
394
+ :where(
362
395
  [data-slot='fill'],
363
396
  [data-slot='thread'],
364
397
  [data-slot='sources-strip'],
@@ -367,12 +400,10 @@
367
400
  [data-slot='textarea'],
368
401
  [data-slot='composer-field']
369
402
  )::-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(
403
+ background: rgb(255 255 255 / 0.30);
404
+ background-clip: content-box;
405
+ }
406
+ :where(
376
407
  [data-slot='fill'],
377
408
  [data-slot='thread'],
378
409
  [data-slot='sources-strip'],
@@ -380,11 +411,9 @@
380
411
  [data-slot='scroll-area-viewport'],
381
412
  [data-slot='textarea'],
382
413
  [data-slot='composer-field']
383
- ) {
384
- scrollbar-color: rgb(0 0 0 / 0.20) transparent;
385
- }
414
+ )::-webkit-scrollbar-corner { background: transparent; }
386
415
 
387
- .t_light :where(
416
+ .t_light :where(
388
417
  [data-slot='fill'],
389
418
  [data-slot='thread'],
390
419
  [data-slot='sources-strip'],
@@ -393,14 +422,12 @@
393
422
  [data-slot='textarea'],
394
423
  [data-slot='composer-field']
395
424
  )::-webkit-scrollbar-thumb {
396
- background: rgb(0 0 0 / 0.20);
397
- background-clip: content-box;
425
+ background: rgb(0 0 0 / 0.20);
426
+ background-clip: content-box;
427
+ }
398
428
  }
399
429
 
400
430
  /* 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
- }
431
+ a diagonal hatch drawn by the UA that matches nothing. A composer that grows
432
+ with its content does not need one. */
433
+ :where([data-slot='textarea'], [data-slot='composer-field']) { resize: none; }
package/dist/styles.css CHANGED
@@ -1957,24 +1957,34 @@
1957
1957
  /* ─────────────────────────────────────────────────────────────────────────────
1958
1958
  Scrollbars, on the surfaces this package scrolls.
1959
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.
1960
+ Left to the platform this is not neutral: Linux/GTK Chrome draws a wide track
1961
+ with STEPPER ARROWS at both ends, so a dark product grows two grey buttons per
1962
+ scroll region. Windows draws a wide light track. macOS overlays a thin one,
1963
+ which is the behaviour the other two should have had.
1964
+
1965
+ THE TWO HALVES MUST NOT BE DECLARED TOGETHER, and that is the whole subtlety.
1966
+ Chromium supports both the standard properties and the `::-webkit-scrollbar`
1967
+ pseudo-elements — but the moment `scrollbar-width` or `scrollbar-color` is
1968
+ anything other than `auto`, it takes the standard path and DISCARDS the entire
1969
+ webkit block. Shipping both is how 8.1.19 removed nothing: measured headed on
1970
+ Linux, as-shipped gave a 10px gutter with arrows, while the authored webkit
1971
+ rule alone gives 8px with none. Isolated: webkit-only 8px no arrows;
1972
+ standard-only 10px arrows; both together 10px arrows.
1973
+
1974
+ Neither half is redundant, so neither can simply be deleted — Firefox
1975
+ implements ONLY the standard properties, Safari ONLY the webkit ones. They are
1976
+ separated by engine instead: the webkit block applies wherever the selector is
1977
+ understood, and the standard properties apply only where it is not.
1978
+
1979
+ And it is invisible headless. Chromium's headless shell paints OVERLAY
1980
+ scrollbars, so nothing is drawn, `offsetWidth - clientWidth` is 0 on a blank
1981
+ page, and `getComputedStyle(el, '::-webkit-scrollbar-button').display` happily
1982
+ reports `none` for a rule that never ran. This must be checked headed.
1976
1983
  ───────────────────────────────────────────────────────────────────────────── */
1977
- :where(
1984
+
1985
+ /* Firefox and anything else with no webkit pseudo-elements. */
1986
+ @supports not selector(::-webkit-scrollbar) {
1987
+ :where(
1978
1988
  [data-slot='fill'],
1979
1989
  [data-slot='thread'],
1980
1990
  [data-slot='sources-strip'],
@@ -1983,12 +1993,44 @@
1983
1993
  [data-slot='textarea'],
1984
1994
  [data-slot='composer-field']
1985
1995
  ) {
1986
- /* Firefox — no pseudo-elements, two properties, same intent. */
1987
- scrollbar-width: thin;
1988
- scrollbar-color: rgb(255 255 255 / 0.18) transparent;
1989
- }
1996
+ scrollbar-width: thin;
1997
+ scrollbar-color: rgb(255 255 255 / 0.18) transparent;
1998
+ }
1999
+ .t_light :where(
2000
+ [data-slot='fill'],
2001
+ [data-slot='thread'],
2002
+ [data-slot='sources-strip'],
2003
+ [data-slot='sidebar-scroll'],
2004
+ [data-slot='scroll-area-viewport'],
2005
+ [data-slot='textarea'],
2006
+ [data-slot='composer-field']
2007
+ ) { scrollbar-color: rgb(0 0 0 / 0.20) transparent; }
2008
+ }
2009
+
2010
+ /* Chromium and Safari.
2011
+ The first rule is the load-bearing one and it is a RESET, not a style.
2012
+ @hanzo/design sets `scrollbar-color` and `scrollbar-width` on `:where(html)`
2013
+ (theme.css), and both INHERIT — so every element in the document already
2014
+ carries a non-`auto` value, Chromium takes the standard path, and the whole
2015
+ webkit block below is discarded before it is read. Nothing declared here can
2016
+ win until the inherited value is put back to `auto` on the element itself.
2017
+ That is why the arrows survived 8.1.19: the competing declaration was one
2018
+ level up and inherited, not a rule anyone would find in this file. */
2019
+ @supports selector(::-webkit-scrollbar) {
2020
+ :where(
2021
+ [data-slot='fill'],
2022
+ [data-slot='thread'],
2023
+ [data-slot='sources-strip'],
2024
+ [data-slot='sidebar-scroll'],
2025
+ [data-slot='scroll-area-viewport'],
2026
+ [data-slot='textarea'],
2027
+ [data-slot='composer-field']
2028
+ ) {
2029
+ scrollbar-width: auto;
2030
+ scrollbar-color: auto;
2031
+ }
1990
2032
 
1991
- :where(
2033
+ :where(
1992
2034
  [data-slot='fill'],
1993
2035
  [data-slot='thread'],
1994
2036
  [data-slot='sources-strip'],
@@ -1996,13 +2038,11 @@
1996
2038
  [data-slot='scroll-area-viewport'],
1997
2039
  [data-slot='textarea'],
1998
2040
  [data-slot='composer-field']
1999
- )::-webkit-scrollbar {
2000
- width: 8px;
2001
- height: 8px;
2002
- }
2041
+ )::-webkit-scrollbar { width: 8px; height: 8px; }
2003
2042
 
2004
- /* The steppers. This is the rule that removes the arrows. */
2005
- :where(
2043
+ /* The steppers. This is the rule that removes the arrows, and the reason the
2044
+ two halves had to be separated at all. */
2045
+ :where(
2006
2046
  [data-slot='fill'],
2007
2047
  [data-slot='thread'],
2008
2048
  [data-slot='sources-strip'],
@@ -2010,13 +2050,9 @@
2010
2050
  [data-slot='scroll-area-viewport'],
2011
2051
  [data-slot='textarea'],
2012
2052
  [data-slot='composer-field']
2013
- )::-webkit-scrollbar-button {
2014
- display: none;
2015
- width: 0;
2016
- height: 0;
2017
- }
2053
+ )::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
2018
2054
 
2019
- :where(
2055
+ :where(
2020
2056
  [data-slot='fill'],
2021
2057
  [data-slot='thread'],
2022
2058
  [data-slot='sources-strip'],
@@ -2024,11 +2060,9 @@
2024
2060
  [data-slot='scroll-area-viewport'],
2025
2061
  [data-slot='textarea'],
2026
2062
  [data-slot='composer-field']
2027
- )::-webkit-scrollbar-track {
2028
- background: transparent;
2029
- }
2063
+ )::-webkit-scrollbar-track { background: transparent; }
2030
2064
 
2031
- :where(
2065
+ :where(
2032
2066
  [data-slot='fill'],
2033
2067
  [data-slot='thread'],
2034
2068
  [data-slot='sources-strip'],
@@ -2037,16 +2071,15 @@
2037
2071
  [data-slot='textarea'],
2038
2072
  [data-slot='composer-field']
2039
2073
  )::-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(
2074
+ background: rgb(255 255 255 / 0.18);
2075
+ border-radius: 999px;
2076
+ /* A transparent border with background-clip keeps the thumb off the very
2077
+ edge without a track colour doing it, so it reads as an overlay rather
2078
+ than a channel. */
2079
+ border: 2px solid transparent;
2080
+ background-clip: content-box;
2081
+ }
2082
+ :where(
2050
2083
  [data-slot='fill'],
2051
2084
  [data-slot='thread'],
2052
2085
  [data-slot='sources-strip'],
@@ -2055,12 +2088,10 @@
2055
2088
  [data-slot='textarea'],
2056
2089
  [data-slot='composer-field']
2057
2090
  )::-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(
2091
+ background: rgb(255 255 255 / 0.30);
2092
+ background-clip: content-box;
2093
+ }
2094
+ :where(
2064
2095
  [data-slot='fill'],
2065
2096
  [data-slot='thread'],
2066
2097
  [data-slot='sources-strip'],
@@ -2068,11 +2099,9 @@
2068
2099
  [data-slot='scroll-area-viewport'],
2069
2100
  [data-slot='textarea'],
2070
2101
  [data-slot='composer-field']
2071
- ) {
2072
- scrollbar-color: rgb(0 0 0 / 0.20) transparent;
2073
- }
2102
+ )::-webkit-scrollbar-corner { background: transparent; }
2074
2103
 
2075
- .t_light :where(
2104
+ .t_light :where(
2076
2105
  [data-slot='fill'],
2077
2106
  [data-slot='thread'],
2078
2107
  [data-slot='sources-strip'],
@@ -2081,17 +2110,15 @@
2081
2110
  [data-slot='textarea'],
2082
2111
  [data-slot='composer-field']
2083
2112
  )::-webkit-scrollbar-thumb {
2084
- background: rgb(0 0 0 / 0.20);
2085
- background-clip: content-box;
2113
+ background: rgb(0 0 0 / 0.20);
2114
+ background-clip: content-box;
2115
+ }
2086
2116
  }
2087
2117
 
2088
2118
  /* 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
- }
2119
+ a diagonal hatch drawn by the UA that matches nothing. A composer that grows
2120
+ with its content does not need one. */
2121
+ :where([data-slot='textarea'], [data-slot='composer-field']) { resize: none; }
2095
2122
 
2096
2123
  ._ovs-contain {overscroll-behavior:contain;}
2097
2124
  .t_unmounted .is_View, .t_unmounted .is_Text { transition: none !important; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/ui",
3
- "version": "8.1.19",
3
+ "version": "8.1.21",
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",