@dxos/plugin-kanban 0.8.4-main.c4373fc → 0.8.4-main.c85a9c8dae

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 (225) hide show
  1. package/dist/lib/browser/KanbanContainer-BCXSJ6KS.mjs +304 -0
  2. package/dist/lib/browser/KanbanContainer-BCXSJ6KS.mjs.map +7 -0
  3. package/dist/lib/browser/KanbanViewEditor-IH5CJ6BW.mjs +66 -0
  4. package/dist/lib/browser/KanbanViewEditor-IH5CJ6BW.mjs.map +7 -0
  5. package/dist/lib/browser/blueprint-definition-HFEKGFJK.mjs +17 -0
  6. package/dist/lib/browser/blueprint-definition-HFEKGFJK.mjs.map +7 -0
  7. package/dist/lib/browser/blueprints/index.mjs +8 -0
  8. package/dist/lib/browser/blueprints/index.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
  10. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-QSWCFMEB.mjs +385 -0
  12. package/dist/lib/browser/chunk-QSWCFMEB.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-RNFIFE2P.mjs +213 -0
  14. package/dist/lib/browser/chunk-RNFIFE2P.mjs.map +7 -0
  15. package/dist/lib/browser/chunk-TLYZSC7O.mjs +29 -0
  16. package/dist/lib/browser/chunk-TLYZSC7O.mjs.map +7 -0
  17. package/dist/lib/browser/index.mjs +65 -68
  18. package/dist/lib/browser/index.mjs.map +4 -4
  19. package/dist/lib/browser/meta.json +1 -1
  20. package/dist/lib/browser/operation-resolver-BRA2OHUE.mjs +162 -0
  21. package/dist/lib/browser/operation-resolver-BRA2OHUE.mjs.map +7 -0
  22. package/dist/lib/browser/react-surface-FOMOGFVW.mjs +99 -0
  23. package/dist/lib/browser/react-surface-FOMOGFVW.mjs.map +7 -0
  24. package/dist/lib/browser/types/index.mjs +14 -5
  25. package/dist/lib/node-esm/KanbanContainer-EHRTLE7M.mjs +305 -0
  26. package/dist/lib/node-esm/KanbanContainer-EHRTLE7M.mjs.map +7 -0
  27. package/dist/lib/node-esm/KanbanViewEditor-WDACFC35.mjs +67 -0
  28. package/dist/lib/node-esm/KanbanViewEditor-WDACFC35.mjs.map +7 -0
  29. package/dist/lib/node-esm/blueprint-definition-NARBX32U.mjs +18 -0
  30. package/dist/lib/node-esm/blueprint-definition-NARBX32U.mjs.map +7 -0
  31. package/dist/lib/node-esm/blueprints/index.mjs +9 -0
  32. package/dist/lib/node-esm/blueprints/index.mjs.map +7 -0
  33. package/dist/lib/node-esm/chunk-4AWDHQVY.mjs +214 -0
  34. package/dist/lib/node-esm/chunk-4AWDHQVY.mjs.map +7 -0
  35. package/dist/lib/node-esm/chunk-CSL3HF2X.mjs +386 -0
  36. package/dist/lib/node-esm/chunk-CSL3HF2X.mjs.map +7 -0
  37. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  38. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  39. package/dist/lib/node-esm/chunk-X3UJUQIV.mjs +31 -0
  40. package/dist/lib/node-esm/chunk-X3UJUQIV.mjs.map +7 -0
  41. package/dist/lib/node-esm/index.mjs +65 -68
  42. package/dist/lib/node-esm/index.mjs.map +4 -4
  43. package/dist/lib/node-esm/meta.json +1 -1
  44. package/dist/lib/node-esm/operation-resolver-UEQ64LCN.mjs +163 -0
  45. package/dist/lib/node-esm/operation-resolver-UEQ64LCN.mjs.map +7 -0
  46. package/dist/lib/node-esm/react-surface-AITKFRBE.mjs +100 -0
  47. package/dist/lib/node-esm/react-surface-AITKFRBE.mjs.map +7 -0
  48. package/dist/lib/node-esm/types/index.mjs +14 -5
  49. package/dist/types/src/KanbanPlugin.d.ts +2 -1
  50. package/dist/types/src/KanbanPlugin.d.ts.map +1 -1
  51. package/dist/types/src/blueprints/index.d.ts +2 -0
  52. package/dist/types/src/blueprints/index.d.ts.map +1 -0
  53. package/dist/types/src/blueprints/kanban-blueprint.d.ts +4 -0
  54. package/dist/types/src/blueprints/kanban-blueprint.d.ts.map +1 -0
  55. package/dist/types/src/capabilities/artifact-definition/artifact-definition.d.ts +12 -0
  56. package/dist/types/src/capabilities/artifact-definition/artifact-definition.d.ts.map +1 -0
  57. package/dist/types/src/capabilities/artifact-definition/index.d.ts +3 -0
  58. package/dist/types/src/capabilities/artifact-definition/index.d.ts.map +1 -0
  59. package/dist/types/src/capabilities/blueprint-definition/blueprint-definition.d.ts +6 -0
  60. package/dist/types/src/capabilities/blueprint-definition/blueprint-definition.d.ts.map +1 -0
  61. package/dist/types/src/capabilities/blueprint-definition/index.d.ts +3 -0
  62. package/dist/types/src/capabilities/blueprint-definition/index.d.ts.map +1 -0
  63. package/dist/types/src/capabilities/index.d.ts +3 -3
  64. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  65. package/dist/types/src/capabilities/operation-resolver/index.d.ts +3 -0
  66. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +1 -0
  67. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +5 -0
  68. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -0
  69. package/dist/types/src/capabilities/react-surface/index.d.ts +3 -0
  70. package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -0
  71. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +5 -0
  72. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -0
  73. package/dist/types/src/components/KanbanBoard/KanbanBoard.d.ts +58 -0
  74. package/dist/types/src/components/KanbanBoard/KanbanBoard.d.ts.map +1 -0
  75. package/dist/types/src/components/KanbanBoard/KanbanBoard.stories.d.ts +70 -0
  76. package/dist/types/src/components/KanbanBoard/KanbanBoard.stories.d.ts.map +1 -0
  77. package/dist/types/src/components/KanbanBoard/KanbanCard.d.ts +10 -0
  78. package/dist/types/src/components/KanbanBoard/KanbanCard.d.ts.map +1 -0
  79. package/dist/types/src/components/KanbanBoard/KanbanColumn.d.ts +9 -0
  80. package/dist/types/src/components/KanbanBoard/KanbanColumn.d.ts.map +1 -0
  81. package/dist/types/src/components/KanbanBoard/index.d.ts +2 -0
  82. package/dist/types/src/components/KanbanBoard/index.d.ts.map +1 -0
  83. package/dist/types/src/components/index.d.ts +1 -2
  84. package/dist/types/src/components/index.d.ts.map +1 -1
  85. package/dist/types/src/containers/KanbanContainer/KanbanContainer.d.ts +6 -0
  86. package/dist/types/src/containers/KanbanContainer/KanbanContainer.d.ts.map +1 -0
  87. package/dist/types/src/containers/KanbanContainer/KanbanContainer.stories.d.ts +77 -0
  88. package/dist/types/src/containers/KanbanContainer/KanbanContainer.stories.d.ts.map +1 -0
  89. package/dist/types/src/containers/KanbanContainer/index.d.ts +3 -0
  90. package/dist/types/src/containers/KanbanContainer/index.d.ts.map +1 -0
  91. package/dist/types/src/containers/KanbanViewEditor/KanbanViewEditor.d.ts +6 -0
  92. package/dist/types/src/containers/KanbanViewEditor/KanbanViewEditor.d.ts.map +1 -0
  93. package/dist/types/src/containers/KanbanViewEditor/index.d.ts +3 -0
  94. package/dist/types/src/containers/KanbanViewEditor/index.d.ts.map +1 -0
  95. package/dist/types/src/containers/index.d.ts +4 -0
  96. package/dist/types/src/containers/index.d.ts.map +1 -0
  97. package/dist/types/src/hooks/index.d.ts +6 -0
  98. package/dist/types/src/hooks/index.d.ts.map +1 -0
  99. package/dist/types/src/hooks/useEchoChangeCallback.d.ts +13 -0
  100. package/dist/types/src/hooks/useEchoChangeCallback.d.ts.map +1 -0
  101. package/dist/types/src/hooks/useKanbanBoardModel.d.ts +16 -0
  102. package/dist/types/src/hooks/useKanbanBoardModel.d.ts.map +1 -0
  103. package/dist/types/src/hooks/useKanbanBoardModel.test.d.ts +2 -0
  104. package/dist/types/src/hooks/useKanbanBoardModel.test.d.ts.map +1 -0
  105. package/dist/types/src/hooks/useKanbanColumnEventHandler.d.ts +22 -0
  106. package/dist/types/src/hooks/useKanbanColumnEventHandler.d.ts.map +1 -0
  107. package/dist/types/src/hooks/useKanbanItemEventHandler.d.ts +19 -0
  108. package/dist/types/src/hooks/useKanbanItemEventHandler.d.ts.map +1 -0
  109. package/dist/types/src/hooks/useProjectionModel.d.ts +15 -0
  110. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  111. package/dist/types/src/meta.d.ts +2 -2
  112. package/dist/types/src/meta.d.ts.map +1 -1
  113. package/dist/types/src/playwright/board-manager.d.ts +5 -0
  114. package/dist/types/src/playwright/board-manager.d.ts.map +1 -0
  115. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  116. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  117. package/dist/types/src/playwright/smoke.spec.d.ts +2 -0
  118. package/dist/types/src/playwright/smoke.spec.d.ts.map +1 -0
  119. package/dist/types/src/testing/KanbanCardTileSimple.d.ts +5 -0
  120. package/dist/types/src/testing/KanbanCardTileSimple.d.ts.map +1 -0
  121. package/dist/types/src/testing/index.d.ts +2 -0
  122. package/dist/types/src/testing/index.d.ts.map +1 -0
  123. package/dist/types/src/translations.d.ts +35 -9
  124. package/dist/types/src/translations.d.ts.map +1 -1
  125. package/dist/types/src/types/Kanban.d.ts +37 -0
  126. package/dist/types/src/types/Kanban.d.ts.map +1 -0
  127. package/dist/types/src/types/constants.d.ts +6 -0
  128. package/dist/types/src/types/constants.d.ts.map +1 -0
  129. package/dist/types/src/types/index.d.ts +2 -0
  130. package/dist/types/src/types/index.d.ts.map +1 -1
  131. package/dist/types/src/types/schema.d.ts +98 -43
  132. package/dist/types/src/types/schema.d.ts.map +1 -1
  133. package/dist/types/src/types/types.d.ts +28 -0
  134. package/dist/types/src/types/types.d.ts.map +1 -1
  135. package/dist/types/src/util/arrangement.d.ts +68 -0
  136. package/dist/types/src/util/arrangement.d.ts.map +1 -0
  137. package/dist/types/src/util/arrangement.test.d.ts +2 -0
  138. package/dist/types/src/util/arrangement.test.d.ts.map +1 -0
  139. package/dist/types/src/util/index.d.ts +2 -0
  140. package/dist/types/src/util/index.d.ts.map +1 -0
  141. package/dist/types/tsconfig.tsbuildinfo +1 -1
  142. package/package.json +67 -46
  143. package/src/KanbanPlugin.tsx +35 -54
  144. package/src/blueprints/index.ts +5 -0
  145. package/src/blueprints/kanban-blueprint.ts +31 -0
  146. package/src/capabilities/artifact-definition/artifact-definition.ts +151 -0
  147. package/src/capabilities/artifact-definition/index.ts +7 -0
  148. package/src/capabilities/blueprint-definition/blueprint-definition.ts +17 -0
  149. package/src/capabilities/blueprint-definition/index.ts +7 -0
  150. package/src/capabilities/index.ts +3 -5
  151. package/src/capabilities/operation-resolver/index.ts +7 -0
  152. package/src/capabilities/operation-resolver/operation-resolver.ts +133 -0
  153. package/src/capabilities/react-surface/index.ts +7 -0
  154. package/src/capabilities/react-surface/react-surface.tsx +86 -0
  155. package/src/components/KanbanBoard/KanbanBoard.stories.tsx +142 -0
  156. package/src/components/KanbanBoard/KanbanBoard.tsx +184 -0
  157. package/src/components/KanbanBoard/KanbanCard.tsx +87 -0
  158. package/src/components/KanbanBoard/KanbanColumn.tsx +69 -0
  159. package/src/components/KanbanBoard/index.ts +5 -0
  160. package/src/components/index.ts +1 -2
  161. package/src/containers/KanbanContainer/KanbanContainer.stories.tsx +263 -0
  162. package/src/containers/KanbanContainer/KanbanContainer.tsx +87 -0
  163. package/src/containers/KanbanContainer/index.ts +7 -0
  164. package/src/containers/KanbanViewEditor/KanbanViewEditor.tsx +63 -0
  165. package/src/containers/KanbanViewEditor/index.ts +7 -0
  166. package/src/containers/index.ts +8 -0
  167. package/src/hooks/index.ts +9 -0
  168. package/src/hooks/useEchoChangeCallback.ts +30 -0
  169. package/src/hooks/useKanbanBoardModel.test.ts +235 -0
  170. package/src/hooks/useKanbanBoardModel.ts +143 -0
  171. package/src/hooks/useKanbanColumnEventHandler.ts +106 -0
  172. package/src/hooks/useKanbanItemEventHandler.ts +133 -0
  173. package/src/hooks/useProjectionModel.ts +58 -0
  174. package/src/meta.ts +8 -4
  175. package/src/playwright/board-manager.ts +13 -0
  176. package/src/playwright/playwright.config.ts +19 -0
  177. package/src/playwright/smoke.spec.ts +107 -0
  178. package/src/testing/KanbanCardTileSimple.tsx +76 -0
  179. package/src/testing/index.ts +5 -0
  180. package/src/translations.ts +13 -6
  181. package/src/types/Kanban.ts +67 -0
  182. package/src/types/constants.ts +9 -0
  183. package/src/types/index.ts +2 -0
  184. package/src/types/schema.ts +84 -38
  185. package/src/types/types.ts +35 -0
  186. package/src/util/arrangement.test.ts +208 -0
  187. package/src/util/arrangement.ts +167 -0
  188. package/src/util/index.ts +5 -0
  189. package/dist/lib/browser/blueprint-definition-UYVX622Q.mjs +0 -28
  190. package/dist/lib/browser/blueprint-definition-UYVX622Q.mjs.map +0 -7
  191. package/dist/lib/browser/chunk-K7KYFJ3P.mjs +0 -80
  192. package/dist/lib/browser/chunk-K7KYFJ3P.mjs.map +0 -7
  193. package/dist/lib/browser/intent-resolver-LR3SDMKQ.mjs +0 -111
  194. package/dist/lib/browser/intent-resolver-LR3SDMKQ.mjs.map +0 -7
  195. package/dist/lib/browser/react-surface-R2ORFB43.mjs +0 -255
  196. package/dist/lib/browser/react-surface-R2ORFB43.mjs.map +0 -7
  197. package/dist/lib/node-esm/blueprint-definition-42P47FUY.mjs +0 -30
  198. package/dist/lib/node-esm/blueprint-definition-42P47FUY.mjs.map +0 -7
  199. package/dist/lib/node-esm/chunk-VDWYSHP4.mjs +0 -82
  200. package/dist/lib/node-esm/chunk-VDWYSHP4.mjs.map +0 -7
  201. package/dist/lib/node-esm/intent-resolver-CKDNVLND.mjs +0 -112
  202. package/dist/lib/node-esm/intent-resolver-CKDNVLND.mjs.map +0 -7
  203. package/dist/lib/node-esm/react-surface-RMRLHCOY.mjs +0 -256
  204. package/dist/lib/node-esm/react-surface-RMRLHCOY.mjs.map +0 -7
  205. package/dist/types/src/capabilities/artifact-definition.d.ts +0 -11
  206. package/dist/types/src/capabilities/artifact-definition.d.ts.map +0 -1
  207. package/dist/types/src/capabilities/blueprint-definition.d.ts +0 -5
  208. package/dist/types/src/capabilities/blueprint-definition.d.ts.map +0 -1
  209. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  210. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  211. package/dist/types/src/capabilities/react-surface.d.ts +0 -4
  212. package/dist/types/src/capabilities/react-surface.d.ts.map +0 -1
  213. package/dist/types/src/components/KanbanContainer.d.ts +0 -7
  214. package/dist/types/src/components/KanbanContainer.d.ts.map +0 -1
  215. package/dist/types/src/components/KanbanContainer.stories.d.ts +0 -41
  216. package/dist/types/src/components/KanbanContainer.stories.d.ts.map +0 -1
  217. package/dist/types/src/components/KanbanViewEditor.d.ts +0 -8
  218. package/dist/types/src/components/KanbanViewEditor.d.ts.map +0 -1
  219. package/src/capabilities/artifact-definition.ts +0 -148
  220. package/src/capabilities/blueprint-definition.ts +0 -30
  221. package/src/capabilities/intent-resolver.ts +0 -71
  222. package/src/capabilities/react-surface.tsx +0 -84
  223. package/src/components/KanbanContainer.stories.tsx +0 -193
  224. package/src/components/KanbanContainer.tsx +0 -95
  225. package/src/components/KanbanViewEditor.tsx +0 -64
