actual-css 0.7.0 → 0.9.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 (144) hide show
  1. package/CHANGELOG.md +217 -0
  2. package/README.md +13 -11
  3. package/components.json +668 -0
  4. package/dist/actual.css +166 -37
  5. package/dist/actual.full.css +1087 -643
  6. package/dist/actual.full.js +1 -1
  7. package/dist/actual.full.min.css +1 -1
  8. package/dist/actual.js +1 -1
  9. package/dist/actual.min.css +1 -1
  10. package/docs/design-notes/accessibility-decisions.md +33 -0
  11. package/docs/design-notes/browser-support.md +62 -30
  12. package/docs/design-notes/column-layout.md +3 -3
  13. package/docs/design-notes/css-anchor.md +75 -21
  14. package/docs/design-notes/enhancement-contract.md +14 -6
  15. package/docs/design-notes/grid.md +4 -4
  16. package/docs/design-notes/naming.md +6 -1
  17. package/docs/design-notes/platform-alignment.md +53 -7
  18. package/docs/design-notes/popover-manual-poc.md +105 -25
  19. package/docs/design-notes/surface-reparenting.md +56 -47
  20. package/docs/design-notes/tooltip-coordinate-space.md +132 -0
  21. package/docs/design-notes/widget-primitives.md +7 -4
  22. package/docs/pages/components/accordion.md +41 -2
  23. package/docs/pages/components/alert.md +85 -30
  24. package/docs/pages/components/app-navigation.md +6 -0
  25. package/docs/pages/components/avatar.md +6 -6
  26. package/docs/pages/components/badge.md +101 -16
  27. package/docs/pages/components/button.md +16 -8
  28. package/docs/pages/components/card.md +10 -10
  29. package/docs/pages/components/dialog.md +21 -6
  30. package/docs/pages/components/drawer.md +13 -0
  31. package/docs/pages/components/fab.md +4 -3
  32. package/docs/pages/components/list.md +42 -4
  33. package/docs/pages/components/navbar.md +17 -1
  34. package/docs/pages/components/pagination.md +2 -2
  35. package/docs/pages/components/skeleton.md +3 -1
  36. package/docs/pages/components/steps.md +19 -0
  37. package/docs/pages/components/table.md +3 -2
  38. package/docs/pages/enhancements/flyout.md +82 -38
  39. package/docs/pages/enhancements/overview.md +25 -0
  40. package/docs/pages/enhancements/status-bar.md +1 -1
  41. package/docs/pages/enhancements/tabs.md +45 -37
  42. package/docs/pages/enhancements/tooltip.md +36 -10
  43. package/docs/pages/examples/overview.md +38 -5
  44. package/docs/pages/forms/choice-cards.md +11 -6
  45. package/docs/pages/forms/combobox.md +1 -1
  46. package/docs/pages/forms/floating-field.md +4 -3
  47. package/docs/pages/forms/joined-controls.md +22 -0
  48. package/docs/pages/forms/otp.md +2 -1
  49. package/docs/pages/forms/overview.md +1 -1
  50. package/docs/pages/forms/states-and-sizes.md +16 -18
  51. package/docs/pages/forms/sticky-actions.md +2 -2
  52. package/docs/pages/forms/switch.md +1 -1
  53. package/docs/pages/foundations/philosophy.md +57 -26
  54. package/docs/pages/foundations/theming.md +2 -2
  55. package/docs/pages/foundations/tokens.md +118 -21
  56. package/docs/pages/foundations/typography.md +35 -14
  57. package/docs/pages/guides/introduction.md +11 -9
  58. package/docs/pages/guides/progressive-enhancement.md +115 -30
  59. package/docs/pages/guides/tailwind.md +3 -2
  60. package/docs/pages/layout/media.md +1 -1
  61. package/docs/pages/layout/overview.md +11 -1
  62. package/docs/pages/layout/scroll-snap.md +3 -3
  63. package/docs/pages/layout/stack.md +17 -9
  64. package/docs/pages/patterns/icon-input.md +4 -2
  65. package/docs/pages/patterns/icon-slot.md +2 -0
  66. package/docs/pages/patterns/intent-nesting.md +1 -1
  67. package/docs/pages/utilities/overview.md +23 -2
  68. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  69. package/docs/pages/utilities/spacing.md +10 -8
  70. package/llms.txt +2 -1
  71. package/package.json +159 -149
  72. package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
  73. package/src/css/components/accordion.css +42 -3
  74. package/src/css/components/alert.css +25 -13
  75. package/src/css/components/app-nav.css +18 -8
  76. package/src/css/components/avatar.css +13 -1
  77. package/src/css/components/badge.css +78 -12
  78. package/src/css/components/breadcrumb.css +2 -3
  79. package/src/css/components/button.css +9 -16
  80. package/src/css/components/card.css +9 -2
  81. package/src/css/components/chat.css +1 -0
  82. package/src/css/components/data-list.css +1 -1
  83. package/src/css/components/drawer.css +101 -32
  84. package/src/css/components/fab.css +0 -6
  85. package/src/css/components/flyout.css +35 -98
  86. package/src/css/components/index.css +0 -1
  87. package/src/css/components/indicator.css +21 -0
  88. package/src/css/components/key.css +10 -0
  89. package/src/css/components/list.css +40 -4
  90. package/src/css/components/menu.css +2 -1
  91. package/src/css/components/modal.css +48 -33
  92. package/src/css/components/navbar.css +37 -4
  93. package/src/css/components/pagination.css +1 -0
  94. package/src/css/components/skeleton.css +15 -4
  95. package/src/css/components/spinner.css +6 -6
  96. package/src/css/components/status-bar.css +0 -5
  97. package/src/css/components/steps.css +45 -59
  98. package/src/css/components/tab.css +29 -9
  99. package/src/css/components/table.css +13 -5
  100. package/src/css/components/tooltip.css +36 -40
  101. package/src/css/core/base.css +23 -1
  102. package/src/css/core/intents.css +17 -6
  103. package/src/css/core/theme.css +27 -0
  104. package/src/css/core/tokens.css +35 -15
  105. package/src/css/core/variants.css +64 -15
  106. package/src/css/forms/choice-card.css +4 -4
  107. package/src/css/forms/choice.css +76 -29
  108. package/src/css/forms/control.css +2 -1
  109. package/src/css/forms/floating-field.css +3 -3
  110. package/src/css/forms/form.css +4 -3
  111. package/src/css/forms/input-icon.css +20 -14
  112. package/src/css/forms/native.css +6 -1
  113. package/src/css/forms/otp.css +9 -1
  114. package/src/css/forms/range.css +2 -0
  115. package/src/css/forms/select.css +29 -9
  116. package/src/css/forms/switch.css +28 -8
  117. package/src/css/layout/app-layout.css +1 -1
  118. package/src/css/layout/grid.css +50 -76
  119. package/src/css/layout/scroll-snap.css +11 -7
  120. package/src/css/layout/scroller.css +3 -4
  121. package/src/css/layout/stack.css +2 -2
  122. package/src/css/typography/fluid.css +4 -3
  123. package/src/css/typography/prose.css +6 -5
  124. package/src/css/utilities/base.css +2 -1
  125. package/src/css/utilities/extra.css +20 -17
  126. package/src/js/context-menu.js +27 -26
  127. package/src/js/dialog.js +2 -1
  128. package/src/js/dismiss.js +5 -0
  129. package/src/js/enhance.js +34 -2
  130. package/src/js/enhancement-loader.js +36 -43
  131. package/src/js/events.js +0 -3
  132. package/src/js/filter.js +0 -1
  133. package/src/js/floating-space.js +42 -0
  134. package/src/js/flyout.js +9 -44
  135. package/src/js/focus-group.js +7 -0
  136. package/src/js/focus.js +6 -0
  137. package/src/js/menu.js +10 -1
  138. package/src/js/selectors.js +5 -9
  139. package/src/js/status.js +6 -5
  140. package/src/js/surface.js +97 -139
  141. package/src/js/tab.js +6 -1
  142. package/src/js/tooltip.js +150 -51
  143. package/src/js/validation.js +1 -1
  144. package/src/css/components/surface.css +0 -49
