Persona glyphDesign system
VisualSite

Persona glyph

Animated glyph tiles for site personas and static menu icons.

npx context-ds add persona-glyph
On the site

Usage

Use it for

Icons in the header's menu panels (MenuGlyph as a menu item's icon), persona cards and use-case links. Give each one in a list its index so they change out of step. 45 named pairs, from scrape, batches and monitor to theme, team and generate.

Avoid

  • Line icons in a blue tile where the site uses a glyph
  • Glyphs at body text size

Dependencies: dot-engine

Example

examples/persona-glyph.tsx
import { MenuGlyph, PersonaGlyph } from "@/components/visuals/persona-glyph";const NAMES = ["scrape", "batches", "monitor", "theme", "team", "generate"] as const;export function Site() {  return (    <div className="flex flex-col items-center gap-8">      <div className="flex flex-wrap justify-center gap-3">        {NAMES.map((name, i) => (          <PersonaGlyph key={name} name={name} index={i} />        ))}      </div>      <div className="flex gap-3">        {NAMES.map((name) => (          <MenuGlyph key={name} name={name} />        ))}      </div>    </div>  );}

Source

components/visuals/persona-glyph.tsx
"use client";import { useCallback, useMemo } from "react";import { DotStory, type LoadDotStory } from "@/components/visuals/dot-engine/dot-story";import { PERSONA_GLYPH_GRID, personaGlyph, type PersonaGlyphName } from "@/components/visuals/stories/persona-glyphs";import { cn } from "@/lib/utils";// Cards join the loop this far apart, so neighbors never melt at the same moment.const STAGGER_SECONDS = 0.85;/** * A small picture in white dots on the brand's blue: a glyph for what a team builds or a product does, giving way to * one for what the API gives it and back. The site uses it for menu icons, persona cards and use-case links; pass * each one's place in its list as `index` so a row of them never changes in step. `still` keeps the glyph. With * reduced motion it shows the glyph. */function PersonaGlyph({ name, index = 0, still = false, className }: { name: PersonaGlyphName; index?: number; still?: boolean; className?: string }) {  const resting = useMemo(() => ({ cells: personaGlyph(name) }), [name]);  const load = useCallback<LoadDotStory>(    async (stage) => {      const { createPersonaGlyphPlayer } = await import("@/components/visuals/persona-glyph-player");      return createPersonaGlyphPlayer(stage, name, index * STAGGER_SECONDS);    },    [name, index],  );  return (    <span data-slot="persona-glyph" className={cn("flex size-14 shrink-0 items-center justify-center rounded-card bg-brand p-2", className)}>      <DotStory grid={PERSONA_GLYPH_GRID} resting={resting} load={load} still={still} className="w-full text-white" />    </span>  );}/** * The header menus' icon: a persona glyph in a 36px blue tile, as the site's Products and Use cases menus draw them. * Held still: an icon in a menu should not move while the pointer is over it. */function MenuGlyph({ name, className }: { name: PersonaGlyphName; className?: string }) {  return <PersonaGlyph name={name} still className={cn("size-9 rounded-control p-2.5", className)} />;}export { MenuGlyph, PersonaGlyph, type PersonaGlyphName };
components/visuals/persona-glyph-player.ts
import { createBeatStoryPlayer, type StoryPicture } from "@/components/visuals/dot-engine/dot-story-beats";import { cellStates, cellsOfStates } from "@/components/visuals/dot-engine/dot-story-cells";import type { DotStoryPlayer, DotStoryStage } from "@/components/visuals/dot-engine/dot-story-player";import type { GlyphCell } from "@/components/visuals/dot-engine/dot-glyph-cells";import { PERSONA_GLYPH_GRID, personaGlyph, personaOutcome, type PersonaGlyphName } from "@/components/visuals/stories/persona-glyphs";/** A persona's glyph, played as a loop: what the team builds gives way to what the API gives it, and back. */// A glyph this small is mostly one-dot lines, which a melt thins to nothing halfway through; a wave from the// middle swaps one glyph for the other without ever emptying the card.const WAVE = { seconds: 0.8, origin: { column: 5, row: 5 } };const OUTCOME_HOLD_SECONDS = 2.2;const GLYPH_HOLD_SECONDS = 2.8;const pictureOfCells = (cells: GlyphCell[]): StoryPicture => {  const states = cellStates(PERSONA_GLYPH_GRID, cells);  return { states, cells: cellsOfStates(PERSONA_GLYPH_GRID, states) };};/** `startsAtSeconds` into the glyph's hold is where this card joins the loop, so a grid of cards does not melt in step. */export function createPersonaGlyphPlayer(stage: DotStoryStage, name: PersonaGlyphName, startsAtSeconds: number): DotStoryPlayer {  return createBeatStoryPlayer(stage, {    grid: PERSONA_GLYPH_GRID,    resting: { beat: 1, heldSeconds: startsAtSeconds % GLYPH_HOLD_SECONDS },    beats: [      { picture: pictureOfCells(personaOutcome(name)), arrival: { kind: "waves", ...WAVE }, holdSeconds: OUTCOME_HOLD_SECONDS },      { picture: pictureOfCells(personaGlyph(name)), arrival: { kind: "waves", ...WAVE }, holdSeconds: GLYPH_HOLD_SECONDS },    ],  });}
components/visuals/stories/persona-glyphs.ts
import { glyphCellsFromRows, type GlyphCell } from '@/components/visuals/dot-engine/dot-glyph-cells';import type { DotGrid } from '@/components/visuals/dot-engine/dot-morph-dots';/** * The small pictures on the persona cards, in the brand dot grid: for each kind of team, a glyph for what it builds * and a second for what the API gives it, which the first gives way to and returns from. */export const PERSONA_GLYPH_GRID: DotGrid = { columns: 11, rows: 11, pitch: 10, dotRadius: 3.7, ringStroke: 1.3, haloRadius: 0 };// Every drawing, one string per row: `#` a solid dot, `o` a ring.const ART = {	cylinder: ['  #######  ', ' #       # ', ' ######### ', ' #       # ', ' #       # ', ' ######### ', ' #       # ', ' #       # ', ' ######### ', ' #       # ', '  #######  '],	chunks: ['### ### ###', '### ### ###', '### ### ###', '           ', '### ooo ###', '### ooo ###', '### ooo ###', '           ', '### ### ###', '### ### ###', '### ### ###'],	robot: ['     #     ', '     #     ', ' ######### ', ' #       # ', ' # ## ## # ', ' # ## ## # ', ' #       # ', ' #  ###  # ', ' #       # ', ' ######### ', '  ##   ##  '],	browser: ['###########', '#o o o    #', '###########', '#         #', '# ####### #', '#         #', '# ###     #', '#         #', '# #####   #', '#         #', '###########'],	magnifier: ['  #####    ', ' #ooooo#   ', '#ooooooo#  ', '#ooooooo#  ', '#ooooooo#  ', '#ooooooo#  ', ' #ooooo#   ', '  ######   ', '       ##  ', '        ## ', '         ##'],	bubble: [' ######### ', '#         #', '# ####### #', '#         #', '# #####   #', '#         #', ' ######### ', '   ##      ', '  ##       ', '  #        ', '           '],	shield: ['     #     ', '  #######  ', ' #ooooooo# ', ' #ooooooo# ', ' #ooooooo# ', ' #ooooooo# ', ' #ooooooo# ', '  #ooooo#  ', '   #ooo#   ', '    ###    ', '     #     '],	eye: ['           ', '           ', '   #####   ', ' ##     ## ', '#   ###   #', '#  #####  #', '#   ###   #', ' ##     ## ', '   #####   ', '           ', '           '],	book: ['           ', ' #### #### ', '#    #    #', '# ## # ## #', '#    #    #', '# ## # ## #', '#    #    #', '# ## # ## #', '#    #    #', ' #### #### ', '           '],	layers: ['     #     ', '   ## ##   ', ' ##     ## ', '   ## ##   ', ' #   #   # ', '  ##   ##  ', '    ###    ', ' #       # ', '  ##   ##  ', '    ###    ', '           '],	file: [' #######   ', ' #     ##  ', ' #      ## ', ' # ###   # ', ' #       # ', ' # ##### # ', ' #       # ', ' # ###   # ', ' #       # ', ' #       # ', ' ######### '],	checklist: ['##  #######', '##         ', '           ', '##  #####  ', '##         ', '           ', '##  #######', '##         ', '           ', '##  ####   ', '##         '],	bolt: ['      ##   ', '     ##    ', '    ##     ', '   ###     ', '  ######## ', '     ###   ', '     ##    ', '    ##     ', '   ##      ', '  ##       ', '  #        '],	pencil: ['        ## ', '       ####', '      ## ##', '     ## ## ', '    ## ##  ', '   ## ##   ', '  ## ##    ', ' # ###     ', ' #  #      ', ' ###       ', '           '],	globe: ['   #####   ', '  # # # #  ', ' #  # #  # ', '###########', '#  #   #  #', '#  #   #  #', '#  #   #  #', '###########', ' #  # #  # ', '  # # # #  ', '   #####   '],	image: ['###########', '#         #', '#      ## #', '#      ## #', '#  #      #', '# ###   # #', '#####  ####', '###########', '###########', '###########', '###########'],	briefcase: ['           ', '   #####   ', '   #   #   ', '###########', '#ooooooooo#', '#ooooooooo#', '#####o#####', '#         #', '#         #', '###########', '           '],	contactCard: ['###########', '#         #', '#  ##     #', '# #### ## #', '#  ##     #', '# #### ## #', '# ####    #', '#      ## #', '#         #', '###########', '           '],	tower: ['  #######  ', '  #     #  ', '  # # # #  ', '  #     #  ', '  # # # #  ', '  #     #  ', '  # # # #  ', '  #     #  ', '  # ### #  ', '  # # # #  ', '###########'],	tiles: ['##### #####', '#   # #   #', '# # # # # #', '#   # #   #', '##### #####', '           ', '##### #####', '#   # #   #', '# # # # # #', '#   # #   #', '##### #####'],	bars: ['         ##', '         ##', '      ## ##', '      ## ##', '      ## ##', '   ## ## ##', '   ## ## ##', '## ## ## ##', '## ## ## ##', '## ## ## ##', '###########'],	trend: ['       ####', '         ##', '        # #', '       #  #', '   #  #    ', '  # ##     ', ' #         ', '#          ', '           ', '###########', '           '],	chain: ['           ', '  ####     ', ' #    #    ', '#   ####   ', '#  #  # #  ', '#  #  #  # ', ' # #  #  # ', '  ####   # ', '   #    #  ', '    ####   ', '           '],	logo: ['    #######', '    #######', '    #######', '  ###ooo###', ' ####ooo###', '#####ooo###', '#######    ', '#######    ', ' #####     ', '  ###      ', '           '],	storefront: [' ######### ', ' #ooooooo# ', '###########', '# # # # # #', ' ######### ', ' #       # ', ' # ##  # # ', ' # ##  # # ', ' # ##    # ', ' # ##    # ', ' ######### '],	tag: ['           ', '           ', '  ######## ', ' ##      # ', '##  ##   # ', '#   ##   # ', '##       # ', ' ##      # ', '  ######## ', '           ', '           '],	scroll: [' ########  ', '#        # ', ' # ##### # ', ' #       # ', ' # ##### # ', ' #       # ', ' # ###   # ', ' #       # ', ' #       ##', ' #        #', '  ######## '],	tick: ['           ', '          #', '         ##', '        ## ', '       ##  ', '#     ##   ', '##   ##    ', ' ## ##     ', '  ###      ', '   #       ', '           '],	signedShield: ['     #     ', '  #######  ', ' ######### ', ' #######o# ', ' ######o## ', ' #o###o### ', ' ##o#o#### ', '  ##o####  ', '   #####   ', '    ###    ', '     #     '],	key: ['           ', '           ', '  ###      ', ' #   #     ', '#     #####', '#     #  # ', ' #   #     ', '  ###      ', '           ', '           ', '           '],	clock: ['   #####   ', '  #     #  ', ' #   #   # ', '#    #    #', '#    #    #', '#    ###  #', '#         #', '#         #', ' #       # ', '  #     #  ', '   #####   '],	log: ['           ', '# #########', '           ', '# ######   ', '           ', '# #########', '           ', '# #####    ', '           ', '# ######## ', '           '],	wand: ['        # #', '         # ', '        # #', '       #   ', '      #    ', '     #     ', '    #      ', '   #       ', '  #        ', ' #         ', '#          '],	bell: ['     #     ', '    ###    ', '   #####   ', '  #######  ', '  #######  ', '  #######  ', '  #######  ', ' ######### ', '###########', '           ', '    ###    '],	office: ['   #####   ', '   #   #   ', '   # # #   ', '   #   ####', '   # # #  #', '   #   # ##', '   # # #  #', '   #   # ##', '   # # #  #', '   #   #  #', '###########'],	bust: ['   #####   ', '  #     #  ', '  #     #  ', '  #     #  ', '   #####   ', '           ', '  #######  ', ' #       # ', '#         #', '#         #', '###########'],	rocket: ['     #     ', '    ###    ', '   #####   ', '   ## ##   ', '   #####   ', '   #####   ', '  #######  ', ' ## ### ## ', ' #  ###  # ', '    # #    ', '     #     '],	form: ['###########', '#         #', '# ####### #', '#         #', '# #####   #', '#         #', '# ####### #', '#         #', '#     ### #', '#     ### #', '###########'],	dashboard: ['###########', '# #       #', '# # ##### #', '# #       #', '# # ## ## #', '# # ## ## #', '# #       #', '# # ##### #', '# #       #', '# #       #', '###########'],	swatches: ['##### ooooo', '##### ooooo', '##### ooooo', '##### ooooo', '##### ooooo', '           ', 'ooooo #####', 'ooooo # # #', 'ooooo ## ##', 'ooooo # # #', 'ooooo #####'],	people: ['  ###   ## ', ' #####  ## ', ' ##### ####', '  ###  ####', '        ## ', ' #####     ', '####### ###', '###########', '###########', '###########', '           '],	mail: ['           ', '###########', '##       ##', '# #     # #', '#  #   #  #', '#   # #   #', '#    #    #', '#         #', '#         #', '###########', '           '],	flow: ['###        ', '###        ', '###        ', ' #         ', ' #     ### ', ' ######### ', ' #     ### ', ' #         ', '###        ', '###        ', '###        '],	megaphone: ['         ##', '       ####', '     ######', '###########', '###########', '###########', '     ######', '  ##   ####', '  ##     ##', '  ##       ', '           '],	pen: ['     #     ', '    ###    ', '    ###    ', '   ## ##   ', '   ## ##   ', '  ##   ##  ', '  ## # ##  ', '  #######  ', '   #####   ', '   #####   ', '    ###    '],	slide: ['###########', '#         #', '# ###     #', '#      ## #', '# ##   ## #', '#    # ## #', '###########', '     #     ', '    ###    ', '   #   #   ', '  #     #  '],	brandedDoc: [' ######### ', ' #       # ', ' # ##    # ', ' # ##    # ', ' #       # ', ' # ##### # ', ' #       # ', ' # ##### # ', ' #       # ', ' # ##    # ', ' ######### '],	palette: ['   #####   ', '  #######  ', ' ##o###o## ', '####### ###', '#o#####    ', '######     ', '#o#####    ', '########## ', ' ##o###o## ', '  #######  ', '   #####   '],	pie: ['   #####   ', '  ###   #  ', ' ####    # ', '#####     #', '#####     #', '###########', '#         #', '#         #', ' #       # ', '  #     #  ', '   #####   '],	share: ['        ###', '        ###', '      ##   ', '    ##     ', '###        ', '###        ', '    ##     ', '      ##   ', '        ###', '        ###', '           '],	spark: ['     #     ', '     #     ', '    ###    ', '    ###    ', '  #######  ', '###########', '  #######  ', '    ###    ', '    ###    ', '     #     ', '     #     '],	textLines: ['###########', '           ', '########   ', '           ', '###########', '           ', '######     ', '           ', '###########', '           ', '#######    '],} satisfies Record<string, string[]>;// Each card's pair: what the team builds or the feature is, then what the API gives it. The last five of the first// group belong to feature cards rather than teams, and the last five of all to the products in the header's menu.const PAIRS = {	database: ['cylinder', 'chunks'],	bot: ['robot', 'browser'],	search: ['magnifier', 'bubble'],	shield: ['shield', 'eye'],	book: ['book', 'layers'],	file: ['file', 'checklist'],	bolt: ['bolt', 'pencil'],	globe: ['globe', 'image'],	briefcase: ['briefcase', 'contactCard'],	building: ['tower', 'tiles'],	chart: ['bars', 'trend'],	link: ['chain', 'logo'],	store: ['storefront', 'tag'],	scroll: ['scroll', 'tick'],	signed: ['signedShield', 'key'],	history: ['clock', 'log'],	wand: ['wand', 'bell'],	company: ['office', 'logo'],	person: ['bust', 'contactCard'],	rocket: ['rocket', 'form'],	theme: ['dashboard', 'swatches'],	portal: ['browser', 'logo'],	team: ['people', 'mail'],	tenants: ['layers', 'swatches'],	agency: ['people', 'swatches'],	whitelabel: ['flow', 'tag'],	email: ['mail', 'swatches'],	design: ['pen', 'slide'],	campaign: ['megaphone', 'mail'],	document: ['file', 'brandedDoc'],	creative: ['palette', 'layers'],	report: ['pie', 'bars'],	social: ['share', 'image'],	generate: ['spark', 'textLines'],	imagegen: ['image', 'swatches'],	writing: ['pencil', 'textLines'],	slides: ['slide', 'swatches'],	chat: ['robot', 'bubble'],	outreach: ['bubble', 'mail'],	scrape: ['browser', 'textLines'],	map: ['globe', 'log'],	batches: ['layers', 'checklist'],	monitor: ['clock', 'bell'],	styleguide: ['palette', 'swatches'],} satisfies Record<string, [keyof typeof ART, keyof typeof ART]>;export type PersonaGlyphName = keyof typeof PAIRS;export const personaGlyph = (name: PersonaGlyphName): GlyphCell[] => glyphCellsFromRows(ART[PAIRS[name][0]]);export const personaOutcome = (name: PersonaGlyphName): GlyphCell[] => glyphCellsFromRows(ART[PAIRS[name][1]]);

Reference: site src/components/sections/usecases/persona-glyph.tsx.