@kelpie/ui 0.3.0 → 0.4.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 (102) hide show
  1. package/dist/api/ApiProvider.d.ts +3 -3
  2. package/dist/api/ApiProvider.d.ts.map +1 -1
  3. package/dist/api/client.d.ts.map +1 -1
  4. package/dist/api/client.js +4 -1
  5. package/dist/api/client.js.map +1 -1
  6. package/dist/api/resources/agentTasks.d.ts +2 -2
  7. package/dist/api/resources/agentTasks.d.ts.map +1 -1
  8. package/dist/api/resources/candidates.d.ts +4 -4
  9. package/dist/api/resources/candidates.d.ts.map +1 -1
  10. package/dist/api/resources/companies.d.ts +4 -4
  11. package/dist/api/resources/companies.d.ts.map +1 -1
  12. package/dist/api/resources/deals.d.ts +6 -6
  13. package/dist/api/resources/deals.d.ts.map +1 -1
  14. package/dist/api/resources/decisions.d.ts +3 -3
  15. package/dist/api/resources/decisions.d.ts.map +1 -1
  16. package/dist/api/resources/forms.d.ts +3 -3
  17. package/dist/api/resources/forms.d.ts.map +1 -1
  18. package/dist/api/resources/handbookPages.d.ts +3 -3
  19. package/dist/api/resources/handbookPages.d.ts.map +1 -1
  20. package/dist/api/resources/notes.d.ts +2 -2
  21. package/dist/api/resources/notes.d.ts.map +1 -1
  22. package/dist/api/resources/opportunities.d.ts +6 -6
  23. package/dist/api/resources/opportunities.d.ts.map +1 -1
  24. package/dist/api/resources/partnerships.d.ts +7 -7
  25. package/dist/api/resources/partnerships.d.ts.map +1 -1
  26. package/dist/api/resources/people.d.ts +4 -4
  27. package/dist/api/resources/people.d.ts.map +1 -1
  28. package/dist/api/resources/planItems.d.ts +6 -6
  29. package/dist/api/resources/planItems.d.ts.map +1 -1
  30. package/dist/api/resources/positions.d.ts +3 -3
  31. package/dist/api/resources/positions.d.ts.map +1 -1
  32. package/dist/api/resources/raises.d.ts +6 -6
  33. package/dist/api/resources/raises.d.ts.map +1 -1
  34. package/dist/api/resources/roles.d.ts +3 -3
  35. package/dist/api/resources/roles.d.ts.map +1 -1
  36. package/dist/api/resources/webhooks.d.ts +2 -2
  37. package/dist/api/resources/webhooks.d.ts.map +1 -1
  38. package/dist/app/KelpieApp.d.ts.map +1 -1
  39. package/dist/app/KelpieApp.js +16 -1
  40. package/dist/app/KelpieApp.js.map +1 -1
  41. package/dist/components/DataTable.d.ts +10 -10
  42. package/dist/components/DataTable.d.ts.map +1 -1
  43. package/dist/components/EntitySearch.d.ts +10 -10
  44. package/dist/components/EntitySearch.d.ts.map +1 -1
  45. package/dist/components/KanbanBoard.d.ts +2 -2
  46. package/dist/components/KanbanBoard.d.ts.map +1 -1
  47. package/dist/components/Shell.d.ts.map +1 -1
  48. package/dist/components/Shell.js +3 -16
  49. package/dist/components/Shell.js.map +1 -1
  50. package/dist/index.d.ts +3 -3
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2 -2
  53. package/dist/index.js.map +1 -1
  54. package/dist/pages/account/AccountLayout.d.ts.map +1 -1
  55. package/dist/pages/account/AccountLayout.js +15 -7
  56. package/dist/pages/account/AccountLayout.js.map +1 -1
  57. package/dist/registry/context.d.ts +1 -2
  58. package/dist/registry/context.d.ts.map +1 -1
  59. package/dist/registry/context.js +0 -3
  60. package/dist/registry/context.js.map +1 -1
  61. package/dist/registry/contributions.d.ts +1 -12
  62. package/dist/registry/contributions.d.ts.map +1 -1
  63. package/dist/registry/contributions.js +0 -7
  64. package/dist/registry/contributions.js.map +1 -1
  65. package/dist/registry/registry.d.ts +1 -3
  66. package/dist/registry/registry.d.ts.map +1 -1
  67. package/dist/registry/registry.js +0 -6
  68. package/dist/registry/registry.js.map +1 -1
  69. package/dist/registry/visibleNav.d.ts +3 -0
  70. package/dist/registry/visibleNav.d.ts.map +1 -0
  71. package/dist/registry/visibleNav.js +27 -0
  72. package/dist/registry/visibleNav.js.map +1 -0
  73. package/package.json +2 -2
  74. package/src/api/ApiProvider.tsx +3 -3
  75. package/src/api/client.ts +4 -1
  76. package/src/api/resources/agentTasks.ts +2 -2
  77. package/src/api/resources/candidates.ts +4 -4
  78. package/src/api/resources/companies.ts +4 -4
  79. package/src/api/resources/deals.ts +6 -6
  80. package/src/api/resources/decisions.ts +3 -3
  81. package/src/api/resources/forms.ts +3 -3
  82. package/src/api/resources/handbookPages.ts +3 -3
  83. package/src/api/resources/notes.ts +2 -2
  84. package/src/api/resources/opportunities.ts +6 -6
  85. package/src/api/resources/partnerships.ts +7 -7
  86. package/src/api/resources/people.ts +4 -4
  87. package/src/api/resources/planItems.ts +6 -6
  88. package/src/api/resources/positions.ts +3 -3
  89. package/src/api/resources/raises.ts +6 -6
  90. package/src/api/resources/roles.ts +3 -3
  91. package/src/api/resources/webhooks.ts +2 -2
  92. package/src/app/KelpieApp.tsx +26 -1
  93. package/src/components/DataTable.tsx +12 -12
  94. package/src/components/EntitySearch.tsx +10 -10
  95. package/src/components/KanbanBoard.tsx +2 -2
  96. package/src/components/Shell.tsx +3 -22
  97. package/src/index.ts +1 -4
  98. package/src/pages/account/AccountLayout.tsx +20 -9
  99. package/src/registry/context.ts +0 -5
  100. package/src/registry/contributions.ts +1 -20
  101. package/src/registry/registry.ts +0 -11
  102. package/src/registry/visibleNav.ts +38 -0
