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.
Files changed (50) hide show
  1. package/bun.lockb +0 -0
  2. package/bunfig.toml +2 -0
  3. package/components/advanced/Comment.astro +148 -0
  4. package/components/advanced/GithubCard.astro +148 -0
  5. package/components/advanced/LinkPreview.astro +82 -0
  6. package/components/advanced/MediumZoom.astro +50 -0
  7. package/components/advanced/QRCode.astro +35 -0
  8. package/components/advanced/Quote.astro +44 -0
  9. package/components/advanced/index.ts +11 -0
  10. package/components/user/Aside.astro +74 -0
  11. package/components/user/Button.astro +79 -0
  12. package/components/user/Card.astro +23 -0
  13. package/components/user/CardList.astro +28 -0
  14. package/components/user/CardListChildren.astro +24 -0
  15. package/components/user/Collapse.astro +84 -0
  16. package/components/user/FormattedDate.astro +21 -0
  17. package/components/user/Label.astro +18 -0
  18. package/components/user/Spoiler.astro +11 -0
  19. package/components/user/Steps.astro +84 -0
  20. package/components/user/TabItem.astro +18 -0
  21. package/components/user/Tabs.astro +266 -0
  22. package/components/user/Timeline.astro +38 -0
  23. package/components/user/index.ts +17 -0
  24. package/index.ts +74 -0
  25. package/package.json +38 -0
  26. package/plugins/link-preview.ts +110 -0
  27. package/plugins/rehype-steps.ts +98 -0
  28. package/plugins/rehype-tabs.ts +112 -0
  29. package/plugins/virtual-user-config.ts +83 -0
  30. package/schemas/favicon.ts +42 -0
  31. package/schemas/head.ts +18 -0
  32. package/schemas/logo.ts +28 -0
  33. package/schemas/social.ts +51 -0
  34. package/types/common.d.ts +48 -0
  35. package/types/index.d.ts +6 -0
  36. package/types/integrations-config.ts +43 -0
  37. package/types/theme-config.ts +125 -0
  38. package/types/user-config.ts +24 -0
  39. package/utils/clsx.ts +24 -0
  40. package/utils/collections.ts +48 -0
  41. package/utils/date.ts +17 -0
  42. package/utils/docsContents.ts +36 -0
  43. package/utils/index.ts +23 -0
  44. package/utils/module.d.ts +25 -0
  45. package/utils/server.ts +11 -0
  46. package/utils/tailwind.ts +7 -0
  47. package/utils/theme.ts +40 -0
  48. package/utils/toast.ts +3 -0
  49. package/utils/toc.ts +41 -0
  50. 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,11 @@
1
+ ---
2
+ import { cn } from '../../utils'
3
+
4
+ const { as: Tag = 'span', class: className } = Astro.props
5
+ ---
6
+
7
+ <Tag
8
+ class={cn('bg-muted rounded text-transparent hover:text-inherit transition-colors', className)}
9
+ >
10
+ <slot />
11
+ </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'