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,33 +1,8 @@
1
1
  import A from "aberdeen";
2
- import { type BaseOptions, type Content, type Slot, type Styling, drawSlot } from "../core.js";
3
-
4
- /**
5
- * Visual weight of a button.
6
- * - `filled`: solid background, highest emphasis.
7
- * - `tonal`: soft tinted background, medium emphasis.
8
- * - `outlined`: bordered, transparent background, lowest emphasis.
9
- *
10
- * Every variant carries at least a visible border, per Staffa's "everything is
11
- * legible at a glance" principle.
12
- */
13
- export type ButtonVariant = "filled" | "tonal" | "outlined";
14
-
15
- /**
16
- * Color of a button.
17
- *
18
- * The four named **semantic roles** map to theme colours and are offered as
19
- * autocomplete suggestions. You may also pass *any* CSS colour the browser
20
- * understands and it becomes the button's accent directly: a literal like
21
- * `"#ef6b00"` / `"rgb(255 107 0)"`, or a theme custom-property reference like
22
- * `"$sWarning"` (Aberdeen's `$name` shorthand for `var(--name)`).
23
- *
24
- * The `(string & {})` member is what keeps the literal suggestions visible while
25
- * still allowing arbitrary strings — TypeScript only widens to `string` lazily.
26
- */
27
- export type ButtonColor = "primary" | "neutral" | "danger" | "success" | (string & {});
2
+ import { type Content, type Slot, type Attributes, drawSlot } from "../core.js";
28
3
 
29
4
  /** Options for {@link button}. */
