terminoir 0.5.0 → 0.6.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.
package/README.md CHANGED
@@ -157,6 +157,9 @@ Building blocks so apps compose screens without CSS of their own. See the demo's
157
157
  | `CalendarGrid` | Week (scrolling hours) and month grids; `CalendarEvent` with `muted`/`color`; helpers `calendarRange`, `shiftCalendar`, `eventsOn` |
158
158
  | `CanvasStage`, `DocumentViewer` | Full-bleed canvas host with floating overlay and side panel; paged viewer with toolbar and highlight marks |
159
159
  | `AccentSwatches` | `bind:value` accent hex from `ACCENT_PALETTE` |
160
+ | `GroupedList`, `SegmentBar` | Items in labelled groups with a colour dot and count, "Show n more" past `limit`, `collapsed` groups behind a toggle (e.g. finished items); a thin bar of proportional coloured segments |
161
+
162
+ `labelColor(name)` resolves palette names (`green`, `gray`…, see `LABEL_PALETTE`) or passes CSS colours through; `Chip`, `GroupedList` and `SegmentBar` share it.
160
163
 
161
164
  ### Utilities (typed, tree-shakeable)
162
165
 
@@ -3,8 +3,8 @@
3
3
  import {
4
4
  addDays,
5
5
  calendarRange,
6
- dayLayout,
7
6
  eventsOn,
7
+ layoutDay,
8
8
  monthGrid,
9
9
  sameDay,
10
10
  startOfWeek,
@@ -52,11 +52,12 @@
52
52
  }
53
53
  </script>
54
54
 
