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.
Files changed (68) hide show
  1. package/README.md +161 -97
  2. package/dist/components/autocomplete.js +21 -20
  3. package/dist/components/box.d.ts +8 -6
  4. package/dist/components/box.js +16 -12
  5. package/dist/components/button.d.ts +21 -35
  6. package/dist/components/button.js +46 -39
  7. package/dist/components/buttonChooser.d.ts +41 -0
  8. package/dist/components/buttonChooser.js +38 -0
  9. package/dist/components/buttonGroup.d.ts +3 -3
  10. package/dist/components/buttonGroup.js +18 -21
  11. package/dist/components/checkbox.js +7 -7
  12. package/dist/components/dialog.d.ts +20 -25
  13. package/dist/components/dialog.js +81 -91
  14. package/dist/components/field.d.ts +8 -6
  15. package/dist/components/field.js +16 -18
  16. package/dist/components/form.d.ts +3 -3
  17. package/dist/components/form.js +4 -4
  18. package/dist/components/main.d.ts +41 -14
  19. package/dist/components/main.js +193 -53
  20. package/dist/components/menu.d.ts +118 -0
  21. package/dist/components/menu.js +218 -0
  22. package/dist/components/select.d.ts +1 -1
  23. package/dist/components/select.js +5 -5
  24. package/dist/components/tabs.d.ts +5 -5
  25. package/dist/components/tabs.js +15 -22
  26. package/dist/components/textarea.js +4 -4
  27. package/dist/components/textline.js +1 -1
  28. package/dist/components/toast.d.ts +37 -0
  29. package/dist/components/toast.js +79 -0
  30. package/dist/components/tooltip.d.ts +32 -0
  31. package/dist/components/tooltip.js +130 -0
  32. package/dist/core.d.ts +26 -39
  33. package/dist/core.js +6 -5
  34. package/dist/icons-helpers.d.ts +46 -0
  35. package/dist/icons-helpers.js +44 -0
  36. package/dist/icons.d.ts +1960 -0
  37. package/dist/icons.js +1972 -0
  38. package/dist/index.d.ts +21 -8
  39. package/dist/index.js +17 -8
  40. package/dist/staffa.esm.js +1 -0
  41. package/dist/theme.d.ts +9 -75
  42. package/dist/theme.js +279 -82
  43. package/package.json +12 -5
  44. package/src/components/autocomplete.ts +21 -20
  45. package/src/components/box.ts +21 -15
  46. package/src/components/button.ts +59 -75
  47. package/src/components/buttonChooser.ts +65 -0
  48. package/src/components/buttonGroup.ts +18 -21
  49. package/src/components/checkbox.ts +7 -7
  50. package/src/components/dialog.ts +101 -102
  51. package/src/components/field.ts +22 -22
  52. package/src/components/form.ts +7 -7
  53. package/src/components/main.ts +212 -52
  54. package/src/components/menu.ts +288 -0
  55. package/src/components/select.ts +4 -4
  56. package/src/components/tabs.ts +20 -27
  57. package/src/components/textarea.ts +4 -4
  58. package/src/components/textline.ts +1 -1
  59. package/src/components/toast.ts +115 -0
  60. package/src/components/tooltip.ts +139 -0
  61. package/src/core.ts +26 -40
  62. package/src/icons-helpers.ts +90 -0
  63. package/src/icons.ts +1977 -0
  64. package/src/index.ts +21 -8
  65. package/src/theme.ts +300 -135
  66. package/dist/components/modal.d.ts +0 -2
  67. package/dist/components/modal.js +0 -2
  68. package/dist/skye.esm.js +0 -1
@@ -1,5 +1,5 @@
1
1
  import A from "aberdeen";
2
- import { type Slot, type Styling, drawSlot } from "../core.js";
2
+ import { type Slot, type Attributes, drawSlot } from "../core.js";
3
3
  import { button } from "./button.js";
4
4
  import { buttonGroup } from "./buttonGroup.js";
