terminoir 0.5.1 → 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
 
@@ -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;
@@ -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';
@@ -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.1",
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",