@openrails/contentkit-ui 0.96.0 → 0.97.1

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 (65) hide show
  1. package/README.md +9 -1
  2. package/dist/client/discovery.d.ts +2 -1
  3. package/dist/client/discovery.js.map +1 -1
  4. package/dist/client/entities.d.ts +6 -0
  5. package/dist/client/entities.js +13 -0
  6. package/dist/client/entities.js.map +1 -1
  7. package/dist/client/generated/routes.d.ts +26 -0
  8. package/dist/client/generated/routes.js +44 -0
  9. package/dist/client/generated/routes.js.map +1 -1
  10. package/dist/client/generated/wire.d.ts +7 -10
  11. package/dist/client/generated/wire.js.map +1 -1
  12. package/dist/client/http.d.ts +1 -0
  13. package/dist/client/http.js +5 -4
  14. package/dist/client/http.js.map +1 -1
  15. package/dist/client/index.d.ts +1 -1
  16. package/dist/client/reader-preferences.js +3 -7
  17. package/dist/client/reader-preferences.js.map +1 -1
  18. package/dist/components/admin/content-manager.d.ts +3 -1
  19. package/dist/components/admin/content-manager.js +241 -239
  20. package/dist/components/admin/content-manager.js.map +1 -1
  21. package/dist/components/admin/group-manager.js +133 -119
  22. package/dist/components/admin/group-manager.js.map +1 -1
  23. package/dist/components/content/editors.d.ts +1 -0
  24. package/dist/components/content/editors.js +1 -0
  25. package/dist/components/content/editors.js.map +1 -1
  26. package/dist/components/content/filters.d.ts +1 -1
  27. package/dist/components/content/filters.js +1 -1
  28. package/dist/components/content/filters.js.map +1 -1
  29. package/dist/components/content/popular.js +37 -32
  30. package/dist/components/content/popular.js.map +1 -1
  31. package/dist/components/groups/banner.d.ts +3 -1
  32. package/dist/components/groups/banner.js +29 -26
  33. package/dist/components/groups/banner.js.map +1 -1
  34. package/dist/components/groups/editor.d.ts +4 -1
  35. package/dist/components/groups/editor.js +369 -315
  36. package/dist/components/groups/editor.js.map +1 -1
  37. package/dist/components/groups/memberships.d.ts +2 -0
  38. package/dist/components/groups/memberships.js +1 -0
  39. package/dist/components/groups/memberships.js.map +1 -1
  40. package/dist/components/groups/parts.d.ts +4 -1
  41. package/dist/components/groups/parts.js +32 -30
  42. package/dist/components/groups/parts.js.map +1 -1
  43. package/dist/index.js +1 -1
  44. package/dist/locales/en.d.ts +2 -0
  45. package/dist/locales/groups/de.js +2 -0
  46. package/dist/locales/groups/de.js.map +1 -1
  47. package/dist/locales/groups/en.d.ts +2 -0
  48. package/dist/locales/groups/en.js +2 -0
  49. package/dist/locales/groups/en.js.map +1 -1
  50. package/dist/locales/groups/es.js +2 -0
  51. package/dist/locales/groups/es.js.map +1 -1
  52. package/dist/locales/groups/ja.js +2 -0
  53. package/dist/locales/groups/ja.js.map +1 -1
  54. package/dist/locales/groups/ko.js +2 -0
  55. package/dist/locales/groups/ko.js.map +1 -1
  56. package/dist/locales/groups/zh.js +2 -0
  57. package/dist/locales/groups/zh.js.map +1 -1
  58. package/dist/react/discovery.d.ts +2 -1
  59. package/dist/react/discovery.js +54 -29
  60. package/dist/react/discovery.js.map +1 -1
  61. package/dist/react/groups.d.ts +3 -1
  62. package/dist/react/groups.js +7 -7
  63. package/dist/react/groups.js.map +1 -1
  64. package/dist/styles.css +1 -1
  65. package/package.json +1 -1
@@ -65,21 +65,22 @@ function S({ group: e, href: t, className: n, children: i }) {
65
65
  children: i
66
66
  });
67
67
  }
