actual-css 0.5.0 → 0.7.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 (101) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/actual.css +1 -0
  3. package/dist/actual.full.css +589 -131
  4. package/dist/actual.full.js +1 -1
  5. package/dist/actual.full.min.css +1 -1
  6. package/dist/actual.min.css +1 -1
  7. package/docs/design-notes/browser-support.md +39 -14
  8. package/docs/design-notes/css-anchor.md +1 -1
  9. package/docs/design-notes/enhancement-contract.md +30 -13
  10. package/docs/design-notes/forced-colors.md +11 -7
  11. package/docs/design-notes/grid.md +4 -4
  12. package/docs/design-notes/naming.md +15 -9
  13. package/docs/design-notes/platform-alignment.md +60 -0
  14. package/docs/design-notes/popover-manual-poc.md +248 -0
  15. package/docs/design-notes/references.md +4 -0
  16. package/docs/design-notes/steps.md +748 -0
  17. package/docs/design-notes/surface-reparenting.md +122 -0
  18. package/docs/design-notes/widget-primitives.md +32 -15
  19. package/docs/navigation.json +1 -0
  20. package/docs/pages/components/alert.md +13 -8
  21. package/docs/pages/components/aura.md +102 -6
  22. package/docs/pages/components/avatar.md +9 -6
  23. package/docs/pages/components/badge.md +11 -7
  24. package/docs/pages/components/button.md +17 -17
  25. package/docs/pages/components/card.md +15 -8
  26. package/docs/pages/components/chat.md +1 -1
  27. package/docs/pages/components/dialog.md +10 -7
  28. package/docs/pages/components/drawer.md +1 -1
  29. package/docs/pages/components/fab.md +12 -10
  30. package/docs/pages/components/indicator.md +6 -6
  31. package/docs/pages/components/list.md +11 -2
  32. package/docs/pages/components/navbar.md +1 -1
  33. package/docs/pages/components/pagination.md +1 -1
  34. package/docs/pages/components/rating.md +4 -4
  35. package/docs/pages/components/skeleton.md +1 -1
  36. package/docs/pages/components/spinner.md +1 -1
  37. package/docs/pages/components/steps.md +376 -52
  38. package/docs/pages/components/table.md +5 -5
  39. package/docs/pages/enhancements/flyout.md +104 -12
  40. package/docs/pages/enhancements/overview.md +2 -2
  41. package/docs/pages/enhancements/scrollspy.md +1 -1
  42. package/docs/pages/enhancements/status-bar.md +20 -0
  43. package/docs/pages/enhancements/tabs.md +1 -1
  44. package/docs/pages/enhancements/tooltip.md +12 -0
  45. package/docs/pages/examples/overview.md +2 -0
  46. package/docs/pages/forms/choice-cards.md +4 -4
  47. package/docs/pages/forms/combobox.md +87 -0
  48. package/docs/pages/forms/floating-field.md +5 -3
  49. package/docs/pages/forms/joined-controls.md +4 -4
  50. package/docs/pages/forms/labels.md +8 -5
  51. package/docs/pages/forms/overview.md +25 -18
  52. package/docs/pages/forms/password.md +3 -2
  53. package/docs/pages/forms/range.md +13 -2
  54. package/docs/pages/forms/states-and-sizes.md +6 -4
  55. package/docs/pages/forms/validation.md +13 -9
  56. package/docs/pages/foundations/philosophy.md +15 -1
  57. package/docs/pages/foundations/theming.md +8 -0
  58. package/docs/pages/foundations/tokens.md +9 -6
  59. package/docs/pages/foundations/typography.md +1 -1
  60. package/docs/pages/guides/bootstrap.md +3 -3
  61. package/docs/pages/guides/introduction.md +1 -1
  62. package/docs/pages/guides/modular-import.md +21 -10
  63. package/docs/pages/guides/progressive-enhancement.md +96 -73
  64. package/docs/pages/guides/tailwind.md +2 -2
  65. package/docs/pages/layout/app-shell.md +4 -4
  66. package/docs/pages/layout/choosing.md +10 -6
  67. package/docs/pages/layout/grid.md +11 -10
  68. package/docs/pages/layout/stack.md +10 -6
  69. package/docs/pages/patterns/structured-lists.md +7 -7
  70. package/docs/pages/utilities/accessibility.md +1 -1
  71. package/docs/pages/utilities/flex.md +20 -13
  72. package/docs/pages/utilities/links.md +1 -1
  73. package/docs/pages/utilities/list.md +1 -1
  74. package/docs/pages/utilities/overflow.md +14 -7
  75. package/docs/pages/utilities/overview.md +22 -0
  76. package/docs/pages/utilities/shape.md +1 -1
  77. package/docs/pages/utilities/sizing-wrapping.md +10 -8
  78. package/docs/pages/utilities/spacing.md +26 -19
  79. package/docs/pages/utilities/text.md +1 -1
  80. package/llms.txt +27 -4
  81. package/package.json +7 -4
  82. package/scripts/reserved-classes.json +4 -1
  83. package/src/css/components/fab.css +44 -8
  84. package/src/css/components/flyout.css +49 -3
  85. package/src/css/components/join.css +7 -0
  86. package/src/css/components/list.css +3 -1
  87. package/src/css/components/menu.css +16 -10
  88. package/src/css/components/steps.css +277 -69
  89. package/src/css/components/tooltip.css +37 -1
  90. package/src/css/core/tokens.css +1 -0
  91. package/src/css/effects/aura.css +101 -11
  92. package/src/css/forms/custom-select.css +1 -1
  93. package/src/css/forms/floating-field.css +8 -6
  94. package/src/css/layout/container-query.css +37 -13
  95. package/src/css/layout/grid.css +7 -7
  96. package/src/css/layout/index.css +1 -1
  97. package/src/js/password.js +1 -1
  98. package/src/js/status.js +38 -2
  99. package/src/js/surface.js +28 -12
  100. package/src/js/tooltip.js +2 -2
  101. package/src/js/floating.js +0 -432
