@burdenoff/microfe-comradecircle 2026.704.1 → 2026.704.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 (34) hide show
  1. package/dist/comradecircle/components/Avatar.js +27 -15
  2. package/dist/comradecircle/components/Avatar.js.map +1 -1
  3. package/dist/comradecircle/components/AvatarUploadField.js +80 -0
  4. package/dist/comradecircle/components/AvatarUploadField.js.map +1 -0
  5. package/dist/comradecircle/components/CircleCard.js +2 -1
  6. package/dist/comradecircle/components/CircleCard.js.map +1 -1
  7. package/dist/comradecircle/components/CreateCircleDialog.js +74 -63
  8. package/dist/comradecircle/components/CreateCircleDialog.js.map +1 -1
  9. package/dist/comradecircle/components/FileImage.js +20 -0
  10. package/dist/comradecircle/components/FileImage.js.map +1 -0
  11. package/dist/comradecircle/components/ImageAttachments.js +97 -0
  12. package/dist/comradecircle/components/ImageAttachments.js.map +1 -0
  13. package/dist/comradecircle/components/MemberIdentity.js +2 -1
  14. package/dist/comradecircle/components/MemberIdentity.js.map +1 -1
  15. package/dist/comradecircle/components/PostCard.js +7 -14
  16. package/dist/comradecircle/components/PostCard.js.map +1 -1
  17. package/dist/comradecircle/components/PostComposer.js +45 -35
  18. package/dist/comradecircle/components/PostComposer.js.map +1 -1
  19. package/dist/comradecircle/hooks/index.js +1 -0
  20. package/dist/comradecircle/hooks/useComradeCircleApi.js +244 -241
  21. package/dist/comradecircle/hooks/useComradeCircleApi.js.map +1 -1
  22. package/dist/comradecircle/hooks/useFiles.js +78 -0
  23. package/dist/comradecircle/hooks/useFiles.js.map +1 -0
  24. package/dist/comradecircle/lib/graphql-cache.js +9 -0
  25. package/dist/comradecircle/lib/graphql-cache.js.map +1 -0
  26. package/dist/comradecircle/pages/circles/CircleDetailPage.js +2 -1
  27. package/dist/comradecircle/pages/circles/CircleDetailPage.js.map +1 -1
  28. package/dist/comradecircle/pages/members/MemberProfilePage.js +2 -1
  29. package/dist/comradecircle/pages/members/MemberProfilePage.js.map +1 -1
  30. package/dist/comradecircle/pages/members/MembersPage.js +2 -1
  31. package/dist/comradecircle/pages/members/MembersPage.js.map +1 -1
  32. package/dist/comradecircle/pages/profile/MyProfilePage.js +100 -89
  33. package/dist/comradecircle/pages/profile/MyProfilePage.js.map +1 -1
  34. package/package.json +1 -1
@@ -1,35 +1,47 @@
1
1
  import { cn as e } from "../utils/cn.js";
2
- import { jsx as t, jsxs as n } from "react/jsx-runtime";
2
+ import { FileImage as t } from "./FileImage.js";
3
+ import { jsx as n, jsxs as r } from "react/jsx-runtime";
3
4
  //#region src/comradecircle/components/Avatar.tsx
