actual-css 0.4.1 → 0.5.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 (74) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +2 -2
  3. package/dist/actual.css +53 -14
  4. package/dist/actual.full.css +300 -91
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/dist/actual.min.css +1 -1
  8. package/docs/design-notes/spacing-units.md +286 -0
  9. package/docs/navigation.json +1 -0
  10. package/docs/pages/components/alert.md +1 -1
  11. package/docs/pages/components/app-navigation.md +2 -0
  12. package/docs/pages/components/badge.md +17 -3
  13. package/docs/pages/components/button.md +9 -2
  14. package/docs/pages/components/card.md +13 -0
  15. package/docs/pages/components/data-list.md +72 -0
  16. package/docs/pages/components/dialog.md +2 -0
  17. package/docs/pages/components/drawer.md +2 -0
  18. package/docs/pages/components/fab.md +2 -0
  19. package/docs/pages/components/meter.md +56 -1
  20. package/docs/pages/components/progress.md +10 -0
  21. package/docs/pages/components/skeleton.md +2 -0
  22. package/docs/pages/components/steps.md +10 -2
  23. package/docs/pages/components/table.md +1 -0
  24. package/docs/pages/enhancements/flyout.md +2 -0
  25. package/docs/pages/enhancements/status-bar.md +10 -1
  26. package/docs/pages/examples/overview.md +2 -1
  27. package/docs/pages/forms/joined-controls.md +2 -0
  28. package/docs/pages/forms/overview.md +1 -1
  29. package/docs/pages/forms/range.md +2 -0
  30. package/docs/pages/forms/states-and-sizes.md +1 -1
  31. package/docs/pages/forms/validation.md +2 -0
  32. package/docs/pages/foundations/philosophy.md +11 -1
  33. package/docs/pages/foundations/tokens.md +7 -3
  34. package/docs/pages/foundations/typography.md +10 -2
  35. package/docs/pages/guides/introduction.md +1 -1
  36. package/docs/pages/layout/scroll-snap.md +2 -0
  37. package/docs/pages/layout/sidebar-layout.md +2 -0
  38. package/docs/pages/layout/topbar.md +2 -0
  39. package/docs/pages/utilities/accessibility.md +18 -0
  40. package/package.json +1 -1
  41. package/scripts/reserved-classes.json +4 -0
  42. package/src/css/components/actions.css +1 -0
  43. package/src/css/components/breadcrumb.css +1 -0
  44. package/src/css/components/data-list.css +27 -0
  45. package/src/css/components/drawer.css +30 -11
  46. package/src/css/components/flyout.css +17 -12
  47. package/src/css/components/index.css +1 -0
  48. package/src/css/components/meter.css +28 -1
  49. package/src/css/components/modal.css +28 -11
  50. package/src/css/components/navbar.css +2 -0
  51. package/src/css/components/pagination.css +1 -0
  52. package/src/css/components/progress.css +5 -0
  53. package/src/css/components/status-bar.css +31 -15
  54. package/src/css/components/steps.css +36 -9
  55. package/src/css/components/surface.css +5 -3
  56. package/src/css/components/tab.css +1 -0
  57. package/src/css/components/tooltip.css +5 -8
  58. package/src/css/core/base.css +7 -1
  59. package/src/css/core/intents.css +6 -0
  60. package/src/css/core/tokens.css +23 -9
  61. package/src/css/core/variants.css +17 -4
  62. package/src/css/forms/form-actions.css +1 -0
  63. package/src/css/forms/form.css +4 -4
  64. package/src/css/layout/cluster.css +1 -0
  65. package/src/css/layout/scroll-snap.css +1 -0
  66. package/src/css/layout/sidebar-layout.css +1 -0
  67. package/src/css/layout/switcher.css +1 -0
  68. package/src/css/layout/topbar.css +1 -0
  69. package/src/css/typography/prose.css +2 -3
  70. package/src/css/utilities/base.css +16 -0
  71. package/src/js/selectors.js +1 -0
  72. package/src/js/status.js +88 -7
  73. package/src/js/surface.js +2 -11
  74. package/src/js/transition.js +20 -0
package/src/js/status.js CHANGED
@@ -2,8 +2,15 @@
2
2
  * Status — singleton floating status bar.
3
3
  *
4
4
  * A status bar is a transient, non-critical feedback area. Keep one element in
