terminoir 0.4.0 → 0.5.1
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 +38 -28
- package/css/components.css +62 -0
- package/css/shell.css +32 -1
- package/dist/components/AccentSwatches.svelte +1 -1
- package/dist/components/AppShell.svelte +33 -5
- package/dist/components/AppShell.svelte.d.ts +10 -1
- package/dist/components/AppShellNavLink.svelte +2 -4
- package/dist/components/Button.svelte +32 -14
- package/dist/components/Button.svelte.d.ts +2 -0
- package/dist/components/CalendarGrid.svelte +260 -0
- package/dist/components/CalendarGrid.svelte.d.ts +12 -0
- package/dist/components/CanvasStage.svelte +81 -0
- package/dist/components/CanvasStage.svelte.d.ts +19 -0
- package/dist/components/Centered.svelte +36 -0
- package/dist/components/Centered.svelte.d.ts +10 -0
- package/dist/components/Checkbox.svelte +64 -12
- package/dist/components/Checkbox.svelte.d.ts +5 -0
- package/dist/components/Chip.svelte +90 -2
- package/dist/components/Chip.svelte.d.ts +8 -0
- package/dist/components/CommandPalette.svelte +178 -97
- package/dist/components/CommandPalette.svelte.d.ts +15 -8
- package/dist/components/ContextMenu.svelte +68 -0
- package/dist/components/ContextMenu.svelte.d.ts +12 -0
- package/dist/components/DataTable.svelte +139 -0
- package/dist/components/DataTable.svelte.d.ts +39 -0
- package/dist/components/DescriptionList.svelte +41 -0
- package/dist/components/DescriptionList.svelte.d.ts +8 -0
- package/dist/components/Dialog.svelte +17 -8
- package/dist/components/Disclosure.svelte +55 -0
- package/dist/components/Disclosure.svelte.d.ts +10 -0
- package/dist/components/DocumentViewer.svelte +105 -0
- package/dist/components/DocumentViewer.svelte.d.ts +23 -0
- package/dist/components/Drawer.svelte +119 -0
- package/dist/components/Drawer.svelte.d.ts +16 -0
- package/dist/components/DropdownMenu.svelte +46 -62
- package/dist/components/DropdownMenu.svelte.d.ts +7 -6
- package/dist/components/FieldMessage.svelte +9 -0
- package/dist/components/FieldMessage.svelte.d.ts +8 -0
- package/dist/components/FileButton.svelte +44 -0
- package/dist/components/FileButton.svelte.d.ts +16 -0
- package/dist/components/FormRow.svelte +62 -0
- package/dist/components/FormRow.svelte.d.ts +13 -0
- package/dist/components/IconField.svelte +80 -5
- package/dist/components/IconField.svelte.d.ts +2 -0
- package/dist/components/Inline.svelte +52 -0
- package/dist/components/Inline.svelte.d.ts +16 -0
- package/dist/components/Input.svelte +23 -3
- package/dist/components/Input.svelte.d.ts +9 -0
- package/dist/components/List.svelte +42 -0
- package/dist/components/List.svelte.d.ts +13 -0
- package/dist/components/ListItem.svelte +114 -0
- package/dist/components/ListItem.svelte.d.ts +17 -0
- package/dist/components/MenuItems.svelte +44 -0
- package/dist/components/MenuItems.svelte.d.ts +8 -0
- package/dist/components/NavSection.svelte +45 -0
- package/dist/components/NavSection.svelte.d.ts +12 -0
- package/dist/components/Page.svelte +53 -0
- package/dist/components/Page.svelte.d.ts +12 -0
- package/dist/components/PageHeader.svelte +91 -0
- package/dist/components/PageHeader.svelte.d.ts +15 -0
- package/dist/components/Prose.svelte +33 -0
- package/dist/components/Prose.svelte.d.ts +8 -0
- package/dist/components/RadioCards.svelte +68 -0
- package/dist/components/RadioCards.svelte.d.ts +11 -0
- package/dist/components/SearchPicker.svelte +171 -0
- package/dist/components/SearchPicker.svelte.d.ts +19 -0
- package/dist/components/Section.svelte +71 -0
- package/dist/components/Section.svelte.d.ts +12 -0
- package/dist/components/SegmentedControl.svelte +63 -0
- package/dist/components/SegmentedControl.svelte.d.ts +12 -0
- package/dist/components/Select.svelte +22 -4
- package/dist/components/Select.svelte.d.ts +9 -1
- package/dist/components/Spacer.svelte +6 -0
- package/dist/components/Spacer.svelte.d.ts +26 -0
- package/dist/components/Stack.svelte +37 -0
- package/dist/components/Stack.svelte.d.ts +12 -0
- package/dist/components/Switch.svelte +6 -2
- package/dist/components/Switch.svelte.d.ts +2 -0
- package/dist/components/Tabs.svelte +6 -4
- package/dist/components/Text.svelte +68 -0
- package/dist/components/Text.svelte.d.ts +15 -0
- package/dist/components/Textarea.svelte +19 -3
- package/dist/components/Textarea.svelte.d.ts +6 -0
- package/dist/components/TitleInput.svelte +62 -0
- package/dist/components/TitleInput.svelte.d.ts +10 -0
- package/dist/index.d.ts +35 -2
- package/dist/index.js +31 -0
- package/dist/utils/calendar.d.ts +48 -0
- package/dist/utils/calendar.js +105 -0
- package/dist/utils/commands.d.ts +22 -0
- package/dist/utils/commands.js +17 -0
- package/dist/utils/componentTypes.d.ts +41 -0
- package/dist/utils/componentTypes.js +1 -0
- package/dist/utils/listNav.d.ts +2 -0
- package/dist/utils/listNav.js +8 -0
- package/dist/utils/menu.d.ts +10 -0
- package/dist/utils/menu.js +1 -0
- package/package.json +5 -3
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils/cn.js';
|
|
3
|
+
import {
|
|
4
|
+
addDays,
|
|
5
|
+
calendarRange,
|
|
6
|
+
eventsOn,
|
|
7
|
+
layoutDay,
|
|
8
|
+
monthGrid,
|
|
9
|
+
sameDay,
|
|
10
|
+
startOfWeek,
|
|
11
|
+
type CalendarEvent,
|
|
12
|
+
type CalendarMode,
|
|
13
|
+
} from '../utils/calendar.js';
|
|
14
|
+
|
|
15
|
+
const HOURS = Array.from({ length: 24 }, (_, h) => h);
|
|
16
|
+
const MONTH_CELL_LIMIT = 3;
|
|
17
|
+
const SCROLL_TO_HOUR = 7;
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
mode = 'week',
|
|
21
|
+
anchor,
|
|
22
|
+
events,
|
|
23
|
+
onEventClick,
|
|
24
|
+
class: className = '',
|
|
25
|
+
}: {
|
|
26
|
+
mode?: CalendarMode;
|
|
27
|
+
/** Any date inside the week or month to show. */
|
|
28
|
+
anchor: Date;
|
|
29
|
+
events: CalendarEvent[];
|
|
30
|
+
onEventClick?: (event: CalendarEvent) => void;
|
|
31
|
+
class?: string;
|
|
32
|
+
} = $props();
|
|
33
|
+
|
|
34
|
+
const today = new Date();
|
|
35
|
+
const range = $derived(calendarRange(anchor, mode));
|
|
36
|
+
/** Seven days from the Monday that starts the visible week (or the first row of the month). */
|
|
37
|
+
const weekDays = $derived(Array.from({ length: 7 }, (_, i) => addDays(startOfWeek(range.start), i)));
|
|
38
|
+
const monthDays = $derived(monthGrid(range.start));
|
|
39
|
+
|
|
40
|
+
let scroller: HTMLDivElement | undefined = $state();
|
|
41
|
+
|
|
42
|
+
$effect(() => {
|
|
43
|
+
if (mode === 'week' && scroller) scroller.scrollTop = (scroller.scrollHeight / 24) * SCROLL_TO_HOUR;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function time(d: Date): string {
|
|
47
|
+
return d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hourLabel(h: number): string {
|
|
51
|
+
return new Date(2000, 0, 1, h).toLocaleTimeString(undefined, { hour: 'numeric' });
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
{#snippet chip(event: CalendarEvent, showTime: boolean, compact = false)}
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
class="terminoir-cal__event terminoir-focusable"
|
|
59
|
+
class:terminoir-cal__event--muted={event.muted}
|
|
60
|
+
class:terminoir-cal__event--compact={compact}
|
|
61
|
+
style:--event-color={event.color}
|
|
62
|
+
title={event.title}
|
|
63
|
+
disabled={!onEventClick}
|
|
64
|
+
onclick={() => onEventClick?.(event)}
|
|
65
|
+
>
|
|
66
|
+
{#if showTime && !event.allDay}<span class="terminoir-cal__time">{time(event.start)}</span>{/if}
|
|
67
|
+
<span class="terminoir-cal__title">{event.title}</span>
|
|
68
|
+
</button>
|
|
69
|
+
{/snippet}
|
|
70
|
+
|
|
71
|
+
{#if mode === 'week'}
|
|
72
|
+
<div class={cn('terminoir-cal terminoir-cal--week', className)}>
|
|
73
|
+
<div class="terminoir-cal__week-head">
|
|
74
|
+
<div></div>
|
|
75
|
+
{#each weekDays as day (day.getTime())}
|
|
76
|
+
<div class="terminoir-cal__day-head" class:terminoir-cal__day-head--today={sameDay(day, today)}>
|
|
77
|
+
<span>{day.toLocaleDateString(undefined, { weekday: 'short' })}</span>
|
|
78
|
+
<strong>{day.getDate()}</strong>
|
|
79
|
+
</div>
|
|
80
|
+
{/each}
|
|
81
|
+
<div class="terminoir-cal__gutter-label">all-day</div>
|
|
82
|
+
{#each weekDays as day (day.getTime())}
|
|
83
|
+
<div class="terminoir-cal__allday">
|
|
84
|
+
{#each eventsOn(events, day).filter((e) => e.allDay) as event (event.id)}{@render chip(event, false)}{/each}
|
|
85
|
+
</div>
|
|
86
|
+
{/each}
|
|
87
|
+
</div>
|
|
88
|
+
<div class="terminoir-cal__scroller" bind:this={scroller}>
|
|
89
|
+
<div class="terminoir-cal__week-body">
|
|
90
|
+
<div class="terminoir-cal__gutter">
|
|
91
|
+
{#each HOURS as h (h)}<div class="terminoir-cal__hour-label">{h === 0 ? '' : hourLabel(h)}</div>{/each}
|
|
92
|
+
</div>
|
|
93
|
+
{#each weekDays as day (day.getTime())}
|
|
94
|
+
<div class="terminoir-cal__day-col" class:terminoir-cal__day-col--today={sameDay(day, today)}>
|
|
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)}
|
|
104
|
+
</div>
|
|
105
|
+
{/each}
|
|
106
|
+
</div>
|
|
107
|
+
{/each}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
{:else}
|
|
112
|
+
<div class={cn('terminoir-cal terminoir-cal--month', className)}>
|
|
113
|
+
{#each weekDays as day (day.getTime())}
|
|
114
|
+
<div class="terminoir-cal__dow">{day.toLocaleDateString(undefined, { weekday: 'short' })}</div>
|
|
115
|
+
{/each}
|
|
116
|
+
{#each monthDays as day (day.getTime())}
|
|
117
|
+
{@const dayEvents = eventsOn(events, day)}
|
|
118
|
+
<div class="terminoir-cal__cell" class:terminoir-cal__cell--outside={day.getMonth() !== range.start.getMonth()}>
|
|
119
|
+
<span class="terminoir-cal__daynum" class:terminoir-cal__daynum--today={sameDay(day, today)}>{day.getDate()}</span>
|
|
120
|
+
{#each dayEvents.slice(0, MONTH_CELL_LIMIT) as event (event.id)}{@render chip(event, true)}{/each}
|
|
121
|
+
{#if dayEvents.length > MONTH_CELL_LIMIT}
|
|
122
|
+
<span class="terminoir-cal__more">+{dayEvents.length - MONTH_CELL_LIMIT} more</span>
|
|
123
|
+
{/if}
|
|
124
|
+
</div>
|
|
125
|
+
{/each}
|
|
126
|
+
</div>
|
|
127
|
+
{/if}
|
|
128
|
+
|
|
129
|
+
<style>
|
|
130
|
+
.terminoir-cal {
|
|
131
|
+
--cal-line: color-mix(in srgb, var(--border) 70%, transparent);
|
|
132
|
+
--cal-hour: 3rem;
|
|
133
|
+
font-size: 0.8125rem;
|
|
134
|
+
border: 1px solid var(--border);
|
|
135
|
+
border-radius: var(--radius);
|
|
136
|
+
overflow: hidden;
|
|
137
|
+
min-width: 0;
|
|
138
|
+
}
|
|
139
|
+
/* Fixed viewport-relative height so the hours scroll inside the grid (it opens near 7am). */
|
|
140
|
+
.terminoir-cal--week { display: flex; flex-direction: column; height: min(72dvh, 52rem); min-height: 24rem; }
|
|
141
|
+
.terminoir-cal__week-head,
|
|
142
|
+
.terminoir-cal__week-body {
|
|
143
|
+
display: grid;
|
|
144
|
+
grid-template-columns: 3.5rem repeat(7, minmax(0, 1fr));
|
|
145
|
+
}
|
|
146
|
+
.terminoir-cal__week-head { border-bottom: 1px solid var(--border); }
|
|
147
|
+
.terminoir-cal__day-head {
|
|
148
|
+
display: flex;
|
|
149
|
+
align-items: baseline;
|
|
150
|
+
justify-content: center;
|
|
151
|
+
gap: var(--space-1);
|
|
152
|
+
padding: var(--space-2) 0;
|
|
153
|
+
color: var(--muted-foreground);
|
|
154
|
+
}
|
|
155
|
+
.terminoir-cal__day-head strong { font-size: 1rem; color: var(--foreground); font-weight: 600; }
|
|
156
|
+
.terminoir-cal__day-head--today strong { color: var(--primary); }
|
|
157
|
+
.terminoir-cal__gutter-label {
|
|
158
|
+
font-size: 0.6875rem;
|
|
159
|
+
color: var(--muted-foreground);
|
|
160
|
+
text-align: right;
|
|
161
|
+
padding: var(--space-1) var(--space-2);
|
|
162
|
+
}
|
|
163
|
+
.terminoir-cal__allday {
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-direction: column;
|
|
166
|
+
gap: 2px;
|
|
167
|
+
padding: 2px;
|
|
168
|
+
min-height: 1.75rem;
|
|
169
|
+
border-left: 1px solid var(--cal-line);
|
|
170
|
+
}
|
|
171
|
+
.terminoir-cal__scroller { flex: 1; overflow-y: auto; min-height: 0; }
|
|
172
|
+
.terminoir-cal__week-body { height: calc(var(--cal-hour) * 24); }
|
|
173
|
+
.terminoir-cal__gutter { display: grid; grid-template-rows: repeat(24, var(--cal-hour)); }
|
|
174
|
+
.terminoir-cal__hour-label {
|
|
175
|
+
font-size: 0.6875rem;
|
|
176
|
+
color: var(--muted-foreground);
|
|
177
|
+
text-align: right;
|
|
178
|
+
padding-right: var(--space-2);
|
|
179
|
+
transform: translateY(-0.5em);
|
|
180
|
+
}
|
|
181
|
+
.terminoir-cal__day-col {
|
|
182
|
+
position: relative;
|
|
183
|
+
border-left: 1px solid var(--cal-line);
|
|
184
|
+
background-image: linear-gradient(var(--cal-line) 1px, transparent 1px);
|
|
185
|
+
background-size: 100% var(--cal-hour);
|
|
186
|
+
}
|
|
187
|
+
.terminoir-cal__day-col--today { background-color: color-mix(in srgb, var(--primary) 4%, transparent); }
|
|
188
|
+
.terminoir-cal__timed { position: absolute; display: flex; }
|
|
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
|
+
}
|
|
199
|
+
.terminoir-cal__event {
|
|
200
|
+
--event-color: var(--primary);
|
|
201
|
+
display: flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
gap: var(--space-1);
|
|
204
|
+
width: 100%;
|
|
205
|
+
min-width: 0;
|
|
206
|
+
padding: 0.15rem 0.35rem;
|
|
207
|
+
border: 0;
|
|
208
|
+
border-left: 3px solid var(--event-color);
|
|
209
|
+
border-radius: calc(var(--radius-control) - 0.3rem);
|
|
210
|
+
background: color-mix(in srgb, var(--event-color) 16%, var(--background));
|
|
211
|
+
color: var(--foreground);
|
|
212
|
+
font: inherit;
|
|
213
|
+
font-size: 0.75rem;
|
|
214
|
+
text-align: left;
|
|
215
|
+
overflow: hidden;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
}
|
|
218
|
+
.terminoir-cal__event:disabled { cursor: default; }
|
|
219
|
+
.terminoir-cal__event--muted { opacity: 0.6; background: transparent; }
|
|
220
|
+
.terminoir-cal__time { color: var(--muted-foreground); flex-shrink: 0; }
|
|
221
|
+
.terminoir-cal__title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
222
|
+
.terminoir-cal--month { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
|
|
223
|
+
.terminoir-cal__dow {
|
|
224
|
+
padding: var(--space-2);
|
|
225
|
+
font-size: 0.6875rem;
|
|
226
|
+
font-weight: 600;
|
|
227
|
+
letter-spacing: 0.06em;
|
|
228
|
+
text-transform: uppercase;
|
|
229
|
+
color: var(--muted-foreground);
|
|
230
|
+
border-bottom: 1px solid var(--border);
|
|
231
|
+
}
|
|
232
|
+
.terminoir-cal__cell {
|
|
233
|
+
display: flex;
|
|
234
|
+
flex-direction: column;
|
|
235
|
+
gap: 2px;
|
|
236
|
+
min-height: 6rem;
|
|
237
|
+
padding: var(--space-1);
|
|
238
|
+
border-top: 1px solid var(--cal-line);
|
|
239
|
+
border-left: 1px solid var(--cal-line);
|
|
240
|
+
min-width: 0;
|
|
241
|
+
}
|
|
242
|
+
.terminoir-cal__cell:nth-child(7n + 1) { border-left: 0; }
|
|
243
|
+
.terminoir-cal__cell--outside { background: color-mix(in srgb, var(--foreground) 2%, transparent); }
|
|
244
|
+
.terminoir-cal__cell--outside .terminoir-cal__daynum { color: var(--muted-foreground); }
|
|
245
|
+
.terminoir-cal__daynum {
|
|
246
|
+
align-self: flex-end;
|
|
247
|
+
min-width: 1.5rem;
|
|
248
|
+
padding: 0 0.3rem;
|
|
249
|
+
border-radius: 999px;
|
|
250
|
+
font-size: 0.75rem;
|
|
251
|
+
text-align: center;
|
|
252
|
+
}
|
|
253
|
+
.terminoir-cal__daynum--today { background: var(--primary); color: var(--primary-foreground); }
|
|
254
|
+
.terminoir-cal__more { font-size: 0.6875rem; color: var(--muted-foreground); padding-left: 0.35rem; }
|
|
255
|
+
@media (max-width: 700px) {
|
|
256
|
+
.terminoir-cal__day-head span { display: none; }
|
|
257
|
+
.terminoir-cal__cell { min-height: 4rem; }
|
|
258
|
+
.terminoir-cal__cell .terminoir-cal__event .terminoir-cal__time { display: none; }
|
|
259
|
+
}
|
|
260
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CalendarEvent, type CalendarMode } from '../utils/calendar.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
mode?: CalendarMode;
|
|
4
|
+
/** Any date inside the week or month to show. */
|
|
5
|
+
anchor: Date;
|
|
6
|
+
events: CalendarEvent[];
|
|
7
|
+
onEventClick?: (event: CalendarEvent) => void;
|
|
8
|
+
class?: string;
|
|
9
|
+
};
|
|
10
|
+
declare const CalendarGrid: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type CalendarGrid = ReturnType<typeof CalendarGrid>;
|
|
12
|
+
export default CalendarGrid;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
stage = $bindable(),
|
|
7
|
+
host = $bindable(),
|
|
8
|
+
label,
|
|
9
|
+
class: className = '',
|
|
10
|
+
overlay,
|
|
11
|
+
panel,
|
|
12
|
+
children,
|
|
13
|
+
}: {
|
|
14
|
+
/** The full-size stage element (observe it for size changes). */
|
|
15
|
+
stage?: HTMLDivElement;
|
|
16
|
+
/** Empty full-size element for a canvas library to mount into. */
|
|
17
|
+
host?: HTMLDivElement;
|
|
18
|
+
/** Accessible description of the canvas. */
|
|
19
|
+
label: string;
|
|
20
|
+
class?: string;
|
|
21
|
+
/** Small floating card, top right (filters, legend, counts). */
|
|
22
|
+
overlay?: Snippet;
|
|
23
|
+
/** Detail panel on the right; a bottom sheet on phones. */
|
|
24
|
+
panel?: Snippet;
|
|
25
|
+
/** Content drawn over the canvas instead of a library (e.g. an error). */
|
|
26
|
+
children?: Snippet;
|
|
27
|
+
} = $props();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div bind:this={stage} class={cn('terminoir-canvas-stage', className)}>
|
|
31
|
+
<div bind:this={host} class="terminoir-canvas-stage__host" role="img" aria-label={label}></div>
|
|
32
|
+
{@render children?.()}
|
|
33
|
+
{#if overlay}
|
|
34
|
+
<div class="terminoir-canvas-stage__overlay glass-popover">{@render overlay()}</div>
|
|
35
|
+
{/if}
|
|
36
|
+
{#if panel}
|
|
37
|
+
<aside class="terminoir-canvas-stage__panel glass-popover">{@render panel()}</aside>
|
|
38
|
+
{/if}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<style>
|
|
42
|
+
.terminoir-canvas-stage {
|
|
43
|
+
position: relative;
|
|
44
|
+
width: 100%;
|
|
45
|
+
height: 100%;
|
|
46
|
+
min-height: 20rem;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
background: var(--background);
|
|
49
|
+
}
|
|
50
|
+
.terminoir-canvas-stage__host { position: absolute; inset: 0; }
|
|
51
|
+
.terminoir-canvas-stage__host :global(canvas) { display: block; }
|
|
52
|
+
.terminoir-canvas-stage__overlay,
|
|
53
|
+
.terminoir-canvas-stage__panel {
|
|
54
|
+
position: absolute;
|
|
55
|
+
z-index: 4;
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
gap: var(--space-3);
|
|
59
|
+
padding: var(--space-4);
|
|
60
|
+
}
|
|
61
|
+
.terminoir-canvas-stage__overlay {
|
|
62
|
+
top: var(--space-3);
|
|
63
|
+
right: var(--space-3);
|
|
64
|
+
width: min(17rem, calc(100% - 2 * var(--space-3)));
|
|
65
|
+
}
|
|
66
|
+
.terminoir-canvas-stage__panel {
|
|
67
|
+
top: var(--space-3);
|
|
68
|
+
right: var(--space-3);
|
|
69
|
+
bottom: var(--space-3);
|
|
70
|
+
width: min(22rem, calc(100% - 2 * var(--space-3)));
|
|
71
|
+
overflow-y: auto;
|
|
72
|
+
}
|
|
73
|
+
@media (max-width: 700px) {
|
|
74
|
+
.terminoir-canvas-stage__panel {
|
|
75
|
+
top: auto;
|
|
76
|
+
left: var(--space-3);
|
|
77
|
+
width: auto;
|
|
78
|
+
max-height: 55%;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** The full-size stage element (observe it for size changes). */
|
|
4
|
+
stage?: HTMLDivElement;
|
|
5
|
+
/** Empty full-size element for a canvas library to mount into. */
|
|
6
|
+
host?: HTMLDivElement;
|
|
7
|
+
/** Accessible description of the canvas. */
|
|
8
|
+
label: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
/** Small floating card, top right (filters, legend, counts). */
|
|
11
|
+
overlay?: Snippet;
|
|
12
|
+
/** Detail panel on the right; a bottom sheet on phones. */
|
|
13
|
+
panel?: Snippet;
|
|
14
|
+
/** Content drawn over the canvas instead of a library (e.g. an error). */
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
};
|
|
17
|
+
declare const CanvasStage: import("svelte").Component<$$ComponentProps, {}, "stage" | "host">;
|
|
18
|
+
type CanvasStage = ReturnType<typeof CanvasStage>;
|
|
19
|
+
export default CanvasStage;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
width = '24rem',
|
|
7
|
+
class: className = '',
|
|
8
|
+
children,
|
|
9
|
+
}: {
|
|
10
|
+
/** Max width of the centred column. */
|
|
11
|
+
width?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
children?: Snippet;
|
|
14
|
+
} = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<!-- Full-viewport centred column: sign-in, loading and error screens. -->
|
|
18
|
+
<div class={cn('terminoir-centered', className)}>
|
|
19
|
+
<div class="terminoir-centered__column" style:max-width={width}>{@render children?.()}</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<style>
|
|
23
|
+
.terminoir-centered {
|
|
24
|
+
min-height: 100dvh;
|
|
25
|
+
display: grid;
|
|
26
|
+
place-items: center;
|
|
27
|
+
padding: var(--space-6) var(--space-4);
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
}
|
|
30
|
+
.terminoir-centered__column {
|
|
31
|
+
width: 100%;
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: var(--space-4);
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** Max width of the centred column. */
|
|
4
|
+
width?: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
children?: Snippet;
|
|
7
|
+
};
|
|
8
|
+
declare const Centered: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type Centered = ReturnType<typeof Centered>;
|
|
10
|
+
export default Centered;
|
|
@@ -3,30 +3,82 @@
|
|
|
3
3
|
|
|
4
4
|
let {
|
|
5
5
|
label,
|
|
6
|
+
hideLabel = false,
|
|
7
|
+
hint,
|
|
6
8
|
checked = $bindable(false),
|
|
9
|
+
strikeWhenChecked = false,
|
|
7
10
|
disabled = false,
|
|
8
11
|
class: className = '',
|
|
9
12
|
id,
|
|
10
13
|
...rest
|
|
11
14
|
}: {
|
|
12
15
|
label: string;
|
|
16
|
+
/** Keep the label for screen readers only. */
|
|
17
|
+
hideLabel?: boolean;
|
|
18
|
+
hint?: string;
|
|
13
19
|
checked?: boolean;
|
|
20
|
+
/** Strike through the label when checked (task lists). */
|
|
21
|
+
strikeWhenChecked?: boolean;
|
|
14
22
|
disabled?: boolean;
|
|
15
23
|
class?: string;
|
|
16
24
|
id?: string;
|
|
17
25
|
} & Record<string, unknown> = $props();
|
|
18
26
|
|
|
19
|
-
const
|
|
27
|
+
const fallbackId = $props.id();
|
|
28
|
+
const inputId = $derived(id ?? fallbackId);
|
|
20
29
|
</script>
|
|
21
30
|
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
<div class={cn('terminoir-checkbox', className)}>
|
|
32
|
+
<label class="terminoir-checkbox__row" for={inputId}>
|
|
33
|
+
<input
|
|
34
|
+
id={inputId}
|
|
35
|
+
type="checkbox"
|
|
36
|
+
class="terminoir-checkbox__input terminoir-focusable"
|
|
37
|
+
bind:checked
|
|
38
|
+
{disabled}
|
|
39
|
+
{...rest}
|
|
40
|
+
/>
|
|
41
|
+
<span
|
|
42
|
+
class={cn(
|
|
43
|
+
'terminoir-checkbox__label',
|
|
44
|
+
hideLabel && 'terminoir-sr-only',
|
|
45
|
+
strikeWhenChecked && checked && 'terminoir-checkbox__label--done',
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
{label}
|
|
49
|
+
</span>
|
|
50
|
+
</label>
|
|
51
|
+
{#if hint}<p class="terminoir-field__hint terminoir-checkbox__hint">{hint}</p>{/if}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<style>
|
|
55
|
+
.terminoir-checkbox {
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
gap: var(--space-1);
|
|
59
|
+
min-width: 0;
|
|
60
|
+
}
|
|
61
|
+
.terminoir-checkbox__row {
|
|
62
|
+
display: inline-flex;
|
|
63
|
+
align-items: flex-start;
|
|
64
|
+
gap: var(--space-2);
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
font-size: 0.875rem;
|
|
67
|
+
line-height: 1.4;
|
|
68
|
+
}
|
|
69
|
+
.terminoir-checkbox__input {
|
|
70
|
+
margin: 0.15rem 0 0;
|
|
71
|
+
accent-color: var(--primary);
|
|
72
|
+
flex-shrink: 0;
|
|
73
|
+
}
|
|
74
|
+
.terminoir-checkbox__label {
|
|
75
|
+
color: var(--foreground);
|
|
76
|
+
min-width: 0;
|
|
77
|
+
overflow-wrap: anywhere;
|
|
78
|
+
}
|
|
79
|
+
.terminoir-checkbox__label--done {
|
|
80
|
+
color: var(--muted-foreground);
|
|
81
|
+
text-decoration: line-through;
|
|
82
|
+
}
|
|
83
|
+
.terminoir-checkbox__hint { padding-left: 1.5rem; }
|
|
84
|
+
</style>
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
type $$ComponentProps = {
|
|
2
2
|
label: string;
|
|
3
|
+
/** Keep the label for screen readers only. */
|
|
4
|
+
hideLabel?: boolean;
|
|
5
|
+
hint?: string;
|
|
3
6
|
checked?: boolean;
|
|
7
|
+
/** Strike through the label when checked (task lists). */
|
|
8
|
+
strikeWhenChecked?: boolean;
|
|
4
9
|
disabled?: boolean;
|
|
5
10
|
class?: string;
|
|
6
11
|
id?: string;
|
|
@@ -5,19 +5,34 @@
|
|
|
5
5
|
|
|
6
6
|
let {
|
|
7
7
|
tone = 'default',
|
|
8
|
+
color,
|
|
9
|
+
onremove,
|
|
10
|
+
removeLabel = 'Remove',
|
|
11
|
+
onclick,
|
|
8
12
|
class: className = '',
|
|
9
13
|
children,
|
|
10
14
|
...rest
|
|
11
15
|
}: {
|
|
12
16
|
tone?: ChipTone;
|
|
17
|
+
/** Dot colour before the text (palette name or CSS colour), e.g. a select option colour. */
|
|
18
|
+
color?: string;
|
|
19
|
+
/** Shows a × button; called when it is pressed. */
|
|
20
|
+
onremove?: () => void;
|
|
21
|
+
/** Accessible name of the × button, e.g. "Remove Rosa". */
|
|
22
|
+
removeLabel?: string;
|
|
23
|
+
/** Makes the chip text a button (e.g. open the linked item). */
|
|
24
|
+
onclick?: (event: MouseEvent) => void;
|
|
13
25
|
class?: string;
|
|
14
26
|
children?: import('svelte').Snippet;
|
|
15
27
|
} & Record<string, unknown> = $props();
|
|
28
|
+
|
|
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);
|
|
16
31
|
</script>
|
|
17
32
|
|
|
18
33
|
<span
|
|
19
34
|
class={cn(
|
|
20
|
-
'terminoir-card__chip
|
|
35
|
+
'terminoir-card__chip terminoir-chip',
|
|
21
36
|
tone === 'overdue' && 'terminoir-card__chip--overdue',
|
|
22
37
|
tone === 'today' && 'terminoir-card__chip--today',
|
|
23
38
|
tone === 'done' && 'terminoir-card__chip--done',
|
|
@@ -25,5 +40,78 @@
|
|
|
25
40
|
)}
|
|
26
41
|
{...rest}
|
|
27
42
|
>
|
|
28
|
-
{
|
|
43
|
+
{#if dot}<span class="terminoir-chip__dot" style:background={dot} aria-hidden="true"></span>{/if}
|
|
44
|
+
{#if onclick}
|
|
45
|
+
<button type="button" class="terminoir-chip__label terminoir-focusable" {onclick}>{@render children?.()}</button>
|
|
46
|
+
{:else}
|
|
47
|
+
<span class="terminoir-chip__label">{@render children?.()}</span>
|
|
48
|
+
{/if}
|
|
49
|
+
{#if onremove}
|
|
50
|
+
<button type="button" class="terminoir-chip__remove terminoir-focusable" aria-label={removeLabel} onclick={onremove}>×</button>
|
|
51
|
+
{/if}
|
|
29
52
|
</span>
|
|
53
|
+
|
|
54
|
+
<style>
|
|
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
|
+
display: inline-flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 0.35rem;
|
|
68
|
+
max-width: 100%;
|
|
69
|
+
min-width: 0;
|
|
70
|
+
line-height: 1.35;
|
|
71
|
+
}
|
|
72
|
+
.terminoir-chip:not(.terminoir-card__chip--overdue):not(.terminoir-card__chip--today):not(.terminoir-card__chip--done) {
|
|
73
|
+
background: color-mix(in srgb, var(--foreground) 7%, transparent);
|
|
74
|
+
border-color: color-mix(in srgb, var(--foreground) 10%, transparent);
|
|
75
|
+
}
|
|
76
|
+
.terminoir-chip__dot {
|
|
77
|
+
width: 0.45rem;
|
|
78
|
+
height: 0.45rem;
|
|
79
|
+
border-radius: 999px;
|
|
80
|
+
flex-shrink: 0;
|
|
81
|
+
}
|
|
82
|
+
.terminoir-chip__label {
|
|
83
|
+
min-width: 0;
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
text-overflow: ellipsis;
|
|
86
|
+
white-space: nowrap;
|
|
87
|
+
}
|
|
88
|
+
button.terminoir-chip__label {
|
|
89
|
+
border: 0;
|
|
90
|
+
padding: 0;
|
|
91
|
+
background: none;
|
|
92
|
+
color: inherit;
|
|
93
|
+
font: inherit;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
}
|
|
96
|
+
button.terminoir-chip__label:hover { text-decoration: underline; }
|
|
97
|
+
.terminoir-chip__remove {
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
width: 1rem;
|
|
102
|
+
height: 1rem;
|
|
103
|
+
margin-right: -0.2rem;
|
|
104
|
+
padding: 0;
|
|
105
|
+
border: 0;
|
|
106
|
+
border-radius: 999px;
|
|
107
|
+
background: transparent;
|
|
108
|
+
color: var(--muted-foreground);
|
|
109
|
+
font: inherit;
|
|
110
|
+
line-height: 1;
|
|
111
|
+
cursor: pointer;
|
|
112
|
+
}
|
|
113
|
+
.terminoir-chip__remove:hover {
|
|
114
|
+
color: var(--foreground);
|
|
115
|
+
background: color-mix(in srgb, var(--foreground) 12%, transparent);
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
type ChipTone = 'default' | 'overdue' | 'today' | 'done';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
tone?: ChipTone;
|
|
4
|
+
/** Dot colour before the text (palette name or CSS colour), e.g. a select option colour. */
|
|
5
|
+
color?: string;
|
|
6
|
+
/** Shows a × button; called when it is pressed. */
|
|
7
|
+
onremove?: () => void;
|
|
8
|
+
/** Accessible name of the × button, e.g. "Remove Rosa". */
|
|
9
|
+
removeLabel?: string;
|
|
10
|
+
/** Makes the chip text a button (e.g. open the linked item). */
|
|
11
|
+
onclick?: (event: MouseEvent) => void;
|
|
4
12
|
class?: string;
|
|
5
13
|
children?: import('svelte').Snippet;
|
|
6
14
|
} & Record<string, unknown>;
|