actual-css 0.10.0 → 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 (71) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/README.md +2 -2
  3. package/components.json +2 -1
  4. package/dist/actual.full.css +314 -106
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/enhancement-contract.md +16 -7
  8. package/docs/design-notes/platform-alignment.md +8 -4
  9. package/docs/design-notes/rich-text.md +61 -0
  10. package/docs/design-notes/steps.md +2 -2
  11. package/docs/navigation.json +2 -0
  12. package/docs/pages/components/aura.md +12 -13
  13. package/docs/pages/components/badge.md +10 -0
  14. package/docs/pages/components/button.md +1 -1
  15. package/docs/pages/components/chat.md +5 -0
  16. package/docs/pages/components/dialog.md +5 -2
  17. package/docs/pages/components/pagination.md +15 -15
  18. package/docs/pages/components/shimmer.md +57 -0
  19. package/docs/pages/components/skeleton.md +2 -1
  20. package/docs/pages/components/spinner.md +11 -6
  21. package/docs/pages/components/table.md +13 -0
  22. package/docs/pages/enhancements/flyout.md +2 -0
  23. package/docs/pages/enhancements/tabs.md +4 -0
  24. package/docs/pages/forms/choice-cards.md +77 -4
  25. package/docs/pages/forms/combobox.md +16 -1
  26. package/docs/pages/forms/overview.md +4 -1
  27. package/docs/pages/forms/range.md +110 -5
  28. package/docs/pages/forms/rich-text.md +110 -0
  29. package/docs/pages/forms/validation.md +5 -1
  30. package/docs/pages/foundations/theming.md +49 -3
  31. package/docs/pages/foundations/tokens.md +15 -5
  32. package/docs/pages/guides/bootstrap.md +2 -1
  33. package/docs/pages/guides/introduction.md +1 -1
  34. package/docs/pages/guides/modular-import.md +1 -1
  35. package/docs/pages/guides/progressive-enhancement.md +13 -14
  36. package/docs/pages/guides/tailwind.md +4 -2
  37. package/llms.txt +4 -3
  38. package/package.json +5 -1
  39. package/reserved-classes.json +1 -0
  40. package/src/css/components/avatar.css +1 -1
  41. package/src/css/components/flyout.css +4 -1
  42. package/src/css/components/meter.css +1 -1
  43. package/src/css/components/pagination.css +3 -2
  44. package/src/css/components/rating.css +1 -1
  45. package/src/css/components/status-bar.css +2 -2
  46. package/src/css/components/steps.css +3 -3
  47. package/src/css/components/tab.css +3 -3
  48. package/src/css/components/table.css +6 -0
  49. package/src/css/components/tooltip.css +1 -1
  50. package/src/css/core/focus.css +14 -4
  51. package/src/css/core/theme.css +8 -0
  52. package/src/css/core/tokens.css +8 -4
  53. package/src/css/core/variants.css +2 -1
  54. package/src/css/effects/aura.css +51 -33
  55. package/src/css/effects/index.css +1 -0
  56. package/src/css/effects/shimmer.css +57 -0
  57. package/src/css/forms/choice-card.css +71 -23
  58. package/src/css/forms/control.css +2 -2
  59. package/src/css/forms/otp.css +9 -1
  60. package/src/css/forms/range.css +64 -21
  61. package/src/js/command.js +61 -32
  62. package/src/js/dialog.js +8 -23
  63. package/src/js/dismiss.js +1 -2
  64. package/src/js/enhance.js +84 -123
  65. package/src/js/full.js +1 -0
  66. package/src/js/password.js +1 -8
  67. package/src/js/range.js +151 -0
  68. package/src/js/scrollspy.js +4 -5
  69. package/src/js/status.js +2 -10
  70. package/src/js/tooltip.js +11 -1
  71. package/src/js/validation.js +4 -10
@@ -13,6 +13,10 @@
13
13
  * --range-track-height
14
14
  * --range-thumb-bg
15
15
  * --range-track-bg
16
+ * --range-fill-bg
17
+ *
18
+ * Runtime-written (by actual-css/js/range, never set by hand):
19
+ * --range-progress
16
20
  */
17
21
 
