fix(quick-260916-dyv): Mindestgroessen inhaltsgetrieben (kleinste bedienbare Kachel je Typ), gespeicherte Minima ueberschrieben, Stoppuhr-Bedienleiste kompakt
- WIDGET_CONSTRAINTS: Minima aus dem Innenaufbau gerechnet (clock 2/2, search 6/2, calendar 3/3, note 4/4, calculator 3/9, favorites 3/3, link 3/2, stopwatch 4/3), Vorgaben unveraendert; Test A pinnt alle 32 Werte per toEqual - dashboard-grid.tsx: effectiveLayouts (useMemo) ueberschreibt minW/minH jedes gespeicherten Eintrags in jedem Breakpoint aus den Konstanten und hebt zu kleine w/h auf das Minimum an — RGL 2.2.3 nimmt gespeicherte Eintraege woertlich (synchronizeLayoutWithChildren) und liest data-grid nur ohne Eintrag - dashboard-grid.test.tsx: Test 5 angepasst, Test 9 (Ueberschreibung je Breakpoint, unbekannter Typ unveraendert) und Test 9b (h 8 -> 9, w >= minW bleibt) - stopwatch-widget.tsx: Bedienleiste px-2 py-1 text-xs, Zeile gap-1 py-1, damit Stop + Runde + Reset in 4 Spalten passen und die Kachel auf 4x3 schrumpfen kann Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -178,7 +178,7 @@ describe('DashboardGrid', () => {
|
|||||||
expect(captured.props?.containerPadding).toBeUndefined();
|
expect(captured.props?.containerPadding).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('quick-260916-bwo Test 5: Widget ohne gespeicherten Eintrag bekommt die verdoppelten Vorgaben als data-grid', async () => {
|
it('quick-260916-bwo Test 5: Widget ohne gespeicherten Eintrag bekommt die Vorgaben und die inhaltsgetriebenen Minima als data-grid', async () => {
|
||||||
captured.props = null;
|
captured.props = null;
|
||||||
const { DashboardGrid } = await import('./dashboard-grid');
|
const { DashboardGrid } = await import('./dashboard-grid');
|
||||||
render(
|
render(
|
||||||
@@ -196,6 +196,81 @@ describe('DashboardGrid', () => {
|
|||||||
const first = children[0];
|
const first = children[0];
|
||||||
expect(isValidElement(first)).toBe(true);
|
expect(isValidElement(first)).toBe(true);
|
||||||
const props = (first as React.ReactElement<Record<string, unknown>>).props;
|
const props = (first as React.ReactElement<Record<string, unknown>>).props;
|
||||||
expect(props['data-grid']).toEqual({ x: 0, y: 0, w: 4, h: 4, minW: 4, minH: 4 });
|
// quick-260916-dyv: Uhr-Minimum 2x2 (vorher verdoppelt 4x4), Vorgabe 4x4 unveraendert.
|
||||||
|
expect(props['data-grid']).toEqual({ x: 0, y: 0, w: 4, h: 4, minW: 2, minH: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('quick-260916-dyv Test 9: gespeicherte minW/minH werden in JEDEM Breakpoint aus WIDGET_CONSTRAINTS ueberschrieben, x/y/w/h bleiben, unbekannte Typen und Schluessel unveraendert', async () => {
|
||||||
|
captured.props = null;
|
||||||
|
const { DashboardGrid } = await import('./dashboard-grid');
|
||||||
|
const layouts = {
|
||||||
|
lg: [
|
||||||
|
{ i: 'inst-1', x: 2, y: 4, w: 6, h: 6, minW: 8, minH: 8 },
|
||||||
|
{ i: 'inst-x', x: 10, y: 0, w: 5, h: 5, minW: 7, minH: 7 },
|
||||||
|
],
|
||||||
|
md: [{ i: 'inst-1', x: 0, y: 0, w: 4, h: 4, minW: 4, minH: 4 }],
|
||||||
|
sm: [],
|
||||||
|
xs: [],
|
||||||
|
xxs: [],
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<DashboardGrid
|
||||||
|
layouts={layouts}
|
||||||
|
widgets={[
|
||||||
|
{ id: 'inst-1', widgetType: 'clock', config: {} },
|
||||||
|
{ id: 'inst-x', widgetType: 'unknown', config: {} },
|
||||||
|
]}
|
||||||
|
isEditMode={false}
|
||||||
|
onLayoutChange={vi.fn()}
|
||||||
|
onRemoveWidget={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const passed = captured.props?.layouts as Record<string, Array<Record<string, unknown>>>;
|
||||||
|
expect(Object.keys(passed)).toEqual(Object.keys(layouts));
|
||||||
|
expect(passed.lg[0]).toEqual({ i: 'inst-1', x: 2, y: 4, w: 6, h: 6, minW: 2, minH: 2 });
|
||||||
|
expect(passed.md[0]).toEqual({ i: 'inst-1', x: 0, y: 0, w: 4, h: 4, minW: 2, minH: 2 });
|
||||||
|
expect(passed.md[0].minW).toBe(2);
|
||||||
|
// Unbekannter Typ: Eintrag bleibt woertlich erhalten (kein Absturz, T-DYV-01).
|
||||||
|
expect(passed.lg[1]).toEqual({ i: 'inst-x', x: 10, y: 0, w: 5, h: 5, minW: 7, minH: 7 });
|
||||||
|
expect(passed.sm).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('quick-260916-dyv Test 9b: gespeicherte Groesse unter dem neuen Minimum wird auf das Minimum angehoben (Rechner h 8 -> 9, w bleibt wenn >= minW)', async () => {
|
||||||
|
captured.props = null;
|
||||||
|
const { DashboardGrid } = await import('./dashboard-grid');
|
||||||
|
const layouts = {
|
||||||
|
lg: [
|
||||||
|
// Rechner: gespeichert mit h 8 (bwo-Minimum), neues Minimum 3x9 -> h wird 9, w 6 bleibt.
|
||||||
|
{ i: 'calc-1', x: 0, y: 0, w: 6, h: 8, minW: 4, minH: 8 },
|
||||||
|
// Suche: gespeichert mit w 4 (unter minW 6) -> w wird 6, h 4 bleibt.
|
||||||
|
{ i: 'srch-1', x: 6, y: 0, w: 4, h: 4, minW: 6, minH: 4 },
|
||||||
|
],
|
||||||
|
md: [{ i: 'calc-1', x: 0, y: 0, w: 3, h: 7, minW: 4, minH: 8 }],
|
||||||
|
sm: [],
|
||||||
|
xs: [],
|
||||||
|
xxs: [],
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<DashboardGrid
|
||||||
|
layouts={layouts}
|
||||||
|
widgets={[
|
||||||
|
{ id: 'calc-1', widgetType: 'calculator', config: {} },
|
||||||
|
{ id: 'srch-1', widgetType: 'search', config: {} },
|
||||||
|
]}
|
||||||
|
isEditMode={false}
|
||||||
|
onLayoutChange={vi.fn()}
|
||||||
|
onRemoveWidget={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const passed = captured.props?.layouts as Record<string, Array<Record<string, unknown>>>;
|
||||||
|
expect(passed.lg[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 6, h: 9, minW: 3, minH: 9 });
|
||||||
|
expect(passed.lg[1]).toEqual({ i: 'srch-1', x: 6, y: 0, w: 6, h: 4, minW: 6, minH: 2 });
|
||||||
|
// Jeder Breakpoint: md-Rechner 3x7 -> 3x9 (w schon = minW, h angehoben).
|
||||||
|
expect(passed.md[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 3, h: 9, minW: 3, minH: 9 });
|
||||||
|
// Eingabe unveraendert (keine Mutation des Store-Objekts).
|
||||||
|
expect(layouts.lg[0].h).toBe(8);
|
||||||
|
expect(layouts.lg[0].minH).toBe(8);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Responsive, noCompactor } from 'react-grid-layout';
|
import { Responsive, noCompactor } from 'react-grid-layout';
|
||||||
import type { ResponsiveLayouts } from 'react-grid-layout';
|
import type { ResponsiveLayouts } from 'react-grid-layout';
|
||||||
import 'react-grid-layout/css/styles.css';
|
import 'react-grid-layout/css/styles.css';
|
||||||
@@ -17,14 +17,72 @@ import { TesseraLogo } from '@/components/brand/tessera-logo';
|
|||||||
const BREAKPOINTS = { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 };
|
const BREAKPOINTS = { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 };
|
||||||
const COLS = { lg: 24, md: 20, sm: 12, xs: 8, xxs: 2 };
|
const COLS = { lg: 24, md: 20, sm: 12, xs: 8, xxs: 2 };
|
||||||
|
|
||||||
|
interface LayoutItemShape {
|
||||||
|
i: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
minW?: number;
|
||||||
|
minH?: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface DashboardGridProps {
|
interface DashboardGridProps {
|
||||||
layouts: Record<string, Array<{ i: string; x: number; y: number; w: number; h: number }>>;
|
layouts: Record<string, Array<LayoutItemShape>>;
|
||||||
widgets: Array<{ id: string; widgetType: string; config: Record<string, unknown> }>;
|
widgets: Array<{ id: string; widgetType: string; config: Record<string, unknown> }>;
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
onLayoutChange: (allLayouts: ResponsiveLayouts) => void;
|
onLayoutChange: (allLayouts: ResponsiveLayouts) => void;
|
||||||
onRemoveWidget: (id: string) => void;
|
onRemoveWidget: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* quick-260916-dyv: Gespeicherte minW/minH aus den Konstanten ueberschreiben.
|
||||||
|
*
|
||||||
|
* Warum: react-grid-layout 2.2.3 nimmt in `synchronizeLayoutWithChildren`
|
||||||
|
* (chunk-WGL5FSZH.mjs:559-562) fuer jedes Kind MIT Eintrag im `layout` den
|
||||||
|
* Eintrag per `cloneLayoutItem` woertlich — inklusive minW/minH — und liest
|
||||||
|
* `data-grid` nur fuer Kinder OHNE Eintrag. Gespeicherte Anordnungen tragen
|
||||||
|
* minW/minH (RGL liefert sie in `onLayoutChange`, 260916-bwo hat sie verdoppelt
|
||||||
|
* persistiert). Eine Aenderung von WIDGET_CONSTRAINTS allein aendert fuer
|
||||||
|
* bestehende Widgets also NICHTS. Deshalb: in JEDEM Breakpoint minW/minH jedes
|
||||||
|
* Eintrags eines bekannten Typs aus der Tabelle setzen; die Konstanten sind
|
||||||
|
* die einzige Quelle (T-DYV-01: manipulierte Minima wirken nicht).
|
||||||
|
*
|
||||||
|
* Liegt die gespeicherte Groesse UNTER dem neuen Minimum (z. B. Rechner mit
|
||||||
|
* h 8, neues minH 9), wird w/h auf das Minimum angehoben: RGL selbst klemmt
|
||||||
|
* w/h nur beim Vergroessern/Verkleinern (`minMaxSize.constrainSize`,
|
||||||
|
* chunk-KDANGDDL.mjs:26-31; `minConstraints` an `Resizable`,
|
||||||
|
* chunk-WGL5FSZH.mjs:472-475), rendert die zu kleine Kachel aber woertlich —
|
||||||
|
* der Rechner bliebe bis zum ersten Anfassen unten abgeschnitten.
|
||||||
|
* Unbekannte Typen bleiben unveraendert; das Eingabeobjekt wird nicht mutiert.
|
||||||
|
*/
|
||||||
|
function applyConstraintMinima(
|
||||||
|
layouts: Record<string, Array<LayoutItemShape>>,
|
||||||
|
widgets: Array<{ id: string; widgetType: string }>,
|
||||||
|
): Record<string, Array<LayoutItemShape>> {
|
||||||
|
const typeById = new Map<string, string>();
|
||||||
|
for (const w of widgets) typeById.set(w.id, w.widgetType);
|
||||||
|
|
||||||
|
const result: Record<string, Array<LayoutItemShape>> = {};
|
||||||
|
for (const key of Object.keys(layouts)) {
|
||||||
|
const entries = layouts[key];
|
||||||
|
if (!Array.isArray(entries)) continue;
|
||||||
|
result[key] = entries.map((entry) => {
|
||||||
|
const type = typeById.get(entry.i);
|
||||||
|
const constraints = type ? WIDGET_CONSTRAINTS[type as WidgetType] : undefined;
|
||||||
|
if (!constraints) return { ...entry };
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
w: Math.max(entry.w, constraints.minW),
|
||||||
|
h: Math.max(entry.h, constraints.minH),
|
||||||
|
minW: constraints.minW,
|
||||||
|
minH: constraints.minH,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dashboard grid wrapper using react-grid-layout v2 Responsive component.
|
* Dashboard grid wrapper using react-grid-layout v2 Responsive component.
|
||||||
* Renders widgets in a draggable/resizable grid when in edit mode.
|
* Renders widgets in a draggable/resizable grid when in edit mode.
|
||||||
@@ -53,6 +111,11 @@ export function DashboardGrid({
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// quick-260916-dyv: minW/minH (und zu kleine w/h) aus WIDGET_CONSTRAINTS —
|
||||||
|
// siehe applyConstraintMinima. Vor dem Leerzustand, damit die Hook-Reihenfolge
|
||||||
|
// stabil bleibt.
|
||||||
|
const effectiveLayouts = useMemo(() => applyConstraintMinima(layouts, widgets), [layouts, widgets]);
|
||||||
|
|
||||||
// Empty state (D-02)
|
// Empty state (D-02)
|
||||||
if (widgets.length === 0) {
|
if (widgets.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -77,7 +140,7 @@ export function DashboardGrid({
|
|||||||
width={width}
|
width={width}
|
||||||
breakpoints={BREAKPOINTS}
|
breakpoints={BREAKPOINTS}
|
||||||
cols={COLS}
|
cols={COLS}
|
||||||
layouts={layouts as ResponsiveLayouts}
|
layouts={effectiveLayouts as ResponsiveLayouts}
|
||||||
rowHeight={20}
|
rowHeight={20}
|
||||||
margin={[8, 8] as [number, number]}
|
margin={[8, 8] as [number, number]}
|
||||||
// containerPadding bewusst nicht gesetzt: react-grid-layout 2.2.3 nimmt
|
// containerPadding bewusst nicht gesetzt: react-grid-layout 2.2.3 nimmt
|
||||||
@@ -98,7 +161,7 @@ export function DashboardGrid({
|
|||||||
<div
|
<div
|
||||||
key={widget.id}
|
key={widget.id}
|
||||||
data-grid={{
|
data-grid={{
|
||||||
...(layouts.lg?.find((l) => l.i === widget.id) ?? {
|
...(effectiveLayouts.lg?.find((l) => l.i === widget.id) ?? {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
w: constraints?.defaultW ?? 4,
|
w: constraints?.defaultW ?? 4,
|
||||||
|
|||||||
@@ -53,24 +53,26 @@ describe('WIDGET_CONSTRAINTS (DASH-11)', () => {
|
|||||||
expect(Object.keys(WIDGET_CONSTRAINTS)).toContain('stopwatch');
|
expect(Object.keys(WIDGET_CONSTRAINTS)).toContain('stopwatch');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('quick-260916-bwo: jede Groesse ist exakt das Doppelte der alten 12-Spalten-Werte', () => {
|
it('Test A (quick-260916-dyv): Minima = kleinste bedienbare Kachel je Typ, Vorgaben unveraendert', () => {
|
||||||
// Raster seit quick-260916-bwo 24 Spalten / 20 px statt 12 / 40 — jeder Wert
|
// Raster 24 Spalten / 20 px (quick-260916-bwo). Die Minima sind seit
|
||||||
// ist das Doppelte, damit Widgets optisch gleich gross bleiben.
|
// quick-260916-dyv inhaltsgetrieben (Rechnung im Plan), die Vorgaben
|
||||||
|
// (defaultW/defaultH) bleiben das Doppelte der alten 12-Spalten-Werte.
|
||||||
|
// 3 und 9 sind ungerade — die bwo-Pruefung "jeder Wert ist gerade" entfaellt.
|
||||||
expect(WIDGET_CONSTRAINTS).toEqual({
|
expect(WIDGET_CONSTRAINTS).toEqual({
|
||||||
clock: { minW: 4, minH: 4, defaultW: 4, defaultH: 4 },
|
clock: { minW: 2, minH: 2, defaultW: 4, defaultH: 4 },
|
||||||
search: { minW: 6, minH: 4, defaultW: 12, defaultH: 4 },
|
search: { minW: 6, minH: 2, defaultW: 12, defaultH: 4 },
|
||||||
calendar: { minW: 6, minH: 6, defaultW: 8, defaultH: 12 },
|
calendar: { minW: 3, minH: 3, defaultW: 8, defaultH: 12 },
|
||||||
note: { minW: 4, minH: 6, defaultW: 6, defaultH: 8 },
|
note: { minW: 4, minH: 4, defaultW: 6, defaultH: 8 },
|
||||||
calculator: { minW: 4, minH: 8, defaultW: 6, defaultH: 10 },
|
calculator: { minW: 3, minH: 9, defaultW: 6, defaultH: 10 },
|
||||||
favorites: { minW: 4, minH: 6, defaultW: 6, defaultH: 10 },
|
favorites: { minW: 3, minH: 3, defaultW: 6, defaultH: 10 },
|
||||||
link: { minW: 4, minH: 4, defaultW: 4, defaultH: 4 },
|
link: { minW: 3, minH: 2, defaultW: 4, defaultH: 4 },
|
||||||
stopwatch: { minW: 4, minH: 4, defaultW: 6, defaultH: 6 },
|
stopwatch: { minW: 4, minH: 3, defaultW: 6, defaultH: 6 },
|
||||||
});
|
});
|
||||||
|
|
||||||
let counted = 0;
|
let counted = 0;
|
||||||
for (const type of ALL_WIDGET_TYPES) {
|
for (const type of ALL_WIDGET_TYPES) {
|
||||||
for (const field of ['minW', 'minH', 'defaultW', 'defaultH'] as const) {
|
for (const field of ['minW', 'minH', 'defaultW', 'defaultH'] as const) {
|
||||||
expect(WIDGET_CONSTRAINTS[type][field] % 2, `${type}.${field} muss gerade sein`).toBe(0);
|
expect(typeof WIDGET_CONSTRAINTS[type][field]).toBe('number');
|
||||||
counted++;
|
counted++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,17 +32,22 @@ export const WIDGET_CONSTRAINTS: Record<
|
|||||||
WidgetType,
|
WidgetType,
|
||||||
{ minW: number; minH: number; defaultW: number; defaultH: number }
|
{ minW: number; minH: number; defaultW: number; defaultH: number }
|
||||||
> = {
|
> = {
|
||||||
// quick-260916-bwo: Raster verdoppelt (24 Spalten / 20 px) — jeder Wert ist das Doppelte des alten 12-Spalten-Werts, Widgets bleiben optisch gleich gross.
|
// quick-260916-dyv: minW/minH = kleinste noch bedienbare Kachel je Typ im
|
||||||
|
// 24-Spalten/20-px-Raster, aus dem Innenaufbau gerechnet (Suche: Auswahl 120 +
|
||||||
|
// Eingabe + Knopf; Rechner: Anzeige 40 + Speicherzeile 28 + 5 Tastenreihen 28 =
|
||||||
|
// 240 px -> 9 Zeilen; Stoppuhr: kompakte Bedienleiste). defaultW/defaultH =
|
||||||
|
// altes 12-Spalten-Mass x2, unveraendert. Gespeicherte minW/minH werden in
|
||||||
|
// dashboard-grid.tsx aus dieser Tabelle ueberschrieben.
|
||||||
// Existing widgets
|
// Existing widgets
|
||||||
clock: { minW: 4, minH: 4, defaultW: 4, defaultH: 4 },
|
clock: { minW: 2, minH: 2, defaultW: 4, defaultH: 4 },
|
||||||
search: { minW: 6, minH: 4, defaultW: 12, defaultH: 4 },
|
search: { minW: 6, minH: 2, defaultW: 12, defaultH: 4 },
|
||||||
calendar: { minW: 6, minH: 6, defaultW: 8, defaultH: 12 },
|
calendar: { minW: 3, minH: 3, defaultW: 8, defaultH: 12 },
|
||||||
note: { minW: 4, minH: 6, defaultW: 6, defaultH: 8 },
|
note: { minW: 4, minH: 4, defaultW: 6, defaultH: 8 },
|
||||||
// Phase 8 new widgets (D-01)
|
// Phase 8 new widgets (D-01)
|
||||||
calculator: { minW: 4, minH: 8, defaultW: 6, defaultH: 10 },
|
calculator: { minW: 3, minH: 9, defaultW: 6, defaultH: 10 },
|
||||||
favorites: { minW: 4, minH: 6, defaultW: 6, defaultH: 10 },
|
favorites: { minW: 3, minH: 3, defaultW: 6, defaultH: 10 },
|
||||||
link: { minW: 4, minH: 4, defaultW: 4, defaultH: 4 },
|
link: { minW: 3, minH: 2, defaultW: 4, defaultH: 4 },
|
||||||
stopwatch: { minW: 4, minH: 4, defaultW: 6, defaultH: 6 },
|
stopwatch: { minW: 4, minH: 3, defaultW: 6, defaultH: 6 },
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -200,14 +200,17 @@ export function StopwatchWidget({ instanceId, config, isEditMode: _isEditMode }:
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Controls
|
||||||
<div className="flex items-center justify-center gap-2 py-2">
|
quick-260916-dyv: kompakte Bedienleiste, damit die laufende Stoppuhr
|
||||||
|
(Stop + Runde + Reset) in 4 Spalten passt (ca. 151 px statt ca. 222 px)
|
||||||
|
und die Kachel auf 4x3 schrumpfen kann. */}
|
||||||
|
<div className="flex items-center justify-center gap-1 py-1">
|
||||||
{!isRunning ? (
|
{!isRunning ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t('stopwatch.start')}
|
aria-label={t('stopwatch.start')}
|
||||||
onClick={handleStart}
|
onClick={handleStart}
|
||||||
className="rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity"
|
className="rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground hover:opacity-90 transition-opacity"
|
||||||
>
|
>
|
||||||
{t('stopwatch.start')}
|
{t('stopwatch.start')}
|
||||||
</button>
|
</button>
|
||||||
@@ -217,7 +220,7 @@ export function StopwatchWidget({ instanceId, config, isEditMode: _isEditMode }:
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={t('stopwatch.stop')}
|
aria-label={t('stopwatch.stop')}
|
||||||
onClick={handleStop}
|
onClick={handleStop}
|
||||||
className="rounded-md bg-muted px-4 py-1.5 text-sm font-medium text-foreground hover:opacity-90 transition-opacity"
|
className="rounded-md bg-muted px-2 py-1 text-xs font-medium text-foreground hover:opacity-90 transition-opacity"
|
||||||
>
|
>
|
||||||
{t('stopwatch.stop')}
|
{t('stopwatch.stop')}
|
||||||
</button>
|
</button>
|
||||||
@@ -225,7 +228,7 @@ export function StopwatchWidget({ instanceId, config, isEditMode: _isEditMode }:
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={t('stopwatch.lap')}
|
aria-label={t('stopwatch.lap')}
|
||||||
onClick={handleLap}
|
onClick={handleLap}
|
||||||
className="rounded-md bg-muted px-4 py-1.5 text-sm font-medium text-foreground hover:opacity-90 transition-opacity"
|
className="rounded-md bg-muted px-2 py-1 text-xs font-medium text-foreground hover:opacity-90 transition-opacity"
|
||||||
>
|
>
|
||||||
{t('stopwatch.lap')}
|
{t('stopwatch.lap')}
|
||||||
</button>
|
</button>
|
||||||
@@ -235,7 +238,7 @@ export function StopwatchWidget({ instanceId, config, isEditMode: _isEditMode }:
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={t('stopwatch.reset')}
|
aria-label={t('stopwatch.reset')}
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
className="rounded-md border border-border px-4 py-1.5 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
className="rounded-md border border-border px-2 py-1 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
{t('stopwatch.reset')}
|
{t('stopwatch.reset')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user