feat(08-01): registry foundation for 4 new widget types + Calculator widget (GREEN)
- widget-registry.tsx: extend WidgetType union with calculator/favorites/link/stopwatch - widget-registry.tsx: add WIDGET_CONSTRAINTS entries with per-widget grid constraints (DASH-11) - widget-registry.tsx: add SVG icons (CalculatorIcon, FavoritesIcon, LinkIcon, StopwatchIcon) - widget-registry.tsx: add WIDGET_REGISTRY entries and wire functions for all 4 new types - calculator-widget.tsx: full arithmetic implementation ported from personal-dashboard (parseDisplay, formatNumber, calculate, keyboard handler with stopPropagation) - widget-catalog-modal.tsx: extend WIDGET_TYPES to include all 8 types - create-widget.dto.ts: extend @IsIn to accept 8 widget types (T-08-01 mitigated) - page.tsx: import CalculatorWidget and call wireCalculatorWidget() - de.json / en.json: add i18n keys for calculator, favorites, link, stopwatch - All 16 tests passing (GREEN)
This commit is contained in:
@@ -2,10 +2,18 @@ import type { ComponentType } from 'react';
|
||||
|
||||
/**
|
||||
* Supported widget types for the dashboard.
|
||||
* clock is fully implemented in this plan; search/calendar/note are
|
||||
* declared here so the catalog shows all four types immediately.
|
||||
* clock/search/calendar/note: implemented in previous plans.
|
||||
* calculator/favorites/link/stopwatch: Phase 8 additions.
|
||||
*/
|
||||
export type WidgetType = 'clock' | 'search' | 'calendar' | 'note';
|
||||
export type WidgetType =
|
||||
| 'clock'
|
||||
| 'search'
|
||||
| 'calendar'
|
||||
| 'note'
|
||||
| 'calculator'
|
||||
| 'favorites'
|
||||
| 'link'
|
||||
| 'stopwatch';
|
||||
|
||||
/**
|
||||
* Props contract that every widget component must accept.
|
||||
@@ -18,16 +26,22 @@ export interface WidgetProps {
|
||||
|
||||
/**
|
||||
* Per-type size constraints for the react-grid-layout grid.
|
||||
* Values from UI-SPEC size table (D-06).
|
||||
* Values from UI-SPEC size table (D-06) and Phase-8 Grid-Constraints (D-01).
|
||||
*/
|
||||
export const WIDGET_CONSTRAINTS: Record<
|
||||
WidgetType,
|
||||
{ minW: number; minH: number; defaultW: number; defaultH: number }
|
||||
> = {
|
||||
// Existing widgets (values unchanged)
|
||||
clock: { minW: 2, minH: 2, defaultW: 2, defaultH: 2 },
|
||||
search: { minW: 3, minH: 2, defaultW: 6, defaultH: 2 },
|
||||
calendar: { minW: 3, minH: 3, defaultW: 4, defaultH: 6 },
|
||||
note: { minW: 2, minH: 3, defaultW: 3, defaultH: 4 },
|
||||
// Phase 8 new widgets (D-01)
|
||||
calculator: { minW: 2, minH: 4, defaultW: 3, defaultH: 5 },
|
||||
favorites: { minW: 2, minH: 3, defaultW: 3, defaultH: 5 },
|
||||
link: { minW: 2, minH: 2, defaultW: 2, defaultH: 2 },
|
||||
stopwatch: { minW: 2, minH: 2, defaultW: 3, defaultH: 3 },
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -147,12 +161,98 @@ function NoteIcon({ className }: { className?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function CalculatorIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<rect x="4" y="2" width="16" height="20" rx="2" />
|
||||
<line x1="8" y1="6" x2="16" y2="6" />
|
||||
<line x1="8" y1="10" x2="8" y2="10" />
|
||||
<line x1="12" y1="10" x2="12" y2="10" />
|
||||
<line x1="16" y1="10" x2="16" y2="10" />
|
||||
<line x1="8" y1="14" x2="8" y2="14" />
|
||||
<line x1="12" y1="14" x2="12" y2="14" />
|
||||
<line x1="16" y1="14" x2="16" y2="18" />
|
||||
<line x1="8" y1="18" x2="8" y2="18" />
|
||||
<line x1="12" y1="18" x2="12" y2="18" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function FavoritesIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function StopwatchIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<circle cx="12" cy="13" r="8" />
|
||||
<polyline points="12 9 12 13 14 15" />
|
||||
<line x1="9" y1="2" x2="15" y2="2" />
|
||||
<line x1="12" y1="2" x2="12" y2="4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of all widget types. Used by the widget catalog modal
|
||||
* and the grid renderer to look up components and constraints.
|
||||
*
|
||||
* clock: Fully implemented this plan.
|
||||
* search, calendar, note: Placeholder components — implemented in later plans.
|
||||
*/
|
||||
export const WIDGET_REGISTRY: Record<WidgetType, WidgetDefinition> = {
|
||||
clock: {
|
||||
@@ -161,8 +261,7 @@ export const WIDGET_REGISTRY: Record<WidgetType, WidgetDefinition> = {
|
||||
descriptionKey: 'clock.description',
|
||||
icon: ClockIcon,
|
||||
...WIDGET_CONSTRAINTS.clock,
|
||||
// Lazy import replaced at module level for clock
|
||||
component: PlaceholderWidget, // Replaced below
|
||||
component: PlaceholderWidget, // Replaced via wireClockWidget()
|
||||
},
|
||||
search: {
|
||||
type: 'search',
|
||||
@@ -178,7 +277,7 @@ export const WIDGET_REGISTRY: Record<WidgetType, WidgetDefinition> = {
|
||||
descriptionKey: 'calendar.description',
|
||||
icon: CalendarIcon,
|
||||
...WIDGET_CONSTRAINTS.calendar,
|
||||
component: PlaceholderWidget,
|
||||
component: PlaceholderWidget, // Replaced via wireCalendarWidget()
|
||||
},
|
||||
note: {
|
||||
type: 'note',
|
||||
@@ -188,15 +287,46 @@ export const WIDGET_REGISTRY: Record<WidgetType, WidgetDefinition> = {
|
||||
...WIDGET_CONSTRAINTS.note,
|
||||
component: PlaceholderWidget, // Replaced via wireNoteWidget()
|
||||
},
|
||||
calculator: {
|
||||
type: 'calculator',
|
||||
nameKey: 'calculator.name',
|
||||
descriptionKey: 'calculator.description',
|
||||
icon: CalculatorIcon,
|
||||
...WIDGET_CONSTRAINTS.calculator,
|
||||
component: PlaceholderWidget, // Replaced via wireCalculatorWidget()
|
||||
},
|
||||
favorites: {
|
||||
type: 'favorites',
|
||||
nameKey: 'favorites.name',
|
||||
descriptionKey: 'favorites.description',
|
||||
icon: FavoritesIcon,
|
||||
...WIDGET_CONSTRAINTS.favorites,
|
||||
component: PlaceholderWidget, // Replaced via wireFavoritesWidget()
|
||||
},
|
||||
link: {
|
||||
type: 'link',
|
||||
nameKey: 'link.name',
|
||||
descriptionKey: 'link.description',
|
||||
icon: LinkIcon,
|
||||
...WIDGET_CONSTRAINTS.link,
|
||||
component: PlaceholderWidget, // Replaced via wireLinkWidget()
|
||||
},
|
||||
stopwatch: {
|
||||
type: 'stopwatch',
|
||||
nameKey: 'stopwatch.name',
|
||||
descriptionKey: 'stopwatch.description',
|
||||
icon: StopwatchIcon,
|
||||
...WIDGET_CONSTRAINTS.stopwatch,
|
||||
component: PlaceholderWidget, // Replaced via wireStopwatchWidget()
|
||||
},
|
||||
};
|
||||
|
||||
// Wire actual widget components lazily to avoid circular deps
|
||||
// (imports are deferred so widget-registry can be imported by tests without
|
||||
// pulling in the entire React tree)
|
||||
|
||||
let clockWired = false;
|
||||
export function wireClockWidget(
|
||||
component: ComponentType<WidgetProps>,
|
||||
) {
|
||||
export function wireClockWidget(component: ComponentType<WidgetProps>) {
|
||||
if (!clockWired) {
|
||||
WIDGET_REGISTRY.clock.component = component;
|
||||
clockWired = true;
|
||||
@@ -204,9 +334,7 @@ export function wireClockWidget(
|
||||
}
|
||||
|
||||
let searchWired = false;
|
||||
export function wireSearchWidget(
|
||||
component: ComponentType<WidgetProps>,
|
||||
) {
|
||||
export function wireSearchWidget(component: ComponentType<WidgetProps>) {
|
||||
if (!searchWired) {
|
||||
WIDGET_REGISTRY.search.component = component;
|
||||
searchWired = true;
|
||||
@@ -214,9 +342,7 @@ export function wireSearchWidget(
|
||||
}
|
||||
|
||||
let calendarWired = false;
|
||||
export function wireCalendarWidget(
|
||||
component: ComponentType<WidgetProps>,
|
||||
) {
|
||||
export function wireCalendarWidget(component: ComponentType<WidgetProps>) {
|
||||
if (!calendarWired) {
|
||||
WIDGET_REGISTRY.calendar.component = component;
|
||||
calendarWired = true;
|
||||
@@ -224,11 +350,41 @@ export function wireCalendarWidget(
|
||||
}
|
||||
|
||||
let noteWired = false;
|
||||
export function wireNoteWidget(
|
||||
component: ComponentType<WidgetProps>,
|
||||
) {
|
||||
export function wireNoteWidget(component: ComponentType<WidgetProps>) {
|
||||
if (!noteWired) {
|
||||
WIDGET_REGISTRY.note.component = component;
|
||||
noteWired = true;
|
||||
}
|
||||
}
|
||||
|
||||
let calculatorWired = false;
|
||||
export function wireCalculatorWidget(component: ComponentType<WidgetProps>) {
|
||||
if (!calculatorWired) {
|
||||
WIDGET_REGISTRY.calculator.component = component;
|
||||
calculatorWired = true;
|
||||
}
|
||||
}
|
||||
|
||||
let favoritesWired = false;
|
||||
export function wireFavoritesWidget(component: ComponentType<WidgetProps>) {
|
||||
if (!favoritesWired) {
|
||||
WIDGET_REGISTRY.favorites.component = component;
|
||||
favoritesWired = true;
|
||||
}
|
||||
}
|
||||
|
||||
let linkWired = false;
|
||||
export function wireLinkWidget(component: ComponentType<WidgetProps>) {
|
||||
if (!linkWired) {
|
||||
WIDGET_REGISTRY.link.component = component;
|
||||
linkWired = true;
|
||||
}
|
||||
}
|
||||
|
||||
let stopwatchWired = false;
|
||||
export function wireStopwatchWidget(component: ComponentType<WidgetProps>) {
|
||||
if (!stopwatchWired) {
|
||||
WIDGET_REGISTRY.stopwatch.component = component;
|
||||
stopwatchWired = true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user