@vit-foundation/ui 0.7.0 → 0.10.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 (39) hide show
  1. package/dist/admin.d.ts +11 -0
  2. package/dist/admin.js +11 -0
  3. package/dist/components/admin/AdminShell.svelte +42 -0
  4. package/dist/components/admin/AdminShell.svelte.d.ts +18 -0
  5. package/dist/components/admin/DecorMosaic.svelte +129 -0
  6. package/dist/components/admin/DecorMosaic.svelte.d.ts +10 -0
  7. package/dist/components/admin/PageHeading.svelte +41 -0
  8. package/dist/components/admin/PageHeading.svelte.d.ts +14 -0
  9. package/dist/components/admin/Sidebar.svelte +149 -0
  10. package/dist/components/admin/Sidebar.svelte.d.ts +23 -0
  11. package/dist/components/contact/ContactForm.svelte +4 -0
  12. package/dist/components/projects/ProjectCard.svelte +11 -3
  13. package/dist/components/timeline/TimelineMilestone.svelte +21 -6
  14. package/dist/components/ui/Button.svelte +118 -23
  15. package/dist/components/ui/Button.svelte.d.ts +30 -4
  16. package/dist/components/ui/Field.svelte +23 -2
  17. package/dist/components/ui/Field.svelte.d.ts +8 -0
  18. package/dist/components/ui/FilterChips.svelte +21 -8
  19. package/dist/components/ui/FilterChips.svelte.d.ts +8 -0
  20. package/dist/components/ui/Icon.svelte +46 -0
  21. package/dist/components/ui/Icon.svelte.d.ts +24 -0
  22. package/dist/components/ui/IconButton.svelte +41 -0
  23. package/dist/components/ui/IconButton.svelte.d.ts +11 -0
  24. package/dist/components/ui/Logo.svelte +63 -0
  25. package/dist/components/ui/Logo.svelte.d.ts +9 -0
  26. package/dist/components/ui/Modal.svelte +79 -0
  27. package/dist/components/ui/Modal.svelte.d.ts +14 -0
  28. package/dist/components/ui/TextField.svelte +116 -0
  29. package/dist/components/ui/TextField.svelte.d.ts +36 -0
  30. package/dist/components/weeklies/SortSelect.svelte +12 -7
  31. package/dist/edit/types.d.ts +1 -1
  32. package/dist/forms/index.d.ts +1 -0
  33. package/dist/forms/index.js +1 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.js +2 -1
  36. package/dist/primitives.d.ts +4 -0
  37. package/dist/primitives.js +4 -0
  38. package/dist/styles/tokens.css +7 -0
  39. package/package.json +6 -1
@@ -1,52 +1,147 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import type { HTMLButtonAttributes } from 'svelte/elements';
3
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
4
4
 
