@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.
Files changed (132) 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/ai/generation-progress/GenerationProgress.svelte +2 -2
  6. package/src/animations/README.md +1 -1
  7. package/src/animations/card-tilt.css +1 -1
  8. package/src/animations/digit-pop-in.css +1 -1
  9. package/src/animations/input-clear-dissolve.css +2 -2
  10. package/src/animations/learn-more-hover.css +2 -2
  11. package/src/animations/like-button.css +1 -1
  12. package/src/animations/plus-menu-morph.css +1 -1
  13. package/src/animations/success-check.css +5 -5
  14. package/src/canvas/headless/canvas-cleanup.test.ts +1 -1
  15. package/src/canvas/ops/ops.ts +1 -1
  16. package/src/charts/area-chart.svelte +11 -1
  17. package/src/charts/bar-chart.css +2 -1
  18. package/src/charts/bar-chart.svelte +18 -3
  19. package/src/charts/charts.css +2 -1
  20. package/src/charts/line-chart.svelte +11 -1
  21. package/src/components/data-table/data-table.ts +4 -5
  22. package/src/components/data-table/pagination.svelte +5 -3
  23. package/src/components/docs/on-this-page.svelte +82 -5
  24. package/src/components/docs/scroll-spy.svelte.ts +6 -1
  25. package/src/components/list-page/detail-header.svelte +7 -3
  26. package/src/components/list-page/empty-state.svelte +4 -2
  27. package/src/components/list-page/filter-popover.svelte +2 -2
  28. package/src/components/list-page/rail-row.svelte +5 -2
  29. package/src/components/mark/mark.svelte +2 -1
  30. package/src/components/section-card/section-card.svelte +3 -1
  31. package/src/components/section-header/section-header.svelte +5 -5
  32. package/src/components/segmented/segment.svelte +1 -1
  33. package/src/components/table-view/table-view.svelte +6 -1
  34. package/src/components/tags-input/tags-input.svelte +1 -1
  35. package/src/components/toolbar/toolbar.svelte +1 -1
  36. package/src/design/index.css +7 -5
  37. package/src/editor/anchors.ts +28 -0
  38. package/src/editor/doc-blocks.ts +168 -0
  39. package/src/editor/doc-page.css +389 -0
  40. package/src/editor/editor.svelte +47 -5
  41. package/src/editor/extensions.ts +26 -2
  42. package/src/editor/markdown.ts +27 -2
  43. package/src/editor/slash-items.ts +18 -0
  44. package/src/effects/reveal/reveal.css +1 -1
  45. package/src/icons/trades.ts +1 -1
  46. package/src/inbox/README.md +6 -4
  47. package/src/inbox/inbox.test.ts +1 -1
  48. package/src/inbox/index.ts +2 -2
  49. package/src/inbox/logic.ts +29 -5
  50. package/src/inbox/thread-inbox.svelte +219 -92
  51. package/src/inbox/types.ts +27 -1
  52. package/src/lib/initials.test.ts +25 -0
  53. package/src/lib/initials.ts +29 -0
  54. package/src/lib/theme-transition.ts +6 -2
  55. package/src/modules.test.ts +4 -64
  56. package/src/panel/README.md +1 -1
  57. package/src/record/README.md +1 -1
  58. package/src/record/info-list.svelte +5 -4
  59. package/src/record/next-up.svelte +1 -1
  60. package/src/record/project-list.svelte +1 -1
  61. package/src/record/steps-rail.svelte +3 -3
  62. package/src/shadcn/badge/badge.svelte +1 -1
  63. package/src/shadcn/card/card-title.svelte +1 -5
  64. package/src/shadcn/empty/empty-description.svelte +1 -1
  65. package/src/shadcn/progress/progress.svelte +8 -1
  66. package/src/shadcn/table/table-cell.svelte +10 -2
  67. package/src/slides/theme.css +2 -2
  68. package/src/status/README.md +1 -1
  69. package/src/status/status-dot.svelte +3 -3
  70. package/src/thread/README.md +2 -0
  71. package/src/thread/index.ts +9 -1
  72. package/src/thread/logic.ts +33 -3
  73. package/src/thread/thread-body.svelte +37 -0
  74. package/src/thread/thread-events.svelte +30 -0
  75. package/src/thread/thread.svelte +74 -59
  76. package/src/thread/types.ts +21 -0
  77. package/src/tokens/craft.css +7 -5
  78. package/src/{tasks → work}/README.md +11 -8
  79. package/src/{tasks → work}/detail/task-thread.svelte +8 -15
  80. package/src/work/docs/doc-editor-page.svelte +194 -0
  81. package/src/work/docs/doc-menu.svelte +333 -0
  82. package/src/work/docs/docs-demo.svelte +227 -0
  83. package/src/work/docs/edited-chip.svelte +125 -0
  84. package/src/work/docs/edited.ts +54 -0
  85. package/src/work/docs/index.ts +38 -0
  86. package/src/work/docs/labels.ts +70 -0
  87. package/src/work/docs/menu.ts +127 -0
  88. package/src/work/docs/outline.ts +60 -0
  89. package/src/{tasks → work}/gantt/gantt-grid.svelte +2 -2
  90. package/src/{tasks → work}/gantt/gantt-group-row.svelte +1 -1
  91. package/src/{tasks → work}/gantt/gantt.css +1 -1
  92. package/src/{tasks → work}/inbox/inbox-card.svelte +113 -50
  93. package/src/work/inbox/inbox-logic.ts +98 -0
  94. package/src/work/inbox/inbox-trace.svelte +58 -0
  95. package/src/{tasks → work}/inbox/inbox-types.ts +2 -0
  96. package/src/{tasks → work}/index.ts +0 -10
  97. package/src/{tasks → work}/kanban-board.svelte +1 -1
  98. package/src/{tasks → work}/kanban-column.svelte +1 -1
  99. package/src/{tasks → work}/task-list.svelte +2 -2
  100. package/src/{tasks → work}/time/time-ring.svelte +1 -1
  101. package/src/{tasks → work}/time/time-tracker.svelte +3 -6
  102. package/src/tasks/inbox/inbox-logic.ts +0 -21
  103. package/src/tasks/inbox/inbox-trace.svelte +0 -51
  104. /package/src/{tasks → work}/ambient.d.ts +0 -0
  105. /package/src/{tasks → work}/board-order.ts +0 -0
  106. /package/src/{tasks → work}/dates.ts +0 -0
  107. /package/src/{tasks → work}/demo.svelte +0 -0
  108. /package/src/{tasks → work}/detail/detail-demo.svelte +0 -0
  109. /package/src/{tasks → work}/detail/detail-types.ts +0 -0
  110. /package/src/{tasks → work}/detail/task-chip.svelte +0 -0
  111. /package/src/{tasks → work}/detail/task-detail.svelte +0 -0
  112. /package/src/{tasks → work}/detail/task-property.svelte +0 -0
  113. /package/src/{tasks → work}/gantt/gantt-bar.svelte +0 -0
  114. /package/src/{tasks → work}/gantt/gantt-header.svelte +0 -0
  115. /package/src/{tasks → work}/gantt/gantt-task-row.svelte +0 -0
  116. /package/src/{tasks → work}/gantt/gantt.test.ts +0 -0
  117. /package/src/{tasks → work}/gantt/logic.test.ts +0 -0
  118. /package/src/{tasks → work}/gantt/logic.ts +0 -0
  119. /package/src/{tasks → work}/inbox/inbox-draft.svelte +0 -0
  120. /package/src/{tasks → work}/inbox/inbox-statuses.ts +0 -0
  121. /package/src/{tasks → work}/inbox/index.ts +0 -0
  122. /package/src/{tasks → work}/kanban-drag.ts +0 -0
  123. /package/src/{tasks → work}/kanban-types.ts +0 -0
  124. /package/src/{tasks → work}/presets/sales.ts +0 -0
  125. /package/src/{tasks → work}/reorder.ts +0 -0
  126. /package/src/{tasks → work}/task-gantt.svelte +0 -0
  127. /package/src/{tasks → work}/task-types.ts +0 -0
  128. /package/src/{tasks → work}/time/duration.ts +0 -0
  129. /package/src/{tasks → work}/time/time-chip.svelte +0 -0
  130. /package/src/{tasks → work}/time/time-dot.svelte +0 -0
  131. /package/src/{tasks → work}/time/time-types.ts +0 -0
  132. /package/src/{tasks → work}/use-kanban-dnd.svelte.ts +0 -0