@@ -0,0 +1,27 @@
1
+ import { useModuleSettings } from '../api/resources/moduleSettings.js';
2
+ import { inSlotOrder } from './registry.js';
3
+ /**
4
+ * Core's nav items merged with a slot's module contributions, ordered, with the
5
+ * entries belonging to a disabled module removed.
6
+ *
7
+ * One hook rather than the rule written out at each nav surface. The shell's
8
+ * sidebar and the account tab strip both render a slot, and a workspace that
9
+ * switched a module off should stop seeing it in both. Two copies of this
10
+ * filter is exactly the sort of pair that drifts, and the symptom is a tab that
11
+ * survives its module being disabled and leads to a page answering 403.
12
+ */
13
+ /**
14
+ * A nav item whose id does not match its module id. Everything else looks itself
15
+ * up directly, which is what keeps a toggleable module's own nav entry working
16
+ * with no change here when one is added later.
17
+ */
18
+ const NAV_ID_TO_MODULE_ID = {
19
+ fundraising: 'raises',
20
+ data: 'import-export',
21
+ };
22
+ export function useVisibleNavItems(coreItems, moduleItems) {
23
+ const { settings } = useModuleSettings();
24
+ const disabledModuleIds = new Set(settings.filter((setting) => !setting.enabled).map((setting) => setting.moduleId));
25
+ return inSlotOrder([...coreItems, ...moduleItems]).filter((item) => !disabledModuleIds.has(NAV_ID_TO_MODULE_ID[item.id] ?? item.id));
26
+ }
27
+ //# sourceMappingURL=visibleNav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visibleNav.js","sourceRoot":"","sources":["../../src/registry/visibleNav.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAA;AAEtE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAE3C;;;;;;;;;GASG;AAEH;;;;GAIG;AACH,MAAM,mBAAmB,GAAqC;IAC5D,WAAW,EAAE,QAAQ;IACrB,IAAI,EAAE,eAAe;CACtB,CAAA;AAED,MAAM,UAAU,kBAAkB,CAChC,SAA6B,EAC7B,WAA+B;IAE/B,MAAM,EAAE,QAAQ,EAAE,GAAG,iBAAiB,EAAE,CAAA;IACxC,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAC/B,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAClF,CAAA;IAED,OAAO,WAAW,CAAC,CAAC,GAAG,SAAS,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,MAAM,CACvD,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,CAC1E,CAAA;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kelpie/ui",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "React UI for Kelpie: components, pages, API client, and the extension registry an assembly composes modules through.",
5
5
  "keywords": [
6
6
  "kelpie",
@@ -48,7 +48,7 @@
48
48
  "@dnd-kit/core": "^6.3.1",
49
49
  "@dnd-kit/sortable": "^10.0.0",
50
50
  "@dnd-kit/utilities": "^3.2.2",
51
- "@kelpie/schemas": "^0.3.0",
51
+ "@kelpie/schemas": "^0.4.0",
52
52
  "@tanstack/react-query": "^5.101.4",
53
53
  "react-markdown": "^10.1.0",
54
54
  "react-router": "^8.3.0",
@@ -10,10 +10,10 @@ import { createQueryClient } from './queryClient.ts'
10
10
 
11
11
  export interface ApiProviderProps {
12
12
  /** Origin plus base path, e.g. `/v1` in the browser. */
13
- readonly baseUrl?: string
13
+ readonly baseUrl?: string | undefined
14
14
  /** A prepared client, for tests and for assemblies that construct their own. */
15
- readonly client?: ApiClient
16
- readonly queryClient?: QueryClient
15
+ readonly client?: ApiClient | undefined
16
+ readonly queryClient?: QueryClient | undefined
17
17
  readonly children: ReactNode
18
18
  }
19
19
 
package/src/api/client.ts CHANGED
@@ -173,7 +173,10 @@ export function createApiClient(options: ApiClientOptions): ApiClient {
173
173
  method,
174
174
  credentials: 'same-origin',
175
175
  headers: headersFor(body),
176
- body: body === undefined ? undefined : JSON.stringify(body),
176
+ // Spread rather than `body: undefined`. `RequestInit` comes from the DOM
177
+ // library, where `body` is optional and does not accept an explicit
178
+ // `undefined`, and it is not ours to widen. A GET carries no body at all.
179
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
177
180
  })
