From 3970b43c4e6d95de231bd8752cc1e3ebf77ba491 Mon Sep 17 00:00:00 2001 From: Coooder <1637120528@qq.com> Date: Sun, 9 Nov 2025 13:06:15 +0800 Subject: [PATCH 1/2] feat: support color picker for tags --- .../app/dashboard/projects/project-card.tsx | 3 +- apps/builder/app/dashboard/projects/tags.tsx | 96 +++++++++++++++---- apps/builder/app/shared/db/user.server.ts | 4 + 3 files changed, 83 insertions(+), 20 deletions(-) diff --git a/apps/builder/app/dashboard/projects/project-card.tsx b/apps/builder/app/dashboard/projects/project-card.tsx index 82d796cf9c55..a32ca82f826f 100644 --- a/apps/builder/app/dashboard/projects/project-card.tsx +++ b/apps/builder/app/dashboard/projects/project-card.tsx @@ -204,12 +204,13 @@ export const ProjectCard = ({ {projectsTags.map((tag) => { const isApplied = projectTagsIds.includes(tag.id); if (isApplied) { + const backgroundColor = tag.color ?? "oklch(0 0 0 / 0.3)"; return ( { + const candidate = typeof value === "string" ? value : undefined; + if (candidate == null) { + return undefined; + } + return /^#[0-9a-f]{6}$/i.test(candidate) ? candidate : undefined; +}; type DeleteConfirmationDialogProps = { onClose: () => void; @@ -139,7 +147,18 @@ const TagsList = ({ defaultChecked={projectTagsIds.includes(tag.id)} /> @@ -221,19 +240,23 @@ const TagEdit = ({ event.preventDefault(); const formData = new FormData(event.currentTarget); const label = ((formData.get("tag") as string) || "").trim(); - if (tag.label === label || !label) { + const color = + normalizeHexColor(formData.get("tagColor")) ?? + tag.color ?? + DEFAULT_TAG_COLOR; + if ((tag.label === label && tag.color === color) || !label) { return; } let updatedTags = []; if (isExisting) { updatedTags = projectsTags.map((availableTag) => { if (availableTag.id === tag.id) { - return { ...availableTag, label }; + return { ...availableTag, label, color }; } return availableTag; }); } else { - updatedTags = [...projectsTags, { id: tag.id, label }]; + updatedTags = [...projectsTags, { id: tag.id, label, color }]; } await nativeClient.user.updateProjectsTags.mutate({ @@ -243,14 +266,34 @@ const TagEdit = ({ onComplete(); }} > - - + + + + + + + + + + ); }; diff --git a/apps/builder/app/shared/db/user.server.ts b/apps/builder/app/shared/db/user.server.ts index e25cc79ede83..16b14a5d6d85 100644 --- a/apps/builder/app/shared/db/user.server.ts +++ b/apps/builder/app/shared/db/user.server.ts @@ -110,6 +110,10 @@ export const createOrLoginWithDev = async ( export const userProjectTagSchema = z.object({ id: z.string(), label: z.string().min(1).max(100), + color: z + .string() + .regex(/^#[0-9a-f]{6}$/i, "Color must be a 6-digit hex value") + .optional(), }); export type ProjectTag = z.infer; From 03a1dfdaec7996715cc772fde5adb8f5b2a981ba Mon Sep 17 00:00:00 2001 From: Coooder <1637120528@qq.com> Date: Sun, 16 Nov 2025 19:33:36 +0800 Subject: [PATCH 2/2] fix: improve for project tags according to feedback --- .../style-panel/shared/color-picker.tsx | 2 +- .../css-value-input/css-value-input.tsx | 2 +- .../style-panel/shared/repeated-style.tsx | 2 +- apps/builder/app/dashboard/projects/colors.ts | 26 +++- .../app/dashboard/projects/project-card.tsx | 3 +- apps/builder/app/dashboard/projects/tags.tsx | 76 +++++++++--- .../src/components}/color-thumb.tsx | 70 ++++++----- .../src/components/simple-color-picker.tsx | 111 ++++++++++++++++++ packages/design-system/src/index.ts | 2 + 9 files changed, 236 insertions(+), 58 deletions(-) rename {apps/builder/app/builder/features/style-panel/shared => packages/design-system/src/components}/color-thumb.tsx (66%) create mode 100644 packages/design-system/src/components/simple-color-picker.tsx diff --git a/apps/builder/app/builder/features/style-panel/shared/color-picker.tsx b/apps/builder/app/builder/features/style-panel/shared/color-picker.tsx index c6e54878d160..2f6e86b33d0a 100644 --- a/apps/builder/app/builder/features/style-panel/shared/color-picker.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/color-picker.tsx @@ -25,7 +25,7 @@ import { toValue } from "@webstudio-is/css-engine"; import { theme } from "@webstudio-is/design-system"; import { CssValueInput } from "./css-value-input"; import type { IntermediateStyleValue } from "./css-value-input/css-value-input"; -import { ColorThumb } from "./color-thumb"; +import { ColorThumb } from "@webstudio-is/design-system"; // To support color names extend([namesPlugin]); diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx index 32e375a3735c..f95be7333462 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx @@ -51,7 +51,7 @@ import { convertUnits } from "./convert-units"; import { mergeRefs } from "@react-aria/utils"; import { composeEventHandlers } from "~/shared/event-utils"; import type { StyleValueSourceColor } from "~/shared/style-object-model"; -import { ColorThumb } from "../color-thumb"; +import { ColorThumb } from "@webstudio-is/design-system"; import { cssButtonDisplay, isComplexValue, diff --git a/apps/builder/app/builder/features/style-panel/shared/repeated-style.tsx b/apps/builder/app/builder/features/style-panel/shared/repeated-style.tsx index 9f86f1c6085f..affb7532385f 100644 --- a/apps/builder/app/builder/features/style-panel/shared/repeated-style.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/repeated-style.tsx @@ -24,7 +24,7 @@ import { import { repeatUntil } from "~/shared/array-utils"; import type { ComputedStyleDecl } from "~/shared/style-object-model"; import { createBatchUpdate, type StyleUpdateOptions } from "./use-style-data"; -import { ColorThumb } from "./color-thumb"; +import { ColorThumb } from "@webstudio-is/design-system"; const isRepeatedValue = ( styleValue: StyleValue diff --git a/apps/builder/app/dashboard/projects/colors.ts b/apps/builder/app/dashboard/projects/colors.ts index 9b05f55fd9b6..cb39ae301c61 100644 --- a/apps/builder/app/dashboard/projects/colors.ts +++ b/apps/builder/app/dashboard/projects/colors.ts @@ -1,6 +1,20 @@ -export const colors = Array.from({ length: 50 }, (_, i) => { - const l = 55 + (i % 3) * 3; // Reduced variation in lightness (55-61%) to lower contrast - const c = 0.14 + (i % 2) * 0.02; // Reduced variation in chroma (0.14-0.16) for balance - const h = (i * 137.5) % 360; // Golden angle for pleasing hue distribution - return `oklch(${l}% ${c.toFixed(2)} ${h.toFixed(1)})`; -}); +export const DEFAULT_TAG_COLOR = "#6B6B6B"; + +export const colors = [ + "#D73A4A", // Red + "#F28B3E", // Orange + "#FBCA04", // Yellow + "#28A745", // Green + "#2088FF", // Teal + "#0366D6", // Blue + "#0052CC", // Indigo + "#8A63D2", // Purple + "#E99695", // Light Pink + "#F9D0C4", // Pink-ish Peach + "#F9E79F", // Pale Yellow + "#CCEBC5", // Light Green + "#D1E7DD", // Light Cyan + "#BFD7FF", // Light Blue + "#C7D2FE", // Azure Light + "#D8B4FE", // Lavender +] as const; diff --git a/apps/builder/app/dashboard/projects/project-card.tsx b/apps/builder/app/dashboard/projects/project-card.tsx index a32ca82f826f..bbe7f4e2db31 100644 --- a/apps/builder/app/dashboard/projects/project-card.tsx +++ b/apps/builder/app/dashboard/projects/project-card.tsx @@ -32,6 +32,7 @@ import { Spinner } from "../shared/spinner"; import { Card, CardContent, CardFooter } from "../shared/card"; import type { User } from "~/shared/db/user.server"; import { TagsDialog } from "./tags"; +import { DEFAULT_TAG_COLOR } from "./colors"; const infoIconStyle = css({ flexShrink: 0 }); @@ -204,7 +205,7 @@ export const ProjectCard = ({ {projectsTags.map((tag) => { const isApplied = projectTagsIds.includes(tag.id); if (isApplied) { - const backgroundColor = tag.color ?? "oklch(0 0 0 / 0.3)"; + const backgroundColor = tag.color ?? DEFAULT_TAG_COLOR; return ( { - const candidate = typeof value === "string" ? value : undefined; - if (candidate == null) { +const normalizeHexColor = (value: string | null | undefined) => { + const candidate = + typeof value === "string" ? value.trim().toLowerCase() : undefined; + if (candidate == null || candidate.length === 0) { return undefined; } - return /^#[0-9a-f]{6}$/i.test(candidate) ? candidate : undefined; + const normalized = candidate.startsWith("#") ? candidate : `#${candidate}`; + return /^#[0-9a-f]{6}$/.test(normalized) ? normalized : undefined; }; +const formatColorForInput = (value?: string) => { + if (value == null || value === "") { + return ""; + } + const normalized = normalizeHexColor(value); + return normalized ? normalized.toUpperCase() : value.toUpperCase(); +}; + +const getDisplayColor = (color: string | undefined) => + color ?? DEFAULT_TAG_COLOR; + type DeleteConfirmationDialogProps = { onClose: () => void; onConfirm: () => void; @@ -151,7 +165,7 @@ const TagsList = ({ color="contrast" key={tag.id} css={{ - background: tag.color ?? "oklch(0 0 0 / 0.3)", + background: getDisplayColor(tag.color), borderRadius: theme.borderRadius[3], paddingInline: theme.spacing[3], width: "fit-content", @@ -233,6 +247,13 @@ const TagEdit = ({ }) => { const revalidator = useRevalidator(); const isExisting = projectsTags.some(({ id }) => id === tag.id); + const [color, setColor] = useState(() => + formatColorForInput(tag.color ?? DEFAULT_TAG_COLOR) + ); + + useEffect(() => { + setColor(formatColorForInput(tag.color ?? DEFAULT_TAG_COLOR)); + }, [tag.color]); return (
{ if (availableTag.id === tag.id) { - return { ...availableTag, label, color }; + return { ...availableTag, label, color: normalizedColor }; } return availableTag; }); } else { - updatedTags = [...projectsTags, { id: tag.id, label, color }]; + updatedTags = [ + ...projectsTags, + { id: tag.id, label, color: normalizedColor }, + ]; } await nativeClient.user.updateProjectsTags.mutate({ @@ -272,7 +297,7 @@ const TagEdit = ({ gap: theme.spacing[8], }} > - + - + { + setColor(event.target.value.toUpperCase()); + }} + placeholder="#AABBCC" + maxLength={7} + prefix={ + { + setColor(formatColorForInput(preset)); + }} + colors={tagColors} + aria-label="Pick tag color" + /> + } aria-label="Tag color" /> @@ -384,7 +422,7 @@ export const Tag = ({ const [searchParams, setSearchParams] = useSearchParams(); const selectedTagsIds = searchParams.getAll("tag"); const isSelected = selectedTagsIds.includes(tag.id); - const color = tag.color ?? "oklch(0 0 0 / 0.3)"; + const color = getDisplayColor(tag.color); return (