actual-css 0.1.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 (129) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/dist/actual.css +4929 -0
  5. package/dist/actual.js +3 -0
  6. package/dist/actual.js.map +31 -0
  7. package/dist/actual.min.css +1 -0
  8. package/docs/README.md +26 -0
  9. package/docs/components.md +860 -0
  10. package/docs/design-notes/accessibility-decisions.md +32 -0
  11. package/docs/design-notes/browser-support.md +93 -0
  12. package/docs/design-notes/cascade-layer.md +52 -0
  13. package/docs/design-notes/css-anchor.md +33 -0
  14. package/docs/design-notes/naming.md +37 -0
  15. package/docs/design-notes/previous-work.md +5 -0
  16. package/docs/design-notes/references.md +324 -0
  17. package/docs/forms.md +1265 -0
  18. package/docs/javascript.md +426 -0
  19. package/docs/layout.md +480 -0
  20. package/docs/mental-model.md +73 -0
  21. package/docs/patterns.md +415 -0
  22. package/docs/reserved.md +7 -0
  23. package/docs/tokens.md +455 -0
  24. package/docs/typography.md +159 -0
  25. package/docs/ui.md +937 -0
  26. package/docs/utilities.md +384 -0
  27. package/llms.txt +17 -0
  28. package/package.json +101 -0
  29. package/src/css/actual.css +17 -0
  30. package/src/css/actual.full.css +2 -0
  31. package/src/css/actual.layer.css +3 -0
  32. package/src/css/base.css +70 -0
  33. package/src/css/components/accordion.css +97 -0
  34. package/src/css/components/actions.css +21 -0
  35. package/src/css/components/alert.css +137 -0
  36. package/src/css/components/avatar.css +199 -0
  37. package/src/css/components/badge.css +143 -0
  38. package/src/css/components/breadcrumb.css +56 -0
  39. package/src/css/components/busy.css +44 -0
  40. package/src/css/components/button.css +216 -0
  41. package/src/css/components/card.css +140 -0
  42. package/src/css/components/drawer.css +122 -0
  43. package/src/css/components/flyout.css +243 -0
  44. package/src/css/components/index.css +41 -0
  45. package/src/css/components/join.css +106 -0
  46. package/src/css/components/key.css +25 -0
  47. package/src/css/components/meter.css +81 -0
  48. package/src/css/components/modal.css +162 -0
  49. package/src/css/components/navbar.css +98 -0
  50. package/src/css/components/overline.css +49 -0
  51. package/src/css/components/pagination.css +30 -0
  52. package/src/css/components/progress.css +126 -0
  53. package/src/css/components/scrollspy.css +27 -0
  54. package/src/css/components/skeleton.css +67 -0
  55. package/src/css/components/spinner.css +60 -0
  56. package/src/css/components/status-bar.css +82 -0
  57. package/src/css/components/surface.css +48 -0
  58. package/src/css/components/tab.css +128 -0
  59. package/src/css/components/table.css +64 -0
  60. package/src/css/components/tooltip.css +107 -0
  61. package/src/css/dialog-fallback.css +56 -0
  62. package/src/css/focus.css +13 -0
  63. package/src/css/forms/choice-card.css +158 -0
  64. package/src/css/forms/choice.css +196 -0
  65. package/src/css/forms/control.css +106 -0
  66. package/src/css/forms/custom-select.css +93 -0
  67. package/src/css/forms/form-actions.css +43 -0
  68. package/src/css/forms/form.css +57 -0
  69. package/src/css/forms/index.css +15 -0
  70. package/src/css/forms/input-icon.css +36 -0
  71. package/src/css/forms/native.css +68 -0
  72. package/src/css/forms/select.css +36 -0
  73. package/src/css/forms/switch.css +127 -0
  74. package/src/css/forms/validation.css +63 -0
  75. package/src/css/grid.css +95 -0
  76. package/src/css/intents.css +33 -0
  77. package/src/css/layout.css +80 -0
  78. package/src/css/optional/index.css +5 -0
  79. package/src/css/optional/layout-extra.css +55 -0
  80. package/src/css/optional/scroll-snap.css +51 -0
  81. package/src/css/optional/scroller.css +51 -0
  82. package/src/css/optional/typography-fluid.css +48 -0
  83. package/src/css/optional/utilities-extra.css +289 -0
  84. package/src/css/print.css +47 -0
  85. package/src/css/prose.css +187 -0
  86. package/src/css/reset.css +56 -0
  87. package/src/css/theme.css +186 -0
  88. package/src/css/themes/corporate.css +60 -0
  89. package/src/css/themes/cyberpunk.css +60 -0
  90. package/src/css/themes/dark.css +32 -0
  91. package/src/css/themes/dim.css +33 -0
  92. package/src/css/themes/edge.css +143 -0
  93. package/src/css/themes/forest.css +65 -0
  94. package/src/css/themes/gradient.css +97 -0
  95. package/src/css/themes/index.css +43 -0
  96. package/src/css/themes/lavender.css +66 -0
  97. package/src/css/themes/mono.css +60 -0
  98. package/src/css/themes/neon.css +66 -0
  99. package/src/css/themes/ocean.css +60 -0
  100. package/src/css/themes/petrol.css +64 -0
  101. package/src/css/themes/spruce.css +64 -0
  102. package/src/css/themes/square.css +68 -0
  103. package/src/css/themes/sunset.css +66 -0
  104. package/src/css/tokens.css +122 -0
  105. package/src/css/utilities.css +162 -0
  106. package/src/css/variants.css +118 -0
  107. package/src/js/command.js +165 -0
  108. package/src/js/context-menu.js +291 -0
  109. package/src/js/dialog-fallback.js +163 -0
  110. package/src/js/dialog.js +503 -0
  111. package/src/js/enhance.js +208 -0
  112. package/src/js/events.js +22 -0
  113. package/src/js/filter.js +122 -0
  114. package/src/js/floating.js +462 -0
  115. package/src/js/flyout.js +187 -0
  116. package/src/js/focus.js +35 -0
  117. package/src/js/index.js +26 -0
  118. package/src/js/input.js +47 -0
  119. package/src/js/keys.js +30 -0
  120. package/src/js/mask.js +184 -0
  121. package/src/js/menu.js +88 -0
  122. package/src/js/password.js +98 -0
  123. package/src/js/scrollspy.js +121 -0
  124. package/src/js/selectors.js +59 -0
  125. package/src/js/status.js +158 -0
  126. package/src/js/surface.js +358 -0
  127. package/src/js/tab.js +173 -0
  128. package/src/js/tooltip.js +374 -0
  129. package/src/js/validation.js +419 -0
