From 0799636c78c2361f7e95aa11e012473e18274e4e Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Sat, 28 Feb 2026 23:16:10 -0700 Subject: [PATCH] Right rail template --- app/(dev)/components-preview/page.tsx | 388 +++++++++++------- .../WebVitalsDashboard.container.tsx | 18 +- app/components/asset/Icon.tsx | 48 +++ app/components/asset/icon/exclamation.svg | 3 + app/components/asset/index.ts | 2 + app/components/cards/Card/Card.view.tsx | 4 +- .../controls/SelectInput/SelectInput.view.tsx | 19 +- .../controls/TextArea/TextArea.view.tsx | 1 + .../controls/TextInput/TextInput.view.tsx | 1 + app/components/icons/Avatar.tsx | 5 +- app/components/navigation/Footer.tsx | 2 + .../navigation/TopNav/TopNav.container.tsx | 27 +- .../navigation/TopNav/TopNav.view.tsx | 3 + .../sections/HeroBanner/HeroBanner.tsx | 1 + .../sections/RuleStack/RuleStack.view.tsx | 35 +- app/components/sections/SectionNumber.tsx | 1 + .../type/ContentLockup/ContentLockup.view.tsx | 15 +- .../utility/CardStack/CardStack.container.tsx | 2 + .../utility/CardStack/CardStack.types.ts | 3 + .../utility/CardStack/CardStack.view.tsx | 50 ++- .../CreateFlowTopNav.view.tsx | 2 +- .../DecisionMakingSidebar.container.tsx | 44 ++ .../DecisionMakingSidebar.types.ts | 31 ++ .../DecisionMakingSidebar.view.tsx | 78 ++++ .../utility/DecisionMakingSidebar/index.tsx | 2 + .../InfoMessageBox.container.tsx | 56 +++ .../InfoMessageBox/InfoMessageBox.types.ts | 29 ++ .../InfoMessageBox/InfoMessageBox.view.tsx | 77 ++++ .../utility/InfoMessageBox/index.tsx | 2 + .../utility/InputLabel/InputLabel.view.tsx | 1 + .../utility/ModalHeader/ModalHeader.view.tsx | 1 + app/components/utility/Tag/Tag.view.tsx | 10 +- app/contexts/MessagesContext.tsx | 4 +- app/create/cards/page.tsx | 25 +- app/create/right-rail/page.tsx | 185 +++++++++ app/create/types.ts | 4 +- app/create/upload/page.tsx | 5 +- app/hooks/useMediaQuery.ts | 3 +- lib/i18n/getTranslation.ts | 8 +- tests/components/ContentBanner.test.tsx | 10 +- tests/components/CreateFlowFooter.test.tsx | 4 +- tests/components/CreateFlowTopNav.test.tsx | 4 +- tests/components/HeaderLockup.test.tsx | 12 +- tests/components/Logo.test.tsx | 2 +- tests/components/MultiSelect.test.tsx | 18 +- tests/components/RelatedArticles.test.tsx | 10 +- tests/components/ReviewPage.test.tsx | 8 +- tests/components/TextInput.test.tsx | 6 +- tests/components/Upload.test.tsx | 28 +- tests/e2e/performance.spec.ts | 6 +- tests/e2e/visual-regression.spec.ts | 4 +- tests/pages/cards.test.jsx | 8 +- tests/pages/right-rail.test.jsx | 145 +++++++ tests/pages/user-journey.test.jsx | 8 +- tests/unit/Card.test.jsx | 10 +- tests/unit/CardStack.test.jsx | 29 +- tests/unit/NumberCard.test.jsx | 15 +- tests/unit/RuleCard.test.jsx | 17 +- tests/unit/RuleStack.test.jsx | 8 +- vitest.setup.ts | 13 +- 60 files changed, 1255 insertions(+), 305 deletions(-) create mode 100644 app/components/asset/Icon.tsx create mode 100644 app/components/asset/icon/exclamation.svg create mode 100644 app/components/asset/index.ts create mode 100644 app/components/utility/DecisionMakingSidebar/DecisionMakingSidebar.container.tsx create mode 100644 app/components/utility/DecisionMakingSidebar/DecisionMakingSidebar.types.ts create mode 100644 app/components/utility/DecisionMakingSidebar/DecisionMakingSidebar.view.tsx create mode 100644 app/components/utility/DecisionMakingSidebar/index.tsx create mode 100644 app/components/utility/InfoMessageBox/InfoMessageBox.container.tsx create mode 100644 app/components/utility/InfoMessageBox/InfoMessageBox.types.ts create mode 100644 app/components/utility/InfoMessageBox/InfoMessageBox.view.tsx create mode 100644 app/components/utility/InfoMessageBox/index.tsx create mode 100644 app/create/right-rail/page.tsx create mode 100644 tests/pages/right-rail.test.jsx diff --git a/app/(dev)/components-preview/page.tsx b/app/(dev)/components-preview/page.tsx index acdacd8..f448360 100644 --- a/app/(dev)/components-preview/page.tsx +++ b/app/(dev)/components-preview/page.tsx @@ -8,6 +8,9 @@ import MultiSelect from "../../components/controls/MultiSelect"; import Image from "next/image"; import { getAssetPath } from "../../../lib/assetUtils"; +/** Module-level counter for unique rule card chip IDs (avoids ref in initial state). */ +let ruleCardIdCounter = 0; + interface ChipData { id: string; label: string; @@ -18,7 +21,13 @@ interface ChipData { // MultiSelect example component with state management function MultiSelectExample({ size }: { size: "S" | "M" }) { - const [options, setOptions] = useState>([ + const [options, setOptions] = useState< + Array<{ + id: string; + label: string; + state: "Unselected" | "Selected" | "Custom"; + }> + >([ { id: "1", label: "1 member", state: "Unselected" }, { id: "2", label: "2-10 members", state: "Unselected" }, { id: "3", label: "10-24 members", state: "Unselected" }, @@ -36,17 +45,14 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected", } - : opt - ) + : opt, + ), ); }; const handleAddClick = () => { const newId = `custom-${Date.now()}`; - setOptions((prev) => [ - ...prev, - { id: newId, label: "", state: "Custom" }, - ]); + setOptions((prev) => [...prev, { id: newId, label: "", state: "Custom" }]); }; const handleCustomConfirm = (chipId: string, value: string) => { @@ -54,8 +60,8 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) { prev.map((opt) => opt.id === chipId ? { ...opt, label: value, state: "Selected" as const } - : opt - ) + : opt, + ), ); }; @@ -84,28 +90,52 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) { } export default function ComponentsPreview() { - const [chipStates, setChipStates] = useState>({ + const [chipStates, setChipStates] = useState< + Record + >({ "default-s": "Unselected", "default-m": "Unselected", "inverse-s": "Unselected", "inverse-m": "Unselected", }); - + // Manage custom chips separately const [customChips, setCustomChips] = useState([ - { id: "custom-1", label: "", state: "Custom", palette: "Default", size: "S" }, - { id: "custom-2", label: "", state: "Custom", palette: "Default", size: "M" }, + { + id: "custom-1", + label: "", + state: "Custom", + palette: "Default", + size: "S", + }, + { + id: "custom-2", + label: "", + state: "Custom", + palette: "Default", + size: "M", + }, ]); // RuleCard categories with chip options and state management - const [ruleCardCategories, setRuleCardCategories] = useState; - onChipClick?: (categoryName: string, chipId: string) => void; - onAddClick?: (categoryName: string) => void; - onCustomChipConfirm?: (categoryName: string, chipId: string, value: string) => void; - onCustomChipClose?: (categoryName: string, chipId: string) => void; - }>>([ + const [ruleCardCategories, setRuleCardCategories] = useState< + Array<{ + name: string; + chipOptions: Array<{ + id: string; + label: string; + state: "Unselected" | "Selected" | "Custom"; + }>; + onChipClick?: (_categoryName: string, _chipId: string) => void; + onAddClick?: (_categoryName: string) => void; + onCustomChipConfirm?: ( + _categoryName: string, + _chipId: string, + _value: string, + ) => void; + onCustomChipClose?: (_categoryName: string, _chipId: string) => void; + }> + >([ { name: "Values", chipOptions: [ @@ -125,17 +155,20 @@ export default function ComponentsPreview() { opt.id === chipId ? { ...opt, - state: opt.state === "Selected" ? "Unselected" : "Selected", + state: + opt.state === "Selected" + ? "Unselected" + : "Selected", } - : opt + : opt, ), } - : cat - ) + : cat, + ), ); }, onAddClick: (categoryName: string) => { - const newId = `custom-${categoryName}-${Date.now()}`; + const newId = `custom-${categoryName}-${++ruleCardIdCounter}`; setRuleCardCategories((prev) => prev.map((cat) => cat.name === categoryName @@ -146,11 +179,15 @@ export default function ComponentsPreview() { { id: newId, label: "", state: "Custom" }, ], } - : cat - ) + : cat, + ), ); }, - onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => { + onCustomChipConfirm: ( + categoryName: string, + chipId: string, + value: string, + ) => { setRuleCardCategories((prev) => prev.map((cat) => cat.name === categoryName @@ -159,11 +196,11 @@ export default function ComponentsPreview() { chipOptions: cat.chipOptions.map((opt) => opt.id === chipId ? { ...opt, label: value, state: "Selected" } - : opt + : opt, ), } - : cat - ) + : cat, + ), ); }, onCustomChipClose: (categoryName: string, chipId: string) => { @@ -172,18 +209,18 @@ export default function ComponentsPreview() { cat.name === categoryName ? { ...cat, - chipOptions: cat.chipOptions.filter((opt) => opt.id !== chipId), + chipOptions: cat.chipOptions.filter( + (opt) => opt.id !== chipId, + ), } - : cat - ) + : cat, + ), ); }, }, { name: "Communication", - chipOptions: [ - { id: "comm-1", label: "Signal", state: "Unselected" }, - ], + chipOptions: [{ id: "comm-1", label: "Signal", state: "Unselected" }], onChipClick: (categoryName: string, chipId: string) => { setRuleCardCategories((prev) => prev.map((cat) => @@ -194,17 +231,20 @@ export default function ComponentsPreview() { opt.id === chipId ? { ...opt, - state: opt.state === "Selected" ? "Unselected" : "Selected", + state: + opt.state === "Selected" + ? "Unselected" + : "Selected", } - : opt + : opt, ), } - : cat - ) + : cat, + ), ); }, onAddClick: (categoryName: string) => { - const newId = `custom-${categoryName}-${Date.now()}`; + const newId = `custom-${categoryName}-${++ruleCardIdCounter}`; setRuleCardCategories((prev) => prev.map((cat) => cat.name === categoryName @@ -215,11 +255,15 @@ export default function ComponentsPreview() { { id: newId, label: "", state: "Custom" }, ], } - : cat - ) + : cat, + ), ); }, - onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => { + onCustomChipConfirm: ( + categoryName: string, + chipId: string, + value: string, + ) => { setRuleCardCategories((prev) => prev.map((cat) => cat.name === categoryName @@ -228,11 +272,11 @@ export default function ComponentsPreview() { chipOptions: cat.chipOptions.map((opt) => opt.id === chipId ? { ...opt, label: value, state: "Selected" } - : opt + : opt, ), } - : cat - ) + : cat, + ), ); }, onCustomChipClose: (categoryName: string, chipId: string) => { @@ -241,10 +285,12 @@ export default function ComponentsPreview() { cat.name === categoryName ? { ...cat, - chipOptions: cat.chipOptions.filter((opt) => opt.id !== chipId), + chipOptions: cat.chipOptions.filter( + (opt) => opt.id !== chipId, + ), } - : cat - ) + : cat, + ), ); }, }, @@ -263,17 +309,20 @@ export default function ComponentsPreview() { opt.id === chipId ? { ...opt, - state: opt.state === "Selected" ? "Unselected" : "Selected", + state: + opt.state === "Selected" + ? "Unselected" + : "Selected", } - : opt + : opt, ), } - : cat - ) + : cat, + ), ); }, onAddClick: (categoryName: string) => { - const newId = `custom-${categoryName}-${Date.now()}`; + const newId = `custom-${categoryName}-${++ruleCardIdCounter}`; setRuleCardCategories((prev) => prev.map((cat) => cat.name === categoryName @@ -284,11 +333,15 @@ export default function ComponentsPreview() { { id: newId, label: "", state: "Custom" }, ], } - : cat - ) + : cat, + ), ); }, - onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => { + onCustomChipConfirm: ( + categoryName: string, + chipId: string, + value: string, + ) => { setRuleCardCategories((prev) => prev.map((cat) => cat.name === categoryName @@ -297,11 +350,11 @@ export default function ComponentsPreview() { chipOptions: cat.chipOptions.map((opt) => opt.id === chipId ? { ...opt, label: value, state: "Selected" } - : opt + : opt, ), } - : cat - ) + : cat, + ), ); }, onCustomChipClose: (categoryName: string, chipId: string) => { @@ -310,10 +363,12 @@ export default function ComponentsPreview() { cat.name === categoryName ? { ...cat, - chipOptions: cat.chipOptions.filter((opt) => opt.id !== chipId), + chipOptions: cat.chipOptions.filter( + (opt) => opt.id !== chipId, + ), } - : cat - ) + : cat, + ), ); }, }, @@ -333,17 +388,20 @@ export default function ComponentsPreview() { opt.id === chipId ? { ...opt, - state: opt.state === "Selected" ? "Unselected" : "Selected", + state: + opt.state === "Selected" + ? "Unselected" + : "Selected", } - : opt + : opt, ), } - : cat - ) + : cat, + ), ); }, onAddClick: (categoryName: string) => { - const newId = `custom-${categoryName}-${Date.now()}`; + const newId = `custom-${categoryName}-${++ruleCardIdCounter}`; setRuleCardCategories((prev) => prev.map((cat) => cat.name === categoryName @@ -354,11 +412,15 @@ export default function ComponentsPreview() { { id: newId, label: "", state: "Custom" }, ], } - : cat - ) + : cat, + ), ); }, - onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => { + onCustomChipConfirm: ( + categoryName: string, + chipId: string, + value: string, + ) => { setRuleCardCategories((prev) => prev.map((cat) => cat.name === categoryName @@ -367,11 +429,11 @@ export default function ComponentsPreview() { chipOptions: cat.chipOptions.map((opt) => opt.id === chipId ? { ...opt, label: value, state: "Selected" } - : opt + : opt, ), } - : cat - ) + : cat, + ), ); }, onCustomChipClose: (categoryName: string, chipId: string) => { @@ -380,10 +442,12 @@ export default function ComponentsPreview() { cat.name === categoryName ? { ...cat, - chipOptions: cat.chipOptions.filter((opt) => opt.id !== chipId), + chipOptions: cat.chipOptions.filter( + (opt) => opt.id !== chipId, + ), } - : cat - ) + : cat, + ), ); }, }, @@ -403,17 +467,20 @@ export default function ComponentsPreview() { opt.id === chipId ? { ...opt, - state: opt.state === "Selected" ? "Unselected" : "Selected", + state: + opt.state === "Selected" + ? "Unselected" + : "Selected", } - : opt + : opt, ), } - : cat - ) + : cat, + ), ); }, onAddClick: (categoryName: string) => { - const newId = `custom-${categoryName}-${Date.now()}`; + const newId = `custom-${categoryName}-${++ruleCardIdCounter}`; setRuleCardCategories((prev) => prev.map((cat) => cat.name === categoryName @@ -424,11 +491,15 @@ export default function ComponentsPreview() { { id: newId, label: "", state: "Custom" }, ], } - : cat - ) + : cat, + ), ); }, - onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => { + onCustomChipConfirm: ( + categoryName: string, + chipId: string, + value: string, + ) => { setRuleCardCategories((prev) => prev.map((cat) => cat.name === categoryName @@ -437,11 +508,11 @@ export default function ComponentsPreview() { chipOptions: cat.chipOptions.map((opt) => opt.id === chipId ? { ...opt, label: value, state: "Selected" } - : opt + : opt, ), } - : cat - ) + : cat, + ), ); }, onCustomChipClose: (categoryName: string, chipId: string) => { @@ -450,10 +521,12 @@ export default function ComponentsPreview() { cat.name === categoryName ? { ...cat, - chipOptions: cat.chipOptions.filter((opt) => opt.id !== chipId), + chipOptions: cat.chipOptions.filter( + (opt) => opt.id !== chipId, + ), } - : cat - ) + : cat, + ), ); }, }, @@ -467,7 +540,8 @@ export default function ComponentsPreview() { Component Preview

- RuleCard, Card, and Chip component examples - states, palettes, sizes, and interactions + RuleCard, Card, and Chip component examples - states, palettes, + sizes, and interactions

@@ -481,7 +555,7 @@ export default function ComponentsPreview() {

Default palette -

+
setChipStates((prev) => ({ ...prev, - "default-s": prev["default-s"] === "Selected" ? "Unselected" : "Selected", + "default-s": + prev["default-s"] === "Selected" + ? "Unselected" + : "Selected", })) } /> @@ -503,7 +580,10 @@ export default function ComponentsPreview() { onClick={() => setChipStates((prev) => ({ ...prev, - "default-m": prev["default-m"] === "Selected" ? "Unselected" : "Selected", + "default-m": + prev["default-m"] === "Selected" + ? "Unselected" + : "Selected", })) } /> @@ -528,27 +608,35 @@ export default function ComponentsPreview() { prev.map((c) => c.id === chip.id ? { ...c, label: value, state: "Selected" } - : c - ) + : c, + ), ); }} onClose={(e) => { e.stopPropagation(); - setCustomChips((prev) => prev.filter((c) => c.id !== chip.id)); + setCustomChips((prev) => + prev.filter((c) => c.id !== chip.id), + ); }} onClick={(e) => { e.stopPropagation(); // Only toggle if the chip is in Selected or Unselected state (not Custom) - if (chip.state === "Selected" || chip.state === "Unselected") { + if ( + chip.state === "Selected" || + chip.state === "Unselected" + ) { setCustomChips((prev) => prev.map((c) => c.id === chip.id ? { ...c, - state: c.state === "Selected" ? "Unselected" : "Selected", + state: + c.state === "Selected" + ? "Unselected" + : "Selected", } - : c - ) + : c, + ), ); } }} @@ -561,7 +649,13 @@ export default function ComponentsPreview() { const newId = `custom-${Date.now()}`; setCustomChips((prev) => [ ...prev, - { id: newId, label: "", state: "Custom", palette: "Default", size: "S" }, + { + id: newId, + label: "", + state: "Custom", + palette: "Default", + size: "S", + }, ]); }} className="flex gap-[var(--measures-spacing-050,2px)] items-center justify-center p-[var(--measures-spacing-200,8px)] rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity" @@ -592,11 +686,14 @@ export default function ComponentsPreview() {
{/* Inverse palette - on white background */} -
+

Inverse palette (on white background)

-
+
setChipStates((prev) => ({ ...prev, - "inverse-s": prev["inverse-s"] === "Selected" ? "Unselected" : "Selected", + "inverse-s": + prev["inverse-s"] === "Selected" + ? "Unselected" + : "Selected", })) } /> @@ -618,7 +718,10 @@ export default function ComponentsPreview() { onClick={() => setChipStates((prev) => ({ ...prev, - "inverse-m": prev["inverse-m"] === "Selected" ? "Unselected" : "Selected", + "inverse-m": + prev["inverse-m"] === "Selected" + ? "Unselected" + : "Selected", })) } /> @@ -641,7 +744,8 @@ export default function ComponentsPreview() {

- Horizontal and vertical orientations with recommended and selected states. + Horizontal and vertical orientations with recommended and selected + states.

@@ -654,7 +758,7 @@ export default function ComponentsPreview() { recommended={true} selected={false} orientation="horizontal" - onClick={() => console.log("Card clicked")} + onClick={() => console.warn("Card clicked")} />
@@ -667,7 +771,7 @@ export default function ComponentsPreview() { recommended={false} selected={true} orientation="horizontal" - onClick={() => console.log("Card clicked")} + onClick={() => console.warn("Card clicked")} />
@@ -681,7 +785,7 @@ export default function ComponentsPreview() { selected={false} orientation="vertical" showInfoIcon={true} - onClick={() => console.log("Card clicked")} + onClick={() => console.warn("Card clicked")} />
@@ -695,7 +799,7 @@ export default function ComponentsPreview() { selected={true} orientation="vertical" showInfoIcon={true} - onClick={() => console.log("Card clicked")} + onClick={() => console.warn("Card clicked")} />
@@ -717,9 +821,9 @@ export default function ComponentsPreview() { className="w-[525px]" logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png" logoAlt="Mutual Aid Mondays" - onClick={() => console.log("Card clicked: Mutual Aid Mondays")} - /> -
+ onClick={() => console.warn("Card clicked: Mutual Aid Mondays")} + /> +
{/* Collapsed State - Medium */} @@ -737,17 +841,17 @@ export default function ComponentsPreview() { className="w-[289px]" logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png" logoAlt="Mutual Aid Mondays" - onClick={() => console.log("Card clicked: Mutual Aid Mondays")} - /> + onClick={() => console.warn("Card clicked: Mutual Aid Mondays")} + />
{/* Expanded State - Large */} -
-

+
+

Expanded State - Large (L) -

-
+

+
console.log("Card clicked: Mutual Aid Mondays")} - /> -
+ onClick={() => console.warn("Card clicked: Mutual Aid Mondays")} + /> +
{/* Expanded State - Medium */} @@ -779,16 +883,16 @@ export default function ComponentsPreview() { logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png" logoAlt="Mutual Aid Mondays" categories={ruleCardCategories} - onClick={() => console.log("Card clicked: Mutual Aid Mondays")} + onClick={() => console.warn("Card clicked: Mutual Aid Mondays")} /> -
- +
+ {/* Different Background Colors */} -
-

+
+

Different Background Colors -

+

} - onClick={() => console.log("Consensus clusters selected")} + onClick={() => console.warn("Consensus clusters selected")} /> } - onClick={() => console.log("Consensus selected")} - /> -
-
+ onClick={() => console.warn("Consensus selected")} + /> +
+ {/* Logo Fallback */} @@ -843,9 +947,9 @@ export default function ComponentsPreview() { size="L" className="w-[525px]" communityInitials="CE" - onClick={() => console.log("Community Example selected")} - /> - + onClick={() => console.warn("Community Example selected")} + /> + {/* MultiSelect Component */} @@ -856,7 +960,7 @@ export default function ComponentsPreview() {
{/* Small size */} - + {/* Medium size */}
diff --git a/app/components/WebVitalsDashboard/WebVitalsDashboard.container.tsx b/app/components/WebVitalsDashboard/WebVitalsDashboard.container.tsx index 0b9ea0c..dafa943 100644 --- a/app/components/WebVitalsDashboard/WebVitalsDashboard.container.tsx +++ b/app/components/WebVitalsDashboard/WebVitalsDashboard.container.tsx @@ -40,7 +40,23 @@ const WebVitalsDashboardContainer = memo(() => { if (typeof window !== "undefined") { import("web-vitals").then((webVitals) => { - const { getCLS, getFID, getFCP, getLCP, getTTFB } = webVitals as any; + const { getCLS, getFID, getFCP, getLCP, getTTFB } = webVitals as { + getCLS: ( + _fn: (_m: { value: number; rating: string }) => void, + ) => void; + getFID: ( + _fn: (_m: { value: number; rating: string }) => void, + ) => void; + getFCP: ( + _fn: (_m: { value: number; rating: string }) => void, + ) => void; + getLCP: ( + _fn: (_m: { value: number; rating: string }) => void, + ) => void; + getTTFB: ( + _fn: (_m: { value: number; rating: string }) => void, + ) => void; + }; getLCP((metric: { value: number; rating: VitalData["rating"] }) => { setVitals((prev) => ({ diff --git a/app/components/asset/Icon.tsx b/app/components/asset/Icon.tsx new file mode 100644 index 0000000..105424b --- /dev/null +++ b/app/components/asset/Icon.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { memo } from "react"; +import ExclamationIcon from "./icon/exclamation.svg"; + +export type IconName = "exclamation"; + +/** SVG import may be a React component or a module object { default: Component } (e.g. with Turbopack) */ +const iconMap: Record< + IconName, + React.ComponentType> | { default: React.ComponentType> } +> = { + exclamation: ExclamationIcon, +}; + +export interface IconProps { + name: IconName; + className?: string; + /** Width and height (default 24) */ + size?: number; + "aria-hidden"?: boolean; +} + +function IconComponent({ + name, + className = "", + size = 24, + "aria-hidden": ariaHidden = true, +}: IconProps) { + const SvgModule = iconMap[name]; + if (!SvgModule) return null; + // Turbopack/bundler may expose SVG as { default: Component } instead of the component directly + const Svg = + typeof SvgModule === "object" && SvgModule !== null && "default" in SvgModule + ? (SvgModule as { default: React.ComponentType> }).default + : (SvgModule as React.ComponentType>); + if (typeof Svg !== "function") return null; + return ( + + ); +} + +export default memo(IconComponent); diff --git a/app/components/asset/icon/exclamation.svg b/app/components/asset/icon/exclamation.svg new file mode 100644 index 0000000..ec01856 --- /dev/null +++ b/app/components/asset/icon/exclamation.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/components/asset/index.ts b/app/components/asset/index.ts new file mode 100644 index 0000000..b97923c --- /dev/null +++ b/app/components/asset/index.ts @@ -0,0 +1,2 @@ +export { default as Icon } from "./Icon"; +export type { IconName, IconProps } from "./Icon"; diff --git a/app/components/cards/Card/Card.view.tsx b/app/components/cards/Card/Card.view.tsx index 3ddcf80..e9ba02d 100644 --- a/app/components/cards/Card/Card.view.tsx +++ b/app/components/cards/Card/Card.view.tsx @@ -42,7 +42,7 @@ export function CardView({ const selectedBorder = selected ? "outline outline-2 outline-dashed outline-black outline-offset-[-2px]" : ""; - const baseClasses = `rounded-[var(--radius-measures-radius-small)] bg-[#FFFFFF] p-4 transition-all duration-200 cursor-pointer ${borderClass} ${selectedBorder} ${className}`; + const baseClasses = `select-none rounded-[var(--radius-measures-radius-small)] bg-[#FFFFFF] p-4 transition-[border-color,box-shadow,outline] duration-200 cursor-pointer ${borderClass} ${selectedBorder} ${className}`; if (orientation === "horizontal") { return ( @@ -93,7 +93,7 @@ export function CardView({

) : null} -
+
diff --git a/app/components/controls/SelectInput/SelectInput.view.tsx b/app/components/controls/SelectInput/SelectInput.view.tsx index c0ed884..2e3bad2 100644 --- a/app/components/controls/SelectInput/SelectInput.view.tsx +++ b/app/components/controls/SelectInput/SelectInput.view.tsx @@ -75,7 +75,8 @@ export function SelectInputView({ }: SelectInputViewProps) { // Styles based on Figma design const containerClasses = "flex flex-col gap-[8px]"; - const labelClasses = "text-[14px] leading-[20px] font-medium font-inter text-[var(--color-content-default-primary)]"; + const labelClasses = + "text-[14px] leading-[20px] font-medium font-inter text-[var(--color-content-default-primary)]"; // Button styles per Figma const getButtonClasses = (): string => { @@ -101,7 +102,9 @@ export function SelectInputView({ cursor-pointer appearance-none m-0 - `.trim().replace(/\s+/g, " "); + ` + .trim() + .replace(/\s+/g, " "); if (disabled) { return `${baseClasses} bg-[var(--color-surface-default-secondary)] text-[var(--color-content-inverse-tertiary,#2d2d2d)] border-[var(--color-border-default-primary)] cursor-not-allowed opacity-40`; @@ -142,10 +145,7 @@ export function SelectInputView({ {label && (
-