@noahbaumgartner/svelte-ui 0.0.7 → 0.0.9

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 (38) hide show
  1. package/dist/components/button/Button.svelte +0 -10
  2. package/dist/components/lightbox/Lightbox.svelte +136 -0
  3. package/dist/components/lightbox/Lightbox.svelte.d.ts +14 -0
  4. package/dist/components/media-preview/MediaPreview.svelte +110 -0
  5. package/dist/components/media-preview/MediaPreview.svelte.d.ts +11 -0
  6. package/dist/components/mesh-gradient/MeshGradient.svelte +19 -2
  7. package/dist/components/page-header/PageHeader.svelte +83 -0
  8. package/dist/components/page-header/PageHeader.svelte.d.ts +13 -0
  9. package/dist/components/section/Section.svelte +9 -5
  10. package/dist/components/section/Section.svelte.d.ts +6 -3
  11. package/dist/components/select/Select.svelte.d.ts +1 -1
  12. package/dist/components/table-of-contents/TableOfContents.svelte +296 -0
  13. package/dist/components/table-of-contents/TableOfContents.svelte.d.ts +7 -0
  14. package/dist/components/tabs/Tabs.svelte +72 -2
  15. package/dist/components/tabs/Tabs.svelte.d.ts +3 -1
  16. package/dist/components/theme-toggle/ThemeToggle.svelte +40 -0
  17. package/dist/components/theme-toggle/ThemeToggle.svelte.d.ts +7 -0
  18. package/dist/components/timeline/Timeline.svelte +122 -0
  19. package/dist/components/timeline/Timeline.svelte.d.ts +21 -0
  20. package/dist/icon.d.ts +7 -7
  21. package/dist/icons/GithubIcon.svelte +25 -0
  22. package/dist/icons/GithubIcon.svelte.d.ts +4 -0
  23. package/dist/index.d.ts +8 -2
  24. package/dist/index.js +7 -1
  25. package/dist/internal/icons/Monitor.svelte +15 -0
  26. package/dist/internal/icons/Monitor.svelte.d.ts +4 -0
  27. package/dist/internal/icons/Moon.svelte +18 -0
  28. package/dist/internal/icons/Moon.svelte.d.ts +4 -0
  29. package/dist/internal/icons/Pause.svelte +14 -0
  30. package/dist/internal/icons/Pause.svelte.d.ts +4 -0
  31. package/dist/internal/icons/Play.svelte +16 -0
  32. package/dist/internal/icons/Play.svelte.d.ts +4 -0
  33. package/dist/internal/icons/Sun.svelte +21 -0
  34. package/dist/internal/icons/Sun.svelte.d.ts +4 -0
  35. package/dist/styles/tokens.css +7 -0
  36. package/package.json +4 -2
  37. package/dist/components/entry/Entry.svelte +0 -94
  38. package/dist/components/entry/Entry.svelte.d.ts +0 -16
