@stnd/ui 0.5.0 → 0.5.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/Accordion.astro +10 -8
- package/AccordionItem.astro +10 -7
- package/Alert.svelte +14 -10
- package/AlertDialog.svelte +16 -9
- package/CfImage.astro +19 -13
- package/Combobox.svelte +17 -11
- package/ContextMenu.svelte +73 -88
- package/ContextMenuItem.svelte +7 -1
- package/ContextMenuItemCheckbox.svelte +6 -0
- package/ContextMenuLabel.svelte +6 -0
- package/ContextMenuSeparator.svelte +7 -0
- package/Dialog.svelte +60 -0
- package/DialogManager.svelte +12 -0
- package/Dropdown.svelte +46 -11
- package/DropdownItem.svelte +7 -1
- package/DropdownLabel.svelte +8 -0
- package/DropdownSeparator.svelte +7 -0
- package/LauncherHint.astro +7 -3
- package/Pagination.astro +9 -10
- package/Panel.svelte +2 -2
- package/Popover.svelte +83 -0
- package/README.md +45 -4
- package/Scroller.astro +6 -3
- package/Scroller.svelte +6 -0
- package/TableOfContents.astro +33 -16
- package/Toast.astro +21 -17
- package/menu.css +12 -0
- package/menu.js +33 -0
- package/menu.test.js +44 -0
- package/package.json +4 -2
package/Accordion.astro
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* Accordion
|
|
3
|
+
* @component Accordion
|
|
4
|
+
* @description Zero-JS accordion container — ensures only one child
|
|
5
|
+
* `<details>` is open at a time via a plain click listener, no framework
|
|
6
|
+
* hydration required.
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* <
|
|
9
|
-
*
|
|
10
|
-
* <AccordionItem title="Item 2">Content</AccordionItem>
|
|
11
|
-
* </Accordion>
|
|
8
|
+
* @example astro
|
|
9
|
+
* <Accordion>
|
|
10
|
+
* <AccordionItem title="Item 1">Content</AccordionItem>
|
|
11
|
+
* <AccordionItem title="Item 2">Content</AccordionItem>
|
|
12
|
+
* </Accordion>
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
16
|
+
/** Extra class(es) for the container. */
|
|
15
17
|
class?: string;
|
|
16
18
|
id?: string;
|
|
17
19
|
}
|
package/AccordionItem.astro
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* AccordionItem
|
|
3
|
+
* @component AccordionItem
|
|
4
|
+
* @description One collapsible section inside an `<Accordion>` — a plain
|
|
5
|
+
* `<details>`/`<summary>` pair, so it works even without JS.
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* <p>Yes. It adheres to the WAI-ARIA design pattern.</p>
|
|
10
|
-
* </AccordionItem>
|
|
7
|
+
* @example astro
|
|
8
|
+
* <AccordionItem title="Is it accessible?" headingLevel="h3">
|
|
9
|
+
* <p>Yes. It adheres to the WAI-ARIA design pattern.</p>
|
|
10
|
+
* </AccordionItem>
|
|
11
11
|
*/
|
|
12
12
|
import Icon from "@stnd/icon/Icon.astro";
|
|
13
13
|
|
|
14
14
|
interface Props {
|
|
15
|
+
/** Title text — ignored if the `title` slot is used instead. */
|
|
15
16
|
title?: string;
|
|
17
|
+
/** Heading tag wrapping the title. Default: `h3`. */
|
|
16
18
|
headingLevel?: "h2" | "h3" | "h4" | "h5" | "h6" | "div";
|
|
19
|
+
/** Initial open state. Default: `false`. */
|
|
17
20
|
open?: boolean;
|
|
18
21
|
class?: string;
|
|
19
22
|
}
|
package/Alert.svelte
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
/**
|
|
3
|
-
* @
|
|
4
|
-
*
|
|
3
|
+
* @component Alert
|
|
4
|
+
* @description Inline notification banner. Wraps the global `.alert` CSS
|
|
5
|
+
* class — use the `class` prop for semantic color variants.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* @property {string} [class] - Semantic intent: "success" | "warning" | "error" | "info". Defaults to accent color.
|
|
8
|
+
* @property {string} [title] - Optional bold title.
|
|
9
|
+
* @property {string} [icon] - Optional string icon (e.g. "✓", "⚠").
|
|
10
|
+
* @property {string} [style] - Extra inline style.
|
|
11
|
+
* @property {import('svelte').Snippet} [children] - Banner content.
|
|
8
12
|
*
|
|
9
|
-
* @example
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* @example svelte
|
|
14
|
+
* <Alert>Basic message</Alert>
|
|
15
|
+
* <Alert class="success" icon="✓" title="Saved">Changes saved.</Alert>
|
|
16
|
+
* <Alert class="error" icon="✕" title="Failed">Something went wrong.</Alert>
|
|
13
17
|
*/
|
|
14
18
|
let {
|
|
15
|
-
icon,
|
|
16
|
-
title,
|
|
19
|
+
icon = "",
|
|
20
|
+
title = "",
|
|
17
21
|
class: className = "",
|
|
18
22
|
style = "",
|
|
19
23
|
children,
|
package/AlertDialog.svelte
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
/**
|
|
3
|
-
* @
|
|
4
|
-
*
|
|
3
|
+
* @component AlertDialog
|
|
4
|
+
* @description A native `<dialog>`-based confirmation modal. Used
|
|
5
|
+
* internally by `DialogManager`/`confirm()` — most consumers should call
|
|
6
|
+
* `confirm()` from `@stnd/ui/dialog.js` instead of mounting this directly.
|
|
7
|
+
*
|
|
8
|
+
* @property {boolean} [open] - Bindable open state.
|
|
9
|
+
* @property {string} [title] - Dialog title.
|
|
10
|
+
* @property {string} [description] - Dialog body text.
|
|
11
|
+
* @property {string} [confirmLabel] - Confirm button label.
|
|
12
|
+
* @property {string} [cancelLabel] - Cancel button label.
|
|
13
|
+
* @property {'neutral' | 'danger'} [intent] - Confirm button styling.
|
|
14
|
+
* @property {() => void} [onconfirm] - Called when confirmed.
|
|
15
|
+
* @property {() => void} [oncancel] - Called when cancelled or dismissed.
|
|
5
16
|
*/
|
|
6
17
|
let {
|
|
7
18
|
open = $bindable(false),
|
|
@@ -34,12 +45,6 @@
|
|
|
34
45
|
onconfirm?.();
|
|
35
46
|
}
|
|
36
47
|
|
|
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
48
|
</script>
|
|
44
49
|
|
|
45
50
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
@@ -47,7 +52,9 @@
|
|
|
47
52
|
<dialog
|
|
48
53
|
bind:this={dialog}
|
|
49
54
|
class="std-dialog"
|
|
50
|
-
|
|
55
|
+
aria-label={title}
|
|
56
|
+
onkeydown={(event) => event.stopPropagation()}
|
|
57
|
+
onclose={() => { if (open) handleClose(); }}
|
|
51
58
|
oncancel={(e) => {
|
|
52
59
|
e.preventDefault();
|
|
53
60
|
handleClose();
|
package/CfImage.astro
CHANGED
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* CfImage
|
|
3
|
+
* @component CfImage
|
|
4
|
+
* @description Responsive image via Cloudflare Image Transformations.
|
|
5
|
+
* Generates a proper `<img srcset="..." sizes="...">` using Cloudflare's
|
|
6
|
+
* `/cdn-cgi/image/` pipeline. Falls back to the original `src` in dev.
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
* /
|
|
8
|
+
* @example astro - Basic
|
|
9
|
+
* <CfImage src="/assets/img/test.png" alt="Hero" sizes="100vw" />
|
|
7
10
|
*
|
|
8
|
-
* @example
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* sizes="(max-width: 600px) 100vw, (max-width: 900px) 50vw, 33vw"
|
|
16
|
-
* loading="eager"
|
|
17
|
-
* />
|
|
11
|
+
* @example astro - 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
18
|
*/
|
|
19
19
|
import { cfImage, cfImageSrcset } from "@stnd/utils";
|
|
20
20
|
|
|
21
21
|
interface Props {
|
|
22
|
+
/** Image source path. */
|
|
22
23
|
src: string;
|
|
24
|
+
/** Alt text — required, no silent fallback. */
|
|
23
25
|
alt: string;
|
|
24
26
|
/** CSS sizes attribute — describes the rendered image width at each breakpoint. */
|
|
25
27
|
sizes?: string;
|
|
26
28
|
/** Widths (px) to include in the srcset. */
|
|
27
29
|
widths?: number[];
|
|
30
|
+
/** Cloudflare output quality (0–100). */
|
|
28
31
|
quality?: number;
|
|
32
|
+
/** Output format passed to Cloudflare (e.g. `avif`, `webp`, `auto`). */
|
|
29
33
|
format?: string;
|
|
30
34
|
/** Cloudflare sharpen (0–10). */
|
|
31
35
|
sharpen?: number;
|
|
32
36
|
class?: string;
|
|
37
|
+
/** Native `loading` attribute. */
|
|
33
38
|
loading?: "lazy" | "eager";
|
|
39
|
+
/** Native `decoding` attribute. */
|
|
34
40
|
decoding?: "async" | "sync" | "auto";
|
|
35
41
|
[key: string]: any;
|
|
36
42
|
}
|
package/Combobox.svelte
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { setContext } from "svelte";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @component Combobox
|
|
6
|
+
* @description Searchable select — type to filter `options`, or type a
|
|
7
|
+
* value not in the list when `allowCustom` is set.
|
|
8
|
+
*/
|
|
9
|
+
|
|
4
10
|
/**
|
|
5
11
|
* @typedef {Object} ComboboxOption
|
|
6
|
-
* @property {string} label
|
|
7
|
-
* @property {string} value
|
|
12
|
+
* @property {string} label - Text shown in the list.
|
|
13
|
+
* @property {string} value - Value stored when this option is selected.
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* @typedef {Object} ComboboxProps
|
|
12
|
-
* @property {ComboboxOption[]} options
|
|
13
|
-
* @property {string} [value]
|
|
14
|
-
* @property {string} [placeholder]
|
|
15
|
-
* @property {string} [searchPlaceholder]
|
|
16
|
-
* @property {string} [emptyText]
|
|
17
|
-
* @property {boolean} [allowCustom]
|
|
18
|
-
* @property {string} [class]
|
|
19
|
-
* @property {string} [name]
|
|
20
|
-
* @property {string} [id]
|
|
18
|
+
* @property {ComboboxOption[]} options - The list to search/select from.
|
|
19
|
+
* @property {string} [value] - Bindable selected value.
|
|
20
|
+
* @property {string} [placeholder] - Placeholder before a value is selected.
|
|
21
|
+
* @property {string} [searchPlaceholder] - Placeholder inside the search input.
|
|
22
|
+
* @property {string} [emptyText] - Shown when no option matches the search.
|
|
23
|
+
* @property {boolean} [allowCustom] - Accept a typed value not present in `options`.
|
|
24
|
+
* @property {string} [class] - Extra class(es) for the trigger.
|
|
25
|
+
* @property {string} [name] - Name for an associated hidden form input.
|
|
26
|
+
* @property {string} [id] - ID for the trigger button.
|
|
21
27
|
*/
|
|
22
28
|
|
|
23
29
|
/** @type {ComboboxProps} */
|
package/ContextMenu.svelte
CHANGED
|
@@ -1,120 +1,105 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { setContext } from "svelte";
|
|
2
|
+
import { setContext, tick } from "svelte";
|
|
3
|
+
import "./menu.css";
|
|
4
|
+
import { clampMenuPosition, menuItems, navigateMenu } from "./menu.js";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @property {string} [class] - Optional class for the trigger wrapper
|
|
7
|
+
* Right-click menu, or controlled menu for virtualized/delegated targets.
|
|
8
|
+
* `position` uses viewport coordinates. `onclose` synchronizes a controlled target.
|
|
9
|
+
* Existing children/content snippets remain supported.
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
let
|
|
15
|
-
let
|
|
16
|
-
let
|
|
17
|
-
let y = $state(0);
|
|
11
|
+
let {
|
|
12
|
+
children = undefined, content = undefined, class: className = "", open = $bindable(false),
|
|
13
|
+
position = null, label = "Actions", onclose = () => {}, ...props
|
|
14
|
+
} = $props();
|
|
15
|
+
let point = $state({ x: 0, y: 0 });
|
|
16
|
+
let location = $state({ x: 0, y: 0 });
|
|
17
|
+
let placed = $state(false);
|
|
18
18
|
let menuElement = $state();
|
|
19
|
-
|
|
20
|
-
function onContextMenu(e) {
|
|
21
|
-
if (e.ctrlKey) return;
|
|
22
|
-
e.preventDefault();
|
|
23
|
-
|
|
24
|
-
let targetX = e.clientX;
|
|
25
|
-
let targetY = e.clientY;
|
|
26
|
-
|
|
27
|
-
x = targetX;
|
|
28
|
-
y = targetY;
|
|
29
|
-
open = true;
|
|
30
|
-
positioned = false;
|
|
31
|
-
|
|
32
|
-
// Use a microtask/timeout to wait for render then measure
|
|
33
|
-
setTimeout(() => {
|
|
34
|
-
if (!menuElement) return;
|
|
35
|
-
|
|
36
|
-
const rect = menuElement.getBoundingClientRect();
|
|
37
|
-
const viewportWidth = window.innerWidth;
|
|
38
|
-
const viewportHeight = window.innerHeight;
|
|
39
|
-
|
|
40
|
-
if (targetX + rect.width > viewportWidth) {
|
|
41
|
-
targetX -= rect.width;
|
|
42
|
-
}
|
|
43
|
-
if (targetY + rect.height > viewportHeight) {
|
|
44
|
-
targetY -= rect.height;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
x = targetX;
|
|
48
|
-
y = targetY;
|
|
49
|
-
positioned = true;
|
|
50
|
-
}, 0);
|
|
51
|
-
}
|
|
19
|
+
let returnFocus;
|
|
52
20
|
|
|
53
21
|
function close() {
|
|
54
22
|
open = false;
|
|
55
|
-
|
|
23
|
+
onclose();
|
|
56
24
|
}
|
|
57
25
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
function handleWindowClick(e) {
|
|
66
|
-
if (open && menuElement && !menuElement.contains(e.target)) {
|
|
67
|
-
close();
|
|
68
|
-
}
|
|
26
|
+
function onContextMenu(event) {
|
|
27
|
+
// Keep Control-click native for existing consumers.
|
|
28
|
+
if (event.ctrlKey) return;
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
event.stopPropagation();
|
|
31
|
+
point = { x: event.clientX, y: event.clientY };
|
|
32
|
+
open = true;
|
|
69
33
|
}
|
|
70
34
|
|
|
71
|
-
|
|
72
|
-
if (open
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
35
|
+
$effect(() => {
|
|
36
|
+
if (!open || !menuElement) return;
|
|
37
|
+
const anchor = position ?? point;
|
|
38
|
+
const element = menuElement;
|
|
39
|
+
returnFocus = document.activeElement;
|
|
40
|
+
placed = false;
|
|
41
|
+
let cancelled = false;
|
|
42
|
+
let focusFrame;
|
|
43
|
+
const onScroll = (event) => { if (!element.contains(event.target)) close(); };
|
|
44
|
+
window.addEventListener("scroll", onScroll, true);
|
|
45
|
+
tick().then(() => {
|
|
46
|
+
if (cancelled) return;
|
|
47
|
+
location = clampMenuPosition(anchor.x, anchor.y, element.offsetWidth, element.offsetHeight, window.innerWidth, window.innerHeight);
|
|
48
|
+
placed = true;
|
|
49
|
+
// Measure without inherited visibility:hidden: menu items transition
|
|
50
|
+
// "all", which can leave their visibility hidden when focus is requested.
|
|
51
|
+
focusFrame = requestAnimationFrame(() => {
|
|
52
|
+
if (!cancelled) (menuItems(element)[0] ?? element).focus();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
return () => {
|
|
56
|
+
cancelled = true;
|
|
57
|
+
cancelAnimationFrame(focusFrame);
|
|
58
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
59
|
+
if (element.contains(document.activeElement) || document.activeElement === document.body) {
|
|
60
|
+
returnFocus?.focus?.();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
setContext("context-menu", { get open() { return open; }, close });
|
|
76
66
|
</script>
|
|
77
67
|
|
|
78
68
|
<svelte:window
|
|
79
|
-
onclick={
|
|
80
|
-
|
|
81
|
-
onscroll={close}
|
|
69
|
+
onclick={(event) => { if (open && !menuElement?.contains(event.target)) close(); }}
|
|
70
|
+
onresize={() => { if (open) close(); }}
|
|
82
71
|
/>
|
|
83
72
|
|
|
84
|
-
|
|
85
|
-
class="context-menu-trigger {className}"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
>
|
|
90
|
-
{@render children?.()}
|
|
91
|
-
</div>
|
|
92
|
-
|
|
73
|
+
{#if children}
|
|
74
|
+
<div class="context-menu-trigger {className}" oncontextmenu={onContextMenu} role="presentation" {...props}>
|
|
75
|
+
{@render children()}
|
|
76
|
+
</div>
|
|
77
|
+
{/if}
|
|
93
78
|
{#if open}
|
|
94
79
|
<div
|
|
95
80
|
bind:this={menuElement}
|
|
96
81
|
class="std-menu-content context-menu-content m-0 no-rhythm"
|
|
97
|
-
style:left="{x}px"
|
|
98
|
-
style:top="{y}px"
|
|
99
|
-
style:
|
|
100
|
-
|
|
101
|
-
tabindex="-1"
|
|
82
|
+
style:left="{location.x}px"
|
|
83
|
+
style:top="{location.y}px"
|
|
84
|
+
style:opacity={placed ? 1 : 0}
|
|
85
|
+
style:pointer-events={placed ? "auto" : "none"}
|
|
86
|
+
role="menu" aria-label={label} tabindex="-1"
|
|
87
|
+
onkeydown={(event) => navigateMenu(event, menuElement, close)}
|
|
88
|
+
oncontextmenu={(event) => event.preventDefault()}
|
|
102
89
|
>
|
|
103
90
|
{@render content?.()}
|
|
104
91
|
</div>
|
|
105
92
|
{/if}
|
|
106
93
|
|
|
107
94
|
<style>
|
|
108
|
-
.context-menu-trigger {
|
|
109
|
-
display: contents;
|
|
110
|
-
}
|
|
111
|
-
|
|
95
|
+
.context-menu-trigger { display: contents; }
|
|
112
96
|
.context-menu-content {
|
|
113
97
|
position: fixed;
|
|
98
|
+
z-index: 10000;
|
|
114
99
|
min-width: var(--space-6);
|
|
100
|
+
max-width: calc(100vw - 16px);
|
|
101
|
+
max-height: calc(100vh - 16px);
|
|
102
|
+
overflow-y: auto;
|
|
115
103
|
}
|
|
116
|
-
|
|
117
|
-
:global(.context-menu-content > *) {
|
|
118
|
-
flex-shrink: 0;
|
|
119
|
-
}
|
|
104
|
+
:global(.context-menu-content > *) { flex-shrink: 0; }
|
|
120
105
|
</style>
|
package/ContextMenuItem.svelte
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import { getContext } from "svelte";
|
|
3
3
|
import Icon from "@stnd/icon/Icon.svelte";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @component ContextMenuItem
|
|
7
|
+
* @description One clickable row inside a `<ContextMenu>`'s `content` slot.
|
|
8
|
+
*/
|
|
9
|
+
|
|
5
10
|
/**
|
|
6
11
|
* @typedef {Object} ContextMenuItemProps
|
|
7
12
|
* @property {import('svelte').Snippet} [children] - Item label
|
|
@@ -13,7 +18,7 @@
|
|
|
13
18
|
* @property {() => void} [onclick] - Click handler
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
|
-
/** @type {ContextMenuItemProps} */
|
|
21
|
+
/** @type {ContextMenuItemProps & import('svelte/elements').HTMLButtonAttributes} */
|
|
17
22
|
let {
|
|
18
23
|
children,
|
|
19
24
|
label,
|
|
@@ -42,6 +47,7 @@
|
|
|
42
47
|
onclick={handleClick}
|
|
43
48
|
role="menuitem"
|
|
44
49
|
aria-disabled={disabled}
|
|
50
|
+
{disabled}
|
|
45
51
|
{...props}
|
|
46
52
|
>
|
|
47
53
|
{#if icon}
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import { getContext } from "svelte";
|
|
3
3
|
import Icon from "@stnd/icon/Icon.svelte";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @component ContextMenuItemCheckbox
|
|
7
|
+
* @description A checkable row inside a `<ContextMenu>`'s `content` slot.
|
|
8
|
+
*/
|
|
9
|
+
|
|
5
10
|
/**
|
|
6
11
|
* @typedef {Object} ContextMenuItemCheckboxProps
|
|
7
12
|
* @property {import('svelte').Snippet} [children] - Item label
|
|
@@ -42,6 +47,7 @@
|
|
|
42
47
|
role="menuitemcheckbox"
|
|
43
48
|
aria-checked={checked}
|
|
44
49
|
aria-disabled={disabled}
|
|
50
|
+
{disabled}
|
|
45
51
|
{...props}
|
|
46
52
|
>
|
|
47
53
|
<div class="item-check">
|
package/ContextMenuLabel.svelte
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
/**
|
|
3
|
+
* @component ContextMenuLabel
|
|
4
|
+
* @description A non-interactive heading row inside a `<ContextMenu>`,
|
|
5
|
+
* used to group related items.
|
|
6
|
+
*/
|
|
7
|
+
|
|
2
8
|
/**
|
|
3
9
|
* @typedef {Object} ContextMenuLabelProps
|
|
4
10
|
* @property {import('svelte').Snippet} [children] - Label content
|
package/Dialog.svelte
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { tick } from "svelte";
|
|
3
|
+
|
|
4
|
+
/** Native modal shell. Content/layout remain the caller's responsibility. */
|
|
5
|
+
let {
|
|
6
|
+
open = $bindable(false), label, children, class: className = "",
|
|
7
|
+
style = "", onclose = () => {}, onkeydown = undefined,
|
|
8
|
+
} = $props();
|
|
9
|
+
let element = $state();
|
|
10
|
+
|
|
11
|
+
function close() {
|
|
12
|
+
if (!open) return;
|
|
13
|
+
open = false;
|
|
14
|
+
onclose();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
$effect(() => {
|
|
18
|
+
if (!element || !open) return;
|
|
19
|
+
const dialog = element;
|
|
20
|
+
const previous = document.activeElement;
|
|
21
|
+
dialog.showModal();
|
|
22
|
+
return () => {
|
|
23
|
+
dialog.close();
|
|
24
|
+
tick().then(() => { if (previous?.isConnected) previous.focus?.(); });
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
30
|
+
<dialog bind:this={element} class="std-modal {className}" {style} aria-label={label}
|
|
31
|
+
oncancel={(event) => { event.preventDefault(); close(); }}
|
|
32
|
+
onclose={() => { if (element && !element.open) close(); }}
|
|
33
|
+
onkeydown={(event) => { event.stopPropagation(); onkeydown?.(event); }}
|
|
34
|
+
onclick={(event) => {
|
|
35
|
+
if (event.target !== element) return;
|
|
36
|
+
const rect = element.getBoundingClientRect();
|
|
37
|
+
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) close();
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
{@render children?.()}
|
|
41
|
+
</dialog>
|
|
42
|
+
|
|
43
|
+
<style>
|
|
44
|
+
.std-modal {
|
|
45
|
+
padding: 0;
|
|
46
|
+
margin: auto;
|
|
47
|
+
width: var(--dialog-width, min(90vw, 580px));
|
|
48
|
+
max-width: calc(100vw - 32px);
|
|
49
|
+
height: var(--dialog-height, auto);
|
|
50
|
+
max-height: var(--dialog-max-height, 88vh);
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
background: var(--color-surface-low);
|
|
53
|
+
color: var(--color-foreground);
|
|
54
|
+
border: 1px solid var(--color-border);
|
|
55
|
+
border-radius: var(--radius-lg);
|
|
56
|
+
box-shadow: var(--shadow-raised);
|
|
57
|
+
}
|
|
58
|
+
.std-modal[open] { display: flex; flex-direction: column; }
|
|
59
|
+
.std-modal::backdrop { background: rgb(0 0 0 / 0.65); backdrop-filter: blur(8px); }
|
|
60
|
+
</style>
|
package/DialogManager.svelte
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
/**
|
|
3
|
+
* @component DialogManager
|
|
4
|
+
* @description Renders confirmation dialogs triggered programmatically —
|
|
5
|
+
* mount `<DialogManagerComponent client:load />` once in your root layout
|
|
6
|
+
* (Standard does this by default), then call `confirm()` from
|
|
7
|
+
* `@stnd/ui/dialog.js` anywhere; no props, no per-dialog markup. Bridges
|
|
8
|
+
* the `dialogState` store (set by `confirm()`) to an `<AlertDialog>`.
|
|
9
|
+
*
|
|
10
|
+
* @example js
|
|
11
|
+
* import { confirm } from "@stnd/ui/dialog.js";
|
|
12
|
+
* const ok = await confirm({ title: "Delete this?", intent: "danger" });
|
|
13
|
+
*/
|
|
2
14
|
import AlertDialog from "./AlertDialog.svelte";
|
|
3
15
|
import { dialogState } from "./dialog.js";
|
|
4
16
|
|