@rtkelly-labs/design-system 0.0.0-bootstrap.0 → 0.16.0-dev.0.37dedac
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/CHANGELOG.md +347 -0
- package/LICENSE +21 -0
- package/README.md +317 -2
- package/dist/components/AlertDialog.js +95 -0
- package/dist/components/AlertDialog.js.map +1 -0
- package/dist/components/AppShell.js +261 -0
- package/dist/components/AppShell.js.map +1 -0
- package/dist/components/AsciiDivider.js +8 -0
- package/dist/components/AsciiDivider.js.map +1 -0
- package/dist/components/AsciiGauge.js +130 -0
- package/dist/components/AsciiGauge.js.map +1 -0
- package/dist/components/Avatar.js +87 -0
- package/dist/components/Avatar.js.map +1 -0
- package/dist/components/Badge.js +25 -0
- package/dist/components/Badge.js.map +1 -0
- package/dist/components/BarChart.js +288 -0
- package/dist/components/BarChart.js.map +1 -0
- package/dist/components/BracketText.js +23 -0
- package/dist/components/BracketText.js.map +1 -0
- package/dist/components/BulletChart.js +50 -0
- package/dist/components/BulletChart.js.map +1 -0
- package/dist/components/Button.js +130 -0
- package/dist/components/Button.js.map +1 -0
- package/dist/components/Card.js +181 -0
- package/dist/components/Card.js.map +1 -0
- package/dist/components/ChartTooltip.js +54 -0
- package/dist/components/ChartTooltip.js.map +1 -0
- package/dist/components/Checkbox.js +55 -0
- package/dist/components/Checkbox.js.map +1 -0
- package/dist/components/ColophonCard.js +122 -0
- package/dist/components/ColophonCard.js.map +1 -0
- package/dist/components/DataTable.js +299 -0
- package/dist/components/DataTable.js.map +1 -0
- package/dist/components/Divider.js +42 -0
- package/dist/components/Divider.js.map +1 -0
- package/dist/components/Drawer.js +81 -0
- package/dist/components/Drawer.js.map +1 -0
- package/dist/components/EmptyState.js +39 -0
- package/dist/components/EmptyState.js.map +1 -0
- package/dist/components/ErrorSummary.js +122 -0
- package/dist/components/ErrorSummary.js.map +1 -0
- package/dist/components/Fieldset.js +37 -0
- package/dist/components/Fieldset.js.map +1 -0
- package/dist/components/Glyph.js +60 -0
- package/dist/components/Glyph.js.map +1 -0
- package/dist/components/Input.js +111 -0
- package/dist/components/Input.js.map +1 -0
- package/dist/components/LinkProvider.js +76 -0
- package/dist/components/LinkProvider.js.map +1 -0
- package/dist/components/Menu.js +144 -0
- package/dist/components/Menu.js.map +1 -0
- package/dist/components/MetadataGrid.js +184 -0
- package/dist/components/MetadataGrid.js.map +1 -0
- package/dist/components/MobileNav.js +120 -0
- package/dist/components/MobileNav.js.map +1 -0
- package/dist/components/Modal.js +79 -0
- package/dist/components/Modal.js.map +1 -0
- package/dist/components/NerdIcon.js +177 -0
- package/dist/components/NerdIcon.js.map +1 -0
- package/dist/components/NoteBlock.js +84 -0
- package/dist/components/NoteBlock.js.map +1 -0
- package/dist/components/PageHeader.js +34 -0
- package/dist/components/PageHeader.js.map +1 -0
- package/dist/components/PageTitle.js +21 -0
- package/dist/components/PageTitle.js.map +1 -0
- package/dist/components/Pagination.js +96 -0
- package/dist/components/Pagination.js.map +1 -0
- package/dist/components/Popover.js +87 -0
- package/dist/components/Popover.js.map +1 -0
- package/dist/components/Progress.js +83 -0
- package/dist/components/Progress.js.map +1 -0
- package/dist/components/RadioGroup.js +74 -0
- package/dist/components/RadioGroup.js.map +1 -0
- package/dist/components/ReportDocument.js +75 -0
- package/dist/components/ReportDocument.js.map +1 -0
- package/dist/components/SectionContainer.js +18 -0
- package/dist/components/SectionContainer.js.map +1 -0
- package/dist/components/Select.js +199 -0
- package/dist/components/Select.js.map +1 -0
- package/dist/components/SiteFooter.js +29 -0
- package/dist/components/SiteFooter.js.map +1 -0
- package/dist/components/SiteHeader.js +59 -0
- package/dist/components/SiteHeader.js.map +1 -0
- package/dist/components/SiteNav.js +141 -0
- package/dist/components/SiteNav.js.map +1 -0
- package/dist/components/Skeleton.js +51 -0
- package/dist/components/Skeleton.js.map +1 -0
- package/dist/components/SocialIcon.js +77 -0
- package/dist/components/SocialIcon.js.map +1 -0
- package/dist/components/Sparkline.js +104 -0
- package/dist/components/Sparkline.js.map +1 -0
- package/dist/components/Spinner.js +65 -0
- package/dist/components/Spinner.js.map +1 -0
- package/dist/components/StatCard.js +53 -0
- package/dist/components/StatCard.js.map +1 -0
- package/dist/components/StatusPage.js +99 -0
- package/dist/components/StatusPage.js.map +1 -0
- package/dist/components/Swatch.js +55 -0
- package/dist/components/Swatch.js.map +1 -0
- package/dist/components/Switch.js +53 -0
- package/dist/components/Switch.js.map +1 -0
- package/dist/components/TLDR.js +53 -0
- package/dist/components/TLDR.js.map +1 -0
- package/dist/components/Table.js +155 -0
- package/dist/components/Table.js.map +1 -0
- package/dist/components/Tabs.js +325 -0
- package/dist/components/Tabs.js.map +1 -0
- package/dist/components/Tag.js +19 -0
- package/dist/components/Tag.js.map +1 -0
- package/dist/components/TelemetryGauge.js +112 -0
- package/dist/components/TelemetryGauge.js.map +1 -0
- package/dist/components/ThemeProvider.js +87 -0
- package/dist/components/ThemeProvider.js.map +1 -0
- package/dist/components/TickerTape.js +196 -0
- package/dist/components/TickerTape.js.map +1 -0
- package/dist/components/Toast.js +200 -0
- package/dist/components/Toast.js.map +1 -0
- package/dist/components/Tooltip.js +69 -0
- package/dist/components/Tooltip.js.map +1 -0
- package/dist/components/TrackCard.js +206 -0
- package/dist/components/TrackCard.js.map +1 -0
- package/dist/components/blog/BlogPost.js +116 -0
- package/dist/components/blog/BlogPost.js.map +1 -0
- package/dist/components/blog/LoremIpsumPost.js +62 -0
- package/dist/components/blog/LoremIpsumPost.js.map +1 -0
- package/dist/components/blog/author.js +25 -0
- package/dist/components/blog/author.js.map +1 -0
- package/dist/components/booleanControl.js +50 -0
- package/dist/components/booleanControl.js.map +1 -0
- package/dist/components/dialogPopupRef.js +28 -0
- package/dist/components/dialogPopupRef.js.map +1 -0
- package/dist/components/dialogSurface.js +75 -0
- package/dist/components/dialogSurface.js.map +1 -0
- package/dist/components/docs/AnchorHeading.js +80 -0
- package/dist/components/docs/AnchorHeading.js.map +1 -0
- package/dist/components/docs/Breadcrumbs.js +26 -0
- package/dist/components/docs/Breadcrumbs.js.map +1 -0
- package/dist/components/docs/CodeBlock.js +63 -0
- package/dist/components/docs/CodeBlock.js.map +1 -0
- package/dist/components/docs/CodeTabs.js +71 -0
- package/dist/components/docs/CodeTabs.js.map +1 -0
- package/dist/components/docs/DocPager.js +28 -0
- package/dist/components/docs/DocPager.js.map +1 -0
- package/dist/components/docs/DocsHeader.js +124 -0
- package/dist/components/docs/DocsHeader.js.map +1 -0
- package/dist/components/docs/DocsLayout.js +61 -0
- package/dist/components/docs/DocsLayout.js.map +1 -0
- package/dist/components/docs/DocsLinkProvider.js +25 -0
- package/dist/components/docs/DocsLinkProvider.js.map +1 -0
- package/dist/components/docs/DocsSidebar.js +147 -0
- package/dist/components/docs/DocsSidebar.js.map +1 -0
- package/dist/components/docs/Prose.js +56 -0
- package/dist/components/docs/Prose.js.map +1 -0
- package/dist/components/docs/TableOfContents.js +71 -0
- package/dist/components/docs/TableOfContents.js.map +1 -0
- package/dist/components/docs/codeTabsStore.js +75 -0
- package/dist/components/docs/codeTabsStore.js.map +1 -0
- package/dist/components/docs/figures/ActivityGrid.js +116 -0
- package/dist/components/docs/figures/ActivityGrid.js.map +1 -0
- package/dist/components/docs/figures/BeforeAfter.js +64 -0
- package/dist/components/docs/figures/BeforeAfter.js.map +1 -0
- package/dist/components/docs/figures/ChangeSummary.js +74 -0
- package/dist/components/docs/figures/ChangeSummary.js.map +1 -0
- package/dist/components/docs/figures/Decision.js +34 -0
- package/dist/components/docs/figures/Decision.js.map +1 -0
- package/dist/components/docs/figures/DrawingSheet.js +37 -0
- package/dist/components/docs/figures/DrawingSheet.js.map +1 -0
- package/dist/components/docs/figures/FigureFrame.js +226 -0
- package/dist/components/docs/figures/FigureFrame.js.map +1 -0
- package/dist/components/docs/figures/FileChangeTree.js +134 -0
- package/dist/components/docs/figures/FileChangeTree.js.map +1 -0
- package/dist/components/docs/figures/FlowDiagram.js +82 -0
- package/dist/components/docs/figures/FlowDiagram.js.map +1 -0
- package/dist/components/docs/figures/GanttChart.js +79 -0
- package/dist/components/docs/figures/GanttChart.js.map +1 -0
- package/dist/components/docs/figures/IndexedSection.js +45 -0
- package/dist/components/docs/figures/IndexedSection.js.map +1 -0
- package/dist/components/docs/figures/MetricStrip.js +23 -0
- package/dist/components/docs/figures/MetricStrip.js.map +1 -0
- package/dist/components/docs/figures/Steps.js +58 -0
- package/dist/components/docs/figures/Steps.js.map +1 -0
- package/dist/components/docs/figures/SwatchCompare.js +42 -0
- package/dist/components/docs/figures/SwatchCompare.js.map +1 -0
- package/dist/components/docs/figures/TermGrid.js +22 -0
- package/dist/components/docs/figures/TermGrid.js.map +1 -0
- package/dist/components/docs/figures/Terminal.js +37 -0
- package/dist/components/docs/figures/Terminal.js.map +1 -0
- package/dist/components/docs/figures/Timeline.js +61 -0
- package/dist/components/docs/figures/Timeline.js.map +1 -0
- package/dist/components/docs/figures/TokenTable.js +42 -0
- package/dist/components/docs/figures/TokenTable.js.map +1 -0
- package/dist/components/docs/figures/TreeDiagram.js +76 -0
- package/dist/components/docs/figures/TreeDiagram.js.map +1 -0
- package/dist/components/docs/figures/UptimeStrip.js +68 -0
- package/dist/components/docs/figures/UptimeStrip.js.map +1 -0
- package/dist/components/docs/figures/index.js +24 -0
- package/dist/components/docs/figures/index.js.map +1 -0
- package/dist/components/docs/index.js +15 -0
- package/dist/components/docs/index.js.map +1 -0
- package/dist/components/docs/mdxComponents.js +108 -0
- package/dist/components/docs/mdxComponents.js.map +1 -0
- package/dist/components/experiments/DesignSandbox.js +206 -0
- package/dist/components/experiments/DesignSandbox.js.map +1 -0
- package/dist/components/experiments/ExperimentsView.js +138 -0
- package/dist/components/experiments/ExperimentsView.js.map +1 -0
- package/dist/components/fieldFrame.js +200 -0
- package/dist/components/fieldFrame.js.map +1 -0
- package/dist/components/floatingSurface.js +35 -0
- package/dist/components/floatingSurface.js.map +1 -0
- package/dist/components/marketing/CTASection.js +47 -0
- package/dist/components/marketing/CTASection.js.map +1 -0
- package/dist/components/marketing/FeatureGrid.js +49 -0
- package/dist/components/marketing/FeatureGrid.js.map +1 -0
- package/dist/components/marketing/Hero.js +44 -0
- package/dist/components/marketing/Hero.js.map +1 -0
- package/dist/components/marketing/PricingGrid.js +65 -0
- package/dist/components/marketing/PricingGrid.js.map +1 -0
- package/dist/components/marketing/SectionGrid.js +51 -0
- package/dist/components/marketing/SectionGrid.js.map +1 -0
- package/dist/components/paginationRange.js +28 -0
- package/dist/components/paginationRange.js.map +1 -0
- package/dist/components/portalTheme.js +14 -0
- package/dist/components/portalTheme.js.map +1 -0
- package/dist/components/saas/SaasLandingPage.js +201 -0
- package/dist/components/saas/SaasLandingPage.js.map +1 -0
- package/dist/components/siteNavContext.js +24 -0
- package/dist/components/siteNavContext.js.map +1 -0
- package/dist/components/slides/Slide.js +84 -0
- package/dist/components/slides/Slide.js.map +1 -0
- package/dist/components/slides/SlideDeck.js +228 -0
- package/dist/components/slides/SlideDeck.js.map +1 -0
- package/dist/components/themeInitScript.js +16 -0
- package/dist/components/themeInitScript.js.map +1 -0
- package/dist/components/themeScope.js +9 -0
- package/dist/components/themeScope.js.map +1 -0
- package/dist/components/toastLifetime.js +21 -0
- package/dist/components/toastLifetime.js.map +1 -0
- package/dist/fonts/DSSymbols-OFL.txt +98 -0
- package/dist/fonts/DSSymbols-Regular.woff2 +0 -0
- package/dist/fonts/SymbolsNerdFontMono-Regular.woff2 +0 -0
- package/dist/hooks/useActiveHeading.js +59 -0
- package/dist/hooks/useActiveHeading.js.map +1 -0
- package/dist/hooks/useCopyToClipboard.js +58 -0
- package/dist/hooks/useCopyToClipboard.js.map +1 -0
- package/dist/index.d.ts +6394 -0
- package/dist/index.js +117 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/accentClasses.js +87 -0
- package/dist/lib/accentClasses.js.map +1 -0
- package/dist/lib/recipe.js +33 -0
- package/dist/lib/recipe.js.map +1 -0
- package/dist/lib/slug.js +39 -0
- package/dist/lib/slug.js.map +1 -0
- package/dist/lib/theme.js +76 -0
- package/dist/lib/theme.js.map +1 -0
- package/dist/lib/tokenRules.js +51 -0
- package/dist/lib/tokenRules.js.map +1 -0
- package/dist/prose.css +766 -0
- package/dist/styles.css +203 -0
- package/dist/theme/ansi.js +77 -0
- package/dist/theme/ansi.js.map +1 -0
- package/dist/theme/contrast.js +342 -0
- package/dist/theme/contrast.js.map +1 -0
- package/dist/theme/levels.js +234 -0
- package/dist/theme/levels.js.map +1 -0
- package/dist/theme/media.js +115 -0
- package/dist/theme/media.js.map +1 -0
- package/dist/theme/recommended.js +105 -0
- package/dist/theme/recommended.js.map +1 -0
- package/dist/theme.css +538 -0
- package/dist/tokens.js +1 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +209 -6
- package/skills/design-system-usage/SKILL.md +75 -0
- package/terminal/rtk-midnight.alacritty.toml +33 -0
- package/terminal/rtk-midnight.ghostty.conf +24 -0
- package/terminal/rtk-midnight.itermcolors +167 -0
- package/terminal/rtk-midnight.windows-terminal.json +23 -0
- package/terminal/rtk-sketch.alacritty.toml +33 -0
- package/terminal/rtk-sketch.ghostty.conf +24 -0
- package/terminal/rtk-sketch.itermcolors +167 -0
- package/terminal/rtk-sketch.windows-terminal.json +23 -0
- package/tokens/palette.midnight.tokens.json +477 -0
- package/tokens/palette.sketch.tokens.json +477 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/Tabs.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, forwardRef, useCallback, useContext, useId, useMemo, useState } from 'react';\nimport type {\n ButtonHTMLAttributes,\n CSSProperties,\n HTMLAttributes,\n KeyboardEvent,\n ReactNode,\n} from 'react';\nimport { cn, recipe } from '../lib/recipe';\nimport { accentVar } from '../lib/theme';\nimport type { AccentToken } from '../lib/theme';\n\n/**\n * The tab model this package has proven, promoted out of the one component\n * that had it.\n *\n * `CodeTabs` carried a hand-rolled tablist — roving `tabIndex`, arrow keys with\n * wrapping, `Home`/`End`, `aria-controls`/`aria-labelledby` pairing and the\n * three ARIA roles — behind ten tests, and none of it was reachable by a\n * consumer. This is that implementation, extracted rather than rewritten, so\n * the behaviour a consumer gets is the behaviour that was already asserted.\n *\n * It is deliberately **not** a wrapper over `@base-ui/react/tabs`: #163\n * proposed that migration and was closed as rejected, because Base UI does not\n * implement typeahead either and the move would have cost the `keepMounted`\n * behaviour that puts every panel in the HTML. There is one tablist in this\n * tree, and this is it.\n *\n * Typeahead is still the one genuine gap, still out of scope here, and still\n * cheapest as an addition to `moveBetweenTabs` below.\n */\n\n/** Which way the strip runs, and therefore which arrow keys traverse it. */\nexport type TabsOrientation = 'horizontal' | 'vertical';\n\n/**\n * The font style for the tabs strip: 'sans' for general UI/app navigation,\n * or 'mono' for technical/code block contexts.\n */\nexport type TabsFont = 'sans' | 'mono';\n\n/**\n * The three tab-strip shapes, loudest first. Named for the part rather than\n * the colour, because the accent is a separate axis.\n */\nexport type TabsVariant = 'merged' | 'underline' | 'segmented';\n\nconst tabs_ = recipe({\n slots: {\n // The root owns no spacing: a page margin is the caller's decision, and\n // `CodeTabs` supplies its own `my-6`.\n root: '',\n strip: 'relative z-10 flex border-2 border-edge-strong',\n caption: 'truncate text-xs text-content-secondary',\n list: 'flex items-end',\n tab: 'shrink-0 whitespace-nowrap transition-colors focus-visible:ring-2 focus-visible:ring-[var(--tabs-accent)] focus-visible:ring-inset',\n },\n variants: {\n font: {\n sans: {\n tab: 'font-sans text-sm font-semibold tracking-normal normal-case',\n caption: 'font-sans text-xs font-semibold uppercase tracking-wider',\n },\n mono: {\n tab: 'font-mono text-xs font-bold uppercase tracking-widest',\n caption: 'font-mono text-xs font-bold uppercase tracking-widest',\n },\n },\n variant: {\n // The loudest: a solid accent tab standing on the panel, its bottom rule\n // dropped so the fill runs into what it labels.\n merged: {\n strip: 'items-end overflow-x-auto bg-surface-raised px-1.5 pt-1.5',\n list: 'gap-1',\n tab: '-mb-0.5 border-2 px-4 py-2',\n },\n // The quietest: no tab shapes, a 4px accent rule on the seam.\n underline: {\n strip: 'items-end overflow-x-auto bg-surface-base px-3 pt-2',\n list: 'gap-1',\n tab: '-mb-0.5 border-b-4 px-4 py-2.5',\n },\n // A title bar with a caption slot; the tabs are a segmented control.\n segmented: {\n strip: 'items-center justify-between gap-4 bg-surface-raised px-3 py-2.5',\n list: 'gap-0 border-2 border-edge-strong',\n tab: 'border-r-2 border-edge-strong px-4 py-2 last:border-r-0',\n },\n },\n /*\n * `horizontal` adds nothing on purpose.\n *\n * Every class the horizontal strip renders is the class `CodeTabs` rendered\n * before the extraction, which is what lets its committed baselines stay\n * valid. The vertical set turns the strip on its side and moves each\n * variant's edge treatment from the bottom of a tab to its trailing side.\n */\n orientation: {\n horizontal: {},\n vertical: {\n root: 'flex items-start',\n strip: 'flex-col items-stretch self-stretch overflow-x-visible',\n list: 'flex-col items-stretch',\n tab: 'w-full text-left',\n },\n },\n selected: {\n true: {},\n false: {},\n },\n },\n defaultVariants: {\n font: 'sans',\n variant: 'merged',\n orientation: 'horizontal',\n selected: false,\n },\n compoundVariants: [\n {\n variant: 'merged',\n selected: true,\n class: {\n tab: 'relative z-10 border-edge-strong border-b-0 bg-[var(--tabs-accent)] text-content-inverse',\n },\n },\n {\n variant: 'merged',\n selected: false,\n class: {\n tab: 'border-transparent text-content-muted hover:border-edge-strong hover:text-content-primary',\n },\n },\n {\n variant: 'underline',\n selected: true,\n class: { tab: 'border-b-[var(--tabs-accent)] text-content-primary' },\n },\n {\n variant: 'underline',\n selected: false,\n class: { tab: 'border-b-transparent text-content-muted hover:text-content-primary' },\n },\n {\n variant: 'segmented',\n selected: true,\n class: { tab: 'bg-[var(--tabs-accent)] text-content-inverse' },\n },\n {\n variant: 'segmented',\n selected: false,\n class: { tab: 'bg-surface-base text-content-muted hover:text-content-primary' },\n },\n\n // Vertical: the seam a variant draws along the bottom of a tab moves to its\n // trailing edge, so the strip reads as a column rather than a row that has\n // been rotated and left with a dangling rule.\n { variant: 'merged', orientation: 'vertical', class: { tab: 'mb-0 -mr-0.5' } },\n {\n variant: 'merged',\n orientation: 'vertical',\n selected: true,\n class: { tab: 'border-b-2 border-r-0' },\n },\n {\n variant: 'underline',\n orientation: 'vertical',\n class: { tab: 'mb-0 border-b-0 border-r-4' },\n },\n {\n variant: 'underline',\n orientation: 'vertical',\n selected: true,\n class: { tab: 'border-r-[var(--tabs-accent)]' },\n },\n {\n variant: 'underline',\n orientation: 'vertical',\n selected: false,\n class: { tab: 'border-r-transparent' },\n },\n {\n variant: 'segmented',\n orientation: 'vertical',\n class: { tab: 'border-b-2 border-r-0 last:border-b-0' },\n },\n ],\n});\n\ninterface TabsContextValue {\n value: string;\n select: (value: string) => void;\n orientation: TabsOrientation;\n variant: TabsVariant;\n font: TabsFont;\n tabId: (value: string) => string;\n panelId: (value: string) => string;\n}\n\nconst TabsContext = createContext<TabsContextValue | null>(null);\n\nfunction useTabsContext(part: string): TabsContextValue {\n const context = useContext(TabsContext);\n if (!context) {\n throw new Error(`<${part}> must be rendered inside <Tabs>.`);\n }\n return context;\n}\n\n/**\n * A value can be any string a caller finds natural — `\"pnpm\"`, `\"Billing\"` —\n * and it has to become part of an `id` that `aria-controls` can reference.\n * HTML5 allows anything but whitespace in an id, but the mapping must also be\n * injective: replacing whitespace with `-` would give `\"billing info\"` and\n * `\"billing-info\"` the same id, and two tabs one `aria-controls` target.\n * `encodeURIComponent` escapes whitespace and is reversible, so distinct\n * values always get distinct ids.\n */\nfunction idPart(value: string): string {\n return encodeURIComponent(value);\n}\n\nexport interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {\n /**\n * The selected tab's `value`, when the caller owns the selection. Supplying\n * it makes the component controlled: it renders what it is given and never\n * changes on its own, so pair it with `onValueChange`.\n */\n value?: string;\n /**\n * The tab selected on first render, when the component owns the selection.\n * There is no \"first tab by default\" — a tab's value is known to the tab,\n * not to the root, and inferring it would mean the root reaching into its\n * own children.\n */\n defaultValue?: string;\n /**\n * Called with the newly selected tab's `value`, whichever device selected it\n * — a click, an arrow key, `Home` or `End`. Fires in both the controlled and\n * the uncontrolled case.\n */\n onValueChange?: (value: string) => void;\n /**\n * Which way the strip runs. It decides the arrow keys as well as the layout:\n * `horizontal` traverses with `ArrowLeft`/`ArrowRight`, `vertical` with\n * `ArrowUp`/`ArrowDown`. `Home` and `End` work in both.\n */\n orientation?: TabsOrientation;\n /** The strip's shape. See {@link TabsVariant}. */\n variant?: TabsVariant;\n /**\n * The font styling family. Defaults to `'sans'` for UI navigation, or\n * `'mono'` for technical/code block contexts.\n */\n font?: TabsFont;\n /**\n * Semantic accent for the selected tab, as an `Emphasis` or an `Intent`.\n * Selection is marked by a fill or a 4px edge in this colour, never by\n * swapping one surface for another — the surfaces are a few percent of\n * lightness apart on the light rungs of the ladder, which is not enough to\n * tell a reader which tab they are on.\n */\n accent?: AccentToken;\n /** A `TabsList` and the `TabsPanel`s it controls, in any arrangement. */\n children: ReactNode;\n /** Merged onto the root. For placement — a margin, a width, a grid cell. */\n className?: string;\n}\n\n/**\n * A tab set: one strip of tabs, one panel visible at a time.\n *\n * This is a **content switcher, not navigation**. The panels hold alternative\n * views of the same thing — a settings section, a dashboard slice, a snippet in\n * another language — and the browser's history is not involved. Links that\n * change the page are a nav, and they look like tabs in far too many systems.\n *\n * The keyboard model is the whole reason this is a component rather than a\n * pair of `div`s: arrow keys traverse the strip and wrap at its ends, `Home`\n * and `End` jump to the first and last tab, and only the selected tab is in the\n * tab order, so `Tab` moves past the strip rather than through it. Selection\n * follows focus, which is what the ARIA practices recommend for panels that are\n * cheap to render.\n *\n * ```tsx\n * <Tabs defaultValue=\"profile\" accent=\"primary\">\n * <TabsList label=\"Account settings\">\n * <TabsTab value=\"profile\">Profile</TabsTab>\n * <TabsTab value=\"billing\">Billing</TabsTab>\n * </TabsList>\n * <TabsPanel value=\"profile\">…</TabsPanel>\n * <TabsPanel value=\"billing\">…</TabsPanel>\n * </Tabs>\n * ```\n *\n * Selection is uncontrolled with `defaultValue` and controlled with\n * `value` + `onValueChange`; `CodeTabs` is the worked example of the\n * controlled form, driving the strip from a store shared across the page.\n */\nexport const Tabs = forwardRef<HTMLDivElement, TabsProps>(function Tabs(\n {\n value,\n defaultValue,\n onValueChange,\n orientation = 'horizontal',\n variant = 'merged',\n font = 'sans',\n accent = 'primary',\n children,\n className,\n style,\n ...props\n },\n ref,\n) {\n const uid = useId();\n const [uncontrolled, setUncontrolled] = useState(defaultValue ?? '');\n const selected = value ?? uncontrolled;\n\n const select = useCallback(\n (next: string) => {\n // A controlled set never moves on its own: the caller's `value` is the\n // answer, and writing to local state as well would make the two disagree\n // the moment the caller declines a change.\n if (value === undefined) setUncontrolled(next);\n onValueChange?.(next);\n },\n [value, onValueChange],\n );\n\n const context = useMemo<TabsContextValue>(\n () => ({\n value: selected,\n select,\n orientation,\n variant,\n font,\n tabId: (v) => `${uid}-tab-${idPart(v)}`,\n panelId: (v) => `${uid}-panel-${idPart(v)}`,\n }),\n [selected, select, orientation, variant, font, uid],\n );\n\n const styles = tabs_({ variant, orientation, font });\n\n // The accent is a runtime value, so it travels as a custom property that the\n // fill and edge utilities read — a utility cannot be assembled at build time\n // from a prop, because Tailwind's scanner reads source text.\n const accented = { '--tabs-accent': accentVar(accent), ...style } as CSSProperties;\n\n return (\n <TabsContext.Provider value={context}>\n <div\n ref={ref}\n {...props}\n data-slot=\"tabs\"\n data-orientation={orientation}\n className={cn(styles.root(), className)}\n style={accented}\n >\n {children}\n </div>\n </TabsContext.Provider>\n );\n});\n\nexport interface TabsListProps extends HTMLAttributes<HTMLDivElement> {\n /**\n * The tab list's accessible name, read before the selected tab. Name the set\n * — \"Account settings\" — rather than repeating the tabs.\n */\n label?: string;\n /**\n * Shown in the strip beside the tabs, for the `segmented` variant's title\n * bar: a filename, a section name. Omit for none.\n */\n caption?: ReactNode;\n /** The `TabsTab`s. Anything else in here is not in the tab order. */\n children: ReactNode;\n /** Merged onto the strip — the bordered bar, not the `tablist` inside it. */\n className?: string;\n}\n\n/**\n * Arrow-key traversal for one tab set.\n *\n * Bound on each tab rather than on the `tablist`: a tablist is not itself\n * focusable, and a key handler on an element that never takes focus is what\n * `jsx-a11y/interactive-supports-focus` rejects. It is still one function, and\n * it walks the DOM from the tab's own tablist, so the traversal order is the\n * DOM order a reader actually sees, with no registry to fall out of step.\n * Disabled tabs are skipped: a native disabled button cannot take focus, so\n * landing on one would select it and leave no tab with `tabIndex={0}`. Which\n * keys traverse follows the orientation set on `Tabs`; `Home` and `End` always\n * work.\n */\nfunction moveBetweenTabs(\n event: KeyboardEvent<HTMLButtonElement>,\n orientation: TabsOrientation,\n select: (value: string) => void,\n) {\n const current = event.currentTarget;\n const tablist = current.closest('[role=\"tablist\"]');\n if (!tablist) return;\n\n const strip = tablist.querySelectorAll<HTMLButtonElement>('[role=\"tab\"]:not(:disabled)');\n const list = Array.from(strip);\n const index = list.indexOf(current);\n if (index === -1) return;\n\n const [back, forward] =\n orientation === 'vertical' ? ['ArrowUp', 'ArrowDown'] : ['ArrowLeft', 'ArrowRight'];\n const last = list.length - 1;\n const next =\n event.key === forward\n ? index === last\n ? 0\n : index + 1\n : event.key === back\n ? index === 0\n ? last\n : index - 1\n : event.key === 'Home'\n ? 0\n : event.key === 'End'\n ? last\n : null;\n\n if (next === null) return;\n event.preventDefault();\n const target = list[next];\n if (!target) return;\n const value = target.dataset.value;\n if (value !== undefined) select(value);\n target.focus();\n}\n\n/**\n * The strip a tab set's tabs stand in, holding the `tablist` whose DOM order is\n * the keyboard traversal order (see `moveBetweenTabs`).\n *\n * The tablist's name comes from `label`, or from `aria-label` /\n * `aria-labelledby` passed here: those two are forwarded to the element that\n * carries `role=\"tablist\"`, not left on the wrapper, where they would name\n * nothing a screen reader announces.\n */\nexport const TabsList = forwardRef<HTMLDivElement, TabsListProps>(function TabsList(\n {\n label,\n caption,\n children,\n className,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n ...props\n },\n ref,\n) {\n const { orientation, variant, font } = useTabsContext('TabsList');\n const styles = tabs_({ variant, orientation, font });\n\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"tabs-list\"\n className={cn(styles.strip(), className)}\n >\n {caption ? (\n <span data-slot=\"tabs-caption\" className={styles.caption()}>\n {caption}\n </span>\n ) : null}\n <div\n role=\"tablist\"\n aria-label={label ?? ariaLabel}\n aria-labelledby={ariaLabelledBy}\n // Only stated when it is not the default: an `aria-orientation` of\n // `horizontal` on a tablist says nothing a screen reader did not\n // already assume.\n aria-orientation={orientation === 'vertical' ? 'vertical' : undefined}\n data-orientation={orientation}\n className={styles.list()}\n >\n {children}\n </div>\n </div>\n );\n});\n\nexport interface TabsTabProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'value'> {\n /**\n * Identifies the tab, and pairs it with the `TabsPanel` carrying the same\n * value. Unique within one tab set; it also becomes part of the element ids\n * the ARIA pairing is built from.\n */\n value: string;\n /** The tab's label. Short — the strip scrolls rather than wraps. */\n children: ReactNode;\n /** Merged onto the tab. */\n className?: string;\n}\n\n/**\n * One tab in a {@link TabsList}.\n *\n * Only the selected tab is in the tab order — a roving `tabIndex`, so `Tab`\n * enters the strip once and leaves it, and the arrow keys move within it. The\n * selected tab is marked by the accent fill or edge its variant declares, and\n * never by a surface swap: `pnpm check:contrast` asserts both devices clear 3:1\n * on every rung of the ladder and reports the surface pair that does not.\n */\nexport const TabsTab = forwardRef<HTMLButtonElement, TabsTabProps>(function TabsTab(\n { value, children, className, onClick, onKeyDown, ...props },\n ref,\n) {\n const context = useTabsContext('TabsTab');\n const selected = context.value === value;\n const styles = tabs_({\n variant: context.variant,\n orientation: context.orientation,\n font: context.font,\n selected,\n });\n\n return (\n <button\n ref={ref}\n {...props}\n type=\"button\"\n role=\"tab\"\n id={context.tabId(value)}\n data-slot=\"tabs-tab\"\n // Read back by `moveBetweenTabs`, which walks the DOM\n // rather than a registry: the tabs a reader can arrow between are exactly\n // the ones rendered, with no bookkeeping to fall out of step.\n data-value={value}\n aria-selected={selected}\n aria-controls={context.panelId(value)}\n tabIndex={selected ? 0 : -1}\n onClick={(event) => {\n onClick?.(event);\n context.select(value);\n }}\n onKeyDown={(event) => {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n moveBetweenTabs(event, context.orientation, context.select);\n }}\n className={cn(styles.tab(), className)}\n >\n {children}\n </button>\n );\n});\n\nexport interface TabsPanelProps extends HTMLAttributes<HTMLDivElement> {\n /** The {@link TabsTab} value this panel belongs to. */\n value: string;\n /**\n * Keep the panel in the document when another tab is selected, `hidden`\n * rather than unmounted.\n *\n * Off by default, because a panel that is expensive to render should not be\n * rendered while nobody is looking at it. `CodeTabs` turns it on deliberately\n * — every variant of a snippet is then in the HTML for a crawler, and a\n * switch costs no re-render of the code.\n */\n keepMounted?: boolean;\n /** The panel's content. */\n children: ReactNode;\n /** Merged onto the panel. */\n className?: string;\n}\n\n/**\n * The content one tab selects.\n *\n * Paired with its tab by `aria-controls` and `aria-labelledby`, so a screen\n * reader moving into the panel is told which tab it belongs to. It carries no\n * styling of its own: what a panel looks like is the surface it holds, and a\n * primitive that padded it would be wrong for half its uses.\n */\nexport const TabsPanel = forwardRef<HTMLDivElement, TabsPanelProps>(function TabsPanel(\n { value, keepMounted = false, children, className, ...props },\n ref,\n) {\n const context = useTabsContext('TabsPanel');\n const selected = context.value === value;\n\n if (!selected && !keepMounted) return null;\n\n const classes = cn(className);\n\n return (\n <div\n ref={ref}\n {...props}\n role=\"tabpanel\"\n id={context.panelId(value)}\n data-slot=\"tabs-panel\"\n aria-labelledby={context.tabId(value)}\n hidden={!selected}\n className={classes || undefined}\n >\n {children}\n </div>\n );\n});\n"],"mappings":"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","names":["Tabs","TabsList","TabsTab","TabsPanel"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/components/Tag.tsx
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { cn } from "../lib/recipe.js";
|
|
4
|
+
import { accentFillClass } from "../lib/accentClasses.js";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
var Tag = forwardRef(function Tag2({ text, href, accent = "secondary", onClick, className = "", prefix = "#" }, ref) {
|
|
7
|
+
const baseClasses = "inline-block font-mono text-xs font-bold uppercase border-2 border-edge-strong px-2 py-1 hover:shadow-hard-sm transition-all focus-visible:ring-2 focus-visible:ring-accent-primary";
|
|
8
|
+
const accentClass = accentFillClass(accent, "secondary");
|
|
9
|
+
const combinedClasses = cn(baseClasses, accentClass, className);
|
|
10
|
+
const label = text.startsWith(prefix) ? text : `${prefix}${text.split(" ").join("-")}`;
|
|
11
|
+
if (href) {
|
|
12
|
+
return /* @__PURE__ */ jsx("a", { "data-slot": "tag", ref, href, className: combinedClasses, onClick, children: label });
|
|
13
|
+
}
|
|
14
|
+
return /* @__PURE__ */ jsx("span", { "data-slot": "tag", ref, className: combinedClasses, onClick, children: label });
|
|
15
|
+
});
|
|
16
|
+
export {
|
|
17
|
+
Tag
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=Tag.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/Tag.tsx"],"sourcesContent":["import { forwardRef } from 'react';\nimport type { Ref } from 'react';\nimport type { ReactNode, MouseEventHandler } from 'react';\nimport type { AccentToken } from '../lib/theme';\nimport { cn } from '../lib/recipe';\nimport { accentFillClass } from '../lib/accentClasses';\n\n\nexport interface TagProps {\n /** Tag text string or custom element. */\n text: string;\n /** Optional custom URL href. If provided, renders as an `<a>` anchor tag. */\n href?: string;\n /** Background accent role. Defaults to `secondary`. */\n accent?: AccentToken;\n /** Click event handler. */\n onClick?: MouseEventHandler<HTMLElement>;\n /** Optional custom CSS classes. */\n className?: string;\n /** Prefix character, defaults to '#'. */\n prefix?: string;\n children?: ReactNode;\n}\n\nexport const Tag = forwardRef<HTMLAnchorElement | HTMLSpanElement, TagProps>(function Tag(\n { text, href, accent = 'secondary', onClick, className = '', prefix = '#' },\n ref,\n) {\n const baseClasses =\n 'inline-block font-mono text-xs font-bold uppercase border-2 border-edge-strong px-2 py-1 hover:shadow-hard-sm transition-all focus-visible:ring-2 focus-visible:ring-accent-primary';\n const accentClass = accentFillClass(accent, 'secondary');\n const combinedClasses = cn(baseClasses, accentClass, className);\n const label = text.startsWith(prefix) ? text : `${prefix}${text.split(' ').join('-')}`;\n\n if (href) {\n return (\n <a data-slot=\"tag\" ref={ref as Ref<HTMLAnchorElement>} href={href} className={combinedClasses} onClick={onClick}>\n {label}\n </a>\n );\n }\n\n return (\n <span data-slot=\"tag\" ref={ref as Ref<HTMLSpanElement>} className={combinedClasses} onClick={onClick}>\n {label}\n </span>\n );\n});\n"],"mappings":";AAAA,SAAS,kBAAkB;AAI3B,SAAS,UAAU;AACnB,SAAS,uBAAuB;AA+B1B;AAZC,IAAM,MAAM,WAA0D,SAASA,KACpF,EAAE,MAAM,MAAM,SAAS,aAAa,SAAS,YAAY,IAAI,SAAS,IAAI,GAC1E,KACA;AACA,QAAM,cACJ;AACF,QAAM,cAAc,gBAAgB,QAAQ,WAAW;AACvD,QAAM,kBAAkB,GAAG,aAAa,aAAa,SAAS;AAC9D,QAAM,QAAQ,KAAK,WAAW,MAAM,IAAI,OAAO,GAAG,MAAM,GAAG,KAAK,MAAM,GAAG,EAAE,KAAK,GAAG,CAAC;AAEpF,MAAI,MAAM;AACR,WACE,oBAAC,OAAE,aAAU,OAAM,KAAoC,MAAY,WAAW,iBAAiB,SAC5F,iBACH;AAAA,EAEJ;AAEA,SACE,oBAAC,UAAK,aAAU,OAAM,KAAkC,WAAW,iBAAiB,SACjF,iBACH;AAEJ,CAAC;","names":["Tag"]}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// src/components/TelemetryGauge.tsx
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { cn } from "../lib/recipe.js";
|
|
4
|
+
import { AsciiGauge } from "./AsciiGauge.js";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
var STATE_BADGE_STYLES = {
|
|
7
|
+
cruise: {
|
|
8
|
+
bg: "bg-intent-success",
|
|
9
|
+
text: "text-content-inverse",
|
|
10
|
+
label: "CRUISE",
|
|
11
|
+
accent: "success"
|
|
12
|
+
},
|
|
13
|
+
braking: {
|
|
14
|
+
bg: "bg-intent-warning",
|
|
15
|
+
text: "text-content-inverse",
|
|
16
|
+
label: "BRAKING",
|
|
17
|
+
accent: "warning"
|
|
18
|
+
},
|
|
19
|
+
halted: {
|
|
20
|
+
bg: "bg-intent-danger",
|
|
21
|
+
text: "text-content-inverse",
|
|
22
|
+
label: "HALTED",
|
|
23
|
+
accent: "danger"
|
|
24
|
+
},
|
|
25
|
+
spooling: {
|
|
26
|
+
bg: "bg-accent-primary",
|
|
27
|
+
text: "text-content-inverse",
|
|
28
|
+
label: "SPOOLING",
|
|
29
|
+
accent: "primary"
|
|
30
|
+
},
|
|
31
|
+
idle: {
|
|
32
|
+
bg: "bg-surface-sunken",
|
|
33
|
+
text: "text-content-muted",
|
|
34
|
+
label: "IDLE",
|
|
35
|
+
accent: "quiet"
|
|
36
|
+
},
|
|
37
|
+
online: {
|
|
38
|
+
bg: "bg-intent-success",
|
|
39
|
+
text: "text-content-inverse",
|
|
40
|
+
label: "ONLINE",
|
|
41
|
+
accent: "success"
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var TelemetryGauge = forwardRef(
|
|
45
|
+
function TelemetryGauge2({
|
|
46
|
+
state = "cruise",
|
|
47
|
+
stateLabel,
|
|
48
|
+
value,
|
|
49
|
+
target,
|
|
50
|
+
metricLabel,
|
|
51
|
+
barSegments = 8,
|
|
52
|
+
gaugeVariant = "block",
|
|
53
|
+
compact = false,
|
|
54
|
+
accent,
|
|
55
|
+
className,
|
|
56
|
+
...props
|
|
57
|
+
}, ref) {
|
|
58
|
+
const badgeConfig = STATE_BADGE_STYLES[state] ?? STATE_BADGE_STYLES.cruise;
|
|
59
|
+
const displayLabel = stateLabel ?? badgeConfig.label;
|
|
60
|
+
const displayMetric = metricLabel ?? `${Math.round(value)}%`;
|
|
61
|
+
return /* @__PURE__ */ jsxs(
|
|
62
|
+
"div",
|
|
63
|
+
{
|
|
64
|
+
ref,
|
|
65
|
+
"data-slot": "telemetry-gauge",
|
|
66
|
+
className: cn(
|
|
67
|
+
"inline-flex items-center gap-2.5 px-2.5 py-1 border border-edge-subtle bg-surface-raised font-mono text-xs select-none shadow-hard-sm",
|
|
68
|
+
className
|
|
69
|
+
),
|
|
70
|
+
...props,
|
|
71
|
+
children: [
|
|
72
|
+
!compact && /* @__PURE__ */ jsx(
|
|
73
|
+
"span",
|
|
74
|
+
{
|
|
75
|
+
"data-slot": "telemetry-gauge-pill",
|
|
76
|
+
className: cn(
|
|
77
|
+
"px-1.5 py-0.5 text-[10px] font-black uppercase tracking-wider",
|
|
78
|
+
badgeConfig.bg,
|
|
79
|
+
badgeConfig.text
|
|
80
|
+
),
|
|
81
|
+
children: displayLabel
|
|
82
|
+
}
|
|
83
|
+
),
|
|
84
|
+
/* @__PURE__ */ jsx(
|
|
85
|
+
"span",
|
|
86
|
+
{
|
|
87
|
+
"data-slot": "telemetry-gauge-metric",
|
|
88
|
+
className: "font-mono text-xs font-bold text-content-primary min-w-[32px] text-right",
|
|
89
|
+
children: displayMetric
|
|
90
|
+
}
|
|
91
|
+
),
|
|
92
|
+
/* @__PURE__ */ jsx(
|
|
93
|
+
AsciiGauge,
|
|
94
|
+
{
|
|
95
|
+
value,
|
|
96
|
+
target,
|
|
97
|
+
length: barSegments,
|
|
98
|
+
variant: gaugeVariant,
|
|
99
|
+
accent: accent ?? badgeConfig.accent,
|
|
100
|
+
className: "text-xs"
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
TelemetryGauge.displayName = "TelemetryGauge";
|
|
109
|
+
export {
|
|
110
|
+
TelemetryGauge
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=TelemetryGauge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/TelemetryGauge.tsx"],"sourcesContent":["import { forwardRef, type HTMLAttributes } from 'react';\nimport { cn } from '../lib/recipe';\nimport type { AccentToken } from '../lib/theme';\nimport { AsciiGauge, type AsciiGaugeVariant } from './AsciiGauge';\n\nexport type TelemetryMachineState =\n | 'cruise'\n | 'braking'\n | 'halted'\n | 'spooling'\n | 'idle'\n | 'online';\n\nexport interface TelemetryGaugeProps extends HTMLAttributes<HTMLDivElement> {\n /** Current machine operating state. */\n state?: TelemetryMachineState;\n /** Custom label override for state pill. Defaults to uppercase state name. */\n stateLabel?: string;\n /** Current value between min and max (0..100). */\n value: number;\n /** Target or benchmark value. */\n target?: number;\n /** Human-readable metric text (e.g. \"52px/s\", \"100%\", \"4.2 MB/s\"). */\n metricLabel?: string;\n /** Total character length of inner ASCII meter (default 8). */\n barSegments?: number;\n /** Visual glyph variant for underlying AsciiGauge. */\n gaugeVariant?: AsciiGaugeVariant;\n /** Responsive compact mode hiding the status pill on narrow viewports. */\n compact?: boolean;\n /** Accent role override for the meter. */\n accent?: AccentToken;\n}\n\ninterface StateBadgeConfig {\n bg: string;\n text: string;\n label: string;\n accent: AccentToken;\n}\n\nconst STATE_BADGE_STYLES: Record<TelemetryMachineState, StateBadgeConfig> = {\n cruise: {\n bg: 'bg-intent-success',\n text: 'text-content-inverse',\n label: 'CRUISE',\n accent: 'success',\n },\n braking: {\n bg: 'bg-intent-warning',\n text: 'text-content-inverse',\n label: 'BRAKING',\n accent: 'warning',\n },\n halted: {\n bg: 'bg-intent-danger',\n text: 'text-content-inverse',\n label: 'HALTED',\n accent: 'danger',\n },\n spooling: {\n bg: 'bg-accent-primary',\n text: 'text-content-inverse',\n label: 'SPOOLING',\n accent: 'primary',\n },\n idle: {\n bg: 'bg-surface-sunken',\n text: 'text-content-muted',\n label: 'IDLE',\n accent: 'quiet',\n },\n online: {\n bg: 'bg-intent-success',\n text: 'text-content-inverse',\n label: 'ONLINE',\n accent: 'success',\n },\n};\n\n/**\n * Composite terminal telemetry instrument HUD.\n *\n * Combines machine operational status pills, live numeric metric readouts,\n * and an embedded AsciiGauge. Designed for header docking, ticker tape end-slots,\n * and real-time talk presence widgets.\n *\n * @example\n * ```tsx\n * <TelemetryGauge state=\"cruise\" value={52} metricLabel=\"52px/s\" target={80} />\n * ```\n */\nexport const TelemetryGauge = forwardRef<HTMLDivElement, TelemetryGaugeProps>(\n function TelemetryGauge(\n {\n state = 'cruise',\n stateLabel,\n value,\n target,\n metricLabel,\n barSegments = 8,\n gaugeVariant = 'block',\n compact = false,\n accent,\n className,\n ...props\n },\n ref,\n ) {\n const badgeConfig = STATE_BADGE_STYLES[state] ?? STATE_BADGE_STYLES.cruise;\n const displayLabel = stateLabel ?? badgeConfig.label;\n const displayMetric = metricLabel ?? `${Math.round(value)}%`;\n\n return (\n <div\n ref={ref}\n data-slot=\"telemetry-gauge\"\n className={cn(\n 'inline-flex items-center gap-2.5 px-2.5 py-1 border border-edge-subtle bg-surface-raised font-mono text-xs select-none shadow-hard-sm',\n className,\n )}\n {...props}\n >\n {/* State badge pill */}\n {!compact && (\n <span\n data-slot=\"telemetry-gauge-pill\"\n className={cn(\n 'px-1.5 py-0.5 text-[10px] font-black uppercase tracking-wider',\n badgeConfig.bg,\n badgeConfig.text,\n )}\n >\n {displayLabel}\n </span>\n )}\n\n {/* Numeric readout */}\n <span\n data-slot=\"telemetry-gauge-metric\"\n className=\"font-mono text-xs font-bold text-content-primary min-w-[32px] text-right\"\n >\n {displayMetric}\n </span>\n\n {/* ASCII meter */}\n <AsciiGauge\n value={value}\n target={target}\n length={barSegments}\n variant={gaugeVariant}\n accent={accent ?? badgeConfig.accent}\n className=\"text-xs\"\n />\n </div>\n );\n },\n);\n\nTelemetryGauge.displayName = 'TelemetryGauge';\n"],"mappings":";AAAA,SAAS,kBAAuC;AAChD,SAAS,UAAU;AAEnB,SAAS,kBAA0C;AA+G7C,SAWI,KAXJ;AAzEN,IAAM,qBAAsE;AAAA,EAC1E,QAAQ;AAAA,IACN,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA,EACA,SAAS;AAAA,IACP,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA,EACA,QAAQ;AAAA,IACN,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA,EACA,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA,EACA,MAAM;AAAA,IACJ,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA,EACA,QAAQ;AAAA,IACN,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AACF;AAcO,IAAM,iBAAiB;AAAA,EAC5B,SAASA,gBACP;AAAA,IACE,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd,eAAe;AAAA,IACf,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,GACA,KACA;AACA,UAAM,cAAc,mBAAmB,KAAK,KAAK,mBAAmB;AACpE,UAAM,eAAe,cAAc,YAAY;AAC/C,UAAM,gBAAgB,eAAe,GAAG,KAAK,MAAM,KAAK,CAAC;AAEzD,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,aAAU;AAAA,QACV,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QACC,GAAG;AAAA,QAGH;AAAA,WAAC,WACA;AAAA,YAAC;AAAA;AAAA,cACC,aAAU;AAAA,cACV,WAAW;AAAA,gBACT;AAAA,gBACA,YAAY;AAAA,gBACZ,YAAY;AAAA,cACd;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,UAIF;AAAA,YAAC;AAAA;AAAA,cACC,aAAU;AAAA,cACV,WAAU;AAAA,cAET;AAAA;AAAA,UACH;AAAA,UAGA;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA,QAAQ;AAAA,cACR,SAAS;AAAA,cACT,QAAQ,UAAU,YAAY;AAAA,cAC9B,WAAU;AAAA;AAAA,UACZ;AAAA;AAAA;AAAA,IACF;AAAA,EAEJ;AACF;AAEA,eAAe,cAAc;","names":["TelemetryGauge"]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/components/ThemeProvider.tsx
|
|
4
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_LEVEL,
|
|
7
|
+
isThemeLevel,
|
|
8
|
+
LEVELS,
|
|
9
|
+
nextLevel,
|
|
10
|
+
SYSTEM_LEVEL,
|
|
11
|
+
THEME_LEVELS
|
|
12
|
+
} from "../theme/levels.js";
|
|
13
|
+
import { THEME_ATTRIBUTE, THEME_STORAGE_KEY } from "./themeInitScript.js";
|
|
14
|
+
import { ScopedLevelContext } from "./themeScope.js";
|
|
15
|
+
import { jsx } from "react/jsx-runtime";
|
|
16
|
+
var ThemeContext = createContext(void 0);
|
|
17
|
+
function readStoredLevel() {
|
|
18
|
+
try {
|
|
19
|
+
const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
|
|
20
|
+
return isThemeLevel(stored) ? stored : void 0;
|
|
21
|
+
} catch {
|
|
22
|
+
return void 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function readSystemLevel() {
|
|
26
|
+
if (typeof window.matchMedia !== "function") return void 0;
|
|
27
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? SYSTEM_LEVEL.dark : SYSTEM_LEVEL.light;
|
|
28
|
+
}
|
|
29
|
+
var ThemeProvider = ({
|
|
30
|
+
children,
|
|
31
|
+
defaultLevel = DEFAULT_LEVEL,
|
|
32
|
+
persist = true,
|
|
33
|
+
followSystem = true,
|
|
34
|
+
scoped = false,
|
|
35
|
+
className
|
|
36
|
+
}) => {
|
|
37
|
+
const [level, setLevelState] = useState(defaultLevel);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (scoped) return;
|
|
40
|
+
const applied = document.documentElement.getAttribute(THEME_ATTRIBUTE);
|
|
41
|
+
if (isThemeLevel(applied)) {
|
|
42
|
+
setLevelState(applied);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const resolved = (persist ? readStoredLevel() : void 0) ?? (followSystem ? readSystemLevel() : void 0) ?? defaultLevel;
|
|
46
|
+
setLevelState(resolved);
|
|
47
|
+
}, [scoped, persist, followSystem, defaultLevel]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (scoped) return;
|
|
50
|
+
document.documentElement.setAttribute(THEME_ATTRIBUTE, level);
|
|
51
|
+
document.documentElement.classList.remove(...THEME_LEVELS);
|
|
52
|
+
document.documentElement.classList.add(level);
|
|
53
|
+
if (!persist) return;
|
|
54
|
+
try {
|
|
55
|
+
window.localStorage.setItem(THEME_STORAGE_KEY, level);
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}, [level, persist, scoped]);
|
|
59
|
+
const setLevel = useCallback((next) => setLevelState(next), []);
|
|
60
|
+
const cycleLevel = useCallback(() => setLevelState((current) => nextLevel(current)), []);
|
|
61
|
+
const value = useMemo(
|
|
62
|
+
() => ({
|
|
63
|
+
level,
|
|
64
|
+
polarity: LEVELS[level].polarity,
|
|
65
|
+
setLevel,
|
|
66
|
+
cycleLevel,
|
|
67
|
+
levels: THEME_LEVELS
|
|
68
|
+
}),
|
|
69
|
+
[level, setLevel, cycleLevel]
|
|
70
|
+
);
|
|
71
|
+
const content = scoped ? /* @__PURE__ */ jsx("div", { "data-slot": "theme-provider", ...{ [THEME_ATTRIBUTE]: level }, className, children }) : children;
|
|
72
|
+
return /* @__PURE__ */ jsx(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx(ScopedLevelContext.Provider, { value: scoped ? level : void 0, children: content }) });
|
|
73
|
+
};
|
|
74
|
+
var useTheme = () => {
|
|
75
|
+
const context = useContext(ThemeContext);
|
|
76
|
+
if (!context) {
|
|
77
|
+
throw new Error("useTheme must be used within a ThemeProvider");
|
|
78
|
+
}
|
|
79
|
+
return context;
|
|
80
|
+
};
|
|
81
|
+
var useOptionalTheme = () => useContext(ThemeContext);
|
|
82
|
+
export {
|
|
83
|
+
ThemeProvider,
|
|
84
|
+
useOptionalTheme,
|
|
85
|
+
useTheme
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=ThemeProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/ThemeProvider.tsx"],"sourcesContent":["'use client';\n\nimport React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';\nimport {\n DEFAULT_LEVEL,\n isThemeLevel,\n LEVELS,\n nextLevel,\n SYSTEM_LEVEL,\n THEME_LEVELS,\n} from '../theme/levels';\nimport type { Polarity, ThemeLevel } from '../theme/levels';\nimport { THEME_ATTRIBUTE, THEME_STORAGE_KEY } from './themeInitScript';\nimport { ScopedLevelContext } from './themeScope';\n\nexport interface ThemeContextValue {\n level: ThemeLevel;\n /** `LEVELS[level].polarity`, lifted out because call sites ask for it constantly. */\n polarity: Polarity;\n setLevel: (level: ThemeLevel) => void;\n /** Step to the next level on the ladder, wrapping at the end. */\n cycleLevel: () => void;\n /** The ladder, in order — for building pickers without re-listing it. */\n levels: readonly ThemeLevel[];\n}\n\nconst ThemeContext = createContext<ThemeContextValue | undefined>(undefined);\n\nexport interface ThemeProviderProps {\n children: React.ReactNode;\n /**\n * Level to use before anything persisted is read, and on the server. Must\n * match whatever {@link getThemeInitScript} was given, or the first paint\n * disagrees with the markup.\n */\n defaultLevel?: ThemeLevel;\n /**\n * Persist the choice to `localStorage`. Turn off for a preview surface — a\n * Storybook decorator, an embedded demo — that should not write to the host\n * page's storage.\n */\n persist?: boolean;\n /**\n * Fall back to the OS `prefers-color-scheme` when nothing is persisted. A\n * rung is not named after a media-query state, so the mapping is the\n * explicit {@link SYSTEM_LEVEL} constant rather than an inference.\n */\n followSystem?: boolean;\n /**\n * Theme a subtree instead of the document.\n *\n * The token layer resolves by ordinary CSS inheritance, so a `bright` panel\n * inside a `midnight` page works at any depth — this is the React API for it.\n * A scoped provider never touches `documentElement` and never persists.\n */\n scoped?: boolean;\n /** Class applied to the wrapper element when `scoped`. */\n className?: string;\n}\n\nfunction readStoredLevel(): ThemeLevel | undefined {\n try {\n const stored = window.localStorage.getItem(THEME_STORAGE_KEY);\n return isThemeLevel(stored) ? stored : undefined;\n } catch {\n // Safari in private mode throws on localStorage access rather than\n // returning null. A theme preference is not worth breaking the render for.\n return undefined;\n }\n}\n\nfunction readSystemLevel(): ThemeLevel | undefined {\n if (typeof window.matchMedia !== 'function') return undefined;\n return window.matchMedia('(prefers-color-scheme: dark)').matches\n ? SYSTEM_LEVEL.dark\n : SYSTEM_LEVEL.light;\n}\n\n/**\n * Holds the active Level and applies it — to `documentElement`, or to a\n * wrapper element when `scoped`. Read it with {@link useTheme}.\n *\n * Pair it with {@link getThemeInitScript} in the document head, so the first\n * paint is already on the right Level; this component only takes over after\n * hydration.\n */\nexport const ThemeProvider: React.FC<ThemeProviderProps> = ({\n children,\n defaultLevel = DEFAULT_LEVEL,\n persist = true,\n followSystem = true,\n scoped = false,\n className,\n}) => {\n // Deliberately not seeded from storage. Server and first client render must\n // agree, so both start at `defaultLevel` and the effect below reconciles.\n const [level, setLevelState] = useState<ThemeLevel>(defaultLevel);\n\n useEffect(() => {\n if (scoped) return;\n const applied = document.documentElement.getAttribute(THEME_ATTRIBUTE);\n // The init script has usually already resolved this before paint; trust it\n // over re-deriving, so the two can never disagree.\n if (isThemeLevel(applied)) {\n setLevelState(applied);\n return;\n }\n const resolved =\n (persist ? readStoredLevel() : undefined) ??\n (followSystem ? readSystemLevel() : undefined) ??\n defaultLevel;\n setLevelState(resolved);\n }, [scoped, persist, followSystem, defaultLevel]);\n\n useEffect(() => {\n if (scoped) return;\n document.documentElement.setAttribute(THEME_ATTRIBUTE, level);\n // The class mirrors the attribute for consumers whose own CSS still selects\n // on it. Removing every level first keeps switching idempotent.\n document.documentElement.classList.remove(...THEME_LEVELS);\n document.documentElement.classList.add(level);\n if (!persist) return;\n try {\n window.localStorage.setItem(THEME_STORAGE_KEY, level);\n } catch {\n // See readStoredLevel.\n }\n }, [level, persist, scoped]);\n\n const setLevel = useCallback((next: ThemeLevel) => setLevelState(next), []);\n const cycleLevel = useCallback(() => setLevelState((current) => nextLevel(current)), []);\n\n const value = useMemo<ThemeContextValue>(\n () => ({\n level,\n polarity: LEVELS[level].polarity,\n setLevel,\n cycleLevel,\n levels: THEME_LEVELS,\n }),\n [level, setLevel, cycleLevel],\n );\n\n const content = scoped ? (\n <div data-slot=\"theme-provider\" {...{ [THEME_ATTRIBUTE]: level }} className={className}>\n {children}\n </div>\n ) : (\n children\n );\n\n return (\n <ThemeContext.Provider value={value}>\n <ScopedLevelContext.Provider value={scoped ? level : undefined}>\n {content}\n </ScopedLevelContext.Provider>\n </ThemeContext.Provider>\n );\n};\n\nexport const useTheme = (): ThemeContextValue => {\n const context = useContext(ThemeContext);\n if (!context) {\n throw new Error('useTheme must be used within a ThemeProvider');\n }\n return context;\n};\n\n/**\n * The level, if there is a provider, without throwing when there is not.\n *\n * For components that *adapt* to the level rather than requiring it — a divider\n * whose glyph differs by polarity, say. Those must still render standalone: in\n * a Storybook story with no decorator, an MDX preview, or a consumer that\n * themes with the attribute alone and never mounts a provider.\n */\nexport const useOptionalTheme = (): ThemeContextValue | undefined =>\n useContext(ThemeContext);\n"],"mappings":";;;AAEA,SAAgB,eAAe,aAAa,YAAY,WAAW,SAAS,gBAAgB;AAC5F;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,iBAAiB,yBAAyB;AACnD,SAAS,0BAA0B;AAmI/B;AAtHJ,IAAM,eAAe,cAA6C,MAAS;AAkC3E,SAAS,kBAA0C;AACjD,MAAI;AACF,UAAM,SAAS,OAAO,aAAa,QAAQ,iBAAiB;AAC5D,WAAO,aAAa,MAAM,IAAI,SAAS;AAAA,EACzC,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,kBAA0C;AACjD,MAAI,OAAO,OAAO,eAAe,WAAY,QAAO;AACpD,SAAO,OAAO,WAAW,8BAA8B,EAAE,UACrD,aAAa,OACb,aAAa;AACnB;AAUO,IAAM,gBAA8C,CAAC;AAAA,EAC1D;AAAA,EACA,eAAe;AAAA,EACf,UAAU;AAAA,EACV,eAAe;AAAA,EACf,SAAS;AAAA,EACT;AACF,MAAM;AAGJ,QAAM,CAAC,OAAO,aAAa,IAAI,SAAqB,YAAY;AAEhE,YAAU,MAAM;AACd,QAAI,OAAQ;AACZ,UAAM,UAAU,SAAS,gBAAgB,aAAa,eAAe;AAGrE,QAAI,aAAa,OAAO,GAAG;AACzB,oBAAc,OAAO;AACrB;AAAA,IACF;AACA,UAAM,YACH,UAAU,gBAAgB,IAAI,YAC9B,eAAe,gBAAgB,IAAI,WACpC;AACF,kBAAc,QAAQ;AAAA,EACxB,GAAG,CAAC,QAAQ,SAAS,cAAc,YAAY,CAAC;AAEhD,YAAU,MAAM;AACd,QAAI,OAAQ;AACZ,aAAS,gBAAgB,aAAa,iBAAiB,KAAK;AAG5D,aAAS,gBAAgB,UAAU,OAAO,GAAG,YAAY;AACzD,aAAS,gBAAgB,UAAU,IAAI,KAAK;AAC5C,QAAI,CAAC,QAAS;AACd,QAAI;AACF,aAAO,aAAa,QAAQ,mBAAmB,KAAK;AAAA,IACtD,QAAQ;AAAA,IAER;AAAA,EACF,GAAG,CAAC,OAAO,SAAS,MAAM,CAAC;AAE3B,QAAM,WAAW,YAAY,CAAC,SAAqB,cAAc,IAAI,GAAG,CAAC,CAAC;AAC1E,QAAM,aAAa,YAAY,MAAM,cAAc,CAAC,YAAY,UAAU,OAAO,CAAC,GAAG,CAAC,CAAC;AAEvF,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL;AAAA,MACA,UAAU,OAAO,KAAK,EAAE;AAAA,MACxB;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,IACA,CAAC,OAAO,UAAU,UAAU;AAAA,EAC9B;AAEA,QAAM,UAAU,SACd,oBAAC,SAAI,aAAU,kBAAkB,GAAG,EAAE,CAAC,eAAe,GAAG,MAAM,GAAG,WAC/D,UACH,IAEA;AAGF,SACE,oBAAC,aAAa,UAAb,EAAsB,OACrB,8BAAC,mBAAmB,UAAnB,EAA4B,OAAO,SAAS,QAAQ,QAClD,mBACH,GACF;AAEJ;AAEO,IAAM,WAAW,MAAyB;AAC/C,QAAM,UAAU,WAAW,YAAY;AACvC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,8CAA8C;AAAA,EAChE;AACA,SAAO;AACT;AAUO,IAAM,mBAAmB,MAC9B,WAAW,YAAY;","names":[]}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/components/TickerTape.tsx
|
|
4
|
+
import {
|
|
5
|
+
forwardRef,
|
|
6
|
+
useEffect,
|
|
7
|
+
useImperativeHandle,
|
|
8
|
+
useRef
|
|
9
|
+
} from "react";
|
|
10
|
+
import { cn } from "../lib/recipe.js";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
function easeOutCubic(t) {
|
|
13
|
+
return 1 - (1 - t) ** 3;
|
|
14
|
+
}
|
|
15
|
+
function easeInCubic(t) {
|
|
16
|
+
return t * t * t;
|
|
17
|
+
}
|
|
18
|
+
var TickerTape = forwardRef(
|
|
19
|
+
function TickerTape2({
|
|
20
|
+
items,
|
|
21
|
+
pixelsPerSecond = 52,
|
|
22
|
+
sticky = false,
|
|
23
|
+
endAddon,
|
|
24
|
+
title = "TERMINAL // LATEST_DISPATCHES",
|
|
25
|
+
brakeDuration = 0.7,
|
|
26
|
+
spoolDuration = 0.9,
|
|
27
|
+
paused = false,
|
|
28
|
+
onVelocityChange,
|
|
29
|
+
className,
|
|
30
|
+
onMouseEnter,
|
|
31
|
+
onMouseLeave,
|
|
32
|
+
...props
|
|
33
|
+
}, ref) {
|
|
34
|
+
const rootRef = useRef(null);
|
|
35
|
+
const trackRef = useRef(null);
|
|
36
|
+
useImperativeHandle(ref, () => rootRef.current);
|
|
37
|
+
const physicsRef = useRef({
|
|
38
|
+
currentRate: 1,
|
|
39
|
+
targetRate: 1,
|
|
40
|
+
rampStartRate: 1,
|
|
41
|
+
rampStartTime: 0,
|
|
42
|
+
scrollPos: 0,
|
|
43
|
+
lastTime: 0,
|
|
44
|
+
isHovered: false
|
|
45
|
+
});
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const track = trackRef.current;
|
|
48
|
+
if (track) {
|
|
49
|
+
track.style.transform = "translate3d(0, 0, 0)";
|
|
50
|
+
}
|
|
51
|
+
const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
52
|
+
if (paused || prefersReducedMotion) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let animationFrameId;
|
|
56
|
+
const tick = (now) => {
|
|
57
|
+
const state = physicsRef.current;
|
|
58
|
+
if (!state.lastTime) {
|
|
59
|
+
state.lastTime = now;
|
|
60
|
+
state.rampStartTime = now;
|
|
61
|
+
}
|
|
62
|
+
const dt = Math.min(0.1, (now - state.lastTime) / 1e3);
|
|
63
|
+
state.lastTime = now;
|
|
64
|
+
if (state.currentRate !== state.targetRate) {
|
|
65
|
+
const duration = state.targetRate === 0 ? brakeDuration : spoolDuration;
|
|
66
|
+
const elapsed = (now - state.rampStartTime) / 1e3;
|
|
67
|
+
const progress = Math.min(
|
|
68
|
+
1,
|
|
69
|
+
Math.max(0, elapsed / Math.max(1e-3, duration))
|
|
70
|
+
);
|
|
71
|
+
const eased = state.targetRate === 0 ? easeOutCubic(progress) : easeInCubic(progress);
|
|
72
|
+
state.currentRate = state.rampStartRate + (state.targetRate - state.rampStartRate) * eased;
|
|
73
|
+
if (progress >= 1) {
|
|
74
|
+
state.currentRate = state.targetRate;
|
|
75
|
+
}
|
|
76
|
+
const currentPct = Math.round(state.currentRate * 100);
|
|
77
|
+
onVelocityChange?.(state.currentRate, currentPct);
|
|
78
|
+
}
|
|
79
|
+
if (track) {
|
|
80
|
+
const loopBoundary = track.scrollWidth / 2;
|
|
81
|
+
if (loopBoundary > 0) {
|
|
82
|
+
state.scrollPos += state.currentRate * pixelsPerSecond * dt;
|
|
83
|
+
if (state.scrollPos >= loopBoundary) {
|
|
84
|
+
state.scrollPos %= loopBoundary;
|
|
85
|
+
}
|
|
86
|
+
track.style.transform = `translate3d(-${state.scrollPos}px, 0, 0)`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
animationFrameId = requestAnimationFrame(tick);
|
|
90
|
+
};
|
|
91
|
+
animationFrameId = requestAnimationFrame(tick);
|
|
92
|
+
return () => cancelAnimationFrame(animationFrameId);
|
|
93
|
+
}, [paused, pixelsPerSecond, brakeDuration, spoolDuration, onVelocityChange]);
|
|
94
|
+
const handleMouseEnter = (e) => {
|
|
95
|
+
const state = physicsRef.current;
|
|
96
|
+
state.isHovered = true;
|
|
97
|
+
state.targetRate = 0;
|
|
98
|
+
state.rampStartRate = state.currentRate;
|
|
99
|
+
state.rampStartTime = performance.now();
|
|
100
|
+
onMouseEnter?.(e);
|
|
101
|
+
};
|
|
102
|
+
const handleMouseLeave = (e) => {
|
|
103
|
+
const state = physicsRef.current;
|
|
104
|
+
state.isHovered = false;
|
|
105
|
+
state.targetRate = 1;
|
|
106
|
+
state.rampStartRate = state.currentRate;
|
|
107
|
+
state.rampStartTime = performance.now();
|
|
108
|
+
onMouseLeave?.(e);
|
|
109
|
+
};
|
|
110
|
+
return (
|
|
111
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Marquee container monitors hover to decelerate animation physics
|
|
112
|
+
/* @__PURE__ */ jsxs(
|
|
113
|
+
"div",
|
|
114
|
+
{
|
|
115
|
+
ref: rootRef,
|
|
116
|
+
role: "region",
|
|
117
|
+
"aria-label": title ?? "Ticker tape",
|
|
118
|
+
"data-slot": "ticker-tape",
|
|
119
|
+
className: cn(
|
|
120
|
+
sticky ? "fixed top-0 left-0 right-0 z-50 shadow-hard-md" : "relative",
|
|
121
|
+
"flex items-stretch border-y-2 border-edge-strong bg-surface-base font-mono text-xs text-content-primary overflow-hidden select-none",
|
|
122
|
+
className
|
|
123
|
+
),
|
|
124
|
+
onMouseEnter: handleMouseEnter,
|
|
125
|
+
onMouseLeave: handleMouseLeave,
|
|
126
|
+
...props,
|
|
127
|
+
children: [
|
|
128
|
+
title && /* @__PURE__ */ jsxs(
|
|
129
|
+
"div",
|
|
130
|
+
{
|
|
131
|
+
"data-slot": "ticker-tape-title",
|
|
132
|
+
className: "flex items-center px-3.5 py-2.5 border-r-2 border-edge-strong bg-surface-raised text-accent-primary font-bold tracking-wider text-[11px] whitespace-nowrap z-10 shrink-0",
|
|
133
|
+
children: [
|
|
134
|
+
/* @__PURE__ */ jsx("span", { className: "mr-2 text-content-muted", children: ">" }),
|
|
135
|
+
title
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
),
|
|
139
|
+
/* @__PURE__ */ jsx("div", { className: "relative flex-1 overflow-hidden py-2.5 flex items-center", children: /* @__PURE__ */ jsxs(
|
|
140
|
+
"div",
|
|
141
|
+
{
|
|
142
|
+
ref: trackRef,
|
|
143
|
+
"data-slot": "ticker-tape-track",
|
|
144
|
+
className: "flex whitespace-nowrap will-change-transform",
|
|
145
|
+
children: [
|
|
146
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-8 px-4 shrink-0", children: items.map((item, idx) => /* @__PURE__ */ jsxs(
|
|
147
|
+
"span",
|
|
148
|
+
{
|
|
149
|
+
className: "inline-flex items-center gap-3",
|
|
150
|
+
children: [
|
|
151
|
+
/* @__PURE__ */ jsx("span", { className: "text-content-muted font-bold", children: "/" }),
|
|
152
|
+
/* @__PURE__ */ jsx("span", { className: "text-content-secondary font-medium hover:text-content-primary transition-colors", children: item })
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
`ticker-item-a-${idx}`
|
|
156
|
+
)) }),
|
|
157
|
+
/* @__PURE__ */ jsx(
|
|
158
|
+
"div",
|
|
159
|
+
{
|
|
160
|
+
className: "flex items-center gap-8 px-4 shrink-0",
|
|
161
|
+
"aria-hidden": "true",
|
|
162
|
+
children: items.map((item, idx) => /* @__PURE__ */ jsxs(
|
|
163
|
+
"span",
|
|
164
|
+
{
|
|
165
|
+
className: "inline-flex items-center gap-3",
|
|
166
|
+
children: [
|
|
167
|
+
/* @__PURE__ */ jsx("span", { className: "text-content-muted font-bold", children: "/" }),
|
|
168
|
+
/* @__PURE__ */ jsx("span", { className: "text-content-secondary font-medium hover:text-content-primary transition-colors", children: item })
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
`ticker-item-b-${idx}`
|
|
172
|
+
))
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
) }),
|
|
178
|
+
endAddon && /* @__PURE__ */ jsx(
|
|
179
|
+
"div",
|
|
180
|
+
{
|
|
181
|
+
"data-slot": "ticker-tape-addon",
|
|
182
|
+
className: "flex items-stretch border-l-2 border-edge-strong bg-surface-raised z-10 shrink-0",
|
|
183
|
+
children: endAddon
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
TickerTape.displayName = "TickerTape";
|
|
193
|
+
export {
|
|
194
|
+
TickerTape
|
|
195
|
+
};
|
|
196
|
+
//# sourceMappingURL=TickerTape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/TickerTape.tsx"],"sourcesContent":["'use client';\n\nimport {\n forwardRef,\n type HTMLAttributes,\n type ReactNode,\n useEffect,\n useImperativeHandle,\n useRef,\n} from 'react';\nimport { cn } from '../lib/recipe';\n\nexport interface TickerTapeProps extends HTMLAttributes<HTMLDivElement> {\n /** Sequence of strings or React elements to cycle continuously. */\n items: ReactNode[];\n /** Velocity in pixels per second (default 52). */\n pixelsPerSecond?: number;\n /** Fixed sticky positioning at top of viewport. */\n sticky?: boolean;\n /** Trailing child component docked to the right edge (e.g. <TelemetryGauge /> or CTA). */\n endAddon?: ReactNode;\n /** Section title prefix displayed before the rolling marquee. */\n title?: string;\n /** Deceleration duration in seconds when hovered (default 0.7s). */\n brakeDuration?: number;\n /** Acceleration duration in seconds when hover leaves (default 0.9s). */\n spoolDuration?: number;\n /** Whether the marquee animation is paused at its resting position (default false). Useful for deterministic rendering, visual testing, or accessibility pause toggles. */\n paused?: boolean;\n /** Optional callback receiving current normalized velocity rate (0.0 to 1.0) and percentage. */\n onVelocityChange?: (rate: number, percentage: number) => void;\n}\n\n/** Cubic easing curves for symmetric momentum braking and flywheel spooling */\nfunction easeOutCubic(t: number): number {\n return 1 - (1 - t) ** 3;\n}\n\nfunction easeInCubic(t: number): number {\n return t * t * t;\n}\n\n/**\n * Terminal Marquee TickerTape component.\n *\n * Implements a continuous virtual RAF transform engine that decouples animation\n * from CSS keyframes, completely eliminating the Chromium animation timeline\n * reset bug upon hover state toggling. Features momentum-based hover braking,\n * dynamic velocity normalization, sticky docking, and a trailing endAddon slot.\n *\n * @example\n * ```tsx\n * <TickerTape\n * items={['RELEASE v0.8.0 DEPLOYED', 'SYSTEM TELEMETRY NOMINAL']}\n * endAddon={<TelemetryGauge state=\"cruise\" value={99} />}\n * />\n * ```\n */\nexport const TickerTape = forwardRef<HTMLDivElement, TickerTapeProps>(\n function TickerTape(\n {\n items,\n pixelsPerSecond = 52,\n sticky = false,\n endAddon,\n title = 'TERMINAL // LATEST_DISPATCHES',\n brakeDuration = 0.7,\n spoolDuration = 0.9,\n paused = false,\n onVelocityChange,\n className,\n onMouseEnter,\n onMouseLeave,\n ...props\n },\n ref,\n ) {\n const rootRef = useRef<HTMLDivElement>(null);\n const trackRef = useRef<HTMLDivElement>(null);\n\n useImperativeHandle(ref, () => rootRef.current as HTMLDivElement);\n\n // Animation physics state (persisted across renders via refs)\n const physicsRef = useRef({\n currentRate: 1.0,\n targetRate: 1.0,\n rampStartRate: 1.0,\n rampStartTime: 0,\n scrollPos: 0,\n lastTime: 0,\n isHovered: false,\n });\n\n useEffect(() => {\n const track = trackRef.current;\n if (track) {\n track.style.transform = 'translate3d(0, 0, 0)';\n }\n\n const prefersReducedMotion =\n typeof window !== 'undefined' &&\n window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n\n if (paused || prefersReducedMotion) {\n return;\n }\n\n let animationFrameId: number;\n\n const tick = (now: number) => {\n const state = physicsRef.current;\n\n if (!state.lastTime) {\n state.lastTime = now;\n state.rampStartTime = now;\n }\n\n const dt = Math.min(0.1, (now - state.lastTime) / 1000);\n state.lastTime = now;\n\n // 1. Calculate rate transition with easing\n if (state.currentRate !== state.targetRate) {\n const duration =\n state.targetRate === 0.0 ? brakeDuration : spoolDuration;\n const elapsed = (now - state.rampStartTime) / 1000;\n const progress = Math.min(\n 1.0,\n Math.max(0.0, elapsed / Math.max(0.001, duration)),\n );\n const eased =\n state.targetRate === 0.0\n ? easeOutCubic(progress)\n : easeInCubic(progress);\n\n state.currentRate =\n state.rampStartRate +\n (state.targetRate - state.rampStartRate) * eased;\n\n if (progress >= 1.0) {\n state.currentRate = state.targetRate;\n }\n\n const currentPct = Math.round(state.currentRate * 100);\n onVelocityChange?.(state.currentRate, currentPct);\n }\n\n // 2. Advance virtual scroll position without resetting\n if (track) {\n const loopBoundary = track.scrollWidth / 2;\n if (loopBoundary > 0) {\n state.scrollPos += state.currentRate * pixelsPerSecond * dt;\n if (state.scrollPos >= loopBoundary) {\n state.scrollPos %= loopBoundary;\n }\n track.style.transform = `translate3d(-${state.scrollPos}px, 0, 0)`;\n }\n }\n\n animationFrameId = requestAnimationFrame(tick);\n };\n\n animationFrameId = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(animationFrameId);\n }, [paused, pixelsPerSecond, brakeDuration, spoolDuration, onVelocityChange]);\n\n const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {\n const state = physicsRef.current;\n state.isHovered = true;\n state.targetRate = 0.0;\n state.rampStartRate = state.currentRate;\n state.rampStartTime = performance.now();\n onMouseEnter?.(e);\n };\n\n const handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {\n const state = physicsRef.current;\n state.isHovered = false;\n state.targetRate = 1.0;\n state.rampStartRate = state.currentRate;\n state.rampStartTime = performance.now();\n onMouseLeave?.(e);\n };\n\n return (\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Marquee container monitors hover to decelerate animation physics\n <div\n ref={rootRef}\n role=\"region\"\n aria-label={title ?? 'Ticker tape'}\n data-slot=\"ticker-tape\"\n className={cn(\n sticky ? 'fixed top-0 left-0 right-0 z-50 shadow-hard-md' : 'relative',\n 'flex items-stretch border-y-2 border-edge-strong bg-surface-base font-mono text-xs text-content-primary overflow-hidden select-none',\n className,\n )}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n {...props}\n >\n {/* Optional leading section title */}\n {title && (\n <div\n data-slot=\"ticker-tape-title\"\n className=\"flex items-center px-3.5 py-2.5 border-r-2 border-edge-strong bg-surface-raised text-accent-primary font-bold tracking-wider text-[11px] whitespace-nowrap z-10 shrink-0\"\n >\n <span className=\"mr-2 text-content-muted\">></span>\n {title}\n </div>\n )}\n\n {/* Marquee viewport */}\n <div className=\"relative flex-1 overflow-hidden py-2.5 flex items-center\">\n <div\n ref={trackRef}\n data-slot=\"ticker-tape-track\"\n className=\"flex whitespace-nowrap will-change-transform\"\n >\n {/* Primary Sequence */}\n <div className=\"flex items-center gap-8 px-4 shrink-0\">\n {items.map((item, idx) => (\n <span\n key={`ticker-item-a-${idx}`}\n className=\"inline-flex items-center gap-3\"\n >\n <span className=\"text-content-muted font-bold\">/</span>\n <span className=\"text-content-secondary font-medium hover:text-content-primary transition-colors\">\n {item}\n </span>\n </span>\n ))}\n </div>\n {/* Duplicate Sequence for seamless infinite loop */}\n <div\n className=\"flex items-center gap-8 px-4 shrink-0\"\n aria-hidden=\"true\"\n >\n {items.map((item, idx) => (\n <span\n key={`ticker-item-b-${idx}`}\n className=\"inline-flex items-center gap-3\"\n >\n <span className=\"text-content-muted font-bold\">/</span>\n <span className=\"text-content-secondary font-medium hover:text-content-primary transition-colors\">\n {item}\n </span>\n </span>\n ))}\n </div>\n </div>\n </div>\n\n {/* Trailing docked slot (endAddon) */}\n {endAddon && (\n <div\n data-slot=\"ticker-tape-addon\"\n className=\"flex items-stretch border-l-2 border-edge-strong bg-surface-raised z-10 shrink-0\"\n >\n {endAddon}\n </div>\n )}\n </div>\n );\n },\n);\n\nTickerTape.displayName = 'TickerTape';\n"],"mappings":"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","names":["TickerTape"]}
|