Sakuga Code coming soon

Sakuga 2.0 · Components · MIT licensed

Every component, live.

Each one renders live from Sakuga’s real bundle, with real AniCanon™ art, above its guidelines, the props it takes and the tokens it reads, as the design system writes them.

106components, each one live
52in the core, for anything built around art
54in AniCanon’s Kit, built from the core
Theme

Sakuga, the core@sakuga/ui

For anything built around art: actions, forms, navigation, content, feedback, media, containers, type and icons. A component belongs here when it would make sense in a product that has never heard of AniCanon.

AniCanonKit@sakuga/anicanonkit

AniCanon's own: its currency (Fragments), its assistant Maki, its characters and wardrobe, its story graph, its productions, its manga pages and its writing room. It is built only from core components and core tokens, and it never restyles a core component. AniCanon is at anicanon.studio.

How a component is placed, and how one is added

Core52

Actions

Forms

Navigation

Content

Feedback

Media

Containers

Foundations

AniCanonKit54

AniCanon

Characters

Story

Maki

Productions

Manga

Writing

Jump to a component

Guide

Core or AniCanonKit

Where a component lives, and how a new one earns its place.

Sakuga is one design system with two libraries, and every component belongs to exactly one.

Sakuga, the core (@sakuga/ui) is for anything built around art: actions, forms, navigation, content, feedback, media, containers, type and icons. A component belongs here when it would make sense in a product that has never heard of AniCanon.

AniCanonKit (@sakuga/anicanonkit) is AniCanon's own: its currency (Fragments), its assistant Maki, its characters and wardrobe, its story graph, its productions, its manga pages and its writing room. It is built only from core components and core tokens, and it never restyles a core component. AniCanon is at anicanon.studio.

To place a component, ask in this order:

  1. Is it an existing component with other props? Then it is that component: add the prop. Two things are one component when one is the other with props, with the same states and nothing lost. Looking alike is not enough.
  2. Does it only work with AniCanon's own data or rules? Fragments, Maki, a character's stats or looks, a faction, a story event, a cut, a production, a manga page. Then it belongs in AniCanonKit.
  3. Otherwise it belongs in the core. Words like episode, cast or character don't make a component AniCanon's: any product about anime has them.

The core reaches into the Kit in one place: Button cost draws AniCanonKit.FragmentSoul, because a fragment amount always carries the mark. Nothing else in the core uses the Kit.

Adding a component

  1. Pass the merge test. If it is an existing component with other props, extend that one instead.
  2. Earn its place. A new component has a real job that repeats on more than one screen. A one-off stays layout and Text.
  3. Place it in the core or AniCanonKit, with the three questions above.
  4. Name it for what it is, not for the screen it first appeared on.
  5. Give it its own tokens. It reads only its own component tokens (<prefix>-*), which alias semantic tokens. It never reads a semantic or core value directly.
  6. Write its README. The first sentence says what it is, then come its guidelines. It ends with a When to use section: what it is for, a line for every component it could be mistaken for that names the one to use instead, the platforms it is used on and its version.
  7. Ship the rest of it: a preview.html whose @dsCard label names its library (Sakuga · Group or AniCanonKit · Group), its interface in index.d.ts, and its line in this README's component list.

Under its ## When to use heading, the section reads like this, and the tools read it in exactly this shape:

Use it for an action with a word on it: the one main action of a view.

- Not for an action inside a sentence or a row of text: use `TextLink`.
- Not for an action that one glyph says better than a word: use `IconButton`.

Used on: Web, iOS, Android
Version: 1.0

The component browser on sakuga.design and its llms.txt are built from these files, so a component added this way appears there with nothing else to edit.

Core · Actions

Button

Button runs an action with a text label: Play, Follow, Remind me, Done.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
An action with a word on it: the one main action of a view, the action beside it, and the confirmation in a dialog that deletes something.
Not for
  • An action inside a sentence or a row of textTextLink
  • An action that one glyph says better than a wordIconButton
  • Narrowing what a feed showsChip
  • Switching between views of the same thingSegmentedControl
  • A choice whose options each need a picture or a line of explanationOptionTiles

Guidelines

primary
The one main action per view (Play, Follow creator, Done). Never two primaries side by side.
secondary
The supporting action beside the primary (Follow, More info).
outline
A standalone action that should not compete with the primary (Remind me).
ghost
Quiet actions in rows and headers (See all).
destructive
The one action that confirms a deletion or takes access away (Delete my account, Disconnect Claude), filled in vermilion (danger). It lives in the dialog that asks first, and it is that dialog's main action, in place of a primary. The way into it is a TextLink variant="destructive", never a red button on the page. Removals you can simply undo (Remove a tag, Unfollow) stay ghost.
  • size="lg" for hero actions, md by default, sm for desktop-only secondary actions. Every size except sm meets the 44px target.
  • shape="pill" on phones; the default rounded corner (radius-md) on desktop.
  • Labels say what happens, in sentence case. Add aria-label when the visible label needs its object ("Follow" → "Follow EBA").
  • Pass href to render a link that looks like a button.
  • An action that spends fragments passes cost ("~100"): the price sits after the label behind a hairline, with the fragment mark. Keep the label a plain verb ("Generate", "Start", "Retake"); never write the price into the label.
  • media and media-secondary: the same two roles drawn over video or key art (hero Play and More info, Skip intro, shorts CTA). They never change with the theme.
  • Pressing sinks it to 97% on motion-instant; it settles back on motion-fast.
  • A disabled primary turns into a quiet fill (button-disabled-bg), not a faded white. Say why it's disabled in a visible line near it, never only in a tooltip.

Props

Prop
Type
What it does
variant
'primary' | 'secondary' | 'outline' | 'ghost' | 'media' | 'media-secondary' | 'destructive'
destructive: the one action that confirms a deletion or takes access away, in the dialog that asks for it.
size
'sm' | 'md' | 'lg'
shape
'rounded' | 'pill'
pill on phones; rounded (radius-md) on desktop.
icon
IconName
iconEnd
IconName
fullWidth
boolean
href
string
disabled
boolean
cost
string
What the action is expected to cost in fragments, e.g. "~100": shown after the label with the fragment mark.
onClick
MouseEventHandler
children
ReactNode
className
string
'aria-label'
string
TypeScript
export interface ButtonProps {
  /** destructive: the one action that confirms a deletion or takes access away, in the dialog that asks for it. */
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'media' | 'media-secondary' | 'destructive';
  size?: 'sm' | 'md' | 'lg';
  /** pill on phones; rounded (radius-md) on desktop. */
  shape?: 'rounded' | 'pill';
  icon?: IconName;
  iconEnd?: IconName;
  fullWidth?: boolean;
  href?: string;
  disabled?: boolean;
  /** What the action is expected to cost in fragments, e.g. "~100": shown after the label with the fragment mark. */
  cost?: string;
  onClick?: MouseEventHandler;
  children?: ReactNode;
  className?: string;
  'aria-label'?: string;
}
TokenLightDarkUse
button-primary-bgaction-primaryaction-primaryButton primary: fill.
button-primary-fgtext-inversetext-inverseButton primary: label and icon.
button-secondary-bgaction-secondaryaction-secondaryButton secondary: fill.
button-secondary-fgtext-primarytext-primaryButton secondary: label and icon.
button-outline-borderborder-controlborder-controlButton outline: edge.
button-outline-fgtext-primarytext-primaryButton outline: label and icon.
button-ghost-fgtext-secondarytext-secondaryButton ghost: label and icon.
button-ghost-hover-bgbg-fill-subtlebg-fill-subtleButton ghost: hover fill.
button-disabled-bgbg-fill-subtlebg-fill-subtleButton primary or media while it can't be used yet: a quiet fill, never a faded white.
button-disabled-fgtext-mutedtext-mutedButton label while disabled.
button-media-bgaction-mediaaction-mediaButton media: fill over video or artwork.
button-media-fgtext-on-action-mediatext-on-action-mediaButton media: label and icon.
button-media-secondary-bgaction-media-secondaryaction-media-secondaryButton media-secondary: glass fill over video or artwork.
button-media-secondary-fgtext-on-mediatext-on-mediaButton media-secondary: label and icon.
button-media-secondary-borderborder-glass-mediaborder-glass-mediaButton media-secondary: edge.
button-destructive-bgdangerdangerButton destructive: the fill of the action that confirms a deletion or takes access away.
button-destructive-fgon-dangeron-dangerButton destructive: its label and icon.

Core · Actions

IconButton

IconButton is a circular control holding one icon, with a required accessible label.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
An action that one glyph says better than a word: back, share, like, captions, the bell.
Not for
  • An action whose meaning needs a wordButton
  • A glyph that only decoratesIcon
  • Asking MakiAskMaki

