@burdenoff/microfe-coolandlovely 2026.706.2 → 2026.706.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coolandlovely/CoolAndLovelyRoot.js +62 -46
- package/dist/coolandlovely/CoolAndLovelyRoot.js.map +1 -1
- package/dist/coolandlovely/components/AddToLookbookButton.js +22 -22
- package/dist/coolandlovely/components/AddToLookbookButton.js.map +1 -1
- package/dist/coolandlovely/components/BrandCover.js +39 -0
- package/dist/coolandlovely/components/BrandCover.js.map +1 -0
- package/dist/coolandlovely/components/BrandCreatorAvatars.js +57 -0
- package/dist/coolandlovely/components/BrandCreatorAvatars.js.map +1 -0
- package/dist/coolandlovely/components/ColourSwatchRow.js +86 -0
- package/dist/coolandlovely/components/ColourSwatchRow.js.map +1 -0
- package/dist/coolandlovely/components/Comments.js +4 -4
- package/dist/coolandlovely/components/Comments.js.map +1 -1
- package/dist/coolandlovely/components/FileImage.js.map +1 -1
- package/dist/coolandlovely/components/ImageUploadField.js +139 -57
- package/dist/coolandlovely/components/ImageUploadField.js.map +1 -1
- package/dist/coolandlovely/components/LookCard.js +77 -0
- package/dist/coolandlovely/components/LookCard.js.map +1 -0
- package/dist/coolandlovely/components/MetricsCard.js +1 -1
- package/dist/coolandlovely/components/MetricsCard.js.map +1 -1
- package/dist/coolandlovely/components/ShopItemCard.js +45 -0
- package/dist/coolandlovely/components/ShopItemCard.js.map +1 -0
- package/dist/coolandlovely/components/StepTracker.js +35 -0
- package/dist/coolandlovely/components/StepTracker.js.map +1 -0
- package/dist/coolandlovely/components/Thumbnail.js +15 -11
- package/dist/coolandlovely/components/Thumbnail.js.map +1 -1
- package/dist/coolandlovely/components/VisibilityChip.js +32 -0
- package/dist/coolandlovely/components/VisibilityChip.js.map +1 -0
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js +94 -69
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js.map +1 -1
- package/dist/coolandlovely/hooks/useFiles.js +49 -32
- package/dist/coolandlovely/hooks/useFiles.js.map +1 -1
- package/dist/coolandlovely/pages/HomePage.js +112 -133
- package/dist/coolandlovely/pages/HomePage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandDetailPage.js +253 -242
- package/dist/coolandlovely/pages/brands/BrandDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandFormPage.js +116 -112
- package/dist/coolandlovely/pages/brands/BrandFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandsListPage.js +13 -13
- package/dist/coolandlovely/pages/brands/BrandsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js +117 -95
- package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js +50 -43
- package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionDetailPage.js +131 -108
- package/dist/coolandlovely/pages/collections/CollectionDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionFormPage.js +1 -1
- package/dist/coolandlovely/pages/collections/CollectionsListPage.js +59 -56
- package/dist/coolandlovely/pages/collections/CollectionsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/discover/DiscoverPage.js +1 -1
- package/dist/coolandlovely/pages/discover/DiscoverPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js +175 -147
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemFormPage.js +1 -1
- package/dist/coolandlovely/pages/items/StyleItemsListPage.js +71 -62
- package/dist/coolandlovely/pages/items/StyleItemsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js +149 -127
- package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js +69 -53
- package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookDetailPage.js +135 -113
- package/dist/coolandlovely/pages/looks/LookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookFormPage.js +122 -110
- package/dist/coolandlovely/pages/looks/LookFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LooksListPage.js +60 -97
- package/dist/coolandlovely/pages/looks/LooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js +372 -210
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js.map +1 -1
- package/dist/coolandlovely/pages/trends/TrendsPage.js +1 -1
- package/dist/coolandlovely/pages/trends/TrendsPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js +130 -94
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js +75 -76
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -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(
|
|
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 +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 <img
|
|
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,71 +1,153 @@
|
|
|
1
1
|
import { useCoolAndLovely as e } from "../context/CoolAndLovelyContext.js";
|
|
2
2
|
import { cn as t } from "../utils/cn.js";
|
|
3
|
-
import {
|
|
3
|
+
import { uploadImageToFilesWithProgress as n, useFileUrl as r } from "../hooks/useFiles.js";
|
|
4
4
|
import { FieldShell as i } from "./Field.js";
|
|
5
|
-
import {
|
|
6
|
-
import { ImagePlus as
|
|
7
|
-
import { jsx as
|
|
5
|
+
import { useEffect as a, useId as o, useRef as s, useState as c } from "react";
|
|
6
|
+
import { ImagePlus as l, Loader2 as u, UploadCloud as d, X as f } from "lucide-react";
|
|
7
|
+
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
8
8
|
//#region src/coolandlovely/components/ImageUploadField.tsx
|
|
9
|
-
var
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
var h = 10 * 1024 * 1024;
|
|
10
|
+
function g(e) {
|
|
11
|
+
if (e < 1024) return `${e} B`;
|
|
12
|
+
let t = e / 1024;
|
|
13
|
+
return t < 1024 ? `${t < 10 ? t.toFixed(1) : Math.round(t)} KB` : `${(t / 1024).toFixed(1)} MB`;
|
|
14
|
+
}
|
|
15
|
+
var _ = ({ label: _, value: v, onChange: y, folder: b, hint: x, accept: S = "image/*", disabled: C = !1, className: w, aspect: T = "square" }) => {
|
|
16
|
+
let { apiGatewayUrl: E } = e(), D = s(null), O = o(), [k, A] = c(!1), [j, M] = c(null), [N, P] = c(null), [F, I] = c(null), [L, R] = c(!1), [z, B] = c();
|
|
17
|
+
a(() => () => {
|
|
18
|
+
F && URL.revokeObjectURL(F);
|
|
19
|
+
}, [F]);
|
|
20
|
+
let V = r(k ? void 0 : v || void 0).data ?? null, H = C || k, U = async (e) => {
|
|
21
|
+
if (!e.type.startsWith("image/")) {
|
|
22
|
+
B("Please choose an image file (JPG or PNG).");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (e.size > h) {
|
|
26
|
+
B("That image is over 10 MB — please choose a smaller file.");
|
|
27
|
+
return;
|
|
21
28
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
B(void 0), P(e.name), I(URL.createObjectURL(e)), M({
|
|
30
|
+
loaded: 0,
|
|
31
|
+
total: e.size,
|
|
32
|
+
pct: 0
|
|
33
|
+
}), A(!0);
|
|
34
|
+
try {
|
|
35
|
+
y((await n(e, E, b, (e) => M(e))).id);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
B(e instanceof Error ? e.message : "Upload failed — please try again.");
|
|
38
|
+
} finally {
|
|
39
|
+
A(!1), M(null), P(null), I(null);
|
|
40
|
+
}
|
|
41
|
+
}, W = (e) => {
|
|
42
|
+
let t = e.target.files?.[0];
|
|
43
|
+
e.target.value = "", t && U(t);
|
|
44
|
+
}, G = (e) => {
|
|
45
|
+
if (e.preventDefault(), R(!1), H) return;
|
|
46
|
+
let t = e.dataTransfer.files?.[0];
|
|
47
|
+
t && U(t);
|
|
48
|
+
}, K = (e) => {
|
|
49
|
+
e.preventDefault(), H || R(!0);
|
|
50
|
+
}, q = (e) => {
|
|
51
|
+
e.preventDefault(), R(!1);
|
|
52
|
+
}, J = j?.pct ?? 0;
|
|
53
|
+
return /* @__PURE__ */ m(i, {
|
|
54
|
+
label: _,
|
|
55
|
+
htmlFor: O,
|
|
56
|
+
hint: x,
|
|
57
|
+
error: z,
|
|
58
|
+
className: w,
|
|
59
|
+
children: [/* @__PURE__ */ p("div", {
|
|
60
|
+
onDragEnter: K,
|
|
61
|
+
onDragOver: K,
|
|
62
|
+
onDragLeave: q,
|
|
63
|
+
onDrop: G,
|
|
64
|
+
children: k ? /* @__PURE__ */ m("div", {
|
|
65
|
+
className: "flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3",
|
|
66
|
+
children: [/* @__PURE__ */ p("div", {
|
|
67
|
+
className: "flex h-14 w-14 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-bg-sunken",
|
|
68
|
+
children: F ? /* @__PURE__ */ p("img", {
|
|
69
|
+
src: F,
|
|
70
|
+
alt: "",
|
|
71
|
+
className: "h-full w-full object-cover"
|
|
72
|
+
}) : /* @__PURE__ */ p(u, { className: "h-5 w-5 animate-spin text-text-secondary" })
|
|
73
|
+
}), /* @__PURE__ */ m("div", {
|
|
74
|
+
className: "min-w-0 flex-1",
|
|
75
|
+
children: [
|
|
76
|
+
/* @__PURE__ */ p("p", {
|
|
77
|
+
className: "truncate text-sm font-medium text-text-primary",
|
|
78
|
+
children: N ?? "Uploading…"
|
|
79
|
+
}),
|
|
80
|
+
/* @__PURE__ */ p("div", {
|
|
81
|
+
className: "mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken",
|
|
82
|
+
children: /* @__PURE__ */ p("div", {
|
|
83
|
+
className: "h-full rounded-full bg-accent-primary transition-[width] duration-200",
|
|
84
|
+
style: { width: `${J}%` }
|
|
85
|
+
})
|
|
86
|
+
}),
|
|
87
|
+
/* @__PURE__ */ m("p", {
|
|
88
|
+
className: "mt-1 text-xs text-text-secondary tabular-nums",
|
|
89
|
+
children: [
|
|
90
|
+
J,
|
|
91
|
+
"%",
|
|
92
|
+
j ? ` · ${g(j.loaded)} of ${g(j.total)}` : ""
|
|
93
|
+
]
|
|
94
|
+
})
|
|
95
|
+
]
|
|
96
|
+
})]
|
|
97
|
+
}) : V ? /* @__PURE__ */ m("div", {
|
|
98
|
+
className: "relative overflow-hidden rounded-lg border border-border-seam bg-bg-sunken",
|
|
99
|
+
children: [/* @__PURE__ */ p("img", {
|
|
100
|
+
src: V,
|
|
35
101
|
alt: "",
|
|
36
|
-
className: "
|
|
37
|
-
})
|
|
38
|
-
className: "
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
102
|
+
className: t("w-full object-cover", T === "wide" ? "aspect-video" : "aspect-square max-h-64")
|
|
103
|
+
}), /* @__PURE__ */ m("div", {
|
|
104
|
+
className: "absolute inset-x-0 bottom-0 flex justify-end gap-2 bg-gradient-to-t from-bg-canvas/85 to-transparent p-2",
|
|
105
|
+
children: [/* @__PURE__ */ m("button", {
|
|
106
|
+
type: "button",
|
|
107
|
+
disabled: H,
|
|
108
|
+
onClick: () => D.current?.click(),
|
|
109
|
+
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)]",
|
|
110
|
+
children: [/* @__PURE__ */ p(l, { className: "h-4 w-4" }), " Replace"]
|
|
111
|
+
}), /* @__PURE__ */ m("button", {
|
|
112
|
+
type: "button",
|
|
113
|
+
disabled: C,
|
|
114
|
+
onClick: () => y(""),
|
|
115
|
+
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)]",
|
|
116
|
+
children: [/* @__PURE__ */ p(f, { className: "h-4 w-4" }), " Remove"]
|
|
117
|
+
})]
|
|
118
|
+
})]
|
|
119
|
+
}) : /* @__PURE__ */ m("button", {
|
|
120
|
+
type: "button",
|
|
121
|
+
disabled: H,
|
|
122
|
+
onClick: () => D.current?.click(),
|
|
123
|
+
className: t("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", L ? "border-accent-primary bg-[var(--color-accent-soft)]" : "border-border-seam hover:border-accent-primary/60 hover:bg-bg-sunken"),
|
|
124
|
+
children: [
|
|
125
|
+
/* @__PURE__ */ p(d, {
|
|
126
|
+
className: "h-8 w-8 text-text-secondary",
|
|
127
|
+
"aria-hidden": "true"
|
|
128
|
+
}),
|
|
129
|
+
/* @__PURE__ */ p("span", {
|
|
130
|
+
className: "text-sm font-medium text-text-primary",
|
|
131
|
+
children: "Drag & drop an image here, or click to browse"
|
|
132
|
+
}),
|
|
133
|
+
/* @__PURE__ */ p("span", {
|
|
134
|
+
className: "text-xs text-text-secondary",
|
|
135
|
+
children: "JPG · PNG · up to 10 MB"
|
|
136
|
+
})
|
|
137
|
+
]
|
|
138
|
+
})
|
|
139
|
+
}), /* @__PURE__ */ p("input", {
|
|
140
|
+
ref: D,
|
|
141
|
+
id: O,
|
|
60
142
|
type: "file",
|
|
61
|
-
accept:
|
|
143
|
+
accept: S,
|
|
62
144
|
className: "sr-only",
|
|
63
|
-
"aria-label":
|
|
64
|
-
onChange:
|
|
145
|
+
"aria-label": _,
|
|
146
|
+
onChange: W
|
|
65
147
|
})]
|
|
66
148
|
});
|
|
67
149
|
};
|
|
68
150
|
//#endregion
|
|
69
|
-
export {
|
|
151
|
+
export { _ as ImageUploadField };
|
|
70
152
|
|
|
71
153
|
//# sourceMappingURL=ImageUploadField.js.map
|
|
@@ -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`). On select it runs the shared\n * presigned upload (initiateUpload -> PUT -> completeUpload) and hands the new\n * File id back via `onChange`. The caller persists it (e.g. logoFileId). Preview\n * resolves the id -> presigned URL via useFileUrl. Clearing sends onChange('').\n *\n * Token-only, no Apollo, no toast dependency — surfaces its own error inline to\n * match the existing form error style.\n */\n\nimport type { FC } from 'react';\nimport { useId, useRef, useState } from 'react';\nimport { ImagePlus, Loader2, X } from 'lucide-react';\nimport { FieldShell } from './Field';\nimport { useCoolAndLovely } from '../context/CoolAndLovelyContext';\nimport { uploadImageToFiles, useFileUrl } 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 preview tile. */\n aspect?: 'square' | 'wide';\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 [error, setError] = useState<string | undefined>();\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\n const handleSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n event.target.value = ''; // allow re-selecting the same file\n if (!file) return;\n setUploading(true);\n setError(undefined);\n try {\n const uploaded = await uploadImageToFiles(file, apiGatewayUrl, folder);\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 }\n };\n\n const locked = disabled || uploading;\n\n return (\n <FieldShell label={label} htmlFor={inputId} hint={hint} error={error} className={className}>\n <div className=\"flex items-center gap-4\">\n <div\n className={cn(\n 'relative flex shrink-0 items-center justify-center overflow-hidden rounded-lg border border-border-seam bg-bg-sunken',\n aspect === 'wide' ? 'h-20 w-32' : 'h-20 w-20'\n )}\n >\n {previewUrl ? (\n <img src={previewUrl} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : uploading ? (\n <Loader2 className=\"h-5 w-5 animate-spin text-text-secondary\" />\n ) : (\n <ImagePlus className=\"h-6 w-6 text-text-secondary\" aria-hidden=\"true\" />\n )}\n </div>\n\n <div className=\"flex items-center gap-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 border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n {uploading ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <ImagePlus className=\"h-4 w-4\" />\n )}\n {uploading ? 'Uploading…' : value ? 'Replace' : 'Upload image'}\n </button>\n {value && !uploading ? (\n <button\n type=\"button\"\n disabled={disabled}\n onClick={() => onChange('')}\n className=\"inline-flex items-center gap-1 rounded-lg px-2 py-2 text-sm text-text-secondary 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 ) : null}\n </div>\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={(e) => void handleSelect(e)}\n />\n </FieldShell>\n );\n};\n"],"mappings":";;;;;;;;AAsCA,IAAa,KAA+C,EAC1D,UACA,UACA,aACA,WACA,SACA,YAAS,WACT,cAAW,IACX,cACA,YAAS,eACL;CACJ,IAAM,EAAE,qBAAkB,GAAkB,EACtC,IAAW,EAAgC,KAAK,EAChD,IAAU,GAAO,EACjB,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,CAAC,GAAO,KAAY,GAA8B,EAIlD,IADW,EAAW,IAAY,KAAA,IAAY,KAAS,KAAA,EAAU,CAC3C,QAAQ,MAE9B,IAAe,OAAO,MAA+C;EACzE,IAAM,IAAO,EAAM,OAAO,QAAQ;AAClC,QAAM,OAAO,QAAQ,IAChB,GAEL;GADA,EAAa,GAAK,EAClB,EAAS,KAAA,EAAU;AACnB,OAAI;AAEF,OADiB,MAAM,EAAmB,GAAM,GAAe,EAAO,EACpD,GAAG;YACd,GAAG;AACV,MAAS,aAAa,QAAQ,EAAE,UAAU,oCAAoC;aACtE;AACR,MAAa,GAAM;;;IAIjB,IAAS,KAAY;AAE3B,QACE,kBAAC,GAAD;EAAmB;EAAO,SAAS;EAAe;EAAa;EAAkB;YAAjF,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IACE,WAAW,EACT,wHACA,MAAW,SAAS,cAAc,YACnC;cAEA,IACC,kBAAC,OAAD;KAAK,KAAK;KAAY,KAAI;KAAG,WAAU;KAA+B,CAAA,GACpE,IACF,kBAAC,GAAD,EAAS,WAAU,4CAA6C,CAAA,GAEhE,kBAAC,GAAD;KAAW,WAAU;KAA8B,eAAY;KAAS,CAAA;IAEtE,CAAA,EAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,UAAU;KACV,eAAe,EAAS,SAAS,OAAO;KACxC,WAAU;eAJZ,CAMG,IACC,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAElC,IAAY,eAAe,IAAQ,YAAY,eACzC;QACR,KAAS,CAAC,IACT,kBAAC,UAAD;KACE,MAAK;KACK;KACV,eAAe,EAAS,GAAG;KAC3B,WAAU;eAJZ,CAME,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA,EAAA,UAClB;SACP,KACA;MACF;MACN,kBAAC,SAAD;GACE,KAAK;GACL,IAAI;GACJ,MAAK;GACG;GACR,WAAU;GACV,cAAY;GACZ,WAAW,MAAM,KAAK,EAAa,EAAE;GACrC,CAAA,CACS"}
|
|
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 & 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":";;;;;;;;AAyCA,IAAM,IAAY,KAAK,OAAO;AAE9B,SAAS,EAAY,GAAuB;AAC1C,KAAI,IAAQ,KAAM,QAAO,GAAG,EAAM;CAClC,IAAM,IAAK,IAAQ;AAGnB,QAFI,IAAK,OAAa,GAAG,IAAK,KAAK,EAAG,QAAQ,EAAE,GAAG,KAAK,MAAM,EAAG,CAAC,OAE3D,IADI,IAAK,MACH,QAAQ,EAAE,CAAC;;AAG1B,IAAa,KAA+C,EAC1D,UACA,UACA,aACA,WACA,SACA,YAAS,WACT,cAAW,IACX,cACA,YAAS,eACL;CACJ,IAAM,EAAE,qBAAkB,GAAkB,EACtC,IAAW,EAAgC,KAAK,EAChD,IAAU,GAAO,EACjB,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,CAAC,GAAU,KAAe,EAAgC,KAAK,EAC/D,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAC7D,CAAC,GAAgB,KAAqB,EAAwB,KAAK,EACnE,CAAC,GAAY,KAAiB,EAAS,GAAM,EAC7C,CAAC,GAAO,KAAY,GAA8B;AAGxD,eACe;AACX,EAAI,KAAgB,IAAI,gBAAgB,EAAe;IAExD,CAAC,EAAe,CAAC;CAIpB,IAAM,IADW,EAAW,IAAY,KAAA,IAAY,KAAS,KAAA,EAAU,CAC3C,QAAQ,MAC9B,IAAS,KAAY,GAErB,IAAS,OAAO,MAAe;AAEnC,MAAI,CAAC,EAAK,KAAK,WAAW,SAAS,EAAE;AACnC,KAAS,4CAA4C;AACrD;;AAEF,MAAI,EAAK,OAAO,GAAW;AACzB,KAAS,2DAA2D;AACpE;;AAQF,EALA,EAAS,KAAA,EAAU,EACnB,EAAe,EAAK,KAAK,EAEzB,EADkB,IAAI,gBAAgB,EAAK,CACf,EAC5B,EAAY;GAAE,QAAQ;GAAG,OAAO,EAAK;GAAM,KAAK;GAAG,CAAC,EACpD,EAAa,GAAK;AAClB,MAAI;AAIF,MAHiB,MAAM,EAA+B,GAAM,GAAe,IAAS,MAClF,EAAY,EAAE,CACf,EACiB,GAAG;WACd,GAAG;AACV,KAAS,aAAa,QAAQ,EAAE,UAAU,oCAAoC;YACtE;AAIR,GAHA,EAAa,GAAM,EACnB,EAAY,KAAK,EACjB,EAAe,KAAK,EACpB,EAAkB,KAAK;;IAIrB,KAAgB,MAA+C;EACnE,IAAM,IAAO,EAAM,OAAO,QAAQ;AAElC,EADA,EAAM,OAAO,QAAQ,IACjB,KAAW,EAAO,EAAK;IAGvB,KAAc,MAAqC;AAGvD,MAFA,EAAM,gBAAgB,EACtB,EAAc,GAAM,EAChB,EAAQ;EACZ,IAAM,IAAO,EAAM,aAAa,QAAQ;AACxC,EAAI,KAAW,EAAO,EAAK;IAGvB,KAAkB,MAAqC;AAE3D,EADA,EAAM,gBAAgB,EACjB,KAAQ,EAAc,GAAK;IAG5B,KAAmB,MAAqC;AAE5D,EADA,EAAM,gBAAgB,EACtB,EAAc,GAAM;IAGhB,IAAM,GAAU,OAAO;AAE7B,QACE,kBAAC,GAAD;EAAmB;EAAO,SAAS;EAAe;EAAa;EAAkB;YAAjF,CACE,kBAAC,OAAD;GACE,aAAa;GACb,YAAY;GACZ,aAAa;GACb,QAAQ;aAEP,IAEC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eACZ,IACC,kBAAC,OAAD;MAAK,KAAK;MAAgB,KAAI;MAAG,WAAU;MAA+B,CAAA,GAE1E,kBAAC,GAAD,EAAS,WAAU,4CAA6C,CAAA;KAE9D,CAAA,EACN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,KAAD;OAAG,WAAU;iBACV,KAAe;OACd,CAAA;MACJ,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,OAAD;QACE,WAAU;QACV,OAAO,EAAE,OAAO,GAAG,EAAI,IAAI;QAC3B,CAAA;OACE,CAAA;MACN,kBAAC,KAAD;OAAG,WAAU;iBAAb;QACG;QAAI;QACJ,IACG,MAAM,EAAY,EAAS,OAAO,CAAC,MAAM,EAAY,EAAS,MAAM,KACpE;QACF;;MACA;OACF;QACJ,IAEF,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KACE,KAAK;KACL,KAAI;KACJ,WAAW,EACT,uBACA,MAAW,SAAS,iBAAiB,yBACtC;KACD,CAAA,EACF,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,MAAK;MACL,UAAU;MACV,eAAe,EAAS,SAAS,OAAO;MACxC,WAAU;gBAJZ,CAME,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAAA,WAC1B;SACT,kBAAC,UAAD;MACE,MAAK;MACK;MACV,eAAe,EAAS,GAAG;MAC3B,WAAU;gBAJZ,CAME,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA,EAAA,UAClB;QACL;OACF;QAGN,kBAAC,UAAD;IACE,MAAK;IACL,UAAU;IACV,eAAe,EAAS,SAAS,OAAO;IACxC,WAAW,EACT,wPACA,IACI,wDACA,uEACL;cATH;KAWE,kBAAC,GAAD;MAAa,WAAU;MAA8B,eAAY;MAAS,CAAA;KAC1E,kBAAC,QAAD;MAAM,WAAU;gBAAwC;MAEjD,CAAA;KACP,kBAAC,QAAD;MAAM,WAAU;gBAA8B;MAA8B,CAAA;KACrE;;GAEP,CAAA,EACN,kBAAC,SAAD;GACE,KAAK;GACL,IAAI;GACJ,MAAK;GACG;GACR,WAAU;GACV,cAAY;GACZ,UAAU;GACV,CAAA,CACS"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { Thumbnail as t } from "./Thumbnail.js";
|
|
3
|
+
import { useLikeLook as n } from "../hooks/useCoolAndLovelyApi.js";
|
|
4
|
+
import { AddToLookbookButton as r } from "./AddToLookbookButton.js";
|
|
5
|
+
import { titleCase as i } from "../utils/formatters.js";
|
|
6
|
+
import { useEffect as a, useState as o } from "react";
|
|
7
|
+
import { Heart as s, Sparkles as c } from "lucide-react";
|
|
8
|
+
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
9
|
+
//#region src/coolandlovely/components/LookCard.tsx
|
|
10
|
+
var d = ({ lookId: t, likeCount: r }) => {
|
|
11
|
+
let i = n(), [c, d] = o(!1), [f, p] = o(r);
|
|
12
|
+
return a(() => {
|
|
13
|
+
c || p(r);
|
|
14
|
+
}, [r, c]), /* @__PURE__ */ u("button", {
|
|
15
|
+
type: "button",
|
|
16
|
+
onClick: () => {
|
|
17
|
+
c || i.isPending || (d(!0), p((e) => e + 1), i.mutate(t, {
|
|
18
|
+
onSuccess: (e) => p(e.likeCount),
|
|
19
|
+
onError: () => {
|
|
20
|
+
d(!1), p(r);
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
},
|
|
24
|
+
disabled: i.isPending,
|
|
25
|
+
"aria-pressed": c,
|
|
26
|
+
"aria-label": c ? "Liked" : "Like this look",
|
|
27
|
+
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)]",
|
|
28
|
+
children: [/* @__PURE__ */ l(s, { className: e("h-3.5 w-3.5", c ? "fill-current text-accent-primary" : "text-text-secondary") }), /* @__PURE__ */ l("span", {
|
|
29
|
+
className: "tabular-nums",
|
|
30
|
+
children: f
|
|
31
|
+
})]
|
|
32
|
+
});
|
|
33
|
+
}, f = ({ id: n, title: a, occasion: o, imageFileId: f, likeCount: p, onOpen: m, showActions: h = !1, className: g }) => /* @__PURE__ */ u("div", {
|
|
34
|
+
className: e("group relative overflow-hidden rounded-xl border border-border-seam bg-bg-surface transition-shadow hover:shadow-lg", g),
|
|
35
|
+
children: [/* @__PURE__ */ l("button", {
|
|
36
|
+
type: "button",
|
|
37
|
+
onClick: m,
|
|
38
|
+
"aria-label": `Open look ${a}`,
|
|
39
|
+
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)]",
|
|
40
|
+
children: /* @__PURE__ */ l(t, {
|
|
41
|
+
icon: /* @__PURE__ */ l(c, { className: "h-8 w-8" }),
|
|
42
|
+
seed: n,
|
|
43
|
+
fileId: f,
|
|
44
|
+
alt: a,
|
|
45
|
+
className: "aspect-[3/4] w-full rounded-none",
|
|
46
|
+
overlay: /* @__PURE__ */ u("div", {
|
|
47
|
+
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",
|
|
48
|
+
children: [/* @__PURE__ */ l("p", {
|
|
49
|
+
className: "truncate text-sm font-semibold text-text-primary",
|
|
50
|
+
children: a
|
|
51
|
+
}), /* @__PURE__ */ u("div", {
|
|
52
|
+
className: "flex items-center justify-between gap-2",
|
|
53
|
+
children: [/* @__PURE__ */ l("span", {
|
|
54
|
+
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",
|
|
55
|
+
children: i(o)
|
|
56
|
+
}), !h && typeof p == "number" && p > 0 ? /* @__PURE__ */ u("span", {
|
|
57
|
+
className: "inline-flex items-center gap-1 text-xs text-text-secondary",
|
|
58
|
+
children: [/* @__PURE__ */ l(s, { className: "h-3.5 w-3.5 fill-current text-accent-primary" }), /* @__PURE__ */ l("span", {
|
|
59
|
+
className: "tabular-nums",
|
|
60
|
+
children: p
|
|
61
|
+
})]
|
|
62
|
+
}) : null]
|
|
63
|
+
})]
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
}), h ? /* @__PURE__ */ u("div", {
|
|
67
|
+
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",
|
|
68
|
+
children: [/* @__PURE__ */ l(d, {
|
|
69
|
+
lookId: n,
|
|
70
|
+
likeCount: p ?? 0
|
|
71
|
+
}), /* @__PURE__ */ l(r, { lookId: n })]
|
|
72
|
+
}) : null]
|
|
73
|
+
});
|
|
74
|
+
//#endregion
|
|
75
|
+
export { f as LookCard };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=LookCard.js.map
|
|
@@ -0,0 +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 * Like heart (optimistic, via `useLikeLook`) + `AddToLookbookButton`. Reused by\n * the home latest-looks, Discover, the looks list and the collaboration\n * attributed-looks grid. Token-only; reuses `Thumbnail`/`FileImage` for imagery.\n */\n\nimport type { FC } from 'react';\nimport { useEffect, useState } from 'react';\nimport { Heart, Sparkles } from 'lucide-react';\nimport { Thumbnail } from './Thumbnail';\nimport { AddToLookbookButton } from './AddToLookbookButton';\nimport { useLikeLook } from '../hooks/useCoolAndLovelyApi';\nimport { titleCase } from '../utils/formatters';\nimport { cn } from '../utils/cn';\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 onOpen: () => void;\n /** Show the hover like/add-to-lookbook overlay cluster. */\n showActions?: boolean;\n className?: string;\n}\n\n/** Optimistic like heart — one-way like (no unlike op exists on the server). */\nconst LikeHeart: FC<{ lookId: string; likeCount: number }> = ({ lookId, likeCount }) => {\n const like = useLikeLook();\n const [liked, setLiked] = useState(false);\n const [count, setCount] = useState(likeCount);\n\n // Keep in sync with the server count until the user likes in this session.\n useEffect(() => {\n if (!liked) setCount(likeCount);\n }, [likeCount, liked]);\n\n const handleClick = () => {\n if (liked || like.isPending) return;\n setLiked(true);\n setCount((current) => current + 1);\n like.mutate(lookId, {\n onSuccess: (look) => setCount(look.likeCount),\n onError: () => {\n setLiked(false);\n setCount(likeCount);\n },\n });\n };\n\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={like.isPending}\n aria-pressed={liked}\n aria-label={liked ? 'Liked' : '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\nexport const LookCard: FC<LookCardProps> = ({\n id,\n title,\n occasion,\n imageFileId,\n likeCount,\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} />\n <AddToLookbookButton lookId={id} />\n </div>\n ) : null}\n </div>\n );\n};\n"],"mappings":";;;;;;;;;AAkCA,IAAM,KAAwD,EAAE,WAAQ,mBAAgB;CACtF,IAAM,IAAO,GAAa,EACpB,CAAC,GAAO,KAAY,EAAS,GAAM,EACnC,CAAC,GAAO,KAAY,EAAS,EAAU;AAoB7C,QAjBA,QAAgB;AACd,EAAK,KAAO,EAAS,EAAU;IAC9B,CAAC,GAAW,EAAM,CAAC,EAgBpB,kBAAC,UAAD;EACE,MAAK;EACL,eAhBsB;AACpB,QAAS,EAAK,cAClB,EAAS,GAAK,EACd,GAAU,MAAY,IAAU,EAAE,EAClC,EAAK,OAAO,GAAQ;IAClB,YAAY,MAAS,EAAS,EAAK,UAAU;IAC7C,eAAe;AAEb,KADA,EAAS,GAAM,EACf,EAAS,EAAU;;IAEtB,CAAC;;EAOA,UAAU,EAAK;EACf,gBAAc;EACd,cAAY,IAAQ,UAAU;EAC9B,WAAU;YANZ,CAQE,kBAAC,GAAD,EACE,WAAW,EACT,eACA,IAAQ,qCAAqC,sBAC9C,EACD,CAAA,EACF,kBAAC,QAAD;GAAM,WAAU;aAAgB;GAAa,CAAA,CACtC;;GAIA,KAA+B,EAC1C,OACA,UACA,aACA,gBACA,cACA,WACA,iBAAc,IACd,mBAGE,kBAAC,OAAD;CACE,WAAW,EACT,uHACA,EACD;WAJH,CAME,kBAAC,UAAD;EACE,MAAK;EACL,SAAS;EACT,cAAY,aAAa;EACzB,WAAU;YAEV,kBAAC,GAAD;GACE,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;GACtC,MAAM;GACN,QAAQ;GACR,KAAK;GACL,WAAU;GACV,SACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAAoD;KAAU,CAAA,EAC3E,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAU,EAAS;MACf,CAAA,EACN,CAAC,KAAe,OAAO,KAAc,YAAY,IAAY,IAC5D,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,kBAAC,GAAD,EAAO,WAAU,gDAAiD,CAAA,EAClE,kBAAC,QAAD;OAAM,WAAU;iBAAgB;OAAiB,CAAA,CAC5C;UACL,KACA;OACF;;GAER,CAAA;EACK,CAAA,EAER,IACC,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD;GAAW,QAAQ;GAAI,WAAW,KAAa;GAAK,CAAA,EACpD,kBAAC,GAAD,EAAqB,QAAQ,GAAM,CAAA,CAC/B;MACJ,KACA"}
|
|
@@ -25,7 +25,7 @@ var s = {
|
|
|
25
25
|
children: [/* @__PURE__ */ a("div", {
|
|
26
26
|
className: "flex items-center justify-between",
|
|
27
27
|
children: [/* @__PURE__ */ i("div", {
|
|
28
|
-
className: "
|
|
28
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-accent-primary",
|
|
29
29
|
children: t
|
|
30
30
|
}), c && f && p && /* @__PURE__ */ a("div", {
|
|
31
31
|
className: e("flex items-center gap-1 text-xs font-medium", f.color),
|
|
@@ -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\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\nexport const MetricsCard: FC<MetricsCardProps> = ({\n icon,\n value,\n label,\n trend,\n trendValue,\n className,\n featured = false,\n}) => {\n const trendInfo = trend ? trendConfig[trend] : null;\n const TrendIcon = trendInfo?.icon;\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\">\n <div className=\"
|
|
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\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\nexport const MetricsCard: FC<MetricsCardProps> = ({\n icon,\n value,\n label,\n trend,\n trendValue,\n className,\n featured = false,\n}) => {\n const trendInfo = trend ? trendConfig[trend] : null;\n const TrendIcon = trendInfo?.icon;\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\">\n <div className=\"inline-flex h-9 w-9 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-accent-primary\">\n {icon}\n </div>\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 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":";;;;;AAsBA,IAAM,IAAkF;CACtF,IAAI;EAAE,MAAM;EAAY,OAAO;EAA4B;CAC3D,MAAM;EAAE,MAAM;EAAc,OAAO;EAA0B;CAC7D,SAAS;EAAE,MAAM;EAAO,OAAO;EAAuB;CACvD,EAEY,KAAqC,EAChD,SACA,UACA,UACA,UACA,eACA,cACA,cAAW,SACP;CACJ,IAAM,IAAY,IAAQ,EAAY,KAAS,MACzC,IAAY,GAAW;AAE7B,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,EACL,KAAS,KAAa,KACrB,kBAAC,OAAD;IAAK,WAAW,EAAG,+CAA+C,EAAU,MAAM;cAAlF,CACE,kBAAC,GAAD,EAAW,WAAU,eAAgB,CAAA,EACpC,KAAc,kBAAC,QAAD;KAAM,WAAU;eAAgB;KAAkB,CAAA,CAC7D;MAEJ;MACN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,KAAD;IAAG,WAAU;cAAqD;IAAU,CAAA,EAC5E,kBAAC,KAAD;IAAG,WAAU;cAAoC;IAAU,CAAA,CACvD;KACI"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { Thumbnail as t } from "./Thumbnail.js";
|
|
3
|
+
import { formatPrice as n } from "../utils/formatters.js";
|
|
4
|
+
import { ChevronRight as r, ShoppingBag as i } from "lucide-react";
|
|
5
|
+
import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
6
|
+
//#region src/coolandlovely/components/ShopItemCard.tsx
|
|
7
|
+
var s = ({ id: s, name: c, brandName: l, price: u, currency: d, imageFileId: f, onOpen: p, className: m }) => /* @__PURE__ */ o("button", {
|
|
8
|
+
type: "button",
|
|
9
|
+
onClick: p,
|
|
10
|
+
"aria-label": `Shop ${c}`,
|
|
11
|
+
className: e("group flex w-full flex-col overflow-hidden rounded-xl border border-border-seam bg-bg-surface text-left transition-shadow hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]", m),
|
|
12
|
+
children: [/* @__PURE__ */ a(t, {
|
|
13
|
+
icon: /* @__PURE__ */ a(i, { className: "h-7 w-7" }),
|
|
14
|
+
seed: s,
|
|
15
|
+
fileId: f,
|
|
16
|
+
alt: c,
|
|
17
|
+
className: "aspect-square w-full rounded-none"
|
|
18
|
+
}), /* @__PURE__ */ o("div", {
|
|
19
|
+
className: "flex flex-col gap-0.5 p-3",
|
|
20
|
+
children: [
|
|
21
|
+
/* @__PURE__ */ a("p", {
|
|
22
|
+
className: "truncate text-sm font-medium text-text-primary",
|
|
23
|
+
children: c
|
|
24
|
+
}),
|
|
25
|
+
l ? /* @__PURE__ */ a("p", {
|
|
26
|
+
className: "truncate text-xs text-text-secondary",
|
|
27
|
+
children: l
|
|
28
|
+
}) : null,
|
|
29
|
+
/* @__PURE__ */ o("div", {
|
|
30
|
+
className: "mt-2 flex items-center justify-between gap-2",
|
|
31
|
+
children: [/* @__PURE__ */ a("span", {
|
|
32
|
+
className: "text-sm font-semibold text-text-primary tabular-nums",
|
|
33
|
+
children: n(u, d)
|
|
34
|
+
}), /* @__PURE__ */ o("span", {
|
|
35
|
+
className: "inline-flex items-center gap-0.5 text-xs font-semibold text-accent-primary",
|
|
36
|
+
children: ["Shop", /* @__PURE__ */ a(r, { className: "h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" })]
|
|
37
|
+
})]
|
|
38
|
+
})
|
|
39
|
+
]
|
|
40
|
+
})]
|
|
41
|
+
});
|
|
42
|
+
//#endregion
|
|
43
|
+
export { s as ShopItemCard };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=ShopItemCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShopItemCard.js","names":[],"sources":["../../../src/coolandlovely/components/ShopItemCard.tsx"],"sourcesContent":["/**\n * ShopItemCard — a product card for the \"shop the look\" grid.\n * @module coolandlovely/components\n *\n * Image + name + brand line + price + a rose \"Shop\" chevron that drills through\n * to the item detail page (the platform has no external shop URL, so \"Shop\" is\n * the internal drill-through). Reuses `Thumbnail`/`FileImage` for imagery.\n */\n\nimport type { FC } from 'react';\nimport { ChevronRight, ShoppingBag } from 'lucide-react';\nimport { Thumbnail } from './Thumbnail';\nimport { formatPrice } from '../utils/formatters';\nimport { cn } from '../utils/cn';\n\ninterface ShopItemCardProps {\n id: string;\n name: string;\n brandName?: string;\n price: number;\n currency: string;\n imageFileId?: string | null;\n onOpen: () => void;\n className?: string;\n}\n\nexport const ShopItemCard: FC<ShopItemCardProps> = ({\n id,\n name,\n brandName,\n price,\n currency,\n imageFileId,\n onOpen,\n className,\n}) => {\n return (\n <button\n type=\"button\"\n onClick={onOpen}\n aria-label={`Shop ${name}`}\n className={cn(\n 'group flex w-full flex-col overflow-hidden rounded-xl border border-border-seam bg-bg-surface text-left transition-shadow hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n className\n )}\n >\n <Thumbnail\n icon={<ShoppingBag className=\"h-7 w-7\" />}\n seed={id}\n fileId={imageFileId}\n alt={name}\n className=\"aspect-square w-full rounded-none\"\n />\n <div className=\"flex flex-col gap-0.5 p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{name}</p>\n {brandName ? <p className=\"truncate text-xs text-text-secondary\">{brandName}</p> : null}\n <div className=\"mt-2 flex items-center justify-between gap-2\">\n <span className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {formatPrice(price, currency)}\n </span>\n <span className=\"inline-flex items-center gap-0.5 text-xs font-semibold text-accent-primary\">\n Shop\n <ChevronRight className=\"h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5\" />\n </span>\n </div>\n </div>\n </button>\n );\n};\n"],"mappings":";;;;;;AA0BA,IAAa,KAAuC,EAClD,OACA,SACA,cACA,UACA,aACA,gBACA,WACA,mBAGE,kBAAC,UAAD;CACE,MAAK;CACL,SAAS;CACT,cAAY,QAAQ;CACpB,WAAW,EACT,0OACA,EACD;WAPH,CASE,kBAAC,GAAD;EACE,MAAM,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;EACzC,MAAM;EACN,QAAQ;EACR,KAAK;EACL,WAAU;EACV,CAAA,EACF,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,KAAD;IAAG,WAAU;cAAkD;IAAS,CAAA;GACvE,IAAY,kBAAC,KAAD;IAAG,WAAU;cAAwC;IAAc,CAAA,GAAG;GACnF,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eACb,EAAY,GAAO,EAAS;KACxB,CAAA,EACP,kBAAC,QAAD;KAAM,WAAU;eAAhB,CAA6F,QAE3F,kBAAC,GAAD,EAAc,WAAU,gEAAiE,CAAA,CACpF;OACH;;GACF;IACC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { titleCase as t } from "../utils/formatters.js";
|
|
3
|
+
import { Check as n } from "lucide-react";
|
|
4
|
+
import { jsx as r, jsxs as i } from "react/jsx-runtime";
|
|
5
|
+
//#region src/coolandlovely/components/StepTracker.tsx
|
|
6
|
+
var a = ({ steps: a, current: o, className: s }) => {
|
|
7
|
+
let c = a.findIndex((e) => e === o), l = c === -1 ? 0 : c;
|
|
8
|
+
return /* @__PURE__ */ r("ol", {
|
|
9
|
+
className: e("flex w-full items-start", s),
|
|
10
|
+
children: a.map((a, o) => {
|
|
11
|
+
let s = o < l ? "done" : o === l ? "current" : "upcoming";
|
|
12
|
+
return /* @__PURE__ */ i("li", {
|
|
13
|
+
className: "relative flex flex-1 flex-col items-center gap-2 text-center",
|
|
14
|
+
children: [
|
|
15
|
+
o > 0 ? /* @__PURE__ */ r("span", {
|
|
16
|
+
"aria-hidden": "true",
|
|
17
|
+
className: e("absolute left-[-50%] top-4 z-0 h-0.5 w-full -translate-y-1/2", o <= l ? "bg-accent-primary" : "bg-border-seam")
|
|
18
|
+
}) : null,
|
|
19
|
+
/* @__PURE__ */ r("span", {
|
|
20
|
+
className: e("relative z-10 flex h-8 w-8 items-center justify-center rounded-full text-xs font-semibold tabular-nums transition-colors", s === "done" && "bg-accent-primary text-action-primary-text", s === "current" && "border-2 border-accent-primary bg-bg-surface text-accent-primary", s === "upcoming" && "border border-border-seam bg-bg-surface text-text-secondary"),
|
|
21
|
+
children: s === "done" ? /* @__PURE__ */ r(n, { className: "h-4 w-4" }) : o + 1
|
|
22
|
+
}),
|
|
23
|
+
/* @__PURE__ */ r("span", {
|
|
24
|
+
className: e("text-xs font-medium", s === "upcoming" ? "text-text-secondary" : "text-text-primary"),
|
|
25
|
+
children: t(a)
|
|
26
|
+
})
|
|
27
|
+
]
|
|
28
|
+
}, a);
|
|
29
|
+
})
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { a as StepTracker };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=StepTracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StepTracker.js","names":[],"sources":["../../../src/coolandlovely/components/StepTracker.tsx"],"sourcesContent":["/**\n * StepTracker — a horizontal lifecycle tracker for any `Coo*Status` machine.\n * @module coolandlovely/components\n *\n * Renders a row of numbered nodes with connectors: steps before `current` are\n * done (accent-filled + check), the `current` step is ringed, upcoming steps are\n * muted. Token-only. Used by the collaboration lifecycle (Proposed → Accepted →\n * In progress → Published) and reusable for brand/collection lifecycles.\n */\n\nimport type { FC } from 'react';\nimport { Check } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { titleCase } from '../utils/formatters';\n\ninterface StepTrackerProps {\n /** Ordered step values (e.g. ['PROPOSED','ACCEPTED','IN_PROGRESS','PUBLISHED']). */\n steps: string[];\n /** The current step value — must be one of `steps`. */\n current: string;\n className?: string;\n}\n\nexport const StepTracker: FC<StepTrackerProps> = ({ steps, current, className }) => {\n const foundIdx = steps.findIndex((step) => step === current);\n const currentIdx = foundIdx === -1 ? 0 : foundIdx;\n\n return (\n <ol className={cn('flex w-full items-start', className)}>\n {steps.map((step, index) => {\n const state = index < currentIdx ? 'done' : index === currentIdx ? 'current' : 'upcoming';\n const connectorDone = index <= currentIdx;\n return (\n <li key={step} className=\"relative flex flex-1 flex-col items-center gap-2 text-center\">\n {index > 0 ? (\n <span\n aria-hidden=\"true\"\n className={cn(\n 'absolute left-[-50%] top-4 z-0 h-0.5 w-full -translate-y-1/2',\n connectorDone ? 'bg-accent-primary' : 'bg-border-seam'\n )}\n />\n ) : null}\n <span\n className={cn(\n 'relative z-10 flex h-8 w-8 items-center justify-center rounded-full text-xs font-semibold tabular-nums transition-colors',\n state === 'done' && 'bg-accent-primary text-action-primary-text',\n state === 'current' &&\n 'border-2 border-accent-primary bg-bg-surface text-accent-primary',\n state === 'upcoming' &&\n 'border border-border-seam bg-bg-surface text-text-secondary'\n )}\n >\n {state === 'done' ? <Check className=\"h-4 w-4\" /> : index + 1}\n </span>\n <span\n className={cn(\n 'text-xs font-medium',\n state === 'upcoming' ? 'text-text-secondary' : 'text-text-primary'\n )}\n >\n {titleCase(step)}\n </span>\n </li>\n );\n })}\n </ol>\n );\n};\n"],"mappings":";;;;;AAuBA,IAAa,KAAqC,EAAE,UAAO,YAAS,mBAAgB;CAClF,IAAM,IAAW,EAAM,WAAW,MAAS,MAAS,EAAQ,EACtD,IAAa,MAAa,KAAK,IAAI;AAEzC,QACE,kBAAC,MAAD;EAAI,WAAW,EAAG,2BAA2B,EAAU;YACpD,EAAM,KAAK,GAAM,MAAU;GAC1B,IAAM,IAAQ,IAAQ,IAAa,SAAS,MAAU,IAAa,YAAY;AAE/E,UACE,kBAAC,MAAD;IAAe,WAAU;cAAzB;KACG,IAAQ,IACP,kBAAC,QAAD;MACE,eAAY;MACZ,WAAW,EACT,gEAPY,KAAS,IAQL,sBAAsB,iBACvC;MACD,CAAA,GACA;KACJ,kBAAC,QAAD;MACE,WAAW,EACT,4HACA,MAAU,UAAU,8CACpB,MAAU,aACR,oEACF,MAAU,cACR,8DACH;gBAEA,MAAU,SAAS,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA,GAAG,IAAQ;MACvD,CAAA;KACP,kBAAC,QAAD;MACE,WAAW,EACT,uBACA,MAAU,aAAa,wBAAwB,oBAChD;gBAEA,EAAU,EAAK;MACX,CAAA;KACJ;MA9BI,EA8BJ;IAEP;EACC,CAAA"}
|
|
@@ -15,20 +15,24 @@ function a(e) {
|
|
|
15
15
|
for (let n = 0; n < e.length; n += 1) t = (t * 31 + e.charCodeAt(n)) % 997;
|
|
16
16
|
return i[t % i.length];
|
|
17
17
|
}
|
|
18
|
-
var o = ({ icon: i, seed: o, className: s, label: c, fileId: l, alt: u }) => /* @__PURE__ */ r("div", {
|
|
18
|
+
var o = ({ icon: i, seed: o, className: s, label: c, fileId: l, alt: u, overlay: d }) => /* @__PURE__ */ r("div", {
|
|
19
19
|
className: e("relative flex items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br", a(o), s),
|
|
20
20
|
"aria-hidden": c ? void 0 : !0,
|
|
21
21
|
"aria-label": c,
|
|
22
|
-
children: [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
children: [
|
|
23
|
+
/* @__PURE__ */ n("span", {
|
|
24
|
+
className: "text-text-secondary opacity-80 transition-transform duration-300 motion-safe:group-hover:scale-[1.03]",
|
|
25
|
+
children: i
|
|
26
|
+
}),
|
|
27
|
+
l ? /* @__PURE__ */ n("span", {
|
|
28
|
+
className: "absolute inset-0 transition-transform duration-300 motion-safe:group-hover:scale-[1.03]",
|
|
29
|
+
children: /* @__PURE__ */ n(t, {
|
|
30
|
+
fileId: l,
|
|
31
|
+
alt: u ?? c ?? ""
|
|
32
|
+
})
|
|
33
|
+
}) : null,
|
|
34
|
+
d
|
|
35
|
+
]
|
|
32
36
|
});
|
|
33
37
|
//#endregion
|
|
34
38
|
export { o as Thumbnail };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Thumbnail.js","names":[],"sources":["../../../src/coolandlovely/components/Thumbnail.tsx"],"sourcesContent":["/**\n * Thumbnail Component\n * @module coolandlovely/components\n *\n * Renders a calm, on-token visual placeholder for a fashion entity. Imagery in\n * CoolAndLovely is referenced from wspace-files-svc via federation (we only\n * have the File id here, not a resolvable URL), so the catalogue/wardrobe/look\n * surfaces render a branded glyph tile keyed by an icon rather than a broken\n * <img>. The seed (entity id) gives each tile a stable accent tint.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { cn } from '../utils/cn';\nimport { FileImage } from './FileImage';\n\ninterface ThumbnailProps {\n icon: ReactNode;\n /** Stable seed (e.g. entity id) used to pick a deterministic accent tint. */\n seed?: string;\n className?: string;\n /** Whether a File reference exists (shows a subtle \"image attached\" hint). */\n hasImage?: boolean;\n label?: string;\n /** wspace-files File id — when it resolves, its image overlays the glyph. */\n fileId?: string | null;\n /** Alt text when a real image is shown. */\n alt?: string;\n}\n\nconst tints = [\n 'from-accent-purple-subtle to-bg-sunken',\n 'from-accent-blue-subtle to-bg-sunken',\n 'from-status-success-bg-subtle to-bg-sunken',\n 'from-status-warning-bg-subtle to-bg-sunken',\n 'from-[var(--color-accent-soft)] to-bg-sunken',\n];\n\nfunction tintFor(seed: string | undefined): string {\n if (!seed) return tints[0];\n let hash = 0;\n for (let i = 0; i < seed.length; i += 1) {\n hash = (hash * 31 + seed.charCodeAt(i)) % 997;\n }\n return tints[hash % tints.length];\n}\n\nexport const Thumbnail: FC<ThumbnailProps> = ({
|
|
1
|
+
{"version":3,"file":"Thumbnail.js","names":[],"sources":["../../../src/coolandlovely/components/Thumbnail.tsx"],"sourcesContent":["/**\n * Thumbnail Component\n * @module coolandlovely/components\n *\n * Renders a calm, on-token visual placeholder for a fashion entity. Imagery in\n * CoolAndLovely is referenced from wspace-files-svc via federation (we only\n * have the File id here, not a resolvable URL), so the catalogue/wardrobe/look\n * surfaces render a branded glyph tile keyed by an icon rather than a broken\n * <img>. The seed (entity id) gives each tile a stable accent tint.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { cn } from '../utils/cn';\nimport { FileImage } from './FileImage';\n\ninterface ThumbnailProps {\n icon: ReactNode;\n /** Stable seed (e.g. entity id) used to pick a deterministic accent tint. */\n seed?: string;\n className?: string;\n /** Whether a File reference exists (shows a subtle \"image attached\" hint). */\n hasImage?: boolean;\n label?: string;\n /** wspace-files File id — when it resolves, its image overlays the glyph. */\n fileId?: string | null;\n /** Alt text when a real image is shown. */\n alt?: string;\n /**\n * Optional content rendered absolutely over the image (e.g. a gradient scrim\n * + overlaid caption). Positioned by the caller; sits above the FileImage.\n */\n overlay?: ReactNode;\n}\n\nconst tints = [\n 'from-accent-purple-subtle to-bg-sunken',\n 'from-accent-blue-subtle to-bg-sunken',\n 'from-status-success-bg-subtle to-bg-sunken',\n 'from-status-warning-bg-subtle to-bg-sunken',\n 'from-[var(--color-accent-soft)] to-bg-sunken',\n];\n\nfunction tintFor(seed: string | undefined): string {\n if (!seed) return tints[0];\n let hash = 0;\n for (let i = 0; i < seed.length; i += 1) {\n hash = (hash * 31 + seed.charCodeAt(i)) % 997;\n }\n return tints[hash % tints.length];\n}\n\nexport const Thumbnail: FC<ThumbnailProps> = ({\n icon,\n seed,\n className,\n label,\n fileId,\n alt,\n overlay,\n}) => {\n return (\n <div\n className={cn(\n 'relative flex items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br',\n tintFor(seed),\n className\n )}\n aria-hidden={label ? undefined : true}\n aria-label={label}\n >\n <span className=\"text-text-secondary opacity-80 transition-transform duration-300 motion-safe:group-hover:scale-[1.03]\">\n {icon}\n </span>\n {fileId ? (\n <span className=\"absolute inset-0 transition-transform duration-300 motion-safe:group-hover:scale-[1.03]\">\n <FileImage fileId={fileId} alt={alt ?? label ?? ''} />\n </span>\n ) : null}\n {overlay}\n </div>\n );\n};\n"],"mappings":";;;;AAkCA,IAAM,IAAQ;CACZ;CACA;CACA;CACA;CACA;CACD;AAED,SAAS,EAAQ,GAAkC;AACjD,KAAI,CAAC,EAAM,QAAO,EAAM;CACxB,IAAI,IAAO;AACX,MAAK,IAAI,IAAI,GAAG,IAAI,EAAK,QAAQ,KAAK,EACpC,MAAQ,IAAO,KAAK,EAAK,WAAW,EAAE,IAAI;AAE5C,QAAO,EAAM,IAAO,EAAM;;AAG5B,IAAa,KAAiC,EAC5C,SACA,SACA,cACA,UACA,WACA,QACA,iBAGE,kBAAC,OAAD;CACE,WAAW,EACT,0FACA,EAAQ,EAAK,EACb,EACD;CACD,eAAa,IAAQ,KAAA,IAAY;CACjC,cAAY;WAPd;EASE,kBAAC,QAAD;GAAM,WAAU;aACb;GACI,CAAA;EACN,IACC,kBAAC,QAAD;GAAM,WAAU;aACd,kBAAC,GAAD;IAAmB;IAAQ,KAAK,KAAO,KAAS;IAAM,CAAA;GACjD,CAAA,GACL;EACH;EACG"}
|