terminoir 0.5.1 → 0.7.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 +6 -1
- package/dist/components/Chip.svelte +2 -11
- package/dist/components/ChipSelect.svelte +228 -0
- package/dist/components/ChipSelect.svelte.d.ts +18 -0
- package/dist/components/DateField.svelte +144 -0
- package/dist/components/DateField.svelte.d.ts +15 -0
- package/dist/components/GroupedList.svelte +149 -0
- package/dist/components/GroupedList.svelte.d.ts +29 -0
- package/dist/components/PropertyRow.svelte +68 -0
- package/dist/components/PropertyRow.svelte.d.ts +12 -0
- package/dist/components/SearchPicker.svelte +2 -0
- package/dist/components/SegmentBar.svelte +50 -0
- package/dist/components/SegmentBar.svelte.d.ts +15 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/utils/componentTypes.d.ts +12 -0
- package/dist/utils/dayLabel.d.ts +7 -0
- package/dist/utils/dayLabel.js +35 -0
- package/dist/utils/palette.d.ts +15 -0
- package/dist/utils/palette.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -152,11 +152,16 @@ Building blocks so apps compose screens without CSS of their own. See the demo's
|
|
|
152
152
|
| `NavSection` | Sidebar section with title and action |
|
|
153
153
|
| `DataTable` | Columns, `cell` snippet, clickable rows, `hideOnMobile`, empty state |
|
|
154
154
|
| `List`, `ListItem`, `DescriptionList`, `FormRow` | Rows with leading/trailing slots; key–value pairs; label-left form rows |
|
|
155
|
-
| `SearchPicker` | Debounced async combobox (`search(q)`), keyboard navigation |
|
|
155
|
+
| `SearchPicker` | Debounced async combobox (`search(q)`), keyboard navigation; grows to fill a row after chips |
|
|
156
|
+
| `PropertyRow` | Compact icon + label row for inline-edited values (record properties); hover highlights the row |
|
|
157
|
+
| `ChipSelect`, `DateField` | Value shown as a coloured `Chip` that opens a grouped option list (`ChipSelectOption`, `ChipSelectGroup`); date shown as "Today" / "Tomorrow" / weekday / "Nov 12" (`dayLabel`), `deadline` tints past and today, opens the native picker, Backspace clears |
|
|
156
158
|
| `TitleInput`, `FileButton`, `RadioCards` | Borderless heading input; button that opens a file picker; card-style radio group |
|
|
157
159
|
| `CalendarGrid` | Week (scrolling hours) and month grids; `CalendarEvent` with `muted`/`color`; helpers `calendarRange`, `shiftCalendar`, `eventsOn` |
|
|
158
160
|
| `CanvasStage`, `DocumentViewer` | Full-bleed canvas host with floating overlay and side panel; paged viewer with toolbar and highlight marks |
|
|
159
161
|
| `AccentSwatches` | `bind:value` accent hex from `ACCENT_PALETTE` |
|
|
162
|
+
| `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 |
|
|
163
|
+
|
|
164
|
+
`labelColor(name)` resolves palette names (`green`, `gray`…, see `LABEL_PALETTE`) or passes CSS colours through; `Chip`, `GroupedList` and `SegmentBar` share it.
|
|
160
165
|
|
|
161
166
|
### Utilities (typed, tree-shakeable)
|
|
162
167
|
|
|
@@ -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
|
|
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,228 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
import type { ChipSelectGroup, ChipSelectOption } from '../utils/componentTypes.js';
|
|
5
|
+
import { moveIndex } from '../utils/listNav.js';
|
|
6
|
+
import Chip from './Chip.svelte';
|
|
7
|
+
import NavIcon from './NavIcon.svelte';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
label,
|
|
11
|
+
value = '',
|
|
12
|
+
options,
|
|
13
|
+
groups = [],
|
|
14
|
+
placeholder = 'Empty',
|
|
15
|
+
clearable = true,
|
|
16
|
+
onchange,
|
|
17
|
+
class: className = '',
|
|
18
|
+
}: {
|
|
19
|
+
/** Accessible name, e.g. the property name. */
|
|
20
|
+
label: string;
|
|
21
|
+
/** Selected option id; empty string for none. */
|
|
22
|
+
value?: string;
|
|
23
|
+
options: ChipSelectOption[];
|
|
24
|
+
/** Headings, in order, for options with a `group`; ungrouped options come first. */
|
|
25
|
+
groups?: ChipSelectGroup[];
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
/** Offer a "Clear" entry when something is selected. */
|
|
28
|
+
clearable?: boolean;
|
|
29
|
+
onchange: (value: string) => void;
|
|
30
|
+
class?: string;
|
|
31
|
+
} = $props();
|
|
32
|
+
|
|
33
|
+
const uid = $props.id();
|
|
34
|
+
const CLEAR = { id: '', label: 'Clear' } satisfies ChipSelectOption;
|
|
35
|
+
|
|
36
|
+
let open = $state(false);
|
|
37
|
+
let active = $state(-1);
|
|
38
|
+
let root: HTMLDivElement | undefined = $state();
|
|
39
|
+
let trigger: HTMLButtonElement | undefined = $state();
|
|
40
|
+
let list: HTMLUListElement | undefined = $state();
|
|
41
|
+
|
|
42
|
+
const selected = $derived(options.find((o) => o.id === value));
|
|
43
|
+
const sections = $derived([
|
|
44
|
+
{ id: '', label: '', options: options.filter((o) => !o.group || !groups.some((g) => g.id === o.group)) },
|
|
45
|
+
...groups.map((g) => ({ ...g, options: options.filter((o) => o.group === g.id) })),
|
|
46
|
+
].filter((section) => section.options.length));
|
|
47
|
+
const choices = $derived([...sections.flatMap((s) => s.options), ...(clearable && selected ? [CLEAR] : [])]);
|
|
48
|
+
|
|
49
|
+
async function show() {
|
|
50
|
+
open = true;
|
|
51
|
+
active = Math.max(0, choices.findIndex((o) => o.id === value));
|
|
52
|
+
await tick();
|
|
53
|
+
list?.focus();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hide() {
|
|
57
|
+
open = false;
|
|
58
|
+
trigger?.focus();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function choose(option: ChipSelectOption | undefined) {
|
|
62
|
+
if (!option) return;
|
|
63
|
+
if (option.id !== value) onchange(option.id);
|
|
64
|
+
hide();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onTriggerKeydown(event: KeyboardEvent) {
|
|
68
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
void show();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function onListKeydown(event: KeyboardEvent) {
|
|
74
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
75
|
+
active = moveIndex(active, event.key === 'ArrowDown' ? 1 : -1, choices.length);
|
|
76
|
+
} else if (event.key === 'Enter' || event.key === ' ') {
|
|
77
|
+
choose(choices[active]);
|
|
78
|
+
} else if (event.key === 'Escape') {
|
|
79
|
+
hide();
|
|
80
|
+
} else if (event.key === 'Tab') {
|
|
81
|
+
open = false;
|
|
82
|
+
return;
|
|
83
|
+
} else {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
$effect(() => {
|
|
91
|
+
if (!open) return;
|
|
92
|
+
const onPointer = (event: PointerEvent) => {
|
|
93
|
+
if (root && !root.contains(event.target as Node)) open = false;
|
|
94
|
+
};
|
|
95
|
+
document.addEventListener('pointerdown', onPointer);
|
|
96
|
+
return () => document.removeEventListener('pointerdown', onPointer);
|
|
97
|
+
});
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
{#snippet item(option: ChipSelectOption)}
|
|
101
|
+
{@const index = choices.indexOf(option)}
|
|
102
|
+
<!-- Listbox pattern: focus stays on the list, which handles arrow keys and Enter. -->
|
|
103
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
104
|
+
<li
|
|
105
|
+
id={`${uid}-opt-${index}`}
|
|
106
|
+
role="option"
|
|
107
|
+
aria-selected={option.id === value}
|
|
108
|
+
class="terminoir-chip-select__option"
|
|
109
|
+
class:terminoir-chip-select__option--active={index === active}
|
|
110
|
+
onpointermove={() => (active = index)}
|
|
111
|
+
onclick={() => choose(option)}
|
|
112
|
+
>
|
|
113
|
+
{#if option === CLEAR}
|
|
114
|
+
<span class="terminoir-chip-select__clear"><NavIcon name="xmark" /> {CLEAR.label}</span>
|
|
115
|
+
{:else}
|
|
116
|
+
<Chip color={option.color ?? 'grey'}>{option.label}</Chip>
|
|
117
|
+
{#if option.id === value}<NavIcon name="check" class="terminoir-chip-select__check" />{/if}
|
|
118
|
+
{/if}
|
|
119
|
+
</li>
|
|
120
|
+
{/snippet}
|
|
121
|
+
|
|
122
|
+
<div bind:this={root} class={cn('terminoir-chip-select', className)}>
|
|
123
|
+
<button
|
|
124
|
+
bind:this={trigger}
|
|
125
|
+
type="button"
|
|
126
|
+
class="terminoir-chip-select__trigger terminoir-focusable"
|
|
127
|
+
aria-haspopup="listbox"
|
|
128
|
+
aria-expanded={open}
|
|
129
|
+
aria-label={`${label}: ${selected?.label ?? placeholder}`}
|
|
130
|
+
onclick={() => (open ? hide() : void show())}
|
|
131
|
+
onkeydown={onTriggerKeydown}
|
|
132
|
+
>
|
|
133
|
+
{#if selected}
|
|
134
|
+
<Chip color={selected.color ?? 'grey'}>{selected.label}</Chip>
|
|
135
|
+
{:else}
|
|
136
|
+
<span class="terminoir-chip-select__placeholder">{placeholder}</span>
|
|
137
|
+
{/if}
|
|
138
|
+
</button>
|
|
139
|
+
{#if open}
|
|
140
|
+
<ul
|
|
141
|
+
bind:this={list}
|
|
142
|
+
class="terminoir-chip-select__list glass-popover terminoir-focusable"
|
|
143
|
+
role="listbox"
|
|
144
|
+
aria-label={label}
|
|
145
|
+
aria-activedescendant={active >= 0 ? `${uid}-opt-${active}` : undefined}
|
|
146
|
+
tabindex="-1"
|
|
147
|
+
onkeydown={onListKeydown}
|
|
148
|
+
>
|
|
149
|
+
{#each sections as section (section.id)}
|
|
150
|
+
{#if section.label}<li role="presentation" class="terminoir-chip-select__heading">{section.label}</li>{/if}
|
|
151
|
+
{#each section.options as option (option.id)}{@render item(option)}{/each}
|
|
152
|
+
{/each}
|
|
153
|
+
{#if clearable && selected}{@render item(CLEAR)}{/if}
|
|
154
|
+
</ul>
|
|
155
|
+
{/if}
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<style>
|
|
159
|
+
.terminoir-chip-select {
|
|
160
|
+
position: relative;
|
|
161
|
+
display: inline-block;
|
|
162
|
+
max-width: 100%;
|
|
163
|
+
}
|
|
164
|
+
.terminoir-chip-select__trigger {
|
|
165
|
+
display: inline-flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
width: max-content;
|
|
168
|
+
max-width: 100%;
|
|
169
|
+
min-height: 1.85rem;
|
|
170
|
+
padding: 0.15rem 0.35rem;
|
|
171
|
+
border: 0;
|
|
172
|
+
border-radius: var(--radius-control);
|
|
173
|
+
background: transparent;
|
|
174
|
+
color: inherit;
|
|
175
|
+
font: inherit;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
}
|
|
178
|
+
.terminoir-chip-select__trigger:hover {
|
|
179
|
+
background: color-mix(in srgb, var(--foreground) 6%, transparent);
|
|
180
|
+
}
|
|
181
|
+
.terminoir-chip-select__placeholder {
|
|
182
|
+
font-size: 0.875rem;
|
|
183
|
+
color: var(--muted-foreground);
|
|
184
|
+
opacity: 0.7;
|
|
185
|
+
}
|
|
186
|
+
.terminoir-chip-select__list {
|
|
187
|
+
position: absolute;
|
|
188
|
+
top: calc(100% + var(--space-1));
|
|
189
|
+
left: 0;
|
|
190
|
+
z-index: 40;
|
|
191
|
+
min-width: 12rem;
|
|
192
|
+
max-height: 18rem;
|
|
193
|
+
overflow-y: auto;
|
|
194
|
+
margin: 0;
|
|
195
|
+
padding: var(--space-1);
|
|
196
|
+
list-style: none;
|
|
197
|
+
}
|
|
198
|
+
.terminoir-chip-select__heading {
|
|
199
|
+
padding: 0.5rem 0.6rem 0.25rem;
|
|
200
|
+
font-size: 0.6875rem;
|
|
201
|
+
letter-spacing: 0.06em;
|
|
202
|
+
text-transform: uppercase;
|
|
203
|
+
color: var(--muted-foreground);
|
|
204
|
+
}
|
|
205
|
+
.terminoir-chip-select__option {
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
justify-content: space-between;
|
|
209
|
+
gap: var(--space-2);
|
|
210
|
+
padding: 0.35rem 0.5rem;
|
|
211
|
+
border-radius: calc(var(--radius-control) - 0.2rem);
|
|
212
|
+
cursor: pointer;
|
|
213
|
+
}
|
|
214
|
+
.terminoir-chip-select__option--active {
|
|
215
|
+
background: color-mix(in srgb, var(--foreground) 8%, transparent);
|
|
216
|
+
}
|
|
217
|
+
.terminoir-chip-select__option :global(.terminoir-chip-select__check) {
|
|
218
|
+
font-size: 0.75rem;
|
|
219
|
+
color: var(--muted-foreground);
|
|
220
|
+
}
|
|
221
|
+
.terminoir-chip-select__clear {
|
|
222
|
+
display: inline-flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
gap: var(--space-2);
|
|
225
|
+
font-size: 0.8125rem;
|
|
226
|
+
color: var(--muted-foreground);
|
|
227
|
+
}
|
|
228
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ChipSelectGroup, ChipSelectOption } from '../utils/componentTypes.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** Accessible name, e.g. the property name. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** Selected option id; empty string for none. */
|
|
6
|
+
value?: string;
|
|
7
|
+
options: ChipSelectOption[];
|
|
8
|
+
/** Headings, in order, for options with a `group`; ungrouped options come first. */
|
|
9
|
+
groups?: ChipSelectGroup[];
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
/** Offer a "Clear" entry when something is selected. */
|
|
12
|
+
clearable?: boolean;
|
|
13
|
+
onchange: (value: string) => void;
|
|
14
|
+
class?: string;
|
|
15
|
+
};
|
|
16
|
+
declare const ChipSelect: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
17
|
+
type ChipSelect = ReturnType<typeof ChipSelect>;
|
|
18
|
+
export default ChipSelect;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils/cn.js';
|
|
3
|
+
import { dayLabel, dayUrgency, parseYmd } from '../utils/dayLabel.js';
|
|
4
|
+
import Chip from './Chip.svelte';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
label,
|
|
8
|
+
value = '',
|
|
9
|
+
deadline = false,
|
|
10
|
+
placeholder = 'Empty',
|
|
11
|
+
onchange,
|
|
12
|
+
class: className = '',
|
|
13
|
+
}: {
|
|
14
|
+
/** Accessible name, e.g. the property name. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** `YYYY-MM-DD`, or empty for no date. */
|
|
17
|
+
value?: string;
|
|
18
|
+
/** Colour past dates as overdue and today as due today. */
|
|
19
|
+
deadline?: boolean;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
/** Called with `YYYY-MM-DD`, or an empty string when cleared. */
|
|
22
|
+
onchange: (value: string) => void;
|
|
23
|
+
class?: string;
|
|
24
|
+
} = $props();
|
|
25
|
+
|
|
26
|
+
let picker: HTMLInputElement | undefined = $state();
|
|
27
|
+
|
|
28
|
+
const date = $derived(parseYmd(value));
|
|
29
|
+
const tone = $derived.by(() => {
|
|
30
|
+
if (!deadline || !date) return 'default';
|
|
31
|
+
const urgency = dayUrgency(date);
|
|
32
|
+
return urgency === 'past' ? 'overdue' : urgency === 'today' ? 'today' : 'default';
|
|
33
|
+
});
|
|
34
|
+
const fullDate = $derived(date?.toLocaleDateString(undefined, { dateStyle: 'full' }) ?? '');
|
|
35
|
+
|
|
36
|
+
function openPicker() {
|
|
37
|
+
if (!picker) return;
|
|
38
|
+
try {
|
|
39
|
+
picker.showPicker();
|
|
40
|
+
} catch {
|
|
41
|
+
picker.focus();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function onKeydown(event: KeyboardEvent) {
|
|
46
|
+
if ((event.key === 'Backspace' || event.key === 'Delete') && value) {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
onchange('');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<span class={cn('terminoir-date-field', className)}>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
class="terminoir-date-field__trigger terminoir-focusable"
|
|
57
|
+
aria-label={`${label}: ${fullDate || placeholder}`}
|
|
58
|
+
title={fullDate || undefined}
|
|
59
|
+
onclick={openPicker}
|
|
60
|
+
onkeydown={onKeydown}
|
|
61
|
+
>
|
|
62
|
+
{#if date}
|
|
63
|
+
<Chip {tone}>{dayLabel(date)}</Chip>
|
|
64
|
+
{:else}
|
|
65
|
+
<span class="terminoir-date-field__placeholder">{placeholder}</span>
|
|
66
|
+
{/if}
|
|
67
|
+
</button>
|
|
68
|
+
{#if date}
|
|
69
|
+
<button type="button" class="terminoir-date-field__clear terminoir-focusable" aria-label={`Clear ${label}`} onclick={() => onchange('')}>×</button>
|
|
70
|
+
{/if}
|
|
71
|
+
<input
|
|
72
|
+
bind:this={picker}
|
|
73
|
+
class="terminoir-date-field__picker"
|
|
74
|
+
type="date"
|
|
75
|
+
tabindex="-1"
|
|
76
|
+
aria-hidden="true"
|
|
77
|
+
{value}
|
|
78
|
+
onchange={(event) => onchange(event.currentTarget.value)}
|
|
79
|
+
/>
|
|
80
|
+
</span>
|
|
81
|
+
|
|
82
|
+
<style>
|
|
83
|
+
.terminoir-date-field {
|
|
84
|
+
position: relative;
|
|
85
|
+
display: inline-flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: var(--space-1);
|
|
88
|
+
}
|
|
89
|
+
.terminoir-date-field__trigger {
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
width: max-content;
|
|
93
|
+
min-height: 1.85rem;
|
|
94
|
+
padding: 0.15rem 0.35rem;
|
|
95
|
+
border: 0;
|
|
96
|
+
border-radius: var(--radius-control);
|
|
97
|
+
background: transparent;
|
|
98
|
+
color: inherit;
|
|
99
|
+
font: inherit;
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
}
|
|
102
|
+
.terminoir-date-field__trigger:hover {
|
|
103
|
+
background: color-mix(in srgb, var(--foreground) 6%, transparent);
|
|
104
|
+
}
|
|
105
|
+
.terminoir-date-field__placeholder {
|
|
106
|
+
font-size: 0.875rem;
|
|
107
|
+
color: var(--muted-foreground);
|
|
108
|
+
opacity: 0.7;
|
|
109
|
+
}
|
|
110
|
+
.terminoir-date-field__clear {
|
|
111
|
+
width: 1.25rem;
|
|
112
|
+
height: 1.25rem;
|
|
113
|
+
padding: 0;
|
|
114
|
+
border: 0;
|
|
115
|
+
border-radius: 999px;
|
|
116
|
+
background: transparent;
|
|
117
|
+
color: var(--muted-foreground);
|
|
118
|
+
font: inherit;
|
|
119
|
+
line-height: 1;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
opacity: 0;
|
|
122
|
+
transition: opacity 120ms ease;
|
|
123
|
+
}
|
|
124
|
+
.terminoir-date-field:hover .terminoir-date-field__clear,
|
|
125
|
+
.terminoir-date-field__clear:focus-visible {
|
|
126
|
+
opacity: 1;
|
|
127
|
+
}
|
|
128
|
+
.terminoir-date-field__clear:hover {
|
|
129
|
+
color: var(--foreground);
|
|
130
|
+
background: color-mix(in srgb, var(--foreground) 10%, transparent);
|
|
131
|
+
}
|
|
132
|
+
/* Kept rendered (not display:none) so showPicker() can anchor the native picker under the trigger. */
|
|
133
|
+
.terminoir-date-field__picker {
|
|
134
|
+
position: absolute;
|
|
135
|
+
left: 0;
|
|
136
|
+
bottom: 0;
|
|
137
|
+
width: 1px;
|
|
138
|
+
height: 1px;
|
|
139
|
+
padding: 0;
|
|
140
|
+
border: 0;
|
|
141
|
+
opacity: 0;
|
|
142
|
+
pointer-events: none;
|
|
143
|
+
}
|
|
144
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
/** Accessible name, e.g. the property name. */
|
|
3
|
+
label: string;
|
|
4
|
+
/** `YYYY-MM-DD`, or empty for no date. */
|
|
5
|
+
value?: string;
|
|
6
|
+
/** Colour past dates as overdue and today as due today. */
|
|
7
|
+
deadline?: boolean;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
/** Called with `YYYY-MM-DD`, or an empty string when cleared. */
|
|
10
|
+
onchange: (value: string) => void;
|
|
11
|
+
class?: string;
|
|
12
|
+
};
|
|
13
|
+
declare const DateField: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
14
|
+
type DateField = ReturnType<typeof DateField>;
|
|
15
|
+
export default DateField;
|
|
@@ -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,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
import NavIcon from './NavIcon.svelte';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
label,
|
|
8
|
+
icon,
|
|
9
|
+
class: className = '',
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
label: string;
|
|
13
|
+
/** NavIcon name hinting at the value's kind, e.g. `calendar`. */
|
|
14
|
+
icon?: string;
|
|
15
|
+
class?: string;
|
|
16
|
+
/** One value control, rendered with its own label hidden. */
|
|
17
|
+
children?: Snippet;
|
|
18
|
+
} = $props();
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<div class={cn('terminoir-property-row', className)}>
|
|
22
|
+
<span class="terminoir-property-row__label" title={label}>
|
|
23
|
+
{#if icon}<NavIcon name={icon} />{/if}
|
|
24
|
+
<span class="terminoir-property-row__text">{label}</span>
|
|
25
|
+
</span>
|
|
26
|
+
<div class="terminoir-property-row__value">{@render children?.()}</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<style>
|
|
30
|
+
.terminoir-property-row {
|
|
31
|
+
display: grid;
|
|
32
|
+
grid-template-columns: minmax(6.5rem, 9.5rem) minmax(0, 1fr);
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: var(--space-3);
|
|
35
|
+
min-height: 2.25rem;
|
|
36
|
+
padding-inline: var(--space-2);
|
|
37
|
+
margin-inline: calc(-1 * var(--space-2));
|
|
38
|
+
border-radius: var(--radius-control);
|
|
39
|
+
transition: background 120ms ease;
|
|
40
|
+
}
|
|
41
|
+
.terminoir-property-row:hover,
|
|
42
|
+
.terminoir-property-row:focus-within {
|
|
43
|
+
background: color-mix(in srgb, var(--foreground) 4%, transparent);
|
|
44
|
+
}
|
|
45
|
+
.terminoir-property-row__label {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: var(--space-2);
|
|
49
|
+
min-width: 0;
|
|
50
|
+
font-size: 0.8125rem;
|
|
51
|
+
color: var(--muted-foreground);
|
|
52
|
+
}
|
|
53
|
+
.terminoir-property-row__label :global(i) {
|
|
54
|
+
width: 1rem;
|
|
55
|
+
flex-shrink: 0;
|
|
56
|
+
text-align: center;
|
|
57
|
+
font-size: 0.75rem;
|
|
58
|
+
opacity: 0.75;
|
|
59
|
+
}
|
|
60
|
+
.terminoir-property-row__text {
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
text-overflow: ellipsis;
|
|
63
|
+
white-space: nowrap;
|
|
64
|
+
}
|
|
65
|
+
.terminoir-property-row__value {
|
|
66
|
+
min-width: 0;
|
|
67
|
+
}
|
|
68
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
label: string;
|
|
4
|
+
/** NavIcon name hinting at the value's kind, e.g. `calendar`. */
|
|
5
|
+
icon?: string;
|
|
6
|
+
class?: string;
|
|
7
|
+
/** One value control, rendered with its own label hidden. */
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
};
|
|
10
|
+
declare const PropertyRow: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type PropertyRow = ReturnType<typeof PropertyRow>;
|
|
12
|
+
export default PropertyRow;
|
|
@@ -136,6 +136,8 @@
|
|
|
136
136
|
</div>
|
|
137
137
|
|
|
138
138
|
<style>
|
|
139
|
+
/* In a row (e.g. after selected chips) the search box takes the remaining width. */
|
|
140
|
+
.terminoir-picker { flex: 1 1 8rem; min-width: 8rem; }
|
|
139
141
|
.terminoir-picker__anchor { position: relative; }
|
|
140
142
|
.terminoir-picker__list {
|
|
141
143
|
position: absolute;
|
|
@@ -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';
|
|
@@ -60,6 +65,11 @@ export { default as ListItem } from './components/ListItem.svelte';
|
|
|
60
65
|
export { default as DescriptionList } from './components/DescriptionList.svelte';
|
|
61
66
|
export type { DescriptionItem } from './utils/componentTypes.js';
|
|
62
67
|
export { default as FormRow } from './components/FormRow.svelte';
|
|
68
|
+
export { default as PropertyRow } from './components/PropertyRow.svelte';
|
|
69
|
+
export { default as ChipSelect } from './components/ChipSelect.svelte';
|
|
70
|
+
export type { ChipSelectGroup, ChipSelectOption } from './utils/componentTypes.js';
|
|
71
|
+
export { default as DateField } from './components/DateField.svelte';
|
|
72
|
+
export { dayLabel, dayUrgency, parseYmd, type DayUrgency } from './utils/dayLabel.js';
|
|
63
73
|
export { default as SearchPicker } from './components/SearchPicker.svelte';
|
|
64
74
|
export { default as FileButton } from './components/FileButton.svelte';
|
|
65
75
|
export { default as TitleInput } from './components/TitleInput.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';
|
|
@@ -57,7 +60,11 @@ export { default as List } from './components/List.svelte';
|
|
|
57
60
|
export { default as ListItem } from './components/ListItem.svelte';
|
|
58
61
|
export { default as DescriptionList } from './components/DescriptionList.svelte';
|
|
59
62
|
export { default as FormRow } from './components/FormRow.svelte';
|
|
63
|
+
export { default as PropertyRow } from './components/PropertyRow.svelte';
|
|
60
64
|
// Inputs
|
|
65
|
+
export { default as ChipSelect } from './components/ChipSelect.svelte';
|
|
66
|
+
export { default as DateField } from './components/DateField.svelte';
|
|
67
|
+
export { dayLabel, dayUrgency, parseYmd } from './utils/dayLabel.js';
|
|
61
68
|
export { default as SearchPicker } from './components/SearchPicker.svelte';
|
|
62
69
|
export { default as FileButton } from './components/FileButton.svelte';
|
|
63
70
|
export { default as TitleInput } from './components/TitleInput.svelte';
|
|
@@ -15,6 +15,18 @@ export type PickerOption = {
|
|
|
15
15
|
icon?: string;
|
|
16
16
|
iconColor?: string;
|
|
17
17
|
};
|
|
18
|
+
export type ChipSelectOption = {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
/** Dot colour (palette name or CSS colour); grey when unset. */
|
|
22
|
+
color?: string;
|
|
23
|
+
/** Id of the `ChipSelectGroup` heading it sits under. */
|
|
24
|
+
group?: string;
|
|
25
|
+
};
|
|
26
|
+
export type ChipSelectGroup = {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
};
|
|
18
30
|
export type RadioCardOption = {
|
|
19
31
|
value: string;
|
|
20
32
|
label: string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** How far a day is from today, for colouring deadlines. */
|
|
2
|
+
export type DayUrgency = 'past' | 'today' | 'future';
|
|
3
|
+
/** Local-midnight Date for `YYYY-MM-DD`, or null when the text is not a calendar date. */
|
|
4
|
+
export declare function parseYmd(ymd: string): Date | null;
|
|
5
|
+
/** "Today", "Tomorrow", "Yesterday", a weekday within the next week, else "Nov 12" (plus the year when it differs). */
|
|
6
|
+
export declare function dayLabel(date: Date, today?: Date): string;
|
|
7
|
+
export declare function dayUrgency(date: Date, today?: Date): DayUrgency;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { addDays, sameDay, startOfDay } from './calendar.js';
|
|
2
|
+
const DAYS_SHOWN_AS_WEEKDAY = 6;
|
|
3
|
+
const YMD = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
4
|
+
/** Local-midnight Date for `YYYY-MM-DD`, or null when the text is not a calendar date. */
|
|
5
|
+
export function parseYmd(ymd) {
|
|
6
|
+
const match = YMD.exec(ymd.trim());
|
|
7
|
+
if (!match)
|
|
8
|
+
return null;
|
|
9
|
+
const [, year, month, day] = match.map(Number);
|
|
10
|
+
const date = new Date(year, month - 1, day);
|
|
11
|
+
return date.getMonth() === month - 1 && date.getDate() === day ? date : null;
|
|
12
|
+
}
|
|
13
|
+
/** "Today", "Tomorrow", "Yesterday", a weekday within the next week, else "Nov 12" (plus the year when it differs). */
|
|
14
|
+
export function dayLabel(date, today = new Date()) {
|
|
15
|
+
const day = startOfDay(date);
|
|
16
|
+
const now = startOfDay(today);
|
|
17
|
+
if (sameDay(day, now))
|
|
18
|
+
return 'Today';
|
|
19
|
+
if (sameDay(day, addDays(now, 1)))
|
|
20
|
+
return 'Tomorrow';
|
|
21
|
+
if (sameDay(day, addDays(now, -1)))
|
|
22
|
+
return 'Yesterday';
|
|
23
|
+
if (day > now && day <= addDays(now, DAYS_SHOWN_AS_WEEKDAY)) {
|
|
24
|
+
return day.toLocaleDateString(undefined, { weekday: 'long' });
|
|
25
|
+
}
|
|
26
|
+
const withYear = day.getFullYear() !== now.getFullYear();
|
|
27
|
+
return day.toLocaleDateString(undefined, { month: 'short', day: 'numeric', ...(withYear && { year: 'numeric' }) });
|
|
28
|
+
}
|
|
29
|
+
export function dayUrgency(date, today = new Date()) {
|
|
30
|
+
const day = startOfDay(date);
|
|
31
|
+
const now = startOfDay(today);
|
|
32
|
+
if (sameDay(day, now))
|
|
33
|
+
return 'today';
|
|
34
|
+
return day < now ? 'past' : 'future';
|
|
35
|
+
}
|
|
@@ -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
|
+
}
|