staffa 0.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.
Files changed (53) hide show
  1. package/README.md +186 -0
  2. package/dist/components/autocomplete.d.ts +44 -0
  3. package/dist/components/autocomplete.js +250 -0
  4. package/dist/components/box.d.ts +31 -0
  5. package/dist/components/box.js +48 -0
  6. package/dist/components/button.d.ts +67 -0
  7. package/dist/components/button.js +83 -0
  8. package/dist/components/buttonGroup.d.ts +31 -0
  9. package/dist/components/buttonGroup.js +46 -0
  10. package/dist/components/checkbox.d.ts +24 -0
  11. package/dist/components/checkbox.js +63 -0
  12. package/dist/components/dialog.d.ts +97 -0
  13. package/dist/components/dialog.js +214 -0
  14. package/dist/components/field.d.ts +50 -0
  15. package/dist/components/field.js +78 -0
  16. package/dist/components/form.d.ts +42 -0
  17. package/dist/components/form.js +59 -0
  18. package/dist/components/main.d.ts +47 -0
  19. package/dist/components/main.js +89 -0
  20. package/dist/components/modal.d.ts +2 -0
  21. package/dist/components/modal.js +2 -0
  22. package/dist/components/select.d.ts +27 -0
  23. package/dist/components/select.js +57 -0
  24. package/dist/components/tabs.d.ts +41 -0
  25. package/dist/components/tabs.js +108 -0
  26. package/dist/components/textarea.d.ts +31 -0
  27. package/dist/components/textarea.js +49 -0
  28. package/dist/components/textline.d.ts +38 -0
  29. package/dist/components/textline.js +32 -0
  30. package/dist/core.d.ts +73 -0
  31. package/dist/core.js +18 -0
  32. package/dist/index.d.ts +83 -0
  33. package/dist/index.js +72 -0
  34. package/dist/skye.esm.js +1 -0
  35. package/dist/theme.d.ts +87 -0
  36. package/dist/theme.js +135 -0
  37. package/package.json +35 -0
  38. package/src/components/autocomplete.ts +272 -0
  39. package/src/components/box.ts +62 -0
  40. package/src/components/button.ts +137 -0
  41. package/src/components/buttonGroup.ts +63 -0
  42. package/src/components/checkbox.ts +70 -0
  43. package/src/components/dialog.ts +257 -0
  44. package/src/components/field.ts +115 -0
  45. package/src/components/form.ts +84 -0
  46. package/src/components/main.ts +110 -0
  47. package/src/components/select.ts +75 -0
  48. package/src/components/tabs.ts +144 -0
  49. package/src/components/textarea.ts +68 -0
  50. package/src/components/textline.ts +66 -0
  51. package/src/core.ts +88 -0
  52. package/src/index.ts +98 -0
  53. package/src/theme.ts +195 -0
