@burdenoff/microfe-coolandlovely 2026.706.1 → 2026.706.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coolandlovely/CoolAndLovelyRoot.js +62 -46
- package/dist/coolandlovely/CoolAndLovelyRoot.js.map +1 -1
- package/dist/coolandlovely/CoolAndLovelyRoutes.js +14 -6
- package/dist/coolandlovely/CoolAndLovelyRoutes.js.map +1 -1
- package/dist/coolandlovely/components/ActorName.js +29 -0
- package/dist/coolandlovely/components/ActorName.js.map +1 -0
- package/dist/coolandlovely/components/AddToLookbookButton.js +81 -0
- package/dist/coolandlovely/components/AddToLookbookButton.js.map +1 -0
- package/dist/coolandlovely/components/BrandCover.js +39 -0
- package/dist/coolandlovely/components/BrandCover.js.map +1 -0
- package/dist/coolandlovely/components/BrandCreatorAvatars.js +57 -0
- package/dist/coolandlovely/components/BrandCreatorAvatars.js.map +1 -0
- package/dist/coolandlovely/components/ColourSwatchRow.js +86 -0
- package/dist/coolandlovely/components/ColourSwatchRow.js.map +1 -0
- package/dist/coolandlovely/components/Comments.js +109 -0
- package/dist/coolandlovely/components/Comments.js.map +1 -0
- package/dist/coolandlovely/components/Field.js +22 -2
- package/dist/coolandlovely/components/Field.js.map +1 -1
- package/dist/coolandlovely/components/FileImage.js +17 -0
- package/dist/coolandlovely/components/FileImage.js.map +1 -0
- package/dist/coolandlovely/components/FollowButton.js +24 -0
- package/dist/coolandlovely/components/FollowButton.js.map +1 -0
- package/dist/coolandlovely/components/ImageUploadField.js +153 -0
- package/dist/coolandlovely/components/ImageUploadField.js.map +1 -0
- package/dist/coolandlovely/components/LookCard.js +77 -0
- package/dist/coolandlovely/components/LookCard.js.map +1 -0
- package/dist/coolandlovely/components/MetricsCard.js +1 -1
- package/dist/coolandlovely/components/MetricsCard.js.map +1 -1
- package/dist/coolandlovely/components/ShopItemCard.js +45 -0
- package/dist/coolandlovely/components/ShopItemCard.js.map +1 -0
- package/dist/coolandlovely/components/StepTracker.js +35 -0
- package/dist/coolandlovely/components/StepTracker.js.map +1 -0
- package/dist/coolandlovely/components/Thumbnail.js +25 -14
- package/dist/coolandlovely/components/Thumbnail.js.map +1 -1
- package/dist/coolandlovely/components/VisibilityChip.js +32 -0
- package/dist/coolandlovely/components/VisibilityChip.js.map +1 -0
- package/dist/coolandlovely/hooks/index.js +2 -0
- package/dist/coolandlovely/hooks/useComments.js +68 -0
- package/dist/coolandlovely/hooks/useComments.js.map +1 -0
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js +155 -60
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js.map +1 -1
- package/dist/coolandlovely/hooks/useFiles.js +111 -0
- package/dist/coolandlovely/hooks/useFiles.js.map +1 -0
- package/dist/coolandlovely/pages/HomePage.js +130 -133
- package/dist/coolandlovely/pages/HomePage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandDetailPage.js +261 -228
- package/dist/coolandlovely/pages/brands/BrandDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandFormPage.js +146 -104
- package/dist/coolandlovely/pages/brands/BrandFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandsListPage.js +13 -11
- package/dist/coolandlovely/pages/brands/BrandsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js +117 -95
- package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js +50 -43
- package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionDetailPage.js +134 -107
- package/dist/coolandlovely/pages/collections/CollectionDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionFormPage.js +117 -102
- package/dist/coolandlovely/pages/collections/CollectionFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionsListPage.js +59 -54
- package/dist/coolandlovely/pages/collections/CollectionsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/discover/DiscoverPage.js +223 -0
- package/dist/coolandlovely/pages/discover/DiscoverPage.js.map +1 -0
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js +176 -144
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemFormPage.js +144 -130
- package/dist/coolandlovely/pages/items/StyleItemFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemsListPage.js +73 -62
- package/dist/coolandlovely/pages/items/StyleItemsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js +215 -102
- package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js +115 -86
- package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookDetailPage.js +217 -155
- package/dist/coolandlovely/pages/looks/LookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookFormPage.js +147 -96
- package/dist/coolandlovely/pages/looks/LookFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LooksListPage.js +60 -95
- package/dist/coolandlovely/pages/looks/LooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js +376 -146
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js.map +1 -1
- package/dist/coolandlovely/pages/trends/TrendsPage.js +249 -0
- package/dist/coolandlovely/pages/trends/TrendsPage.js.map +1 -0
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js +136 -88
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js +75 -74
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrandDetailPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandDetailPage.tsx"],"sourcesContent":["/**\n * Brand Detail Page\n * @module coolandlovely/pages/brands\n *\n * Renders a single brand storefront (`cooBrand`) with its publish-lifecycle\n * transitions (`cooTransitionBrand`), collections and style items.\n */\n\nimport { useParams } from 'react-router-dom';\nimport { Store, Star, ExternalLink, Pencil, ArrowLeft, Shapes, Shirt, Loader2 } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { TagList } from '../../components/TagList';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrand, useTransitionBrand } from '../../hooks';\nimport { categoryIcon } from '../../utils/icons';\nimport { formatPrice, formatDate, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooBrandStatus } from '../../types';\n\n/** Allowed next states per the svc brand state machine (UX guidance only;\n * the service is the authority and rejects illegal jumps). */\nconst NEXT_STATES: Record<CooBrandStatus, CooBrandStatus[]> = {\n DRAFT: ['PENDING_REVIEW', 'ARCHIVED'],\n PENDING_REVIEW: ['PUBLISHED', 'DRAFT', 'ARCHIVED'],\n PUBLISHED: ['SUSPENDED', 'ARCHIVED'],\n SUSPENDED: ['PUBLISHED', 'ARCHIVED'],\n ARCHIVED: [],\n};\n\nexport function BrandDetailPage() {\n const { brandId } = useParams<{ brandId: string }>();\n const { navigateTo } = useCoolAndLovely();\n const query = useBrand(brandId);\n const transition = useTransitionBrand();\n\n if (query.isLoading) {\n return (\n <PageLayout title=\"Brand\">\n <LoadingView label=\"Loading brand…\" />\n </PageLayout>\n );\n }\n\n if (query.isError || !query.data) {\n return (\n <PageLayout title=\"Brand\">\n <ErrorView\n title=\"Brand not found\"\n message=\"This storefront could not be loaded or does not exist.\"\n onRetry={() => void query.refetch()}\n />\n </PageLayout>\n );\n }\n\n const brand = query.data;\n const collections = brand.collections ?? [];\n const items = brand.items ?? [];\n const nextStates = NEXT_STATES[brand.status];\n\n return (\n <PageLayout\n title={brand.name}\n description={`@${brand.handle}`}\n actions={\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/brands/${brand.id}/edit`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Pencil className=\"h-4 w-4\" /> Edit\n </button>\n </div>\n }\n >\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Main column */}\n <div className=\"lg:col-span-2\">\n <GlassCard treatment=\"glass\" className=\"overflow-hidden p-0\">\n <Thumbnail\n icon={<Store className=\"h-12 w-12\" />}\n seed={brand.id}\n className=\"h-44 w-full\"\n />\n <div className=\"p-6\">\n <div className=\"flex items-center gap-2\">\n <h2 className=\"text-xl font-semibold text-text-primary\">{brand.name}</h2>\n {brand.isFeatured && (\n <Star className=\"h-4 w-4 fill-status-warning-text text-status-warning-text\" />\n )}\n <StatusBadge status={brand.status} className=\"ml-auto\" />\n </div>\n {brand.tagline && <p className=\"mt-2 text-sm text-text-primary\">{brand.tagline}</p>}\n {brand.bio && <p className=\"mt-3 text-sm text-text-secondary\">{brand.bio}</p>}\n\n {brand.aesthetics.length > 0 && (\n <div className=\"mt-4\">\n <p className=\"mb-1.5 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Aesthetics\n </p>\n <TagList tags={brand.aesthetics.map(titleCase)} tone=\"accent\" max={12} />\n </div>\n )}\n {brand.tags.length > 0 && (\n <div className=\"mt-3\">\n <p className=\"mb-1.5 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Tags\n </p>\n <TagList tags={brand.tags} max={12} />\n </div>\n )}\n {brand.websiteUrl && (\n <a\n href={brand.websiteUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"mt-4 inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline\"\n >\n <ExternalLink className=\"h-4 w-4\" /> {brand.websiteUrl}\n </a>\n )}\n </div>\n </GlassCard>\n\n {/* Collections */}\n <section className=\"mt-6\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Shapes className=\"h-5 w-5\" /> Collections ({collections.length})\n </h3>\n </div>\n {collections.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No collections yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {collections.map((c) => (\n <button\n key={c.id}\n type=\"button\"\n onClick={() => navigateTo(`/collections/${c.id}`)}\n className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3 text-left transition-colors hover:bg-bg-sunken/50\"\n >\n <Thumbnail\n icon={<Shapes className=\"h-5 w-5\" />}\n seed={c.id}\n className=\"h-11 w-11 shrink-0\"\n />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{c.name}</p>\n <p className=\"text-xs text-text-secondary\">{titleCase(c.season)}</p>\n </div>\n <StatusBadge status={c.status} />\n </button>\n ))}\n </div>\n )}\n </section>\n\n {/* Items */}\n <section className=\"mt-6\">\n <h3 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Shirt className=\"h-5 w-5\" /> Style items ({items.length})\n </h3>\n {items.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No items yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3\">\n {items.map((item) => (\n <button\n key={item.id}\n type=\"button\"\n onClick={() => navigateTo(`/items/${item.id}`)}\n className=\"overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left transition-all hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <Thumbnail\n icon={categoryIcon(item.category, 24)}\n seed={item.id}\n className=\"h-24 w-full\"\n />\n <div className=\"p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{item.name}</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatPrice(item.price, item.currency)}\n </p>\n </div>\n </button>\n ))}\n </div>\n )}\n </section>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-4\">\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Lifecycle</h3>\n <div className=\"mb-3 flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">Current:</span>\n <StatusBadge status={brand.status} />\n </div>\n {nextStates.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n This brand is archived (terminal state).\n </p>\n ) : (\n <div className=\"flex flex-col gap-2\">\n {nextStates.map((next) => (\n <button\n key={next}\n type=\"button\"\n disabled={transition.isPending}\n onClick={() => transition.mutate({ id: brand.id, status: next })}\n className={cn(\n 'inline-flex items-center justify-center gap-2 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50'\n )}\n >\n {transition.isPending && <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />}\n Move to {titleCase(next)}\n </button>\n ))}\n </div>\n )}\n {transition.isError && (\n <p className=\"mt-2 text-xs text-status-error-text\">\n {transition.error instanceof Error ? transition.error.message : 'Transition failed'}\n </p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Details</h3>\n <dl className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Handle</dt>\n <dd className=\"text-text-primary\">@{brand.handle}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Featured</dt>\n <dd className=\"text-text-primary\">{brand.isFeatured ? 'Yes' : 'No'}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Created</dt>\n <dd className=\"text-text-primary\">\n {formatDate(brand.createdAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Updated</dt>\n <dd className=\"text-text-primary\">\n {formatDate(brand.updatedAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n </dl>\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BrandDetailPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandDetailPage.tsx"],"sourcesContent":["/**\n * Brand Detail Page\n * @module coolandlovely/pages/brands\n *\n * Renders a single brand storefront (`cooBrand`) with its publish-lifecycle\n * transitions (`cooTransitionBrand`), collections and style items.\n */\n\nimport { useParams } from 'react-router-dom';\nimport { Store, ExternalLink, Pencil, ArrowLeft, Shapes, Shirt, Loader2, Plus } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { BrandCover } from '../../components/BrandCover';\nimport { TagList } from '../../components/TagList';\nimport { Comments } from '../../components/Comments';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrand, useTransitionBrand, useUpdateBrand } from '../../hooks';\nimport { categoryIcon } from '../../utils/icons';\nimport { formatPrice, formatDate, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooBrandStatus } from '../../types';\n\n/** Allowed next states per the svc brand state machine (UX guidance only;\n * the service is the authority and rejects illegal jumps). */\nconst NEXT_STATES: Record<CooBrandStatus, CooBrandStatus[]> = {\n DRAFT: ['PENDING_REVIEW', 'ARCHIVED'],\n PENDING_REVIEW: ['PUBLISHED', 'DRAFT', 'ARCHIVED'],\n PUBLISHED: ['SUSPENDED', 'ARCHIVED'],\n SUSPENDED: ['PUBLISHED', 'ARCHIVED'],\n ARCHIVED: [],\n};\n\nexport function BrandDetailPage() {\n const { brandId } = useParams<{ brandId: string }>();\n const { navigateTo } = useCoolAndLovely();\n const query = useBrand(brandId);\n const transition = useTransitionBrand();\n const updateBrand = useUpdateBrand();\n\n if (query.isLoading) {\n return (\n <PageLayout title=\"Brand\">\n <LoadingView label=\"Loading brand…\" />\n </PageLayout>\n );\n }\n\n if (query.isError || !query.data) {\n return (\n <PageLayout title=\"Brand\">\n <ErrorView\n title=\"Brand not found\"\n message=\"This storefront could not be loaded or does not exist.\"\n onRetry={() => void query.refetch()}\n />\n </PageLayout>\n );\n }\n\n const brand = query.data;\n const collections = brand.collections ?? [];\n const items = brand.items ?? [];\n const nextStates = NEXT_STATES[brand.status];\n // Only render a website link for safe http(s) URLs (block javascript:/data: sinks).\n const safeWebsiteUrl =\n brand.websiteUrl && /^https?:\\/\\//i.test(brand.websiteUrl) ? brand.websiteUrl : undefined;\n\n return (\n <PageLayout title={brand.name} description={`@${brand.handle}`}>\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Main column */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* Storefront hero — cover + logo + identity overlay, floating actions */}\n <BrandCover\n coverFileId={brand.cover?.id}\n logoFileId={brand.logo?.id}\n name={brand.name}\n seed={brand.id}\n className=\"overflow-hidden rounded-xl border border-border-seam shadow-sm\"\n overlay={\n <>\n <div className=\"absolute right-3 top-3 flex flex-wrap items-center justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-bg-surface/80 px-3 py-2 text-sm font-medium text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/brands/${brand.id}/edit`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text shadow-sm transition-colors hover:bg-action-primary-bg/90\"\n >\n <Pencil className=\"h-4 w-4\" /> Edit\n </button>\n </div>\n <div className=\"absolute inset-x-0 bottom-0 flex items-end gap-3 p-4 sm:gap-4 sm:p-5\">\n <Thumbnail\n icon={<Store className=\"h-7 w-7\" />}\n seed={brand.id}\n fileId={brand.logo?.id}\n alt={`${brand.name} logo`}\n className=\"h-16 w-16 shrink-0 rounded-full shadow-md ring-2 ring-bg-surface\"\n />\n <div className=\"min-w-0 pb-1\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-0.5\">\n <h2 className=\"truncate text-xl font-semibold text-text-primary sm:text-2xl\">\n {brand.name}\n </h2>\n <span className=\"text-sm text-text-secondary\">@{brand.handle}</span>\n </div>\n {brand.tagline && (\n <p className=\"mt-1 line-clamp-2 text-sm text-text-primary\">{brand.tagline}</p>\n )}\n </div>\n </div>\n </>\n }\n />\n\n {/* Storefront meta — aesthetics, status, bio, tags, website */}\n <section className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n {brand.aesthetics.length > 0 ? (\n <TagList tags={brand.aesthetics.map(titleCase)} tone=\"accent\" max={12} />\n ) : (\n <span />\n )}\n <StatusBadge status={brand.status} />\n </div>\n {brand.bio && <p className=\"text-sm text-text-secondary\">{brand.bio}</p>}\n {brand.tags.length > 0 && <TagList tags={brand.tags} max={12} />}\n {safeWebsiteUrl ? (\n <a\n href={safeWebsiteUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex max-w-full items-center gap-1.5 text-sm font-medium text-action-primary-bg hover:underline\"\n >\n <ExternalLink className=\"h-4 w-4 shrink-0\" />{' '}\n <span className=\"break-all\">{safeWebsiteUrl}</span>\n </a>\n ) : brand.websiteUrl ? (\n <p className=\"break-all text-sm text-text-secondary\">{brand.websiteUrl}</p>\n ) : null}\n </section>\n\n {/* Collections */}\n <section className=\"mt-6\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h3 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Shapes className=\"h-5 w-5\" /> Collections ({collections.length})\n </h3>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/collections/new?brandId=${brand.id}`)}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Plus className=\"h-4 w-4\" /> Add collection\n </button>\n </div>\n {collections.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No collections yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {collections.map((c) => (\n <button\n key={c.id}\n type=\"button\"\n onClick={() => navigateTo(`/collections/${c.id}`)}\n className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3 text-left transition-colors hover:bg-bg-sunken/50\"\n >\n <Thumbnail\n icon={<Shapes className=\"h-5 w-5\" />}\n seed={c.id}\n fileId={c.cover?.id}\n alt={c.name}\n className=\"h-11 w-11 shrink-0\"\n />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{c.name}</p>\n <p className=\"text-xs text-text-secondary\">{titleCase(c.season)}</p>\n </div>\n <StatusBadge status={c.status} />\n </button>\n ))}\n </div>\n )}\n </section>\n\n {/* Items */}\n <section className=\"mt-6\">\n <h3 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Shirt className=\"h-5 w-5\" /> Style items ({items.length})\n </h3>\n {items.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No items yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3\">\n {items.map((item) => (\n <button\n key={item.id}\n type=\"button\"\n onClick={() => navigateTo(`/items/${item.id}`)}\n className=\"overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left transition-all hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <Thumbnail\n icon={categoryIcon(item.category, 24)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"h-24 w-full\"\n />\n <div className=\"p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{item.name}</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatPrice(item.price, item.currency)}\n </p>\n </div>\n </button>\n ))}\n </div>\n )}\n </section>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-4\">\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Lifecycle</h3>\n <div className=\"mb-3 flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">Current:</span>\n <StatusBadge status={brand.status} />\n </div>\n {nextStates.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n This brand is archived (terminal state).\n </p>\n ) : (\n <div className=\"flex flex-col gap-2\">\n {nextStates.map((next) => (\n <button\n key={next}\n type=\"button\"\n disabled={transition.isPending}\n onClick={() => transition.mutate({ id: brand.id, status: next })}\n className={cn(\n 'inline-flex items-center justify-center gap-2 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50'\n )}\n >\n {transition.isPending && <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />}\n Move to {titleCase(next)}\n </button>\n ))}\n </div>\n )}\n {transition.isError && (\n <p className=\"mt-2 text-xs text-status-error-text\">\n {transition.error instanceof Error ? transition.error.message : 'Transition failed'}\n </p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Details</h3>\n <dl className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Handle</dt>\n <dd className=\"text-text-primary\">@{brand.handle}</dd>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <dt className=\"text-text-secondary\">Featured</dt>\n <dd className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={brand.isFeatured}\n aria-label={brand.isFeatured ? 'Unfeature this brand' : 'Feature this brand'}\n disabled={updateBrand.isPending}\n onClick={() =>\n updateBrand.mutate({\n id: brand.id,\n input: { isFeatured: !brand.isFeatured },\n })\n }\n className={cn(\n 'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-60',\n brand.isFeatured ? 'bg-action-primary-bg' : 'bg-bg-sunken'\n )}\n >\n <span\n className={cn(\n 'inline-block h-4 w-4 rounded-full bg-bg-surface shadow transition-transform',\n brand.isFeatured ? 'translate-x-[1.125rem]' : 'translate-x-0.5'\n )}\n />\n </button>\n {updateBrand.isPending && (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin text-text-secondary\" />\n )}\n </dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Created</dt>\n <dd className=\"text-text-primary\">\n {formatDate(brand.createdAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Updated</dt>\n <dd className=\"text-text-primary\">\n {formatDate(brand.updatedAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n </dl>\n </GlassCard>\n\n {/* Reviews & feedback — moved into the sidebar to match the storefront layout */}\n <Comments entityType=\"CooBrand\" entityId={brand.id} className=\"mt-0\" />\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,183 +1,225 @@
|
|
|
1
1
|
import { useCoolAndLovely as e } from "../../context/CoolAndLovelyContext.js";
|
|
2
2
|
import { PageLayout as t } from "../../components/PageLayout.js";
|
|
3
3
|
import { ErrorView as n, LoadingView as r } from "../../components/StateViews.js";
|
|
4
|
-
import {
|
|
4
|
+
import { COO_FILE_FOLDERS as i } from "../../hooks/useFiles.js";
|
|
5
|
+
import { useBrand as a, useCreateBrand as o, useUpdateBrand as s } from "../../hooks/useCoolAndLovelyApi.js";
|
|
6
|
+
import { parseCsv as c } from "../../utils/formatters.js";
|
|
5
7
|
import "../../hooks/index.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { useEffect as
|
|
9
|
-
import {
|
|
10
|
-
import { useParams as
|
|
11
|
-
import { jsx as
|
|
12
|
-
import { GlassCard as
|
|
8
|
+
import { TextAreaField as l, TextField as u, ToggleField as d } from "../../components/Field.js";
|
|
9
|
+
import { ImageUploadField as f } from "../../components/ImageUploadField.js";
|
|
10
|
+
import { useEffect as p, useState as m } from "react";
|
|
11
|
+
import { Loader2 as h, Save as g, Store as _ } from "lucide-react";
|
|
12
|
+
import { useParams as v } from "react-router-dom";
|
|
13
|
+
import { jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
14
|
+
import { GlassCard as x } from "@burdenoff/fe-libs/ui";
|
|
13
15
|
//#region src/coolandlovely/pages/brands/BrandFormPage.tsx
|
|
14
|
-
var
|
|
16
|
+
var S = {
|
|
15
17
|
name: "",
|
|
16
18
|
handle: "",
|
|
17
19
|
tagline: "",
|
|
18
20
|
bio: "",
|
|
19
21
|
aesthetics: "",
|
|
20
22
|
tags: "",
|
|
21
|
-
websiteUrl: ""
|
|
23
|
+
websiteUrl: "",
|
|
24
|
+
logoFileId: "",
|
|
25
|
+
coverFileId: "",
|
|
26
|
+
isFeatured: !1
|
|
22
27
|
};
|
|
23
|
-
function
|
|
24
|
-
let { brandId:
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
let e =
|
|
28
|
-
|
|
28
|
+
function C() {
|
|
29
|
+
let { brandId: C } = v(), w = !!C, { navigateTo: T } = e(), E = a(w ? C : void 0), D = o(), O = s(), [k, A] = m(S), [j, M] = m({});
|
|
30
|
+
p(() => {
|
|
31
|
+
if (w && E.data) {
|
|
32
|
+
let e = E.data;
|
|
33
|
+
A({
|
|
29
34
|
name: e.name,
|
|
30
35
|
handle: e.handle,
|
|
31
36
|
tagline: e.tagline ?? "",
|
|
32
37
|
bio: e.bio ?? "",
|
|
33
38
|
aesthetics: e.aesthetics.join(", "),
|
|
34
39
|
tags: e.tags.join(", "),
|
|
35
|
-
websiteUrl: e.websiteUrl ?? ""
|
|
40
|
+
websiteUrl: e.websiteUrl ?? "",
|
|
41
|
+
logoFileId: e.logo?.id ?? "",
|
|
42
|
+
coverFileId: e.cover?.id ?? "",
|
|
43
|
+
isFeatured: e.isFeatured
|
|
36
44
|
});
|
|
37
45
|
}
|
|
38
|
-
}, [
|
|
39
|
-
let
|
|
46
|
+
}, [w, E.data]);
|
|
47
|
+
let N = (e, t) => A((n) => ({
|
|
40
48
|
...n,
|
|
41
49
|
[e]: t
|
|
42
|
-
})),
|
|
50
|
+
})), P = w ? O : D, F = () => {
|
|
43
51
|
let e = {};
|
|
44
|
-
return
|
|
52
|
+
return k.name.trim() || (e.name = "Name is required"), !w && !k.handle.trim() && (e.handle = "Handle is required"), M(e), Object.keys(e).length === 0;
|
|
45
53
|
};
|
|
46
|
-
return
|
|
54
|
+
return w && E.isLoading ? /* @__PURE__ */ y(t, {
|
|
47
55
|
title: "Edit brand",
|
|
48
|
-
children: /* @__PURE__ */
|
|
49
|
-
}) :
|
|
56
|
+
children: /* @__PURE__ */ y(r, { label: "Loading brand…" })
|
|
57
|
+
}) : w && (E.isError || !E.data) ? /* @__PURE__ */ y(t, {
|
|
50
58
|
title: "Edit brand",
|
|
51
|
-
children: /* @__PURE__ */
|
|
59
|
+
children: /* @__PURE__ */ y(n, {
|
|
52
60
|
title: "Brand not found",
|
|
53
|
-
onRetry: () => void
|
|
61
|
+
onRetry: () => void E.refetch()
|
|
54
62
|
})
|
|
55
|
-
}) : /* @__PURE__ */
|
|
56
|
-
title:
|
|
57
|
-
description:
|
|
58
|
-
|
|
59
|
-
type: "button",
|
|
60
|
-
onClick: () => C(S && x ? `/brands/${x}` : "/brands"),
|
|
61
|
-
className: "inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken",
|
|
62
|
-
children: [/* @__PURE__ */ _(f, { className: "h-4 w-4" }), " Cancel"]
|
|
63
|
-
}),
|
|
64
|
-
children: /* @__PURE__ */ _("form", {
|
|
63
|
+
}) : /* @__PURE__ */ y(t, {
|
|
64
|
+
title: w ? "Edit brand" : "New brand",
|
|
65
|
+
description: w ? "Update this storefront" : "Launch a new storefront on the platform",
|
|
66
|
+
children: /* @__PURE__ */ y("form", {
|
|
65
67
|
onSubmit: (e) => {
|
|
66
|
-
e.preventDefault(),
|
|
67
|
-
id:
|
|
68
|
+
e.preventDefault(), F() && (w && C ? O.mutate({
|
|
69
|
+
id: C,
|
|
68
70
|
input: {
|
|
69
|
-
name:
|
|
70
|
-
tagline:
|
|
71
|
-
bio:
|
|
72
|
-
aesthetics:
|
|
73
|
-
tags:
|
|
74
|
-
websiteUrl:
|
|
71
|
+
name: k.name.trim(),
|
|
72
|
+
tagline: k.tagline.trim() || void 0,
|
|
73
|
+
bio: k.bio.trim() || void 0,
|
|
74
|
+
aesthetics: c(k.aesthetics),
|
|
75
|
+
tags: c(k.tags),
|
|
76
|
+
websiteUrl: k.websiteUrl.trim() || void 0,
|
|
77
|
+
logoFileId: k.logoFileId || void 0,
|
|
78
|
+
coverFileId: k.coverFileId || void 0,
|
|
79
|
+
isFeatured: k.isFeatured
|
|
75
80
|
}
|
|
76
|
-
}, { onSuccess: () =>
|
|
77
|
-
name:
|
|
78
|
-
handle:
|
|
79
|
-
tagline:
|
|
80
|
-
bio:
|
|
81
|
-
aesthetics:
|
|
82
|
-
tags:
|
|
83
|
-
websiteUrl:
|
|
84
|
-
|
|
81
|
+
}, { onSuccess: () => T(`/brands/${C}`) }) : D.mutate({
|
|
82
|
+
name: k.name.trim(),
|
|
83
|
+
handle: k.handle.trim(),
|
|
84
|
+
tagline: k.tagline.trim() || void 0,
|
|
85
|
+
bio: k.bio.trim() || void 0,
|
|
86
|
+
aesthetics: c(k.aesthetics),
|
|
87
|
+
tags: c(k.tags),
|
|
88
|
+
websiteUrl: k.websiteUrl.trim() || void 0,
|
|
89
|
+
logoFileId: k.logoFileId || void 0,
|
|
90
|
+
coverFileId: k.coverFileId || void 0
|
|
91
|
+
}, { onSuccess: (e) => {
|
|
92
|
+
k.isFeatured ? O.mutate({
|
|
93
|
+
id: e.id,
|
|
94
|
+
input: { isFeatured: !0 }
|
|
95
|
+
}, { onSettled: () => T(`/brands/${e.id}`) }) : T(`/brands/${e.id}`);
|
|
96
|
+
} }));
|
|
85
97
|
},
|
|
86
98
|
className: "mx-auto max-w-2xl",
|
|
87
|
-
children: /* @__PURE__ */
|
|
99
|
+
children: /* @__PURE__ */ b(x, {
|
|
88
100
|
treatment: "plain",
|
|
89
101
|
className: "p-6",
|
|
90
102
|
children: [
|
|
91
|
-
/* @__PURE__ */
|
|
103
|
+
/* @__PURE__ */ b("div", {
|
|
92
104
|
className: "mb-6 flex items-center gap-3",
|
|
93
|
-
children: [/* @__PURE__ */
|
|
105
|
+
children: [/* @__PURE__ */ y("div", {
|
|
94
106
|
className: "flex h-11 w-11 items-center justify-center rounded-lg bg-accent-purple-subtle text-text-primary",
|
|
95
|
-
children: /* @__PURE__ */ _
|
|
96
|
-
}), /* @__PURE__ */
|
|
107
|
+
children: /* @__PURE__ */ y(_, { className: "h-5 w-5" })
|
|
108
|
+
}), /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("h2", {
|
|
97
109
|
className: "text-base font-semibold text-text-primary",
|
|
98
110
|
children: "Storefront details"
|
|
99
|
-
}), /* @__PURE__ */
|
|
111
|
+
}), /* @__PURE__ */ y("p", {
|
|
100
112
|
className: "text-xs text-text-secondary",
|
|
101
113
|
children: "Name your storefront and tell shoppers what your brand stands for."
|
|
102
114
|
})] })]
|
|
103
115
|
}),
|
|
104
|
-
/* @__PURE__ */
|
|
116
|
+
/* @__PURE__ */ b("div", {
|
|
105
117
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
106
|
-
children: [/* @__PURE__ */
|
|
118
|
+
children: [/* @__PURE__ */ y(u, {
|
|
107
119
|
id: "name",
|
|
108
120
|
label: "Brand name",
|
|
109
|
-
value:
|
|
110
|
-
onChange: (e) =>
|
|
121
|
+
value: k.name,
|
|
122
|
+
onChange: (e) => N("name", e),
|
|
111
123
|
placeholder: "Aurora Atelier",
|
|
112
124
|
required: !0,
|
|
113
|
-
error:
|
|
114
|
-
}), /* @__PURE__ */
|
|
125
|
+
error: j.name
|
|
126
|
+
}), /* @__PURE__ */ y(u, {
|
|
115
127
|
id: "handle",
|
|
116
128
|
label: "Handle",
|
|
117
|
-
value:
|
|
118
|
-
onChange: (e) =>
|
|
129
|
+
value: k.handle,
|
|
130
|
+
onChange: (e) => N("handle", e),
|
|
119
131
|
placeholder: "aurora-atelier",
|
|
120
|
-
required: !
|
|
121
|
-
hint:
|
|
122
|
-
error:
|
|
132
|
+
required: !w,
|
|
133
|
+
hint: w ? "Handle cannot be changed after creation" : "Unique per workspace",
|
|
134
|
+
error: j.handle
|
|
123
135
|
})]
|
|
124
136
|
}),
|
|
125
|
-
/* @__PURE__ */
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
/* @__PURE__ */ b("div", {
|
|
138
|
+
className: "mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
139
|
+
children: [/* @__PURE__ */ y(u, {
|
|
140
|
+
id: "tagline",
|
|
141
|
+
label: "Tagline",
|
|
142
|
+
value: k.tagline,
|
|
143
|
+
onChange: (e) => N("tagline", e),
|
|
144
|
+
placeholder: "Quiet luxury, loud ideas"
|
|
145
|
+
}), /* @__PURE__ */ y(l, {
|
|
146
|
+
id: "bio",
|
|
147
|
+
label: "Bio",
|
|
148
|
+
value: k.bio,
|
|
149
|
+
onChange: (e) => N("bio", e),
|
|
150
|
+
placeholder: "Tell shoppers what your brand stands for…",
|
|
151
|
+
rows: 4
|
|
152
|
+
})]
|
|
141
153
|
}),
|
|
142
|
-
/* @__PURE__ */
|
|
154
|
+
/* @__PURE__ */ b("div", {
|
|
143
155
|
className: "mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
144
|
-
children: [/* @__PURE__ */
|
|
156
|
+
children: [/* @__PURE__ */ y(u, {
|
|
145
157
|
id: "aesthetics",
|
|
146
158
|
label: "Aesthetics",
|
|
147
|
-
value:
|
|
148
|
-
onChange: (e) =>
|
|
159
|
+
value: k.aesthetics,
|
|
160
|
+
onChange: (e) => N("aesthetics", e),
|
|
149
161
|
placeholder: "minimal, Y2K, coastal",
|
|
150
162
|
hint: "Comma-separated descriptors"
|
|
151
|
-
}), /* @__PURE__ */
|
|
163
|
+
}), /* @__PURE__ */ y(u, {
|
|
152
164
|
id: "tags",
|
|
153
165
|
label: "Tags",
|
|
154
|
-
value:
|
|
155
|
-
onChange: (e) =>
|
|
166
|
+
value: k.tags,
|
|
167
|
+
onChange: (e) => N("tags", e),
|
|
156
168
|
placeholder: "sustainable, handmade",
|
|
157
169
|
hint: "Comma-separated"
|
|
158
170
|
})]
|
|
159
171
|
}),
|
|
160
|
-
/* @__PURE__ */
|
|
172
|
+
/* @__PURE__ */ y(u, {
|
|
161
173
|
id: "websiteUrl",
|
|
162
174
|
label: "Website",
|
|
163
175
|
type: "url",
|
|
164
|
-
value:
|
|
165
|
-
onChange: (e) =>
|
|
176
|
+
value: k.websiteUrl,
|
|
177
|
+
onChange: (e) => N("websiteUrl", e),
|
|
166
178
|
placeholder: "https://example.com",
|
|
167
179
|
className: "mt-4"
|
|
168
180
|
}),
|
|
169
|
-
|
|
181
|
+
/* @__PURE__ */ b("div", {
|
|
182
|
+
className: "mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
183
|
+
children: [/* @__PURE__ */ y(f, {
|
|
184
|
+
label: "Logo",
|
|
185
|
+
value: k.logoFileId,
|
|
186
|
+
onChange: (e) => N("logoFileId", e),
|
|
187
|
+
folder: i.brands,
|
|
188
|
+
hint: "Square storefront logo"
|
|
189
|
+
}), /* @__PURE__ */ y(f, {
|
|
190
|
+
label: "Cover",
|
|
191
|
+
aspect: "wide",
|
|
192
|
+
value: k.coverFileId,
|
|
193
|
+
onChange: (e) => N("coverFileId", e),
|
|
194
|
+
folder: i.brands,
|
|
195
|
+
hint: "Wide storefront banner"
|
|
196
|
+
})]
|
|
197
|
+
}),
|
|
198
|
+
/* @__PURE__ */ y(d, {
|
|
199
|
+
id: "isFeatured",
|
|
200
|
+
label: "Feature this brand",
|
|
201
|
+
checked: k.isFeatured,
|
|
202
|
+
onChange: (e) => N("isFeatured", e),
|
|
203
|
+
hint: "Featured brands surface in the Featured filter and carry a star badge.",
|
|
204
|
+
className: "mt-4"
|
|
205
|
+
}),
|
|
206
|
+
P.isError && /* @__PURE__ */ y("p", {
|
|
170
207
|
className: "mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
|
|
171
|
-
children:
|
|
208
|
+
children: P.error instanceof Error ? P.error.message : "Something went wrong"
|
|
172
209
|
}),
|
|
173
|
-
/* @__PURE__ */
|
|
210
|
+
/* @__PURE__ */ b("div", {
|
|
174
211
|
className: "mt-6 flex justify-end gap-2",
|
|
175
|
-
children: /* @__PURE__ */
|
|
212
|
+
children: [/* @__PURE__ */ y("button", {
|
|
213
|
+
type: "button",
|
|
214
|
+
onClick: () => T(w && C ? `/brands/${C}` : "/brands"),
|
|
215
|
+
className: "inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken",
|
|
216
|
+
children: "Cancel"
|
|
217
|
+
}), /* @__PURE__ */ b("button", {
|
|
176
218
|
type: "submit",
|
|
177
|
-
disabled:
|
|
219
|
+
disabled: P.isPending,
|
|
178
220
|
className: "inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50",
|
|
179
|
-
children: [
|
|
180
|
-
})
|
|
221
|
+
children: [P.isPending ? /* @__PURE__ */ y(h, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ y(g, { className: "h-4 w-4" }), w ? "Save changes" : "Create brand"]
|
|
222
|
+
})]
|
|
181
223
|
})
|
|
182
224
|
]
|
|
183
225
|
})
|
|
@@ -185,6 +227,6 @@ function x() {
|
|
|
185
227
|
});
|
|
186
228
|
}
|
|
187
229
|
//#endregion
|
|
188
|
-
export {
|
|
230
|
+
export { C as BrandFormPage };
|
|
189
231
|
|
|
190
232
|
//# sourceMappingURL=BrandFormPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrandFormPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandFormPage.tsx"],"sourcesContent":["/**\n * Brand Create / Edit Page\n * @module coolandlovely/pages/brands\n *\n * Drives `cooCreateBrand` (new) and `cooUpdateBrand` (edit). The route param\n * `brandId` distinguishes the two modes.\n */\n\nimport { useEffect, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, Loader2, Save, Store } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { TextField, TextAreaField } from '../../components/Field';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrand, useCreateBrand, useUpdateBrand } from '../../hooks';\nimport { parseCsv } from '../../utils/formatters';\n\ninterface FormState {\n name: string;\n handle: string;\n tagline: string;\n bio: string;\n aesthetics: string;\n tags: string;\n websiteUrl: string;\n}\n\nconst EMPTY: FormState = {\n name: '',\n handle: '',\n tagline: '',\n bio: '',\n aesthetics: '',\n tags: '',\n websiteUrl: '',\n};\n\nexport function BrandFormPage() {\n const { brandId } = useParams<{ brandId: string }>();\n const isEdit = Boolean(brandId);\n const { navigateTo } = useCoolAndLovely();\n\n const existing = useBrand(isEdit ? brandId : undefined);\n const createBrand = useCreateBrand();\n const updateBrand = useUpdateBrand();\n\n const [form, setForm] = useState<FormState>(EMPTY);\n const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});\n\n useEffect(() => {\n if (isEdit && existing.data) {\n const b = existing.data;\n setForm({\n name: b.name,\n handle: b.handle,\n tagline: b.tagline ?? '',\n bio: b.bio ?? '',\n aesthetics: b.aesthetics.join(', '),\n tags: b.tags.join(', '),\n websiteUrl: b.websiteUrl ?? '',\n });\n }\n }, [isEdit, existing.data]);\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((f) => ({ ...f, [key]: value }));\n\n const mutation = isEdit ? updateBrand : createBrand;\n\n const validate = (): boolean => {\n const next: Partial<Record<keyof FormState, string>> = {};\n if (!form.name.trim()) next.name = 'Name is required';\n if (!isEdit && !form.handle.trim()) next.handle = 'Handle is required';\n setErrors(next);\n return Object.keys(next).length === 0;\n };\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (!validate()) return;\n\n if (isEdit && brandId) {\n updateBrand.mutate(\n {\n id: brandId,\n input: {\n name: form.name.trim(),\n tagline: form.tagline.trim() || undefined,\n bio: form.bio.trim() || undefined,\n aesthetics: parseCsv(form.aesthetics),\n tags: parseCsv(form.tags),\n websiteUrl: form.websiteUrl.trim() || undefined,\n },\n },\n { onSuccess: () => navigateTo(`/brands/${brandId}`) }\n );\n } else {\n createBrand.mutate(\n {\n name: form.name.trim(),\n handle: form.handle.trim(),\n tagline: form.tagline.trim() || undefined,\n bio: form.bio.trim() || undefined,\n aesthetics: parseCsv(form.aesthetics),\n tags: parseCsv(form.tags),\n websiteUrl: form.websiteUrl.trim() || undefined,\n },\n { onSuccess: (brand) => navigateTo(`/brands/${brand.id}`) }\n );\n }\n };\n\n if (isEdit && existing.isLoading) {\n return (\n <PageLayout title=\"Edit brand\">\n <LoadingView label=\"Loading brand…\" />\n </PageLayout>\n );\n }\n\n if (isEdit && (existing.isError || !existing.data)) {\n return (\n <PageLayout title=\"Edit brand\">\n <ErrorView title=\"Brand not found\" onRetry={() => void existing.refetch()} />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={isEdit ? 'Edit brand' : 'New brand'}\n description={isEdit ? 'Update this storefront' : 'Launch a new storefront on the platform'}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && brandId ? `/brands/${brandId}` : '/brands')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-accent-purple-subtle text-text-primary\">\n <Store className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Storefront details</h2>\n <p className=\"text-xs text-text-secondary\">\n Name your storefront and tell shoppers what your brand stands for.\n </p>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"name\"\n label=\"Brand name\"\n value={form.name}\n onChange={(v) => setField('name', v)}\n placeholder=\"Aurora Atelier\"\n required\n error={errors.name}\n />\n <TextField\n id=\"handle\"\n label=\"Handle\"\n value={form.handle}\n onChange={(v) => setField('handle', v)}\n placeholder=\"aurora-atelier\"\n required={!isEdit}\n hint={isEdit ? 'Handle cannot be changed after creation' : 'Unique per workspace'}\n error={errors.handle}\n />\n </div>\n\n <TextField\n id=\"tagline\"\n label=\"Tagline\"\n value={form.tagline}\n onChange={(v) => setField('tagline', v)}\n placeholder=\"Quiet luxury, loud ideas\"\n className=\"mt-4\"\n />\n\n <TextAreaField\n id=\"bio\"\n label=\"Bio\"\n value={form.bio}\n onChange={(v) => setField('bio', v)}\n placeholder=\"Tell shoppers what your brand stands for…\"\n rows={4}\n className=\"mt-4\"\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"aesthetics\"\n label=\"Aesthetics\"\n value={form.aesthetics}\n onChange={(v) => setField('aesthetics', v)}\n placeholder=\"minimal, Y2K, coastal\"\n hint=\"Comma-separated descriptors\"\n />\n <TextField\n id=\"tags\"\n label=\"Tags\"\n value={form.tags}\n onChange={(v) => setField('tags', v)}\n placeholder=\"sustainable, handmade\"\n hint=\"Comma-separated\"\n />\n </div>\n\n <TextField\n id=\"websiteUrl\"\n label=\"Website\"\n type=\"url\"\n value={form.websiteUrl}\n onChange={(v) => setField('websiteUrl', v)}\n placeholder=\"https://example.com\"\n className=\"mt-4\"\n />\n\n {mutation.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {mutation.error instanceof Error ? mutation.error.message : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"submit\"\n disabled={mutation.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {mutation.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {isEdit ? 'Save changes' : 'Create brand'}\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BrandFormPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandFormPage.tsx"],"sourcesContent":["/**\n * Brand Create / Edit Page\n * @module coolandlovely/pages/brands\n *\n * Drives `cooCreateBrand` (new) and `cooUpdateBrand` (edit). The route param\n * `brandId` distinguishes the two modes.\n */\n\nimport { useEffect, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { Loader2, Save, Store } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { TextField, TextAreaField, ToggleField } from '../../components/Field';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrand, useCreateBrand, useUpdateBrand } from '../../hooks';\nimport { COO_FILE_FOLDERS } from '../../hooks/useFiles';\nimport { parseCsv } from '../../utils/formatters';\n\ninterface FormState {\n name: string;\n handle: string;\n tagline: string;\n bio: string;\n aesthetics: string;\n tags: string;\n websiteUrl: string;\n logoFileId: string;\n coverFileId: string;\n isFeatured: boolean;\n}\n\nconst EMPTY: FormState = {\n name: '',\n handle: '',\n tagline: '',\n bio: '',\n aesthetics: '',\n tags: '',\n websiteUrl: '',\n logoFileId: '',\n coverFileId: '',\n isFeatured: false,\n};\n\nexport function BrandFormPage() {\n const { brandId } = useParams<{ brandId: string }>();\n const isEdit = Boolean(brandId);\n const { navigateTo } = useCoolAndLovely();\n\n const existing = useBrand(isEdit ? brandId : undefined);\n const createBrand = useCreateBrand();\n const updateBrand = useUpdateBrand();\n\n const [form, setForm] = useState<FormState>(EMPTY);\n const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});\n\n useEffect(() => {\n if (isEdit && existing.data) {\n const b = existing.data;\n setForm({\n name: b.name,\n handle: b.handle,\n tagline: b.tagline ?? '',\n bio: b.bio ?? '',\n aesthetics: b.aesthetics.join(', '),\n tags: b.tags.join(', '),\n websiteUrl: b.websiteUrl ?? '',\n logoFileId: b.logo?.id ?? '',\n coverFileId: b.cover?.id ?? '',\n isFeatured: b.isFeatured,\n });\n }\n }, [isEdit, existing.data]);\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((f) => ({ ...f, [key]: value }));\n\n const mutation = isEdit ? updateBrand : createBrand;\n\n const validate = (): boolean => {\n const next: Partial<Record<keyof FormState, string>> = {};\n if (!form.name.trim()) next.name = 'Name is required';\n if (!isEdit && !form.handle.trim()) next.handle = 'Handle is required';\n setErrors(next);\n return Object.keys(next).length === 0;\n };\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (!validate()) return;\n\n if (isEdit && brandId) {\n updateBrand.mutate(\n {\n id: brandId,\n input: {\n name: form.name.trim(),\n tagline: form.tagline.trim() || undefined,\n bio: form.bio.trim() || undefined,\n aesthetics: parseCsv(form.aesthetics),\n tags: parseCsv(form.tags),\n websiteUrl: form.websiteUrl.trim() || undefined,\n logoFileId: form.logoFileId || undefined,\n coverFileId: form.coverFileId || undefined,\n isFeatured: form.isFeatured,\n },\n },\n { onSuccess: () => navigateTo(`/brands/${brandId}`) }\n );\n } else {\n createBrand.mutate(\n {\n name: form.name.trim(),\n handle: form.handle.trim(),\n tagline: form.tagline.trim() || undefined,\n bio: form.bio.trim() || undefined,\n aesthetics: parseCsv(form.aesthetics),\n tags: parseCsv(form.tags),\n websiteUrl: form.websiteUrl.trim() || undefined,\n logoFileId: form.logoFileId || undefined,\n coverFileId: form.coverFileId || undefined,\n },\n {\n onSuccess: (brand) => {\n // `CooCreateBrandInput` has no `isFeatured`; if the creator toggled\n // Featured on, persist it with a follow-up `cooUpdateBrand` before\n // navigating (navigate regardless of that call's outcome).\n if (form.isFeatured) {\n updateBrand.mutate(\n { id: brand.id, input: { isFeatured: true } },\n { onSettled: () => navigateTo(`/brands/${brand.id}`) }\n );\n } else {\n navigateTo(`/brands/${brand.id}`);\n }\n },\n }\n );\n }\n };\n\n if (isEdit && existing.isLoading) {\n return (\n <PageLayout title=\"Edit brand\">\n <LoadingView label=\"Loading brand…\" />\n </PageLayout>\n );\n }\n\n if (isEdit && (existing.isError || !existing.data)) {\n return (\n <PageLayout title=\"Edit brand\">\n <ErrorView title=\"Brand not found\" onRetry={() => void existing.refetch()} />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={isEdit ? 'Edit brand' : 'New brand'}\n description={isEdit ? 'Update this storefront' : 'Launch a new storefront on the platform'}\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-accent-purple-subtle text-text-primary\">\n <Store className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Storefront details</h2>\n <p className=\"text-xs text-text-secondary\">\n Name your storefront and tell shoppers what your brand stands for.\n </p>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"name\"\n label=\"Brand name\"\n value={form.name}\n onChange={(v) => setField('name', v)}\n placeholder=\"Aurora Atelier\"\n required\n error={errors.name}\n />\n <TextField\n id=\"handle\"\n label=\"Handle\"\n value={form.handle}\n onChange={(v) => setField('handle', v)}\n placeholder=\"aurora-atelier\"\n required={!isEdit}\n hint={isEdit ? 'Handle cannot be changed after creation' : 'Unique per workspace'}\n error={errors.handle}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"tagline\"\n label=\"Tagline\"\n value={form.tagline}\n onChange={(v) => setField('tagline', v)}\n placeholder=\"Quiet luxury, loud ideas\"\n />\n <TextAreaField\n id=\"bio\"\n label=\"Bio\"\n value={form.bio}\n onChange={(v) => setField('bio', v)}\n placeholder=\"Tell shoppers what your brand stands for…\"\n rows={4}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"aesthetics\"\n label=\"Aesthetics\"\n value={form.aesthetics}\n onChange={(v) => setField('aesthetics', v)}\n placeholder=\"minimal, Y2K, coastal\"\n hint=\"Comma-separated descriptors\"\n />\n <TextField\n id=\"tags\"\n label=\"Tags\"\n value={form.tags}\n onChange={(v) => setField('tags', v)}\n placeholder=\"sustainable, handmade\"\n hint=\"Comma-separated\"\n />\n </div>\n\n <TextField\n id=\"websiteUrl\"\n label=\"Website\"\n type=\"url\"\n value={form.websiteUrl}\n onChange={(v) => setField('websiteUrl', v)}\n placeholder=\"https://example.com\"\n className=\"mt-4\"\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <ImageUploadField\n label=\"Logo\"\n value={form.logoFileId}\n onChange={(id) => setField('logoFileId', id)}\n folder={COO_FILE_FOLDERS.brands}\n hint=\"Square storefront logo\"\n />\n <ImageUploadField\n label=\"Cover\"\n aspect=\"wide\"\n value={form.coverFileId}\n onChange={(id) => setField('coverFileId', id)}\n folder={COO_FILE_FOLDERS.brands}\n hint=\"Wide storefront banner\"\n />\n </div>\n\n <ToggleField\n id=\"isFeatured\"\n label=\"Feature this brand\"\n checked={form.isFeatured}\n onChange={(v) => setField('isFeatured', v)}\n hint=\"Featured brands surface in the Featured filter and carry a star badge.\"\n className=\"mt-4\"\n />\n\n {mutation.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {mutation.error instanceof Error ? mutation.error.message : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && brandId ? `/brands/${brandId}` : '/brands')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={mutation.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {mutation.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {isEdit ? 'Save changes' : 'Create brand'}\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -3,10 +3,10 @@ import { cn as t } from "../../utils/cn.js";
|
|
|
3
3
|
import { PageLayout as n } from "../../components/PageLayout.js";
|
|
4
4
|
import { StatusBadge as r } from "../../components/StatusBadge.js";
|
|
5
5
|
import { ErrorView as i, LoadingView as a } from "../../components/StateViews.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { useBrands as o } from "../../hooks/useCoolAndLovelyApi.js";
|
|
7
|
+
import { titleCase as s } from "../../utils/formatters.js";
|
|
8
8
|
import "../../hooks/index.js";
|
|
9
|
-
import {
|
|
9
|
+
import { BrandCover as c } from "../../components/BrandCover.js";
|
|
10
10
|
import { TagList as l } from "../../components/TagList.js";
|
|
11
11
|
import { EmptyState as u } from "../../components/EmptyState.js";
|
|
12
12
|
import { useState as d } from "react";
|
|
@@ -40,7 +40,7 @@ var b = 24, x = [
|
|
|
40
40
|
}
|
|
41
41
|
];
|
|
42
42
|
function S() {
|
|
43
|
-
let { navigateTo: S } = e(), [C, w] = d("ALL"), [T, E] = d(!1), [D, O] = d(0), k =
|
|
43
|
+
let { navigateTo: S } = e(), [C, w] = d("ALL"), [T, E] = d(!1), [D, O] = d(0), k = o({
|
|
44
44
|
status: C === "ALL" ? void 0 : C,
|
|
45
45
|
featured: T ? !0 : void 0,
|
|
46
46
|
pagination: {
|
|
@@ -94,16 +94,18 @@ function S() {
|
|
|
94
94
|
children: A.map((e) => /* @__PURE__ */ y("button", {
|
|
95
95
|
type: "button",
|
|
96
96
|
onClick: () => S(`/brands/${e.id}`),
|
|
97
|
-
className: "overflow-hidden rounded-
|
|
98
|
-
children: [/* @__PURE__ */ v(
|
|
99
|
-
|
|
97
|
+
className: "group flex flex-col overflow-hidden rounded-xl border border-border-seam bg-bg-surface text-left shadow-sm transition-all hover:border-accent-foreground/20 hover:shadow-md",
|
|
98
|
+
children: [/* @__PURE__ */ v(c, {
|
|
99
|
+
coverFileId: e.cover?.id,
|
|
100
|
+
logoFileId: e.logo?.id,
|
|
101
|
+
name: e.name,
|
|
100
102
|
seed: e.id,
|
|
101
|
-
|
|
103
|
+
showLogo: !0
|
|
102
104
|
}), /* @__PURE__ */ y("div", {
|
|
103
|
-
className: "
|
|
105
|
+
className: "px-4 pb-4 pt-3",
|
|
104
106
|
children: [
|
|
105
107
|
/* @__PURE__ */ y("div", {
|
|
106
|
-
className: "flex items-start justify-between gap-2",
|
|
108
|
+
className: "flex min-h-[2.5rem] items-start justify-between gap-2 pl-20",
|
|
107
109
|
children: [/* @__PURE__ */ y("div", {
|
|
108
110
|
className: "min-w-0",
|
|
109
111
|
children: [/* @__PURE__ */ y("div", {
|
|
@@ -123,7 +125,7 @@ function S() {
|
|
|
123
125
|
children: e.tagline
|
|
124
126
|
}),
|
|
125
127
|
e.aesthetics.length > 0 && /* @__PURE__ */ v(l, {
|
|
126
|
-
tags: e.aesthetics.map(
|
|
128
|
+
tags: e.aesthetics.map(s),
|
|
127
129
|
max: 4,
|
|
128
130
|
className: "mt-3",
|
|
129
131
|
tone: "accent"
|