@lupinum/ginko-docs 0.2.2 → 0.2.3

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 (196) hide show
  1. package/README.md +130 -73
  2. package/app/app.config.ts +14 -12
  3. package/app/app.vue +2 -2
  4. package/app/assets/css/prose.css +2203 -149
  5. package/app/assets/css/tailwind.css +60 -38
  6. package/app/assets/css/theme-palettes.css +309 -0
  7. package/app/components/content/DocumentPageShell.vue +1 -1
  8. package/app/components/content/Feedback.vue +12 -11
  9. package/app/components/content/ImageZoomDialog.vue +84 -0
  10. package/app/components/content/PageMarkdownCopy.vue +2 -0
  11. package/app/components/content/imageZoom.ts +25 -0
  12. package/app/components/mdc/MdcAccordion.vue +6 -3
  13. package/app/components/mdc/MdcAccordionItem.vue +7 -16
  14. package/app/components/mdc/MdcApi.vue +159 -0
  15. package/app/components/mdc/MdcAside.vue +6 -10
  16. package/app/components/mdc/MdcCallout.vue +9 -10
  17. package/app/components/mdc/MdcCard.vue +36 -47
  18. package/app/components/mdc/MdcCards.vue +30 -0
  19. package/app/components/mdc/MdcCodeGroup.vue +4 -0
  20. package/app/components/mdc/MdcCodeTree.vue +234 -0
  21. package/app/components/mdc/MdcCollapse.vue +61 -0
  22. package/app/components/mdc/MdcError.vue +8 -1
  23. package/app/components/mdc/MdcExcerpt.vue +26 -0
  24. package/app/components/mdc/MdcFigure.vue +54 -20
  25. package/app/components/mdc/MdcFiles.vue +120 -0
  26. package/app/components/mdc/MdcIdea.vue +2 -1
  27. package/app/components/mdc/MdcInfo.vue +2 -1
  28. package/app/components/mdc/MdcInlineToc.vue +51 -0
  29. package/app/components/{ui/dialog/DialogHeader.vue → mdc/MdcKbd.vue} +2 -4
  30. package/app/components/mdc/MdcNote.vue +2 -1
  31. package/app/components/mdc/MdcQuiz.vue +43 -83
  32. package/app/components/mdc/MdcQuizQuestion.vue +104 -109
  33. package/app/components/mdc/MdcReadMore.vue +57 -77
  34. package/app/components/mdc/MdcSteps.vue +45 -11
  35. package/app/components/mdc/MdcSuccess.vue +8 -1
  36. package/app/components/mdc/MdcTabs.vue +44 -26
  37. package/app/components/mdc/MdcTimeline.vue +4 -1
  38. package/app/components/mdc/MdcTimelineItem.vue +18 -27
  39. package/app/components/mdc/MdcWarning.vue +8 -1
  40. package/app/components/mdc/api.utils.ts +132 -0
  41. package/app/components/mdc/code-tree.utils.ts +22 -0
  42. package/app/components/mdc/quiz.utils.ts +38 -0
  43. package/app/components/prose/ProseBlockquote.vue +17 -1
  44. package/app/components/prose/ProseH1.vue +1 -1
  45. package/app/components/prose/ProseH2.vue +7 -2
  46. package/app/components/prose/ProseH3.vue +7 -2
  47. package/app/components/prose/ProseH4.vue +17 -2
  48. package/app/components/{ui/kbd/KbdGroup.vue → prose/ProseH5.vue} +4 -3
  49. package/app/components/{ui/card/CardTitle.vue → prose/ProseH6.vue} +4 -3
  50. package/app/components/prose/ProseImg.vue +19 -112
  51. package/app/components/prose/ProsePre.vue +63 -100
  52. package/app/components/prose/ProseTable.vue +5 -3
  53. package/app/components/site/ModeToggle.vue +12 -3
  54. package/app/components/site/SiteBanner.vue +43 -2
  55. package/app/components/site/SiteFooter.vue +3 -5
  56. package/app/components/site/SiteHeader.vue +255 -132
  57. package/app/components/site/SiteHeroCode.vue +164 -0
  58. package/app/components/site/SiteLocaleSwitcher.vue +22 -4
  59. package/app/components/ui/badge/Badge.vue +1 -1
  60. package/app/components/ui/button/Button.vue +1 -1
  61. package/app/components/ui/collapsible/CollapsibleContent.vue +1 -1
  62. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +14 -4
  63. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  64. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +1 -1
  65. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +1 -1
  66. package/app/components/ui/dropdown-menu/index.ts +0 -9
  67. package/app/components/ui/kbd/Kbd.vue +1 -1
  68. package/app/components/ui/kbd/index.ts +0 -1
  69. package/app/components/ui/scroll-area/ScrollArea.vue +1 -1
  70. package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
  71. package/app/components/ui/separator/Separator.vue +1 -1
  72. package/app/components/ui/sheet/SheetContent.vue +1 -1
  73. package/app/components/ui/sheet/SheetDescription.vue +1 -1
  74. package/app/components/ui/sheet/SheetOverlay.vue +1 -1
  75. package/app/components/ui/sheet/SheetTitle.vue +1 -1
  76. package/app/components/ui/sheet/index.ts +0 -3
  77. package/app/components/ui/tabs/Tabs.vue +1 -1
  78. package/app/components/ui/tabs/TabsList.vue +1 -1
  79. package/app/components/ui/tabs/TabsTrigger.vue +1 -1
  80. package/app/components/ui/tabs/index.ts +0 -1
  81. package/app/composables/site-navigation.utils.ts +29 -11
  82. package/app/composables/useContentRouteAlternates.ts +13 -21
  83. package/app/composables/useLocalizedRouteSwitch.ts +1 -1
  84. package/app/composables/useMetaKey.ts +18 -0
  85. package/app/composables/useProseAppearance.ts +24 -0
  86. package/app/composables/useSiteNavigation.ts +3 -18
  87. package/app/config/site.utils.ts +1 -1
  88. package/app/error.vue +1 -5
  89. package/app/features/docs/components/DocsBreadcrumb.vue +37 -0
  90. package/app/features/docs/components/DocsContributeLinks.vue +28 -18
  91. package/app/features/docs/components/DocsMobileToc.vue +12 -8
  92. package/app/features/docs/components/DocsPageContent.vue +84 -47
  93. package/app/features/docs/components/DocsPageNav.vue +1 -1
  94. package/app/features/docs/components/DocsSidebar.vue +30 -20
  95. package/app/features/docs/components/DocsSidebarItem.vue +53 -100
  96. package/app/features/docs/components/DocsSidebarRow.vue +27 -0
  97. package/app/features/docs/components/DocsSidebarTabs.vue +46 -22
  98. package/app/features/docs/components/DocsToc.vue +80 -34
  99. package/app/features/docs/composables/useDocsNavigation.ts +2 -2
  100. package/app/features/docs/composables/useDocsNavigationData.ts +4 -1
  101. package/app/features/docs/composables/useRevealActive.ts +58 -0
  102. package/app/features/docs/composables/useScrollspy.ts +87 -19
  103. package/app/features/docs/docs-navigation.ts +17 -30
  104. package/app/features/docs/toc-context.ts +6 -0
  105. package/app/features/search/command-center.ts +11 -90
  106. package/app/features/search/components/SiteCommandCenter.vue +58 -38
  107. package/app/features/search/useCommandCenter.ts +46 -46
  108. package/app/layouts/docs.vue +1 -1
  109. package/app/lib/errors.ts +2 -49
  110. package/app/pages/blog/[slug].vue +16 -8
  111. package/app/pages/blog/index.vue +26 -13
  112. package/app/pages/docs/index.vue +10 -14
  113. package/app/pages/index.vue +174 -59
  114. package/app/plugins/ginko-docs-theme.ts +12 -0
  115. package/app/utils/content.ts +11 -0
  116. package/app/utils/file-icons.ts +90 -0
  117. package/app/utils/index.ts +7 -1
  118. package/app/utils/repository.ts +20 -0
  119. package/content.js +23 -26
  120. package/content.ts +30 -28
  121. package/i18n/messages/global/blog.ts +4 -0
  122. package/i18n/messages/global/command.ts +6 -2
  123. package/i18n/messages/global/docs.ts +3 -0
  124. package/i18n/messages/global/nav.ts +6 -2
  125. package/i18n/messages/index.ts +3 -3
  126. package/icon-bundle.ts +5 -0
  127. package/index.d.ts +7 -1
  128. package/nuxt.config.ts +2 -2
  129. package/package.json +9 -6
  130. package/runtime/server/agent-markdown.ts +0 -2
  131. package/shared/theme-palettes.ts +37 -0
  132. package/shared/types/app-config.ts +63 -6
  133. package/tags.ts +47 -59
  134. package/app/components/mdc/MdcAlert.vue +0 -34
  135. package/app/components/mdc/MdcCardGroup.vue +0 -47
  136. package/app/components/mdc/MdcDocImg.vue +0 -25
  137. package/app/components/mdc/MdcField.vue +0 -62
  138. package/app/components/mdc/MdcFieldGroup.vue +0 -18
  139. package/app/components/mdc/MdcPassage.vue +0 -32
  140. package/app/components/mdc/MdcQuizOption.vue +0 -91
  141. package/app/components/mdc/MdcReadMoreGroup.vue +0 -18
  142. package/app/components/mdc/MdcShortcut.vue +0 -46
  143. package/app/components/mdc/MdcStep.vue +0 -42
  144. package/app/components/ui/accordion/Accordion.vue +0 -15
  145. package/app/components/ui/accordion/AccordionContent.vue +0 -23
  146. package/app/components/ui/accordion/AccordionItem.vue +0 -24
  147. package/app/components/ui/accordion/AccordionTrigger.vue +0 -35
  148. package/app/components/ui/accordion/index.ts +0 -4
  149. package/app/components/ui/alert/Alert.vue +0 -21
  150. package/app/components/ui/alert/AlertDescription.vue +0 -22
  151. package/app/components/ui/alert/AlertTitle.vue +0 -22
  152. package/app/components/ui/alert/index.ts +0 -34
  153. package/app/components/ui/card/Card.vue +0 -16
  154. package/app/components/ui/card/CardAction.vue +0 -17
  155. package/app/components/ui/card/CardContent.vue +0 -14
  156. package/app/components/ui/card/CardDescription.vue +0 -14
  157. package/app/components/ui/card/CardFooter.vue +0 -14
  158. package/app/components/ui/card/CardHeader.vue +0 -22
  159. package/app/components/ui/card/card-variants.ts +0 -15
  160. package/app/components/ui/card/index.ts +0 -8
  161. package/app/components/ui/checkbox/Checkbox.vue +0 -35
  162. package/app/components/ui/checkbox/index.ts +0 -1
  163. package/app/components/ui/dialog/Dialog.vue +0 -15
  164. package/app/components/ui/dialog/DialogClose.vue +0 -12
  165. package/app/components/ui/dialog/DialogContent.vue +0 -69
  166. package/app/components/ui/dialog/DialogDescription.vue +0 -21
  167. package/app/components/ui/dialog/DialogFooter.vue +0 -15
  168. package/app/components/ui/dialog/DialogOverlay.vue +0 -26
  169. package/app/components/ui/dialog/DialogPortal.vue +0 -12
  170. package/app/components/ui/dialog/DialogTitle.vue +0 -21
  171. package/app/components/ui/dialog/DialogTrigger.vue +0 -12
  172. package/app/components/ui/dialog/index.ts +0 -10
  173. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +0 -36
  174. package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +0 -12
  175. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +0 -25
  176. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +0 -17
  177. package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +0 -15
  178. package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +0 -29
  179. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +0 -31
  180. package/app/components/ui/input/Input.vue +0 -23
  181. package/app/components/ui/input/index.ts +0 -1
  182. package/app/components/ui/label/Label.vue +0 -14
  183. package/app/components/ui/label/index.ts +0 -1
  184. package/app/components/ui/sheet/SheetClose.vue +0 -12
  185. package/app/components/ui/sheet/SheetFooter.vue +0 -12
  186. package/app/components/ui/sheet/SheetHeader.vue +0 -12
  187. package/app/components/ui/switch/Switch.vue +0 -39
  188. package/app/components/ui/switch/index.ts +0 -1
  189. package/app/components/ui/tabs/TabsContent.vue +0 -21
  190. package/app/components/ui/textarea/Textarea.vue +0 -31
  191. package/app/components/ui/textarea/index.ts +0 -1
  192. package/app/features/docs/composables/useDocsEntryPath.ts +0 -13
  193. package/app/lib/utils.ts +0 -7
  194. package/i18n/messages/source.ts +0 -5
  195. package/runtime/markdown/inline-code-highlight.ts +0 -155
  196. package/runtime/utils.ts +0 -7
