@likec4/core 1.34.2 → 1.36.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 (79) hide show
  1. package/README.md +1 -1
  2. package/dist/builder/index.d.mts +15 -4
  3. package/dist/builder/index.mjs +19 -15
  4. package/dist/compute-view/index.d.mts +3 -3
  5. package/dist/compute-view/index.mjs +16 -13
  6. package/dist/compute-view/relationships-view/index.d.mts +3 -3
  7. package/dist/compute-view/relationships-view/index.mjs +2 -2
  8. package/dist/index.d.mts +3 -3
  9. package/dist/index.mjs +14 -9
  10. package/dist/model/connection/deployment/index.d.mts +5 -5
  11. package/dist/model/connection/deployment/index.mjs +5 -4
  12. package/dist/model/connection/index.d.mts +6 -6
  13. package/dist/model/connection/index.mjs +6 -5
  14. package/dist/model/connection/model/index.d.mts +5 -5
  15. package/dist/model/connection/model/index.mjs +3 -3
  16. package/dist/model/index.d.mts +33 -9
  17. package/dist/model/index.mjs +12 -10
  18. package/dist/shared/{core.DAj3LuFH.d.mts → core.BFb3p5d-.d.mts} +4 -2
  19. package/dist/shared/{core.PqNe5WxQ.mjs → core.BlwZ4tBa.mjs} +98 -92
  20. package/dist/shared/core.BpJu1RrF.mjs +5 -0
  21. package/dist/shared/{core.CJSFKrYf.d.mts → core.Bt3kuGUN.d.mts} +3 -3
  22. package/dist/shared/{core.D5qVzkws.mjs → core.Bus32P0S.mjs} +4 -3
  23. package/dist/shared/{core.4SpVCM9m.mjs → core.BvIGRozf.mjs} +364 -28
  24. package/dist/shared/{core.C80Z37Eu.mjs → core.C1GV0WWV.mjs} +1 -1
  25. package/dist/shared/{core.DXRAwwDB.mjs → core.CaJp8Amg.mjs} +3 -2
  26. package/dist/shared/{core.D56nCp7e.mjs → core.CaaYNQDM.mjs} +1 -3
  27. package/dist/shared/core.CnIyafWn.mjs +3 -0
  28. package/dist/shared/{core.D3xEWj5B.d.mts → core.D8bTI5KC.d.mts} +2 -2
  29. package/dist/shared/{core.Di7__sMg.d.mts → core.DHYd2-VU.d.mts} +3 -3
  30. package/dist/shared/{core.C_C-3c1S.mjs → core.DQj9iv_y.mjs} +3 -5
  31. package/dist/shared/{core.DrS4Bf4y.d.mts → core.DoNP3Yl-.d.mts} +187 -11
  32. package/dist/shared/{core.IWLie-Ma.d.mts → core.DxDn5nB5.d.mts} +24 -6
  33. package/dist/shared/{core.CtKXPqN_.mjs → core.HUONfQfd.mjs} +1 -1
  34. package/dist/shared/{core.BTnf14gD.mjs → core.Y3j3s-oq.mjs} +2 -2
  35. package/dist/shared/{core.Br7Fab9l.mjs → core._jnYNUQ-.mjs} +8 -70
  36. package/dist/shared/{core.XptQjhMA.mjs → core.b2BzqGz0.mjs} +2 -2
  37. package/dist/shared/{core.BMCb0wJI.mjs → core.yaRvRnMW.mjs} +1 -1
  38. package/dist/theme/index.mjs +34 -3510
  39. package/dist/types/_aux.d.mts +1 -1
  40. package/dist/types/index.d.mts +3 -3
  41. package/dist/types/index.mjs +6 -4
  42. package/dist/types/scalar.d.mts +2 -0
  43. package/dist/types/scalar.mjs +68 -0
  44. package/dist/utils/index.d.mts +6 -8
  45. package/dist/utils/index.mjs +5 -5
  46. package/dist/utils/iterable/index.d.mts +18 -1
  47. package/dist/utils/iterable/index.mjs +22 -2
  48. package/package.json +11 -16
  49. package/src/builder/Builder.ts +17 -6
  50. package/src/builder/__snapshots__/Builder-style1.spec.ts.snap +9 -0
  51. package/src/builder/__snapshots__/Builder-style2.compute-model.json5 +3 -0
  52. package/src/builder/__snapshots__/Builder-style2.spec.ts.snap +3 -0
  53. package/src/builder/_types.ts +10 -0
  54. package/src/compute-view/dynamic-view/compute.ts +2 -0
  55. package/src/compute-view/element-view/__test__/fixture.ts +1 -0
  56. package/src/compute-view/element-view/predicates/relation-direct.ts +8 -2
  57. package/src/compute-view/utils/applyCustomRelationProperties.ts +2 -0
  58. package/src/model/DeploymentModel.ts +1 -1
  59. package/src/model/LikeC4Model.ts +162 -13
  60. package/src/model/index.ts +8 -0
  61. package/src/model/types.ts +1 -8
  62. package/src/model/utils.ts +66 -0
  63. package/src/model/view/LikeC4ViewModel.ts +62 -7
  64. package/src/model/view/LikeC4ViewsFolder.ts +151 -0
  65. package/src/theme/__test__/theme-index.ts +29 -0
  66. package/src/theme/element.ts +0 -19
  67. package/src/theme/index.ts +49 -3
  68. package/src/types/_aux.ts +6 -0
  69. package/src/types/index.ts +1 -0
  70. package/src/types/model-data.ts +2 -0
  71. package/src/types/model-dump.ts +8 -1
  72. package/src/types/model-spec.ts +4 -0
  73. package/src/types/project.ts +9 -0
  74. package/src/types/view-computed.ts +1 -1
  75. package/src/types/view-parsed.dynamic.ts +2 -2
  76. package/src/utils/compare-natural.ts +14 -12
  77. package/src/utils/index.ts +1 -0
  78. package/src/utils/iterable/first.ts +48 -0
  79. package/src/utils/iterable/index.ts +1 -0