@@ -0,0 +1,86 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import type * as Schema from 'effect/Schema';
7
+ import React, { useMemo } from 'react';
8
+
9
+ import { Capabilities, Capability } from '@dxos/app-framework';
10
+ import { Surface } from '@dxos/app-framework/ui';
11
+ import { Database, JsonSchema, Obj } from '@dxos/echo';
12
+ import { type Collection } from '@dxos/echo';
13
+ import { findAnnotation } from '@dxos/effect';
14
+ import { type FormFieldComponentProps, SelectField, useFormValues } from '@dxos/react-ui-form';
15
+
16
+ import { KanbanContainer, KanbanViewEditor } from '../../containers';
17
+ import { meta } from '../../meta';
18
+ import { Kanban, PivotColumnAnnotationId } from '../../types';
19
+
20
+ export default Capability.makeModule(() =>
21
+ Effect.succeed(
22
+ Capability.contributes(Capabilities.ReactSurface, [
23
+ Surface.create({
24
+ id: meta.id,
25
+ role: ['article', 'section'],
26
+ filter: (data): data is { subject: Kanban.Kanban } => Obj.instanceOf(Kanban.Kanban, data.subject),
27
+ component: ({ data, role }) => <KanbanContainer role={role} subject={data.subject} />,
28
+ }),
29
+ Surface.create({
30
+ id: `${meta.id}/object-settings`,
31
+ role: 'object-settings',
32
+ position: 'hoist',
33
+ filter: (data): data is { subject: Kanban.Kanban } => Obj.instanceOf(Kanban.Kanban, data.subject),
34
+ component: ({ data }) => <KanbanViewEditor subject={data.subject} />,
35
+ }),
36
+ Surface.create({
37
+ id: `${meta.id}/create-initial-schema-form-[pivot-column]`,
38
+ role: 'form-input',
39
+ filter: (
40
+ data,
41
+ ): data is {
42
+ prop: string;
43
+ schema: Schema.Schema<any>;
44
+ target: Database.Database | Collection.Collection | undefined;
45
+ } => {
46
+ const annotation = findAnnotation<boolean>((data.schema as Schema.Schema.All).ast, PivotColumnAnnotationId);
47
+ return !!annotation;
48
+ },
49
+ component: ({ data: { target }, ...inputProps }) => {
50
+ const props = inputProps as any as FormFieldComponentProps;
51
+ const db = Database.isDatabase(target) ? target : target && Obj.getDatabase(target);
52
+ if (!db) {
53
+ return null;
54
+ }
55
+
56
+ const { typename } = useFormValues('KanbanForm');
57
+ const [selectedSchema] = useMemo(
58
+ () => db.schemaRegistry.query({ location: ['database', 'runtime'], typename }).runSync(),
59
+ [db, typename],
60
+ );
61
+ const singleSelectColumns = useMemo(() => {
62
+ const properties = JsonSchema.toJsonSchema(selectedSchema).properties;
63
+ if (!properties) {
64
+ return [];
65
+ }
66
+
67
+ const columns = Object.entries(properties).reduce<string[]>((acc, [key, value]) => {
68
+ if (typeof value === 'object' && value?.format === 'single-select') {
69
+ acc.push(key);
70
+ }
71
+ return acc;
72
+ }, []);
73
+
74
+ return columns;
75
+ }, [selectedSchema]);
76
+
77
+ if (!typename) {
78
+ return null;
79
+ }
80
+
81
+ return <SelectField {...props} options={singleSelectColumns.map((column) => ({ value: column }))} />;
82
+ },
83
+ }),
84
+ ]),
85
+ ),
86
+ );
@@ -0,0 +1,142 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Atom, RegistryContext } from '@effect-atom/atom-react';
6
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
7
+ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
8
+
9
+ import { Filter, JsonSchema, Obj, Query } from '@dxos/echo';
10
+ import { type View } from '@dxos/echo';
11
+ import { faker } from '@dxos/random';
12
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
13
+ import { withMosaic } from '@dxos/react-ui-mosaic/testing';
14
+ import { ProjectionModel, ViewModel, createEchoChangeCallback } from '@dxos/schema';
15
+ import { withRegistry } from '@dxos/storybook-utils';
16
+ import { Organization } from '@dxos/types';
17
+
18
+ import { createEchoChangeCallback as createKanbanChangeCallback } from '../../hooks';
19
+ import { KanbanCardTileSimple } from '../../testing';
20
+ import { translations } from '../../translations';
21
+ import { Kanban } from '../../types';
22
+
23
+ import { KanbanBoard } from './KanbanBoard';
24
+
25
+ faker.seed(1);
26
+
27
+ const createOrg = () => ({
28
+ name: faker.commerce.productName(),
29
+ description: faker.lorem.sentence(),
30
+ image: faker.image.url(),
31
+ website: faker.internet.url(),
32
+ status: faker.helpers.arrayElement(Organization.StatusOptions).id as Organization.Organization['status'],
33
+ });
34
+
35
+ /**
36
+ * In-memory Kanban board: View + ProjectionModel + Kanban + items, no plugin manager or Space.
37
+ * Similar to react-ui-kanban Kanban.stories.tsx.
38
+ */
39
+ const DefaultStory = () => {
40
+ const registry = useContext(RegistryContext);
41
+ const items = useMemo(() => Atom.make<Obj.Unknown[]>([]), []);
42
+ const [state, setState] = useState<{
43
+ view: View.View;
44
+ kanban: Kanban.Kanban;
45
+ projection: ProjectionModel;
46
+ change: ReturnType<typeof createKanbanChangeCallback>;
47
+ }>();
48
+
49
+ useEffect(() => {
50
+ const view = ViewModel.make({
51
+ query: Query.select(Filter.typename(Organization.Organization.typename)),
52
+ jsonSchema: JsonSchema.toJsonSchema(Organization.Organization),
53
+ pivotFieldName: 'status',
54
+ });
55
+ const kanban = Kanban.make({ view });
56
+ const change = createKanbanChangeCallback(kanban);
57
+ const projection = new ProjectionModel({
58
+ registry,
59
+ view,
60
+ baseSchema: JsonSchema.toJsonSchema(Organization.Organization),
61
+ change: createEchoChangeCallback(view),
62
+ });
63
+ projection.normalizeView();
64
+
65
+ const statuses = Organization.StatusOptions.map((o) => o.id);
66
+ const initialItems = Array.from({ length: 12 }, () =>
67
+ Obj.make(Organization.Organization, {
68
+ ...createOrg(),
69
+ status: faker.helpers.arrayElement(statuses) as Organization.Organization['status'],
70
+ }),
71
+ );
72
+
73
+ setState({ view, kanban, projection, change });
74
+ registry.set(items, initialItems);
75
+ }, [registry, items]);
76
+
77
+ const columnFieldPath =
78
+ state?.projection.tryGetFieldProjection(state.projection.getFieldId('status') ?? '')?.props.property ?? 'status';
79
+
80
+ const handleCardAdd = useCallback(
81
+ (columnValue: string | undefined) => {
82
+ if (!state || !columnFieldPath || !registry) return undefined;
83
+ const card = Obj.make(Organization.Organization, {
84
+ ...createOrg(),
85
+ ...(columnFieldPath ? { [columnFieldPath]: columnValue } : {}),
86
+ });
87
+ const current = registry.get(items) ?? [];
88
+ registry.set(items, [...current, card]);
89
+ return card.id;
90
+ },
91
+ [state, columnFieldPath, registry, items],
92
+ );
93
+
94
+ const handleCardRemove = useCallback(
95
+ (card: Obj.Unknown) => {
96
+ if (!registry) return;
97
+ const current = registry.get(items) ?? [];
98
+ registry.set(
99
+ items,
100
+ current.filter((i) => i.id !== card.id),
101
+ );
102
+ },
103
+ [registry, items],
104
+ );
105
+
106
+ if (!state) {
107
+ return <></>;
108
+ }
109
+
110
+ return (
111
+ <KanbanBoard.Root
112
+ kanban={state.kanban}
113
+ projection={state.projection}
114
+ items={items}
115
+ itemTile={KanbanCardTileSimple}
116
+ change={state.change}
117
+ onCardAdd={handleCardAdd}
118
+ onCardRemove={handleCardRemove}
119
+ >
120
+ <KanbanBoard.Content />
121
+ </KanbanBoard.Root>
122
+ );
123
+ };
124
+
125
+ const meta = {
126
+ title: 'plugins/plugin-kanban/components/KanbanBoard',
127
+ component: DefaultStory,
128
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' }), withMosaic(), withRegistry],
129
+ parameters: {
130
+ layout: 'fullscreen',
131
+ translations,
132
+ },
133
+ } satisfies Meta<typeof DefaultStory>;
134
+
135
+ export default meta;
136
+
137
+ type Story = StoryObj<typeof meta>;
138
+
139
+ /**
140
+ * In-memory board with Echo-shaped objects. No plugin manager, client, or Space.
141
+ */
142
+ export const Default: Story = {};
@@ -0,0 +1,184 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Atom, RegistryContext } from '@effect-atom/atom-react';
6
+ import { createContext } from '@radix-ui/react-context';
7
+ import React, {
8
+ type ComponentPropsWithoutRef,
9
+ type ComponentType,
10
+ type PropsWithChildren,
11
+ useCallback,
12
+ useContext,
13
+ useMemo,
14
+ } from 'react';
15
+
16
+ import { Obj } from '@dxos/echo';
17
+ import { useTranslation } from '@dxos/react-ui';
18
+ import { Board, useBoard } from '@dxos/react-ui-mosaic';
19
+ import type { ProjectionModel } from '@dxos/schema';
20
+
21
+ import { useKanbanBoardModel, useKanbanColumnEventHandler } from '../../hooks';
22
+ import { meta } from '../../meta';
23
+ import { type Kanban, type KanbanChangeCallback, UNCATEGORIZED_ATTRIBUTES, UNCATEGORIZED_VALUE } from '../../types';
24
+
25
+ import { KanbanCard, type KanbanCardProps } from './KanbanCard';
26
+ import { KanbanColumn, type KanbanColumnProps } from './KanbanColumn';
27
+
28
+ // TODO(burdon): Rename Kanban.
29
+
30
+ //
31
+ // Context
32
+ //
33
+
34
+ const KANBAN_BOARD_NAME = 'KanbanBoard.Context';
35
+
36
+ /**
37
+ * Context value for the Kanban board.
38
+ * Items are Echo objects (Obj.Unknown).
39
+ */
40
+ type KanbanBoardContextValue = {
41
+ kanbanId: string;
42
+ projection: ProjectionModel | undefined;
43
+ columnFieldPath: string | undefined;
44
+ change: KanbanChangeCallback<Obj.Unknown>;
45
+ pivotFieldId: string | undefined;
46
+ getPivotAttributes: (columnValue: string) => { title: string; color: string };
47
+ itemTile?: ComponentType<KanbanCardProps>; // TODO(burdon): Prop.
48
+ onCardAdd?: (columnValue: string | undefined) => string | undefined;
49
+ onCardRemove?: (card: Obj.Unknown) => void;
50
+ };
51
+
52
+ const [KanbanBoardContext, useKanbanBoard] = createContext<KanbanBoardContextValue>(KANBAN_BOARD_NAME, {
53
+ kanbanId: 'never',
54
+ projection: undefined,
55
+ columnFieldPath: undefined,
56
+ change: { kanban: () => {}, setItemField: () => {} },
57
+ pivotFieldId: undefined,
58
+ getPivotAttributes: (id: string) =>
59
+ id === UNCATEGORIZED_VALUE ? UNCATEGORIZED_ATTRIBUTES : { title: id, color: 'neutral' },
60
+ itemTile: (() => null) as ComponentType<KanbanCardProps>,
61
+ });
62
+
63
+ //
64
+ // Root
65
+ //
66
+
67
+ const KANBAN_BOARD_ROOT = 'KanbanBoard.Root';
68
+
69
+ type KanbanBoardRootProps = PropsWithChildren<
70
+ Pick<KanbanBoardContextValue, 'change' | 'itemTile'> & {
71
+ kanban: Kanban.Kanban;
72
+ /** Required when providing context; Root derives columnFieldPath, pivotFieldId, getPivotAttributes from kanban + projection. */
73
+ projection: ProjectionModel;
74
+ /** Atom of items (e.g. from AtomQuery for DB, or Atom.make([]) for in-memory). */
75
+ items: Atom.Atom<Obj.Unknown[]>;
76
+ onCardAdd?: (columnValue: string | undefined) => string | undefined;
77
+ onCardRemove?: (card: Obj.Unknown) => void;
78
+ } & ComponentPropsWithoutRef<'div'>
79
+ >;
80
+
81
+ export const KanbanBoardRoot = ({
82
+ children,
83
+ change,
84
+ itemTile = KanbanCard,
85
+ kanban,
86
+ projection,
87
+ items,
88
+ onCardAdd,
89
+ onCardRemove,
90
+ ...props
91
+ }: KanbanBoardRootProps) => {
92
+ const registry = useContext(RegistryContext);
93
+ const { t } = useTranslation(meta.id);
94
+ const model = useKanbanBoardModel(kanban, projection, items, registry);
95
+ const columns = model?.getColumns?.() ?? [];
96
+ const view = kanban?.view?.target;
97
+ const pivotFieldId = view?.projection?.pivotFieldId;
98
+ const columnFieldPath = useMemo(() => {
99
+ if (pivotFieldId === undefined || !projection) {
100
+ return undefined;
101
+ }
102
+
103
+ return projection.tryGetFieldProjection(pivotFieldId)?.props.property;
104
+ }, [projection, pivotFieldId]);
105
+
106
+ const getPivotAttributes = useCallback<KanbanBoardContextValue['getPivotAttributes']>(
107
+ (columnValue) => {
108
+ if (columnValue === UNCATEGORIZED_VALUE) {
109
+ return UNCATEGORIZED_ATTRIBUTES;
110
+ }
111
+
112
+ const options = projection?.tryGetFieldProjection(pivotFieldId ?? '')?.props.options ?? [];
113
+ const option = options.find((option) => option.id === columnValue);
114
+ return option ?? ({ title: columnValue, color: 'neutral' } as const);
115
+ },
116
+ [projection, pivotFieldId],
117
+ );
118
+
119
+ if (columns.length === 0) {
120
+ return (
121
+ <div {...props} className='flex flex-1 items-center justify-center p-8 text-center text-description'>
122
+ {t('select pivot placeholder')}
123
+ </div>
124
+ );
125
+ }
126
+
127
+ return (
128
+ <KanbanBoardContext
129
+ kanbanId={Obj.getDXN(kanban).toString()}
130
+ projection={projection}
131
+ columnFieldPath={columnFieldPath}
132
+ pivotFieldId={pivotFieldId}
133
+ getPivotAttributes={getPivotAttributes}
134
+ itemTile={itemTile}
135
+ change={change}
136
+ onCardAdd={onCardAdd}
137
+ onCardRemove={onCardRemove}
138
+ >
139
+ <Board.Root model={model}>
140
+ <div {...props}>{children}</div>
141
+ </Board.Root>
142
+ </KanbanBoardContext>
143
+ );
144
+ };
145
+
146
+ KanbanBoardRoot.displayName = KANBAN_BOARD_ROOT;
147
+
148
+ //
149
+ // KanbanBoardContent
150
+ //
151
+
152
+ const KANBAN_BOARD_CONTENT = 'KanbanBoard.Content';
153
+
154
+ export const KanbanBoardContent = () => {
155
+ const { model } = useBoard(KANBAN_BOARD_CONTENT);
156
+ const { kanbanId, projection, pivotFieldId, change } = useKanbanBoard(KANBAN_BOARD_CONTENT);
157
+
158
+ const columnEventHandler = useKanbanColumnEventHandler({
159
+ id: `${kanbanId}-columns`,
160
+ model,
161
+ projection: projection ?? undefined,
162
+ pivotFieldId: pivotFieldId ?? undefined,
163
+ change,
164
+ });
165
+
166
+ return <Board.Content id={kanbanId} eventHandler={columnEventHandler} Tile={KanbanColumn} />;
167
+ };
168
+
169
+ KanbanBoardContent.displayName = KANBAN_BOARD_CONTENT;
170
+
171
+ //
172
+ // KanbanBoard
173
+ //
174
+
175
+ export const KanbanBoard = {
176
+ Root: KanbanBoardRoot,
177
+ Content: KanbanBoardContent,
178
+ Column: KanbanColumn,
179
+ Card: KanbanCard,
180
+ };
181
+
182
+ export { useKanbanBoard };
183
+
184
+ export type { KanbanBoardRootProps as KanbanBoardProps, KanbanCardProps, KanbanColumnProps };
@@ -0,0 +1,87 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { forwardRef, useCallback, useMemo, useState } from 'react';
6
+
7
+ import { Surface } from '@dxos/app-framework/ui';
8
+ import { useObjectMenuItems, useObjectNavigate } from '@dxos/app-toolkit/ui';
9
+ import { Obj } from '@dxos/echo';
10
+ import { Card, Toolbar, useTranslation } from '@dxos/react-ui';
11
+ import { Menu, createMenuAction } from '@dxos/react-ui-menu';
12
+ import { Focus, Mosaic, type MosaicTileProps, useBoard } from '@dxos/react-ui-mosaic';
13
+
14
+ import { meta } from '../../meta';
15
+
16
+ import { useKanbanBoard } from './KanbanBoard';
17
+
18
+ const KANBAN_CARD_TILE_NAME = 'KanbanBoard.Card';
19
+
20
+ export type KanbanCardProps = Pick<MosaicTileProps<Obj.Unknown>, 'location' | 'data' | 'debug'>;
21
+
22
+ /**
23
+ * Mosaic Tile for Kanban card.
24
+ * Uses Surface for content; requires plugin manager context.
25
+ */
26
+ export const KanbanCard = forwardRef<HTMLDivElement, KanbanCardProps>(({ data, location, debug }, forwardedRef) => {
27
+ const { t } = useTranslation(meta.id);
28
+ const { model } = useBoard(KANBAN_CARD_TILE_NAME);
29
+ const { projection, onCardRemove } = useKanbanBoard(KANBAN_CARD_TILE_NAME);
30
+ const [dragHandle, setDragHandle] = useState<HTMLButtonElement | null>(null);
31
+ const dragHandleRef = useCallback((el: HTMLButtonElement | null) => setDragHandle(el), []);
32
+
33
+ const objectMenuItems = useObjectMenuItems(data);
34
+ const handleNavigate = useObjectNavigate(data);
35
+
36
+ const menuItems = useMemo(
37
+ () => [
38
+ ...objectMenuItems,
39
+ ...(onCardRemove
40
+ ? [
41
+ createMenuAction('remove', () => onCardRemove(data), {
42
+ label: t('remove card label'),
43
+ icon: 'ph--trash--regular',
44
+ }),
45
+ ]
46
+ : []),
47
+ ],
48
+ [objectMenuItems, onCardRemove, data, t],
49
+ );
50
+
51
+ return (
52
+ <Mosaic.Tile
53
+ asChild
54
+ id={model.getItemId(data)}
55
+ data={data}
56
+ location={location}
57
+ debug={debug}
58
+ dragHandle={dragHandle}
59
+ >
60
+ <Focus.Group asChild>
61
+ <Menu.Root>
62
+ <Card.Root ref={forwardedRef} data-testid='board-item'>
63
+ <Card.Toolbar>
64
+ <Card.DragHandle ref={dragHandleRef} />
65
+ <Card.Title onClick={handleNavigate}>{Obj.getLabel(data)}</Card.Title>
66
+ {/* TODO(wittjosiah): Reconcile with Card.Menu. */}
67
+ <Menu.Trigger asChild disabled={!menuItems?.length}>
68
+ <Toolbar.IconButton
69
+ iconOnly
70
+ variant='ghost'
71
+ icon='ph--dots-three-vertical--regular'
72
+ label={t('action menu label')}
73
+ />
74
+ </Menu.Trigger>
75
+ <Menu.Content items={menuItems} />
76
+ </Card.Toolbar>
77
+ <Card.Content>
78
+ {projection && <Surface.Surface role='card--content' limit={1} data={{ subject: data, projection }} />}
79
+ </Card.Content>
80
+ </Card.Root>
81
+ </Menu.Root>
82
+ </Focus.Group>
83
+ </Mosaic.Tile>
84
+ );
85
+ });
86
+
87
+ KanbanCard.displayName = KANBAN_CARD_TILE_NAME;
@@ -0,0 +1,69 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { forwardRef, useRef } from 'react';
6
+
7
+ import type { Obj } from '@dxos/echo';
8
+ import { Board, type MosaicTileProps, useBoard } from '@dxos/react-ui-mosaic';
9
+
10
+ import { useKanbanItemEventHandler } from '../../hooks';
11
+ import { type ColumnStructure, UNCATEGORIZED_VALUE } from '../../types';
12
+
13
+ import { useKanbanBoard } from './KanbanBoard';
14
+
15
+ const KANBAN_COLUMN_NAME = 'KanbanBoard.Column';
16
+
17
+ export type KanbanColumnProps = Pick<MosaicTileProps<ColumnStructure>, 'location' | 'data' | 'debug'>;
18
+
19
+ /**
20
+ * Mosaic Tile for Kanban column.
21
+ */
22
+ export const KanbanColumn = forwardRef<HTMLDivElement, KanbanColumnProps>(
23
+ ({ data: column, location, debug }, forwardedRef) => {
24
+ const { model } = useBoard<ColumnStructure, Obj.Unknown>(KANBAN_COLUMN_NAME);
25
+ const { columnFieldPath, change, onCardAdd, getPivotAttributes, itemTile } = useKanbanBoard(KANBAN_COLUMN_NAME);
26
+
27
+ const { title } = getPivotAttributes(column.columnValue);
28
+ const uncategorized = column.columnValue === UNCATEGORIZED_VALUE;
29
+ const dragHandleRef = useRef<HTMLButtonElement | null>(null);
30
+
31
+ const eventHandler = useKanbanItemEventHandler({
32
+ column,
33
+ columnFieldPath,
34
+ model,
35
+ change,
36
+ });
37
+
38
+ return (
39
+ <Board.Column.Root
40
+ data={column}
41
+ location={location}
42
+ classNames='grid grid-rows-[var(--dx-rail-action)_1fr_var(--dx-rail-action)]'
43
+ debug={debug}
44
+ dragHandleRef={dragHandleRef}
45
+ ref={forwardedRef}
46
+ >
47
+ {uncategorized ? (
48
+ <div className='border-b border-separator p-2' data-testid='board-column-header'>
49
+ <span className='font-medium'>{title}</span>
50
+ </div>
51
+ ) : (
52
+ <Board.Column.Header label={title} dragHandleRef={dragHandleRef as React.RefObject<HTMLButtonElement>} />
53
+ )}
54
+ <Board.Column.Body
55
+ data={column}
56
+ eventHandler={eventHandler}
57
+ Tile={itemTile as React.FC<MosaicTileProps<Obj.Unknown>>}
58
+ />
59
+ {onCardAdd && (
60
+ <Board.Column.Footer
61
+ onAdd={() => onCardAdd(column.columnValue === UNCATEGORIZED_VALUE ? undefined : column.columnValue)}
62
+ />
63
+ )}
64
+ </Board.Column.Root>
65
+ );
66
+ },
67
+ );
68
+
69
+ KanbanColumn.displayName = KANBAN_COLUMN_NAME;
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './KanbanBoard';
@@ -2,5 +2,4 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './KanbanContainer';
6
- export * from './KanbanViewEditor';
5
+ export * from './KanbanBoard';