staffa 0.1.0 → 0.2.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.
Files changed (50) hide show
  1. package/README.md +65 -27
  2. package/dist/components/autocomplete.js +20 -20
  3. package/dist/components/box.d.ts +8 -6
  4. package/dist/components/box.js +15 -11
  5. package/dist/components/button.d.ts +10 -33
  6. package/dist/components/button.js +17 -38
  7. package/dist/components/buttonChooser.d.ts +42 -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 -18
  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 +18 -17
  16. package/dist/components/form.d.ts +3 -3
  17. package/dist/components/form.js +4 -4
  18. package/dist/components/main.d.ts +7 -5
  19. package/dist/components/main.js +25 -23
  20. package/dist/components/select.d.ts +1 -1
  21. package/dist/components/select.js +5 -5
  22. package/dist/components/tabs.d.ts +5 -3
  23. package/dist/components/tabs.js +25 -19
  24. package/dist/components/textarea.js +4 -4
  25. package/dist/components/textline.js +1 -1
  26. package/dist/core.d.ts +26 -39
  27. package/dist/core.js +6 -5
  28. package/dist/index.d.ts +10 -8
  29. package/dist/index.js +9 -8
  30. package/dist/staffa.esm.js +1 -0
  31. package/dist/theme.d.ts +9 -75
  32. package/dist/theme.js +178 -82
  33. package/package.json +3 -2
  34. package/src/components/autocomplete.ts +20 -20
  35. package/src/components/box.ts +20 -14
  36. package/src/components/button.ts +24 -72
  37. package/src/components/buttonChooser.ts +66 -0
  38. package/src/components/buttonGroup.ts +18 -18
  39. package/src/components/checkbox.ts +7 -7
  40. package/src/components/dialog.ts +101 -102
  41. package/src/components/field.ts +24 -21
  42. package/src/components/form.ts +7 -7
  43. package/src/components/main.ts +30 -26
  44. package/src/components/select.ts +4 -4
  45. package/src/components/tabs.ts +29 -22
  46. package/src/components/textarea.ts +4 -4
  47. package/src/components/textline.ts +1 -1
  48. package/src/core.ts +26 -40
  49. package/src/index.ts +10 -8
  50. package/src/theme.ts +190 -135
@@ -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,27 @@ 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
41
  // 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",
42
+ ".s-req": "fg:$s-danger margin-left:2px",
43
+ ".s-help": "font-size:0.82em fg:$s-fg-muted",
44
+ ".s-error": "font-size:0.82em fg:$s-danger",
45
+ // Shared look for text-like controls: a panel fill with a contextual border,
46
+ // the brand accent for focus, and the semantic danger ink when invalid.
47
+ ".s-input": {
48
+ "&": "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;",
49
+ "&:hover:not(:disabled)": "border-color:$s-border-strong",
50
+ "&:focus-visible": "border-color:$s-accent box-shadow: 0 0 0 3px $s-focus; outline:none",
48
51
  "&:disabled": "opacity:0.6 cursor:not-allowed",
49
- "&[aria-invalid=true]": "border-color:$sDanger",
52
+ "&[aria-invalid=true]": "border-color:$s-danger",
50
53
  },
51
54
  });
52
55
 
