@ctrl-ui/react 0.8.20 → 0.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 (161) hide show
  1. package/README.md +1 -0
  2. package/dist/action-bar.d.ts +1 -2
  3. package/dist/action-bar.js +7 -8
  4. package/dist/blocks/design-canvas-inspector.js +4 -1
  5. package/dist/blocks/file-explorer.js +1 -1
  6. package/dist/chat-message.js +4 -13
  7. package/dist/hooks/use-textarea-trigger-menu.d.ts +21 -3
  8. package/dist/hooks/use-textarea-trigger-menu.js +36 -40
  9. package/dist/hooks/use-user-ask.d.ts +1 -0
  10. package/dist/hooks/use-user-ask.js +5 -1
  11. package/dist/knob-contracts/audio-recorder-knobs.d.ts +1 -1
  12. package/dist/knob-contracts/audio-recorder-knobs.js +2 -1
  13. package/dist/knob-contracts/chart-knobs.d.ts +4 -0
  14. package/dist/knob-contracts/chart-knobs.js +24 -0
  15. package/dist/knob-contracts/choice-knobs.d.ts +1 -1
  16. package/dist/knob-contracts/choice-knobs.js +1 -0
  17. package/dist/knob-contracts/dockable-panel-knobs.d.ts +1 -1
  18. package/dist/knob-contracts/dockable-panel-knobs.js +2 -1
  19. package/dist/knob-contracts/emoji-picker-knobs.d.ts +1 -1
  20. package/dist/knob-contracts/emoji-picker-knobs.js +1 -0
  21. package/dist/knob-contracts/empty-knobs.d.ts +1 -1
  22. package/dist/knob-contracts/empty-knobs.js +2 -1
  23. package/dist/knob-contracts/icon-picker-knobs.d.ts +1 -1
  24. package/dist/knob-contracts/icon-picker-knobs.js +3 -1
  25. package/dist/knob-contracts/resizable-knobs.d.ts +1 -1
  26. package/dist/knob-contracts/resizable-knobs.js +2 -2
  27. package/dist/knob-contracts/sidebar-knobs.d.ts +1 -1
  28. package/dist/knob-contracts/sidebar-knobs.js +3 -1
  29. package/dist/knob-contracts/tabs-knobs.d.ts +1 -1
  30. package/dist/knob-contracts/tabs-knobs.js +2 -1
  31. package/dist/knob-contracts/tree-knobs.d.ts +1 -1
  32. package/dist/knob-contracts/tree-knobs.js +2 -1
  33. package/dist/lib/color.d.ts +2 -6
  34. package/dist/lib/color.js +45 -28
  35. package/dist/lib/contrast.d.ts +6 -0
  36. package/dist/lib/contrast.js +33 -1
  37. package/dist/lib/gradient.d.ts +17 -0
  38. package/dist/lib/gradient.js +69 -0
  39. package/dist/markdown-editor/drag-handle.js +73 -10
  40. package/dist/markdown-editor/drag-preview.d.ts +3 -0
  41. package/dist/markdown-editor/drag-preview.js +31 -0
  42. package/dist/markdown-editor/extensions.d.ts +2 -2
  43. package/dist/markdown-editor/extensions.js +25 -3
  44. package/dist/markdown-editor/suggestions.js +5 -5
  45. package/dist/markdown-editor/task-item.d.ts +3 -0
  46. package/dist/markdown-editor/task-item.js +50 -0
  47. package/dist/markdown-editor/toolbar.js +122 -63
  48. package/dist/markdown-editor.js +3 -3
  49. package/dist/skin.d.ts +0 -1
  50. package/dist/styles/index.css +3 -2
  51. package/dist/styles/recipes/action-bar.css +1 -1
  52. package/dist/styles/recipes/activity.css +1 -1
  53. package/dist/styles/recipes/alert.css +9 -10
  54. package/dist/styles/recipes/app-shell-header.css +2 -2
  55. package/dist/styles/recipes/audio-recorder.css +15 -4
  56. package/dist/styles/recipes/audio-visualizer.css +4 -4
  57. package/dist/styles/recipes/avatar.css +3 -3
  58. package/dist/styles/recipes/badge.css +2 -2
  59. package/dist/styles/recipes/button.css +2 -5
  60. package/dist/styles/recipes/card.css +0 -2
  61. package/dist/styles/recipes/chart.css +378 -0
  62. package/dist/styles/recipes/chat-composer-attachment.css +3 -3
  63. package/dist/styles/recipes/chat-composer.css +3 -3
  64. package/dist/styles/recipes/chat-layout.css +2 -2
  65. package/dist/styles/recipes/chat-message-social.css +1 -1
  66. package/dist/styles/recipes/chat-message.css +1 -1
  67. package/dist/styles/recipes/choice.css +9 -3
  68. package/dist/styles/recipes/code-diff.css +1 -1
  69. package/dist/styles/recipes/code.css +1 -1
  70. package/dist/styles/recipes/color-picker-details.css +17 -11
  71. package/dist/styles/recipes/color-picker-values.css +17 -7
  72. package/dist/styles/recipes/color-picker.css +88 -70
  73. package/dist/styles/recipes/context.css +2 -2
  74. package/dist/styles/recipes/dockable-panel.css +9 -2
  75. package/dist/styles/recipes/dynamic-notification-motion.css +1 -55
  76. package/dist/styles/recipes/dynamic-notification.css +0 -1
  77. package/dist/styles/recipes/emoji-picker.css +35 -19
  78. package/dist/styles/recipes/empty.css +19 -14
  79. package/dist/styles/recipes/field-parts.css +2 -2
  80. package/dist/styles/recipes/field.css +6 -4
  81. package/dist/styles/recipes/filter-bar.css +10 -10
  82. package/dist/styles/recipes/flow.css +5 -3
  83. package/dist/styles/recipes/gradient-editor.css +8 -0
  84. package/dist/styles/recipes/icon-picker.css +45 -10
  85. package/dist/styles/recipes/inline-attachment.css +4 -4
  86. package/dist/styles/recipes/inline-citation.css +2 -2
  87. package/dist/styles/recipes/item.css +4 -4
  88. package/dist/styles/recipes/markdown-editor.css +35 -24
  89. package/dist/styles/recipes/markdown.css +23 -27
  90. package/dist/styles/recipes/page-layout.css +13 -11
  91. package/dist/styles/recipes/popup-controls.css +2 -2
  92. package/dist/styles/recipes/popup-lists.css +3 -3
  93. package/dist/styles/recipes/popup.css +11 -9
  94. package/dist/styles/recipes/range.css +4 -5
  95. package/dist/styles/recipes/resizable.css +15 -10
  96. package/dist/styles/recipes/resize-handle.css +2 -2
  97. package/dist/styles/recipes/scroll-area.css +1 -1
  98. package/dist/styles/recipes/sidebar-menu.css +5 -15
  99. package/dist/styles/recipes/sidebar-mobile.css +4 -4
  100. package/dist/styles/recipes/sidebar-parts.css +26 -1
  101. package/dist/styles/recipes/sidebar.css +18 -31
  102. package/dist/styles/recipes/skeleton.css +10 -5
  103. package/dist/styles/recipes/stepper.css +1 -1
  104. package/dist/styles/recipes/switch.css +12 -11
  105. package/dist/styles/recipes/table-of-contents.css +6 -8
  106. package/dist/styles/recipes/table.css +4 -4
  107. package/dist/styles/recipes/tabs.css +10 -5
  108. package/dist/styles/recipes/task-list.css +3 -3
  109. package/dist/styles/recipes/tree.css +10 -3
  110. package/dist/styles/recipes/user-ask-details.css +5 -4
  111. package/dist/styles/recipes/user-ask.css +5 -5
  112. package/dist/styles/theme.css +158 -177
  113. package/dist/ui/chart-cartesian.d.ts +801 -0
  114. package/dist/ui/chart-cartesian.js +420 -0
  115. package/dist/ui/chart-colors.d.ts +7 -0
  116. package/dist/ui/chart-colors.js +27 -0
  117. package/dist/ui/chart-curves.d.ts +7 -0
  118. package/dist/ui/chart-curves.js +87 -0
  119. package/dist/ui/chart-marks.d.ts +68 -0
  120. package/dist/ui/chart-marks.js +227 -0
  121. package/dist/ui/chart-polar.d.ts +103 -0
  122. package/dist/ui/chart-polar.js +124 -0
  123. package/dist/ui/chart-series.d.ts +36 -0
  124. package/dist/ui/chart-series.js +71 -0
  125. package/dist/ui/chart.d.ts +32 -0
  126. package/dist/ui/chart.js +257 -0
  127. package/dist/ui/color-picker-area.d.ts +16 -0
  128. package/dist/ui/color-picker-area.js +175 -0
  129. package/dist/ui/color-picker-context.d.ts +27 -0
  130. package/dist/ui/color-picker-context.js +87 -0
  131. package/dist/ui/color-picker-contrast.d.ts +20 -0
  132. package/dist/ui/color-picker-contrast.js +129 -0
  133. package/dist/ui/color-picker-sliders.d.ts +15 -0
  134. package/dist/ui/color-picker-sliders.js +146 -0
  135. package/dist/ui/color-picker-swatches.d.ts +23 -0
  136. package/dist/ui/color-picker-swatches.js +94 -0
  137. package/dist/ui/color-picker-values.d.ts +59 -0
  138. package/dist/ui/color-picker-values.js +244 -0
  139. package/dist/ui/color-picker.d.ts +15 -110
  140. package/dist/ui/color-picker.js +30 -759
  141. package/dist/ui/emoji-picker.d.ts +10 -1
  142. package/dist/ui/emoji-picker.js +71 -14
  143. package/dist/ui/flow.js +1 -1
  144. package/dist/ui/gradient-editor-controls.d.ts +19 -0
  145. package/dist/ui/gradient-editor-controls.js +75 -0
  146. package/dist/ui/gradient-editor.d.ts +23 -22
  147. package/dist/ui/gradient-editor.js +77 -123
  148. package/dist/ui/icon-picker.d.ts +1 -1
  149. package/dist/ui/icon-picker.js +11 -6
  150. package/dist/ui/markdown-elements.d.ts +3 -1
  151. package/dist/ui/markdown-elements.js +62 -22
  152. package/dist/ui/markdown-heading.d.ts +9 -0
  153. package/dist/ui/markdown-heading.js +25 -0
  154. package/dist/ui/resizable.js +24 -9
  155. package/dist/ui/sidebar-provider.js +3 -12
  156. package/dist/ui/typography.d.ts +1 -0
  157. package/dist/ui/typography.js +12 -3
  158. package/dist/user-ask.js +4 -8
  159. package/package.json +7 -3
  160. package/dist/ui/emoji-icon-picker.d.ts +0 -13
  161. package/dist/ui/emoji-icon-picker.js +0 -50