@@ -29,51 +29,45 @@ const styleLoads = new Map();
29
29
 
30
30
  let moduleImporter = (url) => import(url);
31
31
 
32
- export function loadScript(url) {
32
+ function loadAsset(url, loads, kind) {
33
33
  const absoluteUrl = new URL(url, document.baseURI).href;
34
34
 
35
- if (!scriptLoads.has(absoluteUrl)) {
36
- scriptLoads.set(
37
- absoluteUrl,
38
- new Promise((resolve, reject) => {
39
- const script = document.createElement("script");
40
- script.src = absoluteUrl;
41
- script.onload = resolve;
42
- script.onerror = () => {
43
- scriptLoads.delete(absoluteUrl);
44
- script.remove();
45
- reject(new Error(`Unable to load script: ${absoluteUrl}`));
46
- };
47
- document.head.append(script);
48
- }),
49
- );
35
+ if (!loads.has(absoluteUrl)) {
36
+ const pending = new Promise((resolve, reject) => {
37
+ const element = document.createElement(kind === "script" ? "script" : "link");
38
+ if (kind === "script") {
39
+ element.src = absoluteUrl;
40
+ } else {
41
+ element.rel = "stylesheet";
42
+ element.href = absoluteUrl;
43
+ }
44
+ element.onload = resolve;
45
+ element.onerror = () => {
46
+ element.remove();
47
+ reject(new Error(`Unable to load ${kind}: ${absoluteUrl}`));
48
+ };
49
+ document.head.append(element);
50
+ });
51
+
52
+ // The DOM clears the failed element; the cache evicts the rejected
53
+ // promise. The guard keeps a finished load from wiping out a newer one.
54
+ // A catch never runs synchronously, so the eviction always lands after
55
+ // the `set` above, even when the transport rejects during append.
56
+ loads.set(absoluteUrl, pending);
57
+ pending.catch(() => {
58
+ if (loads.get(absoluteUrl) === pending) loads.delete(absoluteUrl);
59
+ });
50
60
  }
51
61
 
52
- return scriptLoads.get(absoluteUrl);
62
+ return loads.get(absoluteUrl);
53
63
  }
54
64
 
55
- export function loadStyle(url) {
56
- const absoluteUrl = new URL(url, document.baseURI).href;
57
-
58
- if (!styleLoads.has(absoluteUrl)) {
59
- styleLoads.set(
60
- absoluteUrl,
61
- new Promise((resolve, reject) => {
62
- const link = document.createElement("link");
63
- link.rel = "stylesheet";
64
- link.href = absoluteUrl;
65
- link.onload = resolve;
66
- link.onerror = () => {
67
- styleLoads.delete(absoluteUrl);
68
- link.remove();
69
- reject(new Error(`Unable to load style: ${absoluteUrl}`));
70
- };
71
- document.head.append(link);
72
- }),
73
- );
74
- }
65
+ export function loadScript(url) {
66
+ return loadAsset(url, scriptLoads, "script");
67
+ }
75
68
 
76
- return styleLoads.get(absoluteUrl);
69
+ export function loadStyle(url) {
70
+ return loadAsset(url, styleLoads, "style");
77
71
  }
78
72
 
79
73
  function makeInit(name, connect) {
@@ -177,8 +171,7 @@ export function loadEnhancement(name, url) {
177
171
  return promise;
178
172
  }
179
173
 
180
- async function loadManifest(manifest) {
181
- const entries = Object.entries(manifest);
174
+ async function loadManifest(entries) {
182
175
  const results = await Promise.allSettled(
183
176
  entries.map(([name, url]) => loadEnhancement(name, url)),
184
177
  );
@@ -239,7 +232,7 @@ export async function loadEnhancements(scope = document) {
239
232
  }
240
233
  }
241
234
 
242
- const result = await loadManifest(Object.fromEntries(manifest));
235
+ const result = await loadManifest([...manifest]);
243
236
 
244
237
  if (result.failed.length === 0) {
245
238
  for (const block of blocks) block.remove();
@@ -250,7 +243,7 @@ export async function loadEnhancements(scope = document) {
250
243
 
251
244
  async function loadManifestBlock(block) {
252
245
  const entries = parseManifestEntries(block.textContent);
253
- const result = await loadManifest(Object.fromEntries(entries));
246
+ const result = await loadManifest(entries);
254
247
 
255
248
  for (const failure of result.failed) {
256
249
  console.error(`Enhancement "${failure.name}" failed to load:`, failure.error);
@@ -286,7 +279,7 @@ export async function loadResponse(response) {
286
279
  throw new TypeError("Enhance-Modules header must be a JSON object.");
287
280
  }
288
281
 
289
- return loadManifest(manifest);
282
+ return loadManifest(Object.entries(manifest));
290
283
  }
291
284
 
292
285
  export function __setModuleImporter(fn) {
package/src/js/events.js CHANGED
@@ -1,9 +1,6 @@
1
1
  export const ACTUAL_EVENT_PREFIX = "actual";
2
2
 
3
3
  export const EVENTS = {
4
- reposition: `${ACTUAL_EVENT_PREFIX}:reposition`,
5
- hide: `${ACTUAL_EVENT_PREFIX}:hide`,
6
- outOfView: `${ACTUAL_EVENT_PREFIX}:out-of-view`,
7
4
  // Cancelable: dispatched by a surface before it opens.
8
5
  // detail: { surface, options }
9
6
  surfaceOpen: `${ACTUAL_EVENT_PREFIX}:surface-open`,
package/src/js/filter.js CHANGED
@@ -62,7 +62,6 @@ function filterSlug(value, event) {
62
62
  const slug = normalizeText(value)
63
63
  .toLowerCase()
64
64
  .replace(/[^\p{L}\p{N}]+/gu, "-")
65
- .replace(/-{2,}/g, "-")
66
65
  .replace(/^-+/, "");
67
66
 
68
67
  return keepTrailingSeparator ? slug : slug.replace(/-+$/, "");
@@ -0,0 +1,42 @@
1
+ /*
2
+ * The coordinate space and CSS positioning scheme are one decision. Keeping
3
+ * them in one value prevents a caller from writing document coordinates to a
4
+ * fixed box, or viewport coordinates to an absolute one.
5
+ *
6
+ * An open top-layer element with position: absolute uses the initial
7
+ * containing block, so only the anchor determines the correct mode.
8
+ */
9
+ const VIEWPORT_ANCHORED = "dialog:modal, :popover-open";
10
+
11
+ function isViewportAnchored(anchor) {
12
+ const win = anchor?.ownerDocument?.defaultView;
13
+ if (!win) return true;
14
+
15
+ // Modal dialogs and open popovers are viewport-anchored without computing
16
+ // position: fixed, so the ancestor walk below cannot identify them.
17
+ try {
18
+ if (anchor.closest(VIEWPORT_ANCHORED)) return true;
19
+ } catch {
20
+ /* An engine without :modal / :popover-open: the walk is all there is. */
21
+ }
22
+
23
+ for (let el = anchor; el; el = el.parentElement) {
24
+ const { position } = win.getComputedStyle(el);
25
+ // Resolve sticky once even while unstuck: changing mode when it sticks
26
+ // would jump the floating element between coordinate systems mid-scroll.
27
+ if (position === "fixed" || position === "sticky") return true;
28
+ }
29
+ return false;
30
+ }
31
+
32
+ /**
33
+ * @param {Element | null | undefined} anchor
34
+ * @returns {{ space: "viewport" | "document", position: "fixed" | "absolute" }}
35
+ */
36
+ export function positionModeFor(anchor) {
37
+ const space = isViewportAnchored(anchor) ? "viewport" : "document";
38
+ return {
39
+ space,
40
+ position: space === "document" ? "absolute" : "fixed",
41
+ };
42
+ }
package/src/js/flyout.js CHANGED
@@ -3,15 +3,8 @@ import { focusFirstDescendant } from "./focus.js";
3
3
  import { connectMenu, focusFirstMenuItem, focusLastMenuItem } from "./menu.js";
4
4
  import { closeSurface, isSurfaceOpen, openSurface, retainSurface } from "./surface.js";
5
5
 
6
- const panelRefs = new WeakMap();
7
6
  const triggerMap = new WeakMap();
8
7
 
9
- function normalizeBreakpoint(raw) {
10
- const BREAKPOINTS = { sm: 640, md: 768, lg: 1024 };
11
- const value = Number.parseInt(BREAKPOINTS[raw] || raw, 10);
12
- return Number.isFinite(value) ? value : undefined;
13
- }
14
-
15
8
  function readFlyoutOptions(panel) {
16
9
  const opts = { autoClose: "true" };
17
10
  const ds = panel.dataset;
@@ -20,11 +13,6 @@ function readFlyoutOptions(panel) {
20
13
  const d = Number.parseFloat(ds.flyoutDistance);
21
14
  if (Number.isFinite(d)) opts.distance = d;
22
15
  }
23
- if (ds.flyoutMobile) opts.mobile = ds.flyoutMobile;
24
- if (ds.flyoutBreakpoint) {
25
- const bp = normalizeBreakpoint(ds.flyoutBreakpoint);
26
- if (bp != null) opts.breakpoint = bp;
27
- }
28
16
  if (ds.flyoutAutoClose) opts.autoClose = ds.flyoutAutoClose;
29
17
  return opts;
30
18
  }
@@ -93,20 +81,12 @@ function onTriggerKeydown(e) {
93
81
 
94
82
  switch (e.key) {
95
83
  case "ArrowDown":
96
- e.preventDefault();
97
- if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
98
- focusFirstMenuItem(panel);
99
- break;
100
- case "ArrowUp":
101
- e.preventDefault();
102
- if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
103
- focusLastMenuItem(panel);
104
- break;
105
84
  case "Home":
106
85
  e.preventDefault();
107
86
  if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
108
87
  focusFirstMenuItem(panel);
109
88
  break;
89
+ case "ArrowUp":
110
90
  case "End":
111
91
  e.preventDefault();
112
92
  if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
@@ -131,7 +111,7 @@ function connectTrigger(trigger) {
131
111
  if (triggerMap.has(trigger)) return;
132
112
 
133
113
  const controller = new AbortController();
134
- const state = { panel: null, controller, releaseMenu: null };
114
+ const state = { panel: null, controller, releaseMenu: null, releaseSurface: null };
135
115
  triggerMap.set(trigger, state);
136
116
 
137
117
  resolvePanel(trigger, state);
@@ -144,38 +124,23 @@ function connectTrigger(trigger) {
144
124
  trigger.addEventListener("keydown", onTriggerKeydown, { signal: controller.signal });
145
125
  }
146
126
 
147
- function releasePanelReference(trigger, state) {
127
+ function releasePanelReference(state) {
148
128
  state.releaseMenu?.();
149
129
  state.releaseMenu = null;
150
-
151
- const panel = state.panel;
130
+ state.releaseSurface?.();
131
+ state.releaseSurface = null;
152
132
  state.panel = null;
153
- if (!panel) return;
154
-
155
- const panelEntry = panelRefs.get(panel);
156
- if (!panelEntry) return;
157
-
158
- panelEntry.triggers.delete(trigger);
159
- if (panelEntry.triggers.size === 0) {
160
- panelEntry.release();
161
- panelRefs.delete(panel);
162
- }
163
133
  }
164
134
 
165
135
  function resolvePanel(trigger, state) {
166
136
  const panel = panelFor(trigger);
167
137
  if (state.panel === panel && panel?.isConnected) return panel;
168
138
 
169
- releasePanelReference(trigger, state);
139
+ releasePanelReference(state);
170
140
  if (!panel) return null;
171
141
 
172
- let panelEntry = panelRefs.get(panel);
173
- if (!panelEntry) {
174
- const release = retainSurface(panel);
175
- panelEntry = { triggers: new Set(), release };
176
- panelRefs.set(panel, panelEntry);
177
- }
178
- panelEntry.triggers.add(trigger);
142
+ // Surface retention owns the shared lifetime across all triggers and consumers.
143
+ state.releaseSurface = retainSurface(panel);
179
144
  state.panel = panel;
180
145
 
181
146
  if (isMenuFlyout(panel)) {
@@ -197,7 +162,7 @@ function disconnectTrigger(trigger) {
197
162
  if (state.panel?.isConnected && isSurfaceOpen(state.panel)) {
198
163
  closeSurface(state.panel, { restoreFocus: false });
199
164
  }
200
- releasePanelReference(trigger, state);
165
+ releasePanelReference(state);
201
166
  triggerMap.delete(trigger);
202
167
  }
203
168
 
@@ -1,3 +1,10 @@
1
+ /*
2
+ * Focus group — one remembered tab stop and Arrow/Home/End navigation for a
3
+ * composite widget. The consumer supplies getItems(); the controller only
4
+ * updates tabindex and focus, never activation or ARIA. sync() re-reads the
5
+ * candidate list; the primitive does not observe the subtree.
6
+ */
7
+
1
8
  import { itemForKey } from "./keys.js";
2
9
 
3
10
  const ORIENTATIONS = new Set(["horizontal", "vertical"]);
package/src/js/focus.js CHANGED
@@ -9,6 +9,12 @@ const FOCUSABLE_SELECTOR = [
9
9
  "[tabindex]",
10
10
  ].join(",");
11
11
 
12
+ /*
13
+ * The branch below checkVisibility() is not dead code: checkVisibility() is
14
+ * Safari 17.4 and the floor is Safari 17, so it is the branch that runs at the
15
+ * bottom of the supported range — where it also decides whether a closed
16
+ * popover's contents are reachable. Measured there; do not drop it.
17
+ */
12
18
  export function isElementVisible(el) {
13
19
  if (!el || el.hidden) return false;
14
20
  if (typeof el.checkVisibility === "function") return el.checkVisibility();
package/src/js/menu.js CHANGED
@@ -1,3 +1,13 @@
1
+ /*
2
+ * Menu — the shared `.menu > li > .menu-item` vocabulary and wiring.
3
+ *
4
+ * getMenuItems() filters to usable items; onMenuKeydown() adds directional
5
+ * navigation and activation; connectMenu() owns the ref-counted listeners a
6
+ * menu shares across its triggers. Wiring is per menu, not per trigger, so a
7
+ * menu retained by several triggers (or by flyout and context-menu at once)
8
+ * tears down only when the last one releases it.
9
+ */
10
+
1
11
  import { isElementVisible } from "./focus.js";
2
12
  import { connectFocusGroup } from "./focus-group.js";
3
13
  import { firstItem, itemForKey, lastItem } from "./keys.js";
@@ -134,7 +144,6 @@ export function connectMenu(menu, { close }) {
134
144
  released = true;
135
145
  entry.count--;
136
146
  if (entry.count <= 0) {
137
- entry.focusGroup?.disconnect();
138
147
  entry.controller.abort();
139
148
  menuConnections.delete(menu);
140
149
  }
@@ -1,11 +1,11 @@
1
1
  /*
2
2
  * Selectors — the state-class vocabulary the runtime writes.
3
3
  *
4
- * This file no longer lists discovery selectors: behavior discovery moved
5
- * to data-enhance tokens in 0.2. What remains are the class names modules
6
- * write as state (is-open, was-validated, …) and validation's two read-side
7
- * presentation adapters (field, fieldError). To run this runtime against a
8
- * different CSS framework, edit this file (or alias it in a bundler).
4
+ * The file lists the class names modules write as state (is-open,
5
+ * was-validated, …) and validation's two read-side presentation adapters
6
+ * (field, fieldError). Behavior discovery is not listed here: it uses
7
+ * data-enhance tokens. To run this runtime against a different CSS
8
+ * framework, edit this file (or alias it in a bundler).
9
9
  *
10
10
  * data-* attributes and ARIA are not listed: they are each module's own
11
11
  * behavior API, not CSS vocabulary, so they stay framework-neutral by
@@ -27,10 +27,6 @@ export const CLASSES = {
27
27
  // surface.js — state class on the open flyout/menu/dialog surface.
28
28
  // status.js writes the same class on the status bar while a message shows.
29
29
  open: "is-open",
30
- // surface.js — state class while the surface renders as a mobile sheet.
31
- sheet: "is-sheet",
32
- // surface.js — class name written on the generated sheet backdrop.
33
- backdrop: "surface-backdrop",
34
30
  // dialog.js — transient class for the "can't dismiss" shake animation.
35
31
  static: "is-static",
36
32
  // dialog.js — written on <html> while any modal dialog is open.
package/src/js/status.js CHANGED
@@ -58,6 +58,7 @@ import { waitForTransitions } from "./transition.js";
58
58
  const STATUS_SELECTOR = '[data-status][role="status"]';
59
59
  const STATUS_CLASSES_ATTR = "statusClasses";
60
60
  const VIEWPORT_OFFSET_PROPERTY = "--status-viewport-offset";
61
+ const DEFAULT_DURATION_MS = 3000;
61
62
 
62
63
  let statusTimer;
63
64
  // Bumped by every show and every clear. A deferred exit cleanup only runs when
@@ -151,10 +152,10 @@ export function status(message, options = {}) {
151
152
  const target = statusTarget();
152
153
  if (!target || message == null) return;
153
154
 
154
- // A live region with nothing to announce is not a message. `:empty` used to
155
- // absorb this case for free; now that the open state is explicit, an empty
156
- // message has to mean "close" or it would open a blank pill. This also keeps
157
- // status(await response.text()) from leaving a stale message on screen.
155
+ // A live region with nothing to announce is not a message. The open state
156
+ // is explicit, so an empty message has to mean "close" or it would open a
157
+ // blank pill. This also keeps status(await response.text()) from leaving a
158
+ // stale message on screen.
158
159
  if (message === "") {
159
160
  status.clear();
160
161
  return;
@@ -179,7 +180,7 @@ export function status(message, options = {}) {
179
180
  trackViewport(target);
180
181
 
181
182
  if (options.duration !== false) {
182
- statusTimer = setTimeout(() => status.clear(), options.duration ?? 3000);
183
+ statusTimer = setTimeout(() => status.clear(), options.duration ?? DEFAULT_DURATION_MS);
183
184
  }
184
185
  }
185
186