@burdenoff/microfe-coolandlovely 2026.1031.2 → 2026.1101.2

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.
Files changed (141) hide show
  1. package/dist/coolandlovely/CoolAndLovelyRoot.d.ts.map +1 -1
  2. package/dist/coolandlovely/CoolAndLovelyRoot.js +16 -4
  3. package/dist/coolandlovely/CoolAndLovelyRoot.js.map +1 -1
  4. package/dist/coolandlovely/CoolAndLovelyRoutes.d.ts.map +1 -1
  5. package/dist/coolandlovely/CoolAndLovelyRoutes.js +19 -15
  6. package/dist/coolandlovely/CoolAndLovelyRoutes.js.map +1 -1
  7. package/dist/coolandlovely/components/ActivityTrail.js +1 -1
  8. package/dist/coolandlovely/components/ActivityTrail.js.map +1 -1
  9. package/dist/coolandlovely/components/ActorName.js.map +1 -1
  10. package/dist/coolandlovely/components/AddToCartPanel.js.map +1 -1
  11. package/dist/coolandlovely/components/AddToLookbookButton.js.map +1 -1
  12. package/dist/coolandlovely/components/BrandCard.js.map +1 -1
  13. package/dist/coolandlovely/components/BrandCover.js.map +1 -1
  14. package/dist/coolandlovely/components/BrandCreatorAvatars.js.map +1 -1
  15. package/dist/coolandlovely/components/CollaborationFeeCard.js.map +1 -1
  16. package/dist/coolandlovely/components/CollectionCard.js.map +1 -1
  17. package/dist/coolandlovely/components/ColourSwatchRow.js.map +1 -1
  18. package/dist/coolandlovely/components/Comments.js.map +1 -1
  19. package/dist/coolandlovely/components/EmptyState.js.map +1 -1
  20. package/dist/coolandlovely/components/FeedbackWidget.js.map +1 -1
  21. package/dist/coolandlovely/components/Field.js.map +1 -1
  22. package/dist/coolandlovely/components/FileImage.js.map +1 -1
  23. package/dist/coolandlovely/components/FollowButton.js.map +1 -1
  24. package/dist/coolandlovely/components/FulfilmentTimeline.js.map +1 -1
  25. package/dist/coolandlovely/components/ImageUploadField.js +3 -1
  26. package/dist/coolandlovely/components/ImageUploadField.js.map +1 -1
  27. package/dist/coolandlovely/components/LookCard.js.map +1 -1
  28. package/dist/coolandlovely/components/LookbookCard.js.map +1 -1
  29. package/dist/coolandlovely/components/MetricsCard.js.map +1 -1
  30. package/dist/coolandlovely/components/PageLayout.js.map +1 -1
  31. package/dist/coolandlovely/components/PayForOrder.js.map +1 -1
  32. package/dist/coolandlovely/components/RefundPanel.js +1 -1
  33. package/dist/coolandlovely/components/RefundPanel.js.map +1 -1
  34. package/dist/coolandlovely/components/ReturnRequests.js +1 -1
  35. package/dist/coolandlovely/components/ReturnRequests.js.map +1 -1
  36. package/dist/coolandlovely/components/ShopItemCard.js.map +1 -1
  37. package/dist/coolandlovely/components/StateViews.js.map +1 -1
  38. package/dist/coolandlovely/components/StatusBadge.js +1 -1
  39. package/dist/coolandlovely/components/StatusBadge.js.map +1 -1
  40. package/dist/coolandlovely/components/StepTracker.js.map +1 -1
  41. package/dist/coolandlovely/components/StockEditor.js.map +1 -1
  42. package/dist/coolandlovely/components/StoreListingCard.js.map +1 -1
  43. package/dist/coolandlovely/components/TagList.js.map +1 -1
  44. package/dist/coolandlovely/components/Thumbnail.js +1 -1
  45. package/dist/coolandlovely/components/Thumbnail.js.map +1 -1
  46. package/dist/coolandlovely/components/VisibilityChip.js.map +1 -1
  47. package/dist/coolandlovely/components/WardrobeItemCard.js.map +1 -1
  48. package/dist/coolandlovely/components/index.js +2 -1
  49. package/dist/coolandlovely/context/CoolAndLovelyContext.js +2 -1
  50. package/dist/coolandlovely/context/CoolAndLovelyContext.js.map +1 -1
  51. package/dist/coolandlovely/context/index.js +2 -1
  52. package/dist/coolandlovely/hooks/index.js +4 -3
  53. package/dist/coolandlovely/hooks/useActivity.d.ts +1 -1
  54. package/dist/coolandlovely/hooks/useActivity.d.ts.map +1 -1
  55. package/dist/coolandlovely/hooks/useActivity.js.map +1 -1
  56. package/dist/coolandlovely/hooks/useAssistantEntity.js.map +1 -1
  57. package/dist/coolandlovely/hooks/useBrandOrdersApi.d.ts +4 -4
  58. package/dist/coolandlovely/hooks/useBrandOrdersApi.d.ts.map +1 -1
  59. package/dist/coolandlovely/hooks/useBrandOrdersApi.js.map +1 -1
  60. package/dist/coolandlovely/hooks/useCatalogImport.js.map +1 -1
  61. package/dist/coolandlovely/hooks/useComments.d.ts +1 -1
  62. package/dist/coolandlovely/hooks/useComments.d.ts.map +1 -1
  63. package/dist/coolandlovely/hooks/useComments.js.map +1 -1
  64. package/dist/coolandlovely/hooks/useCommerceApi.d.ts +7 -7
  65. package/dist/coolandlovely/hooks/useCommerceApi.d.ts.map +1 -1
  66. package/dist/coolandlovely/hooks/useCommerceApi.js.map +1 -1
  67. package/dist/coolandlovely/hooks/useCoolAndLovelyApi.d.ts +24 -24
  68. package/dist/coolandlovely/hooks/useCoolAndLovelyApi.d.ts.map +1 -1
  69. package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js.map +1 -1
  70. package/dist/coolandlovely/hooks/useEditorialApi.d.ts +95 -0
  71. package/dist/coolandlovely/hooks/useEditorialApi.d.ts.map +1 -0
  72. package/dist/coolandlovely/hooks/useEditorialApi.js +107 -0
  73. package/dist/coolandlovely/hooks/useEditorialApi.js.map +1 -0
  74. package/dist/coolandlovely/hooks/useFiles.js.map +1 -1
  75. package/dist/coolandlovely/hooks/useInventoryApi.d.ts +1 -1
  76. package/dist/coolandlovely/hooks/useInventoryApi.d.ts.map +1 -1
  77. package/dist/coolandlovely/hooks/useInventoryApi.js.map +1 -1
  78. package/dist/coolandlovely/hooks/useTr.js.map +1 -1
  79. package/dist/coolandlovely/index.js +6 -5
  80. package/dist/coolandlovely/pages/HomePage.js.map +1 -1
  81. package/dist/coolandlovely/pages/admin/AdminBrandsPage.js.map +1 -1
  82. package/dist/coolandlovely/pages/admin/AdminCollaborationsPage.js.map +1 -1
  83. package/dist/coolandlovely/pages/admin/AdminFeaturedItemsPage.js.map +1 -1
  84. package/dist/coolandlovely/pages/brands/BrandDetailPage.js.map +1 -1
  85. package/dist/coolandlovely/pages/brands/BrandFormPage.js.map +1 -1
  86. package/dist/coolandlovely/pages/brands/BrandsListPage.js.map +1 -1
  87. package/dist/coolandlovely/pages/brands/ImportCatalogPage.js.map +1 -1
  88. package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js.map +1 -1
  89. package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js +1 -1
  90. package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js.map +1 -1
  91. package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js.map +1 -1
  92. package/dist/coolandlovely/pages/collections/CollectionDetailPage.js.map +1 -1
  93. package/dist/coolandlovely/pages/collections/CollectionFormPage.js.map +1 -1
  94. package/dist/coolandlovely/pages/collections/CollectionsListPage.js.map +1 -1
  95. package/dist/coolandlovely/pages/commerce/BrandOrdersPage.js.map +1 -1
  96. package/dist/coolandlovely/pages/commerce/CartPage.js.map +1 -1
  97. package/dist/coolandlovely/pages/commerce/CreatorEarningsPage.js.map +1 -1
  98. package/dist/coolandlovely/pages/commerce/OrdersPage.js.map +1 -1
  99. package/dist/coolandlovely/pages/discover/DiscoverPage.js +1 -1
  100. package/dist/coolandlovely/pages/discover/DiscoverPage.js.map +1 -1
  101. package/dist/coolandlovely/pages/editorial/EditorialPage.d.ts +2 -0
  102. package/dist/coolandlovely/pages/editorial/EditorialPage.d.ts.map +1 -0
  103. package/dist/coolandlovely/pages/editorial/EditorialPage.js +486 -0
  104. package/dist/coolandlovely/pages/editorial/EditorialPage.js.map +1 -0
  105. package/dist/coolandlovely/pages/items/StoreListedPieceView.js.map +1 -1
  106. package/dist/coolandlovely/pages/items/StyleItemDetailPage.js.map +1 -1
  107. package/dist/coolandlovely/pages/items/StyleItemFormPage.js.map +1 -1
  108. package/dist/coolandlovely/pages/items/StyleItemsListPage.js.map +1 -1
  109. package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js.map +1 -1
  110. package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js.map +1 -1
  111. package/dist/coolandlovely/pages/looks/LookDetailPage.js.map +1 -1
  112. package/dist/coolandlovely/pages/looks/LookFormPage.js.map +1 -1
  113. package/dist/coolandlovely/pages/looks/LooksListPage.js.map +1 -1
  114. package/dist/coolandlovely/pages/profile/StyleProfilePage.js +1 -1
  115. package/dist/coolandlovely/pages/profile/StyleProfilePage.js.map +1 -1
  116. package/dist/coolandlovely/pages/trends/TrendsPage.js.map +1 -1
  117. package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js.map +1 -1
  118. package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js.map +1 -1
  119. package/dist/coolandlovely/store/coolandlovelyStore.js.map +1 -1
  120. package/dist/coolandlovely/store/index.js +2 -1
  121. package/dist/coolandlovely/types/commerce.d.ts.map +1 -1
  122. package/dist/coolandlovely/types/index.d.ts.map +1 -1
  123. package/dist/coolandlovely/utils/cn.js +1 -1
  124. package/dist/coolandlovely/utils/cn.js.map +1 -1
  125. package/dist/coolandlovely/utils/formatters.js.map +1 -1
  126. package/dist/coolandlovely/utils/icons.js.map +1 -1
  127. package/dist/coolandlovely/utils/index.js +2 -1
  128. package/dist/coolandlovely/utils/navigation.js.map +1 -1
  129. package/dist/coolandlovely/utils/paymentProviders.js +22 -22
  130. package/dist/coolandlovely/utils/paymentProviders.js.map +1 -1
  131. package/dist/coolandlovely/utils/sparkline.js +13 -15
  132. package/dist/coolandlovely/utils/sparkline.js.map +1 -1
  133. package/dist/node_modules/clsx/dist/clsx.js +1 -1
  134. package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
  135. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +225 -146
  136. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
  137. package/dist/services/createSandboxAssistantTransport.js.map +1 -1
  138. package/dist/stories/DiscoverPage.stories.d.ts.map +1 -1
  139. package/dist/utils/pageContext.js +1 -1
  140. package/dist/utils/pageContext.js.map +1 -1
  141. package/package.json +6 -4
