@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.
- package/README.md +130 -73
- package/app/app.config.ts +14 -12
- package/app/app.vue +2 -2
- package/app/assets/css/prose.css +2203 -149
- package/app/assets/css/tailwind.css +60 -38
- package/app/assets/css/theme-palettes.css +309 -0
- package/app/components/content/DocumentPageShell.vue +1 -1
- package/app/components/content/Feedback.vue +12 -11
- package/app/components/content/ImageZoomDialog.vue +84 -0
- package/app/components/content/PageMarkdownCopy.vue +2 -0
- package/app/components/content/imageZoom.ts +25 -0
- package/app/components/mdc/MdcAccordion.vue +6 -3
- package/app/components/mdc/MdcAccordionItem.vue +7 -16
- package/app/components/mdc/MdcApi.vue +159 -0
- package/app/components/mdc/MdcAside.vue +6 -10
- package/app/components/mdc/MdcCallout.vue +9 -10
- package/app/components/mdc/MdcCard.vue +36 -47
- package/app/components/mdc/MdcCards.vue +30 -0
- package/app/components/mdc/MdcCodeGroup.vue +4 -0
- package/app/components/mdc/MdcCodeTree.vue +234 -0
- package/app/components/mdc/MdcCollapse.vue +61 -0
- package/app/components/mdc/MdcError.vue +8 -1
- package/app/components/mdc/MdcExcerpt.vue +26 -0
- package/app/components/mdc/MdcFigure.vue +54 -20
- package/app/components/mdc/MdcFiles.vue +120 -0
- package/app/components/mdc/MdcIdea.vue +2 -1
- package/app/components/mdc/MdcInfo.vue +2 -1
- package/app/components/mdc/MdcInlineToc.vue +51 -0
- package/app/components/{ui/dialog/DialogHeader.vue → mdc/MdcKbd.vue} +2 -4
- package/app/components/mdc/MdcNote.vue +2 -1
- package/app/components/mdc/MdcQuiz.vue +43 -83
- package/app/components/mdc/MdcQuizQuestion.vue +104 -109
- package/app/components/mdc/MdcReadMore.vue +57 -77
- package/app/components/mdc/MdcSteps.vue +45 -11
- package/app/components/mdc/MdcSuccess.vue +8 -1
- package/app/components/mdc/MdcTabs.vue +44 -26
- package/app/components/mdc/MdcTimeline.vue +4 -1
- package/app/components/mdc/MdcTimelineItem.vue +18 -27
- package/app/components/mdc/MdcWarning.vue +8 -1
- package/app/components/mdc/api.utils.ts +132 -0
- package/app/components/mdc/code-tree.utils.ts +22 -0
- package/app/components/mdc/quiz.utils.ts +38 -0
- package/app/components/prose/ProseBlockquote.vue +17 -1
- package/app/components/prose/ProseH1.vue +1 -1
- package/app/components/prose/ProseH2.vue +7 -2
- package/app/components/prose/ProseH3.vue +7 -2
- package/app/components/prose/ProseH4.vue +17 -2
- package/app/components/{ui/kbd/KbdGroup.vue → prose/ProseH5.vue} +4 -3
- package/app/components/{ui/card/CardTitle.vue → prose/ProseH6.vue} +4 -3
- package/app/components/prose/ProseImg.vue +19 -112
- package/app/components/prose/ProsePre.vue +63 -100
- package/app/components/prose/ProseTable.vue +5 -3
- package/app/components/site/ModeToggle.vue +12 -3
- package/app/components/site/SiteBanner.vue +43 -2
- package/app/components/site/SiteFooter.vue +3 -5
- package/app/components/site/SiteHeader.vue +255 -132
- package/app/components/site/SiteHeroCode.vue +164 -0
- package/app/components/site/SiteLocaleSwitcher.vue +22 -4
- package/app/components/ui/badge/Badge.vue +1 -1
- package/app/components/ui/button/Button.vue +1 -1
- package/app/components/ui/collapsible/CollapsibleContent.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +14 -4
- package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +1 -1
- package/app/components/ui/dropdown-menu/index.ts +0 -9
- package/app/components/ui/kbd/Kbd.vue +1 -1
- package/app/components/ui/kbd/index.ts +0 -1
- package/app/components/ui/scroll-area/ScrollArea.vue +1 -1
- package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
- package/app/components/ui/separator/Separator.vue +1 -1
- package/app/components/ui/sheet/SheetContent.vue +1 -1
- package/app/components/ui/sheet/SheetDescription.vue +1 -1
- package/app/components/ui/sheet/SheetOverlay.vue +1 -1
- package/app/components/ui/sheet/SheetTitle.vue +1 -1
- package/app/components/ui/sheet/index.ts +0 -3
- package/app/components/ui/tabs/Tabs.vue +1 -1
- package/app/components/ui/tabs/TabsList.vue +1 -1
- package/app/components/ui/tabs/TabsTrigger.vue +1 -1
- package/app/components/ui/tabs/index.ts +0 -1
- package/app/composables/site-navigation.utils.ts +29 -11
- package/app/composables/useContentRouteAlternates.ts +13 -21
- package/app/composables/useLocalizedRouteSwitch.ts +1 -1
- package/app/composables/useMetaKey.ts +18 -0
- package/app/composables/useProseAppearance.ts +24 -0
- package/app/composables/useSiteNavigation.ts +3 -18
- package/app/config/site.utils.ts +1 -1
- package/app/error.vue +1 -5
- package/app/features/docs/components/DocsBreadcrumb.vue +37 -0
- package/app/features/docs/components/DocsContributeLinks.vue +28 -18
- package/app/features/docs/components/DocsMobileToc.vue +12 -8
- package/app/features/docs/components/DocsPageContent.vue +84 -47
- package/app/features/docs/components/DocsPageNav.vue +1 -1
- package/app/features/docs/components/DocsSidebar.vue +30 -20
- package/app/features/docs/components/DocsSidebarItem.vue +53 -100
- package/app/features/docs/components/DocsSidebarRow.vue +27 -0
- package/app/features/docs/components/DocsSidebarTabs.vue +46 -22
- package/app/features/docs/components/DocsToc.vue +80 -34
- package/app/features/docs/composables/useDocsNavigation.ts +2 -2
- package/app/features/docs/composables/useDocsNavigationData.ts +4 -1
- package/app/features/docs/composables/useRevealActive.ts +58 -0
- package/app/features/docs/composables/useScrollspy.ts +87 -19
- package/app/features/docs/docs-navigation.ts +17 -30
- package/app/features/docs/toc-context.ts +6 -0
- package/app/features/search/command-center.ts +11 -90
- package/app/features/search/components/SiteCommandCenter.vue +58 -38
- package/app/features/search/useCommandCenter.ts +46 -46
- package/app/layouts/docs.vue +1 -1
- package/app/lib/errors.ts +2 -49
- package/app/pages/blog/[slug].vue +16 -8
- package/app/pages/blog/index.vue +26 -13
- package/app/pages/docs/index.vue +10 -14
- package/app/pages/index.vue +174 -59
- package/app/plugins/ginko-docs-theme.ts +12 -0
- package/app/utils/content.ts +11 -0
- package/app/utils/file-icons.ts +90 -0
- package/app/utils/index.ts +7 -1
- package/app/utils/repository.ts +20 -0
- package/content.js +23 -26
- package/content.ts +30 -28
- package/i18n/messages/global/blog.ts +4 -0
- package/i18n/messages/global/command.ts +6 -2
- package/i18n/messages/global/docs.ts +3 -0
- package/i18n/messages/global/nav.ts +6 -2
- package/i18n/messages/index.ts +3 -3
- package/icon-bundle.ts +5 -0
- package/index.d.ts +7 -1
- package/nuxt.config.ts +2 -2
- package/package.json +9 -6
- package/runtime/server/agent-markdown.ts +0 -2
- package/shared/theme-palettes.ts +37 -0
- package/shared/types/app-config.ts +63 -6
- package/tags.ts +47 -59
- package/app/components/mdc/MdcAlert.vue +0 -34
- package/app/components/mdc/MdcCardGroup.vue +0 -47
- package/app/components/mdc/MdcDocImg.vue +0 -25
- package/app/components/mdc/MdcField.vue +0 -62
- package/app/components/mdc/MdcFieldGroup.vue +0 -18
- package/app/components/mdc/MdcPassage.vue +0 -32
- package/app/components/mdc/MdcQuizOption.vue +0 -91
- package/app/components/mdc/MdcReadMoreGroup.vue +0 -18
- package/app/components/mdc/MdcShortcut.vue +0 -46
- package/app/components/mdc/MdcStep.vue +0 -42
- package/app/components/ui/accordion/Accordion.vue +0 -15
- package/app/components/ui/accordion/AccordionContent.vue +0 -23
- package/app/components/ui/accordion/AccordionItem.vue +0 -24
- package/app/components/ui/accordion/AccordionTrigger.vue +0 -35
- package/app/components/ui/accordion/index.ts +0 -4
- package/app/components/ui/alert/Alert.vue +0 -21
- package/app/components/ui/alert/AlertDescription.vue +0 -22
- package/app/components/ui/alert/AlertTitle.vue +0 -22
- package/app/components/ui/alert/index.ts +0 -34
- package/app/components/ui/card/Card.vue +0 -16
- package/app/components/ui/card/CardAction.vue +0 -17
- package/app/components/ui/card/CardContent.vue +0 -14
- package/app/components/ui/card/CardDescription.vue +0 -14
- package/app/components/ui/card/CardFooter.vue +0 -14
- package/app/components/ui/card/CardHeader.vue +0 -22
- package/app/components/ui/card/card-variants.ts +0 -15
- package/app/components/ui/card/index.ts +0 -8
- package/app/components/ui/checkbox/Checkbox.vue +0 -35
- package/app/components/ui/checkbox/index.ts +0 -1
- package/app/components/ui/dialog/Dialog.vue +0 -15
- package/app/components/ui/dialog/DialogClose.vue +0 -12
- package/app/components/ui/dialog/DialogContent.vue +0 -69
- package/app/components/ui/dialog/DialogDescription.vue +0 -21
- package/app/components/ui/dialog/DialogFooter.vue +0 -15
- package/app/components/ui/dialog/DialogOverlay.vue +0 -26
- package/app/components/ui/dialog/DialogPortal.vue +0 -12
- package/app/components/ui/dialog/DialogTitle.vue +0 -21
- package/app/components/ui/dialog/DialogTrigger.vue +0 -12
- package/app/components/ui/dialog/index.ts +0 -10
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +0 -36
- package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +0 -12
- package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +0 -25
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +0 -17
- package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +0 -15
- package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +0 -29
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +0 -31
- package/app/components/ui/input/Input.vue +0 -23
- package/app/components/ui/input/index.ts +0 -1
- package/app/components/ui/label/Label.vue +0 -14
- package/app/components/ui/label/index.ts +0 -1
- package/app/components/ui/sheet/SheetClose.vue +0 -12
- package/app/components/ui/sheet/SheetFooter.vue +0 -12
- package/app/components/ui/sheet/SheetHeader.vue +0 -12
- package/app/components/ui/switch/Switch.vue +0 -39
- package/app/components/ui/switch/index.ts +0 -1
- package/app/components/ui/tabs/TabsContent.vue +0 -21
- package/app/components/ui/textarea/Textarea.vue +0 -31
- package/app/components/ui/textarea/index.ts +0 -1
- package/app/features/docs/composables/useDocsEntryPath.ts +0 -13
- package/app/lib/utils.ts +0 -7
- package/i18n/messages/source.ts +0 -5
- package/runtime/markdown/inline-code-highlight.ts +0 -155
- package/runtime/utils.ts +0 -7
|
@@ -1,25 +1,59 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { HTMLAttributes } from "vue";
|
|
3
|
-
import { computed,
|
|
2
|
+
import type { HTMLAttributes, VNode } from "vue";
|
|
3
|
+
import { cloneVNode, computed, h, resolveComponent, useSlots } from "vue";
|
|
4
|
+
import { useProseAppearance } from "../../composables/useProseAppearance";
|
|
4
5
|
import { cn } from "../../utils";
|
|
5
6
|
|
|
6
7
|
const props = withDefaults(
|
|
7
8
|
defineProps<{
|
|
8
|
-
|
|
9
|
+
mode?: "icons" | "numbered";
|
|
10
|
+
appearance?: "quiet" | "tint";
|
|
9
11
|
class?: HTMLAttributes["class"];
|
|
10
12
|
}>(),
|
|
11
|
-
{
|
|
12
|
-
variant: "icons",
|
|
13
|
-
},
|
|
13
|
+
{ mode: "icons" },
|
|
14
14
|
);
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
const slots = useSlots();
|
|
17
|
+
const appearance = useProseAppearance("steps", () => props.appearance);
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
const steps = computed(() => {
|
|
20
|
+
const result: Array<{ heading: VNode; body: VNode[] }> = [];
|
|
21
|
+
for (const node of slots.default?.() ?? []) {
|
|
22
|
+
if (typeof node.props?.id === "string") {
|
|
23
|
+
result.push({ heading: node, body: [] });
|
|
24
|
+
} else if (result.length) {
|
|
25
|
+
result.at(-1)!.body.push(node);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function renderSteps() {
|
|
32
|
+
const Icon = resolveComponent("Icon");
|
|
33
|
+
return h(
|
|
34
|
+
"div",
|
|
35
|
+
{
|
|
36
|
+
class: cn("content-steps not-prose", props.class),
|
|
37
|
+
"data-mode": props.mode,
|
|
38
|
+
"data-appearance": appearance.value,
|
|
39
|
+
},
|
|
40
|
+
steps.value.map((step, index) =>
|
|
41
|
+
h("section", { key: index, class: "content-step" }, [
|
|
42
|
+
h("span", { class: "content-step-marker", "aria-hidden": "true" }, [
|
|
43
|
+
props.mode === "icons"
|
|
44
|
+
? h(Icon, { name: "lucide:check", class: "content-step-icon" })
|
|
45
|
+
: String(index + 1),
|
|
46
|
+
]),
|
|
47
|
+
h("div", { class: "content-step-body content-prose content-prose-trim" }, [
|
|
48
|
+
cloneVNode(step.heading, { class: "content-step-title" }),
|
|
49
|
+
...step.body,
|
|
50
|
+
]),
|
|
51
|
+
]),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
19
55
|
</script>
|
|
20
56
|
|
|
21
57
|
<template>
|
|
22
|
-
<
|
|
23
|
-
<slot />
|
|
24
|
-
</div>
|
|
58
|
+
<component :is="renderSteps" />
|
|
25
59
|
</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
|
|
14
|
+
<MdcCallout
|
|
15
|
+
:title="title"
|
|
16
|
+
:icon="icon"
|
|
17
|
+
type="success"
|
|
18
|
+
:appearance="appearance"
|
|
19
|
+
:class="props.class"
|
|
20
|
+
>
|
|
14
21
|
<slot />
|
|
15
22
|
</MdcCallout>
|
|
16
23
|
</template>
|
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { VNode } from "vue";
|
|
3
|
-
import { h, ref, resolveComponent, useSlots } from "vue";
|
|
3
|
+
import { cloneVNode, h, ref, resolveComponent, useId, useSlots } from "vue";
|
|
4
4
|
import { cn } from "../../utils";
|
|
5
|
+
import { useProseAppearance } from "../../composables/useProseAppearance";
|
|
5
6
|
import { resolveIconifyIcon } from "./icons";
|
|
6
7
|
|
|
7
8
|
const props = withDefaults(
|
|
8
9
|
defineProps<{
|
|
9
|
-
|
|
10
|
+
layout?: "separate" | "line";
|
|
11
|
+
appearance?: "quiet" | "tint";
|
|
10
12
|
padded?: boolean;
|
|
11
13
|
inStack?: boolean;
|
|
12
14
|
}>(),
|
|
13
15
|
{
|
|
14
|
-
|
|
16
|
+
layout: "separate",
|
|
15
17
|
padded: true,
|
|
16
18
|
inStack: false,
|
|
17
19
|
},
|
|
18
20
|
);
|
|
21
|
+
const appearance = useProseAppearance("tabs", () => props.appearance);
|
|
19
22
|
|
|
20
23
|
const slots = useSlots();
|
|
21
|
-
const
|
|
24
|
+
const activeIndex = ref(0);
|
|
25
|
+
const tabsId = useId();
|
|
22
26
|
|
|
23
27
|
type TabItem = {
|
|
24
28
|
icon?: string;
|
|
25
29
|
label: string;
|
|
26
30
|
node: VNode;
|
|
27
|
-
value: string;
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
function getTabLabel(node: VNode, index: number) {
|
|
@@ -44,16 +47,29 @@ function getItems(): TabItem[] {
|
|
|
44
47
|
icon: getTabIcon(node),
|
|
45
48
|
label: getTabLabel(node, index),
|
|
46
49
|
node,
|
|
47
|
-
value: getTabLabel(node, index),
|
|
48
50
|
}));
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
function handleKeydown(event: KeyboardEvent, items: TabItem[], currentIndex: number) {
|
|
54
|
+
let nextIndex: number | undefined;
|
|
55
|
+
if (event.key === "ArrowRight") nextIndex = (currentIndex + 1) % items.length;
|
|
56
|
+
if (event.key === "ArrowLeft") nextIndex = (currentIndex - 1 + items.length) % items.length;
|
|
57
|
+
if (event.key === "Home") nextIndex = 0;
|
|
58
|
+
if (event.key === "End") nextIndex = items.length - 1;
|
|
59
|
+
if (nextIndex === undefined) return;
|
|
60
|
+
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
activeIndex.value = nextIndex;
|
|
63
|
+
const buttons = (event.currentTarget as HTMLElement).parentElement?.querySelectorAll<HTMLElement>(
|
|
64
|
+
'[role="tab"]',
|
|
65
|
+
);
|
|
66
|
+
buttons?.[nextIndex]?.focus();
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
function renderTabs() {
|
|
52
70
|
const items = getItems();
|
|
53
71
|
const Icon = resolveComponent("Icon");
|
|
54
|
-
const
|
|
55
|
-
? activeValue.value
|
|
56
|
-
: activeValue.value || items[0]?.value || "";
|
|
72
|
+
const resolvedIndex = Math.min(activeIndex.value, items.length - 1);
|
|
57
73
|
|
|
58
74
|
if (!items.length) {
|
|
59
75
|
return null;
|
|
@@ -63,6 +79,9 @@ function renderTabs() {
|
|
|
63
79
|
"div",
|
|
64
80
|
{
|
|
65
81
|
class: cn("content-tabs not-prose group", props.inStack && "content-tabs-in-stack"),
|
|
82
|
+
"data-appearance": appearance.value,
|
|
83
|
+
"data-layout": props.layout,
|
|
84
|
+
"data-padded": props.padded ? "true" : "false",
|
|
66
85
|
},
|
|
67
86
|
[
|
|
68
87
|
h("div", { class: "content-tabs-header" }, [
|
|
@@ -72,21 +91,22 @@ function renderTabs() {
|
|
|
72
91
|
class: "content-tabs-list",
|
|
73
92
|
role: "tablist",
|
|
74
93
|
},
|
|
75
|
-
items.map((item) =>
|
|
94
|
+
items.map((item, index) =>
|
|
76
95
|
h(
|
|
77
96
|
"button",
|
|
78
97
|
{
|
|
79
|
-
key:
|
|
98
|
+
key: index,
|
|
80
99
|
type: "button",
|
|
81
100
|
role: "tab",
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
),
|
|
101
|
+
id: `${tabsId}-tab-${index}`,
|
|
102
|
+
"aria-controls": `${tabsId}-panel-${index}`,
|
|
103
|
+
"aria-selected": resolvedIndex === index,
|
|
104
|
+
tabindex: resolvedIndex === index ? 0 : -1,
|
|
105
|
+
class: cn("content-tabs-tab", resolvedIndex === index && "content-tabs-tab-active"),
|
|
87
106
|
onClick: () => {
|
|
88
|
-
|
|
107
|
+
activeIndex.value = index;
|
|
89
108
|
},
|
|
109
|
+
onKeydown: (event: KeyboardEvent) => handleKeydown(event, items, index),
|
|
90
110
|
},
|
|
91
111
|
[
|
|
92
112
|
item.icon
|
|
@@ -105,20 +125,18 @@ function renderTabs() {
|
|
|
105
125
|
h(
|
|
106
126
|
"div",
|
|
107
127
|
{ class: "content-tabs-panels" },
|
|
108
|
-
items.map((item) =>
|
|
128
|
+
items.map((item, index) =>
|
|
109
129
|
h(
|
|
110
130
|
"div",
|
|
111
131
|
{
|
|
112
|
-
key:
|
|
113
|
-
value: item.value,
|
|
132
|
+
key: index,
|
|
114
133
|
role: "tabpanel",
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
style: resolvedValue === item.value ? undefined : { display: "none" },
|
|
134
|
+
id: `${tabsId}-panel-${index}`,
|
|
135
|
+
"aria-labelledby": `${tabsId}-tab-${index}`,
|
|
136
|
+
class: cn("content-tabs-content-area content-prose content-prose-trim"),
|
|
137
|
+
style: resolvedIndex === index ? undefined : { display: "none" },
|
|
120
138
|
},
|
|
121
|
-
[
|
|
139
|
+
[cloneVNode(item.node, { inGroup: true })],
|
|
122
140
|
),
|
|
123
141
|
),
|
|
124
142
|
),
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from "vue";
|
|
3
3
|
import { cn } from "../../utils";
|
|
4
|
+
import { useProseAppearance } from "../../composables/useProseAppearance";
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
class?: HTMLAttributes["class"];
|
|
8
|
+
appearance?: "quiet" | "tint";
|
|
7
9
|
}>();
|
|
10
|
+
const appearance = useProseAppearance("timeline", () => props.appearance);
|
|
8
11
|
</script>
|
|
9
12
|
|
|
10
13
|
<template>
|
|
11
|
-
<div :class="cn('not-prose
|
|
14
|
+
<div :class="cn('content-timeline not-prose', props.class)" :data-appearance="appearance">
|
|
12
15
|
<slot />
|
|
13
16
|
</div>
|
|
14
17
|
</template>
|
|
@@ -1,48 +1,39 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from "vue";
|
|
3
|
+
import { computed } from "vue";
|
|
3
4
|
import { cn } from "../../utils";
|
|
5
|
+
import { resolveIconifyIcon } from "./icons";
|
|
4
6
|
|
|
5
7
|
const props = defineProps<{
|
|
6
8
|
date?: string;
|
|
7
9
|
label?: string;
|
|
8
10
|
title?: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
active?: boolean;
|
|
9
13
|
class?: HTMLAttributes["class"];
|
|
10
14
|
}>();
|
|
15
|
+
|
|
16
|
+
const iconName = computed(() => resolveIconifyIcon(props.icon));
|
|
11
17
|
</script>
|
|
12
18
|
|
|
13
19
|
<template>
|
|
14
|
-
<div :class="cn('
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
>
|
|
20
|
+
<div :class="cn('content-timeline-item', props.class)" :data-active="active || undefined">
|
|
21
|
+
<span class="content-timeline-marker" aria-hidden="true">
|
|
22
|
+
<Icon v-if="iconName" :name="iconName" class="content-timeline-marker-icon" />
|
|
23
|
+
<span class="content-timeline-marker-dot" />
|
|
24
|
+
</span>
|
|
25
|
+
<div v-if="title || date || label" class="content-timeline-head">
|
|
26
|
+
<p v-if="title" class="content-timeline-title">
|
|
27
|
+
{{ title }}
|
|
28
|
+
</p>
|
|
29
|
+
<span v-if="date" class="content-timeline-date">
|
|
25
30
|
{{ date }}
|
|
26
31
|
</span>
|
|
27
|
-
<span
|
|
28
|
-
v-if="label"
|
|
29
|
-
class="inline-flex items-center rounded-md bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary"
|
|
30
|
-
>
|
|
32
|
+
<span v-if="label" class="content-timeline-label">
|
|
31
33
|
{{ label }}
|
|
32
34
|
</span>
|
|
33
35
|
</div>
|
|
34
|
-
<
|
|
35
|
-
{{ title }}
|
|
36
|
-
</p>
|
|
37
|
-
<div
|
|
38
|
-
:class="
|
|
39
|
-
cn(
|
|
40
|
-
'content-prose content-prose-trim text-muted-foreground',
|
|
41
|
-
'[&_a]:font-medium [&_a]:text-foreground [&_a]:underline [&_a]:underline-offset-4',
|
|
42
|
-
'[&_p]:my-2 [&_p]:text-pretty [&_p:first-child]:mt-0 [&_p:last-child]:mb-0',
|
|
43
|
-
)
|
|
44
|
-
"
|
|
45
|
-
>
|
|
36
|
+
<div class="content-timeline-body content-prose content-prose-trim">
|
|
46
37
|
<slot />
|
|
47
38
|
</div>
|
|
48
39
|
</div>
|
|
@@ -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
|
|
14
|
+
<MdcCallout
|
|
15
|
+
:title="title"
|
|
16
|
+
:icon="icon"
|
|
17
|
+
type="warning"
|
|
18
|
+
:appearance="appearance"
|
|
19
|
+
:class="props.class"
|
|
20
|
+
>
|
|
14
21
|
<slot />
|
|
15
22
|
</MdcCallout>
|
|
16
23
|
</template>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export type ApiEntry = {
|
|
2
|
+
name: string;
|
|
3
|
+
annotation?: string;
|
|
4
|
+
optional?: boolean;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
deprecated?: boolean;
|
|
7
|
+
since?: string;
|
|
8
|
+
default?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ApiGroup = {
|
|
13
|
+
label: string;
|
|
14
|
+
entries: ApiEntry[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type InlinePart = {
|
|
18
|
+
code: boolean;
|
|
19
|
+
text: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function authoredError(message: string): never {
|
|
23
|
+
throw new TypeError(`Invalid API groups: ${message}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requiredText(value: unknown, path: string): string {
|
|
27
|
+
if (typeof value !== "string" || !value.trim())
|
|
28
|
+
authoredError(`${path} must be a non-empty string`);
|
|
29
|
+
return value.trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function optionalText(value: unknown, path: string): string | undefined {
|
|
33
|
+
if (value === undefined) return undefined;
|
|
34
|
+
return requiredText(value, path);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function optionalBoolean(value: unknown, path: string): boolean {
|
|
38
|
+
if (value === undefined) return false;
|
|
39
|
+
if (typeof value !== "boolean") authoredError(`${path} must be a boolean`);
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeApiGroups(input: unknown): ApiGroup[] {
|
|
44
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
45
|
+
authoredError("groups must be a non-empty array");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const labels = new Set<string>();
|
|
49
|
+
const ids = new Set<string>();
|
|
50
|
+
return input.map((group, groupIndex): ApiGroup => {
|
|
51
|
+
if (typeof group !== "object" || group === null || Array.isArray(group)) {
|
|
52
|
+
authoredError(`groups[${groupIndex}] must be an object`);
|
|
53
|
+
}
|
|
54
|
+
const raw = group as Record<string, unknown>;
|
|
55
|
+
const label = requiredText(raw.label, `groups[${groupIndex}].label`);
|
|
56
|
+
if (labels.has(label)) authoredError(`duplicate group label "${label}"`);
|
|
57
|
+
labels.add(label);
|
|
58
|
+
if (!Array.isArray(raw.entries) || raw.entries.length === 0) {
|
|
59
|
+
authoredError(`groups[${groupIndex}].entries must be a non-empty array`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const names = new Set<string>();
|
|
63
|
+
const entries = raw.entries.map((entry, entryIndex): ApiEntry => {
|
|
64
|
+
const path = `groups[${groupIndex}].entries[${entryIndex}]`;
|
|
65
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
66
|
+
authoredError(`${path} must be an object`);
|
|
67
|
+
}
|
|
68
|
+
const rawEntry = entry as Record<string, unknown>;
|
|
69
|
+
const name = requiredText(rawEntry.name, `${path}.name`);
|
|
70
|
+
if (names.has(name)) authoredError(`duplicate entry name "${name}" in group "${label}"`);
|
|
71
|
+
names.add(name);
|
|
72
|
+
const required = optionalBoolean(rawEntry.required, `${path}.required`);
|
|
73
|
+
const optional = optionalBoolean(rawEntry.optional, `${path}.optional`);
|
|
74
|
+
if (required && optional) authoredError(`${path} cannot be both required and optional`);
|
|
75
|
+
const id = apiEntryId(label, name);
|
|
76
|
+
if (ids.has(id)) authoredError(`duplicate generated entry id "${id}"`);
|
|
77
|
+
ids.add(id);
|
|
78
|
+
return {
|
|
79
|
+
name,
|
|
80
|
+
annotation: optionalText(rawEntry.annotation, `${path}.annotation`),
|
|
81
|
+
optional,
|
|
82
|
+
required,
|
|
83
|
+
deprecated: optionalBoolean(rawEntry.deprecated, `${path}.deprecated`),
|
|
84
|
+
since: optionalText(rawEntry.since, `${path}.since`),
|
|
85
|
+
default: optionalText(rawEntry.default, `${path}.default`),
|
|
86
|
+
description: optionalText(rawEntry.description, `${path}.description`),
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
return { label, entries };
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Everything after the entry name, built mechanically so authors never write
|
|
95
|
+
* signatures by hand: `?` + `: annotation` + ` = default`.
|
|
96
|
+
*/
|
|
97
|
+
export function signatureTail(entry: ApiEntry): string {
|
|
98
|
+
let tail = entry.optional ? "?" : "";
|
|
99
|
+
if (entry.annotation) tail += `: ${entry.annotation}`;
|
|
100
|
+
if (entry.default) tail += ` = ${entry.default}`;
|
|
101
|
+
return tail;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function slugify(value: string): string {
|
|
105
|
+
return value
|
|
106
|
+
.toLowerCase()
|
|
107
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
108
|
+
.replace(/^-+|-+$/g, "");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Stable deep-link id for an entry row, e.g. `api-flags-pm` for `--pm`. */
|
|
112
|
+
export function apiEntryId(groupLabel: string, name: string): string {
|
|
113
|
+
return `api-${slugify(groupLabel)}-${slugify(name)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Split a description string on backtick pairs so inline code renders as
|
|
118
|
+
* `<code>`. An unmatched backtick stays literal text.
|
|
119
|
+
*/
|
|
120
|
+
export function splitInlineCode(text: string): InlinePart[] {
|
|
121
|
+
const parts: InlinePart[] = [];
|
|
122
|
+
let rest = text;
|
|
123
|
+
for (;;) {
|
|
124
|
+
const match = /`([^`]+)`/.exec(rest);
|
|
125
|
+
if (!match) break;
|
|
126
|
+
if (match.index > 0) parts.push({ code: false, text: rest.slice(0, match.index) });
|
|
127
|
+
parts.push({ code: true, text: match[1]! });
|
|
128
|
+
rest = rest.slice(match.index + match[0].length);
|
|
129
|
+
}
|
|
130
|
+
if (rest) parts.push({ code: false, text: rest });
|
|
131
|
+
return parts;
|
|
132
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function validateCodeTreePaths(paths: string[], defaultValue?: string): void {
|
|
2
|
+
if (paths.length === 0) throw new TypeError("Code tree requires at least one labeled file");
|
|
3
|
+
|
|
4
|
+
const seen = new Set<string>();
|
|
5
|
+
for (const path of paths) {
|
|
6
|
+
const parts = path.split("/");
|
|
7
|
+
if (!path.trim() || parts.some((part) => !part.trim() || part === "." || part === "..")) {
|
|
8
|
+
throw new TypeError(`Invalid code tree path "${path}"`);
|
|
9
|
+
}
|
|
10
|
+
if (seen.has(path)) throw new TypeError(`Duplicate code tree path "${path}"`);
|
|
11
|
+
seen.add(path);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (const path of paths) {
|
|
15
|
+
if (paths.some((candidate) => candidate !== path && candidate.startsWith(`${path}/`))) {
|
|
16
|
+
throw new TypeError(`Code tree file "${path}" is also used as a directory`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (defaultValue !== undefined && !seen.has(defaultValue)) {
|
|
20
|
+
throw new TypeError(`Code tree defaultValue does not match a file: "${defaultValue}"`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type QuizOption = { text: string; correct: boolean };
|
|
2
|
+
export type QuizType = "single" | "multiple";
|
|
3
|
+
|
|
4
|
+
export function parseQuizOptions(input: unknown, type: unknown): QuizOption[] {
|
|
5
|
+
if (type !== "single" && type !== "multiple") {
|
|
6
|
+
throw new TypeError('Quiz type must be "single" or "multiple"');
|
|
7
|
+
}
|
|
8
|
+
if (!Array.isArray(input) || input.length < 2) {
|
|
9
|
+
throw new TypeError("Quiz options must contain at least two choices");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const labels = new Set<string>();
|
|
13
|
+
const options = input.map((value, index): QuizOption => {
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
15
|
+
throw new TypeError(`Quiz option ${index + 1} must be an object`);
|
|
16
|
+
}
|
|
17
|
+
const raw = value as Record<string, unknown>;
|
|
18
|
+
if (typeof raw.text !== "string" || !raw.text.trim()) {
|
|
19
|
+
throw new TypeError(`Quiz option ${index + 1} must have non-empty text`);
|
|
20
|
+
}
|
|
21
|
+
const text = raw.text.trim();
|
|
22
|
+
if (labels.has(text)) throw new TypeError(`Quiz option text must be unique: "${text}"`);
|
|
23
|
+
labels.add(text);
|
|
24
|
+
if (typeof raw.correct !== "boolean") {
|
|
25
|
+
throw new TypeError(`Quiz option ${index + 1}.correct must be a boolean`);
|
|
26
|
+
}
|
|
27
|
+
return { text, correct: raw.correct };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const correctCount = options.filter((option) => option.correct).length;
|
|
31
|
+
if (type === "single" && correctCount !== 1) {
|
|
32
|
+
throw new TypeError("A single-choice quiz must have exactly one correct option");
|
|
33
|
+
}
|
|
34
|
+
if (type === "multiple" && correctCount < 1) {
|
|
35
|
+
throw new TypeError("A multiple-choice quiz must have at least one correct option");
|
|
36
|
+
}
|
|
37
|
+
return options;
|
|
38
|
+
}
|
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from "vue";
|
|
3
|
+
import { computed, useAttrs } from "vue";
|
|
3
4
|
import { cn } from "../../utils";
|
|
5
|
+
import MdcCallout from "../mdc/MdcCallout.vue";
|
|
4
6
|
|
|
5
7
|
const props = defineProps<{
|
|
8
|
+
dataAlert?: "note" | "tip" | "important" | "warning" | "caution";
|
|
6
9
|
class?: HTMLAttributes["class"];
|
|
7
10
|
}>();
|
|
11
|
+
|
|
12
|
+
const attrs = useAttrs();
|
|
13
|
+
const alert = computed(() => props.dataAlert ?? attrs["data-alert"]);
|
|
14
|
+
const callout = computed(() => {
|
|
15
|
+
if (alert.value === "tip") return { type: "success" as const, title: "Tip" };
|
|
16
|
+
if (alert.value === "important") return { type: "info" as const, title: "Important" };
|
|
17
|
+
if (alert.value === "warning") return { type: "warning" as const, title: "Warning" };
|
|
18
|
+
if (alert.value === "caution") return { type: "error" as const, title: "Caution" };
|
|
19
|
+
return { type: "note" as const, title: "Note" };
|
|
20
|
+
});
|
|
8
21
|
</script>
|
|
9
22
|
|
|
10
23
|
<template>
|
|
11
|
-
<
|
|
24
|
+
<MdcCallout v-if="alert" :type="callout.type" :title="callout.title" :class="props.class">
|
|
25
|
+
<slot />
|
|
26
|
+
</MdcCallout>
|
|
27
|
+
<blockquote v-else :class="cn(props.class)">
|
|
12
28
|
<slot />
|
|
13
29
|
</blockquote>
|
|
14
30
|
</template>
|
|
@@ -9,7 +9,7 @@ const props = defineProps<{
|
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<template>
|
|
12
|
-
<h1 :id="id" :class="cn('scroll-mt-
|
|
12
|
+
<h1 :id="id" :class="cn('scroll-mt-[var(--content-scroll-margin)]', props.class)">
|
|
13
13
|
<a v-if="id" :href="`#${id}`" class="inline-flex items-center gap-2">
|
|
14
14
|
<slot />
|
|
15
15
|
</a>
|
|
@@ -9,14 +9,19 @@ const props = defineProps<{
|
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<template>
|
|
12
|
-
<h2
|
|
12
|
+
<h2
|
|
13
|
+
:id="id"
|
|
14
|
+
:class="
|
|
15
|
+
cn('flex scroll-mt-[var(--content-scroll-margin)] flex-row items-center gap-1.5', props.class)
|
|
16
|
+
"
|
|
17
|
+
>
|
|
13
18
|
<a v-if="id" :href="`#${id}`" class="peer" data-card="">
|
|
14
19
|
<slot />
|
|
15
20
|
</a>
|
|
16
21
|
<Icon
|
|
17
22
|
v-if="id"
|
|
18
23
|
name="lucide:link"
|
|
19
|
-
class="size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity peer-hover:opacity-100"
|
|
24
|
+
class="size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity peer-hover:opacity-100 peer-focus-visible:opacity-100"
|
|
20
25
|
aria-hidden="true"
|
|
21
26
|
/>
|
|
22
27
|
<slot v-else />
|
|
@@ -9,14 +9,19 @@ const props = defineProps<{
|
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<template>
|
|
12
|
-
<h3
|
|
12
|
+
<h3
|
|
13
|
+
:id="id"
|
|
14
|
+
:class="
|
|
15
|
+
cn('flex scroll-mt-[var(--content-scroll-margin)] flex-row items-center gap-1.5', props.class)
|
|
16
|
+
"
|
|
17
|
+
>
|
|
13
18
|
<a v-if="id" :href="`#${id}`" class="peer" data-card="">
|
|
14
19
|
<slot />
|
|
15
20
|
</a>
|
|
16
21
|
<Icon
|
|
17
22
|
v-if="id"
|
|
18
23
|
name="lucide:link"
|
|
19
|
-
class="size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity peer-hover:opacity-100"
|
|
24
|
+
class="size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity peer-hover:opacity-100 peer-focus-visible:opacity-100"
|
|
20
25
|
aria-hidden="true"
|
|
21
26
|
/>
|
|
22
27
|
<slot v-else />
|
|
@@ -3,12 +3,27 @@ import type { HTMLAttributes } from "vue";
|
|
|
3
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
|
-
<h4
|
|
12
|
-
|
|
12
|
+
<h4
|
|
13
|
+
:id="id"
|
|
14
|
+
:class="
|
|
15
|
+
cn('flex scroll-mt-[var(--content-scroll-margin)] flex-row items-center gap-1.5', props.class)
|
|
16
|
+
"
|
|
17
|
+
>
|
|
18
|
+
<a v-if="id" :href="`#${id}`" class="peer" data-card="">
|
|
19
|
+
<slot />
|
|
20
|
+
</a>
|
|
21
|
+
<Icon
|
|
22
|
+
v-if="id"
|
|
23
|
+
name="lucide:link"
|
|
24
|
+
class="size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity peer-hover:opacity-100 peer-focus-visible:opacity-100"
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
/>
|
|
27
|
+
<slot v-else />
|
|
13
28
|
</h4>
|
|
14
29
|
</template>
|