@dxos/plugin-space 0.13.0 → 0.14.0

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 (48) hide show
  1. package/dist/lib/chunk-ImportSpaceDialog.mjs +2 -2
  2. package/dist/lib/chunk-ImportSpaceDialog.mjs.map +1 -1
  3. package/dist/lib/chunk-MembersContainer.mjs +8 -5
  4. package/dist/lib/chunk-MembersContainer.mjs.map +1 -1
  5. package/dist/lib/chunk-ObjectMasonry.mjs +1 -1
  6. package/dist/lib/chunk-ObjectMasonry.mjs.map +1 -1
  7. package/dist/lib/chunk-RecordArticle.mjs +4 -3
  8. package/dist/lib/chunk-RecordArticle.mjs.map +1 -1
  9. package/dist/lib/chunk-SpaceHomeDashboard.mjs +1 -1
  10. package/dist/lib/chunk-SpaceHomeDashboard.mjs.map +1 -1
  11. package/dist/lib/chunk-SpaceHomeRecent.mjs +41 -19
  12. package/dist/lib/chunk-SpaceHomeRecent.mjs.map +1 -1
  13. package/dist/lib/chunk-SyncStatus.mjs +2 -2
  14. package/dist/lib/chunk-SyncStatus.mjs.map +1 -1
  15. package/dist/lib/chunk-TypeArticle.mjs +1 -1
  16. package/dist/lib/chunk-TypeArticle.mjs.map +1 -1
  17. package/dist/lib/chunk-app-graph-builder.mjs +8 -4
  18. package/dist/lib/chunk-app-graph-builder.mjs.map +1 -1
  19. package/dist/lib/components.mjs +3 -2
  20. package/dist/lib/components.mjs.map +1 -1
  21. package/dist/lib/dashboard.mjs +3 -1
  22. package/dist/lib/dashboard.mjs.map +1 -1
  23. package/dist/types/src/capabilities/app-graph-builder/extensions/database.d.ts.map +1 -1
  24. package/dist/types/src/capabilities/app-graph-builder/extensions/shared.d.ts +2 -0
  25. package/dist/types/src/capabilities/app-graph-builder/extensions/shared.d.ts.map +1 -1
  26. package/dist/types/src/containers/MembersContainer/MembersContainer.d.ts.map +1 -1
  27. package/dist/types/src/containers/RecordArticle/RecordArticle.d.ts.map +1 -1
  28. package/dist/types/src/containers/SpaceHomeArticle/SpaceHomeArticle.stories.d.ts.map +1 -1
  29. package/dist/types/src/containers/SpaceHomeRecent/SpaceHomeRecent.d.ts.map +1 -1
  30. package/dist/types/src/dashboard/stats.d.ts +2 -0
  31. package/dist/types/src/dashboard/stats.d.ts.map +1 -1
  32. package/dist/types/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +62 -62
  34. package/src/capabilities/app-graph-builder/extensions/database.ts +5 -3
  35. package/src/capabilities/app-graph-builder/extensions/shared.test.ts +25 -1
  36. package/src/capabilities/app-graph-builder/extensions/shared.ts +6 -0
  37. package/src/components/CreateObjectPanel/CreateObjectPanel.tsx +2 -2
  38. package/src/containers/ImportSpaceDialog/ImportSpaceDialog.tsx +2 -2
  39. package/src/containers/MembersContainer/MembersContainer.tsx +3 -2
  40. package/src/containers/ObjectMasonry/ObjectMasonryArticle.tsx +1 -1
  41. package/src/containers/RecordArticle/RecordArticle.tsx +2 -5
  42. package/src/containers/SpaceHomeArticle/SpaceHomeArticle.stories.tsx +6 -1
  43. package/src/containers/SpaceHomeDashboard/SpaceHomeDashboard.tsx +1 -1
  44. package/src/containers/SpaceHomeRecent/SpaceHomeRecent.tsx +71 -28
  45. package/src/containers/SyncStatus/SyncStatus.tsx +2 -2
  46. package/src/containers/TypeArticle/TypeArticle.tsx +1 -1
  47. package/src/dashboard/stats.test.ts +12 -2
  48. package/src/dashboard/stats.ts +4 -0
@@ -65,7 +65,7 @@ var ImportSpaceDialog = () => {
65
65
  gap: "sm",
66
66
  asChild: true,
67
67
  role: "status",
68
- classNames: "my-4 p-8 border-2 border-dashed border-neutral-500/50 rounded-sm",
68
+ classNames: "my-4 p-8 border-2 border-dashed border-separator rounded-sm",
69
69
  children: /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Icon.Icon, {
70
70
  icon: "ph--spinner-gap--regular",
71
71
  size: "xl",
@@ -73,7 +73,7 @@ var ImportSpaceDialog = () => {
73
73
  }), /* @__PURE__ */ jsx("span", { children: t("import-space-dialog.importing.label", { filename: importing }) })] })