@@ -1 +1 @@
1
- {"version":3,"file":"CollaborationFeeCard.js","names":[],"sources":["../../../src/coolandlovely/components/CollaborationFeeCard.tsx"],"sourcesContent":["/**\n * CollaborationFeeCard — the agreed compensation of a collaboration and its payment (BOFF-9637).\n * @module coolandlovely/components\n *\n * Publishing a collaboration with an agreed compensation makes the fee due from the brand. The\n * brand owner pays it here through the provider global-billing chose; the fee only shows as paid\n * once the backend has re-read billing's payment record, and the creator then finds it on their\n * earnings page. The creator sees whether the brand has paid.\n */\n\nimport { type FC, type ReactNode, useState } from 'react';\nimport { CheckCircle2, Loader2, RefreshCw, Wallet } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { useCoolAndLovely } from '../context/CoolAndLovelyContext';\nimport { useTr } from '../hooks';\nimport { usePayCollaborationFee, useSyncCollaborationPayment } from '../hooks/useCommerceApi';\nimport type { CooCollaboration, CooCollaborationPaymentCheckout } from '../types';\nimport { formatDate, formatPrice } from '../utils/formatters';\nimport { PayForOrder } from './PayForOrder';\nimport { StatusBadge } from './StatusBadge';\n\ninterface CollaborationFeeCardProps {\n collaboration: Pick<\n CooCollaboration,\n 'id' | 'title' | 'status' | 'creatorId' | 'compensation' | 'currency' | 'payout'\n >;\n}\n\nconst secondaryButton =\n 'inline-flex w-full 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';\nconst primaryButton =\n 'inline-flex w-full items-center justify-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2.5 text-sm font-semibold text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50';\n\nexport const CollaborationFeeCard: FC<CollaborationFeeCardProps> = ({ collaboration }) => {\n const tr = useTr();\n const { currentUser, navigateTo } = useCoolAndLovely();\n const pay = usePayCollaborationFee();\n const sync = useSyncCollaborationPayment();\n const [session, setSession] = useState<CooCollaborationPaymentCheckout | null>(null);\n\n const payout = collaboration.payout ?? null;\n const isCreator = !!currentUser?.id && currentUser.id === collaboration.creatorId;\n if (!payout && collaboration.compensation <= 0) return null;\n\n const settle = async (): Promise<void> => {\n const result = await sync.mutateAsync(collaboration.id);\n if (result.status === 'PAID') setSession(null);\n };\n\n let body: ReactNode;\n if (!payout) {\n body = (\n <p className=\"text-sm text-text-secondary\">\n {collaboration.status === 'PUBLISHED'\n ? tr(\n 'coolandlovely.collaborationFee.offPlatform',\n 'No on-platform fee is due for this collaboration.'\n )\n : tr(\n 'coolandlovely.collaborationFee.dueOnPublish',\n 'The agreed fee of {{amount}} becomes due from the brand when the collaboration is published.',\n { amount: formatPrice(collaboration.compensation, collaboration.currency) }\n )}\n </p>\n );\n } else if (payout.status === 'PAID') {\n body = (\n <div className=\"space-y-1 text-sm\">\n <p className=\"inline-flex items-center gap-1.5 font-medium text-status-success-text\">\n <CheckCircle2 className=\"h-4 w-4\" />\n {tr('coolandlovely.collaborationFee.paidOn', 'Paid on {{date}}', {\n date: payout.paidAt\n ? formatDate(payout.paidAt, { hour: undefined, minute: undefined })\n : '—',\n })}\n </p>\n {isCreator && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/earnings')}\n className=\"text-sm font-medium text-text-secondary hover:underline\"\n >\n {tr('coolandlovely.collaborationFee.seeEarnings', 'See it in your earnings')}\n </button>\n )}\n </div>\n );\n } else if (isCreator) {\n body = (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.collaborationFee.awaitingBrand',\n 'Waiting for the brand to pay your fee. It shows up in your earnings as soon as they do.'\n )}\n </p>\n );\n } else {\n body = (\n <div className=\"space-y-3\" data-testid=\"collaboration-fee-pay\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.collaborationFee.payHint',\n 'This collaboration is published. Pay the agreed fee to the creator.'\n )}\n </p>\n {session ? (\n <PayForOrder\n checkout={session}\n onSettle={settle}\n description={tr(\n 'coolandlovely.collaborationFee.paymentLabel',\n 'Collaboration fee: {{title}}',\n { title: collaboration.title }\n )}\n unavailableMessage={tr(\n 'coolandlovely.collaborationFee.providerUnavailable',\n 'Online payment is not available for this fee right now. Try again later.'\n )}\n />\n ) : (\n <button\n type=\"button\"\n className={primaryButton}\n disabled={pay.isPending}\n onClick={() => pay.mutate(collaboration.id, { onSuccess: setSession })}\n >\n {pay.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Wallet className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.collaborationFee.pay', 'Pay fee')}\n </button>\n )}\n {payout.paymentProvider && (\n <button\n type=\"button\"\n className={secondaryButton}\n disabled={sync.isPending}\n onClick={() => void settle()}\n >\n {sync.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <RefreshCw className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.collaborationFee.checkPayment', 'Check payment')}\n </button>\n )}\n </div>\n );\n }\n\n const error = pay.error ?? sync.error;\n return (\n <div data-testid=\"collaboration-fee-card\">\n <GlassCard treatment=\"plain\" className=\"space-y-3 p-5\">\n <div className=\"flex items-center justify-between gap-2\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {tr('coolandlovely.collaborationFee.title', 'Collaboration fee')}\n </h3>\n {payout && <StatusBadge status={payout.status} />}\n </div>\n <p className=\"text-2xl font-semibold tabular-nums text-text-primary\">\n {formatPrice(\n payout?.amount ?? collaboration.compensation,\n payout?.currency ?? collaboration.currency\n )}\n </p>\n {body}\n {error && <p className=\"text-sm text-status-error-text\">{error.message}</p>}\n </GlassCard>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"CollaborationFeeCard.js","names":[],"sources":["../../../src/coolandlovely/components/CollaborationFeeCard.tsx"],"sourcesContent":["/**\n * CollaborationFeeCard — the agreed compensation of a collaboration and its payment (BOFF-9637).\n * @module coolandlovely/components\n *\n * Publishing a collaboration with an agreed compensation makes the fee due from the brand. The\n * brand owner pays it here through the provider global-billing chose; the fee only shows as paid\n * once the backend has re-read billing's payment record, and the creator then finds it on their\n * earnings page. The creator sees whether the brand has paid.\n */\n\nimport { type FC, type ReactNode, useState } from 'react';\nimport { CheckCircle2, Loader2, RefreshCw, Wallet } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { useCoolAndLovely } from '../context/CoolAndLovelyContext';\nimport { useTr } from '../hooks';\nimport { usePayCollaborationFee, useSyncCollaborationPayment } from '../hooks/useCommerceApi';\nimport type { CooCollaboration, CooCollaborationPaymentCheckout } from '../types';\nimport { formatDate, formatPrice } from '../utils/formatters';\nimport { PayForOrder } from './PayForOrder';\nimport { StatusBadge } from './StatusBadge';\n\ninterface CollaborationFeeCardProps {\n collaboration: Pick<\n CooCollaboration,\n 'id' | 'title' | 'status' | 'creatorId' | 'compensation' | 'currency' | 'payout'\n >;\n}\n\nconst secondaryButton =\n 'inline-flex w-full 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';\nconst primaryButton =\n 'inline-flex w-full items-center justify-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2.5 text-sm font-semibold text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50';\n\nexport const CollaborationFeeCard: FC<CollaborationFeeCardProps> = ({ collaboration }) => {\n const tr = useTr();\n const { currentUser, navigateTo } = useCoolAndLovely();\n const pay = usePayCollaborationFee();\n const sync = useSyncCollaborationPayment();\n const [session, setSession] = useState<CooCollaborationPaymentCheckout | null>(null);\n\n const payout = collaboration.payout ?? null;\n const isCreator = !!currentUser?.id && currentUser.id === collaboration.creatorId;\n if (!payout && collaboration.compensation <= 0) return null;\n\n const settle = async (): Promise<void> => {\n const result = await sync.mutateAsync(collaboration.id);\n if (result.status === 'PAID') setSession(null);\n };\n\n let body: ReactNode;\n if (!payout) {\n body = (\n <p className=\"text-sm text-text-secondary\">\n {collaboration.status === 'PUBLISHED'\n ? tr(\n 'coolandlovely.collaborationFee.offPlatform',\n 'No on-platform fee is due for this collaboration.'\n )\n : tr(\n 'coolandlovely.collaborationFee.dueOnPublish',\n 'The agreed fee of {{amount}} becomes due from the brand when the collaboration is published.',\n { amount: formatPrice(collaboration.compensation, collaboration.currency) }\n )}\n </p>\n );\n } else if (payout.status === 'PAID') {\n body = (\n <div className=\"space-y-1 text-sm\">\n <p className=\"inline-flex items-center gap-1.5 font-medium text-status-success-text\">\n <CheckCircle2 className=\"h-4 w-4\" />\n {tr('coolandlovely.collaborationFee.paidOn', 'Paid on {{date}}', {\n date: payout.paidAt\n ? formatDate(payout.paidAt, { hour: undefined, minute: undefined })\n : '—',\n })}\n </p>\n {isCreator && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/earnings')}\n className=\"text-sm font-medium text-text-secondary hover:underline\"\n >\n {tr('coolandlovely.collaborationFee.seeEarnings', 'See it in your earnings')}\n </button>\n )}\n </div>\n );\n } else if (isCreator) {\n body = (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.collaborationFee.awaitingBrand',\n 'Waiting for the brand to pay your fee. It shows up in your earnings as soon as they do.'\n )}\n </p>\n );\n } else {\n body = (\n <div className=\"space-y-3\" data-testid=\"collaboration-fee-pay\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.collaborationFee.payHint',\n 'This collaboration is published. Pay the agreed fee to the creator.'\n )}\n </p>\n {session ? (\n <PayForOrder\n checkout={session}\n onSettle={settle}\n description={tr(\n 'coolandlovely.collaborationFee.paymentLabel',\n 'Collaboration fee: {{title}}',\n { title: collaboration.title }\n )}\n unavailableMessage={tr(\n 'coolandlovely.collaborationFee.providerUnavailable',\n 'Online payment is not available for this fee right now. Try again later.'\n )}\n />\n ) : (\n <button\n type=\"button\"\n className={primaryButton}\n disabled={pay.isPending}\n onClick={() => pay.mutate(collaboration.id, { onSuccess: setSession })}\n >\n {pay.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Wallet className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.collaborationFee.pay', 'Pay fee')}\n </button>\n )}\n {payout.paymentProvider && (\n <button\n type=\"button\"\n className={secondaryButton}\n disabled={sync.isPending}\n onClick={() => void settle()}\n >\n {sync.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <RefreshCw className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.collaborationFee.checkPayment', 'Check payment')}\n </button>\n )}\n </div>\n );\n }\n\n const error = pay.error ?? sync.error;\n return (\n <div data-testid=\"collaboration-fee-card\">\n <GlassCard treatment=\"plain\" className=\"space-y-3 p-5\">\n <div className=\"flex items-center justify-between gap-2\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {tr('coolandlovely.collaborationFee.title', 'Collaboration fee')}\n </h3>\n {payout && <StatusBadge status={payout.status} />}\n </div>\n <p className=\"text-2xl font-semibold tabular-nums text-text-primary\">\n {formatPrice(\n payout?.amount ?? collaboration.compensation,\n payout?.currency ?? collaboration.currency\n )}\n </p>\n {body}\n {error && <p className=\"text-sm text-status-error-text\">{error.message}</p>}\n </GlassCard>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CollectionCard.js","names":[],"sources":["../../../src/coolandlovely/components/CollectionCard.tsx"],"sourcesContent":["/**\n * CollectionCard — a seasonal collection drop card for list/grids.\n * @module coolandlovely/components\n *\n * Cover image with a season pill, status badge, brand line, publish date and\n * tags. Extracted from CollectionsListPage for isolated rendering/tests.\n */\n\nimport type { FC } from 'react';\nimport { Shapes } from 'lucide-react';\nimport { Thumbnail } from './Thumbnail';\nimport { StatusBadge } from './StatusBadge';\nimport { TagList } from './TagList';\nimport { titleCase, formatDate } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { CooCollection } from '../types';\n\ninterface CollectionCardProps {\n collection: CooCollection;\n onClick?: () => void;\n className?: string;\n}\n\nexport const CollectionCard: FC<CollectionCardProps> = ({ collection, onClick, className }) => {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={`Open collection ${collection.name}`}\n className={cn(\n 'group 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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n className\n )}\n >\n <div className=\"relative\">\n <Thumbnail\n icon={<Shapes className=\"h-10 w-10\" />}\n seed={collection.id}\n fileId={collection.cover?.id}\n alt={collection.name}\n className=\"aspect-[16/10] w-full\"\n overlay={\n <>\n <div className=\"pointer-events-none absolute inset-0 bg-gradient-to-t from-bg-canvas via-bg-canvas/55 to-transparent\" />\n <div className=\"absolute inset-x-0 bottom-0 flex items-end justify-between gap-2 p-4\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-lg font-semibold text-text-primary\">\n {collection.name}\n </h3>\n {collection.brand?.name && (\n <p className=\"truncate text-xs text-text-secondary\">{collection.brand.name}</p>\n )}\n </div>\n <StatusBadge status={collection.status} className=\"shrink-0 shadow-sm\" />\n </div>\n </>\n }\n />\n <span className=\"absolute right-3 top-3 rounded-full border border-border-seam bg-bg-surface/85 px-2.5 py-0.5 text-xs font-medium text-text-primary backdrop-blur-sm\">\n {titleCase(collection.season)}\n </span>\n </div>\n {(collection.publishAt || collection.tags.length > 0) && (\n <div className=\"p-4\">\n {collection.publishAt && (\n <p className=\"text-xs text-text-secondary\">\n Publishes {formatDate(collection.publishAt, { hour: undefined, minute: undefined })}\n </p>\n )}\n {collection.tags.length > 0 && (\n <TagList\n tags={collection.tags}\n max={4}\n className={collection.publishAt ? 'mt-2' : undefined}\n />\n )}\n </div>\n )}\n </button>\n );\n};\n"],"mappings":";;;;;;;;AAuBA,IAAa,KAA2C,EAAE,eAAY,YAAS,mBAE3E,kBAAC,UAAD;CACE,MAAK;CACI;CACT,cAAY,mBAAmB,EAAW;CAC1C,WAAW,EACT,8PACA,EACD;WAPH,CASE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD;GACE,MAAM,kBAAC,GAAD,EAAQ,WAAU,aAAc,CAAA;GACtC,MAAM,EAAW;GACjB,QAAQ,EAAW,OAAO;GAC1B,KAAK,EAAW;GAChB,WAAU;GACV,SACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD,EAAK,WAAU,wGAAyG,CAAA,EACxH,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAW;MACT,CAAA,EACJ,EAAW,OAAO,QACjB,kBAAC,KAAD;MAAG,WAAU;gBAAwC,EAAW,MAAM;MAAS,CAAA,CAE7E;QACN,kBAAC,GAAD;KAAa,QAAQ,EAAW;KAAQ,WAAU;KAAuB,CAAA,CACrE;MACL,EAAA,CAAA;GAEL,CAAA,EACF,kBAAC,QAAD;GAAM,WAAU;aACb,EAAU,EAAW,OAAO;GACxB,CAAA,CACH;MACJ,EAAW,aAAa,EAAW,KAAK,SAAS,MACjD,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAW,aACV,kBAAC,KAAD;GAAG,WAAU;aAAb,CAA2C,cAC9B,EAAW,EAAW,WAAW;IAAE,MAAM,KAAA;IAAW,QAAQ,KAAA;IAAW,CAAC,CACjF;MAEL,EAAW,KAAK,SAAS,KACxB,kBAAC,GAAD;GACE,MAAM,EAAW;GACjB,KAAK;GACL,WAAW,EAAW,YAAY,SAAS,KAAA;GAC3C,CAAA,CAEA;IAED"}
1
+ {"version":3,"file":"CollectionCard.js","names":[],"sources":["../../../src/coolandlovely/components/CollectionCard.tsx"],"sourcesContent":["/**\n * CollectionCard — a seasonal collection drop card for list/grids.\n * @module coolandlovely/components\n *\n * Cover image with a season pill, status badge, brand line, publish date and\n * tags. Extracted from CollectionsListPage for isolated rendering/tests.\n */\n\nimport type { FC } from 'react';\nimport { Shapes } from 'lucide-react';\nimport { Thumbnail } from './Thumbnail';\nimport { StatusBadge } from './StatusBadge';\nimport { TagList } from './TagList';\nimport { titleCase, formatDate } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { CooCollection } from '../types';\n\ninterface CollectionCardProps {\n collection: CooCollection;\n onClick?: () => void;\n className?: string;\n}\n\nexport const CollectionCard: FC<CollectionCardProps> = ({ collection, onClick, className }) => {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={`Open collection ${collection.name}`}\n className={cn(\n 'group 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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n className\n )}\n >\n <div className=\"relative\">\n <Thumbnail\n icon={<Shapes className=\"h-10 w-10\" />}\n seed={collection.id}\n fileId={collection.cover?.id}\n alt={collection.name}\n className=\"aspect-[16/10] w-full\"\n overlay={\n <>\n <div className=\"pointer-events-none absolute inset-0 bg-gradient-to-t from-bg-canvas via-bg-canvas/55 to-transparent\" />\n <div className=\"absolute inset-x-0 bottom-0 flex items-end justify-between gap-2 p-4\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-lg font-semibold text-text-primary\">\n {collection.name}\n </h3>\n {collection.brand?.name && (\n <p className=\"truncate text-xs text-text-secondary\">{collection.brand.name}</p>\n )}\n </div>\n <StatusBadge status={collection.status} className=\"shrink-0 shadow-sm\" />\n </div>\n </>\n }\n />\n <span className=\"absolute right-3 top-3 rounded-full border border-border-seam bg-bg-surface/85 px-2.5 py-0.5 text-xs font-medium text-text-primary backdrop-blur-sm\">\n {titleCase(collection.season)}\n </span>\n </div>\n {(collection.publishAt || collection.tags.length > 0) && (\n <div className=\"p-4\">\n {collection.publishAt && (\n <p className=\"text-xs text-text-secondary\">\n Publishes {formatDate(collection.publishAt, { hour: undefined, minute: undefined })}\n </p>\n )}\n {collection.tags.length > 0 && (\n <TagList\n tags={collection.tags}\n max={4}\n className={collection.publishAt ? 'mt-2' : undefined}\n />\n )}\n </div>\n )}\n </button>\n );\n};\n"],"mappings":";;;;;;;;AAuBA,IAAa,KAA2C,EAAE,eAAY,YAAS,mBAE3E,kBAAC,UAAD;CACE,MAAK;CACI;CACT,cAAY,mBAAmB,EAAW;CAC1C,WAAW,EACT,8PACA,CACF;WAPF,CASE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD;GACE,MAAM,kBAAC,GAAD,EAAQ,WAAU,YAAa,CAAA;GACrC,MAAM,EAAW;GACjB,QAAQ,EAAW,OAAO;GAC1B,KAAK,EAAW;GAChB,WAAU;GACV,SACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD,EAAK,WAAU,uGAAwG,CAAA,GACvH,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAW;KACV,CAAA,GACH,EAAW,OAAO,QACjB,kBAAC,KAAD;MAAG,WAAU;gBAAwC,EAAW,MAAM;KAAQ,CAAA,CAE7E;QACL,kBAAC,GAAD;KAAa,QAAQ,EAAW;KAAQ,WAAU;IAAsB,CAAA,CACrE;KACL,EAAA,CAAA;EAEL,CAAA,GACD,kBAAC,QAAD;GAAM,WAAU;aACb,EAAU,EAAW,MAAM;EACxB,CAAA,CACH;MACH,EAAW,aAAa,EAAW,KAAK,SAAS,MACjD,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAW,aACV,kBAAC,KAAD;GAAG,WAAU;aAAb,CAA2C,cAC9B,EAAW,EAAW,WAAW;IAAE,MAAM,KAAA;IAAW,QAAQ,KAAA;GAAU,CAAC,CACjF;MAEJ,EAAW,KAAK,SAAS,KACxB,kBAAC,GAAD;GACE,MAAM,EAAW;GACjB,KAAK;GACL,WAAW,EAAW,YAAY,SAAS,KAAA;EAC5C,CAAA,CAEA;GAED"}
@@ -1 +1 @@
1
- {"version":3,"file":"ColourSwatchRow.js","names":[],"sources":["../../../src/coolandlovely/components/ColourSwatchRow.tsx"],"sourcesContent":["/**\n * ColourSwatchRow — render a set of colour names as small swatch dots.\n * @module coolandlovely/components\n *\n * CoolAndLovely style items / wardrobe / palettes carry free-text colour names\n * (e.g. \"navy\", \"rust\", \"camel\"). We map the common ones to a representative\n * CSS colour so the catalogue reads like a real fashion product rather than a\n * row of grey chips. The swatch fill is CONTENT data (the colour the garment\n * actually is), not a theme decision, so an inline `backgroundColor` is used for\n * mapped names; unknown names fall back to a neutral token-styled dot that keeps\n * its label in a tooltip. Everything structural/text uses semantic tokens.\n */\n\nimport type { CSSProperties, FC } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface ColourSwatchRowProps {\n colours: string[];\n /** Max swatches to show before collapsing into a \"+N\" chip. */\n max?: number;\n className?: string;\n}\n\n/**\n * Common fashion colour names → a representative CSS colour. Keys are matched\n * case-insensitively; multi-word names (e.g. \"dusty rose\", \"forest green\") fall\n * back to a keyword scan so \"… green\" still resolves to green.\n */\nconst COLOUR_MAP: Record<string, string> = {\n black: '#111111',\n white: '#ffffff',\n ivory: '#fffff0',\n cream: '#f5f1e3',\n ecru: '#c2b280',\n beige: '#f0e6d2',\n sand: '#d8c3a5',\n stone: '#a8a29e',\n taupe: '#b38b6d',\n grey: '#9ca3af',\n gray: '#9ca3af',\n charcoal: '#36454f',\n silver: '#c0c0c0',\n navy: '#1f2a44',\n blue: '#2563eb',\n cobalt: '#0047ab',\n denim: '#1560bd',\n indigo: '#4f46e5',\n teal: '#0d9488',\n turquoise: '#40e0d0',\n green: '#16a34a',\n olive: '#808000',\n sage: '#9caf88',\n mint: '#a7e8bd',\n emerald: '#10b981',\n khaki: '#c3b091',\n mustard: '#e1ad01',\n yellow: '#eab308',\n gold: '#d4af37',\n orange: '#ea580c',\n rust: '#b7410e',\n terracotta: '#e2725b',\n coral: '#ff7f50',\n peach: '#ffcba4',\n camel: '#c19a6b',\n tan: '#d2b48c',\n brown: '#7c4a2d',\n chocolate: '#5c3a21',\n wine: '#722f37',\n burgundy: '#800020',\n maroon: '#800000',\n red: '#dc2626',\n pink: '#f9a8d4',\n blush: '#e5a4a4',\n rose: '#f43f5e',\n lavender: '#b57edc',\n lilac: '#c8a2c8',\n purple: '#9333ea',\n plum: '#8e4585',\n};\n\n/** Ordered keyword list for the \"contains\" fallback (longer keys first). */\nconst COLOUR_KEYWORDS = Object.keys(COLOUR_MAP).sort((a, b) => b.length - a.length);\n\nfunction resolveColour(name: string): string | null {\n const normalized = name.trim().toLowerCase();\n if (!normalized) return null;\n const direct = COLOUR_MAP[normalized];\n if (direct) return direct;\n const keyword = COLOUR_KEYWORDS.find((key) => normalized.includes(key));\n return keyword ? COLOUR_MAP[keyword] : null;\n}\n\nexport const ColourSwatchRow: FC<ColourSwatchRowProps> = ({ colours, max = 6, className }) => {\n const cleaned = colours.filter((c) => c && c.trim().length > 0);\n if (cleaned.length === 0) return null;\n\n const shown = cleaned.slice(0, max);\n const overflow = cleaned.length - shown.length;\n\n return (\n <div className={cn('flex flex-wrap items-center gap-1.5', className)}>\n {shown.map((colour, index) => {\n const css = resolveColour(colour);\n const style: CSSProperties | undefined = css ? { backgroundColor: css } : undefined;\n return (\n <span\n key={`${colour}-${index}`}\n title={colour}\n aria-label={colour}\n style={style}\n className={cn(\n 'inline-block h-4 w-4 rounded-full ring-1 ring-inset ring-border-seam',\n css ? undefined : 'bg-bg-sunken'\n )}\n />\n );\n })}\n {overflow > 0 ? (\n <span className=\"text-xs font-medium text-text-secondary tabular-nums\">+{overflow}</span>\n ) : null}\n </div>\n );\n};\n"],"mappings":";;;AA4BA,IAAM,IAAqC;CACzC,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,MAAM;CACN,OAAO;CACP,MAAM;CACN,OAAO;CACP,OAAO;CACP,MAAM;CACN,MAAM;CACN,UAAU;CACV,QAAQ;CACR,MAAM;CACN,MAAM;CACN,QAAQ;CACR,OAAO;CACP,QAAQ;CACR,MAAM;CACN,WAAW;CACX,OAAO;CACP,OAAO;CACP,MAAM;CACN,MAAM;CACN,SAAS;CACT,OAAO;CACP,SAAS;CACT,QAAQ;CACR,MAAM;CACN,QAAQ;CACR,MAAM;CACN,YAAY;CACZ,OAAO;CACP,OAAO;CACP,OAAO;CACP,KAAK;CACL,OAAO;CACP,WAAW;CACX,MAAM;CACN,UAAU;CACV,QAAQ;CACR,KAAK;CACL,MAAM;CACN,OAAO;CACP,MAAM;CACN,UAAU;CACV,OAAO;CACP,QAAQ;CACR,MAAM;CACP,EAGK,IAAkB,OAAO,KAAK,EAAW,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO;AAEnF,SAAS,EAAc,GAA6B;CAClD,IAAM,IAAa,EAAK,MAAM,CAAC,aAAa;AAC5C,KAAI,CAAC,EAAY,QAAO;CACxB,IAAM,IAAS,EAAW;AAC1B,KAAI,EAAQ,QAAO;CACnB,IAAM,IAAU,EAAgB,MAAM,MAAQ,EAAW,SAAS,EAAI,CAAC;AACvE,QAAO,IAAU,EAAW,KAAW;;AAGzC,IAAa,KAA6C,EAAE,YAAS,SAAM,GAAG,mBAAgB;CAC5F,IAAM,IAAU,EAAQ,QAAQ,MAAM,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE;AAC/D,KAAI,EAAQ,WAAW,EAAG,QAAO;CAEjC,IAAM,IAAQ,EAAQ,MAAM,GAAG,EAAI,EAC7B,IAAW,EAAQ,SAAS,EAAM;AAExC,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,uCAAuC,EAAU;YAApE,CACG,EAAM,KAAK,GAAQ,MAAU;GAC5B,IAAM,IAAM,EAAc,EAAO;AAEjC,UACE,kBAAC,QAAD;IAEE,OAAO;IACP,cAAY;IACL,OAN8B,IAAM,EAAE,iBAAiB,GAAK,GAAG,KAAA;IAOtE,WAAW,EACT,wEACA,IAAM,KAAA,IAAY,eACnB;IACD,EARK,GAAG,EAAO,GAAG,IAQlB;IAEJ,EACD,IAAW,IACV,kBAAC,QAAD;GAAM,WAAU;aAAhB,CAAuE,KAAE,EAAgB;OACvF,KACA"}
1
+ {"version":3,"file":"ColourSwatchRow.js","names":[],"sources":["../../../src/coolandlovely/components/ColourSwatchRow.tsx"],"sourcesContent":["/**\n * ColourSwatchRow — render a set of colour names as small swatch dots.\n * @module coolandlovely/components\n *\n * CoolAndLovely style items / wardrobe / palettes carry free-text colour names\n * (e.g. \"navy\", \"rust\", \"camel\"). We map the common ones to a representative\n * CSS colour so the catalogue reads like a real fashion product rather than a\n * row of grey chips. The swatch fill is CONTENT data (the colour the garment\n * actually is), not a theme decision, so an inline `backgroundColor` is used for\n * mapped names; unknown names fall back to a neutral token-styled dot that keeps\n * its label in a tooltip. Everything structural/text uses semantic tokens.\n */\n\nimport type { CSSProperties, FC } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface ColourSwatchRowProps {\n colours: string[];\n /** Max swatches to show before collapsing into a \"+N\" chip. */\n max?: number;\n className?: string;\n}\n\n/**\n * Common fashion colour names → a representative CSS colour. Keys are matched\n * case-insensitively; multi-word names (e.g. \"dusty rose\", \"forest green\") fall\n * back to a keyword scan so \"… green\" still resolves to green.\n */\nconst COLOUR_MAP: Record<string, string> = {\n black: '#111111',\n white: '#ffffff',\n ivory: '#fffff0',\n cream: '#f5f1e3',\n ecru: '#c2b280',\n beige: '#f0e6d2',\n sand: '#d8c3a5',\n stone: '#a8a29e',\n taupe: '#b38b6d',\n grey: '#9ca3af',\n gray: '#9ca3af',\n charcoal: '#36454f',\n silver: '#c0c0c0',\n navy: '#1f2a44',\n blue: '#2563eb',\n cobalt: '#0047ab',\n denim: '#1560bd',\n indigo: '#4f46e5',\n teal: '#0d9488',\n turquoise: '#40e0d0',\n green: '#16a34a',\n olive: '#808000',\n sage: '#9caf88',\n mint: '#a7e8bd',\n emerald: '#10b981',\n khaki: '#c3b091',\n mustard: '#e1ad01',\n yellow: '#eab308',\n gold: '#d4af37',\n orange: '#ea580c',\n rust: '#b7410e',\n terracotta: '#e2725b',\n coral: '#ff7f50',\n peach: '#ffcba4',\n camel: '#c19a6b',\n tan: '#d2b48c',\n brown: '#7c4a2d',\n chocolate: '#5c3a21',\n wine: '#722f37',\n burgundy: '#800020',\n maroon: '#800000',\n red: '#dc2626',\n pink: '#f9a8d4',\n blush: '#e5a4a4',\n rose: '#f43f5e',\n lavender: '#b57edc',\n lilac: '#c8a2c8',\n purple: '#9333ea',\n plum: '#8e4585',\n};\n\n/** Ordered keyword list for the \"contains\" fallback (longer keys first). */\nconst COLOUR_KEYWORDS = Object.keys(COLOUR_MAP).sort((a, b) => b.length - a.length);\n\nfunction resolveColour(name: string): string | null {\n const normalized = name.trim().toLowerCase();\n if (!normalized) return null;\n const direct = COLOUR_MAP[normalized];\n if (direct) return direct;\n const keyword = COLOUR_KEYWORDS.find((key) => normalized.includes(key));\n return keyword ? COLOUR_MAP[keyword] : null;\n}\n\nexport const ColourSwatchRow: FC<ColourSwatchRowProps> = ({ colours, max = 6, className }) => {\n const cleaned = colours.filter((c) => c && c.trim().length > 0);\n if (cleaned.length === 0) return null;\n\n const shown = cleaned.slice(0, max);\n const overflow = cleaned.length - shown.length;\n\n return (\n <div className={cn('flex flex-wrap items-center gap-1.5', className)}>\n {shown.map((colour, index) => {\n const css = resolveColour(colour);\n const style: CSSProperties | undefined = css ? { backgroundColor: css } : undefined;\n return (\n <span\n key={`${colour}-${index}`}\n title={colour}\n aria-label={colour}\n style={style}\n className={cn(\n 'inline-block h-4 w-4 rounded-full ring-1 ring-inset ring-border-seam',\n css ? undefined : 'bg-bg-sunken'\n )}\n />\n );\n })}\n {overflow > 0 ? (\n <span className=\"text-xs font-medium text-text-secondary tabular-nums\">+{overflow}</span>\n ) : null}\n </div>\n );\n};\n"],"mappings":";;;AA4BA,IAAM,IAAqC;CACzC,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,MAAM;CACN,OAAO;CACP,MAAM;CACN,OAAO;CACP,OAAO;CACP,MAAM;CACN,MAAM;CACN,UAAU;CACV,QAAQ;CACR,MAAM;CACN,MAAM;CACN,QAAQ;CACR,OAAO;CACP,QAAQ;CACR,MAAM;CACN,WAAW;CACX,OAAO;CACP,OAAO;CACP,MAAM;CACN,MAAM;CACN,SAAS;CACT,OAAO;CACP,SAAS;CACT,QAAQ;CACR,MAAM;CACN,QAAQ;CACR,MAAM;CACN,YAAY;CACZ,OAAO;CACP,OAAO;CACP,OAAO;CACP,KAAK;CACL,OAAO;CACP,WAAW;CACX,MAAM;CACN,UAAU;CACV,QAAQ;CACR,KAAK;CACL,MAAM;CACN,OAAO;CACP,MAAM;CACN,UAAU;CACV,OAAO;CACP,QAAQ;CACR,MAAM;AACR,GAGM,IAAkB,OAAO,KAAK,CAAU,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;AAElF,SAAS,EAAc,GAA6B;CAClD,IAAM,IAAa,EAAK,KAAK,CAAC,CAAC,YAAY;CAC3C,IAAI,CAAC,GAAY,OAAO;CACxB,IAAM,IAAS,EAAW;CAC1B,IAAI,GAAQ,OAAO;CACnB,IAAM,IAAU,EAAgB,MAAM,MAAQ,EAAW,SAAS,CAAG,CAAC;CACtE,OAAO,IAAU,EAAW,KAAW;AACzC;AAEA,IAAa,KAA6C,EAAE,YAAS,SAAM,GAAG,mBAAgB;CAC5F,IAAM,IAAU,EAAQ,QAAQ,MAAM,KAAK,EAAE,KAAK,CAAC,CAAC,SAAS,CAAC;CAC9D,IAAI,EAAQ,WAAW,GAAG,OAAO;CAEjC,IAAM,IAAQ,EAAQ,MAAM,GAAG,CAAG,GAC5B,IAAW,EAAQ,SAAS,EAAM;CAExC,OACE,kBAAC,OAAD;EAAK,WAAW,EAAG,uCAAuC,CAAS;YAAnE,CACG,EAAM,KAAK,GAAQ,MAAU;GAC5B,IAAM,IAAM,EAAc,CAAM;GAEhC,OACE,kBAAC,QAAD;IAEE,OAAO;IACP,cAAY;IACL,OAN8B,IAAM,EAAE,iBAAiB,EAAI,IAAI,KAAA;IAOtE,WAAW,EACT,wEACA,IAAM,KAAA,IAAY,cACpB;GACD,GARM,GAAG,EAAO,GAAG,GAQnB;EAEL,CAAC,GACA,IAAW,IACV,kBAAC,QAAD;GAAM,WAAU;aAAhB,CAAuE,KAAE,CAAe;OACtF,IACD;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"Comments.js","names":[],"sources":["../../../src/coolandlovely/components/Comments.tsx"],"sourcesContent":["/**\n * Comments — reviews & feedback thread for an entity (looks, brands).\n * @module coolandlovely/components\n *\n * Reuses wspace-conversations-svc `EntityComment` (NO coolandlovely comment\n * table). Lists + posts comments for (entityType, entityId). Commenter names\n * resolve via fe-libs useActorProfiles (GLOBAL gateway) — see ActorName.\n */\n\nimport type { FC } from 'react';\nimport { useState } from 'react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { Loader2, MessageSquare, Send } from 'lucide-react';\nimport { ActorName } from './ActorName';\nimport { useAddComment, useEntityComments } from '../hooks/useComments';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { cn } from '../utils/cn';\n\ninterface CommentsProps {\n /** Conversations entity type, e.g. \"CooLook\" / \"CooBrand\". */\n entityType: string;\n /** The entity id (skips rendering until present). */\n entityId?: string;\n /** Section heading (default \"Reviews & feedback\"). */\n title?: string;\n className?: string;\n}\n\nexport const Comments: FC<CommentsProps> = ({\n entityType,\n entityId,\n title = 'Reviews & feedback',\n className,\n}) => {\n const query = useEntityComments(entityType, entityId);\n const addComment = useAddComment();\n const [draft, setDraft] = useState('');\n\n const comments = query.data?.comments ?? [];\n const total = query.data?.total ?? comments.length;\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n const text = draft.trim();\n if (!text || !entityId) return;\n addComment.mutate({ entityType, entityId, text }, { onSuccess: () => setDraft('') });\n };\n\n return (\n <section className={cn('mt-6', className)}>\n <h3 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <MessageSquare className=\"h-5 w-5\" /> {title} ({total})\n </h3>\n\n <GlassCard treatment=\"plain\" className=\"p-4\">\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-2 sm:flex-row sm:items-start\">\n <label htmlFor=\"coo-comment-input\" className=\"sr-only\">\n Write a review or leave feedback\n </label>\n <textarea\n id=\"coo-comment-input\"\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n rows={2}\n placeholder=\"Share a review or leave feedback…\"\n className=\"w-full flex-1 resize-y rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <button\n type=\"submit\"\n disabled={!draft.trim() || !entityId || addComment.isPending}\n className=\"inline-flex shrink-0 items-center justify-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 disabled:opacity-50\"\n >\n {addComment.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Send className=\"h-4 w-4\" />\n )}\n Post\n </button>\n </form>\n {addComment.isError && (\n <p className=\"mt-2 text-xs text-status-error-text\">\n {addComment.error instanceof Error ? addComment.error.message : 'Could not post'}\n </p>\n )}\n </GlassCard>\n\n <div className=\"mt-4\">\n {query.isLoading ? (\n <div className=\"flex items-center justify-center gap-2 py-6 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" /> Loading reviews…\n </div>\n ) : query.isError ? (\n <div className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n Reviews could not be loaded right now.\n <button\n type=\"button\"\n onClick={() => void query.refetch()}\n className=\"ml-2 font-medium text-action-primary-bg hover:underline\"\n >\n Retry\n </button>\n </div>\n ) : comments.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No reviews yet. Be the first to share feedback.\n </p>\n ) : (\n <ul className=\"divide-y divide-border-seam rounded-lg border border-border-seam bg-bg-surface\">\n {comments.map((c) => (\n <li key={c.id} className=\"px-4 py-3\">\n <div className=\"flex items-center justify-between gap-2\">\n <ActorName\n id={c.authorId}\n actorType={c.authorType}\n className=\"truncate text-sm font-medium text-text-primary\"\n />\n <span className=\"shrink-0 text-xs text-text-secondary\">\n {formatRelativeTime(c.createdAt)}\n {c.edited ? ' · edited' : ''}\n </span>\n </div>\n <p className=\"mt-1 whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {c.text}\n </p>\n </li>\n ))}\n </ul>\n )}\n </div>\n </section>\n );\n};\n"],"mappings":";;;;;;;;;AA4BA,IAAa,KAA+B,EAC1C,eACA,aACA,WAAQ,sBACR,mBACI;CACJ,IAAM,IAAQ,EAAkB,GAAY,EAAS,EAC/C,IAAa,GAAe,EAC5B,CAAC,GAAO,KAAY,EAAS,GAAG,EAEhC,IAAW,EAAM,MAAM,YAAY,EAAE,EACrC,IAAQ,EAAM,MAAM,SAAS,EAAS;AAS5C,QACE,kBAAC,WAAD;EAAS,WAAW,EAAG,QAAQ,EAAU;YAAzC;GACE,kBAAC,MAAD;IAAI,WAAU;cAAd;KACE,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;;KAAE;KAAM;KAAG;KAAM;KACnD;;GAEL,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cAAvC,CACE,kBAAC,QAAD;KAAM,WAdU,MAAuB;AAC3C,QAAE,gBAAgB;MAClB,IAAM,IAAO,EAAM,MAAM;AACrB,OAAC,KAAQ,CAAC,KACd,EAAW,OAAO;OAAE;OAAY;OAAU;OAAM,EAAE,EAAE,iBAAiB,EAAS,GAAG,EAAE,CAAC;;KAUlD,WAAU;eAAxC;MACE,kBAAC,SAAD;OAAO,SAAQ;OAAoB,WAAU;iBAAU;OAE/C,CAAA;MACR,kBAAC,YAAD;OACE,IAAG;OACH,OAAO;OACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;OACzC,MAAM;OACN,aAAY;OACZ,WAAU;OACV,CAAA;MACF,kBAAC,UAAD;OACE,MAAK;OACL,UAAU,CAAC,EAAM,MAAM,IAAI,CAAC,KAAY,EAAW;OACnD,WAAU;iBAHZ,CAKG,EAAW,YACV,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAC5B,OAEK;;MACJ;QACN,EAAW,WACV,kBAAC,KAAD;KAAG,WAAU;eACV,EAAW,iBAAiB,QAAQ,EAAW,MAAM,UAAU;KAC9D,CAAA,CAEI;;GAEZ,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAM,YACL,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,EAAA,oBACxC;SACJ,EAAM,UACR,kBAAC,OAAD;KAAK,WAAU;eAAf,CAAoG,0CAElG,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,KAAK,EAAM,SAAS;MACnC,WAAU;gBACX;MAEQ,CAAA,CACL;SACJ,EAAS,WAAW,IACtB,kBAAC,KAAD;KAAG,WAAU;eAAqF;KAE9F,CAAA,GAEJ,kBAAC,MAAD;KAAI,WAAU;eACX,EAAS,KAAK,MACb,kBAAC,MAAD;MAAe,WAAU;gBAAzB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QACE,IAAI,EAAE;QACN,WAAW,EAAE;QACb,WAAU;QACV,CAAA,EACF,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACG,EAAmB,EAAE,UAAU,EAC/B,EAAE,SAAS,cAAc,GACrB;UACH;UACN,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAE;OACD,CAAA,CACD;QAfI,EAAE,GAeN,CACL;KACC,CAAA;IAEH,CAAA;GACE"}
1
+ {"version":3,"file":"Comments.js","names":[],"sources":["../../../src/coolandlovely/components/Comments.tsx"],"sourcesContent":["/**\n * Comments — reviews & feedback thread for an entity (looks, brands).\n * @module coolandlovely/components\n *\n * Reuses wspace-conversations-svc `EntityComment` (NO coolandlovely comment\n * table). Lists + posts comments for (entityType, entityId). Commenter names\n * resolve via fe-libs useActorProfiles (GLOBAL gateway) — see ActorName.\n */\n\nimport type { FC } from 'react';\nimport { useState } from 'react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { Loader2, MessageSquare, Send } from 'lucide-react';\nimport { ActorName } from './ActorName';\nimport { useAddComment, useEntityComments } from '../hooks/useComments';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { cn } from '../utils/cn';\n\ninterface CommentsProps {\n /** Conversations entity type, e.g. \"CooLook\" / \"CooBrand\". */\n entityType: string;\n /** The entity id (skips rendering until present). */\n entityId?: string;\n /** Section heading (default \"Reviews & feedback\"). */\n title?: string;\n className?: string;\n}\n\nexport const Comments: FC<CommentsProps> = ({\n entityType,\n entityId,\n title = 'Reviews & feedback',\n className,\n}) => {\n const query = useEntityComments(entityType, entityId);\n const addComment = useAddComment();\n const [draft, setDraft] = useState('');\n\n const comments = query.data?.comments ?? [];\n const total = query.data?.total ?? comments.length;\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n const text = draft.trim();\n if (!text || !entityId) return;\n addComment.mutate({ entityType, entityId, text }, { onSuccess: () => setDraft('') });\n };\n\n return (\n <section className={cn('mt-6', className)}>\n <h3 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <MessageSquare className=\"h-5 w-5\" /> {title} ({total})\n </h3>\n\n <GlassCard treatment=\"plain\" className=\"p-4\">\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-2 sm:flex-row sm:items-start\">\n <label htmlFor=\"coo-comment-input\" className=\"sr-only\">\n Write a review or leave feedback\n </label>\n <textarea\n id=\"coo-comment-input\"\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n rows={2}\n placeholder=\"Share a review or leave feedback…\"\n className=\"w-full flex-1 resize-y rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <button\n type=\"submit\"\n disabled={!draft.trim() || !entityId || addComment.isPending}\n className=\"inline-flex shrink-0 items-center justify-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 disabled:opacity-50\"\n >\n {addComment.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Send className=\"h-4 w-4\" />\n )}\n Post\n </button>\n </form>\n {addComment.isError && (\n <p className=\"mt-2 text-xs text-status-error-text\">\n {addComment.error instanceof Error ? addComment.error.message : 'Could not post'}\n </p>\n )}\n </GlassCard>\n\n <div className=\"mt-4\">\n {query.isLoading ? (\n <div className=\"flex items-center justify-center gap-2 py-6 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" /> Loading reviews…\n </div>\n ) : query.isError ? (\n <div className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n Reviews could not be loaded right now.\n <button\n type=\"button\"\n onClick={() => void query.refetch()}\n className=\"ml-2 font-medium text-action-primary-bg hover:underline\"\n >\n Retry\n </button>\n </div>\n ) : comments.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No reviews yet. Be the first to share feedback.\n </p>\n ) : (\n <ul className=\"divide-y divide-border-seam rounded-lg border border-border-seam bg-bg-surface\">\n {comments.map((c) => (\n <li key={c.id} className=\"px-4 py-3\">\n <div className=\"flex items-center justify-between gap-2\">\n <ActorName\n id={c.authorId}\n actorType={c.authorType}\n className=\"truncate text-sm font-medium text-text-primary\"\n />\n <span className=\"shrink-0 text-xs text-text-secondary\">\n {formatRelativeTime(c.createdAt)}\n {c.edited ? ' · edited' : ''}\n </span>\n </div>\n <p className=\"mt-1 whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {c.text}\n </p>\n </li>\n ))}\n </ul>\n )}\n </div>\n </section>\n );\n};\n"],"mappings":";;;;;;;;;AA4BA,IAAa,KAA+B,EAC1C,eACA,aACA,WAAQ,sBACR,mBACI;CACJ,IAAM,IAAQ,EAAkB,GAAY,CAAQ,GAC9C,IAAa,EAAc,GAC3B,CAAC,GAAO,KAAY,EAAS,EAAE,GAE/B,IAAW,EAAM,MAAM,YAAY,CAAC,GACpC,IAAQ,EAAM,MAAM,SAAS,EAAS;CAS5C,OACE,kBAAC,WAAD;EAAS,WAAW,EAAG,QAAQ,CAAS;YAAxC;GACE,kBAAC,MAAD;IAAI,WAAU;cAAd;KACE,kBAAC,GAAD,EAAe,WAAU,UAAW,CAAA;KAAC;KAAE;KAAM;KAAG;KAAM;IACpD;;GAEJ,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cAAvC,CACE,kBAAC,QAAD;KAAM,WAdU,MAAuB;MAC3C,EAAE,eAAe;MACjB,IAAM,IAAO,EAAM,KAAK;MACpB,CAAC,KAAQ,CAAC,KACd,EAAW,OAAO;OAAE;OAAY;OAAU;MAAK,GAAG,EAAE,iBAAiB,EAAS,EAAE,EAAE,CAAC;KACrF;KASoC,WAAU;eAAxC;MACE,kBAAC,SAAD;OAAO,SAAQ;OAAoB,WAAU;iBAAU;MAEhD,CAAA;MACP,kBAAC,YAAD;OACE,IAAG;OACH,OAAO;OACP,WAAW,MAAM,EAAS,EAAE,OAAO,KAAK;OACxC,MAAM;OACN,aAAY;OACZ,WAAU;MACX,CAAA;MACD,kBAAC,UAAD;OACE,MAAK;OACL,UAAU,CAAC,EAAM,KAAK,KAAK,CAAC,KAAY,EAAW;OACnD,WAAU;iBAHZ,CAKG,EAAW,YACV,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,IAE3C,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,GAC3B,MAEI;;KACJ;QACL,EAAW,WACV,kBAAC,KAAD;KAAG,WAAU;eACV,EAAW,iBAAiB,QAAQ,EAAW,MAAM,UAAU;IAC/D,CAAA,CAEI;;GAEX,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAM,YACL,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,GAAC,mBACzC;SACH,EAAM,UACR,kBAAC,OAAD;KAAK,WAAU;eAAf,CAAoG,0CAElG,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,KAAK,EAAM,QAAQ;MAClC,WAAU;gBACX;KAEO,CAAA,CACL;SACH,EAAS,WAAW,IACtB,kBAAC,KAAD;KAAG,WAAU;eAAqF;IAE/F,CAAA,IAEH,kBAAC,MAAD;KAAI,WAAU;eACX,EAAS,KAAK,MACb,kBAAC,MAAD;MAAe,WAAU;gBAAzB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QACE,IAAI,EAAE;QACN,WAAW,EAAE;QACb,WAAU;OACX,CAAA,GACD,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACG,EAAmB,EAAE,SAAS,GAC9B,EAAE,SAAS,cAAc,EACtB;SACH;UACL,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAE;MACF,CAAA,CACD;QAfK,EAAE,EAeP,CACL;IACC,CAAA;GAEH,CAAA;EACE;;AAEb"}
@@ -1 +1 @@
1
- {"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/coolandlovely/components/EmptyState.tsx"],"sourcesContent":["/**\n * Empty State Component\n * @module coolandlovely/components\n *\n * Boff UI 2.0: renders an on-brand illustrated empty state. When an explicit\n * `icon` is supplied (legacy callers) it is kept as a calm glyph above the\n * copy; otherwise an illustration (default: empty-data) is used.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface EmptyStateProps {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n className?: string;\n /** Host-served illustration name (e.g. \"empty-search\", \"empty-people\"). */\n illustration?: string;\n}\n\nexport const EmptyState: FC<EmptyStateProps> = ({\n icon,\n title,\n description,\n action,\n className,\n illustration = 'empty-data',\n}) => {\n return (\n <div className={cn('py-8', className)}>\n <IllustratedEmptyState\n illustration={illustration}\n title={title}\n description={\n icon || description ? (\n <span className=\"flex flex-col items-center gap-3\">\n {icon && <span className=\"text-text-secondary opacity-70\">{icon}</span>}\n {description && <span>{description}</span>}\n </span>\n ) : undefined\n }\n action={action}\n alt={title}\n />\n </div>\n );\n};\n"],"mappings":";;;;AAuBA,IAAa,KAAmC,EAC9C,SACA,UACA,gBACA,WACA,cACA,kBAAe,mBAGb,kBAAC,OAAD;CAAK,WAAW,EAAG,QAAQ,EAAU;WACnC,kBAAC,GAAD;EACgB;EACP;EACP,aACE,KAAQ,IACN,kBAAC,QAAD;GAAM,WAAU;aAAhB,CACG,KAAQ,kBAAC,QAAD;IAAM,WAAU;cAAkC;IAAY,CAAA,EACtE,KAAe,kBAAC,QAAD,EAAA,UAAO,GAAmB,CAAA,CACrC;OACL,KAAA;EAEE;EACR,KAAK;EACL,CAAA;CACE,CAAA"}
1
+ {"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/coolandlovely/components/EmptyState.tsx"],"sourcesContent":["/**\n * Empty State Component\n * @module coolandlovely/components\n *\n * Boff UI 2.0: renders an on-brand illustrated empty state. When an explicit\n * `icon` is supplied (legacy callers) it is kept as a calm glyph above the\n * copy; otherwise an illustration (default: empty-data) is used.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface EmptyStateProps {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n className?: string;\n /** Host-served illustration name (e.g. \"empty-search\", \"empty-people\"). */\n illustration?: string;\n}\n\nexport const EmptyState: FC<EmptyStateProps> = ({\n icon,\n title,\n description,\n action,\n className,\n illustration = 'empty-data',\n}) => {\n return (\n <div className={cn('py-8', className)}>\n <IllustratedEmptyState\n illustration={illustration}\n title={title}\n description={\n icon || description ? (\n <span className=\"flex flex-col items-center gap-3\">\n {icon && <span className=\"text-text-secondary opacity-70\">{icon}</span>}\n {description && <span>{description}</span>}\n </span>\n ) : undefined\n }\n action={action}\n alt={title}\n />\n </div>\n );\n};\n"],"mappings":";;;;AAuBA,IAAa,KAAmC,EAC9C,SACA,UACA,gBACA,WACA,cACA,kBAAe,mBAGb,kBAAC,OAAD;CAAK,WAAW,EAAG,QAAQ,CAAS;WAClC,kBAAC,GAAD;EACgB;EACP;EACP,aACE,KAAQ,IACN,kBAAC,QAAD;GAAM,WAAU;aAAhB,CACG,KAAQ,kBAAC,QAAD;IAAM,WAAU;cAAkC;GAAW,CAAA,GACrE,KAAe,kBAAC,QAAD,EAAA,UAAO,EAAkB,CAAA,CACrC;OACJ,KAAA;EAEE;EACR,KAAK;CACN,CAAA;AACE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"FeedbackWidget.js","names":[],"sources":["../../../src/coolandlovely/components/FeedbackWidget.tsx"],"sourcesContent":["/**\n * Lightweight NPS / feedback widget\n * @module coolandlovely/components\n *\n * Floating feedback capture mounted across all CoolAndLovely surfaces.\n * Submissions are stored locally and emitted as analytics events so the\n * product can gather signals without blocking on a backend integration.\n */\n\nimport type { FC } from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { MessageSquarePlus, X, Send } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { TextAreaField } from './Field';\nimport { cn } from '../utils/cn';\nimport { useTr } from '../hooks';\n\nconst STORAGE_KEY = 'coolandlovely-feedback-submitted-at';\nconst COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; // one week\n\ninterface FeedbackSubmission {\n score: number;\n comment: string;\n path: string;\n submittedAt: string;\n}\n\nfunction hasRecentSubmission(): boolean {\n if (typeof window === 'undefined') return false;\n const raw = window.localStorage.getItem(STORAGE_KEY);\n if (!raw) return false;\n try {\n const ts = parseInt(raw, 10);\n return Date.now() - ts < COOLDOWN_MS;\n } catch {\n return false;\n }\n}\n\nfunction recordSubmission() {\n if (typeof window === 'undefined') return;\n window.localStorage.setItem(STORAGE_KEY, String(Date.now()));\n}\n\nfunction emitFeedbackEvent(submission: FeedbackSubmission) {\n // Emit a structured event that the host shell's analytics pipeline can pick up.\n if (typeof window === 'undefined') return;\n window.dispatchEvent(\n new CustomEvent('coolandlovely:feedback_submitted', {\n detail: submission,\n bubbles: true,\n })\n );\n}\n\nexport const FeedbackWidget: FC = () => {\n const tr = useTr();\n const [isOpen, setIsOpen] = useState(false);\n const [score, setScore] = useState<number | null>(null);\n const [comment, setComment] = useState('');\n const [submitted, setSubmitted] = useState(false);\n const [recentlySubmitted, setRecentlySubmitted] = useState(false);\n\n useEffect(() => {\n setRecentlySubmitted(hasRecentSubmission());\n }, []);\n\n const handleToggle = useCallback(() => {\n setIsOpen((open) => !open);\n }, []);\n\n const handleClose = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const handleSubmit = useCallback(() => {\n if (score === null) return;\n\n const submission: FeedbackSubmission = {\n score,\n comment: comment.trim(),\n path: typeof window !== 'undefined' ? window.location.pathname : '',\n submittedAt: new Date().toISOString(),\n };\n\n emitFeedbackEvent(submission);\n recordSubmission();\n setSubmitted(true);\n setRecentlySubmitted(true);\n\n // Auto-close after a short delay so the user sees the thank-you state.\n window.setTimeout(() => {\n setIsOpen(false);\n setScore(null);\n setComment('');\n setSubmitted(false);\n }, 2500);\n }, [score, comment]);\n\n if (recentlySubmitted && !isOpen) {\n // Do not show the launcher at all for a week after submission.\n return null;\n }\n\n return (\n <div className=\"fixed bottom-4 right-4 z-50 flex flex-col items-end gap-3\">\n {isOpen && (\n <GlassCard className=\"w-[min(92vw,360px)] overflow-hidden p-0 shadow-xl\">\n <div className=\"flex items-center justify-between border-b border-border-seam px-4 py-3\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n {tr('coolandlovely.feedback.title', 'How is CoolAndLovely working for you?')}\n </h2>\n <button\n type=\"button\"\n onClick={handleClose}\n aria-label={tr('coolandlovely.feedback.close', 'Close feedback')}\n className=\"rounded-md p-1 text-text-secondary transition-colors hover:bg-bg-surface hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n\n <div className=\"p-4\">\n {submitted ? (\n <div className=\"py-6 text-center\">\n <p className=\"text-sm font-medium text-status-success-text\">\n {tr('coolandlovely.feedback.thankYou', 'Thank you for your feedback!')}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {tr(\n 'coolandlovely.feedback.helpText',\n 'Your input helps us improve CoolAndLovely.'\n )}\n </p>\n </div>\n ) : (\n <div className=\"space-y-4\">\n <div>\n <p className=\"mb-2 text-xs font-medium text-text-secondary\">\n {tr(\n 'coolandlovely.feedback.ratePrompt',\n 'Rate your experience (0 = not likely, 10 = very likely)'\n )}\n </p>\n <div\n className=\"flex justify-between gap-1\"\n role=\"group\"\n aria-label={tr('coolandlovely.feedback.npsLabel', 'NPS score')}\n >\n {Array.from({ length: 11 }, (_, i) => i).map((value) => (\n <button\n key={value}\n type=\"button\"\n onClick={() => setScore(value)}\n aria-pressed={score === value}\n className={cn(\n 'flex h-8 w-7 items-center justify-center rounded-md text-xs font-medium transition-colors',\n score === value\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'border border-border-seam bg-bg-surface text-text-secondary hover:border-action-primary-bg hover:text-text-primary'\n )}\n >\n {value}\n </button>\n ))}\n </div>\n </div>\n\n <TextAreaField\n id=\"feedback-comment\"\n label={tr('coolandlovely.feedback.commentLabel', 'What could be better?')}\n value={comment}\n onChange={setComment}\n placeholder={tr(\n 'coolandlovely.feedback.commentPlaceholder',\n 'Tell us what’s working or what we should improve…'\n )}\n rows={3}\n hint={tr(\n 'coolandlovely.feedback.commentHint',\n 'Optional — your comment is stored with your score and current page.'\n )}\n />\n\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={score === null}\n className={cn(\n 'inline-flex w-full items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors',\n score === null\n ? 'cursor-not-allowed bg-bg-surface text-text-secondary'\n : 'bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90'\n )}\n >\n <Send className=\"h-4 w-4\" aria-hidden=\"true\" />\n {tr('coolandlovely.feedback.submit', 'Submit feedback')}\n </button>\n </div>\n )}\n </div>\n </GlassCard>\n )}\n\n <button\n type=\"button\"\n onClick={handleToggle}\n aria-label={\n isOpen\n ? tr('coolandlovely.feedback.close', 'Close feedback')\n : tr('coolandlovely.feedback.open', 'Give feedback')\n }\n aria-expanded={isOpen}\n className={cn(\n 'flex h-12 w-12 items-center justify-center rounded-full shadow-lg transition-transform hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n isOpen\n ? 'bg-bg-surface text-text-primary'\n : 'bg-action-primary-bg text-action-primary-text'\n )}\n >\n {isOpen ? (\n <X className=\"h-5 w-5\" aria-hidden=\"true\" />\n ) : (\n <MessageSquarePlus className=\"h-5 w-5\" aria-hidden=\"true\" />\n )}\n </button>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"FeedbackWidget.js","names":[],"sources":["../../../src/coolandlovely/components/FeedbackWidget.tsx"],"sourcesContent":["/**\n * Lightweight NPS / feedback widget\n * @module coolandlovely/components\n *\n * Floating feedback capture mounted across all CoolAndLovely surfaces.\n * Submissions are stored locally and emitted as analytics events so the\n * product can gather signals without blocking on a backend integration.\n */\n\nimport type { FC } from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { MessageSquarePlus, X, Send } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { TextAreaField } from './Field';\nimport { cn } from '../utils/cn';\nimport { useTr } from '../hooks';\n\nconst STORAGE_KEY = 'coolandlovely-feedback-submitted-at';\nconst COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; // one week\n\ninterface FeedbackSubmission {\n score: number;\n comment: string;\n path: string;\n submittedAt: string;\n}\n\nfunction hasRecentSubmission(): boolean {\n if (typeof window === 'undefined') return false;\n const raw = window.localStorage.getItem(STORAGE_KEY);\n if (!raw) return false;\n try {\n const ts = parseInt(raw, 10);\n return Date.now() - ts < COOLDOWN_MS;\n } catch {\n return false;\n }\n}\n\nfunction recordSubmission() {\n if (typeof window === 'undefined') return;\n window.localStorage.setItem(STORAGE_KEY, String(Date.now()));\n}\n\nfunction emitFeedbackEvent(submission: FeedbackSubmission) {\n // Emit a structured event that the host shell's analytics pipeline can pick up.\n if (typeof window === 'undefined') return;\n window.dispatchEvent(\n new CustomEvent('coolandlovely:feedback_submitted', {\n detail: submission,\n bubbles: true,\n })\n );\n}\n\nexport const FeedbackWidget: FC = () => {\n const tr = useTr();\n const [isOpen, setIsOpen] = useState(false);\n const [score, setScore] = useState<number | null>(null);\n const [comment, setComment] = useState('');\n const [submitted, setSubmitted] = useState(false);\n const [recentlySubmitted, setRecentlySubmitted] = useState(false);\n\n useEffect(() => {\n setRecentlySubmitted(hasRecentSubmission());\n }, []);\n\n const handleToggle = useCallback(() => {\n setIsOpen((open) => !open);\n }, []);\n\n const handleClose = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const handleSubmit = useCallback(() => {\n if (score === null) return;\n\n const submission: FeedbackSubmission = {\n score,\n comment: comment.trim(),\n path: typeof window !== 'undefined' ? window.location.pathname : '',\n submittedAt: new Date().toISOString(),\n };\n\n emitFeedbackEvent(submission);\n recordSubmission();\n setSubmitted(true);\n setRecentlySubmitted(true);\n\n // Auto-close after a short delay so the user sees the thank-you state.\n window.setTimeout(() => {\n setIsOpen(false);\n setScore(null);\n setComment('');\n setSubmitted(false);\n }, 2500);\n }, [score, comment]);\n\n if (recentlySubmitted && !isOpen) {\n // Do not show the launcher at all for a week after submission.\n return null;\n }\n\n return (\n <div className=\"fixed bottom-4 right-4 z-50 flex flex-col items-end gap-3\">\n {isOpen && (\n <GlassCard className=\"w-[min(92vw,360px)] overflow-hidden p-0 shadow-xl\">\n <div className=\"flex items-center justify-between border-b border-border-seam px-4 py-3\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n {tr('coolandlovely.feedback.title', 'How is CoolAndLovely working for you?')}\n </h2>\n <button\n type=\"button\"\n onClick={handleClose}\n aria-label={tr('coolandlovely.feedback.close', 'Close feedback')}\n className=\"rounded-md p-1 text-text-secondary transition-colors hover:bg-bg-surface hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n\n <div className=\"p-4\">\n {submitted ? (\n <div className=\"py-6 text-center\">\n <p className=\"text-sm font-medium text-status-success-text\">\n {tr('coolandlovely.feedback.thankYou', 'Thank you for your feedback!')}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {tr(\n 'coolandlovely.feedback.helpText',\n 'Your input helps us improve CoolAndLovely.'\n )}\n </p>\n </div>\n ) : (\n <div className=\"space-y-4\">\n <div>\n <p className=\"mb-2 text-xs font-medium text-text-secondary\">\n {tr(\n 'coolandlovely.feedback.ratePrompt',\n 'Rate your experience (0 = not likely, 10 = very likely)'\n )}\n </p>\n <div\n className=\"flex justify-between gap-1\"\n role=\"group\"\n aria-label={tr('coolandlovely.feedback.npsLabel', 'NPS score')}\n >\n {Array.from({ length: 11 }, (_, i) => i).map((value) => (\n <button\n key={value}\n type=\"button\"\n onClick={() => setScore(value)}\n aria-pressed={score === value}\n className={cn(\n 'flex h-8 w-7 items-center justify-center rounded-md text-xs font-medium transition-colors',\n score === value\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'border border-border-seam bg-bg-surface text-text-secondary hover:border-action-primary-bg hover:text-text-primary'\n )}\n >\n {value}\n </button>\n ))}\n </div>\n </div>\n\n <TextAreaField\n id=\"feedback-comment\"\n label={tr('coolandlovely.feedback.commentLabel', 'What could be better?')}\n value={comment}\n onChange={setComment}\n placeholder={tr(\n 'coolandlovely.feedback.commentPlaceholder',\n 'Tell us what’s working or what we should improve…'\n )}\n rows={3}\n hint={tr(\n 'coolandlovely.feedback.commentHint',\n 'Optional — your comment is stored with your score and current page.'\n )}\n />\n\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={score === null}\n className={cn(\n 'inline-flex w-full items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors',\n score === null\n ? 'cursor-not-allowed bg-bg-surface text-text-secondary'\n : 'bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90'\n )}\n >\n <Send className=\"h-4 w-4\" aria-hidden=\"true\" />\n {tr('coolandlovely.feedback.submit', 'Submit feedback')}\n </button>\n </div>\n )}\n </div>\n </GlassCard>\n )}\n\n <button\n type=\"button\"\n onClick={handleToggle}\n aria-label={\n isOpen\n ? tr('coolandlovely.feedback.close', 'Close feedback')\n : tr('coolandlovely.feedback.open', 'Give feedback')\n }\n aria-expanded={isOpen}\n className={cn(\n 'flex h-12 w-12 items-center justify-center rounded-full shadow-lg transition-transform hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n isOpen\n ? 'bg-bg-surface text-text-primary'\n : 'bg-action-primary-bg text-action-primary-text'\n )}\n >\n {isOpen ? (\n <X className=\"h-5 w-5\" aria-hidden=\"true\" />\n ) : (\n <MessageSquarePlus className=\"h-5 w-5\" aria-hidden=\"true\" />\n )}\n </button>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Field.js","names":[],"sources":["../../../src/coolandlovely/components/Field.tsx"],"sourcesContent":["/**\n * Form field primitives\n * @module coolandlovely/components\n *\n * Small, strictly-typed, token-only form controls used across the create/edit\n * forms (no third-party form lib — these wrap native inputs with consistent\n * labels, hints and error styling).\n */\n\nimport type { FC, ReactNode, ChangeEvent } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface FieldShellProps {\n label: string;\n htmlFor?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n children: ReactNode;\n className?: string;\n}\n\nexport const FieldShell: FC<FieldShellProps> = ({\n label,\n htmlFor,\n required,\n hint,\n error,\n children,\n className,\n}) => (\n <div className={cn('flex flex-col gap-1.5', className)}>\n <label htmlFor={htmlFor} className=\"text-sm font-medium text-text-primary\">\n {label}\n {required && <span className=\"ml-0.5 text-status-error-text\">*</span>}\n </label>\n {children}\n {error ? (\n <p className=\"text-xs text-status-error-text\">{error}</p>\n ) : hint ? (\n <p className=\"text-xs text-text-secondary\">{hint}</p>\n ) : null}\n </div>\n);\n\nconst baseControl =\n 'w-full rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\n\ninterface TextFieldProps {\n id?: string;\n label: string;\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n type?: 'text' | 'url' | 'number' | 'date' | 'password';\n /** Native autocomplete hint (e.g. 'off' for one-time API credentials). */\n autoComplete?: string;\n disabled?: boolean;\n className?: string;\n}\n\nexport const TextField: FC<TextFieldProps> = ({\n id,\n label,\n value,\n onChange,\n placeholder,\n required,\n hint,\n error,\n type = 'text',\n autoComplete,\n disabled,\n className,\n}) => (\n <FieldShell\n label={label}\n htmlFor={id}\n required={required}\n hint={hint}\n error={error}\n className={className}\n >\n <input\n id={id}\n type={type}\n autoComplete={autoComplete}\n value={value}\n placeholder={placeholder}\n required={required}\n disabled={disabled}\n onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}\n className={cn(\n baseControl,\n error && 'border-status-error-text',\n disabled && 'cursor-not-allowed opacity-60'\n )}\n />\n </FieldShell>\n);\n\ninterface TextAreaFieldProps {\n id?: string;\n label: string;\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n rows?: number;\n disabled?: boolean;\n className?: string;\n}\n\nexport const TextAreaField: FC<TextAreaFieldProps> = ({\n id,\n label,\n value,\n onChange,\n placeholder,\n required,\n hint,\n error,\n rows = 3,\n disabled,\n className,\n}) => (\n <FieldShell\n label={label}\n htmlFor={id}\n required={required}\n hint={hint}\n error={error}\n className={className}\n >\n <textarea\n id={id}\n value={value}\n placeholder={placeholder}\n required={required}\n rows={rows}\n disabled={disabled}\n onChange={(e: ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}\n className={cn(\n baseControl,\n 'resize-y',\n error && 'border-status-error-text',\n disabled && 'cursor-not-allowed opacity-60'\n )}\n />\n </FieldShell>\n);\n\ninterface ToggleFieldProps {\n id?: string;\n label: string;\n checked: boolean;\n onChange: (checked: boolean) => void;\n hint?: string;\n disabled?: boolean;\n className?: string;\n testId?: string;\n}\n\n/** A token-only checkbox row (used for boolean flags e.g. Featured). */\nexport const ToggleField: FC<ToggleFieldProps> = ({\n id,\n label,\n checked,\n onChange,\n hint,\n disabled,\n className,\n testId,\n}) => (\n <label\n htmlFor={id}\n className={cn(\n 'flex cursor-pointer items-start gap-3 rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5',\n disabled && 'cursor-not-allowed opacity-60',\n className\n )}\n >\n <input\n id={id}\n type=\"checkbox\"\n checked={checked}\n disabled={disabled}\n onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.checked)}\n data-testid={testId}\n className=\"mt-0.5 h-4 w-4 shrink-0 rounded border-border-seam text-action-primary-bg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n />\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium text-text-primary\">{label}</span>\n {hint && <span className=\"mt-0.5 block text-xs text-text-secondary\">{hint}</span>}\n </span>\n </label>\n);\n\ninterface SelectOption {\n value: string;\n label: string;\n}\n\ninterface SelectFieldProps {\n id?: string;\n label: string;\n value: string;\n onChange: (value: string) => void;\n options: SelectOption[];\n required?: boolean;\n hint?: string;\n error?: string;\n placeholder?: string;\n disabled?: boolean;\n className?: string;\n}\n\nexport const SelectField: FC<SelectFieldProps> = ({\n id,\n label,\n value,\n onChange,\n options,\n required,\n hint,\n error,\n placeholder,\n disabled,\n className,\n}) => (\n <FieldShell\n label={label}\n htmlFor={id}\n required={required}\n hint={hint}\n error={error}\n className={className}\n >\n <select\n id={id}\n value={value}\n required={required}\n disabled={disabled}\n onChange={(e: ChangeEvent<HTMLSelectElement>) => onChange(e.target.value)}\n className={cn(\n baseControl,\n error && 'border-status-error-text',\n disabled && 'cursor-not-allowed opacity-60'\n )}\n >\n {placeholder && <option value=\"\">{placeholder}</option>}\n {options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </FieldShell>\n);\n"],"mappings":";;;AAsBA,IAAa,KAAmC,EAC9C,UACA,YACA,aACA,SACA,UACA,aACA,mBAEA,kBAAC,OAAD;CAAK,WAAW,EAAG,yBAAyB,EAAU;WAAtD;EACE,kBAAC,SAAD;GAAgB;GAAS,WAAU;aAAnC,CACG,GACA,KAAY,kBAAC,QAAD;IAAM,WAAU;cAAgC;IAAQ,CAAA,CAC/D;;EACP;EACA,IACC,kBAAC,KAAD;GAAG,WAAU;aAAkC;GAAU,CAAA,GACvD,IACF,kBAAC,KAAD;GAAG,WAAU;aAA+B;GAAS,CAAA,GACnD;EACA;IAGF,IACJ,sMAkBW,KAAiC,EAC5C,OACA,UACA,UACA,aACA,gBACA,aACA,SACA,UACA,UAAO,QACP,iBACA,aACA,mBAEA,kBAAC,GAAD;CACS;CACP,SAAS;CACC;CACJ;CACC;CACI;WAEX,kBAAC,SAAD;EACM;EACE;EACQ;EACP;EACM;EACH;EACA;EACV,WAAW,MAAqC,EAAS,EAAE,OAAO,MAAM;EACxE,WAAW,EACT,GACA,KAAS,4BACT,KAAY,gCACb;EACD,CAAA;CACS,CAAA,EAiBF,KAAyC,EACpD,OACA,UACA,UACA,aACA,gBACA,aACA,SACA,UACA,UAAO,GACP,aACA,mBAEA,kBAAC,GAAD;CACS;CACP,SAAS;CACC;CACJ;CACC;CACI;WAEX,kBAAC,YAAD;EACM;EACG;EACM;EACH;EACJ;EACI;EACV,WAAW,MAAwC,EAAS,EAAE,OAAO,MAAM;EAC3E,WAAW,EACT,GACA,YACA,KAAS,4BACT,KAAY,gCACb;EACD,CAAA;CACS,CAAA,EAeF,KAAqC,EAChD,OACA,UACA,YACA,aACA,SACA,aACA,cACA,gBAEA,kBAAC,SAAD;CACE,SAAS;CACT,WAAW,EACT,wGACA,KAAY,iCACZ,EACD;WANH,CAQE,kBAAC,SAAD;EACM;EACJ,MAAK;EACI;EACC;EACV,WAAW,MAAqC,EAAS,EAAE,OAAO,QAAQ;EAC1E,eAAa;EACb,WAAU;EACV,CAAA,EACF,kBAAC,QAAD;EAAM,WAAU;YAAhB,CACE,kBAAC,QAAD;GAAM,WAAU;aAA+C;GAAa,CAAA,EAC3E,KAAQ,kBAAC,QAAD;GAAM,WAAU;aAA4C;GAAY,CAAA,CAC5E;IACD;IAsBG,KAAqC,EAChD,OACA,UACA,UACA,aACA,YACA,aACA,SACA,UACA,gBACA,aACA,mBAEA,kBAAC,GAAD;CACS;CACP,SAAS;CACC;CACJ;CACC;CACI;WAEX,kBAAC,UAAD;EACM;EACG;EACG;EACA;EACV,WAAW,MAAsC,EAAS,EAAE,OAAO,MAAM;EACzE,WAAW,EACT,GACA,KAAS,4BACT,KAAY,gCACb;YAVH,CAYG,KAAe,kBAAC,UAAD;GAAQ,OAAM;aAAI;GAAqB,CAAA,EACtD,EAAQ,KAAK,MACZ,kBAAC,UAAD;GAAwB,OAAO,EAAI;aAChC,EAAI;GACE,EAFI,EAAI,MAER,CACT,CACK;;CACE,CAAA"}
1
+ {"version":3,"file":"Field.js","names":[],"sources":["../../../src/coolandlovely/components/Field.tsx"],"sourcesContent":["/**\n * Form field primitives\n * @module coolandlovely/components\n *\n * Small, strictly-typed, token-only form controls used across the create/edit\n * forms (no third-party form lib — these wrap native inputs with consistent\n * labels, hints and error styling).\n */\n\nimport type { FC, ReactNode, ChangeEvent } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface FieldShellProps {\n label: string;\n htmlFor?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n children: ReactNode;\n className?: string;\n}\n\nexport const FieldShell: FC<FieldShellProps> = ({\n label,\n htmlFor,\n required,\n hint,\n error,\n children,\n className,\n}) => (\n <div className={cn('flex flex-col gap-1.5', className)}>\n <label htmlFor={htmlFor} className=\"text-sm font-medium text-text-primary\">\n {label}\n {required && <span className=\"ml-0.5 text-status-error-text\">*</span>}\n </label>\n {children}\n {error ? (\n <p className=\"text-xs text-status-error-text\">{error}</p>\n ) : hint ? (\n <p className=\"text-xs text-text-secondary\">{hint}</p>\n ) : null}\n </div>\n);\n\nconst baseControl =\n 'w-full rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\n\ninterface TextFieldProps {\n id?: string;\n label: string;\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n type?: 'text' | 'url' | 'number' | 'date' | 'password';\n /** Native autocomplete hint (e.g. 'off' for one-time API credentials). */\n autoComplete?: string;\n disabled?: boolean;\n className?: string;\n}\n\nexport const TextField: FC<TextFieldProps> = ({\n id,\n label,\n value,\n onChange,\n placeholder,\n required,\n hint,\n error,\n type = 'text',\n autoComplete,\n disabled,\n className,\n}) => (\n <FieldShell\n label={label}\n htmlFor={id}\n required={required}\n hint={hint}\n error={error}\n className={className}\n >\n <input\n id={id}\n type={type}\n autoComplete={autoComplete}\n value={value}\n placeholder={placeholder}\n required={required}\n disabled={disabled}\n onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}\n className={cn(\n baseControl,\n error && 'border-status-error-text',\n disabled && 'cursor-not-allowed opacity-60'\n )}\n />\n </FieldShell>\n);\n\ninterface TextAreaFieldProps {\n id?: string;\n label: string;\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n rows?: number;\n disabled?: boolean;\n className?: string;\n}\n\nexport const TextAreaField: FC<TextAreaFieldProps> = ({\n id,\n label,\n value,\n onChange,\n placeholder,\n required,\n hint,\n error,\n rows = 3,\n disabled,\n className,\n}) => (\n <FieldShell\n label={label}\n htmlFor={id}\n required={required}\n hint={hint}\n error={error}\n className={className}\n >\n <textarea\n id={id}\n value={value}\n placeholder={placeholder}\n required={required}\n rows={rows}\n disabled={disabled}\n onChange={(e: ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}\n className={cn(\n baseControl,\n 'resize-y',\n error && 'border-status-error-text',\n disabled && 'cursor-not-allowed opacity-60'\n )}\n />\n </FieldShell>\n);\n\ninterface ToggleFieldProps {\n id?: string;\n label: string;\n checked: boolean;\n onChange: (checked: boolean) => void;\n hint?: string;\n disabled?: boolean;\n className?: string;\n testId?: string;\n}\n\n/** A token-only checkbox row (used for boolean flags e.g. Featured). */\nexport const ToggleField: FC<ToggleFieldProps> = ({\n id,\n label,\n checked,\n onChange,\n hint,\n disabled,\n className,\n testId,\n}) => (\n <label\n htmlFor={id}\n className={cn(\n 'flex cursor-pointer items-start gap-3 rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5',\n disabled && 'cursor-not-allowed opacity-60',\n className\n )}\n >\n <input\n id={id}\n type=\"checkbox\"\n checked={checked}\n disabled={disabled}\n onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.checked)}\n data-testid={testId}\n className=\"mt-0.5 h-4 w-4 shrink-0 rounded border-border-seam text-action-primary-bg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n />\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium text-text-primary\">{label}</span>\n {hint && <span className=\"mt-0.5 block text-xs text-text-secondary\">{hint}</span>}\n </span>\n </label>\n);\n\ninterface SelectOption {\n value: string;\n label: string;\n}\n\ninterface SelectFieldProps {\n id?: string;\n label: string;\n value: string;\n onChange: (value: string) => void;\n options: SelectOption[];\n required?: boolean;\n hint?: string;\n error?: string;\n placeholder?: string;\n disabled?: boolean;\n className?: string;\n}\n\nexport const SelectField: FC<SelectFieldProps> = ({\n id,\n label,\n value,\n onChange,\n options,\n required,\n hint,\n error,\n placeholder,\n disabled,\n className,\n}) => (\n <FieldShell\n label={label}\n htmlFor={id}\n required={required}\n hint={hint}\n error={error}\n className={className}\n >\n <select\n id={id}\n value={value}\n required={required}\n disabled={disabled}\n onChange={(e: ChangeEvent<HTMLSelectElement>) => onChange(e.target.value)}\n className={cn(\n baseControl,\n error && 'border-status-error-text',\n disabled && 'cursor-not-allowed opacity-60'\n )}\n >\n {placeholder && <option value=\"\">{placeholder}</option>}\n {options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </FieldShell>\n);\n"],"mappings":";;;AAsBA,IAAa,KAAmC,EAC9C,UACA,YACA,aACA,SACA,UACA,aACA,mBAEA,kBAAC,OAAD;CAAK,WAAW,EAAG,yBAAyB,CAAS;WAArD;EACE,kBAAC,SAAD;GAAgB;GAAS,WAAU;aAAnC,CACG,GACA,KAAY,kBAAC,QAAD;IAAM,WAAU;cAAgC;GAAO,CAAA,CAC/D;;EACN;EACA,IACC,kBAAC,KAAD;GAAG,WAAU;aAAkC;EAAS,CAAA,IACtD,IACF,kBAAC,KAAD;GAAG,WAAU;aAA+B;EAAQ,CAAA,IAClD;CACD;IAGD,IACJ,sMAkBW,KAAiC,EAC5C,OACA,UACA,UACA,aACA,gBACA,aACA,SACA,UACA,UAAO,QACP,iBACA,aACA,mBAEA,kBAAC,GAAD;CACS;CACP,SAAS;CACC;CACJ;CACC;CACI;WAEX,kBAAC,SAAD;EACM;EACE;EACQ;EACP;EACM;EACH;EACA;EACV,WAAW,MAAqC,EAAS,EAAE,OAAO,KAAK;EACvE,WAAW,EACT,GACA,KAAS,4BACT,KAAY,+BACd;CACD,CAAA;AACS,CAAA,GAiBD,KAAyC,EACpD,OACA,UACA,UACA,aACA,gBACA,aACA,SACA,UACA,UAAO,GACP,aACA,mBAEA,kBAAC,GAAD;CACS;CACP,SAAS;CACC;CACJ;CACC;CACI;WAEX,kBAAC,YAAD;EACM;EACG;EACM;EACH;EACJ;EACI;EACV,WAAW,MAAwC,EAAS,EAAE,OAAO,KAAK;EAC1E,WAAW,EACT,GACA,YACA,KAAS,4BACT,KAAY,+BACd;CACD,CAAA;AACS,CAAA,GAeD,KAAqC,EAChD,OACA,UACA,YACA,aACA,SACA,aACA,cACA,gBAEA,kBAAC,SAAD;CACE,SAAS;CACT,WAAW,EACT,wGACA,KAAY,iCACZ,CACF;WANF,CAQE,kBAAC,SAAD;EACM;EACJ,MAAK;EACI;EACC;EACV,WAAW,MAAqC,EAAS,EAAE,OAAO,OAAO;EACzE,eAAa;EACb,WAAU;CACX,CAAA,GACD,kBAAC,QAAD;EAAM,WAAU;YAAhB,CACE,kBAAC,QAAD;GAAM,WAAU;aAA+C;EAAY,CAAA,GAC1E,KAAQ,kBAAC,QAAD;GAAM,WAAU;aAA4C;EAAW,CAAA,CAC5E;GACD;IAsBI,KAAqC,EAChD,OACA,UACA,UACA,aACA,YACA,aACA,SACA,UACA,gBACA,aACA,mBAEA,kBAAC,GAAD;CACS;CACP,SAAS;CACC;CACJ;CACC;CACI;WAEX,kBAAC,UAAD;EACM;EACG;EACG;EACA;EACV,WAAW,MAAsC,EAAS,EAAE,OAAO,KAAK;EACxE,WAAW,EACT,GACA,KAAS,4BACT,KAAY,+BACd;YAVF,CAYG,KAAe,kBAAC,UAAD;GAAQ,OAAM;aAAI;EAAoB,CAAA,GACrD,EAAQ,KAAK,MACZ,kBAAC,UAAD;GAAwB,OAAO,EAAI;aAChC,EAAI;EACC,GAFK,EAAI,KAET,CACT,CACK;;AACE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"FileImage.js","names":[],"sources":["../../../src/coolandlovely/components/FileImage.tsx"],"sourcesContent":["/**\n * FileImage — render a wspace-files File id as an <img>.\n * @module coolandlovely/components\n *\n * Resolves the id to a short-lived presigned URL (useFileUrl, 6h TTL). Renders\n * nothing until resolved so callers can lay a glyph fallback underneath (no\n * broken <img>, no layout shift).\n */\n\nimport type { FC } from 'react';\nimport { useFileUrl } from '../hooks/useFiles';\nimport { cn } from '../utils/cn';\n\ninterface FileImageProps {\n fileId?: string | null;\n alt?: string;\n className?: string;\n}\n\nexport const FileImage: FC<FileImageProps> = ({ fileId, alt = '', className }) => {\n const { data: url } = useFileUrl(fileId);\n if (!fileId || !url) return null;\n return (\n <img\n src={url}\n alt={alt}\n loading=\"lazy\"\n className={cn('h-full w-full object-cover', className)}\n />\n );\n};\n"],"mappings":";;;;AAmBA,IAAa,KAAiC,EAAE,WAAQ,SAAM,IAAI,mBAAgB;CAChF,IAAM,EAAE,MAAM,MAAQ,EAAW,EAAO;AAExC,QADI,CAAC,KAAU,CAAC,IAAY,OAE1B,kBAAC,OAAD;EACE,KAAK;EACA;EACL,SAAQ;EACR,WAAW,EAAG,8BAA8B,EAAU;EACtD,CAAA"}
1
+ {"version":3,"file":"FileImage.js","names":[],"sources":["../../../src/coolandlovely/components/FileImage.tsx"],"sourcesContent":["/**\n * FileImage — render a wspace-files File id as an <img>.\n * @module coolandlovely/components\n *\n * Resolves the id to a short-lived presigned URL (useFileUrl, 6h TTL). Renders\n * nothing until resolved so callers can lay a glyph fallback underneath (no\n * broken <img>, no layout shift).\n */\n\nimport type { FC } from 'react';\nimport { useFileUrl } from '../hooks/useFiles';\nimport { cn } from '../utils/cn';\n\ninterface FileImageProps {\n fileId?: string | null;\n alt?: string;\n className?: string;\n}\n\nexport const FileImage: FC<FileImageProps> = ({ fileId, alt = '', className }) => {\n const { data: url } = useFileUrl(fileId);\n if (!fileId || !url) return null;\n return (\n <img\n src={url}\n alt={alt}\n loading=\"lazy\"\n className={cn('h-full w-full object-cover', className)}\n />\n );\n};\n"],"mappings":";;;;AAmBA,IAAa,KAAiC,EAAE,WAAQ,SAAM,IAAI,mBAAgB;CAChF,IAAM,EAAE,MAAM,MAAQ,EAAW,CAAM;CAEvC,OADI,CAAC,KAAU,CAAC,IAAY,OAE1B,kBAAC,OAAD;EACE,KAAK;EACA;EACL,SAAQ;EACR,WAAW,EAAG,8BAA8B,CAAS;CACtD,CAAA;AAEL"}
@@ -1 +1 @@
1
- {"version":3,"file":"FollowButton.js","names":[],"sources":["../../../src/coolandlovely/components/FollowButton.tsx"],"sourcesContent":["/**\n * FollowButton — follow / unfollow a user actor (the follow graph).\n * @module coolandlovely/components\n *\n * Backed by cooFollow / cooUnfollow / cooIsFollowing (added this release). The\n * follow target is an actor id (a look's authorId, a style-profile owner, …).\n * Self-follow is impossible on the server; callers should not render this for\n * their own id.\n */\n\nimport type { FC } from 'react';\nimport { Loader2, UserCheck, UserPlus } from 'lucide-react';\nimport { useFollow, useIsFollowing, useUnfollow } from '../hooks/useCoolAndLovelyApi';\nimport { cn } from '../utils/cn';\n\ninterface FollowButtonProps {\n /** Actor id to follow/unfollow. */\n followeeId?: string | null;\n className?: string;\n size?: 'sm' | 'md';\n}\n\nexport const FollowButton: FC<FollowButtonProps> = ({ followeeId, className, size = 'md' }) => {\n const following = useIsFollowing(followeeId ?? undefined);\n const follow = useFollow();\n const unfollow = useUnfollow();\n\n if (!followeeId) return null;\n\n const isFollowing = following.data ?? false;\n const pending = follow.isPending || unfollow.isPending || following.isLoading;\n\n const handleClick = () => {\n if (!followeeId) return;\n if (isFollowing) {\n unfollow.mutate(followeeId);\n } else {\n follow.mutate(followeeId);\n }\n };\n\n const sizing = size === 'sm' ? 'px-2.5 py-1 text-xs' : 'px-3 py-2 text-sm';\n\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={pending}\n aria-pressed={isFollowing}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-lg font-medium transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n sizing,\n isFollowing\n ? 'border border-border-seam text-text-primary hover:bg-bg-sunken'\n : 'bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90',\n className\n )}\n >\n {pending ? (\n <Loader2 className={cn('animate-spin', size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4')} />\n ) : isFollowing ? (\n <UserCheck className={size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'} />\n ) : (\n <UserPlus className={size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'} />\n )}\n {isFollowing ? 'Following' : 'Follow'}\n </button>\n );\n};\n"],"mappings":";;;;;AAsBA,IAAa,KAAuC,EAAE,eAAY,cAAW,UAAO,WAAW;CAC7F,IAAM,IAAY,EAAe,KAAc,KAAA,EAAU,EACnD,IAAS,GAAW,EACpB,IAAW,GAAa;AAE9B,KAAI,CAAC,EAAY,QAAO;CAExB,IAAM,IAAc,EAAU,QAAQ,IAChC,IAAU,EAAO,aAAa,EAAS,aAAa,EAAU;AAapE,QACE,kBAAC,UAAD;EACE,MAAK;EACL,eAdsB;AACnB,SACD,IACF,EAAS,OAAO,EAAW,GAE3B,EAAO,OAAO,EAAW;;EAUzB,UAAU;EACV,gBAAc;EACd,WAAW,EACT,8LATS,MAAS,OAAO,wBAAwB,qBAWjD,IACI,mEACA,+EACJ,EACD;YAZH,CAcG,IACC,kBAAC,GAAD,EAAS,WAAW,EAAG,gBAAgB,MAAS,OAAO,gBAAgB,UAAU,EAAI,CAAA,GAErF,EADE,IACD,IAEA,GAFD,EAAW,WAAW,MAAS,OAAO,gBAAgB,WAAa,CAED,EAEnE,IAAc,cAAc,SACtB"}
1
+ {"version":3,"file":"FollowButton.js","names":[],"sources":["../../../src/coolandlovely/components/FollowButton.tsx"],"sourcesContent":["/**\n * FollowButton — follow / unfollow a user actor (the follow graph).\n * @module coolandlovely/components\n *\n * Backed by cooFollow / cooUnfollow / cooIsFollowing (added this release). The\n * follow target is an actor id (a look's authorId, a style-profile owner, …).\n * Self-follow is impossible on the server; callers should not render this for\n * their own id.\n */\n\nimport type { FC } from 'react';\nimport { Loader2, UserCheck, UserPlus } from 'lucide-react';\nimport { useFollow, useIsFollowing, useUnfollow } from '../hooks/useCoolAndLovelyApi';\nimport { cn } from '../utils/cn';\n\ninterface FollowButtonProps {\n /** Actor id to follow/unfollow. */\n followeeId?: string | null;\n className?: string;\n size?: 'sm' | 'md';\n}\n\nexport const FollowButton: FC<FollowButtonProps> = ({ followeeId, className, size = 'md' }) => {\n const following = useIsFollowing(followeeId ?? undefined);\n const follow = useFollow();\n const unfollow = useUnfollow();\n\n if (!followeeId) return null;\n\n const isFollowing = following.data ?? false;\n const pending = follow.isPending || unfollow.isPending || following.isLoading;\n\n const handleClick = () => {\n if (!followeeId) return;\n if (isFollowing) {\n unfollow.mutate(followeeId);\n } else {\n follow.mutate(followeeId);\n }\n };\n\n const sizing = size === 'sm' ? 'px-2.5 py-1 text-xs' : 'px-3 py-2 text-sm';\n\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={pending}\n aria-pressed={isFollowing}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-lg font-medium transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n sizing,\n isFollowing\n ? 'border border-border-seam text-text-primary hover:bg-bg-sunken'\n : 'bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90',\n className\n )}\n >\n {pending ? (\n <Loader2 className={cn('animate-spin', size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4')} />\n ) : isFollowing ? (\n <UserCheck className={size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'} />\n ) : (\n <UserPlus className={size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'} />\n )}\n {isFollowing ? 'Following' : 'Follow'}\n </button>\n );\n};\n"],"mappings":";;;;;AAsBA,IAAa,KAAuC,EAAE,eAAY,cAAW,UAAO,WAAW;CAC7F,IAAM,IAAY,EAAe,KAAc,KAAA,CAAS,GAClD,IAAS,EAAU,GACnB,IAAW,EAAY;CAE7B,IAAI,CAAC,GAAY,OAAO;CAExB,IAAM,IAAc,EAAU,QAAQ,IAChC,IAAU,EAAO,aAAa,EAAS,aAAa,EAAU;CAapE,OACE,kBAAC,UAAD;EACE,MAAK;EACL,eAdsB;GACnB,MACD,IACF,EAAS,OAAO,CAAU,IAE1B,EAAO,OAAO,CAAU;EAE5B;EAQI,UAAU;EACV,gBAAc;EACd,WAAW,EACT,8LATS,MAAS,OAAO,wBAAwB,qBAWjD,IACI,mEACA,+EACJ,CACF;YAZF,CAcG,IACC,kBAAC,GAAD,EAAS,WAAW,EAAG,gBAAgB,MAAS,OAAO,gBAAgB,SAAS,EAAI,CAAA,IAEpF,EADE,IACD,IAEA,GAFD,EAAW,WAAW,MAAS,OAAO,gBAAgB,UAAY,CAED,GAElE,IAAc,cAAc,QACvB;;AAEZ"}
@@ -1 +1 @@
1
- {"version":3,"file":"FulfilmentTimeline.js","names":[],"sources":["../../../src/coolandlovely/components/FulfilmentTimeline.tsx"],"sourcesContent":["/**\n * Fulfilment timeline (BOFF-9636)\n * @module coolandlovely/components\n *\n * One brand's progress on an order (confirmed, shipped, delivered) with its tracking details.\n * Shown to the brand on its order detail and to the shopper on theirs.\n */\n\nimport { CheckCircle2, ExternalLink, Package, PackageCheck, Truck } from 'lucide-react';\n\nimport { useTr } from '../hooks';\nimport type { CooOrderFulfilment } from '../types';\nimport { cn } from '../utils/cn';\nimport { formatDate } from '../utils/formatters';\n\n/** Timeline of one brand's fulfilment, shared with the shopper's order detail. */\nexport function FulfilmentTimeline({ fulfilment }: { fulfilment: CooOrderFulfilment }) {\n const tr = useTr();\n const steps: Array<{ key: string; label: string; at: string | null; icon: typeof Package }> = [\n {\n key: 'coolandlovely.fulfilment.confirmed',\n label: 'Confirmed',\n at: fulfilment.confirmedAt,\n icon: CheckCircle2,\n },\n {\n key: 'coolandlovely.fulfilment.shipped',\n label: 'Shipped',\n at: fulfilment.shippedAt,\n icon: Truck,\n },\n {\n key: 'coolandlovely.fulfilment.delivered',\n label: 'Delivered',\n at: fulfilment.deliveredAt,\n icon: PackageCheck,\n },\n ];\n return (\n <div className=\"space-y-3\" data-testid=\"fulfilment-timeline\">\n <ol className=\"flex flex-wrap gap-4\">\n {steps.map(({ key, label, at, icon: Icon }) => (\n <li\n key={key}\n className={cn(\n 'flex items-center gap-1.5 text-sm',\n at ? 'text-text-primary' : 'text-text-secondary opacity-60'\n )}\n >\n <Icon className={cn('h-4 w-4', at && 'text-status-success-text')} />\n <span>{tr(key, label)}</span>\n {at && <span className=\"text-xs text-text-secondary\">{formatDate(at)}</span>}\n </li>\n ))}\n </ol>\n {fulfilment.trackingNumber && (\n <p className=\"text-sm text-text-primary\">\n {fulfilment.carrier} ·{' '}\n <span className=\"font-mono\" data-testid=\"fulfilment-tracking\">\n {fulfilment.trackingNumber}\n </span>\n {fulfilment.trackingUrl && (\n <a\n href={fulfilment.trackingUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"ml-2 inline-flex items-center gap-1 text-accent-primary hover:underline\"\n >\n {tr('coolandlovely.fulfilment.track', 'Track parcel')}\n <ExternalLink className=\"h-3 w-3\" />\n </a>\n )}\n </p>\n )}\n {fulfilment.note && (\n <p className=\"whitespace-pre-line text-sm text-text-secondary\">{fulfilment.note}</p>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAgBA,SAAgB,EAAmB,EAAE,iBAAkD;CACrF,IAAM,IAAK,GAAO;AAqBlB,QACE,kBAAC,OAAD;EAAK,WAAU;EAAY,eAAY;YAAvC;GACE,kBAAC,MAAD;IAAI,WAAU;cAtB4E;KAC5F;MACE,KAAK;MACL,OAAO;MACP,IAAI,EAAW;MACf,MAAM;MACP;KACD;MACE,KAAK;MACL,OAAO;MACP,IAAI,EAAW;MACf,MAAM;MACP;KACD;MACE,KAAK;MACL,OAAO;MACP,IAAI,EAAW;MACf,MAAM;MACP;KACF,CAIY,KAAK,EAAE,QAAK,UAAO,OAAI,MAAM,QAClC,kBAAC,MAAD;KAEE,WAAW,EACT,qCACA,IAAK,sBAAsB,iCAC5B;eALH;MAOE,kBAAC,GAAD,EAAM,WAAW,EAAG,WAAW,KAAM,2BAA2B,EAAI,CAAA;MACpE,kBAAC,QAAD,EAAA,UAAO,EAAG,GAAK,EAAM,EAAQ,CAAA;MAC5B,KAAM,kBAAC,QAAD;OAAM,WAAU;iBAA+B,EAAW,EAAG;OAAQ,CAAA;MACzE;OATE,EASF,CACL;IACC,CAAA;GACJ,EAAW,kBACV,kBAAC,KAAD;IAAG,WAAU;cAAb;KACG,EAAW;KAAQ;KAAG;KACvB,kBAAC,QAAD;MAAM,WAAU;MAAY,eAAY;gBACrC,EAAW;MACP,CAAA;KACN,EAAW,eACV,kBAAC,KAAD;MACE,MAAM,EAAW;MACjB,QAAO;MACP,KAAI;MACJ,WAAU;gBAJZ,CAMG,EAAG,kCAAkC,eAAe,EACrD,kBAAC,GAAD,EAAc,WAAU,WAAY,CAAA,CAClC;;KAEJ;;GAEL,EAAW,QACV,kBAAC,KAAD;IAAG,WAAU;cAAmD,EAAW;IAAS,CAAA;GAElF"}
1
+ {"version":3,"file":"FulfilmentTimeline.js","names":[],"sources":["../../../src/coolandlovely/components/FulfilmentTimeline.tsx"],"sourcesContent":["/**\n * Fulfilment timeline (BOFF-9636)\n * @module coolandlovely/components\n *\n * One brand's progress on an order (confirmed, shipped, delivered) with its tracking details.\n * Shown to the brand on its order detail and to the shopper on theirs.\n */\n\nimport { CheckCircle2, ExternalLink, Package, PackageCheck, Truck } from 'lucide-react';\n\nimport { useTr } from '../hooks';\nimport type { CooOrderFulfilment } from '../types';\nimport { cn } from '../utils/cn';\nimport { formatDate } from '../utils/formatters';\n\n/** Timeline of one brand's fulfilment, shared with the shopper's order detail. */\nexport function FulfilmentTimeline({ fulfilment }: { fulfilment: CooOrderFulfilment }) {\n const tr = useTr();\n const steps: Array<{ key: string; label: string; at: string | null; icon: typeof Package }> = [\n {\n key: 'coolandlovely.fulfilment.confirmed',\n label: 'Confirmed',\n at: fulfilment.confirmedAt,\n icon: CheckCircle2,\n },\n {\n key: 'coolandlovely.fulfilment.shipped',\n label: 'Shipped',\n at: fulfilment.shippedAt,\n icon: Truck,\n },\n {\n key: 'coolandlovely.fulfilment.delivered',\n label: 'Delivered',\n at: fulfilment.deliveredAt,\n icon: PackageCheck,\n },\n ];\n return (\n <div className=\"space-y-3\" data-testid=\"fulfilment-timeline\">\n <ol className=\"flex flex-wrap gap-4\">\n {steps.map(({ key, label, at, icon: Icon }) => (\n <li\n key={key}\n className={cn(\n 'flex items-center gap-1.5 text-sm',\n at ? 'text-text-primary' : 'text-text-secondary opacity-60'\n )}\n >\n <Icon className={cn('h-4 w-4', at && 'text-status-success-text')} />\n <span>{tr(key, label)}</span>\n {at && <span className=\"text-xs text-text-secondary\">{formatDate(at)}</span>}\n </li>\n ))}\n </ol>\n {fulfilment.trackingNumber && (\n <p className=\"text-sm text-text-primary\">\n {fulfilment.carrier} ·{' '}\n <span className=\"font-mono\" data-testid=\"fulfilment-tracking\">\n {fulfilment.trackingNumber}\n </span>\n {fulfilment.trackingUrl && (\n <a\n href={fulfilment.trackingUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"ml-2 inline-flex items-center gap-1 text-accent-primary hover:underline\"\n >\n {tr('coolandlovely.fulfilment.track', 'Track parcel')}\n <ExternalLink className=\"h-3 w-3\" />\n </a>\n )}\n </p>\n )}\n {fulfilment.note && (\n <p className=\"whitespace-pre-line text-sm text-text-secondary\">{fulfilment.note}</p>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAgBA,SAAgB,EAAmB,EAAE,iBAAkD;CACrF,IAAM,IAAK,EAAM;CAqBjB,OACE,kBAAC,OAAD;EAAK,WAAU;EAAY,eAAY;YAAvC;GACE,kBAAC,MAAD;IAAI,WAAU;cACX;KAtBL;MACE,KAAK;MACL,OAAO;MACP,IAAI,EAAW;MACf,MAAM;KACR;KACA;MACE,KAAK;MACL,OAAO;MACP,IAAI,EAAW;MACf,MAAM;KACR;KACA;MACE,KAAK;MACL,OAAO;MACP,IAAI,EAAW;MACf,MAAM;KACR;IAKK,CAAA,CAAM,KAAK,EAAE,QAAK,UAAO,OAAI,MAAM,QAClC,kBAAC,MAAD;KAEE,WAAW,EACT,qCACA,IAAK,sBAAsB,gCAC7B;eALF;MAOE,kBAAC,GAAD,EAAM,WAAW,EAAG,WAAW,KAAM,0BAA0B,EAAI,CAAA;MACnE,kBAAC,QAAD,EAAA,UAAO,EAAG,GAAK,CAAK,EAAQ,CAAA;MAC3B,KAAM,kBAAC,QAAD;OAAM,WAAU;iBAA+B,EAAW,CAAE;MAAQ,CAAA;KACzE;OATG,CASH,CACL;GACC,CAAA;GACH,EAAW,kBACV,kBAAC,KAAD;IAAG,WAAU;cAAb;KACG,EAAW;KAAQ;KAAG;KACvB,kBAAC,QAAD;MAAM,WAAU;MAAY,eAAY;gBACrC,EAAW;KACR,CAAA;KACL,EAAW,eACV,kBAAC,KAAD;MACE,MAAM,EAAW;MACjB,QAAO;MACP,KAAI;MACJ,WAAU;gBAJZ,CAMG,EAAG,kCAAkC,cAAc,GACpD,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA,CAClC;;IAEJ;;GAEJ,EAAW,QACV,kBAAC,KAAD;IAAG,WAAU;cAAmD,EAAW;GAAQ,CAAA;EAElF;;AAET"}
@@ -26,7 +26,9 @@ var _ = ({ label: _, value: v, onChange: y, folder: b, hint: x, accept: S = "ima
26
26
  B("That image is over 10 MB — please choose a smaller file.");
27
27
  return;
28
28
  }
29
- B(void 0), P(e.name), I(URL.createObjectURL(e)), M({
29
+ B(void 0), P(e.name);
30
+ let t = URL.createObjectURL(e);
31
+ I(t), M({
30
32
  loaded: 0,
31
33
  total: e.size,
32
34
  pct: 0
@@ -1 +1 @@
1
- {"version":3,"file":"ImageUploadField.js","names":[],"sources":["../../../src/coolandlovely/components/ImageUploadField.tsx"],"sourcesContent":["/**\n * ImageUploadField — image upload for a coo* entity, backed by wspace-files-svc.\n * @module coolandlovely/components\n *\n * Controlled by the current File id (`value`). Resting state is a real\n * drag-and-drop dropzone (dashed border, cloud-upload glyph, size/type hint);\n * dropping or browsing runs the shared presigned upload (initiateUpload -> PUT\n * -> completeUpload) via `uploadImageToFilesWithProgress`, showing a live rose\n * progress bar. On success the new File id is handed back via `onChange`; the\n * caller persists it (e.g. logoFileId). Clearing sends onChange('').\n *\n * Inline (not a modal), token-only, no Apollo, no toast dependency — surfaces\n * its own errors inline to match the existing form error style.\n */\n\nimport type { DragEvent, FC } from 'react';\nimport { useEffect, useId, useRef, useState } from 'react';\nimport { ImagePlus, Loader2, UploadCloud, X } from 'lucide-react';\nimport { FieldShell } from './Field';\nimport { useCoolAndLovely } from '../context/CoolAndLovelyContext';\nimport { uploadImageToFilesWithProgress, useFileUrl, type UploadProgress } from '../hooks/useFiles';\nimport { cn } from '../utils/cn';\n\ninterface ImageUploadFieldProps {\n label: string;\n /** Current wspace-files File id, or '' / undefined when none. */\n value?: string | null;\n /** Called with the new File id on success, or '' when cleared. */\n onChange: (fileId: string) => void;\n /** Destination folder in wspace-files (see COO_FILE_FOLDERS). */\n folder?: string;\n hint?: string;\n /** `accept` for the input; images only by default. */\n accept?: string;\n disabled?: boolean;\n className?: string;\n /** Aspect of the resolved-image preview tile. */\n aspect?: 'square' | 'wide';\n}\n\n/** 10 MB — the mock's stated ceiling and a sensible cap for garment photos. */\nconst MAX_BYTES = 10 * 1024 * 1024;\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n const kb = bytes / 1024;\n if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`;\n const mb = kb / 1024;\n return `${mb.toFixed(1)} MB`;\n}\n\nexport const ImageUploadField: FC<ImageUploadFieldProps> = ({\n label,\n value,\n onChange,\n folder,\n hint,\n accept = 'image/*',\n disabled = false,\n className,\n aspect = 'square',\n}) => {\n const { apiGatewayUrl } = useCoolAndLovely();\n const inputRef = useRef<HTMLInputElement | null>(null);\n const inputId = useId();\n const [uploading, setUploading] = useState(false);\n const [progress, setProgress] = useState<UploadProgress | null>(null);\n const [pendingName, setPendingName] = useState<string | null>(null);\n const [pendingPreview, setPendingPreview] = useState<string | null>(null);\n const [dragActive, setDragActive] = useState(false);\n const [error, setError] = useState<string | undefined>();\n\n // Revoke the local object-URL preview when it changes / on unmount.\n useEffect(() => {\n return () => {\n if (pendingPreview) URL.revokeObjectURL(pendingPreview);\n };\n }, [pendingPreview]);\n\n // While uploading we skip resolution; the preview shows after onChange lands.\n const resolved = useFileUrl(uploading ? undefined : value || undefined);\n const previewUrl = resolved.data ?? null;\n const locked = disabled || uploading;\n\n const upload = async (file: File) => {\n // Validate before opening a session so bad files never hit the network.\n if (!file.type.startsWith('image/')) {\n setError('Please choose an image file (JPG or PNG).');\n return;\n }\n if (file.size > MAX_BYTES) {\n setError('That image is over 10 MB — please choose a smaller file.');\n return;\n }\n\n setError(undefined);\n setPendingName(file.name);\n const objectUrl = URL.createObjectURL(file);\n setPendingPreview(objectUrl);\n setProgress({ loaded: 0, total: file.size, pct: 0 });\n setUploading(true);\n try {\n const uploaded = await uploadImageToFilesWithProgress(file, apiGatewayUrl, folder, (p) =>\n setProgress(p)\n );\n onChange(uploaded.id);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Upload failed — please try again.');\n } finally {\n setUploading(false);\n setProgress(null);\n setPendingName(null);\n setPendingPreview(null);\n }\n };\n\n const handleSelect = (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n event.target.value = ''; // allow re-selecting the same file\n if (file) void upload(file);\n };\n\n const handleDrop = (event: DragEvent<HTMLDivElement>) => {\n event.preventDefault();\n setDragActive(false);\n if (locked) return;\n const file = event.dataTransfer.files?.[0];\n if (file) void upload(file);\n };\n\n const handleDragOver = (event: DragEvent<HTMLDivElement>) => {\n event.preventDefault();\n if (!locked) setDragActive(true);\n };\n\n const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {\n event.preventDefault();\n setDragActive(false);\n };\n\n const pct = progress?.pct ?? 0;\n\n return (\n <FieldShell label={label} htmlFor={inputId} hint={hint} error={error} className={className}>\n <div\n onDragEnter={handleDragOver}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n >\n {uploading ? (\n /* Uploading — file row with preview, filename and a rose progress bar. */\n <div className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3\">\n <div className=\"flex h-14 w-14 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-bg-sunken\">\n {pendingPreview ? (\n <img src={pendingPreview} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : (\n <Loader2 className=\"h-5 w-5 animate-spin text-text-secondary\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {pendingName ?? 'Uploading…'}\n </p>\n <div className=\"mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-accent-primary transition-[width] duration-200\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary tabular-nums\">\n {pct}%\n {progress\n ? ` · ${formatBytes(progress.loaded)} of ${formatBytes(progress.total)}`\n : ''}\n </p>\n </div>\n </div>\n ) : previewUrl ? (\n /* Resolved image — preview with Replace / Remove actions. */\n <div className=\"relative overflow-hidden rounded-lg border border-border-seam bg-bg-sunken\">\n <img\n src={previewUrl}\n alt=\"\"\n className={cn(\n 'w-full object-cover',\n aspect === 'wide' ? 'aspect-video' : 'aspect-square max-h-64'\n )}\n />\n <div className=\"absolute inset-x-0 bottom-0 flex justify-end gap-2 bg-gradient-to-t from-bg-canvas/85 to-transparent p-2\">\n <button\n type=\"button\"\n disabled={locked}\n onClick={() => inputRef.current?.click()}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-bg-surface/90 px-3 py-1.5 text-sm font-medium text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <ImagePlus className=\"h-4 w-4\" /> Replace\n </button>\n <button\n type=\"button\"\n disabled={disabled}\n onClick={() => onChange('')}\n className=\"inline-flex items-center gap-1 rounded-lg bg-bg-surface/90 px-3 py-1.5 text-sm text-text-secondary shadow-sm backdrop-blur transition-colors hover:bg-status-error-bg-subtle hover:text-status-error-text disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <X className=\"h-4 w-4\" /> Remove\n </button>\n </div>\n </div>\n ) : (\n /* Empty — a real dropzone. */\n <button\n type=\"button\"\n disabled={locked}\n onClick={() => inputRef.current?.click()}\n className={cn(\n 'flex min-h-40 w-full flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 text-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-50',\n dragActive\n ? 'border-accent-primary bg-[var(--color-accent-soft)]'\n : 'border-border-seam hover:border-accent-primary/60 hover:bg-bg-sunken'\n )}\n >\n <UploadCloud className=\"h-8 w-8 text-text-secondary\" aria-hidden=\"true\" />\n <span className=\"text-sm font-medium text-text-primary\">\n Drag &amp; drop an image here, or click to browse\n </span>\n <span className=\"text-xs text-text-secondary\">JPG · PNG · up to 10 MB</span>\n </button>\n )}\n </div>\n <input\n ref={inputRef}\n id={inputId}\n type=\"file\"\n accept={accept}\n className=\"sr-only\"\n aria-label={label}\n onChange={handleSelect}\n />\n </FieldShell>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"ImageUploadField.js","names":[],"sources":["../../../src/coolandlovely/components/ImageUploadField.tsx"],"sourcesContent":["/**\n * ImageUploadField — image upload for a coo* entity, backed by wspace-files-svc.\n * @module coolandlovely/components\n *\n * Controlled by the current File id (`value`). Resting state is a real\n * drag-and-drop dropzone (dashed border, cloud-upload glyph, size/type hint);\n * dropping or browsing runs the shared presigned upload (initiateUpload -> PUT\n * -> completeUpload) via `uploadImageToFilesWithProgress`, showing a live rose\n * progress bar. On success the new File id is handed back via `onChange`; the\n * caller persists it (e.g. logoFileId). Clearing sends onChange('').\n *\n * Inline (not a modal), token-only, no Apollo, no toast dependency — surfaces\n * its own errors inline to match the existing form error style.\n */\n\nimport type { DragEvent, FC } from 'react';\nimport { useEffect, useId, useRef, useState } from 'react';\nimport { ImagePlus, Loader2, UploadCloud, X } from 'lucide-react';\nimport { FieldShell } from './Field';\nimport { useCoolAndLovely } from '../context/CoolAndLovelyContext';\nimport { uploadImageToFilesWithProgress, useFileUrl, type UploadProgress } from '../hooks/useFiles';\nimport { cn } from '../utils/cn';\n\ninterface ImageUploadFieldProps {\n label: string;\n /** Current wspace-files File id, or '' / undefined when none. */\n value?: string | null;\n /** Called with the new File id on success, or '' when cleared. */\n onChange: (fileId: string) => void;\n /** Destination folder in wspace-files (see COO_FILE_FOLDERS). */\n folder?: string;\n hint?: string;\n /** `accept` for the input; images only by default. */\n accept?: string;\n disabled?: boolean;\n className?: string;\n /** Aspect of the resolved-image preview tile. */\n aspect?: 'square' | 'wide';\n}\n\n/** 10 MB — the mock's stated ceiling and a sensible cap for garment photos. */\nconst MAX_BYTES = 10 * 1024 * 1024;\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n const kb = bytes / 1024;\n if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`;\n const mb = kb / 1024;\n return `${mb.toFixed(1)} MB`;\n}\n\nexport const ImageUploadField: FC<ImageUploadFieldProps> = ({\n label,\n value,\n onChange,\n folder,\n hint,\n accept = 'image/*',\n disabled = false,\n className,\n aspect = 'square',\n}) => {\n const { apiGatewayUrl } = useCoolAndLovely();\n const inputRef = useRef<HTMLInputElement | null>(null);\n const inputId = useId();\n const [uploading, setUploading] = useState(false);\n const [progress, setProgress] = useState<UploadProgress | null>(null);\n const [pendingName, setPendingName] = useState<string | null>(null);\n const [pendingPreview, setPendingPreview] = useState<string | null>(null);\n const [dragActive, setDragActive] = useState(false);\n const [error, setError] = useState<string | undefined>();\n\n // Revoke the local object-URL preview when it changes / on unmount.\n useEffect(() => {\n return () => {\n if (pendingPreview) URL.revokeObjectURL(pendingPreview);\n };\n }, [pendingPreview]);\n\n // While uploading we skip resolution; the preview shows after onChange lands.\n const resolved = useFileUrl(uploading ? undefined : value || undefined);\n const previewUrl = resolved.data ?? null;\n const locked = disabled || uploading;\n\n const upload = async (file: File) => {\n // Validate before opening a session so bad files never hit the network.\n if (!file.type.startsWith('image/')) {\n setError('Please choose an image file (JPG or PNG).');\n return;\n }\n if (file.size > MAX_BYTES) {\n setError('That image is over 10 MB — please choose a smaller file.');\n return;\n }\n\n setError(undefined);\n setPendingName(file.name);\n const objectUrl = URL.createObjectURL(file);\n setPendingPreview(objectUrl);\n setProgress({ loaded: 0, total: file.size, pct: 0 });\n setUploading(true);\n try {\n const uploaded = await uploadImageToFilesWithProgress(file, apiGatewayUrl, folder, (p) =>\n setProgress(p)\n );\n onChange(uploaded.id);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Upload failed — please try again.');\n } finally {\n setUploading(false);\n setProgress(null);\n setPendingName(null);\n setPendingPreview(null);\n }\n };\n\n const handleSelect = (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n event.target.value = ''; // allow re-selecting the same file\n if (file) void upload(file);\n };\n\n const handleDrop = (event: DragEvent<HTMLDivElement>) => {\n event.preventDefault();\n setDragActive(false);\n if (locked) return;\n const file = event.dataTransfer.files?.[0];\n if (file) void upload(file);\n };\n\n const handleDragOver = (event: DragEvent<HTMLDivElement>) => {\n event.preventDefault();\n if (!locked) setDragActive(true);\n };\n\n const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {\n event.preventDefault();\n setDragActive(false);\n };\n\n const pct = progress?.pct ?? 0;\n\n return (\n <FieldShell label={label} htmlFor={inputId} hint={hint} error={error} className={className}>\n <div\n onDragEnter={handleDragOver}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n >\n {uploading ? (\n /* Uploading — file row with preview, filename and a rose progress bar. */\n <div className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3\">\n <div className=\"flex h-14 w-14 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-bg-sunken\">\n {pendingPreview ? (\n <img src={pendingPreview} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : (\n <Loader2 className=\"h-5 w-5 animate-spin text-text-secondary\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {pendingName ?? 'Uploading…'}\n </p>\n <div className=\"mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-accent-primary transition-[width] duration-200\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary tabular-nums\">\n {pct}%\n {progress\n ? ` · ${formatBytes(progress.loaded)} of ${formatBytes(progress.total)}`\n : ''}\n </p>\n </div>\n </div>\n ) : previewUrl ? (\n /* Resolved image — preview with Replace / Remove actions. */\n <div className=\"relative overflow-hidden rounded-lg border border-border-seam bg-bg-sunken\">\n <img\n src={previewUrl}\n alt=\"\"\n className={cn(\n 'w-full object-cover',\n aspect === 'wide' ? 'aspect-video' : 'aspect-square max-h-64'\n )}\n />\n <div className=\"absolute inset-x-0 bottom-0 flex justify-end gap-2 bg-gradient-to-t from-bg-canvas/85 to-transparent p-2\">\n <button\n type=\"button\"\n disabled={locked}\n onClick={() => inputRef.current?.click()}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-bg-surface/90 px-3 py-1.5 text-sm font-medium text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <ImagePlus className=\"h-4 w-4\" /> Replace\n </button>\n <button\n type=\"button\"\n disabled={disabled}\n onClick={() => onChange('')}\n className=\"inline-flex items-center gap-1 rounded-lg bg-bg-surface/90 px-3 py-1.5 text-sm text-text-secondary shadow-sm backdrop-blur transition-colors hover:bg-status-error-bg-subtle hover:text-status-error-text disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <X className=\"h-4 w-4\" /> Remove\n </button>\n </div>\n </div>\n ) : (\n /* Empty — a real dropzone. */\n <button\n type=\"button\"\n disabled={locked}\n onClick={() => inputRef.current?.click()}\n className={cn(\n 'flex min-h-40 w-full flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 text-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-50',\n dragActive\n ? 'border-accent-primary bg-[var(--color-accent-soft)]'\n : 'border-border-seam hover:border-accent-primary/60 hover:bg-bg-sunken'\n )}\n >\n <UploadCloud className=\"h-8 w-8 text-text-secondary\" aria-hidden=\"true\" />\n <span className=\"text-sm font-medium text-text-primary\">\n Drag &amp; drop an image here, or click to browse\n </span>\n <span className=\"text-xs text-text-secondary\">JPG · PNG · up to 10 MB</span>\n </button>\n )}\n </div>\n <input\n ref={inputRef}\n id={inputId}\n type=\"file\"\n accept={accept}\n className=\"sr-only\"\n aria-label={label}\n onChange={handleSelect}\n />\n </FieldShell>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"LookCard.js","names":[],"sources":["../../../src/coolandlovely/components/LookCard.tsx"],"sourcesContent":["/**\n * LookCard — an editorial, image-forward card for a look.\n * @module coolandlovely/components\n *\n * A portrait `Thumbnail` (the look photo) with a frosted bottom caption (title +\n * occasion pill) and, when `showActions`, a hover-revealed top-right cluster: a\n * true toggle Like heart (optimistic, via `useLikeLook`/`useUnlikeLook`), a\n * Save bookmark (optimistic, via `useSaveLook`/`useUnsaveLook` — distinct from\n * both liking and clipping into a named lookbook) + `AddToLookbookButton`.\n * Reused by the home latest-looks, Discover, the looks list and the\n * collaboration attributed-looks grid. Token-only; reuses `Thumbnail`/\n * `FileImage` for imagery.\n */\n\nimport type { FC } from 'react';\nimport { useEffect, useState } from 'react';\nimport { Bookmark, Heart, Sparkles } from 'lucide-react';\nimport { Thumbnail } from './Thumbnail';\nimport { AddToLookbookButton } from './AddToLookbookButton';\nimport {\n useLikeLook,\n useSaveLook,\n useUnlikeLook,\n useUnsaveLook,\n} from '../hooks/useCoolAndLovelyApi';\nimport { titleCase } from '../utils/formatters';\nimport { cn } from '../utils/cn';\n\n/** Optimistic, true-toggle like heart (cooLikeLook / cooUnlikeLook). */\nconst LikeHeart: FC<{ lookId: string; likeCount: number; isLikedByMe?: boolean }> = ({\n lookId,\n likeCount,\n isLikedByMe,\n}) => {\n const like = useLikeLook();\n const unlike = useUnlikeLook();\n const [liked, setLiked] = useState(Boolean(isLikedByMe));\n const [count, setCount] = useState(likeCount);\n const pending = like.isPending || unlike.isPending;\n\n // Keep in sync with the server state as long as no toggle is in flight.\n useEffect(() => {\n if (!pending) {\n setLiked(Boolean(isLikedByMe));\n setCount(likeCount);\n }\n }, [likeCount, isLikedByMe, pending]);\n\n const handleClick = () => {\n if (pending) return;\n const next = !liked;\n setLiked(next);\n setCount((current) => current + (next ? 1 : -1));\n const mutation = next ? like : unlike;\n mutation.mutate(lookId, {\n onSuccess: (look) => {\n setLiked(Boolean(look.isLikedByMe));\n setCount(look.likeCount);\n },\n onError: () => {\n setLiked(!next);\n setCount(likeCount);\n },\n });\n };\n\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={pending}\n aria-pressed={liked}\n aria-label={liked ? 'Unlike this look' : 'Like this look'}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-surface/90 px-2 py-1 text-xs font-medium text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface disabled:opacity-60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Heart\n className={cn(\n 'h-3.5 w-3.5',\n liked ? 'fill-current text-accent-primary' : 'text-text-secondary'\n )}\n />\n <span className=\"tabular-nums\">{count}</span>\n </button>\n );\n};\n\n/** Optimistic, true-toggle save/bookmark (cooSaveLook / cooUnsaveLook). */\nconst SaveButton: FC<{ lookId: string; isSavedByMe?: boolean }> = ({ lookId, isSavedByMe }) => {\n const save = useSaveLook();\n const unsave = useUnsaveLook();\n const [saved, setSaved] = useState(Boolean(isSavedByMe));\n const pending = save.isPending || unsave.isPending;\n\n useEffect(() => {\n if (!pending) setSaved(Boolean(isSavedByMe));\n }, [isSavedByMe, pending]);\n\n const handleClick = () => {\n if (pending) return;\n const next = !saved;\n setSaved(next);\n const mutation = next ? save : unsave;\n mutation.mutate(lookId, {\n onSuccess: (look) => setSaved(Boolean(look.isSavedByMe)),\n onError: () => setSaved(!next),\n });\n };\n\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={pending}\n aria-pressed={saved}\n aria-label={saved ? 'Remove from saved' : 'Save this look'}\n className=\"inline-flex items-center justify-center rounded-full bg-bg-surface/90 p-1.5 text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface disabled:opacity-60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Bookmark\n className={cn(\n 'h-3.5 w-3.5',\n saved ? 'fill-current text-accent-primary' : 'text-text-secondary'\n )}\n />\n </button>\n );\n};\n\ninterface LookCardProps {\n id: string;\n title: string;\n /** Occasion label/value (rendered as a pill; enum values are title-cased). */\n occasion: string;\n imageFileId?: string | null;\n likeCount?: number;\n isLikedByMe?: boolean;\n isSavedByMe?: boolean;\n onOpen: () => void;\n /** Show the hover like/save/add-to-lookbook overlay cluster. */\n showActions?: boolean;\n className?: string;\n}\n\nexport const LookCard: FC<LookCardProps> = ({\n id,\n title,\n occasion,\n imageFileId,\n likeCount,\n isLikedByMe,\n isSavedByMe,\n onOpen,\n showActions = false,\n className,\n}) => {\n return (\n <div\n className={cn(\n 'group relative overflow-hidden rounded-xl border border-border-seam bg-bg-surface transition-shadow hover:shadow-lg',\n className\n )}\n >\n <button\n type=\"button\"\n onClick={onOpen}\n aria-label={`Open look ${title}`}\n className=\"block w-full text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Thumbnail\n icon={<Sparkles className=\"h-8 w-8\" />}\n seed={id}\n fileId={imageFileId}\n alt={title}\n className=\"aspect-[3/4] w-full rounded-none\"\n overlay={\n <div className=\"pointer-events-none absolute inset-x-0 bottom-0 flex flex-col gap-1.5 bg-gradient-to-t from-bg-canvas/90 via-bg-canvas/45 to-transparent p-3 pt-10\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">{title}</p>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"inline-flex items-center rounded-full bg-bg-surface/85 px-2 py-0.5 text-[11px] font-medium text-text-primary backdrop-blur\">\n {titleCase(occasion)}\n </span>\n {!showActions && typeof likeCount === 'number' && likeCount > 0 ? (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-secondary\">\n <Heart className=\"h-3.5 w-3.5 fill-current text-accent-primary\" />\n <span className=\"tabular-nums\">{likeCount}</span>\n </span>\n ) : null}\n </div>\n </div>\n }\n />\n </button>\n\n {showActions ? (\n <div className=\"absolute right-2 top-2 flex items-center gap-1.5 opacity-100 transition-opacity duration-200 sm:opacity-0 sm:focus-within:opacity-100 sm:group-hover:opacity-100\">\n <LikeHeart lookId={id} likeCount={likeCount ?? 0} isLikedByMe={isLikedByMe} />\n <SaveButton lookId={id} isSavedByMe={isSavedByMe} />\n <AddToLookbookButton lookId={id} />\n </div>\n ) : null}\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"LookCard.js","names":[],"sources":["../../../src/coolandlovely/components/LookCard.tsx"],"sourcesContent":["/**\n * LookCard — an editorial, image-forward card for a look.\n * @module coolandlovely/components\n *\n * A portrait `Thumbnail` (the look photo) with a frosted bottom caption (title +\n * occasion pill) and, when `showActions`, a hover-revealed top-right cluster: a\n * true toggle Like heart (optimistic, via `useLikeLook`/`useUnlikeLook`), a\n * Save bookmark (optimistic, via `useSaveLook`/`useUnsaveLook` — distinct from\n * both liking and clipping into a named lookbook) + `AddToLookbookButton`.\n * Reused by the home latest-looks, Discover, the looks list and the\n * collaboration attributed-looks grid. Token-only; reuses `Thumbnail`/\n * `FileImage` for imagery.\n */\n\nimport type { FC } from 'react';\nimport { useEffect, useState } from 'react';\nimport { Bookmark, Heart, Sparkles } from 'lucide-react';\nimport { Thumbnail } from './Thumbnail';\nimport { AddToLookbookButton } from './AddToLookbookButton';\nimport {\n useLikeLook,\n useSaveLook,\n useUnlikeLook,\n useUnsaveLook,\n} from '../hooks/useCoolAndLovelyApi';\nimport { titleCase } from '../utils/formatters';\nimport { cn } from '../utils/cn';\n\n/** Optimistic, true-toggle like heart (cooLikeLook / cooUnlikeLook). */\nconst LikeHeart: FC<{ lookId: string; likeCount: number; isLikedByMe?: boolean }> = ({\n lookId,\n likeCount,\n isLikedByMe,\n}) => {\n const like = useLikeLook();\n const unlike = useUnlikeLook();\n const [liked, setLiked] = useState(Boolean(isLikedByMe));\n const [count, setCount] = useState(likeCount);\n const pending = like.isPending || unlike.isPending;\n\n // Keep in sync with the server state as long as no toggle is in flight.\n useEffect(() => {\n if (!pending) {\n setLiked(Boolean(isLikedByMe));\n setCount(likeCount);\n }\n }, [likeCount, isLikedByMe, pending]);\n\n const handleClick = () => {\n if (pending) return;\n const next = !liked;\n setLiked(next);\n setCount((current) => current + (next ? 1 : -1));\n const mutation = next ? like : unlike;\n mutation.mutate(lookId, {\n onSuccess: (look) => {\n setLiked(Boolean(look.isLikedByMe));\n setCount(look.likeCount);\n },\n onError: () => {\n setLiked(!next);\n setCount(likeCount);\n },\n });\n };\n\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={pending}\n aria-pressed={liked}\n aria-label={liked ? 'Unlike this look' : 'Like this look'}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-surface/90 px-2 py-1 text-xs font-medium text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface disabled:opacity-60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Heart\n className={cn(\n 'h-3.5 w-3.5',\n liked ? 'fill-current text-accent-primary' : 'text-text-secondary'\n )}\n />\n <span className=\"tabular-nums\">{count}</span>\n </button>\n );\n};\n\n/** Optimistic, true-toggle save/bookmark (cooSaveLook / cooUnsaveLook). */\nconst SaveButton: FC<{ lookId: string; isSavedByMe?: boolean }> = ({ lookId, isSavedByMe }) => {\n const save = useSaveLook();\n const unsave = useUnsaveLook();\n const [saved, setSaved] = useState(Boolean(isSavedByMe));\n const pending = save.isPending || unsave.isPending;\n\n useEffect(() => {\n if (!pending) setSaved(Boolean(isSavedByMe));\n }, [isSavedByMe, pending]);\n\n const handleClick = () => {\n if (pending) return;\n const next = !saved;\n setSaved(next);\n const mutation = next ? save : unsave;\n mutation.mutate(lookId, {\n onSuccess: (look) => setSaved(Boolean(look.isSavedByMe)),\n onError: () => setSaved(!next),\n });\n };\n\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={pending}\n aria-pressed={saved}\n aria-label={saved ? 'Remove from saved' : 'Save this look'}\n className=\"inline-flex items-center justify-center rounded-full bg-bg-surface/90 p-1.5 text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface disabled:opacity-60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Bookmark\n className={cn(\n 'h-3.5 w-3.5',\n saved ? 'fill-current text-accent-primary' : 'text-text-secondary'\n )}\n />\n </button>\n );\n};\n\ninterface LookCardProps {\n id: string;\n title: string;\n /** Occasion label/value (rendered as a pill; enum values are title-cased). */\n occasion: string;\n imageFileId?: string | null;\n likeCount?: number;\n isLikedByMe?: boolean;\n isSavedByMe?: boolean;\n onOpen: () => void;\n /** Show the hover like/save/add-to-lookbook overlay cluster. */\n showActions?: boolean;\n className?: string;\n}\n\nexport const LookCard: FC<LookCardProps> = ({\n id,\n title,\n occasion,\n imageFileId,\n likeCount,\n isLikedByMe,\n isSavedByMe,\n onOpen,\n showActions = false,\n className,\n}) => {\n return (\n <div\n className={cn(\n 'group relative overflow-hidden rounded-xl border border-border-seam bg-bg-surface transition-shadow hover:shadow-lg',\n className\n )}\n >\n <button\n type=\"button\"\n onClick={onOpen}\n aria-label={`Open look ${title}`}\n className=\"block w-full text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Thumbnail\n icon={<Sparkles className=\"h-8 w-8\" />}\n seed={id}\n fileId={imageFileId}\n alt={title}\n className=\"aspect-[3/4] w-full rounded-none\"\n overlay={\n <div className=\"pointer-events-none absolute inset-x-0 bottom-0 flex flex-col gap-1.5 bg-gradient-to-t from-bg-canvas/90 via-bg-canvas/45 to-transparent p-3 pt-10\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">{title}</p>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"inline-flex items-center rounded-full bg-bg-surface/85 px-2 py-0.5 text-[11px] font-medium text-text-primary backdrop-blur\">\n {titleCase(occasion)}\n </span>\n {!showActions && typeof likeCount === 'number' && likeCount > 0 ? (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-secondary\">\n <Heart className=\"h-3.5 w-3.5 fill-current text-accent-primary\" />\n <span className=\"tabular-nums\">{likeCount}</span>\n </span>\n ) : null}\n </div>\n </div>\n }\n />\n </button>\n\n {showActions ? (\n <div className=\"absolute right-2 top-2 flex items-center gap-1.5 opacity-100 transition-opacity duration-200 sm:opacity-0 sm:focus-within:opacity-100 sm:group-hover:opacity-100\">\n <LikeHeart lookId={id} likeCount={likeCount ?? 0} isLikedByMe={isLikedByMe} />\n <SaveButton lookId={id} isSavedByMe={isSavedByMe} />\n <AddToLookbookButton lookId={id} />\n </div>\n ) : null}\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"LookbookCard.js","names":[],"sources":["../../../src/coolandlovely/components/LookbookCard.tsx"],"sourcesContent":["/**\n * LookbookCard — a themed board card for list/grids.\n * @module coolandlovely/components\n *\n * Cover image, name, item count, visibility chip, description and tags.\n * Extracted from LookbooksListPage for isolated rendering/tests.\n */\n\nimport type { FC } from 'react';\nimport { BookHeart } from 'lucide-react';\nimport { Thumbnail } from './Thumbnail';\nimport { TagList } from './TagList';\nimport { VisibilityChip } from './VisibilityChip';\nimport { cn } from '../utils/cn';\nimport type { CooLookbook } from '../types';\n\ninterface LookbookCardProps {\n lookbook: CooLookbook;\n onClick?: () => void;\n className?: string;\n}\n\nexport const LookbookCard: FC<LookbookCardProps> = ({ lookbook, onClick, className }) => {\n const entryCount = Array.isArray(lookbook.entries) ? lookbook.entries.length : null;\n\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={`Open lookbook ${lookbook.name}`}\n className={cn(\n '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-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-focus-ring)]',\n className\n )}\n >\n <Thumbnail\n icon={<BookHeart className=\"h-10 w-10\" />}\n seed={lookbook.id}\n fileId={lookbook.cover?.id}\n alt={lookbook.name}\n className=\"aspect-[4/3] w-full rounded-none\"\n />\n <div className=\"flex flex-1 flex-col p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">{lookbook.name}</h3>\n {entryCount !== null && (\n <p className=\"mt-0.5 text-xs text-text-secondary tabular-nums\">\n {entryCount} {entryCount === 1 ? 'item' : 'items'}\n </p>\n )}\n </div>\n <VisibilityChip visibility={lookbook.visibility} className=\"shrink-0\" />\n </div>\n {lookbook.description && (\n <p className=\"mt-1.5 line-clamp-2 text-sm text-text-secondary\">{lookbook.description}</p>\n )}\n {lookbook.tags.length > 0 && <TagList tags={lookbook.tags} max={4} className=\"mt-3\" />}\n </div>\n </button>\n );\n};\n"],"mappings":";;;;;;;AAsBA,IAAa,KAAuC,EAAE,aAAU,YAAS,mBAAgB;CACvF,IAAM,IAAa,MAAM,QAAQ,EAAS,QAAQ,GAAG,EAAS,QAAQ,SAAS;AAE/E,QACE,kBAAC,UAAD;EACE,MAAK;EACI;EACT,cAAY,iBAAiB,EAAS;EACtC,WAAW,EACT,qSACA,EACD;YAPH,CASE,kBAAC,GAAD;GACE,MAAM,kBAAC,GAAD,EAAW,WAAU,aAAc,CAAA;GACzC,MAAM,EAAS;GACf,QAAQ,EAAS,OAAO;GACxB,KAAK,EAAS;GACd,WAAU;GACV,CAAA,EACF,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAsD,EAAS;OAAU,CAAA,EACtF,MAAe,QACd,kBAAC,KAAD;OAAG,WAAU;iBAAb;QACG;QAAW;QAAE,MAAe,IAAI,SAAS;QACxC;SAEF;SACN,kBAAC,GAAD;MAAgB,YAAY,EAAS;MAAY,WAAU;MAAa,CAAA,CACpE;;IACL,EAAS,eACR,kBAAC,KAAD;KAAG,WAAU;eAAmD,EAAS;KAAgB,CAAA;IAE1F,EAAS,KAAK,SAAS,KAAK,kBAAC,GAAD;KAAS,MAAM,EAAS;KAAM,KAAK;KAAG,WAAU;KAAS,CAAA;IAClF;KACC"}
1
+ {"version":3,"file":"LookbookCard.js","names":[],"sources":["../../../src/coolandlovely/components/LookbookCard.tsx"],"sourcesContent":["/**\n * LookbookCard — a themed board card for list/grids.\n * @module coolandlovely/components\n *\n * Cover image, name, item count, visibility chip, description and tags.\n * Extracted from LookbooksListPage for isolated rendering/tests.\n */\n\nimport type { FC } from 'react';\nimport { BookHeart } from 'lucide-react';\nimport { Thumbnail } from './Thumbnail';\nimport { TagList } from './TagList';\nimport { VisibilityChip } from './VisibilityChip';\nimport { cn } from '../utils/cn';\nimport type { CooLookbook } from '../types';\n\ninterface LookbookCardProps {\n lookbook: CooLookbook;\n onClick?: () => void;\n className?: string;\n}\n\nexport const LookbookCard: FC<LookbookCardProps> = ({ lookbook, onClick, className }) => {\n const entryCount = Array.isArray(lookbook.entries) ? lookbook.entries.length : null;\n\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={`Open lookbook ${lookbook.name}`}\n className={cn(\n '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-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-focus-ring)]',\n className\n )}\n >\n <Thumbnail\n icon={<BookHeart className=\"h-10 w-10\" />}\n seed={lookbook.id}\n fileId={lookbook.cover?.id}\n alt={lookbook.name}\n className=\"aspect-[4/3] w-full rounded-none\"\n />\n <div className=\"flex flex-1 flex-col p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">{lookbook.name}</h3>\n {entryCount !== null && (\n <p className=\"mt-0.5 text-xs text-text-secondary tabular-nums\">\n {entryCount} {entryCount === 1 ? 'item' : 'items'}\n </p>\n )}\n </div>\n <VisibilityChip visibility={lookbook.visibility} className=\"shrink-0\" />\n </div>\n {lookbook.description && (\n <p className=\"mt-1.5 line-clamp-2 text-sm text-text-secondary\">{lookbook.description}</p>\n )}\n {lookbook.tags.length > 0 && <TagList tags={lookbook.tags} max={4} className=\"mt-3\" />}\n </div>\n </button>\n );\n};\n"],"mappings":";;;;;;;AAsBA,IAAa,KAAuC,EAAE,aAAU,YAAS,mBAAgB;CACvF,IAAM,IAAa,MAAM,QAAQ,EAAS,OAAO,IAAI,EAAS,QAAQ,SAAS;CAE/E,OACE,kBAAC,UAAD;EACE,MAAK;EACI;EACT,cAAY,iBAAiB,EAAS;EACtC,WAAW,EACT,qSACA,CACF;YAPF,CASE,kBAAC,GAAD;GACE,MAAM,kBAAC,GAAD,EAAW,WAAU,YAAa,CAAA;GACxC,MAAM,EAAS;GACf,QAAQ,EAAS,OAAO;GACxB,KAAK,EAAS;GACd,WAAU;EACX,CAAA,GACD,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAsD,EAAS;MAAS,CAAA,GACrF,MAAe,QACd,kBAAC,KAAD;OAAG,WAAU;iBAAb;QACG;QAAW;QAAE,MAAe,IAAI,SAAS;OACzC;QAEF;SACL,kBAAC,GAAD;MAAgB,YAAY,EAAS;MAAY,WAAU;KAAY,CAAA,CACpE;;IACJ,EAAS,eACR,kBAAC,KAAD;KAAG,WAAU;eAAmD,EAAS;IAAe,CAAA;IAEzF,EAAS,KAAK,SAAS,KAAK,kBAAC,GAAD;KAAS,MAAM,EAAS;KAAM,KAAK;KAAG,WAAU;IAAQ,CAAA;GAClF;IACC;;AAEZ"}
@@ -1 +1 @@
1
- {"version":3,"file":"MetricsCard.js","names":[],"sources":["../../../src/coolandlovely/components/MetricsCard.tsx"],"sourcesContent":["/**\n * Metrics Card Component\n * @module coolandlovely/components\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { TrendingUp, TrendingDown, Minus } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport type { TrendDirection } from '../types';\n\ninterface MetricsCardProps {\n icon: ReactNode;\n value: string | number;\n label: string;\n trend?: TrendDirection;\n trendValue?: string;\n className?: string;\n /** Renders the featured hero metric (glass treatment + accent glow). */\n featured?: boolean;\n /**\n * Optional real (non-fabricated) day-by-day series, oldest → newest, used\n * to render a small trend sparkline next to the trend indicator. Omit (or\n * pass a series with < 2 points) to fall back to the text-only trend view\n * — see `buildCreatedAtSparkline` in `utils/sparkline.ts`.\n */\n sparklineData?: number[];\n}\n\nconst trendConfig: Record<TrendDirection, { icon: typeof TrendingUp; color: string }> = {\n up: { icon: TrendingUp, color: 'text-status-success-text' },\n down: { icon: TrendingDown, color: 'text-status-error-text' },\n neutral: { icon: Minus, color: 'text-text-secondary' },\n};\n\n/** Small inline SVG trend line. Uses `currentColor` (semantic accent token) — never a raw hex/RGB value. */\nconst Sparkline: FC<{ data: number[] }> = ({ data }) => {\n const width = 56;\n const height = 20;\n const min = Math.min(...data);\n const max = Math.max(...data);\n const range = max - min || 1;\n const stepX = data.length > 1 ? width / (data.length - 1) : 0;\n const points = data\n .map((v, i) => {\n const x = i * stepX;\n const y = height - ((v - min) / range) * (height - 2) - 1;\n return `${x.toFixed(1)},${y.toFixed(1)}`;\n })\n .join(' ');\n\n return (\n <svg\n viewBox={`0 0 ${width} ${height}`}\n width={width}\n height={height}\n className=\"shrink-0 text-accent-primary\"\n role=\"img\"\n aria-label=\"7-day trend\"\n >\n <polyline\n points={points}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.5}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n};\n\nexport const MetricsCard: FC<MetricsCardProps> = ({\n icon,\n value,\n label,\n trend,\n trendValue,\n className,\n featured = false,\n sparklineData,\n}) => {\n const trendInfo = trend ? trendConfig[trend] : null;\n const TrendIcon = trendInfo?.icon;\n const hasSparkline = Boolean(sparklineData && sparklineData.length > 1);\n\n return (\n <GlassCard\n treatment={featured ? 'glass' : 'metric'}\n glow={featured}\n className={cn('p-6', className)}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-accent-primary\">\n {icon}\n </div>\n <div className=\"flex items-center gap-2\">\n {hasSparkline && sparklineData ? <Sparkline data={sparklineData} /> : null}\n {trend && trendInfo && TrendIcon && (\n <div className={cn('flex items-center gap-1 text-xs font-medium', trendInfo.color)}>\n <TrendIcon className=\"h-3.5 w-3.5\" />\n {trendValue && <span className=\"tabular-nums\">{trendValue}</span>}\n </div>\n )}\n </div>\n </div>\n <div className=\"mt-3\">\n <p className=\"text-2xl font-bold text-text-primary tabular-nums\">{value}</p>\n <p className=\"mt-1 text-sm text-text-secondary\">{label}</p>\n </div>\n </GlassCard>\n );\n};\n"],"mappings":";;;;;AA6BA,IAAM,IAAkF;CACtF,IAAI;EAAE,MAAM;EAAY,OAAO;EAA4B;CAC3D,MAAM;EAAE,MAAM;EAAc,OAAO;EAA0B;CAC7D,SAAS;EAAE,MAAM;EAAO,OAAO;EAAuB;CACvD,EAGK,KAAqC,EAAE,cAAW;CACtD,IAEM,IAAM,KAAK,IAAI,GAAG,EAAK,EAEvB,IADM,KAAK,IAAI,GAAG,EAAK,GACT,KAAO,GACrB,IAAQ,EAAK,SAAS,IAAI,MAAS,EAAK,SAAS,KAAK;AAS5D,QACE,kBAAC,OAAD;EACE,SAAS;EACF;EACC;EACR,WAAU;EACV,MAAK;EACL,cAAW;YAEX,kBAAC,YAAD;GACU,QAlBC,EACZ,KAAK,GAAG,MAAM;IACb,IAAM,IAAI,IAAI,GACR,IAAI,MAAW,IAAI,KAAO,IAAU,KAAc;AACxD,WAAO,GAAG,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE;KACtC,CACD,KAAK,IAAI;GAaN,MAAK;GACL,QAAO;GACP,aAAa;GACb,eAAc;GACd,gBAAe;GACf,CAAA;EACE,CAAA;GAIG,KAAqC,EAChD,SACA,UACA,UACA,UACA,eACA,cACA,cAAW,IACX,uBACI;CACJ,IAAM,IAAY,IAAQ,EAAY,KAAS,MACzC,IAAY,GAAW,MACvB,IAAe,GAAQ,KAAiB,EAAc,SAAS;AAErE,QACE,kBAAC,GAAD;EACE,WAAW,IAAW,UAAU;EAChC,MAAM;EACN,WAAW,EAAG,OAAO,EAAU;YAHjC,CAKE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACZ;IACG,CAAA,EACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,KAAgB,IAAgB,kBAAC,GAAD,EAAW,MAAM,GAAiB,CAAA,GAAG,MACrE,KAAS,KAAa,KACrB,kBAAC,OAAD;KAAK,WAAW,EAAG,+CAA+C,EAAU,MAAM;eAAlF,CACE,kBAAC,GAAD,EAAW,WAAU,eAAgB,CAAA,EACpC,KAAc,kBAAC,QAAD;MAAM,WAAU;gBAAgB;MAAkB,CAAA,CAC7D;OAEJ;MACF;MACN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,KAAD;IAAG,WAAU;cAAqD;IAAU,CAAA,EAC5E,kBAAC,KAAD;IAAG,WAAU;cAAoC;IAAU,CAAA,CACvD;KACI"}
1
+ {"version":3,"file":"MetricsCard.js","names":[],"sources":["../../../src/coolandlovely/components/MetricsCard.tsx"],"sourcesContent":["/**\n * Metrics Card Component\n * @module coolandlovely/components\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { TrendingUp, TrendingDown, Minus } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport type { TrendDirection } from '../types';\n\ninterface MetricsCardProps {\n icon: ReactNode;\n value: string | number;\n label: string;\n trend?: TrendDirection;\n trendValue?: string;\n className?: string;\n /** Renders the featured hero metric (glass treatment + accent glow). */\n featured?: boolean;\n /**\n * Optional real (non-fabricated) day-by-day series, oldest → newest, used\n * to render a small trend sparkline next to the trend indicator. Omit (or\n * pass a series with < 2 points) to fall back to the text-only trend view\n * — see `buildCreatedAtSparkline` in `utils/sparkline.ts`.\n */\n sparklineData?: number[];\n}\n\nconst trendConfig: Record<TrendDirection, { icon: typeof TrendingUp; color: string }> = {\n up: { icon: TrendingUp, color: 'text-status-success-text' },\n down: { icon: TrendingDown, color: 'text-status-error-text' },\n neutral: { icon: Minus, color: 'text-text-secondary' },\n};\n\n/** Small inline SVG trend line. Uses `currentColor` (semantic accent token) — never a raw hex/RGB value. */\nconst Sparkline: FC<{ data: number[] }> = ({ data }) => {\n const width = 56;\n const height = 20;\n const min = Math.min(...data);\n const max = Math.max(...data);\n const range = max - min || 1;\n const stepX = data.length > 1 ? width / (data.length - 1) : 0;\n const points = data\n .map((v, i) => {\n const x = i * stepX;\n const y = height - ((v - min) / range) * (height - 2) - 1;\n return `${x.toFixed(1)},${y.toFixed(1)}`;\n })\n .join(' ');\n\n return (\n <svg\n viewBox={`0 0 ${width} ${height}`}\n width={width}\n height={height}\n className=\"shrink-0 text-accent-primary\"\n role=\"img\"\n aria-label=\"7-day trend\"\n >\n <polyline\n points={points}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.5}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n};\n\nexport const MetricsCard: FC<MetricsCardProps> = ({\n icon,\n value,\n label,\n trend,\n trendValue,\n className,\n featured = false,\n sparklineData,\n}) => {\n const trendInfo = trend ? trendConfig[trend] : null;\n const TrendIcon = trendInfo?.icon;\n const hasSparkline = Boolean(sparklineData && sparklineData.length > 1);\n\n return (\n <GlassCard\n treatment={featured ? 'glass' : 'metric'}\n glow={featured}\n className={cn('p-6', className)}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-accent-primary\">\n {icon}\n </div>\n <div className=\"flex items-center gap-2\">\n {hasSparkline && sparklineData ? <Sparkline data={sparklineData} /> : null}\n {trend && trendInfo && TrendIcon && (\n <div className={cn('flex items-center gap-1 text-xs font-medium', trendInfo.color)}>\n <TrendIcon className=\"h-3.5 w-3.5\" />\n {trendValue && <span className=\"tabular-nums\">{trendValue}</span>}\n </div>\n )}\n </div>\n </div>\n <div className=\"mt-3\">\n <p className=\"text-2xl font-bold text-text-primary tabular-nums\">{value}</p>\n <p className=\"mt-1 text-sm text-text-secondary\">{label}</p>\n </div>\n </GlassCard>\n );\n};\n"],"mappings":";;;;;AA6BA,IAAM,IAAkF;CACtF,IAAI;EAAE,MAAM;EAAY,OAAO;CAA2B;CAC1D,MAAM;EAAE,MAAM;EAAc,OAAO;CAAyB;CAC5D,SAAS;EAAE,MAAM;EAAO,OAAO;CAAsB;AACvD,GAGM,KAAqC,EAAE,cAAW;CACtD,IAEM,IAAM,KAAK,IAAI,GAAG,CAAI,GAEtB,IADM,KAAK,IAAI,GAAG,CACV,IAAM,KAAO,GACrB,IAAQ,EAAK,SAAS,IAAI,MAAS,EAAK,SAAS,KAAK;CAS5D,OACE,kBAAC,OAAD;EACE,SAAS;EACF;EACC;EACR,WAAU;EACV,MAAK;EACL,cAAW;YAEX,kBAAC,YAAD;GACU,QAlBC,EACZ,KAAK,GAAG,MAAM;IACb,IAAM,IAAI,IAAI,GACR,IAAI,MAAW,IAAI,KAAO,IAAU,KAAc;IACxD,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC;GACvC,CAAC,CAAC,CACD,KAAK,GAYM;GACR,MAAK;GACL,QAAO;GACP,aAAa;GACb,eAAc;GACd,gBAAe;EAChB,CAAA;CACE,CAAA;AAET,GAEa,KAAqC,EAChD,SACA,UACA,UACA,UACA,eACA,cACA,cAAW,IACX,uBACI;CACJ,IAAM,IAAY,IAAQ,EAAY,KAAS,MACzC,IAAY,GAAW,MACvB,IAAe,GAAQ,KAAiB,EAAc,SAAS;CAErE,OACE,kBAAC,GAAD;EACE,WAAW,IAAW,UAAU;EAChC,MAAM;EACN,WAAW,EAAG,OAAO,CAAS;YAHhC,CAKE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACZ;GACE,CAAA,GACL,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,KAAgB,IAAgB,kBAAC,GAAD,EAAW,MAAM,EAAgB,CAAA,IAAI,MACrE,KAAS,KAAa,KACrB,kBAAC,OAAD;KAAK,WAAW,EAAG,+CAA+C,EAAU,KAAK;eAAjF,CACE,kBAAC,GAAD,EAAW,WAAU,cAAe,CAAA,GACnC,KAAc,kBAAC,QAAD;MAAM,WAAU;gBAAgB;KAAiB,CAAA,CAC7D;MAEJ;KACF;MACL,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,KAAD;IAAG,WAAU;cAAqD;GAAS,CAAA,GAC3E,kBAAC,KAAD;IAAG,WAAU;cAAoC;GAAS,CAAA,CACvD;IACI;;AAEf"}
@@ -1 +1 @@
1
- {"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/coolandlovely/components/PageLayout.tsx"],"sourcesContent":["/**\n * Page Layout Component\n * @module coolandlovely/components\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AuroraBackground, GradientText } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n /**\n * Enables the living-glass hero backdrop (AuroraBackground) behind the page\n * header. Use on at most ONE page per MFE (the primary landing page).\n */\n hero?: boolean;\n}\n\n/**\n * Renders the page title, gradient-emphasising the final word when the title is\n * a plain string (the \"one gradient hero title per page\" convention). Custom\n * ReactNode titles are rendered verbatim so callers keep full control.\n */\nfunction renderTitle(title: ReactNode): ReactNode {\n if (typeof title !== 'string') return title;\n const trimmed = title.trim();\n const lastSpace = trimmed.lastIndexOf(' ');\n if (lastSpace === -1) {\n return <GradientText as=\"span\">{trimmed}</GradientText>;\n }\n const head = trimmed.slice(0, lastSpace + 1);\n const tail = trimmed.slice(lastSpace + 1);\n return (\n <>\n {head}\n <GradientText as=\"span\">{tail}</GradientText>\n </>\n );\n}\n\nexport const PageLayout: FC<PageLayoutProps> = ({\n title,\n description,\n actions,\n children,\n className,\n hero = false,\n}) => {\n return (\n <div className={cn('flex flex-col h-full', className)}>\n {/* Page Header */}\n <div\n className={cn(\n 'relative overflow-hidden flex flex-col gap-3 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6',\n 'border-b border-border-seam',\n hero ? 'bg-hero-mesh' : 'bg-gloss-card'\n )}\n >\n {hero && <AuroraBackground intensity=\"subtle\" />}\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-semibold text-text-primary tracking-tight\">\n {renderTitle(title)}\n </h1>\n {description && <p className=\"mt-1 text-sm text-text-secondary\">{description}</p>}\n </div>\n {actions && (\n <div className=\"flex flex-wrap items-center gap-2 sm:gap-3 sm:justify-end\">{actions}</div>\n )}\n </div>\n\n {/* Page Content */}\n <div className=\"flex-1 overflow-auto p-4 sm:p-6\">{children}</div>\n </div>\n );\n};\n"],"mappings":";;;;AA2BA,SAAS,EAAY,GAA6B;AAChD,KAAI,OAAO,KAAU,SAAU,QAAO;CACtC,IAAM,IAAU,EAAM,MAAM,EACtB,IAAY,EAAQ,YAAY,IAAI;AAM1C,QALI,MAAc,KACT,kBAAC,GAAD;EAAc,IAAG;YAAQ;EAAuB,CAAA,GAKvD,kBAAA,GAAA,EAAA,UAAA,CAHW,EAAQ,MAAM,GAAG,IAAY,EAAE,EAKxC,kBAAC,GAAD;EAAc,IAAG;YAJR,EAAQ,MAAM,IAAY,EAAE;EAIQ,CAAA,CAC5C,EAAA,CAAA;;AAIP,IAAa,KAAmC,EAC9C,UACA,gBACA,YACA,aACA,cACA,UAAO,SAGL,kBAAC,OAAD;CAAK,WAAW,EAAG,wBAAwB,EAAU;WAArD,CAEE,kBAAC,OAAD;EACE,WAAW,EACT,iHACA,+BACA,IAAO,iBAAiB,gBACzB;YALH;GAOG,KAAQ,kBAAC,GAAD,EAAkB,WAAU,UAAW,CAAA;GAChD,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAY,EAAM;KAChB,CAAA,EACJ,KAAe,kBAAC,KAAD;KAAG,WAAU;eAAoC;KAAgB,CAAA,CAC7E;;GACL,KACC,kBAAC,OAAD;IAAK,WAAU;cAA6D;IAAc,CAAA;GAExF;KAGN,kBAAC,OAAD;EAAK,WAAU;EAAmC;EAAe,CAAA,CAC7D"}
1
+ {"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/coolandlovely/components/PageLayout.tsx"],"sourcesContent":["/**\n * Page Layout Component\n * @module coolandlovely/components\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AuroraBackground, GradientText } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n /**\n * Enables the living-glass hero backdrop (AuroraBackground) behind the page\n * header. Use on at most ONE page per MFE (the primary landing page).\n */\n hero?: boolean;\n}\n\n/**\n * Renders the page title, gradient-emphasising the final word when the title is\n * a plain string (the \"one gradient hero title per page\" convention). Custom\n * ReactNode titles are rendered verbatim so callers keep full control.\n */\nfunction renderTitle(title: ReactNode): ReactNode {\n if (typeof title !== 'string') return title;\n const trimmed = title.trim();\n const lastSpace = trimmed.lastIndexOf(' ');\n if (lastSpace === -1) {\n return <GradientText as=\"span\">{trimmed}</GradientText>;\n }\n const head = trimmed.slice(0, lastSpace + 1);\n const tail = trimmed.slice(lastSpace + 1);\n return (\n <>\n {head}\n <GradientText as=\"span\">{tail}</GradientText>\n </>\n );\n}\n\nexport const PageLayout: FC<PageLayoutProps> = ({\n title,\n description,\n actions,\n children,\n className,\n hero = false,\n}) => {\n return (\n <div className={cn('flex flex-col h-full', className)}>\n {/* Page Header */}\n <div\n className={cn(\n 'relative overflow-hidden flex flex-col gap-3 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6',\n 'border-b border-border-seam',\n hero ? 'bg-hero-mesh' : 'bg-gloss-card'\n )}\n >\n {hero && <AuroraBackground intensity=\"subtle\" />}\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-semibold text-text-primary tracking-tight\">\n {renderTitle(title)}\n </h1>\n {description && <p className=\"mt-1 text-sm text-text-secondary\">{description}</p>}\n </div>\n {actions && (\n <div className=\"flex flex-wrap items-center gap-2 sm:gap-3 sm:justify-end\">{actions}</div>\n )}\n </div>\n\n {/* Page Content */}\n <div className=\"flex-1 overflow-auto p-4 sm:p-6\">{children}</div>\n </div>\n );\n};\n"],"mappings":";;;;AA2BA,SAAS,EAAY,GAA6B;CAChD,IAAI,OAAO,KAAU,UAAU,OAAO;CACtC,IAAM,IAAU,EAAM,KAAK,GACrB,IAAY,EAAQ,YAAY,GAAG;CAMzC,OALI,MAAc,KACT,kBAAC,GAAD;EAAc,IAAG;YAAQ;CAAsB,CAAA,IAKtD,kBAAA,GAAA,EAAA,UAAA,CAHW,EAAQ,MAAM,GAAG,IAAY,CAIrC,GACD,kBAAC,GAAD;EAAc,IAAG;YAJR,EAAQ,MAAM,IAAY,CAIV;CAAmB,CAAA,CAC5C,EAAA,CAAA;AAEN;AAEA,IAAa,KAAmC,EAC9C,UACA,gBACA,YACA,aACA,cACA,UAAO,SAGL,kBAAC,OAAD;CAAK,WAAW,EAAG,wBAAwB,CAAS;WAApD,CAEE,kBAAC,OAAD;EACE,WAAW,EACT,iHACA,+BACA,IAAO,iBAAiB,eAC1B;YALF;GAOG,KAAQ,kBAAC,GAAD,EAAkB,WAAU,SAAU,CAAA;GAC/C,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAY,CAAK;IAChB,CAAA,GACH,KAAe,kBAAC,KAAD;KAAG,WAAU;eAAoC;IAAe,CAAA,CAC7E;;GACJ,KACC,kBAAC,OAAD;IAAK,WAAU;cAA6D;GAAa,CAAA;EAExF;KAGL,kBAAC,OAAD;EAAK,WAAU;EAAmC;CAAc,CAAA,CAC7D"}