@campminder/menu-tree 0.0.1 → 0.1.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.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  /**
2
- * Placeholder export for @campminder/menu-tree.
2
+ * @campminder/menu-tree — the ERP-agnostic menu data model for the `nav` Domain
3
+ * System: the structural types and the JSON Schema they project.
3
4
  *
4
- * The real structural model (MenuTree, MenuCategory, MenuSubCategory,
5
- * MenuLeaf, VisibilityRule) ships in Floor 4 of
6
- * docs/plans/2026-09-02-feature-menu-model-and-drift-guard-plan.md. This
7
- * floor proves the package builds, exports, and is tested — nothing more.
5
+ * The schema is published as a subpath export rather than a JS value, so a
6
+ * consumer validates against the same file `nav` ships:
7
+ *
8
+ * import schema from "@campminder/menu-tree/menu-tree.schema.json";
8
9
  */
9
- export declare const VERSION = "0.0.1";
10
+ export type { MenuCategory, MenuLeaf, MenuSubCategory, MenuTree, VisibilityRule, } from "./model.js";
11
+ /** Kept as a runtime export so the cross-host consumption smoke test has something to assert. */
12
+ export declare const VERSION = "0.1.0";
package/dist/index.js CHANGED
@@ -1,9 +1,2 @@
1
- /**
2
- * Placeholder export for @campminder/menu-tree.
3
- *
4
- * The real structural model (MenuTree, MenuCategory, MenuSubCategory,
5
- * MenuLeaf, VisibilityRule) ships in Floor 4 of
6
- * docs/plans/2026-09-02-feature-menu-model-and-drift-guard-plan.md. This
7
- * floor proves the package builds, exports, and is tested — nothing more.
8
- */
9
- export const VERSION = "0.0.1";
1
+ /** Kept as a runtime export so the cross-host consumption smoke test has something to assert. */
2
+ export const VERSION = "0.1.0";
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The structural menu-tree types — the TypeScript projection of
3
+ * menu-tree.schema.json, and the data model ADR D4 assigns to the `nav` Domain
4
+ * System. Ported from `portals`' packages/components/src/menu-tree.ts on
5
+ * 2026-09-08 (Floor 4).
6
+ *
7
+ * Nothing here is Campminder-specific and nothing here may become so. A tree
8
+ * *instance* (campminder.json), and the host context a filter evaluates against
9
+ * (FilterContext), both stay in the app shell per ADR D6. `nav` has no
10
+ * dependency on `portals`, ever.
11
+ */
12
+ /**
13
+ * Visibility rules, evaluated by the host against its own context. Each rule is
14
+ * an independent predicate; all present rules must hold for the node to be
15
+ * visible.
16
+ *
17
+ * These are exactly the kinds the host evaluates. A `client` rule is NOT one of
18
+ * them — it was a phantom derived from a stale upstream schema and removed on
19
+ * 2026-09-01. Do not reintroduce it, and do not drop `featureKey`, which the
20
+ * same mistake omitted.
21
+ */
22
+ export type VisibilityRule = {
23
+ kind: "feature";
24
+ featureIds: number[];
25
+ } | {
26
+ kind: "featureKey";
27
+ featureKeys: string[];
28
+ } | {
29
+ kind: "versionBranch";
30
+ mask: string;
31
+ };
32
+ /** A navigable endpoint. The only node type carrying a required `href`. */
33
+ export interface MenuLeaf {
34
+ id: string;
35
+ /** May carry an unresolved terminology token, e.g. `{{Bunk:Proper:Default}}`. */
36
+ label: string;
37
+ href: string;
38
+ sortOrder?: number;
39
+ newTab?: boolean;
40
+ popup?: boolean;
41
+ popupProperties?: string;
42
+ paperless?: boolean;
43
+ visibility?: VisibilityRule[];
44
+ }
45
+ export interface MenuSubCategory {
46
+ id: string;
47
+ label: string;
48
+ href?: string;
49
+ sortOrder?: number;
50
+ items: MenuLeaf[];
51
+ visibility?: VisibilityRule[];
52
+ }
53
+ /**
54
+ * `sortOrder` is required here and optional on the nodes below, matching the
55
+ * schema. See the schema's `$comment` for why that asymmetry is deliberate.
56
+ */
57
+ export interface MenuCategory {
58
+ id: string;
59
+ label: string;
60
+ icon?: string;
61
+ href?: string;
62
+ sortOrder: number;
63
+ subCategories: MenuSubCategory[];
64
+ visibility?: VisibilityRule[];
65
+ }
66
+ /** A whole tree is a list of categories — there is no wrapper object. */
67
+ export type MenuTree = MenuCategory[];
package/dist/model.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The structural menu-tree types — the TypeScript projection of
3
+ * menu-tree.schema.json, and the data model ADR D4 assigns to the `nav` Domain
4
+ * System. Ported from `portals`' packages/components/src/menu-tree.ts on
5
+ * 2026-09-08 (Floor 4).
6
+ *
7
+ * Nothing here is Campminder-specific and nothing here may become so. A tree
8
+ * *instance* (campminder.json), and the host context a filter evaluates against
9
+ * (FilterContext), both stay in the app shell per ADR D6. `nav` has no
10
+ * dependency on `portals`, ever.
11
+ */
12
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@campminder/menu-tree",
3
- "version": "0.0.1",
4
- "description": "Scaffold package that will hold the structural menu-tree types and JSON Schema for the nav Domain System; no menu model is implemented yet.",
3
+ "version": "0.1.0",
4
+ "description": "The ERP-agnostic menu data model for the nav Domain System: the structural menu-tree types and the JSON Schema they project.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://dev.azure.com/CampMinderLLC/CampMinder/_git/nav",
@@ -23,11 +23,14 @@
23
23
  "access": "public"
