staffa 0.2.1 → 0.3.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 +153 -127
- package/dist/components/autocomplete.js +3 -2
- package/dist/components/box.js +2 -2
- package/dist/components/button.d.ts +11 -2
- package/dist/components/button.js +36 -8
- package/dist/components/buttonChooser.d.ts +4 -5
- package/dist/components/buttonChooser.js +2 -2
- package/dist/components/buttonGroup.js +0 -3
- package/dist/components/field.js +0 -3
- package/dist/components/main.d.ts +34 -9
- package/dist/components/main.js +187 -49
- package/dist/components/menu.d.ts +118 -0
- package/dist/components/menu.js +218 -0
- package/dist/components/tabs.d.ts +0 -2
- package/dist/components/tabs.js +6 -19
- package/dist/components/toast.d.ts +37 -0
- package/dist/components/toast.js +79 -0
- package/dist/components/tooltip.d.ts +32 -0
- package/dist/components/tooltip.js +130 -0
- package/dist/core.d.ts +1 -1
- package/dist/icons-helpers.d.ts +46 -0
- package/dist/icons-helpers.js +44 -0
- package/dist/icons.d.ts +1960 -0
- package/dist/icons.js +1972 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/staffa.esm.js +1 -1
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +114 -13
- package/package.json +10 -4
- package/src/components/autocomplete.ts +3 -2
- package/src/components/box.ts +2 -2
- package/src/components/button.ts +42 -10
- package/src/components/buttonChooser.ts +6 -7
- package/src/components/buttonGroup.ts +0 -3
- package/src/components/field.ts +0 -3
- package/src/components/main.ts +201 -45
- package/src/components/menu.ts +288 -0
- package/src/components/tabs.ts +6 -20
- package/src/components/toast.ts +115 -0
- package/src/components/tooltip.ts +139 -0
- package/src/core.ts +1 -1
- package/src/icons-helpers.ts +90 -0
- package/src/icons.ts +1977 -0
- package/src/index.ts +11 -0
- package/src/theme.ts +128 -18
- package/dist/components/modal.d.ts +0 -2
- package/dist/components/modal.js +0 -2
- package/dist/skye.esm.js +0 -1
package/README.md
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
# Staffa
|
|
2
2
|
|
|
3
|
-
A small, opinionated component library for the
|
|
4
|
-
[Aberdeen](https://aberdeenjs.org) reactive UI library.
|
|
5
|
-
|
|
6
|
-
Staffa components are **plain functions** that draw DOM through Aberdeen — no JSX,
|
|
7
|
-
no web components, no build step required. You import a single `S` object and
|
|
8
|
-
call its methods:
|
|
3
|
+
A small, opinionated TypeScript component library for the [Aberdeen](https://aberdeenjs.org) reactive UI library.
|
|
9
4
|
|
|
10
5
|
```ts
|
|
11
6
|
import A from "aberdeen";
|
|
12
7
|
import S from "staffa";
|
|
13
8
|
|
|
14
|
-
const $user = A.proxy({ name: "", email: ""
|
|
9
|
+
const $user = A.proxy({ name: "", email: "" });
|
|
15
10
|
|
|
16
11
|
A.mount(document.body, () => {
|
|
17
12
|
S.main({
|
|
@@ -19,11 +14,10 @@ A.mount(document.body, () => {
|
|
|
19
14
|
maxWidth: "40rem",
|
|
20
15
|
content: () => {
|
|
21
16
|
S.form({
|
|
22
|
-
submit: () => console.log(
|
|
17
|
+
submit: () => console.log(A.unproxy($user)),
|
|
23
18
|
content: () => {
|
|
24
19
|
S.textline({ label: "Name", required: true, bind: A.ref($user, "name") });
|
|
25
20
|
S.textline({ label: "Email", type: "email", bind: A.ref($user, "email") });
|
|
26
|
-
S.checkbox({ label: "Email me updates", bind: A.ref($user, "subscribe") });
|
|
27
21
|
},
|
|
28
22
|
actions: () => S.button({ text: "Create account", type: "submit" }),
|
|
29
23
|
});
|
|
@@ -32,9 +26,7 @@ A.mount(document.body, () => {
|
|
|
32
26
|
});
|
|
33
27
|
```
|
|
34
28
|
|
|
35
|
-
Staffa
|
|
36
|
-
|
|
37
|
-
> **Pre-1.0 notice:** Staffa's API is likely to change fairly often before stabilising as 1.0. That shouldn't stop you from using it — the library is small enough that any breaking changes are easy to adapt to yourself.
|
|
29
|
+
Staffa is made to look decent out of the box, but easily customizable at runtime.
|
|
38
30
|
|
|
39
31
|
## Install
|
|
40
32
|
|
|
@@ -42,150 +34,155 @@ Staffa ships a **dark theme** by default and looks reasonable out of the box.
|
|
|
42
34
|
npm install staffa aberdeen
|
|
43
35
|
```
|
|
44
36
|
|
|
45
|
-
Aberdeen is a
|
|
46
|
-
Aberdeen rather than bundling its own (two copies of Aberdeen would mean two
|
|
47
|
-
independent reactivity systems). So install `aberdeen` alongside `staffa`.
|
|
37
|
+
Aberdeen is a peer dependency. Staffa is published as ESM with TypeScript types.
|
|
48
38
|
|
|
49
|
-
|
|
39
|
+
## How it works
|
|
50
40
|
|
|
51
|
-
|
|
41
|
+
### Components are functions
|
|
52
42
|
|
|
53
|
-
Every component takes a single typed options object.
|
|
54
|
-
consistent naming convention:
|
|
43
|
+
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:
|
|
55
44
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
| `contentAttrs` | container components | attr/style string for the element holding the children |
|
|
61
|
-
| `inputAttrs` | form fields | attr/style string for the actual input/control element |
|
|
62
|
-
| `<region>Attrs`| where relevant | sub-region styling, e.g. `headerAttrs`, `footerAttrs` |
|
|
63
|
-
| `label` / `help` / `error` / `disabled` / `required` / `name` | form fields | standard field chrome |
|
|
45
|
+
```ts
|
|
46
|
+
S.button({ text: "Save", disabled: false });
|
|
47
|
+
S.box({ header: "Settings", content: () => { ... } });
|
|
48
|
+
```
|
|
64
49
|
|
|
65
|
-
|
|
66
|
-
`"display:flex gap:$3 .my-class"`. (Write `display:flex`, not bare `flex`.) As
|
|
67
|
-
`attrs` is applied last, it can also override a component's default surface
|
|
68
|
-
classes — pass `".danger"` or `".neutral .outlined"` to recolour a button, etc.
|
|
50
|
+
### Options objects are typed and can be reactive
|
|
69
51
|
|
|
70
|
-
|
|
71
|
-
`text`, a dialog body, ...), you can pass either a **string** — rendered as
|
|
72
|
-
[rich text](#rich-text) — or a `() => void` draw function for custom markup.
|
|
52
|
+
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.
|
|
73
53
|
|
|
74
|
-
|
|
54
|
+
```ts
|
|
55
|
+
const $btn = A.proxy({ text: "Save", disabled: false });
|
|
56
|
+
S.button($btn);
|
|
57
|
+
// ...later:
|
|
58
|
+
$btn.disabled = true; // button updates instantly
|
|
59
|
+
```
|
|
75
60
|
|
|
76
|
-
|
|
77
|
-
`menu`), a scrollable content area, and a `footer`. Set `maxWidth` to center
|
|
78
|
-
the content as a shadowed "sheet".
|
|
79
|
-
- **`S.box(opts | content)`** — a surface with optional `header`/`footer` and a
|
|
80
|
-
padded body. Pass a function as a shorthand for `content`.
|
|
81
|
-
- **`S.tabs(opts)`** — a `tablist` + live panel, with full keyboard navigation.
|
|
82
|
-
- **`S.form(opts | content)`** — opinionated `<form>` that aligns fields in a
|
|
83
|
-
column (or a responsive `grid`) and provides an `actions` bar. Prevents the
|
|
84
|
-
default page reload.
|
|
61
|
+
### Rich text slots
|
|
85
62
|
|
|
86
|
-
|
|
63
|
+
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.
|
|
87
64
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- **`S.select(opts)`** — single-select dropdown backed by a native `<select>`.
|
|
93
|
-
The control is styled; the OS renders the drop-down list.
|
|
94
|
-
- **`S.autocomplete(opts)`** — a type-ahead combobox; supports `multi` (chips),
|
|
95
|
-
`allowCustom` (free text), `required`, and dynamic `options`.
|
|
65
|
+
```ts
|
|
66
|
+
S.button({ text: "Save **now**" });
|
|
67
|
+
S.box({ header: "See the [docs](/docs)", content: () => { ... } });
|
|
68
|
+
```
|
|
96
69
|
|
|
97
|
-
###
|
|
70
|
+
### Surfaces
|
|
98
71
|
|
|
99
|
-
-
|
|
100
|
-
backdrop and fade transition. Lifecycle is tied to the calling reactive scope
|
|
101
|
-
(the dialog disappears when that scope is cleaned up). The `content` slot's
|
|
102
|
-
draw function receives a `close()` function. Nested dialogs stack correctly.
|
|
103
|
-
`S.alert` / `S.confirm` / `S.prompt` are promise-returning shortcuts.
|
|
72
|
+
Staffa builds on **surfaces**: elements marked with `.s-s` that have their own background and derived text/border tokens. Add modifier classes to colour them:
|
|
104
73
|
|
|
105
|
-
|
|
74
|
+
- **level**: `.base`, `.panel`, `.raised`
|
|
75
|
+
- **role**: `.primary`, `.secondary`, `.gradient`, `.neutral`, `.danger`, `.success`, `.warning`
|
|
76
|
+
- **variant**: `.filled`, `.tonal`, `.outlined`
|
|
106
77
|
|
|
107
|
-
|
|
108
|
-
(e.g. `".danger"`, `".neutral .outlined"`), plus `size`, `disabled`, `icon`,
|
|
109
|
-
and `href` (renders an `<a role=button>`). Defaults to a filled `.primary`.
|
|
110
|
-
- **`S.buttonGroup(opts)`** — groups buttons, `attached` (segmented) or `spaced`.
|
|
111
|
-
- **`S.buttonChooser(opts)`** — single-select segmented control bound to a value.
|
|
78
|
+
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:
|
|
112
79
|
|
|
113
|
-
|
|
114
|
-
|
|
80
|
+
```ts
|
|
81
|
+
S.button({ text: "Delete", attrs: ".danger" });
|
|
82
|
+
S.box({ attrs: ".raised.outlined", content: () => { ... } });
|
|
83
|
+
```
|
|
115
84
|
|
|
116
|
-
|
|
85
|
+
Inside any surface, CSS variables are defined for suitable foreground colors (`$s-fg`, `$s-bg`, `$s-fg-muted`, `$s-border`, `$s-accent`, ...), with `color` defaulting to `$s-fg`. By using these, components has access to various foreground colors that will look regardless of the surface it is drawing on.
|
|
117
86
|
|
|
118
|
-
|
|
119
|
-
|
|
87
|
+
### Dark and light modes
|
|
88
|
+
|
|
89
|
+
Dark/light mode is detected from OS preference by default. If you want to override this (based on user preferences), use:
|
|
120
90
|
|
|
121
91
|
```ts
|
|
122
|
-
|
|
123
|
-
S.
|
|
124
|
-
//
|
|
125
|
-
$opts.disabled = true; // the button updates, nothing else re-renders
|
|
92
|
+
S.setDarkMode(true); // force dark
|
|
93
|
+
S.setDarkMode(false); // force light
|
|
94
|
+
S.setDarkMode(undefined); // follow OS
|
|
126
95
|
```
|
|
127
96
|
|
|
128
|
-
|
|
97
|
+
*Hint:* A `buttonChooser` is probably the right component for a color scheme selector.
|
|
129
98
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
99
|
+
### CSS reset
|
|
100
|
+
|
|
101
|
+
Staffa includes a lightweight CSS reset that makes bare semantic HTML look a bit better but unsurprising without additional styling.
|
|
102
|
+
|
|
103
|
+
### Theming
|
|
104
|
+
|
|
105
|
+
The first step in theming is just setting some CSS variables, most commonly the primary and secondary color. This can be done through CSS directly, or using Aberdeen:
|
|
135
106
|
|
|
136
107
|
```ts
|
|
137
|
-
|
|
138
|
-
|
|
108
|
+
A.cssVars["s-primary"] = "#fdda58";
|
|
109
|
+
A.cssVars["s-secondary"] = "#cc5624";
|
|
110
|
+
A.cssVars["s-danger"] = "#ee4422";
|
|
111
|
+
A.cssVars["s-radius"] = "4px";
|
|
139
112
|
```
|
|
140
113
|
|
|
141
|
-
|
|
114
|
+
See `src/theme.ts` for what other CSS variables are being used.
|
|
115
|
+
|
|
116
|
+
If you need further customization, just add some CSS to override the default styling. For instance, to add your own surface type:
|
|
142
117
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
118
|
+
```ts
|
|
119
|
+
// In filled mode, 's-a' is the foreground and 's-b' is the background. "outlined" and "tonal" use the colors in different ways.
|
|
120
|
+
A.insertGlobalCss({".s-s.my-surface": "--s-a:white --s-b:#ef6b00"});
|
|
146
121
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
-
|
|
122
|
+
S.button({
|
|
123
|
+
text: "You'll want to click me",
|
|
124
|
+
attrs: ".my-surface",
|
|
125
|
+
click: () => S.alert("Good work!", {attrs: ".my-surface"})
|
|
126
|
+
});
|
|
127
|
+
```
|
|
150
128
|
|
|
151
|
-
|
|
152
|
-
`.s-s.panel`, ...), and because `attrs` is applied last you can override the look
|
|
153
|
-
from the outside — `S.button({ attrs: ".danger .outlined" })`.
|
|
129
|
+
Note that when changing CSS like this, things *may* break if you upgrade Staffa. The recommended update strategy is therefore: don't!
|
|
154
130
|
|
|
155
|
-
|
|
156
|
-
`$s-border`, `$s-accent`, `$s-link`, ...) so they adapt automatically to wherever
|
|
157
|
-
they're nested. Colours all come from a small palette set on `:root` per mode —
|
|
158
|
-
re-skin by overriding those custom properties:
|
|
131
|
+
If you want to make changes that are dependent upon the current light/dark mode setting, rely on Aberdeen reactivity:
|
|
159
132
|
|
|
160
133
|
```ts
|
|
161
|
-
A(() =>
|
|
162
|
-
|
|
134
|
+
A(() => {
|
|
135
|
+
if (S.getDarkMode()) {
|
|
136
|
+
A.cssVars["s-primary"] = "#aa9944";
|
|
137
|
+
A.insertGlobalCss({".s-s.my-surface": "--s-a:white --s-b:#444444"});
|
|
138
|
+
} else {
|
|
139
|
+
A.cssVars["s-primary"] = "#fdda58";
|
|
140
|
+
A.insertGlobalCss({".s-s.my-surface": "--s-a:black --s-b:#cccccc"});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
163
143
|
```
|
|
164
144
|
|
|
165
|
-
|
|
145
|
+
## Components
|
|
146
|
+
|
|
147
|
+
Components share naming conventions for options: `attrs` (outermost element), `contentAttrs` (children-holding element), `inputAttrs` (form control element), and `<region>Attrs` (sub-regions like `headerAttrs`/`footerAttrs`). Form components consistently support `label`, `help`, `error`, `disabled`, `required`, `name` through the `drawField()` helper.
|
|
166
148
|
|
|
167
|
-
###
|
|
149
|
+
### Layout & containers
|
|
168
150
|
|
|
169
|
-
|
|
151
|
+
- **`S.main(opts)`**: app shell, a sticky header with `icon`, `title`, `subtitle`, `menu`; scrollable content area; footer. Set `maxWidth` to center the content.
|
|
152
|
+
- **`S.box(opts | content)`**: surface with optional `header`/`footer` and padded body. Pass a function for shorthand `{ content }`.
|
|
153
|
+
- **`S.tabs(opts)`**: tablist with live panels and keyboard navigation.
|
|
154
|
+
- **`S.form(opts | content)`**: form aligning fields in a column or responsive grid, with an `actions` bar. Prevents the default page reload.
|
|
170
155
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
S.
|
|
174
|
-
S.
|
|
175
|
-
|
|
156
|
+
### Form fields
|
|
157
|
+
|
|
158
|
+
- **`S.textline(opts)`**: single-line input (`text`, `password`, `email`, `number`, `tel`, `url`, `search`, dates, ...).
|
|
159
|
+
- **`S.textarea(opts)`**: multi-line input.
|
|
160
|
+
- **`S.checkbox(opts)`**: labelled checkbox.
|
|
161
|
+
- **`S.select(opts)`**: single-select dropdown backed by native `<select>` (styled control, OS dropdown).
|
|
162
|
+
- **`S.autocomplete(opts)`**: type-ahead combobox with `multi` (chips), `allowCustom` (free text), `required`, and dynamic `options`.
|
|
163
|
+
|
|
164
|
+
### Dialogs
|
|
176
165
|
|
|
177
|
-
The
|
|
178
|
-
(
|
|
179
|
-
`undefined` when in "auto" mode (useful for a dark/light/auto control).
|
|
166
|
+
- **`S.dialog(opts)`**: modal dialog with backdrop and fade transition. The `content` slot receives a `close()` function. Lifecycle is tied to the calling scope (disappears when cleaned up). Nesting stacks correctly.
|
|
167
|
+
- **`S.alert(msg)` / `S.confirm(msg)` / `S.prompt(msg, initial?)`**: promise-returning shortcuts.
|
|
180
168
|
|
|
181
|
-
|
|
182
|
-
|
|
169
|
+
### Actions
|
|
170
|
+
|
|
171
|
+
- **`S.button(opts | text)`**: button surface; restyle via `attrs` (e.g. `.danger`, `.outlined`), plus `size`, `disabled`, `icon`, `href` (renders `<a role=button>`). Defaults to filled `.primary`.
|
|
172
|
+
- **`S.buttonGroup(opts)`**: groups buttons, `attached` (segmented) or `spaced`.
|
|
173
|
+
- **`S.buttonChooser(opts)`**: single-select segmented control bound to a value.
|
|
174
|
+
|
|
175
|
+
### Other
|
|
176
|
+
|
|
177
|
+
- **`S.menuButton(opts)` / `S.showFloatingMenu(opts)`**: menu actions and floating menus, with keyboard navigation and submenus.
|
|
178
|
+
- **`S.toast(opts)`**: transient notification at the bottom of the viewport.
|
|
179
|
+
- **`S.addTooltip(el, opts)`**: tooltip on hover, attached to an existing element.
|
|
180
|
+
|
|
181
|
+
Two-way binding uses Aberdeen proxies: pass `bind: A.ref($obj, "key")` to form fields.
|
|
183
182
|
|
|
184
183
|
## Browser (no bundler)
|
|
185
184
|
|
|
186
|
-
`staffa/all.js` is a pre-built ESM bundle
|
|
187
|
-
Aberdeen external. Use an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap)
|
|
188
|
-
to tell the browser where to find both:
|
|
185
|
+
`staffa/all.js` is a pre-built ESM bundle. Use an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap):
|
|
189
186
|
|
|
190
187
|
```html
|
|
191
188
|
<script type="importmap">
|
|
@@ -203,22 +200,51 @@ to tell the browser where to find both:
|
|
|
203
200
|
</script>
|
|
204
201
|
```
|
|
205
202
|
|
|
206
|
-
|
|
207
|
-
independent copies.
|
|
203
|
+
It includes all components, but not the icons.
|
|
208
204
|
|
|
209
|
-
##
|
|
205
|
+
## Extending Staffa
|
|
210
206
|
|
|
211
|
-
|
|
212
|
-
npm run build # compile TypeScript to dist/
|
|
213
|
-
npx serve . # then open /demo/ in a browser
|
|
214
|
-
```
|
|
207
|
+
Staffa is designed for extension. A component is simply a plain function taking a typed options object and drawing Aberdeen DOM. This section explains the philosophy so extensions follow the same patterns.
|
|
215
208
|
|
|
216
|
-
|
|
209
|
+
### Design principles
|
|
217
210
|
|
|
218
|
-
|
|
219
|
-
philosophy and the add-a-component checklist.
|
|
211
|
+
1. **Components are functions**. They take one typed options object, emit Aberdeen DOM, and *usually* return nothing.
|
|
220
212
|
|
|
221
|
-
|
|
213
|
+
2. **Reuse option types.** Define options by extending `ContentOptions` (for layout components) or `FieldOptions` (for form controls) from `src/core.ts` and `src/components/field.ts`. Don't reinvent fields like `attrs`, `label`, `help`, etc.
|
|
222
214
|
|
|
223
|
-
|
|
224
|
-
|
|
215
|
+
3. **Reach for reactivity deliberately.** Pass option strings straight to `A` as positional args (the caller's scope). Only wrap a dedicated `A(() => ...)` scope where it matters: input elements (recreation loses focus), or large subtrees you don't want to redraw. Use `A.peek(() => ...)` when you need a value but must not subscribe.
|
|
216
|
+
|
|
217
|
+
4. **Build on surfaces.** Mark elements `.s-s` and add level/role/variant modifiers. Inside them, use the contextual foreground color CSS variables (`$s-fg`, `$s-bg`, `$s-border`, ...) so components adapt to wherever they're nested. Hard-coding colors in components shouldn't be needed, but if you must, make sure you set *both* foreground and background.
|
|
218
|
+
|
|
219
|
+
5. **No outer margins.** Components don't margin themselves; spacing is the parent's job. Content components set default `padding` on the content element; `contentAttrs` overrides it.
|
|
220
|
+
|
|
221
|
+
6. **Make everything styleable.** Provide `attrs`, `contentAttrs`, `inputAttrs`, and `<region>Attrs` hooks so callers can customize. Apply `attrs` last so it can override component classes.
|
|
222
|
+
|
|
223
|
+
7. **Use semantic HTML and ARIA.** Prefer native elements (`<button>`, `<label>`, `<form>`, `<section>`) and native behaviour. Add ARIA only where semantics fall short (e.g. tabs, combobox).
|
|
224
|
+
|
|
225
|
+
8. **Use CSS.** Use `A.insertGlobalCss({...})` at module top level to provide (nested) CSS styling for your component. Give your top-level element the `s-<component-name>` class. Avoid inventing further classes; lean on nesting (`&` for the element, bare key for descendants) and element/structural selectors.
|
|
226
|
+
|
|
227
|
+
9. **Reuse form controls.** Use `drawField()` and call `applyControlAttrs()`.
|
|
228
|
+
|
|
229
|
+
10. **Function over form.** Provide enough contrast. Stick to UI conventions to help users; buttons have a rounded border, links are underlined, text input background is white, etc.
|
|
230
|
+
|
|
231
|
+
### Adding a component to Staffa
|
|
232
|
+
|
|
233
|
+
The previous section is good advice for any project-specific custom, but should definitely be followed for any new components to be included in Staffa. In addition, you'd want to:
|
|
234
|
+
|
|
235
|
+
1. Create `src/components/<name>.ts`.
|
|
236
|
+
2. Define `<Name>Options` extending `ContentOptions`, `FieldOptions`, or a plain interface. Add TSDoc on every option.
|
|
237
|
+
3. Add a TSDoc `@example` on the function.
|
|
238
|
+
4. Register in `src/index.ts` (the `S` object + type re-export).
|
|
239
|
+
5. Extend `smoke.mjs` to render it. Run `npm run smoke` and `npm run build`.
|
|
240
|
+
|
|
241
|
+
See `src/components/button.ts` and `src/components/dialog.ts` for examples.
|
|
242
|
+
|
|
243
|
+
## Commands
|
|
244
|
+
|
|
245
|
+
```sh
|
|
246
|
+
npm run build # compile TypeScript to dist/
|
|
247
|
+
npm run typecheck # check types
|
|
248
|
+
npm run smoke # render every component in jsdom
|
|
249
|
+
npx http-server # allows demo to be viewed at http://localhost:8080/demo
|
|
250
|
+
```
|
|
@@ -13,8 +13,9 @@ A.insertGlobalCss({
|
|
|
13
13
|
".s-chip > button:hover": "fg:$s-fg background:$s-border",
|
|
14
14
|
"input": "flex:1 min-width:6ch border:0 background:transparent color:inherit outline:none padding:0.25em",
|
|
15
15
|
"> .s-menu": "position:absolute top:100% left:0 right:0 z-index:20 margin-top:4px max-height:15rem overflow-y:auto list-style:none p:$1 margin-bottom:0 bg:$s-panel border: 1px solid $s-border; r:$s-radius box-shadow:$s-shadow",
|
|
16
|
-
".s-
|
|
17
|
-
".s-option
|
|
16
|
+
"> .s-menu li": "margin:0",
|
|
17
|
+
".s-option": "padding: 0.45em 0.6em; r:6px cursor:pointer transition: background 0.1s;",
|
|
18
|
+
".s-option[aria-selected=true]": "background: color-mix(in srgb, $s-fg 10%, transparent);",
|
|
18
19
|
".s-add": "fg:$s-accent font-style:italic",
|
|
19
20
|
".s-empty": "padding: 0.45em 0.6em; fg:$s-fg-muted",
|
|
20
21
|
},
|
package/dist/components/box.js
CHANGED
|
@@ -6,10 +6,10 @@ import { drawSlot } from "../core.js";
|
|
|
6
6
|
// it's nested in.
|
|
7
7
|
A.insertGlobalCss({
|
|
8
8
|
".s-box": {
|
|
9
|
-
"&": "display:flex flex-direction:column border: 1px solid $s-border; r: $s-radius; overflow:hidden",
|
|
9
|
+
"&": "display:flex flex-direction:column border: 1px solid $s-border; r: $s-radius-lg; overflow:hidden box-shadow: $s-shadow;",
|
|
10
|
+
"&:not(:first-child)": "margin-top: $3",
|
|
10
11
|
"> header": "display:flex align-items:center gap:$2 padding: $2 $3; border-bottom: 1px solid $s-border; font-weight:600",
|
|
11
12
|
"> footer": "display:flex align-items:center gap:$2 padding: $2 $3; border-top: 1px solid $s-border;",
|
|
12
|
-
// The body is the only plain <div> child; give it the default padding+gap.
|
|
13
13
|
"> div": "p:$3 gap:$3",
|
|
14
14
|
},
|
|
15
15
|
});
|
|
@@ -9,8 +9,6 @@ export interface ButtonOptions {
|
|
|
9
9
|
icon?: Slot;
|
|
10
10
|
/** Click handler. */
|
|
11
11
|
click?: (event: Event) => void;
|
|
12
|
-
/** Size. Defaults to `"md"`. */
|
|
13
|
-
size?: "sm" | "md" | "lg";
|
|
14
12
|
/** Disables the button. */
|
|
15
13
|
disabled?: boolean;
|
|
16
14
|
/** Native button behaviour. Defaults to `"button"`. */
|
|
@@ -23,6 +21,10 @@ export interface ButtonOptions {
|
|
|
23
21
|
* Aberdeen attr/style string applied to the button. A button is a surface, so
|
|
24
22
|
* pass surface modifier classes here to restyle it, e.g. `".danger"`,
|
|
25
23
|
* `".neutral .outlined"`. Defaults to a filled `.primary` surface.
|
|
24
|
+
*
|
|
25
|
+
* Size is set here too, with `.small` or `.large` (medium is the default and
|
|
26
|
+
* needs no class), e.g. `".danger .small"`. A `.small`/`.large` parent (such
|
|
27
|
+
* as a {@link buttonGroup}) also sizes its buttons, so you can set it once.
|
|
26
28
|
*/
|
|
27
29
|
attrs?: Attributes;
|
|
28
30
|
}
|
|
@@ -33,6 +35,13 @@ export interface ButtonOptions {
|
|
|
33
35
|
* Shortcut: pass a string to use it as the label, or a function for custom
|
|
34
36
|
* content.
|
|
35
37
|
*
|
|
38
|
+
* **Tip:** pair `href` with Aberdeen's `interceptLinks()` (called once at app
|
|
39
|
+
* startup) for SPA-style navigation without manual click handlers:
|
|
40
|
+
* ```ts
|
|
41
|
+
* interceptLinks(); // once at root
|
|
42
|
+
* S.button({ href: "/dashboard", text: "Dashboard" }); // navigates via router
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
36
45
|
* @example
|
|
37
46
|
* ```ts
|
|
38
47
|
* S.button({ text: "Save", click: save });
|
|
@@ -8,15 +8,34 @@ A.insertGlobalCss({
|
|
|
8
8
|
"&": "display:inline-flex align-items:center justify-content:center gap:$2 " +
|
|
9
9
|
"font-weight:600 line-height:1.2 white-space:nowrap cursor:pointer text-decoration:none " +
|
|
10
10
|
"border: 1px solid $s-border; r: $s-radius; padding: 0.5em 1em; " +
|
|
11
|
-
"transition: background 0.15s, border-color 0.15s, filter 0.15s, box-shadow 0.15s;",
|
|
11
|
+
"transition: background 0.15s, border-color 0.15s, color 0.15s, filter 0.15s, box-shadow 0.15s, transform 0.08s;",
|
|
12
12
|
"&:focus-visible": "outline:none box-shadow: 0 0 0 3px $s-focus;",
|
|
13
13
|
"&:disabled, &[aria-disabled=true]": "opacity:0.45 cursor:not-allowed pointer-events:none filter:saturate(0.6)",
|
|
14
|
-
|
|
14
|
+
// Every button lifts a little toward the cursor on hover (the transform is in
|
|
15
|
+
// the transition list above). The filled `.gradient` CTA below layers a deeper
|
|
16
|
+
// shadow on top of the same lift, so it still reads as the signature action.
|
|
17
|
+
"&:hover": "filter: brightness(1.08); transform: translateY(-1px)",
|
|
15
18
|
"&.tonal:hover, &.outlined:hover": "background: color-mix(in srgb, $s-b 26%, transparent);",
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
// A filled `.gradient` button (the default) is the app's signature call to
|
|
20
|
+
// action: a borderless gradient with a soft glow that lifts on hover. The
|
|
21
|
+
// gradient fill itself comes from the `.s-s.gradient` surface rule in theme.ts.
|
|
22
|
+
// No border: a filled gradient reads as one solid shape. Dropping the border
|
|
23
|
+
// (rather than making it transparent) also sidesteps a Chromium artifact where
|
|
24
|
+
// a gradient clipped to a transparent rounded border fringes the edge with the
|
|
25
|
+
// gradient's far colour.
|
|
26
|
+
"&.gradient:not(.tonal):not(.outlined)": "border:0 box-shadow: $s-glow;",
|
|
27
|
+
"&.gradient:not(.tonal):not(.outlined):hover": "filter: brightness(1.06); box-shadow: 0 10px 28px color-mix(in srgb, $s-primary 42%, transparent); transform: translateY(-1px);",
|
|
28
|
+
// Subtle press feedback.
|
|
29
|
+
"&:active:not(:disabled):not([aria-disabled=true])": "transform: translateY(1px)",
|
|
30
|
+
// Size: set on the button itself, or inherited from a `.small`/`.large`
|
|
31
|
+
// parent (e.g. a buttonGroup), so a container can size all its buttons at once.
|
|
32
|
+
"&.small, .small > &": "padding: 0.32em 0.7em; font-size:0.85em",
|
|
33
|
+
"&.large, .large > &": "padding: 0.66em 1.3em; font-size:1.1em",
|
|
18
34
|
},
|
|
19
35
|
});
|
|
36
|
+
// Surface-role classes a caller may pass in `attrs`. When one is present we skip
|
|
37
|
+
// the default `.gradient` base so the two roles don't stack on one element.
|
|
38
|
+
const ROLE_CLASS = /\.(gradient|primary|secondary|neutral|danger|success|warning|base|panel|raised)(\.|\s|$)/;
|
|
20
39
|
/**
|
|
21
40
|
* A button. Always carries at least a visible border so its affordance is
|
|
22
41
|
* obvious at a glance.
|
|
@@ -24,6 +43,13 @@ A.insertGlobalCss({
|
|
|
24
43
|
* Shortcut: pass a string to use it as the label, or a function for custom
|
|
25
44
|
* content.
|
|
26
45
|
*
|
|
46
|
+
* **Tip:** pair `href` with Aberdeen's `interceptLinks()` (called once at app
|
|
47
|
+
* startup) for SPA-style navigation without manual click handlers:
|
|
48
|
+
* ```ts
|
|
49
|
+
* interceptLinks(); // once at root
|
|
50
|
+
* S.button({ href: "/dashboard", text: "Dashboard" }); // navigates via router
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
27
53
|
* @example
|
|
28
54
|
* ```ts
|
|
29
55
|
* S.button({ text: "Save", click: save });
|
|
@@ -35,10 +61,12 @@ A.insertGlobalCss({
|
|
|
35
61
|
export function button(opts = {}) {
|
|
36
62
|
const o = typeof opts === "string" ? { text: opts } : typeof opts === "function" ? { content: opts } : opts;
|
|
37
63
|
const tag = o.href != null ? "a" : "button";
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
A(
|
|
64
|
+
// A filled `.gradient` surface by default — the signature CTA. If the caller's
|
|
65
|
+
// `attrs` already names a surface role we omit the default, so `.danger`,
|
|
66
|
+
// `.neutral .outlined`, etc. fully take over (rather than stacking two roles).
|
|
67
|
+
// A bare variant/size (`.outlined`, `.small`) keeps the gradient base.
|
|
68
|
+
const role = o.attrs && ROLE_CLASS.test(o.attrs) ? "" : ".gradient";
|
|
69
|
+
A(`${tag}.s-btn.s-s${role}`, o.attrs, () => {
|
|
42
70
|
if (o.href != null) {
|
|
43
71
|
A(`href=${o.href} role=button`);
|
|
44
72
|
if (o.disabled)
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { type Bindable, type Attributes } from "../core.js";
|
|
1
|
+
import { type Bindable, type Attributes, type Slot } from "../core.js";
|
|
2
2
|
/** Options for {@link buttonChooser}. */
|
|
3
3
|
export interface ButtonChooserOptions {
|
|
4
4
|
/** Aberdeen attr/style string applied to the button group. */
|
|
5
5
|
attrs?: Attributes;
|
|
6
6
|
/**
|
|
7
7
|
* The options to display, as a plain object mapping id → display label.
|
|
8
|
-
* Buttons appear in insertion order.
|
|
8
|
+
* Buttons appear in insertion order. A label may be a plain (rich-text)
|
|
9
|
+
* string, or a draw-function for custom content such as an icon.
|
|
9
10
|
*/
|
|
10
|
-
options: Record<string,
|
|
11
|
+
options: Record<string, Slot>;
|
|
11
12
|
/**
|
|
12
13
|
* Two-way binding for the selected id, or `null` when nothing is selected.
|
|
13
14
|
* Use an `A.proxy` or `A.ref`.
|
|
@@ -18,8 +19,6 @@ export interface ButtonChooserOptions {
|
|
|
18
19
|
* `bind.value` to `null`. Useful for "none / auto" states.
|
|
19
20
|
*/
|
|
20
21
|
allowDeselect?: boolean;
|
|
21
|
-
/** Button size. Defaults to `"md"`. */
|
|
22
|
-
size?: "sm" | "md" | "lg";
|
|
23
22
|
/** Name attribute for the hidden `<input>`, enabling form submission. */
|
|
24
23
|
name?: string;
|
|
25
24
|
}
|
|
@@ -22,8 +22,8 @@ export function buttonChooser(opts) {
|
|
|
22
22
|
buttonGroup({
|
|
23
23
|
attrs: opts.attrs,
|
|
24
24
|
buttons: Object.entries(opts.options).map(([id, label]) => ({
|
|
25
|
-
text: label,
|
|
26
|
-
|
|
25
|
+
text: typeof label === "string" ? label : undefined,
|
|
26
|
+
content: typeof label === "function" ? label : undefined,
|
|
27
27
|
attrs: selected === id ? ".primary" : ".neutral .outlined",
|
|
28
28
|
click: () => {
|
|
29
29
|
opts.bind.value = (opts.allowDeselect && selected === id) ? null : id;
|
|
@@ -6,8 +6,6 @@ A.insertGlobalCss({
|
|
|
6
6
|
"&.s-spaced": "gap:$2 flex-wrap:wrap",
|
|
7
7
|
"&.s-vertical": "flex-direction:column",
|
|
8
8
|
"&.s-attached": "gap:0",
|
|
9
|
-
// When attached, collapse the shared border and square off the touching
|
|
10
|
-
// corners, keeping only the outer ends of the group rounded.
|
|
11
9
|
"&.s-attached:not(.s-vertical) > .s-btn:not(:first-child)": "margin-left:-1px",
|
|
12
10
|
"&.s-attached:not(.s-vertical) > .s-btn:not(:first-child):not(:last-child)": "r:0",
|
|
13
11
|
"&.s-attached:not(.s-vertical) > .s-btn:first-child:not(:last-child)": "border-top-right-radius:0 border-bottom-right-radius:0",
|
|
@@ -16,7 +14,6 @@ A.insertGlobalCss({
|
|
|
16
14
|
"&.s-attached.s-vertical > .s-btn:not(:first-child):not(:last-child)": "r:0",
|
|
17
15
|
"&.s-attached.s-vertical > .s-btn:first-child:not(:last-child)": "border-bottom-left-radius:0 border-bottom-right-radius:0",
|
|
18
16
|
"&.s-attached.s-vertical > .s-btn:last-child:not(:first-child)": "border-top-left-radius:0 border-top-right-radius:0",
|
|
19
|
-
// Keep the hovered/focused button's border above its neighbours.
|
|
20
17
|
"&.s-attached > .s-btn:hover, &.s-attached > .s-btn:focus-visible": "z-index:1",
|
|
21
18
|
},
|
|
22
19
|
});
|
package/dist/components/field.js
CHANGED
|
@@ -5,12 +5,9 @@ A.insertGlobalCss({
|
|
|
5
5
|
"&": "display:flex flex-direction:column gap:$1",
|
|
6
6
|
"> label": "font-weight:600 font-size:0.9em fg:$s-fg user-select:none",
|
|
7
7
|
},
|
|
8
|
-
// Shared, reusable bits (also used by checkbox & autocomplete).
|
|
9
8
|
".s-req": "fg:$s-danger margin-left:2px",
|
|
10
9
|
".s-help": "font-size:0.82em fg:$s-fg-muted",
|
|
11
10
|
".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
11
|
".s-input": {
|
|
15
12
|
"&": "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
13
|
"&:hover:not(:disabled)": "border-color:$s-border-strong",
|