@@ -0,0 +1,234 @@
1
+ <script setup lang="ts">
2
+ import type { VNode } from "vue";
3
+ import { Comment, cloneVNode, h, ref, resolveComponent, useSlots } from "vue";
4
+ import { cn } from "../../utils";
5
+ import { resolveFileIcon } from "../../utils/file-icons";
6
+ import { useProseAppearance } from "../../composables/useProseAppearance";
7
+ import { validateCodeTreePaths } from "./code-tree.utils";
8
+
9
+ type CodeBlockProps = {
10
+ code?: string;
11
+ filename?: string | null;
12
+ language?: string | null;
13
+ meta?: string | null;
14
+ };
15
+
16
+ type TreeLeaf = {
17
+ path: string;
18
+ node: VNode;
19
+ };
20
+
21
+ type TreeNode = {
22
+ label: string;
23
+ path: string;
24
+ children?: TreeNode[];
25
+ };
26
+
27
+ const props = defineProps<{
28
+ /** Path of the file to show initially, e.g. "app/app.config.ts". */
29
+ defaultValue?: string;
30
+ expandAll?: boolean;
31
+ appearance?: "quiet" | "tint";
32
+ }>();
33
+ const appearance = useProseAppearance("code", () => props.appearance);
34
+
35
+ const slots = useSlots();
36
+
37
+ function isRenderableNode(node: VNode) {
38
+ return node.type !== Comment;
39
+ }
40
+
41
+ function cleanLabel(input: string | null | undefined): string {
42
+ return (
43
+ input
44
+ ?.trim()
45
+ .replace(/^\[(.*)]$/, "$1")
46
+ .trim() || ""
47
+ );
48
+ }
49
+
50
+ function labelFromMeta(meta: string | null | undefined): string {
51
+ const match = meta?.match(/\[([^\]]+)]/);
52
+ return cleanLabel(match?.[1]);
53
+ }
54
+
55
+ function resolveLeaves(): TreeLeaf[] {
56
+ const leaves = (slots.default?.() || []).filter(isRenderableNode).map((node) => {
57
+ const nodeProps = (node.props || {}) as CodeBlockProps;
58
+ const path = cleanLabel(nodeProps.filename) || labelFromMeta(nodeProps.meta);
59
+ if (!path) throw new TypeError("Every code tree block requires a filename label");
60
+ return { path, node };
61
+ });
62
+ validateCodeTreePaths(
63
+ leaves.map((leaf) => leaf.path),
64
+ props.defaultValue,
65
+ );
66
+ return leaves;
67
+ }
68
+
69
+ function buildTree(paths: string[]): TreeNode[] {
70
+ const map = new Map<string, TreeNode>();
71
+ const root: TreeNode[] = [];
72
+
73
+ for (const fullPath of paths) {
74
+ const parts = fullPath.split("/");
75
+ let path = "";
76
+
77
+ parts.forEach((part, i) => {
78
+ path = path ? `${path}/${part}` : part;
79
+ if (map.has(path)) return;
80
+
81
+ const node: TreeNode = { label: part, path, ...(i < parts.length - 1 && { children: [] }) };
82
+ map.set(path, node);
83
+ if (i === 0) root.push(node);
84
+ else map.get(parts.slice(0, i).join("/"))?.children?.push(node);
85
+ });
86
+ }
87
+
88
+ const sort = (nodes: TreeNode[]): TreeNode[] =>
89
+ nodes
90
+ .sort((a, b) =>
91
+ !!a.children === !!b.children ? a.label.localeCompare(b.label) : b.children ? 1 : -1,
92
+ )
93
+ .map((node) => ({ ...node, children: node.children && sort(node.children) }));
94
+
95
+ return sort(root);
96
+ }
97
+
98
+ function ancestorsOf(path: string): string[] {
99
+ const parts = path.split("/");
100
+ return parts.slice(0, -1).map((_, i) => parts.slice(0, i + 1).join("/"));
101
+ }
102
+
103
+ const selectedPath = ref(props.defaultValue ?? "");
104
+ const expanded = ref(new Set<string>());
105
+ const expansionChanged = ref(false);
106
+
107
+ function toggleDir(path: string, initial: Set<string>) {
108
+ if (!expansionChanged.value) {
109
+ expanded.value = new Set(initial);
110
+ expansionChanged.value = true;
111
+ }
112
+ if (expanded.value.has(path)) expanded.value.delete(path);
113
+ else expanded.value.add(path);
114
+ }
115
+
116
+ const rowClass = "content-code-tree-row";
117
+
118
+ function renderRows(
119
+ nodes: TreeNode[],
120
+ openPaths: Set<string>,
121
+ activePath: string,
122
+ ): (VNode | null)[] {
123
+ const Icon = resolveComponent("Icon");
124
+
125
+ return nodes.map((node) => {
126
+ if (node.children) {
127
+ const isOpen = openPaths.has(node.path);
128
+ return h("li", { key: node.path, role: "none" }, [
129
+ h(
130
+ "button",
131
+ {
132
+ type: "button",
133
+ class: rowClass,
134
+ "aria-expanded": isOpen ? "true" : "false",
135
+ onClick: () => toggleDir(node.path, openPaths),
136
+ },
137
+ [
138
+ h(Icon, {
139
+ name: "lucide:chevron-right",
140
+ class: "content-code-tree-chevron",
141
+ "data-open": isOpen ? "true" : undefined,
142
+ "aria-hidden": "true",
143
+ }),
144
+ h(Icon, {
145
+ name: isOpen ? "lucide:folder-open" : "lucide:folder",
146
+ class: "content-code-tree-icon",
147
+ "aria-hidden": "true",
148
+ }),
149
+ h("span", { class: "content-code-tree-label" }, node.label),
150
+ ],
151
+ ),
152
+ isOpen
153
+ ? h(
154
+ "ul",
155
+ { role: "group", class: "content-code-tree-branch" },
156
+ renderRows(node.children, openPaths, activePath),
157
+ )
158
+ : null,
159
+ ]);
160
+ }
161
+
162
+ const isSelected = activePath === node.path;
163
+ return h("li", { key: node.path, role: "none" }, [
164
+ h(
165
+ "button",
166
+ {
167
+ type: "button",
168
+ class: cn(rowClass, isSelected && "content-code-tree-row-active"),
169
+ "aria-current": isSelected ? "true" : undefined,
170
+ onClick: () => (selectedPath.value = node.path),
171
+ },
172
+ [
173
+ h("span", { class: "content-code-tree-spacer", "aria-hidden": "true" }),
174
+ h(resolveComponent("Icon"), {
175
+ name: resolveFileIcon(node.label),
176
+ class: "content-code-tree-icon",
177
+ "aria-hidden": "true",
178
+ }),
179
+ h("span", { class: "content-code-tree-label" }, node.label),
180
+ ],
181
+ ),
182
+ ]);
183
+ });
184
+ }
185
+
186
+ function renderCodeTree() {
187
+ const leaves = resolveLeaves();
188
+ if (!leaves.length) return null;
189
+ const activePath = leaves.some((leaf) => leaf.path === selectedPath.value)
190
+ ? selectedPath.value
191
+ : leaves[0]!.path;
192
+ const initialOpenPaths = new Set(
193
+ props.expandAll ? leaves.flatMap((leaf) => ancestorsOf(leaf.path)) : ancestorsOf(activePath),
194
+ );
195
+ const openPaths = expansionChanged.value ? expanded.value : initialOpenPaths;
196
+ const tree = buildTree(leaves.map((leaf) => leaf.path));
197
+
198
+ return h(
199
+ "div",
200
+ {
201
+ class: "content-code-tree not-prose",
202
+ "data-appearance": appearance.value,
203
+ },
204
+ [
205
+ h(
206
+ "ul",
207
+ {
208
+ class: "content-code-tree-list",
209
+ },
210
+ renderRows(tree, openPaths, activePath),
211
+ ),
212
+ h(
213
+ "div",
214
+ { class: "content-code-tree-pane" },
215
+ leaves.map((leaf) =>
216
+ h(
217
+ "div",
218
+ {
219
+ key: leaf.path,
220
+ class: "content-code-tree-panel",
221
+ style: activePath === leaf.path ? undefined : { display: "none" },
222
+ },
223
+ [cloneVNode(leaf.node)],
224
+ ),
225
+ ),
226
+ ),
227
+ ],
228
+ );
229
+ }
230
+ </script>
231
+
232
+ <template>
233
+ <component :is="renderCodeTree" />
234
+ </template>
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, provide, ref, useId } from "vue";
3
+ import { useResizeObserver } from "@vueuse/core";
4
+ import { useI18n } from "#imports";
5
+ import { useProseAppearance } from "../../composables/useProseAppearance";
6
+
7
+ const props = defineProps<{ appearance?: "quiet" | "tint" }>();
8
+ const appearance = useProseAppearance("code", () => props.appearance);
9
+ // The clipped code block must render on the surface the collapse declares.
10
+ provide("contentCodeAppearance", appearance);
11
+
12
+ // Clips tall authored content (usually a long code block) at a fixed height
13
+ // until the reader expands it. Expanding removes the cap entirely, so the page
14
+ // owns scrolling instead of a nested scroll area.
15
+ const { t } = useI18n();
16
+ const regionId = useId();
17
+ const region = ref<HTMLElement | null>(null);
18
+ const expanded = ref(false);
19
+ const overflows = ref(false);
20
+ const lineCount = ref(0);
21
+
22
+ function measure() {
23
+ const el = region.value;
24
+ if (!el) return;
25
+ overflows.value = el.scrollHeight - el.clientHeight > 2;
26
+ }
27
+
28
+ onMounted(() => {
29
+ lineCount.value = region.value?.querySelectorAll("pre .line").length ?? 0;
30
+ measure();
31
+ });
32
+ useResizeObserver(region, measure);
33
+
34
+ const label = computed(() =>
35
+ lineCount.value ? t("docs.showAllLines", { count: lineCount.value }) : t("docs.showMore"),
36
+ );
37
+ </script>
38
+
39
+ <template>
40
+ <div class="content-collapse" :data-appearance="appearance">
41
+ <div
42
+ :id="regionId"
43
+ ref="region"
44
+ class="content-collapse-region"
45
+ :data-collapsed="!expanded || undefined"
46
+ >
47
+ <slot />
48
+ </div>
49
+ <div v-if="!expanded && overflows" class="content-collapse-fade">
50
+ <button
51
+ type="button"
52
+ class="content-collapse-button"
53
+ :aria-controls="regionId"
54
+ aria-expanded="false"
55
+ @click="expanded = true"
56
+ >
57
+ {{ label }}
58
+ </button>
59
+ </div>
60
+ </div>
61
+ </template>
@@ -5,12 +5,19 @@ import MdcCallout from "./MdcCallout.vue";
5
5
  const props = defineProps<{
6
6
  title?: string;
7
7
  icon?: string;
8
+ appearance?: "quiet" | "tint";
8
9
  class?: HTMLAttributes["class"];
9
10
  }>();