package/dist/lib/color.js CHANGED
@@ -3,7 +3,7 @@ const clamp = (n, min, max) => Math.min(max, Math.max(min, n));
3
3
  const clamp01 = (n) => clamp(n, 0, 1);
4
4
  const wrapHue = (h) => (h % 360 + 360) % 360;
5
5
  const round = (n) => Math.round(n);
6
- function hsvaToRgba({ h, s, v, a }) {
6
+ function hsvaToUnroundedRgba({ h, s, v, a }) {
7
7
  const S = clamp01(s / 100);
8
8
  const V = clamp01(v / 100);
9
9
  const c = V * S;
@@ -44,12 +44,21 @@ function hsvaToRgba({ h, s, v, a }) {
44
44
  x
45
45
  ];
46
46
  return {
47
- r: round((r + m) * 255),
48
- g: round((g + m) * 255),
49
- b: round((b + m) * 255),
47
+ r: (r + m) * 255,
48
+ g: (g + m) * 255,
49
+ b: (b + m) * 255,
50
50
  a: clamp01(a)
51
51
  };
52
52
  }
53
+ function hsvaToRgba(hsva) {
54
+ const { r, g, b, a } = hsvaToUnroundedRgba(hsva);
55
+ return {
56
+ r: round(r),
57
+ g: round(g),
58
+ b: round(b),
59
+ a
60
+ };
61
+ }
53
62
  function rgbaToHsva({ r, g, b, a }) {
54
63
  const R = clamp(r, 0, 255) / 255;
55
64
  const G = clamp(g, 0, 255) / 255;
@@ -124,28 +133,44 @@ function rgbToOklch(r255, g255, b255) {
124
133
  H
125
134
  };
126
135
  }
127
- function oklchToRgb(L, C, H) {
136
+ function oklchToLinearRgb(L, C, H) {
128
137
  const hr = H * Math.PI / 180;
129
138
  const a = C * Math.cos(hr);
130
139
  const b = C * Math.sin(hr);
131
- const l_ = L + .3963377774 * a + .2158037573 * b;
132
- const m_ = L - .1055613458 * a - .0638541728 * b;
133
- const s_ = L - .0894841775 * a - 1.291485548 * b;
134
- const l = l_ ** 3;
135
- const m = m_ ** 3;
136
- const s = s_ ** 3;
137
- const r = 4.0767416621 * l - 3.3077115913 * m + .2309699292 * s;
138
- const g = -1.2684380046 * l + 2.6097574011 * m - .3413193965 * s;
139
- const bl = -.0041960863 * l - .7034186147 * m + 1.707614701 * s;
140
- const to = (c) => clamp(round(linearToSrgb(c) * 255), 0, 255);
140
+ const l = (L + .3963377774 * a + .2158037573 * b) ** 3;
141
+ const m = (L - .1055613458 * a - .0638541728 * b) ** 3;
142
+ const s = (L - .0894841775 * a - 1.291485548 * b) ** 3;
143
+ return [
144
+ 4.0767416621 * l - 3.3077115913 * m + .2309699292 * s,
145
+ -1.2684380046 * l + 2.6097574011 * m - .3413193965 * s,
146
+ -.0041960863 * l - .7034186147 * m + 1.707614701 * s
147
+ ];
148
+ }
149
+ const CHROMA_BISECTION_STEPS = 20;
150
+ const fitsSrgb = (linear) => linear.every((channel) => channel >= -1e-4 && channel <= 1.0001);
151
+ function oklchToRgb(L, C, H) {
152
+ const lightness = clamp01(L);
153
+ let linear = oklchToLinearRgb(lightness, C, H);
154
+ if (!fitsSrgb(linear)) {
155
+ let fitting = 0;
156
+ let overflowing = C;
157
+ for (let step = 0; step < CHROMA_BISECTION_STEPS; step++) {
158
+ const candidate = (fitting + overflowing) / 2;
159
+ if (fitsSrgb(oklchToLinearRgb(lightness, candidate, H))) fitting = candidate;
160
+ else overflowing = candidate;
161
+ }
162
+ linear = oklchToLinearRgb(lightness, fitting, H);
163
+ }
164
+ const [r, g, b] = linear;
165
+ const encode = (channel) => clamp(linearToSrgb(channel) * 255, 0, 255);
141
166
  return {
142
- r: to(r),
143
- g: to(g),
144
- b: to(bl)
167
+ r: encode(r),
168
+ g: encode(g),
169
+ b: encode(b)
145
170
  };
146
171
  }
147
172
  function hsvaToOklcha(hsva) {
148
- const { r, g, b } = hsvaToRgba(hsva);
173
+ const { r, g, b } = hsvaToUnroundedRgba(hsva);
149
174
  const { L, C, H } = rgbToOklch(r, g, b);
150
175
  return {
151
176
  L,
@@ -463,14 +488,6 @@ function pointToSaturationValue(rect, offsetX, offsetY) {
463
488
  v: rect.height <= 0 ? 0 : (1 - clamp01(offsetY / rect.height)) * 100
464
489
  };
465
490
  }
466
- function formatGradient(stops, type, angle) {
467
- const list = [...stops].sort((a, b) => a.position - b.position).map((stop) => `${stop.color} ${round(clamp01(stop.position) * 100)}%`).join(", ");
468
- switch (type) {
469
- case "linear": return `linear-gradient(${round(angle)}deg, ${list})`;
470
- case "radial": return `radial-gradient(circle, ${list})`;
471
- case "conic": return `conic-gradient(from ${round(angle)}deg, ${list})`;
472
- }
473
- }
474
491
  function pointToHueSat(rect, offsetX, offsetY) {
475
492
  const cx = rect.width / 2;
476
493
  const cy = rect.height / 2;
@@ -487,4 +504,4 @@ function pointToHueSat(rect, offsetX, offsetY) {
487
504
  };
488
505
  }
489
506
  //#endregion
490
- export { formatColor, formatGradient, getChannels, hexToRgba, hslaToHsva, hslaToRgba, hsvaToHsla, hsvaToOklcha, hsvaToRgba, oklchaToHsva, parseColor, pointToHueSat, pointToSaturationValue, rgbaToHex, rgbaToHsla, rgbaToHsva, setChannel };
507
+ export { formatColor, getChannels, hexToRgba, hslaToHsva, hslaToRgba, hsvaToHsla, hsvaToOklcha, hsvaToRgba, oklchaToHsva, parseColor, pointToHueSat, pointToSaturationValue, preserveAchromatic, rgbaToHex, rgbaToHsla, rgbaToHsva, setChannel };
@@ -27,4 +27,10 @@ export declare function nextFixLevel(levels: WcagLevels): WcagLevel | null;
27
27
  export declare function contrastOf(color: Hsva, background: Rgba): number;
28
28
  export declare function formatContrastRatio(ratio: number): string;
29
29
  export declare function fixColorForContrast(color: Hsva, background: Rgba, target?: number): Hsva | null;
30
+ export type ContrastFailBand = {
31
+ saturation: number;
32
+ failsFrom: number;
33
+ failsTo: number;
34
+ };
35
+ export declare function contrastFailBands(color: Pick<Hsva, "h" | "a">, background: Rgba, target: number, columns?: number): ContrastFailBand[];
30
36
  //#endregion
@@ -72,5 +72,37 @@ function fixColorForContrast(color, background, target = AA_RATIO) {
72
72
  if (best) return pick(best.L);
73
73
  return ratioAtL(0) >= ratioAtL(1) ? pick(0) : pick(1);
74
74
  }
75
+ const BRIGHTNESS_BISECTION_STEPS = 12;
76
+ function lastPassingBrightness(passes, passing, failing) {
77
+ if (!passes(passing)) return passing;
78
+ let pass = passing;
79
+ let fail = failing;
80
+ for (let step = 0; step < BRIGHTNESS_BISECTION_STEPS; step++) {
81
+ const middle = (pass + fail) / 2;
82
+ if (passes(middle)) pass = middle;
83
+ else fail = middle;
84
+ }
85
+ return pass;
86
+ }
87
+ function contrastFailBands(color, background, target, columns = 32) {
88
+ const backgroundLuminance = luminanceFromRgb(background);
89
+ return Array.from({ length: columns + 1 }, (_, column) => {
90
+ const saturation = column / columns * 100;
91
+ const at = (brightness) => ({
92
+ h: color.h,
93
+ s: saturation,
94
+ v: brightness,
95
+ a: color.a
96
+ });
97
+ const darkerThanBackground = (brightness) => luminanceFromRgb(paintedOver(hsvaToRgba(at(brightness)), background)) <= backgroundLuminance;
98
+ const meetsTarget = (brightness) => contrastOf(at(brightness), background) >= target;
99
+ const matchingBackground = lastPassingBrightness(darkerThanBackground, 0, 100);
100
+ return {
101
+ saturation,
102
+ failsFrom: lastPassingBrightness(meetsTarget, 0, matchingBackground),
103
+ failsTo: lastPassingBrightness(meetsTarget, 100, matchingBackground)
104
+ };
105
+ });
106
+ }
75
107
  //#endregion
76
- export { AAA_RATIO, AA_RATIO, TARGET_RATIO, contrastOf, contrastRatio, fixColorForContrast, formatContrastRatio, luminanceFromRgb, nextFixLevel, wcagLevels };
108
+ export { AAA_RATIO, AA_RATIO, TARGET_RATIO, contrastFailBands, contrastOf, contrastRatio, fixColorForContrast, formatContrastRatio, luminanceFromRgb, nextFixLevel, wcagLevels };
@@ -0,0 +1,17 @@
1
+ //#region src/lib/gradient.d.ts
2
+ export type GradientType = "linear" | "radial" | "conic";
3
+ export type GradientInterpolation = "srgb" | "oklab" | "oklch";
4
+ export type GradientStop = {
5
+ id: string;
6
+ position: number;
7
+ color: string;
8
+ };
9
+ export type GradientValue = {
10
+ type: GradientType;
11
+ angle: number;
12
+ interpolation: GradientInterpolation;
13
+ stops: GradientStop[];
14
+ };
15
+ export declare function formatGradient({ type, angle, interpolation, stops }: GradientValue): string;
16
+ export declare function gradientColorAt({ stops, interpolation }: Pick<GradientValue, "stops" | "interpolation">, position: number): string;
17
+ //#endregion
@@ -0,0 +1,69 @@
1
+ import { formatColor, hsvaToOklcha, hsvaToRgba, oklchaToHsva, parseColor, rgbaToHsva } from "./color.js";
2
+ //#region src/lib/gradient.ts
3
+ const clamp01 = (n) => Math.min(1, Math.max(0, n));
4
+ const lerp = (from, to, t) => from + (to - from) * t;
5
+ const byPosition = (a, b) => a.position - b.position;
6
+ function formatGradient({ type, angle, interpolation, stops }) {
7
+ const space = interpolation === "srgb" ? "" : ` in ${interpolation}`;
8
+ const list = [...stops].sort(byPosition).map((stop) => `${stop.color} ${Math.round(clamp01(stop.position) * 100)}%`).join(", ");
9
+ switch (type) {
10
+ case "linear": return `linear-gradient(${Math.round(angle)}deg${space}, ${list})`;
11
+ case "radial": return `radial-gradient(circle${space}, ${list})`;
12
+ case "conic": return `conic-gradient(from ${Math.round(angle)}deg${space}, ${list})`;
13
+ }
14
+ }
15
+ function mixSrgb(from, to, t) {
16
+ const a = hsvaToRgba(from);
17
+ const b = hsvaToRgba(to);
18
+ return rgbaToHsva({
19
+ r: lerp(a.r, b.r, t),
20
+ g: lerp(a.g, b.g, t),
21
+ b: lerp(a.b, b.b, t),
22
+ a: lerp(a.a, b.a, t)
23
+ });
24
+ }
25
+ function mixOklab(from, to, t) {
26
+ const a = hsvaToOklcha(from);
27
+ const b = hsvaToOklcha(to);
28
+ const radians = (hue) => hue * Math.PI / 180;
29
+ const greenRed = lerp(a.C * Math.cos(radians(a.H)), b.C * Math.cos(radians(b.H)), t);
30
+ const blueYellow = lerp(a.C * Math.sin(radians(a.H)), b.C * Math.sin(radians(b.H)), t);
31
+ return oklchaToHsva({
32
+ L: lerp(a.L, b.L, t),
33
+ C: Math.hypot(greenRed, blueYellow),
34
+ H: Math.atan2(blueYellow, greenRed) * 180 / Math.PI,
35
+ a: lerp(a.a, b.a, t)
36
+ });
37
+ }
38
+ function mixOklch(from, to, t) {
39
+ const a = hsvaToOklcha(from);
40
+ const b = hsvaToOklcha(to);
41
+ const shorterArc = (b.H - a.H + 540) % 360 - 180;
42
+ return oklchaToHsva({
43
+ L: lerp(a.L, b.L, t),
44
+ C: lerp(a.C, b.C, t),
45
+ H: a.H + shorterArc * t,
46
+ a: lerp(a.a, b.a, t)
47
+ });
48
+ }
49
+ const MIX = {
50
+ srgb: mixSrgb,
51
+ oklab: mixOklab,
52
+ oklch: mixOklch
53
+ };
54
+ function gradientColorAt({ stops, interpolation }, position) {
55
+ const sorted = [...stops].sort(byPosition);
56
+ const before = sorted.findLast((stop) => stop.position <= position);
57
+ const after = sorted.find((stop) => stop.position >= position);
58
+ const nearest = before ?? after;
59
+ if (!nearest) return "#ffffff";
60
+ if (!before || !after) return nearest.color;
61
+ const from = parseColor(before.color);
62
+ const to = parseColor(after.color);
63
+ if (!from || !to) return nearest.color;
64
+ const span = after.position - before.position;
65
+ const t = span === 0 ? 0 : (position - before.position) / span;
66
+ return formatColor(MIX[interpolation](from, to, t), "hex");
67
+ }
68
+ //#endregion
69
+ export { formatGradient, gradientColorAt };
@@ -2,6 +2,7 @@
2
2
  import { Button } from "../ui/button.js";
3
3
  import { LiveStatus } from "../ui/live-status.js";
4
4
  import { useMarkdownEditor } from "./context.js";
5
+ import { createDragPreview } from "./drag-preview.js";
5
6
  import { useEffect, useRef, useState } from "react";
6
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
8
  import { GripVerticalIcon } from "lucide-react";
@@ -14,9 +15,14 @@ function MarkdownEditorDragHandle({ container }) {
14
15
  const [announcement, setAnnouncement] = useState("");
15
16
  const button = useRef(null);
16
17
  const dragging = useRef(false);
18
+ const preview = useRef(null);
19
+ const preparedDrag = useRef(null);
17
20
  useEffect(() => {
18
21
  const surface = container.current;
19
- if (!editor || !surface || disabled || source) return;
22
+ if (!editor || editor.isDestroyed || !surface || disabled || source) return;
23
+ const view = editor.view;
24
+ let frame = 0;
25
+ let latestEvent = null;
20
26
  function show(blockPosition) {
21
27
  if (!editor || !surface) return;
22
28
  const node = editor.view.nodeDOM(blockPosition);
@@ -27,7 +33,7 @@ function MarkdownEditorDragHandle({ container }) {
27
33
  top
28
34
  });
29
35
  }
30
- function track(event) {
36
+ function updateHandle(event) {
31
37
  if (!editor || dragging.current || button.current?.contains(event.target instanceof Node ? event.target : null)) return;
32
38
  const rect = editor.view.dom.getBoundingClientRect();
33
39
  const padding = Number.parseFloat(getComputedStyle(editor.view.dom).paddingInlineStart);
@@ -39,22 +45,38 @@ function MarkdownEditorDragHandle({ container }) {
39
45
  const resolved = editor.state.doc.resolve(hit.pos);
40
46
  show(resolved.depth > 0 ? resolved.before(1) : hit.pos);
41
47
  }
48
+ function track(event) {
49
+ latestEvent = event;
50
+ if (frame) return;
51
+ frame = requestAnimationFrame(() => {
52
+ frame = 0;
53
+ if (latestEvent) updateHandle(latestEvent);
54
+ });
55
+ }
42
56
  function selectionChanged() {
43
57
  if (!editor || dragging.current) return;
44
58
  const { $from } = editor.state.selection;
45
59
  show($from.depth > 0 ? $from.before(1) : $from.pos);
46
60
  }
47
61
  function hide() {
62
+ cancelAnimationFrame(frame);
63
+ frame = 0;
64
+ latestEvent = null;
48
65
  if (!dragging.current && document.activeElement !== button.current) setBlock(null);
49
66
  }
50
67
  surface.addEventListener("mousemove", track);
51
68
  surface.addEventListener("mouseleave", hide);
52
69
  editor.on("transaction", selectionChanged).on("focus", selectionChanged);
53
70
  return () => {
71
+ cancelAnimationFrame(frame);
54
72
  surface.removeEventListener("mousemove", track);
55
73
  surface.removeEventListener("mouseleave", hide);
56
74
  editor.off("transaction", selectionChanged).off("focus", selectionChanged);
57
- editor.view.dragging = null;
75
+ view.dragging = null;
76
+ preview.current?.remove();
77
+ preview.current = null;
78
+ preparedDrag.current = null;
79
+ dragging.current = false;
58
80
  };
59
81
  }, [
60
82
  editor,
@@ -65,6 +87,21 @@ function MarkdownEditorDragHandle({ container }) {
65
87
  if (!editor || disabled || source) return null;
66
88
  const selected = editor.state.selection.$from;
67
89
  const position = block?.position ?? (selected.depth > 0 ? selected.before(1) : selected.pos);
90
+ function prepareDrag() {
91
+ if (!editor || editor.isDestroyed) return null;
92
+ const selection = NodeSelection.create(editor.state.doc, position);
93
+ const node = editor.view.nodeDOM(position);
94
+ const { dom, text, slice } = editor.view.serializeForClipboard(selection.content());
95
+ return {
96
+ position,
97
+ document: editor.state.doc,
98
+ selection,
99
+ html: dom.innerHTML,
100
+ text,
101
+ slice,
102
+ preview: node instanceof HTMLElement ? createDragPreview(node) : null
103
+ };
104
+ }
68
105
  function move(direction) {
69
106
  if (!editor) return;
70
107
  const { doc, tr } = editor.state;
@@ -90,33 +127,59 @@ function MarkdownEditorDragHandle({ container }) {
90
127
  "aria-description": "Drag this block, or use Alt+ArrowUp and Alt+ArrowDown to move it.",
91
128
  title: "Drag to reorder · Alt+↑/↓ to move",
92
129
  draggable: true,
93
- style: { top: block?.top },
130
+ style: {
131
+ top: block?.top,
132
+ "--cui-button-background": "transparent",
133
+ "--cui-button-hover-background": "transparent",
134
+ "--cui-button-press-background": "transparent",
135
+ "--cui-button-shadow": "none",
136
+ "--cui-button-hover-shadow": "none"
137
+ },
94
138
  onClick: () => editor.commands.setNodeSelection(position),
139
+ onPointerDown: () => {
140
+ preparedDrag.current = prepareDrag();
141
+ },
142
+ onPointerUp: () => {
143
+ preparedDrag.current = null;
144
+ },
95
145
  onKeyDown: (event) => {
96
146
  if (!event.altKey || !["ArrowUp", "ArrowDown"].includes(event.key)) return;
97
147
  event.preventDefault();
98
148
  move(event.key === "ArrowUp" ? -1 : 1);
99
149
  },
100
150
  onDragStart: (event) => {
151
+ const prepared = preparedDrag.current;
152
+ const payload = prepared?.position === position && prepared.document === editor.state.doc ? prepared : prepareDrag();
153
+ preparedDrag.current = null;
154
+ if (!payload) {
155
+ event.preventDefault();
156
+ return;
157
+ }
101
158
  const { view } = editor;
102
- const selection = NodeSelection.create(view.state.doc, position);
103
- const node = view.nodeDOM(position);
159
+ const { html, text, slice, selection, preview: ghost } = payload;
160
+ dragging.current = true;
104
161
  view.dispatch(view.state.tr.setSelection(selection));
105
- const { dom, text, slice } = view.serializeForClipboard(selection.content());
106
162
  event.dataTransfer.clearData();
107
- event.dataTransfer.setData("text/html", dom.innerHTML);
163
+ event.dataTransfer.setData("text/html", html);
108
164
  event.dataTransfer.setData("text/plain", text);
109
165
  event.dataTransfer.effectAllowed = "copyMove";
110
- if (node instanceof HTMLElement) event.dataTransfer.setDragImage(node, 0, 0);
166
+ if (ghost) {
167
+ preview.current?.remove();
168
+ container.current?.append(ghost);
169
+ preview.current = ghost;
170
+ event.dataTransfer.setDragImage(ghost, 0, 0);
171
+ }
111
172
  view.dragging = {
112
173
  slice,
113
174
  move: true
114
175
  };
115
- dragging.current = true;
116
176
  },
117
177
  onDragEnd: () => {
118
178
  editor.view.dragging = null;
119
179
  dragging.current = false;
180
+ preview.current?.remove();
181
+ preview.current = null;
182
+ preparedDrag.current = null;
120
183
  setBlock(null);
121
184
  },
122
185
  children: /* @__PURE__ */ jsx(GripVerticalIcon, { "aria-hidden": "true" })
@@ -0,0 +1,3 @@
1
+ //#region src/markdown-editor/drag-preview.d.ts
2
+ export declare function createDragPreview(node: HTMLElement): HTMLDivElement;
3
+ //#endregion
@@ -0,0 +1,31 @@
1
+ //#region src/markdown-editor/drag-preview.ts
2
+ function createDragPreview(node) {
3
+ const preview = document.createElement("div");
4
+ preview.className = "tiptap";
5
+ preview.dataset.markdownDragPreview = "";
6
+ preview.setAttribute("aria-hidden", "true");
7
+ preview.style.inlineSize = `${Math.min(node.getBoundingClientRect().width, 320)}px`;
8
+ let remainingNodes = 80;
9
+ let remainingText = 2e3;
10
+ function clonePreview(source) {
11
+ remainingNodes--;
12
+ const clone = source.cloneNode(false);
13
+ if (clone instanceof HTMLElement) {
14
+ clone.classList.remove("ProseMirror-selectednode");
15
+ clone.removeAttribute("id");
16
+ }
17
+ if (clone instanceof Text) {
18
+ clone.data = clone.data.slice(0, remainingText);
19
+ remainingText -= clone.data.length;
20
+ }
21
+ for (const child of source.childNodes) {
22
+ if (remainingNodes === 0 || remainingText === 0) break;
23
+ clone.appendChild(clonePreview(child));
24
+ }
25
+ return clone;
26
+ }
27
+ preview.append(clonePreview(node));
28
+ return preview;
29
+ }
30
+ //#endregion
31
+ export { createDragPreview };
@@ -1,9 +1,9 @@
1
- import { JSONContent, Node } from "@tiptap/core";
1
+ import { Extension, JSONContent, Node } from "@tiptap/core";
2
2
  import { MarkdownManager } from "@tiptap/markdown";
3
3
  //#region src/markdown-editor/extensions.d.ts
4
4
  export declare function isEditorUrl(value: string, image?: boolean): boolean;
5
5
  export declare const ImageUpload: Node<any, any>;
6
- export declare function markdownExtensions(): (import("@tiptap/core").Extension<import("@tiptap/markdown").MarkdownExtensionOptions, import("@tiptap/markdown").MarkdownExtensionStorage> | import("@tiptap/core").Extension<import("@tiptap/starter-kit").StarterKitOptions, any> | import("@tiptap/core").Extension<import("@tiptap/extension-table").TableKitOptions, any> | Node<any, any> | Node<import("@tiptap/extension-image").ImageOptions, any> | Node<import("@tiptap/extension-paragraph").ParagraphOptions, any> | Node<import("@tiptap/extension-list").TaskItemOptions, any> | Node<import("@tiptap/extension-list").TaskListOptions, any>)[];
6
+ export declare function markdownExtensions(): (Extension<any, any> | Extension<import("@tiptap/markdown").MarkdownExtensionOptions, import("@tiptap/markdown").MarkdownExtensionStorage> | Extension<import("@tiptap/starter-kit").StarterKitOptions, any> | Extension<import("@tiptap/extension-table").TableKitOptions, any> | Node<any, any> | Node<import("@tiptap/extension-image").ImageOptions, any> | Node<import("@tiptap/extension-paragraph").ParagraphOptions, any> | Node<import("@tiptap/extension-list").TaskItemOptions, any> | Node<import("@tiptap/extension-list").TaskListOptions, any>)[];
7
7
  export declare function createMarkdownCodec(): MarkdownManager;
8
8
  export declare function requiresMarkdownSource(markdown: string, codec: MarkdownManager): boolean;
9
9
  export declare function hasMarkdownContent(document: JSONContent): boolean;
@@ -1,11 +1,32 @@
1
- import { Node } from "@tiptap/core";
1
+ import { markdownHeadingAttributes } from "../ui/markdown-heading.js";
2
+ import { MarkdownTaskItem } from "./task-item.js";
3
+ import { Extension, Node } from "@tiptap/core";
2
4
  import Image from "@tiptap/extension-image";
3
- import { TaskItem, TaskList } from "@tiptap/extension-list";
5
+ import { TaskList } from "@tiptap/extension-list";
4
6
  import Paragraph from "@tiptap/extension-paragraph";
5
7
  import { TableKit } from "@tiptap/extension-table";
6
8
  import { Markdown, MarkdownManager } from "@tiptap/markdown";
7
9
  import StarterKit from "@tiptap/starter-kit";
8
10
  //#region src/markdown-editor/extensions.ts
11
+ const MarkdownHeadings = Extension.create({
12
+ name: "markdownHeadings",
13
+ addGlobalAttributes() {
14
+ return [{
15
+ types: ["heading"],
16
+ attributes: { markdownHeading: {
17
+ default: null,
18
+ renderHTML: (attributes) => markdownHeadingAttributes([
19
+ 1,
20
+ 2,
21
+ 3,
22
+ 4,
23
+ 5,
24
+ 6
25
+ ].find((level) => level === attributes.level) ?? 1)
26
+ } }
27
+ }];
28
+ }
29
+ });
9
30
  function isEditorUrl(value, image = false) {
10
31
  if (!value || /[\s\\]/.test(value) || Array.from(value).some((character) => character.charCodeAt(0) < 32)) return false;
11
32
  if (value.startsWith("/") && !value.startsWith("//")) return true;
@@ -78,12 +99,13 @@ function markdownExtensions() {
78
99
  }
79
100
  }),
80
101
  MarkdownParagraph,
102
+ MarkdownHeadings,
81
103
  SafeImage.configure({
82
104
  allowBase64: false,
83
105
  inline: true
84
106
  }),
85
107
  TaskList,
86
- TaskItem.configure({
108
+ MarkdownTaskItem.configure({
87
109
  nested: true,
88
110
  HTMLAttributes: { "data-type": "taskItem" }
89
111
  }),
@@ -133,20 +133,20 @@ function MarkdownEditorSuggestions({ mentions = [], onMentionSelect }) {
133
133
  }
134
134
  });
135
135
  const report = useEffectEvent(() => {
136
- if (!editor) return;
136
+ if (!editor || editor.isDestroyed) return;
137
137
  const match = enabled ? readTrigger(editor) : null;
138
138
  const coords = match ? editor.view.coordsAtPos(match.end) : null;
139
139
  controller.report(match, coords ? new DOMRect(coords.left, coords.top, 1, coords.bottom - coords.top) : null);
140
140
  });
141
141
  const handleKeyDown = useEffectEvent((event) => {
142
- if (!editor || !enabled || isComposingKey(event) || event.metaKey || event.ctrlKey || event.altKey || !readTrigger(editor)) return;
142
+ if (!editor || editor.isDestroyed || !enabled || isComposingKey(event) || event.metaKey || event.ctrlKey || event.altKey || !readTrigger(editor)) return;
143
143
  if (controller.handleKeyDown(event.key)) {
144
144
  event.preventDefault();
145
145
  event.stopImmediatePropagation();
146
146
  }
147
147
  });
148
148
  useEffect(() => {
149
- if (!editor || !enabled) return;
149
+ if (!editor || editor.isDestroyed || !enabled) return;
150
150
  const dom = editor.view.dom;
151
151
  editor.on("transaction", report).on("focus", report).on("blur", report);
152
152
  dom.addEventListener("keydown", handleKeyDown, true);
@@ -157,7 +157,7 @@ function MarkdownEditorSuggestions({ mentions = [], onMentionSelect }) {
157
157
  }, [editor, enabled]);
158
158
  const open = enabled && controller.open;
159
159
  useEffect(() => {
160
- if (!editor || !open) return;
160
+ if (!editor || editor.isDestroyed || !open) return;
161
161
  const owner = editor.view.dom.ownerDocument;
162
162
  owner.addEventListener("scroll", report, {
163
163
  capture: true,
@@ -180,7 +180,7 @@ function MarkdownEditorSuggestions({ mentions = [], onMentionSelect }) {
180
180
  ]);
181
181
  const { "aria-controls": controls, "aria-activedescendant": activeDescendant } = controller.inputAria;
182
182
  useEffect(() => {
183
- if (!editor || !enabled) return;
183
+ if (!editor || editor.isDestroyed || !enabled) return;
184
184
  const dom = editor.view.dom;
185
185
  const attributes = {
186
186
  "aria-autocomplete": "list",
@@ -0,0 +1,3 @@
1
+ //#region src/markdown-editor/task-item.d.ts
2
+ export declare const MarkdownTaskItem: import("@tiptap/core").Node<import("@tiptap/extension-list").TaskItemOptions, any>;
3
+ //#endregion
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { Checkbox } from "../ui/checkbox.js";
3
+ import { useMarkdownEditor } from "./context.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react";
6
+ import { closeHistory } from "@tiptap/pm/history";
7
+ import { TaskItem } from "@tiptap/extension-list";
8
+ //#region src/markdown-editor/task-item.tsx
9
+ function MarkdownTaskItemView({ editor, node, getPos }) {
10
+ const { disabled } = useMarkdownEditor();
11
+ return /* @__PURE__ */ jsxs(NodeViewWrapper, {
12
+ "data-control-ui": "markdown-editor",
13
+ "data-control-family": "markdown-editor",
14
+ "data-slot": "task-item",
15
+ children: [/* @__PURE__ */ jsx("span", {
16
+ contentEditable: false,
17
+ "data-control-ui": "markdown-editor",
18
+ "data-control-family": "markdown-editor",
19
+ "data-slot": "task-checkbox",
20
+ children: /* @__PURE__ */ jsx(Checkbox, {
21
+ checked: Boolean(node.attrs.checked),
22
+ disabled,
23
+ "aria-label": node.firstChild?.textContent || "Task item",
24
+ onCheckedChange: (checked) => {
25
+ const position = getPos();
26
+ if (!editor.isEditable || position === void 0) return;
27
+ editor.view.dispatch(closeHistory(editor.state.tr).setNodeMarkup(position, void 0, {
28
+ ...node.attrs,
29
+ checked
30
+ }));
31
+ }
32
+ })
33
+ }), /* @__PURE__ */ jsx(NodeViewContent, {
34
+ "data-control-ui": "markdown-editor",
35
+ "data-control-family": "markdown-editor",
36
+ "data-slot": "task-content"
37
+ })]
38
+ });
39
+ }
40
+ const MarkdownTaskItem = TaskItem.extend({ addNodeView() {
41
+ return ReactNodeViewRenderer(MarkdownTaskItemView, {
42
+ as: "li",
43
+ attrs: ({ node }) => ({
44
+ "data-type": "taskItem",
45
+ "data-checked": String(node.attrs.checked)
46
+ })
47
+ });
48
+ } });
49
+ //#endregion
50
+ export { MarkdownTaskItem };