@@ -1,24 +1,31 @@
1
1
  import { u as u$1 } from './core.DbRvwARP.mjs';
2
- import { p } from './core.BTnf14gD.mjs';
2
+ import { p } from './core.Y3j3s-oq.mjs';
3
3
  import { m as m$2 } from './core.ByrmCl9I.mjs';
4
- import { g as hierarchyLevel, n as nameFromFqn, i as isAncestor, d as commonAncestor, l as sortNaturalByFqn, m as sortParentsFirst, p as parentFqn, b as ancestorsFqn, C, c as compareNatural } from './core.C_C-3c1S.mjs';
4
+ import { i as i$2 } from './core.CpwrZi3D.mjs';
5
+ import { g as hierarchyLevel, n as nameFromFqn, i as isAncestor, d as commonAncestor, l as sortNaturalByFqn, m as sortParentsFirst, p as parentFqn, b as ancestorsFqn, C, a as compareNaturalHierarchically, c as compareNatural } from './core.DQj9iv_y.mjs';
5
6
  import { g as getOrCreate } from './core.DcS-0zys.mjs';
6
- import { i, n as n$2, e as DeploymentNodeModel, c as DeployedInstanceModel, N as NestedElementOfDeployedInstanceModel, g as DeploymentRelationModel } from './core.D5qVzkws.mjs';
7
- import { D as DefaultMap } from './core.BMCb0wJI.mjs';
8
- import { a as nonNullable, i as invariant } from './core.D4npX2q8.mjs';
9
- import { s as splitGlobalFqn, F as FqnRef, b as isDeploymentNode, r as extractStep, q as isStepEdgeId, m as isGroupElementKind, a as _type, _ as _stage, i as isOnStage, w as whereOperatorAsPredicate, o as isGlobalFqn, n as GlobalFqn } from './core.Br7Fab9l.mjs';
7
+ import { i as i$1, n as n$3, e as DeploymentNodeModel, c as DeployedInstanceModel, N as NestedElementOfDeployedInstanceModel, g as DeploymentRelationModel } from './core.Bus32P0S.mjs';
8
+ import { D as DefaultMap } from './core.yaRvRnMW.mjs';
9
+ import { i as invariant, a as nonNullable } from './core.D4npX2q8.mjs';
10
+ import { F as FqnRef, b as isDeploymentNode, a as _type, _ as _stage, i as isOnStage, w as whereOperatorAsPredicate } from './core._jnYNUQ-.mjs';
10
11
  import { m as memoizeProp } from './core.B0oEIXqd.mjs';
11
12
  import { i as ifilter } from './core.BH9nN-_I.mjs';
12
- import { a as DefaultElementShape, D as DefaultThemeColor, R as RichText, b as DefaultShapeSize, n as n$1, g as DefaultRelationshipColor, e as DefaultLineStyle } from './core.D56nCp7e.mjs';
13
+ import { splitGlobalFqn, extractStep, isStepEdgeId, isGroupElementKind, isGlobalFqn, GlobalFqn } from '../types/scalar.mjs';
14
+ import { n as n$2 } from './core.CnIyafWn.mjs';
15
+ import { a as DefaultElementShape, D as DefaultThemeColor, R as RichText, b as DefaultShapeSize, g as DefaultRelationshipColor, e as DefaultLineStyle } from './core.CaaYNQDM.mjs';
13
16
  import { a as ihead, i as ifind } from './core.DsMrjY2q.mjs';
14
17
 
15
18
  function r(...t){return u$1(Object.values,t)}
16
19
 
