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 { Plus, X, RefreshCw } from 'lucide-react';
|
||||||
import { useWidgetRegistry } from '../../WidgetRegistry';
|
import { useWidgetRegistry } from '../../WidgetRegistry';
|
||||||
import { WidgetPicker } from './WidgetPicker';
|
import { WidgetPicker } from './WidgetPicker';
|
||||||
|
import { useTranslation } from '../../locales';
|
||||||
|
|
||||||
type Position = { x: number; y: number }; // percentages (0–100)
|
type Position = { x: number; y: number }; // percentages (0–100)
|
||||||
|
|
||||||
@@ -102,20 +103,23 @@ const DraggableWidget = ({ instance, panelRef, onMove, onRemove }: DraggableWidg
|
|||||||
[instance.position, panelRef],
|
[instance.position, panelRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
const endDrag = useCallback(
|
const endDrag = useCallback((ev: ReactPointerEvent) => {
|
||||||
(ev: ReactPointerEvent) => {
|
if (!dragRef.current) return;
|
||||||
if (!dragRef.current) return;
|
dragRef.current = null;
|
||||||
dragRef.current = null;
|
containerRef.current?.releasePointerCapture(ev.pointerId);
|
||||||
containerRef.current?.releasePointerCapture(ev.pointerId);
|
}, []);
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPointerMove = useCallback(
|
const onPointerMove = useCallback(
|
||||||
(ev: ReactPointerEvent) => {
|
(ev: ReactPointerEvent) => {
|
||||||
if (!dragRef.current) return;
|
if (!dragRef.current) return;
|
||||||
const panelRect = panelRef.current?.getBoundingClientRect();
|
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);
|
endDrag(ev);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -165,6 +169,7 @@ const DraggableWidget = ({ instance, panelRef, onMove, onRemove }: DraggableWidg
|
|||||||
// --- WidgetPanel ---
|
// --- WidgetPanel ---
|
||||||
|
|
||||||
export const WidgetPanel = ({ panelId }: { panelId: string }) => {
|
export const WidgetPanel = ({ panelId }: { panelId: string }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [config, setConfig] = useWidgetPanelState(panelId);
|
const [config, setConfig] = useWidgetPanelState(panelId);
|
||||||
const [instances, setInstances] = useState<WidgetInstance[]>(config.instances);
|
const [instances, setInstances] = useState<WidgetInstance[]>(config.instances);
|
||||||
const [showPicker, setShowPicker] = useState(false);
|
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 && (
|
{showPicker && (
|
||||||
<div className="mb-2 rounded-xl border border-border bg-card p-3 shadow-lg">
|
<div className="mb-2 rounded-xl border border-border bg-card p-3 shadow-lg">
|
||||||
<WidgetPicker onSelect={addWidget} />
|
<WidgetPicker onSelect={addWidget} />
|
||||||
@@ -229,7 +236,7 @@ export const WidgetPanel = ({ panelId }: { panelId: string }) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="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"
|
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()}
|
onClick={() => window.location.reload()}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"common": {
|
||||||
|
"save": "Save"
|
||||||
|
},
|
||||||
"minesweeper": {
|
"minesweeper": {
|
||||||
"notInstalled": "Minesweeper is not installed on this server.",
|
"notInstalled": "Minesweeper is not installed on this server.",
|
||||||
"needs": "It needs wine, a 32-bit prefix, and this package beside the platform:",
|
"needs": "It needs wine, a 32-bit prefix, and this package beside the platform:",
|
||||||
@@ -81,5 +84,30 @@
|
|||||||
"saved": "File saved",
|
"saved": "File saved",
|
||||||
"saveFailed": "Failed to save file",
|
"saveFailed": "Failed to save file",
|
||||||
"title": "Code Editor"
|
"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": {
|
"minesweeper": {
|
||||||
"notInstalled": "O Minesweeper não está instalado neste servidor.",
|
"notInstalled": "O Minesweeper não está instalado neste servidor.",
|
||||||
"needs": "Precisa do wine, de um prefixo de 32 bits e deste pacote junto à plataforma:",
|
"needs": "Precisa do wine, de um prefixo de 32 bits e deste pacote junto à plataforma:",
|
||||||
@@ -81,5 +84,30 @@
|
|||||||
"saved": "Ficheiro guardado",
|
"saved": "Ficheiro guardado",
|
||||||
"saveFailed": "Falha ao guardar o ficheiro",
|
"saveFailed": "Falha ao guardar o ficheiro",
|
||||||
"title": "Editor de código"
|
"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 { useCallback } from 'react';
|
||||||
import { useTranslation as useTranslationBase } from 'i18n';
|
import { useTranslation as useTranslationBase } from 'i18n';
|
||||||
import type en from './en-GB.json';
|
import type en from './en-GB.json';
|
||||||
|
import type uiEn from '@/components/locales/en-GB.json';
|
||||||
|
|
||||||
export type PanelTranslationKey = TranslationKeys<typeof en>;
|
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
|
// 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.
|
// pass the same bare key the JSON file uses.
|
||||||
@@ -15,5 +17,9 @@ export const useTranslation = () => {
|
|||||||
(key: PanelTranslationKey, params?: InterpolationParams) => baseT(`panel.${key}`, params),
|
(key: PanelTranslationKey, params?: InterpolationParams) => baseT(`panel.${key}`, params),
|
||||||
[baseT],
|
[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