24
24
  },
25
25
  "devDependencies": {
26
+ "@types/node": "^24",
27
+ "ajv": "^8.17.1",
26
28
  "typescript": "^5.9.3",
27
29
  "vitest": "^2.1.8"
28
30
  },
29
31
  "scripts": {
30
32
  "build": "tsc -p tsconfig.json",
31
- "test": "vitest run"
33
+ "test": "vitest run",
34
+ "typecheck": "tsc -p tsconfig.typecheck.json"
32
35
  }
33
36
  }
@@ -1,6 +1,102 @@
1
1
  {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://campminder.com/schemas/menu-tree.schema.json",
4
- "title": "MenuTree (placeholder — real shape ships in Floor 4)",
5
- "type": "object"
4
+ "title": "MenuTree",
5
+ "description": "The ERP-agnostic shape of a portal navigation tree. A tree instance (one ERP's menu) is NOT described here and does not belong in this package — ADR D6 puts the instance in the app shell.",
6
+ "$comment": "AUTHORITY: the host's C# model — MenuItem in Core/Websites/CampMinder/navigation/NavigationAccessor.cs, and the gates NavigationBuilder actually evaluates. NOT core-js's apps/side-nav/src/data/navigation-data.schema.json, which is stale: it declares a dead `client_ids` and omits the live `feature_keys`. Copying it produced a phantom `client` rule and silently dropped a real gate; corrected 2026-09-01. ONE DELIBERATE DIVERGENCE FROM THE HOST CONTRACT: `sortOrder` is REQUIRED on a category here, where the host leaves it optional. 27 category sort orders were lost in an upstream derivation and nothing caught it, because nothing required them. All 13 categories in the CampMinder tree carry one, so the tightening costs nothing today; it is stated rather than silent so a future portal that legitimately omits one knows to revisit it. `sortOrder` stays optional on sub-categories and leaves, where the real data genuinely omits it (1 of 48 sub-categories and 214 of 249 leaves). Draft-07 rather than 2020-12 so consumers can validate with a default `new Ajv()`.",
7
+ "definitions": {
8
+ "visibilityRule": {
9
+ "$comment": "Exactly the kinds the host evaluates, and no others. Each rule is an independent predicate; all present rules must hold for the node to be visible.",
10
+ "type": "object",
11
+ "oneOf": [
12
+ {
13
+ "properties": {
14
+ "kind": { "const": "feature" },
15
+ "featureIds": { "type": "array", "items": { "type": "integer" } }
16
+ },
17
+ "required": ["kind", "featureIds"],
18
+ "additionalProperties": false
19
+ },
20
+ {
21
+ "properties": {
22
+ "kind": { "const": "featureKey" },
23
+ "featureKeys": { "type": "array", "items": { "type": "string" } }
24
+ },
25
+ "required": ["kind", "featureKeys"],
26
+ "additionalProperties": false
27
+ },
28
+ {
29
+ "properties": {
30
+ "kind": { "const": "versionBranch" },
31
+ "mask": { "type": "string" }
32
+ },
33
+ "required": ["kind", "mask"],
34
+ "additionalProperties": false
35
+ }
36
+ ]
37
+ },
38
+ "menuLeaf": {
39
+ "type": "object",
40
+ "properties": {
41
+ "id": { "type": "string" },
42
+ "label": {
43
+ "type": "string",
44
+ "$comment": "Deliberately unconstrained. Labels may carry unresolved terminology tokens such as `{{Bunk:Proper:Default}}`, which the app shell substitutes per tenant. A pattern here would break that."
45
+ },
46
+ "href": { "type": "string" },
47
+ "sortOrder": { "type": "integer" },
48
+ "newTab": { "type": "boolean" },
49
+ "popup": { "type": "boolean" },
50
+ "popupProperties": { "type": "string" },
51
+ "paperless": { "type": "boolean" },
52
+ "visibility": {
53
+ "type": "array",
54
+ "items": { "$ref": "#/definitions/visibilityRule" }
55
+ }
56
+ },
57
+ "required": ["id", "label", "href"],
58
+ "additionalProperties": false
59
+ },
60
+ "menuSubCategory": {
61
+ "type": "object",
62
+ "properties": {
63
+ "id": { "type": "string" },
64
+ "label": { "type": "string" },
65
+ "href": { "type": "string" },
66
+ "sortOrder": { "type": "integer" },
67
+ "items": {
68
+ "type": "array",
69
+ "items": { "$ref": "#/definitions/menuLeaf" }
70
+ },
71
+ "visibility": {
72
+ "type": "array",
73
+ "items": { "$ref": "#/definitions/visibilityRule" }
74
+ }
75
+ },
76
+ "required": ["id", "label", "items"],
77
+ "additionalProperties": false
78
+ },
79
+ "menuCategory": {
80
+ "type": "object",
81
+ "properties": {
82
+ "id": { "type": "string" },
83
+ "label": { "type": "string" },
84
+ "icon": { "type": "string" },
85
+ "href": { "type": "string" },
86
+ "sortOrder": { "type": "integer" },
87
+ "subCategories": {
88
+ "type": "array",
89
+ "items": { "$ref": "#/definitions/menuSubCategory" }
90
+ },
91
+ "visibility": {
92
+ "type": "array",
93
+ "items": { "$ref": "#/definitions/visibilityRule" }
94
+ }
95
+ },
96
+ "required": ["id", "label", "sortOrder", "subCategories"],
97
+ "additionalProperties": false
98
+ }
99
+ },
100
+ "type": "array",
101
+ "items": { "$ref": "#/definitions/menuCategory" }
6
102
  }