17
20
  var T={asc:(r,n)=>r>n,desc:(r,n)=>r<n};function s(r,n){let[e,...o]=n;if(!m$1(e)){let t=u(...o);return r(e,t)}let a=u(e,...o);return t=>r(t,a)}function u(r,n,...e){let o=typeof r=="function"?r:r[0],a=typeof r=="function"?"asc":r[1],{[a]:t}=T,i=n===void 0?void 0:u(n,...e);return (y,c)=>{let p=o(y),l=o(c);return t(p,l)?1:t(l,p)?-1:i?.(y,c)??0}}function m$1(r){if(d(r))return true;if(typeof r!="object"||!Array.isArray(r))return false;let[n,e,...o]=r;return d(n)&&typeof e=="string"&&e in T&&o.length===0}var d=r=>typeof r=="function"&&r.length===1;
18
21
 
22
+ function i(t,e,n){return typeof e=="number"||e===void 0?r=>r.split(t,e):t.split(e,n)}
23
+
19
24
  function m(...r){return u$1(o,r)}function o(r,t){let e=[...r];return e.sort(t),e}
20
25
 
21
- function a(...r){return s(n,r)}var n=(r,t)=>[...r].sort(t);
26
+ function a$1(...r){return s(n$1,r)}var n$1=(r,t)=>[...r].sort(t);
27
+
28
+ function a(...e){return u$1(n,e)}var n=e=>e.at(-1);
22
29
 
23
30
  function t(...r){return u$1(Object.entries,r)}
24
31
 
