@lupinum/ginko-docs 0.2.2 → 0.2.5

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 (206) hide show
  1. package/README.md +130 -73
  2. package/app/app.config.ts +15 -13
  3. package/app/app.vue +4 -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 +34 -99
  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 +266 -132
  57. package/app/components/site/SiteHeroCode.vue +164 -0
  58. package/app/components/site/SiteLocaleSwitcher.vue +25 -9
  59. package/app/components/site/SiteSocialLinks.vue +31 -0
  60. package/app/components/ui/badge/Badge.vue +1 -1
  61. package/app/components/ui/button/Button.vue +1 -1
  62. package/app/components/ui/collapsible/CollapsibleContent.vue +1 -1
  63. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +14 -4
  64. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  65. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +1 -1
  66. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +1 -1
  67. package/app/components/ui/dropdown-menu/index.ts +0 -9
  68. package/app/components/ui/kbd/Kbd.vue +1 -1
  69. package/app/components/ui/kbd/index.ts +0 -1
  70. package/app/components/ui/scroll-area/ScrollArea.vue +1 -1
  71. package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
  72. package/app/components/ui/separator/Separator.vue +1 -1
  73. package/app/components/ui/sheet/SheetContent.vue +1 -1
  74. package/app/components/ui/sheet/SheetDescription.vue +1 -1
  75. package/app/components/ui/sheet/SheetOverlay.vue +1 -1
  76. package/app/components/ui/sheet/SheetTitle.vue +1 -1
  77. package/app/components/ui/sheet/index.ts +0 -3
  78. package/app/components/ui/tabs/Tabs.vue +1 -1
  79. package/app/components/ui/tabs/TabsList.vue +1 -1
  80. package/app/components/ui/tabs/TabsTrigger.vue +1 -1
  81. package/app/components/ui/tabs/index.ts +0 -1
  82. package/app/composables/site-navigation.utils.ts +69 -12
  83. package/app/composables/useContentRouteAlternates.ts +13 -21
  84. package/app/composables/useLocalizedRouteSwitch.ts +1 -1
  85. package/app/composables/useMetaKey.ts +18 -0
  86. package/app/composables/useProseAppearance.ts +24 -0
  87. package/app/composables/useSiteNavigation.ts +10 -40
  88. package/app/config/site.utils.ts +1 -1
  89. package/app/error.vue +1 -5
  90. package/app/features/docs/components/DocsBreadcrumb.vue +37 -0
  91. package/app/features/docs/components/DocsContributeLinks.vue +28 -18
  92. package/app/features/docs/components/DocsMobileToc.vue +12 -8
  93. package/app/features/docs/components/DocsPageContent.vue +93 -47
  94. package/app/features/docs/components/DocsPageNav.vue +1 -1
  95. package/app/features/docs/components/DocsSidebar.vue +37 -21
  96. package/app/features/docs/components/DocsSidebarItem.vue +53 -100
  97. package/app/features/docs/components/DocsSidebarRow.vue +27 -0
  98. package/app/features/docs/components/DocsSidebarTabs.vue +46 -22
  99. package/app/features/docs/components/DocsToc.vue +80 -34
  100. package/app/features/docs/composables/useDocsNavigation.ts +2 -2
  101. package/app/features/docs/composables/useDocsNavigationData.ts +4 -1
  102. package/app/features/docs/composables/useRevealActive.ts +58 -0
  103. package/app/features/docs/composables/useScrollspy.ts +87 -19
  104. package/app/features/docs/docs-navigation.ts +17 -30
  105. package/app/features/docs/toc-context.ts +6 -0
  106. package/app/features/search/command-center.ts +11 -90
  107. package/app/features/search/components/SiteCommandCenter.vue +58 -38
  108. package/app/features/search/useCommandCenter.ts +48 -48
  109. package/app/layouts/docs.vue +1 -1
  110. package/app/lib/errors.ts +2 -49
  111. package/app/pages/blog/[slug].vue +26 -9
  112. package/app/pages/blog/index.vue +38 -14
  113. package/app/pages/docs/index.vue +10 -14
  114. package/app/pages/index.vue +180 -61
  115. package/app/plugins/ginko-docs-theme.ts +12 -0
  116. package/app/utils/content.ts +11 -0
  117. package/app/utils/file-icons.ts +90 -0
  118. package/app/utils/index.ts +7 -1
  119. package/app/utils/repository.ts +20 -0
  120. package/content.js +68 -50
  121. package/content.ts +51 -32
  122. package/i18n/messages/global/blog.ts +4 -0
  123. package/i18n/messages/global/command.ts +6 -2
  124. package/i18n/messages/global/docs.ts +4 -0
  125. package/i18n/messages/global/nav.ts +6 -3
  126. package/i18n/messages/index.ts +3 -3
  127. package/icon-bundle.ts +9 -56
  128. package/index.d.ts +7 -1
  129. package/modules/feature-routing.ts +15 -0
  130. package/nuxt.config.ts +21 -7
  131. package/package.json +29 -26
  132. package/runtime/server/agent-markdown.ts +0 -2
  133. package/server/api/_ginko-docs/redirects.get.ts +28 -0
  134. package/server/middleware/redirects.ts +22 -0
  135. package/server/routes/[locale]/blog/rss.xml.get.ts +13 -0
  136. package/server/routes/blog/rss.xml.get.ts +5 -0
  137. package/server/utils/blog-feed.ts +49 -0
  138. package/server/utils/feed.ts +70 -0
  139. package/server/utils/redirects.ts +38 -0
  140. package/server/utils/redirects.utils.ts +58 -0
  141. package/shared/theme-palettes.ts +37 -0
  142. package/shared/types/app-config.ts +75 -10
  143. package/tags.ts +47 -59
  144. package/app/components/mdc/MdcAlert.vue +0 -34
  145. package/app/components/mdc/MdcCardGroup.vue +0 -47
  146. package/app/components/mdc/MdcDocImg.vue +0 -25
  147. package/app/components/mdc/MdcField.vue +0 -62
  148. package/app/components/mdc/MdcFieldGroup.vue +0 -18
  149. package/app/components/mdc/MdcPassage.vue +0 -32
  150. package/app/components/mdc/MdcQuizOption.vue +0 -91
  151. package/app/components/mdc/MdcReadMoreGroup.vue +0 -18
  152. package/app/components/mdc/MdcShortcut.vue +0 -46
  153. package/app/components/mdc/MdcStep.vue +0 -42
  154. package/app/components/ui/accordion/Accordion.vue +0 -15
  155. package/app/components/ui/accordion/AccordionContent.vue +0 -23
  156. package/app/components/ui/accordion/AccordionItem.vue +0 -24
  157. package/app/components/ui/accordion/AccordionTrigger.vue +0 -35
  158. package/app/components/ui/accordion/index.ts +0 -4
  159. package/app/components/ui/alert/Alert.vue +0 -21
  160. package/app/components/ui/alert/AlertDescription.vue +0 -22
  161. package/app/components/ui/alert/AlertTitle.vue +0 -22
  162. package/app/components/ui/alert/index.ts +0 -34
  163. package/app/components/ui/card/Card.vue +0 -16
  164. package/app/components/ui/card/CardAction.vue +0 -17
  165. package/app/components/ui/card/CardContent.vue +0 -14
  166. package/app/components/ui/card/CardDescription.vue +0 -14
  167. package/app/components/ui/card/CardFooter.vue +0 -14
  168. package/app/components/ui/card/CardHeader.vue +0 -22
  169. package/app/components/ui/card/card-variants.ts +0 -15
  170. package/app/components/ui/card/index.ts +0 -8
  171. package/app/components/ui/checkbox/Checkbox.vue +0 -35
  172. package/app/components/ui/checkbox/index.ts +0 -1
  173. package/app/components/ui/dialog/Dialog.vue +0 -15
  174. package/app/components/ui/dialog/DialogClose.vue +0 -12
  175. package/app/components/ui/dialog/DialogContent.vue +0 -69
  176. package/app/components/ui/dialog/DialogDescription.vue +0 -21
  177. package/app/components/ui/dialog/DialogFooter.vue +0 -15
  178. package/app/components/ui/dialog/DialogOverlay.vue +0 -26
  179. package/app/components/ui/dialog/DialogPortal.vue +0 -12
  180. package/app/components/ui/dialog/DialogTitle.vue +0 -21
  181. package/app/components/ui/dialog/DialogTrigger.vue +0 -12
  182. package/app/components/ui/dialog/index.ts +0 -10
  183. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +0 -36
  184. package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +0 -12
  185. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +0 -25
  186. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +0 -17
  187. package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +0 -15
  188. package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +0 -29
  189. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +0 -31
  190. package/app/components/ui/input/Input.vue +0 -23
  191. package/app/components/ui/input/index.ts +0 -1
  192. package/app/components/ui/label/Label.vue +0 -14
  193. package/app/components/ui/label/index.ts +0 -1
  194. package/app/components/ui/sheet/SheetClose.vue +0 -12
  195. package/app/components/ui/sheet/SheetFooter.vue +0 -12
  196. package/app/components/ui/sheet/SheetHeader.vue +0 -12
  197. package/app/components/ui/switch/Switch.vue +0 -39
  198. package/app/components/ui/switch/index.ts +0 -1
  199. package/app/components/ui/tabs/TabsContent.vue +0 -21
  200. package/app/components/ui/textarea/Textarea.vue +0 -31
  201. package/app/components/ui/textarea/index.ts +0 -1
  202. package/app/features/docs/composables/useDocsEntryPath.ts +0 -13
  203. package/app/lib/utils.ts +0 -7
  204. package/i18n/messages/source.ts +0 -5
  205. package/runtime/markdown/inline-code-highlight.ts +0 -155
  206. package/runtime/utils.ts +0 -7