@@ -60,8 +63,8 @@ A.insertGlobalCss({
60
63
  *
61
64
  * @param opts The field options.
62
65
  * @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).
66
+ * and must draw the actual control element (using class `s-input` where
67
+ * appropriate, and passing `opts.inputAttrs` as an arg for caller styling).
65
68
  */
66
69
  export function drawField(
67
70
  opts: FieldOptions,
@@ -70,12 +73,12 @@ export function drawField(
70
73
  const id = opts.id ?? uniqueId("field");
71
74
  const isInvalid = () => !!opts.error;
72
75
 
73
- A("div.S_field", opts.root, () => {
76
+ A("div.s-field", opts.attrs, () => {
74
77
  A(() => {
75
78
  if (opts.label != null) {
76
79
  A(`label for=${id}`, () => {
77
80
  drawSlot(opts.label);
78
- if (opts.required) A("span.S_req aria-hidden=true #*");
81
+ if (opts.required) A("span.s-req aria-hidden=true #*");
79
82
  });
80
83
  }
81
84
  });
@@ -83,10 +86,10 @@ export function drawField(
83
86
  drawControl(id, isInvalid);
84
87
 
85
88
  A(() => {
86
- if (opts.help != null && !opts.error) A("div.S_help", () => drawSlot(opts.help));
89
+ if (opts.help != null && !opts.error) A("div.s-help", () => drawSlot(opts.help));
87
90
  });
88
91
  A(() => {
89
- if (opts.error) A("div.S_error role=alert #", opts.error);
92
+ if (opts.error) A("div.s-error role=alert #", opts.error);
90
93
  });
91
94
  });
92
95
  }
@@ -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
  }
@@ -1,8 +1,10 @@
1
1
  import A from "aberdeen";
2
- import { type BaseOptions, type Content, type Slot, type Styling, drawSlot } from "../core.js";
2
+ import { type Content, type Slot, type Attributes, drawSlot } from "../core.js";
3
3
 
4
4
  /** Options for {@link main}. */
5
- export interface MainOptions extends BaseOptions {
5
+ export interface MainOptions {
6
+ /** Aberdeen attr/style string applied to the outermost shell element. */
7
+ attrs?: Attributes;
6
8
  /** App/page title shown in the top bar. */
7
9
  title?: Slot;
8
10
  /** Secondary line under the title. */
@@ -22,25 +24,27 @@ export interface MainOptions extends BaseOptions {
22
24
  */
23
25
  maxWidth?: string;
24
26
  /** Aberdeen attr/style string applied to the content sheet. */
25
- inner?: Styling;
27
+ contentAttrs?: Attributes;
26
28
  /** Aberdeen attr/style string applied to the top bar. */
27
- topbarInner?: Styling;
29
+ topbarAttrs?: Attributes;
28
30
  }
29
31
 
30
32
  A.insertGlobalCss({
31
- ".S_main": {
32
- "&": "display:flex flex-direction:column min-height:100vh max-height:100vh bg:$sBg fg:$sFg",
33
- "> header": "display:flex align-items:center gap:$3 padding: $2 $3; bg:$sSurfaceHi border-bottom: 1px solid $sBorder; position:sticky top:0 z-index:10",
34
- "> header .S_icon": "display:flex align-items:center font-size:1.4em",
35
- "> header .S_titles": "display:flex flex-direction:column min-width:0 flex:1",
36
- "> header .S_title": "font-weight:700 font-size:1.1em line-height:1.2 overflow:hidden text-overflow:ellipsis white-space:nowrap",
37
- "> header .S_subtitle": "fg:$sFgMuted font-size:0.85em overflow:hidden text-overflow:ellipsis white-space:nowrap",
38
- "> header .S_menu": "display:flex align-items:center gap:$2",
33
+ ".s-main": {
34
+ // Colours come from the surface classes added in main(): the shell is a
35
+ // filled base, the bars are raised, the framed sheet is a panel.
36
+ "&": "display:flex flex-direction:column min-height:100vh max-height:100vh",
37
+ "> header": "display:flex align-items:center gap:$3 padding: $2 $3; border-bottom: 1px solid $s-border; position:sticky top:0 z-index:10",
38
+ "> header .s-icon": "display:flex align-items:center font-size:1.4em",
39
+ "> header .s-titles": "display:flex flex-direction:column min-width:0 flex:1",
40
+ "> header .s-title": "font-weight:700 font-size:1.1em line-height:1.2 overflow:hidden text-overflow:ellipsis white-space:nowrap",
41
+ "> header .s-subtitle": "fg:$s-fg-muted font-size:0.85em overflow:hidden text-overflow:ellipsis white-space:nowrap",
42
+ "> header .s-menu": "display:flex align-items:center gap:$2",
39
43
  "> main": "flex:1 overflow-y:auto display:flex flex-direction:column",
40
- "> main > .S_content": "width:100% flex:1",
41
- "> main > .S_content.S_framed": "margin: $3 auto; bg:$sSurface border: 1px solid $sBorder; r:$sRadiusLg box-shadow:$sShadow p:$4",
42
- "> main > .S_content.S_plain": "p:$3",
43
- "> footer": "display:flex align-items:center gap:$2 padding: $2 $3; bg:$sSurfaceHi border-top: 1px solid $sBorder; fg:$sFgMuted",
44
+ "> main > .s-content": "width:100% flex:1",
45
+ "> main > .s-content.s-framed": "margin: $3 auto; border: 1px solid $s-border; r:$s-radius-lg box-shadow:$s-shadow p:$4",
46
+ "> main > .s-content.s-plain": "p:$3",
47
+ "> footer": "display:flex align-items:center gap:$2 padding: $2 $3; border-top: 1px solid $s-border; fg:$s-fg-muted",
44
48
  },
45
49
  });
46
50
 
@@ -65,38 +69,38 @@ A.insertGlobalCss({
65
69
  * ```
66
70
  */
67
71
  export function main(opts: MainOptions = {}): void {
68
- A("div.S_main", opts.root, () => {
72
+ A("div.s-main.s-s.base", opts.attrs, () => {
69
73
  // Top bar — only rendered when there's something to show in it.
70
74
  A(() => {
71
75
  const hasBar = opts.title != null || opts.subtitle != null || opts.icon != null || opts.menu != null;
72
76
  if (!hasBar) return;
73
- A("header", opts.topbarInner, () => {
77
+ A("header.s-s.raised", opts.topbarAttrs, () => {
74
78
  A(() => {
75
- if (opts.icon != null) A("div.S_icon", () => drawSlot(opts.icon));
79
+ if (opts.icon != null) A("div.s-icon", () => drawSlot(opts.icon));
76
80
  });
77
- A("div.S_titles", () => {
81
+ A("div.s-titles", () => {
78
82
  A(() => {
79
- if (opts.title != null) A("div.S_title", () => drawSlot(opts.title));
83
+ if (opts.title != null) A("div.s-title", () => drawSlot(opts.title));
80
84
  });
81
85
  A(() => {
82
- if (opts.subtitle != null) A("div.S_subtitle", () => drawSlot(opts.subtitle));
86
+ if (opts.subtitle != null) A("div.s-subtitle", () => drawSlot(opts.subtitle));
83
87
  });
84
88
  });
85
89
  A(() => {
86
- if (opts.menu) A("div.S_menu", () => opts.menu?.());
90
+ if (opts.menu) A("div.s-menu", () => opts.menu?.());
87
91
  });
88
92
  });
89
93
  });
90
94
 
91
95
  // Scrollable main region with the (optionally framed) content sheet.
92
96
  A("main", () => {
93
- A("div.S_content", opts.inner, () => {
97
+ A("div.s-content", opts.contentAttrs, () => {
94
98
  // Framing applied in its own scope so changing maxWidth doesn't
95
99
  // recreate the content (which holds the whole page).
96
100
  A(() => {
97
101
  const max = opts.maxWidth;
98
- if (max != null) A(".S_framed max-width:", max);
99
- else A(".S_plain");
102
+ if (max != null) A(".s-framed.s-s.panel max-width:", max);
103
+ else A(".s-plain");
100
104
  });
101
105
  if (opts.content) opts.content();
102
106
  });
@@ -17,10 +17,10 @@ export interface SelectOptions extends FieldOptions {
17
17
 
18
18
  // Wrapper provides the chevron via ::after (pseudo-elements on <select> are unreliable).
19
19
  A.insertGlobalCss({
20
- ".S_select_wrap": {
20
+ ".s-select_wrap": {
21
21
  "&": "position:relative display:block",
22
22
  "select": "w:100% cursor:pointer padding-right:2.2em; appearance:none",
23
- "&::after": "content: '▾'; position:absolute right:0.7em top:50%; transform: translateY(-50%); pointer-events:none fg:$sFgMuted font-size:0.85em",
23
+ "&::after": "content: '▾'; position:absolute right:0.7em top:50%; transform: translateY(-50%); pointer-events:none fg:$s-fg-muted font-size:0.85em",
24
24
  },
25
25
  });
26
26
 
@@ -36,8 +36,8 @@ A.insertGlobalCss({
36
36
  */
37
37
  export function select(opts: SelectOptions): void {
38
38
  drawField(opts, (id, isInvalid) => {
39
- A("div.S_select_wrap", opts.control, () => {
40
- A("select.S_input", () => {
39
+ A("div.s-select_wrap", opts.inputAttrs, () => {
40
+ A("select.s-input", () => {
41
41
  applyControlAttrs(opts, id, isInvalid);
42
42
 
43
43
  A("change=", (e: Event) => {
@@ -1,5 +1,5 @@
1
1
  import A from "aberdeen";
2
- import { type BaseOptions, type Bindable, type Content, type Slot, type Styling, drawSlot, uniqueId } from "../core.js";
2
+ import { type Bindable, type Content, type Slot, type Attributes, drawSlot, uniqueId } from "../core.js";
3
3
 
4
4
  /** A single tab definition. */
5
5
  export interface Tab {
@@ -16,7 +16,9 @@ export interface Tab {
16
16
  }
17
17
 
18
18
  /** Options for {@link tabs}. */
19
- export interface TabsOptions extends BaseOptions {
19
+ export interface TabsOptions {
20
+ /** Aberdeen attr/style string applied to the outermost element. */
21
+ attrs?: Attributes;
20
22
  /** The tabs to display. */
21
23
  tabs: Tab[];
22
24
  /**
@@ -27,30 +29,32 @@ export interface TabsOptions extends BaseOptions {
27
29
  /** Visual style of the tab strip. Defaults to `"underline"`. */
28
30
  variant?: "underline" | "pills";
29
31
  /** Aberdeen attr/style string applied to the active panel. */
30
- inner?: Styling;
32
+ contentAttrs?: Attributes;
31
33
  }
32
34
 
33
35
  A.insertGlobalCss({
34
- ".S_tabs": {
36
+ ".s-tabs": {
35
37
  "&": "display:flex flex-direction:column gap:$3",
36
- ".S_tablist": "display:flex gap:$1 align-items:stretch",
37
- ".S_tab":
38
+ ".s-tablist": "display:flex gap:$1 align-items:stretch",
39
+ ".s-tab":
38
40
  "display:inline-flex align-items:center gap:$2 cursor:pointer background:transparent " +
39
- "border:0 fg:$sFgMuted font-weight:600 padding: 0.6em 0.9em; " +
41
+ "border:0 color: $s-fg-muted; font-weight:600 padding: 0.6em 0.9em; " +
40
42
  "transition: color 0.15s, background 0.15s, border-color 0.15s;",
41
- ".S_tab:hover:not(:disabled)": "fg:$sFg",
42
- ".S_tab:disabled": "opacity:0.5 cursor:not-allowed",
43
- ".S_tab:focus-visible": "outline:none box-shadow: 0 0 0 3px $sFocus; r:$sRadius",
44
- // Underline variant.
45
- "&.S_underline .S_tablist": "border-bottom: 1px solid $sBorder;",
46
- "&.S_underline .S_tab": "border-bottom: 2px solid transparent; margin-bottom:-1px",
47
- "&.S_underline .S_tab[aria-selected=true]": "fg:$sFg border-bottom-color:$sPrimary",
48
- // Pills variant.
49
- "&.S_pills .S_tab": "r:$sRadius",
50
- "&.S_pills .S_tab[aria-selected=true]": "fg:$sPrimaryFg background:$sPrimary",
43
+ ".s-tab:hover:not(:disabled)": "color: $s-fg;",
44
+ ".s-tab:disabled": "opacity:0.5 cursor:not-allowed",
45
+ ".s-tab:focus-visible": "outline:none box-shadow: 0 0 0 3px $s-focus; r: $s-radius;",
46
+ // Underline variant — the active marker uses the contextual brand accent.
47
+ "&.s-underline .s-tablist": "border-bottom: 1px solid $s-border;",
48
+ "&.s-underline .s-tab": "border-bottom: 2px solid transparent; margin-bottom:-1px",
49
+ "&.s-underline .s-tab[aria-selected=true]": "color: $s-fg; border-bottom-color: $s-accent;",
50
+ // Pills variant — the active pill gets the `.s-s.primary` surface (added in
51
+ // tabs()), so its `--s-bg`/`--s-fg` are the brand surface's; we just paint
52
+ // (the `.s-tab` rule's transparent background would otherwise win on it).
53
+ "&.s-pills .s-tab": "r: $s-radius;",
54
+ "&.s-pills .s-tab[aria-selected=true]": "background: $s-bg; color: $s-fg;",
51
55
  // The panel has no enclosing box, so no default padding — its content
52
56
  // aligns flush with the tab strip. Callers add padding/flex via `inner`.
53
- ".S_tabpanel": "display:block",
57
+ ".s-tabpanel": "display:block",
54
58
  },
55
59
  });
56
60
 
@@ -81,16 +85,19 @@ export function tabs(opts: TabsOptions): void {
81
85
  $sel.value = keyOf(tab, index);
82
86
  };
83
87
 
84
- A(`div.S_tabs.S_${variant}`, opts.root, () => {
85
- A("div.S_tablist role=tablist", () => {
88
+ A(`div.s-tabs.s-${variant}`, opts.attrs, () => {
89
+ A("div.s-tablist role=tablist", () => {
86
90
  opts.tabs.forEach((tab, index) => {
87
91
  const key = keyOf(tab, index);
88
- A("button.S_tab type=button role=tab", () => {
92
+ A("button.s-tab type=button role=tab", () => {
89
93
  A(`id=${groupId}-tab-${key} aria-controls=${groupId}-panel-${key}`);
90
94
  A(() => {
91
95
  const selected = $sel.value === key;
92
96
  A("aria-selected=", selected ? "true" : "false");
93
97
  A("tabindex=", selected ? "0" : "-1");
98
+ // The active pill is a filled brand surface; the classes flip
99
+ // with selection (Aberdeen removes them when this scope re-runs).
100
+ if (selected && variant === "pills") A(".s-s.primary");
94
101
  });
95
102
  if (tab.disabled) A("disabled=true");
96
103
  A("click=", () => select(tab, index));
@@ -101,7 +108,7 @@ export function tabs(opts: TabsOptions): void {
101
108
  });
102
109
  });
103
110
 
104
- A("div.S_tabpanel role=tabpanel", opts.inner, () => {
111
+ A("div.s-tabpanel role=tabpanel", opts.contentAttrs, () => {
105
112
  A(() => {
106
113
  const selKey = $sel.value;
107
114
  const index = opts.tabs.findIndex((t, i) => keyOf(t, i) === selKey);
@@ -23,8 +23,8 @@ export interface TextareaOptions extends FieldOptions {
23
23
  }
24
24
 
25
25
  A.insertGlobalCss({
26
- "textarea.S_input": "resize:vertical min-height:3em line-height:1.45",
27
- "textarea.S_input.S_autoGrow": "resize:none min-height:2.5em overflow-y:hidden",
26
+ "textarea.s-input": "resize:vertical min-height:3em line-height:1.45",
27
+ "textarea.s-input.s-autoGrow": "resize:none min-height:2.5em overflow-y:hidden",
28
28
  });
29
29
 
30
30
  /**
@@ -40,9 +40,9 @@ export function textarea(opts: TextareaOptions = {}): void {
40
40
  const grow = opts.autoGrow !== false;
41
41
 
42
42
  drawField(opts, (id, isInvalid) => {
43
- const el = A("textarea.S_input", opts.control, () => {
43
+ const el = A("textarea.s-input", opts.inputAttrs, () => {
44
44
  if (grow) {
45
- A(".S_autoGrow");
45
+ A(".s-autoGrow");
46
46
  A("input=", (e: Event) => {
47
47
  fitToContent(e.currentTarget as HTMLTextAreaElement);
48
48
  if (opts.input) opts.input(e);
@@ -53,7 +53,7 @@ export interface TextlineOptions extends FieldOptions {
53
53
  */
54
54
  export function textline(opts: TextlineOptions = {}): void {
55
55
  drawField(opts, (id, isInvalid) => {
56
- A("input.S_input", opts.control, () => {
56
+ A("input.s-input", opts.inputAttrs, () => {
57
57
  A("type=", opts.type ?? "text");
58
58
  if (opts.placeholder != null) A("placeholder=", opts.placeholder);
59
59
  if (opts.autocomplete != null) A("autocomplete=", opts.autocomplete);
package/src/core.ts CHANGED
@@ -11,16 +11,20 @@ import A from "aberdeen";
11
11
 
12
12
  /**
13
13
  * An Aberdeen attribute/style/class string, e.g. `"display:flex gap:$3 .my-class"`.
14
+ *
15
+ * Common values are our surface modifier classes:
16
+ * - for colors: `.panel` `.raised` `.neutral` `.primary` `.danger` `.success` and `.warning`
17
+ * - for variant: `.filled` `.tonal` and `.outlined`
14
18
  *
15
19
  * These strings are passed straight through to {@link A} as positional
16
20
  * arguments, so they accept the full Aberdeen shorthand syntax: CSS shortcuts
17
21
  * (`p`, `mt`, `bg`, `r`, ...), spacing variables (`$1`..`$12`), CSS custom
18
- * properties (`$sPrimary`), classes (`.foo`) and attributes (`aria-label=Hi`).
22
+ * properties (`$s-primary`), classes (`.foo`) and attributes (`aria-label=Hi`).
19
23
  *
20
24
  * Note: because Aberdeen interprets a leading bare word as an element name, write
21
25
  * `display:flex` rather than just `flex`.
22
26
  */
23
- export type Styling = string;
27
+ export type Attributes = string;
24
28
 
25
29
  /** A reactive "value box", such as the result of `A.proxy(x)` or `A.ref(obj, key)`. */
26
30
  export type Bindable<T> = { value: T };
@@ -29,46 +33,27 @@ export type Bindable<T> = { value: T };
29
33
  export type Content = () => void;
30
34
 
31
35
  /**
32
- * Something that renders a small piece of content: either a plain string (drawn
33
- * as a text node) or a draw function (for icons, badges, custom markup, ...).
34
- */
35
- export type Slot = string | Content;
36
-
37
- /**
38
- * Options shared by *every* Staffa component.
36
+ * Something that renders a small piece of content: either a plain string or a
37
+ * draw function (for icons, badges, custom markup, ...).
39
38
  *
40
- * The {@link BaseOptions.root | root} string is applied to the outermost element
41
- * of the widget, letting callers tweak layout, spacing or add classes without
42
- * forking the component.
39
+ * A string is drawn as **rich text** (see {@link drawSlot}): Aberdeen's `rich`
40
+ * markup is applied, so `*italic*`, `**bold**`, `` `code` `` and
41
+ * `[links](/path)` render as inline elements (text is safely escaped).
42
+ *
43
+ * The optional `Args` type parameter lets a slot's draw-function receive
44
+ * arguments — e.g. a dialog body is a `Slot<[close: () => void]>`.
43
45
  */
44
- export interface BaseOptions {
45
- /**
46
- * Aberdeen attr/style string applied to the widget's root element.
47
- *
48
- * It is passed as a positional argument to {@link A}, so a *change* to it on a
49
- * proxied options object re-runs the caller's scope (recreating the widget).
50
- * That's fine for `root` — it rarely changes at runtime.
51
- */
52
- root?: Styling;
53
- }
46
+ export type Slot<Args extends unknown[] = []> = string | ((...args: Args) => void);
54
47
 
55
48
  /**
56
- * Options for components that wrap a single block of caller-provided content.
57
- *
58
- * Such components render an *inner* element (the one that actually holds the
59
- * children) which is given sensible default padding and `gap` in CSS. Override
60
- * those via {@link ContentOptions.inner | inner}, whose declarations win because
61
- * they're applied as inline styles.
49
+ * Options for components that wrap a single block of caller-provided content,
50
+ * with an `attrs` escape hatch on the outermost element.
62
51
  */
63
- export interface ContentOptions extends BaseOptions {
52
+ export interface ContentOptions {
53
+ /** Aberdeen attr/style string applied to the widget's outermost element. */
54
+ attrs?: Attributes;
64
55
  /** Draws the children of this component. */
65
56
  content?: Content;
66
- /**
67
- * Aberdeen attr/style string applied to the inner (content-holding) element.
68
- * Add `display:flex` here if you want the children laid out as a flex
69
- * row/column.
70
- */
71
- inner?: Styling;
72
57
  }
73
58
 
74
59
  let idCounter = 0;
@@ -78,11 +63,12 @@ export function uniqueId(prefix = "s"): string {
78
63
  }
79
64
 
80
65
  /**
81
- * Draw a {@link Slot} into the current element: call it if it's a function,
82
- * otherwise emit it as a text node.
66
+ * Draw a {@link Slot} into the current element: call it (with any extra `args`)
67
+ * if it's a function, otherwise emit the string as **rich text** via Aberdeen's
68
+ * `rich` markup (`*italic*`, `**bold**`, `` `code` ``, `[link](/path)`).
83
69
  */
84
- export function drawSlot(slot: Slot | undefined): void {
70
+ export function drawSlot<Args extends unknown[] = []>(slot: Slot<Args> | undefined, ...args: Args): void {
85
71
  if (slot == null) return;
86
- if (typeof slot === "function") slot();
87
- else A("#", slot);
72
+ if (typeof slot === "function") slot(...args);
73
+ else A("rich=", slot);
88
74
  }
package/src/index.ts CHANGED
@@ -30,12 +30,14 @@
30
30
  * mutate it. See `AGENTS.md` for the design philosophy.
31
31
  */
32
32
  // Importing the theme module installs spacing vars, the reactive theme and the
33
- // base stylesheet. Customise by mutating S.darkTheme / S.lightTheme.
34
- import { darkTheme, lightTheme, setDarkMode, getDarkMode } from "./theme.js";
33
+ // base stylesheet. Customise it from your app with A.insertGlobalCss (see
34
+ // theme.ts); toggle modes with setDarkMode / getDarkMode.
35
+ import { setDarkMode, getDarkMode } from "./theme.js";
35
36
 
36
37
  import { autocomplete } from "./components/autocomplete.js";
37
38
  import { box } from "./components/box.js";
38
39
  import { button } from "./components/button.js";
40
+ import { buttonChooser } from "./components/buttonChooser.js";
39
41
  import { buttonGroup } from "./components/buttonGroup.js";
40
42
  import { checkbox } from "./components/checkbox.js";
41
43
  import { form } from "./components/form.js";
@@ -60,11 +62,10 @@ export const S = {
60
62
  checkbox,
61
63
  tabs,
62
64
  button,
65
+ buttonChooser,
63
66
  buttonGroup,
64
67
  autocomplete,
65
68
  select,
66
- darkTheme,
67
- lightTheme,
68
69
  setDarkMode,
69
70
  getDarkMode,
70
71
  };
@@ -72,14 +73,14 @@ export const S = {
72
73
  export default S;
73
74
 
74
75
  // Re-export theming and shared types for advanced use.
75
- export { type Theme, darkTheme, lightTheme, setDarkMode, getDarkMode } from "./theme.js";
76
+ export { setDarkMode, getDarkMode } from "./theme.js";
77
+ export type { SurfaceRole, Variant } from "./theme.js";
76
78
  export type {
77
- BaseOptions,
78
79
  ContentOptions,
79
80
  Bindable,
80
81
  Content,
81
82
  Slot,
82
- Styling,
83
+ Attributes as Styling,
83
84
  } from "./core.js";
84
85
  export { drawSlot, uniqueId } from "./core.js";
85
86
 
@@ -91,7 +92,8 @@ export type { TextlineOptions, TextlineType } from "./components/textline.js";
91
92
  export type { TextareaOptions } from "./components/textarea.js";
92
93
  export type { CheckboxOptions } from "./components/checkbox.js";
93
94
  export type { Tab, TabsOptions } from "./components/tabs.js";
94
- export type { ButtonOptions, ButtonVariant, ButtonColor } from "./components/button.js";
95
+ export type { ButtonOptions } from "./components/button.js";
96
+ export type { ButtonChooserOptions } from "./components/buttonChooser.js";
95
97
  export type { ButtonGroupOptions } from "./components/buttonGroup.js";
96
98
  export type { AutocompleteOptions, AutocompleteOptionInput } from "./components/autocomplete.js";
97
99
  export type { SelectOptions, SelectOptionInput } from "./components/select.js";