10
11
  </script>
11
12
 
12
13
  <template>
13
- <MdcCallout :title="title" :icon="icon" type="error" :class="props.class">
14
+ <MdcCallout
15
+ :title="title"
16
+ :icon="icon"
17
+ type="error"
18
+ :appearance="appearance"
19
+ :class="props.class"
20
+ >
14
21
  <slot />
15
22
  </MdcCallout>
16
23
  </template>
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { useProseAppearance } from "../../composables/useProseAppearance";
4
+ import { cn } from "../../utils";
5
+
6
+ const props = defineProps<{
7
+ label?: string;
8
+ source?: string;
9
+ appearance?: "quiet" | "tint";
10
+ class?: HTMLAttributes["class"];
11
+ }>();
12
+
13
+ const appearance = useProseAppearance("excerpt", () => props.appearance);
14
+ </script>
15
+
16
+ <template>
17
+ <figure :class="cn('content-excerpt not-prose', props.class)" :data-appearance="appearance">
18
+ <p v-if="label" class="content-excerpt-label">
19
+ {{ label }}
20
+ </p>
21
+ <blockquote class="content-excerpt-body content-prose content-prose-trim">
22
+ <slot />
23
+ </blockquote>
24
+ <figcaption v-if="source" class="content-excerpt-source">{{ source }}</figcaption>
25
+ </figure>
26
+ </template>
@@ -1,20 +1,41 @@
1
1
  <script setup lang="ts">