5
- * the HTML, empty by default; JavaScript only updates its text content. It is
6
- * hidden visually when empty but stays available as a live region.
5
+ * the HTML, empty by default; JavaScript writes its text content and toggles
6
+ * the shared `.is-open` state class, which is the visual state the CSS
7
+ * animates. The element itself is never hidden, so it stays a live region.
8
+ *
9
+ * Showing and clearing are asymmetric on purpose. A show writes content first
10
+ * and opens after; a clear only closes, and empties content and intent once the
11
+ * exit transition has finished, so the bar leaves in the state it was shown in
12
+ * rather than collapsing to a neutral empty pill mid-animation. An empty
13
+ * message is a clear, not a blank pill.
7
14
  *
8
15
  * Importing the module auto-wires it to Actual's validation: a form that fails
9
16
  * to submit shows its data-validation-message in the status bar. The bar is a
@@ -39,17 +46,62 @@
39
46
 
40
47
  import { registerCommands, targetFor } from "./command.js";
41
48
  import { EVENTS } from "./events.js";
49
+ import { CLASSES } from "./selectors.js";
50
+ import { waitForTransitions } from "./transition.js";
42
51
 
43
52
  const STATUS_SELECTOR = '[data-status][role="status"]';
44
53
  const STATUS_CLASSES_ATTR = "statusClasses";
54
+ const VIEWPORT_OFFSET_PROPERTY = "--status-viewport-offset";
45
55
 
46
56
  let statusTimer;
57
+ // Bumped by every show and every clear. A deferred exit cleanup only runs when
58
+ // its own generation is still current, so a message that arrives mid-exit is
59
+ // never emptied by the previous message's cleanup.
60
+ let statusGeneration = 0;
61
+ let viewportTracker = null;
47
62
 
48
63
  function statusTarget() {
49
64
  if (typeof document === "undefined") return null;
50
65
  return document.querySelector(STATUS_SELECTOR);
51
66
  }
52
67
 
68
+ // The bar is fixed to the layout viewport, so the mobile software keyboard
69
+ // covers it. visualViewport reports the occluded band; the CSS adds it to the
70
+ // bottom inset. Tracking runs only while a message is on screen.
71
+ function trackViewport(target) {
72
+ if (viewportTracker?.target === target) return;
73
+ releaseViewport();
74
+
75
+ const view = target.ownerDocument?.defaultView;
76
+ const viewport = view?.visualViewport;
77
+ if (!viewport) return;
78
+
79
+ const controller = new AbortController();
80
+ const update = () => {
81
+ // Self-heal: an app that drops the bar while a message is up never reaches
82
+ // the clear() path, so the tracker would outlive its element.
83
+ if (!target.isConnected) {
84
+ releaseViewport();
85
+ return;
86
+ }
87
+ const occluded = Math.max(0, view.innerHeight - viewport.height - viewport.offsetTop);
88
+ target.style.setProperty(VIEWPORT_OFFSET_PROPERTY, `${occluded}px`);
89
+ };
90
+
91
+ viewport.addEventListener("resize", update, { signal: controller.signal });
92
+ viewport.addEventListener("scroll", update, { signal: controller.signal });
93
+ update();
94
+
95
+ viewportTracker = { target, stop: () => controller.abort() };
96
+ }
97
+
98
+ function releaseViewport() {
99
+ if (!viewportTracker) return;
100
+ viewportTracker.stop();
101
+ viewportTracker.target.style.removeProperty(VIEWPORT_OFFSET_PROPERTY);
102
+ viewportTracker = null;
103
+ }
104
+
53
105
  function intentClasses(value) {
54
106
  return typeof value === "string" ? value.trim().split(/\s+/).filter(Boolean) : [];
55
107
  }
