actual-css 0.14.0 → 0.14.1

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.
@@ -35,6 +35,10 @@ The named exports each module carries:
35
35
  - `selectors` — `CLASSES`
36
36
 
37
37
  `surface` also owns outside-click and Escape dismissal, and focus restore.
38
+ A click on an element with `data-flyout-close` closes the open panel around
39
+ it, whichever widget opened that panel. The attribute keeps the `flyout` name
40
+ because the panels authors write are flyouts; `surface` is only the code that
41
+ opens and closes them.
38
42
  `menu` filters to usable items.
39
43
 
40
44
  ## Focus group
@@ -67,9 +67,10 @@ For a modal bottom sheet or action sheet, use `dialog.drawer`; use
67
67
  `dialog.modal` for a centered modal. Those components own modal focus,
68
68
  dismissal, scroll locking, and backdrop behavior explicitly.
69
69
 
70
- Use `data-flyout-placement` for the preferred anchored placement. It accepts the
71
- placement strings supported by the floating runtime, such as `bottom-start`,
72
- `bottom-end`, `top-start`, `right`, or `left`.
70
+ Use `data-flyout-placement` for the preferred anchored placement: a side
71
+ (`top`, `bottom`, `left`, `right`), optionally followed by `-start` or `-end`,
72
+ such as `bottom-start` or `right`. An unknown value logs a console warning and
73
+ falls back to `bottom-start`.
73
74
 
74
75
  Use `data-flyout-distance` for the trigger gap in pixels. The default is `4`.
75
76
 
@@ -56,7 +56,8 @@ is a no-op until the module is loaded again with a DOM.
56
56
  Load the full bundle **or** the source modules, never both: each evaluation
57
57
  owns its registry and document handlers, so a dist bundle mixed with
58
58
  `actual-css/js/*` source subpaths (or two dist bundles) registers every
59
- behavior twice and a flyout opens then immediately closes.
59
+ behavior twice and a flyout opens then immediately closes. The second copy
60
+ logs a console warning when it loads.
60
61
 
61
62
  ## Component pages
62
63
 
@@ -115,7 +115,9 @@ viewport-aware maximum width.
115
115
  ## Positioning
116
116
 
117
117
  Set `data-tooltip-placement` to control where the tooltip appears relative to
118
- its trigger. The arrow follows the placement automatically.
118
+ its trigger: a side (`top`, `bottom`, `left`, `right`), optionally followed by
119
+ `-start` or `-end`. An unknown value logs a console warning and falls back to
120
+ `top`. The arrow follows the placement automatically.
119
121
 
120
122
  The runtime also picks the coordinate space, and writes `position` to match. A
121
123
  tooltip whose trigger scrolls with the page is placed in document coordinates
@@ -4,8 +4,8 @@
4
4
 
5
5
  The examples live in the repository under `demo/` and load `actual.full.css`
6
6
  and `actual.full.js` from `dist/`, plus the theme palettes bundle from
7
- `demo/assets/`. The bundle is committed and regenerated by
8
- `bun run build:themes`, so the pages open from a fresh checkout.
7
+ `demo/assets/`. Both are build output: in a clone, run `bun run build:dist`,
8
+ `bun run build:js` and `bun run build:themes` before opening a page.
9
9
 
10
10
  ## Templates
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "actual-css",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Plain CSS component framework with semantic classes, universal variants, small tokens, theme hooks, and progressive enhancements.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.4.3",
@@ -132,6 +132,7 @@
132
132
  "build:js": "bun scripts/build-js.js",
133
133
  "build:themes": "bun scripts/build-themes.js",
134
134
  "build:size": "bun scripts/build-size.js",
135
+ "build:pages": "bun scripts/pages.js",
135
136
  "export:design": "bun scripts/export-design.js",
136
137
  "check:enhance": "bun run scripts/check-enhance.js",
137
138
  "generate:reserved": "bun scripts/generate-reserved-classes.js",
@@ -157,9 +158,9 @@
157
158
  },
158
159
  "devDependencies": {
159
160
  "@biomejs/biome": "^2.5.15",
160
- "happy-dom": "^20.14.0"
161
+ "happy-dom": "^21.0.1"
161
162
  },
162
163
  "dependencies": {
163
- "@lekoala/floating": "^0.2.0"
164
+ "@lekoala/floating": "^0.3.0"
164
165
  }
165
166
  }
@@ -49,6 +49,10 @@
49
49
  its invisible reach. */
50
50
  margin: calc(var(--color-swatch-reach) / -2);
51
51
  padding: 0;
52
+ /* No in-flow text, so the baseline would be the box's bottom edge and an
53
+ inline swatch would ride above the line (a flyout trigger beside its
54
+ label). A palette in .cluster or .grid ignores it. */
55
+ vertical-align: middle;
52
56
  border: 0;
53
57
  background: transparent;
54
58
  color: inherit;
package/src/js/enhance.js CHANGED
@@ -31,6 +31,20 @@
31
31
  const registries = new WeakMap();
32
32
  const ownedNames = new WeakMap();
33
33
 
