{
  "components": [
    {
      "id": "theme",
      "name": "Theme",
      "section": "foundations",
      "summary": "An app's colours for light and dark, installed once, and the gradient pairs the derived square picks from. Nothing draws a colour it did not ask the theme for.",
      "desktop": [
        "theme"
      ],
      "android": {
        "at": [
          "ui/Theme.kt: ArkTheme, LocalArkTheme, Space"
        ],
        "how": [
          "Inside a MaterialTheme, provide LocalArkTheme with ArkTheme(art): the derived square's pairs, ArkTheme.NeutralLight and NeutralDark until an app gives its own.",
          "Space.xs … Space.xxl, Space.page, Space.corner: the spacing every shared component lays out by."
        ]
      },
      "ios": {
        "at": [
          "swift/ArkUI: Space, Squares, \\.squares, Color(light:dark:)"
        ],
        "how": [
          "import ArkUI. Space.xs … Space.xxl, Space.page, Space.corner: the spacing, Android's.",
          "Set the derived square's colours once at the root: .environment(\\.squares, Squares(light:dark:)); Squares.neutral until an app does."
        ]
      },
      "shots": []
    },
    {
      "id": "style",
      "name": "Style",
      "section": "foundations",
      "summary": "Text tones, buttons, scrollbars, grounds and surfaces, and the rows a cursor lights.",
      "desktop": [
        "style"
      ],
      "android": null,
      "ios": null,
      "shots": []
    },
    {
      "id": "icons",
      "name": "Icons",
      "section": "foundations",
      "summary": "The Lucide drawings, tinted from the theme.",
      "desktop": [
        "glyphs",
        "icon"
      ],
      "android": {
        "at": [
          "ui/Glyphs.kt: ArkGlyphs",
          "ui/Components.kt: Glyph"
        ],
        "how": [
          "Glyph(icon, description, modifier, size = 24.dp)",
          "ArkGlyphs.More, Back, Chevron, Volume, Muted are ImageVectors; tinted by LocalContentColor."
        ]
      },
      "ios": null,
      "shots": []
    },
    {
      "id": "text",
      "name": "Text",
      "section": "foundations",
      "summary": "Words fitted to a width, cut where a reader loses least, and numbers said as words.",
      "desktop": [
        "fit",
        "line",
        "format",
        "clipboard"
      ],
      "android": null,
      "ios": null,
      "shots": []
    },
    {
      "id": "art",
      "name": "Art",
      "section": "foundations",
      "summary": "The derived square, the same on every device for one name, and the real picture over it once it arrives.",
      "desktop": [
        "art",
        "images"
      ],
      "android": {
        "at": [
          "ui/Art.kt: Art, Square, Cover"
        ],
        "how": [
          "Square(seed, modifier)",
          "Cover(seed, art, side, modifier, round = false, corner = Space.corner): the square until the picture loads.",
          "Art.hash is FNV-1a over UTF-16, the desktop's."
        ]
      },
      "ios": {
        "at": [
          "swift/ArkUI: Square, Art (ArkUICore)"
        ],
        "how": [
          "Square(seed:): the same square for the same name as Android's and the desktop's.",
          "Art.hash and Art.pick are in ArkUICore, which needs only Foundation."
        ]
      },
      "shots": [
        {
          "id": "art",
          "kind": "screenshot",
          "what": "Squares derived from twelve names.",
          "phones": [
            "android",
            "ios"
          ]
        }
      ]
    },
    {
      "id": "navigation",
      "name": "Navigation",
      "section": "components",
      "summary": "Where the places are: a sidebar on the desktop, tabs on a phone. The place under the cursor, or the tab that is lit, is the page.",
      "desktop": [
        "sidebar"
      ],
      "android": {
        "at": [
          "ui/Tabs.kt: TabStack, Reselect, LocalReselect"
        ],
        "how": [
          "TabStack keeps each tab's content alive while another is shown, each with its Reselect in LocalReselect.",
          "Reselect.ask() is the lit tab tapped again."
        ]
      },
      "ios": null,
      "shots": []
    },
    {
      "id": "lists",
      "name": "Lists",
      "section": "components",
      "summary": "Rows of what a page holds: what each is, a line under it, and where it goes.",
      "desktop": [
        "table",
        "outline"
      ],
      "android": {
        "at": [
          "ui/Components.kt: ListRow, Chevron"
        ],
        "how": [
          "ListRow(onClick, title, under, trailing = { Chevron() }) { picture }"
        ]
      },
      "ios": null,
      "shots": [
        {
          "id": "lists",
          "kind": "screenshot",
          "what": "Rows with a picture and a line under each.",
          "phones": [
            "android"
          ]
        }
      ]
    },
    {
      "id": "reorder",
      "name": "Reorderable list",
      "section": "components",
      "summary": "Rows dragged into a new order, the rest sliding out of the way: what a queue is rearranged with.",
      "desktop": [
        "reorder"
      ],
      "android": {
        "at": [
          "ui/Reorder.kt: rememberReorder, ReorderableItem"
        ],
        "how": [
          "A LazyColumn drawn in reorder.order(items, key), each row in ReorderableItem(reorder, key, keys): a long press lifts it, and onMove(from, to) is told on the drop.",
          "The rows that make way are the list's own animateItem on Springs.MOVE; a tap still does what a tap did.",
          "In sections, rememberReorder(list, lead, isHeader) { key, landing -> … }: a row dragged past a header changes section, a header held folds every section to its header and moves its own, and the drop is told as Landing(section, before)."
        ]
      },
      "ios": {
        "at": [
          "swift/ArkUI: Reorderable, ReorderHandle"
        ],
        "how": [
          "Reorderable(items, key:, onMove:) { item, handle in … }: the row places the handle it is given, and is dragged by it, as the Music app's queue is.",
          "The rows that make way move on Springs.move; the row keeps its tap and its context menu."
        ]
      },
      "shots": []
    },
    {
      "id": "cards",
      "name": "Cards",
      "section": "components",
      "summary": "Pictures with a name and a line under each, in a grid or a shelf; a square, or a circle.",
      "desktop": [
        "cards"
      ],
      "android": {
        "at": [
          "ui/Components.kt: CoverCard"
        ],
        "how": [
          "CoverCard(seed, art, title, under, side, onClick, round = false, opens = false)",
          "A shelf is a LazyRow of them, `side` from the screen's width."
        ]
      },
      "ios": null,
      "shots": [
        {
          "id": "cards",
          "kind": "screenshot",
          "what": "Cards, square and round.",
          "phones": [
            "android"
          ]
        }
      ]
    },
    {
      "id": "headers",
      "name": "Headers",
      "section": "components",
      "summary": "How a page opens: a banner or a bar with its title and back, then headings over its sections.",
      "desktop": [
        "hero"
      ],
      "android": {
        "at": [
          "ui/Components.kt: PageBar, TabTitle, Heading"
        ],
        "how": [
          "PageBar(title, onBack, actions = {})",
          "TabTitle(title, actions = {})",
          "Heading(title, modifier, action = null, onAction = {})"
        ]
      },
      "ios": null,
      "shots": [
        {
          "id": "headers",
          "kind": "screenshot",
          "what": "A page's bar, a tab's title, a heading.",
          "phones": [
            "android"
          ]
        }
      ]
    },
    {
      "id": "empty",
      "name": "Empty states",
      "section": "components",
      "summary": "What a page says while it has nothing to show, or is still asking for it.",
      "desktop": [],
      "android": {
        "at": [
          "ui/Components.kt: Empty, Fetching"
        ],
        "how": [
          "Empty(icon, title, sentence, modifier, action = null, onAction = {})",
          "Fetching(host, delayMs): said only after a beat, so a quick answer never flashes it."
        ]
      },
      "ios": null,
      "shots": [
        {
          "id": "empty",
          "kind": "screenshot",
          "what": "Nothing here yet, and still asking.",
          "phones": [
            "android"
          ]
        }
      ]
    },
    {
      "id": "account",
      "name": "Account and sign-in",
      "section": "components",
      "summary": "Who is signed in, as a circle of their picture or their initials, and the screens that get them signed in.",
      "desktop": [
        "account"
      ],
      "android": {
        "at": [
          "ui/Onboarding.kt: OpeningScreen, ServerScreen, SignInScreen, PermissionScreen, OnboardingPage"
        ],
        "how": [
          "SignInScreen(mark, host, error, phase, onSignIn, onChangeServer)",
          "OnboardingPage is the frame each screen is drawn in: the mark, a title, a sentence, then its own."
        ]
      },
      "ios": null,
      "shots": [
        {
          "id": "sign-in",
          "kind": "screenshot",
          "what": "Signing in to a server.",
          "phones": [
            "android"
          ]
        }
      ]
    },
    {
      "id": "slider",
      "name": "Slider",
      "section": "components",
      "summary": "A level to drag, and a button beside it to mute it.",
      "desktop": [],
      "android": {
        "at": [
          "ui/Volume.kt: VolumeSlider"
        ],
        "how": [
          "VolumeSlider(level, muted, onLevel, onDone, onMute, modifier, enabled = true, about = \"Volume\")"
        ]
      },
      "ios": null,
      "shots": [
        {
          "id": "slider",
          "kind": "screenshot",
          "what": "A level and its mute.",
          "phones": [
            "android"
          ]
        }
      ]
    },
    {
      "id": "menu",
      "name": "Menu",
      "section": "overlays",
      "summary": "What can be done about one thing: a dropdown at the pointer or under a control on the desktop, a sheet from the foot of a phone.",
      "desktop": [
        "menu",
        "context",
        "panel"
      ],
      "android": {
        "at": [
          "ui/Components.kt: SheetRow, MoreButton"
        ],
        "how": [
          "SheetRow(icon, label, onClick, trailing = {}, emphasis = false) is one entry of a sheet.",
          "MoreButton(about, origin, onMore) opens it from a row."
        ]
      },
      "ios": null,
      "shots": [
        {
          "id": "sheet",
          "kind": "screenshot",
          "what": "A sheet's entries.",
          "phones": [
            "android"
          ]
        }
      ]
    },
    {
      "id": "command",
      "name": "Command box",
      "section": "overlays",
      "summary": "One search box over everything, opened by Ctrl+K from anywhere.",
      "desktop": [
        "command"
      ],
      "android": null,
      "ios": null,
      "shots": []
    },
    {
      "id": "picker",
      "name": "Picker",
      "section": "overlays",
      "summary": "Choices each ticked or not, and a last row that names a new one.",
      "desktop": [
        "picker"
      ],
      "android": null,
      "ios": null,
      "shots": []
    },
    {
      "id": "dialog",
      "name": "Dialog",
      "section": "overlays",
      "summary": "A window over the page as the page dims, a sheet along its edge, and what goes under every overlay.",
      "desktop": [
        "dialog",
        "sheet",
        "layer"
      ],
      "android": null,
      "ios": null,
      "shots": []
    },
    {
      "id": "expanding-card",
      "name": "Expanding card",
      "section": "motion",
      "summary": "A bar at the foot of the screen that rises into the whole screen and falls back, following a finger or on its own.",
      "desktop": [],
      "android": {
        "at": [
          "ui/ExpandingCard.kt: ExpandingCard, ExpandingCardState, rememberExpandingCard"
        ],
        "how": [
          "ExpandingCard(state, stand, onOpen, collapsed, expanded, traveler): `collapsed` is the bar's face, `expanded` the screen's with a `slot` where `traveler`, the picture, lands.",
          "state.open() and state.close() move it on its springs; ExpandingCardState.held(p, density) holds it at p for a screenshot."
        ]
      },
      "ios": {
        "at": [
          "swift/ArkUI: View.expandingCard(isPresented:bar:expanded:) (iOS 26)"
        ],
        "how": [
          "On a TabView: tabView.expandingCard(isPresented: $open) { bar } expanded: { screen }. The bar sits on glass above the tab bar (tabViewBottomAccessory) and zooms into the screen and back (navigationTransition(.zoom)).",
          "The system does the motion, a swipe down on the screen included; `isPresented` going false zooms it back into the bar."
        ]
      },
      "shots": [
        {
          "id": "expanding-card",
          "kind": "recording",
          "what": "The bar rising to the screen, and falling back.",
          "phones": [
            "android",
            "ios"
          ]
        }
      ]
    },
    {
      "id": "glass",
      "name": "Glass",
      "section": "motion",
      "summary": "Buttons and chips on glass that changes shape: pieces near each other in one space join, a piece that appears buds from its neighbour, and a button's glass grows into the menu or the sheet it opens and shrinks back. On Android it is iOS 26's Liquid Glass as it moves, and only that: the shape and the motion, on a solid fill.",
      "desktop": [],
      "android": {
        "at": [
          "kotlin/arkui/src/dev/arkui/Glass.kt: GlassContainer, Modifier.glass, GlassMorph, GlassShape, Glass"
        ],
        "how": [
          "GlassContainer(modifier, spacing = Space.s) { … }: one surface for every piece of glass in it, as SwiftUI's GlassEffectContainer. Two pieces nearer than `spacing` are joined by a neck that thickens as they close; a piece appearing buds out of its nearest neighbour, and one going shrinks back into it. Best as large as the screen, since a menu opens over all of it.",
          "Modifier.glass(shape = GlassShape.Capsule, id = null): this element on glass, a capsule or GlassShape.rounded(corner). Moved in its layout, it glides there on Springs.MOVE, its content with it. `id` is what the container knows it by, as glassEffectID.",
          "GlassMorph(expanded, onDismiss, modifier, shape, id, menuWidth = Glass.menuWidth, menuCorner = Glass.menuCorner, sheet = false, held = null, collapsed = { button }) { menu }: a button whose glass becomes its menu, its bounds and corners on one spring, the button's face fading as the menu comes in, anchored at the button's edge. A tap outside, or back, calls onDismiss. It must be in a GlassContainer.",
          "sheet = true opens it as iOS 26's floating sheets instead: as wide as the space less Glass.sheetMargin each side, resting on its foot that far up; give it menuCorner = Glass.sheetCorner for the iPhone's corners. Still grown out of the button, and shrunk back into it.",
          "held = p holds a GlassMorph at p of the way, 0 to 1, whatever `expanded` says: a screenshot's frame.",
          "The fill is the scheme's surfaceBright with a soft shadow (Glass.fill(), Glass.shadow): no blur, no refraction, no highlight yet."
        ]
      },
      "ios": {
        "at": [
          "The system's: GlassEffectContainer, glassEffect, glassEffectID (iOS 26)"
        ],
        "how": [
          "Pieces in one GlassEffectContainer(spacing:) blend as they come within its spacing: Android's GlassContainer is this.",
          "A button and its panel given one glassEffectID in one namespace, one shown at a time and swapped inside withAnimation(Springs.move): the glass changes shape from one to the other, as GlassMorph does.",
          "A sheet: .sheet(isPresented:) at a height short of the screen (presentationDetents) floats on glass, and zoomed out of its button (matchedTransitionSource on the button, navigationTransition(.zoom(sourceID:in:)) on the sheet) grows out of it and back, as GlassMorph(sheet = true) does.",
          "The glass is the system's own Liquid Glass, blur and light included."
        ]
      },
      "shots": [
        {
          "id": "glass-menu",
          "kind": "recording",
          "what": "A glass button growing into its menu, held, and shrinking back.",
          "phones": [
            "android",
            "ios"
          ]
        },
        {
          "id": "glass-sheet",
          "kind": "recording",
          "what": "The same button growing into a sheet at the foot of the screen, and back.",
          "phones": [
            "android",
            "ios"
          ]
        },
        {
          "id": "glass-merge",
          "kind": "recording",
          "what": "Three capsules of glass sliding together into one, and parting.",
          "phones": [
            "android",
            "ios"
          ]
        },
        {
          "id": "glass-menu-open",
          "kind": "screenshot",
          "what": "The menu, open over its page.",
          "phones": [
            "android",
            "ios"
          ]
        }
      ]
    },
    {
      "id": "pages",
      "name": "Page motion",
      "section": "motion",
      "summary": "Going in and coming back: a page grows over the last, and a swipe from the edge takes it away.",
      "desktop": [
        "swipe",
        "history"
      ],
      "android": {
        "at": [
          "ui/Motion.kt: PageFrame, Moving, PageBack, Hero"
        ],
        "how": [
          "PageBack(motion, blocked, toFirst): the edge swipe, following the finger.",
          "Hero carries a picture from the card to the page's header."
        ]
      },
      "ios": null,
      "shots": []
    },
    {
      "id": "springs",
      "name": "Springs",
      "section": "motion",
      "summary": "The numbers and curves everything moves by, the same on both phones; on the desktop, the curves and what moves along them.",
      "desktop": [
        "motion"
      ],
      "android": {
        "at": [
          "ui/Springs.kt: Springs"
        ],
        "how": [
          "Springs.settle() and the rest: spring specs with a cap on how long any move takes."
        ]
      },
      "ios": {
        "at": [
          "swift/ArkUI: Springs"
        ],
        "how": [
          "Springs.move, Springs.size, Springs.fade: Android's springs converted exactly (docs/plan-arkui-swift.md)."
        ]
      },
      "shots": []
    },
    {
      "id": "scroll",
      "name": "Scroll",
      "section": "motion",
      "summary": "Keeping the cursor on screen, and back to the top when the lit tab is chosen again.",
      "desktop": [
        "scroll"
      ],
      "android": {
        "at": [
          "ui/Tabs.kt: ScrollsToTop"
        ],
        "how": [
          "ScrollsToTop(listState) or ScrollsToTop(gridState) inside a tab's page: a Reselect scrolls it home."
        ]
      },
      "ios": {
        "at": [
          "The system's: TabView with a NavigationStack per tab"
        ],
        "how": [
          "Nothing to add: the shown tab, tapped again, takes its stack back to its root and then its list to the top.",
          "To do it from code, ScrollViewReader's scrollTo(first, anchor: .top) inside withAnimation(Springs.move). The recording sets a ScrollPosition a frame at a time along Springs.move's curve, which plays the same."
        ]
      },
      "shots": [
        {
          "id": "scroll-to-top",
          "kind": "recording",
          "what": "A long list scrolled down, then asked to go to its top.",
          "phones": [
            "android",
            "ios"
          ]
        }
      ]
    },
    {
      "id": "keys",
      "name": "Keys",
      "section": "behaviour",
      "summary": "The keyboard grammar and the grids it moves a cursor over.",
      "desktop": [
        "vim"
      ],
      "android": null,
      "ios": null,
      "shots": []
    },
    {
      "id": "routes",
      "name": "Routes",
      "section": "behaviour",
      "summary": "Places as addresses, kept in step with the address bar or the back stack.",
      "desktop": [
        "route",
        "url"
      ],
      "android": {
        "at": [
          "ui/Destination.kt: Destination, Destinations"
        ],
        "how": [
          "Destination(owner, name) { content }: a page's own ViewModel store, kept while it is on the stack."
        ]
      },
      "ios": null,
      "shots": []
    },
    {
      "id": "waking",
      "name": "Waking",
      "section": "behaviour",
      "summary": "What wakes a window that runs no loop, and what holds the splash until the first screen is ready.",
      "desktop": [
        "wake"
      ],
      "android": {
        "at": [
          "ui/Splash.kt: holdSplashUntil"
        ],
        "how": [
          "holdSplashUntil(ceilingMs) { ready }: in onCreate, before setContent."
        ]
      },
      "ios": null,
      "shots": []
    }
  ],
  "entries": [
    {
      "module": "theme",
      "name": "Theme",
      "summary": "An app's palette, installed once: light and dark iced palettes and the derived square's gradient pairs for each. Every style closure asks `of(theme)` rather than writing a colour down, which is why dark mode works.",
      "api": [
        "Palette { light, dark, light_art, dark_art }",
        "Palette::NEUTRAL",
        "install(palette) -> bool",
        "palette()",
        "of(&Theme) -> &Extended",
        "is_dark(&Theme)",
        "art(&Theme)"
      ],
      "rules": [
        "Install before the first frame; a second install is refused, so one window has one answer to \"what colour is this\".",
        "NEUTRAL (AppKit greys, system blue) is the only colour arkui owns. The docs' pictures are drawn in it."
      ]
    },
    {
      "module": "style",
      "name": "Style",
      "summary": "The style closures every widget is handed: text tones, buttons, scrollbars, the page's ground and the surface a step off it, hairlines, and the rows a cursor lights.",
      "api": [
        "dim, faint(alpha), accent (text)",
        "action, quiet (buttons)",
        "seek (slider)",
        "bars (scrollable: never the accent)",
        "page, surface, SURFACE_STEP (containers)",
        "edge, edge_color, EDGE_STEP, hairline, outline",
        "row(theme, on_cursor, focused, odd)",
        "entry(theme, on_cursor, focused), entry_text(theme, lit)",
        "on_row(theme, on_cursor, accent, dim)"
      ],
      "rules": [
        "A scrollbar is never the accent.",
        "A row under the cursor is marked whether or not its pane has the keys, and lit only while it has."
      ]
    },
    {
      "module": "glyphs",
      "name": "Glyphs",
      "summary": "The table of Lucide drawings, as SVG bytes. `LICENSE.lucide` sits beside it.",
      "api": [
        "ADD_TO, ALBUM, ARTIST, BACK, CHEVRON, CLOSE, COMPOSER, DEBUG, DEVICES, DOWN, HOME, LAPTOP, LIBRARY, LINK, MINUS, MORE, MUTED, NEXT, NOTE, OFFLINE, ONLINE, PAUSE, PHONE, PLAY, PLAYING, PLAYLIST, PLUS, PREVIOUS, PULPIT, SEARCH, SHOW, SHUFFLE, SIGN_OUT, SKIP_BACK, SKIP_FORWARD, SPEAKER, STOP, TICK, TICKED, TRANSCRIPT, UNTICK, VOLUME"
      ],
      "rules": [
        "Drawn with currentColor; the colour is the icon's tint, never the file's."
      ]
    },
    {
      "module": "icon",
      "name": "Icon",
      "summary": "A glyph drawn at a size in a colour asked of the palette. The named ones are the tones a row, a panel or a control needs.",
      "api": [
        "SIZE (15)",
        "tinted(shape, size, |palette| colour)",
        "plain(shape, dim)",
        "accent(shape, on_cursor)",
        "line(shape, lit)",
        "faint(shape, on_cursor)",
        "tick(on_cursor)",
        "more(on_cursor)",
        "chevron(lit)"
      ],
      "rules": [
        "The colour goes through the svg style filter, so one drawing serves every tone."
      ]
    },
    {
      "module": "format",
      "name": "Format",
      "summary": "Words for numbers: a clock, a count spelt out, a plural, a span of time, a day.",
      "api": [
        "clock(secs)",
        "spell(ms)",
        "plural(n, word)",
        "span(born, died)",
        "day(ms, now)"
      ],
      "rules": [
        "A length past an hour is h:mm:ss."
      ]
    },
    {
      "module": "fit",
      "name": "Fit",
      "summary": "Cutting text to a width: from the end or through the middle, measured in pixels, and the characters the typeface cannot draw spelt in ones it can.",
      "api": [
        "Cut::{End, Middle}",
        "tail(s, chars)",
        "middle(s, chars)",
        "within(body, px, cut, measure)",
        "drawable(s)"
      ],
      "rules": [
        "A name loses its middle, a heading its end."
      ]
    },
    {
      "module": "clipboard",
      "name": "Clipboard",
      "summary": "Copy, cut and paste in every text box, in a browser too: the window's view handed the browser's clipboard, and a paste put in where the keys are once the browser hands it over.",
      "api": [
        "view(view): the whole window's, once",
        "pastes() -> Subscription<()>"
      ],
      "rules": [
        "A paste is asked for and arrives later; it goes into whichever box has the keys, at its cursor, as typed text does.",
        "On the desktop it is iced's own clipboard, untouched."
      ]
    },
    {
      "module": "url",
      "name": "URL",
      "summary": "Percent-encoding for a fragment, so a `/`, a `#` or an accent survives the address bar.",
      "api": [
        "encode(s, &mut out)",
        "encoded(s)",
        "decode(s)"
      ],
      "rules": []
    },
    {
      "module": "sidebar",
      "name": "Sidebar",
      "summary": "A column of places down the left of a window, grouped under headings; the one under the cursor is the page beside it.",
      "api": [
        "Item { glyph, label, heading, trailing, press }",
        "count(n, lit)",
        "lit(i, cursor, focused)",
        "WIDTH (200)",
        "view(items, cursor, focused, width, padding, id)"
      ],
      "rules": [
        "One highlight, not two: the cursor is what the page shows, marked always, lit only while the sidebar has the keys.",
        "A heading is drawn where the group changes, not stored as a row, so row n is the cursor's n.",
        "Its pixel of edge is part of its width."
      ]
    },
    {
      "module": "panel",
      "name": "Panel",
      "summary": "The shape every context window is: a bordered panel, a title, and fixed-height entries with a glyph column that is there whether or not it has a glyph.",
      "api": [
        "panel(body, width)",
        "entry(glyph, label, trailing)",
        "title(label)",
        "label(body, lit)",
        "width_for(labels, min, max)",
        "chars(width, chrome)",
        "titled_height(rows)",
        "hang_above(right, bottom, w, h)",
        "PADDING, ENTRY, TITLE, ENTRY_CHAR, ENTRY_CHROME, EDGE, RADIUS"
      ],
      "rules": [
        "Heights are declared constants: a panel is placed before iced lays it out."
      ]
    },
    {
      "module": "table",
      "name": "Table",
      "summary": "Rows of cells cut to their columns at layout, headings over them, section labels and a gutter for a row's mark.",
      "api": [
        "cell(body, width, on_cursor, accent, dim)",
        "cell_cut(…, cut, …)",
        "heading(label, width)",
        "section(label, indent)",
        "gutter()",
        "row(line, on_cursor, focused, odd)",
        "GUTTER, ROW_PADDING, SPACING, CELL_SIZE, HEADING_SIZE"
      ],
      "rules": [
        "A cell loses its middle and a heading its end, each to its own column.",
        "Columns are portions, so no text column pushes another off a narrow window."
      ]
    },
    {
      "module": "outline",
      "name": "Outline",
      "summary": "A table whose rows are gathered under group headings: the cursor walks the rows alone, and rows and groups are dragged, or shifted from the keyboard, into a new order.",
      "api": [
        "Group { head, rows, foot }",
        "Look { line, zebra, rules }",
        "view(groups, lit, focused, look, drag, settle, now, grab, menu, row)",
        "Drag::press, drag, drop -> Dropped",
        "shift(sizes, first, last, down) -> Shift",
        "groups(count, first, last, down)",
        "rows(groups), group_of(sizes, n), rows_of(sizes, g), progress(sizes, n)"
      ],
      "rules": [
        "A heading is drawn, not a row: the cursor's n counts rows, so j crosses a heading without stopping on it.",
        "Every line is one height, so a drag is reorder's arithmetic; a heading held folds every group to its heading.",
        "Every row of whole groups selected is the groups: they are what a shift moves."
      ]
    },
    {
      "module": "line",
      "name": "Line",
      "summary": "One line of text cut, at layout, to the width it is given; `gives_way` lays it out after the rest of its row.",
      "api": [
        "Line::new(body, size, cut, colour)",
        "Line::gives_way()"
      ],
      "rules": [
        "A row lays out in order, so a line first in it would take everything; giving way puts it last."
      ]
    },
    {
      "module": "cards",
      "name": "Cards",
      "summary": "A grid of pictures with a name and a line under each, the arithmetic of how many fit, and a page's header of picture, name and facts.",
      "api": [
        "Shelf { card, gap, padding }, Shelf::DEFAULT",
        "Shelf::columns(pane), side(pane), room(pane), grid(cells, pane)",
        "Card { seed, title, under, round, picture, open }",
        "view(shelf, cards, pane, cursor, empty, on_hover, scroll, on_scroll)",
        "view_card(side, card, on_cursor, hover)",
        "header(picture, kind, name, under, open_under, facts, padding)",
        "empty_page(message, padding)"
      ],
      "rules": [
        "A person is a circle and a thing a square.",
        "The keyboard and the view divide the same pane width, so `l` lands where it looks."
      ]
    },
    {
      "module": "hero",
      "name": "Hero",
      "summary": "The wide banner a page can open with, the bold face, section headings, and a listing of entries in columns with a \"more\" after them.",
      "api": [
        "banner(picture, kind, name, width)",
        "height(width), ASPECT, TALLEST",
        "heading(title)",
        "Entry { over, title, under, count, open }",
        "listing(entries, columns, cursor, on_hover)",
        "grid(entries, columns)",
        "entry(picture, e, on_cursor, hover)",
        "more(label, lit)",
        "columns(room), COLUMN",
        "BOLD, FIRA_SANS_BOLD"
      ],
      "rules": [
        "Fira Sans Bold is embedded: a browser has no system fonts, and with none bold is not bold."
      ]
    },
    {
      "module": "art",
      "name": "Art",
      "summary": "The derived square: a gradient picked from a name, the same on every device, and the real picture over it once it arrives.",
      "api": [
        "hash(s) (FNV-1a over UTF-16)",
        "pair(seed)",
        "square(seed, size, corner)",
        "picture(seed, handle, side, corner)"
      ],
      "rules": [
        "The hash is the phones' too, so a name has one square everywhere."
      ]
    },
    {
      "module": "account",
      "name": "Account",
      "summary": "Who is signed in as a circle — their picture, or initials on a colour picked from their name — and the menu that hangs off it.",
      "api": [
        "Who { name, id }, Who::full(), Who::initials()",
        "Account { who, header, entries }",
        "Account::signed_in, turned_away, alone",
        "Account::menu(right, bottom, window)",
        "face(who, picture, side)",
        "button(account, picture, press)",
        "origin(…)",
        "HEIGHT (28)"
      ],
      "rules": [
        "Nobody goes offline by hand: the menu signs out, and nothing else."
      ]
    },
    {
      "module": "menu",
      "name": "Menu",
      "summary": "The dropdown: a title and entries, as wide as its longest entry, opened at the pointer or lined up under a control, with submenus beside it.",
      "api": [
        "Menu::open(title, entries, cursor, window, anchor)",
        "Entry::run(glyph, label, msg), Entry::submenu(…)",
        "Anchor::{Pointer, RightEdge(x)}",
        "view(menu, focused, at, activate)",
        "Menu::submenu_origin, origin_for, height, width_for",
        "fit, right_aligned, submenu_height",
        "MIN_WIDTH, MAX_WIDTH, SUBMENU_OVERLAP, DWELL"
      ],
      "rules": [
        "A right click opens at the pointer; a control's menu opens at the control.",
        "As wide as its longest entry, within 198..=420: AppKit's rule.",
        "A submenu laps the parent by exactly its padding, so the lit row touches its edge.",
        "A submenu opens after a 200ms rest, not on the way past."
      ]
    },
    {
      "module": "picker",
      "name": "Picker",
      "summary": "A list of choices each ticked or not, with an optional last row that names a new one — on its own in the middle, or as a menu's submenu.",
      "api": [
        "Picker::new(title, choices, new_row)",
        "Choice { value, name, on }",
        "Picker::activate() -> Picked::{Toggled, Naming}",
        "Picker::land, rows, grid, width_for",
        "view(picker, focused, Messages { at, activate, name, create })",
        "focus_naming()",
        "MIN_WIDTH, MAX_WIDTH, MAX_HEIGHT"
      ],
      "rules": [
        "A submenu draws no header: the menu it hangs off is still up with the title."
      ]
    },
    {
      "module": "reorder",
      "name": "Reorder",
      "summary": "A list whose rows are dragged into a new order: the row lifts and follows the pointer, and the rows it passes slide out of its way.",
      "api": [
        "Reorder::press(from, rows, row, y, now)",
        "drag(y, now), drop(now) -> Drop::{Click, Moved { from, to, settle }}",
        "list(rows, row, drag, settle, now)",
        "moved(items, from, to)",
        "THRESHOLD, SLIDING (spring 1.0, 0.3 s), SETTLING (spring 0.85, 0.3 s, from the pointer's speed)"
      ],
      "rules": [
        "Under THRESHOLD pixels a press is a click, so a row still does what a click on it did.",
        "Where a row lands is arithmetic on how far the pointer moved, the rows being one height: nothing laid out is measured.",
        "The app owns the order: it moves its row on a drop, and draws the new order with the Settle until its own order says so."
      ]
    },
    {
      "module": "context",
      "name": "Context",
      "summary": "A menu, its submenu, or a picker alone, and the rules for them together: which one has the keys, the dwell, `l` in and `h` out, `<Esc>`.",
      "api": [
        "Context::new()",
        "focus() -> Option<Layer>",
        "open_menu, close_menu, open_picker, close_picker",
        "land(i), picker_at(i), tick(), chosen(), activate_picker()",
        "cancel(), travel(motion)",
        "Layer::{Menu, Picker}"
      ],
      "rules": [
        "Closing a menu takes its submenu with it.",
        "An app refuses its rows' hover while a layer is up: a backdrop cannot swallow a hover."
      ]
    },
    {
      "module": "layer",
      "name": "Layer",
      "summary": "What goes under and around an overlay: a backdrop that closes on a click and swallows the wheel, a dimmed one, and pinned or centred placement.",
      "api": [
        "backdrop(close, swallow)",
        "dimmed(close, swallow)",
        "pinned(content, at)",
        "centred(content)"
      ],
      "rules": [
        "A submenu gets no backdrop of its own: a second one over the menu would eat its clicks."
      ]
    },
    {
      "module": "dialog",
      "name": "Dialog",
      "summary": "A titled window fading in and growing from 95% over a page as the page dims, as shadcn/ui's does without the blur.",
      "api": [
        "shown(now) -> Shown",
        "open(&shown, now), dimmed(&shown, now) -> 0..=1",
        "view(title, close, body, width, height, open)",
        "backdrop(close, swallow, dim)",
        "SPRING (damping 1.0, response 0.3 s), DIM_LEAD (0.75), FROM (0.95)"
      ],
      "rules": [
        "Closed, it fades and shrinks back the way it came: the app keeps its Shown, hidden, and draws it until Shown::gone.",
        "Keep a clock running while Shown::moving."
      ]
    },
    {
      "module": "command",
      "name": "Command box",
      "summary": "One search box over everything, opened by Ctrl+K, as shadcn/ui's command dialog: what it finds in groups under it, the keys walking it.",
      "api": [
        "Command::new(now), typed(query, list), step(down, groups, list), land(at), scrolled(y), chosen(groups)",
        "view(command, placeholder, groups, messages, open)",
        "panel(command, placeholder, groups, messages, width): the box alone, to pin as a dropdown",
        "list(command, groups, messages, width): what was found alone, under a box of the app's",
        "Group { heading, items }, Item { glyph, label, detail, choose }",
        "keys(open) -> Subscription<Press>, press(key, mods, open), opens(key, mods)",
        "WIDTH, LIST_HEIGHT, ROW, HEADING"
      ],
      "rules": [
        "The app searches and the box draws: it is handed the groups for what is typed.",
        "Its keys are heard whatever has the keyboard (keys): a text box takes <Esc> for itself.",
        "Its rows are declared heights, so the list scrolls to the keys by arithmetic."
      ]
    },
    {
      "module": "sheet",
      "name": "Sheet",
      "summary": "A panel the window's height sliding in from its right-hand edge over the page dimmed, as shadcn/ui's sheet does without the blur.",
      "api": [
        "shown(now) -> Shown",
        "open(&shown, now) -> 0..=1",
        "view(title, description, close, body, width, open)",
        "width(window) -> three quarters, at most MAX_WIDTH",
        "body_height(window, described)",
        "SPRING (damping 1.0, response 0.4 s)"
      ],
      "rules": [
        "The page under it dims with it, as under a dialog (dialog::backdrop with dialog::dimmed(&shown, now))."
      ]
    },
    {
      "module": "motion",
      "name": "Motion",
      "summary": "What moves a while and stops, on springs: a value sliding, something shown and hidden, whether to move at all, and a widget that draws what it holds moved, scaled and veiled.",
      "api": [
        "Spring { damping, response }, state(x, v, t), or_instant()",
        "SMOOTH, INSTANT, FADE",
        "Shown::new(spring, now), at(now), hide(now), show(now), moving(now), gone(now)",
        "Slide::new(from, to, now, spring), moving_at(speed), at(now), speed(now), toward(to, now, spring)",
        "reduced(), ALWAYS_ANIMATE",
        "transformed(content, by, scale, veil, radius)"
      ],
      "rules": [
        "Nothing ticks at rest: where a spring is, is worked out from the time whenever it is drawn.",
        "Sent somewhere else on its way, a move turns from where it is drawn at the speed it was going: nothing jumps.",
        "With less motion asked for (reduced), what slides or grows only fades.",
        "iced has no opacity for a group of widgets, so a fade is a veil of the ground over what fades, thinning."
      ]
    },
    {
      "module": "vim",
      "name": "Keys",
      "summary": "The keyboard grammar — counts, gg/G, ^d/^u, / and n/N, <Tab>, <Esc> — and the grids it moves a cursor over.",
      "api": [
        "Keys::new(), press(key, mods) -> Option<Action>, pending(), mode()",
        "Keys::bind(seq), set_bindings(seqs), visual(), leave_visual()",
        "Action::{Move, Activate, Toggle, Search, Match, Cycle, Cancel, Jump, Key, Visual (v or V), Inner (i and a letter), Bound}",
        "Motion::{Down, Up, Left, Right, First, Last, To}",
        "Grid { cells, columns }, column, row, step, progress",
        "Section, Page",
        "presses()",
        "find(labels, needle, at, delta)",
        "PAGE"
      ],
      "rules": [
        "`step` returns None only at that edge, which means \"not mine\": the app says what lies past it."
      ]
    },
    {
      "module": "route",
      "name": "Route",
      "summary": "Places as URL fragments, and the address bar kept in step: reconciled, not pushed, and a scrub one history entry. Each page here has one.",
      "api": [
        "trait Route { fragment(), parse() }",
        "join(kind, name), split(fragment)",
        "Router::new(), sync(&place), replace_next(), poll(ready), go(by), can_go(by)",
        "AddressBar, Browser, Nav"
      ],
      "rules": [
        "`parse` never fails: an unknown fragment is home.",
        "Call `sync` after every update."
      ]
    },
    {
      "module": "scroll",
      "name": "Scroll",
      "summary": "Keeping a keyboard cursor on screen.",
      "api": [
        "reveal(id, grid, at)",
        "reveal_to(id, progress)"
      ],
      "rules": []
    },
    {
      "module": "history",
      "name": "History",
      "summary": "Back and forward every way an app is asked: the browser's buttons, ^O and ^I, and two fingers sideways. The router and the swipe, wired once.",
      "api": [
        "History::new(), go(by), can_go(by), wheel(delta, now), tick(now), active(), sync(&place), replace_next(), poll(ready), view(page, now)",
        "scrolled(&place, y), arrive(&place) -> y: back and forward land where a page was left",
        "wheel() -> Subscription<ScrollDelta>"
      ],
      "rules": [
        "A jump and a swipe both move the bar; the app arrives by its next poll, the road the back button takes.",
        "The arrow shows only where there is somewhere to go."
      ]
    },
    {
      "module": "swipe",
      "name": "Swipe",
      "summary": "Two fingers sideways as back and forward, with the arrow drawn over the page as they go.",
      "api": [
        "Swipe::feed, tick, allow, active, offset, arrow",
        "band(…)",
        "view(page, swipe, now)",
        "FAR, LET_GO, QUIET, BACK, STRETCH"
      ],
      "rules": [
        "It goes when the fingers lift, and only where there is somewhere to go.",
        "In a browser the browser's own back does this instead."
      ]
    },
    {
      "module": "wake",
      "name": "Wake",
      "summary": "What wakes a window that runs no loop: a bell anything can ring, one timer at a deadline, and the browser's address bar moving.",
      "api": [
        "Bell::new(), ring(), rung(), subscription()",
        "at(key, delay)",
        "page_changes(f)"
      ],
      "rules": [
        "With nothing playing, dwelling or arriving, a window runs no timer."
      ]
    },
    {
      "module": "images",
      "name": "Images",
      "summary": "A picture cache: fetched off the render thread, decoded and bounded, kept on disk natively and in the Cache API in a browser. A failure is remembered.",
      "api": [
        "Images::new(name)",
        "want(url) -> Option<Task<Loaded>>",
        "loaded(l), handle(url), bounded(url)",
        "Loaded, Rgba",
        "BOUND (384), WIDE (2400)"
      ],
      "rules": [
        "The renderer is handed pixels at the size they are drawn, not a megapixel JPEG to decode inside a frame."
      ]
    }
  ]
}
