staffa 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -48
- package/dist/components/autocomplete.js +1 -1
- package/dist/components/box.d.ts +8 -16
- package/dist/components/box.js +21 -27
- package/dist/components/button.d.ts +40 -0
- package/dist/components/button.js +85 -12
- package/dist/components/buttonChooser.js +1 -1
- package/dist/components/checkbox.js +3 -3
- package/dist/components/field.js +3 -3
- package/dist/components/main.d.ts +134 -71
- package/dist/components/main.js +245 -174
- package/dist/components/menu.d.ts +72 -14
- package/dist/components/menu.js +231 -34
- package/dist/components/pages.d.ts +638 -0
- package/dist/components/pages.js +1510 -0
- package/dist/components/panels.d.ts +448 -225
- package/dist/components/panels.js +819 -435
- package/dist/components/tabs.d.ts +37 -0
- package/dist/components/tabs.js +128 -69
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/glyphs.d.ts +24 -0
- package/dist/glyphs.js +25 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -4
- package/dist/staffa.esm.js +1 -1
- package/dist/theme.d.ts +67 -0
- package/dist/theme.js +12 -2
- package/package.json +2 -2
- package/skill/BoxOptions.md +7 -12
- package/skill/IconButtonOptions.md +41 -0
- package/skill/MainOptions.md +106 -58
- package/skill/MenuItem.md +16 -1
- package/skill/MenuListOptions.md +24 -0
- package/skill/MenuOptions.md +3 -2
- package/skill/Panel.md +190 -0
- package/skill/PanelStack.md +106 -0
- package/skill/SKILL.md +172 -64
- package/skill/ScrollStripOptions.md +21 -0
- package/skill/box.md +1 -4
- package/skill/closeNav.md +3 -3
- package/skill/iconButton.md +27 -0
- package/skill/main.md +13 -9
- package/skill/menu.md +29 -0
- package/skill/scrollStrip.md +28 -0
- package/src/components/autocomplete.ts +1 -1
- package/src/components/box.ts +29 -39
- package/src/components/button.ts +109 -8
- package/src/components/buttonChooser.ts +1 -1
- package/src/components/checkbox.ts +3 -3
- package/src/components/field.ts +3 -3
- package/src/components/main.ts +381 -188
- package/src/components/menu.ts +265 -37
- package/src/components/panels.ts +1136 -526
- package/src/components/tabs.ts +134 -68
- package/src/core.ts +1 -1
- package/src/index.ts +4 -4
- package/src/theme.ts +14 -3
- package/skill/Page.md +0 -119
- package/skill/panels.md +0 -10
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
## scrollStrip · function
|
|
2
|
+
|
|
3
|
+
A horizontal row that scrolls when its content outgrows it, with a ‹ / ›
|
|
4
|
+
button appearing over whichever end still has something left to reach — a
|
|
5
|
+
bare scroll area says nothing about itself to a mouse, and a scrollbar under
|
|
6
|
+
a row of chrome reads as a mistake. The row's own scrollbar is hidden, and
|
|
7
|
+
the buttons scroll it by most of a width at a time.
|
|
8
|
+
|
|
9
|
+
This is what `tabs` puts its tab strip in, and what the routed
|
|
10
|
+
`main` shell puts its breadcrumb stack in. Reach for it for any row of
|
|
11
|
+
chrome that can outgrow its space: a filter bar, a row of chips, a toolbar.
|
|
12
|
+
To bring one of its children into view — after selecting it from elsewhere,
|
|
13
|
+
say — call `revealInStrip` with that child.
|
|
14
|
+
|
|
15
|
+
**Signature:** `(opts: ScrollStripOptions) => void`
|
|
16
|
+
|
|
17
|
+
**Parameters:**
|
|
18
|
+
|
|
19
|
+
- `opts: ScrollStripOptions`
|
|
20
|
+
|
|
21
|
+
**Examples:**
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
S.scrollStrip({
|
|
25
|
+
attrs: "gap:$1",
|
|
26
|
+
content: () => { for (const tag of tags) S.button({ content: tag, attrs: ".small" }); },
|
|
27
|
+
});
|
|
28
|
+
```
|
|
@@ -161,7 +161,7 @@ export function autocomplete(opts: AutocompleteOptions): void {
|
|
|
161
161
|
});
|
|
162
162
|
|
|
163
163
|
inputEl = A("input type=text role=combobox autocomplete=off", () => {
|
|
164
|
-
A(
|
|
164
|
+
A("id=", id, `aria-controls=${menuId} aria-autocomplete=list`);
|
|
165
165
|
if (opts.placeholder != null) A("placeholder=", opts.placeholder);
|
|
166
166
|
if (opts.disabled) A("disabled=true");
|
|
167
167
|
if (opts.required) A("aria-required=true");
|
package/src/components/box.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
import { type ContentOptions, type Slot, type Attributes, drawSlot } from "../core.js";
|
|
3
|
-
import {
|
|
3
|
+
import { x as closeIcon } from "../icons.js";
|
|
4
|
+
import { iconButton } from "./button.js";
|
|
4
5
|
|
|
5
6
|
/** Options for {@link box}. */
|
|
6
7
|
export interface BoxOptions extends ContentOptions {
|
|
@@ -9,21 +10,16 @@ export interface BoxOptions extends ContentOptions {
|
|
|
9
10
|
/** Footer content, drawn in a styled bar below the body. */
|
|
10
11
|
footer?: Slot;
|
|
11
12
|
/**
|
|
12
|
-
* Draws a small ✕ button in the box's top-right corner
|
|
13
|
+
* Draws a small ✕ button in the box's top-right corner — in the header row when
|
|
13
14
|
* there is a {@link BoxOptions.header | header}, floating over the body when
|
|
14
|
-
* there isn't.
|
|
15
|
+
* there isn't — and runs this when it's clicked.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* any column, top of the stack or not. A box in a column further left closes
|
|
21
|
-
* just that column and leaves the others alone. Outside a routed shell it
|
|
22
|
-
* does nothing but warn.
|
|
23
|
-
*
|
|
24
|
-
* Pass a function to run that instead, for a dismissal of your own.
|
|
17
|
+
* It is plain furniture: a box that happens to sit in a page of a routed
|
|
18
|
+
* `S.main()` does **not** close that page — the shell's breadcrumbs are the
|
|
19
|
+
* way out of those. Wire it to `$panel.close()` yourself if a box really is
|
|
20
|
+
* the whole page and wants its own ✕.
|
|
25
21
|
*/
|
|
26
|
-
close?:
|
|
22
|
+
close?: () => void;
|
|
27
23
|
/** Aberdeen attr/style string applied to the body (content-holding) element. */
|
|
28
24
|
contentAttrs?: Attributes;
|
|
29
25
|
/** Aberdeen attr/style string applied to the header bar. */
|
|
@@ -49,16 +45,11 @@ A.insertGlobalCss({
|
|
|
49
45
|
"> header": "display:flex align-items:center gap:$2 padding: $2 $3; border:0 border-bottom: 1px solid $s-faint; r:0 font-weight:600",
|
|
50
46
|
"> footer": "display:flex align-items:center justify-content:flex-end gap:$2 padding: $2 $3; border:0 border-top: 1px solid $s-faint; r:0",
|
|
51
47
|
"> div": "p:$3 gap:$3",
|
|
52
|
-
// The
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"width:1.6rem height:1.6rem p:0 border:0 background:transparent cursor:pointer " +
|
|
58
|
-
"fg:$s-muted font-size:0.95rem line-height:1 r:$s-radius-sm " +
|
|
59
|
-
"transition: color 0.12s, background 0.12s;",
|
|
60
|
-
".s-box-close:hover": "fg:$s-text background: color-mix(in srgb, $s-text 8%, transparent);",
|
|
61
|
-
// Without a header there is no row to sit in, so it floats over the body.
|
|
48
|
+
// The ✕ is an `S.iconButton` (see `drawCloseButton`); this only places it.
|
|
49
|
+
// In a header row it parks at the far end...
|
|
50
|
+
"> header > .s-box-close": "margin-left:auto",
|
|
51
|
+
// ...and without a header there is no row to sit in, so it floats over the
|
|
52
|
+
// body's top-right corner instead.
|
|
62
53
|
"> .s-box-close": "position:absolute top:$2 right:$2 z-index:1",
|
|
63
54
|
},
|
|
64
55
|
});
|
|
@@ -73,9 +64,6 @@ A.insertGlobalCss({
|
|
|
73
64
|
*
|
|
74
65
|
* Shortcut: pass a function to use it directly as the body content.
|
|
75
66
|
*
|
|
76
|
-
* {@link BoxOptions.close | `close: true`} adds a ✕ that closes the panel the box
|
|
77
|
-
* is drawn in: the usual way back out of a screen in a routed `S.main()`.
|
|
78
|
-
*
|
|
79
67
|
* @example
|
|
80
68
|
* ```ts
|
|
81
69
|
* const $user = A.proxy({name: "Kvothe"});
|
|
@@ -83,7 +71,7 @@ A.insertGlobalCss({
|
|
|
83
71
|
* S.textline({ label: "Name", bind: A.ref($user, "name") });
|
|
84
72
|
* }});
|
|
85
73
|
* S.box(() => A("p#Just some content")); // shorthand
|
|
86
|
-
* S.box({ header: "
|
|
74
|
+
* S.box({ header: "Draft", close: () => discard(), content: drawDraft }); // ✕ runs discard()
|
|
87
75
|
* ```
|
|
88
76
|
*/
|
|
89
77
|
export function box(opts: BoxOptions | Slot = {}): void {
|
|
@@ -93,12 +81,15 @@ export function box(opts: BoxOptions | Slot = {}): void {
|
|
|
93
81
|
// Header and footer get their own scopes so toggling them doesn't recreate
|
|
94
82
|
// the body (which may hold focused inputs / lots of content).
|
|
95
83
|
A(() => {
|
|
84
|
+
// The typeof guards against v0.9's `close: true` (removed API) reaching
|
|
85
|
+
// us from unchecked JS: a ✕ whose handler isn't a function would render
|
|
86
|
+
// but do nothing, which is worse than not rendering at all.
|
|
96
87
|
if (o.header != null) {
|
|
97
88
|
A("header.s-s.neutral", o.headerAttrs, () => {
|
|
98
89
|
drawSlot(o.header);
|
|
99
|
-
if (o.close) drawCloseButton(o.close);
|
|
90
|
+
if (typeof o.close === "function") drawCloseButton(o.close);
|
|
100
91
|
});
|
|
101
|
-
} else if (o.close) {
|
|
92
|
+
} else if (typeof o.close === "function") {
|
|
102
93
|
drawCloseButton(o.close);
|
|
103
94
|
}
|
|
104
95
|
});
|
|
@@ -114,16 +105,15 @@ export function box(opts: BoxOptions | Slot = {}): void {
|
|
|
114
105
|
}
|
|
115
106
|
|
|
116
107
|
/**
|
|
117
|
-
* The box's
|
|
118
|
-
*
|
|
119
|
-
*
|
|
108
|
+
* The box's ✕: one definition, so the glyph, the label and the hit area are
|
|
109
|
+
* identical whether it sits in the header row or floats over a headerless body.
|
|
110
|
+
* The `.s-box-close` class is only a hook for the placement rules above.
|
|
120
111
|
*/
|
|
121
|
-
function drawCloseButton(close:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
A("span aria-hidden=true #✕");
|
|
112
|
+
function drawCloseButton(close: () => void): void {
|
|
113
|
+
iconButton({
|
|
114
|
+
icon: closeIcon,
|
|
115
|
+
ariaLabel: "Close",
|
|
116
|
+
click: close,
|
|
117
|
+
attrs: ".s-box-close",
|
|
128
118
|
});
|
|
129
119
|
}
|
package/src/components/button.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
import { type Slot, type Attributes, drawSlot } from "../core.js";
|
|
3
3
|
|
|
4
|
+
/** Options for {@link iconButton}. */
|
|
5
|
+
export interface IconButtonOptions {
|
|
6
|
+
/** The glyph, usually one of the `staffa/icons` draw functions. */
|
|
7
|
+
icon: Slot;
|
|
8
|
+
/** What it does, for screen readers. Required: there is no visible text to read. */
|
|
9
|
+
ariaLabel: string;
|
|
10
|
+
/** Click handler. */
|
|
11
|
+
click?: (event: Event) => void;
|
|
12
|
+
/** Render as a link (`<a role=button>`) pointing here instead of a `<button>`. */
|
|
13
|
+
href?: string;
|
|
14
|
+
/** Disables it. */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Aberdeen attr/style string applied to the button. `.small` and `.large`
|
|
18
|
+
* size the hit area (medium is the default and needs no class); a `.small`
|
|
19
|
+
* or `.large` parent sizes the ones inside it, as with {@link button}.
|
|
20
|
+
*/
|
|
21
|
+
attrs?: Attributes;
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
/** Options for {@link button}. */
|
|
5
25
|
export interface ButtonOptions {
|
|
6
26
|
/** Button content: a string for plain text, or a function for custom markup. */
|
|
@@ -51,6 +71,11 @@ A.insertGlobalCss({
|
|
|
51
71
|
// button (which is already near-white) darkens toward its ink instead.
|
|
52
72
|
"&.tonal:hover, &.outlined:hover": "background: color-mix(in srgb, $s-bg 24%, transparent);",
|
|
53
73
|
"&.neutral:hover": "filter:none background: color-mix(in srgb, $s-text 8%, $s-bg);",
|
|
74
|
+
// The button sizes its glyph, for the same reason `.s-icon-btn` does below:
|
|
75
|
+
// a caller can't know what the button beside it passed, and only a rule
|
|
76
|
+
// here makes every icon in a row come out alike. It rides the font size,
|
|
77
|
+
// so a `.small`/`.large` button scales its icon with its text.
|
|
78
|
+
"> svg": "width:1.25em height:1.25em",
|
|
54
79
|
// Subtle press feedback.
|
|
55
80
|
"&:active:not(:disabled)": "transform: translateY(1px)",
|
|
56
81
|
// Size: set on the button itself, or inherited from a `.small`/`.large`
|
|
@@ -58,8 +83,91 @@ A.insertGlobalCss({
|
|
|
58
83
|
"&.small, .small > &": "padding: $m1 $m2; font-size:0.85em border-radius:$s-radius-sm",
|
|
59
84
|
"&.large, .large > &": "font-size:1.4em border-radius:$s-radius-lg",
|
|
60
85
|
},
|
|
86
|
+
// A bare glyph in a square hit area: no fill and no edge, just ink that lifts
|
|
87
|
+
// on hover. Deliberately *not* a `.s-s` surface — chrome that sits beside a
|
|
88
|
+
// title (a ✕, a ☰) should read as an affordance on the bar, not as
|
|
89
|
+
// another button competing with it, and a filled or outlined box around a
|
|
90
|
+
// 16px glyph is exactly what makes a top bar look busy.
|
|
91
|
+
".s-icon-btn": {
|
|
92
|
+
"&":
|
|
93
|
+
"display:inline-flex align-items:center justify-content:center flex-shrink:0 " +
|
|
94
|
+
"width:2rem height:2rem p:0 border:0 background:transparent cursor:pointer " +
|
|
95
|
+
"fg:$s-muted r:$s-radius-sm line-height:1 font-size:1rem text-decoration:none " +
|
|
96
|
+
"transition: color 0.12s, background 0.12s;",
|
|
97
|
+
// The container sizes the glyph, rather than trusting whatever the caller
|
|
98
|
+
// passed: a row of icon buttons only reads as a row when every glyph in it
|
|
99
|
+
// is the same size, and the caller of one of them can't know about the
|
|
100
|
+
// others. CSS beats the `width`/`height` attributes the icon set writes, so
|
|
101
|
+
// `iconButton({ icon: trash2 })` and a hand-sized glyph come out alike; an
|
|
102
|
+
// `attrs` override still wins over this, being an inline style. The same
|
|
103
|
+
// rule is on `.s-btn` above and on a floating menu's rows in menu.ts, so
|
|
104
|
+
// one `1.25em` governs the lot. (`S.main`'s nav rows are deliberately out
|
|
105
|
+
// of it — see the note there.)
|
|
106
|
+
"> svg": "width:1.25em height:1.25em",
|
|
107
|
+
// The ink resolves against whatever surface it sits on, so one treatment
|
|
108
|
+
// works on the page, in a box header, and on a coloured bar alike.
|
|
109
|
+
"&:hover:not(:disabled):not([aria-disabled=true])":
|
|
110
|
+
"fg:$s-text background: color-mix(in srgb, $s-text 10%, transparent);",
|
|
111
|
+
"&:focus-visible": "outline: 3px solid $s-focus; outline-offset:1px",
|
|
112
|
+
// The glyph rides the font size, so it scales with the hit area.
|
|
113
|
+
"&.small, .small > &": "width:1.6rem height:1.6rem font-size:0.8rem",
|
|
114
|
+
"&.large, .large > &": "width:2.4rem height:2.4rem font-size:1.2rem",
|
|
115
|
+
},
|
|
61
116
|
});
|
|
62
117
|
|
|
118
|
+
/**
|
|
119
|
+
* A bare glyph in a square hit area — no fill, no border, just ink that lifts on
|
|
120
|
+
* hover. The quiet end of the button family, for chrome that has to sit beside
|
|
121
|
+
* something more important without competing with it: a ✕ on a box, the ☰ a
|
|
122
|
+
* routed `S.main()` puts in its top bar, the verbs in a
|
|
123
|
+
* {@link Panel.actions | page's actions}.
|
|
124
|
+
*
|
|
125
|
+
* Reach for {@link button} instead whenever the thing has a name worth reading;
|
|
126
|
+
* an icon alone is only unambiguous for a handful of universal actions.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* import { trash2, share2 } from "staffa/icons";
|
|
131
|
+
*
|
|
132
|
+
* $panel.actions = () => {
|
|
133
|
+
* S.iconButton({ icon: share2, ariaLabel: "Share", click: share });
|
|
134
|
+
* S.iconButton({ icon: trash2, ariaLabel: "Delete", click: del, attrs: "fg:$s-danger" });
|
|
135
|
+
* };
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function iconButton(opts: IconButtonOptions): void {
|
|
139
|
+
const tag = opts.href != null ? "a" : "button";
|
|
140
|
+
A(`${tag}.s-icon-btn`, opts.attrs, () => {
|
|
141
|
+
applyActionBehavior(opts);
|
|
142
|
+
A("aria-label=", opts.ariaLabel);
|
|
143
|
+
drawSlot(opts.icon);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The link-or-button plumbing {@link button} and {@link iconButton} share:
|
|
149
|
+
* href/type, disabling, label and click. A disabled link keeps `role=button`
|
|
150
|
+
* and `aria-disabled` but loses its `href` — an anchor without one is out of
|
|
151
|
+
* the tab order and follows nothing, which is what makes it as disabled as
|
|
152
|
+
* the `<button>` form's real `disabled` attribute.
|
|
153
|
+
*/
|
|
154
|
+
function applyActionBehavior(o: {
|
|
155
|
+
href?: string;
|
|
156
|
+
disabled?: boolean;
|
|
157
|
+
click?: (event: Event) => void;
|
|
158
|
+
type?: string;
|
|
159
|
+
}): void {
|
|
160
|
+
if (o.href != null) {
|
|
161
|
+
A("role=button");
|
|
162
|
+
if (o.disabled) A("aria-disabled=true");
|
|
163
|
+
else A("href=", o.href);
|
|
164
|
+
} else {
|
|
165
|
+
A("type=", o.type ?? "button");
|
|
166
|
+
if (o.disabled) A("disabled=true");
|
|
167
|
+
}
|
|
168
|
+
if (o.click && !o.disabled) A("click=", o.click);
|
|
169
|
+
}
|
|
170
|
+
|
|
63
171
|
/**
|
|
64
172
|
* A button. Tonal and outlined variants show a border; filled variants rely on
|
|
65
173
|
* their solid background for affordance.
|
|
@@ -94,15 +202,8 @@ export function button(opts: ButtonOptions | Slot = {}): void {
|
|
|
94
202
|
// role (`.danger`, `.neutral`, a custom `.brand`) or variant (`.outlined`); no
|
|
95
203
|
// role detection needed, since the default lives in CSS, not here.
|
|
96
204
|
A(`${tag}.s-btn.s-s.shadow`, o.attrs, () => {
|
|
97
|
-
|
|
98
|
-
A(`href=${o.href} role=button`);
|
|
99
|
-
if (o.disabled) A("aria-disabled=true");
|
|
100
|
-
} else {
|
|
101
|
-
A("type=", o.type ?? "button");
|
|
102
|
-
if (o.disabled) A("disabled=true");
|
|
103
|
-
}
|
|
205
|
+
applyActionBehavior(o);
|
|
104
206
|
if (o.ariaLabel) A("aria-label=", o.ariaLabel);
|
|
105
|
-
if (o.click) A("click=", o.click);
|
|
106
207
|
|
|
107
208
|
drawSlot(o.icon);
|
|
108
209
|
drawSlot(o.content);
|
|
@@ -62,6 +62,6 @@ export function buttonChooser(opts: ButtonChooserOptions): void {
|
|
|
62
62
|
|
|
63
63
|
if (opts.name) {
|
|
64
64
|
// Hidden input carries the value into native form submission.
|
|
65
|
-
A(() => A(
|
|
65
|
+
A(() => A("input type=hidden name=", opts.name, "value=", opts.bind.value ?? ""));
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -40,10 +40,10 @@ export function checkbox(opts: CheckboxOptions = {}): void {
|
|
|
40
40
|
const id = opts.id ?? uniqueId("check");
|
|
41
41
|
|
|
42
42
|
A("div.s-check", opts.attrs, () => {
|
|
43
|
-
A(
|
|
43
|
+
A("label for=", id, () => {
|
|
44
44
|
A("input type=checkbox", opts.inputAttrs, () => {
|
|
45
|
-
A(
|
|
46
|
-
if (opts.name) A(
|
|
45
|
+
A("id=", id);
|
|
46
|
+
if (opts.name) A("name=", opts.name);
|
|
47
47
|
// `checked` is a boolean attribute: only set it when actually true.
|
|
48
48
|
if (opts.checked && !opts.bind) A("checked=true");
|
|
49
49
|
if (opts.change) A("change=", opts.change);
|
package/src/components/field.ts
CHANGED
|
@@ -75,7 +75,7 @@ export function drawField(
|
|
|
75
75
|
A("div.s-field", opts.attrs, () => {
|
|
76
76
|
A(() => {
|
|
77
77
|
if (opts.label != null) {
|
|
78
|
-
A(
|
|
78
|
+
A("label for=", id, () => {
|
|
79
79
|
drawSlot(opts.label);
|
|
80
80
|
if (opts.required) A("span.s-req aria-hidden=true #*");
|
|
81
81
|
});
|
|
@@ -104,8 +104,8 @@ export function applyControlAttrs(
|
|
|
104
104
|
isInvalid: () => boolean,
|
|
105
105
|
bind?: Bindable<unknown>,
|
|
106
106
|
): void {
|
|
107
|
-
A(
|
|
108
|
-
if (opts.name) A(
|
|
107
|
+
A("id=", id);
|
|
108
|
+
if (opts.name) A("name=", opts.name);
|
|
109
109
|
A(() => {
|
|
110
110
|
if (opts.disabled) A("disabled=true");
|
|
111
111
|
});
|