widgets panel keyed (516 -> 515)
Also adds tUi to officerdev's useTranslation, mirroring officer-web: the brief says to reach for ui.* where the string already exists there, and 'Cancel' does. Panel strings stay in panel.* — a ui.* key may not name a feature, and a panel is one.
This commit is contained in:
@@ -6,6 +6,7 @@ import { useAuth } from 'hooks/useAuth';
|
||||
import { Plus, X, RefreshCw } from 'lucide-react';
|
||||
import { useWidgetRegistry } from '../../WidgetRegistry';
|
||||
import { WidgetPicker } from './WidgetPicker';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
type Position = { x: number; y: number }; // percentages (0–100)
|
||||
|
||||
@@ -102,20 +103,23 @@ const DraggableWidget = ({ instance, panelRef, onMove, onRemove }: DraggableWidg
|
||||
[instance.position, panelRef],
|
||||
);
|
||||
|
||||
const endDrag = useCallback(
|
||||
(ev: ReactPointerEvent) => {
|
||||
if (!dragRef.current) return;
|
||||
dragRef.current = null;
|
||||
containerRef.current?.releasePointerCapture(ev.pointerId);
|
||||
},
|
||||
[],
|
||||
);
|
||||
const endDrag = useCallback((ev: ReactPointerEvent) => {
|
||||
if (!dragRef.current) return;
|
||||
dragRef.current = null;
|
||||
containerRef.current?.releasePointerCapture(ev.pointerId);
|
||||
}, []);
|
||||
|
||||
const onPointerMove = useCallback(
|
||||
(ev: ReactPointerEvent) => {
|
||||
if (!dragRef.current) return;
|
||||
const panelRect = panelRef.current?.getBoundingClientRect();
|
||||
if (panelRect && (ev.clientX < panelRect.left || ev.clientX > panelRect.right || ev.clientY < panelRect.top || ev.clientY > panelRect.bottom)) {
|
||||
if (
|
||||
panelRect &&
|
||||
(ev.clientX < panelRect.left ||
|
||||
ev.clientX > panelRect.right ||
|
||||
ev.clientY < panelRect.top ||
|
||||
ev.clientY > panelRect.bottom)
|
||||
) {
|
||||
endDrag(ev);
|
||||
return;
|
||||
}
|
||||
@@ -165,6 +169,7 @@ const DraggableWidget = ({ instance, panelRef, onMove, onRemove }: DraggableWidg
|
||||
// --- WidgetPanel ---
|
||||
|
||||
export const WidgetPanel = ({ panelId }: { panelId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const [config, setConfig] = useWidgetPanelState(panelId);
|
||||
const [instances, setInstances] = useState<WidgetInstance[]>(config.instances);
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
@@ -220,7 +225,9 @@ export const WidgetPanel = ({ panelId }: { panelId: string }) => {
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className={`absolute bottom-4 right-4 flex flex-col items-end transition-opacity ${showPicker ? 'opacity-100' : 'opacity-0 group-hover/panel:opacity-100'}`}>
|
||||
<div
|
||||
className={`absolute bottom-4 right-4 flex flex-col items-end transition-opacity ${showPicker ? 'opacity-100' : 'opacity-0 group-hover/panel:opacity-100'}`}
|
||||
>
|
||||
{showPicker && (
|
||||
<div className="mb-2 rounded-xl border border-border bg-card p-3 shadow-lg">
|
||||
<WidgetPicker onSelect={addWidget} />
|
||||
@@ -229,7 +236,7 @@ export const WidgetPanel = ({ panelId }: { panelId: string }) => {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
title="Resync widgets (reload to pick up newly-deployed widgets)"
|
||||
title={t('widgets.resync')}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-border bg-card text-muted-foreground shadow-lg transition-colors hover:text-foreground cursor-pointer"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"save": "Save"
|
||||
},
|
||||
"minesweeper": {
|
||||
"notInstalled": "Minesweeper is not installed on this server.",
|
||||
"needs": "It needs wine, a 32-bit prefix, and this package beside the platform:",
|
||||
@@ -81,5 +84,30 @@
|
||||
"saved": "File saved",
|
||||
"saveFailed": "Failed to save file",
|
||||
"title": "Code Editor"
|
||||
},
|
||||
"widgets": {
|
||||
"resync": "Resync widgets (reload to pick up newly-deployed widgets)"
|
||||
},
|
||||
"preview": {
|
||||
"title": "Preview",
|
||||
"reload": "Reload",
|
||||
"changeUrl": "Change URL",
|
||||
"openNewTab": "Open in a new tab",
|
||||
"pointAtApp": "Point this panel at a running app",
|
||||
"open": "Open",
|
||||
"hint": "Anything the browser can reach. It stays on this panel and survives a reload.",
|
||||
"connecting": "Connecting…"
|
||||
},
|
||||
"sendOff": {
|
||||
"files": "Files",
|
||||
"where": "Where",
|
||||
"loading": "Loading…",
|
||||
"landsIn": "Lands in their",
|
||||
"landedIn": "Landed in",
|
||||
"whoMaySend": "Who may send to me",
|
||||
"allowedLandIn": "Files from anyone you allow land in",
|
||||
"removeFile": "Remove {{name}}",
|
||||
"sendTo": "Send to {{name}}",
|
||||
"turningOff": "Turning someone off stops future sends and leaves what they already sent alone."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"save": "Guardar"
|
||||
},
|
||||
"minesweeper": {
|
||||
"notInstalled": "O Minesweeper não está instalado neste servidor.",
|
||||
"needs": "Precisa do wine, de um prefixo de 32 bits e deste pacote junto à plataforma:",
|
||||
@@ -81,5 +84,30 @@
|
||||
"saved": "Ficheiro guardado",
|
||||
"saveFailed": "Falha ao guardar o ficheiro",
|
||||
"title": "Editor de código"
|
||||
},
|
||||
"widgets": {
|
||||
"resync": "Ressincronizar widgets (recarregue para incluir widgets recém-implementados)"
|
||||
},
|
||||
"preview": {
|
||||
"title": "Pré-visualização",
|
||||
"reload": "Recarregar",
|
||||
"changeUrl": "Alterar URL",
|
||||
"openNewTab": "Abrir num novo separador",
|
||||
"pointAtApp": "Aponte este painel para uma aplicação em execução",
|
||||
"open": "Abrir",
|
||||
"hint": "Qualquer coisa que o navegador consiga alcançar. Fica neste painel e sobrevive a um recarregamento.",
|
||||
"connecting": "A ligar…"
|
||||
},
|
||||
"sendOff": {
|
||||
"files": "Ficheiros",
|
||||
"where": "Onde",
|
||||
"loading": "A carregar…",
|
||||
"landsIn": "Aterra na pasta",
|
||||
"landedIn": "Aterrou em",
|
||||
"whoMaySend": "Quem me pode enviar",
|
||||
"allowedLandIn": "Os ficheiros de quem autorizar aterram em",
|
||||
"removeFile": "Remover {{name}}",
|
||||
"sendTo": "Enviar para {{name}}",
|
||||
"turningOff": "Desativar alguém impede envios futuros e deixa intacto o que já enviou."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ import type { InterpolationParams, TranslationKeys } from 'i18n';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation as useTranslationBase } from 'i18n';
|
||||
import type en from './en-GB.json';
|
||||
import type uiEn from '@/components/locales/en-GB.json';
|
||||
|
||||
export type PanelTranslationKey = TranslationKeys<typeof en>;
|
||||
type UiTranslationKey = TranslationKeys<typeof uiEn>;
|
||||
|
||||
// The `panel.` prefix is namespace wiring for the merged catalog (D6) — callers never spell it, they
|
||||
// pass the same bare key the JSON file uses.
|
||||
@@ -15,5 +17,9 @@ export const useTranslation = () => {
|
||||
(key: PanelTranslationKey, params?: InterpolationParams) => baseT(`panel.${key}`, params),
|
||||
[baseT],
|
||||
);
|
||||
return { t, locale };
|
||||
// `ui.*` for the words the shared components already own — "Cancel", "Close". Panel-specific strings
|
||||
// stay in `panel.*`: a ui.* key may not name a feature, and a panel is a feature.
|
||||
const tUi = useCallback((key: UiTranslationKey, params?: InterpolationParams) => baseT(`ui.${key}`, params), [baseT]);
|
||||
|
||||
return { t, tUi, locale };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user