@@ -7,8 +7,10 @@
7
7
  * messages stay in the surrounding .field, not inside the cell, so the
8
8
  * resting label can center on the control alone.
9
9
  *
10
- * The control must precede the label: the float is driven by the sibling
11
- * combinator and :placeholder-shown. Every .input/.textarea needs
10
+ * The control must precede the label: the float is driven by the general
11
+ * sibling combinator and :placeholder-shown. Injected nodes may appear between
12
+ * the control and the label (password managers, extension wrappers); the cell
13
+ * contract stays a single control plus one label. Every .input/.textarea needs
12
14
  * placeholder=" " to expose a reliable empty state — without it the label
13
15
  * only reacts to focus.
14
16
  *
@@ -56,14 +58,14 @@
56
58
  }
57
59
 
58
60
  /* Single-line controls rest in the middle until they receive a value. */
59
- .floating-field > :is(.input, .select) + .field-label {
61
+ .floating-field > :is(.input, .select) ~ .field-label {
60
62
  inset-block-start: 50%;
61
63
  transform: translateY(-50%);
62
64
  }
63
65
 
64
66
  /* An empty textarea label follows the control's normal first-line position;
65
67
  the control itself keeps the extra headroom reserved for its floated state. */
66
- .floating-field > .textarea + .field-label {
68
+ .floating-field > .textarea ~ .field-label {
67
69
  inset-block-start: calc(1em + var(--border-width));
68
70
  transform: none;
69
71
  }
@@ -72,7 +74,7 @@
72
74
  not have propagated the value to the placeholder state yet. */
73
75
  .floating-field
74
76
  > :is(.input, .textarea):is(:focus, :autofill, :not(:placeholder-shown))
75
- + .field-label {
77
+ ~ .field-label {
76
78
  inset-block-start: var(--floating-label-top);
77
79
  transform: none;
78
80
  font-size: var(--font-size-xs);
@@ -91,7 +93,7 @@
91
93
  .input[type="month"],
92
94
  .input[type="week"]
93
95
  )
94
- + .field-label {
96
+ ~ .field-label {
95
97
  inset-block-start: var(--floating-label-top);
96
98
  transform: none;
97
99
  font-size: var(--font-size-xs);
@@ -1,15 +1,39 @@
1
- /* Convenience helper: establishes a query container named actual-grid for
2
- descendants. Use it for a component that changes representation according
3
- to its allocated inline size, and to give .grid-N the named context it needs
4
- for balanced subdivision (see grid.css). The name matters: grids only respond
5
- to an explicitly actual-grid-named container, never to an incidental
6
- query container. Layout primitives are responsive without it; this adds
7
- precision, it is never a prerequisite.
8
-
9
- The element takes inline-size containment, so put it on a normal
10
- block-level wrapper, never on a shrink-to-fit item (a .cluster child, a
11
- float, an inline-block): its contents would stop contributing to its own
12
- inline size and it would collapse. */
1
+ /* Convenience helper for size-aware components: establishes an inline-size
2
+ query container named actual-container. The name is the contract, not this
3
+ class — an author can declare `container: actual-container / inline-size` on
4
+ their own region and get the same result.
5
+
6
+ actual-container is shared. Granting it to a region may enable size-aware
7
+ enhancements for any Actual component inside that region, not only the one
8
+ that motivated the container: .grid-N reads it for balanced subdivision,
9
+ .steps for its label collapse, and future components will read the same
10
+ context. That is the scope of the API, not a caveat.
11
+
12
+ An anonymous query container is not part of it. A region that declares
13
+ `container-type: inline-size` for the application's own purposes carries no
14
+ actual-container name, so no framework component changes behaviour there.
15
+ Opting into the protocol is always explicit. Nothing here is a prerequisite
16
+ either — every primitive is responsive without it; this adds precision.
17
+
18
+ Place it where the width you want measured lives: usually a wrapper for a
19
+ grid, whose queried children sit inside it, and the component itself for
20
+ .steps, whose own row is the width in question.
21
+
22
+ The rule for where it is safe is the containment rule, not a list of
23
+ primitives. Query containment removes the element's intrinsic inline-size
24
+ contribution, so:
25
+
26
+ safe the element's width is imposed by its context
27
+ (.steps sets inline-size: 100%, so it survives even as a
28
+ .cluster child — it wraps and fills)
29
+
30
+ unsafe the element's width depends on its contents' intrinsic
31
+ contribution (a bare .grid child of .cluster measures 0 once
32
+ contained, as does a float, inline-block, table, grid auto track,
33
+ max-content parent, or an abspos box sized by its contents)
34
+
35
+ So .cluster is neither safe nor unsafe; what matters is where the child's
36
+ width comes from. */
13
37
  .container-query {
14
- container: actual-grid / inline-size;
38
+ container: actual-container / inline-size;
15
39
  }
@@ -86,13 +86,13 @@
86
86
  min-inline-size: 0;
87
87
  }
88
88
 
89
- /* Balanced subdivision, activated by an ancestor container named actual-grid.
89
+ /* Balanced subdivision, activated by an ancestor container named actual-container.
90
90
  * A .grid-N inside such a container enters only divisor states of N. The name
91
91
  * is deliberate: a grid must not key off the nearest anonymous query container,
92
92
  * or a region that sets container-type: inline-size for an unrelated reason
93
93
  * would silently change the grid. The author opts the grid in by naming the
94
94
  * context — .container-query is the official helper, or declare
95
- * container: actual-grid / inline-size on your own region.
95
+ * container: actual-container / inline-size on your own region.
96
96
  *
97
97
  * .grid-2 2 -> 1
98
98
  * .grid-3 3 -> 1
@@ -109,7 +109,7 @@
109
109
  * inline contribution and collapses it inside .cluster, .topbar, floats, and
110
110
  * any shrink-to-fit box. See docs/design-notes/grid.md.
111
111
  *
112
- * Without an actual-grid ancestor none of these @container rules resolve, so
112
+ * Without an actual-container ancestor none of these @container rules resolve, so
113
113
  * the auto-fill baseline above stays. The min-width: 0 rule is the lowest
114
114
  * step: it pins the grid to one column inside a container before any larger
115
115
  * threshold fires (a bare grid would otherwise show a 2 + 1 orphan at 28-48rem).
@@ -125,25 +125,25 @@
125
125
  * Ascending min-width only: the last matching step wins by cascade order.
126
126
  */
127
127
  @supports (container-type: inline-size) {
128
- @container actual-grid (min-width: 0px) {
128
+ @container actual-container (min-width: 0px) {
129
129
  :where(.grid-2, .grid-3, .grid-4, .grid-6) {
130
130
  grid-template-columns: 1fr;
131
131
  }
132
132
  }
133
133
 
134
- @container actual-grid (min-width: 28rem) {
134
+ @container actual-container (min-width: 28rem) {
135
135
  :where(.grid-2, .grid-4, .grid-6) {
136
136
  grid-template-columns: repeat(2, minmax(0, 1fr));
137
137
  }
138
138
  }
139
139
 
140
- @container actual-grid (min-width: 48rem) {
140
+ @container actual-container (min-width: 48rem) {
141
141
  :where(.grid-3, .grid-6) {
142
142
  grid-template-columns: repeat(3, minmax(0, 1fr));
143
143
  }
144
144
  }
145
145
 
146
- @container actual-grid (min-width: 64rem) {
146
+ @container actual-container (min-width: 64rem) {
147
147
  :where(.grid-4) {
148
148
  grid-template-columns: repeat(4, minmax(0, 1fr));
149
149
  }
@@ -14,7 +14,7 @@
14
14
  * .sidebar-layout main content + secondary aside
15
15
  * .grid responsive collection; item width is the contract
16
16
  * .grid-N structural density of N; balanced inside a container named
17
- * actual-grid (in grid.css)
17
+ * actual-container (in grid.css)
18
18
  * .column-layout explicit 12-column region composition; the author owns
19
19
  * narrow-container recomposition
20
20
  * .scroller theme-aware scrollbar density and colour
@@ -92,7 +92,7 @@ if (typeof document !== "undefined") {
92
92
  submitRoots.add(document);
93
93
  document.addEventListener("submit", handleSubmit);
94
94
 
95
- window.addEventListener("pagehide", () => {
95
+ document.defaultView?.addEventListener("pagehide", () => {
96
96
  for (const input of revealed) {
97
97
  // Opportunistic housekeeping: an input removed from the DOM before
98
98
  // navigation no longer needs reverting and can leave the set.
package/src/js/status.js CHANGED
@@ -31,6 +31,12 @@
31
31
  * - Any other code, without importing this module: dispatch `actual:status`
32
32
  * directly — `detail: { message, intent, duration }`, or `{}` to clear.
33
33
  *
34
+ * A message originating inside an open dialog temporarily mounts the singleton
35
+ * bar in that dialog. Content outside a modal dialog is inert and cannot sit
36
+ * above its top layer, so this keeps both the visual feedback and live region
37
+ * in the active subtree. Programmatic callers pass the originating element as
38
+ * `options.source`; event and command entry points propagate it automatically.
39
+ *
34
40
  * The element contract is `[data-status][role="status"]`. Both are required:
35
41
  * "status" is a generic domain word, so `role="status"` keeps the
36
42
  * document-wide lookup from matching an unrelated app element that happens to
@@ -59,12 +65,39 @@ let statusTimer;
59
65
  // never emptied by the previous message's cleanup.
60
66
  let statusGeneration = 0;
61
67
  let viewportTracker = null;
68
+ const statusHomes = new WeakMap();
62
69
 
63
70
  function statusTarget() {
64
71
  if (typeof document === "undefined") return null;
65
72
  return document.querySelector(STATUS_SELECTOR);
66
73
  }
67
74
 
75
+ function restoreStatusTarget(target) {
76
+ const home = statusHomes.get(target);
77
+ if (!home) return;
78
+
79
+ const parent = home.parent?.isConnected ? home.parent : target.ownerDocument?.body;
80
+ if (parent) {
81
+ const reference = home.nextSibling?.parentNode === parent ? home.nextSibling : null;
82
+ parent.insertBefore(target, reference);
83
+ }
84
+ statusHomes.delete(target);
85
+ }
86
+
87
+ function mountStatusTarget(target, source) {
88
+ const dialog = source?.closest?.("dialog[open]");
89
+ if (!dialog || dialog.ownerDocument !== target.ownerDocument) {
90
+ restoreStatusTarget(target);
91
+ return;
92
+ }
93
+ if (dialog.contains(target)) return;
94
+
95
+ if (!statusHomes.has(target)) {
96
+ statusHomes.set(target, { parent: target.parentNode, nextSibling: target.nextSibling });
97
+ }
98
+ dialog.append(target);
99
+ }
100
+
68
101
  // The bar is fixed to the layout viewport, so the mobile software keyboard
69
102
  // covers it. visualViewport reports the occluded band; the CSS adds it to the
70
103
  // bottom inset. Tracking runs only while a message is on screen.
@@ -130,6 +163,8 @@ export function status(message, options = {}) {
130
163
  clearTimeout(statusTimer);
131
164
  statusGeneration++;
132
165
 
166
+ mountStatusTarget(target, options.source);
167
+
133
168
  clearIntentClasses(target);
134
169
  const intent = intentClasses(options.intent);
135
170
  if (intent.length) {
@@ -168,6 +203,7 @@ status.clear = function clear() {
168
203
  target.textContent = "";
169
204
  clearIntentClasses(target);
170
205
  releaseViewport();
206
+ restoreStatusTarget(target);
171
207
  });
172
208
  };
173
209
 
@@ -198,7 +234,7 @@ function connectStatusTrigger(trigger, target) {
198
234
  if (typeof document !== "undefined") {
199
235
  document.addEventListener(EVENTS.invalid, (event) => {
200
236
  const message = event.detail?.message;
201
- if (message) status(message, { intent: "danger" });
237
+ if (message) status(message, { intent: "danger", source: event.target });
202
238
  });
203
239
 
204
240
  // Public contract: any code can trigger the status bar this way, without
@@ -209,7 +245,7 @@ if (typeof document !== "undefined") {
209
245
  // paths end in the same exit, so a detail that lost its message never
210
246
  // strands a stale one on screen.
211
247
  if (message != null) {
212
- status(message, { intent, duration });
248
+ status(message, { intent, duration, source: event.target });
213
249
  } else {
214
250
  status.clear();
215
251
  }
package/src/js/surface.js CHANGED
@@ -1,7 +1,7 @@
1
+ import { autoUpdate, reposition, repositionAt } from "@lekoala/floating";
1
2
  import enhance from "./enhance.js";
2
3
  import { registerEscapeDismissal } from "./escape.js";
3
4
  import { EVENTS } from "./events.js";
4
- import { autoUpdate, reposition, repositionAt } from "./floating.js";
5
5
 
6
6
  import { CLASSES } from "./selectors.js";
7
7
  import { waitForTransitions } from "./transition.js";
@@ -210,6 +210,10 @@ function startSurfaceResources(menu, state) {
210
210
  const armKey = (event) => {
211
211
  if (!event.defaultPrevented && SCROLL_KEYS.has(event.key)) arm(event);
212
212
  };
213
+ const dismissOnScroll = (event) => {
214
+ if (state.scrollIntentAt == null || menu.contains(event.target)) return;
215
+ closeSurface(menu);
216
+ };
213
217
  const captureOptions = { signal: controller.signal, capture: true };
214
218
  const passiveCaptureOptions = { ...captureOptions, passive: true };
215
219
  const doc = menu.ownerDocument;
@@ -217,20 +221,13 @@ function startSurfaceResources(menu, state) {
217
221
  doc.addEventListener("touchmove", arm, passiveCaptureOptions);
218
222
  doc.addEventListener("wheel", arm, passiveCaptureOptions);
219
223
  doc.addEventListener("keydown", armKey, { signal: controller.signal });
224
+ doc.addEventListener("scroll", dismissOnScroll, passiveCaptureOptions);
220
225
  state.stopScrollIntent = () => controller.abort();
221
226
  }
222
- state.stopTracking = autoUpdate(menu, ({ type, targets, timeStamp }) => {
227
+ const anchor = state.point ? null : state.trigger || state.source;
228
+ state.stopTracking = autoUpdate(anchor, menu, ({ type }) => {
223
229
  if (menu.hidden) return;
224
- if (
225
- type === "scroll" &&
226
- state.dismissOnScroll &&
227
- state.scrollIntentAt != null &&
228
- timeStamp >= state.scrollIntentAt &&
229
- [...targets].some((target) => target !== menu && !menu.contains(target))
230
- ) {
231
- closeSurface(menu);
232
- return;
233
- }
230
+ if (type === "scroll" && state.dismissOnScroll) return;
234
231
  applyPresentation(menu, state);
235
232
  if (!positionSurface(menu)) closeSurface(menu);
236
233
  });
@@ -249,11 +246,30 @@ export function prepareSurface(menu) {
249
246
  reaperFor(menu)?.refresh(menu);
250
247
  }
251
248
 
249
+ /*
250
+ * One lifecycle owner per surface. surface.js opens by clearing [hidden] and
251
+ * adding .is-open; it never calls showPopover(). So a [popover] panel handed to
252
+ * the runtime has two owners and stays shut: the platform keeps a popover it
253
+ * was never asked to show closed, whatever the runtime writes.
254
+ *
255
+ * This covers every popover value. "manual" is a candidate future transport for
256
+ * this module, which does not make it a legitimate mix today. Delete this guard
257
+ * in the commit that makes surface.js call showPopover().
258
+ */
259
+ function warnPopoverConflict(panel) {
260
+ if (!panel.hasAttribute("popover")) return;
261
+ console.warn(
262
+ "Actual: the surface lifecycle cannot own a [popover] element — choose one lifecycle owner (remove the popover attribute, or drop the Actual enhancement).",
263
+ panel,
264
+ );
265
+ }
266
+
252
267
  export function retainSurface(panel) {
253
268
  if (!panel) return () => {};
254
269
 
255
270
  let entry = surfaceRetainers.get(panel);
256
271
  if (!entry) {
272
+ warnPopoverConflict(panel);
257
273
  prepareSurface(panel);
258
274
  entry = { count: 0 };
259
275
  surfaceRetainers.set(panel, entry);
package/src/js/tooltip.js CHANGED
@@ -27,9 +27,9 @@
27
27
  * Hide: blur, pointer leave, Escape
28
28
  */
29
29
 
30
+ import { autoUpdate, reposition } from "@lekoala/floating";
30
31
  import enhance from "./enhance.js";
31
32
  import { registerEscapeDismissal } from "./escape.js";
32
- import { autoUpdate, reposition } from "./floating.js";
33
33
  import { CLASSES } from "./selectors.js";
34
34
 
35
35
  const SHOW_DELAY_MS = 150;
@@ -163,7 +163,7 @@ function updateTrackedTip(tip, state) {
163
163
  function startTracking(tip, state) {
164
164
  if (state.stopTracking) return;
165
165
 
166
- state.stopTracking = autoUpdate(tip, () => updateTrackedTip(tip, state));
166
+ state.stopTracking = autoUpdate(state.activeRef, tip, () => updateTrackedTip(tip, state));
167
167
  }
168
168
 
169
169
  function stopTracking(state) {