@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
@@ -246,16 +246,6 @@
246
246
  background-color: color-mix(in srgb, var(--color-surface) 75%, transparent);
247
247
  }
248
248
 
249
- @media (pointer: coarse) {
250
- .button {
251
- min-height: 44px;
252
- }
253
-
254
- .button--icon-only {
255
- min-width: 44px;
256
- }
257
- }
258
-
259
249
  @media (forced-colors: active) {
260
250
  .button {
261
251
  border: 1px solid ButtonText;
@@ -0,0 +1,136 @@
1
+ <script lang="ts">
2
+ import { fade } from 'svelte/transition';
3
+ import X from '../../internal/icons/X.svelte';
4
+ import Button from '../button/Button.svelte';
5
+
6
+ let {
7
+ src,
8
+ alt = '',
9
+ aspect,
10
+ onclose,
11
+ label = 'Image',
12
+ closeLabel = 'Close'
13
+ }: {
14
+ src?: string;
15
+ alt?: string;
16
+ /** width / height of the image; needed for SVGs without intrinsic size */
17
+ aspect?: number;
18
+ onclose: () => void;
19
+ /** Accessible name of the dialog. */
20
+ label?: string;
21
+ /** Accessible name of the close button. */
22
+ closeLabel?: string;
23
+ } = $props();
24
+ </script>
25
+
26
+ <svelte:window
27
+ onkeydown={(event) => {
28
+ if (event.key === 'Escape') onclose();
29
+ }}
30
+ onscroll={() => {
31
+ if (src) onclose();
32
+ }}
33
+ />
34
+
35
+ {#if src}
36
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_interactive_supports_focus -->
37
+ <div
38
+ class="lightbox-overlay"
39
+ role="dialog"
40
+ aria-modal="true"
41
+ aria-label={label}
42
+ onclick={onclose}
43
+ in:fade={{ duration: 200 }}
44
+ out:fade={{ duration: 100 }}
45
+ >
46
+ <div class="lightbox-frame">
47
+ <img {src} {alt} class="lightbox-img" style:--aspect={aspect} />
48
+ <Button
49
+ variant="overlay"
50
+ size="lg"
51
+ icon={X}
52
+ label={closeLabel}
53
+ class="lightbox-close"
54
+ onclick={(event) => {
55
+ event.stopPropagation();
56
+ onclose();
57
+ }}
58
+ />
59
+ </div>
60
+ </div>
61
+ {/if}
62
+
63
+ <style>
64
+ .lightbox-overlay {
65
+ position: fixed;
66
+ inset: 0;
67
+ z-index: 100;
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: center;
71
+ padding-block: var(--lightbox-pad-block);
72
+ padding-inline: var(--lightbox-pad-inline);
73
+ background-color: color-mix(in srgb, var(--color-bg) 60%, transparent);
74
+ backdrop-filter: blur(16px);
75
+ -webkit-backdrop-filter: blur(16px);
76
+ border: none;
77
+ cursor: zoom-out;
78
+ }
79
+
80
+ @media (min-width: 640px) {
81
+ .lightbox-overlay {
82
+ --lightbox-pad-block: 64px;
83
+ --lightbox-pad-inline: 48px;
84
+ }
85
+ }
86
+
87
+ @media (min-width: 1024px) {
88
+ .lightbox-overlay {
89
+ --lightbox-pad-block: 96px;
90
+ --lightbox-pad-inline: 96px;
91
+ }
92
+ }
93
+
94
+ .lightbox-frame {
95
+ position: relative;
96
+ display: block;
97
+ width: fit-content;
98
+ max-width: 100%;
99
+ text-align: center;
100
+ }
101
+
102
+ .lightbox-img {
103
+ display: block;
104
+ margin-inline: auto;
105
+ max-width: 100%;
106
+ max-height: calc(100dvh - 2 * var(--lightbox-pad-block));
107
+ width: auto;
108
+ height: auto;
109
+ aspect-ratio: var(--aspect, auto);
110
+ /* images without intrinsic size (SVG with only a viewBox) need an explicit width */
111
+ min-width: min(
112
+ calc(100vw - 2 * var(--lightbox-pad-inline)),
113
+ calc((100dvh - 2 * var(--lightbox-pad-block)) * var(--aspect, 0))
114
+ );
115
+ border: 1px solid var(--color-border);
116
+ border-radius: 16px;
117
+ box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.25);
118
+ }
119
+
120
+ :global(.lightbox-close) {
121
+ position: absolute;
122
+ top: 12px;
123
+ right: 12px;
124
+ opacity: 0;
125
+ }
126
+
127
+ :global(.lightbox-close:focus-visible) {
128
+ opacity: 1;
129
+ }
130
+
131
+ @media (hover: hover) {
132
+ .lightbox-frame:hover :global(.lightbox-close) {
133
+ opacity: 1;
134
+ }
135
+ }
136
+ </style>
@@ -0,0 +1,14 @@
1
+ type $$ComponentProps = {
2
+ src?: string;
3
+ alt?: string;
4
+ /** width / height of the image; needed for SVGs without intrinsic size */
5
+ aspect?: number;
6
+ onclose: () => void;
7
+ /** Accessible name of the dialog. */
8
+ label?: string;
9
+ /** Accessible name of the close button. */
10
+ closeLabel?: string;
11
+ };
12
+ declare const Lightbox: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type Lightbox = ReturnType<typeof Lightbox>;
14
+ export default Lightbox;
@@ -0,0 +1,110 @@
1
+ <script lang="ts">
2
+ import Pause from '../../internal/icons/Pause.svelte';
3
+ import Play from '../../internal/icons/Play.svelte';
4
+ import Button from '../button/Button.svelte';
5
+
6
+ let {
7
+ src,
8
+ alt = '',
9
+ aspectRatio = '15 / 8',
10
+ playLabel = 'Play',
11
+ pauseLabel = 'Pause'
12
+ }: {
13
+ src?: string;
14
+ alt?: string;
15
+ /** CSS aspect-ratio of the frame. */
16
+ aspectRatio?: string;
17
+ playLabel?: string;
18
+ pauseLabel?: string;
19
+ } = $props();
20
+
21
+ let videoEl: HTMLVideoElement | undefined = $state();
22
+ let gifEl: HTMLImageElement | undefined = $state();
23
+ let canvasEl: HTMLCanvasElement | undefined = $state();
24
+ let playing = $state(true);
25
+
26
+ let mediaType = $derived(
27
+ /\.gif$/i.test(src ?? '')
28
+ ? 'gif'
29
+ : /\.(png|jpe?g|webp|avif)$/i.test(src ?? '')
30
+ ? 'image'
31
+ : 'video'
32
+ );
33
+
34
+ function toggle() {
35
+ if (mediaType === 'video') {
36
+ if (!videoEl) return;
37
+ if (playing) {
38
+ videoEl.pause();
39
+ } else {
40
+ videoEl.play();
41
+ }
42
+ } else if (mediaType === 'gif') {
43
+ if (playing && gifEl && canvasEl) {
44
+ canvasEl.width = gifEl.naturalWidth;
45
+ canvasEl.height = gifEl.naturalHeight;
46
+ canvasEl.getContext('2d')?.drawImage(gifEl, 0, 0);
47
+ }
48
+ }
49
+ playing = !playing;
50
+ }
51
+ </script>
52
+
53
+ <div class="media-preview" style:--media-preview-aspect-ratio={aspectRatio}>
54
+ {#if mediaType === 'image'}
55
+ <img class="media-preview-content" {src} {alt} />
56
+ {:else if mediaType === 'video'}
57
+ <video bind:this={videoEl} class="media-preview-content" {src} autoplay loop muted playsinline
58
+ ></video>
59
+ {:else}
60
+ <img
61
+ bind:this={gifEl}
62
+ class="media-preview-content"
63
+ {src}
64
+ {alt}
65
+ style:display={playing ? 'block' : 'none'}
66
+ />
67
+ <canvas
68
+ bind:this={canvasEl}
69
+ class="media-preview-content"
70
+ style:display={playing ? 'none' : 'block'}
71
+ ></canvas>
72
+ {/if}
73
+
74
+ {#if mediaType !== 'image'}
75
+ <Button
76
+ variant="overlay"
77
+ size="lg"
78
+ icon={playing ? Pause : Play}
79
+ label={playing ? pauseLabel : playLabel}
80
+ onclick={toggle}
81
+ class="media-preview-toggle"
82
+ />
83
+ {/if}
84
+ </div>
85
+
86
+ <style>
87
+ .media-preview {
88
+ position: relative;
89
+ width: 100%;
90
+ aspect-ratio: var(--media-preview-aspect-ratio);
91
+ overflow: hidden;
92
+ border: 1px solid var(--color-border);
93
+ border-radius: 10px;
94
+ background-color: var(--color-surface);
95
+ box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.25);
96
+ }
97
+
98
+ .media-preview-content {
99
+ display: block;
100
+ width: 100%;
101
+ height: 100%;
102
+ object-fit: cover;
103
+ }
104
+
105
+ :global(.media-preview-toggle) {
106
+ position: absolute;
107
+ top: 12px;
108
+ right: 12px;
109
+ }
110
+ </style>
@@ -0,0 +1,11 @@
1
+ type $$ComponentProps = {
2
+ src?: string;
3
+ alt?: string;
4
+ /** CSS aspect-ratio of the frame. */
5
+ aspectRatio?: string;
6
+ playLabel?: string;
7
+ pauseLabel?: string;
8
+ };
9
+ declare const MediaPreview: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type MediaPreview = ReturnType<typeof MediaPreview>;
11
+ export default MediaPreview;
@@ -45,7 +45,7 @@
45
45
  {...rest}
46
46
  bind:this={ref}
47
47
  class={['mesh-gradient', className]}
48
- style:background-color={colors[0]}
48
+ style:--mesh-color={colors[0]}
49
49
  aria-hidden="true"
50
50
  >
51
51
  {#each blobs as blob, i (i)}
@@ -55,7 +55,7 @@
55
55
  style:top="{blob.y}%"
56
56
  style:width="{blob.size}%"
57
57
  style:height="{blob.size}%"
58
- style:background-color={blob.color}
58
+ style:--mesh-color={blob.color}
59
59
  style:filter="blur({blob.blur}px)"
60
60
  ></div>
61
61
  {/each}
@@ -69,12 +69,29 @@
69
69
  width: 100%;
70
70
  height: 100%;
71
71
  overflow: hidden;
72
+ /* Keeps the overlay noise from blending with the page behind, which lights up the edge in dark mode. */
73
+ isolation: isolate;
74
+ background-color: var(--mesh-color);
72
75
  }
73
76
 
74
77
  .mesh-gradient-blob {
75
78
  position: absolute;
76
79
  border-radius: 50%;
77
80
  transform: translate(-50%, -50%);
81
+ background-color: var(--mesh-color);
82
+ }
83
+
84
+ /* Pale colours would glare against a dark page, so they are mixed towards the page colour. */
85
+ @media (prefers-color-scheme: dark) {
86
+ :root:not([data-theme='light']) .mesh-gradient,
87
+ :root:not([data-theme='light']) .mesh-gradient-blob {
88
+ background-color: color-mix(in srgb, var(--mesh-color) 70%, var(--color-base));
89
+ }
90
+ }
91
+
92
+ :root[data-theme='dark'] .mesh-gradient,
93
+ :root[data-theme='dark'] .mesh-gradient-blob {
94
+ background-color: color-mix(in srgb, var(--mesh-color) 70%, var(--color-base));
78
95
  }
79
96
 
80
97
  .mesh-gradient::after {
@@ -0,0 +1,83 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Section from '../section/Section.svelte';
4
+
5
+ let {
6
+ eyebrow,
7
+ title,
8
+ description,
9
+ children,
10
+ footer
11
+ }: {
12
+ eyebrow?: string;
13
+ title: string | Snippet;
14
+ description?: string | Snippet;
15
+ /** Below the description, e.g. a media preview. */
16
+ children?: Snippet;
17
+ /** Below the header box, e.g. a divider with actions. */
18
+ footer?: Snippet;
19
+ } = $props();
20
+ </script>
21
+
22
+ <Section width="full" surface={false} padded={false}>
23
+ <div class="page-header-box">
24
+ {#if eyebrow}
25
+ <span class="page-header-eyebrow">{eyebrow}</span>
26
+ {/if}
27
+ <h1 class="page-header-title">
28
+ {#if typeof title === 'string'}{title}{:else}{@render title()}{/if}
29
+ </h1>
30
+ {#if description}
31
+ <p class="page-header-description">
32
+ {#if typeof description === 'string'}{description}{:else}{@render description()}{/if}
33
+ </p>
34
+ {/if}
35
+ {@render children?.()}
36
+ </div>
37
+ {@render footer?.()}
38
+ </Section>
39
+
40
+ <style>
41
+ .page-header-box {
42
+ margin-inline: auto;
43
+ max-width: 1280px;
44
+ padding: 56px 0 30px;
45
+ display: flex;
46
+ flex-direction: column;
47
+ align-items: center;
48
+ user-select: none;
49
+ }
50
+
51
+ .page-header-eyebrow {
52
+ font-size: 14px;
53
+ font-weight: 500;
54
+ margin-bottom: 4px;
55
+ }
56
+
57
+ .page-header-title {
58
+ /* Tighter lines for multi-line titles; the margin gives back the half-leading
59
+ of the default 1.5, so the gaps above and below stay the same. */
60
+ line-height: 1.15;
61
+ margin-block: 0.175em;
62
+ text-align: center;
63
+ }
64
+
65
+ .page-header-description {
66
+ margin-top: 8px;
67
+ max-width: 440px;
68
+ text-align: center;
69
+ color: var(--color-text-secondary);
70
+ }
71
+
72
+ @media (min-width: 640px) {
73
+ .page-header-box {
74
+ padding: 72px 0 40px;
75
+ }
76
+ }
77
+
78
+ @media (min-width: 1024px) {
79
+ .page-header-box {
80
+ padding-bottom: 64px;
81
+ }
82
+ }
83
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ eyebrow?: string;
4
+ title: string | Snippet;
5
+ description?: string | Snippet;
6
+ /** Below the description, e.g. a media preview. */
7
+ children?: Snippet;
8
+ /** Below the header box, e.g. a divider with actions. */
9
+ footer?: Snippet;
10
+ };
11
+ declare const PageHeader: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type PageHeader = ReturnType<typeof PageHeader>;
13
+ export default PageHeader;
@@ -6,8 +6,8 @@
6
6
  /** The root element. Bindable. */
7
7
  ref?: HTMLElement | null;
8
8
  /**
9
- * `contained` is an inset, rounded grey box; `full` is a grey band that runs edge to edge
10
- * and keeps its content at `--section-max-width`.
9
+ * `contained` is an inset, rounded grey box with the page background to its left and right;
10
+ * `full` is a band that runs edge to edge and keeps its content at `--section-max-width`.
11
11
  */
12
12
  width?: 'contained' | 'full';
13
13
  /**
@@ -15,7 +15,10 @@
15
15
  * `contained` (give the box your own padding via `class`).
16
16
  */
17
17
  padded?: boolean;
18
- /** Fills the section with `--color-surface`. Off, it sits on the page background. */
18
+ /**
19
+ * Fills the section with `--color-surface` (grey). On by default for `contained`, off for
20
+ * `full`.
21
+ */
19
22
  surface?: boolean;
20
23
  children?: Snippet;
21
24
  };
@@ -24,13 +27,14 @@
24
27
  ref = $bindable(null),
25
28
  width = 'contained',
26
29
  padded,
27
- surface = true,
30
+ surface,
28
31
  class: className,
29
32
  children,
30
33
  ...rest
31
34
  }: Props = $props();
32
35
 
33
36
  let hasPadding = $derived(padded ?? width === 'full');
37
+ let hasSurface = $derived(surface ?? width === 'contained');
34
38
  </script>
35
39
 
36
40
  <section
@@ -39,7 +43,7 @@
39
43
  class={[
40
44
  'section',
41
45
  `section--${width}`,
42
- { 'section--padded': hasPadding, 'section--surface': surface },
46
+ { 'section--padded': hasPadding, 'section--surface': hasSurface },
43
47
  className
44
48
  ]}
45
49
  >
@@ -4,8 +4,8 @@ type Props = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
4
4
  /** The root element. Bindable. */
5
5
  ref?: HTMLElement | null;
6
6
  /**
7
- * `contained` is an inset, rounded grey box; `full` is a grey band that runs edge to edge
8
- * and keeps its content at `--section-max-width`.
7
+ * `contained` is an inset, rounded grey box with the page background to its left and right;
8
+ * `full` is a band that runs edge to edge and keeps its content at `--section-max-width`.
9
9
  */
10
10
  width?: 'contained' | 'full';
11
11
  /**
@@ -13,7 +13,10 @@ type Props = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
13
13
  * `contained` (give the box your own padding via `class`).
14
14
  */
15
15
  padded?: boolean;
16
- /** Fills the section with `--color-surface`. Off, it sits on the page background. */
16
+ /**
17
+ * Fills the section with `--color-surface` (grey). On by default for `contained`, off for
18
+ * `full`.
19
+ */
17
20
  surface?: boolean;
18
21
  children?: Snippet;
19
22
  };
@@ -6,7 +6,7 @@ export type SelectOption<T extends string | number | null = string> = {
6
6
  import { type Snippet } from 'svelte';
7
7
  import type { HTMLButtonAttributes } from 'svelte/elements';
8
8
  declare function $$render<T extends string | number | null>(): {
9
- props: Omit<HTMLButtonAttributes, "children" | "role" | "onchange" | "name" | "type" | "value"> & {
9
+ props: Omit<HTMLButtonAttributes, "name" | "type" | "role" | "children" | "onchange" | "value"> & {
10
10
  /** Value of the chosen option. Bindable. */
11
11
  value?: T;
12
12
  options: SelectOption<T>[];