2
2
  import type { HTMLAttributes } from "vue";
3
+ import { Motion } from "motion-v";
3
4
  import { computed } from "vue";
5
+ import { useI18n } from "#imports";
6
+ import ImageZoomDialog from "#ginko-docs/components/content/ImageZoomDialog.vue";
7
+ import { useGinkoDocsConfig } from "#ginko-docs/composables/useGinkoDocsConfig";
4
8
  import { cn } from "../../utils";
9
+ import { useProseAppearance } from "../../composables/useProseAppearance";
5
10
 
6
- const props = defineProps<{
7
- src?: string;
8
- alt?: string;
9
- caption?: string;
10
- width?: string | number;
11
- height?: string | number;
12
- bleed?: boolean | string;
13
- aspect?: "auto" | "video" | "wide" | "square" | "portrait";
14
- fit?: "cover" | "contain";
15
- class?: HTMLAttributes["class"];
16
- }>();
11
+ const props = withDefaults(
12
+ defineProps<{
13
+ src?: string;
14
+ alt?: string;
15
+ caption?: string;
16
+ width?: string | number;
17
+ height?: string | number;
18
+ bleed?: boolean | string;
19
+ aspect?: "auto" | "video" | "wide" | "square" | "portrait";
20
+ fit?: "cover" | "contain";
21
+ zoom?: boolean | string;
22
+ class?: HTMLAttributes["class"];
23
+ appearance?: "quiet" | "tint";
24
+ }>(),
25
+ {
26
+ // Vue casts an absent Boolean-typed prop to false; "auto" preserves the
27
+ // site-wide images.zoom default unless the author overrides it.
28
+ zoom: "auto",
29
+ },
30
+ );
31
+ const appearance = useProseAppearance("figure", () => props.appearance);
17
32
 
