@glasshome/ui 0.5.4 → 1.0.1

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.
Files changed (87) hide show
  1. package/README.md +59 -25
  2. package/dist/index.d.ts +10 -3
  3. package/dist/index.js +9 -9
  4. package/dist/lib/alert-tones.d.ts +18 -0
  5. package/dist/lib/badge-tone.d.ts +2 -0
  6. package/dist/lib/button-variants.d.ts +6 -16
  7. package/dist/lib/card-classes.d.ts +19 -0
  8. package/dist/lib/glass-tone.d.ts +2 -0
  9. package/dist/lib/input-classes.d.ts +2 -0
  10. package/dist/lib/logo-lockup.d.ts +38 -0
  11. package/dist/lib/overlay-classes.d.ts +2 -0
  12. package/dist/lib/section-tokens.d.ts +4 -0
  13. package/dist/lib/tier-badge.d.ts +13 -0
  14. package/dist/solid/alert-dialog.d.ts +1 -3
  15. package/dist/solid/alert.d.ts +2 -11
  16. package/dist/solid/badge.d.ts +11 -46
  17. package/dist/solid/bottom-sheet/bottom-sheet.d.ts +1 -1
  18. package/dist/solid/bottom-sheet/constants.d.ts +0 -1
  19. package/dist/solid/bottom-sheet/drag-controller.d.ts +1 -1
  20. package/dist/solid/bottom-sheet/index.d.ts +1 -2
  21. package/dist/solid/breadcrumb.d.ts +1 -1
  22. package/dist/solid/button-group.d.ts +2 -2
  23. package/dist/solid/button.d.ts +1 -1
  24. package/dist/solid/card.d.ts +10 -2
  25. package/dist/solid/carousel.d.ts +1 -1
  26. package/dist/solid/charts.d.ts +45 -0
  27. package/dist/solid/count-pill.d.ts +10 -0
  28. package/dist/solid/data-table.d.ts +66 -0
  29. package/dist/solid/dock.d.ts +3 -0
  30. package/dist/solid/entity-data.d.ts +52 -0
  31. package/dist/solid/field.d.ts +1 -1
  32. package/dist/solid/form.d.ts +1 -1
  33. package/dist/solid/index.d.ts +74 -64
  34. package/dist/solid/index.js +11050 -10848
  35. package/dist/solid/input-group.d.ts +1 -1
  36. package/dist/solid/item.d.ts +3 -2
  37. package/dist/solid/logo.d.ts +23 -0
  38. package/dist/solid/number-field.d.ts +3 -0
  39. package/dist/solid/overlay.d.ts +3 -0
  40. package/dist/solid/page-header.d.ts +18 -0
  41. package/dist/solid/popover.d.ts +7 -1
  42. package/dist/solid/scope-indicator.d.ts +8 -0
  43. package/dist/solid/section-card.d.ts +54 -0
  44. package/dist/solid/select.d.ts +1 -18
  45. package/dist/solid/settings-row.d.ts +34 -0
  46. package/dist/solid/sliding-indicator.d.ts +32 -0
  47. package/dist/solid/sonner.d.ts +35 -4
  48. package/dist/solid/spinner.d.ts +2 -1
  49. package/dist/solid/switch.d.ts +1 -1
  50. package/dist/solid/toggle-group.d.ts +1 -1
  51. package/dist/solid/widget-card.d.ts +29 -0
  52. package/dist/solid/widget-identity.d.ts +47 -0
  53. package/dist/solid/widget-trust-badge.d.ts +16 -0
  54. package/dist/tokens/index.d.ts +3 -3
  55. package/dist/tokens/index.js +11 -11
  56. package/dist/tokens/presets.d.ts +1 -1
  57. package/dist/{utils-JJqwPy_d.js → utils-gbdcvo14.js} +598 -506
  58. package/package.json +39 -17
  59. package/scripts/check-tokens.ts +60 -0
  60. package/scripts/check-ui-drift.mjs +116 -0
  61. package/src/astro/Alert.astro +63 -0
  62. package/src/astro/Badge.astro +16 -0
  63. package/src/astro/Button.astro +43 -0
  64. package/src/astro/Card.astro +39 -0
  65. package/src/astro/Logo.astro +69 -0
  66. package/src/astro/TierBadge.astro +33 -0
  67. package/src/lib/alert-tones.ts +65 -0
  68. package/src/lib/badge-tone.ts +4 -0
  69. package/src/lib/button-variants.ts +44 -0
  70. package/src/lib/card-classes.ts +37 -0
  71. package/src/lib/glass-tone.ts +23 -0
  72. package/src/lib/input-classes.ts +5 -0
  73. package/src/lib/logo-lockup.ts +49 -0
  74. package/src/lib/overlay-classes.ts +7 -0
  75. package/src/lib/section-tokens.ts +8 -0
  76. package/src/lib/tier-badge.ts +31 -0
  77. package/src/lib/use-is-mobile.ts +17 -0
  78. package/src/lib/utils.ts +6 -0
  79. package/src/styles/globals.css +157 -0
  80. package/src/styles/theme.css +68 -12
  81. package/dist/solid/calendar.d.ts +0 -17
  82. package/dist/solid/command.d.ts +0 -24
  83. package/dist/solid/drawer.d.ts +0 -22
  84. package/dist/solid/glass-effect.d.ts +0 -6
  85. package/dist/solid/menubar.d.ts +0 -25
  86. package/dist/solid/navigation-menu.d.ts +0 -19
  87. package/dist/solid/sidebar.d.ts +0 -108