18
22
  /* Intent boundary — inherited intents do not tint this component.
@@ -22,10 +26,17 @@
22
26
  }
23
27
 
24
28
  .range {
25
- --range-thumb-size: 1.25rem;
26
- --range-track-height: 0.25rem;
29
+ --range-thumb-size: 1.125rem;
30
+ --range-track-height: 0.625rem;
27
31
  --range-thumb-bg: var(--intent, var(--primary));
28
32
  --range-track-bg: var(--border);
33
+ --range-fill-bg: var(--intent, var(--primary));
34
+ /* Fill share, written by actual-css/js/range. 0% with no runtime means a
35
+ plain track — the control stays fully usable without JavaScript. */
36
+ --range-progress: 0%;
37
+ /* Logical fill direction: the gradient below cannot use flow-relative
38
+ keywords, so RTL flips the angle through this hook instead. */
39
+ --range-fill-from: to right;
29
40
 
30
41
  appearance: none;
31
42
  -webkit-appearance: none;
@@ -35,16 +46,29 @@
35
46
  cursor: pointer;
36
47
  }
37
48
 
49
+ :where([dir="rtl"]) .range,
50
+ .range:dir(rtl) {
51
+ --range-fill-from: to left;
52
+ }
53
+
38
54
  .range::-webkit-slider-runnable-track {
39
55
  block-size: var(--range-track-height);
40
56
  border-radius: var(--radius-full);
41
- background: var(--range-track-bg);
57
+ background: linear-gradient(
58
+ var(--range-fill-from),
59
+ var(--range-fill-bg) var(--range-progress),
60
+ var(--range-track-bg) var(--range-progress)
61
+ );
42
62
  }
43
63
 
44
64
  .range::-moz-range-track {
45
65
  block-size: var(--range-track-height);
46
66
  border-radius: var(--radius-full);
47
- background: var(--range-track-bg);
67
+ background: linear-gradient(
68
+ var(--range-fill-from),
69
+ var(--range-fill-bg) var(--range-progress),
70
+ var(--range-track-bg) var(--range-progress)
71
+ );
48
72
  }
49
73
 
50
74
  .range::-webkit-slider-thumb {
@@ -57,6 +81,10 @@
57
81
  background: var(--range-thumb-bg);
58
82
  margin-block-start: calc((var(--range-track-height) - var(--range-thumb-size)) / 2);
59
83
  cursor: inherit;
84
+ /* Permanent surface separator: the fill defaults to the thumb color, so
85
+ without this the thumb would melt into the filled part of the track.
86
+ Focus-visible keeps this ring and adds the --focus ring outside it. */
87
+ box-shadow: 0 0 0 2px var(--surface-opaque);
60
88
  transition: box-shadow var(--duration);
61
89
  }
62
90
 
@@ -67,6 +95,7 @@
67
95
  border: 0;
68
96
  background: var(--range-thumb-bg);
69
97
  cursor: inherit;
98
+ box-shadow: 0 0 0 2px var(--surface-opaque);
70
99
  transition: box-shadow var(--duration);
71
100
  }
72
101
 
@@ -77,20 +106,21 @@
77
106
  /*
78
107
  * A thumb pseudo-element takes no reliable outline, so its ring is a solid
79
108
  * --focus shadow (the transparent outline above covers forced colors). The
80
- * 1px surface-coloured separator keeps the ring visually distinct from the
81
- * small, coloured thumb — unlike a full-size input where the same gap would
82
- * read as an unwanted void.
109
+ * surface-coloured separator keeps the ring visually distinct from the
110
+ * coloured thumb — unlike a full-size input where the same gap would
111
+ * read as an unwanted void. It doubles as the resting separator between
112
+ * the thumb and the same-coloured fill.
83
113
  */
84
114
  .range:focus-visible::-webkit-slider-thumb {
85
115
  box-shadow:
86
- 0 0 0 1px var(--surface),
87
- 0 0 0 calc(1px + var(--focus-ring-width)) var(--focus);
116
+ 0 0 0 2px var(--surface-opaque),
117
+ 0 0 0 calc(2px + var(--focus-ring-width)) var(--focus);
88
118
  }
89
119
 
90
120
  .range:focus-visible::-moz-range-thumb {
91
121
  box-shadow:
92
- 0 0 0 1px var(--surface),
93
- 0 0 0 calc(1px + var(--focus-ring-width)) var(--focus);
122
+ 0 0 0 2px var(--surface-opaque),
123
+ 0 0 0 calc(2px + var(--focus-ring-width)) var(--focus);
94
124
  }
95
125
 
96
126
  .range:disabled {
@@ -98,28 +128,41 @@
98
128
  cursor: not-allowed;
99
129
  opacity: var(--disabled-opacity);
100
130
  }