68
- function C({ kind: e, language: n, exclude: r, label: i, onPick: s, client: l, autoFocus: u, className: v }) {
69
- let { t: y } = t(), b = f(), [x, S] = m(""), [C, w] = m(0), T = c(x, {
68
+ function C({ kind: e, language: n, exclude: r, label: i, onPick: s, client: l, autoFocus: u, className: v, management: y }) {
69
+ let { t: b } = t(), x = f(), [S, C] = m(""), [w, T] = m(0), E = c(S, {
70
70
  entity_kinds: [e],
71
71
  display_language: n,
72
72
  limit: 10,
73
- client: l
74
- }), E = p(() => T.entities.filter((t) => t.kind === e && !r?.includes(t.id)).slice(0, 8), [
75
- T.entities,
73
+ client: l,
74
+ management: y
75
+ }), D = p(() => E.entities.filter((t) => t.kind === e && !r?.includes(t.id)).slice(0, 8), [
76
+ E.entities,
76
77
  e,
77
78
  r
78
- ]), D = x.trim();
79
- d(() => w(0), [D]);
80
- let O = (e) => {
81
- S(""), s(e);
82
- }, k = E.length > 0;
79
+ ]), O = S.trim();
80
+ d(() => T(0), [O]);
81
+ let k = (e) => {
82
+ C(""), s(e);
83
+ }, A = D.length > 0;
83
84
  return /* @__PURE__ */ g("div", {
84
85
  className: _("relative grid min-w-48 flex-1 gap-1", v),
85
86
  "data-ckui": "group-picker",
@@ -87,33 +88,33 @@ function C({ kind: e, language: n, exclude: r, label: i, onPick: s, client: l, a
87
88
  /* @__PURE__ */ h(a, {
88
89
  role: "combobox",
89
90
  "aria-label": i,
90
- "aria-expanded": k,
91
- "aria-controls": `${b}-list`,
92
- "aria-activedescendant": k ? `${b}-${C}` : void 0,
91
+ "aria-expanded": A,
92
+ "aria-controls": `${x}-list`,
93
+ "aria-activedescendant": A ? `${x}-${w}` : void 0,
93
94
  "aria-autocomplete": "list",
94
95
  autoComplete: "off",
95
96
  className: "h-8",
96
- value: x,
97
- placeholder: y("groups.searchPlaceholder"),
97
+ value: S,
98
+ placeholder: b("groups.searchPlaceholder"),
98
99
  autoFocus: u,
99
- onChange: (e) => S(e.target.value),
100
+ onChange: (e) => C(e.target.value),
100
101
  onKeyDown: (e) => {
101
- k && (e.key === "ArrowDown" ? (e.preventDefault(), w((e) => Math.min(E.length - 1, e + 1))) : e.key === "ArrowUp" ? (e.preventDefault(), w((e) => Math.max(0, e - 1))) : e.key === "Enter" ? (e.preventDefault(), O(E[C])) : e.key === "Escape" && S(""));
102
+ A && (e.key === "ArrowDown" ? (e.preventDefault(), T((e) => Math.min(D.length - 1, e + 1))) : e.key === "ArrowUp" ? (e.preventDefault(), T((e) => Math.max(0, e - 1))) : e.key === "Enter" ? (e.preventDefault(), k(D[w])) : e.key === "Escape" && C(""));
102
103
  }
103
104
  }),
104
- k && /* @__PURE__ */ h("ul", {
105
- id: `${b}-list`,
105
+ A && /* @__PURE__ */ h("ul", {
106
+ id: `${x}-list`,
106
107
  role: "listbox",
107
108
  "aria-label": i,
108
109
  className: "absolute top-full z-10 mt-1 grid w-full overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md",
109
- children: E.map((e, t) => /* @__PURE__ */ g("li", {
110
- id: `${b}-${t}`,
110
+ children: D.map((e, t) => /* @__PURE__ */ g("li", {
111
+ id: `${x}-${t}`,
111
112
  role: "option",
112
- "aria-selected": t === C,
113
- className: _("flex cursor-pointer items-baseline justify-between gap-2 px-3 py-1.5", t === C && "bg-accent text-accent-foreground"),
114
- onMouseEnter: () => w(t),
113
+ "aria-selected": t === w,
114
+ className: _("flex cursor-pointer items-baseline justify-between gap-2 px-3 py-1.5", t === w && "bg-accent text-accent-foreground"),
115
+ onMouseEnter: () => T(t),
115
116
  onMouseDown: (t) => {
116
- t.preventDefault(), O(e);
117
+ t.preventDefault(), k(e);
117
118
  },
118
119
  children: [/* @__PURE__ */ h("span", {
119
120
  className: "min-w-0 truncate",
@@ -124,12 +125,12 @@ function C({ kind: e, language: n, exclude: r, label: i, onPick: s, client: l, a
124
125
  })]
125
126
  }, e.id))
126
127
  }),
127
- D && !k && T.query === D && !T.loading && !T.error && /* @__PURE__ */ h("p", {
128
+ O && !A && E.query === O && !E.loading && !E.error && /* @__PURE__ */ h("p", {
128
129
  className: "text-xs text-muted-foreground",
129
130
  role: "status",
130
- children: y("groups.noMatch")
131
+ children: b("groups.noMatch")
131
132
  }),
132
- /* @__PURE__ */ h(o, { error: T.error })
133
+ /* @__PURE__ */ h(o, { error: E.error })
133
134
  ]
134
135
  });
135
136
  }
@@ -138,13 +139,14 @@ function w(e, t = {}) {
138
139
  return d(() => {
139
140
  if (!o) return;
140
141
  let e = new AbortController(), r = o.split(",");
141
- return Promise.all(r.map((r) => n.entities.get(r, t.language, e.signal).catch(() => null))).then((t) => {
142
+ return Promise.all(r.map((r) => (t.management ? n.entities.getForManagement(r, t.language, e.signal) : n.entities.get(r, t.language, e.signal)).catch(() => null))).then((t) => {
142
143
  e.signal.aborted || a(new Map(t.flatMap((e, t) => e ? [[r[t], e]] : [])));
143
144
  }), () => e.abort();
144
145
  }, [
145
146
  o,
146
147
  n,
147
- t.language
148
+ t.language,
149
+ t.management
148
150
  ]), r;
149
151
  }
150
152
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"parts.js","names":[],"sources":["../../../src/components/groups/parts.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useCallback, useContext, useEffect, useId, useMemo, useState, type MouseEvent, type ReactNode } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport { humanize } from \"../../client/group-fields.js\";\nimport type { Avatar, GroupCard, GroupDetail, GroupsCover } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { ContentKitContext, useContentKitClient } from \"../../react/context.js\";\nimport { useTypeahead } from \"../../react/discovery.js\";\nimport { Input } from \"#ckui/ui/input\";\nimport { useScopeProps } from \"../../scope.js\";\nimport { RenditionImg } from \"../rendition-img.js\";\nimport { ErrorLine } from \"../social/parts.js\";\n\n/** A group as covers and links need it. */\nexport type GroupLike = Pick<GroupCard, \"id\" | \"kind\" | \"name\" | \"slug\" | \"path\"> & { cover?: GroupsCover; avatar?: Avatar };\n\n/** Display names of kinds, attributes, roles and values by identifier; a missing one shows the identifier humanized. */\nexport type Labels = Readonly<Record<string, string>>;\n\nexport const labelOf = (labels: Labels | undefined, id: string) => labels?.[id] ?? humanize(id);\n\n\nexport interface GroupCoverProps {\n group: GroupLike;\n /** Shown instead, e.g. a cover just uploaded. */\n src?: string;\n className?: string;\n}\n\n/** A group's cover or avatar image; its own styling root, so it renders anywhere in the host. */\nfunction GroupImage({ group, src, className, shape, slot }: GroupCoverProps & { shape: string; slot: \"group-cover\" | \"group-avatar\" }) {\n const scope = useScopeProps();\n const box = cn(scope.className, \"overflow-hidden bg-muted\", shape, className);\n const root = { \"data-ckui-theme\": scope[\"data-ckui-theme\"], style: scope.style, \"data-ckui\": slot };\n const initial = (\n <span {...root} className={cn(box, \"grid place-items-center font-semibold text-muted-foreground\", slot === \"group-cover\" && \"text-lg\")}>\n {(group.name || group.slug).trim().charAt(0).toUpperCase() || \"?\"}\n </span>\n );\n if (src) return <img {...root} src={src} alt=\"\" className={cn(box, \"object-cover\")} />;\n const renditions = (slot === \"group-cover\" ? group.cover : group.avatar)?.renditions;\n if (renditions?.length) return <RenditionImg {...root} outputs={renditions} fit=\"cover\" skeleton className={box} fallback={initial} />;\n return initial;\n}\n\n/** A group's cover: its renditions, else its initial on a muted tile. */\nexport function GroupCover(p: GroupCoverProps) {\n return <GroupImage {...p} shape=\"rounded-md\" slot=\"group-cover\" />;\n}\n\n/** A group's avatar: its renditions, else its initial in a circle. */\nexport function GroupAvatar(p: GroupCoverProps) {\n return <GroupImage {...p} shape=\"aspect-square rounded-full\" slot=\"group-avatar\" />;\n}\n\n/** A link to a group's page: `href`, else its canonical path; in-app through the provider's navigate. */\nexport function GroupLink({ group, href, className, children }: { group: GroupLike; href?: (g: GroupLike) => string | undefined; className?: string; children: ReactNode }) {\n const navigate = useContext(ContentKitContext)?.navigate;\n const url = href ? href(group) : group.path;\n const onClick = useCallback(\n (e: MouseEvent<HTMLAnchorElement>) => {\n if (!navigate || !url || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n navigate(url);\n },\n [navigate, url],\n );\n if (!url) return <span className={className}>{children}</span>;\n return (\n <a href={url} className={className} onClick={onClick}>\n {children}\n </a>\n );\n}\n\nexport interface GroupPickerProps {\n kind: string;\n /** Names in this language. */\n language?: string;\n /** Groups not offered, by id. */\n exclude?: readonly string[];\n label: string;\n onPick: (group: GroupCard) => void;\n client?: ContentKitClient;\n autoFocus?: boolean;\n className?: string;\n}\n\n/** A typeahead over one kind's groups (GET /v1/typeahead). */\nexport function GroupPicker({ kind, language, exclude, label, onPick, client, autoFocus, className }: GroupPickerProps) {\n const { t } = useMessages();\n const id = useId();\n const [text, setText] = useState(\"\");\n const [active, setActive] = useState(0);\n const found = useTypeahead(text, { entity_kinds: [kind], display_language: language, limit: 10, client });\n const results = useMemo(() => found.entities.filter((g) => g.kind === kind && !exclude?.includes(g.id)).slice(0, 8), [found.entities, kind, exclude]);\n const q = text.trim();\n useEffect(() => setActive(0), [q]);\n const pick = (g: GroupCard) => {\n setText(\"\");\n onPick(g);\n };\n const open = results.length > 0;\n return (\n <div className={cn(\"relative grid min-w-48 flex-1 gap-1\", className)} data-ckui=\"group-picker\">\n <Input\n role=\"combobox\"\n aria-label={label}\n aria-expanded={open}\n aria-controls={`${id}-list`}\n aria-activedescendant={open ? `${id}-${active}` : undefined}\n aria-autocomplete=\"list\"\n autoComplete=\"off\"\n className=\"h-8\"\n value={text}\n placeholder={t(\"groups.searchPlaceholder\")}\n autoFocus={autoFocus}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={(e) => {\n if (!open) return;\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((a) => Math.min(results.length - 1, a + 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((a) => Math.max(0, a - 1));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n pick(results[active]!);\n } else if (e.key === \"Escape\") setText(\"\");\n }}\n />\n {open && (\n <ul id={`${id}-list`} role=\"listbox\" aria-label={label} className=\"absolute top-full z-10 mt-1 grid w-full overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md\">\n {results.map((g, i) => (\n <li\n key={g.id}\n id={`${id}-${i}`}\n role=\"option\"\n aria-selected={i === active}\n className={cn(\"flex cursor-pointer items-baseline justify-between gap-2 px-3 py-1.5\", i === active && \"bg-accent text-accent-foreground\")}\n onMouseEnter={() => setActive(i)}\n onMouseDown={(e) => {\n e.preventDefault();\n pick(g);\n }}\n >\n <span className=\"min-w-0 truncate\">{g.name || g.slug}</span>\n <span className=\"text-xs text-muted-foreground\">{g.slug}</span>\n </li>\n ))}\n </ul>\n )}\n {q && !open && found.query === q && !found.loading && !found.error && (\n <p className=\"text-xs text-muted-foreground\" role=\"status\">\n {t(\"groups.noMatch\")}\n </p>\n )}\n <ErrorLine error={found.error} />\n </div>\n );\n}\n\n/** Groups read by id, for names where only ids are at hand (edges); a missing or deleted one is left out. */\nexport function useGroupNames(ids: readonly string[], o: { language?: string; client?: ContentKitClient } = {}): ReadonlyMap<string, GroupDetail> {\n const client = useContentKitClient(o.client);\n const [found, setFound] = useState<ReadonlyMap<string, GroupDetail>>(new Map());\n const key = [...new Set(ids)].sort().join(\",\");\n useEffect(() => {\n if (!key) return;\n const ctl = new AbortController();\n const list = key.split(\",\");\n // A merged group answers as its survivor: keep it under the id asked for.\n Promise.all(list.map((id) => client.entities.get(id, o.language, ctl.signal).catch(() => null))).then((all) => {\n if (!ctl.signal.aborted) setFound(new Map(all.flatMap((g, i) => (g ? [[list[i]!, g] as const] : []))));\n });\n return () => ctl.abort();\n }, [key, client, o.language]);\n return found;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"parts.js","names":[],"sources":["../../../src/components/groups/parts.tsx"],"sourcesContent":["import { cn } from \"cn\";\nimport { useCallback, useContext, useEffect, useId, useMemo, useState, type MouseEvent, type ReactNode } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport { humanize } from \"../../client/group-fields.js\";\nimport type { Avatar, GroupCard, GroupDetail, GroupsCover } from \"../../client/generated/wire.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { ContentKitContext, useContentKitClient } from \"../../react/context.js\";\nimport { useTypeahead } from \"../../react/discovery.js\";\nimport { Input } from \"#ckui/ui/input\";\nimport { useScopeProps } from \"../../scope.js\";\nimport { RenditionImg } from \"../rendition-img.js\";\nimport { ErrorLine } from \"../social/parts.js\";\n\n/** A group as covers and links need it. */\nexport type GroupLike = Pick<GroupCard, \"id\" | \"kind\" | \"name\" | \"slug\" | \"path\"> & { cover?: GroupsCover; avatar?: Avatar };\n\n/** Display names of kinds, attributes, roles and values by identifier; a missing one shows the identifier humanized. */\nexport type Labels = Readonly<Record<string, string>>;\n\nexport const labelOf = (labels: Labels | undefined, id: string) => labels?.[id] ?? humanize(id);\n\n\nexport interface GroupCoverProps {\n group: GroupLike;\n /** Shown instead, e.g. a cover just uploaded. */\n src?: string;\n className?: string;\n}\n\n/** A group's cover or avatar image; its own styling root, so it renders anywhere in the host. */\nfunction GroupImage({ group, src, className, shape, slot }: GroupCoverProps & { shape: string; slot: \"group-cover\" | \"group-avatar\" }) {\n const scope = useScopeProps();\n const box = cn(scope.className, \"overflow-hidden bg-muted\", shape, className);\n const root = { \"data-ckui-theme\": scope[\"data-ckui-theme\"], style: scope.style, \"data-ckui\": slot };\n const initial = (\n <span {...root} className={cn(box, \"grid place-items-center font-semibold text-muted-foreground\", slot === \"group-cover\" && \"text-lg\")}>\n {(group.name || group.slug).trim().charAt(0).toUpperCase() || \"?\"}\n </span>\n );\n if (src) return <img {...root} src={src} alt=\"\" className={cn(box, \"object-cover\")} />;\n const renditions = (slot === \"group-cover\" ? group.cover : group.avatar)?.renditions;\n if (renditions?.length) return <RenditionImg {...root} outputs={renditions} fit=\"cover\" skeleton className={box} fallback={initial} />;\n return initial;\n}\n\n/** A group's cover: its renditions, else its initial on a muted tile. */\nexport function GroupCover(p: GroupCoverProps) {\n return <GroupImage {...p} shape=\"rounded-md\" slot=\"group-cover\" />;\n}\n\n/** A group's avatar: its renditions, else its initial in a circle. */\nexport function GroupAvatar(p: GroupCoverProps) {\n return <GroupImage {...p} shape=\"aspect-square rounded-full\" slot=\"group-avatar\" />;\n}\n\n/** A link to a group's page: `href`, else its canonical path; in-app through the provider's navigate. */\nexport function GroupLink({ group, href, className, children }: { group: GroupLike; href?: (g: GroupLike) => string | undefined; className?: string; children: ReactNode }) {\n const navigate = useContext(ContentKitContext)?.navigate;\n const url = href ? href(group) : group.path;\n const onClick = useCallback(\n (e: MouseEvent<HTMLAnchorElement>) => {\n if (!navigate || !url || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n navigate(url);\n },\n [navigate, url],\n );\n if (!url) return <span className={className}>{children}</span>;\n return (\n <a href={url} className={className} onClick={onClick}>\n {children}\n </a>\n );\n}\n\nexport interface GroupPickerProps {\n kind: string;\n /** Names in this language. */\n language?: string;\n /** Groups not offered, by id. */\n exclude?: readonly string[];\n label: string;\n onPick: (group: GroupCard) => void;\n client?: ContentKitClient;\n autoFocus?: boolean;\n /** Staff lookup, including internal entities. Requires entity-management permission. */\n management?: boolean;\n className?: string;\n}\n\n/** A typeahead over one kind's groups (GET /v1/typeahead). */\nexport function GroupPicker({ kind, language, exclude, label, onPick, client, autoFocus, className, management }: GroupPickerProps) {\n const { t } = useMessages();\n const id = useId();\n const [text, setText] = useState(\"\");\n const [active, setActive] = useState(0);\n const found = useTypeahead(text, { entity_kinds: [kind], display_language: language, limit: 10, client, management });\n const results = useMemo(() => found.entities.filter((g) => g.kind === kind && !exclude?.includes(g.id)).slice(0, 8), [found.entities, kind, exclude]);\n const q = text.trim();\n useEffect(() => setActive(0), [q]);\n const pick = (g: GroupCard) => {\n setText(\"\");\n onPick(g);\n };\n const open = results.length > 0;\n return (\n <div className={cn(\"relative grid min-w-48 flex-1 gap-1\", className)} data-ckui=\"group-picker\">\n <Input\n role=\"combobox\"\n aria-label={label}\n aria-expanded={open}\n aria-controls={`${id}-list`}\n aria-activedescendant={open ? `${id}-${active}` : undefined}\n aria-autocomplete=\"list\"\n autoComplete=\"off\"\n className=\"h-8\"\n value={text}\n placeholder={t(\"groups.searchPlaceholder\")}\n autoFocus={autoFocus}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={(e) => {\n if (!open) return;\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((a) => Math.min(results.length - 1, a + 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((a) => Math.max(0, a - 1));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n pick(results[active]!);\n } else if (e.key === \"Escape\") setText(\"\");\n }}\n />\n {open && (\n <ul id={`${id}-list`} role=\"listbox\" aria-label={label} className=\"absolute top-full z-10 mt-1 grid w-full overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md\">\n {results.map((g, i) => (\n <li\n key={g.id}\n id={`${id}-${i}`}\n role=\"option\"\n aria-selected={i === active}\n className={cn(\"flex cursor-pointer items-baseline justify-between gap-2 px-3 py-1.5\", i === active && \"bg-accent text-accent-foreground\")}\n onMouseEnter={() => setActive(i)}\n onMouseDown={(e) => {\n e.preventDefault();\n pick(g);\n }}\n >\n <span className=\"min-w-0 truncate\">{g.name || g.slug}</span>\n <span className=\"text-xs text-muted-foreground\">{g.slug}</span>\n </li>\n ))}\n </ul>\n )}\n {q && !open && found.query === q && !found.loading && !found.error && (\n <p className=\"text-xs text-muted-foreground\" role=\"status\">\n {t(\"groups.noMatch\")}\n </p>\n )}\n <ErrorLine error={found.error} />\n </div>\n );\n}\n\n/** Groups read by id, for names where only ids are at hand (edges); a missing or deleted one is left out. */\nexport function useGroupNames(ids: readonly string[], o: { language?: string; client?: ContentKitClient; management?: boolean } = {}): ReadonlyMap<string, GroupDetail> {\n const client = useContentKitClient(o.client);\n const [found, setFound] = useState<ReadonlyMap<string, GroupDetail>>(new Map());\n const key = [...new Set(ids)].sort().join(\",\");\n useEffect(() => {\n if (!key) return;\n const ctl = new AbortController();\n const list = key.split(\",\");\n // A merged group answers as its survivor: keep it under the id asked for.\n Promise.all(list.map((id) => (o.management ? client.entities.getForManagement(id, o.language, ctl.signal) : client.entities.get(id, o.language, ctl.signal)).catch(() => null))).then((all) => {\n if (!ctl.signal.aborted) setFound(new Map(all.flatMap((g, i) => (g ? [[list[i]!, g] as const] : []))));\n });\n return () => ctl.abort();\n }, [key, client, o.language, o.management]);\n return found;\n}\n"],"mappings":"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"}