@@ -1,14 +1,15 @@
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<{
6
+ id?: string;
6
7
  class?: HTMLAttributes["class"];
7
8
  }>();
8
9
  </script>
9
10
 
10
11
  <template>
11
- <kbd data-slot="kbd-group" :class="cn('inline-flex items-center gap-1', props.class)">
12
+ <h5 :id="id" :class="cn('scroll-mt-[var(--content-scroll-margin)]', props.class)">
12
13
  <slot />
13
- </kbd>
14
+ </h5>
14
15
  </template>
@@ -1,14 +1,15 @@
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<{
6
+ id?: string;
6
7
  class?: HTMLAttributes["class"];
7
8
  }>();
8
9
  </script>
9
10
 
10
11
  <template>
11
- <h3 data-slot="card-title" :class="cn('leading-none font-semibold', props.class)">
12
+ <h6 :id="id" :class="cn('scroll-mt-[var(--content-scroll-margin)]', props.class)">
12
13
  <slot />
13
- </h3>
14
+ </h6>
14
15
  </template>
@@ -1,120 +1,55 @@
1
1
  <script setup lang="ts">
2
2
  import type { HTMLAttributes } from "vue";
3
- import { computed, ref } from "vue";
3
+ import { computed, resolveComponent } from "vue";
4
+ import { Motion } from "motion-v";
4
5
  import { useI18n } from "#imports";
