@hanzo/ui 8.0.47 → 8.0.48
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/backends/gui/glass.cjs +22 -13
- package/dist/backends/gui/glass.d.ts +37 -6
- package/dist/backends/gui/glass.d.ts.map +1 -1
- package/dist/backends/gui/glass.js +23 -14
- package/dist/backends/gui/glass.js.map +1 -1
- package/dist/glass.cjs +256 -0
- package/dist/glass.css +140 -0
- package/dist/glass.d.ts +243 -0
- package/dist/glass.d.ts.map +1 -0
- package/dist/glass.js +252 -0
- package/dist/glass.js.map +1 -0
- package/dist/product/SlideOver.cjs +6 -1
- package/dist/product/SlideOver.d.ts.map +1 -1
- package/dist/product/SlideOver.js +6 -1
- package/dist/product/SlideOver.js.map +1 -1
- package/dist/styles/motion.css +10 -3
- package/dist/styles.css +156 -14
- package/dist/theme.css +146 -11
- package/package.json +9 -2
|
@@ -10,22 +10,31 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
10
10
|
*
|
|
11
11
|
* It is a surface, not a layout: it composes AROUND content and adds nothing
|
|
12
12
|
* but the material. The component itself is a solid raised panel; the
|
|
13
|
-
* translucency + blur live in
|
|
14
|
-
* keyed by [data-slot="glass"] — so a platform that cannot blur (older
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* translucency + blur live in glass.css under `@supports (backdrop-filter)`,
|
|
14
|
+
* keyed by [data-slot="glass"] — so a platform that cannot blur (older engines,
|
|
15
|
+
* native without a blur view) keeps the solid panel and content never becomes
|
|
16
|
+
* illegible over a busy backdrop. One material, declared once.
|
|
17
|
+
*
|
|
18
|
+
* The look comes from `glass(level)` in `@hanzo/ui/glass`, not from a `styled`
|
|
19
|
+
* of its own. It used to be both, and they disagreed: the frame's stand-in
|
|
20
|
+
* ground was `$color2` while the material is 72% of `--background`, so a
|
|
21
|
+
* browser that could blur and one that could not showed two different colours
|
|
22
|
+
* of menu. A component and a recipe describing the same material in two places
|
|
23
|
+
* is how they end up describing two materials. This is the component FORM of
|
|
24
|
+
* that value — it exists so a surface built from scratch does not have to reach
|
|
25
|
+
* for an `XStack` and remember to spread. Every surface the library already
|
|
26
|
+
* names (dialog, popover, select, dropdown, tooltip) is glass by slot and needs
|
|
27
|
+
* neither.
|
|
17
28
|
*
|
|
18
29
|
* Page sections never wear glass; they sit on the surface ladder ($color2/3).
|
|
19
30
|
*/
|
|
20
31
|
const gui_1 = require("@hanzo/gui");
|
|
32
|
+
const glass_1 = require("../../glass.cjs");
|
|
21
33
|
const slot_1 = require("./slot.cjs");
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
bg: '$color2',
|
|
29
|
-
});
|
|
30
|
-
const Glass = (props) => (0, jsx_runtime_1.jsx)(GlassFrame, { ...(0, slot_1.slot)('glass'), ...props });
|
|
34
|
+
/**
|
|
35
|
+
* The border WIDTH is geometry and belongs to the component, not to the
|
|
36
|
+
* material — a sticky bar cut from the same glass wants one edge, not four —
|
|
37
|
+
* so the recipe states only the colour and this states the default width.
|
|
38
|
+
*/
|
|
39
|
+
const Glass = ({ level = 2, ...props }) => ((0, jsx_runtime_1.jsx)(gui_1.YStack, { ...(0, slot_1.slot)('glass'), borderWidth: 1, ...(0, glass_1.glass)(level), ...props }));
|
|
31
40
|
exports.Glass = Glass;
|
|
@@ -1,9 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glass — the ONE frosted material, for FLOATING chrome only (menus, dialogs,
|
|
3
|
+
* popovers, docked toolbars). macOS/iOS vibrancy: a translucent ground the
|
|
4
|
+
* backdrop blurs through, sealed with a hairline border.
|
|
5
|
+
*
|
|
6
|
+
* It is a surface, not a layout: it composes AROUND content and adds nothing
|
|
7
|
+
* but the material. The component itself is a solid raised panel; the
|
|
8
|
+
* translucency + blur live in glass.css under `@supports (backdrop-filter)`,
|
|
9
|
+
* keyed by [data-slot="glass"] — so a platform that cannot blur (older engines,
|
|
10
|
+
* native without a blur view) keeps the solid panel and content never becomes
|
|
11
|
+
* illegible over a busy backdrop. One material, declared once.
|
|
12
|
+
*
|
|
13
|
+
* The look comes from `glass(level)` in `@hanzo/ui/glass`, not from a `styled`
|
|
14
|
+
* of its own. It used to be both, and they disagreed: the frame's stand-in
|
|
15
|
+
* ground was `$color2` while the material is 72% of `--background`, so a
|
|
16
|
+
* browser that could blur and one that could not showed two different colours
|
|
17
|
+
* of menu. A component and a recipe describing the same material in two places
|
|
18
|
+
* is how they end up describing two materials. This is the component FORM of
|
|
19
|
+
* that value — it exists so a surface built from scratch does not have to reach
|
|
20
|
+
* for an `XStack` and remember to spread. Every surface the library already
|
|
21
|
+
* names (dialog, popover, select, dropdown, tooltip) is glass by slot and needs
|
|
22
|
+
* neither.
|
|
23
|
+
*
|
|
24
|
+
* Page sections never wear glass; they sit on the surface ladder ($color2/3).
|
|
25
|
+
*/
|
|
26
|
+
import { YStack } from '@hanzo/gui';
|
|
1
27
|
import type { ComponentProps } from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
28
|
+
import { type Lift } from '../../glass.js';
|
|
29
|
+
export type GlassProps = ComponentProps<typeof YStack> & {
|
|
30
|
+
/** 2 is anchored — a menu, a toast, a bar. 3 floats free over the page. */
|
|
31
|
+
level?: Lift;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The border WIDTH is geometry and belongs to the component, not to the
|
|
35
|
+
* material — a sticky bar cut from the same glass wants one edge, not four —
|
|
36
|
+
* so the recipe states only the colour and this states the default width.
|
|
37
|
+
*/
|
|
38
|
+
declare const Glass: ({ level, ...props }: GlassProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
39
|
export { Glass };
|
|
9
40
|
//# sourceMappingURL=glass.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"glass.d.ts","sourceRoot":"","sources":["../../../src/backends/gui/glass.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"glass.d.ts","sourceRoot":"","sources":["../../../src/backends/gui/glass.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AACnC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAE3C,OAAO,EAAE,KAAK,IAAI,EAAS,MAAM,aAAa,CAAA;AAG9C,MAAM,MAAM,UAAU,GAAG,cAAc,CAAC,OAAO,MAAM,CAAC,GAAG;IACvD,2EAA2E;IAC3E,KAAK,CAAC,EAAE,IAAI,CAAA;CACb,CAAA;AAED;;;;GAIG;AACH,QAAA,MAAM,KAAK,GAAI,qBAAyB,UAAU,4CAEjD,CAAA;AAED,OAAO,EAAE,KAAK,EAAE,CAAA"}
|
|
@@ -7,23 +7,32 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
7
7
|
*
|
|
8
8
|
* It is a surface, not a layout: it composes AROUND content and adds nothing
|
|
9
9
|
* but the material. The component itself is a solid raised panel; the
|
|
10
|
-
* translucency + blur live in
|
|
11
|
-
* keyed by [data-slot="glass"] — so a platform that cannot blur (older
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* translucency + blur live in glass.css under `@supports (backdrop-filter)`,
|
|
11
|
+
* keyed by [data-slot="glass"] — so a platform that cannot blur (older engines,
|
|
12
|
+
* native without a blur view) keeps the solid panel and content never becomes
|
|
13
|
+
* illegible over a busy backdrop. One material, declared once.
|
|
14
|
+
*
|
|
15
|
+
* The look comes from `glass(level)` in `@hanzo/ui/glass`, not from a `styled`
|
|
16
|
+
* of its own. It used to be both, and they disagreed: the frame's stand-in
|
|
17
|
+
* ground was `$color2` while the material is 72% of `--background`, so a
|
|
18
|
+
* browser that could blur and one that could not showed two different colours
|
|
19
|
+
* of menu. A component and a recipe describing the same material in two places
|
|
20
|
+
* is how they end up describing two materials. This is the component FORM of
|
|
21
|
+
* that value — it exists so a surface built from scratch does not have to reach
|
|
22
|
+
* for an `XStack` and remember to spread. Every surface the library already
|
|
23
|
+
* names (dialog, popover, select, dropdown, tooltip) is glass by slot and needs
|
|
24
|
+
* neither.
|
|
14
25
|
*
|
|
15
26
|
* Page sections never wear glass; they sit on the surface ladder ($color2/3).
|
|
16
27
|
*/
|
|
17
|
-
import { YStack
|
|
28
|
+
import { YStack } from '@hanzo/gui';
|
|
29
|
+
import { glass } from '../../glass.js';
|
|
18
30
|
import { slot } from './slot.js';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
bg: '$color2',
|
|
26
|
-
});
|
|
27
|
-
const Glass = (props) => _jsx(GlassFrame, { ...slot('glass'), ...props });
|
|
31
|
+
/**
|
|
32
|
+
* The border WIDTH is geometry and belongs to the component, not to the
|
|
33
|
+
* material — a sticky bar cut from the same glass wants one edge, not four —
|
|
34
|
+
* so the recipe states only the colour and this states the default width.
|
|
35
|
+
*/
|
|
36
|
+
const Glass = ({ level = 2, ...props }) => (_jsx(YStack, { ...slot('glass'), borderWidth: 1, ...glass(level), ...props }));
|
|
28
37
|
export { Glass };
|
|
29
38
|
//# sourceMappingURL=glass.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"glass.js","sourceRoot":"","sources":["../../../src/backends/gui/glass.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ
|
|
1
|
+
{"version":3,"file":"glass.js","sourceRoot":"","sources":["../../../src/backends/gui/glass.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAGnC,OAAO,EAAa,KAAK,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAO7B;;;;GAIG;AACH,MAAM,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,KAAK,EAAc,EAAE,EAAE,CAAC,CACrD,KAAC,MAAM,OAAK,IAAI,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC,KAAM,KAAK,CAAC,KAAK,CAAC,KAAM,KAAK,GAAI,CAC3E,CAAA;AAED,OAAO,EAAE,KAAK,EAAE,CAAA"}
|
package/dist/glass.cjs
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.screen = exports.accent = exports.selected = exports.row = exports.rows = exports.panel = exports.scrim = exports.glass = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The chrome, as values — `@hanzo/ui/glass`.
|
|
6
|
+
*
|
|
7
|
+
* import { glass, panel, scrim } from '@hanzo/ui/glass'
|
|
8
|
+
* import '@hanzo/ui/glass.css' // or '@hanzo/ui/theme.css', which inlines it
|
|
9
|
+
*
|
|
10
|
+
* Eight recipes, because eight things were being spelled out by hand on every
|
|
11
|
+
* page and drifting apart every time: what a floating surface is made of, what
|
|
12
|
+
* a resting one is made of, what dims the page under a modal, how a set of
|
|
13
|
+
* rows is grouped, what one of those rows looks like, which item in a set is
|
|
14
|
+
* the current one, which control is the loud one, and what it means to fill the
|
|
15
|
+
* screen. Each is ONE value here and nowhere else.
|
|
16
|
+
*
|
|
17
|
+
* They are plain objects, spread onto any `@hanzo/gui` element:
|
|
18
|
+
*
|
|
19
|
+
* <YStack {...panel}>…</YStack>
|
|
20
|
+
* <XStack {...glass(2)} borderBottomWidth={1}>…</XStack>
|
|
21
|
+
*
|
|
22
|
+
* A recipe rather than a component or a variant, because a variant only reaches
|
|
23
|
+
* the one component that declares it and half of what wants these is an
|
|
24
|
+
* `XStack`, a `SizableText` or a `Link`. One value that spreads onto any
|
|
25
|
+
* element beats N spellings of it.
|
|
26
|
+
*
|
|
27
|
+
* The material and the ladder live in `glass.css`; these name them. Nothing
|
|
28
|
+
* here restates a colour, a shadow or a blur radius — see that file for where
|
|
29
|
+
* the values come from (`@hanzo/design`).
|
|
30
|
+
*/
|
|
31
|
+
const slot_1 = require("./backends/gui/slot.cjs");
|
|
32
|
+
/**
|
|
33
|
+
* Glass — the material floating chrome is made of, and only floating chrome.
|
|
34
|
+
*
|
|
35
|
+
* Menus, dialogs, popovers, toasts, sticky bars: things with page under them.
|
|
36
|
+
* Vibrancy is a lens, so a surface covering nothing but the flat canvas has
|
|
37
|
+
* nothing to soften — an in-flow card wearing this is not glass, it is the page
|
|
38
|
+
* with extra steps, one shade off what it sits on and harder to find than
|
|
39
|
+
* `panel`. Over content -> `glass`, in the flow -> `panel`. No third case.
|
|
40
|
+
*
|
|
41
|
+
* The library's own dialogs, popovers, selects, dropdowns and tooltips are
|
|
42
|
+
* already glass BY SLOT — `glass.css` attaches the material to what a thing IS,
|
|
43
|
+
* so they need nothing from a call site and cannot fall out of step. This is
|
|
44
|
+
* for the surfaces no slot names: a hand-rolled bar, a custom menu, a docked
|
|
45
|
+
* toolbar.
|
|
46
|
+
*
|
|
47
|
+
* MATERIAL ONLY, no geometry. What glass is made of does not tell you what
|
|
48
|
+
* shape it was cut into: a menu is rounded on four sides, a sticky bar is
|
|
49
|
+
* full-bleed with one hairline along its bottom. Baking `borderRadius` and
|
|
50
|
+
* `borderWidth` in here would make every bar undo them, so edges stay at the
|
|
51
|
+
* call site — which only ever needs to say WHICH ones (`borderBottomWidth={1}`),
|
|
52
|
+
* never what colour, because the material already knows.
|
|
53
|
+
*
|
|
54
|
+
* The level is the one thing that genuinely varies, so it is the argument
|
|
55
|
+
* (`selected(on)` is shaped the same way). 2 is anchored — a menu, a toast, a
|
|
56
|
+
* bar, lifted off the page but tied to it. 3 is a modal, floating free over
|
|
57
|
+
* everything with a scrim under it.
|
|
58
|
+
*
|
|
59
|
+
* `backgroundColor` is the opaque fallback, and it is load-bearing rather than
|
|
60
|
+
* belt-and-braces: the material lives inside `@supports (backdrop-filter)`, so
|
|
61
|
+
* a browser that cannot blur gets no rule at all — and a menu with no
|
|
62
|
+
* background is a menu you read the page through.
|
|
63
|
+
*/
|
|
64
|
+
const glass = (level = 2) => ({
|
|
65
|
+
className: `glass elevation-${level}`,
|
|
66
|
+
backgroundColor: '$background',
|
|
67
|
+
borderColor: '$borderColor',
|
|
68
|
+
});
|
|
69
|
+
exports.glass = glass;
|
|
70
|
+
/**
|
|
71
|
+
* The scrim under anything modal — the dim that makes a floating panel read as
|
|
72
|
+
* floating rather than as more page.
|
|
73
|
+
*
|
|
74
|
+
* Hand-rolled modals reach for `backgroundColor="black"`, with no alpha. That
|
|
75
|
+
* does not dim the page behind them, it DELETES it — and a glass panel over a
|
|
76
|
+
* solid black wall has nothing left to be translucent about, so the scrim
|
|
77
|
+
* cancels the very material it exists to make read.
|
|
78
|
+
*
|
|
79
|
+
* The value is a design token, not a literal, because both languages need it
|
|
80
|
+
* and only one of them can hold the copy: `[data-slot="dialog-overlay"]` reads
|
|
81
|
+
* `--surface-scrim` in `glass.css` and this reads the same token, so the dim
|
|
82
|
+
* behind a library dialog and the dim behind a hand-rolled one are the same dim
|
|
83
|
+
* by construction rather than by two people remembering a number.
|
|
84
|
+
*/
|
|
85
|
+
exports.scrim = { backgroundColor: 'var(--surface-scrim, rgb(0 0 0 / .8))' };
|
|
86
|
+
/**
|
|
87
|
+
* A panel: the quiet surface content in the FLOW sits on.
|
|
88
|
+
*
|
|
89
|
+
* `$color2` is a step off the page, so the fill alone separates the panel from
|
|
90
|
+
* the page and the hairline only finishes the edge. Panels are otherwise
|
|
91
|
+
* variously `$background` (a border with nothing behind it), `$color3` (which
|
|
92
|
+
* is the ACTIVE-item fill — a state, not a surface) and as many radii as there
|
|
93
|
+
* are authors.
|
|
94
|
+
*
|
|
95
|
+
* Rung 1 on the ladder, which is NOT the same claim as "this floats". A cast
|
|
96
|
+
* shadow does say "above the page", and that stays false for a panel in the
|
|
97
|
+
* flow — so rung 1 spends almost nothing on shadow and buys its depth from the
|
|
98
|
+
* lit top edge instead. On a near-black canvas that is the only half that reads
|
|
99
|
+
* at all.
|
|
100
|
+
*/
|
|
101
|
+
exports.panel = {
|
|
102
|
+
backgroundColor: '$color2',
|
|
103
|
+
borderWidth: 1,
|
|
104
|
+
borderColor: '$borderColor',
|
|
105
|
+
borderRadius: '$6',
|
|
106
|
+
className: 'elevation-1',
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* A group of rows — the settings card, and the reason a settings page ends up
|
|
110
|
+
* with four cards where it wanted one.
|
|
111
|
+
*
|
|
112
|
+
* Four toggles as four separate panels reads as four unrelated decisions, each
|
|
113
|
+
* with its own four edges; the same four inside one card, parted by hairlines,
|
|
114
|
+
* reads as one set and drops twelve edges of noise. That is the macOS settings
|
|
115
|
+
* group, and it is the shape every settings surface reaches for by hand.
|
|
116
|
+
*
|
|
117
|
+
* `overflow: hidden` is not decoration — the separators are drawn by the card
|
|
118
|
+
* (`[data-slot="rows"] > * + *`), and without the clip the topmost one runs
|
|
119
|
+
* straight through the rounded corner and out the side.
|
|
120
|
+
*
|
|
121
|
+
* It spreads `panel` WHOLE, so moving the panel up or down the ladder moves its
|
|
122
|
+
* groups with it. The separator hangs off a slot rather than a second class
|
|
123
|
+
* name for the same reason: there is no string to keep in step.
|
|
124
|
+
*/
|
|
125
|
+
exports.rows = {
|
|
126
|
+
...exports.panel,
|
|
127
|
+
...(0, slot_1.slot)('rows'),
|
|
128
|
+
overflow: 'hidden',
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* One line inside a `rows` card: name on the left, control hard right.
|
|
132
|
+
*
|
|
133
|
+
* The rhythm, so it stops being re-decided per file. Pair it with a label at
|
|
134
|
+
* `fontSize="$3"` in `$color` and any explanation under it at `$1` in
|
|
135
|
+
* `$color11` — both halves matter, because a row whose label is written at
|
|
136
|
+
* `$color11` (the SECONDARY colour) gives the name of the setting and the
|
|
137
|
+
* footnote about it the same weight of grey, and the row has no first thing to
|
|
138
|
+
* read.
|
|
139
|
+
*
|
|
140
|
+
* No border here. A row that draws its own top hairline is a row that has to
|
|
141
|
+
* know whether it is first, and the card already knows — see `rows`.
|
|
142
|
+
*/
|
|
143
|
+
exports.row = {
|
|
144
|
+
flexDirection: 'row',
|
|
145
|
+
alignItems: 'center',
|
|
146
|
+
justifyContent: 'space-between',
|
|
147
|
+
gap: '$4',
|
|
148
|
+
paddingHorizontal: '$4',
|
|
149
|
+
paddingVertical: '$3',
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Selected — the ONE "you are here" look, wherever a set offers a choice: nav
|
|
153
|
+
* rows, tabs, a filter chip.
|
|
154
|
+
*
|
|
155
|
+
* `$color3` fill with a full-strength label, one step above the panel it sits
|
|
156
|
+
* on. The alternatives in the wild are a pure-white lozenge, and — far more
|
|
157
|
+
* often — no answer at all: an unstyled `TabsTrigger` paints a selected tab
|
|
158
|
+
* exactly like its neighbours, so the page simply does not say which one you
|
|
159
|
+
* are on. Nothing in the markup looks wrong; the state has no picture.
|
|
160
|
+
*
|
|
161
|
+
* The label is `$color12`, not `$color`, and that is a measured distinction
|
|
162
|
+
* rather than a preference. Themes NEST: at the page scope `--color` is
|
|
163
|
+
* hsl(0 0% 100%), but inside a sidebar's scope it is re-based to hsl(0 0% 80%)
|
|
164
|
+
* — the SAME value as `--color11`. So a selected row asking for `$color` gets
|
|
165
|
+
* exactly its neighbours' colour, and had been doing so all along; the markup
|
|
166
|
+
* said "highlight this" and the token quietly collapsed underneath. `$color12`
|
|
167
|
+
* is the theme's full-strength foreground in every scope, so the state survives
|
|
168
|
+
* nesting.
|
|
169
|
+
*/
|
|
170
|
+
const selected = (on) => on
|
|
171
|
+
? { backgroundColor: '$color3', color: '$color12' }
|
|
172
|
+
: { backgroundColor: 'transparent', color: '$color11' };
|
|
173
|
+
exports.selected = selected;
|
|
174
|
+
/**
|
|
175
|
+
* The primary control — the ONE loud thing a page is allowed.
|
|
176
|
+
*
|
|
177
|
+
* `$color5` on `$color6` is a raised pushbutton: legible, quiet, monochrome.
|
|
178
|
+
*
|
|
179
|
+
* Under-emphasis is the failure mode, and it leaves no mark on a screenshot,
|
|
180
|
+
* which is exactly why it needs a rule rather than an eye. `Button`'s `default`
|
|
181
|
+
* variant is a QUIET control on the surface ladder (`$color2`, the same fill
|
|
182
|
+
* `panel` uses), so a dialog whose only action is an unnamed `<Button>` paints
|
|
183
|
+
* its single ask as another surface. Name a variant on every control, and when
|
|
184
|
+
* one is the primary action, spread this. Never `variant="default"`.
|
|
185
|
+
*
|
|
186
|
+
* Not `variant="primary"` either, even though it paints the same pixels — a
|
|
187
|
+
* variant only reaches a `Button`, and half the loud controls in a real app are
|
|
188
|
+
* an `XStack`, a `SizableText` or a `Link`.
|
|
189
|
+
*/
|
|
190
|
+
exports.accent = {
|
|
191
|
+
backgroundColor: '$color5',
|
|
192
|
+
borderWidth: 1,
|
|
193
|
+
borderColor: '$color6',
|
|
194
|
+
/**
|
|
195
|
+
* The foreground is part of the recipe, not an afterthought.
|
|
196
|
+
*
|
|
197
|
+
* Spell the fill by hand and you own the label too, and that half is the one
|
|
198
|
+
* people drop: a hand-spelled `color="$color1"` (4% grey) on a fill that has
|
|
199
|
+
* since moved to `$color2` (8%) paints a label at 1.07:1 — measured in the
|
|
200
|
+
* browser, not guessed. The sign-in modal's only button was invisible.
|
|
201
|
+
*
|
|
202
|
+
* And it only reaches a label the BUTTON paints. This is the half that is
|
|
203
|
+
* easy to miss: `<Button {...accent}><SizableText>…</SizableText></Button>`
|
|
204
|
+
* puts a second component between the recipe and the text, and that component
|
|
205
|
+
* resolves its own colour from the theme scope the Button just mounted —
|
|
206
|
+
* where `--color` is re-based to 80%. Measured: fill rgb(51,51,51) and border
|
|
207
|
+
* rgb(69,69,69), correct, with the label at rgb(204,204,204) — `$color11`,
|
|
208
|
+
* the QUIET foreground, on the loudest control in the dialog.
|
|
209
|
+
*
|
|
210
|
+
* Two guesses about the cure are both WRONG, and each was measured:
|
|
211
|
+
*
|
|
212
|
+
* - "Just don't name a colour." A bare `<SizableText>` inside the Button
|
|
213
|
+
* still measures rgb(204,204,204). Silence is not neutral here; the
|
|
214
|
+
* nested scope answers for you. The rule is to SAY `$color12`, not to say
|
|
215
|
+
* nothing.
|
|
216
|
+
* - "Hoist the size to the Button and drop the wrapper." `fontSize="$1"` on
|
|
217
|
+
* the Button does not reach the label — still the `size` default. So a
|
|
218
|
+
* control that genuinely wants a smaller label keeps its wrapper.
|
|
219
|
+
*
|
|
220
|
+
* Which leaves two honest shapes. No size of your own -> hand the string
|
|
221
|
+
* straight to the Button and let its text host paint it. A deliberate size or
|
|
222
|
+
* `numberOfLines` -> keep the wrapper and give it `color="$color12"`.
|
|
223
|
+
*
|
|
224
|
+
* BUTTON ONLY. A plain `XStack` mounts no theme scope, so a label inside one
|
|
225
|
+
* that asks for `$color` measures rgb(255,255,255) — correct, and not to be
|
|
226
|
+
* "fixed". The ban is exactly as wide as the re-basing.
|
|
227
|
+
*/
|
|
228
|
+
color: '$color12',
|
|
229
|
+
hoverStyle: { backgroundColor: '$color6', color: '$color12' },
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* A state that owns the whole screen: a loading gate, a 404, a crash screen, an
|
|
233
|
+
* OAuth callback, a receipt. It stands alone — no shell above it — so it must
|
|
234
|
+
* MEASURE the screen itself.
|
|
235
|
+
*
|
|
236
|
+
* `minHeight="100%"` does not measure it, and that is the bug this value ends.
|
|
237
|
+
* A percentage resolves against the parent's computed height; every ancestor up
|
|
238
|
+
* to `<body>` is `height: auto`, and `@hanzo/gui`'s provider span is
|
|
239
|
+
* `display: contents`, so there is no box in between either. The percentage
|
|
240
|
+
* resolves to auto, the stack shrink-wraps its content, and `justifyContent:
|
|
241
|
+
* center` then centres inside THAT — perfectly, and invisibly, in a box the
|
|
242
|
+
* height of the content. Measured: a brand mark at y=0 of a 903px viewport with
|
|
243
|
+
* 860px of black under it, across ten states carrying the identical line.
|
|
244
|
+
*
|
|
245
|
+
* `dvh`, not `vh`: on a phone the URL bar collapses and `100vh` overflows by
|
|
246
|
+
* its height.
|
|
247
|
+
*
|
|
248
|
+
* The centring travels WITH the measure because the two are one decision — a
|
|
249
|
+
* state that fills the screen holds a paragraph of content — and half of this
|
|
250
|
+
* recipe is what produced the defect in the first place.
|
|
251
|
+
*/
|
|
252
|
+
exports.screen = {
|
|
253
|
+
minHeight: '100dvh',
|
|
254
|
+
alignItems: 'center',
|
|
255
|
+
justifyContent: 'center',
|
|
256
|
+
};
|
package/dist/glass.css
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* Glass — the material floating chrome is made of, and the ladder it stands on.
|
|
2
|
+
*
|
|
3
|
+
* import '@hanzo/ui/glass.css' // this slice alone
|
|
4
|
+
* import '@hanzo/ui/theme.css' // the whole sheet; this is inlined in it
|
|
5
|
+
*
|
|
6
|
+
* Menus, dialogs, popovers, toasts, sticky bars: things with page UNDER them.
|
|
7
|
+
* That last clause is the whole rule. Vibrancy is a lens — translucent ground
|
|
8
|
+
* plus a backdrop blur — so it shows a softened version of whatever it covers,
|
|
9
|
+
* and covering nothing but the flat canvas it has nothing to soften. An in-flow
|
|
10
|
+
* card wearing this is not glass; it is the page with extra steps. So: over
|
|
11
|
+
* content -> glass, in the flow -> the surface ladder. There is no third case.
|
|
12
|
+
*
|
|
13
|
+
* ── Values come from @hanzo/design ──────────────────────────────────────────
|
|
14
|
+
* Every colour, shadow and light below is a design token. The fallbacks are
|
|
15
|
+
* design's OWN published values, present so a host that imports this file
|
|
16
|
+
* without design's sheet still gets a ladder instead of a silently-dropped
|
|
17
|
+
* declaration (an undefined var() invalidates the whole property). They are a
|
|
18
|
+
* mirror, not a second opinion — glass.test.ts reads @hanzo/design/styles.css
|
|
19
|
+
* and fails if any of them stops matching, so the copy cannot drift.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/* ── The material ───────────────────────────────────────────────────────────
|
|
23
|
+
Applied BY SLOT, not by call site. Every surface below is floating chrome by
|
|
24
|
+
construction — a popover is over the page or it is nothing — so which of them
|
|
25
|
+
is glass was never a decision a call site should get to make. It was making
|
|
26
|
+
it anyway: in the console, of 135 floating surfaces exactly ONE had reached
|
|
27
|
+
for the material by hand and a dozen dialogs had gone the other way and
|
|
28
|
+
pinned an opaque fill. Reaching them one by one is 134 edits that agree today
|
|
29
|
+
and drift apart on the next one somebody adds. A new dialog is glass because
|
|
30
|
+
it is a dialog.
|
|
31
|
+
|
|
32
|
+
`.glass` is here for the same reason `glass()` exists in the recipes module:
|
|
33
|
+
a hand-rolled bar or a custom menu is floating chrome that no slot names.
|
|
34
|
+
|
|
35
|
+
The ground is derived from --background rather than fixed, so the material
|
|
36
|
+
follows the theme instead of assuming a dark one.
|
|
37
|
+
|
|
38
|
+
The opaque `background-color` outside the @supports block is load-bearing
|
|
39
|
+
rather than belt-and-braces: a browser that cannot blur gets no rule at all
|
|
40
|
+
from inside it, and a menu with no background is a menu you read the page
|
|
41
|
+
through. !important beats the compiled atomic background class, which lands
|
|
42
|
+
in an inline <style> a bundler orders after this sheet, and it beats the call
|
|
43
|
+
sites that pin a fill by hand — those become no-ops instead of holes. */
|
|
44
|
+
@supports (backdrop-filter: blur(8px)) {
|
|
45
|
+
.glass,
|
|
46
|
+
[data-slot="glass"],
|
|
47
|
+
[data-slot="dialog-content"],
|
|
48
|
+
[data-slot="popover-content"],
|
|
49
|
+
[data-slot="select-content"],
|
|
50
|
+
[data-slot="dropdown-menu-content"],
|
|
51
|
+
[data-slot="dropdown-menu-sub-content"],
|
|
52
|
+
[data-slot="tooltip-content"] {
|
|
53
|
+
background-color: color-mix(in oklab, var(--background) 72%, transparent) !important;
|
|
54
|
+
-webkit-backdrop-filter: blur(20px) saturate(1.8);
|
|
55
|
+
backdrop-filter: blur(20px) saturate(1.8);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ── Depth — three rungs, and the lit edge is the load-bearing half ──────────
|
|
60
|
+
A cast shadow is how a light page shows lift. On a near-black canvas it is
|
|
61
|
+
nearly free of information: black on near-black moves no pixels, so a raised
|
|
62
|
+
panel wearing only a shadow reads exactly as flat as one wearing none. What
|
|
63
|
+
says "this surface is above that one" in a dark UI is the LIT EDGE — the top
|
|
64
|
+
lip catching light, which is why every macOS/iOS glass panel has one. Design
|
|
65
|
+
spends the same two tokens for the same reason and zeroes --edge-highlight in
|
|
66
|
+
`.light`, where a white line on a white card is nothing and the drop does all
|
|
67
|
+
the work: one composition, both themes, no branch here.
|
|
68
|
+
|
|
69
|
+
Levels are a LADDER, not a palette — 1 rests in the flow, 2 lifts off it, 3
|
|
70
|
+
floats over the page. Nothing needs a fourth; if something seems to, it is
|
|
71
|
+
sitting at the wrong level.
|
|
72
|
+
|
|
73
|
+
Anchored chrome — a menu pinned to the control that opened it, a tooltip to
|
|
74
|
+
its target — is rung 2. A modal has no anchor and a scrim of its own, so it
|
|
75
|
+
floats free at 3. Those slots share the rung's rule rather than restating its
|
|
76
|
+
value, so the ladder cannot come apart from the surfaces standing on it.
|
|
77
|
+
|
|
78
|
+
Glass over glass (a select opened inside a dialog) is deliberately left to
|
|
79
|
+
stack: two 72% grounds land near 92%, so the nested surface reads as MORE
|
|
80
|
+
solid than its parent, which is the right answer and the one macOS gives. */
|
|
81
|
+
.elevation-1 {
|
|
82
|
+
box-shadow:
|
|
83
|
+
var(--edge-highlight, inset 0 1px 0 0 rgb(255 255 255 / .10)),
|
|
84
|
+
var(--shadow-sm, 0 1px 2px 0 rgb(0 0 0 / .40)) !important;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.elevation-2,
|
|
88
|
+
[data-slot="popover-content"],
|
|
89
|
+
[data-slot="select-content"],
|
|
90
|
+
[data-slot="dropdown-menu-content"],
|
|
91
|
+
[data-slot="dropdown-menu-sub-content"],
|
|
92
|
+
[data-slot="tooltip-content"] {
|
|
93
|
+
box-shadow:
|
|
94
|
+
var(--edge-highlight, inset 0 1px 0 0 rgb(255 255 255 / .10)),
|
|
95
|
+
var(--shadow-lg, 0 10px 15px -3px rgb(0 0 0 / .55), 0 4px 6px -4px rgb(0 0 0 / .55)) !important;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.elevation-3,
|
|
99
|
+
[data-slot="dialog-content"] {
|
|
100
|
+
box-shadow:
|
|
101
|
+
var(--edge-highlight, inset 0 1px 0 0 rgb(255 255 255 / .10)),
|
|
102
|
+
var(--shadow-floating, 0 24px 60px -16px rgb(0 0 0 / .75)) !important;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ── The scrim ──────────────────────────────────────────────────────────────
|
|
106
|
+
The dim that makes a floating panel read as floating rather than as more
|
|
107
|
+
page. gui's overlay dims TWICE — a translucent ground under a further
|
|
108
|
+
`opacity` — landing at about a quarter black. Opaque chrome gets away with
|
|
109
|
+
it; glass does not, because the blur behind a translucent panel only reads as
|
|
110
|
+
depth when the ground behind it is genuinely darker.
|
|
111
|
+
|
|
112
|
+
One value, in one place: the recipes module reads the same token, so the dim
|
|
113
|
+
behind a component dialog and the dim behind a hand-rolled one are the same
|
|
114
|
+
dim by construction rather than by two people remembering a number. */
|
|
115
|
+
[data-slot="dialog-overlay"] {
|
|
116
|
+
opacity: 1;
|
|
117
|
+
background-color: var(--surface-scrim, rgb(0 0 0 / .8));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ── Grouped rows ───────────────────────────────────────────────────────────
|
|
121
|
+
One card, hairlines between its rows — the macOS settings group. Four
|
|
122
|
+
separate cards for four toggles reads as four unrelated decisions, each with
|
|
123
|
+
its own four edges; the same four inside one card reads as one set and drops
|
|
124
|
+
twelve edges of noise.
|
|
125
|
+
|
|
126
|
+
The separator is drawn by the PARENT, because only the parent knows which
|
|
127
|
+
child is last. Every by-hand version either left a trailing hairline sitting
|
|
128
|
+
against the card's own bottom border (two parallel lines, 1px apart) or made
|
|
129
|
+
each row take a `first` flag it should never have needed. `> * + *` has
|
|
130
|
+
neither failure mode and needs nothing from the call site.
|
|
131
|
+
|
|
132
|
+
Pair with `overflow: hidden` on the card (the `rows` recipe does) so the
|
|
133
|
+
separators stop at the radius instead of poking through the rounded corner.
|
|
134
|
+
|
|
135
|
+
--borderColor first: that is the variable the gui theme scope re-bases, and
|
|
136
|
+
it is what a card's own edge is drawn with. Falling through to --border keeps
|
|
137
|
+
a host that has design's tokens but no gui scope from losing the line. */
|
|
138
|
+
[data-slot="rows"] > * + * {
|
|
139
|
+
border-top: 1px solid var(--borderColor, var(--border));
|
|
140
|
+
}
|