5
5
  import { textline } from "./textline.js";
@@ -10,14 +10,14 @@ export interface DialogOptions {
10
10
  header?: Slot;
11
11
  /** Slot rendered in the styled footer bar. */
12
12
  footer?: Slot;
13
+ /** Aberdeen attr/style string applied to the dialog panel. A surface — pass modifier classes (e.g. `".warning"`) to recolour it. */
14
+ attrs?: Attributes;
13
15
  /** Aberdeen attr/style string applied to the header bar. */
14
- headerInner?: Styling;
16
+ headerAttrs?: Attributes;
15
17
  /** Aberdeen attr/style string applied to the footer bar. */
16
- footerInner?: Styling;
18
+ footerAttrs?: Attributes;
17
19
  /** Aberdeen attr/style string applied to the scrollable content `<div>`. */
18
- inner?: Styling;
19
- /** Aberdeen attr/style string applied to the dialog panel itself. */
20
- root?: Styling;
20
+ contentAttrs?: Attributes;
21
21
  /**
22
22
  * Allow closing via Esc or clicking the backdrop. Defaults to `true`.
23
23
  * May be changed on a proxied options object while the dialog is open
@@ -25,10 +25,16 @@ export interface DialogOptions {
25
25
  */
26
26
  allowCancel?: boolean;
27
27
  /**
28
- * Dialog body. Receives a `close()` function — call it to dismiss the dialog
29
- * programmatically.
28
+ * When set to `true` (default) the model will be destroyed when the `dialog()`-calling
29
+ * scope is destroyed.
30
30
  */
31
- content?: (close: () => void) => void;
31
+ cancelWithScope?: boolean;
32
+ /**
33
+ * Dialog body. A {@link Slot} whose draw-function receives a `close()` function
34
+ * — call it to dismiss the dialog programmatically. (A plain string renders as
35
+ * rich text.)
36
+ */
37
+ content?: Slot<[close: () => void]>;
32
38
  /**
33
39
  * Called when the dialog closes for any reason (explicit `close()`, Esc, or
34
40
  * backdrop click). Useful when you want a side-effect on close but don't need
@@ -37,48 +43,84 @@ export interface DialogOptions {
37
43
  onClose?: () => void;
38
44
  }
39
45
 
40
- // Transition helper classes.
41
- // `.S_backdrop` = backdrop, hidden when another backdrop follows it in the DOM.
42
- // `.S_dialog` = dialog box, slides + fades in/out.
43
46
  A.insertGlobalCss({
44
- ".S_backdrop": {
45
- "&": "position:fixed inset:0 z-index:200; background: rgba(0,0,0,0.55); transition: opacity 0.2s ease;",
46
- "&:not(:has(~ .S_backdrop))": "display:block",
47
- "&:not(:has(~ .S_backdrop)) + .S_dialog": "display:flex flex-direction:column",
48
- // Transition states: applied momentarily on create; re-applied on destroy.
47
+ ".s-backdrop": {
48
+ "&": "position:fixed inset:0 z-index:200 display:block background: rgba(0,0,0,0.55); transition: opacity 0.4s ease-in-out;",
49
49
  "&.hidden": "opacity:0 pointer-events:none",
50
50
  },
51
- ".S_dialog": {
51
+ ".s-dialog": {
52
52
  "&":
53
- "position:fixed z-index:201 top:50% left:50% " +
53
+ "position:fixed z-index:200 top:50% left:50% " +
54
+ "display:flex flex-direction:column " +
54
55
  "transform:translate(-50%,-50%) " +
55
56
  "min-width:20rem max-width:min(90vw,44rem) max-height:min(88vh,800px) " +
56
- "bg:$sSurface border: 1px solid $sBorder; r:$sRadiusLg box-shadow:$sShadow overflow:hidden " +
57
- "transition: opacity 0.2s ease, transform 0.2s ease;",
58
- // Header and footer are fixed; only the content <div> scrolls.
57
+ "border: 1px solid $s-border; r: $s-radius-lg; box-shadow: $s-shadow; overflow:hidden " +
58
+ "transition: opacity 0.2s ease-out, transform 0.2s ease-out;",
59
59
  "> header":
60
60
  "display:flex align-items:center gap:$2 padding: $2 $3; " +
61
- "bg:$sSurfaceHi border-bottom: 1px solid $sBorder; font-weight:600 flex-shrink:0",
61
+ "border-bottom: 1px solid $s-border; font-weight:600 flex-shrink:0",
62
62
  "> footer":
63
63
  "display:flex align-items:center gap:$2 padding: $2 $3; " +
64
- "bg:$sSurfaceHi border-top: 1px solid $sBorder; flex-shrink:0",
64
+ "border-top: 1px solid $s-border; flex-shrink:0",
65
65
  "> div": "p:$3 gap:$3 display:flex flex-direction:column overflow-y:auto flex:1 min-height:0",
66
- "&.hidden": "opacity:0 pointer-events:none transform: translate(-50%, calc(-50% + 20px));",
67
- "&.hidden *": "pointer-events:none",
66
+ "&.hidden": "opacity:0 pointer-events:none transform: translate(-50%, calc(-50% + 20px)); pointer-events:none",
68
67
  },
69
68
  });
70
69
 
70
+ const dialogs = A.proxy({} as Record<number,{resolve: (value: void | PromiseLike<void>) => void, opts: DialogOptions}>);
71
+ let dialogCount = 0;
72
+
73
+ const topDialogId = A.derive(() => {
74
+ const keys = Object.keys(dialogs);
75
+ if (keys.length) return keys[keys.length-1];
76
+ });
77
+
78
+ A.mount(document.body, () => {
79
+ A.onEach(dialogs, ({resolve, opts}, dialogId) => {
80
+ const close = () => { delete dialogs[dialogId]; };
81
+
82
+ A.clean(() => {
83
+ // Fires when this render is torn down — either because $closed became
84
+ // true (normal close) or because the parent reactive scope was cleaned up.
85
+ opts.onClose?.();
86
+ resolve();
87
+ });
88
+
89
+ // Backdrop - hide when not the top dialog
90
+ const overlaid = A.derive(() => topDialogId.value != dialogId);
91
+ A("div.s-backdrop create=hidden destroy=hidden .hidden=", overlaid, "click=", () => {
92
+ if (opts.allowCancel !== false) close();
93
+ });
94
+
95
+ // Dialog itself
96
+ A("div.s-dialog.s-s.panel create=hidden destroy=hidden", opts.attrs, () => {
97
+ A(() => {
98
+ if (opts.header != null) {
99
+ A("header.s-s.raised", opts.headerAttrs, () => drawSlot(opts.header));
100
+ }
101
+ });
102
+
103
+ A("div", opts.contentAttrs, () => {
104
+ drawSlot(opts.content, close);
105
+ });
106
+
107
+ A(() => {
108
+ if (opts.footer != null) {
109
+ A("footer.s-s.raised", opts.footerAttrs, () => drawSlot(opts.footer));
110
+ }
111
+ });
112
+ });
113
+ });
114
+ })
115
+
71
116
  /**
72
117
  * A dialog rendered into `document.body` via `A.mount`, with a dimming backdrop
73
118
  * that fades in and out. Returns a `Promise<void>` that resolves when the dialog
74
119
  * closes. Lifecycle is also tied to the parent reactive scope — when that scope
75
120
  * is cleaned up the dialog disappears and the promise resolves.
76
121
  *
77
- * Only the **last** open dialog (and its backdrop) is visible; earlier pairs are
78
- * hidden via the CSS `+` selector, so nested dialogs stack correctly.
79
- *
80
- * The header and footer are pinned; only the body content scrolls when it is
81
- * taller than `88vh`.
122
+ * Multiple dialogs stack correctly: each new pair (backdrop + dialog) has a
123
+ * higher z-index, while older dialogs are pushed behind their covering backdrop.
82
124
  *
83
125
  * @example
84
126
  * ```ts
@@ -87,68 +129,34 @@ A.insertGlobalCss({
87
129
  * content: (close) => {
88
130
  * A("p #Are you sure?");
89
131
  * S.button({ text: "Yes", click: () => { doIt(); close(); } });
90
- * S.button({ text: "Cancel", variant: "outlined", click: close });
132
+ * S.button({ text: "Cancel", attrs: ".neutral .outlined", click: close });
91
133
  * },
92
134
  * });
93
135
  * ```
94
136
  */
95
137
  export function dialog(opts: DialogOptions): Promise<void> {
96
- return new Promise<void>((resolve) => {
97
- const $closed = A.proxy(false);
98
- const close = () => { $closed.value = true; };
99
-
100
- let resolved = false;
101
- const onDone = () => {
102
- if (resolved) return;
103
- resolved = true;
104
- opts.onClose?.();
105
- resolve();
106
- };
107
-
108
- // A.mount ties this scope to the calling reactive scope — when the parent
109
- // scope is torn down, the backdrop and dialog are removed from body too.
110
- A.mount(document.body, () => {
111
- // The 'peek' is there such that when 'closed' is first set, this scope doesn't need to watch anything anymore.
112
- if (A.peek($closed, "value"), $closed.value) return;
113
-
114
- // Global Esc listener — registered here so it's removed on close.
115
- const onKey = (e: KeyboardEvent) => {
116
- if (e.key === "Escape" && opts.allowCancel !== false) close();
117
- };
118
- document.addEventListener("keydown", onKey);
119
- A.clean(() => {
120
- document.removeEventListener("keydown", onKey);
121
- // Fires when this render is torn down — either because $closed became
122
- // true (normal close) or because the parent reactive scope was cleaned up.
123
- onDone();
124
- });
125
-
126
- // Backdrop: fades in on creation, fades out on removal.
127
- A("div.S_backdrop create=hidden destroy=hidden", () => {
128
- A("click=", () => {
129
- if (opts.allowCancel !== false) close();
130
- });
131
- });
132
-
133
- // Dialog panel: fades + slides in/out.
134
- A("div.S_dialog create=hidden destroy=hidden", opts.root, () => {
135
- A(() => {
136
- if (opts.header != null) {
137
- A("header", opts.headerInner, () => drawSlot(opts.header));
138
+ if (!dialogCount) {
139
+ // Install Esc handler the first time we create a dialog
140
+ document.addEventListener("keydown", (e: KeyboardEvent) => {
141
+ if (e.key === "Escape" && opts.allowCancel !== false) {
142
+ const ds = A.unproxy(dialogs);
143
+ // Search for top-most dialog
144
+ for(let i=dialogCount; i>0; i--) {
145
+ if (ds[i]) {
146
+ if (ds[i].opts.allowCancel !== false) {
147
+ // The clean handler should call resolve and onClose
148
+ delete dialogs[i];
149
+ }
150
+ break;
138
151
  }
139
- });
140
-
141
- A("div", opts.inner, () => {
142
- if (opts.content) opts.content(close);
143
- });
144
-
145
- A(() => {
146
- if (opts.footer != null) {
147
- A("footer", opts.footerInner, () => drawSlot(opts.footer));
148
- }
149
- });
150
- });
152
+ }
153
+ }
151
154
  });
155
+ }
156
+ const dialogId = ++dialogCount;
157
+ if (opts.cancelWithScope !== false) A.clean(() => { delete dialogs[dialogId]; });
158
+ return new Promise<void>((resolve) => {
159
+ dialogs[dialogId] = {resolve, opts};
152
160
  });
153
161
  }
154
162
 
@@ -156,8 +164,6 @@ export function dialog(opts: DialogOptions): Promise<void> {
156
164
  * Shows a message dialog with a single OK button. Returns a `Promise<void>`
157
165
  * that resolves when the user dismisses it.
158
166
  *
159
- * All properties of `opts` override the defaults, including `content`.
160
- *
161
167
  * @example
162
168
  * ```ts
163
169
  * await S.alert("File saved successfully.");
@@ -169,7 +175,7 @@ export function alert(message: string, opts: Partial<DialogOptions> = {}): Promi
169
175
  allowCancel: true,
170
176
  content: (close) => {
171
177
  A("p", () => { A("#", message); });
172
- buttonGroup({ layout: "spaced", root: "align-self:flex-end", content: () => {
178
+ buttonGroup({ layout: "spaced", attrs: "align-self:flex-end", content: () => {
173
179
  button({ text: "OK", click: close });
174
180
  }});
175
181
  },
@@ -179,10 +185,7 @@ export function alert(message: string, opts: Partial<DialogOptions> = {}): Promi
179
185
 
180
186
  /**
181
187
  * Shows a confirmation dialog with Cancel and OK buttons. Returns a
182
- * `Promise<boolean>` — `true` if the user clicked OK, `false` otherwise
183
- * (including Esc / backdrop click when `allowCancel` is not `false`).
184
- *
185
- * All properties of `opts` override the defaults, including `content`.
188
+ * `Promise<boolean>` — `true` if the user clicked OK, `false` otherwise.
186
189
  *
187
190
  * @example
188
191
  * ```ts
@@ -197,8 +200,8 @@ export function confirm(message: string, opts: Partial<DialogOptions> = {}): Pro
197
200
  allowCancel: true,
198
201
  content: (close) => {
199
202
  A("p", () => { A("#", message); });
200
- buttonGroup({ layout: "spaced", root: "align-self:flex-end", content: () => {
201
- button({ text: "Cancel", variant: "outlined", color: "neutral", click: close });
203
+ buttonGroup({ layout: "spaced", attrs: "align-self:flex-end", content: () => {
204
+ button({ text: "Cancel", attrs: ".neutral .outlined", click: close });
202
205
  button({ text: "OK", click: () => { confirmed = true; close(); } });
203
206
  }});
204
207
  },
@@ -213,10 +216,7 @@ export function confirm(message: string, opts: Partial<DialogOptions> = {}): Pro
213
216
 
214
217
  /**
215
218
  * Shows a prompt dialog with a text input. Returns a `Promise<string | null>` —
216
- * the entered string if the user confirmed, or `null` if cancelled (Esc /
217
- * backdrop click / Cancel button).
218
- *
219
- * All properties of `opts` override the defaults, including `content`.
219
+ * the entered string if the user confirmed, or `null` if cancelled.
220
220
  *
221
221
  * @example
222
222
  * ```ts
@@ -233,7 +233,6 @@ export function prompt(message: string, defaultValue = "", opts: Partial<DialogO
233
233
  content: (close) => {
234
234
  A("p", () => { A("#", message); });
235
235
  const $v = A.proxy({ value: defaultValue });
236
- // Wrap in a form so Enter submits; display:contents keeps flex layout intact.
237
236
  A("form display:contents", () => {
238
237
  A("submit=", (e: Event) => {
239
238
  e.preventDefault();
@@ -241,8 +240,8 @@ export function prompt(message: string, defaultValue = "", opts: Partial<DialogO
241
240
  close();
242
241
  });
243
242
  textline({ bind: A.ref($v, "value") });
244
- buttonGroup({ layout: "spaced", root: "align-self:flex-end", content: () => {
245
- button({ text: "Cancel", variant: "outlined", color: "neutral", type: "button", click: close });
243
+ buttonGroup({ layout: "spaced", attrs: "align-self:flex-end", content: () => {
244
+ button({ text: "Cancel", attrs: ".neutral .outlined", type: "button", click: close });
246
245
  button({ text: "OK", type: "submit" });
247
246
  }});
248
247
  });
@@ -1,5 +1,5 @@
1
1
  import A from "aberdeen";
2
- import { type BaseOptions, type Bindable, type Slot, type Styling, drawSlot, uniqueId } from "../core.js";
2
+ import { type Bindable, type Slot, type Attributes, drawSlot, uniqueId } from "../core.js";
3
3
 
4
4
  /**
5
5
  * Options shared by all *form field* components (textline, textarea, checkbox,
@@ -9,7 +9,9 @@ import { type BaseOptions, type Bindable, type Slot, type Styling, drawSlot, uni
9
9
  * itself, and optional help/error text below it. {@link form} relies on this
10
10
  * shared structure to align groups of fields.
11
11
  */
12
- export interface FieldOptions extends BaseOptions {
12
+ export interface FieldOptions {
13
+ /** Aberdeen attr/style string applied to the field's wrapper element. */
14
+ attrs?: Attributes;
13
15
  /** Visible label, associated with the control via `for`/`id` for a11y. */
14
16
  label?: Slot;
15
17
  /** Helper text shown beneath the control. */
@@ -27,26 +29,24 @@ export interface FieldOptions extends BaseOptions {
27
29
  name?: string;
28
30
  /** Explicit id for the control; auto-generated when omitted. */
29
31
  id?: string;
30
- /** Aberdeen attr/style string applied to the control element itself. */
31
- control?: Styling;
32
+ /** Aberdeen attr/style string applied to the control (input) element itself. */
33
+ inputAttrs?: Attributes;
32
34
  }
33
35
 
34
36
  A.insertGlobalCss({
35
- ".S_field": {
37
+ ".s-field": {
36
38
  "&": "display:flex flex-direction:column gap:$1",
37
- "> label": "font-weight:600 font-size:0.9em fg:$sFg user-select:none",
39
+ "> label": "font-weight:600 font-size:0.9em fg:$s-fg user-select:none",
38
40
  },
39
- // Shared, reusable bits (also used by checkbox & autocomplete).
40
- ".S_req": "fg:$sDanger margin-left:2px",
41
- ".S_help": "font-size:0.82em fg:$sFgMuted",
42
- ".S_error": "font-size:0.82em fg:$sDanger",
43
- // Shared look for text-like controls.
44
- ".S_input": {
45
- "&": "w:100% bg:$sSurface fg:$sFg border: 1px solid $sBorder; r:$sRadius padding: 0.55em 0.7em; transition: border-color 0.15s, box-shadow 0.15s;",
46
- "&:hover:not(:disabled)": "border-color:$sBorderStrong",
47
- "&:focus-visible": "border-color:$sPrimary box-shadow: 0 0 0 3px $sFocus; outline:none",
41
+ ".s-req": "fg:$s-danger margin-left:2px",
42
+ ".s-help": "font-size:0.82em fg:$s-fg-muted",
43
+ ".s-error": "font-size:0.82em fg:$s-danger",
44
+ ".s-input": {
45
+ "&": "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;",
46
+ "&:hover:not(:disabled)": "border-color:$s-border-strong",
47
+ "&:focus-visible": "border-color:$s-accent box-shadow: 0 0 0 3px $s-focus; outline:none",
48
48
  "&:disabled": "opacity:0.6 cursor:not-allowed",
49
- "&[aria-invalid=true]": "border-color:$sDanger",
49
+ "&[aria-invalid=true]": "border-color:$s-danger",
50
50
  },
51
51
  });
52
52
 
@@ -60,8 +60,8 @@ A.insertGlobalCss({
60
60
  *
61
61
  * @param opts The field options.
62
62
  * @param drawControl Receives the resolved `id` and the live "invalid" getter,
63
- * and must draw the actual control element (using class `S_input` where
64
- * appropriate, and passing `opts.control` as an arg for caller styling).
63
+ * and must draw the actual control element (using class `s-input` where
64
+ * appropriate, and passing `opts.inputAttrs` as an arg for caller styling).
65
65
  */
66
66
  export function drawField(
67
67
  opts: FieldOptions,
@@ -70,12 +70,12 @@ export function drawField(
70
70
  const id = opts.id ?? uniqueId("field");
71
71
  const isInvalid = () => !!opts.error;
72
72
 
73
- A("div.S_field", opts.root, () => {
73
+ A("div.s-field", opts.attrs, () => {
74
74
  A(() => {
75
75
  if (opts.label != null) {
76
76
  A(`label for=${id}`, () => {
77
77
  drawSlot(opts.label);
78
- if (opts.required) A("span.S_req aria-hidden=true #*");
78
+ if (opts.required) A("span.s-req aria-hidden=true #*");
79
79
  });
80
80
  }
81
81
  });
@@ -83,10 +83,10 @@ export function drawField(
83
83
  drawControl(id, isInvalid);
84
84
 
85
85
  A(() => {
86
- if (opts.help != null && !opts.error) A("div.S_help", () => drawSlot(opts.help));
86
+ if (opts.help != null && !opts.error) A("div.s-help", () => drawSlot(opts.help));
87
87
  });
88
88
  A(() => {
89
- if (opts.error) A("div.S_error role=alert #", opts.error);
89
+ if (opts.error) A("div.s-error role=alert #", opts.error);
90
90
  });
91
91
  });
92
92
  }
@@ -1,5 +1,5 @@
1
1
  import A from "aberdeen";
2
- import { type Content, type ContentOptions, type Styling } from "../core.js";
2
+ import { type Content, type ContentOptions, type Attributes } from "../core.js";
3
3
 
4
4
  /** Options for {@link form}. */
5
5
  export interface FormOptions extends ContentOptions {
@@ -12,20 +12,20 @@ export interface FormOptions extends ContentOptions {
12
12
  /**
13
13
  * Layout of fields. `"stacked"` (default) is a single column; `"grid"` packs
14
14
  * fields into a responsive multi-column grid. A field can span the full grid
15
- * width by adding the `.S_wide` class (e.g. `root: ".S_wide"`).
15
+ * width by adding the `.s-wide` class (e.g. `attrs: ".s-wide"`).
16
16
  */
17
17
  layout?: "stacked" | "grid";
18
18
  /** Aberdeen attr/style string for the action bar. */
19
- actionsInner?: Styling;
19
+ actionsAttrs?: Attributes;
20
20
  /** Footer actions (typically a {@link import("./buttonGroup").buttonGroup} or buttons). */
21
21
  actions?: Content;
22
22
  }
23
23
 
24
24
  A.insertGlobalCss({
25
- ".S_form": {
25
+ ".s-form": {
26
26
  "&": "display:flex flex-direction:column gap:$3",
27
27
  "&.grid": "display:grid grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap:$3",
28
- "&.grid > .S_wide, &.grid > footer": "grid-column: 1 / -1;",
28
+ "&.grid > .s-wide, &.grid > footer": "grid-column: 1 / -1;",
29
29
  "> footer": "display:flex align-items:center gap:$2 flex-wrap:wrap margin-top:$1",
30
30
  },
31
31
  });
@@ -54,7 +54,7 @@ A.insertGlobalCss({
54
54
  export function form(opts: FormOptions | Content = {}): void {
55
55
  const o: FormOptions = typeof opts === "function" ? { content: opts } : opts;
56
56
 
57
- A(`form.S_form`, o.root, o.inner, () => {
57
+ A(`form.s-form`, o.attrs, () => {
58
58
  // Toggle grid class in its own scope so changing layout doesn't recreate
59
59
  // the fields (which would lose focus / input state).
60
60
  A(() => {
@@ -78,7 +78,7 @@ export function form(opts: FormOptions | Content = {}): void {
78
78
 
79
79
  // Own scope so toggling actions doesn't recreate the fields above.
80
80
  A(() => {
81
- if (o.actions) A("footer", o.actionsInner, () => o.actions?.());
81
+ if (o.actions) A("footer", o.actionsAttrs, () => o.actions?.());
82
82
  });
83
83
  });
84
84
  }