5
- import {
6
- Dialog,
7
- DialogContent,
8
- DialogDescription,
9
- DialogTitle,
10
- } from "#ginko-docs/components/ui/dialog";
6
+ import ImageZoomDialog from "#ginko-docs/components/content/ImageZoomDialog.vue";
11
7
  import { useGinkoDocsConfig } from "#ginko-docs/composables/useGinkoDocsConfig";
12
8
  import { cn } from "../../utils";
13
9
 
14
- const props = withDefaults(
15
- defineProps<{
16
- src?: string;
17
- alt?: string;
18
- title?: string;
19
- caption?: string;
20
- description?: string;
21
- width?: string | number;
22
- height?: string | number;
23
- bleed?: boolean | string;
24
- aspect?: "auto" | "video" | "wide" | "square" | "portrait";
25
- fit?: "cover" | "contain";
26
- align?: "left" | "center" | "right";
27
- class?: HTMLAttributes["class"];
28
- }>(),
29
- {
30
- aspect: "auto",
31
- fit: "cover",
32
- align: "center",
33
- },
34
- );
10
+ const props = defineProps<{
11
+ src?: string;
12
+ alt?: string;
13
+ title?: string;
14
+ width?: string | number;
15
+ height?: string | number;
16
+ class?: HTMLAttributes["class"];
17
+ }>();
35
18
 