33
+ const { t } = useI18n();
34
+ const config = useGinkoDocsConfig();
35
+ const zoomEnabled = computed(() => {
36
+ if (props.zoom === "auto") return config.images?.zoom !== false;
37
+ return props.zoom === true || props.zoom === "true";
38
+ });
18
39
  const shouldBleed = computed(
19
40
  () => props.bleed === true || props.bleed === "true" || props.bleed === "outside",
20
41
  );
@@ -28,23 +49,36 @@ const aspectClass = computed(() => {
28
49
  portrait: "aspect-[4/5]",
29
50
  }[props.aspect ?? "auto"];
30
51
  });
52
+
53
+ const imageClass = computed(() =>
54
+ cn("w-full", aspectClass.value, props.fit === "contain" ? "object-contain" : "object-cover"),
55
+ );
31
56
  </script>
32
57
 
33
58
  <template>
34
59
  <figure
35
60
  :class="cn('content-media not-prose', props.class)"
36
61
  :data-bleed="shouldBleed ? 'true' : undefined"
62
+ :data-appearance="appearance"
37
63
  >
38
- <img
39
- v-if="src"
40
- :src="src"
41
- :alt="alt"
42
- :width="width"
43
- :height="height"
44
- :class="cn('w-full', aspectClass, fit === 'contain' ? 'object-contain' : 'object-cover')"
45
- />
64
+ <template v-if="src">
65
+ <ImageZoomDialog v-if="zoomEnabled" :src="src" :alt="alt" :label="caption">
66
+ <template #trigger="{ layoutId, transition }">
67
+ <button
68
+ type="button"
69
+ class="block w-full cursor-zoom-in rounded-[inherit] focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
70
+ :aria-label="`${t('docs.zoomImage')}: ${alt ?? ''}`"
71
+ >
72
+ <Motion as-child :layout-id="layoutId" :transition="transition">
73
+ <img :src="src" :alt="alt" :width="width" :height="height" :class="imageClass" />
74
+ </Motion>
75
+ </button>
76
+ </template>
77
+ </ImageZoomDialog>
78
+ <img v-else :src="src" :alt="alt" :width="width" :height="height" :class="imageClass" />
79
+ </template>
46
80
  <slot />
