@stnd/ui 0.5.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.
@@ -0,0 +1,52 @@
1
+ ---
2
+ /**
3
+ * Accordion.astro
4
+ *
5
+ * Renders an accordion container that ensures only one child details is open at a time.
6
+ *
7
+ * Usage:
8
+ * <Accordion>
9
+ * <AccordionItem title="Item 1">Content</AccordionItem>
10
+ * <AccordionItem title="Item 2">Content</AccordionItem>
11
+ * </Accordion>
12
+ */
13
+
14
+ interface Props {
15
+ class?: string;
16
+ id?: string;
17
+ }
18
+
19
+ const { class: className, id } = Astro.props;
20
+ ---
21
+
22
+ <div id={id} class:list={["stnd-accordion group grid-row", className]}>
23
+ <slot />
24
+ </div>
25
+
26
+ <script>
27
+ (() => {
28
+ const accordions = document.querySelectorAll(".stnd-accordion");
29
+ accordions.forEach((accordion) => {
30
+ accordion.addEventListener("click", (event) => {
31
+ const target = event.target as Element | null;
32
+ if (!target) return;
33
+ const summary = target.closest("summary");
34
+ if (!summary) return;
35
+ const details = summary.closest("details");
36
+ if (!details) return;
37
+ accordion.querySelectorAll("details").forEach((detailsEl) => {
38
+ if (detailsEl !== details) {
39
+ detailsEl.removeAttribute("open");
40
+ }
41
+ });
42
+ });
43
+ });
44
+ })();
45
+ </script>
46
+
47
+ <style>
48
+ .stnd-accordion {
49
+ display: flex;
50
+ flex-direction: column;
51
+ }
52
+ </style>
@@ -0,0 +1,65 @@
1
+ ---
2
+ /**
3
+ * AccordionItem.astro
4
+ *
5
+ * Renders an individual accordion section.
6
+ *
7
+ * Usage:
8
+ * <AccordionItem title="Is it accessible?" headingLevel="h3">
9
+ * <p>Yes. It adheres to the WAI-ARIA design pattern.</p>
10
+ * </AccordionItem>
11
+ */
12
+ import Icon from "@stnd/icon/Icon.astro";
13
+
14
+ interface Props {
15
+ title?: string;
16
+ headingLevel?: "h2" | "h3" | "h4" | "h5" | "h6" | "div";
17
+ open?: boolean;
18
+ class?: string;
19
+ }
20
+
21
+ const {
22
+ title,
23
+ headingLevel: Heading = "h3",
24
+ open = false,
25
+ class: className,
26
+ } = Astro.props;
27
+ ---
28
+
29
+ <details class:list={["group stnd-accordion-item", className]} open={open}>
30
+ <summary class="w-full">
31
+ <Heading class="stnd-accordion-heading">
32
+ <slot name="title">{title}</slot>
33
+ <Icon icon="caret-down" />
34
+ </Heading>
35
+ </summary>
36
+ <section class="stnd-accordion-content">
37
+ <slot />
38
+ </section>
39
+ </details>
40
+
41
+ <style>
42
+ .stnd-accordion-item {
43
+ box-shadow: var(--shadow-ambient);
44
+ padding-bottom: var(--space);
45
+ padding-inline: var(--space);
46
+ margin-inline: calc(var(--space) * -1);
47
+ }
48
+
49
+ .stnd-accordion-item + .stnd-accordion-item {
50
+ margin-top: var(--space);
51
+ }
52
+
53
+ .stnd-accordion-heading {
54
+ margin: 0;
55
+ display: flex;
56
+ justify-content: space-between;
57
+ align-items: center;
58
+ vertical-align: middle;
59
+ padding: 0;
60
+ }
61
+
62
+ .stnd-accordion-content {
63
+ margin-block-start: var(--space);
64
+ }
65
+ </style>
package/Alert.svelte ADDED
@@ -0,0 +1,59 @@
1
+ <script>
2
+ /**
3
+ * @stnd/ui/Alert.svelte
4
+ * Standard UI Alert — inline notification banner.
5
+ *
6
+ * Wraps the global `.alert` CSS class. Use the `class` prop to apply
7
+ * semantic color variants: "success" | "warning" | "error" | "info"
8
+ *
9
+ * @example
10
+ * <Alert>Basic message</Alert>
11
+ * <Alert class="success" icon="✓" title="Saved">Changes saved.</Alert>
12
+ * <Alert class="error" icon="✕" title="Failed">Something went wrong.</Alert>
13
+ */
14
+ let {
15
+ icon,
16
+ title,
17
+ class: className = "",
18
+ style = "",
19
+ children,
20
+ } = $props();
21
+ </script>
22
+
23
+ <div class={["alert", icon && "has-icon", className].filter(Boolean).join(" ")} {style}>
24
+ {#if icon}
25
+ <span class="alert-icon">{icon}</span>
26
+ {/if}
27
+ <div class="alert-body">
28
+ {#if title}<strong class="alert-title">{title}</strong>{/if}
29
+ {@render children?.()}
30
+ </div>
31
+ </div>
32
+
33
+ <style>
34
+ .has-icon {
35
+ display: flex;
36
+ gap: var(--space-trim, 0.5em);
37
+ align-items: flex-start;
38
+ }
39
+
40
+ .alert-icon {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ font-size: var(--size-sm, 0.875rem);
45
+ flex-shrink: 0;
46
+ padding-block-start: 0.15em;
47
+ }
48
+
49
+ .alert-body {
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: 0;
53
+ }
54
+
55
+ .alert-title {
56
+ font-size: var(--size-sm, 0.875rem);
57
+ display: block;
58
+ }
59
+ </style>
@@ -0,0 +1,146 @@
1
+ <script>
2
+ /**
3
+ * @stnd/ui/AlertDialog.svelte
4
+ * Standard UI Alert Dialog
5
+ */
6
+ let {
7
+ open = $bindable(false),
8
+ title = "Are you absolutely sure?",
9
+ description = "This action cannot be undone.",
10
+ confirmLabel = "Continue",
11
+ cancelLabel = "Cancel",
12
+ intent = "danger", // 'neutral' | 'danger'
13
+ onconfirm,
14
+ oncancel,
15
+ } = $props();
16
+
17
+ let dialog = $state();
18
+
19
+ $effect(() => {
20
+ if (open && dialog && !dialog.open) {
21
+ dialog.showModal();
22
+ } else if (!open && dialog && dialog.open) {
23
+ dialog.close();
24
+ }
25
+ });
26
+
27
+ function handleClose() {
28
+ open = false;
29
+ oncancel?.();
30
+ }
31
+
32
+ function handleConfirm() {
33
+ open = false;
34
+ onconfirm?.();
35
+ }
36
+
37
+ function handleKeydown(e) {
38
+ if (e.key === "Escape") {
39
+ e.preventDefault(); // Prevent default browser escape (which just closes it) so we can sync state
40
+ handleClose();
41
+ }
42
+ }
43
+ </script>
44
+
45
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
46
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
47
+ <dialog
48
+ bind:this={dialog}
49
+ class="std-dialog"
50
+ onclose={handleClose}
51
+ oncancel={(e) => {
52
+ e.preventDefault();
53
+ handleClose();
54
+ }}
55
+ onclick={(e) => {
56
+ if (e.target === dialog) handleClose();
57
+ }}
58
+ >
59
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
60
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
61
+ <div
62
+ class="std-dialog-content no-rhythm"
63
+ onclick={(e) => e.stopPropagation()}
64
+ role="document"
65
+ >
66
+ <h4 class="std-dialog-title">{title}</h4>
67
+
68
+ {#if description}
69
+ <p class="std-dialog-desc">{description}</p>
70
+ {/if}
71
+
72
+ <footer class="std-dialog-footer">
73
+ <button class="ghost" onclick={handleClose}>
74
+ {cancelLabel}
75
+ </button>
76
+ <button
77
+ class={intent === "danger" ? "destructive" : ""}
78
+ onclick={handleConfirm}
79
+ >
80
+ {confirmLabel}
81
+ </button>
82
+ </footer>
83
+ </div>
84
+ </dialog>
85
+
86
+ <style>
87
+ .std-dialog {
88
+ background: transparent;
89
+ border: none;
90
+ padding: var(--space);
91
+ margin: auto;
92
+ max-width: 90vw;
93
+ width: 400px;
94
+ color: var(--color-foreground);
95
+ user-select: none;
96
+ background: var(--color-surface-high);
97
+ backdrop-filter: var(--filter-glass);
98
+ border-radius: var(--radius);
99
+ box-shadow: var(--shadow-glow), var(--shadow-raised);
100
+
101
+ &::backdrop {
102
+ background: var(--color-glass);
103
+ backdrop-filter: var(--filter-glass);
104
+ animation: fade-in var(--duration-fast) var(--ease-stnd);
105
+ }
106
+
107
+ &[open] {
108
+ animation: scale-in var(--duration-fast) var(--ease-stnd);
109
+ }
110
+ }
111
+
112
+ .std-dialog-title {
113
+ font-weight: var(--font-weight-bold);
114
+ }
115
+ .std-dialog-desc {
116
+ margin-block-start: var(--space-half);
117
+ color: var(--color-muted);
118
+ font-size: var(--size-sm);
119
+ }
120
+ .std-dialog-footer {
121
+ display: flex;
122
+ justify-content: flex-end;
123
+ gap: var(--space-half);
124
+ margin-block-start: var(--space);
125
+ }
126
+
127
+ @keyframes fade-in {
128
+ from {
129
+ opacity: 0;
130
+ }
131
+ to {
132
+ opacity: 1;
133
+ }
134
+ }
135
+
136
+ @keyframes scale-in {
137
+ from {
138
+ opacity: 0;
139
+ transform: scale(0.95);
140
+ }
141
+ to {
142
+ opacity: 1;
143
+ transform: scale(1);
144
+ }
145
+ }
146
+ </style>
package/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # @stnd/ui
2
+
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Folio Load order
8
+
9
+ ## 0.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Folio order
14
+
15
+ ## 0.3.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 2d0ae02: NPM Test
20
+
21
+ ## 0.2.0
22
+
23
+ ### Minor Changes
24
+
25
+ - 6d8888a: First release
package/CfImage.astro ADDED
@@ -0,0 +1,70 @@
1
+ ---
2
+ /**
3
+ * CfImage — Responsive image via Cloudflare Image Transformations.
4
+ *
5
+ * Generates a proper <img srcset="..." sizes="..."> using Cloudflare's
6
+ * /cdn-cgi/image/ pipeline. Falls back to the original src in dev.
7
+ *
8
+ * @example Basic
9
+ * <CfImage src="/assets/img/hero.jpg" alt="Hero" sizes="100vw" />
10
+ *
11
+ * @example Card thumbnail (image is ~1/3 of viewport on desktop)
12
+ * <CfImage
13
+ * src={cover}
14
+ * alt={title}
15
+ * sizes="(max-width: 600px) 100vw, (max-width: 900px) 50vw, 33vw"
16
+ * loading="eager"
17
+ * />
18
+ */
19
+ import { cfImage, cfImageSrcset } from "@stnd/utils";
20
+
21
+ interface Props {
22
+ src: string;
23
+ alt: string;
24
+ /** CSS sizes attribute — describes the rendered image width at each breakpoint. */
25
+ sizes?: string;
26
+ /** Widths (px) to include in the srcset. */
27
+ widths?: number[];
28
+ quality?: number;
29
+ format?: string;
30
+ /** Cloudflare sharpen (0–10). */
31
+ sharpen?: number;
32
+ class?: string;
33
+ loading?: "lazy" | "eager";
34
+ decoding?: "async" | "sync" | "auto";
35
+ [key: string]: any;
36
+ }
37
+
38
+ const {
39
+ src,
40
+ alt,
41
+ sizes = "100vw",
42
+ widths = [640, 960, 1280, 1920],
43
+ quality = 85,
44
+ format = "auto",
45
+ sharpen,
46
+ class: className,
47
+ loading = "lazy",
48
+ decoding = "async",
49
+ ...rest
50
+ } = Astro.props;
51
+
52
+ const isDev = import.meta.env.DEV;
53
+ const enabled = !isDev;
54
+
55
+ // Default src: largest width ≤ 1280 as the 1× fallback
56
+ const defaultWidth = widths.find((w) => w <= 1280) ?? widths[widths.length - 1];
57
+ const imgSrc = cfImage(src, { width: defaultWidth, quality, format, sharpen }, enabled);
58
+ const srcset = cfImageSrcset(src, { widths, quality, format, sharpen }, enabled);
59
+ ---
60
+
61
+ <img
62
+ src={imgSrc}
63
+ srcset={srcset || undefined}
64
+ sizes={srcset ? sizes : undefined}
65
+ alt={alt}
66
+ class={className}
67
+ loading={loading}
68
+ decoding={decoding}
69
+ {...rest}
70
+ />