178
181
  }
179
182
 
@@ -106,8 +106,8 @@ const runs = createReadOnlyResourceHooks<AgentRun>({
106
106
  })
107
107
 
108
108
  export interface AgentRunFilters {
109
- readonly agentId?: string
110
- readonly status?: AgentRunStatus
109
+ readonly agentId?: string | undefined
110
+ readonly status?: AgentRunStatus | undefined
111
111
  }
112
112
 
113
113
  export function useAgentRuns(
@@ -32,12 +32,12 @@ const candidates = createResourceHooks<Candidate, CreateCandidateInput, Candidat
32
32
  /** The documented filters on `GET /v1/candidates`. */
33
33
  export interface CandidateFilters {
34
34
  /** Candidacies on any of these roles. Repeats on the wire. */
35
- readonly roleIds?: readonly string[]
35
+ readonly roleIds?: readonly string[] | undefined
36
36
  /** Candidacies held by any of these people. Repeats on the wire. */
37
- readonly personIds?: readonly string[]
37
+ readonly personIds?: readonly string[] | undefined
38
38
  /** Candidacies in any of these pipeline states. Repeats on the wire. */
39
- readonly statuses?: readonly CandidateStatus[]
40
- readonly limit?: number
39
+ readonly statuses?: readonly CandidateStatus[] | undefined
40
+ readonly limit?: number | undefined
41
41
  }
42
42
 
43
43
  function candidateQuery(filters: CandidateFilters): QueryParameters {
@@ -31,12 +31,12 @@ const companies = createResourceHooks<Company, CreateCompanyInput, CompanyInput>
31
31
  /** The documented filters on `GET /v1/companies`. */
32
32
  export interface CompanyFilters {
33
33
  /** Matches name, domain, industry, summary, account type, and tags. */
34
- readonly term?: string
34
+ readonly term?: string | undefined
35
35
  /** Companies where any of these people holds a position. Repeats on the wire. */
36
- readonly personIds?: readonly string[]
37
- readonly limit?: number
36
+ readonly personIds?: readonly string[] | undefined
37
+ readonly limit?: number | undefined
38
38
  /** `field` ascending, `-field` descending. Only `name`, `created_at`, `updated_at` are sortable. */
39
- readonly sort?: string
39
+ readonly sort?: string | undefined
40
40
  }
41
41
 
42
42
  function companyQuery(filters: CompanyFilters): QueryParameters {
@@ -35,16 +35,16 @@ const deals = createResourceHooks<Deal, CreateDealInput, DealInput>({
35
35
  /** The documented filters on `GET /v1/deals`. */
36
36
  export interface DealFilters {
37
37
  /** Matches the deal's name, summary, tags, competitors, and its company's name. */
38
- readonly term?: string
38
+ readonly term?: string | undefined
39
39
  /** Deals at any of these companies. Repeats on the wire. */
40
- readonly companyIds?: readonly string[]
40
+ readonly companyIds?: readonly string[] | undefined
41
41
  /** Deals sitting in any of these stages. Repeats on the wire. */
42
- readonly stageIds?: readonly string[]
42
+ readonly stageIds?: readonly string[] | undefined
43
43
  /** Deals any of these people are on. Repeats on the wire. */
44
- readonly personIds?: readonly string[]
45
- readonly limit?: number
44
+ readonly personIds?: readonly string[] | undefined
45
+ readonly limit?: number | undefined
46
46
  /** `field` ascending, `-field` descending. Only `name`, `created_at`, `updated_at` are sortable. */
47
- readonly sort?: string
47
+ readonly sort?: string | undefined
48
48
  }
49
49
 
50
50
  function dealQuery(filters: DealFilters): QueryParameters {
@@ -29,10 +29,10 @@ const decisions = createResourceHooks<Decision, CreateDecisionInput, DecisionInp
29
29
  })
30
30
 
31
31
  export interface DecisionFilters {
32
- readonly targetType?: RecordTargetType
33
- readonly targetId?: string
32
+ readonly targetType?: RecordTargetType | undefined
33
+ readonly targetId?: string | undefined
34
34
  /** Matches the body, the rationale, the target type, and the target's name. */
35
- readonly term?: string
35
+ readonly term?: string | undefined
36
36
  }
37
37
 
38
38
  function decisionQuery(filters: DecisionFilters): QueryParameters {
@@ -60,10 +60,10 @@ const forms = createResourceHooks<Form, CreateFormInput, FormSettingsInput>({
60
60
 
61
61
  export interface FormFilters {
62
62
  /** Matches a form's name and its description. */
63
- readonly term?: string
64
- readonly status?: 'active' | 'paused'
63
+ readonly term?: string | undefined
64
+ readonly status?: 'active' | 'paused' | undefined
65
65
  /** `field` ascending, `-field` descending. Only `name`, `created_at`, `updated_at` are sortable. */
66
- readonly sort?: string
66
+ readonly sort?: string | undefined
67
67
  }
68
68
 
69
69
  function formQuery(filters: FormFilters): QueryParameters {
@@ -26,10 +26,10 @@ const pages = createResourceHooks<HandbookPage, CreateHandbookPageInput, Handboo
26
26
  /** The documented filters on `GET /v1/handbook_pages`. */
27
27
  export interface HandbookPageFilters {
28
28
  /** Matches a page's title and its body. A handbook is searched for what it says. */
29
- readonly term?: string
29
+ readonly term?: string | undefined
30
30
  /** Pages at these slugs. Repeats on the wire. */
31
- readonly slugs?: readonly string[]
32
- readonly limit?: number
31
+ readonly slugs?: readonly string[] | undefined
32
+ readonly limit?: number | undefined
33
33
  }
34
34
 
35
35
  function handbookQuery(filters: HandbookPageFilters): QueryParameters {
@@ -32,8 +32,8 @@ export interface NoteFilters {
32
32
  readonly targetType: RecordTargetType
33
33
  /** Notes on any of these records. Repeats on the wire; all one target type. */
34
34
  readonly targetIds: readonly string[]
35
- readonly pinned?: boolean
36
- readonly limit?: number
35
+ readonly pinned?: boolean | undefined
36
+ readonly limit?: number | undefined
37
37
  }
38
38
 
39
39
  function noteQuery(filters: NoteFilters): QueryParameters {
@@ -34,16 +34,16 @@ const opportunities = createResourceHooks<Opportunity, CreateOpportunityInput, O
34
34
  /** The documented filters on `GET /v1/opportunities`. */
35
35
  export interface OpportunityFilters {
36
36
  /** Matches the opportunity's name, kind, summary, tags, and its company's name. */
37
- readonly term?: string
37
+ readonly term?: string | undefined
38
38
  /** Exact kinds ("Grant", "Accelerator", …). Repeats on the wire. */
39
- readonly kinds?: readonly string[]
39
+ readonly kinds?: readonly string[] | undefined
40
40
  /** Opportunities at any of these companies. Repeats on the wire. */
41
- readonly companyIds?: readonly string[]
41
+ readonly companyIds?: readonly string[] | undefined
42
42
  /** Opportunities sitting in any of these stages. Repeats on the wire. */
43
- readonly stageIds?: readonly string[]
44
- readonly limit?: number
43
+ readonly stageIds?: readonly string[] | undefined
44
+ readonly limit?: number | undefined
45
45
  /** `field` ascending, `-field` descending. Only `name`, `created_at`, `updated_at` are sortable. */
46
- readonly sort?: string
46
+ readonly sort?: string | undefined
47
47
  }
48
48
 
49
49
  function opportunityQuery(filters: OpportunityFilters): QueryParameters {
@@ -36,18 +36,18 @@ const partnerships = createResourceHooks<Partnership, CreatePartnershipInput, Pa
36
36
  /** The documented filters on `GET /v1/partnerships`. */
37
37
  export interface PartnershipFilters {
38
38
  /** Matches the partnership's name, kind, summary, tags, and its company's name. */
39
- readonly term?: string
39
+ readonly term?: string | undefined
40
40
  /** Exact kinds ("Integration", "Co-marketing", …). Repeats on the wire. */
41
- readonly kinds?: readonly string[]
41
+ readonly kinds?: readonly string[] | undefined
42
42
  /** Partnerships with any of these companies. Repeats on the wire. */
43
- readonly companyIds?: readonly string[]
43
+ readonly companyIds?: readonly string[] | undefined
44
44
  /** Partnerships sitting in any of these stages. Repeats on the wire. */
45
- readonly stageIds?: readonly string[]
45
+ readonly stageIds?: readonly string[] | undefined
46
46
  /** Partnerships any of these people are on. Repeats on the wire. */
47
- readonly personIds?: readonly string[]
48
- readonly limit?: number
47
+ readonly personIds?: readonly string[] | undefined
48
+ readonly limit?: number | undefined
49
49
  /** `field` ascending, `-field` descending. Only `name`, `created_at`, `updated_at` are sortable. */
50
- readonly sort?: string
50
+ readonly sort?: string | undefined
51
51
  }
52
52
 
53
53
  function partnershipQuery(filters: PartnershipFilters): QueryParameters {
@@ -31,12 +31,12 @@ const people = createResourceHooks<Person, CreatePersonInput, PersonInput>({
31
31
  /** The documented filters on `GET /v1/people`. There is no generic filter DSL. */
32
32
  export interface PeopleFilters {
33
33
  /** Matches name, email, summary, tags, and the titles and companies they hold. */
34
- readonly term?: string
34
+ readonly term?: string | undefined
35
35
  /** People holding a position at any of these companies. Repeats on the wire. */
36
- readonly companyIds?: readonly string[]
37
- readonly limit?: number
36
+ readonly companyIds?: readonly string[] | undefined
37
+ readonly limit?: number | undefined
38
38
  /** `field` ascending, `-field` descending. Only `name`, `created_at`, `updated_at` are sortable. */
39
- readonly sort?: string
39
+ readonly sort?: string | undefined
40
40
  }
41
41
 
42
42
  function peopleQuery(filters: PeopleFilters): QueryParameters {
@@ -35,15 +35,15 @@ const planItems = createResourceHooks<PlanItem, CreatePlanItemInput, PlanItemInp
35
35
  export const MAX_PAGE_SIZE = 200
36
36
 
37
37
  export interface PlanItemFilters {
38
- readonly targetType?: PipelineKind
38
+ readonly targetType?: PipelineKind | undefined
39
39
  /** The records to read steps for. Repeats on the wire: `?target_id=a&target_id=b`. */
40
- readonly targetIds?: readonly string[]
41
- readonly statuses?: readonly PlanItemStatus[]
40
+ readonly targetIds?: readonly string[] | undefined
41
+ readonly statuses?: readonly PlanItemStatus[] | undefined
42
42
  /** Inclusive `YYYY-MM-DD` bounds. The calendar asks for one month with the pair. */
43
- readonly from?: string
44
- readonly to?: string
43
+ readonly from?: string | undefined
44
+ readonly to?: string | undefined
45
45
  /** Page size. Worth setting only when the filter already bounds the answer. */
46
- readonly limit?: number
46
+ readonly limit?: number | undefined
47
47
  }
48
48
 
49
49
  function planItemQuery(filters: PlanItemFilters): QueryParameters {
@@ -37,10 +37,10 @@ const positions = createResourceHooks<Position, CreatePositionInput, PositionTit
37
37
 
38
38
  export interface PositionFilters {
39
39
  /** Positions held by any of these people. Repeats on the wire. */
40
- readonly personIds?: readonly string[]
40
+ readonly personIds?: readonly string[] | undefined
41
41
  /** Positions held at any of these companies. Repeats on the wire. */
42
- readonly companyIds?: readonly string[]
43
- readonly limit?: number
42
+ readonly companyIds?: readonly string[] | undefined
43
+ readonly limit?: number | undefined
44
44
  }
45
45
 
46
46
  function positionQuery(filters: PositionFilters): QueryParameters {
@@ -35,16 +35,16 @@ const raises = createResourceHooks<Raise, CreateRaiseInput, RaiseInput>({
35
35
  /** The documented filters on `GET /v1/raises`. */
36
36
  export interface RaiseFilters {
37
37
  /** Matches the raise's name, summary, tags, and its firm's name. */
38
- readonly term?: string
38
+ readonly term?: string | undefined
39
39
  /** Raises with any of these firms. Repeats on the wire. */
40
- readonly companyIds?: readonly string[]
40
+ readonly companyIds?: readonly string[] | undefined
41
41
  /** Raises sitting in any of these stages. Repeats on the wire. */
42
- readonly stageIds?: readonly string[]
42
+ readonly stageIds?: readonly string[] | undefined
43
43
  /** Raises any of these people are key on. Repeats on the wire. */
44
- readonly personIds?: readonly string[]
45
- readonly limit?: number
44
+ readonly personIds?: readonly string[] | undefined
45
+ readonly limit?: number | undefined
46
46
  /** `field` ascending, `-field` descending. Only `name`, `created_at`, `updated_at` are sortable. */
47
- readonly sort?: string
47
+ readonly sort?: string | undefined
48
48
  }
49
49
 
50
50
  function raiseQuery(filters: RaiseFilters): QueryParameters {
@@ -31,10 +31,10 @@ const roles = createResourceHooks<Role, CreateRoleInput, RoleInput>({
31
31
  /** The documented filters on `GET /v1/roles`. */
32
32
  export interface RoleFilters {
33
33
  /** Matches the role's title, which is what the mockup's filter box matches. */
34
- readonly term?: string
34
+ readonly term?: string | undefined
35
35
  /** Roles in any of these statuses. Repeats on the wire. */
36
- readonly statuses?: readonly RoleStatus[]
37
- readonly limit?: number
36
+ readonly statuses?: readonly RoleStatus[] | undefined
37
+ readonly limit?: number | undefined
38
38
  }
39
39
 
40
40
  function roleQuery(filters: RoleFilters): QueryParameters {
@@ -41,7 +41,7 @@ const webhooks = createResourceHooks<Webhook, CreateWebhookInput, WebhookInput>(
41
41
  })
42
42
 
43
43
  export interface WebhookFilters {
44
- readonly status?: Webhook['status']
44
+ readonly status?: Webhook['status'] | undefined
45
45
  }
46
46
 
47
47
  function webhookQuery(filters: WebhookFilters): QueryParameters {
@@ -61,7 +61,7 @@ export function useDeleteWebhook(): MutationResult<string, void> {
61
61
  }
62
62
 
63
63
  export interface WebhookDeliveryFilters {
64
- readonly status?: WebhookDelivery['status']
64
+ readonly status?: WebhookDelivery['status'] | undefined
65
65
  }
66
66
 
67
67
  /**
@@ -47,6 +47,7 @@ import { HandbookStepPage } from '../pages/onboarding/HandbookStepPage.tsx'
47
47
  import { InvitesStepPage } from '../pages/onboarding/InvitesStepPage.tsx'
48
48
  import { WorkspaceStepPage } from '../pages/onboarding/WorkspaceStepPage.tsx'
49
49
  import { UiExtensionProvider } from '../registry/UiExtensionProvider.tsx'
50
+ import type { RouteContribution } from '../registry/contributions.ts'
50
51
  import { useModuleRoutes } from '../registry/context.ts'
51
52
  import type { UiExtensions } from '../registry/registry.ts'
52
53
  import { SessionGate } from './SessionGate.tsx'
@@ -80,8 +81,25 @@ export function KelpieApp({ extensions, baseUrl }: KelpieAppProps): React.JSX.El
80
81
  )
81
82
  }
82
83
 
84
+ /**
85
+ * A module route under `account/` renders inside `AccountLayout`, everything
86
+ * else directly under the shell.
87
+ *
88
+ * The prefix is the whole rule, and it is what makes `nav.account` mean
89
+ * anything: a module contributing a tab there needs its page to keep the tab
90
+ * strip, and a route mounted as a sibling of `account` would lose it the moment
91
+ * the tab was clicked. Core's own account pages are nested for the same reason.
92
+ */
93
+ const ACCOUNT_PREFIX = 'account/'
94
+
95
+ function isAccountRoute(route: RouteContribution): boolean {
96
+ return route.path.startsWith(ACCOUNT_PREFIX)
97
+ }
98
+
83
99
  function AppRoutes(): React.JSX.Element {
84
100
  const moduleRoutes = useModuleRoutes()
101
+ const accountRoutes = moduleRoutes.filter(isAccountRoute)
102
+ const shellRoutes = moduleRoutes.filter((route) => !isAccountRoute(route))
85
103
 
86
104
  return (
87
105
  <Routes>
@@ -146,8 +164,15 @@ function AppRoutes(): React.JSX.Element {
146
164
  <Route path="profile" element={<ProfilePage />} />
147
165
  <Route path="security" element={<SecurityPage />} />
148
166
  <Route path="preferences" element={<PreferencesPage />} />
167
+ {accountRoutes.map((route) => (
168
+ <Route
169
+ key={route.path}
170
+ path={route.path.slice(ACCOUNT_PREFIX.length)}
171
+ element={route.element}
172
+ />
173
+ ))}
149
174
  </Route>
150
- {moduleRoutes.map((route) => (
175
+ {shellRoutes.map((route) => (
151
176
  <Route key={route.path} path={route.path} element={route.element} />
152
177
  ))}
153
178
  {/* Last so every named route above wins first. Nested here rather than
@@ -3,10 +3,10 @@ import type { ReactNode } from 'react'
3
3
  export interface Column<TRow> {
4
4
  readonly key: string
5
5
  readonly header: string
6
- readonly className?: string
6
+ readonly className?: string | undefined
7
7
  readonly render: (row: TRow) => ReactNode
8
8
  /** The `?sort=` field this column drives. Omit for a column with no server-side sort. */
9
- readonly sortKey?: string
9
+ readonly sortKey?: string | undefined
10
10
  }
11
11
 
12
12
  export interface DataTableGroup<TRow> {
@@ -22,19 +22,19 @@ export interface EmptyStateAction {
22
22
 
23
23
  export interface DataTableProps<TRow> {
24
24
  readonly columns: readonly Column<TRow>[]
25
- readonly rows?: readonly TRow[]
26
- readonly groups?: readonly DataTableGroup<TRow>[]
27
- readonly onRowClick?: (row: TRow) => void
28
- readonly emptyMessage?: string
25
+ readonly rows?: readonly TRow[] | undefined
26
+ readonly groups?: readonly DataTableGroup<TRow>[] | undefined
27
+ readonly onRowClick?: ((row: TRow) => void) | undefined
28
+ readonly emptyMessage?: string | undefined
29
29
  /** Replaces the default "Add a record to get started." line under `emptyMessage`. */
30
- readonly emptyDescription?: string
30
+ readonly emptyDescription?: string | undefined
31
31
  /** A button rendered in the empty state. Omit when there is nothing to do about it — a filter miss, not a genuinely empty list. */
32
- readonly emptyAction?: EmptyStateAction
32
+ readonly emptyAction?: EmptyStateAction | undefined
33
33
  readonly getRowId: (row: TRow) => string
34
34
  /** Current `?sort=` value: `field` ascending, `-field` descending, `undefined` for the resource's default. */
35
- readonly sort?: string
35
+ readonly sort?: string | undefined
36
36
  /** Fires with the next `?sort=` value when a sortable header is clicked. Required for any column to be clickable. */
37
- readonly onSortChange?: (sort: string | undefined) => void
37
+ readonly onSortChange?: ((sort: string | undefined) => void) | undefined
38
38
  }
39
39
 
40
40
  type SortDirection = 'asc' | 'desc'
@@ -162,7 +162,7 @@ function GroupRows<TRow>({
162
162
  }: {
163
163
  readonly group: DataTableGroup<TRow>
164
164
  readonly columns: readonly Column<TRow>[]
165
- readonly onRowClick?: (row: TRow) => void
165
+ readonly onRowClick?: ((row: TRow) => void) | undefined
166
166
  readonly getRowId: (row: TRow) => string
167
167
  }): React.JSX.Element {
168
168
  return (
@@ -189,7 +189,7 @@ function DataRow<TRow>({
189
189
  }: {
190
190
  readonly row: TRow
191
191
  readonly columns: readonly Column<TRow>[]
192
- readonly onRowClick?: (row: TRow) => void
192
+ readonly onRowClick?: ((row: TRow) => void) | undefined
193
193
  }): React.JSX.Element {
194
194
  return (
195
195
  <tr
@@ -13,7 +13,7 @@ import { useEffect, useId, useMemo, useRef, useState } from 'react'
13
13
  export interface SearchOption {
14
14
  readonly id: string
15
15
  readonly label: string
16
- readonly meta?: string
16
+ readonly meta?: string | undefined
17
17
  }
18
18
 
19
19
  export interface EntitySearchProps {
@@ -21,17 +21,17 @@ export interface EntitySearchProps {
21
21
  readonly value: string
22
22
  readonly onChange: (id: string) => void
23
23
  /** Set to run the search server-side. Receives what has been typed, trimmed of nothing. */
24
- readonly onQueryChange?: (query: string) => void
24
+ readonly onQueryChange?: ((query: string) => void) | undefined
25
25
  /** When set, typing a name that is not an exact match offers a create row. */
26
- readonly onCreate?: (label: string) => void
27
- readonly createLabel?: (query: string) => string
28
- readonly placeholder?: string
29
- readonly emptyMessage?: string
26
+ readonly onCreate?: ((label: string) => void) | undefined
27
+ readonly createLabel?: ((query: string) => string) | undefined
28
+ readonly placeholder?: string | undefined
29
+ readonly emptyMessage?: string | undefined
30
30
  /** Most rows shown at once. The list says so when there are more. */
31
- readonly limit?: number
32
- readonly required?: boolean
33
- readonly className?: string
34
- readonly size?: 'sm' | 'md'
31
+ readonly limit?: number | undefined
32
+ readonly required?: boolean | undefined
33
+ readonly className?: string | undefined
34
+ readonly size?: 'sm' | 'md' | undefined
35
35
  }
36
36
 
37
37
  export function EntitySearch({
@@ -28,8 +28,8 @@ export interface KanbanCard {
28
28
  readonly id: string
29
29
  readonly stage: string
30
30
  readonly title: string
31
- readonly meta?: string
32
- readonly valueLabel?: string
31
+ readonly meta?: string | undefined
32
+ readonly valueLabel?: string | undefined
33
33
  readonly href: string
34
34
  }
35
35
 
@@ -2,13 +2,12 @@ import { useEffect, useRef, useState } from 'react'
2
2
  import { NavLink, Outlet, useLocation, useNavigate } from 'react-router'
3
3
 
4
4
  import { useAccount, useTheme } from '../api/resources/account.ts'
5
- import { useModuleSettings } from '../api/resources/moduleSettings.ts'
6
5
  import { useLogOut } from '../api/resources/session.ts'
7
6
  import { initialsOf } from '../lib/names.ts'
8
7
  import type { ThemePreference } from '../lib/theme.ts'
9
8
  import { useNavItems } from '../registry/context.ts'
10
9
  import type { NavItem } from '../registry/contributions.ts'
11
- import { inSlotOrder } from '../registry/registry.ts'
10
+ import { useVisibleNavItems } from '../registry/visibleNav.ts'
12
11
  import { ErrorBoundary } from './ErrorBoundary.tsx'
13
12
 
14
13
  /**
@@ -51,16 +50,6 @@ const CORE_NAV: readonly NavItem[] = [
51
50
  */
52
51
  const TOP_LEVEL_NAV_IDS: ReadonlySet<string> = new Set(['dashboard', 'handbook', 'planning', 'decisions'])
53
52
 
54
- /**
55
- * A nav item whose id does not match its module id. Everything else looks
56
- * itself up directly, which is what keeps a toggleable module's own nav entry
57
- * working with no change here when one is added later.
58
- */
59
- const NAV_ID_TO_MODULE_ID: Readonly<Record<string, string>> = {
60
- fundraising: 'raises',
61
- data: 'import-export',
62
- }
63
-
64
53
  function linkClass({ isActive }: { isActive: boolean }): string {
65
54
  return [
66
55
  'block rounded-md px-2 py-1 text-[13px] transition-colors duration-100',
@@ -89,12 +78,6 @@ export function Shell(): React.JSX.Element {
89
78
  const logOut = useLogOut()
90
79
  const moduleNav = useNavItems('primary')
91
80
  const moduleAdminNav = useNavItems('admin')
92
- const { settings } = useModuleSettings()
93
- const disabledModuleIds = new Set(
94
- settings.filter((setting) => !setting.enabled).map((setting) => setting.moduleId),
95
- )
96
- const isModuleHidden = (navId: string): boolean =>
97
- disabledModuleIds.has(NAV_ID_TO_MODULE_ID[navId] ?? navId)
98
81
  const [menuOpen, setMenuOpen] = useState(false)
99
82
  // The same preference the account page writes, so this button and that page
100
83
  // cannot disagree about which theme the account is on.
@@ -128,12 +111,10 @@ export function Shell(): React.JSX.Element {
128
111
  }
129
112
  }, [menuOpen])
130
113
 
131
- const navItems = inSlotOrder([...CORE_NAV, ...moduleNav]).filter((item) => !isModuleHidden(item.id))
114
+ const navItems = useVisibleNavItems(CORE_NAV, moduleNav)
132
115
  const topLevelItems = navItems.filter((item) => TOP_LEVEL_NAV_IDS.has(item.id))
133
116
  const crmItems = navItems.filter((item) => !TOP_LEVEL_NAV_IDS.has(item.id))
134
- const adminItems = inSlotOrder([...CORE_ADMIN_NAV, ...moduleAdminNav]).filter(
135
- (item) => !isModuleHidden(item.id),
136
- )
117
+ const adminItems = useVisibleNavItems(CORE_ADMIN_NAV, moduleAdminNav)
137
118
 
138
119
  return (
139
120
  <div className="flex min-h-screen bg-surface">
package/src/index.ts CHANGED
@@ -155,12 +155,10 @@ export type { DueBucketId, PlanAttention as PlanAttentionSummary } from './lib/p
155
155
  export { applyTheme, getStoredTheme, resolveTheme, setStoredTheme, watchSystemTheme } from './lib/theme.ts'
156
156
  export type { ThemePreference } from './lib/theme.ts'
157
157
 
158
- export { EXTENSIBLE_RECORD_TYPES, INTEGRATION_CATEGORIES, NAV_SLOTS } from './registry/contributions.ts'
158
+ export { EXTENSIBLE_RECORD_TYPES, NAV_SLOTS } from './registry/contributions.ts'
159
159
  export type {
160
160
  DashboardCard,
161
161
  ExtensibleRecordType,
162
- IntegrationCategory,
163
- IntegrationProvider,
164
162
  NavItem,
165
163
  NavSlot,
166
164
  RecordContext,
@@ -177,7 +175,6 @@ export type { UiExtensions, UiModule, UiModuleContext } from './registry/registr
177
175
 
178
176
  export {
179
177
  useDashboardCards,
180
- useIntegrationProviders,
181
178
  useModuleRoutes,
182
179
  useNavItems,
183
180
  useOverridable,