From 6457347dd65840ef5dfecc08d8d04d941fb21e54 Mon Sep 17 00:00:00 2001 From: Paul Roberts Date: Sat, 6 Jun 2026 11:37:57 +0000 Subject: [PATCH] docs(patterns): add 'Patterns We Like' scrapbook + link from README A running list of UI effects/interactions worth keeping, each with the four-point bar (does a job, respects reduced-motion, transform/opacity only, degrades) it must clear before shipping. README structure + quick-start now point to it. --- README.md | 3 +- docs/patterns/README.md | 90 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 docs/patterns/README.md diff --git a/README.md b/README.md index bf08a5c..0050195 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ KDCDesignSystem/ ├── components/ # Per-component specs and anatomy ├── themes/ # Light, dark, high-contrast theme overrides ├── templates/ # Web, mobile, email, print, presentation -├── docs/ # Getting started, principles, a11y, voice & tone +├── docs/ # Getting started, principles, patterns, a11y, voice & tone ├── examples/ # Reference implementations └── scripts/ # Token build / export scripts ``` @@ -36,6 +36,7 @@ KDCDesignSystem/ 1. Read [`docs/getting-started/README.md`](./docs/getting-started/README.md). 2. Skim [`design.md`](./design.md) for the token vocabulary. 3. Pick your platform under [`examples/`](./examples/). +4. Browse [`docs/patterns/README.md`](./docs/patterns/README.md) — a scrapbook of UI effects & interactions we like (and the bar each clears before shipping). ## Status diff --git a/docs/patterns/README.md b/docs/patterns/README.md new file mode 100644 index 0000000..7815fbc --- /dev/null +++ b/docs/patterns/README.md @@ -0,0 +1,90 @@ +# Patterns We Like + +A running scrapbook of UI effects, interactions and micro-patterns we've come across and want to keep — so a future app or project can reach for something proven instead of reinventing it. + +This list sits slightly apart from the canonical [principles](../principles/README.md) on purpose. Those say *"clarity over cleverness"* and *"motion is feedback, never decorates."* The entries here are often **delight** effects that flirt with that line. That's the point of the doc: keep the tempting stuff in one place **and** record the bar each must clear before it earns a spot in a shipping product. A pattern we like is not a pattern we'll always use. + +## The bar (when a delight effect earns its place) + +Before any effect below ships in a KDC product, it has to clear all four: + +- ✅ **It does a job.** It communicates state, affordance, continuity or confirmation — not just "looks nice." If it's purely decorative, it doesn't ship (per [Motion](../../foundations/motion/README.md)). +- ✅ **It respects `prefers-reduced-motion: reduce`** — collapses to a static/instant state, no exceptions. +- ✅ **It animates `transform` / `opacity` only** — never `width` / `height` / layout. Compositor-friendly or it doesn't go in. +- ✅ **It degrades gracefully** — works without it, and doesn't trap focus, block input, or break on keyboard/touch. + +If an entry can't meet the bar in a given spot, that's a "no" for that spot — not a reason to drop the pattern from the list. + +## How to add an entry + +Keep it light — append a block in this shape. Don't turn it into a form. + +``` +### +**Status:** idea | trialed | adopted-in- +**What it is:** one line. +**Why we like it:** one or two lines. +**Snippet:** the smallest working version. +**Fits / avoid:** where it earns its place, and where it doesn't. +**Watch-outs:** the honest gotchas — performance, a11y, edge cases. +**Found:** where it came from (link / who / when). +``` + +**Status legend** — `idea` (caught our eye, untried) · `trialed` (prototyped in a branch) · `adopted-in-` (shipped, name the app). + +--- + +## Entries + +### Proximity-magnification dock + +**Status:** idea + +**What it is:** macOS-dock-style hover magnification — across a row (or column) of equal items, the one nearest the cursor scales up, neighbours scale less, tapering to nothing past a set radius. The bulge follows the pointer. + +**Why we like it:** tiny amount of code, self-resetting (far items naturally land back at scale 1), and `scale` is compositor-cheap. Used in the right place it doubles as a targeting affordance — it literally points at the thing you're about to click. + +**Snippet** — the whole trick is "distance → 0..1 closeness → size": + +```js +// Vanilla, horizontal. 120 = reach in px; 0.5 = max growth (→ 1.5×). +onpointermove = e => + document.querySelectorAll(".dock > *").forEach(el => { + const r = el.getBoundingClientRect(); + const t = Math.max(0, 1 - Math.abs(e.clientX - (r.x + r.width / 2)) / 120); + el.style.scale = 1 + t * 0.5; + }); +``` + +In our apps (all React 19) it belongs in an effect with cached rects + rAF + cleanup, and gated on reduced-motion: + +```jsx +useEffect(() => { + if (matchMedia("(prefers-reduced-motion: reduce)").matches) return; // clears the bar + const items = [...dockRef.current.children]; + let rects = items.map(el => el.getBoundingClientRect()); // cache — don't measure per-move + const remeasure = () => { rects = items.map(el => el.getBoundingClientRect()); }; + const onMove = e => requestAnimationFrame(() => items.forEach((el, i) => { + const c = rects[i].x + rects[i].width / 2; // use clientY + rect.y for a vertical dock + const t = Math.max(0, 1 - Math.abs(e.clientX - c) / 120); + el.style.scale = 1 + t * 0.5; + })); + window.addEventListener("pointermove", onMove); + window.addEventListener("resize", remeasure); + return () => { window.removeEventListener("pointermove", onMove); window.removeEventListener("resize", remeasure); }; +}, []); +``` + +**Fits / avoid:** +- ✅ A row/column of **≥ 5 equal icon targets you actually click** — tool palettes, icon rails, app launchers. (KDC candidate: the floor-plan tool palette in `kdc_void_planner` — `Palette.tsx`, a vertical rail of ~10 tool buttons. Vertical, so drive it off `clientY`.) +- ❌ **2–3 item navs** — the ripple is invisible with so few items. +- ❌ **Step indicators / status strips** — those communicate *progress/state*, not click targets. Magnifying them is semantically confusing and fails the bar (it does no job). + +**Watch-outs:** +- **Layout thrash.** `getBoundingClientRect()` per `pointermove` forces synchronous layout. Cache the rects (re-measure on `resize`/`scroll`), and batch the writes in `requestAnimationFrame`. +- **Horizontal-only** as written (`clientX`). Swap to `clientY` + `rect.y` for a vertical rail. +- **Neighbours overlap rather than push apart.** `scale` doesn't reflow, so a growing item grows *over* its neighbours instead of displacing them (the real Dock shoves siblings outward). Looks fine with generous gaps/padding; less polished without. +- **`transform-origin`.** For a docked edge, set the origin to that edge (e.g. `transform-origin: bottom` / `left`) so items grow *out of* the dock, not in both directions. +- **Reduced motion + input parity.** Bail when `prefers-reduced-motion` is set; never let the scale affect hit-testing/keyboard order. + +**Found:** Paul, May 2026 — a "proximity hack" snippet. Decoded + React-adapted in-session.