diff --git a/components/drops/view/item/rate/give/DropListItemRateGiveSubmit.tsx b/components/drops/view/item/rate/give/DropListItemRateGiveSubmit.tsx index 74c93fcd66..9d10c854d9 100644 --- a/components/drops/view/item/rate/give/DropListItemRateGiveSubmit.tsx +++ b/components/drops/view/item/rate/give/DropListItemRateGiveSubmit.tsx @@ -123,6 +123,7 @@ export default function DropListItemRateGiveSubmit({ max_rating: 0, reaction: null, boosted: false, + bookmarked: false, }; draft.context_profile_context = { diff --git a/components/home/HomePageContent.tsx b/components/home/HomePageContent.tsx index 33903a8fd8..20db6dc028 100644 --- a/components/home/HomePageContent.tsx +++ b/components/home/HomePageContent.tsx @@ -1,6 +1,6 @@ "use client"; -import SubmissionCarouselSection from "./SubmissionCarouselSection"; +import { HeroHeader } from "./hero"; import { NowMintingSection } from "./now-minting"; import { NextMintLeadingSection } from "./next-mint-leading"; import { BoostedSection } from "./boosted"; @@ -9,8 +9,9 @@ import { ExploreWavesSection } from "./explore-waves"; export default function HomePageContent() { return (
+ - +
diff --git a/components/home/boosted/BoostedDropCardHome.tsx b/components/home/boosted/BoostedDropCardHome.tsx index 1eea728b7b..cb32055792 100644 --- a/components/home/boosted/BoostedDropCardHome.tsx +++ b/components/home/boosted/BoostedDropCardHome.tsx @@ -9,6 +9,7 @@ import ContentDisplay from "@/components/waves/drops/ContentDisplay"; import { buildProcessedContent } from "@/components/waves/drops/media-utils"; import type { ApiDrop } from "@/generated/models/ApiDrop"; import { getScaledImageUri, ImageScale } from "@/helpers/image.helpers"; +import { getTimeAgoShort } from "@/helpers/Helpers"; import { LinkIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; import { memo, useCallback, useMemo } from "react"; @@ -28,13 +29,14 @@ const BoostedDropCardHome = memo( [part?.content, part?.media] ); - // Check if content is primarily a link - const urlRegex = /^https?:\/\/[^\s]+$/i; + // Check if content starts with a link (for icon display) const textContent = part?.content ?? ""; - const isLink = urlRegex.test(textContent.trim()); - const textColorClass = isLink ? "tw-text-primary-300" : "tw-text-iron-300"; + const startsWithLink = /^https?:\/\//i.test(textContent.trim()); const { author, wave, boosts } = drop; + const MAX_FIRE_ICONS = 5; + const fireIconsToShow = Math.min(boosts, MAX_FIRE_ICONS); + const remainingBoosts = boosts - MAX_FIRE_ICONS; const waveName = wave.name; const handleCardKeyDown = useCallback( (event: React.KeyboardEvent) => { @@ -53,23 +55,35 @@ const BoostedDropCardHome = memo( tabIndex={0} onClick={onClick} onKeyDown={handleCardKeyDown} - className="hover:tw-border-50/10 tw-group tw-relative tw-flex tw-w-60 tw-flex-shrink-0 tw-cursor-pointer tw-flex-col tw-overflow-hidden tw-rounded-xl tw-border tw-border-solid tw-border-white/5 tw-bg-black tw-p-0 tw-text-left tw-transition-all tw-duration-500 tw-ease-out hover:tw--translate-y-1.5 hover:tw-shadow-[0_20px_40px_-15px_rgba(0,0,0,0.5)]" + className="hover:tw-border-50/10 tw-group tw-relative tw-flex tw-w-full tw-cursor-pointer tw-flex-col tw-overflow-hidden tw-rounded-xl tw-border tw-border-solid tw-border-white/5 tw-bg-black tw-p-0 tw-text-left tw-transition-all tw-duration-500 tw-ease-out hover:tw--translate-y-1.5 hover:tw-shadow-[0_0_40px_-5px_rgba(255,255,255,0.15)]" > {/* Inner Highlight (Glass Edge) */}
-
- - - {boosts} + {/* Time ago */} +
+ + {getTimeAgoShort(drop.created_at)}
-
- {media ? ( +
+ {Array.from({ length: fireIconsToShow }).map((_, i) => ( + + ))} + {remainingBoosts > 0 && ( + + +{remainingBoosts} + + )} +
+ + {media ? ( +
{/* Glow effect */}
- ) : ( -
- {isLink ? ( - - ) : ( - - {"\u201C"} - - )} -
- -
-
- )} -
- -
- -
- {author.handle ? ( - event.stopPropagation()} - className="tw-truncate tw-text-sm tw-font-medium tw-text-iron-50 tw-no-underline tw-transition-colors desktop-hover:hover:tw-text-white" - > - {author.handle} - +
+ ) : ( +
+ {startsWithLink ? ( + ) : ( - - Anonymous + + {"\u201C"} )} - event.stopPropagation()} - className="tw-truncate tw-text-xs tw-text-iron-500 tw-no-underline tw-transition-colors desktop-hover:hover:tw-text-iron-300" - > - {waveName} - +
+ )} + +
+ {/* Author */} + event.stopPropagation()} + className="tw-flex tw-items-center tw-gap-2 tw-no-underline tw-transition-opacity desktop-hover:hover:tw-opacity-80" + > + + + {author.handle ?? "Anonymous"} + + + + {/* Wave */} + event.stopPropagation()} + className="tw-group/wave tw-flex tw-items-center tw-gap-2 tw-no-underline tw-transition-all" + > + {waveName} + {wave.picture && ( + {waveName} + )} +
); diff --git a/components/home/boosted/BoostedSection.tsx b/components/home/boosted/BoostedSection.tsx index 49cdbca0c0..ae0ca1f6e3 100644 --- a/components/home/boosted/BoostedSection.tsx +++ b/components/home/boosted/BoostedSection.tsx @@ -2,43 +2,43 @@ import type { ApiDrop } from "@/generated/models/ApiDrop"; import { useBoostedDrops } from "@/hooks/useBoostedDrops"; -import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; +import { useMediaQuery } from "@/hooks/useMediaQuery"; import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useMemo } from "react"; +import Masonry from "react-masonry-css"; import BoostedDropCardHome from "./BoostedDropCardHome"; const BOOSTED_DROPS_LIMIT = 50; +const MAX_ROWS = 3; + +const MASONRY_BREAKPOINTS = { + default: 4, + 1280: 3, + 1024: 3, + 768: 2, + 640: 1, +}; export function BoostedSection() { const router = useRouter(); - const scrollRef = useRef(null); - const [canScrollLeft, setCanScrollLeft] = useState(false); - const [canScrollRight, setCanScrollRight] = useState(false); const { data: drops, isLoading } = useBoostedDrops({ limit: BOOSTED_DROPS_LIMIT, }); - const updateScrollState = useCallback(() => { - const container = scrollRef.current; - if (!container) return; - const maxScrollLeft = container.scrollWidth - container.clientWidth; - const epsilon = 1; - setCanScrollLeft(container.scrollLeft > epsilon); - setCanScrollRight(container.scrollLeft < maxScrollLeft - epsilon); - }, []); + // Detect breakpoints to determine column count + const isXl = useMediaQuery("(min-width: 1281px)"); + const isLg = useMediaQuery("(min-width: 1024px)"); + const isMd = useMediaQuery("(min-width: 768px)"); + const isSm = useMediaQuery("(min-width: 640px)"); - useEffect(() => { - const container = scrollRef.current; - if (!container) return; - updateScrollState(); - const handleScroll = () => updateScrollState(); - container.addEventListener("scroll", handleScroll, { passive: true }); - window.addEventListener("resize", updateScrollState); - return () => { - container.removeEventListener("scroll", handleScroll); - window.removeEventListener("resize", updateScrollState); - }; - }, [updateScrollState, drops?.length]); + // Calculate max items based on columns * MAX_ROWS (mobile: 1x6) + const visibleDrops = useMemo(() => { + if (!drops) return []; + if (!isSm) return drops.slice(0, 6); // Mobile: 1 col Ă— 6 items + const columns = isXl ? 4 : isLg ? 3 : 2; + const maxItems = columns * MAX_ROWS; + return drops.slice(0, maxItems); + }, [drops, isXl, isLg, isMd, isSm]); const handleDropClick = useCallback( (drop: ApiDrop) => { @@ -47,15 +47,6 @@ export function BoostedSection() { [router] ); - const scroll = (direction: "left" | "right") => { - if (!scrollRef.current) return; - const scrollAmount = 260; - scrollRef.current.scrollBy({ - left: direction === "left" ? -scrollAmount : scrollAmount, - behavior: "smooth", - }); - }; - if (isLoading) { return (
@@ -68,57 +59,36 @@ export function BoostedSection() { ); } - if (!drops || drops.length === 0) { + if (!drops || visibleDrops.length === 0) { return null; } return ( -
+
-
-
- - Boosted Drops - -

- Community-boosted right now -

-
-
- - -
+
+ + Boosted Drops + +

+ Community-boosted right now +

- {/* Horizontal scroll container */} -
- {drops.map((drop) => ( - handleDropClick(drop)} - /> + {visibleDrops.map((drop) => ( +
+ handleDropClick(drop)} + /> +
))} -
+
); diff --git a/components/home/explore-waves/ExploreWavesSection.tsx b/components/home/explore-waves/ExploreWavesSection.tsx index 7fc6783e2d..94328765f3 100644 --- a/components/home/explore-waves/ExploreWavesSection.tsx +++ b/components/home/explore-waves/ExploreWavesSection.tsx @@ -18,14 +18,10 @@ export function ExploreWavesSection() { } = useQuery({ queryKey: ["explore-waves-homepage", WAVES_LIMIT], queryFn: async () => { - return await commonApiFetch({ - endpoint: "waves-overview", - params: { - type: "RECENTLY_DROPPED_TO", - limit: String(WAVES_LIMIT), - offset: "0", - }, + const data = await commonApiFetch({ + endpoint: "waves-overview/hot", }); + return data.slice(0, WAVES_LIMIT); }, staleTime: 5 * 60 * 1000, // 5 minutes }); @@ -40,16 +36,13 @@ export function ExploreWavesSection() { } return ( -
+
- Explore waves + Tired of bot replies? -

- Browse channels—jump into the conversation. -

+

+ 6529 +

+

+ Building a decentralized network state +

+

+ Join the most interesting chats in crypto +

+
+ ); +} diff --git a/components/home/hero/index.ts b/components/home/hero/index.ts index f1f4adc1be..34c96a1bf6 100644 --- a/components/home/hero/index.ts +++ b/components/home/hero/index.ts @@ -1 +1,2 @@ export { default as CarouselHeader } from "./CarouselHeader"; +export { default as HeroHeader } from "./HeroHeader"; diff --git a/components/home/now-minting/NowMintingCountdownActive.tsx b/components/home/now-minting/NowMintingCountdownActive.tsx index 8c276731a3..215552870e 100644 --- a/components/home/now-minting/NowMintingCountdownActive.tsx +++ b/components/home/now-minting/NowMintingCountdownActive.tsx @@ -12,26 +12,25 @@ export default function NowMintingCountdownActive({ countdown, }: NowMintingCountdownActiveProps) { return ( -
-
-
- +
+
+ {countdown.title} {countdown.isActive && ( -
+
- + Live
)}
-
-
+
+
@@ -39,7 +38,7 @@ export default function NowMintingCountdownActive({ {countdown.showMintBtn && ( Mint -
-
-
+
+
+
- + Error fetching mint information diff --git a/components/home/now-minting/NowMintingCountdownFinalized.tsx b/components/home/now-minting/NowMintingCountdownFinalized.tsx index 340edbb7a9..1e2ddf5346 100644 --- a/components/home/now-minting/NowMintingCountdownFinalized.tsx +++ b/components/home/now-minting/NowMintingCountdownFinalized.tsx @@ -2,14 +2,13 @@ import ClockIcon from "@/components/utils/icons/ClockIcon"; export default function NowMintingCountdownFinalized() { return ( -
-
-
-
+
+
+
-

+

Mint phase complete

diff --git a/components/home/now-minting/NowMintingCountdownLoading.tsx b/components/home/now-minting/NowMintingCountdownLoading.tsx index 289c61639f..e8a20a9b94 100644 --- a/components/home/now-minting/NowMintingCountdownLoading.tsx +++ b/components/home/now-minting/NowMintingCountdownLoading.tsx @@ -1,11 +1,10 @@ export default function NowMintingCountdownLoading() { return ( -

-
-
+
+
-
-
+
+
); diff --git a/components/home/now-minting/NowMintingCountdownSoldOut.tsx b/components/home/now-minting/NowMintingCountdownSoldOut.tsx index e503e8a413..99fc012bc6 100644 --- a/components/home/now-minting/NowMintingCountdownSoldOut.tsx +++ b/components/home/now-minting/NowMintingCountdownSoldOut.tsx @@ -2,14 +2,13 @@ import { CheckCircleIcon } from "@heroicons/react/24/outline"; export default function NowMintingCountdownSoldOut() { return ( -
-
-
- - +
+
+ +
- + Mint complete diff --git a/components/home/now-minting/NowMintingDetails.tsx b/components/home/now-minting/NowMintingDetails.tsx index 8e730037ff..eedb503448 100644 --- a/components/home/now-minting/NowMintingDetails.tsx +++ b/components/home/now-minting/NowMintingDetails.tsx @@ -17,20 +17,18 @@ export default function NowMintingDetails({ nft }: NowMintingDetailsProps) { if (value <= 0) return "N/A"; return `${value.toFixed(5)} ETH`; }; + const floorPrice = formatEth(nft.floor_price); return (
-
+
- + +
Edition Details -
+
{details.map(({ label, value }) => (
@@ -45,7 +45,7 @@ export default function NowMintingDetailsAccordion({ Distribution Plan View diff --git a/components/home/now-minting/NowMintingHeader.tsx b/components/home/now-minting/NowMintingHeader.tsx index 8096e331f7..5ecf37b3b6 100644 --- a/components/home/now-minting/NowMintingHeader.tsx +++ b/components/home/now-minting/NowMintingHeader.tsx @@ -28,10 +28,6 @@ export default function NowMintingHeader({ return (
- - Card #{cardNumber} - - -
- {profile?.pfp ? ( - {artistName} - ) : ( -
- )} - - {hasHandle ? ( - + + Card #{cardNumber} + + + {profile?.pfp ? ( + {artistName} ) : ( - artistName +
)} + + {hasHandle ? ( + + ) : ( + artistName + )} +
diff --git a/components/home/now-minting/NowMintingSection.tsx b/components/home/now-minting/NowMintingSection.tsx index 762935392f..a18639a622 100644 --- a/components/home/now-minting/NowMintingSection.tsx +++ b/components/home/now-minting/NowMintingSection.tsx @@ -9,25 +9,25 @@ export default function NowMintingSection() { if (isFetching && !nft) { return ( -
- +
+ Latest Drop -
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
{...new Array(4).map((_, i) => (
@@ -35,8 +35,8 @@ export default function NowMintingSection() {
))}
-
-
+
+
@@ -50,18 +50,18 @@ export default function NowMintingSection() { } return ( -
- +
+ Latest Drop -
-
-
+
+
+
-
+
diff --git a/components/home/now-minting/NowMintingStatsGrid.tsx b/components/home/now-minting/NowMintingStatsGrid.tsx index c877095421..444650ec8c 100644 --- a/components/home/now-minting/NowMintingStatsGrid.tsx +++ b/components/home/now-minting/NowMintingStatsGrid.tsx @@ -25,12 +25,12 @@ export default function NowMintingStatsGrid({ const statusTone = manifoldClaim?.isFinalized ? "ended" : status; const editionSize = manifoldClaim - ? formatEditionSize(manifoldClaim) + ? formatEditionSize(manifoldClaim).replace(/\s*\/\s*/, "/") : undefined; const mintPrice = manifoldClaim ? formatClaimCost(manifoldClaim) : undefined; return ( -
+
- {label} + + {label} + {isLoading ? ( ) : ( - + {value} )} diff --git a/components/waves/drops/ContentSegmentComponent.tsx b/components/waves/drops/ContentSegmentComponent.tsx index e0290d519b..83f852e2f6 100644 --- a/components/waves/drops/ContentSegmentComponent.tsx +++ b/components/waves/drops/ContentSegmentComponent.tsx @@ -6,6 +6,49 @@ interface ContentSegmentComponentProps { readonly index: number; } +const URL_REGEX = /(https?:\/\/[^\s<]+[^\s<.,;:!?)\]"'])/g; + +function linkifyText(text: string, segmentIndex: number): React.ReactNode[] { + const parts: React.ReactNode[] = []; + let lastIndex = 0; + let match; + let partIndex = 0; + + const regex = new RegExp(URL_REGEX.source, "g"); + while ((match = regex.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push( + + {text.slice(lastIndex, match.index)} + + ); + } + parts.push( + e.stopPropagation()} + className="tw-text-primary-300 hover:tw-text-primary-400 tw-transition tw-duration-300" + > + {match[0]} + + ); + lastIndex = regex.lastIndex; + } + + if (lastIndex < text.length) { + parts.push( + + {text.slice(lastIndex)} + + ); + } + + return parts.length > 0 ? parts : [text]; +} + /** * Component to render a single segment of content (text or media) */ @@ -14,7 +57,7 @@ export default function ContentSegmentComponent({ index, }: ContentSegmentComponentProps) { if (segment.type === "text") { - return {segment.content}; + return {linkifyText(segment.content, index)}; } if (segment.mediaInfo) { diff --git a/components/waves/drops/WaveDropActionsAddReaction.tsx b/components/waves/drops/WaveDropActionsAddReaction.tsx index e5b1341c85..9cfbc990a1 100644 --- a/components/waves/drops/WaveDropActionsAddReaction.tsx +++ b/components/waves/drops/WaveDropActionsAddReaction.tsx @@ -92,6 +92,7 @@ const WaveDropActionsAddReaction: React.FC<{ max_rating: 0, reaction: null, boosted: false, + bookmarked: false, }; draft.context_profile_context = { diff --git a/components/waves/drops/WaveDropReactions.tsx b/components/waves/drops/WaveDropReactions.tsx index 6d36b8c711..3aa71af1ab 100644 --- a/components/waves/drops/WaveDropReactions.tsx +++ b/components/waves/drops/WaveDropReactions.tsx @@ -295,6 +295,7 @@ function WaveDropReaction({ max_rating: 0, reaction: null, boosted: false, + bookmarked: false, }; draft.context_profile_context = { diff --git a/generated/models/ApiDropContextProfileContext.ts b/generated/models/ApiDropContextProfileContext.ts index b32a0d564e..1c895d25dd 100644 --- a/generated/models/ApiDropContextProfileContext.ts +++ b/generated/models/ApiDropContextProfileContext.ts @@ -19,6 +19,7 @@ export class ApiDropContextProfileContext { 'max_rating': number; 'reaction': string | null; 'boosted': boolean; + 'bookmarked': boolean; static readonly discriminator: string | undefined = undefined; @@ -54,6 +55,12 @@ export class ApiDropContextProfileContext { "baseName": "boosted", "type": "boolean", "format": "" + }, + { + "name": "bookmarked", + "baseName": "bookmarked", + "type": "boolean", + "format": "" } ]; static getAttributeTypeMap() { diff --git a/hooks/drops/useDropBoostMutation.ts b/hooks/drops/useDropBoostMutation.ts index fc319aa75e..86a3a3f50e 100644 --- a/hooks/drops/useDropBoostMutation.ts +++ b/hooks/drops/useDropBoostMutation.ts @@ -63,6 +63,7 @@ export const useDropBoostMutation = (): UseDropBoostMutationReturn => { max_rating: 0, reaction: null, boosted: false, + bookmarked: false, }; draft.context_profile_context = { diff --git a/openapi.yaml b/openapi.yaml index e56994b5a4..d5e42c9b3f 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1050,6 +1050,95 @@ paths: description: successful operation "404": description: Drop not found + /drops/{dropId}/bookmark: + post: + tags: + - Drops + summary: Bookmark a drop + description: Requires the user to be authenticated + operationId: bookmarkDrop + parameters: + - name: dropId + in: path + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ApiDrop" + "400": + description: Invalid request + "404": + description: Drop not found + delete: + tags: + - Drops + summary: Remove bookmark from drop + description: Requires the user to be authenticated + operationId: unbookmarkDrop + parameters: + - name: dropId + in: path + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ApiDrop" + "404": + description: Drop not found + /drops-bookmarked/: + get: + tags: + - Drops + summary: Get bookmarked drops for authenticated user + description: >- + Requires the user to be authenticated. Returns drops bookmarked by the + current user, sorted by bookmark time. + operationId: getBookmarkedDrops + parameters: + - name: wave_id + in: query + description: Filter by wave + required: false + schema: + type: string + - name: page + in: query + required: false + schema: + type: integer + format: int64 + minimum: 1 + - name: page_size + in: query + required: false + schema: + type: integer + format: int64 + minimum: 1 + maximum: 200 + - name: sort_direction + in: query + description: Default is DESC (newest bookmarks first) + required: false + schema: + $ref: "#/components/schemas/ApiPageSortDirection" + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ApiDropsPage" /drops/{dropId}: get: tags: @@ -4145,6 +4234,22 @@ paths: type: array items: $ref: "#/components/schemas/ApiWave" + /waves-overview/hot: + get: + tags: + - Waves + summary: Get hot waves overview. + description: Returns up to 25 public waves ranked by activity in the last 24 hours. + operationId: getHotWavesOverview + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ApiWave" /waves: get: tags: @@ -6853,6 +6958,7 @@ components: - max_rating - reaction - boosted + - bookmarked properties: rating: type: number @@ -6868,6 +6974,8 @@ components: nullable: true boosted: type: boolean + bookmarked: + type: boolean ApiDropMedia: type: object required: diff --git a/package-lock.json b/package-lock.json index def78b3550..2a7f552b75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,6 +89,7 @@ "react-dom": "^19.2.3", "react-error-boundary": "^6.0.0", "react-markdown": "^9.0.1", + "react-masonry-css": "^1.0.16", "react-redux": "^9.1.2", "react-scroll": "^1.9.0", "react-toastify": "^10.0.5", @@ -26914,6 +26915,15 @@ "react": ">=18" } }, + "node_modules/react-masonry-css": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/react-masonry-css/-/react-masonry-css-1.0.16.tgz", + "integrity": "sha512-KSW0hR2VQmltt/qAa3eXOctQDyOu7+ZBevtKgpNDSzT7k5LA/0XntNa9z9HKCdz3QlxmJHglTZ18e4sX4V8zZQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", diff --git a/package.json b/package.json index 17d489426a..54a26bb372 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "react-dom": "^19.2.3", "react-error-boundary": "^6.0.0", "react-markdown": "^9.0.1", + "react-masonry-css": "^1.0.16", "react-redux": "^9.1.2", "react-scroll": "^1.9.0", "react-toastify": "^10.0.5", diff --git a/scripts/worktree/wt-common.sh b/scripts/worktree/wt-common.sh index 6bae8a0944..4c8fc1506f 100644 --- a/scripts/worktree/wt-common.sh +++ b/scripts/worktree/wt-common.sh @@ -9,20 +9,27 @@ setup_vscode_settings() { local settings_file="$vscode_path/settings.json" # Keep existing color if present to avoid changing themes on sync runs. + local colors=("red" "orange" "yellow" "green" "blue" "purple" "pink" "teal") local existing_color="" if [[ -f "$settings_file" ]]; then existing_color=$(grep -m1 '"titleBar.activeBackground"' "$settings_file" 2>/dev/null | sed -E 's/.*"titleBar.activeBackground"\s*:\s*"([^"]+)".*/\1/') || true fi - - local colors=("red" "orange" "yellow" "green" "blue" "purple" "pink" "teal") - local color=${existing_color:-${colors[$RANDOM % ${#colors[@]}]}} + # Validate extracted color is one of our known colors + local valid_color="" + for c in "${colors[@]}"; do + if [[ "$existing_color" == "$c" ]]; then + valid_color="$existing_color" + break + fi + done + local color=${valid_color:-${colors[$RANDOM % ${#colors[@]}]}} mkdir -p "$vscode_path" - cat > "$settings_file" < "$settings_file" <<'EOF' { "workbench.colorCustomizations": { - "titleBar.activeBackground": "$color", - "titleBar.inactiveBackground": "$color" + "titleBar.activeBackground": "__COLOR__", + "titleBar.inactiveBackground": "__COLOR__" }, "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode", @@ -87,12 +94,13 @@ setup_vscode_settings() { // --- Tailwind IntelliSense (if using the extension) --- "tailwindCSS.experimental.classRegex": [ "['\"`]([^'\"`]*tw-[^'\"`]*)['\"`]", - ["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] , - ["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] , + ["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"], + ["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"], ["cva\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] ] } EOF + sed -i '' "s/__COLOR__/$color/g" "$settings_file" printf "%s" "$color" }