4
- var r = {
5
+ var i = {
5
6
  xs: "h-6 w-6 text-[0.6rem]",
6
7
  sm: "h-8 w-8 text-xs",
7
8
  md: "h-10 w-10 text-sm",
8
9
  lg: "h-14 w-14 text-lg",
9
10
  xl: "h-20 w-20 text-2xl"
10
- }, i = {
11
+ }, a = {
11
12
  ONLINE: "bg-status-success-text",
12
13
  AWAY: "bg-status-warning-text",
13
14
  BUSY: "bg-status-error-text",
14
15
  OFFLINE: "bg-text-tertiary"
15
16
  };
16
- function a(e) {
17
+ function o(e) {
17
18
  if (!e) return "?";
18
19
  let t = e.trim().split(/\s+/).filter(Boolean);
19
20
  return t.length === 0 ? "?" : t.length === 1 ? t[0].slice(0, 2).toUpperCase() : (t[0][0] + t[t.length - 1][0]).toUpperCase();
20
21
  }
21
- var o = ({ name: o, size: s = "md", presence: c, className: l }) => /* @__PURE__ */ n("span", {
22
- className: e("relative inline-flex shrink-0", l),
23
- children: [/* @__PURE__ */ t("span", {
22
+ var s = ({ name: s, size: c = "md", presence: l, fileId: u, className: d }) => {
23
+ let f = /* @__PURE__ */ n("span", {
24
24
  "aria-hidden": "true",
25
- className: e("inline-flex items-center justify-center rounded-full bg-[var(--color-accent-soft)] font-semibold text-text-primary ring-1 ring-border-seam", r[s]),
26
- children: a(o)
27
- }), c && /* @__PURE__ */ t("span", {
28
- className: e("absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full ring-2 ring-bg-surface", i[c], c === "ONLINE" && "motion-safe:animate-vc-glow-pulse"),
29
- "aria-label": `Presence: ${c.toLowerCase()}`
30
- })]
31
- });
25
+ className: e("flex h-full w-full items-center justify-center rounded-full bg-[var(--color-accent-soft)] font-semibold text-text-primary", i[c]),
26
+ children: o(s)
27
+ });
28
+ return /* @__PURE__ */ r("span", {
29
+ className: e("relative inline-flex shrink-0", d),
30
+ children: [/* @__PURE__ */ n("span", {
31
+ className: e("inline-flex items-center justify-center overflow-hidden rounded-full ring-1 ring-border-seam", i[c]),
32
+ children: u ? /* @__PURE__ */ n(t, {
33
+ fileId: u,
34
+ alt: s ?? "Avatar",
35
+ className: "h-full w-full rounded-full",
36
+ fallback: f
37
+ }) : f
38
+ }), l && /* @__PURE__ */ n("span", {
39
+ className: e("absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full ring-2 ring-bg-surface", a[l], l === "ONLINE" && "motion-safe:animate-vc-glow-pulse"),
40
+ "aria-label": `Presence: ${l.toLowerCase()}`
41
+ })]
42
+ });
43
+ };
32
44
  //#endregion
33
- export { o as Avatar };
45
+ export { s as Avatar };
34
46
 
35
47
  //# sourceMappingURL=Avatar.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Avatar.js","names":[],"sources":["../../../src/comradecircle/components/Avatar.tsx"],"sourcesContent":["/**\n * Avatar — initials-based, token-only avatar with an optional presence dot.\n * @module comradecircle/components\n *\n * File-backed avatars are not rendered here: in this subgraph `File` resolves\n * `id` only (no URL), so we render deterministic initials on the brand\n * accent-soft tint. Clean, theme-aware, and never a broken image.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport type { ComPresence } from '../types';\n\ntype AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n\nconst sizeClass: Record<AvatarSize, string> = {\n xs: 'h-6 w-6 text-[0.6rem]',\n sm: 'h-8 w-8 text-xs',\n md: 'h-10 w-10 text-sm',\n lg: 'h-14 w-14 text-lg',\n xl: 'h-20 w-20 text-2xl',\n};\n\nconst presenceDot: Record<ComPresence, string> = {\n ONLINE: 'bg-status-success-text',\n AWAY: 'bg-status-warning-text',\n BUSY: 'bg-status-error-text',\n OFFLINE: 'bg-text-tertiary',\n};\n\nexport function initialsOf(name: string | null | undefined): string {\n if (!name) return '?';\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return '?';\n if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase();\n return (parts[0]![0]! + parts[parts.length - 1]![0]!).toUpperCase();\n}\n\ninterface AvatarProps {\n name?: string | null;\n size?: AvatarSize;\n presence?: ComPresence | null;\n className?: string;\n}\n\nexport const Avatar: FC<AvatarProps> = ({ name, size = 'md', presence, className }) => {\n return (\n <span className={cn('relative inline-flex shrink-0', className)}>\n <span\n aria-hidden=\"true\"\n className={cn(\n 'inline-flex items-center justify-center rounded-full bg-[var(--color-accent-soft)] font-semibold text-text-primary ring-1 ring-border-seam',\n sizeClass[size]\n )}\n >\n {initialsOf(name)}\n </span>\n {presence && (\n <span\n className={cn(\n 'absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full ring-2 ring-bg-surface',\n presenceDot[presence],\n presence === 'ONLINE' && 'motion-safe:animate-vc-glow-pulse'\n )}\n aria-label={`Presence: ${presence.toLowerCase()}`}\n />\n )}\n </span>\n );\n};\n"],"mappings":";;;AAeA,IAAM,IAAwC;CAC5C,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACL,EAEK,IAA2C;CAC/C,QAAQ;CACR,MAAM;CACN,MAAM;CACN,SAAS;CACV;AAED,SAAgB,EAAW,GAAyC;AAClE,KAAI,CAAC,EAAM,QAAO;CAClB,IAAM,IAAQ,EAAK,MAAM,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ;AAGtD,QAFI,EAAM,WAAW,IAAU,MAC3B,EAAM,WAAW,IAAU,EAAM,GAAI,MAAM,GAAG,EAAE,CAAC,aAAa,IAC1D,EAAM,GAAI,KAAM,EAAM,EAAM,SAAS,GAAI,IAAK,aAAa;;AAUrE,IAAa,KAA2B,EAAE,SAAM,UAAO,MAAM,aAAU,mBAEnE,kBAAC,QAAD;CAAM,WAAW,EAAG,iCAAiC,EAAU;WAA/D,CACE,kBAAC,QAAD;EACE,eAAY;EACZ,WAAW,EACT,8IACA,EAAU,GACX;YAEA,EAAW,EAAK;EACZ,CAAA,EACN,KACC,kBAAC,QAAD;EACE,WAAW,EACT,mFACA,EAAY,IACZ,MAAa,YAAY,oCAC1B;EACD,cAAY,aAAa,EAAS,aAAa;EAC/C,CAAA,CAEC"}
1
+ {"version":3,"file":"Avatar.js","names":[],"sources":["../../../src/comradecircle/components/Avatar.tsx"],"sourcesContent":["/**\n * Avatar — initials-based, token-only avatar with an optional presence dot.\n * @module comradecircle/components\n *\n * File-backed avatars are not rendered here: in this subgraph `File` resolves\n * `id` only (no URL), so we render deterministic initials on the brand\n * accent-soft tint. Clean, theme-aware, and never a broken image.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport { FileImage } from './FileImage';\nimport type { ComPresence } from '../types';\n\ntype AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n\nconst sizeClass: Record<AvatarSize, string> = {\n xs: 'h-6 w-6 text-[0.6rem]',\n sm: 'h-8 w-8 text-xs',\n md: 'h-10 w-10 text-sm',\n lg: 'h-14 w-14 text-lg',\n xl: 'h-20 w-20 text-2xl',\n};\n\nconst presenceDot: Record<ComPresence, string> = {\n ONLINE: 'bg-status-success-text',\n AWAY: 'bg-status-warning-text',\n BUSY: 'bg-status-error-text',\n OFFLINE: 'bg-text-tertiary',\n};\n\nexport function initialsOf(name: string | null | undefined): string {\n if (!name) return '?';\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return '?';\n if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase();\n return (parts[0]![0]! + parts[parts.length - 1]![0]!).toUpperCase();\n}\n\ninterface AvatarProps {\n name?: string | null;\n size?: AvatarSize;\n presence?: ComPresence | null;\n /** wspace-files-svc file id — when set + resolvable, renders the image. */\n fileId?: string | null;\n className?: string;\n}\n\nexport const Avatar: FC<AvatarProps> = ({ name, size = 'md', presence, fileId, className }) => {\n const initials = (\n <span\n aria-hidden=\"true\"\n className={cn(\n 'flex h-full w-full items-center justify-center rounded-full bg-[var(--color-accent-soft)] font-semibold text-text-primary',\n sizeClass[size]\n )}\n >\n {initialsOf(name)}\n </span>\n );\n\n return (\n <span className={cn('relative inline-flex shrink-0', className)}>\n <span\n className={cn(\n 'inline-flex items-center justify-center overflow-hidden rounded-full ring-1 ring-border-seam',\n sizeClass[size]\n )}\n >\n {fileId ? (\n <FileImage\n fileId={fileId}\n alt={name ?? 'Avatar'}\n className=\"h-full w-full rounded-full\"\n fallback={initials}\n />\n ) : (\n initials\n )}\n </span>\n {presence && (\n <span\n className={cn(\n 'absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full ring-2 ring-bg-surface',\n presenceDot[presence],\n presence === 'ONLINE' && 'motion-safe:animate-vc-glow-pulse'\n )}\n aria-label={`Presence: ${presence.toLowerCase()}`}\n />\n )}\n </span>\n );\n};\n"],"mappings":";;;;AAgBA,IAAM,IAAwC;CAC5C,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACL,EAEK,IAA2C;CAC/C,QAAQ;CACR,MAAM;CACN,MAAM;CACN,SAAS;CACV;AAED,SAAgB,EAAW,GAAyC;AAClE,KAAI,CAAC,EAAM,QAAO;CAClB,IAAM,IAAQ,EAAK,MAAM,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ;AAGtD,QAFI,EAAM,WAAW,IAAU,MAC3B,EAAM,WAAW,IAAU,EAAM,GAAI,MAAM,GAAG,EAAE,CAAC,aAAa,IAC1D,EAAM,GAAI,KAAM,EAAM,EAAM,SAAS,GAAI,IAAK,aAAa;;AAYrE,IAAa,KAA2B,EAAE,SAAM,UAAO,MAAM,aAAU,WAAQ,mBAAgB;CAC7F,IAAM,IACJ,kBAAC,QAAD;EACE,eAAY;EACZ,WAAW,EACT,6HACA,EAAU,GACX;YAEA,EAAW,EAAK;EACZ,CAAA;AAGT,QACE,kBAAC,QAAD;EAAM,WAAW,EAAG,iCAAiC,EAAU;YAA/D,CACE,kBAAC,QAAD;GACE,WAAW,EACT,gGACA,EAAU,GACX;aAEA,IACC,kBAAC,GAAD;IACU;IACR,KAAK,KAAQ;IACb,WAAU;IACV,UAAU;IACV,CAAA,GAEF;GAEG,CAAA,EACN,KACC,kBAAC,QAAD;GACE,WAAW,EACT,mFACA,EAAY,IACZ,MAAa,YAAY,oCAC1B;GACD,cAAY,aAAa,EAAS,aAAa;GAC/C,CAAA,CAEC"}
@@ -0,0 +1,80 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { FILE_FOLDERS as t, uploadImageToFiles as n } from "../hooks/useFiles.js";
3
+ import { Avatar as r } from "./Avatar.js";
4
+ import { describeError as i, notify as a } from "../lib/toast.js";
5
+ import { useId as o, useRef as s, useState as c } from "react";
6
+ import { Camera as l, Loader2 as u } from "lucide-react";
7
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
8
+ //#region src/comradecircle/components/AvatarUploadField.tsx
9
+ var p = 5 * 1024 * 1024;
10
+ function m({ fileId: m, name: h, onUploaded: g, label: _ = "Upload avatar", disabled: v = !1 }) {
11
+ let y = s(null), b = o(), [x, S] = c(!1), [C, w] = c(null);
12
+ return /* @__PURE__ */ f("div", {
13
+ className: "flex items-center gap-4",
14
+ children: [/* @__PURE__ */ f("div", {
15
+ className: "relative",
16
+ children: [C ? /* @__PURE__ */ d("img", {
17
+ src: C,
18
+ alt: h ?? "Avatar",
19
+ className: "h-16 w-16 rounded-full object-cover ring-1 ring-border-seam"
20
+ }) : /* @__PURE__ */ d(r, {
21
+ name: h,
22
+ fileId: m,
23
+ size: "lg"
24
+ }), x && /* @__PURE__ */ d("span", {
25
+ className: "absolute inset-0 flex items-center justify-center rounded-full bg-bg-canvas/60",
26
+ children: /* @__PURE__ */ d(u, { className: "h-5 w-5 animate-spin text-text-primary" })
27
+ })]
28
+ }), /* @__PURE__ */ f("div", { children: [
29
+ /* @__PURE__ */ d("input", {
30
+ ref: y,
31
+ id: b,
32
+ type: "file",
33
+ accept: "image/*",
34
+ className: "sr-only",
35
+ onChange: async (e) => {
36
+ let r = e.target.files?.[0];
37
+ if (e.target.value = "", r) {
38
+ if (!r.type.startsWith("image/")) {
39
+ a.error("Unsupported file", "Please choose an image.");
40
+ return;
41
+ }
42
+ if (r.size > p) {
43
+ a.error("Image too large", "Please choose an image under 5 MB.");
44
+ return;
45
+ }
46
+ S(!0);
47
+ try {
48
+ let e = await n(r, t.avatars);
49
+ w(e.url), g(e.id), a.success("Avatar updated");
50
+ } catch (e) {
51
+ a.error("Upload failed", i(e));
52
+ } finally {
53
+ S(!1);
54
+ }
55
+ }
56
+ },
57
+ disabled: v || x
58
+ }),
59
+ /* @__PURE__ */ f("button", {
60
+ type: "button",
61
+ onClick: () => y.current?.click(),
62
+ disabled: v || x,
63
+ className: e("inline-flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken", (v || x) && "cursor-not-allowed opacity-60"),
64
+ children: [
65
+ /* @__PURE__ */ d(l, { className: "h-4 w-4" }),
66
+ " ",
67
+ _
68
+ ]
69
+ }),
70
+ /* @__PURE__ */ d("p", {
71
+ className: "mt-1 text-xs text-text-tertiary",
72
+ children: "PNG or JPG, up to 5 MB."
73
+ })
74
+ ] })]
75
+ });
76
+ }
77
+ //#endregion
78
+ export { m as AvatarUploadField };
79
+
80
+ //# sourceMappingURL=AvatarUploadField.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AvatarUploadField.js","names":[],"sources":["../../../src/comradecircle/components/AvatarUploadField.tsx"],"sourcesContent":["/**\n * AvatarUploadField — upload a circular avatar image to wspace-files-svc.\n * @module comradecircle/components\n *\n * On select, uploads via {@link uploadImageToFiles} and hands the resulting file\n * id back to the caller (persisted as avatarFileId). Shows the current avatar\n * (by file id) or the freshly-uploaded preview; falls back to initials.\n */\n\nimport { useId, useRef, useState } from 'react';\nimport { Camera, Loader2 } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { Avatar } from './Avatar';\nimport { uploadImageToFiles, FILE_FOLDERS } from '../hooks/useFiles';\nimport { notify, describeError } from '../lib/toast';\n\ninterface AvatarUploadFieldProps {\n /** Current avatar file id (persisted). */\n fileId?: string | null;\n /** Name used for the initials fallback. */\n name?: string | null;\n /** Called with the new file id after a successful upload. */\n onUploaded: (fileId: string) => void;\n label?: string;\n disabled?: boolean;\n}\n\nconst MAX_BYTES = 5 * 1024 * 1024;\n\nexport function AvatarUploadField({\n fileId,\n name,\n onUploaded,\n label = 'Upload avatar',\n disabled = false,\n}: AvatarUploadFieldProps) {\n const inputRef = useRef<HTMLInputElement | null>(null);\n const inputId = useId();\n const [uploading, setUploading] = useState(false);\n const [previewUrl, setPreviewUrl] = useState<string | null>(null);\n\n const handleSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n event.target.value = '';\n if (!file) return;\n if (!file.type.startsWith('image/')) {\n notify.error('Unsupported file', 'Please choose an image.');\n return;\n }\n if (file.size > MAX_BYTES) {\n notify.error('Image too large', 'Please choose an image under 5 MB.');\n return;\n }\n setUploading(true);\n try {\n const uploaded = await uploadImageToFiles(file, FILE_FOLDERS.avatars);\n setPreviewUrl(uploaded.url);\n onUploaded(uploaded.id);\n notify.success('Avatar updated');\n } catch (err) {\n notify.error('Upload failed', describeError(err));\n } finally {\n setUploading(false);\n }\n };\n\n return (\n <div className=\"flex items-center gap-4\">\n <div className=\"relative\">\n {previewUrl ? (\n <img\n src={previewUrl}\n alt={name ?? 'Avatar'}\n className=\"h-16 w-16 rounded-full object-cover ring-1 ring-border-seam\"\n />\n ) : (\n <Avatar name={name} fileId={fileId} size=\"lg\" />\n )}\n {uploading && (\n <span className=\"absolute inset-0 flex items-center justify-center rounded-full bg-bg-canvas/60\">\n <Loader2 className=\"h-5 w-5 animate-spin text-text-primary\" />\n </span>\n )}\n </div>\n <div>\n <input\n ref={inputRef}\n id={inputId}\n type=\"file\"\n accept=\"image/*\"\n className=\"sr-only\"\n onChange={handleSelect}\n disabled={disabled || uploading}\n />\n <button\n type=\"button\"\n onClick={() => inputRef.current?.click()}\n disabled={disabled || uploading}\n className={cn(\n 'inline-flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken',\n (disabled || uploading) && 'cursor-not-allowed opacity-60'\n )}\n >\n <Camera className=\"h-4 w-4\" /> {label}\n </button>\n <p className=\"mt-1 text-xs text-text-tertiary\">PNG or JPG, up to 5 MB.</p>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;AA2BA,IAAM,IAAY,IAAI,OAAO;AAE7B,SAAgB,EAAkB,EAChC,WACA,SACA,eACA,WAAQ,iBACR,cAAW,MACc;CACzB,IAAM,IAAW,EAAgC,KAAK,EAChD,IAAU,GAAO,EACjB,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,CAAC,GAAY,KAAiB,EAAwB,KAAK;AA2BjE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,IACC,kBAAC,OAAD;IACE,KAAK;IACL,KAAK,KAAQ;IACb,WAAU;IACV,CAAA,GAEF,kBAAC,GAAD;IAAc;IAAc;IAAQ,MAAK;IAAO,CAAA,EAEjD,KACC,kBAAC,QAAD;IAAM,WAAU;cACd,kBAAC,GAAD,EAAS,WAAU,0CAA2C,CAAA;IACzD,CAAA,CAEL;MACN,kBAAC,OAAD,EAAA,UAAA;GACE,kBAAC,SAAD;IACE,KAAK;IACL,IAAI;IACJ,MAAK;IACL,QAAO;IACP,WAAU;IACV,UAlDa,OAAO,MAA+C;KACzE,IAAM,IAAO,EAAM,OAAO,QAAQ;AAClC,WAAM,OAAO,QAAQ,IAChB,GACL;UAAI,CAAC,EAAK,KAAK,WAAW,SAAS,EAAE;AACnC,SAAO,MAAM,oBAAoB,0BAA0B;AAC3D;;AAEF,UAAI,EAAK,OAAO,GAAW;AACzB,SAAO,MAAM,mBAAmB,qCAAqC;AACrE;;AAEF,QAAa,GAAK;AAClB,UAAI;OACF,IAAM,IAAW,MAAM,EAAmB,GAAM,EAAa,QAAQ;AAGrE,OAFA,EAAc,EAAS,IAAI,EAC3B,EAAW,EAAS,GAAG,EACvB,EAAO,QAAQ,iBAAiB;eACzB,GAAK;AACZ,SAAO,MAAM,iBAAiB,EAAc,EAAI,CAAC;gBACzC;AACR,SAAa,GAAM;;;;IA8Bf,UAAU,KAAY;IACtB,CAAA;GACF,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAS,SAAS,OAAO;IACxC,UAAU,KAAY;IACtB,WAAW,EACT,6KACC,KAAY,MAAc,gCAC5B;cAPH;KASE,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;;KAAE;KACzB;;GACT,kBAAC,KAAD;IAAG,WAAU;cAAkC;IAA2B,CAAA;GACtE,EAAA,CAAA,CACF"}
@@ -18,7 +18,8 @@ function h({ circle: h, membership: g, onOpen: _ }) {
18
18
  className: "flex items-start gap-3",
19
19
  children: [/* @__PURE__ */ d(n, {
20
20
  name: h.name,
21
- size: "lg"
21
+ size: "lg",
22
+ fileId: h.avatar?.id
22
23
  }), /* @__PURE__ */ f("div", {
23
24
  className: "min-w-0 flex-1",
24
25
  children: [
@@ -1 +1 @@
1
- {"version":3,"file":"CircleCard.js","names":[],"sources":["../../../src/comradecircle/components/CircleCard.tsx"],"sourcesContent":["/**\n * CircleCard — a discovery/overview tile for a community circle.\n * @module comradecircle/components\n *\n * Writes `comJoinCircle` / `comLeaveCircle`. When the caller already belongs to\n * the circle the card shows their role and an Open action; otherwise a Join\n * action. Membership is supplied by the parent (which knows the caller's set).\n */\n\nimport { Users, Hash } from 'lucide-react';\nimport { GlassCard, Button } from '@burdenoff/fe-libs/ui';\nimport { Avatar } from './Avatar';\nimport { StatusBadge } from './StatusBadge';\nimport { useComJoinCircle, useComLeaveCircle } from '../hooks';\nimport { formatEnumLabel, formatNumber } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComCircle, ComMembership } from '../types';\n\ninterface CircleCardProps {\n circle: ComCircle;\n membership?: ComMembership | null;\n onOpen: () => void;\n}\n\nexport function CircleCard({ circle, membership, onOpen }: CircleCardProps) {\n const join = useComJoinCircle();\n const leave = useComLeaveCircle();\n const isMember = membership?.status === 'ACTIVE';\n\n const handleJoin = async () => {\n try {\n await join.mutateAsync(circle.id);\n notify.success('Joined circle', `You are now a member of ${circle.name}.`);\n } catch (err) {\n notify.error('Could not join', describeError(err));\n }\n };\n\n const handleLeave = async () => {\n try {\n await leave.mutateAsync(circle.id);\n notify.success('Left circle', `You have left ${circle.name}.`);\n } catch (err) {\n notify.error('Could not leave', describeError(err));\n }\n };\n\n return (\n <GlassCard treatment=\"plain\" className=\"flex flex-col p-5\">\n <div className=\"flex items-start gap-3\">\n <Avatar name={circle.name} size=\"lg\" />\n <div className=\"min-w-0 flex-1\">\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"truncate text-left text-base font-semibold text-text-primary hover:underline\"\n >\n {circle.name}\n </button>\n {circle.topic && <p className=\"truncate text-sm text-text-secondary\">{circle.topic}</p>}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={circle.visibility} />\n {circle.status !== 'ACTIVE' && <StatusBadge status={circle.status} />}\n {isMember && membership && <StatusBadge status={membership.role} />}\n </div>\n </div>\n </div>\n\n {circle.description && (\n <p className=\"mt-3 line-clamp-2 text-sm text-text-secondary\">{circle.description}</p>\n )}\n\n {circle.tags.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {circle.tags.slice(0, 4).map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n </span>\n ))}\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between gap-3 border-t border-border-seam pt-3\">\n <div className=\"flex items-center gap-3 text-xs text-text-tertiary\">\n <span className=\"inline-flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" /> {formatNumber(circle.memberCount)}\n </span>\n <span className=\"inline-flex items-center gap-1\">\n <Hash className=\"h-3.5 w-3.5\" /> {circle.channels?.length ?? 0}\n </span>\n </div>\n <div className=\"flex items-center gap-2\">\n {isMember ? (\n <>\n <Button size=\"sm\" variant=\"outline\" onClick={handleLeave} disabled={leave.isPending}>\n Leave\n </Button>\n <Button size=\"sm\" onClick={onOpen}>\n Open\n </Button>\n </>\n ) : circle.status === 'ACTIVE' ? (\n <Button size=\"sm\" onClick={handleJoin} disabled={join.isPending}>\n Join\n </Button>\n ) : (\n <span className=\"text-xs text-text-tertiary\">{formatEnumLabel(circle.status)}</span>\n )}\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;;;AAwBA,SAAgB,EAAW,EAAE,WAAQ,eAAY,aAA2B;CAC1E,IAAM,IAAO,GAAkB,EACzB,IAAQ,GAAmB,EAC3B,IAAW,GAAY,WAAW;AAoBxC,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAQ,MAAM,EAAO;KAAM,MAAK;KAAO,CAAA,EACvC,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,WAAU;iBAET,EAAO;OACD,CAAA;MACR,EAAO,SAAS,kBAAC,KAAD;OAAG,WAAU;iBAAwC,EAAO;OAAU,CAAA;MACvF,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD,EAAa,QAAQ,EAAO,YAAc,CAAA;QACzC,EAAO,WAAW,YAAY,kBAAC,GAAD,EAAa,QAAQ,EAAO,QAAU,CAAA;QACpE,KAAY,KAAc,kBAAC,GAAD,EAAa,QAAQ,EAAW,MAAQ,CAAA;QAC/D;;MACF;OACF;;GAEL,EAAO,eACN,kBAAC,KAAD;IAAG,WAAU;cAAiD,EAAO;IAAgB,CAAA;GAGtF,EAAO,KAAK,SAAS,KACpB,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAO,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,MAC5B,kBAAC,QAAD;KAEE,WAAU;eAFZ,CAGC,KACG,EACG;OAJA,EAIA,CACP;IACE,CAAA;GAGR,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA;;OAAE,EAAa,EAAO,YAAY;OAC9D;SACP,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD,EAAM,WAAU,eAAgB,CAAA;;OAAE,EAAO,UAAU,UAAU;OACxD;QACH;QACN,kBAAC,OAAD;KAAK,WAAU;eACZ,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;MAAQ,MAAK;MAAK,SAAQ;MAAU,SA3D5B,YAAY;AAC9B,WAAI;AAEF,QADA,MAAM,EAAM,YAAY,EAAO,GAAG,EAClC,EAAO,QAAQ,eAAe,iBAAiB,EAAO,KAAK,GAAG;gBACvD,GAAK;AACZ,UAAO,MAAM,mBAAmB,EAAc,EAAI,CAAC;;;MAsDe,UAAU,EAAM;gBAAW;MAE5E,CAAA,EACT,kBAAC,GAAD;MAAQ,MAAK;MAAK,SAAS;gBAAQ;MAE1B,CAAA,CACR,EAAA,CAAA,GACD,EAAO,WAAW,WACpB,kBAAC,GAAD;MAAQ,MAAK;MAAK,SA5ET,YAAY;AAC7B,WAAI;AAEF,QADA,MAAM,EAAK,YAAY,EAAO,GAAG,EACjC,EAAO,QAAQ,iBAAiB,2BAA2B,EAAO,KAAK,GAAG;gBACnE,GAAK;AACZ,UAAO,MAAM,kBAAkB,EAAc,EAAI,CAAC;;;MAuEL,UAAU,EAAK;gBAAW;MAExD,CAAA,GAET,kBAAC,QAAD;MAAM,WAAU;gBAA8B,EAAgB,EAAO,OAAO;MAAQ,CAAA;KAElF,CAAA,CACF;;GACI"}
1
+ {"version":3,"file":"CircleCard.js","names":[],"sources":["../../../src/comradecircle/components/CircleCard.tsx"],"sourcesContent":["/**\n * CircleCard — a discovery/overview tile for a community circle.\n * @module comradecircle/components\n *\n * Writes `comJoinCircle` / `comLeaveCircle`. When the caller already belongs to\n * the circle the card shows their role and an Open action; otherwise a Join\n * action. Membership is supplied by the parent (which knows the caller's set).\n */\n\nimport { Users, Hash } from 'lucide-react';\nimport { GlassCard, Button } from '@burdenoff/fe-libs/ui';\nimport { Avatar } from './Avatar';\nimport { StatusBadge } from './StatusBadge';\nimport { useComJoinCircle, useComLeaveCircle } from '../hooks';\nimport { formatEnumLabel, formatNumber } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComCircle, ComMembership } from '../types';\n\ninterface CircleCardProps {\n circle: ComCircle;\n membership?: ComMembership | null;\n onOpen: () => void;\n}\n\nexport function CircleCard({ circle, membership, onOpen }: CircleCardProps) {\n const join = useComJoinCircle();\n const leave = useComLeaveCircle();\n const isMember = membership?.status === 'ACTIVE';\n\n const handleJoin = async () => {\n try {\n await join.mutateAsync(circle.id);\n notify.success('Joined circle', `You are now a member of ${circle.name}.`);\n } catch (err) {\n notify.error('Could not join', describeError(err));\n }\n };\n\n const handleLeave = async () => {\n try {\n await leave.mutateAsync(circle.id);\n notify.success('Left circle', `You have left ${circle.name}.`);\n } catch (err) {\n notify.error('Could not leave', describeError(err));\n }\n };\n\n return (\n <GlassCard treatment=\"plain\" className=\"flex flex-col p-5\">\n <div className=\"flex items-start gap-3\">\n <Avatar name={circle.name} size=\"lg\" fileId={circle.avatar?.id} />\n <div className=\"min-w-0 flex-1\">\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"truncate text-left text-base font-semibold text-text-primary hover:underline\"\n >\n {circle.name}\n </button>\n {circle.topic && <p className=\"truncate text-sm text-text-secondary\">{circle.topic}</p>}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={circle.visibility} />\n {circle.status !== 'ACTIVE' && <StatusBadge status={circle.status} />}\n {isMember && membership && <StatusBadge status={membership.role} />}\n </div>\n </div>\n </div>\n\n {circle.description && (\n <p className=\"mt-3 line-clamp-2 text-sm text-text-secondary\">{circle.description}</p>\n )}\n\n {circle.tags.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {circle.tags.slice(0, 4).map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n </span>\n ))}\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between gap-3 border-t border-border-seam pt-3\">\n <div className=\"flex items-center gap-3 text-xs text-text-tertiary\">\n <span className=\"inline-flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" /> {formatNumber(circle.memberCount)}\n </span>\n <span className=\"inline-flex items-center gap-1\">\n <Hash className=\"h-3.5 w-3.5\" /> {circle.channels?.length ?? 0}\n </span>\n </div>\n <div className=\"flex items-center gap-2\">\n {isMember ? (\n <>\n <Button size=\"sm\" variant=\"outline\" onClick={handleLeave} disabled={leave.isPending}>\n Leave\n </Button>\n <Button size=\"sm\" onClick={onOpen}>\n Open\n </Button>\n </>\n ) : circle.status === 'ACTIVE' ? (\n <Button size=\"sm\" onClick={handleJoin} disabled={join.isPending}>\n Join\n </Button>\n ) : (\n <span className=\"text-xs text-text-tertiary\">{formatEnumLabel(circle.status)}</span>\n )}\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;;;AAwBA,SAAgB,EAAW,EAAE,WAAQ,eAAY,aAA2B;CAC1E,IAAM,IAAO,GAAkB,EACzB,IAAQ,GAAmB,EAC3B,IAAW,GAAY,WAAW;AAoBxC,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAQ,MAAM,EAAO;KAAM,MAAK;KAAK,QAAQ,EAAO,QAAQ;KAAM,CAAA,EAClE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,WAAU;iBAET,EAAO;OACD,CAAA;MACR,EAAO,SAAS,kBAAC,KAAD;OAAG,WAAU;iBAAwC,EAAO;OAAU,CAAA;MACvF,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD,EAAa,QAAQ,EAAO,YAAc,CAAA;QACzC,EAAO,WAAW,YAAY,kBAAC,GAAD,EAAa,QAAQ,EAAO,QAAU,CAAA;QACpE,KAAY,KAAc,kBAAC,GAAD,EAAa,QAAQ,EAAW,MAAQ,CAAA;QAC/D;;MACF;OACF;;GAEL,EAAO,eACN,kBAAC,KAAD;IAAG,WAAU;cAAiD,EAAO;IAAgB,CAAA;GAGtF,EAAO,KAAK,SAAS,KACpB,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAO,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,MAC5B,kBAAC,QAAD;KAEE,WAAU;eAFZ,CAGC,KACG,EACG;OAJA,EAIA,CACP;IACE,CAAA;GAGR,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA;;OAAE,EAAa,EAAO,YAAY;OAC9D;SACP,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD,EAAM,WAAU,eAAgB,CAAA;;OAAE,EAAO,UAAU,UAAU;OACxD;QACH;QACN,kBAAC,OAAD;KAAK,WAAU;eACZ,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;MAAQ,MAAK;MAAK,SAAQ;MAAU,SA3D5B,YAAY;AAC9B,WAAI;AAEF,QADA,MAAM,EAAM,YAAY,EAAO,GAAG,EAClC,EAAO,QAAQ,eAAe,iBAAiB,EAAO,KAAK,GAAG;gBACvD,GAAK;AACZ,UAAO,MAAM,mBAAmB,EAAc,EAAI,CAAC;;;MAsDe,UAAU,EAAM;gBAAW;MAE5E,CAAA,EACT,kBAAC,GAAD;MAAQ,MAAK;MAAK,SAAS;gBAAQ;MAE1B,CAAA,CACR,EAAA,CAAA,GACD,EAAO,WAAW,WACpB,kBAAC,GAAD;MAAQ,MAAK;MAAK,SA5ET,YAAY;AAC7B,WAAI;AAEF,QADA,MAAM,EAAK,YAAY,EAAO,GAAG,EACjC,EAAO,QAAQ,iBAAiB,2BAA2B,EAAO,KAAK,GAAG;gBACnE,GAAK;AACZ,UAAO,MAAM,kBAAkB,EAAc,EAAI,CAAC;;;MAuEL,UAAU,EAAK;gBAAW;MAExD,CAAA,GAET,kBAAC,QAAD;MAAM,WAAU;gBAA8B,EAAgB,EAAO,OAAO;MAAQ,CAAA;KAElF,CAAA,CACF;;GACI"}
@@ -4,138 +4,149 @@ import { describeError as t, notify as n } from "../lib/toast.js";
4
4
  import { formatEnumLabel as r } from "../utils/formatters.js";
5
5
  import { slugify as i } from "../utils/slug.js";
6
6
  import { TagInput as a } from "./TagInput.js";
7
- import { useState as o } from "react";
8
- import { jsx as s, jsxs as c } from "react/jsx-runtime";
9
- import { Button as l, Input as u, Label as d, ResponsiveDialog as f, ResponsiveDialogContent as p, ResponsiveDialogDescription as m, ResponsiveDialogFooter as h, ResponsiveDialogHeader as g, ResponsiveDialogTitle as _, Select as v, SelectContent as y, SelectItem as b, SelectTrigger as x, SelectValue as S, Textarea as C } from "@burdenoff/fe-libs/ui";
7
+ import { AvatarUploadField as o } from "./AvatarUploadField.js";
8
+ import { useState as s } from "react";
9
+ import { jsx as c, jsxs as l } from "react/jsx-runtime";
10
+ import { Button as u, Input as d, Label as f, ResponsiveDialog as p, ResponsiveDialogContent as m, ResponsiveDialogDescription as h, ResponsiveDialogFooter as g, ResponsiveDialogHeader as _, ResponsiveDialogTitle as v, Select as y, SelectContent as b, SelectItem as x, SelectTrigger as S, SelectValue as C, Textarea as w } from "@burdenoff/fe-libs/ui";
10
11
  //#region src/comradecircle/components/CreateCircleDialog.tsx
11
- var w = [
12
+ var T = [
12
13
  "PUBLIC",
13
14
  "PRIVATE",
14
15
  "SECRET"
15
16
  ];
16
- function T({ open: T, onOpenChange: E, onCreated: D }) {
17
- let O = e(), [k, A] = o(""), [j, M] = o(""), [N, P] = o(!1), [F, I] = o(""), [L, R] = o(""), [z, B] = o("PUBLIC"), [V, H] = o([]), [U, W] = o(null), G = N ? j : i(k), K = () => {
18
- A(""), M(""), P(!1), I(""), R(""), B("PUBLIC"), H([]), W(null);
17
+ function E({ open: E, onOpenChange: D, onCreated: O }) {
18
+ let k = e(), [A, j] = s(""), [M, N] = s(""), [P, F] = s(!1), [I, L] = s(""), [R, z] = s(""), [B, V] = s("PUBLIC"), [H, U] = s([]), [W, G] = s(null), [K, q] = s(null), J = P ? M : i(A), Y = () => {
19
+ j(""), N(""), F(!1), L(""), z(""), V("PUBLIC"), U([]), G(null), q(null);
19
20
  };
20
- return /* @__PURE__ */ s(f, {
21
- open: T,
22
- onOpenChange: E,
23
- children: /* @__PURE__ */ c(p, {
21
+ return /* @__PURE__ */ c(p, {
22
+ open: E,
23
+ onOpenChange: D,
24
+ children: /* @__PURE__ */ l(m, {
24
25
  className: "sm:max-w-lg",
25
26
  children: [
26
- /* @__PURE__ */ c(g, { children: [/* @__PURE__ */ s(_, { children: "Create a circle" }), /* @__PURE__ */ s(m, { children: "A circle is a community space with its own channels, members, events, and polls." })] }),
27
- /* @__PURE__ */ c("div", {
27
+ /* @__PURE__ */ l(_, { children: [/* @__PURE__ */ c(v, { children: "Create a circle" }), /* @__PURE__ */ c(h, { children: "A circle is a community space with its own channels, members, events, and polls." })] }),
28
+ /* @__PURE__ */ l("div", {
28
29
  className: "flex flex-col gap-4 py-2",
29
30
  children: [
30
- /* @__PURE__ */ c("div", {
31
+ /* @__PURE__ */ l("div", {
31
32
  className: "grid gap-1.5",
32
- children: [/* @__PURE__ */ s(d, {
33
+ children: [/* @__PURE__ */ c(f, { children: "Avatar" }), /* @__PURE__ */ c(o, {
34
+ fileId: W,
35
+ name: A || "Circle",
36
+ onUploaded: G,
37
+ label: "Upload circle avatar"
38
+ })]
39
+ }),
40
+ /* @__PURE__ */ l("div", {
41
+ className: "grid gap-1.5",
42
+ children: [/* @__PURE__ */ c(f, {
33
43
  htmlFor: "circle-name",
34
44
  children: "Name"
35
- }), /* @__PURE__ */ s(u, {
45
+ }), /* @__PURE__ */ c(d, {
36
46
  id: "circle-name",
37
- value: k,
38
- onChange: (e) => A(e.target.value),
47
+ value: A,
48
+ onChange: (e) => j(e.target.value),
39
49
  placeholder: "e.g. Product Guild"
40
50
  })]
41
51
  }),
42
- /* @__PURE__ */ c("div", {
52
+ /* @__PURE__ */ l("div", {
43
53
  className: "grid gap-1.5",
44
- children: [/* @__PURE__ */ s(d, {
54
+ children: [/* @__PURE__ */ c(f, {
45
55
  htmlFor: "circle-slug",
46
56
  children: "URL slug"
47
- }), /* @__PURE__ */ s(u, {
57
+ }), /* @__PURE__ */ c(d, {
48
58
  id: "circle-slug",
49
- value: G,
59
+ value: J,
50
60
  onChange: (e) => {
51
- P(!0), M(i(e.target.value));
61
+ F(!0), N(i(e.target.value));
52
62
  },
53
63
  placeholder: "product-guild"
54
64
  })]
55
65
  }),
56
- /* @__PURE__ */ c("div", {
66
+ /* @__PURE__ */ l("div", {
57
67
  className: "grid gap-1.5",
58
- children: [/* @__PURE__ */ s(d, {
68
+ children: [/* @__PURE__ */ c(f, {
59
69
  htmlFor: "circle-topic",
60
70
  children: "Topic"
61
- }), /* @__PURE__ */ s(u, {
71
+ }), /* @__PURE__ */ c(d, {
62
72
  id: "circle-topic",
63
- value: F,
64
- onChange: (e) => I(e.target.value),
73
+ value: I,
74
+ onChange: (e) => L(e.target.value),
65
75
  placeholder: "A short one-line topic"
66
76
  })]
67
77
  }),
68
- /* @__PURE__ */ c("div", {
78
+ /* @__PURE__ */ l("div", {
69
79
  className: "grid gap-1.5",
70
- children: [/* @__PURE__ */ s(d, {
80
+ children: [/* @__PURE__ */ c(f, {
71
81
  htmlFor: "circle-desc",
72
82
  children: "Description"
73
- }), /* @__PURE__ */ s(C, {
83
+ }), /* @__PURE__ */ c(w, {
74
84
  id: "circle-desc",
75
- value: L,
76
- onChange: (e) => R(e.target.value),
85
+ value: R,
86
+ onChange: (e) => z(e.target.value),
77
87
  placeholder: "What is this circle about?",
78
88
  rows: 3
79
89
  })]
80
90
  }),
81
- /* @__PURE__ */ c("div", {
91
+ /* @__PURE__ */ l("div", {
82
92
  className: "grid gap-1.5",
83
- children: [/* @__PURE__ */ s(d, { children: "Visibility" }), /* @__PURE__ */ c(v, {
84
- value: z,
85
- onValueChange: (e) => B(e),
86
- children: [/* @__PURE__ */ s(x, { children: /* @__PURE__ */ s(S, {}) }), /* @__PURE__ */ s(y, { children: w.map((e) => /* @__PURE__ */ s(b, {
93
+ children: [/* @__PURE__ */ c(f, { children: "Visibility" }), /* @__PURE__ */ l(y, {
94
+ value: B,
95
+ onValueChange: (e) => V(e),
96
+ children: [/* @__PURE__ */ c(S, { children: /* @__PURE__ */ c(C, {}) }), /* @__PURE__ */ c(b, { children: T.map((e) => /* @__PURE__ */ c(x, {
87
97
  value: e,
88
98
  children: r(e)
89
99
  }, e)) })]
90
100
  })]
91
101
  }),
92
- /* @__PURE__ */ c("div", {
102
+ /* @__PURE__ */ l("div", {
93
103
  className: "grid gap-1.5",
94
- children: [/* @__PURE__ */ s(d, {
104
+ children: [/* @__PURE__ */ c(f, {
95
105
  htmlFor: "circle-tags",
96
106
  children: "Tags"
97
- }), /* @__PURE__ */ s(a, {
107
+ }), /* @__PURE__ */ c(a, {
98
108
  id: "circle-tags",
99
- value: V,
100
- onChange: H,
109
+ value: H,
110
+ onChange: U,
101
111
  placeholder: "Add tags…"
102
112
  })]
103
113
  }),
104
- U && /* @__PURE__ */ s("p", {
114
+ K && /* @__PURE__ */ c("p", {
105
115
  className: "text-sm text-status-error-text",
106
- children: U
116
+ children: K
107
117
  })
108
118
  ]
109
119
  }),
110
- /* @__PURE__ */ c(h, { children: [/* @__PURE__ */ s(l, {
120
+ /* @__PURE__ */ l(g, { children: [/* @__PURE__ */ c(u, {
111
121
  variant: "outline",
112
- onClick: () => E(!1),
122
+ onClick: () => D(!1),
113
123
  children: "Cancel"
114
- }), /* @__PURE__ */ s(l, {
124
+ }), /* @__PURE__ */ c(u, {
115
125
  onClick: async () => {
116
- if (!k.trim()) {
117
- W("Circle name is required.");
126
+ if (!A.trim()) {
127
+ q("Circle name is required.");
118
128
  return;
119
129
  }
120
- if (!G) {
121
- W("A URL slug is required.");
130
+ if (!J) {
131
+ q("A URL slug is required.");
122
132
  return;
123
133
  }
124
134
  try {
125
- let e = await O.mutateAsync({
126
- name: k.trim(),
127
- slug: G,
128
- topic: F.trim() || null,
129
- description: L.trim() || null,
130
- visibility: z,
131
- tags: V.length ? V : null
135
+ let e = await k.mutateAsync({
136
+ name: A.trim(),
137
+ slug: J,
138
+ topic: I.trim() || null,
139
+ description: R.trim() || null,
140
+ visibility: B,
141
+ avatarFileId: W,
142
+ tags: H.length ? H : null
132
143
  });
133
- n.success("Circle created", `${e.name} is ready.`), E(!1), K(), D?.(e);
144
+ n.success("Circle created", `${e.name} is ready.`), D(!1), Y(), O?.(e);
134
145
  } catch (e) {
135
- W(t(e));
146
+ q(t(e));
136
147
  }
137
148
  },
138
- disabled: O.isPending,
149
+ disabled: k.isPending,
139
150
  children: "Create circle"
140
151
  })] })
141
152
  ]
@@ -143,6 +154,6 @@ function T({ open: T, onOpenChange: E, onCreated: D }) {
143
154
  });
144
155
  }
145
156
  //#endregion
146
- export { T as CreateCircleDialog };
157
+ export { E as CreateCircleDialog };
147
158
 
148
159
  //# sourceMappingURL=CreateCircleDialog.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CreateCircleDialog.js","names":[],"sources":["../../../src/comradecircle/components/CreateCircleDialog.tsx"],"sourcesContent":["/**\n * CreateCircleDialog — create a new community circle.\n * @module comradecircle/components\n *\n * Writes `comCreateCircle`. Slug auto-derives from the name (editable).\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { TagInput } from './TagInput';\nimport { useComCreateCircle } from '../hooks';\nimport { slugify } from '../utils/slug';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComCircle, ComCircleVisibility } from '../types';\n\nconst VISIBILITIES: ComCircleVisibility[] = ['PUBLIC', 'PRIVATE', 'SECRET'];\n\ninterface CreateCircleDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onCreated?: (circle: ComCircle) => void;\n}\n\nexport function CreateCircleDialog({ open, onOpenChange, onCreated }: CreateCircleDialogProps) {\n const createCircle = useComCreateCircle();\n const [name, setName] = useState('');\n const [slug, setSlug] = useState('');\n const [slugTouched, setSlugTouched] = useState(false);\n const [topic, setTopic] = useState('');\n const [description, setDescription] = useState('');\n const [visibility, setVisibility] = useState<ComCircleVisibility>('PUBLIC');\n const [tags, setTags] = useState<string[]>([]);\n const [error, setError] = useState<string | null>(null);\n\n const effectiveSlug = slugTouched ? slug : slugify(name);\n\n const reset = () => {\n setName('');\n setSlug('');\n setSlugTouched(false);\n setTopic('');\n setDescription('');\n setVisibility('PUBLIC');\n setTags([]);\n setError(null);\n };\n\n const submit = async () => {\n if (!name.trim()) {\n setError('Circle name is required.');\n return;\n }\n if (!effectiveSlug) {\n setError('A URL slug is required.');\n return;\n }\n try {\n const created = await createCircle.mutateAsync({\n name: name.trim(),\n slug: effectiveSlug,\n topic: topic.trim() || null,\n description: description.trim() || null,\n visibility,\n tags: tags.length ? tags : null,\n });\n notify.success('Circle created', `${created.name} is ready.`);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Create a circle</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n A circle is a community space with its own channels, members, events, and polls.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex flex-col gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-name\">Name</Label>\n <Input\n id=\"circle-name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"e.g. Product Guild\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-slug\">URL slug</Label>\n <Input\n id=\"circle-slug\"\n value={effectiveSlug}\n onChange={(e) => {\n setSlugTouched(true);\n setSlug(slugify(e.target.value));\n }}\n placeholder=\"product-guild\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-topic\">Topic</Label>\n <Input\n id=\"circle-topic\"\n value={topic}\n onChange={(e) => setTopic(e.target.value)}\n placeholder=\"A short one-line topic\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-desc\">Description</Label>\n <Textarea\n id=\"circle-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n placeholder=\"What is this circle about?\"\n rows={3}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Visibility</Label>\n <Select\n value={visibility}\n onValueChange={(v) => setVisibility(v as ComCircleVisibility)}\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {VISIBILITIES.map((v) => (\n <SelectItem key={v} value={v}>\n {formatEnumLabel(v)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-tags\">Tags</Label>\n <TagInput id=\"circle-tags\" value={tags} onChange={setTags} placeholder=\"Add tags…\" />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={createCircle.isPending}>\n Create circle\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CreateCircleDialog.js","names":[],"sources":["../../../src/comradecircle/components/CreateCircleDialog.tsx"],"sourcesContent":["/**\n * CreateCircleDialog — create a new community circle.\n * @module comradecircle/components\n *\n * Writes `comCreateCircle`. Slug auto-derives from the name (editable).\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { TagInput } from './TagInput';\nimport { AvatarUploadField } from './AvatarUploadField';\nimport { useComCreateCircle } from '../hooks';\nimport { slugify } from '../utils/slug';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComCircle, ComCircleVisibility } from '../types';\n\nconst VISIBILITIES: ComCircleVisibility[] = ['PUBLIC', 'PRIVATE', 'SECRET'];\n\ninterface CreateCircleDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onCreated?: (circle: ComCircle) => void;\n}\n\nexport function CreateCircleDialog({ open, onOpenChange, onCreated }: CreateCircleDialogProps) {\n const createCircle = useComCreateCircle();\n const [name, setName] = useState('');\n const [slug, setSlug] = useState('');\n const [slugTouched, setSlugTouched] = useState(false);\n const [topic, setTopic] = useState('');\n const [description, setDescription] = useState('');\n const [visibility, setVisibility] = useState<ComCircleVisibility>('PUBLIC');\n const [tags, setTags] = useState<string[]>([]);\n const [avatarFileId, setAvatarFileId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const effectiveSlug = slugTouched ? slug : slugify(name);\n\n const reset = () => {\n setName('');\n setSlug('');\n setSlugTouched(false);\n setTopic('');\n setDescription('');\n setVisibility('PUBLIC');\n setTags([]);\n setAvatarFileId(null);\n setError(null);\n };\n\n const submit = async () => {\n if (!name.trim()) {\n setError('Circle name is required.');\n return;\n }\n if (!effectiveSlug) {\n setError('A URL slug is required.');\n return;\n }\n try {\n const created = await createCircle.mutateAsync({\n name: name.trim(),\n slug: effectiveSlug,\n topic: topic.trim() || null,\n description: description.trim() || null,\n visibility,\n avatarFileId,\n tags: tags.length ? tags : null,\n });\n notify.success('Circle created', `${created.name} is ready.`);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Create a circle</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n A circle is a community space with its own channels, members, events, and polls.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex flex-col gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label>Avatar</Label>\n <AvatarUploadField\n fileId={avatarFileId}\n name={name || 'Circle'}\n onUploaded={setAvatarFileId}\n label=\"Upload circle avatar\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-name\">Name</Label>\n <Input\n id=\"circle-name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"e.g. Product Guild\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-slug\">URL slug</Label>\n <Input\n id=\"circle-slug\"\n value={effectiveSlug}\n onChange={(e) => {\n setSlugTouched(true);\n setSlug(slugify(e.target.value));\n }}\n placeholder=\"product-guild\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-topic\">Topic</Label>\n <Input\n id=\"circle-topic\"\n value={topic}\n onChange={(e) => setTopic(e.target.value)}\n placeholder=\"A short one-line topic\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-desc\">Description</Label>\n <Textarea\n id=\"circle-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n placeholder=\"What is this circle about?\"\n rows={3}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Visibility</Label>\n <Select\n value={visibility}\n onValueChange={(v) => setVisibility(v as ComCircleVisibility)}\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {VISIBILITIES.map((v) => (\n <SelectItem key={v} value={v}>\n {formatEnumLabel(v)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-tags\">Tags</Label>\n <TagInput id=\"circle-tags\" value={tags} onChange={setTags} placeholder=\"Add tags…\" />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={createCircle.isPending}>\n Create circle\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { useFileUrl as t } from "../hooks/useFiles.js";
3
+ import { useState as n } from "react";
4
+ import { Fragment as r, jsx as i } from "react/jsx-runtime";
5
+ import { Skeleton as a } from "@burdenoff/fe-libs/ui";
6
+ //#region src/comradecircle/components/FileImage.tsx
7
+ function o({ fileId: o, alt: s, className: c, fallback: l = null }) {
8
+ let { data: u, isLoading: d, isError: f } = t(o), [p, m] = n(!1);
9
+ return o ? d ? /* @__PURE__ */ i(a, { className: e("h-full w-full", c) }) : f || p || !u ? /* @__PURE__ */ i(r, { children: l }) : /* @__PURE__ */ i("img", {
10
+ src: u,
11
+ alt: s,
12
+ loading: "lazy",
13
+ onError: () => m(!0),
14
+ className: e("object-cover", c)
15
+ }) : /* @__PURE__ */ i(r, { children: l });
16
+ }
17
+ //#endregion
18
+ export { o as FileImage };
19
+
20
+ //# sourceMappingURL=FileImage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileImage.js","names":[],"sources":["../../../src/comradecircle/components/FileImage.tsx"],"sourcesContent":["/**\n * FileImage — render a wspace-files-svc file id as an image.\n * @module comradecircle/components\n *\n * Resolves the id to a presigned URL via {@link useFileUrl}. Shows a skeleton\n * while resolving and renders nothing (so the caller's fallback shows) if the id\n * is absent or the URL fails to resolve.\n */\n\nimport { useState } from 'react';\nimport { Skeleton } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { useFileUrl } from '../hooks/useFiles';\n\ninterface FileImageProps {\n fileId?: string | null;\n alt: string;\n className?: string;\n /** Rendered when there is no fileId, resolution fails, or the image errors. */\n fallback?: React.ReactNode;\n}\n\nexport function FileImage({ fileId, alt, className, fallback = null }: FileImageProps) {\n const { data: url, isLoading, isError } = useFileUrl(fileId);\n const [imgError, setImgError] = useState(false);\n\n if (!fileId) return <>{fallback}</>;\n if (isLoading) return <Skeleton className={cn('h-full w-full', className)} />;\n if (isError || imgError || !url) return <>{fallback}</>;\n\n return (\n <img\n src={url}\n alt={alt}\n loading=\"lazy\"\n onError={() => setImgError(true)}\n className={cn('object-cover', className)}\n />\n );\n}\n"],"mappings":";;;;;;AAsBA,SAAgB,EAAU,EAAE,WAAQ,QAAK,cAAW,cAAW,QAAwB;CACrF,IAAM,EAAE,MAAM,GAAK,cAAW,eAAY,EAAW,EAAO,EACtD,CAAC,GAAU,KAAe,EAAS,GAAM;AAM/C,QAJK,IACD,IAAkB,kBAAC,GAAD,EAAU,WAAW,EAAG,iBAAiB,EAAU,EAAI,CAAA,GACzE,KAAW,KAAY,CAAC,IAAY,kBAAA,GAAA,EAAA,UAAG,GAAY,CAAA,GAGrD,kBAAC,OAAD;EACE,KAAK;EACA;EACL,SAAQ;EACR,eAAe,EAAY,GAAK;EAChC,WAAW,EAAG,gBAAgB,EAAU;EACxC,CAAA,GAXgB,kBAAA,GAAA,EAAA,UAAG,GAAY,CAAA"}
@@ -0,0 +1,97 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { FILE_FOLDERS as t, uploadImageToFiles as n } from "../hooks/useFiles.js";
3
+ import { FileImage as r } from "./FileImage.js";
4
+ import { describeError as i, notify as a } from "../lib/toast.js";
5
+ import { useRef as o, useState as s } from "react";
6
+ import { ImagePlus as c, Loader2 as l, X as u } from "lucide-react";
7
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
8
+ //#region src/comradecircle/components/ImageAttachments.tsx
9
+ var p = 10 * 1024 * 1024, m = 6;
10
+ function h({ fileIds: e, onChange: h, disabled: g = !1 }) {
11
+ let _ = o(null), [v, y] = s(!1), [b, x] = s({}), S = async (r) => {
12
+ let o = Array.from(r.target.files ?? []);
13
+ if (r.target.value = "", o.length === 0) return;
14
+ let s = m - e.length;
15
+ if (s <= 0) {
16
+ a.warning("Attachment limit reached", `Up to ${m} images per post.`);
17
+ return;
18
+ }
19
+ y(!0);
20
+ try {
21
+ let r = [], i = {};
22
+ for (let e of o.slice(0, s)) {
23
+ if (!e.type.startsWith("image/")) continue;
24
+ if (e.size > p) {
25
+ a.error("Image too large", `${e.name} exceeds 10 MB.`);
26
+ continue;
27
+ }
28
+ let o = await n(e, t.posts);
29
+ r.push(o.id), i[o.id] = o.url;
30
+ }
31
+ r.length && (x((e) => ({
32
+ ...e,
33
+ ...i
34
+ })), h([...e, ...r]));
35
+ } catch (e) {
36
+ a.error("Upload failed", i(e));
37
+ } finally {
38
+ y(!1);
39
+ }
40
+ }, C = (t) => h(e.filter((e) => e !== t));
41
+ return /* @__PURE__ */ f("div", { children: [
42
+ e.length > 0 && /* @__PURE__ */ d("div", {
43
+ className: "mb-2 flex flex-wrap gap-2",
44
+ children: e.map((e) => /* @__PURE__ */ f("div", {
45
+ className: "relative h-20 w-20 overflow-hidden rounded-lg ring-1 ring-border-seam",
46
+ children: [b[e] ? /* @__PURE__ */ d("img", {
47
+ src: b[e],
48
+ alt: "attachment",
49
+ className: "h-full w-full object-cover"
50
+ }) : /* @__PURE__ */ d(r, {
51
+ fileId: e,
52
+ alt: "attachment",
53
+ className: "h-full w-full"
54
+ }), /* @__PURE__ */ d("button", {
55
+ type: "button",
56
+ onClick: () => C(e),
57
+ "aria-label": "Remove attachment",
58
+ className: "absolute right-0.5 top-0.5 rounded-full bg-bg-canvas/70 p-0.5 text-text-primary hover:bg-bg-canvas",
59
+ children: /* @__PURE__ */ d(u, { className: "h-3.5 w-3.5" })
60
+ })]
61
+ }, e))
62
+ }),
63
+ /* @__PURE__ */ d("input", {
64
+ ref: _,
65
+ type: "file",
66
+ accept: "image/*",
67
+ multiple: !0,
68
+ className: "sr-only",
69
+ onChange: S,
70
+ disabled: g || v
71
+ }),
72
+ /* @__PURE__ */ f("button", {
73
+ type: "button",
74
+ onClick: () => _.current?.click(),
75
+ disabled: g || v || e.length >= m,
76
+ className: "inline-flex items-center gap-1.5 text-xs font-medium text-text-secondary transition-colors hover:text-text-primary disabled:opacity-50",
77
+ children: [v ? /* @__PURE__ */ d(l, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ d(c, { className: "h-4 w-4" }), v ? "Uploading…" : "Add images"]
78
+ })
79
+ ] });
80
+ }
81
+ function g({ fileIds: t, className: n }) {
82
+ return t.length === 0 ? null : /* @__PURE__ */ d("div", {
83
+ className: e("mt-3 grid gap-2", t.length === 1 ? "grid-cols-1" : "grid-cols-2 sm:grid-cols-3", n),
84
+ children: t.map((n) => /* @__PURE__ */ d("div", {
85
+ className: e("overflow-hidden rounded-xl ring-1 ring-border-seam", t.length === 1 ? "max-h-96" : "aspect-square"),
86
+ children: /* @__PURE__ */ d(r, {
87
+ fileId: n,
88
+ alt: "Post attachment",
89
+ className: "h-full w-full"
90
+ })
91
+ }, n))
92
+ });
93
+ }
94
+ //#endregion
95
+ export { g as AttachmentGrid, h as AttachmentPicker };
96
+
97
+ //# sourceMappingURL=ImageAttachments.js.map