@heyramzi/ui 2.7.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/ai/generation-progress/GenerationProgress.svelte +2 -2
- package/src/animations/README.md +1 -1
- package/src/animations/card-tilt.css +1 -1
- package/src/animations/digit-pop-in.css +1 -1
- package/src/animations/input-clear-dissolve.css +2 -2
- package/src/animations/learn-more-hover.css +2 -2
- package/src/animations/like-button.css +1 -1
- package/src/animations/plus-menu-morph.css +1 -1
- package/src/animations/success-check.css +5 -5
- 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.css +2 -1
- package/src/charts/bar-chart.svelte +18 -3
- package/src/charts/charts.css +2 -1
- 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 +4 -2
- 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/design/index.css +7 -5
- 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/effects/reveal/reveal.css +1 -1
- 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/lib/theme-transition.ts +6 -2
- 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/slides/theme.css +2 -2
- package/src/status/README.md +1 -1
- package/src/status/status-dot.svelte +3 -3
- 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/tokens/craft.css +7 -5
- 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-grid.svelte +2 -2
- package/src/{tasks → work}/gantt/gantt-group-row.svelte +1 -1
- package/src/{tasks → work}/gantt/gantt.css +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}/kanban-column.svelte +1 -1
- package/src/{tasks → work}/task-list.svelte +2 -2
- package/src/{tasks → work}/time/time-ring.svelte +1 -1
- package/src/{tasks → work}/time/time-tracker.svelte +3 -6
- 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-header.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt-task-row.svelte +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-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-types.ts +0 -0
- /package/src/{tasks → work}/use-kanban-dnd.svelte.ts +0 -0
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
* side rail and the compact bar, so the two can never disagree.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export type DocHeading = {
|
|
6
|
+
export type DocHeading = {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
/** 2 or 3. A list with no levels is flat; the edge ticks indent a 3 under its 2. */
|
|
10
|
+
level?: 2 | 3;
|
|
11
|
+
};
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* A heading's offset in scroller coordinates. A framed document must supply its own, since
|
|
@@ -77,14 +77,18 @@
|
|
|
77
77
|
flex sibling it wrapped to its own row on every two-line title. -->
|
|
78
78
|
<!-- No text-balance: it evens the lines out, so a two-line title stopped
|
|
79
79
|
short of the corner instead of running the full width. -->
|
|
80
|
-
<h2
|
|
80
|
+
<h2
|
|
81
|
+
class="min-w-0 font-heading text-lg [overflow-wrap:anywhere] font-semibold leading-snug tracking-tight"
|
|
82
|
+
>
|
|
81
83
|
{title}{#if badge}<span class="ms-2 inline-flex align-middle">{@render badge()}</span>{/if}
|
|
82
84
|
</h2>
|
|
83
85
|
{#if facts.length > 0}
|
|
84
86
|
<!-- Each dot hangs off the fact it leads, so a wrapped line's dot stays
|
|
85
87
|
in the gutter. A standalone flex-item dot dangled on the line above. -->
|
|
86
88
|
<div class="mt-1 overflow-hidden">
|
|
87
|
-
<p
|
|
89
|
+
<p
|
|
90
|
+
class="-ms-3 flex flex-wrap items-center text-sm text-muted-foreground [overflow-wrap:anywhere]"
|
|
91
|
+
>
|
|
88
92
|
{#each facts as fact, i (i)}
|
|
89
93
|
<span
|
|
90
94
|
class="relative ps-3 before:absolute before:start-1 before:opacity-40 before:content-['·']"
|
|
@@ -95,7 +99,7 @@
|
|
|
95
99
|
</div>
|
|
96
100
|
{/if}
|
|
97
101
|
{#if note}
|
|
98
|
-
<p class="mt-1 text-sm text-muted-foreground">{note}</p>
|
|
102
|
+
<p class="mt-1 text-sm text-muted-foreground [overflow-wrap:anywhere]">{note}</p>
|
|
99
103
|
{/if}
|
|
100
104
|
<!-- With a picture or a corner, actions sit in this column. Pinned right
|
|
101
105
|
they wrapped under the picture and read like its caption. -->
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
<Icon weight={iconWeight} class="size-8" />
|
|
46
46
|
</Empty.Media>
|
|
47
47
|
{/if}
|
|
48
|
-
<Empty.Title class="font-heading text-lg font-semibold"
|
|
48
|
+
<Empty.Title class="font-heading text-lg font-semibold [overflow-wrap:anywhere]"
|
|
49
|
+
>{title}</Empty.Title
|
|
50
|
+
>
|
|
49
51
|
{#if description}
|
|
50
52
|
<Empty.Description>{description}</Empty.Description>
|
|
51
53
|
{/if}
|
|
@@ -67,7 +69,7 @@
|
|
|
67
69
|
<style>
|
|
68
70
|
/* Never `:nth-child` here: one such rule makes every DOM insert on the page restyle its later siblings. */
|
|
69
71
|
:global(.t-empty) > :global(*) {
|
|
70
|
-
animation: t-empty-in var(--duration-
|
|
72
|
+
animation: t-empty-in var(--duration-slow) var(--ease-smooth-out) both;
|
|
71
73
|
animation-delay: calc((sibling-index() - 1) * var(--duration-stagger));
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
{/if}
|
|
72
72
|
{#if hasCount}
|
|
73
73
|
<span
|
|
74
|
-
class="ml-0.5 inline-flex
|
|
74
|
+
class="ml-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-brand/15 px-1 text-[10px] tabular-nums font-semibold text-brand dark:text-brand-light"
|
|
75
75
|
>
|
|
76
|
-
{count}
|
|
76
|
+
{count.toLocaleString()}
|
|
77
77
|
</span>
|
|
78
78
|
{/if}
|
|
79
79
|
</Popover.Trigger>
|
|
@@ -122,6 +122,7 @@
|
|
|
122
122
|
titleLines === 2 ? "line-clamp-2" : "truncate",
|
|
123
123
|
selected && "text-accent-foreground",
|
|
124
124
|
)}
|
|
125
|
+
{title}
|
|
125
126
|
>
|
|
126
127
|
{title}
|
|
127
128
|
</span>
|
|
@@ -130,7 +131,7 @@
|
|
|
130
131
|
{#if preview || trailing}
|
|
131
132
|
<span class="flex min-w-0 items-center text-xs text-muted-foreground">
|
|
132
133
|
{#if preview}
|
|
133
|
-
<span class="truncate">{preview}</span>
|
|
134
|
+
<span class="truncate" title={preview}>{preview}</span>
|
|
134
135
|
{/if}
|
|
135
136
|
{#if trailing}
|
|
136
137
|
<span class="ml-auto flex shrink-0 items-center gap-1.5 pl-2">{@render trailing()}</span
|
|
@@ -146,7 +147,9 @@
|
|
|
146
147
|
{/if}
|
|
147
148
|
{@const flexible =
|
|
148
149
|
facts.length > 1 && i === (metaTruncate === "last" ? facts.length - 1 : 0)}
|
|
149
|
-
<span class={flexible ? "truncate" : "shrink-0"}
|
|
150
|
+
<span class={flexible ? "truncate" : "shrink-0"} title={flexible ? fact : undefined}
|
|
151
|
+
>{fact}</span
|
|
152
|
+
>
|
|
150
153
|
{/each}
|
|
151
154
|
{#if metaTrail}
|
|
152
155
|
<span class="ml-1.5 flex shrink-0 items-center gap-1">{@render metaTrail()}</span>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// IMPORTS
|
|
4
4
|
// =========================
|
|
5
5
|
import { cn } from "../../lib/utils.js";
|
|
6
|
+
import { initial } from "../../lib/initials.js";
|
|
6
7
|
|
|
7
8
|
// =========================
|
|
8
9
|
// TYPES
|
|
@@ -61,6 +62,6 @@
|
|
|
61
62
|
className,
|
|
62
63
|
)}
|
|
63
64
|
>
|
|
64
|
-
{label
|
|
65
|
+
{initial(label, "")}
|
|
65
66
|
</span>
|
|
66
67
|
{/if}
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
<!-- =========================== -->
|
|
33
33
|
<Card.Root class={cn("gap-0 py-0", className)}>
|
|
34
34
|
<header class="flex min-h-14 items-center justify-between gap-3 border-b px-4 py-3 sm:px-6">
|
|
35
|
-
<Card.Title as="h3" class="text-base font-medium"
|
|
35
|
+
<Card.Title as="h3" class="min-w-0 text-base font-medium [overflow-wrap:anywhere]"
|
|
36
|
+
>{title}</Card.Title
|
|
37
|
+
>
|
|
36
38
|
{#if actions}
|
|
37
39
|
<div class="flex shrink-0 items-center gap-2">{@render actions()}</div>
|
|
38
40
|
{/if}
|
|
@@ -34,18 +34,18 @@
|
|
|
34
34
|
<header class="flex items-end justify-between gap-3">
|
|
35
35
|
<div class="min-w-0 space-y-1">
|
|
36
36
|
<svelte:element this={`h${level}`} class="flex items-center gap-2">
|
|
37
|
-
<SectionLabel>{title}</SectionLabel>
|
|
37
|
+
<SectionLabel class="min-w-0 [overflow-wrap:anywhere]">{title}</SectionLabel>
|
|
38
38
|
{#if count !== undefined}
|
|
39
39
|
<span
|
|
40
|
-
class="rounded-md bg-muted px-1.5 py-0.5 text-xs leading-none font-medium tabular-nums text-muted-foreground"
|
|
41
|
-
aria-label={`${count} ${countLabel}`}
|
|
40
|
+
class="shrink-0 rounded-md bg-muted px-1.5 py-0.5 text-xs leading-none font-medium tabular-nums text-muted-foreground"
|
|
41
|
+
aria-label={`${count.toLocaleString()} ${countLabel}`}
|
|
42
42
|
>
|
|
43
|
-
{count}
|
|
43
|
+
{count.toLocaleString()}
|
|
44
44
|
</span>
|
|
45
45
|
{/if}
|
|
46
46
|
</svelte:element>
|
|
47
47
|
{#if description}
|
|
48
|
-
<p class="text-sm text-muted-foreground">{description}</p>
|
|
48
|
+
<p class="text-sm text-muted-foreground [overflow-wrap:anywhere]">{description}</p>
|
|
49
49
|
{/if}
|
|
50
50
|
</div>
|
|
51
51
|
{#if actions}
|
|
@@ -154,6 +154,11 @@
|
|
|
154
154
|
function textOf(column: TableColumn<Row>, row: Row): string {
|
|
155
155
|
const value = (row as Record<string, unknown>)[key(column)];
|
|
156
156
|
if (column.format) return column.format(value, row);
|
|
157
|
+
// WHY: a raw 1284 or 0.30000000000000004 in an amount column. Only `numeric`
|
|
158
|
+
// columns: an unformatted year or id must print as it is.
|
|
159
|
+
if (column.numeric && typeof value === "number") {
|
|
160
|
+
return Number.isFinite(value) ? value.toLocaleString() : "";
|
|
161
|
+
}
|
|
157
162
|
return value === null || value === undefined ? "" : String(value);
|
|
158
163
|
}
|
|
159
164
|
|
|
@@ -227,7 +232,7 @@
|
|
|
227
232
|
<Table.Cell
|
|
228
233
|
class={cn(
|
|
229
234
|
"align-top",
|
|
230
|
-
column.numeric ? "text-end tabular-nums" : "whitespace-normal",
|
|
235
|
+
column.numeric ? "text-end tabular-nums" : "whitespace-normal [overflow-wrap:anywhere]",
|
|
231
236
|
column.muted && "text-muted-foreground",
|
|
232
237
|
column.interactive && "relative z-10",
|
|
233
238
|
column.hideBelow && HIDE[column.hideBelow],
|
package/src/design/index.css
CHANGED
|
@@ -93,12 +93,13 @@
|
|
|
93
93
|
--duration-micro: 80ms;
|
|
94
94
|
--duration-quick: 150ms; /* every close, text swap, tooltip in */
|
|
95
95
|
--duration-fast: 250ms; /* every open, tabs, icon swap, page slide */
|
|
96
|
-
--duration-medium:
|
|
97
|
-
--duration-slow:
|
|
98
|
-
--duration-
|
|
96
|
+
--duration-medium: 280ms; /* panel close, toast close */
|
|
97
|
+
--duration-slow: 300ms; /* panel open, skeleton reveal, emphasis: badge, text reveal, success check. The UI ceiling. */
|
|
98
|
+
--duration-overlay: 500ms; /* modal, drawer, marketing explainer: the only place past 300ms */
|
|
99
|
+
--duration-loop: 500ms; /* one cycle of a spinner or flowing dash, not a transition */
|
|
99
100
|
|
|
100
101
|
--ease-smooth-out: cubic-bezier(0.22, 1, 0.36, 1); /* all surface motion */
|
|
101
|
-
--ease-in-out:
|
|
102
|
+
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* swaps, cross-fades, on-screen movement */
|
|
102
103
|
--ease-out: ease-out; /* tooltips */
|
|
103
104
|
--ease-linear: linear; /* shimmer, pulse, spinner */
|
|
104
105
|
|
|
@@ -147,7 +148,8 @@
|
|
|
147
148
|
--transition-duration-fast: var(--duration-fast);
|
|
148
149
|
--transition-duration-medium: var(--duration-medium);
|
|
149
150
|
--transition-duration-slow: var(--duration-slow);
|
|
150
|
-
--transition-duration-
|
|
151
|
+
--transition-duration-overlay: var(--duration-overlay);
|
|
152
|
+
--transition-duration-loop: var(--duration-loop);
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
/* Each bits-ui variant matches v2's `data-open` and v1's `data-state`, and ships
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Heading anchors: one slug per heading, unique within a document. Pure. */
|
|
2
|
+
|
|
3
|
+
/** Lowercase, words joined by `-`, accents dropped. An empty result is `section`. */
|
|
4
|
+
export function slugify(text: string): string {
|
|
5
|
+
const slug = text
|
|
6
|
+
.normalize("NFKD")
|
|
7
|
+
.replace(/[̀-ͯ]/g, "")
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^\p{L}\p{N}]+/gu, "-")
|
|
10
|
+
.replace(/^-+|-+$/g, "");
|
|
11
|
+
return slug || "section";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** One unique anchor per label, in order: a repeat becomes `name-2`, `name-3`. */
|
|
15
|
+
export function anchorIds(labels: string[]): string[] {
|
|
16
|
+
const seen = new Map<string, number>();
|
|
17
|
+
const taken = new Set<string>();
|
|
18
|
+
return labels.map((label) => {
|
|
19
|
+
const base = slugify(label);
|
|
20
|
+
let count = (seen.get(base) ?? 0) + 1;
|
|
21
|
+
let id = count === 1 ? base : `${base}-${count}`;
|
|
22
|
+
// A heading literally called "Setup 2" must not steal the second "Setup".
|
|
23
|
+
while (taken.has(id)) id = `${base}-${++count}`;
|
|
24
|
+
seen.set(base, count);
|
|
25
|
+
taken.add(id);
|
|
26
|
+
return id;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heading anchors with a hover "Copy link", and code blocks with line numbers and a language
|
|
3
|
+
* label. Both are decorations, so the document stays plain Tiptap JSON.
|
|
4
|
+
*/
|
|
5
|
+
import { Extension } from "@tiptap/core";
|
|
6
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
7
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
8
|
+
import { anchorIds } from "./anchors.js";
|
|
9
|
+
|
|
10
|
+
const LINK_ICON = `<svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M240 88.23a54.43 54.43 0 0 1-16 37L189.25 160a54.27 54.27 0 0 1-38.63 16h-.05A54.63 54.63 0 0 1 96 119.84a8 8 0 0 1 16 .45A38.62 38.62 0 0 0 150.58 160a38.4 38.4 0 0 0 27.31-11.31l34.75-34.75a38.63 38.63 0 0 0-54.63-54.63l-11 11A8 8 0 0 1 135.7 59l11-11a54.65 54.65 0 0 1 77.3 0A54.86 54.86 0 0 1 240 88.23ZM109 185.66l-11 11A38.41 38.41 0 0 1 70.6 208a38.63 38.63 0 0 1-27.29-65.94L78 107.31a38.63 38.63 0 0 1 54.63 0 8 8 0 0 0 11.31-11.31 54.65 54.65 0 0 0-77.3 0L32 130.75A54.62 54.62 0 0 0 70.56 224a54.28 54.28 0 0 0 38.64-16l11-11a8 8 0 0 0-11.2-11.34Z"/></svg>`;
|
|
11
|
+
|
|
12
|
+
export type HeadingAnchorsOptions = {
|
|
13
|
+
/** The word on the hover button. */
|
|
14
|
+
label: string;
|
|
15
|
+
/** Runs after the link is copied; a host shows a toast here. */
|
|
16
|
+
oncopy?: (url: string, id: string) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const anchorsKey = new PluginKey("headingAnchors");
|
|
20
|
+
|
|
21
|
+
/** Gives every heading an `id` and a hover "Copy link" that copies a URL to that section. */
|
|
22
|
+
export const HeadingAnchors = Extension.create<HeadingAnchorsOptions>({
|
|
23
|
+
name: "headingAnchors",
|
|
24
|
+
|
|
25
|
+
addOptions() {
|
|
26
|
+
return { label: "Copy link", oncopy: undefined };
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
addProseMirrorPlugins() {
|
|
30
|
+
const options = this.options;
|
|
31
|
+
return [
|
|
32
|
+
new Plugin({
|
|
33
|
+
key: anchorsKey,
|
|
34
|
+
props: {
|
|
35
|
+
decorations(state) {
|
|
36
|
+
const found: { pos: number; size: number; label: string }[] = [];
|
|
37
|
+
state.doc.descendants((node, pos) => {
|
|
38
|
+
if (node.type.name !== "heading") return;
|
|
39
|
+
found.push({ pos, size: node.nodeSize, label: node.textContent.trim() });
|
|
40
|
+
});
|
|
41
|
+
if (found.length === 0) return DecorationSet.empty;
|
|
42
|
+
const ids = anchorIds(found.map((heading) => heading.label));
|
|
43
|
+
const decorations = found.flatMap((heading, i) => {
|
|
44
|
+
const id = ids[i];
|
|
45
|
+
return [
|
|
46
|
+
Decoration.node(heading.pos, heading.pos + heading.size, {
|
|
47
|
+
id,
|
|
48
|
+
class: "has-anchor",
|
|
49
|
+
}),
|
|
50
|
+
Decoration.widget(
|
|
51
|
+
heading.pos + heading.size - 1,
|
|
52
|
+
() => {
|
|
53
|
+
const button = document.createElement("button");
|
|
54
|
+
button.type = "button";
|
|
55
|
+
button.className = "heading-anchor";
|
|
56
|
+
button.contentEditable = "false";
|
|
57
|
+
button.innerHTML = `${LINK_ICON}<span>${options.label}</span>`;
|
|
58
|
+
button.addEventListener("mousedown", (event) => event.preventDefault());
|
|
59
|
+
button.addEventListener("click", () => {
|
|
60
|
+
const { origin, pathname, search } = window.location;
|
|
61
|
+
const url = `${origin}${pathname}${search}#${id}`;
|
|
62
|
+
void navigator.clipboard?.writeText(url).catch(() => {});
|
|
63
|
+
button.dataset.copied = "true";
|
|
64
|
+
setTimeout(() => delete button.dataset.copied, 1400);
|
|
65
|
+
options.oncopy?.(url, id);
|
|
66
|
+
});
|
|
67
|
+
return button;
|
|
68
|
+
},
|
|
69
|
+
{ side: 1, key: `anchor-${id}`, ignoreSelection: true },
|
|
70
|
+
),
|
|
71
|
+
];
|
|
72
|
+
});
|
|
73
|
+
return DecorationSet.create(state.doc, decorations);
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
];
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export type CodeLanguage = { value: string; label: string };
|
|
82
|
+
|
|
83
|
+
export const CODE_LANGUAGES: CodeLanguage[] = [
|
|
84
|
+
{ value: "", label: "Plain text" },
|
|
85
|
+
{ value: "typescript", label: "TypeScript" },
|
|
86
|
+
{ value: "javascript", label: "JavaScript" },
|
|
87
|
+
{ value: "svelte", label: "Svelte" },
|
|
88
|
+
{ value: "html", label: "HTML" },
|
|
89
|
+
{ value: "css", label: "CSS" },
|
|
90
|
+
{ value: "json", label: "JSON" },
|
|
91
|
+
{ value: "bash", label: "Bash" },
|
|
92
|
+
{ value: "sql", label: "SQL" },
|
|
93
|
+
{ value: "python", label: "Python" },
|
|
94
|
+
{ value: "markdown", label: "Markdown" },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/** "1\n2\n3" for a block of three lines: the gutter, which CSS prints beside the code. */
|
|
98
|
+
export function lineNumbers(code: string): string {
|
|
99
|
+
const count = code.split("\n").length;
|
|
100
|
+
return Array.from({ length: count }, (_, i) => i + 1).join("\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const codeKey = new PluginKey("codeBlockChrome");
|
|
104
|
+
|
|
105
|
+
/** Line numbers in a gutter and a language picker in the corner of every code block. */
|
|
106
|
+
export const CodeBlockChrome = Extension.create({
|
|
107
|
+
name: "codeBlockChrome",
|
|
108
|
+
|
|
109
|
+
addProseMirrorPlugins() {
|
|
110
|
+
return [
|
|
111
|
+
new Plugin({
|
|
112
|
+
key: codeKey,
|
|
113
|
+
props: {
|
|
114
|
+
decorations(state) {
|
|
115
|
+
const decorations: Decoration[] = [];
|
|
116
|
+
state.doc.descendants((node, pos) => {
|
|
117
|
+
if (node.type.name !== "codeBlock") return;
|
|
118
|
+
const language = typeof node.attrs.language === "string" ? node.attrs.language : "";
|
|
119
|
+
decorations.push(
|
|
120
|
+
Decoration.node(pos, pos + node.nodeSize, {
|
|
121
|
+
"data-lines": lineNumbers(node.textContent),
|
|
122
|
+
}),
|
|
123
|
+
Decoration.widget(
|
|
124
|
+
pos + 1,
|
|
125
|
+
(view, getPos) => {
|
|
126
|
+
const select = document.createElement("select");
|
|
127
|
+
select.className = "code-language";
|
|
128
|
+
select.contentEditable = "false";
|
|
129
|
+
select.setAttribute("aria-label", "Language");
|
|
130
|
+
const known = CODE_LANGUAGES.some((item) => item.value === language);
|
|
131
|
+
const options = known
|
|
132
|
+
? CODE_LANGUAGES
|
|
133
|
+
: [...CODE_LANGUAGES, { value: language, label: language }];
|
|
134
|
+
for (const item of options) {
|
|
135
|
+
const option = document.createElement("option");
|
|
136
|
+
option.value = item.value;
|
|
137
|
+
option.textContent = item.label;
|
|
138
|
+
option.selected = item.value === language;
|
|
139
|
+
select.append(option);
|
|
140
|
+
}
|
|
141
|
+
select.addEventListener("mousedown", (event) => event.stopPropagation());
|
|
142
|
+
select.addEventListener("change", () => {
|
|
143
|
+
const at = (getPos() ?? 1) - 1;
|
|
144
|
+
const target = view.state.doc.nodeAt(at);
|
|
145
|
+
if (!target) return;
|
|
146
|
+
view.dispatch(
|
|
147
|
+
view.state.tr.setNodeMarkup(at, undefined, {
|
|
148
|
+
...target.attrs,
|
|
149
|
+
language: select.value || null,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
view.focus();
|
|
153
|
+
});
|
|
154
|
+
return select;
|
|
155
|
+
},
|
|
156
|
+
{ side: -1, key: `lang-${pos}-${language}`, stopEvent: () => true },
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
return decorations.length
|
|
161
|
+
? DecorationSet.create(state.doc, decorations)
|
|
162
|
+
: DecorationSet.empty;
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
];
|
|
167
|
+
},
|
|
168
|
+
});
|