@@ -0,0 +1,291 @@
1
+ import enhance from "./enhance.js";
2
+ import { EVENTS } from "./events.js";
3
+ import { focusFirstMenuItem, hasMenuItems } from "./menu.js";
4
+ import { CLASSES } from "./selectors.js";
5
+ import {
6
+ closeSurface,
7
+ disconnectSurface,
8
+ isSurfaceOpen,
9
+ openSurface,
10
+ prepareSurface,
11
+ } from "./surface.js";
12
+
13
+ const LONG_PRESS_MS = 450;
14
+ const MOVE_TOLERANCE = 10;
15
+ const CONTEXT_MENU_SELECTOR = `menu.${CLASSES.flyout}`;
16
+ const CONTEXT_TARGET_SELECTOR = "[data-context-menu]";
17
+ const contextMap = new WeakMap();
18
+ const contextByMenu = new WeakMap();
19
+
20
+ // Returns the detail from the last accepted `actual:context-menu` event for a
21
+ // menu. This is intentionally optional: static menus need no application JS.
22
+ export function contextFor(menu) {
23
+ return contextByMenu.get(menu) || null;
24
+ }
25
+
26
+ function menuFor(target) {
27
+ const id = target.getAttribute("data-context-menu");
28
+ const menu = id ? target.ownerDocument.getElementById(id) : null;
29
+ return menu?.matches(CONTEXT_MENU_SELECTOR) ? menu : null;
30
+ }
31
+
32
+ function shouldIgnoreNativeTarget(target) {
33
+ return !!target.closest?.(
34
+ "a, button, input, textarea, select, option, [contenteditable], [role='button'], [role='link'], [data-context-menu-ignore]",
35
+ );
36
+ }
37
+
38
+ function getContextScope(target) {
39
+ if (!target.hasAttribute("data-context-menu-scope")) return null;
40
+
41
+ const value = target.getAttribute("data-context-menu-scope").trim();
42
+ if (!value || value === "self") return target;
43
+ if (value === "parent") return target.parentElement;
44
+
45
+ try {
46
+ return target.closest(value) || target.ownerDocument.querySelector(value);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function getLongPressDelay(target, menu) {
53
+ const value = target.dataset.contextMenuLongPress ?? menu.dataset.contextMenuLongPress;
54
+ if (value == null || value === "false" || value === "none") return null;
55
+ if (value === "") return LONG_PRESS_MS;
56
+
57
+ const delay = Number.parseInt(value, 10);
58
+ return Number.isFinite(delay) ? delay : LONG_PRESS_MS;
59
+ }
60
+
61
+ function focusMenuContainer(menu) {
62
+ if (!menu.hasAttribute("tabindex")) {
63
+ menu.tabIndex = -1;
64
+ }
65
+ menu.focus();
66
+ }
67
+
68
+ // Pointer-triggered opens (right-click, long-press) focus the menu container,
69
+ // not the first item: a pointer-set focus ring is invisible (:focus-visible
70
+ // doesn't match), so focusing "Open" makes the first ArrowDown look like it
71
+ // skips to the second item. Focusing the container keeps the ring hidden and
72
+ // lets keys.js's out-of-list fallback (indexOf === -1) land ArrowDown/ArrowUp
73
+ // on the first/last item, matching native OS context menus. Keyboard-triggered
74
+ // opens (openFromKeyboard) still focus the first item directly.
75
+ function focusMenu(menu, mode) {
76
+ if (!hasMenuItems(menu)) return;
77
+ if (mode === "first-item") {
78
+ focusFirstMenuItem(menu);
79
+ } else {
80
+ focusMenuContainer(menu);
81
+ }
82
+ }
83
+
84
+ function requestContextMenu(context, menu, opts = {}) {
85
+ const detail = {
86
+ menu,
87
+ context,
88
+ origin: opts.origin || context,
89
+ trigger: opts.trigger,
90
+ };
91
+ const event = new CustomEvent(EVENTS.contextMenu, {
92
+ bubbles: true,
93
+ cancelable: true,
94
+ detail,
95
+ });
96
+ if (!context.dispatchEvent(event)) return null;
97
+ contextByMenu.set(menu, detail);
98
+ return detail;
99
+ }
100
+
101
+ function openContextMenu(context, menu, opts = {}) {
102
+ if (!requestContextMenu(context, menu, opts)) return;
103
+ if (isSurfaceOpen(menu)) closeSurface(menu);
104
+ if (
105
+ openSurface(menu, {
106
+ source: context,
107
+ x: opts.x,
108
+ y: opts.y,
109
+ placement: opts.placement || "bottom-start",
110
+ distance: opts.distance ?? 2,
111
+ mobile: opts.mobile || menu.dataset.flyoutMobile || "auto",
112
+ scope: opts.scope || getContextScope(context),
113
+ restoreFocusTo: opts.restoreFocusTo,
114
+ })
115
+ ) {
116
+ focusMenu(menu, opts.focus);
117
+ }
118
+ }
119
+
120
+ function openFromKeyboard(context, menu, origin = context, restoreFocusTo) {
121
+ const rect = origin.getBoundingClientRect();
122
+ openContextMenu(context, menu, {
123
+ x: rect.left,
124
+ y: rect.bottom,
125
+ placement: "bottom-start",
126
+ distance: 4,
127
+ focus: "first-item",
128
+ origin,
129
+ trigger: "keyboard",
130
+ restoreFocusTo,
131
+ });
132
+ }
133
+
134
+ function clearLongPress(state) {
135
+ if (state.timer) {
136
+ clearTimeout(state.timer);
137
+ state.timer = null;
138
+ }
139
+ state.pointerId = null;
140
+ }
141
+
142
+ function connectContextTarget(target) {
143
+ if (contextMap.has(target)) return;
144
+ const menu = menuFor(target);
145
+ if (!menu) return;
146
+
147
+ prepareSurface(menu);
148
+ const controller = new AbortController();
149
+ const state = {
150
+ controller,
151
+ menu,
152
+ timer: null,
153
+ pointerId: null,
154
+ startX: 0,
155
+ startY: 0,
156
+ suppressClickUntil: 0,
157
+ };
158
+
159
+ target.addEventListener(
160
+ "contextmenu",
161
+ (e) => {
162
+ if (shouldIgnoreNativeTarget(e.target)) return;
163
+ e.preventDefault();
164
+ e.stopPropagation();
165
+ openContextMenu(target, menu, {
166
+ x: e.clientX,
167
+ y: e.clientY,
168
+ origin: e.target,
169
+ trigger: "pointer",
170
+ });
171
+ },
172
+ { signal: controller.signal },
173
+ );
174
+
175
+ target.addEventListener(
176
+ "keydown",
177
+ (e) => {
178
+ if (e.key !== "ContextMenu" && !(e.shiftKey && e.key === "F10")) return;
179
+ if (shouldIgnoreNativeTarget(e.target)) return;
180
+ e.preventDefault();
181
+ openFromKeyboard(target, menu, e.target, target);
182
+ },
183
+ { signal: controller.signal },
184
+ );
185
+
186
+ target.addEventListener(
187
+ "pointerdown",
188
+ (e) => {
189
+ const delay = getLongPressDelay(target, menu);
190
+ if (e.pointerType !== "touch" || delay == null || shouldIgnoreNativeTarget(e.target)) {
191
+ return;
192
+ }
193
+
194
+ clearLongPress(state);
195
+ state.pointerId = e.pointerId;
196
+ state.startX = e.clientX;
197
+ state.startY = e.clientY;
198
+ state.timer = setTimeout(() => {
199
+ state.timer = null;
200
+ state.suppressClickUntil = Date.now() + 750;
201
+ openContextMenu(target, menu, {
202
+ x: state.startX,
203
+ y: state.startY,
204
+ origin: e.target,
205
+ trigger: "touch",
206
+ });
207
+ }, delay);
208
+ },
209
+ { signal: controller.signal, passive: true },
210
+ );
211
+
212
+ target.addEventListener(
213
+ "pointermove",
214
+ (e) => {
215
+ if (e.pointerId !== state.pointerId || !state.timer) return;
216
+ if (
217
+ Math.abs(e.clientX - state.startX) > MOVE_TOLERANCE ||
218
+ Math.abs(e.clientY - state.startY) > MOVE_TOLERANCE
219
+ ) {
220
+ clearLongPress(state);
221
+ }
222
+ },
223
+ { signal: controller.signal, passive: true },
224
+ );
225
+
226
+ for (const type of ["pointerup", "pointercancel", "pointerleave"]) {
227
+ target.addEventListener(
228
+ type,
229
+ (e) => {
230
+ if (e.pointerId === state.pointerId) clearLongPress(state);
231
+ },
232
+ { signal: controller.signal, passive: true },
233
+ );
234
+ }
235
+
236
+ target.addEventListener(
237
+ "click",
238
+ (e) => {
239
+ if (Date.now() <= state.suppressClickUntil) {
240
+ e.preventDefault();
241
+ e.stopPropagation();
242
+ state.suppressClickUntil = 0;
243
+ }
244
+ },
245
+ { signal: controller.signal, capture: true },
246
+ );
247
+
248
+ contextMap.set(target, state);
249
+ }
250
+
251
+ function disconnectContextTarget(target) {
252
+ const state = contextMap.get(target);
253
+ if (!state) return;
254
+ clearLongPress(state);
255
+ state.controller.abort();
256
+ if (contextFor(state.menu)?.context === target) contextByMenu.delete(state.menu);
257
+ contextMap.delete(target);
258
+ }
259
+
260
+ function connectContextMenu(menu) {
261
+ const controller = new AbortController();
262
+ menu.addEventListener(
263
+ EVENTS.surfaceOpen,
264
+ (event) => {
265
+ const options = event.detail?.options;
266
+ const trigger = options?.trigger;
267
+ const context = trigger?.closest?.(CONTEXT_TARGET_SELECTOR);
268
+ if (!context || menuFor(context) !== menu) return;
269
+ if (!requestContextMenu(context, menu, { origin: trigger, trigger: "button" })) {
270
+ event.preventDefault();
271
+ return;
272
+ }
273
+ options.source = context;
274
+ options.restoreFocusTo = trigger;
275
+ },
276
+ { signal: controller.signal },
277
+ );
278
+ return () => {
279
+ controller.abort();
280
+ contextByMenu.delete(menu);
281
+ disconnectSurface(menu);
282
+ };
283
+ }
284
+
285
+ enhance({
286
+ [CONTEXT_TARGET_SELECTOR]: (target) => {
287
+ connectContextTarget(target);
288
+ return () => disconnectContextTarget(target);
289
+ },
290
+ [CONTEXT_MENU_SELECTOR]: (menu) => connectContextMenu(menu),
291
+ });
@@ -0,0 +1,163 @@
1
+ /*
2
+ * Dialog fallback — minimal per-element shim for browsers without native
3
+ * <dialog> support (Safari <= 15.3, Firefox <= 97, both below Baseline 2023).
4
+ *
5
+ * Standalone side-effect module: the default runtime imports it, a custom
6
+ * build opts out by omitting it — dialog.js has no dependency on this file
7
+ * and falls back to its acknowledgement alert without it. On supporting
8
+ * browsers importing this module registers nothing.
9
+ *
10
+ * Not a polyfill: no top layer, no focus trap, no inert background. It only
11
+ * provides show()/showModal()/close(), an `open` property, and the
12
+ * form[method=dialog] close-on-submit contract (otherwise those forms really
13
+ * navigate on legacy browsers), so dialog.js can run its normal wiring —
14
+ * backdrop click, Escape cancel, close events, focus restore, and the scroll
15
+ * lock all come from that wiring, not from here.
16
+ *
17
+ * Presentation lives in dialog-fallback.css, keyed on the classes written
18
+ * below. The simulated backdrop is split in two there: a spread box-shadow
19
+ * for the dimming, and a transparent ::before covering the viewport for
20
+ * hit-testing — clicks on it target the dialog element outside its own box,
21
+ * which dialog.js already treats as a backdrop click, and it blocks pointer
22
+ * interaction with the page behind the modal.
23
+ *
24
+ * An adopter-provided polyfill (e.g. dialog-polyfill) still wins: elements it
25
+ * already patched expose showModal() and are left untouched.
26
+ */
27
+
28
+ import { targetFor } from "./command.js";
29
+ import enhance from "./enhance.js";
30
+ import { CLASSES } from "./selectors.js";
31
+
32
+ function supportsDialog() {
33
+ return (
34
+ typeof HTMLDialogElement !== "undefined" &&
35
+ typeof HTMLDialogElement.prototype.showModal === "function"
36
+ );
37
+ }
38
+
39
+ function focusInitial(dialog) {
40
+ const target = dialog.querySelector("[autofocus]") || dialog;
41
+ if (target === dialog && !dialog.hasAttribute("tabindex")) {
42
+ dialog.setAttribute("tabindex", "-1");
43
+ }
44
+ target.focus?.();
45
+ }
46
+
47
+ /**
48
+ * Patch one dialog element with rudimentary show/showModal/close support.
49
+ * No-op when the element is already controllable (native or polyfilled).
50
+ *
51
+ * @param {HTMLElement} dialog A <dialog> element.
52
+ */
53
+ export default function shimDialog(dialog) {
54
+ if (
55
+ typeof dialog.show === "function" &&
56
+ typeof dialog.showModal === "function" &&
57
+ typeof dialog.close === "function"
58
+ ) {
59
+ return;
60
+ }
61
+
62
+ const doc = dialog.ownerDocument;
63
+ let escController = null;
64
+ let savedScroll = null;
65
+
66
+ function stopEscapeListener() {
67
+ escController?.abort();
68
+ escController = null;
69
+ }
70
+
71
+ function handleSubmit(event) {
72
+ const form = event.target;
73
+ // Read the attribute: on these browsers "dialog" is an invalid method
74
+ // value, so the reflected form.method property reports "get" and the
75
+ // form would otherwise really submit and navigate away.
76
+ if (form?.getAttribute?.("method")?.toLowerCase() !== "dialog") return;
77
+ event.preventDefault();
78
+ const submitter =
79
+ event.submitter || (doc.activeElement?.form === form ? doc.activeElement : null);
80
+ dialog.close(submitter?.value || "");
81
+ }
82
+
83
+ function handleEscape(event) {
84
+ if (event.key !== "Escape") return;
85
+ event.preventDefault();
86
+ const cancel = new Event("cancel", { cancelable: true });
87
+ // dialog.js always claims the cancel event and decides itself whether to
88
+ // close; only an unwired dialog falls through to the direct close.
89
+ if (dialog.dispatchEvent(cancel)) {
90
+ dialog.close();
91
+ }
92
+ }
93
+
94
+ function open(modal) {
95
+ if (dialog.open) return;
96
+ // Old WebKit clamps the scroll position to 0 when the root element gains
97
+ // overflow:hidden (the has-modal-open lock applied right after this),
98
+ // losing the user's place. Remember it and put it back after close.
99
+ const win = doc.defaultView;
100
+ savedScroll = modal && win ? { x: win.scrollX, y: win.scrollY } : null;
101
+ dialog.setAttribute("open", "");
102
+ dialog.classList.toggle(CLASSES.fallbackModal, modal);
103
+ if (modal) {
104
+ escController = new AbortController();
105
+ doc.addEventListener("keydown", handleEscape, { signal: escController.signal });
106
+ }
107
+ focusInitial(dialog);
108
+ }
109
+
110
+ Object.defineProperty(dialog, "open", {
111
+ configurable: true,
112
+ get() {
113
+ return this.hasAttribute("open");
114
+ },
115
+ set(value) {
116
+ this.toggleAttribute("open", Boolean(value));
117
+ },
118
+ });
119
+
120
+ dialog.returnValue = "";
121
+ dialog.classList.add(CLASSES.dialogFallback);
122
+ dialog.addEventListener("submit", handleSubmit);
123
+ dialog.show = () => open(false);
124
+ dialog.showModal = () => open(true);
125
+ dialog.close = (returnValue = "") => {
126
+ if (!dialog.open) return;
127
+ stopEscapeListener();
128
+ dialog.returnValue = returnValue;
129
+ dialog.removeAttribute("open");
130
+ dialog.classList.remove(CLASSES.fallbackModal);
131
+ dialog.dispatchEvent(new Event("close"));
132
+ // After the close event: dialog.js has lifted the scroll lock and moved
133
+ // focus back to the trigger, so the restored position sticks.
134
+ if (savedScroll) {
135
+ doc.defaultView?.scrollTo?.(savedScroll.x, savedScroll.y);
136
+ savedScroll = null;
137
+ }
138
+ };
139
+ }
140
+
141
+ if (typeof document !== "undefined" && !supportsDialog()) {
142
+ enhance({
143
+ dialog: (el) => {
144
+ shimDialog(el);
145
+ },
146
+ });
147
+
148
+ // A dialog inserted right before its trigger's click may not be scanned by
149
+ // enhance() yet, so shim command targets in capture phase too — before the
150
+ // delegated command routing resolves them.
151
+ document.addEventListener(
152
+ "click",
153
+ (event) => {
154
+ const trigger = event.target?.closest?.("button[commandfor][command]");
155
+ if (!trigger) return;
156
+ const dialog = targetFor(trigger);
157
+ if (dialog?.localName === "dialog") {
158
+ shimDialog(dialog);
159
+ }
160
+ },
161
+ true,
162
+ );
163
+ }