@baybreezy/docd 0.0.1

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 (268) hide show
  1. package/app/app.config.ts +239 -0
  2. package/app/app.vue +19 -0
  3. package/app/assets/css/tailwind.css +335 -0
  4. package/app/assets/css/theme.css +1171 -0
  5. package/app/components/OgImage/Docs.takumi.vue +69 -0
  6. package/app/components/OgImage/Landing.takumi.vue +64 -0
  7. package/app/components/OgImage/Site.takumi.vue +32 -0
  8. package/app/components/Ui/Avatar/Avatar.vue +75 -0
  9. package/app/components/Ui/Avatar/Badge.vue +34 -0
  10. package/app/components/Ui/Avatar/Fallback.vue +30 -0
  11. package/app/components/Ui/Avatar/Group.vue +29 -0
  12. package/app/components/Ui/Avatar/GroupCount.vue +29 -0
  13. package/app/components/Ui/Avatar/Image.vue +29 -0
  14. package/app/components/Ui/Badge.vue +86 -0
  15. package/app/components/Ui/BadgeGroup.vue +314 -0
  16. package/app/components/Ui/BgPattern/Grid.vue +137 -0
  17. package/app/components/Ui/BorderBeam.vue +97 -0
  18. package/app/components/Ui/Breadcrumbs.vue +103 -0
  19. package/app/components/Ui/Button.vue +169 -0
  20. package/app/components/Ui/ButtonGroup/ButtonGroup.vue +54 -0
  21. package/app/components/Ui/ButtonGroup/Separator.vue +38 -0
  22. package/app/components/Ui/ButtonGroup/Text.vue +33 -0
  23. package/app/components/Ui/Card/Action.vue +31 -0
  24. package/app/components/Ui/Card/Card.vue +58 -0
  25. package/app/components/Ui/Card/Content.vue +34 -0
  26. package/app/components/Ui/Card/Description.vue +36 -0
  27. package/app/components/Ui/Card/Footer.vue +31 -0
  28. package/app/components/Ui/Card/Header.vue +31 -0
  29. package/app/components/Ui/Card/Title.vue +37 -0
  30. package/app/components/Ui/Collapsible/Collapsible.vue +16 -0
  31. package/app/components/Ui/Collapsible/Content.vue +27 -0
  32. package/app/components/Ui/Collapsible/Trigger.vue +13 -0
  33. package/app/components/Ui/Command/Command.vue +115 -0
  34. package/app/components/Ui/Command/Dialog.vue +65 -0
  35. package/app/components/Ui/Command/Empty.vue +32 -0
  36. package/app/components/Ui/Command/Footer.vue +40 -0
  37. package/app/components/Ui/Command/Group.vue +58 -0
  38. package/app/components/Ui/Command/Input.vue +57 -0
  39. package/app/components/Ui/Command/Item.vue +102 -0
  40. package/app/components/Ui/Command/List.vue +33 -0
  41. package/app/components/Ui/Command/Separator.vue +30 -0
  42. package/app/components/Ui/Command/Shortcut.vue +28 -0
  43. package/app/components/Ui/Container.vue +46 -0
  44. package/app/components/Ui/ContextMenu/Arrow.vue +43 -0
  45. package/app/components/Ui/ContextMenu/CheckboxItem.vue +42 -0
  46. package/app/components/Ui/ContextMenu/Content.vue +53 -0
  47. package/app/components/Ui/ContextMenu/ContextMenu.vue +14 -0
  48. package/app/components/Ui/ContextMenu/Group.vue +12 -0
  49. package/app/components/Ui/ContextMenu/Item.vue +65 -0
  50. package/app/components/Ui/ContextMenu/ItemIndicator.vue +30 -0
  51. package/app/components/Ui/ContextMenu/Label.vue +35 -0
  52. package/app/components/Ui/ContextMenu/Portal.vue +12 -0
  53. package/app/components/Ui/ContextMenu/RadioGroup.vue +16 -0
  54. package/app/components/Ui/ContextMenu/RadioItem.vue +40 -0
  55. package/app/components/Ui/ContextMenu/Separator.vue +25 -0
  56. package/app/components/Ui/ContextMenu/Shortcut.vue +30 -0
  57. package/app/components/Ui/ContextMenu/Sub.vue +15 -0
  58. package/app/components/Ui/ContextMenu/SubContent.vue +55 -0
  59. package/app/components/Ui/ContextMenu/SubTrigger.vue +42 -0
  60. package/app/components/Ui/ContextMenu/Trigger.vue +12 -0
  61. package/app/components/Ui/Dialog/Close.vue +12 -0
  62. package/app/components/Ui/Dialog/Content.vue +79 -0
  63. package/app/components/Ui/Dialog/Description.vue +29 -0
  64. package/app/components/Ui/Dialog/Dialog.vue +14 -0
  65. package/app/components/Ui/Dialog/Footer.vue +32 -0
  66. package/app/components/Ui/Dialog/Header.vue +32 -0
  67. package/app/components/Ui/Dialog/Overlay.vue +25 -0
  68. package/app/components/Ui/Dialog/Portal.vue +12 -0
  69. package/app/components/Ui/Dialog/Title.vue +29 -0
  70. package/app/components/Ui/Dialog/Trigger.vue +12 -0
  71. package/app/components/Ui/Divider.vue +102 -0
  72. package/app/components/Ui/Drawer/Close.vue +12 -0
  73. package/app/components/Ui/Drawer/Content.vue +67 -0
  74. package/app/components/Ui/Drawer/Description.vue +33 -0
  75. package/app/components/Ui/Drawer/Drawer.vue +17 -0
  76. package/app/components/Ui/Drawer/Footer.vue +35 -0
  77. package/app/components/Ui/Drawer/Header.vue +34 -0
  78. package/app/components/Ui/Drawer/Overlay.vue +20 -0
  79. package/app/components/Ui/Drawer/Portal.vue +12 -0
  80. package/app/components/Ui/Drawer/Title.vue +35 -0
  81. package/app/components/Ui/Drawer/Trigger.vue +12 -0
  82. package/app/components/Ui/DropdownMenu/Arrow.vue +43 -0
  83. package/app/components/Ui/DropdownMenu/CheckboxItem.vue +41 -0
  84. package/app/components/Ui/DropdownMenu/Content.vue +56 -0
  85. package/app/components/Ui/DropdownMenu/DropdownMenu.vue +15 -0
  86. package/app/components/Ui/DropdownMenu/Group.vue +12 -0
  87. package/app/components/Ui/DropdownMenu/Item.vue +64 -0
  88. package/app/components/Ui/DropdownMenu/ItemIndicator.vue +32 -0
  89. package/app/components/Ui/DropdownMenu/Label.vue +34 -0
  90. package/app/components/Ui/DropdownMenu/Portal.vue +12 -0
  91. package/app/components/Ui/DropdownMenu/RadioGroup.vue +16 -0
  92. package/app/components/Ui/DropdownMenu/RadioItem.vue +40 -0
  93. package/app/components/Ui/DropdownMenu/Separator.vue +25 -0
  94. package/app/components/Ui/DropdownMenu/Shortcut.vue +44 -0
  95. package/app/components/Ui/DropdownMenu/Sub.vue +14 -0
  96. package/app/components/Ui/DropdownMenu/SubContent.vue +56 -0
  97. package/app/components/Ui/DropdownMenu/SubTrigger.vue +47 -0
  98. package/app/components/Ui/DropdownMenu/Trigger.vue +12 -0
  99. package/app/components/Ui/FancyIcon.vue +243 -0
  100. package/app/components/Ui/GradientDivider.vue +26 -0
  101. package/app/components/Ui/IframeLazy.vue +97 -0
  102. package/app/components/Ui/Input.vue +97 -0
  103. package/app/components/Ui/InputGroup/Addon.vue +66 -0
  104. package/app/components/Ui/InputGroup/Button.vue +50 -0
  105. package/app/components/Ui/InputGroup/Input.vue +25 -0
  106. package/app/components/Ui/InputGroup/InputGroup.vue +44 -0
  107. package/app/components/Ui/InputGroup/Text.vue +33 -0
  108. package/app/components/Ui/InputGroup/Textarea.vue +25 -0
  109. package/app/components/Ui/Kbd/Group.vue +32 -0
  110. package/app/components/Ui/Kbd/Kbd.vue +62 -0
  111. package/app/components/Ui/Label.vue +40 -0
  112. package/app/components/Ui/Popover/Anchor.vue +13 -0
  113. package/app/components/Ui/Popover/Arrow.vue +42 -0
  114. package/app/components/Ui/Popover/Close.vue +13 -0
  115. package/app/components/Ui/Popover/Content.vue +55 -0
  116. package/app/components/Ui/Popover/Popover.vue +15 -0
  117. package/app/components/Ui/Popover/Portal.vue +13 -0
  118. package/app/components/Ui/Popover/Trigger.vue +12 -0
  119. package/app/components/Ui/Popover/X.vue +38 -0
  120. package/app/components/Ui/ScrollArea/Corner.vue +12 -0
  121. package/app/components/Ui/ScrollArea/ScrollArea.vue +38 -0
  122. package/app/components/Ui/ScrollArea/Scrollbar.vue +39 -0
  123. package/app/components/Ui/ScrollArea/Thumb.vue +39 -0
  124. package/app/components/Ui/ScrollArea/Viewport.vue +27 -0
  125. package/app/components/Ui/Scrollspy.vue +61 -0
  126. package/app/components/Ui/Separator.vue +40 -0
  127. package/app/components/Ui/Skeleton.vue +39 -0
  128. package/app/components/Ui/Sonner.vue +25 -0
  129. package/app/components/Ui/Tabs/Content.vue +27 -0
  130. package/app/components/Ui/Tabs/Indicator.vue +37 -0
  131. package/app/components/Ui/Tabs/List.vue +37 -0
  132. package/app/components/Ui/Tabs/Tabs.vue +25 -0
  133. package/app/components/Ui/Tabs/Trigger.vue +41 -0
  134. package/app/components/Ui/Textarea.vue +81 -0
  135. package/app/components/Ui/Tooltip/Arrow.vue +30 -0
  136. package/app/components/Ui/Tooltip/Content.vue +50 -0
  137. package/app/components/Ui/Tooltip/Portal.vue +12 -0
  138. package/app/components/Ui/Tooltip/Provider.vue +14 -0
  139. package/app/components/Ui/Tooltip/Tooltip.vue +25 -0
  140. package/app/components/Ui/Tooltip/Trigger.vue +12 -0
  141. package/app/components/Ui/Tree/Item.vue +15 -0
  142. package/app/components/Ui/Tree/Tree.vue +15 -0
  143. package/app/components/Ui/Tree/Virtualizer.vue +13 -0
  144. package/app/components/component-api/ComponentApiSection.vue +135 -0
  145. package/app/components/content/docs/Callout/DocsCallout.vue +99 -0
  146. package/app/components/content/prose/ProseA.global.vue +31 -0
  147. package/app/components/content/prose/ProseBlockquote.global.vue +28 -0
  148. package/app/components/content/prose/ProseCallout.global.vue +306 -0
  149. package/app/components/content/prose/ProseCard.global.vue +100 -0
  150. package/app/components/content/prose/ProseCodeCollapse.global.vue +125 -0
  151. package/app/components/content/prose/ProseCodeCopy.global.vue +67 -0
  152. package/app/components/content/prose/ProseCodeGroup.global.vue +104 -0
  153. package/app/components/content/prose/ProseCodeTree.global.vue +277 -0
  154. package/app/components/content/prose/ProseCollapsible.global.vue +64 -0
  155. package/app/components/content/prose/ProseColorModeImage.global.vue +58 -0
  156. package/app/components/content/prose/ProseComponentApi.global.vue +104 -0
  157. package/app/components/content/prose/ProseComponentEvents.global.vue +27 -0
  158. package/app/components/content/prose/ProseComponentExposed.global.vue +27 -0
  159. package/app/components/content/prose/ProseComponentProps.global.vue +27 -0
  160. package/app/components/content/prose/ProseComponentSlots.global.vue +27 -0
  161. package/app/components/content/prose/ProseEm.global.vue +5 -0
  162. package/app/components/content/prose/ProseField.global.vue +118 -0
  163. package/app/components/content/prose/ProseFieldGroup.global.vue +62 -0
  164. package/app/components/content/prose/ProseH1.global.vue +58 -0
  165. package/app/components/content/prose/ProseH2.global.vue +58 -0
  166. package/app/components/content/prose/ProseH3.global.vue +58 -0
  167. package/app/components/content/prose/ProseH4.global.vue +58 -0
  168. package/app/components/content/prose/ProseH5.global.vue +47 -0
  169. package/app/components/content/prose/ProseH6.global.vue +47 -0
  170. package/app/components/content/prose/ProseHr.global.vue +33 -0
  171. package/app/components/content/prose/ProseIconList.global.vue +113 -0
  172. package/app/components/content/prose/ProseImg.global.vue +119 -0
  173. package/app/components/content/prose/ProseLead.global.vue +28 -0
  174. package/app/components/content/prose/ProseLi.global.vue +25 -0
  175. package/app/components/content/prose/ProseMermaid.global.vue +127 -0
  176. package/app/components/content/prose/ProseOl.global.vue +25 -0
  177. package/app/components/content/prose/ProseP.global.vue +25 -0
  178. package/app/components/content/prose/ProsePmInstall.global.vue +60 -0
  179. package/app/components/content/prose/ProsePmRun.global.vue +51 -0
  180. package/app/components/content/prose/ProsePmX.global.vue +56 -0
  181. package/app/components/content/prose/ProsePre.global.vue +181 -0
  182. package/app/components/content/prose/ProseShowCase.global.vue +56 -0
  183. package/app/components/content/prose/ProseSmartIcon.global.vue +54 -0
  184. package/app/components/content/prose/ProseStep.global.vue +22 -0
  185. package/app/components/content/prose/ProseSteps.global.vue +22 -0
  186. package/app/components/content/prose/ProseStrong.global.vue +34 -0
  187. package/app/components/content/prose/ProseTable.global.vue +34 -0
  188. package/app/components/content/prose/ProseTabs.global.vue +104 -0
  189. package/app/components/content/prose/ProseTabsInner.global.vue +274 -0
  190. package/app/components/content/prose/ProseTbody.global.vue +5 -0
  191. package/app/components/content/prose/ProseTd.global.vue +25 -0
  192. package/app/components/content/prose/ProseTh.global.vue +25 -0
  193. package/app/components/content/prose/ProseThead.global.vue +9 -0
  194. package/app/components/content/prose/ProseTr.global.vue +35 -0
  195. package/app/components/content/prose/ProseUl.global.vue +25 -0
  196. package/app/components/docs/DocsExtraStuff.vue +39 -0
  197. package/app/components/docs/DocsGithubLink.vue +11 -0
  198. package/app/components/docs/DocsHeader.vue +3 -0
  199. package/app/components/docs/DocsLogo.vue +74 -0
  200. package/app/components/docs/DocsMobileBar.vue +26 -0
  201. package/app/components/docs/DocsMobileNav.vue +49 -0
  202. package/app/components/docs/DocsNav.vue +161 -0
  203. package/app/components/docs/DocsPageTransition.vue +32 -0
  204. package/app/components/docs/DocsSearch.vue +220 -0
  205. package/app/components/docs/DocsSearchButton.vue +20 -0
  206. package/app/components/docs/DocsThemeCustomizer.vue +229 -0
  207. package/app/components/docs/DocsThemeToggler.vue +98 -0
  208. package/app/components/docs/DocsTocRail.vue +162 -0
  209. package/app/components/docs/DocsToclink.vue +52 -0
  210. package/app/components/docs/DocsZigZagRail.vue +70 -0
  211. package/app/components/landing/Hero.vue +222 -0
  212. package/app/components/page/PageFooter.vue +49 -0
  213. package/app/components/page/PageHeader.vue +68 -0
  214. package/app/components/page/PageHeaderLinks.vue +138 -0
  215. package/app/components/page/PageMobileToc.vue +162 -0
  216. package/app/components/page/PageToc.vue +33 -0
  217. package/app/composables/useDefaultSlotItems.ts +90 -0
  218. package/app/composables/useDocPage.ts +66 -0
  219. package/app/composables/useDocSearch.ts +23 -0
  220. package/app/composables/useDocd.ts +107 -0
  221. package/app/composables/useLogoAssets.ts +319 -0
  222. package/app/composables/usePm.ts +73 -0
  223. package/app/composables/useProseComponentMeta.ts +30 -0
  224. package/app/composables/useScrollspy.ts +327 -0
  225. package/app/composables/useSeo.ts +240 -0
  226. package/app/composables/useUIConfig.ts +36 -0
  227. package/app/layouts/default.vue +5 -0
  228. package/app/layouts/docs.vue +64 -0
  229. package/app/middleware/colorMode.global.ts +8 -0
  230. package/app/middleware/doc-page-layout.ts +11 -0
  231. package/app/middleware/landing-page-layout.ts +11 -0
  232. package/app/pages/[...slug].vue +63 -0
  233. package/app/plugins/mermaid.client.ts +35 -0
  234. package/app/plugins/navigation-direction.client.ts +15 -0
  235. package/app/plugins/theme-restore.client.ts +7 -0
  236. package/app/templates/landing.vue +40 -0
  237. package/app/transitions/presets.ts +71 -0
  238. package/app/types/prose-component-meta.d.ts +4 -0
  239. package/app/utils/og.ts +26 -0
  240. package/app/utils/prerender.ts +9 -0
  241. package/app/utils/translucent.ts +56 -0
  242. package/app/utils/tw-helper.ts +4 -0
  243. package/content.config.ts +81 -0
  244. package/index.d.ts +25 -0
  245. package/modules/config.ts +89 -0
  246. package/modules/custom-icons.ts +17 -0
  247. package/modules/markdown-rewrite.ts +96 -0
  248. package/modules/prose-component-meta.ts +150 -0
  249. package/modules/routing.ts +22 -0
  250. package/modules/skills/index.ts +167 -0
  251. package/modules/skills/runtime/server/routes/skills-files.ts +49 -0
  252. package/modules/skills/runtime/server/routes/skills-index.ts +8 -0
  253. package/nuxt.config.ts +96 -0
  254. package/nuxt.schema.ts +288 -0
  255. package/package.json +95 -0
  256. package/server/mcp/tools/get-page.ts +66 -0
  257. package/server/mcp/tools/list-pages.ts +61 -0
  258. package/server/routes/sitemap.xml.ts +73 -0
  259. package/server/utils/content.ts +3 -0
  260. package/tailwind.config.cjs +55 -0
  261. package/utils/component-api-content.ts +467 -0
  262. package/utils/component-api-discovery.ts +165 -0
  263. package/utils/component-api-schema.ts +28 -0
  264. package/utils/component-api.ts +243 -0
  265. package/utils/generate-prose-component-meta.ts +303 -0
  266. package/utils/git.ts +123 -0
  267. package/utils/meta.ts +40 -0
  268. package/utils/pages.ts +21 -0
