staffa 0.1.0 → 0.2.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/README.md +65 -27
- package/dist/components/autocomplete.js +20 -20
- package/dist/components/box.d.ts +8 -6
- package/dist/components/box.js +15 -11
- package/dist/components/button.d.ts +10 -33
- package/dist/components/button.js +17 -38
- package/dist/components/buttonChooser.d.ts +42 -0
- package/dist/components/buttonChooser.js +38 -0
- package/dist/components/buttonGroup.d.ts +3 -3
- package/dist/components/buttonGroup.js +18 -18
- package/dist/components/checkbox.js +7 -7
- package/dist/components/dialog.d.ts +20 -25
- package/dist/components/dialog.js +81 -91
- package/dist/components/field.d.ts +8 -6
- package/dist/components/field.js +18 -17
- package/dist/components/form.d.ts +3 -3
- package/dist/components/form.js +4 -4
- package/dist/components/main.d.ts +7 -5
- package/dist/components/main.js +25 -23
- package/dist/components/select.d.ts +1 -1
- package/dist/components/select.js +5 -5
- package/dist/components/tabs.d.ts +5 -3
- package/dist/components/tabs.js +25 -19
- package/dist/components/textarea.js +4 -4
- package/dist/components/textline.js +1 -1
- package/dist/core.d.ts +26 -39
- package/dist/core.js +6 -5
- package/dist/index.d.ts +10 -8
- package/dist/index.js +9 -8
- package/dist/staffa.esm.js +1 -0
- package/dist/theme.d.ts +9 -75
- package/dist/theme.js +178 -82
- package/package.json +3 -2
- package/src/components/autocomplete.ts +20 -20
- package/src/components/box.ts +20 -14
- package/src/components/button.ts +24 -72
- package/src/components/buttonChooser.ts +66 -0
- package/src/components/buttonGroup.ts +18 -18
- package/src/components/checkbox.ts +7 -7
- package/src/components/dialog.ts +101 -102
- package/src/components/field.ts +24 -21
- package/src/components/form.ts +7 -7
- package/src/components/main.ts +30 -26
- package/src/components/select.ts +4 -4
- package/src/components/tabs.ts +29 -22
- package/src/components/textarea.ts +4 -4
- package/src/components/textline.ts +1 -1
- package/src/core.ts +26 -40
- package/src/index.ts +10 -8
- package/src/theme.ts +190 -135
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
import { button } from "./button.js";
|
|
3
3
|
A.insertGlobalCss({
|
|
4
|
-
".
|
|
4
|
+
".s-bgroup": {
|
|
5
5
|
"&": "display:inline-flex align-items:stretch",
|
|
6
|
-
"&.
|
|
7
|
-
"&.
|
|
8
|
-
"&.
|
|
6
|
+
"&.s-spaced": "gap:$2 flex-wrap:wrap",
|
|
7
|
+
"&.s-vertical": "flex-direction:column",
|
|
8
|
+
"&.s-attached": "gap:0",
|
|
9
9
|
// When attached, collapse the shared border and square off the touching
|
|
10
10
|
// corners, keeping only the outer ends of the group rounded.
|
|
11
|
-
"&.
|
|
12
|
-
"&.
|
|
13
|
-
"&.
|
|
14
|
-
"&.
|
|
15
|
-
"&.
|
|
16
|
-
"&.
|
|
17
|
-
"&.
|
|
18
|
-
"&.
|
|
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
19
|
// Keep the hovered/focused button's border above its neighbours.
|
|
20
|
-
"&.
|
|
20
|
+
"&.s-attached > .s-btn:hover, &.s-attached > .s-btn:focus-visible": "z-index:1",
|
|
21
21
|
},
|
|
22
22
|
});
|
|
23
23
|
/**
|
|
@@ -27,16 +27,16 @@ A.insertGlobalCss({
|
|
|
27
27
|
* @example
|
|
28
28
|
* ```ts
|
|
29
29
|
* S.buttonGroup({ buttons: [
|
|
30
|
-
* { text: "Day",
|
|
31
|
-
* { text: "Week",
|
|
32
|
-
* { text: "Month",
|
|
30
|
+
* { text: "Day", attrs: ".neutral .outlined" },
|
|
31
|
+
* { text: "Week", attrs: ".neutral .outlined" },
|
|
32
|
+
* { text: "Month", attrs: ".neutral .outlined" },
|
|
33
33
|
* ]});
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
36
|
export function buttonGroup(opts = {}) {
|
|
37
37
|
const layout = opts.layout ?? "attached";
|
|
38
|
-
const cls = `.
|
|
39
|
-
A(`div.
|
|
38
|
+
const cls = `.s-${layout}${opts.vertical ? ".s-vertical" : ""}`;
|
|
39
|
+
A(`div.s-bgroup${cls} role=group`, opts.attrs, () => {
|
|
40
40
|
if (opts.buttons)
|
|
41
41
|
for (const b of opts.buttons)
|
|
42
42
|
button(b);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
import { drawSlot, uniqueId } from "../core.js";
|
|
3
3
|
A.insertGlobalCss({
|
|
4
|
-
".
|
|
4
|
+
".s-check": {
|
|
5
5
|
"&": "display:flex flex-direction:column gap:$1",
|
|
6
6
|
"> label": "display:flex align-items:center gap:$2 cursor:pointer user-select:none",
|
|
7
7
|
"> label:has(input:disabled)": "cursor:not-allowed opacity:0.6",
|
|
8
8
|
// Native control styled with accent-color: accessible and zero-fuss.
|
|
9
|
-
"input": "width:1.15em height:1.15em accent-color:$
|
|
9
|
+
"input": "width:1.15em height:1.15em accent-color:$s-accent cursor:inherit m:0",
|
|
10
10
|
},
|
|
11
11
|
});
|
|
12
12
|
/**
|
|
@@ -21,9 +21,9 @@ A.insertGlobalCss({
|
|
|
21
21
|
*/
|
|
22
22
|
export function checkbox(opts = {}) {
|
|
23
23
|
const id = opts.id ?? uniqueId("check");
|
|
24
|
-
A("div.
|
|
24
|
+
A("div.s-check", opts.attrs, () => {
|
|
25
25
|
A(`label for=${id}`, () => {
|
|
26
|
-
A("input type=checkbox", opts.
|
|
26
|
+
A("input type=checkbox", opts.inputAttrs, () => {
|
|
27
27
|
A(`id=${id}`);
|
|
28
28
|
if (opts.name)
|
|
29
29
|
A(`name=${opts.name}`);
|
|
@@ -48,16 +48,16 @@ export function checkbox(opts = {}) {
|
|
|
48
48
|
if (opts.label != null)
|
|
49
49
|
drawSlot(opts.label);
|
|
50
50
|
if (opts.required)
|
|
51
|
-
A("span.
|
|
51
|
+
A("span.s-req aria-hidden=true #*");
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
54
|
A(() => {
|
|
55
55
|
if (opts.help != null && !opts.error)
|
|
56
|
-
A("div.
|
|
56
|
+
A("div.s-help", () => drawSlot(opts.help));
|
|
57
57
|
});
|
|
58
58
|
A(() => {
|
|
59
59
|
if (opts.error)
|
|
60
|
-
A("div.
|
|
60
|
+
A("div.s-error role=alert #", opts.error);
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { type Slot, type
|
|
1
|
+
import { type Slot, type Attributes } from "../core.js";
|
|
2
2
|
/** Options for {@link dialog}. */
|
|
3
3
|
export interface DialogOptions {
|
|
4
4
|
/** Slot rendered in the styled header bar. */
|
|
5
5
|
header?: Slot;
|
|
6
6
|
/** Slot rendered in the styled footer bar. */
|
|
7
7
|
footer?: Slot;
|
|
8
|
+
/** Aberdeen attr/style string applied to the dialog panel. A surface — pass modifier classes (e.g. `".warning"`) to recolour it. */
|
|
9
|
+
attrs?: Attributes;
|
|
8
10
|
/** Aberdeen attr/style string applied to the header bar. */
|
|
9
|
-
|
|
11
|
+
headerAttrs?: Attributes;
|
|
10
12
|
/** Aberdeen attr/style string applied to the footer bar. */
|
|
11
|
-
|
|
13
|
+
footerAttrs?: Attributes;
|
|
12
14
|
/** Aberdeen attr/style string applied to the scrollable content `<div>`. */
|
|
13
|
-
|
|
14
|
-
/** Aberdeen attr/style string applied to the dialog panel itself. */
|
|
15
|
-
root?: Styling;
|
|
15
|
+
contentAttrs?: Attributes;
|
|
16
16
|
/**
|
|
17
17
|
* Allow closing via Esc or clicking the backdrop. Defaults to `true`.
|
|
18
18
|
* May be changed on a proxied options object while the dialog is open
|
|
@@ -20,10 +20,16 @@ export interface DialogOptions {
|
|
|
20
20
|
*/
|
|
21
21
|
allowCancel?: boolean;
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* When set to `true` (default) the model will be destroyed when the `dialog()`-calling
|
|
24
|
+
* scope is destroyed.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
cancelWithScope?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Dialog body. A {@link Slot} whose draw-function receives a `close()` function
|
|
29
|
+
* — call it to dismiss the dialog programmatically. (A plain string renders as
|
|
30
|
+
* rich text.)
|
|
31
|
+
*/
|
|
32
|
+
content?: Slot<[close: () => void]>;
|
|
27
33
|
/**
|
|
28
34
|
* Called when the dialog closes for any reason (explicit `close()`, Esc, or
|
|
29
35
|
* backdrop click). Useful when you want a side-effect on close but don't need
|
|
@@ -37,11 +43,8 @@ export interface DialogOptions {
|
|
|
37
43
|
* closes. Lifecycle is also tied to the parent reactive scope — when that scope
|
|
38
44
|
* is cleaned up the dialog disappears and the promise resolves.
|
|
39
45
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* The header and footer are pinned; only the body content scrolls when it is
|
|
44
|
-
* taller than `88vh`.
|
|
46
|
+
* Multiple dialogs stack correctly: each new pair (backdrop + dialog) has a
|
|
47
|
+
* higher z-index, while older dialogs are pushed behind their covering backdrop.
|
|
45
48
|
*
|
|
46
49
|
* @example
|
|
47
50
|
* ```ts
|
|
@@ -50,7 +53,7 @@ export interface DialogOptions {
|
|
|
50
53
|
* content: (close) => {
|
|
51
54
|
* A("p #Are you sure?");
|
|
52
55
|
* S.button({ text: "Yes", click: () => { doIt(); close(); } });
|
|
53
|
-
* S.button({ text: "Cancel",
|
|
56
|
+
* S.button({ text: "Cancel", attrs: ".neutral .outlined", click: close });
|
|
54
57
|
* },
|
|
55
58
|
* });
|
|
56
59
|
* ```
|
|
@@ -60,8 +63,6 @@ export declare function dialog(opts: DialogOptions): Promise<void>;
|
|
|
60
63
|
* Shows a message dialog with a single OK button. Returns a `Promise<void>`
|
|
61
64
|
* that resolves when the user dismisses it.
|
|
62
65
|
*
|
|
63
|
-
* All properties of `opts` override the defaults, including `content`.
|
|
64
|
-
*
|
|
65
66
|
* @example
|
|
66
67
|
* ```ts
|
|
67
68
|
* await S.alert("File saved successfully.");
|
|
@@ -70,10 +71,7 @@ export declare function dialog(opts: DialogOptions): Promise<void>;
|
|
|
70
71
|
export declare function alert(message: string, opts?: Partial<DialogOptions>): Promise<void>;
|
|
71
72
|
/**
|
|
72
73
|
* 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`.
|
|
74
|
+
* `Promise<boolean>` — `true` if the user clicked OK, `false` otherwise.
|
|
77
75
|
*
|
|
78
76
|
* @example
|
|
79
77
|
* ```ts
|
|
@@ -83,10 +81,7 @@ export declare function alert(message: string, opts?: Partial<DialogOptions>): P
|
|
|
83
81
|
export declare function confirm(message: string, opts?: Partial<DialogOptions>): Promise<boolean>;
|
|
84
82
|
/**
|
|
85
83
|
* 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
|
|
87
|
-
* backdrop click / Cancel button).
|
|
88
|
-
*
|
|
89
|
-
* All properties of `opts` override the defaults, including `content`.
|
|
84
|
+
* the entered string if the user confirmed, or `null` if cancelled.
|
|
90
85
|
*
|
|
91
86
|
* @example
|
|
92
87
|
* ```ts
|
|
@@ -3,44 +3,74 @@ import { drawSlot } from "../core.js";
|
|
|
3
3
|
import { button } from "./button.js";
|
|
4
4
|
import { buttonGroup } from "./buttonGroup.js";
|
|
5
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
6
|
A.insertGlobalCss({
|
|
10
|
-
".
|
|
11
|
-
"&": "position:fixed inset:0 z-index:200
|
|
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.
|
|
7
|
+
".s-backdrop": {
|
|
8
|
+
"&": "position:fixed inset:0 z-index:200 display:block background: rgba(0,0,0,0.55); transition: opacity 0.4s ease-in-out;",
|
|
15
9
|
"&.hidden": "opacity:0 pointer-events:none",
|
|
16
10
|
},
|
|
17
|
-
".
|
|
18
|
-
"&": "position:fixed z-index:
|
|
11
|
+
".s-dialog": {
|
|
12
|
+
"&": "position:fixed z-index:200 top:50% left:50% " +
|
|
13
|
+
"display:flex flex-direction:column " +
|
|
19
14
|
"transform:translate(-50%,-50%) " +
|
|
20
15
|
"min-width:20rem max-width:min(90vw,44rem) max-height:min(88vh,800px) " +
|
|
21
|
-
"
|
|
22
|
-
"transition: opacity 0.2s ease, transform 0.2s ease;",
|
|
23
|
-
// Header and footer are fixed; only the content <div> scrolls.
|
|
16
|
+
"border: 1px solid $s-border; r: $s-radius-lg; box-shadow: $s-shadow; overflow:hidden " +
|
|
17
|
+
"transition: opacity 0.2s ease-out, transform 0.2s ease-out;",
|
|
24
18
|
"> header": "display:flex align-items:center gap:$2 padding: $2 $3; " +
|
|
25
|
-
"
|
|
19
|
+
"border-bottom: 1px solid $s-border; font-weight:600 flex-shrink:0",
|
|
26
20
|
"> footer": "display:flex align-items:center gap:$2 padding: $2 $3; " +
|
|
27
|
-
"
|
|
21
|
+
"border-top: 1px solid $s-border; flex-shrink:0",
|
|
28
22
|
"> 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",
|
|
23
|
+
"&.hidden": "opacity:0 pointer-events:none transform: translate(-50%, calc(-50% + 20px)); pointer-events:none",
|
|
31
24
|
},
|
|
32
25
|
});
|
|
26
|
+
const dialogs = A.proxy({});
|
|
27
|
+
let dialogCount = 0;
|
|
28
|
+
const topDialogId = A.derive(() => {
|
|
29
|
+
const keys = Object.keys(dialogs);
|
|
30
|
+
if (keys.length)
|
|
31
|
+
return keys[keys.length - 1];
|
|
32
|
+
});
|
|
33
|
+
A.mount(document.body, () => {
|
|
34
|
+
A.onEach(dialogs, ({ resolve, opts }, dialogId) => {
|
|
35
|
+
const close = () => { delete dialogs[dialogId]; };
|
|
36
|
+
A.clean(() => {
|
|
37
|
+
// Fires when this render is torn down — either because $closed became
|
|
38
|
+
// true (normal close) or because the parent reactive scope was cleaned up.
|
|
39
|
+
opts.onClose?.();
|
|
40
|
+
resolve();
|
|
41
|
+
});
|
|
42
|
+
// Backdrop - hide when not the top dialog
|
|
43
|
+
const overlaid = A.derive(() => topDialogId.value != dialogId);
|
|
44
|
+
A("div.s-backdrop create=hidden destroy=hidden .hidden=", overlaid, "click=", () => {
|
|
45
|
+
if (opts.allowCancel !== false)
|
|
46
|
+
close();
|
|
47
|
+
});
|
|
48
|
+
// Dialog itself
|
|
49
|
+
A("div.s-dialog.s-s.panel create=hidden destroy=hidden", opts.attrs, () => {
|
|
50
|
+
A(() => {
|
|
51
|
+
if (opts.header != null) {
|
|
52
|
+
A("header.s-s.raised", opts.headerAttrs, () => drawSlot(opts.header));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
A("div", opts.contentAttrs, () => {
|
|
56
|
+
drawSlot(opts.content, close);
|
|
57
|
+
});
|
|
58
|
+
A(() => {
|
|
59
|
+
if (opts.footer != null) {
|
|
60
|
+
A("footer.s-s.raised", opts.footerAttrs, () => drawSlot(opts.footer));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
33
66
|
/**
|
|
34
67
|
* A dialog rendered into `document.body` via `A.mount`, with a dimming backdrop
|
|
35
68
|
* that fades in and out. Returns a `Promise<void>` that resolves when the dialog
|
|
36
69
|
* closes. Lifecycle is also tied to the parent reactive scope — when that scope
|
|
37
70
|
* is cleaned up the dialog disappears and the promise resolves.
|
|
38
71
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* The header and footer are pinned; only the body content scrolls when it is
|
|
43
|
-
* taller than `88vh`.
|
|
72
|
+
* Multiple dialogs stack correctly: each new pair (backdrop + dialog) has a
|
|
73
|
+
* higher z-index, while older dialogs are pushed behind their covering backdrop.
|
|
44
74
|
*
|
|
45
75
|
* @example
|
|
46
76
|
* ```ts
|
|
@@ -49,74 +79,41 @@ A.insertGlobalCss({
|
|
|
49
79
|
* content: (close) => {
|
|
50
80
|
* A("p #Are you sure?");
|
|
51
81
|
* S.button({ text: "Yes", click: () => { doIt(); close(); } });
|
|
52
|
-
* S.button({ text: "Cancel",
|
|
82
|
+
* S.button({ text: "Cancel", attrs: ".neutral .outlined", click: close });
|
|
53
83
|
* },
|
|
54
84
|
* });
|
|
55
85
|
* ```
|
|
56
86
|
*/
|
|
57
87
|
export function dialog(opts) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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));
|
|
88
|
+
if (!dialogCount) {
|
|
89
|
+
// Install Esc handler the first time we create a dialog
|
|
90
|
+
document.addEventListener("keydown", (e) => {
|
|
91
|
+
if (e.key === "Escape" && opts.allowCancel !== false) {
|
|
92
|
+
const ds = A.unproxy(dialogs);
|
|
93
|
+
// Search for top-most dialog
|
|
94
|
+
for (let i = dialogCount; i > 0; i--) {
|
|
95
|
+
if (ds[i]) {
|
|
96
|
+
if (ds[i].opts.allowCancel !== false) {
|
|
97
|
+
// The clean handler should call resolve and onClose
|
|
98
|
+
delete dialogs[i];
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
108
101
|
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
111
104
|
});
|
|
105
|
+
}
|
|
106
|
+
const dialogId = ++dialogCount;
|
|
107
|
+
if (opts.cancelWithScope !== false)
|
|
108
|
+
A.clean(() => { delete dialogs[dialogId]; });
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
dialogs[dialogId] = { resolve, opts };
|
|
112
111
|
});
|
|
113
112
|
}
|
|
114
113
|
/**
|
|
115
114
|
* Shows a message dialog with a single OK button. Returns a `Promise<void>`
|
|
116
115
|
* that resolves when the user dismisses it.
|
|
117
116
|
*
|
|
118
|
-
* All properties of `opts` override the defaults, including `content`.
|
|
119
|
-
*
|
|
120
117
|
* @example
|
|
121
118
|
* ```ts
|
|
122
119
|
* await S.alert("File saved successfully.");
|
|
@@ -128,7 +125,7 @@ export function alert(message, opts = {}) {
|
|
|
128
125
|
allowCancel: true,
|
|
129
126
|
content: (close) => {
|
|
130
127
|
A("p", () => { A("#", message); });
|
|
131
|
-
buttonGroup({ layout: "spaced",
|
|
128
|
+
buttonGroup({ layout: "spaced", attrs: "align-self:flex-end", content: () => {
|
|
132
129
|
button({ text: "OK", click: close });
|
|
133
130
|
} });
|
|
134
131
|
},
|
|
@@ -137,10 +134,7 @@ export function alert(message, opts = {}) {
|
|
|
137
134
|
}
|
|
138
135
|
/**
|
|
139
136
|
* 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`.
|
|
137
|
+
* `Promise<boolean>` — `true` if the user clicked OK, `false` otherwise.
|
|
144
138
|
*
|
|
145
139
|
* @example
|
|
146
140
|
* ```ts
|
|
@@ -155,8 +149,8 @@ export function confirm(message, opts = {}) {
|
|
|
155
149
|
allowCancel: true,
|
|
156
150
|
content: (close) => {
|
|
157
151
|
A("p", () => { A("#", message); });
|
|
158
|
-
buttonGroup({ layout: "spaced",
|
|
159
|
-
button({ text: "Cancel",
|
|
152
|
+
buttonGroup({ layout: "spaced", attrs: "align-self:flex-end", content: () => {
|
|
153
|
+
button({ text: "Cancel", attrs: ".neutral .outlined", click: close });
|
|
160
154
|
button({ text: "OK", click: () => { confirmed = true; close(); } });
|
|
161
155
|
} });
|
|
162
156
|
},
|
|
@@ -170,10 +164,7 @@ export function confirm(message, opts = {}) {
|
|
|
170
164
|
}
|
|
171
165
|
/**
|
|
172
166
|
* 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
|
|
174
|
-
* backdrop click / Cancel button).
|
|
175
|
-
*
|
|
176
|
-
* All properties of `opts` override the defaults, including `content`.
|
|
167
|
+
* the entered string if the user confirmed, or `null` if cancelled.
|
|
177
168
|
*
|
|
178
169
|
* @example
|
|
179
170
|
* ```ts
|
|
@@ -190,7 +181,6 @@ export function prompt(message, defaultValue = "", opts = {}) {
|
|
|
190
181
|
content: (close) => {
|
|
191
182
|
A("p", () => { A("#", message); });
|
|
192
183
|
const $v = A.proxy({ value: defaultValue });
|
|
193
|
-
// Wrap in a form so Enter submits; display:contents keeps flex layout intact.
|
|
194
184
|
A("form display:contents", () => {
|
|
195
185
|
A("submit=", (e) => {
|
|
196
186
|
e.preventDefault();
|
|
@@ -198,8 +188,8 @@ export function prompt(message, defaultValue = "", opts = {}) {
|
|
|
198
188
|
close();
|
|
199
189
|
});
|
|
200
190
|
textline({ bind: A.ref($v, "value") });
|
|
201
|
-
buttonGroup({ layout: "spaced",
|
|
202
|
-
button({ text: "Cancel",
|
|
191
|
+
buttonGroup({ layout: "spaced", attrs: "align-self:flex-end", content: () => {
|
|
192
|
+
button({ text: "Cancel", attrs: ".neutral .outlined", type: "button", click: close });
|
|
203
193
|
button({ text: "OK", type: "submit" });
|
|
204
194
|
} });
|
|
205
195
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Bindable, type Slot, type Attributes } from "../core.js";
|
|
2
2
|
/**
|
|
3
3
|
* Options shared by all *form field* components (textline, textarea, checkbox,
|
|
4
4
|
* autocomplete, ...).
|
|
@@ -7,7 +7,9 @@ import { type BaseOptions, type Bindable, type Slot, type Styling } from "../cor
|
|
|
7
7
|
* itself, and optional help/error text below it. {@link form} relies on this
|
|
8
8
|
* shared structure to align groups of fields.
|
|
9
9
|
*/
|
|
10
|
-
export interface FieldOptions
|
|
10
|
+
export interface FieldOptions {
|
|
11
|
+
/** Aberdeen attr/style string applied to the field's wrapper element. */
|
|
12
|
+
attrs?: Attributes;
|
|
11
13
|
/** Visible label, associated with the control via `for`/`id` for a11y. */
|
|
12
14
|
label?: Slot;
|
|
13
15
|
/** Helper text shown beneath the control. */
|
|
@@ -25,8 +27,8 @@ export interface FieldOptions extends BaseOptions {
|
|
|
25
27
|
name?: string;
|
|
26
28
|
/** Explicit id for the control; auto-generated when omitted. */
|
|
27
29
|
id?: string;
|
|
28
|
-
/** Aberdeen attr/style string applied to the control element itself. */
|
|
29
|
-
|
|
30
|
+
/** Aberdeen attr/style string applied to the control (input) element itself. */
|
|
31
|
+
inputAttrs?: Attributes;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* Render the standard field chrome (label + control + help/error) around a
|
|
@@ -38,8 +40,8 @@ export interface FieldOptions extends BaseOptions {
|
|
|
38
40
|
*
|
|
39
41
|
* @param opts The field options.
|
|
40
42
|
* @param drawControl Receives the resolved `id` and the live "invalid" getter,
|
|
41
|
-
* and must draw the actual control element (using class `
|
|
42
|
-
* appropriate, and passing `opts.
|
|
43
|
+
* and must draw the actual control element (using class `s-input` where
|
|
44
|
+
* appropriate, and passing `opts.inputAttrs` as an arg for caller styling).
|
|
43
45
|
*/
|
|
44
46
|
export declare function drawField(opts: FieldOptions, drawControl: (id: string, isInvalid: () => boolean) => void): void;
|
|
45
47
|
/**
|
package/dist/components/field.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
import { drawSlot, uniqueId } from "../core.js";
|
|
3
3
|
A.insertGlobalCss({
|
|
4
|
-
".
|
|
4
|
+
".s-field": {
|
|
5
5
|
"&": "display:flex flex-direction:column gap:$1",
|
|
6
|
-
"> label": "font-weight:600 font-size:0.9em fg:$
|
|
6
|
+
"> label": "font-weight:600 font-size:0.9em fg:$s-fg user-select:none",
|
|
7
7
|
},
|
|
8
8
|
// Shared, reusable bits (also used by checkbox & autocomplete).
|
|
9
|
-
".
|
|
10
|
-
".
|
|
11
|
-
".
|
|
12
|
-
// Shared look for text-like controls
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"&:
|
|
9
|
+
".s-req": "fg:$s-danger margin-left:2px",
|
|
10
|
+
".s-help": "font-size:0.82em fg:$s-fg-muted",
|
|
11
|
+
".s-error": "font-size:0.82em fg:$s-danger",
|
|
12
|
+
// Shared look for text-like controls: a panel fill with a contextual border,
|
|
13
|
+
// the brand accent for focus, and the semantic danger ink when invalid.
|
|
14
|
+
".s-input": {
|
|
15
|
+
"&": "w:100% bg:$s-panel fg:$s-ink border: 1px solid $s-border; r:$s-radius padding: 0.55em 0.7em; transition: border-color 0.15s, box-shadow 0.15s;",
|
|
16
|
+
"&:hover:not(:disabled)": "border-color:$s-border-strong",
|
|
17
|
+
"&:focus-visible": "border-color:$s-accent box-shadow: 0 0 0 3px $s-focus; outline:none",
|
|
17
18
|
"&:disabled": "opacity:0.6 cursor:not-allowed",
|
|
18
|
-
"&[aria-invalid=true]": "border-color:$
|
|
19
|
+
"&[aria-invalid=true]": "border-color:$s-danger",
|
|
19
20
|
},
|
|
20
21
|
});
|
|
21
22
|
/**
|
|
@@ -28,30 +29,30 @@ A.insertGlobalCss({
|
|
|
28
29
|
*
|
|
29
30
|
* @param opts The field options.
|
|
30
31
|
* @param drawControl Receives the resolved `id` and the live "invalid" getter,
|
|
31
|
-
* and must draw the actual control element (using class `
|
|
32
|
-
* appropriate, and passing `opts.
|
|
32
|
+
* and must draw the actual control element (using class `s-input` where
|
|
33
|
+
* appropriate, and passing `opts.inputAttrs` as an arg for caller styling).
|
|
33
34
|
*/
|
|
34
35
|
export function drawField(opts, drawControl) {
|
|
35
36
|
const id = opts.id ?? uniqueId("field");
|
|
36
37
|
const isInvalid = () => !!opts.error;
|
|
37
|
-
A("div.
|
|
38
|
+
A("div.s-field", opts.attrs, () => {
|
|
38
39
|
A(() => {
|
|
39
40
|
if (opts.label != null) {
|
|
40
41
|
A(`label for=${id}`, () => {
|
|
41
42
|
drawSlot(opts.label);
|
|
42
43
|
if (opts.required)
|
|
43
|
-
A("span.
|
|
44
|
+
A("span.s-req aria-hidden=true #*");
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
});
|
|
47
48
|
drawControl(id, isInvalid);
|
|
48
49
|
A(() => {
|
|
49
50
|
if (opts.help != null && !opts.error)
|
|
50
|
-
A("div.
|
|
51
|
+
A("div.s-help", () => drawSlot(opts.help));
|
|
51
52
|
});
|
|
52
53
|
A(() => {
|
|
53
54
|
if (opts.error)
|
|
54
|
-
A("div.
|
|
55
|
+
A("div.s-error role=alert #", opts.error);
|
|
55
56
|
});
|
|
56
57
|
});
|
|
57
58
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Content, type ContentOptions, type
|
|
1
|
+
import { type Content, type ContentOptions, type Attributes } from "../core.js";
|
|
2
2
|
/** Options for {@link form}. */
|
|
3
3
|
export interface FormOptions extends ContentOptions {
|
|
4
4
|
/**
|
|
@@ -10,11 +10,11 @@ export interface FormOptions extends ContentOptions {
|
|
|
10
10
|
/**
|
|
11
11
|
* Layout of fields. `"stacked"` (default) is a single column; `"grid"` packs
|
|
12
12
|
* fields into a responsive multi-column grid. A field can span the full grid
|
|
13
|
-
* width by adding the `.
|
|
13
|
+
* width by adding the `.s-wide` class (e.g. `attrs: ".s-wide"`).
|
|
14
14
|
*/
|
|
15
15
|
layout?: "stacked" | "grid";
|
|
16
16
|
/** Aberdeen attr/style string for the action bar. */
|
|
17
|
-
|
|
17
|
+
actionsAttrs?: Attributes;
|
|
18
18
|
/** Footer actions (typically a {@link import("./buttonGroup").buttonGroup} or buttons). */
|
|
19
19
|
actions?: Content;
|
|
20
20
|
}
|
package/dist/components/form.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
A.insertGlobalCss({
|
|
3
|
-
".
|
|
3
|
+
".s-form": {
|
|
4
4
|
"&": "display:flex flex-direction:column gap:$3",
|
|
5
5
|
"&.grid": "display:grid grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap:$3",
|
|
6
|
-
"&.grid > .
|
|
6
|
+
"&.grid > .s-wide, &.grid > footer": "grid-column: 1 / -1;",
|
|
7
7
|
"> footer": "display:flex align-items:center gap:$2 flex-wrap:wrap margin-top:$1",
|
|
8
8
|
},
|
|
9
9
|
});
|
|
@@ -30,7 +30,7 @@ A.insertGlobalCss({
|
|
|
30
30
|
*/
|
|
31
31
|
export function form(opts = {}) {
|
|
32
32
|
const o = typeof opts === "function" ? { content: opts } : opts;
|
|
33
|
-
A(`form.
|
|
33
|
+
A(`form.s-form`, o.attrs, () => {
|
|
34
34
|
// Toggle grid class in its own scope so changing layout doesn't recreate
|
|
35
35
|
// the fields (which would lose focus / input state).
|
|
36
36
|
A(() => {
|
|
@@ -53,7 +53,7 @@ export function form(opts = {}) {
|
|
|
53
53
|
// Own scope so toggling actions doesn't recreate the fields above.
|
|
54
54
|
A(() => {
|
|
55
55
|
if (o.actions)
|
|
56
|
-
A("footer", o.
|
|
56
|
+
A("footer", o.actionsAttrs, () => o.actions?.());
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
}
|