feat(eidos): drag pickup-lift as a global motion token (--motion-scale-lift)

A grabbed surface scales slightly toward the viewer (depth 'picked up' cue). This
is a cross-component primitive, not a float-panel literal:

- New --motion-scale-lift (1.02) in STATIC_MOTION.scale + MOTION_SCALE_KEYS (the
  only >1 in the scale family); validated by validateEidosConfig.
- FloatPanel consumes it: [data-float-panel-content][data-dragging] { scale:
  var(--motion-scale-lift) } + will-change: translate, scale, eased lift/settle,
  static during the move (composited), suppressed under reduced-motion.
- Documented in eidos-motion.md §11 (primitives table) and THEMING.md §14 (new
  'Superficies arrastrables: el lift de pickup' pattern) so any draggable surface
  (slider thumb, sortable item, drawer) reuses the SAME token, not its own literal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
active-uix
dev 4 months ago
parent b3ef470646
commit 1e6a9df874

@ -1358,6 +1358,44 @@ Añade al recipe del componente si:
- Es específico (un slider drag, una calendar swap).
- No reacciona a una señal de sema, sino a un `data-state` transition.
### Superficies arrastrables: el lift de "pickup"
Cuando el usuario **agarra y arrastra** una superficie, ésta debe **subir hacia
él** (profundidad: "lo he cogido"). Es un cue transversal — NO específico de un
componente — así que el factor de escala es un **token global de motion**, no un
literal por recipe:
| Token | Valor | Uso |
|---|---|---|
| `--motion-scale-lift` | `1.02` | escala de pickup al arrastrar (el único `>1` de la familia `--motion-scale-*`) |
**Patrón (cualquier draggable lo aplica igual)** — gateado por el data-attr de
arrastre que su morfo declara (`data-dragging`, `data-grabbed`, …), emparejado
con una elevación de sombra, y suprimido bajo reduced-motion:
```css
/* float-panel, un thumb de slider arrastrándose, un item sortable, un drawer… */
[data-x][data-dragging] {
scale: var(--motion-scale-lift); /* sube hacia el usuario */
box-shadow: var(--…-shadow-active); /* + eleva la sombra */
}
@media (prefers-reduced-motion: reduce) {
[data-x][data-dragging] { scale: 1; transition: none; }
}
```
Reglas:
- **`scale` (no `transform`)** para componer con la posición, que viaja por
`translate` (propiedades distintas) → el lift no pelea con el arrastre 1:1.
- El cambio de `scale` se **transiciona** (lift al agarrar / settle al soltar);
durante el move queda **estático** (compuesto, sin coste por frame).
- `will-change: translate, scale` mientras dura el gesto.
- Un theme retematiza el lift en `STATIC_MOTION.scale.lift` (`primitives/static.ts`)
— todos los draggables lo heredan. No redefinir el 1.02 por componente.
Hoy lo consume `float-panel` (`[data-float-panel-content][data-dragging]`); un
slider/sortable que añada arrastre debe leer el MISMO token, no inventar el suyo.
---
## 15. Comparación con librerías de referencia