@@ -68,7 +75,7 @@ class ElementModel {
68
75
  */
69
76
  get tags() {
70
77
  return memoizeProp(this, Symbol.for("tags"), () => {
71
- return i([
78
+ return i$1([
72
79
  ...this.$element.tags ?? [],
73
80
  ...this.$model.specification.elements[this.$element.kind]?.tags ?? []
74
81
  ]);
@@ -226,13 +233,13 @@ class ElementModel {
226
233
  * @returns true if the element is deployed
227
234
  */
228
235
  isDeployed() {
229
- return n$1(ihead(this.deployments()));
236
+ return n$2(ihead(this.deployments()));
230
237
  }
231
238
  deployments() {
232
239
  return this.$model.deployment.instancesOf(this);
233
240
  }
234
241
  hasMetadata() {
235
- return !!this.$element.metadata && !n$2(this.$element.metadata);
242
+ return !!this.$element.metadata && !n$3(this.$element.metadata);
236
243
  }
237
244
  getMetadata(field) {
238
245
  if (field) {
@@ -248,9 +255,37 @@ class ElementModel {
248
255
  }
249
256
  }
250
257
 
251
- function getId(element) {
258
+ const getId = (element) => {
252
259
  return typeof element === "string" ? element : element.id;
253
- }
260
+ };
261
+ const VIEW_FOLDERS_SEPARATOR = "/";
262
+ const splitViewTitle = (title) => {
263
+ invariant(!title.includes("\n"), "View title cannot contain newlines");
264
+ if (title.includes(VIEW_FOLDERS_SEPARATOR)) {
265
+ const segments = title.split(VIEW_FOLDERS_SEPARATOR).map((s) => s.trim()).filter((s) => s.length > 0);
266
+ if (i$2(segments, 1)) {
267
+ return segments;
268
+ }
269
+ return [""];
270
+ }
271
+ return [title.trim()];
272
+ };
273
+ const normalizeViewPath = (title) => {
274
+ return splitViewTitle(title).join(VIEW_FOLDERS_SEPARATOR);
275
+ };
276
+ const getViewFolderPath = (title) => {
277
+ const segments = splitViewTitle(title);
278
+ if (!i$2(segments, 2)) {
279
+ return null;
280
+ }
281
+ return segments.slice(0, -1).join(VIEW_FOLDERS_SEPARATOR);
282
+ };
283
+ const extractViewTitleFromPath = (title) => {
284
+ if (!title.includes(VIEW_FOLDERS_SEPARATOR)) {
285
+ return title.trim();
286
+ }
287
+ return splitViewTitle(title).pop() ?? title;
288
+ };
254
289
 
255
290
  class LikeC4DeploymentModel {
256
291
  constructor($model) {
@@ -579,13 +614,13 @@ class RelationshipModel {
579
614
  return `${this.source.id} -> ${this.target.id}`;
580
615
  }
581
616
  get title() {
582
- if (!n$1(this.$relationship.title)) {
617
+ if (!n$2(this.$relationship.title)) {
583
618
  return null;
584
619
  }
585
620
  return this.$relationship.title;
586
621
  }
587
622
  get technology() {
588
- if (!n$1(this.$relationship.technology)) {
623
+ if (!n$2(this.$relationship.technology)) {
589
624
  return null;
590
625
  }
591
626
  return this.$relationship.technology;
@@ -629,7 +664,7 @@ class RelationshipModel {
629
664
  return true;
630
665
  }
631
666
  hasMetadata() {
632
- return !!this.$relationship.metadata && !n$2(this.$relationship.metadata);
667
+ return !!this.$relationship.metadata && !n$3(this.$relationship.metadata);
633
668
  }
634
669
  getMetadata(field) {
635
670
  if (field) {
@@ -851,13 +886,13 @@ class NodeModel {
851
886
  * Check if this node references to logical model element.
852
887
  */
853
888
  hasElement() {
854
- return n$1(this.#node.modelRef);
889
+ return n$2(this.#node.modelRef);
855
890
  }
856
891
  /**
857
892
  * Check if this node references to deployment element (Node or Instance).
858
893
  */
859
894
  hasDeployment() {
860
- return n$1(this.#node.deploymentRef);
895
+ return n$2(this.#node.deploymentRef);
861
896
  }
862
897
  /**
863
898
  * Check if this node references to deployed instance
@@ -891,9 +926,23 @@ class LikeC4ViewModel {
891
926
  #allTags = new DefaultMap((_key) => /* @__PURE__ */ new Set());
892
927
  $view;
893
928
  $model;
894
- constructor(model, view) {
929
+ title;
930
+ /**
931
+ * View folder this view belongs to.
932
+ * If view is top-level, this is the root folder.
933
+ */
934
+ folder;
935
+ /**
936
+ * Path to this view, processed by {@link normalizeViewPath}
937
+ *
938
+ * @example
939
+ * "Group 1/Group 2/View"
940
+ */
941
+ viewPath;
942
+ constructor(model, view, folder) {
895
943
  this.$model = model;
896
944
  this.$view = view;
945
+ this.folder = folder;
897
946
  for (const node of view.nodes) {
898
947
  const el = new NodeModel(this, Object.freeze(node));
899
948
  this.#nodes.set(node.id, el);
@@ -925,6 +974,8 @@ class LikeC4ViewModel {
925
974
  }
926
975
  this.#edges.set(edge.id, edgeModel);
927
976
  }
977
+ this.title = this.$view.title ? extractViewTitleFromPath(this.$view.title) : null;
978
+ this.viewPath = this.$view.title ? normalizeViewPath(this.$view.title) : this.$view.id;
928
979
  }
929
980
  get _type() {
930
981
  return this.$view[_type];
@@ -932,8 +983,38 @@ class LikeC4ViewModel {
932
983
  get id() {
933
984
  return this.$view.id;
934
985
  }
935
- get title() {
936
- return this.$view.title;
986
+ /**
987
+ * Returns title if defined, otherwise returns its {@link id}
988
+ */
989
+ get titleOrId() {
990
+ return this.title ?? this.id;
991
+ }
992
+ /**
993
+ * Returns title if defined, otherwise returns `Untitled`.
994
+ */
995
+ get titleOrUntitled() {
996
+ return this.title ?? "Untitled";
997
+ }
998
+ /**
999
+ * Returns path to this view as an array of groups and this view as the last element
1000
+ * If view is top-level, returns only this view.
1001
+ *
1002
+ * @example
1003
+ * viewPath = "Group 1/Group 2/View"
1004
+ *
1005
+ * breadcrumbs = [
1006
+ * "Group 1", // folder
1007
+ * "Group 1/Group 2", // folder
1008
+ * "Group 1/Group 2/View" // view
1009
+ * ]
1010
+ */
1011
+ get breadcrumbs() {
1012
+ return memoizeProp(this, "breadcrumbs", () => {
1013
+ if (!this.folder.isRoot) {
1014
+ return [...this.folder.breadcrumbs, this];
1015
+ }
1016
+ return [this];
1017
+ });
937
1018
  }
938
1019
  get description() {
939
1020
  return RichText.memoize(this, this.$view.description);
@@ -1066,7 +1147,7 @@ class LikeC4ViewModel {
1066
1147
  return this.$view[_type] === "element";
1067
1148
  }
1068
1149
  isScopedElementView() {
1069
- return this.$view[_type] === "element" && n$1(this.$view.viewOf);
1150
+ return this.$view[_type] === "element" && n$2(this.$view.viewOf);
1070
1151
  }
1071
1152
  isDeploymentView() {
1072
1153
  return this.$view[_type] === "deployment";
@@ -1076,6 +1157,136 @@ class LikeC4ViewModel {
1076
1157
  }
1077
1158
  }
1078
1159
 
1160
+ class LikeC4ViewsFolder {
1161
+ $model;
1162
+ /**
1163
+ * Path to this view folder, processed by {@link normalizeViewPath}
1164
+ *
1165
+ * @example
1166
+ * "Folder 1/Folder 2/Folder 3"
1167
+ */
1168
+ path;
1169
+ /**
1170
+ * Title of this view folder.
1171
+ *
1172
+ * @example
1173
+ * // title is the last segment of the path
1174
+ * path = "Folder 1/Folder 2/Folder 3"
1175
+ * title = "Folder 3"
1176
+ */
1177
+ title;
1178
+ /**
1179
+ * Whether this is the root view folder.
1180
+ *
1181
+ * !NOTE
1182
+ * Root folder is special folder with an empty path and used only for internal purposes. \
1183
+ * It is not visible to the user and should not be used in the code.
1184
+ */
1185
+ isRoot;
1186
+ parentPath;
1187
+ defaultViewId;
1188
+ constructor($model, path, defaultViewId) {
1189
+ this.$model = $model;
1190
+ this.path = path.join("/");
1191
+ this.isRoot = this.path === "";
1192
+ this.title = a(path);
1193
+ if (this.isRoot) {
1194
+ this.parentPath = void 0;
1195
+ } else {
1196
+ this.parentPath = path.slice(0, -1).join("/");
1197
+ }
1198
+ this.defaultViewId = defaultViewId;
1199
+ }
1200
+ /**
1201
+ * Default view of this view folder.\
1202
+ * It is for the case when there is a view at the same path as this folder.\
1203
+ * (if there are multiple views at the same path, the first one is chosen)
1204
+ *
1205
+ * @example
1206
+ * ```
1207
+ * // Assume the following views exist:
1208
+ * const views = [
1209
+ * "Folder 1/ Folder 2 / View",
1210
+ * "Folder 1/ Folder 2 / View / Subview",
1211
+ * ]
1212
+ * ```
1213
+ * Group with path `Folder 1/ Folder 2 / View`\
1214
+ * will have default view `Folder 1/ Folder 2 / View`
1215
+ */
1216
+ get defaultView() {
1217
+ if (!this.defaultViewId) {
1218
+ return null;
1219
+ }
1220
+ return this.$model.view(this.defaultViewId);
1221
+ }
1222
+ /**
1223
+ * Returns path to this view folder as an array of ancestors (excluding root) and this view folder as the last element
1224
+ *
1225
+ * @throws Error if this is the root folder.
1226
+ */
1227
+ get breadcrumbs() {
1228
+ invariant(!this.isRoot, "Root view folder has no breadcrumbs");
1229
+ return memoizeProp(this, "breadcrumbs", () => {
1230
+ const parent = this.parent;
1231
+ if (parent) {
1232
+ if (parent.isRoot) {
1233
+ return [parent, this];
1234
+ }
1235
+ return [...parent.breadcrumbs, this];
1236
+ }
1237
+ return [this];
1238
+ });
1239
+ }
1240
+ /**
1241
+ * Returns parent folder
1242
+ *
1243
+ * @throws Error if this is the root folder.
1244
+ */
1245
+ get parent() {
1246
+ invariant(!this.isRoot, "Root view folder has no parent");
1247
+ if (n$3(this.parentPath)) {
1248
+ return null;
1249
+ }
1250
+ return this.$model.viewFolder(this.parentPath);
1251
+ }
1252
+ /**
1253
+ * Returns sorted set of children
1254
+ * - First folders
1255
+ * - Then views
1256
+ */
1257
+ get children() {
1258
+ return this.$model.viewFolderItems(this.path);
1259
+ }
1260
+ /**
1261
+ * Returns sorted array of children folders
1262
+ */
1263
+ get folders() {
1264
+ return memoizeProp(this, "folders", () => {
1265
+ const folders = [];
1266
+ for (const child of this.children) {
1267
+ if (child instanceof LikeC4ViewsFolder) {
1268
+ folders.push(child);
1269
+ }
1270
+ }
1271
+ return folders;
1272
+ });
1273
+ }
1274
+ /**
1275
+ * Returns all views in this view folder.
1276
+ */
1277
+ get views() {
1278
+ return memoizeProp(this, "views", () => {
1279
+ const views = [];
1280
+ for (const child of this.children) {
1281
+ if (child instanceof LikeC4ViewModel) {
1282
+ views.push(child);
1283
+ }
1284
+ }
1285
+ return views;
1286
+ });
1287
+ }
1288
+ }
1289
+
1079
1290
  class LikeC4Model {
1080
1291
  /**
1081
1292
  * Don't use in runtime, only for type inference
@@ -1095,6 +1306,11 @@ class LikeC4Model {
1095
1306
  // Relationships inside the element, among descendants
1096
1307
  _internal = new DefaultMap(() => /* @__PURE__ */ new Set());
1097
1308
  _views = /* @__PURE__ */ new Map();
1309
+ _rootViewFolder;
1310
+ _viewFolders = /* @__PURE__ */ new Map();
1311
+ _viewFolderItems = new DefaultMap(
1312
+ () => /* @__PURE__ */ new Set()
1313
+ );
1098
1314
  _allTags = new DefaultMap(
1099
1315
  () => /* @__PURE__ */ new Set()
1100
1316
  );
@@ -1116,6 +1332,7 @@ class LikeC4Model {
1116
1332
  const {
1117
1333
  _stage: stage = "layouted",
1118
1334
  projectId = "unknown",
1335
+ project,
1119
1336
  globals,
1120
1337
  imports,
1121
1338
  deployments,
@@ -1127,6 +1344,7 @@ class LikeC4Model {
1127
1344
  return new LikeC4Model({
1128
1345
  [_stage]: stage,
1129
1346
  projectId,
1347
+ project,
1130
1348
  globals: {
1131
1349
  predicates: globals?.predicates ?? {},
1132
1350
  dynamicPredicates: globals?.dynamicPredicates ?? {},
@@ -1169,17 +1387,56 @@ class LikeC4Model {
1169
1387
  }
1170
1388
  this.deployment = new LikeC4DeploymentModel(this);
1171
1389
  if (isOnStage($data, "computed") || isOnStage($data, "layouted")) {
1390
+ const compare = compareNaturalHierarchically(VIEW_FOLDERS_SEPARATOR);
1172
1391
  const views = C(
1173
1392
  r($data.views),
1174
- m((a, b) => compareNatural(a.title ?? "untitled", b.title ?? "untitled"))
1393
+ m$2((view) => ({
1394
+ view,
1395
+ path: normalizeViewPath(view.title ?? view.id),
1396
+ folderPath: view.title && getViewFolderPath(view.title) || ""
1397
+ })),
1398
+ // Sort hierarchically by groups, but keep same order within groups
1399
+ m((a, b) => compare(a.folderPath, b.folderPath))
1175
1400
  );
1176
- for (const view of views) {
1177
- const vm = new LikeC4ViewModel(this, view);
1401
+ const getOrCreateFolder = (path) => {
1402
+ let folder = this._viewFolders.get(path);
1403
+ if (!folder) {
1404
+ const segments = i(path, VIEW_FOLDERS_SEPARATOR);
1405
+ invariant(i$2(segments, 1), `View group path "${path}" must have at least one element`);
1406
+ let defaultView;
1407
+ if (path === "") {
1408
+ defaultView = views.find((view) => view.view.id === "index");
1409
+ } else {
1410
+ defaultView = views.find((view) => view.path === path);
1411
+ }
1412
+ folder = new LikeC4ViewsFolder(this, segments, defaultView?.view.id);
1413
+ this._viewFolders.set(path, folder);
1414
+ }
1415
+ return folder;
1416
+ };
1417
+ this._rootViewFolder = getOrCreateFolder("");
1418
+ for (const { folderPath } of views) {
1419
+ if (this._viewFolders.has(folderPath)) {
1420
+ continue;
1421
+ }
1422
+ i(folderPath, VIEW_FOLDERS_SEPARATOR).reduce((parent, segment) => {
1423
+ const path = [...parent, segment];
1424
+ const folder = getOrCreateFolder(path.join(VIEW_FOLDERS_SEPARATOR));
1425
+ this._viewFolderItems.get(parent.join(VIEW_FOLDERS_SEPARATOR)).add(folder);
1426
+ return path;
1427
+ }, []);
1428
+ }
1429
+ for (const { view, folderPath } of views) {
1430
+ const vm = new LikeC4ViewModel(this, view, getOrCreateFolder(folderPath));
1431
+ this._viewFolderItems.get(folderPath).add(vm);
1178
1432
  this._views.set(view.id, vm);
1179
1433
  for (const tag of vm.tags) {
1180
1434
  this._allTags.get(tag).add(vm);
1181
1435
  }
1182
1436
  }
1437
+ } else {
1438
+ this._rootViewFolder = new LikeC4ViewsFolder(this, [""], void 0);
1439
+ this._viewFolders.set(this._rootViewFolder.path, this._rootViewFolder);
1183
1440
  }
1184
1441
  }
1185
1442
  /**
@@ -1234,6 +1491,11 @@ class LikeC4Model {
1234
1491
  get projectId() {
1235
1492
  return this.$data.projectId ?? "unknown";
1236
1493
  }
1494
+ get project() {
1495
+ return this.$data.project ?? memoizeProp(this, Symbol.for("project"), () => ({
1496
+ id: this.projectId
1497
+ }));
1498
+ }
1237
1499
  get specification() {
1238
1500
  return this.$data.specification;
1239
1501
  }
@@ -1250,6 +1512,21 @@ class LikeC4Model {
1250
1512
  }
1251
1513
  }));
1252
1514
  }
1515
+ /**
1516
+ * Returns the element with the given FQN.
1517
+ *
1518
+ * @throws Error if element is not found\
1519
+ * Use {@link findElement} if you don't want to throw an error
1520
+ *
1521
+ * @note Method is type-safe for typed model
1522
+
1523
+ * @example
1524
+ * model.element('cloud.frontend')
1525
+ * // or object with id property of scalar.Fqn
1526
+ * model.element({
1527
+ * id: 'dashboard',
1528
+ * })
1529
+ */
1253
1530
  element(el) {
1254
1531
  if (el instanceof ElementModel) {
1255
1532
  return el;
@@ -1257,6 +1534,15 @@ class LikeC4Model {
1257
1534
  const id = getId(el);
1258
1535
  return nonNullable(this._elements.get(id), `Element ${id} not found`);
1259
1536
  }
1537
+ /**
1538
+ * Returns the element with the given FQN.
1539
+ *
1540
+ * @returns Element if found, null otherwise
1541
+ * @note Method is not type-safe as {@link element}
1542
+ *
1543
+ * @example
1544
+ * model.findElement('cloud.frontend')
1545
+ */
1260
1546
  findElement(el) {
1261
1547
  return this._elements.get(getId(el)) ?? null;
1262
1548
  }
@@ -1307,14 +1593,62 @@ class LikeC4Model {
1307
1593
  }
1308
1594
  /**
1309
1595
  * Returns a specific view by its ID.
1596
+ * @note Method is type-safe for typed model
1597
+ * @throws Error if view is not found\
1598
+ * Use {@link findView} if you don't want to throw an error
1599
+ *
1600
+ * @example
1601
+ * model.view('index')
1602
+ * // or object with id property of scalar.ViewId
1603
+ * model.view({
1604
+ * id: 'index',
1605
+ * })
1310
1606
  */
1311
1607
  view(viewId) {
1312
1608
  const id = getId(viewId);
1313
1609
  return nonNullable(this._views.get(id), `View ${id} not found`);
1314
1610
  }
1611
+ /**
1612
+ * Returns a specific view by its ID.
1613
+ * @note Method is not type-safe as {@link view}
1614
+ *
1615
+ * @example
1616
+ * model.findView('index')
1617
+ */
1315
1618
  findView(viewId) {
1316
1619
  return this._views.get(viewId) ?? null;
1317
1620
  }
1621
+ /**
1622
+ * Returns a view folder by its path.
1623
+ * Path is extracted from the view title, e.g. "Group 1/Group 2/View" -> "Group 1/Group 2"
1624
+ * @throws Error if view folder is not found.
1625
+ */
1626
+ viewFolder(path) {
1627
+ return nonNullable(this._viewFolders.get(path), `View folder ${path} not found`);
1628
+ }
1629
+ /**
1630
+ * Root folder is a special one with an empty path and used only for internal purposes.
1631
+ * It is not visible to the user and should be used only to get top-level folders and views.
1632
+ */
1633
+ get rootViewFolder() {
1634
+ return this._rootViewFolder;
1635
+ }
1636
+ /**
1637
+ * Whether the model has any view folders.
1638
+ */
1639
+ get hasViewFolders() {
1640
+ return this._viewFolders.size > 1;
1641
+ }
1642
+ /**
1643
+ * Returns all children of a view folder.
1644
+ * Path is extracted from the view title, e.g. "Group 1/Group 2/View" -> "Group 1/Group 2"
1645
+ *
1646
+ * @throws Error if view folder is not found.
1647
+ */
1648
+ viewFolderItems(path) {
1649
+ invariant(this._viewFolders.has(path), `View folder ${path} not found`);
1650
+ return this._viewFolderItems.get(path);
1651
+ }
1318
1652
  /**
1319
1653
  * Returns the parent element of given element.
1320
1654
  * @see ancestors
@@ -1403,7 +1737,8 @@ class LikeC4Model {
1403
1737
  return;
1404
1738
  }
1405
1739
  /**
1406
- * Returns all tags used in the model, sorted alphabetically.
1740
+ * Returns array of all tags used in the model, sorted naturally.\
1741
+ * Use {@link specification.tags} to get all defined tags
1407
1742
  */
1408
1743
  get tags() {
1409
1744
  return memoizeProp(this, "tags", () => m([...this._allTags.keys()], compareNatural));
@@ -1420,7 +1755,7 @@ class LikeC4Model {
1420
1755
  tagged
1421
1756
  })),
1422
1757
  m((a, b) => compareNatural(a.tag, b.tag)),
1423
- a(
1758
+ a$1(
1424
1759
  [p("count"), "desc"]
1425
1760
  )
1426
1761
  ));
@@ -1583,6 +1918,7 @@ class LikeC4Model {
1583
1918
  LikeC4Model2.EMPTY = LikeC4Model2.create({
1584
1919
  _stage: "computed",
1585
1920
  projectId: "default",
1921
+ project: { id: "default" },
1586
1922
  specification: {
1587
1923
  elements: {},
1588
1924
  relationships: {},
@@ -1636,4 +1972,4 @@ function isEdgeModel(edge) {
1636
1972
  return edge instanceof EdgeModel;
1637
1973
  }
1638
1974
 
1639
- export { ElementModel as E, LikeC4Model as L, NodeModel as N, RelationshipModel as R, LikeC4DeploymentModel as a, EdgeModel as b, LikeC4ViewModel as c, isDeployedInstanceModel as d, isDeploymentElementModel as e, isNestedElementOfDeployedInstanceModel as f, isDeploymentRelationModel as g, isRelationModel as h, isDeploymentNodeModel as i, isElementModel as j, isLikeC4ViewModel as k, isNodeModel as l, isEdgeModel as m, a as n, r, t };
1975
+ export { ElementModel as E, LikeC4Model as L, NodeModel as N, RelationshipModel as R, VIEW_FOLDERS_SEPARATOR as V, LikeC4DeploymentModel as a, EdgeModel as b, LikeC4ViewModel as c, LikeC4ViewsFolder as d, extractViewTitleFromPath as e, isDeployedInstanceModel as f, getViewFolderPath as g, isDeploymentElementModel as h, isDeploymentNodeModel as i, isNestedElementOfDeployedInstanceModel as j, isDeploymentRelationModel as k, isRelationModel as l, isElementModel as m, normalizeViewPath as n, isLikeC4ViewModel as o, isNodeModel as p, isEdgeModel as q, r, a$1 as s, t, a as u };
@@ -1,4 +1,4 @@
1
- import { i as isAncestor } from './core.C_C-3c1S.mjs';
1
+ import { i as isAncestor } from './core.DQj9iv_y.mjs';
2
2
 
3
3
  var Connection;
4
4
  ((Connection2) => {
@@ -1,5 +1,6 @@
1
- import { F as FqnRef, q as isStepEdgeId } from './core.Br7Fab9l.mjs';
2
- import { n } from './core.D56nCp7e.mjs';
1
+ import { F as FqnRef } from './core._jnYNUQ-.mjs';
2
+ import { n } from './core.CnIyafWn.mjs';
3
+ import { isStepEdgeId } from '../types/scalar.mjs';
3
4
 
4
5
  function o(r){return Array.isArray(r)}
5
6
 
@@ -1,8 +1,6 @@
1
1
  import { m as memoizeProp, b as markdownToText, a as markdownToHtml } from './core.B0oEIXqd.mjs';
2
2
  import { n as nonexhaustive } from './core.D4npX2q8.mjs';
3
3
 
4
- function n(e){return !!e}
5
-
6
4
  const DefaultThemeColor = "primary";
7
5
  const DefaultElementShape = "rectangle";
8
6
  const DefaultShapeSize = "md";
@@ -165,4 +163,4 @@ class RichText {
165
163
  }
166
164
  }
167
165
 
168
- export { DefaultThemeColor as D, RichText as R, DefaultElementShape as a, DefaultShapeSize as b, DefaultPaddingSize as c, DefaultTextSize as d, DefaultLineStyle as e, DefaultArrowType as f, DefaultRelationshipColor as g, n };
166
+ export { DefaultThemeColor as D, RichText as R, DefaultElementShape as a, DefaultShapeSize as b, DefaultPaddingSize as c, DefaultTextSize as d, DefaultLineStyle as e, DefaultArrowType as f, DefaultRelationshipColor as g };
@@ -0,0 +1,3 @@
1
+ function n(e){return !!e}
2
+
3
+ export { n };
@@ -1,5 +1,5 @@
1
- import { E as ElementModel, e as DeploymentElementModel } from './core.DrS4Bf4y.mjs';
2
- import { A as Any } from './core.DAj3LuFH.mjs';
1
+ import { E as ElementModel, e as DeploymentElementModel } from './core.DoNP3Yl-.mjs';
2
+ import { A as Any } from './core.BFb3p5d-.mjs';
3
3
  import { F as Fqn } from './core.668kXpZq.mjs';
4
4
 
5
5
  interface Connection<Elem = ElementModel<Any> | DeploymentElementModel<Any>, Id = string> {
@@ -1,6 +1,6 @@
1
- import { C as Connection, c as customInspectSymbol } from './core.D3xEWj5B.mjs';
2
- import { e as DeploymentElementModel, i as RelationshipsAccum, D as DeploymentNodeModel, R as RelationshipModel, b as DeploymentRelationModel } from './core.DrS4Bf4y.mjs';
3
- import { A as Any, B as EdgeId } from './core.DAj3LuFH.mjs';
1
+ import { C as Connection, c as customInspectSymbol } from './core.D8bTI5KC.mjs';
2
+ import { e as DeploymentElementModel, i as RelationshipsAccum, D as DeploymentNodeModel, R as RelationshipModel, b as DeploymentRelationModel } from './core.DoNP3Yl-.mjs';
3
+ import { A as Any, C as EdgeId } from './core.BFb3p5d-.mjs';
4
4
  import { I as IteratorLike } from './core.DuSLYTG6.mjs';
5
5
 
6
6
  /**
@@ -92,10 +92,8 @@ function compareNaturalHierarchically(separator = ".") {
92
92
  if (!b) return 1;
93
93
  const aParts = a.split(separator);
94
94
  const bParts = b.split(separator);
95
- if (aParts.length !== bParts.length) {
96
- return aParts.length - bParts.length;
97
- }
98
- for (let i = 0; i < aParts.length; i++) {
95
+ const minLength = Math.min(aParts.length, bParts.length);
96
+ for (let i = 0; i < minLength; i++) {
99
97
  const aPart = aParts[i];
100
98
  const bPart = bParts[i];
101
99
  const comparison = compare(aPart, bPart);
@@ -103,7 +101,7 @@ function compareNaturalHierarchically(separator = ".") {
103
101
  return comparison;
104
102
  }
105
103
  }
106
- return 0;
104
+ return aParts.length - bParts.length;
107
105
  };
108
106
  }
109
107