@heyramzi/ui 2.22.1 → 2.24.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/COMPONENTS.md +3 -6
- package/dist/ai/confirmation/index.js +3 -6
- package/dist/ai/index.js +1 -5
- package/dist/canvas/editor/rebase.js +26 -2
- package/dist/charts/dashboard/index.js +1 -3
- package/dist/charts/primitives/chart-utils.js +14 -0
- package/dist/components/chip/index.js +1 -0
- package/dist/components/language-flag/flags.js +5 -0
- package/dist/components/language-flag/index.js +1 -0
- package/dist/components/list-page/index.js +1 -3
- package/dist/components/work-parts/index.js +1 -1
- package/dist/editor/doc-blocks.js +2 -1
- package/dist/editor/safe-url.js +26 -0
- package/dist/editor/serializer.js +10 -4
- package/dist/editor/slash-query.js +10 -0
- package/dist/effects/animated-beam/path-calculator.svelte.js +13 -6
- package/dist/lib/dates.js +41 -0
- package/dist/lib/panel.js +2 -2
- package/dist/lib/tone.js +13 -0
- package/dist/record/format.js +2 -18
- package/dist/record/logic.js +3 -6
- package/dist/status/index.js +1 -1
- package/dist/triage/index.js +4 -0
- package/dist/triage/types.js +12 -0
- package/dist/work/dates.js +2 -3
- package/dist/work/detail/detail-types.js +0 -8
- package/dist/work/docs/edited.js +8 -7
- package/dist/work/inbox/inbox-logic.js +5 -20
- package/dist/work/index.js +0 -2
- package/dist/work/time/duration.js +2 -3
- package/package.json +7 -21
- package/scripts/sync-shadcn.mjs +4 -6
- package/src/ai/confirmation/Confirmation.svelte +2 -2
- package/src/ai/confirmation/{ConfirmationRequest.svelte → ConfirmationState.svelte} +6 -3
- package/src/ai/confirmation/confirmation-context.svelte.ts +2 -2
- package/src/ai/confirmation/confirmation-parts.test.svelte +17 -0
- package/src/ai/confirmation/confirmation-parts.test.ts +23 -0
- package/src/ai/confirmation/index.ts +6 -21
- package/src/ai/index.ts +3 -13
- package/src/ai/typing-dots/TypingDots.svelte +32 -12
- package/src/ai/typing-dots/typing-dots.test.ts +36 -0
- package/src/animations/modal.css +4 -1
- package/src/canvas/editor/rebase.test.ts +37 -0
- package/src/canvas/editor/rebase.ts +33 -1
- package/src/canvas/ui/canvas-ui.css +44 -1
- package/src/charts/README.md +2 -2
- package/src/charts/area-chart.svelte +5 -12
- package/src/charts/bar-chart.svelte +6 -14
- package/src/charts/chart-tokens.css +19 -0
- package/src/charts/dashboard/data-table.svelte +21 -21
- package/src/charts/dashboard/index.ts +1 -3
- package/src/charts/dashboard/rating-chip.svelte +9 -11
- package/src/charts/dashboard/stat-tile.svelte +4 -5
- package/src/charts/donut-chart.svelte +1 -1
- package/src/charts/gauge-chart.svelte +1 -1
- package/src/charts/line-chart.svelte +5 -12
- package/src/charts/number-chart.svelte +1 -1
- package/src/charts/primitives/chart-utils.test.ts +17 -0
- package/src/charts/primitives/chart-utils.ts +16 -0
- package/src/charts/ring-chart.svelte +2 -2
- package/src/charts/sparkline.svelte +1 -1
- package/src/charts/stat-card.svelte +1 -1
- package/src/charts/time-series-chart.svelte +9 -5
- package/src/components/chip/count-chip.svelte +35 -0
- package/src/components/chip/index.ts +1 -0
- package/src/components/data-table/data-table.ts +1 -1
- package/src/components/data-table/row-context-menu.svelte +3 -4
- package/src/components/data-table/sortable-head.svelte +45 -10
- package/src/components/language-flag/flags.ts +5 -0
- package/src/components/language-flag/index.ts +1 -0
- package/src/components/language-flag/language-flag.svelte +3 -11
- package/src/components/list-page/empty-state.svelte +90 -12
- package/src/components/list-page/filter-popover.svelte +2 -5
- package/src/components/list-page/grouped-list.svelte +2 -4
- package/src/components/list-page/index.ts +1 -3
- package/src/components/section-header/section-header.svelte +2 -6
- package/src/components/segmented/segment.svelte +13 -19
- package/src/components/segmented/segmented-filter.svelte +1 -2
- package/src/components/table-view/table-view.svelte +31 -45
- package/src/components/table-view/table-view.test.ts +6 -3
- package/src/components/work-parts/group-head.svelte +1 -1
- package/src/components/work-parts/index.ts +1 -1
- package/src/editor/doc-blocks.ts +2 -1
- package/src/editor/editor.svelte +7 -5
- package/src/editor/safe-url.ts +32 -0
- package/src/editor/serializer.test.ts +58 -0
- package/src/editor/serializer.ts +9 -6
- package/src/editor/slash-query.test.ts +19 -0
- package/src/editor/slash-query.ts +11 -0
- package/src/effects/animated-beam/path-calculator.svelte.ts +19 -5
- package/src/effects/marquee/Marquee.css +6 -3
- package/src/effects/marquee/Marquee.svelte +6 -2
- package/src/inbox/thread-inbox.svelte +6 -4
- package/src/lib/dates.test.ts +63 -0
- package/src/lib/dates.ts +63 -0
- package/src/lib/panel.svelte +116 -0
- package/src/lib/panel.ts +2 -2
- package/src/lib/tone.ts +15 -0
- package/src/modules.test.ts +13 -1
- package/src/panel/side-panel.svelte +4 -4
- package/src/record/format.ts +3 -15
- package/src/record/logic.ts +2 -6
- package/src/record/record-stats.svelte +4 -3
- package/src/record/steps-rail.svelte +7 -4
- package/src/shadcn/alert-dialog/alert-dialog-overlay.svelte +1 -1
- package/src/shadcn/button/button.svelte +1 -1
- package/src/shadcn/button/button.test.ts +2 -2
- package/src/shadcn/command/command-dialog.svelte +5 -10
- package/src/shadcn/dialog/dialog-overlay.svelte +1 -1
- package/src/shadcn/input/input.svelte +2 -2
- package/src/shadcn/sheet/sheet-overlay.svelte +1 -1
- package/src/status/index.ts +1 -1
- package/src/status/status-pill.svelte +2 -8
- package/src/thread/thread.svelte +3 -6
- package/src/tokens/craft.css +30 -0
- package/src/triage/README.md +28 -0
- package/src/triage/index.ts +10 -0
- package/src/triage/triage-plan.svelte +132 -0
- package/src/triage/triage-queue.svelte +48 -0
- package/src/triage/triage-task.svelte +95 -0
- package/src/triage/types.ts +35 -0
- package/src/work/README.md +0 -1
- package/src/work/dates.ts +3 -3
- package/src/work/demo.svelte +9 -1
- package/src/work/detail/detail-demo.svelte +30 -27
- package/src/work/detail/detail-types.ts +1 -29
- package/src/work/detail/task-detail.svelte +18 -17
- package/src/work/docs/doc-menu.svelte +2 -1
- package/src/work/docs/edited.ts +4 -7
- package/src/work/inbox/inbox-card.svelte +4 -9
- package/src/work/inbox/inbox-logic.ts +4 -16
- package/src/work/index.ts +1 -3
- package/src/work/kanban-column.svelte +5 -3
- package/src/work/time/duration.ts +2 -3
- package/src/work/time/time-chip.svelte +1 -3
- package/dist/ai/thinking-dots/index.js +0 -3
- package/dist/components/icon/index.js +0 -1
- package/dist/components/locale-switch/index.js +0 -1
- package/dist/icons/trades.js +0 -49
- package/dist/status/pill-tint.js +0 -13
- package/dist/work/presets/sales.js +0 -39
- package/src/ai/confirmation/ConfirmationAccepted.svelte +0 -23
- package/src/ai/confirmation/ConfirmationFailed.svelte +0 -23
- package/src/ai/confirmation/ConfirmationRejected.svelte +0 -23
- package/src/ai/thinking-dots/ThinkingDots.svelte +0 -42
- package/src/ai/thinking-dots/index.ts +0 -5
- package/src/charts/dashboard/dashboard-empty.svelte +0 -55
- package/src/charts/dashboard/data-panel.svelte +0 -81
- package/src/charts/dashboard/data-sort-head.svelte +0 -102
- package/src/components/icon/icon.svelte +0 -23
- package/src/components/icon/index.ts +0 -1
- package/src/components/list-page/detail-empty.svelte +0 -45
- package/src/components/list-page/detail-panel.svelte +0 -82
- package/src/components/list-page/rail-empty.svelte +0 -44
- package/src/components/locale-switch/index.ts +0 -1
- package/src/components/locale-switch/locale-switch.svelte +0 -59
- package/src/components/work-parts/count-chip.svelte +0 -20
- package/src/icons/TradeIcon.svelte +0 -16
- package/src/icons/trades.ts +0 -89
- package/src/status/pill-tint.ts +0 -16
- package/src/work/detail/task-property.svelte +0 -17
- package/src/work/detail/task-thread.svelte +0 -221
- package/src/work/presets/sales.ts +0 -16
|
@@ -40,9 +40,7 @@
|
|
|
40
40
|
"rounded-2xl! top-[max(0.5rem,env(safe-area-inset-top))] right-2 bottom-auto left-2 w-auto max-h-[70svh] translate-y-0 gap-0 overflow-hidden p-0 sm:p-0",
|
|
41
41
|
// WHY no border and no card background here: the palette is a floating
|
|
42
42
|
|
|
43
|
-
// object, so its edge is a lit rim and a cast shadow rather than a drawn line, and the surface
|
|
44
|
-
|
|
45
|
-
// can read the dimmed page through it.
|
|
43
|
+
// object, so its edge is a lit rim and a cast shadow rather than a drawn line, and the surface belongs to the Command root beneath.
|
|
46
44
|
"border-0 bg-transparent shadow-edge-overlay",
|
|
47
45
|
// WHY a third of the way down rather than centred: the eye is already at
|
|
48
46
|
|
|
@@ -55,12 +53,9 @@
|
|
|
55
53
|
{showCloseButton}
|
|
56
54
|
{portalProps}
|
|
57
55
|
>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class="bg-popover supports-backdrop-filter:bg-popover/94 supports-backdrop-filter:backdrop-blur-2xl"
|
|
63
|
-
{children}
|
|
64
|
-
/>
|
|
56
|
+
<!-- WHY the root's opaque `bg-popover`, no glass: `.t-modal` on the content sets `will-change:
|
|
57
|
+
opacity`, which makes the content the backdrop root, so a blur here read only the empty content
|
|
58
|
+
box, never the page. Its 94% surface let page text show through the rows (light, 390px). -->
|
|
59
|
+
<Command {...restProps} bind:value bind:ref {children} />
|
|
65
60
|
</Dialog.Content>
|
|
66
61
|
</Dialog.Root>
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
bind:this={ref}
|
|
26
26
|
data-slot={dataSlot}
|
|
27
27
|
class={cn(
|
|
28
|
-
"dark:bg-input/30 border-input enabled:hover:border-foreground/20 focus-visible:border-ring focus-visible:shadow-(--ring-focus) aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-
|
|
28
|
+
"dark:bg-input/30 border-input enabled:hover:border-foreground/20 focus-visible:border-ring focus-visible:shadow-(--ring-focus) aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-(--control-height) rounded-lg border bg-transparent px-2.5 py-1 text-base shadow-field transition-[color,background-color,border-color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] file:h-6 file:text-sm file:font-medium aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
29
29
|
className,
|
|
30
30
|
)}
|
|
31
31
|
type="file"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
bind:this={ref}
|
|
39
39
|
data-slot={dataSlot}
|
|
40
40
|
class={cn(
|
|
41
|
-
"dark:bg-input/30 border-input enabled:hover:border-foreground/20 focus-visible:border-ring focus-visible:shadow-(--ring-focus) aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-
|
|
41
|
+
"dark:bg-input/30 border-input enabled:hover:border-foreground/20 focus-visible:border-ring focus-visible:shadow-(--ring-focus) aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-(--control-height) rounded-lg border bg-transparent px-2.5 py-1 text-base shadow-field transition-[color,background-color,border-color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] file:h-6 file:text-sm file:font-medium aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
42
42
|
className,
|
|
43
43
|
)}
|
|
44
44
|
{type}
|
package/src/status/index.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
import type { Tone } from "../lib/tone.js";
|
|
3
|
-
import { PILL_TINT } from "./pill-tint.js";
|
|
4
|
-
|
|
5
|
-
export { PILL_TINT };
|
|
6
|
-
</script>
|
|
7
|
-
|
|
8
1
|
<script lang="ts">
|
|
9
2
|
import type { Snippet } from "svelte";
|
|
10
3
|
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
import { TONE_TINT, type Tone } from "../lib/tone.js";
|
|
11
5
|
import { cn } from "../lib/utils.js";
|
|
12
6
|
|
|
13
7
|
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "class" | "children"> {
|
|
@@ -30,7 +24,7 @@
|
|
|
30
24
|
"transition-[background-color,color] duration-quick ease-smooth-out motion-reduce:transition-none",
|
|
31
25
|
"[&>svg]:size-3 [&>svg]:shrink-0",
|
|
32
26
|
icon && "pl-1.5",
|
|
33
|
-
|
|
27
|
+
TONE_TINT[tone],
|
|
34
28
|
className,
|
|
35
29
|
)}
|
|
36
30
|
{...restProps}
|
package/src/thread/thread.svelte
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import { ArrowBendDownRight, Check, MapPin, PencilSimple, Trash } from "phosphor-svelte";
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { shortDate } from "../lib/dates.js";
|
|
5
6
|
import { initial } from "../lib/initials.js";
|
|
6
7
|
import { AgentAvatar } from "../agents/index.js";
|
|
7
8
|
import RichRenderer from "../editor/renderer.svelte";
|
|
@@ -64,8 +65,8 @@
|
|
|
64
65
|
avatar,
|
|
65
66
|
attachments,
|
|
66
67
|
anchorChip,
|
|
67
|
-
when = (comment) => shortDate(comment.createdAt),
|
|
68
|
-
whenEvent = (event) => shortDate(event.at),
|
|
68
|
+
when = (comment) => shortDate(comment.createdAt, { locale }),
|
|
69
|
+
whenEvent = (event) => shortDate(event.at, { locale }),
|
|
69
70
|
onPost,
|
|
70
71
|
onEdit,
|
|
71
72
|
onDelete,
|
|
@@ -84,10 +85,6 @@
|
|
|
84
85
|
let shown = $derived(showResolved ? nodes : nodes.filter((node) => !node.comment.resolved));
|
|
85
86
|
let items = $derived(interleave(shown, events));
|
|
86
87
|
|
|
87
|
-
function shortDate(at: string): string {
|
|
88
|
-
return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric" }).format(new Date(at));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
88
|
let draft = $state("");
|
|
92
89
|
let replyTo = $state<string | null>(null);
|
|
93
90
|
let replyDraft = $state("");
|
package/src/tokens/craft.css
CHANGED
|
@@ -23,6 +23,21 @@
|
|
|
23
23
|
--text-2xs: 0.6875rem; /* a count, a kbd hint, a meta line under a tile; the floor */
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/* ── Control height ──────────────────────────────────────────
|
|
27
|
+
One height for every control that sits in a row together: an input, a
|
|
28
|
+
default button, a select trigger. 32px on a desktop row, 44px under a finger
|
|
29
|
+
(DESIGN.md rule 12), so a search field and its button always line up. Write
|
|
30
|
+
it `h-(--control-height)`. */
|
|
31
|
+
:root {
|
|
32
|
+
--control-height: 2rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@media (any-pointer: coarse) {
|
|
36
|
+
:root {
|
|
37
|
+
--control-height: 2.75rem;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
/* ── Elevation ───────────────────────────────────────────────
|
|
27
42
|
`--elev-card` and its hover carry their own hairline, so a box that also
|
|
28
43
|
draws `border` gets a 2px edge. The other steps carry none. */
|
|
@@ -88,6 +103,21 @@
|
|
|
88
103
|
--elev-field: inset 0 1px 2px color-mix(in oklab, black 40%, transparent);
|
|
89
104
|
}
|
|
90
105
|
|
|
106
|
+
/* ── Scrim ───────────────────────────────────────────────────
|
|
107
|
+
The dim behind a dialog, an alert dialog and a sheet. A dim with a 2px blur:
|
|
108
|
+
the page behind stays legible as the page you came from, so the overlay reads
|
|
109
|
+
as on top of it, not instead of it. Dark needs a deeper dim, since 32% black
|
|
110
|
+
on an already dark page barely moves it. */
|
|
111
|
+
:root {
|
|
112
|
+
--scrim: color-mix(in oklab, var(--elev-tint) 32%, transparent);
|
|
113
|
+
--scrim-blur: var(--blur-small);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* After `:root`, since `.dark` sits on the same element at the same specificity. */
|
|
117
|
+
.dark {
|
|
118
|
+
--scrim: color-mix(in oklab, black 55%, transparent);
|
|
119
|
+
}
|
|
120
|
+
|
|
91
121
|
/* ── Focus ───────────────────────────────────────────────────
|
|
92
122
|
One variable, so every control focuses the same way: a solid accent edge,
|
|
93
123
|
then a soft halo. */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @heyramzi/ui/triage
|
|
2
|
+
|
|
3
|
+
The three pieces of an email triage screen. They never fetch: you pass the words and callbacks, and they hand back what the person did.
|
|
4
|
+
|
|
5
|
+
- `TriagePlan` is one email with the plan the model proposes ("Create a task in Clients") and three ways out: approve, type another task, dismiss.
|
|
6
|
+
- `TriageTask` is one settled email: what happened, the task it became, when, and Undo when the host passes `onUndo`.
|
|
7
|
+
- `TriageQueue` is the list around them, with the empty state. `kind="plans"` stacks cards, `kind="tasks"` draws one divided list.
|
|
8
|
+
|
|
9
|
+
```svelte
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
import { TriageQueue, TriagePlan } from "@heyramzi/ui/triage";
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<TriageQueue items={emails} key={(e) => e.id} empty="Nothing to review.">
|
|
15
|
+
{#snippet row(e)}
|
|
16
|
+
<TriagePlan
|
|
17
|
+
sender={e.from}
|
|
18
|
+
subject={e.subject}
|
|
19
|
+
plan={e.plan}
|
|
20
|
+
onApprove={() => approve(e.id)}
|
|
21
|
+
onPick={(task) => approve(e.id, task)}
|
|
22
|
+
onDismiss={() => dismiss(e.id)}
|
|
23
|
+
/>
|
|
24
|
+
{/snippet}
|
|
25
|
+
</TriageQueue>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`labels` on each piece is a partial, so a host translates it. A callback may return a promise: the buttons stay disabled until it settles.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { default as TriagePlan } from "./triage-plan.svelte";
|
|
2
|
+
export { default as TriageTask } from "./triage-task.svelte";
|
|
3
|
+
export { default as TriageQueue } from "./triage-queue.svelte";
|
|
4
|
+
export { DEFAULT_TRIAGE_PLAN_LABELS, DEFAULT_TRIAGE_TASK_LABELS } from "./types.js";
|
|
5
|
+
export type {
|
|
6
|
+
TriageOutcome,
|
|
7
|
+
TriagePlanLabels,
|
|
8
|
+
TriageTaskLabels,
|
|
9
|
+
TriageTaskLink,
|
|
10
|
+
} from "./types.js";
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from "../shadcn/button/index.js";
|
|
3
|
+
import { Input } from "../shadcn/input/index.js";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { DEFAULT_TRIAGE_PLAN_LABELS, type TriagePlanLabels } from "./types.js";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Who sent the email. */
|
|
9
|
+
sender: string;
|
|
10
|
+
subject: string;
|
|
11
|
+
/** The first lines of the body. */
|
|
12
|
+
snippet?: string;
|
|
13
|
+
/** When it arrived, as text the host already formatted. */
|
|
14
|
+
received?: string | null;
|
|
15
|
+
/** The ISO time behind `received`, for the `time` element. */
|
|
16
|
+
receivedAt?: string | null;
|
|
17
|
+
/** What the plan will do, as one sentence. Without it the card only offers to dismiss. */
|
|
18
|
+
plan?: string | null;
|
|
19
|
+
/** Why the model wasn't sure. */
|
|
20
|
+
reason?: string | null;
|
|
21
|
+
/** A failed attempt, shown on the card. */
|
|
22
|
+
error?: string | null;
|
|
23
|
+
labels?: Partial<TriagePlanLabels>;
|
|
24
|
+
/** Run the plan. The buttons stay disabled until it settles. */
|
|
25
|
+
onApprove: () => void | Promise<void>;
|
|
26
|
+
/** Run a task the person typed instead of the plan. */
|
|
27
|
+
onPick: (task: string) => void | Promise<void>;
|
|
28
|
+
onDismiss: () => void | Promise<void>;
|
|
29
|
+
class?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
sender,
|
|
34
|
+
subject,
|
|
35
|
+
snippet,
|
|
36
|
+
received,
|
|
37
|
+
receivedAt,
|
|
38
|
+
plan = null,
|
|
39
|
+
reason,
|
|
40
|
+
error,
|
|
41
|
+
labels,
|
|
42
|
+
onApprove,
|
|
43
|
+
onPick,
|
|
44
|
+
onDismiss,
|
|
45
|
+
class: className,
|
|
46
|
+
}: Props = $props();
|
|
47
|
+
|
|
48
|
+
const id = $props.id();
|
|
49
|
+
let words = $derived({ ...DEFAULT_TRIAGE_PLAN_LABELS, ...labels });
|
|
50
|
+
let picking = $state(false);
|
|
51
|
+
let busy = $state(false);
|
|
52
|
+
let task = $state("");
|
|
53
|
+
|
|
54
|
+
async function run(job: () => void | Promise<void>) {
|
|
55
|
+
busy = true;
|
|
56
|
+
try {
|
|
57
|
+
await job();
|
|
58
|
+
} finally {
|
|
59
|
+
busy = false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<article class={cn("bg-card border-border rounded-xl border p-5", className)}>
|
|
65
|
+
<div class="flex items-start justify-between gap-3">
|
|
66
|
+
<h2 class="text-foreground min-w-0 text-sm font-semibold [overflow-wrap:anywhere]">
|
|
67
|
+
{sender}: “{subject}”
|
|
68
|
+
</h2>
|
|
69
|
+
{#if received}
|
|
70
|
+
<time class="text-muted-foreground shrink-0 text-xs" datetime={receivedAt ?? undefined}>
|
|
71
|
+
{received}
|
|
72
|
+
</time>
|
|
73
|
+
{/if}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{#if snippet}
|
|
77
|
+
<p class="text-muted-foreground mt-2 line-clamp-2 text-sm [overflow-wrap:anywhere]">
|
|
78
|
+
{snippet}
|
|
79
|
+
</p>
|
|
80
|
+
{/if}
|
|
81
|
+
|
|
82
|
+
{#if plan}
|
|
83
|
+
<p
|
|
84
|
+
class="bg-primary/10 text-primary mt-4 inline-block max-w-full rounded-md px-2.5 py-1 text-xs font-medium [overflow-wrap:anywhere]"
|
|
85
|
+
>
|
|
86
|
+
{plan}
|
|
87
|
+
</p>
|
|
88
|
+
{/if}
|
|
89
|
+
{#if reason}
|
|
90
|
+
<p class="text-muted-foreground mt-2 text-xs [overflow-wrap:anywhere]">{reason}</p>
|
|
91
|
+
{/if}
|
|
92
|
+
|
|
93
|
+
{#if error}
|
|
94
|
+
<p
|
|
95
|
+
class="bg-destructive/10 text-destructive mt-3 rounded-lg px-3 py-2 text-sm [overflow-wrap:anywhere]"
|
|
96
|
+
role="alert"
|
|
97
|
+
>
|
|
98
|
+
{error}
|
|
99
|
+
</p>
|
|
100
|
+
{/if}
|
|
101
|
+
|
|
102
|
+
{#if picking}
|
|
103
|
+
<form
|
|
104
|
+
class="mt-4 space-y-2"
|
|
105
|
+
onsubmit={(event) => {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
run(() => onPick(task.trim()));
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
<label for="{id}-task" class="text-foreground block text-sm font-medium">
|
|
111
|
+
{words.taskLabel}
|
|
112
|
+
</label>
|
|
113
|
+
<Input id="{id}-task" bind:value={task} required autocomplete="off" />
|
|
114
|
+
<div class="flex flex-wrap gap-2">
|
|
115
|
+
<Button type="submit" disabled={busy}>{words.useTask}</Button>
|
|
116
|
+
<Button type="button" variant="outline" onclick={() => (picking = false)}>
|
|
117
|
+
{words.cancel}
|
|
118
|
+
</Button>
|
|
119
|
+
</div>
|
|
120
|
+
</form>
|
|
121
|
+
{:else}
|
|
122
|
+
<div class="mt-4 flex flex-wrap gap-2">
|
|
123
|
+
{#if plan}
|
|
124
|
+
<Button disabled={busy} onclick={() => run(onApprove)}>{words.approve}</Button>
|
|
125
|
+
<Button variant="outline" onclick={() => (picking = true)}>{words.pick}</Button>
|
|
126
|
+
{/if}
|
|
127
|
+
<Button variant="ghost" disabled={busy} onclick={() => run(onDismiss)}>
|
|
128
|
+
{words.dismiss}
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
{/if}
|
|
132
|
+
</article>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import { cn } from "../lib/utils.js";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
items: readonly T[];
|
|
7
|
+
/** The key a row is tracked by. */
|
|
8
|
+
key: (item: T) => string;
|
|
9
|
+
/**
|
|
10
|
+
* `plans` stacks cards that need a decision. `tasks` is one divided list of what was
|
|
11
|
+
* decided.
|
|
12
|
+
*/
|
|
13
|
+
kind?: "plans" | "tasks";
|
|
14
|
+
/** One row: a `TriagePlan` for `plans`, a `TriageTask` for `tasks`. */
|
|
15
|
+
row: Snippet<[T]>;
|
|
16
|
+
/** What the person reads when nothing is waiting. */
|
|
17
|
+
empty: string;
|
|
18
|
+
/** The glyph above the empty text. */
|
|
19
|
+
icon?: Snippet;
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let { items, key, kind = "plans", row, empty, icon, class: className }: Props = $props();
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
{#if items.length === 0}
|
|
27
|
+
<div
|
|
28
|
+
class={cn(
|
|
29
|
+
"border-border flex flex-col items-center gap-3 rounded-xl border border-dashed px-6 py-14 text-center",
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
<span class="text-muted-foreground" aria-hidden="true">{@render icon?.()}</span>
|
|
34
|
+
<p class="text-muted-foreground text-sm">{empty}</p>
|
|
35
|
+
</div>
|
|
36
|
+
{:else if kind === "plans"}
|
|
37
|
+
<div class={cn("space-y-4", className)}>
|
|
38
|
+
{#each items as item (key(item))}
|
|
39
|
+
{@render row(item)}
|
|
40
|
+
{/each}
|
|
41
|
+
</div>
|
|
42
|
+
{:else}
|
|
43
|
+
<ul class={cn("bg-card border-border divide-border divide-y rounded-xl border", className)}>
|
|
44
|
+
{#each items as item (key(item))}
|
|
45
|
+
{@render row(item)}
|
|
46
|
+
{/each}
|
|
47
|
+
</ul>
|
|
48
|
+
{/if}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from "../shadcn/button/index.js";
|
|
3
|
+
import StatusBadge from "../status/status-badge.svelte";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_TRIAGE_TASK_LABELS,
|
|
7
|
+
type TriageOutcome,
|
|
8
|
+
type TriageTaskLabels,
|
|
9
|
+
type TriageTaskLink,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
sender: string;
|
|
14
|
+
subject: string;
|
|
15
|
+
/** What happened to the email. */
|
|
16
|
+
outcome: TriageOutcome;
|
|
17
|
+
/** The task it became. */
|
|
18
|
+
task?: TriageTaskLink | null;
|
|
19
|
+
/** When it settled, as text the host already formatted. */
|
|
20
|
+
when: string;
|
|
21
|
+
whenAt?: string | null;
|
|
22
|
+
/** Why it failed. */
|
|
23
|
+
detail?: string | null;
|
|
24
|
+
error?: string | null;
|
|
25
|
+
labels?: Partial<TriageTaskLabels>;
|
|
26
|
+
/** Takes the change back. The button shows only when this is passed. */
|
|
27
|
+
onUndo?: () => void | Promise<void>;
|
|
28
|
+
class?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
sender,
|
|
33
|
+
subject,
|
|
34
|
+
outcome,
|
|
35
|
+
task,
|
|
36
|
+
when,
|
|
37
|
+
whenAt,
|
|
38
|
+
detail,
|
|
39
|
+
error,
|
|
40
|
+
labels,
|
|
41
|
+
onUndo,
|
|
42
|
+
class: className,
|
|
43
|
+
}: Props = $props();
|
|
44
|
+
|
|
45
|
+
let words = $derived({ ...DEFAULT_TRIAGE_TASK_LABELS, ...labels });
|
|
46
|
+
let busy = $state(false);
|
|
47
|
+
|
|
48
|
+
async function undo() {
|
|
49
|
+
busy = true;
|
|
50
|
+
try {
|
|
51
|
+
await onUndo?.();
|
|
52
|
+
} finally {
|
|
53
|
+
busy = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<li
|
|
59
|
+
class={cn(
|
|
60
|
+
"flex flex-col gap-3 px-5 py-4 sm:flex-row sm:items-center sm:justify-between",
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
<div class="min-w-0 space-y-1">
|
|
65
|
+
<p class="text-foreground text-sm font-medium [overflow-wrap:anywhere]">
|
|
66
|
+
{sender}: {subject}
|
|
67
|
+
</p>
|
|
68
|
+
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
69
|
+
<StatusBadge tone={outcome.tone} capitalize={false}>{outcome.label}</StatusBadge>
|
|
70
|
+
{#if task}
|
|
71
|
+
<a
|
|
72
|
+
href={task.url}
|
|
73
|
+
target="_blank"
|
|
74
|
+
rel="noreferrer"
|
|
75
|
+
class="text-primary min-w-0 text-xs [overflow-wrap:anywhere] hover:underline"
|
|
76
|
+
>
|
|
77
|
+
{task.name ?? words.openTask}
|
|
78
|
+
</a>
|
|
79
|
+
{/if}
|
|
80
|
+
<time class="text-muted-foreground text-xs" datetime={whenAt ?? undefined}>{when}</time>
|
|
81
|
+
</div>
|
|
82
|
+
{#if detail}
|
|
83
|
+
<p class="text-muted-foreground text-xs [overflow-wrap:anywhere]">{detail}</p>
|
|
84
|
+
{/if}
|
|
85
|
+
{#if error}
|
|
86
|
+
<p class="text-destructive text-xs [overflow-wrap:anywhere]" role="alert">{error}</p>
|
|
87
|
+
{/if}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{#if onUndo}
|
|
91
|
+
<Button variant="outline" class="shrink-0" disabled={busy} onclick={undo}>
|
|
92
|
+
{words.undo}
|
|
93
|
+
</Button>
|
|
94
|
+
{/if}
|
|
95
|
+
</li>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Tone } from "../lib/tone.js";
|
|
2
|
+
|
|
3
|
+
/** The words a plan card shows. Pass a partial to translate or reword. */
|
|
4
|
+
export type TriagePlanLabels = {
|
|
5
|
+
approve: string;
|
|
6
|
+
pick: string;
|
|
7
|
+
dismiss: string;
|
|
8
|
+
/** The label over the field where the person types the task to use instead. */
|
|
9
|
+
taskLabel: string;
|
|
10
|
+
useTask: string;
|
|
11
|
+
cancel: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_TRIAGE_PLAN_LABELS: TriagePlanLabels = {
|
|
15
|
+
approve: "Approve",
|
|
16
|
+
pick: "Pick another task",
|
|
17
|
+
dismiss: "Dismiss",
|
|
18
|
+
taskLabel: "Task",
|
|
19
|
+
useTask: "Use this task",
|
|
20
|
+
cancel: "Cancel",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** The words a task row shows. */
|
|
24
|
+
export type TriageTaskLabels = { undo: string; openTask: string };
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_TRIAGE_TASK_LABELS: TriageTaskLabels = {
|
|
27
|
+
undo: "Undo",
|
|
28
|
+
openTask: "Open task",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** What happened to a settled email: the chip's words and its tone. */
|
|
32
|
+
export type TriageOutcome = { label: string; tone: Tone };
|
|
33
|
+
|
|
34
|
+
/** The task the email ended up as, when there is one. */
|
|
35
|
+
export type TriageTaskLink = { name?: string | null; url: string };
|
package/src/work/README.md
CHANGED
|
@@ -286,7 +286,6 @@ exported helpers.
|
|
|
286
286
|
A badge also takes its text as children, for a caller that already has the word.
|
|
287
287
|
- `defineStatuses`: builds a status registry from a plain map.
|
|
288
288
|
- `SidePanel`: a sheet for one record, above.
|
|
289
|
-
- `salesStatuses`: an example pipeline map.
|
|
290
289
|
|
|
291
290
|
## Styling
|
|
292
291
|
|
package/src/work/dates.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Dates are plain "YYYY-MM-DD" strings, counted as whole UTC days. A timeline
|
|
2
2
|
// only needs days, and day numbers dodge every timezone and DST edge.
|
|
3
|
+
import { localDay } from "../lib/dates.js";
|
|
4
|
+
|
|
3
5
|
const DAY_MS = 86_400_000;
|
|
4
6
|
|
|
5
7
|
/** Days since 1970-01-01 for an ISO date. Anything after the date part is ignored. */
|
|
@@ -20,9 +22,7 @@ export function addDays(iso: string, days: number): string {
|
|
|
20
22
|
|
|
21
23
|
/** Today in the viewer's own timezone, as an ISO date. */
|
|
22
24
|
export function todayIso(): string {
|
|
23
|
-
|
|
24
|
-
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60_000);
|
|
25
|
-
return local.toISOString().slice(0, 10);
|
|
25
|
+
return localDay();
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** 0 is Sunday, as in `Date.getUTCDay`. */
|
package/src/work/demo.svelte
CHANGED
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
import StatusDot from "../status/status-dot.svelte";
|
|
21
21
|
import { defineStatuses } from "../status/define-statuses.js";
|
|
22
22
|
import { addDays, dayNumber, todayIso } from "./dates.js";
|
|
23
|
-
import { salesStatuses } from "./presets/sales.js";
|
|
24
23
|
import type { KanbanMove } from "./kanban-types.js";
|
|
25
24
|
import type { GanttChange } from "./task-types.js";
|
|
26
25
|
import TimeTracker from "./time/time-tracker.svelte";
|
|
@@ -48,6 +47,15 @@
|
|
|
48
47
|
done: { label: "Done", tone: "success", variant: "completed" },
|
|
49
48
|
});
|
|
50
49
|
|
|
50
|
+
const salesStatuses = defineStatuses({
|
|
51
|
+
new: { label: "New", tone: "accent", variant: "open" },
|
|
52
|
+
contacted: { label: "Contacted", tone: "info", variant: "open" },
|
|
53
|
+
proposal: { label: "Proposal", tone: "accent", variant: "progress", progress: 0.25 },
|
|
54
|
+
negotiation: { label: "Negotiation", tone: "warning", variant: "progress", progress: 0.75 },
|
|
55
|
+
won: { label: "Won", tone: "success", variant: "completed" },
|
|
56
|
+
lost: { label: "Lost", tone: "destructive", variant: "lost" },
|
|
57
|
+
});
|
|
58
|
+
|
|
51
59
|
const columns = [
|
|
52
60
|
{ id: "todo", label: "To do", hue: "var(--color-muted-foreground)" },
|
|
53
61
|
{ id: "doing", label: "In progress", hue: "var(--color-info)" },
|