@heyramzi/ui 2.8.0 → 2.9.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 +1 -1
- package/README.md +1 -1
- package/package.json +58 -74
- package/src/agents/vendor/draw.ts +4 -4
- package/src/canvas/headless/canvas-cleanup.test.ts +1 -1
- package/src/canvas/ops/ops.ts +1 -1
- package/src/charts/area-chart.svelte +11 -1
- package/src/charts/bar-chart.svelte +18 -3
- package/src/charts/line-chart.svelte +11 -1
- package/src/components/data-table/data-table.ts +4 -5
- package/src/components/data-table/pagination.svelte +5 -3
- package/src/components/docs/on-this-page.svelte +82 -5
- package/src/components/docs/scroll-spy.svelte.ts +6 -1
- package/src/components/list-page/detail-header.svelte +7 -3
- package/src/components/list-page/empty-state.svelte +3 -1
- package/src/components/list-page/filter-popover.svelte +2 -2
- package/src/components/list-page/rail-row.svelte +5 -2
- package/src/components/mark/mark.svelte +2 -1
- package/src/components/section-card/section-card.svelte +3 -1
- package/src/components/section-header/section-header.svelte +5 -5
- package/src/components/segmented/segment.svelte +1 -1
- package/src/components/table-view/table-view.svelte +6 -1
- package/src/components/tags-input/tags-input.svelte +1 -1
- package/src/components/toolbar/toolbar.svelte +1 -1
- package/src/editor/anchors.ts +28 -0
- package/src/editor/doc-blocks.ts +168 -0
- package/src/editor/doc-page.css +389 -0
- package/src/editor/editor.svelte +47 -5
- package/src/editor/extensions.ts +26 -2
- package/src/editor/markdown.ts +27 -2
- package/src/editor/slash-items.ts +18 -0
- package/src/icons/trades.ts +1 -1
- package/src/inbox/README.md +6 -4
- package/src/inbox/inbox.test.ts +1 -1
- package/src/inbox/index.ts +2 -2
- package/src/inbox/logic.ts +29 -5
- package/src/inbox/thread-inbox.svelte +219 -92
- package/src/inbox/types.ts +27 -1
- package/src/lib/initials.test.ts +25 -0
- package/src/lib/initials.ts +29 -0
- package/src/modules.test.ts +4 -64
- package/src/panel/README.md +1 -1
- package/src/record/README.md +1 -1
- package/src/record/info-list.svelte +5 -4
- package/src/record/next-up.svelte +1 -1
- package/src/record/project-list.svelte +1 -1
- package/src/record/steps-rail.svelte +3 -3
- package/src/shadcn/badge/badge.svelte +1 -1
- package/src/shadcn/card/card-title.svelte +1 -5
- package/src/shadcn/empty/empty-description.svelte +1 -1
- package/src/shadcn/progress/progress.svelte +8 -1
- package/src/shadcn/table/table-cell.svelte +10 -2
- package/src/status/README.md +1 -1
- package/src/thread/README.md +2 -0
- package/src/thread/index.ts +9 -1
- package/src/thread/logic.ts +33 -3
- package/src/thread/thread-body.svelte +37 -0
- package/src/thread/thread-events.svelte +30 -0
- package/src/thread/thread.svelte +74 -59
- package/src/thread/types.ts +21 -0
- package/src/{tasks → work}/README.md +11 -8
- package/src/{tasks → work}/detail/task-thread.svelte +8 -15
- package/src/work/docs/doc-editor-page.svelte +194 -0
- package/src/work/docs/doc-menu.svelte +333 -0
- package/src/work/docs/docs-demo.svelte +227 -0
- package/src/work/docs/edited-chip.svelte +125 -0
- package/src/work/docs/edited.ts +54 -0
- package/src/work/docs/index.ts +38 -0
- package/src/work/docs/labels.ts +70 -0
- package/src/work/docs/menu.ts +127 -0
- package/src/work/docs/outline.ts +60 -0
- package/src/{tasks → work}/gantt/gantt-group-row.svelte +1 -1
- package/src/{tasks → work}/inbox/inbox-card.svelte +113 -50
- package/src/work/inbox/inbox-logic.ts +98 -0
- package/src/work/inbox/inbox-trace.svelte +58 -0
- package/src/{tasks → work}/inbox/inbox-types.ts +2 -0
- package/src/{tasks → work}/index.ts +0 -10
- package/src/{tasks → work}/kanban-board.svelte +1 -1
- package/src/{tasks → work}/task-list.svelte +1 -1
- package/src/{tasks → work}/time/time-tracker.svelte +2 -5
- package/src/tasks/inbox/inbox-logic.ts +0 -21
- package/src/tasks/inbox/inbox-trace.svelte +0 -51
- /package/src/{tasks → work}/ambient.d.ts +0 -0
- /package/src/{tasks → work}/board-order.ts +0 -0
- /package/src/{tasks → work}/dates.ts +0 -0
- /package/src/{tasks → work}/demo.svelte +0 -0
- /package/src/{tasks → work}/detail/detail-demo.svelte +0 -0
- /package/src/{tasks → work}/detail/detail-types.ts +0 -0
- /package/src/{tasks → work}/detail/task-chip.svelte +0 -0
- /package/src/{tasks → work}/detail/task-detail.svelte +0 -0
- /package/src/{tasks → work}/detail/task-property.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt-bar.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt-grid.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt-header.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt-task-row.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt.css +0 -0
- /package/src/{tasks → work}/gantt/gantt.test.ts +0 -0
- /package/src/{tasks → work}/gantt/logic.test.ts +0 -0
- /package/src/{tasks → work}/gantt/logic.ts +0 -0
- /package/src/{tasks → work}/inbox/inbox-draft.svelte +0 -0
- /package/src/{tasks → work}/inbox/inbox-statuses.ts +0 -0
- /package/src/{tasks → work}/inbox/index.ts +0 -0
- /package/src/{tasks → work}/kanban-column.svelte +0 -0
- /package/src/{tasks → work}/kanban-drag.ts +0 -0
- /package/src/{tasks → work}/kanban-types.ts +0 -0
- /package/src/{tasks → work}/presets/sales.ts +0 -0
- /package/src/{tasks → work}/reorder.ts +0 -0
- /package/src/{tasks → work}/task-gantt.svelte +0 -0
- /package/src/{tasks → work}/task-types.ts +0 -0
- /package/src/{tasks → work}/time/duration.ts +0 -0
- /package/src/{tasks → work}/time/time-chip.svelte +0 -0
- /package/src/{tasks → work}/time/time-dot.svelte +0 -0
- /package/src/{tasks → work}/time/time-ring.svelte +0 -0
- /package/src/{tasks → work}/time/time-types.ts +0 -0
- /package/src/{tasks → work}/use-kanban-dnd.svelte.ts +0 -0
package/src/editor/editor.svelte
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { Editor } from "@tiptap/core";
|
|
6
6
|
import { untrack } from "svelte";
|
|
7
7
|
import "./content.css";
|
|
8
|
+
import "./doc-page.css";
|
|
8
9
|
import { buildExtensions } from "./extensions.js";
|
|
9
10
|
import { buildSlashItems, type SlashItem } from "./slash-items.js";
|
|
10
11
|
import { parseYoutubeUrl } from "./youtube-url.js";
|
|
@@ -22,6 +23,17 @@
|
|
|
22
23
|
minHeight?: string;
|
|
23
24
|
preset?: "full" | "notes";
|
|
24
25
|
blockIds?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* `box` is the framed editor with a toolbar. `page` is a document: no frame, no toolbar,
|
|
28
|
+
* type set by `.doc-surface` (see doc-page.css), heading anchors on.
|
|
29
|
+
*/
|
|
30
|
+
variant?: "box" | "page";
|
|
31
|
+
/** The word on a heading's hover button, for `page`. */
|
|
32
|
+
copyLinkLabel?: string;
|
|
33
|
+
/** A heading's link was copied. */
|
|
34
|
+
oncopylink?: (url: string, id: string) => void;
|
|
35
|
+
/** The live editor, for a host that needs focus or the document. */
|
|
36
|
+
instance?: Editor | null;
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
// =========================
|
|
@@ -34,6 +46,10 @@
|
|
|
34
46
|
minHeight = "28rem",
|
|
35
47
|
preset = "full",
|
|
36
48
|
blockIds = false,
|
|
49
|
+
variant = "box",
|
|
50
|
+
copyLinkLabel = "Copy link",
|
|
51
|
+
oncopylink,
|
|
52
|
+
instance = $bindable(null),
|
|
37
53
|
}: Props = $props();
|
|
38
54
|
|
|
39
55
|
// =========================
|
|
@@ -59,6 +75,11 @@
|
|
|
59
75
|
// DERIVED
|
|
60
76
|
// =========================
|
|
61
77
|
let editor: Editor | null = $derived(tick >= 0 ? editorInstance : null);
|
|
78
|
+
let isPage = $derived(variant === "page");
|
|
79
|
+
|
|
80
|
+
$effect(() => {
|
|
81
|
+
instance = editorInstance;
|
|
82
|
+
});
|
|
62
83
|
|
|
63
84
|
let slashItems = $derived.by((): SlashItem[] =>
|
|
64
85
|
editor
|
|
@@ -204,7 +225,12 @@
|
|
|
204
225
|
function mount() {
|
|
205
226
|
if (!editorElement || editorInstance) return;
|
|
206
227
|
|
|
207
|
-
const extensions = buildExtensions({
|
|
228
|
+
const extensions = buildExtensions({
|
|
229
|
+
preset,
|
|
230
|
+
placeholder,
|
|
231
|
+
blockIds,
|
|
232
|
+
anchors: variant === "page" ? { label: copyLinkLabel, oncopy: oncopylink } : false,
|
|
233
|
+
});
|
|
208
234
|
|
|
209
235
|
editorInstance = new Editor({
|
|
210
236
|
element: editorElement,
|
|
@@ -309,10 +335,14 @@
|
|
|
309
335
|
<!-- =========================== -->
|
|
310
336
|
<div
|
|
311
337
|
class="editor-wrapper"
|
|
312
|
-
class:focused={editor?.isFocused}
|
|
338
|
+
class:focused={editor?.isFocused && !isPage}
|
|
339
|
+
class:page={isPage}
|
|
340
|
+
class:doc-surface={isPage}
|
|
313
341
|
style="--editor-min-height: {minHeight};"
|
|
314
342
|
>
|
|
315
|
-
|
|
343
|
+
{#if !isPage}
|
|
344
|
+
<EditorToolbar {editor} {tick} {preset} onInsertYoutube={insertYoutubeVideo} />
|
|
345
|
+
{/if}
|
|
316
346
|
|
|
317
347
|
<!-- Editor surface -->
|
|
318
348
|
<div bind:this={editorElement} class="editor-surface tiptap-content"></div>
|
|
@@ -334,7 +364,7 @@
|
|
|
334
364
|
{/if}
|
|
335
365
|
|
|
336
366
|
<!-- Status bar -->
|
|
337
|
-
{#if editor}
|
|
367
|
+
{#if editor && !isPage}
|
|
338
368
|
<div class="status-bar">
|
|
339
369
|
<span class="status-hint">Type <kbd>/</kbd> for commands</span>
|
|
340
370
|
</div>
|
|
@@ -361,12 +391,24 @@
|
|
|
361
391
|
box-shadow: 0 0 0 3px oklch(from var(--color-brand) l c h / 0.06);
|
|
362
392
|
}
|
|
363
393
|
|
|
394
|
+
/* A document page owns its frame, so the editor draws none. */
|
|
395
|
+
.editor-wrapper.page {
|
|
396
|
+
border: 0;
|
|
397
|
+
border-radius: 0;
|
|
398
|
+
background: transparent;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.editor-wrapper.page .editor-surface :global(.tiptap) {
|
|
402
|
+
outline: none;
|
|
403
|
+
min-height: var(--editor-min-height, 28rem);
|
|
404
|
+
}
|
|
405
|
+
|
|
364
406
|
/* ---- EDITOR SURFACE ---- */
|
|
365
407
|
.editor-surface {
|
|
366
408
|
min-height: var(--editor-min-height, 28rem);
|
|
367
409
|
}
|
|
368
410
|
|
|
369
|
-
.editor-surface :global(.tiptap) {
|
|
411
|
+
.editor-wrapper:not(.page) .editor-surface :global(.tiptap) {
|
|
370
412
|
padding: 2rem 2.5rem;
|
|
371
413
|
outline: none;
|
|
372
414
|
min-height: var(--editor-min-height, 28rem);
|
package/src/editor/extensions.ts
CHANGED
|
@@ -3,7 +3,10 @@ import { StarterKit } from "@tiptap/starter-kit";
|
|
|
3
3
|
import { Image } from "@tiptap/extension-image";
|
|
4
4
|
import { Placeholder } from "@tiptap/extension-placeholder";
|
|
5
5
|
import { DragHandle } from "@tiptap/extension-drag-handle";
|
|
6
|
+
import { TaskItem, TaskList } from "@tiptap/extension-list";
|
|
7
|
+
import { Table, TableCell, TableHeader, TableRow } from "@tiptap/extension-table";
|
|
6
8
|
import { BlockIdExtension } from "./block-id.js";
|
|
9
|
+
import { CodeBlockChrome, HeadingAnchors } from "./doc-blocks.js";
|
|
7
10
|
|
|
8
11
|
const CmsBlock = TiptapNode.create({
|
|
9
12
|
name: "cmsBlock",
|
|
@@ -127,17 +130,38 @@ export type ExtensionOptions = {
|
|
|
127
130
|
preset: "full" | "notes";
|
|
128
131
|
placeholder: string;
|
|
129
132
|
blockIds: boolean;
|
|
133
|
+
/** A document page: heading anchors with a hover "Copy link", and the label for it. */
|
|
134
|
+
anchors?: { label: string; oncopy?: (url: string, id: string) => void } | false;
|
|
130
135
|
};
|
|
131
136
|
|
|
132
137
|
/** The extension list for a preset: `full` adds CMS blocks, YouTube, images and H4. */
|
|
133
|
-
export function buildExtensions({
|
|
138
|
+
export function buildExtensions({
|
|
139
|
+
preset,
|
|
140
|
+
placeholder,
|
|
141
|
+
blockIds,
|
|
142
|
+
anchors = false,
|
|
143
|
+
}: ExtensionOptions): Extensions {
|
|
134
144
|
const full = preset === "full";
|
|
135
145
|
return dedupeExtensions([
|
|
136
146
|
StarterKit.configure({
|
|
137
147
|
heading: { levels: full ? [1, 2, 3, 4] : [1, 2, 3] },
|
|
138
148
|
link: { openOnClick: false },
|
|
139
149
|
}),
|
|
140
|
-
...(full
|
|
150
|
+
...(full
|
|
151
|
+
? [
|
|
152
|
+
CmsBlock,
|
|
153
|
+
Youtube,
|
|
154
|
+
Image.configure({ inline: false }),
|
|
155
|
+
TaskList,
|
|
156
|
+
TaskItem.configure({ nested: true }),
|
|
157
|
+
Table.configure({ resizable: false }),
|
|
158
|
+
TableRow,
|
|
159
|
+
TableHeader,
|
|
160
|
+
TableCell,
|
|
161
|
+
CodeBlockChrome,
|
|
162
|
+
]
|
|
163
|
+
: []),
|
|
164
|
+
...(anchors ? [HeadingAnchors.configure(anchors)] : []),
|
|
141
165
|
Placeholder.configure({ placeholder }),
|
|
142
166
|
dragHandle(),
|
|
143
167
|
...(blockIds ? [BlockIdExtension] : []),
|
package/src/editor/markdown.ts
CHANGED
|
@@ -392,6 +392,28 @@ function youtubeToMd(node: TiptapNode): string {
|
|
|
392
392
|
|
|
393
393
|
const dropped: MdWriter = () => "";
|
|
394
394
|
|
|
395
|
+
function taskBullet(item: TiptapNode): string {
|
|
396
|
+
return item.attrs?.checked === true ? "- [x]" : "- [ ]";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// GFM pipe table. The first row is the header; a cell holds its paragraphs on one line.
|
|
400
|
+
function tableToMd(node: TiptapNode): string {
|
|
401
|
+
const rows = (node.content ?? []).map((row) =>
|
|
402
|
+
(row.content ?? []).map((cell) =>
|
|
403
|
+
(cell.content ?? [])
|
|
404
|
+
.map((block) => inlineToMd(block.content ?? []))
|
|
405
|
+
.join(" ")
|
|
406
|
+
.replace(/\|/g, "\\|"),
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
if (rows.length === 0) return "";
|
|
410
|
+
const width = Math.max(...rows.map((row) => row.length));
|
|
411
|
+
const line = (cells: string[]) =>
|
|
412
|
+
`| ${Array.from({ length: width }, (_, i) => cells[i] ?? "").join(" | ")} |`;
|
|
413
|
+
const rule = `| ${Array.from({ length: width }, () => "---").join(" | ")} |`;
|
|
414
|
+
return [line(rows[0]), rule, ...rows.slice(1).map(line)].join("\n");
|
|
415
|
+
}
|
|
416
|
+
|
|
395
417
|
const MD_WRITERS: Record<string, MdWriter> = {
|
|
396
418
|
paragraph: (node) => inlineToMd(node.content ?? []),
|
|
397
419
|
heading: headingToMd,
|
|
@@ -405,10 +427,13 @@ const MD_WRITERS: Record<string, MdWriter> = {
|
|
|
405
427
|
horizontalRule: () => "---",
|
|
406
428
|
hardBreak: () => " \n",
|
|
407
429
|
youtube: youtubeToMd,
|
|
408
|
-
|
|
430
|
+
taskList: (node, listDepth) =>
|
|
431
|
+
(node.content ?? []).map((item) => listItemToMd(item, taskBullet(item), listDepth)).join("\n"),
|
|
432
|
+
taskItem: (node, listDepth) => listItemContentsToMd(node, listDepth),
|
|
433
|
+
table: tableToMd,
|
|
434
|
+
// WHY: images and cmsBlock are out of scope for the notes bridge.
|
|
409
435
|
image: dropped,
|
|
410
436
|
cmsBlock: dropped,
|
|
411
|
-
table: dropped,
|
|
412
437
|
tableRow: dropped,
|
|
413
438
|
tableCell: dropped,
|
|
414
439
|
tableHeader: dropped,
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
ImageSquare,
|
|
13
13
|
Paragraph,
|
|
14
14
|
VideoCamera,
|
|
15
|
+
ListChecks,
|
|
16
|
+
Table,
|
|
15
17
|
} from "phosphor-svelte";
|
|
16
18
|
|
|
17
19
|
export type SlashItem = {
|
|
@@ -62,6 +64,22 @@ export function buildSlashItems(editor: Editor, query: string, options: SlashOpt
|
|
|
62
64
|
icon: ListNumbers,
|
|
63
65
|
command: () => run().toggleOrderedList().run(),
|
|
64
66
|
},
|
|
67
|
+
...(full
|
|
68
|
+
? [
|
|
69
|
+
{
|
|
70
|
+
label: "Checklist",
|
|
71
|
+
description: "Tasks with checkboxes",
|
|
72
|
+
icon: ListChecks,
|
|
73
|
+
command: () => run().toggleTaskList().run(),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
label: "Table",
|
|
77
|
+
description: "Rows and columns",
|
|
78
|
+
icon: Table,
|
|
79
|
+
command: () => run().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
: []),
|
|
65
83
|
{
|
|
66
84
|
label: "Quote",
|
|
67
85
|
description: "Blockquote",
|
package/src/icons/trades.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/*
|
|
18
|
-
* car-garage, hammer / wave-saw-tool, helmet, trowel, grid-dots, leaf, car, crane. Svelte 5 port is ` @tabler/icons-svelte-runes` (runes, not
|
|
18
|
+
* car-garage, hammer / wave-saw-tool, helmet, trowel, grid-dots, leaf, car, crane. Svelte 5 port is ` @tabler/icons-svelte-runes` (runes, not the Svelte 4 ` @tabler/icons-svelte`).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import {
|
package/src/inbox/README.md
CHANGED
|
@@ -21,7 +21,9 @@ One threaded inbox. Entries from any source (a comment, a status change, an assi
|
|
|
21
21
|
- **Grouping.** `threadsOf(entries, { unreadFirst })` keys on `subject.type` plus `subject.id`, sorts threads by their newest entry and entries oldest first. An entry with no subject id is dropped. A malformed timestamp sorts last. Each thread carries `unread`, `latestAt` and `pending`.
|
|
22
22
|
- **Needs you.** A `proposal` entry whose `proposal` (an `InboxItem`) is still open pins to the top of its thread, drawn with `InboxCard`. The "Needs you" filter keeps threads that hold one. Verdicts come out of `onDecide`.
|
|
23
23
|
- **Filters.** By subject type, by actor name and by Needs you. `filterThreads` runs them without the component.
|
|
24
|
-
- **Keys.** `j` and `k` move, `u` marks the thread read (or unread when nothing is unread), `h` opens the snooze row (`1`, `2`, `3` pick a time), and `a`, `d`, `s`, `e` act on the pinned proposal. A key does nothing while focus is in an input, a textarea, a select or an editable element, or while
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
24
|
+
- **Keys.** `j` and `k` move, `u` marks the thread read (or unread when nothing is unread) and Alt+U marks everything read, `h` opens the snooze row (`1`, `2`, `3` pick a time) or brings a snoozed thread back, Shift+S unfollows, Backspace clears the thread and Shift+Backspace clears every read one, and `a`, `d`, `s`, `e` act on the pinned proposal. These are Linear's keys. A key does nothing while focus is in an input, a textarea, a select or an editable element, or while Cmd or Ctrl is held. `inboxKeyOf(event)` is the same check.
|
|
25
|
+
- **Clear.** `onClear(thread)` takes a thread out of the inbox and opens the next one. `onClearRead(threads)` gets every thread with nothing unread. A thread holding a pending proposal is never cleared. The host decides what cleared means (Linear archives, so it can be undone) and stops passing the entries. The buttons and the hint show only when the callback is passed.
|
|
26
|
+
- **Snooze.** `onSnooze({ thread, until })` gets a `Date`: 3 hours on, tomorrow 9:00 or next Monday 9:00 (`snoozeChoices(now)`). The host decides what snoozed means and stops passing the entries. With `onShowSnoozed` the filter offers "Show snoozed": the host sets `showSnoozed`, passes the snoozed entries with `subject.snoozedUntil`, and `onUnsnooze(thread)` brings one back.
|
|
27
|
+
- **Read.** `onRead(thread, read)` is true when the thread had unread entries, so the host marks them all read. `onReadAll()` marks every thread read.
|
|
28
|
+
- **Unfollow.** `onUnfollow(thread)` shows on a thread whose `subject.followed` is set. The host stops new notifications for it.
|
|
29
|
+
- **Words.** `labels` is a partial of `InboxLabels`. Status and assignment lines have `{from}`, `{to}` and `{assignee}` slots. `typeLabels` names the subject types. The `avatar` and `status` snippets let the app draw its own portrait and status badge. Below the pinned card the thread draws through `@heyramzi/ui/thread`: comments as comments, status moves and assignments as event lines. `threadLabels` gives that thread its words, and the `attachments` snippet draws under a comment.
|
package/src/inbox/inbox.test.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
threadsOf,
|
|
10
10
|
} from "./logic.js";
|
|
11
11
|
import type { InboxEntry, InboxSubject } from "./types.js";
|
|
12
|
-
import type { InboxItem } from "../
|
|
12
|
+
import type { InboxItem } from "../work/inbox/inbox-types.js";
|
|
13
13
|
|
|
14
14
|
const tsk7: InboxSubject = { type: "task", id: "7", ref: "TSK-7", title: "Write the pricing page" };
|
|
15
15
|
const tsk9: InboxSubject = { type: "task", id: "9", ref: "TSK-9", title: "Fix the footer" };
|
package/src/inbox/index.ts
CHANGED
|
@@ -27,5 +27,5 @@ export type {
|
|
|
27
27
|
} from "./types.js";
|
|
28
28
|
|
|
29
29
|
// The approval card a pinned proposal is drawn with, and the types its `proposal` field carries.
|
|
30
|
-
export { default as InboxCard } from "../
|
|
31
|
-
export type { InboxItem, InboxVerdict } from "../
|
|
30
|
+
export { default as InboxCard } from "../work/inbox/inbox-card.svelte";
|
|
31
|
+
export type { InboxItem, InboxVerdict } from "../work/inbox/inbox-types.js";
|
package/src/inbox/logic.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { isOpen, isTyping } from "../
|
|
1
|
+
import { isOpen, isTyping } from "../work/inbox/inbox-logic.js";
|
|
2
2
|
import {
|
|
3
3
|
ALL,
|
|
4
4
|
DEFAULT_INBOX_LABELS,
|
|
5
5
|
type InboxEntry,
|
|
6
|
+
type InboxEntryKind,
|
|
6
7
|
type InboxFilter,
|
|
7
8
|
type InboxKey,
|
|
8
9
|
type InboxLabels,
|
|
@@ -97,13 +98,15 @@ export function subjectTypes(threads: InboxThread[]): string[] {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
export function actorNames(threads: InboxThread[]): string[] {
|
|
100
|
-
return [
|
|
101
|
+
return [
|
|
102
|
+
...new Set(threads.flatMap((thread) => thread.entries.map((entry) => entry.actor.name))),
|
|
103
|
+
].filter(Boolean);
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
type KeyEventLike = Pick<
|
|
104
107
|
KeyboardEvent,
|
|
105
108
|
"key" | "metaKey" | "ctrlKey" | "altKey" | "defaultPrevented" | "target"
|
|
106
|
-
|
|
109
|
+
> & { shiftKey?: boolean; code?: string };
|
|
107
110
|
|
|
108
111
|
const KEYS: Record<string, InboxKey> = {
|
|
109
112
|
j: "next",
|
|
@@ -113,13 +116,20 @@ const KEYS: Record<string, InboxKey> = {
|
|
|
113
116
|
a: "approve",
|
|
114
117
|
d: "decline",
|
|
115
118
|
s: "steer",
|
|
119
|
+
S: "unfollow",
|
|
116
120
|
e: "edit",
|
|
117
121
|
};
|
|
118
122
|
|
|
119
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* What a key press asks for, or null when it isn't ours: focus is in a text field, or a modifier
|
|
125
|
+
* other than Linear's Shift+S, Shift+Backspace and Alt+U is held.
|
|
126
|
+
*/
|
|
120
127
|
export function inboxKeyOf(event: KeyEventLike): InboxKey | null {
|
|
121
|
-
if (event.defaultPrevented || event.metaKey || event.ctrlKey
|
|
128
|
+
if (event.defaultPrevented || event.metaKey || event.ctrlKey) return null;
|
|
122
129
|
if (isTyping(event.target)) return null;
|
|
130
|
+
// Option+U on a Mac types a dead key, so the physical key decides.
|
|
131
|
+
if (event.altKey) return event.code === "KeyU" && !event.shiftKey ? "readAll" : null;
|
|
132
|
+
if (event.key === "Backspace") return event.shiftKey ? "clearRead" : "clear";
|
|
123
133
|
return KEYS[event.key] ?? null;
|
|
124
134
|
}
|
|
125
135
|
|
|
@@ -167,3 +177,17 @@ export function entryLine(entry: InboxEntry, labels: InboxLabels): string {
|
|
|
167
177
|
return labels.commented;
|
|
168
178
|
}
|
|
169
179
|
}
|
|
180
|
+
|
|
181
|
+
/** An entry as a sentence: who, then what. With no known actor it starts with the verb, capitalised. */
|
|
182
|
+
export function sentenceOf(entry: InboxEntry, labels: InboxLabels): string {
|
|
183
|
+
const line = entryLine(entry, labels);
|
|
184
|
+
if (entry.actor.name) return `${entry.actor.name} ${line}`;
|
|
185
|
+
return line.charAt(0).toUpperCase() + line.slice(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** What happened to the subject, as against something a person wrote. */
|
|
189
|
+
const EVENT_KINDS: ReadonlySet<InboxEntryKind> = new Set(["status", "assigned", "proposal"]);
|
|
190
|
+
|
|
191
|
+
export function isEvent(entry: InboxEntry): boolean {
|
|
192
|
+
return EVENT_KINDS.has(entry.kind);
|
|
193
|
+
}
|