5
- interface Props extends HTMLButtonAttributes {
5
+ interface CommonProps {
6
6
  /**
7
- * The submitting count of the remote form this button posts, or `null`
8
- * for a control that genuinely has no pending state — the classic
9
- * `action="?/…"` post, which has no count to read.
10
- *
11
- * Required and explicitly nullable, for the reason **field constraint**
12
- * gives: optional made "this control cannot be pending" and "nobody
13
- * thought about it" one value, and two submits had already taken the
14
- * second — both logout, both double-submittable. A count rather than a
15
- * boolean because that is what kit hands back, and `> 0` was the rule
16
- * twelve call sites re-derived. It belongs here, once.
7
+ * A closed named set, not orthogonal booleans (the PageShell rationale).
8
+ * `primary` is the brand fill and the default — the only look this
9
+ * component had before variants existed, so old call sites render
10
+ * byte-identically. `navy` and `ghost` come from the admin app.
17
11
  */
18
- pending: number | null;
12
+ variant?: 'primary' | 'navy' | 'ghost';
13
+ size?: 'md' | 'sm';
19
14
  children: Snippet;
20
15
  }
21
16
 
22
- let { pending, children, type = 'button', disabled = false, ...rest }: Props = $props();
17
+ type ButtonProps = CommonProps &
18
+ Omit<HTMLButtonAttributes, 'type'> & {
19
+ type?: 'button' | 'submit' | 'reset';
20
+ /**
21
+ * The submitting count of the remote form this button posts, or `null`
22
+ * for a control that genuinely has no pending state — the classic
23
+ * `action="?/…"` post, which has no count to read.
24
+ *
25
+ * Required and explicitly nullable, for the reason **field constraint**
26
+ * gives: optional made "this control cannot be pending" and "nobody
27
+ * thought about it" one value, and two submits had already taken the
28
+ * second — both logout, both double-submittable. A count rather than a
29
+ * boolean because that is what kit hands back, and `> 0` was the rule
30
+ * twelve call sites re-derived. It belongs here, once.
31
+ */
32
+ pending: number | null;
33
+ href?: never;
34
+ download?: never;
35
+ };
36
+
37
+ type AnchorProps = CommonProps &
38
+ Omit<HTMLAnchorAttributes, 'download' | 'type'> & {
39
+ /**
40
+ * Renders an anchor styled as a button (downloads, external tools).
41
+ * An anchor has no form behind it, so the branch admits no `pending`
42
+ * and no `disabled` — an unavailable action is a button's business.
43
+ */
44
+ href: string;
45
+ download?: boolean;
46
+ pending?: never;
47
+ disabled?: never;
48
+ type?: never;
49
+ };
50
+
51
+ type Props = ButtonProps | AnchorProps;
52
+
53
+ let {
54
+ variant = 'primary',
55
+ size = 'md',
56
+ pending = null,
57
+ href = undefined,
58
+ download = false,
59
+ children,
60
+ type = 'button',
61
+ disabled = false,
62
+ ...rest
63
+ }: Props = $props();
23
64
 
24
65
  // `disabled` stays a separate prop: a button can be unavailable for a reason
25
66
  // of its own — the Google button before consent — as well as while posting.
26
67
  const isPending = $derived((pending ?? 0) > 0);
27
68
  </script>
28
69
 
29
- <button {type} {...rest} disabled={disabled || isPending} aria-busy={isPending || undefined}>
30
- {@render children()}
31
- </button>
70
+ {#if href !== undefined}
71
+ <a
72
+ class="button {variant} {size}"
73
+ {href}
74
+ download={download ? '' : undefined}
75
+ {...rest as HTMLAnchorAttributes}
76
+ >
77
+ {@render children()}
78
+ </a>
79
+ {:else}
80
+ <button
81
+ class="button {variant} {size}"
82
+ {type}
83
+ {...rest as HTMLButtonAttributes}
84
+ disabled={disabled || isPending}
85
+ aria-busy={isPending || undefined}
86
+ >
87
+ {@render children()}
88
+ </button>
89
+ {/if}
32
90
 
33
91
  <style>
34
- button {
92
+ /* The `primary md` declarations reproduce the pre-variant component
93
+ exactly — consumers of the old single look must not repaint. */
94
+ .button {
35
95
  padding: var(--space-2) var(--space-4);
36
96
  border: none;
37
97
  border-radius: var(--radius);
38
- background: var(--color-brand);
39
- color: var(--color-surface);
40
98
  font-weight: 600;
41
99
  cursor: pointer;
42
- transition: background var(--transition-fast);
100
+ transition:
101
+ background var(--transition-fast),
102
+ filter var(--transition-fast);
103
+ }
104
+
105
+ a.button {
106
+ display: inline-block;
107
+ text-decoration: none;
108
+ text-align: center;
109
+ }
110
+
111
+ .sm {
112
+ padding: var(--space-1) var(--space-3);
113
+ font-size: var(--text-sm);
43
114
  }
44
115
 
45
- button:hover {
116
+ .primary {
117
+ background: var(--color-brand);
118
+ color: var(--color-surface);
119
+ }
120
+
121
+ .primary:hover:not(:disabled) {
122
+ background: var(--color-navy);
123
+ }
124
+
125
+ .navy {
46
126
  background: var(--color-navy);
127
+ color: var(--color-surface);
128
+ }
129
+
130
+ .navy:hover:not(:disabled) {
131
+ filter: brightness(1.25);
132
+ }
133
+
134
+ .ghost {
135
+ background: transparent;
136
+ color: var(--color-ink);
137
+ border: 1px solid var(--color-ink);
138
+ }
139
+
140
+ .ghost:hover:not(:disabled) {
141
+ background: color-mix(in srgb, var(--color-ink) 8%, transparent);
47
142
  }
48
143
 
49
- button:disabled {
144
+ .button:disabled {
50
145
  opacity: 0.6;
51
146
  cursor: default;
52
147
  }
@@ -1,6 +1,18 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { HTMLButtonAttributes } from 'svelte/elements';
3
- interface Props extends HTMLButtonAttributes {
2
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
3
+ interface CommonProps {
4
+ /**
5
+ * A closed named set, not orthogonal booleans (the PageShell rationale).
6
+ * `primary` is the brand fill and the default — the only look this
7
+ * component had before variants existed, so old call sites render
8
+ * byte-identically. `navy` and `ghost` come from the admin app.
9
+ */
10
+ variant?: 'primary' | 'navy' | 'ghost';
11
+ size?: 'md' | 'sm';
12
+ children: Snippet;
13
+ }
14
+ type ButtonProps = CommonProps & Omit<HTMLButtonAttributes, 'type'> & {
15
+ type?: 'button' | 'submit' | 'reset';
4
16
  /**
5
17
  * The submitting count of the remote form this button posts, or `null`
6
18
  * for a control that genuinely has no pending state — the classic
@@ -14,8 +26,22 @@ interface Props extends HTMLButtonAttributes {
14
26
  * twelve call sites re-derived. It belongs here, once.
15
27
  */
16
28
  pending: number | null;
17
- children: Snippet;
18
- }
29
+ href?: never;
30
+ download?: never;
31
+ };
32
+ type AnchorProps = CommonProps & Omit<HTMLAnchorAttributes, 'download' | 'type'> & {
33
+ /**
34
+ * Renders an anchor styled as a button (downloads, external tools).
35
+ * An anchor has no form behind it, so the branch admits no `pending`
36
+ * and no `disabled` — an unavailable action is a button's business.
37
+ */
38
+ href: string;
39
+ download?: boolean;
40
+ pending?: never;
41
+ disabled?: never;
42
+ type?: never;
43
+ };
44
+ type Props = ButtonProps | AnchorProps;
19
45
  declare const Button: import("svelte").Component<Props, {}, "">;
20
46
  type Button = ReturnType<typeof Button>;
21
47
  export default Button;
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { FieldConstraint } from '../../content/types.js';
4
+ import ActionLabel from '../../edit/ActionLabel.svelte';
5
+ import type { EditDescriptor } from '../../edit/types.js';
4
6
 
5
7
  /** Attributes the control must spread (`{...attrs}`) to stay wired. */
6
8
  interface ControlAttrs {
@@ -46,18 +48,37 @@
46
48
  constraint: FieldConstraint | null;
47
49
  /** Render the label visually hidden (surrounding context already labels it). */
48
50
  hideLabel?: boolean;
51
+ /**
52
+ * Edit descriptor for the LABEL's wording. While editing, only the
53
+ * label swaps for editable text — an editable span is not a <label>,
54
+ * so clicking it no longer focuses the control, and the control itself
55
+ * stays rendered and live. Hidden labels are never offered.
56
+ */
57
+ labelEdit?: EditDescriptor;
49
58
  /** The input/textarea/select, spreading the given attrs. */
50
59
  children: Snippet<[ControlAttrs]>;
51
60
  }
52
61
 
53
- let { id, label, field, constraint, hideLabel = false, children }: Props = $props();
62
+ let {
63
+ id,
64
+ label,
65
+ field,
66
+ constraint,
67
+ hideLabel = false,
68
+ labelEdit = undefined,
69
+ children
70
+ }: Props = $props();
54
71
 
55
72
  const errorMessage = $derived(field.issues()?.[0]?.message);
56
73
  const showError = $derived(errorMessage !== undefined);
57
74
  </script>
58
75
 
59
76
  <div class="field">
60
- <label class:visually-hidden={hideLabel} for={id}>{label}</label>
77
+ <ActionLabel edit={hideLabel ? undefined : labelEdit} value={label}>
78
+ {#snippet control()}
79
+ <label class:visually-hidden={hideLabel} for={id}>{label}</label>
80
+ {/snippet}
81
+ </ActionLabel>
61
82
  {@render children({
62
83
  id,
63
84
  'aria-describedby': showError ? `${id}-error` : undefined,
@@ -1,5 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { FieldConstraint } from '../../content/types.js';
3
+ import type { EditDescriptor } from '../../edit/types.js';
3
4
  /** Attributes the control must spread (`{...attrs}`) to stay wired. */
4
5
  interface ControlAttrs {
5
6
  id: string;
@@ -47,6 +48,13 @@ interface Props {
47
48
  constraint: FieldConstraint | null;
48
49
  /** Render the label visually hidden (surrounding context already labels it). */
49
50
  hideLabel?: boolean;
51
+ /**
52
+ * Edit descriptor for the LABEL's wording. While editing, only the
53
+ * label swaps for editable text — an editable span is not a <label>,
54
+ * so clicking it no longer focuses the control, and the control itself
55
+ * stays rendered and live. Hidden labels are never offered.
56
+ */
57
+ labelEdit?: EditDescriptor;
50
58
  /** The input/textarea/select, spreading the given attrs. */
51
59
  children: Snippet<[ControlAttrs]>;
52
60
  }
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import ActionLabel from '../../edit/ActionLabel.svelte';
3
+ import type { EditDescriptor } from '../../edit/types.js';
2
4
  import GhostButton from './GhostButton.svelte';
3
5
 
4
6
  interface Chip {
@@ -13,9 +15,16 @@
13
15
  label: string;
14
16
  /** Called with the toggled value, or null when the active chip is unselected. */
15
17
  onchange: (value: string | null) => void;
18
+ /**
19
+ * Edit descriptors for the chip LABELS — a function because the host
20
+ * owns what a chip's wording is (a theme's localized name, a category
21
+ * message). While editing, a chip with a descriptor renders as
22
+ * editable text instead of a button (see ActionLabel).
23
+ */
24
+ editFor?: (chip: Chip) => EditDescriptor | undefined;
16
25
  }
17
26
 
18
- let { chips, selected, label, onchange }: Props = $props();
27
+ let { chips, selected, label, onchange, editFor = undefined }: Props = $props();
19
28
 
20
29
  function toggle(value: string): void {
21
30
  onchange(selected === value ? null : value);
@@ -24,13 +33,17 @@
24
33
 
25
34
  <div class="chips" role="group" aria-label={label}>
26
35
  {#each chips as chip (chip.value)}
27
- <GhostButton
28
- variant="chip"
29
- aria-pressed={selected === chip.value}
30
- onclick={() => toggle(chip.value)}
31
- >
32
- {chip.label}
33
- </GhostButton>
36
+ <ActionLabel edit={editFor?.(chip)} value={chip.label}>
37
+ {#snippet control()}
38
+ <GhostButton
39
+ variant="chip"
40
+ aria-pressed={selected === chip.value}
41
+ onclick={() => toggle(chip.value)}
42
+ >
43
+ {chip.label}
44
+ </GhostButton>
45
+ {/snippet}
46
+ </ActionLabel>
34
47
  {/each}
35
48
  </div>
36
49
 
@@ -1,3 +1,4 @@
1
+ import type { EditDescriptor } from '../../edit/types.js';
1
2
  interface Chip {
2
3
  value: string;
3
4
  label: string;
@@ -9,6 +10,13 @@ interface Props {
9
10
  label: string;
10
11
  /** Called with the toggled value, or null when the active chip is unselected. */
11
12
  onchange: (value: string | null) => void;
13
+ /**
14
+ * Edit descriptors for the chip LABELS — a function because the host
15
+ * owns what a chip's wording is (a theme's localized name, a category
16
+ * message). While editing, a chip with a descriptor renders as
17
+ * editable text instead of a button (see ActionLabel).
18
+ */
19
+ editFor?: (chip: Chip) => EditDescriptor | undefined;
12
20
  }
13
21
  declare const FilterChips: import("svelte").Component<Props, {}, "">;
14
22
  type FilterChips = ReturnType<typeof FilterChips>;
@@ -0,0 +1,46 @@
1
+ <script lang="ts" module>
2
+ /** Stroke-based 24×24 icon paths. A closed set: extending it is a package
3
+ * change, so every consumer's `IconName` stays honest. */
4
+ const PATHS = {
5
+ home: 'M4 11l8-7 8 7v9a1 1 0 0 1-1 1h-4v-6h-6v6H5a1 1 0 0 1-1-1z',
6
+ clock: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zm0 4v5l3.5 2',
7
+ gear: 'M12 9.5A2.5 2.5 0 1 0 12 14.5 2.5 2.5 0 0 0 12 9.5zm8-.5-1.8-.5a6.6 6.6 0 0 0-.6-1.4l.9-1.6-1.9-1.9-1.6.9c-.5-.3-.9-.5-1.4-.6L13 2h-2l-.5 1.8c-.5.2-1 .4-1.4.6l-1.6-.9-1.9 1.9.9 1.6c-.3.5-.5.9-.6 1.4L4 9v2l1.8.5c.2.5.4 1 .6 1.4l-.9 1.6 1.9 1.9 1.6-.9c.5.3.9.5 1.4.6L11 22h2l.5-1.8c.5-.2 1-.4 1.4-.6l1.6.9 1.9-1.9-.9-1.6c.3-.5.5-.9.6-1.4L20 15z',
8
+ pencil: 'M4 20l1-4L16.5 4.5a2.1 2.1 0 0 1 3 3L8 19zm10.5-13.5l3 3',
9
+ trash:
10
+ 'M5 7h14M10 7V5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2m3 0v12a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V7m3 4v6m4-6v6',
11
+ duplicate: 'M9 9h10v12H9zM5 15V3h10',
12
+ plus: 'M12 5v14M5 12h14',
13
+ 'chevron-right': 'M9 5l7 7-7 7',
14
+ 'chevron-left': 'M15 5l-7 7 7 7',
15
+ close: 'M6 6l12 12M18 6L6 18',
16
+ globe:
17
+ 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM3 12h18M12 3c-2.5 2.4-3.8 5.5-3.8 9s1.3 6.6 3.8 9c2.5-2.4 3.8-5.5 3.8-9S14.5 5.4 12 3z',
18
+ logout: 'M15 4h4a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-4M10 8l-4 4 4 4M6 12h10'
19
+ } as const;
20
+
21
+ export type IconName = keyof typeof PATHS;
22
+ </script>
23
+
24
+ <script lang="ts">
25
+ interface Props {
26
+ name: IconName;
27
+ size?: number;
28
+ }
29
+
30
+ let { name, size = 20 }: Props = $props();
31
+ </script>
32
+
33
+ <svg
34
+ viewBox="0 0 24 24"
35
+ width={size}
36
+ height={size}
37
+ fill="none"
38
+ stroke="currentColor"
39
+ stroke-width="1.8"
40
+ stroke-linecap="round"
41
+ stroke-linejoin="round"
42
+ aria-hidden="true"
43
+ focusable="false"
44
+ >
45
+ <path d={PATHS[name]} />
46
+ </svg>
@@ -0,0 +1,24 @@
1
+ /** Stroke-based 24×24 icon paths. A closed set: extending it is a package
2
+ * change, so every consumer's `IconName` stays honest. */
3
+ declare const PATHS: {
4
+ readonly home: "M4 11l8-7 8 7v9a1 1 0 0 1-1 1h-4v-6h-6v6H5a1 1 0 0 1-1-1z";
5
+ readonly clock: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zm0 4v5l3.5 2";
6
+ readonly gear: "M12 9.5A2.5 2.5 0 1 0 12 14.5 2.5 2.5 0 0 0 12 9.5zm8-.5-1.8-.5a6.6 6.6 0 0 0-.6-1.4l.9-1.6-1.9-1.9-1.6.9c-.5-.3-.9-.5-1.4-.6L13 2h-2l-.5 1.8c-.5.2-1 .4-1.4.6l-1.6-.9-1.9 1.9.9 1.6c-.3.5-.5.9-.6 1.4L4 9v2l1.8.5c.2.5.4 1 .6 1.4l-.9 1.6 1.9 1.9 1.6-.9c.5.3.9.5 1.4.6L11 22h2l.5-1.8c.5-.2 1-.4 1.4-.6l1.6.9 1.9-1.9-.9-1.6c.3-.5.5-.9.6-1.4L20 15z";
7
+ readonly pencil: "M4 20l1-4L16.5 4.5a2.1 2.1 0 0 1 3 3L8 19zm10.5-13.5l3 3";
8
+ readonly trash: "M5 7h14M10 7V5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2m3 0v12a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V7m3 4v6m4-6v6";
9
+ readonly duplicate: "M9 9h10v12H9zM5 15V3h10";
10
+ readonly plus: "M12 5v14M5 12h14";
11
+ readonly 'chevron-right': "M9 5l7 7-7 7";
12
+ readonly 'chevron-left': "M15 5l-7 7 7 7";
13
+ readonly close: "M6 6l12 12M18 6L6 18";
14
+ readonly globe: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM3 12h18M12 3c-2.5 2.4-3.8 5.5-3.8 9s1.3 6.6 3.8 9c2.5-2.4 3.8-5.5 3.8-9S14.5 5.4 12 3z";
15
+ readonly logout: "M15 4h4a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-4M10 8l-4 4 4 4M6 12h10";
16
+ };
17
+ export type IconName = keyof typeof PATHS;
18
+ interface Props {
19
+ name: IconName;
20
+ size?: number;
21
+ }
22
+ declare const Icon: import("svelte").Component<Props, {}, "">;
23
+ type Icon = ReturnType<typeof Icon>;
24
+ export default Icon;
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import Icon, { type IconName } from './Icon.svelte';
3
+
4
+ interface Props {
5
+ icon: IconName;
6
+ /** Accessible name — icon-only buttons must say what they do. */
7
+ label: string;
8
+ disabled?: boolean;
9
+ onclick?: (event: MouseEvent) => void;
10
+ }
11
+
12
+ let { icon, label, disabled = false, onclick }: Props = $props();
13
+ </script>
14
+
15
+ <button type="button" aria-label={label} title={label} {disabled} {onclick}>
16
+ <Icon name={icon} size={18} />
17
+ </button>
18
+
19
+ <style>
20
+ button {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ padding: 0.35rem;
25
+ border: none;
26
+ border-radius: 6px;
27
+ background: transparent;
28
+ color: var(--color-ink);
29
+ cursor: pointer;
30
+ transition: background var(--transition-fast);
31
+ }
32
+
33
+ button:hover:not(:disabled) {
34
+ background: color-mix(in srgb, var(--color-ink) 8%, transparent);
35
+ }
36
+
37
+ button:disabled {
38
+ opacity: 0.4;
39
+ cursor: not-allowed;
40
+ }
41
+ </style>
@@ -0,0 +1,11 @@
1
+ import { type IconName } from './Icon.svelte';
2
+ interface Props {
3
+ icon: IconName;
4
+ /** Accessible name — icon-only buttons must say what they do. */
5
+ label: string;
6
+ disabled?: boolean;
7
+ onclick?: (event: MouseEvent) => void;
8
+ }
9
+ declare const IconButton: import("svelte").Component<Props, {}, "">;
10
+ type IconButton = ReturnType<typeof IconButton>;
11
+ export default IconButton;
@@ -0,0 +1,63 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ /** Height of the head mark in px. */
4
+ size?: number;
5
+ /** Adds the "Brain VIT" wordmark under the mark. */
6
+ withWordmark?: boolean;
7
+ }
8
+
9
+ let { size = 32, withWordmark = false }: Props = $props();
10
+ </script>
11
+
12
+ <span class="logo">
13
+ <svg
14
+ viewBox="0 0 48 56"
15
+ width={size * (48 / 56)}
16
+ height={size}
17
+ aria-hidden={withWordmark ? undefined : true}
18
+ role={withWordmark ? 'img' : undefined}
19
+ focusable="false"
20
+ >
21
+ {#if withWordmark}<title>Brain VIT</title>{/if}
22
+ <path
23
+ d="M25 2c11.6 0 21 9.4 21 21 0 6.9-3.3 13-8.5 16.8V54H17v-9h-4.5A4.5 4.5 0 0 1 8 40.5V34H4.2c-1.7 0-2.7-1.9-1.7-3.3L8 22.4C9 11 16.2 2 25 2Z"
24
+ fill="currentColor"
25
+ />
26
+ <g stroke="var(--color-surface)" stroke-width="3.2" stroke-linecap="round">
27
+ <line x1="26" y1="13" x2="26" y2="31" />
28
+ <line x1="18.2" y1="17.5" x2="33.8" y2="26.5" />
29
+ <line x1="18.2" y1="26.5" x2="33.8" y2="17.5" />
30
+ </g>
31
+ </svg>
32
+ {#if withWordmark}
33
+ <span class="wordmark"><em>Brain</em> <strong>VIT</strong></span>
34
+ {/if}
35
+ </span>
36
+
37
+ <style>
38
+ .logo {
39
+ display: inline-flex;
40
+ flex-direction: column;
41
+ align-items: center;
42
+ gap: var(--space-2);
43
+ color: var(--color-ink);
44
+ }
45
+
46
+ .wordmark {
47
+ font-family: var(--font-serif);
48
+ font-size: 2.5rem;
49
+ line-height: 1;
50
+ letter-spacing: 0.01em;
51
+ white-space: nowrap;
52
+ }
53
+
54
+ .wordmark em {
55
+ color: var(--color-brand);
56
+ font-style: italic;
57
+ font-weight: 600;
58
+ }
59
+
60
+ .wordmark strong {
61
+ font-weight: 600;
62
+ }
63
+ </style>
@@ -0,0 +1,9 @@
1
+ interface Props {
2
+ /** Height of the head mark in px. */
3
+ size?: number;
4
+ /** Adds the "Brain VIT" wordmark under the mark. */
5
+ withWordmark?: boolean;
6
+ }
7
+ declare const Logo: import("svelte").Component<Props, {}, "">;
8
+ type Logo = ReturnType<typeof Logo>;
9
+ export default Logo;
@@ -0,0 +1,79 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import IconButton from './IconButton.svelte';
4
+
5
+ interface Props {
6
+ open: boolean;
7
+ title: string;
8
+ onclose: () => void;
9
+ /** Accessible name of the close control. A prop, not a UiMessages key:
10
+ * `messages` is replaced wholesale by hosts, and a new required key
11
+ * would break every existing catalog. */
12
+ closeLabel?: string;
13
+ children: Snippet;
14
+ }
15
+
16
+ let { open, title, onclose, closeLabel = 'Tanca', children }: Props = $props();
17
+
18
+ let dialog: HTMLDialogElement | undefined = $state();
19
+
20
+ // Native <dialog>: showModal() traps focus and layers the backdrop; the
21
+ // cancel event covers Escape.
22
+ $effect(() => {
23
+ if (!dialog) return;
24
+ if (open && !dialog.open) dialog.showModal();
25
+ if (!open && dialog.open) dialog.close();
26
+ });
27
+
28
+ function onclick(event: MouseEvent) {
29
+ // Backdrop click: the dialog element itself is the target only outside
30
+ // the panel content.
31
+ if (event.target === dialog) onclose();
32
+ }
33
+ </script>
34
+
35
+ <dialog bind:this={dialog} oncancel={onclose} {onclick}>
36
+ <div class="panel">
37
+ <div class="top">
38
+ <h2>{title}</h2>
39
+ <IconButton icon="close" label={closeLabel} onclick={onclose} />
40
+ </div>
41
+ {@render children()}
42
+ </div>
43
+ </dialog>
44
+
45
+ <style>
46
+ dialog {
47
+ margin: auto;
48
+ border: none;
49
+ border-radius: var(--radius);
50
+ padding: 0;
51
+ background: var(--color-surface);
52
+ box-shadow: var(--shadow-2);
53
+ min-width: min(34rem, 90vw);
54
+ }
55
+
56
+ dialog::backdrop {
57
+ background: color-mix(in srgb, var(--color-ink) 45%, transparent);
58
+ }
59
+
60
+ .panel {
61
+ padding: var(--space-4) var(--space-5) var(--space-5);
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: var(--space-4);
65
+ }
66
+
67
+ .top {
68
+ display: flex;
69
+ align-items: flex-start;
70
+ justify-content: space-between;
71
+ gap: var(--space-3);
72
+ }
73
+
74
+ h2 {
75
+ font-weight: 600;
76
+ font-size: var(--text-lg);
77
+ margin: 0;
78
+ }
79
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ open: boolean;
4
+ title: string;
5
+ onclose: () => void;
6
+ /** Accessible name of the close control. A prop, not a UiMessages key:
7
+ * `messages` is replaced wholesale by hosts, and a new required key
8
+ * would break every existing catalog. */
9
+ closeLabel?: string;
10
+ children: Snippet;
11
+ }
12
+ declare const Modal: import("svelte").Component<Props, {}, "">;
13
+ type Modal = ReturnType<typeof Modal>;
14
+ export default Modal;