@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.
Files changed (115) hide show
  1. package/COMPONENTS.md +1 -1
  2. package/README.md +1 -1
  3. package/package.json +58 -74
  4. package/src/agents/vendor/draw.ts +4 -4
  5. package/src/canvas/headless/canvas-cleanup.test.ts +1 -1
  6. package/src/canvas/ops/ops.ts +1 -1
  7. package/src/charts/area-chart.svelte +11 -1
  8. package/src/charts/bar-chart.svelte +18 -3
  9. package/src/charts/line-chart.svelte +11 -1
  10. package/src/components/data-table/data-table.ts +4 -5
  11. package/src/components/data-table/pagination.svelte +5 -3
  12. package/src/components/docs/on-this-page.svelte +82 -5
  13. package/src/components/docs/scroll-spy.svelte.ts +6 -1
  14. package/src/components/list-page/detail-header.svelte +7 -3
  15. package/src/components/list-page/empty-state.svelte +3 -1
  16. package/src/components/list-page/filter-popover.svelte +2 -2
  17. package/src/components/list-page/rail-row.svelte +5 -2
  18. package/src/components/mark/mark.svelte +2 -1
  19. package/src/components/section-card/section-card.svelte +3 -1
  20. package/src/components/section-header/section-header.svelte +5 -5
  21. package/src/components/segmented/segment.svelte +1 -1
  22. package/src/components/table-view/table-view.svelte +6 -1
  23. package/src/components/tags-input/tags-input.svelte +1 -1
  24. package/src/components/toolbar/toolbar.svelte +1 -1
  25. package/src/editor/anchors.ts +28 -0
  26. package/src/editor/doc-blocks.ts +168 -0
  27. package/src/editor/doc-page.css +389 -0
  28. package/src/editor/editor.svelte +47 -5
  29. package/src/editor/extensions.ts +26 -2
  30. package/src/editor/markdown.ts +27 -2
  31. package/src/editor/slash-items.ts +18 -0
  32. package/src/icons/trades.ts +1 -1
  33. package/src/inbox/README.md +6 -4
  34. package/src/inbox/inbox.test.ts +1 -1
  35. package/src/inbox/index.ts +2 -2
  36. package/src/inbox/logic.ts +29 -5
  37. package/src/inbox/thread-inbox.svelte +219 -92
  38. package/src/inbox/types.ts +27 -1
  39. package/src/lib/initials.test.ts +25 -0
  40. package/src/lib/initials.ts +29 -0
  41. package/src/modules.test.ts +4 -64
  42. package/src/panel/README.md +1 -1
  43. package/src/record/README.md +1 -1
  44. package/src/record/info-list.svelte +5 -4
  45. package/src/record/next-up.svelte +1 -1
  46. package/src/record/project-list.svelte +1 -1
  47. package/src/record/steps-rail.svelte +3 -3
  48. package/src/shadcn/badge/badge.svelte +1 -1
  49. package/src/shadcn/card/card-title.svelte +1 -5
  50. package/src/shadcn/empty/empty-description.svelte +1 -1
  51. package/src/shadcn/progress/progress.svelte +8 -1
  52. package/src/shadcn/table/table-cell.svelte +10 -2
  53. package/src/status/README.md +1 -1
  54. package/src/thread/README.md +2 -0
  55. package/src/thread/index.ts +9 -1
  56. package/src/thread/logic.ts +33 -3
  57. package/src/thread/thread-body.svelte +37 -0
  58. package/src/thread/thread-events.svelte +30 -0
  59. package/src/thread/thread.svelte +74 -59
  60. package/src/thread/types.ts +21 -0
  61. package/src/{tasks → work}/README.md +11 -8
  62. package/src/{tasks → work}/detail/task-thread.svelte +8 -15
  63. package/src/work/docs/doc-editor-page.svelte +194 -0
  64. package/src/work/docs/doc-menu.svelte +333 -0
  65. package/src/work/docs/docs-demo.svelte +227 -0
  66. package/src/work/docs/edited-chip.svelte +125 -0
  67. package/src/work/docs/edited.ts +54 -0
  68. package/src/work/docs/index.ts +38 -0
  69. package/src/work/docs/labels.ts +70 -0
  70. package/src/work/docs/menu.ts +127 -0
  71. package/src/work/docs/outline.ts +60 -0
  72. package/src/{tasks → work}/gantt/gantt-group-row.svelte +1 -1
  73. package/src/{tasks → work}/inbox/inbox-card.svelte +113 -50
  74. package/src/work/inbox/inbox-logic.ts +98 -0
  75. package/src/work/inbox/inbox-trace.svelte +58 -0
  76. package/src/{tasks → work}/inbox/inbox-types.ts +2 -0
  77. package/src/{tasks → work}/index.ts +0 -10
  78. package/src/{tasks → work}/kanban-board.svelte +1 -1
  79. package/src/{tasks → work}/task-list.svelte +1 -1
  80. package/src/{tasks → work}/time/time-tracker.svelte +2 -5
  81. package/src/tasks/inbox/inbox-logic.ts +0 -21
  82. package/src/tasks/inbox/inbox-trace.svelte +0 -51
  83. /package/src/{tasks → work}/ambient.d.ts +0 -0
  84. /package/src/{tasks → work}/board-order.ts +0 -0
  85. /package/src/{tasks → work}/dates.ts +0 -0
  86. /package/src/{tasks → work}/demo.svelte +0 -0
  87. /package/src/{tasks → work}/detail/detail-demo.svelte +0 -0
  88. /package/src/{tasks → work}/detail/detail-types.ts +0 -0
  89. /package/src/{tasks → work}/detail/task-chip.svelte +0 -0
  90. /package/src/{tasks → work}/detail/task-detail.svelte +0 -0
  91. /package/src/{tasks → work}/detail/task-property.svelte +0 -0
  92. /package/src/{tasks → work}/gantt/gantt-bar.svelte +0 -0
  93. /package/src/{tasks → work}/gantt/gantt-grid.svelte +0 -0
  94. /package/src/{tasks → work}/gantt/gantt-header.svelte +0 -0
  95. /package/src/{tasks → work}/gantt/gantt-task-row.svelte +0 -0
  96. /package/src/{tasks → work}/gantt/gantt.css +0 -0
  97. /package/src/{tasks → work}/gantt/gantt.test.ts +0 -0
  98. /package/src/{tasks → work}/gantt/logic.test.ts +0 -0
  99. /package/src/{tasks → work}/gantt/logic.ts +0 -0
  100. /package/src/{tasks → work}/inbox/inbox-draft.svelte +0 -0
  101. /package/src/{tasks → work}/inbox/inbox-statuses.ts +0 -0
  102. /package/src/{tasks → work}/inbox/index.ts +0 -0
  103. /package/src/{tasks → work}/kanban-column.svelte +0 -0
  104. /package/src/{tasks → work}/kanban-drag.ts +0 -0
  105. /package/src/{tasks → work}/kanban-types.ts +0 -0
  106. /package/src/{tasks → work}/presets/sales.ts +0 -0
  107. /package/src/{tasks → work}/reorder.ts +0 -0
  108. /package/src/{tasks → work}/task-gantt.svelte +0 -0
  109. /package/src/{tasks → work}/task-types.ts +0 -0
  110. /package/src/{tasks → work}/time/duration.ts +0 -0
  111. /package/src/{tasks → work}/time/time-chip.svelte +0 -0
  112. /package/src/{tasks → work}/time/time-dot.svelte +0 -0
  113. /package/src/{tasks → work}/time/time-ring.svelte +0 -0
  114. /package/src/{tasks → work}/time/time-types.ts +0 -0
  115. /package/src/{tasks → work}/use-kanban-dnd.svelte.ts +0 -0