@@ -0,0 +1,83 @@
1
+ import A from "aberdeen";
2
+ import { drawSlot } from "../core.js";
3
+ // The color role sets a local `--c` (and `--cfg` for text on filled); the
4
+ // variant rules consume them, so we avoid writing colour×variant rules.
5
+ A.insertGlobalCss({
6
+ ".S_btn": {
7
+ "&": "--c:$sPrimary --cfg:$sPrimaryFg " +
8
+ "display:inline-flex align-items:center justify-content:center gap:$2 " +
9
+ "font-weight:600 line-height:1.2 white-space:nowrap cursor:pointer text-decoration:none " +
10
+ "border: 1px solid transparent; r:$sRadius padding: 0.5em 1em; " +
11
+ "transition: background 0.15s, border-color 0.15s, filter 0.15s, box-shadow 0.15s;",
12
+ "&:focus-visible": "outline:none box-shadow: 0 0 0 3px $sFocus;",
13
+ "&:disabled, &[aria-disabled=true]": "opacity:0.45 cursor:not-allowed pointer-events:none filter:saturate(0.6)",
14
+ // Colour roles.
15
+ "&.S_neutral": "--c:$sBorderStrong --cfg:$sFg",
16
+ "&.S_danger": "--c:$sDanger --cfg:#fff",
17
+ "&.S_success": "--c:$sSuccess --cfg:#08110d",
18
+ // Variants.
19
+ "&.S_filled": "background:$c color:$cfg border-color:$c",
20
+ "&.S_filled:hover": "filter:brightness(1.1)",
21
+ "&.S_tonal": "color:$c background: color-mix(in srgb, $c 20%, transparent); border-color: color-mix(in srgb, $c 30%, transparent);",
22
+ "&.S_tonal:hover": "background: color-mix(in srgb, $c 30%, transparent);",
23
+ "&.S_outlined": "color:$c background:transparent border-color: color-mix(in srgb, $c 55%, $sBorder);",
24
+ "&.S_outlined:hover": "background: color-mix(in srgb, $c 12%, transparent);",
25
+ // Sizes.
26
+ "&.S_sm": "padding: 0.32em 0.7em; font-size:0.85em",
27
+ "&.S_lg": "padding: 0.66em 1.3em; font-size:1.1em",
28
+ },
29
+ });
30
+ /**
31
+ * A button. Always carries at least a visible border so its affordance is
32
+ * obvious at a glance, regardless of {@link ButtonVariant | variant}.
33
+ *
34
+ * Shortcut: pass a string to use it as the label, or a function for custom
35
+ * content.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * S.button({ text: "Save", click: save });
40
+ * S.button({ text: "Delete", color: "danger", variant: "outlined", click: del });
41
+ * S.button("Cancel"); // shorthand for { text: "Cancel" }
42
+ * S.button({ href: "/docs", text: "Docs" }); // renders an <a role=button>
43
+ * ```
44
+ */
45
+ export function button(opts = {}) {
46
+ const o = typeof opts === "string" ? { text: opts } : typeof opts === "function" ? { content: opts } : opts;
47
+ const tag = o.href != null ? "a" : "button";
48
+ const variant = o.variant ?? "filled";
49
+ const color = o.color ?? "primary";
50
+ const size = o.size === "sm" || o.size === "lg" ? `.S_${o.size}` : "";
51
+ // Semantic roles select a colour class (the CSS sets `--c`/`--cfg`); any other
52
+ // value is a raw CSS colour we assign to `--c`, which the variant rules consume
53
+ // via `var(--c)`. (`primary` is the base default — its class is a no-op.)
54
+ const semantic = color === "primary" || color === "neutral" || color === "danger" || color === "success";
55
+ const colorCls = semantic ? `.S_${color}` : "";
56
+ const el = A(`${tag}.S_btn.S_${variant}${colorCls}${size}`, o.root, o.inner, () => {
57
+ if (o.href != null) {
58
+ A(`href=${o.href} role=button`);
59
+ if (o.disabled)
60
+ A("aria-disabled=true");
61
+ }
62
+ else {
63
+ A("type=", o.type ?? "button");
64
+ if (o.disabled)
65
+ A("disabled=true");
66
+ }
67
+ if (o.ariaLabel)
68
+ A("aria-label=", o.ariaLabel);
69
+ if (o.click)
70
+ A("click=", o.click);
71
+ drawSlot(o.icon);
72
+ if (o.content)
73
+ o.content();
74
+ else if (o.text != null)
75
+ A("#", o.text);
76
+ });
77
+ // Aberdeen's inline styler doesn't set CSS custom properties, so assign the
78
+ // custom accent on the element directly. A leading `$` is Aberdeen's shorthand
79
+ // for a CSS variable reference, so expand it to `var(--name)`.
80
+ if (!semantic && el instanceof HTMLElement) {
81
+ el.style.setProperty("--c", color.startsWith("$") ? `var(--${color.slice(1)})` : color);
82
+ }
83
+ }
@@ -0,0 +1,31 @@
1
+ import type { ContentOptions } from "../core.js";
2
+ import { type ButtonOptions } from "./button.js";
3
+ /** Options for {@link buttonGroup}. */
4
+ export interface ButtonGroupOptions extends ContentOptions {
5
+ /**
6
+ * Declarative list of buttons. Rendered in order. Alternatively (or
7
+ * additionally) draw buttons yourself via {@link ContentOptions.content}.
8
+ */
9
+ buttons?: ButtonOptions[];
10
+ /**
11
+ * `"attached"` (default) joins the buttons into a single segmented control
12
+ * with shared borders; `"spaced"` lays them out with a normal gap.
13
+ */
14
+ layout?: "attached" | "spaced";
15
+ /** Stack vertically instead of horizontally. */
16
+ vertical?: boolean;
17
+ }
18
+ /**
19
+ * Groups related buttons, either as a joined segmented control (`attached`) or
20
+ * spaced out. A `role=group` is applied for assistive tech.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * S.buttonGroup({ buttons: [
25
+ * { text: "Day", variant: "outlined", color: "neutral" },
26
+ * { text: "Week", variant: "outlined", color: "neutral" },
27
+ * { text: "Month", variant: "outlined", color: "neutral" },
28
+ * ]});
29
+ * ```
30
+ */
31
+ export declare function buttonGroup(opts?: ButtonGroupOptions): void;
@@ -0,0 +1,46 @@
1
+ import A from "aberdeen";
2
+ import { button } from "./button.js";
3
+ A.insertGlobalCss({
4
+ ".S_bgroup": {
5
+ "&": "display:inline-flex align-items:stretch",
6
+ "&.S_spaced": "gap:$2 flex-wrap:wrap",
7
+ "&.S_vertical": "flex-direction:column",
8
+ "&.S_attached": "gap:0",
9
+ // When attached, collapse the shared border and square off the touching
10
+ // corners, keeping only the outer ends of the group rounded.
11
+ "&.S_attached:not(.S_vertical) > .S_btn:not(:first-child)": "margin-left:-1px",
12
+ "&.S_attached:not(.S_vertical) > .S_btn:not(:first-child):not(:last-child)": "r:0",
13
+ "&.S_attached:not(.S_vertical) > .S_btn:first-child:not(:last-child)": "border-top-right-radius:0 border-bottom-right-radius:0",
14
+ "&.S_attached:not(.S_vertical) > .S_btn:last-child:not(:first-child)": "border-top-left-radius:0 border-bottom-left-radius:0",
15
+ "&.S_attached.S_vertical > .S_btn:not(:first-child)": "margin-top:-1px",
16
+ "&.S_attached.S_vertical > .S_btn:not(:first-child):not(:last-child)": "r:0",
17
+ "&.S_attached.S_vertical > .S_btn:first-child:not(:last-child)": "border-bottom-left-radius:0 border-bottom-right-radius:0",
18
+ "&.S_attached.S_vertical > .S_btn:last-child:not(:first-child)": "border-top-left-radius:0 border-top-right-radius:0",
19
+ // Keep the hovered/focused button's border above its neighbours.
20
+ "&.S_attached > .S_btn:hover, &.S_attached > .S_btn:focus-visible": "z-index:1",
21
+ },
22
+ });
23
+ /**
24
+ * Groups related buttons, either as a joined segmented control (`attached`) or
25
+ * spaced out. A `role=group` is applied for assistive tech.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * S.buttonGroup({ buttons: [
30
+ * { text: "Day", variant: "outlined", color: "neutral" },
31
+ * { text: "Week", variant: "outlined", color: "neutral" },
32
+ * { text: "Month", variant: "outlined", color: "neutral" },
33
+ * ]});
34
+ * ```
35
+ */
36
+ export function buttonGroup(opts = {}) {
37
+ const layout = opts.layout ?? "attached";
38
+ const cls = `.S_${layout}${opts.vertical ? ".S_vertical" : ""}`;
39
+ A(`div.S_bgroup${cls} role=group`, opts.root, opts.inner, () => {
40
+ if (opts.buttons)
41
+ for (const b of opts.buttons)
42
+ button(b);
43
+ if (opts.content)
44
+ opts.content();
45
+ });
46
+ }
@@ -0,0 +1,24 @@
1
+ import { type Bindable, type Slot } from "../core.js";
2
+ import type { FieldOptions } from "./field.js";
3
+ /** Options for {@link checkbox}. */
4
+ export interface CheckboxOptions extends Omit<FieldOptions, "label"> {
5
+ /** The label shown next to the box. Required for a meaningful checkbox. */
6
+ label?: Slot;
7
+ /** Two-way binding target holding a boolean. */
8
+ bind?: Bindable<boolean>;
9
+ /** Static initial checked state. */
10
+ checked?: boolean;
11
+ /** Fired on `change` with the native event. */
12
+ change?: (event: Event) => void;
13
+ }
14
+ /**
15
+ * A checkbox with an associated, clickable label. Uses the native `<input
16
+ * type=checkbox>` (styled with `accent-color`) for full keyboard and screen
17
+ * reader support.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * S.checkbox({ label: "Subscribe to newsletter", bind: A.ref($prefs, "newsletter") });
22
+ * ```
23
+ */
24
+ export declare function checkbox(opts?: CheckboxOptions): void;
@@ -0,0 +1,63 @@
1
+ import A from "aberdeen";
2
+ import { drawSlot, uniqueId } from "../core.js";
3
+ A.insertGlobalCss({
4
+ ".S_check": {
5
+ "&": "display:flex flex-direction:column gap:$1",
6
+ "> label": "display:flex align-items:center gap:$2 cursor:pointer user-select:none",
7
+ "> label:has(input:disabled)": "cursor:not-allowed opacity:0.6",
8
+ // Native control styled with accent-color: accessible and zero-fuss.
9
+ "input": "width:1.15em height:1.15em accent-color:$sPrimary cursor:inherit m:0",
10
+ },
11
+ });
12
+ /**
13
+ * A checkbox with an associated, clickable label. Uses the native `<input
14
+ * type=checkbox>` (styled with `accent-color`) for full keyboard and screen
15
+ * reader support.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * S.checkbox({ label: "Subscribe to newsletter", bind: A.ref($prefs, "newsletter") });
20
+ * ```
21
+ */
22
+ export function checkbox(opts = {}) {
23
+ const id = opts.id ?? uniqueId("check");
24
+ A("div.S_check", opts.root, () => {
25
+ A(`label for=${id}`, () => {
26
+ A("input type=checkbox", opts.control, () => {
27
+ A(`id=${id}`);
28
+ if (opts.name)
29
+ A(`name=${opts.name}`);
30
+ // `checked` is a boolean attribute: only set it when actually true.
31
+ if (opts.checked && !opts.bind)
32
+ A("checked=true");
33
+ if (opts.change)
34
+ A("change=", opts.change);
35
+ A(() => {
36
+ if (opts.disabled)
37
+ A("disabled=true");
38
+ });
39
+ A(() => {
40
+ if (opts.required)
41
+ A("aria-required=true");
42
+ });
43
+ if (opts.bind)
44
+ A("bind=", opts.bind);
45
+ });
46
+ // Own scope so the label text/required marker don't recreate the input.
47
+ A(() => {
48
+ if (opts.label != null)
49
+ drawSlot(opts.label);
50
+ if (opts.required)
51
+ A("span.S_req aria-hidden=true #*");
52
+ });
53
+ });
54
+ A(() => {
55
+ if (opts.help != null && !opts.error)
56
+ A("div.S_help", () => drawSlot(opts.help));
57
+ });
58
+ A(() => {
59
+ if (opts.error)
60
+ A("div.S_error role=alert #", opts.error);
61
+ });
62
+ });
63
+ }
@@ -0,0 +1,97 @@
1
+ import { type Slot, type Styling } from "../core.js";
2
+ /** Options for {@link dialog}. */
3
+ export interface DialogOptions {
4
+ /** Slot rendered in the styled header bar. */
5
+ header?: Slot;
6
+ /** Slot rendered in the styled footer bar. */
7
+ footer?: Slot;
8
+ /** Aberdeen attr/style string applied to the header bar. */
9
+ headerInner?: Styling;
10
+ /** Aberdeen attr/style string applied to the footer bar. */
11
+ footerInner?: Styling;
12
+ /** Aberdeen attr/style string applied to the scrollable content `<div>`. */
13
+ inner?: Styling;
14
+ /** Aberdeen attr/style string applied to the dialog panel itself. */
15
+ root?: Styling;
16
+ /**
17
+ * Allow closing via Esc or clicking the backdrop. Defaults to `true`.
18
+ * May be changed on a proxied options object while the dialog is open
19
+ * (e.g. lock when form data is dirty).
20
+ */
21
+ allowCancel?: boolean;
22
+ /**
23
+ * Dialog body. Receives a `close()` function — call it to dismiss the dialog
24
+ * programmatically.
25
+ */
26
+ content?: (close: () => void) => void;
27
+ /**
28
+ * Called when the dialog closes for any reason (explicit `close()`, Esc, or
29
+ * backdrop click). Useful when you want a side-effect on close but don't need
30
+ * the Promise returned by {@link dialog}.
31
+ */
32
+ onClose?: () => void;
33
+ }
34
+ /**
35
+ * A dialog rendered into `document.body` via `A.mount`, with a dimming backdrop
36
+ * that fades in and out. Returns a `Promise<void>` that resolves when the dialog
37
+ * closes. Lifecycle is also tied to the parent reactive scope — when that scope
38
+ * is cleaned up the dialog disappears and the promise resolves.
39
+ *
40
+ * Only the **last** open dialog (and its backdrop) is visible; earlier pairs are
41
+ * hidden via the CSS `+` selector, so nested dialogs stack correctly.
42
+ *
43
+ * The header and footer are pinned; only the body content scrolls when it is
44
+ * taller than `88vh`.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * S.dialog({
49
+ * header: "Confirm",
50
+ * content: (close) => {
51
+ * A("p #Are you sure?");
52
+ * S.button({ text: "Yes", click: () => { doIt(); close(); } });
53
+ * S.button({ text: "Cancel", variant: "outlined", click: close });
54
+ * },
55
+ * });
56
+ * ```
57
+ */
58
+ export declare function dialog(opts: DialogOptions): Promise<void>;
59
+ /**
60
+ * Shows a message dialog with a single OK button. Returns a `Promise<void>`
61
+ * that resolves when the user dismisses it.
62
+ *
63
+ * All properties of `opts` override the defaults, including `content`.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * await S.alert("File saved successfully.");
68
+ * ```
69
+ */
70
+ export declare function alert(message: string, opts?: Partial<DialogOptions>): Promise<void>;
71
+ /**
72
+ * Shows a confirmation dialog with Cancel and OK buttons. Returns a
73
+ * `Promise<boolean>` — `true` if the user clicked OK, `false` otherwise
74
+ * (including Esc / backdrop click when `allowCancel` is not `false`).
75
+ *
76
+ * All properties of `opts` override the defaults, including `content`.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * if (await S.confirm("Delete this item?")) deleteItem();
81
+ * ```
82
+ */
83
+ export declare function confirm(message: string, opts?: Partial<DialogOptions>): Promise<boolean>;
84
+ /**
85
+ * Shows a prompt dialog with a text input. Returns a `Promise<string | null>` —
86
+ * the entered string if the user confirmed, or `null` if cancelled (Esc /
87
+ * backdrop click / Cancel button).
88
+ *
89
+ * All properties of `opts` override the defaults, including `content`.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * const name = await S.prompt("Enter your name:", "Alice");
94
+ * if (name !== null) greet(name);
95
+ * ```
96
+ */
97
+ export declare function prompt(message: string, defaultValue?: string, opts?: Partial<DialogOptions>): Promise<string | null>;
@@ -0,0 +1,214 @@
1
+ import A from "aberdeen";
2
+ import { drawSlot } from "../core.js";
3
+ import { button } from "./button.js";
4
+ import { buttonGroup } from "./buttonGroup.js";
5
+ import { textline } from "./textline.js";
6
+ // Transition helper classes.
7
+ // `.S_backdrop` = backdrop, hidden when another backdrop follows it in the DOM.
8
+ // `.S_dialog` = dialog box, slides + fades in/out.
9
+ A.insertGlobalCss({
10
+ ".S_backdrop": {
11
+ "&": "position:fixed inset:0 z-index:200; background: rgba(0,0,0,0.55); transition: opacity 0.2s ease;",
12
+ "&:not(:has(~ .S_backdrop))": "display:block",
13
+ "&:not(:has(~ .S_backdrop)) + .S_dialog": "display:flex flex-direction:column",
14
+ // Transition states: applied momentarily on create; re-applied on destroy.
15
+ "&.hidden": "opacity:0 pointer-events:none",
16
+ },
17
+ ".S_dialog": {
18
+ "&": "position:fixed z-index:201 top:50% left:50% " +
19
+ "transform:translate(-50%,-50%) " +
20
+ "min-width:20rem max-width:min(90vw,44rem) max-height:min(88vh,800px) " +
21
+ "bg:$sSurface border: 1px solid $sBorder; r:$sRadiusLg box-shadow:$sShadow overflow:hidden " +
22
+ "transition: opacity 0.2s ease, transform 0.2s ease;",
23
+ // Header and footer are fixed; only the content <div> scrolls.
24
+ "> header": "display:flex align-items:center gap:$2 padding: $2 $3; " +
25
+ "bg:$sSurfaceHi border-bottom: 1px solid $sBorder; font-weight:600 flex-shrink:0",
26
+ "> footer": "display:flex align-items:center gap:$2 padding: $2 $3; " +
27
+ "bg:$sSurfaceHi border-top: 1px solid $sBorder; flex-shrink:0",
28
+ "> div": "p:$3 gap:$3 display:flex flex-direction:column overflow-y:auto flex:1 min-height:0",
29
+ "&.hidden": "opacity:0 pointer-events:none transform: translate(-50%, calc(-50% + 20px));",
30
+ "&.hidden *": "pointer-events:none",
31
+ },
32
+ });
33
+ /**
34
+ * A dialog rendered into `document.body` via `A.mount`, with a dimming backdrop
35
+ * that fades in and out. Returns a `Promise<void>` that resolves when the dialog
36
+ * closes. Lifecycle is also tied to the parent reactive scope — when that scope
37
+ * is cleaned up the dialog disappears and the promise resolves.
38
+ *
39
+ * Only the **last** open dialog (and its backdrop) is visible; earlier pairs are
40
+ * hidden via the CSS `+` selector, so nested dialogs stack correctly.
41
+ *
42
+ * The header and footer are pinned; only the body content scrolls when it is
43
+ * taller than `88vh`.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * S.dialog({
48
+ * header: "Confirm",
49
+ * content: (close) => {
50
+ * A("p #Are you sure?");
51
+ * S.button({ text: "Yes", click: () => { doIt(); close(); } });
52
+ * S.button({ text: "Cancel", variant: "outlined", click: close });
53
+ * },
54
+ * });
55
+ * ```
56
+ */
57
+ export function dialog(opts) {
58
+ return new Promise((resolve) => {
59
+ const $closed = A.proxy(false);
60
+ const close = () => { $closed.value = true; };
61
+ let resolved = false;
62
+ const onDone = () => {
63
+ if (resolved)
64
+ return;
65
+ resolved = true;
66
+ opts.onClose?.();
67
+ resolve();
68
+ };
69
+ // A.mount ties this scope to the calling reactive scope — when the parent
70
+ // scope is torn down, the backdrop and dialog are removed from body too.
71
+ A.mount(document.body, () => {
72
+ // The 'peek' is there such that when 'closed' is first set, this scope doesn't need to watch anything anymore.
73
+ if (A.peek($closed, "value"), $closed.value)
74
+ return;
75
+ // Global Esc listener — registered here so it's removed on close.
76
+ const onKey = (e) => {
77
+ if (e.key === "Escape" && opts.allowCancel !== false)
78
+ close();
79
+ };
80
+ document.addEventListener("keydown", onKey);
81
+ A.clean(() => {
82
+ document.removeEventListener("keydown", onKey);
83
+ // Fires when this render is torn down — either because $closed became
84
+ // true (normal close) or because the parent reactive scope was cleaned up.
85
+ onDone();
86
+ });
87
+ // Backdrop: fades in on creation, fades out on removal.
88
+ A("div.S_backdrop create=hidden destroy=hidden", () => {
89
+ A("click=", () => {
90
+ if (opts.allowCancel !== false)
91
+ close();
92
+ });
93
+ });
94
+ // Dialog panel: fades + slides in/out.
95
+ A("div.S_dialog create=hidden destroy=hidden", opts.root, () => {
96
+ A(() => {
97
+ if (opts.header != null) {
98
+ A("header", opts.headerInner, () => drawSlot(opts.header));
99
+ }
100
+ });
101
+ A("div", opts.inner, () => {
102
+ if (opts.content)
103
+ opts.content(close);
104
+ });
105
+ A(() => {
106
+ if (opts.footer != null) {
107
+ A("footer", opts.footerInner, () => drawSlot(opts.footer));
108
+ }
109
+ });
110
+ });
111
+ });
112
+ });
113
+ }
114
+ /**
115
+ * Shows a message dialog with a single OK button. Returns a `Promise<void>`
116
+ * that resolves when the user dismisses it.
117
+ *
118
+ * All properties of `opts` override the defaults, including `content`.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * await S.alert("File saved successfully.");
123
+ * ```
124
+ */
125
+ export function alert(message, opts = {}) {
126
+ return dialog({
127
+ header: "Alert",
128
+ allowCancel: true,
129
+ content: (close) => {
130
+ A("p", () => { A("#", message); });
131
+ buttonGroup({ layout: "spaced", root: "align-self:flex-end", content: () => {
132
+ button({ text: "OK", click: close });
133
+ } });
134
+ },
135
+ ...opts,
136
+ });
137
+ }
138
+ /**
139
+ * Shows a confirmation dialog with Cancel and OK buttons. Returns a
140
+ * `Promise<boolean>` — `true` if the user clicked OK, `false` otherwise
141
+ * (including Esc / backdrop click when `allowCancel` is not `false`).
142
+ *
143
+ * All properties of `opts` override the defaults, including `content`.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * if (await S.confirm("Delete this item?")) deleteItem();
148
+ * ```
149
+ */
150
+ export function confirm(message, opts = {}) {
151
+ return new Promise((resolve) => {
152
+ let confirmed = false;
153
+ dialog({
154
+ header: "Confirm",
155
+ allowCancel: true,
156
+ content: (close) => {
157
+ A("p", () => { A("#", message); });
158
+ buttonGroup({ layout: "spaced", root: "align-self:flex-end", content: () => {
159
+ button({ text: "Cancel", variant: "outlined", color: "neutral", click: close });
160
+ button({ text: "OK", click: () => { confirmed = true; close(); } });
161
+ } });
162
+ },
163
+ ...opts,
164
+ onClose: () => {
165
+ resolve(confirmed);
166
+ opts.onClose?.();
167
+ },
168
+ });
169
+ });
170
+ }
171
+ /**
172
+ * Shows a prompt dialog with a text input. Returns a `Promise<string | null>` —
173
+ * the entered string if the user confirmed, or `null` if cancelled (Esc /
174
+ * backdrop click / Cancel button).
175
+ *
176
+ * All properties of `opts` override the defaults, including `content`.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * const name = await S.prompt("Enter your name:", "Alice");
181
+ * if (name !== null) greet(name);
182
+ * ```
183
+ */
184
+ export function prompt(message, defaultValue = "", opts = {}) {
185
+ return new Promise((resolve) => {
186
+ let result = null;
187
+ dialog({
188
+ header: "Input",
189
+ allowCancel: true,
190
+ content: (close) => {
191
+ A("p", () => { A("#", message); });
192
+ const $v = A.proxy({ value: defaultValue });
193
+ // Wrap in a form so Enter submits; display:contents keeps flex layout intact.
194
+ A("form display:contents", () => {
195
+ A("submit=", (e) => {
196
+ e.preventDefault();
197
+ result = $v.value;
198
+ close();
199
+ });
200
+ textline({ bind: A.ref($v, "value") });
201
+ buttonGroup({ layout: "spaced", root: "align-self:flex-end", content: () => {
202
+ button({ text: "Cancel", variant: "outlined", color: "neutral", type: "button", click: close });
203
+ button({ text: "OK", type: "submit" });
204
+ } });
205
+ });
206
+ },
207
+ ...opts,
208
+ onClose: () => {
209
+ resolve(result);
210
+ opts.onClose?.();
211
+ },
212
+ });
213
+ });
214
+ }
@@ -0,0 +1,50 @@
1
+ import { type BaseOptions, type Bindable, type Slot, type Styling } from "../core.js";
2
+ /**
3
+ * Options shared by all *form field* components (textline, textarea, checkbox,
4
+ * autocomplete, ...).
5
+ *
6
+ * Fields share a consistent vertical layout: an optional label, the control
7
+ * itself, and optional help/error text below it. {@link form} relies on this
8
+ * shared structure to align groups of fields.
9
+ */
10
+ export interface FieldOptions extends BaseOptions {
11
+ /** Visible label, associated with the control via `for`/`id` for a11y. */
12
+ label?: Slot;
13
+ /** Helper text shown beneath the control. */
14
+ help?: Slot;
15
+ /**
16
+ * Error message shown beneath the control. When set, the control is marked
17
+ * `aria-invalid` and styled accordingly. May be reactive.
18
+ */
19
+ error?: string;
20
+ /** Disables the control. */
21
+ disabled?: boolean;
22
+ /** Marks the field required (adds a `*` and the `aria-required` attribute). */
23
+ required?: boolean;
24
+ /** The `name` attribute, for native form submission. */
25
+ name?: string;
26
+ /** Explicit id for the control; auto-generated when omitted. */
27
+ id?: string;
28
+ /** Aberdeen attr/style string applied to the control element itself. */
29
+ control?: Styling;
30
+ }
31
+ /**
32
+ * Render the standard field chrome (label + control + help/error) around a
33
+ * caller-supplied control.
34
+ *
35
+ * Each piece is read inside its own small reactive scope, so e.g. flipping
36
+ * `error` on a proxied options object only re-renders the error line — not the
37
+ * control.
38
+ *
39
+ * @param opts The field options.
40
+ * @param drawControl Receives the resolved `id` and the live "invalid" getter,
41
+ * and must draw the actual control element (using class `S_input` where
42
+ * appropriate, and passing `opts.control` as an arg for caller styling).
43
+ */
44
+ export declare function drawField(opts: FieldOptions, drawControl: (id: string, isInvalid: () => boolean) => void): void;
45
+ /**
46
+ * Apply the shared, reactive control attributes (`id`, `name`, `disabled`,
47
+ * `required`, `aria-invalid`, `bind`) to the current element. The dynamic ones
48
+ * each get their own scope so the control element is never recreated.
49
+ */
50
+ export declare function applyControlAttrs(opts: FieldOptions, id: string, isInvalid: () => boolean, bind?: Bindable<unknown>): void;