Merge pull request 'adilallo/feature/PageTemplateImplementations' (#42) from adilallo/feature/PageTemplateImplementations into main
Reviewed-on: #42
This commit was merged in pull request #42.
This commit is contained in:
@@ -2,7 +2,7 @@ name: CI Pipeline
|
||||
run-name: "${{ gitea.actor }} triggered CI pipeline"
|
||||
|
||||
on:
|
||||
workflow_dispatch: {} # Manual trigger only - run tests locally before merging
|
||||
workflow_dispatch: {} # Manual trigger only - run tests locally before merging
|
||||
# Auto-runs disabled for solo development
|
||||
# Re-enable when ready for collaborators:
|
||||
# pull_request:
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import RuleCard from "../components/cards/RuleCard";
|
||||
import Chip from "../components/controls/Chip";
|
||||
import MultiSelect from "../components/controls/MultiSelect";
|
||||
import RuleCard from "../../components/cards/RuleCard";
|
||||
import Card from "../../components/cards/Card";
|
||||
import Chip from "../../components/controls/Chip";
|
||||
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;
|
||||
@@ -17,7 +21,13 @@ interface ChipData {
|
||||
|
||||
// MultiSelect example component with state management
|
||||
function MultiSelectExample({ size }: { size: "S" | "M" }) {
|
||||
const [options, setOptions] = useState<Array<{ id: string; label: string; state: "Unselected" | "Selected" | "Custom" }>>([
|
||||
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" },
|
||||
@@ -35,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) => {
|
||||
@@ -53,8 +60,8 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) {
|
||||
prev.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, label: value, state: "Selected" as const }
|
||||
: opt
|
||||
)
|
||||
: opt,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -83,28 +90,52 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) {
|
||||
}
|
||||
|
||||
export default function ComponentsPreview() {
|
||||
const [chipStates, setChipStates] = useState<Record<string, "Unselected" | "Selected">>({
|
||||
const [chipStates, setChipStates] = useState<
|
||||
Record<string, "Unselected" | "Selected">
|
||||
>({
|
||||
"default-s": "Unselected",
|
||||
"default-m": "Unselected",
|
||||
"inverse-s": "Unselected",
|
||||
"inverse-m": "Unselected",
|
||||
});
|
||||
|
||||
|
||||
// Manage custom chips separately
|
||||
const [customChips, setCustomChips] = useState<ChipData[]>([
|
||||
{ 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<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;
|
||||
}>>([
|
||||
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: [
|
||||
@@ -124,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
|
||||
@@ -145,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
|
||||
@@ -158,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) => {
|
||||
@@ -171,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) =>
|
||||
@@ -193,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
|
||||
@@ -214,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
|
||||
@@ -227,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) => {
|
||||
@@ -240,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,
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -262,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
|
||||
@@ -283,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
|
||||
@@ -296,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) => {
|
||||
@@ -309,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,
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -332,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
|
||||
@@ -353,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
|
||||
@@ -366,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) => {
|
||||
@@ -379,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,
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -402,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
|
||||
@@ -423,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
|
||||
@@ -436,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) => {
|
||||
@@ -449,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,
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -466,7 +540,8 @@ export default function ComponentsPreview() {
|
||||
Component Preview
|
||||
</h1>
|
||||
<p className="font-inter text-[18px] leading-[24px] text-[var(--color-content-default-secondary)]">
|
||||
RuleCard and Chip component examples - states, palettes, sizes, and interactions
|
||||
RuleCard, Card, and Chip component examples - states, palettes,
|
||||
sizes, and interactions
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -480,7 +555,7 @@ export default function ComponentsPreview() {
|
||||
<div className="space-y-[var(--spacing-scale-016)]">
|
||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
|
||||
Default palette
|
||||
</h3>
|
||||
</h3>
|
||||
<div className="flex flex-wrap items-center gap-[var(--spacing-scale-016)]">
|
||||
<Chip
|
||||
label="Small"
|
||||
@@ -490,7 +565,10 @@ export default function ComponentsPreview() {
|
||||
onClick={() =>
|
||||
setChipStates((prev) => ({
|
||||
...prev,
|
||||
"default-s": prev["default-s"] === "Selected" ? "Unselected" : "Selected",
|
||||
"default-s":
|
||||
prev["default-s"] === "Selected"
|
||||
? "Unselected"
|
||||
: "Selected",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
@@ -502,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",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
@@ -527,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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
@@ -560,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"
|
||||
@@ -591,11 +686,14 @@ export default function ComponentsPreview() {
|
||||
</div>
|
||||
|
||||
{/* Inverse palette - on white background */}
|
||||
<div className="space-y-[var(--spacing-scale-016)]">
|
||||
<div className="space-y-[var(--spacing-scale-016)]">
|
||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
|
||||
Inverse palette (on white background)
|
||||
</h3>
|
||||
<div className="!bg-white p-[var(--spacing-scale-032)] rounded-[var(--radius-300,12px)]" style={{ backgroundColor: '#ffffff' }}>
|
||||
<div
|
||||
className="!bg-white p-[var(--spacing-scale-032)] rounded-[var(--radius-300,12px)]"
|
||||
style={{ backgroundColor: "#ffffff" }}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-[var(--spacing-scale-016)]">
|
||||
<Chip
|
||||
label="Small"
|
||||
@@ -605,7 +703,10 @@ export default function ComponentsPreview() {
|
||||
onClick={() =>
|
||||
setChipStates((prev) => ({
|
||||
...prev,
|
||||
"inverse-s": prev["inverse-s"] === "Selected" ? "Unselected" : "Selected",
|
||||
"inverse-s":
|
||||
prev["inverse-s"] === "Selected"
|
||||
? "Unselected"
|
||||
: "Selected",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
@@ -617,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",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
@@ -633,6 +737,75 @@ export default function ComponentsPreview() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Card Component - Create flow selection card variants */}
|
||||
<section className="space-y-[var(--spacing-scale-024)]">
|
||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||
Card Component
|
||||
</h2>
|
||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||
<p className="font-inter text-[18px] leading-[24px] text-[var(--color-content-default-secondary)]">
|
||||
Horizontal and vertical orientations with recommended and selected
|
||||
states.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl">
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
|
||||
Horizontal + Recommended
|
||||
</h3>
|
||||
<Card
|
||||
label="Label"
|
||||
supportText="Members vote to resolve a dispute democratically."
|
||||
recommended={true}
|
||||
selected={false}
|
||||
orientation="horizontal"
|
||||
onClick={() => console.warn("Card clicked")}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
|
||||
Horizontal + Selected
|
||||
</h3>
|
||||
<Card
|
||||
label="Label"
|
||||
supportText="Members vote to resolve a dispute democratically."
|
||||
recommended={false}
|
||||
selected={true}
|
||||
orientation="horizontal"
|
||||
onClick={() => console.warn("Card clicked")}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
|
||||
Vertical + Recommended
|
||||
</h3>
|
||||
<Card
|
||||
label="Label"
|
||||
supportText="Invite-only"
|
||||
recommended={true}
|
||||
selected={false}
|
||||
orientation="vertical"
|
||||
showInfoIcon={true}
|
||||
onClick={() => console.warn("Card clicked")}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
|
||||
Vertical + Selected
|
||||
</h3>
|
||||
<Card
|
||||
label="Label"
|
||||
supportText="Invite-only"
|
||||
recommended={false}
|
||||
selected={true}
|
||||
orientation="vertical"
|
||||
showInfoIcon={true}
|
||||
onClick={() => console.warn("Card clicked")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Collapsed State - Large */}
|
||||
<section className="space-y-[var(--spacing-scale-024)]">
|
||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||
@@ -648,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")}
|
||||
/>
|
||||
</div>
|
||||
onClick={() => console.warn("Card clicked: Mutual Aid Mondays")}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Collapsed State - Medium */}
|
||||
@@ -668,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")}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Expanded State - Large */}
|
||||
<section className="space-y-[var(--spacing-scale-024)]">
|
||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||
<section className="space-y-[var(--spacing-scale-024)]">
|
||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||
Expanded State - Large (L)
|
||||
</h2>
|
||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||
</h2>
|
||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||
<RuleCard
|
||||
title="Mutual Aid Mondays"
|
||||
description="Mutual Aid Monday is a grassroots community in Denver, founded in November 2020 by Kelsang Virya, dedicated to supporting neighbors experiencing homelessness."
|
||||
@@ -689,9 +862,9 @@ 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")}
|
||||
/>
|
||||
</div>
|
||||
onClick={() => console.warn("Card clicked: Mutual Aid Mondays")}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Expanded State - Medium */}
|
||||
@@ -710,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")}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Different Background Colors */}
|
||||
<section className="space-y-[var(--spacing-scale-024)]">
|
||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||
<section className="space-y-[var(--spacing-scale-024)]">
|
||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||
Different Background Colors
|
||||
</h2>
|
||||
</h2>
|
||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-[var(--spacing-scale-024)]">
|
||||
<RuleCard
|
||||
@@ -737,7 +910,7 @@ export default function ComponentsPreview() {
|
||||
height={103}
|
||||
/>
|
||||
}
|
||||
onClick={() => console.log("Consensus clusters selected")}
|
||||
onClick={() => console.warn("Consensus clusters selected")}
|
||||
/>
|
||||
<RuleCard
|
||||
title="Consensus"
|
||||
@@ -754,10 +927,10 @@ export default function ComponentsPreview() {
|
||||
height={103}
|
||||
/>
|
||||
}
|
||||
onClick={() => console.log("Consensus selected")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
onClick={() => console.warn("Consensus selected")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Logo Fallback */}
|
||||
@@ -774,9 +947,9 @@ export default function ComponentsPreview() {
|
||||
size="L"
|
||||
className="w-[525px]"
|
||||
communityInitials="CE"
|
||||
onClick={() => console.log("Community Example selected")}
|
||||
/>
|
||||
</div>
|
||||
onClick={() => console.warn("Community Example selected")}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* MultiSelect Component */}
|
||||
@@ -787,7 +960,7 @@ export default function ComponentsPreview() {
|
||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||
{/* Small size */}
|
||||
<MultiSelectExample size="S" />
|
||||
|
||||
|
||||
{/* Medium size */}
|
||||
<MultiSelectExample size="M" />
|
||||
</div>
|
||||
|
||||
@@ -189,7 +189,7 @@ export default async function BlogPostPage({ params }: PageProps) {
|
||||
url: "https://communityrule.com",
|
||||
logo: {
|
||||
"@type": "ImageObject",
|
||||
url: "https://communityrule.com/assets/Logo.svg",
|
||||
url: "https://communityrule.com/assets/logo/Logo.svg",
|
||||
},
|
||||
},
|
||||
datePublished: post.frontmatter.date,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAllBlogPosts } from "../../../lib/content";
|
||||
import ContentThumbnailTemplate from "../../../components/content/ContentThumbnailTemplate";
|
||||
import ContentThumbnailTemplate from "../../components/content/ContentThumbnailTemplate";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
||||
+18
-12
@@ -12,12 +12,15 @@ const LogoWall = dynamic(() => import("../components/sections/LogoWall"), {
|
||||
ssr: true,
|
||||
});
|
||||
|
||||
const NumberedCards = dynamic(() => import("../components/sections/NumberedCards"), {
|
||||
loading: () => (
|
||||
<section className="py-[var(--spacing-scale-032)] min-h-[300px]" />
|
||||
),
|
||||
ssr: true,
|
||||
});
|
||||
const NumberedCards = dynamic(
|
||||
() => import("../components/sections/NumberedCards"),
|
||||
{
|
||||
loading: () => (
|
||||
<section className="py-[var(--spacing-scale-032)] min-h-[300px]" />
|
||||
),
|
||||
ssr: true,
|
||||
},
|
||||
);
|
||||
|
||||
const RuleStack = dynamic(() => import("../components/sections/RuleStack"), {
|
||||
loading: () => (
|
||||
@@ -26,12 +29,15 @@ const RuleStack = dynamic(() => import("../components/sections/RuleStack"), {
|
||||
ssr: true,
|
||||
});
|
||||
|
||||
const FeatureGrid = dynamic(() => import("../components/sections/FeatureGrid"), {
|
||||
loading: () => (
|
||||
<section className="py-[var(--spacing-scale-032)] min-h-[500px]" />
|
||||
),
|
||||
ssr: true,
|
||||
});
|
||||
const FeatureGrid = dynamic(
|
||||
() => import("../components/sections/FeatureGrid"),
|
||||
{
|
||||
loading: () => (
|
||||
<section className="py-[var(--spacing-scale-032)] min-h-[500px]" />
|
||||
),
|
||||
ssr: true,
|
||||
},
|
||||
);
|
||||
|
||||
const QuoteBlock = dynamic(() => import("../components/sections/QuoteBlock"), {
|
||||
loading: () => (
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
export type ContextMenuItemSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large";
|
||||
export type ContextMenuItemSizeValue =
|
||||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "Small"
|
||||
| "Medium"
|
||||
| "Large";
|
||||
|
||||
export interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
|
||||
@@ -40,7 +40,25 @@ const WebVitalsDashboardContainer = memo(() => {
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
import("web-vitals").then((webVitals) => {
|
||||
const { getCLS, getFID, getFCP, getLCP, getTTFB } = webVitals as any;
|
||||
// web-vitals v4 typings don't expose legacy get* names the same way; runtime bundle still provides them for this dashboard.
|
||||
const { getCLS, getFID, getFCP, getLCP, getTTFB } =
|
||||
webVitals as unknown 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) => ({
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"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<React.SVGProps<SVGSVGElement>>
|
||||
| { default: React.ComponentType<React.SVGProps<SVGSVGElement>> }
|
||||
> = {
|
||||
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<React.SVGProps<SVGSVGElement>>;
|
||||
}
|
||||
).default
|
||||
: (SvgModule as React.ComponentType<React.SVGProps<SVGSVGElement>>);
|
||||
if (typeof Svg !== "function") return null;
|
||||
return (
|
||||
<Svg
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
aria-hidden={ariaHidden}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(IconComponent);
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.25 14.0386V5.53857H12.75V14.0386H11.25ZM11.25 18.4616V16.9616H12.75V18.4616H11.25Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 224 B |
@@ -0,0 +1,3 @@
|
||||
export { default as Icon } from "./Icon";
|
||||
export type { IconName, IconProps } from "./Icon";
|
||||
export { default as Logo } from "./logo";
|
||||
@@ -0,0 +1,134 @@
|
||||
import { memo } from "react";
|
||||
import Link from "next/link";
|
||||
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
|
||||
|
||||
interface LogoProps {
|
||||
size?:
|
||||
| "default"
|
||||
| "footer"
|
||||
| "createFlow"
|
||||
| "topNavFolderTop"
|
||||
| "topNavHeader";
|
||||
/**
|
||||
* Visual style: default (dark on light) or inverse (e.g. black/white on teal).
|
||||
* @default "default"
|
||||
*/
|
||||
palette?: "default" | "inverse";
|
||||
/**
|
||||
* Whether to show the "CommunityRule" wordmark.
|
||||
* @default true
|
||||
*/
|
||||
wordmark?: boolean;
|
||||
}
|
||||
|
||||
interface SizeConfig {
|
||||
containerHeight: string;
|
||||
gap: string;
|
||||
textSize: string;
|
||||
lineHeight: string;
|
||||
iconSize: string;
|
||||
}
|
||||
|
||||
const Logo = memo<LogoProps>(
|
||||
({ size = "default", palette = "default", wordmark = true }) => {
|
||||
// Size configurations
|
||||
const sizes: Record<string, SizeConfig> = {
|
||||
default: {
|
||||
containerHeight: "h-[41px]",
|
||||
gap: "gap-[8.28px]",
|
||||
textSize: "text-[21.97px]",
|
||||
lineHeight: "leading-[27.05px]",
|
||||
iconSize: "w-[27.05px] h-[27.05px]",
|
||||
},
|
||||
footer: {
|
||||
containerHeight:
|
||||
"h-[41px] sm:h-[calc(40px*1.37)] lg:h-[calc(40px*2.05)]",
|
||||
gap: "gap-[8.28px] sm:gap-[calc(8px*1.37)] lg:gap-[calc(8px*2.05)]",
|
||||
textSize:
|
||||
"text-[21.97px] sm:text-[calc(21.97px*1.37)] lg:text-[calc(21.97px*2.05)]",
|
||||
lineHeight:
|
||||
"leading-[27.05px] sm:leading-[calc(27.05px*1.37)] lg:leading-[calc(27.05px*2.05)]",
|
||||
iconSize:
|
||||
"w-[27.05px] h-[27.05px] sm:w-[calc(27.05px*1.37)] sm:h-[calc(27.05px*1.37)] lg:w-[calc(27.05px*2.05)] lg:h-[calc(27.05px*2.05)]",
|
||||
},
|
||||
createFlow: {
|
||||
containerHeight: "h-[30px] md:h-[41px]",
|
||||
gap: "gap-[6px] md:gap-[8.28px]",
|
||||
textSize: "text-[16.48px] md:text-[21.97px]",
|
||||
lineHeight: "leading-[20.28px] md:leading-[27.05px]",
|
||||
iconSize: "w-[20.28px] h-[20.28px] md:w-[27.05px] md:h-[27.05px]",
|
||||
},
|
||||
topNavFolderTop: {
|
||||
containerHeight:
|
||||
"h-[14.11px] sm:h-[21.06px] md:h-[32.24px] lg:h-[28px] xl:h-[36px]",
|
||||
gap: "gap-0 sm:gap-[3.19px] md:gap-[4.89px] lg:gap-[6.55px] xl:gap-[8.64px]",
|
||||
textSize:
|
||||
"text-[11.57px] sm:text-[11.69px] md:text-[17.89px] lg:text-[21.97px] xl:text-[29.01px]",
|
||||
lineHeight:
|
||||
"leading-[14.24px] sm:leading-[14.39px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[35.7px]",
|
||||
iconSize:
|
||||
"w-[14.11px] h-[14.11px] sm:w-[14.39px] sm:h-[14.39px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[35.7px] xl:h-[35.7px]",
|
||||
},
|
||||
topNavHeader: {
|
||||
containerHeight:
|
||||
"h-[20.85px] sm:h-[20.85px] md:h-[17.91px] lg:h-[28px] xl:h-[34px]",
|
||||
gap: "gap-0 sm:gap-[4.21px] md:gap-[6.51px] lg:gap-[6.55px] xl:gap-[8.19px]",
|
||||
textSize:
|
||||
"text-[11.57px] sm:text-[11.57px] md:text-[17.89px] lg:text-[21.97px] xl:text-[27.47px]",
|
||||
lineHeight:
|
||||
"leading-[14.24px] sm:leading-[14.24px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[33.81px]",
|
||||
iconSize:
|
||||
"w-[14.24px] h-[14.24px] sm:w-[14.24px] sm:h-[14.24px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[33.81px] xl:h-[33.81px]",
|
||||
},
|
||||
};
|
||||
|
||||
const config = sizes[size || "default"] || sizes.default;
|
||||
const isInverse = palette === "inverse";
|
||||
const textColorClass = isInverse
|
||||
? "text-[var(--color-content-invert-primary)]"
|
||||
: "text-[var(--color-content-default-primary)]";
|
||||
const wordmarkVisibilityClass =
|
||||
size === "topNavFolderTop" || size === "topNavHeader"
|
||||
? wordmark
|
||||
? "hidden sm:block"
|
||||
: "hidden"
|
||||
: wordmark
|
||||
? ""
|
||||
: "hidden";
|
||||
|
||||
return (
|
||||
<Link href="/" className="block" aria-label="CommunityRule Logo">
|
||||
<div
|
||||
className={`flex items-center ${config.containerHeight} ${
|
||||
wordmark ? config.gap : ""
|
||||
} transition-all duration-200 ease-in-out hover:scale-[1.02] cursor-pointer`}
|
||||
>
|
||||
{/* Logo Text - responsive visibility for topNav sizes */}
|
||||
<div
|
||||
className={`font-bricolage-grotesque ${textColorClass} ${config.textSize} ${config.lineHeight} font-normal tracking-[0px] transition-colors duration-200 ${wordmarkVisibilityClass}`}
|
||||
aria-label="CommunityRule"
|
||||
>
|
||||
CommunityRule
|
||||
</div>
|
||||
|
||||
{/* Vector Icon */}
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.LOGO)}
|
||||
alt="CommunityRule Logo Icon"
|
||||
width={27.05}
|
||||
height={27.05}
|
||||
className={`flex-shrink-0 ${config.iconSize} transition-all duration-200 ${
|
||||
isInverse ? "brightness-0" : ""
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Logo.displayName = "Logo";
|
||||
|
||||
export default Logo;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Logo";
|
||||
@@ -109,15 +109,11 @@ const Button = memo<ButtonProps>(
|
||||
const variant = getVariantFromTypeAndPalette(buttonType, buttonPalette);
|
||||
|
||||
const sizeStyles: Record<string, string> = {
|
||||
xsmall:
|
||||
"p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]",
|
||||
small:
|
||||
"p-[var(--spacing-scale-008)] gap-[var(--spacing-scale-002)]",
|
||||
xsmall: "p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]",
|
||||
small: "p-[var(--spacing-scale-008)] gap-[var(--spacing-scale-002)]",
|
||||
medium: "p-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]",
|
||||
large:
|
||||
"p-[var(--spacing-scale-012)] gap-[var(--spacing-scale-006)]",
|
||||
xlarge:
|
||||
"p-[var(--spacing-scale-016)] gap-[var(--spacing-scale-008)]",
|
||||
large: "p-[var(--spacing-scale-012)] gap-[var(--spacing-scale-006)]",
|
||||
xlarge: "p-[var(--spacing-scale-016)] gap-[var(--spacing-scale-008)]",
|
||||
};
|
||||
|
||||
const fontStyles: Record<string, string> = {
|
||||
@@ -135,7 +131,8 @@ const Button = memo<ButtonProps>(
|
||||
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-[1.5px] border-transparent hover:bg-[var(--color-surface-default-primary)] hover:text-[var(--color-content-default-brand-primary)] hover:border-[var(--color-border-default-brand-primary)] hover:scale-[1.02] focus:bg-[var(--color-surface-default-primary)] focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:border-transparent focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
||||
outline:
|
||||
"bg-transparent text-[var(--color-content-default-primary)] border-[1.5px] border-[var(--color-border-invert-primary)] hover:bg-transparent hover:text-[var(--color-content-default-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-default-brand-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-default-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-border-invert-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[1.5px] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
||||
"outline-inverse": "bg-transparent text-[var(--color-content-invert-primary)] border-[1.5px] border-[var(--color-border-default-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-invert-brand-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-invert-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-border-default-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus:scale-[1.02] active:bg-transparent active:text-[var(--color-content-invert-primary)] active:border-[1.5px] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
||||
"outline-inverse":
|
||||
"bg-transparent text-[var(--color-content-invert-primary)] border-[1.5px] border-[var(--color-border-default-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-invert-brand-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-invert-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-border-default-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus:scale-[1.02] active:bg-transparent active:text-[var(--color-content-invert-primary)] active:border-[1.5px] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
||||
ghost:
|
||||
"bg-transparent text-[var(--color-content-default-brand-primary)] border-[1.5px] border-transparent hover:bg-transparent hover:text-[var(--color-content-default-primary)] hover:border-transparent hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:border-transparent focus:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
||||
"ghost-inverse":
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { CardView } from "./Card.view";
|
||||
import type { CardProps } from "./Card.types";
|
||||
|
||||
const CardContainer = memo<CardProps>(
|
||||
({
|
||||
label,
|
||||
supportText = "",
|
||||
recommended = false,
|
||||
selected = false,
|
||||
orientation = "horizontal",
|
||||
showInfoIcon = false,
|
||||
id,
|
||||
className = "",
|
||||
onClick,
|
||||
}) => {
|
||||
const handleClick = () => {
|
||||
if (onClick) onClick();
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
handleClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CardView
|
||||
label={label}
|
||||
supportText={supportText}
|
||||
recommended={recommended}
|
||||
selected={selected}
|
||||
orientation={orientation}
|
||||
showInfoIcon={showInfoIcon}
|
||||
id={id}
|
||||
className={className}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CardContainer.displayName = "Card";
|
||||
|
||||
export default CardContainer;
|
||||
@@ -0,0 +1,25 @@
|
||||
export interface CardProps {
|
||||
label: string;
|
||||
supportText?: string;
|
||||
recommended?: boolean;
|
||||
selected?: boolean;
|
||||
orientation: "horizontal" | "vertical";
|
||||
showInfoIcon?: boolean;
|
||||
/** Optional id for the card root (e.g. data-card-id for focus after modal close). */
|
||||
id?: string;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export interface CardViewProps {
|
||||
label: string;
|
||||
supportText: string;
|
||||
recommended: boolean;
|
||||
selected: boolean;
|
||||
orientation: "horizontal" | "vertical";
|
||||
showInfoIcon: boolean;
|
||||
id: string | undefined;
|
||||
className: string;
|
||||
onClick: () => void;
|
||||
onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import Tag from "../../utility/Tag";
|
||||
import type { CardViewProps } from "./Card.types";
|
||||
|
||||
function InfoIcon() {
|
||||
return (
|
||||
<span
|
||||
className="flex h-[var(--spacing-scale-016)] w-[var(--spacing-scale-016)] shrink-0 items-center justify-center rounded-full border border-[var(--color-content-invert-brand-secondary)] bg-transparent font-inter text-[10px] font-medium leading-none text-[var(--color-content-invert-brand-secondary)]"
|
||||
aria-hidden
|
||||
>
|
||||
?
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTag({
|
||||
recommended,
|
||||
selected,
|
||||
}: {
|
||||
recommended: boolean;
|
||||
selected: boolean;
|
||||
}) {
|
||||
if (selected) return <Tag variant="selected" />;
|
||||
if (recommended) return <Tag variant="recommended" />;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function CardView({
|
||||
label,
|
||||
supportText,
|
||||
recommended,
|
||||
selected,
|
||||
orientation,
|
||||
showInfoIcon,
|
||||
id: cardId,
|
||||
className,
|
||||
onClick,
|
||||
onKeyDown,
|
||||
}: CardViewProps) {
|
||||
const borderClass = "border border-[var(--color-border-default-primary)]";
|
||||
const selectedBorder = selected
|
||||
? "outline outline-2 outline-dashed outline-black outline-offset-[-2px]"
|
||||
: "";
|
||||
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 (
|
||||
<div
|
||||
{...(cardId ? { "data-card-id": cardId } : {})}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={supportText ? `${label}: ${supportText}` : label}
|
||||
className={baseClasses}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<div className="flex flex-col gap-2 items-start w-full">
|
||||
<CardTag recommended={recommended} selected={selected} />
|
||||
<span className="font-inter text-base font-semibold leading-6 text-black w-full">
|
||||
{label}
|
||||
</span>
|
||||
{supportText ? (
|
||||
<p className="font-inter text-sm font-normal leading-5 text-black w-full">
|
||||
{supportText}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...(cardId ? { "data-card-id": cardId } : {})}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={supportText ? `${label}: ${supportText}` : label}
|
||||
className={`${baseClasses} flex flex-row items-center justify-between gap-4`}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<div className="min-w-0 flex-1 flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-inter text-base font-semibold leading-6 text-black">
|
||||
{label}
|
||||
</span>
|
||||
{showInfoIcon ? <InfoIcon /> : null}
|
||||
</div>
|
||||
{supportText ? (
|
||||
<p className="font-inter text-sm font-normal leading-5 text-black">
|
||||
{supportText}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="shrink-0 w-[6rem]">
|
||||
<CardTag recommended={recommended} selected={selected} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./Card.container";
|
||||
export type { CardProps } from "./Card.types";
|
||||
@@ -29,7 +29,8 @@ interface NumberCardProps {
|
||||
|
||||
const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
|
||||
// Base classes common to all sizes
|
||||
const baseClasses = "bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg";
|
||||
const baseClasses =
|
||||
"bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg";
|
||||
|
||||
// If size prop is provided, use explicit size classes
|
||||
// Otherwise, use responsive breakpoints for backward compatibility
|
||||
@@ -40,16 +41,22 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
|
||||
const sizeClasses = {
|
||||
Small: "flex flex-col items-end justify-center gap-4 p-5 relative",
|
||||
Medium: "flex flex-row items-center gap-8 p-8 relative",
|
||||
Large: "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
|
||||
XLarge: "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
|
||||
Large:
|
||||
"flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
|
||||
XLarge:
|
||||
"flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
|
||||
};
|
||||
|
||||
// Text size classes
|
||||
const textClasses = {
|
||||
Small: "font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]",
|
||||
Medium: "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
|
||||
Large: "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
|
||||
XLarge: "font-bricolage-grotesque font-medium text-[32px] leading-[32px] text-[#141414]",
|
||||
Small:
|
||||
"font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]",
|
||||
Medium:
|
||||
"font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
|
||||
Large:
|
||||
"font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
|
||||
XLarge:
|
||||
"font-bricolage-grotesque font-medium text-[32px] leading-[32px] text-[#141414]",
|
||||
};
|
||||
|
||||
// Section number wrapper classes - Small doesn't need a wrapper
|
||||
@@ -74,11 +81,9 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
|
||||
<div className={`${baseClasses} ${sizeClasses[size]}`}>
|
||||
{/* Section Number - Direct child for Small */}
|
||||
<SectionNumber number={number} />
|
||||
|
||||
|
||||
{/* Card Content */}
|
||||
<p className={textClasses[size]}>
|
||||
{text}
|
||||
</p>
|
||||
<p className={textClasses[size]}>{text}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -92,9 +97,7 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
|
||||
|
||||
{/* Card Content */}
|
||||
<div className={contentClasses[size]}>
|
||||
<p className={textClasses[size]}>
|
||||
{text}
|
||||
</p>
|
||||
<p className={textClasses[size]}>{text}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -103,7 +106,9 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
|
||||
// Responsive breakpoints for backward compatibility (matches original behavior)
|
||||
// Maps to: Small (mobile) -> Medium (sm) -> Large (lg) -> XLarge (xl)
|
||||
return (
|
||||
<div className={`${baseClasses} flex flex-col gap-4 p-5 sm:flex-row sm:gap-8 sm:p-8 sm:items-center lg:flex-col lg:gap-[22px] lg:items-start lg:justify-end lg:p-8 lg:relative lg:h-[238px]`}>
|
||||
<div
|
||||
className={`${baseClasses} flex flex-col gap-4 p-5 sm:flex-row sm:gap-8 sm:p-8 sm:items-center lg:flex-col lg:gap-[22px] lg:items-start lg:justify-end lg:p-8 lg:relative lg:h-[238px]`}
|
||||
>
|
||||
{/* Section Number - Responsive positioning */}
|
||||
<div className="flex justify-end items-end sm:justify-start sm:flex-shrink-0 lg:absolute lg:top-8 lg:right-8">
|
||||
<SectionNumber number={number} />
|
||||
|
||||
@@ -5,7 +5,11 @@ export interface Category {
|
||||
chipOptions: ChipOption[];
|
||||
onChipClick?: (categoryName: string, chipId: string) => void;
|
||||
onAddClick?: (categoryName: string) => void;
|
||||
onCustomChipConfirm?: (categoryName: string, chipId: string, value: string) => void;
|
||||
onCustomChipConfirm?: (
|
||||
categoryName: string,
|
||||
chipId: string,
|
||||
value: string,
|
||||
) => void;
|
||||
onCustomChipClose?: (categoryName: string, chipId: string) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,34 +28,39 @@ export function RuleCardView({
|
||||
const isMedium = size === "M";
|
||||
const isSmall = size === "S";
|
||||
const isExtraSmall = size === "XS";
|
||||
|
||||
|
||||
// Card dimensions - use CSS classes from className if provided, otherwise use size-based logic
|
||||
// Check if className already has padding/gap classes
|
||||
const hasResponsivePadding = className?.includes("p-[") || className?.includes("px-[") || className?.includes("py-[") || className?.includes("pt-[") || className?.includes("pb-[");
|
||||
const hasResponsivePadding =
|
||||
className?.includes("p-[") ||
|
||||
className?.includes("px-[") ||
|
||||
className?.includes("py-[") ||
|
||||
className?.includes("pt-[") ||
|
||||
className?.includes("pb-[");
|
||||
const hasResponsiveGap = className?.includes("gap-[");
|
||||
|
||||
|
||||
const cardPadding = hasResponsivePadding
|
||||
? "" // If className has responsive padding, don't add size-based padding
|
||||
: isLarge || isSmall
|
||||
? "p-[24px]"
|
||||
: isMedium
|
||||
? "p-[16px]"
|
||||
: "pb-[24px] pt-[12px] px-[12px]"; // XS: asymmetric padding
|
||||
? "p-[24px]"
|
||||
: isMedium
|
||||
? "p-[16px]"
|
||||
: "pb-[24px] pt-[12px] px-[12px]"; // XS: asymmetric padding
|
||||
const cardGap = expanded
|
||||
? "gap-[16px]"
|
||||
: hasResponsiveGap
|
||||
? "" // If className has responsive gap, don't add size-based gap
|
||||
: isLarge
|
||||
? "gap-[10px]"
|
||||
: isMedium
|
||||
? "gap-[12px]"
|
||||
: "gap-[18px]"; // XS and S: 18px gap
|
||||
? "" // If className has responsive gap, don't add size-based gap
|
||||
: isLarge
|
||||
? "gap-[10px]"
|
||||
: isMedium
|
||||
? "gap-[12px]"
|
||||
: "gap-[18px]"; // XS and S: 18px gap
|
||||
const cardWidth = expanded
|
||||
? isLarge
|
||||
? "w-[568px]"
|
||||
: isMedium
|
||||
? "w-[398px]"
|
||||
: "" // XS and S: no fixed width
|
||||
? "w-[398px]"
|
||||
: "" // XS and S: no fixed width
|
||||
: "";
|
||||
|
||||
// Logo/Icon dimensions - use CSS responsive classes
|
||||
@@ -81,19 +86,21 @@ export function RuleCardView({
|
||||
const descriptionClass = isLarge
|
||||
? "font-inter font-medium text-[18px] leading-[24px]"
|
||||
: isMedium
|
||||
? "font-inter font-medium text-[14px] leading-[16px]"
|
||||
: isSmall
|
||||
? "font-inter font-medium text-[14px] leading-[16px]" // S: 14px, medium, Inter
|
||||
: "font-inter font-medium text-[12px] leading-[14px]"; // XS: 12px, medium, Inter
|
||||
? "font-inter font-medium text-[14px] leading-[16px]"
|
||||
: isSmall
|
||||
? "font-inter font-medium text-[14px] leading-[16px]" // S: 14px, medium, Inter
|
||||
: "font-inter font-medium text-[12px] leading-[14px]"; // XS: 12px, medium, Inter
|
||||
|
||||
// Render logo/icon
|
||||
const renderLogo = () => {
|
||||
if (logoUrl) {
|
||||
// Check if it's a localhost URL or external URL that needs regular img tag
|
||||
const isLocalhost = logoUrl.startsWith("http://localhost") || logoUrl.startsWith("https://localhost");
|
||||
|
||||
const isLocalhost =
|
||||
logoUrl.startsWith("http://localhost") ||
|
||||
logoUrl.startsWith("https://localhost");
|
||||
|
||||
const containerClass = `${logoContainerClass} relative rounded-full overflow-hidden mix-blend-luminosity max-[639px]:p-[16px] min-[640px]:max-[1023px]:p-[12px]`;
|
||||
|
||||
|
||||
if (isLocalhost) {
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
@@ -108,7 +115,7 @@ export function RuleCardView({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
<Image
|
||||
@@ -121,15 +128,17 @@ export function RuleCardView({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (icon) {
|
||||
return (
|
||||
<div className={`${logoContainerClass} flex items-center justify-center max-[639px]:p-[16px] min-[640px]:max-[1023px]:p-[12px]`}>
|
||||
<div
|
||||
className={`${logoContainerClass} flex items-center justify-center max-[639px]:p-[16px] min-[640px]:max-[1023px]:p-[12px]`}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (communityInitials) {
|
||||
const initialsSize = `
|
||||
max-[639px]:text-[16px]
|
||||
@@ -138,26 +147,29 @@ export function RuleCardView({
|
||||
min-[1440px]:text-[36px]
|
||||
`;
|
||||
return (
|
||||
<div className={`${logoContainerClass} rounded-full bg-[var(--color-surface-default-primary)] flex items-center justify-center`}>
|
||||
<span className={`${initialsSize} font-bricolage-grotesque font-bold text-[var(--color-content-default-primary,white)]`}>
|
||||
<div
|
||||
className={`${logoContainerClass} rounded-full bg-[var(--color-surface-default-primary)] flex items-center justify-center`}
|
||||
>
|
||||
<span
|
||||
className={`${initialsSize} font-bricolage-grotesque font-bold text-[var(--color-content-default-primary,white)]`}
|
||||
>
|
||||
{communityInitials}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
// Border radius - use CSS classes if provided via className, otherwise use size-based logic
|
||||
const borderRadiusClass = className?.includes("rounded-")
|
||||
const borderRadiusClass = className?.includes("rounded-")
|
||||
? "" // If className already has border radius, don't add size-based one
|
||||
: isExtraSmall
|
||||
? "rounded-[var(--measures-radius-200,8px)]"
|
||||
: isSmall
|
||||
? "rounded-[var(--measures-radius-300,12px)]"
|
||||
: "rounded-[var(--radius-measures-radius-small)]";
|
||||
: isExtraSmall
|
||||
? "rounded-[var(--measures-radius-200,8px)]"
|
||||
: isSmall
|
||||
? "rounded-[var(--measures-radius-300,12px)]"
|
||||
: "rounded-[var(--radius-measures-radius-small)]";
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -170,48 +182,60 @@ export function RuleCardView({
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
{/* Outermost container with bottom border - taller to match Figma */}
|
||||
<div className={`
|
||||
<div
|
||||
className={`
|
||||
border-b border-black border-solid flex items-center relative shrink-0 w-full
|
||||
max-[639px]:h-[72px]
|
||||
min-[640px]:max-[1023px]:h-[80px]
|
||||
min-[1024px]:max-[1439px]:h-[88px]
|
||||
min-[1440px]:h-[136px]
|
||||
`}>
|
||||
`}
|
||||
>
|
||||
{/* Logo/Icon - fixed width/height, vertically centered, does not touch bottom */}
|
||||
{renderLogo() && (
|
||||
<div className={`
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center shrink-0
|
||||
max-[639px]:w-[72px] max-[639px]:h-[72px] max-[639px]:border-r max-[639px]:border-black max-[639px]:border-solid
|
||||
min-[640px]:max-[1023px]:w-[80px] min-[640px]:max-[1023px]:h-[80px] min-[640px]:max-[1023px]:border-r min-[640px]:max-[1023px]:border-black min-[640px]:max-[1023px]:border-solid
|
||||
min-[1024px]:max-[1439px]:w-[56px] min-[1024px]:max-[1439px]:h-[56px]
|
||||
min-[1440px]:w-[103px] min-[1440px]:h-[103px]
|
||||
`}>
|
||||
`}
|
||||
>
|
||||
{renderLogo()}
|
||||
</div>
|
||||
)}
|
||||
{/* Spacing between icon and title */}
|
||||
<div className="
|
||||
<div
|
||||
className="
|
||||
max-[1023px]:hidden
|
||||
min-[1024px]:w-[16px] min-[1024px]:shrink-0
|
||||
" />
|
||||
"
|
||||
/>
|
||||
{/* Container with no padding and left border - extends full height to touch bottom */}
|
||||
{title && (
|
||||
<div className={`
|
||||
<div
|
||||
className={`
|
||||
flex-1 min-w-0 h-full flex
|
||||
max-[1023px]:border-0
|
||||
min-[1024px]:border-l min-[1024px]:border-black min-[1024px]:border-solid
|
||||
`}>
|
||||
`}
|
||||
>
|
||||
{/* Inner container for header text with padding */}
|
||||
<div className={`
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center w-full
|
||||
max-[639px]:pl-[8px] max-[639px]:py-[8px]
|
||||
min-[640px]:max-[1023px]:pl-[12px] min-[640px]:max-[1023px]:py-[12px]
|
||||
min-[1024px]:max-[1439px]:px-[16px] min-[1024px]:max-[1439px]:py-[12px]
|
||||
min-[1440px]:px-[16px] min-[1440px]:py-[24px]
|
||||
`}>
|
||||
<h3 className={`${titleClass} text-black overflow-hidden text-ellipsis w-full`}>
|
||||
{title}
|
||||
</h3>
|
||||
`}
|
||||
>
|
||||
<h3
|
||||
className={`${titleClass} text-black overflow-hidden text-ellipsis w-full`}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -237,7 +261,11 @@ export function RuleCardView({
|
||||
category.onAddClick?.(category.name);
|
||||
}}
|
||||
onCustomChipConfirm={(chipId, value) => {
|
||||
category.onCustomChipConfirm?.(category.name, chipId, value);
|
||||
category.onCustomChipConfirm?.(
|
||||
category.name,
|
||||
chipId,
|
||||
value,
|
||||
);
|
||||
}}
|
||||
onCustomChipClose={(chipId) => {
|
||||
category.onCustomChipClose?.(category.name, chipId);
|
||||
@@ -250,11 +278,9 @@ export function RuleCardView({
|
||||
</div>
|
||||
)}
|
||||
{/* Footer: Description */}
|
||||
{description && (
|
||||
{description && (
|
||||
<div className="border-t border-black border-solid pt-[16px] relative shrink-0 w-full">
|
||||
<p className={`${descriptionClass} text-black`}>
|
||||
{description}
|
||||
</p>
|
||||
<p className={`${descriptionClass} text-black`}>{description}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -263,8 +289,8 @@ export function RuleCardView({
|
||||
description && (
|
||||
<div className="flex items-center justify-center relative shrink-0 w-full">
|
||||
<p className={`${descriptionClass} text-black flex-1`}>
|
||||
{description}
|
||||
</p>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { BlogPost } from "../../../../lib/content";
|
||||
|
||||
export type ContentContainerSizeValue = "xs" | "responsive" | "Xs" | "Responsive";
|
||||
export type ContentContainerSizeValue =
|
||||
| "xs"
|
||||
| "responsive"
|
||||
| "Xs"
|
||||
| "Responsive";
|
||||
|
||||
export interface ContentContainerProps {
|
||||
post: BlogPost;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { BlogPost } from "../../../../lib/content";
|
||||
|
||||
export type ContentThumbnailTemplateVariantValue = "vertical" | "horizontal" | "Vertical" | "Horizontal";
|
||||
export type ContentThumbnailTemplateVariantValue =
|
||||
| "vertical"
|
||||
| "horizontal"
|
||||
| "Vertical"
|
||||
| "Horizontal";
|
||||
|
||||
export interface ContentThumbnailTemplateProps {
|
||||
post: BlogPost;
|
||||
|
||||
@@ -4,7 +4,10 @@ import { memo } from "react";
|
||||
import { useComponentId } from "../../../hooks";
|
||||
import { CheckboxView } from "./Checkbox.view";
|
||||
import type { CheckboxProps } from "./Checkbox.types";
|
||||
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
|
||||
import {
|
||||
normalizeMode,
|
||||
normalizeState,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
const CheckboxContainer = memo<CheckboxProps>(
|
||||
({
|
||||
@@ -24,7 +27,7 @@ const CheckboxContainer = memo<CheckboxProps>(
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const mode = normalizeMode(modeProp);
|
||||
const state = normalizeState(stateProp);
|
||||
|
||||
|
||||
const isInverse = mode === "inverse";
|
||||
const isStandard = mode === "standard";
|
||||
|
||||
@@ -43,7 +46,9 @@ const CheckboxContainer = memo<CheckboxProps>(
|
||||
transition-all
|
||||
duration-200
|
||||
ease-in-out
|
||||
`.trim().replace(/\s+/g, " ");
|
||||
`
|
||||
.trim()
|
||||
.replace(/\s+/g, " ");
|
||||
|
||||
// Get box styles based on state and checked status per Figma designs
|
||||
const getBoxStyles = (): string => {
|
||||
|
||||
@@ -22,8 +22,10 @@ const CheckboxGroupContainer = ({
|
||||
const groupId = name || `checkbox-group-${generatedId}`;
|
||||
|
||||
// Internal state to track checked values (only used if value prop is not provided)
|
||||
const [internalCheckedValues, setInternalCheckedValues] = useState<string[]>([]);
|
||||
|
||||
const [internalCheckedValues, setInternalCheckedValues] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
// Use controlled value if provided, otherwise use internal state
|
||||
const checkedValues = value !== undefined ? value : internalCheckedValues;
|
||||
|
||||
|
||||
@@ -23,10 +23,7 @@ export function CheckboxGroupView({
|
||||
// If there's subtext, render checkbox without label and handle layout separately
|
||||
if (option.subtext) {
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
className="flex gap-[8px] items-start"
|
||||
>
|
||||
<div key={option.value} className="flex gap-[8px] items-start">
|
||||
<Checkbox
|
||||
checked={isChecked}
|
||||
mode={mode}
|
||||
|
||||
@@ -41,7 +41,10 @@ const ChipContainer = memo<ChipProps>(
|
||||
}
|
||||
}, [isCustom]);
|
||||
|
||||
const handleCheck = (value: string, event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const handleCheck = (
|
||||
value: string,
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
) => {
|
||||
if (onCheck && value.trim()) {
|
||||
onCheck(value.trim(), event);
|
||||
// Reset input after successful check
|
||||
@@ -63,7 +66,10 @@ const ChipContainer = memo<ChipProps>(
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter" && inputValue.trim() && onCheck) {
|
||||
event.preventDefault();
|
||||
handleCheck(inputValue.trim(), event as unknown as React.MouseEvent<HTMLButtonElement>);
|
||||
handleCheck(
|
||||
inputValue.trim(),
|
||||
event as unknown as React.MouseEvent<HTMLButtonElement>,
|
||||
);
|
||||
} else if (event.key === "Escape" && onClose) {
|
||||
event.preventDefault();
|
||||
handleClose(event as unknown as React.MouseEvent<HTMLButtonElement>);
|
||||
@@ -95,4 +101,3 @@ const ChipContainer = memo<ChipProps>(
|
||||
ChipContainer.displayName = "Chip";
|
||||
|
||||
export default ChipContainer;
|
||||
|
||||
|
||||
@@ -68,4 +68,3 @@ export interface ChipViewProps {
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,32 +42,26 @@ function ChipView({
|
||||
// Palette + state styling based on Figma examples
|
||||
// Use consistent border width to prevent layout shift
|
||||
const borderWidth = isSmall ? "border-[1.25px]" : "border-2";
|
||||
|
||||
let background = "bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
|
||||
let border =
|
||||
`${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`;
|
||||
|
||||
let background =
|
||||
"bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
|
||||
let border = `${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`;
|
||||
let textColor =
|
||||
"text-[color:var(--color-content-default-brand-primary,#fefcc9)]";
|
||||
|
||||
if (isDefault) {
|
||||
if (state === "custom") {
|
||||
background =
|
||||
"bg-[var(--color-surface-default-secondary,#141414)]"; // dark background for custom
|
||||
background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background for custom
|
||||
border = "border-none";
|
||||
textColor =
|
||||
"text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
|
||||
textColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
|
||||
} else if (state === "disabled") {
|
||||
background =
|
||||
"bg-[var(--color-surface-default-secondary,#141414)]"; // dark background
|
||||
background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background
|
||||
border = "border-none";
|
||||
textColor =
|
||||
"text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
|
||||
textColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
|
||||
} else if (isSelected) {
|
||||
background =
|
||||
"bg-[var(--color-surface-inverse-brandaccent,#fdfaa8)]"; // yellow selected
|
||||
background = "bg-[var(--color-surface-inverse-brandaccent,#fdfaa8)]"; // yellow selected
|
||||
border = `${borderWidth} border-[var(--color-border-default-brand-primary,#fdfaa8)]`;
|
||||
textColor =
|
||||
"text-[color:var(--color-content-inverse-primary,black)]";
|
||||
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
|
||||
} else {
|
||||
// Unselected default
|
||||
background =
|
||||
@@ -78,24 +72,20 @@ function ChipView({
|
||||
}
|
||||
} else if (isInverse) {
|
||||
if (state === "disabled") {
|
||||
background =
|
||||
"bg-[var(--color-surface-inverse-tertiary,#d2d2d2)]";
|
||||
background = "bg-[var(--color-surface-inverse-tertiary,#d2d2d2)]";
|
||||
border = "border-none";
|
||||
textColor =
|
||||
"text-[color:var(--color-content-inverse-primary,black)]";
|
||||
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
|
||||
} else if (isSelected) {
|
||||
background =
|
||||
"bg-[var(--color-surface-default-semi-opaque,rgba(0,0,0,0.05))]";
|
||||
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
|
||||
textColor =
|
||||
"text-[color:var(--color-content-inverse-primary,black)]";
|
||||
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
|
||||
} else {
|
||||
// Unselected / custom inverse
|
||||
background =
|
||||
"bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
|
||||
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
|
||||
textColor =
|
||||
"text-[color:var(--color-content-inverse-primary,black)]";
|
||||
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +124,9 @@ function ChipView({
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement | HTMLDivElement>) => {
|
||||
const handleClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement | HTMLDivElement>,
|
||||
) => {
|
||||
if (isDisabled) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
@@ -162,7 +154,9 @@ function ChipView({
|
||||
}}
|
||||
{...sharedA11y}
|
||||
>
|
||||
<div className={`flex items-center ${isSmall ? "gap-[8px]" : "gap-[12px]"}`}>
|
||||
<div
|
||||
className={`flex items-center ${isSmall ? "gap-[8px]" : "gap-[12px]"}`}
|
||||
>
|
||||
{/* Check button */}
|
||||
{onCheck && (
|
||||
<button
|
||||
@@ -208,7 +202,9 @@ function ChipView({
|
||||
placeholder="Type to add"
|
||||
className="bg-transparent border-none outline-none flex-1 min-w-0 font-inter font-normal text-[color:var(--color-content-default-tertiary,#b4b4b4)] placeholder:text-[color:var(--color-content-default-tertiary,#b4b4b4)]"
|
||||
style={{
|
||||
fontSize: isSmall ? "var(--sizing-300,12px)" : "var(--sizing-400,16px)",
|
||||
fontSize: isSmall
|
||||
? "var(--sizing-300,12px)"
|
||||
: "var(--sizing-400,16px)",
|
||||
lineHeight: isSmall ? "16px" : "24px",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -259,9 +255,7 @@ function ChipView({
|
||||
onClick={handleClick}
|
||||
{...sharedA11y}
|
||||
>
|
||||
<span className="flex items-center justify-center">
|
||||
{label}
|
||||
</span>
|
||||
<span className="flex items-center justify-center">{label}</span>
|
||||
{onRemove && !isDisabled && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -284,4 +278,3 @@ function ChipView({
|
||||
ChipView.displayName = "ChipView";
|
||||
|
||||
export default memo(ChipView);
|
||||
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { default } from "./Chip.container";
|
||||
export type { ChipProps } from "./Chip.types";
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
import { memo } from "react";
|
||||
import MultiSelectView from "./MultiSelect.view";
|
||||
import type { MultiSelectProps } from "./MultiSelect.types";
|
||||
import { normalizeMultiSelectSize, normalizeChipPalette } from "../../../../lib/propNormalization";
|
||||
import {
|
||||
normalizeMultiSelectSize,
|
||||
normalizeChipPalette,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
const MultiSelectContainer = memo<MultiSelectProps>(
|
||||
({
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ChipStateValue, ChipPaletteValue } from "../../../../lib/propNormalization";
|
||||
import type {
|
||||
ChipStateValue,
|
||||
ChipPaletteValue,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
export interface ChipOption {
|
||||
id: string;
|
||||
|
||||
@@ -31,7 +31,9 @@ function MultiSelectView({
|
||||
const chipSize = isSmall ? "S" : "M";
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col ${isSmall ? "gap-[var(--measures-spacing-200,8px)]" : "gap-[var(--measures-spacing-300,12px)]"} items-start relative w-full ${className}`}>
|
||||
<div
|
||||
className={`flex flex-col ${isSmall ? "gap-[var(--measures-spacing-200,8px)]" : "gap-[var(--measures-spacing-300,12px)]"} items-start relative w-full ${className}`}
|
||||
>
|
||||
{/* Label using InputLabel component */}
|
||||
{formHeader && label && (
|
||||
<InputLabel
|
||||
@@ -45,7 +47,9 @@ function MultiSelectView({
|
||||
)}
|
||||
|
||||
{/* Chips container */}
|
||||
<div className={`flex flex-wrap ${gapClass} items-center relative shrink-0 w-full`}>
|
||||
<div
|
||||
className={`flex flex-wrap ${gapClass} items-center relative shrink-0 w-full`}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<Chip
|
||||
key={option.id}
|
||||
@@ -74,7 +78,7 @@ function MultiSelectView({
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Add button - Circular button with border (not ghost) when no text, ghost style when text provided */}
|
||||
{/* Add button — icon-only: bordered circle + brand icon (chips stay yellow). With label: Figma 19688:38288 — brand + icon, primary label text, no fill/border. */}
|
||||
{addButton && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -87,18 +91,26 @@ function MultiSelectView({
|
||||
!addButtonText
|
||||
? // Circular button with border (RuleCard style)
|
||||
`bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))] border-[1.25px] ${isInverse ? "border-[var(--color-border-default-primary,#141414)]" : "border-[var(--color-border-default-tertiary,#464646)]"} border-solid flex items-center justify-center ${isSmall ? "size-[30px]" : "size-[40px]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`
|
||||
: // Ghost button style (standalone MultiSelect)
|
||||
`flex ${isSmall ? "gap-[var(--measures-spacing-050,2px)]" : "gap-[var(--measures-spacing-150,6px)]"} items-center justify-center ${isSmall ? "p-[var(--measures-spacing-200,8px)]" : "p-[var(--measures-spacing-300,12px)]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`
|
||||
: // Text add control (default palette: white label + brand “+”; inverse: inverse primary for both)
|
||||
`flex items-center justify-center overflow-hidden rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity ${
|
||||
isSmall
|
||||
? "gap-[var(--measures-spacing-100,4px)] px-[var(--measures-spacing-300,12px)] py-[var(--measures-spacing-200,8px)]"
|
||||
: "gap-[var(--measures-spacing-150,6px)] px-[var(--space-400,16px)] py-[var(--measures-spacing-300,12px)]"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{/* Plus icon */}
|
||||
{/* Plus icon — brand accent; selection chips keep full yellow fill separately */}
|
||||
<svg
|
||||
width={isSmall ? "14" : "20"}
|
||||
height={isSmall ? "14" : "20"}
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={`${isInverse ? "text-[var(--color-content-inverse-primary,black)]" : "text-[var(--color-content-default-brand-primary,#fefcc9)]"} shrink-0`}
|
||||
className={`shrink-0 ${
|
||||
!addButtonText && isInverse
|
||||
? "text-[var(--color-content-inverse-primary,black)]"
|
||||
: "text-[var(--color-content-default-brand-primary,#fefcc9)]"
|
||||
}`}
|
||||
>
|
||||
<path
|
||||
d="M7 3V11M3 7H11"
|
||||
@@ -108,9 +120,14 @@ function MultiSelectView({
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
{/* Text - only show if addButtonText is provided */}
|
||||
{addButtonText && (
|
||||
<span className={`font-inter font-medium ${isSmall ? "text-[length:var(--sizing-300,12px)] leading-[14px]" : "text-[length:var(--sizing-400,16px)] leading-[20px]"} ${isInverse ? "text-[color:var(--color-content-inverse-primary,black)]" : "text-[color:var(--color-content-default-brand-primary,#fefcc9)]"}`}>
|
||||
<span
|
||||
className={`font-inter font-medium ${isSmall ? "text-[length:var(--sizing-300,12px)] leading-[14px]" : "text-[length:var(--sizing-400,16px)] leading-[20px]"} ${
|
||||
isInverse
|
||||
? "text-[color:var(--color-content-inverse-primary,black)]"
|
||||
: "text-[color:var(--color-content-default-primary,white)]"
|
||||
}`}
|
||||
>
|
||||
{addButtonText}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
import { memo, useCallback, useId } from "react";
|
||||
import { RadioButtonView } from "./RadioButton.view";
|
||||
import type { RadioButtonProps } from "./RadioButton.types";
|
||||
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
|
||||
import {
|
||||
normalizeMode,
|
||||
normalizeState,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
const RadioButtonContainer = ({
|
||||
checked = false,
|
||||
@@ -22,10 +25,10 @@ const RadioButtonContainer = ({
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const mode = normalizeMode(modeProp);
|
||||
const state = normalizeState(stateProp);
|
||||
|
||||
|
||||
// If state is "selected", it means checked in Figma terms
|
||||
const normalizedState = state === "selected" || checked ? "selected" : state;
|
||||
|
||||
|
||||
const isInverse = mode === "inverse";
|
||||
const isStandard = mode === "standard";
|
||||
|
||||
@@ -42,7 +45,9 @@ const RadioButtonContainer = ({
|
||||
duration-200
|
||||
ease-in-out
|
||||
p-[4px]
|
||||
`.trim().replace(/\s+/g, " ");
|
||||
`
|
||||
.trim()
|
||||
.replace(/\s+/g, " ");
|
||||
|
||||
// Get box styles based on mode and checked status per Figma designs
|
||||
const getBoxStyles = (): string => {
|
||||
@@ -55,12 +60,12 @@ const RadioButtonContainer = ({
|
||||
const defaultBorder = checked
|
||||
? "border-[var(--color-border-default-brand-primary,#fdfaa8)]"
|
||||
: "border-[var(--color-border-default-tertiary,#464646)]";
|
||||
|
||||
|
||||
// When focused and checked, border should be invert tertiary (#2d2d2d) per Figma
|
||||
const focusBorder = checked
|
||||
? "focus:border-[var(--color-content-invert-tertiary,#2d2d2d)]"
|
||||
: "focus:border-[var(--color-border-default-tertiary,#464646)]";
|
||||
|
||||
|
||||
return `${baseBox} bg-[var(--color-surface-default-primary)] border border-solid ${defaultBorder} hover:border-[var(--color-border-default-brand-primary,#fdfaa8)] ${focusBorder} focus:shadow-[0px_0px_0px_2px_var(--color-border-invert-primary,white),0px_0px_0px_4px_var(--color-border-default-primary,#141414)] focus:outline-none`;
|
||||
}
|
||||
|
||||
@@ -73,15 +78,16 @@ const RadioButtonContainer = ({
|
||||
const defaultBorder = checked
|
||||
? "border-[var(--color-border-default-brand-primary,#fdfaa8)]"
|
||||
: "border-[var(--color-border-invert-primary,white)]";
|
||||
|
||||
|
||||
// Hover border: inverse brand primary for both selected and unselected per Figma
|
||||
const hoverBorder = "hover:border-[var(--color-border-invert-brand-primary,#6c6701)]";
|
||||
|
||||
const hoverBorder =
|
||||
"hover:border-[var(--color-border-invert-brand-primary,#6c6701)]";
|
||||
|
||||
// Focus border: when focused and checked, border should be white per Figma
|
||||
const focusBorder = checked
|
||||
? "focus:border-[var(--color-border-invert-primary,white)]"
|
||||
: "focus:border-[var(--color-border-invert-primary,white)]";
|
||||
|
||||
|
||||
return `${baseBox} bg-transparent border border-solid ${defaultBorder} ${hoverBorder} ${focusBorder} focus:shadow-[0px_0px_0px_2px_var(--color-border-default-primary,#141414),0px_0px_0px_4px_var(--color-border-invert-primary,white)] focus:outline-none`;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ export function RadioButtonView({
|
||||
checked && mode === "standard"
|
||||
? "bg-[var(--color-content-default-brand-primary,#fefcc9)] group-hover:!bg-[#333000]"
|
||||
: checked && mode === "inverse"
|
||||
? "bg-[var(--color-content-default-primary,#000000)]"
|
||||
: "bg-transparent"
|
||||
? "bg-[var(--color-content-default-primary,#000000)]"
|
||||
: "bg-transparent"
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
import { memo, useCallback, useId } from "react";
|
||||
import { RadioGroupView } from "./RadioGroup.view";
|
||||
import type { RadioGroupProps } from "./RadioGroup.types";
|
||||
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
|
||||
import {
|
||||
normalizeMode,
|
||||
normalizeState,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
const RadioGroupContainer = ({
|
||||
name,
|
||||
@@ -19,10 +22,11 @@ const RadioGroupContainer = ({
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const mode = normalizeMode(modeProp);
|
||||
// Normalize state, but handle "With Subtext" separately (it's represented by options with subtext)
|
||||
const state = typeof stateProp === "string" &&
|
||||
const state =
|
||||
typeof stateProp === "string" &&
|
||||
(stateProp.toLowerCase() === "with subtext" || stateProp === "With Subtext")
|
||||
? "default" // "With Subtext" is handled via RadioOption.subtext, use default state
|
||||
: normalizeState(stateProp);
|
||||
? "default" // "With Subtext" is handled via RadioOption.subtext, use default state
|
||||
: normalizeState(stateProp);
|
||||
// Generate unique ID for accessibility if not provided
|
||||
const generatedId = useId();
|
||||
const groupId = name || `radio-group-${generatedId}`;
|
||||
|
||||
@@ -24,10 +24,7 @@ export function RadioGroupView({
|
||||
// If there's subtext, render radio button without label and handle layout separately
|
||||
if (option.subtext) {
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
className="flex gap-[8px] items-start"
|
||||
>
|
||||
<div key={option.value} className="flex gap-[8px] items-start">
|
||||
<RadioButton
|
||||
checked={isSelected}
|
||||
mode={mode}
|
||||
|
||||
@@ -16,7 +16,11 @@ import React, {
|
||||
import { useClickOutside } from "../../../hooks";
|
||||
import { SelectInputView } from "./SelectInput.view";
|
||||
import type { SelectInputProps } from "./SelectInput.types";
|
||||
import { normalizeState, normalizeSmallMediumLargeSize, normalizeLabelVariant } from "../../../../lib/propNormalization";
|
||||
import {
|
||||
normalizeState,
|
||||
normalizeSmallMediumLargeSize,
|
||||
normalizeLabelVariant,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
||||
(
|
||||
@@ -46,23 +50,28 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
||||
ref,
|
||||
) => {
|
||||
// Determine if label should be shown
|
||||
const shouldShowLabel = showLabel !== undefined ? showLabel : (labelText !== undefined);
|
||||
|
||||
const shouldShowLabel =
|
||||
showLabel !== undefined ? showLabel : labelText !== undefined;
|
||||
|
||||
// Normalize state - handle "state5" as disabled
|
||||
let normalizedState = externalStateProp;
|
||||
if (normalizedState === "state5" || normalizedState === "State5") {
|
||||
normalizedState = "default"; // Map to default, disabled prop handles the disabled state
|
||||
}
|
||||
const externalState = normalizeState(normalizedState);
|
||||
|
||||
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
// Note: labelVariant and size are normalized for future use but not yet implemented in the view
|
||||
const _labelVariant = labelVariantProp ? normalizeLabelVariant(labelVariantProp) : undefined;
|
||||
const _size = sizeProp ? normalizeSmallMediumLargeSize(sizeProp) : undefined;
|
||||
const _labelVariant = labelVariantProp
|
||||
? normalizeLabelVariant(labelVariantProp)
|
||||
: undefined;
|
||||
const _size = sizeProp
|
||||
? normalizeSmallMediumLargeSize(sizeProp)
|
||||
: undefined;
|
||||
// Mark as intentionally unused for future implementation
|
||||
void _labelVariant;
|
||||
void _size;
|
||||
|
||||
|
||||
const generatedId = useId();
|
||||
const selectId = id || `select-input-${generatedId}`;
|
||||
const labelId = `${selectId}-label`;
|
||||
@@ -73,11 +82,14 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
||||
|
||||
// Internal state management: track if focused and how (mouse vs keyboard)
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [focusMethod, setFocusMethod] = useState<"mouse" | "keyboard" | null>(null);
|
||||
const [focusMethod, setFocusMethod] = useState<"mouse" | "keyboard" | null>(
|
||||
null,
|
||||
);
|
||||
const wasMouseDownRef = useRef(false);
|
||||
|
||||
// Determine if we should auto-manage focus (only when state is "default" or undefined)
|
||||
const shouldAutoManageFocus = externalState === "default" || externalState === undefined;
|
||||
const shouldAutoManageFocus =
|
||||
externalState === "default" || externalState === undefined;
|
||||
|
||||
// Sync internal state with external value prop
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,8 +7,18 @@ export interface SelectOptionData {
|
||||
|
||||
import type { StateValue } from "../../../../lib/propNormalization";
|
||||
|
||||
export type SelectInputLabelVariantValue = "default" | "horizontal" | "Default" | "Horizontal";
|
||||
export type SelectInputSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large";
|
||||
export type SelectInputLabelVariantValue =
|
||||
| "default"
|
||||
| "horizontal"
|
||||
| "Default"
|
||||
| "Horizontal";
|
||||
export type SelectInputSizeValue =
|
||||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "Small"
|
||||
| "Medium"
|
||||
| "Large";
|
||||
|
||||
export interface SelectInputProps {
|
||||
id?: string;
|
||||
|
||||
@@ -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 && (
|
||||
<div className="flex flex-wrap gap-[var(--measures-spacing-200,4px_8px)] items-baseline pr-[var(--measures-spacing-100,4px)] relative shrink-0 w-full">
|
||||
<div className="flex gap-[var(--measures-spacing-050,2px)] items-center relative shrink-0">
|
||||
<label
|
||||
id={labelId}
|
||||
className={labelClasses}
|
||||
>
|
||||
<label id={labelId} className={labelClasses}>
|
||||
{label}
|
||||
</label>
|
||||
{asterisk && (
|
||||
@@ -155,6 +155,7 @@ export function SelectInputView({
|
||||
)}
|
||||
{iconHelp && (
|
||||
<div className="relative shrink-0 size-[12px]">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.ICON_HELP)}
|
||||
alt="Help"
|
||||
@@ -186,8 +187,10 @@ export function SelectInputView({
|
||||
onFocus={onButtonFocus}
|
||||
onBlur={onButtonBlur}
|
||||
>
|
||||
<span className={`flex-1 text-left ${iconRight ? "pr-[32px]" : ""} ${textColorClass}`}>
|
||||
{textData ? displayText : placeholder}
|
||||
<span
|
||||
className={`flex-1 text-left ${iconRight ? "pr-[32px]" : ""} ${textColorClass}`}
|
||||
>
|
||||
{textData ? displayText : _placeholder}
|
||||
</span>
|
||||
{iconRight && (
|
||||
<div className="flex items-center justify-center shrink-0">
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
export type SelectOptionSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large";
|
||||
export type SelectOptionSizeValue =
|
||||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "Small"
|
||||
| "Medium"
|
||||
| "Large";
|
||||
|
||||
export interface SelectOptionProps {
|
||||
children?: React.ReactNode;
|
||||
|
||||
@@ -17,7 +17,7 @@ const SwitchContainer = memo(
|
||||
className = "",
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const state = normalizeState(stateProp);
|
||||
|
||||
|
||||
@@ -4,7 +4,12 @@ import { memo, forwardRef } from "react";
|
||||
import { useComponentId, useFormField } from "../../../hooks";
|
||||
import { TextAreaView } from "./TextArea.view";
|
||||
import type { TextAreaProps } from "./TextArea.types";
|
||||
import { normalizeInputState, normalizeSmallMediumLargeSize, normalizeLabelVariant } from "../../../../lib/propNormalization";
|
||||
import {
|
||||
normalizeInputState,
|
||||
normalizeSmallMediumLargeSize,
|
||||
normalizeLabelVariant,
|
||||
normalizeTextAreaAppearance,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
(
|
||||
@@ -27,6 +32,7 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
textHint = false,
|
||||
formHeader = true,
|
||||
showHelpIcon = false,
|
||||
appearance: appearanceProp = "default",
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -35,6 +41,7 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
const size = normalizeSmallMediumLargeSize(sizeProp);
|
||||
const labelVariant = normalizeLabelVariant(labelVariantProp);
|
||||
const state = normalizeInputState(stateProp);
|
||||
const appearance = normalizeTextAreaAppearance(appearanceProp);
|
||||
// Generate unique ID for accessibility if not provided
|
||||
const { id: textareaId, labelId } = useComponentId("textarea", id);
|
||||
|
||||
@@ -74,11 +81,26 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
},
|
||||
};
|
||||
|
||||
// State styles
|
||||
// State styles (embedded: Figma 20736-12668 – borderless, darker grey block, white text)
|
||||
const getStateStyles = (): {
|
||||
textarea: string;
|
||||
label: string;
|
||||
} => {
|
||||
if (appearance === "embedded") {
|
||||
if (disabled) {
|
||||
return {
|
||||
textarea:
|
||||
"border-0 bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)] cursor-not-allowed opacity-60",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
}
|
||||
return {
|
||||
textarea:
|
||||
"border-0 bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-default-tertiary)] focus:ring-inset",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
return {
|
||||
textarea:
|
||||
@@ -138,8 +160,8 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
: `${currentSize.label} font-inter`;
|
||||
|
||||
const textareaClasses = `
|
||||
w-full border transition-all duration-200 ease-in-out
|
||||
focus:outline-none focus:ring-0 resize-none
|
||||
scrollbar-design w-full transition-all duration-200 ease-in-out resize-none
|
||||
${appearance === "embedded" ? "rounded-[var(--radius-300,12px)]" : "border"}
|
||||
${currentSize.textarea}
|
||||
${stateStyles.textarea}
|
||||
${className}
|
||||
@@ -180,6 +202,7 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
textHint={textHint}
|
||||
formHeader={formHeader}
|
||||
showHelpIcon={showHelpIcon}
|
||||
appearance={appearance}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import type { InputStateValue } from "../../../../lib/propNormalization";
|
||||
|
||||
export type TextAreaSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large";
|
||||
export type TextAreaLabelVariantValue = "default" | "horizontal" | "Default" | "Horizontal";
|
||||
export type TextAreaSizeValue =
|
||||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "Small"
|
||||
| "Medium"
|
||||
| "Large";
|
||||
export type TextAreaLabelVariantValue =
|
||||
| "default"
|
||||
| "horizontal"
|
||||
| "Default"
|
||||
| "Horizontal";
|
||||
|
||||
export type TextAreaAppearanceValue =
|
||||
| "default"
|
||||
| "embedded"
|
||||
| "Default"
|
||||
| "Embedded";
|
||||
|
||||
export interface TextAreaProps extends Omit<
|
||||
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
@@ -47,6 +63,12 @@ export interface TextAreaProps extends Omit<
|
||||
* @default false
|
||||
*/
|
||||
showHelpIcon?: boolean;
|
||||
/**
|
||||
* Visual appearance. "embedded" matches Create modal sections (Figma 20736-12668):
|
||||
* borderless, darker grey background, white text. "default" is standard bordered input.
|
||||
* @default "default"
|
||||
*/
|
||||
appearance?: TextAreaAppearanceValue;
|
||||
}
|
||||
|
||||
export interface TextAreaViewProps {
|
||||
@@ -73,4 +95,5 @@ export interface TextAreaViewProps {
|
||||
textHint?: boolean;
|
||||
formHeader?: boolean;
|
||||
showHelpIcon?: boolean;
|
||||
appearance?: "default" | "embedded";
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@ export const TextAreaView = forwardRef<HTMLTextAreaElement, TextAreaViewProps>(
|
||||
textHint = false,
|
||||
formHeader = true,
|
||||
showHelpIcon = false,
|
||||
appearance: _appearance,
|
||||
// Component-only props: do not pass to DOM
|
||||
size: _size,
|
||||
labelVariant: _labelVariant,
|
||||
state: _state,
|
||||
error: _error,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -42,6 +48,7 @@ export const TextAreaView = forwardRef<HTMLTextAreaElement, TextAreaViewProps>(
|
||||
</label>
|
||||
{showHelpIcon && (
|
||||
<div className="relative shrink-0 size-[12px]">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.ICON_HELP)}
|
||||
alt="Help"
|
||||
|
||||
@@ -4,12 +4,16 @@ import { memo, forwardRef, useState, useRef } from "react";
|
||||
import { useComponentId, useFormField } from "../../../hooks";
|
||||
import { TextInputView } from "./TextInput.view";
|
||||
import type { TextInputProps } from "./TextInput.types";
|
||||
import { normalizeInputState } from "../../../../lib/propNormalization";
|
||||
import {
|
||||
normalizeInputState,
|
||||
normalizeTextInputSize,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
|
||||
(
|
||||
{
|
||||
state: externalStateProp = "default",
|
||||
inputSize: inputSizeProp = "medium",
|
||||
disabled = false,
|
||||
error = false,
|
||||
label,
|
||||
@@ -31,18 +35,22 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
|
||||
) => {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const externalState = normalizeInputState(externalStateProp);
|
||||
|
||||
const inputSize = normalizeTextInputSize(inputSizeProp);
|
||||
|
||||
// Generate unique ID for accessibility if not provided
|
||||
const { id: inputId, labelId } = useComponentId("text-input", id);
|
||||
|
||||
// Internal state management: track if focused and how (mouse vs keyboard)
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [focusMethod, setFocusMethod] = useState<"mouse" | "keyboard" | null>(null);
|
||||
const [focusMethod, setFocusMethod] = useState<"mouse" | "keyboard" | null>(
|
||||
null,
|
||||
);
|
||||
const wasMouseDownRef = useRef(false);
|
||||
|
||||
// Determine if we should auto-manage focus (only when state is "default" or undefined)
|
||||
// If state is "active", "hover", or "focus", respect it and don't override
|
||||
const shouldAutoManageFocus = externalState === "default" || externalState === undefined;
|
||||
const shouldAutoManageFocus =
|
||||
externalState === "default" || externalState === undefined;
|
||||
|
||||
// Determine actual state:
|
||||
// - Active: when clicked (mouse focus)
|
||||
@@ -59,13 +67,21 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
|
||||
// Determine if input is filled (has value)
|
||||
const isFilled = Boolean(value && value.trim().length > 0);
|
||||
|
||||
// Fixed size styles (medium only per Figma designs)
|
||||
const sizeStyles = {
|
||||
input: "h-[40px] px-[12px] py-[8px] text-[16px]",
|
||||
label: "text-[14px] leading-[20px] font-medium",
|
||||
container: "gap-[8px]",
|
||||
radius: "var(--measures-radius-200,8px)",
|
||||
};
|
||||
// Size styles based on inputSize prop
|
||||
const sizeStyles =
|
||||
inputSize === "small"
|
||||
? {
|
||||
input: "h-[32px] px-[10px] py-[6px] text-[14px]",
|
||||
label: "text-[12px] leading-[16px] font-medium",
|
||||
container: "gap-[6px]",
|
||||
radius: "var(--measures-radius-200,8px)",
|
||||
}
|
||||
: {
|
||||
input: "h-[40px] px-[12px] py-[8px] text-[16px]",
|
||||
label: "text-[14px] leading-[20px] font-medium",
|
||||
container: "gap-[8px]",
|
||||
radius: "var(--measures-radius-200,8px)",
|
||||
};
|
||||
|
||||
// State styles based on Figma designs
|
||||
const getStateStyles = (): {
|
||||
@@ -158,17 +174,20 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
|
||||
: "text-[var(--color-content-default-tertiary,#b4b4b4)]";
|
||||
|
||||
// Form field handlers with disabled state handling
|
||||
const { handleChange, handleBlur } = useFormField<HTMLInputElement>(disabled, {
|
||||
onChange,
|
||||
onBlur: (e) => {
|
||||
if (shouldAutoManageFocus) {
|
||||
setIsFocused(false);
|
||||
setFocusMethod(null);
|
||||
wasMouseDownRef.current = false;
|
||||
}
|
||||
onBlur?.(e);
|
||||
const { handleChange, handleBlur } = useFormField<HTMLInputElement>(
|
||||
disabled,
|
||||
{
|
||||
onChange,
|
||||
onBlur: (e) => {
|
||||
if (shouldAutoManageFocus) {
|
||||
setIsFocused(false);
|
||||
setFocusMethod(null);
|
||||
wasMouseDownRef.current = false;
|
||||
}
|
||||
onBlur?.(e);
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
// Handle mouse down to detect mouse clicks
|
||||
const handleMouseDown = () => {
|
||||
@@ -180,19 +199,19 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
|
||||
// Custom focus handler to detect mouse vs keyboard
|
||||
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (disabled) return;
|
||||
|
||||
|
||||
// Detect if focus came from keyboard (Tab) or mouse (click)
|
||||
// If mouseDown was detected before focus, it's a mouse click (active)
|
||||
// Otherwise, it's keyboard navigation (focus)
|
||||
const method = wasMouseDownRef.current ? "mouse" : "keyboard";
|
||||
|
||||
|
||||
if (shouldAutoManageFocus) {
|
||||
setIsFocused(true);
|
||||
setFocusMethod(method);
|
||||
// Reset mouse down flag after focus is processed
|
||||
wasMouseDownRef.current = false;
|
||||
}
|
||||
|
||||
|
||||
onFocus?.(e);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { InputStateValue } from "../../../../lib/propNormalization";
|
||||
|
||||
export type TextInputSizeValue = "small" | "medium" | "Small" | "Medium";
|
||||
|
||||
export interface TextInputProps extends Omit<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
"size" | "onChange" | "onFocus" | "onBlur"
|
||||
@@ -9,6 +11,12 @@ export interface TextInputProps extends Omit<
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
*/
|
||||
state?: InputStateValue;
|
||||
/**
|
||||
* Size variant. Accepts both PascalCase (Figma) and lowercase (codebase).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
* @default "medium"
|
||||
*/
|
||||
inputSize?: TextInputSizeValue;
|
||||
disabled?: boolean;
|
||||
error?: boolean;
|
||||
label?: string;
|
||||
@@ -21,9 +29,10 @@ export interface TextInputProps extends Omit<
|
||||
showHelpIcon?: boolean;
|
||||
/**
|
||||
* Whether to show hint text below input (Figma prop).
|
||||
* Can be a boolean or a string to display custom text (e.g., character count).
|
||||
* @default false
|
||||
*/
|
||||
textHint?: boolean;
|
||||
textHint?: boolean | string;
|
||||
/**
|
||||
* Whether to show form header (label and help icon) above input (Figma prop).
|
||||
* @default true
|
||||
@@ -55,6 +64,6 @@ export interface TextInputViewProps {
|
||||
isFilled?: boolean;
|
||||
inputWrapperClasses?: string;
|
||||
focusRingClasses?: string;
|
||||
textHint?: boolean;
|
||||
textHint?: boolean | string;
|
||||
formHeader?: boolean;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
|
||||
</label>
|
||||
{showHelpIcon && (
|
||||
<div className="relative shrink-0 size-[12px]">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.ICON_HELP)}
|
||||
alt="Help"
|
||||
@@ -80,7 +81,7 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
|
||||
{textHint && (
|
||||
<div className="flex items-start relative shrink-0 w-full">
|
||||
<p className="flex-[1_0_0] font-inter font-normal leading-[16px] min-h-px min-w-px relative text-[color:var(--color-content-default-tertiary,#b4b4b4)] text-[length:var(--sizing-300,12px)]">
|
||||
Hint text here
|
||||
{typeof textHint === "string" ? textHint : "Hint text here"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
import { memo, useCallback, useId, forwardRef } from "react";
|
||||
import { ToggleGroupView } from "./ToggleGroup.view";
|
||||
import type { ToggleGroupProps } from "./ToggleGroup.types";
|
||||
import { normalizeToggleState, normalizeToggleGroupPosition } from "../../../../lib/propNormalization";
|
||||
import {
|
||||
normalizeToggleState,
|
||||
normalizeToggleGroupPosition,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
const ToggleGroupContainer = memo(
|
||||
forwardRef<HTMLButtonElement, ToggleGroupProps>((props, _ref) => {
|
||||
@@ -19,7 +22,7 @@ const ToggleGroupContainer = memo(
|
||||
onBlur,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const position = normalizeToggleGroupPosition(positionProp);
|
||||
const state = normalizeToggleState(stateProp);
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { StateValue } from "../../../../lib/propNormalization";
|
||||
|
||||
export type ToggleGroupPositionValue = "left" | "middle" | "right" | "Left" | "Middle" | "Right";
|
||||
export type ToggleGroupPositionValue =
|
||||
| "left"
|
||||
| "middle"
|
||||
| "right"
|
||||
| "Left"
|
||||
| "Middle"
|
||||
| "Right";
|
||||
|
||||
export interface ToggleGroupProps extends Omit<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import UploadView from "./Upload.view";
|
||||
import type { UploadProps } from "./Upload.types";
|
||||
|
||||
const UploadContainer = memo<UploadProps>(
|
||||
({ active = true, label, showHelpIcon = true, onClick, className = "" }) => {
|
||||
return (
|
||||
<UploadView
|
||||
active={active}
|
||||
label={label}
|
||||
showHelpIcon={showHelpIcon}
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
UploadContainer.displayName = "Upload";
|
||||
|
||||
export default UploadContainer;
|
||||
@@ -0,0 +1,34 @@
|
||||
export interface UploadProps {
|
||||
/**
|
||||
* Whether the upload component is in active state.
|
||||
* When active, button has white background with black text.
|
||||
* When inactive, button has dark background with gray text.
|
||||
* @default true
|
||||
*/
|
||||
active?: boolean;
|
||||
/**
|
||||
* Label text displayed above the upload component
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* Whether to show help icon next to label
|
||||
* @default true
|
||||
*/
|
||||
showHelpIcon?: boolean;
|
||||
/**
|
||||
* Callback when upload button is clicked
|
||||
*/
|
||||
onClick?: () => void;
|
||||
/**
|
||||
* Additional CSS classes
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface UploadViewProps {
|
||||
active: boolean;
|
||||
label?: string;
|
||||
showHelpIcon: boolean;
|
||||
onClick?: () => void;
|
||||
className: string;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import InputLabel from "../../utility/InputLabel";
|
||||
import type { UploadViewProps } from "./Upload.types";
|
||||
|
||||
function UploadView({
|
||||
active = true,
|
||||
label,
|
||||
showHelpIcon = true,
|
||||
onClick,
|
||||
className = "",
|
||||
}: UploadViewProps) {
|
||||
const isActive = active;
|
||||
|
||||
// Button styles based on active state
|
||||
const buttonBgClass = isActive
|
||||
? "bg-[var(--color-surface-invert-primary,white)]"
|
||||
: "bg-[var(--color-surface-default-secondary,#141414)]";
|
||||
|
||||
const buttonTextColor = isActive
|
||||
? "text-[color:var(--color-content-invert-primary,black)]"
|
||||
: "text-[color:var(--color-content-invert-tertiary,#2d2d2d)]";
|
||||
|
||||
// Description text color based on active state
|
||||
const descriptionTextColor = isActive
|
||||
? "text-[color:var(--color-content-default-primary,white)]"
|
||||
: "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
|
||||
|
||||
// Icon color based on active state
|
||||
const iconColor = isActive
|
||||
? "text-[color:var(--color-content-invert-primary,black)]"
|
||||
: "text-[color:var(--color-content-invert-tertiary,#2d2d2d)]";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col gap-[var(--measures-spacing-300,12px)] items-start relative w-full ${className}`}
|
||||
>
|
||||
{/* Label using InputLabel component */}
|
||||
{label && (
|
||||
<InputLabel
|
||||
label={label}
|
||||
helpIcon={showHelpIcon}
|
||||
asterisk={false}
|
||||
helperText={false}
|
||||
size="S"
|
||||
palette="Default"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Upload container */}
|
||||
<div className="bg-[var(--color-surface-default-secondary,#141414)] flex gap-[24px] items-center justify-center px-[var(--measures-spacing-600,24px)] py-[var(--measures-spacing-1200,48px)] rounded-[var(--measures-radius-200,8px)] shrink-0 w-full">
|
||||
{/* Upload button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`${buttonBgClass} flex gap-[var(--measures-spacing-150,6px)] items-center justify-center overflow-clip p-[var(--measures-spacing-300,12px)] rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`}
|
||||
aria-label="Upload"
|
||||
>
|
||||
{/* Upload icon */}
|
||||
<div className={`relative shrink-0 size-[20px] ${iconColor}`}>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="size-full"
|
||||
>
|
||||
<path
|
||||
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<polyline
|
||||
points="17 8 12 3 7 8"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
y1="3"
|
||||
x2="12"
|
||||
y2="15"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{/* Button text */}
|
||||
<div
|
||||
className={`flex flex-col font-inter font-medium justify-center leading-[0] relative shrink-0 text-[length:var(--sizing-400,16px)] whitespace-nowrap ${buttonTextColor}`}
|
||||
>
|
||||
<p className="leading-[20px]">Upload</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Description text */}
|
||||
<div
|
||||
className={`flex flex-[1_0_0] flex-col font-inter font-normal h-[32px] justify-center leading-[0] min-h-px min-w-px relative text-[length:var(--sizing-350,14px)] ${descriptionTextColor}`}
|
||||
>
|
||||
<p className="leading-[20px] whitespace-pre-wrap">
|
||||
Add images, PDFs, and other files to the policy
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
UploadView.displayName = "UploadView";
|
||||
|
||||
export default memo(UploadView);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./Upload.container";
|
||||
export type { UploadProps } from "./Upload.types";
|
||||
@@ -1,7 +1,15 @@
|
||||
import { memo } from "react";
|
||||
import { normalizeSize } from "../../../lib/propNormalization";
|
||||
|
||||
export type AvatarSizeValue = "small" | "medium" | "large" | "xlarge" | "Small" | "Medium" | "Large" | "XLarge";
|
||||
export type AvatarSizeValue =
|
||||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "xlarge"
|
||||
| "Small"
|
||||
| "Medium"
|
||||
| "Large"
|
||||
| "XLarge";
|
||||
|
||||
interface AvatarProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
src: string;
|
||||
@@ -19,7 +27,8 @@ const Avatar = memo<AvatarProps>(
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const size = normalizeSize(sizeProp, "small");
|
||||
const sizeStyles: Record<string, string> = {
|
||||
small: "w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)] border-[1.5px] border-[#FFFFFF4D] border-solid",
|
||||
small:
|
||||
"w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)] border-[1.5px] border-[#FFFFFF4D] border-solid",
|
||||
medium: "w-[var(--spacing-scale-018)] h-[var(--spacing-scale-018)]",
|
||||
large: "w-[var(--spacing-scale-024)] h-[var(--spacing-scale-024)]",
|
||||
xlarge: "w-[var(--spacing-scale-032)] h-[var(--spacing-scale-032)]",
|
||||
@@ -27,7 +36,10 @@ const Avatar = memo<AvatarProps>(
|
||||
|
||||
const baseStyles = `rounded-[var(--radius-measures-radius-full)] object-cover box-border ${sizeStyles[size]} ${className}`;
|
||||
|
||||
return <img src={src} alt={alt} className={baseStyles} {...props} />;
|
||||
return (
|
||||
/* eslint-disable-next-line @next/next/no-img-element -- avatar image from URL */
|
||||
<img src={src} alt={alt} className={baseStyles} {...props} />
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { memo } from "react";
|
||||
import Link from "next/link";
|
||||
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
||||
|
||||
interface LogoProps {
|
||||
size?:
|
||||
| "default"
|
||||
| "footer"
|
||||
| "createFlow"
|
||||
| "topNavFolderTop"
|
||||
| "topNavHeader";
|
||||
showText?: boolean;
|
||||
}
|
||||
|
||||
interface SizeConfig {
|
||||
containerHeight: string;
|
||||
gap: string;
|
||||
textSize: string;
|
||||
lineHeight: string;
|
||||
iconSize: string;
|
||||
}
|
||||
|
||||
const Logo = memo<LogoProps>(({ size = "default", showText = true }) => {
|
||||
// Size configurations
|
||||
const sizes: Record<string, SizeConfig> = {
|
||||
default: {
|
||||
containerHeight: "h-[41px]",
|
||||
gap: "gap-[8.28px]",
|
||||
textSize: "text-[21.97px]",
|
||||
lineHeight: "leading-[27.05px]",
|
||||
iconSize: "w-[27.05px] h-[27.05px]",
|
||||
},
|
||||
footer: {
|
||||
containerHeight: "h-[41px] sm:h-[calc(40px*1.37)] lg:h-[calc(40px*2.05)]",
|
||||
gap: "gap-[8.28px] sm:gap-[calc(8px*1.37)] lg:gap-[calc(8px*2.05)]",
|
||||
textSize: "text-[21.97px] sm:text-[calc(21.97px*1.37)] lg:text-[calc(21.97px*2.05)]",
|
||||
lineHeight: "leading-[27.05px] sm:leading-[calc(27.05px*1.37)] lg:leading-[calc(27.05px*2.05)]",
|
||||
iconSize: "w-[27.05px] h-[27.05px] sm:w-[calc(27.05px*1.37)] sm:h-[calc(27.05px*1.37)] lg:w-[calc(27.05px*2.05)] lg:h-[calc(27.05px*2.05)]",
|
||||
},
|
||||
createFlow: {
|
||||
containerHeight: "h-[30px] md:h-[41px]",
|
||||
gap: "gap-[6px] md:gap-[8.28px]",
|
||||
textSize: "text-[16.48px] md:text-[21.97px]",
|
||||
lineHeight: "leading-[20.28px] md:leading-[27.05px]",
|
||||
iconSize: "w-[20.28px] h-[20.28px] md:w-[27.05px] md:h-[27.05px]",
|
||||
},
|
||||
topNavFolderTop: {
|
||||
containerHeight: "h-[14.11px] sm:h-[21.06px] md:h-[32.24px] lg:h-[28px] xl:h-[36px]",
|
||||
gap: "gap-0 sm:gap-[3.19px] md:gap-[4.89px] lg:gap-[6.55px] xl:gap-[8.64px]",
|
||||
textSize: "text-[11.57px] sm:text-[11.69px] md:text-[17.89px] lg:text-[21.97px] xl:text-[29.01px]",
|
||||
lineHeight: "leading-[14.24px] sm:leading-[14.39px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[35.7px]",
|
||||
iconSize: "w-[14.11px] h-[14.11px] sm:w-[14.39px] sm:h-[14.39px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[35.7px] xl:h-[35.7px]",
|
||||
},
|
||||
topNavHeader: {
|
||||
containerHeight: "h-[20.85px] sm:h-[20.85px] md:h-[17.91px] lg:h-[28px] xl:h-[34px]",
|
||||
gap: "gap-0 sm:gap-[4.21px] md:gap-[6.51px] lg:gap-[6.55px] xl:gap-[8.19px]",
|
||||
textSize: "text-[11.57px] sm:text-[11.57px] md:text-[17.89px] lg:text-[21.97px] xl:text-[27.47px]",
|
||||
lineHeight: "leading-[14.24px] sm:leading-[14.24px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[33.81px]",
|
||||
iconSize: "w-[14.24px] h-[14.24px] sm:w-[14.24px] sm:h-[14.24px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[33.81px] xl:h-[33.81px]",
|
||||
},
|
||||
};
|
||||
|
||||
const config = sizes[size || "default"] || sizes.default;
|
||||
|
||||
return (
|
||||
<Link href="/" className="block" aria-label="CommunityRule Logo">
|
||||
<div
|
||||
className={`flex items-center ${config.containerHeight} ${
|
||||
showText ? config.gap : ""
|
||||
} transition-all duration-200 ease-in-out hover:scale-[1.02] cursor-pointer`}
|
||||
>
|
||||
{/* Logo Text - responsive visibility for topNav sizes */}
|
||||
<div
|
||||
className={`font-bricolage-grotesque ${
|
||||
size === "topNavFolderTop"
|
||||
? "text-[var(--color-content-inverse-primary)]"
|
||||
: "text-[var(--color-content-default-primary)]"
|
||||
} ${config.textSize} ${
|
||||
config.lineHeight
|
||||
} font-normal tracking-[0px] transition-colors duration-200 ${
|
||||
size === "topNavFolderTop" || size === "topNavHeader"
|
||||
? showText
|
||||
? "hidden sm:block"
|
||||
: "hidden"
|
||||
: showText
|
||||
? ""
|
||||
: "hidden"
|
||||
}`}
|
||||
aria-label="CommunityRule"
|
||||
>
|
||||
CommunityRule
|
||||
</div>
|
||||
|
||||
{/* Vector Icon */}
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.LOGO)}
|
||||
alt="CommunityRule Logo Icon"
|
||||
width={27.05}
|
||||
height={27.05}
|
||||
className={`flex-shrink-0 ${
|
||||
config.iconSize
|
||||
} transition-all duration-200 ${
|
||||
size === "topNavFolderTop" ? "filter brightness-0" : ""
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
Logo.displayName = "Logo";
|
||||
|
||||
export default Logo;
|
||||
@@ -3,7 +3,10 @@
|
||||
import { memo } from "react";
|
||||
import { AlertView } from "./Alert.view";
|
||||
import type { AlertProps } from "./Alert.types";
|
||||
import { normalizeAlertStatus, normalizeAlertType } from "../../../../lib/propNormalization";
|
||||
import {
|
||||
normalizeAlertStatus,
|
||||
normalizeAlertType,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
const AlertContainer = memo<AlertProps>(
|
||||
({
|
||||
|
||||
@@ -10,6 +10,7 @@ const CreateContainer = memo<CreateProps>(
|
||||
onClose,
|
||||
title,
|
||||
description,
|
||||
headerContent,
|
||||
children,
|
||||
footerContent,
|
||||
showBackButton = true,
|
||||
@@ -113,6 +114,7 @@ const CreateContainer = memo<CreateProps>(
|
||||
onClose={onClose}
|
||||
title={title}
|
||||
description={description}
|
||||
headerContent={headerContent}
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
children={children}
|
||||
footerContent={footerContent}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
export interface CreateProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
/** Default header: title + description. Omit to use title/description. */
|
||||
title?: string;
|
||||
description?: string;
|
||||
/** Custom header slot. When set, replaces title/description for full control. */
|
||||
headerContent?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
footerContent?: React.ReactNode;
|
||||
showBackButton?: boolean;
|
||||
@@ -17,35 +20,12 @@ export interface CreateProps {
|
||||
className?: string;
|
||||
ariaLabel?: string;
|
||||
ariaLabelledBy?: string;
|
||||
/**
|
||||
* Whether to enable Create block array content type (Figma prop).
|
||||
* @default false
|
||||
*/
|
||||
/** Figma / design alignment (unused in implementation). */
|
||||
createBlockArray?: boolean;
|
||||
/**
|
||||
* Whether to enable Text input content type (Figma prop).
|
||||
* @default false
|
||||
*/
|
||||
textInput?: boolean;
|
||||
/**
|
||||
* Whether to enable Text area content type (Figma prop).
|
||||
* @default false
|
||||
*/
|
||||
textArea?: boolean;
|
||||
/**
|
||||
* Whether to enable Multi-select content type (Figma prop).
|
||||
* @default false
|
||||
*/
|
||||
multiSelect?: boolean;
|
||||
/**
|
||||
* Whether to enable Upload content type (Figma prop).
|
||||
* @default false
|
||||
*/
|
||||
upload?: boolean;
|
||||
/**
|
||||
* Whether to enable Proportion content type (Figma prop).
|
||||
* @default false
|
||||
*/
|
||||
proportion?: boolean;
|
||||
}
|
||||
|
||||
@@ -54,6 +34,7 @@ export interface CreateViewProps {
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
headerContent?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
footerContent?: React.ReactNode;
|
||||
showBackButton: boolean;
|
||||
|
||||
@@ -11,6 +11,7 @@ export function CreateView({
|
||||
onClose,
|
||||
title,
|
||||
description,
|
||||
headerContent,
|
||||
children,
|
||||
footerContent,
|
||||
showBackButton,
|
||||
@@ -40,21 +41,23 @@ export function CreateView({
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Create Dialog */}
|
||||
{/* Create Dialog: max-h ensures modal fits viewport; content scrolls inside */}
|
||||
<div
|
||||
ref={createRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={ariaLabel}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
className={`fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-[var(--color-surface-default-primary)] rounded-[var(--radius-500,20px)] shadow-[0px_0px_48px_0px_rgba(0,0,0,0.1)] w-[560px] max-h-[728px] flex flex-col overflow-hidden z-[9999] ${className}`}
|
||||
className={`fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-[var(--color-surface-default-primary)] rounded-[var(--radius-500,20px)] shadow-[0px_0px_48px_0px_rgba(0,0,0,0.1)] w-[560px] max-h-[90vh] flex min-h-0 flex-col overflow-hidden z-[9999] ${className}`}
|
||||
>
|
||||
{/* Header with close buttons */}
|
||||
<ModalHeader onClose={onClose} onMoreOptions={onClose} />
|
||||
|
||||
{/* Header Lockup Section (Sticky) */}
|
||||
{(title || description) && (
|
||||
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0 sticky top-[48px] z-[2]">
|
||||
{/* Header: custom headerContent (when provided) or default title/description */}
|
||||
{headerContent !== undefined ? (
|
||||
<div className="shrink-0">{headerContent}</div>
|
||||
) : title || description ? (
|
||||
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
|
||||
<ContentLockup
|
||||
title={title}
|
||||
description={description}
|
||||
@@ -62,14 +65,14 @@ export function CreateView({
|
||||
alignment="left"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* Content Area (Scrollable) */}
|
||||
<div className="flex flex-col gap-[var(--spacing-scale-024)] px-[24px] pb-[96px] overflow-x-clip overflow-y-auto relative shrink-0 flex-1">
|
||||
{/* Content Area (scrollable when content overflows) */}
|
||||
<div className="scrollbar-design flex min-h-0 flex-1 flex-col gap-[var(--spacing-scale-024)] overflow-x-clip overflow-y-auto px-[24px] pb-6 pt-0">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{/* Footer (always visible at bottom of modal) */}
|
||||
<ModalFooter
|
||||
showBackButton={showBackButton}
|
||||
showNextButton={showNextButton}
|
||||
|
||||
@@ -6,7 +6,13 @@ import type { TooltipProps } from "./Tooltip.types";
|
||||
import { normalizeTooltipPosition } from "../../../../lib/propNormalization";
|
||||
|
||||
const TooltipContainer = memo<TooltipProps>(
|
||||
({ children, text, position: positionProp = "top", className = "", disabled = false }) => {
|
||||
({
|
||||
children,
|
||||
text,
|
||||
position: positionProp = "top",
|
||||
className = "",
|
||||
disabled = false,
|
||||
}) => {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const position = normalizeTooltipPosition(positionProp);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { memo } from "react";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import Link from "next/link";
|
||||
import Logo from "../icons/Logo";
|
||||
import Logo from "../asset/logo";
|
||||
import Separator from "../utility/Separator";
|
||||
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
||||
|
||||
@@ -40,7 +40,7 @@ const Footer = memo(() => {
|
||||
lg:gap-[var(--spacing-measures-spacing-060,60px)]"
|
||||
>
|
||||
{/* Logo */}
|
||||
<Logo size="footer" />
|
||||
<Logo size="footer" wordmark />
|
||||
|
||||
{/* Content section */}
|
||||
<div className="flex flex-col items-start w-full gap-[var(--spacing-measures-spacing-048,48px)] sm:flex-row sm:justify-between sm:gap-0">
|
||||
@@ -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 */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.BLUESKY_LOGO)}
|
||||
alt="Bluesky"
|
||||
@@ -82,6 +83,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.gitlab.ariaLabel")}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- social icon */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.GITLAB_ICON)}
|
||||
alt="GitLab"
|
||||
|
||||
@@ -25,7 +25,7 @@ const MenuBar = memo<MenuBarProps>(
|
||||
({ children, className = "", size: sizeProp = "X Small", ...props }) => {
|
||||
const size = normalizeMenuBarSize(sizeProp);
|
||||
const t = useTranslation("menuBar");
|
||||
|
||||
|
||||
// Size styles based on Figma specifications
|
||||
const sizeStyles: Record<
|
||||
"X Small" | "Small" | "Medium" | "Large" | "X Large",
|
||||
|
||||
@@ -32,11 +32,17 @@ const MenuBarItemContainer = memo<MenuBarItemProps>(
|
||||
"X Small" | "Small" | "Medium" | "Large" | "X Large",
|
||||
string
|
||||
> = {
|
||||
"X Small": reducedPadding ? "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]" : "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]",
|
||||
"X Small": reducedPadding
|
||||
? "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]"
|
||||
: "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]",
|
||||
Small: "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]",
|
||||
Medium: reducedPadding ? "px-[var(--spacing-scale-002)] py-[var(--spacing-scale-008)] h-[32px]" : "px-[var(--spacing-scale-008)] py-[var(--spacing-scale-008)] h-[32px]",
|
||||
Large: "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]",
|
||||
"X Large": "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]",
|
||||
Medium: reducedPadding
|
||||
? "px-[var(--spacing-scale-002)] py-[var(--spacing-scale-008)] h-[32px]"
|
||||
: "px-[var(--spacing-scale-008)] py-[var(--spacing-scale-008)] h-[32px]",
|
||||
Large:
|
||||
"px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]",
|
||||
"X Large":
|
||||
"px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]",
|
||||
};
|
||||
|
||||
// Text styles based on Figma specifications
|
||||
@@ -46,41 +52,34 @@ const MenuBarItemContainer = memo<MenuBarItemProps>(
|
||||
> = {
|
||||
"X Small":
|
||||
"font-inter text-[10px] leading-[12px] font-medium tracking-[0%]",
|
||||
Small:
|
||||
"font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
|
||||
Medium:
|
||||
"font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
|
||||
Large:
|
||||
"font-inter text-[16px] leading-[20px] font-medium tracking-[0%]",
|
||||
Small: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
|
||||
Medium: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
|
||||
Large: "font-inter text-[16px] leading-[20px] font-medium tracking-[0%]",
|
||||
"X Large":
|
||||
"font-inter text-[24px] leading-[28px] font-normal tracking-[0%]",
|
||||
};
|
||||
|
||||
// State styles for Default mode (yellow text on dark background)
|
||||
const defaultModeStyles: Record<
|
||||
"default" | "hover" | "selected",
|
||||
string
|
||||
> = {
|
||||
default:
|
||||
"bg-transparent text-[var(--color-content-default-brand-primary,#fefcc9)] hover:bg-[var(--color-gray-800)] hover:text-[var(--color-content-default-brand-primary,#fefcc9)]",
|
||||
hover:
|
||||
"bg-[var(--color-gray-800)] text-[var(--color-content-default-brand-primary,#fefcc9)]",
|
||||
selected:
|
||||
"border border-[var(--color-border-default-brand-primary,#fdfaa8)] text-[var(--color-content-default-brand-primary,#fefcc9)] bg-transparent hover:bg-[var(--color-gray-800)]",
|
||||
};
|
||||
const defaultModeStyles: Record<"default" | "hover" | "selected", string> =
|
||||
{
|
||||
default:
|
||||
"bg-transparent text-[var(--color-content-default-brand-primary,#fefcc9)] hover:bg-[var(--color-gray-800)] hover:text-[var(--color-content-default-brand-primary,#fefcc9)]",
|
||||
hover:
|
||||
"bg-[var(--color-gray-800)] text-[var(--color-content-default-brand-primary,#fefcc9)]",
|
||||
selected:
|
||||
"border border-[var(--color-border-default-brand-primary,#fdfaa8)] text-[var(--color-content-default-brand-primary,#fefcc9)] bg-transparent hover:bg-[var(--color-gray-800)]",
|
||||
};
|
||||
|
||||
// State styles for Inverse mode (black text on yellow background)
|
||||
const inverseModeStyles: Record<
|
||||
"default" | "hover" | "selected",
|
||||
string
|
||||
> = {
|
||||
default:
|
||||
"bg-transparent text-[var(--color-content-inverse-primary,black)] hover:bg-[var(--color-surface-brand-accent,#4d4a00)] hover:text-[var(--color-content-inverse-primary,black)]",
|
||||
hover:
|
||||
"bg-[var(--color-surface-brand-accent,#4d4a00)] text-[var(--color-content-inverse-primary,black)]",
|
||||
selected:
|
||||
"border border-[var(--color-border-default-primary,#141414)] text-[var(--color-content-inverse-primary,black)] bg-transparent hover:bg-[var(--color-surface-brand-accent,#4d4a00)]",
|
||||
};
|
||||
const inverseModeStyles: Record<"default" | "hover" | "selected", string> =
|
||||
{
|
||||
default:
|
||||
"bg-transparent text-[var(--color-content-inverse-primary,black)] hover:bg-[var(--color-surface-brand-accent,#4d4a00)] hover:text-[var(--color-content-inverse-primary,black)]",
|
||||
hover:
|
||||
"bg-[var(--color-surface-brand-accent,#4d4a00)] text-[var(--color-content-inverse-primary,black)]",
|
||||
selected:
|
||||
"border border-[var(--color-border-default-primary,#141414)] text-[var(--color-content-inverse-primary,black)] bg-transparent hover:bg-[var(--color-surface-brand-accent,#4d4a00)]",
|
||||
};
|
||||
|
||||
// Get state styles based on mode
|
||||
const stateStyles =
|
||||
|
||||
@@ -5,14 +5,9 @@ export type MenuBarItemSizeValue =
|
||||
| "Large"
|
||||
| "X Large";
|
||||
|
||||
export type MenuBarItemStateValue =
|
||||
| "default"
|
||||
| "hover"
|
||||
| "selected";
|
||||
export type MenuBarItemStateValue = "default" | "hover" | "selected";
|
||||
|
||||
export type MenuBarItemModeValue =
|
||||
| "default"
|
||||
| "inverse";
|
||||
export type MenuBarItemModeValue = "default" | "inverse";
|
||||
|
||||
export interface MenuBarItemProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
href?: string;
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
import { memo } from "react";
|
||||
import NavigationItemView from "./NavigationItem.view";
|
||||
import type { NavigationItemProps } from "./NavigationItem.types";
|
||||
import { normalizeNavigationItemVariant, normalizeNavigationItemSize } from "../../../../lib/propNormalization";
|
||||
import {
|
||||
normalizeNavigationItemVariant,
|
||||
normalizeNavigationItemSize,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
const NavigationItemContainer = memo<NavigationItemProps>(
|
||||
({
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export type NavigationItemVariantValue = "default" | "Default";
|
||||
export type NavigationItemSizeValue = "default" | "xsmall" | "Default" | "XSmall";
|
||||
export type NavigationItemSizeValue =
|
||||
| "default"
|
||||
| "xsmall"
|
||||
| "Default"
|
||||
| "XSmall";
|
||||
|
||||
export interface NavigationItemProps extends Omit<
|
||||
React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import MenuBarItem from "../MenuBarItem";
|
||||
import Button from "../../buttons/Button";
|
||||
import AvatarContainer from "../../utility/AvatarContainer";
|
||||
import Avatar from "../../icons/Avatar";
|
||||
import Logo from "../../icons/Logo";
|
||||
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
|
||||
import { TopNavView } from "./TopNav.view";
|
||||
import type { TopNavProps, NavSize } from "./TopNav.types";
|
||||
@@ -19,13 +18,9 @@ export const avatarImages = [
|
||||
];
|
||||
|
||||
const TopNavContainer = memo<TopNavProps>(
|
||||
({
|
||||
folderTop = false,
|
||||
loggedIn = false,
|
||||
profile = false,
|
||||
logIn = true,
|
||||
}) => {
|
||||
({ folderTop = false, loggedIn = false, profile = false, logIn = true }) => {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const t = useTranslation("header");
|
||||
|
||||
// Schema markup for site navigation
|
||||
@@ -34,7 +29,9 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
"@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 +51,10 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
|
||||
const renderNavigationItems = (size: NavSize) => {
|
||||
// Map NavSize to Figma MenuBarItem sizes
|
||||
const sizeMap: Record<NavSize, "X Small" | "Small" | "Medium" | "Large" | "X Large"> = {
|
||||
const sizeMap: Record<
|
||||
NavSize,
|
||||
"X Small" | "Small" | "Medium" | "Large" | "X Large"
|
||||
> = {
|
||||
default: "Small",
|
||||
xsmall: "X Small",
|
||||
xsmallUseCases: "X Small",
|
||||
@@ -85,7 +85,10 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
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}
|
||||
</MenuBarItem>
|
||||
@@ -113,7 +116,10 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
|
||||
const renderLoginButton = (size: NavSize) => {
|
||||
// Map NavSize to Figma MenuBarItem sizes
|
||||
const sizeMap: Record<NavSize, "X Small" | "Small" | "Medium" | "Large" | "X Large"> = {
|
||||
const sizeMap: Record<
|
||||
NavSize,
|
||||
"X Small" | "Small" | "Medium" | "Large" | "X Large"
|
||||
> = {
|
||||
default: "Small",
|
||||
xsmall: "X Small",
|
||||
xsmallUseCases: "X Small",
|
||||
@@ -159,7 +165,7 @@ const TopNavContainer = memo<TopNavProps>(
|
||||
size={buttonSize}
|
||||
buttonType={buttonType}
|
||||
palette={palette}
|
||||
href="/create/informational"
|
||||
onClick={() => router.push("/create/informational")}
|
||||
ariaLabel={t("ariaLabels.createNewRule")}
|
||||
>
|
||||
{renderAvatarGroup(containerSize, avatarSize)}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getAssetPath } from "../../../../lib/assetUtils";
|
||||
import MenuBar from "../MenuBar";
|
||||
import type { TopNavViewProps } from "./TopNav.types";
|
||||
|
||||
import Logo from "../../icons/Logo";
|
||||
import Logo from "../../asset/logo";
|
||||
|
||||
function TopNavView({
|
||||
folderTop,
|
||||
@@ -44,7 +44,11 @@ function TopNavView({
|
||||
{/* Header Tab - Yellow tab container with decorative Union images */}
|
||||
<div className="HeaderTab header-breakpoint-transition relative bg-[var(--color-surface-inverse-brand-primary)] rounded-tl-[var(--radius-measures-radius-medium)] rounded-tr-[var(--radius-measures-radius-medium)] sm:rounded-t-[var(--radius-measures-radius-xlarge)] md:rounded-t-[var(--radius-measures-radius-xlarge)] lg:rounded-t-[var(--radius-measures-radius-xlarge)] xl:rounded-t-[var(--radius-measures-radius-xlarge)] pl-[var(--spacing-scale-012)] pr-[var(--spacing-scale-048)] h-[var(--spacing-scale-040)] sm:pl-[var(--spacing-scale-012)] sm:h-[52px] sm:pr-[var(--spacing-scale-006)] md:h-[52px] md:pl-[var(--spacing-scale-024)] md:pr-[var(--spacing-scale-012)] lg:h-[52px] lg:pl-[var(--spacing-scale-024)] lg:pr-[var(--spacing-scale-048)] xl:h-[64px] xl:pl-[var(--spacing-scale-032)] xl:pr-[var(--spacing-scale-120)] md:gap-[var(--spacing-scale-032)] flex-1 min-w-0 min-w-[197px] sm:min-w-0 sm:mr-[var(--spacing-scale-008)] md:mr-[185px] lg:mr-[var(--spacing-scale-024)] xl:mr-[var(--spacing-scale-032)] flex items-center self-end">
|
||||
{/* Logo - Consistent left positioning within HeaderTab */}
|
||||
<Logo size={logoSize} showText={true} />
|
||||
<Logo
|
||||
size={logoSize}
|
||||
wordmark
|
||||
palette={folderTop ? "inverse" : "default"}
|
||||
/>
|
||||
|
||||
{/* XSmall menu bar - positioned next to logo */}
|
||||
<div className="block sm:hidden -me-[2px]">
|
||||
@@ -55,18 +59,21 @@ function TopNavView({
|
||||
</div>
|
||||
|
||||
{/* Decorative Union images for tab appearance */}
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- decorative SVG, not content */}
|
||||
<img
|
||||
src={getAssetPath("assets/Union_xsm.svg")}
|
||||
alt=""
|
||||
role="presentation"
|
||||
className="absolute -bottom-[3px] -right-[52px] w-[61px] h-[24px] sm:w-[61px] sm:h-[31.5px] sm:hidden -z-10"
|
||||
/>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- decorative SVG */}
|
||||
<img
|
||||
src={getAssetPath("assets/Union_sm_md_lg.svg")}
|
||||
alt=""
|
||||
role="presentation"
|
||||
className="absolute -bottom-[3.7px] -right-[53px] w-[61px] h-[24px] sm:w-[61px] sm:h-[31.5px] hidden sm:block xl:hidden -z-10"
|
||||
/>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- decorative SVG */}
|
||||
<img
|
||||
src={getAssetPath("assets/Union_xlg.svg")}
|
||||
alt=""
|
||||
@@ -87,7 +94,9 @@ function TopNavView({
|
||||
|
||||
{/* 640-1023px (md: breakpoint): MenuBar Small */}
|
||||
<div className="hidden md:block lg:hidden">
|
||||
<MenuBar size="Small">{renderNavigationItems("homeMd")}</MenuBar>
|
||||
<MenuBar size="Small">
|
||||
{renderNavigationItems("homeMd")}
|
||||
</MenuBar>
|
||||
</div>
|
||||
|
||||
{/* 1024-1440px (lg: breakpoint): MenuBar Large */}
|
||||
@@ -158,7 +167,11 @@ function TopNavView({
|
||||
aria-label={t("ariaLabels.mainNavigation")}
|
||||
>
|
||||
{/* Logo - Consistent left positioning across all breakpoints */}
|
||||
<Logo size={logoSize} showText={true} />
|
||||
<Logo
|
||||
size={logoSize}
|
||||
wordmark
|
||||
palette={folderTop ? "inverse" : "default"}
|
||||
/>
|
||||
|
||||
{/* Navigation Links - Consistent center positioning */}
|
||||
<div className="flex items-center flex-1 justify-end sm:flex-none sm:justify-center">
|
||||
@@ -190,7 +203,9 @@ function TopNavView({
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block" data-testid="nav-xl">
|
||||
<MenuBar size="X Large">{renderNavigationItems("xlarge")}</MenuBar>
|
||||
<MenuBar size="X Large">
|
||||
{renderNavigationItems("xlarge")}
|
||||
</MenuBar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -44,7 +44,9 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
|
||||
onContactClick,
|
||||
}) => {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const variant = normalizeAskOrganizerVariant(variantProp) as AskOrganizerVariant;
|
||||
const variant = normalizeAskOrganizerVariant(
|
||||
variantProp,
|
||||
) as AskOrganizerVariant;
|
||||
const t = useTranslation();
|
||||
const defaultButtonText = buttonText ?? t("askOrganizer.buttonText");
|
||||
const defaultButtonHref = buttonHref ?? t("askOrganizer.buttonHref");
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface CommunityRuleDocumentEntry {
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface CommunityRuleDocumentSection {
|
||||
categoryName: string;
|
||||
entries: CommunityRuleDocumentEntry[];
|
||||
}
|
||||
|
||||
export interface CommunityRuleDocumentProps {
|
||||
sections: CommunityRuleDocumentSection[];
|
||||
className?: string;
|
||||
/** When true, wrap in white background with left teal bar (small breakpoint). */
|
||||
useCardStyle?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import type { CommunityRuleDocumentProps } from "./CommunityRuleDocument.types";
|
||||
|
||||
const SECTION_GAP = "var(--measures-spacing-1200, 64px)";
|
||||
const TEAL_BG = "var(--color-teal-teal50, #c9fef9)";
|
||||
const SECTION_LINE_COLOR = "var(--color-border-default-tertiary, #464646)";
|
||||
|
||||
function CommunityRuleDocumentView({
|
||||
sections,
|
||||
className = "",
|
||||
useCardStyle = false,
|
||||
}: CommunityRuleDocumentProps) {
|
||||
const rootClass = useCardStyle
|
||||
? `rounded-[12px] bg-white pl-3 border-l-4 ${className}`
|
||||
: className;
|
||||
const rootStyle = useCardStyle ? { borderLeftColor: TEAL_BG } : undefined;
|
||||
|
||||
const sectionLineStyle = useCardStyle
|
||||
? undefined
|
||||
: { borderLeftColor: SECTION_LINE_COLOR };
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col min-w-0 ${rootClass}`}
|
||||
style={{ gap: SECTION_GAP, ...rootStyle }}
|
||||
>
|
||||
{sections.map((section, sectionIndex) => (
|
||||
<div
|
||||
key={sectionIndex}
|
||||
className={`flex flex-col min-w-0 ${!useCardStyle ? "border-l pl-3" : ""}`}
|
||||
style={sectionLineStyle}
|
||||
>
|
||||
{/* Section content: line runs full height of this block via border-left */}
|
||||
<div className="flex flex-1 flex-col gap-4 min-w-0">
|
||||
<p className="font-inter font-medium text-[16px] leading-[20px] text-[var(--color-content-invert-secondary,#1f1f1f)] shrink-0">
|
||||
{section.categoryName}
|
||||
</p>
|
||||
<div className="flex flex-col min-w-0" style={{ gap: "24px" }}>
|
||||
{section.entries.map((entry, entryIndex) => (
|
||||
<div
|
||||
key={entryIndex}
|
||||
className="flex flex-col min-w-0"
|
||||
style={{ gap: "6px" }}
|
||||
>
|
||||
<p className="font-inter font-bold text-[20px] leading-[28px] text-[var(--color-content-invert-primary)] shrink-0">
|
||||
{entry.title}
|
||||
</p>
|
||||
<p className="font-inter font-normal text-[14px] leading-[20px] text-[var(--color-content-invert-primary)] shrink-0">
|
||||
{entry.body}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CommunityRuleDocumentView.displayName = "CommunityRuleDocumentView";
|
||||
|
||||
export default memo(CommunityRuleDocumentView);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./CommunityRuleDocument.view";
|
||||
export type { CommunityRuleDocumentProps } from "./CommunityRuleDocument.types";
|
||||
@@ -46,6 +46,7 @@ const HeroBanner = memo<HeroBannerProps>(
|
||||
|
||||
{/* Hero Image Container */}
|
||||
<div className="w-full h-full md:flex-1 rounded-[8px] overflow-hidden relative z-10 flex items-center justify-center">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- dynamic path from getAssetPath */}
|
||||
<img
|
||||
src={getAssetPath("assets/HeroImage.png")}
|
||||
alt={imageAlt}
|
||||
|
||||
@@ -16,19 +16,24 @@ export function RuleStackView({
|
||||
}: RuleStackViewProps) {
|
||||
const t = useTranslation("pages.home.ruleStack");
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
|
||||
// Debug: Log button text to ensure translation works
|
||||
const buttonText = t("button.seeAllTemplates");
|
||||
|
||||
// Determine current breakpoint for RuleCard size
|
||||
// 320-639: XS, 640-767: S, 768-1023: S, 1024-1439: M, 1440+: L
|
||||
const isMax639 = useMediaQuery("(max-width: 639px)");
|
||||
const isMin640Max1023 = useMediaQuery("(min-width: 640px) and (max-width: 1023px)");
|
||||
const isMin1024Max1439 = useMediaQuery("(min-width: 1024px) and (max-width: 1439px)");
|
||||
const isMin640Max1023 = useMediaQuery(
|
||||
"(min-width: 640px) and (max-width: 1023px)",
|
||||
);
|
||||
const isMin1024Max1439 = useMediaQuery(
|
||||
"(min-width: 1024px) and (max-width: 1439px)",
|
||||
);
|
||||
const isMin1440 = useMediaQuery("(min-width: 1440px)");
|
||||
|
||||
// Handle hydration: only use media queries after mount
|
||||
useEffect(() => {
|
||||
// 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({
|
||||
</div>
|
||||
|
||||
{/* See all templates button */}
|
||||
<div className="
|
||||
<div
|
||||
className="
|
||||
flex justify-center w-full
|
||||
max-[767px]:mt-[var(--measures-spacing-600,24px)]
|
||||
min-[768px]:max-[1023px]:mt-[var(--measures-spacing-800,32px)]
|
||||
min-[1024px]:mt-[var(--measures-spacing-1000,40px)]
|
||||
">
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
size="large"
|
||||
>
|
||||
"
|
||||
>
|
||||
<Button buttonType="outline" palette="default" size="large">
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import { memo } from "react";
|
||||
import { normalizeSectionHeaderVariant } from "../../../lib/propNormalization";
|
||||
|
||||
export type SectionHeaderVariantValue = "default" | "multi-line" | "Default" | "Multi-Line";
|
||||
export type SectionHeaderVariantValue =
|
||||
| "default"
|
||||
| "multi-line"
|
||||
| "Default"
|
||||
| "Multi-Line";
|
||||
|
||||
interface SectionHeaderProps {
|
||||
title: string;
|
||||
|
||||
@@ -26,6 +26,7 @@ const SectionNumber = memo<SectionNumberProps>(({ number }) => {
|
||||
|
||||
return (
|
||||
<div className="relative size-[40px] overflow-visible -rotate-[15deg]">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- dynamic src from getImageSrc */}
|
||||
<img
|
||||
src={getImageSrc(number)}
|
||||
alt={`Section ${number}`}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
import { memo } from "react";
|
||||
import ContentLockupView from "./ContentLockup.view";
|
||||
import type { ContentLockupProps, VariantStyle } from "./ContentLockup.types";
|
||||
import { normalizeContentLockupVariant, normalizeAlignment } from "../../../../lib/propNormalization";
|
||||
import {
|
||||
normalizeContentLockupVariant,
|
||||
normalizeAlignment,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
const ContentLockupContainer = memo<ContentLockupProps>(
|
||||
({
|
||||
|
||||
@@ -60,12 +60,15 @@ function ContentLockupView({
|
||||
</h1>
|
||||
) : null}
|
||||
{variant === "hero" && (
|
||||
<img
|
||||
src={getAssetPath("assets/Shapes_1.svg")}
|
||||
alt=""
|
||||
className={styles.shape}
|
||||
role="presentation"
|
||||
/>
|
||||
<>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- decorative shape SVG */}
|
||||
<img
|
||||
src={getAssetPath("assets/Shapes_1.svg")}
|
||||
alt=""
|
||||
className={styles.shape}
|
||||
role="presentation"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -93,19 +96,32 @@ function ContentLockupView({
|
||||
<div className="flex justify-start">
|
||||
{/* Small button for xsm and sm breakpoints */}
|
||||
<div className="block md:hidden">
|
||||
<Button buttonType="filled" palette={variant === "hero" ? "default" : "inverse"} size="small">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette={variant === "hero" ? "default" : "inverse"}
|
||||
size="small"
|
||||
>
|
||||
{ctaText}
|
||||
</Button>
|
||||
</div>
|
||||
{/* Large button for md and lg breakpoints */}
|
||||
<div className="hidden md:block xl:hidden">
|
||||
<Button buttonType="filled" palette={variant === "hero" ? "default" : "inverse"} size="large" className={buttonClassName}>
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette={variant === "hero" ? "default" : "inverse"}
|
||||
size="large"
|
||||
className={buttonClassName}
|
||||
>
|
||||
{ctaText}
|
||||
</Button>
|
||||
</div>
|
||||
{/* XLarge button for xl breakpoint */}
|
||||
<div className="hidden xl:block">
|
||||
<Button buttonType="filled" palette={variant === "hero" ? "default" : "inverse"} size="xlarge">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette={variant === "hero" ? "default" : "inverse"}
|
||||
size="xlarge"
|
||||
>
|
||||
{ctaText}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import HeaderLockupView from "./HeaderLockup.view";
|
||||
import type { HeaderLockupProps } from "./HeaderLockup.types";
|
||||
import {
|
||||
normalizeHeaderLockupJustification,
|
||||
normalizeHeaderLockupSize,
|
||||
normalizeHeaderLockupPalette,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
const HeaderLockupContainer = memo<HeaderLockupProps>(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
justification: justificationProp = "left",
|
||||
size: sizeProp = "L",
|
||||
palette: paletteProp = "default",
|
||||
}) => {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const justification = normalizeHeaderLockupJustification(justificationProp);
|
||||
const size = normalizeHeaderLockupSize(sizeProp);
|
||||
const palette = normalizeHeaderLockupPalette(paletteProp);
|
||||
|
||||
return (
|
||||
<HeaderLockupView
|
||||
title={title}
|
||||
description={description}
|
||||
justification={justification}
|
||||
size={size}
|
||||
palette={palette}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
HeaderLockupContainer.displayName = "HeaderLockup";
|
||||
|
||||
export default HeaderLockupContainer;
|
||||
@@ -0,0 +1,45 @@
|
||||
export type HeaderLockupJustificationValue =
|
||||
| "left"
|
||||
| "center"
|
||||
| "Left"
|
||||
| "Center";
|
||||
export type HeaderLockupSizeValue = "L" | "M" | "l" | "m";
|
||||
export type HeaderLockupPaletteValue =
|
||||
| "default"
|
||||
| "inverse"
|
||||
| "Default"
|
||||
| "Inverse";
|
||||
|
||||
export interface HeaderLockupProps {
|
||||
/**
|
||||
* Title text (required)
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Description text (optional)
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* Text justification. Accepts both PascalCase (Figma) and lowercase (codebase).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
*/
|
||||
justification?: HeaderLockupJustificationValue;
|
||||
/**
|
||||
* Size variant. Accepts both PascalCase (Figma) and lowercase (codebase).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
*/
|
||||
size?: HeaderLockupSizeValue;
|
||||
/**
|
||||
* Palette. Default = light text (dark bg); Inverse = dark text (light bg).
|
||||
* Accepts both PascalCase (Figma) and lowercase (codebase).
|
||||
*/
|
||||
palette?: HeaderLockupPaletteValue;
|
||||
}
|
||||
|
||||
export interface HeaderLockupViewProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
justification: "left" | "center";
|
||||
size: "L" | "M";
|
||||
palette: "default" | "inverse";
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import type { HeaderLockupViewProps } from "./HeaderLockup.types";
|
||||
|
||||
function HeaderLockupView({
|
||||
title,
|
||||
description,
|
||||
justification,
|
||||
size,
|
||||
palette,
|
||||
}: HeaderLockupViewProps) {
|
||||
const isL = size === "L";
|
||||
const isLeft = justification === "left";
|
||||
const isInverse = palette === "inverse";
|
||||
|
||||
const titleColorClass = isInverse
|
||||
? "text-[var(--color-content-invert-primary)]"
|
||||
: "text-[var(--color-content-default-primary,white)]";
|
||||
const descriptionColorClass = isInverse
|
||||
? "text-[#2d2d2d]"
|
||||
: "text-[var(--color-content-default-tertiary,#b4b4b4)]";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col gap-[var(--measures-spacing-200,8px)] py-[12px] relative ${
|
||||
isLeft ? "items-start" : "items-center"
|
||||
}`}
|
||||
>
|
||||
{/* Title */}
|
||||
<div className="flex items-center relative shrink-0 w-full">
|
||||
<h1
|
||||
className={`flex-[1_0_0] min-h-px min-w-px overflow-hidden relative ${titleColorClass} text-ellipsis whitespace-pre-wrap ${
|
||||
isLeft ? "text-left" : "text-center"
|
||||
} ${
|
||||
isL
|
||||
? "font-bricolage-grotesque font-extrabold text-[36px] leading-[44px]"
|
||||
: "font-bricolage-grotesque font-bold text-[28px] leading-[36px]"
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{description && (
|
||||
<p
|
||||
className={`font-inter font-normal max-w-[640px] overflow-hidden relative shrink-0 ${descriptionColorClass} text-ellipsis w-full whitespace-pre-wrap ${
|
||||
isLeft ? "" : "text-center"
|
||||
} ${
|
||||
isL ? "text-[18px] leading-[1.3]" : "text-[14px] leading-[20px]"
|
||||
}`}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
HeaderLockupView.displayName = "HeaderLockupView";
|
||||
|
||||
export default memo(HeaderLockupView);
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./HeaderLockup.container";
|
||||
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import NumberedListView from "./NumberedList.view";
|
||||
import type { NumberedListProps } from "./NumberedList.types";
|
||||
import { normalizeNumberedListSize } from "../../../../lib/propNormalization";
|
||||
|
||||
const NumberedListContainer = memo<NumberedListProps>(
|
||||
({ items, size: sizeProp = "M" }) => {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const size = normalizeNumberedListSize(sizeProp);
|
||||
|
||||
return <NumberedListView items={items} size={size} />;
|
||||
},
|
||||
);
|
||||
|
||||
NumberedListContainer.displayName = "NumberedList";
|
||||
|
||||
export default NumberedListContainer;
|
||||
@@ -0,0 +1,23 @@
|
||||
export type NumberedListSizeValue = "M" | "S" | "m" | "s";
|
||||
|
||||
export interface NumberedListItem {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface NumberedListProps {
|
||||
/**
|
||||
* Array of list items, each with title and description
|
||||
*/
|
||||
items: NumberedListItem[];
|
||||
/**
|
||||
* Size variant. Accepts both PascalCase (Figma) and lowercase (codebase).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
*/
|
||||
size?: NumberedListSizeValue;
|
||||
}
|
||||
|
||||
export interface NumberedListViewProps {
|
||||
items: NumberedListItem[];
|
||||
size: "M" | "S";
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import type { NumberedListViewProps } from "./NumberedList.types";
|
||||
|
||||
function NumberedListView({ items, size }: NumberedListViewProps) {
|
||||
const isM = size === "M";
|
||||
|
||||
return (
|
||||
<ol className="flex flex-col gap-[var(--measures-spacing-600,24px)] items-start relative w-full list-none">
|
||||
{items.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="flex gap-[12px] items-center relative shrink-0 w-full"
|
||||
>
|
||||
{/* Number Indicator */}
|
||||
<div
|
||||
className={`bg-[var(--color-surface-inverse-primary,white)] flex flex-col items-center justify-center px-[11.2px] py-[4px] relative rounded-full shrink-0 ${
|
||||
isM ? "size-[32px]" : "size-[24px]"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex flex-col justify-center leading-[0] overflow-hidden relative shrink-0 text-[var(--color-content-inverse-primary,black)] text-ellipsis whitespace-nowrap ${
|
||||
isM
|
||||
? "font-inter font-bold text-[20px] leading-[28px]"
|
||||
: "font-bricolage-grotesque font-bold text-[16px] leading-[22px]"
|
||||
}`}
|
||||
>
|
||||
<span>{index + 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex flex-[1_0_0] flex-col gap-[var(--measures-spacing-100,4px)] items-start justify-center min-h-px min-w-px">
|
||||
{/* Title */}
|
||||
<div className="flex items-center relative shrink-0 w-full">
|
||||
<h3
|
||||
className={`flex-[1_0_0] min-h-px min-w-px overflow-hidden relative text-[var(--color-content-default-primary,white)] text-ellipsis whitespace-pre-wrap ${
|
||||
isM
|
||||
? "font-inter font-bold text-[20px] leading-[28px]"
|
||||
: "font-bricolage-grotesque font-bold text-[16px] leading-[22px]"
|
||||
}`}
|
||||
>
|
||||
{item.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p
|
||||
className={`font-inter font-normal max-w-[640px] overflow-hidden relative shrink-0 text-[var(--color-content-default-tertiary,#b4b4b4)] text-ellipsis w-full whitespace-pre-wrap ${
|
||||
isM
|
||||
? "text-[14px] leading-[20px]"
|
||||
: "text-[12px] leading-[16px]"
|
||||
}`}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
NumberedListView.displayName = "NumberedListView";
|
||||
|
||||
export default memo(NumberedListView);
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./NumberedList.container";
|
||||
@@ -1,7 +1,15 @@
|
||||
import { memo } from "react";
|
||||
import { normalizeSize } from "../../../lib/propNormalization";
|
||||
|
||||
export type AvatarContainerSizeValue = "small" | "medium" | "large" | "xlarge" | "Small" | "Medium" | "Large" | "XLarge";
|
||||
export type AvatarContainerSizeValue =
|
||||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "xlarge"
|
||||
| "Small"
|
||||
| "Medium"
|
||||
| "Large"
|
||||
| "XLarge";
|
||||
|
||||
interface AvatarContainerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useCallback, useState } from "react";
|
||||
import { CardStackView } from "./CardStack.view";
|
||||
import type { CardStackProps } from "./CardStack.types";
|
||||
|
||||
const DEFAULT_TOGGLE_LABEL = "See all communication approaches";
|
||||
const DEFAULT_SHOW_LESS_LABEL = "Show less";
|
||||
|
||||
const CardStackContainer = memo<CardStackProps>(
|
||||
({
|
||||
cards,
|
||||
selectedId: controlledSelectedId,
|
||||
selectedIds: controlledSelectedIds,
|
||||
onCardSelect: controlledOnCardSelect,
|
||||
expanded: controlledExpanded,
|
||||
onToggleExpand: controlledOnToggleExpand,
|
||||
hasMore = true,
|
||||
toggleLabel = DEFAULT_TOGGLE_LABEL,
|
||||
showLessLabel = DEFAULT_SHOW_LESS_LABEL,
|
||||
title = "",
|
||||
description = "",
|
||||
layout = "default",
|
||||
className = "",
|
||||
}) => {
|
||||
const [internalExpanded, setInternalExpanded] = useState(false);
|
||||
const [internalSelectedIds, setInternalSelectedIds] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
const expanded =
|
||||
controlledExpanded !== undefined ? controlledExpanded : internalExpanded;
|
||||
|
||||
const handleToggleExpand = useCallback(() => {
|
||||
if (controlledOnToggleExpand) {
|
||||
controlledOnToggleExpand();
|
||||
} else {
|
||||
setInternalExpanded((prev) => !prev);
|
||||
}
|
||||
}, [controlledOnToggleExpand]);
|
||||
|
||||
const selectedIds =
|
||||
controlledSelectedIds !== undefined
|
||||
? controlledSelectedIds
|
||||
: controlledSelectedId !== undefined
|
||||
? controlledSelectedId
|
||||
? [controlledSelectedId]
|
||||
: []
|
||||
: internalSelectedIds;
|
||||
|
||||
const handleCardSelect = useCallback(
|
||||
(id: string) => {
|
||||
if (controlledOnCardSelect) {
|
||||
controlledOnCardSelect(id);
|
||||
} else {
|
||||
setInternalSelectedIds((prev) =>
|
||||
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
|
||||
);
|
||||
}
|
||||
},
|
||||
[controlledOnCardSelect],
|
||||
);
|
||||
|
||||
return (
|
||||
<CardStackView
|
||||
cards={cards}
|
||||
selectedIds={selectedIds}
|
||||
onCardSelect={handleCardSelect}
|
||||
expanded={expanded}
|
||||
onToggleExpand={handleToggleExpand}
|
||||
hasMore={hasMore}
|
||||
toggleLabel={toggleLabel}
|
||||
showLessLabel={showLessLabel}
|
||||
title={title}
|
||||
description={description}
|
||||
layout={layout}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CardStackContainer.displayName = "CardStack";
|
||||
|
||||
export default CardStackContainer;
|
||||
@@ -0,0 +1,38 @@
|
||||
export interface CardStackItem {
|
||||
id: string;
|
||||
label: string;
|
||||
supportText?: string;
|
||||
recommended?: boolean;
|
||||
}
|
||||
|
||||
export interface CardStackProps {
|
||||
cards: CardStackItem[];
|
||||
selectedId?: string | null;
|
||||
selectedIds?: string[];
|
||||
onCardSelect?: (id: string) => void;
|
||||
expanded?: boolean;
|
||||
onToggleExpand?: () => void;
|
||||
hasMore?: boolean;
|
||||
toggleLabel?: string;
|
||||
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;
|
||||
}
|
||||
|
||||
export interface CardStackViewProps {
|
||||
cards: CardStackItem[];
|
||||
selectedIds: string[];
|
||||
onCardSelect: (id: string) => void;
|
||||
expanded: boolean;
|
||||
onToggleExpand: () => void;
|
||||
hasMore: boolean;
|
||||
toggleLabel: string;
|
||||
showLessLabel: string;
|
||||
title: string;
|
||||
description: string;
|
||||
layout: "default" | "singleStack";
|
||||
className: string;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import HeaderLockup from "../../type/HeaderLockup";
|
||||
import Card from "../../cards/Card";
|
||||
import type { CardStackViewProps } from "./CardStack.types";
|
||||
|
||||
export function CardStackView({
|
||||
cards,
|
||||
selectedIds,
|
||||
onCardSelect,
|
||||
expanded,
|
||||
onToggleExpand,
|
||||
hasMore,
|
||||
toggleLabel,
|
||||
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);
|
||||
|
||||
// Single stack: always one column; expand reveals more in same stack (scrollable)
|
||||
if (layout === "singleStack") {
|
||||
const displayedCards = expanded ? cards : compactCards;
|
||||
return (
|
||||
<div className={`flex w-full flex-col gap-6 min-w-0 ${className}`}>
|
||||
{title || description ? (
|
||||
<div className="min-w-0 shrink-0">
|
||||
<HeaderLockup
|
||||
title={title}
|
||||
description={description}
|
||||
justification="center"
|
||||
size="L"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-col gap-[8px] w-full min-w-0">
|
||||
{displayedCards.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="vertical"
|
||||
showInfoIcon={true}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{hasMore ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleExpand}
|
||||
className="font-inter text-base font-normal leading-6 text-[var(--color-gray-000)] underline hover:opacity-90 focus:outline-none self-center cursor-pointer"
|
||||
>
|
||||
{expanded ? showLessLabel : toggleLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex w-full flex-col gap-6 min-w-0 ${className}`}>
|
||||
{title || description ? (
|
||||
<div className="min-w-0">
|
||||
<HeaderLockup
|
||||
title={title}
|
||||
description={description}
|
||||
justification="center"
|
||||
size="L"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{expanded ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-6 w-full">
|
||||
{cards.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="vertical"
|
||||
showInfoIcon={true}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Compact under 640: single column, up to 5 recommended cards */}
|
||||
<div className="flex flex-col gap-6 w-full md:hidden">
|
||||
{compactCards.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="vertical"
|
||||
showInfoIcon={true}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* Compact 640+: 6-col grid so each card spans 2; second row centered (cols 2–3 and 4–5) */}
|
||||
<div className="hidden md:grid grid-cols-6 gap-x-4 gap-y-6 w-full">
|
||||
{compactCards.map((item, index) => {
|
||||
const colClass =
|
||||
index <= 2
|
||||
? "md:col-span-2"
|
||||
: index === 3 && compactCards.length === 4
|
||||
? "md:col-start-3 md:col-span-2"
|
||||
: index === 3
|
||||
? "md:col-start-2 md:col-span-2"
|
||||
: "md:col-start-4 md:col-span-2";
|
||||
return (
|
||||
<div key={item.id} className={colClass}>
|
||||
<Card
|
||||
id={item.id}
|
||||
label={item.label}
|
||||
supportText={item.supportText}
|
||||
recommended={item.recommended ?? false}
|
||||
selected={isSelected(item.id)}
|
||||
orientation="horizontal"
|
||||
showInfoIcon={false}
|
||||
onClick={() => onCardSelect(item.id)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasMore ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleExpand}
|
||||
className="font-inter text-base font-normal leading-6 text-[var(--color-gray-000)] underline hover:opacity-90 focus:outline-none self-center cursor-pointer"
|
||||
>
|
||||
{expanded ? showLessLabel : toggleLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./CardStack.container";
|
||||
export type { CardStackProps, CardStackItem } from "./CardStack.types";
|
||||
@@ -5,11 +5,12 @@ import { CreateFlowFooterView } from "./CreateFlowFooter.view";
|
||||
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
||||
|
||||
const CreateFlowFooterContainer = memo<CreateFlowFooterProps>(
|
||||
({ secondButton, progressBar = true, className = "" }) => {
|
||||
({ secondButton, progressBar = true, onBackClick, className = "" }) => {
|
||||
return (
|
||||
<CreateFlowFooterView
|
||||
secondButton={secondButton}
|
||||
progressBar={progressBar}
|
||||
onBackClick={onBackClick}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,10 @@ export interface CreateFlowFooterProps {
|
||||
* @default true
|
||||
*/
|
||||
progressBar?: boolean;
|
||||
/**
|
||||
* Callback function for Back button click
|
||||
*/
|
||||
onBackClick?: () => void;
|
||||
/**
|
||||
* Additional CSS classes
|
||||
*/
|
||||
|
||||
@@ -5,11 +5,12 @@ import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
||||
export function CreateFlowFooterView({
|
||||
secondButton,
|
||||
progressBar = true,
|
||||
onBackClick,
|
||||
className = "",
|
||||
}: CreateFlowFooterProps) {
|
||||
return (
|
||||
<footer
|
||||
className={`sticky bottom-0 z-50 bg-black w-full ${className}`}
|
||||
className={`bg-black w-full ${className}`}
|
||||
role="contentinfo"
|
||||
aria-label="Create Flow Footer"
|
||||
>
|
||||
@@ -28,14 +29,14 @@ export function CreateFlowFooterView({
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
className="md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] !px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]"
|
||||
onClick={onBackClick}
|
||||
disabled={!onBackClick}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
{/* Second Button - Right */}
|
||||
{secondButton && (
|
||||
<div className="flex-shrink-0">{secondButton}</div>
|
||||
)}
|
||||
{secondButton && <div className="flex-shrink-0">{secondButton}</div>}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -15,13 +15,14 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
|
||||
onExport,
|
||||
onEdit,
|
||||
onExit,
|
||||
buttonPalette,
|
||||
className = "",
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleExit = () => {
|
||||
const handleExit = (options?: { saveDraft?: boolean }) => {
|
||||
if (onExit) {
|
||||
onExit();
|
||||
onExit(options);
|
||||
} else {
|
||||
// Default behavior: navigate to home
|
||||
router.push("/");
|
||||
@@ -38,6 +39,7 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
|
||||
onExport={onExport}
|
||||
onEdit={onEdit}
|
||||
onExit={handleExit}
|
||||
buttonPalette={buttonPalette}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Type definitions for CreateFlowTopNav component
|
||||
*
|
||||
*
|
||||
* Top navigation bar for the create rule flow.
|
||||
* Includes logo and action buttons (Share, Export, Edit, Exit).
|
||||
*/
|
||||
@@ -39,9 +39,15 @@ export interface CreateFlowTopNavProps {
|
||||
*/
|
||||
onEdit?: () => void;
|
||||
/**
|
||||
* Callback when Exit/Save & Exit button is clicked
|
||||
* Callback when Exit/Save & Exit button is clicked.
|
||||
* When user is logged in, called with { saveDraft: true } to stub "Save & Exit".
|
||||
*/
|
||||
onExit?: () => void;
|
||||
onExit?: (options?: { saveDraft?: boolean }) => void;
|
||||
/**
|
||||
* Palette for nav buttons (e.g. "inverse" on completed page to match teal background)
|
||||
* @default "default"
|
||||
*/
|
||||
buttonPalette?: "default" | "inverse";
|
||||
/**
|
||||
* Additional CSS classes
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Logo from "../../icons/Logo";
|
||||
import Logo from "../../asset/logo";
|
||||
import Button from "../../buttons/Button";
|
||||
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
|
||||
|
||||
@@ -11,13 +11,14 @@ export function CreateFlowTopNavView({
|
||||
onExport,
|
||||
onEdit,
|
||||
onExit,
|
||||
buttonPalette = "default",
|
||||
className = "",
|
||||
}: CreateFlowTopNavProps) {
|
||||
const exitButtonText = loggedIn ? "Save & Exit" : "Exit";
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`bg-black w-full border-b border-[var(--color-border-default-tertiary)] ${className}`}
|
||||
className={`bg-black w-full ${className}`}
|
||||
role="banner"
|
||||
aria-label="Create Rule Flow Navigation"
|
||||
>
|
||||
@@ -27,14 +28,14 @@ export function CreateFlowTopNavView({
|
||||
aria-label="Create Flow Navigation"
|
||||
>
|
||||
{/* Logo - Left */}
|
||||
<Logo size="createFlow" showText={true} />
|
||||
<Logo size="createFlow" wordmark palette={buttonPalette} />
|
||||
|
||||
{/* Button Group - Right */}
|
||||
<div className="flex items-center gap-[var(--spacing-scale-012,12px)]">
|
||||
{hasShare && (
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
palette={buttonPalette}
|
||||
size="xsmall"
|
||||
onClick={onShare}
|
||||
ariaLabel="Share"
|
||||
@@ -47,7 +48,7 @@ export function CreateFlowTopNavView({
|
||||
{hasExport && (
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
palette={buttonPalette}
|
||||
size="xsmall"
|
||||
onClick={onExport}
|
||||
ariaLabel="Export"
|
||||
@@ -74,7 +75,7 @@ export function CreateFlowTopNavView({
|
||||
{hasEdit && (
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
palette={buttonPalette}
|
||||
size="xsmall"
|
||||
onClick={onEdit}
|
||||
ariaLabel="Edit"
|
||||
@@ -86,9 +87,9 @@ export function CreateFlowTopNavView({
|
||||
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
palette={buttonPalette}
|
||||
size="xsmall"
|
||||
onClick={onExit}
|
||||
onClick={() => onExit?.({ saveDraft: loggedIn })}
|
||||
ariaLabel={exitButtonText}
|
||||
className="md:!text-[12px] md:!leading-[14px] !text-[10px] !leading-[12px] !px-[var(--spacing-scale-006,6px)] md:!px-[var(--spacing-scale-008,8px)] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]"
|
||||
>
|
||||
|
||||
@@ -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<DecisionMakingSidebarProps>(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
messageBoxTitle,
|
||||
messageBoxItems,
|
||||
messageBoxCheckedIds,
|
||||
onMessageBoxCheckboxChange,
|
||||
size: sizeProp = "L",
|
||||
justification: justificationProp = "left",
|
||||
className = "",
|
||||
}) => {
|
||||
const size = normalizeHeaderLockupSize(sizeProp);
|
||||
const justification = normalizeHeaderLockupJustification(justificationProp);
|
||||
|
||||
return (
|
||||
<DecisionMakingSidebarView
|
||||
title={title}
|
||||
description={description}
|
||||
messageBoxTitle={messageBoxTitle}
|
||||
messageBoxItems={messageBoxItems}
|
||||
messageBoxCheckedIds={messageBoxCheckedIds}
|
||||
onMessageBoxCheckboxChange={onMessageBoxCheckboxChange}
|
||||
size={size}
|
||||
justification={justification}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
DecisionMakingSidebarContainer.displayName = "DecisionMakingSidebar";
|
||||
|
||||
export default DecisionMakingSidebarContainer;
|
||||
@@ -0,0 +1,33 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"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 (
|
||||
<div className={`flex flex-col gap-3 w-full min-w-0 ${className}`}>
|
||||
{isStringDescription ? (
|
||||
<HeaderLockup
|
||||
title={title}
|
||||
description={description as string}
|
||||
justification={justification}
|
||||
size={size}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`flex flex-col gap-[var(--measures-spacing-200,8px)] py-[12px] relative ${
|
||||
isLeft ? "items-start" : "items-center"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center relative shrink-0 w-full">
|
||||
<h1
|
||||
className={`flex-[1_0_0] min-h-px min-w-px overflow-hidden relative text-[var(--color-content-default-primary,white)] text-ellipsis whitespace-pre-wrap ${
|
||||
isLeft ? "text-left" : "text-center"
|
||||
} ${
|
||||
isL
|
||||
? "font-bricolage-grotesque font-extrabold text-[36px] leading-[44px]"
|
||||
: "font-bricolage-grotesque font-bold text-[28px] leading-[36px]"
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
{description != null && (
|
||||
<p
|
||||
className={`font-inter font-normal max-w-[640px] overflow-hidden relative shrink-0 text-[var(--color-content-default-tertiary,#b4b4b4)] text-ellipsis w-full whitespace-pre-wrap ${
|
||||
isLeft ? "" : "text-center"
|
||||
} ${
|
||||
isL ? "text-[18px] leading-[1.3]" : "text-[14px] leading-[20px]"
|
||||
}`}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<InfoMessageBox
|
||||
title={messageBoxTitle}
|
||||
items={messageBoxItems}
|
||||
checkedIds={messageBoxCheckedIds}
|
||||
onCheckboxChange={onMessageBoxCheckboxChange ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DecisionMakingSidebarView.displayName = "DecisionMakingSidebarView";
|
||||
|
||||
export default memo(DecisionMakingSidebarView);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user