actual-css 0.9.2 → 0.11.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 (136) hide show
  1. package/CHANGELOG.md +169 -1
  2. package/README.md +65 -44
  3. package/components.json +35 -14
  4. package/dist/actual.full.css +877 -583
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/app-ui.md +1 -1
  8. package/docs/design-notes/column-layout.md +103 -520
  9. package/docs/design-notes/enhancement-contract.md +16 -7
  10. package/docs/design-notes/forced-colors.md +12 -1
  11. package/docs/design-notes/naming.md +1 -1
  12. package/docs/design-notes/platform-alignment.md +8 -4
  13. package/docs/design-notes/rich-text.md +61 -0
  14. package/docs/design-notes/steps.md +2 -2
  15. package/docs/design-notes/surface-reparenting.md +8 -9
  16. package/docs/navigation.json +4 -0
  17. package/docs/pages/components/accordion.md +55 -1
  18. package/docs/pages/components/alert.md +9 -15
  19. package/docs/pages/components/aura.md +12 -13
  20. package/docs/pages/components/badge.md +16 -7
  21. package/docs/pages/components/bleed.md +42 -0
  22. package/docs/pages/components/button.md +30 -4
  23. package/docs/pages/components/card.md +21 -86
  24. package/docs/pages/components/chat.md +5 -0
  25. package/docs/pages/components/close.md +48 -0
  26. package/docs/pages/components/data-list.md +26 -1
  27. package/docs/pages/components/dialog.md +21 -18
  28. package/docs/pages/components/drawer.md +9 -9
  29. package/docs/pages/components/navbar.md +5 -5
  30. package/docs/pages/components/pagination.md +15 -15
  31. package/docs/pages/components/shimmer.md +57 -0
  32. package/docs/pages/components/skeleton.md +2 -1
  33. package/docs/pages/components/spinner.md +14 -9
  34. package/docs/pages/components/table.md +13 -0
  35. package/docs/pages/enhancements/flyout.md +2 -0
  36. package/docs/pages/enhancements/overview.md +5 -0
  37. package/docs/pages/enhancements/tabs.md +4 -0
  38. package/docs/pages/enhancements/tooltip.md +3 -1
  39. package/docs/pages/examples/overview.md +2 -1
  40. package/docs/pages/forms/choice-cards.md +116 -4
  41. package/docs/pages/forms/combobox.md +39 -12
  42. package/docs/pages/forms/joined-controls.md +2 -5
  43. package/docs/pages/forms/overview.md +10 -1
  44. package/docs/pages/forms/range.md +110 -5
  45. package/docs/pages/forms/rich-text.md +110 -0
  46. package/docs/pages/forms/validation.md +5 -1
  47. package/docs/pages/foundations/theming.md +110 -18
  48. package/docs/pages/foundations/tokens.md +41 -13
  49. package/docs/pages/guides/bootstrap.md +2 -1
  50. package/docs/pages/guides/introduction.md +15 -23
  51. package/docs/pages/guides/modular-import.md +54 -7
  52. package/docs/pages/guides/progressive-enhancement.md +19 -17
  53. package/docs/pages/guides/tailwind.md +21 -7
  54. package/docs/pages/layout/column-layout.md +92 -54
  55. package/docs/pages/layout/media.md +1 -1
  56. package/docs/pages/layout/overview.md +1 -1
  57. package/docs/pages/utilities/overview.md +1 -1
  58. package/docs/pages/utilities/sizing-wrapping.md +5 -3
  59. package/llms.txt +25 -4
  60. package/package.json +10 -6
  61. package/reserved-classes.json +6 -30
  62. package/skills/actual-css/PROJECT.md.example +65 -0
  63. package/skills/actual-css/README.md +64 -0
  64. package/skills/actual-css/SKILL.md +234 -0
  65. package/skills/actual-css/references/accessibility.md +44 -0
  66. package/skills/actual-css/references/api-and-vocabulary.md +94 -0
  67. package/skills/actual-css/references/enhancements.md +59 -0
  68. package/skills/actual-css/references/forms.md +49 -0
  69. package/skills/actual-css/references/layout-and-theming.md +82 -0
  70. package/skills/actual-css/references/review-checklist.md +51 -0
  71. package/skills/actual-css/scripts/inspect-actual-css.mjs +171 -0
  72. package/src/css/components/accordion.css +44 -7
  73. package/src/css/components/alert.css +23 -43
  74. package/src/css/components/app-nav.css +9 -0
  75. package/src/css/components/avatar.css +1 -1
  76. package/src/css/components/badge.css +11 -39
  77. package/src/css/components/bleed.css +79 -0
  78. package/src/css/components/button.css +6 -17
  79. package/src/css/components/card.css +7 -40
  80. package/src/css/components/close.css +91 -0
  81. package/src/css/components/data-list.css +18 -0
  82. package/src/css/components/drawer.css +15 -56
  83. package/src/css/components/fab.css +10 -0
  84. package/src/css/components/flyout.css +5 -1
  85. package/src/css/components/index.css +2 -0
  86. package/src/css/components/join.css +36 -20
  87. package/src/css/components/menu.css +12 -0
  88. package/src/css/components/meter.css +1 -1
  89. package/src/css/components/modal.css +10 -57
  90. package/src/css/components/navbar.css +27 -0
  91. package/src/css/components/pagination.css +3 -2
  92. package/src/css/components/rating.css +1 -1
  93. package/src/css/components/status-bar.css +2 -2
  94. package/src/css/components/steps.css +3 -3
  95. package/src/css/components/tab.css +3 -3
  96. package/src/css/components/table.css +6 -0
  97. package/src/css/components/tooltip.css +1 -1
  98. package/src/css/core/focus.css +19 -3
  99. package/src/css/core/theme.css +33 -43
  100. package/src/css/core/tokens.css +9 -5
  101. package/src/css/core/variants.css +5 -34
  102. package/src/css/effects/aura.css +51 -33
  103. package/src/css/effects/index.css +1 -0
  104. package/src/css/effects/shimmer.css +57 -0
  105. package/src/css/forms/choice-card.css +142 -24
  106. package/src/css/forms/choice.css +2 -3
  107. package/src/css/forms/control.css +17 -10
  108. package/src/css/forms/custom-select.css +4 -4
  109. package/src/css/forms/native.css +4 -4
  110. package/src/css/forms/otp.css +9 -1
  111. package/src/css/forms/range.css +72 -14
  112. package/src/css/forms/switch.css +2 -3
  113. package/src/css/layout/app-layout.css +1 -1
  114. package/src/css/layout/column-layout.css +26 -111
  115. package/src/css/utilities/extra.css +7 -6
  116. package/src/js/command.js +61 -32
  117. package/src/js/dialog.js +103 -47
  118. package/src/js/dismiss.js +1 -2
  119. package/src/js/enhance.js +91 -126
  120. package/src/js/escape.js +1 -0
  121. package/src/js/flyout.js +10 -1
  122. package/src/js/focus-group.js +2 -1
  123. package/src/js/full.js +1 -0
  124. package/src/js/keys.js +14 -0
  125. package/src/js/menu.js +2 -1
  126. package/src/js/password.js +1 -8
  127. package/src/js/range.js +151 -0
  128. package/src/js/scrollspy.js +4 -5
  129. package/src/js/status.js +21 -10
  130. package/src/js/surface.js +12 -8
  131. package/src/js/tab.js +2 -1
  132. package/src/js/tooltip.js +29 -2
  133. package/src/js/validation.js +8 -15
  134. package/dist/actual.css +0 -876
  135. package/dist/actual.js +0 -1
  136. package/dist/actual.min.css +0 -1