package/package.json CHANGED
@@ -1,10 +1,8 @@
1
1
  {
2
2
  "name": "@glasshome/ui",
3
- "version": "0.5.4",
3
+ "version": "1.0.1",
4
4
  "description": "SolidJS component library for GlassHome, built on Kobalte",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
6
  "homepage": "https://github.com/glasshome/ui#readme",
9
7
  "bugs": {
10
8
  "url": "https://github.com/glasshome/ui/issues"
@@ -13,9 +11,11 @@
13
11
  "access": "public"
14
12
  },
15
13
  "engines": {
16
- "node": ">=18"
14
+ "node": ">=20"
17
15
  },
18
- "sideEffects": false,
16
+ "sideEffects": [
17
+ "**/*.css"
18
+ ],
19
19
  "keywords": [
20
20
  "solidjs",
21
21
  "components",
@@ -42,23 +42,35 @@
42
42
  "types": "./dist/tokens/index.d.ts",
43
43
  "import": "./dist/tokens/index.js"
44
44
  },
45
+ "./astro/*": "./src/astro/*",
45
46
  "./styles": "./src/styles/globals.css",
46
47
  "./package.json": "./package.json",
47
- "./styles/theme": "./src/styles/theme.css"
48
+ "./styles/theme": "./src/styles/theme.css",
49
+ "./scripts/*": "./scripts/*"
48
50
  },
49
51
  "files": [
50
52
  "dist",
51
- "src/styles"
53
+ "src/astro",
54
+ "src/lib",
55
+ "src/styles",
56
+ "scripts"
52
57
  ],
53
58
  "scripts": {
54
59
  "build": "vite build && tsc",
55
60
  "check:tokens": "bun scripts/check-tokens.ts",
61
+ "check:types": "tsc --noEmit -p tsconfig.test.json",
62
+ "check:publish": "publint && attw --pack . --profile esm-only --exclude-entrypoints ./styles ./styles/theme",
56
63
  "dev": "vite build --watch",
57
- "lint": "biome check ."
64
+ "lint": "biome check .",
65
+ "test": "vitest run",
66
+ "test:watch": "vitest",
67
+ "dev:gallery": "vite --config vite.dev.config.ts"
58
68
  },
59
69
  "peerDependencies": {
60
- "@glasshome/sync-layer": ">=0.1.0",
61
- "solid-js": "^1.9.9"
70
+ "astro": ">=4",
71
+ "solid-js": "^1.9.9",
72
+ "tailwindcss": "^4.1.13",
73
+ "tw-animate-css": "^1.4.0"
62
74
  },
63
75
  "dependencies": {
64
76
  "@iconify-icon/solid": "^3.0.3",
@@ -66,24 +78,34 @@
66
78
  "clsx": "^2.1.1",
67
79
  "cva": "^1.0.0-beta.4",
68
80
  "embla-carousel": "^8.5.1",
69
- "lucide-solid": "^1.21.0",
70
81
  "solid-sonner": "^0.3.1",
71
- "tailwind-merge": "^3.4.0",
72
- "tailwindcss": "^4.1.13",
73
- "tw-animate-css": "^1.4.0"
82
+ "tailwind-merge": "^3.4.0"
74
83
  },