47
- <figcaption v-if="caption || alt" class="text-center">
81
+ <figcaption v-if="caption || alt">
48
82
  {{ caption || alt }}
49
83
  </figcaption>
50
84
  </figure>
@@ -0,0 +1,120 @@
1
+ <script setup lang="ts">
2
+ import type { VNode } from "vue";
3
+ import { h, resolveComponent, useSlots } from "vue";
4
+ import { useProseAppearance } from "../../composables/useProseAppearance";
5
+ import { resolveFileIcon } from "../../utils/file-icons";
6
+
7
+ type FileEntry = {
8
+ name: string;
9
+ path: string;
10
+ children?: FileEntry[];
11
+ };
12
+
13
+ const props = defineProps<{
14
+ active?: string;
15
+ annotations?: Record<string, string>;
16
+ appearance?: "quiet" | "tint";
17
+ }>();
18
+
19
+ const slots = useSlots();
20
+ const appearance = useProseAppearance("files", () => props.appearance);
21
+
22
+ function childrenOf(node: VNode): VNode[] {
23
+ if (Array.isArray(node.children)) return node.children.filter(isVNode);
24
+ if (node.children && typeof node.children === "object") {
25
+ const slot = (node.children as { default?: () => unknown }).default;
26
+ const children = typeof slot === "function" ? slot() : [];
27
+ if (Array.isArray(children)) return children.filter(isVNode);
28
+ return isVNode(children) ? [children] : [];
29
+ }
30
+ return [];
31
+ }
32
+
33
+ function isVNode(value: unknown): value is VNode {
34
+ return value !== null && typeof value === "object" && "type" in value;
35
+ }
36
+
37
+ function textOf(value: unknown): string {
38
+ if (typeof value === "string") return value;
39
+ if (Array.isArray(value)) return value.map(textOf).join("");
40
+ if (!isVNode(value)) return "";
41
+ if (typeof value.children === "string") return value.children;
42
+ if (value.children && typeof value.children === "object") {
43
+ const slot = (value.children as { default?: () => unknown }).default;
44
+ if (typeof slot === "function") return textOf(slot());
45
+ }
46
+ return childrenOf(value).map(textOf).join("");
47
+ }
48
+
49
+ function parseList(list: VNode, parent = ""): FileEntry[] {
50
+ return childrenOf(list)
51
+ .map((item) => {
52
+ const children = childrenOf(item);
53
+ const nested = children.length > 1 ? children.at(-1) : undefined;
54
+ const name = children
55
+ .filter((node) => node !== nested)
56
+ .map(textOf)
57
+ .join("")
58
+ .trim();
59
+ const path = parent ? `${parent}/${name}` : name;
60
+ return {
61
+ name,
62
+ path,
63
+ ...(nested ? { children: parseList(nested, path) } : {}),
64
+ };
65
+ })
66
+ .filter((entry) => entry.name.length > 0);
67
+ }
68
+
69
+ function renderEntries(items: FileEntry[]): VNode {
70
+ const Icon = resolveComponent("Icon");
71
+ return h(
72
+ "ul",
73
+ { class: "content-files-list" },
74
+ items.map((entry) => {
75
+ const folder = Boolean(entry.children);
76
+ const active = props.active === entry.path;
77
+ return h("li", { key: entry.path, class: "content-files-item" }, [
78
+ h(
79
+ "div",
80
+ {
81
+ class: "content-files-row",
82
+ "data-active": active || undefined,
83
+ "aria-current": active ? "true" : undefined,
84
+ },
85
+ [
86
+ h(Icon, {
87
+ name: folder ? "lucide:folder-open" : resolveFileIcon(entry.name),
88
+ class: "content-files-icon",
89
+ "aria-hidden": "true",
90
+ }),
91
+ h("span", { class: "content-files-name" }, entry.name),
92
+ props.annotations?.[entry.path]
93
+ ? h("span", { class: "content-files-annotation" }, props.annotations[entry.path])
94
+ : null,
95
+ ],
96
+ ),
97
+ entry.children?.length ? renderEntries(entry.children) : null,
98
+ ]);
99
+ }),
100
+ );
101
+ }
102
+
103
+ function renderFiles() {
104
+ const list = slots.default?.()[0];
105
+ const entries = list ? parseList(list) : [];
106
+ if (!entries.length) return null;
107
+ return h(
108
+ "div",
109
+ {
110
+ class: "content-files not-prose",
111
+ "data-appearance": appearance.value,
112
+ },
113
+ [renderEntries(entries)],
114
+ );
115
+ }
116
+ </script>
117
+
118
+ <template>
119
+ <component :is="renderFiles" />
120
+ </template>
@@ -5,12 +5,13 @@ import MdcCallout from "./MdcCallout.vue";
5
5
  const props = defineProps<{
6
6
  title?: string;
7
7
  icon?: string;
8
+ appearance?: "quiet" | "tint";
8
9
  class?: HTMLAttributes["class"];
9
10
  }>();
