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
package/src/components/field.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
|
-
import { type
|
|
2
|
+
import { type Bindable, type Slot, type Attributes, drawSlot, uniqueId } from "../core.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Options shared by all *form field* components (textline, textarea, checkbox,
|
|
@@ -9,7 +9,9 @@ import { type BaseOptions, type Bindable, type Slot, type Styling, drawSlot, uni
|
|
|
9
9
|
* itself, and optional help/error text below it. {@link form} relies on this
|
|
10
10
|
* shared structure to align groups of fields.
|
|
11
11
|
*/
|
|
12
|
-
export interface FieldOptions
|
|
12
|
+
export interface FieldOptions {
|
|
13
|
+
/** Aberdeen attr/style string applied to the field's wrapper element. */
|
|
14
|
+
attrs?: Attributes;
|
|
13
15
|
/** Visible label, associated with the control via `for`/`id` for a11y. */
|
|
14
16
|
label?: Slot;
|
|
15
17
|
/** Helper text shown beneath the control. */
|
|
@@ -27,26 +29,27 @@ export interface FieldOptions extends BaseOptions {
|
|
|
27
29
|
name?: string;
|
|
28
30
|
/** Explicit id for the control; auto-generated when omitted. */
|
|
29
31
|
id?: string;
|
|
30
|
-
/** Aberdeen attr/style string applied to the control element itself. */
|
|
31
|
-
|
|
32
|
+
/** Aberdeen attr/style string applied to the control (input) element itself. */
|
|
33
|
+
inputAttrs?: Attributes;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
A.insertGlobalCss({
|
|
35
|
-
".
|
|
37
|
+
".s-field": {
|
|
36
38
|
"&": "display:flex flex-direction:column gap:$1",
|
|
37
|
-
"> label": "font-weight:600 font-size:0.9em fg:$
|
|
39
|
+
"> label": "font-weight:600 font-size:0.9em fg:$s-fg user-select:none",
|
|
38
40
|
},
|
|
39
41
|
// Shared, reusable bits (also used by checkbox & autocomplete).
|
|
40
|
-
".
|
|
41
|
-
".
|
|
42
|
-
".
|
|
43
|
-
// Shared look for text-like controls
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"&:
|
|
42
|
+
".s-req": "fg:$s-danger margin-left:2px",
|
|
43
|
+
".s-help": "font-size:0.82em fg:$s-fg-muted",
|
|
44
|
+
".s-error": "font-size:0.82em fg:$s-danger",
|
|
45
|
+
// Shared look for text-like controls: a panel fill with a contextual border,
|
|
46
|
+
// the brand accent for focus, and the semantic danger ink when invalid.
|
|
47
|
+
".s-input": {
|
|
48
|
+
"&": "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;",
|
|
49
|
+
"&:hover:not(:disabled)": "border-color:$s-border-strong",
|
|
50
|
+
"&:focus-visible": "border-color:$s-accent box-shadow: 0 0 0 3px $s-focus; outline:none",
|
|
48
51
|
"&:disabled": "opacity:0.6 cursor:not-allowed",
|
|
49
|
-
"&[aria-invalid=true]": "border-color:$
|
|
52
|
+
"&[aria-invalid=true]": "border-color:$s-danger",
|
|
50
53
|
},
|
|
51
54
|
});
|
|
52
55
|
|
|
@@ -60,8 +63,8 @@ A.insertGlobalCss({
|
|
|
60
63
|
*
|
|
61
64
|
* @param opts The field options.
|
|
62
65
|
* @param drawControl Receives the resolved `id` and the live "invalid" getter,
|
|
63
|
-
* and must draw the actual control element (using class `
|
|
64
|
-
* appropriate, and passing `opts.
|
|
66
|
+
* and must draw the actual control element (using class `s-input` where
|
|
67
|
+
* appropriate, and passing `opts.inputAttrs` as an arg for caller styling).
|
|
65
68
|
*/
|
|
66
69
|
export function drawField(
|
|
67
70
|
opts: FieldOptions,
|
|
@@ -70,12 +73,12 @@ export function drawField(
|
|
|
70
73
|
const id = opts.id ?? uniqueId("field");
|
|
71
74
|
const isInvalid = () => !!opts.error;
|
|
72
75
|
|
|
73
|
-
A("div.
|
|
76
|
+
A("div.s-field", opts.attrs, () => {
|
|
74
77
|
A(() => {
|
|
75
78
|
if (opts.label != null) {
|
|
76
79
|
A(`label for=${id}`, () => {
|
|
77
80
|
drawSlot(opts.label);
|
|
78
|
-
if (opts.required) A("span.
|
|
81
|
+
if (opts.required) A("span.s-req aria-hidden=true #*");
|
|
79
82
|
});
|
|
80
83
|
}
|
|
81
84
|
});
|
|
@@ -83,10 +86,10 @@ export function drawField(
|
|
|
83
86
|
drawControl(id, isInvalid);
|
|
84
87
|
|
|
85
88
|
A(() => {
|
|
86
|
-
if (opts.help != null && !opts.error) A("div.
|
|
89
|
+
if (opts.help != null && !opts.error) A("div.s-help", () => drawSlot(opts.help));
|
|
87
90
|
});
|
|
88
91
|
A(() => {
|
|
89
|
-
if (opts.error) A("div.
|
|
92
|
+
if (opts.error) A("div.s-error role=alert #", opts.error);
|
|
90
93
|
});
|
|
91
94
|
});
|
|
92
95
|
}
|
package/src/components/form.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
|
-
import { type Content, type ContentOptions, type
|
|
2
|
+
import { type Content, type ContentOptions, type Attributes } from "../core.js";
|
|
3
3
|
|
|
4
4
|
/** Options for {@link form}. */
|
|
5
5
|
export interface FormOptions extends ContentOptions {
|
|
@@ -12,20 +12,20 @@ export interface FormOptions extends ContentOptions {
|
|
|
12
12
|
/**
|
|
13
13
|
* Layout of fields. `"stacked"` (default) is a single column; `"grid"` packs
|
|
14
14
|
* fields into a responsive multi-column grid. A field can span the full grid
|
|
15
|
-
* width by adding the `.
|
|
15
|
+
* width by adding the `.s-wide` class (e.g. `attrs: ".s-wide"`).
|
|
16
16
|
*/
|
|
17
17
|
layout?: "stacked" | "grid";
|
|
18
18
|
/** Aberdeen attr/style string for the action bar. */
|
|
19
|
-
|
|
19
|
+
actionsAttrs?: Attributes;
|
|
20
20
|
/** Footer actions (typically a {@link import("./buttonGroup").buttonGroup} or buttons). */
|
|
21
21
|
actions?: Content;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
A.insertGlobalCss({
|
|
25
|
-
".
|
|
25
|
+
".s-form": {
|
|
26
26
|
"&": "display:flex flex-direction:column gap:$3",
|
|
27
27
|
"&.grid": "display:grid grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap:$3",
|
|
28
|
-
"&.grid > .
|
|
28
|
+
"&.grid > .s-wide, &.grid > footer": "grid-column: 1 / -1;",
|
|
29
29
|
"> footer": "display:flex align-items:center gap:$2 flex-wrap:wrap margin-top:$1",
|
|
30
30
|
},
|
|
31
31
|
});
|
|
@@ -54,7 +54,7 @@ A.insertGlobalCss({
|
|
|
54
54
|
export function form(opts: FormOptions | Content = {}): void {
|
|
55
55
|
const o: FormOptions = typeof opts === "function" ? { content: opts } : opts;
|
|
56
56
|
|
|
57
|
-
A(`form.
|
|
57
|
+
A(`form.s-form`, o.attrs, () => {
|
|
58
58
|
// Toggle grid class in its own scope so changing layout doesn't recreate
|
|
59
59
|
// the fields (which would lose focus / input state).
|
|
60
60
|
A(() => {
|
|
@@ -78,7 +78,7 @@ export function form(opts: FormOptions | Content = {}): void {
|
|
|
78
78
|
|
|
79
79
|
// Own scope so toggling actions doesn't recreate the fields above.
|
|
80
80
|
A(() => {
|
|
81
|
-
if (o.actions) A("footer", o.
|
|
81
|
+
if (o.actions) A("footer", o.actionsAttrs, () => o.actions?.());
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
84
|
}
|
package/src/components/main.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
|
-
import { type
|
|
2
|
+
import { type Content, type Slot, type Attributes, drawSlot } from "../core.js";
|
|
3
3
|
|
|
4
4
|
/** Options for {@link main}. */
|
|
5
|
-
export interface MainOptions
|
|
5
|
+
export interface MainOptions {
|
|
6
|
+
/** Aberdeen attr/style string applied to the outermost shell element. */
|
|
7
|
+
attrs?: Attributes;
|
|
6
8
|
/** App/page title shown in the top bar. */
|
|
7
9
|
title?: Slot;
|
|
8
10
|
/** Secondary line under the title. */
|
|
@@ -22,25 +24,27 @@ export interface MainOptions extends BaseOptions {
|
|
|
22
24
|
*/
|
|
23
25
|
maxWidth?: string;
|
|
24
26
|
/** Aberdeen attr/style string applied to the content sheet. */
|
|
25
|
-
|
|
27
|
+
contentAttrs?: Attributes;
|
|
26
28
|
/** Aberdeen attr/style string applied to the top bar. */
|
|
27
|
-
|
|
29
|
+
topbarAttrs?: Attributes;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
A.insertGlobalCss({
|
|
31
|
-
".
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
"> header
|
|
36
|
-
"> header .
|
|
37
|
-
"> header .
|
|
38
|
-
"> header .
|
|
33
|
+
".s-main": {
|
|
34
|
+
// Colours come from the surface classes added in main(): the shell is a
|
|
35
|
+
// filled base, the bars are raised, the framed sheet is a panel.
|
|
36
|
+
"&": "display:flex flex-direction:column min-height:100vh max-height:100vh",
|
|
37
|
+
"> header": "display:flex align-items:center gap:$3 padding: $2 $3; border-bottom: 1px solid $s-border; position:sticky top:0 z-index:10",
|
|
38
|
+
"> header .s-icon": "display:flex align-items:center font-size:1.4em",
|
|
39
|
+
"> header .s-titles": "display:flex flex-direction:column min-width:0 flex:1",
|
|
40
|
+
"> header .s-title": "font-weight:700 font-size:1.1em line-height:1.2 overflow:hidden text-overflow:ellipsis white-space:nowrap",
|
|
41
|
+
"> header .s-subtitle": "fg:$s-fg-muted font-size:0.85em overflow:hidden text-overflow:ellipsis white-space:nowrap",
|
|
42
|
+
"> header .s-menu": "display:flex align-items:center gap:$2",
|
|
39
43
|
"> main": "flex:1 overflow-y:auto display:flex flex-direction:column",
|
|
40
|
-
"> main > .
|
|
41
|
-
"> main > .
|
|
42
|
-
"> main > .
|
|
43
|
-
"> footer": "display:flex align-items:center gap:$2 padding: $2 $3;
|
|
44
|
+
"> main > .s-content": "width:100% flex:1",
|
|
45
|
+
"> main > .s-content.s-framed": "margin: $3 auto; border: 1px solid $s-border; r:$s-radius-lg box-shadow:$s-shadow p:$4",
|
|
46
|
+
"> main > .s-content.s-plain": "p:$3",
|
|
47
|
+
"> footer": "display:flex align-items:center gap:$2 padding: $2 $3; border-top: 1px solid $s-border; fg:$s-fg-muted",
|
|
44
48
|
},
|
|
45
49
|
});
|
|
46
50
|
|
|
@@ -65,38 +69,38 @@ A.insertGlobalCss({
|
|
|
65
69
|
* ```
|
|
66
70
|
*/
|
|
67
71
|
export function main(opts: MainOptions = {}): void {
|
|
68
|
-
A("div.
|
|
72
|
+
A("div.s-main.s-s.base", opts.attrs, () => {
|
|
69
73
|
// Top bar — only rendered when there's something to show in it.
|
|
70
74
|
A(() => {
|
|
71
75
|
const hasBar = opts.title != null || opts.subtitle != null || opts.icon != null || opts.menu != null;
|
|
72
76
|
if (!hasBar) return;
|
|
73
|
-
A("header", opts.
|
|
77
|
+
A("header.s-s.raised", opts.topbarAttrs, () => {
|
|
74
78
|
A(() => {
|
|
75
|
-
if (opts.icon != null) A("div.
|
|
79
|
+
if (opts.icon != null) A("div.s-icon", () => drawSlot(opts.icon));
|
|
76
80
|
});
|
|
77
|
-
A("div.
|
|
81
|
+
A("div.s-titles", () => {
|
|
78
82
|
A(() => {
|
|
79
|
-
if (opts.title != null) A("div.
|
|
83
|
+
if (opts.title != null) A("div.s-title", () => drawSlot(opts.title));
|
|
80
84
|
});
|
|
81
85
|
A(() => {
|
|
82
|
-
if (opts.subtitle != null) A("div.
|
|
86
|
+
if (opts.subtitle != null) A("div.s-subtitle", () => drawSlot(opts.subtitle));
|
|
83
87
|
});
|
|
84
88
|
});
|
|
85
89
|
A(() => {
|
|
86
|
-
if (opts.menu) A("div.
|
|
90
|
+
if (opts.menu) A("div.s-menu", () => opts.menu?.());
|
|
87
91
|
});
|
|
88
92
|
});
|
|
89
93
|
});
|
|
90
94
|
|
|
91
95
|
// Scrollable main region with the (optionally framed) content sheet.
|
|
92
96
|
A("main", () => {
|
|
93
|
-
A("div.
|
|
97
|
+
A("div.s-content", opts.contentAttrs, () => {
|
|
94
98
|
// Framing applied in its own scope so changing maxWidth doesn't
|
|
95
99
|
// recreate the content (which holds the whole page).
|
|
96
100
|
A(() => {
|
|
97
101
|
const max = opts.maxWidth;
|
|
98
|
-
if (max != null) A(".
|
|
99
|
-
else A(".
|
|
102
|
+
if (max != null) A(".s-framed.s-s.panel max-width:", max);
|
|
103
|
+
else A(".s-plain");
|
|
100
104
|
});
|
|
101
105
|
if (opts.content) opts.content();
|
|
102
106
|
});
|
package/src/components/select.ts
CHANGED
|
@@ -17,10 +17,10 @@ export interface SelectOptions extends FieldOptions {
|
|
|
17
17
|
|
|
18
18
|
// Wrapper provides the chevron via ::after (pseudo-elements on <select> are unreliable).
|
|
19
19
|
A.insertGlobalCss({
|
|
20
|
-
".
|
|
20
|
+
".s-select_wrap": {
|
|
21
21
|
"&": "position:relative display:block",
|
|
22
22
|
"select": "w:100% cursor:pointer padding-right:2.2em; appearance:none",
|
|
23
|
-
"&::after": "content: '▾'; position:absolute right:0.7em top:50%; transform: translateY(-50%); pointer-events:none fg:$
|
|
23
|
+
"&::after": "content: '▾'; position:absolute right:0.7em top:50%; transform: translateY(-50%); pointer-events:none fg:$s-fg-muted font-size:0.85em",
|
|
24
24
|
},
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -36,8 +36,8 @@ A.insertGlobalCss({
|
|
|
36
36
|
*/
|
|
37
37
|
export function select(opts: SelectOptions): void {
|
|
38
38
|
drawField(opts, (id, isInvalid) => {
|
|
39
|
-
A("div.
|
|
40
|
-
A("select.
|
|
39
|
+
A("div.s-select_wrap", opts.inputAttrs, () => {
|
|
40
|
+
A("select.s-input", () => {
|
|
41
41
|
applyControlAttrs(opts, id, isInvalid);
|
|
42
42
|
|
|
43
43
|
A("change=", (e: Event) => {
|
package/src/components/tabs.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
|
-
import { type
|
|
2
|
+
import { type Bindable, type Content, type Slot, type Attributes, drawSlot, uniqueId } from "../core.js";
|
|
3
3
|
|
|
4
4
|
/** A single tab definition. */
|
|
5
5
|
export interface Tab {
|
|
@@ -16,7 +16,9 @@ export interface Tab {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Options for {@link tabs}. */
|
|
19
|
-
export interface TabsOptions
|
|
19
|
+
export interface TabsOptions {
|
|
20
|
+
/** Aberdeen attr/style string applied to the outermost element. */
|
|
21
|
+
attrs?: Attributes;
|
|
20
22
|
/** The tabs to display. */
|
|
21
23
|
tabs: Tab[];
|
|
22
24
|
/**
|
|
@@ -27,30 +29,32 @@ export interface TabsOptions extends BaseOptions {
|
|
|
27
29
|
/** Visual style of the tab strip. Defaults to `"underline"`. */
|
|
28
30
|
variant?: "underline" | "pills";
|
|
29
31
|
/** Aberdeen attr/style string applied to the active panel. */
|
|
30
|
-
|
|
32
|
+
contentAttrs?: Attributes;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
A.insertGlobalCss({
|
|
34
|
-
".
|
|
36
|
+
".s-tabs": {
|
|
35
37
|
"&": "display:flex flex-direction:column gap:$3",
|
|
36
|
-
".
|
|
37
|
-
".
|
|
38
|
+
".s-tablist": "display:flex gap:$1 align-items:stretch",
|
|
39
|
+
".s-tab":
|
|
38
40
|
"display:inline-flex align-items:center gap:$2 cursor:pointer background:transparent " +
|
|
39
|
-
"border:0 fg
|
|
41
|
+
"border:0 color: $s-fg-muted; font-weight:600 padding: 0.6em 0.9em; " +
|
|
40
42
|
"transition: color 0.15s, background 0.15s, border-color 0.15s;",
|
|
41
|
-
".
|
|
42
|
-
".
|
|
43
|
-
".
|
|
44
|
-
// Underline variant.
|
|
45
|
-
"&.
|
|
46
|
-
"&.
|
|
47
|
-
"&.
|
|
48
|
-
// Pills variant.
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
".s-tab:hover:not(:disabled)": "color: $s-fg;",
|
|
44
|
+
".s-tab:disabled": "opacity:0.5 cursor:not-allowed",
|
|
45
|
+
".s-tab:focus-visible": "outline:none box-shadow: 0 0 0 3px $s-focus; r: $s-radius;",
|
|
46
|
+
// Underline variant — the active marker uses the contextual brand accent.
|
|
47
|
+
"&.s-underline .s-tablist": "border-bottom: 1px solid $s-border;",
|
|
48
|
+
"&.s-underline .s-tab": "border-bottom: 2px solid transparent; margin-bottom:-1px",
|
|
49
|
+
"&.s-underline .s-tab[aria-selected=true]": "color: $s-fg; border-bottom-color: $s-accent;",
|
|
50
|
+
// Pills variant — the active pill gets the `.s-s.primary` surface (added in
|
|
51
|
+
// tabs()), so its `--s-bg`/`--s-fg` are the brand surface's; we just paint
|
|
52
|
+
// (the `.s-tab` rule's transparent background would otherwise win on it).
|
|
53
|
+
"&.s-pills .s-tab": "r: $s-radius;",
|
|
54
|
+
"&.s-pills .s-tab[aria-selected=true]": "background: $s-bg; color: $s-fg;",
|
|
51
55
|
// The panel has no enclosing box, so no default padding — its content
|
|
52
56
|
// aligns flush with the tab strip. Callers add padding/flex via `inner`.
|
|
53
|
-
".
|
|
57
|
+
".s-tabpanel": "display:block",
|
|
54
58
|
},
|
|
55
59
|
});
|
|
56
60
|
|
|
@@ -81,16 +85,19 @@ export function tabs(opts: TabsOptions): void {
|
|
|
81
85
|
$sel.value = keyOf(tab, index);
|
|
82
86
|
};
|
|
83
87
|
|
|
84
|
-
A(`div.
|
|
85
|
-
A("div.
|
|
88
|
+
A(`div.s-tabs.s-${variant}`, opts.attrs, () => {
|
|
89
|
+
A("div.s-tablist role=tablist", () => {
|
|
86
90
|
opts.tabs.forEach((tab, index) => {
|
|
87
91
|
const key = keyOf(tab, index);
|
|
88
|
-
A("button.
|
|
92
|
+
A("button.s-tab type=button role=tab", () => {
|
|
89
93
|
A(`id=${groupId}-tab-${key} aria-controls=${groupId}-panel-${key}`);
|
|
90
94
|
A(() => {
|
|
91
95
|
const selected = $sel.value === key;
|
|
92
96
|
A("aria-selected=", selected ? "true" : "false");
|
|
93
97
|
A("tabindex=", selected ? "0" : "-1");
|
|
98
|
+
// The active pill is a filled brand surface; the classes flip
|
|
99
|
+
// with selection (Aberdeen removes them when this scope re-runs).
|
|
100
|
+
if (selected && variant === "pills") A(".s-s.primary");
|
|
94
101
|
});
|
|
95
102
|
if (tab.disabled) A("disabled=true");
|
|
96
103
|
A("click=", () => select(tab, index));
|
|
@@ -101,7 +108,7 @@ export function tabs(opts: TabsOptions): void {
|
|
|
101
108
|
});
|
|
102
109
|
});
|
|
103
110
|
|
|
104
|
-
A("div.
|
|
111
|
+
A("div.s-tabpanel role=tabpanel", opts.contentAttrs, () => {
|
|
105
112
|
A(() => {
|
|
106
113
|
const selKey = $sel.value;
|
|
107
114
|
const index = opts.tabs.findIndex((t, i) => keyOf(t, i) === selKey);
|
|
@@ -23,8 +23,8 @@ export interface TextareaOptions extends FieldOptions {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
A.insertGlobalCss({
|
|
26
|
-
"textarea.
|
|
27
|
-
"textarea.
|
|
26
|
+
"textarea.s-input": "resize:vertical min-height:3em line-height:1.45",
|
|
27
|
+
"textarea.s-input.s-autoGrow": "resize:none min-height:2.5em overflow-y:hidden",
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -40,9 +40,9 @@ export function textarea(opts: TextareaOptions = {}): void {
|
|
|
40
40
|
const grow = opts.autoGrow !== false;
|
|
41
41
|
|
|
42
42
|
drawField(opts, (id, isInvalid) => {
|
|
43
|
-
const el = A("textarea.
|
|
43
|
+
const el = A("textarea.s-input", opts.inputAttrs, () => {
|
|
44
44
|
if (grow) {
|
|
45
|
-
A(".
|
|
45
|
+
A(".s-autoGrow");
|
|
46
46
|
A("input=", (e: Event) => {
|
|
47
47
|
fitToContent(e.currentTarget as HTMLTextAreaElement);
|
|
48
48
|
if (opts.input) opts.input(e);
|
|
@@ -53,7 +53,7 @@ export interface TextlineOptions extends FieldOptions {
|
|
|
53
53
|
*/
|
|
54
54
|
export function textline(opts: TextlineOptions = {}): void {
|
|
55
55
|
drawField(opts, (id, isInvalid) => {
|
|
56
|
-
A("input.
|
|
56
|
+
A("input.s-input", opts.inputAttrs, () => {
|
|
57
57
|
A("type=", opts.type ?? "text");
|
|
58
58
|
if (opts.placeholder != null) A("placeholder=", opts.placeholder);
|
|
59
59
|
if (opts.autocomplete != null) A("autocomplete=", opts.autocomplete);
|
package/src/core.ts
CHANGED
|
@@ -11,16 +11,20 @@ import A from "aberdeen";
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* An Aberdeen attribute/style/class string, e.g. `"display:flex gap:$3 .my-class"`.
|
|
14
|
+
*
|
|
15
|
+
* Common values are our surface modifier classes:
|
|
16
|
+
* - for colors: `.panel` `.raised` `.neutral` `.primary` `.danger` `.success` and `.warning`
|
|
17
|
+
* - for variant: `.filled` `.tonal` and `.outlined`
|
|
14
18
|
*
|
|
15
19
|
* These strings are passed straight through to {@link A} as positional
|
|
16
20
|
* arguments, so they accept the full Aberdeen shorthand syntax: CSS shortcuts
|
|
17
21
|
* (`p`, `mt`, `bg`, `r`, ...), spacing variables (`$1`..`$12`), CSS custom
|
|
18
|
-
* properties (`$
|
|
22
|
+
* properties (`$s-primary`), classes (`.foo`) and attributes (`aria-label=Hi`).
|
|
19
23
|
*
|
|
20
24
|
* Note: because Aberdeen interprets a leading bare word as an element name, write
|
|
21
25
|
* `display:flex` rather than just `flex`.
|
|
22
26
|
*/
|
|
23
|
-
export type
|
|
27
|
+
export type Attributes = string;
|
|
24
28
|
|
|
25
29
|
/** A reactive "value box", such as the result of `A.proxy(x)` or `A.ref(obj, key)`. */
|
|
26
30
|
export type Bindable<T> = { value: T };
|
|
@@ -29,46 +33,27 @@ export type Bindable<T> = { value: T };
|
|
|
29
33
|
export type Content = () => void;
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
|
-
* Something that renders a small piece of content: either a plain string
|
|
33
|
-
*
|
|
34
|
-
*/
|
|
35
|
-
export type Slot = string | Content;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Options shared by *every* Staffa component.
|
|
36
|
+
* Something that renders a small piece of content: either a plain string or a
|
|
37
|
+
* draw function (for icons, badges, custom markup, ...).
|
|
39
38
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* A string is drawn as **rich text** (see {@link drawSlot}): Aberdeen's `rich`
|
|
40
|
+
* markup is applied, so `*italic*`, `**bold**`, `` `code` `` and
|
|
41
|
+
* `[links](/path)` render as inline elements (text is safely escaped).
|
|
42
|
+
*
|
|
43
|
+
* The optional `Args` type parameter lets a slot's draw-function receive
|
|
44
|
+
* arguments — e.g. a dialog body is a `Slot<[close: () => void]>`.
|
|
43
45
|
*/
|
|
44
|
-
export
|
|
45
|
-
/**
|
|
46
|
-
* Aberdeen attr/style string applied to the widget's root element.
|
|
47
|
-
*
|
|
48
|
-
* It is passed as a positional argument to {@link A}, so a *change* to it on a
|
|
49
|
-
* proxied options object re-runs the caller's scope (recreating the widget).
|
|
50
|
-
* That's fine for `root` — it rarely changes at runtime.
|
|
51
|
-
*/
|
|
52
|
-
root?: Styling;
|
|
53
|
-
}
|
|
46
|
+
export type Slot<Args extends unknown[] = []> = string | ((...args: Args) => void);
|
|
54
47
|
|
|
55
48
|
/**
|
|
56
|
-
* Options for components that wrap a single block of caller-provided content
|
|
57
|
-
*
|
|
58
|
-
* Such components render an *inner* element (the one that actually holds the
|
|
59
|
-
* children) which is given sensible default padding and `gap` in CSS. Override
|
|
60
|
-
* those via {@link ContentOptions.inner | inner}, whose declarations win because
|
|
61
|
-
* they're applied as inline styles.
|
|
49
|
+
* Options for components that wrap a single block of caller-provided content,
|
|
50
|
+
* with an `attrs` escape hatch on the outermost element.
|
|
62
51
|
*/
|
|
63
|
-
export interface ContentOptions
|
|
52
|
+
export interface ContentOptions {
|
|
53
|
+
/** Aberdeen attr/style string applied to the widget's outermost element. */
|
|
54
|
+
attrs?: Attributes;
|
|
64
55
|
/** Draws the children of this component. */
|
|
65
56
|
content?: Content;
|
|
66
|
-
/**
|
|
67
|
-
* Aberdeen attr/style string applied to the inner (content-holding) element.
|
|
68
|
-
* Add `display:flex` here if you want the children laid out as a flex
|
|
69
|
-
* row/column.
|
|
70
|
-
*/
|
|
71
|
-
inner?: Styling;
|
|
72
57
|
}
|
|
73
58
|
|
|
74
59
|
let idCounter = 0;
|
|
@@ -78,11 +63,12 @@ export function uniqueId(prefix = "s"): string {
|
|
|
78
63
|
}
|
|
79
64
|
|
|
80
65
|
/**
|
|
81
|
-
* Draw a {@link Slot} into the current element: call it
|
|
82
|
-
* otherwise emit
|
|
66
|
+
* Draw a {@link Slot} into the current element: call it (with any extra `args`)
|
|
67
|
+
* if it's a function, otherwise emit the string as **rich text** via Aberdeen's
|
|
68
|
+
* `rich` markup (`*italic*`, `**bold**`, `` `code` ``, `[link](/path)`).
|
|
83
69
|
*/
|
|
84
|
-
export function drawSlot(slot: Slot | undefined): void {
|
|
70
|
+
export function drawSlot<Args extends unknown[] = []>(slot: Slot<Args> | undefined, ...args: Args): void {
|
|
85
71
|
if (slot == null) return;
|
|
86
|
-
if (typeof slot === "function") slot();
|
|
87
|
-
else A("
|
|
72
|
+
if (typeof slot === "function") slot(...args);
|
|
73
|
+
else A("rich=", slot);
|
|
88
74
|
}
|
package/src/index.ts
CHANGED
|
@@ -30,12 +30,14 @@
|
|
|
30
30
|
* mutate it. See `AGENTS.md` for the design philosophy.
|
|
31
31
|
*/
|
|
32
32
|
// Importing the theme module installs spacing vars, the reactive theme and the
|
|
33
|
-
// base stylesheet. Customise
|
|
34
|
-
|
|
33
|
+
// base stylesheet. Customise it from your app with A.insertGlobalCss (see
|
|
34
|
+
// theme.ts); toggle modes with setDarkMode / getDarkMode.
|
|
35
|
+
import { setDarkMode, getDarkMode } from "./theme.js";
|
|
35
36
|
|
|
36
37
|
import { autocomplete } from "./components/autocomplete.js";
|
|
37
38
|
import { box } from "./components/box.js";
|
|
38
39
|
import { button } from "./components/button.js";
|
|
40
|
+
import { buttonChooser } from "./components/buttonChooser.js";
|
|
39
41
|
import { buttonGroup } from "./components/buttonGroup.js";
|
|
40
42
|
import { checkbox } from "./components/checkbox.js";
|
|
41
43
|
import { form } from "./components/form.js";
|
|
@@ -60,11 +62,10 @@ export const S = {
|
|
|
60
62
|
checkbox,
|
|
61
63
|
tabs,
|
|
62
64
|
button,
|
|
65
|
+
buttonChooser,
|
|
63
66
|
buttonGroup,
|
|
64
67
|
autocomplete,
|
|
65
68
|
select,
|
|
66
|
-
darkTheme,
|
|
67
|
-
lightTheme,
|
|
68
69
|
setDarkMode,
|
|
69
70
|
getDarkMode,
|
|
70
71
|
};
|
|
@@ -72,14 +73,14 @@ export const S = {
|
|
|
72
73
|
export default S;
|
|
73
74
|
|
|
74
75
|
// Re-export theming and shared types for advanced use.
|
|
75
|
-
export {
|
|
76
|
+
export { setDarkMode, getDarkMode } from "./theme.js";
|
|
77
|
+
export type { SurfaceRole, Variant } from "./theme.js";
|
|
76
78
|
export type {
|
|
77
|
-
BaseOptions,
|
|
78
79
|
ContentOptions,
|
|
79
80
|
Bindable,
|
|
80
81
|
Content,
|
|
81
82
|
Slot,
|
|
82
|
-
Styling,
|
|
83
|
+
Attributes as Styling,
|
|
83
84
|
} from "./core.js";
|
|
84
85
|
export { drawSlot, uniqueId } from "./core.js";
|
|
85
86
|
|
|
@@ -91,7 +92,8 @@ export type { TextlineOptions, TextlineType } from "./components/textline.js";
|
|
|
91
92
|
export type { TextareaOptions } from "./components/textarea.js";
|
|
92
93
|
export type { CheckboxOptions } from "./components/checkbox.js";
|
|
93
94
|
export type { Tab, TabsOptions } from "./components/tabs.js";
|
|
94
|
-
export type { ButtonOptions
|
|
95
|
+
export type { ButtonOptions } from "./components/button.js";
|
|
96
|
+
export type { ButtonChooserOptions } from "./components/buttonChooser.js";
|
|
95
97
|
export type { ButtonGroupOptions } from "./components/buttonGroup.js";
|
|
96
98
|
export type { AutocompleteOptions, AutocompleteOptionInput } from "./components/autocomplete.js";
|
|
97
99
|
export type { SelectOptions, SelectOptionInput } from "./components/select.js";
|