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,4 +1,4 @@
1
- import { type BaseOptions, type Bindable, type Content, type Slot, type Styling } from "../core.js";
1
+ import { type Bindable, type Content, type Slot, type Attributes } from "../core.js";
2
2
  /** A single tab definition. */
3
3
  export interface Tab {
4
4
  /** Stable id used as the selection value. Falls back to the array index. */
@@ -13,7 +13,9 @@ export interface Tab {
13
13
  disabled?: boolean;
14
14
  }
15
15
  /** Options for {@link tabs}. */
16
- export interface TabsOptions extends BaseOptions {
16
+ export interface TabsOptions {
17
+ /** Aberdeen attr/style string applied to the outermost element. */
18
+ attrs?: Attributes;
17
19
  /** The tabs to display. */
18
20
  tabs: Tab[];
19
21
  /**
@@ -21,10 +23,8 @@ export interface TabsOptions extends BaseOptions {
21
23
  * its own internal selection, starting at the first tab.
22
24
  */
23
25
  bind?: Bindable<string>;
24
- /** Visual style of the tab strip. Defaults to `"underline"`. */
25
- variant?: "underline" | "pills";
26
26
  /** Aberdeen attr/style string applied to the active panel. */
27
- inner?: Styling;
27
+ contentAttrs?: Attributes;
28
28
  }
29
29
  /**
30
30
  * A tabbed view. Renders an ARIA `tablist` of buttons and a single live panel
@@ -1,25 +1,19 @@
1
1
  import A from "aberdeen";
2
2
  import { drawSlot, uniqueId } from "../core.js";
3
3
  A.insertGlobalCss({
4
- ".S_tabs": {
4
+ ".s-tabs": {
5
5
  "&": "display:flex flex-direction:column gap:$3",
6
- ".S_tablist": "display:flex gap:$1 align-items:stretch",
7
- ".S_tab": "display:inline-flex align-items:center gap:$2 cursor:pointer background:transparent " +
8
- "border:0 fg:$sFgMuted font-weight:600 padding: 0.6em 0.9em; " +
6
+ ".s-tablist": "display:flex gap:$1 align-items:stretch overflow-x:auto scrollbar-width:none border-bottom: 1px solid $s-border;",
7
+ ".s-tablist::-webkit-scrollbar": "display:none",
8
+ ".s-tab": "display:inline-flex align-items:center gap:$2 cursor:pointer background:transparent " +
9
+ "border:0 color: $s-fg-muted; font-weight:600 padding: 0.6em 0.9em; " +
10
+ "border-bottom: 3px solid transparent; margin-bottom:-1px " +
9
11
  "transition: color 0.15s, background 0.15s, border-color 0.15s;",
10
- ".S_tab:hover:not(:disabled)": "fg:$sFg",
11
- ".S_tab:disabled": "opacity:0.5 cursor:not-allowed",
12
- ".S_tab:focus-visible": "outline:none box-shadow: 0 0 0 3px $sFocus; r:$sRadius",
13
- // Underline variant.
14
- "&.S_underline .S_tablist": "border-bottom: 1px solid $sBorder;",
15
- "&.S_underline .S_tab": "border-bottom: 2px solid transparent; margin-bottom:-1px",
16
- "&.S_underline .S_tab[aria-selected=true]": "fg:$sFg border-bottom-color:$sPrimary",
17
- // Pills variant.
18
- "&.S_pills .S_tab": "r:$sRadius",
19
- "&.S_pills .S_tab[aria-selected=true]": "fg:$sPrimaryFg background:$sPrimary",
20
- // The panel has no enclosing box, so no default padding — its content
21
- // aligns flush with the tab strip. Callers add padding/flex via `inner`.
22
- ".S_tabpanel": "display:block",
12
+ ".s-tab:hover:not(:disabled), .s-tab[aria-selected=true]": "color: $s-fg;",
13
+ ".s-tab:disabled": "opacity:0.5 cursor:not-allowed",
14
+ ".s-tab:focus-visible": "outline:none box-shadow: 0 0 0 3px $s-focus; r: $s-radius;",
15
+ ".s-tab[aria-selected=true]": "border-image: $s-gradient 1;",
16
+ ".s-tabpanel": "display:block",
23
17
  },
24
18
  });
25
19
  /**
@@ -35,7 +29,6 @@ A.insertGlobalCss({
35
29
  * ```
36
30
  */
37
31
  export function tabs(opts) {
38
- const variant = opts.variant ?? "underline";
39
32
  const groupId = uniqueId("tabs");
40
33
  // Resolve a tab's selection key (its id, or its index as a string).
41
34
  const keyOf = (tab, index) => tab.id ?? String(index);
@@ -46,11 +39,11 @@ export function tabs(opts) {
46
39
  return;
47
40
  $sel.value = keyOf(tab, index);
48
41
  };
49
- A(`div.S_tabs.S_${variant}`, opts.root, () => {
50
- A("div.S_tablist role=tablist", () => {
42
+ A("div.s-tabs", opts.attrs, () => {
43
+ A("div.s-tablist role=tablist", () => {
51
44
  opts.tabs.forEach((tab, index) => {
52
45
  const key = keyOf(tab, index);
53
- A("button.S_tab type=button role=tab", () => {
46
+ A("button.s-tab type=button role=tab", () => {
54
47
  A(`id=${groupId}-tab-${key} aria-controls=${groupId}-panel-${key}`);
55
48
  A(() => {
56
49
  const selected = $sel.value === key;
@@ -66,7 +59,7 @@ export function tabs(opts) {
66
59
  });
67
60
  });
68
61
  });
69
- A("div.S_tabpanel role=tabpanel", opts.inner, () => {
62
+ A("div.s-tabpanel role=tabpanel", opts.contentAttrs, () => {
70
63
  A(() => {
71
64
  const selKey = $sel.value;
72
65
  const index = opts.tabs.findIndex((t, i) => keyOf(t, i) === selKey);
@@ -1,8 +1,8 @@
1
1
  import A from "aberdeen";
2
2
  import { applyControlAttrs, drawField } from "./field.js";
3
3
  A.insertGlobalCss({
4
- "textarea.S_input": "resize:vertical min-height:3em line-height:1.45",
5
- "textarea.S_input.S_autoGrow": "resize:none min-height:2.5em overflow-y:hidden",
4
+ "textarea.s-input": "resize:vertical min-height:3em line-height:1.45",
5
+ "textarea.s-input.s-autoGrow": "resize:none min-height:2.5em overflow-y:hidden",
6
6
  });
7
7
  /**
8
8
  * A multi-line text input. Shares the field chrome and styling of
@@ -16,9 +16,9 @@ A.insertGlobalCss({
16
16
  export function textarea(opts = {}) {
17
17
  const grow = opts.autoGrow !== false;
18
18
  drawField(opts, (id, isInvalid) => {
19
- const el = A("textarea.S_input", opts.control, () => {
19
+ const el = A("textarea.s-input", opts.inputAttrs, () => {
20
20
  if (grow) {
21
- A(".S_autoGrow");
21
+ A(".s-autoGrow");
22
22
  A("input=", (e) => {
23
23
  fitToContent(e.currentTarget);
24
24
  if (opts.input)
@@ -14,7 +14,7 @@ import { applyControlAttrs, drawField } from "./field.js";
14
14
  */
15
15
  export function textline(opts = {}) {
16
16
  drawField(opts, (id, isInvalid) => {
17
- A("input.S_input", opts.control, () => {
17
+ A("input.s-input", opts.inputAttrs, () => {
18
18
  A("type=", opts.type ?? "text");
19
19
  if (opts.placeholder != null)
20
20
  A("placeholder=", opts.placeholder);
@@ -0,0 +1,37 @@
1
+ import { type Slot, type Attributes } from "../core.js";
2
+ import type { SurfaceRole } from "../theme.js";
3
+ /** Options for {@link toast}. */
4
+ export interface ToastOptions {
5
+ /** Primary message. A string is rendered as rich text. */
6
+ message: Slot;
7
+ /** Optional bold title above the message. */
8
+ title?: Slot;
9
+ /**
10
+ * Colour role. Defaults to `"neutral"`.
11
+ * Use `"success"` / `"danger"` / `"warning"` for semantic feedback.
12
+ */
13
+ type?: SurfaceRole;
14
+ /**
15
+ * Auto-dismiss delay in milliseconds. Defaults to `4000`.
16
+ * Pass `0` to make the toast persistent until dismissed manually.
17
+ */
18
+ duration?: number;
19
+ /** Show a close button. Defaults to `true`. */
20
+ dismissible?: boolean;
21
+ /** Aberdeen attr/style string applied to the toast element. */
22
+ attrs?: Attributes;
23
+ }
24
+ /**
25
+ * Show a toast notification. Returns a `dismiss()` function to remove it
26
+ * programmatically. Auto-dismisses after `duration` ms (default 4 000).
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * S.toast({ message: "Saved!", type: "success" });
31
+ * S.toast({ title: "Error", message: "Upload failed.", type: "danger", duration: 0 });
32
+ * const off = S.toast({ message: "Uploading…", duration: 0, dismissible: false });
33
+ * // later:
34
+ * off();
35
+ * ```
36
+ */
37
+ export declare function toast(opts: ToastOptions): () => void;
@@ -0,0 +1,79 @@
1
+ import A from "aberdeen";
2
+ import { grow, shrink } from "aberdeen/transitions";
3
+ import { drawSlot } from "../core.js";
4
+ A.insertGlobalCss({
5
+ ".s-toasts": "position:fixed bottom:$3 right:$3 z-index:400 " +
6
+ "display:flex flex-direction:column gap:$2 " +
7
+ "pointer-events:none max-width:min(90vw,24rem) w:24rem",
8
+ ".s-toast": {
9
+ "&": "display:flex align-items:flex-start gap:$2 " +
10
+ "padding: $3; border: 1px solid $s-border; r:$s-radius box-shadow:$s-shadow " +
11
+ "pointer-events:auto",
12
+ ".s-toast-body": "display:flex flex-direction:column gap:$1 flex:1 min-width:0",
13
+ ".s-toast-title": "font-weight:700 line-height:1.3",
14
+ ".s-toast-msg": "font-size:0.9em fg:$s-fg-muted line-height:1.4",
15
+ ".s-toast-close": "cursor:pointer border:0 background:transparent fg:$s-fg-muted font-size:1.1em line-height:1 " +
16
+ "padding: 0 0.15em; r:4px flex-shrink:0 align-self:flex-start",
17
+ ".s-toast-close:hover": "fg:$s-fg",
18
+ ".s-toast-close:focus-visible": "outline:none box-shadow: 0 0 0 3px $s-focus; fg:$s-fg",
19
+ },
20
+ });
21
+ let toastCount = 0;
22
+ // Keyed by stable ID so A.onEach scopes are per-toast — removing one never re-renders others.
23
+ const toasts = A.proxy({});
24
+ A.mount(document.body, () => {
25
+ A("div.s-toasts aria-live=polite aria-atomic=false", () => {
26
+ A.onEach(toasts, (entry) => {
27
+ const { opts } = entry;
28
+ const role = opts.type === "danger" || opts.type === "warning" ? "alert" : "status";
29
+ const surface = opts.type ?? "neutral";
30
+ A(`div.s-toast.s-s.${surface} role=${role}`, "create=", grow, "destroy=", shrink, opts.attrs, () => {
31
+ A("div.s-toast-body", () => {
32
+ A(() => {
33
+ if (opts.title != null)
34
+ A("div.s-toast-title", () => drawSlot(opts.title));
35
+ });
36
+ A("div.s-toast-msg", () => drawSlot(opts.message));
37
+ });
38
+ A(() => {
39
+ if (opts.dismissible === false)
40
+ return;
41
+ A("button.s-toast-close type=button aria-label=Dismiss", () => {
42
+ A("#×");
43
+ A("click=", () => dismiss(entry.id));
44
+ });
45
+ });
46
+ });
47
+ });
48
+ });
49
+ });
50
+ function dismiss(id) {
51
+ delete toasts[id];
52
+ }
53
+ /**
54
+ * Show a toast notification. Returns a `dismiss()` function to remove it
55
+ * programmatically. Auto-dismisses after `duration` ms (default 4 000).
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * S.toast({ message: "Saved!", type: "success" });
60
+ * S.toast({ title: "Error", message: "Upload failed.", type: "danger", duration: 0 });
61
+ * const off = S.toast({ message: "Uploading…", duration: 0, dismissible: false });
62
+ * // later:
63
+ * off();
64
+ * ```
65
+ */
66
+ export function toast(opts) {
67
+ const id = ++toastCount;
68
+ toasts[id] = { id, opts };
69
+ const duration = opts.duration ?? 4000;
70
+ let timer;
71
+ if (duration > 0) {
72
+ timer = setTimeout(() => dismiss(id), duration);
73
+ }
74
+ return () => {
75
+ if (timer != null)
76
+ clearTimeout(timer);
77
+ dismiss(id);
78
+ };
79
+ }
@@ -0,0 +1,32 @@
1
+ import { type Slot, type Attributes } from "../core.js";
2
+ /** Options for {@link addTooltip}. */
3
+ export interface TooltipOptions {
4
+ /** The tooltip text or draw function. A string is rendered as rich text. */
5
+ tip: Slot;
6
+ /**
7
+ * Which side the tooltip appears on. Defaults to `"top"`.
8
+ * Automatically flips to the opposite side when there isn't enough room.
9
+ */
10
+ placement?: "top" | "bottom" | "left" | "right";
11
+ /** Aberdeen attr/style string applied to the tip panel. */
12
+ attrs?: Attributes;
13
+ }
14
+ /**
15
+ * Attaches a tooltip to the current element: adds hover/focus handlers via
16
+ * {@link A} so the tip appears when the element is hovered or keyboard-focused.
17
+ * The tip panel is rendered into `document.body` via a portal, so it is never
18
+ * clipped by `overflow:hidden` ancestors. Position is computed from the
19
+ * element's bounding rect and automatically flips when near the viewport edge.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * A("button #Save", () => {
24
+ * S.addTooltip({ tip: "Saves your work to the cloud" });
25
+ * });
26
+ *
27
+ * A("button #Delete", () => {
28
+ * S.addTooltip({ tip: "Dangerous — cannot be undone", placement: "bottom" });
29
+ * });
30
+ * ```
31
+ */
32
+ export declare function addTooltip(opts: TooltipOptions): void;
@@ -0,0 +1,130 @@
1
+ import A from "aberdeen";
2
+ import { drawSlot } from "../core.js";
3
+ A.insertGlobalCss({
4
+ ".s-tt-tip": {
5
+ "&": "position:fixed z-index:500 " +
6
+ "max-width:20rem w:max-content " +
7
+ "bg:$s-raised fg:$s-fg border: 1px solid $s-border-strong; " +
8
+ "r:$s-radius box-shadow:$s-shadow " +
9
+ "padding: 0.3em 0.65em; font-size:0.85em line-height:1.4 " +
10
+ "pointer-events:none",
11
+ },
12
+ });
13
+ // ─── Global portal state ────────────────────────────────────────────────────
14
+ // At most one tooltip is visible at a time. The anchor is the element the
15
+ // handlers were attached to (its bounding rect drives positioning).
16
+ const $ttActive = A.proxy(undefined);
17
+ let hideTimer = null;
18
+ // Hide tooltip when the page scrolls (anchor has moved).
19
+ if (typeof window !== "undefined") {
20
+ window.addEventListener("scroll", () => { $ttActive.value = undefined; }, { capture: true, passive: true });
21
+ }
22
+ function computePos(rect, tipW, tipH, placement) {
23
+ const gap = 7;
24
+ const vw = window.innerWidth;
25
+ const vh = window.innerHeight;
26
+ let x = 0, y = 0;
27
+ if (placement === "bottom") {
28
+ x = rect.left + (rect.width - tipW) / 2;
29
+ y = rect.bottom + gap;
30
+ if (y + tipH > vh - 8) {
31
+ y = rect.top - tipH - gap;
32
+ }
33
+ }
34
+ else if (placement === "left") {
35
+ x = rect.left - tipW - gap;
36
+ y = rect.top + (rect.height - tipH) / 2;
37
+ if (x < 8) {
38
+ x = rect.right + gap;
39
+ }
40
+ }
41
+ else if (placement === "right") {
42
+ x = rect.right + gap;
43
+ y = rect.top + (rect.height - tipH) / 2;
44
+ if (x + tipW > vw - 8) {
45
+ x = rect.left - tipW - gap;
46
+ }
47
+ }
48
+ else {
49
+ // top (default)
50
+ x = rect.left + (rect.width - tipW) / 2;
51
+ y = rect.top - tipH - gap;
52
+ if (y < 8) {
53
+ y = rect.bottom + gap;
54
+ }
55
+ }
56
+ return {
57
+ x: Math.max(8, Math.min(x, vw - tipW - 8)),
58
+ y: Math.max(8, Math.min(y, vh - tipH - 8)),
59
+ };
60
+ }
61
+ function scheduleHide() {
62
+ if (hideTimer)
63
+ clearTimeout(hideTimer);
64
+ hideTimer = setTimeout(() => {
65
+ $ttActive.value = undefined;
66
+ hideTimer = null;
67
+ }, 100);
68
+ }
69
+ // ─── Portal ──────────────────────────────────────────────────────────────────
70
+ A.mount(document.body, () => {
71
+ const active = $ttActive.value;
72
+ if (!active)
73
+ return;
74
+ const { opts, anchor } = active;
75
+ const placement = opts.placement ?? "top";
76
+ const tipEl = A("div.s-tt-tip role=tooltip visibility:hidden", opts.attrs, () => {
77
+ A("mouseenter=", () => {
78
+ if (hideTimer) {
79
+ clearTimeout(hideTimer);
80
+ hideTimer = null;
81
+ }
82
+ });
83
+ A("mouseleave=", scheduleHide);
84
+ drawSlot(opts.tip);
85
+ });
86
+ requestAnimationFrame(() => {
87
+ if (!document.body.contains(tipEl))
88
+ return;
89
+ const { x, y } = computePos(anchor.getBoundingClientRect(), tipEl.offsetWidth, tipEl.offsetHeight, placement);
90
+ tipEl.style.left = x + "px";
91
+ tipEl.style.top = y + "px";
92
+ tipEl.style.visibility = "";
93
+ });
94
+ });
95
+ // ─── Public component ────────────────────────────────────────────────────────
96
+ /**
97
+ * Attaches a tooltip to the current element: adds hover/focus handlers via
98
+ * {@link A} so the tip appears when the element is hovered or keyboard-focused.
99
+ * The tip panel is rendered into `document.body` via a portal, so it is never
100
+ * clipped by `overflow:hidden` ancestors. Position is computed from the
101
+ * element's bounding rect and automatically flips when near the viewport edge.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * A("button #Save", () => {
106
+ * S.addTooltip({ tip: "Saves your work to the cloud" });
107
+ * });
108
+ *
109
+ * A("button #Delete", () => {
110
+ * S.addTooltip({ tip: "Dangerous — cannot be undone", placement: "bottom" });
111
+ * });
112
+ * ```
113
+ */
114
+ export function addTooltip(opts) {
115
+ const show = (e) => {
116
+ if (hideTimer) {
117
+ clearTimeout(hideTimer);
118
+ hideTimer = null;
119
+ }
120
+ $ttActive.value = { opts, anchor: e.currentTarget };
121
+ };
122
+ A("mouseenter=", show);
123
+ A("mouseleave=", scheduleHide);
124
+ A("focusin=", show);
125
+ A("focusout=", scheduleHide);
126
+ A.clean(() => {
127
+ if ($ttActive.value?.opts === opts)
128
+ $ttActive.value = undefined;
129
+ });
130
+ }
package/dist/core.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Shared building blocks for the Skye component library.
2
+ * Shared building blocks for the Staffa component library.
3
3
  *
4
- * Every component in Skye is "just an Aberdeen draw function": a plain function
4
+ * Every component in Staffa is "just an Aberdeen draw function": a plain function
5
5
  * that takes a single, strongly typed options object and emits DOM through
6
6
  * Aberdeen's {@link A} function. This module defines the option-type hierarchy
7
7
  * that all components build on, plus a couple of tiny helpers.
@@ -9,15 +9,19 @@
9
9
  /**
10
10
  * An Aberdeen attribute/style/class string, e.g. `"display:flex gap:$3 .my-class"`.
11
11
  *
12
+ * Common values are our surface modifier classes:
13
+ * - for colors: `.panel` `.raised` `.neutral` `.primary` `.secondary` `.gradient` `.danger` `.success` and `.warning`
14
+ * - for variant: `.filled` `.tonal` and `.outlined`
15
+ *
12
16
  * These strings are passed straight through to {@link A} as positional
13
17
  * arguments, so they accept the full Aberdeen shorthand syntax: CSS shortcuts
14
18
  * (`p`, `mt`, `bg`, `r`, ...), spacing variables (`$1`..`$12`), CSS custom
15
- * properties (`$sPrimary`), classes (`.foo`) and attributes (`aria-label=Hi`).
19
+ * properties (`$s-primary`), classes (`.foo`) and attributes (`aria-label=Hi`).
16
20
  *
17
21
  * Note: because Aberdeen interprets a leading bare word as an element name, write
18
22
  * `display:flex` rather than just `flex`.
19
23
  */
20
- export type Styling = string;
24
+ export type Attributes = string;
21
25
  /** A reactive "value box", such as the result of `A.proxy(x)` or `A.ref(obj, key)`. */
22
26
  export type Bindable<T> = {
23
27
  value: T;
@@ -25,49 +29,32 @@ export type Bindable<T> = {
25
29
  /** A content function. It runs inside the relevant element's reactive scope. */
26
30
  export type Content = () => void;
27
31
  /**
28
- * Something that renders a small piece of content: either a plain string (drawn
29
- * as a text node) or a draw function (for icons, badges, custom markup, ...).
30
- */
31
- export type Slot = string | Content;
32
- /**
33
- * Options shared by *every* Skye component.
32
+ * Something that renders a small piece of content: either a plain string or a
33
+ * draw function (for icons, badges, custom markup, ...).
34
34
  *
35
- * The {@link BaseOptions.root | root} string is applied to the outermost element
36
- * of the widget, letting callers tweak layout, spacing or add classes without
37
- * forking the component.
35
+ * A string is drawn as **rich text** (see {@link drawSlot}): Aberdeen's `rich`
36
+ * markup is applied, so `*italic*`, `**bold**`, `` `code` `` and
37
+ * `[links](/path)` render as inline elements (text is safely escaped).
38
+ *
39
+ * The optional `Args` type parameter lets a slot's draw-function receive
40
+ * arguments — e.g. a dialog body is a `Slot<[close: () => void]>`.
38
41
  */
39
- export interface BaseOptions {
40
- /**
41
- * Aberdeen attr/style string applied to the widget's root element.
42
- *
43
- * It is passed as a positional argument to {@link A}, so a *change* to it on a
44
- * proxied options object re-runs the caller's scope (recreating the widget).
45
- * That's fine for `root` — it rarely changes at runtime.
46
- */
47
- root?: Styling;
48
- }
42
+ export type Slot<Args extends unknown[] = []> = string | ((...args: Args) => void);
49
43
  /**
50
- * Options for components that wrap a single block of caller-provided content.
51
- *
52
- * Such components render an *inner* element (the one that actually holds the
53
- * children) which is given sensible default padding and `gap` in CSS. Override
54
- * those via {@link ContentOptions.inner | inner}, whose declarations win because
55
- * they're applied as inline styles.
44
+ * Options for components that wrap a single block of caller-provided content,
45
+ * with an `attrs` escape hatch on the outermost element.
56
46
  */
57
- export interface ContentOptions extends BaseOptions {
47
+ export interface ContentOptions {
48
+ /** Aberdeen attr/style string applied to the widget's outermost element. */
49
+ attrs?: Attributes;
58
50
  /** Draws the children of this component. */
59
51
  content?: Content;
60
- /**
61
- * Aberdeen attr/style string applied to the inner (content-holding) element.
62
- * Add `display:flex` here if you want the children laid out as a flex
63
- * row/column.
64
- */
65
- inner?: Styling;
66
52
  }
67
53
  /** Generates a process-unique id, used to wire `<label for>` to its control. */
68
54
  export declare function uniqueId(prefix?: string): string;
69
55
  /**
70
- * Draw a {@link Slot} into the current element: call it if it's a function,
71
- * otherwise emit it as a text node.
56
+ * Draw a {@link Slot} into the current element: call it (with any extra `args`)
57
+ * if it's a function, otherwise emit the string as **rich text** via Aberdeen's
58
+ * `rich` markup (`*italic*`, `**bold**`, `` `code` ``, `[link](/path)`).
72
59
  */
73
- export declare function drawSlot(slot: Slot | undefined): void;
60
+ export declare function drawSlot<Args extends unknown[] = []>(slot: Slot<Args> | undefined, ...args: Args): void;
package/dist/core.js CHANGED
@@ -5,14 +5,15 @@ export function uniqueId(prefix = "s") {
5
5
  return `${prefix}-${++idCounter}`;
6
6
  }
7
7
  /**
8
- * Draw a {@link Slot} into the current element: call it if it's a function,
9
- * otherwise emit it as a text node.
8
+ * Draw a {@link Slot} into the current element: call it (with any extra `args`)
9
+ * if it's a function, otherwise emit the string as **rich text** via Aberdeen's
10
+ * `rich` markup (`*italic*`, `**bold**`, `` `code` ``, `[link](/path)`).
10
11
  */
11
- export function drawSlot(slot) {
12
+ export function drawSlot(slot, ...args) {
12
13
  if (slot == null)
13
14
  return;
14
15
  if (typeof slot === "function")
15
- slot();
16
+ slot(...args);
16
17
  else
17
- A("#", slot);
18
+ A("rich=", slot);
18
19
  }
@@ -0,0 +1,46 @@
1
+ /** Stroke line-cap, as accepted by SVG's `stroke-linecap`. */
2
+ export type IconCap = "butt" | "round" | "square";
3
+ /** Stroke line-join, as accepted by SVG's `stroke-linejoin`. */
4
+ export type IconJoin = "arcs" | "bevel" | "miter" | "miter-clip" | "round";
5
+ /** Per-call overrides for a drawn icon. Anything omitted falls back to the
6
+ * module defaults (see {@link setDefaults}). */
7
+ export interface IconOptions {
8
+ /** Width & height, as a CSS length. A bare number is treated as pixels by
9
+ * SVG. Pass e.g. `"1em"` to scale the icon with the surrounding font. */
10
+ size?: number | string;
11
+ /** Stroke colour. Defaults to `"currentColor"`, so the icon inherits the
12
+ * current text colour. */
13
+ color?: string;
14
+ /** Stroke width in viewBox units (the viewBox is 24×24). */
15
+ strokeWidth?: number;
16
+ /** Stroke line-cap. */
17
+ cap?: IconCap;
18
+ /** Stroke line-join. */
19
+ join?: IconJoin;
20
+ /** Aberdeen attr/style string applied to the `<svg>` element. */
21
+ attrs?: string;
22
+ }
23
+ /** The resolved, always-present defaults backing {@link IconOptions}. */
24
+ export interface IconDefaults {
25
+ size: number | string;
26
+ color: string;
27
+ strokeWidth: number;
28
+ cap: IconCap;
29
+ join: IconJoin;
30
+ }
31
+ /**
32
+ * Override the module-wide icon defaults. Affects every icon drawn afterwards.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { setDefaults } from "staffa/icons";
37
+ * setDefaults({ size: "1.25em", strokeWidth: 1.5 });
38
+ * ```
39
+ */
40
+ export declare function setDefaults(opts: Partial<IconDefaults>): void;
41
+ /**
42
+ * Turn a piece of inner-SVG markup into an icon draw-function. The returned
43
+ * function emits a freshly-built `<svg>` into the current Aberdeen scope,
44
+ * applying the {@link IconOptions} (or the module defaults).
45
+ */
46
+ export declare function mk(inner: string): (opts?: IconOptions) => void;
@@ -0,0 +1,44 @@
1
+ import A from "aberdeen";
2
+ const defaults = {
3
+ size: 24,
4
+ color: "currentColor",
5
+ strokeWidth: 2,
6
+ cap: "round",
7
+ join: "round",
8
+ };
9
+ /**
10
+ * Override the module-wide icon defaults. Affects every icon drawn afterwards.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { setDefaults } from "staffa/icons";
15
+ * setDefaults({ size: "1.25em", strokeWidth: 1.5 });
16
+ * ```
17
+ */
18
+ export function setDefaults(opts) {
19
+ Object.assign(defaults, opts);
20
+ }
21
+ /**
22
+ * Draw a single icon: build one `<svg>` through Aberdeen (applying the
23
+ * {@link IconOptions} or the module defaults) and fill in its inner markup.
24
+ *
25
+ * This is the shared body behind every icon. {@link mk} hands it the icon's
26
+ * `inner` markup, so the per-icon closures stay tiny instead of each carrying
27
+ * a copy of this logic.
28
+ */
29
+ function drawIcon(inner, opts) {
30
+ const size = opts.size ?? defaults.size;
31
+ const el = A('svg.s-icon aria-hidden=true viewBox="0 0 24 24" fill=none', "width=", size, "height=", size, "stroke=", opts.color ?? defaults.color, "stroke-width=", opts.strokeWidth ?? defaults.strokeWidth, "stroke-linecap=", opts.cap ?? defaults.cap, "stroke-linejoin=", opts.join ?? defaults.join, opts.attrs);
32
+ // Drop the primitives in via innerHTML: setting it on the `<svg>` itself
33
+ // makes the parser put the children in the SVG namespace. (Aberdeen's
34
+ // `html=` builds them in the HTML namespace, leaving them non-rendering.)
35
+ el.innerHTML = inner;
36
+ }
37
+ /**
38
+ * Turn a piece of inner-SVG markup into an icon draw-function. The returned
39
+ * function emits a freshly-built `<svg>` into the current Aberdeen scope,
40
+ * applying the {@link IconOptions} (or the module defaults).
41
+ */
42
+ export function mk(inner) {
43
+ return (opts = {}) => drawIcon(inner, opts);
44
+ }