36
19
  const { t } = useI18n();
37
20
  const config = useGinkoDocsConfig();
38
21
  const zoomEnabled = computed(() => config.images?.zoom !== false);
39
- const zoomOpen = ref(false);
40
22
 
41
- const shouldBleed = computed(() => props.bleed === true || props.bleed === "true");
42
-
43
- const aspectClass = computed(() => {
44
- return {
45
- auto: "",
46
- video: "aspect-video",
47
- wide: "aspect-[21/9]",
48
- square: "aspect-square",
49
- portrait: "aspect-[4/5]",
50
- }[props.aspect];
51
- });
52
-
53
- const imageClass = computed(() =>
54
- cn("w-full", aspectClass.value, props.fit === "contain" ? "object-contain" : "object-cover"),
55
- );
23
+ // External images skip NuxtImg; the layer ships no ipx domain allowlist.
24
+ const isLocalAsset = computed(() => Boolean(props.src?.startsWith("/")));
25
+ const imageComponent = computed(() => (isLocalAsset.value ? resolveComponent("NuxtImg") : "img"));
26
+ const imageAttrs = computed(() => ({
27
+ src: props.src,
28
+ alt: props.alt ?? "",
29
+ title: props.title,
30
+ width: props.width,
31
+ height: props.height,
32
+ loading: "lazy" as const,
33
+ decoding: "async" as const,
34
+ class: cn("content-prose-image", props.class),
35
+ ...(isLocalAsset.value ? { sizes: "100vw md:704px" } : {}),
36
+ }));
56
37
  </script>
57
38
 
58
39
  <template>
59
- <figure
60
- :class="cn('content-media not-prose', props.class)"
61
- :data-bleed="shouldBleed ? 'true' : undefined"
62
- >
63
- <template v-if="src">
40
+ <ImageZoomDialog v-if="src && zoomEnabled" :src="src" :alt="alt" :label="title">
41
+ <template #trigger="{ layoutId, transition }">
42
+ <!-- A button is phrasing content, so a bare markdown image stays valid inside its paragraph. -->
64
43
  <button
65
- v-if="zoomEnabled"
66
44
  type="button"
67
- class="block w-full cursor-zoom-in rounded-[inherit] focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
45
+ class="block max-w-full cursor-zoom-in rounded-[var(--radius)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
68
46
  :aria-label="`${t('docs.zoomImage')}: ${alt ?? ''}`"
69
- @click="zoomOpen = true"
70
47
  >
71
- <NuxtImg
72
- :src="src"
73
- :alt="alt"
74
- :width="width"
75
- :height="height"
76
- sizes="100vw md:768px"
77
- loading="lazy"
78
- :class="imageClass"
79
- />
48
+ <Motion as-child :layout-id="layoutId" :transition="transition">
49
+ <component :is="imageComponent" v-bind="imageAttrs" />
50
+ </Motion>
80
51
  </button>
81
- <NuxtImg
82
- v-else
83
- :src="src"
84
- :alt="alt"
85
- :width="width"
86
- :height="height"
87
- sizes="100vw md:768px"
88
- loading="lazy"
89
- :class="imageClass"
90
- />
91
52
  </template>
92
- <figcaption
93
- v-if="caption || title || alt || description"
94
- :class="
95
- cn(
96
- align === 'left' && 'text-left',
97
- align === 'center' && 'text-center',
98
- align === 'right' && 'text-right',
99
- )
100
- "
101
- >
102
- <span>{{ caption || title || alt }}</span>
103
- <span v-if="description" class="content-media-description">
104
- {{ description }}
105
- </span>
106
- </figcaption>
107
-
108
- <Dialog v-if="zoomEnabled" v-model:open="zoomOpen">
109
- <DialogContent
110
- class="max-h-[90dvh] w-auto max-w-[min(96vw,80rem)] overflow-auto border-none bg-transparent p-0 shadow-none"
111
- >
112
- <DialogTitle class="sr-only">{{
113
- caption || title || alt || t("docs.zoomImage")
114
- }}</DialogTitle>
115
- <DialogDescription v-if="description" class="sr-only">{{ description }}</DialogDescription>
116
- <img :src="src" :alt="alt" class="max-h-[88dvh] w-auto rounded-lg object-contain" />
117
- </DialogContent>
118
- </Dialog>
119
- </figure>
53
+ </ImageZoomDialog>
54
+ <component :is="imageComponent" v-else-if="src" v-bind="imageAttrs" />
120
55
  </template>
