@@ -30,17 +30,17 @@
type="button"
role="tab"
class="fader"
- :class="{ 'fader--active': axis.key === active, 'fader--lifting': axis.key === hot }"
+ :class="{ 'fader--active': axis.key === active, 'fader--rising': rising.has(axis.key) }"
:style="{ '--hue': `var(${axis.hue})`, '--level': levels[axis.key] ?? 0, '--i': i }"
:data-axis="axis.key"
:aria-selected="axis.key === active"
:aria-controls="`panel-${uid}`"
:tabindex="axis.key === active ? 0 : -1"
- @click="choose(axis.key)"
- @pointerenter="hoverKey = axis.key; lift(axis.key)"
+ @pointerdown="lastPointer = $event.pointerType"
+ @pointerenter="onEnter($event, axis.key)"
@pointerleave="hoverKey = null"
- @focus="focusKey = axis.key; lift(axis.key)"
- @blur="focusKey = null"
+ @focus="onFocus($event, axis.key)"
+ @click="onClick($event, axis.key)"
>
@@ -132,88 +132,132 @@ const tabs = ref([])
const current = computed(() => props.axes.find(a => a.key === active.value) ?? props.axes[0])
// ── Motion ──────────────────────────────────────────────────────────────
-// The cursors move on their own, slowly: each one drifts, and every few
-// beats another axis is pushed up towards émancipation. Visitors watch it;
-// they can still pick an axis (click, arrows), which stops the rotation.
+// Yvv's instructions, word for word: docs/consignes-yvv.md (29/09 and 03/11).
+//
+// Before any gesture (29/09): the cursors move on their own, slowly; each one
+// drifts, and every few beats another axis is pushed up towards émancipation.
// SSR renders every cursor at the bottom, so the first move is the rise.
//
-// Under the visitor's hand (hover or focus), the board holds still: the
-// hovered cursor rises, the others keep their level. From the first touch
-// on, what is up stays up — the visitor pushes the cursors, one by one.
+// The visitor's hand takes over for good (03/11): from the first hover,
+// keyboard focus or tap on a cursor, the rotation stops. The touched cursor
+// replays its rise, from the bottom to its own graduation, and its panel
+// shows. The others do not move; a cursor never touched stays where it was.
const BEAT_MS = 2600
const BEATS_PER_AXIS = 3
+/** Graduation of each axis, from asservissement (0) to émancipation (1) */
+const GRADUATION: Record = { numerique: 3 / 4, economie: 1 / 2, politique: 2 / 3 }
+/** A focus and a click within this delay are one gesture (a tap): one rise */
+const SAME_GESTURE_MS = 500
const levels = reactive>>({})
+/** Cursors replaying their rise (fast transition) */
+const rising = reactive(new Set())
const reduced = ref(false)
const visible = ref(true)
-const chosen = ref(false)
+const engaged = ref(false)
const pointerIn = ref(false)
const focusIn = ref(false)
const hoverKey = ref(null)
-const focusKey = ref(null)
-/** The cursor under the visitor's hand: it rises faster */
-const hot = computed(() => hoverKey.value ?? focusKey.value)
-/** Axes the visitor has pushed up: they never go back down */
-const pushed = new Set()
-let engaged = false
-let held = false
+let lastPointer = ''
+const lastRise: Partial> = {}
+const riseRun: Partial> = {}
let beat = 0
let timer: ReturnType | undefined
-function upLevel(i: number) {
- return reduced.value ? 0.94 : 0.87 + 0.06 * Math.sin(beat * 0.9 + i * 2.1)
-}
-
-function downLevel(i: number) {
- return reduced.value ? 0.3 : 0.3 + 0.14 * Math.sin(beat * 0.9 + i * 2.1)
-}
-
function place() {
props.axes.forEach((axis, i) => {
- const up = axis.key === active.value || pushed.has(axis.key)
- levels[axis.key] = up ? upLevel(i) : downLevel(i)
+ const isActive = axis.key === active.value
+ if (reduced.value) {
+ levels[axis.key] = isActive ? 0.94 : 0.3
+ return
+ }
+ const wave = Math.sin(beat * 0.9 + i * 2.1)
+ levels[axis.key] = isActive ? 0.87 + 0.06 * wave : 0.3 + 0.14 * wave
})
}
+function faderEl(key: AxisKey) {
+ return root.value?.querySelector(`[data-axis="${key}"]`) ?? null
+}
+
/** Level currently drawn (mid-transition included), read from the DOM */
function drawnLevel(key: AxisKey) {
- const el = root.value?.querySelector(`[data-axis="${key}"]`)
+ const el = faderEl(key)
const v = el ? Number.parseFloat(getComputedStyle(el).getPropertyValue('--level')) : Number.NaN
return Number.isFinite(v) ? v : (levels[key] ?? 0)
}
-/** Stop every cursor where it stands, except the one being lifted */
-function freeze() {
- for (const axis of props.axes) {
- if (axis.key !== hot.value) levels[axis.key] = drawnLevel(axis.key)
+/** First gesture: the rotation stops for good, every cursor stays where it is drawn */
+function engage() {
+ if (engaged.value) return
+ for (const axis of props.axes) levels[axis.key] = drawnLevel(axis.key)
+ engaged.value = true
+ clearInterval(timer)
+}
+
+/** The touched cursor replays its rise: back to the bottom, then up to its graduation */
+async function rise(key: AxisKey) {
+ lastRise[key] = Date.now()
+ const target = GRADUATION[key] ?? 0.75
+ if (reduced.value) {
+ levels[key] = target
+ return
+ }
+ const run = (riseRun[key] ?? 0) + 1
+ riseRun[key] = run
+ rising.delete(key)
+ levels[key] = 0
+ await nextTick()
+ // Commit the bottom (no transition) before the rise starts from it
+ const el = faderEl(key)
+ if (el) getComputedStyle(el).getPropertyValue('--level')
+ if (riseRun[key] !== run) return
+ rising.add(key)
+ levels[key] = target
+}
+
+/** A cursor under the visitor's hand: it rises and its panel shows */
+function touch(key: AxisKey) {
+ engage()
+ active.value = key
+ if (Date.now() - (lastRise[key] ?? 0) < SAME_GESTURE_MS) return
+ rise(key)
+}
+
+function onEnter(e: PointerEvent, key: AxisKey) {
+ // A finger scrolling over a cursor is not a rollover: taps go through the click
+ if (e.pointerType === 'touch') return
+ hoverKey.value = key
+ touch(key)
+}
+
+function isKeyboardFocus(el: HTMLElement) {
+ try {
+ return el.matches(':focus-visible')
+ }
+ catch {
+ return true
}
}
-function hold() {
- if (held) return
- held = true
- freeze()
+function onFocus(e: FocusEvent, key: AxisKey) {
+ // Keyboard focus is a rollover; a focus from the mouse or a tap is handled elsewhere
+ if (hoverKey.value === key || !isKeyboardFocus(e.currentTarget as HTMLElement)) return
+ touch(key)
}
-function release() {
- if (pointerIn.value || focusIn.value) return
- held = false
+function onClick(e: MouseEvent, key: AxisKey) {
+ const pointer = (e as PointerEvent).pointerType || lastPointer
+ lastPointer = ''
+ if (pointer === 'touch') touch(key)
+ else {
+ engage()
+ active.value = key
+ }
}
function onFocusOut(e: FocusEvent) {
focusIn.value = !!root.value?.contains(e.relatedTarget as Node | null)
- release()
-}
-
-/** The visitor touches an axis: it rises, nothing else moves or comes down */
-function lift(key: AxisKey) {
- if (!engaged) {
- engaged = true
- pushed.add(active.value)
- }
- pushed.add(key)
- const i = props.axes.findIndex(a => a.key === key)
- levels[key] = Math.max(drawnLevel(key), upLevel(i))
}
function advance() {
@@ -222,24 +266,18 @@ function advance() {
}
function onBeat() {
- if (!visible.value || held) return
+ if (!visible.value || engaged.value) return
beat++
- if (!chosen.value && beat % BEATS_PER_AXIS === 0) advance()
+ if (!pointerIn.value && !focusIn.value && beat % BEATS_PER_AXIS === 0) advance()
place()
}
-function choose(key: AxisKey) {
- chosen.value = true
- active.value = key
-}
-
-watch(active, (key) => {
- if (engaged) pushed.add(key)
- if (held) lift(key)
- else place()
+watch(active, () => {
+ if (!engaged.value) place()
})
function onKeydown(e: KeyboardEvent) {
+ lastPointer = ''
const keys = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End']
if (!keys.includes(e.key)) return
e.preventDefault()
@@ -250,7 +288,7 @@ function onKeydown(e: KeyboardEvent) {
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') next = idx === 0 ? last : idx - 1
if (e.key === 'Home') next = 0
if (e.key === 'End') next = last
- choose(props.axes[next]!.key)
+ touch(props.axes[next]!.key)
nextTick(() => tabs.value[next]?.focus())
}
@@ -345,9 +383,13 @@ onMounted(() => {
transition-delay: calc(var(--i) * 160ms);
}
-/* Under the visitor's hand, the cursor answers at once */
-.fader--lifting {
- transition: --level 0.9s cubic-bezier(0.22, 1, 0.36, 1);
+/* The visitor's hand has taken over: nothing glides any more but the touched cursor */
+.console--hand .fader {
+ transition: none;
+}
+
+.console--hand .fader--rising {
+ transition: --level 1.1s cubic-bezier(0.22, 1, 0.36, 1);
}
.fader:focus-visible {
@@ -615,7 +657,7 @@ onMounted(() => {
@media (prefers-reduced-motion: reduce) {
.fader,
- .fader--lifting,
+ .console--hand .fader--rising,
.fader-fill,
.fader-name,
.fader-lever,
diff --git a/docs/consignes-yvv.md b/docs/consignes-yvv.md
new file mode 100644
index 0000000..1196bd7
--- /dev/null
+++ b/docs/consignes-yvv.md
@@ -0,0 +1,48 @@
+# Consignes d'Yvv — design du site
+
+Ce fichier rassemble les consignes qu'Yvv donne en conversation, avec leur date. Les consignes
+entre guillemets sont recopiées mot pour mot ; les autres le disent. Elles l'emportent sur les choix
+d'interprétation des sessions.
+
+Pourquoi ce fichier : la graduation de la console, donnée le 29/09, s'est perdue avec le transcript
+où elle était écrite. Une consigne qui ne vit que dans une conversation finit par disparaître.
+
+## Accueil — la console des trois curseurs
+
+Code : `app/components/home/HomeConsole.vue`. Libellés : `site/pages/home.yml > console`.
+
+### 29/09/2026
+
+Le transcript a été effacé. Ces quatre consignes sont reconstituées d'après le commit `201cfbb` et le
+relais de la session orch le 03/11 : ce ne sont pas les mots d'Yvv.
+
+- l'axe de la console a Asservissement en bas et Émancipation en haut ;
+- une animation où les curseurs bougent lentement, pour qu'on ne croie pas devoir les manipuler ;
+- « AUTONOMIE » devant les légendes des curseurs ;
+- « Autonomie numérique, économique, citoyenne » et « Code source, création monétaire, décision »
+ mis en valeur.
+
+### 03/11/2026, vers 03h58 (à la session orch)
+
+Mot pour mot, d'après `~/harness/etat/reponses-yvv.md` :
+
+> « Et retrouve les consignes, comme par exemple celles pour l'animation (curseurs qui montent au
+> rollover et ne fait pas descendre les autres, je t'avais donné une graduation). »
+
+### 03/11/2026, entre 05h40 et 05h45 (relayée mot pour mot par la session orch)
+
+> « Je t'avais dit déjà tout ça. Au rollover, numérique monte à 3/4, économie monte à 1/2, citoyenne
+> monte à 2/3. Les curseurs restent. Si on rollover à nouveau ils refont leur montée. Et le pavé
+> correspondant s'affiche bien sûr au même rollover. »
+
+Ce que fait le code :
+- Un survol à la souris, un focus au clavier ou un tap sur un curseur arrête pour de bon la rotation
+ automatique.
+- Le curseur touché repart d'en bas et monte en 1,1 s à son cran : numérique 0,75, économique 0,50,
+ citoyenne 0,667. Il y reste.
+- Le panneau de l'axe s'affiche au même geste.
+- Les autres curseurs ne bougent pas. Un curseur jamais touché reste là où il était.
+- Un nouveau survol rejoue la montée.
+- Avant tout geste, l'animation du 29/09 est inchangée.
+- Sous `prefers-reduced-motion`, il n'y a aucune transition, mais les crans sont les mêmes.
+- Un défilement au doigt qui part d'un curseur ne compte pas comme un survol.