10
11
  </script>
11
12
 
12
13
  <template>
13
- <MdcCallout :title="title" :icon="icon" type="idea" :class="props.class">
14
+ <MdcCallout :title="title" :icon="icon" type="idea" :appearance="appearance" :class="props.class">
14
15
  <slot />
15
16
  </MdcCallout>
16
17
  </template>
@@ -5,12 +5,13 @@ import MdcCallout from "./MdcCallout.vue";
5
5
  const props = defineProps<{
6
6
  title?: string;
7
7
  icon?: string;
8
+ appearance?: "quiet" | "tint";
8
9
  class?: HTMLAttributes["class"];
9
10
  }>();
10
11
  </script>
11
12
 
12
13
  <template>
13
- <MdcCallout :title="title" :icon="icon" type="info" :class="props.class">
14
+ <MdcCallout :title="title" :icon="icon" type="info" :appearance="appearance" :class="props.class">
14
15
  <slot />
15
16
  </MdcCallout>
16
17
  </template>
@@ -0,0 +1,51 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { computed, inject } from "vue";
4
+ import {
5
+ Collapsible,
6
+ CollapsibleContent,
7
+ CollapsibleTrigger,
8
+ } from "#ginko-docs/components/ui/collapsible";
9
+ import DocsToc from "#ginko-docs/features/docs/components/DocsToc.vue";
10
+ import { docsTocKey } from "#ginko-docs/features/docs/toc-context";
11
+ import { filterTocByDepth } from "#ginko-docs/utils/content";
12
+ import { useI18n } from "#imports";
13
+ import { cn } from "../../utils";
14
+
15
+ const props = defineProps<{
16
+ title?: string;
17
+ /** Restrict to headings up to this depth (within the page TOC depth). */
18
+ depth?: number;
19
+ open?: boolean | string;
20
+ class?: HTMLAttributes["class"];
21
+ }>();
22
+
23
+ const { t } = useI18n();
24
+ const pageToc = inject(docsTocKey, undefined);
25
+
26
+ const items = computed(() => {
27
+ const all = pageToc?.value ?? [];
28
+ return props.depth ? filterTocByDepth(all, props.depth) : all;
29
+ });
30
+
31
+ const defaultOpen = computed(
32
+ () => props.open !== undefined && props.open !== false && props.open !== "false",
33
+ );
34
+ </script>
35
+
36
+ <template>
37
+ <Collapsible
38
+ v-if="items.length"
39
+ :default-open="defaultOpen"
40
+ :class="cn('content-inline-toc not-prose', props.class)"
41
+ >
42
+ <CollapsibleTrigger class="content-inline-toc-trigger">
43
+ <Icon name="lucide:list" class="content-inline-toc-icon" aria-hidden="true" />
44
+ <span class="content-inline-toc-title">{{ title ?? t("docs.toc") }}</span>
45
+ <Icon name="lucide:chevron-down" class="content-inline-toc-chevron" aria-hidden="true" />
46
+ </CollapsibleTrigger>
47
+ <CollapsibleContent class="content-inline-toc-content">
48
+ <DocsToc :items="items" :show-title="false" />
49
+ </CollapsibleContent>
50
+ </Collapsible>
51
+ </template>
@@ -1,12 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import type { HTMLAttributes } from "vue";
3
- import { cn } from "#ginko-docs/lib/utils";
3
+ import { cn } from "../../utils";
4
4
 
5
5
  const props = defineProps<{ class?: HTMLAttributes["class"] }>();
6
6
  </script>
7
7
 
8
8
  <template>
9
- <div data-slot="dialog-header" :class="cn('flex flex-col gap-1.5', props.class)">
10
- <slot />
11
- </div>
9
+ <kbd :class="cn('content-kbd', props.class)"><slot /></kbd>
12
10
  </template>
@@ -5,12 +5,13 @@ import MdcCallout from "./MdcCallout.vue";
5
5
  const props = defineProps<{
6
6
  title?: string;
7
7
  icon?: string;
8
+ appearance?: "quiet" | "tint";
8
9
  class?: HTMLAttributes["class"];
9
10
  }>();
10
11
  </script>
11
12
 
12
13
  <template>
13
- <MdcCallout :title="title" :icon="icon" type="note" :class="props.class">
14
+ <MdcCallout :title="title" :icon="icon" type="note" :appearance="appearance" :class="props.class">
14
15
  <slot />
15
16
  </MdcCallout>
16
17
  </template>