specra 0.1.13 → 0.2.0
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/LICENSE.MD +25 -4
- package/README.md +67 -58
- package/config/specra.config.schema.json +16 -0
- package/config/svelte-config.js +63 -0
- package/dist/api-parser.types.d.ts +59 -0
- package/dist/api-parser.types.js +5 -0
- package/dist/api.types.d.ts +137 -0
- package/dist/api.types.js +5 -0
- package/dist/category.d.ts +21 -0
- package/dist/category.js +48 -0
- package/dist/components/ConfigProvider.svelte +13 -0
- package/dist/components/ConfigProvider.svelte.d.ts +31 -0
- package/dist/components/docs/Accordion.svelte +18 -0
- package/dist/components/docs/Accordion.svelte.d.ts +10 -0
- package/dist/components/docs/AccordionItem.svelte +41 -0
- package/dist/components/docs/AccordionItem.svelte.d.ts +10 -0
- package/dist/components/docs/Badge.svelte +28 -0
- package/dist/components/docs/Badge.svelte.d.ts +9 -0
- package/dist/components/docs/Breadcrumb.svelte +80 -0
- package/dist/components/docs/Breadcrumb.svelte.d.ts +8 -0
- package/dist/components/docs/Callout.svelte +96 -0
- package/dist/components/docs/Callout.svelte.d.ts +10 -0
- package/dist/components/docs/Card.svelte +63 -0
- package/dist/components/docs/Card.svelte.d.ts +12 -0
- package/dist/components/docs/CardGrid.svelte +24 -0
- package/dist/components/docs/CardGrid.svelte.d.ts +8 -0
- package/dist/components/docs/CategoryIndex.svelte +110 -0
- package/dist/components/docs/CategoryIndex.svelte.d.ts +29 -0
- package/dist/components/docs/CodeBlock.svelte +172 -0
- package/dist/components/docs/CodeBlock.svelte.d.ts +8 -0
- package/dist/components/docs/Column.svelte +25 -0
- package/dist/components/docs/Column.svelte.d.ts +8 -0
- package/dist/components/docs/Columns.svelte +38 -0
- package/dist/components/docs/Columns.svelte.d.ts +13 -0
- package/dist/components/docs/DevModeBadge.svelte +15 -0
- package/dist/components/docs/DevModeBadge.svelte.d.ts +18 -0
- package/dist/components/docs/DocBadge.svelte +28 -0
- package/dist/components/docs/DocBadge.svelte.d.ts +9 -0
- package/dist/components/docs/DocLayout.svelte +107 -0
- package/dist/components/docs/DocLayout.svelte.d.ts +32 -0
- package/dist/components/docs/DocLoading.svelte +53 -0
- package/dist/components/docs/DocLoading.svelte.d.ts +18 -0
- package/dist/components/docs/DocMetadata.svelte +106 -0
- package/dist/components/docs/DocMetadata.svelte.d.ts +18 -0
- package/dist/components/docs/DocNavigation.svelte +56 -0
- package/dist/components/docs/DocNavigation.svelte.d.ts +12 -0
- package/dist/components/docs/DocTags.svelte +22 -0
- package/dist/components/docs/DocTags.svelte.d.ts +6 -0
- package/dist/components/docs/DraftBadge.svelte +10 -0
- package/dist/components/docs/DraftBadge.svelte.d.ts +18 -0
- package/dist/components/docs/Footer.svelte +72 -0
- package/dist/components/docs/Footer.svelte.d.ts +7 -0
- package/dist/components/docs/Frame.svelte +27 -0
- package/dist/components/docs/Frame.svelte.d.ts +9 -0
- package/dist/components/docs/Header.svelte +123 -0
- package/dist/components/docs/Header.svelte.d.ts +9 -0
- package/dist/components/docs/HeaderWithMenu.svelte +34 -0
- package/dist/components/docs/HeaderWithMenu.svelte.d.ts +17 -0
- package/dist/components/docs/HotReloadIndicator.svelte +44 -0
- package/dist/components/docs/HotReloadIndicator.svelte.d.ts +3 -0
- package/dist/components/docs/Icon.svelte +103 -0
- package/dist/components/docs/Icon.svelte.d.ts +11 -0
- package/dist/components/docs/Image.svelte +88 -0
- package/dist/components/docs/Image.svelte.d.ts +11 -0
- package/dist/components/docs/ImageCard.svelte +91 -0
- package/dist/components/docs/ImageCard.svelte.d.ts +12 -0
- package/dist/components/docs/ImageCardGrid.svelte +25 -0
- package/dist/components/docs/ImageCardGrid.svelte.d.ts +8 -0
- package/dist/components/docs/LayoutProviders.svelte +57 -0
- package/dist/components/docs/LayoutProviders.svelte.d.ts +9 -0
- package/dist/components/docs/Logo.svelte +25 -0
- package/dist/components/docs/Logo.svelte.d.ts +11 -0
- package/dist/components/docs/Math.svelte +54 -0
- package/dist/components/docs/Math.svelte.d.ts +7 -0
- package/dist/components/docs/MdxContent.svelte +41 -0
- package/dist/components/docs/MdxHotReload.svelte +78 -0
- package/dist/components/docs/MdxHotReload.svelte.d.ts +9 -0
- package/dist/components/docs/MdxLayout.svelte +16 -0
- package/dist/components/docs/MdxLayout.svelte.d.ts +6 -0
- package/dist/components/docs/Mermaid.svelte +88 -0
- package/dist/components/docs/Mermaid.svelte.d.ts +7 -0
- package/dist/components/docs/MobileDocLayout.svelte +211 -0
- package/dist/components/docs/MobileDocLayout.svelte.d.ts +35 -0
- package/dist/components/docs/MobileSidebar.svelte +122 -0
- package/dist/components/docs/MobileSidebar.svelte.d.ts +31 -0
- package/dist/components/docs/MobileSidebarWrapper.svelte +122 -0
- package/dist/components/docs/MobileSidebarWrapper.svelte.d.ts +32 -0
- package/dist/components/docs/NotFoundContent.svelte +40 -0
- package/dist/components/docs/NotFoundContent.svelte.d.ts +6 -0
- package/dist/components/docs/SearchHighlight.svelte +116 -0
- package/dist/components/docs/SearchHighlight.svelte.d.ts +3 -0
- package/dist/components/docs/SearchModal.svelte +239 -0
- package/dist/components/docs/SearchModal.svelte.d.ts +9 -0
- package/dist/components/docs/Sidebar.svelte +69 -0
- package/dist/components/docs/Sidebar.svelte.d.ts +31 -0
- package/dist/components/docs/SidebarMenuItems.svelte +344 -0
- package/dist/components/docs/SidebarMenuItems.svelte.d.ts +33 -0
- package/dist/components/docs/SidebarSkeleton.svelte +50 -0
- package/dist/components/docs/SidebarSkeleton.svelte.d.ts +18 -0
- package/dist/components/docs/SiteBanner.svelte +92 -0
- package/dist/components/docs/SiteBanner.svelte.d.ts +7 -0
- package/dist/components/docs/Step.svelte +44 -0
- package/dist/components/docs/Step.svelte.d.ts +8 -0
- package/dist/components/docs/Steps.svelte +15 -0
- package/dist/components/docs/Steps.svelte.d.ts +7 -0
- package/dist/components/docs/Tab.svelte +40 -0
- package/dist/components/docs/Tab.svelte.d.ts +8 -0
- package/dist/components/docs/TabGroups.svelte +183 -0
- package/dist/components/docs/TabGroups.svelte.d.ts +25 -0
- package/dist/components/docs/TableOfContents.svelte +100 -0
- package/dist/components/docs/TableOfContents.svelte.d.ts +9 -0
- package/dist/components/docs/Tabs.svelte +69 -0
- package/dist/components/docs/Tabs.svelte.d.ts +8 -0
- package/dist/components/docs/ThemeToggle.svelte +16 -0
- package/dist/components/docs/ThemeToggle.svelte.d.ts +18 -0
- package/dist/components/docs/Tooltip.svelte +44 -0
- package/dist/components/docs/Tooltip.svelte.d.ts +10 -0
- package/dist/components/docs/VersionSwitcher.svelte +95 -0
- package/dist/components/docs/VersionSwitcher.svelte.d.ts +7 -0
- package/dist/components/docs/Video.svelte +84 -0
- package/dist/components/docs/Video.svelte.d.ts +12 -0
- package/dist/components/docs/api/ApiEndpoint.svelte +61 -0
- package/dist/components/docs/api/ApiEndpoint.svelte.d.ts +11 -0
- package/dist/components/docs/api/ApiParams.svelte +80 -0
- package/dist/components/docs/api/ApiParams.svelte.d.ts +14 -0
- package/dist/components/docs/api/ApiPlayground.svelte +259 -0
- package/dist/components/docs/api/ApiPlayground.svelte.d.ts +16 -0
- package/dist/components/docs/api/ApiReference.svelte +278 -0
- package/dist/components/docs/api/ApiReference.svelte.d.ts +23 -0
- package/dist/components/docs/api/ApiResponse.svelte +66 -0
- package/dist/components/docs/api/ApiResponse.svelte.d.ts +9 -0
- package/dist/components/docs/api/index.d.ts +5 -0
- package/dist/components/docs/api/index.js +5 -0
- package/dist/components/docs/componentTextProps.d.ts +3 -0
- package/dist/components/docs/componentTextProps.js +61 -0
- package/dist/components/docs/index.d.ts +54 -0
- package/dist/components/docs/index.js +56 -0
- package/dist/components/global/VersionNotFound.svelte +48 -0
- package/dist/components/global/VersionNotFound.svelte.d.ts +7 -0
- package/dist/components/global/index.d.ts +1 -0
- package/dist/components/global/index.js +1 -0
- package/dist/components/index.d.ts +6 -822
- package/dist/components/index.js +11 -3854
- package/dist/components/ui/Badge.svelte +48 -0
- package/dist/components/ui/Badge.svelte.d.ts +15 -0
- package/dist/components/ui/Button.svelte +58 -0
- package/dist/components/ui/Button.svelte.d.ts +17 -0
- package/dist/components/ui/Dialog.svelte +16 -0
- package/dist/components/ui/Dialog.svelte.d.ts +9 -0
- package/dist/components/ui/DialogClose.svelte +16 -0
- package/dist/components/ui/DialogClose.svelte.d.ts +9 -0
- package/dist/components/ui/DialogContent.svelte +43 -0
- package/dist/components/ui/DialogContent.svelte.d.ts +10 -0
- package/dist/components/ui/DialogDescription.svelte +21 -0
- package/dist/components/ui/DialogDescription.svelte.d.ts +9 -0
- package/dist/components/ui/DialogFooter.svelte +20 -0
- package/dist/components/ui/DialogFooter.svelte.d.ts +9 -0
- package/dist/components/ui/DialogHeader.svelte +20 -0
- package/dist/components/ui/DialogHeader.svelte.d.ts +9 -0
- package/dist/components/ui/DialogTitle.svelte +21 -0
- package/dist/components/ui/DialogTitle.svelte.d.ts +9 -0
- package/dist/components/ui/Input.svelte +23 -0
- package/dist/components/ui/Input.svelte.d.ts +8 -0
- package/dist/components/ui/Textarea.svelte +19 -0
- package/dist/components/ui/Textarea.svelte.d.ts +7 -0
- package/dist/components/ui/index.d.ts +11 -0
- package/dist/components/ui/index.js +11 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +9 -0
- package/dist/config.schema.json +471 -0
- package/dist/config.server.d.ts +46 -0
- package/dist/config.server.js +149 -0
- package/dist/{mdx-ColN3Cyg.d.mts → config.types.d.ts} +22 -75
- package/dist/config.types.js +39 -0
- package/dist/dev-utils.d.ts +29 -0
- package/dist/dev-utils.js +63 -0
- package/dist/index.d.ts +19 -4
- package/dist/index.js +25 -4861
- package/dist/mdx-cache.d.ts +41 -0
- package/dist/mdx-cache.js +160 -0
- package/dist/mdx-components.js +50 -1931
- package/dist/mdx-security.d.ts +76 -0
- package/dist/mdx-security.js +217 -0
- package/dist/mdx.d.ts +73 -0
- package/dist/mdx.js +1099 -0
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +2 -0
- package/dist/middleware/security.d.ts +22 -47
- package/dist/middleware/security.js +111 -137
- package/dist/parsers/base-parser.d.ts +14 -0
- package/dist/parsers/base-parser.js +1 -0
- package/dist/parsers/index.d.ts +16 -0
- package/dist/parsers/index.js +51 -0
- package/dist/parsers/openapi-parser.d.ts +18 -0
- package/dist/parsers/openapi-parser.js +209 -0
- package/dist/parsers/postman-parser.d.ts +20 -0
- package/dist/parsers/postman-parser.js +260 -0
- package/dist/parsers/specra-parser.d.ts +10 -0
- package/dist/parsers/specra-parser.js +18 -0
- package/dist/redirects.d.ts +12 -0
- package/dist/redirects.js +30 -0
- package/dist/remark-code-meta.d.ts +6 -0
- package/dist/remark-code-meta.js +21 -0
- package/dist/sidebar-utils.d.ts +59 -0
- package/dist/sidebar-utils.js +144 -0
- package/dist/stores/config.d.ts +20 -0
- package/dist/stores/config.js +45 -0
- package/dist/stores/index.d.ts +4 -0
- package/dist/stores/index.js +4 -0
- package/dist/stores/sidebar.d.ts +7 -0
- package/dist/stores/sidebar.js +12 -0
- package/dist/stores/tabs.d.ts +6 -0
- package/dist/stores/tabs.js +41 -0
- package/dist/stores/theme.d.ts +7 -0
- package/dist/stores/theme.js +75 -0
- package/dist/{styles.css → styles/globals.css} +136 -6
- package/dist/toc.d.ts +9 -0
- package/dist/toc.js +15 -0
- package/dist/utils.d.ts +13 -0
- package/dist/utils.js +30 -0
- package/package.json +47 -90
- package/dist/app/api/mdx-watch/route.d.mts +0 -10
- package/dist/app/api/mdx-watch/route.d.ts +0 -10
- package/dist/app/api/mdx-watch/route.js +0 -118
- package/dist/app/api/mdx-watch/route.js.map +0 -1
- package/dist/app/api/mdx-watch/route.mjs +0 -91
- package/dist/app/api/mdx-watch/route.mjs.map +0 -1
- package/dist/chunk-6S3EJVEO.mjs +0 -259
- package/dist/chunk-6S3EJVEO.mjs.map +0 -1
- package/dist/chunk-BE7EROIW.mjs +0 -212
- package/dist/chunk-BE7EROIW.mjs.map +0 -1
- package/dist/chunk-CWHRZHZO.mjs +0 -168
- package/dist/chunk-CWHRZHZO.mjs.map +0 -1
- package/dist/chunk-D5VDVYFY.mjs +0 -1325
- package/dist/chunk-D5VDVYFY.mjs.map +0 -1
- package/dist/chunk-WMCO2UX5.mjs +0 -585
- package/dist/chunk-WMCO2UX5.mjs.map +0 -1
- package/dist/chunk-XEMGCPZZ.mjs +0 -475
- package/dist/chunk-XEMGCPZZ.mjs.map +0 -1
- package/dist/components/index.d.mts +0 -822
- package/dist/components/index.js.map +0 -1
- package/dist/components/index.mjs +0 -3741
- package/dist/components/index.mjs.map +0 -1
- package/dist/index.d.mts +0 -4
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -1897
- package/dist/index.mjs.map +0 -1
- package/dist/layouts/index.d.mts +0 -34
- package/dist/layouts/index.d.ts +0 -34
- package/dist/layouts/index.js +0 -453
- package/dist/layouts/index.js.map +0 -1
- package/dist/layouts/index.mjs +0 -173
- package/dist/layouts/index.mjs.map +0 -1
- package/dist/lib/index.d.mts +0 -583
- package/dist/lib/index.d.ts +0 -583
- package/dist/lib/index.js +0 -1595
- package/dist/lib/index.js.map +0 -1
- package/dist/lib/index.mjs +0 -111
- package/dist/lib/index.mjs.map +0 -1
- package/dist/mdx-ColN3Cyg.d.ts +0 -352
- package/dist/mdx-components.d.mts +0 -86
- package/dist/mdx-components.d.ts +0 -86
- package/dist/mdx-components.js.map +0 -1
- package/dist/mdx-components.mjs +0 -206
- package/dist/mdx-components.mjs.map +0 -1
- package/dist/middleware/security.d.mts +0 -82
- package/dist/middleware/security.js.map +0 -1
- package/dist/middleware/security.mjs +0 -84
- package/dist/middleware/security.mjs.map +0 -1
- package/dist/styles.css.map +0 -1
- package/dist/styles.d.mts +0 -2
- package/dist/styles.d.ts +0 -2
- package/dist/styles.js +0 -2
- package/dist/styles.js.map +0 -1
- package/dist/styles.mjs +0 -1
- package/dist/styles.mjs.map +0 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ChevronDown } from 'lucide-svelte';
|
|
3
|
+
import { goto } from '$app/navigation';
|
|
4
|
+
import type { TabGroup, SpecraConfig } from '../../config.types.js';
|
|
5
|
+
import Icon from './Icon.svelte';
|
|
6
|
+
|
|
7
|
+
interface DocItem {
|
|
8
|
+
slug: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
meta?: {
|
|
11
|
+
tab_group?: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
};
|
|
15
|
+
tabGroup?: string;
|
|
16
|
+
categoryTabGroup?: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
tabGroups: TabGroup[];
|
|
22
|
+
activeTabId?: string;
|
|
23
|
+
onTabChange?: (tabId: string) => void;
|
|
24
|
+
mobileOnly?: boolean;
|
|
25
|
+
docs?: DocItem[];
|
|
26
|
+
version?: string;
|
|
27
|
+
flush?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let { tabGroups, activeTabId, onTabChange, mobileOnly = false, docs, version, flush = false }: Props = $props();
|
|
31
|
+
|
|
32
|
+
let dropdownOpen = $state(false);
|
|
33
|
+
|
|
34
|
+
// Filter out tabs that have no associated docs
|
|
35
|
+
let filteredTabGroups = $derived.by(() => {
|
|
36
|
+
if (!docs) return tabGroups;
|
|
37
|
+
return tabGroups.filter((tab) => {
|
|
38
|
+
const hasDocsInTab = docs.some((doc) => {
|
|
39
|
+
const docTabGroup = doc.meta?.tab_group || doc.categoryTabGroup;
|
|
40
|
+
return docTabGroup === tab.id || (!docTabGroup && tab.id === tabGroups[0]?.id);
|
|
41
|
+
});
|
|
42
|
+
return hasDocsInTab;
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let activeTab = $derived(activeTabId || filteredTabGroups[0]?.id || '');
|
|
47
|
+
let activeTabData = $derived(filteredTabGroups.find((tab) => tab.id === activeTab));
|
|
48
|
+
|
|
49
|
+
function handleTabChange(tabId: string) {
|
|
50
|
+
onTabChange?.(tabId);
|
|
51
|
+
dropdownOpen = false;
|
|
52
|
+
|
|
53
|
+
// Navigate to the first item in the new tab group if docs are provided
|
|
54
|
+
if (docs && version) {
|
|
55
|
+
const firstDocInTab = docs.find((doc) => {
|
|
56
|
+
const docTabGroup = doc.meta?.tab_group || doc.categoryTabGroup;
|
|
57
|
+
return docTabGroup === tabId || (!docTabGroup && tabId === filteredTabGroups[0]?.id);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (firstDocInTab) {
|
|
61
|
+
goto(`/docs/${version}/${firstDocInTab.slug}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
{#if filteredTabGroups.length > 0}
|
|
68
|
+
{#if mobileOnly}
|
|
69
|
+
<!-- Mobile only version (for sidebar) -->
|
|
70
|
+
<div class="relative">
|
|
71
|
+
<button
|
|
72
|
+
onclick={() => (dropdownOpen = !dropdownOpen)}
|
|
73
|
+
class="flex items-center justify-between w-full px-3 py-2 text-sm font-medium text-foreground bg-muted/50 rounded-lg hover:bg-muted transition-colors"
|
|
74
|
+
aria-label="Select tab group"
|
|
75
|
+
aria-expanded={dropdownOpen}
|
|
76
|
+
>
|
|
77
|
+
<div class="flex items-center gap-2">
|
|
78
|
+
{#if activeTabData?.icon}
|
|
79
|
+
<Icon icon={activeTabData.icon} size={16} class="shrink-0" />
|
|
80
|
+
{/if}
|
|
81
|
+
<span>{activeTabData?.label}</span>
|
|
82
|
+
</div>
|
|
83
|
+
<ChevronDown
|
|
84
|
+
class="h-4 w-4 transition-transform {dropdownOpen ? 'rotate-180' : ''}"
|
|
85
|
+
/>
|
|
86
|
+
</button>
|
|
87
|
+
|
|
88
|
+
{#if dropdownOpen}
|
|
89
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
90
|
+
<div
|
|
91
|
+
class="fixed inset-0 z-40"
|
|
92
|
+
onclick={() => (dropdownOpen = false)}
|
|
93
|
+
onkeydown={(e) => { if (e.key === 'Escape') dropdownOpen = false; }}
|
|
94
|
+
></div>
|
|
95
|
+
<div class="absolute top-full left-0 right-0 mt-2 bg-background border border-border rounded-lg shadow-lg z-50 max-h-[60vh] overflow-y-auto">
|
|
96
|
+
{#each filteredTabGroups as tab}
|
|
97
|
+
{@const isActive = tab.id === activeTab}
|
|
98
|
+
<button
|
|
99
|
+
onclick={() => handleTabChange(tab.id)}
|
|
100
|
+
class="flex items-center gap-2 w-full px-3 py-2 text-sm font-medium text-left transition-colors first:rounded-t-lg last:rounded-b-lg {isActive
|
|
101
|
+
? 'bg-primary/10 text-primary'
|
|
102
|
+
: 'text-muted-foreground hover:bg-accent hover:text-foreground'}"
|
|
103
|
+
>
|
|
104
|
+
{#if tab.icon}
|
|
105
|
+
<Icon icon={tab.icon} size={16} class="shrink-0" />
|
|
106
|
+
{/if}
|
|
107
|
+
{tab.label}
|
|
108
|
+
</button>
|
|
109
|
+
{/each}
|
|
110
|
+
</div>
|
|
111
|
+
{/if}
|
|
112
|
+
</div>
|
|
113
|
+
{:else}
|
|
114
|
+
<!-- Full version with sticky header -->
|
|
115
|
+
<div class="sticky top-16 z-30 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
116
|
+
<div class="{flush ? '' : 'container mx-auto'} px-4 md:px-6">
|
|
117
|
+
<!-- Mobile Dropdown -->
|
|
118
|
+
<div class="md:hidden relative">
|
|
119
|
+
<button
|
|
120
|
+
onclick={() => (dropdownOpen = !dropdownOpen)}
|
|
121
|
+
class="flex items-center justify-between w-full px-4 py-3 text-sm font-medium text-foreground"
|
|
122
|
+
aria-label="Select tab"
|
|
123
|
+
aria-expanded={dropdownOpen}
|
|
124
|
+
>
|
|
125
|
+
<div class="flex items-center gap-2">
|
|
126
|
+
{#if activeTabData?.icon}
|
|
127
|
+
<Icon icon={activeTabData.icon} size={16} class="shrink-0" />
|
|
128
|
+
{/if}
|
|
129
|
+
{activeTabData?.label}
|
|
130
|
+
</div>
|
|
131
|
+
<ChevronDown
|
|
132
|
+
class="h-4 w-4 transition-transform {dropdownOpen ? 'rotate-180' : ''}"
|
|
133
|
+
/>
|
|
134
|
+
</button>
|
|
135
|
+
|
|
136
|
+
{#if dropdownOpen}
|
|
137
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
138
|
+
<div
|
|
139
|
+
class="fixed inset-0 z-40"
|
|
140
|
+
onclick={() => (dropdownOpen = false)}
|
|
141
|
+
onkeydown={(e) => { if (e.key === 'Escape') dropdownOpen = false; }}
|
|
142
|
+
></div>
|
|
143
|
+
<div class="absolute top-full left-0 right-0 bg-background border border-border shadow-lg z-50 max-h-[60vh] overflow-y-auto">
|
|
144
|
+
{#each filteredTabGroups as tab}
|
|
145
|
+
{@const isActive = tab.id === activeTab}
|
|
146
|
+
<button
|
|
147
|
+
onclick={() => handleTabChange(tab.id)}
|
|
148
|
+
class="flex items-center gap-2 w-full px-4 py-3 text-sm font-medium text-left transition-colors {isActive
|
|
149
|
+
? 'bg-primary/10 text-primary'
|
|
150
|
+
: 'text-muted-foreground hover:bg-accent hover:text-foreground'}"
|
|
151
|
+
>
|
|
152
|
+
{#if tab.icon}
|
|
153
|
+
<Icon icon={tab.icon} size={16} class="shrink-0" />
|
|
154
|
+
{/if}
|
|
155
|
+
{tab.label}
|
|
156
|
+
</button>
|
|
157
|
+
{/each}
|
|
158
|
+
</div>
|
|
159
|
+
{/if}
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<!-- Desktop Tabs -->
|
|
163
|
+
<nav class="hidden md:flex gap-1" aria-label="Documentation tabs">
|
|
164
|
+
{#each filteredTabGroups as tab}
|
|
165
|
+
{@const isActive = tab.id === activeTab}
|
|
166
|
+
<button
|
|
167
|
+
onclick={() => handleTabChange(tab.id)}
|
|
168
|
+
class="flex items-center gap-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-all border-b-2 {isActive
|
|
169
|
+
? 'border-primary text-primary'
|
|
170
|
+
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'}"
|
|
171
|
+
aria-current={isActive ? 'page' : undefined}
|
|
172
|
+
>
|
|
173
|
+
{#if tab.icon}
|
|
174
|
+
<Icon icon={tab.icon} size={16} class="shrink-0" />
|
|
175
|
+
{/if}
|
|
176
|
+
{tab.label}
|
|
177
|
+
</button>
|
|
178
|
+
{/each}
|
|
179
|
+
</nav>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
{/if}
|
|
183
|
+
{/if}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TabGroup } from '../../config.types.js';
|
|
2
|
+
interface DocItem {
|
|
3
|
+
slug: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
meta?: {
|
|
6
|
+
tab_group?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
tabGroup?: string;
|
|
11
|
+
categoryTabGroup?: string;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
interface Props {
|
|
15
|
+
tabGroups: TabGroup[];
|
|
16
|
+
activeTabId?: string;
|
|
17
|
+
onTabChange?: (tabId: string) => void;
|
|
18
|
+
mobileOnly?: boolean;
|
|
19
|
+
docs?: DocItem[];
|
|
20
|
+
version?: string;
|
|
21
|
+
flush?: boolean;
|
|
22
|
+
}
|
|
23
|
+
declare const TabGroups: import("svelte").Component<Props, {}, "">;
|
|
24
|
+
type TabGroups = ReturnType<typeof TabGroups>;
|
|
25
|
+
export default TabGroups;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { browser } from '$app/environment';
|
|
3
|
+
import type { SpecraConfig } from '../../config.types.js';
|
|
4
|
+
import type { TOCItem } from '../../toc.js';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
items: TOCItem[];
|
|
8
|
+
config: SpecraConfig;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { items, config }: Props = $props();
|
|
12
|
+
|
|
13
|
+
let activeId = $state('');
|
|
14
|
+
|
|
15
|
+
const showToc = $derived(config.navigation?.showTableOfContents !== false);
|
|
16
|
+
const maxDepth = $derived(config.navigation?.tocMaxDepth ?? 3);
|
|
17
|
+
const filteredItems = $derived(items.filter((item) => item.level <= maxDepth));
|
|
18
|
+
|
|
19
|
+
// Check if tab groups are configured
|
|
20
|
+
const hasTabGroups = $derived(
|
|
21
|
+
config.navigation?.tabGroups != null && config.navigation.tabGroups.length > 0
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Adjust top position based on whether tabs are present
|
|
25
|
+
const stickyTop = $derived(hasTabGroups ? 'top-[7.5rem]' : 'top-24');
|
|
26
|
+
const maxHeight = $derived(hasTabGroups ? 'max-h-[calc(100vh-10rem)]' : 'max-h-[calc(100vh-7rem)]');
|
|
27
|
+
|
|
28
|
+
$effect(() => {
|
|
29
|
+
if (!browser || !showToc || filteredItems.length === 0) return;
|
|
30
|
+
|
|
31
|
+
const observer = new IntersectionObserver(
|
|
32
|
+
(entries) => {
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (entry.isIntersecting) {
|
|
35
|
+
activeId = entry.target.id;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
rootMargin: '-80px 0px -80% 0px',
|
|
42
|
+
threshold: 0,
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Observe all heading elements
|
|
47
|
+
const headingElements = filteredItems
|
|
48
|
+
.map((item) => document.getElementById(item.id))
|
|
49
|
+
.filter(Boolean) as HTMLElement[];
|
|
50
|
+
|
|
51
|
+
headingElements.forEach((el) => observer.observe(el));
|
|
52
|
+
|
|
53
|
+
return () => {
|
|
54
|
+
headingElements.forEach((el) => observer.unobserve(el));
|
|
55
|
+
observer.disconnect();
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function handleClick(e: MouseEvent, id: string) {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
const element = document.getElementById(id);
|
|
62
|
+
if (element) {
|
|
63
|
+
const offset = 100;
|
|
64
|
+
const elementPosition = element.getBoundingClientRect().top;
|
|
65
|
+
const offsetPosition = elementPosition + window.scrollY - offset;
|
|
66
|
+
|
|
67
|
+
window.scrollTo({
|
|
68
|
+
top: offsetPosition,
|
|
69
|
+
behavior: 'smooth',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Update URL without jumping
|
|
73
|
+
window.history.replaceState(null, '', `#${id}`);
|
|
74
|
+
|
|
75
|
+
// Manually set active ID after scroll
|
|
76
|
+
activeId = id;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
{#if showToc && filteredItems.length > 0}
|
|
82
|
+
<aside class="w-64 hidden xl:block shrink-0 sticky {stickyTop} self-start">
|
|
83
|
+
<div class="{maxHeight} overflow-y-auto bg-muted/30 dark:bg-muted/10 rounded-2xl p-4 border border-border/50">
|
|
84
|
+
<h3 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-4 px-2">On this page</h3>
|
|
85
|
+
<nav class="space-y-1">
|
|
86
|
+
{#each filteredItems as item, index}
|
|
87
|
+
<a
|
|
88
|
+
href="#{item.id}"
|
|
89
|
+
onclick={(e) => handleClick(e, item.id)}
|
|
90
|
+
class="block text-sm transition-all cursor-pointer rounded-xl px-3 py-2 {item.level === 3 ? 'ml-3' : ''} {activeId === item.id
|
|
91
|
+
? 'text-primary font-medium'
|
|
92
|
+
: 'text-foreground hover:bg-accent/50'}"
|
|
93
|
+
>
|
|
94
|
+
{item.title}
|
|
95
|
+
</a>
|
|
96
|
+
{/each}
|
|
97
|
+
</nav>
|
|
98
|
+
</div>
|
|
99
|
+
</aside>
|
|
100
|
+
{/if}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SpecraConfig } from '../../config.types.js';
|
|
2
|
+
import type { TOCItem } from '../../toc.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
items: TOCItem[];
|
|
5
|
+
config: SpecraConfig;
|
|
6
|
+
}
|
|
7
|
+
declare const TableOfContents: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type TableOfContents = ReturnType<typeof TableOfContents>;
|
|
9
|
+
export default TableOfContents;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { setContext } from 'svelte';
|
|
3
|
+
import { writable } from 'svelte/store';
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
children?: Snippet;
|
|
8
|
+
defaultValue?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { children, defaultValue }: Props = $props();
|
|
12
|
+
|
|
13
|
+
// Registration system: Tab children register themselves
|
|
14
|
+
let tabs = $state<Array<{ label: string }>>([]);
|
|
15
|
+
let activeTab = $state(defaultValue || '');
|
|
16
|
+
|
|
17
|
+
function registerTab(label: string) {
|
|
18
|
+
// Avoid duplicate registrations
|
|
19
|
+
if (!tabs.find((t) => t.label === label)) {
|
|
20
|
+
tabs = [...tabs, { label }];
|
|
21
|
+
}
|
|
22
|
+
// If no active tab yet, set first
|
|
23
|
+
if (!activeTab) {
|
|
24
|
+
activeTab = label;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function setActiveTab(label: string) {
|
|
29
|
+
activeTab = label;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Store for reactive access from children
|
|
33
|
+
const activeTabStore = writable(activeTab || '');
|
|
34
|
+
$effect(() => {
|
|
35
|
+
activeTabStore.set(activeTab);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
setContext('specra-tabs', {
|
|
39
|
+
registerTab,
|
|
40
|
+
activeTab: activeTabStore,
|
|
41
|
+
setActiveTab,
|
|
42
|
+
});
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<div class="my-6">
|
|
46
|
+
<!-- Tab buttons -->
|
|
47
|
+
<div class="flex items-center gap-1 border-b border-border mb-4">
|
|
48
|
+
{#each tabs as tab}
|
|
49
|
+
{@const isActive = activeTab === tab.label}
|
|
50
|
+
<button
|
|
51
|
+
onclick={() => setActiveTab(tab.label)}
|
|
52
|
+
class="px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px
|
|
53
|
+
{isActive
|
|
54
|
+
? 'border-primary text-primary'
|
|
55
|
+
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
|
|
56
|
+
}"
|
|
57
|
+
>
|
|
58
|
+
{tab.label}
|
|
59
|
+
</button>
|
|
60
|
+
{/each}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- Tab content -->
|
|
64
|
+
<div>
|
|
65
|
+
{#if children}
|
|
66
|
+
{@render children()}
|
|
67
|
+
{/if}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Moon, Sun } from 'lucide-svelte';
|
|
3
|
+
import { themeStore } from '../../stores/theme.js';
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<button
|
|
7
|
+
onclick={() => themeStore.toggle()}
|
|
8
|
+
class="flex items-center justify-center w-9 h-9 rounded-md border border-border bg-background hover:bg-accent transition-colors"
|
|
9
|
+
aria-label="Toggle theme"
|
|
10
|
+
>
|
|
11
|
+
{#if $themeStore === 'dark'}
|
|
12
|
+
<Sun class="h-4 w-4 text-foreground" />
|
|
13
|
+
{:else}
|
|
14
|
+
<Moon class="h-4 w-4 text-foreground" />
|
|
15
|
+
{/if}
|
|
16
|
+
</button>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const ThemeToggle: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type ThemeToggle = InstanceType<typeof ThemeToggle>;
|
|
18
|
+
export default ThemeToggle;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
content: string;
|
|
8
|
+
position?: TooltipPosition;
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { content, position = 'top', children }: Props = $props();
|
|
13
|
+
|
|
14
|
+
let isVisible = $state(false);
|
|
15
|
+
|
|
16
|
+
const positions: Record<TooltipPosition, string> = {
|
|
17
|
+
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
|
|
18
|
+
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
|
|
19
|
+
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
|
|
20
|
+
right: 'left-full top-1/2 -translate-y-1/2 ml-2',
|
|
21
|
+
};
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<span
|
|
25
|
+
class="relative inline-flex underline decoration-dotted cursor-help"
|
|
26
|
+
onmouseenter={() => (isVisible = true)}
|
|
27
|
+
onmouseleave={() => (isVisible = false)}
|
|
28
|
+
onfocusin={() => (isVisible = true)}
|
|
29
|
+
onfocusout={() => (isVisible = false)}
|
|
30
|
+
role="button"
|
|
31
|
+
tabindex="0"
|
|
32
|
+
>
|
|
33
|
+
{#if children}
|
|
34
|
+
{@render children()}
|
|
35
|
+
{/if}
|
|
36
|
+
{#if isVisible}
|
|
37
|
+
<span
|
|
38
|
+
class="absolute {positions[position]} z-50 px-2 py-1 text-xs text-white bg-gray-900 dark:bg-gray-700 rounded whitespace-nowrap pointer-events-none"
|
|
39
|
+
role="tooltip"
|
|
40
|
+
>
|
|
41
|
+
{content}
|
|
42
|
+
</span>
|
|
43
|
+
{/if}
|
|
44
|
+
</span>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
|
|
3
|
+
interface Props {
|
|
4
|
+
content: string;
|
|
5
|
+
position?: TooltipPosition;
|
|
6
|
+
children?: Snippet;
|
|
7
|
+
}
|
|
8
|
+
declare const Tooltip: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type Tooltip = ReturnType<typeof Tooltip>;
|
|
10
|
+
export default Tooltip;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ChevronDown, Check } from 'lucide-svelte';
|
|
3
|
+
import { goto } from '$app/navigation';
|
|
4
|
+
import { page } from '$app/stores';
|
|
5
|
+
import { browser } from '$app/environment';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
currentVersion: string;
|
|
9
|
+
versions: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { currentVersion, versions }: Props = $props();
|
|
13
|
+
|
|
14
|
+
let isOpen = $state(false);
|
|
15
|
+
let dropdownEl = $state<HTMLDivElement | null>(null);
|
|
16
|
+
|
|
17
|
+
$effect(() => {
|
|
18
|
+
if (!browser || !isOpen) return;
|
|
19
|
+
|
|
20
|
+
function handleClickOutside(e: MouseEvent) {
|
|
21
|
+
if (dropdownEl && !dropdownEl.contains(e.target as Node)) {
|
|
22
|
+
isOpen = false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function handleEscape(e: KeyboardEvent) {
|
|
27
|
+
if (e.key === 'Escape') {
|
|
28
|
+
isOpen = false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
document.addEventListener('click', handleClickOutside);
|
|
33
|
+
document.addEventListener('keydown', handleEscape);
|
|
34
|
+
|
|
35
|
+
return () => {
|
|
36
|
+
document.removeEventListener('click', handleClickOutside);
|
|
37
|
+
document.removeEventListener('keydown', handleEscape);
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function switchVersion(version: string) {
|
|
42
|
+
if (version === currentVersion) {
|
|
43
|
+
isOpen = false;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const currentPath = $page.url.pathname;
|
|
48
|
+
const newPath = currentPath.replace(
|
|
49
|
+
new RegExp(`/${currentVersion}(/|$)`),
|
|
50
|
+
`/${version}$1`
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
isOpen = false;
|
|
54
|
+
goto(newPath);
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
{#if versions.length > 1}
|
|
59
|
+
<div class="relative" bind:this={dropdownEl}>
|
|
60
|
+
<button
|
|
61
|
+
onclick={() => (isOpen = !isOpen)}
|
|
62
|
+
class="flex items-center gap-1.5 h-8 px-3 text-sm font-medium rounded-md border border-border bg-background hover:bg-accent transition-colors text-foreground"
|
|
63
|
+
aria-expanded={isOpen}
|
|
64
|
+
aria-haspopup="listbox"
|
|
65
|
+
aria-label="Switch version"
|
|
66
|
+
>
|
|
67
|
+
<span>{currentVersion}</span>
|
|
68
|
+
<ChevronDown class="h-3.5 w-3.5 text-muted-foreground transition-transform {isOpen ? 'rotate-180' : ''}" />
|
|
69
|
+
</button>
|
|
70
|
+
|
|
71
|
+
{#if isOpen}
|
|
72
|
+
<div
|
|
73
|
+
class="absolute top-full right-0 mt-1 w-40 py-1 bg-popover border border-border rounded-md shadow-lg z-50"
|
|
74
|
+
role="listbox"
|
|
75
|
+
aria-label="Available versions"
|
|
76
|
+
>
|
|
77
|
+
{#each versions as version}
|
|
78
|
+
<button
|
|
79
|
+
role="option"
|
|
80
|
+
aria-selected={version === currentVersion}
|
|
81
|
+
onclick={() => switchVersion(version)}
|
|
82
|
+
class="w-full flex items-center justify-between px-3 py-1.5 text-sm transition-colors {version === currentVersion
|
|
83
|
+
? 'text-primary bg-accent/50 font-medium'
|
|
84
|
+
: 'text-foreground hover:bg-accent'}"
|
|
85
|
+
>
|
|
86
|
+
<span>{version}</span>
|
|
87
|
+
{#if version === currentVersion}
|
|
88
|
+
<Check class="h-3.5 w-3.5 text-primary" />
|
|
89
|
+
{/if}
|
|
90
|
+
</button>
|
|
91
|
+
{/each}
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
{/if}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
src: string;
|
|
4
|
+
caption?: string;
|
|
5
|
+
autoplay?: boolean;
|
|
6
|
+
loop?: boolean;
|
|
7
|
+
muted?: boolean;
|
|
8
|
+
controls?: boolean;
|
|
9
|
+
poster?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
src,
|
|
14
|
+
caption,
|
|
15
|
+
autoplay = false,
|
|
16
|
+
loop = false,
|
|
17
|
+
muted = false,
|
|
18
|
+
controls = true,
|
|
19
|
+
poster,
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
// YouTube URL detection
|
|
23
|
+
function getYouTubeId(url: string): string | null {
|
|
24
|
+
const match = url.match(
|
|
25
|
+
/(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/
|
|
26
|
+
);
|
|
27
|
+
return match ? match[1] : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Vimeo URL detection
|
|
31
|
+
function getVimeoId(url: string): string | null {
|
|
32
|
+
const match = url.match(/vimeo\.com\/(\d+)/);
|
|
33
|
+
return match ? match[1] : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let isYouTube = $derived(src.includes('youtube.com') || src.includes('youtu.be'));
|
|
37
|
+
let isVimeo = $derived(src.includes('vimeo.com'));
|
|
38
|
+
let youtubeId = $derived(getYouTubeId(src));
|
|
39
|
+
let vimeoId = $derived(getVimeoId(src));
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<figure class="my-6">
|
|
43
|
+
<div class="relative rounded-xl border border-border overflow-hidden bg-muted/30">
|
|
44
|
+
{#if isYouTube && youtubeId}
|
|
45
|
+
<div class="relative w-full" style="padding-bottom: 56.25%;">
|
|
46
|
+
<iframe
|
|
47
|
+
class="absolute top-0 left-0 w-full h-full"
|
|
48
|
+
src="https://www.youtube.com/embed/{youtubeId}{autoplay ? '?autoplay=1' : ''}"
|
|
49
|
+
title="YouTube video"
|
|
50
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
51
|
+
allowfullscreen
|
|
52
|
+
></iframe>
|
|
53
|
+
</div>
|
|
54
|
+
{:else if isVimeo && vimeoId}
|
|
55
|
+
<div class="relative w-full" style="padding-bottom: 56.25%;">
|
|
56
|
+
<iframe
|
|
57
|
+
class="absolute top-0 left-0 w-full h-full"
|
|
58
|
+
src="https://player.vimeo.com/video/{vimeoId}{autoplay ? '?autoplay=1' : ''}"
|
|
59
|
+
title="Vimeo video"
|
|
60
|
+
allow="autoplay; fullscreen; picture-in-picture"
|
|
61
|
+
allowfullscreen
|
|
62
|
+
></iframe>
|
|
63
|
+
</div>
|
|
64
|
+
{:else}
|
|
65
|
+
<video
|
|
66
|
+
{src}
|
|
67
|
+
{controls}
|
|
68
|
+
autoplay={autoplay}
|
|
69
|
+
{loop}
|
|
70
|
+
{muted}
|
|
71
|
+
{poster}
|
|
72
|
+
class="w-full h-auto"
|
|
73
|
+
>
|
|
74
|
+
Your browser does not support the video tag.
|
|
75
|
+
<track kind="captions" />
|
|
76
|
+
</video>
|
|
77
|
+
{/if}
|
|
78
|
+
</div>
|
|
79
|
+
{#if caption}
|
|
80
|
+
<figcaption class="mt-2 text-center text-sm text-muted-foreground italic">
|
|
81
|
+
{caption}
|
|
82
|
+
</figcaption>
|
|
83
|
+
{/if}
|
|
84
|
+
</figure>
|