@glasshome/ui 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SPEC.md +143 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -9
- package/dist/lib/alert-tones.d.ts +3 -3
- package/dist/lib/input-classes.d.ts +3 -2
- package/dist/solid/alert-dialog.d.ts +7 -1
- package/dist/solid/index.d.ts +2 -1
- package/dist/solid/index.js +4095 -3616
- package/dist/solid/list-reorder.d.ts +38 -0
- package/dist/solid/schema-form.d.ts +36 -0
- package/dist/solid/section-card.d.ts +1 -0
- package/dist/solid/slider.d.ts +11 -0
- package/dist/tokens/index.js +3 -3
- package/dist/{utils-BLMzDBJk.js → utils-C2Qf83PT.js} +144 -144
- package/package.json +6 -4
- package/scripts/check-structural-classes.ts +43 -0
- package/src/lib/alert-tones.ts +5 -4
- package/src/lib/button-variants.ts +3 -0
- package/src/lib/input-classes.ts +18 -3
- package/src/styles/globals.css +51 -2
- package/src/styles/theme.css +28 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glasshome/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "SolidJS component library for GlassHome, built on Kobalte",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/glasshome/ui#readme",
|
|
@@ -53,14 +53,16 @@
|
|
|
53
53
|
"src/astro",
|
|
54
54
|
"src/lib",
|
|
55
55
|
"src/styles",
|
|
56
|
-
"scripts"
|
|
56
|
+
"scripts",
|
|
57
|
+
"SPEC.md"
|
|
57
58
|
],
|
|
58
59
|
"scripts": {
|
|
59
|
-
"build": "vite build && tsc",
|
|
60
|
+
"build": "rm -rf dist && vite build && tsc",
|
|
61
|
+
"check:structural": "bun scripts/check-structural-classes.ts",
|
|
60
62
|
"check:tokens": "bun scripts/check-tokens.ts",
|
|
61
63
|
"check:types": "tsc --noEmit -p tsconfig.test.json",
|
|
62
64
|
"check:publish": "publint && attw --pack . --profile esm-only --exclude-entrypoints ./styles ./styles/theme",
|
|
63
|
-
"dev": "vite build --watch",
|
|
65
|
+
"dev": "tsc -b --force && vite build --watch",
|
|
64
66
|
"lint": "biome check .",
|
|
65
67
|
"test": "vitest run",
|
|
66
68
|
"test:watch": "vitest",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Gate B3 (sdk-2.md): a package class string may not depend on the consumer's
|
|
2
|
+
// Tailwind scanning package source. Arbitrary variants like `[&>svg]:` are the
|
|
3
|
+
// tell: unlike plain utilities, no consumer's own code coincidentally
|
|
4
|
+
// generates them, so they silently vanish for any consumer whose build never
|
|
5
|
+
// scans this package (the stacked-carousel break). Structural child styling
|
|
6
|
+
// ships as real CSS in styles/globals.css instead (see .carousel-stack,
|
|
7
|
+
// .gh-alert-*). Escape hatch: a `structural-ok: <reason>` comment on or
|
|
8
|
+
// directly above the offending line.
|
|
9
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
const LIB_DIR = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../src/lib");
|
|
13
|
+
const ARBITRARY_VARIANT = /\[&[^\]]*\]:/;
|
|
14
|
+
const COMMENT_LINE = /^\s*(\/\/|\/\*|\*)/;
|
|
15
|
+
|
|
16
|
+
let findings = 0;
|
|
17
|
+
|
|
18
|
+
for (const entry of readdirSync(LIB_DIR).sort()) {
|
|
19
|
+
if (!entry.endsWith(".ts") || entry.endsWith(".test.ts")) continue;
|
|
20
|
+
const file = path.join(LIB_DIR, entry);
|
|
21
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
22
|
+
lines.forEach((text, i) => {
|
|
23
|
+
if (COMMENT_LINE.test(text)) return;
|
|
24
|
+
if (!ARBITRARY_VARIANT.test(text)) return;
|
|
25
|
+
if (text.includes("structural-ok:")) return;
|
|
26
|
+
// Walk the comment block directly above for the escape marker.
|
|
27
|
+
for (let j = i - 1; j >= 0 && COMMENT_LINE.test(lines[j] ?? ""); j--) {
|
|
28
|
+
if (lines[j]?.includes("structural-ok:")) return;
|
|
29
|
+
}
|
|
30
|
+
findings++;
|
|
31
|
+
console.error(
|
|
32
|
+
`src/lib/${entry}:${i + 1} consumer-generated structural class (arbitrary variant): ${text.trim().slice(0, 120)}`,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (findings > 0) {
|
|
38
|
+
console.error(
|
|
39
|
+
`\n${findings} arbitrary variant(s) in lib class modules. Move the styling to real CSS in src/styles/globals.css (see .carousel-stack) or annotate with a \`structural-ok: <reason>\` comment.`,
|
|
40
|
+
);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
console.log("check-structural-classes: lib class strings are consumer-independent.");
|
package/src/lib/alert-tones.ts
CHANGED
|
@@ -47,10 +47,12 @@ export const alertIconFill = (c: string) => `color-mix(in srgb, ${c} 16%, transp
|
|
|
47
47
|
|
|
48
48
|
export const ALERT_CLASS =
|
|
49
49
|
"relative overflow-hidden flex items-start gap-3 rounded-lg border p-3 backdrop-blur-sm";
|
|
50
|
+
/* gh-alert-* svg chrome lives in globals.css as real CSS: an arbitrary variant
|
|
51
|
+
* here only works if the consumer's Tailwind scans package source. */
|
|
50
52
|
export const ALERT_ICON_CLASS =
|
|
51
|
-
"flex size-8 shrink-0 items-center justify-center rounded-md
|
|
53
|
+
"gh-alert-icon flex size-8 shrink-0 items-center justify-center rounded-md";
|
|
52
54
|
export const ALERT_ICON_BG_CLASS =
|
|
53
|
-
"pointer-events-none absolute -right-3 -bottom-4
|
|
55
|
+
"gh-alert-icon-bg pointer-events-none absolute -right-3 -bottom-4";
|
|
54
56
|
export function alertIconBgStyle(color: string): Record<string, string> {
|
|
55
57
|
return {
|
|
56
58
|
color,
|
|
@@ -61,5 +63,4 @@ export function alertIconBgStyle(color: string): Record<string, string> {
|
|
|
61
63
|
}
|
|
62
64
|
export const ALERT_CONTENT_CLASS = "relative z-10 min-w-0 flex-1";
|
|
63
65
|
export const ALERT_TITLE_CLASS = "font-semibold text-base leading-snug";
|
|
64
|
-
export const ALERT_DESCRIPTION_CLASS =
|
|
65
|
-
"text-foreground/80 text-sm leading-snug [&:not(:first-child)]:mt-0.5";
|
|
66
|
+
export const ALERT_DESCRIPTION_CLASS = "gh-alert-desc text-foreground/80 text-sm leading-snug";
|
|
@@ -4,6 +4,9 @@ import { cva } from "cva";
|
|
|
4
4
|
* muddy over dark heroes/sections. Every class is literal for Tailwind's
|
|
5
5
|
* scanner. `size: none` is sizeless for callers that own height/padding. */
|
|
6
6
|
export const buttonVariants = cva({
|
|
7
|
+
// structural-ok: the [&_svg]: utilities stay in Tailwind's utility layer so a
|
|
8
|
+
// caller's own [&_svg]:size-* override can still win; unlayered real CSS in
|
|
9
|
+
// globals.css would beat every consumer utility regardless of specificity.
|
|
7
10
|
base: "inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-full font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
8
11
|
variants: {
|
|
9
12
|
variant: {
|
package/src/lib/input-classes.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
|
-
/* Recessed (concave) glass field
|
|
2
|
-
*
|
|
3
|
-
|
|
1
|
+
/* Recessed (concave) glass field: text inputs, textareas, and the pickers that
|
|
2
|
+
* wear a field (Select trigger + listbox, EntitySelector, AreaPicker). The fill
|
|
3
|
+
* and edge come from the theme's --field/--field-edge pair rather than --input
|
|
4
|
+
* directly, because the two themes need different material: on a dark ground a
|
|
5
|
+
* dug-out fill reads as depth, on a light one the same drop reads as `disabled`,
|
|
6
|
+
* so light fields sit at the card and carry a solid border instead. Both are
|
|
7
|
+
* opaque — a translucent field shows whatever sits behind it (worst inside the
|
|
8
|
+
* overlapping Select listbox). The recess itself is .glass-sink's rim, which is
|
|
9
|
+
* theme independent. */
|
|
10
|
+
export const INPUT_SURFACE =
|
|
11
|
+
"glass glass-sink [--glass-base:var(--field)] [--glass-edge:var(--field-edge)] [--glass-light:0.04]";
|
|
12
|
+
|
|
13
|
+
/* Toggle-family chrome and rails: checkbox box, radio ring, switch track, slider
|
|
14
|
+
* rail, chart wells. Same concave glass, but keyed to --input in both themes:
|
|
15
|
+
* these are not fields you type into, they are unfilled controls whose whole job
|
|
16
|
+
* is to read as an empty well, so the fill must stay visibly below the card even
|
|
17
|
+
* in the light theme. */
|
|
18
|
+
export const FIELD_CHROME = "glass glass-sink [--glass-base:var(--input)] [--glass-light:0.04]";
|
|
4
19
|
|
|
5
20
|
export const INPUT_CLASS = `flex h-9 w-full min-w-0 rounded-md ${INPUT_SURFACE} px-3 py-1 text-base outline-none transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40`;
|
package/src/styles/globals.css
CHANGED
|
@@ -109,6 +109,13 @@
|
|
|
109
109
|
* children can read it; a nested .glass re-declares it from its own knob,
|
|
110
110
|
* which resets the chain and keeps the no-leak guarantee. */
|
|
111
111
|
--surface-tone: var(--glass-tone);
|
|
112
|
+
/* Second tint axis. Ordinary (inheriting) properties, so they are re-declared
|
|
113
|
+
* on every .glass to reset the chain at each glass boundary (same no-leak
|
|
114
|
+
* trick as --surface-tone). Defaults reproduce the single-tone wash exactly;
|
|
115
|
+
* override tone-2/wash-2/angle together for a two-tone tint (slider fills). */
|
|
116
|
+
--glass-tone-2: var(--glass-tone);
|
|
117
|
+
--glass-wash-2: calc(var(--glass-wash) / 3);
|
|
118
|
+
--glass-wash-angle: 135deg;
|
|
112
119
|
border: 1px solid var(--glass-edge);
|
|
113
120
|
/* Base is a gradient layer (not background-color) so the frost slice can sit
|
|
114
121
|
* BELOW it — background-color always paints under every image layer, which
|
|
@@ -118,9 +125,9 @@
|
|
|
118
125
|
radial-gradient(120% 120% at 22% 8%, oklch(1 0 0 / var(--glass-light)), transparent 58%),
|
|
119
126
|
radial-gradient(120% 120% at 82% 100%, oklch(0 0 0 / var(--glass-shade)), transparent 58%),
|
|
120
127
|
linear-gradient(
|
|
121
|
-
|
|
128
|
+
var(--glass-wash-angle),
|
|
122
129
|
color-mix(in srgb, var(--glass-tone) var(--glass-wash), transparent),
|
|
123
|
-
color-mix(in srgb, var(--glass-tone)
|
|
130
|
+
color-mix(in srgb, var(--glass-tone-2) var(--glass-wash-2), transparent)
|
|
124
131
|
),
|
|
125
132
|
linear-gradient(var(--glass-base), var(--glass-base)), var(--glass-frost, none);
|
|
126
133
|
box-shadow:
|
|
@@ -130,6 +137,32 @@
|
|
|
130
137
|
0 10px 30px -16px oklch(0 0 0 / var(--glass-lift));
|
|
131
138
|
}
|
|
132
139
|
|
|
140
|
+
/* Two-tone edge: border-color cannot gradient, so the border goes transparent
|
|
141
|
+
* and a masked 1px ring runs tone-1→tone-2 at the .glass-tint edge alpha.
|
|
142
|
+
* Must stay unlayered (the formula's border is unlayered and beats any @layer)
|
|
143
|
+
* and must read the inheriting mirrors, not the inherits:false knobs, which
|
|
144
|
+
* resolve to initial on a pseudo. Element must be positioned. */
|
|
145
|
+
:where(.glass-edge-gradient) {
|
|
146
|
+
border-color: transparent;
|
|
147
|
+
}
|
|
148
|
+
:where(.glass-edge-gradient)::before {
|
|
149
|
+
content: "";
|
|
150
|
+
position: absolute;
|
|
151
|
+
inset: 0;
|
|
152
|
+
border-radius: inherit;
|
|
153
|
+
padding: 1px;
|
|
154
|
+
background: linear-gradient(
|
|
155
|
+
var(--glass-wash-angle),
|
|
156
|
+
oklch(from var(--surface-tone) l c h / 0.45),
|
|
157
|
+
oklch(from var(--glass-tone-2) l c h / 0.45)
|
|
158
|
+
);
|
|
159
|
+
mask:
|
|
160
|
+
linear-gradient(#fff 0 0) content-box,
|
|
161
|
+
linear-gradient(#fff 0 0);
|
|
162
|
+
mask-composite: exclude;
|
|
163
|
+
pointer-events: none;
|
|
164
|
+
}
|
|
165
|
+
|
|
133
166
|
/* PageHeader banner surface: bespoke three-stop card gradient (its material
|
|
134
167
|
* predates the one-glass formula) over the same frost slot .glass uses, so
|
|
135
168
|
* hosts wire the performant-blur engine identically on both. Frost defaults
|
|
@@ -171,6 +204,22 @@
|
|
|
171
204
|
}
|
|
172
205
|
}
|
|
173
206
|
|
|
207
|
+
/* Alert/toast icon chrome + description spacing: structural (same rationale as
|
|
208
|
+
* .carousel-stack below), so real CSS instead of [&>svg]: arbitrary variants.
|
|
209
|
+
* Values mirror the former utilities: size-[18px], size-28, mt-0.5. */
|
|
210
|
+
.gh-alert-icon > svg {
|
|
211
|
+
width: 18px;
|
|
212
|
+
height: 18px;
|
|
213
|
+
}
|
|
214
|
+
.gh-alert-icon-bg > svg {
|
|
215
|
+
width: 7rem;
|
|
216
|
+
height: 7rem;
|
|
217
|
+
stroke-width: 2.5px;
|
|
218
|
+
}
|
|
219
|
+
.gh-alert-desc:not(:first-child) {
|
|
220
|
+
margin-top: 0.125rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
174
223
|
/* Stacked carousel modes (fade, wipe) put every slide in one grid cell. This is
|
|
175
224
|
* structural, so it ships as real CSS: a Tailwind arbitrary variant written in
|
|
176
225
|
* package source never reaches a consumer, because Tailwind skips node_modules
|
package/src/styles/theme.css
CHANGED
|
@@ -126,15 +126,31 @@
|
|
|
126
126
|
--muted-foreground: oklch(0.34 0 0);
|
|
127
127
|
--accent: oklch(0.6 0.2 195);
|
|
128
128
|
--accent-foreground: oklch(1 0 0);
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
/* Semantic roles are one value per role, used as fill AND as text, so on this
|
|
130
|
+
* L=0.975 ground they have to clear 4.5:1 (3:1 for --ring, a focus indicator)
|
|
131
|
+
* against --background and --card. The dark theme's high-lightness versions
|
|
132
|
+
* measure 2.1-3.6:1 here, so the light theme drops lightness and holds hue;
|
|
133
|
+
* chroma comes down only as far as staying near the sRGB gamut edge needs.
|
|
134
|
+
* Measured vs --background / --card / white-on-fill:
|
|
135
|
+
* success 5.24 / 5.56 / 5.63, warning 5.17 / 5.49 / 5.55,
|
|
136
|
+
* destructive 5.21 / 5.53 / 5.59, ring 4.68 / 4.97 / 5.03. */
|
|
137
|
+
--success: oklch(0.5 0.15 145);
|
|
138
|
+
--warning: oklch(0.52 0.13 85);
|
|
131
139
|
--primary-tint-foreground: oklch(0.42 0.2 215.221);
|
|
132
140
|
--accent-tint-foreground: oklch(0.46 0.16 195);
|
|
133
|
-
--destructive: oklch(0.
|
|
141
|
+
--destructive: oklch(0.54 0.19 23.0704);
|
|
134
142
|
--destructive-foreground: oklch(1 0 0);
|
|
135
143
|
--border: oklch(0.9 0.01 250);
|
|
136
144
|
--input: oklch(0.9 0.012 250);
|
|
137
|
-
--ring: oklch(0.
|
|
145
|
+
--ring: oklch(0.53 0.18 195);
|
|
146
|
+
/* Field material (INPUT_SURFACE). The recess is the .glass-sink rim, not a
|
|
147
|
+
* darker fill: on a card of L=0.995 an --input fill of L=0.9 is a nine-point
|
|
148
|
+
* drop, the same cue this library spends on `disabled`, so a form of light
|
|
149
|
+
* fields reads switched off. Light fields sit AT the card and get a solid
|
|
150
|
+
* --border edge instead — the boundary still measures 1.33:1 against the
|
|
151
|
+
* card, exactly what the fill drop used to carry, at 1px instead of a slab. */
|
|
152
|
+
--field: var(--card);
|
|
153
|
+
--field-edge: var(--border);
|
|
138
154
|
--chart-1: oklch(0.7459 0.1483 156.4499);
|
|
139
155
|
--chart-2: oklch(0.48 0.2 215.221);
|
|
140
156
|
--chart-3: oklch(0.7336 0.1758 50.5517);
|
|
@@ -192,11 +208,16 @@
|
|
|
192
208
|
--destructive: oklch(0.7106 0.1661 22.2162);
|
|
193
209
|
--destructive-foreground: oklch(1 0 0);
|
|
194
210
|
--border: oklch(0.26 0.012 250);
|
|
195
|
-
/*
|
|
196
|
-
* so
|
|
197
|
-
*
|
|
211
|
+
/* On a dark ground a dug-out surface catches more light than the wall around
|
|
212
|
+
* it, so the field sits a touch ABOVE the card (0.17) rather than below, and
|
|
213
|
+
* the .glass-sink rim does the rest. A hair of the card's cool tint (chroma
|
|
214
|
+
* 0.01) keeps it from going flat gray. Same rail color as the slider track. */
|
|
198
215
|
--input: oklch(0.19 0.01 250);
|
|
199
216
|
--ring: oklch(0.6 0.2 195);
|
|
217
|
+
/* Dark fields keep the dug-out fill (it reads as depth here, not as disabled)
|
|
218
|
+
* and the glass formula's own soft edge. See the light block. */
|
|
219
|
+
--field: var(--input);
|
|
220
|
+
--field-edge: color-mix(in srgb, var(--border) 60%, transparent);
|
|
200
221
|
--chart-1: oklch(0.8003 0.1821 151.711);
|
|
201
222
|
--chart-2: oklch(0.48 0.2 215.221);
|
|
202
223
|
--chart-3: oklch(0.8077 0.1035 19.5706);
|