Files
platform/src/apps/officer-web/Screens/Dashboard/Settings/ProfileSettings/DockSettings.tsx
T
pastilhas ad8ed88519 profile settings keyed (425 -> 352)
ProfileSettings reaches zero: api keys, profile, voice, dock, languages and the
model defaults.

Language names now come from Intl.DisplayNames rather than the catalog. The runtime
already knows "German" in English and "alemao" in Portuguese, so 28 names would have
been 56 hand-written entries that Intl gets right for free -- including for locales
not shipped yet. Same reasoning D7 gives for dates and numbers. The tag LIST stays
hardcoded: which languages you may claim to speak is a product choice.

ApiKeys had toLocaleDateString(undefined, ...), which followed the BROWSER locale --
the exact contradiction D7 describes. It uses useFormat().date now. Its revoked-key
toggle hand-rolled a plural with a ternary on n === 1; that is a real plural entry now.

Scanner: an Authorization header value like "Bearer <token>" is not a sentence.
Excluded by auth-scheme prefix; verified the delta is exactly the four such
credentials across both remaining areas.
2026-08-23 04:57:48 +00:00

247 lines
8.1 KiB
TypeScript

import { useMemo, useState, useCallback, type DragEvent } from 'react';
import { X, Plus, RotateCcw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useDock } from 'officerdev';
import { usePermissions } from 'hooks/usePermissions';
import { useDockItems, defaultDockPaths } from '@/Screens/Dashboard/Layout/Dock';
import { useTranslation } from '@/lib/i18n';
type DockPillProps = {
label: string;
path: string;
color: string;
visible: boolean;
onAction: (path: string) => void;
onDragStart: (ev: DragEvent, path: string) => void;
onDropOnPill?: (draggedPath: string) => void;
dropIndicator?: 'left' | 'right' | null;
onDragOverPill?: (ev: DragEvent) => void;
onDragLeavePill?: () => void;
};
const DockPill = ({
label,
path,
color,
visible,
onAction,
onDragStart,
onDropOnPill,
dropIndicator,
onDragOverPill,
onDragLeavePill,
}: DockPillProps) => (
<span
draggable
onDragStart={(ev) => onDragStart(ev, path)}
onDragOver={onDragOverPill}
onDragLeave={onDragLeavePill}
onDrop={
onDropOnPill
? (ev) => {
ev.preventDefault();
ev.stopPropagation();
const p = ev.dataTransfer.getData('text/plain');
if (p) onDropOnPill(p);
}
: undefined
}
className={`relative inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium cursor-grab active:cursor-grabbing select-none border border-duck-dark/15 dark:border-foreground/15 bg-background/60 text-duck-dark/80 dark:text-foreground/80 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors ${dropIndicator === 'left' ? 'ring-l-2 ring-duck-teal' : ''} ${dropIndicator === 'right' ? 'ring-r-2 ring-duck-teal' : ''}`}
>
{dropIndicator === 'left' && <span className="absolute -left-1 top-1 bottom-1 w-0.5 rounded-full bg-duck-teal" />}
<span className="w-2 h-2 rounded-full shrink-0" style={{ background: color }} />
{label}
<button
onClick={() => onAction(path)}
className="ml-0.5 p-0.5 rounded-full hover:bg-duck-dark/10 dark:hover:bg-foreground/10 cursor-pointer transition-colors"
>
{visible ? <X className="h-3 w-3" /> : <Plus className="h-3 w-3" />}
</button>
{dropIndicator === 'right' && <span className="absolute -right-1 top-1 bottom-1 w-0.5 rounded-full bg-duck-teal" />}
</span>
);
type DropZoneProps = {
label: string;
children: React.ReactNode;
onDrop: (path: string) => void;
};
const DropZone = ({ label, children, onDrop }: DropZoneProps) => {
const [over, setOver] = useState(false);
const handleDragOver = useCallback((ev: DragEvent) => {
ev.preventDefault();
setOver(true);
}, []);
const handleDragLeave = useCallback(() => setOver(false), []);
const handleDrop = useCallback(
(ev: DragEvent) => {
ev.preventDefault();
setOver(false);
const path = ev.dataTransfer.getData('text/plain');
if (path) onDrop(path);
},
[onDrop],
);
return (
<div className="grid gap-1.5">
<span className="text-xs font-medium text-duck-dark/50 dark:text-foreground/50 uppercase tracking-wide">
{label}
</span>
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`min-h-[48px] p-2 rounded-lg border border-dashed transition-colors flex flex-wrap gap-1.5 ${over ? 'border-duck-teal bg-duck-teal/5' : 'border-duck-dark/15 dark:border-foreground/15'}`}
>
{children}
</div>
</div>
);
};
export const DockSettings = () => {
const { t } = useTranslation();
// Same composition as the dock itself. Offering a pin for an uninstalled feature would let someone
// pin a tile that cannot appear, which reads as the setting being broken.
const { plugins } = usePermissions();
const allDockItems = useDockItems(plugins);
const { items, allItems, setItems, reset } = useDock(
allDockItems,
useMemo(() => defaultDockPaths(plugins), [plugins]),
);
const [dropTarget, setDropTarget] = useState<{ path: string; side: 'left' | 'right' } | null>(null);
const visiblePaths = new Set(items.map((i) => i.to));
const hiddenItems = allItems.filter((i) => !visiblePaths.has(i.to));
const onDragStart = useCallback((ev: DragEvent, path: string) => {
ev.dataTransfer.setData('text/plain', path);
ev.dataTransfer.effectAllowed = 'move';
}, []);
const addItem = useCallback(
(path: string) => {
if (visiblePaths.has(path)) return;
setItems([...items.map((i) => i.to), path]);
},
[items, visiblePaths, setItems],
);
const removeItem = useCallback(
(path: string) => {
setItems(items.filter((i) => i.to !== path).map((i) => i.to));
},
[items, setItems],
);
const insertAt = useCallback(
(draggedPath: string, targetPath: string, side: 'left' | 'right') => {
const paths = items.filter((i) => i.to !== draggedPath).map((i) => i.to);
const targetIdx = paths.indexOf(targetPath);
if (targetIdx === -1) return;
const insertIdx = side === 'left' ? targetIdx : targetIdx + 1;
paths.splice(insertIdx, 0, draggedPath);
setItems(paths);
setDropTarget(null);
},
[items, setItems],
);
const handlePillDragOver = useCallback((ev: DragEvent, pillPath: string) => {
ev.preventDefault();
ev.dataTransfer.dropEffect = 'move';
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
const midX = rect.left + rect.width / 2;
const side = ev.clientX < midX ? 'left' : 'right';
setDropTarget((prev) => (prev?.path === pillPath && prev?.side === side ? prev : { path: pillPath, side }));
}, []);
const handlePillDragLeave = useCallback(() => setDropTarget(null), []);
const handlePillDrop = useCallback(
(draggedPath: string, targetPath: string) => {
if (draggedPath === targetPath) {
setDropTarget(null);
return;
}
const side = dropTarget?.path === targetPath ? dropTarget.side : 'right';
insertAt(draggedPath, targetPath, side);
},
[dropTarget, insertAt],
);
const onDropVisible = useCallback(
(path: string) => {
setDropTarget(null);
if (visiblePaths.has(path)) return;
addItem(path);
},
[visiblePaths, addItem],
);
const onDropHidden = useCallback(
(path: string) => {
setDropTarget(null);
if (!visiblePaths.has(path)) return;
removeItem(path);
},
[visiblePaths, removeItem],
);
return (
<div className="grid gap-4">
<DropZone label={t('settings.profile.dock.visible')} onDrop={onDropVisible}>
{items.length === 0 && (
<span className="text-xs text-duck-dark/30 dark:text-foreground/30 py-1">
{t('settings.profile.dock.dragToShow')}
</span>
)}
{items.map((item) => (
<DockPill
key={item.to}
label={item.label}
path={item.to}
color={item.color}
visible
onAction={removeItem}
onDragStart={onDragStart}
dropIndicator={dropTarget?.path === item.to ? dropTarget.side : null}
onDragOverPill={(ev) => handlePillDragOver(ev, item.to)}
onDragLeavePill={handlePillDragLeave}
onDropOnPill={(draggedPath) => handlePillDrop(draggedPath, item.to)}
/>
))}
</DropZone>
<DropZone label={t('settings.profile.dock.hidden')} onDrop={onDropHidden}>
{hiddenItems.length === 0 && (
<span className="text-xs text-duck-dark/30 dark:text-foreground/30 py-1">
{t('settings.profile.dock.allVisible')}
</span>
)}
{hiddenItems.map((item) => (
<DockPill
key={item.to}
label={item.label}
path={item.to}
color={item.color}
visible={false}
onAction={addItem}
onDragStart={onDragStart}
/>
))}
</DropZone>
<Button type="button" variant="outline" onClick={reset} className="w-full h-9 text-sm cursor-pointer">
<RotateCcw className="h-3.5 w-3.5 mr-1.5" />
{t('settings.profile.dock.reset')}
</Button>
</div>
);
};