@@ -0,0 +1,327 @@
1
+ type TargetLike =
2
+ | HTMLElement
3
+ | Window
4
+ | Document
5
+ | null
6
+ | undefined
7
+ | MaybeRefOrGetter<HTMLElement | Window | Document | null | undefined>;
8
+
9
+ type AnchorRecord = {
10
+ anchor: HTMLElement;
11
+ section: HTMLElement;
12
+ sectionId: string;
13
+ offset: number;
14
+ };
15
+
16
+ export interface UseScrollspyOptions {
17
+ /** Container that actually scrolls (ref or element). Omit to use `window`. */
18
+ target?: TargetLike;
19
+ /** Element containing the ANCHORS (ref or element). If omitted, use the returned `root` ref. */
20
+ root?: HTMLElement | null | Ref<HTMLElement | null | undefined>;
21
+ /** Base data-* name -> anchors should have `data-[name]-anchor="sectionId"`. */
22
+ dataAttribute?: string;
23
+ /** Default offset from top (px) when computing active section / scrolling. */
24
+ offset?: number;
25
+ /** Smooth scroll on click. */
26
+ smooth?: boolean;
27
+ /** Update URL hash as active section changes. */
28
+ history?: boolean;
29
+ /** Throttle time (ms) for scroll handler. */
30
+ throttleTime?: number;
31
+ /** Whether to mark only the current section or every visible section as active. */
32
+ mode?: "single" | "multiple";
33
+ /** Optional callback when active id changes (you can also watch `activeId`). */
34
+ onChange?: (id: string) => void;
35
+ }
36
+
37
+ export function useScrollspy(options: UseScrollspyOptions = {}) {
38
+ const {
39
+ target,
40
+ root: rootOpt,
41
+ dataAttribute = "scrollspy",
42
+ offset = 0,
43
+ smooth = true,
44
+ history = true,
45
+ throttleTime = 50,
46
+ mode = "single",
47
+ onChange,
48
+ } = options;
49
+
50
+ // Anchors live inside this container; use returned `root` if none passed
51
+ const root = (rootOpt ?? ref<HTMLElement | null>(null)) as Ref<HTMLElement | null>;
52
+ const anchors = shallowRef<HTMLElement[]>([]);
53
+ const activeId = ref<string | null>(null);
54
+ const activeIds = ref<string[]>([]);
55
+ const prevId = ref<string | null>(null);
56
+
57
+ // --- Resolve the scroller (window by default) ---
58
+ const scroller = shallowRef<Window | HTMLElement | null>(null);
59
+
60
+ function sectionTopWithinScroller(sectionEl: HTMLElement): number {
61
+ const se = scroller.value;
62
+ const rect = sectionEl.getBoundingClientRect();
63
+ if (!se || se === window) {
64
+ return rect.top + (window.scrollY || document.documentElement.scrollTop);
65
+ }
66
+
67
+ const scRect = se.getBoundingClientRect();
68
+ return rect.top - scRect.top + se.scrollTop;
69
+ }
70
+
71
+ function resolveScroller() {
72
+ const t = unrefElement(target as any);
73
+ if (!t || t === window || t === document) {
74
+ scroller.value = window;
75
+ } else if (t instanceof HTMLElement) {
76
+ scroller.value = t;
77
+ } else {
78
+ scroller.value = window;
79
+ }
80
+ }
81
+
82
+ onMounted(resolveScroller);
83
+ watch(() => unrefElement(target as any), resolveScroller, { immediate: true });
84
+
85
+ // --- Discover anchors inside `root` and keep updated on DOM changes ---
86
+ function queryAnchors() {
87
+ anchors.value = root.value
88
+ ? Array.from(root.value.querySelectorAll<HTMLElement>(`[data-${dataAttribute}-anchor]`))
89
+ : [];
90
+ // After any DOM change that updates anchors (e.g. route-driven re-render),
91
+ // recompute the active section without waiting for a scroll event.
92
+ void nextTick(() => handleScroll());
93
+ }
94
+ onMounted(queryAnchors);
95
+ useMutationObserver(root, queryAnchors, { childList: true, subtree: true });
96
+
97
+ // Helpers for scroll metrics
98
+ function getScrollTop(se: Window | HTMLElement | null) {
99
+ if (!se || se === window) {
100
+ return window.scrollY || document.documentElement.scrollTop;
101
+ }
102
+
103
+ return se.scrollTop;
104
+ }
105
+
106
+ function getScrollHeight(se: Window | HTMLElement | null) {
107
+ if (!se || se === window) {
108
+ return document.documentElement.scrollHeight;
109
+ }
110
+
111
+ return se.scrollHeight;
112
+ }
113
+
114
+ function getClientHeight(se: Window | HTMLElement | null) {
115
+ if (!se || se === window) {
116
+ return window.innerHeight;
117
+ }
118
+
119
+ return se.clientHeight;
120
+ }
121
+
122
+ function getViewportBounds(customOffset: number) {
123
+ const se = scroller.value;
124
+ if (!se || se === window) {
125
+ return {
126
+ top: customOffset,
127
+ bottom: window.innerHeight,
128
+ };
129
+ }
130
+
131
+ const rect = se.getBoundingClientRect();
132
+
133
+ return {
134
+ top: rect.top + customOffset,
135
+ bottom: rect.bottom,
136
+ };
137
+ }
138
+
139
+ function getAnchorRecords() {
140
+ return anchors.value.reduce<AnchorRecord[]>((records, anchor) => {
141
+ const sectionId =
142
+ anchor.getAttribute(`data-${dataAttribute}-anchor`)?.replace("#", "") || null;
143
+ if (!sectionId) return records;
144
+
145
+ const section = document.getElementById(sectionId);
146
+ if (!section) return records;
147
+
148
+ let customOffset = offset;
149
+ const dataOffset = anchor.getAttribute(`data-${dataAttribute}-offset`);
150
+ if (dataOffset) customOffset = parseInt(dataOffset, 10);
151
+
152
+ records.push({
153
+ anchor,
154
+ section,
155
+ sectionId,
156
+ offset: customOffset,
157
+ });
158
+
159
+ return records;
160
+ }, []);
161
+ }
162
+
163
+ // --- Core: compute active anchor on scroll ---
164
+ function setActive(nextActiveIds: string[], sectionId: string | null, force = false) {
165
+ const activeIdSet = new Set(nextActiveIds);
166
+
167
+ anchors.value.forEach((anchor) => {
168
+ const id = anchor.getAttribute(`data-${dataAttribute}-anchor`)?.replace("#", "") || null;
169
+ if (id && activeIdSet.has(id)) anchor.setAttribute("data-active", "true");
170
+ else anchor.removeAttribute("data-active");
171
+ });
172
+
173
+ activeIds.value = nextActiveIds;
174
+ activeId.value = sectionId;
175
+
176
+ if (sectionId && history && (force || prevId.value !== sectionId)) {
177
+ window.history.replaceState({}, "", `#${sectionId}`);
178
+ }
179
+
180
+ if (sectionId && prevId.value !== sectionId) {
181
+ onChange?.(sectionId);
182
+ prevId.value = sectionId;
183
+ }
184
+ }
185
+
186
+ function handleScroll() {
187
+ if (!anchors.value.length) {
188
+ setActive([], null);
189
+ return;
190
+ }
191
+
192
+ const se = scroller.value;
193
+ const scrollTop = getScrollTop(se);
194
+ const records = getAnchorRecords();
195
+ if (!records.length) {
196
+ setActive([], null);
197
+ return;
198
+ }
199
+
200
+ let activeIdx = 0;
201
+ let minDelta = Infinity;
202
+
203
+ records.forEach((record, idx) => {
204
+ const top = sectionTopWithinScroller(record.section) - record.offset;
205
+ const delta = Math.abs(top - scrollTop);
206
+
207
+ if (top <= scrollTop && delta < minDelta) {
208
+ minDelta = delta;
209
+ activeIdx = idx;
210
+ }
211
+ });
212
+
213
+ // At bottom → force last anchor active
214
+ const atBottom = scrollTop + getClientHeight(se) >= getScrollHeight(se) - 2;
215
+ if (atBottom) activeIdx = records.length - 1;
216
+
217
+ const currentId = records[activeIdx]?.sectionId || null;
218
+ const nextActiveIds =
219
+ mode === "multiple"
220
+ ? records
221
+ .filter((record) => {
222
+ const rect = record.section.getBoundingClientRect();
223
+ const viewport = getViewportBounds(record.offset);
224
+ return rect.bottom > viewport.top && rect.top < viewport.bottom;
225
+ })
226
+ .map((record) => record.sectionId)
227
+ : currentId
228
+ ? [currentId]
229
+ : [];
230
+
231
+ setActive(nextActiveIds, currentId);
232
+ }
233
+
234
+ const onScroll = throttleTime
235
+ ? useThrottleFn(handleScroll, throttleTime, true, false)
236
+ : handleScroll;
237
+
238
+ // Rebind when scroller changes
239
+ useEventListener(scroller, "scroll", onScroll, { passive: true });
240
+
241
+ // --- Click to scroll ---
242
+ function scrollTo(
243
+ idOrEl: string | HTMLElement,
244
+ behavior: ScrollBehavior = smooth ? "smooth" : "auto"
245
+ ) {
246
+ let anchorEl: HTMLElement | null = null;
247
+ let sectionId: string | null = null;
248
+
249
+ if (typeof idOrEl === "string") {
250
+ sectionId = idOrEl;
251
+ anchorEl =
252
+ anchors.value.find((a) => a.getAttribute(`data-${dataAttribute}-anchor`) === idOrEl) ||
253
+ anchors.value.find(
254
+ (a) => a.getAttribute(`data-${dataAttribute}-anchor`) === `#${idOrEl}`
255
+ ) ||
256
+ null;
257
+ } else {
258
+ anchorEl = idOrEl;
259
+ sectionId = anchorEl.getAttribute(`data-${dataAttribute}-anchor`)?.replace("#", "") || null;
260
+ }
261
+ if (!sectionId) return;
262
+
263
+ const sectionEl = document.getElementById(sectionId);
264
+ if (!sectionEl) return;
265
+
266
+ let customOffset = offset;
267
+ const dataOffset = anchorEl?.getAttribute?.(`data-${dataAttribute}-offset`);
268
+ if (dataOffset) customOffset = parseInt(dataOffset, 10);
269
+
270
+ const top = sectionTopWithinScroller(sectionEl) - customOffset;
271
+ const se = scroller.value ?? window;
272
+
273
+ if ("scrollTo" in se) {
274
+ (se as Window | HTMLElement).scrollTo({ top, left: 0, behavior });
275
+ } else {
276
+ window.scrollTo({ top, left: 0, behavior });
277
+ }
278
+ setActive([sectionId], sectionId, true);
279
+ }
280
+
281
+ // Bind click listeners to anchors and auto-clean when the list changes
282
+ watch(
283
+ anchors,
284
+ (list, _old, onCleanup) => {
285
+ const stops: Array<() => void> = [];
286
+ list.forEach((el) => {
287
+ stops.push(
288
+ useEventListener(el, "click", (e) => {
289
+ e.preventDefault();
290
+ scrollTo(el as HTMLElement);
291
+ })
292
+ );
293
+ });
294
+ onCleanup(() => stops.forEach((s) => s()));
295
+ },
296
+ { immediate: true }
297
+ );
298
+
299
+ // Initial sync after mount
300
+ onMounted(() => {
301
+ // If URL has a hash, scroll there
302
+ const raw = window.location.hash.replace("#", "");
303
+ if (raw) {
304
+ const safe = window.CSS && "escape" in window.CSS ? (window.CSS as any).escape(raw) : raw;
305
+ const targetAnchor = root.value?.querySelector(
306
+ `[data-${dataAttribute}-anchor="${safe}"], [data-${dataAttribute}-anchor="#${safe}"]`
307
+ ) as HTMLElement | null;
308
+ if (targetAnchor) scrollTo(targetAnchor, "auto");
309
+ }
310
+ // Compute initial active anchor after layout has settled (double-RAF is more
311
+ // reliable than a fixed timeout because it waits for two full paint frames).
312
+ requestAnimationFrame(() => requestAnimationFrame(() => handleScroll()));
313
+ });
314
+
315
+ return {
316
+ /** Container that holds the anchors (use this if you didn't pass `root`) */
317
+ root,
318
+ /** Currently active section id */
319
+ activeId,
320
+ /** Currently active section ids */
321
+ activeIds,
322
+ /** Programmatically scroll to a section id or anchor element */
323
+ scrollTo,
324
+ /** Force recompute (e.g., after dynamic layout changes) */
325
+ recompute: () => handleScroll(),
326
+ };
327
+ }
@@ -0,0 +1,240 @@
1
+ import type { ContentNavigationItem } from "@nuxt/content";
2
+ import { joinURL, withoutTrailingSlash } from "ufo";
3
+ import type { MaybeRefOrGetter } from "vue";
4
+
5
+ export interface BreadcrumbItem {
6
+ title: string;
7
+ path: string;
8
+ }
9
+
10
+ /**
11
+ * Find breadcrumb path to a page in the navigation tree
12
+ */
13
+ export function findPageBreadcrumbs(
14
+ navigation: ContentNavigationItem[] | undefined,
15
+ path: string,
16
+ currentPath: BreadcrumbItem[] = []
17
+ ): BreadcrumbItem[] | undefined {
18
+ if (!navigation) return undefined;
19
+
20
+ for (const item of navigation) {
21
+ const itemPath = [...currentPath, { title: item.title, path: item.path }];
22
+
23
+ if (item.path === path) {
24
+ return itemPath;
25
+ }
26
+
27
+ if (item.children) {
28
+ const found = findPageBreadcrumbs(item.children, path, itemPath);
29
+ if (found) return found;
30
+ }
31
+ }
32
+
33
+ return undefined;
34
+ }
35
+
36
+ export interface UseSeoOptions {
37
+ /**
38
+ * Page title
39
+ */
40
+ title: MaybeRefOrGetter<string | undefined>;
41
+ /**
42
+ * Page description
43
+ */
44
+ description: MaybeRefOrGetter<string | undefined>;
45
+ /**
46
+ * Page type for og:type (default: 'article' for docs, 'website' for landing)
47
+ */
48
+ type?: MaybeRefOrGetter<"website" | "article">;
49
+ /**
50
+ * Custom OG image URL (absolute)
51
+ */
52
+ ogImage?: MaybeRefOrGetter<string | undefined>;
53
+ /**
54
+ * Published date for article schema
55
+ */
56
+ publishedAt?: MaybeRefOrGetter<string | undefined>;
57
+ /**
58
+ * Modified date for article schema
59
+ */
60
+ modifiedAt?: MaybeRefOrGetter<string | undefined>;
61
+ /**
62
+ * Breadcrumb items for BreadcrumbList schema
63
+ */
64
+ breadcrumbs?: MaybeRefOrGetter<BreadcrumbItem[] | undefined>;
65
+ /**
66
+ * Language for the page
67
+ */
68
+ lang?: MaybeRefOrGetter<string | undefined>;
69
+ }
70
+
71
+ /**
72
+ * Composable for comprehensive SEO setup including:
73
+ * - Meta tags (title, description, og:*, twitter:*)
74
+ * - Canonical URLs
75
+ * - Hreflang tags for i18n
76
+ * - JSON-LD structured data
77
+ */
78
+ export function useSeo(options: UseSeoOptions) {
79
+ const route = useRoute();
80
+ const site = useSiteConfig();
81
+ const { logo } = useDocd();
82
+ const title = computed(() => toValue(options.title));
83
+ const description = computed(() => toValue(options.description));
84
+ const type = computed(() => toValue(options.type) || "article");
85
+ const ogImage = computed(() => toValue(options.ogImage));
86
+ const publishedAt = computed(() => toValue(options.publishedAt));
87
+ const modifiedAt = computed(() => toValue(options.modifiedAt));
88
+ const breadcrumbs = computed(() => toValue(options.breadcrumbs));
89
+ const lang = computed(() => toValue(options.lang) || site.language || "en");
90
+
91
+ // Build canonical URL
92
+ const canonicalUrl = computed(() => {
93
+ if (!site.url) return undefined;
94
+ return joinURL(site.url, route.path);
95
+ });
96
+
97
+ // Base URL for building other URLs
98
+ const baseUrl = computed(() => (site.url ? withoutTrailingSlash(site.url) : ""));
99
+
100
+ // Set meta tags
101
+ useSeoMeta({
102
+ title,
103
+ description,
104
+ ogTitle: title,
105
+ ogDescription: description,
106
+ ogType: type,
107
+ ogUrl: canonicalUrl,
108
+ twitterTitle: title,
109
+ twitterDescription: description,
110
+ twitterCard: "summary_large_image",
111
+ });
112
+
113
+ // Set canonical link
114
+ useHead({
115
+ link: computed(() => {
116
+ const links: Array<{ rel: string; href?: string; hreflang?: string }> = [];
117
+
118
+ // Canonical URL
119
+ if (canonicalUrl.value) {
120
+ links.push({
121
+ rel: "canonical",
122
+ href: canonicalUrl.value,
123
+ });
124
+ }
125
+
126
+ // Logo favicon as icon
127
+ if (logo.value?.favicon) {
128
+ links.push({
129
+ rel: "icon",
130
+ href: logo.value.favicon,
131
+ });
132
+ } else if (logo.value?.light) {
133
+ links.push({
134
+ rel: "icon",
135
+ href: logo.value.light,
136
+ });
137
+ } else if (logo.value?.dark) {
138
+ links.push({
139
+ rel: "icon",
140
+ href: logo.value.dark,
141
+ });
142
+ }
143
+
144
+ return links;
145
+ }),
146
+ });
147
+
148
+ // Custom OG image handling
149
+ if (ogImage.value) {
150
+ useSeoMeta({
151
+ ogImage: ogImage.value,
152
+ twitterImage: ogImage.value,
153
+ twitterCard: "summary_large_image",
154
+ });
155
+ }
156
+
157
+ // JSON-LD structured data
158
+ useHead({
159
+ htmlAttrs: { lang },
160
+ script: computed(() => {
161
+ const scripts: Array<{ type: string; innerHTML: string }> = [];
162
+
163
+ if (!baseUrl.value || !title.value) return scripts;
164
+
165
+ const pageUrl = joinURL(baseUrl.value, route.path);
166
+
167
+ // Article schema for documentation pages
168
+ if (type.value === "article") {
169
+ const articleSchema: Record<string, unknown> = {
170
+ "@context": "https://schema.org",
171
+ "@type": "Article",
172
+ headline: title.value,
173
+ description: description.value,
174
+ url: pageUrl,
175
+ mainEntityOfPage: {
176
+ "@type": "WebPage",
177
+ "@id": pageUrl,
178
+ },
179
+ };
180
+
181
+ if (publishedAt.value) {
182
+ articleSchema.datePublished = publishedAt.value;
183
+ }
184
+
185
+ if (modifiedAt.value) {
186
+ articleSchema.dateModified = modifiedAt.value;
187
+ }
188
+
189
+ if (site.name) {
190
+ articleSchema.publisher = {
191
+ "@type": "Organization",
192
+ name: site.name,
193
+ };
194
+ }
195
+
196
+ scripts.push({
197
+ type: "application/ld+json",
198
+ innerHTML: JSON.stringify(articleSchema),
199
+ });
200
+ }
201
+
202
+ // WebSite schema for landing pages
203
+ if (type.value === "website") {
204
+ const websiteSchema: Record<string, unknown> = {
205
+ "@context": "https://schema.org",
206
+ "@type": "WebSite",
207
+ name: site.name || title.value,
208
+ description: description.value,
209
+ url: baseUrl.value,
210
+ };
211
+
212
+ scripts.push({
213
+ type: "application/ld+json",
214
+ innerHTML: JSON.stringify(websiteSchema),
215
+ });
216
+ }
217
+
218
+ // BreadcrumbList schema for navigation
219
+ if (breadcrumbs.value && breadcrumbs.value.length > 0) {
220
+ const breadcrumbSchema = {
221
+ "@context": "https://schema.org",
222
+ "@type": "BreadcrumbList",
223
+ itemListElement: breadcrumbs.value.map((item, index) => ({
224
+ "@type": "ListItem",
225
+ position: index + 1,
226
+ name: item.title,
227
+ item: joinURL(baseUrl.value, item.path),
228
+ })),
229
+ };
230
+
231
+ scripts.push({
232
+ type: "application/ld+json",
233
+ innerHTML: JSON.stringify(breadcrumbSchema),
234
+ });
235
+ }
236
+
237
+ return scripts;
238
+ }),
239
+ });
240
+ }
@@ -0,0 +1,36 @@
1
+ import type { BorderType, DocdExtraLink, DocdTransitionConfig, DocLogoConfig } from "../app.config";
2
+
3
+ interface DocdUIMap {
4
+ header: {
5
+ title?: string;
6
+ hideSearch?: boolean;
7
+ logo?: DocLogoConfig;
8
+ };
9
+ footer?: {
10
+ hideThemeCustomizer?: boolean;
11
+ hideLightDarkToggle?: boolean;
12
+ };
13
+ toc: {
14
+ title?: string;
15
+ icon?: string;
16
+ };
17
+ borderType: BorderType;
18
+ transition: DocdTransitionConfig;
19
+ extraLinks: DocdExtraLink[];
20
+ }
21
+
22
+ export function useUIConfig<K extends keyof DocdUIMap>(section: K): ComputedRef<DocdUIMap[K]> {
23
+ const appConfig = useAppConfig();
24
+ return computed(() => {
25
+ const ui = (appConfig.docd as { ui?: Partial<DocdUIMap> } | undefined)?.ui;
26
+ return (ui?.[section] ?? defaultValues[section]) as DocdUIMap[K];
27
+ });
28
+ }
29
+
30
+ const defaultValues: DocdUIMap = {
31
+ header: {},
32
+ toc: { title: "On this page", icon: "lucide:list" },
33
+ borderType: "dashed",
34
+ transition: { name: "fade", duration: 0.35, easing: "easeOut" },
35
+ extraLinks: [],
36
+ };
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <main>
3
+ <slot />
4
+ </main>
5
+ </template>
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <div class="grid min-h-dvh w-full grid-cols-1 tablet:grid-cols-[var(--nav-width)_minmax(0,1fr)]">
3
+ <aside
4
+ class="sticky top-0 z-20 hidden h-dvh flex-col border-r text-card-foreground tablet:flex"
5
+ :class="isDashed ? 'border-dashed' : ''"
6
+ >
7
+ <div
8
+ class="sticky top-0 mb-4 border-b bg-background/80 backdrop-blur-lg"
9
+ :class="isDashed ? 'border-dashed' : ''"
10
+ >
11
+ <div class="flex h-(--header-height) items-center justify-between px-4">
12
+ <DocsLogo />
13
+ <DocsSearchButton v-if="!hideSearch" />
14
+ </div>
15
+ </div>
16
+ <UiScrollArea
17
+ class="h-[calc(100dvh-calc(var(--header-height)*2))] flex-1 mask-[linear-gradient(to_bottom,transparent,white_12px,white_calc(100%-12px),transparent)] px-4"
18
+ >
19
+ <DocsNav v-if="navigation" :items="navigation" />
20
+ </UiScrollArea>
21
+ <div
22
+ class="mt-auto flex h-(--header-height) items-center justify-between gap-3 border-t px-2"
23
+ :class="isDashed ? 'border-dashed' : ''"
24
+ >
25
+ <template v-if="github">
26
+ <DocsGithubLink />
27
+ </template>
28
+ <div class="ml-auto flex items-center gap-1.5">
29
+ <template v-if="github && (!hideThemeCustomizer || !hideLightDarkToggle)">
30
+ <UiDivider
31
+ :type="isDashed ? 'dashed' : 'solid'"
32
+ orientation="vertical"
33
+ class="h-[calc(var(--header-height)-2rem)]"
34
+ />
35
+ </template>
36
+ <DocsThemeCustomizer v-if="!hideThemeCustomizer" />
37
+ <DocsThemeToggler v-if="!hideLightDarkToggle" class="dark:text-muted-foreground" />
38
+ </div>
39
+ </div>
40
+ </aside>
41
+ <main class="flex min-h-dvh flex-col">
42
+ <header
43
+ class="sticky top-0 z-50 shrink-0 bg-background/80 backdrop-blur-lg"
44
+ :class="isDashed ? 'border-dashed' : ''"
45
+ >
46
+ <DocsMobileBar />
47
+ <div class="border-b" :class="isDashed ? 'border-dashed' : ''">
48
+ <UiContainer class="flex h-(--header-height) items-center">
49
+ <DocsHeader />
50
+ </UiContainer>
51
+ </div>
52
+ </header>
53
+ <PageMobileToc />
54
+ <div class="grow">
55
+ <slot />
56
+ </div>
57
+ </main>
58
+ </div>
59
+ </template>
60
+
61
+ <script lang="ts" setup>
62
+ const { navigation } = await useDocPage();
63
+ const { isDashed, github, hideSearch, hideLightDarkToggle, hideThemeCustomizer } = useDocd();
64
+ </script>
@@ -0,0 +1,8 @@
1
+ export default defineNuxtRouteMiddleware((to) => {
2
+ const appConfig = useAppConfig();
3
+ const forced = appConfig.docd.ui?.colorMode;
4
+
5
+ if (forced === "light" || forced === "dark") {
6
+ to.meta.colorMode = forced;
7
+ }
8
+ });
@@ -0,0 +1,11 @@
1
+ interface DocPageLayout {
2
+ layout?: string;
3
+ }
4
+
5
+ export default defineNuxtRouteMiddleware(async (to) => {
6
+ const page = (await queryCollection("docs" as never)
7
+ .path(to.path)
8
+ .first()) as DocPageLayout | null;
9
+
10
+ setPageLayout(page?.layout || "docs");
11
+ });
@@ -0,0 +1,11 @@
1
+ interface LandingPageLayout {
2
+ layout?: string;
3
+ }
4
+
5
+ export default defineNuxtRouteMiddleware(async (to) => {
6
+ const page = (await queryCollection("landing" as never)
7
+ .path(to.path)
8
+ .first()) as LandingPageLayout | null;
9
+
10
+ setPageLayout(page?.layout || "default");
11
+ });