74
74
  }) : /* @__PURE__ */ jsxs(FileUploader, {
75
75
  types: ["json", "tar"],
76
- classes: "block my-4 p-8 border-2 border-dashed border-neutral-500/50 rounded-sm flex items-center justify-center gap-2 cursor-pointer",
76
+ classes: "block my-4 p-8 border-2 border-dashed border-separator rounded-sm flex items-center justify-center gap-2 cursor-pointer",
77
77
  dropMessageStyle: {
78
78
  border: "none",
79
79
  backgroundColor: "#EEE"
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-ImportSpaceDialog.mjs","names":[],"sources":["../../src/containers/ImportSpaceDialog/ImportSpaceDialog.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { useCallback, useState } from 'react';\nimport { FileUploader } from 'react-drag-drop-files';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport * as GraphPath from '@dxos/app-toolkit/GraphPath';\nimport * as LayoutOperation from '@dxos/app-toolkit/LayoutOperation';\nimport { log } from '@dxos/log';\nimport * as Button from '@dxos/react-ui/Button';\nimport * as Dialog from '@dxos/react-ui/Dialog';\nimport * as UiHooks from '@dxos/react-ui/Hooks';\nimport * as Icon from '@dxos/react-ui/Icon';\nimport * as Layout from '@dxos/react-ui/Layout';\nimport * as SystemButton from '@dxos/react-ui/SystemButton';\n\nimport { meta } from '#meta';\nimport { SpaceOperation } from '#types';\n\nexport const ImportSpaceDialog = () => {\n const { t } = UiHooks.useTranslation(meta.profile.key);\n const { invokePromise } = Hooks.useOperationInvoker();\n\n const [importing, setImporting] = useState<string>();\n\n const handleFile = useCallback(\n async (file: File) => {\n setImporting(file.name);\n let space: { id: string } | undefined;\n try {\n const contents = new Uint8Array(await file.arrayBuffer());\n const { data: result, error } = await invokePromise(SpaceOperation.ImportSpace, {\n archive: { filename: file.name, contents },\n });\n if (error) {\n throw error;\n }\n space = result?.space;\n } catch (error) {\n log.catch(error);\n await invokePromise(LayoutOperation.AddToast, {\n id: `${meta.profile.key}/import-space-failed`,\n icon: 'ph--warning--regular',\n title: ['import-space-failed.title', { ns: meta.profile.key }],\n description: error instanceof Error ? error.message : String(error),\n closeLabel: ['dismiss.label', { ns: meta.profile.key }],\n });\n return;\n } finally {\n setImporting(undefined);\n }\n\n // Outside the import's catch: the space already exists, so a failure here must not invite a retry that duplicates it.\n await invokePromise(LayoutOperation.UpdateDialog, { state: false });\n if (space) {\n await invokePromise(LayoutOperation.SwitchWorkspace, { subject: GraphPath.getSpacePath(space.id) });\n }\n },\n [invokePromise],\n );\n\n return (\n <Dialog.Content>\n <Dialog.Header>\n <Dialog.Title>{t('import-space-dialog.title')}</Dialog.Title>\n <Dialog.CloseTrigger asChild>\n <SystemButton.Close />\n </Dialog.CloseTrigger>\n </Dialog.Header>\n <Dialog.Body>\n <p className='my-4'>{t('import-space-dialog.description')}</p>\n {importing ? (\n <Layout.Flex\n align='center'\n justify='center'\n gap='sm'\n asChild\n role='status'\n classNames='my-4 p-8 border-2 border-dashed border-neutral-500/50 rounded-sm'\n >\n <div>\n <Icon.Icon icon='ph--spinner-gap--regular' size='xl' spin />\n <span>{t('import-space-dialog.importing.label', { filename: importing })}</span>\n </div>\n </Layout.Flex>\n ) : (\n <FileUploader\n types={['json', 'tar']}\n classes='block my-4 p-8 border-2 border-dashed border-neutral-500/50 rounded-sm flex items-center justify-center gap-2 cursor-pointer'\n dropMessageStyle={{ border: 'none', backgroundColor: '#EEE' }}\n handleChange={(file: File) => {\n void handleFile(file);\n }}\n >\n <Icon.Icon icon='ph--file-plus--duotone' size='xl' />\n <span>{t('import-space-dialog.upload.label')}</span>\n </FileUploader>\n )}\n </Dialog.Body>\n <Dialog.Footer>\n <Dialog.CloseTrigger asChild>\n <Button.Root variant='primary'>{t('cancel.label')}</Button.Root>\n </Dialog.CloseTrigger>\n </Dialog.Footer>\n </Dialog.Content>\n );\n};\n\nImportSpaceDialog.displayName = 'ImportSpaceDialog';\n"],"mappings":";;;;;;;;;;;;;;;;;AAqBA,IAAa,0BAA0B;CACrC,MAAM,EAAE,MAAM,QAAQ,eAAe,KAAK,QAAQ,GAAG;CACrD,MAAM,EAAE,kBAAkB,MAAM,oBAAoB;CAEpD,MAAM,CAAC,WAAW,gBAAgB,SAAiB;CAEnD,MAAM,aAAa,YACjB,OAAO,SAAe;EACpB,aAAa,KAAK,IAAI;EACtB,IAAI;EACJ,IAAI;GACF,MAAM,WAAW,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;GACxD,MAAM,EAAE,MAAM,QAAQ,UAAU,MAAM,cAAc,eAAe,aAAa,EAC9E,SAAS;IAAE,UAAU,KAAK;IAAM;GAAS,EAC3C,CAAC;GACD,IAAI,OACF,MAAM;GAER,QAAQ,QAAQ;EAClB,SAAS,OAAO;GACd,IAAI,MAAM,OAAI,KAAA,GAAA;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA,KAAA;GAAA,CAAC;GACf,MAAM,cAAc,gBAAgB,UAAU;IAC5C,IAAI,GAAG,KAAK,QAAQ,IAAI;IACxB,MAAM;IACN,OAAO,CAAC,6BAA6B,EAAE,IAAI,KAAK,QAAQ,IAAI,CAAC;IAC7D,aAAa,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IAClE,YAAY,CAAC,iBAAiB,EAAE,IAAI,KAAK,QAAQ,IAAI,CAAC;GACxD,CAAC;GACD;EACF,UAAU;GACR,aAAa,KAAA,CAAS;EACxB;EAGA,MAAM,cAAc,gBAAgB,cAAc,EAAE,OAAO,MAAM,CAAC;EAClE,IAAI,OACF,MAAM,cAAc,gBAAgB,iBAAiB,EAAE,SAAS,UAAU,aAAa,MAAM,EAAE,EAAE,CAAC;CAEtG,GACA,CAAC,aAAa,CAChB;CAEA,OACE,qBAAC,OAAO,SAAR,EAAA,UAAA;EACE,qBAAC,OAAO,QAAR,EAAA,UAAA,CACE,oBAAC,OAAO,OAAR,EAAA,UAAe,EAAE,2BAA2B,EAAgB,CAAA,GAC5D,oBAAC,OAAO,cAAR;GAAqB,SAAA;GACnB,UAAA,oBAAC,aAAa,OAAd,CAAqB,CAAA;EACF,CAAA,CACR,EAAA,CAAA;EACf,qBAAC,OAAO,MAAR,EAAA,UAAA,CACE,oBAAC,KAAD;GAAG,WAAU;GAAQ,UAAA,EAAE,iCAAiC;EAAK,CAAA,GAC5D,YACC,oBAAC,OAAO,MAAR;GACE,OAAM;GACN,SAAQ;GACR,KAAI;GACJ,SAAA;GACA,MAAK;GACL,YAAW;GAEX,UAAA,qBAAC,OAAD,EAAA,UAAA,CACE,oBAAC,KAAK,MAAN;IAAW,MAAK;IAA2B,MAAK;IAAK,MAAA;GAAM,CAAA,GAC3D,oBAAC,QAAD,EAAA,UAAO,EAAE,uCAAuC,EAAE,UAAU,UAAU,CAAC,EAAQ,CAAA,CAC5E,EAAA,CAAA;EACM,CAAA,IAEb,qBAAC,cAAD;GACE,OAAO,CAAC,QAAQ,KAAK;GACrB,SAAQ;GACR,kBAAkB;IAAE,QAAQ;IAAQ,iBAAiB;GAAO;GAC5D,eAAe,SAAe;IAC5B,WAAgB,IAAI;GACtB;GANF,UAAA,CAQE,oBAAC,KAAK,MAAN;IAAW,MAAK;IAAyB,MAAK;GAAM,CAAA,GACpD,oBAAC,QAAD,EAAA,UAAO,EAAE,kCAAkC,EAAQ,CAAA,CACvC;EAEL,CAAA,CAAA,EAAA,CAAA;EACb,oBAAC,OAAO,QAAR,EAAA,UACE,oBAAC,OAAO,cAAR;GAAqB,SAAA;GACnB,UAAA,oBAAC,OAAO,MAAR;IAAa,SAAQ;IAAW,UAAA,EAAE,cAAc;GAAe,CAAA;EAC5C,CAAA,EACR,CAAA;CACD,EAAA,CAAA;AAEpB;AAEA,kBAAkB,cAAc"}
1
+ {"version":3,"file":"chunk-ImportSpaceDialog.mjs","names":[],"sources":["../../src/containers/ImportSpaceDialog/ImportSpaceDialog.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { useCallback, useState } from 'react';\nimport { FileUploader } from 'react-drag-drop-files';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport * as GraphPath from '@dxos/app-toolkit/GraphPath';\nimport * as LayoutOperation from '@dxos/app-toolkit/LayoutOperation';\nimport { log } from '@dxos/log';\nimport * as Button from '@dxos/react-ui/Button';\nimport * as Dialog from '@dxos/react-ui/Dialog';\nimport * as UiHooks from '@dxos/react-ui/Hooks';\nimport * as Icon from '@dxos/react-ui/Icon';\nimport * as Layout from '@dxos/react-ui/Layout';\nimport * as SystemButton from '@dxos/react-ui/SystemButton';\n\nimport { meta } from '#meta';\nimport { SpaceOperation } from '#types';\n\nexport const ImportSpaceDialog = () => {\n const { t } = UiHooks.useTranslation(meta.profile.key);\n const { invokePromise } = Hooks.useOperationInvoker();\n\n const [importing, setImporting] = useState<string>();\n\n const handleFile = useCallback(\n async (file: File) => {\n setImporting(file.name);\n let space: { id: string } | undefined;\n try {\n const contents = new Uint8Array(await file.arrayBuffer());\n const { data: result, error } = await invokePromise(SpaceOperation.ImportSpace, {\n archive: { filename: file.name, contents },\n });\n if (error) {\n throw error;\n }\n space = result?.space;\n } catch (error) {\n log.catch(error);\n await invokePromise(LayoutOperation.AddToast, {\n id: `${meta.profile.key}/import-space-failed`,\n icon: 'ph--warning--regular',\n title: ['import-space-failed.title', { ns: meta.profile.key }],\n description: error instanceof Error ? error.message : String(error),\n closeLabel: ['dismiss.label', { ns: meta.profile.key }],\n });\n return;\n } finally {\n setImporting(undefined);\n }\n\n // Outside the import's catch: the space already exists, so a failure here must not invite a retry that duplicates it.\n await invokePromise(LayoutOperation.UpdateDialog, { state: false });\n if (space) {\n await invokePromise(LayoutOperation.SwitchWorkspace, { subject: GraphPath.getSpacePath(space.id) });\n }\n },\n [invokePromise],\n );\n\n return (\n <Dialog.Content>\n <Dialog.Header>\n <Dialog.Title>{t('import-space-dialog.title')}</Dialog.Title>\n <Dialog.CloseTrigger asChild>\n <SystemButton.Close />\n </Dialog.CloseTrigger>\n </Dialog.Header>\n <Dialog.Body>\n <p className='my-4'>{t('import-space-dialog.description')}</p>\n {importing ? (\n <Layout.Flex\n align='center'\n justify='center'\n gap='sm'\n asChild\n role='status'\n classNames='my-4 p-8 border-2 border-dashed border-separator rounded-sm'\n >\n <div>\n <Icon.Icon icon='ph--spinner-gap--regular' size='xl' spin />\n <span>{t('import-space-dialog.importing.label', { filename: importing })}</span>\n </div>\n </Layout.Flex>\n ) : (\n <FileUploader\n types={['json', 'tar']}\n classes='block my-4 p-8 border-2 border-dashed border-separator rounded-sm flex items-center justify-center gap-2 cursor-pointer'\n dropMessageStyle={{ border: 'none', backgroundColor: '#EEE' }}\n handleChange={(file: File) => {\n void handleFile(file);\n }}\n >\n <Icon.Icon icon='ph--file-plus--duotone' size='xl' />\n <span>{t('import-space-dialog.upload.label')}</span>\n </FileUploader>\n )}\n </Dialog.Body>\n <Dialog.Footer>\n <Dialog.CloseTrigger asChild>\n <Button.Root variant='primary'>{t('cancel.label')}</Button.Root>\n </Dialog.CloseTrigger>\n </Dialog.Footer>\n </Dialog.Content>\n );\n};\n\nImportSpaceDialog.displayName = 'ImportSpaceDialog';\n"],"mappings":";;;;;;;;;;;;;;;;;AAqBA,IAAa,0BAA0B;CACrC,MAAM,EAAE,MAAM,QAAQ,eAAe,KAAK,QAAQ,GAAG;CACrD,MAAM,EAAE,kBAAkB,MAAM,oBAAoB;CAEpD,MAAM,CAAC,WAAW,gBAAgB,SAAiB;CAEnD,MAAM,aAAa,YACjB,OAAO,SAAe;EACpB,aAAa,KAAK,IAAI;EACtB,IAAI;EACJ,IAAI;GACF,MAAM,WAAW,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;GACxD,MAAM,EAAE,MAAM,QAAQ,UAAU,MAAM,cAAc,eAAe,aAAa,EAC9E,SAAS;IAAE,UAAU,KAAK;IAAM;GAAS,EAC3C,CAAC;GACD,IAAI,OACF,MAAM;GAER,QAAQ,QAAQ;EAClB,SAAS,OAAO;GACd,IAAI,MAAM,OAAI,KAAA,GAAA;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA,KAAA;GAAA,CAAC;GACf,MAAM,cAAc,gBAAgB,UAAU;IAC5C,IAAI,GAAG,KAAK,QAAQ,IAAI;IACxB,MAAM;IACN,OAAO,CAAC,6BAA6B,EAAE,IAAI,KAAK,QAAQ,IAAI,CAAC;IAC7D,aAAa,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IAClE,YAAY,CAAC,iBAAiB,EAAE,IAAI,KAAK,QAAQ,IAAI,CAAC;GACxD,CAAC;GACD;EACF,UAAU;GACR,aAAa,KAAA,CAAS;EACxB;EAGA,MAAM,cAAc,gBAAgB,cAAc,EAAE,OAAO,MAAM,CAAC;EAClE,IAAI,OACF,MAAM,cAAc,gBAAgB,iBAAiB,EAAE,SAAS,UAAU,aAAa,MAAM,EAAE,EAAE,CAAC;CAEtG,GACA,CAAC,aAAa,CAChB;CAEA,OACE,qBAAC,OAAO,SAAR,EAAA,UAAA;EACE,qBAAC,OAAO,QAAR,EAAA,UAAA,CACE,oBAAC,OAAO,OAAR,EAAA,UAAe,EAAE,2BAA2B,EAAgB,CAAA,GAC5D,oBAAC,OAAO,cAAR;GAAqB,SAAA;GACnB,UAAA,oBAAC,aAAa,OAAd,CAAqB,CAAA;EACF,CAAA,CACR,EAAA,CAAA;EACf,qBAAC,OAAO,MAAR,EAAA,UAAA,CACE,oBAAC,KAAD;GAAG,WAAU;GAAQ,UAAA,EAAE,iCAAiC;EAAK,CAAA,GAC5D,YACC,oBAAC,OAAO,MAAR;GACE,OAAM;GACN,SAAQ;GACR,KAAI;GACJ,SAAA;GACA,MAAK;GACL,YAAW;GAEX,UAAA,qBAAC,OAAD,EAAA,UAAA,CACE,oBAAC,KAAK,MAAN;IAAW,MAAK;IAA2B,MAAK;IAAK,MAAA;GAAM,CAAA,GAC3D,oBAAC,QAAD,EAAA,UAAO,EAAE,uCAAuC,EAAE,UAAU,UAAU,CAAC,EAAQ,CAAA,CAC5E,EAAA,CAAA;EACM,CAAA,IAEb,qBAAC,cAAD;GACE,OAAO,CAAC,QAAQ,KAAK;GACrB,SAAQ;GACR,kBAAkB;IAAE,QAAQ;IAAQ,iBAAiB;GAAO;GAC5D,eAAe,SAAe;IAC5B,WAAgB,IAAI;GACtB;GANF,UAAA,CAQE,oBAAC,KAAK,MAAN;IAAW,MAAK;IAAyB,MAAK;GAAM,CAAA,GACpD,oBAAC,QAAD,EAAA,UAAO,EAAE,kCAAkC,EAAQ,CAAA,CACvC;EAEL,CAAA,CAAA,EAAA,CAAA;EACb,oBAAC,OAAO,QAAR,EAAA,UACE,oBAAC,OAAO,cAAR;GAAqB,SAAA;GACnB,UAAA,oBAAC,OAAO,MAAR;IAAa,SAAQ;IAAW,UAAA,EAAE,cAAc;GAAe,CAAA;EAC5C,CAAA,EACR,CAAA;CACD,EAAA,CAAA;AAEpB;AAEA,kBAAkB,cAAc"}
@@ -3,6 +3,7 @@ import { SpaceOperation } from "#types";
3
3
  import { meta } from "#meta";
4
4
  import { useMemo, useState } from "react";
5
5
  import * as Icon from "@dxos/react-ui/Icon";
6
+ import * as Layout from "@dxos/react-ui/Layout";
6
7
  import { hexToEmoji } from "@dxos/util";
7
8
  import * as Surface from "@dxos/app-framework/Surface";
8
9
  import * as AppSurface from "@dxos/app-toolkit/AppSurface";
@@ -34,7 +35,7 @@ var handleInvitationEvent = (invitation, subscription) => {
34
35
  }), void 0, {
35
36
  "~LogMeta": "~LogMeta",
36
37
  F: __dxlog_file,
37
- L: 56,
38
+ L: 57,
38
39
  S: void 0
39
40
  });
40
41
  subscription.unsubscribe();
@@ -107,7 +108,7 @@ var MembersContainer = ({ space, createInvitationUrl }) => {
107
108
  if (error) log.catch(error, void 0, {
108
109
  "~LogMeta": "~LogMeta",
109
110
  F: __dxlog_file,
110
- L: 139,
111
+ L: 140,
111
112
  S: void 0
112
113
  });
113
114
  const result = data ?? {
@@ -145,7 +146,7 @@ var MembersContainer = ({ space, createInvitationUrl }) => {
145
146
  onAction: () => void navigator.clipboard.writeText(joinUrl).catch((error) => log.warn("failed to copy join link", { error }, {
146
147
  "~LogMeta": "~LogMeta",
147
148
  F: __dxlog_file,
148
- L: 184,
149
+ L: 185,
149
150
  S: void 0
150
151
  }))
151
152
  } : {}
@@ -254,9 +255,11 @@ var InvitationQR = ({ id, url, onCancel }) => {
254
255
  className: "text-fg-muted",
255
256
  children: t("qr-code.description", { ns: meta.profile.key })
256
257
  }),
257
- /* @__PURE__ */ jsxs("div", {
258
+ /* @__PURE__ */ jsxs(Layout.Grid, {
258
259
  role: "group",
259
- className: "grid grid-cols-[1fr_min-content] my-2 gap-2",
260
+ cols: ["fill", "min"],
261
+ gap: "sm",
262
+ classNames: "my-2",
260
263
  children: [
261
264
  /* @__PURE__ */ jsxs("div", {
262
265
  className: "w-full aspect-square relative text-fg-muted",
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-MembersContainer.mjs","names":[],"sources":["../../src/containers/MembersContainer/MembersContainer.tsx"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Option from 'effect/Option';\nimport React, { type Dispatch, type SetStateAction, useMemo, useState } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport * as Surface from '@dxos/app-framework/Surface';\nimport * as AppAnnotation from '@dxos/app-toolkit/AppAnnotation';\nimport * as AppSurface from '@dxos/app-toolkit/AppSurface';\nimport * as LayoutOperation from '@dxos/app-toolkit/LayoutOperation';\nimport { Annotation, Obj } from '@dxos/echo';\nimport { log } from '@dxos/log';\nimport { useConfig } from '@dxos/react-client';\nimport { type SpaceMember_Role, useSpaceInvitations } from '@dxos/react-client/echo';\nimport { useContacts } from '@dxos/react-client/halo';\nimport {\n type CancellableInvitationObservable,\n type Invitation,\n Invitation_AuthMethod,\n Invitation_State,\n Invitation_Type,\n InvitationEncoder,\n} from '@dxos/react-client/invitations';\nimport { Form } from '@dxos/react-ui-form';\nimport * as Button from '@dxos/react-ui/Button';\nimport * as UiHooks from '@dxos/react-ui/Hooks';\nimport * as Icon from '@dxos/react-ui/Icon';\nimport * as QrCode from '@dxos/react-ui/QrCode';\nimport * as SystemButton from '@dxos/react-ui/SystemButton';\nimport {\n type ActionMenuItem,\n AuthCode,\n BifurcatedAction,\n Centered,\n Emoji,\n InvitationList,\n SpaceMemberList,\n Viewport,\n contactDisplayName,\n contactKeyHex,\n translationKey as shellTranslationKey,\n} from '@dxos/shell/react';\nimport { hexToEmoji } from '@dxos/util';\n\nimport { meta } from '#meta';\nimport { SpaceOperation } from '#types';\n\n// TODO(wittjosiah): Copied from Shell.\nconst activeActionKey = 'dxos:react-shell/space-manager/active-action';\n\nconst handleInvitationEvent = (invitation: Invitation, subscription: ZenObservable.Subscription) => {\n const invitationCode = InvitationEncoder.encode(invitation);\n if (invitation.state === Invitation_State.CONNECTING) {\n log.info(JSON.stringify({ invitationCode, authCode: invitation.authCode }));\n subscription.unsubscribe();\n }\n};\n\nexport type MembersContainerProps = AppSurface.SpaceArticleProps<{\n createInvitationUrl: (invitationCode: string) => string;\n}>;\n\nexport const MembersContainer = ({ space, createInvitationUrl }: MembersContainerProps) => {\n const { t } = UiHooks.useTranslation(meta.profile.key);\n const config = useConfig();\n const { invokePromise } = Hooks.useOperationInvoker();\n const invitations = useSpaceInvitations(space.key);\n const visibleInvitations = invitations?.filter(\n (invitation) => ![Invitation_State.CANCELLED].includes(invitation.get().state),\n );\n\n const [activeAction, setInternalActiveAction] = useState(localStorage.getItem(activeActionKey) ?? 'inviteMany');\n\n const setActiveAction = (nextAction: string) => {\n setInternalActiveAction(nextAction);\n localStorage.setItem(activeActionKey, nextAction);\n };\n\n // TODO(wittjosiah): Track which was the most recently viewed object.\n const target = Annotation.get(space.properties, AppAnnotation.RootCollectionAnnotation).pipe(Option.getOrUndefined)\n ?.target?.objects[0]?.target;\n\n const inviteActions = useMemo(\n (): Record<string, ActionMenuItem> => ({\n inviteOne: {\n label: t('invite-one.label', { ns: shellTranslationKey }),\n description: t('invite-one.description', { ns: shellTranslationKey }),\n icon: 'ph--user-plus--regular',\n testId: 'membersContainer.inviteOne',\n onClick: async () => {\n const { data: invitation } = await invokePromise(SpaceOperation.Share, {\n space,\n type: Invitation_Type.INTERACTIVE,\n authMethod: Invitation_AuthMethod.SHARED_SECRET,\n multiUse: false,\n target: target && Obj.getURI(target),\n });\n if (invitation && config.values.runtime?.app?.env?.DX_ENVIRONMENT !== 'production') {\n const subscription: ZenObservable.Subscription = (invitation as CancellableInvitationObservable).subscribe(\n (invitation: Invitation) => handleInvitationEvent(invitation, subscription),\n );\n }\n },\n },\n inviteMany: {\n label: t('invite-many.label', { ns: shellTranslationKey }),\n description: t('invite-many.description', { ns: shellTranslationKey }),\n icon: 'ph--users-three--regular',\n testId: 'membersContainer.inviteMany',\n onClick: async () => {\n const { data: invitation } = await invokePromise(SpaceOperation.Share, {\n space,\n type: Invitation_Type.DELEGATED,\n authMethod: Invitation_AuthMethod.KNOWN_PUBLIC_KEY,\n multiUse: true,\n target: target && Obj.getURI(target),\n });\n if (invitation && config.values.runtime?.app?.env?.DX_ENVIRONMENT !== 'production') {\n const subscription: ZenObservable.Subscription = (invitation as CancellableInvitationObservable).subscribe(\n (invitation: Invitation) => handleInvitationEvent(invitation, subscription),\n );\n }\n },\n },\n }),\n [t, space, target, invokePromise],\n );\n\n const contacts = useContacts();\n const isSurfaceAvailable = Surface.useIsAvailable();\n const contactPickerData = useMemo(\n (): AppSurface.ContactPickerData => ({\n space,\n onAdd: async (identityKeys: string[], role: SpaceMember_Role) => {\n const { data, error } = await invokePromise(SpaceOperation.AddMembers, { space, identityKeys, role });\n if (error) {\n log.catch(error);\n }\n const result = data ?? {\n joinUrl: '',\n failed: identityKeys.map((key) => ({ key, error: error?.message ?? 'Unknown error' })),\n notNotified: [],\n };\n const namesOf = (entries: readonly { key: string }[]) =>\n entries\n .map(({ key }) => {\n const contact = contacts.find((candidate) => contactKeyHex(candidate) === key);\n return contact ? contactDisplayName(contact) : key.slice(0, 8);\n })\n .join(', ');\n if (result.failed.length > 0) {\n const names = namesOf(result.failed);\n await invokePromise(LayoutOperation.AddToast, {\n id: `${meta.profile.key}/add-members-failed`,\n title: ['add-members-failed-toast.title', { ns: meta.profile.key }],\n // Label tuples carry no interpolation values, so the names are resolved here.\n description: t('add-members-failed-toast.description', { names }),\n icon: 'ph--warning--regular',\n });\n }\n if (result.notNotified.length > 0) {\n const names = namesOf(result.notNotified);\n const accountRequired = result.notNotified.some(({ reason }) => reason === 'account-required');\n const { joinUrl } = result;\n await invokePromise(LayoutOperation.AddToast, {\n id: `${meta.profile.key}/add-members-not-notified`,\n title: ['add-members-not-notified-toast.title', { ns: meta.profile.key }],\n // Label tuples carry no interpolation values, so the names are resolved here.\n description: t(\n accountRequired\n ? 'add-members-not-notified-account-toast.description'\n : 'add-members-not-notified-toast.description',\n { names },\n ),\n icon: 'ph--bell-slash--regular',\n ...(joinUrl\n ? {\n actionLabel: ['copy-link.label', { ns: meta.profile.key }],\n onAction: () =>\n void navigator.clipboard\n .writeText(joinUrl)\n .catch((error) => log.warn('failed to copy join link', { error })),\n }\n : {}),\n });\n }\n\n return result;\n },\n }),\n [t, space, contacts, invokePromise],\n );\n const showContactPicker = isSurfaceAvailable({ type: AppSurface.ContactPicker, data: contactPickerData });\n\n const [selectedInvitation, setSelectedInvitation] = useState<CancellableInvitationObservable | null>(null);\n const handleSend = (event: { type: 'selectInvitation'; invitation: CancellableInvitationObservable }) => {\n setSelectedInvitation(event.invitation);\n };\n const handleBack = () => {\n setSelectedInvitation(null);\n };\n\n return (\n <Form.Root variant='settings'>\n <Form.Viewport scroll>\n <Form.Content>\n <Form.FieldSet label={t('members-verbose.label')} description={t('members.description')}>\n <Form.FieldSet label={t('members.label')}>\n <SpaceMemberList spaceKey={space.key} includeSelf />\n </Form.FieldSet>\n {showContactPicker && (\n <Form.FieldSet label={t('add-known-people.label')}>\n <Surface.Surface type={AppSurface.ContactPicker} data={contactPickerData} limit={1} />\n </Form.FieldSet>\n )}\n <Form.FieldSet\n label={t('invitations.label')}\n description={selectedInvitation ? undefined : t('space-invitation.description')}\n >\n {selectedInvitation && <InvitationSection {...selectedInvitation} onBack={handleBack} />}\n {!selectedInvitation && (\n <>\n <InvitationList\n send={handleSend}\n invitations={visibleInvitations ?? []}\n onClickRemove={(invitation) => invitation.cancel()}\n createInvitationUrl={createInvitationUrl}\n />\n <BifurcatedAction\n actions={inviteActions}\n activeAction={activeAction}\n onChangeActiveAction={setActiveAction as Dispatch<SetStateAction<string>>}\n data-testid='membersContainer.createInvitation'\n />\n </>\n )}\n </Form.FieldSet>\n </Form.FieldSet>\n </Form.Content>\n </Form.Viewport>\n </Form.Root>\n );\n};\n\n// TODO(wittjosiah): Reconcile the below components with DevicesContainer in @dxos/plugin-client.\n\ntype InvitationComponentProps = Partial<\n Pick<Invitation, 'authCode' | 'invitationId'> & {\n state: Invitation_State;\n url: string;\n onBack: () => void;\n }\n>;\n\nconst InvitationSection = ({\n state = Invitation_State.INIT,\n authCode,\n invitationId = 'never',\n url = 'never',\n onBack,\n}: InvitationComponentProps) => {\n const activeView =\n state < 0\n ? 'init'\n : state >= Invitation_State.CANCELLED\n ? 'complete'\n : state >= Invitation_State.READY_FOR_AUTHENTICATION && authCode\n ? 'auth-code'\n : 'qr-code';\n return (\n <Viewport.Root activeView={activeView}>\n <Viewport.Views>\n <Viewport.View id='init'>\n {/* This view intentionally left blank while conditionally rendering the viewport. */}\n </Viewport.View>\n <Viewport.View id='complete'>\n <InvitationComplete statusValue={state} />\n </Viewport.View>\n <Viewport.View id='auth-code'>\n <InvitationAuthCode id={invitationId} code={authCode ?? 'never'} onCancel={onBack} />\n </Viewport.View>\n <Viewport.View id='qr-code'>\n <InvitationQR id={invitationId} url={url} onCancel={onBack} />\n </Viewport.View>\n </Viewport.Views>\n </Viewport.Root>\n );\n};\n\nconst InvitationQR = ({ id, url, onCancel }: { id: string; url: string; onCancel?: () => void }) => {\n const { t } = UiHooks.useTranslation(shellTranslationKey);\n const qrLabel = UiHooks.useId('members-container__qr-code');\n const emoji = hexToEmoji(id);\n return (\n <>\n <p className='text-fg-muted'>{t('qr-code.description', { ns: meta.profile.key })}</p>\n <div role='group' className='grid grid-cols-[1fr_min-content] my-2 gap-2'>\n <div className='w-full aspect-square relative text-fg-muted'>\n <QrCode.QrCode aria-labelledby={qrLabel} errorCorrection='Q' value={url ?? 'never'} />\n <Centered>\n <Emoji text={emoji} />\n </Centered>\n </div>\n <span id={qrLabel} className='sr-only'>\n {t('qr.label')}\n </span>\n <SystemButton.Clipboard value={url ?? 'never'} />\n </div>\n <Button.Root variant='ghost' onClick={onCancel}>\n {t('cancel.label')}\n </Button.Root>\n </>\n );\n};\n\nconst InvitationAuthCode = ({ id, code, onCancel }: { id: string; code: string; onCancel?: () => void }) => {\n const { t } = UiHooks.useTranslation(shellTranslationKey);\n const emoji = hexToEmoji(id);\n\n return (\n <>\n <p className='text-fg-muted'>{t('auth-other-device-emoji.message')}</p>\n {emoji && <Emoji text={emoji} className='mx-auto my-2 text-center' />}\n <p className='text-fg-muted'>{t('auth-code.message')}</p>\n <AuthCode code={code} large classNames='mx-auto my-2 text-center grow' />\n <Button.Root variant='ghost' onClick={onCancel}>\n {t('cancel.label')}\n </Button.Root>\n </>\n );\n};\n\nconst InvitationComplete = ({ statusValue }: { statusValue: number }) => {\n return statusValue > 0 ? (\n <Icon.Icon icon='ph--check--regular' size='xl' classNames='m-trim-xs' />\n ) : (\n <Icon.Icon icon='ph--x--regular' size='xl' classNames='m-trim-xs' />\n );\n};\n\nMembersContainer.displayName = 'MembersContainer';\n"],"mappings":"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"}
1
+ {"version":3,"file":"chunk-MembersContainer.mjs","names":[],"sources":["../../src/containers/MembersContainer/MembersContainer.tsx"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Option from 'effect/Option';\nimport React, { type Dispatch, type SetStateAction, useMemo, useState } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport * as Surface from '@dxos/app-framework/Surface';\nimport * as AppAnnotation from '@dxos/app-toolkit/AppAnnotation';\nimport * as AppSurface from '@dxos/app-toolkit/AppSurface';\nimport * as LayoutOperation from '@dxos/app-toolkit/LayoutOperation';\nimport { Annotation, Obj } from '@dxos/echo';\nimport { log } from '@dxos/log';\nimport { useConfig } from '@dxos/react-client';\nimport { type SpaceMember_Role, useSpaceInvitations } from '@dxos/react-client/echo';\nimport { useContacts } from '@dxos/react-client/halo';\nimport {\n type CancellableInvitationObservable,\n type Invitation,\n Invitation_AuthMethod,\n Invitation_State,\n Invitation_Type,\n InvitationEncoder,\n} from '@dxos/react-client/invitations';\nimport { Form } from '@dxos/react-ui-form';\nimport * as Button from '@dxos/react-ui/Button';\nimport * as UiHooks from '@dxos/react-ui/Hooks';\nimport * as Icon from '@dxos/react-ui/Icon';\nimport * as Layout from '@dxos/react-ui/Layout';\nimport * as QrCode from '@dxos/react-ui/QrCode';\nimport * as SystemButton from '@dxos/react-ui/SystemButton';\nimport {\n type ActionMenuItem,\n AuthCode,\n BifurcatedAction,\n Centered,\n Emoji,\n InvitationList,\n SpaceMemberList,\n Viewport,\n contactDisplayName,\n contactKeyHex,\n translationKey as shellTranslationKey,\n} from '@dxos/shell/react';\nimport { hexToEmoji } from '@dxos/util';\n\nimport { meta } from '#meta';\nimport { SpaceOperation } from '#types';\n\n// TODO(wittjosiah): Copied from Shell.\nconst activeActionKey = 'dxos:react-shell/space-manager/active-action';\n\nconst handleInvitationEvent = (invitation: Invitation, subscription: ZenObservable.Subscription) => {\n const invitationCode = InvitationEncoder.encode(invitation);\n if (invitation.state === Invitation_State.CONNECTING) {\n log.info(JSON.stringify({ invitationCode, authCode: invitation.authCode }));\n subscription.unsubscribe();\n }\n};\n\nexport type MembersContainerProps = AppSurface.SpaceArticleProps<{\n createInvitationUrl: (invitationCode: string) => string;\n}>;\n\nexport const MembersContainer = ({ space, createInvitationUrl }: MembersContainerProps) => {\n const { t } = UiHooks.useTranslation(meta.profile.key);\n const config = useConfig();\n const { invokePromise } = Hooks.useOperationInvoker();\n const invitations = useSpaceInvitations(space.key);\n const visibleInvitations = invitations?.filter(\n (invitation) => ![Invitation_State.CANCELLED].includes(invitation.get().state),\n );\n\n const [activeAction, setInternalActiveAction] = useState(localStorage.getItem(activeActionKey) ?? 'inviteMany');\n\n const setActiveAction = (nextAction: string) => {\n setInternalActiveAction(nextAction);\n localStorage.setItem(activeActionKey, nextAction);\n };\n\n // TODO(wittjosiah): Track which was the most recently viewed object.\n const target = Annotation.get(space.properties, AppAnnotation.RootCollectionAnnotation).pipe(Option.getOrUndefined)\n ?.target?.objects[0]?.target;\n\n const inviteActions = useMemo(\n (): Record<string, ActionMenuItem> => ({\n inviteOne: {\n label: t('invite-one.label', { ns: shellTranslationKey }),\n description: t('invite-one.description', { ns: shellTranslationKey }),\n icon: 'ph--user-plus--regular',\n testId: 'membersContainer.inviteOne',\n onClick: async () => {\n const { data: invitation } = await invokePromise(SpaceOperation.Share, {\n space,\n type: Invitation_Type.INTERACTIVE,\n authMethod: Invitation_AuthMethod.SHARED_SECRET,\n multiUse: false,\n target: target && Obj.getURI(target),\n });\n if (invitation && config.values.runtime?.app?.env?.DX_ENVIRONMENT !== 'production') {\n const subscription: ZenObservable.Subscription = (invitation as CancellableInvitationObservable).subscribe(\n (invitation: Invitation) => handleInvitationEvent(invitation, subscription),\n );\n }\n },\n },\n inviteMany: {\n label: t('invite-many.label', { ns: shellTranslationKey }),\n description: t('invite-many.description', { ns: shellTranslationKey }),\n icon: 'ph--users-three--regular',\n testId: 'membersContainer.inviteMany',\n onClick: async () => {\n const { data: invitation } = await invokePromise(SpaceOperation.Share, {\n space,\n type: Invitation_Type.DELEGATED,\n authMethod: Invitation_AuthMethod.KNOWN_PUBLIC_KEY,\n multiUse: true,\n target: target && Obj.getURI(target),\n });\n if (invitation && config.values.runtime?.app?.env?.DX_ENVIRONMENT !== 'production') {\n const subscription: ZenObservable.Subscription = (invitation as CancellableInvitationObservable).subscribe(\n (invitation: Invitation) => handleInvitationEvent(invitation, subscription),\n );\n }\n },\n },\n }),\n [t, space, target, invokePromise],\n );\n\n const contacts = useContacts();\n const isSurfaceAvailable = Surface.useIsAvailable();\n const contactPickerData = useMemo(\n (): AppSurface.ContactPickerData => ({\n space,\n onAdd: async (identityKeys: string[], role: SpaceMember_Role) => {\n const { data, error } = await invokePromise(SpaceOperation.AddMembers, { space, identityKeys, role });\n if (error) {\n log.catch(error);\n }\n const result = data ?? {\n joinUrl: '',\n failed: identityKeys.map((key) => ({ key, error: error?.message ?? 'Unknown error' })),\n notNotified: [],\n };\n const namesOf = (entries: readonly { key: string }[]) =>\n entries\n .map(({ key }) => {\n const contact = contacts.find((candidate) => contactKeyHex(candidate) === key);\n return contact ? contactDisplayName(contact) : key.slice(0, 8);\n })\n .join(', ');\n if (result.failed.length > 0) {\n const names = namesOf(result.failed);\n await invokePromise(LayoutOperation.AddToast, {\n id: `${meta.profile.key}/add-members-failed`,\n title: ['add-members-failed-toast.title', { ns: meta.profile.key }],\n // Label tuples carry no interpolation values, so the names are resolved here.\n description: t('add-members-failed-toast.description', { names }),\n icon: 'ph--warning--regular',\n });\n }\n if (result.notNotified.length > 0) {\n const names = namesOf(result.notNotified);\n const accountRequired = result.notNotified.some(({ reason }) => reason === 'account-required');\n const { joinUrl } = result;\n await invokePromise(LayoutOperation.AddToast, {\n id: `${meta.profile.key}/add-members-not-notified`,\n title: ['add-members-not-notified-toast.title', { ns: meta.profile.key }],\n // Label tuples carry no interpolation values, so the names are resolved here.\n description: t(\n accountRequired\n ? 'add-members-not-notified-account-toast.description'\n : 'add-members-not-notified-toast.description',\n { names },\n ),\n icon: 'ph--bell-slash--regular',\n ...(joinUrl\n ? {\n actionLabel: ['copy-link.label', { ns: meta.profile.key }],\n onAction: () =>\n void navigator.clipboard\n .writeText(joinUrl)\n .catch((error) => log.warn('failed to copy join link', { error })),\n }\n : {}),\n });\n }\n\n return result;\n },\n }),\n [t, space, contacts, invokePromise],\n );\n const showContactPicker = isSurfaceAvailable({ type: AppSurface.ContactPicker, data: contactPickerData });\n\n const [selectedInvitation, setSelectedInvitation] = useState<CancellableInvitationObservable | null>(null);\n const handleSend = (event: { type: 'selectInvitation'; invitation: CancellableInvitationObservable }) => {\n setSelectedInvitation(event.invitation);\n };\n const handleBack = () => {\n setSelectedInvitation(null);\n };\n\n return (\n <Form.Root variant='settings'>\n <Form.Viewport scroll>\n <Form.Content>\n <Form.FieldSet label={t('members-verbose.label')} description={t('members.description')}>\n <Form.FieldSet label={t('members.label')}>\n <SpaceMemberList spaceKey={space.key} includeSelf />\n </Form.FieldSet>\n {showContactPicker && (\n <Form.FieldSet label={t('add-known-people.label')}>\n <Surface.Surface type={AppSurface.ContactPicker} data={contactPickerData} limit={1} />\n </Form.FieldSet>\n )}\n <Form.FieldSet\n label={t('invitations.label')}\n description={selectedInvitation ? undefined : t('space-invitation.description')}\n >\n {selectedInvitation && <InvitationSection {...selectedInvitation} onBack={handleBack} />}\n {!selectedInvitation && (\n <>\n <InvitationList\n send={handleSend}\n invitations={visibleInvitations ?? []}\n onClickRemove={(invitation) => invitation.cancel()}\n createInvitationUrl={createInvitationUrl}\n />\n <BifurcatedAction\n actions={inviteActions}\n activeAction={activeAction}\n onChangeActiveAction={setActiveAction as Dispatch<SetStateAction<string>>}\n data-testid='membersContainer.createInvitation'\n />\n </>\n )}\n </Form.FieldSet>\n </Form.FieldSet>\n </Form.Content>\n </Form.Viewport>\n </Form.Root>\n );\n};\n\n// TODO(wittjosiah): Reconcile the below components with DevicesContainer in @dxos/plugin-client.\n\ntype InvitationComponentProps = Partial<\n Pick<Invitation, 'authCode' | 'invitationId'> & {\n state: Invitation_State;\n url: string;\n onBack: () => void;\n }\n>;\n\nconst InvitationSection = ({\n state = Invitation_State.INIT,\n authCode,\n invitationId = 'never',\n url = 'never',\n onBack,\n}: InvitationComponentProps) => {\n const activeView =\n state < 0\n ? 'init'\n : state >= Invitation_State.CANCELLED\n ? 'complete'\n : state >= Invitation_State.READY_FOR_AUTHENTICATION && authCode\n ? 'auth-code'\n : 'qr-code';\n return (\n <Viewport.Root activeView={activeView}>\n <Viewport.Views>\n <Viewport.View id='init'>\n {/* This view intentionally left blank while conditionally rendering the viewport. */}\n </Viewport.View>\n <Viewport.View id='complete'>\n <InvitationComplete statusValue={state} />\n </Viewport.View>\n <Viewport.View id='auth-code'>\n <InvitationAuthCode id={invitationId} code={authCode ?? 'never'} onCancel={onBack} />\n </Viewport.View>\n <Viewport.View id='qr-code'>\n <InvitationQR id={invitationId} url={url} onCancel={onBack} />\n </Viewport.View>\n </Viewport.Views>\n </Viewport.Root>\n );\n};\n\nconst InvitationQR = ({ id, url, onCancel }: { id: string; url: string; onCancel?: () => void }) => {\n const { t } = UiHooks.useTranslation(shellTranslationKey);\n const qrLabel = UiHooks.useId('members-container__qr-code');\n const emoji = hexToEmoji(id);\n return (\n <>\n <p className='text-fg-muted'>{t('qr-code.description', { ns: meta.profile.key })}</p>\n <Layout.Grid role='group' cols={['fill', 'min']} gap='sm' classNames='my-2'>\n <div className='w-full aspect-square relative text-fg-muted'>\n <QrCode.QrCode aria-labelledby={qrLabel} errorCorrection='Q' value={url ?? 'never'} />\n <Centered>\n <Emoji text={emoji} />\n </Centered>\n </div>\n <span id={qrLabel} className='sr-only'>\n {t('qr.label')}\n </span>\n <SystemButton.Clipboard value={url ?? 'never'} />\n </Layout.Grid>\n <Button.Root variant='ghost' onClick={onCancel}>\n {t('cancel.label')}\n </Button.Root>\n </>\n );\n};\n\nconst InvitationAuthCode = ({ id, code, onCancel }: { id: string; code: string; onCancel?: () => void }) => {\n const { t } = UiHooks.useTranslation(shellTranslationKey);\n const emoji = hexToEmoji(id);\n\n return (\n <>\n <p className='text-fg-muted'>{t('auth-other-device-emoji.message')}</p>\n {emoji && <Emoji text={emoji} className='mx-auto my-2 text-center' />}\n <p className='text-fg-muted'>{t('auth-code.message')}</p>\n <AuthCode code={code} large classNames='mx-auto my-2 text-center grow' />\n <Button.Root variant='ghost' onClick={onCancel}>\n {t('cancel.label')}\n </Button.Root>\n </>\n );\n};\n\nconst InvitationComplete = ({ statusValue }: { statusValue: number }) => {\n return statusValue > 0 ? (\n <Icon.Icon icon='ph--check--regular' size='xl' classNames='m-trim-xs' />\n ) : (\n <Icon.Icon icon='ph--x--regular' size='xl' classNames='m-trim-xs' />\n );\n};\n\nMembersContainer.displayName = 'MembersContainer';\n"],"mappings":"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"}
@@ -193,7 +193,7 @@ var ObjectMasonryArticle = ({ role, attendableId, objects, emptyMessage }) => {
193
193
  items
194
194
  }) }),
195
195
  /* @__PURE__ */ jsx(Panel.Footer, {
196
- classNames: "flex items-center p-1 border-t border-separator-subtle",
196
+ classNames: "items-center p-1 border-t border-separator-subtle",
197
197
  children: t("item-count.label", { count: items.length })
198
198
  })
199
199
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-ObjectMasonry.mjs","names":[],"sources":["../../src/containers/ObjectMasonry/ObjectTile.tsx","../../src/containers/ObjectMasonry/ObjectMasonry.tsx","../../src/containers/ObjectMasonry/ObjectMasonryArticle.tsx","../../src/containers/ObjectMasonry/index.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Option from 'effect/Option';\nimport React, { useCallback, useMemo } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport * as Surface from '@dxos/app-framework/Surface';\nimport * as AppSurface from '@dxos/app-toolkit/AppSurface';\nimport * as CollectionOperation from '@dxos/app-toolkit/CollectionOperation';\nimport * as ObjectCard from '@dxos/app-toolkit/ObjectCard';\nimport * as TypeOptions from '@dxos/app-toolkit/TypeOptions';\nimport { Obj, Type } from '@dxos/echo';\nimport { useObject } from '@dxos/echo-react';\nimport * as Card from '@dxos/react-ui/Card';\nimport * as Focus from '@dxos/react-ui/Focus';\nimport * as UiHooks from '@dxos/react-ui/Hooks';\nimport * as Menu from '@dxos/react-ui/Menu';\nimport * as Tag from '@dxos/react-ui/Tag';\nimport { CardAnnotation } from '@dxos/schema';\nimport { osTranslations } from '@dxos/ui-theme';\n\nimport { useArchiveMenuItem } from '#hooks';\nimport { meta } from '#meta';\n\n/** Callbacks are absent on a read-only tile (e.g. a staged merge result). */\nexport type TileData = {\n object: Obj.Unknown;\n current: boolean;\n onSelect?: (id: string) => void;\n onOpen?: (object: Obj.Unknown) => void;\n onDelete?: (object: Obj.Unknown) => void;\n};\n\nexport const TileAdapter = ({ data }: { data: TileData | undefined; index: number }) => {\n if (!data?.object) {\n return null;\n }\n\n return <ObjectTile {...data} />;\n};\n\n/** Selectable header-only card for a single object. */\nexport const ObjectTile = ({ object, current, onSelect, onOpen, onDelete }: TileData) => {\n const { t } = UiHooks.useTranslation(meta.profile.key);\n const { invokePromise } = Hooks.useOperationInvoker();\n // Subscribe so the label re-renders when the object changes.\n const [live] = useObject(object);\n const typename = Obj.getTypename(live);\n const label =\n Obj.getLabel(live) ||\n t('object-name.placeholder', { ns: typename ?? meta.profile.key, defaultValue: t('object-name.placeholder') });\n\n // Render a content preview body only for types that opt in via `CardAnnotation`.\n const type = Obj.getType(object);\n const showCardContent = !!type && Option.getOrElse(CardAnnotation.get(Type.getSchema(type)), () => false);\n const cardData = useMemo<AppSurface.ObjectCardData>(() => ({ subject: object }), [object]);\n\n const { archived, item: archiveItem } = useArchiveMenuItem(object);\n\n // `Focus.Item` calls `onCurrentChange` on click and on Enter. A card click toggles selection —\n // the companion follows the selection, so navigating away on every click would fight the review\n // workflow; opening stays available from the card menu.\n const handleCurrentChange = useCallback(() => onSelect?.(object.id), [onSelect, object]);\n\n const menuItems = useMemo(\n () => [\n ...(onOpen\n ? [\n {\n icon: 'ph--arrow-square-out--regular',\n label: t('open-object.label', {\n ns: typename ?? meta.profile.key,\n defaultValue: t('open-object.label'),\n }),\n onClick: () => onOpen(object),\n },\n ]\n : []),\n // Offered where the tile is interactive, which is where it can be opened.\n ...(onOpen && TypeOptions.isUserObject(object)\n ? [\n {\n icon: CollectionOperation.OpenAddToCollection.meta.icon,\n label: t('add-to-collection.label', { ns: osTranslations }),\n onClick: () => void invokePromise(CollectionOperation.OpenAddToCollection, { object }),\n },\n ]\n : []),\n ...(onDelete\n ? [\n {\n icon: 'ph--trash--regular',\n label: t('delete-object.label', {\n ns: typename ?? meta.profile.key,\n defaultValue: t('delete-object.label'),\n }),\n onClick: () => onDelete(object),\n },\n ]\n : []),\n ...(archiveItem ? [archiveItem] : []),\n ],\n [t, typename, onOpen, onDelete, archiveItem, object, invokePromise],\n );\n\n return (\n <Focus.Item asChild current={current} onCurrentChange={handleCurrentChange}>\n <ObjectCard.Root classNames={['dx-hover', onSelect && 'cursor-pointer', current && 'dx-current']}>\n <ObjectCard.Header\n subject={live}\n menu={\n menuItems.length > 0 && (\n <Card.Menu label={t('toolbar-menu.label', { ns: osTranslations })}>\n {menuItems.map((menuItem) => (\n <Menu.Item\n key={menuItem.label}\n item={{ value: menuItem.label, label: menuItem.label, icon: menuItem.icon }}\n onClick={menuItem.onClick}\n />\n ))}\n </Card.Menu>\n )\n }\n >\n {label}\n </ObjectCard.Header>\n {archived && (\n <Card.Row>\n <Tag.Tag classNames='justify-self-start'>{t('archived.label')}</Tag.Tag>\n </Card.Row>\n )}\n {showCardContent && <Surface.Surface type={AppSurface.CardContent} data={cardData} limit={1} />}\n </ObjectCard.Root>\n </Focus.Item>\n );\n};\n\nObjectTile.displayName = 'ObjectTile';\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React from 'react';\n\nimport { Masonry } from '@dxos/react-ui-masonry';\n\nimport { TileAdapter, type TileData } from './ObjectTile.tsx';\n\nexport type ObjectMasonryProps = {\n /** Keys the persisted column layout; stable per view, so the grid does not reflow on remount. */\n cacheKey: string;\n items: TileData[];\n};\n\n/** A grid of object cards — the shared body of every view that shows a set of objects as tiles. */\nexport const ObjectMasonry = ({ cacheKey, items }: ObjectMasonryProps) => (\n <Masonry.Root Tile={TileAdapter}>\n <Masonry.Content>\n <Masonry.Viewport cacheKey={cacheKey} getId={(data) => data.object.id} items={items} />\n </Masonry.Content>\n </Masonry.Root>\n);\n\nObjectMasonry.displayName = 'ObjectMasonry';\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { useCallback, useMemo } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport * as GraphPath from '@dxos/app-toolkit/GraphPath';\nimport * as LayoutOperation from '@dxos/app-toolkit/LayoutOperation';\nimport { Obj } from '@dxos/echo';\nimport { useSelection, useSelectionActions } from '@dxos/react-ui-attention';\nimport { SearchList, useSearchListResults } from '@dxos/react-ui-search';\nimport * as UiHooks from '@dxos/react-ui/Hooks';\nimport * as Panel from '@dxos/react-ui/Panel';\nimport * as Status from '@dxos/react-ui/Status';\nimport * as Toolbar from '@dxos/react-ui/Toolbar';\n\nimport { meta } from '#meta';\n\nimport { ObjectMasonry } from './ObjectMasonry.tsx';\nimport { type TileData } from './ObjectTile.tsx';\n\nexport type ObjectMasonryArticleProps = {\n role?: string;\n /** The graph node being viewed: the selection context, the layout cache key, and the open pivot. */\n attendableId: string;\n objects: readonly Obj.Unknown[];\n /** Shown when `objects` is empty; defaults to the generic \"nothing here\" message. */\n emptyMessage?: string;\n};\n\n/**\n * A node's objects as a searchable grid of cards, with selection shared through the node's\n * attention context so a companion panel follows what is picked.\n *\n * The objects are given rather than queried: the callers are nodes that stand for a set (a type, a\n * project's chats, its artifacts) and each knows its own membership rule. What they share is how\n * that set is presented, which is all this owns.\n */\nexport const ObjectMasonryArticle = ({ role, attendableId, objects, emptyMessage }: ObjectMasonryArticleProps) => {\n const { t } = UiHooks.useTranslation(meta.profile.key);\n const { invokePromise } = Hooks.useOperationInvoker();\n\n // Ordered by label: the query returns index order, which reads as arbitrary to someone scanning a\n // directory of cards. Sorted on the INPUT, leaving the search below free to rank by match score.\n const ordered = useMemo(\n () =>\n [...objects].sort((a, b) =>\n (Obj.getLabel(a) ?? '').localeCompare(Obj.getLabel(b) ?? '', undefined, { sensitivity: 'base' }),\n ),\n [objects],\n );\n\n const { results, handleSearch } = useSearchListResults<Obj.Unknown>({\n items: ordered,\n extract: (object) => Obj.getLabel(object) ?? '',\n });\n\n const selectedIds = useSelection(attendableId, 'multi');\n const { toggle: toggleSelected } = useSelectionActions(attendableId);\n\n const handleOpen = useCallback(\n (object: Obj.Unknown) => {\n const id = Obj.getURI(object);\n void invokePromise(LayoutOperation.Select, { contextId: attendableId, subject: { mode: 'single', id } });\n void invokePromise(LayoutOperation.Open, {\n // Opened under the node being viewed rather than the object's canonical path, so an object\n // reached from this set opens within it.\n subject: [GraphPath.getCollectionObjectPath(attendableId, object.id)],\n pivotId: attendableId,\n disposition: 'add',\n navigation: 'immediate',\n });\n },\n [attendableId, invokePromise],\n );\n\n const items = useMemo<TileData[]>(\n () =>\n results.map((object) => ({\n object,\n current: selectedIds.includes(object.id),\n onSelect: toggleSelected,\n onOpen: handleOpen,\n })),\n [results, selectedIds, toggleSelected, handleOpen],\n );\n\n // Empty for two different reasons, which want different words: nothing here at all, versus\n // nothing matching what was typed.\n const empty =\n objects.length === 0\n ? (emptyMessage ?? t('type-collection-empty.message'))\n : results.length === 0\n ? t('search-no-results.message')\n : undefined;\n\n return (\n <SearchList.Root onSearch={handleSearch}>\n <Panel.Root role={role}>\n <Panel.Header>\n <Toolbar.Root>\n <SearchList.Input placeholder={t('search-placeholder.label')} />\n </Toolbar.Root>\n </Panel.Header>\n <Panel.Body>\n {empty ? (\n <Status.Empty classNames='h-full'>{empty}</Status.Empty>\n ) : (\n <ObjectMasonry cacheKey={attendableId} items={items} />\n )}\n </Panel.Body>\n <Panel.Footer classNames='flex items-center p-1 border-t border-separator-subtle'>\n {t('item-count.label', { count: items.length })}\n </Panel.Footer>\n </Panel.Root>\n </SearchList.Root>\n );\n};\n\nObjectMasonryArticle.displayName = 'ObjectMasonryArticle';\n","//\n// Copyright 2026 DXOS.org\n//\n\nexport * from './ObjectMasonry.tsx';\nexport * from './ObjectMasonryArticle.tsx';\nexport * from './ObjectTile.tsx';\n"],"mappings":"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"}
1
+ {"version":3,"file":"chunk-ObjectMasonry.mjs","names":[],"sources":["../../src/containers/ObjectMasonry/ObjectTile.tsx","../../src/containers/ObjectMasonry/ObjectMasonry.tsx","../../src/containers/ObjectMasonry/ObjectMasonryArticle.tsx","../../src/containers/ObjectMasonry/index.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Option from 'effect/Option';\nimport React, { useCallback, useMemo } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport * as Surface from '@dxos/app-framework/Surface';\nimport * as AppSurface from '@dxos/app-toolkit/AppSurface';\nimport * as CollectionOperation from '@dxos/app-toolkit/CollectionOperation';\nimport * as ObjectCard from '@dxos/app-toolkit/ObjectCard';\nimport * as TypeOptions from '@dxos/app-toolkit/TypeOptions';\nimport { Obj, Type } from '@dxos/echo';\nimport { useObject } from '@dxos/echo-react';\nimport * as Card from '@dxos/react-ui/Card';\nimport * as Focus from '@dxos/react-ui/Focus';\nimport * as UiHooks from '@dxos/react-ui/Hooks';\nimport * as Menu from '@dxos/react-ui/Menu';\nimport * as Tag from '@dxos/react-ui/Tag';\nimport { CardAnnotation } from '@dxos/schema';\nimport { osTranslations } from '@dxos/ui-theme';\n\nimport { useArchiveMenuItem } from '#hooks';\nimport { meta } from '#meta';\n\n/** Callbacks are absent on a read-only tile (e.g. a staged merge result). */\nexport type TileData = {\n object: Obj.Unknown;\n current: boolean;\n onSelect?: (id: string) => void;\n onOpen?: (object: Obj.Unknown) => void;\n onDelete?: (object: Obj.Unknown) => void;\n};\n\nexport const TileAdapter = ({ data }: { data: TileData | undefined; index: number }) => {\n if (!data?.object) {\n return null;\n }\n\n return <ObjectTile {...data} />;\n};\n\n/** Selectable header-only card for a single object. */\nexport const ObjectTile = ({ object, current, onSelect, onOpen, onDelete }: TileData) => {\n const { t } = UiHooks.useTranslation(meta.profile.key);\n const { invokePromise } = Hooks.useOperationInvoker();\n // Subscribe so the label re-renders when the object changes.\n const [live] = useObject(object);\n const typename = Obj.getTypename(live);\n const label =\n Obj.getLabel(live) ||\n t('object-name.placeholder', { ns: typename ?? meta.profile.key, defaultValue: t('object-name.placeholder') });\n\n // Render a content preview body only for types that opt in via `CardAnnotation`.\n const type = Obj.getType(object);\n const showCardContent = !!type && Option.getOrElse(CardAnnotation.get(Type.getSchema(type)), () => false);\n const cardData = useMemo<AppSurface.ObjectCardData>(() => ({ subject: object }), [object]);\n\n const { archived, item: archiveItem } = useArchiveMenuItem(object);\n\n // `Focus.Item` calls `onCurrentChange` on click and on Enter. A card click toggles selection —\n // the companion follows the selection, so navigating away on every click would fight the review\n // workflow; opening stays available from the card menu.\n const handleCurrentChange = useCallback(() => onSelect?.(object.id), [onSelect, object]);\n\n const menuItems = useMemo(\n () => [\n ...(onOpen\n ? [\n {\n icon: 'ph--arrow-square-out--regular',\n label: t('open-object.label', {\n ns: typename ?? meta.profile.key,\n defaultValue: t('open-object.label'),\n }),\n onClick: () => onOpen(object),\n },\n ]\n : []),\n // Offered where the tile is interactive, which is where it can be opened.\n ...(onOpen && TypeOptions.isUserObject(object)\n ? [\n {\n icon: CollectionOperation.OpenAddToCollection.meta.icon,\n label: t('add-to-collection.label', { ns: osTranslations }),\n onClick: () => void invokePromise(CollectionOperation.OpenAddToCollection, { object }),\n },\n ]\n : []),\n ...(onDelete\n ? [\n {\n icon: 'ph--trash--regular',\n label: t('delete-object.label', {\n ns: typename ?? meta.profile.key,\n defaultValue: t('delete-object.label'),\n }),\n onClick: () => onDelete(object),\n },\n ]\n : []),\n ...(archiveItem ? [archiveItem] : []),\n ],\n [t, typename, onOpen, onDelete, archiveItem, object, invokePromise],\n );\n\n return (\n <Focus.Item asChild current={current} onCurrentChange={handleCurrentChange}>\n <ObjectCard.Root classNames={['dx-hover', onSelect && 'cursor-pointer', current && 'dx-current']}>\n <ObjectCard.Header\n subject={live}\n menu={\n menuItems.length > 0 && (\n <Card.Menu label={t('toolbar-menu.label', { ns: osTranslations })}>\n {menuItems.map((menuItem) => (\n <Menu.Item\n key={menuItem.label}\n item={{ value: menuItem.label, label: menuItem.label, icon: menuItem.icon }}\n onClick={menuItem.onClick}\n />\n ))}\n </Card.Menu>\n )\n }\n >\n {label}\n </ObjectCard.Header>\n {archived && (\n <Card.Row>\n <Tag.Tag classNames='justify-self-start'>{t('archived.label')}</Tag.Tag>\n </Card.Row>\n )}\n {showCardContent && <Surface.Surface type={AppSurface.CardContent} data={cardData} limit={1} />}\n </ObjectCard.Root>\n </Focus.Item>\n );\n};\n\nObjectTile.displayName = 'ObjectTile';\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React from 'react';\n\nimport { Masonry } from '@dxos/react-ui-masonry';\n\nimport { TileAdapter, type TileData } from './ObjectTile.tsx';\n\nexport type ObjectMasonryProps = {\n /** Keys the persisted column layout; stable per view, so the grid does not reflow on remount. */\n cacheKey: string;\n items: TileData[];\n};\n\n/** A grid of object cards — the shared body of every view that shows a set of objects as tiles. */\nexport const ObjectMasonry = ({ cacheKey, items }: ObjectMasonryProps) => (\n <Masonry.Root Tile={TileAdapter}>\n <Masonry.Content>\n <Masonry.Viewport cacheKey={cacheKey} getId={(data) => data.object.id} items={items} />\n </Masonry.Content>\n </Masonry.Root>\n);\n\nObjectMasonry.displayName = 'ObjectMasonry';\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { useCallback, useMemo } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport * as GraphPath from '@dxos/app-toolkit/GraphPath';\nimport * as LayoutOperation from '@dxos/app-toolkit/LayoutOperation';\nimport { Obj } from '@dxos/echo';\nimport { useSelection, useSelectionActions } from '@dxos/react-ui-attention';\nimport { SearchList, useSearchListResults } from '@dxos/react-ui-search';\nimport * as UiHooks from '@dxos/react-ui/Hooks';\nimport * as Panel from '@dxos/react-ui/Panel';\nimport * as Status from '@dxos/react-ui/Status';\nimport * as Toolbar from '@dxos/react-ui/Toolbar';\n\nimport { meta } from '#meta';\n\nimport { ObjectMasonry } from './ObjectMasonry.tsx';\nimport { type TileData } from './ObjectTile.tsx';\n\nexport type ObjectMasonryArticleProps = {\n role?: string;\n /** The graph node being viewed: the selection context, the layout cache key, and the open pivot. */\n attendableId: string;\n objects: readonly Obj.Unknown[];\n /** Shown when `objects` is empty; defaults to the generic \"nothing here\" message. */\n emptyMessage?: string;\n};\n\n/**\n * A node's objects as a searchable grid of cards, with selection shared through the node's\n * attention context so a companion panel follows what is picked.\n *\n * The objects are given rather than queried: the callers are nodes that stand for a set (a type, a\n * project's chats, its artifacts) and each knows its own membership rule. What they share is how\n * that set is presented, which is all this owns.\n */\nexport const ObjectMasonryArticle = ({ role, attendableId, objects, emptyMessage }: ObjectMasonryArticleProps) => {\n const { t } = UiHooks.useTranslation(meta.profile.key);\n const { invokePromise } = Hooks.useOperationInvoker();\n\n // Ordered by label: the query returns index order, which reads as arbitrary to someone scanning a\n // directory of cards. Sorted on the INPUT, leaving the search below free to rank by match score.\n const ordered = useMemo(\n () =>\n [...objects].sort((a, b) =>\n (Obj.getLabel(a) ?? '').localeCompare(Obj.getLabel(b) ?? '', undefined, { sensitivity: 'base' }),\n ),\n [objects],\n );\n\n const { results, handleSearch } = useSearchListResults<Obj.Unknown>({\n items: ordered,\n extract: (object) => Obj.getLabel(object) ?? '',\n });\n\n const selectedIds = useSelection(attendableId, 'multi');\n const { toggle: toggleSelected } = useSelectionActions(attendableId);\n\n const handleOpen = useCallback(\n (object: Obj.Unknown) => {\n const id = Obj.getURI(object);\n void invokePromise(LayoutOperation.Select, { contextId: attendableId, subject: { mode: 'single', id } });\n void invokePromise(LayoutOperation.Open, {\n // Opened under the node being viewed rather than the object's canonical path, so an object\n // reached from this set opens within it.\n subject: [GraphPath.getCollectionObjectPath(attendableId, object.id)],\n pivotId: attendableId,\n disposition: 'add',\n navigation: 'immediate',\n });\n },\n [attendableId, invokePromise],\n );\n\n const items = useMemo<TileData[]>(\n () =>\n results.map((object) => ({\n object,\n current: selectedIds.includes(object.id),\n onSelect: toggleSelected,\n onOpen: handleOpen,\n })),\n [results, selectedIds, toggleSelected, handleOpen],\n );\n\n // Empty for two different reasons, which want different words: nothing here at all, versus\n // nothing matching what was typed.\n const empty =\n objects.length === 0\n ? (emptyMessage ?? t('type-collection-empty.message'))\n : results.length === 0\n ? t('search-no-results.message')\n : undefined;\n\n return (\n <SearchList.Root onSearch={handleSearch}>\n <Panel.Root role={role}>\n <Panel.Header>\n <Toolbar.Root>\n <SearchList.Input placeholder={t('search-placeholder.label')} />\n </Toolbar.Root>\n </Panel.Header>\n <Panel.Body>\n {empty ? (\n <Status.Empty classNames='h-full'>{empty}</Status.Empty>\n ) : (\n <ObjectMasonry cacheKey={attendableId} items={items} />\n )}\n </Panel.Body>\n <Panel.Footer classNames='items-center p-1 border-t border-separator-subtle'>\n {t('item-count.label', { count: items.length })}\n </Panel.Footer>\n </Panel.Root>\n </SearchList.Root>\n );\n};\n\nObjectMasonryArticle.displayName = 'ObjectMasonryArticle';\n","//\n// Copyright 2026 DXOS.org\n//\n\nexport * from './ObjectMasonry.tsx';\nexport * from './ObjectMasonryArticle.tsx';\nexport * from './ObjectTile.tsx';\n"],"mappings":"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"}
@@ -14,7 +14,6 @@ import { useObject } from "@dxos/echo-react";
14
14
  import { ActionToolbar, MenuBuilder, graphActions, isToolbarAction, useMenuBuilder } from "@dxos/react-ui-menu";
15
15
  import * as Hooks from "@dxos/react-ui/Hooks";
16
16
  import { jsx, jsxs } from "react/jsx-runtime";
17
- import { mx } from "@dxos/ui-theme";
18
17
  import * as Field from "@dxos/react-ui/Field";
19
18
  import * as ScrollArea from "@dxos/react-ui/ScrollArea";
20
19
  import * as Panel from "@dxos/react-ui/Panel";
@@ -68,8 +67,10 @@ var RecordArticle = ({ role, subject, attendableId }) => {
68
67
  limit: 1
69
68
  })]
70
69
  }),
71
- relatedObjects.length > 0 && /* @__PURE__ */ jsxs("div", {
72
- className: mx("dx-expand flex flex-col gap-form-gap", singleColumn ? "dx-card-max-width" : "w-full"),
70
+ relatedObjects.length > 0 && /* @__PURE__ */ jsxs(Layout.Flex, {
71
+ column: true,
72
+ gap: "form",
73
+ classNames: ["dx-expand", singleColumn ? "dx-card-max-width" : "w-full"],
73
74
  children: [
74
75
  /* @__PURE__ */ jsx(Field.Root, { children: /* @__PURE__ */ jsx(Field.Label, { children: t("related-objects.label") }) }),
75
76
  /* @__PURE__ */ jsx(RelatedTypeFilter, {
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-RecordArticle.mjs","names":[],"sources":["../../src/containers/RecordArticle/RecordArticle.tsx"],"sourcesContent":["//\n// Copyright 2023 DXOS.org\n//\n\nimport React, { useCallback } from 'react';\n\nimport * as Surface from '@dxos/app-framework/Surface';\nimport * as AppSurface from '@dxos/app-toolkit/AppSurface';\nimport * as ToolkitHooks from '@dxos/app-toolkit/Hooks';\nimport * as ToolkitObjectCard from '@dxos/app-toolkit/ObjectCard';\nimport { Obj, Type } from '@dxos/echo';\nimport { useObject } from '@dxos/echo-react';\nimport * as GraphHooks from '@dxos/plugin-graph/Hooks';\nimport { Masonry } from '@dxos/react-ui-masonry';\nimport {\n type ActionExecutor,\n type ActionGraphProps,\n ActionToolbar,\n MenuBuilder,\n graphActions,\n isToolbarAction,\n useMenuBuilder,\n} from '@dxos/react-ui-menu';\nimport * as Card from '@dxos/react-ui/Card';\nimport * as Field from '@dxos/react-ui/Field';\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport * as Layout from '@dxos/react-ui/Layout';\nimport * as Panel from '@dxos/react-ui/Panel';\nimport * as ScrollArea from '@dxos/react-ui/ScrollArea';\nimport { mx } from '@dxos/ui-theme';\n\nimport { ObjectCard, RelatedTypeFilter } from '#components';\nimport { useRelatedObjects, useRelatedTypeFilter } from '#hooks';\nimport { meta } from '#meta';\nimport { SpaceSurface } from '#types';\n\nexport const RecordArticle = ({ role, subject, attendableId }: AppSurface.ObjectArticleProps) => {\n const { t } = Hooks.useTranslation(meta.profile.key);\n const { actions, onAction } = useMenuActions(attendableId);\n useObject(subject);\n // Obj.getType fails for database-registered (dynamic) schemas due to DXN mismatch;\n // fall back to typename query which matches TypeSchema.typename.\n const db = Obj.getDatabase(subject);\n const typename = Obj.getTypename(subject);\n const schema =\n Obj.getType(subject) ??\n (typename && db\n ? db.graph.registry\n .list()\n .filter(Type.isType)\n .find((t) => Type.getTypename(t) === typename)\n : undefined);\n const icon = schema && Type.getDatabase(schema) != null ? 'ph--cube--regular' : undefined;\n\n // Keyed by the record, not this article, so the Related companion shares the same filter.\n const relatedObjects = useRelatedObjects(db, subject, { references: true, relations: true });\n const { types, items: related, toggle } = useRelatedTypeFilter(relatedObjects, Obj.getURI(subject).toString());\n const singleColumn = related.length === 1;\n\n return (\n <Panel.Root role={role}>\n <Panel.Header>\n <ActionToolbar {...actions} attendableId={attendableId} onAction={onAction} />\n </Panel.Header>\n <Panel.Body asChild>\n <ScrollArea.Root orientation='vertical'>\n <ScrollArea.Viewport classNames='p-4 space-y-4'>\n <ToolkitObjectCard.Root>\n <ToolkitObjectCard.Header subject={subject} icon={icon} />\n <Card.Body>\n <Surface.Surface type={AppSurface.CardContent} data={{ subject }} limit={1} />\n </Card.Body>\n </ToolkitObjectCard.Root>\n\n {/* TODO(burdon): Only show label if surface exists? */}\n {/* TODO(burdon): Remove this section — move the related actions into the object menu. */}\n <Layout.Flex column gap='form'>\n <Field.Root>\n <Field.Label>{t('related-actions.label')}</Field.Label>\n </Field.Root>\n <Surface.Surface type={SpaceSurface.Prompts} data={{ subject, attendableId: subject.id }} limit={1} />\n </Layout.Flex>\n\n {/* Gated on the unfiltered set so hiding every type does not remove the filter itself. */}\n {relatedObjects.length > 0 && (\n <div\n className={mx('dx-expand flex flex-col gap-form-gap', singleColumn ? 'dx-card-max-width' : 'w-full')}\n >\n <Field.Root>\n <Field.Label>{t('related-objects.label')}</Field.Label>\n </Field.Root>\n {/* `self-start` so the group sizes to its icons rather than stretching this column. */}\n <RelatedTypeFilter classNames='self-start' types={types} onToggle={toggle} />\n {/* The masonry's own gutter would inset these cards relative to the record card above,\n which shares this column — the scroll padding is the article's to own, not theirs. */}\n {/* `centered={false}` on the ROOT, which is column alignment — distinct from\n `Content`'s prop of the same name below (ScrollArea's scrollbar padding). Centred\n columns drift right of the record card above them, which shares this column. */}\n <Masonry.Root Tile={ObjectCard} columns={singleColumn ? 1 : undefined} centered={false}>\n <Masonry.Content padding={false}>\n <Masonry.Viewport items={related} />\n </Masonry.Content>\n </Masonry.Root>\n </div>\n )}\n </ScrollArea.Viewport>\n </ScrollArea.Root>\n </Panel.Body>\n </Panel.Root>\n );\n};\n\n//\n// Hooks\n//\n\n/**\n * Toolbar actions for the record, sourced from its own app-graph node (`disposition: 'toolbar'`).\n *\n * The record view is type-agnostic, so it must not know which actions a given type affords. Any plugin\n * contributes them to the object's node instead — plugin-crm donates enrichment for `Person` /\n * `Organization` — which keeps plugin-space free of a dependency on them and makes each action appear\n * only for the types that declare it.\n */\nconst useMenuActions = (\n attendableId?: string,\n): { actions: ReturnType<typeof useMenuBuilder>; onAction: ActionExecutor } => {\n const { graph } = ToolkitHooks.useAppGraph();\n const runAction = GraphHooks.useActionRunner();\n\n const menuActions = useMenuBuilder(\n (get): ActionGraphProps =>\n attendableId\n ? MenuBuilder.make()\n .subgraph(graphActions(graph, get, attendableId, { filter: isToolbarAction }))\n .build()\n : MenuBuilder.make().build(),\n [graph, attendableId],\n );\n\n const onAction: ActionExecutor = useCallback(\n (action) => {\n void runAction(action, { caller: meta.profile.key });\n },\n [runAction],\n );\n\n return { actions: menuActions, onAction };\n};\n\nRecordArticle.displayName = 'RecordArticle';\n"],"mappings":"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"}
1
+ {"version":3,"file":"chunk-RecordArticle.mjs","names":[],"sources":["../../src/containers/RecordArticle/RecordArticle.tsx"],"sourcesContent":["//\n// Copyright 2023 DXOS.org\n//\n\nimport React, { useCallback } from 'react';\n\nimport * as Surface from '@dxos/app-framework/Surface';\nimport * as AppSurface from '@dxos/app-toolkit/AppSurface';\nimport * as ToolkitHooks from '@dxos/app-toolkit/Hooks';\nimport * as ToolkitObjectCard from '@dxos/app-toolkit/ObjectCard';\nimport { Obj, Type } from '@dxos/echo';\nimport { useObject } from '@dxos/echo-react';\nimport * as GraphHooks from '@dxos/plugin-graph/Hooks';\nimport { Masonry } from '@dxos/react-ui-masonry';\nimport {\n type ActionExecutor,\n type ActionGraphProps,\n ActionToolbar,\n MenuBuilder,\n graphActions,\n isToolbarAction,\n useMenuBuilder,\n} from '@dxos/react-ui-menu';\nimport * as Card from '@dxos/react-ui/Card';\nimport * as Field from '@dxos/react-ui/Field';\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport * as Layout from '@dxos/react-ui/Layout';\nimport * as Panel from '@dxos/react-ui/Panel';\nimport * as ScrollArea from '@dxos/react-ui/ScrollArea';\n\nimport { ObjectCard, RelatedTypeFilter } from '#components';\nimport { useRelatedObjects, useRelatedTypeFilter } from '#hooks';\nimport { meta } from '#meta';\nimport { SpaceSurface } from '#types';\n\nexport const RecordArticle = ({ role, subject, attendableId }: AppSurface.ObjectArticleProps) => {\n const { t } = Hooks.useTranslation(meta.profile.key);\n const { actions, onAction } = useMenuActions(attendableId);\n useObject(subject);\n // Obj.getType fails for database-registered (dynamic) schemas due to DXN mismatch;\n // fall back to typename query which matches TypeSchema.typename.\n const db = Obj.getDatabase(subject);\n const typename = Obj.getTypename(subject);\n const schema =\n Obj.getType(subject) ??\n (typename && db\n ? db.graph.registry\n .list()\n .filter(Type.isType)\n .find((t) => Type.getTypename(t) === typename)\n : undefined);\n const icon = schema && Type.getDatabase(schema) != null ? 'ph--cube--regular' : undefined;\n\n // Keyed by the record, not this article, so the Related companion shares the same filter.\n const relatedObjects = useRelatedObjects(db, subject, { references: true, relations: true });\n const { types, items: related, toggle } = useRelatedTypeFilter(relatedObjects, Obj.getURI(subject).toString());\n const singleColumn = related.length === 1;\n\n return (\n <Panel.Root role={role}>\n <Panel.Header>\n <ActionToolbar {...actions} attendableId={attendableId} onAction={onAction} />\n </Panel.Header>\n <Panel.Body asChild>\n <ScrollArea.Root orientation='vertical'>\n <ScrollArea.Viewport classNames='p-4 space-y-4'>\n <ToolkitObjectCard.Root>\n <ToolkitObjectCard.Header subject={subject} icon={icon} />\n <Card.Body>\n <Surface.Surface type={AppSurface.CardContent} data={{ subject }} limit={1} />\n </Card.Body>\n </ToolkitObjectCard.Root>\n\n {/* TODO(burdon): Only show label if surface exists? */}\n {/* TODO(burdon): Remove this section — move the related actions into the object menu. */}\n <Layout.Flex column gap='form'>\n <Field.Root>\n <Field.Label>{t('related-actions.label')}</Field.Label>\n </Field.Root>\n <Surface.Surface type={SpaceSurface.Prompts} data={{ subject, attendableId: subject.id }} limit={1} />\n </Layout.Flex>\n\n {/* Gated on the unfiltered set so hiding every type does not remove the filter itself. */}\n {relatedObjects.length > 0 && (\n <Layout.Flex column gap='form' classNames={['dx-expand', singleColumn ? 'dx-card-max-width' : 'w-full']}>\n <Field.Root>\n <Field.Label>{t('related-objects.label')}</Field.Label>\n </Field.Root>\n {/* `self-start` so the group sizes to its icons rather than stretching this column. */}\n <RelatedTypeFilter classNames='self-start' types={types} onToggle={toggle} />\n {/* The masonry's own gutter would inset these cards relative to the record card above,\n which shares this column — the scroll padding is the article's to own, not theirs. */}\n {/* `centered={false}` on the ROOT, which is column alignment — distinct from\n `Content`'s prop of the same name below (ScrollArea's scrollbar padding). Centred\n columns drift right of the record card above them, which shares this column. */}\n <Masonry.Root Tile={ObjectCard} columns={singleColumn ? 1 : undefined} centered={false}>\n <Masonry.Content padding={false}>\n <Masonry.Viewport items={related} />\n </Masonry.Content>\n </Masonry.Root>\n </Layout.Flex>\n )}\n </ScrollArea.Viewport>\n </ScrollArea.Root>\n </Panel.Body>\n </Panel.Root>\n );\n};\n\n//\n// Hooks\n//\n\n/**\n * Toolbar actions for the record, sourced from its own app-graph node (`disposition: 'toolbar'`).\n *\n * The record view is type-agnostic, so it must not know which actions a given type affords. Any plugin\n * contributes them to the object's node instead — plugin-crm donates enrichment for `Person` /\n * `Organization` — which keeps plugin-space free of a dependency on them and makes each action appear\n * only for the types that declare it.\n */\nconst useMenuActions = (\n attendableId?: string,\n): { actions: ReturnType<typeof useMenuBuilder>; onAction: ActionExecutor } => {\n const { graph } = ToolkitHooks.useAppGraph();\n const runAction = GraphHooks.useActionRunner();\n\n const menuActions = useMenuBuilder(\n (get): ActionGraphProps =>\n attendableId\n ? MenuBuilder.make()\n .subgraph(graphActions(graph, get, attendableId, { filter: isToolbarAction }))\n .build()\n : MenuBuilder.make().build(),\n [graph, attendableId],\n );\n\n const onAction: ActionExecutor = useCallback(\n (action) => {\n void runAction(action, { caller: meta.profile.key });\n },\n [runAction],\n );\n\n return { actions: menuActions, onAction };\n};\n\nRecordArticle.displayName = 'RecordArticle';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmCA,IAAa,iBAAiB,EAAE,MAAM,SAAS,mBAAkD;CAC/F,MAAM,EAAE,MAAM,MAAM,eAAe,KAAK,QAAQ,GAAG;CACnD,MAAM,EAAE,SAAS,aAAa,iBAAe,YAAY;CACzD,UAAU,OAAO;CAGjB,MAAM,KAAK,IAAI,YAAY,OAAO;CAClC,MAAM,WAAW,IAAI,YAAY,OAAO;CACxC,MAAM,SACJ,IAAI,QAAQ,OAAO,MAClB,YAAY,KACT,GAAG,MAAM,SACN,KAAK,CAAA,CACL,OAAO,KAAK,MAAM,CAAA,CAClB,MAAM,MAAM,KAAK,YAAY,CAAC,MAAM,QAAQ,IAC/C,KAAA;CACN,MAAM,OAAO,UAAU,KAAK,YAAY,MAAM,KAAK,OAAO,sBAAsB,KAAA;CAGhF,MAAM,iBAAiB,kBAAkB,IAAI,SAAS;EAAE,YAAY;EAAM,WAAW;CAAK,CAAC;CAC3F,MAAM,EAAE,OAAO,OAAO,SAAS,WAAW,qBAAqB,gBAAgB,IAAI,OAAO,OAAO,CAAC,CAAC,SAAS,CAAC;CAC7G,MAAM,eAAe,QAAQ,WAAW;CAExC,OACE,qBAAC,MAAM,MAAP;EAAkB;EAAlB,UAAA,CACE,oBAAC,MAAM,QAAP,EAAA,UACE,oBAAC,eAAD;GAAe,GAAI;GAAuB;GAAwB;EAAW,CAAA,EACjE,CAAA,GACd,oBAAC,MAAM,MAAP;GAAY,SAAA;GACV,UAAA,oBAAC,WAAW,MAAZ;IAAiB,aAAY;IAC3B,UAAA,qBAAC,WAAW,UAAZ;KAAqB,YAAW;KAAhC,UAAA;MACE,qBAAC,kBAAkB,MAAnB,EAAA,UAAA,CACE,oBAAC,kBAAkB,QAAnB;OAAmC;OAAe;MAAO,CAAA,GACzD,oBAAC,KAAK,MAAN,EAAA,UACE,oBAAC,QAAQ,SAAT;OAAiB,MAAM,WAAW;OAAa,MAAM,EAAE,QAAQ;OAAG,OAAO;MAAI,CAAA,EACpE,CAAA,CACW,EAAA,CAAA;MAIxB,qBAAC,OAAO,MAAR;OAAa,QAAA;OAAO,KAAI;OAAxB,UAAA,CACE,oBAAC,MAAM,MAAP,EAAA,UACE,oBAAC,MAAM,OAAP,EAAA,UAAc,EAAE,uBAAuB,EAAe,CAAA,EAC5C,CAAA,GACZ,oBAAC,QAAQ,SAAT;QAAiB,MAAM,aAAa;QAAS,MAAM;SAAE;SAAS,cAAc,QAAQ;QAAG;QAAG,OAAO;OAAI,CAAA,CAC1F;;MAGZ,eAAe,SAAS,KACvB,qBAAC,OAAO,MAAR;OAAa,QAAA;OAAO,KAAI;OAAO,YAAY,CAAC,aAAa,eAAe,sBAAsB,QAAQ;OAAtG,UAAA;QACE,oBAAC,MAAM,MAAP,EAAA,UACE,oBAAC,MAAM,OAAP,EAAA,UAAc,EAAE,uBAAuB,EAAe,CAAA,EAC5C,CAAA;QAEZ,oBAAC,mBAAD;SAAmB,YAAW;SAAoB;SAAO,UAAU;QAAS,CAAA;QAM5E,oBAAC,QAAQ,MAAT;SAAc,MAAM;SAAY,SAAS,eAAe,IAAI,KAAA;SAAW,UAAU;SAC/E,UAAA,oBAAC,QAAQ,SAAT;UAAiB,SAAS;UACxB,UAAA,oBAAC,QAAQ,UAAT,EAAkB,OAAO,QAAU,CAAA;SACpB,CAAA;QACL,CAAA;OACH;;KAEI;;GACN,CAAA;EACP,CAAA,CACF;;AAEhB;;;;;;;;;AAcA,IAAM,oBACJ,iBAC6E;CAC7E,MAAM,EAAE,UAAU,aAAa,YAAY;CAC3C,MAAM,YAAY,WAAW,gBAAgB;CAmB7C,OAAO;EAAE,SAjBW,gBACjB,QACC,eACI,YAAY,KAAK,CAAA,CACd,SAAS,aAAa,OAAO,KAAK,cAAc,EAAE,QAAQ,gBAAgB,CAAC,CAAC,CAAA,CAC5E,MAAM,IACT,YAAY,KAAK,CAAC,CAAC,MAAM,GAC/B,CAAC,OAAO,YAAY,CAUJ;EAAa,UAPE,aAC9B,WAAW;GACV,UAAe,QAAQ,EAAE,QAAQ,KAAK,QAAQ,IAAI,CAAC;EACrD,GACA,CAAC,SAAS,CAGmB;CAAS;AAC1C;AAEA,cAAc,cAAc"}
@@ -57,7 +57,7 @@ var SpaceHomeDashboard = ({ space, stats = STAT_IDS, onClose }) => {
57
57
  "active-days": activity.length,
58
58
  "plugins": plugins
59
59
  };
60
- if (!space) return null;
60
+ if (!space || counts.length === 0) return null;
61
61
  return /* @__PURE__ */ jsxs(HomeSection.Root, { children: [
62
62
  /* @__PURE__ */ jsx(HomeSection.Header, {
63
63
  title: t("space-home.dashboard.heading"),
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-SpaceHomeDashboard.mjs","names":[],"sources":["../../src/containers/SpaceHomeDashboard/activity.ts","../../src/containers/SpaceHomeDashboard/SpaceHomeDashboard.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { Aggregate, Filter, Query } from '@dxos/echo';\nimport { type ActivityDatum } from '@dxos/react-ui-dashboard';\n\nexport const HOURLY_ACTIVITY_QUERY = Query.select(Filter.changes()).aggregate({\n hour: Aggregate.time('time', 'hour'),\n count: Aggregate.count(),\n});\n\nexport type HourCount = {\n readonly hour: number | null;\n readonly count: number;\n};\n\nexport const toActivity = (rows: readonly HourCount[]): ActivityDatum[] => {\n const days = new Map<number, number>();\n for (const { hour, count } of rows) {\n if (hour === null) {\n continue;\n }\n const date = new Date(hour);\n const day = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();\n days.set(day, (days.get(day) ?? 0) + count);\n }\n return [...days].sort(([a], [b]) => a - b).map(([day, value]) => ({ date: new Date(day), value }));\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { useAtomValue } from '@effect/atom-react/Hooks';\nimport React, { useMemo } from 'react';\n\nimport * as PluginManagerProvider from '@dxos/app-framework/PluginManagerProvider';\nimport * as HomeSection from '@dxos/app-toolkit/HomeSection';\nimport { Collection, Type } from '@dxos/echo';\nimport { useQuery } from '@dxos/echo-react';\nimport { type Space, useMembers } from '@dxos/react-client/echo';\nimport { Dashboard } from '@dxos/react-ui-dashboard';\nimport * as Hooks from '@dxos/react-ui/Hooks';\n\nimport { SPACE_STATS_QUERY, countObjects, countTypenames } from '#dashboard';\nimport { meta } from '#meta';\n\nimport { HOURLY_ACTIVITY_QUERY, toActivity } from './activity.ts';\n\nconst STAT_IDS = ['objects', 'types', 'collections', 'members', 'active-days', 'plugins'] as const;\n\nexport type SpaceStatId = (typeof STAT_IDS)[number];\n\ntype SpaceHomeDashboardProps = {\n space?: Space;\n /** Stat cards to render, in order; defaults to all. */\n stats?: readonly SpaceStatId[];\n onClose?: () => void;\n};\n\nconst COLLECTION_TYPENAME = Type.getTypename(Collection.Collection);\n\nexport const SpaceHomeDashboard = ({ space, stats = STAT_IDS, onClose }: SpaceHomeDashboardProps) => {\n const { t } = Hooks.useTranslation(meta.profile.key);\n const members = useMembers(space?.key);\n\n const manager = PluginManagerProvider.usePluginManager();\n const core = useAtomValue(manager.core);\n const enabled = useAtomValue(manager.enabled);\n const plugins = useMemo(() => enabled.filter((id) => !core.includes(id)).length, [core, enabled]);\n\n const counts = useQuery(space?.db, SPACE_STATS_QUERY);\n const hours = useQuery(space?.db, HOURLY_ACTIVITY_QUERY);\n const activity = useMemo(() => toActivity(hours), [hours]);\n\n const values: Record<SpaceStatId, number> = {\n 'objects': countObjects(counts),\n 'types': countTypenames(counts),\n 'collections': countObjects(counts, COLLECTION_TYPENAME),\n 'members': members.length,\n 'active-days': activity.length,\n 'plugins': plugins,\n };\n\n if (!space) {\n return null;\n }\n\n return (\n <HomeSection.Root>\n <HomeSection.Header title={t('space-home.dashboard.heading')} onClose={onClose} />\n <Dashboard.Stats>\n {stats.map((id) => (\n <Dashboard.Stat key={id}>\n <Dashboard.StatLabel>{t(`space-home.dashboard.${id}.label`)}</Dashboard.StatLabel>\n <Dashboard.StatValue value={values[id]} data-testid={`space-home-dashboard.${id}`} />\n </Dashboard.Stat>\n ))}\n </Dashboard.Stats>\n {/* Smaller cells so a full year fits the 40rem home column without horizontal scroll. */}\n <Dashboard.Activity classNames='[--dx-dashboard-cell:0.75rem]' data={activity} endDate={new Date()} />\n </HomeSection.Root>\n );\n};\n\nSpaceHomeDashboard.displayName = 'SpaceHomeDashboard';\n"],"mappings":";;;;;;;;;;;;;AAOA,IAAa,wBAAwB,MAAM,OAAO,OAAO,QAAQ,CAAC,CAAC,CAAC,UAAU;CAC5E,MAAM,UAAU,KAAK,QAAQ,MAAM;CACnC,OAAO,UAAU,MAAM;AACzB,CAAC;AAOD,IAAa,cAAc,SAAgD;CACzE,MAAM,uBAAO,IAAI,IAAoB;CACrC,KAAK,MAAM,EAAE,MAAM,WAAW,MAAM;EAClC,IAAI,SAAS,MACX;EAEF,MAAM,OAAO,IAAI,KAAK,IAAI;EAC1B,MAAM,MAAM,IAAI,KAAK,KAAK,YAAY,GAAG,KAAK,SAAS,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ;EAClF,KAAK,IAAI,MAAM,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK;CAC5C;CACA,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,YAAY;EAAE,MAAM,IAAI,KAAK,GAAG;EAAG;CAAM,EAAE;AACnG;;;ACRA,IAAM,WAAW;CAAC;CAAW;CAAS;CAAe;CAAW;CAAe;AAAS;AAWxF,IAAM,sBAAsB,KAAK,YAAY,WAAW,UAAU;AAElE,IAAa,sBAAsB,EAAE,OAAO,QAAQ,UAAU,cAAuC;CACnG,MAAM,EAAE,MAAM,MAAM,eAAe,KAAK,QAAQ,GAAG;CACnD,MAAM,UAAU,WAAW,OAAO,GAAG;CAErC,MAAM,UAAU,sBAAsB,iBAAiB;CACvD,MAAM,OAAO,aAAa,QAAQ,IAAI;CACtC,MAAM,UAAU,aAAa,QAAQ,OAAO;CAC5C,MAAM,UAAU,cAAc,QAAQ,QAAQ,OAAO,CAAC,KAAK,SAAS,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,OAAO,CAAC;CAEhG,MAAM,SAAS,SAAS,OAAO,IAAI,iBAAiB;CACpD,MAAM,QAAQ,SAAS,OAAO,IAAI,qBAAqB;CACvD,MAAM,WAAW,cAAc,WAAW,KAAK,GAAG,CAAC,KAAK,CAAC;CAEzD,MAAM,SAAsC;EAC1C,WAAW,aAAa,MAAM;EAC9B,SAAS,eAAe,MAAM;EAC9B,eAAe,aAAa,QAAQ,mBAAmB;EACvD,WAAW,QAAQ;EACnB,eAAe,SAAS;EACxB,WAAW;CACb;CAEA,IAAI,CAAC,OACH,OAAO;CAGT,OACE,qBAAC,YAAY,MAAb,EAAA,UAAA;EACE,oBAAC,YAAY,QAAb;GAAoB,OAAO,EAAE,8BAA8B;GAAY;EAAU,CAAA;EACjF,oBAAC,UAAU,OAAX,EAAA,UACG,MAAM,KAAK,OACV,qBAAC,UAAU,MAAX,EAAA,UAAA,CACE,oBAAC,UAAU,WAAX,EAAA,UAAsB,EAAE,wBAAwB,GAAG,OAAO,EAAuB,CAAA,GACjF,oBAAC,UAAU,WAAX;GAAqB,OAAO,OAAO;GAAK,eAAa,wBAAwB;EAAO,CAAA,CACtE,EAAA,GAHK,EAGL,CACjB,EACc,CAAA;EAEjB,oBAAC,UAAU,UAAX;GAAoB,YAAW;GAAgC,MAAM;GAAU,yBAAS,IAAI,KAAK;EAAI,CAAA;CACrF,EAAA,CAAA;AAEtB;AAEA,mBAAmB,cAAc"}
1
+ {"version":3,"file":"chunk-SpaceHomeDashboard.mjs","names":[],"sources":["../../src/containers/SpaceHomeDashboard/activity.ts","../../src/containers/SpaceHomeDashboard/SpaceHomeDashboard.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { Aggregate, Filter, Query } from '@dxos/echo';\nimport { type ActivityDatum } from '@dxos/react-ui-dashboard';\n\nexport const HOURLY_ACTIVITY_QUERY = Query.select(Filter.changes()).aggregate({\n hour: Aggregate.time('time', 'hour'),\n count: Aggregate.count(),\n});\n\nexport type HourCount = {\n readonly hour: number | null;\n readonly count: number;\n};\n\nexport const toActivity = (rows: readonly HourCount[]): ActivityDatum[] => {\n const days = new Map<number, number>();\n for (const { hour, count } of rows) {\n if (hour === null) {\n continue;\n }\n const date = new Date(hour);\n const day = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();\n days.set(day, (days.get(day) ?? 0) + count);\n }\n return [...days].sort(([a], [b]) => a - b).map(([day, value]) => ({ date: new Date(day), value }));\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { useAtomValue } from '@effect/atom-react/Hooks';\nimport React, { useMemo } from 'react';\n\nimport * as PluginManagerProvider from '@dxos/app-framework/PluginManagerProvider';\nimport * as HomeSection from '@dxos/app-toolkit/HomeSection';\nimport { Collection, Type } from '@dxos/echo';\nimport { useQuery } from '@dxos/echo-react';\nimport { type Space, useMembers } from '@dxos/react-client/echo';\nimport { Dashboard } from '@dxos/react-ui-dashboard';\nimport * as Hooks from '@dxos/react-ui/Hooks';\n\nimport { SPACE_STATS_QUERY, countObjects, countTypenames } from '#dashboard';\nimport { meta } from '#meta';\n\nimport { HOURLY_ACTIVITY_QUERY, toActivity } from './activity.ts';\n\nconst STAT_IDS = ['objects', 'types', 'collections', 'members', 'active-days', 'plugins'] as const;\n\nexport type SpaceStatId = (typeof STAT_IDS)[number];\n\ntype SpaceHomeDashboardProps = {\n space?: Space;\n /** Stat cards to render, in order; defaults to all. */\n stats?: readonly SpaceStatId[];\n onClose?: () => void;\n};\n\nconst COLLECTION_TYPENAME = Type.getTypename(Collection.Collection);\n\nexport const SpaceHomeDashboard = ({ space, stats = STAT_IDS, onClose }: SpaceHomeDashboardProps) => {\n const { t } = Hooks.useTranslation(meta.profile.key);\n const members = useMembers(space?.key);\n\n const manager = PluginManagerProvider.usePluginManager();\n const core = useAtomValue(manager.core);\n const enabled = useAtomValue(manager.enabled);\n const plugins = useMemo(() => enabled.filter((id) => !core.includes(id)).length, [core, enabled]);\n\n const counts = useQuery(space?.db, SPACE_STATS_QUERY);\n const hours = useQuery(space?.db, HOURLY_ACTIVITY_QUERY);\n const activity = useMemo(() => toActivity(hours), [hours]);\n\n const values: Record<SpaceStatId, number> = {\n 'objects': countObjects(counts),\n 'types': countTypenames(counts),\n 'collections': countObjects(counts, COLLECTION_TYPENAME),\n 'members': members.length,\n 'active-days': activity.length,\n 'plugins': plugins,\n };\n\n if (!space || counts.length === 0) {\n return null;\n }\n\n return (\n <HomeSection.Root>\n <HomeSection.Header title={t('space-home.dashboard.heading')} onClose={onClose} />\n <Dashboard.Stats>\n {stats.map((id) => (\n <Dashboard.Stat key={id}>\n <Dashboard.StatLabel>{t(`space-home.dashboard.${id}.label`)}</Dashboard.StatLabel>\n <Dashboard.StatValue value={values[id]} data-testid={`space-home-dashboard.${id}`} />\n </Dashboard.Stat>\n ))}\n </Dashboard.Stats>\n {/* Smaller cells so a full year fits the 40rem home column without horizontal scroll. */}\n <Dashboard.Activity classNames='[--dx-dashboard-cell:0.75rem]' data={activity} endDate={new Date()} />\n </HomeSection.Root>\n );\n};\n\nSpaceHomeDashboard.displayName = 'SpaceHomeDashboard';\n"],"mappings":";;;;;;;;;;;;;AAOA,IAAa,wBAAwB,MAAM,OAAO,OAAO,QAAQ,CAAC,CAAC,CAAC,UAAU;CAC5E,MAAM,UAAU,KAAK,QAAQ,MAAM;CACnC,OAAO,UAAU,MAAM;AACzB,CAAC;AAOD,IAAa,cAAc,SAAgD;CACzE,MAAM,uBAAO,IAAI,IAAoB;CACrC,KAAK,MAAM,EAAE,MAAM,WAAW,MAAM;EAClC,IAAI,SAAS,MACX;EAEF,MAAM,OAAO,IAAI,KAAK,IAAI;EAC1B,MAAM,MAAM,IAAI,KAAK,KAAK,YAAY,GAAG,KAAK,SAAS,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ;EAClF,KAAK,IAAI,MAAM,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK;CAC5C;CACA,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,YAAY;EAAE,MAAM,IAAI,KAAK,GAAG;EAAG;CAAM,EAAE;AACnG;;;ACRA,IAAM,WAAW;CAAC;CAAW;CAAS;CAAe;CAAW;CAAe;AAAS;AAWxF,IAAM,sBAAsB,KAAK,YAAY,WAAW,UAAU;AAElE,IAAa,sBAAsB,EAAE,OAAO,QAAQ,UAAU,cAAuC;CACnG,MAAM,EAAE,MAAM,MAAM,eAAe,KAAK,QAAQ,GAAG;CACnD,MAAM,UAAU,WAAW,OAAO,GAAG;CAErC,MAAM,UAAU,sBAAsB,iBAAiB;CACvD,MAAM,OAAO,aAAa,QAAQ,IAAI;CACtC,MAAM,UAAU,aAAa,QAAQ,OAAO;CAC5C,MAAM,UAAU,cAAc,QAAQ,QAAQ,OAAO,CAAC,KAAK,SAAS,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,OAAO,CAAC;CAEhG,MAAM,SAAS,SAAS,OAAO,IAAI,iBAAiB;CACpD,MAAM,QAAQ,SAAS,OAAO,IAAI,qBAAqB;CACvD,MAAM,WAAW,cAAc,WAAW,KAAK,GAAG,CAAC,KAAK,CAAC;CAEzD,MAAM,SAAsC;EAC1C,WAAW,aAAa,MAAM;EAC9B,SAAS,eAAe,MAAM;EAC9B,eAAe,aAAa,QAAQ,mBAAmB;EACvD,WAAW,QAAQ;EACnB,eAAe,SAAS;EACxB,WAAW;CACb;CAEA,IAAI,CAAC,SAAS,OAAO,WAAW,GAC9B,OAAO;CAGT,OACE,qBAAC,YAAY,MAAb,EAAA,UAAA;EACE,oBAAC,YAAY,QAAb;GAAoB,OAAO,EAAE,8BAA8B;GAAY;EAAU,CAAA;EACjF,oBAAC,UAAU,OAAX,EAAA,UACG,MAAM,KAAK,OACV,qBAAC,UAAU,MAAX,EAAA,UAAA,CACE,oBAAC,UAAU,WAAX,EAAA,UAAsB,EAAE,wBAAwB,GAAG,OAAO,EAAuB,CAAA,GACjF,oBAAC,UAAU,WAAX;GAAqB,OAAO,OAAO;GAAK,eAAa,wBAAwB;EAAO,CAAA,CACtE,EAAA,GAHK,EAGL,CACjB,EACc,CAAA;EAEjB,oBAAC,UAAU,UAAX;GAAoB,YAAW;GAAgC,MAAM;GAAU,yBAAS,IAAI,KAAK;EAAI,CAAA;CACrF,EAAA,CAAA;AAEtB;AAEA,mBAAmB,cAAc"}
@@ -1,5 +1,5 @@
1
1
  import * as AppCapabilities from "@dxos/app-toolkit/AppCapabilities";
2
- import { Collection, Filter, Obj, Order, Query, Type } from "@dxos/echo";
2
+ import { Aggregate, Collection, Filter, Obj, Order, Query, Type } from "@dxos/echo";
3
3
  import { meta } from "#meta";
4
4
  import { useCallback, useMemo } from "react";
5
5
  import { Masonry } from "@dxos/react-ui-masonry";
@@ -15,42 +15,64 @@ import { getStyles } from "@dxos/ui-theme";
15
15
  import * as Hooks from "@dxos/app-framework/Hooks";
16
16
  import * as TypeOptions from "@dxos/app-toolkit/TypeOptions";
17
17
  import * as LayoutOperation from "@dxos/app-toolkit/LayoutOperation";
18
+ import * as Array from "effect/Array";
19
+ import * as Status from "@dxos/react-ui/Status";
18
20
  import * as HomeSection from "@dxos/app-toolkit/HomeSection";
19
21
  //#region src/containers/SpaceHomeRecent/SpaceHomeRecent.tsx
20
22
  /** Number of recently-modified objects to surface as cards. */
21
23
  var RECENT_LIMIT = 10;
22
- /**
23
- * Recent-objects region for the Home article. Queries the most-recently-modified objects of
24
- * registered, non-hidden, non-relation, non-collection types and renders them as a Masonry of
25
- * tiles. Renders nothing (no heading) when the space has no recent objects — the starter-prompt
26
- * contributor (plugin-assistant) fills the empty state instead.
27
- */
28
24
  var SpaceHomeRecent = ({ space, onClose }) => {
29
25
  const { t } = UiHooks.useTranslation(meta.profile.key);
30
- const schemas = Hooks.useCapabilities(AppCapabilities.Schema);
31
- const filter = useMemo(() => {
32
- const collectionTypename = Type.getTypename(Collection.Collection);
33
- const types = schemas.flat().filter(Type.isType).filter((type) => TypeOptions.isUserType(type)).filter((type) => Type.getTypename(type) !== collectionTypename);
34
- return types.length > 0 ? Filter.or(...types.map((type) => Filter.type(type))) : void 0;
35
- }, [schemas]);
36
- const query = useMemo(() => Query.select(filter ?? Filter.everything()).orderBy(Order.updated("desc")).limit(RECENT_LIMIT), [filter]);
37
- const recent = useQuery(filter && space ? space.db : void 0, query);
38
- if (recent.length === 0) return null;
26
+ const { recent, placeholderCount } = useRecentObjects(space);
27
+ const items = useMemo(() => placeholderCount > 0 ? Array.makeBy(placeholderCount, (index) => new Placeholder(`placeholder-${index}`)) : recent, [placeholderCount, recent]);
28
+ if (recent.length === 0 && placeholderCount === 0) return null;
39
29
  return /* @__PURE__ */ jsxs(HomeSection.Root, { children: [/* @__PURE__ */ jsx(HomeSection.Header, {
40
30
  title: t("space-home.recent.heading"),
41
31
  onClose
42
32
  }), /* @__PURE__ */ jsx(Masonry.Root, {
43
- Tile: RecentObjectTile,
33
+ Tile: RecentTile,
44
34
  children: /* @__PURE__ */ jsx(Masonry.Content, {
45
35
  padding: false,
46
36
  scrollbars: false,
47
37
  children: /* @__PURE__ */ jsx(Masonry.Viewport, {
48
- items: recent,
49
- getId: (object) => object.id
38
+ items,
39
+ getId: (item) => item.id,
40
+ cacheKey: space && `${space.id}/recent`
50
41
  })
51
42
  })
52
43
  })] });
53
44
  };
45
+ var useRecentObjects = (space) => {
46
+ const schemas = Hooks.useCapabilities(AppCapabilities.Schema);
47
+ const filter = useMemo(() => recentObjectsFilter(schemas.flat()), [schemas]);
48
+ const query = useMemo(() => Query.select(filter ?? Filter.everything()).orderBy(Order.updated("desc")).limit(RECENT_LIMIT), [filter]);
49
+ const countQuery = useMemo(() => Query.select(filter ?? Filter.everything()).aggregate({ count: Aggregate.count() }), [filter]);
50
+ const db = filter && space ? space.db : void 0;
51
+ const recent = useQuery(db, query);
52
+ const [total] = useQuery(db, countQuery);
53
+ return {
54
+ recent,
55
+ placeholderCount: recent.length === 0 ? Math.min(total?.count ?? 0, RECENT_LIMIT) : 0
56
+ };
57
+ };
58
+ var recentObjectsFilter = (schemas) => {
59
+ const collectionTypename = Type.getTypename(Collection.Collection);
60
+ const types = Array.dedupeWith(schemas.filter(Type.isType).filter((type) => TypeOptions.isUserType(type)).filter((type) => Type.getTypename(type) !== collectionTypename), (a, b) => Type.getURI(a) === Type.getURI(b));
61
+ return types.length > 0 ? Filter.or(...types.map((type) => Filter.type(type))) : void 0;
62
+ };
63
+ var Placeholder = class {
64
+ id;
65
+ constructor(id) {
66
+ this.id = id;
67
+ }
68
+ };
69
+ var RecentTile = ({ data, index }) => data instanceof Placeholder ? /* @__PURE__ */ jsx(PlaceholderTile, {}) : /* @__PURE__ */ jsx(RecentObjectTile, {
70
+ data,
71
+ index
72
+ });
73
+ RecentTile.displayName = "RecentTile";
74
+ var PlaceholderTile = () => /* @__PURE__ */ jsx(Card.Root, { children: /* @__PURE__ */ jsxs(Card.Header, { children: [/* @__PURE__ */ jsx(Layout.Block, { children: /* @__PURE__ */ jsx(Status.Skeleton, { variant: "circle" }) }), /* @__PURE__ */ jsx(Card.Title, { children: /* @__PURE__ */ jsx(Status.Skeleton, { variant: "text" }) })] }) });
75
+ PlaceholderTile.displayName = "PlaceholderTile";
54
76
  var RecentObjectTile = ({ data }) => {
55
77
  const { invokePromise } = Hooks.useOperationInvoker();
56
78
  const { t } = UiHooks.useTranslation(meta.profile.key);
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-SpaceHomeRecent.mjs","names":[],"sources":["../../src/containers/SpaceHomeRecent/SpaceHomeRecent.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport React, { useCallback, useMemo } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport * as AppCapabilities from '@dxos/app-toolkit/AppCapabilities';\nimport * as GraphPath from '@dxos/app-toolkit/GraphPath';\nimport * as HomeSection from '@dxos/app-toolkit/HomeSection';\nimport * as LayoutOperation from '@dxos/app-toolkit/LayoutOperation';\nimport * as TypeOptions from '@dxos/app-toolkit/TypeOptions';\nimport { Collection, Filter, Obj, Order, Query, Type } from '@dxos/echo';\nimport { useQuery } from '@dxos/echo-react';\nimport { type Space } from '@dxos/react-client/echo';\nimport { Masonry } from '@dxos/react-ui-masonry';\nimport * as Card from '@dxos/react-ui/Card';\nimport * as UiHooks from '@dxos/react-ui/Hooks';\nimport * as Icon from '@dxos/react-ui/Icon';\nimport * as Layout from '@dxos/react-ui/Layout';\nimport * as Theme from '@dxos/react-ui/Theme';\nimport { getStyles } from '@dxos/ui-theme';\n\nimport { meta } from '#meta';\n\n/** Number of recently-modified objects to surface as cards. */\nconst RECENT_LIMIT = 10;\n\ntype SpaceScopedProps = {\n space?: Space;\n onClose?: () => void;\n};\n\n/**\n * Recent-objects region for the Home article. Queries the most-recently-modified objects of\n * registered, non-hidden, non-relation, non-collection types and renders them as a Masonry of\n * tiles. Renders nothing (no heading) when the space has no recent objects — the starter-prompt\n * contributor (plugin-assistant) fills the empty state instead.\n */\nexport const SpaceHomeRecent = ({ space, onClose }: SpaceScopedProps) => {\n const { t } = UiHooks.useTranslation(meta.profile.key);\n\n const schemas = Hooks.useCapabilities(AppCapabilities.Schema);\n const filter = useMemo(() => {\n const collectionTypename = Type.getTypename(Collection.Collection);\n const types = schemas\n .flat()\n .filter(Type.isType)\n .filter((type) => TypeOptions.isUserType(type))\n .filter((type) => Type.getTypename(type) !== collectionTypename);\n return types.length > 0 ? Filter.or(...types.map((type) => Filter.type(type))) : undefined;\n }, [schemas]);\n\n const query = useMemo(\n () =>\n Query.select(filter ?? Filter.everything())\n .orderBy(Order.updated('desc'))\n .limit(RECENT_LIMIT),\n [filter],\n );\n\n const recent = useQuery(filter && space ? space.db : undefined, query);\n if (recent.length === 0) {\n return null;\n }\n\n return (\n <HomeSection.Root>\n <HomeSection.Header title={t('space-home.recent.heading')} onClose={onClose} />\n <Masonry.Root Tile={RecentObjectTile}>\n <Masonry.Content padding={false} scrollbars={false}>\n <Masonry.Viewport items={recent} getId={(object) => object.id} />\n </Masonry.Content>\n </Masonry.Root>\n </HomeSection.Root>\n );\n};\n\nconst RecentObjectTile = ({ data }: { data: Obj.Unknown; index: number }) => {\n const { invokePromise } = Hooks.useOperationInvoker();\n const { t } = UiHooks.useTranslation(meta.profile.key);\n const typename = Obj.getTypename(data);\n const label = Theme.toLocalizedString(\n Obj.getLabel(data) ?? (typename ? ['object-name.placeholder', { ns: typename, defaultValue: 'New item' }] : ''),\n t,\n );\n const iconAnnotation = Obj.getIcon(data);\n const icon = iconAnnotation?.icon ?? 'ph--circle-dashed--regular';\n const iconStyles = iconAnnotation?.hue ? getStyles(iconAnnotation.hue) : undefined;\n\n const handleClick = useCallback(() => {\n void invokePromise(LayoutOperation.Open, { subject: [GraphPath.getObjectPathFromObject(data)] });\n }, [invokePromise, data]);\n\n return (\n <Card.Root role='button' classNames='cursor-pointer' onClick={handleClick}>\n <Card.Header>\n <Layout.Block>\n <Icon.Icon icon={icon} classNames={iconStyles?.text} />\n </Layout.Block>\n <Card.Title>{label}</Card.Title>\n </Card.Header>\n </Card.Root>\n );\n};\n\nRecentObjectTile.displayName = 'RecentObjectTile';\n\nSpaceHomeRecent.displayName = 'SpaceHomeRecent';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0BA,IAAM,eAAe;;;;;;;AAarB,IAAa,mBAAmB,EAAE,OAAO,cAAgC;CACvE,MAAM,EAAE,MAAM,QAAQ,eAAe,KAAK,QAAQ,GAAG;CAErD,MAAM,UAAU,MAAM,gBAAgB,gBAAgB,MAAM;CAC5D,MAAM,SAAS,cAAc;EAC3B,MAAM,qBAAqB,KAAK,YAAY,WAAW,UAAU;EACjE,MAAM,QAAQ,QACX,KAAK,CAAA,CACL,OAAO,KAAK,MAAM,CAAA,CAClB,QAAQ,SAAS,YAAY,WAAW,IAAI,CAAC,CAAA,CAC7C,QAAQ,SAAS,KAAK,YAAY,IAAI,MAAM,kBAAkB;EACjE,OAAO,MAAM,SAAS,IAAI,OAAO,GAAG,GAAG,MAAM,KAAK,SAAS,OAAO,KAAK,IAAI,CAAC,CAAC,IAAI,KAAA;CACnF,GAAG,CAAC,OAAO,CAAC;CAEZ,MAAM,QAAQ,cAEV,MAAM,OAAO,UAAU,OAAO,WAAW,CAAC,CAAA,CACvC,QAAQ,MAAM,QAAQ,MAAM,CAAC,CAAA,CAC7B,MAAM,YAAY,GACvB,CAAC,MAAM,CACT;CAEA,MAAM,SAAS,SAAS,UAAU,QAAQ,MAAM,KAAK,KAAA,GAAW,KAAK;CACrE,IAAI,OAAO,WAAW,GACpB,OAAO;CAGT,OACE,qBAAC,YAAY,MAAb,EAAA,UAAA,CACE,oBAAC,YAAY,QAAb;EAAoB,OAAO,EAAE,2BAA2B;EAAY;CAAU,CAAA,GAC9E,oBAAC,QAAQ,MAAT;EAAc,MAAM;EAClB,UAAA,oBAAC,QAAQ,SAAT;GAAiB,SAAS;GAAO,YAAY;GAC3C,UAAA,oBAAC,QAAQ,UAAT;IAAkB,OAAO;IAAQ,QAAQ,WAAW,OAAO;GAAK,CAAA;EACjD,CAAA;CACL,CAAA,CACE,EAAA,CAAA;AAEtB;AAEA,IAAM,oBAAoB,EAAE,WAAiD;CAC3E,MAAM,EAAE,kBAAkB,MAAM,oBAAoB;CACpD,MAAM,EAAE,MAAM,QAAQ,eAAe,KAAK,QAAQ,GAAG;CACrD,MAAM,WAAW,IAAI,YAAY,IAAI;CACrC,MAAM,QAAQ,MAAM,kBAClB,IAAI,SAAS,IAAI,MAAM,WAAW,CAAC,2BAA2B;EAAE,IAAI;EAAU,cAAc;CAAW,CAAC,IAAI,KAC5G,CACF;CACA,MAAM,iBAAiB,IAAI,QAAQ,IAAI;CACvC,MAAM,OAAO,gBAAgB,QAAQ;CACrC,MAAM,aAAa,gBAAgB,MAAM,UAAU,eAAe,GAAG,IAAI,KAAA;CAEzE,MAAM,cAAc,kBAAkB;EACpC,cAAmB,gBAAgB,MAAM,EAAE,SAAS,CAAC,UAAU,wBAAwB,IAAI,CAAC,EAAE,CAAC;CACjG,GAAG,CAAC,eAAe,IAAI,CAAC;CAExB,OACE,oBAAC,KAAK,MAAN;EAAW,MAAK;EAAS,YAAW;EAAiB,SAAS;EAC5D,UAAA,qBAAC,KAAK,QAAN,EAAA,UAAA,CACE,oBAAC,OAAO,OAAR,EAAA,UACE,oBAAC,KAAK,MAAN;GAAiB;GAAM,YAAY,YAAY;EAAO,CAAA,EAC1C,CAAA,GACd,oBAAC,KAAK,OAAN,EAAA,UAAa,MAAkB,CAAA,CACpB,EAAA,CAAA;CACJ,CAAA;AAEf;AAEA,iBAAiB,cAAc;AAE/B,gBAAgB,cAAc"}
1
+ {"version":3,"file":"chunk-SpaceHomeRecent.mjs","names":[],"sources":["../../src/containers/SpaceHomeRecent/SpaceHomeRecent.tsx"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Array from 'effect/Array';\nimport React, { useCallback, useMemo } from 'react';\n\nimport * as Hooks from '@dxos/app-framework/Hooks';\nimport * as AppCapabilities from '@dxos/app-toolkit/AppCapabilities';\nimport * as GraphPath from '@dxos/app-toolkit/GraphPath';\nimport * as HomeSection from '@dxos/app-toolkit/HomeSection';\nimport * as LayoutOperation from '@dxos/app-toolkit/LayoutOperation';\nimport * as TypeOptions from '@dxos/app-toolkit/TypeOptions';\nimport { Aggregate, Collection, Filter, Obj, Order, Query, Type } from '@dxos/echo';\nimport { useQuery } from '@dxos/echo-react';\nimport { type Space } from '@dxos/react-client/echo';\nimport { Masonry } from '@dxos/react-ui-masonry';\nimport * as Card from '@dxos/react-ui/Card';\nimport * as UiHooks from '@dxos/react-ui/Hooks';\nimport * as Icon from '@dxos/react-ui/Icon';\nimport * as Layout from '@dxos/react-ui/Layout';\nimport * as Status from '@dxos/react-ui/Status';\nimport * as Theme from '@dxos/react-ui/Theme';\nimport { getStyles } from '@dxos/ui-theme';\n\nimport { meta } from '#meta';\n\n/** Number of recently-modified objects to surface as cards. */\nconst RECENT_LIMIT = 10;\n\ntype SpaceScopedProps = {\n space?: Space;\n onClose?: () => void;\n};\n\nexport const SpaceHomeRecent = ({ space, onClose }: SpaceScopedProps) => {\n const { t } = UiHooks.useTranslation(meta.profile.key);\n\n const { recent, placeholderCount } = useRecentObjects(space);\n const items = useMemo<RecentItem[]>(\n () =>\n placeholderCount > 0\n ? Array.makeBy(placeholderCount, (index) => new Placeholder(`placeholder-${index}`))\n : recent,\n [placeholderCount, recent],\n );\n if (recent.length === 0 && placeholderCount === 0) {\n return null;\n }\n\n return (\n <HomeSection.Root>\n <HomeSection.Header title={t('space-home.recent.heading')} onClose={onClose} />\n <Masonry.Root Tile={RecentTile}>\n <Masonry.Content padding={false} scrollbars={false}>\n <Masonry.Viewport items={items} getId={(item) => item.id} cacheKey={space && `${space.id}/recent`} />\n </Masonry.Content>\n </Masonry.Root>\n </HomeSection.Root>\n );\n};\n\nconst useRecentObjects = (space?: Space): { recent: Obj.Unknown[]; placeholderCount: number } => {\n const schemas = Hooks.useCapabilities(AppCapabilities.Schema);\n const filter = useMemo(() => recentObjectsFilter(schemas.flat()), [schemas]);\n const query = useMemo(\n () =>\n Query.select(filter ?? Filter.everything())\n .orderBy(Order.updated('desc'))\n .limit(RECENT_LIMIT),\n [filter],\n );\n const countQuery = useMemo(\n () => Query.select(filter ?? Filter.everything()).aggregate({ count: Aggregate.count() }),\n [filter],\n );\n\n const db = filter && space ? space.db : undefined;\n const recent = useQuery(db, query);\n const [total] = useQuery(db, countQuery);\n return { recent, placeholderCount: recent.length === 0 ? Math.min(total?.count ?? 0, RECENT_LIMIT) : 0 };\n};\n\nconst recentObjectsFilter = (schemas: readonly unknown[]) => {\n const collectionTypename = Type.getTypename(Collection.Collection);\n const types = Array.dedupeWith(\n schemas\n .filter(Type.isType)\n .filter((type) => TypeOptions.isUserType(type))\n .filter((type) => Type.getTypename(type) !== collectionTypename),\n (a, b) => Type.getURI(a) === Type.getURI(b),\n );\n return types.length > 0 ? Filter.or(...types.map((type) => Filter.type(type))) : undefined;\n};\n\nclass Placeholder {\n constructor(readonly id: string) {}\n}\n\ntype RecentItem = Obj.Unknown | Placeholder;\n\nconst RecentTile = ({ data, index }: { data: RecentItem; index: number }) =>\n data instanceof Placeholder ? <PlaceholderTile /> : <RecentObjectTile data={data} index={index} />;\n\nRecentTile.displayName = 'RecentTile';\n\nconst PlaceholderTile = () => (\n <Card.Root>\n <Card.Header>\n <Layout.Block>\n <Status.Skeleton variant='circle' />\n </Layout.Block>\n <Card.Title>\n <Status.Skeleton variant='text' />\n </Card.Title>\n </Card.Header>\n </Card.Root>\n);\n\nPlaceholderTile.displayName = 'PlaceholderTile';\n\nconst RecentObjectTile = ({ data }: { data: Obj.Unknown; index: number }) => {\n const { invokePromise } = Hooks.useOperationInvoker();\n const { t } = UiHooks.useTranslation(meta.profile.key);\n const typename = Obj.getTypename(data);\n const label = Theme.toLocalizedString(\n Obj.getLabel(data) ?? (typename ? ['object-name.placeholder', { ns: typename, defaultValue: 'New item' }] : ''),\n t,\n );\n const iconAnnotation = Obj.getIcon(data);\n const icon = iconAnnotation?.icon ?? 'ph--circle-dashed--regular';\n const iconStyles = iconAnnotation?.hue ? getStyles(iconAnnotation.hue) : undefined;\n\n const handleClick = useCallback(() => {\n void invokePromise(LayoutOperation.Open, { subject: [GraphPath.getObjectPathFromObject(data)] });\n }, [invokePromise, data]);\n\n return (\n <Card.Root role='button' classNames='cursor-pointer' onClick={handleClick}>\n <Card.Header>\n <Layout.Block>\n <Icon.Icon icon={icon} classNames={iconStyles?.text} />\n </Layout.Block>\n <Card.Title>{label}</Card.Title>\n </Card.Header>\n </Card.Root>\n );\n};\n\nRecentObjectTile.displayName = 'RecentObjectTile';\n\nSpaceHomeRecent.displayName = 'SpaceHomeRecent';\n"],"mappings":"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"}
@@ -129,7 +129,7 @@ var EdgeConnectionPopover = ({ status }) => {
129
129
  children: [
130
130
  /* @__PURE__ */ jsx(Icon.Icon, {
131
131
  icon: "ph--arrow-up--regular",
132
- classNames: "text-green-500"
132
+ classNames: "text-green-text"
133
133
  }),
134
134
  /* @__PURE__ */ jsx("span", {
135
135
  className: "text-fg-muted",
@@ -154,7 +154,7 @@ var EdgeConnectionPopover = ({ status }) => {
154
154
  children: [
155
155
  /* @__PURE__ */ jsx(Icon.Icon, {
156
156
  icon: "ph--arrow-down--regular",
157
- classNames: "text-orange-500"
157
+ classNames: "text-orange-text"
158
158
  }),
159
159
  /* @__PURE__ */ jsx("span", {
160
160
  className: "text-fg-muted",