@vit-foundation/ui 0.9.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.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Shell-level composition for the foundation's ADMIN apps (Brain and any
3
+ * future internal tool). Primitives stay in /primitives; only what shapes an
4
+ * admin page lives here. The host owns routes, permissions, and auth — these
5
+ * components receive already-filtered data through props, the Nav/Footer
6
+ * genericization pattern.
7
+ */
8
+ export { default as AdminShell } from './components/admin/AdminShell.svelte';
9
+ export { default as DecorMosaic } from './components/admin/DecorMosaic.svelte';
10
+ export { default as PageHeading } from './components/admin/PageHeading.svelte';
11
+ export { default as Sidebar, type SidebarItem } from './components/admin/Sidebar.svelte';
package/dist/admin.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Shell-level composition for the foundation's ADMIN apps (Brain and any
3
+ * future internal tool). Primitives stay in /primitives; only what shapes an
4
+ * admin page lives here. The host owns routes, permissions, and auth — these
5
+ * components receive already-filtered data through props, the Nav/Footer
6
+ * genericization pattern.
7
+ */
8
+ export { default as AdminShell } from './components/admin/AdminShell.svelte';
9
+ export { default as DecorMosaic } from './components/admin/DecorMosaic.svelte';
10
+ export { default as PageHeading } from './components/admin/PageHeading.svelte';
11
+ export { default as Sidebar } from './components/admin/Sidebar.svelte';
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Sidebar, { type SidebarItem } from './Sidebar.svelte';
4
+
5
+ interface Props {
6
+ /** Rail entries, already permission-filtered by the host. */
7
+ items: SidebarItem[];
8
+ /** Accessible name of the sidebar nav landmark. */
9
+ navLabel?: string;
10
+ logo?: Snippet;
11
+ /** Bottom of the rail — the host's logout form. */
12
+ footer?: Snippet;
13
+ /** The page content. */
14
+ children: Snippet;
15
+ /** Highlighting override for tests and stories. */
16
+ url?: URL;
17
+ }
18
+
19
+ let { items, navLabel = 'Principal', logo, footer, children, url = undefined }: Props = $props();
20
+ </script>
21
+
22
+ <div class="shell">
23
+ <Sidebar {items} label={navLabel} {logo} {footer} {url} />
24
+ <main class="content">
25
+ {@render children()}
26
+ </main>
27
+ </div>
28
+
29
+ <style>
30
+ /* The sidebar is fixed at 4.5rem; the content pays for it once, here,
31
+ instead of every page knowing the rail's width. */
32
+ .content {
33
+ margin-left: 4.5rem;
34
+ min-height: 100vh;
35
+ }
36
+
37
+ @media print {
38
+ .content {
39
+ margin-left: 0;
40
+ }
41
+ }
42
+ </style>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type SidebarItem } from './Sidebar.svelte';
3
+ interface Props {
4
+ /** Rail entries, already permission-filtered by the host. */
5
+ items: SidebarItem[];
6
+ /** Accessible name of the sidebar nav landmark. */
7
+ navLabel?: string;
8
+ logo?: Snippet;
9
+ /** Bottom of the rail — the host's logout form. */
10
+ footer?: Snippet;
11
+ /** The page content. */
12
+ children: Snippet;
13
+ /** Highlighting override for tests and stories. */
14
+ url?: URL;
15
+ }
16
+ declare const AdminShell: import("svelte").Component<Props, {}, "">;
17
+ type AdminShell = ReturnType<typeof AdminShell>;
18
+ export default AdminShell;
@@ -0,0 +1,129 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ /**
4
+ * 'login' — full-page geometric mosaic behind an auth card;
5
+ * 'app' — lighter scatter on app pages (top-right + bottom strip).
6
+ */
7
+ variant?: 'login' | 'app';
8
+ }
9
+
10
+ let { variant = 'app' }: Props = $props();
11
+ </script>
12
+
13
+ <!-- Decorative geometric mosaic (brand shapes: squares, circles, quarter
14
+ circles, triangles). Purely visual; pointer events pass through. Named
15
+ apart from DecorShapes: that one is the website's corner composition,
16
+ this is the admin apps' full mosaic. -->
17
+ <div class="decor" aria-hidden="true">
18
+ {#if variant === 'login'}
19
+ <svg class="cluster tl" viewBox="0 0 320 400" width="320" height="400">
20
+ <circle cx="120" cy="40" r="40" fill="var(--color-navy)" />
21
+ <rect x="240" y="0" width="80" height="80" fill="var(--color-navy)" />
22
+ <circle cx="40" cy="160" r="40" fill="var(--color-navy)" />
23
+ <path d="M80 120h80v80l-80-80z" fill="var(--color-ink)" />
24
+ <path d="M80 200 L160 120 L160 200 Z" fill="var(--color-navy)" />
25
+ <rect x="160" y="120" width="80" height="80" fill="var(--color-navy)" />
26
+ <rect x="0" y="80" width="60" height="60" fill="var(--color-cream)" />
27
+ <path d="M80 200 A80 80 0 0 1 160 280 L80 280 Z" fill="var(--color-navy)" />
28
+ <rect x="160" y="200" width="80" height="80" fill="var(--color-cream)" />
29
+ <path d="M160 200 L240 200 L160 280 Z" fill="var(--color-brand)" />
30
+ <path d="M0 280 L80 280 L80 360 Z" fill="var(--color-ink)" />
31
+ <path d="M0 280 L80 360 L0 360 Z" fill="var(--color-brand)" />
32
+ <rect x="80" y="280" width="80" height="80" fill="var(--color-navy)" />
33
+ <circle cx="120" cy="320" r="40" fill="var(--color-cream)" />
34
+ </svg>
35
+ <svg class="cluster tr" viewBox="0 0 280 280" width="280" height="280">
36
+ <path d="M0 0h80v80H0z" fill="var(--color-navy)" />
37
+ <path d="M0 0 L80 0 L0 80 Z" fill="var(--color-ink)" />
38
+ <rect x="80" y="0" width="80" height="80" fill="var(--color-brand)" />
39
+ <path d="M80 0 L160 0 L80 80 Z" fill="var(--color-ink)" />
40
+ <circle cx="220" cy="60" r="45" fill="var(--color-cream)" />
41
+ <path d="M0 80 L80 160 L0 160 Z" fill="var(--color-ink)" />
42
+ <path d="M0 80 L80 80 L80 160 Z" fill="var(--color-navy)" />
43
+ <path d="M80 160 L160 80 L160 160 Z" fill="var(--color-brand)" />
44
+ </svg>
45
+ <svg class="cluster br" viewBox="0 0 400 160" width="400" height="160">
46
+ <path
47
+ d="M0 0 A80 80 0 0 1 80 80 L0 80 Z"
48
+ fill="color-mix(in srgb, var(--color-brand) 70%, var(--color-ink))"
49
+ />
50
+ <rect x="80" y="0" width="80" height="80" fill="var(--color-ink)" />
51
+ <path d="M80 0 L160 80 L80 80 Z" fill="var(--color-navy)" />
52
+ <circle cx="240" cy="60" r="42" fill="var(--color-brand)" />
53
+ <circle cx="330" cy="60" r="42" fill="var(--color-brand)" />
54
+ <rect x="180" y="100" width="60" height="60" fill="var(--color-navy)" opacity="0.15" />
55
+ </svg>
56
+ <svg class="cluster bl" viewBox="0 0 300 120" width="300" height="120">
57
+ <circle cx="60" cy="60" r="50" fill="var(--color-cream)" />
58
+ <rect x="150" y="10" width="90" height="90" fill="var(--color-brand)" />
59
+ </svg>
60
+ {:else}
61
+ <svg class="cluster tr" viewBox="0 0 560 180" width="560" height="180">
62
+ <rect x="0" y="0" width="90" height="90" fill="var(--color-navy)" />
63
+ <path d="M0 0 L90 0 L0 90 Z" fill="var(--color-ink)" />
64
+ <path d="M90 0 A90 90 0 0 1 180 90 L90 90 Z" fill="var(--color-cream)" />
65
+ <circle cx="300" cy="45" r="55" fill="var(--color-brand)" />
66
+ <circle cx="420" cy="45" r="55" fill="var(--color-brand)" />
67
+ <circle cx="120" cy="160" r="60" fill="var(--color-brand)" />
68
+ <rect x="470" y="120" width="90" height="90" fill="var(--color-navy)" />
69
+ </svg>
70
+ <svg class="cluster bottom" viewBox="0 0 900 120" width="900" height="120">
71
+ <circle cx="80" cy="90" r="70" fill="var(--color-cream)" />
72
+ <rect x="380" y="30" width="110" height="110" fill="var(--color-brand)" />
73
+ <circle cx="800" cy="100" r="70" fill="var(--color-navy)" />
74
+ </svg>
75
+ {/if}
76
+ </div>
77
+
78
+ <style>
79
+ .decor {
80
+ position: absolute;
81
+ inset: 0;
82
+ overflow: hidden;
83
+ pointer-events: none;
84
+ z-index: 0;
85
+ }
86
+
87
+ .cluster {
88
+ position: absolute;
89
+ }
90
+
91
+ .tl {
92
+ top: 4%;
93
+ left: 3%;
94
+ }
95
+
96
+ .tr {
97
+ top: 0;
98
+ right: 0;
99
+ }
100
+
101
+ .br {
102
+ bottom: 10%;
103
+ right: 0;
104
+ }
105
+
106
+ .bl {
107
+ bottom: -2%;
108
+ left: 12%;
109
+ }
110
+
111
+ .bottom {
112
+ bottom: -3rem;
113
+ left: 8%;
114
+ }
115
+
116
+ @media (max-width: 900px) {
117
+ .tl,
118
+ .bl,
119
+ .br {
120
+ display: none;
121
+ }
122
+ }
123
+
124
+ @media print {
125
+ .decor {
126
+ display: none;
127
+ }
128
+ }
129
+ </style>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ /**
3
+ * 'login' — full-page geometric mosaic behind an auth card;
4
+ * 'app' — lighter scatter on app pages (top-right + bottom strip).
5
+ */
6
+ variant?: 'login' | 'app';
7
+ }
8
+ declare const DecorMosaic: import("svelte").Component<Props, {}, "">;
9
+ type DecorMosaic = ReturnType<typeof DecorMosaic>;
10
+ export default DecorMosaic;
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ /**
4
+ * Italic brand-colored word ("My", "Hi", "Workers"). Optional: a plain
5
+ * title is a heading too, and forcing an accent made call sites
6
+ * hand-roll their own h1.
7
+ */
8
+ accent?: string;
9
+ /** The serif rest of the heading ("Time Control", "Users"). */
10
+ rest: string;
11
+ level?: 1 | 2;
12
+ }
13
+
14
+ let { accent, rest, level = 1 }: Props = $props();
15
+ </script>
16
+
17
+ <svelte:element this={`h${level}`} class="heading">
18
+ {#if accent}<em>{accent}</em>{/if}
19
+ {rest}
20
+ </svelte:element>
21
+
22
+ <style>
23
+ .heading {
24
+ font-family: var(--font-serif);
25
+ font-weight: 600;
26
+ font-size: 3rem;
27
+ line-height: var(--leading-tight);
28
+ margin: 0;
29
+ color: var(--color-ink);
30
+ }
31
+
32
+ h2.heading {
33
+ font-size: 2.1rem;
34
+ }
35
+
36
+ em {
37
+ color: var(--color-brand);
38
+ font-style: italic;
39
+ font-weight: 600;
40
+ }
41
+ </style>
@@ -0,0 +1,14 @@
1
+ interface Props {
2
+ /**
3
+ * Italic brand-colored word ("My", "Hi", "Workers"). Optional: a plain
4
+ * title is a heading too, and forcing an accent made call sites
5
+ * hand-roll their own h1.
6
+ */
7
+ accent?: string;
8
+ /** The serif rest of the heading ("Time Control", "Users"). */
9
+ rest: string;
10
+ level?: 1 | 2;
11
+ }
12
+ declare const PageHeading: import("svelte").Component<Props, {}, "">;
13
+ type PageHeading = ReturnType<typeof PageHeading>;
14
+ export default PageHeading;
@@ -0,0 +1,149 @@
1
+ <script lang="ts" module>
2
+ import type { IconName } from '../ui/Icon.svelte';
3
+
4
+ /** One rail entry. The host owns routes, labels, and who sees what — a
5
+ * permission-filtered list arrives here already filtered. */
6
+ export interface SidebarItem {
7
+ href: string;
8
+ icon: IconName;
9
+ label: string;
10
+ }
11
+ </script>
12
+
13
+ <script lang="ts">
14
+ import type { Snippet } from 'svelte';
15
+ import { getUiConfig } from '../../config/context.js';
16
+ import { isPathUnder } from '../../utils/paths.js';
17
+ import Icon from '../ui/Icon.svelte';
18
+
19
+ interface Props {
20
+ items: SidebarItem[];
21
+ /** Accessible name of the nav landmark. */
22
+ label?: string;
23
+ /** Brand mark above the rail; the host owns its logo and home link. */
24
+ logo?: Snippet;
25
+ /** Bottom slot — the host's logout form lives here, never in the package. */
26
+ footer?: Snippet;
27
+ /** The URL highlighting reads from. Only tests and stories pass one. */
28
+ url?: URL;
29
+ }
30
+
31
+ let { items, label = 'Principal', logo, footer, url = undefined }: Props = $props();
32
+
33
+ const config = getUiConfig();
34
+
35
+ // The localhost fallback keeps a provider-less render (a story, a bare
36
+ // test) standing: nothing highlights, nothing crashes — the Nav pattern.
37
+ const currentUrl = $derived(url ?? config.url() ?? new URL('http://localhost/'));
38
+ const currentPath = $derived(config.canonicalPathname(currentUrl));
39
+
40
+ // '/' matches only itself; a section stays current on its subpages.
41
+ const isCurrent = (href: string): boolean =>
42
+ href === '/' ? currentPath === '/' : isPathUnder(currentPath, href);
43
+ </script>
44
+
45
+ <nav class="sidebar" aria-label={label}>
46
+ {#if logo}
47
+ {@render logo()}
48
+ {/if}
49
+
50
+ <div class="rail">
51
+ {#each items as item (item.href)}
52
+ <a
53
+ href={item.href}
54
+ class="item"
55
+ class:active={isCurrent(item.href)}
56
+ aria-label={item.label}
57
+ aria-current={isCurrent(item.href) ? 'page' : undefined}
58
+ title={item.label}
59
+ >
60
+ <Icon name={item.icon} />
61
+ </a>
62
+ {/each}
63
+ </div>
64
+
65
+ {#if footer}
66
+ <div class="footer">
67
+ {@render footer()}
68
+ </div>
69
+ {/if}
70
+ </nav>
71
+
72
+ <style>
73
+ .sidebar {
74
+ position: fixed;
75
+ top: 0;
76
+ left: 0;
77
+ bottom: 0;
78
+ width: 4.5rem;
79
+ display: flex;
80
+ flex-direction: column;
81
+ align-items: center;
82
+ padding: var(--space-4) 0;
83
+ gap: var(--space-4);
84
+ z-index: var(--z-raised);
85
+ }
86
+
87
+ .rail {
88
+ display: flex;
89
+ flex-direction: column;
90
+ align-items: center;
91
+ gap: var(--space-1);
92
+ background: var(--color-surface);
93
+ border-radius: 999px;
94
+ padding: var(--space-2);
95
+ box-shadow: var(--shadow-1);
96
+ }
97
+
98
+ .item {
99
+ display: inline-flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ width: 2.4rem;
103
+ height: 2.4rem;
104
+ border-radius: 999px;
105
+ color: var(--color-navy);
106
+ border: none;
107
+ background: transparent;
108
+ cursor: pointer;
109
+ transition: background var(--transition-fast);
110
+ }
111
+
112
+ .item:hover {
113
+ background: color-mix(in srgb, var(--color-navy) 10%, transparent);
114
+ }
115
+
116
+ .item.active {
117
+ background: var(--color-navy);
118
+ color: var(--color-surface);
119
+ }
120
+
121
+ .footer {
122
+ margin-top: auto;
123
+ }
124
+
125
+ /* The host's footer control (a logout submit) should sit like an item. */
126
+ .footer :global(button) {
127
+ display: inline-flex;
128
+ align-items: center;
129
+ justify-content: center;
130
+ width: 2.4rem;
131
+ height: 2.4rem;
132
+ border-radius: 999px;
133
+ color: var(--color-navy);
134
+ border: none;
135
+ background: transparent;
136
+ cursor: pointer;
137
+ transition: background var(--transition-fast);
138
+ }
139
+
140
+ .footer :global(button:hover) {
141
+ background: color-mix(in srgb, var(--color-navy) 10%, transparent);
142
+ }
143
+
144
+ @media print {
145
+ .sidebar {
146
+ display: none;
147
+ }
148
+ }
149
+ </style>
@@ -0,0 +1,23 @@
1
+ import type { IconName } from '../ui/Icon.svelte';
2
+ /** One rail entry. The host owns routes, labels, and who sees what — a
3
+ * permission-filtered list arrives here already filtered. */
4
+ export interface SidebarItem {
5
+ href: string;
6
+ icon: IconName;
7
+ label: string;
8
+ }
9
+ import type { Snippet } from 'svelte';
10
+ interface Props {
11
+ items: SidebarItem[];
12
+ /** Accessible name of the nav landmark. */
13
+ label?: string;
14
+ /** Brand mark above the rail; the host owns its logo and home link. */
15
+ logo?: Snippet;
16
+ /** Bottom slot — the host's logout form lives here, never in the package. */
17
+ footer?: Snippet;
18
+ /** The URL highlighting reads from. Only tests and stories pass one. */
19
+ url?: URL;
20
+ }
21
+ declare const Sidebar: import("svelte").Component<Props, {}, "">;
22
+ type Sidebar = ReturnType<typeof Sidebar>;
23
+ export default Sidebar;
@@ -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;
@@ -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;
@@ -0,0 +1,116 @@
1
+ <script lang="ts">
2
+ import type { FullAutoFill, HTMLInputAttributes } from 'svelte/elements';
3
+
4
+ interface Props {
5
+ label: string;
6
+ type?: 'text' | 'email' | 'password' | 'time' | 'date';
7
+ id: string;
8
+ value?: string;
9
+ required?: boolean;
10
+ autocomplete?: FullAutoFill;
11
+ /**
12
+ * How the field presents itself. Three, which is what the call sites
13
+ * always were:
14
+ *
15
+ * - `field` — the labelled form field, the default.
16
+ * - `cell` — a table or modal cell. Smaller, and the label goes to the
17
+ * screen reader only.
18
+ * - `bare` — full size, label for the screen reader only, wording
19
+ * carried by the placeholder.
20
+ *
21
+ * It replaces two booleans that encoded three variants between them: no
22
+ * call site ever passed both, one of the four combinations meant
23
+ * nothing, and the compact one quietly did THREE things.
24
+ */
25
+ variant?: 'field' | 'cell' | 'bare';
26
+ /** Overrides the label-derived default ('user' rather than 'email'). */
27
+ placeholder?: string;
28
+ error?: string | null;
29
+ /**
30
+ * A remote form field's attachment (`login.fields.email.as('email')`),
31
+ * spread onto the input instead of binding — without it every remote
32
+ * form routes around this module and re-declares the underline styling.
33
+ */
34
+ attributes?: HTMLInputAttributes;
35
+ }
36
+
37
+ let {
38
+ label,
39
+ type = 'text',
40
+ id,
41
+ value = $bindable(''),
42
+ required = false,
43
+ autocomplete,
44
+ variant = 'field',
45
+ placeholder,
46
+ error = null,
47
+ attributes
48
+ }: Props = $props();
49
+
50
+ const shared = $derived({
51
+ id,
52
+ type,
53
+ required,
54
+ autocomplete,
55
+ placeholder: placeholder ?? (variant === 'field' ? label.toLowerCase() : undefined),
56
+ 'aria-invalid': error ? true : undefined,
57
+ 'aria-describedby': error ? `${id}-error` : undefined
58
+ });
59
+ </script>
60
+
61
+ <div class="field" class:cell={variant === 'cell'}>
62
+ <label for={id} class:visually-hidden={variant !== 'field'}>{label}</label>
63
+ {#if attributes}
64
+ <!-- The form owns the value and its name; `type` comes from `as(...)`. -->
65
+ <input {...shared} {...attributes} />
66
+ {:else}
67
+ <input {...shared} bind:value />
68
+ {/if}
69
+ {#if error}
70
+ <p class="error" id="{id}-error">{error}</p>
71
+ {/if}
72
+ </div>
73
+
74
+ <style>
75
+ .field {
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: var(--space-1);
79
+ }
80
+
81
+ label {
82
+ font-size: var(--text-sm);
83
+ font-weight: 500;
84
+ }
85
+
86
+ input {
87
+ border: none;
88
+ border-bottom: 1.5px solid var(--color-brand);
89
+ background: transparent;
90
+ padding: 0.4rem 0.1rem;
91
+ font-family: var(--font-sans);
92
+ font-size: var(--text-base);
93
+ color: var(--color-ink);
94
+ }
95
+
96
+ input::placeholder {
97
+ font-style: italic;
98
+ color: var(--color-ink-muted);
99
+ }
100
+
101
+ input:focus-visible {
102
+ outline: none;
103
+ border-bottom-color: var(--color-navy);
104
+ }
105
+
106
+ .cell input {
107
+ font-size: 0.9rem;
108
+ padding: 0.2rem 0.1rem;
109
+ }
110
+
111
+ .error {
112
+ color: var(--series-8);
113
+ font-size: 0.8rem;
114
+ margin: 0;
115
+ }
116
+ </style>
@@ -0,0 +1,36 @@
1
+ import type { FullAutoFill, HTMLInputAttributes } from 'svelte/elements';
2
+ interface Props {
3
+ label: string;
4
+ type?: 'text' | 'email' | 'password' | 'time' | 'date';
5
+ id: string;
6
+ value?: string;
7
+ required?: boolean;
8
+ autocomplete?: FullAutoFill;
9
+ /**
10
+ * How the field presents itself. Three, which is what the call sites
11
+ * always were:
12
+ *
13
+ * - `field` — the labelled form field, the default.
14
+ * - `cell` — a table or modal cell. Smaller, and the label goes to the
15
+ * screen reader only.
16
+ * - `bare` — full size, label for the screen reader only, wording
17
+ * carried by the placeholder.
18
+ *
19
+ * It replaces two booleans that encoded three variants between them: no
20
+ * call site ever passed both, one of the four combinations meant
21
+ * nothing, and the compact one quietly did THREE things.
22
+ */
23
+ variant?: 'field' | 'cell' | 'bare';
24
+ /** Overrides the label-derived default ('user' rather than 'email'). */
25
+ placeholder?: string;
26
+ error?: string | null;
27
+ /**
28
+ * A remote form field's attachment (`login.fields.email.as('email')`),
29
+ * spread onto the input instead of binding — without it every remote
30
+ * form routes around this module and re-declares the underline styling.
31
+ */
32
+ attributes?: HTMLInputAttributes;
33
+ }
34
+ declare const TextField: import("svelte").Component<Props, {}, "value">;
35
+ type TextField = ReturnType<typeof TextField>;
36
+ export default TextField;
@@ -5,6 +5,7 @@ export { default as FormResultSlot } from '../components/ui/FormResultSlot.svelt
5
5
  export { default as Honeypot } from '../components/ui/Honeypot.svelte';
6
6
  export { default as LocaleField } from '../components/ui/LocaleField.svelte';
7
7
  export { default as NewsletterIntentField } from '../components/ui/NewsletterIntentField.svelte';
8
+ export { default as TextField } from '../components/ui/TextField.svelte';
8
9
  export { COMMENT_BODY, CONTACT_MESSAGE, CONTACT_NAME, DISPLAY_NAME, EMAIL, LOGIN_PASSWORD, PASSWORD } from './constraints.js';
9
10
  export { hasNewsletterIntent, HONEYPOT_FIELD, isNewsletterIntent, NEWSLETTER_INTENT_PARAM, NEWSLETTER_INTENT_VALUE, withNewsletterIntent } from './transport.js';
10
11
  export type { FormFail, FormFieldIssue, FormResultLike, FormResultOf, KeyedRemoteForms, RemoteField, RemoteFormAttributes, RemoteFormInstance } from './types.js';
@@ -5,5 +5,6 @@ export { default as FormResultSlot } from '../components/ui/FormResultSlot.svelt
5
5
  export { default as Honeypot } from '../components/ui/Honeypot.svelte';
6
6
  export { default as LocaleField } from '../components/ui/LocaleField.svelte';
7
7
  export { default as NewsletterIntentField } from '../components/ui/NewsletterIntentField.svelte';
8
+ export { default as TextField } from '../components/ui/TextField.svelte';
8
9
  export { COMMENT_BODY, CONTACT_MESSAGE, CONTACT_NAME, DISPLAY_NAME, EMAIL, LOGIN_PASSWORD, PASSWORD } from './constraints.js';
9
10
  export { hasNewsletterIntent, HONEYPOT_FIELD, isNewsletterIntent, NEWSLETTER_INTENT_PARAM, NEWSLETTER_INTENT_VALUE, withNewsletterIntent } from './transport.js';
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from './primitives.js';
2
2
  export * from './chrome.js';
3
3
  export * from './content-components.js';
4
4
  export * from './community.js';
5
+ export * from './admin.js';
5
6
  export * from './forms/index.js';
6
7
  export * from './config/index.js';
7
8
  export * from './edit/index.js';
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // The whole surface, flat — importable piecemeal through the semantic
2
2
  // subpaths: ./primitives, ./forms, ./chrome, ./content, ./community,
3
- // ./edit, ./config, ./testing.
3
+ // ./edit, ./config, ./admin, ./testing.
4
4
  export * from './primitives.js';
5
5
  export * from './chrome.js';
6
6
  export * from './content-components.js';
7
7
  export * from './community.js';
8
+ export * from './admin.js';
8
9
  // The form seam: form components, structural remote-form types, transport
9
10
  // constants, field bounds.
10
11
  export * from './forms/index.js';
@@ -10,7 +10,11 @@ export { default as DateText } from './components/ui/DateText.svelte';
10
10
  export { default as DecorShapes } from './components/ui/DecorShapes.svelte';
11
11
  export { default as FilterChips } from './components/ui/FilterChips.svelte';
12
12
  export { default as GhostButton } from './components/ui/GhostButton.svelte';
13
+ export { default as Icon, type IconName } from './components/ui/Icon.svelte';
14
+ export { default as IconButton } from './components/ui/IconButton.svelte';
13
15
  export { default as Link } from './components/ui/Link.svelte';
16
+ export { default as Logo } from './components/ui/Logo.svelte';
17
+ export { default as Modal } from './components/ui/Modal.svelte';
14
18
  export { default as Pagination } from './components/ui/Pagination.svelte';
15
19
  export { default as RichText } from './components/ui/RichText.svelte';
16
20
  export { default as SearchInput } from './components/ui/SearchInput.svelte';
@@ -10,7 +10,11 @@ export { default as DateText } from './components/ui/DateText.svelte';
10
10
  export { default as DecorShapes } from './components/ui/DecorShapes.svelte';
11
11
  export { default as FilterChips } from './components/ui/FilterChips.svelte';
12
12
  export { default as GhostButton } from './components/ui/GhostButton.svelte';
13
+ export { default as Icon } from './components/ui/Icon.svelte';
14
+ export { default as IconButton } from './components/ui/IconButton.svelte';
13
15
  export { default as Link } from './components/ui/Link.svelte';
16
+ export { default as Logo } from './components/ui/Logo.svelte';
17
+ export { default as Modal } from './components/ui/Modal.svelte';
14
18
  export { default as Pagination } from './components/ui/Pagination.svelte';
15
19
  export { default as RichText } from './components/ui/RichText.svelte';
16
20
  export { default as SearchInput } from './components/ui/SearchInput.svelte';
@@ -30,6 +30,7 @@
30
30
 
31
31
  /* Typography */
32
32
  --font-sans: system-ui, -apple-system, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
33
+ --font-serif: Georgia, 'Times New Roman', serif;
33
34
  --text-sm: 0.875rem;
34
35
  --text-base: 1rem;
35
36
  --text-lg: 1.25rem;
@@ -52,6 +53,12 @@
52
53
  --radius-lg: 16px;
53
54
  --content-max: 1200px;
54
55
 
56
+ /* Elevation & layering */
57
+ --shadow-1: 0 2px 10px rgb(16 20 24 / 10%);
58
+ --shadow-2: 0 12px 40px rgb(16 20 24 / 20%);
59
+ --z-raised: 10;
60
+ --z-overlay: 100;
61
+
55
62
  /* Motion */
56
63
  --transition-fast: 150ms ease;
57
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vit-foundation/ui",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -72,6 +72,11 @@
72
72
  "svelte": "./dist/config/index.js",
73
73
  "default": "./dist/config/index.js"
74
74
  },
75
+ "./admin": {
76
+ "types": "./dist/admin.d.ts",
77
+ "svelte": "./dist/admin.js",
78
+ "default": "./dist/admin.js"
79
+ },
75
80
  "./testing": {
76
81
  "types": "./dist/testing/remote-form.d.ts",
77
82
  "svelte": "./dist/testing/remote-form.js",