@krizaka/ui 3.36.0 → 3.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activity-feed.js +1 -1
- package/dist/activity-feed.js.map +1 -1
- package/dist/app-shell.js +3 -3
- package/dist/app-shell.js.map +1 -1
- package/dist/badge.d.ts +1 -1
- package/dist/badge.js +1 -1
- package/dist/badge.js.map +1 -1
- package/dist/blocks.css +30 -30
- package/dist/card.d.ts +6 -6
- package/dist/card.js +2 -2
- package/dist/card.js.map +1 -1
- package/dist/chip-row.js +1 -1
- package/dist/chip-row.js.map +1 -1
- package/dist/chip.js +1 -1
- package/dist/chip.js.map +1 -1
- package/dist/{chunk-QDXV544C.js → chunk-4QH6CP2Y.js} +2 -2
- package/dist/chunk-4QH6CP2Y.js.map +1 -0
- package/dist/{chunk-CKSHLSDH.js → chunk-AAP3U66V.js} +2 -2
- package/dist/{chunk-7UCPGXKN.js → chunk-C3NF7V5Y.js} +2 -2
- package/dist/{chunk-7UCPGXKN.js.map → chunk-C3NF7V5Y.js.map} +1 -1
- package/dist/{chunk-K2ZYB4HP.js → chunk-S5LP7KKW.js} +2 -2
- package/dist/{chunk-63KCDMX2.js → chunk-VPO27AFS.js} +4 -4
- package/dist/chunk-VPO27AFS.js.map +1 -0
- package/dist/{chunk-YOTQD7JD.js → chunk-WYQ277F2.js} +2 -2
- package/dist/{chunk-2DGLWTPS.js → chunk-YKOLME7R.js} +3 -3
- package/dist/{chunk-2DGLWTPS.js.map → chunk-YKOLME7R.js.map} +1 -1
- package/dist/code-block.js +4 -4
- package/dist/code-block.js.map +1 -1
- package/dist/combobox.js +2 -2
- package/dist/data-table.js +3 -3
- package/dist/file-upload.js +3 -3
- package/dist/file-upload.js.map +1 -1
- package/dist/filter-bar.js +2 -2
- package/dist/genui.js +4 -4
- package/dist/landing-page.js +1 -6
- package/dist/landing-page.js.map +1 -1
- package/dist/language-switch.js +1 -1
- package/dist/marketing-layout.js +1 -1
- package/dist/marketing-layout.js.map +1 -1
- package/dist/media-grid.js +1 -1
- package/dist/meter.js +2 -2
- package/dist/meter.js.map +1 -1
- package/dist/motion.css +2 -2
- package/dist/native.js +4 -4
- package/dist/native.js.map +1 -1
- package/dist/nav-bar.js +7 -7
- package/dist/nav-bar.js.map +1 -1
- package/dist/product-page.js +1 -6
- package/dist/product-page.js.map +1 -1
- package/dist/progress.js +3 -3
- package/dist/progress.js.map +1 -1
- package/dist/rail-nav.js +1 -1
- package/dist/rail-nav.js.map +1 -1
- package/dist/service-card.js +2 -2
- package/dist/service-card.js.map +1 -1
- package/dist/settings-layout.js +1 -1
- package/dist/settings-layout.js.map +1 -1
- package/dist/site-footer.js +2 -2
- package/dist/site-footer.js.map +1 -1
- package/dist/skip-link.js +1 -1
- package/dist/slot-picker.js +1 -1
- package/dist/slot-picker.js.map +1 -1
- package/dist/step-progress.js +1 -1
- package/dist/step-progress.js.map +1 -1
- package/dist/tabs.js +4 -4
- package/dist/tabs.js.map +1 -1
- package/dist/tag-input.js +2 -2
- package/dist/tag-selector.js +1 -1
- package/dist/toast.js +1 -1
- package/dist/toast.js.map +1 -1
- package/package.json +5 -5
- package/registry/activity-feed.json +1 -1
- package/registry/app-shell.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/card.json +1 -1
- package/registry/chip-row.json +1 -1
- package/registry/chip.json +1 -1
- package/registry/examples/badge/native/dot.dark.png +0 -0
- package/registry/examples/badge/native/dot.light.png +0 -0
- package/registry/examples/badge/native/scrim.dark.png +0 -0
- package/registry/examples/badge/native/scrim.light.png +0 -0
- package/registry/examples/badge/native/tones.dark.png +0 -0
- package/registry/examples/badge/native/tones.light.png +0 -0
- package/registry/examples/card/native/default.dark.png +0 -0
- package/registry/examples/card/native/default.light.png +0 -0
- package/registry/examples/card/native/elevated.dark.png +0 -0
- package/registry/examples/card/native/elevated.light.png +0 -0
- package/registry/examples/description-list/native/job-details.dark.png +0 -0
- package/registry/examples/description-list/native/job-details.light.png +0 -0
- package/registry/examples/meta-row/native/video.dark.png +0 -0
- package/registry/examples/meta-row/native/video.light.png +0 -0
- package/registry/examples/swipe-deck/native/discover.dark.png +0 -0
- package/registry/examples/swipe-deck/native/discover.light.png +0 -0
- package/registry/index.json +5 -5
- package/registry/landing-page.json +1 -1
- package/registry/language-switch.json +1 -1
- package/registry/map.json +1 -1
- package/registry/meter.json +1 -1
- package/registry/nav-bar.json +2 -2
- package/registry/product-page.json +1 -1
- package/registry/progress.json +1 -1
- package/registry/rail-nav.json +2 -2
- package/registry/service-card.json +1 -1
- package/registry/settings-layout.json +1 -1
- package/registry/site-footer.json +2 -2
- package/registry/skip-link.json +1 -1
- package/registry/slot-picker.json +1 -1
- package/registry/step-progress.json +1 -1
- package/registry/tabs.json +1 -1
- package/registry/toast.json +1 -1
- package/dist/chunk-63KCDMX2.js.map +0 -1
- package/dist/chunk-QDXV544C.js.map +0 -1
- /package/dist/{chunk-CKSHLSDH.js.map → chunk-AAP3U66V.js.map} +0 -0
- /package/dist/{chunk-K2ZYB4HP.js.map → chunk-S5LP7KKW.js.map} +0 -0
- /package/dist/{chunk-YOTQD7JD.js.map → chunk-WYQ277F2.js.map} +0 -0
package/registry/app-shell.json
CHANGED
|
@@ -368,7 +368,7 @@
|
|
|
368
368
|
"files": [
|
|
369
369
|
{
|
|
370
370
|
"path": "app-shell/app-shell.tsx",
|
|
371
|
-
"content": "// Client (the collapse, the drawer, the shortcuts). The shell of an application screen, read from the products' consoles\n// (Orochia admin and studio, Orazaka web-admin and web-client): a sidebar of sections that collapses to its icons on a\n// desktop, a header with the screen's title, a search that opens the app's command palette and its actions, the content —\n// and below `md` the sidebar becomes a drawer opened from the header (Radix Dialog: focus trap, Escape, focus return).\n// Keyboard: ⌘/Ctrl+B folds the sidebar, ⌘/Ctrl+K opens the search, `g` then a letter goes to a section (`shortcut` of an\n// item). Words arrive as props; links through `asChild`.\n// `desktop` (ADR-002): the shell is the window of a desktop app (the Orazaka Mac app, Tauri 2): the sidebar is always\n// there (a window has a minimum width, no drawer), its top clears the traffic lights, the header and the sidebar's top are\n// the title bar (`data-tauri-drag-region`: dragging them moves the window), the density is the desktop's (32 px rows,\n// 13 px text, a 48 px header), the chrome is not selectable, and the content scrolls under a fixed header.\nimport { SkipLink } from \"@krizaka/ui/skip-link\";\nimport { Dialog as DialogPrimitive, Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\nimport { ChevronGlyph, CloseGlyph, SearchGlyph } from \"../glyphs\";\nimport { Kbd } from \"../kbd\";\nimport { ariaKeyShortcuts, formatShortcut, useApple, useShortcuts } from \"./shortcuts\";\n\nexport const appShell = tv({\n slots: {\n root: \"flex min-h-dvh w-full bg-surface-0 text-fg data-desktop:h-[var(--kz-window-height,100dvh)] data-desktop:overflow-hidden\",\n sidebar:\n \"sticky top-0 hidden h-dvh shrink-0 flex-col border-r border-border-subtle bg-surface-1 transition-[width] duration-300 motion-reduce:transition-none md:flex \" +\n \"w-64 data-collapsed:w-[4.25rem] \" +\n \"[[data-desktop]_&]:static [[data-desktop]_&]:flex [[data-desktop]_&]:h-auto [[data-desktop]_&]:w-60 [[data-desktop]_&]:select-none [[data-desktop]_&]:data-collapsed:w-[4.75rem]\",\n titlebar: \"h-[var(--kz-titlebar-inset,1.75rem)] shrink-0\",\n drawer:\n \"kz-dialog fixed inset-y-0 left-0 z-50 flex w-[min(20rem,88vw)] flex-col border-r border-border-default bg-surface-1 pl-[env(safe-area-inset-left)] shadow-lg focus:outline-hidden\",\n overlay: \"kz-overlay fixed inset-0 z-50 bg-overlay\",\n brand: \"flex h-14 shrink-0 items-center gap-2.5 px-4 font-display text-[15px] font-bold tracking-tight [[data-collapsed]_&]:justify-center [[data-collapsed]_&]:px-0 [[data-desktop]_&]:h-11\",\n nav: \"flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto px-2 py-2\",\n group: \"mt-3 mb-1 px-3 font-mono text-[10.5px] font-semibold text-fg-secondary [[data-collapsed]_&]:sr-only\",\n item:\n \"group/item relative flex h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-fg-secondary no-underline transition-colors \" +\n \"hover:bg-surface-2 hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring \" +\n \"aria-[current=page]:bg-accent-soft aria-[current=page]:text-fg \" +\n \"aria-[current=page]:before:absolute aria-[current=page]:before:inset-y-2 aria-[current=page]:before:-left-2 aria-[current=page]:before:w-1 aria-[current=page]:before:rounded-full aria-[current=page]:before:bg-accent \" +\n \"[[data-collapsed]_&]:justify-center [[data-collapsed]_&]:px-0 [&>svg]:h-[18px] [&>svg]:w-[18px] [&>svg]:shrink-0 \" +\n \"[[data-desktop]_&]:h-8 [[data-desktop]_&]:cursor-default [[data-desktop]_&]:rounded-md [[data-desktop]_&]:text-[13px] [[data-desktop]_&_svg]:h-4 [[data-desktop]_&_svg]:w-4\",\n itemLabel: \"min-w-0 flex-1 truncate [[data-collapsed]_&]:sr-only\",\n itemHint: \"ml-auto hidden items-center gap-0.5 text-fg-secondary group-hover/item:flex group-focus-visible/item:flex [[data-collapsed]_&]:!hidden\",\n itemBadge:\n \"ml-auto inline-flex min-w-5 items-center justify-center rounded-full bg-accent px-1.5 text-[11px] font-bold text-on-accent \" +\n \"[[data-collapsed]_&]:absolute [[data-collapsed]_&]:right-1.5 [[data-collapsed]_&]:top-1 [[data-collapsed]_&]:min-w-4 [[data-collapsed]_&]:px-1 [[data-collapsed]_&]:text-[10px]\",\n footer: \"shrink-0 border-t border-border-subtle p-2\",\n collapse:\n \"hidden h-9 w-full items-center gap-2 rounded-lg px-3 text-xs font-medium text-fg-secondary hover:bg-surface-2 hover:text-fg md:flex \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring [[data-collapsed]_&]:justify-center [[data-collapsed]_&]:px-0\",\n body: \"flex min-w-0 flex-1 flex-col [[data-desktop]_&]:min-h-0 [[data-desktop]_&]:overflow-y-auto\",\n header:\n \"sticky top-0 z-30 flex h-14 shrink-0 items-center gap-3 border-b border-border-subtle bg-surface-0 px-4 md:px-6 \" +\n \"[[data-desktop]_&]:h-12 [[data-desktop]_&]:select-none [[data-desktop]_&]:px-5\",\n menuButton:\n \"inline-flex h-9 w-9 items-center justify-center rounded-lg border border-border-subtle bg-surface-1 text-fg md:hidden [[data-desktop]_&]:hidden \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n title: \"min-w-0 flex-1 truncate font-display text-base font-bold tracking-tight [[data-desktop]_&]:text-[15px]\",\n search:\n \"inline-flex h-9 min-w-9 items-center justify-center gap-2 rounded-lg border border-border-subtle bg-surface-1 px-2.5 text-sm text-fg-secondary transition-colors \" +\n \"hover:border-border-default hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring md:w-56 md:justify-start [&>svg]:h-4 [&>svg]:w-4 [&>svg]:shrink-0\",\n searchLabel: \"sr-only md:not-sr-only md:flex-1 md:truncate md:text-left\",\n searchKeys: \"hidden items-center gap-0.5 md:flex\",\n actions: \"ml-auto flex shrink-0 items-center gap-2\",\n main: \"min-w-0 flex-1 px-4 py-6 md:px-6 md:py-8 [[data-desktop]_&]:px-6 [[data-desktop]_&]:py-6\",\n },\n});\nconst s = appShell();\n\ninterface ShellState {\n collapsed: boolean;\n setCollapsed: (value: boolean) => void;\n open: boolean;\n setOpen: (value: boolean) => void;\n inDrawer: boolean;\n desktop: boolean;\n}\nconst ShellContext = React.createContext<ShellState | null>(null);\nconst useShell = () => {\n const ctx = React.useContext(ShellContext);\n if (!ctx) throw new Error(\"AppShell parts must be inside <AppShell.Root>\");\n return ctx;\n};\n\nexport type AppShellRootProps = React.ComponentProps<\"div\"> & {\n /** The sidebar starts collapsed to its icons (desktop). */\n defaultCollapsed?: boolean;\n /** Controlled collapse (desktop): pair with `onCollapsedChange` to persist the choice. */\n collapsed?: boolean;\n /** Called when the sidebar collapses or expands. */\n onCollapsedChange?: (collapsed: boolean) => void;\n /** The words of the \"skip to content\" link (`Skip to content`), the first tab stop; it lands on `AppShell.Main`. Without it, no link. */\n skipLabel?: string;\n /**\n * The shortcut that folds and unfolds the sidebar (a combination: `mod+b` is ⌘B on a Mac, Ctrl+B elsewhere), or `false`\n * for none.\n * @default \"mod+b\"\n */\n collapseShortcut?: string | false;\n /**\n * The shell is a desktop app's window (the Orazaka Mac app, Tauri 2 — ADR-002): no drawer, the sidebar's top clears the\n * window's traffic lights (`--kz-titlebar-inset`, 28 px; the window's height is `--kz-window-height`, the viewport by default), the header and that top move the window\n * (`data-tauri-drag-region`), a denser layout, the chrome not selectable. In a browser it is a fixed-height shell.\n */\n desktop?: boolean;\n};\n\n/** The shell: holds the collapse (desktop) and the drawer (phone) state. Children: Sidebar, then Body. */\nexport function AppShellRoot({ defaultCollapsed = false, collapsed: controlled, onCollapsedChange, skipLabel, collapseShortcut = \"mod+b\", desktop = false, className, children, ...props }: AppShellRootProps) {\n const [own, setOwn] = React.useState(defaultCollapsed);\n const [open, setOpen] = React.useState(false);\n const collapsed = controlled ?? own;\n const setCollapsed = React.useCallback(\n (value: boolean) => {\n if (controlled === undefined) setOwn(value);\n onCollapsedChange?.(value);\n },\n [controlled, onCollapsedChange],\n );\n const value = React.useMemo(() => ({ collapsed, setCollapsed, open, setOpen, inDrawer: false, desktop }), [collapsed, setCollapsed, open, desktop]);\n useShortcuts(collapseShortcut ? { [collapseShortcut]: () => setCollapsed(!collapsed) } : {});\n return (\n <ShellContext.Provider value={value}>\n <div data-desktop={desktop ? \"\" : undefined} className={s.root({ className })} {...props}>\n {skipLabel && <SkipLink href=\"#content\">{skipLabel}</SkipLink>}\n {children}\n </div>\n </ShellContext.Provider>\n );\n}\n\nexport type AppShellSidebarProps = Omit<React.ComponentProps<\"nav\">, \"children\"> & {\n /** The navigation's accessible name (`Studio`, `Administration`). */\n label: string;\n /** The brand at the top: the product's mark and name (a link home through `asChild` of your router, or plain). */\n brand?: React.ReactNode;\n /** The sections: `AppShell.Group` and `AppShell.Item`. */\n children: React.ReactNode;\n /** At the bottom, above the collapse button: the account, a plan, the version. */\n footer?: React.ReactNode;\n /** The collapse button's words, `[collapse, expand]` (desktop only). Without it the sidebar cannot collapse. */\n collapseLabels?: readonly [string, string];\n /** The drawer's close button words (phone). */\n closeLabel: string;\n};\n\nfunction SidebarContent({ brand, children, footer, collapseLabels, closeLabel }: Pick<AppShellSidebarProps, \"brand\" | \"children\" | \"footer\" | \"collapseLabels\" | \"closeLabel\">) {\n const { collapsed, setCollapsed, inDrawer, desktop } = useShell();\n const drag = desktop ? { \"data-tauri-drag-region\": \"\" } : {};\n return (\n <>\n {desktop && <div aria-hidden className={s.titlebar()} {...drag} />}\n <div className={s.brand()} {...drag}>\n {brand}\n {inDrawer && (\n <DialogPrimitive.Close className={cn(s.menuButton(), \"ml-auto flex\")} aria-label={closeLabel}>\n <CloseGlyph className=\"h-4 w-4\" />\n </DialogPrimitive.Close>\n )}\n </div>\n <div className={s.nav()}>{children}</div>\n {(footer || (collapseLabels && !inDrawer)) && (\n <div className={s.footer()}>\n {footer}\n {collapseLabels && !inDrawer && (\n <button type=\"button\" className={s.collapse()} aria-expanded={!collapsed} onClick={() => setCollapsed(!collapsed)}>\n <ChevronGlyph direction={collapsed ? \"right\" : \"left\"} className=\"h-4 w-4\" />\n <span className={collapsed ? \"sr-only\" : undefined}>{collapsed ? collapseLabels[1] : collapseLabels[0]}</span>\n </button>\n )}\n </div>\n )}\n </>\n );\n}\n\n/** The sidebar: on a desktop a column that collapses to its icons; on a phone the same content in a drawer. */\nexport function AppShellSidebar({ label, brand, children, footer, collapseLabels, closeLabel, className, ...props }: AppShellSidebarProps) {\n const shell = useShell();\n const content = { brand, children, footer, collapseLabels, closeLabel };\n return (\n <>\n <nav aria-label={label} data-collapsed={shell.collapsed ? \"\" : undefined} className={s.sidebar({ className })} {...props}>\n <SidebarContent {...content} />\n </nav>\n {!shell.desktop && (\n <DialogPrimitive.Root open={shell.open} onOpenChange={shell.setOpen}>\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay className={s.overlay()} />\n <DialogPrimitive.Content className={s.drawer()} aria-describedby={undefined}>\n <DialogPrimitive.Title className=\"sr-only\">{label}</DialogPrimitive.Title>\n <ShellContext.Provider value={{ ...shell, collapsed: false, inDrawer: true }}>\n <nav aria-label={label} className=\"flex min-h-0 flex-1 flex-col\" onClick={(e) => (e.target as Element).closest(\"a\") && shell.setOpen(false)}>\n <SidebarContent {...content} />\n </nav>\n </ShellContext.Provider>\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n </DialogPrimitive.Root>\n )}\n </>\n );\n}\n\nexport type AppShellGroupProps = React.ComponentProps<\"p\"> & {\n /** The group's name, above its items (hidden when the sidebar is collapsed). */\n children: React.ReactNode;\n};\n\n/** A heading between groups of items. */\nexport function AppShellGroup({ className, ...props }: AppShellGroupProps) {\n return <p className={s.group({ className })} {...props} />;\n}\n\nexport type AppShellItemProps = Omit<React.ComponentProps<\"a\">, \"children\"> & {\n /** The item's icon (a signature icon): it stays when the sidebar collapses. */\n icon?: React.ReactNode;\n /** Its words: the accessible name, hidden visually when collapsed (the icon carries a tooltip through `title`). */\n label: string;\n /** The screen it opens is the current one: `aria-current=\"page\"`. */\n current?: boolean;\n /** A count beside it (unread, pending): a number or a few characters. */\n badge?: React.ReactNode;\n /**\n * A keyboard shortcut that goes to it: a sequence (`g d`: G then D) or a combination (`mod+1`). It clicks the item, so a\n * router link navigates. The keys are shown on hover and focus when the sidebar is open.\n */\n shortcut?: string;\n /** Renders your link (Next's `Link`) with the item's look: pass it as the only child. */\n asChild?: boolean;\n /** Your link element when `asChild`. */\n children?: React.ReactElement;\n};\n\n/** A destination of the sidebar: icon, words, a count. */\nexport function AppShellItem({ icon, label, current, badge, shortcut, asChild, children, className, ...props }: AppShellItemProps) {\n const { collapsed, inDrawer } = useShell();\n const element = React.useRef<HTMLAnchorElement>(null);\n const apple = useApple();\n // Only the sidebar's own items listen: the drawer holds a copy of them, open or not.\n useShortcuts(shortcut && !inDrawer ? { [shortcut]: () => element.current?.click() } : {});\n const Comp = asChild ? Slot.Root : \"a\";\n const inner = (\n <>\n {icon}\n <span className={s.itemLabel()}>{label}</span>\n {badge != null && <span className={s.itemBadge()}>{badge}</span>}\n {shortcut && !inDrawer ? (\n <span className={s.itemHint()} aria-hidden>\n {formatShortcut(shortcut, apple).map((key) => (\n <Kbd key={key} size=\"sm\">\n {key}\n </Kbd>\n ))}\n </span>\n ) : null}\n </>\n );\n return (\n <Comp ref={element} aria-current={current ? \"page\" : undefined} aria-keyshortcuts={shortcut ? ariaKeyShortcuts(shortcut, apple) : undefined} title={collapsed ? label : undefined} className={s.item({ className })} {...props}>\n {asChild && children ? React.cloneElement(children, undefined, inner) : inner}\n </Comp>\n );\n}\n\n/** The column right of the sidebar: Header, then Main. */\nexport function AppShellBody({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div className={s.body({ className })} {...props} />;\n}\n\nexport type AppShellHeaderProps = Omit<React.ComponentProps<\"header\">, \"title\"> & {\n /** The screen's title (an `h1`). */\n title?: React.ReactNode;\n /** The button that opens the drawer on a phone: its words. */\n menuLabel: string;\n /** The screen's actions, on the right. */\n actions?: React.ReactNode;\n};\n\n/** The top of the screen: the drawer's button (phone), the title, the actions. Sticky. */\nexport function AppShellHeader({ title, menuLabel, actions, className, children, ...props }: AppShellHeaderProps) {\n const { open, setOpen, desktop } = useShell();\n const drag = desktop ? { \"data-tauri-drag-region\": \"\" } : {};\n return (\n <header className={s.header({ className })} {...drag} {...props}>\n <button type=\"button\" className={s.menuButton()} aria-label={menuLabel} aria-expanded={open} aria-haspopup=\"dialog\" onClick={() => setOpen(true)}>\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n <path d=\"M4 7h16M4 12h16M4 17h10\" />\n </svg>\n </button>\n {title && (\n <h1 className={s.title()} {...drag}>\n {title}\n </h1>\n )}\n {children}\n {actions && <div className={s.actions()}>{actions}</div>}\n </header>\n );\n}\n\nexport type AppShellSearchProps = Omit<React.ComponentProps<\"button\">, \"children\" | \"onClick\"> & {\n /** The words (\"Search\", \"Search creators\"): the accessible name, shown beside the keys on a desktop. */\n label: string;\n /** Called when the button is pressed or the shortcut is typed: open your command palette (`CommandDialog`) here. */\n onOpen: () => void;\n /**\n * The shortcut that opens it (a combination); shown on its keys.\n * @default \"mod+k\"\n */\n shortcut?: string;\n};\n\n/** The header's search: a button that looks like a field (a magnifier, its words, ⌘K) and opens the app's command palette. */\nexport function AppShellSearch({ label, onOpen, shortcut = \"mod+k\", className, ...props }: AppShellSearchProps) {\n const apple = useApple();\n useShortcuts({ [shortcut]: () => onOpen() });\n return (\n <button type=\"button\" className={s.search({ className })} aria-keyshortcuts={ariaKeyShortcuts(shortcut, apple)} onClick={onOpen} {...props}>\n <SearchGlyph />\n <span className={s.searchLabel()}>{label}</span>\n <span className={s.searchKeys()} aria-hidden>\n {formatShortcut(shortcut, apple).map((key) => (\n <Kbd key={key} size=\"sm\">\n {key}\n </Kbd>\n ))}\n </span>\n </button>\n );\n}\n\n/** The screen's content. */\nexport function AppShellMain({ className, ...props }: React.ComponentProps<\"main\">) {\n return <main id=\"content\" tabIndex={-1} className={s.main({ className })} {...props} />;\n}\n\nexport const AppShell = {\n Root: AppShellRoot,\n Sidebar: AppShellSidebar,\n Group: AppShellGroup,\n Item: AppShellItem,\n Body: AppShellBody,\n Header: AppShellHeader,\n Search: AppShellSearch,\n Main: AppShellMain,\n};\n"
|
|
371
|
+
"content": "// Client (the collapse, the drawer, the shortcuts). The shell of an application screen, read from the products' consoles\n// (Orochia admin and studio, Orazaka web-admin and web-client): a sidebar of sections that collapses to its icons on a\n// desktop, a header with the screen's title, a search that opens the app's command palette and its actions, the content —\n// and below `md` the sidebar becomes a drawer opened from the header (Radix Dialog: focus trap, Escape, focus return).\n// Keyboard: ⌘/Ctrl+B folds the sidebar, ⌘/Ctrl+K opens the search, `g` then a letter goes to a section (`shortcut` of an\n// item). Words arrive as props; links through `asChild`.\n// `desktop` (ADR-002): the shell is the window of a desktop app (the Orazaka Mac app, Tauri 2): the sidebar is always\n// there (a window has a minimum width, no drawer), its top clears the traffic lights, the header and the sidebar's top are\n// the title bar (`data-tauri-drag-region`: dragging them moves the window), the density is the desktop's (32 px rows,\n// 13 px text, a 48 px header), the chrome is not selectable, and the content scrolls under a fixed header.\nimport { SkipLink } from \"@krizaka/ui/skip-link\";\nimport { Dialog as DialogPrimitive, Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\nimport { ChevronGlyph, CloseGlyph, SearchGlyph } from \"../glyphs\";\nimport { Kbd } from \"../kbd\";\nimport { ariaKeyShortcuts, formatShortcut, useApple, useShortcuts } from \"./shortcuts\";\n\nexport const appShell = tv({\n slots: {\n root: \"flex min-h-dvh w-full bg-surface-0 text-fg data-desktop:h-[var(--kz-window-height,100dvh)] data-desktop:overflow-hidden\",\n sidebar:\n \"sticky top-0 hidden h-dvh shrink-0 flex-col border-r border-border-subtle bg-surface-1 transition-[width] duration-300 motion-reduce:transition-none md:flex \" +\n \"w-64 data-collapsed:w-[4.25rem] \" +\n \"[[data-desktop]_&]:static [[data-desktop]_&]:flex [[data-desktop]_&]:h-auto [[data-desktop]_&]:w-60 [[data-desktop]_&]:select-none [[data-desktop]_&]:data-collapsed:w-[4.75rem]\",\n titlebar: \"h-[var(--kz-titlebar-inset,1.75rem)] shrink-0\",\n drawer:\n \"kz-dialog fixed inset-y-0 left-0 z-50 flex w-[min(20rem,88vw)] flex-col border-r border-border-default bg-surface-1 pl-[env(safe-area-inset-left)] shadow-lg focus:outline-hidden\",\n overlay: \"kz-overlay fixed inset-0 z-50 bg-overlay\",\n brand: \"flex h-14 shrink-0 items-center gap-2.5 px-4 font-display text-[15px] font-bold tracking-tight [[data-collapsed]_&]:justify-center [[data-collapsed]_&]:px-0 [[data-desktop]_&]:h-11\",\n nav: \"flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto px-2 py-2\",\n group: \"mt-3 mb-1 px-3 font-mono text-[10.5px] font-semibold text-fg-secondary [[data-collapsed]_&]:sr-only\",\n item:\n \"group/item relative flex h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-fg-secondary no-underline transition-colors \" +\n \"hover:bg-surface-2 hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring \" +\n \"aria-[current=page]:bg-accent-soft aria-[current=page]:text-fg \" +\n \"aria-[current=page]:before:absolute aria-[current=page]:before:inset-y-2 aria-[current=page]:before:-left-2 aria-[current=page]:before:w-1 aria-[current=page]:before:rounded-sm aria-[current=page]:before:bg-accent \" +\n \"[[data-collapsed]_&]:justify-center [[data-collapsed]_&]:px-0 [&>svg]:h-[18px] [&>svg]:w-[18px] [&>svg]:shrink-0 \" +\n \"[[data-desktop]_&]:h-8 [[data-desktop]_&]:cursor-default [[data-desktop]_&]:rounded-md [[data-desktop]_&]:text-[13px] [[data-desktop]_&_svg]:h-4 [[data-desktop]_&_svg]:w-4\",\n itemLabel: \"min-w-0 flex-1 truncate [[data-collapsed]_&]:sr-only\",\n itemHint: \"ml-auto hidden items-center gap-0.5 text-fg-secondary group-hover/item:flex group-focus-visible/item:flex [[data-collapsed]_&]:!hidden\",\n itemBadge:\n \"ml-auto inline-flex min-w-5 items-center justify-center rounded-sm bg-accent px-1.5 text-[11px] font-bold text-on-accent \" +\n \"[[data-collapsed]_&]:absolute [[data-collapsed]_&]:right-1.5 [[data-collapsed]_&]:top-1 [[data-collapsed]_&]:min-w-4 [[data-collapsed]_&]:px-1 [[data-collapsed]_&]:text-[10px]\",\n footer: \"shrink-0 border-t border-border-subtle p-2\",\n collapse:\n \"hidden h-9 w-full items-center gap-2 rounded-lg px-3 text-xs font-medium text-fg-secondary hover:bg-surface-2 hover:text-fg md:flex \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring [[data-collapsed]_&]:justify-center [[data-collapsed]_&]:px-0\",\n body: \"flex min-w-0 flex-1 flex-col [[data-desktop]_&]:min-h-0 [[data-desktop]_&]:overflow-y-auto\",\n header:\n \"sticky top-0 z-30 flex h-14 shrink-0 items-center gap-3 border-b border-border-subtle bg-surface-0 px-4 md:px-6 \" +\n \"[[data-desktop]_&]:h-12 [[data-desktop]_&]:select-none [[data-desktop]_&]:px-5\",\n menuButton:\n \"inline-flex h-9 w-9 items-center justify-center rounded-lg border border-border-subtle bg-surface-1 text-fg md:hidden [[data-desktop]_&]:hidden \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n title: \"min-w-0 flex-1 truncate font-display text-base font-bold tracking-tight [[data-desktop]_&]:text-[15px]\",\n search:\n \"inline-flex h-9 min-w-9 items-center justify-center gap-2 rounded-lg border border-border-subtle bg-surface-1 px-2.5 text-sm text-fg-secondary transition-colors \" +\n \"hover:border-border-default hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring md:w-56 md:justify-start [&>svg]:h-4 [&>svg]:w-4 [&>svg]:shrink-0\",\n searchLabel: \"sr-only md:not-sr-only md:flex-1 md:truncate md:text-left\",\n searchKeys: \"hidden items-center gap-0.5 md:flex\",\n actions: \"ml-auto flex shrink-0 items-center gap-2\",\n main: \"min-w-0 flex-1 px-4 py-6 md:px-6 md:py-8 [[data-desktop]_&]:px-6 [[data-desktop]_&]:py-6\",\n },\n});\nconst s = appShell();\n\ninterface ShellState {\n collapsed: boolean;\n setCollapsed: (value: boolean) => void;\n open: boolean;\n setOpen: (value: boolean) => void;\n inDrawer: boolean;\n desktop: boolean;\n}\nconst ShellContext = React.createContext<ShellState | null>(null);\nconst useShell = () => {\n const ctx = React.useContext(ShellContext);\n if (!ctx) throw new Error(\"AppShell parts must be inside <AppShell.Root>\");\n return ctx;\n};\n\nexport type AppShellRootProps = React.ComponentProps<\"div\"> & {\n /** The sidebar starts collapsed to its icons (desktop). */\n defaultCollapsed?: boolean;\n /** Controlled collapse (desktop): pair with `onCollapsedChange` to persist the choice. */\n collapsed?: boolean;\n /** Called when the sidebar collapses or expands. */\n onCollapsedChange?: (collapsed: boolean) => void;\n /** The words of the \"skip to content\" link (`Skip to content`), the first tab stop; it lands on `AppShell.Main`. Without it, no link. */\n skipLabel?: string;\n /**\n * The shortcut that folds and unfolds the sidebar (a combination: `mod+b` is ⌘B on a Mac, Ctrl+B elsewhere), or `false`\n * for none.\n * @default \"mod+b\"\n */\n collapseShortcut?: string | false;\n /**\n * The shell is a desktop app's window (the Orazaka Mac app, Tauri 2 — ADR-002): no drawer, the sidebar's top clears the\n * window's traffic lights (`--kz-titlebar-inset`, 28 px; the window's height is `--kz-window-height`, the viewport by default), the header and that top move the window\n * (`data-tauri-drag-region`), a denser layout, the chrome not selectable. In a browser it is a fixed-height shell.\n */\n desktop?: boolean;\n};\n\n/** The shell: holds the collapse (desktop) and the drawer (phone) state. Children: Sidebar, then Body. */\nexport function AppShellRoot({ defaultCollapsed = false, collapsed: controlled, onCollapsedChange, skipLabel, collapseShortcut = \"mod+b\", desktop = false, className, children, ...props }: AppShellRootProps) {\n const [own, setOwn] = React.useState(defaultCollapsed);\n const [open, setOpen] = React.useState(false);\n const collapsed = controlled ?? own;\n const setCollapsed = React.useCallback(\n (value: boolean) => {\n if (controlled === undefined) setOwn(value);\n onCollapsedChange?.(value);\n },\n [controlled, onCollapsedChange],\n );\n const value = React.useMemo(() => ({ collapsed, setCollapsed, open, setOpen, inDrawer: false, desktop }), [collapsed, setCollapsed, open, desktop]);\n useShortcuts(collapseShortcut ? { [collapseShortcut]: () => setCollapsed(!collapsed) } : {});\n return (\n <ShellContext.Provider value={value}>\n <div data-desktop={desktop ? \"\" : undefined} className={s.root({ className })} {...props}>\n {skipLabel && <SkipLink href=\"#content\">{skipLabel}</SkipLink>}\n {children}\n </div>\n </ShellContext.Provider>\n );\n}\n\nexport type AppShellSidebarProps = Omit<React.ComponentProps<\"nav\">, \"children\"> & {\n /** The navigation's accessible name (`Studio`, `Administration`). */\n label: string;\n /** The brand at the top: the product's mark and name (a link home through `asChild` of your router, or plain). */\n brand?: React.ReactNode;\n /** The sections: `AppShell.Group` and `AppShell.Item`. */\n children: React.ReactNode;\n /** At the bottom, above the collapse button: the account, a plan, the version. */\n footer?: React.ReactNode;\n /** The collapse button's words, `[collapse, expand]` (desktop only). Without it the sidebar cannot collapse. */\n collapseLabels?: readonly [string, string];\n /** The drawer's close button words (phone). */\n closeLabel: string;\n};\n\nfunction SidebarContent({ brand, children, footer, collapseLabels, closeLabel }: Pick<AppShellSidebarProps, \"brand\" | \"children\" | \"footer\" | \"collapseLabels\" | \"closeLabel\">) {\n const { collapsed, setCollapsed, inDrawer, desktop } = useShell();\n const drag = desktop ? { \"data-tauri-drag-region\": \"\" } : {};\n return (\n <>\n {desktop && <div aria-hidden className={s.titlebar()} {...drag} />}\n <div className={s.brand()} {...drag}>\n {brand}\n {inDrawer && (\n <DialogPrimitive.Close className={cn(s.menuButton(), \"ml-auto flex\")} aria-label={closeLabel}>\n <CloseGlyph className=\"h-4 w-4\" />\n </DialogPrimitive.Close>\n )}\n </div>\n <div className={s.nav()}>{children}</div>\n {(footer || (collapseLabels && !inDrawer)) && (\n <div className={s.footer()}>\n {footer}\n {collapseLabels && !inDrawer && (\n <button type=\"button\" className={s.collapse()} aria-expanded={!collapsed} onClick={() => setCollapsed(!collapsed)}>\n <ChevronGlyph direction={collapsed ? \"right\" : \"left\"} className=\"h-4 w-4\" />\n <span className={collapsed ? \"sr-only\" : undefined}>{collapsed ? collapseLabels[1] : collapseLabels[0]}</span>\n </button>\n )}\n </div>\n )}\n </>\n );\n}\n\n/** The sidebar: on a desktop a column that collapses to its icons; on a phone the same content in a drawer. */\nexport function AppShellSidebar({ label, brand, children, footer, collapseLabels, closeLabel, className, ...props }: AppShellSidebarProps) {\n const shell = useShell();\n const content = { brand, children, footer, collapseLabels, closeLabel };\n return (\n <>\n <nav aria-label={label} data-collapsed={shell.collapsed ? \"\" : undefined} className={s.sidebar({ className })} {...props}>\n <SidebarContent {...content} />\n </nav>\n {!shell.desktop && (\n <DialogPrimitive.Root open={shell.open} onOpenChange={shell.setOpen}>\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay className={s.overlay()} />\n <DialogPrimitive.Content className={s.drawer()} aria-describedby={undefined}>\n <DialogPrimitive.Title className=\"sr-only\">{label}</DialogPrimitive.Title>\n <ShellContext.Provider value={{ ...shell, collapsed: false, inDrawer: true }}>\n <nav aria-label={label} className=\"flex min-h-0 flex-1 flex-col\" onClick={(e) => (e.target as Element).closest(\"a\") && shell.setOpen(false)}>\n <SidebarContent {...content} />\n </nav>\n </ShellContext.Provider>\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n </DialogPrimitive.Root>\n )}\n </>\n );\n}\n\nexport type AppShellGroupProps = React.ComponentProps<\"p\"> & {\n /** The group's name, above its items (hidden when the sidebar is collapsed). */\n children: React.ReactNode;\n};\n\n/** A heading between groups of items. */\nexport function AppShellGroup({ className, ...props }: AppShellGroupProps) {\n return <p className={s.group({ className })} {...props} />;\n}\n\nexport type AppShellItemProps = Omit<React.ComponentProps<\"a\">, \"children\"> & {\n /** The item's icon (a signature icon): it stays when the sidebar collapses. */\n icon?: React.ReactNode;\n /** Its words: the accessible name, hidden visually when collapsed (the icon carries a tooltip through `title`). */\n label: string;\n /** The screen it opens is the current one: `aria-current=\"page\"`. */\n current?: boolean;\n /** A count beside it (unread, pending): a number or a few characters. */\n badge?: React.ReactNode;\n /**\n * A keyboard shortcut that goes to it: a sequence (`g d`: G then D) or a combination (`mod+1`). It clicks the item, so a\n * router link navigates. The keys are shown on hover and focus when the sidebar is open.\n */\n shortcut?: string;\n /** Renders your link (Next's `Link`) with the item's look: pass it as the only child. */\n asChild?: boolean;\n /** Your link element when `asChild`. */\n children?: React.ReactElement;\n};\n\n/** A destination of the sidebar: icon, words, a count. */\nexport function AppShellItem({ icon, label, current, badge, shortcut, asChild, children, className, ...props }: AppShellItemProps) {\n const { collapsed, inDrawer } = useShell();\n const element = React.useRef<HTMLAnchorElement>(null);\n const apple = useApple();\n // Only the sidebar's own items listen: the drawer holds a copy of them, open or not.\n useShortcuts(shortcut && !inDrawer ? { [shortcut]: () => element.current?.click() } : {});\n const Comp = asChild ? Slot.Root : \"a\";\n const inner = (\n <>\n {icon}\n <span className={s.itemLabel()}>{label}</span>\n {badge != null && <span className={s.itemBadge()}>{badge}</span>}\n {shortcut && !inDrawer ? (\n <span className={s.itemHint()} aria-hidden>\n {formatShortcut(shortcut, apple).map((key) => (\n <Kbd key={key} size=\"sm\">\n {key}\n </Kbd>\n ))}\n </span>\n ) : null}\n </>\n );\n return (\n <Comp ref={element} aria-current={current ? \"page\" : undefined} aria-keyshortcuts={shortcut ? ariaKeyShortcuts(shortcut, apple) : undefined} title={collapsed ? label : undefined} className={s.item({ className })} {...props}>\n {asChild && children ? React.cloneElement(children, undefined, inner) : inner}\n </Comp>\n );\n}\n\n/** The column right of the sidebar: Header, then Main. */\nexport function AppShellBody({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div className={s.body({ className })} {...props} />;\n}\n\nexport type AppShellHeaderProps = Omit<React.ComponentProps<\"header\">, \"title\"> & {\n /** The screen's title (an `h1`). */\n title?: React.ReactNode;\n /** The button that opens the drawer on a phone: its words. */\n menuLabel: string;\n /** The screen's actions, on the right. */\n actions?: React.ReactNode;\n};\n\n/** The top of the screen: the drawer's button (phone), the title, the actions. Sticky. */\nexport function AppShellHeader({ title, menuLabel, actions, className, children, ...props }: AppShellHeaderProps) {\n const { open, setOpen, desktop } = useShell();\n const drag = desktop ? { \"data-tauri-drag-region\": \"\" } : {};\n return (\n <header className={s.header({ className })} {...drag} {...props}>\n <button type=\"button\" className={s.menuButton()} aria-label={menuLabel} aria-expanded={open} aria-haspopup=\"dialog\" onClick={() => setOpen(true)}>\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n <path d=\"M4 7h16M4 12h16M4 17h10\" />\n </svg>\n </button>\n {title && (\n <h1 className={s.title()} {...drag}>\n {title}\n </h1>\n )}\n {children}\n {actions && <div className={s.actions()}>{actions}</div>}\n </header>\n );\n}\n\nexport type AppShellSearchProps = Omit<React.ComponentProps<\"button\">, \"children\" | \"onClick\"> & {\n /** The words (\"Search\", \"Search creators\"): the accessible name, shown beside the keys on a desktop. */\n label: string;\n /** Called when the button is pressed or the shortcut is typed: open your command palette (`CommandDialog`) here. */\n onOpen: () => void;\n /**\n * The shortcut that opens it (a combination); shown on its keys.\n * @default \"mod+k\"\n */\n shortcut?: string;\n};\n\n/** The header's search: a button that looks like a field (a magnifier, its words, ⌘K) and opens the app's command palette. */\nexport function AppShellSearch({ label, onOpen, shortcut = \"mod+k\", className, ...props }: AppShellSearchProps) {\n const apple = useApple();\n useShortcuts({ [shortcut]: () => onOpen() });\n return (\n <button type=\"button\" className={s.search({ className })} aria-keyshortcuts={ariaKeyShortcuts(shortcut, apple)} onClick={onOpen} {...props}>\n <SearchGlyph />\n <span className={s.searchLabel()}>{label}</span>\n <span className={s.searchKeys()} aria-hidden>\n {formatShortcut(shortcut, apple).map((key) => (\n <Kbd key={key} size=\"sm\">\n {key}\n </Kbd>\n ))}\n </span>\n </button>\n );\n}\n\n/** The screen's content. */\nexport function AppShellMain({ className, ...props }: React.ComponentProps<\"main\">) {\n return <main id=\"content\" tabIndex={-1} className={s.main({ className })} {...props} />;\n}\n\nexport const AppShell = {\n Root: AppShellRoot,\n Sidebar: AppShellSidebar,\n Group: AppShellGroup,\n Item: AppShellItem,\n Body: AppShellBody,\n Header: AppShellHeader,\n Search: AppShellSearch,\n Main: AppShellMain,\n};\n"
|
|
372
372
|
},
|
|
373
373
|
{
|
|
374
374
|
"path": "app-shell/index.ts",
|
package/registry/badge.json
CHANGED
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
},
|
|
252
252
|
{
|
|
253
253
|
"path": "badge/recipe.ts",
|
|
254
|
-
"content": "// The badge's tones and sizes, written once as classes of the Krizaka tokens: the web wears them with `tv(badgeRecipe)`,\n// React Native reads the same classes into styles (native/badge.tsx). Status tones are soft (a tint, a coloured dot, a\n// text role): the invariant status tokens are too light to carry small text on a light surface, or to sit under white\n// text (WCAG AA).\nimport { defineRecipe } from \"../recipes/define\";\n\nexport const badgeRecipe = defineRecipe({\n base: \"inline-flex shrink-0 items-center gap-1 whitespace-nowrap rounded-
|
|
254
|
+
"content": "// The badge's tones and sizes, written once as classes of the Krizaka tokens: the web wears them with `tv(badgeRecipe)`,\n// React Native reads the same classes into styles (native/badge.tsx). Status tones are soft (a tint, a coloured dot, a\n// text role): the invariant status tokens are too light to carry small text on a light surface, or to sit under white\n// text (WCAG AA).\nimport { defineRecipe } from \"../recipes/define\";\n\nexport const badgeRecipe = defineRecipe({\n base: \"inline-flex shrink-0 items-center gap-1 whitespace-nowrap rounded-sm font-semibold \",\n variants: {\n tone: {\n neutral: \"bg-surface-3 text-fg-secondary\",\n accent: \"bg-accent-soft text-fg ring-1 ring-inset ring-accent/40 [&>[data-dot]]:bg-accent\",\n success: \"bg-success/15 text-fg ring-1 ring-inset ring-success/40 [&>[data-dot]]:bg-success\",\n warning: \"bg-warning/15 text-fg ring-1 ring-inset ring-warning/40 [&>[data-dot]]:bg-warning\",\n danger: \"bg-danger/15 text-fg ring-1 ring-inset ring-danger/40 [&>[data-dot]]:bg-danger\",\n /** On a media: veil and text invariant, identical in both themes. */\n scrim: \"bg-scrim text-fg-on-media backdrop-blur-md\",\n },\n size: { sm: \"px-2 py-0.5 text-[10px]\", md: \"px-2.5 py-1 text-[11px]\" },\n pulse: { true: \"[&>[data-dot]]:motion-safe:animate-pulse\" },\n },\n defaultVariants: { tone: \"neutral\", size: \"sm\" },\n});\n"
|
|
255
255
|
},
|
|
256
256
|
{
|
|
257
257
|
"path": "meta.ts",
|
package/registry/card.json
CHANGED
|
@@ -437,7 +437,7 @@
|
|
|
437
437
|
},
|
|
438
438
|
{
|
|
439
439
|
"path": "card/recipe.ts",
|
|
440
|
-
"content": "// The card's parts, radii, tones and paddings, written once as classes of the Krizaka tokens: the web wears them with\n// `tv(cardRecipe)`, React Native reads the same classes into styles (native/card.tsx): a part of the card that exists only\n// on the web (the hover's tint) is simply ignored there.\nimport { defineRecipe } from \"../recipes/define\";\n\nexport const cardRecipe = defineRecipe({\n slots: {\n root:\n \"group relative flex flex-col overflow-hidden border border-border-default bg-surface-1 text-fg transition-colors \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n media: \"relative overflow-hidden bg-media\",\n image: \"h-full w-full object-cover\",\n fallback: \"flex h-full w-full items-center justify-center bg-linear-to-br from-accent/15 via-media to-accent-2/15 text-accent/60\",\n overlay: \"absolute z-10 flex items-center gap-1.5\",\n body: \"relative flex flex-1 flex-col gap-3\",\n title: \"line-clamp-1 text-sm font-semibold text-fg transition-colors group-hover:text-fg-accent\",\n description: \"line-clamp-2 text-xs text-fg-secondary\",\n stat: \"min-w-0\",\n statLabel: \"text-[
|
|
440
|
+
"content": "// The card's parts, radii, tones and paddings, written once as classes of the Krizaka tokens: the web wears them with\n// `tv(cardRecipe)`, React Native reads the same classes into styles (native/card.tsx): a part of the card that exists only\n// on the web (the hover's tint) is simply ignored there.\nimport { defineRecipe } from \"../recipes/define\";\n\nexport const cardRecipe = defineRecipe({\n slots: {\n root:\n \"group relative flex flex-col overflow-hidden border border-border-default bg-surface-1 text-fg transition-colors \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n media: \"relative overflow-hidden bg-media\",\n image: \"h-full w-full object-cover\",\n fallback: \"flex h-full w-full items-center justify-center bg-linear-to-br from-accent/15 via-media to-accent-2/15 text-accent/60\",\n overlay: \"absolute z-10 flex items-center gap-1.5\",\n body: \"relative flex flex-1 flex-col gap-3\",\n title: \"line-clamp-1 text-sm font-semibold text-fg transition-colors group-hover:text-fg-accent\",\n description: \"line-clamp-2 text-xs text-fg-secondary\",\n stat: \"min-w-0\",\n statLabel: \"font-mono text-[11.5px] text-fg-secondary\",\n statValue: \"font-display text-xl font-black tabular-nums text-fg\",\n footer: \"relative mt-auto flex items-center gap-2 border-t border-border-subtle pt-3 text-xs text-fg-secondary\",\n },\n variants: {\n radius: { md: { root: \"rounded-md\" }, lg: { root: \"rounded-lg\" }, xl: { root: \"rounded-xl\" } },\n tone: { default: {}, elevated: { root: \"bg-surface-2 shadow-md\" }, glass: { root: \"bg-glass border-glass-border\" } },\n interactive: { true: { root: \"duration-150 hover:border-border-strong hover:bg-surface-2\" } },\n padding: { none: { body: \"p-0\" }, sm: { body: \"p-3\" }, md: { body: \"p-4\" }, lg: { body: \"p-6\" } },\n },\n defaultVariants: { radius: \"xl\", tone: \"default\", padding: \"md\" },\n});\n"
|
|
441
441
|
},
|
|
442
442
|
{
|
|
443
443
|
"path": "meta.ts",
|
package/registry/chip-row.json
CHANGED
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"files": [
|
|
123
123
|
{
|
|
124
124
|
"path": "chip-row/chip-row.tsx",
|
|
125
|
-
"content": "// Client (arrow-key focus only). A row of chip links that scrolls sideways with scroll-snap on a phone (the trailing edge\n// fades, no scrollbar) and wraps from `md`: an audience picker, the filters of a page. Each item is a `ChipLink`-style\n// link at least 44 px high; the current one carries `aria-current`. Tab goes through the links; ← → Home End move between them.\nimport { Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { chipVariants } from \"../chip-link\";\n\nexport const chipRow = tv({\n slots: {\n root: \"flex w-full flex-col gap-2\",\n hint: \"text-xs text-fg-muted\",\n list:\n \"m-0 flex list-none snap-x snap-proximity gap-2 overflow-x-auto p-0 pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden \" +\n \"[mask-image:linear-gradient(to_right,#000_calc(100%-2rem),transparent)] md:flex-wrap md:overflow-visible md:pb-0 md:[mask-image:none]\",\n item: \"snap-start\",\n badge: \"rounded-
|
|
125
|
+
"content": "// Client (arrow-key focus only). A row of chip links that scrolls sideways with scroll-snap on a phone (the trailing edge\n// fades, no scrollbar) and wraps from `md`: an audience picker, the filters of a page. Each item is a `ChipLink`-style\n// link at least 44 px high; the current one carries `aria-current`. Tab goes through the links; ← → Home End move between them.\nimport { Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { chipVariants } from \"../chip-link\";\n\nexport const chipRow = tv({\n slots: {\n root: \"flex w-full flex-col gap-2\",\n hint: \"text-xs text-fg-muted\",\n list:\n \"m-0 flex list-none snap-x snap-proximity gap-2 overflow-x-auto p-0 pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden \" +\n \"[mask-image:linear-gradient(to_right,#000_calc(100%-2rem),transparent)] md:flex-wrap md:overflow-visible md:pb-0 md:[mask-image:none]\",\n item: \"snap-start\",\n badge: \"rounded-sm bg-surface-3 px-1.5 py-0.5 text-[0.625rem] font-semibold leading-none text-fg-muted\",\n },\n});\n\nconst s = chipRow();\n\nexport type ChipRowProps = Omit<React.ComponentProps<\"nav\">, \"aria-label\"> & {\n /** What the links choose among (\"For whom?\"): the row's accessible name, passed translated. */\n label: string;\n /** A line under the label that says how to choose; read with the row. */\n hint?: React.ReactNode;\n};\n\n/** A scroll-snap row of chip links; wraps from `md`. Items: `ChipRow.Item`. */\nfunction ChipRowRoot({ label, hint, className, children, ...props }: ChipRowProps) {\n const id = React.useId();\n const list = React.useRef<HTMLUListElement>(null);\n const move = (event: React.KeyboardEvent<HTMLUListElement>) => {\n const keys = [\"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"];\n if (!keys.includes(event.key)) return;\n const links = [...(list.current?.querySelectorAll<HTMLElement>(\"a[href],button:not([disabled])\") ?? [])];\n const at = links.indexOf(event.target as HTMLElement);\n if (at < 0) return;\n const last = links.length - 1;\n const to = event.key === \"Home\" ? 0 : event.key === \"End\" ? last : Math.min(last, Math.max(0, at + (event.key === \"ArrowRight\" ? 1 : -1)));\n event.preventDefault();\n links[to]?.focus();\n links[to]?.scrollIntoView?.({ block: \"nearest\", inline: \"nearest\" });\n };\n return (\n <nav aria-label={label} aria-describedby={hint ? `${id}-hint` : undefined} className={s.root({ className })} {...props}>\n {hint ? (\n <p id={`${id}-hint`} className={s.hint()}>\n {hint}\n </p>\n ) : null}\n <ul ref={list} className={s.list()} onKeyDown={move}>\n {children}\n </ul>\n </nav>\n );\n}\n\nexport type ChipRowItemProps = React.ComponentProps<\"a\"> & {\n /** The current choice: the accent and `aria-current`. */\n selected?: boolean;\n /** What `aria-current` says when selected.\n * @default \"page\" */\n current?: \"page\" | \"true\";\n /** A small tag after the label (\"Soon\"), passed translated. */\n badge?: React.ReactNode;\n /** Renders your router's `<Link>` as the chip; put the label and the badge in your link's children yourself. */\n asChild?: boolean;\n};\n\n/** One link of the row. */\nfunction ChipRowItem({ selected = false, current = \"page\", badge, asChild, className, children, ...props }: ChipRowItemProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return (\n <li className={s.item()}>\n <Comp\n data-state={selected ? \"on\" : \"off\"}\n aria-current={selected ? current : undefined}\n className={chipVariants({ selected, className: [\"min-h-11\", className] })}\n {...props}\n >\n {asChild ? (\n children\n ) : (\n <>\n {children}\n {badge ? <span className={s.badge()}>{badge}</span> : null}\n </>\n )}\n </Comp>\n </li>\n );\n}\n\nexport const ChipRow = Object.assign(ChipRowRoot, { Item: ChipRowItem });\n"
|
|
126
126
|
},
|
|
127
127
|
{
|
|
128
128
|
"path": "chip-row/index.ts",
|
package/registry/chip.json
CHANGED
|
@@ -412,7 +412,7 @@
|
|
|
412
412
|
},
|
|
413
413
|
{
|
|
414
414
|
"path": "chip/recipe.ts",
|
|
415
|
-
"content": "// The chip's parts and sizes, written once as classes of the Krizaka tokens: the web wears them with `tv(chipRecipe)`,\n// React Native reads the same classes into styles (native/chip.tsx). `data-[state=on]:` is the chosen chip.\nimport { defineRecipe } from \"../recipes/define\";\n\nexport const chipRecipe = defineRecipe({\n slots: {\n group: \"flex flex-wrap items-center gap-2 data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch\",\n base:\n \"inline-flex shrink-0 select-none items-center justify-center gap-1.5 whitespace-nowrap rounded-
|
|
415
|
+
"content": "// The chip's parts and sizes, written once as classes of the Krizaka tokens: the web wears them with `tv(chipRecipe)`,\n// React Native reads the same classes into styles (native/chip.tsx). `data-[state=on]:` is the chosen chip.\nimport { defineRecipe } from \"../recipes/define\";\n\nexport const chipRecipe = defineRecipe({\n slots: {\n group: \"flex flex-wrap items-center gap-2 data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch\",\n base:\n \"inline-flex shrink-0 select-none items-center justify-center gap-1.5 whitespace-nowrap rounded-sm border font-semibold transition-colors \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40 \" +\n \"border-border-default bg-surface-1 text-fg-secondary hover:border-border-strong hover:text-fg \" +\n \"data-[state=on]:border-accent data-[state=on]:bg-accent-soft data-[state=on]:text-fg\",\n remove:\n \"-mr-1.5 inline-flex h-6 w-6 items-center justify-center rounded-full text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n },\n variants: {\n size: { sm: { base: \"h-7 px-3 text-xs\" }, md: { base: \"h-9 px-3.5 text-xs\" } },\n },\n defaultVariants: { size: \"md\" },\n});\n"
|
|
416
416
|
},
|
|
417
417
|
{
|
|
418
418
|
"path": "meta.ts",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/registry/index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krizaka/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.38.0",
|
|
4
4
|
"templatePurposes": {
|
|
5
5
|
"convince": {
|
|
6
6
|
"title": "Convince",
|
|
@@ -3110,7 +3110,7 @@
|
|
|
3110
3110
|
"native": 2
|
|
3111
3111
|
},
|
|
3112
3112
|
"dependencies": [
|
|
3113
|
-
"@krizaka/tokens@^3.
|
|
3113
|
+
"@krizaka/tokens@^3.38.0",
|
|
3114
3114
|
"@protomaps/basemaps@5.7.2",
|
|
3115
3115
|
"tailwind-variants@^3.3.1"
|
|
3116
3116
|
],
|
|
@@ -3549,7 +3549,7 @@
|
|
|
3549
3549
|
"native": 0
|
|
3550
3550
|
},
|
|
3551
3551
|
"dependencies": [
|
|
3552
|
-
"@krizaka/tokens@^3.
|
|
3552
|
+
"@krizaka/tokens@^3.38.0",
|
|
3553
3553
|
"radix-ui@^1.7.0",
|
|
3554
3554
|
"tailwind-variants@^3.3.1"
|
|
3555
3555
|
],
|
|
@@ -4212,7 +4212,7 @@
|
|
|
4212
4212
|
"native": 0
|
|
4213
4213
|
},
|
|
4214
4214
|
"dependencies": [
|
|
4215
|
-
"@krizaka/tokens@^3.
|
|
4215
|
+
"@krizaka/tokens@^3.38.0",
|
|
4216
4216
|
"radix-ui@^1.7.0",
|
|
4217
4217
|
"tailwind-variants@^3.3.1"
|
|
4218
4218
|
],
|
|
@@ -4555,7 +4555,7 @@
|
|
|
4555
4555
|
"native": 0
|
|
4556
4556
|
},
|
|
4557
4557
|
"dependencies": [
|
|
4558
|
-
"@krizaka/tokens@^3.
|
|
4558
|
+
"@krizaka/tokens@^3.38.0",
|
|
4559
4559
|
"radix-ui@^1.7.0",
|
|
4560
4560
|
"tailwind-variants@^3.3.1"
|
|
4561
4561
|
],
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
},
|
|
157
157
|
{
|
|
158
158
|
"path": "landing-page/landing-page.tsx",
|
|
159
|
-
"content": "// Server-safe. A landing page (blocks.css `.kz-tpl-*`): a split hero — the promise and its actions beside the product\n// alive (a recording, a demo) — a band of proofs, the features as a bento where the main one takes the room, then the\n// closing call. The blocks come through their package entries, so their client parts keep their boundary.\nimport { CtaPanel } from \"@krizaka/ui/cta-panel\";\nimport { Page, Section } from \"@krizaka/ui/page\";\nimport { SectionBackdrop } from \"@krizaka/ui/section-backdrop\";\nimport type * as React from \"react\";\n\nexport interface LandingFeature {\n /** A stable key. */\n id: string;\n /** Its name. */\n title: React.ReactNode;\n /** One or two sentences. */\n text: React.ReactNode;\n /** A signature icon above the title. */\n icon?: React.ReactNode;\n /** A visual at the bottom of the tile (a chart, a screenshot, a code line). */\n visual?: React.ReactNode;\n /**\n * Its weight in the bento: `lg` (four columns, two rows — the main one), `md` (three), `sm` (two).\n * @default \"sm\"\n */\n size?: \"lg\" | \"md\" | \"sm\";\n}\n\nexport type LandingPageProps = Omit<React.HTMLAttributes<HTMLElement>, \"title\"> & {\n /** A mono line above the title. */\n eyebrow?: React.ReactNode;\n /** The promise: the page's `h1`. */\n title: React.ReactNode;\n /** One or two sentences under it. */\n lead?: React.ReactNode;\n /** The hero's actions (the main one cut, with shimmer). */\n actions?: React.ReactNode;\n /** The product, alive, beside the words: a recording, a live demo (framed as a window). */\n visual: React.ReactNode;\n /** The window's caption (`orochia.app — studio`). */\n visualLabel?: React.ReactNode;\n /** Figures or facts in a band under the hero: `{ value, label }`. */\n proofs?: readonly { value: React.ReactNode; label: React.ReactNode }[];\n /** The features' section: its eyebrow, title and lead. */\n featuresHeading?: { eyebrow?: React.ReactNode; title: React.ReactNode; lead?: React.ReactNode };\n /** The features, as a bento. */\n features: readonly LandingFeature[];\n /** Between the features and the closing call: a story, a quote, testimonials. */\n children?: React.ReactNode;\n /** The closing call: its title, body and actions. */\n closing?: { title: React.ReactNode; body?: React.ReactNode; actions: React.ReactNode };\n};\n\n/** A landing page: split hero with the product alive, proofs, a bento of features, the closing call. */\nexport function LandingPage({ eyebrow, title, lead, actions, visual, visualLabel, proofs, featuresHeading, features, closing, children, ...props }: LandingPageProps) {\n return (\n <div {...props}>\n <SectionBackdrop as=\"header\" grid className=\"kz-hero\">\n <Page className=\"kz-tpl-hero-frame\">\n <div className=\"kz-tpl-split\">\n <div className=\"kz-hero-text\">\n {eyebrow && <p className=\"kz-eyebrow\">{eyebrow}</p>}\n <h1 className=\"kz-hero-title\">{title}</h1>\n {lead && <p className=\"kz-hero-lead\">{lead}</p>}\n {actions && <div className=\"kz-tpl-actions kz-hero-extra\">{actions}</div>}\n </div>\n <figure className=\"kz-tpl-stage\">\n <div className=\"kz-tpl-stage-bar\" aria-hidden>\n
|
|
159
|
+
"content": "// Server-safe. A landing page (blocks.css `.kz-tpl-*`): a split hero — the promise and its actions beside the product\n// alive (a recording, a demo) — a band of proofs, the features as a bento where the main one takes the room, then the\n// closing call. The blocks come through their package entries, so their client parts keep their boundary.\nimport { CtaPanel } from \"@krizaka/ui/cta-panel\";\nimport { Page, Section } from \"@krizaka/ui/page\";\nimport { SectionBackdrop } from \"@krizaka/ui/section-backdrop\";\nimport type * as React from \"react\";\n\nexport interface LandingFeature {\n /** A stable key. */\n id: string;\n /** Its name. */\n title: React.ReactNode;\n /** One or two sentences. */\n text: React.ReactNode;\n /** A signature icon above the title. */\n icon?: React.ReactNode;\n /** A visual at the bottom of the tile (a chart, a screenshot, a code line). */\n visual?: React.ReactNode;\n /**\n * Its weight in the bento: `lg` (four columns, two rows — the main one), `md` (three), `sm` (two).\n * @default \"sm\"\n */\n size?: \"lg\" | \"md\" | \"sm\";\n}\n\nexport type LandingPageProps = Omit<React.HTMLAttributes<HTMLElement>, \"title\"> & {\n /** A mono line above the title. */\n eyebrow?: React.ReactNode;\n /** The promise: the page's `h1`. */\n title: React.ReactNode;\n /** One or two sentences under it. */\n lead?: React.ReactNode;\n /** The hero's actions (the main one cut, with shimmer). */\n actions?: React.ReactNode;\n /** The product, alive, beside the words: a recording, a live demo (framed as a window). */\n visual: React.ReactNode;\n /** The window's caption (`orochia.app — studio`). */\n visualLabel?: React.ReactNode;\n /** Figures or facts in a band under the hero: `{ value, label }`. */\n proofs?: readonly { value: React.ReactNode; label: React.ReactNode }[];\n /** The features' section: its eyebrow, title and lead. */\n featuresHeading?: { eyebrow?: React.ReactNode; title: React.ReactNode; lead?: React.ReactNode };\n /** The features, as a bento. */\n features: readonly LandingFeature[];\n /** Between the features and the closing call: a story, a quote, testimonials. */\n children?: React.ReactNode;\n /** The closing call: its title, body and actions. */\n closing?: { title: React.ReactNode; body?: React.ReactNode; actions: React.ReactNode };\n};\n\n/** A landing page: split hero with the product alive, proofs, a bento of features, the closing call. */\nexport function LandingPage({ eyebrow, title, lead, actions, visual, visualLabel, proofs, featuresHeading, features, closing, children, ...props }: LandingPageProps) {\n return (\n <div {...props}>\n <SectionBackdrop as=\"header\" grid className=\"kz-hero\">\n <Page className=\"kz-tpl-hero-frame\">\n <div className=\"kz-tpl-split\">\n <div className=\"kz-hero-text\">\n {eyebrow && <p className=\"kz-eyebrow\">{eyebrow}</p>}\n <h1 className=\"kz-hero-title\">{title}</h1>\n {lead && <p className=\"kz-hero-lead\">{lead}</p>}\n {actions && <div className=\"kz-tpl-actions kz-hero-extra\">{actions}</div>}\n </div>\n <figure className=\"kz-tpl-stage\">\n <div className=\"kz-tpl-stage-bar\" aria-hidden>\n {visualLabel && <span>{visualLabel}</span>}\n </div>\n {visual}\n </figure>\n </div>\n </Page>\n </SectionBackdrop>\n {proofs && proofs.length > 0 && (\n <Page>\n <dl className=\"kz-tpl-band\">\n {proofs.map((p, i) => (\n <div key={i}>\n <dt>{p.value}</dt>\n <dd>{p.label}</dd>\n </div>\n ))}\n </dl>\n </Page>\n )}\n <Section id=\"features\" eyebrow={featuresHeading?.eyebrow} title={featuresHeading?.title} lead={featuresHeading?.lead} reveal aria-label={featuresHeading ? undefined : \"Features\"}>\n <ul className=\"kz-tpl-bento\">\n {features.map((f) => (\n <li key={f.id} data-size={f.size === \"sm\" ? undefined : f.size} data-reveal=\"\">\n {f.icon && <span className=\"kz-block-icon\" aria-hidden>{f.icon}</span>}\n <h3>{f.title}</h3>\n <p>{f.text}</p>\n {f.visual && <div className=\"kz-tpl-bento-visual\">{f.visual}</div>}\n </li>\n ))}\n </ul>\n </Section>\n {children}\n {closing && (\n <Section spacing=\"sm\" aria-labelledby=\"landing-closing\">\n <CtaPanel id=\"landing-closing\" title={closing.title} body={closing.body} reveal>\n {closing.actions}\n </CtaPanel>\n </Section>\n )}\n </div>\n );\n}\n"
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
162
|
"path": "landing-page/meta.ts",
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
},
|
|
168
168
|
{
|
|
169
169
|
"path": "language-switch/language-switch.tsx",
|
|
170
|
-
"content": "// Client (a disclosure). The one language switch of the family: a globe and the current code (FR), opening a short list of\n// the languages by their own names — links with `hreflang` that keep the page. A disclosure, not a Radix menu: the items\n// are navigation, and the bar must not pay for a menu engine (Escape, outside press, focus-out and the arrows are here).\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport type { Language } from \"./href\";\n\nexport const languageSwitch = tv({\n slots: {\n root: \"relative inline-flex\",\n trigger:\n \"group/language inline-flex h-9 shrink-0 select-none items-center gap-1.5 rounded-
|
|
170
|
+
"content": "// Client (a disclosure). The one language switch of the family: a globe and the current code (FR), opening a short list of\n// the languages by their own names — links with `hreflang` that keep the page. A disclosure, not a Radix menu: the items\n// are navigation, and the bar must not pay for a menu engine (Escape, outside press, focus-out and the arrows are here).\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport type { Language } from \"./href\";\n\nexport const languageSwitch = tv({\n slots: {\n root: \"relative inline-flex\",\n trigger:\n \"group/language inline-flex h-9 shrink-0 select-none items-center gap-1.5 rounded-sm px-2.5 font-mono text-xs font-semibold text-fg-secondary transition-colors \" +\n \"hover:bg-surface-3 hover:text-fg aria-expanded:bg-surface-3 aria-expanded:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n glyph: \"h-4 w-4 shrink-0\",\n panel:\n \"kz-pop absolute right-0 top-full z-50 mt-2 grid min-w-44 max-w-[calc(100vw-2rem)] gap-0.5 rounded-xl border border-border-default bg-surface-1 p-1.5 shadow-lg\",\n heading: \"px-2.5 pb-1 pt-1.5 text-[11px] font-semibold text-fg-secondary\",\n item:\n \"flex min-h-9 items-center justify-between gap-6 rounded-lg px-2.5 py-2 text-sm font-medium text-fg no-underline outline-hidden transition-colors \" +\n \"hover:bg-surface-2 focus-visible:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring aria-[current=true]:bg-accent-soft\",\n check: \"h-4 w-4 shrink-0 text-fg-accent\",\n inline: \"inline-flex items-center gap-1 rounded-sm border border-border-subtle bg-surface-2/60 p-0.5\",\n inlineLink:\n \"inline-flex min-h-9 items-center rounded-sm px-3 text-sm font-medium text-fg-secondary no-underline transition-colors hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=true]:bg-surface-3 aria-[current=true]:text-fg\",\n },\n});\nconst s = languageSwitch();\n\nexport type LanguageSwitchProps = {\n /** The control's accessible name (“Language”, “Langue”) — passed translated. */\n label: string;\n /** The code of the page's language (`fr`). */\n current: string;\n /** The languages offered, each with its name in its own language and the address of the same page (`languagesFor`). */\n languages: readonly Language[];\n /**\n * `menu`: a globe and the code, opening a menu (the bar). `inline`: every language in a row (a mobile panel, a settings page).\n * @default \"menu\"\n */\n variant?: \"menu\" | \"inline\";\n /** Your router's `<Link>` (it receives `href`, `hrefLang`, `lang`). A plain `<a>` otherwise. */\n linkAs?: React.ElementType;\n /** Opens the menu at first render (a story, a screenshot). */\n defaultOpen?: boolean;\n /** Classes of the control. */\n className?: string;\n};\n\nconst Globe = ({ className }: { className?: string }) => (\n <svg viewBox=\"0 0 24 24\" className={className} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M3 12h18M12 3c2.5 2.6 3.8 5.6 3.8 9S14.5 18.4 12 21c-2.5-2.6-3.8-5.6-3.8-9S9.5 5.6 12 3Z\" />\n </svg>\n);\n\n/**\n * The language switch, the same on every Krizaka site and app: a compact control — a globe and the current code\n * (`FR`) — that opens a menu of the languages named in their own language (Français · English), each a link with\n * `hreflang` to the same page. It shows the language you are in, never the “other” one. `variant=\"inline\"` lays the\n * languages in a row for a phone's panel or a settings page.\n */\nexport function LanguageSwitch({ label, current, languages, variant = \"menu\", linkAs, defaultOpen = false, className }: LanguageSwitchProps) {\n const Link = linkAs ?? \"a\";\n const id = React.useId();\n const [open, setOpen] = React.useState(defaultOpen);\n const root = React.useRef<HTMLDivElement>(null);\n const trigger = React.useRef<HTMLButtonElement>(null);\n const active = languages.find((l) => l.code === current);\n\n React.useEffect(() => {\n if (!open) return;\n // A press outside closes it (the page under it stays usable).\n const onPress = (e: PointerEvent) => {\n if (e.target instanceof Node && !root.current?.contains(e.target)) setOpen(false);\n };\n document.addEventListener(\"pointerdown\", onPress);\n return () => document.removeEventListener(\"pointerdown\", onPress);\n }, [open]);\n\n if (variant === \"inline\") {\n return (\n <div role=\"group\" aria-label={label} className={s.inline({ className })}>\n {languages.map((l) => (\n <Link key={l.code} href={l.href} hrefLang={l.code} lang={l.code} aria-current={l.code === current ? \"true\" : undefined} className={s.inlineLink()}>\n {l.name}\n </Link>\n ))}\n </div>\n );\n }\n\n const links = () => [...(root.current?.querySelectorAll<HTMLElement>(\"[data-language]\") ?? [])];\n const close = (focus: boolean) => {\n setOpen(false);\n if (focus) trigger.current?.focus();\n };\n return (\n <div\n ref={root}\n className={s.root({ className })}\n onKeyDown={(e) => {\n if (e.key === \"Escape\" && open) {\n e.stopPropagation();\n close(true);\n }\n if (open && (e.key === \"ArrowDown\" || e.key === \"ArrowUp\" || e.key === \"Home\" || e.key === \"End\")) {\n e.preventDefault();\n const all = links();\n const at = all.indexOf(document.activeElement as HTMLElement);\n const next = e.key === \"Home\" ? 0 : e.key === \"End\" ? all.length - 1 : (at + (e.key === \"ArrowDown\" ? 1 : -1) + all.length) % all.length;\n all[next]?.focus();\n }\n }}\n // Tab out of the switch closes it.\n onBlur={(e) => {\n if (open && !root.current?.contains(e.relatedTarget as Node | null)) setOpen(false);\n }}\n >\n <button\n ref={trigger}\n type=\"button\"\n aria-label={active ? `${label}: ${active.name}` : label}\n aria-expanded={open}\n aria-controls={id}\n onClick={() => setOpen(!open)}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n requestAnimationFrame(() => (links().find((l) => l.getAttribute(\"aria-current\") === \"true\") ?? links()[0])?.focus());\n }\n }}\n className={s.trigger()}\n >\n <Globe className={s.glyph()} />\n {/* Drawn by CSS: the code is no text of the button, so its name (`Language: English`) never mismatches what is visible. */}\n <span aria-hidden data-code={current} className=\"after:content-[attr(data-code)]\" />\n </button>\n {open && (\n <div id={id} role=\"group\" aria-label={label} className={s.panel()}>\n <p aria-hidden className={s.heading()}>\n {label}\n </p>\n {languages.map((l) => (\n <Link key={l.code} data-language=\"\" href={l.href} hrefLang={l.code} lang={l.code} aria-current={l.code === current ? \"true\" : undefined} className={s.item()}>\n {l.name}\n {l.code === current && (\n <svg viewBox=\"0 0 24 24\" className={s.check()} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M20 6 9 17l-5-5\" />\n </svg>\n )}\n </Link>\n ))}\n </div>\n )}\n </div>\n );\n}\n"
|
|
171
171
|
},
|
|
172
172
|
{
|
|
173
173
|
"path": "language-switch/meta.ts",
|
package/registry/map.json
CHANGED
package/registry/meter.json
CHANGED
|
@@ -342,7 +342,7 @@
|
|
|
342
342
|
},
|
|
343
343
|
{
|
|
344
344
|
"path": "meter/meter.tsx",
|
|
345
|
-
"content": "// Server-safe. A measure within a known range — a quota, a balance, a share — as opposed to the progress of a task.\n// `role=\"meter\"`, its value in words; thresholds turn it to warning then danger (or the reverse when high is good);\n// split into parts (90 % to the creator, 10 % to the platform) it becomes a stacked bar or ring with its legend: the\n// text version of what it draws.\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nexport const meter = tv({\n slots: {\n root: \"flex w-full flex-col gap-1.5\",\n header: \"flex items-baseline gap-2 text-xs\",\n caption: \"min-w-0 flex-1 truncate font-medium text-fg\",\n figure: \"shrink-0 tabular-nums text-fg-secondary\",\n track: \"relative flex h-2 w-full gap-0.5 overflow-hidden rounded-
|
|
345
|
+
"content": "// Server-safe. A measure within a known range — a quota, a balance, a share — as opposed to the progress of a task.\n// `role=\"meter\"`, its value in words; thresholds turn it to warning then danger (or the reverse when high is good);\n// split into parts (90 % to the creator, 10 % to the platform) it becomes a stacked bar or ring with its legend: the\n// text version of what it draws.\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nexport const meter = tv({\n slots: {\n root: \"flex w-full flex-col gap-1.5\",\n header: \"flex items-baseline gap-2 text-xs\",\n caption: \"min-w-0 flex-1 truncate font-medium text-fg\",\n figure: \"shrink-0 tabular-nums text-fg-secondary\",\n track: \"relative flex h-2 w-full gap-0.5 overflow-hidden rounded-sm bg-surface-3\",\n fill: \"h-full rounded-sm transition-[width] duration-700 motion-reduce:transition-none\",\n legend: \"mt-1 flex flex-wrap gap-x-4 gap-y-1 text-xs text-fg-secondary\",\n swatch: \"inline-block h-2 w-2 shrink-0 rounded-full\",\n ring: \"relative inline-flex shrink-0 items-center justify-center\",\n center: \"absolute inset-0 flex flex-col items-center justify-center text-center\",\n },\n variants: {\n size: { sm: { track: \"h-1.5\", ring: \"h-14 w-14\" }, md: { ring: \"h-24 w-24\" }, lg: { track: \"h-3\", ring: \"h-36 w-36\" } },\n },\n defaultVariants: { size: \"md\" },\n});\n\n/** The colour of a fill: by tone (thresholds) or by part (the accent, a neutral, accent-2, a lighter neutral). */\nconst BG = { ok: \"bg-accent\", success: \"bg-success\", warning: \"bg-warning\", danger: \"bg-danger\" } as const;\nconst STROKE = { ok: \"stroke-accent\", success: \"stroke-success\", warning: \"stroke-warning\", danger: \"stroke-danger\" } as const;\nconst PART_BG = [\"bg-accent\", \"bg-fg-secondary\", \"bg-accent-2\", \"bg-border-strong\"];\nconst PART_STROKE = [\"stroke-accent\", \"stroke-fg-secondary\", \"stroke-accent-2\", \"stroke-border-strong\"];\n\nexport interface MeterPart {\n /** Its name (“To the creator”) — passed translated. */\n label: string;\n /** Its value, in the meter's unit. */\n value: number;\n /** Its value in words (“€9.00”), in the legend and for assistive technology; default: its percentage. */\n valueText?: string;\n}\n\nexport type MeterProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n /** The accessible name (“Storage”, “Split of a tip”) — passed translated; shown with `showLabel`. */\n label: string;\n /** The value, between `min` and `max` (ignored with `parts`). */\n value?: number;\n /**\n * The bottom of the range.\n * @default 0\n */\n min?: number;\n /**\n * The top of the range.\n * @default 100\n */\n max?: number;\n /** The value in words (“42 GB of 50 GB”) — passed translated; default: the percentage. */\n valueText?: string;\n /**\n * Ratios (0–1) where the tone changes: past `high` it is in danger, past `low` in warning — or the reverse with\n * `optimum=\"high\"` (a balance: the lower, the worse). Without thresholds the fill is the brand accent.\n */\n thresholds?: { readonly low: number; readonly high: number };\n /**\n * Which end is good.\n * @default \"low\"\n */\n optimum?: \"low\" | \"high\";\n /** Split into parts that add up to `max`: a stacked bar or ring, with its legend. */\n parts?: readonly MeterPart[];\n /**\n * bar · ring.\n * @default \"bar\"\n */\n variant?: \"bar\" | \"ring\";\n /**\n * sm · md · lg: the bar's thickness, the ring's diameter.\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Shows the label and the value above the bar. */\n showLabel?: boolean;\n /** A ring's centre: the value, a word. */\n children?: React.ReactNode;\n};\n\n/** A measure within a range — a quota, a balance, a share — toned by thresholds or split into parts. */\nexport function Meter({\n label,\n value = 0,\n min = 0,\n max = 100,\n valueText,\n thresholds,\n optimum = \"low\",\n parts,\n variant = \"bar\",\n size = \"md\",\n showLabel,\n className,\n children,\n ...props\n}: MeterProps) {\n const s = meter({ size });\n const span = max - min || 1;\n const ratio = Math.min(1, Math.max(0, (value - min) / span));\n const level = !thresholds ? \"ok\" : ratio >= thresholds.high ? (optimum === \"low\" ? \"danger\" : \"success\") : ratio >= thresholds.low ? \"warning\" : optimum === \"low\" ? \"success\" : \"danger\";\n const words = valueText ?? `${Math.round(ratio * 100)} %`;\n const shares = (parts ?? []).map((p, i) => ({ ...p, i, ratio: Math.min(1, Math.max(0, p.value / span)), words: p.valueText ?? `${Math.round((p.value / span) * 100)} %` }));\n const a11y = parts\n ? { role: \"group\" as const, \"aria-label\": label }\n : { role: \"meter\" as const, \"aria-label\": label, \"aria-valuemin\": min, \"aria-valuemax\": max, \"aria-valuenow\": Math.min(max, Math.max(min, value)), \"aria-valuetext\": words };\n const legend = parts && (\n <ul className={s.legend()}>\n {shares.map((p) => (\n <li key={p.i} className=\"flex items-center gap-1.5\">\n <span aria-hidden className={[s.swatch(), PART_BG[p.i % PART_BG.length]].join(\" \")} />\n <span className=\"text-fg\">{p.label}</span>\n <span className=\"tabular-nums\">{p.words}</span>\n </li>\n ))}\n </ul>\n );\n\n if (variant === \"ring\") {\n const r = 44;\n let offset = 0;\n const arcs = parts\n ? shares.map((p) => {\n const arc = { cls: PART_STROKE[p.i % PART_STROKE.length]!, len: p.ratio * 100, start: offset };\n offset += arc.len;\n return arc;\n })\n : [{ cls: STROKE[level], len: ratio * 100, start: 0 }];\n return (\n <div {...a11y} data-level={parts ? undefined : level} className={[\"inline-flex flex-col items-center gap-2\", className].filter(Boolean).join(\" \")} {...props}>\n <div className={s.ring()}>\n <svg viewBox=\"0 0 100 100\" className=\"h-full w-full -rotate-90\" aria-hidden>\n <circle cx=\"50\" cy=\"50\" r={r} fill=\"none\" strokeWidth=\"9\" className=\"stroke-border-default\" />\n {arcs.map((a, i) => (\n <circle\n key={i}\n cx=\"50\"\n cy=\"50\"\n r={r}\n fill=\"none\"\n strokeWidth=\"9\"\n pathLength={100}\n strokeDasharray={`${Math.max(0, a.len - (parts ? 1 : 0))} ${100 - Math.max(0, a.len - (parts ? 1 : 0))}`}\n strokeDashoffset={-a.start}\n className={[a.cls, \"transition-[stroke-dasharray] duration-700 motion-reduce:transition-none\"].join(\" \")}\n />\n ))}\n </svg>\n {children !== undefined && <div className={s.center()}>{children}</div>}\n </div>\n {legend}\n </div>\n );\n }\n\n return (\n <div {...a11y} data-level={parts ? undefined : level} className={s.root({ className })} {...props}>\n {showLabel && (\n <div aria-hidden className={s.header()}>\n <span className={s.caption()}>{label}</span>\n {!parts && <span className={s.figure()}>{words}</span>}\n </div>\n )}\n <div aria-hidden className={s.track()}>\n {parts ? (\n shares.map((p) => <span key={p.i} className={[s.fill(), PART_BG[p.i % PART_BG.length]].join(\" \")} style={{ width: `${p.ratio * 100}%` }} />)\n ) : (\n <span className={[s.fill(), BG[level]].join(\" \")} style={{ width: `${ratio * 100}%` }} />\n )}\n </div>\n {legend}\n </div>\n );\n}\n\nexport interface MeterValueProps {\n /** The figure. */\n value: React.ReactNode;\n /** A word under it. */\n caption?: React.ReactNode;\n}\n\n/** For the ring's centre when it shows a figure and a word. */\nexport function MeterValue({ value, caption }: MeterValueProps) {\n return (\n <>\n <span className=\"font-display text-lg font-semibold text-fg tabular-nums\">{value}</span>\n {caption && <span className=\"text-[10px] text-fg-secondary\">{caption}</span>}\n </>\n );\n}\n"
|
|
346
346
|
}
|
|
347
347
|
],
|
|
348
348
|
"dependencies": [
|