@krizaka/ui 3.41.1 → 3.42.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.
- package/dist/bird-plate.js +1 -1
- package/dist/{chunk-M4DBVG43.js → chunk-MHUUNC5W.js} +4 -3
- package/dist/chunk-MHUUNC5W.js.map +1 -0
- package/dist/{chunk-EZC54B5C.js → chunk-MIMOTE4W.js} +3 -3
- package/dist/{chunk-EZC54B5C.js.map → chunk-MIMOTE4W.js.map} +1 -1
- package/dist/{chunk-CC2FBLLR.js → chunk-P4TTKYC4.js} +2 -2
- package/dist/chunk-P4TTKYC4.js.map +1 -0
- package/dist/flock.d.ts +7 -1
- package/dist/flock.js +386 -50
- package/dist/flock.js.map +1 -1
- package/dist/mascot-quote.js +1 -1
- package/dist/mascot-state.js +1 -1
- package/dist/mascots.d.ts +3 -1
- package/dist/mascots.js +1 -1
- package/dist/nav-bar.js +1 -1
- package/dist/site-footer.d.ts +6 -4
- package/dist/site-footer.js +30 -6
- package/dist/site-footer.js.map +1 -1
- package/dist/sites.js +1 -1
- package/dist/sites.js.map +1 -1
- package/package.json +6 -6
- package/registry/examples/flock/scenes.d.ts +4 -0
- package/registry/examples/flock/scenes.js +28 -0
- package/registry/examples/flock/scenes.tsx +24 -0
- package/registry/flock.json +33 -5
- package/registry/index.json +8 -8
- package/registry/map.json +1 -1
- package/registry/mascots.json +15 -1
- package/registry/nav-bar.json +1 -1
- package/registry/rail-nav.json +1 -1
- package/registry/site-footer.json +16 -2
- package/registry/sites.json +1 -1
- package/dist/chunk-CC2FBLLR.js.map +0 -1
- package/dist/chunk-M4DBVG43.js.map +0 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/flock/scenes.tsx
|
|
4
|
+
import { Flock } from "@krizaka/ui/flock";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
function FlockScenes() {
|
|
7
|
+
return /* @__PURE__ */ jsx(
|
|
8
|
+
Flock,
|
|
9
|
+
{
|
|
10
|
+
className: "max-w-4xl",
|
|
11
|
+
ratio: 16 / 5,
|
|
12
|
+
scenes: true,
|
|
13
|
+
tempo: 1.4,
|
|
14
|
+
birds: [
|
|
15
|
+
{ bird: "pigeon", x: 10, y: 96, size: 11 },
|
|
16
|
+
{ bird: "owl", x: 24, y: 96, size: 11 },
|
|
17
|
+
{ bird: "lovebird", x: 38, y: 96, size: 11 },
|
|
18
|
+
{ bird: "flamingo", x: 52, y: 96, size: 13 },
|
|
19
|
+
{ bird: "crane", x: 66, y: 96, size: 11 },
|
|
20
|
+
{ bird: "magpie", x: 79, y: 96, size: 11 },
|
|
21
|
+
{ bird: "beaver", x: 92, y: 96, size: 11 }
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
FlockScenes as default
|
|
28
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Flock } from "@krizaka/ui/flock";
|
|
2
|
+
|
|
3
|
+
// The scenes: each bird does what it promises, one thing at a time and never twice the same. The owl reads a book, the
|
|
4
|
+
// flamingo speaks and its neighbour answers, the crane steps out and bows, the beaver taps a log, the magpie takes a
|
|
5
|
+
// shiny point — and the pigeon hands its parcel over, which the receiver opens in its own way.
|
|
6
|
+
export default function FlockScenes() {
|
|
7
|
+
return (
|
|
8
|
+
<Flock
|
|
9
|
+
className="max-w-4xl"
|
|
10
|
+
ratio={16 / 5}
|
|
11
|
+
scenes
|
|
12
|
+
tempo={1.4}
|
|
13
|
+
birds={[
|
|
14
|
+
{ bird: "pigeon", x: 10, y: 96, size: 11 },
|
|
15
|
+
{ bird: "owl", x: 24, y: 96, size: 11 },
|
|
16
|
+
{ bird: "lovebird", x: 38, y: 96, size: 11 },
|
|
17
|
+
{ bird: "flamingo", x: 52, y: 96, size: 13 },
|
|
18
|
+
{ bird: "crane", x: 66, y: 96, size: 11 },
|
|
19
|
+
{ bird: "magpie", x: 79, y: 96, size: 11 },
|
|
20
|
+
{ bird: "beaver", x: 92, y: 96, size: 11 },
|
|
21
|
+
]}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
package/registry/flock.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"name": "flock",
|
|
3
3
|
"type": "primitive",
|
|
4
4
|
"title": "Flock",
|
|
5
|
-
"summary": "The Krizaka birds standing on a stage, alive: each at ease in character, two neighbours glancing at each other, the pigeon carrying its parcel across, a bird looking up when you come near or tap it.",
|
|
6
|
-
"description": "The Krizaka birds standing on a stage, alive: each at ease in character, two neighbours glancing at each other, the pigeon carrying its parcel across, a bird looking up when you come near or tap it.",
|
|
5
|
+
"summary": "The Krizaka birds standing on a stage, alive: each at ease in character, two neighbours glancing at each other, the pigeon carrying its parcel across, a bird looking up when you come near or tap it — and, with `scenes`, a short scene for each bird.",
|
|
6
|
+
"description": "The Krizaka birds standing on a stage, alive: each at ease in character, two neighbours glancing at each other, the pigeon carrying its parcel across, a bird looking up when you come near or tap it — and, with `scenes`, a short scene for each bird.",
|
|
7
7
|
"status": "beta",
|
|
8
8
|
"deprecated": null,
|
|
9
9
|
"category": "blocks",
|
|
@@ -44,13 +44,15 @@
|
|
|
44
44
|
"Give the stage the ratio of its backdrop (`ratio`) and the birds' places in percent: they keep their positions at any width.",
|
|
45
45
|
"Keep each bird to its job: the falcon watches, the owl reads, the duck builds, the flamingo answers, the pigeon delivers.",
|
|
46
46
|
"Make it the page's own: `active` is the bird of the product the reader is on (it looks up and jumps), `greet` has every bird jump once as the flock comes into view, `tempo` makes the moments more frequent.",
|
|
47
|
+
"`scenes` gives each bird a short scene true to its promise (the owl reads a book, the flamingo and the lovebird talk, the crane bows, the beaver taps a log, the magpie takes a shiny point, the duck builds, the falcon scans) and has the pigeon hand its parcel over, which the receiver opens in its own way. One at a time, at random, with long pauses now and then: leave `tempo` near 1 to 1.7.",
|
|
47
48
|
"Turn a moment off with `glance={false}`, `courier={false}` or `react={false}` on a page that already moves."
|
|
48
49
|
],
|
|
49
50
|
"accessibility": {
|
|
50
51
|
"keyboard": [],
|
|
51
52
|
"notes": [
|
|
52
53
|
"Decorative (`aria-hidden`): it never carries information; put the words beside it.",
|
|
53
|
-
"Under `prefers-reduced-motion` the birds are still: no idle motion, no glance, no delivery, no reaction.",
|
|
54
|
+
"Under `prefers-reduced-motion` the birds are still: no idle motion, no glance, no delivery, no scene, no reaction.",
|
|
55
|
+
"It waits for the page to settle (`html[data-motion=\"settling\"]`) and pauses off screen (the stage is a `data-motion-scope`).",
|
|
54
56
|
"The moments pause while the flock is off screen and while the tab is hidden; there is no frame loop."
|
|
55
57
|
]
|
|
56
58
|
},
|
|
@@ -124,6 +126,13 @@
|
|
|
124
126
|
"description": "A bird looks up when the pointer comes near, and jumps a little when tapped. Default `true`.",
|
|
125
127
|
"required": false
|
|
126
128
|
},
|
|
129
|
+
{
|
|
130
|
+
"name": "scenes",
|
|
131
|
+
"type": "boolean",
|
|
132
|
+
"default": "false",
|
|
133
|
+
"description": "Scenes, true to each bird's promise: the owl reads a book, the flamingo and the lovebird talk with a speech mark, the\ncrane steps and bows, the beaver taps a log, the magpie takes a shiny point, the duck builds, the falcon scans — and the\npigeon hands its parcel over, which the receiver opens in its own way. One at a time, at random. Default `false`.",
|
|
134
|
+
"required": false
|
|
135
|
+
},
|
|
127
136
|
{
|
|
128
137
|
"name": "tempo",
|
|
129
138
|
"type": "number",
|
|
@@ -150,6 +159,14 @@
|
|
|
150
159
|
"module": "@krizaka/ui/registry/examples/flock/ridge",
|
|
151
160
|
"code": "import { Flock } from \"@krizaka/ui/flock\";\n\n// The five of the story on a ridge: the falcon watches, the owl reads, the duck builds, the flamingo answers and the\n// pigeon delivers. Places are percents of the stage, so they hold at any width.\nexport default function FlockRidge() {\n return (\n <Flock\n className=\"max-w-3xl\"\n ratio={16 / 6}\n birds={[\n { bird: \"falcon\", x: 20, y: 96, size: 13 },\n { bird: \"owl\", x: 35, y: 96, size: 13 },\n { bird: \"duck\", x: 50, y: 96, size: 13 },\n { bird: \"flamingo\", x: 65, y: 96, size: 13 },\n { bird: \"pigeon\", x: 80, y: 96, size: 13 },\n ]}\n />\n );\n}\n"
|
|
152
161
|
},
|
|
162
|
+
{
|
|
163
|
+
"name": "scenes",
|
|
164
|
+
"title": "The scenes",
|
|
165
|
+
"description": "The owl reads, the flamingo and the lovebird talk, the crane bows, the beaver taps a log, the magpie takes a shiny point, and the pigeon hands its parcel over.",
|
|
166
|
+
"path": "examples/flock/scenes.tsx",
|
|
167
|
+
"module": "@krizaka/ui/registry/examples/flock/scenes",
|
|
168
|
+
"code": "import { Flock } from \"@krizaka/ui/flock\";\n\n// The scenes: each bird does what it promises, one thing at a time and never twice the same. The owl reads a book, the\n// flamingo speaks and its neighbour answers, the crane steps out and bows, the beaver taps a log, the magpie takes a\n// shiny point — and the pigeon hands its parcel over, which the receiver opens in its own way.\nexport default function FlockScenes() {\n return (\n <Flock\n className=\"max-w-4xl\"\n ratio={16 / 5}\n scenes\n tempo={1.4}\n birds={[\n { bird: \"pigeon\", x: 10, y: 96, size: 11 },\n { bird: \"owl\", x: 24, y: 96, size: 11 },\n { bird: \"lovebird\", x: 38, y: 96, size: 11 },\n { bird: \"flamingo\", x: 52, y: 96, size: 13 },\n { bird: \"crane\", x: 66, y: 96, size: 11 },\n { bird: \"magpie\", x: 79, y: 96, size: 11 },\n { bird: \"beaver\", x: 92, y: 96, size: 11 },\n ]}\n />\n );\n}\n"
|
|
169
|
+
},
|
|
153
170
|
{
|
|
154
171
|
"name": "pair",
|
|
155
172
|
"title": "Two birds",
|
|
@@ -164,7 +181,7 @@
|
|
|
164
181
|
"files": [
|
|
165
182
|
{
|
|
166
183
|
"path": "flock/flock.tsx",
|
|
167
|
-
"content": "\"use client\";\n\n// The flock, alive: birds standing on a stage, each at ease in character (Mascot `life`), and the moments between\n// them — two birds glance at each other, the pigeon carries its parcel across, a bird looks up when you come near or tap\n// it. Transform and opacity only: CSS for each bird's own life, a timer for the moments (no frame loop), and nothing at\n// all when the flock is off screen, the tab is hidden or the person asked for less motion.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport type { MascotBird, MascotLook } from \"../mascots\";\nimport { Mascot } from \"../mascots\";\n\nexport interface FlockBird {\n /** Which bird. */\n bird: MascotBird;\n /** Where its feet are, across the stage: 0 to 100 (% of the stage's width). */\n x: number;\n /** Where its feet are, down the stage: 0 to 100 (% of the stage's height). */\n y: number;\n /** How wide it is: % of the stage's width. */\n size: number;\n}\n\nexport interface FlockMoments {\n /** Two neighbours look at each other now and then. Default `true`. */\n glance?: boolean;\n /** The pigeon, when the flock has one, carries its parcel across to another bird and comes back. Default `true`. */\n courier?: boolean;\n /** A bird looks up when the pointer comes near, and jumps a little when tapped. Default `true`. */\n react?: boolean;\n}\n\nexport type FlockProps = Omit<React.ComponentProps<\"div\">, \"children\"> &\n FlockMoments & {\n /** The birds on the stage. */\n birds: readonly FlockBird[];\n /** The stage's shape, width over height (default 16 / 9). It keeps its ratio at any width, so the birds keep their places. */\n ratio?: number;\n /** The birds' colours: the host's accent (`brand`, default) or the Krizaka story's (`own`). */\n tone?: \"brand\" | \"own\";\n /** The source of chance (default `Math.random`): give it a fixed one in a test. */\n random?: () => number;\n /** How lively the moments are: 1 is calm (default), 2 doubles how often neighbours glance and the pigeon delivers. */\n tempo?: number;\n /** A bird the page points at (a product the reader is on): it looks up and jumps once each time it changes. */\n active?: MascotBird | null;\n /** The birds greet the reader once as the flock first comes into view: each jumps in turn, left to right. */\n greet?: boolean;\n };\n\n// @fixed: the rhythm of the moments — how long a glance lasts, how often one happens, how long a delivery takes. A\n// choreography's pace, not a business parameter.\nconst GLANCE_HOLD_MS = [1500, 2400] as const;\nconst GLANCE_EVERY_MS = [4200, 9000] as const;\nconst COURIER_FIRST_MS = 9000;\nconst COURIER_EVERY_MS = [26000, 38000] as const;\nconst FLIGHT_MS = 2600;\nconst HAND_OVER_MS = 1300;\nconst LOOK_BACK_MS = 1600;\n\nconst between = (random: () => number, [lo, hi]: readonly [number, number], tempo = 1) => (lo + random() * (hi - lo)) / tempo;\nconst GREET_STEP_MS = 110;\nconst POKE_MS = 700;\n\n/** `true` while the page may move: no reduced-motion request, the tab visible. */\nfunction useMayMove(): boolean {\n const [may, setMay] = React.useState(false);\n React.useEffect(() => {\n const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n const update = () => setMay(!query.matches && document.visibilityState !== \"hidden\");\n update();\n query.addEventListener(\"change\", update);\n document.addEventListener(\"visibilitychange\", update);\n return () => {\n query.removeEventListener(\"change\", update);\n document.removeEventListener(\"visibilitychange\", update);\n };\n }, []);\n return may;\n}\n\n/** `true` while the element is on screen. */\nfunction useOnScreen(ref: React.RefObject<Element | null>): boolean {\n const [on, setOn] = React.useState(false);\n React.useEffect(() => {\n const el = ref.current;\n if (!el || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver(([entry]) => setOn(Boolean(entry?.isIntersecting)), { threshold: 0.1 });\n observer.observe(el);\n return () => observer.disconnect();\n }, [ref]);\n return on;\n}\n\n/**\n * The flock on its stage, alive. Decorative (`aria-hidden`): put the words beside it. Each bird idles in character; every\n * few seconds two neighbours glance at each other; every half minute the pigeon carries its parcel to another bird and\n * returns; a bird looks up when you come near. All of it pauses off screen and in a hidden tab, and the flock is still\n * under `prefers-reduced-motion`.\n */\nexport function Flock({ birds, ratio = 16 / 9, tone = \"brand\", glance = true, courier = true, react = true, random = Math.random, tempo = 1, active = null, greet = false, className, style, ...props }: FlockProps) {\n const stage = React.useRef<HTMLDivElement>(null);\n const bodies = React.useRef<(HTMLDivElement | null)[]>([]);\n const may = useMayMove();\n const onScreen = useOnScreen(stage);\n const running = may && onScreen;\n const [looks, setLooks] = React.useState<Record<number, MascotLook>>({});\n const [flying, setFlying] = React.useState<{ index: number; bank: number } | null>(null);\n const [poked, setPoked] = React.useState<Record<number, boolean>>({});\n\n // Left to right: a bird's neighbours are the birds beside it on the stage.\n const order = React.useMemo(() => birds.map((_, i) => i).sort((a, b) => birds[a]!.x - birds[b]!.x), [birds]);\n const look = (index: number, to: MascotLook) => setLooks((l) => (l[index] === to ? l : { ...l, [index]: to }));\n const rest = (...indexes: number[]) => setLooks((l) => Object.fromEntries(Object.entries(l).filter(([k]) => !indexes.includes(Number(k)))));\n\n // Glances: two neighbours turn towards each other, hold, and look away.\n React.useEffect(() => {\n if (!running || !glance || order.length < 2) return;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let back: ReturnType<typeof setTimeout> | undefined;\n const next = () => {\n timer = setTimeout(() => {\n const at = Math.floor(random() * (order.length - 1));\n const [a, b] = [order[at]!, order[at + 1]!];\n look(a, \"right\");\n look(b, \"left\");\n back = setTimeout(() => {\n rest(a, b);\n next();\n }, between(random, GLANCE_HOLD_MS));\n }, between(random, GLANCE_EVERY_MS, tempo));\n };\n next();\n return () => {\n clearTimeout(timer);\n clearTimeout(back);\n };\n }, [running, glance, order, random, tempo]);\n\n // The delivery: the pigeon takes its parcel across to the farthest bird, hands it over, and flies back.\n React.useEffect(() => {\n const pigeon = birds.findIndex((b) => b.bird === \"pigeon\");\n if (!running || !courier || pigeon < 0 || birds.length < 2) return;\n let alive = true;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let animation: Animation | undefined;\n const wait = (ms: number) => new Promise<void>((resolve) => (timer = setTimeout(resolve, ms)));\n const trip = async () => {\n const el = bodies.current[pigeon];\n const box = stage.current?.getBoundingClientRect();\n if (!el || !box || typeof el.animate !== \"function\") return;\n // The farthest bird from the pigeon, so the parcel crosses the whole stage.\n const from = birds[pigeon]!;\n const target = birds.reduce((far, b, i) => (i !== pigeon && Math.abs(b.x - from.x) > Math.abs(birds[far]!.x - from.x) ? i : far), pigeon === 0 ? 1 : 0);\n const to = birds[target]!;\n const side = to.x > from.x ? 1 : -1;\n // It lands beside the receiver, not on it.\n const dx = ((to.x - from.x) / 100) * box.width - side * (to.size / 100) * box.width * 0.55;\n const lift = (from.size / 100) * box.width * 0.35;\n const fly = async (fromX: number, toX: number) => {\n setFlying({ index: pigeon, bank: Math.sign(toX - fromX) });\n animation = el.animate(\n [\n { transform: `translate(${fromX}px, 0)`, easing: \"ease-out\" },\n { transform: `translate(${(fromX + toX) / 2}px, ${-lift}px)`, easing: \"ease-in\", offset: 0.5 },\n { transform: `translate(${toX}px, 0)` },\n ],\n { duration: FLIGHT_MS, fill: \"forwards\" },\n );\n await animation.finished.catch(() => undefined);\n setFlying(null);\n };\n look(target, side > 0 ? \"left\" : \"right\");\n await fly(0, dx);\n if (!alive) return;\n // The hand-over: the receiver looks at the pigeon, the pigeon hops on the doorstep.\n setPoked((p) => ({ ...p, [pigeon]: true }));\n await wait(HAND_OVER_MS);\n if (!alive) return;\n setPoked((p) => ({ ...p, [pigeon]: false }));\n look(target, \"up\");\n await fly(dx, 0);\n animation?.cancel();\n rest(target);\n };\n const schedule = (ms: number) => {\n timer = setTimeout(async () => {\n await trip();\n if (alive) schedule(between(random, COURIER_EVERY_MS, tempo));\n }, ms);\n };\n schedule(COURIER_FIRST_MS / tempo);\n return () => {\n alive = false;\n clearTimeout(timer);\n animation?.cancel();\n setFlying(null);\n };\n }, [running, courier, birds, random, tempo]);\n\n // The greeting: the first time the flock is on screen, each bird jumps in turn.\n const greeted = React.useRef(false);\n React.useEffect(() => {\n if (!running || !greet || greeted.current) return;\n greeted.current = true;\n const timers: ReturnType<typeof setTimeout>[] = [];\n order.forEach((index, n) => {\n timers.push(setTimeout(() => setPoked((p) => ({ ...p, [index]: true })), n * GREET_STEP_MS));\n timers.push(setTimeout(() => setPoked((p) => ({ ...p, [index]: false })), n * GREET_STEP_MS + POKE_MS));\n });\n return () => timers.forEach(clearTimeout);\n }, [running, greet, order]);\n\n // The bird the page points at looks up and jumps.\n React.useEffect(() => {\n const index = active ? birds.findIndex((b) => b.bird === active) : -1;\n if (!running || index < 0) return;\n const start = setTimeout(() => {\n look(index, \"up\");\n setPoked((p) => ({ ...p, [index]: true }));\n }, 0);\n const t = setTimeout(() => setPoked((p) => ({ ...p, [index]: false })), POKE_MS);\n return () => {\n clearTimeout(start);\n clearTimeout(t);\n setPoked((p) => ({ ...p, [index]: false }));\n rest(index);\n };\n }, [active, running, birds]);\n\n // A bird looks up when you come near, and jumps when you tap it.\n const hover = (index: number, event: React.PointerEvent) => {\n if (!running || !react || event.pointerType !== \"mouse\") return;\n look(index, \"up\");\n };\n const leave = (index: number) => {\n if (!react) return;\n rest(index);\n };\n const tap = (index: number) => {\n if (!running || !react) return;\n look(index, \"up\");\n setPoked((p) => ({ ...p, [index]: true }));\n setTimeout(() => {\n setPoked((p) => ({ ...p, [index]: false }));\n rest(index);\n }, LOOK_BACK_MS);\n };\n\n return (\n <div\n ref={stage}\n aria-hidden\n data-flock=\"\"\n data-running={running ? \"\" : undefined}\n className={cn(\"relative w-full\", className)}\n style={{ aspectRatio: String(ratio), containerType: \"inline-size\", ...style }}\n {...props}\n >\n {birds.map((b, i) => (\n <div\n key={`${b.bird}-${i}`}\n ref={(el) => {\n bodies.current[i] = el;\n }}\n data-flock-bird={b.bird}\n className=\"absolute touch-manipulation\"\n style={{ left: `${b.x - b.size / 2}%`, width: `${b.size}%`, top: `calc(${b.y}% - ${b.size * 0.9}cqw)`, aspectRatio: \"1\", pointerEvents: react ? \"auto\" : \"none\" }}\n onPointerEnter={(event) => hover(i, event)}\n onPointerLeave={() => leave(i)}\n onPointerDown={() => tap(i)}\n >\n <Mascot\n bird={b.bird}\n tone={tone}\n life\n look={looks[i]}\n flying={flying?.index === i}\n bank={flying?.index === i ? flying.bank : 0}\n poked={poked[i]}\n size={120}\n style={{ width: \"100%\", height: \"100%\", display: \"block\", overflow: \"visible\" }}\n />\n </div>\n ))}\n </div>\n );\n}\n"
|
|
184
|
+
"content": "\"use client\";\n\n// The flock, alive: birds standing on a stage, each at ease in character (Mascot `life`), and the moments between\n// them — two birds glance at each other, the pigeon carries its parcel across, a bird looks up when you come near or tap\n// it. Transform and opacity only: CSS for each bird's own life, a timer for the moments (no frame loop), and nothing at\n// all when the flock is off screen, the tab is hidden or the person asked for less motion.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport type { MascotBird, MascotLook } from \"../mascots\";\nimport { Mascot } from \"../mascots\";\nimport { BEATS, neighbourOf, Prop, receive, type Shown, type Stage } from \"./scenes\";\n\nexport interface FlockBird {\n /** Which bird. */\n bird: MascotBird;\n /** Where its feet are, across the stage: 0 to 100 (% of the stage's width). */\n x: number;\n /** Where its feet are, down the stage: 0 to 100 (% of the stage's height). */\n y: number;\n /** How wide it is: % of the stage's width. */\n size: number;\n}\n\nexport interface FlockMoments {\n /** Two neighbours look at each other now and then. Default `true`. */\n glance?: boolean;\n /** The pigeon, when the flock has one, carries its parcel across to another bird and comes back. Default `true`. */\n courier?: boolean;\n /** A bird looks up when the pointer comes near, and jumps a little when tapped. Default `true`. */\n react?: boolean;\n /**\n * Scenes, true to each bird's promise: the owl reads a book, the flamingo and the lovebird talk with a speech mark, the\n * crane steps and bows, the beaver taps a log, the magpie takes a shiny point, the duck builds, the falcon scans — and the\n * pigeon hands its parcel over, which the receiver opens in its own way. One at a time, at random. Default `false`.\n */\n scenes?: boolean;\n}\n\nexport type FlockProps = Omit<React.ComponentProps<\"div\">, \"children\"> &\n FlockMoments & {\n /** The birds on the stage. */\n birds: readonly FlockBird[];\n /** The stage's shape, width over height (default 16 / 9). It keeps its ratio at any width, so the birds keep their places. */\n ratio?: number;\n /** The birds' colours: the host's accent (`brand`, default) or the Krizaka story's (`own`). */\n tone?: \"brand\" | \"own\";\n /** The source of chance (default `Math.random`): give it a fixed one in a test. */\n random?: () => number;\n /** How lively the moments are: 1 is calm (default), 2 doubles how often neighbours glance and the pigeon delivers. */\n tempo?: number;\n /** A bird the page points at (a product the reader is on): it looks up and jumps once each time it changes. */\n active?: MascotBird | null;\n /** The birds greet the reader once as the flock first comes into view: each jumps in turn, left to right. */\n greet?: boolean;\n };\n\n// @fixed: the rhythm of the moments — how long a glance lasts, how often one happens, how long a delivery takes. A\n// choreography's pace, not a business parameter.\nconst GLANCE_HOLD_MS = [1500, 2400] as const;\nconst GLANCE_EVERY_MS = [4200, 9000] as const;\nconst COURIER_FIRST_MS = 9000;\nconst COURIER_EVERY_MS = [26000, 38000] as const;\nconst FLIGHT_MS = 2600;\nconst HAND_OVER_MS = 1300;\nconst LOOK_BACK_MS = 1600;\n\nconst between = (random: () => number, [lo, hi]: readonly [number, number], tempo = 1) => (lo + random() * (hi - lo)) / tempo;\n// @fixed: the scenes' pace — the wait between two beats, and the rarer long pause.\nconst SCENE_EVERY_MS = [2600, 5200] as const;\nconst SCENE_PAUSE_MS = [8000, 12000] as const;\nconst GREET_STEP_MS = 110;\nconst POKE_MS = 700;\n\n/** `true` while the page may move: no reduced-motion request, the tab visible. */\nfunction useMayMove(): boolean {\n const [may, setMay] = React.useState(false);\n React.useEffect(() => {\n const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n // The page settles first: `html[data-motion=\"settling\"]` (MotionObserver) holds every loop until it is `ready`.\n const settled = () => document.documentElement.getAttribute(\"data-motion\") !== \"settling\";\n const update = () => setMay(!query.matches && document.visibilityState !== \"hidden\" && settled());\n update();\n query.addEventListener(\"change\", update);\n document.addEventListener(\"visibilitychange\", update);\n const watch = typeof MutationObserver === \"undefined\" ? null : new MutationObserver(update);\n watch?.observe(document.documentElement, { attributes: true, attributeFilter: [\"data-motion\"] });\n return () => {\n query.removeEventListener(\"change\", update);\n document.removeEventListener(\"visibilitychange\", update);\n watch?.disconnect();\n };\n }, []);\n return may;\n}\n\n/** `true` while the element is on screen. */\nfunction useOnScreen(ref: React.RefObject<Element | null>): boolean {\n const [on, setOn] = React.useState(false);\n React.useEffect(() => {\n const el = ref.current;\n if (!el || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver(([entry]) => setOn(Boolean(entry?.isIntersecting)), { threshold: 0.1 });\n observer.observe(el);\n return () => observer.disconnect();\n }, [ref]);\n return on;\n}\n\n/**\n * The flock on its stage, alive. Decorative (`aria-hidden`): put the words beside it. Each bird idles in character; every\n * few seconds two neighbours glance at each other; every half minute the pigeon carries its parcel to another bird and\n * returns; a bird looks up when you come near. All of it pauses off screen and in a hidden tab, and the flock is still\n * under `prefers-reduced-motion`.\n */\nexport function Flock({ birds, ratio = 16 / 9, tone = \"brand\", glance = true, courier = true, react = true, scenes = false, random = Math.random, tempo = 1, active = null, greet = false, className, style, ...props }: FlockProps) {\n const stage = React.useRef<HTMLDivElement>(null);\n const bodies = React.useRef<(HTMLDivElement | null)[]>([]);\n const may = useMayMove();\n const onScreen = useOnScreen(stage);\n const running = may && onScreen;\n const [looks, setLooks] = React.useState<Record<number, MascotLook>>({});\n const [flying, setFlying] = React.useState<{ index: number; bank: number } | null>(null);\n const [poked, setPoked] = React.useState<Record<number, boolean>>({});\n const [shown, setShown] = React.useState<Shown[]>([]);\n const [empty, setEmpty] = React.useState<Record<number, boolean>>({});\n // Birds a scene holds (a glance or a trip leaves them alone), and the scene or the trip in progress.\n const held = React.useRef(new Set<number>());\n const busy = React.useRef<\"scene\" | \"trip\" | null>(null);\n const ids = React.useRef(0);\n\n // Left to right: a bird's neighbours are the birds beside it on the stage.\n const order = React.useMemo(() => birds.map((_, i) => i).sort((a, b) => birds[a]!.x - birds[b]!.x), [birds]);\n const look = React.useCallback((index: number, to: MascotLook) => setLooks((l) => (l[index] === to ? l : { ...l, [index]: to })), []);\n const rest = React.useCallback((...indexes: number[]) => setLooks((l) => Object.fromEntries(Object.entries(l).filter(([k]) => !indexes.includes(Number(k))))), []);\n\n // What a scene can do, bound to one run of an effect: every timer and animation it starts ends with the run.\n const stageFor = React.useCallback((run: { timers: Set<ReturnType<typeof setTimeout>>; anims: Set<Animation> }): Stage => {\n const later = (fn: () => void, ms: number) => {\n const t = setTimeout(() => {\n run.timers.delete(t);\n fn();\n }, ms);\n run.timers.add(t);\n };\n const poke = (index: number, ms = POKE_MS) => {\n setPoked((p) => ({ ...p, [index]: true }));\n later(() => setPoked((p) => ({ ...p, [index]: false })), ms);\n };\n const show = (index: number, kind: Shown[\"kind\"], ms: number, side?: 1 | -1, mark?: string) => {\n const item: Shown = { id: ++ids.current, at: index, kind, ms, side, mark };\n setShown((all) => [...all, item]);\n later(() => setShown((all) => all.filter((x) => x.id !== item.id)), ms);\n };\n return {\n birds,\n order,\n random,\n look,\n rest,\n poke,\n show,\n say: (index, mark, ms) => show(index, \"say\", ms, undefined, mark),\n wait: (ms) => new Promise<void>((resolve) => later(resolve, ms)),\n move: async (index, keyframes, ms) => {\n const el = bodies.current[index];\n if (!el || typeof el.animate !== \"function\") return;\n const animation = el.animate(keyframes, { duration: ms });\n run.anims.add(animation);\n await animation.finished.catch(() => undefined);\n run.anims.delete(animation);\n },\n };\n }, [birds, order, random, look, rest]);\n\n // Glances: two neighbours turn towards each other, hold, and look away.\n React.useEffect(() => {\n if (!running || !glance || order.length < 2) return;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let back: ReturnType<typeof setTimeout> | undefined;\n const next = () => {\n timer = setTimeout(() => {\n const at = Math.floor(random() * (order.length - 1));\n const [a, b] = [order[at]!, order[at + 1]!];\n if (held.current.has(a) || held.current.has(b)) return next();\n look(a, \"right\");\n look(b, \"left\");\n back = setTimeout(() => {\n rest(a, b);\n next();\n }, between(random, GLANCE_HOLD_MS));\n }, between(random, GLANCE_EVERY_MS, tempo));\n };\n next();\n return () => {\n clearTimeout(timer);\n clearTimeout(back);\n };\n }, [running, glance, order, random, tempo, look, rest]);\n\n // The delivery: the pigeon takes its parcel across to the farthest bird, hands it over, and flies back.\n React.useEffect(() => {\n const pigeon = birds.findIndex((b) => b.bird === \"pigeon\");\n if (!running || !courier || pigeon < 0 || birds.length < 2) return;\n let alive = true;\n const heldNow = held.current;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let animation: Animation | undefined;\n const run = { timers: new Set<ReturnType<typeof setTimeout>>(), anims: new Set<Animation>() };\n // The farthest bird from the pigeon, so the parcel crosses the whole stage.\n const from = birds[pigeon]!;\n const target = birds.reduce((far, b, i) => (i !== pigeon && Math.abs(b.x - from.x) > Math.abs(birds[far]!.x - from.x) ? i : far), pigeon === 0 ? 1 : 0);\n const wait = (ms: number) => new Promise<void>((resolve) => (timer = setTimeout(resolve, ms)));\n const trip = async () => {\n // With scenes, the trip waits for the beat in progress and holds the stage while it lasts.\n while (alive && scenes && busy.current === \"scene\") await wait(400);\n if (!alive) return;\n busy.current = \"trip\";\n const el = bodies.current[pigeon];\n const box = stage.current?.getBoundingClientRect();\n if (!el || !box || typeof el.animate !== \"function\") return;\n const to = birds[target]!;\n const side = to.x > from.x ? 1 : -1;\n // It lands beside the receiver, not on it.\n const dx = ((to.x - from.x) / 100) * box.width - side * (to.size / 100) * box.width * 0.55;\n const lift = (from.size / 100) * box.width * 0.35;\n const fly = async (fromX: number, toX: number) => {\n setFlying({ index: pigeon, bank: Math.sign(toX - fromX) });\n animation = el.animate(\n [\n { transform: `translate(${fromX}px, 0)`, easing: \"ease-out\" },\n { transform: `translate(${(fromX + toX) / 2}px, ${-lift}px)`, easing: \"ease-in\", offset: 0.5 },\n { transform: `translate(${toX}px, 0)` },\n ],\n { duration: FLIGHT_MS, fill: \"forwards\" },\n );\n await animation.finished.catch(() => undefined);\n setFlying(null);\n };\n look(target, side > 0 ? \"left\" : \"right\");\n held.current.add(target);\n await fly(0, dx);\n if (!alive) return;\n // The hand-over: the receiver looks at the pigeon, the pigeon hops on the doorstep.\n setPoked((p) => ({ ...p, [pigeon]: true }));\n if (scenes) {\n // …and the parcel changes hands: the receiver takes it and reacts in character, the pigeon flies back light.\n setEmpty((e) => ({ ...e, [pigeon]: true }));\n const reaction = receive(stageFor(run), target, pigeon);\n await wait(HAND_OVER_MS);\n if (!alive) return;\n setPoked((p) => ({ ...p, [pigeon]: false }));\n await reaction;\n } else {\n await wait(HAND_OVER_MS);\n if (!alive) return;\n setPoked((p) => ({ ...p, [pigeon]: false }));\n }\n if (!alive) return;\n look(target, \"up\");\n await fly(dx, 0);\n animation?.cancel();\n setEmpty((e) => ({ ...e, [pigeon]: false }));\n rest(target);\n };\n const schedule = (ms: number) => {\n timer = setTimeout(async () => {\n await trip();\n busy.current = null;\n held.current.delete(target);\n if (alive) schedule(between(random, COURIER_EVERY_MS, tempo));\n }, ms);\n };\n schedule(COURIER_FIRST_MS / tempo);\n return () => {\n alive = false;\n clearTimeout(timer);\n animation?.cancel();\n run.timers.forEach(clearTimeout);\n run.anims.forEach((a) => a.cancel());\n if (scenes) {\n setShown([]);\n setEmpty({});\n heldNow.clear();\n busy.current = null;\n }\n setFlying(null);\n };\n }, [running, courier, birds, random, tempo, scenes, stageFor, look, rest]);\n\n // The scenes: one beat at a time, picked at random from what the flock holds — never the same bird twice in a row, never\n // at a regular beat (short waits, now and then a long pause) — so the minute is never the same twice.\n React.useEffect(() => {\n if (!running || !scenes || birds.length < 1) return;\n let alive = true;\n const heldNow = held.current;\n const run = { timers: new Set<ReturnType<typeof setTimeout>>(), anims: new Set<Animation>() };\n const st = stageFor(run);\n const beats = BEATS.flatMap((beat) => birds.flatMap((b, i) => (b.bird === beat.bird ? [{ beat, index: i }] : [])));\n if (!beats.length) return;\n let last = -1;\n const next = async () => {\n const long = random() < 0.2;\n await st.wait(between(random, long ? SCENE_PAUSE_MS : SCENE_EVERY_MS, tempo));\n while (alive && busy.current === \"trip\") await st.wait(600);\n if (!alive) return;\n const pool = beats.filter((x) => x.index !== last);\n const pick = (pool.length ? pool : beats)[Math.floor(random() * (pool.length ? pool.length : beats.length))]!;\n last = pick.index;\n busy.current = \"scene\";\n // The scene's birds are held, so no glance cuts across it.\n const near = neighbourOf(st, pick.index);\n held.current.add(pick.index);\n if (near !== null) held.current.add(near);\n await pick.beat.run(st, pick.index);\n held.current.delete(pick.index);\n if (near !== null) held.current.delete(near);\n if (busy.current === \"scene\") busy.current = null;\n if (alive) void next();\n };\n void next();\n return () => {\n alive = false;\n run.timers.forEach(clearTimeout);\n run.anims.forEach((a) => a.cancel());\n setShown([]);\n heldNow.clear();\n busy.current = null;\n };\n }, [running, scenes, birds, random, tempo, stageFor]);\n\n // The greeting: the first time the flock is on screen, each bird jumps in turn.\n const greeted = React.useRef(false);\n React.useEffect(() => {\n if (!running || !greet || greeted.current) return;\n greeted.current = true;\n const timers: ReturnType<typeof setTimeout>[] = [];\n order.forEach((index, n) => {\n timers.push(setTimeout(() => setPoked((p) => ({ ...p, [index]: true })), n * GREET_STEP_MS));\n timers.push(setTimeout(() => setPoked((p) => ({ ...p, [index]: false })), n * GREET_STEP_MS + POKE_MS));\n });\n return () => timers.forEach(clearTimeout);\n }, [running, greet, order]);\n\n // The bird the page points at looks up and jumps.\n React.useEffect(() => {\n const index = active ? birds.findIndex((b) => b.bird === active) : -1;\n if (!running || index < 0) return;\n const start = setTimeout(() => {\n look(index, \"up\");\n setPoked((p) => ({ ...p, [index]: true }));\n }, 0);\n const t = setTimeout(() => setPoked((p) => ({ ...p, [index]: false })), POKE_MS);\n return () => {\n clearTimeout(start);\n clearTimeout(t);\n setPoked((p) => ({ ...p, [index]: false }));\n rest(index);\n };\n }, [active, running, birds, look, rest]);\n\n // A bird looks up when you come near, and jumps when you tap it.\n const hover = (index: number, event: React.PointerEvent) => {\n if (!running || !react || event.pointerType !== \"mouse\") return;\n look(index, \"up\");\n };\n const leave = (index: number) => {\n if (!react) return;\n rest(index);\n };\n const tap = (index: number) => {\n if (!running || !react) return;\n look(index, \"up\");\n setPoked((p) => ({ ...p, [index]: true }));\n setTimeout(() => {\n setPoked((p) => ({ ...p, [index]: false }));\n rest(index);\n }, LOOK_BACK_MS);\n };\n\n return (\n <div\n ref={stage}\n aria-hidden\n data-flock=\"\"\n data-running={running ? \"\" : undefined}\n data-motion-scope=\"\"\n className={cn(\"relative w-full\", className)}\n style={{ aspectRatio: String(ratio), containerType: \"inline-size\", ...style }}\n {...props}\n >\n {birds.map((b, i) => (\n <div\n key={`${b.bird}-${i}`}\n ref={(el) => {\n bodies.current[i] = el;\n }}\n data-flock-bird={b.bird}\n className=\"absolute touch-manipulation\"\n style={{ left: `${b.x - b.size / 2}%`, width: `${b.size}%`, top: `calc(${b.y}% - ${b.size * 0.9}cqw)`, aspectRatio: \"1\", pointerEvents: react ? \"auto\" : \"none\" }}\n onPointerEnter={(event) => hover(i, event)}\n onPointerLeave={() => leave(i)}\n onPointerDown={() => tap(i)}\n >\n <Mascot\n bird={b.bird}\n tone={tone}\n life\n look={looks[i]}\n flying={flying?.index === i}\n bank={flying?.index === i ? flying.bank : 0}\n poked={poked[i]}\n carrying={!empty[i]}\n size={120}\n style={{ width: \"100%\", height: \"100%\", display: \"block\", overflow: \"visible\" }}\n />\n {shown\n .filter((x) => x.at === i)\n .map((x) => (\n <Prop key={x.id} shown={x} size={b.size} />\n ))}\n </div>\n ))}\n </div>\n );\n}\n"
|
|
168
185
|
},
|
|
169
186
|
{
|
|
170
187
|
"path": "flock/index.ts",
|
|
@@ -172,7 +189,11 @@
|
|
|
172
189
|
},
|
|
173
190
|
{
|
|
174
191
|
"path": "flock/meta.ts",
|
|
175
|
-
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Flock\",\n summary:\n \"The Krizaka birds standing on a stage, alive: each at ease in character, two neighbours glancing at each other, the pigeon carrying its parcel across, a bird looking up when you come near or tap it.\",\n status: \"beta\",\n category: \"blocks\",\n platforms: \"web\",\n parity: {\n native: { status: \"n/a\", reason: \"A decorative stage for a public page's first screen; an app screen speaks with MascotState.\", use: \"mascot-state\" },\n desktop: \"web\",\n },\n whenToUse: [\n \"On a company's home, where the flock is the scene: five birds on a ridge, under the words.\",\n \"On an about or story page, where the birds are the subject and a still drawing would feel dead.\",\n ],\n whenNotToUse: [\n { when: \"A bird that answers a state (a wait, an error, an empty list).\", use: \"mascot-state\" },\n { when: \"One bird at ease inside a card or beside a rule.\", use: \"mascots\" },\n { when: \"The flock as a still plate with its promises.\", use: \"bird-plate\" },\n ],\n bestPractices: [\n \"One flock per page, under the words and never behind them: the birds are the scene, not the message.\",\n \"Give the stage the ratio of its backdrop (`ratio`) and the birds' places in percent: they keep their positions at any width.\",\n \"Keep each bird to its job: the falcon watches, the owl reads, the duck builds, the flamingo answers, the pigeon delivers.\",\n \"Make it the page's own: `active` is the bird of the product the reader is on (it looks up and jumps), `greet` has every bird jump once as the flock comes into view, `tempo` makes the moments more frequent.\",\n \"Turn a moment off with `glance={false}`, `courier={false}` or `react={false}` on a page that already moves.\",\n ],\n accessibility: {\n keyboard: [],\n notes: [\n \"Decorative (`aria-hidden`): it never carries information; put the words beside it.\",\n \"Under `prefers-reduced-motion` the birds are still: no idle motion, no glance, no delivery, no reaction.\",\n \"The moments pause while the flock is off screen and while the tab is hidden; there is no frame loop.\",\n ],\n },\n related: [\"mascots\", \"mascot-state\", \"bird-plate\", \"landscape\"],\n web: {\n imports: [\"Flock\"],\n examples: [\n { name: \"ridge\", title: \"The flock on its ridge\", description: \"Five birds across a stage, each at ease; neighbours glance, the pigeon delivers.\" },\n { name: \"pair\", title: \"Two birds\", description: \"The owl and the duck: they look at each other now and then.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
192
|
+
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Flock\",\n summary:\n \"The Krizaka birds standing on a stage, alive: each at ease in character, two neighbours glancing at each other, the pigeon carrying its parcel across, a bird looking up when you come near or tap it — and, with `scenes`, a short scene for each bird.\",\n status: \"beta\",\n category: \"blocks\",\n platforms: \"web\",\n parity: {\n native: { status: \"n/a\", reason: \"A decorative stage for a public page's first screen; an app screen speaks with MascotState.\", use: \"mascot-state\" },\n desktop: \"web\",\n },\n whenToUse: [\n \"On a company's home, where the flock is the scene: five birds on a ridge, under the words.\",\n \"On an about or story page, where the birds are the subject and a still drawing would feel dead.\",\n ],\n whenNotToUse: [\n { when: \"A bird that answers a state (a wait, an error, an empty list).\", use: \"mascot-state\" },\n { when: \"One bird at ease inside a card or beside a rule.\", use: \"mascots\" },\n { when: \"The flock as a still plate with its promises.\", use: \"bird-plate\" },\n ],\n bestPractices: [\n \"One flock per page, under the words and never behind them: the birds are the scene, not the message.\",\n \"Give the stage the ratio of its backdrop (`ratio`) and the birds' places in percent: they keep their positions at any width.\",\n \"Keep each bird to its job: the falcon watches, the owl reads, the duck builds, the flamingo answers, the pigeon delivers.\",\n \"Make it the page's own: `active` is the bird of the product the reader is on (it looks up and jumps), `greet` has every bird jump once as the flock comes into view, `tempo` makes the moments more frequent.\",\n \"`scenes` gives each bird a short scene true to its promise (the owl reads a book, the flamingo and the lovebird talk, the crane bows, the beaver taps a log, the magpie takes a shiny point, the duck builds, the falcon scans) and has the pigeon hand its parcel over, which the receiver opens in its own way. One at a time, at random, with long pauses now and then: leave `tempo` near 1 to 1.7.\",\n \"Turn a moment off with `glance={false}`, `courier={false}` or `react={false}` on a page that already moves.\",\n ],\n accessibility: {\n keyboard: [],\n notes: [\n \"Decorative (`aria-hidden`): it never carries information; put the words beside it.\",\n \"Under `prefers-reduced-motion` the birds are still: no idle motion, no glance, no delivery, no scene, no reaction.\",\n \"It waits for the page to settle (`html[data-motion=\\\"settling\\\"]`) and pauses off screen (the stage is a `data-motion-scope`).\",\n \"The moments pause while the flock is off screen and while the tab is hidden; there is no frame loop.\",\n ],\n },\n related: [\"mascots\", \"mascot-state\", \"bird-plate\", \"landscape\"],\n web: {\n imports: [\"Flock\"],\n examples: [\n { name: \"ridge\", title: \"The flock on its ridge\", description: \"Five birds across a stage, each at ease; neighbours glance, the pigeon delivers.\" },\n { name: \"scenes\", title: \"The scenes\", description: \"The owl reads, the flamingo and the lovebird talk, the crane bows, the beaver taps a log, the magpie takes a shiny point, and the pigeon hands its parcel over.\" },\n { name: \"pair\", title: \"Two birds\", description: \"The owl and the duck: they look at each other now and then.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"path": "flock/scenes.tsx",
|
|
196
|
+
"content": "\"use client\";\n\n// The flock's scenes: what a bird does between the idle motions, each one true to the promise it carries — the owl reads\n// (a book, a leaf turned), the flamingo and the lovebird answer (a speech mark, a reply), the crane steps and bows, the\n// beaver carries a log and taps it, the magpie spots a shiny point and takes it, the duck builds, the falcon scans — and the\n// pigeon's parcel changes hands: the receiver reacts in its own way. A director picks one beat at a time from what the\n// flock holds, with chance in the waits and in the choice, so it never loops the same minute twice. Transform and opacity\n// only (Web Animations on a few small elements, timers between beats); nothing runs off screen or under reduced motion.\nimport * as React from \"react\";\n\nimport type { MascotBird, MascotLook } from \"../mascots\";\nimport { PARCEL } from \"../mascots/geometry\";\n\nexport type PropKind = \"say\" | \"book\" | \"log\" | \"glint\" | \"gift\";\n\n/** A small thing a bird shows for a while: a speech mark, a book, a log, a shiny point, a parcel. */\nexport interface Shown {\n id: number;\n /** Index of the bird it belongs to. */\n at: number;\n kind: PropKind;\n /** How long it stays (ms). */\n ms: number;\n /** The mark of a speech bubble. */\n mark?: string;\n /** Which side it comes from (1: right, -1: left). */\n side?: 1 | -1;\n}\n\n/** What a beat can do on the stage (built by `Flock`; every wait ends when the flock stops). */\nexport interface Stage {\n birds: readonly { bird: MascotBird; x: number; y: number; size: number }[];\n /** Indexes left to right. */\n order: readonly number[];\n random: () => number;\n look: (index: number, to: MascotLook) => void;\n rest: (...indexes: number[]) => void;\n poke: (index: number, ms?: number) => void;\n say: (index: number, mark: string, ms: number) => void;\n show: (index: number, kind: PropKind, ms: number, side?: 1 | -1) => void;\n /** Move a bird's body for a while (transform only). */\n move: (index: number, keyframes: Keyframe[], ms: number) => Promise<void>;\n wait: (ms: number) => Promise<void>;\n}\n\nconst side = (s: Stage, i: number, j: number): 1 | -1 => (s.birds[j]!.x >= s.birds[i]!.x ? 1 : -1);\nconst towards = (s: Stage, i: number, j: number): MascotLook => (side(s, i, j) > 0 ? \"right\" : \"left\");\n/** Where a bird is among its neighbours, left to right: the one beside it (the nearer one when there are two). */\nexport function neighbourOf(s: Stage, i: number): number | null {\n const at = s.order.indexOf(i);\n const near = [s.order[at - 1], s.order[at + 1]].filter((n): n is number => n !== undefined);\n if (!near.length) return null;\n return near.reduce((a, b) => (Math.abs(s.birds[a]!.x - s.birds[i]!.x) <= Math.abs(s.birds[b]!.x - s.birds[i]!.x) ? a : b));\n}\n\n// @fixed: the choreography's pace — how long each beat lasts. A rhythm to the eye, not a business parameter.\nconst BOOK_MS = 4800;\nconst LOG_MS = 3800;\nconst GLINT_MS = 3400;\nconst SAY_MS = 1500;\n\n/** The owl reads: it looks down at its book, a leaf turns, and it turns another. A neighbour glances over. */\nasync function read(s: Stage, i: number) {\n const j = neighbourOf(s, i);\n s.look(i, \"left\");\n s.show(i, \"book\", BOOK_MS);\n await s.wait(BOOK_MS * 0.45);\n if (j !== null && s.random() < 0.5) {\n s.look(j, towards(s, j, i));\n await s.wait(1100);\n s.rest(j);\n }\n await s.wait(BOOK_MS * 0.5);\n s.rest(i);\n}\n\n/** The flamingo or the lovebird speaks; its neighbour listens, then answers, and they both light up. */\nasync function chat(s: Stage, i: number) {\n const j = neighbourOf(s, i);\n s.look(i, j === null ? \"up\" : towards(s, i, j));\n s.say(i, \"…\", SAY_MS);\n await s.wait(SAY_MS * 0.6);\n if (j === null) {\n await s.wait(SAY_MS * 0.6);\n s.rest(i);\n return;\n }\n s.look(j, towards(s, j, i));\n await s.wait(SAY_MS * 0.7);\n s.say(j, s.random() < 0.5 ? \"!\" : \"?\", SAY_MS);\n s.poke(j);\n await s.wait(SAY_MS * 0.5);\n s.poke(i);\n await s.wait(SAY_MS * 0.7);\n s.rest(i, j);\n}\n\n/** The crane steps out of the line, bows to its neighbour, and steps back. */\nasync function bow(s: Stage, i: number) {\n const j = neighbourOf(s, i);\n const dir = j === null ? 1 : side(s, i, j);\n const step = `${(s.birds[i]!.size * 0.14 * dir).toFixed(2)}cqw`;\n const lean = `rotate(${dir * 11}deg)`;\n const at = (x: string, r: string) => `translateX(${x}) ${r}`;\n const run = s.move(\n i,\n [\n { transform: at(\"0cqw\", \"rotate(0deg)\"), easing: \"ease-in-out\" },\n { transform: at(step, \"rotate(0deg)\"), offset: 0.22, easing: \"ease-in-out\" },\n { transform: at(step, lean), offset: 0.42, easing: \"ease-in-out\" },\n { transform: at(step, lean), offset: 0.6, easing: \"ease-in-out\" },\n { transform: at(step, \"rotate(0deg)\"), offset: 0.74, easing: \"ease-in-out\" },\n { transform: at(\"0cqw\", \"rotate(0deg)\") },\n ],\n 3800,\n );\n if (j !== null) {\n await s.wait(1700);\n s.look(j, towards(s, j, i));\n await s.wait(1200);\n s.poke(j);\n await s.wait(500);\n s.rest(j);\n }\n await run;\n}\n\n/** The beaver brings a log, taps it into place three times, and takes it away. */\nasync function tap(s: Stage, i: number) {\n s.show(i, \"log\", LOG_MS, s.birds[i]!.x > 50 ? 1 : -1);\n await s.wait(LOG_MS * 0.3);\n for (let n = 0; n < 3; n++) {\n s.poke(i, 420);\n await s.wait(520);\n }\n await s.wait(LOG_MS * 0.3);\n}\n\n/** The magpie sees something shiny on the ground, looks, hops, and takes it. */\nasync function shine(s: Stage, i: number) {\n const dir: 1 | -1 = s.birds[i]!.x > 50 ? -1 : 1;\n const to: MascotLook = dir > 0 ? \"right\" : \"left\";\n s.show(i, \"glint\", GLINT_MS, dir);\n await s.wait(GLINT_MS * 0.28);\n s.look(i, to);\n await s.wait(GLINT_MS * 0.3);\n s.poke(i);\n await s.wait(GLINT_MS * 0.25);\n s.look(i, \"up\");\n await s.wait(GLINT_MS * 0.2);\n s.rest(i);\n}\n\n/** The duck builds: quick nods in a burst, like typing a block into place. */\nasync function build(s: Stage, i: number) {\n for (let n = 0; n < 4; n++) {\n s.poke(i, 300);\n await s.wait(380);\n }\n}\n\n/** The falcon watches: a slow scan, left then right, and a look up. */\nasync function scan(s: Stage, i: number) {\n for (const to of [\"left\", \"up\", \"right\"] as const) {\n s.look(i, to);\n await s.wait(1100);\n }\n s.rest(i);\n}\n\n/** The pigeon's parcel has just been handed over: the receiver reacts in its own way (about 2 s). */\nexport async function receive(s: Stage, i: number, from: number) {\n const bird = s.birds[i]!.bird;\n s.look(i, towards(s, i, from));\n s.show(i, \"gift\", 2400);\n await s.wait(350);\n if (bird === \"crane\") await bow(s, i);\n else if (bird === \"magpie\") {\n s.poke(i);\n await s.wait(700);\n s.show(i, \"glint\", GLINT_MS * 0.6, side(s, i, from));\n await s.wait(1200);\n } else if (bird === \"beaver\" || bird === \"duck\") {\n s.poke(i, 420);\n await s.wait(520);\n s.poke(i, 420);\n await s.wait(900);\n } else if (bird === \"owl\") {\n s.say(i, \"?\", SAY_MS);\n await s.wait(900);\n s.poke(i);\n await s.wait(700);\n } else {\n s.say(i, \"!\", SAY_MS);\n s.poke(i);\n await s.wait(1500);\n }\n}\n\nexport interface Beat {\n id: string;\n bird: MascotBird;\n run: (s: Stage, index: number) => Promise<void>;\n}\n\n/** What each bird of the flock can do on its own (the pigeon's trip is the flock's courier). */\nexport const BEATS: readonly Beat[] = [\n { id: \"read\", bird: \"owl\", run: read },\n { id: \"chat\", bird: \"flamingo\", run: chat },\n { id: \"chat\", bird: \"lovebird\", run: chat },\n { id: \"bow\", bird: \"crane\", run: bow },\n { id: \"tap\", bird: \"beaver\", run: tap },\n { id: \"shine\", bird: \"magpie\", run: shine },\n { id: \"build\", bird: \"duck\", run: build },\n { id: \"scan\", bird: \"falcon\", run: scan },\n];\n\n// ─── The small things a bird shows ───────────────────────────────────────────────────────────────\n\nconst PAPER = \"var(--kz-surface-1)\";\nconst INK = \"var(--kz-text-primary)\";\nconst LINE = \"var(--kz-border-strong, currentColor)\";\n\n/** One thing, in its own frame: it comes in, stays for `ms`, and goes (opacity and transform only). */\nfunction lifeFrames(from: string, to: string): Keyframe[] {\n return [\n { opacity: 0, transform: from, offset: 0 },\n { opacity: 1, transform: \"none\", offset: 0.14 },\n { opacity: 1, transform: \"none\", offset: 0.84 },\n { opacity: 0, transform: to, offset: 1 },\n ];\n}\n\nconst LEAF: Keyframe[] = [\n { transform: \"scaleX(1)\", offset: 0 },\n { transform: \"scaleX(1)\", offset: 0.25 },\n { transform: \"scaleX(-1)\", offset: 0.38 },\n { transform: \"scaleX(1)\", offset: 0.39 },\n { transform: \"scaleX(1)\", offset: 0.62 },\n { transform: \"scaleX(-1)\", offset: 0.75 },\n { transform: \"scaleX(-1)\", offset: 1 },\n];\n\nexport function Prop({ shown, size }: { shown: Shown; size: number }) {\n const ref = React.useRef<HTMLDivElement>(null);\n const leaf = React.useRef<HTMLDivElement>(null);\n const dir = shown.side ?? 1;\n React.useEffect(() => {\n const el = ref.current;\n if (!el || typeof el.animate !== \"function\") return;\n const from: Record<PropKind, [string, string]> = {\n say: [\"translateY(30%) scale(.5)\", \"translateY(-12%) scale(.9)\"],\n book: [\"translateY(40%) scale(.8)\", \"translateY(20%) scale(.9)\"],\n log: [`translateX(${190 * dir}%)`, `translateX(${190 * dir}%)`],\n glint: [\"scale(.2) rotate(-60deg)\", `translate(${-90 * dir}%, -170%) scale(.2) rotate(60deg)`],\n gift: [\"translateY(40%) scale(.3)\", \"translateY(-30%) scale(1.4)\"],\n };\n const [a, b] = from[shown.kind];\n const animations = [el.animate(lifeFrames(a, b), { duration: shown.ms, fill: \"both\", easing: \"ease-out\" })];\n if (shown.kind === \"book\" && leaf.current) animations.push(leaf.current.animate(LEAF, { duration: shown.ms, fill: \"both\", easing: \"ease-in-out\" }));\n if (shown.kind === \"glint\") animations.push(el.firstElementChild!.animate([{ transform: \"scale(1)\" }, { transform: \"scale(1.35)\" }, { transform: \"scale(1)\" }], { duration: 700, iterations: Math.ceil(shown.ms / 700), easing: \"ease-in-out\" }));\n return () => animations.forEach((x) => x.cancel());\n }, [shown, dir]);\n\n const unit = (n: number) => `calc(${size}cqw * ${n})`;\n const base: React.CSSProperties = { position: \"absolute\", pointerEvents: \"none\", opacity: 0 };\n\n if (shown.kind === \"say\")\n return (\n <div ref={ref} data-flock-prop=\"say\" style={{ ...base, left: \"58%\", top: \"-6%\", width: unit(0.34), height: unit(0.24), transformOrigin: \"20% 100%\" }}>\n <span\n style={{ position: \"absolute\", inset: 0, display: \"grid\", placeItems: \"center\", background: PAPER, border: `1.5px solid ${LINE}`, borderRadius: \"999px\", color: INK, font: `600 ${unit(0.17)}/1 var(--font-mono, ui-monospace, monospace)` }}\n >\n {shown.mark ?? \"…\"}\n </span>\n <span style={{ position: \"absolute\", left: \"14%\", bottom: \"-12%\", width: unit(0.07), height: unit(0.07), background: PAPER, borderRight: `1.5px solid ${LINE}`, borderBottom: `1.5px solid ${LINE}`, transform: \"rotate(45deg)\" }} />\n </div>\n );\n\n if (shown.kind === \"book\")\n return (\n <div ref={ref} data-flock-prop=\"book\" style={{ ...base, left: \"17%\", bottom: \"-2%\", width: \"66%\", height: unit(0.2), transformOrigin: \"50% 100%\" }}>\n {[0, 1].map((n) => (\n <span key={n} style={{ position: \"absolute\", top: 0, bottom: 0, [n ? \"right\" : \"left\"]: 0, width: \"50%\", background: `repeating-linear-gradient(${PAPER} 0 18%, ${LINE} 18% 24%)`, border: `1.5px solid ${LINE}`, borderRadius: n ? \"0 3px 3px 0\" : \"3px 0 0 3px\" }} />\n ))}\n <div ref={leaf} style={{ position: \"absolute\", top: 0, bottom: 0, left: \"50%\", width: \"50%\", transformOrigin: \"0 50%\", background: PAPER, border: `1.5px solid ${LINE}`, borderRadius: \"0 3px 3px 0\" }} />\n </div>\n );\n\n if (shown.kind === \"log\")\n return (\n <div ref={ref} data-flock-prop=\"log\" style={{ ...base, left: \"22%\", bottom: \"-3%\", width: \"56%\", height: unit(0.17), background: PARCEL[2], borderRadius: \"999px\", boxShadow: `inset -${unit(0.04)} 0 0 ${PARCEL[1]}` }}>\n <span style={{ position: \"absolute\", left: 0, top: 0, bottom: 0, aspectRatio: \"1\", borderRadius: \"50%\", background: `radial-gradient(${PARCEL[0]} 0 25%, ${PARCEL[1]} 26% 55%, ${PARCEL[2]} 56%)`, border: `1px solid ${PARCEL[2]}` }} />\n </div>\n );\n\n if (shown.kind === \"glint\")\n return (\n <div ref={ref} data-flock-prop=\"glint\" style={{ ...base, [dir > 0 ? \"left\" : \"right\"]: \"94%\", bottom: \"2%\", width: unit(0.2), height: unit(0.2) }}>\n <span style={{ position: \"absolute\", inset: 0, background: PARCEL[0], clipPath: \"polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%)\" }} />\n </div>\n );\n\n return (\n <div ref={ref} data-flock-prop=\"gift\" style={{ ...base, left: \"58%\", top: \"2%\", width: unit(0.26), height: unit(0.22), background: PARCEL[0], border: `1.5px solid ${PARCEL[2]}`, borderRadius: 2 }}>\n <span style={{ position: \"absolute\", left: \"44%\", top: 0, bottom: 0, width: \"12%\", background: PARCEL[2] }} />\n <span style={{ position: \"absolute\", top: \"42%\", left: 0, right: 0, height: \"16%\", background: PARCEL[2] }} />\n </div>\n );\n}\n"
|
|
176
197
|
},
|
|
177
198
|
{
|
|
178
199
|
"path": "meta.ts",
|
|
@@ -249,6 +270,13 @@
|
|
|
249
270
|
"description": "A bird looks up when the pointer comes near, and jumps a little when tapped. Default `true`.",
|
|
250
271
|
"required": false
|
|
251
272
|
},
|
|
273
|
+
{
|
|
274
|
+
"name": "scenes",
|
|
275
|
+
"type": "boolean",
|
|
276
|
+
"default": "false",
|
|
277
|
+
"description": "Scenes, true to each bird's promise: the owl reads a book, the flamingo and the lovebird talk with a speech mark, the\ncrane steps and bows, the beaver taps a log, the magpie takes a shiny point, the duck builds, the falcon scans — and the\npigeon hands its parcel over, which the receiver opens in its own way. One at a time, at random. Default `false`.",
|
|
278
|
+
"required": false
|
|
279
|
+
},
|
|
252
280
|
{
|
|
253
281
|
"name": "tempo",
|
|
254
282
|
"type": "number",
|
package/registry/index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krizaka/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.42.0",
|
|
4
4
|
"templatePurposes": {
|
|
5
5
|
"convince": {
|
|
6
6
|
"title": "Convince",
|
|
@@ -2345,8 +2345,8 @@
|
|
|
2345
2345
|
"name": "flock",
|
|
2346
2346
|
"type": "primitive",
|
|
2347
2347
|
"title": "Flock",
|
|
2348
|
-
"summary": "The Krizaka birds standing on a stage, alive: each at ease in character, two neighbours glancing at each other, the pigeon carrying its parcel across, a bird looking up when you come near or tap it.",
|
|
2349
|
-
"description": "The Krizaka birds standing on a stage, alive: each at ease in character, two neighbours glancing at each other, the pigeon carrying its parcel across, a bird looking up when you come near or tap it.",
|
|
2348
|
+
"summary": "The Krizaka birds standing on a stage, alive: each at ease in character, two neighbours glancing at each other, the pigeon carrying its parcel across, a bird looking up when you come near or tap it — and, with `scenes`, a short scene for each bird.",
|
|
2349
|
+
"description": "The Krizaka birds standing on a stage, alive: each at ease in character, two neighbours glancing at each other, the pigeon carrying its parcel across, a bird looking up when you come near or tap it — and, with `scenes`, a short scene for each bird.",
|
|
2350
2350
|
"status": "beta",
|
|
2351
2351
|
"category": "blocks",
|
|
2352
2352
|
"platforms": "web",
|
|
@@ -2364,7 +2364,7 @@
|
|
|
2364
2364
|
}
|
|
2365
2365
|
},
|
|
2366
2366
|
"examples": {
|
|
2367
|
-
"web":
|
|
2367
|
+
"web": 3,
|
|
2368
2368
|
"native": 0
|
|
2369
2369
|
},
|
|
2370
2370
|
"dependencies": [],
|
|
@@ -3142,7 +3142,7 @@
|
|
|
3142
3142
|
"native": 2
|
|
3143
3143
|
},
|
|
3144
3144
|
"dependencies": [
|
|
3145
|
-
"@krizaka/tokens@^3.
|
|
3145
|
+
"@krizaka/tokens@^3.42.0",
|
|
3146
3146
|
"@protomaps/basemaps@5.7.2",
|
|
3147
3147
|
"tailwind-variants@^3.3.1"
|
|
3148
3148
|
],
|
|
@@ -3581,7 +3581,7 @@
|
|
|
3581
3581
|
"native": 0
|
|
3582
3582
|
},
|
|
3583
3583
|
"dependencies": [
|
|
3584
|
-
"@krizaka/tokens@^3.
|
|
3584
|
+
"@krizaka/tokens@^3.42.0",
|
|
3585
3585
|
"radix-ui@^1.7.0",
|
|
3586
3586
|
"tailwind-variants@^3.3.1"
|
|
3587
3587
|
],
|
|
@@ -4244,7 +4244,7 @@
|
|
|
4244
4244
|
"native": 0
|
|
4245
4245
|
},
|
|
4246
4246
|
"dependencies": [
|
|
4247
|
-
"@krizaka/tokens@^3.
|
|
4247
|
+
"@krizaka/tokens@^3.42.0",
|
|
4248
4248
|
"radix-ui@^1.7.0",
|
|
4249
4249
|
"tailwind-variants@^3.3.1"
|
|
4250
4250
|
],
|
|
@@ -4587,7 +4587,7 @@
|
|
|
4587
4587
|
"native": 0
|
|
4588
4588
|
},
|
|
4589
4589
|
"dependencies": [
|
|
4590
|
-
"@krizaka/tokens@^3.
|
|
4590
|
+
"@krizaka/tokens@^3.42.0",
|
|
4591
4591
|
"radix-ui@^1.7.0",
|
|
4592
4592
|
"tailwind-variants@^3.3.1"
|
|
4593
4593
|
],
|
package/registry/map.json
CHANGED
package/registry/mascots.json
CHANGED
|
@@ -95,6 +95,13 @@
|
|
|
95
95
|
"description": "Which bird of the flock: `falcon` (watch, security), `owl` (reading, documents), `duck` (building, code), `flamingo` (answers, support), `pigeon` (delivery, notifications), `lovebird` (introductions, two people who meet), `crane` (listening, a calm moment), `magpie` (the score: points, standings, trophies), `beaver` (the shop: a small business at work).",
|
|
96
96
|
"required": false
|
|
97
97
|
},
|
|
98
|
+
{
|
|
99
|
+
"name": "carrying",
|
|
100
|
+
"type": "boolean",
|
|
101
|
+
"default": "true",
|
|
102
|
+
"description": "Whether the pigeon holds its parcel (default `true`): a flock hands it over and the bird flies back light.",
|
|
103
|
+
"required": false
|
|
104
|
+
},
|
|
98
105
|
{
|
|
99
106
|
"name": "flying",
|
|
100
107
|
"type": "boolean",
|
|
@@ -408,7 +415,7 @@
|
|
|
408
415
|
},
|
|
409
416
|
{
|
|
410
417
|
"path": "mascots/mascot.tsx",
|
|
411
|
-
"content": "// The Krizaka flock on the web: one bird, one mood, drawn from ./geometry in the host's brand colours.\nimport type * as React from \"react\";\nimport { useId } from \"react\";\n\nimport { archEye, BIRDS, brow, lidEye, LIGHT, type MascotBird, type MascotMood, type MascotPalette, type MascotTone, MOOD_PARTS, OWN_PALETTES, PARCEL, SHADE, upperLid } from \"./geometry\";\nimport { type MascotLook, phaseOf } from \"./life\";\n\nconst CSS = `\n.kzb-bird { transform-box: view-box; transform-origin: 60px 108px; }\n.kzb-eye { transform-box: fill-box; transform-origin: center; }\n.kzb-wing { transform-box: view-box; }\n[data-animated] .kzb-bird { animation: kzb-bob 3.6s ease-in-out infinite; }\n[data-animated] .kzb-blink { animation: kzb-blink 5.2s ease-in-out infinite; }\n[data-animated][data-mood=\"welcome\"] .kzb-wing-r { animation: kzb-wave 1.1s ease-in-out infinite alternate; }\n[data-animated][data-mood=\"thinking\"] .kzb-dot { animation: kzb-dot 1.5s ease-in-out infinite; }\n[data-animated][data-mood=\"thinking\"] .kzb-dot:nth-child(2) { animation-delay: .2s; }\n[data-animated][data-mood=\"thinking\"] .kzb-dot:nth-child(3) { animation-delay: .4s; }\n[data-animated][data-mood=\"success\"] .kzb-bird { animation: kzb-hop 1.6s cubic-bezier(.3,1.6,.5,1) 1, kzb-bob 3.6s ease-in-out 1.6s infinite; }\n[data-animated][data-mood=\"success\"] .kzb-badge { animation: kzb-pop .5s cubic-bezier(.3,1.6,.5,1) .3s both; }\n[data-animated][data-mood=\"success\"] .kzb-sparkle { animation: kzb-twinkle 2.4s ease-in-out infinite; }\n[data-animated][data-mood=\"error\"] .kzb-bird { animation: kzb-shake .5s ease-in-out .2s 2, kzb-bob 3.6s ease-in-out 1.2s infinite; }\n[data-animated][data-mood=\"empty\"] .kzb-bird { animation: kzb-sway 5s ease-in-out infinite; }\n.kzb-badge, .kzb-sparkle, .kzb-dot { transform-box: fill-box; transform-origin: center; }\n@keyframes kzb-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2.5px); } }\n@keyframes kzb-blink { 0%, 92%, 100% { transform: scaleY(1); } 95% { transform: scaleY(.1); } }\n@keyframes kzb-wave { from { transform: rotate(-8deg); } to { transform: rotate(-38deg); } }\n@keyframes kzb-dot { 0%, 100% { opacity: .35; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-2px); } }\n@keyframes kzb-hop { 0% { transform: translateY(0); } 30% { transform: translateY(-9px); } 60% { transform: translateY(0); } 100% { transform: translateY(0); } }\n@keyframes kzb-pop { from { transform: scale(0); } to { transform: scale(1); } }\n@keyframes kzb-twinkle { 0%, 100% { opacity: .25; transform: scale(.7); } 50% { opacity: 1; transform: scale(1); } }\n@keyframes kzb-shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }\n@keyframes kzb-sway { 0%, 100% { transform: rotate(0); } 50% { transform: rotate(-2deg); } }\n@media (prefers-reduced-motion: reduce) {\n .kzb-bird, .kzb-blink, .kzb-wing, .kzb-wing-r, .kzb-hop, .kzb-head, .kzb-parcel, .kzb-dot, .kzb-badge, .kzb-sparkle { animation: none !important; }\n .kzb-look, .kzb-pupil { transition: none !important; transform: none !important; }\n}\n`;\n\n/** The host's roles (`tone=\"brand\"`): the accent paints the bird, so it wears the product it is shown in. */\nconst BRAND: MascotPalette = {\n from: \"var(--kz-accent-2, var(--kz-accent))\",\n to: \"var(--kz-accent)\",\n wing: \"var(--kz-accent)\",\n detail: \"var(--kz-accent-2, var(--kz-accent))\",\n beak: \"var(--kz-warning)\",\n eye: \"var(--kz-text-primary)\",\n};\nconst OUTLINE = \"var(--kz-border-strong, color-mix(in srgb, currentColor 35%, transparent))\";\nconst SURFACE = \"var(--kz-surface-0)\";\n\nexport type MascotProps = Omit<React.ComponentProps<\"svg\">, \"children\"> & {\n /** Which bird of the flock: `falcon` (watch, security), `owl` (reading, documents), `duck` (building, code), `flamingo` (answers, support), `pigeon` (delivery, notifications), `lovebird` (introductions, two people who meet), `crane` (listening, a calm moment), `magpie` (the score: points, standings, trophies), `beaver` (the shop: a small business at work). */\n bird?: MascotBird;\n /** What the moment is: `welcome` (onboarding), `thinking` (a long task running), `success` (it is done), `empty` (nothing here yet), `error` (it failed). */\n mood?: MascotMood;\n /** `brand` (default): the host's accent, so the bird wears the product it is in. `own`: the bird's signature colours of the Krizaka story. */\n tone?: MascotTone;\n /** Width and height in pixels. */\n size?: number;\n /** A light idle motion and the mood's gesture. Off with `false`; always off under `prefers-reduced-motion`. */\n animated?: boolean;\n /** An accessible name (the bird becomes an image); without it the mascot is decorative. */\n title?: string;\n /**\n * The bird at ease, in character (mood `welcome`, the default): it breathes, blinks in its own rhythm, tilts its head,\n * ruffles a wing, now and then hops — the falcon watches, the owl reads, the duck builds, the flamingo answers, the\n * pigeon delivers. CSS only, still under `prefers-reduced-motion` and with `animated={false}`.\n */\n life?: boolean;\n /** Where in its cycles the bird starts, 0 to 1 (default: from the instance's id), so two birds never move together. */\n phase?: number;\n /** Where it looks (`life`): `left` and `right` turn the head and the eyes, `up` raises them, `center` rests. */\n look?: MascotLook;\n /** In flight (`life`): the wings beat, the body leans into the trip — the pigeon of a delivery. */\n flying?: boolean;\n /** Which way it leans in flight, -1 (left) to 1 (right). */\n bank?: number;\n /** A tap or a poke just happened (`life`): a small jump. Set it for a moment, then unset. */\n poked?: boolean;\n};\n\n/**\n * One of the nine Krizaka figures in one mood — for the moments a product speaks to a person: a welcome, a long wait,\n * a success, an empty list, an error. Drawn in the host's brand colours by default (`tone=\"own\"` for the story's).\n */\nexport function Mascot({ bird = \"owl\", mood = \"welcome\", tone = \"brand\", size = 96, animated = true, title, life = false, phase, look, flying, bank, poked, className, style, ...props }: MascotProps) {\n const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n const g = BIRDS[bird];\n const alive = life && mood === \"welcome\";\n const ph = phase ?? phaseOf(uid);\n const p = tone === \"own\" ? OWN_PALETTES[bird] : BRAND;\n const body = `url(#kzb-${uid})`;\n const line = { stroke: OUTLINE, strokeWidth: 1.4, strokeLinejoin: \"round\" as const };\n const eyeR = g.eyes.white || g.eyes.pupil * 1.2;\n const eyes = g.eyes.at.map((at, i) => {\n const glance = mood === \"thinking\" ? 1.4 : 0;\n if (mood === \"success\") return <path key={i} d={archEye(at, eyeR * 0.85)} stroke={g.visor ? p.eye : OUTLINE} strokeWidth={2.2} strokeLinecap=\"round\" fill=\"none\" />;\n if (mood === \"empty\" && g.eyes.white > 0)\n return (\n <g key={i}>\n <circle cx={at[0]} cy={at[1]} r={g.eyes.white} fill={LIGHT} {...line} />\n <circle cx={at[0]} cy={at[1] + g.eyes.pupil * 0.7} r={g.eyes.pupil} fill={SHADE} />\n <path d={upperLid(at, g.eyes.white)} fill={p.detail} {...line} />\n </g>\n );\n if (mood === \"empty\") return <path key={i} d={lidEye(at, eyeR * 0.8)} stroke={g.visor ? p.eye : OUTLINE} strokeWidth={2.2} strokeLinecap=\"round\" fill=\"none\" />;\n return (\n <g key={i} className=\"kzb-eye kzb-blink\">\n {g.eyes.white > 0 && <circle cx={at[0]} cy={at[1]} r={g.eyes.white} fill={LIGHT} {...line} />}\n <g className=\"kzb-pupil\">\n <circle cx={at[0] + glance} cy={at[1] - glance} r={g.eyes.pupil} fill={g.visor ? p.eye : SHADE} />\n {g.eyes.white > 0 && <circle cx={at[0] + glance - g.eyes.pupil * 0.35} cy={at[1] - glance - g.eyes.pupil * 0.35} r={g.eyes.pupil * 0.35} fill={LIGHT} />}\n </g>\n </g>\n );\n });\n const [wl, wr] = g.wings;\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 120 120\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n data-bird={bird}\n data-mood={mood}\n data-animated={animated ? \"\" : undefined}\n data-life={alive ? \"\" : undefined}\n data-look={alive && look && look !== \"center\" ? look : undefined}\n data-flying={alive && flying ? \"\" : undefined}\n data-poked={alive && poked ? \"\" : undefined}\n className={className}\n style={alive ? ({ \"--kzb-ph\": ph, \"--kzb-bank\": bank ?? 0, ...style } as React.CSSProperties) : style}\n {...(title ? { role: \"img\", \"aria-label\": title } : { \"aria-hidden\": true })}\n {...props}\n >\n <style>{CSS}</style>\n <defs>\n <linearGradient id={`kzb-${uid}`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" style={{ stopColor: p.from }} />\n <stop offset=\"100%\" style={{ stopColor: p.to }} />\n </linearGradient>\n </defs>\n <ellipse cx={g.shadow[0]} cy={g.shadow[1]} rx={g.shadow[2]} ry={g.shadow[3]} fill={SHADE} opacity={0.14} />\n {mood === \"empty\" && <path d={MOOD_PARTS.tray} stroke={OUTLINE} strokeWidth={1.6} strokeDasharray=\"3 3\" strokeLinejoin=\"round\" />}\n <g className=\"kzb-hop\">\n <g className=\"kzb-bird\">\n {g.legs.map((d) => (\n <path key={d} d={d} stroke={p.beak} strokeWidth={2.6} strokeLinecap=\"round\" />\n ))}\n {g.back?.map((d) => (d.endsWith(\"Z\") ? <path key={d} d={d} fill={p.detail} {...line} /> : <path key={d} d={d} stroke={OUTLINE} strokeWidth={1.4} />))}\n {g.parcel && (\n <g className=\"kzb-parcel\" {...line}>\n {g.parcel.map((d, i) => (\n <path key={d} d={d} fill={PARCEL[i]} />\n ))}\n </g>\n )}\n <path d={g.body} fill={body} {...line} />\n <path d={g.belly} fill={LIGHT} opacity={0.28} />\n {[wl, wr].map((w, i) => {\n const turn = mood === \"success\" ? (i === 0 ? 28 : -28) : mood === \"welcome\" && i === 1 && !alive ? -20 : 0;\n return (\n <g key={i} className={i === 1 ? \"kzb-wing kzb-wing-r\" : \"kzb-wing kzb-wing-l\"} style={{ transformOrigin: `${w.pivot[0]}px ${w.pivot[1]}px`, transform: turn ? `rotate(${turn}deg)` : undefined }}>\n <path d={w.d} fill={p.wing} {...line} />\n <path d={w.d} fill={SHADE} opacity={0.16} />\n </g>\n );\n })}\n <g className=\"kzb-look\">\n <g className=\"kzb-head\">\n {g.neck && <path d={g.neck.d} stroke={body} strokeWidth={g.neck.width} strokeLinecap=\"round\" />}\n {g.head.r > 0 && <circle cx={g.head.cx} cy={g.head.cy} r={g.head.r} fill={body} {...line} />}\n {g.details.map((d) => (\n <path key={d} d={d} fill={p.detail} {...line} />\n ))}\n {g.visor && <rect x={g.visor.x} y={g.visor.y} width={g.visor.w} height={g.visor.h} rx={4} fill={SURFACE} {...line} />}\n {eyes}\n {g.glasses && (\n <g stroke={OUTLINE} strokeWidth={1.3}>\n {g.eyes.at.map(([x, y]) => (\n <circle key={x} cx={x} cy={y} r={6.5} fill={LIGHT} fillOpacity={0.12} />\n ))}\n <path d={`M${g.eyes.at[0][0] + 6.5} ${g.eyes.at[0][1]} L${g.eyes.at[1][0] - 6.5} ${g.eyes.at[1][1]}`} />\n </g>\n )}\n {mood === \"error\" &&\n g.eyes.at.map((at, i) => <path key={i} d={brow(at, eyeR, g.eyes.at.length === 1 ? 1 : i === 0 ? -1 : 1)} stroke={OUTLINE} strokeWidth={2} strokeLinecap=\"round\" />)}\n <path d={g.beak} fill={p.beak} {...line} />\n </g>\n </g>\n </g>\n </g>\n {mood === \"thinking\" && (\n <g>\n {MOOD_PARTS.dots.map(([cx, cy, r]) => (\n <circle key={cx} className=\"kzb-dot\" cx={cx} cy={cy} r={r} fill=\"var(--kz-text-secondary)\" />\n ))}\n </g>\n )}\n {mood === \"success\" && (\n <>\n {MOOD_PARTS.sparkles.map((d) => (\n <path key={d} className=\"kzb-sparkle\" d={d} fill={p.from} />\n ))}\n <g className=\"kzb-badge\">\n <circle cx={MOOD_PARTS.badge.cx} cy={MOOD_PARTS.badge.cy} r={MOOD_PARTS.badge.r} fill=\"var(--kz-success)\" />\n <path d={MOOD_PARTS.check} stroke={SURFACE} strokeWidth={2.6} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </g>\n </>\n )}\n {mood === \"error\" && (\n <g className=\"kzb-badge\">\n <circle cx={MOOD_PARTS.badge.cx} cy={MOOD_PARTS.badge.cy} r={MOOD_PARTS.badge.r} fill=\"var(--kz-danger)\" />\n <path d={MOOD_PARTS.bang.line} stroke={SURFACE} strokeWidth={2.8} strokeLinecap=\"round\" />\n <circle cx={MOOD_PARTS.bang.dot[0]} cy={MOOD_PARTS.bang.dot[1]} r={1.6} fill={SURFACE} />\n </g>\n )}\n </svg>\n );\n}\n"
|
|
418
|
+
"content": "// The Krizaka flock on the web: one bird, one mood, drawn from ./geometry in the host's brand colours.\nimport type * as React from \"react\";\nimport { useId } from \"react\";\n\nimport { archEye, BIRDS, brow, lidEye, LIGHT, type MascotBird, type MascotMood, type MascotPalette, type MascotTone, MOOD_PARTS, OWN_PALETTES, PARCEL, SHADE, upperLid } from \"./geometry\";\nimport { type MascotLook, phaseOf } from \"./life\";\n\nconst CSS = `\n.kzb-bird { transform-box: view-box; transform-origin: 60px 108px; }\n.kzb-eye { transform-box: fill-box; transform-origin: center; }\n.kzb-wing { transform-box: view-box; }\n[data-animated] .kzb-bird { animation: kzb-bob 3.6s ease-in-out infinite; }\n[data-animated] .kzb-blink { animation: kzb-blink 5.2s ease-in-out infinite; }\n[data-animated][data-mood=\"welcome\"] .kzb-wing-r { animation: kzb-wave 1.1s ease-in-out infinite alternate; }\n[data-animated][data-mood=\"thinking\"] .kzb-dot { animation: kzb-dot 1.5s ease-in-out infinite; }\n[data-animated][data-mood=\"thinking\"] .kzb-dot:nth-child(2) { animation-delay: .2s; }\n[data-animated][data-mood=\"thinking\"] .kzb-dot:nth-child(3) { animation-delay: .4s; }\n[data-animated][data-mood=\"success\"] .kzb-bird { animation: kzb-hop 1.6s cubic-bezier(.3,1.6,.5,1) 1, kzb-bob 3.6s ease-in-out 1.6s infinite; }\n[data-animated][data-mood=\"success\"] .kzb-badge { animation: kzb-pop .5s cubic-bezier(.3,1.6,.5,1) .3s both; }\n[data-animated][data-mood=\"success\"] .kzb-sparkle { animation: kzb-twinkle 2.4s ease-in-out infinite; }\n[data-animated][data-mood=\"error\"] .kzb-bird { animation: kzb-shake .5s ease-in-out .2s 2, kzb-bob 3.6s ease-in-out 1.2s infinite; }\n[data-animated][data-mood=\"empty\"] .kzb-bird { animation: kzb-sway 5s ease-in-out infinite; }\n.kzb-badge, .kzb-sparkle, .kzb-dot { transform-box: fill-box; transform-origin: center; }\n@keyframes kzb-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2.5px); } }\n@keyframes kzb-blink { 0%, 92%, 100% { transform: scaleY(1); } 95% { transform: scaleY(.1); } }\n@keyframes kzb-wave { from { transform: rotate(-8deg); } to { transform: rotate(-38deg); } }\n@keyframes kzb-dot { 0%, 100% { opacity: .35; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-2px); } }\n@keyframes kzb-hop { 0% { transform: translateY(0); } 30% { transform: translateY(-9px); } 60% { transform: translateY(0); } 100% { transform: translateY(0); } }\n@keyframes kzb-pop { from { transform: scale(0); } to { transform: scale(1); } }\n@keyframes kzb-twinkle { 0%, 100% { opacity: .25; transform: scale(.7); } 50% { opacity: 1; transform: scale(1); } }\n@keyframes kzb-shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }\n@keyframes kzb-sway { 0%, 100% { transform: rotate(0); } 50% { transform: rotate(-2deg); } }\n@media (prefers-reduced-motion: reduce) {\n .kzb-bird, .kzb-blink, .kzb-wing, .kzb-wing-r, .kzb-hop, .kzb-head, .kzb-parcel, .kzb-dot, .kzb-badge, .kzb-sparkle { animation: none !important; }\n .kzb-look, .kzb-pupil { transition: none !important; transform: none !important; }\n}\n`;\n\n/** The host's roles (`tone=\"brand\"`): the accent paints the bird, so it wears the product it is shown in. */\nconst BRAND: MascotPalette = {\n from: \"var(--kz-accent-2, var(--kz-accent))\",\n to: \"var(--kz-accent)\",\n wing: \"var(--kz-accent)\",\n detail: \"var(--kz-accent-2, var(--kz-accent))\",\n beak: \"var(--kz-warning)\",\n eye: \"var(--kz-text-primary)\",\n};\nconst OUTLINE = \"var(--kz-border-strong, color-mix(in srgb, currentColor 35%, transparent))\";\nconst SURFACE = \"var(--kz-surface-0)\";\n\nexport type MascotProps = Omit<React.ComponentProps<\"svg\">, \"children\"> & {\n /** Which bird of the flock: `falcon` (watch, security), `owl` (reading, documents), `duck` (building, code), `flamingo` (answers, support), `pigeon` (delivery, notifications), `lovebird` (introductions, two people who meet), `crane` (listening, a calm moment), `magpie` (the score: points, standings, trophies), `beaver` (the shop: a small business at work). */\n bird?: MascotBird;\n /** What the moment is: `welcome` (onboarding), `thinking` (a long task running), `success` (it is done), `empty` (nothing here yet), `error` (it failed). */\n mood?: MascotMood;\n /** `brand` (default): the host's accent, so the bird wears the product it is in. `own`: the bird's signature colours of the Krizaka story. */\n tone?: MascotTone;\n /** Width and height in pixels. */\n size?: number;\n /** A light idle motion and the mood's gesture. Off with `false`; always off under `prefers-reduced-motion`. */\n animated?: boolean;\n /** An accessible name (the bird becomes an image); without it the mascot is decorative. */\n title?: string;\n /**\n * The bird at ease, in character (mood `welcome`, the default): it breathes, blinks in its own rhythm, tilts its head,\n * ruffles a wing, now and then hops — the falcon watches, the owl reads, the duck builds, the flamingo answers, the\n * pigeon delivers. CSS only, still under `prefers-reduced-motion` and with `animated={false}`.\n */\n life?: boolean;\n /** Where in its cycles the bird starts, 0 to 1 (default: from the instance's id), so two birds never move together. */\n phase?: number;\n /** Where it looks (`life`): `left` and `right` turn the head and the eyes, `up` raises them, `center` rests. */\n look?: MascotLook;\n /** In flight (`life`): the wings beat, the body leans into the trip — the pigeon of a delivery. */\n flying?: boolean;\n /** Which way it leans in flight, -1 (left) to 1 (right). */\n bank?: number;\n /** Whether the pigeon holds its parcel (default `true`): a flock hands it over and the bird flies back light. */\n carrying?: boolean;\n /** A tap or a poke just happened (`life`): a small jump. Set it for a moment, then unset. */\n poked?: boolean;\n};\n\n/**\n * One of the nine Krizaka figures in one mood — for the moments a product speaks to a person: a welcome, a long wait,\n * a success, an empty list, an error. Drawn in the host's brand colours by default (`tone=\"own\"` for the story's).\n */\nexport function Mascot({ bird = \"owl\", mood = \"welcome\", tone = \"brand\", size = 96, animated = true, title, life = false, phase, look, flying, bank, poked, carrying = true, className, style, ...props }: MascotProps) {\n const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n const g = BIRDS[bird];\n const alive = life && mood === \"welcome\";\n const ph = phase ?? phaseOf(uid);\n const p = tone === \"own\" ? OWN_PALETTES[bird] : BRAND;\n const body = `url(#kzb-${uid})`;\n const line = { stroke: OUTLINE, strokeWidth: 1.4, strokeLinejoin: \"round\" as const };\n const eyeR = g.eyes.white || g.eyes.pupil * 1.2;\n const eyes = g.eyes.at.map((at, i) => {\n const glance = mood === \"thinking\" ? 1.4 : 0;\n if (mood === \"success\") return <path key={i} d={archEye(at, eyeR * 0.85)} stroke={g.visor ? p.eye : OUTLINE} strokeWidth={2.2} strokeLinecap=\"round\" fill=\"none\" />;\n if (mood === \"empty\" && g.eyes.white > 0)\n return (\n <g key={i}>\n <circle cx={at[0]} cy={at[1]} r={g.eyes.white} fill={LIGHT} {...line} />\n <circle cx={at[0]} cy={at[1] + g.eyes.pupil * 0.7} r={g.eyes.pupil} fill={SHADE} />\n <path d={upperLid(at, g.eyes.white)} fill={p.detail} {...line} />\n </g>\n );\n if (mood === \"empty\") return <path key={i} d={lidEye(at, eyeR * 0.8)} stroke={g.visor ? p.eye : OUTLINE} strokeWidth={2.2} strokeLinecap=\"round\" fill=\"none\" />;\n return (\n <g key={i} className=\"kzb-eye kzb-blink\">\n {g.eyes.white > 0 && <circle cx={at[0]} cy={at[1]} r={g.eyes.white} fill={LIGHT} {...line} />}\n <g className=\"kzb-pupil\">\n <circle cx={at[0] + glance} cy={at[1] - glance} r={g.eyes.pupil} fill={g.visor ? p.eye : SHADE} />\n {g.eyes.white > 0 && <circle cx={at[0] + glance - g.eyes.pupil * 0.35} cy={at[1] - glance - g.eyes.pupil * 0.35} r={g.eyes.pupil * 0.35} fill={LIGHT} />}\n </g>\n </g>\n );\n });\n const [wl, wr] = g.wings;\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 120 120\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n data-bird={bird}\n data-mood={mood}\n data-animated={animated ? \"\" : undefined}\n data-life={alive ? \"\" : undefined}\n data-look={alive && look && look !== \"center\" ? look : undefined}\n data-flying={alive && flying ? \"\" : undefined}\n data-poked={alive && poked ? \"\" : undefined}\n className={className}\n style={alive ? ({ \"--kzb-ph\": ph, \"--kzb-bank\": bank ?? 0, ...style } as React.CSSProperties) : style}\n {...(title ? { role: \"img\", \"aria-label\": title } : { \"aria-hidden\": true })}\n {...props}\n >\n <style>{CSS}</style>\n <defs>\n <linearGradient id={`kzb-${uid}`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" style={{ stopColor: p.from }} />\n <stop offset=\"100%\" style={{ stopColor: p.to }} />\n </linearGradient>\n </defs>\n <ellipse cx={g.shadow[0]} cy={g.shadow[1]} rx={g.shadow[2]} ry={g.shadow[3]} fill={SHADE} opacity={0.14} />\n {mood === \"empty\" && <path d={MOOD_PARTS.tray} stroke={OUTLINE} strokeWidth={1.6} strokeDasharray=\"3 3\" strokeLinejoin=\"round\" />}\n <g className=\"kzb-hop\">\n <g className=\"kzb-bird\">\n {g.legs.map((d) => (\n <path key={d} d={d} stroke={p.beak} strokeWidth={2.6} strokeLinecap=\"round\" />\n ))}\n {g.back?.map((d) => (d.endsWith(\"Z\") ? <path key={d} d={d} fill={p.detail} {...line} /> : <path key={d} d={d} stroke={OUTLINE} strokeWidth={1.4} />))}\n {g.parcel && carrying && (\n <g className=\"kzb-parcel\" {...line}>\n {g.parcel.map((d, i) => (\n <path key={d} d={d} fill={PARCEL[i]} />\n ))}\n </g>\n )}\n <path d={g.body} fill={body} {...line} />\n <path d={g.belly} fill={LIGHT} opacity={0.28} />\n {[wl, wr].map((w, i) => {\n const turn = mood === \"success\" ? (i === 0 ? 28 : -28) : mood === \"welcome\" && i === 1 && !alive ? -20 : 0;\n return (\n <g key={i} className={i === 1 ? \"kzb-wing kzb-wing-r\" : \"kzb-wing kzb-wing-l\"} style={{ transformOrigin: `${w.pivot[0]}px ${w.pivot[1]}px`, transform: turn ? `rotate(${turn}deg)` : undefined }}>\n <path d={w.d} fill={p.wing} {...line} />\n <path d={w.d} fill={SHADE} opacity={0.16} />\n </g>\n );\n })}\n <g className=\"kzb-look\">\n <g className=\"kzb-head\">\n {g.neck && <path d={g.neck.d} stroke={body} strokeWidth={g.neck.width} strokeLinecap=\"round\" />}\n {g.head.r > 0 && <circle cx={g.head.cx} cy={g.head.cy} r={g.head.r} fill={body} {...line} />}\n {g.details.map((d) => (\n <path key={d} d={d} fill={p.detail} {...line} />\n ))}\n {g.visor && <rect x={g.visor.x} y={g.visor.y} width={g.visor.w} height={g.visor.h} rx={4} fill={SURFACE} {...line} />}\n {eyes}\n {g.glasses && (\n <g stroke={OUTLINE} strokeWidth={1.3}>\n {g.eyes.at.map(([x, y]) => (\n <circle key={x} cx={x} cy={y} r={6.5} fill={LIGHT} fillOpacity={0.12} />\n ))}\n <path d={`M${g.eyes.at[0][0] + 6.5} ${g.eyes.at[0][1]} L${g.eyes.at[1][0] - 6.5} ${g.eyes.at[1][1]}`} />\n </g>\n )}\n {mood === \"error\" &&\n g.eyes.at.map((at, i) => <path key={i} d={brow(at, eyeR, g.eyes.at.length === 1 ? 1 : i === 0 ? -1 : 1)} stroke={OUTLINE} strokeWidth={2} strokeLinecap=\"round\" />)}\n <path d={g.beak} fill={p.beak} {...line} />\n </g>\n </g>\n </g>\n </g>\n {mood === \"thinking\" && (\n <g>\n {MOOD_PARTS.dots.map(([cx, cy, r]) => (\n <circle key={cx} className=\"kzb-dot\" cx={cx} cy={cy} r={r} fill=\"var(--kz-text-secondary)\" />\n ))}\n </g>\n )}\n {mood === \"success\" && (\n <>\n {MOOD_PARTS.sparkles.map((d) => (\n <path key={d} className=\"kzb-sparkle\" d={d} fill={p.from} />\n ))}\n <g className=\"kzb-badge\">\n <circle cx={MOOD_PARTS.badge.cx} cy={MOOD_PARTS.badge.cy} r={MOOD_PARTS.badge.r} fill=\"var(--kz-success)\" />\n <path d={MOOD_PARTS.check} stroke={SURFACE} strokeWidth={2.6} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </g>\n </>\n )}\n {mood === \"error\" && (\n <g className=\"kzb-badge\">\n <circle cx={MOOD_PARTS.badge.cx} cy={MOOD_PARTS.badge.cy} r={MOOD_PARTS.badge.r} fill=\"var(--kz-danger)\" />\n <path d={MOOD_PARTS.bang.line} stroke={SURFACE} strokeWidth={2.8} strokeLinecap=\"round\" />\n <circle cx={MOOD_PARTS.bang.dot[0]} cy={MOOD_PARTS.bang.dot[1]} r={1.6} fill={SURFACE} />\n </g>\n )}\n </svg>\n );\n}\n"
|
|
412
419
|
},
|
|
413
420
|
{
|
|
414
421
|
"path": "mascots/meta.ts",
|
|
@@ -464,6 +471,13 @@
|
|
|
464
471
|
"description": "Which bird of the flock: `falcon` (watch, security), `owl` (reading, documents), `duck` (building, code), `flamingo` (answers, support), `pigeon` (delivery, notifications), `lovebird` (introductions, two people who meet), `crane` (listening, a calm moment), `magpie` (the score: points, standings, trophies), `beaver` (the shop: a small business at work).",
|
|
465
472
|
"required": false
|
|
466
473
|
},
|
|
474
|
+
{
|
|
475
|
+
"name": "carrying",
|
|
476
|
+
"type": "boolean",
|
|
477
|
+
"default": "true",
|
|
478
|
+
"description": "Whether the pigeon holds its parcel (default `true`): a flock hands it over and the bird flies back light.",
|
|
479
|
+
"required": false
|
|
480
|
+
},
|
|
467
481
|
{
|
|
468
482
|
"name": "flying",
|
|
469
483
|
"type": "boolean",
|
package/registry/nav-bar.json
CHANGED
package/registry/rail-nav.json
CHANGED
|
@@ -107,6 +107,13 @@
|
|
|
107
107
|
"description": "The end of the signature line: legal links, a language switch (`SiteFooter.Link`s or any inline content).",
|
|
108
108
|
"required": false
|
|
109
109
|
},
|
|
110
|
+
{
|
|
111
|
+
"name": "path",
|
|
112
|
+
"type": "string",
|
|
113
|
+
"default": null,
|
|
114
|
+
"description": "The path of the page being read on this site (`/en/story`, `/blog/…`): the company link that points to this very\npage says so (`aria-current=\"page\"`, a visible state) and a click on it scrolls to the top instead of reloading.",
|
|
115
|
+
"required": false
|
|
116
|
+
},
|
|
110
117
|
{
|
|
111
118
|
"name": "variant",
|
|
112
119
|
"type": "\"site\" | \"docs\" | \"app\"",
|
|
@@ -325,11 +332,11 @@
|
|
|
325
332
|
},
|
|
326
333
|
{
|
|
327
334
|
"path": "site-footer/site-footer.tsx",
|
|
328
|
-
"content": "// Client (the marks hold an id). The footer shared by every site of the Krizaka family — one calm band (Cahier, ADR-004;\n// peer review krizaka-ui#434: « too much, a double footer »): the site's own columns and the family's columns side by\n// side — Products · Open source (GitHub with them) · Krizaka (our story, the blog, contact) — then one line: the\n// signature (“A Krizaka product”, by the kind of the current site, to www), the version, the place, the legal links.\n// No site block (the bar already names the site), no column that lists only Krizaka, no link twice.\nimport { Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { FamilyMark } from \"../family-mark\";\nimport { KrizakaLogo } from \"../marks/KrizakaLogo\";\nimport { companyLinks, type FamilyLabels, familyLabels, siteById, type SiteId, sitesByGroup } from \"../sites/sites\";\n\nexport const siteFooter = tv({\n slots: {\n root: \"w-full border-t border-border-subtle bg-surface-0 text-fg\",\n inner:\n \"mx-auto grid w-full max-w-[calc(var(--kz-page-max,72rem)_+_2*var(--kz-page-gutter,clamp(16px,3.2vw,32px)))] grid-cols-[minmax(0,1fr)] gap-10 px-[var(--kz-page-gutter,clamp(16px,3.2vw,32px))] py-12\",\n band: \"grid grid-cols-[minmax(0,1fr)] gap-8 lg:grid-flow-col lg:auto-cols-[minmax(0,1fr)]\",\n own: \"grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-8\",\n family: \"grid grid-cols-2 gap-8 sm:grid-cols-3\",\n column: \"grid min-w-0 content-start gap-3\",\n title: \"text-[11px] font-semibold text-fg-secondary\",\n list: \"grid min-w-0 gap-2\",\n link:\n \"inline-flex min-h-6 min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 rounded-md text-sm [overflow-wrap:anywhere] text-fg-secondary no-underline transition-colors hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:text-fg aria-[current=true]:text-fg aria-[current=true]:font-semibold\",\n mark: \"flex h-4 w-4 shrink-0 items-center justify-center text-fg\",\n tag: \"rounded-sm bg-surface-3 px-1.5 py-px text-[10px] font-semibold text-fg-secondary\",\n bottom: \"flex flex-wrap items-center gap-x-5 gap-y-3 border-t border-border-subtle pt-6 text-xs text-fg-secondary\",\n signature:\n \"inline-flex items-center gap-2 rounded-md font-medium text-fg no-underline hover:text-fg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n version:\n \"rounded-sm border border-border-subtle bg-surface-2 px-2 py-0.5 font-mono text-[11px] text-fg-secondary\",\n legal: \"flex flex-wrap items-center gap-x-4 gap-y-2 sm:ml-auto\",\n },\n variants: {\n // `site`: the full footer. `docs`: no site block, the columns in one row, on the documentation's width. `app`: one\n // slim line inside a signed-in product — signature, version, legal links.\n variant: {\n site: {},\n docs: {\n inner: \"max-w-[calc(var(--kz-page-max-docs,88rem)_+_2*var(--kz-page-gutter,clamp(16px,3.2vw,32px)))] gap-6 py-8\",\n own: \"sm:grid-cols-4\",\n },\n app: {\n root: \"bg-transparent\",\n inner: \"gap-0 py-3\",\n bottom: \"border-t-0 pt-0\",\n },\n },\n },\n defaultVariants: { variant: \"site\" },\n});\n\nexport type SiteFooterProps = Omit<React.ComponentProps<\"footer\">, \"children\"> & {\n /** The site this footer belongs to (`@krizaka/ui/sites`): its mark and line, its signature, marked in the family. */\n current: SiteId;\n /**\n * The family's words, passed translated: `familyLabels.fr` from `@krizaka/ui/sites`, or your own of the same shape.\n * @default familyLabels.en\n */\n labels?: FamilyLabels;\n /** The site's own columns (`SiteFooter.Column`), before the family. */\n children?: React.ReactNode;\n /** The version the site runs (`1.2.3`, shown `v1.2.3`). */\n version?: string;\n /** The environment, when it is not production (`DEV`). */\n env?: string;\n /**\n * Shows the family in columns (the Krizaka sites: www, ui., java.). A product is independent once deployed: pass\n * `family={false}` and the footer holds only its own columns, the legal links, the version and one publisher line —\n * “A Krizaka product”, linking to www.krizaka.com.\n * @default true\n */\n family?: boolean;\n /** The end of the signature line: legal links, a language switch (`SiteFooter.Link`s or any inline content). */\n legal?: React.ReactNode;\n /**\n * `site` — the full footer of a marketing or company site. `docs` — a compact one for documentation pages (no site\n * block, the columns in a row, the documentation's width; never the family). `app` — one slim line inside a signed-in\n * product: the publisher line, the version and the legal links (no columns, no family); render it once, in the\n * layout, and not at all where the app shell has a sidebar that holds them.\n * @default \"site\"\n */\n variant?: \"site\" | \"docs\" | \"app\";\n};\n\n/** The footer of a Krizaka site, one band: its own columns and the family's, then one line — signature, version, Montréal, legal. */\nexport function SiteFooter({\n current,\n labels = familyLabels.en,\n children,\n version,\n env,\n legal,\n family: familyProp = true,\n variant = \"site\",\n className,\n ...props\n}: SiteFooterProps) {\n const id = React.useId();\n const s = siteFooter({ variant });\n const family = variant === \"site\" && familyProp;\n const slim = variant === \"app\";\n const site = siteById(current);\n const krizaka = siteById(\"krizaka\");\n const tag = [env, version && `v${version.replace(/^v/, \"\")}`].filter(Boolean).join(\" · \");\n const own = React.Children.toArray(children).length > 0;\n const lang = labels.lang ?? \"en\";\n const company = labels.company ?? familyLabels.en.company!;\n // Products · Open source (GitHub, every repository, closes it) · Krizaka (the company's pages): three columns, no\n // column that lists only Krizaka.\n const groups = sitesByGroup();\n const columns = [\n ...groups.filter((g) => g.group === \"products\"),\n { group: \"open-source\" as const, sites: groups.filter((g) => g.group === \"open-source\" || g.group === \"github\").flatMap((g) => g.sites) },\n ];\n return (\n <footer className={s.root({ className })} {...props}>\n <div className={s.inner()}>\n {!slim && (own || family) && (\n <div className={s.band()}>\n {own && <div className={s.own()}>{children}</div>}\n {family && (\n <nav aria-label={labels.family} className={s.family()}>\n {columns.map(({ group, sites }) => (\n <div key={group} className={s.column()}>\n <p id={`${id}-${group}`} className={s.title()}>\n {labels.groups[group]}\n </p>\n <ul aria-labelledby={`${id}-${group}`} className={s.list()}>\n {sites.map((item) => (\n <li key={item.id}>\n <a href={item.url} aria-current={item.id === current ? \"true\" : undefined} className={s.link()}>\n <span className={s.mark()}>\n <FamilyMark mark={item.mark} size={16} />\n </span>\n {item.name}\n {item.status !== \"released\" && <span className={s.tag()}>{labels.status[item.status]}</span>}\n {item.id === current && <span className=\"sr-only\">({labels.current})</span>}\n </a>\n </li>\n ))}\n </ul>\n </div>\n ))}\n <div className={s.column()}>\n <p id={`${id}-krizaka`} className={s.title()}>\n {labels.groups.krizaka}\n </p>\n <ul aria-labelledby={`${id}-krizaka`} className={s.list()}>\n {companyLinks.map((l) => (\n <li key={l.id}>\n <a href={l.url[lang]} className={s.link()}>\n {company[l.id]}\n </a>\n </li>\n ))}\n </ul>\n </div>\n </nav>\n )}\n </div>\n )}\n <div className={s.bottom()}>\n <a href={krizaka.url} className={s.signature()}>\n <KrizakaLogo size={18} animated={false} />\n {labels.signature[site.kind]}\n </a>\n {tag && (\n <span className={s.version()}>\n <span className=\"sr-only\">{labels.version} </span>\n {tag}\n </span>\n )}\n {!slim && <span>{labels.place}</span>}\n {legal && <div className={s.legal()}>{legal}</div>}\n </div>\n </div>\n </footer>\n );\n}\n\nexport type SiteFooterColumnProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** The column's heading (“Docs”) — passed translated. */\n title: string;\n /** Its links (`SiteFooter.Link`). */\n children: React.ReactNode;\n};\n\n/** One of the site's own columns: a heading and its links. */\nexport function SiteFooterColumn({ title, children, className, ...props }: SiteFooterColumnProps) {\n const id = React.useId();\n // A group with no link is not a column: only a heading over nothing.\n if (React.Children.toArray(children).length === 0) return null;\n const s = siteFooter();\n return (\n <div className={s.column({ className })} {...props}>\n <p id={id} className={s.title()}>\n {title}\n </p>\n <ul aria-labelledby={id} className={s.list()}>\n {React.Children.toArray(children).map((child, i) => <li key={i}>{child}</li>)}\n </ul>\n </div>\n );\n}\n\nexport type SiteFooterLinkProps = React.ComponentProps<\"a\"> & {\n /** The current page: `aria-current=\"page\"`. */\n active?: boolean;\n /** Renders your router's `<Link>`. */\n asChild?: boolean;\n};\n\n/** A link of the footer. */\nexport function SiteFooterLink({ active, asChild, className, ...props }: SiteFooterLinkProps) {\n const s = siteFooter();\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp aria-current={active ? \"page\" : undefined} className={s.link({ className })} {...props} />;\n}\n\nSiteFooter.Column = SiteFooterColumn;\nSiteFooter.Link = SiteFooterLink;\n"
|
|
335
|
+
"content": "// Client (the marks hold an id). The footer shared by every site of the Krizaka family — one calm band (Cahier, ADR-004;\n// peer review krizaka-ui#434: « too much, a double footer »): the site's own columns and the family's columns side by\n// side — Products · Open source (GitHub with them) · Krizaka (our story, the blog, contact) — then one line, with no\n// rule above it (a bottom bar under the columns read as a second footer, 2026-10-21): the signature (“A Krizaka product”, by the kind of the current site, to www), the version, the place, the legal links.\n// No site block (the bar already names the site), no column that lists only Krizaka, no link twice.\nimport { Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { FamilyMark } from \"../family-mark\";\nimport { KrizakaLogo } from \"../marks/KrizakaLogo\";\nimport { companyLinks, type FamilyLabels, familyLabels, siteById, type SiteId, sitesByGroup } from \"../sites/sites\";\n\nexport const siteFooter = tv({\n slots: {\n root: \"w-full border-t border-border-subtle bg-surface-0 text-fg\",\n inner:\n \"mx-auto grid w-full max-w-[calc(var(--kz-page-max,72rem)_+_2*var(--kz-page-gutter,clamp(16px,3.2vw,32px)))] grid-cols-[minmax(0,1fr)] gap-10 px-[var(--kz-page-gutter,clamp(16px,3.2vw,32px))] py-12\",\n band: \"grid grid-cols-[minmax(0,1fr)] gap-8 lg:grid-flow-col lg:auto-cols-[minmax(0,1fr)]\",\n own: \"grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-8\",\n family: \"grid grid-cols-2 gap-8 sm:grid-cols-3\",\n column: \"grid min-w-0 content-start gap-3\",\n title: \"text-[11px] font-semibold text-fg-secondary\",\n list: \"grid min-w-0 gap-2\",\n link:\n \"inline-flex min-h-6 min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 rounded-md text-sm [overflow-wrap:anywhere] text-fg-secondary no-underline transition-colors hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:text-fg aria-[current=page]:font-semibold aria-[current=true]:text-fg aria-[current=true]:font-semibold\",\n mark: \"flex h-4 w-4 shrink-0 items-center justify-center text-fg\",\n tag: \"rounded-sm bg-surface-3 px-1.5 py-px text-[10px] font-semibold text-fg-secondary\",\n bottom: \"flex flex-wrap items-center gap-x-5 gap-y-3 text-xs text-fg-secondary\",\n signature:\n \"inline-flex items-center gap-2 rounded-md font-medium text-fg no-underline hover:text-fg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n version:\n \"rounded-sm border border-border-subtle bg-surface-2 px-2 py-0.5 font-mono text-[11px] text-fg-secondary\",\n legal: \"flex flex-wrap items-center gap-x-4 gap-y-2 sm:ml-auto\",\n },\n variants: {\n // `site`: the full footer. `docs`: no site block, the columns in one row, on the documentation's width. `app`: one\n // slim line inside a signed-in product — signature, version, legal links.\n variant: {\n site: {},\n docs: {\n inner: \"max-w-[calc(var(--kz-page-max-docs,88rem)_+_2*var(--kz-page-gutter,clamp(16px,3.2vw,32px)))] gap-6 py-8\",\n own: \"sm:grid-cols-4\",\n },\n app: {\n root: \"bg-transparent\",\n inner: \"gap-0 py-3\",\n },\n },\n },\n defaultVariants: { variant: \"site\" },\n});\n\nconst bare = (p: string) => p.replace(/^\\/(en|fr)(?=\\/|$)/, \"\").replace(/\\/+$/, \"\") || \"/\";\n\n/** Is `href` (an absolute family address) the page `path` of the site at `siteUrl`? Locale prefix and trailing slash do not count. */\nfunction isThisPage(href: string, siteUrl: string, path: string): boolean {\n try {\n const target = new URL(href);\n return target.host === new URL(siteUrl).host && bare(target.pathname) === bare(path.split(/[?#]/)[0]!);\n } catch {\n return false;\n }\n}\n\n/** The link to the page you are on: no reload, back to the top (the browser would otherwise keep or restore the scroll). */\nfunction backToTop(event: React.MouseEvent<HTMLAnchorElement>) {\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return;\n event.preventDefault();\n window.scrollTo({ top: 0, behavior: \"auto\" });\n}\n\nexport type SiteFooterProps = Omit<React.ComponentProps<\"footer\">, \"children\"> & {\n /** The site this footer belongs to (`@krizaka/ui/sites`): its mark and line, its signature, marked in the family. */\n current: SiteId;\n /**\n * The family's words, passed translated: `familyLabels.fr` from `@krizaka/ui/sites`, or your own of the same shape.\n * @default familyLabels.en\n */\n labels?: FamilyLabels;\n /** The site's own columns (`SiteFooter.Column`), before the family. */\n children?: React.ReactNode;\n /** The version the site runs (`1.2.3`, shown `v1.2.3`). */\n version?: string;\n /** The environment, when it is not production (`DEV`). */\n env?: string;\n /**\n * The path of the page being read on this site (`/en/story`, `/blog/…`): the company link that points to this very\n * page says so (`aria-current=\"page\"`, a visible state) and a click on it scrolls to the top instead of reloading.\n */\n path?: string;\n /**\n * Shows the family in columns (the Krizaka sites: www, ui., java.). A product is independent once deployed: pass\n * `family={false}` and the footer holds only its own columns, the legal links, the version and one publisher line —\n * “A Krizaka product”, linking to www.krizaka.com.\n * @default true\n */\n family?: boolean;\n /** The end of the signature line: legal links, a language switch (`SiteFooter.Link`s or any inline content). */\n legal?: React.ReactNode;\n /**\n * `site` — the full footer of a marketing or company site. `docs` — a compact one for documentation pages (no site\n * block, the columns in a row, the documentation's width; never the family). `app` — one slim line inside a signed-in\n * product: the publisher line, the version and the legal links (no columns, no family); render it once, in the\n * layout, and not at all where the app shell has a sidebar that holds them.\n * @default \"site\"\n */\n variant?: \"site\" | \"docs\" | \"app\";\n};\n\n/** The footer of a Krizaka site, one band: its own columns and the family's, then one line — signature, version, Montréal, legal. */\nexport function SiteFooter({\n current,\n labels = familyLabels.en,\n children,\n version,\n env,\n path,\n legal,\n family: familyProp = true,\n variant = \"site\",\n className,\n ...props\n}: SiteFooterProps) {\n const id = React.useId();\n const s = siteFooter({ variant });\n const family = variant === \"site\" && familyProp;\n const slim = variant === \"app\";\n const site = siteById(current);\n const krizaka = siteById(\"krizaka\");\n const tag = [env, version && `v${version.replace(/^v/, \"\")}`].filter(Boolean).join(\" · \");\n const own = React.Children.toArray(children).length > 0;\n const lang = labels.lang ?? \"en\";\n const company = labels.company ?? familyLabels.en.company!;\n const here = (href: string) => path !== undefined && isThisPage(href, site.url, path);\n // Products · Open source (GitHub, every repository, closes it) · Krizaka (the company's pages): three columns, no\n // column that lists only Krizaka.\n const groups = sitesByGroup();\n const columns = [\n ...groups.filter((g) => g.group === \"products\"),\n { group: \"open-source\" as const, sites: groups.filter((g) => g.group === \"open-source\" || g.group === \"github\").flatMap((g) => g.sites) },\n ];\n return (\n <footer className={s.root({ className })} {...props}>\n <div className={s.inner()}>\n {!slim && (own || family) && (\n <div className={s.band()}>\n {own && <div className={s.own()}>{children}</div>}\n {family && (\n <nav aria-label={labels.family} className={s.family()}>\n {columns.map(({ group, sites }) => (\n <div key={group} className={s.column()}>\n <p id={`${id}-${group}`} className={s.title()}>\n {labels.groups[group]}\n </p>\n <ul aria-labelledby={`${id}-${group}`} className={s.list()}>\n {sites.map((item) => (\n <li key={item.id}>\n <a href={item.url} aria-current={item.id === current ? \"true\" : undefined} className={s.link()}>\n <span className={s.mark()}>\n <FamilyMark mark={item.mark} size={16} />\n </span>\n {item.name}\n {item.status !== \"released\" && <span className={s.tag()}>{labels.status[item.status]}</span>}\n {item.id === current && <span className=\"sr-only\">({labels.current})</span>}\n </a>\n </li>\n ))}\n </ul>\n </div>\n ))}\n <div className={s.column()}>\n <p id={`${id}-krizaka`} className={s.title()}>\n {labels.groups.krizaka}\n </p>\n <ul aria-labelledby={`${id}-krizaka`} className={s.list()}>\n {companyLinks.map((l) => (\n <li key={l.id}>\n <a\n href={l.url[lang]}\n aria-current={here(l.url[lang]) ? \"page\" : undefined}\n onClick={here(l.url[lang]) ? backToTop : undefined}\n className={s.link()}\n >\n {company[l.id]}\n </a>\n </li>\n ))}\n </ul>\n </div>\n </nav>\n )}\n </div>\n )}\n <div className={s.bottom()}>\n <a href={krizaka.url} className={s.signature()}>\n <KrizakaLogo size={18} animated={false} />\n {labels.signature[site.kind]}\n </a>\n {tag && (\n <span className={s.version()}>\n <span className=\"sr-only\">{labels.version} </span>\n {tag}\n </span>\n )}\n {!slim && <span>{labels.place}</span>}\n {legal && <div className={s.legal()}>{legal}</div>}\n </div>\n </div>\n </footer>\n );\n}\n\nexport type SiteFooterColumnProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** The column's heading (“Docs”) — passed translated. */\n title: string;\n /** Its links (`SiteFooter.Link`). */\n children: React.ReactNode;\n};\n\n/** One of the site's own columns: a heading and its links. */\nexport function SiteFooterColumn({ title, children, className, ...props }: SiteFooterColumnProps) {\n const id = React.useId();\n // A group with no link is not a column: only a heading over nothing.\n if (React.Children.toArray(children).length === 0) return null;\n const s = siteFooter();\n return (\n <div className={s.column({ className })} {...props}>\n <p id={id} className={s.title()}>\n {title}\n </p>\n <ul aria-labelledby={id} className={s.list()}>\n {React.Children.toArray(children).map((child, i) => <li key={i}>{child}</li>)}\n </ul>\n </div>\n );\n}\n\nexport type SiteFooterLinkProps = React.ComponentProps<\"a\"> & {\n /** The current page: `aria-current=\"page\"`. */\n active?: boolean;\n /** Renders your router's `<Link>`. */\n asChild?: boolean;\n};\n\n/** A link of the footer. */\nexport function SiteFooterLink({ active, asChild, className, ...props }: SiteFooterLinkProps) {\n const s = siteFooter();\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp aria-current={active ? \"page\" : undefined} className={s.link({ className })} {...props} />;\n}\n\nSiteFooter.Column = SiteFooterColumn;\nSiteFooter.Link = SiteFooterLink;\n"
|
|
329
336
|
}
|
|
330
337
|
],
|
|
331
338
|
"dependencies": [
|
|
332
|
-
"@krizaka/tokens@^3.
|
|
339
|
+
"@krizaka/tokens@^3.42.0",
|
|
333
340
|
"radix-ui@^1.7.0",
|
|
334
341
|
"tailwind-variants@^3.3.1"
|
|
335
342
|
],
|
|
@@ -387,6 +394,13 @@
|
|
|
387
394
|
"description": "The end of the signature line: legal links, a language switch (`SiteFooter.Link`s or any inline content).",
|
|
388
395
|
"required": false
|
|
389
396
|
},
|
|
397
|
+
{
|
|
398
|
+
"name": "path",
|
|
399
|
+
"type": "string",
|
|
400
|
+
"default": null,
|
|
401
|
+
"description": "The path of the page being read on this site (`/en/story`, `/blog/…`): the company link that points to this very\npage says so (`aria-current=\"page\"`, a visible state) and a click on it scrolls to the top instead of reloading.",
|
|
402
|
+
"required": false
|
|
403
|
+
},
|
|
390
404
|
{
|
|
391
405
|
"name": "variant",
|
|
392
406
|
"type": "\"site\" | \"docs\" | \"app\"",
|