@hyperframes/studio 0.7.6 → 0.7.8
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/dist/assets/{index-B_NnmU6q.js → index--lOAjFJl.js} +1 -1
- package/dist/assets/{index-ysftPins.js → index-BSyZKYmH.js} +204 -203
- package/dist/assets/{index-DsBhGFPe.js → index-Dgeszckd.js} +1 -1
- package/dist/assets/index-svYFaNuq.css +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.html +2 -2
- package/dist/index.js +3650 -2431
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/components/StudioRightPanel.tsx +7 -1
- package/src/components/editor/AnimationCard.tsx +6 -0
- package/src/components/editor/DomEditOverlay.tsx +4 -12
- package/src/components/editor/EaseCurveSection.tsx +175 -99
- package/src/components/editor/GsapAnimationSection.tsx +2 -0
- package/src/components/editor/KeyframeEaseList.tsx +67 -3
- package/src/components/editor/MarqueeOverlay.tsx +40 -0
- package/src/components/editor/OffCanvasIndicators.tsx +2 -7
- package/src/components/editor/PropertyPanel.tsx +23 -2
- package/src/components/editor/Transform3DCube.tsx +313 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +2 -0
- package/src/components/editor/gsapAnimationConstants.ts +11 -35
- package/src/components/editor/gsapAnimationHelpers.test.ts +1 -1
- package/src/components/editor/marqueeCommit.ts +63 -20
- package/src/components/editor/propertyPanel3dTransform.tsx +311 -92
- package/src/components/editor/propertyPanelHelpers.ts +46 -18
- package/src/components/editor/propertyPanelTimingSection.tsx +35 -2
- package/src/components/editor/transform3dProjection.test.ts +99 -0
- package/src/components/editor/transform3dProjection.ts +172 -0
- package/src/components/editor/useMotionPathData.ts +2 -1
- package/src/components/sidebar/AssetContextMenu.tsx +97 -0
- package/src/components/sidebar/AssetsTab.tsx +364 -266
- package/src/components/sidebar/AudioRow.tsx +202 -0
- package/src/components/sidebar/assetHelpers.ts +40 -0
- package/src/contexts/DomEditContext.tsx +8 -0
- package/src/hooks/gsapDragCommit.test.ts +9 -5
- package/src/hooks/gsapDragCommit.ts +129 -9
- package/src/hooks/gsapRuntimeBridge.ts +22 -0
- package/src/hooks/gsapRuntimeKeyframes.test.ts +47 -0
- package/src/hooks/gsapRuntimeKeyframes.ts +63 -5
- package/src/hooks/gsapRuntimePatch.ts +162 -35
- package/src/hooks/useAnimatedPropertyCommit.ts +256 -78
- package/src/hooks/useDomEditCommits.ts +0 -2
- package/src/hooks/useDomEditSession.ts +24 -2
- package/src/hooks/useDomEditWiring.ts +3 -0
- package/src/hooks/useDomGeometryCommits.ts +3 -31
- package/src/hooks/useGestureCommit.ts +0 -4
- package/src/hooks/useGsapAwareEditing.ts +31 -5
- package/src/hooks/useGsapKeyframeOps.ts +4 -1
- package/src/hooks/useGsapScriptCommits.ts +0 -12
- package/src/hooks/useGsapSelectionHandlers.ts +12 -9
- package/src/hooks/useGsapTweenCache.test.ts +45 -1
- package/src/hooks/useGsapTweenCache.ts +125 -42
- package/src/hooks/useMusicBeatAnalysis.ts +36 -21
- package/src/hooks/useRazorSplit.ts +0 -3
- package/src/utils/marqueeGeometry.test.ts +15 -98
- package/src/utils/marqueeGeometry.ts +1 -158
- package/dist/assets/index-wJZf6FK3.css +0 -1
- package/src/utils/editDebugLog.ts +0 -9
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { projectAxes, projectCubeFaces, rotate, wrapDeg } from "./transform3dProjection";
|
|
3
|
+
|
|
4
|
+
const OPTS = { cx: 50, cy: 50, r: 30, persp: 4 };
|
|
5
|
+
|
|
6
|
+
describe("projectCubeFaces", () => {
|
|
7
|
+
it("shows the front face (and only front-facing faces) at identity", () => {
|
|
8
|
+
const faces = projectCubeFaces(0, 0, 0, OPTS);
|
|
9
|
+
const ids = faces.map((f) => f.id);
|
|
10
|
+
expect(ids).toContain("front");
|
|
11
|
+
// Head-on: side/top/bottom normals are edge-on (n.z ≈ 0) → culled.
|
|
12
|
+
expect(ids).not.toContain("back");
|
|
13
|
+
expect(faces.length).toBeGreaterThanOrEqual(1);
|
|
14
|
+
expect(faces.length).toBeLessThanOrEqual(3);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("reveals the top face when tilted forward on X", () => {
|
|
18
|
+
const ids = projectCubeFaces(45, 0, 0, OPTS).map((f) => f.id);
|
|
19
|
+
expect(ids).toContain("front");
|
|
20
|
+
expect(ids).toContain("top");
|
|
21
|
+
expect(ids).not.toContain("bottom");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("reveals a side face when rotated on Y (CSS rotateY convention)", () => {
|
|
25
|
+
const ids = projectCubeFaces(0, 45, 0, OPTS).map((f) => f.id);
|
|
26
|
+
expect(ids).toContain("front");
|
|
27
|
+
expect(ids).toContain("left");
|
|
28
|
+
expect(ids).not.toContain("right");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("never returns more than 3 faces (a cube shows at most 3 at once)", () => {
|
|
32
|
+
for (const [rx, ry, rz] of [
|
|
33
|
+
[30, 30, 0],
|
|
34
|
+
[60, 60, 45],
|
|
35
|
+
[135, 20, 90],
|
|
36
|
+
]) {
|
|
37
|
+
expect(projectCubeFaces(rx, ry, rz, OPTS).length).toBeLessThanOrEqual(3);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("rotationZ rolls the silhouette without changing which faces are visible", () => {
|
|
42
|
+
const a = projectCubeFaces(0, 0, 0, OPTS)
|
|
43
|
+
.map((f) => f.id)
|
|
44
|
+
.sort();
|
|
45
|
+
const b = projectCubeFaces(0, 0, 90, OPTS)
|
|
46
|
+
.map((f) => f.id)
|
|
47
|
+
.sort();
|
|
48
|
+
expect(b).toEqual(a);
|
|
49
|
+
// …but the projected coordinates differ (it rolled).
|
|
50
|
+
expect(projectCubeFaces(0, 0, 90, OPTS)[0]?.points).not.toEqual(
|
|
51
|
+
projectCubeFaces(0, 0, 0, OPTS)[0]?.points,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("paints far faces before near faces (painter's order)", () => {
|
|
56
|
+
const faces = projectCubeFaces(35, 35, 0, OPTS);
|
|
57
|
+
for (let i = 1; i < faces.length; i++) {
|
|
58
|
+
expect(faces[i]!.depth).toBeGreaterThanOrEqual(faces[i - 1]!.depth);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("projectAxes", () => {
|
|
64
|
+
it("returns the three X/Y/Z axes with standard colors", () => {
|
|
65
|
+
const axes = projectAxes(0, 0, 0, OPTS);
|
|
66
|
+
expect(axes.map((a) => a.id).sort()).toEqual(["x", "y", "z"]);
|
|
67
|
+
const z = axes.find((a) => a.id === "z")!;
|
|
68
|
+
expect(z.color).toMatch(/#/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("flags the toward-viewer axis as front at identity (Z points at the camera)", () => {
|
|
72
|
+
const z = projectAxes(0, 0, 0, OPTS).find((a) => a.id === "z")!;
|
|
73
|
+
expect(z.front).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("rotating 180° on Y flips the Z axis away from the viewer", () => {
|
|
77
|
+
const z = projectAxes(0, 180, 0, OPTS).find((a) => a.id === "z")!;
|
|
78
|
+
expect(z.front).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("rotate", () => {
|
|
83
|
+
it("90° on Y maps +x to -z (right edge swings to the back)", () => {
|
|
84
|
+
const v = rotate({ x: 1, y: 0, z: 0 }, 0, 90, 0);
|
|
85
|
+
expect(v.x).toBeCloseTo(0, 5);
|
|
86
|
+
expect(v.z).toBeCloseTo(-1, 5);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("wrapDeg", () => {
|
|
91
|
+
it("wraps into (-180, 180]", () => {
|
|
92
|
+
expect(wrapDeg(0)).toBe(0);
|
|
93
|
+
expect(wrapDeg(180)).toBe(180);
|
|
94
|
+
expect(wrapDeg(190)).toBe(-170);
|
|
95
|
+
expect(wrapDeg(-190)).toBe(170);
|
|
96
|
+
expect(wrapDeg(360)).toBe(0);
|
|
97
|
+
expect(wrapDeg(540)).toBe(180);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure cube projection for the 3D-transform widget. Given an element's
|
|
3
|
+
* rotationX/Y/Z (degrees), project a unit cube to 2D SVG polygons with
|
|
4
|
+
* back-face culling and painter's-order sorting — no DOM, no React, so it
|
|
5
|
+
* unit-tests in isolation. Used by Transform3DCube to draw a live preview of
|
|
6
|
+
* the element's orientation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface Vec3 {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
z: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProjectedFace {
|
|
16
|
+
/** Stable face id (front/back/left/right/top/bottom) for keying + theming. */
|
|
17
|
+
id: string;
|
|
18
|
+
/** SVG polygon points: "x,y x,y x,y x,y". */
|
|
19
|
+
points: string;
|
|
20
|
+
/** 0..1 brightness from how front-facing the rotated normal is (lighting cue). */
|
|
21
|
+
shade: number;
|
|
22
|
+
/** Mean depth of the face's corners; larger = nearer the viewer. */
|
|
23
|
+
depth: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEG = Math.PI / 180;
|
|
27
|
+
|
|
28
|
+
/** Rotate a point intrinsically by X, then Y, then Z (degrees). */
|
|
29
|
+
export function rotate(v: Vec3, rx: number, ry: number, rz: number): Vec3 {
|
|
30
|
+
let { x, y, z } = v;
|
|
31
|
+
const cx = Math.cos(rx * DEG);
|
|
32
|
+
const sx = Math.sin(rx * DEG);
|
|
33
|
+
[y, z] = [y * cx - z * sx, y * sx + z * cx];
|
|
34
|
+
const cy = Math.cos(ry * DEG);
|
|
35
|
+
const sy = Math.sin(ry * DEG);
|
|
36
|
+
[x, z] = [x * cy + z * sy, -x * sy + z * cy];
|
|
37
|
+
const cz = Math.cos(rz * DEG);
|
|
38
|
+
const sz = Math.sin(rz * DEG);
|
|
39
|
+
[x, y] = [x * cz - y * sz, x * sz + y * cz];
|
|
40
|
+
return { x, y, z };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Unit-cube corners (±1) — 0-3 back face (z=-1), 4-7 front face (z=1).
|
|
44
|
+
const CORNERS: Vec3[] = [
|
|
45
|
+
{ x: -1, y: -1, z: -1 },
|
|
46
|
+
{ x: 1, y: -1, z: -1 },
|
|
47
|
+
{ x: 1, y: 1, z: -1 },
|
|
48
|
+
{ x: -1, y: 1, z: -1 },
|
|
49
|
+
{ x: -1, y: -1, z: 1 },
|
|
50
|
+
{ x: 1, y: -1, z: 1 },
|
|
51
|
+
{ x: 1, y: 1, z: 1 },
|
|
52
|
+
{ x: -1, y: 1, z: 1 },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const FACES: { id: string; idx: [number, number, number, number]; normal: Vec3 }[] = [
|
|
56
|
+
{ id: "front", idx: [4, 5, 6, 7], normal: { x: 0, y: 0, z: 1 } },
|
|
57
|
+
{ id: "back", idx: [1, 0, 3, 2], normal: { x: 0, y: 0, z: -1 } },
|
|
58
|
+
{ id: "left", idx: [0, 4, 7, 3], normal: { x: -1, y: 0, z: 0 } },
|
|
59
|
+
{ id: "right", idx: [5, 1, 2, 6], normal: { x: 1, y: 0, z: 0 } },
|
|
60
|
+
// y=+1 corners project to screen-top (SVG y is flipped), so that face is "top".
|
|
61
|
+
{ id: "top", idx: [7, 6, 2, 3], normal: { x: 0, y: 1, z: 0 } },
|
|
62
|
+
{ id: "bottom", idx: [4, 5, 1, 0], normal: { x: 0, y: -1, z: 0 } },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export interface ProjectOpts {
|
|
66
|
+
/** Center of the SVG viewport. */
|
|
67
|
+
cx: number;
|
|
68
|
+
cy: number;
|
|
69
|
+
/** Half-extent of the cube in SVG units (drawn cube ≈ 2·r before perspective). */
|
|
70
|
+
r: number;
|
|
71
|
+
/** Weak-perspective strength in units of `r` (larger = flatter; ~3-6 reads as 3D). */
|
|
72
|
+
persp?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function round(n: number): number {
|
|
76
|
+
return Math.round(n * 100) / 100;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Directional light (upper-front-right), normalized — drives per-face shading so
|
|
80
|
+
// top/front/side read as distinct planes instead of one flat fill.
|
|
81
|
+
const LIGHT = (() => {
|
|
82
|
+
const v = { x: 0.32, y: 0.56, z: 0.76 };
|
|
83
|
+
const m = Math.hypot(v.x, v.y, v.z);
|
|
84
|
+
return { x: v.x / m, y: v.y / m, z: v.z / m };
|
|
85
|
+
})();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Project the cube at the given orientation. Returns only the front-facing
|
|
89
|
+
* faces (≤3), painter-sorted far→near so the SVG draws nearer faces on top.
|
|
90
|
+
* Screen Y is flipped (SVG y grows downward). `shade` is a 0..1 lambert term
|
|
91
|
+
* from {@link LIGHT} for clean directional face tones.
|
|
92
|
+
*/
|
|
93
|
+
export function projectCubeFaces(
|
|
94
|
+
rx: number,
|
|
95
|
+
ry: number,
|
|
96
|
+
rz: number,
|
|
97
|
+
opts: ProjectOpts,
|
|
98
|
+
): ProjectedFace[] {
|
|
99
|
+
const { cx, cy, r } = opts;
|
|
100
|
+
const persp = opts.persp ?? 4;
|
|
101
|
+
const view = (v: Vec3) => rotate(v, rx, ry, rz);
|
|
102
|
+
const rotated = CORNERS.map(view);
|
|
103
|
+
const faces: ProjectedFace[] = [];
|
|
104
|
+
for (const f of FACES) {
|
|
105
|
+
const n = view(f.normal);
|
|
106
|
+
if (n.z <= 1e-6) continue; // back-face cull: normal must point toward viewer
|
|
107
|
+
const corners = f.idx.map((i) => rotated[i]!);
|
|
108
|
+
const depth = corners.reduce((s, p) => s + p.z, 0) / 4;
|
|
109
|
+
const points = corners
|
|
110
|
+
.map((p) => {
|
|
111
|
+
// Weak perspective: nearer corners (higher z) project slightly larger.
|
|
112
|
+
const s = persp / (persp - p.z);
|
|
113
|
+
return `${round(cx + p.x * r * s)},${round(cy - p.y * r * s)}`;
|
|
114
|
+
})
|
|
115
|
+
.join(" ");
|
|
116
|
+
const lum = Math.max(0, n.x * LIGHT.x + n.y * LIGHT.y + n.z * LIGHT.z);
|
|
117
|
+
faces.push({ id: f.id, points, shade: 0.3 + lum * 0.7, depth });
|
|
118
|
+
}
|
|
119
|
+
faces.sort((a, b) => a.depth - b.depth);
|
|
120
|
+
return faces;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ProjectedAxis {
|
|
124
|
+
id: "x" | "y" | "z";
|
|
125
|
+
/** Axis color (standard X=red, Y=green, Z=blue). */
|
|
126
|
+
color: string;
|
|
127
|
+
/** Tip position in SVG coords. */
|
|
128
|
+
x2: number;
|
|
129
|
+
y2: number;
|
|
130
|
+
/** Whether the tip points toward the viewer (drawn in front of the cube). */
|
|
131
|
+
front: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const AXES: { id: "x" | "y" | "z"; dir: Vec3; color: string }[] = [
|
|
135
|
+
{ id: "x", dir: { x: 1, y: 0, z: 0 }, color: "#ff6b81" },
|
|
136
|
+
{ id: "y", dir: { x: 0, y: 1, z: 0 }, color: "#5ff08a" },
|
|
137
|
+
{ id: "z", dir: { x: 0, y: 0, z: 1 }, color: "#62b6ff" },
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Project the 3 orientation axes (from the cube center) for an X/Y/Z gizmo.
|
|
142
|
+
* Orthographic — thin lines don't need perspective. `front` lets the caller draw
|
|
143
|
+
* away-facing axes behind the cube and toward-facing axes on top.
|
|
144
|
+
*/
|
|
145
|
+
export function projectAxes(
|
|
146
|
+
rx: number,
|
|
147
|
+
ry: number,
|
|
148
|
+
rz: number,
|
|
149
|
+
opts: ProjectOpts,
|
|
150
|
+
): ProjectedAxis[] {
|
|
151
|
+
const { cx, cy, r } = opts;
|
|
152
|
+
const len = r * 1.5;
|
|
153
|
+
const view = (v: Vec3) => rotate(v, rx, ry, rz);
|
|
154
|
+
return AXES.map((a) => {
|
|
155
|
+
const p = view(a.dir);
|
|
156
|
+
return {
|
|
157
|
+
id: a.id,
|
|
158
|
+
color: a.color,
|
|
159
|
+
x2: round(cx + p.x * len),
|
|
160
|
+
y2: round(cy - p.y * len),
|
|
161
|
+
front: p.z >= -1e-6,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Wrap an angle to (-180, 180] so drag accumulation never runs away. */
|
|
167
|
+
export function wrapDeg(deg: number): number {
|
|
168
|
+
let d = deg % 360;
|
|
169
|
+
if (d > 180) d -= 360;
|
|
170
|
+
if (d <= -180) d += 360;
|
|
171
|
+
return d;
|
|
172
|
+
}
|
|
@@ -119,7 +119,8 @@ export function useMotionPathData(
|
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
121
|
const recompute = () => {
|
|
122
|
-
|
|
122
|
+
// Position-only: never let a co-located size/scale tween shadow the path.
|
|
123
|
+
const read = readRuntimeKeyframes(iframeRef.current, selector, undefined, ["x", "y"]);
|
|
123
124
|
const next = buildMotionPathGeometry(read);
|
|
124
125
|
setGeometry((prev) =>
|
|
125
126
|
prev?.points === next?.points && prev?.kind === next?.kind ? prev : next,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export function ContextMenu({
|
|
2
|
+
x,
|
|
3
|
+
y,
|
|
4
|
+
asset,
|
|
5
|
+
onClose,
|
|
6
|
+
onCopy,
|
|
7
|
+
onDelete,
|
|
8
|
+
onRename,
|
|
9
|
+
}: {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
asset: string;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
onCopy: (path: string) => void;
|
|
15
|
+
onDelete?: (path: string) => void;
|
|
16
|
+
onRename?: (oldPath: string, newPath: string) => void;
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className="fixed inset-0 z-[200]"
|
|
21
|
+
onClick={onClose}
|
|
22
|
+
onContextMenu={(e) => {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
onClose();
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<div
|
|
28
|
+
className="absolute bg-neutral-900 border border-neutral-700 rounded-lg shadow-xl py-1 min-w-[140px] text-xs"
|
|
29
|
+
style={{ left: x, top: y }}
|
|
30
|
+
>
|
|
31
|
+
<button
|
|
32
|
+
onClick={(e) => {
|
|
33
|
+
e.stopPropagation();
|
|
34
|
+
onCopy(asset);
|
|
35
|
+
onClose();
|
|
36
|
+
}}
|
|
37
|
+
className="w-full text-left px-3 py-1.5 text-neutral-300 hover:bg-neutral-800 transition-colors"
|
|
38
|
+
>
|
|
39
|
+
Copy path
|
|
40
|
+
</button>
|
|
41
|
+
{onRename && (
|
|
42
|
+
<button
|
|
43
|
+
onClick={(e) => {
|
|
44
|
+
e.stopPropagation();
|
|
45
|
+
onClose();
|
|
46
|
+
}}
|
|
47
|
+
className="w-full text-left px-3 py-1.5 text-neutral-300 hover:bg-neutral-800 transition-colors"
|
|
48
|
+
>
|
|
49
|
+
Rename
|
|
50
|
+
</button>
|
|
51
|
+
)}
|
|
52
|
+
{onDelete && (
|
|
53
|
+
<button
|
|
54
|
+
onClick={(e) => {
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
onDelete(asset);
|
|
57
|
+
onClose();
|
|
58
|
+
}}
|
|
59
|
+
className="w-full text-left px-3 py-1.5 text-red-400 hover:bg-neutral-800 transition-colors"
|
|
60
|
+
>
|
|
61
|
+
Delete
|
|
62
|
+
</button>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function DeleteConfirm({
|
|
70
|
+
name,
|
|
71
|
+
onConfirm,
|
|
72
|
+
onCancel,
|
|
73
|
+
}: {
|
|
74
|
+
name: string;
|
|
75
|
+
onConfirm: () => void;
|
|
76
|
+
onCancel: () => void;
|
|
77
|
+
}) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="px-2 py-1.5 bg-red-950/30 border-l-2 border-red-500 flex items-center justify-between gap-2">
|
|
80
|
+
<span className="text-[10px] text-red-400 truncate">Delete {name}?</span>
|
|
81
|
+
<div className="flex items-center gap-1 flex-shrink-0">
|
|
82
|
+
<button
|
|
83
|
+
onClick={onConfirm}
|
|
84
|
+
className="px-2 py-0.5 text-[10px] rounded bg-red-600 text-white hover:bg-red-500 transition-colors"
|
|
85
|
+
>
|
|
86
|
+
Delete
|
|
87
|
+
</button>
|
|
88
|
+
<button
|
|
89
|
+
onClick={onCancel}
|
|
90
|
+
className="px-2 py-0.5 text-[10px] rounded text-neutral-400 hover:text-neutral-200 transition-colors"
|
|
91
|
+
>
|
|
92
|
+
Cancel
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|