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 && (
-
+
{label}
{asterisk && (
@@ -155,6 +155,7 @@ export function SelectInputView({
)}
{iconHelp && (
+ {/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
-
- {textData ? displayText : placeholder}
+
+ {textData ? displayText : _placeholder}
{iconRight && (
diff --git a/app/components/controls/TextArea/TextArea.view.tsx b/app/components/controls/TextArea/TextArea.view.tsx
index b7b1fc4..cf8783c 100644
--- a/app/components/controls/TextArea/TextArea.view.tsx
+++ b/app/components/controls/TextArea/TextArea.view.tsx
@@ -48,6 +48,7 @@ export const TextAreaView = forwardRef
(
{showHelpIcon && (
+ {/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
(
{showHelpIcon && (
+ {/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
(
const baseStyles = `rounded-[var(--radius-measures-radius-full)] object-cover box-border ${sizeStyles[size]} ${className}`;
- return
;
+ return (
+ /* eslint-disable-next-line @next/next/no-img-element -- avatar image from URL */
+
+ );
},
);
diff --git a/app/components/navigation/Footer.tsx b/app/components/navigation/Footer.tsx
index fd7eb24..c9865cf 100644
--- a/app/components/navigation/Footer.tsx
+++ b/app/components/navigation/Footer.tsx
@@ -66,6 +66,7 @@ const Footer = memo(() => {
className="flex items-center gap-[var(--spacing-measures-spacing-06,6px)] hover:opacity-80 active:opacity-60 focus:opacity-80 focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-primary)] transition-opacity p-2 -m-2 cursor-pointer group"
aria-label={t("social.bluesky.ariaLabel")}
>
+ {/* eslint-disable-next-line @next/next/no-img-element -- social logo */}
{
className="flex items-center gap-[var(--spacing-measures-spacing-06,6px)] hover:opacity-80 active:opacity-60 focus:opacity-80 focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-primary)] transition-opacity p-2 -m-2 cursor-pointer group"
aria-label={t("social.gitlab.ariaLabel")}
>
+ {/* eslint-disable-next-line @next/next/no-img-element -- social icon */}
(
- ({
- folderTop = false,
- loggedIn = false,
- profile = false,
- logIn = true,
- }) => {
+ ({ folderTop = false, loggedIn = false, profile = false, logIn = true }) => {
const pathname = usePathname();
const t = useTranslation("header");
@@ -34,7 +28,9 @@ const TopNavContainer = memo
(
"@type": "WebSite",
name: "CommunityRule",
url: "https://communityrule.com",
- ...(folderTop && { description: "Build operating manuals for successful communities" }),
+ ...(folderTop && {
+ description: "Build operating manuals for successful communities",
+ }),
potentialAction: {
"@type": "SearchAction",
target: "https://communityrule.com/search?q={search_term_string}",
@@ -54,7 +50,10 @@ const TopNavContainer = memo(
const renderNavigationItems = (size: NavSize) => {
// Map NavSize to Figma MenuBarItem sizes
- const sizeMap: Record = {
+ const sizeMap: Record<
+ NavSize,
+ "X Small" | "Small" | "Medium" | "Large" | "X Large"
+ > = {
default: "Small",
xsmall: "X Small",
xsmallUseCases: "X Small",
@@ -85,7 +84,10 @@ const TopNavContainer = memo(
mode={mode}
state={pathname === item.href ? "selected" : "default"}
reducedPadding={isUseCases}
- ariaLabel={t("ariaLabels.navigateToPage").replace("{text}", item.text)}
+ ariaLabel={t("ariaLabels.navigateToPage").replace(
+ "{text}",
+ item.text,
+ )}
>
{item.text}
@@ -113,7 +115,10 @@ const TopNavContainer = memo(
const renderLoginButton = (size: NavSize) => {
// Map NavSize to Figma MenuBarItem sizes
- const sizeMap: Record = {
+ const sizeMap: Record<
+ NavSize,
+ "X Small" | "Small" | "Medium" | "Large" | "X Large"
+ > = {
default: "Small",
xsmall: "X Small",
xsmallUseCases: "X Small",
diff --git a/app/components/navigation/TopNav/TopNav.view.tsx b/app/components/navigation/TopNav/TopNav.view.tsx
index 2c39212..013ec8d 100644
--- a/app/components/navigation/TopNav/TopNav.view.tsx
+++ b/app/components/navigation/TopNav/TopNav.view.tsx
@@ -55,18 +55,21 @@ function TopNavView({
{/* Decorative Union images for tab appearance */}
+ {/* eslint-disable-next-line @next/next/no-img-element -- decorative SVG, not content */}
+ {/* eslint-disable-next-line @next/next/no-img-element -- decorative SVG */}
+ {/* eslint-disable-next-line @next/next/no-img-element -- decorative SVG */}
(
{/* Hero Image Container */}
+ {/* eslint-disable-next-line @next/next/no-img-element -- dynamic path from getAssetPath */}
{
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer breakpoint until after mount to avoid hydration mismatch
setIsMounted(true);
}, []);
@@ -38,12 +43,12 @@ export function RuleStackView({
? isMax639
? "XS"
: isMin640Max1023
- ? "S"
- : isMin1024Max1439
- ? "M"
- : isMin1440
- ? "L"
- : "M"
+ ? "S"
+ : isMin1024Max1439
+ ? "M"
+ : isMin1440
+ ? "L"
+ : "M"
: "M";
// Icon sizes: XS=40px, S=56px, M=56px, L=90px
@@ -150,17 +155,15 @@ export function RuleStackView({
{/* See all templates button */}
-
-
+ "
+ >
+
{buttonText}
diff --git a/app/components/sections/SectionNumber.tsx b/app/components/sections/SectionNumber.tsx
index 0595c48..5c687e1 100644
--- a/app/components/sections/SectionNumber.tsx
+++ b/app/components/sections/SectionNumber.tsx
@@ -26,6 +26,7 @@ const SectionNumber = memo
(({ number }) => {
return (
+ {/* eslint-disable-next-line @next/next/no-img-element -- dynamic src from getImageSrc */}
) : null}
{variant === "hero" && (
-
+ <>
+ {/* eslint-disable-next-line @next/next/no-img-element -- decorative shape SVG */}
+
+ >
)}
diff --git a/app/components/utility/CardStack/CardStack.container.tsx b/app/components/utility/CardStack/CardStack.container.tsx
index 7624537..5626ea8 100644
--- a/app/components/utility/CardStack/CardStack.container.tsx
+++ b/app/components/utility/CardStack/CardStack.container.tsx
@@ -20,6 +20,7 @@ const CardStackContainer = memo(
showLessLabel = DEFAULT_SHOW_LESS_LABEL,
title = "",
description = "",
+ layout = "default",
className = "",
}) => {
const [internalExpanded, setInternalExpanded] = useState(false);
@@ -68,6 +69,7 @@ const CardStackContainer = memo(
showLessLabel={showLessLabel}
title={title}
description={description}
+ layout={layout}
className={className}
/>
);
diff --git a/app/components/utility/CardStack/CardStack.types.ts b/app/components/utility/CardStack/CardStack.types.ts
index 7dfe180..2c23324 100644
--- a/app/components/utility/CardStack/CardStack.types.ts
+++ b/app/components/utility/CardStack/CardStack.types.ts
@@ -17,6 +17,8 @@ export interface CardStackProps {
showLessLabel?: string;
title?: string;
description?: string;
+ /** "default" = compact grid/column + expanded grid; "singleStack" = always one column, expand shows more in same stack */
+ layout?: "default" | "singleStack";
className?: string;
}
@@ -31,5 +33,6 @@ export interface CardStackViewProps {
showLessLabel: string;
title: string;
description: string;
+ layout: "default" | "singleStack";
className: string;
}
diff --git a/app/components/utility/CardStack/CardStack.view.tsx b/app/components/utility/CardStack/CardStack.view.tsx
index 23eb8de..34af000 100644
--- a/app/components/utility/CardStack/CardStack.view.tsx
+++ b/app/components/utility/CardStack/CardStack.view.tsx
@@ -15,17 +15,59 @@ export function CardStackView({
showLessLabel,
title,
description,
+ layout,
className,
}: CardStackViewProps) {
const isSelected = (id: string) => selectedIds.includes(id);
// Compact: recommended only (up to 5). Expanded: all cards.
- const compactCards = cards
- .filter((c) => c.recommended ?? false)
- .slice(0, 5);
+ const compactCards = cards.filter((c) => c.recommended ?? false).slice(0, 5);
+
+ // Single stack: always one column; expand reveals more in same stack (scrollable)
+ if (layout === "singleStack") {
+ const displayedCards = expanded ? cards : compactCards;
+ return (
+
+ {title || description ? (
+
+
+
+ ) : null}
+
+ {displayedCards.map((item) => (
+ onCardSelect(item.id)}
+ />
+ ))}
+
+ {hasMore ? (
+
+ {expanded ? showLessLabel : toggleLabel}
+
+ ) : null}
+
+ );
+ }
return (
- {(title || description) ? (
+ {title || description ? (
diff --git a/app/components/utility/DecisionMakingSidebar/DecisionMakingSidebar.container.tsx b/app/components/utility/DecisionMakingSidebar/DecisionMakingSidebar.container.tsx
new file mode 100644
index 0000000..74c687c
--- /dev/null
+++ b/app/components/utility/DecisionMakingSidebar/DecisionMakingSidebar.container.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import { memo } from "react";
+import DecisionMakingSidebarView from "./DecisionMakingSidebar.view";
+import type { DecisionMakingSidebarProps } from "./DecisionMakingSidebar.types";
+import {
+ normalizeHeaderLockupJustification,
+ normalizeHeaderLockupSize,
+} from "../../../../lib/propNormalization";
+
+const DecisionMakingSidebarContainer = memo(
+ ({
+ title,
+ description,
+ messageBoxTitle,
+ messageBoxItems,
+ messageBoxCheckedIds,
+ onMessageBoxCheckboxChange,
+ size: sizeProp = "L",
+ justification: justificationProp = "left",
+ className = "",
+ }) => {
+ const size = normalizeHeaderLockupSize(sizeProp);
+ const justification = normalizeHeaderLockupJustification(justificationProp);
+
+ return (
+
+ );
+ },
+);
+
+DecisionMakingSidebarContainer.displayName = "DecisionMakingSidebar";
+
+export default DecisionMakingSidebarContainer;
diff --git a/app/components/utility/DecisionMakingSidebar/DecisionMakingSidebar.types.ts b/app/components/utility/DecisionMakingSidebar/DecisionMakingSidebar.types.ts
new file mode 100644
index 0000000..8579770
--- /dev/null
+++ b/app/components/utility/DecisionMakingSidebar/DecisionMakingSidebar.types.ts
@@ -0,0 +1,31 @@
+import type { ReactNode } from "react";
+import type {
+ HeaderLockupJustificationValue,
+ HeaderLockupSizeValue,
+} from "../../type/HeaderLockup/HeaderLockup.types";
+import type { InfoMessageBoxItem } from "../InfoMessageBox/InfoMessageBox.types";
+
+export interface DecisionMakingSidebarProps {
+ title: string;
+ /** Description text or ReactNode (e.g. with underlined "add") */
+ description?: string | ReactNode;
+ messageBoxTitle: string;
+ messageBoxItems: InfoMessageBoxItem[];
+ messageBoxCheckedIds?: string[];
+ onMessageBoxCheckboxChange?: (id: string, checked: boolean) => void;
+ size?: HeaderLockupSizeValue;
+ justification?: HeaderLockupJustificationValue;
+ className?: string;
+}
+
+export interface DecisionMakingSidebarViewProps {
+ title: string;
+ description: string | ReactNode | undefined;
+ messageBoxTitle: string;
+ messageBoxItems: InfoMessageBoxItem[];
+ messageBoxCheckedIds: string[] | undefined;
+ onMessageBoxCheckboxChange: ((id: string, checked: boolean) => void) | undefined;
+ size: "L" | "M";
+ justification: "left" | "center";
+ className: string;
+}
diff --git a/app/components/utility/DecisionMakingSidebar/DecisionMakingSidebar.view.tsx b/app/components/utility/DecisionMakingSidebar/DecisionMakingSidebar.view.tsx
new file mode 100644
index 0000000..3012e4b
--- /dev/null
+++ b/app/components/utility/DecisionMakingSidebar/DecisionMakingSidebar.view.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import { memo } from "react";
+import HeaderLockup from "../../type/HeaderLockup";
+import InfoMessageBox from "../InfoMessageBox";
+import type { DecisionMakingSidebarViewProps } from "./DecisionMakingSidebar.types";
+
+function DecisionMakingSidebarView({
+ title,
+ description,
+ messageBoxTitle,
+ messageBoxItems,
+ messageBoxCheckedIds,
+ onMessageBoxCheckboxChange,
+ size,
+ justification,
+ className,
+}: DecisionMakingSidebarViewProps) {
+ const isL = size === "L";
+ const isLeft = justification === "left";
+ const isStringDescription = typeof description === "string";
+
+ return (
+
+ {isStringDescription ? (
+
+ ) : (
+
+
+
+ {title}
+
+
+ {description != null && (
+
+ {description}
+
+ )}
+
+ )}
+
+
+ );
+}
+
+DecisionMakingSidebarView.displayName = "DecisionMakingSidebarView";
+
+export default memo(DecisionMakingSidebarView);
diff --git a/app/components/utility/DecisionMakingSidebar/index.tsx b/app/components/utility/DecisionMakingSidebar/index.tsx
new file mode 100644
index 0000000..30e82d5
--- /dev/null
+++ b/app/components/utility/DecisionMakingSidebar/index.tsx
@@ -0,0 +1,2 @@
+export { default } from "./DecisionMakingSidebar.container";
+export type { DecisionMakingSidebarProps } from "./DecisionMakingSidebar.types";
diff --git a/app/components/utility/InfoMessageBox/InfoMessageBox.container.tsx b/app/components/utility/InfoMessageBox/InfoMessageBox.container.tsx
new file mode 100644
index 0000000..763d8ee
--- /dev/null
+++ b/app/components/utility/InfoMessageBox/InfoMessageBox.container.tsx
@@ -0,0 +1,56 @@
+"use client";
+
+import { memo, useCallback, useState } from "react";
+import InfoMessageBoxView from "./InfoMessageBox.view";
+import type { InfoMessageBoxProps } from "./InfoMessageBox.types";
+
+const InfoMessageBoxContainer = memo(
+ ({
+ title,
+ items,
+ icon,
+ checkedIds: controlledCheckedIds,
+ onCheckboxChange,
+ className = "",
+ }) => {
+ const [internalCheckedIds, setInternalCheckedIds] = useState([]);
+ const checkedIds =
+ controlledCheckedIds !== undefined
+ ? controlledCheckedIds
+ : internalCheckedIds;
+
+ const handleGroupChange = useCallback(
+ (newValue: string[]) => {
+ if (controlledCheckedIds === undefined) {
+ setInternalCheckedIds(newValue);
+ }
+ if (!onCheckboxChange) return;
+ const prevSet = new Set(checkedIds);
+ const newSet = new Set(newValue);
+ items.forEach((item) => {
+ const nowChecked = newSet.has(item.id);
+ const wasChecked = prevSet.has(item.id);
+ if (nowChecked !== wasChecked) {
+ onCheckboxChange(item.id, nowChecked);
+ }
+ });
+ },
+ [checkedIds, controlledCheckedIds, items, onCheckboxChange],
+ );
+
+ return (
+
+ );
+ },
+);
+
+InfoMessageBoxContainer.displayName = "InfoMessageBox";
+
+export default InfoMessageBoxContainer;
diff --git a/app/components/utility/InfoMessageBox/InfoMessageBox.types.ts b/app/components/utility/InfoMessageBox/InfoMessageBox.types.ts
new file mode 100644
index 0000000..1a60054
--- /dev/null
+++ b/app/components/utility/InfoMessageBox/InfoMessageBox.types.ts
@@ -0,0 +1,29 @@
+import type { ReactNode } from "react";
+
+export interface InfoMessageBoxItem {
+ id: string;
+ label: string;
+}
+
+export interface InfoMessageBoxProps {
+ /** Heading text for the message box */
+ title: string;
+ /** Checkbox items (id used as value for CheckboxGroup) */
+ items: InfoMessageBoxItem[];
+ /** Optional icon (e.g. exclamation); default exclamation icon used if not provided */
+ icon?: ReactNode;
+ /** Controlled checked ids; if undefined, uncontrolled */
+ checkedIds?: string[];
+ /** Callback when a checkbox is toggled */
+ onCheckboxChange?: (id: string, checked: boolean) => void;
+ className?: string;
+}
+
+export interface InfoMessageBoxViewProps {
+ title: string;
+ items: InfoMessageBoxItem[];
+ icon?: ReactNode;
+ checkedIds: string[];
+ onGroupChange: (value: string[]) => void;
+ className: string;
+}
diff --git a/app/components/utility/InfoMessageBox/InfoMessageBox.view.tsx b/app/components/utility/InfoMessageBox/InfoMessageBox.view.tsx
new file mode 100644
index 0000000..e2e44ff
--- /dev/null
+++ b/app/components/utility/InfoMessageBox/InfoMessageBox.view.tsx
@@ -0,0 +1,77 @@
+"use client";
+
+import { memo } from "react";
+import CheckboxGroup from "../../controls/CheckboxGroup";
+import type { InfoMessageBoxViewProps } from "./InfoMessageBox.types";
+
+/** Exclamation icon per Figma 19751:35053 – vertical bar + dot inside circle; circle bg white 10% opacity, no border */
+function ExclamationIconInline() {
+ const fillColor = "var(--color-content-default-primary, white)";
+ return (
+
+
+
+
+ );
+}
+
+function InfoMessageBoxView({
+ title,
+ items,
+ icon,
+ checkedIds,
+ onGroupChange,
+ className,
+}: InfoMessageBoxViewProps) {
+ const options = items.map((item) => ({
+ value: item.id,
+ label: item.label,
+ }));
+
+ const handleChange = (data: { value: string[] }) => {
+ onGroupChange(data.value);
+ };
+
+ return (
+
+
+
+ {icon ?? }
+
+
+ {title}
+
+
+
+
+
+
+ );
+}
+
+export default memo(InfoMessageBoxView);
diff --git a/app/components/utility/InfoMessageBox/index.tsx b/app/components/utility/InfoMessageBox/index.tsx
new file mode 100644
index 0000000..881390d
--- /dev/null
+++ b/app/components/utility/InfoMessageBox/index.tsx
@@ -0,0 +1,2 @@
+export { default } from "./InfoMessageBox.container";
+export type { InfoMessageBoxProps, InfoMessageBoxItem } from "./InfoMessageBox.types";
diff --git a/app/components/utility/InputLabel/InputLabel.view.tsx b/app/components/utility/InputLabel/InputLabel.view.tsx
index 79fe5c4..632c8e9 100644
--- a/app/components/utility/InputLabel/InputLabel.view.tsx
+++ b/app/components/utility/InputLabel/InputLabel.view.tsx
@@ -75,6 +75,7 @@ function InputLabelView({
{helpIcon && (
+ {/* eslint-disable-next-line @next/next/no-img-element -- icon from asset path */}
+ {/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
{children}
diff --git a/app/contexts/MessagesContext.tsx b/app/contexts/MessagesContext.tsx
index 92e4fc7..cb17c37 100644
--- a/app/contexts/MessagesContext.tsx
+++ b/app/contexts/MessagesContext.tsx
@@ -39,11 +39,11 @@ export function useMessages(): Messages {
*/
function getTranslationValue(messages: Messages, key: string): string {
const keys = key.split(".");
- let value: any = messages;
+ let value: unknown = messages;
for (const k of keys) {
if (value && typeof value === "object" && k in value) {
- value = value[k as keyof typeof value];
+ value = (value as Record
)[k];
} else {
return key; // Fallback to key if path not found
}
diff --git a/app/create/cards/page.tsx b/app/create/cards/page.tsx
index 2814345..184ee80 100644
--- a/app/create/cards/page.tsx
+++ b/app/create/cards/page.tsx
@@ -52,8 +52,7 @@ const ADD_PLATFORM_MODALS: Record<
},
[VIDEO_MEETINGS_CARD_ID]: {
title: "Video Meetings",
- description:
- "Synchronous video calls for remote face-to-face interaction.",
+ description: "Synchronous video calls for remote face-to-face interaction.",
nextButtonText: "Add Platform",
},
};
@@ -93,12 +92,12 @@ const ADD_PLATFORM_SECTION_DEFAULTS: Record<
*/
function CreateModalSection({
title,
- value,
+ value: _value,
onChange,
}: {
title: string;
value: string;
- onChange: (value: string) => void;
+ onChange: (_value: string) => void;
}) {
return (
@@ -115,7 +114,7 @@ function CreateModalSection({