Guidelines

  • solid on desktop surfaces; plain inside a bar that already has a fill (player controls).
  • glass for phone chrome floating over the page (back, share); media over video or manga pages, which stays dark in both themes.
  • pressed turns it into a toggle (aria-pressed); the pressed glyph takes icon-button-pressed-fg and settles in once when turned on. Use it for Like and Captions.
  • Pressing sinks it to 90% on motion-instant; it settles back on motion-fast.
  • 44px by default, size="lg" 52px for the desktop action rail.
  • The label names the object: "Share EBA", "Next page", "Remind me about episode 2".
  • With href it renders a link, for an icon that goes somewhere (Back, Open in Cut); without it, a button.
  • primary: the one action as an icon alone, filled (a phone's floating New). One per screen.
  • attention: something waits behind it, such as unread notifications on the bell. The state dot, the same one a current row and a waiting line use, sits on the glyph's upper corner. The label says how many: "Notifications, 3 new".

Props

Prop
Type
What it does
icon
IconName
Leave out when passing custom children (the CC box).
labelrequired
string
Required accessible name, e.g. "Share EBA".
variant
'solid' | 'glass' | 'media' | 'plain' | 'primary'
solid on desktop, glass for phone chrome over content, media over video or pages, plain inside a bar.
size
'md' | 'lg'
iconSize
number
pressed
boolean
Makes it a toggle (aria-pressed).
attention
boolean
Something waits behind it, e.g. unread notifications on the bell: the state dot on the glyph's corner. Say how many in the label.
disabled
boolean
onClick
MouseEventHandler
className
string
href
string
Renders a link instead of a button, for an icon that goes somewhere (Back, Open in Cut).
TypeScript
export interface IconButtonProps {
  /** Leave out when passing custom children (the CC box). */
  icon?: IconName;
  /** Required accessible name, e.g. "Share EBA". */
  label: string;
  /** solid on desktop, glass for phone chrome over content, media over video or pages, plain inside a bar. */
  variant?: 'solid' | 'glass' | 'media' | 'plain' | 'primary';
  size?: 'md' | 'lg';
  iconSize?: number;
  /** Makes it a toggle (aria-pressed). */
  pressed?: boolean;
  /** Something waits behind it, e.g. unread notifications on the bell: the state dot on the glyph's corner. Say how many in the label. */
  attention?: boolean;
  disabled?: boolean;
  onClick?: MouseEventHandler;
  className?: string;
  /** Renders a link instead of a button, for an icon that goes somewhere (Back, Open in Cut). */
  href?: string;
}
TokenLightDarkUse
icon-button-solid-bgbg-surface-raisedbg-surface-raisedIconButton solid: fill (desktop).
icon-button-solid-hover-bgbg-surface-hoverbg-surface-hoverIconButton solid: hover fill.
icon-button-fgtext-primarytext-primaryIconButton: glyph colour for solid, glass and plain.
icon-button-glass-bgbg-glassbg-glassIconButton glass: fill (phone chrome).
icon-button-glass-borderborder-glassborder-glassIconButton glass: edge.
icon-button-media-bgbg-glass-mediabg-glass-mediaIconButton media: fill over video or pages.
icon-button-media-borderborder-glass-mediaborder-glass-mediaIconButton media: edge.
icon-button-media-fgtext-on-mediatext-on-mediaIconButton media: glyph.
icon-button-media-focustext-on-mediatext-on-mediaIconButton media: focus ring over video, both themes.
icon-button-pressed-fgaccentaccentIconButton: glyph when toggled on (a liked heart).
ib-primary-bgaction-primaryaction-primaryIconButton: the primary action as an icon alone, filled (a phone's floating action).

Core · Actions

Chip

Chip filters a feed by kind: All, Episodes, Shorts, Manga, Following.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
Narrowing what a feed or a list shows, one kind at a time; with variant="soft", picking several words from a set, such as traits.
Not for

Guidelines

  • Use a row of chips inside Chip.Group (named by label); exactly one is selected.
  • Chips filter what is shown. They never navigate and never run an action; that is Button.
  • 44px tall on every platform so they are easy to hit.
  • A chip sinks on press and its fill changes on motion-fast.
  • On phones pass scroll to Chip.Group: one row that scrolls sideways instead of wrapping.

Icon and soft

icon adds a leading glyph in the accent. variant="soft" is for multi-select sets such as traits: selected chips take chip-soft-selected-bg with an accent edge instead of the solid fill.

Props

ChipGroupProps

Prop
Type
What it does
labelrequired
string
scroll
boolean
One row that scrolls sideways (phones).
childrenrequired
ReactNode
className
string

ChipProps

Prop
Type
What it does
selected
boolean
onClick
MouseEventHandler
childrenrequired
ReactNode
className
string
TypeScript
export interface ChipGroupProps {
  label: string;
  /** One row that scrolls sideways (phones). */
  scroll?: boolean;
  children: ReactNode;
  className?: string;
}

export interface ChipProps {
  selected?: boolean;
  onClick?: MouseEventHandler;
  children: ReactNode;
  className?: string;
}
TokenLightDarkUse
chip-bgbg-fill-subtlebg-fill-subtleChip: idle fill.
chip-fgtext-primarytext-primaryChip: idle name.
chip-active-bgaction-primaryaction-primaryChip: selected fill.
chip-active-fgtext-inversetext-inverseChip: selected name.
chip-iconaccentaccentChip: leading icon.
chip-soft-selected-bgaccent-subtleaccent-subtleChip, soft: selected fill (multi-select).
chip-soft-selected-borderaccentaccentChip, soft: selected edge.

Core · Actions

SegmentedControl

SegmentedControl switches between two to four views of the same thing: series sections, Following / For you, reading direction.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
Switching between two to four views of the same thing (series sections, Following and For you), or a setting with two to four named values.
Not for
  • Five options or moreSelect
  • One setting that is on or offSwitch
  • Narrowing a feed by kindChip
  • Going to another part of the productTabBar

Guidelines

  • 2–4 short options. More than four is a list, not a segment.
  • fill on the page; glass when it floats over artwork or video (the shorts feed).
  • The selected segment uses segmented-item-active-bg, drawn as one indicator that slides to the new segment on motion-base; each segment is a button with aria-pressed, inside a group named by label.

Props

Prop
Type
What it does
optionsrequired
{ label: string; value?: string }[]
valuerequired
string
onChange
(value: string) => void
labelrequired
string
variant
'fill' | 'glass'
fullWidth
boolean
className
string
TypeScript
export interface SegmentedControlProps {
  options: { label: string; value?: string }[];
  value: string;
  onChange?: (value: string) => void;
  label: string;
  variant?: 'fill' | 'glass';
  fullWidth?: boolean;
  className?: string;
}
TokenLightDarkUse
segmented-track-bgbg-fill-subtlebg-fill-subtleSegmentedControl: track.
segmented-item-fgtext-primarytext-primarySegmentedControl: idle segments.
segmented-item-active-bgaction-primaryaction-primarySegmentedControl: the selected segment.
segmented-item-active-fgtext-inversetext-inverseSegmentedControl: the selected segment name.
segmented-glass-bgbg-glass-mediabg-glass-mediaSegmentedControl glass: track over artwork or video, in both themes.
segmented-glass-borderborder-glass-mediaborder-glass-mediaSegmentedControl glass: track edge.
segmented-glass-item-fgtext-on-mediatext-on-mediaSegmentedControl glass: idle segments over artwork.
segmented-glass-active-bgaction-mediaaction-mediaSegmentedControl glass: selected segment over artwork.
segmented-glass-active-fgtext-on-action-mediatext-on-action-mediaSegmentedControl glass: selected segment name over artwork.

Core · Actions

OptionTiles

OptionTiles is the Studio's picker: a grid of tiles, each an AniCanon icon over a short name, for shot type, angle, depth, movement, mood, lighting, weather, effects and frame format.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
A choice that is clearer seen than read: Studio's direction pickers (shot, angle, lighting) as tiles, and with size="lg" the first choice of a flow, a pack or a plan as cards.
Not for

Guidelines

  • Every option keeps its own drawn icon (shot-closeup, lighting-neon, movement-dolly-in…). Auto uses auto.
  • One choice by default; multiple for effects. Tapping the chosen tile again clears it back to Auto.
  • With one choice, the selection ring slides to the new tile on motion-base; a tile sinks on press.
  • Put a help on every field: a bold first line saying what it controls, then examples. It shows through FieldHelp.
  • Pass note with the chosen option's one-line description, so the creator learns what it does.
  • 4 columns for long lists, 3 for short ones.

Large: choice cards

size="lg" turns each option into a card with icon, label, description, footnote and an optional badge. An option that sets a price passes cost ("~10 a second"), shown under the description with the fragment mark. Use it for the first choice of a flow (how to make something), three to four cards, with the chosen option's form directly below on the same screen.

An option with disabled (a plan that is not open yet, with a "Coming soon" badge) dims and cannot be chosen.

Fragments packs and plans pass image (the pack's or plan's own art: Spark, Bloom and Cascade grow from one flame to an orbit; Creator and Studio have their own) and amount (the fragments it gives, "24,990"), which is the card's headline with the mark. cost stays what an option charges, never what it gives. The art stands on its own stage above the name, and the chosen card's art comes forward a little. With columns={1} (a phone's sheet) the art sits beside the words. Put the price in description ("$24.99 · never expires") and on the action ("Buy Bloom · $24.99").

Props

TileOption

Prop
Type
What it does
valuerequired
string
labelrequired
string
icon
IconName
description
string
image
string
Large tiles: the thing's own art on a stage above its name (a Fragments pack or plan).
amount
string
Large tiles: the fragments it gives, set as the headline with the mark, e.g. "24,990".
cost
string
Large tiles: a fragment price under the description, e.g. "~10 a second".
disabled
boolean
Shown dimmed and cannot be chosen (a plan not open yet).
badge
string
footnote
string

OptionTilesProps · The Studio's icon tile picker: one or many options, each with an AniCanon icon.

Prop
Type
What it does
label
string
id
string
optionsrequired
TileOption[]
valuerequired
string | string[]
multiple
boolean
size
'lg'
lg: choice cards with description, footnote and badge.
columns
number
iconSize
number
note
string
help
{ title: string; text: ReactNode }
onChange
(value: string) => void
className
string
TypeScript
export interface TileOption {
  value: string;
  label: string;
  icon?: IconName;
  description?: string;
  /** Large tiles: the thing's own art on a stage above its name (a Fragments pack or plan). */
  image?: string;
  /** Large tiles: the fragments it gives, set as the headline with the mark, e.g. "24,990". */
  amount?: string;
  /** Large tiles: a fragment price under the description, e.g. "~10 a second". */
  cost?: string;
  /** Shown dimmed and cannot be chosen (a plan not open yet). */
  disabled?: boolean;
  badge?: string;
  footnote?: string;
}

/** The Studio's icon tile picker: one or many options, each with an AniCanon icon. */
export interface OptionTilesProps {
  label?: string;
  id?: string;
  options: TileOption[];
  value: string | string[];
  multiple?: boolean;
  /** lg: choice cards with description, footnote and badge. */
  size?: 'lg';
  columns?: number;
  iconSize?: number;
  note?: string;
  help?: { title: string; text: ReactNode };
  onChange?: (value: string) => void;
  className?: string;
}
TokenLightDarkUse
option-tile-bgbg-fill-subtlebg-fill-subtleOptionTiles: idle tile.
option-tile-fgtext-secondarytext-secondaryOptionTiles: idle icon and name.
option-tile-hover-bgbg-fillbg-fillOptionTiles: hovered tile.
option-tile-selected-bgaccent-subtleaccent-subtleOptionTiles: chosen tile wash.
option-tile-selected-borderaccentaccentOptionTiles: chosen tile edge.
option-tile-selected-fgtext-primarytext-primaryOptionTiles: chosen tile icon and name.
option-tile-labeltext-mutedtext-mutedOptionTiles: the field name above the grid.
option-tile-notetext-mutedtext-mutedOptionTiles: the line describing the chosen option.
option-card-bgbg-surfacebg-surfaceOptionTiles large: card.
option-card-borderborder-dividerborder-dividerOptionTiles large: edge.
option-card-selected-bgaccent-subtleaccent-subtleOptionTiles large: chosen card.
option-card-selected-borderaccentaccentOptionTiles large: chosen edge.
option-card-iconaccentaccentOptionTiles large: icon.
option-card-texttext-secondarytext-secondaryOptionTiles large: description.
option-card-foottext-mutedtext-mutedOptionTiles large: footnote.

Core · Actions

Switch

Switch turns one thing on or off, with its name as the label (the visible text sits beside it). on is the state; disabled greys it.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
One setting that takes effect the moment it changes: Loop this track, Name who speaks.
Not for

Guidelines

  • Use it for a setting that takes effect at once (Lower the music under voices, Name who speaks, Loop this track).
  • A choice between named options is a SegmentedControl, not a switch.
  • The thumb crosses the track on motion-fast and stretches while held.
  • showLabel sets the label beside it, the whole row toggling: the settings row of a panel (Loops without a seam).
  • hint is a quiet line under the label saying what the setting does; a child sits at the row's end, before the switch (the audio's language).

Props

Prop
Type
What it does
on
boolean
labelrequired
string
The setting's name, read by screen readers.
showLabel
boolean
Sets the label beside it; the whole row toggles.
hint
string
With showLabel: a quiet line under the label.
disabled
boolean
onChange
(on: boolean) => void
children
ReactNode
With showLabel: sits at the row's end, before the switch.
className
string
TypeScript
export interface SwitchProps {
  on?: boolean;
  /** The setting's name, read by screen readers. */
  label: string;
  /** Sets the label beside it; the whole row toggles. */
  showLabel?: boolean;
  /** With showLabel: a quiet line under the label. */
  hint?: string;
  disabled?: boolean;
  onChange?: (on: boolean) => void;
  /** With showLabel: sits at the row's end, before the switch. */
  children?: ReactNode;
  className?: string;
}
TokenLightDarkUse
switchrow-fgtext-primarytext-primarySwitch with its label shown: the label.

Core · Actions

PanelRow

PanelRow is Studio’s folded section: an icon, the section’s name and its current value, with the chevron that opens it. open turns the chevron on motion-fast; onClick toggles it; href makes it a link to where the section is edited.

@sakuga/uiv1.0Used onWeb

Use it for
A folded Studio section: its icon, its name and what it is set to (Close-up · push in · low), with the chevron that opens it.
Not for
  • Several kinds of setting listed together, each opening its full choicePropertyList
  • A Studio card with its controls showingPanel
  • One thing on a row of a listListRow
  • One setting on or offSwitch

Guidelines

  • The value says what is set (Close-up · push in · low), not a description.

Props

Prop
Type
What it does
iconrequired
IconName
titlerequired
string
value
string
open
boolean
href
string
onClick
MouseEventHandler
className
string
TypeScript
export interface PanelRowProps {
  icon: IconName;
  title: string;
  value?: string;
  open?: boolean;
  href?: string;
  onClick?: MouseEventHandler;
  className?: string;
}

Core · Actions

TextLink is a quiet action inside a line or a row of text: Accept, Decline, Why; Ask Maki; Use it.

@sakuga/uiv1.0Used onWeb · iOS · Android

Core · Forms

TextField

A labelled text input or text area. The label sits above, the hint below. Use highlight for a value that changed while editing.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
Words a person types, on one line or in a text area: a name, a description, a search.
Not for

Guidelines

Motion: state changes fade with motion-fast; entrances use motion-base and ease-enter.

Example

Sakuga.TextField({ label: "Name", value: "Plaid overshirt" })

Props

Prop
Type
What it does
label
string
Set it, or the placeholder becomes the accessible name (search fields).
icon
string
Leading icon for single-line fields, e.g. search.
value
string
placeholder
string
As value.
multiline
boolean
A text area; rows, minHeight.
rows
number
minHeight
number
hint
string
One line under the control.
optional
boolean
Adds " · optional".
help
{ title?: string; text: string }
Adds a FieldHelp tooltip.
highlight
boolean
Accent edge for a changed value.
maxWidth
number
TypeScript
export interface TextFieldProps {
  label?: string;
  icon?: string;
  value?: string;
  placeholder?: string;
  multiline?: boolean;
  rows?: number;
  minHeight?: number;
  hint?: string;
  optional?: boolean;
  help?: { title?: string; text: string };
  highlight?: boolean;
  maxWidth?: number;
}
TokenLightDarkUse
field-labeltext-mutedtext-mutedTextField and Select: label.
field-hinttext-mutedtext-mutedTextField and Select: hint under the control.
field-bgbg-canvasbg-canvasTextField and Select: control fill.
field-borderborder-controlborder-controlTextField and Select: control edge.
field-fgtext-primarytext-primaryTextField and Select: value.
field-placeholdertext-mutedtext-mutedTextField and Select: placeholder.
field-highlightaccentaccentTextField: a field that changed or needs attention.

Core · Forms

Select

A labelled single-choice control that opens a list. Shows the chosen value with an optional leading icon.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
One choice from a short list of plain values, in a labelled field that opens the list: a faction, with its icon.
Not for

Guidelines

States

  • Closed: the field shows the chosen value and a chevron.
  • Open: the field edge turns select-open-border, the chevron flips, and a menu drops in under it on select-menu-bg. The chosen option sits on select-option-selected-bg with a check; hover is select-option-hover-bg. The menu is a listbox and each row an option, so arrow keys and screen readers work as a native select.
  • Empty: the placeholder in field-placeholder.

On phones, prefer the platform picker (an iOS menu or an Android dropdown menu) and keep this field as its trigger.

Motion: the menu drops from the field on motion-fast with ease-enter and leaves on motion-instant with ease-exit. Escape or a press outside closes it.

Example

Sakuga.Select({ label: "Faction", value: "Heros", icon: "faction-hero" })

Props

SelectOption

Prop
Type
What it does
labelrequired
string
value
string
Empty shows the placeholder.
icon
IconName
Leading glyph for the value.
hint
string

SelectProps

Prop
Type
What it does
label
string
value
string
Empty shows the placeholder.
placeholder
string
icon
IconName
Leading glyph for the value.
hint
string
optional
boolean
options
SelectOption[]
The list it opens. Without options it is display-only.
highlight
boolean
Rings the field while a picker beside it is choosing for it.
open
boolean
Start open. Clicking the field toggles it; picking an option closes it.
onChange
(value: string) => void
Called with the picked option's value (or label).
TypeScript
export interface SelectOption {
  label: string;
  value?: string;
  icon?: IconName;
  hint?: string;
}

export interface SelectProps {
  label?: string;
  value?: string;
  placeholder?: string;
  icon?: IconName;
  hint?: string;
  optional?: boolean;
  options?: SelectOption[];
  /** Rings the field while a picker beside it is choosing for it. */
  highlight?: boolean;
  open?: boolean;
  onChange?: (value: string) => void;
}
TokenLightDarkUse
select-open-borderaccentaccentSelect: the edge of a select whose menu is open.
select-menu-bgbg-surface-raisedbg-surface-raisedSelect: the open menu.
select-menu-borderborder-dividerborder-dividerSelect: the open menu edge.
select-option-hover-bgbg-fill-subtlebg-fill-subtleSelect: an option under the pointer.
select-option-selected-bgbg-fill-selectedbg-fill-selectedSelect: the chosen option.
select-option-checkaccentaccentSelect: the check beside the chosen option.

Core · Forms

DropZone

Where a file goes: drop or choose. Shows empty, busy (reading the file) and filled states.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
Where a file goes, dropped or chosen, then read and held: a clip to bring in.
Not for
  • A page or a list with nothing in it yetEmptyState
  • A frame waiting for a picture that will be madeThumb
  • Choosing one thing already in the story, such as a placePicker

Guidelines

Motion: state changes fade with motion-fast; entrances use motion-base and ease-enter.

Example

Sakuga.DropZone({ title: "Drop a clip, or choose a file", hint: "…" })

Props

Prop
Type
What it does
state
'empty' | 'busy' | 'filled'
title
string
hint
string
As title.
icon
IconName
Default upload.
height
number
TypeScript
export interface DropZoneProps {
  state?: 'empty' | 'busy' | 'filled';
  title?: string;
  hint?: string;
  icon?: IconName;
  height?: number;
}
TokenLightDarkUse
drop-borderborder-placeholderborder-placeholderDropZone: dashed edge.
drop-hover-borderborder-controlborder-controlDropZone: edge on hover.
drop-fgtext-mutedtext-mutedDropZone: hint.
drop-titletext-primarytext-primaryDropZone: title.
drop-iconaccentaccentDropZone: icon.
drop-filled-bgbg-surface-raisedbg-surface-raisedDropZone: filled and busy fill.

Core · Forms

FieldHelp

FieldHelp is the ⓘ beside a field name. It explains the field in one bold line plus examples, on hover or keyboard focus.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
The ⓘ beside a field's name that says what the field controls, in one bold line and examples: shot type, depth of field.
Not for
  • Context everyone should see, kept on the pageNotice
  • A hover card on a mention or a small formPopover
  • An action shown as a glyph aloneIconButton

Guidelines

  • Use it on every Studio control a newcomer might not know (shot type, depth of field, Story Map).
  • Write the first line as what the field controls, then "Examples:".
  • It is a real button, so keyboard users reach it.

Props

Prop
Type
What it does
labelrequired
string
title
string
open
boolean
childrenrequired
ReactNode
TypeScript
/** The ⓘ helper beside a field name, with its tooltip. */
export interface FieldHelpProps {
  label: string;
  title?: string;
  open?: boolean;
  children: ReactNode;
}
TokenLightDarkUse
tooltip-bgbg-surface-raisedbg-surface-raisedFieldHelp: tooltip surface.
tooltip-fgtext-primarytext-primaryFieldHelp: tooltip text.
tooltip-borderborder-dividerborder-dividerFieldHelp: tooltip edge.
field-help-fgtext-mutedtext-mutedFieldHelp: the info glyph.

Core · Forms

SwatchPicker

SwatchPicker chooses a colour by eye: round swatches, the chosen one ringed. Skin tone, a creature's coat.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
A colour chosen by eye from round swatches: a skin tone, a creature's coat (Tabby, Calico).
Not for
  • A choice that reads best as an icon over a nameOptionTiles
  • A short list of plain wordsSelect
  • Adjusting colour in the editorSliderRow

Guidelines

  • Options are {label, color}; each swatch is named by its label for screen readers.
  • labels writes the names under the swatches (coats, where the names matter: Tabby, Calico).
  • A patterned option passes pattern (stripes, patches, spots) and the marks colours drawn over its base.
  • Put it in a Field with its label; a free description can follow it in the same field.

Props

Prop
Type
What it does
label
string
value
string
labels
boolean
columns
number
optionsrequired
{ label: string; value?: string; color: string; pattern?: 'stripes' | 'patches' | 'spots'; marks?: string[] }[]
onChange
(value: string) => void
className
string
TypeScript
/** Round swatches, the chosen one ringed; a pattern draws its marks over the base colour. */
export interface SwatchPickerProps {
  label?: string;
  value?: string;
  labels?: boolean;
  columns?: number;
  options: { label: string; value?: string; color: string; pattern?: 'stripes' | 'patches' | 'spots'; marks?: string[] }[];
  onChange?: (value: string) => void;
  className?: string;
}

Core · Forms

Field

Field puts the form's label over a control that does not carry its own (a segmented control, chips, a group of options), with an optional hint under it.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
The label and hint over a control that has none of its own: a segmented control, chips, a group of options, swatches.
Not for
  • Typed words, which carry their own labelTextField
  • A list choice, which carries its own labelSelect
  • Explaining a field on hover or focusFieldHelp
  • A label over a strip or a group inside a panelSectionHeader

Guidelines

  • It uses the same label and hint as TextField and Select, so a form reads as one column whatever the control.
  • optional adds "· optional" to the label.

Props

Prop
Type
What it does
labelrequired
string
hint
string
optional
boolean
id
string
childrenrequired
ReactNode
className
string
TypeScript
/** The form's label over a control that has none of its own, with an optional hint. */
export interface FieldProps {
  label: string;
  hint?: string;
  optional?: boolean;
  id?: string;
  children: ReactNode;
  className?: string;
}

Core · Forms

SliderRow

SliderRow is a labelled value on a track: the name, the track (0–100) and the readout as the creator reads it (+0.2, −3 dB, 100%). disabled dims the track.

@sakuga/uiv1.0Used onWeb

Use it for
A value set by feel on a track, with its readout in the unit people use: colour, volume, opacity and loudness in the editor.
Not for

Guidelines

  • Used in the editor for colour, volume, opacity and loudness.
  • The readout is in the unit people use, never the raw 0–100.
  • onChange makes the track draggable and arrow-key operable (Shift moves 10). The thumb grows under the pointer and follows it with no easing.

Props

Prop
Type
What it does
labelrequired
string
valuerequired
number
0–100 along the track.
readoutrequired
string
As people read it: "+0.2", "−3 dB".
disabled
boolean
onChange
(value: number) => void
Makes the track draggable and arrow-key operable; called with 0–100.
className
string
TypeScript
export interface SliderRowProps {
  label: string;
  /** 0–100 along the track. */
  value: number;
  /** As people read it: "+0.2", "−3 dB". */
  readout: string;
  disabled?: boolean;
  /** Makes the track draggable and arrow-key operable; called with 0–100. */
  onChange?: (value: number) => void;
  className?: string;
}
TokenLightDarkUse
slidertrack-thumb-shadowblack-a50black-a50SliderRow: the knob's shadow.

Core · Forms

NumberField

NumberField is a number you type or step a unit at a time: its name, a step down, the value with its unit, a step up.

@sakuga/uiv1.0Used onWeb

Use it for
A number where one unit matters, typed or stepped a unit at a time: a 1px border, 2° of rotation, a font size.
Not for

Guidelines

  • Use it, not a slider, when one unit matters: a 1px border, 2° of rotation, a font size. A slider is for a feel (strength, mix), where the readout is the point.
  • unit follows the value ("px", "°", "pt"). Steps call onStep with −1 or +1.
  • In a narrow inspector stack one per row; two may share a row only when both fit at their longest value.

Props

Prop
Type
What it does
labelrequired
string
valuerequired
string | number
unit
string
Shown after the value: "px", "°", "pt".
onStep
(delta: -1 | 1) => void
Called with −1 or +1.
readOnly
boolean
id
string
className
string
TypeScript
/** A number you type or step a unit at a time, never drag: a 1px border is one press. */
export interface NumberFieldProps {
  label: string;
  value: string | number;
  /** Shown after the value: "px", "°", "pt". */
  unit?: string;
  /** Called with −1 or +1. */
  onStep?: (delta: -1 | 1) => void;
  readOnly?: boolean;
  id?: string;
  className?: string;
}
TokenLightDarkUse
num-labeltext-mutedtext-mutedNumberField: the number's name.
num-bgbg-canvasbg-canvasNumberField: the value box.
num-borderborder-controlborder-controlNumberField: the value box's edge.
num-fgtext-primarytext-primaryNumberField: the value.
num-focusaccentaccentNumberField: the value box's edge while typing.

Core · Navigation

TopNav

TopNav is the desktop header: the AniCanon mark and wordmark, the main sections as text links, and search and account on the right.

@sakuga/uiv1.0Used onWeb

Use it for
The desktop header: the AniCanon mark, up to five main sections (Home, Series, Shorts, Manga, Creators), search and the account.
Not for

Guidelines

  • Five links at most: Home, Series, Shorts, Manga, Creators. Mark the current one with current; it gets aria-current="page" and the topnav-indicator underline, which slides to a newly current link on motion-base.
  • It sits on the page or over the hero's top scrim, never in a filled bar.
  • Phones never show TopNav; they use TabBar.
  • actions (a prop, or children with slot="actions") sit before search and the account: the notifications bell, an IconButton with attention while something is unread.

Props

NavLink

Prop
Type
What it does
labelrequired
string
href
string
current
boolean
onClick
MouseEventHandler

TopNavProps

Prop
Type
What it does
brand
{ logo?: string; name?: string; href?: string }
linksrequired
NavLink[]
actions
ReactNode
label
string
className
string

TopNavAccount · TopNav, viewer header. Pass account so the signed-in person always shows at the right; create adds the Create button; search shows whenever account or create is set (set search={false} to hide). The header fills its container's width.

Prop
Type
What it does
namerequired
string
image
string
TypeScript
export interface NavLink {
  label: string;
  href?: string;
  current?: boolean;
  onClick?: MouseEventHandler;
}

export interface TopNavProps {
  brand?: { logo?: string; name?: string; href?: string };
  links: NavLink[];
  actions?: ReactNode;
  label?: string;
  className?: string;
}

/** TopNav, viewer header. Pass `account` so the signed-in person always shows at the right; `create` adds the Create button; search shows whenever account or create is set (set `search={false}` to hide). The header fills its container's width. */
export interface TopNavAccount {
  name: string;
  image?: string;
}
TokenLightDarkUse
topnav-link-fgtext-secondarytext-secondaryTopNav: idle links.
topnav-link-active-fgtext-primarytext-primaryTopNav: the current link.
topnav-indicatoraccentaccentTopNav: underline under the current link.
topnav-brand-fgtext-primarytext-primaryTopNav: the AniCanon wordmark.

Core · Navigation

SideNav

The creator sidebar: product, a switcher for the current workspace, grouped sections, a footer slot and the signed-in account at the bottom. The account is always shown. The current item reads in ink weight with the small state dot beside its name: no fill, no edge.

@sakuga/uiv1.0Used onWeb

Use it for
The creator sidebar: the switcher for the current workspace, grouped sections and the signed-in account at the bottom.
Not for
  • The main sections a viewer moves between on desktopTopNav
  • The views and scenes inside a productionProductionNav
  • Navigation on a phoneTabBar

Guidelines

Motion: state changes fade with motion-fast; entrances use motion-base and ease-enter.

Example

Sakuga.SideNav({ brand, switcher, groups, account })

Props

Prop
Type
What it does
brand
{ logo?: string; name?: string; product?: string }
switcher
{ label: string; image?: string }
Current series or workspace.
groupsrequired
{ title?: string; items: { label: string; icon?: IconName; current?: boolean; href?: string }[] }[]
accountrequired
{ name: string; handle?: string; image?: string }
Always pass it.
children
unknown
Footer slot above the account.
TypeScript
export interface SideNavProps {
  brand?: { logo?: string; name?: string; product?: string };
  switcher?: { label: string; image?: string };
  groups: { title?: string; items: { label: string; icon?: IconName; current?: boolean; href?: string }[] }[];
  account: { name: string; handle?: string; image?: string };
  children?: unknown;
}
TokenLightDarkUse
sidebar-bgbg-surfacebg-surfaceStudio sidebar: its panel.
sidebar-borderborder-hairlineborder-hairlineStudio sidebar: edge against the page.
sidebar-grouptext-mutedtext-mutedStudio sidebar: group headings (Make, Cast and world).
sidebar-item-fgtext-secondarytext-secondaryStudio sidebar: idle items.
sidebar-item-hover-bgbg-fill-subtlebg-fill-subtleStudio sidebar: hovered item.
sidebar-item-active-fgtext-primarytext-primaryStudio sidebar: the current page name.

Core · Navigation

TabBar

TabBar is the phone's main navigation: a floating glass capsule with four destinations and a separate round Create button.

@sakuga/uiv1.0Used oniOS · Android

Use it for
The phone's main navigation: four destinations (Home, Shorts, Manga, You) and the separate Create button.
Not for

Guidelines

  • Exactly four items: Home, Shorts, Manga, You. The current item fills with tabbar-item-active-bg; the fill slides to a new item on motion-base.
  • Create is separate from the destinations because it starts a task rather than going somewhere. Pass onCreate={null} to hide it.
  • Float it 12px from the sides and above the home indicator; content scrolls under it.
  • On iOS use the system tab bar with Liquid Glass and these four items, not a re-drawn copy.

Props

TabItem

Prop
Type
What it does
labelrequired
string
iconrequired
IconName
href
string
current
boolean
onClick
MouseEventHandler

TabBarProps

Prop
Type
What it does
itemsrequired
TabItem[]
onCreate
MouseEventHandler | null
createLabel
string
label
string
className
string
TypeScript
export interface TabItem {
  label: string;
  icon: IconName;
  href?: string;
  current?: boolean;
  onClick?: MouseEventHandler;
}

export interface TabBarProps {
  items: TabItem[];
  onCreate?: MouseEventHandler | null;
  createLabel?: string;
  label?: string;
  className?: string;
}
TokenLightDarkUse
tabbar-bgbg-glassbg-glassTabBar: the glass capsule.
tabbar-borderborder-glassborder-glassTabBar: capsule edge.
tabbar-item-fgtext-secondarytext-secondaryTabBar: idle items.
tabbar-item-active-bgaction-primaryaction-primaryTabBar: the current item capsule.
tabbar-item-active-fgtext-inversetext-inverseTabBar: the current item glyph and name.
tabbar-create-fgtext-primarytext-primaryTabBar: the Create button glyph.

Core · Navigation

SectionHeader

SectionHeader opens a shelf: a heading, the line that explains what is on it, and an optional See all link.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
The heading that opens a shelf, with the line that says what is on it ("Shorts · videos under a minute") and an optional See all link.
Not for
  • A labelled group of rows in a Story panel, with its countPanelSection
  • The top of a side panel in StoryPanelHeader
  • The top of a production viewViewHeader
  • The label over a single control in a formField

Guidelines

  • Always write the subtitle. It tells a new viewer what the shelf holds: "Shorts · videos under a minute", "Places in the story".
  • size="lg" (title-2) on desktop, md (title-3) on phone.
  • Give the See all link an ariaLabel with its object ("See all EBA shorts").
  • size="xs" is a label over a strip or a group inside a panel ("The issue, spread by spread"): 12px, no subtitle, no link; children sit at the row's end, such as a key.
  • icon and count head a group in a list (a faction and how many are in it). With no action link, children sit at the row's end (a button).
  • size="sm" (17 px) heads a section inside a page.

Props

Prop
Type
What it does
titlerequired
ReactNode
subtitle
ReactNode
The line that explains the shelf: "Shorts · videos under a minute".
action
{ label: string; href?: string; ariaLabel?: string; onClick?: MouseEventHandler }
size
'lg' | 'md' | 'xs'
lg = desktop (title-2), md = phone (title-3), xs = a label over a strip or a group inside a panel (12px, no subtitle or action).
as
'h2' | 'h3'
id
string
children
ReactNode
xs only: what sits at the end of the row, e.g. a key.
className
string
TypeScript
export interface SectionHeaderProps {
  title: ReactNode;
  /** The line that explains the shelf: "Shorts · videos under a minute". */
  subtitle?: ReactNode;
  action?: { label: string; href?: string; ariaLabel?: string; onClick?: MouseEventHandler };
  /** lg = desktop (title-2), md = phone (title-3), xs = a label over a strip or a group inside a panel (12px, no subtitle or action). */
  size?: 'lg' | 'md' | 'xs';
  as?: 'h2' | 'h3';
  id?: string;
  /** xs only: what sits at the end of the row, e.g. a key. */
  children?: ReactNode;
  className?: string;
}
TokenLightDarkUse
section-header-titletext-primarytext-primarySectionHeader: heading.
section-header-subtitletext-mutedtext-mutedSectionHeader: the line that explains the shelf.
section-header-actiontext-secondarytext-secondarySectionHeader: the See all link.
sec-xs-titletext-mutedtext-mutedSectionHeader xs: the small label over a section.

Core · Navigation

Pagination

Pagination reads a long list a page at a time: what this page holds, then previous, the page numbers and next. A list of one page shows none.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
Reading a long list a page at a time, under the list it pages: productions at 24 a page, counted as "1–24 of 31 shorts".
Not for
  • Where a person is in a flow they step throughStepper
  • A shelf that leads to everything on itSectionHeader
  • The pages of a manga issue in the editorIssueStrip

Guidelines

  • Pass page, pages and, when the total is known, total, pageSize and noun: the count reads "1–24 of 31 shorts".
  • Lists of productions page at 24, as the product's clips list does; search and filters sit above the list and send you back to page 1.
  • Far pages fold into an ellipsis around the current one: 1 … 4 5 6 … 12.
  • hrefs gives each page an address; without it the pages are buttons that call onChange with the page.
  • The current page's fill slides to the new page on motion-base; numbers sink on press.
  • Put it under the list it pages, across the list's full width. One per list.

Props

Prop
Type
What it does
pagerequired
number
pagesrequired
number
total
number
With total, the count reads "1–24 of 31 shorts"; without, "Page 1 of 2".
pageSize
number
noun
string
label
string
hrefs
string[]
One address per page, in order.
onChange
(page: number) => void
Without hrefs: called with the page to show.
className
string
TypeScript
/** Renders nothing for a single page. */
export interface PaginationProps {
  page: number;
  pages: number;
  /** With total, the count reads "1–24 of 31 shorts"; without, "Page 1 of 2". */
  total?: number;
  pageSize?: number;
  noun?: string;
  label?: string;
  /** One address per page, in order. */
  hrefs?: string[];
  /** Without hrefs: called with the page to show. */
  onChange?: (page: number) => void;
  className?: string;
}

Core · Navigation

FlowHeader

FlowHeader is the full-screen flow's top bar: close (nothing is kept), what is being made and for what, the steps when there are several, then what sits at the end.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
The top bar of a full-screen flow: close, what is being made and for what ("New issue · Bad Memories"), the steps when there are several, and the balance.
Not for

Guidelines

  • title and context: "New issue · Bad Memories". closeLabel says what closing does ("Close. Nothing is created").
  • steps and current draw a Stepper in the middle, only when the flow has more than one step.
  • slot="end": the balance (AniCanonKit.FragmentBalance) in AniCanon.
  • One way out per screen: this close button. Back lives in FlowFooter, and only when a step precedes.

Props

Prop
Type
What it does
titlerequired
string
context
string
closeLabel
string
closeHref
string
onClose
MouseEventHandler
steps
string[]
current
number
stepsLabel
string
children
ReactNode
className
string
TypeScript
/** The full-screen flow's top bar. Slot: slot="end" (the balance). */
export interface FlowHeaderProps {
  title: string;
  context?: string;
  closeLabel?: string;
  closeHref?: string;
  onClose?: MouseEventHandler;
  steps?: string[];
  current?: number;
  stepsLabel?: string;
  children?: ReactNode;
  className?: string;
}
TokenLightDarkUse
flowhead-ruleborder-hairlineborder-hairlineFlowHeader: the rule under the bar.
flowhead-titletext-primarytext-primaryFlowHeader: what is being made.
flowhead-contexttext-mutedtext-mutedFlowHeader: what it is made for.

Core · Navigation

FlowFooter

FlowFooter closes a step of the full-screen flow, inline under its content: Back on the left when a step precedes, then the note and the action on the right.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
The foot of a step in a full-screen flow: Back when a step precedes, a note on what happens next or what it costs, then the one primary action.
Not for

Guidelines

  • note says what happens next or what it costs ("Free. Nothing is generated until you start it").
  • The action is its child: one primary Button, with its cost when it spends.
  • back takes label, href or onClick.
  • slot="start" holds what leaves the step when it is not Back (Cancel creation, Skip for now, Show details).

Props

Prop
Type
What it does
note
string
back
{ label?: string; href?: string; onClick?: MouseEventHandler }
children
ReactNode
className
string
TypeScript
/** The full-screen flow's footer: Back (or slot="start" for another way out), the note, the action as its child. */
export interface FlowFooterProps {
  note?: string;
  back?: { label?: string; href?: string; onClick?: MouseEventHandler };
  children?: ReactNode;
  className?: string;
}
TokenLightDarkUse
flowfoot-ruleborder-hairlineborder-hairlineFlowFooter: the rule over the footer.
flowfoot-notetext-mutedtext-mutedFlowFooter: the note beside the action.

Core · Navigation

Stepper

Where a person is in a short multi-step flow: done steps get a check, the current one fills. Use it in the top bar of a full-screen flow, three to five steps. Never for navigation between pages.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
Where a person is in a short flow of three to five steps, in the top bar of a full-screen flow; variant="bar" on a phone.
Not for
  • Moving between the pages of a listPagination
  • A job's stages while it runs, with the time each tookStages
  • How far something being made has comeProgress
  • What a character is ready withReadiness

Guidelines

Motion: moving on fills the line toward the next step on motion-base; the new current step settles in and a done step's check appears with ease-enter. variant="bar" is the phone's form: a segment per step under the title, then "Step 2 of 3 · Photos". tone="media" draws it over a camera.

Example

Sakuga.Stepper({ steps: ["How","Approve","Portrait"], current: 1 })

Props

Prop
Type
What it does
stepsrequired
string[]
Step names, short nouns.
currentrequired
number
Index of the current step.
label
string
Accessible name, default "Steps".
variant
'steps' | 'bar'
tone
'media'
TypeScript
/** `variant="bar"`: a phone's segments with "Step 2 of 3 · Photos"; `tone="media"` over a camera. */
export interface StepperProps {
  steps: string[];
  current: number;
  label?: string;
  variant?: 'steps' | 'bar';
  tone?: 'media';
}
TokenLightDarkUse
stepper-fgtext-mutedtext-mutedStepper: upcoming steps.
stepper-current-fgtext-primarytext-primaryStepper: current and done labels.
stepper-current-bgaction-primaryaction-primaryStepper: current step number fill.
stepper-current-numtext-inversetext-inverseStepper: current step number.
stepper-done-bgbg-fillbg-fillStepper: done step fill.
stepper-ringborder-controlborder-controlStepper: upcoming step ring.
stepper-sepborder-dividerborder-dividerStepper: line between steps.

Core · Content

MediaCard

MediaCard shows one episode, short or manga issue: its artwork at its real shape, then a title and a metadata line.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
One episode, short or manga issue: its art at its real shape (16:9, 9:16 or a manga page), its title and a metadata line.
Not for
  • A picture in a frame, such as a take or a library itemThumb
  • An episode or an issue announced but not out yetPremiereSlot
  • A page of an issue in the editorMangaPage
  • A cast member on the cast pageCastCard

Guidelines

  • kind sets the frame: episode 16:9, short 9:16, manga 1:1.414 with a paper corner and a spine shadow. Never put one kind's art in another's frame.
  • Continue watching passes logo (the series logo laid over the art) and progress (0–1).
  • Shorts pass duration and count; episodes pass duration; manga pass badge for New.
  • meta joins facts with a middle dot: "EBA · Season 1 · Episode 1". In a creator's list it says where the production is ("Cut · 3 takes", "14 Oct · 3.4K views"); attention marks the ones waiting for the creator with the accent dot, below the art, never over it.
  • A linked card's frame sinks on press; the art leans in on hover and focus, and watched progress draws in when the card appears.
  • Give the card a width from the layout (a grid column or a fixed shelf width); the frame keeps its ratio.
  • The corner follows the size: thumbnails 160px wide or less use radius-sm, 64px or less radius-xs, so a small frame never turns into a pill.

Props

Prop
Type
What it does
kindrequired
'episode' | 'short' | 'manga'
Sets the frame ratio: episode 16:9, short 9:16, manga 1:1.414.
imagerequired
string
alt
string
imagePosition
string
title
ReactNode
meta
ReactNode
duration
string
m:ss, shown as a Badge.
count
string
badge
ReactNode
logo
string
The series logo laid over the artwork (Continue watching).
logoAlt
string
progress
number
0–1 watched.
attention
boolean
The meta line says this one waits for you (takes to review, a plan to approve, ready to publish): it reads in the text colour with the accent dot.
href
string
label
string
width
number | string
className
string
TypeScript
export interface MediaCardProps {
  /** Sets the frame ratio: episode 16:9, short 9:16, manga 1:1.414. */
  kind: 'episode' | 'short' | 'manga';
  image: string;
  alt?: string;
  imagePosition?: string;
  title?: ReactNode;
  meta?: ReactNode;
  /** m:ss, shown as a Badge. */
  duration?: string;
  count?: string;
  badge?: ReactNode;
  /** The series logo laid over the artwork (Continue watching). */
  logo?: string;
  logoAlt?: string;
  /** 0–1 watched. */
  progress?: number;
  /** The meta line says this one waits for you (takes to review, a plan to approve, ready to publish): it reads in the text colour with the accent dot. */
  attention?: boolean;
  href?: string;
  label?: string;
  width?: number | string;
  className?: string;
}
TokenLightDarkUse
media-card-titletext-primarytext-primaryMediaCard: title under the artwork.
media-card-metatext-mutedtext-mutedMediaCard: metadata under the title.
media-card-placeholderbg-surfacebg-surfaceMediaCard: frame behind artwork while it loads.
media-card-progress-trackmedia-progress-trackmedia-progress-trackMediaCard: unwatched part of the progress line.
media-card-progress-fillaccentaccentMediaCard: watched part of the progress line.
media-card-scrimbg-scrim-labelbg-scrim-labelMediaCard: shade under a series logo laid over the artwork.

Core · Content

Badge

Badge is a small label laid over artwork: a duration, a view count, or New.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
A small label over artwork (a duration, a view count, New) and, on a surface, a state such as rendering or one that waits for you.
Not for

Guidelines

  • Only on artwork. It always sits on badge-bg, so it reads on any picture in either theme.
  • Durations in m:ss, counts short ("1.2M views").
  • variant="new" for a just-released issue or episode. One New badge per card at most.
  • variant="alert" for a state that changed for the worse: a place burning or destroyed, a character dead. It fills with badge-alert-bg; use it for that one state, never for decoration.
  • icon puts a 14 px glyph before the label. Use it when the state has a glyph (a trait, a warning); leave it off for durations and counts.
  • On a surface, not over a picture: variant="neutral" for a state (rendering, setup not finished), attention for one that waits for you, outline for where something comes from (Ep 1, Short). dot puts a dot before a state that is live.

Props

Prop
Type
What it does
variant
'default' | 'new' | 'alert' | 'neutral' | 'attention' | 'outline'
dot
boolean
A dot before a state that is live.
icon
IconName
childrenrequired
ReactNode
className
string
TypeScript
/** On artwork: default, new, alert. On a surface: neutral (a state), attention (it waits for you), outline (where it comes from). */
export interface BadgeProps {
  variant?: 'default' | 'new' | 'alert' | 'neutral' | 'attention' | 'outline';
  /** A dot before a state that is live. */
  dot?: boolean;
  icon?: IconName;
  children: ReactNode;
  className?: string;
}
TokenLightDarkUse
badge-bgbg-scrim-labelbg-scrim-labelBadge: duration and count labels laid over artwork.
badge-fgtext-on-mediatext-on-mediaBadge: label glyphs.
badge-new-bgtext-on-mediatext-on-mediaBadge new: the white 'New' flag over artwork.
badge-new-fgbg-mediabg-mediaBadge new: its word.
badge-alert-bgaccentaccentBadge: a state that changed for the worse (burning, dead, destroyed).
badge-alert-fgon-accenton-accentBadge: text and icon on badge-alert-bg.

Core · Content

CastAvatar

CastAvatar shows a person: a character (a round portrait from their identity sheet), or a creator (an initial until they add a picture), with their name and a one-line role.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
A person with their name and a one-line role: a character's round portrait from their identity sheet, or a creator's initial until they add a picture.
Not for
  • A cast member on the cast page, with what they have and what is happening to themCastCard
  • Who or what a panel holds, with its @ or # mark and a noteMentionChip
  • A character as one card with stats and traitsCharacterCard
  • A character named inside proseMention

Guidelines

  • size="lg" (104px) on the desktop series page, md (72px) on phone and in the player.
  • The role is a few words a viewer understands ("Leads the E.R.A.", "Power of wind"), never internal production data.
  • The portrait is decorative; the name is the label.
  • Without an image it shows the initial in the same circle, until a portrait is approved.
  • selected (true or false) makes it a choice: a ring and a check say it is chosen (who wears a look).
  • muted dims the face: the character is no longer in the story at that moment (dead, gone).
  • Under a small face (sm, xs) in a column, the name reads at 12 px and the name and role are cut to the cell, so a panel grid of four stays tidy; larger faces wrap their names.
  • layout="row" puts a small portrait beside the name and role (the people in a character's life). size is lg, md, sm (44), xs (36) or a number of pixels.

Props

Prop
Type
What it does
image
string
initial
string
name
string
role
string
size
'xs' | 'sm' | 'md' | 'lg' | number
layout
'row'
selected
boolean
muted
boolean
No longer in the story at this moment: the face dims.
onClick
MouseEventHandler
label
string
The name when none is shown.
href
string
width
number | string
className
string
TypeScript
/** A cast member. Without an image it shows `initial`; with `selected` (true or false) it is a choice; `layout="row"` puts the name beside a small portrait. */
export interface CastAvatarProps {
  image?: string;
  initial?: string;
  name?: string;
  role?: string;
  size?: 'xs' | 'sm' | 'md' | 'lg' | number;
  layout?: 'row';
  selected?: boolean;
  /** No longer in the story at this moment: the face dims. */
  muted?: boolean;
  onClick?: MouseEventHandler;
  /** The name when none is shown. */
  label?: string;
  href?: string;
  width?: number | string;
  className?: string;
}
TokenLightDarkUse
cast-avatar-nametext-primarytext-primaryCastAvatar: character name.
cast-avatar-roletext-mutedtext-mutedCastAvatar: role line.
cast-avatar-placeholderbg-surface-raisedbg-surface-raisedCastAvatar: portrait frame while it loads.

Core · Content

Thumb

Thumb is a picture in a frame: cover-cropped, 6 px corners, selected adds the accent ring, muted greys a rejected take. Give it width and height, or leave them out for a full-width 16:9 frame (ratio changes it).

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
A picture in a frame: a take (selected, or muted when rejected), a library item, a panel to pick, a frame not made yet.
Not for
  • An episode, short or manga issue a viewer opensMediaCard
  • A person's portrait with their name and roleCastAvatar
  • A manga page at its trimMangaPage
  • One look on white with its occasionLookPlate
  • A whole page or list with nothing in it yetEmptyState

Guidelines

  • imageStyle moves the crop (object-position:30% 40%).
  • figure lays a cut-out over the picture (a character on a place). Prefer a real render.
  • onClick makes it a toggle button (aria-pressed follows selected) that sinks on press; the accent ring grows in on motion-fast.
  • title and meta caption it under the picture (a library item, a panel to pick).
  • placeholder is a frame that is not made yet: an empty frame naming what will fill it ("Board drawn when you approve the cut"). Never fake a picture.
  • radius="none" sits it flush inside other art (a split view, a loupe of frames); radius="lg" for a cover.
  • Mount it; never draw .sk-thumb by hand.
  • fit="contain" shows the whole picture (a sheet); paper sets it on the white a sheet is drawn on, in either theme. radius is none, md, lg, xl or full (a round crop of a face). develop lets a new picture develop in once.

Props

Prop
Type
What it does
title
string
Caption under the picture.
meta
string
placeholder
string
A frame not made yet, naming what will fill it.
radius
'none' | 'md' | 'lg' | 'xl' | 'full'
fit
'cover' | 'contain'
contain: the whole picture, e.g. a sheet.
paper
boolean
The white a character sheet is drawn on.
develop
boolean
The picture develops in, once.
image
string
imageStyle
string
CSS, e.g. "object-position:30% 40%".
figure
string
figureStyle
string
width
number | string
height
number | string
ratio
string
selected
boolean
muted
boolean
alt
string
onClick
MouseEventHandler
Makes it a toggle button; aria-pressed follows selected.
label
string
The button's name when the picture's alt does not say it.
children
ReactNode
className
string
TypeScript
export interface ThumbProps {
  /** Caption under the picture. */
  title?: string;
  meta?: string;
  /** A frame not made yet, naming what will fill it. */
  placeholder?: string;
  radius?: 'none' | 'md' | 'lg' | 'xl' | 'full';
  /** contain: the whole picture, e.g. a sheet. */
  fit?: 'cover' | 'contain';
  /** The white a character sheet is drawn on. */
  paper?: boolean;
  /** The picture develops in, once. */
  develop?: boolean;
  image?: string;
  /** CSS, e.g. "object-position:30% 40%". */
  imageStyle?: string;
  figure?: string;
  figureStyle?: string;
  width?: number | string;
  height?: number | string;
  ratio?: string;
  selected?: boolean;
  muted?: boolean;
  alt?: string;
  /** Makes it a toggle button; aria-pressed follows selected. */
  onClick?: MouseEventHandler;
  /** The button's name when the picture's alt does not say it. */
  label?: string;
  children?: ReactNode;
  className?: string;
}
TokenLightDarkUse
thumbnail-ring-activeaccentaccentPage strip: ring around the current page.
thumbnail-ringborder-glass-mediaborder-glass-mediaPage strip: ring around the other pages.
thumb-captiontext-primarytext-primaryThumb: a caption's title.
thumb-caption-metatext-mutedtext-mutedThumb: a caption's quiet line.
thumb-placeholder-edgeborder-placeholderborder-placeholderThumb: the edge of a frame that is not made yet.
thumb-placeholder-fgtext-mutedtext-mutedThumb: what the empty frame will hold.
thumb-paperneutral-0neutral-0Thumb: the white a character or face sheet is drawn on, in either theme.

Core · Content

List and ListRow

ListRow is one row of a List: a title and a quiet line under it, with an optional icon or image before it, a meta column (a timecode) and whatever sits at the end (a switch, a button, a check). selected marks the current row the way every list and nav does: its title in ink weight with the small state-dot beside it, no fill and no edge; href makes it a link; loose gives it more room (settings lists).

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
One thing on one row of a List: a title and a quiet line, with what sits at the end (a switch, a button, a check); loose in settings lists.
Not for
  • A quiet action inside a line of textTextLink
  • A row on Story with a picture, a state and the work that told itEntityRow
  • Each kind of setting with its value, opening the full choicePropertyList
  • Measured facts, a name and its valueFactList
  • A cut on an ekonte sheetCutRow

Guidelines

  • A linked row sinks on press and its picture leans in on hover; rows never take a hover fill. onClick is for client-side navigation.
  • One row, one thing. A row never holds two actions of the same weight.
  • List wraps rows and names the list (label).
  • attention reads the quiet line in the text colour after the same state-dot: a take that waits for you, as MediaCard marks a production.
  • slot="lead" replaces the icon or image (a play button); slot="main" replaces the title and quiet line (a sound's waveform and its length).
  • chevron adds a trailing chevron when the row opens a page. List with surface sets the rows in a surface, as a phone groups them.

Props

ListProps

Prop
Type
What it does
label
string
surface
boolean
The rows sit in a surface (a phone's grouped list).
childrenrequired
ReactNode
className
string

ListRowProps

Prop
Type
What it does
titlerequired
ReactNode
sub
ReactNode
icon
IconName
image
string
imageStyle
string
imageWidth
number
imageHeight
number
muted
boolean
meta
string
A leading column, e.g. a timecode.
selected
boolean
attention
boolean
The quiet line waits for you: text colour with the accent dot. Slots: slot=\"lead\" (in place of icon or image), slot=\"main\" (in place of title and sub).
loose
boolean
chevron
boolean
A trailing chevron: the row opens something.
href
string
onClick
MouseEventHandler
With href, for client-side navigation.
children
ReactNode
What sits at the end: a switch, a button.
className
string
TypeScript
export interface ListProps {
  label?: string;
  /** The rows sit in a surface (a phone's grouped list). */
  surface?: boolean;
  children: ReactNode;
  className?: string;
}

export interface ListRowProps {
  title: ReactNode;
  sub?: ReactNode;
  icon?: IconName;
  image?: string;
  imageStyle?: string;
  imageWidth?: number;
  imageHeight?: number;
  muted?: boolean;
  /** A leading column, e.g. a timecode. */
  meta?: string;
  selected?: boolean;
  /** The quiet line waits for you: text colour with the accent dot. Slots: slot=\"lead\" (in place of icon or image), slot=\"main\" (in place of title and sub). */
  attention?: boolean;
  loose?: boolean;
  /** A trailing chevron: the row opens something. */
  chevron?: boolean;
  href?: string;
  /** With href, for client-side navigation. */
  onClick?: MouseEventHandler;
  /** What sits at the end: a switch, a button. */
  children?: ReactNode;
  className?: string;
}
TokenLightDarkUse
listrow-attentiontext-primarytext-primaryListRow: a line that waits for you.

Core · Content

FactList

FactList pairs a quiet name with its value, two columns, values to the right. For measured facts (a colour match across a join, the length of a trimmed cut), never for things the creator edits. layout="grid" sets each fact as a small block, its name over its value, with an optional note under it ("since Fire at Sé"): who a character is at a point in the story.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
Measured facts as a quiet name and its value (a colour match across a join, the length of a trimmed cut), and with layout="grid" who a character is at a point in the story.
Not for
  • Settings the creator edits, each opening its choicePropertyList
  • A character's picked facts, each with its picker's iconCharacterStats
  • A Studio section showing what it is set toPanelRow

Props

Prop
Type
What it does
itemsrequired
{ label: string; value: ReactNode; note?: string }[]
layout
'columns' | 'grid'
columns
number
className
string
TypeScript
/** `layout="grid"`: small blocks, each value with a quiet `note` (since when, from where). */
export interface FactListProps {
  items: { label: string; value: ReactNode; note?: string }[];
  layout?: 'columns' | 'grid';
  columns?: number;
  className?: string;
}

Core · Content

TagList

TagList keeps words as tags: a character's visual vibe, the garments read from a photo.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
Words kept as tags, each one removable and restorable: a character's visual vibe, the garments read from a photo.
Not for
  • Picking words from a set, such as traitsChip
  • A label over artworkBadge
  • Who or what a panel holds, with its pictureMentionChip
  • A free sentence someone typesTextField

Guidelines

  • Each tag carries its own button: removeLabel names what it does ("Leave out Silver chain"). A tag that is off reads struck through and its button puts it back (restoreLabel).
  • With a placeholder the tags sit in a field you type more into.
  • Short words, lower case unless a name.

Props

Prop
Type
What it does
itemsrequired
(string | { label: string; off?: boolean })[]
placeholder
string
label
string
inputLabel
string
removeLabel
string
restoreLabel
string
className
string
TypeScript
/** Words kept as tags, each with its own remove (or, once off, add back). A placeholder sets them in a box you type more into. */
export interface TagListProps {
  items: (string | { label: string; off?: boolean })[];
  placeholder?: string;
  label?: string;
  inputLabel?: string;
  removeLabel?: string;
  restoreLabel?: string;
  className?: string;
}

Core · Content

PropertyList

PropertyList shows each kind of setting on one row with what it is set to; a row opens the full choice.

@sakuga/uiv1.0Used onWeb

Use it for
Each kind of setting on a row with what it is set to, where a row opens the full choice: Studio's direction for a panel (shot type, angle, lighting, lens).
Not for
  • One section that folds open on its ownPanelRow
  • Measured facts that are never editedFactList
  • Every option of one kind, laid out to choose fromOptionTiles

Guidelines

  • Use it when there are more options than fit a panel: the panel says what is set, the dialog (or picker) holds every option. Studio's direction for a panel is the example: shot type, angle, lighting, lens.
  • label names the group; slot="action" sits beside it ("All options").
  • Each item takes label, value and an optional icon; give href or onClick to open its choice.

Props

Prop
Type
What it does
label
string
itemsrequired
{ label: string; value: ReactNode; icon?: IconName; href?: string; onClick?: MouseEventHandler }[]
children
ReactNode
className
string
TypeScript
/** Each kind of setting on one row with what it is set to; a row opens the full choice. Slot: slot="action" (beside the label, e.g. "All options"). */
export interface PropertyListProps {
  label?: string;
  items: { label: string; value: ReactNode; icon?: IconName; href?: string; onClick?: MouseEventHandler }[];
  children?: ReactNode;
  className?: string;
}
TokenLightDarkUse
props-labeltext-mutedtext-mutedPropertyList: its label.
props-edgeborder-hairlineborder-hairlinePropertyList: the box and the rules between rows.
props-texttext-primarytext-primaryPropertyList: a setting's name.
props-valuetext-mutedtext-mutedPropertyList: what it is set to.

Core · Feedback

Notice

A single line of context inside a page: something to know (info) or something that needs action (attention), with an optional action on the right. Not for errors that block, use the page state for those.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
One line of context inside a page: something to know, or something that needs action, with an optional action.
Not for
  • Confirming that something finishedToast
  • A failure where the content should beEmptyState
  • A correction pinned to work on a sheetSlip

Guidelines

Motion: state changes fade with motion-fast; entrances use motion-base and ease-enter.

Example

Sakuga.Notice({ tone: "attention", title: "…", text: "…" })

Props

Prop
Type
What it does
tone
'info' | 'attention'
icon
IconName
Defaults by tone.
title
string
Bold lead-in.
text
string
children
unknown
Action, usually a small Button.
TypeScript
export interface NoticeProps {
  tone?: 'info' | 'attention';
  icon?: IconName;
  title?: string;
  text?: string;
  children?: unknown;
}
TokenLightDarkUse
notice-info-bgbg-fill-subtlebg-fill-subtleNotice, info: fill.
notice-info-icontext-mutedtext-mutedNotice, info: icon.
notice-attention-bgaccent-subtleaccent-subtleNotice, attention: fill.
notice-attention-icontext-accenttext-accentNotice, attention: icon.
notice-titletext-primarytext-primaryNotice: bold lead.
notice-texttext-secondarytext-secondaryNotice: body text.

Core · Feedback

Toast

A short confirmation that something finished, rising into a corner. One line of title and one of detail, optional action, always dismissable. Never for errors that need a decision.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
A short confirmation that something finished, with an optional action.
Not for
  • An error that needs a decisionDialog
  • Context that stays on the pageNotice
  • Work that is still runningProgress

Guidelines

Motion: it rises in on motion-base with ease-enter and leaves on motion-fast with ease-exit.

Example

Sakuga.Toast({ title: "Plaid overshirt joined his looks", text: "20 fragments charged." })

Props

Prop
Type
What it does
titlerequired
string
text
string
icon
IconName
Leading glyph or avatar.
image
string
As icon.
open
boolean
Set it to control the toast: false plays its exit, then it unmounts. Without it, Dismiss plays the exit and then calls onDismiss.
onDismiss
() => void
Without open, called after the exit has played.
children
unknown
An action button.
TypeScript
export interface ToastProps {
  title: string;
  text?: string;
  icon?: IconName;
  image?: string;
  /** Set it to control the toast: false plays its exit, then it unmounts. Without it, Dismiss plays the exit and then calls onDismiss. */
  open?: boolean;
  onDismiss?: () => void;
  children?: unknown;
}
TokenLightDarkUse
toast-bgbg-surfacebg-surfaceToast: surface.
toast-borderborder-dividerborder-dividerToast: edge.
toast-iconaccentaccentToast: leading icon.

Core · Feedback

Loader

Sakuga's loader is a ball bouncing on twos, the first exercise every animator draws. Nine drawings, each held for two frames at 24 fps, with the top held twice. The spacing is tight at the top and wide near the ground, the ball stretches as it falls and squashes on contact, and the drawing before shows through as an onion skin. Use it anywhere something is being made or loading: a busy drop zone, a stage, a frame waiting for its picture, a button.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
Something being made or loading whose length is unknown: a busy drop zone, a frame waiting for its picture, a button.
Not for

Guidelines

One bounce takes 0.75 s (18 frames), held on twos, with a hard cut between drawings and no easing. The ball and its onion skin take the accent (load-accent). The ground follows the text colour, so over dark art it stays light.

With reduced motion, the ball rests at the top of its arc.

AniCanon's own loader, its mark with the ring tracing round it, is AniCanonKit.Loader.

Example

<Sakuga.Loader size={16} label="Loading the pages" />

Props

Prop
Type
What it does
size
number
Pixel size. It reads down to 16; give a frame that waits for a picture 48 or more. Default 24
label
string
Announced to screen readers. Default "Loading"
className
string
style
Record<string, string | number>
TypeScript
/** Sakuga's loader: a ball bouncing on twos, stretched as it falls, squashed on contact, the drawing before as an onion skin. It reads down to 16 px. With reduced motion the ball rests at the top. */
export interface LoaderProps {
  size?: number;
  label?: string;
  className?: string;
  style?: Record<string, string | number>;
}

Core · Feedback

EmptyState

EmptyState stands in place of content while there is none: what is happening or what went wrong, in a sentence, then what can be done.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
The place where content would be while there is none: nothing yet, something being made, or something that failed.
Not for
  • One line of context beside content that is thereNotice
  • A job whose share done is knownProgress
  • A frame waiting for its pictureThumb

Guidelines

  • title says it plainly ("Ponytail didn’t finish", "Warming up the booth"); text says what is kept and what happens next.
  • tone="attention" with icon="warning" when something failed; pass a loader in slot="media" while something is being made.
  • Children follow the text: what it cost (a held or returned amount), a quiet "Show details".
  • It never apologises and never blames the creator. Nothing is charged for a failure, and the text says so.

Props

Prop
Type
What it does
title
string
text
string
icon
IconName
tone
'attention'
children
ReactNode
className
string
TypeScript
/** A state in place of content: what is happening or what went wrong, then what can be done (children). Slot: slot="media" (a loader). */
export interface EmptyStateProps {
  title?: string;
  text?: string;
  icon?: IconName;
  tone?: 'attention';
  children?: ReactNode;
  className?: string;
}

Core · Feedback

Progress

Progress is how far something being made has come, as a line that fills in the accent.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
How far something being made has come, when the share done is known.
Not for
  • Work whose length is unknownLoader
  • The playhead of an episode or a shortScrubber
  • Where a person is in a flow they step throughStepper

Guidelines

  • value is 0 to 100; label names what is being made, for screen readers ("Rendering your take").
  • It sits under the words that say what is happening, never in place of them: a title, then the line, then what is held and when it lands.
  • tone="media" over art or video, so the track stays light on a dark picture.
  • A job with stages shows them with Stepper above the line; a job whose length is unknown uses Loader, not a line that never moves.

Props

Prop
Type
What it does
valuerequired
number
label
string
tone
'media'
className
string
TypeScript
/** How far something being made has come, 0 to 100; `tone: 'media'` over art. */
export interface ProgressProps {
  value: number;
  label?: string;
  tone?: 'media';
  className?: string;
}

Core · Media

Scrubber

Scrubber shows and sets the playhead: played, buffered and remaining time, with the current time and total length.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
Showing and setting the playhead of an episode or a short, over video or the player bar: played, buffered and remaining time, with chapter marks.
Not for

Guidelines

  • It is a slider with spoken values ("3 minutes 12 seconds of 11 minutes 36 seconds"). Arrow keys move 10 seconds, Page keys a minute, Home and End jump to the ends.
  • Always over video or the player bar, so it uses the media tokens and stays the same in both themes.
  • showTimes={false} when the times are shown elsewhere (phone shorts).
  • markers draws chapter marks (in seconds) cut into the track.
  • The thumb grows and the track thickens under the pointer. Dragging follows the pointer with no easing; playback moves the fill linearly.

Props

Prop
Type
What it does
valuerequired
number
maxrequired
number
markers
number[]
Chapter marks, in seconds.
buffered
number
step
number
label
string
showTimes
boolean
onChange
(seconds: number) => void
className
string
TypeScript
export interface ScrubberProps {
  value: number;
  max: number;
  /** Chapter marks, in seconds. */
  markers?: number[];
  buffered?: number;
  step?: number;
  label?: string;
  showTimes?: boolean;
  onChange?: (seconds: number) => void;
  className?: string;
}
TokenLightDarkUse
scrubber-trackmedia-progress-trackmedia-progress-trackScrubber: unplayed range.
scrubber-buffermedia-progress-buffermedia-progress-bufferScrubber: buffered range.
scrubber-fillprogress-fillprogress-fillScrubber: played range.
scrubber-thumbtext-on-mediatext-on-mediaScrubber: handle.
scrubber-timetext-on-mediatext-on-mediaScrubber: timecodes.
scrubber-markerbg-mediabg-mediaScrubber: chapter marks cut into the track.

Core · Media

Caption

Caption is one subtitle line drawn over video.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
One subtitle line over an episode or a short, at most two lines, centred above the player controls.
Not for
  • The quiet line that explains a heading or a fieldText
  • The caption tracks of a cut in the editorTimeline
  • A planned line with its speaker and translationDialogueLine

Guidelines

  • On by default for every episode and short; the viewer turns it off with the captions toggle.
  • size="lg" (24px) in the desktop player, md (16px) on phone and in shorts.
  • At most two lines, centred, above the player controls.

Props

Prop
Type
What it does
size
'lg' | 'md'
childrenrequired
ReactNode
className
string
TypeScript
export interface CaptionProps {
  size?: 'lg' | 'md';
  children: ReactNode;
  className?: string;
}
TokenLightDarkUse
caption-bgbg-scrim-captionbg-scrim-captionCaption: box behind a subtitle line.
caption-fgtext-on-mediatext-on-mediaCaption: subtitle glyphs.

Core · Media

AudioPlayer

A voice line or sound clip: a round play button, a Waveform you can click, drag or arrow through to seek, and the time. Use it wherever a character's voice or an audio take is heard. Listening only: voice settings live in casting, never next to the player.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
Hearing a voice line or an audio take: a round play button, a waveform to seek through, and the time.
Not for

Guidelines

The waveform is the core Waveform: bars 2px wide with 2px gaps, never thicker, as many as fit. The played colour sweeps through them linearly as value advances; dragging seeks with no easing. The play button sinks on press and its icon swaps in.

Example

<Sakuga.AudioPlayer title="His voice" subtitle="Cast as Shounen Hero" duration={4} value={2}>
  <Sakuga.Button variant="ghost" size="sm" icon="mic">Recast</Sakuga.Button>
</Sakuga.AudioPlayer>
<Sakuga.AudioPlayer size="sm" duration={4} value={0} />

Props

Prop
Type
What it does
title
string
With a title the play button grows to 48 and the waveform sits under the titles.
subtitle
string
As title.
durationrequired
number
Played part of the waveform is the accent.
value
number
As duration.
playing
boolean
onToggle
(playing: boolean) => void
onSeek
(seconds: number) => void
peaks
number[]
Real loudness, resampled to the bars that fit; without it a calm, even shape is drawn from seed.
bars
number
seed
number
size
'sm' | 'md'
sm for cards and rows.
variant
'default' | 'media'
media on dark art.
className
string
style
Record<string, string | number>
children
unknown
Trailing actions beside the title (Recast, a menu).
TypeScript
export interface AudioPlayerProps {
  title?: string;
  subtitle?: string;
  duration: number;
  value?: number;
  playing?: boolean;
  onToggle?: (playing: boolean) => void;
  onSeek?: (seconds: number) => void;
  peaks?: number[];
  bars?: number;
  seed?: number;
  size?: 'sm' | 'md';
  variant?: 'default' | 'media';
  className?: string;
  style?: Record<string, string | number>;
  children?: unknown;
}

Tokens: --audio-play-bg, --audio-play-fg, --audio-wave, --audio-wave-played, --audio-time.

Core · Media

Waveform

Waveform draws a sound as bars: a voice line, a take, a track in the cut, a generated sound option. It is the one drawing every sound in Sakuga uses: AudioPlayer is a play button, a Waveform and the time, and Timeline draws its audio, dialogue and music tracks with it.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
A sound drawn as bars: a voice line, a take, a track in the cut, a generated sound option (selected on the one in use).
Not for

Guidelines

  • Bars are 2px wide with 2px gaps and never thicker; as many as fit the width. peaks (0–1) is the real loudness, resampled to those bars; without it a calm, even shape is drawn from seed. bars fixes the count.
  • value of duration (seconds) puts the played part in the accent. It is one clipped layer, so playback sweeps through the bars linearly.
  • onSeek makes it the slider, named by label: click or drag to seek (the position stays under the pointer, never eased), arrow keys move a second, Home and End jump to the ends, and the value is spoken ("1 second of 4 seconds"). Without onSeek it is a picture, and screen readers skip it.
  • selected marks the sound option in use: its bars take the accent on motion-fast.
  • size md (28px) or sm (20px); variant="media" on dark art.
  • Tokens: --waveform-bar, --waveform-played, --waveform-selected. AudioPlayer feeds its --audio-wave tokens in; Timeline its per-track wave tokens.

Props

Prop
Type
What it does
peaks
number[]
Real loudness, 0–1, resampled to the bars that fit.
bars
number
A fixed bar count instead of fitting the width.
seed
number
Shapes the calm placeholder drawn without peaks.
value
number
Seconds; the played part takes the accent.
duration
number
size
'sm' | 'md'
variant
'default' | 'media'
selected
boolean
The sound option in use.
onSeek
(seconds: number) => void
Makes it the slider; called with seconds.
label
string
The slider's name, e.g. "Position in His voice".
className
string
style
Record<string, string | number>
TypeScript
/** A voice or sound clip: play, a fine waveform you can seek, and the time. Title and subtitle are optional; children render as trailing actions beside the title. A sound as 2px bars with 2px gaps across its width: AudioPlayer, Timeline's sound tracks and sound options draw with it. A slider only with onSeek. */
export interface WaveformProps {
  /** Real loudness, 0–1, resampled to the bars that fit. */
  peaks?: number[];
  /** A fixed bar count instead of fitting the width. */
  bars?: number;
  /** Shapes the calm placeholder drawn without peaks. */
  seed?: number;
  /** Seconds; the played part takes the accent. */
  value?: number;
  duration?: number;
  size?: 'sm' | 'md';
  variant?: 'default' | 'media';
  /** The sound option in use. */
  selected?: boolean;
  /** Makes it the slider; called with seconds. */
  onSeek?: (seconds: number) => void;
  /** The slider's name, e.g. "Position in His voice". */
  label?: string;
  className?: string;
  style?: Record<string, string | number>;
}

Core · Media

Timeline

Timeline is the cut's tracks under the preview, shared by every production (episode or short, Quick or Directed).

@sakuga/uiv1.0Used onWeb

Use it for
The cut's tracks under the preview: video, boards, overlays, sound, captions and effects in time, with the playhead and trim grips on the selected item.
Not for
  • The whole cut at a glance, in planned proportionProgramBar
  • What gets generated for a cut on a frame gridTimesheet
  • A manga page's layersLayerStrip
  • The playhead of an episode a viewer watchesScrubber

Guidelines

  • duration and every item's start and length are in seconds. The ruler steps itself (1, 5 or 30 s) unless step is set; rate names the frame rate shown at the ruler's head ("24 fps").
  • Tracks have a kind: video (a picture strip), board (storyboard panels standing in for video), overlay (text and images), audio (waveform), caption (one language per track), effect.
  • Item state: selected, gap (planned time with no picture yet; never hide it), board, rejected.
  • A selected item shows a grip at each end: drag one to trim the item, so its time shrinks or grows from that end. While trimming, pass trimmed ({ start, end } in seconds) to draw the cut-off footage as a dashed ghost beyond the item.
  • markers name points in time (a scene start, a beat). playhead draws the current time across every track.
  • It never snaps item boundaries on its own: start and length are the author's. While you drag, an edge snaps to another item or the playhead and a line shows where.
  • reorderable gives every track head a drag handle: the order of titles and images decides what draws in front (the picture is always at the back). While a track is moved, pass moving ({ track, to }, indexes) to lift it and draw the line where it lands.
  • offset (seconds) labels the ruler in program time when the timeline shows a zoomed window of a longer cut.
  • Track kinds dialogue, music and audio (sound) draw the core Waveform at the item's width, in their own wave tokens; pass an item's peaks for its real loudness. Tracks are told apart by their heads (icon and name), not by colour; the accent marks only the selection and the playhead.

Props

Prop
Type
What it does
durationrequired
number
playhead
number
offset
number
step
number
rate
string
label
string
markers
{ at: number; label: string }[]
reorderable
boolean
Track heads carry a drag handle.
moving
{ track: number; to: number }
A track being moved: its index and where it lands.
tracksrequired
{ label: string; kind: 'video' | 'board' | 'overlay' | 'audio' | 'caption' | 'effect'; items: { start: number; length: number; label?: string; image?: string; /** Sound tracks: real loudness, 0–1. */ peaks?: number[]; state?: 'selected' | 'gap' | 'board' | 'rejected'; /** Seconds trimmed off while trimming, drawn as dashed ghosts before and after the item. A selected item shows a grip at each end. */ trimmed?: { start?: number; end?: number } }[] }[]
TypeScript
export interface TimelineProps {
  duration: number;
  playhead?: number;
  offset?: number;
  step?: number;
  rate?: string;
  label?: string;
  markers?: { at: number; label: string }[];
  /** Track heads carry a drag handle. */
  reorderable?: boolean;
  /** A track being moved: its index and where it lands. */
  moving?: { track: number; to: number };
  tracks: { label: string; kind: 'video' | 'board' | 'overlay' | 'audio' | 'caption' | 'effect'; items: { start: number; length: number; label?: string; image?: string; /** Sound tracks: real loudness, 0–1. */ peaks?: number[]; state?: 'selected' | 'gap' | 'board' | 'rejected'; /** Seconds trimmed off while trimming, drawn as dashed ghosts before and after the item. A selected item shows a grip at each end. */ trimmed?: { start?: number; end?: number } }[] }[];
}
TokenLightDarkUse
timeline-bgbg-surfacebg-surfaceTimeline: ground.
timeline-borderborder-hairlineborder-hairlineTimeline: rules between rows and the ruler.
timeline-labeltext-mutedtext-mutedTimeline: track names and text items.
timeline-rulertext-mutedtext-mutedTimeline: time ticks.
timeline-marker-bgaccent-subtleaccent-subtleTimeline: a named marker.
timeline-marker-fgtext-accenttext-accentTimeline: marker label.
timeline-item-bgbg-surface-raisedbg-surface-raisedTimeline: a video item without picture.
timeline-item-borderborder-dividerborder-dividerTimeline: item outline.
timeline-audio-bgbg-fill-subtlebg-fill-subtleTimeline: an audio item.
timeline-wavetext-mutedtext-mutedTimeline: audio waveform bars.
timeline-text-bgbg-fill-subtlebg-fill-subtleTimeline: caption and overlay items.
timeline-effect-bgaccent-subtleaccent-subtleTimeline: an effect item.
timeline-selectedaccentaccentTimeline: the selected item.
timeline-gap-borderborder-placeholderborder-placeholderTimeline: a planned gap with no picture yet.
timeline-label-bgbg-scrim-labelbg-scrim-labelTimeline: label plate over a picture.
timeline-label-fgtext-on-mediatext-on-mediaTimeline: label over a picture.
timeline-playheadaccentaccentTimeline: the playhead.
timeline-video-edgeborder-dividerborder-dividerTimeline: the edge of a video item.
timeline-dialogue-bgbg-fill-subtlebg-fill-subtleTimeline: a line of dialogue.
timeline-music-bgbg-fill-subtlebg-fill-subtleTimeline: music.
timeline-audio-wavetext-mutedtext-mutedTimeline: sound waveform.
timeline-dialogue-wavetext-secondarytext-secondaryTimeline: dialogue waveform.
timeline-music-wavetext-mutedtext-mutedTimeline: music waveform.
timeline-caption-fgtext-secondarytext-secondaryTimeline: caption text.
timeline-effect-fgtext-secondarytext-secondaryTimeline: effect name.
tl-lift-shadowblack-a50black-a50Timeline: the shadow under a track being moved.

Core · Containers

Panel

Panel is a Studio card: an AniCanon icon, a title, one line that explains it, an optional action, then its controls.

@sakuga/uiv1.0Used onWeb

Use it for
A Studio card that explains itself: an AniCanon icon, a title, a one-line subtitle, an optional action, then its controls.
Not for

Guidelines

  • Every Panel explains itself in its subtitle ("Core shot decisions, with a technique library behind them").
  • The header icon comes from the AniCanon set and takes panel-icon.
  • Put collapsed settings in rows below Panels rather than opening every Panel at once.
  • The action may be a component: pass it in slot="action" (a close button, Remove).
  • selected marks the one picked among several panels (a take chosen to cast).

Props

Prop
Type
What it does
icon
IconName
title
string
subtitle
string
action
ReactNode
selected
boolean
One choice among several (a take picked).
children
ReactNode
Or slot=\"action\".
className
string
TypeScript
/** A Studio card: icon, title, one explaining line, an optional action, then its content. */
export interface PanelProps {
  icon?: IconName;
  title?: string;
  subtitle?: string;
  action?: ReactNode;
  /** One choice among several (a take picked). */
  selected?: boolean;
  /** Or slot=\"action\". */
  children?: ReactNode;
  className?: string;
}
TokenLightDarkUse
panel-bgbg-surfacebg-surfacePanel: card surface in the Studio.
panel-borderborder-hairlineborder-hairlinePanel: card edge.
panel-iconaccentaccentPanel: header icon.
panel-titletext-primarytext-primaryPanel: header title.
panel-subtitletext-mutedtext-mutedPanel: header line under the title.

Core · Containers

Sheet

Sheet is the bottom panel on phones for choices that belong to the current screen: audio and subtitles, reading direction.

@sakuga/uiv1.0Used oniOS · Android

Use it for
Choices that belong to the current phone screen: audio and subtitles, reading direction, a row's menu.
Not for

Guidelines

  • It slides up from the bottom edge on motion-slow with ease-enter, over a dim sheet-scrim, with a grab handle and a Done button. Tapping the scrim closes it; it slides back down on motion-base with ease-exit.
  • The selected option's fill slides within its group, and its check settles in.
  • Group options with Sheet.Group (a short heading) and Sheet.Option (one choice, check on the selected one).
  • media over video or manga pages, so it stays dark in either theme.
  • Desktop uses a popover menu for the same choices, not a sheet.
  • An option can hold a lead (a CastAvatar, a Thumb) and two lines of Text; the row grows past its 48 px when it needs to. doneLabel={null} drops Done when an option or a button below is the action.

Props

SheetProps

Prop
Type
What it does
openrequired
boolean
titlerequired
string
onClose
() => void
media
boolean
doneLabel
string | null
childrenrequired
ReactNode
className
string

SheetGroupProps

Prop
Type
What it does
titlerequired
string
childrenrequired
ReactNode

SheetOptionProps · Holds a lead (CastAvatar, Thumb) and one or two lines; grows past 48 px when it needs to.

Prop
Type
What it does
selected
boolean
onSelect
MouseEventHandler
childrenrequired
ReactNode
TypeScript
export interface SheetProps {
  open: boolean;
  title: string;
  onClose?: () => void;
  media?: boolean;
  doneLabel?: string | null;
  children: ReactNode;
  className?: string;
}

export interface SheetGroupProps {
  title: string;
  children: ReactNode;
}

/** Holds a lead (CastAvatar, Thumb) and one or two lines; grows past 48 px when it needs to. */
export interface SheetOptionProps {
  selected?: boolean;
  onSelect?: MouseEventHandler;
  children: ReactNode;
}
TokenLightDarkUse
sheet-bgbg-glass-strongbg-glass-strongSheet: the sliding panel over the page.
sheet-media-bgbg-glass-media-strongbg-glass-media-strongSheet media: the panel over video.
sheet-handleborder-controlborder-controlSheet: the grab handle.
sheet-scrimbg-overlaybg-overlaySheet: the dim layer behind it.
sheet-row-selected-bgbg-fill-selectedbg-fill-selectedSheet: the chosen row.
sheet-checkaccentaccentSheet: the check beside the chosen row.
sheet-media-done-bgaction-mediaaction-mediaSheet media: the Done button over video.
sheet-media-done-fgtext-on-action-mediatext-on-action-mediaSheet media: Done label.
sheet-fgtext-primarytext-primarySheet: row names.
sheet-titletext-mutedtext-mutedSheet: group headings.
sheet-borderborder-glassborder-glassSheet: panel edge.
sheet-media-fgtext-on-mediatext-on-mediaSheet media: row names over video.
sheet-media-titletext-on-media-mutedtext-on-media-mutedSheet media: group headings over video.
sheet-media-borderborder-glass-mediaborder-glass-mediaSheet media: panel edge.

Core · Containers

Dialog

Dialog is a desktop window over the page: pickers (references, opening frame, video plan), confirmations (story conflicts) and last checks (clip options).

@sakuga/uiv1.0Used onWeb

Use it for
A task or a check that needs the page's whole attention on desktop: choosing references, a story conflict, the last check before something is spent or deleted.
Not for
  • The same task on a phoneSheet
  • A menu or a hover card anchored to what opened itPopover
  • Choosing from a searchable list under a fieldPicker
  • A confirmation that needs no answerToast

Guidelines

  • Title says the task; description says what happens and what it costs, if anything.
  • The main action sits right in the footer, labelled with what it does ("Add 2 references", "Generate clip").
  • It settles in on motion-slow with ease-enter and leaves on motion-base with ease-exit.
  • Phones use Sheet instead.
  • The footer's buttons are footer, or children in slot="footer".

Props

Prop
Type
What it does
openrequired
boolean
titlerequired
string
description
string
footer
ReactNode
width
number | string
onClose
() => void
children
ReactNode
className
string
TypeScript
/** A desktop dialog over the page (pickers, confirmations). Phones use Sheet. Slot: slot="footer" (the same as `footer`). */
export interface DialogProps {
  open: boolean;
  title: string;
  description?: string;
  footer?: ReactNode;
  width?: number | string;
  onClose?: () => void;
  children?: ReactNode;
  className?: string;
}
TokenLightDarkUse
dialog-bgbg-surfacebg-surfaceDialog: panel.
dialog-borderborder-dividerborder-dividerDialog: panel edge.
dialog-scrimbg-overlaybg-overlayDialog: dim layer behind it.
dialog-titletext-primarytext-primaryDialog: title.
dialog-descriptiontext-mutedtext-mutedDialog: line under the title.

Core · Containers

Popover

Popover is a raised surface that floats over the page: a menu, a hover card, a small form.

@sakuga/uiv1.0Used onWeb

Use it for
A raised surface anchored to what opened it: a menu, a hover card on a mention, a small form.
Not for
  • A list of options with searchPicker
  • A task that needs the page's whole attentionDialog
  • The same choices on a phoneSheet

Guidelines

  • anchored (default) pins it under its field: wrap the field in a relative container and put the Popover inside. Pass anchored={false} and a style with a position to place it yourself, as for a hover card on a mention.
  • Content is yours. For a list of choices use Picker, which is a Popover with search and options.
  • One Popover open at a time. Escape and a click outside close it.
  • It drops from its field on motion-fast with ease-enter. open={false} plays its exit (motion-instant, ease-exit) and then removes it.

Props

Prop
Type
What it does
open
boolean
false plays its exit, then it unmounts. Default true.
anchored
boolean
label
string
role
string
style
Record<string, string | number>
children
ReactNode
TypeScript
export interface PopoverProps {
  /** false plays its exit, then it unmounts. Default true. */
  open?: boolean;
  anchored?: boolean;
  label?: string;
  role?: string;
  style?: Record<string, string | number>;
  children?: ReactNode;
}
TokenLightDarkUse
popover-bgbg-surface-raisedbg-surface-raisedPopover and Picker: surface.
popover-borderborder-dividerborder-dividerPopover and Picker: outline.

Core · Containers

Picker

Picker is the anchored chooser under a field: optional search, then options in groups.

@sakuga/uiv1.0Used onWeb

Use it for
Choosing one thing from many under a field, with search and groups: a cast member, a place, an event.
Not for
  • A short list of plain valuesSelect
  • Options that each need an explanation or a priceOptionTiles
  • The same choice on a phoneSheet

Guidelines

  • Each option has a label, an optional hint, and one picture: image (place art, or a portrait with shape: 'round'), initial, glyph (an event type) or icon.
  • selected shows the check; dim is for an option that exists but is out of reach.
  • Group titles say why the options are together ("Already at Sé station"). Put suggestions from the story above plain search results.
  • open works as on Popover: it drops in, and plays its exit before it goes.
  • Extra controls that refine the choice (how two events connect) go as children, after the groups.

Props

PickerOption

Prop
Type
What it does
labelrequired
string
hint
string
image
string
shape
'round'
initial
string
glyph
'event' | 'twist' | 'revelation' | 'clue' | 'goal'
icon
IconName
selected
boolean
dim
boolean

PickerProps

Prop
Type
What it does
open
boolean
As on Popover.
label
string
search
{ placeholder?: string; value?: string }
groupsrequired
{ title?: string; options: PickerOption[] }[]
anchored
boolean
style
Record<string, string | number>
children
ReactNode
TypeScript
export interface PickerOption {
  label: string;
  hint?: string;
  image?: string;
  shape?: 'round';
  initial?: string;
  glyph?: 'event' | 'twist' | 'revelation' | 'clue' | 'goal';
  icon?: IconName;
  selected?: boolean;
  dim?: boolean;
}

export interface PickerProps {
  /** As on Popover. */
  open?: boolean;
  label?: string;
  search?: { placeholder?: string; value?: string };
  groups: { title?: string; options: PickerOption[] }[];
  anchored?: boolean;
  style?: Record<string, string | number>;
  children?: ReactNode;
}
TokenLightDarkUse
popover-fgtext-primarytext-primaryPicker: option label.
popover-mutedtext-mutedtext-mutedPicker: hints, icons and dim options.
popover-grouptext-mutedtext-mutedPicker: group title.
popover-slot-bgbg-fill-subtlebg-fill-subtlePicker: initials slot.
popover-selected-bgaccent-subtleaccent-subtlePicker: the selected option.
popover-checktext-accenttext-accentPicker: check on the selected option.

Core · Containers

StatusBar

StatusBar is the slim bar at the foot of a working surface such as the writing room: where the work sits, whether it is saved, how long it is.

@sakuga/uiv1.0Used onWeb

Use it for
The slim bar at the foot of a working surface such as the writing room: where the work sits, whether it is saved, how long it is.
Not for
  • Actions on the workViewHeader
  • The transport and timecode under the editor's pictureMonitorBar
  • The foot of a step in a full-screen flowFlowFooter
  • Confirming that something finishedToast

Guidelines

  • Children are the items, in order; rules separate them. One child with slot="end" sits at the far end (a cost note).
  • Keep it to facts and one control at most. Actions belong in the top bar.

Props

Prop
Type
What it does
items
ReactNode[]
end
ReactNode
children
ReactNode
TypeScript
/** Children are the items, separated by rules; a child with slot="end" sits at the far end. */
export interface StatusBarProps {
  items?: ReactNode[];
  end?: ReactNode;
  children?: ReactNode;
}
TokenLightDarkUse
statusbar-bgbg-surfacebg-surfaceStatusBar: surface.
statusbar-borderborder-hairlineborder-hairlineStatusBar: top rule.
statusbar-fgtext-mutedtext-mutedStatusBar: items.
statusbar-sepborder-dividerborder-dividerStatusBar: separators.

Core · Containers

FloatingToolbar

FloatingToolbar sits over a selection and offers what can be done with it.

@sakuga/uiv1.0Used onWeb

Use it for
What can be done with a selection, two or three ghost buttons just above it: Make it an event, Ask Maki.
Not for

Guidelines

  • Two or three ghost buttons at most ("Make it an event", Ask Maki). Position it with style just above the selection.
  • It disappears when the selection does.

Props

Prop
Type
What it does
label
string
style
Record<string, string | number>
childrenrequired
ReactNode
TypeScript
export interface FloatingToolbarProps {
  label?: string;
  style?: Record<string, string | number>;
  children: ReactNode;
}
TokenLightDarkUse
toolbar-bgbg-surface-raisedbg-surface-raisedFloatingToolbar: surface.
toolbar-borderborder-dividerborder-dividerFloatingToolbar: outline.

Core · Foundations

Icon

Icon draws one glyph from AniCanon's outline icon set: 24px grid, 2px stroke, round caps and joins, currentColor.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
One glyph from AniCanon's outline set inside a control or beside words: a field's leading glyph, a tab bar item, a Studio panel's header.
Not for

Guidelines

  • Pass name from the set; the preview lists every name.
  • Size 18–22 inside controls, 22 in the tab bar. The glyph sits inside a 44px target, never as the target.
  • An Icon is decorative (aria-hidden) unless you pass title. When an icon is the only content of a control, name the control instead (IconButton label).
  • Colour comes from the parent's color. icon-fg is the default.
  • play and pause are filled shapes; every other icon is a stroke.
  • heart-filled is the liked state of heart. captions, volume-off, clock and sliders cover player and reader controls.
  • The set now holds AniCanon's own drawn icons, copied exactly from the portal: shot, angle, depth, movement, composition, lens, mood, lighting, weather, effects, frames, and the registry icons (camera, characters, backdrops, collection, folder, video…). auto marks every Auto option, where the director chooses for you: four framing marks around the subject, like a viewfinder. It is never a sparkle.
  • Character pickers use filled silhouettes that follow the world's morphology: human (body-*, height-*, age-*, figure-*), anthro (anthro-*) and creature (creature-age-*, creature-build-*, creature-size-*). Pick the set from the project's morphology, never mix them. gender-*, faction-* and trait-* are shared.

Props

StudioIconNameone of 88 names (listed in the TypeScript)
PickerIconNameone of 79 names (listed in the TypeScript)
IconNameone of 54 names (listed in the TypeScript)

IconProps

Prop
Type
What it does
namerequired
IconName
size
number
title
string
strokeWidth
number
className
string
TypeScript
/** One name from the AniCanon outline icon set (24px grid, 2px stroke). */
/** AniCanon icon names (cinematography and registry sets), plus the outline glyphs below. */
export type StudioIconName = "angle-birds-eye" | "angle-dutch" | "angle-eye-level" | "angle-worms-eye" | "aspect" | "auto" | "backdrops" | "bolt" | "camera" | "characters" | "collection" | "composition-centered" | "composition-foreground-frame" | "composition-layered-depth" | "composition-leading-lines" | "composition-negative-space" | "composition-thirds" | "depth-balanced" | "depth-deep" | "depth-shallow" | "design-system" | "document" | "effect-fog" | "effect-lens-flare" | "effect-motion-blur" | "effect-particles" | "effect-rain" | "effect-sparks" | "folder" | "frame-cinematic" | "frame-classic" | "frame-portrait" | "frame-square" | "image" | "info" | "lens-anamorphic" | "lens-handheld" | "lens-macro-detail" | "lens-telephoto" | "lens-wide" | "lighting-backlit" | "lighting-cold" | "lighting-contrast" | "lighting-golden" | "lighting-neon" | "lighting-rim" | "lighting-soft" | "mood-chaotic" | "mood-comedic" | "mood-cozy" | "mood-epic" | "mood-melancholic" | "mood-mysterious" | "mood-romantic" | "mood-tense" | "movement-arc-around" | "movement-crane-up" | "movement-dolly-in" | "movement-dolly-out" | "movement-pan-left" | "movement-pan-right" | "movement-slow-pan" | "movement-static-camera" | "movement-tracking-shot" | "shot-closeup" | "shot-extreme" | "shot-extreme-wide" | "shot-full" | "shot-high" | "shot-low" | "shot-medium" | "shot-medium-closeup" | "shot-ots" | "shot-wide" | "sun" | "trash" | "upload" | "video" | "warning" | "weather-clear" | "weather-fog" | "weather-haze" | "weather-heavy-rain" | "weather-overcast" | "weather-rain" | "weather-snow" | "weather-storm" | "weather-wind";

/** Character picker silhouettes: human, anthro, creature, figure, gender, faction, traits. */
export type PickerIconName = "age-adult" | "age-baby" | "age-child" | "age-elder" | "age-teen" | "anthro-age-adult" | "anthro-age-baby" | "anthro-age-child" | "anthro-age-elder" | "anthro-age-teen" | "anthro-body-athletic" | "anthro-body-average" | "anthro-body-curvy" | "anthro-body-fit" | "anthro-body-muscular" | "anthro-body-petite" | "anthro-body-slim" | "anthro-height-average" | "anthro-height-short" | "anthro-height-tall" | "anthro-height-very-short" | "anthro-height-very-tall" | "body-athletic" | "body-average" | "body-curvy" | "body-fit" | "body-muscular" | "body-petite" | "body-slim" | "creature-age-adult" | "creature-age-elder" | "creature-age-young" | "creature-build-lean" | "creature-build-massive" | "creature-build-powerful" | "creature-build-sleek" | "creature-build-stocky" | "creature-size-large" | "creature-size-medium" | "creature-size-small" | "creature-size-tiny" | "creature-size-towering" | "faction-hero" | "faction-independent" | "faction-neutral" | "faction-villain" | "figure-flat" | "figure-full" | "figure-large" | "figure-medium" | "figure-small" | "gender-female" | "gender-male" | "gender-non-binary" | "height-average" | "height-short" | "height-tall" | "height-very-short" | "height-very-tall" | "trait-bold" | "trait-brooding" | "trait-calm" | "trait-charismatic" | "trait-confident" | "trait-energetic" | "trait-fierce" | "trait-gentle" | "trait-introverted" | "trait-kind" | "trait-leader" | "trait-mischievous" | "trait-mysterious" | "trait-playful" | "trait-serious" | "trait-shy" | "trait-stern" | "trait-stoic" | "trait-wise" | "trait-witty";

export type IconName = StudioIconName | PickerIconName | 'play' | 'pause' | 'more' | 'eye' | 'edit' | 'mic' | 'occasion-everyday' | 'occasion-uniform' | 'occasion-athletic' | 'occasion-formal' | 'occasion-festival' | 'occasion-swimwear' | 'occasion-combat' | 'occasion-sleepwear' | 'occasion-winter' | 'occasion-hanger' | 'search' | 'back' | 'forward' | 'chevron-up' | 'chevron-down' | 'share' | 'plus' | 'bell' | 'heart' | 'comment' | 'bookmark' | 'home' | 'shorts' | 'manga' | 'you' | 'language' | 'settings' | 'fullscreen' | 'skip-back' | 'skip-forward' | 'volume' | 'close' | 'place' | 'check' | 'clock' | 'volume-off' | 'captions' | 'sliders' | 'heart-filled' | 'image' | 'template' | 'film' | 'palette' | 'calendar' | 'link' | 'users';

export interface IconProps {
  name: IconName;
  size?: number;
  title?: string;
  strokeWidth?: number;
  className?: string;
}
TokenLightDarkUse
icon-fgtext-primarytext-primaryIcon: default glyph colour; icons follow currentColor.

Core · Foundations

Text

Text sets words in Sakuga's type scale: a variant names the style, tone changes only its colour.

@sakuga/uiv1.0Used onWeb · iOS · Android

Use it for
Words on a board or in a component, set in Sakuga's type scale: titles, body, the caption line that explains, a field's name, an eyebrow.
Not for
  • A subtitle line over videoCaption
  • An action inside a line of textTextLink
  • A character or a place named inside proseMention
  • A proposed change in the words, the old struck and the new underlinedRevision
  • The heading that opens a shelfSectionHeader

Guidelines

  • title-1 to title-4 (Funnel Display, 30 to 17 px), body (14/20), label (14/20 semibold), caption (13/18, the quiet line that explains), small (12/16), micro (12/16 bold, a field's name) and eyebrow (10.5/14 capitals, what kind of thing it is).
  • tone: primary, secondary, muted, accent, and on-media or on-media-muted over a picture. Bold words inside body, caption and small read in the strong colour.
  • With no variant it only sets a tone, inside other text. numeric lines figures up (timecodes, seconds).
  • mark highlights the words a check flagged inside a sentence (the part of a prompt the story contradicts), or the words a person selected. Writing read by Maki takes a kind: mark="stated" (said in the text: a quiet fill), mark="inferred" (read between the lines: a dotted underline); a conflict keeps the plain mark.
  • as picks the element; titles default to headings, body and caption to paragraphs, the rest to spans.
  • Never set type by hand on a board or in a component: if a size is missing from the scale, it is missing from Sakuga.
  • display (64 px) is a name that is the whole page, as a character's; lead (18/28) is the paragraph that introduces it. One of each per page at most.

Props

Prop
Type
What it does
variant
'display' | 'title-1' | 'title-2' | 'title-3' | 'title-4' | 'lead' | 'body' | 'label' | 'caption' | 'small' | 'micro' | 'eyebrow'
display: a name that is the page (64 px); lead: the paragraph that introduces it (18/28).
tone
'primary' | 'secondary' | 'muted' | 'accent' | 'on-media' | 'on-media-muted'
numeric
boolean
Tabular figures.
mark
boolean | 'stated' | 'inferred'
Highlights the words: what a check flagged or a selection (true); what writing states or implies ('stated', 'inferred').
as
string
id
string
children
ReactNode
className
string
TypeScript
/** A type style from the scale; tone changes only the colour. With no variant it only sets a tone, inside other text. */
export interface TextProps {
  /** display: a name that is the page (64 px); lead: the paragraph that introduces it (18/28). */
  variant?: 'display' | 'title-1' | 'title-2' | 'title-3' | 'title-4' | 'lead' | 'body' | 'label' | 'caption' | 'small' | 'micro' | 'eyebrow';
  tone?: 'primary' | 'secondary' | 'muted' | 'accent' | 'on-media' | 'on-media-muted';
  /** Tabular figures. */
  numeric?: boolean;
  /** Highlights the words: what a check flagged or a selection (true); what writing states or implies ('stated', 'inferred'). */
  mark?: boolean | 'stated' | 'inferred';
  as?: string;
  id?: string;
  children?: ReactNode;
  className?: string;
}
TokenLightDarkUse
txt-primarytext-primarytext-primaryText: titles, body and labels.
txt-secondarytext-secondarytext-secondaryText: captions.
txt-mutedtext-mutedtext-mutedText: small lines, micro labels and eyebrows.
txt-strongtext-primarytext-primaryText: bold words inside body, captions and small lines.
txt-accenttext-accenttext-accentText: the accent tone, for a number or a mention.
txt-on-mediatext-on-mediatext-on-mediaText: over a picture or the dark stage.
txt-on-media-mutedtext-on-media-mutedtext-on-media-mutedText: a quiet line over a picture or the dark stage.

AniCanonKit · AniCanon

FragmentSoul

FragmentSoul is the Fragments currency mark: a hitodama spirit-flame in flat cel layers with a deep violet ink line.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
The mark every fragment amount carries: bare beside a balance figure, or seated in a medallion on its tinted disc.
Not for
  • The balance with its amount, in a top bar, the sidebar or a sentenceFragmentBalance
  • The price on the action that spends itButton
  • The price on a choice cardOptionTiles

Guidelines

  • Its colours are fixed artwork (spirit violet, magenta, pink, a hot core). Never recolour, outline or put it on a gradient.
  • Bare, it sits beside a balance figure. In a medallion, seat it on a disc tinted with fragments-rim.
  • The balance card is the soul-vault: fragments-vault-bg, a fragments-rim edge, a flat dot-screentone watermark, a faint large flame bleeding off one corner, and the pink fragments-eyebrow. No gradient washes.
  • dim shows a held or dormant balance.

Props

Prop
Type
What it does
size
number
dim
boolean
className
string
TypeScript
/** The Fragments currency mark (soul-flame). Fixed artwork colours; never recolour. */
export interface FragmentSoulProps {
  size?: number;
  dim?: boolean;
  className?: string;
}

AniCanonKit · AniCanon

FragmentBalance

The Fragments balance in its places: a pill in top bars, a block in the sidebar, a held line while a generation is running, inline: the mark and an amount inside a sentence ("~520 is held when you start"), and card: the balance as a wallet card. Built only from core components and tokens; it lives in AniCanonKit, not the core.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
The Fragments balance in its places: a pill in a top bar, a block in the sidebar, a hold while a generation runs, an amount inside a sentence ("~520 is held when you start"), and the wallet card that leads the Fragments page on desktop and phone.
Not for

Guidelines

card is the vault at wallet size, in a bank card's proportions (1.586:1, the ISO ID-1 card, with its small corner), for the Fragments screen on phone and desktop. It is also the balance at stake on the app-consent page. The balance is its headline, title (the plan) sits in the corner, held and text run along the foot, and image rests in the far corner behind nothing but the card. It keeps its dark vault in both themes, like a card in a wallet. Use it once per screen, at the top. Say the promise on it: "available · Fragments never expire" goes in caption.

Props

Prop
Type
What it does
variant
'pill' | 'block' | 'held' | 'inline' | 'card'
amountrequired
string
caption
string
title
string
As caption.
text
string
As caption.
held
string
card: what jobs in progress hold, read along the foot with the mark ("520" → 520 held).
image
string
card: the plan's own art, resting in the far corner.
TypeScript
/** inline: the mark and the amount inside a sentence, e.g. a price or a hold. card: the balance as a wallet card (it leads the Fragments page on desktop and phone): title is the plan, held what jobs in progress hold, image the plan's own art. */
export interface FragmentBalanceProps {
  variant?: 'pill' | 'block' | 'held' | 'inline' | 'card';
  amount: string;
  caption?: string;
  title?: string;
  text?: string;
  held?: string;
  image?: string;
}
TokenLightDarkUse
fragments-vault-bgspirit-vaultspirit-vaultFragments balance card: the soul-vault surface, dark in both themes.
fragments-rimspirit-500spirit-500Fragments balance card: violet rim and the dot-screentone watermark.
fragments-eyebrowaccentaccentFragments balance card: the pink FRAGMENTS eyebrow.
fragments-amounttext-on-mediatext-on-mediaFragments balance card: the balance figure.
fragments-captiontext-on-media-mutedtext-on-media-mutedFragments balance card: secondary lines.

AniCanonKit · AniCanon

AniCanon Loader

AniCanon's loader is its mark: the ring traces once around in the accent while the crescent and the moon dot stay exactly where the logo has them. Never move the crescent or the moon dot. Use it on AniCanon's own surfaces where something is generating: card status bars, Activity rows, look plates, stages. Built only from core tokens; it lives in AniCanonKit, not the core, whose Loader is Sakuga's own.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
Generation on AniCanon's own surfaces: a card's status bar, an Activity row, a look plate being drawn, the running stage.
Not for
  • Plain loading in a drop zone, a frame or a buttonLoader
  • Work whose share done is knownProgress
  • A job's stages in orderStages

Guidelines

AniCanon Loader

On media surfaces the mark follows the text colour, so it stays light on dark art.

Reduced motion: the arc stops and the mark stays visible as a static logo.

Sakuga.Loader used to draw this mark; it is now the core loader. Boards that should show AniCanon's mark use AniCanonKit.Loader.

Example

<AniCanonKit.Loader size={16} label="Drawing the portrait" />

Props

Prop
Type
What it does
size
number
Pixel size. Reads down to 14. Default 24
label
string
Announced to screen readers. Default "Loading"
className
string
style
Record<string, string | number>
TypeScript
/** AniCanon's loader, the mark: its ring traces in the accent; the crescent and moon dot never move. */
export interface LoaderProps {
  size?: number;
  label?: string;
  className?: string;
  style?: Record<string, string | number>;
}

Tokens: --loader-arc (accent), --loader-mark (currentColor), --loader-track-opacity (.2).

AniCanonKit · AniCanon

ActivityRow and ActivityPanel

Running and finished work: ActivityRow sits in the sidebar, ActivityPanel opens from it with groups of jobs (running with progress, needing you with an action, earlier). Built only from core components and tokens; it lives in AniCanonKit, not the core.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
Running and finished work: the row in the sidebar with its count, and the panel that opens from it with jobs running, needing you and earlier.
Not for
  • One job's stages in orderStages
  • How far one job has come, where it is being madeProgress
  • A short confirmation that something finishedToast

Props

ActivityRowProps

Prop
Type
What it does
countrequired
number
The chevron turns with expanded; the panel rises in when it opens.
line
string
As count.
expanded
boolean
As count.
onClick
MouseEventHandler
As count.

ActivityPanelProps

Prop
Type
What it does
groupsrequired
{ title: string; jobs: { title: string; text?: string; image?: string; initial?: string; progress?: number; time?: string; action?: { label: string; primary?: boolean } }[] }[]
TypeScript
export interface ActivityRowProps {
  count: number;
  line?: string;
  expanded?: boolean;
  onClick?: MouseEventHandler;
}

export interface ActivityPanelProps {
  groups: { title: string; jobs: { title: string; text?: string; image?: string; initial?: string; progress?: number; time?: string; action?: { label: string; primary?: boolean } }[] }[];
}
TokenLightDarkUse
activity-bgbg-surfacebg-surfaceKit Activity: panel.
activity-row-bgbg-fill-subtlebg-fill-subtleKit Activity: sidebar row.
activity-baraccentaccentKit Activity: job progress.
activity-grouptext-mutedtext-mutedKit Activity: group heading.

AniCanonKit · AniCanon

PremiereSlot

PremiereSlot holds the place of something that is announced but not out yet: the next episode, the next manga issue.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
Something announced but not out yet: the next episode or the next manga issue, with its premiere time ("Premieres Friday · 21:00") and Remind me.
Not for
  • An episode, short or issue that is outMediaCard
  • A frame of a production that is not made yetThumb
  • A place where there is nothing to show yetEmptyState

Guidelines

  • The dashed premiere-slot-border says "not here yet"; the premiere time is the one premiere-slot-when accent.
  • Times as day and 24-hour time: "Premieres Friday · 21:00".
  • compact in sidebars and on phone. Remind me is an outline Button; pass onRemind={null} to hide it.
  • Pass image when the release already has artwork (a series still or cover); the time moves above the title.

Props

Prop
Type
What it does
whenrequired
string
titlerequired
string
image
string
Artwork for the frame; without it the frame shows the time.
subtitle
string
compact
boolean
onRemind
MouseEventHandler | null
remindLabel
string
className
string
TypeScript
export interface PremiereSlotProps {
  when: string;
  title: string;
  /** Artwork for the frame; without it the frame shows the time. */
  image?: string;
  subtitle?: string;
  compact?: boolean;
  onRemind?: MouseEventHandler | null;
  remindLabel?: string;
  className?: string;
}
TokenLightDarkUse
premiere-slot-borderborder-placeholderborder-placeholderPremiereSlot: dashed edge.
premiere-slot-whentext-accenttext-accentPremiereSlot: premiere day and time.
premiere-slot-titletext-primarytext-primaryPremiereSlot: what premieres.
premiere-slot-framebg-surfacebg-surfacePremiereSlot: empty frame where the artwork will go.
premiere-slot-subtitletext-mutedtext-mutedPremiereSlot: the line under the title.

AniCanonKit · AniCanon

Stages

A job's stages in order, while it runs and after: done ones ticked with the time they took, the running one with AniCanon's loader, the rest numbered.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
A job's stages in order, while it runs and after: done ones with the time they took, the running one with AniCanon's loader, a failed one with why ("Designing the first look").
Not for

Guidelines

  • A failed stage is ringed and says why in its text, in the creator's words; the stages after it wait.
  • size="sm" on a phone.
  • Say what each stage makes ("Designing the first look"), never the model or the provider.

Props

Prop
Type
What it does
itemsrequired
{ title: string; text?: string; state?: 'done' | 'running' | 'todo' | 'failed'; time?: string }[]
size
'md' | 'sm'
label
string
className
string
TypeScript
/** A job's stages in order. */
export interface StagesProps {
  items: { title: string; text?: string; state?: 'done' | 'running' | 'todo' | 'failed'; time?: string }[];
  size?: 'md' | 'sm';
  label?: string;
  className?: string;
}

AniCanonKit · Characters

CharacterCard

A character as one card: stage (silhouette, avatar or 2:3 portrait), role, name, stat tiles, traits, an optional listen-only voice and a note. The same card in creation, editing, looks, voice and face flows. Built only from core components and tokens; it lives in AniCanonKit, not the core.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
A character as one card in every character flow (creation, editing, looks, voice and face): the stage, role, name, stats, traits and a note.
Not for

Props

Prop
Type
What it does
namerequired
string
role
string
As name.
stage
{ kind: 'icon' | 'avatar' | 'portrait'; image?: string; icon?: IconName }
stats
{ icon: IconName; value: string; label: string; changed?: boolean }[]
Drawn as CharacterStats tiles; changed rings a stat edited in this session.
traits
{ icon: IconName; label: string }[]
voice
{ duration: number; value?: number }
Listen-only player.
note
string
TypeScript
export interface CharacterCardProps {
  name: string;
  role?: string;
  stage?: { kind: 'icon' | 'avatar' | 'portrait'; image?: string; icon?: IconName };
  stats?: { icon: IconName; value: string; label: string; changed?: boolean }[];
  traits?: { icon: IconName; label: string }[];
  voice?: { duration: number; value?: number };
  note?: string;
}
TokenLightDarkUse
character-card-bgbg-surfacebg-surfaceKit CharacterCard: card.
character-card-borderborder-dividerborder-dividerKit CharacterCard: edge.
character-card-stagebg-mediabg-mediaKit CharacterCard: stage behind silhouette or avatar.
character-card-roletext-accenttext-accentKit CharacterCard: role eyebrow.

AniCanonKit · Characters

CharacterStats

Who a character is at a glance: each fact as its picker's icon, the value and what it is.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
Who a character is at a glance: each fact as its picker's icon, the value and what it is, as tiles in a card or on a phone, or as pills across the character's page.
Not for
  • Who a character is at a point in the story, with since whenFactList
  • The whole character as one cardCharacterCard
  • What a character is ready withReadiness
  • Words kept as tags, such as a visual vibeTagList

Guidelines

  • Tiles (the default) in a CharacterCard and on a phone, three across. changed rings a fact edited in this session.
  • layout="pills" lays them across a character's page, beside the portrait.
  • The icon is the same one the picker shows, from the world's morphology (human, anthro, creature).

Props

Prop
Type
What it does
statsrequired
{ icon?: IconName; value: string; label: string; changed?: boolean }[]
layout
'tiles' | 'pills'
columns
number
className
string
TypeScript
/** Characters: each fact as its picker's icon, the value and what it is. Tiles in a card or on a phone; pills across a page. */
export interface CharacterStatsProps {
  stats: { icon?: IconName; value: string; label: string; changed?: boolean }[];
  layout?: 'tiles' | 'pills';
  columns?: number;
  className?: string;
}

AniCanonKit · Characters

LookPlate

One look as a 2:3 plate on white, with its occasion. States: ready, wearing, showing, review, new, drawing, add. Built only from core components and tokens; it lives in AniCanonKit, not the core.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
One look as a 2:3 plate on white with its occasion, in each of its states: ready, wearing, drawing, waiting for review, or the plate that adds one.
Not for

Props

Prop
Type
What it does
labelrequired
string
occasion
string
As label.
image
string
As label.
state
'ready' | 'wearing' | 'showing' | 'review' | 'new' | 'drawing' | 'add'
TypeScript
export interface LookPlateProps {
  label: string;
  occasion?: string;
  image?: string;
  state?: 'ready' | 'wearing' | 'showing' | 'review' | 'new' | 'drawing' | 'add';
}
TokenLightDarkUse
look-plate-bgneutral-0neutral-0Kit LookPlate: white behind portraits and sheets, both themes.
look-plate-borderborder-hairlineborder-hairlineKit LookPlate: edge.
look-plate-currentaccentaccentKit LookPlate: the look shown or worn.
look-plate-add-borderborder-placeholderborder-placeholderKit LookPlate: add tile edge.

AniCanonKit · Characters

CastCard

A cast member on the cast page: the portrait (an initial until one is approved), the role, the name, what they have, and what is happening to them now.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
One cast member on the cast page: the portrait, role and name, what they have, what is happening to them now, and the next step at its foot.
Not for

Guidelines

  • state is a live fact about them (attention when it waits on the creator, busy while something is drawn).
  • action is the next step, across the card's foot: attention (review), draft (continue setup), busy with the loader and the time it has run.
  • One state and one action at most. CastGrid lays a faction's cast six across.

Props

Prop
Type
What it does
namerequired
string
image
string
initial
string
role
string
meta
string
href
string
state
{ label: string; tone?: 'attention' | 'busy' }
action
{ label: string; tone?: 'attention' | 'draft' | 'busy'; time?: string }
TypeScript
/** Characters: a cast member on the cast page; `state` is what is happening to them, `action` what waits on the creator. */
export interface CastCardProps {
  name: string;
  image?: string;
  initial?: string;
  role?: string;
  meta?: string;
  href?: string;
  state?: { label: string; tone?: 'attention' | 'busy' };
  action?: { label: string; tone?: 'attention' | 'draft' | 'busy'; time?: string };
}

AniCanonKit · Characters

CastGrid

A faction's cast as CastCards, six across on desktop. Pass items; each card links to the character's page. Head each grid with a SectionHeader carrying the faction's icon and count.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
A faction's cast on the cast page: its CastCards six across on desktop, each linking to the character's page.
Not for

Props

Prop
Type
What it does
itemsrequired
CastCardProps[]
columns
number
label
string
className
string
TypeScript
export interface CastGridProps {
  items: CastCardProps[];
  columns?: number;
  label?: string;
  className?: string;
}

AniCanonKit · Characters

CoverageTable

Wardrobe coverage: a row per character and a column per occasion the scripts ask for.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
Wardrobe coverage across the cast: a row per character, a column per occasion the scripts ask for, and the gaps where a scene needs a look and nothing fits.
Not for

Guidelines

  • A cell says what they have (own, "2 looks"), that a scene needs a look and nothing fits (gap, "3 scenes · nothing"), or that nothing is needed yet (idle).
  • legend names the three states over the table; summary counts the gaps and says which script they come from.
  • A gap is the only accent; nothing else is coloured.

Props

Prop
Type
What it does
occasionsrequired
string[]
rowsrequired
{ name: string; image?: string; cells: { state: 'own' | 'gap' | 'idle'; label?: string }[] }[]
legend
{ state: 'own' | 'gap' | 'idle'; label: string }[]
summary
string
rowLabel
string
label
string
className
string
TypeScript
/** Characters: wardrobe coverage, a row per character and a column per occasion. */
export interface CoverageTableProps {
  occasions: string[];
  rows: { name: string; image?: string; cells: { state: 'own' | 'gap' | 'idle'; label?: string }[] }[];
  legend?: { state: 'own' | 'gap' | 'idle'; label: string }[];
  summary?: string;
  rowLabel?: string;
  label?: string;
  className?: string;
}

AniCanonKit · Characters

Readiness

What a character is ready with, at a glance on their page: looks, voice, face, props. Each part is ticked, or marked when it waits on the creator, with a quiet line under it ("1 waiting on you", "Optional").

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
What a character is ready with, at a glance on their page: looks, voice, face and props, each ticked or marked when it waits on the creator.
Not for

Props

Prop
Type
What it does
itemsrequired
{ label: string; detail?: string; state?: 'done' | 'attention' }[]
label
string
className
string
TypeScript
/** Characters: what a character is ready with. */
export interface ReadinessProps {
  items: { label: string; detail?: string; state?: 'done' | 'attention' }[];
  label?: string;
  className?: string;
}

AniCanonKit · Characters

VoiceTiles

Voices to cast from, each a tile with its name and a few words on how it sounds ("Bright, determined"). Choosing a tile plays the voice and selects it; five across.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
Casting a voice: a tile per voice with its name and a few words on how it sounds ("Bright, determined"), where choosing one plays it and selects it.
Not for

Props

Prop
Type
What it does
optionsrequired
{ label: string; value?: string; description?: string }[]
value
string
columns
number
label
string
onChange
(value: string) => void
className
string
TypeScript
/** Characters: voices to cast from, each in a few words. */
export interface VoiceTilesProps {
  options: { label: string; value?: string; description?: string }[];
  value?: string;
  columns?: number;
  label?: string;
  onChange?: (value: string) => void;
  className?: string;
}

AniCanonKit · Story

PanelHeader

PanelHeader opens every side panel in Story with the same grammar: a small eyebrow naming what the panel shows, the title, an optional subtitle; icon actions (most common first, the rest in ⋯), then close.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
The head of every side panel in Story and of an inspector: an eyebrow, the title, an optional subtitle, icon actions, then close (none on the world panel or an inspector).
Not for

Guidelines

  • Actions go in slot="actions". A picture (a place's art, a portrait) goes in slot="media".
  • The world panel is the default and has no close (close={false}).
  • An inspector opens with it too: what is selected as the title, where it stands as the subtitle, its delete as the one action, and no close (close={false}).

Props

Prop
Type
What it does
eyebrow
string
titlerequired
string
subtitle
string
close
boolean
closeLabel
string
TypeScript
/** Slots: slot="actions" (icon buttons before close), slot="media". Every side panel's and inspector's head. Slots: slot="actions" (icon buttons before close; an inspector's delete), slot="media". */
export interface PanelHeaderProps {
  eyebrow?: string;
  title: string;
  subtitle?: string;
  close?: boolean;
  closeLabel?: string;
}
TokenLightDarkUse
panel-eyebrowtext-accenttext-accentPanelHeader: the small label naming what the panel shows.

AniCanonKit · Story

PanelSection

PanelSection is a labelled group of rows with its count on the right ("Changes 3").

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
A labelled group of rows inside a Story panel, with its count on the right ("Changes 3").
Not for
  • The head of the panel itselfPanelHeader
  • A group outside Story, such as a faction in a listSectionHeader
  • Studio's folded section with its current valuePanelRow
  • Rows outside Story that open pagesList

Guidelines

  • Rows are EntityRows. A short line after the rows goes in slot="footer".

Props

Prop
Type
What it does
labelrequired
string
count
string | number
childrenrequired
ReactNode
TypeScript
/** Slot: slot="footer". */
export interface PanelSectionProps {
  label: string;
  count?: string | number;
  children: ReactNode;
}
TokenLightDarkUse
panel-section-labeltext-mutedtext-mutedPanelSection: label and count.

AniCanonKit · Story

EntityRow

EntityRow is the one row kind on Story: a picture, the name, its state, and the work that told it as a chip on the right.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
Every row on Story: a character, a bond, a place, an event or a question, with its state and the work that told it.
Not for
  • A row outside Story, such as a take or a settingListRow
  • An open question with its planted, raised and answered stepsQuestionRow
  • Something found in writing, waiting for the personProposalCard
  • The people in a character's life beside their nameCastAvatar

Guidelines

  • visual picks the picture: a portrait (avatar), two overlapping portraits for a bond (pair), a place's art (place), an event's shape (event), a question's three-step track (question).
  • State is before/after (drawn as StateChange) or any badges in slot="state".
  • faded for something that no longer counts at this moment.

Props

Visualone of 9 names (listed in the TypeScript)

EntityRowProps · Slots: slot="visual" (a custom picture), slot="state", slot="end".

Prop
Type
What it does
visual
Visual
namerequired
string
before
string
after
string
afterVariant
'new' | 'alert'
afterIcon
IconName
beforeIcon
IconName
chip
string
faded
boolean
TypeScript
type Visual = { kind: 'avatar'; image?: string; initial?: string; size?: number } | { kind: 'pair'; images: string[] } | { kind: 'place'; image?: string; width?: number; height?: number } | { kind: 'event'; type: EventType } | { kind: 'question'; stage: 1 | 2 | 3 } | { kind: 'icon'; icon: IconName } | { kind: 'maki'; image: string };

/** Slots: slot="visual" (a custom picture), slot="state", slot="end". */
export interface EntityRowProps {
  visual?: Visual;
  name: string;
  before?: string;
  after?: string;
  afterVariant?: 'new' | 'alert';
  afterIcon?: IconName;
  beforeIcon?: IconName;
  chip?: string;
  faded?: boolean;
}
TokenLightDarkUse
entity-nametext-primarytext-primaryEntityRow: name.
entity-chip-borderborder-dividerborder-dividerEntityRow: the told-by chip outline.
entity-chip-fgtext-mutedtext-mutedEntityRow: the told-by chip label.
entity-visual-bgbg-fill-subtlebg-fill-subtleEntityRow: picture placeholder.
entity-visual-fgtext-mutedtext-mutedEntityRow: initials and icons in the picture slot.
entity-pair-gapbg-surfacebg-surfaceEntityRow: the ring between two overlapping portraits.

AniCanonKit · Story

StateChange

StateChange is a change written as before → after: two badges, the before faded.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
A change written as before and after, two badges with the before faded: a place that starts burning, a bond that turns to rivals.
Not for
  • A single state laid over artworkBadge
  • A proposed change to wordsRevision
  • The change as a whole row with its pictureEntityRow

Guidelines

  • afterVariant="alert" for a change for the worse (burning, dead, rivals). Leave before out for a state with no prior value.

Props

Prop
Type
What it does
before
string
afterrequired
string
afterVariant
'new' | 'alert'
afterIcon
IconName
beforeIcon
IconName
TypeScript
export interface StateChangeProps {
  before?: string;
  after: string;
  afterVariant?: 'new' | 'alert';
  afterIcon?: IconName;
  beforeIcon?: IconName;
}
TokenLightDarkUse
state-arrowtext-mutedtext-mutedStateChange: the arrow between before and after.

AniCanonKit · Story

EventGlyph

EventGlyph is the shape of an event type, the same everywhere a story event appears: map, scrubber, rows, pickers.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
The shape of a story event's type wherever one appears (map, scrubber, rows, pickers): event, twist, revelation, clue or goal.
Not for
  • A glyph from the outline setIcon
  • An event as a row with its stateEntityRow

Guidelines

  • event (filled circle), twist (diamond), revelation (ring), clue (rounded square), goal (triangle).
  • tone="auto" paints a plain event in the text colour and the other four in the accent. muted is for events outside the moment.

Props

EventType'event' | 'twist' | 'revelation' | 'clue' | 'goal'

EventGlyphProps

Prop
Type
What it does
typerequired
EventType
size
number
tone
'auto' | 'primary' | 'accent' | 'muted'
TypeScript
type EventType = 'event' | 'twist' | 'revelation' | 'clue' | 'goal';

export interface EventGlyphProps {
  type: EventType;
  size?: number;
  tone?: 'auto' | 'primary' | 'accent' | 'muted';
}
TokenLightDarkUse
glyph-eventtext-primarytext-primaryEventGlyph: an event, a plain happening.
glyph-mutedtext-mutedtext-mutedEventGlyph: an event outside the current moment.

AniCanonKit · Story

StoryMap

StoryMap draws the story as hubs and satellites: characters (portraits) and places (art) are hubs; events are small shapes between the hubs they involve, linked to each.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
The story drawn as hubs and satellites: characters and places as hubs, events as small shapes between the ones they involve, and a selection's causes and conflicts.
Not for
  • Moving through the story in story orderStoryScrubber
  • The same characters, places and events as rows in a panelEntityRow
  • The open questions the audience waits onQuestionRow

Guidelines

  • States sit on hubs as badges. A selection lights its hubs (focus) and dims the rest (dim); events after the moment are ghost.
  • A proposed event (being added) is drawn dashed in the accent; a just-added one is new with a New badge.
  • arcs draw causes and conflicts of the selection; dashed marks a conflict.
  • Positions are given by the page. Never draw every link at full weight.

Props

BadgeSpec{ label: string; variant?: 'new' | 'alert'; icon?: IconName }

StoryMapProps

Prop
Type
What it does
height
number
width
number
label
string
chip
string
tools
boolean
legend
boolean
hubsrequired
{ id: string; kind: 'character' | 'place'; x: number; y: number; size?: number; image?: string; initial?: string; label: string; badge?: BadgeSpec; state?: 'focus' | 'dim' }[]
eventsrequired
{ id: string; x: number; y: number; type: EventType; label: string; state?: 'focus' | 'dim' | 'ghost' | 'proposed' | 'new'; unplaced?: boolean; badge?: BadgeSpec }[]
edgesrequired
{ x1: number; y1: number; x2: number; y2: number; tone?: 'accent'; dim?: boolean; dashed?: boolean }[]
arcs
{ x1: number; y1: number; x2: number; y2: number; dashed?: boolean; bend?: number }[]
TypeScript
type BadgeSpec = { label: string; variant?: 'new' | 'alert'; icon?: IconName };

export interface StoryMapProps {
  height?: number;
  width?: number;
  label?: string;
  chip?: string;
  tools?: boolean;
  legend?: boolean;
  hubs: { id: string; kind: 'character' | 'place'; x: number; y: number; size?: number; image?: string; initial?: string; label: string; badge?: BadgeSpec; state?: 'focus' | 'dim' }[];
  events: { id: string; x: number; y: number; type: EventType; label: string; state?: 'focus' | 'dim' | 'ghost' | 'proposed' | 'new'; unplaced?: boolean; badge?: BadgeSpec }[];
  edges: { x1: number; y1: number; x2: number; y2: number; tone?: 'accent'; dim?: boolean; dashed?: boolean }[];
  arcs?: { x1: number; y1: number; x2: number; y2: number; dashed?: boolean; bend?: number }[];
}
TokenLightDarkUse
map-bgbg-surfacebg-surfaceStoryMap: ground.
map-borderborder-hairlineborder-hairlineStoryMap: outline and legend outline.
map-edgeborder-dividerborder-dividerStoryMap: a link between an event and a hub.
map-edge-activeaccentaccentStoryMap: links, causes and conflicts of the selection.
map-hub-ringborder-dividerborder-dividerStoryMap: ring around hubs and event nodes.
map-focusaccentaccentStoryMap: ring on a selected or lit hub or event.
map-node-bgbg-canvasbg-canvasStoryMap: event node fill.
map-node-emptybg-surface-raisedbg-surface-raisedStoryMap: a hub with no art yet.
map-labeltext-secondarytext-secondaryStoryMap: event labels and legend.
map-label-strongtext-primarytext-primaryStoryMap: hub names.
map-label-accenttext-accenttext-accentStoryMap: the label of a selected, new or proposed event.
map-legend-bgbg-canvasbg-canvasStoryMap: legend surface.

AniCanonKit · Story

StoryScrubber

StoryScrubber moves through the story in story order (when things happen in the world, not when they were released).

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
Moving through the story in story order, when things happen in the world: a column per event under arc brackets, and a handle that names the moment ("After Fire at Sé").
Not for

Guidelines

  • One column per event, grouped under arc brackets. Under each event, marks show what told it (episode, manga, short, Studio).
  • The handle sits after an event and names it ("After Fire at Sé"), or "Now" at the end. Two handles compare two moments.
  • Events with no place yet wait in the Not placed box at the end.

Props

Prop
Type
What it does
eventsrequired
{ type: EventType; title?: string; told?: IconName[]; inside?: boolean; mark?: boolean }[]
arcsrequired
{ name: string; count: number; on?: boolean }[]
handlesrequired
{ at: number; label: string }[]
fillrequired
[number, number]
unplaced
{ state?: 'event' | 'none'; on?: boolean; icon?: IconName }
filter
string | false
valueText
string
TypeScript
export interface StoryScrubberProps {
  events: { type: EventType; title?: string; told?: IconName[]; inside?: boolean; mark?: boolean }[];
  arcs: { name: string; count: number; on?: boolean }[];
  handles: { at: number; label: string }[];
  fill: [number, number];
  unplaced?: { state?: 'event' | 'none'; on?: boolean; icon?: IconName };
  filter?: string | false;
  valueText?: string;
}
TokenLightDarkUse
scrub-trackbg-fillbg-fillStoryScrubber: the story-order line.
scrub-fillaccentaccentStoryScrubber: the stretch included at the handle.
scrub-handleaccentaccentStoryScrubber: handle and its pill.
scrub-handle-fgon-accenton-accentStoryScrubber: handle pill label.
scrub-node-bgbg-canvasbg-canvasStoryScrubber: event node fill.
scrub-node-ringborder-dividerborder-dividerStoryScrubber: event node ring.
scrub-node-ring-onborder-controlborder-controlStoryScrubber: ring of an included event.
scrub-arcborder-dividerborder-dividerStoryScrubber: arc brackets.
scrub-labeltext-mutedtext-mutedStoryScrubber: arc names, told-by marks and key.
scrub-label-ontext-secondarytext-secondaryStoryScrubber: an arc inside the moment.
scrub-unplaced-borderborder-placeholderborder-placeholderStoryScrubber: the Not placed box.

AniCanonKit · Story

QuestionRow

QuestionRow is one open question the audience is waiting on: planted → raised → answered, each step a real event with its place.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
One open question the audience is waiting on, from planted to raised to answered, each step a real event with its place.
Not for

Guidelines

  • An empty Answered step invites the event that pays it off. quiet flags a question nothing has touched for a long time; answered fades the row.

Props

Prop
Type
What it does
questionrequired
string
state
'waiting' | 'quiet' | 'answered'
note
string
selected
boolean
plantedrequired
{ label: string; image?: string; told?: string }
raised
{ label: string; image?: string; told?: string }[]
answered
{ label: string; image?: string; told?: string }
TypeScript
export interface QuestionRowProps {
  question: string;
  state?: 'waiting' | 'quiet' | 'answered';
  note?: string;
  selected?: boolean;
  planted: { label: string; image?: string; told?: string };
  raised?: { label: string; image?: string; told?: string }[];
  answered?: { label: string; image?: string; told?: string };
}
TokenLightDarkUse
question-pipbg-fillbg-fillOpen question track: a step not reached.
question-pip-onaccentaccentOpen question track: a step reached.
question-bgbg-surfacebg-surfaceQuestionRow: surface.
question-borderborder-hairlineborder-hairlineQuestionRow: outline.
question-selectedaccentaccentQuestionRow: the selected question.
question-titletext-primarytext-primaryQuestionRow: question and event names.
question-notetext-mutedtext-mutedQuestionRow: notes, step labels and arrows.
question-event-bgbg-canvasbg-canvasQuestionRow: an event in a step.
question-empty-borderborder-placeholderborder-placeholderQuestionRow: an empty step.

AniCanonKit · Maki

MakiPanel

MakiPanel is Maki's column: the mascot and name, Chat and Review, the conversation, and the composer. It opens beside the map from the top-bar Maki button.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
The chat with Maki in its own column beside the map: the mascot and name, Chat and Review, the conversation and the composer, opened from the Maki button in the top bar.
Not for

Props

Prop
Type
What it does
avatarrequired
string
review
number
view
'Chat' | 'Review'
suggestions
string[]
placeholder
string
hint
string
childrenrequired
ReactNode
TypeScript
export interface MakiPanelProps {
  avatar: string;
  review?: number;
  view?: 'Chat' | 'Review';
  suggestions?: string[];
  placeholder?: string;
  hint?: string;
  children: ReactNode;
}
TokenLightDarkUse
maki-nametext-accenttext-accentMaki: the name above Maki's turn.
maki-texttext-primarytext-primaryMaki: message text.
maki-quote-bgbg-canvasbg-canvasMaki: a quoted line.
maki-quote-fgtext-secondarytext-secondaryMaki: quoted text.
maki-dividerborder-hairlineborder-hairlineMakiPanel: dividers under the header and above the composer.

AniCanonKit · Maki

MakiTurn

MakiTurn is one thing Maki says in the chat, under Maki's face and name.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
One thing Maki says in the chat, under Maki's face and name: plain sentences, with any cards inside it.
Not for
  • The person's own messageChatMessage
  • Something Maki proposes or reports, such as a story editMakiCard
  • A check pinned to work on a sheetSlip

Guidelines

  • Plain sentences. Quote the exact words an issue collides with in a <q>. Cards (MakiCard) go inside the turn.
  • avatarSize grows the mascot for a moment worth marking (a fix accepted).

Props

Prop
Type
What it does
avatarrequired
string
avatarSize
number
name
string
childrenrequired
ReactNode
TypeScript
export interface MakiTurnProps {
  avatar: string;
  avatarSize?: number;
  name?: string;
  children: ReactNode;
}

AniCanonKit · Maki

ChatMessage

ChatMessage is the person's own message in a chat with Maki, set to the right.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
The person's own message in a chat with Maki, set to the right.
Not for

Props

Prop
Type
What it does
childrenrequired
ReactNode
TypeScript
export interface ChatMessageProps {
  children: ReactNode;
}
TokenLightDarkUse
chat-mine-bgaccent-subtleaccent-subtleChatMessage: the person's own message.

AniCanonKit · Maki

MakiCard

MakiCard is something Maki proposes or reports inside a turn: an issue, a story edit, a section to add, a reason to keep.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
Something Maki proposes or reports inside a turn: an issue, a story edit, a section to add, with what was checked and Accept as the primary.
Not for
  • Maki's words around itMakiTurn
  • Something found in writing, beside the words it came fromProposalCard
  • A proposed change shown in place in the wordsRevision
  • A correction pinned to work on a sheetSlip

Guidelines

  • Eyebrow says what kind ("Maki · Story edit"); badge marks an issue's severity.
  • check states what was verified ("The story stays consistent with this edit"). Actions go in slot="actions"; Accept is the primary.
  • Maki proposes; only the person's Accept writes. muted for a card already decided.

Props

Prop
Type
What it does
eyebrow
string
title
string
badge
BadgeSpec
check
string
muted
boolean
children
ReactNode
TypeScript
/** Slot: slot="actions". */
export interface MakiCardProps {
  eyebrow?: string;
  title?: string;
  badge?: BadgeSpec;
  check?: string;
  muted?: boolean;
  children?: ReactNode;
}
TokenLightDarkUse
maki-card-bgbg-canvasbg-canvasMakiCard: surface.
maki-card-borderborder-dividerborder-dividerMakiCard: outline.
maki-card-eyebrowtext-mutedtext-mutedMakiCard: eyebrow.
maki-check-fgtext-secondarytext-secondaryMakiCard: the check line.
maki-check-icontext-accenttext-accentMakiCard: the check mark.

AniCanonKit · Maki

MakiComposer

MakiComposer is where the person writes to Maki: suggested replies as chips above, the field, attach and send.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
Where the person writes to Maki: suggested replies as chips above the field, attach and send.
Not for

Props

Prop
Type
What it does
placeholder
string
suggestions
string[]
hint
string
attach
boolean
TypeScript
export interface MakiComposerProps {
  placeholder?: string;
  suggestions?: string[];
  hint?: string;
  attach?: boolean;
}
TokenLightDarkUse
maki-composer-bgbg-canvasbg-canvasMakiComposer: field.
maki-composer-borderborder-controlborder-controlMakiComposer: field outline.
maki-composer-placeholdertext-mutedtext-mutedMakiComposer: placeholder and hint.

AniCanonKit · Maki

AskMaki

AskMaki is Maki's one button: Maki's face and a label. It carries Maki's face, never a sparkle.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
The one button that asks Maki: handed a conflict, a selection or a question at size="sm", or in a view's header as the Maki pill with its count, which opens Maki's panel.
Not for
  • An action that is not about MakiButton
  • A quiet Ask Maki inside a line or a row of textTextLink
  • Maki's column with the conversationMakiPanel

Guidelines

  • Handed something (a conflict, a selection, a question): size="sm", the label says what Maki is asked ("Ask Maki how it could be answered").
  • In a view's header: label="Maki", size="md", shape="pill", with count (how many things Maki wants looked at, left out when there is nothing). It opens Maki's panel; open says the panel is open, and the pill sits on the accent tint.
  • href makes it a link to Maki's page instead of a button.

Props

Prop
Type
What it does
avatarrequired
string
label
string
variant
'secondary' | 'ghost' | 'outline'
size
'sm' | 'md'
shape
'pill'
count
string
How many things Maki wants looked at.
open
boolean
Maki's panel is open beside the view.
href
string
onClick
MouseEventHandler
TypeScript
/** Maki's one button. sm: handed a conflict or a selection. md with shape 'pill' and label 'Maki': Maki in a view's header, with `count` and `open`. */
export interface AskMakiProps {
  avatar: string;
  label?: string;
  variant?: 'secondary' | 'ghost' | 'outline';
  size?: 'sm' | 'md';
  shape?: 'pill';
  /** How many things Maki wants looked at. */
  count?: string;
  /** Maki's panel is open beside the view. */
  open?: boolean;
  href?: string;
  onClick?: MouseEventHandler;
}

AniCanonKit · Productions

CutSheet

CutSheet is a scene read the way anime reads it: an ekonte sheet. Rows are numbered cuts; columns are set per view (Plan: C · Picture · Action · Dialogue · Sec · Notes; Make: C · Board · Latest take · Video · Sec; lists: art · title · where it is · story). Rules between rows, no boxes. Rows are CutRows. surface sets the sheet on its page, the paper it is printed on; what follows the sheet (the next scene, a quote) goes in slot="footer" under it.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
A scene read as an ekonte sheet: numbered cuts as rows, columns set per view (Plan, Make, lists), rules between rows and no boxes.
Not for
  • The cut's tracks in timeTimeline
  • What gets generated for one cut, on a frame gridTimesheet
  • One row of the sheetCutRow
  • A plain list of things with a title and a quiet lineList
  • The script as it is writtenManuscript

Props

Prop
Type
What it does
columnsrequired
{ label: string; width?: string; align?: 'end' }[]
label
string
surface
boolean
On its page; slot="footer" sits under the sheet.
TypeScript
/** Productions: a scene read as an ekonte sheet. */
export interface CutSheetProps {
  columns: { label: string; width?: string; align?: 'end' }[];
  label?: string;
  /** On its page; slot="footer" sits under the sheet. */
  surface?: boolean;
}
TokenLightDarkUse
cutsheet-texttext-primarytext-primaryCutSheet: cell text.
cutsheet-headtext-mutedtext-mutedCutSheet: column labels.
cutsheet-ruleborder-hairlineborder-hairlineCutSheet: the rule between rows.
cutsheet-rule-headborder-dividerborder-dividerCutSheet: the rule under the column labels.
cutsheet-selected-bgbg-surface-raisedbg-surface-raisedCutSheet: the selected row.
cutsheet-changedtext-primarytext-primaryCutSheet: the margin bar on a row changed since you last read it.
cutsheet-page-bgbg-surfacebg-surfaceCutSheet on its page: the paper.
cutsheet-page-edgeborder-hairlineborder-hairlineCutSheet on its page: the page's edge.

AniCanonKit · Productions

CutRow

CutRow is one cut (or video, or production) in a CutSheet. Its children are the cells, in column order. selected bands the row (the cut open beside the sheet); changed puts a bar in the margin when it changed since you last read it; span makes one cell run under the row's columns (a Timesheet under its cut).

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

A CutRow only exists inside a cut sheet. See it live in CutSheet.

Use it for
One cut, video or production as a row in a CutSheet, its cells in column order, banded while it is open beside the sheet.
Not for

Props

Prop
Type
What it does
selected
boolean
changed
boolean
span
boolean
TypeScript
export interface CutRowProps {
  selected?: boolean;
  changed?: boolean;
  span?: boolean;
}

AniCanonKit · Productions

CutCell

CutCell is the cut number, with its approval: approved (filled check) or waiting (an empty ring). note adds a short line under it, such as the video it is (V3).

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
A cut's number with its approval, approved or waiting, and a short note under it such as the video it is (V3).
Not for

Props

Prop
Type
What it does
numberrequired
string
state
'approved' | 'waiting'
stateLabel
string
note
string
TypeScript
export interface CutCellProps {
  number: string;
  state?: 'approved' | 'waiting';
  stateLabel?: string;
  note?: string;
}
TokenLightDarkUse
cut-numbertext-primarytext-primaryCutCell: the cut number.
cut-number-selectedtext-accenttext-accentCutCell: the selected cut's number.
cut-notetext-mutedtext-mutedCutCell: a short note under the number.
cut-state-borderborder-controlborder-controlCutCell: waiting for approval.
cut-state-ok-bgtext-primarytext-primaryCutCell: approved.
cut-state-ok-fgbg-canvasbg-canvasCutCell: approved check.

AniCanonKit · Productions

Shorthand

Shorthand is camera and timing notation as an ekonte writes it: shot size, angle, move and when it starts and stops (C.U · low · T.U 0+00 → 5+00). Small caps, tabular figures. tone="accent" for a state that waits for you (To review), tone="secondary" for a settled one; the default is the notation colour.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
Camera and timing notation as an ekonte writes it: shot size, angle, move and when it starts and stops (C.U · low · T.U 0+00 → 5+00).
Not for
  • Choosing a shot, an angle or a moveOptionTiles
  • The direction set on a panel, a row per kindPropertyList
  • Studio's folded section with its current valuePanelRow
  • A cut's number and approvalCutCell
TokenLightDarkUse
shorthand-fgtext-secondarytext-secondaryShorthand: camera and timing notation.
shorthand-accenttext-accenttext-accentShorthand: a state that waits for you.
shorthand-secondarytext-secondarytext-secondaryShorthand: a settled state.

AniCanonKit · Productions

Revision

Revision shows a proposed change in place: the old words struck, the new ones underlined. Accept, Decline and Why sit beside it, per cut. Without before it is only an insertion: a new line proposed where there was none.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
A proposed change shown in place, the old words struck and the new ones underlined, with Accept, Decline and Why beside it, or a new line where there was none.
Not for
  • A change of state, such as alive to deadStateChange
  • The Director's note on a cutSlip
  • Something Maki proposes inside the chatMakiCard
  • An event or link found in writingProposalCard

Props

Prop
Type
What it does
before
string
Leave out for an insertion.
afterrequired
string
TypeScript
export interface RevisionProps {
  /** Leave out for an insertion. */
  before?: string;
  after: string;
}
TokenLightDarkUse
revision-fromtext-mutedtext-mutedRevision: the struck text.
revision-markaccentaccentRevision: the underline under the new text.

AniCanonKit · Productions

Slip

Slip is the correction slip, the only tinted surface on a sheet: the Director's revision note, a check, a review note pinned to a take, a story check. done turns it into an outline. Slot: action (Ask Maki).

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
The correction slip, the only tinted surface on a sheet: the Director's revision note, a check, a review note pinned to a take, a story check.
Not for
  • One line of context inside a pageNotice
  • A proposed change in the words themselvesRevision
  • Something Maki proposes inside the chatMakiCard
  • A confirmation that something finishedToast

Props

Prop
Type
What it does
label
string
done
boolean
TypeScript
/** Slots: slot="action". */
export interface SlipProps {
  label?: string;
  done?: boolean;
}
TokenLightDarkUse
slip-bgaccent-subtleaccent-subtleSlip: the correction slip, the only tinted surface on a sheet.
slip-fgtext-primarytext-primarySlip: text.
slip-labeltext-accenttext-accentSlip: label and action.
slip-done-borderaccent-subtleaccent-subtleSlip: a resolved slip's edge.
slip-done-fgtext-secondarytext-secondarySlip: a resolved slip's text.

AniCanonKit · Productions

Timesheet

Timesheet is what gets generated for a cut, on a frame grid (24 a second, a heavier tick each second): each video's bracket, the key frames it starts and ends on, and one line per video (where it opens, how it continues).

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
What gets generated for a cut, on a frame grid: each video's bracket, the key frames it starts and ends on, and one line per video.
Not for

Props

Prop
Type
What it does
secondsrequired
number
fps
number
label
string
note
string
spansrequired
{ start: number; end: number; label: string; detail?: string }[]
keys
{ at: number; label: string }[]
TypeScript
export interface TimesheetProps {
  seconds: number;
  fps?: number;
  label?: string;
  note?: string;
  spans: { start: number; end: number; label: string; detail?: string }[];
  keys?: { at: number; label: string }[];
}
TokenLightDarkUse
tsheet-bgbg-canvasbg-canvasTimesheet: the frame grid.
tsheet-tickborder-hairlineborder-hairlineTimesheet: a quarter-second tick.
tsheet-tick-secondborder-controlborder-controlTimesheet: a second.
tsheet-spantext-secondarytext-secondaryTimesheet: a video's bracket.
tsheet-key-bgtext-primarytext-primaryTimesheet: a key frame.
tsheet-key-fgbg-canvasbg-canvasTimesheet: key number.
tsheet-metatext-mutedtext-mutedTimesheet: labels on top.
tsheet-labeltext-secondarytext-secondaryTimesheet: span labels.
tsheet-label-strongtext-primarytext-primaryTimesheet: span names.

AniCanonKit · Productions

ScreenplayLine

ScreenplayLine is one block of a screenplay inside a ManuscriptSection: slug, action, cue (with extension such as O.S.), paren, dialogue, transition.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
One block of a screenplay inside a section of the script: a slugline, action, a cue with its extension (O.S.), a parenthetical, dialogue or a transition.
Not for

Guidelines

  • flagged underlines a line Maki found a story conflict in.

Props

Prop
Type
What it does
type
'slug' | 'action' | 'cue' | 'paren' | 'dialogue' | 'transition'
extension
string
flagged
boolean
TypeScript
export interface ScreenplayLineProps {
  type?: 'slug' | 'action' | 'cue' | 'paren' | 'dialogue' | 'transition';
  extension?: string;
  flagged?: boolean;
}
TokenLightDarkUse
screenplay-texttext-primarytext-primaryScreenplayLine: text.
screenplay-metatext-mutedtext-mutedScreenplayLine: parentheticals, extensions and transitions.
screenplay-flagaccentaccentScreenplayLine: a line Maki flagged.

AniCanonKit · Productions

DialogueLine

DialogueLine is a planned line: speaker, delivery, where it falls in the beat, the line in the spoken language and its translation. The line may carry markup as children instead of line: a Revision inside the words.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
A planned line: the speaker, the delivery, where it falls in the beat, the line in its spoken language and its translation.
Not for

Props

Prop
Type
What it does
speakerrequired
string
line
string
Or the line as children, when it carries markup.
image
string
delivery
string
at
string
translation
string
TypeScript
export interface DialogueLineProps {
  speaker: string;
  /** Or the line as children, when it carries markup. */
  line?: string;
  image?: string;
  delivery?: string;
  at?: string;
  translation?: string;
}
TokenLightDarkUse
dline-speakertext-primarytext-primaryDialogueLine: speaker.
dline-texttext-primarytext-primaryDialogueLine: the line.
dline-metatext-mutedtext-mutedDialogueLine: delivery, offset and translation.

AniCanonKit · Productions

ProgramBar

ProgramBar is the whole cut at a glance: sections in planned proportion (cut, partial with gaps, missing), the window the Timeline shows, the playhead, and numbered note marks from screening.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
The whole cut at a glance: its sections in planned proportion, the window the Timeline shows, the playhead and numbered notes from screening.
Not for
  • The cut's tracks to editTimeline
  • The playhead of an episode or a short being watchedScrubber
  • How far something being made has comeProgress
  • Moving through the story in story orderStoryScrubber

Props

Prop
Type
What it does
durationrequired
number
planned
number
total
string
playhead
number
window
{ start: number; length: number }
sections
{ label: string; start: number; length: number; state?: 'cut' | 'partial' | 'missing' }[]
marks
{ at: number; label: string; done?: boolean }[]
TypeScript
export interface ProgramBarProps {
  duration: number;
  planned?: number;
  total?: string;
  playhead?: number;
  window?: { start: number; length: number };
  sections?: { label: string; start: number; length: number; state?: 'cut' | 'partial' | 'missing' }[];
  marks?: { at: number; label: string; done?: boolean }[];
}
TokenLightDarkUse
pbar-bgbg-fill-subtlebg-fill-subtleProgramBar: the track.
pbar-borderborder-hairlineborder-hairlineProgramBar: track edge.
pbar-cutbg-surface-raisedbg-surface-raisedProgramBar: a section in the cut.
pbar-partialborder-controlborder-controlProgramBar: dashed edge on a section with gaps.
pbar-missingborder-placeholderborder-placeholderProgramBar: a planned section not cut yet.
pbar-labeltext-secondarytext-secondaryProgramBar: section label.
pbar-metatext-mutedtext-mutedProgramBar: total and missing labels.
pbar-windowaccentaccentProgramBar: the part the timeline shows, the playhead and open notes.
pbar-mark-fgon-accenton-accentProgramBar: note number.

AniCanonKit · Productions

ProductionNav

ProductionNav is the side bar inside a production (an episode, a short, and later a manga issue). The app's SideNav is not shown there: a production is a focused, full-screen workspace.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
The side bar inside a production (an episode, a short, a manga issue): its views, scenes and the facts that matter while making it, folding to a rail of views and tools in the editor.
Not for
  • The creator sidebar outside a productionSideNav
  • The top of a view with its title and actionsViewHeader
  • The steps of a full-screen flowStepper
  • The desktop header across the appTopNav

Guidelines

  • Expanded (Script, Plan, Make, Screen, Publish): back to the list, the production's cover, title and format; the views with their state (note, and state: 'attention' when something needs you); the scenes with their picture and state; the facts that matter while making it (runtime against plan, story place, video plan); the fragments balance.
  • collapsed (Cut): one 72px rail with the cover, the views as icons, a separator and the editor's tools (currentTool marks the open one), so the picture gets the room.
  • Every place it can take you is a link: back.href, and href on each view, scene and tool. Left out, it points nowhere (#).
  • The current view, scene and tool read in ink weight with the small state dot (state-dot) beside the name; on the rail the dot sits at the edge, centred on the icon. No fill, no edge.

Props

Prop
Type
What it does
back
{ label: string; href?: string }
production
{ title: string; meta?: string; image?: string }
viewsrequired
{ icon: string; label: string; note?: string; state?: 'attention'; href?: string }[]
current
string
sections
{ title?: string; selected?: number; items: { number: number; title: string; image?: string; note?: string; state?: 'attention'; href?: string }[] }
facts
{ label: string; value: string; percent?: number }[]
balance
string
collapsed
boolean
tools
{ icon: string; label: string; short?: string; href?: string }[]
currentTool
string
TypeScript
export interface ProductionNavProps {
  back?: { label: string; href?: string };
  production?: { title: string; meta?: string; image?: string };
  views: { icon: string; label: string; note?: string; state?: 'attention'; href?: string }[];
  current?: string;
  sections?: { title?: string; selected?: number; items: { number: number; title: string; image?: string; note?: string; state?: 'attention'; href?: string }[] };
  facts?: { label: string; value: string; percent?: number }[];
  balance?: string;
  collapsed?: boolean;
  tools?: { icon: string; label: string; short?: string; href?: string }[];
  currentTool?: string;
}
TokenLightDarkUse
prodnav-bgbg-surfacebg-surfaceProductionNav: background (the production's own side bar).
prodnav-borderborder-hairlineborder-hairlineProductionNav: edge and rail separator.
prodnav-fgtext-primarytext-primaryProductionNav: title and current items.
prodnav-itemtext-secondarytext-secondaryProductionNav: items.
prodnav-mutedtext-mutedtext-mutedProductionNav: labels, notes and meta.
prodnav-attentiontext-accenttext-accentProductionNav: a note that needs you.
prodnav-mediabg-mediabg-mediaProductionNav: behind thumbnails.
prodnav-placeholderborder-placeholderborder-placeholderProductionNav: a scene with no picture yet.
prodnav-trackbg-fillbg-fillProductionNav: runtime track.
prodnav-filltext-secondarytext-secondaryProductionNav: runtime made.

AniCanonKit · Productions

ViewHeader

ViewHeader opens a production view: the view as a quiet eyebrow, the title (one line, cut with an ellipsis), the meta under it, and the view’s actions on the right as children (Maki, then at most one primary).

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
The top of a production view: the view as an eyebrow, the title on one line, the meta under it, and the view's actions on the right (Maki, then at most one primary).
Not for

Props

Prop
Type
What it does
titlerequired
string
eyebrow
string
meta
string
children
ReactNode
The view's actions.
className
string
TypeScript
export interface ViewHeaderProps {
  title: string;
  eyebrow?: string;
  meta?: string;
  /** The view's actions. */
  children?: ReactNode;
  className?: string;
}

AniCanonKit · Productions

EditorShell

EditorShell is the full-screen editor (Cut). Slots: nav (the ProductionNav rail), header (EditorHeader), tool (the open tool’s panel, 312 px), inspector (the selected item, 300 px), overlay (a dialog over the editor). Its main content is the centre column: Monitor, MonitorBar, the ProgramBar and the Timeline.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
The full-screen editor (Cut, and Art and Lettering in manga): the rail, header, tool panel, inspector and the centre column with the monitor, program bar and timeline, with focus for the canvas alone.
Not for
  • A production view outside the editorViewHeader
  • A full-screen flow that makes one thing step by stepFlowHeader
  • A task over the pageDialog

Guidelines

  • EditorHeader: title, timecode, an optional status (Rebuilds exactly) and the actions as children.
  • Monitor: the picture (image, imageStyle) with labels in its corner (labelsEnd for the other corner) and overlays as children; guides draws title-safe or an effect's area (accent for the selected one) and region the area an edit works on, with its label; centered puts a frame in the middle; fill lets it take the column's height (the manga page canvas); fit="contain" shows the whole picture (a take in Studio) instead of filling the frame.
  • MonitorBar: slots transport and end, the timecode, the edit buttons as children.
  • Nothing floats over the picture except the corner labels and what you are editing on it.
  • CanvasStage replaces the Monitor while an item is selected on the canvas: the frame, the item at its box (percent of the frame; it may run past the edges, shown dimmed), and a box with eight handles to move and resize it.
  • focus turns the shell into focus mode: the rail, header, tool panel and inspector fold away; the monitor grows and the transport and timeline stay, since editing needs them. The float slot holds what comes back on demand, FloatingToolbars with media buttons: the tools, the production's name and Leave focus. They appear when the pointer moves and hide after 2 s still.
  • Motion follows the editor motion sheet: panels on motion-base, selection on motion-instant with ease-enter, focus on motion-slow; dragging never eases. Under reduced motion every move becomes a short fade or nothing.

Props

EditorShellProps

Prop
Type
What it does
height
number
Slots: nav, header, tool, inspector, overlay, float (focus mode only). Children are the centre column.
focus
boolean
Focus mode: the panels fold away, the centre fills, float holds the bars that come back on demand.
inspectorLabel
string
children
ReactNode
className
string

CanvasStageProps · The canvas with one item selected: box in percent of the frame, may run past its edges.

Prop
Type
What it does
image
string
imageStyle
string
alt
string
box
{ x: number; y: number; w: number; h: number }
ratio
string
height
number
label
string
children
ReactNode
className
string

EditorHeaderProps

Prop
Type
What it does
titlerequired
string
timecode
string
status
string
e.g. "Rebuilds exactly"
statusHint
string
children
ReactNode
The actions.

MonitorProps

Prop
Type
What it does
guides
{ x: number; y: number; w: number; h: number; accent?: boolean }[]
Title-safe or an effect's area, in percent; accent for the selected one.
region
{ x: number; y: number; w: number; h: number; label?: string }
The area an edit works on, with its label.
labelsEnd
string[]
Labels in the other corner.
fill
boolean
Takes the column's height instead of height.
image
string
imageStyle
string
CSS, e.g. "object-position:50% 40%".
alt
string
labels
string[]
Corner labels: the cut and the timecode.
height
number
centered
boolean
A 9:16 frame in the middle.
fit
'cover' | 'contain'
contain: the whole picture, not filling the frame.
children
ReactNode
className
string

MonitorBarProps

Prop
Type
What it does
timecode
string
children
ReactNode
Slots: transport, end. Children are the edit buttons.
TypeScript
export interface EditorShellProps {
  /** Slots: nav, header, tool, inspector, overlay, float (focus mode only). Children are the centre column. */
  height?: number;
  /** Focus mode: the panels fold away, the centre fills, `float` holds the bars that come back on demand. */
  focus?: boolean;
  inspectorLabel?: string;
  children?: ReactNode;
  className?: string;
}

/** The canvas with one item selected: `box` in percent of the frame, may run past its edges. */
export interface CanvasStageProps {
  image?: string;
  imageStyle?: string;
  alt?: string;
  box?: { x: number; y: number; w: number; h: number };
  ratio?: string;
  height?: number;
  label?: string;
  children?: ReactNode;
  className?: string;
}

export interface EditorHeaderProps {
  title: string;
  timecode?: string;
  /** e.g. "Rebuilds exactly" */
  status?: string;
  statusHint?: string;
  /** The actions. */
  children?: ReactNode;
}

export interface MonitorProps {
  /** Title-safe or an effect's area, in percent; accent for the selected one. */
  guides?: { x: number; y: number; w: number; h: number; accent?: boolean }[];
  /** The area an edit works on, with its label. */
  region?: { x: number; y: number; w: number; h: number; label?: string };
  /** Labels in the other corner. */
  labelsEnd?: string[];
  /** Takes the column's height instead of `height`. */
  fill?: boolean;
  image?: string;
  /** CSS, e.g. "object-position:50% 40%". */
  imageStyle?: string;
  alt?: string;
  /** Corner labels: the cut and the timecode. */
  labels?: string[];
  height?: number;
  /** A 9:16 frame in the middle. */
  centered?: boolean;
  /** contain: the whole picture, not filling the frame. */
  fit?: 'cover' | 'contain';
  children?: ReactNode;
  className?: string;
}

export interface MonitorBarProps {
  timecode?: string;
  /** Slots: transport, end. Children are the edit buttons. */
  children?: ReactNode;
}
TokenLightDarkUse
monitor-guide-accentaccentaccentMonitor: the guide of what is selected (an effect's area).
monitor-regionaccentaccentMonitor: the region an edit works on.
monitor-region-fgon-accenton-accentMonitor: the region's label.
monitor-guidewhite-a62white-a62Monitor: a dashed guide over the picture (title-safe, an area).

AniCanonKit · Productions

TrimFrames

TrimFrames shows a cut’s in and out frames with their timecodes and a frame-at-a-time nudge under each. active (in or out) rings the frame being trimmed. Each frame takes image, imageStyle, timecode and an optional href. Frame size with frameWidth and frameHeight (60 × 107 for 9:16).

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
The selected cut's in and out frames with their timecodes and a frame-at-a-time nudge under each, ringing the one being trimmed.
Not for
  • Trimming an item on the tracks by its endsTimeline
  • A picture in a frameThumb
  • A number stepped a unit at a timeNumberField
  • The frame grid of what gets generated for a cutTimesheet

Props

TrimFrame

Prop
Type
What it does
imagerequired
string
imageStyle
string
timecoderequired
string
href
string

TrimFramesProps

Prop
Type
What it does
inrequired
TrimFrame
outrequired
TrimFrame
active
'in' | 'out'
frameWidth
number
frameHeight
number
label
string
TypeScript
export interface TrimFrame {
  image: string;
  imageStyle?: string;
  timecode: string;
  href?: string;
}

export interface TrimFramesProps {
  in: TrimFrame;
  out: TrimFrame;
  active?: 'in' | 'out';
  frameWidth?: number;
  frameHeight?: number;
  label?: string;
}

AniCanonKit · Productions

VolumeControl

VolumeControl is a sound’s mute button, its level and the readout (Muted when muted). what names the sound for the buttons’ labels (the video’s own sound). onMute and onChange (0–100) make it operable; the track drags like SliderRow's.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
A sound's mute button, its level and the readout (Muted when muted), such as the video's own sound on the selected cut.
Not for

Props

Prop
Type
What it does
valuerequired
number
muted
boolean
what
string
Names the sound in the buttons' labels.
href
string
onMute
MouseEventHandler
onChange
(value: number) => void
Called with 0–100.
className
string
TypeScript
export interface VolumeControlProps {
  value: number;
  muted?: boolean;
  /** Names the sound in the buttons' labels. */
  what?: string;
  href?: string;
  onMute?: MouseEventHandler;
  /** Called with 0–100. */
  onChange?: (value: number) => void;
  className?: string;
}

AniCanonKit · Productions

PromptComposer

PromptComposer is Studio's prompt: who and where it binds on top, the words, then its tools.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
The prompt that makes a take (in Studio, and for episodes in Quick mode): the cast, places and references it binds on top, the words, then its tools with Generate and its cost, and variant="media" as the phone's dock over the take.
Not for

Guidelines

  • slot="refs": MentionChips for the cast (@), places (#) and references ($) the prompt binds.
  • The words are its children; a mention inside them is a Mention.
  • slot="tools": the opening frame, the length, templates, with Generate and its cost at the end of the row (or in slot="footer").
  • placeholder shows while there are no words; slot="hint" is the one line that says what is missing before Generate works ("Add a place, or describe the moment in 24 characters or more").
  • variant="media" is the phone's dock over the take: glass, and its chips take tone="media".
  • Episodes in Quick mode and Studio take from the same composer.

Props

Prop
Type
What it does
label
string
promptLabel
string
placeholder
string
variant
'media'
The phone's glass dock over the take.
children
ReactNode
className
string
TypeScript
/** Studio's prompt. Slots: slot="refs" (MentionChips), slot="hint", slot="tools", slot="footer". The words are its children; `placeholder` shows while there are none. */
export interface PromptComposerProps {
  label?: string;
  promptLabel?: string;
  placeholder?: string;
  /** The phone's glass dock over the take. */
  variant?: 'media';
  children?: ReactNode;
  className?: string;
}
TokenLightDarkUse
composer-bgbg-surfacebg-surfacePromptComposer: its surface.
composer-edgeborder-dividerborder-dividerPromptComposer: its edge.
composer-fgtext-primarytext-primaryPromptComposer: the words.
composer-ruleborder-hairlineborder-hairlinePromptComposer: the rule over its tools.

AniCanonKit · Manga

MangaPage

MangaPage draws one page of an issue at its trim (1448 × 2048): its art once drawn, or its layout's frames before.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
One page of an issue at its trim: its art once drawn, its layout's frames before, or blank when it is not made yet, with a selection only on the editor's canvas.
Not for

Guidelines

  • Pass image for a drawn page, frames (in percent of the page) for one only laid out. A frame drawing or waiting says so inside it; numbers prints each frame's number (4.2).
  • selected rings the page; selection puts one item (a panel, a balloon) under handles, in percent. Only the editor's canvas shows a selection.
  • title and meta caption it under the page; on a spread the right page's caption takes align="end".
  • blank is a page not made yet, such as the back cover. Never fake art for a page that is not drawn.
  • mask (percent) marks the area an edit redraws, on a page or a single panel's picture (ratio to its shape); maskLabel names it.
  • In slot="action" a caption takes its menu button (Rename, Move, Delete).

Props

Prop
Type
What it does
mask
{ x: number; y: number; w: number; h: number }
The area an edit redraws, in percent. Slot: slot=\"action\" beside the caption.
maskLabel
string
width
number
ratio
number
Height over width; defaults to the B5 trim.
image
string
frames
MangaFrame[]
numbers
boolean
selected
boolean
blank
boolean
A page not made yet, e.g. the back cover.
selection
{ x: number; y: number; w: number; h: number }
One item selected on the page, with handles, in percent.
selectionLabel
string
label
string
alt
string
title
string
meta
string
align
'end'
The caption's alignment: the right page of a spread reads from its outer edge.
children
ReactNode
className
string
TypeScript
/** Manga: one page at its trim ratio (1448 × 2048), its art once drawn or its layout's frames before. With title or meta it becomes a figure with a caption under it. */
export interface MangaPageProps {
  /** The area an edit redraws, in percent. Slot: slot=\"action\" beside the caption. */
  mask?: { x: number; y: number; w: number; h: number };
  maskLabel?: string;
  width?: number;
  /** Height over width; defaults to the B5 trim. */
  ratio?: number;
  image?: string;
  frames?: MangaFrame[];
  numbers?: boolean;
  selected?: boolean;
  /** A page not made yet, e.g. the back cover. */
  blank?: boolean;
  /** One item selected on the page, with handles, in percent. */
  selection?: { x: number; y: number; w: number; h: number };
  selectionLabel?: string;
  label?: string;
  alt?: string;
  title?: string;
  meta?: string;
  /** The caption's alignment: the right page of a spread reads from its outer edge. */
  align?: 'end';
  children?: ReactNode;
  className?: string;
}
TokenLightDarkUse
mpage-paperbg-surfacebg-surfaceMangaPage: a page before it is drawn.
mpage-blankbg-fill-subtlebg-fill-subtleMangaPage: a page not made yet, such as the back cover.
mpage-edgeborder-hairlineborder-hairlineMangaPage: the page's edge.
mpage-frameborder-controlborder-controlMangaPage: a frame of the layout.
mpage-busybg-fill-subtlebg-fill-subtleMangaPage: a frame being drawn or waiting.
mpage-texttext-secondarytext-secondaryMangaPage: a frame's number and state.
mpage-selectedaccentaccentMangaPage: the selected page, spread or region.
mpage-handletext-on-mediatext-on-mediaMangaPage: a selection handle.
mpage-dimbg-mediabg-mediaMangaPage: the page outside the selected region.
mpage-captext-secondarytext-secondaryMangaPage: the caption's title.
mpage-cap-metatext-mutedtext-mutedMangaPage: the caption's quiet line.
mpage-maskaccentaccentMangaPage: the area an edit redraws.

AniCanonKit · Manga

IssueStrip

IssueStrip shows the issue as the reader turns it: the cover alone, then the content pages paired into spreads, with a rule at every page turn.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
The issue as the reader turns it, in Layout and under the editor's canvas: the cover alone, then the pages in spreads with a rule at every page turn.
Not for

Guidelines

  • spreads is a list of spreads, each a list of MangaPage props. Mark the open page selected.
  • selectSpread (Spread mode) rings the whole spread that holds the selected page instead of the page.
  • size is the page width: 56 in Layout, 30 or 18 under the editor's canvas. label names the strip; false hides it.
  • It is a map, not a reader: it shows where a page sits and where the turns fall. Page turns matter to the story, so never hide them.

Props

Prop
Type
What it does
spreadsrequired
MangaPageProps[][]
size
number
Page width; 30 in the editor, 56 in Layout.
selectSpread
boolean
A selected page selects its whole spread (Spread mode).
label
string | false
The header; false hides it.
turnLabel
string
className
string
TypeScript
/** Manga: the issue as the reader turns it. Covers stand alone, content pages pair into spreads, a rule marks every page turn. */
export interface IssueStripProps {
  spreads: MangaPageProps[][];
  /** Page width; 30 in the editor, 56 in Layout. */
  size?: number;
  /** A selected page selects its whole spread (Spread mode). */
  selectSpread?: boolean;
  /** The header; false hides it. */
  label?: string | false;
  turnLabel?: string;
  className?: string;
}
TokenLightDarkUse
istrip-turnborder-controlborder-controlIssueStrip and PageWall: a page turn.
istrip-texttext-mutedtext-mutedIssueStrip: its label and key.
istrip-selectedaccentaccentIssueStrip: the selected spread in Spread mode.

AniCanonKit · Manga

PageWall

PageWall is Proof: every page of the issue at once, in spreads, each with its caption and how far it has come.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
The Proof view: every page of the issue at once in spreads, each with its caption and how far it has come (Laid out, Drawn, Lettered, Proofed).
Not for

Guidelines

  • spreads as for IssueStrip; each page adds title, meta (its stage in words) and stage, the number of steps it has passed (Laid out, Drawn, Lettered, Proofed).
  • columns spreads to a row (3 on desktop); a page-turn rule sits between spreads in a row.
  • Choosing a page opens it beside the wall; the wall itself never edits.

Props

Prop
Type
What it does
spreadsrequired
(MangaPageProps & { /** 0–4: how many of `steps` it has passed. */ stage?: number })[][]
size
number
columns
number
steps
string[]
Default: Laid out, Drawn, Lettered, Proofed.
label
string
className
string
TypeScript
/** Manga Proof: every page at once, in spreads, each with its caption and its stage. */
export interface PageWallProps {
  spreads: (MangaPageProps & { /** 0–4: how many of `steps` it has passed. */ stage?: number })[][];
  size?: number;
  columns?: number;
  /** Default: Laid out, Drawn, Lettered, Proofed. */
  steps?: string[];
  label?: string;
  className?: string;
}
TokenLightDarkUse
wall-stagebg-fillbg-fillPageWall: a stage not reached.
wall-stage-ontext-primarytext-primaryPageWall: a stage reached.
wall-captext-secondarytext-secondaryPageWall: a page's caption title.
wall-cap-metatext-mutedtext-mutedPageWall: a page's stage in words.

AniCanonKit · Manga

LayerStrip

LayerStrip lists a page's layers by kind, in reading order, under the editor's canvas: panels, lettering, sound effects.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
A page's layers under the editor's canvas: panels, lettering and sound effects, a track per kind in reading order, with the issue strip on top.
Not for

Guidelines

  • One track per kind; each item is its number (3.2) and, for words, how it is set and its first words. Panels show their picture once drawn.
  • Choosing an item selects it on the page and the inspector edits that kind: a panel's direction and frame, a balloon's type and shape, a sound effect's style.
  • Children sit on top: the issue's IssueStrip at size 18, so the page is always placed in its issue.
  • An empty kind says emptyLabel ("None yet"), never hides.

Props

Prop
Type
What it does
tracksrequired
{ kind: 'panels' | 'lettering' | 'sfx'; label: string; icon?: IconName; items: { number: string; label?: string; image?: string; selected?: boolean; muted?: boolean }[] }[]
emptyLabel
string
children
ReactNode
className
string
TypeScript
/** Manga editor: a page's layers by kind, in reading order. Choosing one selects it on the page; the inspector edits that kind. Children sit on top (the issue strip). */
export interface LayerStripProps {
  tracks: { kind: 'panels' | 'lettering' | 'sfx'; label: string; icon?: IconName; items: { number: string; label?: string; image?: string; selected?: boolean; muted?: boolean }[] }[];
  emptyLabel?: string;
  children?: ReactNode;
  className?: string;
}
TokenLightDarkUse
layers-bgbg-surfacebg-surfaceLayerStrip: the strip.
layers-edgeborder-hairlineborder-hairlineLayerStrip: its edge and divider.
layers-headtext-secondarytext-secondaryLayerStrip: a kind's name.
layers-itembg-fill-subtlebg-fill-subtleLayerStrip: a layer.
layers-item-selectedbg-surface-raisedbg-surface-raisedLayerStrip: the selected layer.
layers-selectedaccentaccentLayerStrip: the selected layer's ring.
layers-texttext-primarytext-primaryLayerStrip: a layer's number.
layers-mutedtext-mutedtext-mutedLayerStrip: counts, lines and empty text.
layers-emptyborder-placeholderborder-placeholderLayerStrip: a panel not drawn yet.

AniCanonKit · Manga

MentionChip

MentionChip shows who or what a panel holds: a cast member (@), a place (#) or a prop, with its picture and a quiet note.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
Who or what a panel or a prompt holds: a cast member (@), a place (#), a reference ($) or a prop, with its picture and a quiet note such as the outfit and the emotion.
Not for
  • A character's face, name and role in a rowCastAvatar
  • A mention inside proseMention
  • Narrowing a feed by kindChip
  • Words kept as removable tagsTagList

Guidelines

  • kind sets the mark: character writes @Bruce, place writes #Bruce's apartment, reference writes $rain-look (a picture the creator added, with what it is for as the note), prop has no mark.
  • tone="media" over art, in the phone's dock.
  • note is what this panel needs of it: the outfit and the emotion for a character ("Winter coat · tired").
  • Pass the sheet's picture as image; a prop with no sheet yet shows no picture, never a stand-in.

Props

Prop
Type
What it does
namerequired
string
kind
'character' | 'place' | 'reference' | 'prop'
tone
'media'
image
string
note
string
href
string
className
string
TypeScript
/** The inspector's title row: what is selected, its delete, and a quiet line under. A cast member (@), a place (#) or a prop as a chip with its picture and a quiet note: the outfit, the emotion. */
export interface MentionChipProps {
  name: string;
  kind?: 'character' | 'place' | 'reference' | 'prop';
  tone?: 'media';
  image?: string;
  note?: string;
  href?: string;
  className?: string;
}
TokenLightDarkUse
mchip-bgbg-fill-subtlebg-fill-subtleMentionChip: the chip.
mchip-texttext-primarytext-primaryMentionChip: the name.
mchip-notetext-mutedtext-mutedMentionChip: outfit, emotion or role.

AniCanonKit · Writing

Manuscript

Manuscript is the script's page: its tools on top (the block type, where the cursor is), the sections, and what it reports under them (saved, length, Maki's check).

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
The script's page, for episodes and manga issues alike: its tools on top, the numbered sections, and what it reports under them (saved, length, Maki's check).
Not for

Guidelines

  • The body holds ManuscriptSections; the one being written is current.
  • slot="toolbar" and slot="footer" (a StatusBar). label names the script for screen readers.
  • Episodes and manga issues write in the same page.

Props

Prop
Type
What it does
label
string
childrenrequired
ReactNode
className
string
TypeScript
/** Writing: the script's page. Slots: slot="toolbar", slot="footer". */
export interface ManuscriptProps {
  label?: string;
  children: ReactNode;
  className?: string;
}
TokenLightDarkUse
manuscript-bgbg-surfacebg-surfaceManuscript: the script's page.
manuscript-edgeborder-hairlineborder-hairlineManuscript: the page's edge.
manuscript-ruleborder-hairlineborder-hairlineManuscript: the rule under its tools.

AniCanonKit · Writing

ManuscriptSection

ManuscriptSection is one numbered section of a scene note or an imported script: #number, title (or a slugline with slug), the prose, the word count.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
One numbered section of a scene note or an imported script: its number, title or slugline, the prose and the word count, with what was found in it in the margin.
Not for

Guidelines

  • What was found in it sits beside it in slot="margin" as ProposalCards, so every proposal stays next to the words it came from.
  • A hover card or selection toolbar goes in slot="overlay".
  • current raises the section being written.

Props

Prop
Type
What it does
numberrequired
number
title
string
slug
boolean
words
number
current
boolean
The section being written.
childrenrequired
ReactNode
TypeScript
/** Slots: slot="margin" (proposals beside the section), slot="overlay" (a hover card or toolbar). */
export interface ManuscriptSectionProps {
  number: number;
  title?: string;
  slug?: boolean;
  words?: number;
  /** The section being written. */
  current?: boolean;
  children: ReactNode;
}
TokenLightDarkUse
manuscript-numbertext-accenttext-accentManuscriptSection: the #number.
manuscript-texttext-primarytext-primaryManuscriptSection: title and prose.
manuscript-metatext-mutedtext-mutedManuscriptSection: word count.
msec-current-bgbg-surface-raisedbg-surface-raisedManuscriptSection: the section being written.

AniCanonKit · Writing

Mention

Mention is a reference inside prose: @ a character (filled), # a place (underlined). active rings the one whose card is open.

@sakuga/anicanonkitv1.0Used onWeb · iOS · AndroidFrom AniCanon

Use it for
A reference inside prose or a prompt: @ a character, # a place, $ a reference the creator added, ringed while its card is open.
Not for
  • Who or what a panel or a prompt binds, with its picture and noteMentionChip
  • A quiet action inside a line of textTextLink
  • Words a check flagged inside a sentenceText

Guidelines

$ a reference the creator added (kind="reference").

Props

Prop
Type
What it does
kind
'character' | 'place' | 'reference'
active
boolean
childrenrequired
string
TypeScript
export interface MentionProps {
  kind?: 'character' | 'place' | 'reference';
  active?: boolean;
  children: string;
}
TokenLightDarkUse
mention-character-bgaccent-subtleaccent-subtleMention: @character fill.
mention-character-fgtext-accenttext-accentMention: @character text.
mention-place-fgtext-primarytext-primaryMention: #place text.
mention-place-ruletext-mutedtext-mutedMention: #place underline.
mention-active-ringaccentaccentMention: the mention whose card is open.

AniCanonKit · Writing

ProposalCard

ProposalCard is one event, change or link found in writing, waiting for the person.

@sakuga/anicanonkitv1.0Used onWebFrom AniCanon

Use it for
One event, change or link found in writing, waiting for the person beside the words it came from: stated, inferred, or a conflict with its Ask Maki.
Not for
  • Something Maki proposes inside the chatMakiCard
  • A change shown in place in the wordsRevision
  • A check pinned to a cut on a sheetSlip

Guidelines

  • stated: said in the text; accepted by default. inferred: a reading between the lines; dashed and never accepted for you. conflict: breaks the story; carries its action (Ask Maki).
  • accepted drives the toggle.

Props

Prop
Type
What it does
titlerequired
string
note
string
kind
'stated' | 'inferred' | 'conflict'
accepted
boolean
visual
Visual
TypeScript
/** Slots: slot="visual", slot="action". */
export interface ProposalCardProps {
  title: string;
  note?: string;
  kind?: 'stated' | 'inferred' | 'conflict';
  accepted?: boolean;
  visual?: Visual;
}
TokenLightDarkUse
proposal-bgbg-surfacebg-surfaceProposalCard: a stated proposal.
proposal-borderborder-hairlineborder-hairlineProposalCard: outline.
proposal-inferred-borderborder-placeholderborder-placeholderProposalCard: dashed outline of an inferred proposal.
proposal-conflict-borderaccentaccentProposalCard: a conflict.
proposal-titletext-primarytext-primaryProposalCard: title.
proposal-notetext-mutedtext-mutedProposalCard: note.

Build around the art.