@@ -0,0 +1,296 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte';
3
+ import { SvelteSet } from 'svelte/reactivity';
4
+ import ChevronDown from '../../internal/icons/ChevronDown.svelte';
5
+
6
+ let {
7
+ container,
8
+ variant = 'sidebar'
9
+ }: {
10
+ container: HTMLElement | undefined;
11
+ variant?: 'sidebar' | 'mobile';
12
+ } = $props();
13
+
14
+ type TocItem = { id: string; text: string; level: number };
15
+
16
+ let items: TocItem[] = $state([]);
17
+ let activeId: string | null = $state(null);
18
+ let expanded = $state(false);
19
+
20
+ let activeItem = $derived(items.find((item) => item.id === activeId) ?? items[0]);
21
+
22
+ function slugify(text: string) {
23
+ return text
24
+ .toLowerCase()
25
+ .trim()
26
+ .replace(/[äáàâ]/g, 'a')
27
+ .replace(/[öóòô]/g, 'o')
28
+ .replace(/[üúùû]/g, 'u')
29
+ .replace(/ß/g, 'ss')
30
+ .replace(/[^a-z0-9]+/g, '-')
31
+ .replace(/^-+|-+$/g, '');
32
+ }
33
+
34
+ $effect(() => {
35
+ if (!container) return;
36
+
37
+ const headings = Array.from(container.querySelectorAll('h1, h2')) as HTMLElement[];
38
+ const usedIds = new SvelteSet<string>();
39
+
40
+ items = headings.map((heading) => {
41
+ if (!heading.id) {
42
+ let id = slugify(heading.textContent ?? '');
43
+ let unique = id;
44
+ let i = 2;
45
+ while (usedIds.has(unique)) {
46
+ unique = `${id}-${i}`;
47
+ i++;
48
+ }
49
+ heading.id = unique;
50
+ }
51
+ usedIds.add(heading.id);
52
+ return {
53
+ id: heading.id,
54
+ text: heading.textContent ?? '',
55
+ level: heading.tagName === 'H1' ? 1 : 2
56
+ };
57
+ });
58
+
59
+ const observer = new IntersectionObserver(
60
+ (entries) => {
61
+ for (const entry of entries) {
62
+ if (entry.isIntersecting) {
63
+ activeId = (entry.target as HTMLElement).id;
64
+ break;
65
+ }
66
+ }
67
+ },
68
+ { rootMargin: '0px 0px -70% 0px', threshold: 0 }
69
+ );
70
+
71
+ headings.forEach((heading) => observer.observe(heading));
72
+
73
+ return () => observer.disconnect();
74
+ });
75
+
76
+ const scrollOffset = $derived(variant === 'mobile' ? 130 : 64);
77
+
78
+ async function handleClick(event: MouseEvent, id: string) {
79
+ event.preventDefault();
80
+ expanded = false;
81
+ // Wait for the mobile panel's collapse to actually reach the DOM before
82
+ // measuring the heading position, otherwise the still-expanded panel's
83
+ // extra height gets baked into the scroll target and it overshoots.
84
+ await tick();
85
+ const target = document.getElementById(id);
86
+ if (!target) return;
87
+ const top = target.getBoundingClientRect().top + window.scrollY - scrollOffset;
88
+
89
+ // Mobile Safari can silently drop a new smooth scroll if one is still
90
+ // animating, so snap to the current position first to clear it before
91
+ // requesting the next smooth scroll on the following frame.
92
+ window.scrollTo({ top: window.scrollY, behavior: 'instant' });
93
+ requestAnimationFrame(() => {
94
+ window.scrollTo({ top, behavior: 'smooth' });
95
+ });
96
+
97
+ history.replaceState(null, '', `#${id}`);
98
+ }
99
+ </script>
100
+
101
+ {#if items.length > 0}
102
+ {#if variant === 'mobile'}
103
+ <div class="toc-mobile">
104
+ {#if expanded}
105
+ <div class="toc-mobile-trigger">
106
+ <a
107
+ href="#{items[0].id}"
108
+ class="toc-item toc-mobile-title"
109
+ class:toc-item--sub={items[0].level === 2}
110
+ class:toc-item--active={activeId === items[0].id}
111
+ onclick={(event) => handleClick(event, items[0].id)}
112
+ >
113
+ {items[0].text}
114
+ </a>
115
+ <button
116
+ type="button"
117
+ class="toc-mobile-chevron-btn"
118
+ onclick={() => (expanded = !expanded)}
119
+ aria-expanded={expanded}
120
+ aria-label="Inhaltsverzeichnis einklappen"
121
+ >
122
+ <ChevronDown class="toc-mobile-chevron" style="transform: rotate(180deg)" />
123
+ </button>
124
+ </div>
125
+ {:else}
126
+ <button
127
+ type="button"
128
+ class="toc-mobile-trigger"
129
+ onclick={() => (expanded = !expanded)}
130
+ aria-expanded={expanded}
131
+ aria-label="Inhaltsverzeichnis ausklappen"
132
+ >
133
+ <span class="toc-mobile-title">{activeItem?.text}</span>
134
+ <ChevronDown class="toc-mobile-chevron" />
135
+ </button>
136
+ {/if}
137
+
138
+ {#if expanded}
139
+ <nav class="toc-mobile-panel" aria-label="Inhaltsverzeichnis">
140
+ {#each items.slice(1) as item (item.id)}
141
+ <a
142
+ href="#{item.id}"
143
+ class="toc-item"
144
+ class:toc-item--sub={item.level === 2}
145
+ class:toc-item--active={activeId === item.id}
146
+ onclick={(event) => handleClick(event, item.id)}
147
+ >
148
+ {item.text}
149
+ </a>
150
+ {/each}
151
+ </nav>
152
+ {/if}
153
+ </div>
154
+ {:else}
155
+ <nav class="toc" aria-label="Inhaltsverzeichnis">
156
+ {#each items as item (item.id)}
157
+ <a
158
+ href="#{item.id}"
159
+ class="toc-item"
160
+ class:toc-item--sub={item.level === 2}
161
+ class:toc-item--active={activeId === item.id}
162
+ onclick={(event) => handleClick(event, item.id)}
163
+ >
164
+ {item.text}
165
+ </a>
166
+ {/each}
167
+ </nav>
168
+ {/if}
169
+ {/if}
170
+
171
+ <style>
172
+ .toc {
173
+ display: flex;
174
+ flex-direction: column;
175
+ gap: 10px;
176
+ user-select: none;
177
+ }
178
+
179
+ .toc-item {
180
+ font-size: 12px;
181
+ color: var(--color-text-muted);
182
+ text-decoration: none;
183
+ line-height: 1.4;
184
+ border-radius: 4px;
185
+ outline: none;
186
+ transition: color 150ms ease;
187
+ }
188
+
189
+ .toc-item:hover {
190
+ color: var(--color-text);
191
+ }
192
+
193
+ .toc-item:focus-visible {
194
+ outline: 2px solid var(--color-ink);
195
+ outline-offset: 2px;
196
+ }
197
+
198
+ .toc-item--sub {
199
+ padding-left: 16px;
200
+ }
201
+
202
+ .toc-item--active {
203
+ color: var(--color-text);
204
+ font-weight: 600;
205
+ }
206
+
207
+ .toc-mobile {
208
+ background-color: var(--color-bg);
209
+ border-bottom: 1px solid var(--color-border);
210
+ user-select: none;
211
+ }
212
+
213
+ .toc-mobile-trigger {
214
+ display: flex;
215
+ align-items: center;
216
+ justify-content: space-between;
217
+ gap: 12px;
218
+ width: 100%;
219
+ padding: 14px 24px;
220
+ font-size: 14px;
221
+ }
222
+
223
+ button.toc-mobile-trigger {
224
+ background-color: transparent;
225
+ border: none;
226
+ font: inherit;
227
+ font-size: 14px;
228
+ color: var(--color-text);
229
+ text-align: left;
230
+ outline: none;
231
+ cursor: pointer;
232
+ }
233
+
234
+ button.toc-mobile-trigger:focus-visible {
235
+ outline: 2px solid var(--color-ink);
236
+ outline-offset: -2px;
237
+ }
238
+
239
+ .toc-mobile-title {
240
+ flex: 1;
241
+ overflow: hidden;
242
+ text-overflow: ellipsis;
243
+ white-space: nowrap;
244
+ }
245
+
246
+ a.toc-mobile-title {
247
+ font-size: 14px;
248
+ }
249
+
250
+ span.toc-mobile-title {
251
+ color: var(--color-text);
252
+ }
253
+
254
+ .toc-mobile-chevron-btn {
255
+ display: flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ flex-shrink: 0;
259
+ padding: 0;
260
+ background-color: transparent;
261
+ border: none;
262
+ border-radius: 4px;
263
+ color: var(--color-text);
264
+ outline: none;
265
+ cursor: pointer;
266
+ }
267
+
268
+ .toc-mobile-chevron-btn:focus-visible {
269
+ outline: 2px solid var(--color-ink);
270
+ outline-offset: 2px;
271
+ }
272
+
273
+ .toc-mobile-trigger :global(.toc-mobile-chevron) {
274
+ width: 16px;
275
+ height: 16px;
276
+ flex-shrink: 0;
277
+ transition: transform 200ms ease;
278
+ }
279
+
280
+ .toc-mobile-panel {
281
+ display: flex;
282
+ flex-direction: column;
283
+ gap: 16px;
284
+ padding: 4px 24px 20px;
285
+ }
286
+
287
+ .toc-mobile-panel .toc-item {
288
+ font-size: 14px;
289
+ }
290
+
291
+ @media (min-width: 640px) {
292
+ .toc-mobile {
293
+ display: none;
294
+ }
295
+ }
296
+ </style>
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ container: HTMLElement | undefined;
3
+ variant?: 'sidebar' | 'mobile';
4
+ };
5
+ declare const TableOfContents: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type TableOfContents = ReturnType<typeof TableOfContents>;
7
+ export default TableOfContents;
@@ -23,7 +23,9 @@
23
23
  options: TabsOption<T>[];
24
24
  /** Accessible name for the tab list. */
25
25
  label?: string;
26
- size?: 'sm' | 'md';
26
+ size?: 'sm' | 'md' | 'lg';
27
+ /** `segmented` sits in a pill-shaped track; `text` is plain text where only the selected tab is emphasised. */
28
+ variant?: 'segmented' | 'text';
27
29
  ref?: HTMLDivElement | null;
28
30
  /** Optional panel below the tabs; receives the selected value. Omit to render the content yourself. */
29
31
  children?: Snippet<[value: T]>;
@@ -37,6 +39,7 @@
37
39
  options,
38
40
  label,
39
41
  size = 'md',
42
+ variant = 'segmented',
40
43
  ref = $bindable(null),
41
44
  class: className,
42
45
  children,
@@ -100,7 +103,7 @@
100
103
  }
101
104
  </script>
102
105
 
103
- <div {...rest} bind:this={ref} class={['tabs', `tabs--${size}`, className]}>
106
+ <div {...rest} bind:this={ref} class={['tabs', `tabs--${size}`, `tabs--${variant}`, className]}>
104
107
  <div
105
108
  class={[
106
109
  'tabs-scroller',
@@ -280,6 +283,63 @@
280
283
  border-radius: 5px;
281
284
  }
282
285
 
286
+ .tabs--lg .tabs-list {
287
+ border-radius: 12px;
288
+ }
289
+
290
+ .tabs--lg .tabs-tab {
291
+ height: 32px;
292
+ padding: 0 14px;
293
+ font-size: 14px;
294
+ border-radius: 9px;
295
+ }
296
+
297
+ /* Text: no track and no pill, the selected tab is only darker. Padding stays vertical so the first label lines up with the content below. */
298
+ .tabs--text .tabs-list {
299
+ gap: 20px;
300
+ padding: 0;
301
+ background-color: transparent;
302
+ border-radius: 0;
303
+ }
304
+
305
+ .tabs--text .tabs-tab {
306
+ height: auto;
307
+ padding: 4px 0;
308
+ font-size: 16px;
309
+ border-radius: 0;
310
+ }
311
+
312
+ .tabs--text.tabs--sm .tabs-list {
313
+ gap: 16px;
314
+ }
315
+
316
+ .tabs--text.tabs--sm .tabs-tab {
317
+ padding: 2px 0;
318
+ font-size: 14px;
319
+ }
320
+
321
+ .tabs--text.tabs--lg .tabs-list {
322
+ gap: 24px;
323
+ }
324
+
325
+ .tabs--text.tabs--lg .tabs-tab {
326
+ font-size: 18px;
327
+ }
328
+
329
+ .tabs--text .tabs-tab--selected {
330
+ background-color: transparent;
331
+ box-shadow: none;
332
+ }
333
+
334
+ /* An outline would be clipped by the scroller, so the focus ring is an underline. */
335
+ .tabs--text .tabs-tab:focus-visible {
336
+ outline: none;
337
+ text-decoration: underline;
338
+ text-decoration-color: var(--color-accent);
339
+ text-decoration-thickness: 2px;
340
+ text-underline-offset: 4px;
341
+ }
342
+
283
343
  .tabs-panel {
284
344
  font-size: 13px;
285
345
  color: var(--color-text);
@@ -315,6 +375,16 @@
315
375
  box-shadow: none;
316
376
  }
317
377
 
378
+ /* Text has no track and no filled tab, so the selected tab is told apart by weight */
379
+ .tabs--text .tabs-list {
380
+ border: none;
381
+ }
382
+
383
+ .tabs--text .tabs-tab--selected {
384
+ color: ButtonText;
385
+ font-weight: 700;
386
+ }
387
+
318
388
  .tabs-tab:disabled {
319
389
  color: GrayText;
320
390
  }
@@ -18,7 +18,9 @@ declare function $$render<T extends string>(): {
18
18
  options: TabsOption<T>[];
19
19
  /** Accessible name for the tab list. */
20
20
  label?: string;
21
- size?: "sm" | "md";
21
+ size?: "sm" | "md" | "lg";
22
+ /** `segmented` sits in a pill-shaped track; `text` is plain text where only the selected tab is emphasised. */
23
+ variant?: "segmented" | "text";
22
24
  ref?: HTMLDivElement | null;
23
25
  /** Optional panel below the tabs; receives the selected value. Omit to render the content yourself. */
24
26
  children?: Snippet<[value: T]>;
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import Sun from '../../internal/icons/Sun.svelte';
4
+ import Moon from '../../internal/icons/Moon.svelte';
5
+ import Button from '../button/Button.svelte';
6
+
7
+ type Theme = 'light' | 'dark';
8
+
9
+ /** Accessible name of the button for each current theme; the click switches to the other one. */
10
+ let {
11
+ labels = { light: 'Theme: Light', dark: 'Theme: Dark' }
12
+ }: { labels?: Record<Theme, string> } = $props();
13
+
14
+ let theme: Theme = $state('light');
15
+
16
+ const icons = { light: Sun, dark: Moon };
17
+
18
+ onMount(() => {
19
+ const query = window.matchMedia('(prefers-color-scheme: dark)');
20
+ const systemTheme = (): Theme => (query.matches ? 'dark' : 'light');
21
+
22
+ const stored = localStorage.getItem('theme');
23
+ theme = stored === 'light' || stored === 'dark' ? stored : systemTheme();
24
+
25
+ // Without an explicit choice the toggle keeps following the system.
26
+ const onSystemChange = () => {
27
+ if (!localStorage.getItem('theme')) theme = systemTheme();
28
+ };
29
+ query.addEventListener('change', onSystemChange);
30
+ return () => query.removeEventListener('change', onSystemChange);
31
+ });
32
+
33
+ function toggle() {
34
+ theme = theme === 'light' ? 'dark' : 'light';
35
+ document.documentElement.dataset.theme = theme;
36
+ localStorage.setItem('theme', theme);
37
+ }
38
+ </script>
39
+
40
+ <Button variant="ghost" size="lg" icon={icons[theme]} label={labels[theme]} onclick={toggle} />
@@ -0,0 +1,7 @@
1
+ type Theme = 'light' | 'dark';
2
+ type $$ComponentProps = {
3
+ labels?: Record<Theme, string>;
4
+ };
5
+ declare const ThemeToggle: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ThemeToggle = ReturnType<typeof ThemeToggle>;
7
+ export default ThemeToggle;
@@ -0,0 +1,122 @@
1
+ <script module lang="ts">
2
+ export type TimelineItem = {
3
+ /** Muted text left of the item, e.g. "2021 - 2025". */
4
+ period: string;
5
+ /** Rendered as an `<h3>`. */
6
+ title: string;
7
+ /** Muted line below the title, e.g. a company or institution. */
8
+ subtitle?: string;
9
+ /** Turns the subtitle into a Link. */
10
+ href?: string;
11
+ description?: string;
12
+ };
13
+ </script>
14
+
15
+ <script lang="ts">
16
+ import type { HTMLAttributes } from 'svelte/elements';
17
+ import Link from '../link/Link.svelte';
18
+
19
+ type Props = Omit<HTMLAttributes<HTMLElement>, 'title'> & {
20
+ /** Rendered as an `<h2>` next to the list. */
21
+ title?: string;
22
+ items: TimelineItem[];
23
+ ref?: HTMLElement | null;
24
+ };
25
+
26
+ let { title, items, class: className, ref = $bindable(null), ...rest }: Props = $props();
27
+ </script>
28
+
29
+ <section {...rest} bind:this={ref} class={['timeline', className]}>
30
+ {#if title}
31
+ <h2 class="timeline-title">{title}</h2>
32
+ {/if}
33
+ <ol class="timeline-list">
34
+ {#each items as item (item.title + item.period)}
35
+ <li class="timeline-item">
36
+ <span class="timeline-period">{item.period}</span>
37
+ <div class="timeline-body">
38
+ <h3 class="timeline-item-title">{item.title}</h3>
39
+ {#if item.subtitle}
40
+ <p class="timeline-subtitle">
41
+ {#if item.href}
42
+ <Link href={item.href}>{item.subtitle}</Link>
43
+ {:else}
44
+ {item.subtitle}
45
+ {/if}
46
+ </p>
47
+ {/if}
48
+ {#if item.description}
49
+ <p class="timeline-description">{item.description}</p>
50
+ {/if}
51
+ </div>
52
+ </li>
53
+ {/each}
54
+ </ol>
55
+ </section>
56
+
57
+ <style>
58
+ .timeline {
59
+ display: grid;
60
+ gap: 32px;
61
+ }
62
+
63
+ .timeline-title {
64
+ font-size: clamp(24px, 2vw + 16px, 32px);
65
+ font-weight: 500;
66
+ letter-spacing: -0.01em;
67
+ }
68
+
69
+ .timeline-list {
70
+ border-bottom: 1px solid var(--color-border);
71
+ }
72
+
73
+ .timeline-item {
74
+ display: grid;
75
+ gap: 8px;
76
+ padding-block: 24px;
77
+ border-top: 1px solid var(--color-border);
78
+ }
79
+
80
+ .timeline-period {
81
+ font-size: 14px;
82
+ color: var(--color-text-muted);
83
+ }
84
+
85
+ .timeline-body {
86
+ display: flex;
87
+ flex-direction: column;
88
+ gap: 4px;
89
+ }
90
+
91
+ .timeline-item-title {
92
+ font-size: 20px;
93
+ font-weight: 500;
94
+ }
95
+
96
+ .timeline-subtitle {
97
+ font-size: 15px;
98
+ color: var(--color-text-muted);
99
+ }
100
+
101
+ .timeline-description {
102
+ margin-top: 8px;
103
+ max-width: 640px;
104
+ }
105
+
106
+ @media (min-width: 768px) {
107
+ .timeline {
108
+ grid-template-columns: 1fr 2fr;
109
+ gap: 64px;
110
+ }
111
+
112
+ .timeline-item {
113
+ grid-template-columns: 180px 1fr;
114
+ gap: 32px;
115
+ padding-block: 32px;
116
+ }
117
+
118
+ .timeline-period {
119
+ padding-top: 4px;
120
+ }
121
+ }
122
+ </style>
@@ -0,0 +1,21 @@
1
+ export type TimelineItem = {
2
+ /** Muted text left of the item, e.g. "2021 - 2025". */
3
+ period: string;
4
+ /** Rendered as an `<h3>`. */
5
+ title: string;
6
+ /** Muted line below the title, e.g. a company or institution. */
7
+ subtitle?: string;
8
+ /** Turns the subtitle into a Link. */
9
+ href?: string;
10
+ description?: string;
11
+ };
12
+ import type { HTMLAttributes } from 'svelte/elements';
13
+ type Props = Omit<HTMLAttributes<HTMLElement>, 'title'> & {
14
+ /** Rendered as an `<h2>` next to the list. */
15
+ title?: string;
16
+ items: TimelineItem[];
17
+ ref?: HTMLElement | null;
18
+ };
19
+ declare const Timeline: import("svelte").Component<Props, {}, "ref">;
20
+ type Timeline = ReturnType<typeof Timeline>;
21
+ export default Timeline;
package/dist/icon.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- import type { Component } from 'svelte';
1
+ import type { LucideIcon, LucideProps } from '@lucide/svelte';
2
+ export type { LucideProps as IconProps };
2
3
  /**
3
- * A Svelte component that renders an icon, from any icon library
4
- * (`@lucide/svelte`, `phosphor-svelte`, an inline SVG component, …).
4
+ * A Svelte component that renders an icon and takes the props of a Lucide icon
5
+ * (`size`, `strokeWidth`, `class`, `aria-hidden`, …). Any `@lucide/svelte` icon
6
+ * qualifies, as does an inline SVG component typed with `IconProps` (see
7
+ * `icons/`).
5
8
  *
6
9
  * Components only pass `class` and `aria-hidden`; the size comes from CSS, so
7
10
  * the icon should fill its box (`width`/`height: 1em` or a fixed size is fine).
8
11
  */
9
- export type IconComponent = Component<{
10
- class?: string;
11
- 'aria-hidden'?: boolean | 'true' | 'false';
12
- }>;
12
+ export type IconComponent = LucideIcon;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import type { IconProps } from '../icon.js';
3
+
4
+ // Lucide dropped its brand icons in v1, so the GitHub mark lives here. It
5
+ // takes the same props as a Lucide icon so it can be passed as `icon`.
6
+ let { size = 24, strokeWidth = 2, class: className, ...rest }: IconProps = $props();
7
+ </script>
8
+
9
+ <svg
10
+ {...rest}
11
+ class={className}
12
+ width={size}
13
+ height={size}
14
+ viewBox="0 0 24 24"
15
+ fill="none"
16
+ stroke="currentColor"
17
+ stroke-width={strokeWidth}
18
+ stroke-linecap="round"
19
+ stroke-linejoin="round"
20
+ >
21
+ <path
22
+ d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"
23
+ />
24
+ <path d="M9 18c-4.51 2-5-2-7-2" />
25
+ </svg>
@@ -0,0 +1,4 @@
1
+ import type { IconProps } from '../icon.js';
2
+ declare const GithubIcon: import("svelte").Component<IconProps, {}, "">;
3
+ type GithubIcon = ReturnType<typeof GithubIcon>;
4
+ export default GithubIcon;