75
84
  "devDependencies": {
85
+ "@arethetypeswrong/cli": "^0.18.5",
86
+ "@biomejs/biome": "2.4.15",
87
+ "@solidjs/testing-library": "^0.8.10",
88
+ "@tailwindcss/vite": "^4.1.13",
76
89
  "@types/json-schema": "^7.0.15",
77
90
  "@types/node": "^25.2.2",
91
+ "happy-dom": "^20.11.0",
92
+ "publint": "^0.3.21",
93
+ "tailwindcss": "^4.1.13",
94
+ "tw-animate-css": "^1.4.0",
78
95
  "typescript": "^5",
79
96
  "vite": "^8.1.0",
80
97
  "vite-plugin-solid": "^2.11.12",
81
- "@biomejs/biome": "2.4.15"
98
+ "vitest": "^4.1.10"
82
99
  },
83
100
  "repository": {
84
101
  "type": "git",
85
- "url": "https://github.com/glasshome/ui.git"
102
+ "url": "git+https://github.com/glasshome/ui.git"
86
103
  },
87
104
  "author": "GlassHome Labs",
88
- "license": "MIT"
105
+ "license": "MIT",
106
+ "peerDependenciesMeta": {
107
+ "astro": {
108
+ "optional": true
109
+ }
110
+ }
89
111
  }
