delete two unreferenced components from another codebase
MarkdownEditor and ui/start-confetti-button had no importers anywhere in the repo and both carried a different product's content: MarkdownEditor hardcoded https://app.pertento.ai/logo.png with alt="Pertento", and start-confetti-button spoke about 'the experiment' and 'Draft/Created' statuses. Translating either would have put feature words into ui.*, which D6 forbids — a shared namespace may not name a feature. todo/inputs-outside-forms named MarkdownEditor as deliberately-unwrapped, so that entry is updated rather than left pointing at a deleted file.
This commit is contained in:
@@ -1,115 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import md from 'markdown-it';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
import rehypeRaw from 'rehype-raw';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Code, Eye } from 'lucide-react';
|
|
||||||
|
|
||||||
type MarkdownEditorProps = {
|
|
||||||
content: string;
|
|
||||||
onChange: (content: string) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
greeting?: string;
|
|
||||||
firstName?: string;
|
|
||||||
lastName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function MarkdownEditor({
|
|
||||||
content,
|
|
||||||
onChange,
|
|
||||||
placeholder = 'Enter markdown...',
|
|
||||||
greeting,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
}: MarkdownEditorProps) {
|
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
|
||||||
|
|
||||||
const markdown = md({
|
|
||||||
html: true,
|
|
||||||
breaks: true,
|
|
||||||
linkify: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const processGreeting = (text: string) => {
|
|
||||||
return text
|
|
||||||
.replace(/\{\{firstName\}\}/g, firstName || '')
|
|
||||||
.replace(/\{\{fullName\}\}/g, `${firstName || ''} ${lastName || ''}`.trim());
|
|
||||||
};
|
|
||||||
|
|
||||||
const processedGreeting = greeting ? processGreeting(greeting) : '';
|
|
||||||
const greetingHtml = processedGreeting ? `<p>${processedGreeting}</p>` : '';
|
|
||||||
const preview = markdown.render(content);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col rounded-lg border border-border bg-background overflow-hidden">
|
|
||||||
{/* Toolbar */}
|
|
||||||
<div className="flex items-center gap-2 border-b border-border bg-muted px-4 py-2">
|
|
||||||
<Button size="sm" variant={!showPreview ? 'default' : 'outline'} onClick={() => setShowPreview(false)}>
|
|
||||||
<Code className="h-4 w-4 mr-2" />
|
|
||||||
Markdown
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant={showPreview ? 'default' : 'outline'} onClick={() => setShowPreview(true)}>
|
|
||||||
<Eye className="h-4 w-4 mr-2" />
|
|
||||||
Preview
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content Area */}
|
|
||||||
<div className="flex-1 overflow-hidden">
|
|
||||||
{!showPreview ? (
|
|
||||||
<textarea
|
|
||||||
rows={15}
|
|
||||||
name="content"
|
|
||||||
value={content}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}
|
|
||||||
placeholder={placeholder}
|
|
||||||
className="w-full h-full resize-none p-4 font-mono text-sm focus:outline-none bg-background text-foreground placeholder:text-muted-foreground overflow-auto"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-full overflow-auto bg-slate-50 dark:bg-slate-900 p-4">
|
|
||||||
{/* Email wrapper simulation */}
|
|
||||||
<div className="mx-auto max-w-2xl rounded-lg bg-background p-8 shadow-lg">
|
|
||||||
{/* Logo */}
|
|
||||||
<div className="mb-8 text-center">
|
|
||||||
<img src="https://app.pertento.ai/logo.png" alt="Pertento" className="h-16 mx-auto" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
{processedGreeting && <p className="text-base leading-relaxed">{processedGreeting}</p>}
|
|
||||||
<style>{`
|
|
||||||
.markdown-preview h1 { font-size: 2rem; font-weight: bold; margin: 1.5rem 0 1rem 0; }
|
|
||||||
.markdown-preview h2 { font-size: 1.5rem; font-weight: bold; margin: 1.25rem 0 0.75rem 0; }
|
|
||||||
.markdown-preview h3 { font-size: 1.25rem; font-weight: bold; margin: 1rem 0 0.5rem 0; }
|
|
||||||
.markdown-preview p { margin: 0.75rem 0; line-height: 1.6; }
|
|
||||||
/* list-style is restored explicitly — tailwind preflight strips it, which leaves an
|
|
||||||
ordered list indented but unnumbered. */
|
|
||||||
.markdown-preview ul, .markdown-preview ol { margin: 1rem 0; padding-left: 2rem; list-style-position: outside; }
|
|
||||||
.markdown-preview ul { list-style-type: disc; }
|
|
||||||
.markdown-preview ol { list-style-type: decimal; }
|
|
||||||
.markdown-preview ul ul { list-style-type: circle; }
|
|
||||||
.markdown-preview ol ol { list-style-type: lower-alpha; }
|
|
||||||
.markdown-preview li { margin: 0.5rem 0; line-height: 1.6; }
|
|
||||||
.markdown-preview code { background-color: rgb(229, 229, 229); color: rgb(31, 41, 55); padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.875rem; }
|
|
||||||
.markdown-preview pre { background-color: rgb(17, 24, 39); color: rgb(243, 244, 246); padding: 1rem; border-radius: 0.5rem; overflow-x: auto; margin: 1rem 0; }
|
|
||||||
.markdown-preview pre code { background-color: transparent; color: inherit; padding: 0; }
|
|
||||||
.markdown-preview a { color: rgb(37, 99, 235); text-decoration: underline; }
|
|
||||||
.markdown-preview blockquote { border-left: 4px solid rgb(209, 213, 219); padding-left: 1rem; font-style: italic; margin: 1rem 0; }
|
|
||||||
.dark .markdown-preview code { background-color: rgb(55, 65, 81); color: rgb(243, 244, 246); }
|
|
||||||
.dark .markdown-preview a { color: rgb(96, 165, 250); }
|
|
||||||
.dark .markdown-preview blockquote { border-left-color: rgb(75, 85, 99); }
|
|
||||||
`}</style>
|
|
||||||
<div className="markdown-preview">
|
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
|
|
||||||
{content}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Play } from 'lucide-react';
|
|
||||||
|
|
||||||
export default function StartConfettiButton({
|
|
||||||
isDraftLike,
|
|
||||||
onStart,
|
|
||||||
className,
|
|
||||||
label = 'Start',
|
|
||||||
}: {
|
|
||||||
isDraftLike: boolean;
|
|
||||||
onStart?: () => void;
|
|
||||||
className?: string;
|
|
||||||
label?: string;
|
|
||||||
}) {
|
|
||||||
const [showConfetti, setShowConfetti] = useState(false);
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
||||||
const rafRef = useRef<number | null>(null);
|
|
||||||
|
|
||||||
// ---- Confetti Engine (inline, light) ----
|
|
||||||
const runConfetti = useCallback(() => {
|
|
||||||
const canvas = canvasRef.current!;
|
|
||||||
const ctx = canvas.getContext('2d')!;
|
|
||||||
const DPR = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
|
|
||||||
|
|
||||||
// size canvas
|
|
||||||
const resize = () => {
|
|
||||||
const { innerWidth: w, innerHeight: h } = window;
|
|
||||||
canvas.width = Math.floor(w * DPR);
|
|
||||||
canvas.height = Math.floor(h * DPR);
|
|
||||||
canvas.style.width = `${w}px`;
|
|
||||||
canvas.style.height = `${h}px`;
|
|
||||||
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
||||||
};
|
|
||||||
resize();
|
|
||||||
|
|
||||||
const onResize = () => resize();
|
|
||||||
window.addEventListener('resize', onResize);
|
|
||||||
|
|
||||||
const colors = [
|
|
||||||
'#ef4444', // red-500
|
|
||||||
'#f59e0b', // amber-500
|
|
||||||
'#10b981', // emerald-500
|
|
||||||
'#3b82f6', // blue-500
|
|
||||||
'#8b5cf6', // violet-500
|
|
||||||
'#ec4899', // pink-500
|
|
||||||
'#22c55e', // green-500
|
|
||||||
'#eab308', // yellow-500
|
|
||||||
];
|
|
||||||
|
|
||||||
type Particle = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
w: number;
|
|
||||||
h: number;
|
|
||||||
vx: number;
|
|
||||||
vy: number;
|
|
||||||
rot: number;
|
|
||||||
rotSpeed: number;
|
|
||||||
tilt: number;
|
|
||||||
tiltSpeed: number;
|
|
||||||
color: string;
|
|
||||||
alpha: number;
|
|
||||||
decay: number;
|
|
||||||
shape: 'rect' | 'triangle';
|
|
||||||
};
|
|
||||||
|
|
||||||
const rand = (min: number, max: number) => Math.random() * (max - min) + min;
|
|
||||||
|
|
||||||
const particles: Particle[] = [];
|
|
||||||
const bursts = 5;
|
|
||||||
const perBurst = 80;
|
|
||||||
const spread = Math.PI / 3;
|
|
||||||
const gravity = 0.25;
|
|
||||||
const air = 0.002;
|
|
||||||
const floorFriction = 0.96;
|
|
||||||
|
|
||||||
for (let b = 0; b < bursts; b++) {
|
|
||||||
const originX = (window.innerWidth / (bursts + 1)) * (b + 1);
|
|
||||||
const originY = window.innerHeight * 0.15;
|
|
||||||
const angleBase = -Math.PI / 2;
|
|
||||||
for (let i = 0; i < perBurst; i++) {
|
|
||||||
const angle = angleBase + rand(-spread, spread);
|
|
||||||
const speed = rand(6, 12);
|
|
||||||
particles.push({
|
|
||||||
x: originX + rand(-8, 8),
|
|
||||||
y: originY + rand(-8, 8),
|
|
||||||
w: rand(4, 8),
|
|
||||||
h: rand(6, 14),
|
|
||||||
vx: Math.cos(angle) * speed,
|
|
||||||
vy: Math.sin(angle) * speed,
|
|
||||||
rot: rand(0, Math.PI * 2),
|
|
||||||
rotSpeed: rand(-0.2, 0.2),
|
|
||||||
tilt: rand(-0.8, 0.8),
|
|
||||||
tiltSpeed: rand(-0.02, 0.02),
|
|
||||||
color: colors[(Math.random() * colors.length) | 0] ?? '#ef4444',
|
|
||||||
alpha: 1,
|
|
||||||
decay: rand(0.004, 0.01),
|
|
||||||
shape: Math.random() < 0.3 ? 'triangle' : 'rect',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tick = () => {
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
for (const p of particles) {
|
|
||||||
// physics
|
|
||||||
p.vy += gravity;
|
|
||||||
p.vx *= 1 - air;
|
|
||||||
p.vy *= 1 - air * 0.5;
|
|
||||||
p.x += p.vx;
|
|
||||||
p.y += p.vy;
|
|
||||||
p.rot += p.rotSpeed;
|
|
||||||
p.tilt += p.tiltSpeed;
|
|
||||||
p.alpha -= p.decay;
|
|
||||||
|
|
||||||
// floor bounce & friction
|
|
||||||
const floor = window.innerHeight - 6;
|
|
||||||
if (p.y > floor) {
|
|
||||||
p.y = floor;
|
|
||||||
p.vy *= -0.45;
|
|
||||||
p.vx *= floorFriction;
|
|
||||||
p.rotSpeed *= floorFriction;
|
|
||||||
p.tiltSpeed *= floorFriction;
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw
|
|
||||||
if (p.alpha > 0) {
|
|
||||||
ctx.globalAlpha = Math.max(0, p.alpha);
|
|
||||||
ctx.save();
|
|
||||||
ctx.translate(p.x, p.y);
|
|
||||||
ctx.rotate(p.rot + p.tilt);
|
|
||||||
ctx.fillStyle = p.color;
|
|
||||||
|
|
||||||
if (p.shape === 'rect') {
|
|
||||||
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
|
||||||
} else {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, -p.h / 2);
|
|
||||||
ctx.lineTo(p.w / 2, p.h / 2);
|
|
||||||
ctx.lineTo(-p.w / 2, p.h / 2);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const alive = particles.some((p) => p.alpha > 0.02);
|
|
||||||
if (alive) {
|
|
||||||
rafRef.current = requestAnimationFrame(tick);
|
|
||||||
} else {
|
|
||||||
// cleanup
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
window.removeEventListener('resize', onResize);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tick();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleStart = () => {
|
|
||||||
onStart?.();
|
|
||||||
|
|
||||||
if (isDraftLike) {
|
|
||||||
setShowConfetti(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showConfetti) return;
|
|
||||||
|
|
||||||
runConfetti();
|
|
||||||
|
|
||||||
const t = setTimeout(() => {
|
|
||||||
setShowConfetti(false);
|
|
||||||
}, 2200);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(t);
|
|
||||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
||||||
rafRef.current = null;
|
|
||||||
};
|
|
||||||
}, [showConfetti, runConfetti]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={handleStart}
|
|
||||||
className={`min-w-24 ${isDraftLike ? 'bg-green-600 text-white hover:bg-green-700' : ''} ${className ?? ''}`}
|
|
||||||
disabled={!isDraftLike}
|
|
||||||
title={isDraftLike ? 'Start the experiment' : 'Only available for Draft/Created'}
|
|
||||||
>
|
|
||||||
<Play className="w-4 h-4 mr-2" />
|
|
||||||
{label}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Confetti Overlay */}
|
|
||||||
{showConfetti && (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 z-100 pointer-events-none mask-[radial-gradient(80%_80%_at_50%_40%,#000,transparent)]"
|
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
|
||||||
<div className="absolute inset-x-0 top-0 h-32 bg-linear-to-b from-white/60 to-transparent" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -49,9 +49,10 @@ handling.
|
|||||||
- `Dashboards/DashboardPreview`'s `NamePanel` (name + description) has its submit button in a
|
- `Dashboards/DashboardPreview`'s `NamePanel` (name + description) has its submit button in a
|
||||||
_different_, independently-mounted panel (`CreatePanel`) elsewhere in the workspace layout tree — a
|
_different_, independently-mounted panel (`CreatePanel`) elsewhere in the workspace layout tree — a
|
||||||
`<form>` can't span two panels without restructuring the wizard.
|
`<form>` can't span two panels without restructuring the wizard.
|
||||||
- `ColorPicker` and `MarkdownEditor` have zero call sites anywhere in the repo right now; both are
|
- `ColorPicker` has zero call sites anywhere in the repo right now; it is shaped to be embedded in a
|
||||||
shaped to be embedded in a future caller's own form (uncontrolled inputs with `name` attributes, or a
|
future caller's own form (uncontrolled inputs with `name` attributes), so there's nothing to wrap yet.
|
||||||
plain controlled textarea with no submit of its own), so there's nothing to wrap yet.
|
`MarkdownEditor` was named here too and was deleted on 2026-08-23 during the localization pass — it
|
||||||
|
had no call sites and hardcoded another product's logo URL.
|
||||||
- `widgets/Weather`'s `LocationSetup` is a live autocomplete with several result rows and no single
|
- `widgets/Weather`'s `LocationSetup` is a live autocomplete with several result rows and no single
|
||||||
obvious Enter target (which of N matches?).
|
obvious Enter target (which of N matches?).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user