new components
This commit is contained in:
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { Archive, ArchiveRestore, BookOpen, Bot, Check, Copy, Cpu, FileText, Flag, Menu, Mic, PanelLeftOpen, Paperclip, RotateCcw, ScanText, Search, SendHorizontal, Share2, Square, User as UserIcon, Volume2, VolumeX, X } from "lucide-react";
|
||||
import type { Attachment, ChatMessage, Conversation, DriverModel, User } from "../lib/types.ts";
|
||||
import { api, ApiError, type ReportReason } from "../lib/api.ts";
|
||||
import { AssistantAvatar, Avatar, Button, Field, FlowerMark, IconButton, Modal, Picker, type PickerGroup, Spinner } from "./ui.tsx";
|
||||
import { AssistantAvatar, Avatar, Button, copyText, Field, FlowerMark, IconButton, Modal, Picker, type PickerGroup, Spinner } from "./ui.tsx";
|
||||
import { Markdown } from "./Markdown.tsx";
|
||||
import { UserMessageContent } from "./MessageContent.tsx";
|
||||
import { cn } from "../lib/cn.ts";
|
||||
@@ -17,10 +17,11 @@ function MessageBubble({ msg, index, authorAvatar, authorName, onReport, reports
|
||||
const isUser = msg.role === "user";
|
||||
const [copied, setCopied] = useState(false);
|
||||
function copy() {
|
||||
navigator.clipboard.writeText(msg.content).then(() => {
|
||||
copyText(msg.content).then((ok) => {
|
||||
if (!ok) return;
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
function searchWeb() {
|
||||
window.open(`https://duckduckgo.com/?q=${encodeURIComponent(msg.content.slice(0, 400))}`, "_blank", "noopener,noreferrer");
|
||||
|
||||
@@ -4,6 +4,16 @@ import remarkGfm from "remark-gfm";
|
||||
import { CopyButton } from "./ui.tsx";
|
||||
import { highlightCode } from "../lib/highlight.tsx";
|
||||
|
||||
/** Raw source text of a react-markdown <code> element (children may be split). */
|
||||
export function extractCodeText(children: unknown): string {
|
||||
if (children === null || children === undefined) return "";
|
||||
if (typeof children === "string" || typeof children === "number") return String(children);
|
||||
if (Array.isArray(children)) return children.map(extractCodeText).join("");
|
||||
const props = (children as { props?: { children?: unknown } })?.props;
|
||||
if (props && typeof props === "object" && "children" in props) return extractCodeText(props.children);
|
||||
return "";
|
||||
}
|
||||
|
||||
function CodeBlock({ code, lang }: { code: string; lang: string }) {
|
||||
const nodes = useMemo(() => highlightCode(code, lang), [code, lang]);
|
||||
return (
|
||||
@@ -31,9 +41,10 @@ export function Markdown({ text }: { text: string }) {
|
||||
// react-markdown nests <code> inside <pre>; render our block instead.
|
||||
const child = Array.isArray(children) ? children[0] : children;
|
||||
const props = (child as { props?: { className?: string; children?: unknown } })?.props;
|
||||
const className = props?.className || "";
|
||||
if (!props || typeof props !== "object") return <pre>{children}</pre>;
|
||||
const className = typeof props.className === "string" ? props.className : "";
|
||||
const lang = className.replace("language-", "");
|
||||
const code = String(props?.children ?? "").replace(/\n$/, "");
|
||||
const code = extractCodeText(props.children).replace(/\n$/, "");
|
||||
return <CodeBlock code={code} lang={lang} />;
|
||||
},
|
||||
code({ children }) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Archive, ArchiveRestore, Eye, Fingerprint, Globe, FileText, ImagePlus, KeyRound, Link2, Mail, Mic, Palette, Pencil, Play, Plug, ScanText, Search, Settings2, Share2, ShieldCheck, Sparkles, Square, Tag, Trash2, Unplug, User as UserIcon, Volume2, X } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Archive, ArchiveRestore, Eye, Fingerprint, Globe, FileText, ImagePlus, KeyRound, Link2, Mail, Mic, Palette, Pencil, Pipette, Play, Plug, ScanText, Search, Settings2, Share2, ShieldCheck, Sparkles, Square, Tag, Trash2, Unplug, User as UserIcon, Volume2, X } from "lucide-react";
|
||||
import { api, type UserProvider, type VoicePreview } from "../lib/api.ts";
|
||||
import { pushToast } from "../lib/toasts.ts";
|
||||
import { cn } from "../lib/cn.ts";
|
||||
import { ACCENT_PRESETS, applyAccent, currentAccent, previewAccent, type AccentState } from "../lib/accent.ts";
|
||||
import { ACCENT_PRESETS, applyAccent, currentAccent, hexToHsv, hsvToHex, previewAccent, type AccentState } from "../lib/accent.ts";
|
||||
import { detectLang, getVoicePrefs, listTtsVoices, OPENAI_STATIC_VOICES, pickVoice, playText, setVoicePrefs, stopPlayback, ttsSupported, type VoicePrefs } from "../lib/voice.ts";
|
||||
import { FEATURES, setFeature, useFeatures } from "../lib/features.ts";
|
||||
import type { Conversation, FilePreview, User } from "../lib/types.ts";
|
||||
@@ -794,6 +794,7 @@ function ArchivedSection({ onView, onChanged }: { onView: (c: Conversation) => v
|
||||
function AccentSection() {
|
||||
const { t } = useT();
|
||||
const [state, setState] = useState<AccentState>(() => currentAccent());
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
|
||||
function pick(presetId: string) {
|
||||
const next = { ...state, presetId };
|
||||
@@ -802,13 +803,6 @@ function AccentSection() {
|
||||
pushToast(t("toast.accentChanged"), { icon: "accent", tag: "accent" });
|
||||
}
|
||||
|
||||
function pickCustom(hex: string) {
|
||||
const next = { presetId: "custom", customHex: hex };
|
||||
setState(next);
|
||||
previewAccent(hex);
|
||||
pushToast(t("toast.accentChanged"), { icon: "accent", tag: "accent" });
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="mb-1.5 block text-sm font-medium opacity-80">{t("settings.accent")}</span>
|
||||
@@ -830,26 +824,213 @@ function AccentSection() {
|
||||
style={{ backgroundColor: p.base }}
|
||||
/>
|
||||
))}
|
||||
<label
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPickerOpen((o) => !o)}
|
||||
aria-expanded={pickerOpen}
|
||||
aria-label={t("settings.customAria")}
|
||||
title={t("settings.customColor")}
|
||||
className={cn(
|
||||
"relative grid h-8 w-8 cursor-pointer place-items-center overflow-hidden rounded-full border-2 transition active:scale-90",
|
||||
state.presetId === "custom"
|
||||
"grid h-8 w-8 place-items-center rounded-full border-2 transition active:scale-90",
|
||||
state.presetId === "custom" || pickerOpen
|
||||
? "border-stone-900 dark:border-white"
|
||||
: "border-transparent hover:scale-110",
|
||||
)}
|
||||
style={{ background: state.presetId === "custom" ? state.customHex : undefined }}
|
||||
title={t("settings.customColor")}
|
||||
style={{ background: "conic-gradient(#f43f5e,#f59e0b,#84cc16,#14b8a6,#3b82f6,#8b5cf6,#f43f5e)" }}
|
||||
>
|
||||
<Sparkles size={14} className={cn("text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]", state.presetId === "custom" && "opacity-0")} />
|
||||
<input
|
||||
type="color"
|
||||
aria-label={t("settings.customAria")}
|
||||
value={state.presetId === "custom" ? state.customHex : "#10b981"}
|
||||
onChange={(e) => pickCustom(e.target.value)}
|
||||
onBlur={() => applyAccent(state)} // persist the last previewed value
|
||||
className="absolute inset-0 cursor-pointer opacity-0"
|
||||
<Pipette size={14} className="text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]" />
|
||||
</button>
|
||||
</div>
|
||||
{pickerOpen && (
|
||||
<AccentCustomPanel
|
||||
initialHex={state.presetId === "custom" ? state.customHex : (ACCENT_PRESETS.find((p) => p.id === state.presetId)?.base ?? "#10b981")}
|
||||
onPreview={(hex) => {
|
||||
setState({ presetId: "custom", customHex: hex });
|
||||
previewAccent(hex);
|
||||
}}
|
||||
onCommit={(hex) => {
|
||||
const next = { presetId: "custom", customHex: hex };
|
||||
setState(next);
|
||||
applyAccent(next);
|
||||
pushToast(t("toast.accentChanged"), { icon: "accent", tag: "accent" });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Custom accent picker: SV square + hue slider + hex field ---------- */
|
||||
|
||||
function AccentCustomPanel({ initialHex, onPreview, onCommit }: {
|
||||
initialHex: string;
|
||||
onPreview: (hex: string) => void;
|
||||
onCommit: (hex: string) => void;
|
||||
}) {
|
||||
const { t } = useT();
|
||||
const start = hexToHsv(initialHex) ?? { h: 160, s: 84, v: 73 };
|
||||
const [h, setH] = useState(start.h);
|
||||
const [s, setS] = useState(start.s);
|
||||
const [v, setV] = useState(start.v);
|
||||
const [hexInput, setHexInput] = useState(() => hsvToHex(start.h, start.s, start.v));
|
||||
const [hexError, setHexError] = useState(false);
|
||||
const [drag, setDrag] = useState<null | "sv" | "hue">(null);
|
||||
const svRef = useRef<HTMLDivElement>(null);
|
||||
const hueRef = useRef<HTMLDivElement>(null);
|
||||
// Synchronous mirror (pointerup fires before setState flushes).
|
||||
const hsvRef = useRef({ h: start.h, s: start.s, v: start.v });
|
||||
|
||||
const currentHex = useMemo(() => hsvToHex(h, s, v), [h, s, v]);
|
||||
|
||||
function applyHsv(nh: number, ns: number, nv: number) {
|
||||
const c = {
|
||||
h: Math.min(359.99, Math.max(0, nh)),
|
||||
s: Math.min(100, Math.max(0, ns)),
|
||||
v: Math.min(100, Math.max(0, nv)),
|
||||
};
|
||||
hsvRef.current = c;
|
||||
setH(c.h);
|
||||
setS(c.s);
|
||||
setV(c.v);
|
||||
const hex = hsvToHex(c.h, c.s, c.v);
|
||||
setHexInput(hex);
|
||||
setHexError(false);
|
||||
onPreview(hex);
|
||||
}
|
||||
|
||||
function posToSv(e: React.PointerEvent): void {
|
||||
const el = svRef.current;
|
||||
if (!el) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
applyHsv(hsvRef.current.h, ((e.clientX - r.left) / r.width) * 100, (1 - (e.clientY - r.top) / r.height) * 100);
|
||||
}
|
||||
|
||||
function posToHue(e: React.PointerEvent): void {
|
||||
const el = hueRef.current;
|
||||
if (!el) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
const nh = ((e.clientX - r.left) / r.width) * 360;
|
||||
const c = hsvRef.current;
|
||||
applyHsv(nh, c.s, c.v);
|
||||
}
|
||||
|
||||
function begin(kind: "sv" | "hue", e: React.PointerEvent) {
|
||||
(e.target as HTMLElement).setPointerCapture?.(e.pointerId);
|
||||
setDrag(kind);
|
||||
if (kind === "sv") posToSv(e);
|
||||
else posToHue(e);
|
||||
}
|
||||
|
||||
function commitHex(raw: string) {
|
||||
const m = /^#?([0-9a-fA-F]{6})$/.exec(raw.trim());
|
||||
if (!m) {
|
||||
setHexError(true);
|
||||
setHexInput(currentHex);
|
||||
return;
|
||||
}
|
||||
const hex = `#${m[1]!.toLowerCase()}`;
|
||||
const c = hexToHsv(hex);
|
||||
if (c) {
|
||||
hsvRef.current = c;
|
||||
setH(c.h);
|
||||
setS(c.s);
|
||||
setV(c.v);
|
||||
setHexInput(hex);
|
||||
setHexError(false);
|
||||
onPreview(hex);
|
||||
onCommit(hex);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 space-y-3 rounded-2xl border border-stone-200/70 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div
|
||||
ref={svRef}
|
||||
role="slider"
|
||||
aria-label={t("settings.customAria")}
|
||||
aria-valuetext={currentHex}
|
||||
tabIndex={0}
|
||||
onPointerDown={(e) => begin("sv", e)}
|
||||
onPointerMove={(e) => { if (drag === "sv") posToSv(e); }}
|
||||
onPointerUp={() => { if (drag === "sv") { setDrag(null); onCommit(hsvToHex(hsvRef.current.h, hsvRef.current.s, hsvRef.current.v)); } }}
|
||||
onPointerCancel={() => setDrag(null)}
|
||||
onKeyDown={(e) => {
|
||||
const step = e.shiftKey ? 10 : 2;
|
||||
const c = hsvRef.current;
|
||||
if (e.key === "ArrowLeft") { applyHsv(c.h, c.s - step, c.v); onCommit(hsvToHex(Math.min(359.99, Math.max(0, c.h)), Math.min(100, Math.max(0, c.s - step)), c.v)); }
|
||||
else if (e.key === "ArrowRight") { applyHsv(c.h, c.s + step, c.v); onCommit(hsvToHex(Math.min(359.99, Math.max(0, c.h)), Math.min(100, Math.max(0, c.s + step)), c.v)); }
|
||||
else if (e.key === "ArrowUp") { applyHsv(c.h, c.s, c.v + step); onCommit(hsvToHex(Math.min(359.99, Math.max(0, c.h)), c.s, Math.min(100, Math.max(0, c.v + step)))); }
|
||||
else if (e.key === "ArrowDown") { applyHsv(c.h, c.s, c.v - step); onCommit(hsvToHex(Math.min(359.99, Math.max(0, c.h)), c.s, Math.min(100, Math.max(0, c.v - step)))); }
|
||||
else return;
|
||||
e.preventDefault();
|
||||
}}
|
||||
className="relative h-40 w-full cursor-crosshair touch-none select-none overflow-hidden rounded-2xl"
|
||||
style={{ background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${h}, 100%, 50%))` }}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute h-4 w-4 rounded-full border-2 border-white shadow-md"
|
||||
style={{ left: `${s}%`, top: `${100 - v}%`, transform: "translate(-50%, -50%)", backgroundColor: currentHex }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={hueRef}
|
||||
role="slider"
|
||||
aria-label={t("settings.accent")}
|
||||
aria-valuetext={String(Math.round(h))}
|
||||
tabIndex={0}
|
||||
onPointerDown={(e) => begin("hue", e)}
|
||||
onPointerMove={(e) => { if (drag === "hue") posToHue(e); }}
|
||||
onPointerUp={() => { if (drag === "hue") { setDrag(null); onCommit(hsvToHex(hsvRef.current.h, hsvRef.current.s, hsvRef.current.v)); } }}
|
||||
onPointerCancel={() => setDrag(null)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
||||
const nh = hsvRef.current.h + (e.key === "ArrowRight" ? 4 : -4);
|
||||
const c = hsvRef.current;
|
||||
applyHsv(nh, c.s, c.v);
|
||||
onCommit(hsvToHex(nh, c.s, c.v));
|
||||
e.preventDefault();
|
||||
}}
|
||||
className="relative h-3 w-full cursor-pointer touch-none select-none rounded-full"
|
||||
style={{ background: "linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00)" }}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute top-1/2 h-5 w-5 rounded-full border-2 border-white bg-transparent shadow-md"
|
||||
style={{ left: `calc(${(h / 360) * 100}% - 10px)`, transform: "translateY(-50%)", backgroundColor: currentHex }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-9 w-9 shrink-0 rounded-full border border-stone-200 dark:border-zinc-700" style={{ backgroundColor: currentHex }} aria-hidden />
|
||||
<Field label={t("accent.hexLabel")}>
|
||||
<Input
|
||||
value={hexInput}
|
||||
onChange={(e) => {
|
||||
setHexInput(e.target.value);
|
||||
const m = /^#?([0-9a-fA-F]{6})$/.exec(e.target.value.trim());
|
||||
if (m) {
|
||||
const hex = `#${m[1]!.toLowerCase()}`;
|
||||
const c = hexToHsv(hex);
|
||||
if (c) {
|
||||
hsvRef.current = c;
|
||||
setH(c.h);
|
||||
setS(c.s);
|
||||
setV(c.v);
|
||||
setHexError(false);
|
||||
onPreview(hex);
|
||||
}
|
||||
} else {
|
||||
setHexError(e.target.value.trim().length > 0);
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => commitHex(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") commitHex((e.target as HTMLInputElement).value); }}
|
||||
maxLength={7}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
className={cn("font-mono uppercase", hexError && "!border-red-500")}
|
||||
/>
|
||||
</label>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -633,6 +633,28 @@ export function Toggle({
|
||||
);
|
||||
}
|
||||
|
||||
/** Clipboard write with legacy fallback (non-secure contexts lack the async API). */
|
||||
export async function copyText(text: string): Promise<boolean> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch {
|
||||
try {
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = text;
|
||||
ta.style.position = "fixed";
|
||||
ta.style.opacity = "0";
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
const ok = document.execCommand("copy");
|
||||
document.body.removeChild(ta);
|
||||
return ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function CopyButton({ text, label }: { text: string; label?: string }) {
|
||||
const { t } = useT();
|
||||
const [done, setDone] = useState(false);
|
||||
@@ -640,7 +662,8 @@ export function CopyButton({ text, label }: { text: string; label?: string }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
copyText(text).then((ok) => {
|
||||
if (!ok) return;
|
||||
setDone(true);
|
||||
setTimeout(() => setDone(false), 1500);
|
||||
});
|
||||
|
||||
@@ -66,8 +66,42 @@ function hslToRgbTriplet(h: number, s: number, l: number): string {
|
||||
return `${to(r)} ${to(g)} ${to(b)}`;
|
||||
}
|
||||
|
||||
/** Build the 50–950 ramp from a base hex (HSL ramp, emerald-style). */
|
||||
export function shadesFor(baseHex: string): AccentShades {
|
||||
/** HSV helpers for the custom color picker (no native input involved). */
|
||||
export function hexToHsv(hex: string): { h: number; s: number; v: number } | null {
|
||||
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
||||
if (!m) return null;
|
||||
const n = parseInt(m[1]!, 16);
|
||||
const r = ((n >> 16) & 255) / 255, g = ((n >> 8) & 255) / 255, b = (n & 255) / 255;
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
const d = max - min;
|
||||
let h = 0;
|
||||
if (d !== 0) {
|
||||
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
|
||||
else if (max === g) h = ((b - r) / d + 2) * 60;
|
||||
else h = ((r - g) / d + 4) * 60;
|
||||
}
|
||||
return { h, s: max === 0 ? 0 : (d / max) * 100, v: max * 100 };
|
||||
}
|
||||
|
||||
export function hsvToHex(h: number, s: number, v: number): string {
|
||||
const sn = Math.min(100, Math.max(0, s)) / 100;
|
||||
const vn = Math.min(100, Math.max(0, v)) / 100;
|
||||
const hh = ((h % 360) + 360) % 360;
|
||||
const c = vn * sn;
|
||||
const x = c * (1 - Math.abs(((hh / 60) % 2) - 1));
|
||||
const m = vn - c;
|
||||
let r = 0, g = 0, b = 0;
|
||||
if (hh < 60) [r, g, b] = [c, x, 0];
|
||||
else if (hh < 120) [r, g, b] = [x, c, 0];
|
||||
else if (hh < 180) [r, g, b] = [0, c, x];
|
||||
else if (hh < 240) [r, g, b] = [0, x, c];
|
||||
else if (hh < 300) [r, g, b] = [x, 0, c];
|
||||
else [r, g, b] = [c, 0, x];
|
||||
const to = (zv: number) => Math.round((zv + m) * 255).toString(16).padStart(2, "0");
|
||||
return `#${to(r)}${to(g)}${to(b)}`;
|
||||
}
|
||||
|
||||
/** Build the 50–950 ramp from a base hex (HSL ramp, emerald-style). */export function shadesFor(baseHex: string): AccentShades {
|
||||
const { h, s } = hexToHsl(baseHex);
|
||||
const out = {} as AccentShades;
|
||||
for (const k of SHADE_KEYS) {
|
||||
|
||||
@@ -225,6 +225,7 @@ const en = {
|
||||
"accent.preset.orange": "Orange",
|
||||
"accent.preset.teal": "Teal",
|
||||
"accent.preset.pink": "Pink",
|
||||
"accent.hexLabel": "Hex color",
|
||||
|
||||
"feat.thinking.label": "Thinking",
|
||||
"feat.thinking.hint":
|
||||
@@ -948,6 +949,7 @@ const es: Dict = {
|
||||
"accent.preset.orange": "Naranja",
|
||||
"accent.preset.teal": "Turquesa",
|
||||
"accent.preset.pink": "Rosa fuerte",
|
||||
"accent.hexLabel": "Hex color",
|
||||
|
||||
"feat.thinking.label": "Razonamiento",
|
||||
"feat.thinking.hint":
|
||||
@@ -1664,6 +1666,7 @@ const fr: Dict = {
|
||||
"accent.preset.orange": "Orange",
|
||||
"accent.preset.teal": "Sarcelle",
|
||||
"accent.preset.pink": "Rose vif",
|
||||
"accent.hexLabel": "Hex color",
|
||||
|
||||
"feat.thinking.label": "Réflexion",
|
||||
"feat.thinking.hint": "Demande au modèle de raisonner étape par étape.",
|
||||
@@ -2375,6 +2378,7 @@ const ru: Dict = {
|
||||
"accent.preset.orange": "Оранжевый",
|
||||
"accent.preset.teal": "Бирюза",
|
||||
"accent.preset.pink": "Розовый",
|
||||
"accent.hexLabel": "Hex color",
|
||||
|
||||
"feat.thinking.label": "Размышление",
|
||||
"feat.thinking.hint": "Просить модель рассуждать шаг за шагом.",
|
||||
@@ -3078,6 +3082,7 @@ const it: Dict = {
|
||||
"accent.preset.orange": "Arancione",
|
||||
"accent.preset.teal": "Teal",
|
||||
"accent.preset.pink": "Rosa shocking",
|
||||
"accent.hexLabel": "Hex color",
|
||||
|
||||
"feat.thinking.label": "Ragionamento",
|
||||
"feat.thinking.hint": "Chiedi al modello di ragionare passo passo.",
|
||||
@@ -3784,6 +3789,7 @@ const pt: Dict = {
|
||||
"accent.preset.orange": "Laranja",
|
||||
"accent.preset.teal": "Turquesa",
|
||||
"accent.preset.pink": "Rosa choque",
|
||||
"accent.hexLabel": "Hex color",
|
||||
|
||||
"feat.thinking.label": "Raciocínio",
|
||||
"feat.thinking.hint": "Pede ao modelo para raciocinar passo a passo.",
|
||||
|
||||
@@ -53,3 +53,11 @@ like <model> <button>
|
||||
|
||||
|
||||
---- pouvoir dans admin centers, sur users, envoyez un lien mdp reset à un user
|
||||
|
||||
|
||||
--- <input type="color" aria-label="Custom accent color" class="absolute inset-0 cursor-pointer opacity-0" value="#10b981">
|
||||
|
||||
Ajouter un vrai composant pour pouvoir chosisir la couleur accentique sans dépendre du système.
|
||||
Utiliser un vrai copiosant come shadcdn.
|
||||
|
||||
Avec un vrai accent color picket etpouvoir mettre sontpropr e code hex pour la couleur.
|
||||
|
||||
Reference in New Issue
Block a user