@@ -1,9 +1,18 @@
1
1
  <script setup lang="ts">
2
- import type { HTMLAttributes } from "vue";
3
- import { computed, ref, useId } from "vue";
4
- import { useClipboard } from "@vueuse/core";
2
+ import type { ComputedRef, HTMLAttributes } from "vue";
3
+ import { computed, inject, onMounted, ref, useId } from "vue";
4
+ import { useClipboard, useEventListener, useResizeObserver } from "@vueuse/core";
5
5
  import { useI18n } from "#imports";
6
6
  import { cn } from "../../utils";
7
+ import { useProseAppearance } from "../../composables/useProseAppearance";
8
+ import {
9
+ EXTENSION_ICONS,
10
+ FALLBACK_FILE_ICON,
11
+ FILENAME_ICONS,
12
+ LANGUAGE_ICONS,
13
+ filenameExtension,
14
+ normalizedFilename,
15
+ } from "../../utils/file-icons";
7
16
 
8
17
  const props = defineProps<{
9
18
  code?: string;
@@ -14,7 +23,14 @@ const props = defineProps<{
14
23
  icon?: string | null;
15
24
  inGroup?: boolean;
16
25
  class?: HTMLAttributes["class"];
26
+ appearance?: "quiet" | "tint";
17
27
  }>();
28
+ // Instance prop wins, then a surrounding ::collapse wrapper, then config.
29
+ const wrapperAppearance = inject<ComputedRef<"quiet" | "tint"> | null>(
30
+ "contentCodeAppearance",
31
+ null,
32
+ );
33
+ const appearance = useProseAppearance("code", () => props.appearance ?? wrapperAppearance?.value);
18
34
 
19
35
  const { copy, copied } = useClipboard();
20
36
  const { t } = useI18n();
@@ -24,72 +40,6 @@ const codeElement = ref<HTMLElement | null>(null);
24
40
  const regionId = useId();
25
41
 
26
42
  const HIDDEN_LANGUAGES = new Set(["text", "plaintext", "txt", "plain"]);
27
- const FALLBACK_FILE_ICON = "lucide:file";
28
-
29
- const FILENAME_ICONS: Record<string, string> = {
30
- ".env": "lucide:file-key",
31
- ".env.example": "lucide:file-key",
32
- ".gitignore": "lucide:git-branch",
33
- "components.json": "lucide:blocks",
34
- "eslint.config.cjs": "lucide:badge-check",
35
- "eslint.config.js": "lucide:badge-check",
36
- "eslint.config.mjs": "lucide:badge-check",
37
- "nuxt.config.js": "logos:nuxt-icon",
38
- "nuxt.config.ts": "logos:nuxt-icon",
39
- "package.json": "lucide:package",
40
- "pnpm-lock.yaml": "lucide:package",
41
- "pnpm-workspace.yaml": "lucide:package",
42
- "tailwind.config.js": "lucide:palette",
43
- "tailwind.config.ts": "lucide:palette",
44
- "tsconfig.json": "lucide:file-cog",
45
- "vite.config.js": "lucide:zap",
46
- "vite.config.ts": "lucide:zap",
47
- };
48
-
49
- const EXTENSION_ICONS: Record<string, string> = {
50
- bash: "lucide:terminal",
51
- cjs: "lucide:file-json",
52
- css: "lucide:braces",
53
- csv: "lucide:table",
54
- htm: "lucide:code-xml",
55
- html: "lucide:code-xml",
56
- js: "lucide:file-json",
57
- json: "lucide:braces",
58
- jsx: "lucide:atom",
59
- md: "lucide:file-text",
60
- mdc: "lucide:file-text",
61
- mjs: "lucide:file-json",
62
- mts: "lucide:file-code",
63
- scss: "lucide:braces",
64
- sh: "lucide:terminal",
65
- ts: "logos:typescript-icon",
66
- tsx: "lucide:atom",
67
- txt: "lucide:file-text",
68
- vue: "logos:vue",
69
- yaml: "lucide:file-cog",
70
- yml: "lucide:file-cog",
71
- };
72
-
73
- const LANGUAGE_ICONS: Record<string, string> = {
74
- bash: "lucide:terminal",
75
- css: "lucide:braces",
76
- html: "lucide:code-xml",
77
- javascript: "lucide:file-json",
78
- js: "lucide:file-json",
79
- json: "lucide:braces",
80
- jsx: "lucide:atom",
81
- markdown: "lucide:file-text",
82
- mdc: "lucide:file-text",
83
- shell: "lucide:terminal",
84
- sh: "lucide:terminal",
85
- ts: "logos:typescript-icon",
86
- tsx: "lucide:atom",
87
- typescript: "logos:typescript-icon",
88
- vue: "logos:vue",
89
- "vue-html": "logos:vue",
90
- yaml: "lucide:file-cog",
91
- yml: "lucide:file-cog",
92
- };
93
43
 
94
44
  function parseIconFromMeta(meta: string | null | undefined): string | null {
95
45
  if (!meta) return null;
@@ -97,19 +47,6 @@ function parseIconFromMeta(meta: string | null | undefined): string | null {
97
47
  return match?.[1] ?? null;
98
48
  }
99
49
 
100
- function normalizedFilename(filename: string | null | undefined): string | null {
101
- return filename?.trim().replace(/^\.\//, "").toLowerCase() || null;
102
- }
103
-
104
- function filenameExtension(filename: string | null | undefined): string | null {
105
- const normalized = normalizedFilename(filename);
106
- const basename = normalized?.split("/").pop();
107
- const index = basename?.lastIndexOf(".") ?? -1;
108
-
109
- if (!basename || index < 0 || index === basename.length - 1) return null;
110
- return basename.slice(index + 1);
111
- }
112
-
113
50
  function normalizedLanguage(language: string | null | undefined): string | null {
114
51
  return language?.trim().toLowerCase() || null;
115
52
  }
@@ -142,12 +79,29 @@ const iconName = computed(() => {
142
79
  async function copyCode() {
143
80
  await copy(props.code || codeElement.value?.innerText || "");
144
81
  }
82
+
83
+ // A right-edge fade signals horizontally clipped code; it disappears once the
84
+ // reader reaches the end of the line.
85
+ const scrollElement = ref<HTMLElement | null>(null);
86
+ const canScrollRight = ref(false);
87
+
88
+ function measureOverflow() {
89
+ const el = scrollElement.value;
90
+ if (!el) return;
91
+ canScrollRight.value =
92
+ el.scrollWidth - el.clientWidth > 2 && el.scrollLeft + el.clientWidth < el.scrollWidth - 2;
93
+ }
94
+
95
+ onMounted(measureOverflow);
96
+ useEventListener(scrollElement, "scroll", measureOverflow, { passive: true });
97
+ useResizeObserver(scrollElement, measureOverflow);
145
98
  </script>
146
99
 
147
100
  <template>
148
101
  <figure
149
102
  dir="ltr"
150
103
  data-fd-codeblock
104
+ :data-appearance="appearance"
151
105
  tabindex="-1"
152
106
  :class="
153
107
  cn(
@@ -194,24 +148,33 @@ async function copyCode() {
194
148
  </button>
195
149
  </div>
196
150
 
197
- <div
198
- role="region"
199
- tabindex="0"
200
- :aria-label="
201
- (label ? `${t('docs.codeSample')}: ${label}` : t('docs.codeSample')) + ` (${regionId})`
202
- "
203
- :class="
204
- cn(
205
- 'fd-scroll-container max-h-[600px] overflow-auto focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-inset',
206
- !label && !props.inGroup && 'content-codeblock-inline-copy',
207
- )
208
- "
209
- >
210
- <pre
211
- ref="codeElement"
212
- :class="cn('w-max min-w-full', props.class)"
213
- v-bind="$attrs"
214
- ><slot /></pre>
151
+ <div class="content-codeblock-scroll-wrap">
152
+ <div
153
+ ref="scrollElement"
154
+ role="region"
155
+ tabindex="0"
156
+ :aria-label="
157
+ (label ? `${t('docs.codeSample')}: ${label}` : t('docs.codeSample')) + ` (${regionId})`
158
+ "
159
+ :class="
160
+ cn(
161
+ 'fd-scroll-container max-h-[600px] overflow-auto focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-inset',
162
+ !label && !props.inGroup && 'content-codeblock-inline-copy',
163
+ )
164
+ "
165
+ >
166
+ <pre
167
+ ref="codeElement"
168
+ :class="cn('w-max min-w-full', props.class)"
169
+ v-bind="$attrs"
170
+ ><slot /></pre>
171
+ </div>
172
+
173
+ <div
174
+ class="content-codeblock-overflow-fade"
175
+ :data-hidden="!canScrollRight"
176
+ aria-hidden="true"
177
+ />
215
178
  </div>
216
179
  </figure>
217
180
  </template>
@@ -9,8 +9,10 @@ const props = defineProps<{
9
9
 
10
10
  <template>
11
11
  <div :class="cn('content-table', props.class)">
12
- <table>
13
- <slot />
14
- </table>
12
+ <div class="content-table-scroll">
13
+ <table>
14
+ <slot />
15
+ </table>
16
+ </div>
15
17
  </div>
16
18
  </template>
@@ -3,7 +3,7 @@ import type { HTMLAttributes } from "vue";
3
3
  import { Button } from "#ginko-docs/components/ui/button";
4
4
  import { computed } from "vue";
5
5
  import { useColorMode, useI18n } from "#imports";
6
- import { cn } from "#ginko-docs/lib/utils";
6
+ import { cn } from "#ginko-docs/utils";
7
7
  import {
8
8
  DropdownMenu,
9
9
  DropdownMenuContent,
@@ -14,7 +14,7 @@ import {
14
14
 
15
15
  const props = withDefaults(
16
16
  defineProps<{
17
- variant?: "icon" | "menu-row" | "menu-tile";
17
+ variant?: "icon" | "menu-row" | "menu-tile" | "pill";
18
18
  class?: HTMLAttributes["class"];
19
19
  }>(),
20
20
  {
@@ -95,12 +95,21 @@ function setColorModePreference(value: unknown) {
95
95
  aria-hidden="true"
96
96
  />
97
97
  </Button>
98
+ <Button
99
+ v-else-if="variant === 'pill'"
100
+ variant="outline"
101
+ :class="cn('h-9 gap-2 rounded-full px-4 text-sm font-semibold', props.class)"
102
+ >
103
+ <Icon :name="triggerIcon" class="size-4" aria-hidden="true" />
104
+ <span>{{ triggerLabel }}</span>
105
+ <span class="sr-only">{{ t("theme.toggle") }}</span>
106
+ </Button>
98
107
  <Button v-else variant="outline" size="icon" :class="cn('relative shrink-0', props.class)">
99
108
  <Icon :name="nextExplicitColorModeIcon" class="size-[1.2rem]" aria-hidden="true" />
100
109
  <span class="sr-only">{{ t("theme.toggle") }}</span>
101
110
  </Button>
102
111
  </DropdownMenuTrigger>
103
- <DropdownMenuContent align="end" class="w-44">
112
+ <DropdownMenuContent align="end" class="w-44" :portal-disabled="variant !== 'icon'">
104
113
  <DropdownMenuRadioGroup
105
114
  :model-value="colorMode.preference"
106
115
  @update:model-value="setColorModePreference"
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { useLocalStorage } from "@vueuse/core";
3
- import { computed, ref, onMounted } from "vue";
3
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
4
4
  import { useI18n } from "#imports";
5
5
  import { useGinkoDocsConfig } from "#ginko-docs/composables/useGinkoDocsConfig";
6
6
  import { useSiteNavigation } from "#ginko-docs/composables/useSiteNavigation";
@@ -25,11 +25,52 @@ const visible = computed(
25
25
  (!props.landing || config.banner.showOnLanding) &&
26
26
  (!hydrated.value ? true : !dismissed.value),
27
27
  );
28
+
29
+ // The banner scrolls away above the sticky header, so sticky consumers
30
+ // (docs sidebar/TOC heights, mobile menu top) need its *currently visible*
31
+ // height. This component is the single writer of --site-banner-height.
32
+ const bannerElement = ref<HTMLElement | null>(null);
33
+ let rafId: number | null = null;
34
+ let lastHeight = -1;
35
+
36
+ function updateBannerHeightVar() {
37
+ rafId = null;
38
+ const element = bannerElement.value;
39
+ const height = element
40
+ ? Math.max(0, Math.min(element.getBoundingClientRect().bottom, element.offsetHeight))
41
+ : 0;
42
+ if (height === lastHeight) return;
43
+ lastHeight = height;
44
+ document.documentElement.style.setProperty("--site-banner-height", `${height}px`);
45
+ }
46
+
47
+ function scheduleBannerHeightVar() {
48
+ if (rafId !== null) return;
49
+ rafId = requestAnimationFrame(updateBannerHeightVar);
50
+ }
51
+
52
+ watch(visible, () => {
53
+ void nextTick(scheduleBannerHeightVar);
54
+ });
55
+
56
+ onMounted(() => {
57
+ window.addEventListener("scroll", scheduleBannerHeightVar, { passive: true });
58
+ window.addEventListener("resize", scheduleBannerHeightVar, { passive: true });
59
+ scheduleBannerHeightVar();
60
+ });
61
+
62
+ onBeforeUnmount(() => {
63
+ window.removeEventListener("scroll", scheduleBannerHeightVar);
64
+ window.removeEventListener("resize", scheduleBannerHeightVar);
65
+ if (rafId !== null) cancelAnimationFrame(rafId);
66
+ document.documentElement.style.setProperty("--site-banner-height", "0px");
67
+ });
28
68
  </script>
29
69
 
30
70
  <template>
31
71
  <div
32
72
  v-if="visible"
73
+ ref="bannerElement"
33
74
  role="region"
34
75
  :aria-label="banner.text"
35
76
  class="relative z-30 bg-primary px-10 py-2.5 text-center text-sm leading-5 font-medium text-primary-foreground"
@@ -46,7 +87,7 @@ const visible = computed(
46
87
  </span>
47
88
  <button
48
89
  type="button"
49
- class="absolute top-1/2 right-2 flex size-8 -translate-y-1/2 items-center justify-center rounded p-1 opacity-70 transition-opacity hover:opacity-100"
90
+ class="absolute top-1/2 right-2 flex size-8 -translate-y-1/2 items-center justify-center rounded p-1 opacity-70 transition-opacity hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-primary-foreground/50 focus-visible:outline-none"
50
91
  :aria-label="t('banner.dismiss')"
51
92
  @click="dismissed = true"
52
93
  >
@@ -3,17 +3,15 @@ import { computed } from "vue";
3
3
  import { useI18n } from "#imports";
4
4
  import { useLocalizedPath } from "#ginko-docs/composables/useLocalizedPath";
5
5
  import { useSiteNavigation } from "#ginko-docs/composables/useSiteNavigation";
6
- import { useDocsEntryPath } from "#ginko-docs/features/docs/composables/useDocsEntryPath";
7
6
 
8
- const { site, footerNav } = useSiteNavigation();
7
+ const { site, socialLinks } = useSiteNavigation();
9
8
  const { t } = useI18n();
10
9
  const localizedPath = useLocalizedPath();
11
- const docsEntryPath = await useDocsEntryPath();
12
10
 
13
11
  const currentYearDate = new Date();
14
12
  const resources = computed(() => [
15
- { label: t("nav.documentation"), href: docsEntryPath.value, external: false },
16
- ...footerNav.value.resources,
13
+ { label: t("nav.documentation"), href: localizedPath("docs"), external: false },
14
+ ...socialLinks.value,
17
15
  ]);
18
16
  </script>
19
17