@heyramzi/tasks 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 +36 -9
- package/package.json +12 -2
- package/src/ambient.d.ts +3 -6
- package/src/board-order.ts +17 -17
- package/src/dates.ts +1 -1
- package/src/demo.svelte +14 -19
- package/src/inbox/inbox-card.svelte +4 -3
- package/src/inbox/inbox-draft.svelte +2 -2
- package/src/inbox/inbox-statuses.ts +7 -9
- package/src/inbox/inbox-trace.svelte +2 -1
- package/src/inbox/inbox.svelte +1 -1
- package/src/index.ts +2 -7
- package/src/kanban-board.svelte +3 -3
- package/src/kanban-column.svelte +1 -1
- package/src/reorder.ts +1 -2
- package/src/side-panel-types.ts +16 -0
- package/src/side-panel.svelte +193 -0
- package/src/status-badge.svelte +21 -15
- package/src/status-dot.svelte +2 -2
- package/src/status-icon.svelte +2 -2
- package/src/status-types.ts +1 -3
- package/src/task-gantt.svelte +4 -5
- package/src/task-list.svelte +3 -3
- package/src/task-types.ts +0 -10
- package/src/time/duration.ts +1 -1
- package/src/time/time-chip.svelte +4 -4
- package/src/time/time-dot.svelte +1 -1
- package/src/time/time-ring.svelte +2 -2
- package/src/time/time-tracker.svelte +6 -9
- package/src/icons/board-icon.svelte +0 -11
- package/src/icons/chevron-down.svg +0 -1
- package/src/icons/chevron-right.svg +0 -1
- package/src/icons/clock.svg +0 -1
- package/src/icons/close.svg +0 -1
- package/src/icons/gantt-icon.svelte +0 -11
- package/src/icons/list-icon.svelte +0 -16
- package/src/icons/milestone-flag.svg +0 -1
- package/src/icons/play.svg +0 -1
- package/src/icons/spinner.svg +0 -1
- package/src/inbox/icons/call.svg +0 -1
- package/src/inbox/icons/email.svg +0 -1
- package/src/inbox/icons/form.svg +0 -1
- package/src/inbox/icons/slack.svg +0 -1
- package/src/lib/cn.ts +0 -11
- package/src/task-view-switch.svelte +0 -118
- package/src/tone.ts +0 -54
package/README.md
CHANGED
|
@@ -6,11 +6,12 @@ Status badges, dots and icons come from one map too. Live at
|
|
|
6
6
|
[ui.heyramzi.com/tasks](https://ui.heyramzi.com/tasks).
|
|
7
7
|
|
|
8
8
|
Every view knows nothing about your data. You pass items and a few accessors,
|
|
9
|
-
and it hands back a change to persist.
|
|
10
|
-
|
|
9
|
+
and it hands back a change to persist. It builds on `@heyramzi/ui` and
|
|
10
|
+
`phosphor-svelte`, which are peers, plus `svelte`. This package was
|
|
11
|
+
`@heyramzi/board` up to 0.2.
|
|
11
12
|
|
|
12
13
|
```
|
|
13
|
-
pnpm add @heyramzi/tasks
|
|
14
|
+
pnpm add @heyramzi/tasks @heyramzi/ui phosphor-svelte
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
## Board
|
|
@@ -194,10 +195,34 @@ It opens scrolled to today, a third of the way in. Call `scrollToToday()` on a
|
|
|
194
195
|
|
|
195
196
|
## View switch
|
|
196
197
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
`
|
|
198
|
+
There is no switch in this package. The demo flips between views with
|
|
199
|
+
`SegmentedFilter` from `@heyramzi/ui/components/segmented` and the Phosphor
|
|
200
|
+
icons `Kanban`, `ListBullets` and `ChartBarHorizontal`. Phosphor has no gantt
|
|
201
|
+
icon, so `ChartBarHorizontal` stands in.
|
|
202
|
+
|
|
203
|
+
## Side panel
|
|
204
|
+
|
|
205
|
+
`SidePanel` is a sheet for one record: a title, a description, status chips, doors
|
|
206
|
+
out to other tools (`openIn`), a copy-a-prompt `handoff`, a strip of `stats`, then
|
|
207
|
+
the body and an optional pinned `footer`. You pass the tools; the panel knows none.
|
|
208
|
+
Import it from `@heyramzi/tasks` or alone from `@heyramzi/tasks/side-panel`.
|
|
209
|
+
|
|
210
|
+
```svelte
|
|
211
|
+
<SidePanel
|
|
212
|
+
bind:open
|
|
213
|
+
title="Pricing page"
|
|
214
|
+
description="Ada, due Friday"
|
|
215
|
+
badges={[{ label: "Review", tone: "warning" }]}
|
|
216
|
+
openIn={[{ tool: github, href: issueUrl, label: "Open the issue on GitHub" }]}
|
|
217
|
+
stats={[{ label: "Logged", value: "6h 30m", sub: "of 8h" }]}
|
|
218
|
+
>
|
|
219
|
+
...
|
|
220
|
+
</SidePanel>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`tool` is an `OpenInTool` from `@heyramzi/ui/components/open-in`:
|
|
224
|
+
`{ name, action, icon }`. `bare` hands the whole panel to the body, and `width`
|
|
225
|
+
runs from `sm` to `2xl`.
|
|
201
226
|
|
|
202
227
|
## Time tracking
|
|
203
228
|
|
|
@@ -258,11 +283,13 @@ exported helpers.
|
|
|
258
283
|
- `StatusBadge`, `StatusDot`, `StatusIcon`: chips, dots and icons from one config.
|
|
259
284
|
A badge also takes its text as children, for a caller that already has the word.
|
|
260
285
|
- `defineStatuses`: builds a status registry from a plain map.
|
|
261
|
-
- `
|
|
286
|
+
- `SidePanel`: a sheet for one record, above.
|
|
262
287
|
- `salesStatuses`: an example pipeline map.
|
|
263
288
|
|
|
264
289
|
## Styling
|
|
265
290
|
|
|
291
|
+
The severity scale (`TONE_CLASS`, `TONE_TEXT`, `TONE_ICON`, `TONE_COLOR`) lives in
|
|
292
|
+
`@heyramzi/ui/lib/tone.js`, and `insetWellClass` in `@heyramzi/ui/lib/panel.js`.
|
|
266
293
|
The accent tone reads `--tone-accent` and falls back to `--color-primary`. Set
|
|
267
294
|
`--tone-accent` when your primary is a neutral and your accent is a brand color.
|
|
268
295
|
|
|
@@ -275,7 +302,7 @@ same values when they aren't. Everything goes still under
|
|
|
275
302
|
|
|
276
303
|
## Demo
|
|
277
304
|
|
|
278
|
-
`src/demo.svelte` puts one task list behind a Board, List and Gantt
|
|
305
|
+
`src/demo.svelte` puts one task list behind a Board, List and Gantt filter, with
|
|
279
306
|
sorting, a Days and Weeks scale and a status badge strip.
|
|
280
307
|
|
|
281
308
|
The fractional ordering in `board-order.ts` is the CC0 algorithm from
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyramzi/tasks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Svelte 5 task views: a drag-and-drop board, a grouped list and a gantt timeline from one config, plus status badges, dots and icons.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -74,6 +74,11 @@
|
|
|
74
74
|
"types": "./src/inbox/index.ts",
|
|
75
75
|
"default": "./src/inbox/index.ts"
|
|
76
76
|
},
|
|
77
|
+
"./side-panel": {
|
|
78
|
+
"svelte": "./src/side-panel.svelte",
|
|
79
|
+
"types": "./src/side-panel.svelte",
|
|
80
|
+
"default": "./src/side-panel.svelte"
|
|
81
|
+
},
|
|
77
82
|
"./presets/sales": {
|
|
78
83
|
"svelte": "./src/presets/sales.ts",
|
|
79
84
|
"types": "./src/presets/sales.ts",
|
|
@@ -85,13 +90,18 @@
|
|
|
85
90
|
},
|
|
86
91
|
"./demo.svelte": "./src/demo.svelte"
|
|
87
92
|
},
|
|
93
|
+
"dependencies": {
|
|
94
|
+
"@heyramzi/utils": "^1.3.3"
|
|
95
|
+
},
|
|
88
96
|
"peerDependencies": {
|
|
89
97
|
"@heyramzi/ui": "*",
|
|
98
|
+
"phosphor-svelte": "^3.1.0",
|
|
90
99
|
"svelte": "^5.0.0"
|
|
91
100
|
},
|
|
92
101
|
"devDependencies": {
|
|
93
|
-
"@heyramzi/ui": "
|
|
102
|
+
"@heyramzi/ui": "8.0.0",
|
|
94
103
|
"@types/node": "^26.6.4",
|
|
104
|
+
"phosphor-svelte": "^3.1.0",
|
|
95
105
|
"svelte-check": "^4.7.6",
|
|
96
106
|
"typescript": "~7.0.2"
|
|
97
107
|
},
|
package/src/ambient.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
//
|
|
2
|
-
// without pulling in vite/client.
|
|
3
|
-
declare module "*.
|
|
4
|
-
const svg: string;
|
|
5
|
-
export default svg;
|
|
6
|
-
}
|
|
1
|
+
// A kit component pulls its stylesheet in with a side-effect import. Vite handles it at
|
|
2
|
+
// build time; this declaration lets svelte-check accept it without pulling in vite/client.
|
|
3
|
+
declare module "*.css";
|
package/src/board-order.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// (see the README credit). Kept inline so this package carries no runtime deps.
|
|
5
5
|
const DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
6
6
|
const INT_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
7
|
-
const ZERO = DIGITS
|
|
7
|
+
const ZERO = DIGITS.charAt(0);
|
|
8
8
|
|
|
9
9
|
function integerLength(head: string): number {
|
|
10
10
|
const i = INT_DIGITS.indexOf(head);
|
|
@@ -14,13 +14,13 @@ function integerLength(head: string): number {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function integerPart(key: string): string {
|
|
17
|
-
const length = integerLength(key
|
|
17
|
+
const length = integerLength(key.charAt(0));
|
|
18
18
|
if (length > key.length) throw new Error("invalid order key: " + key);
|
|
19
19
|
return key.slice(0, length);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function smallestInteger(): string {
|
|
23
|
-
return INT_DIGITS
|
|
23
|
+
return INT_DIGITS.charAt(0) + ZERO.repeat(INT_DIGITS.length / 2);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function midpoint(a: string, b: string | null): string {
|
|
@@ -33,44 +33,44 @@ function midpoint(a: string, b: string | null): string {
|
|
|
33
33
|
if (n > 0) return b.slice(0, n) + midpoint(a.slice(n), b.slice(n));
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const digitA = a ? DIGITS.indexOf(a
|
|
37
|
-
const digitB = b != null ? DIGITS.indexOf(b
|
|
36
|
+
const digitA = a ? DIGITS.indexOf(a.charAt(0)) : 0;
|
|
37
|
+
const digitB = b != null ? DIGITS.indexOf(b.charAt(0)) : DIGITS.length;
|
|
38
38
|
if (digitB - digitA > 1) {
|
|
39
|
-
return DIGITS
|
|
39
|
+
return DIGITS.charAt(Math.round(0.5 * (digitA + digitB)));
|
|
40
40
|
}
|
|
41
41
|
if (b && b.length > 1) return b.slice(0, 1);
|
|
42
|
-
return DIGITS
|
|
42
|
+
return DIGITS.charAt(digitA) + midpoint(a.slice(1), null);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function incrementInteger(x: string): string | null {
|
|
46
|
-
const head = x
|
|
46
|
+
const head = x.charAt(0);
|
|
47
47
|
let trailing = "";
|
|
48
48
|
for (let i = x.length - 1; i >= 1; i--) {
|
|
49
|
-
const d = DIGITS.indexOf(x
|
|
49
|
+
const d = DIGITS.indexOf(x.charAt(i)) + 1;
|
|
50
50
|
if (d === DIGITS.length) trailing = ZERO + trailing;
|
|
51
|
-
else return head + x.slice(1, i) + DIGITS
|
|
51
|
+
else return head + x.slice(1, i) + DIGITS.charAt(d) + trailing;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
const headIndex = INT_DIGITS.indexOf(head);
|
|
55
55
|
if (headIndex === INT_DIGITS.length - 1) return null;
|
|
56
|
-
const h = INT_DIGITS
|
|
56
|
+
const h = INT_DIGITS.charAt(headIndex + 1);
|
|
57
57
|
const delta = integerLength(h) - integerLength(head);
|
|
58
58
|
return h + (delta > 0 ? trailing + ZERO : delta < 0 ? trailing.slice(1) : trailing);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function decrementInteger(x: string): string | null {
|
|
62
|
-
const head = x
|
|
63
|
-
const last = DIGITS
|
|
62
|
+
const head = x.charAt(0);
|
|
63
|
+
const last = DIGITS.charAt(DIGITS.length - 1);
|
|
64
64
|
let trailing = "";
|
|
65
65
|
for (let i = x.length - 1; i >= 1; i--) {
|
|
66
|
-
const d = DIGITS.indexOf(x
|
|
66
|
+
const d = DIGITS.indexOf(x.charAt(i)) - 1;
|
|
67
67
|
if (d === -1) trailing = last + trailing;
|
|
68
|
-
else return head + x.slice(1, i) + DIGITS
|
|
68
|
+
else return head + x.slice(1, i) + DIGITS.charAt(d) + trailing;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
const headIndex = INT_DIGITS.indexOf(head);
|
|
72
72
|
if (headIndex === 0) return null;
|
|
73
|
-
const h = INT_DIGITS
|
|
73
|
+
const h = INT_DIGITS.charAt(headIndex - 1);
|
|
74
74
|
const delta = integerLength(h) - integerLength(head);
|
|
75
75
|
return h + (delta > 0 ? trailing + last : delta < 0 ? trailing.slice(1) : trailing);
|
|
76
76
|
}
|
|
@@ -80,7 +80,7 @@ export function keyBetween(a: string | null, b: string | null): string {
|
|
|
80
80
|
if (a != null && b != null && a > b) [a, b] = [b, a];
|
|
81
81
|
|
|
82
82
|
if (a == null) {
|
|
83
|
-
if (b == null) return INT_DIGITS
|
|
83
|
+
if (b == null) return INT_DIGITS.charAt(INT_DIGITS.length / 2) + ZERO;
|
|
84
84
|
const ib = integerPart(b);
|
|
85
85
|
const fb = b.slice(ib.length);
|
|
86
86
|
if (ib === smallestInteger()) return ib + midpoint("", fb);
|
package/src/dates.ts
CHANGED
|
@@ -4,7 +4,7 @@ const DAY_MS = 86_400_000;
|
|
|
4
4
|
|
|
5
5
|
/** Days since 1970-01-01 for an ISO date. Anything after the date part is ignored. */
|
|
6
6
|
export function dayNumber(iso: string): number {
|
|
7
|
-
const [y, m, d] = iso.slice(0, 10).split("-").map(Number);
|
|
7
|
+
const [y = 1970, m = 1, d = 1] = iso.slice(0, 10).split("-").map(Number);
|
|
8
8
|
return Math.round(Date.UTC(y, m - 1, d) / DAY_MS);
|
|
9
9
|
}
|
|
10
10
|
|
package/src/demo.svelte
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
import KanbanBoard from "./kanban-board.svelte";
|
|
3
3
|
import TaskList from "./task-list.svelte";
|
|
4
4
|
import TaskGantt from "./task-gantt.svelte";
|
|
5
|
-
import
|
|
5
|
+
import { SegmentedFilter } from "@heyramzi/ui/components/segmented";
|
|
6
|
+
import { ChartBarHorizontal, Kanban, ListBullets } from "phosphor-svelte";
|
|
6
7
|
import StatusBadge from "./status-badge.svelte";
|
|
7
8
|
import StatusDot from "./status-dot.svelte";
|
|
8
|
-
import BoardIcon from "./icons/board-icon.svelte";
|
|
9
|
-
import ListIcon from "./icons/list-icon.svelte";
|
|
10
|
-
import GanttIcon from "./icons/gantt-icon.svelte";
|
|
11
9
|
import { defineStatuses } from "./define-statuses.js";
|
|
12
10
|
import { addDays, dayNumber, todayIso } from "./dates.js";
|
|
13
11
|
import { salesStatuses } from "./presets/sales.js";
|
|
@@ -63,12 +61,16 @@
|
|
|
63
61
|
]);
|
|
64
62
|
|
|
65
63
|
const views = [
|
|
66
|
-
{
|
|
67
|
-
{
|
|
68
|
-
{
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
{ value: "board", label: "Board", icon: Kanban },
|
|
65
|
+
{ value: "list", label: "List", icon: ListBullets },
|
|
66
|
+
{ value: "gantt", label: "Gantt", icon: ChartBarHorizontal },
|
|
67
|
+
] as const;
|
|
68
|
+
const scales = [
|
|
69
|
+
{ value: "days", label: "Days" },
|
|
70
|
+
{ value: "weeks", label: "Weeks" },
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
73
|
+
let view = $state<(typeof views)[number]["value"]>("board");
|
|
72
74
|
let sortKey = $state("manual");
|
|
73
75
|
let scale = $state<"days" | "weeks">("days");
|
|
74
76
|
|
|
@@ -172,21 +174,14 @@
|
|
|
172
174
|
<div class="flex flex-col gap-6 rounded-xl border border-border bg-background p-4">
|
|
173
175
|
<div class="flex h-[600px] flex-col gap-3">
|
|
174
176
|
<div class="flex flex-wrap items-center gap-2">
|
|
175
|
-
<
|
|
177
|
+
<SegmentedFilter options={views} bind:value={view} ariaLabel="Task view" block={false} />
|
|
176
178
|
<div class="ml-auto flex items-center gap-3">
|
|
177
179
|
<label class="flex cursor-pointer items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
|
178
180
|
<input type="checkbox" bind:checked={tracking} class="size-3.5 accent-[var(--tone-accent,var(--color-primary))]" />
|
|
179
181
|
Time tracking
|
|
180
182
|
</label>
|
|
181
183
|
{#if view === "gantt"}
|
|
182
|
-
<
|
|
183
|
-
views={[
|
|
184
|
-
{ id: "days", label: "Days" },
|
|
185
|
-
{ id: "weeks", label: "Weeks" },
|
|
186
|
-
]}
|
|
187
|
-
bind:value={scale}
|
|
188
|
-
label="Timeline scale"
|
|
189
|
-
/>
|
|
184
|
+
<SegmentedFilter options={scales} bind:value={scale} ariaLabel="Timeline scale" block={false} />
|
|
190
185
|
{:else}
|
|
191
186
|
<label for="tasks-sort" class="text-xs font-medium text-muted-foreground">Sort</label>
|
|
192
187
|
<select
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import { PixelLoader } from "@heyramzi/ui/ai/loading-state";
|
|
5
5
|
import { Button } from "@heyramzi/ui/shadcn/button";
|
|
6
6
|
import { Textarea } from "@heyramzi/ui/shadcn/textarea";
|
|
7
|
-
import { cn } from "
|
|
7
|
+
import { cn } from "@heyramzi/utils";
|
|
8
8
|
import StatusBadge from "../status-badge.svelte";
|
|
9
9
|
import StatusDot from "../status-dot.svelte";
|
|
10
|
-
import { TONE_TEXT } from "
|
|
10
|
+
import { TONE_TEXT } from "@heyramzi/ui/lib/tone.js";
|
|
11
11
|
import InboxDraftEditor from "./inbox-draft.svelte";
|
|
12
12
|
import { canUndo, isOpen, startDraft } from "./inbox-logic.js";
|
|
13
13
|
import {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
let open = $derived(isOpen(item));
|
|
38
38
|
let level = $derived(confidenceLevel(item.confidence.score));
|
|
39
39
|
let tone = $derived(inboxConfidence.tone(level));
|
|
40
|
+
let SourceIcon = $derived(SOURCE_ICON[item.trace.source.type]);
|
|
40
41
|
|
|
41
42
|
let pickedDraft = $state<string>();
|
|
42
43
|
let edits = $state<InboxDraftEdits>({});
|
|
@@ -185,7 +186,7 @@
|
|
|
185
186
|
|
|
186
187
|
<div class="text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-1 pl-6 text-xs">
|
|
187
188
|
<span class="inline-flex items-center gap-1.5">
|
|
188
|
-
<
|
|
189
|
+
<SourceIcon size={14} aria-hidden="true" />
|
|
189
190
|
{SOURCE_LABEL[item.trace.source.type]}, {item.trace.source.at}
|
|
190
191
|
</span>
|
|
191
192
|
<span class={cn("inline-flex items-center gap-1.5 tabular-nums", TONE_TEXT[tone])}>
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { Input } from "@heyramzi/ui/shadcn/input";
|
|
3
3
|
import { Textarea } from "@heyramzi/ui/shadcn/textarea";
|
|
4
4
|
import { ToggleGroup, ToggleGroupItem } from "@heyramzi/ui/shadcn/toggle-group";
|
|
5
|
-
import { cn } from "
|
|
6
|
-
import { insetWellClass } from "
|
|
5
|
+
import { cn } from "@heyramzi/utils";
|
|
6
|
+
import { insetWellClass } from "@heyramzi/ui/lib/panel.js";
|
|
7
7
|
import type { InboxDraft, InboxDraftEdits } from "./inbox-types.js";
|
|
8
8
|
|
|
9
9
|
type Props = {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { defineStatuses } from "../define-statuses.js";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import formIcon from "./icons/form.svg?raw";
|
|
5
|
-
import slackIcon from "./icons/slack.svg?raw";
|
|
2
|
+
import { ClipboardText, EnvelopeSimple, Hash, Phone } from "phosphor-svelte";
|
|
3
|
+
import type { PhosphorIcon } from "@heyramzi/ui/lib/icon-types.js";
|
|
6
4
|
import type { InboxSourceType } from "./inbox-types.js";
|
|
7
5
|
|
|
8
6
|
/** The lifecycle of an item. The dot and the badge both read this one map. */
|
|
@@ -35,11 +33,11 @@ export function confidenceLevel(score: number): "high" | "medium" | "low" {
|
|
|
35
33
|
return "low";
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
export const SOURCE_ICON: Record<InboxSourceType,
|
|
39
|
-
call:
|
|
40
|
-
email:
|
|
41
|
-
slack:
|
|
42
|
-
form:
|
|
36
|
+
export const SOURCE_ICON: Record<InboxSourceType, PhosphorIcon> = {
|
|
37
|
+
call: Phone,
|
|
38
|
+
email: EnvelopeSimple,
|
|
39
|
+
slack: Hash,
|
|
40
|
+
form: ClipboardText,
|
|
43
41
|
};
|
|
44
42
|
|
|
45
43
|
export const SOURCE_LABEL: Record<InboxSourceType, string> = {
|
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
let { trace }: Props = $props();
|
|
9
9
|
|
|
10
10
|
let count = $derived(trace.evidence.length);
|
|
11
|
+
let SourceIcon = $derived(SOURCE_ICON[trace.source.type]);
|
|
11
12
|
</script>
|
|
12
13
|
|
|
13
14
|
<!-- WHY the shared Reasoning and not a local disclosure: it is already the
|
|
14
15
|
collapsed-by-default "how the agent got here" pattern, with the same chevron. -->
|
|
15
16
|
<Reasoning>
|
|
16
17
|
<ReasoningTrigger class="w-auto">
|
|
17
|
-
<
|
|
18
|
+
<SourceIcon size={14} class="shrink-0" aria-hidden="true" />
|
|
18
19
|
<span>
|
|
19
20
|
Trace: {SOURCE_LABEL[trace.source.type]}, {trace.source.at}, {count}
|
|
20
21
|
{count === 1 ? "quote" : "quotes"}
|
package/src/inbox/inbox.svelte
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { Kbd, KbdGroup } from "@heyramzi/ui/shadcn/kbd";
|
|
7
7
|
import { Tabs, TabsList, TabsTrigger } from "@heyramzi/ui/shadcn/tabs";
|
|
8
8
|
import { ToggleGroup, ToggleGroupItem } from "@heyramzi/ui/shadcn/toggle-group";
|
|
9
|
-
import { cn } from "
|
|
9
|
+
import { cn } from "@heyramzi/utils";
|
|
10
10
|
import StatusDot from "../status-dot.svelte";
|
|
11
11
|
import InboxCard from "./inbox-card.svelte";
|
|
12
12
|
import { flatten, groupByProject, inLane, isTyping, laneOf } from "./inbox-logic.js";
|
package/src/index.ts
CHANGED
|
@@ -17,28 +17,23 @@ export { default as StatusBadge } from "./status-badge.svelte";
|
|
|
17
17
|
export { default as StatusDot } from "./status-dot.svelte";
|
|
18
18
|
export { default as StatusIcon } from "./status-icon.svelte";
|
|
19
19
|
export { defineStatuses } from "./define-statuses.js";
|
|
20
|
-
export { TONE_CLASS, TONE_COLOR, TONE_ICON, TONE_TEXT, insetWellClass } from "./tone.js";
|
|
21
20
|
export type {
|
|
22
21
|
StatusDef,
|
|
23
22
|
StatusDotVariant,
|
|
24
23
|
StatusId,
|
|
25
24
|
StatusMap,
|
|
26
25
|
Statuses,
|
|
27
|
-
Tone,
|
|
28
26
|
} from "./status-types.js";
|
|
27
|
+
export { default as SidePanel } from "./side-panel.svelte";
|
|
28
|
+
export type { PanelStat, SidePanelWidth } from "./side-panel-types.js";
|
|
29
29
|
export { default as TaskList } from "./task-list.svelte";
|
|
30
30
|
export { default as TaskGantt } from "./task-gantt.svelte";
|
|
31
|
-
export { default as TaskViewSwitch } from "./task-view-switch.svelte";
|
|
32
|
-
export { default as BoardIcon } from "./icons/board-icon.svelte";
|
|
33
|
-
export { default as ListIcon } from "./icons/list-icon.svelte";
|
|
34
|
-
export { default as GanttIcon } from "./icons/gantt-icon.svelte";
|
|
35
31
|
export { addDays, dayNumber, isoFromDay, todayIso } from "./dates.js";
|
|
36
32
|
export type {
|
|
37
33
|
GanttChange,
|
|
38
34
|
GanttLabelContext,
|
|
39
35
|
TaskListGroupContext,
|
|
40
36
|
TaskListRowContext,
|
|
41
|
-
TaskView,
|
|
42
37
|
} from "./task-types.js";
|
|
43
38
|
export { default as TimeTracker } from "./time/time-tracker.svelte";
|
|
44
39
|
export { default as TimeChip } from "./time/time-chip.svelte";
|
package/src/kanban-board.svelte
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
import KanbanColumn from "./kanban-column.svelte";
|
|
8
8
|
import { liftDragImage } from "./kanban-drag.js";
|
|
9
9
|
import { boardOrderAt } from "./board-order.js";
|
|
10
|
-
import { cn } from "
|
|
10
|
+
import { cn } from "@heyramzi/utils";
|
|
11
11
|
import { createKanbanDnd } from "./use-kanban-dnd.svelte.js";
|
|
12
|
-
import
|
|
12
|
+
import { CaretDown } from "phosphor-svelte";
|
|
13
13
|
import type {
|
|
14
14
|
KanbanCardContext,
|
|
15
15
|
KanbanColumn as KanbanColumnDef,
|
|
@@ -447,7 +447,7 @@
|
|
|
447
447
|
"size-2.5 transition-transform duration-[var(--duration-fast,160ms)] ease-[var(--ease-smooth-out,ease-out)] [&>svg]:size-full",
|
|
448
448
|
!railOpen && "-rotate-90",
|
|
449
449
|
)}
|
|
450
|
-
aria-hidden="true"
|
|
450
|
+
aria-hidden="true"><CaretDown weight="bold" /></span
|
|
451
451
|
>
|
|
452
452
|
{hiddenLabel}
|
|
453
453
|
</button>
|
package/src/kanban-column.svelte
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
-
import { cn } from "
|
|
4
|
+
import { cn } from "@heyramzi/utils";
|
|
5
5
|
|
|
6
6
|
type Props = {
|
|
7
7
|
/** Any CSS color. Only shows while a card is held over the column. */
|
package/src/reorder.ts
CHANGED
|
@@ -14,7 +14,6 @@ export function reorderByIds<Item extends Identifiable>(
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const next = items.slice();
|
|
17
|
-
|
|
18
|
-
next.splice(overIndex, 0, moved);
|
|
17
|
+
next.splice(overIndex, 0, ...next.splice(activeIndex, 1));
|
|
19
18
|
return next;
|
|
20
19
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// In a .ts file, not the panel's module script, so a plain `tsc` consumer of the
|
|
2
|
+
// barrel can read them. tsc sees a .svelte file as a default export only.
|
|
3
|
+
|
|
4
|
+
/** How wide the panel opens from `sm` (a short form) to `2xl` (a reader). Under
|
|
5
|
+
the `sm` breakpoint every panel is full width. */
|
|
6
|
+
export type SidePanelWidth = "sm" | "md" | "lg" | "xl" | "2xl";
|
|
7
|
+
|
|
8
|
+
/** One number the record is judged on. They sit in the header, in the order given. */
|
|
9
|
+
export type PanelStat = {
|
|
10
|
+
label: string;
|
|
11
|
+
value: string;
|
|
12
|
+
/** A quiet note beside the value: the rate behind a count, or what the figure is. */
|
|
13
|
+
sub?: string;
|
|
14
|
+
/** One severity vocabulary: a count that should be zero is `bad`. */
|
|
15
|
+
tone?: "default" | "muted" | "good" | "bad";
|
|
16
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { PanelStat, SidePanelWidth } from "./side-panel-types.js";
|
|
3
|
+
export type { PanelStat, SidePanelWidth };
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
// =========================
|
|
8
|
+
// IMPORTS
|
|
9
|
+
// =========================
|
|
10
|
+
import type { Snippet } from "svelte";
|
|
11
|
+
import * as Sheet from "@heyramzi/ui/shadcn/sheet";
|
|
12
|
+
import { OpenIn, type OpenInTool } from "@heyramzi/ui/components/open-in";
|
|
13
|
+
import { CopyButton } from "@heyramzi/ui/components/copy";
|
|
14
|
+
import type { Tone } from "@heyramzi/ui/lib/tone.js";
|
|
15
|
+
import { cn } from "@heyramzi/utils";
|
|
16
|
+
import StatusBadge from "./status-badge.svelte";
|
|
17
|
+
|
|
18
|
+
// =========================
|
|
19
|
+
// PROPS
|
|
20
|
+
// =========================
|
|
21
|
+
type Props = {
|
|
22
|
+
open: boolean;
|
|
23
|
+
/** Names the dialog for screen readers and heads the panel. */
|
|
24
|
+
title: string;
|
|
25
|
+
/** Merged onto the title. A short path is set `font-mono`, a subject is not. */
|
|
26
|
+
titleClass?: string;
|
|
27
|
+
/** The facts the record is filed under, read out under the title. */
|
|
28
|
+
description?: string;
|
|
29
|
+
/** Status chips, left of the action row. */
|
|
30
|
+
badges?: { label: string; tone: Tone }[];
|
|
31
|
+
/** Doors out to another tool. `text` replaces the tool's name on the button. */
|
|
32
|
+
openIn?: { tool: OpenInTool; href: string; label: string; text?: string }[];
|
|
33
|
+
/** A prompt for an AI about this record, copied on click. */
|
|
34
|
+
handoff?: { value: () => string | Promise<string>; label: string; copiedLabel: string };
|
|
35
|
+
/** The record's numbers, as a strip under the badges. Same place in every panel. */
|
|
36
|
+
stats?: PanelStat[];
|
|
37
|
+
/** Anything else the record can do, drawn last in the action row. */
|
|
38
|
+
actions?: Snippet;
|
|
39
|
+
/** Pinned actions. The body scrolls above it; the caller aligns the row. */
|
|
40
|
+
footer?: Snippet;
|
|
41
|
+
children: Snippet;
|
|
42
|
+
width?: SidePanelWidth;
|
|
43
|
+
/** The body brings its own header: the title is read out but not drawn, and
|
|
44
|
+
the body is neither padded nor scrolled by the panel. */
|
|
45
|
+
bare?: boolean;
|
|
46
|
+
/** Fires once when the panel closes, from the close button, Escape or the overlay. */
|
|
47
|
+
onClose?: () => void;
|
|
48
|
+
};
|
|
49
|
+
let {
|
|
50
|
+
open = $bindable(),
|
|
51
|
+
title,
|
|
52
|
+
titleClass,
|
|
53
|
+
description,
|
|
54
|
+
badges = [],
|
|
55
|
+
openIn = [],
|
|
56
|
+
handoff,
|
|
57
|
+
stats = [],
|
|
58
|
+
actions,
|
|
59
|
+
footer,
|
|
60
|
+
children,
|
|
61
|
+
width = "sm",
|
|
62
|
+
bare = false,
|
|
63
|
+
onClose,
|
|
64
|
+
}: Props = $props();
|
|
65
|
+
|
|
66
|
+
// =========================
|
|
67
|
+
// STATE
|
|
68
|
+
// =========================
|
|
69
|
+
let panel = $state<HTMLElement | null>(null);
|
|
70
|
+
|
|
71
|
+
// =========================
|
|
72
|
+
// DERIVED
|
|
73
|
+
// =========================
|
|
74
|
+
let hasRow = $derived(badges.length > 0 || openIn.length > 0 || Boolean(handoff || actions));
|
|
75
|
+
const STAT_TONE = {
|
|
76
|
+
default: "",
|
|
77
|
+
muted: "text-muted-foreground",
|
|
78
|
+
good: "text-success",
|
|
79
|
+
bad: "text-destructive",
|
|
80
|
+
} as const;
|
|
81
|
+
const WIDTH: Record<SidePanelWidth, string> = {
|
|
82
|
+
sm: "sm:max-w-sm",
|
|
83
|
+
md: "sm:max-w-md",
|
|
84
|
+
lg: "sm:max-w-xl",
|
|
85
|
+
xl: "sm:max-w-2xl",
|
|
86
|
+
"2xl": "sm:max-w-3xl",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// =========================
|
|
90
|
+
// HANDLERS
|
|
91
|
+
// =========================
|
|
92
|
+
// WHY focus goes to the panel unless a text field comes first: the dialog otherwise
|
|
93
|
+
// opens on its first control, which draws a ring on a record nobody has touched.
|
|
94
|
+
// A form that opens on its field keeps the default, since that is where you type.
|
|
95
|
+
const FIRST_CONTROL =
|
|
96
|
+
'a[href], button:not([disabled]), input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled])';
|
|
97
|
+
function onOpenAutoFocus(event: Event) {
|
|
98
|
+
if (!panel || panel.querySelector(FIRST_CONTROL)?.matches("input, textarea, select")) return;
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
panel.focus();
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<!-- =========================== -->
|
|
105
|
+
<!-- MARKUP -->
|
|
106
|
+
<!-- =========================== -->
|
|
107
|
+
<Sheet.Root
|
|
108
|
+
bind:open
|
|
109
|
+
onOpenChange={(next) => {
|
|
110
|
+
if (!next) onClose?.();
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<Sheet.Content
|
|
114
|
+
side="right"
|
|
115
|
+
bind:ref={panel}
|
|
116
|
+
tabindex={-1}
|
|
117
|
+
class={cn("flex w-full flex-col gap-0 p-0", WIDTH[width], bare && "overflow-hidden")}
|
|
118
|
+
{onOpenAutoFocus}
|
|
119
|
+
>
|
|
120
|
+
{#if bare}
|
|
121
|
+
<Sheet.Header class="sr-only">
|
|
122
|
+
<Sheet.Title>{title}</Sheet.Title>
|
|
123
|
+
</Sheet.Header>
|
|
124
|
+
{@render children()}
|
|
125
|
+
{:else}
|
|
126
|
+
<Sheet.Header class="gap-1 border-b border-border">
|
|
127
|
+
<!-- WHY the right padding: the close button sits over this corner and
|
|
128
|
+
a long title runs straight under it. -->
|
|
129
|
+
<Sheet.Title class={cn("pr-9 text-base leading-snug", titleClass)}>{title}</Sheet.Title>
|
|
130
|
+
{#if description}
|
|
131
|
+
<Sheet.Description>{description}</Sheet.Description>
|
|
132
|
+
{/if}
|
|
133
|
+
{#if hasRow}
|
|
134
|
+
<div class="flex items-center justify-between gap-3 pt-1.5">
|
|
135
|
+
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
|
136
|
+
{#each badges as badge (`${badge.tone}:${badge.label}`)}
|
|
137
|
+
<StatusBadge tone={badge.tone}>{badge.label}</StatusBadge>
|
|
138
|
+
{/each}
|
|
139
|
+
</div>
|
|
140
|
+
<div class="flex shrink-0 items-center gap-1">
|
|
141
|
+
{#each openIn as door (door.href)}
|
|
142
|
+
<OpenIn
|
|
143
|
+
tool={door.tool}
|
|
144
|
+
href={door.href}
|
|
145
|
+
title={door.label}
|
|
146
|
+
label={door.text}
|
|
147
|
+
variant="button"
|
|
148
|
+
size="sm"
|
|
149
|
+
/>
|
|
150
|
+
{/each}
|
|
151
|
+
{#if handoff}
|
|
152
|
+
<CopyButton
|
|
153
|
+
agent
|
|
154
|
+
value={handoff.value}
|
|
155
|
+
label={handoff.label}
|
|
156
|
+
copiedLabel={handoff.copiedLabel}
|
|
157
|
+
/>
|
|
158
|
+
{/if}
|
|
159
|
+
{@render actions?.()}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
{/if}
|
|
163
|
+
{#if stats.length > 0}
|
|
164
|
+
<dl class="flex flex-wrap gap-x-6 gap-y-3 pt-3">
|
|
165
|
+
{#each stats as stat (stat.label)}
|
|
166
|
+
<div class="min-w-0">
|
|
167
|
+
<dt class="text-xs text-muted-foreground">{stat.label}</dt>
|
|
168
|
+
<dd class={cn("text-sm font-medium tabular-nums", STAT_TONE[stat.tone ?? "default"])}>
|
|
169
|
+
{stat.value}
|
|
170
|
+
{#if stat.sub}
|
|
171
|
+
<span class="text-xs font-normal text-muted-foreground">{stat.sub}</span>
|
|
172
|
+
{/if}
|
|
173
|
+
</dd>
|
|
174
|
+
</div>
|
|
175
|
+
{/each}
|
|
176
|
+
</dl>
|
|
177
|
+
{/if}
|
|
178
|
+
</Sheet.Header>
|
|
179
|
+
|
|
180
|
+
<!-- WHY the body pads and spaces its sections itself: a panel that lets the
|
|
181
|
+
caller choose drifts, one with a grey band and one with a white page. -->
|
|
182
|
+
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4">
|
|
183
|
+
{@render children()}
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{#if footer}
|
|
187
|
+
<Sheet.Footer class="mt-0 flex-row items-center justify-end border-t border-border">
|
|
188
|
+
{@render footer()}
|
|
189
|
+
</Sheet.Footer>
|
|
190
|
+
{/if}
|
|
191
|
+
{/if}
|
|
192
|
+
</Sheet.Content>
|
|
193
|
+
</Sheet.Root>
|
package/src/status-badge.svelte
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
import { cn } from "@heyramzi/utils";
|
|
5
|
+
import { Badge } from "@heyramzi/ui/shadcn/badge";
|
|
6
|
+
import { Spinner } from "@heyramzi/ui/shadcn/spinner";
|
|
7
|
+
import { TONE_COLOR, type Tone } from "@heyramzi/ui/lib/tone.js";
|
|
8
|
+
import type { Statuses } from "./status-types.js";
|
|
7
9
|
|
|
8
|
-
type Props = {
|
|
10
|
+
type Props = Omit<HTMLAttributes<HTMLSpanElement>, "class" | "children"> & {
|
|
9
11
|
/** A registry from `defineStatuses`. Supplies the label and tone. */
|
|
10
12
|
statuses?: Statuses;
|
|
11
13
|
/** The status id to look up in `statuses`. Leave it out when you pass `children`. */
|
|
@@ -14,6 +16,8 @@
|
|
|
14
16
|
label?: string;
|
|
15
17
|
/** Override the registry tone. */
|
|
16
18
|
tone?: Tone;
|
|
19
|
+
/** Shows a dot in the tone's colour before the label. */
|
|
20
|
+
dot?: boolean;
|
|
17
21
|
/** Shows a spinner: a background job is still working on the record. */
|
|
18
22
|
busy?: boolean;
|
|
19
23
|
/** Capitalizes the first letter of each word. Turn off for a name or a number. */
|
|
@@ -28,30 +32,32 @@
|
|
|
28
32
|
status,
|
|
29
33
|
label,
|
|
30
34
|
tone,
|
|
35
|
+
dot = false,
|
|
31
36
|
busy = false,
|
|
32
37
|
capitalize = true,
|
|
33
38
|
children,
|
|
34
39
|
class: className,
|
|
40
|
+
...restProps
|
|
35
41
|
}: Props = $props();
|
|
36
42
|
|
|
37
43
|
let resolvedTone = $derived(tone ?? (statuses && status ? statuses.tone(status) : "neutral"));
|
|
38
44
|
let resolvedLabel = $derived(label ?? (statuses && status ? statuses.label(status) : (status ?? "")));
|
|
39
45
|
</script>
|
|
40
46
|
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
<Badge tone={resolvedTone} class={cn(capitalize && "capitalize", className)} {...restProps}>
|
|
48
|
+
{#if dot}
|
|
49
|
+
<span
|
|
50
|
+
class="size-1.5 shrink-0 rounded-full"
|
|
51
|
+
style:background-color={TONE_COLOR[resolvedTone]}
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
></span>
|
|
54
|
+
{/if}
|
|
49
55
|
{#if busy}
|
|
50
|
-
<
|
|
56
|
+
<Spinner class="size-3" aria-hidden="true" />
|
|
51
57
|
{/if}
|
|
52
58
|
{#if children}
|
|
53
59
|
{@render children()}
|
|
54
60
|
{:else}
|
|
55
61
|
{resolvedLabel}
|
|
56
62
|
{/if}
|
|
57
|
-
</
|
|
63
|
+
</Badge>
|
package/src/status-dot.svelte
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { cn } from "
|
|
3
|
-
import { TONE_COLOR } from "
|
|
2
|
+
import { cn } from "@heyramzi/utils";
|
|
3
|
+
import { TONE_COLOR } from "@heyramzi/ui/lib/tone.js";
|
|
4
4
|
import type { StatusDotVariant, Statuses } from "./status-types.js";
|
|
5
5
|
|
|
6
6
|
interface Props {
|
package/src/status-icon.svelte
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Component } from "svelte";
|
|
3
3
|
import StatusDot from "./status-dot.svelte";
|
|
4
|
-
import { cn } from "
|
|
5
|
-
import { TONE_ICON } from "
|
|
4
|
+
import { cn } from "@heyramzi/utils";
|
|
5
|
+
import { TONE_ICON } from "@heyramzi/ui/lib/tone.js";
|
|
6
6
|
import type { Statuses } from "./status-types.js";
|
|
7
7
|
|
|
8
8
|
type Props = {
|
package/src/status-types.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { Component } from "svelte";
|
|
2
|
-
|
|
3
|
-
/** The one severity scale the kit ships. */
|
|
4
|
-
export type Tone = "neutral" | "accent" | "info" | "success" | "warning" | "destructive";
|
|
2
|
+
import type { Tone } from "@heyramzi/ui/lib/tone.js";
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
5
|
* The shape the dot can take. `progress` needs a `progress` value between 0 and 1.
|
package/src/task-gantt.svelte
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
<script lang="ts" generics="T">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
-
import { cn } from "
|
|
3
|
+
import { cn } from "@heyramzi/utils";
|
|
4
4
|
import { dayNumber, isoFromDay, todayIso, weekday } from "./dates.js";
|
|
5
5
|
import type { KanbanColumn as KanbanColumnDef } from "./kanban-types.js";
|
|
6
6
|
import type { GanttChange, GanttLabelContext } from "./task-types.js";
|
|
7
|
-
import
|
|
8
|
-
import flagIcon from "./icons/milestone-flag.svg?raw";
|
|
7
|
+
import { CaretRight, FlagPennant } from "phosphor-svelte";
|
|
9
8
|
|
|
10
9
|
type Props<T> = {
|
|
11
10
|
items: T[];
|
|
@@ -534,7 +533,7 @@
|
|
|
534
533
|
class="-ml-1 grid size-5 shrink-0 place-items-center rounded-md text-muted-foreground outline-none transition-colors duration-[var(--duration-quick,150ms)] hover:bg-foreground/5 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60"
|
|
535
534
|
>
|
|
536
535
|
<span class="g-chevron size-3.5 [&>svg]:size-full" class:g-chevron-open={row.open} aria-hidden="true">
|
|
537
|
-
|
|
536
|
+
<CaretRight weight="bold" />
|
|
538
537
|
</span>
|
|
539
538
|
</button>
|
|
540
539
|
{:else}
|
|
@@ -567,7 +566,7 @@
|
|
|
567
566
|
onpointerenter={() => (hoveredId = row.id)}
|
|
568
567
|
onpointerleave={() => (hoveredId = hoveredId === row.id ? null : hoveredId)}
|
|
569
568
|
>
|
|
570
|
-
<span class="size-5 [&>svg]:size-full" aria-hidden="true"
|
|
569
|
+
<span class="size-5 [&>svg]:size-full" aria-hidden="true"><FlagPennant weight="fill" /></span>
|
|
571
570
|
</button>
|
|
572
571
|
{:else if !row.own}
|
|
573
572
|
<!-- A parent with no dates of its own: the span of what is below it. Not draggable. -->
|
package/src/task-list.svelte
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
import { cubicOut } from "svelte/easing";
|
|
5
5
|
import { liftDragImage } from "./kanban-drag.js";
|
|
6
6
|
import { boardOrderAt } from "./board-order.js";
|
|
7
|
-
import { cn } from "
|
|
7
|
+
import { cn } from "@heyramzi/utils";
|
|
8
8
|
import { createKanbanDnd } from "./use-kanban-dnd.svelte.js";
|
|
9
|
-
import
|
|
9
|
+
import { CaretRight } from "phosphor-svelte";
|
|
10
10
|
import type { KanbanColumn as KanbanColumnDef, KanbanMove, KanbanSortKey } from "./kanban-types.js";
|
|
11
11
|
import type { TaskListGroupContext, TaskListRowContext } from "./task-types.js";
|
|
12
12
|
|
|
@@ -228,7 +228,7 @@
|
|
|
228
228
|
class="-ml-1 grid size-6 place-items-center rounded-md text-muted-foreground outline-none transition-colors duration-[var(--duration-quick,150ms)] hover:bg-foreground/5 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60"
|
|
229
229
|
>
|
|
230
230
|
<span class="tl-chevron size-3.5 [&>svg]:size-full" class:tl-chevron-open={open} aria-hidden="true">
|
|
231
|
-
|
|
231
|
+
<CaretRight weight="bold" />
|
|
232
232
|
</span>
|
|
233
233
|
</button>
|
|
234
234
|
{/if}
|
package/src/task-types.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { Component } from "svelte";
|
|
2
|
-
|
|
3
1
|
/** Second argument handed to the list row snippet. */
|
|
4
2
|
export type TaskListRowContext = {
|
|
5
3
|
groupId: string;
|
|
@@ -25,14 +23,6 @@ export type GanttChange = {
|
|
|
25
23
|
end: string;
|
|
26
24
|
};
|
|
27
25
|
|
|
28
|
-
/** One option in the view switch. */
|
|
29
|
-
export type TaskView = {
|
|
30
|
-
id: string;
|
|
31
|
-
label: string;
|
|
32
|
-
/** Any Svelte component that takes `size` and `class`, e.g. `BoardIcon`. */
|
|
33
|
-
icon?: Component<{ size?: number; class?: string }>;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
26
|
/** Second argument handed to the gantt label snippet. */
|
|
37
27
|
export type GanttLabelContext = {
|
|
38
28
|
/** 0 for a top-level task. Only grows when `getParent` is set. */
|
package/src/time/duration.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Tone } from "
|
|
1
|
+
import type { Tone } from "@heyramzi/ui/lib/tone.js";
|
|
2
2
|
|
|
3
3
|
/** "90", "45m", "1h", "1.5h", "1h30", "1h 30m" and "1:30" to whole minutes. Null when it isn't a duration. */
|
|
4
4
|
export function parseMinutes(text: string): number | null {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { cn } from "
|
|
3
|
-
import { TONE_CLASS, TONE_TEXT } from "
|
|
2
|
+
import { cn } from "@heyramzi/utils";
|
|
3
|
+
import { TONE_CLASS, TONE_TEXT } from "@heyramzi/ui/lib/tone.js";
|
|
4
4
|
import { budgetTone, formatMinutes } from "./duration.js";
|
|
5
5
|
import TimeDot from "./time-dot.svelte";
|
|
6
6
|
import TimeRing from "./time-ring.svelte";
|
|
7
|
-
import
|
|
7
|
+
import { Clock } from "phosphor-svelte";
|
|
8
8
|
|
|
9
9
|
type Props = {
|
|
10
10
|
/** Minutes logged on the task. */
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
{:else if estimate}
|
|
54
54
|
<TimeRing value={minutes} max={estimate} size={12} stroke={6} />
|
|
55
55
|
{:else}
|
|
56
|
-
<span class="size-3 shrink-0 [&>svg]:size-full" aria-hidden="true"
|
|
56
|
+
<span class="size-3 shrink-0 [&>svg]:size-full" aria-hidden="true"><Clock /></span>
|
|
57
57
|
{/if}
|
|
58
58
|
<span aria-hidden="true">
|
|
59
59
|
{formatMinutes(minutes)}{#if estimate}<span class={cn("font-normal", tone === "neutral" ? "text-muted-foreground/70" : TONE_TEXT[tone])}> / {formatMinutes(estimate)}</span>{/if}
|
package/src/time/time-dot.svelte
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
-
import { cn } from "
|
|
4
|
-
import { TONE_TEXT } from "
|
|
3
|
+
import { cn } from "@heyramzi/utils";
|
|
4
|
+
import { TONE_TEXT } from "@heyramzi/ui/lib/tone.js";
|
|
5
5
|
import { budgetTone } from "./duration.js";
|
|
6
6
|
|
|
7
7
|
type Props = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts" generics="T">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
-
import { cn } from "
|
|
4
|
-
import { TONE_TEXT } from "
|
|
3
|
+
import { cn } from "@heyramzi/utils";
|
|
4
|
+
import { TONE_TEXT } from "@heyramzi/ui/lib/tone.js";
|
|
5
5
|
import {
|
|
6
6
|
budgetTone,
|
|
7
7
|
dayLabel,
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
import type { TimeLabels, TimeLogInput, TimeRunning, TimeStopInfo } from "./time-types.js";
|
|
16
16
|
import TimeDot from "./time-dot.svelte";
|
|
17
17
|
import TimeRing from "./time-ring.svelte";
|
|
18
|
-
import
|
|
19
|
-
import playIcon from "../icons/play.svg?raw";
|
|
18
|
+
import { Play, Stop, X } from "phosphor-svelte";
|
|
20
19
|
|
|
21
20
|
type Props<T> = {
|
|
22
21
|
/** Every entry logged on the task. Order within a day is kept. */
|
|
@@ -255,11 +254,9 @@
|
|
|
255
254
|
class:is-running={!!running}
|
|
256
255
|
>
|
|
257
256
|
{#if running}
|
|
258
|
-
<
|
|
259
|
-
<rect x="6" y="6" width="12" height="12" rx="2.5" />
|
|
260
|
-
</svg>
|
|
257
|
+
<Stop weight="fill" class="size-4.5" aria-hidden="true" />
|
|
261
258
|
{:else}
|
|
262
|
-
<span class="size-5 translate-x-px [&>svg]:size-full" aria-hidden="true"
|
|
259
|
+
<span class="size-5 translate-x-px [&>svg]:size-full" aria-hidden="true"><Play weight="fill" /></span>
|
|
263
260
|
{/if}
|
|
264
261
|
</button>
|
|
265
262
|
</div>
|
|
@@ -404,7 +401,7 @@
|
|
|
404
401
|
aria-label={labels.deleteEntry(formatMinutes(getMinutes(entry)))}
|
|
405
402
|
class="time-delete -my-1 -me-1 grid size-8 shrink-0 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-destructive/10 hover:text-destructive focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
|
406
403
|
>
|
|
407
|
-
<span class="size-3.5 [&>svg]:size-full" aria-hidden="true"
|
|
404
|
+
<span class="size-3.5 [&>svg]:size-full" aria-hidden="true"><X weight="bold" /></span>
|
|
408
405
|
</button>
|
|
409
406
|
{/if}
|
|
410
407
|
</li>
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
let { size = 16, class: className }: { size?: number; class?: string } = $props();
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden="true" class={className}>
|
|
6
|
-
<g stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
|
7
|
-
<line x1="4" y1="3" x2="4" y2="11"></line>
|
|
8
|
-
<line x1="8" y1="3" x2="8" y2="7.5"></line>
|
|
9
|
-
<line x1="12" y1="3" x2="12" y2="13"></line>
|
|
10
|
-
</g>
|
|
11
|
-
</svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 3.5 5 6.5 8 3.5"/></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
package/src/icons/clock.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M8 4.75V8l2.25 1.5"/></svg>
|
package/src/icons/close.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="m4 4 8 8M12 4l-8 8"/></svg>
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
let { size = 16, class: className }: { size?: number; class?: string } = $props();
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden="true" class={className}>
|
|
6
|
-
<g stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
|
7
|
-
<line x1="2" y1="4" x2="8" y2="4"></line>
|
|
8
|
-
<line x1="5" y1="8" x2="12" y2="8"></line>
|
|
9
|
-
<line x1="9" y1="12" x2="14" y2="12"></line>
|
|
10
|
-
</g>
|
|
11
|
-
</svg>
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
let { size = 16, class: className }: { size?: number; class?: string } = $props();
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden="true" class={className}>
|
|
6
|
-
<g stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
|
7
|
-
<line x1="6" y1="4" x2="14" y2="4"></line>
|
|
8
|
-
<line x1="6" y1="8" x2="14" y2="8"></line>
|
|
9
|
-
<line x1="6" y1="12" x2="14" y2="12"></line>
|
|
10
|
-
</g>
|
|
11
|
-
<g fill="currentColor">
|
|
12
|
-
<circle cx="2.75" cy="4" r="1"></circle>
|
|
13
|
-
<circle cx="2.75" cy="8" r="1"></circle>
|
|
14
|
-
<circle cx="2.75" cy="12" r="1"></circle>
|
|
15
|
-
</g>
|
|
16
|
-
</svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 2v12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/><path d="M4.75 2.75h8l-2 3 2 3h-8z" fill="currentColor"/></svg>
|
package/src/icons/play.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.6v12.8a1 1 0 0 0 1.5.86l10.6-6.4a1 1 0 0 0 0-1.72L9.5 4.74A1 1 0 0 0 8 5.6Z"/></svg>
|
package/src/icons/spinner.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="3" opacity="0.25"/><path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>
|
package/src/inbox/icons/call.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 2.5h2l1 3-1.5 1a7 7 0 0 0 4 4l1-1.5 3 1v2a1.5 1.5 0 0 1-1.5 1.5A10.5 10.5 0 0 1 2 4a1.5 1.5 0 0 1 1.5-1.5Z"/></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3.5" width="12" height="9" rx="1.5"/><path d="m2.5 4.5 5.5 4 5.5-4"/></svg>
|
package/src/inbox/icons/form.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="2" width="10" height="12" rx="1.5"/><path d="M5.5 5.5h5M5.5 8h5M5.5 10.5h3"/></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 2.5 5.5 13.5M10.5 2.5l-1 11M3 6h10.5M2.5 10H13"/></svg>
|
package/src/lib/cn.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export type ClassValue = string | false | null | undefined;
|
|
2
|
-
|
|
3
|
-
/** Join class values, dropping empties. Deliberately dependency-free. */
|
|
4
|
-
export function cn(...values: ClassValue[]): string {
|
|
5
|
-
let out = "";
|
|
6
|
-
for (const value of values) {
|
|
7
|
-
if (!value) continue;
|
|
8
|
-
out = out ? out + " " + value : value;
|
|
9
|
-
}
|
|
10
|
-
return out;
|
|
11
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { cn } from "./lib/cn.js";
|
|
3
|
-
import type { TaskView } from "./task-types.js";
|
|
4
|
-
|
|
5
|
-
type Props = {
|
|
6
|
-
views: TaskView[];
|
|
7
|
-
/** The active view id. Bindable. */
|
|
8
|
-
value: string;
|
|
9
|
-
onchange?: (id: string) => void;
|
|
10
|
-
label?: string;
|
|
11
|
-
class?: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
let {
|
|
15
|
-
views,
|
|
16
|
-
value = $bindable(),
|
|
17
|
-
onchange,
|
|
18
|
-
label = "View",
|
|
19
|
-
class: className,
|
|
20
|
-
}: Props = $props();
|
|
21
|
-
|
|
22
|
-
let track = $state<HTMLDivElement>();
|
|
23
|
-
let pill = $state<{ x: number; width: number } | null>(null);
|
|
24
|
-
// The pill's first position is written without a transition, or it slides in from 0.
|
|
25
|
-
let placed = $state(false);
|
|
26
|
-
|
|
27
|
-
function measure(): void {
|
|
28
|
-
const button = track?.querySelector<HTMLElement>(`[data-view="${CSS.escape(value)}"]`);
|
|
29
|
-
if (!button) return;
|
|
30
|
-
pill = { x: button.offsetLeft, width: button.offsetWidth };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
$effect(() => {
|
|
34
|
-
void value;
|
|
35
|
-
void views.length;
|
|
36
|
-
measure();
|
|
37
|
-
if (!placed) requestAnimationFrame(() => (placed = true));
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
$effect(() => {
|
|
41
|
-
if (!track) return;
|
|
42
|
-
const observer = new ResizeObserver(() => measure());
|
|
43
|
-
observer.observe(track);
|
|
44
|
-
return () => observer.disconnect();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
function select(id: string): void {
|
|
48
|
-
if (id === value) return;
|
|
49
|
-
value = id;
|
|
50
|
-
onchange?.(id);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function onKeyDown(e: KeyboardEvent): void {
|
|
54
|
-
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
55
|
-
e.preventDefault();
|
|
56
|
-
const at = views.findIndex((view) => view.id === value);
|
|
57
|
-
const next = views[(at + (e.key === "ArrowRight" ? 1 : -1) + views.length) % views.length];
|
|
58
|
-
select(next.id);
|
|
59
|
-
track?.querySelector<HTMLElement>(`[data-view="${CSS.escape(next.id)}"]`)?.focus();
|
|
60
|
-
}
|
|
61
|
-
</script>
|
|
62
|
-
|
|
63
|
-
<div
|
|
64
|
-
bind:this={track}
|
|
65
|
-
role="tablist"
|
|
66
|
-
aria-label={label}
|
|
67
|
-
tabindex="-1"
|
|
68
|
-
onkeydown={onKeyDown}
|
|
69
|
-
class={cn(
|
|
70
|
-
"task-view-switch relative inline-flex shrink-0 items-center gap-0.5 rounded-lg bg-muted/70 p-0.5 dark:bg-muted/40",
|
|
71
|
-
className,
|
|
72
|
-
)}
|
|
73
|
-
>
|
|
74
|
-
{#if pill}
|
|
75
|
-
<span
|
|
76
|
-
aria-hidden="true"
|
|
77
|
-
class="tvs-pill absolute inset-y-0.5 left-0 rounded-md bg-background shadow-sm ring-1 ring-foreground/5 dark:bg-foreground/10"
|
|
78
|
-
class:tvs-pill-placed={placed}
|
|
79
|
-
style:width="{pill.width}px"
|
|
80
|
-
style:transform="translateX({pill.x}px)"
|
|
81
|
-
></span>
|
|
82
|
-
{/if}
|
|
83
|
-
{#each views as view (view.id)}
|
|
84
|
-
{@const active = view.id === value}
|
|
85
|
-
<button
|
|
86
|
-
type="button"
|
|
87
|
-
role="tab"
|
|
88
|
-
data-view={view.id}
|
|
89
|
-
aria-selected={active}
|
|
90
|
-
tabindex={active ? 0 : -1}
|
|
91
|
-
onclick={() => select(view.id)}
|
|
92
|
-
class={cn(
|
|
93
|
-
"relative z-10 inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 text-xs font-medium outline-none transition-colors duration-[var(--duration-quick,150ms)] focus-visible:ring-2 focus-visible:ring-ring/60",
|
|
94
|
-
active ? "text-foreground" : "text-muted-foreground hover:text-foreground",
|
|
95
|
-
)}
|
|
96
|
-
>
|
|
97
|
-
{#if view.icon}
|
|
98
|
-
{@const Icon = view.icon}
|
|
99
|
-
<Icon size={14} class="shrink-0" />
|
|
100
|
-
{/if}
|
|
101
|
-
{view.label}
|
|
102
|
-
</button>
|
|
103
|
-
{/each}
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
<style>
|
|
107
|
-
.tvs-pill-placed {
|
|
108
|
-
transition:
|
|
109
|
-
transform var(--duration-fast, 250ms) var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)),
|
|
110
|
-
width var(--duration-fast, 250ms) var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
@media (prefers-reduced-motion: reduce) {
|
|
114
|
-
.tvs-pill-placed {
|
|
115
|
-
transition: none;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
</style>
|
package/src/tone.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { Tone } from "./status-types.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The severity scale, as Tailwind classes for a chip and as CSS colors for the
|
|
5
|
-
* dot. Tokens are plain CSS variables, so a host app can retheme them. The
|
|
6
|
-
* accent tone reads `--tone-accent` and falls back to `--color-primary`, for a
|
|
7
|
-
* host whose primary is a neutral and whose accent is a brand color.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export const TONE_CLASS: Record<Tone, string> = {
|
|
11
|
-
neutral: "border-border bg-muted/40 text-muted-foreground",
|
|
12
|
-
accent:
|
|
13
|
-
"border-[color-mix(in_oklab,var(--tone-accent,var(--color-primary))_30%,transparent)] bg-[color-mix(in_oklab,var(--tone-accent,var(--color-primary))_10%,transparent)] text-[color:var(--tone-accent,var(--color-primary))]",
|
|
14
|
-
info: "border-info/30 bg-info/10 text-info-800 dark:text-info-400",
|
|
15
|
-
success: "border-success/30 bg-success/10 text-success-800 dark:text-success-400",
|
|
16
|
-
warning: "border-warning/30 bg-warning/10 text-warning-800 dark:text-warning-400",
|
|
17
|
-
destructive: "border-destructive/30 bg-destructive/10 text-destructive",
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/** The same scale as text, for a verdict inside a line of muted facts. */
|
|
21
|
-
export const TONE_TEXT: Record<Tone, string> = {
|
|
22
|
-
neutral: "text-muted-foreground",
|
|
23
|
-
accent: "text-[color:var(--tone-accent,var(--color-primary))]",
|
|
24
|
-
info: "text-info-800 dark:text-info-400",
|
|
25
|
-
success: "text-success-800 dark:text-success-400",
|
|
26
|
-
warning: "text-warning-800 dark:text-warning-400",
|
|
27
|
-
destructive: "text-destructive",
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/** The same scale as icon color. */
|
|
31
|
-
export const TONE_ICON: Record<Tone, string> = {
|
|
32
|
-
neutral: "text-muted-foreground",
|
|
33
|
-
accent: "text-[color:var(--tone-accent,var(--color-primary))]",
|
|
34
|
-
info: "text-info",
|
|
35
|
-
success: "text-success",
|
|
36
|
-
warning: "text-warning",
|
|
37
|
-
destructive: "text-destructive",
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/** The scale as CSS colors, for the SVG dot. */
|
|
41
|
-
export const TONE_COLOR: Record<Tone, string> = {
|
|
42
|
-
neutral: "var(--color-muted-foreground)",
|
|
43
|
-
accent: "var(--tone-accent,var(--color-primary))",
|
|
44
|
-
info: "var(--color-info)",
|
|
45
|
-
success: "var(--color-success)",
|
|
46
|
-
warning: "var(--color-warning)",
|
|
47
|
-
destructive: "var(--color-destructive)",
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* The inset well for a control block inside a panel. A visible step
|
|
52
|
-
* at the same depth, for filter rows and grouped controls.
|
|
53
|
-
*/
|
|
54
|
-
export const insetWellClass = "rounded-lg border border-border bg-muted/40 p-2";
|