staffa 0.5.0 → 0.6.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 (62) hide show
  1. package/README.md +6 -0
  2. package/dist/components/button.js +1 -2
  3. package/dist/components/checkbox.js +1 -1
  4. package/dist/components/field.js +0 -1
  5. package/dist/components/tabs.js +0 -1
  6. package/dist/components/toast.d.ts +2 -2
  7. package/dist/components/toast.js +42 -16
  8. package/dist/staffa.esm.js +1 -1
  9. package/dist/theme.js +10 -3
  10. package/package.json +5 -4
  11. package/skill/Attributes.md +10 -0
  12. package/skill/AutocompleteOptions.md +37 -0
  13. package/skill/BoxOptions.md +33 -0
  14. package/skill/ButtonChooserOptions.md +37 -0
  15. package/skill/ButtonGroupOptions.md +23 -0
  16. package/skill/ButtonOptions.md +57 -0
  17. package/skill/CheckboxOptions.md +27 -0
  18. package/skill/ContentOptions.md +16 -0
  19. package/skill/DialogOptions.md +70 -0
  20. package/skill/FieldOptions.md +63 -0
  21. package/skill/FloatingMenuOptions.md +21 -0
  22. package/skill/FormOptions.md +31 -0
  23. package/skill/MainOptions.md +91 -0
  24. package/skill/MenuItem.md +52 -0
  25. package/skill/MenuOptions.md +25 -0
  26. package/skill/SKILL.md +548 -0
  27. package/skill/SelectOptions.md +21 -0
  28. package/skill/Slot.md +13 -0
  29. package/skill/Tab.md +33 -0
  30. package/skill/TabsOptions.md +28 -0
  31. package/skill/TextareaOptions.md +51 -0
  32. package/skill/TextlineOptions.md +45 -0
  33. package/skill/TextlineType.md +18 -0
  34. package/skill/ToastOptions.md +40 -0
  35. package/skill/TooltipOptions.md +22 -0
  36. package/skill/addTooltip.md +25 -0
  37. package/skill/alert.md +17 -0
  38. package/skill/autocomplete.md +22 -0
  39. package/skill/box.md +25 -0
  40. package/skill/button.md +29 -0
  41. package/skill/buttonChooser.md +23 -0
  42. package/skill/buttonGroup.md +20 -0
  43. package/skill/checkbox.md +17 -0
  44. package/skill/confirm.md +17 -0
  45. package/skill/dialog.md +28 -0
  46. package/skill/form.md +28 -0
  47. package/skill/getDarkMode.md +12 -0
  48. package/skill/main.md +33 -0
  49. package/skill/menuButton.md +27 -0
  50. package/skill/prompt.md +19 -0
  51. package/skill/select.md +17 -0
  52. package/skill/showFloatingMenu.md +22 -0
  53. package/skill/tabs.md +19 -0
  54. package/skill/textarea.md +16 -0
  55. package/skill/textline.md +19 -0
  56. package/skill/toast.md +20 -0
  57. package/src/components/button.ts +1 -2
  58. package/src/components/checkbox.ts +1 -1
  59. package/src/components/field.ts +1 -2
  60. package/src/components/tabs.ts +1 -2
  61. package/src/components/toast.ts +48 -18
  62. package/src/theme.ts +10 -0