101
-
102
131
  /* Forced colors: the track must stay a visible line against the page (Canvas
103
- would merge with it) and the thumb needs a real boundary to remain
104
- distinguishable from the track. Disabled keeps GrayText via :disabled.
105
- Focus stays on the transparent outline of :focus-visible. Track and thumb
106
- fills are written on the properties, not only the hooks: an application
107
- hook override at the same specificity loads after this file and would win
108
- the variable, leaving a Canvas-remapped fill. */
132
+ would merge with it) and the thumb needs a real boundary to remain
133
+ distinguishable from the track. The fill keeps its two-tone split with
134
+ system colors so the value stays readable. Disabled keeps GrayText via
135
+ :disabled. Focus stays on the transparent outline of :focus-visible. Track
136
+ and thumb fills are written on the properties, not only the hooks: an
137
+ application hook override at the same specificity loads after this file
138
+ and would win the variable, leaving a Canvas-remapped fill.
139
+ The split lives in a background-image, which forced-color-adjust: auto
140
+ drops — so the tracks opt out locally. The colors stay system keywords,
141
+ keeping native contrast adaptation; only the host keeps adjusting. */
109
142
  @media (forced-colors: active) {
110
143
  .range {
111
144
  --range-track-bg: CanvasText;
112
145
  --range-thumb-bg: ButtonFace;
146
+ --range-fill-bg: Highlight;
113
147
  }
114
148
 
115
149
  .range::-webkit-slider-runnable-track {
116
- background: CanvasText;
150
+ forced-color-adjust: none;
151
+ background: linear-gradient(
152
+ var(--range-fill-from),
153
+ Highlight var(--range-progress),
154
+ CanvasText var(--range-progress)
155
+ );
117
156
  }
118
157
 
119
158
  .range::-moz-range-track {
120
- background: CanvasText;
159
+ forced-color-adjust: none;
160
+ background: linear-gradient(
161
+ var(--range-fill-from),
162
+ Highlight var(--range-progress),
163
+ CanvasText var(--range-progress)
164
+ );
121
165
  }
122
-
123
166
  .range::-webkit-slider-thumb {
124
167
  background: ButtonFace;
125
168
  border: var(--border-width) solid ButtonText;
package/src/js/command.js CHANGED
@@ -7,6 +7,12 @@
7
7
  * late targets, changed commandfor values, and same-id replacements therefore
8
8
  * work immediately without an observer, scan, refresh call, or per-element
9
9
  * listener.
10
+ *
11
+ * A routed command cancels the click, so the browser never dispatches the
12
+ * native `command` event to the target, and the router does not re-dispatch
13
+ * one. Listen to the behavior's own events instead. Accessibility state that
14
+ * must exist before interaction (`aria-controls`, `aria-haspopup`,
15
+ * `aria-pressed`) belongs in the markup: the router has no connection step.
10
16
  */
11
17
 
12
18
  /* One registry per document owns its delegated listener and command handlers.
@@ -14,18 +20,45 @@
14
20
  * behavior definitions only, never DOM triggers or resolved targets. */
15
21
  const registries = new WeakMap();
16
22
 
17
- function commandNames(commands, caller) {
18
- const names = Array.isArray(commands) ? commands : [commands];
19
- if (!names.length || names.some((name) => typeof name !== "string" || !name)) {
20
- throw new TypeError(`${caller} requires one or more command names.`);
21
- }
22
- return [...new Set(names)];
23
+ // The HTML `command` keywords. Any other value that does not start with "--"
24
+ // is the invalid state, which the browser ignores. Extend this list when HTML
25
+ // adds a keyword: registerCommands() throws on an unknown non-"--" name, so a
26
+ // stale list fails loudly instead of silently swallowing the new keyword.
27
+ // A future path that listens to the native `command` event instead of the
28
+ // click would need a per-tier matrix (native support varies) plus
29
+ // no-double-execution tests — the click stays the single routing event until
30
+ // then, which is why a routed command cancels it by design (see above).
31
+ const NATIVE_COMMANDS = new Set([
32
+ "toggle-popover",
33
+ "show-popover",
34
+ "hide-popover",
35
+ "close",
36
+ "request-close",
37
+ "show-modal",
38
+ ]);
39
+
40
+ function isCustom(command) {
41
+ return command.startsWith("--");
23
42
  }
24
43
 
25
44
  // Native command keywords are ASCII case-insensitive. Custom commands keep
26
45
  // their exact spelling, as required by the command invoker contract.
27
46
  function commandKey(command) {
28
- return command.startsWith("--") ? command : command.toLowerCase();
47
+ return isCustom(command) ? command : command.toLowerCase();
48
+ }
49
+
50
+ function commandNames(commands, caller) {
51
+ const names = Array.isArray(commands) ? commands : [commands];
52
+ if (!names.length || names.some((name) => typeof name !== "string")) {
53
+ throw new TypeError(`${caller} requires one or more command names.`);
54
+ }
55
+ const invalid = names.find((name) => !isCustom(name) && !NATIVE_COMMANDS.has(commandKey(name)));
56
+ if (invalid !== undefined) {
57
+ throw new TypeError(
58
+ `${caller}: "${invalid}" is neither a native command nor a custom "--" command.`,
59
+ );
60
+ }
61
+ return [...new Set(names.map(commandKey))];
29
62
  }
30
63
 
31
64
  /**
@@ -46,23 +79,25 @@ export function targetFor(trigger) {
46
79
  /**
47
80
  * Build the button selector corresponding to one or more command names.
48
81
  *
82
+ * Native keywords match case-insensitively, custom commands exactly — the
83
+ * same rule the router applies.
84
+ *
49
85
  * @param {string | string[]} commands Command name or names to include.
50
86
  * @returns {string} A selector matching command buttons with `commandfor`.
51
- * @throws {TypeError} When no valid command name is provided.
87
+ * @throws {TypeError} When a name is neither native nor a custom `--` command.
52
88
  */
53
89
  export function commandSelector(commands) {
54
- const attributes = commandNames(commands, "commandSelector()").map(
55
- (name) => `[command="${name.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"]`,
56
- );
90
+ const attributes = commandNames(commands, "commandSelector()").map((name) => {
91
+ const value = name.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
92
+ return `[command="${value}"${isCustom(name) ? "" : " i"}]`;
93
+ });
57
94
  return `button[commandfor]:is(${attributes.join(", ")})`;
58
95
  }
59
96
 
60
97
  function triggerFromEvent(event, doc) {
61
- const ElementClass = doc.defaultView?.Element;
62
- if (!ElementClass) return null;
63
-
64
98
  for (const node of event.composedPath?.() ?? [event.target]) {
65
- if (node instanceof ElementClass && node.matches("button[commandfor][command]")) {
99
+ // nodeType rather than instanceof: nodes from another realm still count.
100
+ if (node.nodeType === 1 && node.matches("button[commandfor][command]")) {
66
101
  return node;
67
102
  }
68
103
  if (node === doc) break;
@@ -78,7 +113,7 @@ function createRegistry(doc) {
78
113
  if (event.defaultPrevented) return;
79
114
 
80
115
  const trigger = triggerFromEvent(event, doc);
81
- if (!trigger || trigger.disabled) return;
116
+ if (!trigger || trigger.matches(":disabled")) return;
82
117
 
83
118
  const command = commandKey(trigger.getAttribute("command"));
84
119
  const registration = commands.get(command);
@@ -87,7 +122,10 @@ function createRegistry(doc) {
87
122
  const target = registration.resolve(trigger);
88
123
  if (!target) return;
89
124
 
90
- registration.prepare?.(trigger, target, command);
125
+ // One routed command owns the click, even when the handler throws. This
126
+ // is also what stops a second copy of this module, with its own registry
127
+ // and listener, from handling the same activation.
128
+ event.preventDefault();
91
129
  registration.handle(event, trigger, target, command);
92
130
  }
93
131
 
@@ -98,31 +136,22 @@ function createRegistry(doc) {
98
136
  /**
99
137
  * Register one behavior for one or more command names.
100
138
  *
101
- * `prepare`, when present, runs immediately before `handle` on every matching
102
- * action. It must be idempotent; it is useful for semantics derived from the
103
- * resolved target, without introducing a connection lifecycle.
139
+ * The router cancels the click once `resolve` returns a target, so `handle`
140
+ * runs with `event.defaultPrevented` already true.
104
141
  *
105
142
  * @param {string | string[]} commands Command name or names owned by this behavior.
106
143
  * @param {object} options Behavior callbacks.
107
144
  * @param {(trigger: HTMLButtonElement) => Element | null} [options.resolve=targetFor]
108
145
  * Resolver evaluated for every action.
109
- * @param {(trigger: HTMLButtonElement, target: Element, command: string) => void} [options.prepare]
110
- * Optional idempotent callback run immediately before the handler.
111
146
  * @param {(event: MouseEvent, trigger: HTMLButtonElement, target: Element, command: string) => void} options.handle
112
147
  * Command handler.
113
148
  * @returns {{ disconnect: () => void }} An idempotent registration teardown handle.
114
149
  * @throws {TypeError} When command names or callbacks are invalid.
115
150
  * @throws {Error} When a command already has an owner in the current document.
116
151
  */
117
- export function registerCommands(commands, { resolve = targetFor, prepare, handle } = {}) {
118
- const names = [
119
- ...new Set(commandNames(commands, "registerCommands()").map((name) => commandKey(name))),
120
- ];
121
- if (
122
- typeof resolve !== "function" ||
123
- (prepare !== undefined && typeof prepare !== "function") ||
124
- typeof handle !== "function"
125
- ) {
152
+ export function registerCommands(commands, { resolve = targetFor, handle } = {}) {
153
+ const names = commandNames(commands, "registerCommands()");
154
+ if (typeof resolve !== "function" || typeof handle !== "function") {
126
155
  throw new TypeError("registerCommands() requires function callbacks.");
127
156
  }
128
157
  if (typeof document === "undefined") return { disconnect() {} };
@@ -139,7 +168,7 @@ export function registerCommands(commands, { resolve = targetFor, prepare, handl
139
168
  throw new Error(`Command "${conflict}" is already registered in this document.`);
140
169
  }
141
170
 
142
- const registration = { resolve, prepare, handle };
171
+ const registration = { resolve, handle };
143
172
  for (const name of names) registry.commands.set(name, registration);
144
173
 
145
174
  let connected = true;
package/src/js/dialog.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * Close button: <button commandfor="dialog-id" command="request-close">
6
6
  * Dialog: <dialog id="dialog-id" data-dialog-dismissible>
7
7
  *
8
- * Command semantics are explicit: show-modal calls showModal(), show calls
8
+ * Command semantics are explicit: show-modal calls showModal(), --show calls
9
9
  * show(). data-dialog-modal still defines the default for direct openDialog()
10
10
  * calls when no command mode is forced.
11
11
  * data-dialog-dismissible gates backdrop click only: with it, the runtime
@@ -34,7 +34,9 @@ import { CLASSES } from "./selectors.js";
34
34
 
35
35
  const dialogMap = new WeakMap();
36
36
  const wiredDialogs = new Set();
37
- const DIALOG_COMMANDS = ["show-modal", "show", "request-close", "close"];
37
+ // HTML has no non-modal dialog keyword, so non-modal opening is the custom
38
+ // `--show` command.
39
+ const DIALOG_COMMANDS = ["show-modal", "--show", "request-close", "close"];
38
40
  const DIALOG_SELECTOR = "dialog";
39
41
  const DIALOG_TITLE_SELECTOR = "[data-title], h1, h2, h3, h4, h5, h6";
40
42
  const STATIC_SHAKE_MS = 250;
@@ -519,28 +521,11 @@ registerCommands(DIALOG_COMMANDS, {
519
521
  const dialog = targetFor(trigger);
520
522
  return isDialogElement(dialog) ? dialog : null;
521
523
  },
522
- prepare: (trigger, dialog, command) => {
523
- if (isDialogElement(dialog)) {
524
- ensureDialogWired(dialog);
525
- }
526
-
527
- trigger.setAttribute("aria-controls", dialog.id);
528
-
529
- if (
530
- !trigger.hasAttribute("aria-haspopup") &&
531
- (command === "show-modal" || command === "show")
532
- ) {
533
- trigger.setAttribute("aria-haspopup", "dialog");
534
- }
535
- },
536
- handle: (event, trigger, dialog, command) => {
537
- event.preventDefault();
538
-
539
- if (!isDialogElement(dialog)) {
540
- return;
541
- }
524
+ handle: (_event, trigger, dialog, command) => {
525
+ // A dialog inserted since the last enhance() pass is wired on first use.
526
+ ensureDialogWired(dialog);
542
527
 
543
- if (command === "show-modal" || command === "show") {
528
+ if (command === "show-modal" || command === "--show") {
544
529
  openDialogWithViewTransition(dialog, trigger, command === "show-modal");
545
530
  return;
546
531
  }
package/src/js/dismiss.js CHANGED
@@ -7,8 +7,7 @@ import { registerCommands } from "./command.js";
7
7
  import { EVENTS } from "./events.js";
8
8
 
9
9
  registerCommands("--dismiss", {
10
- handle(event, trigger, target) {
11
- event.preventDefault();
10
+ handle(_event, trigger, target) {
12
11
  target.hidden = true;
13
12
  target.dispatchEvent(
14
13
  new CustomEvent(EVENTS.dismiss, {