@dorsk/tsumikit 0.2.16 → 0.4.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.
@@ -1,13 +1,17 @@
1
1
  <script lang="ts">
2
- // Sidebar navigation item: icon + label. When the surrounding `sidebar`
3
- // query container gets narrow (≤ 8rem e.g. the user drags AppShell's
4
- // resizable sidebar down to a rail) the label hides and the icon centers,
5
- // purely in CSS. `title`/`aria-label` carry the name so the icon-only state
6
- // stays accessible and shows a tooltip. Renders an <a> when `href` is set,
7
- // else a <button>.
2
+ // Sidebar navigation item: icon + label, with an optional trailing count
3
+ // `badge`. When the surrounding `sidebar` query container gets narrow
4
+ // (≤ 8rem — e.g. the user drags AppShell's resizable sidebar down to a rail)
5
+ // the label hides and the icon centers, purely in CSS; a non-empty badge then
6
+ // collapses to a small dot indicator so the count cue survives the rail.
7
+ // `title`/`aria-label` carry the name so the icon-only state stays accessible
8
+ // and shows a tooltip. Renders an <a> when `href` is set, else a <button>.
8
9
  import type { Snippet } from 'svelte';
9
10
  import Icon from '../atoms/Icon.svelte';
10
11
  import type { IconName } from '../atoms/Icon.svelte';
12
+ import Badge from '../atoms/Badge.svelte';
13
+
14
+ type BadgeTone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
11
15
 
12
16
  let {
13
17
  icon,
@@ -15,6 +19,8 @@
15
19
  href,
16
20
  active = false,
17
21
  onclick,
22
+ badge,
23
+ badgeTone = 'neutral',
18
24
  children,
19
25
  ...rest
20
26
  }: {
@@ -23,9 +29,17 @@
23
29
  href?: string;
24
30
  active?: boolean;
25
31
  onclick?: (e: MouseEvent) => void;
32
+ /** Optional trailing count pill (e.g. unread / missing items). When the
33
+ * item is `active` it adopts the accent fill; otherwise it uses `badgeTone`. */
34
+ badge?: string | number;
35
+ /** Tone for the trailing badge when the item is not active. */
36
+ badgeTone?: BadgeTone;
26
37
  children?: Snippet;
27
38
  [key: string]: unknown;
28
39
  } = $props();
40
+
41
+ // Treat 0 / '' as "no badge" so callers can pass a raw count without guarding.
42
+ const hasBadge = $derived(badge !== undefined && badge !== null && badge !== '' && badge !== 0);
29
43
  </script>
30
44
 
31
45
  <svelte:element
@@ -42,11 +56,21 @@
42
56
  >
43
57
  <Icon name={icon} size={18} />
44
58
  <span class="nav-label">{label}</span>