@@ -95,7 +95,7 @@
95
95
  "group-data-[state=active]/segment:bg-brand/14 group-data-[state=active]/segment:text-brand",
96
96
  )}
97
97
  >
98
- {count}
98
+ {count.toLocaleString()}
99
99
  </span>
100
100
  {:else if done}
101
101
  <span
@@ -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],
@@ -198,7 +198,7 @@
198
198
  removable ? "rounded-(--radius-xs) pr-0.5" : "rounded-(--radius-sm)",
199
199
  )}
200
200
  >
201
- <span class="truncate">{tag}</span>
201
+ <span class="truncate" title={tag}>{tag}</span>
202
202
  {#if removable}
203
203
  <button
204
204
  type="button"
@@ -115,7 +115,7 @@
115
115
  {item.label}
116
116
  {#if item.count != null}
117
117
  <span class="ms-auto pe-2 text-xs text-muted-foreground tabular-nums"
118
- >{item.count}</span
118
+ >{item.count.toLocaleString()}</span
119
119
  >
120
120
  {/if}
121
121
  </DropdownMenu.RadioItem>
@@ -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
+ });
@@ -0,0 +1,389 @@
1
+ /* A document's type, scoped to `.doc-surface` so other editors keep theirs: 15/24 body,
2
+ 19 and 17 for the two heading steps. */
3
+
4
+ .doc-surface .tiptap {
5
+ font-family: var(--font-sans);
6
+ font-size: 0.9375rem;
7
+ line-height: 1.6;
8
+ font-weight: 450;
9
+ letter-spacing: -0.003em;
10
+ color: var(--color-foreground);
11
+ caret-color: var(--color-brand);
12
+ -webkit-font-smoothing: antialiased;
13
+ }
14
+
15
+ /* The page is the writing surface, so it takes no ring of its own; the caret is the focus. */
16
+ .doc-surface .tiptap:focus-visible {
17
+ outline: none;
18
+ box-shadow: none;
19
+ }
20
+
21
+ .doc-surface .tiptap :is(h1, h2, h3, h4) {
22
+ font-family: var(--font-heading);
23
+ font-weight: 600;
24
+ letter-spacing: -0.012em;
25
+ color: var(--color-foreground);
26
+ scroll-margin-top: 1.5rem;
27
+ }
28
+
29
+ .doc-surface .tiptap h1 {
30
+ font-size: 1.375rem;
31
+ line-height: calc(28 / 22);
32
+ margin: 2.25rem 0 0.5rem;
33
+ }
34
+
35
+ .doc-surface .tiptap h2 {
36
+ font-size: 1.1875rem;
37
+ line-height: calc(26 / 19);
38
+ margin: 2rem 0 0.375rem;
39
+ }
40
+
41
+ .doc-surface .tiptap h3 {
42
+ font-size: 1.0625rem;
43
+ line-height: calc(24 / 17);
44
+ margin: 1.5rem 0 0.25rem;
45
+ }
46
+
47
+ .doc-surface .tiptap h4 {
48
+ font-size: 0.9375rem;
49
+ line-height: 1.6;
50
+ margin: 1.25rem 0 0.25rem;
51
+ }
52
+
53
+ .doc-surface .tiptap > :first-child {
54
+ margin-top: 0;
55
+ }
56
+
57
+ .doc-surface .tiptap p {
58
+ margin: 0 0 0.5rem;
59
+ }
60
+
61
+ .doc-surface .tiptap :is(ul, ol) {
62
+ margin: 0 0 0.5rem;
63
+ padding-left: 1.5rem;
64
+ }
65
+
66
+ .doc-surface .tiptap li {
67
+ margin: 0;
68
+ }
69
+
70
+ .doc-surface .tiptap li > p {
71
+ margin: 0;
72
+ }
73
+
74
+ .doc-surface .tiptap li::marker {
75
+ color: var(--color-muted-foreground);
76
+ }
77
+
78
+ .doc-surface .tiptap a {
79
+ color: var(--color-foreground);
80
+ text-decoration: underline;
81
+ text-decoration-color: color-mix(in oklab, var(--color-foreground) 35%, transparent);
82
+ text-underline-offset: 3px;
83
+ }
84
+
85
+ .doc-surface .tiptap :not(pre) > code {
86
+ font-family: var(--font-mono);
87
+ font-size: 0.85em;
88
+ padding: 0.1em 0.35em;
89
+ border-radius: var(--radius-xs);
90
+ background: color-mix(in oklab, var(--color-foreground) 7%, transparent);
91
+ }
92
+
93
+ .doc-surface .tiptap blockquote {
94
+ margin: 0.75rem 0;
95
+ padding-left: 1rem;
96
+ font-style: normal;
97
+ color: var(--color-foreground);
98
+ }
99
+
100
+ .doc-surface .tiptap blockquote::before {
101
+ top: 0;
102
+ bottom: 0;
103
+ width: 2px;
104
+ background: color-mix(in oklab, var(--color-foreground) 22%, transparent);
105
+ }
106
+
107
+ .doc-surface .tiptap hr {
108
+ border: 0;
109
+ border-top: 1px solid var(--color-border);
110
+ margin: 1.5rem 0;
111
+ }
112
+
113
+ /* ---- Checklist ---- */
114
+ .doc-surface .tiptap ul[data-type="taskList"] {
115
+ list-style: none;
116
+ padding-left: 0;
117
+ }
118
+
119
+ .doc-surface .tiptap ul[data-type="taskList"] ul[data-type="taskList"] {
120
+ padding-left: 1.625rem;
121
+ margin-bottom: 0;
122
+ }
123
+
124
+ .doc-surface .tiptap ul[data-type="taskList"] > li {
125
+ display: flex;
126
+ align-items: flex-start;
127
+ gap: 0.625rem;
128
+ }
129
+
130
+ .doc-surface .tiptap ul[data-type="taskList"] > li > label {
131
+ position: relative;
132
+ display: flex;
133
+ height: 1.5rem;
134
+ align-items: center;
135
+ user-select: none;
136
+ }
137
+
138
+ .doc-surface .tiptap ul[data-type="taskList"] > li > div {
139
+ min-width: 0;
140
+ flex: 1;
141
+ }
142
+
143
+ .doc-surface .tiptap ul[data-type="taskList"] > li input[type="checkbox"] {
144
+ appearance: none;
145
+ margin: 0;
146
+ width: 1rem;
147
+ height: 1rem;
148
+ flex-shrink: 0;
149
+ cursor: pointer;
150
+ border-radius: var(--radius-xs);
151
+ border: 1.5px solid color-mix(in oklab, var(--color-foreground) 32%, transparent);
152
+ background-position: center;
153
+ background-repeat: no-repeat;
154
+ background-size: 0.75rem;
155
+ transition:
156
+ background-color var(--duration-fast) var(--ease-smooth-out),
157
+ border-color var(--duration-fast) var(--ease-smooth-out);
158
+ }
159
+
160
+ .doc-surface .tiptap ul[data-type="taskList"] > li input[type="checkbox"]:checked {
161
+ border-color: var(--color-brand);
162
+ background-color: var(--color-brand);
163
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='white' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 6.4 5 8.8l4.5-5'/%3E%3C/svg%3E");
164
+ }
165
+
166
+ .doc-surface .tiptap ul[data-type="taskList"] > li input[type="checkbox"]:focus-visible {
167
+ outline: 2px solid color-mix(in oklab, var(--color-brand) 55%, transparent);
168
+ outline-offset: 2px;
169
+ }
170
+
171
+ .doc-surface .tiptap ul[data-type="taskList"] > li[data-checked="true"] > div {
172
+ color: var(--color-muted-foreground);
173
+ }
174
+
175
+ /* ---- Table ---- */
176
+ .doc-surface .tiptap .tableWrapper {
177
+ overflow-x: auto;
178
+ margin: 0.75rem 0 1rem;
179
+ }
180
+
181
+ .doc-surface .tiptap table {
182
+ width: 100%;
183
+ margin: 0.75rem 0 1rem;
184
+ border-collapse: separate;
185
+ border-spacing: 0;
186
+ table-layout: fixed;
187
+ font-size: 0.875rem;
188
+ line-height: calc(20 / 14);
189
+ border: 1px solid var(--color-border);
190
+ border-radius: var(--radius-lg);
191
+ overflow: hidden;
192
+ }
193
+
194
+ .doc-surface .tiptap .tableWrapper table {
195
+ margin: 0;
196
+ }
197
+
198
+ .doc-surface .tiptap :is(th, td) {
199
+ position: relative;
200
+ padding: 0.5rem 0.75rem;
201
+ vertical-align: top;
202
+ text-align: left;
203
+ border-right: 1px solid var(--color-border);
204
+ border-bottom: 1px solid var(--color-border);
205
+ }
206
+
207
+ .doc-surface .tiptap :is(th, td):last-child {
208
+ border-right: 0;
209
+ }
210
+
211
+ .doc-surface .tiptap tr:last-child > :is(th, td) {
212
+ border-bottom: 0;
213
+ }
214
+
215
+ /* Beats the blog renderer's uppercase header row and bold first column. */
216
+ .doc-surface .tiptap table tr > :is(th, td) {
217
+ font-size: 0.875rem;
218
+ font-weight: 400;
219
+ text-transform: none;
220
+ letter-spacing: 0;
221
+ background: transparent;
222
+ color: var(--color-foreground);
223
+ }
224
+
225
+ .doc-surface .tiptap table tr > th {
226
+ font-weight: 550;
227
+ background: color-mix(in oklab, var(--color-foreground) 3.5%, transparent);
228
+ }
229
+
230
+ .doc-surface .tiptap table tr:not(:first-child) > td:first-child {
231
+ font-weight: 400;
232
+ }
233
+
234
+ @media (hover: hover) and (pointer: fine) {
235
+ .doc-surface .tiptap table tr:hover > td {
236
+ background: transparent;
237
+ }
238
+ }
239
+
240
+ .doc-surface .tiptap :is(th, td) > p {
241
+ margin: 0;
242
+ }
243
+
244
+ .doc-surface .tiptap .selectedCell::after {
245
+ content: "";
246
+ position: absolute;
247
+ inset: 0;
248
+ pointer-events: none;
249
+ background: color-mix(in oklab, var(--color-brand) 14%, transparent);
250
+ }
251
+
252
+ /* ---- Code block: line numbers, language label ---- */
253
+ .doc-surface .tiptap pre {
254
+ position: relative;
255
+ margin: 0.75rem 0 1rem;
256
+ padding: 0.75rem 1rem 0.75rem 3.25rem;
257
+ overflow-x: auto;
258
+ border: 1px solid var(--color-border);
259
+ border-radius: var(--radius-lg);
260
+ background: color-mix(in oklab, var(--color-foreground) 3%, transparent);
261
+ font-family: var(--font-mono);
262
+ font-size: 0.8125rem;
263
+ line-height: calc(20 / 13);
264
+ font-weight: 400;
265
+ tab-size: 2;
266
+ }
267
+
268
+ .doc-surface .tiptap pre::before {
269
+ content: attr(data-lines);
270
+ position: absolute;
271
+ top: 0.75rem;
272
+ left: 0;
273
+ width: 2.25rem;
274
+ white-space: pre;
275
+ text-align: right;
276
+ color: color-mix(in oklab, var(--color-muted-foreground) 70%, transparent);
277
+ font-variant-numeric: tabular-nums;
278
+ pointer-events: none;
279
+ user-select: none;
280
+ }
281
+
282
+ .doc-surface .tiptap pre code {
283
+ display: block;
284
+ min-width: max-content;
285
+ font-size: inherit;
286
+ background: none;
287
+ padding: 0;
288
+ }
289
+
290
+ .doc-surface .tiptap .code-language {
291
+ position: absolute;
292
+ top: 0.375rem;
293
+ right: 0.5rem;
294
+ appearance: none;
295
+ height: 1.375rem;
296
+ padding: 0 0.5rem;
297
+ border: 0;
298
+ border-radius: var(--radius-xs);
299
+ background: transparent;
300
+ color: var(--color-muted-foreground);
301
+ font-family: var(--font-sans);
302
+ font-size: 0.6875rem;
303
+ font-weight: 500;
304
+ cursor: pointer;
305
+ opacity: 0;
306
+ transition:
307
+ opacity var(--duration-fast) var(--ease-smooth-out),
308
+ background-color var(--duration-fast) var(--ease-smooth-out);
309
+ }
310
+
311
+ .doc-surface .tiptap pre:focus-within .code-language,
312
+ .doc-surface .tiptap .code-language:focus-visible {
313
+ opacity: 1;
314
+ }
315
+
316
+ @media (hover: hover) and (pointer: fine) {
317
+ .doc-surface .tiptap pre:hover .code-language {
318
+ opacity: 1;
319
+ }
320
+
321
+ .doc-surface .tiptap .code-language:hover {
322
+ background: color-mix(in oklab, var(--color-foreground) 7%, transparent);
323
+ color: var(--color-foreground);
324
+ }
325
+ }
326
+
327
+ @media (hover: none) {
328
+ .doc-surface .tiptap .code-language {
329
+ opacity: 1;
330
+ }
331
+ }
332
+
333
+ /* ---- Heading anchor ---- */
334
+ .doc-surface .tiptap .heading-anchor {
335
+ display: inline-flex;
336
+ align-items: center;
337
+ gap: 0.3125rem;
338
+ margin-left: 0.625rem;
339
+ padding: 0 0.375rem;
340
+ height: 1.25rem;
341
+ border: 0;
342
+ border-radius: var(--radius-xs);
343
+ background: transparent;
344
+ color: var(--color-muted-foreground);
345
+ font-family: var(--font-sans);
346
+ font-size: 0.75rem;
347
+ line-height: 1;
348
+ font-weight: 450;
349
+ letter-spacing: 0;
350
+ vertical-align: 0.1em;
351
+ cursor: pointer;
352
+ opacity: 0;
353
+ user-select: none;
354
+ transition:
355
+ opacity var(--duration-fast) var(--ease-smooth-out),
356
+ color var(--duration-fast) var(--ease-smooth-out);
357
+ }
358
+
359
+ .doc-surface .tiptap .heading-anchor[data-copied="true"] {
360
+ color: var(--color-brand);
361
+ opacity: 1;
362
+ }
363
+
364
+ .doc-surface .tiptap .heading-anchor:focus-visible {
365
+ opacity: 1;
366
+ outline: 2px solid color-mix(in oklab, var(--color-brand) 55%, transparent);
367
+ }
368
+
369
+ @media (hover: hover) and (pointer: fine) {
370
+ .doc-surface .tiptap .has-anchor:hover .heading-anchor {
371
+ opacity: 1;
372
+ }
373
+
374
+ .doc-surface .tiptap .heading-anchor:hover {
375
+ color: var(--color-foreground);
376
+ }
377
+ }
378
+
379
+ @media (hover: none) {
380
+ .doc-surface .tiptap .heading-anchor {
381
+ display: none;
382
+ }
383
+ }
384
+
385
+ @media (prefers-reduced-motion: reduce) {
386
+ .doc-surface .tiptap :is(.heading-anchor, .code-language, input[type="checkbox"]) {
387
+ transition: none;
388
+ }
389
+ }