@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.
Files changed (284) hide show
  1. package/CHANGELOG.md +347 -0
  2. package/LICENSE +21 -0
  3. package/README.md +317 -2
  4. package/dist/components/AlertDialog.js +95 -0
  5. package/dist/components/AlertDialog.js.map +1 -0
  6. package/dist/components/AppShell.js +261 -0
  7. package/dist/components/AppShell.js.map +1 -0
  8. package/dist/components/AsciiDivider.js +8 -0
  9. package/dist/components/AsciiDivider.js.map +1 -0
  10. package/dist/components/AsciiGauge.js +130 -0
  11. package/dist/components/AsciiGauge.js.map +1 -0
  12. package/dist/components/Avatar.js +87 -0
  13. package/dist/components/Avatar.js.map +1 -0
  14. package/dist/components/Badge.js +25 -0
  15. package/dist/components/Badge.js.map +1 -0
  16. package/dist/components/BarChart.js +288 -0
  17. package/dist/components/BarChart.js.map +1 -0
  18. package/dist/components/BracketText.js +23 -0
  19. package/dist/components/BracketText.js.map +1 -0
  20. package/dist/components/BulletChart.js +50 -0
  21. package/dist/components/BulletChart.js.map +1 -0
  22. package/dist/components/Button.js +130 -0
  23. package/dist/components/Button.js.map +1 -0
  24. package/dist/components/Card.js +181 -0
  25. package/dist/components/Card.js.map +1 -0
  26. package/dist/components/ChartTooltip.js +54 -0
  27. package/dist/components/ChartTooltip.js.map +1 -0
  28. package/dist/components/Checkbox.js +55 -0
  29. package/dist/components/Checkbox.js.map +1 -0
  30. package/dist/components/ColophonCard.js +122 -0
  31. package/dist/components/ColophonCard.js.map +1 -0
  32. package/dist/components/DataTable.js +299 -0
  33. package/dist/components/DataTable.js.map +1 -0
  34. package/dist/components/Divider.js +42 -0
  35. package/dist/components/Divider.js.map +1 -0
  36. package/dist/components/Drawer.js +81 -0
  37. package/dist/components/Drawer.js.map +1 -0
  38. package/dist/components/EmptyState.js +39 -0
  39. package/dist/components/EmptyState.js.map +1 -0
  40. package/dist/components/ErrorSummary.js +122 -0
  41. package/dist/components/ErrorSummary.js.map +1 -0
  42. package/dist/components/Fieldset.js +37 -0
  43. package/dist/components/Fieldset.js.map +1 -0
  44. package/dist/components/Glyph.js +60 -0
  45. package/dist/components/Glyph.js.map +1 -0
  46. package/dist/components/Input.js +111 -0
  47. package/dist/components/Input.js.map +1 -0
  48. package/dist/components/LinkProvider.js +76 -0
  49. package/dist/components/LinkProvider.js.map +1 -0
  50. package/dist/components/Menu.js +144 -0
  51. package/dist/components/Menu.js.map +1 -0
  52. package/dist/components/MetadataGrid.js +184 -0
  53. package/dist/components/MetadataGrid.js.map +1 -0
  54. package/dist/components/MobileNav.js +120 -0
  55. package/dist/components/MobileNav.js.map +1 -0
  56. package/dist/components/Modal.js +79 -0
  57. package/dist/components/Modal.js.map +1 -0
  58. package/dist/components/NerdIcon.js +177 -0
  59. package/dist/components/NerdIcon.js.map +1 -0
  60. package/dist/components/NoteBlock.js +84 -0
  61. package/dist/components/NoteBlock.js.map +1 -0
  62. package/dist/components/PageHeader.js +34 -0
  63. package/dist/components/PageHeader.js.map +1 -0
  64. package/dist/components/PageTitle.js +21 -0
  65. package/dist/components/PageTitle.js.map +1 -0
  66. package/dist/components/Pagination.js +96 -0
  67. package/dist/components/Pagination.js.map +1 -0
  68. package/dist/components/Popover.js +87 -0
  69. package/dist/components/Popover.js.map +1 -0
  70. package/dist/components/Progress.js +83 -0
  71. package/dist/components/Progress.js.map +1 -0
  72. package/dist/components/RadioGroup.js +74 -0
  73. package/dist/components/RadioGroup.js.map +1 -0
  74. package/dist/components/ReportDocument.js +75 -0
  75. package/dist/components/ReportDocument.js.map +1 -0
  76. package/dist/components/SectionContainer.js +18 -0
  77. package/dist/components/SectionContainer.js.map +1 -0
  78. package/dist/components/Select.js +199 -0
  79. package/dist/components/Select.js.map +1 -0
  80. package/dist/components/SiteFooter.js +29 -0
  81. package/dist/components/SiteFooter.js.map +1 -0
  82. package/dist/components/SiteHeader.js +59 -0
  83. package/dist/components/SiteHeader.js.map +1 -0
  84. package/dist/components/SiteNav.js +141 -0
  85. package/dist/components/SiteNav.js.map +1 -0
  86. package/dist/components/Skeleton.js +51 -0
  87. package/dist/components/Skeleton.js.map +1 -0
  88. package/dist/components/SocialIcon.js +77 -0
  89. package/dist/components/SocialIcon.js.map +1 -0
  90. package/dist/components/Sparkline.js +104 -0
  91. package/dist/components/Sparkline.js.map +1 -0
  92. package/dist/components/Spinner.js +65 -0
  93. package/dist/components/Spinner.js.map +1 -0
  94. package/dist/components/StatCard.js +53 -0
  95. package/dist/components/StatCard.js.map +1 -0
  96. package/dist/components/StatusPage.js +99 -0
  97. package/dist/components/StatusPage.js.map +1 -0
  98. package/dist/components/Swatch.js +55 -0
  99. package/dist/components/Swatch.js.map +1 -0
  100. package/dist/components/Switch.js +53 -0
  101. package/dist/components/Switch.js.map +1 -0
  102. package/dist/components/TLDR.js +53 -0
  103. package/dist/components/TLDR.js.map +1 -0
  104. package/dist/components/Table.js +155 -0
  105. package/dist/components/Table.js.map +1 -0
  106. package/dist/components/Tabs.js +325 -0
  107. package/dist/components/Tabs.js.map +1 -0
  108. package/dist/components/Tag.js +19 -0
  109. package/dist/components/Tag.js.map +1 -0
  110. package/dist/components/TelemetryGauge.js +112 -0
  111. package/dist/components/TelemetryGauge.js.map +1 -0
  112. package/dist/components/ThemeProvider.js +87 -0
  113. package/dist/components/ThemeProvider.js.map +1 -0
  114. package/dist/components/TickerTape.js +196 -0
  115. package/dist/components/TickerTape.js.map +1 -0
  116. package/dist/components/Toast.js +200 -0
  117. package/dist/components/Toast.js.map +1 -0
  118. package/dist/components/Tooltip.js +69 -0
  119. package/dist/components/Tooltip.js.map +1 -0
  120. package/dist/components/TrackCard.js +206 -0
  121. package/dist/components/TrackCard.js.map +1 -0
  122. package/dist/components/blog/BlogPost.js +116 -0
  123. package/dist/components/blog/BlogPost.js.map +1 -0
  124. package/dist/components/blog/LoremIpsumPost.js +62 -0
  125. package/dist/components/blog/LoremIpsumPost.js.map +1 -0
  126. package/dist/components/blog/author.js +25 -0
  127. package/dist/components/blog/author.js.map +1 -0
  128. package/dist/components/booleanControl.js +50 -0
  129. package/dist/components/booleanControl.js.map +1 -0
  130. package/dist/components/dialogPopupRef.js +28 -0
  131. package/dist/components/dialogPopupRef.js.map +1 -0
  132. package/dist/components/dialogSurface.js +75 -0
  133. package/dist/components/dialogSurface.js.map +1 -0
  134. package/dist/components/docs/AnchorHeading.js +80 -0
  135. package/dist/components/docs/AnchorHeading.js.map +1 -0
  136. package/dist/components/docs/Breadcrumbs.js +26 -0
  137. package/dist/components/docs/Breadcrumbs.js.map +1 -0
  138. package/dist/components/docs/CodeBlock.js +63 -0
  139. package/dist/components/docs/CodeBlock.js.map +1 -0
  140. package/dist/components/docs/CodeTabs.js +71 -0
  141. package/dist/components/docs/CodeTabs.js.map +1 -0
  142. package/dist/components/docs/DocPager.js +28 -0
  143. package/dist/components/docs/DocPager.js.map +1 -0
  144. package/dist/components/docs/DocsHeader.js +124 -0
  145. package/dist/components/docs/DocsHeader.js.map +1 -0
  146. package/dist/components/docs/DocsLayout.js +61 -0
  147. package/dist/components/docs/DocsLayout.js.map +1 -0
  148. package/dist/components/docs/DocsLinkProvider.js +25 -0
  149. package/dist/components/docs/DocsLinkProvider.js.map +1 -0
  150. package/dist/components/docs/DocsSidebar.js +147 -0
  151. package/dist/components/docs/DocsSidebar.js.map +1 -0
  152. package/dist/components/docs/Prose.js +56 -0
  153. package/dist/components/docs/Prose.js.map +1 -0
  154. package/dist/components/docs/TableOfContents.js +71 -0
  155. package/dist/components/docs/TableOfContents.js.map +1 -0
  156. package/dist/components/docs/codeTabsStore.js +75 -0
  157. package/dist/components/docs/codeTabsStore.js.map +1 -0
  158. package/dist/components/docs/figures/ActivityGrid.js +116 -0
  159. package/dist/components/docs/figures/ActivityGrid.js.map +1 -0
  160. package/dist/components/docs/figures/BeforeAfter.js +64 -0
  161. package/dist/components/docs/figures/BeforeAfter.js.map +1 -0
  162. package/dist/components/docs/figures/ChangeSummary.js +74 -0
  163. package/dist/components/docs/figures/ChangeSummary.js.map +1 -0
  164. package/dist/components/docs/figures/Decision.js +34 -0
  165. package/dist/components/docs/figures/Decision.js.map +1 -0
  166. package/dist/components/docs/figures/DrawingSheet.js +37 -0
  167. package/dist/components/docs/figures/DrawingSheet.js.map +1 -0
  168. package/dist/components/docs/figures/FigureFrame.js +226 -0
  169. package/dist/components/docs/figures/FigureFrame.js.map +1 -0
  170. package/dist/components/docs/figures/FileChangeTree.js +134 -0
  171. package/dist/components/docs/figures/FileChangeTree.js.map +1 -0
  172. package/dist/components/docs/figures/FlowDiagram.js +82 -0
  173. package/dist/components/docs/figures/FlowDiagram.js.map +1 -0
  174. package/dist/components/docs/figures/GanttChart.js +79 -0
  175. package/dist/components/docs/figures/GanttChart.js.map +1 -0
  176. package/dist/components/docs/figures/IndexedSection.js +45 -0
  177. package/dist/components/docs/figures/IndexedSection.js.map +1 -0
  178. package/dist/components/docs/figures/MetricStrip.js +23 -0
  179. package/dist/components/docs/figures/MetricStrip.js.map +1 -0
  180. package/dist/components/docs/figures/Steps.js +58 -0
  181. package/dist/components/docs/figures/Steps.js.map +1 -0
  182. package/dist/components/docs/figures/SwatchCompare.js +42 -0
  183. package/dist/components/docs/figures/SwatchCompare.js.map +1 -0
  184. package/dist/components/docs/figures/TermGrid.js +22 -0
  185. package/dist/components/docs/figures/TermGrid.js.map +1 -0
  186. package/dist/components/docs/figures/Terminal.js +37 -0
  187. package/dist/components/docs/figures/Terminal.js.map +1 -0
  188. package/dist/components/docs/figures/Timeline.js +61 -0
  189. package/dist/components/docs/figures/Timeline.js.map +1 -0
  190. package/dist/components/docs/figures/TokenTable.js +42 -0
  191. package/dist/components/docs/figures/TokenTable.js.map +1 -0
  192. package/dist/components/docs/figures/TreeDiagram.js +76 -0
  193. package/dist/components/docs/figures/TreeDiagram.js.map +1 -0
  194. package/dist/components/docs/figures/UptimeStrip.js +68 -0
  195. package/dist/components/docs/figures/UptimeStrip.js.map +1 -0
  196. package/dist/components/docs/figures/index.js +24 -0
  197. package/dist/components/docs/figures/index.js.map +1 -0
  198. package/dist/components/docs/index.js +15 -0
  199. package/dist/components/docs/index.js.map +1 -0
  200. package/dist/components/docs/mdxComponents.js +108 -0
  201. package/dist/components/docs/mdxComponents.js.map +1 -0
  202. package/dist/components/experiments/DesignSandbox.js +206 -0
  203. package/dist/components/experiments/DesignSandbox.js.map +1 -0
  204. package/dist/components/experiments/ExperimentsView.js +138 -0
  205. package/dist/components/experiments/ExperimentsView.js.map +1 -0
  206. package/dist/components/fieldFrame.js +200 -0
  207. package/dist/components/fieldFrame.js.map +1 -0
  208. package/dist/components/floatingSurface.js +35 -0
  209. package/dist/components/floatingSurface.js.map +1 -0
  210. package/dist/components/marketing/CTASection.js +47 -0
  211. package/dist/components/marketing/CTASection.js.map +1 -0
  212. package/dist/components/marketing/FeatureGrid.js +49 -0
  213. package/dist/components/marketing/FeatureGrid.js.map +1 -0
  214. package/dist/components/marketing/Hero.js +44 -0
  215. package/dist/components/marketing/Hero.js.map +1 -0
  216. package/dist/components/marketing/PricingGrid.js +65 -0
  217. package/dist/components/marketing/PricingGrid.js.map +1 -0
  218. package/dist/components/marketing/SectionGrid.js +51 -0
  219. package/dist/components/marketing/SectionGrid.js.map +1 -0
  220. package/dist/components/paginationRange.js +28 -0
  221. package/dist/components/paginationRange.js.map +1 -0
  222. package/dist/components/portalTheme.js +14 -0
  223. package/dist/components/portalTheme.js.map +1 -0
  224. package/dist/components/saas/SaasLandingPage.js +201 -0
  225. package/dist/components/saas/SaasLandingPage.js.map +1 -0
  226. package/dist/components/siteNavContext.js +24 -0
  227. package/dist/components/siteNavContext.js.map +1 -0
  228. package/dist/components/slides/Slide.js +84 -0
  229. package/dist/components/slides/Slide.js.map +1 -0
  230. package/dist/components/slides/SlideDeck.js +228 -0
  231. package/dist/components/slides/SlideDeck.js.map +1 -0
  232. package/dist/components/themeInitScript.js +16 -0
  233. package/dist/components/themeInitScript.js.map +1 -0
  234. package/dist/components/themeScope.js +9 -0
  235. package/dist/components/themeScope.js.map +1 -0
  236. package/dist/components/toastLifetime.js +21 -0
  237. package/dist/components/toastLifetime.js.map +1 -0
  238. package/dist/fonts/DSSymbols-OFL.txt +98 -0
  239. package/dist/fonts/DSSymbols-Regular.woff2 +0 -0
  240. package/dist/fonts/SymbolsNerdFontMono-Regular.woff2 +0 -0
  241. package/dist/hooks/useActiveHeading.js +59 -0
  242. package/dist/hooks/useActiveHeading.js.map +1 -0
  243. package/dist/hooks/useCopyToClipboard.js +58 -0
  244. package/dist/hooks/useCopyToClipboard.js.map +1 -0
  245. package/dist/index.d.ts +6394 -0
  246. package/dist/index.js +117 -0
  247. package/dist/index.js.map +1 -0
  248. package/dist/lib/accentClasses.js +87 -0
  249. package/dist/lib/accentClasses.js.map +1 -0
  250. package/dist/lib/recipe.js +33 -0
  251. package/dist/lib/recipe.js.map +1 -0
  252. package/dist/lib/slug.js +39 -0
  253. package/dist/lib/slug.js.map +1 -0
  254. package/dist/lib/theme.js +76 -0
  255. package/dist/lib/theme.js.map +1 -0
  256. package/dist/lib/tokenRules.js +51 -0
  257. package/dist/lib/tokenRules.js.map +1 -0
  258. package/dist/prose.css +766 -0
  259. package/dist/styles.css +203 -0
  260. package/dist/theme/ansi.js +77 -0
  261. package/dist/theme/ansi.js.map +1 -0
  262. package/dist/theme/contrast.js +342 -0
  263. package/dist/theme/contrast.js.map +1 -0
  264. package/dist/theme/levels.js +234 -0
  265. package/dist/theme/levels.js.map +1 -0
  266. package/dist/theme/media.js +115 -0
  267. package/dist/theme/media.js.map +1 -0
  268. package/dist/theme/recommended.js +105 -0
  269. package/dist/theme/recommended.js.map +1 -0
  270. package/dist/theme.css +538 -0
  271. package/dist/tokens.js +1 -0
  272. package/dist/tokens.js.map +1 -0
  273. package/package.json +209 -6
  274. package/skills/design-system-usage/SKILL.md +75 -0
  275. package/terminal/rtk-midnight.alacritty.toml +33 -0
  276. package/terminal/rtk-midnight.ghostty.conf +24 -0
  277. package/terminal/rtk-midnight.itermcolors +167 -0
  278. package/terminal/rtk-midnight.windows-terminal.json +23 -0
  279. package/terminal/rtk-sketch.alacritty.toml +33 -0
  280. package/terminal/rtk-sketch.ghostty.conf +24 -0
  281. package/terminal/rtk-sketch.itermcolors +167 -0
  282. package/terminal/rtk-sketch.windows-terminal.json +23 -0
  283. package/tokens/palette.midnight.tokens.json +477 -0
  284. package/tokens/palette.sketch.tokens.json +477 -0