package/src/js/status.js CHANGED
@@ -67,6 +67,21 @@ let statusTimer;
67
67
  let statusGeneration = 0;
68
68
  let viewportTracker = null;
69
69
  const statusHomes = new WeakMap();
70
+ // Reference kept only while the bar is temporarily mounted in a dialog. If
71
+ // that dialog is removed from the DOM (htmx/Turbo swap), document lookup can
72
+ // no longer find the bar — this reference lets the next status()/clear()
73
+ // restore it to its home instead of silently no-opping forever.
74
+ let mountedStatusTarget = null;
75
+
76
+ function healDetachedStatusTarget() {
77
+ if (!mountedStatusTarget || mountedStatusTarget.isConnected) return;
78
+ if (!statusHomes.has(mountedStatusTarget)) {
79
+ mountedStatusTarget = null;
80
+ return;
81
+ }
82
+ restoreStatusTarget(mountedStatusTarget);
83
+ mountedStatusTarget = null;
84
+ }
70
85
 
71
86
  function statusTarget() {
72
87
  if (typeof document === "undefined") return null;
@@ -83,6 +98,7 @@ function restoreStatusTarget(target) {
83
98
  parent.insertBefore(target, reference);
84
99
  }
85
100
  statusHomes.delete(target);
101
+ if (mountedStatusTarget === target) mountedStatusTarget = null;
86
102
  }