45
- {#if children}<span class="nav-trail">{@render children()}</span>{/if}
59
+ {#if hasBadge}
60
+ <span class="nav-trail">
61
+ <Badge size="sm" tone={active ? 'neutral' : badgeTone} active={active}>{badge}</Badge>
62
+ </span>
63
+ <!-- Rail fallback: the pill is hidden by the container query below, this dot
64
+ takes over so the "has items" cue still reads in the icon rail. -->
65
+ <span class="nav-dot" class:active aria-hidden="true"></span>
66
+ {:else if children}
67
+ <span class="nav-trail">{@render children()}</span>
68
+ {/if}
46
69
  </svelte:element>
47
70
 
48
71
  <style>
49
72
  .nav-item {
73
+ position: relative;
50
74
  display: flex;
51
75
  align-items: center;
52
76
  gap: var(--sp-2);
@@ -74,14 +98,36 @@
74
98
  background: color-mix(in srgb, var(--accent) 14%, transparent);
75
99
  color: var(--accent);
76
100
  }
101
+ /* Active-route accent inset: a left rail marker that survives the icon-rail
102
+ collapse (the tint background does too, but the marker reads at a glance). */
103
+ .nav-item.active::before {
104
+ content: '';
105
+ position: absolute;
106
+ left: 0;
107
+ top: 50%;
108
+ transform: translateY(-50%);
109
+ width: 3px;
110
+ height: 60%;
111
+ border-radius: var(--r-pill);
112
+ background: var(--accent);
113
+ }
77
114
  .nav-label {
78
115
  flex: 1;
79
116
  min-width: 0; /* allow the label to shrink/ellipsis instead of overflowing */
80
117
  overflow: hidden;
81
118
  text-overflow: ellipsis;
82
119
  }
120
+ .nav-trail {
121
+ flex: none;
122
+ }
123
+ /* Dot is the rail-width stand-in for the badge: hidden at full width. */
124
+ .nav-dot {
125
+ display: none;
126
+ }
127
+
83
128
  /* Icon-rail: when the sidebar container is narrow, drop the label + trailing
84
- slot and center the icon. Driven by the sidebar's width, not the viewport. */
129
+ pill and center the icon. Driven by the sidebar's width, not the viewport.
130
+ A badge collapses to a dot pinned to the icon's top-right corner. */
85
131
  @container sidebar (max-width: 8rem) {
86
132
  .nav-item {
87
133
  justify-content: center;
@@ -91,5 +137,18 @@
91
137
  .nav-trail {
92
138
  display: none;
93
139
  }
140
+ .nav-dot {
141
+ display: block;
142
+ position: absolute;
143
+ top: 0.4rem;
144
+ right: 0.9rem;
145
+ width: 6px;
146
+ height: 6px;
147
+ border-radius: 50%;
148
+ background: var(--warn);
149
+ }
150
+ .nav-dot.active {
151
+ background: var(--accent);
152
+ }
94
153
  }
95
154
  </style>
@@ -1,11 +1,17 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { IconName } from '../atoms/Icon.svelte';
3
+ type BadgeTone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
3
4
  type $$ComponentProps = {
4
5
  icon: IconName;
5
6
  label: string;
6
7
  href?: string;
7
8
  active?: boolean;
8
9
  onclick?: (e: MouseEvent) => void;
10
+ /** Optional trailing count pill (e.g. unread / missing items). When the
11
+ * item is `active` it adopts the accent fill; otherwise it uses `badgeTone`. */
12
+ badge?: string | number;
13
+ /** Tone for the trailing badge when the item is not active. */
14
+ badgeTone?: BadgeTone;
9
15
  children?: Snippet;
10
16
  [key: string]: unknown;
11
17
  };
@@ -0,0 +1,71 @@
1
+ <script lang="ts">
2
+ // A grouped block of sidebar nav items: an optional uppercase mono section
3
+ // `label` (eyebrow) plus consistent tight spacing between the NavItems passed
4
+ // as children. Saves consumers from re-implementing the label + gap inline for
5
+ // every group. At icon-rail width (sidebar container ≤ 8rem) the label hides
6
+ // and the group spacing tightens so the rail stays compact; a thin top divider
7
+ // then stands in for the dropped label to keep the groups visually separated.
8
+ import type { Snippet } from 'svelte';
9
+
10
+ let {
11
+ label,
12
+ children,
13
+ ...rest
14
+ }: {
15
+ /** Optional section heading; rendered uppercase + mono. Omit for an
16
+ * unlabeled group (still gets the group spacing + rail divider). */
17
+ label?: string;
18
+ children?: Snippet;
19
+ [key: string]: unknown;
20
+ } = $props();
21
+ </script>
22
+
23
+ <div class="nav-section" {...rest}>
24
+ {#if label}
25
+ <div class="nav-section-label">{label}</div>
26
+ {/if}
27
+ <div class="nav-section-items">
28
+ {@render children?.()}
29
+ </div>
30
+ </div>
31
+
32
+ <style>
33
+ .nav-section {
34
+ display: flex;
35
+ flex-direction: column;
36
+ gap: var(--sp-1);
37
+ }
38
+ /* Groups separate themselves with breathing room; the first one sits flush. */
39
+ .nav-section + :global(.nav-section),
40
+ .nav-section:not(:first-child) {
41
+ margin-top: var(--sp-3);
42
+ }
43
+ .nav-section-label {
44
+ padding: 0 var(--sp-3);
45
+ margin-bottom: var(--sp-1);
46
+ color: var(--text-faint);
47
+ font-family: var(--font-mono);
48
+ font-size: var(--fs-xs);
49
+ font-weight: var(--fw-semibold);
50
+ text-transform: uppercase;
51
+ letter-spacing: 0.06em;
52
+ }
53
+ .nav-section-items {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 2px;
57
+ }
58
+
59
+ /* Icon-rail: drop the textual label and use a hairline divider to keep groups
60
+ distinct when the sidebar collapses to a rail. */
61
+ @container sidebar (max-width: 8rem) {
62
+ .nav-section-label {
63
+ display: none;
64
+ }
65
+ .nav-section:not(:first-child) {
66
+ margin-top: var(--sp-2);
67
+ padding-top: var(--sp-2);
68
+ border-top: 1px solid var(--border);
69
+ }
70
+ }
71
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ /** Optional section heading; rendered uppercase + mono. Omit for an
4
+ * unlabeled group (still gets the group spacing + rail divider). */
5
+ label?: string;
6
+ children?: Snippet;
7
+ [key: string]: unknown;
8
+ };
9
+ declare const NavSection: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type NavSection = ReturnType<typeof NavSection>;
11
+ export default NavSection;
@@ -7,12 +7,15 @@
7
7
  import Button from '../atoms/Button.svelte';
8
8
  import Select from '../atoms/Select.svelte';
9
9
 
10
+ type Option = { value: string; label: string };
11
+
10
12
  let {
11
13
  glyph,
12
14
  label,
13
15
  title,
14
16
  value,
15
17
  options,
18
+ groups,
16
19
  onchange,
17
20
  class: klass = ''
18
21
  }: {
@@ -21,7 +24,11 @@
21
24
  label: string;
22
25
  title?: string;
23
26
  value: string;
24
- options: { value: string; label: string }[];
27
+ // Flat option list. Ignored when `groups` is given.
28
+ options?: Option[];
29
+ // Sectioned options, rendered as native <optgroup> blocks (TSU-1). Used by
30
+ // the theme picker to split light/dark; falls back to `options` otherwise.
31
+ groups?: { label: string; options: Option[] }[];
25
32
  onchange: (value: string) => void;
26
33
  class?: string;
27
34
  } = $props();
@@ -38,9 +45,19 @@
38
45
  {value}
39
46
  onchange={(e) => onchange((e.currentTarget as HTMLSelectElement).value)}
40
47
  >
41
- {#each options as o (o.value)}
42
- <option value={o.value}>{o.label}</option>
43
- {/each}
48
+ {#if groups}
49
+ {#each groups as g (g.label)}
50
+ <optgroup label={g.label}>
51
+ {#each g.options as o (o.value)}
52
+ <option value={o.value}>{o.label}</option>
53
+ {/each}
54
+ </optgroup>
55
+ {/each}
56
+ {:else}
57
+ {#each options ?? [] as o (o.value)}
58
+ <option value={o.value}>{o.label}</option>
59
+ {/each}
60
+ {/if}
44
61
  </Select>
45
62
  </Button>
46
63
  </span>
@@ -1,11 +1,16 @@
1
+ type Option = {
2
+ value: string;
3
+ label: string;
4
+ };
1
5
  type $$ComponentProps = {
2
6
  glyph: string;
3
7
  label: string;
4
8
  title?: string;
5
9
  value: string;
6
- options: {
7
- value: string;
10
+ options?: Option[];
11
+ groups?: {
8
12
  label: string;
13
+ options: Option[];
9
14
  }[];
10
15
  onchange: (value: string) => void;
11
16
  class?: string;
@@ -8,7 +8,16 @@
8
8
 
9
9
  let { class: klass = '' }: { class?: string } = $props();
10
10
 
11
- const options = THEMES.map((t) => ({ value: t.id, label: `${t.icon} ${t.label}` }));
11
+ // Split the registry into light/dark sections (TSU-1) so the long list is
12
+ // scannable. `<optgroup>` keeps native popup/keyboard behaviour for free.
13
+ const toOption = (t: (typeof THEMES)[number]) => ({
14
+ value: t.id,
15
+ label: `${t.icon} ${t.label}`
16
+ });
17
+ const groups = [
18
+ { label: '— light', options: THEMES.filter((t) => t.mode === 'light').map(toOption) },
19
+ { label: '— dark', options: THEMES.filter((t) => t.mode === 'dark').map(toOption) }
20
+ ];
12
21
  </script>
13
22
 
14
23
  <SelectButton
@@ -17,6 +26,6 @@
17
26
  label="Theme"
18
27
  title={`Theme: ${theme.label}`}
19
28
  value={theme.current}
20
- {options}
29
+ {groups}
21
30
  onchange={(v) => theme.set(v as (typeof THEMES)[number]['id'])}
22
31
  />
package/dist/index.d.ts CHANGED
@@ -21,6 +21,7 @@ export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
21
21
  export { default as Cluster } from './components/layouts/Cluster.svelte';
22
22
  export { default as Container } from './components/layouts/Container.svelte';
23
23
  export { default as NavItem } from './components/layouts/NavItem.svelte';
24
+ export { default as NavSection } from './components/layouts/NavSection.svelte';
24
25
  export { default as Stack } from './components/layouts/Stack.svelte';
25
26
  export { type AccordionItem, default as Accordion, } from './components/molecules/Accordion.svelte';
26
27
  export { default as CodeBlock } from './components/molecules/CodeBlock.svelte';
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
28
28
  export { default as Cluster } from './components/layouts/Cluster.svelte';
29
29
  export { default as Container } from './components/layouts/Container.svelte';
30
30
  export { default as NavItem } from './components/layouts/NavItem.svelte';
31
+ export { default as NavSection } from './components/layouts/NavSection.svelte';
31
32
  export { default as Stack } from './components/layouts/Stack.svelte';
32
33
  export { default as Accordion, } from './components/molecules/Accordion.svelte';
33
34
  export { default as CodeBlock } from './components/molecules/CodeBlock.svelte';
@@ -1,88 +1,153 @@
1
1
  export declare const THEMES: readonly [{
2
- readonly id: "dark";
3
- readonly label: "Dark";
4
- readonly icon: "☾";
5
- readonly themeColor: "#0f1115";
6
- }, {
7
2
  readonly id: "light";
8
3
  readonly label: "Light";
9
4
  readonly icon: "☀";
10
5
  readonly themeColor: "#f6f7f9";
6
+ readonly mode: "light";
7
+ }, {
8
+ readonly id: "highcontrast";
9
+ readonly label: "High Contrast Light";
10
+ readonly icon: "◻";
11
+ readonly themeColor: "#ffffff";
12
+ readonly mode: "light";
13
+ }, {
14
+ readonly id: "gruvboxlight";
15
+ readonly label: "Gruvbox Light";
16
+ readonly icon: "◇";
17
+ readonly themeColor: "#fbf1c7";
18
+ readonly mode: "light";
19
+ }, {
20
+ readonly id: "solarizedlight";
21
+ readonly label: "Solarized Light";
22
+ readonly icon: "◑";
23
+ readonly themeColor: "#fdf6e3";
24
+ readonly mode: "light";
25
+ }, {
26
+ readonly id: "everforestlight";
27
+ readonly label: "Everforest Light";
28
+ readonly icon: "✾";
29
+ readonly themeColor: "#fdf6e3";
30
+ readonly mode: "light";
31
+ }, {
32
+ readonly id: "rosepinedawn";
33
+ readonly label: "Rosé Pine Dawn";
34
+ readonly icon: "✿";
35
+ readonly themeColor: "#faf4ed";
36
+ readonly mode: "light";
37
+ }, {
38
+ readonly id: "latte";
39
+ readonly label: "Catppuccin Latte";
40
+ readonly icon: "L";
41
+ readonly themeColor: "#eff1f5";
42
+ readonly mode: "light";
43
+ }, {
44
+ readonly id: "nordlight";
45
+ readonly label: "Nord Light";
46
+ readonly icon: "n";
47
+ readonly themeColor: "#eceff4";
48
+ readonly mode: "light";
49
+ }, {
50
+ readonly id: "tokyoday";
51
+ readonly label: "Tokyo Night Day";
52
+ readonly icon: "✧";
53
+ readonly themeColor: "#e1e2e7";
54
+ readonly mode: "light";
55
+ }, {
56
+ readonly id: "kanagawalotus";
57
+ readonly label: "Kanagawa Lotus";
58
+ readonly icon: "❁";
59
+ readonly themeColor: "#f2ecbc";
60
+ readonly mode: "light";
11
61
  }, {
12
62
  readonly id: "sepia";
13
63
  readonly label: "Sepia";
14
64
  readonly icon: "✶";
15
65
  readonly themeColor: "#f4ecd8";
66
+ readonly mode: "light";
67
+ }, {
68
+ readonly id: "dark";
69
+ readonly label: "Dark";
70
+ readonly icon: "☾";
71
+ readonly themeColor: "#0f1115";
72
+ readonly mode: "dark";
16
73
  }, {
17
74
  readonly id: "colorblind";
18
75
  readonly label: "Color-blind safe";
19
- readonly icon: "";
76
+ readonly icon: "";
20
77
  readonly themeColor: "#16181d";
78
+ readonly mode: "dark";
21
79
  }, {
22
80
  readonly id: "mocha";
23
81
  readonly label: "Catppuccin Mocha";
24
82
  readonly icon: "M";
25
83
  readonly themeColor: "#1e1e2e";
84
+ readonly mode: "dark";
26
85
  }, {
27
86
  readonly id: "dracula";
28
87
  readonly label: "Dracula";
29
88
  readonly icon: "D";
30
89
  readonly themeColor: "#282a36";
90
+ readonly mode: "dark";
31
91
  }, {
32
92
  readonly id: "nord";
33
93
  readonly label: "Nord";
34
94
  readonly icon: "N";
35
95
  readonly themeColor: "#2e3440";
96
+ readonly mode: "dark";
36
97
  }, {
37
98
  readonly id: "tokyonight";
38
99
  readonly label: "Tokyo Night";
39
100
  readonly icon: "✦";
40
101
  readonly themeColor: "#1a1b26";
102
+ readonly mode: "dark";
41
103
  }, {
42
104
  readonly id: "gruvbox";
43
105
  readonly label: "Gruvbox";
44
106
  readonly icon: "◆";
45
107
  readonly themeColor: "#282828";
108
+ readonly mode: "dark";
46
109
  }, {
47
110
  readonly id: "solarized";
48
111
  readonly label: "Solarized Dark";
49
- readonly icon: "";
112
+ readonly icon: "";
50
113
  readonly themeColor: "#002b36";
114
+ readonly mode: "dark";
51
115
  }, {
52
116
  readonly id: "rosepine";
53
117
  readonly label: "Rosé Pine";
54
118
  readonly icon: "❀";
55
119
  readonly themeColor: "#191724";
120
+ readonly mode: "dark";
56
121
  }, {
57
122
  readonly id: "onedark";
58
123
  readonly label: "One Dark";
59
124
  readonly icon: "①";
60
125
  readonly themeColor: "#282c34";
126
+ readonly mode: "dark";
61
127
  }, {
62
128
  readonly id: "everforest";
63
129
  readonly label: "Everforest";
64
130
  readonly icon: "☘";
65
131
  readonly themeColor: "#2d353b";
132
+ readonly mode: "dark";
66
133
  }, {
67
134
  readonly id: "monokai";
68
135
  readonly label: "Monokai";
69
136
  readonly icon: "✸";
70
137
  readonly themeColor: "#272822";
138
+ readonly mode: "dark";
71
139
  }, {
72
140
  readonly id: "amoled";
73
141
  readonly label: "AMOLED (high contrast)";
74
142
  readonly icon: "◼";
75
143
  readonly themeColor: "#000000";
76
- }, {
77
- readonly id: "highcontrast";
78
- readonly label: "High Contrast Light";
79
- readonly icon: "◻";
80
- readonly themeColor: "#ffffff";
144
+ readonly mode: "dark";
81
145
  }];
82
146
  export type Mode = (typeof THEMES)[number]['id'];
147
+ export type ThemeMode = (typeof THEMES)[number]['mode'];
83
148
  type ThemeOption = (typeof THEMES)[number];
84
149
  declare class Theme {
85
- current: "dark" | "light" | "sepia" | "colorblind" | "mocha" | "dracula" | "nord" | "tokyonight" | "gruvbox" | "solarized" | "rosepine" | "onedark" | "everforest" | "monokai" | "amoled" | "highcontrast";
150
+ current: "light" | "highcontrast" | "gruvboxlight" | "solarizedlight" | "everforestlight" | "rosepinedawn" | "latte" | "nordlight" | "tokyoday" | "kanagawalotus" | "sepia" | "dark" | "colorblind" | "mocha" | "dracula" | "nord" | "tokyonight" | "gruvbox" | "solarized" | "rosepine" | "onedark" | "everforest" | "monokai" | "amoled";
86
151
  constructor();
87
152
  private apply;
88
153
  get option(): ThemeOption;
@@ -1,25 +1,55 @@
1
1
  import { browser } from '../env';
2
2
  // Theme registry — the single list the picker and the store both read. Adding a
3
3
  // theme = one entry here + one [data-theme="id"] block in variables.css. Nothing
4
- // else changes. `themeColor` drives the mobile browser-chrome <meta theme-color>.
4
+ // else changes. `themeColor` drives the mobile browser-chrome <meta theme-color>;
5
+ // `mode` groups the theme into the picker's light/dark sections (TSU-1).
5
6
  const KEY = 'tsumikit-theme';
6
7
  export const THEMES = [
7
- { id: 'dark', label: 'Dark', icon: '☾', themeColor: '#0f1115' },
8
- { id: 'light', label: 'Light', icon: '☀', themeColor: '#f6f7f9' },
9
- { id: 'sepia', label: 'Sepia', icon: '✶', themeColor: '#f4ecd8' },
10
- { id: 'colorblind', label: 'Color-blind safe', icon: '◑', themeColor: '#16181d' },
11
- { id: 'mocha', label: 'Catppuccin Mocha', icon: 'M', themeColor: '#1e1e2e' },
12
- { id: 'dracula', label: 'Dracula', icon: 'D', themeColor: '#282a36' },
13
- { id: 'nord', label: 'Nord', icon: 'N', themeColor: '#2e3440' },
14
- { id: 'tokyonight', label: 'Tokyo Night', icon: '✦', themeColor: '#1a1b26' },
15
- { id: 'gruvbox', label: 'Gruvbox', icon: '◆', themeColor: '#282828' },
16
- { id: 'solarized', label: 'Solarized Dark', icon: '◐', themeColor: '#002b36' },
17
- { id: 'rosepine', label: 'Rosé Pine', icon: '', themeColor: '#191724' },
18
- { id: 'onedark', label: 'One Dark', icon: '①', themeColor: '#282c34' },
19
- { id: 'everforest', label: 'Everforest', icon: '☘', themeColor: '#2d353b' },
20
- { id: 'monokai', label: 'Monokai', icon: '✸', themeColor: '#272822' },
21
- { id: 'amoled', label: 'AMOLED (high contrast)', icon: '', themeColor: '#000000' },
22
- { id: 'highcontrast', label: 'High Contrast Light', icon: '◻', themeColor: '#ffffff' },
8
+ // ── Light ── bright, paper-white surfaces
9
+ { id: 'light', label: 'Light', icon: '☀', themeColor: '#f6f7f9', mode: 'light' },
10
+ {
11
+ id: 'highcontrast',
12
+ label: 'High Contrast Light',
13
+ icon: '',
14
+ themeColor: '#ffffff',
15
+ mode: 'light',
16
+ },
17
+ // ── Medium-light (TSU-1) ── easy on the eyes, not blinding, distinct bases
18
+ { id: 'gruvboxlight', label: 'Gruvbox Light', icon: '', themeColor: '#fbf1c7', mode: 'light' },
19
+ {
20
+ id: 'solarizedlight',
21
+ label: 'Solarized Light',
22
+ icon: '',
23
+ themeColor: '#fdf6e3',
24
+ mode: 'light',
25
+ },
26
+ {
27
+ id: 'everforestlight',
28
+ label: 'Everforest Light',
29
+ icon: '✾',
30
+ themeColor: '#fdf6e3',
31
+ mode: 'light',
32
+ },
33
+ { id: 'rosepinedawn', label: 'Rosé Pine Dawn', icon: '✿', themeColor: '#faf4ed', mode: 'light' },
34
+ { id: 'latte', label: 'Catppuccin Latte', icon: 'L', themeColor: '#eff1f5', mode: 'light' },
35
+ { id: 'nordlight', label: 'Nord Light', icon: 'n', themeColor: '#eceff4', mode: 'light' },
36
+ { id: 'tokyoday', label: 'Tokyo Night Day', icon: '✧', themeColor: '#e1e2e7', mode: 'light' },
37
+ { id: 'kanagawalotus', label: 'Kanagawa Lotus', icon: '❁', themeColor: '#f2ecbc', mode: 'light' },
38
+ { id: 'sepia', label: 'Sepia', icon: '✶', themeColor: '#f4ecd8', mode: 'light' },
39
+ // ── Dark ──
40
+ { id: 'dark', label: 'Dark', icon: '☾', themeColor: '#0f1115', mode: 'dark' },
41
+ { id: 'colorblind', label: 'Color-blind safe', icon: '◐', themeColor: '#16181d', mode: 'dark' },
42
+ { id: 'mocha', label: 'Catppuccin Mocha', icon: 'M', themeColor: '#1e1e2e', mode: 'dark' },
43
+ { id: 'dracula', label: 'Dracula', icon: 'D', themeColor: '#282a36', mode: 'dark' },
44
+ { id: 'nord', label: 'Nord', icon: 'N', themeColor: '#2e3440', mode: 'dark' },
45
+ { id: 'tokyonight', label: 'Tokyo Night', icon: '✦', themeColor: '#1a1b26', mode: 'dark' },
46
+ { id: 'gruvbox', label: 'Gruvbox', icon: '◆', themeColor: '#282828', mode: 'dark' },
47
+ { id: 'solarized', label: 'Solarized Dark', icon: '◒', themeColor: '#002b36', mode: 'dark' },
48
+ { id: 'rosepine', label: 'Rosé Pine', icon: '❀', themeColor: '#191724', mode: 'dark' },
49
+ { id: 'onedark', label: 'One Dark', icon: '①', themeColor: '#282c34', mode: 'dark' },
50
+ { id: 'everforest', label: 'Everforest', icon: '☘', themeColor: '#2d353b', mode: 'dark' },
51
+ { id: 'monokai', label: 'Monokai', icon: '✸', themeColor: '#272822', mode: 'dark' },
52
+ { id: 'amoled', label: 'AMOLED (high contrast)', icon: '◼', themeColor: '#000000', mode: 'dark' },
23
53
  ];
24
54
  const ORDER = THEMES.map((t) => t.id);
25
55
  function isMode(value) {
@@ -649,3 +649,262 @@
649
649
  --mach-fg-sl: 70% 82%;
650
650
  --mach-border-sl: 45% 40%;
651
651
  }
652
+
653
+ /* ===================================================================
654
+ Medium-light tier (TSU-1) — light but not blinding. Cream/paper or
655
+ soft-tinted backgrounds (never pure white) with dark, saturated
656
+ accents so body text clears WCAG AA. Each ports a famous palette in a
657
+ distinct color base. They share the light-theme contract: color-scheme
658
+ light, soft shadows, pale-tint/dark-ink machine badges.
659
+ =================================================================== */
660
+
661
+ /* ---- Gruvbox Light (medium) — warm wood/cream, faded orange accent. */
662
+ [data-theme="gruvboxlight"] {
663
+ color-scheme: light;
664
+ --c-bg: #fbf1c7;
665
+ --c-bg-elev: #f9f5d7;
666
+ --c-bg-elev-2: #ebdbb2;
667
+ --c-surface: #f9f5d7;
668
+ --c-border: #d5c4a1;
669
+ --c-border-strong: #bdae93;
670
+ --c-text: #3c3836;
671
+ --c-text-muted: #504945;
672
+ --c-text-faint: #7c6f64;
673
+ --c-accent: #af3a03;
674
+ --c-accent-ink: #fbf1c7;
675
+ --c-accent-dim: #d65d0e;
676
+ --c-blue: #076678;
677
+ --c-amber: #b57614;
678
+ --c-red: #9d0006;
679
+ --c-green: #79740e;
680
+ --c-violet: #8f3f71;
681
+ --c-gold: #b57614;
682
+ --c-teal: #427b58;
683
+ --md-code-bg: color-mix(in srgb, var(--c-blue) 12%, var(--c-bg));
684
+ --shadow-sm: 0 1px 2px rgba(80, 60, 30, 0.1);
685
+ --shadow-md: 0 6px 20px rgba(80, 60, 30, 0.14);
686
+ --shadow-lg: 0 12px 40px rgba(80, 60, 30, 0.2);
687
+ --mach-bg-sl: 45% 86%;
688
+ --mach-fg-sl: 55% 30%;
689
+ --mach-border-sl: 40% 68%;
690
+ }
691
+
692
+ /* ---- Solarized Light — Ethan Schoonover's base3 paper with 16-color accents. */
693
+ [data-theme="solarizedlight"] {
694
+ color-scheme: light;
695
+ --c-bg: #fdf6e3;
696
+ --c-bg-elev: #fbf3e0;
697
+ --c-bg-elev-2: #eee8d5;
698
+ --c-surface: #fbf3e0;
699
+ --c-border: #ddd6c1;
700
+ --c-border-strong: #cbc4ad;
701
+ --c-text: #073642;
702
+ --c-text-muted: #586e75;
703
+ --c-text-faint: #839496;
704
+ --c-accent: #2aa198;
705
+ --c-accent-ink: #fdf6e3;
706
+ --c-accent-dim: #2aa198;
707
+ --c-blue: #268bd2;
708
+ --c-amber: #b58900;
709
+ --c-red: #dc322f;
710
+ --c-green: #859900;
711
+ --c-violet: #6c71c4;
712
+ --c-gold: #b58900;
713
+ --c-teal: #2aa198;
714
+ --md-code-bg: color-mix(in srgb, var(--c-blue) 12%, var(--c-bg));
715
+ --shadow-sm: 0 1px 2px rgba(88, 110, 117, 0.12);
716
+ --shadow-md: 0 6px 20px rgba(88, 110, 117, 0.16);
717
+ --shadow-lg: 0 12px 40px rgba(88, 110, 117, 0.22);
718
+ --mach-bg-sl: 45% 86%;
719
+ --mach-fg-sl: 50% 30%;
720
+ --mach-border-sl: 40% 68%;
721
+ }
722
+
723
+ /* ---- Everforest Light (medium) — soft sage/green paper, low-contrast calm. */
724
+ [data-theme="everforestlight"] {
725
+ color-scheme: light;
726
+ --c-bg: #fdf6e3;
727
+ --c-bg-elev: #f4f0d9;
728
+ --c-bg-elev-2: #efebd4;
729
+ --c-surface: #f4f0d9;
730
+ --c-border: #e0dcc7;
731
+ --c-border-strong: #bdc3af;
732
+ --c-text: #5c6a72;
733
+ --c-text-muted: #708089;
734
+ --c-text-faint: #939f91;
735
+ --c-accent: #8da101;
736
+ --c-accent-ink: #fdf6e3;
737
+ --c-accent-dim: #8da101;
738
+ --c-blue: #3a94c5;
739
+ --c-amber: #dfa000;
740
+ --c-red: #f85552;
741
+ --c-green: #8da101;
742
+ --c-violet: #df69ba;
743
+ --c-gold: #dfa000;
744
+ --c-teal: #35a77c;
745
+ --md-code-bg: color-mix(in srgb, var(--c-blue) 12%, var(--c-bg));
746
+ --shadow-sm: 0 1px 2px rgba(92, 106, 114, 0.12);
747
+ --shadow-md: 0 6px 20px rgba(92, 106, 114, 0.16);
748
+ --shadow-lg: 0 12px 40px rgba(92, 106, 114, 0.22);
749
+ --mach-bg-sl: 35% 86%;
750
+ --mach-fg-sl: 45% 32%;
751
+ --mach-border-sl: 30% 68%;
752
+ }
753
+
754
+ /* ---- Rosé Pine Dawn — official light variant: muted rose, iris, foam. */
755
+ [data-theme="rosepinedawn"] {
756
+ color-scheme: light;
757
+ --c-bg: #faf4ed;
758
+ --c-bg-elev: #fffaf3;
759
+ --c-bg-elev-2: #f2e9e1;
760
+ --c-surface: #fffaf3;
761
+ --c-border: #dfdad9;
762
+ --c-border-strong: #cecacd;
763
+ --c-text: #575279;
764
+ --c-text-muted: #797593;
765
+ --c-text-faint: #9893a5;
766
+ --c-accent: #907aa9;
767
+ --c-accent-ink: #faf4ed;
768
+ --c-accent-dim: #907aa9;
769
+ --c-blue: #286983;
770
+ --c-amber: #ea9d34;
771
+ --c-red: #b4637a;
772
+ --c-green: #56949f;
773
+ --c-violet: #907aa9;
774
+ --c-gold: #ea9d34;
775
+ --c-teal: #56949f;
776
+ --md-code-bg: color-mix(in srgb, var(--c-blue) 12%, var(--c-bg));
777
+ --shadow-sm: 0 1px 2px rgba(87, 82, 121, 0.1);
778
+ --shadow-md: 0 6px 20px rgba(87, 82, 121, 0.14);
779
+ --shadow-lg: 0 12px 40px rgba(87, 82, 121, 0.2);
780
+ --mach-bg-sl: 35% 88%;
781
+ --mach-fg-sl: 30% 38%;
782
+ --mach-border-sl: 28% 72%;
783
+ }
784
+
785
+ /* ---- Catppuccin Latte — the official light flavour: neutral with mauve accent. */
786
+ [data-theme="latte"] {
787
+ color-scheme: light;
788
+ --c-bg: #eff1f5;
789
+ --c-bg-elev: #e6e9ef;
790
+ --c-bg-elev-2: #dce0e8;
791
+ --c-surface: #e6e9ef;
792
+ --c-border: #ccd0da;
793
+ --c-border-strong: #bcc0cc;
794
+ --c-text: #4c4f69;
795
+ --c-text-muted: #5c5f77;
796
+ --c-text-faint: #8c8fa1;
797
+ --c-accent: #8839ef;
798
+ --c-accent-ink: #eff1f5;
799
+ --c-accent-dim: #8839ef;
800
+ --c-blue: #1e66f5;
801
+ --c-amber: #df8e1d;
802
+ --c-red: #d20f39;
803
+ --c-green: #40a02b;
804
+ --c-violet: #8839ef;
805
+ --c-gold: #df8e1d;
806
+ --c-teal: #179299;
807
+ --md-code-bg: color-mix(in srgb, var(--c-blue) 10%, var(--c-bg));
808
+ --shadow-sm: 0 1px 2px rgba(76, 79, 105, 0.1);
809
+ --shadow-md: 0 6px 20px rgba(76, 79, 105, 0.14);
810
+ --shadow-lg: 0 12px 40px rgba(76, 79, 105, 0.2);
811
+ --mach-bg-sl: 45% 88%;
812
+ --mach-fg-sl: 45% 34%;
813
+ --mach-border-sl: 35% 72%;
814
+ }
815
+
816
+ /* ---- Nord Light — Snow Storm base, Polar Night ink, darkened Frost/Aurora
817
+ accents so they read against the pale blue-gray paper. */
818
+ [data-theme="nordlight"] {
819
+ color-scheme: light;
820
+ --c-bg: #eceff4;
821
+ --c-bg-elev: #f4f6f9;
822
+ --c-bg-elev-2: #e5e9f0;
823
+ --c-surface: #f4f6f9;
824
+ --c-border: #d8dee9;
825
+ --c-border-strong: #c2cad6;
826
+ --c-text: #2e3440;
827
+ --c-text-muted: #434c5e;
828
+ --c-text-faint: #6b7689;
829
+ --c-accent: #5e81ac;
830
+ --c-accent-ink: #eceff4;
831
+ --c-accent-dim: #5e81ac;
832
+ --c-blue: #5e81ac;
833
+ --c-amber: #9a6f12;
834
+ --c-red: #bf616a;
835
+ --c-green: #5a7444;
836
+ --c-violet: #9a6c93;
837
+ --c-gold: #9a6f12;
838
+ --c-teal: #3a8a87;
839
+ --md-code-bg: color-mix(in srgb, var(--c-blue) 12%, var(--c-bg));
840
+ --shadow-sm: 0 1px 2px rgba(46, 52, 64, 0.1);
841
+ --shadow-md: 0 6px 20px rgba(46, 52, 64, 0.14);
842
+ --shadow-lg: 0 12px 40px rgba(46, 52, 64, 0.2);
843
+ --mach-bg-sl: 30% 88%;
844
+ --mach-fg-sl: 35% 34%;
845
+ --mach-border-sl: 28% 72%;
846
+ }
847
+
848
+ /* ---- Tokyo Night Day — the official light variant: muted blue paper, the
849
+ signature blue ink, saturated day accents. */
850
+ [data-theme="tokyoday"] {
851
+ color-scheme: light;
852
+ --c-bg: #e1e2e7;
853
+ --c-bg-elev: #eaeaee;
854
+ --c-bg-elev-2: #d5d6db;
855
+ --c-surface: #eaeaee;
856
+ --c-border: #c4c8da;
857
+ --c-border-strong: #a8acc4;
858
+ --c-text: #3760bf;
859
+ --c-text-muted: #6172b0;
860
+ --c-text-faint: #848cb5;
861
+ --c-accent: #2e7de9;
862
+ --c-accent-ink: #ffffff;
863
+ --c-accent-dim: #2e7de9;
864
+ --c-blue: #2e7de9;
865
+ --c-amber: #8c6c3e;
866
+ --c-red: #f52a65;
867
+ --c-green: #587539;
868
+ --c-violet: #9854f1;
869
+ --c-gold: #8c6c3e;
870
+ --c-teal: #118c74;
871
+ --md-code-bg: color-mix(in srgb, var(--c-blue) 12%, var(--c-bg));
872
+ --shadow-sm: 0 1px 2px rgba(55, 96, 191, 0.1);
873
+ --shadow-md: 0 6px 20px rgba(55, 96, 191, 0.14);
874
+ --shadow-lg: 0 12px 40px rgba(55, 96, 191, 0.2);
875
+ --mach-bg-sl: 40% 88%;
876
+ --mach-fg-sl: 45% 34%;
877
+ --mach-border-sl: 32% 72%;
878
+ }
879
+
880
+ /* ---- Kanagawa Lotus — the official light variant: warm woodblock paper,
881
+ ink-violet accent, muted earthen hues (brownish base). */
882
+ [data-theme="kanagawalotus"] {
883
+ color-scheme: light;
884
+ --c-bg: #f2ecbc;
885
+ --c-bg-elev: #ebe3b0;
886
+ --c-bg-elev-2: #e5ddb0;
887
+ --c-surface: #ebe3b0;
888
+ --c-border: #d5cea3;
889
+ --c-border-strong: #c9c193;
890
+ --c-text: #545464;
891
+ --c-text-muted: #766b90;
892
+ --c-text-faint: #a09cac;
893
+ --c-accent: #624c83;
894
+ --c-accent-ink: #f2ecbc;
895
+ --c-accent-dim: #624c83;
896
+ --c-blue: #4d699b;
897
+ --c-amber: #cc6d00;
898
+ --c-red: #c84053;
899
+ --c-green: #6f894e;
900
+ --c-violet: #624c83;
901
+ --c-gold: #836f4a;
902
+ --c-teal: #597b75;
903
+ --md-code-bg: color-mix(in srgb, var(--c-blue) 12%, var(--c-bg));
904
+ --shadow-sm: 0 1px 2px rgba(84, 84, 100, 0.12);
905
+ --shadow-md: 0 6px 20px rgba(84, 84, 100, 0.16);
906
+ --shadow-lg: 0 12px 40px rgba(84, 84, 100, 0.22);
907
+ --mach-bg-sl: 35% 84%;
908
+ --mach-fg-sl: 35% 34%;
909
+ --mach-border-sl: 30% 66%;
910
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.2.16",
3
+ "version": "0.4.0",
4
4
  "description": "Minimal, dependency-free Svelte 5 + pure-CSS UI kit. Token-driven atoms, molecules & layouts with theming out of the box.",
5
5
  "type": "module",
6
6
  "license": "MIT",