@@ -66,7 +118,17 @@ export function status(message, options = {}) {
66
118
  const target = statusTarget();
67
119
  if (!target || message == null) return;
68
120
 
121
+ // A live region with nothing to announce is not a message. `:empty` used to
122
+ // absorb this case for free; now that the open state is explicit, an empty
123
+ // message has to mean "close" or it would open a blank pill. This also keeps
124
+ // status(await response.text()) from leaving a stale message on screen.
125
+ if (message === "") {
126
+ status.clear();
127
+ return;
128
+ }
129
+
69
130
  clearTimeout(statusTimer);
131
+ statusGeneration++;
70
132
 
71
133
  clearIntentClasses(target);
72
134
  const intent = intentClasses(options.intent);
@@ -75,7 +137,11 @@ export function status(message, options = {}) {
75
137
  target.dataset[STATUS_CLASSES_ATTR] = intent.join(" ");
76
138
  }
77
139
 
140
+ // Content and intent are in place before the bar opens, so it enters at its
141
+ // final size and color.
78
142
  target.textContent = message;
143
+ target.classList.add(CLASSES.open);
144
+ trackViewport(target);
79
145
 
80
146
  if (options.duration !== false) {
81
147
  statusTimer = setTimeout(() => status.clear(), options.duration ?? 3000);
@@ -83,12 +149,26 @@ export function status(message, options = {}) {
83
149
  }
84
150
 
85
151
  status.clear = function clear() {
152
+ clearTimeout(statusTimer);
153
+
86
154
  const target = statusTarget();
87
- if (target) {
155
+ if (!target) {
156
+ // No bar to animate out — but one may have been removed while open, and its
157
+ // viewport listeners and element reference still have to go.
158
+ releaseViewport();
159
+ return;
160
+ }
161
+
162
+ const generation = ++statusGeneration;
163
+ target.classList.remove(CLASSES.open);
164
+
165
+ waitForTransitions(target).then(() => {
166
+ // A show during the exit owns the bar now; leave its message alone.
167
+ if (generation !== statusGeneration) return;
88
168
  target.textContent = "";
89
169
  clearIntentClasses(target);
90
- }
91
- clearTimeout(statusTimer);
170
+ releaseViewport();
171
+ });
92
172
  };
93
173
 
94
174
  const STATUS_SHOW_COMMANDS = ["--status"];
@@ -125,8 +205,9 @@ if (typeof document !== "undefined") {
125
205
  // importing this module. A message shows it; omitting one clears it.
126
206
  document.addEventListener(EVENTS.status, (event) => {
127
207
  const { message, intent, duration } = event.detail ?? {};
128
- // `message != null` keeps "" (an intentional empty message) on the show
129
- // path, consistent with status() itself; an absent message clears.
208
+ // An absent message clears here; "" reaches status() and closes there. Both
209
+ // paths end in the same exit, so a detail that lost its message never
210
+ // strands a stale one on screen.
130
211
  if (message != null) {
131
212
  status(message, { intent, duration });
132
213
  } else {
package/src/js/surface.js CHANGED
@@ -4,6 +4,7 @@ import { EVENTS } from "./events.js";
4
4
  import { autoUpdate, reposition, repositionAt } from "./floating.js";
5
5
 
6
6
  import { CLASSES } from "./selectors.js";
7
+ import { waitForTransitions } from "./transition.js";
7
8
 
8
9
  const openSurfaces = new Set();
9
10
  const surfaceMap = new WeakMap();
@@ -46,16 +47,6 @@ function reaperFor(menu) {
46
47
  return reaper;
47
48
  }
48
49
 
49
- function waitForAnimations(...elements) {
50
- const animations = elements
51
- .filter(Boolean)
52
- .flatMap((el) =>
53
- typeof el.getAnimations === "function" ? el.getAnimations({ subtree: true }) : [],
54
- );
55
-
56
- return Promise.allSettled(animations.map((animation) => animation.finished));
57
- }
58
-
59
50
  function getSurfaceRoot(menu, anchor) {
60
51
  return anchor?.closest("dialog") || menu.closest("dialog") || menu.ownerDocument.body;
61
52
  }
@@ -360,7 +351,7 @@ export function closeSurface(menu, opts = {}) {
360
351
  state.restoreFocusTo.focus({ preventScroll: true });
361
352
  }
362
353
 
363
- waitForAnimations(menu, backdrop).then(() => {
354
+ waitForTransitions(menu, backdrop).then(() => {
364
355
  if (!state || state.closeId !== closeId) return;
365
356
  state.isSheet = false;
366
357
  menu.classList.remove(CLASSES.sheet);
@@ -0,0 +1,20 @@
1
+ /*
2
+ * CSS transition lifecycle.
3
+ *
4
+ * getAnimations() exposes both CSS transitions and keyframe animations.
5
+ * Cleanup that belongs to a state transition must not wait for arbitrary
6
+ * author animations, which may be infinite. CSSTransition instances expose
7
+ * transitionProperty; using that structural contract avoids a global check
8
+ * and keeps component JavaScript independent from the properties CSS owns.
9
+ */
10
+
11
+ export function waitForTransitions(...elements) {
12
+ const transitions = elements
13
+ .filter(Boolean)
14
+ .flatMap((element) =>
15
+ typeof element.getAnimations === "function" ? element.getAnimations({ subtree: true }) : [],
16
+ )
17
+ .filter((animation) => typeof animation.transitionProperty === "string");
18
+
19
+ return Promise.allSettled(transitions.map((transition) => transition.finished));
20
+ }