staffa 0.3.1 → 0.4.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 +45 -23
- package/dist/components/box.d.ts +2 -2
- package/dist/components/box.js +3 -4
- package/dist/components/button.d.ts +9 -11
- package/dist/components/button.js +16 -18
- package/dist/components/buttonChooser.d.ts +5 -5
- package/dist/components/buttonChooser.js +7 -5
- package/dist/components/buttonGroup.d.ts +3 -3
- package/dist/components/buttonGroup.js +5 -5
- package/dist/components/dialog.d.ts +2 -2
- package/dist/components/dialog.js +10 -10
- package/dist/components/form.d.ts +4 -4
- package/dist/components/form.js +6 -6
- package/dist/components/main.d.ts +5 -5
- package/dist/components/main.js +28 -40
- package/dist/components/menu.d.ts +4 -4
- package/dist/components/menu.js +8 -6
- package/dist/components/tabs.d.ts +3 -3
- package/dist/components/tabs.js +1 -1
- package/dist/components/toast.d.ts +1 -3
- package/dist/components/toast.js +2 -2
- package/dist/components/tooltip.js +2 -2
- package/dist/core.d.ts +10 -4
- package/dist/core.js +13 -0
- package/dist/index.d.ts +19 -65
- package/dist/index.js +18 -48
- package/dist/staffa.esm.js +1 -1
- package/dist/theme.d.ts +0 -7
- package/dist/theme.js +37 -14
- package/package.json +9 -2
- package/src/components/box.ts +5 -5
- package/src/components/button.ts +21 -22
- package/src/components/buttonChooser.ts +10 -8
- package/src/components/buttonGroup.ts +5 -4
- package/src/components/dialog.ts +10 -10
- package/src/components/form.ts +8 -8
- package/src/components/main.ts +31 -40
- package/src/components/menu.ts +9 -7
- package/src/components/tabs.ts +4 -4
- package/src/components/toast.ts +3 -5
- package/src/components/tooltip.ts +2 -2
- package/src/core.ts +16 -5
- package/src/index.ts +21 -76
- package/src/theme.ts +36 -22
package/README.md
CHANGED
|
@@ -4,25 +4,23 @@ A small, opinionated TypeScript component library for the [Aberdeen](https://abe
|
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import A from "aberdeen";
|
|
7
|
-
import S from "staffa";
|
|
7
|
+
import * as S from "staffa";
|
|
8
8
|
|
|
9
9
|
const $user = A.proxy({ name: "", email: "" });
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
});
|
|
11
|
+
S.main({
|
|
12
|
+
title: "Sign up",
|
|
13
|
+
maxWidth: "40rem",
|
|
14
|
+
content: () => {
|
|
15
|
+
S.form({
|
|
16
|
+
submit: () => console.log(A.unproxy($user)),
|
|
17
|
+
content: () => {
|
|
18
|
+
S.textline({ label: "Name", required: true, bind: A.ref($user, "name") });
|
|
19
|
+
S.textline({ label: "Email", type: "email", bind: A.ref($user, "email") });
|
|
20
|
+
},
|
|
21
|
+
actions: () => S.button({ content: "Create account", type: "submit" }),
|
|
22
|
+
});
|
|
23
|
+
},
|
|
26
24
|
});
|
|
27
25
|
```
|
|
28
26
|
|
|
@@ -43,7 +41,7 @@ Aberdeen is a peer dependency. Staffa is published as ESM with TypeScript types.
|
|
|
43
41
|
Every component takes a single typed options object and draws DOM via Aberdeen. No classes, no web components. The `S` object collects all component functions:
|
|
44
42
|
|
|
45
43
|
```ts
|
|
46
|
-
S.button({
|
|
44
|
+
S.button({ content: "Save", disabled: false });
|
|
47
45
|
S.box({ header: "Settings", content: () => { ... } });
|
|
48
46
|
```
|
|
49
47
|
|
|
@@ -52,7 +50,7 @@ S.box({ header: "Settings", content: () => { ... } });
|
|
|
52
50
|
All components get their options in a typed object. The object may be an Aberdeen proxy, if you want to update the component in-place.
|
|
53
51
|
|
|
54
52
|
```ts
|
|
55
|
-
const $btn = A.proxy({
|
|
53
|
+
const $btn = A.proxy({ content: "Save", disabled: false });
|
|
56
54
|
S.button($btn);
|
|
57
55
|
// ...later:
|
|
58
56
|
$btn.disabled = true; // button updates instantly
|
|
@@ -63,7 +61,7 @@ $btn.disabled = true; // button updates instantly
|
|
|
63
61
|
Anywhere a component takes content, a `label`, `header`, button `text`, dialog body, etc, you can pass either a string or a `() => void` draw function. Strings render as **rich text**: `*italic*`, `**bold**`, `` `code` ``, `[link](/path)`. All text is safely escaped.
|
|
64
62
|
|
|
65
63
|
```ts
|
|
66
|
-
S.button({
|
|
64
|
+
S.button({ content: "Save **now**" });
|
|
67
65
|
S.box({ header: "See the [docs](/docs)", content: () => { ... } });
|
|
68
66
|
```
|
|
69
67
|
|
|
@@ -78,7 +76,7 @@ Staffa builds on **surfaces**: elements marked with `.s-s` that have their own b
|
|
|
78
76
|
Components are built from these (`S.button` is a `.s-s.primary.filled`, `S.box` a `.s-s.panel`, etc.). Because component options include an optional `attrs` string, which has Aberdeen `A()` string semantics, you can easily override it:
|
|
79
77
|
|
|
80
78
|
```ts
|
|
81
|
-
S.button({
|
|
79
|
+
S.button({ content: "Delete", attrs: ".danger" });
|
|
82
80
|
S.box({ attrs: ".raised.outlined", content: () => { ... } });
|
|
83
81
|
```
|
|
84
82
|
|
|
@@ -120,7 +118,7 @@ If you need further customization, just add some CSS to override the default sty
|
|
|
120
118
|
A.insertGlobalCss({".s-s.my-surface": "--s-a:white --s-b:#ef6b00"});
|
|
121
119
|
|
|
122
120
|
S.button({
|
|
123
|
-
|
|
121
|
+
content: "You'll want to click me",
|
|
124
122
|
attrs: ".my-surface",
|
|
125
123
|
click: () => S.alert("Good work!", {attrs: ".my-surface"})
|
|
126
124
|
});
|
|
@@ -184,7 +182,7 @@ import { sparkles, bell } from "staffa/icons.js";
|
|
|
184
182
|
Each icon is a draw function usable anywhere a slot is accepted (e.g. a button `icon`), or called directly. Customize per call, or globally via `setDefaults()`:
|
|
185
183
|
|
|
186
184
|
```ts
|
|
187
|
-
S.button({
|
|
185
|
+
S.button({ content: "Save", icon: bell });
|
|
188
186
|
sparkles({ size: "1.5em", color: "var(--s-primary)", strokeWidth: 1.5 });
|
|
189
187
|
```
|
|
190
188
|
|
|
@@ -213,7 +211,7 @@ Two-way binding uses Aberdeen proxies: pass `bind: A.ref($obj, "key")` to form f
|
|
|
213
211
|
</script>
|
|
214
212
|
<script type="module">
|
|
215
213
|
import A from "aberdeen";
|
|
216
|
-
import S from "staffa/all.js";
|
|
214
|
+
import * as S from "staffa/all.js";
|
|
217
215
|
// ...
|
|
218
216
|
</script>
|
|
219
217
|
```
|
|
@@ -265,4 +263,28 @@ npm run build # compile TypeScript to dist/
|
|
|
265
263
|
npm run typecheck # check types
|
|
266
264
|
npm run smoke # render every component in jsdom
|
|
267
265
|
npx http-server # allows demo to be viewed at http://localhost:8080/demo
|
|
266
|
+
npx shotest test # visual tests: click through the demo, screenshotting every step
|
|
267
|
+
npx shotest review # review/accept the visual changes against the baseline
|
|
268
268
|
```
|
|
269
|
+
|
|
270
|
+
The visual tests (`tests/*.spec.ts`) need a build first (`npm run build`); they serve the repo root themselves and click through every demo page. Accepted baselines live in `test-accepted/`.
|
|
271
|
+
|
|
272
|
+
## AI skill
|
|
273
|
+
|
|
274
|
+
If you use Claude Code, GitHub Copilot or another AI agents that supports Skills, Staffa includes a `skill/` directory that provides specialized knowledge to the AI about how to use the library effectively.
|
|
275
|
+
|
|
276
|
+
To use this, it is recommended to symlink the skill into your project's `.claude/skills` directory:
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
mkdir -p .claude/skills
|
|
280
|
+
ln -s ../../node_modules/staffa/skill .claude/skills/staffa
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Breaking changes
|
|
284
|
+
|
|
285
|
+
- **0.4**
|
|
286
|
+
- There is no default export anymore: replace `import S from "staffa"` with `import * as S from "staffa"`.
|
|
287
|
+
- `S.button` no longer has a `text` option: use `content` instead (it accepts a string or a draw function).
|
|
288
|
+
- The `Content` type is gone: use `Slot` instead. The `Styling` type alias is now exported as `Attributes`.
|
|
289
|
+
- `S.buttonChooser` uses `undefined` instead of `null` for "nothing selected" (in `bind` and with `allowDeselect`).
|
|
290
|
+
|
package/dist/components/box.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ContentOptions, type Slot, type Attributes } from "../core.js";
|
|
2
2
|
/** Options for {@link box}. */
|
|
3
3
|
export interface BoxOptions extends ContentOptions {
|
|
4
4
|
/** Header content, drawn in a styled bar above the body. */
|
|
@@ -30,4 +30,4 @@ export interface BoxOptions extends ContentOptions {
|
|
|
30
30
|
* S.box(() => A("p#Just some content")); // shorthand
|
|
31
31
|
* ```
|
|
32
32
|
*/
|
|
33
|
-
export declare function box(opts?: BoxOptions |
|
|
33
|
+
export declare function box(opts?: BoxOptions | Slot): void;
|
package/dist/components/box.js
CHANGED
|
@@ -9,7 +9,7 @@ A.insertGlobalCss({
|
|
|
9
9
|
"&": "display:flex flex-direction:column border: 1px solid $s-border; r: $s-radius-lg; overflow:hidden box-shadow: $s-shadow;",
|
|
10
10
|
"&:not(:first-child)": "margin-top: $3",
|
|
11
11
|
"> header": "display:flex align-items:center gap:$2 padding: $2 $3; border-bottom: 1px solid $s-border; font-weight:600",
|
|
12
|
-
"> footer": "display:flex align-items:center gap:$2 padding: $2 $3; border-top: 1px solid $s-border;",
|
|
12
|
+
"> footer": "display:flex align-items:center justify-content:flex-end gap:$2 padding: $2 $3; border-top: 1px solid $s-border;",
|
|
13
13
|
"> div": "p:$3 gap:$3",
|
|
14
14
|
},
|
|
15
15
|
});
|
|
@@ -32,7 +32,7 @@ A.insertGlobalCss({
|
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
34
|
export function box(opts = {}) {
|
|
35
|
-
const o = typeof opts === "function" ? { content: opts } : opts;
|
|
35
|
+
const o = typeof opts === "string" || typeof opts === "function" ? { content: opts } : opts;
|
|
36
36
|
A("section.s-box.s-s.panel", o.attrs, () => {
|
|
37
37
|
// Header and footer get their own scopes so toggling them doesn't recreate
|
|
38
38
|
// the body (which may hold focused inputs / lots of content).
|
|
@@ -41,8 +41,7 @@ export function box(opts = {}) {
|
|
|
41
41
|
A("header.s-s.raised", o.headerAttrs, () => drawSlot(o.header));
|
|
42
42
|
});
|
|
43
43
|
A("div", o.contentAttrs, () => {
|
|
44
|
-
|
|
45
|
-
o.content();
|
|
44
|
+
drawSlot(o.content);
|
|
46
45
|
});
|
|
47
46
|
A(() => {
|
|
48
47
|
if (o.footer != null)
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Slot, type Attributes } from "../core.js";
|
|
2
2
|
/** Options for {@link button}. */
|
|
3
3
|
export interface ButtonOptions {
|
|
4
|
-
/** Button
|
|
5
|
-
|
|
6
|
-
/** Custom content (overrides {@link ButtonOptions.text | text}). */
|
|
7
|
-
content?: Content;
|
|
4
|
+
/** Button content: a string for plain text, or a function for custom markup. */
|
|
5
|
+
content?: Slot;
|
|
8
6
|
/** Leading icon/adornment, drawn before the label. */
|
|
9
7
|
icon?: Slot;
|
|
10
8
|
/** Click handler. */
|
|
@@ -39,15 +37,15 @@ export interface ButtonOptions {
|
|
|
39
37
|
* startup) for SPA-style navigation without manual click handlers:
|
|
40
38
|
* ```ts
|
|
41
39
|
* interceptLinks(); // once at root
|
|
42
|
-
* S.button({ href: "/dashboard",
|
|
40
|
+
* S.button({ href: "/dashboard", content: "Dashboard" }); // navigates via router
|
|
43
41
|
* ```
|
|
44
42
|
*
|
|
45
43
|
* @example
|
|
46
44
|
* ```ts
|
|
47
|
-
* S.button({
|
|
48
|
-
* S.button({
|
|
49
|
-
* S.button("Cancel"); // shorthand for {
|
|
50
|
-
* S.button({ href: "/docs",
|
|
45
|
+
* S.button({ content: "Save", click: save });
|
|
46
|
+
* S.button({ content: "Delete", attrs: ".danger .outlined", click: del });
|
|
47
|
+
* S.button("Cancel"); // shorthand for { content: "Cancel" }
|
|
48
|
+
* S.button({ href: "/docs", content: "Docs" }); // renders an <a role=button>
|
|
51
49
|
* ```
|
|
52
50
|
*/
|
|
53
|
-
export declare function button(opts?: ButtonOptions |
|
|
51
|
+
export declare function button(opts?: ButtonOptions | Slot): void;
|
|
@@ -17,14 +17,15 @@ A.insertGlobalCss({
|
|
|
17
17
|
"&:hover": "filter: brightness(1.08); transform: translateY(-1px)",
|
|
18
18
|
"&.tonal:hover, &.outlined:hover": "background: color-mix(in srgb, $s-b 26%, transparent);",
|
|
19
19
|
// A filled `.gradient` button (the default) is the app's signature call to
|
|
20
|
-
// action: a borderless gradient with a
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// gradient
|
|
26
|
-
|
|
27
|
-
"&.gradient:not(.tonal):not(.outlined)
|
|
20
|
+
// action: a borderless gradient with a hairline top highlight (a hint of
|
|
21
|
+
// top-lighting that sells the fill as a lit, rounded shape) over a soft glow.
|
|
22
|
+
// The gradient fill itself comes from the `.s-s.gradient` surface rule in
|
|
23
|
+
// theme.ts. No border: a filled gradient reads as one solid shape. Dropping
|
|
24
|
+
// the border (rather than making it transparent) also sidesteps a Chromium
|
|
25
|
+
// artifact where a gradient clipped to a transparent rounded border fringes
|
|
26
|
+
// the edge with the gradient's far colour.
|
|
27
|
+
"&.gradient:not(.tonal):not(.outlined)": "border:0 box-shadow: inset 0 1px 0 color-mix(in srgb, white 25%, transparent), $s-glow;",
|
|
28
|
+
"&.gradient:not(.tonal):not(.outlined):hover": "filter: brightness(1.05); box-shadow: inset 0 1px 0 color-mix(in srgb, white 25%, transparent), 0 7px 18px color-mix(in srgb, $s-primary 34%, transparent); transform: translateY(-1px);",
|
|
28
29
|
// Subtle press feedback.
|
|
29
30
|
"&:active:not(:disabled):not([aria-disabled=true])": "transform: translateY(1px)",
|
|
30
31
|
// Size: set on the button itself, or inherited from a `.small`/`.large`
|
|
@@ -47,19 +48,19 @@ const ROLE_CLASS = /\.(gradient|primary|secondary|neutral|danger|success|warning
|
|
|
47
48
|
* startup) for SPA-style navigation without manual click handlers:
|
|
48
49
|
* ```ts
|
|
49
50
|
* interceptLinks(); // once at root
|
|
50
|
-
* S.button({ href: "/dashboard",
|
|
51
|
+
* S.button({ href: "/dashboard", content: "Dashboard" }); // navigates via router
|
|
51
52
|
* ```
|
|
52
53
|
*
|
|
53
54
|
* @example
|
|
54
55
|
* ```ts
|
|
55
|
-
* S.button({
|
|
56
|
-
* S.button({
|
|
57
|
-
* S.button("Cancel"); // shorthand for {
|
|
58
|
-
* S.button({ href: "/docs",
|
|
56
|
+
* S.button({ content: "Save", click: save });
|
|
57
|
+
* S.button({ content: "Delete", attrs: ".danger .outlined", click: del });
|
|
58
|
+
* S.button("Cancel"); // shorthand for { content: "Cancel" }
|
|
59
|
+
* S.button({ href: "/docs", content: "Docs" }); // renders an <a role=button>
|
|
59
60
|
* ```
|
|
60
61
|
*/
|
|
61
62
|
export function button(opts = {}) {
|
|
62
|
-
const o = typeof opts === "string"
|
|
63
|
+
const o = typeof opts === "string" || typeof opts === "function" ? { content: opts } : opts;
|
|
63
64
|
const tag = o.href != null ? "a" : "button";
|
|
64
65
|
// A filled `.gradient` surface by default — the signature CTA. If the caller's
|
|
65
66
|
// `attrs` already names a surface role we omit the default, so `.danger`,
|
|
@@ -82,9 +83,6 @@ export function button(opts = {}) {
|
|
|
82
83
|
if (o.click)
|
|
83
84
|
A("click=", o.click);
|
|
84
85
|
drawSlot(o.icon);
|
|
85
|
-
|
|
86
|
-
o.content();
|
|
87
|
-
else if (o.text != null)
|
|
88
|
-
A("#", o.text);
|
|
86
|
+
drawSlot(o.content);
|
|
89
87
|
});
|
|
90
88
|
}
|
|
@@ -10,13 +10,13 @@ export interface ButtonChooserOptions {
|
|
|
10
10
|
*/
|
|
11
11
|
options: Record<string, Slot>;
|
|
12
12
|
/**
|
|
13
|
-
* Two-way binding for the selected id, or `
|
|
13
|
+
* Two-way binding for the selected id, or `undefined` when nothing is selected.
|
|
14
14
|
* Use an `A.proxy` or `A.ref`.
|
|
15
15
|
*/
|
|
16
|
-
bind: Bindable<string |
|
|
16
|
+
bind: Bindable<string | undefined>;
|
|
17
17
|
/**
|
|
18
18
|
* When `true`, clicking the already-selected button deselects it, setting
|
|
19
|
-
* `bind.value` to `
|
|
19
|
+
* `bind.value` to `undefined`. Useful for "none / auto" states.
|
|
20
20
|
*/
|
|
21
21
|
allowDeselect?: boolean;
|
|
22
22
|
/** Name attribute for the hidden `<input>`, enabling form submission. */
|
|
@@ -24,14 +24,14 @@ export interface ButtonChooserOptions {
|
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* A single-selection segmented control: an attached button group where exactly
|
|
27
|
-
* one button is active at a time. Optionally allows deselecting back to `
|
|
27
|
+
* one button is active at a time. Optionally allows deselecting back to `undefined`.
|
|
28
28
|
*
|
|
29
29
|
* Renders a hidden `<input>` alongside (when `name` is set) so the selected
|
|
30
30
|
* value is included in native form submission.
|
|
31
31
|
*
|
|
32
32
|
* @example
|
|
33
33
|
* ```ts
|
|
34
|
-
* const $view = A.proxy({ value: "day" as string |
|
|
34
|
+
* const $view = A.proxy({ value: "day" as string | undefined });
|
|
35
35
|
* S.buttonChooser({
|
|
36
36
|
* options: { day: "Day", week: "Week", month: "Month" },
|
|
37
37
|
* bind: $view,
|
|
@@ -2,14 +2,14 @@ import A from "aberdeen";
|
|
|
2
2
|
import { buttonGroup } from "./buttonGroup.js";
|
|
3
3
|
/**
|
|
4
4
|
* A single-selection segmented control: an attached button group where exactly
|
|
5
|
-
* one button is active at a time. Optionally allows deselecting back to `
|
|
5
|
+
* one button is active at a time. Optionally allows deselecting back to `undefined`.
|
|
6
6
|
*
|
|
7
7
|
* Renders a hidden `<input>` alongside (when `name` is set) so the selected
|
|
8
8
|
* value is included in native form submission.
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
11
|
* ```ts
|
|
12
|
-
* const $view = A.proxy({ value: "day" as string |
|
|
12
|
+
* const $view = A.proxy({ value: "day" as string | undefined });
|
|
13
13
|
* S.buttonChooser({
|
|
14
14
|
* options: { day: "Day", week: "Week", month: "Month" },
|
|
15
15
|
* bind: $view,
|
|
@@ -22,11 +22,13 @@ export function buttonChooser(opts) {
|
|
|
22
22
|
buttonGroup({
|
|
23
23
|
attrs: opts.attrs,
|
|
24
24
|
buttons: Object.entries(opts.options).map(([id, label]) => ({
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
content: label,
|
|
26
|
+
// Icon-only options (draw-function labels) get the id as their
|
|
27
|
+
// accessible name; plain-text labels speak for themselves.
|
|
28
|
+
ariaLabel: typeof label === "function" ? id : undefined,
|
|
27
29
|
attrs: selected === id ? ".primary" : ".neutral .outlined",
|
|
28
30
|
click: () => {
|
|
29
|
-
opts.bind.value = (opts.allowDeselect && selected === id) ?
|
|
31
|
+
opts.bind.value = (opts.allowDeselect && selected === id) ? undefined : id;
|
|
30
32
|
},
|
|
31
33
|
})),
|
|
32
34
|
});
|
|
@@ -22,9 +22,9 @@ export interface ButtonGroupOptions extends ContentOptions {
|
|
|
22
22
|
* @example
|
|
23
23
|
* ```ts
|
|
24
24
|
* S.buttonGroup({ buttons: [
|
|
25
|
-
* {
|
|
26
|
-
* {
|
|
27
|
-
* {
|
|
25
|
+
* { content: "Day", attrs: ".neutral .outlined" },
|
|
26
|
+
* { content: "Week", attrs: ".neutral .outlined" },
|
|
27
|
+
* { content: "Month", attrs: ".neutral .outlined" },
|
|
28
28
|
* ]});
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
|
+
import { drawSlot } from "../core.js";
|
|
2
3
|
import { button } from "./button.js";
|
|
3
4
|
A.insertGlobalCss({
|
|
4
5
|
".s-bgroup": {
|
|
@@ -24,9 +25,9 @@ A.insertGlobalCss({
|
|
|
24
25
|
* @example
|
|
25
26
|
* ```ts
|
|
26
27
|
* S.buttonGroup({ buttons: [
|
|
27
|
-
* {
|
|
28
|
-
* {
|
|
29
|
-
* {
|
|
28
|
+
* { content: "Day", attrs: ".neutral .outlined" },
|
|
29
|
+
* { content: "Week", attrs: ".neutral .outlined" },
|
|
30
|
+
* { content: "Month", attrs: ".neutral .outlined" },
|
|
30
31
|
* ]});
|
|
31
32
|
* ```
|
|
32
33
|
*/
|
|
@@ -37,7 +38,6 @@ export function buttonGroup(opts = {}) {
|
|
|
37
38
|
if (opts.buttons)
|
|
38
39
|
for (const b of opts.buttons)
|
|
39
40
|
button(b);
|
|
40
|
-
|
|
41
|
-
opts.content();
|
|
41
|
+
drawSlot(opts.content);
|
|
42
42
|
});
|
|
43
43
|
}
|
|
@@ -52,8 +52,8 @@ export interface DialogOptions {
|
|
|
52
52
|
* header: "Confirm",
|
|
53
53
|
* content: (close) => {
|
|
54
54
|
* A("p #Are you sure?");
|
|
55
|
-
* S.button({
|
|
56
|
-
* S.button({
|
|
55
|
+
* S.button({ content: "Yes", click: () => { doIt(); close(); } });
|
|
56
|
+
* S.button({ content: "Cancel", attrs: ".neutral .outlined", click: close });
|
|
57
57
|
* },
|
|
58
58
|
* });
|
|
59
59
|
* ```
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
|
-
import { drawSlot } from "../core.js";
|
|
2
|
+
import { drawSlot, mountPortal } from "../core.js";
|
|
3
3
|
import { button } from "./button.js";
|
|
4
4
|
import { buttonGroup } from "./buttonGroup.js";
|
|
5
5
|
import { textline } from "./textline.js";
|
|
@@ -17,7 +17,7 @@ A.insertGlobalCss({
|
|
|
17
17
|
"transition: opacity 0.2s ease-out, transform 0.2s ease-out;",
|
|
18
18
|
"> header": "display:flex align-items:center gap:$2 padding: $2 $3; " +
|
|
19
19
|
"border-bottom: 1px solid $s-border; font-weight:600 flex-shrink:0",
|
|
20
|
-
"> footer": "display:flex align-items:center gap:$2 padding: $2 $3; " +
|
|
20
|
+
"> footer": "display:flex align-items:center justify-content:flex-end gap:$2 padding: $2 $3; " +
|
|
21
21
|
"border-top: 1px solid $s-border; flex-shrink:0",
|
|
22
22
|
"> div": "p:$3 gap:$3 display:flex flex-direction:column overflow-y:auto flex:1 min-height:0",
|
|
23
23
|
"&.hidden": "opacity:0 pointer-events:none transform: translate(-50%, calc(-50% + 20px)); pointer-events:none",
|
|
@@ -30,7 +30,7 @@ const topDialogId = A.derive(() => {
|
|
|
30
30
|
if (keys.length)
|
|
31
31
|
return keys[keys.length - 1];
|
|
32
32
|
});
|
|
33
|
-
|
|
33
|
+
mountPortal(() => {
|
|
34
34
|
A.onEach(dialogs, ({ resolve, opts }, dialogId) => {
|
|
35
35
|
const close = () => { delete dialogs[dialogId]; };
|
|
36
36
|
A.clean(() => {
|
|
@@ -78,8 +78,8 @@ A.mount(document.body, () => {
|
|
|
78
78
|
* header: "Confirm",
|
|
79
79
|
* content: (close) => {
|
|
80
80
|
* A("p #Are you sure?");
|
|
81
|
-
* S.button({
|
|
82
|
-
* S.button({
|
|
81
|
+
* S.button({ content: "Yes", click: () => { doIt(); close(); } });
|
|
82
|
+
* S.button({ content: "Cancel", attrs: ".neutral .outlined", click: close });
|
|
83
83
|
* },
|
|
84
84
|
* });
|
|
85
85
|
* ```
|
|
@@ -126,7 +126,7 @@ export function alert(message, opts = {}) {
|
|
|
126
126
|
content: (close) => {
|
|
127
127
|
A("p", () => { A("#", message); });
|
|
128
128
|
buttonGroup({ layout: "spaced", attrs: "align-self:flex-end", content: () => {
|
|
129
|
-
button({
|
|
129
|
+
button({ content: "OK", click: close });
|
|
130
130
|
} });
|
|
131
131
|
},
|
|
132
132
|
...opts,
|
|
@@ -150,8 +150,8 @@ export function confirm(message, opts = {}) {
|
|
|
150
150
|
content: (close) => {
|
|
151
151
|
A("p", () => { A("#", message); });
|
|
152
152
|
buttonGroup({ layout: "spaced", attrs: "align-self:flex-end", content: () => {
|
|
153
|
-
button({
|
|
154
|
-
button({
|
|
153
|
+
button({ content: "Cancel", attrs: ".neutral .outlined", click: close });
|
|
154
|
+
button({ content: "OK", click: () => { confirmed = true; close(); } });
|
|
155
155
|
} });
|
|
156
156
|
},
|
|
157
157
|
...opts,
|
|
@@ -189,8 +189,8 @@ export function prompt(message, defaultValue = "", opts = {}) {
|
|
|
189
189
|
});
|
|
190
190
|
textline({ bind: A.ref($v, "value") });
|
|
191
191
|
buttonGroup({ layout: "spaced", attrs: "align-self:flex-end", content: () => {
|
|
192
|
-
button({
|
|
193
|
-
button({
|
|
192
|
+
button({ content: "Cancel", attrs: ".neutral .outlined", type: "button", click: close });
|
|
193
|
+
button({ content: "OK", type: "submit" });
|
|
194
194
|
} });
|
|
195
195
|
});
|
|
196
196
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ContentOptions, type Attributes, type Slot } from "../core.js";
|
|
2
2
|
/** Options for {@link form}. */
|
|
3
3
|
export interface FormOptions extends ContentOptions {
|
|
4
4
|
/**
|
|
@@ -16,7 +16,7 @@ export interface FormOptions extends ContentOptions {
|
|
|
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
|
-
actions?:
|
|
19
|
+
actions?: Slot;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* An opinionated `<form>` wrapper that lays its fields out consistently — a clean
|
|
@@ -35,8 +35,8 @@ export interface FormOptions extends ContentOptions {
|
|
|
35
35
|
* S.textline({ label: "Name", required: true, bind: A.ref($u, "name") });
|
|
36
36
|
* S.textline({ label: "Email", type: "email", bind: A.ref($u, "email") });
|
|
37
37
|
* },
|
|
38
|
-
* actions: () => S.button({
|
|
38
|
+
* actions: () => S.button({ content: "Save", type: "submit" }),
|
|
39
39
|
* });
|
|
40
40
|
* ```
|
|
41
41
|
*/
|
|
42
|
-
export declare function form(opts?: FormOptions |
|
|
42
|
+
export declare function form(opts?: FormOptions | Slot): void;
|
package/dist/components/form.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
|
+
import { drawSlot } from "../core.js";
|
|
2
3
|
A.insertGlobalCss({
|
|
3
4
|
".s-form": {
|
|
4
5
|
"&": "display:flex flex-direction:column gap:$3",
|
|
5
6
|
"&.grid": "display:grid grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap:$3",
|
|
6
7
|
"&.grid > .s-wide, &.grid > footer": "grid-column: 1 / -1;",
|
|
7
|
-
"> footer": "display:flex align-items:center gap:$2 flex-wrap:wrap margin-top:$1",
|
|
8
|
+
"> footer": "display:flex align-items:center justify-content:flex-end gap:$2 flex-wrap:wrap margin-top:$1",
|
|
8
9
|
},
|
|
9
10
|
});
|
|
10
11
|
/**
|
|
@@ -24,12 +25,12 @@ A.insertGlobalCss({
|
|
|
24
25
|
* S.textline({ label: "Name", required: true, bind: A.ref($u, "name") });
|
|
25
26
|
* S.textline({ label: "Email", type: "email", bind: A.ref($u, "email") });
|
|
26
27
|
* },
|
|
27
|
-
* actions: () => S.button({
|
|
28
|
+
* actions: () => S.button({ content: "Save", type: "submit" }),
|
|
28
29
|
* });
|
|
29
30
|
* ```
|
|
30
31
|
*/
|
|
31
32
|
export function form(opts = {}) {
|
|
32
|
-
const o = typeof opts === "function" ? { content: opts } : opts;
|
|
33
|
+
const o = typeof opts === "string" || typeof opts === "function" ? { content: opts } : opts;
|
|
33
34
|
A(`form.s-form`, o.attrs, () => {
|
|
34
35
|
// Toggle grid class in its own scope so changing layout doesn't recreate
|
|
35
36
|
// the fields (which would lose focus / input state).
|
|
@@ -48,12 +49,11 @@ export function form(opts = {}) {
|
|
|
48
49
|
o.submit(data, event);
|
|
49
50
|
}
|
|
50
51
|
});
|
|
51
|
-
|
|
52
|
-
o.content();
|
|
52
|
+
drawSlot(o.content);
|
|
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.actionsAttrs, () => o.actions
|
|
56
|
+
A("footer", o.actionsAttrs, () => drawSlot(o.actions));
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Slot, type Attributes } from "../core.js";
|
|
2
2
|
import { type MenuOptions } from "./menu.js";
|
|
3
3
|
/** Options for {@link main}. */
|
|
4
4
|
export interface MainOptions {
|
|
@@ -11,9 +11,9 @@ export interface MainOptions {
|
|
|
11
11
|
/** Leading icon/logo in the top bar. */
|
|
12
12
|
icon?: Slot;
|
|
13
13
|
/** Action area on the right of the top bar (buttons, menu, ...). */
|
|
14
|
-
menu?:
|
|
15
|
-
/** The scrollable page content. */
|
|
16
|
-
content?:
|
|
14
|
+
menu?: Slot;
|
|
15
|
+
/** The scrollable page content. A string is rendered as rich text. */
|
|
16
|
+
content?: Slot;
|
|
17
17
|
/** Footer content, pinned below the scroll area. */
|
|
18
18
|
footer?: Slot;
|
|
19
19
|
/**
|
|
@@ -65,7 +65,7 @@ export interface MainOptions {
|
|
|
65
65
|
* ],
|
|
66
66
|
* },
|
|
67
67
|
* navPosition: "left",
|
|
68
|
-
* menu: () => S.button({
|
|
68
|
+
* menu: () => S.button({ content: "New", attrs: ".small" }),
|
|
69
69
|
* content: () => drawPage(),
|
|
70
70
|
* footer: "© 2026",
|
|
71
71
|
* });
|