@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.
- package/Accordion.astro +52 -0
- package/AccordionItem.astro +65 -0
- package/Alert.svelte +59 -0
- package/AlertDialog.svelte +146 -0
- package/CHANGELOG.md +25 -0
- package/CfImage.astro +70 -0
- package/Combobox.svelte +458 -0
- package/ContextMenu.svelte +120 -0
- package/ContextMenuItem.svelte +64 -0
- package/ContextMenuItemCheckbox.svelte +105 -0
- package/ContextMenuLabel.svelte +18 -0
- package/ContextMenuSeparator.svelte +1 -0
- package/DialogManager.svelte +32 -0
- package/Dropdown.svelte +107 -0
- package/DropdownItem.svelte +87 -0
- package/DropdownLabel.svelte +11 -0
- package/DropdownSeparator.svelte +1 -0
- package/LauncherHint.astro +35 -0
- package/Pagination.astro +154 -0
- package/Panel.svelte +365 -0
- package/README.md +150 -0
- package/Scroller.astro +11 -0
- package/Scroller.svelte +44 -0
- package/TableOfContents.astro +217 -0
- package/Toast.astro +202 -0
- package/dialog.js +53 -0
- package/package.json +35 -0
- package/transitions.js +78 -0
package/Accordion.astro
ADDED
|
@@ -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
|
+
/>
|