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:
2026-08-23 04:18:08 +00:00
parent e3e790fd2b
commit be29ae7aa7
3 changed files with 4 additions and 330 deletions
@@ -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>
)}
</>
);
}
+4 -3
View File
@@ -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?).