34
+ // Each evaluation owns its registries, so a second copy (the dist bundle plus
35
+ // source modules, or two bundles) registers every behavior twice and fails
36
+ // silently — a flyout opens then closes. Every built-in imports this module,
37
+ // so the marker catches any duplicated runtime once, here.
38
+ const RUNTIME = Symbol.for("actual-css.runtime");
39
+ if (typeof document !== "undefined") {
40
+ if (globalThis[RUNTIME]) {
41
+ console.warn(
42
+ "actual-css: a second copy of the JavaScript runtime is loaded. Load the full bundle or the source modules, never both.",
43
+ );
44
+ }
45
+ globalThis[RUNTIME] = true;
46
+ }
47
+
34
48
  // Node type constants as literals, not the realm's Node global, so nodes from
35
49
  // another document/window compare correctly.
36
50
  const ELEMENT_NODE = 1;
@@ -29,6 +29,32 @@ function isViewportAnchored(anchor) {
29
29
  return false;
30
30
  }
31
31
 
32
+ // Mirrors the placements @lekoala/floating accepts; it throws on any other
33
+ // value, and from an autoUpdate callback that throw would repeat on every
34
+ // scroll. Markup is validated here instead, so a typo warns once.
35
+ const PLACEMENTS = new Set(
36
+ ["top", "bottom", "left", "right"].flatMap((side) => [side, `${side}-start`, `${side}-end`]),
37
+ );
38
+ const warnedPlacements = new WeakSet();
39
+
40
+ /**
41
+ * The author's placement attribute, or null when absent or unknown.
42
+ *
43
+ * @param {Element | null | undefined} el
44
+ * @param {string} attribute
45
+ * @returns {string | null}
46
+ */
47
+ export function readPlacement(el, attribute) {
48
+ const value = el?.getAttribute(attribute);
49
+ if (!value) return null;
50
+ if (PLACEMENTS.has(value)) return value;
51
+ if (!warnedPlacements.has(el)) {
52
+ warnedPlacements.add(el);
53
+ console.warn(`Unknown ${attribute}="${value}" ignored`, el);
54
+ }
55
+ return null;
56
+ }
57
+
32
58
  /**
33
59
  * @param {Element | null | undefined} anchor
34
60
  * @returns {{ space: "viewport" | "document", position: "fixed" | "absolute" }}
package/src/js/flyout.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { registerEnhancement } from "./enhance.js";
2
+ import { readPlacement } from "./floating-space.js";
2
3
  import { focusFirstDescendant } from "./focus.js";
3
4
  import { shouldIgnoreKey } from "./keys.js";
4
5
  import { connectMenu, focusFirstMenuItem, focusLastMenuItem } from "./menu.js";
@@ -9,7 +10,8 @@ const triggerMap = new WeakMap();
9
10
  function readFlyoutOptions(panel) {
10
11
  const opts = { autoClose: "true" };
11
12
  const ds = panel.dataset;
12
- if (ds.flyoutPlacement) opts.placement = ds.flyoutPlacement;
13
+ const placement = readPlacement(panel, "data-flyout-placement");
14
+ if (placement) opts.placement = placement;
13
15
  if (ds.flyoutDistance != null) {
14
16
  const d = Number.parseFloat(ds.flyoutDistance);
15
17
  if (Number.isFinite(d)) opts.distance = d;
@@ -11,10 +11,13 @@
11
11
  */
12
12
 
13
13
  function relax(source) {
14
+ // Double-quoted strings are matched first and copied verbatim, so text
15
+ // such as `http:` inside them is never mistaken for an unquoted key.
14
16
  return source.replace(
15
- /([a-zA-Z_$][\w$-]*)\s*:|'((?:\\'|[^'])*)'/g,
16
- (_match, key, stringContent) => {
17
+ /"(?:\\.|[^"\\])*"|([a-zA-Z_$][\w$-]*)\s*:|'((?:\\'|[^'])*)'/g,
18
+ (match, key, stringContent) => {
17
19
  if (key) return `"${key}":`;
20
+ if (stringContent === undefined) return match;
18
21
  const unescaped = stringContent.replace(/\\'/g, "'");
19
22
  const escaped = unescaped.replace(/"/g, '\\"');
20
23
  return `"${escaped}"`;
package/src/js/tooltip.js CHANGED
@@ -51,7 +51,7 @@
51
51
  import { autoUpdate, reposition } from "@lekoala/floating";
52
52
  import enhance from "./enhance.js";
53
53
  import { registerEscapeDismissal } from "./escape.js";
54
- import { positionModeFor } from "./floating-space.js";
54
+ import { positionModeFor, readPlacement } from "./floating-space.js";
55
55
  import { CLASSES } from "./selectors.js";
56
56
 
57
57
  const SHOW_DELAY_MS = 150;
@@ -144,7 +144,7 @@ export function isTooltipVisible(tip) {
144
144
  * the tip's own placement never enters into it.
145
145
  */
146
146
  function placementFor(ref) {
147
- return ref?.getAttribute("data-tooltip-placement") || "top";
147
+ return readPlacement(ref, "data-tooltip-placement") ?? "top";
148
148
  }
149
149
 
150
150
  function hasOption(trigger, name) {