Create InputLabel component and add RuleCard chips
This commit is contained in:
+341
-26
@@ -97,33 +97,360 @@ export default function ComponentsPreview() {
|
|||||||
{ id: "custom-2", label: "", state: "Custom", palette: "Default", size: "M" },
|
{ 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",
|
name: "Values",
|
||||||
items: ["Consciousness", "Ecology", "Abundance", "Art", "Decisiveness"],
|
chipOptions: [
|
||||||
createUrl: "/create/value",
|
{ 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",
|
name: "Communication",
|
||||||
items: ["Signal"],
|
chipOptions: [
|
||||||
createUrl: "/create/communication",
|
{ 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",
|
name: "Membership",
|
||||||
items: ["Open Admission"],
|
chipOptions: [
|
||||||
createUrl: "/create/membership",
|
{ 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",
|
name: "Decision-making",
|
||||||
items: ["Lazy Consensus", "Modified Consensus"],
|
chipOptions: [
|
||||||
createUrl: "/create/decision-making",
|
{ 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",
|
name: "Conflict management",
|
||||||
items: ["Code of Conduct", "Restorative Justice"],
|
chipOptions: [
|
||||||
createUrl: "/create/conflict-management",
|
{ 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 (
|
return (
|
||||||
<div className="min-h-screen bg-[var(--color-surface-default-primary)] p-[var(--spacing-scale-032)]">
|
<div className="min-h-screen bg-[var(--color-surface-default-primary)] p-[var(--spacing-scale-032)]">
|
||||||
@@ -355,13 +682,7 @@ export default function ComponentsPreview() {
|
|||||||
className="w-[568px]"
|
className="w-[568px]"
|
||||||
logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png"
|
logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png"
|
||||||
logoAlt="Mutual Aid Mondays"
|
logoAlt="Mutual Aid Mondays"
|
||||||
categories={sampleCategories}
|
categories={ruleCardCategories}
|
||||||
onPillClick={(category, item) => {
|
|
||||||
console.log(`Pill clicked: ${category} - ${item}`);
|
|
||||||
}}
|
|
||||||
onCreateClick={(category) => {
|
|
||||||
console.log(`Create clicked: ${category}`);
|
|
||||||
}}
|
|
||||||
onClick={() => console.log("Card clicked: Mutual Aid Mondays")}
|
onClick={() => console.log("Card clicked: Mutual Aid Mondays")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -382,13 +703,7 @@ export default function ComponentsPreview() {
|
|||||||
className="w-[398px]"
|
className="w-[398px]"
|
||||||
logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png"
|
logoUrl="http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png"
|
||||||
logoAlt="Mutual Aid Mondays"
|
logoAlt="Mutual Aid Mondays"
|
||||||
categories={sampleCategories}
|
categories={ruleCardCategories}
|
||||||
onPillClick={(category, item) => {
|
|
||||||
console.log(`Pill clicked: ${category} - ${item}`);
|
|
||||||
}}
|
|
||||||
onCreateClick={(category) => {
|
|
||||||
console.log(`Create clicked: ${category}`);
|
|
||||||
}}
|
|
||||||
onClick={() => console.log("Card clicked: Mutual Aid Mondays")}
|
onClick={() => console.log("Card clicked: Mutual Aid Mondays")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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<InputLabelProps>(
|
||||||
|
({
|
||||||
|
label,
|
||||||
|
helpIcon = false,
|
||||||
|
asterisk = false,
|
||||||
|
helperText = false,
|
||||||
|
size: sizeProp = "S",
|
||||||
|
palette: paletteProp = "Default",
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
|
const size = normalizeInputLabelSize(sizeProp);
|
||||||
|
const palette = normalizeInputLabelPalette(paletteProp);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputLabelView
|
||||||
|
label={label}
|
||||||
|
helpIcon={helpIcon}
|
||||||
|
asterisk={asterisk}
|
||||||
|
helperText={helperText}
|
||||||
|
size={size}
|
||||||
|
palette={palette}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
InputLabelContainer.displayName = "InputLabel";
|
||||||
|
|
||||||
|
export default InputLabelContainer;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className={`${containerClass} ${className}`}>
|
||||||
|
<div className={labelContainerClass}>
|
||||||
|
<div className="flex gap-px items-start relative shrink-0">
|
||||||
|
<p
|
||||||
|
className={`font-inter font-normal ${labelTextSize} ${labelColor} relative shrink-0`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
{asterisk && (
|
||||||
|
<p
|
||||||
|
className={`font-inter font-medium ${asteriskSize} relative shrink-0 text-[color:var(--color-content-default-negative-primary,#ea4845)]`}
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{helpIcon && (
|
||||||
|
<div className={`relative shrink-0 ${helpIconSize}`}>
|
||||||
|
<img
|
||||||
|
src={getAssetPath(ASSETS.ICON_HELP)}
|
||||||
|
alt="Help"
|
||||||
|
className="block max-w-none size-full"
|
||||||
|
style={
|
||||||
|
helpIconFilter
|
||||||
|
? {
|
||||||
|
filter: helpIconFilter,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{helperText && (
|
||||||
|
<p
|
||||||
|
className={`flex-[1_0_0] font-inter font-normal ${helperTextSize} min-h-px min-w-px relative ${helperTextColor} text-right`}
|
||||||
|
>
|
||||||
|
{typeof helperText === "string" ? helperText : "Optional text"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputLabelView.displayName = "InputLabelView";
|
||||||
|
|
||||||
|
export default memo(InputLabelView);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import InputLabel from "./InputLabel.container";
|
||||||
|
|
||||||
|
export default InputLabel;
|
||||||
@@ -3,13 +3,14 @@
|
|||||||
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 } from "../../../lib/propNormalization";
|
import { normalizeMultiSelectSize, normalizeChipPalette } from "../../../lib/propNormalization";
|
||||||
|
|
||||||
const MultiSelectContainer = memo<MultiSelectProps>(
|
const MultiSelectContainer = memo<MultiSelectProps>(
|
||||||
({
|
({
|
||||||
label,
|
label,
|
||||||
showHelpIcon = true,
|
showHelpIcon = true,
|
||||||
size: sizeProp = "M",
|
size: sizeProp = "M",
|
||||||
|
palette: paletteProp = "Default",
|
||||||
options,
|
options,
|
||||||
onChipClick,
|
onChipClick,
|
||||||
onAddClick,
|
onAddClick,
|
||||||
@@ -20,12 +21,14 @@ const MultiSelectContainer = memo<MultiSelectProps>(
|
|||||||
className = "",
|
className = "",
|
||||||
}) => {
|
}) => {
|
||||||
const size = normalizeMultiSelectSize(sizeProp);
|
const size = normalizeMultiSelectSize(sizeProp);
|
||||||
|
const palette = normalizeChipPalette(paletteProp);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelectView
|
<MultiSelectView
|
||||||
label={label}
|
label={label}
|
||||||
showHelpIcon={showHelpIcon}
|
showHelpIcon={showHelpIcon}
|
||||||
size={size}
|
size={size}
|
||||||
|
palette={palette}
|
||||||
options={options}
|
options={options}
|
||||||
onChipClick={onChipClick}
|
onChipClick={onChipClick}
|
||||||
onAddClick={onAddClick}
|
onAddClick={onAddClick}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ChipStateValue, ChipSizeValue } from "../../../lib/propNormalization";
|
import type { ChipStateValue, ChipSizeValue, ChipPaletteValue } from "../../../lib/propNormalization";
|
||||||
|
|
||||||
export interface ChipOption {
|
export interface ChipOption {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -22,6 +22,11 @@ export interface MultiSelectProps {
|
|||||||
* Accepts both uppercase (Figma) and lowercase values.
|
* Accepts both uppercase (Figma) and lowercase values.
|
||||||
*/
|
*/
|
||||||
size?: MultiSelectSizeValue;
|
size?: MultiSelectSizeValue;
|
||||||
|
/**
|
||||||
|
* Palette for chips: "Default" or "Inverse"
|
||||||
|
* Accepts both PascalCase (Figma) and lowercase values.
|
||||||
|
*/
|
||||||
|
palette?: ChipPaletteValue;
|
||||||
/**
|
/**
|
||||||
* Array of chip options to display
|
* Array of chip options to display
|
||||||
*/
|
*/
|
||||||
@@ -57,6 +62,7 @@ export interface MultiSelectViewProps {
|
|||||||
label?: string;
|
label?: string;
|
||||||
showHelpIcon: boolean;
|
showHelpIcon: boolean;
|
||||||
size: "s" | "m";
|
size: "s" | "m";
|
||||||
|
palette: "default" | "inverse";
|
||||||
options: ChipOption[];
|
options: ChipOption[];
|
||||||
onChipClick?: (chipId: string) => void;
|
onChipClick?: (chipId: string) => void;
|
||||||
onAddClick?: () => void;
|
onAddClick?: () => void;
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
|
||||||
import Chip from "../Chip";
|
import Chip from "../Chip";
|
||||||
|
import InputLabel from "../InputLabel";
|
||||||
import type { MultiSelectViewProps } from "./MultiSelect.types";
|
import type { MultiSelectViewProps } from "./MultiSelect.types";
|
||||||
|
|
||||||
function MultiSelectView({
|
function MultiSelectView({
|
||||||
label,
|
label,
|
||||||
showHelpIcon,
|
showHelpIcon,
|
||||||
size,
|
size,
|
||||||
|
palette,
|
||||||
options,
|
options,
|
||||||
onChipClick,
|
onChipClick,
|
||||||
onAddClick,
|
onAddClick,
|
||||||
@@ -19,44 +20,27 @@ function MultiSelectView({
|
|||||||
className,
|
className,
|
||||||
}: MultiSelectViewProps) {
|
}: MultiSelectViewProps) {
|
||||||
const isSmall = size === "s";
|
const isSmall = size === "s";
|
||||||
|
const isInverse = palette === "inverse";
|
||||||
|
|
||||||
// Size-based spacing and typography
|
// Size-based spacing
|
||||||
const gapClass = isSmall
|
const gapClass = isSmall
|
||||||
? "gap-[var(--measures-spacing-200,8px)]"
|
? "gap-[var(--measures-spacing-200,8px)]"
|
||||||
: "gap-[var(--measures-spacing-300,12px)]";
|
: "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";
|
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 with help icon */}
|
{/* Label using InputLabel component */}
|
||||||
{label && (
|
{label && (
|
||||||
<div className={`flex flex-wrap ${labelGapClass} items-baseline ${isSmall ? "pr-[var(--measures-spacing-100,4px)]" : ""} relative shrink-0 w-full`}>
|
<InputLabel
|
||||||
<div className={`flex ${isSmall ? "gap-[var(--measures-spacing-050,2px)]" : "gap-[var(--measures-spacing-100,4px)]"} items-center relative shrink-0`}>
|
label={label}
|
||||||
<label className={`font-inter font-normal ${labelTextSize} text-[color:var(--color-content-default-primary,white)]`}>
|
helpIcon={showHelpIcon}
|
||||||
{label}
|
asterisk={false}
|
||||||
</label>
|
helperText={false}
|
||||||
{showHelpIcon && (
|
size={size === "s" ? "S" : "M"}
|
||||||
<div className={`relative shrink-0 ${helpIconSize}`}>
|
palette={palette === "inverse" ? "Inverse" : "Default"}
|
||||||
<img
|
/>
|
||||||
src={getAssetPath(ASSETS.ICON_HELP)}
|
|
||||||
alt="Help"
|
|
||||||
className="block max-w-none size-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Chips container */}
|
{/* Chips container */}
|
||||||
@@ -66,7 +50,7 @@ function MultiSelectView({
|
|||||||
key={option.id}
|
key={option.id}
|
||||||
label={option.state === "Custom" ? "" : option.label}
|
label={option.state === "Custom" ? "" : option.label}
|
||||||
state={option.state || "Unselected"}
|
state={option.state || "Unselected"}
|
||||||
palette="Default"
|
palette={palette === "inverse" ? "Inverse" : "Default"}
|
||||||
size={chipSize}
|
size={chipSize}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Only toggle if not in Custom state
|
// 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 && (
|
{showAddButton && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -97,7 +81,13 @@ function MultiSelectView({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onAddClick?.();
|
onAddClick?.();
|
||||||
}}
|
}}
|
||||||
className={`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`}
|
className={
|
||||||
|
!addButtonText
|
||||||
|
? // Circular button with border (RuleCard style)
|
||||||
|
`bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))] border-[1.25px] ${isInverse ? "border-[var(--color-border-default-primary,#141414)]" : "border-[var(--color-border-default-tertiary,#464646)]"} border-solid flex items-center justify-center ${isSmall ? "size-[30px]" : "size-[40px]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`
|
||||||
|
: // Ghost button style (standalone MultiSelect)
|
||||||
|
`flex ${isSmall ? "gap-[var(--measures-spacing-050,2px)]" : "gap-[var(--measures-spacing-150,6px)]"} items-center justify-center ${isSmall ? "p-[var(--measures-spacing-200,8px)]" : "p-[var(--measures-spacing-300,12px)]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Plus icon */}
|
{/* Plus icon */}
|
||||||
<svg
|
<svg
|
||||||
@@ -106,7 +96,7 @@ function MultiSelectView({
|
|||||||
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="text-[var(--color-content-default-brand-primary,#fefcc9)] shrink-0"
|
className={`${isInverse ? "text-[var(--color-content-inverse-primary,black)]" : "text-[var(--color-content-default-brand-primary,#fefcc9)]"} shrink-0`}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M7 3V11M3 7H11"
|
d="M7 3V11M3 7H11"
|
||||||
@@ -116,10 +106,12 @@ function MultiSelectView({
|
|||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{/* Text */}
|
{/* Text - only show if addButtonText is provided */}
|
||||||
<span className={`font-inter font-medium ${isSmall ? "text-[length:var(--sizing-300,12px)] leading-[14px]" : "text-[length:var(--sizing-400,16px)] leading-[20px]"} text-[color:var(--color-content-default-brand-primary,#fefcc9)]`}>
|
{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>
|
{addButtonText}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ const RuleCardContainer = memo<RuleCardProps>(
|
|||||||
expanded = false,
|
expanded = false,
|
||||||
size: sizeProp,
|
size: sizeProp,
|
||||||
categories,
|
categories,
|
||||||
onPillClick,
|
|
||||||
onCreateClick,
|
|
||||||
logoUrl,
|
logoUrl,
|
||||||
logoAlt,
|
logoAlt,
|
||||||
communityInitials,
|
communityInitials,
|
||||||
@@ -77,8 +75,6 @@ const RuleCardContainer = memo<RuleCardProps>(
|
|||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
size={size}
|
size={size}
|
||||||
categories={categories}
|
categories={categories}
|
||||||
onPillClick={onPillClick}
|
|
||||||
onCreateClick={onCreateClick}
|
|
||||||
logoUrl={logoUrl}
|
logoUrl={logoUrl}
|
||||||
logoAlt={logoAlt}
|
logoAlt={logoAlt}
|
||||||
communityInitials={communityInitials}
|
communityInitials={communityInitials}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
import type { ChipOption } from "../MultiSelect/MultiSelect.types";
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
name: string;
|
name: string;
|
||||||
items: string[];
|
chipOptions: ChipOption[];
|
||||||
createUrl?: string;
|
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 {
|
export interface RuleCardProps {
|
||||||
@@ -14,8 +19,6 @@ export interface RuleCardProps {
|
|||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
size?: "L" | "M" | "l" | "m";
|
size?: "L" | "M" | "l" | "m";
|
||||||
categories?: Category[];
|
categories?: Category[];
|
||||||
onPillClick?: (category: string, item: string) => void;
|
|
||||||
onCreateClick?: (category: string) => void;
|
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
logoAlt?: string;
|
logoAlt?: string;
|
||||||
communityInitials?: string;
|
communityInitials?: string;
|
||||||
@@ -32,8 +35,6 @@ export interface RuleCardViewProps {
|
|||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
size: "L" | "M";
|
size: "L" | "M";
|
||||||
categories?: Category[];
|
categories?: Category[];
|
||||||
onPillClick?: (category: string, item: string) => void;
|
|
||||||
onCreateClick?: (category: string) => void;
|
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
logoAlt?: string;
|
logoAlt?: string;
|
||||||
communityInitials?: string;
|
communityInitials?: string;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useTranslation } from "../../contexts/MessagesContext";
|
import { useTranslation } from "../../contexts/MessagesContext";
|
||||||
|
import MultiSelect from "../MultiSelect";
|
||||||
import type { RuleCardViewProps } from "./RuleCard.types";
|
import type { RuleCardViewProps } from "./RuleCard.types";
|
||||||
|
|
||||||
export function RuleCardView({
|
export function RuleCardView({
|
||||||
@@ -15,8 +16,6 @@ export function RuleCardView({
|
|||||||
expanded,
|
expanded,
|
||||||
size,
|
size,
|
||||||
categories,
|
categories,
|
||||||
onPillClick,
|
|
||||||
onCreateClick,
|
|
||||||
logoUrl,
|
logoUrl,
|
||||||
logoAlt,
|
logoAlt,
|
||||||
communityInitials,
|
communityInitials,
|
||||||
@@ -111,21 +110,6 @@ export function RuleCardView({
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle pill click with stopPropagation
|
|
||||||
const handlePillClick = (e: React.MouseEvent<HTMLButtonElement>, categoryName: string, item: string) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (onPillClick) {
|
|
||||||
onPillClick(categoryName, item);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle create button click with stopPropagation
|
|
||||||
const handleCreateClick = (e: React.MouseEvent<HTMLButtonElement>, categoryName: string) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (onCreateClick) {
|
|
||||||
onCreateClick(categoryName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -157,44 +141,33 @@ export function RuleCardView({
|
|||||||
|
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<>
|
<>
|
||||||
{/* Categories Section */}
|
{/* Categories Section - Using MultiSelect */}
|
||||||
{categories && categories.length > 0 && (
|
{categories && categories.length > 0 && (
|
||||||
<div className="flex flex-col gap-[16px] items-start px-[12px] relative shrink-0 w-full">
|
<div className="flex flex-col gap-[16px] items-start px-[12px] relative shrink-0 w-full">
|
||||||
{categories.map((category, categoryIndex) => (
|
{categories.map((category, categoryIndex) => (
|
||||||
<div key={categoryIndex} className="flex flex-col gap-[var(--spacing-scale-008)] items-start relative shrink-0 w-full">
|
<MultiSelect
|
||||||
{/* Category Label */}
|
key={categoryIndex}
|
||||||
<div className="flex items-baseline gap-[var(--spacing-scale-008)] pr-[var(--spacing-scale-004)] relative shrink-0 w-full">
|
label={category.name}
|
||||||
<h4 className={`${categoryLabelClass} text-[var(--color-content-inverse-primary)]`}>
|
showHelpIcon={false}
|
||||||
{category.name}
|
size="S"
|
||||||
</h4>
|
palette="Inverse"
|
||||||
</div>
|
options={category.chipOptions}
|
||||||
{/* Pills Container */}
|
onChipClick={(chipId) => {
|
||||||
<div className="flex flex-wrap gap-[var(--spacing-scale-008)] items-center relative shrink-0 w-full">
|
category.onChipClick?.(category.name, chipId);
|
||||||
{/* Pills */}
|
}}
|
||||||
{category.items && category.items.map((item, itemIndex) => (
|
onAddClick={() => {
|
||||||
<button
|
category.onAddClick?.(category.name);
|
||||||
key={itemIndex}
|
}}
|
||||||
type="button"
|
onCustomChipConfirm={(chipId, value) => {
|
||||||
className="bg-transparent border-[1.25px] border-[var(--color-content-inverse-primary)] h-[30px] px-[var(--spacing-scale-008)] rounded-[var(--radius-measures-radius-full)] flex items-center justify-center shrink-0 cursor-pointer"
|
category.onCustomChipConfirm?.(category.name, chipId, value);
|
||||||
onClick={(e) => handlePillClick(e, category.name, item)}
|
}}
|
||||||
aria-label={`Edit ${item}`}
|
onCustomChipClose={(chipId) => {
|
||||||
>
|
category.onCustomChipClose?.(category.name, chipId);
|
||||||
<span className={`${pillTextClass} text-[var(--color-content-inverse-primary)]`}>
|
}}
|
||||||
{item}
|
showAddButton={true}
|
||||||
</span>
|
addButtonText="" // Empty text for icon-only circular button
|
||||||
</button>
|
className="w-full"
|
||||||
))}
|
/>
|
||||||
{/* Add Button */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-transparent border-[1.25px] border-[var(--color-content-inverse-primary)] size-[30px] rounded-[var(--radius-measures-radius-full)] flex items-center justify-center shrink-0 cursor-pointer"
|
|
||||||
onClick={(e) => handleCreateClick(e, category.name)}
|
|
||||||
aria-label={`Add new ${category.name}`}
|
|
||||||
>
|
|
||||||
<span className="text-[var(--color-content-inverse-primary)] text-[14px] leading-[14px]">+</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -618,3 +618,35 @@ export function normalizeMultiSelectSize(
|
|||||||
}
|
}
|
||||||
return defaultValue;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user