staffa 0.13.0 → 0.14.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.
@@ -37,8 +37,10 @@ A.insertGlobalCss({
37
37
  */
38
38
  export function select(opts: SelectOptions): void {
39
39
  drawField(opts, (id, isInvalid) => {
40
- A("div.s-select_wrap", opts.inputAttrs, () => {
41
- A("select.s-input", () => {
40
+ A("div.s-select_wrap", () => {
41
+ // On the <select> itself, like every other field control — an
42
+ // aria-label on the wrapper div would label nothing.
43
+ A("select.s-input", opts.inputAttrs, () => {
42
44
  applyControlAttrs(opts, id, isInvalid);
43
45
 
44
46
  A("change=", (e: Event) => {
@@ -1,5 +1,5 @@
1
1
  import A from "aberdeen";
2
- import { type Bindable, type Slot, type Attributes, drawSlot, uniqueId } from "../core.js";
2
+ import { cssZoom, type Bindable, type Slot, type Attributes, drawSlot, uniqueId } from "../core.js";
3
3
  import { mk } from "../icons-helpers.js";
4
4
 
5
5
  /** A single tab definition. */
@@ -143,9 +143,13 @@ export function revealInStrip(el: HTMLElement): void {
143
143
  // The overlays are 2.4em wide; clearing a little more than that keeps the
144
144
  // revealed item from sitting right against one.
145
145
  const pad = parseFloat(getComputedStyle(row).fontSize) * 2.6;
146
+ // The rects are in window coordinates while `scrollBy` counts in the row's
147
+ // own space; their difference scales over by the row's zoom (see `cssZoom`).
148
+ const z = cssZoom(row);
146
149
  const box = el.getBoundingClientRect(), strip = row.getBoundingClientRect();
147
- if (box.left < strip.left + pad) row.scrollBy({ left: box.left - strip.left - pad, behavior: "smooth" });
148
- else if (box.right > strip.right - pad) row.scrollBy({ left: box.right - strip.right + pad, behavior: "smooth" });
150
+ const left = (box.left - strip.left) / z, right = (box.right - strip.right) / z;
151
+ if (left < pad) row.scrollBy({ left: left - pad, behavior: "smooth" });
152
+ else if (right > -pad) row.scrollBy({ left: right + pad, behavior: "smooth" });
149
153
  }
150
154
 
151
155
  /**
@@ -32,7 +32,9 @@ A.insertGlobalCss({
32
32
  ".s-toasts":
33
33
  "position:fixed bottom:$3 right:$3 z-index:400 " +
34
34
  "display:flex flex-direction:column gap:$2 " +
35
- "pointer-events:none max-width:min(90vw,24rem) w:24rem",
35
+ // 90vw divided by --s-zoom: viewport units shrink with the page-fitting
36
+ // zoom (see `watchScale` in main.ts), and this cap means the window.
37
+ "pointer-events:none max-width:min(calc(90vw/var(--s-zoom,1)),24rem) w:24rem",
36
38
  ".s-toast": {
37
39
  "&":
38
40
  "display:flex align-items:flex-start gap:$2 " +
@@ -1,5 +1,5 @@
1
1
  import A from "aberdeen";
2
- import { type Slot, type Attributes, drawSlot, mountPortal } from "../core.js";
2
+ import { cssZoom, type Slot, type Attributes, drawSlot, mountPortal } from "../core.js";
3
3
 
4
4
  /** Options for {@link addTooltip}. */
5
5
  export interface TooltipOptions {
@@ -39,10 +39,12 @@ if (typeof window !== "undefined") {
39
39
  window.addEventListener("scroll", () => { $ttActive.value = undefined; }, { capture: true, passive: true });
40
40
  }
41
41
 
42
- function computePos(rect: DOMRect, tipW: number, tipH: number, placement: string): { x: number; y: number } {
42
+ function computePos(rect: DOMRect, tipW: number, tipH: number, placement: string, zoom: number): { x: number; y: number } {
43
+ // `rect` is handed over already in the tip's own coordinate space; the
44
+ // window's size has to be brought into it too (see `cssZoom`).
43
45
  const gap = 7;
44
- const vw = window.innerWidth;
45
- const vh = window.innerHeight;
46
+ const vw = window.innerWidth / zoom;
47
+ const vh = window.innerHeight / zoom;
46
48
  let x = 0, y = 0;
47
49
 
48
50
  if (placement === "bottom") {
@@ -96,7 +98,12 @@ mountPortal(() => {
96
98
 
97
99
  requestAnimationFrame(() => {
98
100
  if (!document.body.contains(tipEl)) return;
99
- const { x, y } = computePos(anchor.getBoundingClientRect(), tipEl.offsetWidth, tipEl.offsetHeight, placement);
101
+ // The anchor's rect is in window coordinates; the left/top set below live
102
+ // in the tip's own space — scale it over before computing (see `cssZoom`).
103
+ const z = cssZoom(tipEl);
104
+ const r = anchor.getBoundingClientRect();
105
+ const rect = new DOMRect(r.x / z, r.y / z, r.width / z, r.height / z);
106
+ const { x, y } = computePos(rect, tipEl.offsetWidth, tipEl.offsetHeight, placement, z);
100
107
  tipEl.style.left = x + "px";
101
108
  tipEl.style.top = y + "px";
102
109
  tipEl.style.visibility = "";
package/src/core.ts CHANGED
@@ -62,6 +62,25 @@ export interface ContentOptions {
62
62
  */
63
63
  export const NARROW_PX = 640;
64
64
 
65
+ /**
66
+ * The narrowest layout the library is designed for. A panel must work at this
67
+ * width (panels.ts sizes its columns from it), and a window narrower than this
68
+ * is shown the 360px layout scaled down to fit rather than squeezed further
69
+ * (see `watchScale` in main.ts).
70
+ */
71
+ export const MIN_PX = 360;
72
+
73
+ /**
74
+ * `el`'s effective CSS zoom: the factor between its own coordinate space and
75
+ * the window's — 1 wherever `zoom` isn't in play, or in browsers that predate
76
+ * it. A `getBoundingClientRect()` or pointer coordinate is in window space;
77
+ * divide it by this before using it as a length or scroll offset, which live
78
+ * in the element's own space. (`offsetWidth` and friends already do.)
79
+ */
80
+ export function cssZoom(el: Element): number {
81
+ return (el as { currentCSSZoom?: number }).currentCSSZoom ?? 1;
82
+ }
83
+
65
84
  let idCounter = 0;
66
85
  /** Generates a process-unique id, used to wire `<label for>` to its control. */
67
86
  export function uniqueId(prefix = "s"): string {
package/src/index.ts CHANGED
@@ -41,7 +41,7 @@ export { buttonGroup, type ButtonGroupOptions } from "./components/buttonGroup.j
41
41
  export { checkbox, type CheckboxOptions } from "./components/checkbox.js";
42
42
  export { form, type FormOptions } from "./components/form.js";
43
43
  export { main, closeNav, type MainOptions } from "./components/main.js";
44
- export { type PanelStack, type Panel, type Routes, type RouteHandler, type RouteTable, type AncestorsHandler, type AncestorTable, type PathParams, type SegParams } from "./components/panels.js";
44
+ export { type PanelStack, type Panel, type PanelSize, type Routes, type RouteHandler, type RouteTable, type AncestorsHandler, type AncestorTable, type PathParams, type SegParams } from "./components/panels.js";
45
45
  export { menu, menuButton, showFloatingMenu, addContextMenu, isFloatingMenuOpen, closeFloatingMenu, type MenuListOptions, type MenuOptions, type MenuEntry, type MenuItem, type MenuSeparator, type FloatingMenuOptions, type ContextMenuOptions } from "./components/menu.js";
46
46
  export { dialog, alert, confirm, prompt, isDialogOpen, type DialogOptions } from "./components/dialog.js";
47
47
  export { select, type SelectOptions, type SelectOptionInput } from "./components/select.js";
package/src/theme.ts CHANGED
@@ -59,16 +59,31 @@ import A from "aberdeen";
59
59
  */
60
60
 
61
61
  /**
62
- * The surface sheen: the subtle single-colour wash every surface (and the page)
63
- * is painted with, as a `background:` declaration. A shared constant rather
64
- * than a CSS custom property, deliberately: `var()`s inside a custom property
65
- * resolve where the property is *defined*, so a `--s-sheen` at `:root` would
66
- * paint every surface with the page's wash instead of its own `$s-bg`'s. Also
67
- * used by panels.ts, where the routed columns have to visually *be* the
68
- * surface around them.
62
+ * The subtle single-colour wash a surface is painted with, as a `background:`
63
+ * declaration, at the given angle. Shared constants rather than a CSS custom
64
+ * property, deliberately: `var()`s inside a custom property resolve where the
65
+ * property is *defined*, so a `--s-sheen` at `:root` would paint every surface
66
+ * with the page's wash instead of its own `$s-bg`'s.
69
67
  */
70
- export const SURFACE_SHEEN =
71
- "background: linear-gradient(170deg, color-mix(in oklab, $s-bg, white 9%), color-mix(in oklab, $s-bg, black 9%));";
68
+ const sheen = (angle: string) =>
69
+ `background: linear-gradient(${angle}, color-mix(in oklab, $s-bg, white 9%), color-mix(in oklab, $s-bg, black 9%));`;
70
+
71
+ /** The surface sheen: the wash every surface (and the page) is painted with. */
72
+ export const SURFACE_SHEEN = sheen("170deg");
73
+
74
+ /**
75
+ * The same wash, straight down — for panels.ts, where the routed columns and
76
+ * the ground beside them have to look like one continuous surface.
77
+ *
78
+ * A gradient at an angle takes its extent from the box's *width* as well as its
79
+ * height, so a 430px column and the 1720px region behind it would paint
80
+ * different slices of the same wash and meet at a visible step. Straight down,
81
+ * the extent is the height alone — which every column shares exactly with the
82
+ * region (a column is `top:0 bottom:0` in it) and so with every other column.
83
+ * The 10° of tilt is worth losing there; it buys the one place in the app where
84
+ * boxes of *different widths* must be seamless.
85
+ */
86
+ export const PANEL_SHEEN = sheen("180deg");
72
87
 
73
88
  const STORAGE_KEY = "staffa:darkMode";
74
89