30
- export interface ButtonOptions extends BaseOptions {
5
+ export interface ButtonOptions {
31
6
  /** Button label text. */
32
7
  text?: string;
33
8
  /** Custom content (overrides {@link ButtonOptions.text | text}). */
@@ -36,12 +11,6 @@ export interface ButtonOptions extends BaseOptions {
36
11
  icon?: Slot;
37
12
  /** Click handler. */
38
13
  click?: (event: Event) => void;
39
- /** Visual weight. Defaults to `"filled"`. */
40
- variant?: ButtonVariant;
41
- /** Color role. Defaults to `"primary"`. */
42
- color?: ButtonColor;
43
- /** Size. Defaults to `"md"`. */
44
- size?: "sm" | "md" | "lg";
45
14
  /** Disables the button. */
46
15
  disabled?: boolean;
47
16
  /** Native button behaviour. Defaults to `"button"`. */
@@ -50,50 +19,76 @@ export interface ButtonOptions extends BaseOptions {
50
19
  href?: string;
51
20
  /** Accessible label, when the button has only an icon. */
52
21
  ariaLabel?: string;
53
- /** Aberdeen attr/style string applied to the button element. */
54
- inner?: Styling;
22
+ /**
23
+ * Aberdeen attr/style string applied to the button. A button is a surface, so
24
+ * pass surface modifier classes here to restyle it, e.g. `".danger"`,
25
+ * `".neutral .outlined"`. Defaults to a filled `.primary` surface.
26
+ *
27
+ * Size is set here too, with `.small` or `.large` (medium is the default and
28
+ * needs no class), e.g. `".danger .small"`. A `.small`/`.large` parent (such
29
+ * as a {@link buttonGroup}) also sizes its buttons, so you can set it once.
30
+ */
31
+ attrs?: Attributes;
55
32
  }
56
33
 
57
- // The color role sets a local `--c` (and `--cfg` for text on filled); the
58
- // variant rules consume them, so we avoid writing colour×variant rules.
34
+ // The button is a `.s-s` surface (defaulting to `.primary` in button() below), so
35
+ // its colours come from the surface classes in theme.ts. This rule only handles
36
+ // layout, border, focus, hover and sizing.
59
37
  A.insertGlobalCss({
60
- ".S_btn": {
38
+ ".s-btn": {
61
39
  "&":
62
- "--c:$sPrimary --cfg:$sPrimaryFg " +
63
40
  "display:inline-flex align-items:center justify-content:center gap:$2 " +
64
41
  "font-weight:600 line-height:1.2 white-space:nowrap cursor:pointer text-decoration:none " +
65
- "border: 1px solid transparent; r:$sRadius padding: 0.5em 1em; " +
66
- "transition: background 0.15s, border-color 0.15s, filter 0.15s, box-shadow 0.15s;",
67
- "&:focus-visible": "outline:none box-shadow: 0 0 0 3px $sFocus;",
42
+ "border: 1px solid $s-border; r: $s-radius; padding: 0.5em 1em; " +
43
+ "transition: background 0.15s, border-color 0.15s, color 0.15s, filter 0.15s, box-shadow 0.15s, transform 0.08s;",
44
+ "&:focus-visible": "outline:none box-shadow: 0 0 0 3px $s-focus;",
68
45
  "&:disabled, &[aria-disabled=true]": "opacity:0.45 cursor:not-allowed pointer-events:none filter:saturate(0.6)",
69
- // Colour roles.
70
- "&.S_neutral": "--c:$sBorderStrong --cfg:$sFg",
71
- "&.S_danger": "--c:$sDanger --cfg:#fff",
72
- "&.S_success": "--c:$sSuccess --cfg:#08110d",
73
- // Variants.
74
- "&.S_filled": "background:$c color:$cfg border-color:$c",
75
- "&.S_filled:hover": "filter:brightness(1.1)",
76
- "&.S_tonal": "color:$c background: color-mix(in srgb, $c 20%, transparent); border-color: color-mix(in srgb, $c 30%, transparent);",
77
- "&.S_tonal:hover": "background: color-mix(in srgb, $c 30%, transparent);",
78
- "&.S_outlined": "color:$c background:transparent border-color: color-mix(in srgb, $c 55%, $sBorder);",
79
- "&.S_outlined:hover": "background: color-mix(in srgb, $c 12%, transparent);",
80
- // Sizes.
81
- "&.S_sm": "padding: 0.32em 0.7em; font-size:0.85em",
82
- "&.S_lg": "padding: 0.66em 1.3em; font-size:1.1em",
46
+ // Every button lifts a little toward the cursor on hover (the transform is in
47
+ // the transition list above). The filled `.gradient` CTA below layers a deeper
48
+ // shadow on top of the same lift, so it still reads as the signature action.
49
+ "&:hover": "filter: brightness(1.08); transform: translateY(-1px)",
50
+ "&.tonal:hover, &.outlined:hover": "background: color-mix(in srgb, $s-b 26%, transparent);",
51
+ // A filled `.gradient` button (the default) is the app's signature call to
52
+ // action: a borderless gradient with a soft glow that lifts on hover. The
53
+ // gradient fill itself comes from the `.s-s.gradient` surface rule in theme.ts.
54
+ // No border: a filled gradient reads as one solid shape. Dropping the border
55
+ // (rather than making it transparent) also sidesteps a Chromium artifact where
56
+ // a gradient clipped to a transparent rounded border fringes the edge with the
57
+ // gradient's far colour.
58
+ "&.gradient:not(.tonal):not(.outlined)": "border:0 box-shadow: $s-glow;",
59
+ "&.gradient:not(.tonal):not(.outlined):hover":
60
+ "filter: brightness(1.06); box-shadow: 0 10px 28px color-mix(in srgb, $s-primary 42%, transparent); transform: translateY(-1px);",
61
+ // Subtle press feedback.
62
+ "&:active:not(:disabled):not([aria-disabled=true])": "transform: translateY(1px)",
63
+ // Size: set on the button itself, or inherited from a `.small`/`.large`
64
+ // parent (e.g. a buttonGroup), so a container can size all its buttons at once.
65
+ "&.small, .small > &": "padding: 0.32em 0.7em; font-size:0.85em",
66
+ "&.large, .large > &": "padding: 0.66em 1.3em; font-size:1.1em",
83
67
  },
84
68
  });
85
69
 
70
+ // Surface-role classes a caller may pass in `attrs`. When one is present we skip
71
+ // the default `.gradient` base so the two roles don't stack on one element.
72
+ const ROLE_CLASS = /\.(gradient|primary|secondary|neutral|danger|success|warning|base|panel|raised)(\.|\s|$)/;
73
+
86
74
  /**
87
75
  * A button. Always carries at least a visible border so its affordance is
88
- * obvious at a glance, regardless of {@link ButtonVariant | variant}.
76
+ * obvious at a glance.
89
77
  *
90
78
  * Shortcut: pass a string to use it as the label, or a function for custom
91
79
  * content.
92
80
  *
81
+ * **Tip:** pair `href` with Aberdeen's `interceptLinks()` (called once at app
82
+ * startup) for SPA-style navigation without manual click handlers:
83
+ * ```ts
84
+ * interceptLinks(); // once at root
85
+ * S.button({ href: "/dashboard", text: "Dashboard" }); // navigates via router
86
+ * ```
87
+ *
93
88
  * @example
94
89
  * ```ts
95
90
  * S.button({ text: "Save", click: save });
96
- * S.button({ text: "Delete", color: "danger", variant: "outlined", click: del });
91
+ * S.button({ text: "Delete", attrs: ".danger .outlined", click: del });
97
92
  * S.button("Cancel"); // shorthand for { text: "Cancel" }
98
93
  * S.button({ href: "/docs", text: "Docs" }); // renders an <a role=button>
99
94
  * ```
@@ -102,17 +97,13 @@ export function button(opts: ButtonOptions | string | Content = {}): void {
102
97
  const o: ButtonOptions = typeof opts === "string" ? { text: opts } : typeof opts === "function" ? { content: opts } : opts;
103
98
 
104
99
  const tag = o.href != null ? "a" : "button";
105
- const variant = o.variant ?? "filled";
106
- const color = o.color ?? "primary";
107
- const size = o.size === "sm" || o.size === "lg" ? `.S_${o.size}` : "";
108
-
109
- // Semantic roles select a colour class (the CSS sets `--c`/`--cfg`); any other
110
- // value is a raw CSS colour we assign to `--c`, which the variant rules consume
111
- // via `var(--c)`. (`primary` is the base default — its class is a no-op.)
112
- const semantic = color === "primary" || color === "neutral" || color === "danger" || color === "success";
113
- const colorCls = semantic ? `.S_${color}` : "";
114
100
 
115
- const el = A(`${tag}.S_btn.S_${variant}${colorCls}${size}`, o.root, o.inner, () => {
101
+ // A filled `.gradient` surface by default — the signature CTA. If the caller's
102
+ // `attrs` already names a surface role we omit the default, so `.danger`,
103
+ // `.neutral .outlined`, etc. fully take over (rather than stacking two roles).
104
+ // A bare variant/size (`.outlined`, `.small`) keeps the gradient base.
105
+ const role = o.attrs && ROLE_CLASS.test(o.attrs) ? "" : ".gradient";
106
+ A(`${tag}.s-btn.s-s${role}`, o.attrs, () => {
116
107
  if (o.href != null) {
117
108
  A(`href=${o.href} role=button`);
118
109
  if (o.disabled) A("aria-disabled=true");
@@ -127,11 +118,4 @@ export function button(opts: ButtonOptions | string | Content = {}): void {
127
118
  if (o.content) o.content();
128
119
  else if (o.text != null) A("#", o.text);
129
120
  });
130
-
131
- // Aberdeen's inline styler doesn't set CSS custom properties, so assign the
132
- // custom accent on the element directly. A leading `$` is Aberdeen's shorthand
133
- // for a CSS variable reference, so expand it to `var(--name)`.
134
- if (!semantic && el instanceof HTMLElement) {
135
- el.style.setProperty("--c", color.startsWith("$") ? `var(--${color.slice(1)})` : color);
136
- }
137
121
  }
@@ -0,0 +1,65 @@
1
+ import A from "aberdeen";
2
+ import { type Bindable, type Attributes, type Slot } from "../core.js";
3
+ import { buttonGroup } from "./buttonGroup.js";
4
+
5
+ /** Options for {@link buttonChooser}. */
6
+ export interface ButtonChooserOptions {
7
+ /** Aberdeen attr/style string applied to the button group. */
8
+ attrs?: Attributes;
9
+ /**
10
+ * The options to display, as a plain object mapping id → display label.
11
+ * Buttons appear in insertion order. A label may be a plain (rich-text)
12
+ * string, or a draw-function for custom content such as an icon.
13
+ */
14
+ options: Record<string, Slot>;
15
+ /**
16
+ * Two-way binding for the selected id, or `null` when nothing is selected.
17
+ * Use an `A.proxy` or `A.ref`.
18
+ */
19
+ bind: Bindable<string | null>;
20
+ /**
21
+ * When `true`, clicking the already-selected button deselects it, setting
22
+ * `bind.value` to `null`. Useful for "none / auto" states.
23
+ */
24
+ allowDeselect?: boolean;
25
+ /** Name attribute for the hidden `<input>`, enabling form submission. */
26
+ name?: string;
27
+ }
28
+
29
+ /**
30
+ * A single-selection segmented control: an attached button group where exactly
31
+ * one button is active at a time. Optionally allows deselecting back to `null`.
32
+ *
33
+ * Renders a hidden `<input>` alongside (when `name` is set) so the selected
34
+ * value is included in native form submission.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const $view = A.proxy({ value: "day" as string | null });
39
+ * S.buttonChooser({
40
+ * options: { day: "Day", week: "Week", month: "Month" },
41
+ * bind: $view,
42
+ * });
43
+ * ```
44
+ */
45
+ export function buttonChooser(opts: ButtonChooserOptions): void {
46
+ A(() => {
47
+ const selected = opts.bind.value;
48
+ buttonGroup({
49
+ attrs: opts.attrs,
50
+ buttons: Object.entries(opts.options).map(([id, label]) => ({
51
+ text: typeof label === "string" ? label : undefined,
52
+ content: typeof label === "function" ? label : undefined,
53
+ attrs: selected === id ? ".primary" : ".neutral .outlined",
54
+ click: () => {
55
+ opts.bind.value = (opts.allowDeselect && selected === id) ? null : id;
56
+ },
57
+ })),
58
+ });
59
+ });
60
+
61
+ if (opts.name) {
62
+ // Hidden input carries the value into native form submission.
63
+ A(() => A(`input type=hidden name=${opts.name} value=`, opts.bind.value ?? ""));
64
+ }
65
+ }
@@ -19,23 +19,20 @@ export interface ButtonGroupOptions extends ContentOptions {
19
19
  }
20
20
 
21
21
  A.insertGlobalCss({
22
- ".S_bgroup": {
22
+ ".s-bgroup": {
23
23
  "&": "display:inline-flex align-items:stretch",
24
- "&.S_spaced": "gap:$2 flex-wrap:wrap",
25
- "&.S_vertical": "flex-direction:column",
26
- "&.S_attached": "gap:0",
27
- // When attached, collapse the shared border and square off the touching
28
- // corners, keeping only the outer ends of the group rounded.
29
- "&.S_attached:not(.S_vertical) > .S_btn:not(:first-child)": "margin-left:-1px",
30
- "&.S_attached:not(.S_vertical) > .S_btn:not(:first-child):not(:last-child)": "r:0",
31
- "&.S_attached:not(.S_vertical) > .S_btn:first-child:not(:last-child)": "border-top-right-radius:0 border-bottom-right-radius:0",
32
- "&.S_attached:not(.S_vertical) > .S_btn:last-child:not(:first-child)": "border-top-left-radius:0 border-bottom-left-radius:0",
33
- "&.S_attached.S_vertical > .S_btn:not(:first-child)": "margin-top:-1px",
34
- "&.S_attached.S_vertical > .S_btn:not(:first-child):not(:last-child)": "r:0",
35
- "&.S_attached.S_vertical > .S_btn:first-child:not(:last-child)": "border-bottom-left-radius:0 border-bottom-right-radius:0",
36
- "&.S_attached.S_vertical > .S_btn:last-child:not(:first-child)": "border-top-left-radius:0 border-top-right-radius:0",
37
- // Keep the hovered/focused button's border above its neighbours.
38
- "&.S_attached > .S_btn:hover, &.S_attached > .S_btn:focus-visible": "z-index:1",
24
+ "&.s-spaced": "gap:$2 flex-wrap:wrap",
25
+ "&.s-vertical": "flex-direction:column",
26
+ "&.s-attached": "gap:0",
27
+ "&.s-attached:not(.s-vertical) > .s-btn:not(:first-child)": "margin-left:-1px",
28
+ "&.s-attached:not(.s-vertical) > .s-btn:not(:first-child):not(:last-child)": "r:0",
29
+ "&.s-attached:not(.s-vertical) > .s-btn:first-child:not(:last-child)": "border-top-right-radius:0 border-bottom-right-radius:0",
30
+ "&.s-attached:not(.s-vertical) > .s-btn:last-child:not(:first-child)": "border-top-left-radius:0 border-bottom-left-radius:0",
31
+ "&.s-attached.s-vertical > .s-btn:not(:first-child)": "margin-top:-1px",
32
+ "&.s-attached.s-vertical > .s-btn:not(:first-child):not(:last-child)": "r:0",
33
+ "&.s-attached.s-vertical > .s-btn:first-child:not(:last-child)": "border-bottom-left-radius:0 border-bottom-right-radius:0",
34
+ "&.s-attached.s-vertical > .s-btn:last-child:not(:first-child)": "border-top-left-radius:0 border-top-right-radius:0",
35
+ "&.s-attached > .s-btn:hover, &.s-attached > .s-btn:focus-visible": "z-index:1",
39
36
  },
40
37
  });
41
38
 
@@ -46,17 +43,17 @@ A.insertGlobalCss({
46
43
  * @example
47
44
  * ```ts
48
45
  * S.buttonGroup({ buttons: [
49
- * { text: "Day", variant: "outlined", color: "neutral" },
50
- * { text: "Week", variant: "outlined", color: "neutral" },
51
- * { text: "Month", variant: "outlined", color: "neutral" },
46
+ * { text: "Day", attrs: ".neutral .outlined" },
47
+ * { text: "Week", attrs: ".neutral .outlined" },
48
+ * { text: "Month", attrs: ".neutral .outlined" },
52
49
  * ]});
53
50
  * ```
54
51
  */
55
52
  export function buttonGroup(opts: ButtonGroupOptions = {}): void {
56
53
  const layout = opts.layout ?? "attached";
57
- const cls = `.S_${layout}${opts.vertical ? ".S_vertical" : ""}`;
54
+ const cls = `.s-${layout}${opts.vertical ? ".s-vertical" : ""}`;
58
55
 
59
- A(`div.S_bgroup${cls} role=group`, opts.root, opts.inner, () => {
56
+ A(`div.s-bgroup${cls} role=group`, opts.attrs, () => {
60
57
  if (opts.buttons) for (const b of opts.buttons) button(b);
61
58
  if (opts.content) opts.content();
62
59
  });
@@ -15,12 +15,12 @@ export interface CheckboxOptions extends Omit<FieldOptions, "label"> {
15
15
  }
16
16
 
17
17
  A.insertGlobalCss({
18
- ".S_check": {
18
+ ".s-check": {
19
19
  "&": "display:flex flex-direction:column gap:$1",
20
20
  "> label": "display:flex align-items:center gap:$2 cursor:pointer user-select:none",
21
21
  "> label:has(input:disabled)": "cursor:not-allowed opacity:0.6",
22
22
  // Native control styled with accent-color: accessible and zero-fuss.
23
- "input": "width:1.15em height:1.15em accent-color:$sPrimary cursor:inherit m:0",
23
+ "input": "width:1.15em height:1.15em accent-color:$s-accent cursor:inherit m:0",
24
24
  },
25
25
  });
26
26
 
@@ -37,9 +37,9 @@ A.insertGlobalCss({
37
37
  export function checkbox(opts: CheckboxOptions = {}): void {
38
38
  const id = opts.id ?? uniqueId("check");
39
39
 
40
- A("div.S_check", opts.root, () => {
40
+ A("div.s-check", opts.attrs, () => {
41
41
  A(`label for=${id}`, () => {
42
- A("input type=checkbox", opts.control, () => {
42
+ A("input type=checkbox", opts.inputAttrs, () => {
43
43
  A(`id=${id}`);
44
44
  if (opts.name) A(`name=${opts.name}`);
45
45
  // `checked` is a boolean attribute: only set it when actually true.
@@ -56,15 +56,15 @@ export function checkbox(opts: CheckboxOptions = {}): void {
56
56
  // Own scope so the label text/required marker don't recreate the input.
57
57
  A(() => {
58
58
  if (opts.label != null) drawSlot(opts.label);
59
- if (opts.required) A("span.S_req aria-hidden=true #*");
59
+ if (opts.required) A("span.s-req aria-hidden=true #*");
60
60
  });
61
61
  });
62
62
 
63
63
  A(() => {
64
- if (opts.help != null && !opts.error) A("div.S_help", () => drawSlot(opts.help));
64
+ if (opts.help != null && !opts.error) A("div.s-help", () => drawSlot(opts.help));
65
65
  });
66
66
  A(() => {
67
- if (opts.error) A("div.S_error role=alert #", opts.error);
67
+ if (opts.error) A("div.s-error role=alert #", opts.error);
68
68
  });
69
69
  });
70
70
  }