@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.
- package/README.md +1 -0
- package/dist/action-bar.d.ts +1 -2
- package/dist/action-bar.js +7 -8
- package/dist/blocks/design-canvas-inspector.js +4 -1
- package/dist/blocks/file-explorer.js +1 -1
- package/dist/chat-message.js +4 -13
- package/dist/hooks/use-textarea-trigger-menu.d.ts +21 -3
- package/dist/hooks/use-textarea-trigger-menu.js +36 -40
- package/dist/hooks/use-user-ask.d.ts +1 -0
- package/dist/hooks/use-user-ask.js +5 -1
- package/dist/knob-contracts/audio-recorder-knobs.d.ts +1 -1
- package/dist/knob-contracts/audio-recorder-knobs.js +2 -1
- package/dist/knob-contracts/chart-knobs.d.ts +4 -0
- package/dist/knob-contracts/chart-knobs.js +24 -0
- package/dist/knob-contracts/choice-knobs.d.ts +1 -1
- package/dist/knob-contracts/choice-knobs.js +1 -0
- package/dist/knob-contracts/dockable-panel-knobs.d.ts +1 -1
- package/dist/knob-contracts/dockable-panel-knobs.js +2 -1
- package/dist/knob-contracts/emoji-picker-knobs.d.ts +1 -1
- package/dist/knob-contracts/emoji-picker-knobs.js +1 -0
- package/dist/knob-contracts/empty-knobs.d.ts +1 -1
- package/dist/knob-contracts/empty-knobs.js +2 -1
- package/dist/knob-contracts/icon-picker-knobs.d.ts +1 -1
- package/dist/knob-contracts/icon-picker-knobs.js +3 -1
- package/dist/knob-contracts/resizable-knobs.d.ts +1 -1
- package/dist/knob-contracts/resizable-knobs.js +2 -2
- package/dist/knob-contracts/sidebar-knobs.d.ts +1 -1
- package/dist/knob-contracts/sidebar-knobs.js +3 -1
- package/dist/knob-contracts/tabs-knobs.d.ts +1 -1
- package/dist/knob-contracts/tabs-knobs.js +2 -1
- package/dist/knob-contracts/tree-knobs.d.ts +1 -1
- package/dist/knob-contracts/tree-knobs.js +2 -1
- package/dist/lib/color.d.ts +2 -6
- package/dist/lib/color.js +45 -28
- package/dist/lib/contrast.d.ts +6 -0
- package/dist/lib/contrast.js +33 -1
- package/dist/lib/gradient.d.ts +17 -0
- package/dist/lib/gradient.js +69 -0
- package/dist/markdown-editor/drag-handle.js +73 -10
- package/dist/markdown-editor/drag-preview.d.ts +3 -0
- package/dist/markdown-editor/drag-preview.js +31 -0
- package/dist/markdown-editor/extensions.d.ts +2 -2
- package/dist/markdown-editor/extensions.js +25 -3
- package/dist/markdown-editor/suggestions.js +5 -5
- package/dist/markdown-editor/task-item.d.ts +3 -0
- package/dist/markdown-editor/task-item.js +50 -0
- package/dist/markdown-editor/toolbar.js +122 -63
- package/dist/markdown-editor.js +3 -3
- package/dist/skin.d.ts +0 -1
- package/dist/styles/index.css +3 -2
- package/dist/styles/recipes/action-bar.css +1 -1
- package/dist/styles/recipes/activity.css +1 -1
- package/dist/styles/recipes/alert.css +9 -10
- package/dist/styles/recipes/app-shell-header.css +2 -2
- package/dist/styles/recipes/audio-recorder.css +15 -4
- package/dist/styles/recipes/audio-visualizer.css +4 -4
- package/dist/styles/recipes/avatar.css +3 -3
- package/dist/styles/recipes/badge.css +2 -2
- package/dist/styles/recipes/button.css +2 -5
- package/dist/styles/recipes/card.css +0 -2
- package/dist/styles/recipes/chart.css +378 -0
- package/dist/styles/recipes/chat-composer-attachment.css +3 -3
- package/dist/styles/recipes/chat-composer.css +3 -3
- package/dist/styles/recipes/chat-layout.css +2 -2
- package/dist/styles/recipes/chat-message-social.css +1 -1
- package/dist/styles/recipes/chat-message.css +1 -1
- package/dist/styles/recipes/choice.css +9 -3
- package/dist/styles/recipes/code-diff.css +1 -1
- package/dist/styles/recipes/code.css +1 -1
- package/dist/styles/recipes/color-picker-details.css +17 -11
- package/dist/styles/recipes/color-picker-values.css +17 -7
- package/dist/styles/recipes/color-picker.css +88 -70
- package/dist/styles/recipes/context.css +2 -2
- package/dist/styles/recipes/dockable-panel.css +9 -2
- package/dist/styles/recipes/dynamic-notification-motion.css +1 -55
- package/dist/styles/recipes/dynamic-notification.css +0 -1
- package/dist/styles/recipes/emoji-picker.css +35 -19
- package/dist/styles/recipes/empty.css +19 -14
- package/dist/styles/recipes/field-parts.css +2 -2
- package/dist/styles/recipes/field.css +6 -4
- package/dist/styles/recipes/filter-bar.css +10 -10
- package/dist/styles/recipes/flow.css +5 -3
- package/dist/styles/recipes/gradient-editor.css +8 -0
- package/dist/styles/recipes/icon-picker.css +45 -10
- package/dist/styles/recipes/inline-attachment.css +4 -4
- package/dist/styles/recipes/inline-citation.css +2 -2
- package/dist/styles/recipes/item.css +4 -4
- package/dist/styles/recipes/markdown-editor.css +35 -24
- package/dist/styles/recipes/markdown.css +23 -27
- package/dist/styles/recipes/page-layout.css +13 -11
- package/dist/styles/recipes/popup-controls.css +2 -2
- package/dist/styles/recipes/popup-lists.css +3 -3
- package/dist/styles/recipes/popup.css +11 -9
- package/dist/styles/recipes/range.css +4 -5
- package/dist/styles/recipes/resizable.css +15 -10
- package/dist/styles/recipes/resize-handle.css +2 -2
- package/dist/styles/recipes/scroll-area.css +1 -1
- package/dist/styles/recipes/sidebar-menu.css +5 -15
- package/dist/styles/recipes/sidebar-mobile.css +4 -4
- package/dist/styles/recipes/sidebar-parts.css +26 -1
- package/dist/styles/recipes/sidebar.css +18 -31
- package/dist/styles/recipes/skeleton.css +10 -5
- package/dist/styles/recipes/stepper.css +1 -1
- package/dist/styles/recipes/switch.css +12 -11
- package/dist/styles/recipes/table-of-contents.css +6 -8
- package/dist/styles/recipes/table.css +4 -4
- package/dist/styles/recipes/tabs.css +10 -5
- package/dist/styles/recipes/task-list.css +3 -3
- package/dist/styles/recipes/tree.css +10 -3
- package/dist/styles/recipes/user-ask-details.css +5 -4
- package/dist/styles/recipes/user-ask.css +5 -5
- package/dist/styles/theme.css +158 -177
- package/dist/ui/chart-cartesian.d.ts +801 -0
- package/dist/ui/chart-cartesian.js +420 -0
- package/dist/ui/chart-colors.d.ts +7 -0
- package/dist/ui/chart-colors.js +27 -0
- package/dist/ui/chart-curves.d.ts +7 -0
- package/dist/ui/chart-curves.js +87 -0
- package/dist/ui/chart-marks.d.ts +68 -0
- package/dist/ui/chart-marks.js +227 -0
- package/dist/ui/chart-polar.d.ts +103 -0
- package/dist/ui/chart-polar.js +124 -0
- package/dist/ui/chart-series.d.ts +36 -0
- package/dist/ui/chart-series.js +71 -0
- package/dist/ui/chart.d.ts +32 -0
- package/dist/ui/chart.js +257 -0
- package/dist/ui/color-picker-area.d.ts +16 -0
- package/dist/ui/color-picker-area.js +175 -0
- package/dist/ui/color-picker-context.d.ts +27 -0
- package/dist/ui/color-picker-context.js +87 -0
- package/dist/ui/color-picker-contrast.d.ts +20 -0
- package/dist/ui/color-picker-contrast.js +129 -0
- package/dist/ui/color-picker-sliders.d.ts +15 -0
- package/dist/ui/color-picker-sliders.js +146 -0
- package/dist/ui/color-picker-swatches.d.ts +23 -0
- package/dist/ui/color-picker-swatches.js +94 -0
- package/dist/ui/color-picker-values.d.ts +59 -0
- package/dist/ui/color-picker-values.js +244 -0
- package/dist/ui/color-picker.d.ts +15 -110
- package/dist/ui/color-picker.js +30 -759
- package/dist/ui/emoji-picker.d.ts +10 -1
- package/dist/ui/emoji-picker.js +71 -14
- package/dist/ui/flow.js +1 -1
- package/dist/ui/gradient-editor-controls.d.ts +19 -0
- package/dist/ui/gradient-editor-controls.js +75 -0
- package/dist/ui/gradient-editor.d.ts +23 -22
- package/dist/ui/gradient-editor.js +77 -123
- package/dist/ui/icon-picker.d.ts +1 -1
- package/dist/ui/icon-picker.js +11 -6
- package/dist/ui/markdown-elements.d.ts +3 -1
- package/dist/ui/markdown-elements.js +62 -22
- package/dist/ui/markdown-heading.d.ts +9 -0
- package/dist/ui/markdown-heading.js +25 -0
- package/dist/ui/resizable.js +24 -9
- package/dist/ui/sidebar-provider.js +3 -12
- package/dist/ui/typography.d.ts +1 -0
- package/dist/ui/typography.js +12 -3
- package/dist/user-ask.js +4 -8
- package/package.json +7 -3
- package/dist/ui/emoji-icon-picker.d.ts +0 -13
- 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
|
|
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:
|
|
48
|
-
g:
|
|
49
|
-
b:
|
|
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
|
|
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
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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:
|
|
143
|
-
g:
|
|
144
|
-
b:
|
|
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 } =
|
|
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,
|
|
507
|
+
export { formatColor, getChannels, hexToRgba, hslaToHsva, hslaToRgba, hsvaToHsla, hsvaToOklcha, hsvaToRgba, oklchaToHsva, parseColor, pointToHueSat, pointToSaturationValue, preserveAchromatic, rgbaToHex, rgbaToHsla, rgbaToHsva, setChannel };
|
package/dist/lib/contrast.d.ts
CHANGED
|
@@ -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
|
package/dist/lib/contrast.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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: {
|
|
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 =
|
|
103
|
-
|
|
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",
|
|
163
|
+
event.dataTransfer.setData("text/html", html);
|
|
108
164
|
event.dataTransfer.setData("text/plain", text);
|
|
109
165
|
event.dataTransfer.effectAllowed = "copyMove";
|
|
110
|
-
if (
|
|
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,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(): (
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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,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 };
|