87
103
 
88
104
  function mountStatusTarget(target, source) {
@@ -97,6 +113,7 @@ function mountStatusTarget(target, source) {
97
113
  statusHomes.set(target, { parent: target.parentNode, nextSibling: target.nextSibling });
98
114
  }
99
115
  dialog.append(target);
116
+ mountedStatusTarget = target;
100
117
  }
101
118
 
102
119
  // The bar is fixed to the layout viewport, so the mobile software keyboard
@@ -149,6 +166,7 @@ function clearIntentClasses(target) {
149
166
  }
150
167
 
151
168
  export function status(message, options = {}) {
169
+ healDetachedStatusTarget();
152
170
  const target = statusTarget();
153
171
  if (!target || message == null) return;
154
172
 
@@ -186,6 +204,7 @@ export function status(message, options = {}) {
186
204
 
187
205
  status.clear = function clear() {
188
206
  clearTimeout(statusTimer);
207
+ healDetachedStatusTarget();
189
208
 
190
209
  const target = statusTarget();
191
210
  if (!target) {
@@ -228,10 +247,6 @@ function resolveStatusTrigger(trigger) {
228
247
  return target && target === statusTarget() ? target : null;
229
248
  }
230
249
 
231
- function connectStatusTrigger(trigger, target) {
232
- trigger.setAttribute("aria-controls", target.id);
233
- }
234
-
235
250
  if (typeof document !== "undefined") {
236
251
  document.addEventListener(EVENTS.invalid, (event) => {
237
252
  const message = event.detail?.message;
@@ -254,9 +269,7 @@ if (typeof document !== "undefined") {
254
269
 
255
270
  registerCommands(STATUS_SHOW_COMMANDS, {
256
271
  resolve: resolveStatusTrigger,
257
- prepare: connectStatusTrigger,
258
- handle: (event, trigger) => {
259
- event.preventDefault();
272
+ handle: (_event, trigger) => {
260
273
  dispatchStatusEvent(trigger, {
261
274
  message: trigger.getAttribute("data-status-message"),
262
275
  intent: trigger.getAttribute("data-status-intent") ?? undefined,
@@ -267,9 +280,7 @@ if (typeof document !== "undefined") {
267
280
 
268
281
  registerCommands(STATUS_CLEAR_COMMANDS, {
269
282
  resolve: resolveStatusTrigger,
270
- prepare: connectStatusTrigger,
271
- handle: (event, trigger) => {
272
- event.preventDefault();
283
+ handle: (_event, trigger) => {
273
284
  dispatchStatusEvent(trigger, {});
274
285
  },
275
286
  });
package/src/js/surface.js CHANGED
@@ -222,7 +222,10 @@ function startSurfaceResources(menu, state) {
222
222
  }
223
223
 
224
224
  export function isSurfaceOpen(menu) {
225
- return menu.classList.contains(CLASSES.open);
225
+ // The open set is the source of truth; .is-open is only its visual
226
+ // projection. A morph touching classes must not orphan Escape entries,
227
+ // position tracking, or the open set.
228
+ return openSurfaces.has(menu);
226
229
  }
227
230
 
228
231
  export function prepareSurface(menu) {
@@ -304,8 +307,14 @@ export function openSurface(menu, opts = {}) {
304
307
  state.coordinateSpace = mode.space;
305
308
  menu.style.position = mode.position;
306
309
 
310
+ // Register before promoting: a position failure below closes the surface
311
+ // again, and that close must observe the open state to tear down.
312
+ openSurfaces.add(menu);
307
313
  // Promote before measuring: a closed popover has no box to position.
308
- if (!showTransport(menu)) return false;
314
+ if (!showTransport(menu)) {
315
+ openSurfaces.delete(menu);
316
+ return false;
317
+ }
309
318
  menu.classList.add(CLASSES.open);
310
319
  syncExpanded(menu, true);
311
320
 
@@ -335,13 +344,12 @@ export function openSurface(menu, opts = {}) {
335
344
  * from; placing focus in the new surface is the caller's job, and both
336
345
  * callers in tree do it as soon as this returns.
337
346
  */
338
- for (const other of openSurfaces) {
347
+ for (const other of [...openSurfaces]) {
339
348
  if (other !== menu && other.ownerDocument === menu.ownerDocument) {
340
349
  closeSurface(other, { restoreFocus: false });
341
350
  }
342
351
  }
343
352
 
344
- openSurfaces.add(menu);
345
353
  startSurfaceResources(menu, state);
346
354
  return true;
347
355
  }
@@ -411,7 +419,3 @@ function ensureDocumentClick(menu) {
411
419
  clickBoundDocuments.add(doc);
412
420
  doc.addEventListener("click", onDocumentClick);
413
421
  }
414
-
415
- export function getSurfaceAutoClose(menu) {
416
- return surfaceMap.get(menu)?.autoClose ?? "outside";
417
- }
package/src/js/tab.js CHANGED
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { registerEnhancement } from "./enhance.js";
20
- import { itemForKey } from "./keys.js";
20
+ import { itemForKey, shouldIgnoreKey } from "./keys.js";
21
21
 
22
22
  const TABLIST_SELECTOR = '[data-enhance~="tabs"]';
23
23
 
@@ -100,6 +100,7 @@ function initialize(list) {
100
100
 
101
101
  function onKeydown(e) {
102
102
  const list = e.currentTarget;
103
+ if (shouldIgnoreKey(e)) return;
103
104
  const tab = e.target.closest('[role="tab"]');
104
105
  if (!tab || tab.closest(TABLIST_SELECTOR) !== list) return;
105
106
 
package/src/js/tooltip.js CHANGED
@@ -23,7 +23,16 @@
23
23
  * for tooltips that are never triggered. AJAX-loaded triggers
24
24
  * work automatically via bubbling delegated listeners.
25
25
  *
26
- * Show: hover + focus (150ms delay), click toggle, or always visible
26
+ * Snapshot, not reactive: generated tooltip text is synchronized when
27
+ * the tooltip is shown. Changes to data-tooltip do not rewrite
28
+ * a tooltip that is already shown. Always-visible generated
29
+ * tooltips therefore keep their initial text; use an explicit
30
+ * tooltip when its content must update dynamically.
31
+ *
32
+ * Show: hover + keyboard focus (150ms delay), click toggle, or always
33
+ * visible. Focus counts only when it matches :focus-visible, so
34
+ * a pointer press, or focus handed back by a closing dialog after
35
+ * a pointer close, shows nothing.
27
36
  * Hide: focus leaving the trigger (focusout), pointer leave, Escape
28
37
  *
29
38
  * Geometry: a trigger scrolled out of the positioning boundary takes its tip
@@ -367,7 +376,10 @@ function ensureTip(trigger) {
367
376
  const text = trigger.getAttribute("data-tooltip");
368
377
  const doc = trigger.ownerDocument;
369
378
 
370
- // shorthand: data-tooltip="text" → create element lazily
379
+ // shorthand: data-tooltip="text" → create element lazily.
380
+ // Shorthand content is always text. Never use innerHTML here:
381
+ // data-tooltip may hold application/user-provided strings. Rich content
382
+ // belongs in an explicit author-owned tooltip.
371
383
  if (text) {
372
384
  tip = doc.createElement("div");
373
385
  tip.className = CLASSES.tooltip;
@@ -467,6 +479,14 @@ function show(tip, ref, immediate = false) {
467
479
  const state = tipStates.get(tip);
468
480
  if (!state) return;
469
481
 
482
+ // Shorthand tips copy data-tooltip text at creation; re-read it on every
483
+ // show so a "Copy" → "Copied!" update is reflected without rewiring.
484
+ // Text only, never innerHTML — see the note at creation above.
485
+ if (state.generated) {
486
+ const text = ref.getAttribute("data-tooltip");
487
+ if (text && tip.textContent !== text) tip.textContent = text;
488
+ }
489
+
470
490
  state.activeRef = ref;
471
491
  const mode = positionModeFor(ref);
472
492
  state.space = mode.space;
@@ -502,6 +522,13 @@ function handleTriggerIntent(e) {
502
522
  )
503
523
  return;
504
524
 
525
+ // why: focus is a show intent only when it is keyboard focus. A pointer
526
+ // press focuses the trigger too, and a dialog or drawer closed with the
527
+ // pointer hands focus back to its invoker; neither asks for a label the
528
+ // user just acted on. :focus-visible is the browser's own modality verdict,
529
+ // the one the focus rings already follow, so Tab still shows the tip.
530
+ if (e.type === "focusin" && !e.target.matches(":focus-visible")) return;
531
+
505
532
  const tip = ensureTip(trigger);
506
533
  if (!tip) return;
507
534
 
@@ -11,19 +11,20 @@
11
11
  * Opt in with data-enhance="validation". The .needs-validation class
12
12
  * remains valid for presentation-only (CSS :user-invalid feedback without
13
13
  * the JS behavior). Importing the module registers the behavior; there is
14
- * no init call.
14
+ * no init call. To opt existing forms in by selector, use
15
+ * applyEnhancement("validation", selector) so they join the enhance()
16
+ * lifecycle (inserted forms connect, removed forms tear down).
15
17
  *
16
18
  * The marker is a static, init-time contract. Removing the form from the
17
19
  * DOM tears down everything, including the managed novalidate attribute.
18
20
  */
19
21
 
20
- import enhance, { enhancementSelector } from "./enhance.js";
22
+ import { registerEnhancement } from "./enhance.js";
21
23
  import { EVENTS } from "./events.js";
22
24
  import { CLASSES } from "./selectors.js";
23
25
 
24
26
  const NOVALIDATE = "novalidate";
25
27
  const WAS_VALIDATED_CLASS = CLASSES.wasValidated;
26
- const VALIDATION_SELECTOR = enhancementSelector("validation");
27
28
  const FIELD_CLASS = CLASSES.field;
28
29
  const DANGER_CLASS = CLASSES.danger;
29
30
  const MANAGED_NOVALIDATE_ATTR = "validationManagedNovalidate";
@@ -398,13 +399,6 @@ export class FormValidator {
398
399
  rules[name] = callback;
399
400
  }
400
401
 
401
- static init(selector = `form${VALIDATION_SELECTOR}`) {
402
- if (typeof document === "undefined") return;
403
- for (const form of document.querySelectorAll(selector)) {
404
- if (isFormElement(form)) connectForm(form);
405
- }
406
- }
407
-
408
402
  static setFieldError(el, message) {
409
403
  el.setCustomValidity(message || "invalid");
410
404
  el.dataset.validationErrors = "server";
@@ -435,10 +429,9 @@ export class FormValidator {
435
429
  }
436
430
  }
437
431
 
438
- if (typeof document !== "undefined") {
439
- enhance({
440
- [`form${VALIDATION_SELECTOR}`]: (form) => connectForm(form),
441
- });
442
- }
432
+ // Registered by name, not through a bare enhance(), so applyEnhancement()
433
+ // refreshes it like the other token behaviors. The token only means
434
+ // something on a form.
435
+ registerEnhancement("validation", (el) => (isFormElement(el) ? connectForm(el) : undefined));
443
436
 
444
437
  export default FormValidator;