@@ -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
+ }
@@ -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({ preset, placeholder, blockIds });
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
- <EditorToolbar {editor} {tick} {preset} onInsertYoutube={insertYoutubeVideo} />
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);
@@ -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({ preset, placeholder, blockIds }: ExtensionOptions): Extensions {
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 ? [CmsBlock, Youtube, Image.configure({ inline: false })] : []),
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] : []),
@@ -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
- // WHY: images, cmsBlock and tables are out of scope for the notes bridge.
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",
@@ -4,7 +4,7 @@
4
4
  .hr-reveal {
5
5
  --hr-reveal-name: hr-reveal-fade;
6
6
  --hr-reveal-play: paused;
7
- --hr-reveal-duration: var(--duration-slow, 400ms);
7
+ --hr-reveal-duration: var(--duration-slow, 300ms);
8
8
  --hr-reveal-ease: var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1));
9
9
  --hr-reveal-stagger: var(--duration-stagger, 40ms);
10
10
  --hr-reveal-delay: 0ms;
@@ -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 legacy ` @tabler/icons-svelte`).
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 {
@@ -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 a modifier is held. `inboxKeyOf(event)` is the same check.
25
- - **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.
26
- - **Read.** `onRead(thread, read)` is true when the thread had unread entries, so the host marks them all read.
27
- - **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.
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.
@@ -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 "../tasks/inbox/inbox-types.js";
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" };
@@ -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 "../tasks/inbox/inbox-card.svelte";
31
- export type { InboxItem, InboxVerdict } from "../tasks/inbox/inbox-types.js";
30
+ export { default as InboxCard } from "../work/inbox/inbox-card.svelte";
31
+ export type { InboxItem, InboxVerdict } from "../work/inbox/inbox-types.js";