@glasshome/ui 1.0.1 → 1.1.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/dist/index.d.ts +1 -0
- package/dist/index.js +9 -9
- package/dist/lib/carousel-classes.d.ts +7 -0
- package/dist/lib/carousel-init.d.ts +9 -0
- package/dist/solid/carousel.d.ts +7 -1
- package/dist/solid/icon-picker.d.ts +14 -0
- package/dist/solid/index.d.ts +4 -3
- package/dist/solid/index.js +4011 -3533
- package/dist/solid/schema-form.d.ts +3 -0
- package/dist/solid/section-card.d.ts +1 -0
- package/dist/solid/settings-row.d.ts +9 -2
- package/dist/{utils-gbdcvo14.js → utils-BLMzDBJk.js} +343 -330
- package/package.json +3 -1
- package/src/astro/Carousel.astro +79 -0
- package/src/lib/carousel-classes.ts +35 -0
- package/src/lib/carousel-init.ts +87 -0
- package/src/styles/globals.css +59 -0
- package/src/styles/theme.css +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glasshome/ui",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.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",
|
|
@@ -78,6 +78,8 @@
|
|
|
78
78
|
"clsx": "^2.1.1",
|
|
79
79
|
"cva": "^1.0.0-beta.4",
|
|
80
80
|
"embla-carousel": "^8.5.1",
|
|
81
|
+
"embla-carousel-autoplay": "^8.5.1",
|
|
82
|
+
"embla-carousel-fade": "^8.5.1",
|
|
81
83
|
"solid-sonner": "^0.3.1",
|
|
82
84
|
"tailwind-merge": "^3.4.0"
|
|
83
85
|
},
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Server-rendered carousel. Slides live in the HTML, so they are indexable and
|
|
4
|
+
* the first one is the LCP image; the client script only adds behaviour.
|
|
5
|
+
* Use this on content pages; use the Solid <Carousel> inside an app.
|
|
6
|
+
*
|
|
7
|
+
* Slides go in the default slot as <CarouselItem>-shaped elements, or pass
|
|
8
|
+
* `count` to have dots rendered for you.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
CAROUSEL_DOTS,
|
|
12
|
+
CAROUSEL_VIEWPORT,
|
|
13
|
+
type CarouselTransition,
|
|
14
|
+
carouselDot,
|
|
15
|
+
carouselTrack,
|
|
16
|
+
} from "../lib/carousel-classes";
|
|
17
|
+
import { cn } from "../lib/utils";
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
transition?: CarouselTransition;
|
|
21
|
+
/** Advance every N ms. Skipped under prefers-reduced-motion. */
|
|
22
|
+
autoplay?: number;
|
|
23
|
+
loop?: boolean;
|
|
24
|
+
/** Slide count; when set, dots are rendered. */
|
|
25
|
+
count?: number;
|
|
26
|
+
class?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
transition = "slide",
|
|
31
|
+
autoplay,
|
|
32
|
+
loop = false,
|
|
33
|
+
count,
|
|
34
|
+
class: className,
|
|
35
|
+
...rest
|
|
36
|
+
} = Astro.props;
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
<div
|
|
40
|
+
data-carousel
|
|
41
|
+
data-carousel-transition={transition}
|
|
42
|
+
data-carousel-autoplay={autoplay}
|
|
43
|
+
data-carousel-loop={loop ? "" : undefined}
|
|
44
|
+
class={cn("relative", className)}
|
|
45
|
+
role="region"
|
|
46
|
+
aria-roledescription="carousel"
|
|
47
|
+
{...rest}
|
|
48
|
+
>
|
|
49
|
+
<div data-carousel-viewport class={CAROUSEL_VIEWPORT}>
|
|
50
|
+
<div class={carouselTrack(transition)}>
|
|
51
|
+
<slot />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<slot name="controls" />
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
count && (
|
|
59
|
+
<div class={CAROUSEL_DOTS} data-carousel-dots>
|
|
60
|
+
{Array.from({ length: count }, (_, i) => (
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
data-carousel-dot
|
|
64
|
+
aria-label={`Go to slide ${i + 1}`}
|
|
65
|
+
aria-current={i === 0}
|
|
66
|
+
class={carouselDot(i === 0)}
|
|
67
|
+
/>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<script>
|
|
75
|
+
import { initCarousels } from "../lib/carousel-init";
|
|
76
|
+
|
|
77
|
+
initCarousels();
|
|
78
|
+
document.addEventListener("astro:page-load", () => initCarousels());
|
|
79
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* Shared by the Solid <Carousel> and astro/Carousel.astro so the two render the
|
|
2
|
+
* same markup. Astro renders slides server-side (SEO, LCP) and enhances them on
|
|
3
|
+
* the client with the same embla engine. */
|
|
4
|
+
|
|
5
|
+
export type CarouselTransition = "slide" | "fade" | "wipe";
|
|
6
|
+
|
|
7
|
+
export const CAROUSEL_VIEWPORT = "h-full overflow-hidden";
|
|
8
|
+
|
|
9
|
+
/** Stacked modes put every slide in one grid cell; slide lays out a flex track. */
|
|
10
|
+
export function carouselTrack(
|
|
11
|
+
transition: CarouselTransition,
|
|
12
|
+
orientation: "horizontal" | "vertical" = "horizontal",
|
|
13
|
+
) {
|
|
14
|
+
return transition === "slide"
|
|
15
|
+
? `flex ${orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col"}`
|
|
16
|
+
: "carousel-stack";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function carouselItem(
|
|
20
|
+
transition: CarouselTransition,
|
|
21
|
+
orientation: "horizontal" | "vertical" = "horizontal",
|
|
22
|
+
) {
|
|
23
|
+
// Stacked slides fill their grid cell; only a flex track needs basis/gutter.
|
|
24
|
+
if (transition === "wipe") return "min-w-0 carousel-wipe";
|
|
25
|
+
if (transition === "fade") return "min-w-0";
|
|
26
|
+
return `min-w-0 shrink-0 grow-0 basis-full ${orientation === "horizontal" ? "pl-4" : "pt-4"}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const CAROUSEL_DOTS = "flex items-center justify-center gap-2";
|
|
30
|
+
|
|
31
|
+
export function carouselDot(active: boolean) {
|
|
32
|
+
return `h-1.5 rounded-full transition-all hover:bg-foreground/70 ${
|
|
33
|
+
active ? "w-6 bg-foreground/80" : "w-1.5 bg-foreground/40"
|
|
34
|
+
}`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import EmblaCarousel from "embla-carousel";
|
|
2
|
+
import Autoplay from "embla-carousel-autoplay";
|
|
3
|
+
import Fade from "embla-carousel-fade";
|
|
4
|
+
import { type CarouselTransition, carouselDot } from "./carousel-classes.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Client enhancement for astro/Carousel.astro. The markup is already in the
|
|
8
|
+
* server HTML; this only adds behaviour, so the slides stay visible (and
|
|
9
|
+
* indexable) if the script never runs.
|
|
10
|
+
*
|
|
11
|
+
* Emits `carousel:select` with `{ index }` so a host page can sync its own
|
|
12
|
+
* chrome (labels, lightboxes) without owning the carousel.
|
|
13
|
+
*/
|
|
14
|
+
export function initCarousels(root: ParentNode = document) {
|
|
15
|
+
const roots = Array.from(root.querySelectorAll("[data-carousel]")) as HTMLElement[];
|
|
16
|
+
for (const el of roots) {
|
|
17
|
+
if (el.dataset.carouselReady) continue;
|
|
18
|
+
el.dataset.carouselReady = "1";
|
|
19
|
+
|
|
20
|
+
const viewport = el.querySelector("[data-carousel-viewport]") as HTMLElement | null;
|
|
21
|
+
if (!viewport) continue;
|
|
22
|
+
|
|
23
|
+
const transition = (el.dataset.carouselTransition ?? "slide") as CarouselTransition;
|
|
24
|
+
const autoplayMs = Number(el.dataset.carouselAutoplay ?? 0);
|
|
25
|
+
const loop = el.dataset.carouselLoop === "";
|
|
26
|
+
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
27
|
+
|
|
28
|
+
const plugins = [];
|
|
29
|
+
if (transition !== "slide") plugins.push(Fade());
|
|
30
|
+
if (autoplayMs > 0 && !reduced) {
|
|
31
|
+
// Manual nav should not kill autoplay for good; hovering pauses it and
|
|
32
|
+
// leaving resumes, which is what a hero carousel wants.
|
|
33
|
+
plugins.push(
|
|
34
|
+
Autoplay({
|
|
35
|
+
delay: autoplayMs,
|
|
36
|
+
stopOnInteraction: false,
|
|
37
|
+
stopOnMouseEnter: true,
|
|
38
|
+
stopOnFocusIn: true,
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const embla = EmblaCarousel(viewport, { loop }, plugins);
|
|
44
|
+
const dots = Array.from(el.querySelectorAll("[data-carousel-dot]")) as HTMLButtonElement[];
|
|
45
|
+
|
|
46
|
+
let previous = -1;
|
|
47
|
+
const onSelect = () => {
|
|
48
|
+
const index = embla.selectedScrollSnap();
|
|
49
|
+
embla.slideNodes().forEach((node, i) => {
|
|
50
|
+
node.toggleAttribute("data-selected", i === index);
|
|
51
|
+
// The outgoing slide stays visible one layer down until the sweep lands.
|
|
52
|
+
node.toggleAttribute("data-prev", i === previous && i !== index);
|
|
53
|
+
});
|
|
54
|
+
previous = index;
|
|
55
|
+
dots.forEach((dot, i) => {
|
|
56
|
+
dot.className = carouselDot(i === index);
|
|
57
|
+
dot.setAttribute("aria-current", String(i === index));
|
|
58
|
+
});
|
|
59
|
+
el.dispatchEvent(new CustomEvent("carousel:select", { detail: { index }, bubbles: true }));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
el.addEventListener("keydown", (e) => {
|
|
63
|
+
const key = (e as KeyboardEvent).key;
|
|
64
|
+
if (key !== "ArrowLeft" && key !== "ArrowRight") return;
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
if (key === "ArrowLeft") embla.scrollPrev();
|
|
67
|
+
else embla.scrollNext();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// A host that covers the carousel (a lightbox) pauses it, so the slide
|
|
71
|
+
// does not change behind the overlay.
|
|
72
|
+
const autoplay = (embla.plugins() as { autoplay?: { play: () => void; stop: () => void } })
|
|
73
|
+
.autoplay;
|
|
74
|
+
el.addEventListener("carousel:pause", () => autoplay?.stop());
|
|
75
|
+
el.addEventListener("carousel:resume", () => autoplay?.play());
|
|
76
|
+
|
|
77
|
+
dots.forEach((dot, i) => {
|
|
78
|
+
dot.addEventListener("click", () => embla.scrollTo(i));
|
|
79
|
+
});
|
|
80
|
+
el.querySelector("[data-carousel-prev]")?.addEventListener("click", () => embla.scrollPrev());
|
|
81
|
+
el.querySelector("[data-carousel-next]")?.addEventListener("click", () => embla.scrollNext());
|
|
82
|
+
|
|
83
|
+
onSelect();
|
|
84
|
+
embla.on("select", onSelect);
|
|
85
|
+
embla.on("reInit", onSelect);
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/styles/globals.css
CHANGED
|
@@ -101,6 +101,14 @@
|
|
|
101
101
|
* mixed toward transparent-black down to mud on light surfaces. Deliberately
|
|
102
102
|
* unlayered so the material owns border/background/box-shadow; tune via knobs. */
|
|
103
103
|
:where(.glass) {
|
|
104
|
+
/* Descendant-readable mirror of this surface's tone. The --glass-* knobs are
|
|
105
|
+
* @property inherits:false so a nested glass element never picks up an
|
|
106
|
+
* ancestor's tint — correct for surfaces, but it also means a plain child
|
|
107
|
+
* (an icon, a label) reading var(--glass-tone) silently gets `transparent`
|
|
108
|
+
* and paints nothing. --surface-tone is an ordinary inheriting property, so
|
|
109
|
+
* children can read it; a nested .glass re-declares it from its own knob,
|
|
110
|
+
* which resets the chain and keeps the no-leak guarantee. */
|
|
111
|
+
--surface-tone: var(--glass-tone);
|
|
104
112
|
border: 1px solid var(--glass-edge);
|
|
105
113
|
/* Base is a gradient layer (not background-color) so the frost slice can sit
|
|
106
114
|
* BELOW it — background-color always paints under every image layer, which
|
|
@@ -162,3 +170,54 @@
|
|
|
162
170
|
background: var(--muted-foreground);
|
|
163
171
|
}
|
|
164
172
|
}
|
|
173
|
+
|
|
174
|
+
/* Stacked carousel modes (fade, wipe) put every slide in one grid cell. This is
|
|
175
|
+
* structural, so it ships as real CSS: a Tailwind arbitrary variant written in
|
|
176
|
+
* package source never reaches a consumer, because Tailwind skips node_modules
|
|
177
|
+
* when scanning for class names. */
|
|
178
|
+
.carousel-stack {
|
|
179
|
+
display: grid;
|
|
180
|
+
}
|
|
181
|
+
.carousel-stack > * {
|
|
182
|
+
grid-column: 1;
|
|
183
|
+
grid-row: 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Carousel `wipe`: the incoming slide sweeps in diagonally over the previous
|
|
187
|
+
* one, which stays fully visible beneath until the sweep lands. Only the
|
|
188
|
+
* incoming slide animates: giving the outgoing one a matching transition makes
|
|
189
|
+
* it collapse at the same time, which reads as two wipes from opposite corners.
|
|
190
|
+
* Slides are opaque here; embla's fade plugin only stops the track translating,
|
|
191
|
+
* and its opacity would wash out the sweep. */
|
|
192
|
+
.carousel-wipe {
|
|
193
|
+
/* Content choreography, not a state change: the motion scale's rungs are for
|
|
194
|
+
* micro-interactions and are far too quick to read as a sweep. Override per
|
|
195
|
+
* instance by setting --carousel-wipe on the carousel. */
|
|
196
|
+
--carousel-wipe: 1600ms;
|
|
197
|
+
clip-path: polygon(0 0, 300% 0, 0 300%, 0 0);
|
|
198
|
+
/* The fade plugin fades AND parks inactive slides off-screen
|
|
199
|
+
* (transform: translate(containerWidth + 2)), on embla's own short scroll
|
|
200
|
+
* duration. Both have to be neutralised or the sweep reveals empty space
|
|
201
|
+
* once the outgoing slide leaves: in wipe mode clip-path is the only reveal,
|
|
202
|
+
* and z-index alone decides what shows through. Inline styles, hence
|
|
203
|
+
* !important. */
|
|
204
|
+
opacity: 1 !important;
|
|
205
|
+
transform: none !important;
|
|
206
|
+
}
|
|
207
|
+
.carousel-wipe[data-prev] {
|
|
208
|
+
z-index: 1;
|
|
209
|
+
}
|
|
210
|
+
.carousel-wipe[data-selected] {
|
|
211
|
+
z-index: 2;
|
|
212
|
+
animation: carousel-wipe-in var(--carousel-wipe) ease-in-out;
|
|
213
|
+
}
|
|
214
|
+
@keyframes carousel-wipe-in {
|
|
215
|
+
from {
|
|
216
|
+
clip-path: polygon(0 0, 0 0, 0 0, 0 0);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
@media (prefers-reduced-motion: reduce) {
|
|
220
|
+
.carousel-wipe[data-selected] {
|
|
221
|
+
animation: none;
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/styles/theme.css
CHANGED
|
@@ -160,6 +160,14 @@
|
|
|
160
160
|
--shadow-2xl: 0px 2px 3px 0px hsl(0 0% 0% / 0.4);
|
|
161
161
|
--tracking-normal: 0em;
|
|
162
162
|
--spacing: 0.27rem;
|
|
163
|
+
|
|
164
|
+
/* Motion. Durations name the gesture, not the number: micro is a hover or
|
|
165
|
+
* border change, state a tone or chip swap, expand an open/close. Theme
|
|
166
|
+
* independent, so declared once; zeroed under prefers-reduced-motion. */
|
|
167
|
+
--ease-emphasis: cubic-bezier(0.22, 1, 0.36, 1);
|
|
168
|
+
--duration-micro: 120ms;
|
|
169
|
+
--duration-state: 160ms;
|
|
170
|
+
--duration-expand: 200ms;
|
|
163
171
|
}
|
|
164
172
|
|
|
165
173
|
.dark {
|
|
@@ -418,6 +426,13 @@
|
|
|
418
426
|
::view-transition-new(area-title) {
|
|
419
427
|
animation: none !important;
|
|
420
428
|
}
|
|
429
|
+
|
|
430
|
+
/* Anything timed off the motion scale stops moving, no per-component opt-in. */
|
|
431
|
+
:root {
|
|
432
|
+
--duration-micro: 0ms;
|
|
433
|
+
--duration-state: 0ms;
|
|
434
|
+
--duration-expand: 0ms;
|
|
435
|
+
}
|
|
421
436
|
}
|
|
422
437
|
|
|
423
438
|
/* Performance: disable all backdrop-filter when reduce-blur is active */
|