Files
librodrome/app/components/ui/OiseauPose.vue
T
YvvandClaude Opus 5.5 bb8fad9ff3 identité 5 : la liste fermée des règles mesurées, appliquée à toutes les pages
Ordre IDENTITE-DA-5 (03/11 14h41, relayé par la session orch ; seuils confirmés par DA) :
- docs/da-oiseaux.md : « Règles mesurées (liste fermée) », règles a à i avec leurs seuils, telles
  que le contrôle les mesure (pixels peints, ligne de base), et la phrase juste sur l'acrobate ;
- UiOiseauPose : pose `bout`, une ancre sans taille dans le texte, sur sa ligne de base, pour un
  oiseau au bout (ou au début) d'un titre ;
- au bout de leur titre : trapéziste, clown, marionnettiste, DJ (/en-musique), droneuse, vigie,
  alchimiste, relieur, et l'oiseau de l'intitulé au téléphone ;
- authentification : le pompeur de Ğ1 perché sur la carte du projet (plus de colonne en marge) ;
- accueil : l'accordéoniste au bout du titre du format (plus de paire avec la lectrice, plus de
  trait sur le lien) ;
- soudeuse sur la ligne de base de son titre ; administrateur réseau et cartographe sortent par le
  côté de leur carte, pas par un coin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:07:35 +01:00

171 lines
5.8 KiB
Vue

<script setup lang="ts">
// A bird of the librodrome placed on its block (composition of 03/11, docs/consignes-yvv.md):
// it stands on a real edge of the page, next to the content its trade is about.
// The host block is the positioning context: position relative, overflow visible.
//
// Poses
// perche on the block's top edge (bottom: 100%), x with left / right
// marge-g beside the block, on its left; feet at the block's bottom, raised by `bottom`
// marge-d the same, on its right
// derriere large and pale, behind the block (z-index -1): only what sticks out shows.
// Needs a stacking context above the block (isolation: isolate) and a block
// that creates none itself (no z-index, transform, opacity, filter).
// libre placed only by left / right / top / bottom (an empty area next to its block)
//
// `bout`: a perched bird at the end (or the start) of a line of a title. The component is then a
// zero-size anchor in the text, at its baseline, written right after the text (the gap is `left`,
// 12 px by default) or right before it (the gap is `right`): the bird follows the end of the text
// at every width, wherever the line breaks (rule e, docs/da-oiseaux.md).
//
// Birds in the margin, behind a block or free need room around the column: they show from
// 1200 px wide, the perched ones from 768 px (`seuil` overrides, for instance 1024 for a perched
// bird whose block has no void above it until the grid splits in two); under 768 px, only a
// bird marked `mobile`.
// A perched bird needs, above its edge, a void at least as tall as itself (DA rule of 03/11);
// where blocks stand close, the bird goes in the margin or behind the block.
//
// Drawing rules of the studio DA (kit README, §2.5): one colour (currentColor), proportions
// intact and never cropped, 72 px wide at least, opacity 0.18-0.28 next to text and 0.10-0.14
// large behind a card, mirror allowed except for the calligraphe and the typographe.
// Birds standing on an edge do not move; only the one behind a card drifts a little (Yvv, 03/11).
import { OISEAUX } from '~/utils/oiseaux'
type Pose = 'perche' | 'marge-g' | 'marge-d' | 'derriere' | 'libre'
const props = withDefaults(defineProps<{
nom: string
pose?: Pose
/** Width in px: S 72-90, M 108-156, L 200-250 */
w?: number
left?: string
right?: string
top?: string
bottom?: string
/** Gap between the block and a bird in the margin */
ecart?: string
retourne?: boolean
/** Shown under 768 px (one bird per page at most) */
mobile?: boolean
/** Shown under 768 px only: the phone place of a bird placed elsewhere on wider screens */
mobileSeul?: boolean
/** Narrowest window (px) where the bird shows: 1200 by default for the margin, behind
* and free poses, 768 for a perched bird */
seuil?: 768 | 1024 | 1200
ton?: 'primary' | 'accent'
op?: number
/** At the end (or the start) of a line of text: written in the text, without a space */
bout?: boolean
}>(), { pose: 'perche', w: 80, ecart: '2.5rem', ton: 'primary' })
/** These two write and compose from left to right: never mirrored */
const NO_MIRROR = new Set(['calligraphe', 'typographe'])
const sol = computed(() => OISEAUX[props.nom]?.sol ?? 5)
const style = computed(() => {
const s: Record<string, string> = { 'width': `${Math.max(72, props.w)}px`, '--sol': `${sol.value}%` }
if (props.pose === 'perche') s.bottom = props.bout ? '0px' : '100%'
if (props.bout && props.left === undefined && props.right === undefined) s.left = '12px'
if (props.pose === 'marge-g') s.right = `calc(100% + ${props.ecart})`
if (props.pose === 'marge-d') s.left = `calc(100% + ${props.ecart})`
if (props.pose === 'marge-g' || props.pose === 'marge-d') s.bottom = '0px'
for (const k of ['left', 'right', 'top', 'bottom'] as const) {
if (props[k] !== undefined) s[k] = props[k]!
}
if (props.op !== undefined) s.opacity = String(props.op)
return s
})
const classes = computed(() => [
'oiseau-pose',
`oiseau-pose--${props.pose}`,
{
'oiseau-pose--retourne': props.retourne && !NO_MIRROR.has(props.nom),
'oiseau-pose--mobile': props.mobile || props.mobileSeul,
'oiseau-pose--mobile-seul': props.mobileSeul,
'oiseau-pose--large': (props.seuil ?? (props.pose === 'perche' ? 768 : 1200)) >= 1200,
'oiseau-pose--moyen': props.seuil === 1024,
'oiseau-pose--accent': props.ton === 'accent',
},
])
</script>
<template>
<span v-if="bout" class="oiseau-bout"><UiOiseau :nom="nom" :class="classes" :style="style" :data-oiseau="nom" :data-pose="pose" /></span>
<UiOiseau v-else :nom="nom" :class="classes" :style="style" :data-oiseau="nom" :data-pose="pose" />
</template>
<style scoped>
/* The anchor of a bird at the end of a line: no size, on the text's baseline */
.oiseau-bout {
position: relative;
display: inline-block;
width: 0;
height: 0;
vertical-align: baseline;
}
.oiseau-pose {
position: absolute;
z-index: 1;
pointer-events: none;
color: hsl(var(--color-primary));
opacity: 0.24;
/* The drawing's ground line lands on the edge */
transform: translateY(var(--sol));
}
.oiseau-pose--accent {
color: hsl(var(--color-accent));
}
.oiseau-pose--perche {
opacity: 0.28;
}
.oiseau-pose--derriere {
z-index: -1;
opacity: 0.12;
animation: oiseau-derive 12s ease-in-out infinite alternate;
}
.oiseau-pose--retourne {
scale: -1 1;
}
@keyframes oiseau-derive {
from { translate: 0 0; }
to { translate: 0 -6px; }
}
@media (max-width: 1199px) {
.oiseau-pose--large {
display: none;
}
}
@media (max-width: 1023px) {
.oiseau-pose--moyen {
display: none;
}
}
@media (min-width: 768px) {
.oiseau-pose--mobile-seul {
display: none;
}
}
@media (max-width: 767px) {
.oiseau-pose:not(.oiseau-pose--mobile) {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.oiseau-pose--derriere {
animation: none;
}
}
</style>