@glasshome/ui 1.0.0 → 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.
- package/package.json +2 -1
- package/src/lib/alert-tones.ts +65 -0
- package/src/lib/badge-tone.ts +4 -0
- package/src/lib/button-variants.ts +44 -0
- package/src/lib/card-classes.ts +37 -0
- package/src/lib/glass-tone.ts +23 -0
- package/src/lib/input-classes.ts +5 -0
- package/src/lib/logo-lockup.ts +49 -0
- package/src/lib/overlay-classes.ts +7 -0
- package/src/lib/section-tokens.ts +8 -0
- package/src/lib/tier-badge.ts +31 -0
- package/src/lib/use-is-mobile.ts +17 -0
- package/src/lib/utils.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glasshome/ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "SolidJS component library for GlassHome, built on Kobalte",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/glasshome/ui#readme",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"files": [
|
|
52
52
|
"dist",
|
|
53
53
|
"src/astro",
|
|
54
|
+
"src/lib",
|
|
54
55
|
"src/styles",
|
|
55
56
|
"scripts"
|
|
56
57
|
],
|
|
@@ -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,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";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const CARD_KNOBS =
|
|
2
|
+
"glass [--glass-wash:20%] [--glass-rim:0.3] [--glass-lift:0.45] [--glass-shade:0.05] dark:[--glass-shade:0]";
|
|
3
|
+
|
|
4
|
+
export const CARD_SURFACE_BASE = `${CARD_KNOBS} [--glass-base:color-mix(in_srgb,var(--card)_60%,transparent)]`;
|
|
5
|
+
|
|
6
|
+
/* Split from the surface so dash's performant-blur engine can gate it off and
|
|
7
|
+
* paint a precomputed frost instead. */
|
|
8
|
+
export const CARD_BLUR = "backdrop-blur-[var(--glass-blur,24px)] backdrop-saturate-[1.8]";
|
|
9
|
+
|
|
10
|
+
export const CARD_SURFACE = `${CARD_SURFACE_BASE} ${CARD_BLUR}`;
|
|
11
|
+
|
|
12
|
+
/* Opaque fill, no blur: for busy/dark backgrounds and transform-animated
|
|
13
|
+
* contexts where backdrop-blur can't run. */
|
|
14
|
+
export const CARD_SURFACE_OPAQUE = CARD_KNOBS;
|
|
15
|
+
|
|
16
|
+
/* Recessed track frame (tabs, segmented controls). */
|
|
17
|
+
export const TRACK_SURFACE = "border border-border/50 bg-card/40 backdrop-blur-sm";
|
|
18
|
+
|
|
19
|
+
export const CARD_PADDING = {
|
|
20
|
+
slots: "flex flex-col gap-3 py-3 md:gap-4 md:py-4",
|
|
21
|
+
md: "p-4",
|
|
22
|
+
sm: "p-3",
|
|
23
|
+
none: "",
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export type CardPadding = keyof typeof CARD_PADDING;
|
|
27
|
+
|
|
28
|
+
export const CARD_INTERACTIVE =
|
|
29
|
+
"cursor-pointer transition-colors duration-200 hover:border-border hover:bg-foreground/[0.03] focus-within:border-border";
|
|
30
|
+
|
|
31
|
+
export const CARD_HEADER_CLASS =
|
|
32
|
+
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] md:px-4";
|
|
33
|
+
export const CARD_TITLE_CLASS = "font-semibold leading-none";
|
|
34
|
+
export const CARD_DESCRIPTION_CLASS = "text-muted-foreground text-sm";
|
|
35
|
+
export const CARD_ACTION_CLASS = "col-start-2 row-span-2 row-start-1 self-start justify-self-end";
|
|
36
|
+
export const CARD_CONTENT_CLASS = "px-3 md:px-4";
|
|
37
|
+
export const CARD_FOOTER_CLASS = "flex items-center px-3 md:px-4 [.border-t]:pt-6";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* Per-tone label contrast: light tones (amber, lime) mix further toward
|
|
2
|
+
* --foreground so they read as strongly as dark ones. */
|
|
3
|
+
const TONE_L: Record<string, number> = {
|
|
4
|
+
"var(--primary)": 0.48,
|
|
5
|
+
"var(--accent)": 0.6,
|
|
6
|
+
"var(--success)": 0.68,
|
|
7
|
+
"var(--warning)": 0.76,
|
|
8
|
+
"var(--destructive)": 0.66,
|
|
9
|
+
"var(--muted-foreground)": 0.55,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function toneTextMix(color: string): number {
|
|
13
|
+
const known = TONE_L[color];
|
|
14
|
+
const raw = color.match(/oklch\(\s*([0-9.]+)/)?.[1];
|
|
15
|
+
const l = known ?? (raw ? Number.parseFloat(raw) : 0.6);
|
|
16
|
+
return Math.min(70, Math.max(46, Math.round(71 - (l - 0.5) * 60)));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* oklab, not oklch: no hue channel, so a hued tone desaturates toward neutral
|
|
20
|
+
* instead of swinging toward yellow. */
|
|
21
|
+
export function glassToneText(color: string): string {
|
|
22
|
+
return `color-mix(in oklab, ${color} ${toneTextMix(color)}%, var(--foreground))`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* Recessed (concave) glass field. Opaque --input fill: a translucent field
|
|
2
|
+
* shows whatever sits behind it (worst inside the overlapping Select listbox). */
|
|
3
|
+
export const INPUT_SURFACE = "glass glass-sink [--glass-base:var(--input)] [--glass-light:0.04]";
|
|
4
|
+
|
|
5
|
+
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`;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GlassHome lockup spec, pure — shared by the Solid <Logo>, the Astro
|
|
3
|
+
* <Logo>, and any `.astro` consumer that needs the class strings without
|
|
4
|
+
* hydrating a component.
|
|
5
|
+
*
|
|
6
|
+
* One ratio everywhere (audit L2): the sub-brand line is the dominant element,
|
|
7
|
+
* the wordmark sits above it as a smaller eyebrow. Sub-line spec (audit L1):
|
|
8
|
+
* text-primary, uppercase, bold, tracking-[0.18em] — never restyled per
|
|
9
|
+
* surface, never recolored, and the mark is never stretched or used as an
|
|
10
|
+
* avatar.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type LogoSize = "sm" | "md" | "lg";
|
|
14
|
+
|
|
15
|
+
export const LOGO_DEFAULT_SRC = "/assets/glasshome_logo.png";
|
|
16
|
+
export const LOGO_DEFAULT_SUB = "Dash";
|
|
17
|
+
export const LOGO_DEFAULT_SIZE: LogoSize = "md";
|
|
18
|
+
|
|
19
|
+
/** Rendered pixel size of the mark per size, for width/height attrs (CLS). */
|
|
20
|
+
export const LOGO_MARK_PX: Record<LogoSize, number> = {
|
|
21
|
+
sm: 32,
|
|
22
|
+
md: 48,
|
|
23
|
+
lg: 64,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const LOGO_SIZES: Record<
|
|
27
|
+
LogoSize,
|
|
28
|
+
{ mark: string; name: string; sub: string; gap: string }
|
|
29
|
+
> = {
|
|
30
|
+
sm: { mark: "h-8 w-8", name: "text-xs", sub: "text-base", gap: "gap-1.5" },
|
|
31
|
+
md: { mark: "h-12 w-12", name: "text-base", sub: "text-2xl", gap: "gap-2" },
|
|
32
|
+
lg: { mark: "h-16 w-16", name: "text-lg", sub: "text-3xl", gap: "gap-2" },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The stack sits on the mark's baseline (items-end), so the sub-brand line
|
|
37
|
+
* bottoms out level with the mark rather than floating centred against it.
|
|
38
|
+
*/
|
|
39
|
+
export const LOGO_ROOT_CLASS = "inline-flex items-end";
|
|
40
|
+
/** The two lines are pinned by an explicit gap, never by line-height slack. */
|
|
41
|
+
export const LOGO_STACK_CLASS = "flex flex-col gap-0.5";
|
|
42
|
+
/**
|
|
43
|
+
* Compose as cn(SIZE, BASE), not cn(BASE, SIZE): tailwind-merge treats a
|
|
44
|
+
* font-size class as carrying a line-height, so a trailing `text-2xl` would
|
|
45
|
+
* strip the `leading-none` these rely on.
|
|
46
|
+
*/
|
|
47
|
+
export const LOGO_MARK_CLASS = "shrink-0";
|
|
48
|
+
export const LOGO_NAME_CLASS = "whitespace-nowrap font-bold leading-none tracking-tight";
|
|
49
|
+
export const LOGO_SUB_CLASS = "font-bold text-primary uppercase leading-none tracking-[0.18em]";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/* Floating-panel glass: opaque popover fill so menus stay legible over anything. */
|
|
2
|
+
export const OVERLAY_SURFACE =
|
|
3
|
+
"glass [--glass-base:var(--popover)] [--glass-rim:0.3] [--glass-lift:0.55]";
|
|
4
|
+
|
|
5
|
+
/* Modal scrim behind dialogs/sheets. BottomSheet keeps its own unblurred scrim
|
|
6
|
+
* (backdrop-blur is too slow on mobile). */
|
|
7
|
+
export const SCRIM_CLASS = "bg-background/70 backdrop-blur-sm";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* Concentric radius: inner = outer - padding, clamped for tiny --radius themes. */
|
|
2
|
+
export const SECTION_OUTER_RADIUS = "rounded-[var(--radius)]";
|
|
3
|
+
export const SECTION_INNER_RADIUS = "rounded-[max(0px,calc(var(--radius)-0.75rem))]";
|
|
4
|
+
export const SECTION_PADDING = "p-3";
|
|
5
|
+
|
|
6
|
+
const SECTION_ROW_SURFACE = `${SECTION_INNER_RADIUS} border border-border/60 bg-card/60`;
|
|
7
|
+
|
|
8
|
+
export const SECTION_ROW_CLASS = `${SECTION_ROW_SURFACE} ${SECTION_PADDING}`;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* Metallic tier chip: 135deg brushed sweep, bright ends, dip in the middle. */
|
|
2
|
+
|
|
3
|
+
export const TIER_BADGE_CLASS =
|
|
4
|
+
"inline-flex cursor-default select-none items-center rounded-full px-2 py-0.5 font-black text-[10px] uppercase tracking-wider";
|
|
5
|
+
|
|
6
|
+
/** Optional sweep shaping. Default is the symmetric brushed look (both ends
|
|
7
|
+
* `hi`, dipping to `lo` at the midpoint). The collectible/license golds are a
|
|
8
|
+
* lit bevel instead: they dip early and land on a darker `end`. */
|
|
9
|
+
export interface TierSweep {
|
|
10
|
+
/** Far end of the sweep. Default: `hi` (symmetric). */
|
|
11
|
+
end?: string;
|
|
12
|
+
/** Where `lo` sits, in percent. Default: 50. */
|
|
13
|
+
stop?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Inline style for the metallic pill body. Keys are CSS property names (works
|
|
17
|
+
* as a Solid `style` object and, joined, as an Astro `style` string). */
|
|
18
|
+
export function tierBadgeStyle(
|
|
19
|
+
hi: string,
|
|
20
|
+
lo: string,
|
|
21
|
+
text: string,
|
|
22
|
+
sweep: TierSweep = {},
|
|
23
|
+
): Record<string, string> {
|
|
24
|
+
const end = sweep.end ?? hi;
|
|
25
|
+
const stop = sweep.stop ?? 50;
|
|
26
|
+
return {
|
|
27
|
+
background: `linear-gradient(135deg, ${hi}, ${lo} ${stop}%, ${end})`,
|
|
28
|
+
color: text,
|
|
29
|
+
"box-shadow": "inset 0 1px 0 oklch(1 0 0 / 0.45), 0 1px 2px oklch(0 0 0 / 0.25)",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createSignal, onCleanup, onMount } from "solid-js";
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768;
|
|
4
|
+
|
|
5
|
+
export function createIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = createSignal<boolean | undefined>(undefined);
|
|
7
|
+
|
|
8
|
+
onMount(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
10
|
+
const onChange = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
11
|
+
mql.addEventListener("change", onChange);
|
|
12
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
13
|
+
onCleanup(() => mql.removeEventListener("change", onChange));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return isMobile;
|
|
17
|
+
}
|