@ -26,10 +26,13 @@
font-size: var(--float-panel-font-size);
line-height: var(--float-panel-line-height);
box-shadow: var(--float-panel-shadow);
/* Only the elevation lift (→ topmost) is eased here. Position (`translate`)
is never transitioned so the drag stays 1:1; presence is the preset's job. */
transition: box-shadow var(--float-panel-transition-duration)
var(--float-panel-transition-ease);
/* The elevation lift (→ topmost) AND the drag pickup-scale are eased here.
Position (`translate`) is never transitioned so the drag stays 1:1; presence
(scale on `[data-state]`) is the motion preset's job — distinct from this
steady-state `scale` transition, which only fires on the grab/settle. */
transition:
box-shadow var(--float-panel-transition-duration) var(--float-panel-transition-ease),
scale var(--float-panel-transition-duration) var(--float-panel-transition-ease);
}
/* The focused/frontmost panel lifts. */
@ -51,6 +54,15 @@
box-shadow: var(--float-panel-shadow-active);
}
/* Dragging also lifts the surface TOWARD the user (a subtle scale) — the depth
"pickup" cue paired with the elevation shadow. Move only: resize changes the
real size, so scaling it too would read wrong. `scale` composes with the drag
`translate` (distinct properties), is static during the move (composited, no
per-frame cost), and eases in on grab / out on settle via the base transition. */
[data-float-panel-content][data-dragging] {
scale: var(--motion-scale-lift);
}
[data-float-panel-content]:focus-visible {
outline: none;
box-shadow:
@ -323,4 +335,17 @@
/* Reduced-motion for the open/close is handled by the motion system itself
(the `scale-fade` preset degrades to opacity-only under `[data-motion='reduce']`
+ `@media (prefers-reduced-motion)`), so no recipe-level override is needed. */
+ `@media (prefers-reduced-motion)`), so no recipe-level override is needed.
The drag pickup-scale IS a recipe transition, so suppress it here — the shadow
lift alone still conveys the grab. */
[data-motion='reduce'] [data-float-panel-content][data-dragging],
[data-float-panel-content][data-dragging][data-motion='reduce'] {
scale: 1;
transition: none;
}
@media (prefers-reduced-motion: reduce) {
[data-float-panel-content][data-dragging] {
scale: 1;
transition: none;
}
}

@ -437,7 +437,7 @@ Tokens existentes (`STATIC_MOTION`, generados, leídos por css y js):
| `--duration-{k}` | `instant 0` · `fast 120` · `normal 180` · `moderate 240` · `slow 320` ms |
| `--ease-{k}` | `default` · `out` · `in` · `spring` · `alert` · `symmetric` |
| `--motion-distance-{k}` | `xs 2` · `sm 4` · `md 8` · `lg 16` px |
| `--motion-scale-{k}` | `enter 0.985` · `press 0.97` (fade-through usa `0.92` — F6 lo tokeniza) |
| `--motion-scale-{k}` | `enter 0.985` · `press 0.97` · `through 0.92` (fade-through) · `lift 1.02` (pickup de drag — el único `>1`, ver §Draggable surfaces en THEMING) |
**Vars de override (runtime, NO tokens de theme)** — un componente las pone en su
elemento; el preset las lee con fallback al token:

@ -229,6 +229,7 @@
--motion-scale-enter: 0.985;
--motion-scale-press: 0.97;
--motion-scale-through: 0.92;
--motion-scale-lift: 1.02;
--motion-stagger: 20ms;
--icon-size-xxs: calc(12px * var(--scaling));
--icon-size-xs: calc(14px * var(--scaling));

@ -509,7 +509,7 @@ export const EASE_KEYS = [
'emphasized'
] as const;
export const MOTION_DISTANCE_KEYS = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
export const MOTION_SCALE_KEYS = ['enter', 'press', 'through'] as const;
export const MOTION_SCALE_KEYS = ['enter', 'press', 'through', 'lift'] as const;
export const ICON_STROKE_WIDTH_KEYS = ['sm', 'md', 'lg'] as const;
export type DurationKey = (typeof DURATION_KEYS)[number];

@ -157,7 +157,12 @@ export const STATIC_MOTION = {
enter: '0.985',
press: '0.97',
// Fade-through (Material) incoming scale.
through: '0.92'
through: '0.92',
// Pickup LIFT — a grabbed surface scales slightly TOWARD the viewer (depth
// "picked up" cue). Cross-component: any draggable surface (float-panel,
// a slider thumb, a sortable item…) applies it on its grab/drag state,
// paired with an elevation shadow. >1 (the only grow in the scale family).
lift: '1.02'
},
stagger: '20ms',
// Expressive set (Carbon's productive/expressive split): emphasized easing

@ -1019,7 +1019,7 @@ export class FloatPanelContentProvider {
el.style.top = '0px';
el.style.pointerEvents = 'auto';
el.style.zIndex = String(p.zIndex);
el.style.willChange = active ? 'translate' : '';
el.style.willChange = active ? 'translate, scale' : '';
el.style.userSelect = active ? 'none' : '';
// Geometry: only when no gesture is driving the element directly.
// Round to whole pixels — `translate` is a transform and doesn't snap to

Loading…
Cancel
Save

Powered by TurnKey Linux.