@@ -0,0 +1,60 @@
1
+ // Fails when styles/theme.css and tokens/presets.ts (midnight-glass) drift.
2
+ import { readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { DEFAULT_THEME_ID, getPreset, type ThemeColors } from "../src/tokens";
5
+
6
+ const css = readFileSync(
7
+ path.resolve(path.dirname(new URL(import.meta.url).pathname), "../src/styles/theme.css"),
8
+ "utf8",
9
+ );
10
+
11
+ const KEYS: Record<string, keyof ThemeColors> = {
12
+ "--primary": "primary",
13
+ "--accent": "accent",
14
+ "--secondary": "secondary",
15
+ "--border": "border",
16
+ "--card": "card",
17
+ "--background": "background",
18
+ "--muted": "muted",
19
+ "--muted-foreground": "mutedForeground",
20
+ "--input": "input",
21
+ "--ring": "ring",
22
+ "--destructive": "destructive",
23
+ "--popover": "popover",
24
+ };
25
+
26
+ function cssVars(block: string): Record<string, string> {
27
+ const match = css.match(new RegExp(`${block}\\s*\\{([^}]*)\\}`));
28
+ if (!match) throw new Error(`block not found in theme.css: ${block}`);
29
+ const vars: Record<string, string> = {};
30
+ for (const line of match[1].split("\n")) {
31
+ const m = line.match(/^\s*(--[\w-]+):\s*(.+?);\s*$/);
32
+ if (m) vars[m[1]] = m[2];
33
+ }
34
+ return vars;
35
+ }
36
+
37
+ const preset = getPreset(DEFAULT_THEME_ID);
38
+ if (!preset) throw new Error(`preset missing: ${DEFAULT_THEME_ID}`);
39
+
40
+ let drift = 0;
41
+ for (const [mode, block] of [
42
+ ["light", ":root"],
43
+ ["dark", ".dark"],
44
+ ] as const) {
45
+ const vars = cssVars(block);
46
+ for (const [cssKey, tokenKey] of Object.entries(KEYS)) {
47
+ const cssValue = vars[cssKey];
48
+ const tokenValue = preset.colors[mode][tokenKey];
49
+ if (cssValue !== tokenValue) {
50
+ console.error(`${mode} ${cssKey}: css="${cssValue}" tokens="${tokenValue}"`);
51
+ drift++;
52
+ }
53
+ }
54
+ }
55
+
56
+ if (drift > 0) {
57
+ console.error(`${drift} token(s) drifted between theme.css and tokens/presets.ts`);
58
+ process.exit(1);
59
+ }
60
+ console.log("theme.css and tokens/presets.ts in sync");
@@ -0,0 +1,116 @@
1
+ // Deny-by-default UI drift guard, shared by hub and dash. Glass, panels,
2
+ // pills, and callouts must come from @glasshome/ui (see SPEC.md); this scans
3
+ // app markup for the raw idioms those primitives replace.
4
+ //
5
+ // Escape hatch: `ui-drift-ok <reason>` in a comment on the line or the line
6
+ // above. SKIP_UI_DRIFT_CHECK=1 skips entirely.
7
+ import { readdirSync, readFileSync, statSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ const RULES = [
11
+ [
12
+ "hand-rolled blur",
13
+ /(?:backdrop-blur(?:-\w+|-\[[^\]]+\])?|backdrop-filter)/,
14
+ "blur belongs to the material: use <Card>/CARD_BLUR, OVERLAY_SURFACE, SCRIM_CLASS, or TRACK_SURFACE",
15
+ ],
16
+ [
17
+ "hand-rolled surface fill",
18
+ /\b(?<!hover:)(?<!focus:)(?<!active:)(?<!group-hover:)bg-(?:card|background|popover)\/\d+/,
19
+ "translucent surface fills come from <Card>, <Overlay>, or a --glass-base knob",
20
+ ],
21
+ [
22
+ "bespoke pill",
23
+ /rounded-full[^"'`]*\bborder\b[^"'`]*bg-(?:card|background)\b/,
24
+ 'use <Badge tone> or <Button size="none">',
25
+ ],
26
+ [
27
+ "raw palette color",
28
+ /\b(?:bg|text|border)-(?:amber|green|yellow|red|blue|orange|emerald|rose|sky)-\d{3}\b/,
29
+ "use theme vars: var(--success)/var(--warning)/var(--destructive) via <Badge>/<Alert>",
30
+ ],
31
+ [
32
+ "tinted callout",
33
+ /border-(?:primary|destructive|warning|success)\/\d+[^"'`]*bg-(?:primary|destructive|warning|success)\/\d+/,
34
+ "use <Alert tone> (Solid or astro) or ALERT_TONES",
35
+ ],
36
+ [
37
+ "shadcn field chrome",
38
+ /border-input\s[^"'`]*bg-(?:background|transparent)\b/,
39
+ "use INPUT_CLASS / INPUT_SURFACE / FIELD_CHROME from @glasshome/ui",
40
+ ],
41
+ [
42
+ "solid badge",
43
+ /border-transparent\s+bg-primary\s+text-primary-foreground/,
44
+ "use <Badge tone> — there are no solid badge variants",
45
+ ],
46
+ ];
47
+
48
+ function walk(dir) {
49
+ const out = [];
50
+ for (const name of readdirSync(dir)) {
51
+ const p = join(dir, name);
52
+ const s = statSync(p);
53
+ if (s.isDirectory()) out.push(...walk(p));
54
+ else if (/\.(astro|tsx)$/.test(p)) out.push(p);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ export function checkUiDrift({ root = "src", allowFiles = [], allowPrefixes = [] } = {}) {
60
+ if (process.env.SKIP_UI_DRIFT_CHECK === "1") return 0;
61
+
62
+ const offenders = [];
63
+ for (const file of walk(root)) {
64
+ if (allowFiles.includes(file)) continue;
65
+ if (allowPrefixes.some((p) => file.startsWith(p))) continue;
66
+ const text = readFileSync(file, "utf8");
67
+ const lines = text.split("\n");
68
+ const flagged = new Set();
69
+ lines.forEach((line, i) => {
70
+ if (line.includes("ui-drift-ok") || (i > 0 && lines[i - 1].includes("ui-drift-ok"))) return;
71
+ for (const [type, re, fix] of RULES) {
72
+ if (re.test(line)) {
73
+ offenders.push({ file, line: i + 1, type, fix });
74
+ flagged.add(type);
75
+ }
76
+ }
77
+ });
78
+ // Class lists split across lines evade line rules: rerun on a
79
+ // whitespace-collapsed copy and report file-level hits not already seen.
80
+ if (!text.includes("ui-drift-ok")) {
81
+ const collapsed = text.replace(/\s+/g, " ");
82
+ for (const [type, re, fix] of RULES) {
83
+ if (!flagged.has(type) && re.test(collapsed)) {
84
+ offenders.push({ file, line: 0, type: `${type} (multi-line)`, fix });
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ if (offenders.length === 0) {
91
+ console.log("ui:check — no hand-rolled primitives found ✓");
92
+ return 0;
93
+ }
94
+
95
+ const byType = {};
96
+ for (const o of offenders) {
97
+ byType[o.type] ??= [];
98
+ byType[o.type].push(o);
99
+ }
100
+ console.error(
101
+ `ui:check — ${offenders.length} hand-rolled primitive(s) duplicating @glasshome/ui:\n`,
102
+ );
103
+ for (const [type, list] of Object.entries(byType)) {
104
+ console.error(` ${type} (${list.length}) — ${list[0].fix}`);
105
+ for (const o of list) console.error(` ${o.file}${o.line ? `:${o.line}` : ""}`);
106
+ console.error("");
107
+ }
108
+ console.error(
109
+ "Migrate to the package primitive, or add `ui-drift-ok <reason>` in a line comment if bespoke is intended.",
110
+ );
111
+ return 1;
112
+ }
113
+
114
+ if (import.meta.url === `file://${process.argv[1]}`) {
115
+ process.exit(checkUiDrift());
116
+ }
@@ -0,0 +1,63 @@
1
+ ---
2
+ /**
3
+ * Static tone alert/callout for Astro / SSR pages — the same `ALERT_TONES` table
4
+ * and surface recipe the Solid <Alert> and hub's docs Callout use, rendered as
5
+ * plain HTML with zero JS and the lucide glyph inlined as SVG (no icon runtime).
6
+ * Drop this into `.astro` markup instead of hand-writing a
7
+ * `rounded-lg border border-primary/40 bg-primary/10` note box.
8
+ */
9
+ import {
10
+ ALERT_CLASS,
11
+ ALERT_CONTENT_CLASS,
12
+ ALERT_DESCRIPTION_CLASS,
13
+ ALERT_ICON_BG_CLASS,
14
+ ALERT_ICON_PATHS,
15
+ ALERT_TITLE_CLASS,
16
+ ALERT_TONES,
17
+ type AlertTone,
18
+ alertIconBgStyle,
19
+ cn,
20
+ glassToneText,
21
+ } from "@glasshome/ui";
22
+
23
+ interface Props {
24
+ tone?: AlertTone;
25
+ /** Bold heading line. Optional. */
26
+ title?: string;
27
+ class?: string;
28
+ }
29
+
30
+ const { tone = "info", title, class: className, ...rest } = Astro.props;
31
+ const t = ALERT_TONES[tone];
32
+
33
+ const surfaceStyle = `--glass-tone:${t.color};color:var(--foreground)`;
34
+
35
+ const iconBgStyle = Object.entries(alertIconBgStyle(t.color))
36
+ .map(([k, v]) => `${k}:${v}`)
37
+ .join(";");
38
+ ---
39
+
40
+ <div
41
+ data-slot="alert"
42
+ role={tone === "destructive" ? "alert" : "status"}
43
+ class={cn(ALERT_CLASS, "glass glass-tint", className)}
44
+ style={surfaceStyle}
45
+ {...rest}
46
+ >
47
+ <span class={ALERT_ICON_BG_CLASS} style={iconBgStyle} aria-hidden="true">
48
+ <svg
49
+ xmlns="http://www.w3.org/2000/svg"
50
+ viewBox="0 0 24 24"
51
+ fill="none"
52
+ stroke="currentColor"
53
+ stroke-width="2"
54
+ stroke-linecap="round"
55
+ stroke-linejoin="round"
56
+ set:html={ALERT_ICON_PATHS[tone]}
57
+ />
58
+ </span>
59
+ <div class={ALERT_CONTENT_CLASS}>
60
+ {title && <p data-slot="alert-title" class={ALERT_TITLE_CLASS} style={`color:${glassToneText(t.color)}`}>{title}</p>}
61
+ <div data-slot="alert-description" class={ALERT_DESCRIPTION_CLASS}><slot /></div>
62
+ </div>
63
+ </div>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { BADGE_DEFAULT_TONE, BADGE_TONE_CLASS } from "../lib/badge-tone";
3
+ import { cn } from "../lib/utils";
4
+
5
+ interface Props {
6
+ tone?: string;
7
+ as?: keyof HTMLElementTagNameMap;
8
+ class?: string;
9
+ }
10
+
11
+ const { tone = BADGE_DEFAULT_TONE, as: Tag = "span", class: className, ...rest } = Astro.props;
12
+ ---
13
+
14
+ <Tag data-slot="badge" class={cn(BADGE_TONE_CLASS, "glass glass-tint", className)} style={`--glass-tone:${tone}`} {...rest}>
15
+ <slot />
16
+ </Tag>
@@ -0,0 +1,43 @@
1
+ ---
2
+ /**
3
+ * Static button/CTA for Astro / SSR pages — the same `buttonVariants` recipe the
4
+ * Solid <Button> uses, rendered as a plain <a>/<button> with zero JS. Drop this
5
+ * into `.astro` markup instead of hand-writing a `rounded-full bg-primary px-6`
6
+ * pill.
7
+ *
8
+ * Renders an <a> when `href` is set, else a <button>. `size` defaults to `none`
9
+ * (sizeless: color + shape only) so marketing CTAs supply their own height and
10
+ * padding via `class`, matching how the hub pill tokens are used.
11
+ */
12
+ import { buttonVariants, cn } from "@glasshome/ui";
13
+
14
+ interface Props {
15
+ variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
16
+ size?: "default" | "sm" | "lg" | "icon" | "none";
17
+ href?: string;
18
+ type?: "button" | "submit" | "reset";
19
+ class?: string;
20
+ }
21
+
22
+ const {
23
+ variant = "default",
24
+ size = "none",
25
+ href,
26
+ type = "button",
27
+ class: className,
28
+ ...rest
29
+ } = Astro.props;
30
+ const cls = cn(buttonVariants({ variant, size }), className);
31
+ ---
32
+
33
+ {
34
+ href ? (
35
+ <a data-slot="button" href={href} class={cls} {...rest}>
36
+ <slot />
37
+ </a>
38
+ ) : (
39
+ <button data-slot="button" type={type} class={cls} {...rest}>
40
+ <slot />
41
+ </button>
42
+ )
43
+ }
@@ -0,0 +1,39 @@
1
+ ---
2
+ import {
3
+ CARD_INTERACTIVE,
4
+ CARD_PADDING,
5
+ CARD_SURFACE,
6
+ CARD_SURFACE_OPAQUE,
7
+ type CardPadding,
8
+ } from "../lib/card-classes";
9
+ import { cn } from "../lib/utils";
10
+
11
+ interface Props {
12
+ as?: keyof HTMLElementTagNameMap;
13
+ interactive?: boolean;
14
+ opaque?: boolean;
15
+ padding?: CardPadding;
16
+ class?: string;
17
+ }
18
+
19
+ const {
20
+ as: Tag = "div",
21
+ interactive = false,
22
+ opaque = false,
23
+ padding = "none",
24
+ class: className,
25
+ ...rest
26
+ } = Astro.props;
27
+
28
+ const classes = cn(
29
+ opaque ? CARD_SURFACE_OPAQUE : CARD_SURFACE,
30
+ "rounded-[var(--radius)]",
31
+ CARD_PADDING[padding],
32
+ interactive && CARD_INTERACTIVE,
33
+ className,
34
+ );
35
+ ---
36
+
37
+ <Tag data-slot="card" class={classes} {...rest}>
38
+ <slot />
39
+ </Tag>
@@ -0,0 +1,69 @@
1
+ ---
2
+ /**
3
+ * The GlassHome lockup for Astro / SSR pages — the same spec the Solid <Logo>
4
+ * renders, as plain HTML with zero JS. Use this instead of hand-building a
5
+ * mark + "GlassHome" + sub-line stack; that is how the lockup ended up in six
6
+ * shapes across the repos.
7
+ *
8
+ * `href` renders the whole lockup as a link. The `sub` slot replaces the
9
+ * sub-brand line for surfaces that animate over it (e.g. hub's launch
10
+ * countdown cross-fade); it inherits nothing, so pass LOGO_SUB_CLASS yourself.
11
+ */
12
+ import {
13
+ cn,
14
+ LOGO_DEFAULT_SIZE,
15
+ LOGO_DEFAULT_SRC,
16
+ LOGO_DEFAULT_SUB,
17
+ LOGO_MARK_CLASS,
18
+ LOGO_MARK_PX,
19
+ LOGO_NAME_CLASS,
20
+ LOGO_ROOT_CLASS,
21
+ LOGO_SIZES,
22
+ LOGO_STACK_CLASS,
23
+ LOGO_SUB_CLASS,
24
+ type LogoSize,
25
+ } from "@glasshome/ui";
26
+
27
+ interface Props {
28
+ /** Sub-brand line. Defaults to "Dash". */
29
+ sub?: string;
30
+ size?: LogoSize;
31
+ /** Renders the lockup as a link when set. */
32
+ href?: string;
33
+ /** Override the mark asset. Both apps serve the default path. */
34
+ src?: string;
35
+ class?: string;
36
+ /** Extra classes for the mark, e.g. a hover transform. */
37
+ markClass?: string;
38
+ }
39
+
40
+ const {
41
+ sub = LOGO_DEFAULT_SUB,
42
+ size = LOGO_DEFAULT_SIZE,
43
+ href,
44
+ src = LOGO_DEFAULT_SRC,
45
+ class: className,
46
+ markClass,
47
+ ...rest
48
+ } = Astro.props;
49
+
50
+ const s = LOGO_SIZES[size];
51
+ const px = LOGO_MARK_PX[size];
52
+ const Tag = href ? "a" : "div";
53
+ ---
54
+
55
+ <Tag data-slot="logo" class={cn(LOGO_ROOT_CLASS, s.gap, className)} href={href} {...rest}>
56
+ <img
57
+ src={src}
58
+ alt=""
59
+ width={px}
60
+ height={px}
61
+ class={cn(LOGO_MARK_CLASS, s.mark, markClass)}
62
+ />
63
+ <span class={LOGO_STACK_CLASS}>
64
+ <span data-slot="logo-name" class={cn(s.name, LOGO_NAME_CLASS)}>GlassHome</span>
65
+ <slot name="sub">
66
+ <span data-slot="logo-sub" class={cn(s.sub, LOGO_SUB_CLASS)}>{sub}</span>
67
+ </slot>
68
+ </span>
69
+ </Tag>
@@ -0,0 +1,33 @@
1
+ ---
2
+ /**
3
+ * Static metallic tier chip for Astro / SSR pages (PRO / EARLY BIRD / CREATOR /
4
+ * FOUNDER) — the same tier tokens the Solid <TierBadge> uses, rendered as plain
5
+ * HTML with zero JS. One family; only the hi/lo/text triplet changes per tier.
6
+ */
7
+ import { TIER_BADGE_CLASS, tierBadgeStyle } from "../lib/tier-badge";
8
+ import { cn } from "../lib/utils";
9
+
10
+ interface Props {
11
+ /** Gradient high stop (the near end of the diagonal sweep, and by default
12
+ * the far end too). */
13
+ hi: string;
14
+ /** Gradient low stop (the dip). */
15
+ lo: string;
16
+ /** Dark label color. */
17
+ text: string;
18
+ /** Far end of the sweep. Default: `hi` (symmetric brushed look). */
19
+ end?: string;
20
+ /** Where `lo` sits, in percent. Default: 50. */
21
+ stop?: number;
22
+ class?: string;
23
+ }
24
+
25
+ const { hi, lo, text, end, stop, class: className, ...rest } = Astro.props;
26
+ const style = Object.entries(tierBadgeStyle(hi, lo, text, { end, stop }))
27
+ .map(([k, v]) => `${k}:${v}`)
28
+ .join(";");
29
+ ---
30
+
31
+ <span data-slot="tier-badge" class={cn(TIER_BADGE_CLASS, className)} style={style} {...rest}>
32
+ <slot />
33
+ </span>
@@ -0,0 +1,65 @@
1
+ export type AlertTone = "info" | "warning" | "success" | "destructive";
2
+
3
+ export type AlertToneStyle = {
4
+ color: string;
5
+ icon: string;
6
+ text: string;
7
+ };
8
+
9
+ /* Text mixes in oklab: no hue channel, a hued tone desaturates without swinging yellow. */
10
+ export const ALERT_TONES: Record<AlertTone, AlertToneStyle> = {
11
+ info: {
12
+ color: "var(--primary)",
13
+ icon: "lucide:info",
14
+ text: "var(--primary-tint-foreground)",
15
+ },
16
+ warning: {
17
+ color: "var(--warning)",
18
+ icon: "lucide:alert-triangle",
19
+ text: "color-mix(in oklab, var(--warning) 55%, var(--foreground))",
20
+ },
21
+ success: {
22
+ color: "var(--success)",
23
+ icon: "lucide:check-circle-2",
24
+ text: "color-mix(in oklab, var(--success) 60%, var(--foreground))",
25
+ },
26
+ destructive: {
27
+ color: "var(--destructive)",
28
+ icon: "lucide:alert-octagon",
29
+ text: "color-mix(in oklab, var(--destructive) 65%, var(--foreground))",
30
+ },
31
+ };
32
+
33
+ /* Lucide glyph paths (viewBox 0 0 24 24), rendered by both the Solid and Astro <Alert>. */
34
+ export const ALERT_ICON_PATHS: Record<AlertTone, string> = {
35
+ info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
36
+ warning:
37
+ '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
38
+ success: '<path d="M21.801 10A10 10 0 1 1 17 3.335"/><path d="m9 11 3 3L22 4"/>',
39
+ destructive:
40
+ '<path d="M12 16h.01"/><path d="M12 8v4"/><path d="M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688A2 2 0 0 1 15.312 22H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z"/>',
41
+ };
42
+
43
+ /* srgb, not oklch: oklch drags a tone mixed toward transparent down to mud. */
44
+ export const alertBorder = (c: string) => `color-mix(in srgb, ${c} 28%, transparent)`;
45
+ export const alertFill = (c: string) => `color-mix(in srgb, ${c} 9%, transparent)`;
46
+ export const alertIconFill = (c: string) => `color-mix(in srgb, ${c} 16%, transparent)`;
47
+
48
+ export const ALERT_CLASS =
49
+ "relative overflow-hidden flex items-start gap-3 rounded-lg border p-3 backdrop-blur-sm";
50
+ export const ALERT_ICON_CLASS =
51
+ "flex size-8 shrink-0 items-center justify-center rounded-md [&>svg]:size-[18px]";
52
+ export const ALERT_ICON_BG_CLASS =
53
+ "pointer-events-none absolute -right-3 -bottom-4 [&>svg]:size-28 [&>svg]:[stroke-width:2.5px]";
54
+ export function alertIconBgStyle(color: string): Record<string, string> {
55
+ return {
56
+ color,
57
+ opacity: "0.32",
58
+ "-webkit-mask-image": "radial-gradient(140% 140% at 100% 100%, #000 45%, transparent 95%)",
59
+ "mask-image": "radial-gradient(140% 140% at 100% 100%, #000 45%, transparent 95%)",
60
+ };
61
+ }
62
+ export const ALERT_CONTENT_CLASS = "relative z-10 min-w-0 flex-1";
63
+ 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";
@@ -0,0 +1,4 @@
1
+ export const BADGE_DEFAULT_TONE = "var(--primary)";
2
+
3
+ export const BADGE_TONE_CLASS =
4
+ "inline-flex w-fit shrink-0 cursor-default select-none items-center gap-1.5 whitespace-nowrap rounded-full border px-2.5 py-0.5 font-medium text-xs";
@@ -0,0 +1,44 @@
1
+ import { cva } from "cva";
2
+
3
+ /* Buttons are glass pills on an OPAQUE --card base: a translucent fill goes
4
+ * muddy over dark heroes/sections. Every class is literal for Tailwind's
5
+ * scanner. `size: none` is sizeless for callers that own height/padding. */
6
+ export const buttonVariants = cva({
7
+ 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
+ variants: {
9
+ variant: {
10
+ default:
11
+ "glass glass-tint [--glass-text:70%] [--glass-tone:var(--primary)] [--glass-wash:30%] hover:[--glass-edge:color-mix(in_srgb,var(--primary)_60%,transparent)] hover:[--glass-wash:40%]",
12
+ destructive:
13
+ "glass glass-tint [--glass-text:61%] [--glass-tone:var(--destructive)] [--glass-wash:30%] focus-visible:ring-destructive/30 hover:[--glass-edge:color-mix(in_srgb,var(--destructive)_60%,transparent)] hover:[--glass-wash:40%]",
14
+ secondary:
15
+ "glass glass-tint [--glass-tone:var(--accent)] [--glass-wash:30%] hover:[--glass-edge:color-mix(in_srgb,var(--accent)_60%,transparent)] hover:[--glass-wash:40%]",
16
+ outline:
17
+ "glass [--glass-edge:var(--border)] hover:[--glass-base:var(--muted)] dark:[--glass-base:var(--input)] dark:hover:[--glass-base:var(--muted)]",
18
+ ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50",
19
+ link: "text-primary underline-offset-4 hover:underline",
20
+ },
21
+ size: {
22
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
23
+ sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
24
+ lg: "h-10 px-6 has-[>svg]:px-4",
25
+ icon: "size-9 p-2",
26
+ none: "",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ });
34
+
35
+ // Evaluated once for static/SSR call sites (marketing CTAs, .astro frontmatter)
36
+ // that want a class string without hydrating <Button>. Sizeless: callers add
37
+ // their own height and padding.
38
+ export const BUTTON_DEFAULT_CLASS = buttonVariants({ variant: "default", size: "none" });
39
+ export const BUTTON_OUTLINE_CLASS = buttonVariants({ variant: "outline", size: "none" });
40
+
41
+ /* Round ghost icon button (copy buttons, small icon actions). Size and text
42
+ * color stay at the call site. */
43
+ export const ICON_BUTTON_CLASS =
44
+ "inline-flex shrink-0 cursor-pointer items-center justify-center rounded-full transition-all duration-150 hover:bg-primary/10 active:scale-90";