@@ -13,7 +13,7 @@ export interface ToastOptions {
13
13
  */
14
14
  type?: "success" | "danger" | "warning" | "neutral"
15
15
  /**
16
- * Auto-dismiss delay in milliseconds. Defaults to `4000`.
16
+ * Auto-dismiss delay in milliseconds. Defaults to `6000`.
17
17
  * Pass `0` to make the toast persistent until dismissed manually.
18
18
  */
19
19
  duration?: number;
@@ -37,7 +37,7 @@ A.insertGlobalCss({
37
37
  "&":
38
38
  "display:flex align-items:flex-start gap:$2 " +
39
39
  "padding: $3; border: 1px solid $s-border; r:$s-radius box-shadow:$s-shadow " +
40
- "pointer-events:auto",
40
+ "pointer-events:auto position:relative overflow:hidden",
41
41
  ".s-toast-body": "display:flex flex-direction:column gap:$1 flex:1 min-width:0",
42
42
  ".s-toast-title": "font-weight:700 line-height:1.3",
43
43
  ".s-toast-msg": "font-size:0.9em fg:$s-fg-muted line-height:1.4",
@@ -46,6 +46,7 @@ A.insertGlobalCss({
46
46
  "padding: 0 0.15em; r:4px flex-shrink:0 align-self:flex-start",
47
47
  ".s-toast-close:hover": "fg:$s-fg",
48
48
  ".s-toast-close:focus-visible": "outline:none box-shadow: 0 0 0 3px $s-focus; fg:$s-fg",
49
+ ".s-toast-progress": "position:absolute bottom:0 left:0 right:0 height:2px background:$s-accent width:100%",
49
50
  },
50
51
  });
51
52
 
@@ -54,15 +55,47 @@ let toastCount = 0;
54
55
  const toasts = A.proxy({} as Record<number, ToastEntry>);
55
56
 
56
57
  mountPortal(() => {
57
- // Only redraws when emptiness flips, keeping the DOM clean while no toasts show.
58
- if (A.isEmpty(toasts)) return;
58
+ // In initial peek is here to NOT subscribe to changes once `toasts` first becomes non empty,
59
+ // leaving the container in the DOM forever after. (So as not to cut of hide animations.)
60
+ if (A.peek(() => A.isEmpty(toasts)) && A.isEmpty(toasts)) return;
59
61
  A("div.s-toasts aria-live=polite aria-atomic=false", () => {
60
62
  A.onEach(toasts, (entry) => {
61
- const { opts } = entry;
63
+ const { opts, id } = entry;
62
64
  const role = opts.type === "danger" || opts.type === "warning" ? "alert" : "status";
63
65
  const surface = opts.type ?? "neutral";
66
+ const duration = opts.duration ?? 6000;
67
+
68
+ let timer: ReturnType<typeof setTimeout> | undefined;
69
+ let progressEl: HTMLElement | null = null;
70
+
71
+ const startCountdown = () => {
72
+ clearTimeout(timer);
73
+ if (progressEl) {
74
+ progressEl.style.transition = "none";
75
+ progressEl.style.width = "100%";
76
+ progressEl.offsetWidth; // force reflow so transition triggers
77
+ progressEl.style.transition = `width ${duration}ms linear`;
78
+ progressEl.style.width = "0%";
79
+ }
80
+ timer = setTimeout(() => dismiss(id), duration);
81
+ };
82
+
83
+ const stopCountdown = () => {
84
+ clearTimeout(timer);
85
+ timer = undefined;
86
+ if (progressEl) {
87
+ progressEl.style.transition = "none";
88
+ progressEl.style.width = "100%";
89
+ }
90
+ };
91
+
92
+ A.clean(() => clearTimeout(timer));
64
93
 
65
94
  A(`div.s-toast.s-s.${surface} role=${role}`, "create=", grow, "destroy=", shrink, opts.attrs, () => {
95
+ if (duration > 0) {
96
+ A("mouseenter=", stopCountdown);
97
+ A("mouseleave=", startCountdown);
98
+ }
66
99
  A("div.s-toast-body", () => {
67
100
  A(() => {
68
101
  if (opts.title != null) A("div.s-toast-title", () => drawSlot(opts.title));
@@ -73,10 +106,17 @@ mountPortal(() => {
73
106
  if (opts.dismissible === false) return;
74
107
  A("button.s-toast-close type=button aria-label=Dismiss", () => {
75
108
  A("#×");
76
- A("click=", () => dismiss(entry.id));
109
+ A("click=", () => dismiss(id));
77
110
  });
78
111
  });
112
+ if (duration > 0) {
113
+ progressEl = A("div.s-toast-progress") as HTMLElement;
114
+ }
79
115
  });
116
+
117
+ if (duration > 0) {
118
+ requestAnimationFrame(startCountdown);
119
+ }
80
120
  });
81
121
  });
82
122
  });
@@ -87,7 +127,7 @@ function dismiss(id: number): void {
87
127
 
88
128
  /**
89
129
  * Show a toast notification. Returns a `dismiss()` function to remove it
90
- * programmatically. Auto-dismisses after `duration` ms (default 4 000).
130
+ * programmatically. Auto-dismisses after `duration` ms (default 6 000).
91
131
  *
92
132
  * @example
93
133
  * ```ts
@@ -100,15 +140,5 @@ function dismiss(id: number): void {
100
140
  export function toast(opts: ToastOptions): () => void {
101
141
  const id = ++toastCount;
102
142
  toasts[id] = { id, opts };
103
-
104
- const duration = opts.duration ?? 4000;
105
- let timer: ReturnType<typeof setTimeout> | undefined;
106
- if (duration > 0) {
107
- timer = setTimeout(() => dismiss(id), duration);
108
- }
109
-
110
- return () => {
111
- if (timer != null) clearTimeout(timer);
112
- dismiss(id);
113
- };
143
+ return () => dismiss(id);
114
144
  }
package/src/theme.ts CHANGED
@@ -332,6 +332,16 @@ if (typeof document !== "undefined" && typeof requestAnimationFrame === "functio
332
332
  requestAnimationFrame(() => requestAnimationFrame(() => root.classList.remove("s-preload")));
333
333
  }
334
334
 
335
+ // ── Disabled region ───────────────────────────────────────────────────────────
336
+ // aria-disabled="true" on any container dims it and blocks pointer events on
337
+ // it and all descendants, matching the per-element disabled look. Keyboard
338
+ // access to focusable descendants is unaffected — add the `inert` attribute too
339
+ // if you need that.
340
+ A.insertGlobalCss({
341
+ ":disabled, [aria-disabled=true]": "opacity:0.45 filter:saturate(0.6) user-select:none",
342
+ ":disabled, [aria-disabled=true], :disabled *, [aria-disabled=true] *": "pointer-events:none cursor:not-allowed",
343
+ });
344
+
335
345
  // ── Flow content: vertical rhythm & light typography ─────────────────────────
336
346
  // Sensible block defaults for *any* content — your own UI just as much as
337
347
  // markdown-rendered HTML. The rhythm: strip the browser's block margins, then