staffa 0.1.0 → 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 +161 -97
- package/dist/components/autocomplete.js +21 -20
- package/dist/components/box.d.ts +8 -6
- package/dist/components/box.js +16 -12
- package/dist/components/button.d.ts +21 -35
- package/dist/components/button.js +46 -39
- package/dist/components/buttonChooser.d.ts +41 -0
- package/dist/components/buttonChooser.js +38 -0
- package/dist/components/buttonGroup.d.ts +3 -3
- package/dist/components/buttonGroup.js +18 -21
- 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 +16 -18
- package/dist/components/form.d.ts +3 -3
- package/dist/components/form.js +4 -4
- package/dist/components/main.d.ts +41 -14
- package/dist/components/main.js +193 -53
- package/dist/components/menu.d.ts +118 -0
- package/dist/components/menu.js +218 -0
- package/dist/components/select.d.ts +1 -1
- package/dist/components/select.js +5 -5
- package/dist/components/tabs.d.ts +5 -5
- package/dist/components/tabs.js +15 -22
- package/dist/components/textarea.js +4 -4
- package/dist/components/textline.js +1 -1
- 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 +26 -39
- package/dist/core.js +6 -5
- 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 +21 -8
- package/dist/index.js +17 -8
- package/dist/staffa.esm.js +1 -0
- package/dist/theme.d.ts +9 -75
- package/dist/theme.js +279 -82
- package/package.json +12 -5
- package/src/components/autocomplete.ts +21 -20
- package/src/components/box.ts +21 -15
- package/src/components/button.ts +59 -75
- package/src/components/buttonChooser.ts +65 -0
- package/src/components/buttonGroup.ts +18 -21
- package/src/components/checkbox.ts +7 -7
- package/src/components/dialog.ts +101 -102
- package/src/components/field.ts +22 -22
- package/src/components/form.ts +7 -7
- package/src/components/main.ts +212 -52
- package/src/components/menu.ts +288 -0
- package/src/components/select.ts +4 -4
- package/src/components/tabs.ts +20 -27
- package/src/components/textarea.ts +4 -4
- package/src/components/textline.ts +1 -1
- package/src/components/toast.ts +115 -0
- package/src/components/tooltip.ts +139 -0
- package/src/core.ts +26 -40
- package/src/icons-helpers.ts +90 -0
- package/src/icons.ts +1977 -0
- package/src/index.ts +21 -8
- package/src/theme.ts +300 -135
- 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,117 +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
|
-
across all components:
|
|
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
|
-
| `inner` | container components | attr/style string for the element holding the children |
|
|
61
|
-
| `control` | form fields | attr/style string for the actual input element |
|
|
62
|
-
| `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
|
+
```
|
|
63
49
|
|
|
64
|
-
|
|
65
|
-
e.g. `"display:flex gap:$3 .my-class"`. (Write `display:flex`, not bare `flex`.)
|
|
50
|
+
### Options objects are typed and can be reactive
|
|
66
51
|
|
|
67
|
-
|
|
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.
|
|
68
53
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
- **`S.form(opts | content)`** — opinionated `<form>` that aligns fields in a
|
|
76
|
-
column (or a responsive `grid`) and provides an `actions` bar. Prevents the
|
|
77
|
-
default page reload.
|
|
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
|
+
```
|
|
78
60
|
|
|
79
|
-
###
|
|
61
|
+
### Rich text slots
|
|
80
62
|
|
|
81
|
-
|
|
82
|
-
`number`, `tel`, `url`, `search`, dates, ...).
|
|
83
|
-
- **`S.textarea(opts)`** — multi-line input.
|
|
84
|
-
- **`S.checkbox(opts)`** — labelled checkbox.
|
|
85
|
-
- **`S.select(opts)`** — single-select dropdown backed by a native `<select>`.
|
|
86
|
-
The control is styled; the OS renders the drop-down list.
|
|
87
|
-
- **`S.autocomplete(opts)`** — a type-ahead combobox; supports `multi` (chips),
|
|
88
|
-
`allowCustom` (free text), `required`, and dynamic `options`.
|
|
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.
|
|
89
64
|
|
|
90
|
-
|
|
65
|
+
```ts
|
|
66
|
+
S.button({ text: "Save **now**" });
|
|
67
|
+
S.box({ header: "See the [docs](/docs)", content: () => { ... } });
|
|
68
|
+
```
|
|
91
69
|
|
|
92
|
-
|
|
93
|
-
backdrop and fade transition. Lifecycle is tied to the calling reactive scope
|
|
94
|
-
(the modal disappears when that scope is cleaned up). The `content` callback
|
|
95
|
-
receives a `close()` function. Nested modals stack correctly.
|
|
70
|
+
### Surfaces
|
|
96
71
|
|
|
97
|
-
|
|
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:
|
|
73
|
+
|
|
74
|
+
- **level**: `.base`, `.panel`, `.raised`
|
|
75
|
+
- **role**: `.primary`, `.secondary`, `.gradient`, `.neutral`, `.danger`, `.success`, `.warning`
|
|
76
|
+
- **variant**: `.filled`, `.tonal`, `.outlined`
|
|
98
77
|
|
|
99
|
-
|
|
100
|
-
`color` is `primary` | `neutral` | `danger` | `success`; plus `size`,
|
|
101
|
-
`disabled`, `icon`, and `href` (renders an `<a role=button>`).
|
|
102
|
-
- **`S.buttonGroup(opts)`** — groups buttons, `attached` (segmented) or `spaced`.
|
|
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:
|
|
103
79
|
|
|
104
|
-
|
|
105
|
-
|
|
80
|
+
```ts
|
|
81
|
+
S.button({ text: "Delete", attrs: ".danger" });
|
|
82
|
+
S.box({ attrs: ".raised.outlined", content: () => { ... } });
|
|
83
|
+
```
|
|
106
84
|
|
|
107
|
-
|
|
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.
|
|
108
86
|
|
|
109
|
-
|
|
110
|
-
|
|
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:
|
|
111
90
|
|
|
112
91
|
```ts
|
|
113
|
-
|
|
114
|
-
S.
|
|
115
|
-
//
|
|
116
|
-
$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
|
|
117
95
|
```
|
|
118
96
|
|
|
119
|
-
|
|
97
|
+
*Hint:* A `buttonChooser` is probably the right component for a color scheme selector.
|
|
98
|
+
|
|
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.
|
|
120
102
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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:
|
|
124
106
|
|
|
125
107
|
```ts
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
108
|
+
A.cssVars["s-primary"] = "#fdda58";
|
|
109
|
+
A.cssVars["s-secondary"] = "#cc5624";
|
|
110
|
+
A.cssVars["s-danger"] = "#ee4422";
|
|
111
|
+
A.cssVars["s-radius"] = "4px";
|
|
129
112
|
```
|
|
130
113
|
|
|
131
|
-
See
|
|
132
|
-
|
|
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:
|
|
117
|
+
|
|
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"});
|
|
121
|
+
|
|
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
|
+
```
|
|
133
128
|
|
|
134
|
-
|
|
129
|
+
Note that when changing CSS like this, things *may* break if you upgrade Staffa. The recommended update strategy is therefore: don't!
|
|
135
130
|
|
|
136
|
-
|
|
131
|
+
If you want to make changes that are dependent upon the current light/dark mode setting, rely on Aberdeen reactivity:
|
|
137
132
|
|
|
138
133
|
```ts
|
|
139
|
-
|
|
140
|
-
S.
|
|
141
|
-
|
|
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
|
+
});
|
|
142
143
|
```
|
|
143
144
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`
|
|
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.
|
|
147
148
|
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
### Layout & containers
|
|
150
|
+
|
|
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.
|
|
155
|
+
|
|
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
|
|
165
|
+
|
|
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.
|
|
168
|
+
|
|
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.
|
|
150
182
|
|
|
151
183
|
## Browser (no bundler)
|
|
152
184
|
|
|
153
|
-
`staffa/all.js` is a pre-built ESM bundle
|
|
154
|
-
Aberdeen external. Use an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap)
|
|
155
|
-
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):
|
|
156
186
|
|
|
157
187
|
```html
|
|
158
188
|
<script type="importmap">
|
|
@@ -170,17 +200,51 @@ to tell the browser where to find both:
|
|
|
170
200
|
</script>
|
|
171
201
|
```
|
|
172
202
|
|
|
173
|
-
|
|
174
|
-
|
|
203
|
+
It includes all components, but not the icons.
|
|
204
|
+
|
|
205
|
+
## Extending Staffa
|
|
206
|
+
|
|
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.
|
|
208
|
+
|
|
209
|
+
### Design principles
|
|
175
210
|
|
|
176
|
-
|
|
211
|
+
1. **Components are functions**. They take one typed options object, emit Aberdeen DOM, and *usually* return nothing.
|
|
212
|
+
|
|
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.
|
|
214
|
+
|
|
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
|
|
177
244
|
|
|
178
245
|
```sh
|
|
179
246
|
npm run build # compile TypeScript to dist/
|
|
180
|
-
|
|
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
|
|
181
250
|
```
|
|
182
|
-
|
|
183
|
-
`npm run smoke` builds and renders every component in jsdom as a quick check.
|
|
184
|
-
|
|
185
|
-
Contributing or extending Staffa? See [`AGENTS.md`](./AGENTS.md) for the design
|
|
186
|
-
philosophy and the add-a-component checklist.
|
|
@@ -2,21 +2,22 @@ import A from "aberdeen";
|
|
|
2
2
|
import { uniqueId } from "../core.js";
|
|
3
3
|
import { drawField } from "./field.js";
|
|
4
4
|
A.insertGlobalCss({
|
|
5
|
-
".
|
|
5
|
+
".s-ac": {
|
|
6
6
|
"&": "position:relative",
|
|
7
|
-
"> .
|
|
8
|
-
"> .
|
|
9
|
-
"> .
|
|
10
|
-
"&[aria-invalid=true] > .
|
|
11
|
-
".
|
|
12
|
-
".
|
|
13
|
-
".
|
|
7
|
+
"> .s-control": "display:flex flex-wrap:wrap align-items:center gap:$1 bg:$s-panel fg:$s-ink border: 1px solid $s-border; r:$s-radius padding: 0.3em 0.4em; cursor:text; transition: border-color 0.15s, box-shadow 0.15s;",
|
|
8
|
+
"> .s-control:hover": "border-color:$s-border-strong",
|
|
9
|
+
"> .s-control:focus-within": "border-color:$s-accent box-shadow: 0 0 0 3px $s-focus;",
|
|
10
|
+
"&[aria-invalid=true] > .s-control": "border-color:$s-danger",
|
|
11
|
+
".s-chip": "display:inline-flex align-items:center gap:$1 font-size:0.85em bg:$s-raised border: 1px solid $s-border; r:$s-radius padding: 0.1em 0.2em 0.1em 0.5em;",
|
|
12
|
+
".s-chip > button": "cursor:pointer border:0 background:transparent fg:$s-fg-muted font-size:1.1em line-height:1 padding: 0 0.2em; r:4px",
|
|
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
|
-
"> .
|
|
16
|
-
".
|
|
17
|
-
".
|
|
18
|
-
".
|
|
19
|
-
".
|
|
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-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);",
|
|
19
|
+
".s-add": "fg:$s-accent font-style:italic",
|
|
20
|
+
".s-empty": "padding: 0.45em 0.6em; fg:$s-fg-muted",
|
|
20
21
|
},
|
|
21
22
|
});
|
|
22
23
|
function normOption(o) {
|
|
@@ -93,17 +94,17 @@ export function autocomplete(opts) {
|
|
|
93
94
|
opts.bind.value = arr.filter((v) => v !== value);
|
|
94
95
|
};
|
|
95
96
|
drawField(opts, (id, isInvalid) => {
|
|
96
|
-
A("div.
|
|
97
|
+
A("div.s-ac", opts.inputAttrs, () => {
|
|
97
98
|
A(() => A("aria-invalid=", isInvalid() ? "true" : "false"));
|
|
98
99
|
let inputEl;
|
|
99
|
-
A("div.
|
|
100
|
+
A("div.s-control", () => {
|
|
100
101
|
A("click=", () => inputEl?.focus());
|
|
101
102
|
// Chips for multi-select.
|
|
102
103
|
A(() => {
|
|
103
104
|
if (!opts.multi)
|
|
104
105
|
return;
|
|
105
106
|
for (const value of selectedValues()) {
|
|
106
|
-
A("span.
|
|
107
|
+
A("span.s-chip", () => {
|
|
107
108
|
A("span #", A.peek(() => labelFor(value)));
|
|
108
109
|
A("button type=button aria-label=", `Remove ${value}`, () => {
|
|
109
110
|
A("#×");
|
|
@@ -152,9 +153,9 @@ export function autocomplete(opts) {
|
|
|
152
153
|
const list = filtered();
|
|
153
154
|
const q = $st.query.trim();
|
|
154
155
|
const showAdd = opts.allowCustom !== false && q !== "" && !list.some((o) => o.label.toLowerCase() === q.toLowerCase());
|
|
155
|
-
A("ul.
|
|
156
|
+
A("ul.s-menu role=listbox", `id=${menuId}`, () => {
|
|
156
157
|
list.forEach((option, i) => {
|
|
157
|
-
A("li.
|
|
158
|
+
A("li.s-option role=option", `id=${menuId}-opt-${i}`, () => {
|
|
158
159
|
A(() => A("aria-selected=", $st.active === i ? "true" : "false"));
|
|
159
160
|
A("#", option.label);
|
|
160
161
|
A("mousedown=", (e) => e.preventDefault());
|
|
@@ -165,14 +166,14 @@ export function autocomplete(opts) {
|
|
|
165
166
|
});
|
|
166
167
|
});
|
|
167
168
|
if (showAdd) {
|
|
168
|
-
A("li.
|
|
169
|
+
A("li.s-option.s-add role=option", () => {
|
|
169
170
|
A("#", `Add "${q}"`);
|
|
170
171
|
A("mousedown=", (e) => e.preventDefault());
|
|
171
172
|
A("click=", () => commit(q, inputEl));
|
|
172
173
|
});
|
|
173
174
|
}
|
|
174
175
|
if (list.length === 0 && !showAdd) {
|
|
175
|
-
A("li.
|
|
176
|
+
A("li.s-empty #No matches");
|
|
176
177
|
}
|
|
177
178
|
});
|
|
178
179
|
});
|
package/dist/components/box.d.ts
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
import { type Content, type ContentOptions, type Slot, type
|
|
1
|
+
import { type Content, 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. */
|
|
5
5
|
header?: Slot;
|
|
6
6
|
/** Footer content, drawn in a styled bar below the body. */
|
|
7
7
|
footer?: Slot;
|
|
8
|
+
/** Aberdeen attr/style string applied to the body (content-holding) element. */
|
|
9
|
+
contentAttrs?: Attributes;
|
|
8
10
|
/** Aberdeen attr/style string applied to the header bar. */
|
|
9
|
-
|
|
11
|
+
headerAttrs?: Attributes;
|
|
10
12
|
/** Aberdeen attr/style string applied to the footer bar. */
|
|
11
|
-
|
|
13
|
+
footerAttrs?: Attributes;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* A surface container — the workhorse layout primitive. Has an optional styled
|
|
15
17
|
* header and footer, and a padded body that holds {@link ContentOptions.content}.
|
|
16
18
|
*
|
|
17
19
|
* The body gets default `padding` and matching `gap`; add `display:flex` via
|
|
18
|
-
* {@link
|
|
19
|
-
* flex container.
|
|
20
|
+
* {@link BoxOptions.contentAttrs | contentAttrs} if you want its children laid
|
|
21
|
+
* out as a flex container.
|
|
20
22
|
*
|
|
21
23
|
* Shortcut: pass a function to use it directly as the body content.
|
|
22
24
|
*
|
|
23
25
|
* @example
|
|
24
26
|
* ```ts
|
|
25
|
-
* S.box({ header: "Profile",
|
|
27
|
+
* S.box({ header: "Profile", contentAttrs: "display:flex flex-direction:column", content: () => {
|
|
26
28
|
* S.textline({ label: "Name", bind: A.ref($user, "name") });
|
|
27
29
|
* }});
|
|
28
30
|
* S.box(() => A("p#Just some content")); // shorthand
|
package/dist/components/box.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
import { drawSlot } from "../core.js";
|
|
3
|
+
// The box itself is a `.panel` surface; its header/footer are `.raised`
|
|
4
|
+
// surfaces (classes set on the elements in `box()` below). Colours and borders
|
|
5
|
+
// come from the contextual tokens, so a box stays legible on whatever surface
|
|
6
|
+
// it's nested in.
|
|
3
7
|
A.insertGlobalCss({
|
|
4
|
-
".
|
|
5
|
-
"&": "display:flex flex-direction:column
|
|
6
|
-
"
|
|
7
|
-
">
|
|
8
|
-
|
|
8
|
+
".s-box": {
|
|
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",
|
|
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;",
|
|
9
13
|
"> div": "p:$3 gap:$3",
|
|
10
14
|
},
|
|
11
15
|
});
|
|
@@ -14,14 +18,14 @@ A.insertGlobalCss({
|
|
|
14
18
|
* header and footer, and a padded body that holds {@link ContentOptions.content}.
|
|
15
19
|
*
|
|
16
20
|
* The body gets default `padding` and matching `gap`; add `display:flex` via
|
|
17
|
-
* {@link
|
|
18
|
-
* flex container.
|
|
21
|
+
* {@link BoxOptions.contentAttrs | contentAttrs} if you want its children laid
|
|
22
|
+
* out as a flex container.
|
|
19
23
|
*
|
|
20
24
|
* Shortcut: pass a function to use it directly as the body content.
|
|
21
25
|
*
|
|
22
26
|
* @example
|
|
23
27
|
* ```ts
|
|
24
|
-
* S.box({ header: "Profile",
|
|
28
|
+
* S.box({ header: "Profile", contentAttrs: "display:flex flex-direction:column", content: () => {
|
|
25
29
|
* S.textline({ label: "Name", bind: A.ref($user, "name") });
|
|
26
30
|
* }});
|
|
27
31
|
* S.box(() => A("p#Just some content")); // shorthand
|
|
@@ -29,20 +33,20 @@ A.insertGlobalCss({
|
|
|
29
33
|
*/
|
|
30
34
|
export function box(opts = {}) {
|
|
31
35
|
const o = typeof opts === "function" ? { content: opts } : opts;
|
|
32
|
-
A("section.
|
|
36
|
+
A("section.s-box.s-s.panel", o.attrs, () => {
|
|
33
37
|
// Header and footer get their own scopes so toggling them doesn't recreate
|
|
34
38
|
// the body (which may hold focused inputs / lots of content).
|
|
35
39
|
A(() => {
|
|
36
40
|
if (o.header != null)
|
|
37
|
-
A("header", o.
|
|
41
|
+
A("header.s-s.raised", o.headerAttrs, () => drawSlot(o.header));
|
|
38
42
|
});
|
|
39
|
-
A("div", o.
|
|
43
|
+
A("div", o.contentAttrs, () => {
|
|
40
44
|
if (o.content)
|
|
41
45
|
o.content();
|
|
42
46
|
});
|
|
43
47
|
A(() => {
|
|
44
48
|
if (o.footer != null)
|
|
45
|
-
A("footer", o.
|
|
49
|
+
A("footer.s-s.raised", o.footerAttrs, () => drawSlot(o.footer));
|
|
46
50
|
});
|
|
47
51
|
});
|
|
48
52
|
}
|
|
@@ -1,29 +1,6 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
/**
|
|
3
|
-
* Visual weight of a button.
|
|
4
|
-
* - `filled`: solid background, highest emphasis.
|
|
5
|
-
* - `tonal`: soft tinted background, medium emphasis.
|
|
6
|
-
* - `outlined`: bordered, transparent background, lowest emphasis.
|
|
7
|
-
*
|
|
8
|
-
* Every variant carries at least a visible border, per Skye's "everything is
|
|
9
|
-
* legible at a glance" principle.
|
|
10
|
-
*/
|
|
11
|
-
export type ButtonVariant = "filled" | "tonal" | "outlined";
|
|
12
|
-
/**
|
|
13
|
-
* Color of a button.
|
|
14
|
-
*
|
|
15
|
-
* The four named **semantic roles** map to theme colours and are offered as
|
|
16
|
-
* autocomplete suggestions. You may also pass *any* CSS colour the browser
|
|
17
|
-
* understands and it becomes the button's accent directly: a literal like
|
|
18
|
-
* `"#ef6b00"` / `"rgb(255 107 0)"`, or a theme custom-property reference like
|
|
19
|
-
* `"$sWarning"` (Aberdeen's `$name` shorthand for `var(--name)`).
|
|
20
|
-
*
|
|
21
|
-
* The `(string & {})` member is what keeps the literal suggestions visible while
|
|
22
|
-
* still allowing arbitrary strings — TypeScript only widens to `string` lazily.
|
|
23
|
-
*/
|
|
24
|
-
export type ButtonColor = "primary" | "neutral" | "danger" | "success" | (string & {});
|
|
1
|
+
import { type Content, type Slot, type Attributes } from "../core.js";
|
|
25
2
|
/** Options for {@link button}. */
|
|
26
|
-
export interface ButtonOptions
|
|
3
|
+
export interface ButtonOptions {
|
|
27
4
|
/** Button label text. */
|
|
28
5
|
text?: string;
|
|
29
6
|
/** Custom content (overrides {@link ButtonOptions.text | text}). */
|
|
@@ -32,12 +9,6 @@ export interface ButtonOptions extends BaseOptions {
|
|
|
32
9
|
icon?: Slot;
|
|
33
10
|
/** Click handler. */
|
|
34
11
|
click?: (event: Event) => void;
|
|
35
|
-
/** Visual weight. Defaults to `"filled"`. */
|
|
36
|
-
variant?: ButtonVariant;
|
|
37
|
-
/** Color role. Defaults to `"primary"`. */
|
|
38
|
-
color?: ButtonColor;
|
|
39
|
-
/** Size. Defaults to `"md"`. */
|
|
40
|
-
size?: "sm" | "md" | "lg";
|
|
41
12
|
/** Disables the button. */
|
|
42
13
|
disabled?: boolean;
|
|
43
14
|
/** Native button behaviour. Defaults to `"button"`. */
|
|
@@ -46,20 +17,35 @@ export interface ButtonOptions extends BaseOptions {
|
|
|
46
17
|
href?: string;
|
|
47
18
|
/** Accessible label, when the button has only an icon. */
|
|
48
19
|
ariaLabel?: string;
|
|
49
|
-
/**
|
|
50
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Aberdeen attr/style string applied to the button. A button is a surface, so
|
|
22
|
+
* pass surface modifier classes here to restyle it, e.g. `".danger"`,
|
|
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.
|
|
28
|
+
*/
|
|
29
|
+
attrs?: Attributes;
|
|
51
30
|
}
|
|
52
31
|
/**
|
|
53
32
|
* A button. Always carries at least a visible border so its affordance is
|
|
54
|
-
* obvious at a glance
|
|
33
|
+
* obvious at a glance.
|
|
55
34
|
*
|
|
56
35
|
* Shortcut: pass a string to use it as the label, or a function for custom
|
|
57
36
|
* content.
|
|
58
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
|
+
*
|
|
59
45
|
* @example
|
|
60
46
|
* ```ts
|
|
61
47
|
* S.button({ text: "Save", click: save });
|
|
62
|
-
* S.button({ text: "Delete",
|
|
48
|
+
* S.button({ text: "Delete", attrs: ".danger .outlined", click: del });
|
|
63
49
|
* S.button("Cancel"); // shorthand for { text: "Cancel" }
|
|
64
50
|
* S.button({ href: "/docs", text: "Docs" }); // renders an <a role=button>
|
|
65
51
|
* ```
|