adilallo/feature/PageTemplateImplementations #42
@@ -2,7 +2,7 @@ name: CI Pipeline
|
|||||||
run-name: "${{ gitea.actor }} triggered CI pipeline"
|
run-name: "${{ gitea.actor }} triggered CI pipeline"
|
||||||
|
|
||||||
on:
|
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
|
# Auto-runs disabled for solo development
|
||||||
# Re-enable when ready for collaborators:
|
# Re-enable when ready for collaborators:
|
||||||
# pull_request:
|
# pull_request:
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import RuleCard from "../components/cards/RuleCard";
|
import RuleCard from "../../components/cards/RuleCard";
|
||||||
import Chip from "../components/controls/Chip";
|
import Card from "../../components/cards/Card";
|
||||||
import MultiSelect from "../components/controls/MultiSelect";
|
import Chip from "../../components/controls/Chip";
|
||||||
|
import MultiSelect from "../../components/controls/MultiSelect";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { getAssetPath } from "../../../lib/assetUtils";
|
import { getAssetPath } from "../../../lib/assetUtils";
|
||||||
|
|
||||||
|
/** Module-level counter for unique rule card chip IDs (avoids ref in initial state). */
|
||||||
|
let ruleCardIdCounter = 0;
|
||||||
|
|
||||||
interface ChipData {
|
interface ChipData {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -17,7 +21,13 @@ interface ChipData {
|
|||||||
|
|
||||||
// MultiSelect example component with state management
|
// MultiSelect example component with state management
|
||||||
function MultiSelectExample({ size }: { size: "S" | "M" }) {
|
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: "1", label: "1 member", state: "Unselected" },
|
||||||
{ id: "2", label: "2-10 members", state: "Unselected" },
|
{ id: "2", label: "2-10 members", state: "Unselected" },
|
||||||
{ id: "3", label: "10-24 members", state: "Unselected" },
|
{ id: "3", label: "10-24 members", state: "Unselected" },
|
||||||
@@ -35,17 +45,14 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) {
|
|||||||
...opt,
|
...opt,
|
||||||
state: opt.state === "Selected" ? "Unselected" : "Selected",
|
state: opt.state === "Selected" ? "Unselected" : "Selected",
|
||||||
}
|
}
|
||||||
: opt
|
: opt,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddClick = () => {
|
const handleAddClick = () => {
|
||||||
const newId = `custom-${Date.now()}`;
|
const newId = `custom-${Date.now()}`;
|
||||||
setOptions((prev) => [
|
setOptions((prev) => [...prev, { id: newId, label: "", state: "Custom" }]);
|
||||||
...prev,
|
|
||||||
{ id: newId, label: "", state: "Custom" },
|
|
||||||
]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCustomConfirm = (chipId: string, value: string) => {
|
const handleCustomConfirm = (chipId: string, value: string) => {
|
||||||
@@ -53,8 +60,8 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) {
|
|||||||
prev.map((opt) =>
|
prev.map((opt) =>
|
||||||
opt.id === chipId
|
opt.id === chipId
|
||||||
? { ...opt, label: value, state: "Selected" as const }
|
? { ...opt, label: value, state: "Selected" as const }
|
||||||
: opt
|
: opt,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,7 +90,9 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ComponentsPreview() {
|
export default function ComponentsPreview() {
|
||||||
const [chipStates, setChipStates] = useState<Record<string, "Unselected" | "Selected">>({
|
const [chipStates, setChipStates] = useState<
|
||||||
|
Record<string, "Unselected" | "Selected">
|
||||||
|
>({
|
||||||
"default-s": "Unselected",
|
"default-s": "Unselected",
|
||||||
"default-m": "Unselected",
|
"default-m": "Unselected",
|
||||||
"inverse-s": "Unselected",
|
"inverse-s": "Unselected",
|
||||||
@@ -92,19 +101,41 @@ export default function ComponentsPreview() {
|
|||||||
|
|
||||||
// Manage custom chips separately
|
// Manage custom chips separately
|
||||||
const [customChips, setCustomChips] = useState<ChipData[]>([
|
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
|
// RuleCard categories with chip options and state management
|
||||||
const [ruleCardCategories, setRuleCardCategories] = useState<Array<{
|
const [ruleCardCategories, setRuleCardCategories] = useState<
|
||||||
name: string;
|
Array<{
|
||||||
chipOptions: Array<{ id: string; label: string; state: "Unselected" | "Selected" | "Custom" }>;
|
name: string;
|
||||||
onChipClick?: (categoryName: string, chipId: string) => void;
|
chipOptions: Array<{
|
||||||
onAddClick?: (categoryName: string) => void;
|
id: string;
|
||||||
onCustomChipConfirm?: (categoryName: string, chipId: string, value: string) => void;
|
label: string;
|
||||||
onCustomChipClose?: (categoryName: string, chipId: string) => void;
|
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",
|
name: "Values",
|
||||||
chipOptions: [
|
chipOptions: [
|
||||||
@@ -124,17 +155,20 @@ export default function ComponentsPreview() {
|
|||||||
opt.id === chipId
|
opt.id === chipId
|
||||||
? {
|
? {
|
||||||
...opt,
|
...opt,
|
||||||
state: opt.state === "Selected" ? "Unselected" : "Selected",
|
state:
|
||||||
|
opt.state === "Selected"
|
||||||
|
? "Unselected"
|
||||||
|
: "Selected",
|
||||||
}
|
}
|
||||||
: opt
|
: opt,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onAddClick: (categoryName: string) => {
|
onAddClick: (categoryName: string) => {
|
||||||
const newId = `custom-${categoryName}-${Date.now()}`;
|
const newId = `custom-${categoryName}-${++ruleCardIdCounter}`;
|
||||||
setRuleCardCategories((prev) =>
|
setRuleCardCategories((prev) =>
|
||||||
prev.map((cat) =>
|
prev.map((cat) =>
|
||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
@@ -145,11 +179,15 @@ export default function ComponentsPreview() {
|
|||||||
{ id: newId, label: "", state: "Custom" },
|
{ id: newId, label: "", state: "Custom" },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => {
|
onCustomChipConfirm: (
|
||||||
|
categoryName: string,
|
||||||
|
chipId: string,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
setRuleCardCategories((prev) =>
|
setRuleCardCategories((prev) =>
|
||||||
prev.map((cat) =>
|
prev.map((cat) =>
|
||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
@@ -158,11 +196,11 @@ export default function ComponentsPreview() {
|
|||||||
chipOptions: cat.chipOptions.map((opt) =>
|
chipOptions: cat.chipOptions.map((opt) =>
|
||||||
opt.id === chipId
|
opt.id === chipId
|
||||||
? { ...opt, label: value, state: "Selected" }
|
? { ...opt, label: value, state: "Selected" }
|
||||||
: opt
|
: opt,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onCustomChipClose: (categoryName: string, chipId: string) => {
|
onCustomChipClose: (categoryName: string, chipId: string) => {
|
||||||
@@ -171,18 +209,18 @@ export default function ComponentsPreview() {
|
|||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
? {
|
? {
|
||||||
...cat,
|
...cat,
|
||||||
chipOptions: cat.chipOptions.filter((opt) => opt.id !== chipId),
|
chipOptions: cat.chipOptions.filter(
|
||||||
|
(opt) => opt.id !== chipId,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Communication",
|
name: "Communication",
|
||||||
chipOptions: [
|
chipOptions: [{ id: "comm-1", label: "Signal", state: "Unselected" }],
|
||||||
{ id: "comm-1", label: "Signal", state: "Unselected" },
|
|
||||||
],
|
|
||||||
onChipClick: (categoryName: string, chipId: string) => {
|
onChipClick: (categoryName: string, chipId: string) => {
|
||||||
setRuleCardCategories((prev) =>
|
setRuleCardCategories((prev) =>
|
||||||
prev.map((cat) =>
|
prev.map((cat) =>
|
||||||
@@ -193,17 +231,20 @@ export default function ComponentsPreview() {
|
|||||||
opt.id === chipId
|
opt.id === chipId
|
||||||
? {
|
? {
|
||||||
...opt,
|
...opt,
|
||||||
state: opt.state === "Selected" ? "Unselected" : "Selected",
|
state:
|
||||||
|
opt.state === "Selected"
|
||||||
|
? "Unselected"
|
||||||
|
: "Selected",
|
||||||
}
|
}
|
||||||
: opt
|
: opt,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onAddClick: (categoryName: string) => {
|
onAddClick: (categoryName: string) => {
|
||||||
const newId = `custom-${categoryName}-${Date.now()}`;
|
const newId = `custom-${categoryName}-${++ruleCardIdCounter}`;
|
||||||
setRuleCardCategories((prev) =>
|
setRuleCardCategories((prev) =>
|
||||||
prev.map((cat) =>
|
prev.map((cat) =>
|
||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
@@ -214,11 +255,15 @@ export default function ComponentsPreview() {
|
|||||||
{ id: newId, label: "", state: "Custom" },
|
{ id: newId, label: "", state: "Custom" },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => {
|
onCustomChipConfirm: (
|
||||||
|
categoryName: string,
|
||||||
|
chipId: string,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
setRuleCardCategories((prev) =>
|
setRuleCardCategories((prev) =>
|
||||||
prev.map((cat) =>
|
prev.map((cat) =>
|
||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
@@ -227,11 +272,11 @@ export default function ComponentsPreview() {
|
|||||||
chipOptions: cat.chipOptions.map((opt) =>
|
chipOptions: cat.chipOptions.map((opt) =>
|
||||||
opt.id === chipId
|
opt.id === chipId
|
||||||
? { ...opt, label: value, state: "Selected" }
|
? { ...opt, label: value, state: "Selected" }
|
||||||
: opt
|
: opt,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onCustomChipClose: (categoryName: string, chipId: string) => {
|
onCustomChipClose: (categoryName: string, chipId: string) => {
|
||||||
@@ -240,10 +285,12 @@ export default function ComponentsPreview() {
|
|||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
? {
|
? {
|
||||||
...cat,
|
...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.id === chipId
|
||||||
? {
|
? {
|
||||||
...opt,
|
...opt,
|
||||||
state: opt.state === "Selected" ? "Unselected" : "Selected",
|
state:
|
||||||
|
opt.state === "Selected"
|
||||||
|
? "Unselected"
|
||||||
|
: "Selected",
|
||||||
}
|
}
|
||||||
: opt
|
: opt,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onAddClick: (categoryName: string) => {
|
onAddClick: (categoryName: string) => {
|
||||||
const newId = `custom-${categoryName}-${Date.now()}`;
|
const newId = `custom-${categoryName}-${++ruleCardIdCounter}`;
|
||||||
setRuleCardCategories((prev) =>
|
setRuleCardCategories((prev) =>
|
||||||
prev.map((cat) =>
|
prev.map((cat) =>
|
||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
@@ -283,11 +333,15 @@ export default function ComponentsPreview() {
|
|||||||
{ id: newId, label: "", state: "Custom" },
|
{ id: newId, label: "", state: "Custom" },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => {
|
onCustomChipConfirm: (
|
||||||
|
categoryName: string,
|
||||||
|
chipId: string,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
setRuleCardCategories((prev) =>
|
setRuleCardCategories((prev) =>
|
||||||
prev.map((cat) =>
|
prev.map((cat) =>
|
||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
@@ -296,11 +350,11 @@ export default function ComponentsPreview() {
|
|||||||
chipOptions: cat.chipOptions.map((opt) =>
|
chipOptions: cat.chipOptions.map((opt) =>
|
||||||
opt.id === chipId
|
opt.id === chipId
|
||||||
? { ...opt, label: value, state: "Selected" }
|
? { ...opt, label: value, state: "Selected" }
|
||||||
: opt
|
: opt,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onCustomChipClose: (categoryName: string, chipId: string) => {
|
onCustomChipClose: (categoryName: string, chipId: string) => {
|
||||||
@@ -309,10 +363,12 @@ export default function ComponentsPreview() {
|
|||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
? {
|
? {
|
||||||
...cat,
|
...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.id === chipId
|
||||||
? {
|
? {
|
||||||
...opt,
|
...opt,
|
||||||
state: opt.state === "Selected" ? "Unselected" : "Selected",
|
state:
|
||||||
|
opt.state === "Selected"
|
||||||
|
? "Unselected"
|
||||||
|
: "Selected",
|
||||||
}
|
}
|
||||||
: opt
|
: opt,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onAddClick: (categoryName: string) => {
|
onAddClick: (categoryName: string) => {
|
||||||
const newId = `custom-${categoryName}-${Date.now()}`;
|
const newId = `custom-${categoryName}-${++ruleCardIdCounter}`;
|
||||||
setRuleCardCategories((prev) =>
|
setRuleCardCategories((prev) =>
|
||||||
prev.map((cat) =>
|
prev.map((cat) =>
|
||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
@@ -353,11 +412,15 @@ export default function ComponentsPreview() {
|
|||||||
{ id: newId, label: "", state: "Custom" },
|
{ id: newId, label: "", state: "Custom" },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => {
|
onCustomChipConfirm: (
|
||||||
|
categoryName: string,
|
||||||
|
chipId: string,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
setRuleCardCategories((prev) =>
|
setRuleCardCategories((prev) =>
|
||||||
prev.map((cat) =>
|
prev.map((cat) =>
|
||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
@@ -366,11 +429,11 @@ export default function ComponentsPreview() {
|
|||||||
chipOptions: cat.chipOptions.map((opt) =>
|
chipOptions: cat.chipOptions.map((opt) =>
|
||||||
opt.id === chipId
|
opt.id === chipId
|
||||||
? { ...opt, label: value, state: "Selected" }
|
? { ...opt, label: value, state: "Selected" }
|
||||||
: opt
|
: opt,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onCustomChipClose: (categoryName: string, chipId: string) => {
|
onCustomChipClose: (categoryName: string, chipId: string) => {
|
||||||
@@ -379,10 +442,12 @@ export default function ComponentsPreview() {
|
|||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
? {
|
? {
|
||||||
...cat,
|
...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.id === chipId
|
||||||
? {
|
? {
|
||||||
...opt,
|
...opt,
|
||||||
state: opt.state === "Selected" ? "Unselected" : "Selected",
|
state:
|
||||||
|
opt.state === "Selected"
|
||||||
|
? "Unselected"
|
||||||
|
: "Selected",
|
||||||
}
|
}
|
||||||
: opt
|
: opt,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onAddClick: (categoryName: string) => {
|
onAddClick: (categoryName: string) => {
|
||||||
const newId = `custom-${categoryName}-${Date.now()}`;
|
const newId = `custom-${categoryName}-${++ruleCardIdCounter}`;
|
||||||
setRuleCardCategories((prev) =>
|
setRuleCardCategories((prev) =>
|
||||||
prev.map((cat) =>
|
prev.map((cat) =>
|
||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
@@ -423,11 +491,15 @@ export default function ComponentsPreview() {
|
|||||||
{ id: newId, label: "", state: "Custom" },
|
{ id: newId, label: "", state: "Custom" },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => {
|
onCustomChipConfirm: (
|
||||||
|
categoryName: string,
|
||||||
|
chipId: string,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
setRuleCardCategories((prev) =>
|
setRuleCardCategories((prev) =>
|
||||||
prev.map((cat) =>
|
prev.map((cat) =>
|
||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
@@ -436,11 +508,11 @@ export default function ComponentsPreview() {
|
|||||||
chipOptions: cat.chipOptions.map((opt) =>
|
chipOptions: cat.chipOptions.map((opt) =>
|
||||||
opt.id === chipId
|
opt.id === chipId
|
||||||
? { ...opt, label: value, state: "Selected" }
|
? { ...opt, label: value, state: "Selected" }
|
||||||
: opt
|
: opt,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: cat
|
: cat,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onCustomChipClose: (categoryName: string, chipId: string) => {
|
onCustomChipClose: (categoryName: string, chipId: string) => {
|
||||||
@@ -449,10 +521,12 @@ export default function ComponentsPreview() {
|
|||||||
cat.name === categoryName
|
cat.name === categoryName
|
||||||
? {
|
? {
|
||||||
...cat,
|
...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
|
Component Preview
|
||||||
</h1>
|
</h1>
|
||||||
<p className="font-inter text-[18px] leading-[24px] text-[var(--color-content-default-secondary)]">
|
<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>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -480,7 +555,7 @@ export default function ComponentsPreview() {
|
|||||||
<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)]">
|
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
|
||||||
Default palette
|
Default palette
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap items-center gap-[var(--spacing-scale-016)]">
|
<div className="flex flex-wrap items-center gap-[var(--spacing-scale-016)]">
|
||||||
<Chip
|
<Chip
|
||||||
label="Small"
|
label="Small"
|
||||||
@@ -490,7 +565,10 @@ export default function ComponentsPreview() {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setChipStates((prev) => ({
|
setChipStates((prev) => ({
|
||||||
...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={() =>
|
onClick={() =>
|
||||||
setChipStates((prev) => ({
|
setChipStates((prev) => ({
|
||||||
...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) =>
|
prev.map((c) =>
|
||||||
c.id === chip.id
|
c.id === chip.id
|
||||||
? { ...c, label: value, state: "Selected" }
|
? { ...c, label: value, state: "Selected" }
|
||||||
: c
|
: c,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onClose={(e) => {
|
onClose={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setCustomChips((prev) => prev.filter((c) => c.id !== chip.id));
|
setCustomChips((prev) =>
|
||||||
|
prev.filter((c) => c.id !== chip.id),
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Only toggle if the chip is in Selected or Unselected state (not Custom)
|
// 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) =>
|
setCustomChips((prev) =>
|
||||||
prev.map((c) =>
|
prev.map((c) =>
|
||||||
c.id === chip.id
|
c.id === chip.id
|
||||||
? {
|
? {
|
||||||
...c,
|
...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()}`;
|
const newId = `custom-${Date.now()}`;
|
||||||
setCustomChips((prev) => [
|
setCustomChips((prev) => [
|
||||||
...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"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Inverse palette - on white background */}
|
{/* 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)]">
|
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
|
||||||
Inverse palette (on white background)
|
Inverse palette (on white background)
|
||||||
</h3>
|
</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)]">
|
<div className="flex flex-wrap items-center gap-[var(--spacing-scale-016)]">
|
||||||
<Chip
|
<Chip
|
||||||
label="Small"
|
label="Small"
|
||||||
@@ -605,7 +703,10 @@ export default function ComponentsPreview() {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setChipStates((prev) => ({
|
setChipStates((prev) => ({
|
||||||
...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={() =>
|
onClick={() =>
|
||||||
setChipStates((prev) => ({
|
setChipStates((prev) => ({
|
||||||
...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>
|
</div>
|
||||||
</section>
|
</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 */}
|
{/* Collapsed State - Large */}
|
||||||
<section className="space-y-[var(--spacing-scale-024)]">
|
<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)]">
|
<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]"
|
className="w-[525px]"
|
||||||
logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png"
|
logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png"
|
||||||
logoAlt="Mutual Aid Mondays"
|
logoAlt="Mutual Aid Mondays"
|
||||||
onClick={() => console.log("Card clicked: Mutual Aid Mondays")}
|
onClick={() => console.warn("Card clicked: Mutual Aid Mondays")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Collapsed State - Medium */}
|
{/* Collapsed State - Medium */}
|
||||||
@@ -668,17 +841,17 @@ export default function ComponentsPreview() {
|
|||||||
className="w-[289px]"
|
className="w-[289px]"
|
||||||
logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png"
|
logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png"
|
||||||
logoAlt="Mutual Aid Mondays"
|
logoAlt="Mutual Aid Mondays"
|
||||||
onClick={() => console.log("Card clicked: Mutual Aid Mondays")}
|
onClick={() => console.warn("Card clicked: Mutual Aid Mondays")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Expanded State - Large */}
|
{/* Expanded State - Large */}
|
||||||
<section className="space-y-[var(--spacing-scale-024)]">
|
<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)]">
|
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||||
Expanded State - Large (L)
|
Expanded State - Large (L)
|
||||||
</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="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||||
<RuleCard
|
<RuleCard
|
||||||
title="Mutual Aid Mondays"
|
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."
|
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"
|
logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png"
|
||||||
logoAlt="Mutual Aid Mondays"
|
logoAlt="Mutual Aid Mondays"
|
||||||
categories={ruleCardCategories}
|
categories={ruleCardCategories}
|
||||||
onClick={() => console.log("Card clicked: Mutual Aid Mondays")}
|
onClick={() => console.warn("Card clicked: Mutual Aid Mondays")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Expanded State - Medium */}
|
{/* Expanded State - Medium */}
|
||||||
@@ -710,16 +883,16 @@ export default function ComponentsPreview() {
|
|||||||
logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png"
|
logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png"
|
||||||
logoAlt="Mutual Aid Mondays"
|
logoAlt="Mutual Aid Mondays"
|
||||||
categories={ruleCardCategories}
|
categories={ruleCardCategories}
|
||||||
onClick={() => console.log("Card clicked: Mutual Aid Mondays")}
|
onClick={() => console.warn("Card clicked: Mutual Aid Mondays")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Different Background Colors */}
|
{/* Different Background Colors */}
|
||||||
<section className="space-y-[var(--spacing-scale-024)]">
|
<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)]">
|
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||||
Different Background Colors
|
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="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)]">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-[var(--spacing-scale-024)]">
|
||||||
<RuleCard
|
<RuleCard
|
||||||
@@ -737,7 +910,7 @@ export default function ComponentsPreview() {
|
|||||||
height={103}
|
height={103}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onClick={() => console.log("Consensus clusters selected")}
|
onClick={() => console.warn("Consensus clusters selected")}
|
||||||
/>
|
/>
|
||||||
<RuleCard
|
<RuleCard
|
||||||
title="Consensus"
|
title="Consensus"
|
||||||
@@ -754,10 +927,10 @@ export default function ComponentsPreview() {
|
|||||||
height={103}
|
height={103}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onClick={() => console.log("Consensus selected")}
|
onClick={() => console.warn("Consensus selected")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Logo Fallback */}
|
{/* Logo Fallback */}
|
||||||
@@ -774,9 +947,9 @@ export default function ComponentsPreview() {
|
|||||||
size="L"
|
size="L"
|
||||||
className="w-[525px]"
|
className="w-[525px]"
|
||||||
communityInitials="CE"
|
communityInitials="CE"
|
||||||
onClick={() => console.log("Community Example selected")}
|
onClick={() => console.warn("Community Example selected")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* MultiSelect Component */}
|
{/* MultiSelect Component */}
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export default async function BlogPostPage({ params }: PageProps) {
|
|||||||
url: "https://communityrule.com",
|
url: "https://communityrule.com",
|
||||||
logo: {
|
logo: {
|
||||||
"@type": "ImageObject",
|
"@type": "ImageObject",
|
||||||
url: "https://communityrule.com/assets/Logo.svg",
|
url: "https://communityrule.com/assets/logo/Logo.svg",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
datePublished: post.frontmatter.date,
|
datePublished: post.frontmatter.date,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getAllBlogPosts } from "../../../lib/content";
|
import { getAllBlogPosts } from "../../../lib/content";
|
||||||
import ContentThumbnailTemplate from "../../../components/content/ContentThumbnailTemplate";
|
import ContentThumbnailTemplate from "../../components/content/ContentThumbnailTemplate";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|||||||
+18
-12
@@ -12,12 +12,15 @@ const LogoWall = dynamic(() => import("../components/sections/LogoWall"), {
|
|||||||
ssr: true,
|
ssr: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const NumberedCards = dynamic(() => import("../components/sections/NumberedCards"), {
|
const NumberedCards = dynamic(
|
||||||
loading: () => (
|
() => import("../components/sections/NumberedCards"),
|
||||||
<section className="py-[var(--spacing-scale-032)] min-h-[300px]" />
|
{
|
||||||
),
|
loading: () => (
|
||||||
ssr: true,
|
<section className="py-[var(--spacing-scale-032)] min-h-[300px]" />
|
||||||
});
|
),
|
||||||
|
ssr: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const RuleStack = dynamic(() => import("../components/sections/RuleStack"), {
|
const RuleStack = dynamic(() => import("../components/sections/RuleStack"), {
|
||||||
loading: () => (
|
loading: () => (
|
||||||
@@ -26,12 +29,15 @@ const RuleStack = dynamic(() => import("../components/sections/RuleStack"), {
|
|||||||
ssr: true,
|
ssr: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const FeatureGrid = dynamic(() => import("../components/sections/FeatureGrid"), {
|
const FeatureGrid = dynamic(
|
||||||
loading: () => (
|
() => import("../components/sections/FeatureGrid"),
|
||||||
<section className="py-[var(--spacing-scale-032)] min-h-[500px]" />
|
{
|
||||||
),
|
loading: () => (
|
||||||
ssr: true,
|
<section className="py-[var(--spacing-scale-032)] min-h-[500px]" />
|
||||||
});
|
),
|
||||||
|
ssr: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const QuoteBlock = dynamic(() => import("../components/sections/QuoteBlock"), {
|
const QuoteBlock = dynamic(() => import("../components/sections/QuoteBlock"), {
|
||||||
loading: () => (
|
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> {
|
export interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|||||||
@@ -40,7 +40,25 @@ const WebVitalsDashboardContainer = memo(() => {
|
|||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
import("web-vitals").then((webVitals) => {
|
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"] }) => {
|
getLCP((metric: { value: number; rating: VitalData["rating"] }) => {
|
||||||
setVitals((prev) => ({
|
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 variant = getVariantFromTypeAndPalette(buttonType, buttonPalette);
|
||||||
|
|
||||||
const sizeStyles: Record<string, string> = {
|
const sizeStyles: Record<string, string> = {
|
||||||
xsmall:
|
xsmall: "p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]",
|
||||||
"p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]",
|
small: "p-[var(--spacing-scale-008)] 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)]",
|
medium: "p-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]",
|
||||||
large:
|
large: "p-[var(--spacing-scale-012)] gap-[var(--spacing-scale-006)]",
|
||||||
"p-[var(--spacing-scale-012)] gap-[var(--spacing-scale-006)]",
|
xlarge: "p-[var(--spacing-scale-016)] gap-[var(--spacing-scale-008)]",
|
||||||
xlarge:
|
|
||||||
"p-[var(--spacing-scale-016)] gap-[var(--spacing-scale-008)]",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fontStyles: Record<string, string> = {
|
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",
|
"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:
|
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",
|
"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:
|
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",
|
"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":
|
"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 }) => {
|
const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
|
||||||
// Base classes common to all sizes
|
// 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
|
// If size prop is provided, use explicit size classes
|
||||||
// Otherwise, use responsive breakpoints for backward compatibility
|
// Otherwise, use responsive breakpoints for backward compatibility
|
||||||
@@ -40,16 +41,22 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
|
|||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
Small: "flex flex-col items-end justify-center gap-4 p-5 relative",
|
Small: "flex flex-col items-end justify-center gap-4 p-5 relative",
|
||||||
Medium: "flex flex-row items-center gap-8 p-8 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",
|
Large:
|
||||||
XLarge: "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
|
"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
|
// Text size classes
|
||||||
const textClasses = {
|
const textClasses = {
|
||||||
Small: "font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]",
|
Small:
|
||||||
Medium: "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
|
"font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]",
|
||||||
Large: "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
|
Medium:
|
||||||
XLarge: "font-bricolage-grotesque font-medium text-[32px] leading-[32px] text-[#141414]",
|
"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
|
// Section number wrapper classes - Small doesn't need a wrapper
|
||||||
@@ -76,9 +83,7 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
|
|||||||
<SectionNumber number={number} />
|
<SectionNumber number={number} />
|
||||||
|
|
||||||
{/* Card Content */}
|
{/* Card Content */}
|
||||||
<p className={textClasses[size]}>
|
<p className={textClasses[size]}>{text}</p>
|
||||||
{text}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -92,9 +97,7 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
|
|||||||
|
|
||||||
{/* Card Content */}
|
{/* Card Content */}
|
||||||
<div className={contentClasses[size]}>
|
<div className={contentClasses[size]}>
|
||||||
<p className={textClasses[size]}>
|
<p className={textClasses[size]}>{text}</p>
|
||||||
{text}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -103,7 +106,9 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
|
|||||||
// Responsive breakpoints for backward compatibility (matches original behavior)
|
// Responsive breakpoints for backward compatibility (matches original behavior)
|
||||||
// Maps to: Small (mobile) -> Medium (sm) -> Large (lg) -> XLarge (xl)
|
// Maps to: Small (mobile) -> Medium (sm) -> Large (lg) -> XLarge (xl)
|
||||||
return (
|
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 */}
|
{/* 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">
|
<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} />
|
<SectionNumber number={number} />
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ export interface Category {
|
|||||||
chipOptions: ChipOption[];
|
chipOptions: ChipOption[];
|
||||||
onChipClick?: (categoryName: string, chipId: string) => void;
|
onChipClick?: (categoryName: string, chipId: string) => void;
|
||||||
onAddClick?: (categoryName: 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;
|
onCustomChipClose?: (categoryName: string, chipId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,31 +31,36 @@ export function RuleCardView({
|
|||||||
|
|
||||||
// Card dimensions - use CSS classes from className if provided, otherwise use size-based logic
|
// Card dimensions - use CSS classes from className if provided, otherwise use size-based logic
|
||||||
// Check if className already has padding/gap classes
|
// 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 hasResponsiveGap = className?.includes("gap-[");
|
||||||
|
|
||||||
const cardPadding = hasResponsivePadding
|
const cardPadding = hasResponsivePadding
|
||||||
? "" // If className has responsive padding, don't add size-based padding
|
? "" // If className has responsive padding, don't add size-based padding
|
||||||
: isLarge || isSmall
|
: isLarge || isSmall
|
||||||
? "p-[24px]"
|
? "p-[24px]"
|
||||||
: isMedium
|
: isMedium
|
||||||
? "p-[16px]"
|
? "p-[16px]"
|
||||||
: "pb-[24px] pt-[12px] px-[12px]"; // XS: asymmetric padding
|
: "pb-[24px] pt-[12px] px-[12px]"; // XS: asymmetric padding
|
||||||
const cardGap = expanded
|
const cardGap = expanded
|
||||||
? "gap-[16px]"
|
? "gap-[16px]"
|
||||||
: hasResponsiveGap
|
: hasResponsiveGap
|
||||||
? "" // If className has responsive gap, don't add size-based gap
|
? "" // If className has responsive gap, don't add size-based gap
|
||||||
: isLarge
|
: isLarge
|
||||||
? "gap-[10px]"
|
? "gap-[10px]"
|
||||||
: isMedium
|
: isMedium
|
||||||
? "gap-[12px]"
|
? "gap-[12px]"
|
||||||
: "gap-[18px]"; // XS and S: 18px gap
|
: "gap-[18px]"; // XS and S: 18px gap
|
||||||
const cardWidth = expanded
|
const cardWidth = expanded
|
||||||
? isLarge
|
? isLarge
|
||||||
? "w-[568px]"
|
? "w-[568px]"
|
||||||
: isMedium
|
: isMedium
|
||||||
? "w-[398px]"
|
? "w-[398px]"
|
||||||
: "" // XS and S: no fixed width
|
: "" // XS and S: no fixed width
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
// Logo/Icon dimensions - use CSS responsive classes
|
// Logo/Icon dimensions - use CSS responsive classes
|
||||||
@@ -81,16 +86,18 @@ export function RuleCardView({
|
|||||||
const descriptionClass = isLarge
|
const descriptionClass = isLarge
|
||||||
? "font-inter font-medium text-[18px] leading-[24px]"
|
? "font-inter font-medium text-[18px] leading-[24px]"
|
||||||
: isMedium
|
: isMedium
|
||||||
? "font-inter font-medium text-[14px] leading-[16px]"
|
? "font-inter font-medium text-[14px] leading-[16px]"
|
||||||
: isSmall
|
: isSmall
|
||||||
? "font-inter font-medium text-[14px] leading-[16px]" // S: 14px, medium, Inter
|
? "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-[12px] leading-[14px]"; // XS: 12px, medium, Inter
|
||||||
|
|
||||||
// Render logo/icon
|
// Render logo/icon
|
||||||
const renderLogo = () => {
|
const renderLogo = () => {
|
||||||
if (logoUrl) {
|
if (logoUrl) {
|
||||||
// Check if it's a localhost URL or external URL that needs regular img tag
|
// 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]`;
|
const containerClass = `${logoContainerClass} relative rounded-full overflow-hidden mix-blend-luminosity max-[639px]:p-[16px] min-[640px]:max-[1023px]:p-[12px]`;
|
||||||
|
|
||||||
@@ -124,7 +131,9 @@ export function RuleCardView({
|
|||||||
|
|
||||||
if (icon) {
|
if (icon) {
|
||||||
return (
|
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}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -138,8 +147,12 @@ export function RuleCardView({
|
|||||||
min-[1440px]:text-[36px]
|
min-[1440px]:text-[36px]
|
||||||
`;
|
`;
|
||||||
return (
|
return (
|
||||||
<div className={`${logoContainerClass} rounded-full bg-[var(--color-surface-default-primary)] flex items-center justify-center`}>
|
<div
|
||||||
<span className={`${initialsSize} font-bricolage-grotesque font-bold text-[var(--color-content-default-primary,white)]`}>
|
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}
|
{communityInitials}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,15 +162,14 @@ export function RuleCardView({
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Border radius - use CSS classes if provided via className, otherwise use size-based logic
|
// 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
|
? "" // If className already has border radius, don't add size-based one
|
||||||
: isExtraSmall
|
: isExtraSmall
|
||||||
? "rounded-[var(--measures-radius-200,8px)]"
|
? "rounded-[var(--measures-radius-200,8px)]"
|
||||||
: isSmall
|
: isSmall
|
||||||
? "rounded-[var(--measures-radius-300,12px)]"
|
? "rounded-[var(--measures-radius-300,12px)]"
|
||||||
: "rounded-[var(--radius-measures-radius-small)]";
|
: "rounded-[var(--radius-measures-radius-small)]";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -170,48 +182,60 @@ export function RuleCardView({
|
|||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
>
|
>
|
||||||
{/* Outermost container with bottom border - taller to match Figma */}
|
{/* 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
|
border-b border-black border-solid flex items-center relative shrink-0 w-full
|
||||||
max-[639px]:h-[72px]
|
max-[639px]:h-[72px]
|
||||||
min-[640px]:max-[1023px]:h-[80px]
|
min-[640px]:max-[1023px]:h-[80px]
|
||||||
min-[1024px]:max-[1439px]:h-[88px]
|
min-[1024px]:max-[1439px]:h-[88px]
|
||||||
min-[1440px]:h-[136px]
|
min-[1440px]:h-[136px]
|
||||||
`}>
|
`}
|
||||||
|
>
|
||||||
{/* Logo/Icon - fixed width/height, vertically centered, does not touch bottom */}
|
{/* Logo/Icon - fixed width/height, vertically centered, does not touch bottom */}
|
||||||
{renderLogo() && (
|
{renderLogo() && (
|
||||||
<div className={`
|
<div
|
||||||
|
className={`
|
||||||
flex items-center justify-center shrink-0
|
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
|
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-[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-[1024px]:max-[1439px]:w-[56px] min-[1024px]:max-[1439px]:h-[56px]
|
||||||
min-[1440px]:w-[103px] min-[1440px]:h-[103px]
|
min-[1440px]:w-[103px] min-[1440px]:h-[103px]
|
||||||
`}>
|
`}
|
||||||
|
>
|
||||||
{renderLogo()}
|
{renderLogo()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Spacing between icon and title */}
|
{/* Spacing between icon and title */}
|
||||||
<div className="
|
<div
|
||||||
|
className="
|
||||||
max-[1023px]:hidden
|
max-[1023px]:hidden
|
||||||
min-[1024px]:w-[16px] min-[1024px]:shrink-0
|
min-[1024px]:w-[16px] min-[1024px]:shrink-0
|
||||||
" />
|
"
|
||||||
|
/>
|
||||||
{/* Container with no padding and left border - extends full height to touch bottom */}
|
{/* Container with no padding and left border - extends full height to touch bottom */}
|
||||||
{title && (
|
{title && (
|
||||||
<div className={`
|
<div
|
||||||
|
className={`
|
||||||
flex-1 min-w-0 h-full flex
|
flex-1 min-w-0 h-full flex
|
||||||
max-[1023px]:border-0
|
max-[1023px]:border-0
|
||||||
min-[1024px]:border-l min-[1024px]:border-black min-[1024px]:border-solid
|
min-[1024px]:border-l min-[1024px]:border-black min-[1024px]:border-solid
|
||||||
`}>
|
`}
|
||||||
|
>
|
||||||
{/* Inner container for header text with padding */}
|
{/* Inner container for header text with padding */}
|
||||||
<div className={`
|
<div
|
||||||
|
className={`
|
||||||
flex items-center justify-center w-full
|
flex items-center justify-center w-full
|
||||||
max-[639px]:pl-[8px] max-[639px]:py-[8px]
|
max-[639px]:pl-[8px] max-[639px]:py-[8px]
|
||||||
min-[640px]:max-[1023px]:pl-[12px] min-[640px]:max-[1023px]:py-[12px]
|
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-[1024px]:max-[1439px]:px-[16px] min-[1024px]:max-[1439px]:py-[12px]
|
||||||
min-[1440px]:px-[16px] min-[1440px]:py-[24px]
|
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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -237,7 +261,11 @@ export function RuleCardView({
|
|||||||
category.onAddClick?.(category.name);
|
category.onAddClick?.(category.name);
|
||||||
}}
|
}}
|
||||||
onCustomChipConfirm={(chipId, value) => {
|
onCustomChipConfirm={(chipId, value) => {
|
||||||
category.onCustomChipConfirm?.(category.name, chipId, value);
|
category.onCustomChipConfirm?.(
|
||||||
|
category.name,
|
||||||
|
chipId,
|
||||||
|
value,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
onCustomChipClose={(chipId) => {
|
onCustomChipClose={(chipId) => {
|
||||||
category.onCustomChipClose?.(category.name, chipId);
|
category.onCustomChipClose?.(category.name, chipId);
|
||||||
@@ -250,11 +278,9 @@ export function RuleCardView({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Footer: Description */}
|
{/* Footer: Description */}
|
||||||
{description && (
|
{description && (
|
||||||
<div className="border-t border-black border-solid pt-[16px] relative shrink-0 w-full">
|
<div className="border-t border-black border-solid pt-[16px] relative shrink-0 w-full">
|
||||||
<p className={`${descriptionClass} text-black`}>
|
<p className={`${descriptionClass} text-black`}>{description}</p>
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -263,8 +289,8 @@ export function RuleCardView({
|
|||||||
description && (
|
description && (
|
||||||
<div className="flex items-center justify-center relative shrink-0 w-full">
|
<div className="flex items-center justify-center relative shrink-0 w-full">
|
||||||
<p className={`${descriptionClass} text-black flex-1`}>
|
<p className={`${descriptionClass} text-black flex-1`}>
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { BlogPost } from "../../../../lib/content";
|
import type { BlogPost } from "../../../../lib/content";
|
||||||
|
|
||||||
export type ContentContainerSizeValue = "xs" | "responsive" | "Xs" | "Responsive";
|
export type ContentContainerSizeValue =
|
||||||
|
| "xs"
|
||||||
|
| "responsive"
|
||||||
|
| "Xs"
|
||||||
|
| "Responsive";
|
||||||
|
|
||||||
export interface ContentContainerProps {
|
export interface ContentContainerProps {
|
||||||
post: BlogPost;
|
post: BlogPost;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { BlogPost } from "../../../../lib/content";
|
import type { BlogPost } from "../../../../lib/content";
|
||||||
|
|
||||||
export type ContentThumbnailTemplateVariantValue = "vertical" | "horizontal" | "Vertical" | "Horizontal";
|
export type ContentThumbnailTemplateVariantValue =
|
||||||
|
| "vertical"
|
||||||
|
| "horizontal"
|
||||||
|
| "Vertical"
|
||||||
|
| "Horizontal";
|
||||||
|
|
||||||
export interface ContentThumbnailTemplateProps {
|
export interface ContentThumbnailTemplateProps {
|
||||||
post: BlogPost;
|
post: BlogPost;
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { memo } from "react";
|
|||||||
import { useComponentId } from "../../../hooks";
|
import { useComponentId } from "../../../hooks";
|
||||||
import { CheckboxView } from "./Checkbox.view";
|
import { CheckboxView } from "./Checkbox.view";
|
||||||
import type { CheckboxProps } from "./Checkbox.types";
|
import type { CheckboxProps } from "./Checkbox.types";
|
||||||
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
|
import {
|
||||||
|
normalizeMode,
|
||||||
|
normalizeState,
|
||||||
|
} from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
const CheckboxContainer = memo<CheckboxProps>(
|
const CheckboxContainer = memo<CheckboxProps>(
|
||||||
({
|
({
|
||||||
@@ -43,7 +46,9 @@ const CheckboxContainer = memo<CheckboxProps>(
|
|||||||
transition-all
|
transition-all
|
||||||
duration-200
|
duration-200
|
||||||
ease-in-out
|
ease-in-out
|
||||||
`.trim().replace(/\s+/g, " ");
|
`
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, " ");
|
||||||
|
|
||||||
// Get box styles based on state and checked status per Figma designs
|
// Get box styles based on state and checked status per Figma designs
|
||||||
const getBoxStyles = (): string => {
|
const getBoxStyles = (): string => {
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ const CheckboxGroupContainer = ({
|
|||||||
const groupId = name || `checkbox-group-${generatedId}`;
|
const groupId = name || `checkbox-group-${generatedId}`;
|
||||||
|
|
||||||
// Internal state to track checked values (only used if value prop is not provided)
|
// 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
|
// Use controlled value if provided, otherwise use internal state
|
||||||
const checkedValues = value !== undefined ? value : internalCheckedValues;
|
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 there's subtext, render checkbox without label and handle layout separately
|
||||||
if (option.subtext) {
|
if (option.subtext) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={option.value} className="flex gap-[8px] items-start">
|
||||||
key={option.value}
|
|
||||||
className="flex gap-[8px] items-start"
|
|
||||||
>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ const ChipContainer = memo<ChipProps>(
|
|||||||
}
|
}
|
||||||
}, [isCustom]);
|
}, [isCustom]);
|
||||||
|
|
||||||
const handleCheck = (value: string, event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleCheck = (
|
||||||
|
value: string,
|
||||||
|
event: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
) => {
|
||||||
if (onCheck && value.trim()) {
|
if (onCheck && value.trim()) {
|
||||||
onCheck(value.trim(), event);
|
onCheck(value.trim(), event);
|
||||||
// Reset input after successful check
|
// Reset input after successful check
|
||||||
@@ -63,7 +66,10 @@ const ChipContainer = memo<ChipProps>(
|
|||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (event.key === "Enter" && inputValue.trim() && onCheck) {
|
if (event.key === "Enter" && inputValue.trim() && onCheck) {
|
||||||
event.preventDefault();
|
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) {
|
} else if (event.key === "Escape" && onClose) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleClose(event as unknown as React.MouseEvent<HTMLButtonElement>);
|
handleClose(event as unknown as React.MouseEvent<HTMLButtonElement>);
|
||||||
@@ -95,4 +101,3 @@ const ChipContainer = memo<ChipProps>(
|
|||||||
ChipContainer.displayName = "Chip";
|
ChipContainer.displayName = "Chip";
|
||||||
|
|
||||||
export default ChipContainer;
|
export default ChipContainer;
|
||||||
|
|
||||||
|
|||||||
@@ -68,4 +68,3 @@ export interface ChipViewProps {
|
|||||||
inputRef?: React.RefObject<HTMLInputElement>;
|
inputRef?: React.RefObject<HTMLInputElement>;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,31 +43,25 @@ function ChipView({
|
|||||||
// Use consistent border width to prevent layout shift
|
// Use consistent border width to prevent layout shift
|
||||||
const borderWidth = isSmall ? "border-[1.25px]" : "border-2";
|
const borderWidth = isSmall ? "border-[1.25px]" : "border-2";
|
||||||
|
|
||||||
let background = "bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
|
let background =
|
||||||
let border =
|
"bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
|
||||||
`${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`;
|
let border = `${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`;
|
||||||
let textColor =
|
let textColor =
|
||||||
"text-[color:var(--color-content-default-brand-primary,#fefcc9)]";
|
"text-[color:var(--color-content-default-brand-primary,#fefcc9)]";
|
||||||
|
|
||||||
if (isDefault) {
|
if (isDefault) {
|
||||||
if (state === "custom") {
|
if (state === "custom") {
|
||||||
background =
|
background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background for custom
|
||||||
"bg-[var(--color-surface-default-secondary,#141414)]"; // dark background for custom
|
|
||||||
border = "border-none";
|
border = "border-none";
|
||||||
textColor =
|
textColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
|
||||||
"text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
|
|
||||||
} else if (state === "disabled") {
|
} else if (state === "disabled") {
|
||||||
background =
|
background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background
|
||||||
"bg-[var(--color-surface-default-secondary,#141414)]"; // dark background
|
|
||||||
border = "border-none";
|
border = "border-none";
|
||||||
textColor =
|
textColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
|
||||||
"text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
|
|
||||||
} else if (isSelected) {
|
} else if (isSelected) {
|
||||||
background =
|
background = "bg-[var(--color-surface-inverse-brandaccent,#fdfaa8)]"; // yellow selected
|
||||||
"bg-[var(--color-surface-inverse-brandaccent,#fdfaa8)]"; // yellow selected
|
|
||||||
border = `${borderWidth} border-[var(--color-border-default-brand-primary,#fdfaa8)]`;
|
border = `${borderWidth} border-[var(--color-border-default-brand-primary,#fdfaa8)]`;
|
||||||
textColor =
|
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
|
||||||
"text-[color:var(--color-content-inverse-primary,black)]";
|
|
||||||
} else {
|
} else {
|
||||||
// Unselected default
|
// Unselected default
|
||||||
background =
|
background =
|
||||||
@@ -78,24 +72,20 @@ function ChipView({
|
|||||||
}
|
}
|
||||||
} else if (isInverse) {
|
} else if (isInverse) {
|
||||||
if (state === "disabled") {
|
if (state === "disabled") {
|
||||||
background =
|
background = "bg-[var(--color-surface-inverse-tertiary,#d2d2d2)]";
|
||||||
"bg-[var(--color-surface-inverse-tertiary,#d2d2d2)]";
|
|
||||||
border = "border-none";
|
border = "border-none";
|
||||||
textColor =
|
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
|
||||||
"text-[color:var(--color-content-inverse-primary,black)]";
|
|
||||||
} else if (isSelected) {
|
} else if (isSelected) {
|
||||||
background =
|
background =
|
||||||
"bg-[var(--color-surface-default-semi-opaque,rgba(0,0,0,0.05))]";
|
"bg-[var(--color-surface-default-semi-opaque,rgba(0,0,0,0.05))]";
|
||||||
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
|
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
|
||||||
textColor =
|
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
|
||||||
"text-[color:var(--color-content-inverse-primary,black)]";
|
|
||||||
} else {
|
} else {
|
||||||
// Unselected / custom inverse
|
// Unselected / custom inverse
|
||||||
background =
|
background =
|
||||||
"bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
|
"bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]";
|
||||||
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
|
border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`;
|
||||||
textColor =
|
textColor = "text-[color:var(--color-content-inverse-primary,black)]";
|
||||||
"text-[color:var(--color-content-inverse-primary,black)]";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +124,9 @@ function ChipView({
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement | HTMLDivElement>) => {
|
const handleClick = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement | HTMLDivElement>,
|
||||||
|
) => {
|
||||||
if (isDisabled) {
|
if (isDisabled) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
@@ -162,7 +154,9 @@ function ChipView({
|
|||||||
}}
|
}}
|
||||||
{...sharedA11y}
|
{...sharedA11y}
|
||||||
>
|
>
|
||||||
<div className={`flex items-center ${isSmall ? "gap-[8px]" : "gap-[12px]"}`}>
|
<div
|
||||||
|
className={`flex items-center ${isSmall ? "gap-[8px]" : "gap-[12px]"}`}
|
||||||
|
>
|
||||||
{/* Check button */}
|
{/* Check button */}
|
||||||
{onCheck && (
|
{onCheck && (
|
||||||
<button
|
<button
|
||||||
@@ -208,7 +202,9 @@ function ChipView({
|
|||||||
placeholder="Type to add"
|
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)]"
|
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={{
|
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",
|
lineHeight: isSmall ? "16px" : "24px",
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
@@ -259,9 +255,7 @@ function ChipView({
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
{...sharedA11y}
|
{...sharedA11y}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-center">
|
<span className="flex items-center justify-center">{label}</span>
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
{onRemove && !isDisabled && (
|
{onRemove && !isDisabled && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -284,4 +278,3 @@ function ChipView({
|
|||||||
ChipView.displayName = "ChipView";
|
ChipView.displayName = "ChipView";
|
||||||
|
|
||||||
export default memo(ChipView);
|
export default memo(ChipView);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export { default } from "./Chip.container";
|
export { default } from "./Chip.container";
|
||||||
export type { ChipProps } from "./Chip.types";
|
export type { ChipProps } from "./Chip.types";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import MultiSelectView from "./MultiSelect.view";
|
import MultiSelectView from "./MultiSelect.view";
|
||||||
import type { MultiSelectProps } from "./MultiSelect.types";
|
import type { MultiSelectProps } from "./MultiSelect.types";
|
||||||
import { normalizeMultiSelectSize, normalizeChipPalette } from "../../../../lib/propNormalization";
|
import {
|
||||||
|
normalizeMultiSelectSize,
|
||||||
|
normalizeChipPalette,
|
||||||
|
} from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
const MultiSelectContainer = memo<MultiSelectProps>(
|
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 {
|
export interface ChipOption {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ function MultiSelectView({
|
|||||||
const chipSize = isSmall ? "S" : "M";
|
const chipSize = isSmall ? "S" : "M";
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Label using InputLabel component */}
|
||||||
{formHeader && label && (
|
{formHeader && label && (
|
||||||
<InputLabel
|
<InputLabel
|
||||||
@@ -45,7 +47,9 @@ function MultiSelectView({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Chips container */}
|
{/* 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) => (
|
{options.map((option) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={option.id}
|
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 && (
|
{addButton && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -87,18 +91,26 @@ function MultiSelectView({
|
|||||||
!addButtonText
|
!addButtonText
|
||||||
? // Circular button with border (RuleCard style)
|
? // 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`
|
`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)
|
: // Text add control (default palette: white label + brand “+”; inverse: inverse primary for both)
|
||||||
`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`
|
`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
|
<svg
|
||||||
width={isSmall ? "14" : "20"}
|
width={isSmall ? "14" : "20"}
|
||||||
height={isSmall ? "14" : "20"}
|
height={isSmall ? "14" : "20"}
|
||||||
viewBox="0 0 14 14"
|
viewBox="0 0 14 14"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
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
|
<path
|
||||||
d="M7 3V11M3 7H11"
|
d="M7 3V11M3 7H11"
|
||||||
@@ -108,9 +120,14 @@ function MultiSelectView({
|
|||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{/* Text - only show if addButtonText is provided */}
|
|
||||||
{addButtonText && (
|
{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}
|
{addButtonText}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
import { memo, useCallback, useId } from "react";
|
import { memo, useCallback, useId } from "react";
|
||||||
import { RadioButtonView } from "./RadioButton.view";
|
import { RadioButtonView } from "./RadioButton.view";
|
||||||
import type { RadioButtonProps } from "./RadioButton.types";
|
import type { RadioButtonProps } from "./RadioButton.types";
|
||||||
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
|
import {
|
||||||
|
normalizeMode,
|
||||||
|
normalizeState,
|
||||||
|
} from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
const RadioButtonContainer = ({
|
const RadioButtonContainer = ({
|
||||||
checked = false,
|
checked = false,
|
||||||
@@ -42,7 +45,9 @@ const RadioButtonContainer = ({
|
|||||||
duration-200
|
duration-200
|
||||||
ease-in-out
|
ease-in-out
|
||||||
p-[4px]
|
p-[4px]
|
||||||
`.trim().replace(/\s+/g, " ");
|
`
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, " ");
|
||||||
|
|
||||||
// Get box styles based on mode and checked status per Figma designs
|
// Get box styles based on mode and checked status per Figma designs
|
||||||
const getBoxStyles = (): string => {
|
const getBoxStyles = (): string => {
|
||||||
@@ -75,7 +80,8 @@ const RadioButtonContainer = ({
|
|||||||
: "border-[var(--color-border-invert-primary,white)]";
|
: "border-[var(--color-border-invert-primary,white)]";
|
||||||
|
|
||||||
// Hover border: inverse brand primary for both selected and unselected per Figma
|
// 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
|
// Focus border: when focused and checked, border should be white per Figma
|
||||||
const focusBorder = checked
|
const focusBorder = checked
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ export function RadioButtonView({
|
|||||||
checked && mode === "standard"
|
checked && mode === "standard"
|
||||||
? "bg-[var(--color-content-default-brand-primary,#fefcc9)] group-hover:!bg-[#333000]"
|
? "bg-[var(--color-content-default-brand-primary,#fefcc9)] group-hover:!bg-[#333000]"
|
||||||
: checked && mode === "inverse"
|
: checked && mode === "inverse"
|
||||||
? "bg-[var(--color-content-default-primary,#000000)]"
|
? "bg-[var(--color-content-default-primary,#000000)]"
|
||||||
: "bg-transparent"
|
: "bg-transparent"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
import { memo, useCallback, useId } from "react";
|
import { memo, useCallback, useId } from "react";
|
||||||
import { RadioGroupView } from "./RadioGroup.view";
|
import { RadioGroupView } from "./RadioGroup.view";
|
||||||
import type { RadioGroupProps } from "./RadioGroup.types";
|
import type { RadioGroupProps } from "./RadioGroup.types";
|
||||||
import { normalizeMode, normalizeState } from "../../../../lib/propNormalization";
|
import {
|
||||||
|
normalizeMode,
|
||||||
|
normalizeState,
|
||||||
|
} from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
const RadioGroupContainer = ({
|
const RadioGroupContainer = ({
|
||||||
name,
|
name,
|
||||||
@@ -19,10 +22,11 @@ const RadioGroupContainer = ({
|
|||||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||||
const mode = normalizeMode(modeProp);
|
const mode = normalizeMode(modeProp);
|
||||||
// Normalize state, but handle "With Subtext" separately (it's represented by options with subtext)
|
// 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")
|
(stateProp.toLowerCase() === "with subtext" || stateProp === "With Subtext")
|
||||||
? "default" // "With Subtext" is handled via RadioOption.subtext, use default state
|
? "default" // "With Subtext" is handled via RadioOption.subtext, use default state
|
||||||
: normalizeState(stateProp);
|
: normalizeState(stateProp);
|
||||||
// Generate unique ID for accessibility if not provided
|
// Generate unique ID for accessibility if not provided
|
||||||
const generatedId = useId();
|
const generatedId = useId();
|
||||||
const groupId = name || `radio-group-${generatedId}`;
|
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 there's subtext, render radio button without label and handle layout separately
|
||||||
if (option.subtext) {
|
if (option.subtext) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={option.value} className="flex gap-[8px] items-start">
|
||||||
key={option.value}
|
|
||||||
className="flex gap-[8px] items-start"
|
|
||||||
>
|
|
||||||
<RadioButton
|
<RadioButton
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ import React, {
|
|||||||
import { useClickOutside } from "../../../hooks";
|
import { useClickOutside } from "../../../hooks";
|
||||||
import { SelectInputView } from "./SelectInput.view";
|
import { SelectInputView } from "./SelectInput.view";
|
||||||
import type { SelectInputProps } from "./SelectInput.types";
|
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>(
|
const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
||||||
(
|
(
|
||||||
@@ -46,7 +50,8 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
|||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
// Determine if label should be shown
|
// 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
|
// Normalize state - handle "state5" as disabled
|
||||||
let normalizedState = externalStateProp;
|
let normalizedState = externalStateProp;
|
||||||
@@ -57,8 +62,12 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
|||||||
|
|
||||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
// 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
|
// Note: labelVariant and size are normalized for future use but not yet implemented in the view
|
||||||
const _labelVariant = labelVariantProp ? normalizeLabelVariant(labelVariantProp) : undefined;
|
const _labelVariant = labelVariantProp
|
||||||
const _size = sizeProp ? normalizeSmallMediumLargeSize(sizeProp) : undefined;
|
? normalizeLabelVariant(labelVariantProp)
|
||||||
|
: undefined;
|
||||||
|
const _size = sizeProp
|
||||||
|
? normalizeSmallMediumLargeSize(sizeProp)
|
||||||
|
: undefined;
|
||||||
// Mark as intentionally unused for future implementation
|
// Mark as intentionally unused for future implementation
|
||||||
void _labelVariant;
|
void _labelVariant;
|
||||||
void _size;
|
void _size;
|
||||||
@@ -73,11 +82,14 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
|||||||
|
|
||||||
// Internal state management: track if focused and how (mouse vs keyboard)
|
// Internal state management: track if focused and how (mouse vs keyboard)
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
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);
|
const wasMouseDownRef = useRef(false);
|
||||||
|
|
||||||
// Determine if we should auto-manage focus (only when state is "default" or undefined)
|
// 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
|
// Sync internal state with external value prop
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -7,8 +7,18 @@ export interface SelectOptionData {
|
|||||||
|
|
||||||
import type { StateValue } from "../../../../lib/propNormalization";
|
import type { StateValue } from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
export type SelectInputLabelVariantValue = "default" | "horizontal" | "Default" | "Horizontal";
|
export type SelectInputLabelVariantValue =
|
||||||
export type SelectInputSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large";
|
| "default"
|
||||||
|
| "horizontal"
|
||||||
|
| "Default"
|
||||||
|
| "Horizontal";
|
||||||
|
export type SelectInputSizeValue =
|
||||||
|
| "small"
|
||||||
|
| "medium"
|
||||||
|
| "large"
|
||||||
|
| "Small"
|
||||||
|
| "Medium"
|
||||||
|
| "Large";
|
||||||
|
|
||||||
export interface SelectInputProps {
|
export interface SelectInputProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ export function SelectInputView({
|
|||||||
}: SelectInputViewProps) {
|
}: SelectInputViewProps) {
|
||||||
// Styles based on Figma design
|
// Styles based on Figma design
|
||||||
const containerClasses = "flex flex-col gap-[8px]";
|
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
|
// Button styles per Figma
|
||||||
const getButtonClasses = (): string => {
|
const getButtonClasses = (): string => {
|
||||||
@@ -101,7 +102,9 @@ export function SelectInputView({
|
|||||||
cursor-pointer
|
cursor-pointer
|
||||||
appearance-none
|
appearance-none
|
||||||
m-0
|
m-0
|
||||||
`.trim().replace(/\s+/g, " ");
|
`
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, " ");
|
||||||
|
|
||||||
if (disabled) {
|
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`;
|
return `${baseClasses} bg-[var(--color-surface-default-secondary)] text-[var(--color-content-inverse-tertiary,#2d2d2d)] border-[var(--color-border-default-primary)] cursor-not-allowed opacity-40`;
|
||||||
@@ -142,10 +145,7 @@ export function SelectInputView({
|
|||||||
{label && (
|
{label && (
|
||||||
<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 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">
|
<div className="flex gap-[var(--measures-spacing-050,2px)] items-center relative shrink-0">
|
||||||
<label
|
<label id={labelId} className={labelClasses}>
|
||||||
id={labelId}
|
|
||||||
className={labelClasses}
|
|
||||||
>
|
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
{asterisk && (
|
{asterisk && (
|
||||||
@@ -155,6 +155,7 @@ export function SelectInputView({
|
|||||||
)}
|
)}
|
||||||
{iconHelp && (
|
{iconHelp && (
|
||||||
<div className="relative shrink-0 size-[12px]">
|
<div className="relative shrink-0 size-[12px]">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
|
||||||
<img
|
<img
|
||||||
src={getAssetPath(ASSETS.ICON_HELP)}
|
src={getAssetPath(ASSETS.ICON_HELP)}
|
||||||
alt="Help"
|
alt="Help"
|
||||||
@@ -186,8 +187,10 @@ export function SelectInputView({
|
|||||||
onFocus={onButtonFocus}
|
onFocus={onButtonFocus}
|
||||||
onBlur={onButtonBlur}
|
onBlur={onButtonBlur}
|
||||||
>
|
>
|
||||||
<span className={`flex-1 text-left ${iconRight ? "pr-[32px]" : ""} ${textColorClass}`}>
|
<span
|
||||||
{textData ? displayText : placeholder}
|
className={`flex-1 text-left ${iconRight ? "pr-[32px]" : ""} ${textColorClass}`}
|
||||||
|
>
|
||||||
|
{textData ? displayText : _placeholder}
|
||||||
</span>
|
</span>
|
||||||
{iconRight && (
|
{iconRight && (
|
||||||
<div className="flex items-center justify-center shrink-0">
|
<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 {
|
export interface SelectOptionProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import { memo, forwardRef } from "react";
|
|||||||
import { useComponentId, useFormField } from "../../../hooks";
|
import { useComponentId, useFormField } from "../../../hooks";
|
||||||
import { TextAreaView } from "./TextArea.view";
|
import { TextAreaView } from "./TextArea.view";
|
||||||
import type { TextAreaProps } from "./TextArea.types";
|
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>(
|
const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||||
(
|
(
|
||||||
@@ -27,6 +32,7 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|||||||
textHint = false,
|
textHint = false,
|
||||||
formHeader = true,
|
formHeader = true,
|
||||||
showHelpIcon = false,
|
showHelpIcon = false,
|
||||||
|
appearance: appearanceProp = "default",
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -35,6 +41,7 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|||||||
const size = normalizeSmallMediumLargeSize(sizeProp);
|
const size = normalizeSmallMediumLargeSize(sizeProp);
|
||||||
const labelVariant = normalizeLabelVariant(labelVariantProp);
|
const labelVariant = normalizeLabelVariant(labelVariantProp);
|
||||||
const state = normalizeInputState(stateProp);
|
const state = normalizeInputState(stateProp);
|
||||||
|
const appearance = normalizeTextAreaAppearance(appearanceProp);
|
||||||
// Generate unique ID for accessibility if not provided
|
// Generate unique ID for accessibility if not provided
|
||||||
const { id: textareaId, labelId } = useComponentId("textarea", id);
|
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 = (): {
|
const getStateStyles = (): {
|
||||||
textarea: string;
|
textarea: string;
|
||||||
label: 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) {
|
if (disabled) {
|
||||||
return {
|
return {
|
||||||
textarea:
|
textarea:
|
||||||
@@ -138,8 +160,8 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|||||||
: `${currentSize.label} font-inter`;
|
: `${currentSize.label} font-inter`;
|
||||||
|
|
||||||
const textareaClasses = `
|
const textareaClasses = `
|
||||||
w-full border transition-all duration-200 ease-in-out
|
scrollbar-design w-full transition-all duration-200 ease-in-out resize-none
|
||||||
focus:outline-none focus:ring-0 resize-none
|
${appearance === "embedded" ? "rounded-[var(--radius-300,12px)]" : "border"}
|
||||||
${currentSize.textarea}
|
${currentSize.textarea}
|
||||||
${stateStyles.textarea}
|
${stateStyles.textarea}
|
||||||
${className}
|
${className}
|
||||||
@@ -180,6 +202,7 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|||||||
textHint={textHint}
|
textHint={textHint}
|
||||||
formHeader={formHeader}
|
formHeader={formHeader}
|
||||||
showHelpIcon={showHelpIcon}
|
showHelpIcon={showHelpIcon}
|
||||||
|
appearance={appearance}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
import type { InputStateValue } from "../../../../lib/propNormalization";
|
import type { InputStateValue } from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
export type TextAreaSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large";
|
export type TextAreaSizeValue =
|
||||||
export type TextAreaLabelVariantValue = "default" | "horizontal" | "Default" | "Horizontal";
|
| "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<
|
export interface TextAreaProps extends Omit<
|
||||||
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||||
@@ -47,6 +63,12 @@ export interface TextAreaProps extends Omit<
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
showHelpIcon?: boolean;
|
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 {
|
export interface TextAreaViewProps {
|
||||||
@@ -73,4 +95,5 @@ export interface TextAreaViewProps {
|
|||||||
textHint?: boolean;
|
textHint?: boolean;
|
||||||
formHeader?: boolean;
|
formHeader?: boolean;
|
||||||
showHelpIcon?: boolean;
|
showHelpIcon?: boolean;
|
||||||
|
appearance?: "default" | "embedded";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ export const TextAreaView = forwardRef<HTMLTextAreaElement, TextAreaViewProps>(
|
|||||||
textHint = false,
|
textHint = false,
|
||||||
formHeader = true,
|
formHeader = true,
|
||||||
showHelpIcon = false,
|
showHelpIcon = false,
|
||||||
|
appearance: _appearance,
|
||||||
|
// Component-only props: do not pass to DOM
|
||||||
|
size: _size,
|
||||||
|
labelVariant: _labelVariant,
|
||||||
|
state: _state,
|
||||||
|
error: _error,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -42,6 +48,7 @@ export const TextAreaView = forwardRef<HTMLTextAreaElement, TextAreaViewProps>(
|
|||||||
</label>
|
</label>
|
||||||
{showHelpIcon && (
|
{showHelpIcon && (
|
||||||
<div className="relative shrink-0 size-[12px]">
|
<div className="relative shrink-0 size-[12px]">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
|
||||||
<img
|
<img
|
||||||
src={getAssetPath(ASSETS.ICON_HELP)}
|
src={getAssetPath(ASSETS.ICON_HELP)}
|
||||||
alt="Help"
|
alt="Help"
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ import { memo, forwardRef, useState, useRef } from "react";
|
|||||||
import { useComponentId, useFormField } from "../../../hooks";
|
import { useComponentId, useFormField } from "../../../hooks";
|
||||||
import { TextInputView } from "./TextInput.view";
|
import { TextInputView } from "./TextInput.view";
|
||||||
import type { TextInputProps } from "./TextInput.types";
|
import type { TextInputProps } from "./TextInput.types";
|
||||||
import { normalizeInputState } from "../../../../lib/propNormalization";
|
import {
|
||||||
|
normalizeInputState,
|
||||||
|
normalizeTextInputSize,
|
||||||
|
} from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
|
const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
state: externalStateProp = "default",
|
state: externalStateProp = "default",
|
||||||
|
inputSize: inputSizeProp = "medium",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
error = false,
|
error = false,
|
||||||
label,
|
label,
|
||||||
@@ -31,18 +35,22 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
|
|||||||
) => {
|
) => {
|
||||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||||
const externalState = normalizeInputState(externalStateProp);
|
const externalState = normalizeInputState(externalStateProp);
|
||||||
|
const inputSize = normalizeTextInputSize(inputSizeProp);
|
||||||
|
|
||||||
// Generate unique ID for accessibility if not provided
|
// Generate unique ID for accessibility if not provided
|
||||||
const { id: inputId, labelId } = useComponentId("text-input", id);
|
const { id: inputId, labelId } = useComponentId("text-input", id);
|
||||||
|
|
||||||
// Internal state management: track if focused and how (mouse vs keyboard)
|
// Internal state management: track if focused and how (mouse vs keyboard)
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
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);
|
const wasMouseDownRef = useRef(false);
|
||||||
|
|
||||||
// Determine if we should auto-manage focus (only when state is "default" or undefined)
|
// 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
|
// 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:
|
// Determine actual state:
|
||||||
// - Active: when clicked (mouse focus)
|
// - Active: when clicked (mouse focus)
|
||||||
@@ -59,13 +67,21 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
|
|||||||
// Determine if input is filled (has value)
|
// Determine if input is filled (has value)
|
||||||
const isFilled = Boolean(value && value.trim().length > 0);
|
const isFilled = Boolean(value && value.trim().length > 0);
|
||||||
|
|
||||||
// Fixed size styles (medium only per Figma designs)
|
// Size styles based on inputSize prop
|
||||||
const sizeStyles = {
|
const sizeStyles =
|
||||||
input: "h-[40px] px-[12px] py-[8px] text-[16px]",
|
inputSize === "small"
|
||||||
label: "text-[14px] leading-[20px] font-medium",
|
? {
|
||||||
container: "gap-[8px]",
|
input: "h-[32px] px-[10px] py-[6px] text-[14px]",
|
||||||
radius: "var(--measures-radius-200,8px)",
|
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
|
// State styles based on Figma designs
|
||||||
const getStateStyles = (): {
|
const getStateStyles = (): {
|
||||||
@@ -158,17 +174,20 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
|
|||||||
: "text-[var(--color-content-default-tertiary,#b4b4b4)]";
|
: "text-[var(--color-content-default-tertiary,#b4b4b4)]";
|
||||||
|
|
||||||
// Form field handlers with disabled state handling
|
// Form field handlers with disabled state handling
|
||||||
const { handleChange, handleBlur } = useFormField<HTMLInputElement>(disabled, {
|
const { handleChange, handleBlur } = useFormField<HTMLInputElement>(
|
||||||
onChange,
|
disabled,
|
||||||
onBlur: (e) => {
|
{
|
||||||
if (shouldAutoManageFocus) {
|
onChange,
|
||||||
setIsFocused(false);
|
onBlur: (e) => {
|
||||||
setFocusMethod(null);
|
if (shouldAutoManageFocus) {
|
||||||
wasMouseDownRef.current = false;
|
setIsFocused(false);
|
||||||
}
|
setFocusMethod(null);
|
||||||
onBlur?.(e);
|
wasMouseDownRef.current = false;
|
||||||
|
}
|
||||||
|
onBlur?.(e);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
// Handle mouse down to detect mouse clicks
|
// Handle mouse down to detect mouse clicks
|
||||||
const handleMouseDown = () => {
|
const handleMouseDown = () => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { InputStateValue } from "../../../../lib/propNormalization";
|
import type { InputStateValue } from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
|
export type TextInputSizeValue = "small" | "medium" | "Small" | "Medium";
|
||||||
|
|
||||||
export interface TextInputProps extends Omit<
|
export interface TextInputProps extends Omit<
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
"size" | "onChange" | "onFocus" | "onBlur"
|
"size" | "onChange" | "onFocus" | "onBlur"
|
||||||
@@ -9,6 +11,12 @@ export interface TextInputProps extends Omit<
|
|||||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||||
*/
|
*/
|
||||||
state?: InputStateValue;
|
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;
|
disabled?: boolean;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -21,9 +29,10 @@ export interface TextInputProps extends Omit<
|
|||||||
showHelpIcon?: boolean;
|
showHelpIcon?: boolean;
|
||||||
/**
|
/**
|
||||||
* Whether to show hint text below input (Figma prop).
|
* 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
|
* @default false
|
||||||
*/
|
*/
|
||||||
textHint?: boolean;
|
textHint?: boolean | string;
|
||||||
/**
|
/**
|
||||||
* Whether to show form header (label and help icon) above input (Figma prop).
|
* Whether to show form header (label and help icon) above input (Figma prop).
|
||||||
* @default true
|
* @default true
|
||||||
@@ -55,6 +64,6 @@ export interface TextInputViewProps {
|
|||||||
isFilled?: boolean;
|
isFilled?: boolean;
|
||||||
inputWrapperClasses?: string;
|
inputWrapperClasses?: string;
|
||||||
focusRingClasses?: string;
|
focusRingClasses?: string;
|
||||||
textHint?: boolean;
|
textHint?: boolean | string;
|
||||||
formHeader?: boolean;
|
formHeader?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
|
|||||||
</label>
|
</label>
|
||||||
{showHelpIcon && (
|
{showHelpIcon && (
|
||||||
<div className="relative shrink-0 size-[12px]">
|
<div className="relative shrink-0 size-[12px]">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
|
||||||
<img
|
<img
|
||||||
src={getAssetPath(ASSETS.ICON_HELP)}
|
src={getAssetPath(ASSETS.ICON_HELP)}
|
||||||
alt="Help"
|
alt="Help"
|
||||||
@@ -80,7 +81,7 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
|
|||||||
{textHint && (
|
{textHint && (
|
||||||
<div className="flex items-start relative shrink-0 w-full">
|
<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)]">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
import { memo, useCallback, useId, forwardRef } from "react";
|
import { memo, useCallback, useId, forwardRef } from "react";
|
||||||
import { ToggleGroupView } from "./ToggleGroup.view";
|
import { ToggleGroupView } from "./ToggleGroup.view";
|
||||||
import type { ToggleGroupProps } from "./ToggleGroup.types";
|
import type { ToggleGroupProps } from "./ToggleGroup.types";
|
||||||
import { normalizeToggleState, normalizeToggleGroupPosition } from "../../../../lib/propNormalization";
|
import {
|
||||||
|
normalizeToggleState,
|
||||||
|
normalizeToggleGroupPosition,
|
||||||
|
} from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
const ToggleGroupContainer = memo(
|
const ToggleGroupContainer = memo(
|
||||||
forwardRef<HTMLButtonElement, ToggleGroupProps>((props, _ref) => {
|
forwardRef<HTMLButtonElement, ToggleGroupProps>((props, _ref) => {
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import type { StateValue } from "../../../../lib/propNormalization";
|
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<
|
export interface ToggleGroupProps extends Omit<
|
||||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
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 { memo } from "react";
|
||||||
import { normalizeSize } from "../../../lib/propNormalization";
|
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> {
|
interface AvatarProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||||
src: string;
|
src: string;
|
||||||
@@ -19,7 +27,8 @@ const Avatar = memo<AvatarProps>(
|
|||||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||||
const size = normalizeSize(sizeProp, "small");
|
const size = normalizeSize(sizeProp, "small");
|
||||||
const sizeStyles: Record<string, string> = {
|
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)]",
|
medium: "w-[var(--spacing-scale-018)] h-[var(--spacing-scale-018)]",
|
||||||
large: "w-[var(--spacing-scale-024)] h-[var(--spacing-scale-024)]",
|
large: "w-[var(--spacing-scale-024)] h-[var(--spacing-scale-024)]",
|
||||||
xlarge: "w-[var(--spacing-scale-032)] h-[var(--spacing-scale-032)]",
|
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}`;
|
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 { memo } from "react";
|
||||||
import { AlertView } from "./Alert.view";
|
import { AlertView } from "./Alert.view";
|
||||||
import type { AlertProps } from "./Alert.types";
|
import type { AlertProps } from "./Alert.types";
|
||||||
import { normalizeAlertStatus, normalizeAlertType } from "../../../../lib/propNormalization";
|
import {
|
||||||
|
normalizeAlertStatus,
|
||||||
|
normalizeAlertType,
|
||||||
|
} from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
const AlertContainer = memo<AlertProps>(
|
const AlertContainer = memo<AlertProps>(
|
||||||
({
|
({
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const CreateContainer = memo<CreateProps>(
|
|||||||
onClose,
|
onClose,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
headerContent,
|
||||||
children,
|
children,
|
||||||
footerContent,
|
footerContent,
|
||||||
showBackButton = true,
|
showBackButton = true,
|
||||||
@@ -113,6 +114,7 @@ const CreateContainer = memo<CreateProps>(
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={title}
|
title={title}
|
||||||
description={description}
|
description={description}
|
||||||
|
headerContent={headerContent}
|
||||||
// eslint-disable-next-line react/no-children-prop
|
// eslint-disable-next-line react/no-children-prop
|
||||||
children={children}
|
children={children}
|
||||||
footerContent={footerContent}
|
footerContent={footerContent}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
export interface CreateProps {
|
export interface CreateProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
/** Default header: title + description. Omit to use title/description. */
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
/** Custom header slot. When set, replaces title/description for full control. */
|
||||||
|
headerContent?: React.ReactNode;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
footerContent?: React.ReactNode;
|
footerContent?: React.ReactNode;
|
||||||
showBackButton?: boolean;
|
showBackButton?: boolean;
|
||||||
@@ -17,35 +20,12 @@ export interface CreateProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
ariaLabelledBy?: string;
|
ariaLabelledBy?: string;
|
||||||
/**
|
/** Figma / design alignment (unused in implementation). */
|
||||||
* Whether to enable Create block array content type (Figma prop).
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
createBlockArray?: boolean;
|
createBlockArray?: boolean;
|
||||||
/**
|
|
||||||
* Whether to enable Text input content type (Figma prop).
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
textInput?: boolean;
|
textInput?: boolean;
|
||||||
/**
|
|
||||||
* Whether to enable Text area content type (Figma prop).
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
textArea?: boolean;
|
textArea?: boolean;
|
||||||
/**
|
|
||||||
* Whether to enable Multi-select content type (Figma prop).
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
multiSelect?: boolean;
|
multiSelect?: boolean;
|
||||||
/**
|
|
||||||
* Whether to enable Upload content type (Figma prop).
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
upload?: boolean;
|
upload?: boolean;
|
||||||
/**
|
|
||||||
* Whether to enable Proportion content type (Figma prop).
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
proportion?: boolean;
|
proportion?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +34,7 @@ export interface CreateViewProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
headerContent?: React.ReactNode;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
footerContent?: React.ReactNode;
|
footerContent?: React.ReactNode;
|
||||||
showBackButton: boolean;
|
showBackButton: boolean;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export function CreateView({
|
|||||||
onClose,
|
onClose,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
headerContent,
|
||||||
children,
|
children,
|
||||||
footerContent,
|
footerContent,
|
||||||
showBackButton,
|
showBackButton,
|
||||||
@@ -40,21 +41,23 @@ export function CreateView({
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Create Dialog */}
|
{/* Create Dialog: max-h ensures modal fits viewport; content scrolls inside */}
|
||||||
<div
|
<div
|
||||||
ref={createRef}
|
ref={createRef}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
aria-labelledby={ariaLabelledBy}
|
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 */}
|
{/* Header with close buttons */}
|
||||||
<ModalHeader onClose={onClose} onMoreOptions={onClose} />
|
<ModalHeader onClose={onClose} onMoreOptions={onClose} />
|
||||||
|
|
||||||
{/* Header Lockup Section (Sticky) */}
|
{/* Header: custom headerContent (when provided) or default title/description */}
|
||||||
{(title || description) && (
|
{headerContent !== undefined ? (
|
||||||
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0 sticky top-[48px] z-[2]">
|
<div className="shrink-0">{headerContent}</div>
|
||||||
|
) : title || description ? (
|
||||||
|
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
|
||||||
<ContentLockup
|
<ContentLockup
|
||||||
title={title}
|
title={title}
|
||||||
description={description}
|
description={description}
|
||||||
@@ -62,14 +65,14 @@ export function CreateView({
|
|||||||
alignment="left"
|
alignment="left"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
{/* Content Area (Scrollable) */}
|
{/* Content Area (scrollable when content overflows) */}
|
||||||
<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">
|
<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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer (always visible at bottom of modal) */}
|
||||||
<ModalFooter
|
<ModalFooter
|
||||||
showBackButton={showBackButton}
|
showBackButton={showBackButton}
|
||||||
showNextButton={showNextButton}
|
showNextButton={showNextButton}
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ import type { TooltipProps } from "./Tooltip.types";
|
|||||||
import { normalizeTooltipPosition } from "../../../../lib/propNormalization";
|
import { normalizeTooltipPosition } from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
const TooltipContainer = memo<TooltipProps>(
|
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)
|
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||||
const position = normalizeTooltipPosition(positionProp);
|
const position = normalizeTooltipPosition(positionProp);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { useTranslation } from "../../contexts/MessagesContext";
|
import { useTranslation } from "../../contexts/MessagesContext";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Logo from "../icons/Logo";
|
import Logo from "../asset/logo";
|
||||||
import Separator from "../utility/Separator";
|
import Separator from "../utility/Separator";
|
||||||
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ const Footer = memo(() => {
|
|||||||
lg:gap-[var(--spacing-measures-spacing-060,60px)]"
|
lg:gap-[var(--spacing-measures-spacing-060,60px)]"
|
||||||
>
|
>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Logo size="footer" />
|
<Logo size="footer" wordmark />
|
||||||
|
|
||||||
{/* Content section */}
|
{/* 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">
|
<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"
|
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")}
|
aria-label={t("social.bluesky.ariaLabel")}
|
||||||
>
|
>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element -- social logo */}
|
||||||
<img
|
<img
|
||||||
src={getAssetPath(ASSETS.BLUESKY_LOGO)}
|
src={getAssetPath(ASSETS.BLUESKY_LOGO)}
|
||||||
alt="Bluesky"
|
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"
|
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")}
|
aria-label={t("social.gitlab.ariaLabel")}
|
||||||
>
|
>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element -- social icon */}
|
||||||
<img
|
<img
|
||||||
src={getAssetPath(ASSETS.GITLAB_ICON)}
|
src={getAssetPath(ASSETS.GITLAB_ICON)}
|
||||||
alt="GitLab"
|
alt="GitLab"
|
||||||
|
|||||||
@@ -32,11 +32,17 @@ const MenuBarItemContainer = memo<MenuBarItemProps>(
|
|||||||
"X Small" | "Small" | "Medium" | "Large" | "X Large",
|
"X Small" | "Small" | "Medium" | "Large" | "X Large",
|
||||||
string
|
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)]",
|
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]",
|
Medium: reducedPadding
|
||||||
Large: "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]",
|
? "px-[var(--spacing-scale-002)] py-[var(--spacing-scale-008)] h-[32px]"
|
||||||
"X Large": "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]",
|
: "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
|
// Text styles based on Figma specifications
|
||||||
@@ -46,41 +52,34 @@ const MenuBarItemContainer = memo<MenuBarItemProps>(
|
|||||||
> = {
|
> = {
|
||||||
"X Small":
|
"X Small":
|
||||||
"font-inter text-[10px] leading-[12px] font-medium tracking-[0%]",
|
"font-inter text-[10px] leading-[12px] font-medium tracking-[0%]",
|
||||||
Small:
|
Small: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
|
||||||
"font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
|
Medium: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
|
||||||
Medium:
|
Large: "font-inter text-[16px] leading-[20px] font-medium tracking-[0%]",
|
||||||
"font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
|
|
||||||
Large:
|
|
||||||
"font-inter text-[16px] leading-[20px] font-medium tracking-[0%]",
|
|
||||||
"X Large":
|
"X Large":
|
||||||
"font-inter text-[24px] leading-[28px] font-normal tracking-[0%]",
|
"font-inter text-[24px] leading-[28px] font-normal tracking-[0%]",
|
||||||
};
|
};
|
||||||
|
|
||||||
// State styles for Default mode (yellow text on dark background)
|
// State styles for Default mode (yellow text on dark background)
|
||||||
const defaultModeStyles: Record<
|
const defaultModeStyles: Record<"default" | "hover" | "selected", string> =
|
||||||
"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)]",
|
||||||
default:
|
hover:
|
||||||
"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)]",
|
"bg-[var(--color-gray-800)] text-[var(--color-content-default-brand-primary,#fefcc9)]",
|
||||||
hover:
|
selected:
|
||||||
"bg-[var(--color-gray-800)] text-[var(--color-content-default-brand-primary,#fefcc9)]",
|
"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)]",
|
||||||
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)
|
// State styles for Inverse mode (black text on yellow background)
|
||||||
const inverseModeStyles: Record<
|
const inverseModeStyles: Record<"default" | "hover" | "selected", string> =
|
||||||
"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)]",
|
||||||
default:
|
hover:
|
||||||
"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)]",
|
"bg-[var(--color-surface-brand-accent,#4d4a00)] text-[var(--color-content-inverse-primary,black)]",
|
||||||
hover:
|
selected:
|
||||||
"bg-[var(--color-surface-brand-accent,#4d4a00)] text-[var(--color-content-inverse-primary,black)]",
|
"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)]",
|
||||||
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
|
// Get state styles based on mode
|
||||||
const stateStyles =
|
const stateStyles =
|
||||||
|
|||||||
@@ -5,14 +5,9 @@ export type MenuBarItemSizeValue =
|
|||||||
| "Large"
|
| "Large"
|
||||||
| "X Large";
|
| "X Large";
|
||||||
|
|
||||||
export type MenuBarItemStateValue =
|
export type MenuBarItemStateValue = "default" | "hover" | "selected";
|
||||||
| "default"
|
|
||||||
| "hover"
|
|
||||||
| "selected";
|
|
||||||
|
|
||||||
export type MenuBarItemModeValue =
|
export type MenuBarItemModeValue = "default" | "inverse";
|
||||||
| "default"
|
|
||||||
| "inverse";
|
|
||||||
|
|
||||||
export interface MenuBarItemProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
export interface MenuBarItemProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||||
href?: string;
|
href?: string;
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import NavigationItemView from "./NavigationItem.view";
|
import NavigationItemView from "./NavigationItem.view";
|
||||||
import type { NavigationItemProps } from "./NavigationItem.types";
|
import type { NavigationItemProps } from "./NavigationItem.types";
|
||||||
import { normalizeNavigationItemVariant, normalizeNavigationItemSize } from "../../../../lib/propNormalization";
|
import {
|
||||||
|
normalizeNavigationItemVariant,
|
||||||
|
normalizeNavigationItemSize,
|
||||||
|
} from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
const NavigationItemContainer = memo<NavigationItemProps>(
|
const NavigationItemContainer = memo<NavigationItemProps>(
|
||||||
({
|
({
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
export type NavigationItemVariantValue = "default" | "Default";
|
export type NavigationItemVariantValue = "default" | "Default";
|
||||||
export type NavigationItemSizeValue = "default" | "xsmall" | "Default" | "XSmall";
|
export type NavigationItemSizeValue =
|
||||||
|
| "default"
|
||||||
|
| "xsmall"
|
||||||
|
| "Default"
|
||||||
|
| "XSmall";
|
||||||
|
|
||||||
export interface NavigationItemProps extends Omit<
|
export interface NavigationItemProps extends Omit<
|
||||||
React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||||
import MenuBarItem from "../MenuBarItem";
|
import MenuBarItem from "../MenuBarItem";
|
||||||
import Button from "../../buttons/Button";
|
import Button from "../../buttons/Button";
|
||||||
import AvatarContainer from "../../utility/AvatarContainer";
|
import AvatarContainer from "../../utility/AvatarContainer";
|
||||||
import Avatar from "../../icons/Avatar";
|
import Avatar from "../../icons/Avatar";
|
||||||
import Logo from "../../icons/Logo";
|
|
||||||
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
|
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
|
||||||
import { TopNavView } from "./TopNav.view";
|
import { TopNavView } from "./TopNav.view";
|
||||||
import type { TopNavProps, NavSize } from "./TopNav.types";
|
import type { TopNavProps, NavSize } from "./TopNav.types";
|
||||||
@@ -19,13 +18,9 @@ export const avatarImages = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const TopNavContainer = memo<TopNavProps>(
|
const TopNavContainer = memo<TopNavProps>(
|
||||||
({
|
({ folderTop = false, loggedIn = false, profile = false, logIn = true }) => {
|
||||||
folderTop = false,
|
|
||||||
loggedIn = false,
|
|
||||||
profile = false,
|
|
||||||
logIn = true,
|
|
||||||
}) => {
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
const t = useTranslation("header");
|
const t = useTranslation("header");
|
||||||
|
|
||||||
// Schema markup for site navigation
|
// Schema markup for site navigation
|
||||||
@@ -34,7 +29,9 @@ const TopNavContainer = memo<TopNavProps>(
|
|||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
name: "CommunityRule",
|
name: "CommunityRule",
|
||||||
url: "https://communityrule.com",
|
url: "https://communityrule.com",
|
||||||
...(folderTop && { description: "Build operating manuals for successful communities" }),
|
...(folderTop && {
|
||||||
|
description: "Build operating manuals for successful communities",
|
||||||
|
}),
|
||||||
potentialAction: {
|
potentialAction: {
|
||||||
"@type": "SearchAction",
|
"@type": "SearchAction",
|
||||||
target: "https://communityrule.com/search?q={search_term_string}",
|
target: "https://communityrule.com/search?q={search_term_string}",
|
||||||
@@ -54,7 +51,10 @@ const TopNavContainer = memo<TopNavProps>(
|
|||||||
|
|
||||||
const renderNavigationItems = (size: NavSize) => {
|
const renderNavigationItems = (size: NavSize) => {
|
||||||
// Map NavSize to Figma MenuBarItem sizes
|
// 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",
|
default: "Small",
|
||||||
xsmall: "X Small",
|
xsmall: "X Small",
|
||||||
xsmallUseCases: "X Small",
|
xsmallUseCases: "X Small",
|
||||||
@@ -85,7 +85,10 @@ const TopNavContainer = memo<TopNavProps>(
|
|||||||
mode={mode}
|
mode={mode}
|
||||||
state={pathname === item.href ? "selected" : "default"}
|
state={pathname === item.href ? "selected" : "default"}
|
||||||
reducedPadding={isUseCases}
|
reducedPadding={isUseCases}
|
||||||
ariaLabel={t("ariaLabels.navigateToPage").replace("{text}", item.text)}
|
ariaLabel={t("ariaLabels.navigateToPage").replace(
|
||||||
|
"{text}",
|
||||||
|
item.text,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{item.text}
|
{item.text}
|
||||||
</MenuBarItem>
|
</MenuBarItem>
|
||||||
@@ -113,7 +116,10 @@ const TopNavContainer = memo<TopNavProps>(
|
|||||||
|
|
||||||
const renderLoginButton = (size: NavSize) => {
|
const renderLoginButton = (size: NavSize) => {
|
||||||
// Map NavSize to Figma MenuBarItem sizes
|
// 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",
|
default: "Small",
|
||||||
xsmall: "X Small",
|
xsmall: "X Small",
|
||||||
xsmallUseCases: "X Small",
|
xsmallUseCases: "X Small",
|
||||||
@@ -159,7 +165,7 @@ const TopNavContainer = memo<TopNavProps>(
|
|||||||
size={buttonSize}
|
size={buttonSize}
|
||||||
buttonType={buttonType}
|
buttonType={buttonType}
|
||||||
palette={palette}
|
palette={palette}
|
||||||
href="/create/informational"
|
onClick={() => router.push("/create/informational")}
|
||||||
ariaLabel={t("ariaLabels.createNewRule")}
|
ariaLabel={t("ariaLabels.createNewRule")}
|
||||||
>
|
>
|
||||||
{renderAvatarGroup(containerSize, avatarSize)}
|
{renderAvatarGroup(containerSize, avatarSize)}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getAssetPath } from "../../../../lib/assetUtils";
|
|||||||
import MenuBar from "../MenuBar";
|
import MenuBar from "../MenuBar";
|
||||||
import type { TopNavViewProps } from "./TopNav.types";
|
import type { TopNavViewProps } from "./TopNav.types";
|
||||||
|
|
||||||
import Logo from "../../icons/Logo";
|
import Logo from "../../asset/logo";
|
||||||
|
|
||||||
function TopNavView({
|
function TopNavView({
|
||||||
folderTop,
|
folderTop,
|
||||||
@@ -44,7 +44,11 @@ function TopNavView({
|
|||||||
{/* Header Tab - Yellow tab container with decorative Union images */}
|
{/* 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">
|
<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 - 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 */}
|
{/* XSmall menu bar - positioned next to logo */}
|
||||||
<div className="block sm:hidden -me-[2px]">
|
<div className="block sm:hidden -me-[2px]">
|
||||||
@@ -55,18 +59,21 @@ function TopNavView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Decorative Union images for tab appearance */}
|
{/* Decorative Union images for tab appearance */}
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element -- decorative SVG, not content */}
|
||||||
<img
|
<img
|
||||||
src={getAssetPath("assets/Union_xsm.svg")}
|
src={getAssetPath("assets/Union_xsm.svg")}
|
||||||
alt=""
|
alt=""
|
||||||
role="presentation"
|
role="presentation"
|
||||||
className="absolute -bottom-[3px] -right-[52px] w-[61px] h-[24px] sm:w-[61px] sm:h-[31.5px] sm:hidden -z-10"
|
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
|
<img
|
||||||
src={getAssetPath("assets/Union_sm_md_lg.svg")}
|
src={getAssetPath("assets/Union_sm_md_lg.svg")}
|
||||||
alt=""
|
alt=""
|
||||||
role="presentation"
|
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"
|
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
|
<img
|
||||||
src={getAssetPath("assets/Union_xlg.svg")}
|
src={getAssetPath("assets/Union_xlg.svg")}
|
||||||
alt=""
|
alt=""
|
||||||
@@ -87,7 +94,9 @@ function TopNavView({
|
|||||||
|
|
||||||
{/* 640-1023px (md: breakpoint): MenuBar Small */}
|
{/* 640-1023px (md: breakpoint): MenuBar Small */}
|
||||||
<div className="hidden md:block lg:hidden">
|
<div className="hidden md:block lg:hidden">
|
||||||
<MenuBar size="Small">{renderNavigationItems("homeMd")}</MenuBar>
|
<MenuBar size="Small">
|
||||||
|
{renderNavigationItems("homeMd")}
|
||||||
|
</MenuBar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 1024-1440px (lg: breakpoint): MenuBar Large */}
|
{/* 1024-1440px (lg: breakpoint): MenuBar Large */}
|
||||||
@@ -158,7 +167,11 @@ function TopNavView({
|
|||||||
aria-label={t("ariaLabels.mainNavigation")}
|
aria-label={t("ariaLabels.mainNavigation")}
|
||||||
>
|
>
|
||||||
{/* Logo - Consistent left positioning across all breakpoints */}
|
{/* 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 */}
|
{/* Navigation Links - Consistent center positioning */}
|
||||||
<div className="flex items-center flex-1 justify-end sm:flex-none sm:justify-center">
|
<div className="flex items-center flex-1 justify-end sm:flex-none sm:justify-center">
|
||||||
@@ -190,7 +203,9 @@ function TopNavView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden xl:block" data-testid="nav-xl">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ const AskOrganizerContainer = memo<AskOrganizerProps>(
|
|||||||
onContactClick,
|
onContactClick,
|
||||||
}) => {
|
}) => {
|
||||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
// 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 t = useTranslation();
|
||||||
const defaultButtonText = buttonText ?? t("askOrganizer.buttonText");
|
const defaultButtonText = buttonText ?? t("askOrganizer.buttonText");
|
||||||
const defaultButtonHref = buttonHref ?? t("askOrganizer.buttonHref");
|
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 */}
|
{/* Hero Image Container */}
|
||||||
<div className="w-full h-full md:flex-1 rounded-[8px] overflow-hidden relative z-10 flex items-center justify-center">
|
<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
|
<img
|
||||||
src={getAssetPath("assets/HeroImage.png")}
|
src={getAssetPath("assets/HeroImage.png")}
|
||||||
alt={imageAlt}
|
alt={imageAlt}
|
||||||
|
|||||||
@@ -23,12 +23,17 @@ export function RuleStackView({
|
|||||||
// Determine current breakpoint for RuleCard size
|
// Determine current breakpoint for RuleCard size
|
||||||
// 320-639: XS, 640-767: S, 768-1023: S, 1024-1439: M, 1440+: L
|
// 320-639: XS, 640-767: S, 768-1023: S, 1024-1439: M, 1440+: L
|
||||||
const isMax639 = useMediaQuery("(max-width: 639px)");
|
const isMax639 = useMediaQuery("(max-width: 639px)");
|
||||||
const isMin640Max1023 = useMediaQuery("(min-width: 640px) and (max-width: 1023px)");
|
const isMin640Max1023 = useMediaQuery(
|
||||||
const isMin1024Max1439 = useMediaQuery("(min-width: 1024px) and (max-width: 1439px)");
|
"(min-width: 640px) and (max-width: 1023px)",
|
||||||
|
);
|
||||||
|
const isMin1024Max1439 = useMediaQuery(
|
||||||
|
"(min-width: 1024px) and (max-width: 1439px)",
|
||||||
|
);
|
||||||
const isMin1440 = useMediaQuery("(min-width: 1440px)");
|
const isMin1440 = useMediaQuery("(min-width: 1440px)");
|
||||||
|
|
||||||
// Handle hydration: only use media queries after mount
|
// Handle hydration: only use media queries after mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer breakpoint until after mount to avoid hydration mismatch
|
||||||
setIsMounted(true);
|
setIsMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -38,12 +43,12 @@ export function RuleStackView({
|
|||||||
? isMax639
|
? isMax639
|
||||||
? "XS"
|
? "XS"
|
||||||
: isMin640Max1023
|
: isMin640Max1023
|
||||||
? "S"
|
? "S"
|
||||||
: isMin1024Max1439
|
: isMin1024Max1439
|
||||||
? "M"
|
? "M"
|
||||||
: isMin1440
|
: isMin1440
|
||||||
? "L"
|
? "L"
|
||||||
: "M"
|
: "M"
|
||||||
: "M";
|
: "M";
|
||||||
|
|
||||||
// Icon sizes: XS=40px, S=56px, M=56px, L=90px
|
// Icon sizes: XS=40px, S=56px, M=56px, L=90px
|
||||||
@@ -150,17 +155,15 @@ export function RuleStackView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* See all templates button */}
|
{/* See all templates button */}
|
||||||
<div className="
|
<div
|
||||||
|
className="
|
||||||
flex justify-center w-full
|
flex justify-center w-full
|
||||||
max-[767px]:mt-[var(--measures-spacing-600,24px)]
|
max-[767px]:mt-[var(--measures-spacing-600,24px)]
|
||||||
min-[768px]:max-[1023px]:mt-[var(--measures-spacing-800,32px)]
|
min-[768px]:max-[1023px]:mt-[var(--measures-spacing-800,32px)]
|
||||||
min-[1024px]:mt-[var(--measures-spacing-1000,40px)]
|
min-[1024px]:mt-[var(--measures-spacing-1000,40px)]
|
||||||
">
|
"
|
||||||
<Button
|
>
|
||||||
buttonType="outline"
|
<Button buttonType="outline" palette="default" size="large">
|
||||||
palette="default"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { normalizeSectionHeaderVariant } from "../../../lib/propNormalization";
|
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 {
|
interface SectionHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const SectionNumber = memo<SectionNumberProps>(({ number }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative size-[40px] overflow-visible -rotate-[15deg]">
|
<div className="relative size-[40px] overflow-visible -rotate-[15deg]">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element -- dynamic src from getImageSrc */}
|
||||||
<img
|
<img
|
||||||
src={getImageSrc(number)}
|
src={getImageSrc(number)}
|
||||||
alt={`Section ${number}`}
|
alt={`Section ${number}`}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import ContentLockupView from "./ContentLockup.view";
|
import ContentLockupView from "./ContentLockup.view";
|
||||||
import type { ContentLockupProps, VariantStyle } from "./ContentLockup.types";
|
import type { ContentLockupProps, VariantStyle } from "./ContentLockup.types";
|
||||||
import { normalizeContentLockupVariant, normalizeAlignment } from "../../../../lib/propNormalization";
|
import {
|
||||||
|
normalizeContentLockupVariant,
|
||||||
|
normalizeAlignment,
|
||||||
|
} from "../../../../lib/propNormalization";
|
||||||
|
|
||||||
const ContentLockupContainer = memo<ContentLockupProps>(
|
const ContentLockupContainer = memo<ContentLockupProps>(
|
||||||
({
|
({
|
||||||
|
|||||||
@@ -60,12 +60,15 @@ function ContentLockupView({
|
|||||||
</h1>
|
</h1>
|
||||||
) : null}
|
) : null}
|
||||||
{variant === "hero" && (
|
{variant === "hero" && (
|
||||||
<img
|
<>
|
||||||
src={getAssetPath("assets/Shapes_1.svg")}
|
{/* eslint-disable-next-line @next/next/no-img-element -- decorative shape SVG */}
|
||||||
alt=""
|
<img
|
||||||
className={styles.shape}
|
src={getAssetPath("assets/Shapes_1.svg")}
|
||||||
role="presentation"
|
alt=""
|
||||||
/>
|
className={styles.shape}
|
||||||
|
role="presentation"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -93,19 +96,32 @@ function ContentLockupView({
|
|||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
{/* Small button for xsm and sm breakpoints */}
|
{/* Small button for xsm and sm breakpoints */}
|
||||||
<div className="block md:hidden">
|
<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}
|
{ctaText}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* Large button for md and lg breakpoints */}
|
{/* Large button for md and lg breakpoints */}
|
||||||
<div className="hidden md:block xl:hidden">
|
<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}
|
{ctaText}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* XLarge button for xl breakpoint */}
|
{/* XLarge button for xl breakpoint */}
|
||||||
<div className="hidden xl:block">
|
<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}
|
{ctaText}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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 { memo } from "react";
|
||||||
import { normalizeSize } from "../../../lib/propNormalization";
|
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> {
|
interface AvatarContainerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
children?: React.ReactNode;
|
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";
|
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
||||||
|
|
||||||
const CreateFlowFooterContainer = memo<CreateFlowFooterProps>(
|
const CreateFlowFooterContainer = memo<CreateFlowFooterProps>(
|
||||||
({ secondButton, progressBar = true, className = "" }) => {
|
({ secondButton, progressBar = true, onBackClick, className = "" }) => {
|
||||||
return (
|
return (
|
||||||
<CreateFlowFooterView
|
<CreateFlowFooterView
|
||||||
secondButton={secondButton}
|
secondButton={secondButton}
|
||||||
progressBar={progressBar}
|
progressBar={progressBar}
|
||||||
|
onBackClick={onBackClick}
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ export interface CreateFlowFooterProps {
|
|||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
progressBar?: boolean;
|
progressBar?: boolean;
|
||||||
|
/**
|
||||||
|
* Callback function for Back button click
|
||||||
|
*/
|
||||||
|
onBackClick?: () => void;
|
||||||
/**
|
/**
|
||||||
* Additional CSS classes
|
* Additional CSS classes
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
|
|||||||
export function CreateFlowFooterView({
|
export function CreateFlowFooterView({
|
||||||
secondButton,
|
secondButton,
|
||||||
progressBar = true,
|
progressBar = true,
|
||||||
|
onBackClick,
|
||||||
className = "",
|
className = "",
|
||||||
}: CreateFlowFooterProps) {
|
}: CreateFlowFooterProps) {
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer
|
||||||
className={`sticky bottom-0 z-50 bg-black w-full ${className}`}
|
className={`bg-black w-full ${className}`}
|
||||||
role="contentinfo"
|
role="contentinfo"
|
||||||
aria-label="Create Flow Footer"
|
aria-label="Create Flow Footer"
|
||||||
>
|
>
|
||||||
@@ -28,14 +29,14 @@ export function CreateFlowFooterView({
|
|||||||
palette="default"
|
palette="default"
|
||||||
size="xsmall"
|
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)]"
|
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
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Second Button - Right */}
|
{/* Second Button - Right */}
|
||||||
{secondButton && (
|
{secondButton && <div className="flex-shrink-0">{secondButton}</div>}
|
||||||
<div className="flex-shrink-0">{secondButton}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
|
|||||||
onExport,
|
onExport,
|
||||||
onEdit,
|
onEdit,
|
||||||
onExit,
|
onExit,
|
||||||
|
buttonPalette,
|
||||||
className = "",
|
className = "",
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleExit = () => {
|
const handleExit = (options?: { saveDraft?: boolean }) => {
|
||||||
if (onExit) {
|
if (onExit) {
|
||||||
onExit();
|
onExit(options);
|
||||||
} else {
|
} else {
|
||||||
// Default behavior: navigate to home
|
// Default behavior: navigate to home
|
||||||
router.push("/");
|
router.push("/");
|
||||||
@@ -38,6 +39,7 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
|
|||||||
onExport={onExport}
|
onExport={onExport}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onExit={handleExit}
|
onExit={handleExit}
|
||||||
|
buttonPalette={buttonPalette}
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,9 +39,15 @@ export interface CreateFlowTopNavProps {
|
|||||||
*/
|
*/
|
||||||
onEdit?: () => void;
|
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
|
* Additional CSS classes
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Logo from "../../icons/Logo";
|
import Logo from "../../asset/logo";
|
||||||
import Button from "../../buttons/Button";
|
import Button from "../../buttons/Button";
|
||||||
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
|
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
|
||||||
|
|
||||||
@@ -11,13 +11,14 @@ export function CreateFlowTopNavView({
|
|||||||
onExport,
|
onExport,
|
||||||
onEdit,
|
onEdit,
|
||||||
onExit,
|
onExit,
|
||||||
|
buttonPalette = "default",
|
||||||
className = "",
|
className = "",
|
||||||
}: CreateFlowTopNavProps) {
|
}: CreateFlowTopNavProps) {
|
||||||
const exitButtonText = loggedIn ? "Save & Exit" : "Exit";
|
const exitButtonText = loggedIn ? "Save & Exit" : "Exit";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={`bg-black w-full border-b border-[var(--color-border-default-tertiary)] ${className}`}
|
className={`bg-black w-full ${className}`}
|
||||||
role="banner"
|
role="banner"
|
||||||
aria-label="Create Rule Flow Navigation"
|
aria-label="Create Rule Flow Navigation"
|
||||||
>
|
>
|
||||||
@@ -27,14 +28,14 @@ export function CreateFlowTopNavView({
|
|||||||
aria-label="Create Flow Navigation"
|
aria-label="Create Flow Navigation"
|
||||||
>
|
>
|
||||||
{/* Logo - Left */}
|
{/* Logo - Left */}
|
||||||
<Logo size="createFlow" showText={true} />
|
<Logo size="createFlow" wordmark palette={buttonPalette} />
|
||||||
|
|
||||||
{/* Button Group - Right */}
|
{/* Button Group - Right */}
|
||||||
<div className="flex items-center gap-[var(--spacing-scale-012,12px)]">
|
<div className="flex items-center gap-[var(--spacing-scale-012,12px)]">
|
||||||
{hasShare && (
|
{hasShare && (
|
||||||
<Button
|
<Button
|
||||||
buttonType="outline"
|
buttonType="outline"
|
||||||
palette="default"
|
palette={buttonPalette}
|
||||||
size="xsmall"
|
size="xsmall"
|
||||||
onClick={onShare}
|
onClick={onShare}
|
||||||
ariaLabel="Share"
|
ariaLabel="Share"
|
||||||
@@ -47,7 +48,7 @@ export function CreateFlowTopNavView({
|
|||||||
{hasExport && (
|
{hasExport && (
|
||||||
<Button
|
<Button
|
||||||
buttonType="outline"
|
buttonType="outline"
|
||||||
palette="default"
|
palette={buttonPalette}
|
||||||
size="xsmall"
|
size="xsmall"
|
||||||
onClick={onExport}
|
onClick={onExport}
|
||||||
ariaLabel="Export"
|
ariaLabel="Export"
|
||||||
@@ -74,7 +75,7 @@ export function CreateFlowTopNavView({
|
|||||||
{hasEdit && (
|
{hasEdit && (
|
||||||
<Button
|
<Button
|
||||||
buttonType="outline"
|
buttonType="outline"
|
||||||
palette="default"
|
palette={buttonPalette}
|
||||||
size="xsmall"
|
size="xsmall"
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
ariaLabel="Edit"
|
ariaLabel="Edit"
|
||||||
@@ -86,9 +87,9 @@ export function CreateFlowTopNavView({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
buttonType="outline"
|
buttonType="outline"
|
||||||
palette="default"
|
palette={buttonPalette}
|
||||||
size="xsmall"
|
size="xsmall"
|
||||||
onClick={onExit}
|
onClick={() => onExit?.({ saveDraft: loggedIn })}
|
||||||
ariaLabel={exitButtonText}
|
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]"
|
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);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./DecisionMakingSidebar.container";
|
||||||
|
export type { DecisionMakingSidebarProps } from "./DecisionMakingSidebar.types";
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo, useCallback, useState } from "react";
|
||||||
|
import InfoMessageBoxView from "./InfoMessageBox.view";
|
||||||
|
import type { InfoMessageBoxProps } from "./InfoMessageBox.types";
|
||||||
|
|
||||||
|
const InfoMessageBoxContainer = memo<InfoMessageBoxProps>(
|
||||||
|
({
|
||||||
|
title,
|
||||||
|
items,
|
||||||
|
icon,
|
||||||
|
checkedIds: controlledCheckedIds,
|
||||||
|
onCheckboxChange,
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
|
const [internalCheckedIds, setInternalCheckedIds] = useState<string[]>([]);
|
||||||
|
const checkedIds =
|
||||||
|
controlledCheckedIds !== undefined
|
||||||
|
? controlledCheckedIds
|
||||||
|
: internalCheckedIds;
|
||||||
|
|
||||||
|
const handleGroupChange = useCallback(
|
||||||
|
(newValue: string[]) => {
|
||||||
|
if (controlledCheckedIds === undefined) {
|
||||||
|
setInternalCheckedIds(newValue);
|
||||||
|
}
|
||||||
|
if (!onCheckboxChange) return;
|
||||||
|
const prevSet = new Set(checkedIds);
|
||||||
|
const newSet = new Set(newValue);
|
||||||
|
items.forEach((item) => {
|
||||||
|
const nowChecked = newSet.has(item.id);
|
||||||
|
const wasChecked = prevSet.has(item.id);
|
||||||
|
if (nowChecked !== wasChecked) {
|
||||||
|
onCheckboxChange(item.id, nowChecked);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[checkedIds, controlledCheckedIds, items, onCheckboxChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfoMessageBoxView
|
||||||
|
title={title}
|
||||||
|
items={items}
|
||||||
|
icon={icon}
|
||||||
|
checkedIds={checkedIds}
|
||||||
|
onGroupChange={handleGroupChange}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
InfoMessageBoxContainer.displayName = "InfoMessageBox";
|
||||||
|
|
||||||
|
export default InfoMessageBoxContainer;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user