astro-pure 1.0.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/bun.lockb +0 -0
- package/bunfig.toml +2 -0
- package/components/advanced/Comment.astro +148 -0
- package/components/advanced/GithubCard.astro +148 -0
- package/components/advanced/LinkPreview.astro +82 -0
- package/components/advanced/MediumZoom.astro +50 -0
- package/components/advanced/QRCode.astro +35 -0
- package/components/advanced/Quote.astro +44 -0
- package/components/advanced/index.ts +11 -0
- package/components/user/Aside.astro +74 -0
- package/components/user/Button.astro +79 -0
- package/components/user/Card.astro +23 -0
- package/components/user/CardList.astro +28 -0
- package/components/user/CardListChildren.astro +24 -0
- package/components/user/Collapse.astro +84 -0
- package/components/user/FormattedDate.astro +21 -0
- package/components/user/Label.astro +18 -0
- package/components/user/Spoiler.astro +11 -0
- package/components/user/Steps.astro +84 -0
- package/components/user/TabItem.astro +18 -0
- package/components/user/Tabs.astro +266 -0
- package/components/user/Timeline.astro +38 -0
- package/components/user/index.ts +17 -0
- package/index.ts +74 -0
- package/package.json +38 -0
- package/plugins/link-preview.ts +110 -0
- package/plugins/rehype-steps.ts +98 -0
- package/plugins/rehype-tabs.ts +112 -0
- package/plugins/virtual-user-config.ts +83 -0
- package/schemas/favicon.ts +42 -0
- package/schemas/head.ts +18 -0
- package/schemas/logo.ts +28 -0
- package/schemas/social.ts +51 -0
- package/types/common.d.ts +48 -0
- package/types/index.d.ts +6 -0
- package/types/integrations-config.ts +43 -0
- package/types/theme-config.ts +125 -0
- package/types/user-config.ts +24 -0
- package/utils/clsx.ts +24 -0
- package/utils/collections.ts +48 -0
- package/utils/date.ts +17 -0
- package/utils/docsContents.ts +36 -0
- package/utils/index.ts +23 -0
- package/utils/module.d.ts +25 -0
- package/utils/server.ts +11 -0
- package/utils/tailwind.ts +7 -0
- package/utils/theme.ts +40 -0
- package/utils/toast.ts +3 -0
- package/utils/toc.ts +41 -0
- package/virtual.d.ts +2 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { cn } from '../../utils'
|
|
3
|
+
|
|
4
|
+
const { as: Tag = 'div', class: className, href, heading, subheading, date } = Astro.props
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<Tag
|
|
8
|
+
class={cn(
|
|
9
|
+
'not-prose block relative rounded-2xl border border-border bg-muted px-5 py-3',
|
|
10
|
+
href && 'transition-all hover:border-foreground/25 hover:shadow-sm',
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
href={href}
|
|
14
|
+
>
|
|
15
|
+
<div class='flex flex-col gap-y-1.5'>
|
|
16
|
+
<div class='flex flex-col gap-y-0.5'>
|
|
17
|
+
<h2 class='text-lg font-medium'>{heading}</h2>
|
|
18
|
+
<p class='text-muted-foreground'>{subheading}</p>
|
|
19
|
+
<p class='text-muted-foreground'>{date}</p>
|
|
20
|
+
</div>
|
|
21
|
+
<slot />
|
|
22
|
+
</div>
|
|
23
|
+
</Tag>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CardListData } from 'virtual:types'
|
|
3
|
+
|
|
4
|
+
import Collapse from '../user/Collapse.astro'
|
|
5
|
+
import CardListChildren from './CardListChildren.astro'
|
|
6
|
+
|
|
7
|
+
type Props = CardListData & {
|
|
8
|
+
collapse?: boolean
|
|
9
|
+
class?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { title, list, collapse, class: className } = Astro.props
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div class={className}>
|
|
16
|
+
{
|
|
17
|
+
collapse ? (
|
|
18
|
+
<Collapse title={title} class='not-prose'>
|
|
19
|
+
<CardListChildren children={list} />
|
|
20
|
+
</Collapse>
|
|
21
|
+
) : (
|
|
22
|
+
<div class='not-prose my-3 flex flex-col gap-y-2 rounded-xl border px-4 py-3 sm:py-4'>
|
|
23
|
+
<p class='text-lg font-medium text-foreground'>{title}</p>
|
|
24
|
+
<CardListChildren children={list} />
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CardList } from 'virtual:types'
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../utils'
|
|
5
|
+
|
|
6
|
+
type Props = { children: CardList }
|
|
7
|
+
const { children } = Astro.props
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<ul class='ms-5 flex list-disc flex-col gap-y-1'>
|
|
11
|
+
{
|
|
12
|
+
children.map((child) => {
|
|
13
|
+
const Tag = child.link ? 'a' : 'p'
|
|
14
|
+
return (
|
|
15
|
+
<li>
|
|
16
|
+
<Tag href={child.link} class={cn('block', Tag == 'a' && 'text-foreground')}>
|
|
17
|
+
{child.title}
|
|
18
|
+
</Tag>
|
|
19
|
+
{child.children && child.children.length > 0 && <Astro.self children={child.children} />}
|
|
20
|
+
</li>
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
</ul>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { cn } from '../../utils'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
class?: string
|
|
6
|
+
title: string
|
|
7
|
+
}
|
|
8
|
+
const { class: className, title, ...props } = Astro.props
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<collapse-component class='group/expand'>
|
|
12
|
+
<div
|
|
13
|
+
class={cn(
|
|
14
|
+
'rounded-xl border border-border px-3 my-4 sm:px-4 group-[.expanded]/expand:bg-muted',
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
<slot name='before' />
|
|
20
|
+
<div
|
|
21
|
+
class='group/highlight expand-title sticky top-0 z-20 flex cursor-pointer items-center justify-between py-1.5 group-[.expanded]/expand:bg-muted sm:py-2'
|
|
22
|
+
>
|
|
23
|
+
<p class='m-0 transition-colors group-hover/highlight:text-primary'>{title}</p>
|
|
24
|
+
<div class='expand-button'>
|
|
25
|
+
<svg
|
|
26
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
27
|
+
width='16'
|
|
28
|
+
height='16'
|
|
29
|
+
viewBox='0 0 24 24'
|
|
30
|
+
fill='none'
|
|
31
|
+
stroke-width='2.5'
|
|
32
|
+
stroke-linecap='round'
|
|
33
|
+
stroke-linejoin='round'
|
|
34
|
+
class='my-1 stroke-muted-foreground transition-all duration-300 group-hover/highlight:stroke-primary group-[.expanded]/expand:-rotate-90'
|
|
35
|
+
>
|
|
36
|
+
<line
|
|
37
|
+
x1='5'
|
|
38
|
+
y1='12'
|
|
39
|
+
x2='19'
|
|
40
|
+
y2='12'
|
|
41
|
+
class='translate-x-1 scale-x-100 duration-300 ease-in-out group-[.expanded]/expand:translate-x-4 group-[.expanded]/expand:scale-x-0'
|
|
42
|
+
></line>
|
|
43
|
+
<polyline
|
|
44
|
+
points='12 5 19 12 12 19'
|
|
45
|
+
class='translate-x-1 duration-300 ease-in-out group-[.expanded]/expand:translate-x-0'
|
|
46
|
+
></polyline>
|
|
47
|
+
</svg>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div
|
|
51
|
+
class='expand-conetent grid opacity-0 transition-all duration-300 ease-in-out group-[.expanded]/expand:mb-3 group-[.expanded]/expand:opacity-100 sm:group-[.expanded]/expand:mb-4'
|
|
52
|
+
>
|
|
53
|
+
<div class='overflow-hidden'>
|
|
54
|
+
<slot />
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</collapse-component>
|
|
59
|
+
|
|
60
|
+
<script>
|
|
61
|
+
class Collapse extends HTMLElement {
|
|
62
|
+
constructor() {
|
|
63
|
+
super()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
connectedCallback() {
|
|
67
|
+
const expandTitle = this.querySelector('.expand-title')
|
|
68
|
+
// const expandable = this.querySelector('.expandable')
|
|
69
|
+
expandTitle?.addEventListener('click', () => {
|
|
70
|
+
this.classList.toggle('expanded')
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
customElements.define('collapse-component', Collapse)
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<style>
|
|
78
|
+
.expand-conetent {
|
|
79
|
+
grid-template-rows: 0fr;
|
|
80
|
+
}
|
|
81
|
+
.expanded .expand-conetent {
|
|
82
|
+
grid-template-rows: 1fr;
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
3
|
+
|
|
4
|
+
import { cn, getFormattedDate } from '../../utils'
|
|
5
|
+
|
|
6
|
+
type Props = HTMLAttributes<'time'> & {
|
|
7
|
+
date: Date
|
|
8
|
+
dateTimeOptions?: Intl.DateTimeFormatOptions
|
|
9
|
+
class?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { date, dateTimeOptions, class: className, ...attrs } = Astro.props
|
|
13
|
+
|
|
14
|
+
const postDate = getFormattedDate(date, dateTimeOptions)
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<span class={cn('text-muted-foreground font-mono', className)} {...attrs}>
|
|
18
|
+
<time datetime={date.toISOString()}>
|
|
19
|
+
{postDate}
|
|
20
|
+
</time>
|
|
21
|
+
</span>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { cn } from '../../utils'
|
|
3
|
+
|
|
4
|
+
const { class: className, as: Tag = 'div', title, href, ...props } = Astro.props
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<Tag
|
|
8
|
+
class={cn(
|
|
9
|
+
className,
|
|
10
|
+
'flex flex-row items-center justify-center gap-x-2',
|
|
11
|
+
href && 'hover:opacity-75 transition-all'
|
|
12
|
+
)}
|
|
13
|
+
href={href}
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
<slot name='icon' />
|
|
17
|
+
<p>{title}</p>
|
|
18
|
+
</Tag>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
// https://github.com/withastro/starlight/blob/main/packages/starlight/user-components/Steps.astro
|
|
3
|
+
import { processSteps } from '../../plugins/rehype-steps'
|
|
4
|
+
|
|
5
|
+
const content = await Astro.slots.render('default')
|
|
6
|
+
const { html } = processSteps(content)
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<Fragment set:html={html} />
|
|
10
|
+
|
|
11
|
+
<style is:global>
|
|
12
|
+
.sl-steps {
|
|
13
|
+
--bullet-size: calc(1.75rem);
|
|
14
|
+
--bullet-margin: 0.375rem;
|
|
15
|
+
|
|
16
|
+
list-style: none !important;
|
|
17
|
+
counter-reset: steps-counter var(--sl-steps-start, 0);
|
|
18
|
+
padding-inline-start: 0 !important;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.sl-steps > li {
|
|
22
|
+
counter-increment: steps-counter;
|
|
23
|
+
position: relative;
|
|
24
|
+
padding-inline-start: calc(var(--bullet-size) + 1rem);
|
|
25
|
+
/* HACK: Keeps any `margin-bottom` inside the `<li>`’s padding box to avoid gaps in the hairline border. */
|
|
26
|
+
padding-bottom: 1px;
|
|
27
|
+
/* Prevent bullets from touching in short list items. */
|
|
28
|
+
min-height: calc(var(--bullet-size) + var(--bullet-margin));
|
|
29
|
+
}
|
|
30
|
+
.sl-steps > li + li {
|
|
31
|
+
/* Remove margin between steps. */
|
|
32
|
+
margin-top: 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Custom list marker element. */
|
|
36
|
+
.sl-steps > li::before {
|
|
37
|
+
content: counter(steps-counter);
|
|
38
|
+
position: absolute;
|
|
39
|
+
top: 0;
|
|
40
|
+
inset-inline-start: 0;
|
|
41
|
+
width: var(--bullet-size);
|
|
42
|
+
height: var(--bullet-size);
|
|
43
|
+
line-height: var(--bullet-size);
|
|
44
|
+
|
|
45
|
+
font-size: 0.8125rem;
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
text-align: center;
|
|
48
|
+
color: hsl(var(--foreground) / var(--tw-text-opacity, 1));
|
|
49
|
+
background-color: hsl(var(--primary-foreground) / var(--tw-bg-opacity, 1));
|
|
50
|
+
border-radius: 99rem;
|
|
51
|
+
box-shadow: inset 0 0 0 1px hsl(var(--border) / var(--tw-border-opacity, 1));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Vertical guideline linking list numbers. */
|
|
55
|
+
.sl-steps > li::after {
|
|
56
|
+
--guide-width: 1px;
|
|
57
|
+
content: '';
|
|
58
|
+
position: absolute;
|
|
59
|
+
top: calc(var(--bullet-size) + var(--bullet-margin));
|
|
60
|
+
bottom: var(--bullet-margin);
|
|
61
|
+
inset-inline-start: calc((var(--bullet-size) - var(--guide-width)) / 2);
|
|
62
|
+
width: var(--guide-width);
|
|
63
|
+
background-color: hsl(var(--border) / var(--tw-border-opacity, 1));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Adjust first item inside a step so that it aligns vertically with the number
|
|
67
|
+
even if using a larger font size (e.g. a heading) */
|
|
68
|
+
.sl-steps > li > :first-child {
|
|
69
|
+
/*
|
|
70
|
+
The `lh` unit is not yet supported by all browsers in our support matrix
|
|
71
|
+
— see https://caniuse.com/mdn-css_types_length_lh
|
|
72
|
+
In unsupported browsers we approximate this using our known line-heights.
|
|
73
|
+
*/
|
|
74
|
+
--lh: calc(1.75em);
|
|
75
|
+
--shift-y: calc(0.5 * (var(--bullet-size) - var(--lh)));
|
|
76
|
+
margin-top: 0;
|
|
77
|
+
transform: translateY(var(--shift-y));
|
|
78
|
+
margin-bottom: var(--shift-y);
|
|
79
|
+
color: hsl(var(--foreground) / var(--tw-text-opacity, 1));
|
|
80
|
+
}
|
|
81
|
+
.sl-steps > li > :first-child:where(h1, h2, h3, h4, h5, h6) {
|
|
82
|
+
--lh: calc(1.2em);
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
// https://github.com/withastro/starlight/blob/main/packages/starlight/user-components/TabItem.astro
|
|
3
|
+
import { TabItemTagname } from '../../plugins/rehype-tabs'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
label: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { label } = Astro.props
|
|
10
|
+
|
|
11
|
+
if (!label) {
|
|
12
|
+
throw new Error('Missing prop `label` on `<TabItem>` component.')
|
|
13
|
+
}
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<TabItemTagname data-label={label}>
|
|
17
|
+
<slot />
|
|
18
|
+
</TabItemTagname>
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
// https://github.com/withastro/starlight/blob/main/packages/starlight/user-components/Tabs.astro
|
|
3
|
+
import { processPanels } from '../../plugins/rehype-tabs'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
syncKey?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { syncKey } = Astro.props
|
|
10
|
+
const panelHtml = await Astro.slots.render('default')
|
|
11
|
+
const { html, panels } = processPanels(panelHtml)
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Synced tabs are persisted across page using `localStorage`. The script used to restore the
|
|
15
|
+
* active tab for a given sync key has a few requirements:
|
|
16
|
+
*
|
|
17
|
+
* - The script should only be included when at least one set of synced tabs is present on the page.
|
|
18
|
+
* - The script should be inlined to avoid a flash of invalid active tab.
|
|
19
|
+
* - The script should only be included once per page.
|
|
20
|
+
*
|
|
21
|
+
* To do so, we keep track of whether the script has been rendered using a variable stored using
|
|
22
|
+
* `Astro.locals` which will be reset for each new page. The value is tracked using an untyped
|
|
23
|
+
* symbol on purpose to avoid Starlight users to get autocomplete for it and avoid potential
|
|
24
|
+
* clashes with user-defined variables.
|
|
25
|
+
*
|
|
26
|
+
* The restore script defines a custom element `starlight-tabs-restore` that will be included in
|
|
27
|
+
* each set of synced tabs to restore the active tab based on the persisted value using the
|
|
28
|
+
* `connectedCallback` lifecycle method. To ensure this callback can access all tabs and panels for
|
|
29
|
+
* the current set of tabs, the script should be rendered before the tabs themselves.
|
|
30
|
+
*/
|
|
31
|
+
const isSynced = syncKey !== undefined
|
|
32
|
+
const didRenderSyncedTabsRestoreScriptSymbol = Symbol.for(
|
|
33
|
+
'starlight:did-render-synced-tabs-restore-script'
|
|
34
|
+
)
|
|
35
|
+
// @ts-expect-error - See bove
|
|
36
|
+
const shouldRenderSyncedTabsRestoreScript = isSynced && Astro.locals[didRenderSyncedTabsRestoreScriptSymbol] !== true
|
|
37
|
+
|
|
38
|
+
if (isSynced) {
|
|
39
|
+
// @ts-expect-error - See above
|
|
40
|
+
Astro.locals[didRenderSyncedTabsRestoreScriptSymbol] = true
|
|
41
|
+
}
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
{/* Inlined to avoid a flash of invalid active tab. */}
|
|
45
|
+
{shouldRenderSyncedTabsRestoreScript && (
|
|
46
|
+
<script is:inline>
|
|
47
|
+
class StarlightTabsRestore extends HTMLElement {
|
|
48
|
+
connectedCallback() {
|
|
49
|
+
const starlightTabs = this.closest('starlight-tabs');
|
|
50
|
+
if (!(starlightTabs instanceof HTMLElement) || typeof localStorage === 'undefined') return;
|
|
51
|
+
const syncKey = starlightTabs.dataset.syncKey;
|
|
52
|
+
if (!syncKey) return;
|
|
53
|
+
const label = localStorage.getItem(`starlight-synced-tabs__${syncKey}`);
|
|
54
|
+
if (!label) return;
|
|
55
|
+
const tabs = starlightTabs ? [...starlightTabs.querySelectorAll('[role="tab"]')] : [];
|
|
56
|
+
const tabIndexToRestore = tabs.findIndex(
|
|
57
|
+
(tab) => tab instanceof HTMLAnchorElement && tab.textContent?.trim() === label
|
|
58
|
+
);
|
|
59
|
+
const panels = starlightTabs?.querySelectorAll(':scope > [role="tabpanel"]');
|
|
60
|
+
const newTab = tabs[tabIndexToRestore];
|
|
61
|
+
const newPanel = panels[tabIndexToRestore];
|
|
62
|
+
if (tabIndexToRestore < 1 || !newTab || !newPanel) return;
|
|
63
|
+
tabs[0]?.setAttribute('aria-selected', 'false');
|
|
64
|
+
tabs[0]?.setAttribute('tabindex', '-1');
|
|
65
|
+
panels?.[0]?.setAttribute('hidden', 'true');
|
|
66
|
+
newTab.removeAttribute('tabindex');
|
|
67
|
+
newTab.setAttribute('aria-selected', 'true');
|
|
68
|
+
newPanel.removeAttribute('hidden');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
customElements.define('starlight-tabs-restore', StarlightTabsRestore);
|
|
72
|
+
</script>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
<starlight-tabs data-sync-key={syncKey}>
|
|
76
|
+
{
|
|
77
|
+
panels && (
|
|
78
|
+
<div class='tablist-wrapper not-content'>
|
|
79
|
+
<ul role='tablist' class='my-0'>
|
|
80
|
+
{panels.map(({ label, panelId, tabId }, idx) => (
|
|
81
|
+
<li role='presentation' class='tab'>
|
|
82
|
+
<a
|
|
83
|
+
role='tab'
|
|
84
|
+
href={'#' + panelId}
|
|
85
|
+
id={tabId}
|
|
86
|
+
aria-selected={idx === 0 ? 'true' : 'false'}
|
|
87
|
+
tabindex={idx !== 0 ? -1 : 0}
|
|
88
|
+
>
|
|
89
|
+
{label}
|
|
90
|
+
</a>
|
|
91
|
+
</li>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
<Fragment set:html={html} />
|
|
98
|
+
{isSynced && <starlight-tabs-restore />}
|
|
99
|
+
</starlight-tabs>
|
|
100
|
+
|
|
101
|
+
<style>
|
|
102
|
+
starlight-tabs {
|
|
103
|
+
display: block;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.tablist-wrapper {
|
|
107
|
+
overflow-x: auto;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
[role='tablist'] {
|
|
111
|
+
display: flex;
|
|
112
|
+
list-style: none;
|
|
113
|
+
border-bottom: 2px solid hsl(var(--border) / var(--tw-border-opacity, 1));
|
|
114
|
+
padding: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.tab {
|
|
118
|
+
margin-bottom: -2px;
|
|
119
|
+
}
|
|
120
|
+
.tab > [role='tab'] {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 0.5rem;
|
|
124
|
+
padding: 0.2rem 1.25rem;
|
|
125
|
+
text-decoration: none;
|
|
126
|
+
border-bottom: 2px solid hsl(var(--border) / var(--tw-border-opacity, 1));
|
|
127
|
+
color: hsl(var(--foreground) / var(--tw-text-opacity, 1));
|
|
128
|
+
outline-offset: -0.1875rem;
|
|
129
|
+
overflow-wrap: initial;
|
|
130
|
+
}
|
|
131
|
+
.tab [role='tab'][aria-selected='true'] {
|
|
132
|
+
color: hsl(var(--primary) / var(--tw-text-opacity, 1));
|
|
133
|
+
border-color: hsl(var(--primary) / var(--tw-text-opacity, 1));
|
|
134
|
+
font-weight: 600;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.tablist-wrapper ~ :global([role='tabpanel']) {
|
|
138
|
+
margin-top: 1rem;
|
|
139
|
+
}
|
|
140
|
+
</style>
|
|
141
|
+
|
|
142
|
+
<script>
|
|
143
|
+
class StarlightTabs extends HTMLElement {
|
|
144
|
+
// A map of sync keys to all tabs that are synced to that key.
|
|
145
|
+
static #syncedTabs = new Map<string, StarlightTabs[]>()
|
|
146
|
+
|
|
147
|
+
tabs: HTMLAnchorElement[]
|
|
148
|
+
panels: HTMLElement[]
|
|
149
|
+
#syncKey: string | undefined
|
|
150
|
+
// The storage key prefix should be in sync with the one used in the restore script.
|
|
151
|
+
#storageKeyPrefix = 'starlight-synced-tabs__'
|
|
152
|
+
|
|
153
|
+
constructor() {
|
|
154
|
+
super()
|
|
155
|
+
const tablist = this.querySelector<HTMLUListElement>('[role="tablist"]')!
|
|
156
|
+
this.tabs = [...tablist.querySelectorAll<HTMLAnchorElement>('[role="tab"]')]
|
|
157
|
+
this.panels = [...this.querySelectorAll<HTMLElement>(':scope > [role="tabpanel"]')]
|
|
158
|
+
this.#syncKey = this.dataset.syncKey
|
|
159
|
+
|
|
160
|
+
if (this.#syncKey) {
|
|
161
|
+
const syncedTabs = StarlightTabs.#syncedTabs.get(this.#syncKey) ?? []
|
|
162
|
+
syncedTabs.push(this)
|
|
163
|
+
StarlightTabs.#syncedTabs.set(this.#syncKey, syncedTabs)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.tabs.forEach((tab, i) => {
|
|
167
|
+
// Handle clicks for mouse users
|
|
168
|
+
tab.addEventListener('click', (e) => {
|
|
169
|
+
e.preventDefault()
|
|
170
|
+
const currentTab = tablist.querySelector('[aria-selected="true"]')
|
|
171
|
+
if (e.currentTarget !== currentTab) {
|
|
172
|
+
this.switchTab(e.currentTarget as HTMLAnchorElement, i)
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// Handle keyboard input
|
|
177
|
+
tab.addEventListener('keydown', (e) => {
|
|
178
|
+
const index = this.tabs.indexOf(e.currentTarget as HTMLAnchorElement)
|
|
179
|
+
// Work out which key the user is pressing and
|
|
180
|
+
// Calculate the new tab's index where appropriate
|
|
181
|
+
const nextIndex =
|
|
182
|
+
e.key === 'ArrowLeft'
|
|
183
|
+
? index - 1
|
|
184
|
+
: e.key === 'ArrowRight'
|
|
185
|
+
? index + 1
|
|
186
|
+
: e.key === 'Home'
|
|
187
|
+
? 0
|
|
188
|
+
: e.key === 'End'
|
|
189
|
+
? this.tabs.length - 1
|
|
190
|
+
: null
|
|
191
|
+
if (nextIndex === null) return
|
|
192
|
+
if (this.tabs[nextIndex]) {
|
|
193
|
+
e.preventDefault()
|
|
194
|
+
this.switchTab(this.tabs[nextIndex], nextIndex)
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
switchTab(newTab: HTMLAnchorElement | null | undefined, index: number, shouldSync = true) {
|
|
201
|
+
if (!newTab) return
|
|
202
|
+
|
|
203
|
+
// If tabs should be synced, we store the current position so we can restore it after
|
|
204
|
+
// switching tabs to prevent the page from jumping when the new tab content is of a different
|
|
205
|
+
// height than the previous tab.
|
|
206
|
+
const previousTabsOffset = shouldSync ? this.getBoundingClientRect().top : 0
|
|
207
|
+
|
|
208
|
+
// Mark all tabs as unselected and hide all tab panels.
|
|
209
|
+
this.tabs.forEach((tab) => {
|
|
210
|
+
tab.setAttribute('aria-selected', 'false')
|
|
211
|
+
tab.setAttribute('tabindex', '-1')
|
|
212
|
+
})
|
|
213
|
+
this.panels.forEach((oldPanel) => {
|
|
214
|
+
oldPanel.hidden = true
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// Show new panel and mark new tab as selected.
|
|
218
|
+
const newPanel = this.panels[index]
|
|
219
|
+
if (newPanel) newPanel.hidden = false
|
|
220
|
+
// Restore active tab to the default tab order.
|
|
221
|
+
newTab.removeAttribute('tabindex')
|
|
222
|
+
newTab.setAttribute('aria-selected', 'true')
|
|
223
|
+
if (shouldSync) {
|
|
224
|
+
newTab.focus()
|
|
225
|
+
StarlightTabs.#syncTabs(this, newTab)
|
|
226
|
+
window.scrollTo({
|
|
227
|
+
top: window.scrollY + (this.getBoundingClientRect().top - previousTabsOffset)
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#persistSyncedTabs(label: string) {
|
|
233
|
+
if (!this.#syncKey || typeof localStorage === 'undefined') return
|
|
234
|
+
localStorage.setItem(this.#storageKeyPrefix + this.#syncKey, label)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static #syncTabs(emitter: StarlightTabs, newTab: HTMLAnchorElement) {
|
|
238
|
+
const syncKey = emitter.#syncKey
|
|
239
|
+
const label = StarlightTabs.#getTabLabel(newTab)
|
|
240
|
+
if (!syncKey || !label) return
|
|
241
|
+
const syncedTabs = StarlightTabs.#syncedTabs.get(syncKey)
|
|
242
|
+
if (!syncedTabs) return
|
|
243
|
+
|
|
244
|
+
for (const receiver of syncedTabs) {
|
|
245
|
+
if (receiver === emitter) continue
|
|
246
|
+
const labelIndex = receiver.tabs.findIndex(
|
|
247
|
+
(tab) => StarlightTabs.#getTabLabel(tab) === label
|
|
248
|
+
)
|
|
249
|
+
if (labelIndex === -1) continue
|
|
250
|
+
receiver.switchTab(receiver.tabs[labelIndex], labelIndex, false)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
emitter.#persistSyncedTabs(label)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
static #getTabLabel(tab: HTMLAnchorElement) {
|
|
257
|
+
// `textContent` returns the content of all elements. In the case of a tab with an icon, this
|
|
258
|
+
// could potentially include extra spaces due to the presence of the SVG icon.
|
|
259
|
+
// To sync tabs with the same sync key and label, no matter the presence of an icon, we trim
|
|
260
|
+
// these extra spaces.
|
|
261
|
+
return tab.textContent?.trim()
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
customElements.define('starlight-tabs', StarlightTabs)
|
|
266
|
+
</script>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { TimelineEvent } from 'virtual:types'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
class?: string
|
|
6
|
+
events: TimelineEvent[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { class: className, events, ...props } = Astro.props
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class={className} {...props}>
|
|
13
|
+
<ul class='ps-0 sm:ps-2'>
|
|
14
|
+
{
|
|
15
|
+
events.map((event, index) => (
|
|
16
|
+
<li class='group relative flex list-none gap-x-3 rounded-full ps-0 sm:gap-x-2'>
|
|
17
|
+
{/* circle */}
|
|
18
|
+
<span class='z-10 my-2 ms-2 h-3 w-3 min-w-3 rounded-full border-2 border-muted-foreground transition-transform group-hover:scale-125' />
|
|
19
|
+
{/* line */}
|
|
20
|
+
{index !== events.length - 1 && (
|
|
21
|
+
<span
|
|
22
|
+
class='absolute start-[12px] top-[20px] w-1 bg-border'
|
|
23
|
+
style={{ height: 'calc(100% - 4px)' }}
|
|
24
|
+
/>
|
|
25
|
+
)}
|
|
26
|
+
<div class='flex gap-2 max-sm:flex-col'>
|
|
27
|
+
<samp class='w-fit grow-0 rounded-md py-1 text-sm max-sm:bg-primary-foreground max-sm:px-2 sm:min-w-[82px] sm:text-right'>
|
|
28
|
+
{event.date}
|
|
29
|
+
</samp>
|
|
30
|
+
<div>
|
|
31
|
+
<Fragment set:html={event.content} />
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</li>
|
|
35
|
+
))
|
|
36
|
+
}
|
|
37
|
+
</ul>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Caontainer
|
|
2
|
+
export { default as Card } from './Card.astro'
|
|
3
|
+
export { default as Collapse } from './Collapse.astro'
|
|
4
|
+
export { default as Aside } from './Aside.astro'
|
|
5
|
+
export { default as Tabs } from './Tabs.astro'
|
|
6
|
+
export { default as TabItem } from './TabItem.astro'
|
|
7
|
+
|
|
8
|
+
// List
|
|
9
|
+
export { default as CardList } from './CardList.astro'
|
|
10
|
+
export { default as Timeline } from './Timeline.astro'
|
|
11
|
+
export { default as Steps } from './Steps.astro'
|
|
12
|
+
|
|
13
|
+
// Simple text rerender
|
|
14
|
+
export { default as Button } from './Button.astro'
|
|
15
|
+
export { default as Spoiler } from './Spoiler.astro'
|
|
16
|
+
export { default as FormattedDate } from './FormattedDate.astro'
|
|
17
|
+
export { default as Label } from './Label.astro'
|