diff --git a/app/components-preview/page.tsx b/app/components-preview/page.tsx
index 2a68e17..f7ba62f 100644
--- a/app/components-preview/page.tsx
+++ b/app/components-preview/page.tsx
@@ -97,33 +97,360 @@ export default function ComponentsPreview() {
{ id: "custom-2", label: "", state: "Custom", palette: "Default", size: "M" },
]);
- const sampleCategories = [
+ // RuleCard categories with chip options and state management
+ const [ruleCardCategories, setRuleCardCategories] = useState([
{
name: "Values",
- items: ["Consciousness", "Ecology", "Abundance", "Art", "Decisiveness"],
- createUrl: "/create/value",
+ chipOptions: [
+ { id: "values-1", label: "Consciousness", state: "Unselected" as const },
+ { id: "values-2", label: "Ecology", state: "Unselected" as const },
+ { id: "values-3", label: "Abundance", state: "Unselected" as const },
+ { id: "values-4", label: "Art", state: "Unselected" as const },
+ { id: "values-5", label: "Decisiveness", state: "Unselected" as const },
+ ],
+ onChipClick: (categoryName: string, chipId: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.map((opt) =>
+ opt.id === chipId
+ ? {
+ ...opt,
+ state: opt.state === "Selected" ? ("Unselected" as const) : ("Selected" as const),
+ }
+ : opt
+ ),
+ }
+ : cat
+ )
+ );
+ },
+ onAddClick: (categoryName: string) => {
+ const newId = `custom-${categoryName}-${Date.now()}`;
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: [
+ ...cat.chipOptions,
+ { id: newId, label: "", state: "Custom" as const },
+ ],
+ }
+ : cat
+ )
+ );
+ },
+ onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.map((opt) =>
+ opt.id === chipId
+ ? { ...opt, label: value, state: "Selected" as const }
+ : opt
+ ),
+ }
+ : cat
+ )
+ );
+ },
+ onCustomChipClose: (categoryName: string, chipId: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.filter((opt) => opt.id !== chipId),
+ }
+ : cat
+ )
+ );
+ },
},
{
name: "Communication",
- items: ["Signal"],
- createUrl: "/create/communication",
+ chipOptions: [
+ { id: "comm-1", label: "Signal", state: "Unselected" as const },
+ ],
+ onChipClick: (categoryName: string, chipId: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.map((opt) =>
+ opt.id === chipId
+ ? {
+ ...opt,
+ state: opt.state === "Selected" ? ("Unselected" as const) : ("Selected" as const),
+ }
+ : opt
+ ),
+ }
+ : cat
+ )
+ );
+ },
+ onAddClick: (categoryName: string) => {
+ const newId = `custom-${categoryName}-${Date.now()}`;
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: [
+ ...cat.chipOptions,
+ { id: newId, label: "", state: "Custom" as const },
+ ],
+ }
+ : cat
+ )
+ );
+ },
+ onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.map((opt) =>
+ opt.id === chipId
+ ? { ...opt, label: value, state: "Selected" as const }
+ : opt
+ ),
+ }
+ : cat
+ )
+ );
+ },
+ onCustomChipClose: (categoryName: string, chipId: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.filter((opt) => opt.id !== chipId),
+ }
+ : cat
+ )
+ );
+ },
},
{
name: "Membership",
- items: ["Open Admission"],
- createUrl: "/create/membership",
+ chipOptions: [
+ { id: "membership-1", label: "Open Admission", state: "Unselected" as const },
+ ],
+ onChipClick: (categoryName: string, chipId: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.map((opt) =>
+ opt.id === chipId
+ ? {
+ ...opt,
+ state: opt.state === "Selected" ? ("Unselected" as const) : ("Selected" as const),
+ }
+ : opt
+ ),
+ }
+ : cat
+ )
+ );
+ },
+ onAddClick: (categoryName: string) => {
+ const newId = `custom-${categoryName}-${Date.now()}`;
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: [
+ ...cat.chipOptions,
+ { id: newId, label: "", state: "Custom" as const },
+ ],
+ }
+ : cat
+ )
+ );
+ },
+ onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.map((opt) =>
+ opt.id === chipId
+ ? { ...opt, label: value, state: "Selected" as const }
+ : opt
+ ),
+ }
+ : cat
+ )
+ );
+ },
+ onCustomChipClose: (categoryName: string, chipId: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.filter((opt) => opt.id !== chipId),
+ }
+ : cat
+ )
+ );
+ },
},
{
name: "Decision-making",
- items: ["Lazy Consensus", "Modified Consensus"],
- createUrl: "/create/decision-making",
+ chipOptions: [
+ { id: "decision-1", label: "Lazy Consensus", state: "Unselected" as const },
+ { id: "decision-2", label: "Modified Consensus", state: "Unselected" as const },
+ ],
+ onChipClick: (categoryName: string, chipId: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.map((opt) =>
+ opt.id === chipId
+ ? {
+ ...opt,
+ state: opt.state === "Selected" ? ("Unselected" as const) : ("Selected" as const),
+ }
+ : opt
+ ),
+ }
+ : cat
+ )
+ );
+ },
+ onAddClick: (categoryName: string) => {
+ const newId = `custom-${categoryName}-${Date.now()}`;
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: [
+ ...cat.chipOptions,
+ { id: newId, label: "", state: "Custom" as const },
+ ],
+ }
+ : cat
+ )
+ );
+ },
+ onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.map((opt) =>
+ opt.id === chipId
+ ? { ...opt, label: value, state: "Selected" as const }
+ : opt
+ ),
+ }
+ : cat
+ )
+ );
+ },
+ onCustomChipClose: (categoryName: string, chipId: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.filter((opt) => opt.id !== chipId),
+ }
+ : cat
+ )
+ );
+ },
},
{
name: "Conflict management",
- items: ["Code of Conduct", "Restorative Justice"],
- createUrl: "/create/conflict-management",
+ chipOptions: [
+ { id: "conflict-1", label: "Code of Conduct", state: "Unselected" as const },
+ { id: "conflict-2", label: "Restorative Justice", state: "Unselected" as const },
+ ],
+ onChipClick: (categoryName: string, chipId: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.map((opt) =>
+ opt.id === chipId
+ ? {
+ ...opt,
+ state: opt.state === "Selected" ? ("Unselected" as const) : ("Selected" as const),
+ }
+ : opt
+ ),
+ }
+ : cat
+ )
+ );
+ },
+ onAddClick: (categoryName: string) => {
+ const newId = `custom-${categoryName}-${Date.now()}`;
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: [
+ ...cat.chipOptions,
+ { id: newId, label: "", state: "Custom" as const },
+ ],
+ }
+ : cat
+ )
+ );
+ },
+ onCustomChipConfirm: (categoryName: string, chipId: string, value: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.map((opt) =>
+ opt.id === chipId
+ ? { ...opt, label: value, state: "Selected" as const }
+ : opt
+ ),
+ }
+ : cat
+ )
+ );
+ },
+ onCustomChipClose: (categoryName: string, chipId: string) => {
+ setRuleCardCategories((prev) =>
+ prev.map((cat) =>
+ cat.name === categoryName
+ ? {
+ ...cat,
+ chipOptions: cat.chipOptions.filter((opt) => opt.id !== chipId),
+ }
+ : cat
+ )
+ );
+ },
},
- ];
+ ]);
return (
@@ -355,13 +682,7 @@ export default function ComponentsPreview() {
className="w-[568px]"
logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png"
logoAlt="Mutual Aid Mondays"
- categories={sampleCategories}
- onPillClick={(category, item) => {
- console.log(`Pill clicked: ${category} - ${item}`);
- }}
- onCreateClick={(category) => {
- console.log(`Create clicked: ${category}`);
- }}
+ categories={ruleCardCategories}
onClick={() => console.log("Card clicked: Mutual Aid Mondays")}
/>
@@ -382,13 +703,7 @@ export default function ComponentsPreview() {
className="w-[398px]"
logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png"
logoAlt="Mutual Aid Mondays"
- categories={sampleCategories}
- onPillClick={(category, item) => {
- console.log(`Pill clicked: ${category} - ${item}`);
- }}
- onCreateClick={(category) => {
- console.log(`Create clicked: ${category}`);
- }}
+ categories={ruleCardCategories}
onClick={() => console.log("Card clicked: Mutual Aid Mondays")}
/>
diff --git a/app/components/InputLabel/InputLabel.container.tsx b/app/components/InputLabel/InputLabel.container.tsx
new file mode 100644
index 0000000..1060ba7
--- /dev/null
+++ b/app/components/InputLabel/InputLabel.container.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { memo } from "react";
+import InputLabelView from "./InputLabel.view";
+import type { InputLabelProps } from "./InputLabel.types";
+import {
+ normalizeInputLabelSize,
+ normalizeInputLabelPalette,
+} from "../../../lib/propNormalization";
+
+const InputLabelContainer = memo(
+ ({
+ label,
+ helpIcon = false,
+ asterisk = false,
+ helperText = false,
+ size: sizeProp = "S",
+ palette: paletteProp = "Default",
+ className = "",
+ }) => {
+ const size = normalizeInputLabelSize(sizeProp);
+ const palette = normalizeInputLabelPalette(paletteProp);
+
+ return (
+
+ );
+ },
+);
+
+InputLabelContainer.displayName = "InputLabel";
+
+export default InputLabelContainer;
diff --git a/app/components/InputLabel/InputLabel.types.ts b/app/components/InputLabel/InputLabel.types.ts
new file mode 100644
index 0000000..7e37ae6
--- /dev/null
+++ b/app/components/InputLabel/InputLabel.types.ts
@@ -0,0 +1,44 @@
+export type InputLabelSizeValue = "S" | "M" | "s" | "m";
+export type InputLabelPaletteValue = "Default" | "Inverse" | "default" | "inverse";
+
+export interface InputLabelProps {
+ /**
+ * The label text to display
+ */
+ label: string;
+ /**
+ * Show help icon next to label
+ */
+ helpIcon?: boolean;
+ /**
+ * Show asterisk (*) to indicate required field
+ */
+ asterisk?: boolean;
+ /**
+ * Helper text to display on the right side.
+ * If boolean true, shows "Optional text".
+ * If string, shows the provided text.
+ */
+ helperText?: boolean | string;
+ /**
+ * Size variant: "S" (small) or "M" (medium)
+ * Accepts both uppercase (Figma) and lowercase values.
+ */
+ size?: InputLabelSizeValue;
+ /**
+ * Palette variant: "Default" or "Inverse"
+ * Accepts both PascalCase (Figma) and lowercase values.
+ */
+ palette?: InputLabelPaletteValue;
+ className?: string;
+}
+
+export interface InputLabelViewProps {
+ label: string;
+ helpIcon: boolean;
+ asterisk: boolean;
+ helperText: boolean | string;
+ size: "s" | "m";
+ palette: "default" | "inverse";
+ className: string;
+}
diff --git a/app/components/InputLabel/InputLabel.view.tsx b/app/components/InputLabel/InputLabel.view.tsx
new file mode 100644
index 0000000..9b5ab56
--- /dev/null
+++ b/app/components/InputLabel/InputLabel.view.tsx
@@ -0,0 +1,106 @@
+"use client";
+
+import { memo } from "react";
+import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
+import type { InputLabelViewProps } from "./InputLabel.types";
+
+function InputLabelView({
+ label,
+ helpIcon,
+ asterisk,
+ helperText,
+ size,
+ palette,
+ className = "",
+}: InputLabelViewProps) {
+ const isSmall = size === "s";
+ const isInverse = palette === "inverse";
+
+ // Size-based typography
+ const labelTextSize = isSmall
+ ? "text-[length:var(--sizing-350,14px)] leading-[20px]"
+ : "text-[length:var(--sizing-400,16px)] leading-[24px]";
+
+ const helperTextSize = isSmall
+ ? "text-[length:var(--measures-sizing-250,10px)] leading-[var(--measures-spacing-350,14px)]"
+ : "text-[length:var(--sizing-300,12px)] leading-[16px]";
+
+ const asteriskSize = isSmall
+ ? "text-[length:var(--measures-sizing-250,10px)] leading-[var(--measures-spacing-300,12px)]"
+ : "text-[length:var(--measures-spacing-300,12px)] leading-[var(--measures-spacing-300,12px)]";
+
+ // Palette-based colors
+ const labelColor = isInverse
+ ? "text-[color:var(--color-content-inverse-secondary,#1f1f1f)]"
+ : "text-[color:var(--color-content-default-secondary,#d2d2d2)]";
+
+ const helperTextColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
+
+ // Layout: S uses flex-wrap with baseline, M uses flex with center
+ const containerClass = isSmall
+ ? "flex flex-wrap gap-[var(--measures-spacing-200,4px_8px)] items-baseline pr-[var(--measures-spacing-100,4px)] relative w-full"
+ : "flex gap-[4px] items-center relative w-full";
+
+ const labelContainerClass = isSmall
+ ? "flex gap-[var(--measures-spacing-050,2px)] items-center relative shrink-0"
+ : "flex gap-[var(--measures-spacing-100,4px)] items-center relative shrink-0";
+
+ const helpIconSize = isSmall ? "size-[12px]" : "size-[16px]";
+
+ // Help icon color filter based on palette
+ // Default: Light yellow (#f6f06f / rgba(246, 240, 111, 1)) - SVG already has this color
+ // Inverse: Dark yellow (#8c8505 / rgba(140, 133, 5, 1))
+ // For default, no filter needed as SVG already has the correct yellow
+ // For inverse, darken the yellow
+ const helpIconFilter = isInverse
+ ? "brightness(0.57) saturate(100%)" // Dark yellow (#8c8505) - darken the existing yellow
+ : undefined; // No filter for default - use SVG's native yellow color
+
+ return (
+
+
+
+
+ {label}
+
+ {asterisk && (
+
+ *
+
+ )}
+
+ {helpIcon && (
+
+
})
+
+ )}
+
+ {helperText && (
+
+ {typeof helperText === "string" ? helperText : "Optional text"}
+
+ )}
+
+ );
+}
+
+InputLabelView.displayName = "InputLabelView";
+
+export default memo(InputLabelView);
diff --git a/app/components/InputLabel/index.tsx b/app/components/InputLabel/index.tsx
new file mode 100644
index 0000000..b2d805a
--- /dev/null
+++ b/app/components/InputLabel/index.tsx
@@ -0,0 +1,3 @@
+import InputLabel from "./InputLabel.container";
+
+export default InputLabel;
diff --git a/app/components/MultiSelect/MultiSelect.container.tsx b/app/components/MultiSelect/MultiSelect.container.tsx
index 086498e..1613576 100644
--- a/app/components/MultiSelect/MultiSelect.container.tsx
+++ b/app/components/MultiSelect/MultiSelect.container.tsx
@@ -3,13 +3,14 @@
import { memo } from "react";
import MultiSelectView from "./MultiSelect.view";
import type { MultiSelectProps } from "./MultiSelect.types";
-import { normalizeMultiSelectSize } from "../../../lib/propNormalization";
+import { normalizeMultiSelectSize, normalizeChipPalette } from "../../../lib/propNormalization";
const MultiSelectContainer = memo(
({
label,
showHelpIcon = true,
size: sizeProp = "M",
+ palette: paletteProp = "Default",
options,
onChipClick,
onAddClick,
@@ -20,12 +21,14 @@ const MultiSelectContainer = memo(
className = "",
}) => {
const size = normalizeMultiSelectSize(sizeProp);
+ const palette = normalizeChipPalette(paletteProp);
return (
void;
onAddClick?: () => void;
diff --git a/app/components/MultiSelect/MultiSelect.view.tsx b/app/components/MultiSelect/MultiSelect.view.tsx
index a526e68..b59d8c9 100644
--- a/app/components/MultiSelect/MultiSelect.view.tsx
+++ b/app/components/MultiSelect/MultiSelect.view.tsx
@@ -1,14 +1,15 @@
"use client";
import { memo } from "react";
-import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
import Chip from "../Chip";
+import InputLabel from "../InputLabel";
import type { MultiSelectViewProps } from "./MultiSelect.types";
function MultiSelectView({
label,
showHelpIcon,
size,
+ palette,
options,
onChipClick,
onAddClick,
@@ -19,44 +20,27 @@ function MultiSelectView({
className,
}: MultiSelectViewProps) {
const isSmall = size === "s";
+ const isInverse = palette === "inverse";
- // Size-based spacing and typography
+ // Size-based spacing
const gapClass = isSmall
? "gap-[var(--measures-spacing-200,8px)]"
: "gap-[var(--measures-spacing-300,12px)]";
- const labelGapClass = isSmall
- ? "gap-[var(--measures-spacing-200,4px_8px)]"
- : "gap-[4px]";
-
- const labelTextSize = isSmall
- ? "text-[length:var(--sizing-350,14px)] leading-[20px]"
- : "text-[length:var(--sizing-400,16px)] leading-[24px]";
-
- const helpIconSize = isSmall ? "size-[12px]" : "size-[16px]";
-
const chipSize = isSmall ? "S" : "M";
return (
- {/* Label with help icon */}
+ {/* Label using InputLabel component */}
{label && (
-
-
-
- {showHelpIcon && (
-
-
})
-
- )}
-
-
+
)}
{/* Chips container */}
@@ -66,7 +50,7 @@ function MultiSelectView({
key={option.id}
label={option.state === "Custom" ? "" : option.label}
state={option.state || "Unselected"}
- palette="Default"
+ palette={palette === "inverse" ? "Inverse" : "Default"}
size={chipSize}
onClick={() => {
// Only toggle if not in Custom state
@@ -89,7 +73,7 @@ function MultiSelectView({
/>
))}
- {/* Add button - Ghost button style */}
+ {/* Add button - Circular button with border (not ghost) when no text, ghost style when text provided */}
{showAddButton && (
)}
diff --git a/app/components/RuleCard/RuleCard.container.tsx b/app/components/RuleCard/RuleCard.container.tsx
index 855bbb7..84105b6 100644
--- a/app/components/RuleCard/RuleCard.container.tsx
+++ b/app/components/RuleCard/RuleCard.container.tsx
@@ -29,8 +29,6 @@ const RuleCardContainer = memo(
expanded = false,
size: sizeProp,
categories,
- onPillClick,
- onCreateClick,
logoUrl,
logoAlt,
communityInitials,
@@ -77,8 +75,6 @@ const RuleCardContainer = memo(
expanded={expanded}
size={size}
categories={categories}
- onPillClick={onPillClick}
- onCreateClick={onCreateClick}
logoUrl={logoUrl}
logoAlt={logoAlt}
communityInitials={communityInitials}
diff --git a/app/components/RuleCard/RuleCard.types.ts b/app/components/RuleCard/RuleCard.types.ts
index ab5c367..f7bda7f 100644
--- a/app/components/RuleCard/RuleCard.types.ts
+++ b/app/components/RuleCard/RuleCard.types.ts
@@ -1,7 +1,12 @@
+import type { ChipOption } from "../MultiSelect/MultiSelect.types";
+
export interface Category {
name: string;
- items: string[];
- createUrl?: string;
+ chipOptions: ChipOption[];
+ onChipClick?: (categoryName: string, chipId: string) => void;
+ onAddClick?: (categoryName: string) => void;
+ onCustomChipConfirm?: (categoryName: string, chipId: string, value: string) => void;
+ onCustomChipClose?: (categoryName: string, chipId: string) => void;
}
export interface RuleCardProps {
@@ -14,8 +19,6 @@ export interface RuleCardProps {
expanded?: boolean;
size?: "L" | "M" | "l" | "m";
categories?: Category[];
- onPillClick?: (category: string, item: string) => void;
- onCreateClick?: (category: string) => void;
logoUrl?: string;
logoAlt?: string;
communityInitials?: string;
@@ -32,8 +35,6 @@ export interface RuleCardViewProps {
expanded: boolean;
size: "L" | "M";
categories?: Category[];
- onPillClick?: (category: string, item: string) => void;
- onCreateClick?: (category: string) => void;
logoUrl?: string;
logoAlt?: string;
communityInitials?: string;
diff --git a/app/components/RuleCard/RuleCard.view.tsx b/app/components/RuleCard/RuleCard.view.tsx
index ed9edcf..2b92295 100644
--- a/app/components/RuleCard/RuleCard.view.tsx
+++ b/app/components/RuleCard/RuleCard.view.tsx
@@ -2,6 +2,7 @@
import Image from "next/image";
import { useTranslation } from "../../contexts/MessagesContext";
+import MultiSelect from "../MultiSelect";
import type { RuleCardViewProps } from "./RuleCard.types";
export function RuleCardView({
@@ -15,8 +16,6 @@ export function RuleCardView({
expanded,
size,
categories,
- onPillClick,
- onCreateClick,
logoUrl,
logoAlt,
communityInitials,
@@ -111,21 +110,6 @@ export function RuleCardView({
return null;
};
- // Handle pill click with stopPropagation
- const handlePillClick = (e: React.MouseEvent, categoryName: string, item: string) => {
- e.stopPropagation();
- if (onPillClick) {
- onPillClick(categoryName, item);
- }
- };
-
- // Handle create button click with stopPropagation
- const handleCreateClick = (e: React.MouseEvent, categoryName: string) => {
- e.stopPropagation();
- if (onCreateClick) {
- onCreateClick(categoryName);
- }
- };
return (
- {/* Categories Section */}
+ {/* Categories Section - Using MultiSelect */}
{categories && categories.length > 0 && (
{categories.map((category, categoryIndex) => (
-
- {/* Category Label */}
-
-
- {category.name}
-
-
- {/* Pills Container */}
-
- {/* Pills */}
- {category.items && category.items.map((item, itemIndex) => (
-
- ))}
- {/* Add Button */}
-
-
-
+
{
+ category.onChipClick?.(category.name, chipId);
+ }}
+ onAddClick={() => {
+ category.onAddClick?.(category.name);
+ }}
+ onCustomChipConfirm={(chipId, value) => {
+ category.onCustomChipConfirm?.(category.name, chipId, value);
+ }}
+ onCustomChipClose={(chipId) => {
+ category.onCustomChipClose?.(category.name, chipId);
+ }}
+ showAddButton={true}
+ addButtonText="" // Empty text for icon-only circular button
+ className="w-full"
+ />
))}
)}
diff --git a/lib/propNormalization.ts b/lib/propNormalization.ts
index ea8600e..0c5d6de 100644
--- a/lib/propNormalization.ts
+++ b/lib/propNormalization.ts
@@ -618,3 +618,35 @@ export function normalizeMultiSelectSize(
}
return defaultValue;
}
+
+/**
+ * Normalize InputLabel size prop values (S/M -> s/m)
+ */
+export function normalizeInputLabelSize(
+ value: string | undefined,
+ defaultValue: "s" = "s",
+): "s" | "m" {
+ if (!value) return defaultValue;
+ const normalized = value.toLowerCase();
+ const sizes = ["s", "m"];
+ if (sizes.includes(normalized)) {
+ return normalized as typeof defaultValue;
+ }
+ return defaultValue;
+}
+
+/**
+ * Normalize InputLabel palette prop values (Default/Inverse -> default/inverse)
+ */
+export function normalizeInputLabelPalette(
+ value: string | undefined,
+ defaultValue: "default" = "default",
+): "default" | "inverse" {
+ if (!value) return defaultValue;
+ const normalized = value.toLowerCase();
+ const palettes = ["default", "inverse"];
+ if (palettes.includes(normalized)) {
+ return normalized as typeof defaultValue;
+ }
+ return defaultValue;
+}