@@ -0,0 +1,228 @@
1
+ "use client";
2
+
3
+ // src/components/slides/SlideDeck.tsx
4
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ import { ChevronLeft, ChevronRight, FileText, Maximize2, Minimize2, Play, Pause } from "lucide-react";
6
+ import { useHotkeys } from "@tanstack/react-hotkeys";
7
+ import { Button } from "../Button.js";
8
+ import { Tooltip } from "../Tooltip.js";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
10
+ function speakerNotesOf(node) {
11
+ if (!React.isValidElement(node)) return void 0;
12
+ const notes = node.props.speakerNotes;
13
+ return notes?.trim() ? notes : void 0;
14
+ }
15
+ var SLIDE_DECK_HOTKEYS = [
16
+ { hotkey: "ArrowRight", label: "Next", description: "Advance one slide, wrapping at the end", action: "next" },
17
+ { hotkey: "Space", label: "Next", description: "Advance one slide, as \u2192 does", action: "next" },
18
+ { hotkey: "ArrowLeft", label: "Previous", description: "Go back one slide, wrapping at the start", action: "previous" },
19
+ { hotkey: "F", label: "Fullscreen", description: "Toggle fullscreen on the deck frame", action: "fullscreen", preventsDefault: false },
20
+ { hotkey: "N", label: "Notes", description: "Toggle the presenter panel \u2014 bound only when a slide carries notes", action: "notes", requires: "notes" }
21
+ ];
22
+ var SlideDeck = ({
23
+ children,
24
+ aspectRatio = "16:9",
25
+ autoPlayInterval = 0,
26
+ slide,
27
+ onSlideChange,
28
+ chrome = true
29
+ }) => {
30
+ const [uncontrolledSlide, setUncontrolledSlide] = useState(0);
31
+ const [isFullscreen, setIsFullscreen] = useState(false);
32
+ const [isPlaying, setIsPlaying] = useState(false);
33
+ const [showNotes, setShowNotes] = useState(false);
34
+ const containerRef = useRef(null);
35
+ const totalSlides = React.Children.count(children);
36
+ const isControlled = slide !== void 0;
37
+ const currentSlide = isControlled ? slide : uncontrolledSlide;
38
+ const slideList = React.Children.toArray(children);
39
+ const deckHasNotes = slideList.some((child) => speakerNotesOf(child) !== void 0);
40
+ const currentNotes = speakerNotesOf(slideList[currentSlide]);
41
+ const goTo = useCallback(
42
+ (next) => {
43
+ if (totalSlides === 0) return;
44
+ const wrapped = (next % totalSlides + totalSlides) % totalSlides;
45
+ if (!isControlled) setUncontrolledSlide(wrapped);
46
+ onSlideChange?.(wrapped);
47
+ },
48
+ [isControlled, onSlideChange, totalSlides]
49
+ );
50
+ const nextSlide = useCallback(
51
+ () => goTo(currentSlide + 1),
52
+ [goTo, currentSlide]
53
+ );
54
+ const prevSlide = useCallback(
55
+ () => goTo(currentSlide - 1),
56
+ [goTo, currentSlide]
57
+ );
58
+ const toggleFullscreen = useCallback(() => {
59
+ if (!containerRef.current) return;
60
+ if (!document.fullscreenElement) {
61
+ containerRef.current.requestFullscreen().catch((err) => console.error(err));
62
+ setIsFullscreen(true);
63
+ } else {
64
+ document.exitFullscreen().catch((err) => console.error(err));
65
+ setIsFullscreen(false);
66
+ }
67
+ }, []);
68
+ const deckActions = useMemo(
69
+ () => ({
70
+ next: nextSlide,
71
+ previous: prevSlide,
72
+ fullscreen: toggleFullscreen,
73
+ notes: () => setShowNotes((visible) => !visible)
74
+ }),
75
+ [nextSlide, prevSlide, toggleFullscreen]
76
+ );
77
+ useHotkeys(
78
+ SLIDE_DECK_HOTKEYS.map((binding) => ({
79
+ hotkey: binding.hotkey,
80
+ callback: () => deckActions[binding.action](),
81
+ options: {
82
+ enabled: chrome && (binding.requires !== "notes" || deckHasNotes),
83
+ preventDefault: binding.preventsDefault !== false,
84
+ stopPropagation: false,
85
+ conflictBehavior: "allow",
86
+ meta: { name: binding.label, description: binding.description }
87
+ }
88
+ }))
89
+ );
90
+ useEffect(() => {
91
+ if (!isPlaying || autoPlayInterval <= 0) return;
92
+ const timer = setInterval(() => {
93
+ nextSlide();
94
+ }, autoPlayInterval);
95
+ return () => clearInterval(timer);
96
+ }, [isPlaying, autoPlayInterval, nextSlide]);
97
+ return /* @__PURE__ */ jsxs(
98
+ "div",
99
+ {
100
+ "data-slot": "slide-deck",
101
+ ref: containerRef,
102
+ style: {
103
+ width: "100%",
104
+ maxWidth: isFullscreen ? "100vw" : "1000px",
105
+ margin: "0 auto",
106
+ border: "3px solid var(--ds-border-strong)",
107
+ boxShadow: isFullscreen ? "none" : "8px 8px 0px 0px var(--ds-shadow-color)",
108
+ backgroundColor: "var(--ds-surface-base)",
109
+ display: "flex",
110
+ flexDirection: "column"
111
+ },
112
+ children: [
113
+ /* @__PURE__ */ jsx(
114
+ "div",
115
+ {
116
+ style: {
117
+ width: "100%",
118
+ aspectRatio: isFullscreen ? "auto" : aspectRatio === "16:9" ? "16 / 9" : "4 / 3",
119
+ flex: isFullscreen ? 1 : "none",
120
+ overflow: "hidden",
121
+ position: "relative"
122
+ },
123
+ children: slideList[currentSlide]
124
+ }
125
+ ),
126
+ chrome && /* @__PURE__ */ jsxs(
127
+ "div",
128
+ {
129
+ style: {
130
+ padding: "1rem 1.5rem",
131
+ borderTop: "2px solid var(--ds-border-strong)",
132
+ backgroundColor: "var(--ds-surface-base)",
133
+ display: "flex",
134
+ justifyContent: "space-between",
135
+ alignItems: "center",
136
+ fontFamily: "var(--ds-font-mono)"
137
+ },
138
+ children: [
139
+ /* @__PURE__ */ jsxs("div", { style: { color: "var(--ds-accent-secondary)", fontWeight: 700, fontSize: "0.9rem" }, children: [
140
+ "SLIDE ",
141
+ String(currentSlide + 1).padStart(2, "0"),
142
+ " / ",
143
+ String(totalSlides).padStart(2, "0")
144
+ ] }),
145
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.75rem", alignItems: "center" }, children: [
146
+ /* @__PURE__ */ jsxs(Button, { onClick: prevSlide, style: { padding: "0.4rem 0.8rem" }, children: [
147
+ /* @__PURE__ */ jsx(ChevronLeft, { size: 16 }),
148
+ " PREV"
149
+ ] }),
150
+ autoPlayInterval > 0 && /* @__PURE__ */ jsx(Tooltip, { content: isPlaying ? "Pause autoplay" : "Start autoplay", children: /* @__PURE__ */ jsx(
151
+ Button,
152
+ {
153
+ onClick: () => setIsPlaying(!isPlaying),
154
+ "aria-pressed": isPlaying,
155
+ "aria-label": isPlaying ? "Pause autoplay" : "Start autoplay",
156
+ style: { padding: "0.4rem 0.8rem" },
157
+ children: isPlaying ? /* @__PURE__ */ jsx(Pause, { size: 16, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Play, { size: 16, "aria-hidden": "true" })
158
+ }
159
+ ) }),
160
+ /* @__PURE__ */ jsxs(Button, { onClick: nextSlide, style: { padding: "0.4rem 0.8rem" }, children: [
161
+ "NEXT ",
162
+ /* @__PURE__ */ jsx(ChevronRight, { size: 16 })
163
+ ] }),
164
+ deckHasNotes && /* @__PURE__ */ jsx(Tooltip, { content: showNotes ? "Hide notes (N)" : "Speaker notes (N)", children: /* @__PURE__ */ jsx(
165
+ Button,
166
+ {
167
+ onClick: () => setShowNotes((visible) => !visible),
168
+ "aria-pressed": showNotes,
169
+ "aria-label": showNotes ? "Hide speaker notes" : "Show speaker notes",
170
+ style: { padding: "0.4rem 0.8rem" },
171
+ children: /* @__PURE__ */ jsx(FileText, { size: 16, "aria-hidden": "true" })
172
+ }
173
+ ) }),
174
+ /* @__PURE__ */ jsx(Tooltip, { content: isFullscreen ? "Exit fullscreen (F)" : "Fullscreen (F)", children: /* @__PURE__ */ jsx(
175
+ Button,
176
+ {
177
+ onClick: toggleFullscreen,
178
+ "aria-pressed": isFullscreen,
179
+ "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
180
+ style: { padding: "0.4rem 0.8rem" },
181
+ children: isFullscreen ? /* @__PURE__ */ jsx(Minimize2, { size: 16, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Maximize2, { size: 16, "aria-hidden": "true" })
182
+ }
183
+ ) })
184
+ ] })
185
+ ]
186
+ }
187
+ ),
188
+ showNotes && /* @__PURE__ */ jsxs(
189
+ "div",
190
+ {
191
+ style: {
192
+ padding: "1rem 1.5rem",
193
+ borderTop: "2px solid var(--ds-border-strong)",
194
+ backgroundColor: "var(--ds-surface-raised)",
195
+ fontFamily: "var(--ds-font-mono)",
196
+ fontSize: "0.85rem",
197
+ lineHeight: 1.6,
198
+ color: "var(--ds-text-primary)",
199
+ whiteSpace: "pre-wrap"
200
+ },
201
+ children: [
202
+ /* @__PURE__ */ jsx(
203
+ "div",
204
+ {
205
+ style: {
206
+ fontWeight: 700,
207
+ textTransform: "uppercase",
208
+ fontSize: "0.7rem",
209
+ letterSpacing: "0.08em",
210
+ color: "var(--ds-accent-secondary)",
211
+ marginBottom: "0.5rem"
212
+ },
213
+ children: "[ Speaker notes ]"
214
+ }
215
+ ),
216
+ currentNotes ?? "No notes on this slide."
217
+ ]
218
+ }
219
+ )
220
+ ]
221
+ }
222
+ );
223
+ };
224
+ export {
225
+ SLIDE_DECK_HOTKEYS,
226
+ SlideDeck
227
+ };
228
+ //# sourceMappingURL=SlideDeck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/slides/SlideDeck.tsx"],"sourcesContent":["'use client';\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { ChevronLeft, ChevronRight, FileText, Maximize2, Minimize2, Play, Pause } from 'lucide-react';\nimport { useHotkeys, type RegisterableHotkey } from '@tanstack/react-hotkeys';\nimport { Button } from '../Button';\nimport { Tooltip } from '../Tooltip';\nimport type { SlideProps } from './Slide';\n\n/**\n * The notes on one slide, if it has any.\n *\n * Reading a child's props is not a thing to do lightly, but it is the right\n * shape here: notes belong *with* the slide that needs them, and the presenter\n * surface belongs to the deck. The alternative — a `notes` array on the deck,\n * indexed by position — puts the two halves of one fact in different files and\n * silently misaligns the moment a slide is inserted.\n */\nfunction speakerNotesOf(node: React.ReactNode): string | undefined {\n if (!React.isValidElement(node)) return undefined;\n const notes = (node.props as Partial<SlideProps>).speakerNotes;\n return notes?.trim() ? notes : undefined;\n}\n\n/**\n * One key on the deck's keyboard surface, as registered and as documented.\n * This array is the only place a deck key is written: `SlideDeck` registers\n * it with `@tanstack/react-hotkeys`, the presenter sees it in the controls,\n * and the docs cheatsheet renders it — three consumers of one list, so none\n * of them can drift from the other two.\n */\nexport interface SlideDeckHotkey {\n /** The binding in @tanstack/react-hotkeys notation — `ArrowRight`, `f`. */\n hotkey: RegisterableHotkey;\n /** What the key does, in the deck's own words. */\n label: string;\n /** The one line the cheatsheet shows beside the key. */\n description: string;\n action: 'next' | 'previous' | 'fullscreen' | 'notes';\n /**\n * Whether the browser's own behaviour (caret, scroll, space-scroll) is\n * swallowed when the binding fires. `false` for fullscreen, whose `F` is\n * not a scrolling key and whose default action belongs to nobody.\n */\n preventsDefault?: boolean;\n /** A binding only exists when the deck has the feature it drives. */\n requires?: 'notes';\n}\n\nexport const SLIDE_DECK_HOTKEYS: readonly SlideDeckHotkey[] = [\n { hotkey: 'ArrowRight', label: 'Next', description: 'Advance one slide, wrapping at the end', action: 'next' },\n { hotkey: 'Space', label: 'Next', description: 'Advance one slide, as → does', action: 'next' },\n { hotkey: 'ArrowLeft', label: 'Previous', description: 'Go back one slide, wrapping at the start', action: 'previous' },\n { hotkey: 'F', label: 'Fullscreen', description: 'Toggle fullscreen on the deck frame', action: 'fullscreen', preventsDefault: false },\n { hotkey: 'N', label: 'Notes', description: 'Toggle the presenter panel — bound only when a slide carries notes', action: 'notes', requires: 'notes' },\n];\n\nexport interface SlideDeckProps {\n children: React.ReactElement[];\n aspectRatio?: '16:9' | '4:3';\n /**\n * Milliseconds between automatic advances. `0` — the default — disables\n * autoplay and hides the play control entirely, so a deck that does not want\n * it gains no chrome. Non-zero offers the control; it is right for a kiosk\n * and wrong for a talk, where the presenter owns the pacing.\n */\n autoPlayInterval?: number;\n /**\n * The slide to show. Pass it to drive the deck from outside — a router, a\n * story, a frame renderer — and the deck stops owning the index. Omit it and\n * the deck keeps its own, which is the existing behaviour.\n *\n * The deck holding the index is why it could not previously be screenshotted\n * at a chosen slide, deep-linked, or driven by anything but a person.\n */\n slide?: number;\n /** Fires with the slide the deck wants to move to, controlled or not. */\n onSlideChange?: (slide: number) => void;\n /**\n * The presenter affordances — control bar, arrow keys, fullscreen. On by\n * default. Turn them off for a surface that already owns its own navigation,\n * or that has no person in front of it at all.\n */\n chrome?: boolean;\n}\n\n/**\n * The presentation shell around a set of `Slide`s: framing, paging, fullscreen\n * and optional autoplay.\n *\n * It owns the aspect ratio so slides do not have to, wraps at both ends (next\n * from the last slide returns to the first), and exposes fullscreen through the\n * Fullscreen API on its own container rather than the document — so a deck\n * embedded in a page goes fullscreen without taking the page with it.\n *\n * **Presenter notes.** Any child carrying `speakerNotes` makes a notes control\n * appear, and `N` toggles a panel below the frame showing the current slide's.\n * Both the control and the panel are absent unless some slide has notes, and\n * the panel starts closed — so notes never reach a projector unasked, and a\n * deck without them renders exactly as it did before the feature existed.\n *\n * Keyboard: the bindings in `SLIDE_DECK_HOTKEYS` — `←`/`→` or space to page,\n * `F` for fullscreen, `N` for notes when any slide carries them.\n *\n * Slides are indexed by position, so the children must be a stable array. A\n * conditional slide that disappears shifts every index after it.\n *\n * ```tsx\n * <SlideDeck aspectRatio=\"16:9\">\n * <Slide title=\"Overview\">…</Slide>\n * <Slide title=\"Deployments\">…</Slide>\n * </SlideDeck>\n * ```\n */\nexport const SlideDeck: React.FC<SlideDeckProps> = ({\n children,\n aspectRatio = '16:9',\n autoPlayInterval = 0,\n slide,\n onSlideChange,\n chrome = true,\n}) => {\n // Only consulted when uncontrolled. `slide` winning outright is what keeps a\n // controlled deck from ever disagreeing with its caller.\n const [uncontrolledSlide, setUncontrolledSlide] = useState(0);\n const [isFullscreen, setIsFullscreen] = useState(false);\n const [isPlaying, setIsPlaying] = useState(false);\n const [showNotes, setShowNotes] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n\n const totalSlides = React.Children.count(children);\n const isControlled = slide !== undefined;\n const currentSlide = isControlled ? slide : uncontrolledSlide;\n const slideList = React.Children.toArray(children);\n\n /**\n * Notes are opt-in twice over: off until asked for, and invisible as a\n * feature unless some slide actually carries them. A deck without notes\n * therefore renders exactly as it did before presenter notes existed.\n */\n const deckHasNotes = slideList.some((child) => speakerNotesOf(child) !== undefined);\n const currentNotes = speakerNotesOf(slideList[currentSlide]);\n\n const goTo = useCallback(\n (next: number) => {\n // Guarded rather than left to the modulo: an empty deck used to send\n // `prevSlide` to -1, which indexed nothing and rendered blank.\n if (totalSlides === 0) return;\n const wrapped = ((next % totalSlides) + totalSlides) % totalSlides;\n if (!isControlled) setUncontrolledSlide(wrapped);\n onSlideChange?.(wrapped);\n },\n [isControlled, onSlideChange, totalSlides],\n );\n\n const nextSlide = useCallback(\n () => goTo(currentSlide + 1),\n [goTo, currentSlide],\n );\n const prevSlide = useCallback(\n () => goTo(currentSlide - 1),\n [goTo, currentSlide],\n );\n\n const toggleFullscreen = useCallback(() => {\n if (!containerRef.current) return;\n if (!document.fullscreenElement) {\n containerRef.current.requestFullscreen().catch((err) => console.error(err));\n setIsFullscreen(true);\n } else {\n document.exitFullscreen().catch((err) => console.error(err));\n setIsFullscreen(false);\n }\n }, []);\n\n const deckActions = useMemo(\n () => ({\n next: nextSlide,\n previous: prevSlide,\n fullscreen: toggleFullscreen,\n notes: () => setShowNotes((visible) => !visible),\n }),\n [nextSlide, prevSlide, toggleFullscreen],\n );\n\n // `SLIDE_DECK_HOTKEYS` replaces a hand-rolled `window` keydown switch:\n // registration is declarative, `enabled` stands in for the old\n // `if (!chrome) return` and the notes gate (a disabled binding does not\n // fire and does not preventDefault, but stays visible in devtools), and\n // single-key bindings default to `ignoreInputs` — the old handler stole\n // arrows and space from any focused text field, which this no longer does.\n // `stopPropagation: false` and the per-binding `preventsDefault` keep the\n // wired behaviour identical; `conflictBehavior: 'allow'` reproduces what\n // two `window` listeners did for two mounted decks — both respond, neither\n // warns. One tightening is real: the old switch read `e.key`, so Shift+F\n // and Shift+N fired; the bindings are plain F and N, which is what a\n // presenter means.\n useHotkeys(\n SLIDE_DECK_HOTKEYS.map((binding) => ({\n hotkey: binding.hotkey,\n callback: () => deckActions[binding.action](),\n options: {\n enabled: chrome && (binding.requires !== 'notes' || deckHasNotes),\n preventDefault: binding.preventsDefault !== false,\n stopPropagation: false,\n conflictBehavior: 'allow',\n meta: { name: binding.label, description: binding.description },\n },\n })),\n );\n\n useEffect(() => {\n if (!isPlaying || autoPlayInterval <= 0) return;\n const timer = setInterval(() => {\n nextSlide();\n }, autoPlayInterval);\n return () => clearInterval(timer);\n }, [isPlaying, autoPlayInterval, nextSlide]);\n\n return (\n <div data-slot=\"slide-deck\"\n ref={containerRef}\n style={{\n width: '100%',\n maxWidth: isFullscreen ? '100vw' : '1000px',\n margin: '0 auto',\n border: '3px solid var(--ds-border-strong)',\n boxShadow: isFullscreen ? 'none' : '8px 8px 0px 0px var(--ds-shadow-color)',\n backgroundColor: 'var(--ds-surface-base)',\n display: 'flex',\n flexDirection: 'column',\n }}\n >\n {/* Slide viewport */}\n <div\n style={{\n width: '100%',\n aspectRatio: isFullscreen ? 'auto' : (aspectRatio === '16:9' ? '16 / 9' : '4 / 3'),\n flex: isFullscreen ? 1 : 'none',\n overflow: 'hidden',\n position: 'relative',\n }}\n >\n {slideList[currentSlide]}\n </div>\n\n {/* Control bar — the presenter affordances, off when `chrome` is false. */}\n {chrome && (\n <div\n style={{\n padding: '1rem 1.5rem',\n borderTop: '2px solid var(--ds-border-strong)',\n backgroundColor: 'var(--ds-surface-base)',\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n fontFamily: 'var(--ds-font-mono)',\n }}\n >\n {/* Slide Counter */}\n <div style={{ color: 'var(--ds-accent-secondary)', fontWeight: 700, fontSize: '0.9rem' }}>\n SLIDE {String(currentSlide + 1).padStart(2, '0')} / {String(totalSlides).padStart(2, '0')}\n </div>\n\n {/* Action Controls */}\n <div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center' }}>\n <Button onClick={prevSlide} style={{ padding: '0.4rem 0.8rem' }}>\n <ChevronLeft size={16} /> PREV\n </Button>\n\n {autoPlayInterval > 0 && (\n <Tooltip content={isPlaying ? 'Pause autoplay' : 'Start autoplay'}>\n <Button\n onClick={() => setIsPlaying(!isPlaying)}\n aria-pressed={isPlaying}\n aria-label={isPlaying ? 'Pause autoplay' : 'Start autoplay'}\n style={{ padding: '0.4rem 0.8rem' }}\n >\n {isPlaying ? <Pause size={16} aria-hidden=\"true\" /> : <Play size={16} aria-hidden=\"true\" />}\n </Button>\n </Tooltip>\n )}\n\n <Button onClick={nextSlide} style={{ padding: '0.4rem 0.8rem' }}>\n NEXT <ChevronRight size={16} />\n </Button>\n\n {/*\n * The icon-only controls carry a `Tooltip` rather than `title=\"\"`\n * (#166): the hint now reaches a keyboard user on focus, and says\n * the shortcut. The `aria-label` stays the accessible name — a\n * tooltip is visual, and restates it for sighted users.\n */}\n {deckHasNotes && (\n <Tooltip content={showNotes ? 'Hide notes (N)' : 'Speaker notes (N)'}>\n <Button\n onClick={() => setShowNotes((visible) => !visible)}\n aria-pressed={showNotes}\n aria-label={showNotes ? 'Hide speaker notes' : 'Show speaker notes'}\n style={{ padding: '0.4rem 0.8rem' }}\n >\n <FileText size={16} aria-hidden=\"true\" />\n </Button>\n </Tooltip>\n )}\n\n <Tooltip content={isFullscreen ? 'Exit fullscreen (F)' : 'Fullscreen (F)'}>\n <Button\n onClick={toggleFullscreen}\n aria-pressed={isFullscreen}\n aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}\n style={{ padding: '0.4rem 0.8rem' }}\n >\n {isFullscreen ? <Minimize2 size={16} aria-hidden=\"true\" /> : <Maximize2 size={16} aria-hidden=\"true\" />}\n </Button>\n </Tooltip>\n </div>\n </div>\n )}\n\n {/*\n Presenter notes. Below the control bar rather than over the slide, so\n that a deck mirrored to a projector shows the audience the slide and\n nothing else — the notes are outside the frame the viewport clips.\n */}\n {showNotes && (\n <div\n style={{\n padding: '1rem 1.5rem',\n borderTop: '2px solid var(--ds-border-strong)',\n backgroundColor: 'var(--ds-surface-raised)',\n fontFamily: 'var(--ds-font-mono)',\n fontSize: '0.85rem',\n lineHeight: 1.6,\n color: 'var(--ds-text-primary)',\n whiteSpace: 'pre-wrap',\n }}\n >\n <div\n style={{\n fontWeight: 700,\n textTransform: 'uppercase',\n fontSize: '0.7rem',\n letterSpacing: '0.08em',\n color: 'var(--ds-accent-secondary)',\n marginBottom: '0.5rem',\n }}\n >\n [ Speaker notes ]\n </div>\n {currentNotes ?? 'No notes on this slide.'}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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","names":[]}
@@ -0,0 +1,16 @@
1
+ // src/components/themeInitScript.ts
2
+ import { DEFAULT_LEVEL, SYSTEM_LEVEL, THEME_LEVELS } from "../theme/levels.js";
3
+ var THEME_STORAGE_KEY = "ds-theme-level";
4
+ var THEME_ATTRIBUTE = "data-theme";
5
+ function getThemeInitScript(options = {}) {
6
+ const { defaultLevel = DEFAULT_LEVEL, followSystem = true } = options;
7
+ const levels = JSON.stringify(THEME_LEVELS);
8
+ const system = JSON.stringify(SYSTEM_LEVEL);
9
+ return `(function(){try{var l=${levels},s=${system},k=${JSON.stringify(THEME_STORAGE_KEY)};var v=null;try{v=localStorage.getItem(k)}catch(e){}if(l.indexOf(v)===-1){v=${followSystem ? `(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)?s.dark:s.light` : JSON.stringify(defaultLevel)}}document.documentElement.setAttribute(${JSON.stringify(THEME_ATTRIBUTE)},v)}catch(e){}})();`;
10
+ }
11
+ export {
12
+ THEME_ATTRIBUTE,
13
+ THEME_STORAGE_KEY,
14
+ getThemeInitScript
15
+ };
16
+ //# sourceMappingURL=themeInitScript.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/themeInitScript.ts"],"sourcesContent":["/**\n * The pre-paint theme script and the two names it shares with `ThemeProvider`.\n *\n * A module of its own rather than a part of `ThemeProvider.tsx` because that\n * file is a client module (`'use client'`), and a server component importing a\n * value from a client module receives a client reference it cannot call. The\n * canonical caller of `getThemeInitScript` is a root layout, which in the Next\n * App Router is a server component — so this has to stay server-safe.\n */\nimport { DEFAULT_LEVEL, SYSTEM_LEVEL, THEME_LEVELS } from '../theme/levels';\nimport type { ThemeLevel } from '../theme/levels';\n\n/** Where the chosen level is persisted. Shared with {@link getThemeInitScript}. */\nexport const THEME_STORAGE_KEY = 'ds-theme-level';\n\n/** The attribute the CSS keys off. Shared with {@link getThemeInitScript}. */\nexport const THEME_ATTRIBUTE = 'data-theme';\n\n/**\n * The script to run before first paint, so the page never flashes the default\n * level and then corrects itself.\n *\n * Drop the returned string into an inline `<script>` in the document head,\n * ahead of the stylesheet. React cannot do this job: anything it renders runs\n * after hydration, which is already too late — and reading `localStorage` in a\n * `useState` initialiser (what this component used to do) makes the server and\n * client render different markup, which is a hydration mismatch.\n *\n * ```tsx\n * <script dangerouslySetInnerHTML={{ __html: getThemeInitScript() }} />\n * ```\n */\nexport function getThemeInitScript(\n options: { defaultLevel?: ThemeLevel; followSystem?: boolean } = {},\n): string {\n const { defaultLevel = DEFAULT_LEVEL, followSystem = true } = options;\n // Serialised rather than interpolated loosely, so the level list and the\n // system mapping in this script cannot drift from levels.ts.\n const levels = JSON.stringify(THEME_LEVELS);\n const system = JSON.stringify(SYSTEM_LEVEL);\n return `(function(){try{var l=${levels},s=${system},k=${JSON.stringify(THEME_STORAGE_KEY)};var v=null;try{v=localStorage.getItem(k)}catch(e){}if(l.indexOf(v)===-1){v=${\n followSystem\n ? `(window.matchMedia&&window.matchMedia(\"(prefers-color-scheme: dark)\").matches)?s.dark:s.light`\n : JSON.stringify(defaultLevel)\n }}document.documentElement.setAttribute(${JSON.stringify(THEME_ATTRIBUTE)},v)}catch(e){}})();`;\n}\n"],"mappings":";AASA,SAAS,eAAe,cAAc,oBAAoB;AAInD,IAAM,oBAAoB;AAG1B,IAAM,kBAAkB;AAgBxB,SAAS,mBACd,UAAiE,CAAC,GAC1D;AACR,QAAM,EAAE,eAAe,eAAe,eAAe,KAAK,IAAI;AAG9D,QAAM,SAAS,KAAK,UAAU,YAAY;AAC1C,QAAM,SAAS,KAAK,UAAU,YAAY;AAC1C,SAAO,yBAAyB,MAAM,MAAM,MAAM,MAAM,KAAK,UAAU,iBAAiB,CAAC,+EACvF,eACI,kGACA,KAAK,UAAU,YAAY,CACjC,0CAA0C,KAAK,UAAU,eAAe,CAAC;AAC3E;","names":[]}
@@ -0,0 +1,9 @@
1
+ "use client";
2
+
3
+ // src/components/themeScope.ts
4
+ import { createContext } from "react";
5
+ var ScopedLevelContext = createContext(void 0);
6
+ export {
7
+ ScopedLevelContext
8
+ };
9
+ //# sourceMappingURL=themeScope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/themeScope.ts"],"sourcesContent":["'use client';\n\nimport { createContext } from 'react';\nimport type { ThemeLevel } from '../theme/levels';\n\n// The nearest provider's Level when that provider is scoped, `undefined` when\n// it is unscoped or absent. Kept out of `ThemeProvider.tsx` because the package\n// re-exports that module wholesale, and this is internal.\nexport const ScopedLevelContext = createContext<ThemeLevel | undefined>(undefined);\n"],"mappings":";;;AAEA,SAAS,qBAAqB;AAMvB,IAAM,qBAAqB,cAAsC,MAAS;","names":[]}
@@ -0,0 +1,21 @@
1
+ // src/components/toastLifetime.ts
2
+ var DEFAULT_TIMEOUT = 6e3;
3
+ var MS_PER_WORD = 300;
4
+ var NOTICE_MS = 2e3;
5
+ function words(text) {
6
+ return typeof text === "string" ? text.split(/\s+/).filter(Boolean).length : 0;
7
+ }
8
+ function resolveLifetime(options, providerTimeout) {
9
+ if (options.timeout !== void 0) return options.timeout;
10
+ if (options.action) return 0;
11
+ if (providerTimeout === 0) return 0;
12
+ const reading = NOTICE_MS + (words(options.title) + words(options.description)) * MS_PER_WORD;
13
+ return Math.max(providerTimeout, reading);
14
+ }
15
+ export {
16
+ DEFAULT_TIMEOUT,
17
+ MS_PER_WORD,
18
+ NOTICE_MS,
19
+ resolveLifetime
20
+ };
21
+ //# sourceMappingURL=toastLifetime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/toastLifetime.ts"],"sourcesContent":["/**\n * How long a toast stays up — policy, kept out of `Toast.tsx` so it can be\n * tested directly without being exported from the package. `src/index.ts`\n * re-exports named members of `Toast.tsx` only, and nothing re-exports this.\n */\n\n/** The fields of a toast the lifetime depends on. */\ninterface LifetimeInput {\n title: string;\n description?: unknown;\n action?: unknown;\n timeout?: number;\n}\n\n/**\n * Six seconds. Base UI's default is five; the extra second is for the reader\n * #243 names — a screen reader still announcing when the timer fires.\n */\nexport const DEFAULT_TIMEOUT = 6000;\n\n/**\n * Reading time, as a floor under the lifetime.\n *\n * 200 words a minute is a conservative reading speed (300ms a word), and the\n * two seconds on top are for noticing the toast at all before reading starts.\n * Only text can be counted: a `description` that is a React node contributes\n * nothing, and the caller who built one can pass `timeout` themselves.\n */\nexport const MS_PER_WORD = 300;\nexport const NOTICE_MS = 2000;\n\nfunction words(text: unknown): number {\n return typeof text === 'string' ? text.split(/\\s+/).filter(Boolean).length : 0;\n}\n\n/**\n * The lifetime in milliseconds, `0` meaning \"until dismissed\".\n *\n * 1. An explicit `timeout` always wins — including `0`.\n * 2. A toast with an action persists. A control that can vanish on a timer is\n * one a keyboard or screen-reader user may never reach; pausing on hover\n * protects only the pointer.\n * 3. A provider `timeout` of `0` means every toast persists.\n * 4. Otherwise the provider's timeout, raised to the reading time of the text.\n */\nexport function resolveLifetime(options: LifetimeInput, providerTimeout: number): number {\n if (options.timeout !== undefined) return options.timeout;\n if (options.action) return 0;\n if (providerTimeout === 0) return 0;\n const reading = NOTICE_MS + (words(options.title) + words(options.description)) * MS_PER_WORD;\n return Math.max(providerTimeout, reading);\n}\n"],"mappings":";AAkBO,IAAM,kBAAkB;AAUxB,IAAM,cAAc;AACpB,IAAM,YAAY;AAEzB,SAAS,MAAM,MAAuB;AACpC,SAAO,OAAO,SAAS,WAAW,KAAK,MAAM,KAAK,EAAE,OAAO,OAAO,EAAE,SAAS;AAC/E;AAYO,SAAS,gBAAgB,SAAwB,iBAAiC;AACvF,MAAI,QAAQ,YAAY,OAAW,QAAO,QAAQ;AAClD,MAAI,QAAQ,OAAQ,QAAO;AAC3B,MAAI,oBAAoB,EAAG,QAAO;AAClC,QAAM,UAAU,aAAa,MAAM,QAAQ,KAAK,IAAI,MAAM,QAAQ,WAAW,KAAK;AAClF,SAAO,KAAK,IAAI,iBAAiB,OAAO;AAC1C;","names":[]}
@@ -0,0 +1,98 @@
1
+ DSSymbols-Regular.woff2 is a subset of JetBrains Mono Regular 2.304
2
+ (https://github.com/JetBrains/JetBrainsMono, release v2.304), cut to the symbol
3
+ blocks named by its unicode-range in src/styles.css with fontTools pyftsubset.
4
+ No Reserved Font Name is declared. It is served under the family name "DS Symbols".
5
+
6
+ Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
7
+
8
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
9
+ This license is copied below, and is also available with a FAQ at:
10
+ https://scripts.sil.org/OFL
11
+
12
+
13
+ -----------------------------------------------------------
14
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
15
+ -----------------------------------------------------------
16
+
17
+ PREAMBLE
18
+ The goals of the Open Font License (OFL) are to stimulate worldwide
19
+ development of collaborative font projects, to support the font creation
20
+ efforts of academic and linguistic communities, and to provide a free and
21
+ open framework in which fonts may be shared and improved in partnership
22
+ with others.
23
+
24
+ The OFL allows the licensed fonts to be used, studied, modified and
25
+ redistributed freely as long as they are not sold by themselves. The
26
+ fonts, including any derivative works, can be bundled, embedded,
27
+ redistributed and/or sold with any software provided that any reserved
28
+ names are not used by derivative works. The fonts and derivatives,
29
+ however, cannot be released under any other type of license. The
30
+ requirement for fonts to remain under this license does not apply
31
+ to any document created using the fonts or their derivatives.
32
+
33
+ DEFINITIONS
34
+ "Font Software" refers to the set of files released by the Copyright
35
+ Holder(s) under this license and clearly marked as such. This may
36
+ include source files, build scripts and documentation.
37
+
38
+ "Reserved Font Name" refers to any names specified as such after the
39
+ copyright statement(s).
40
+
41
+ "Original Version" refers to the collection of Font Software components as
42
+ distributed by the Copyright Holder(s).
43
+
44
+ "Modified Version" refers to any derivative made by adding to, deleting,
45
+ or substituting -- in part or in whole -- any of the components of the
46
+ Original Version, by changing formats or by porting the Font Software to a
47
+ new environment.
48
+
49
+ "Author" refers to any designer, engineer, programmer, technical
50
+ writer or other person who contributed to the Font Software.
51
+
52
+ PERMISSION & CONDITIONS
53
+ Permission is hereby granted, free of charge, to any person obtaining
54
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
55
+ redistribute, and sell modified and unmodified copies of the Font
56
+ Software, subject to the following conditions:
57
+
58
+ 1) Neither the Font Software nor any of its individual components,
59
+ in Original or Modified Versions, may be sold by itself.
60
+
61
+ 2) Original or Modified Versions of the Font Software may be bundled,
62
+ redistributed and/or sold with any software, provided that each copy
63
+ contains the above copyright notice and this license. These can be
64
+ included either as stand-alone text files, human-readable headers or
65
+ in the appropriate machine-readable metadata fields within text or
66
+ binary files as long as those fields can be easily viewed by the user.
67
+
68
+ 3) No Modified Version of the Font Software may use the Reserved Font
69
+ Name(s) unless explicit written permission is granted by the corresponding
70
+ Copyright Holder. This restriction only applies to the primary font name as
71
+ presented to the users.
72
+
73
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
74
+ Software shall not be used to promote, endorse or advertise any
75
+ Modified Version, except to acknowledge the contribution(s) of the
76
+ Copyright Holder(s) and the Author(s) or with their explicit written
77
+ permission.
78
+
79
+ 5) The Font Software, modified or unmodified, in part or in whole,
80
+ must be distributed entirely under this license, and must not be
81
+ distributed under any other license. The requirement for fonts to
82
+ remain under this license does not apply to any document created
83
+ using the Font Software.
84
+
85
+ TERMINATION
86
+ This license becomes null and void if any of the above conditions are
87
+ not met.
88
+
89
+ DISCLAIMER
90
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
91
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
92
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
93
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
94
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
95
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
96
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
97
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
98
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ // src/hooks/useActiveHeading.ts
4
+ import { useEffect, useState } from "react";
5
+ function useActiveHeading(ids, { offset, enabled = true } = {}) {
6
+ const [active, setActive] = useState(null);
7
+ const key = ids.join("|");
8
+ useEffect(() => {
9
+ if (!enabled || typeof window === "undefined") return;
10
+ const headingIds = key ? key.split("|") : [];
11
+ if (headingIds.length === 0) {
12
+ setActive(null);
13
+ return;
14
+ }
15
+ let frame = 0;
16
+ const resolveOffset = () => {
17
+ if (typeof offset === "number") return offset;
18
+ const raw = getComputedStyle(document.documentElement).getPropertyValue(
19
+ "--docs-header-height"
20
+ );
21
+ const parsed = Number.parseFloat(raw);
22
+ return Number.isFinite(parsed) ? parsed : 0;
23
+ };
24
+ const measure = () => {
25
+ frame = 0;
26
+ const line = resolveOffset() + 24;
27
+ const scrolledToBottom = window.innerHeight + window.scrollY >= document.body.scrollHeight - 2;
28
+ if (scrolledToBottom) {
29
+ setActive(headingIds[headingIds.length - 1] ?? null);
30
+ return;
31
+ }
32
+ let current = null;
33
+ for (const id of headingIds) {
34
+ const el = document.getElementById(id);
35
+ if (!el) continue;
36
+ if (el.getBoundingClientRect().top <= line) current = id;
37
+ else break;
38
+ }
39
+ setActive(current);
40
+ };
41
+ const onScroll = () => {
42
+ if (frame) return;
43
+ frame = window.requestAnimationFrame(measure);
44
+ };
45
+ measure();
46
+ window.addEventListener("scroll", onScroll, { passive: true });
47
+ window.addEventListener("resize", onScroll);
48
+ return () => {
49
+ if (frame) window.cancelAnimationFrame(frame);
50
+ window.removeEventListener("scroll", onScroll);
51
+ window.removeEventListener("resize", onScroll);
52
+ };
53
+ }, [key, offset, enabled]);
54
+ return active;
55
+ }
56
+ export {
57
+ useActiveHeading
58
+ };
59
+ //# sourceMappingURL=useActiveHeading.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useActiveHeading.ts"],"sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\n\nexport interface UseActiveHeadingOptions {\n /**\n * Distance from the top of the viewport, in px, that counts as the \"reading\n * line\". A heading is active once it scrolls above this line. Defaults to\n * reading the `--docs-header-height` custom property so scroll-spy stays in\n * sync with the sticky header without callers passing the number around.\n */\n offset?: number;\n /** Disable the listener entirely (e.g. when no TOC is rendered). */\n enabled?: boolean;\n}\n\n/**\n * Scroll-spy over a list of heading ids. Returns the id of the heading the\n * reader is currently inside — the last one whose top edge has passed the\n * reading line — or `null` above the first heading.\n *\n * Uses measured rects on a rAF-throttled scroll listener rather than an\n * IntersectionObserver. \"Last heading above a line\" is awkward to express as an\n * observer `rootMargin` (a heading scrolled far past the top stops intersecting\n * and would stop reporting), and doc pages have few enough headings that\n * measuring them is cheap.\n */\nexport function useActiveHeading(\n ids: readonly string[],\n { offset, enabled = true }: UseActiveHeadingOptions = {},\n): string | null {\n const [active, setActive] = useState<string | null>(null);\n\n // Depend on the joined key rather than the array identity: callers almost\n // always build `ids` inline from a TOC, so a fresh array arrives every render\n // and an identity dep would rebind the listener continuously.\n const key = ids.join('|');\n\n useEffect(() => {\n if (!enabled || typeof window === 'undefined') return;\n\n const headingIds = key ? key.split('|') : [];\n if (headingIds.length === 0) {\n setActive(null);\n return;\n }\n\n let frame = 0;\n\n const resolveOffset = (): number => {\n if (typeof offset === 'number') return offset;\n const raw = getComputedStyle(document.documentElement).getPropertyValue(\n '--docs-header-height',\n );\n const parsed = Number.parseFloat(raw);\n return Number.isFinite(parsed) ? parsed : 0;\n };\n\n const measure = () => {\n frame = 0;\n // A little breathing room below the header so a heading counts as \"read\"\n // just before it tucks underneath it.\n const line = resolveOffset() + 24;\n\n // Bottom of the page: the final sections may be too short to ever cross\n // the line, so pin to the last heading instead of leaving it unreachable.\n const scrolledToBottom =\n window.innerHeight + window.scrollY >= document.body.scrollHeight - 2;\n if (scrolledToBottom) {\n setActive(headingIds[headingIds.length - 1] ?? null);\n return;\n }\n\n let current: string | null = null;\n for (const id of headingIds) {\n const el = document.getElementById(id);\n if (!el) continue;\n if (el.getBoundingClientRect().top <= line) current = id;\n else break;\n }\n\n setActive(current);\n };\n\n const onScroll = () => {\n if (frame) return;\n frame = window.requestAnimationFrame(measure);\n };\n\n measure();\n window.addEventListener('scroll', onScroll, { passive: true });\n window.addEventListener('resize', onScroll);\n\n return () => {\n if (frame) window.cancelAnimationFrame(frame);\n window.removeEventListener('scroll', onScroll);\n window.removeEventListener('resize', onScroll);\n };\n }, [key, offset, enabled]);\n\n return active;\n}\n"],"mappings":";;;AAEA,SAAS,WAAW,gBAAgB;AAyB7B,SAAS,iBACd,KACA,EAAE,QAAQ,UAAU,KAAK,IAA6B,CAAC,GACxC;AACf,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAwB,IAAI;AAKxD,QAAM,MAAM,IAAI,KAAK,GAAG;AAExB,YAAU,MAAM;AACd,QAAI,CAAC,WAAW,OAAO,WAAW,YAAa;AAE/C,UAAM,aAAa,MAAM,IAAI,MAAM,GAAG,IAAI,CAAC;AAC3C,QAAI,WAAW,WAAW,GAAG;AAC3B,gBAAU,IAAI;AACd;AAAA,IACF;AAEA,QAAI,QAAQ;AAEZ,UAAM,gBAAgB,MAAc;AAClC,UAAI,OAAO,WAAW,SAAU,QAAO;AACvC,YAAM,MAAM,iBAAiB,SAAS,eAAe,EAAE;AAAA,QACrD;AAAA,MACF;AACA,YAAM,SAAS,OAAO,WAAW,GAAG;AACpC,aAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAAA,IAC5C;AAEA,UAAM,UAAU,MAAM;AACpB,cAAQ;AAGR,YAAM,OAAO,cAAc,IAAI;AAI/B,YAAM,mBACJ,OAAO,cAAc,OAAO,WAAW,SAAS,KAAK,eAAe;AACtE,UAAI,kBAAkB;AACpB,kBAAU,WAAW,WAAW,SAAS,CAAC,KAAK,IAAI;AACnD;AAAA,MACF;AAEA,UAAI,UAAyB;AAC7B,iBAAW,MAAM,YAAY;AAC3B,cAAM,KAAK,SAAS,eAAe,EAAE;AACrC,YAAI,CAAC,GAAI;AACT,YAAI,GAAG,sBAAsB,EAAE,OAAO,KAAM,WAAU;AAAA,YACjD;AAAA,MACP;AAEA,gBAAU,OAAO;AAAA,IACnB;AAEA,UAAM,WAAW,MAAM;AACrB,UAAI,MAAO;AACX,cAAQ,OAAO,sBAAsB,OAAO;AAAA,IAC9C;AAEA,YAAQ;AACR,WAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;AAC7D,WAAO,iBAAiB,UAAU,QAAQ;AAE1C,WAAO,MAAM;AACX,UAAI,MAAO,QAAO,qBAAqB,KAAK;AAC5C,aAAO,oBAAoB,UAAU,QAAQ;AAC7C,aAAO,oBAAoB,UAAU,QAAQ;AAAA,IAC/C;AAAA,EACF,GAAG,CAAC,KAAK,QAAQ,OAAO,CAAC;AAEzB,SAAO;AACT;","names":[]}
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ // src/hooks/useCopyToClipboard.ts
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ function useCopyToClipboard(resetAfter = 2e3) {
6
+ const [copied, setCopied] = useState(false);
7
+ const [failed, setFailed] = useState(false);
8
+ const timer = useRef(void 0);
9
+ useEffect(() => {
10
+ return () => {
11
+ if (timer.current) clearTimeout(timer.current);
12
+ };
13
+ }, []);
14
+ const copy = useCallback(
15
+ async (value) => {
16
+ const ok = await writeToClipboard(value);
17
+ setCopied(ok);
18
+ setFailed(!ok);
19
+ if (timer.current) clearTimeout(timer.current);
20
+ timer.current = setTimeout(() => {
21
+ setCopied(false);
22
+ setFailed(false);
23
+ }, resetAfter);
24
+ return ok;
25
+ },
26
+ [resetAfter]
27
+ );
28
+ return { copied, failed, copy };
29
+ }
30
+ async function writeToClipboard(value) {
31
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
32
+ try {
33
+ await navigator.clipboard.writeText(value);
34
+ return true;
35
+ } catch {
36
+ }
37
+ }
38
+ if (typeof document === "undefined") return false;
39
+ try {
40
+ const el = document.createElement("textarea");
41
+ el.value = value;
42
+ el.setAttribute("readonly", "");
43
+ el.style.position = "fixed";
44
+ el.style.top = "-9999px";
45
+ el.style.opacity = "0";
46
+ document.body.appendChild(el);
47
+ el.select();
48
+ const ok = document.execCommand("copy");
49
+ document.body.removeChild(el);
50
+ return ok;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+ export {
56
+ useCopyToClipboard
57
+ };
58
+ //# sourceMappingURL=useCopyToClipboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useCopyToClipboard.ts"],"sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nexport interface UseCopyToClipboardResult {\n /** True for `resetAfter` ms following a successful copy. */\n copied: boolean;\n /** True if the last attempt threw — permissions, insecure context, no API. */\n failed: boolean;\n copy: (value: string) => Promise<boolean>;\n}\n\n/**\n * Clipboard write with transient \"copied\" feedback.\n *\n * Falls back to a hidden `<textarea>` + `document.execCommand('copy')` when the\n * async Clipboard API is unavailable. That matters more than it looks: the API\n * requires a secure context, so a docs site opened over plain `http://` on a\n * LAN address — exactly how you'd preview a static build — has no\n * `navigator.clipboard` at all, and the copy-link affordance would silently do\n * nothing.\n */\nexport function useCopyToClipboard(resetAfter = 2000): UseCopyToClipboardResult {\n const [copied, setCopied] = useState(false);\n const [failed, setFailed] = useState(false);\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n useEffect(() => {\n return () => {\n if (timer.current) clearTimeout(timer.current);\n };\n }, []);\n\n const copy = useCallback(\n async (value: string): Promise<boolean> => {\n const ok = await writeToClipboard(value);\n\n setCopied(ok);\n setFailed(!ok);\n\n if (timer.current) clearTimeout(timer.current);\n timer.current = setTimeout(() => {\n setCopied(false);\n setFailed(false);\n }, resetAfter);\n\n return ok;\n },\n [resetAfter],\n );\n\n return { copied, failed, copy };\n}\n\nasync function writeToClipboard(value: string): Promise<boolean> {\n if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {\n try {\n await navigator.clipboard.writeText(value);\n return true;\n } catch {\n // Fall through to the execCommand path.\n }\n }\n\n if (typeof document === 'undefined') return false;\n\n try {\n const el = document.createElement('textarea');\n el.value = value;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.top = '-9999px';\n el.style.opacity = '0';\n document.body.appendChild(el);\n el.select();\n const ok = document.execCommand('copy');\n document.body.removeChild(el);\n return ok;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;AAEA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AAoBlD,SAAS,mBAAmB,aAAa,KAAgC;AAC9E,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,QAAQ,OAAkD,MAAS;AAEzE,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,MAAM,QAAS,cAAa,MAAM,OAAO;AAAA,IAC/C;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,OAAO;AAAA,IACX,OAAO,UAAoC;AACzC,YAAM,KAAK,MAAM,iBAAiB,KAAK;AAEvC,gBAAU,EAAE;AACZ,gBAAU,CAAC,EAAE;AAEb,UAAI,MAAM,QAAS,cAAa,MAAM,OAAO;AAC7C,YAAM,UAAU,WAAW,MAAM;AAC/B,kBAAU,KAAK;AACf,kBAAU,KAAK;AAAA,MACjB,GAAG,UAAU;AAEb,aAAO;AAAA,IACT;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,SAAO,EAAE,QAAQ,QAAQ,KAAK;AAChC;AAEA,eAAe,iBAAiB,OAAiC;AAC/D,MAAI,OAAO,cAAc,eAAe,UAAU,WAAW,WAAW;AACtE,QAAI;AACF,YAAM,UAAU,UAAU,UAAU,KAAK;AACzC,aAAO;AAAA,IACT,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,OAAO,aAAa,YAAa,QAAO;AAE5C,MAAI;AACF,UAAM,KAAK,SAAS,cAAc,UAAU;AAC5C,OAAG,QAAQ;AACX,OAAG,aAAa,YAAY,EAAE;AAC9B,OAAG,MAAM,WAAW;AACpB,OAAG,MAAM,MAAM;AACf,OAAG,MAAM,UAAU;AACnB,aAAS,KAAK,YAAY,EAAE;AAC5B,OAAG,OAAO;AACV,UAAM,KAAK,SAAS,YAAY,MAAM;AACtC,aAAS,KAAK,YAAY,EAAE;AAC5B,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}