55
- {#snippet chip(event: CalendarEvent, showTime: boolean)}
55
+ {#snippet chip(event: CalendarEvent, showTime: boolean, compact = false)}
56
56
  <button
57
57
  type="button"
58
58
  class="terminoir-cal__event terminoir-focusable"
59
59
  class:terminoir-cal__event--muted={event.muted}
60
+ class:terminoir-cal__event--compact={compact}
60
61
  style:--event-color={event.color}
61
62
  title={event.title}
62
63
  disabled={!onEventClick}
@@ -91,10 +92,15 @@
91
92
  </div>
92
93
  {#each weekDays as day (day.getTime())}
93
94
  <div class="terminoir-cal__day-col" class:terminoir-cal__day-col--today={sameDay(day, today)}>
94
- {#each eventsOn(events, day).filter((e) => !e.allDay) as event (event.id)}
95
- {@const layout = dayLayout(event)}
96
- <div class="terminoir-cal__timed" style:top={`${layout.top}%`} style:height={`${layout.height}%`}>
97
- {@render chip(event, true)}
95
+ {#each layoutDay(eventsOn(events, day).filter((e) => !e.allDay)) as placed (placed.event.id)}
96
+ <div
97
+ class="terminoir-cal__timed"
98
+ style:top={`${placed.top}%`}
99
+ style:height={`${placed.height}%`}
100
+ style:left={`calc(${(placed.lane / placed.lanes) * 100}% + 2px)`}
101
+ style:width={`calc(${100 / placed.lanes}% - 4px)`}
102
+ >
103
+ {@render chip(placed.event, true, placed.compact)}
98
104
  </div>
99
105
  {/each}
100
106
  </div>
@@ -179,8 +185,17 @@
179
185
  background-size: 100% var(--cal-hour);
180
186
  }
181
187
  .terminoir-cal__day-col--today { background-color: color-mix(in srgb, var(--primary) 4%, transparent); }
182
- .terminoir-cal__timed { position: absolute; left: 2px; right: 2px; display: flex; }
188
+ .terminoir-cal__timed { position: absolute; display: flex; }
183
189
  .terminoir-cal__timed .terminoir-cal__event { align-items: flex-start; height: 100%; flex-direction: column; gap: 0; }
190
+ .terminoir-cal__timed .terminoir-cal__event--compact {
191
+ flex-direction: row-reverse;
192
+ justify-content: flex-end;
193
+ align-items: center;
194
+ gap: var(--space-1);
195
+ padding-block: 0;
196
+ font-size: 0.6875rem;
197
+ line-height: 1;
198
+ }
184
199
  .terminoir-cal__event {
185
200
  --event-color: var(--primary);
186
201
  display: flex;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../utils/cn.js';
3
+ import { labelColor } from '../utils/palette.js';
3
4
 
4
5
  type ChipTone = 'default' | 'overdue' | 'today' | 'done';
5
6
 
@@ -26,8 +27,7 @@
26
27
  children?: import('svelte').Snippet;
27
28
  } & Record<string, unknown> = $props();
28
29
 
29
- const PALETTE = new Set(['grey', 'gray', 'blue', 'green', 'red', 'yellow', 'orange', 'purple', 'pink', 'teal']);
30
- const dot = $derived(color ? (PALETTE.has(color) ? `var(--chip-${color === 'gray' ? 'grey' : color})` : color) : undefined);
30
+ const dot = $derived(labelColor(color));
31
31
  </script>
32
32
 
33
33
  <span
@@ -53,15 +53,6 @@
53
53
 
54
54
  <style>
55
55
  .terminoir-chip {
56
- --chip-grey: #8b8f98;
57
- --chip-blue: #5b8def;
58
- --chip-green: #3fae6a;
59
- --chip-red: #e5534b;
60
- --chip-yellow: #d4a72c;
61
- --chip-orange: #e0823d;
62
- --chip-purple: #9b6bdf;
63
- --chip-pink: #d9669f;
64
- --chip-teal: #2fa7a0;
65
56
  display: inline-flex;
66
57
  align-items: center;
67
58
  gap: 0.35rem;
@@ -28,19 +28,27 @@
28
28
  }
29
29
  }
30
30
 
31
- function onKeydown(event: KeyboardEvent) {
32
- if (event.key === 'Escape') {
33
- open = false;
34
- onclose?.();
35
- }
36
- }
31
+ let dialogEl: HTMLDivElement | undefined = $state();
37
32
 
33
+ /** Escape closes from anywhere, not only when focus is already inside the dialog. */
38
34
  $effect(() => {
39
35
  if (!open || typeof document === 'undefined') return;
40
36
  const prev = document.body.style.overflow;
41
37
  document.body.style.overflow = 'hidden';
38
+ const onKey = (event: KeyboardEvent) => {
39
+ if (event.key === 'Escape') {
40
+ open = false;
41
+ onclose?.();
42
+ }
43
+ };
44
+ document.addEventListener('keydown', onKey);
45
+ queueMicrotask(() => {
46
+ const first = dialogEl?.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex="-1"])');
47
+ (first ?? dialogEl)?.focus();
48
+ });
42
49
  return () => {
43
50
  document.body.style.overflow = prev;
51
+ document.removeEventListener('keydown', onKey);
44
52
  };
45
53
  });
46
54
  </script>
@@ -50,9 +58,9 @@
50
58
  class="terminoir-dialog-backdrop"
51
59
  role="presentation"
52
60
  onclick={onBackdropClick}
53
- onkeydown={onKeydown}
54
61
  >
55
62
  <div
63
+ bind:this={dialogEl}
56
64
  class={cn('terminoir-dialog glass-modal terminoir-focusable', className)}
57
65
  role="dialog"
58
66
  aria-modal="true"
@@ -0,0 +1,149 @@
1
+ <script lang="ts">
2
+ import { cn } from '../utils/cn.js';
3
+ import { labelColor } from '../utils/palette.js';
4
+ import Disclosure from './Disclosure.svelte';
5
+ import List from './List.svelte';
6
+ import ListItem from './ListItem.svelte';
7
+
8
+ export type GroupedListItem = {
9
+ id: string;
10
+ title: string;
11
+ /** Short text on the right, e.g. a date. */
12
+ meta?: string;
13
+ metaTone?: 'muted' | 'danger' | 'warning';
14
+ };
15
+
16
+ export type GroupedListGroup = {
17
+ key: string;
18
+ label: string;
19
+ /** Palette name or CSS colour for the heading dot. */
20
+ color?: string | null;
21
+ /** Folded behind a "Label (n)" toggle, e.g. finished items. */
22
+ collapsed?: boolean;
23
+ items: GroupedListItem[];
24
+ };
25
+
26
+ let {
27
+ groups,
28
+ label,
29
+ limit = 6,
30
+ onselect,
31
+ class: className = '',
32
+ }: {
33
+ /** Shown in order; empty groups are skipped. One group with an empty label renders as a plain list. */
34
+ groups: GroupedListGroup[];
35
+ /** Accessible name for the lists. */
36
+ label: string;
37
+ /** Rows per open group before a "Show n more" button. */
38
+ limit?: number;
39
+ onselect?: (id: string) => void;
40
+ class?: string;
41
+ } = $props();
42
+
43
+ let expanded = $state<Record<string, boolean>>({});
44
+
45
+ const visible = $derived(groups.filter((g) => g.items.length > 0));
46
+ const open = $derived(visible.filter((g) => !g.collapsed));
47
+ const folded = $derived(visible.filter((g) => g.collapsed));
48
+
49
+ function rows(group: GroupedListGroup): GroupedListItem[] {
50
+ return expanded[group.key] || group.items.length <= limit + 1 ? group.items : group.items.slice(0, limit);
51
+ }
52
+ </script>
53
+
54
+ {#snippet items(group: GroupedListGroup, all: boolean)}
55
+ <List label={group.label ? `${label}: ${group.label}` : label} bordered={false}>
56
+ {#each all ? group.items : rows(group) as item (item.id)}
57
+ <ListItem title={item.title} onclick={onselect ? () => onselect(item.id) : undefined}>
58
+ {#snippet trailing()}
59
+ {#if item.meta}
60
+ <span class={cn('terminoir-grouped-list__meta', item.metaTone && `terminoir-grouped-list__meta--${item.metaTone}`)}>{item.meta}</span>
61
+ {/if}
62
+ {/snippet}
63
+ </ListItem>
64
+ {/each}
65
+ </List>
66
+ {#if !all && rows(group).length < group.items.length}
67
+ <button type="button" class="terminoir-grouped-list__more terminoir-focusable" onclick={() => (expanded[group.key] = true)}>
68
+ Show {group.items.length - rows(group).length} more
69
+ </button>
70
+ {/if}
71
+ {/snippet}
72
+
73
+ <div class={cn('terminoir-grouped-list', className)}>
74
+ {#each open as group (group.key)}
75
+ <div class="terminoir-grouped-list__group">
76
+ {#if group.label}
77
+ <div class="terminoir-grouped-list__heading">
78
+ {#if labelColor(group.color)}<span class="terminoir-grouped-list__dot" style:background={labelColor(group.color)} aria-hidden="true"></span>{/if}
79
+ <span>{group.label}</span>
80
+ <span class="terminoir-grouped-list__count">{group.items.length}</span>
81
+ </div>
82
+ {/if}
83
+ {@render items(group, false)}
84
+ </div>
85
+ {/each}
86
+ {#each folded as group (group.key)}
87
+ <Disclosure summary={`${group.label} (${group.items.length})`}>
88
+ {@render items(group, true)}
89
+ </Disclosure>
90
+ {/each}
91
+ </div>
92
+
93
+ <style>
94
+ .terminoir-grouped-list {
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: var(--space-4);
98
+ min-width: 0;
99
+ }
100
+ .terminoir-grouped-list__group {
101
+ display: flex;
102
+ flex-direction: column;
103
+ gap: var(--space-1);
104
+ min-width: 0;
105
+ }
106
+ .terminoir-grouped-list__heading {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: var(--space-2);
110
+ font-size: 0.75rem;
111
+ font-weight: 500;
112
+ color: var(--muted-foreground);
113
+ }
114
+ .terminoir-grouped-list__dot {
115
+ width: 0.45rem;
116
+ height: 0.45rem;
117
+ border-radius: 999px;
118
+ flex-shrink: 0;
119
+ }
120
+ .terminoir-grouped-list__count {
121
+ font-variant-numeric: tabular-nums;
122
+ opacity: 0.75;
123
+ }
124
+ .terminoir-grouped-list__group :global(.terminoir-list-item__row) {
125
+ min-height: 2.25rem;
126
+ padding: 0.3rem 0.5rem;
127
+ border-radius: var(--radius-control);
128
+ }
129
+ .terminoir-grouped-list__meta {
130
+ font-size: 0.75rem;
131
+ font-variant-numeric: tabular-nums;
132
+ color: var(--muted-foreground);
133
+ white-space: nowrap;
134
+ }
135
+ .terminoir-grouped-list__meta--danger { color: var(--destructive); }
136
+ .terminoir-grouped-list__meta--warning { color: var(--status-today); }
137
+ .terminoir-grouped-list__more {
138
+ align-self: flex-start;
139
+ border: 0;
140
+ background: transparent;
141
+ color: var(--muted-foreground);
142
+ font: inherit;
143
+ font-size: 0.8125rem;
144
+ padding: 0.25rem 0.5rem;
145
+ cursor: pointer;
146
+ border-radius: var(--radius-control);
147
+ }
148
+ .terminoir-grouped-list__more:hover { color: var(--foreground); }
149
+ </style>
@@ -0,0 +1,29 @@
1
+ export type GroupedListItem = {
2
+ id: string;
3
+ title: string;
4
+ /** Short text on the right, e.g. a date. */
5
+ meta?: string;
6
+ metaTone?: 'muted' | 'danger' | 'warning';
7
+ };
8
+ export type GroupedListGroup = {
9
+ key: string;
10
+ label: string;
11
+ /** Palette name or CSS colour for the heading dot. */
12
+ color?: string | null;
13
+ /** Folded behind a "Label (n)" toggle, e.g. finished items. */
14
+ collapsed?: boolean;
15
+ items: GroupedListItem[];
16
+ };
17
+ type $$ComponentProps = {
18
+ /** Shown in order; empty groups are skipped. One group with an empty label renders as a plain list. */
19
+ groups: GroupedListGroup[];
20
+ /** Accessible name for the lists. */
21
+ label: string;
22
+ /** Rows per open group before a "Show n more" button. */
23
+ limit?: number;
24
+ onselect?: (id: string) => void;
25
+ class?: string;
26
+ };
27
+ declare const GroupedList: import("svelte").Component<$$ComponentProps, {}, "">;
28
+ type GroupedList = ReturnType<typeof GroupedList>;
29
+ export default GroupedList;
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import { cn } from '../utils/cn.js';
3
+ import { labelColor } from '../utils/palette.js';
4
+
5
+ export type Segment = { label: string; count: number; color?: string | null };
6
+
7
+ let {
8
+ segments,
9
+ label,
10
+ class: className = '',
11
+ }: {
12
+ /** In order; zero counts are skipped. Colours are palette names or CSS colours. */
13
+ segments: Segment[];
14
+ /** Accessible name, e.g. "Status of 30 linked tasks". */
15
+ label: string;
16
+ class?: string;
17
+ } = $props();
18
+
19
+ const total = $derived(segments.reduce((sum, s) => sum + Math.max(0, s.count), 0));
20
+ const shown = $derived(segments.filter((s) => s.count > 0));
21
+ const summary = $derived(shown.map((s) => `${s.label} ${s.count}`).join(', '));
22
+ </script>
23
+
24
+ {#if total > 0}
25
+ <div class={cn('terminoir-segment-bar', className)} role="img" aria-label={`${label}: ${summary}`}>
26
+ {#each shown as segment (segment.label)}
27
+ <span
28
+ class="terminoir-segment-bar__part"
29
+ style:flex-grow={segment.count}
30
+ style:background={labelColor(segment.color) ?? 'color-mix(in srgb, var(--foreground) 22%, transparent)'}
31
+ title={`${segment.label}: ${segment.count}`}
32
+ ></span>
33
+ {/each}
34
+ </div>
35
+ {/if}
36
+
37
+ <style>
38
+ .terminoir-segment-bar {
39
+ display: flex;
40
+ gap: 2px;
41
+ width: 100%;
42
+ height: 0.375rem;
43
+ border-radius: 999px;
44
+ overflow: hidden;
45
+ }
46
+ .terminoir-segment-bar__part {
47
+ flex-basis: 0;
48
+ min-width: 0.375rem;
49
+ }
50
+ </style>
@@ -0,0 +1,15 @@
1
+ export type Segment = {
2
+ label: string;
3
+ count: number;
4
+ color?: string | null;
5
+ };
6
+ type $$ComponentProps = {
7
+ /** In order; zero counts are skipped. Colours are palette names or CSS colours. */
8
+ segments: Segment[];
9
+ /** Accessible name, e.g. "Status of 30 linked tasks". */
10
+ label: string;
11
+ class?: string;
12
+ };
13
+ declare const SegmentBar: import("svelte").Component<$$ComponentProps, {}, "">;
14
+ type SegmentBar = ReturnType<typeof SegmentBar>;
15
+ export default SegmentBar;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { cn } from './utils/cn.js';
2
2
  export { ACCENT_PALETTE, accentFromString, type AccentColor } from './utils/accent.js';
3
+ export { LABEL_PALETTE, labelColor, type LabelColor } from './utils/palette.js';
3
4
  export { ICON_PICKER_OPTIONS, NAV_ICON_NAMES, iconClassName, type IconPickerId } from './utils/icons.js';
4
5
  export { readStorage, writeStorage, namespacedKey } from './utils/storage.js';
5
6
  export { FONT_REGISTRY, FONT_PICKER_UI_OPTIONS, UI_FONT_OPTIONS, CODE_FONT_OPTIONS, applyFontPreferences, loadFontPreferences, persistFontPreferences, getFontOption, type FontOption, type FontPreferences, type FontCategory, } from './utils/fonts.js';
@@ -45,6 +46,10 @@ export { default as Stack } from './components/Stack.svelte';
45
46
  export { default as Inline } from './components/Inline.svelte';
46
47
  export { default as Text } from './components/Text.svelte';
47
48
  export { default as Disclosure } from './components/Disclosure.svelte';
49
+ export { default as GroupedList } from './components/GroupedList.svelte';
50
+ export type { GroupedListGroup, GroupedListItem } from './components/GroupedList.svelte';
51
+ export { default as SegmentBar } from './components/SegmentBar.svelte';
52
+ export type { Segment } from './components/SegmentBar.svelte';
48
53
  export { default as NavSection } from './components/NavSection.svelte';
49
54
  export { default as Spacer } from './components/Spacer.svelte';
50
55
  export { default as Centered } from './components/Centered.svelte';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { cn } from './utils/cn.js';
2
2
  export { ACCENT_PALETTE, accentFromString } from './utils/accent.js';
3
+ export { LABEL_PALETTE, labelColor } from './utils/palette.js';
3
4
  export { ICON_PICKER_OPTIONS, NAV_ICON_NAMES, iconClassName } from './utils/icons.js';
4
5
  export { readStorage, writeStorage, namespacedKey } from './utils/storage.js';
5
6
  export { FONT_REGISTRY, FONT_PICKER_UI_OPTIONS, UI_FONT_OPTIONS, CODE_FONT_OPTIONS, applyFontPreferences, loadFontPreferences, persistFontPreferences, getFontOption, } from './utils/fonts.js';
@@ -44,6 +45,8 @@ export { default as Stack } from './components/Stack.svelte';
44
45
  export { default as Inline } from './components/Inline.svelte';
45
46
  export { default as Text } from './components/Text.svelte';
46
47
  export { default as Disclosure } from './components/Disclosure.svelte';
48
+ export { default as GroupedList } from './components/GroupedList.svelte';
49
+ export { default as SegmentBar } from './components/SegmentBar.svelte';
47
50
  export { default as NavSection } from './components/NavSection.svelte';
48
51
  export { default as Spacer } from './components/Spacer.svelte';
49
52
  export { default as Centered } from './components/Centered.svelte';
@@ -26,10 +26,23 @@ export declare function calendarRange(anchor: Date, mode: CalendarMode): {
26
26
  export declare function shiftCalendar(anchor: Date, mode: CalendarMode, delta: number): Date;
27
27
  /** Six Monday-start weeks covering the month of `monthStart`. */
28
28
  export declare function monthGrid(monthStart: Date): Date[];
29
- /** Events starting on `day`, all-day first, then by start time. */
29
+ /** Events showing on `day` (multi-day all-day events on each day), all-day first, then by start time. */
30
30
  export declare function eventsOn(events: CalendarEvent[], day: Date): CalendarEvent[];
31
31
  /** Vertical position of a timed event in a day column, as percentages of the day. */
32
32
  export declare function dayLayout(event: CalendarEvent): {
33
33
  top: number;
34
34
  height: number;
35
35
  };
36
+ export type PlacedEvent = {
37
+ event: CalendarEvent;
38
+ /** Percent of the day from the top. */
39
+ top: number;
40
+ height: number;
41
+ /** Side-by-side column within its overlap group, and how many columns the group has. */
42
+ lane: number;
43
+ lanes: number;
44
+ /** Short enough to render as one line. */
45
+ compact: boolean;
46
+ };
47
+ /** Lay out a day's timed events so overlapping ones sit side by side instead of on top of each other. */
48
+ export declare function layoutDay(events: CalendarEvent[]): PlacedEvent[];
@@ -46,10 +46,18 @@ export function monthGrid(monthStart) {
46
46
  const first = startOfWeek(monthStart);
47
47
  return Array.from({ length: MONTH_GRID_DAYS }, (_, i) => addDays(first, i));
48
48
  }
49
- /** Events starting on `day`, all-day first, then by start time. */
49
+ /** Whether `event` shows on `day`: timed events on their start day, all-day events on every day they span. */
50
+ function occursOn(event, day) {
51
+ if (!event.allDay)
52
+ return sameDay(event.start, day);
53
+ const dayStart = startOfDay(day).getTime();
54
+ const end = Math.max(event.end.getTime(), addDays(startOfDay(event.start), 1).getTime());
55
+ return startOfDay(event.start).getTime() <= dayStart && dayStart < end;
56
+ }
57
+ /** Events showing on `day` (multi-day all-day events on each day), all-day first, then by start time. */
50
58
  export function eventsOn(events, day) {
51
59
  return events
52
- .filter((e) => sameDay(e.start, day))
60
+ .filter((e) => occursOn(e, day))
53
61
  .sort((a, b) => Number(b.allDay) - Number(a.allDay) || a.start.getTime() - b.start.getTime());
54
62
  }
55
63
  /** Vertical position of a timed event in a day column, as percentages of the day. */
@@ -59,3 +67,39 @@ export function dayLayout(event) {
59
67
  const top = (minutes / MINUTES_PER_DAY) * 100;
60
68
  return { top, height: Math.min((duration / MINUTES_PER_DAY) * 100, 100 - top) };
61
69
  }
70
+ const COMPACT_MAX_MINUTES = 30;
71
+ /** Lay out a day's timed events so overlapping ones sit side by side instead of on top of each other. */
72
+ export function layoutDay(events) {
73
+ const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime() || b.end.getTime() - a.end.getTime());
74
+ const placed = [];
75
+ let group = [];
76
+ let laneEnds = [];
77
+ let groupEnd = -Infinity;
78
+ const closeGroup = () => {
79
+ for (const item of group)
80
+ item.lanes = laneEnds.length;
81
+ group = [];
82
+ laneEnds = [];
83
+ };
84
+ for (const event of sorted) {
85
+ const start = event.start.getTime();
86
+ const end = Math.max(event.end.getTime(), start + MIN_EVENT_MINUTES * 60000);
87
+ if (start >= groupEnd)
88
+ closeGroup();
89
+ let lane = laneEnds.findIndex((laneEnd) => laneEnd <= start);
90
+ if (lane === -1) {
91
+ lane = laneEnds.length;
92
+ laneEnds.push(end);
93
+ }
94
+ else {
95
+ laneEnds[lane] = end;
96
+ }
97
+ groupEnd = Math.max(groupEnd, end);
98
+ const minutes = (event.end.getTime() - start) / 60000;
99
+ const item = { event, ...dayLayout(event), lane, lanes: 1, compact: minutes <= COMPACT_MAX_MINUTES };
100
+ group.push(item);
101
+ placed.push(item);
102
+ }
103
+ closeGroup();
104
+ return placed;
105
+ }
@@ -0,0 +1,15 @@
1
+ /** Named colours for user-chosen labels (select options, tags). Shared by Chip, GroupedList, SegmentBar. */
2
+ export declare const LABEL_PALETTE: {
3
+ readonly grey: "#8b8f98";
4
+ readonly blue: "#5b8def";
5
+ readonly green: "#3fae6a";
6
+ readonly red: "#e5534b";
7
+ readonly yellow: "#d4a72c";
8
+ readonly orange: "#e0823d";
9
+ readonly purple: "#9b6bdf";
10
+ readonly pink: "#d9669f";
11
+ readonly teal: "#2fa7a0";
12
+ };
13
+ export type LabelColor = keyof typeof LABEL_PALETTE;
14
+ /** CSS colour for a palette name (`gray` = `grey`) or any CSS colour; undefined stays undefined. */
15
+ export declare function labelColor(color: string | null | undefined): string | undefined;
@@ -0,0 +1,19 @@
1
+ /** Named colours for user-chosen labels (select options, tags). Shared by Chip, GroupedList, SegmentBar. */
2
+ export const LABEL_PALETTE = {
3
+ grey: '#8b8f98',
4
+ blue: '#5b8def',
5
+ green: '#3fae6a',
6
+ red: '#e5534b',
7
+ yellow: '#d4a72c',
8
+ orange: '#e0823d',
9
+ purple: '#9b6bdf',
10
+ pink: '#d9669f',
11
+ teal: '#2fa7a0',
12
+ };
13
+ /** CSS colour for a palette name (`gray` = `grey`) or any CSS colour; undefined stays undefined. */
14
+ export function labelColor(color) {
15
+ if (!color)
16
+ return undefined;
17
+ const name = color === 'gray' ? 'grey' : color;
18
+ return name in LABEL_PALETTE ? LABEL_PALETTE[name] : color;
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminoir",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Terminoir \u2014 Liquid Glass design tokens and Svelte 5 UI components.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",