@likec4/core 1.34.1 → 1.35.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 (56) hide show
  1. package/dist/builder/index.d.mts +2 -2
  2. package/dist/builder/index.mjs +11 -10
  3. package/dist/compute-view/index.d.mts +2 -2
  4. package/dist/compute-view/index.mjs +12 -11
  5. package/dist/compute-view/relationships-view/index.d.mts +2 -2
  6. package/dist/compute-view/relationships-view/index.mjs +2 -2
  7. package/dist/index.d.mts +2 -2
  8. package/dist/index.mjs +9 -8
  9. package/dist/model/connection/deployment/index.d.mts +4 -4
  10. package/dist/model/connection/deployment/index.mjs +3 -3
  11. package/dist/model/connection/index.d.mts +5 -5
  12. package/dist/model/connection/index.mjs +4 -4
  13. package/dist/model/connection/model/index.d.mts +4 -4
  14. package/dist/model/connection/model/index.mjs +3 -3
  15. package/dist/model/index.d.mts +31 -7
  16. package/dist/model/index.mjs +9 -9
  17. package/dist/shared/{core.D3xEWj5B.d.mts → core.72IoI17N.d.mts} +1 -1
  18. package/dist/shared/{core.DXRAwwDB.mjs → core.B1ZN723l.mjs} +1 -1
  19. package/dist/shared/core.BpJu1RrF.mjs +5 -0
  20. package/dist/shared/{core.C80Z37Eu.mjs → core.C1GV0WWV.mjs} +1 -1
  21. package/dist/shared/{core.D5qVzkws.mjs → core.CQS11dNK.mjs} +2 -2
  22. package/dist/shared/{core.C_C-3c1S.mjs → core.DQj9iv_y.mjs} +3 -5
  23. package/dist/shared/{core.IWLie-Ma.d.mts → core.Ddua-wiI.d.mts} +3 -0
  24. package/dist/shared/{core.CJSFKrYf.d.mts → core.DhTO8rgm.d.mts} +2 -2
  25. package/dist/shared/{core.CtKXPqN_.mjs → core.HUONfQfd.mjs} +1 -1
  26. package/dist/shared/{core.Di7__sMg.d.mts → core.NBNeVyag.d.mts} +2 -2
  27. package/dist/shared/{core.PqNe5WxQ.mjs → core.RnUJywKk.mjs} +94 -92
  28. package/dist/shared/{core.DrS4Bf4y.d.mts → core.SciGxb--.d.mts} +183 -8
  29. package/dist/shared/{core.BTnf14gD.mjs → core.Y3j3s-oq.mjs} +2 -2
  30. package/dist/shared/{core.XptQjhMA.mjs → core.b2BzqGz0.mjs} +2 -2
  31. package/dist/shared/{core.4SpVCM9m.mjs → core.rpF8esVq.mjs} +354 -28
  32. package/dist/shared/{core.BMCb0wJI.mjs → core.yaRvRnMW.mjs} +1 -1
  33. package/dist/theme/index.mjs +33 -3
  34. package/dist/types/index.d.mts +2 -2
  35. package/dist/types/index.mjs +2 -2
  36. package/dist/utils/index.d.mts +6 -8
  37. package/dist/utils/index.mjs +5 -5
  38. package/dist/utils/iterable/index.d.mts +18 -1
  39. package/dist/utils/iterable/index.mjs +22 -2
  40. package/package.json +6 -5
  41. package/src/compute-view/element-view/predicates/relation-direct.ts +8 -2
  42. package/src/model/DeploymentModel.ts +1 -1
  43. package/src/model/LikeC4Model.ts +146 -7
  44. package/src/model/index.ts +8 -0
  45. package/src/model/types.ts +1 -8
  46. package/src/model/utils.ts +66 -0
  47. package/src/model/view/LikeC4ViewModel.ts +62 -7
  48. package/src/model/view/LikeC4ViewsFolder.ts +151 -0
  49. package/src/theme/__test__/theme-index.ts +29 -0
  50. package/src/theme/index.ts +49 -3
  51. package/src/types/model-spec.ts +4 -0
  52. package/src/utils/compare-natural.ts +14 -12
  53. package/src/utils/index.ts +1 -0
  54. package/src/utils/iterable/first.ts +48 -0
  55. package/src/utils/iterable/index.ts +1 -0
  56. package/dist/shared/{core.Br7Fab9l.mjs → core.BajmN8zR.mjs} +6 -6
@@ -0,0 +1,151 @@
1
+ import { isEmpty, last } from 'remeda'
2
+ import type { aux, NonEmptyArray } from '../../types'
3
+ import { invariant, memoizeProp } from '../../utils'
4
+ import type { LikeC4Model } from '../LikeC4Model'
5
+ import { LikeC4ViewModel } from './LikeC4ViewModel'
6
+
7
+ export class LikeC4ViewsFolder<A extends aux.Any = aux.Any> {
8
+ public readonly $model: LikeC4Model<A>
9
+
10
+ /**
11
+ * Path to this view folder, processed by {@link normalizeViewPath}
12
+ *
13
+ * @example
14
+ * "Folder 1/Folder 2/Folder 3"
15
+ */
16
+ public readonly path: string
17
+
18
+ /**
19
+ * Title of this view folder.
20
+ *
21
+ * @example
22
+ * // title is the last segment of the path
23
+ * path = "Folder 1/Folder 2/Folder 3"
24
+ * title = "Folder 3"
25
+ */
26
+ public readonly title: string
27
+
28
+ /**
29
+ * Whether this is the root view folder.
30
+ *
31
+ * !NOTE
32
+ * Root folder is special folder with an empty path and used only for internal purposes. \
33
+ * It is not visible to the user and should not be used in the code.
34
+ */
35
+ public readonly isRoot: boolean
36
+
37
+ protected readonly parentPath: string | undefined
38
+
39
+ protected readonly defaultViewId: aux.StrictViewId<A> | undefined
40
+
41
+ constructor(
42
+ $model: LikeC4Model<A>,
43
+ path: NonEmptyArray<string>,
44
+ defaultViewId: aux.StrictViewId<A> | undefined,
45
+ ) {
46
+ this.$model = $model
47
+ this.path = path.join('/')
48
+ this.isRoot = this.path === ''
49
+ this.title = last(path)
50
+ if (this.isRoot) {
51
+ this.parentPath = undefined
52
+ } else {
53
+ this.parentPath = path.slice(0, -1).join('/')
54
+ }
55
+ this.defaultViewId = defaultViewId
56
+ }
57
+
58
+ /**
59
+ * Default view of this view folder.\
60
+ * It is for the case when there is a view at the same path as this folder.\
61
+ * (if there are multiple views at the same path, the first one is chosen)
62
+ *
63
+ * @example
64
+ * ```
65
+ * // Assume the following views exist:
66
+ * const views = [
67
+ * "Folder 1/ Folder 2 / View",
68
+ * "Folder 1/ Folder 2 / View / Subview",
69
+ * ]
70
+ * ```
71
+ * Group with path `Folder 1/ Folder 2 / View`\
72
+ * will have default view `Folder 1/ Folder 2 / View`
73
+ */
74
+ get defaultView(): LikeC4ViewModel<A> | null {
75
+ if (!this.defaultViewId) {
76
+ return null
77
+ }
78
+ return this.$model.view(this.defaultViewId)
79
+ }
80
+
81
+ /**
82
+ * Returns path to this view folder as an array of ancestors (excluding root) and this view folder as the last element
83
+ *
84
+ * @throws Error if this is the root folder.
85
+ */
86
+ get breadcrumbs(): [...LikeC4ViewsFolder<A>[], this] {
87
+ invariant(!this.isRoot, 'Root view folder has no breadcrumbs')
88
+ return memoizeProp(this, 'breadcrumbs', () => {
89
+ const parent = this.parent
90
+ if (parent) {
91
+ if (parent.isRoot) {
92
+ return [parent, this]
93
+ }
94
+ return [...parent.breadcrumbs, this]
95
+ }
96
+ return [this]
97
+ })
98
+ }
99
+
100
+ /**
101
+ * Returns parent folder
102
+ *
103
+ * @throws Error if this is the root folder.
104
+ */
105
+ get parent(): LikeC4ViewsFolder<A> | null {
106
+ invariant(!this.isRoot, 'Root view folder has no parent')
107
+ if (isEmpty(this.parentPath)) {
108
+ return null
109
+ }
110
+ return this.$model.viewFolder(this.parentPath)
111
+ }
112
+
113
+ /**
114
+ * Returns sorted set of children
115
+ * - First folders
116
+ * - Then views
117
+ */
118
+ get children(): ReadonlySet<LikeC4ViewsFolder<A> | LikeC4ViewModel<A>> {
119
+ return this.$model.viewFolderItems(this.path)
120
+ }
121
+
122
+ /**
123
+ * Returns sorted array of children folders
124
+ */
125
+ get folders(): ReadonlyArray<LikeC4ViewsFolder<A>> {
126
+ return memoizeProp(this, 'folders', () => {
127
+ const folders: LikeC4ViewsFolder<A>[] = []
128
+ for (const child of this.children) {
129
+ if (child instanceof LikeC4ViewsFolder) {
130
+ folders.push(child)
131
+ }
132
+ }
133
+ return folders
134
+ })
135
+ }
136
+
137
+ /**
138
+ * Returns all views in this view folder.
139
+ */
140
+ get views(): ReadonlyArray<LikeC4ViewModel<A>> {
141
+ return memoizeProp(this, 'views', () => {
142
+ const views: LikeC4ViewModel<A>[] = []
143
+ for (const child of this.children) {
144
+ if (child instanceof LikeC4ViewModel) {
145
+ views.push(child)
146
+ }
147
+ }
148
+ return views
149
+ })
150
+ }
151
+ }
@@ -0,0 +1,29 @@
1
+ import type { HexColor } from "../../types";
2
+
3
+ export const lightValue = {
4
+ elements: {
5
+ fill: "#2ec2fb" as HexColor,
6
+ stroke: "#1fabe1" as HexColor,
7
+ hiContrast: "#00002d" as HexColor,
8
+ loContrast: "#00153d" as HexColor,
9
+ },
10
+ relationships: {
11
+ lineColor: "#4ccdfb" as HexColor,
12
+ labelColor: "#6fd8fc" as HexColor,
13
+ labelBgColor: "#0084b2" as HexColor,
14
+ },
15
+ }
16
+
17
+ export const darkValue = {
18
+ elements: {
19
+ fill: "#2c40dc" as HexColor,
20
+ stroke: "#1f32c4" as HexColor,
21
+ hiContrast: "#ffffff" as HexColor,
22
+ loContrast: "#fffeff" as HexColor,
23
+ },
24
+ relationships: {
25
+ lineColor: "#5362e1" as HexColor,
26
+ labelColor: "#7a87e9" as HexColor,
27
+ labelBgColor: "#0a259c" as HexColor,
28
+ },
29
+ }
@@ -1,7 +1,13 @@
1
1
  import { generateColors } from '@mantine/colors-generator'
2
+ import chroma from 'chroma-js';
2
3
  import type { ColorLiteral, HexColor, LikeC4Theme, ThemeColorValues } from '../types'
3
4
  import { ElementColors } from './element'
4
5
  import { RelationshipColors } from './relationships'
6
+ import { isDeepEqual } from 'remeda'
7
+
8
+ const CONTRAST_MIN_WITH_FOREGROUND = 60
9
+ const CONTRAST_START_TONE_DIFFERENCE = 2
10
+ const CONTRAST_STEP_TONE_DIFFERENCE = 1
5
11
 
6
12
  export const defaultTheme: LikeC4Theme = {
7
13
  elements: ElementColors,
@@ -57,12 +63,15 @@ export function computeColorValues(color: ColorLiteral): ThemeColorValues {
57
63
  if (color.match(/^#([0-9a-f]{3}){1,2}$/i)) {
58
64
  const colors = generateColors(color)
59
65
 
66
+ const fillColor = colors[6]
67
+ const contrastedColors = getContrastedColorsAPCA(fillColor)
68
+
60
69
  return {
61
70
  elements: {
62
- fill: colors[6] as HexColor,
71
+ fill: fillColor as HexColor,
63
72
  stroke: colors[7] as HexColor,
64
- hiContrast: colors[0] as HexColor,
65
- loContrast: colors[1] as HexColor,
73
+ hiContrast: contrastedColors[0] as HexColor,
74
+ loContrast: contrastedColors[1] as HexColor,
66
75
  },
67
76
  relationships: {
68
77
  lineColor: colors[4] as HexColor,
@@ -78,4 +87,41 @@ export function computeColorValues(color: ColorLiteral): ThemeColorValues {
78
87
  }
79
88
  }
80
89
 
90
+ function getContrastedColorsAPCA(refColor: string): [string, string] {
91
+ const refColorChroma = chroma(refColor)
92
+
93
+ // Start with 2 steps tone difference in the CIELAB color space from reference
94
+ let lightColorRgb = refColorChroma.brighten(CONTRAST_START_TONE_DIFFERENCE);
95
+ let darkColorRgb = refColorChroma.darken(CONTRAST_START_TONE_DIFFERENCE);
96
+
97
+ let previousLight
98
+ let previousDark
99
+ let contrastWithLight
100
+ let contrastWithDark
101
+ do {
102
+ // Store previous colors for loop condition
103
+ previousLight = lightColorRgb
104
+ previousDark = darkColorRgb
105
+
106
+ // Change tone by 1 step each loop
107
+ lightColorRgb = lightColorRgb.brighten(CONTRAST_STEP_TONE_DIFFERENCE)
108
+ darkColorRgb = darkColorRgb.darken(CONTRAST_STEP_TONE_DIFFERENCE)
109
+
110
+ // Calculate contrast of each color with the reference color
111
+ contrastWithLight = chroma.contrastAPCA(refColorChroma, lightColorRgb);
112
+ contrastWithDark = chroma.contrastAPCA(refColorChroma, darkColorRgb);
113
+ }
114
+ // Stop if one of the contrast is high enough or if we reach max value for each (aka when they are equal between two rounds)
115
+ while (Math.abs(contrastWithLight) < CONTRAST_MIN_WITH_FOREGROUND
116
+ && Math.abs(contrastWithDark) < CONTRAST_MIN_WITH_FOREGROUND
117
+ && (!isDeepEqual(lightColorRgb, previousLight) || !isDeepEqual(darkColorRgb, previousDark)))
118
+
119
+ // Choose the max contrast between the two
120
+ if (Math.abs(contrastWithLight) > Math.abs(contrastWithDark)) {
121
+ return [lightColorRgb.brighten(0.4).hex(), lightColorRgb.hex()]
122
+ } else {
123
+ return [darkColorRgb.darken(0.4).hex(), darkColorRgb.hex()]
124
+ }
125
+ }
126
+
81
127
  export { ElementColors, RelationshipColors }
@@ -1,6 +1,7 @@
1
1
  import type { IsNever } from 'type-fest'
2
2
  import type * as aux from './_aux'
3
3
  import type { Any } from './_aux'
4
+ import type { Link, NonEmptyArray } from './_common'
4
5
  import type * as scalar from './scalar'
5
6
  import type {
6
7
  Icon,
@@ -24,8 +25,11 @@ import type {
24
25
  */
25
26
  export interface ElementSpecification {
26
27
  tags?: scalar.Tag[]
28
+ title?: string
29
+ description?: scalar.MarkdownOrString
27
30
  technology?: string
28
31
  notation?: string
32
+ links?: NonEmptyArray<Link>
29
33
  style: {
30
34
  shape?: ElementShape
31
35
  icon?: Icon
@@ -13,22 +13,20 @@ export function compareNatural(a: string | undefined, b: string | undefined): -1
13
13
  }
14
14
 
15
15
  /**
16
- * Compares two strings hierarchically based on their depth.
16
+ * Compares two strings lexicographically first, then hierarchically based on their depth.\
17
17
  * From parent nodes to leaves
18
18
  *
19
19
  * @example
20
20
  * const lines = [
21
+ * 'b.c',
22
+ * 'b',
21
23
  * 'a.b.c',
22
- * 'a',
23
- * 'a.b',
24
- * 'a.c.c',
25
24
  * ]
26
25
  * lines.sort(compareNaturalHierarchically('.'))
27
26
  * // [
28
- * // 'a',
29
- * // 'a.b',
30
27
  * // 'a.b.c',
31
- * // 'a.c.c',
28
+ * // 'b',
29
+ * // 'b.c',
32
30
  * // ]
33
31
  */
34
32
  export function compareNaturalHierarchically(
@@ -40,10 +38,12 @@ export function compareNaturalHierarchically(
40
38
  if (!b) return 1
41
39
  const aParts = a.split(separator)
42
40
  const bParts = b.split(separator)
43
- if (aParts.length !== bParts.length) {
44
- return aParts.length - bParts.length
45
- }
46
- for (let i = 0; i < aParts.length; i++) {
41
+
42
+ // Find the minimum length to avoid out-of-bounds access
43
+ const minLength = Math.min(aParts.length, bParts.length)
44
+
45
+ // Compare segments until we find a difference
46
+ for (let i = 0; i < minLength; i++) {
47
47
  const aPart = aParts[i]!
48
48
  const bPart = bParts[i]!
49
49
  const comparison = compare(aPart, bPart)
@@ -51,6 +51,8 @@ export function compareNaturalHierarchically(
51
51
  return comparison
52
52
  }
53
53
  }
54
- return 0
54
+
55
+ // If all common segments are equal, shorter paths come first
56
+ return aParts.length - bParts.length
55
57
  }
56
58
  }
@@ -41,6 +41,7 @@ export {
41
41
  export {
42
42
  ifilter,
43
43
  ifind,
44
+ ifirst,
44
45
  iflat,
45
46
  ihead,
46
47
  imap,
@@ -0,0 +1,48 @@
1
+ import type { IteratorLike } from '../../types'
2
+ import { invariant } from '../../utils/invariant'
3
+
4
+ /**
5
+ * Takes the first N elements from an iterable.
6
+ * Composable first version of `ifirst`.
7
+ * @signature
8
+ * ifirst(count)(data)
9
+ */
10
+ export function ifirst<T>(count: number): (iterable: Iterable<T>) => IteratorLike<T>
11
+
12
+ /**
13
+ * Takes the first N elements from an iterable.
14
+ * Data first version of `ifirst`.
15
+ * @signature
16
+ * ifirst(data, count)
17
+ * @example
18
+ * ifirst([1, 2, 3, 4, 5], 3) // => Iterable<[1, 2, 3]>
19
+ */
20
+ export function ifirst<T>(iterable: Iterable<T>, count: number): IteratorLike<T>
21
+
22
+ export function ifirst(
23
+ arg1: unknown,
24
+ arg2?: unknown,
25
+ ): IteratorLike<unknown> | ((it: Iterable<unknown>) => IteratorLike<unknown>) {
26
+ const count = (arg2 ?? arg1) as number
27
+ invariant(typeof count === 'number' && count >= 0, 'Count must be a non-negative number')
28
+
29
+ function* _first(iter: Iterable<unknown>): IteratorLike<unknown> {
30
+ let taken = 0
31
+ for (const value of iter) {
32
+ if (taken >= count) {
33
+ break
34
+ }
35
+ yield value
36
+ taken++
37
+ }
38
+ return
39
+ }
40
+
41
+ if (arg2 === undefined) {
42
+ // Composable version: ifirst(count)(iterable)
43
+ return (iterable: Iterable<unknown>) => _first(iterable)
44
+ }
45
+
46
+ // Data-first version: ifirst(iterable, count)
47
+ return _first(arg1 as Iterable<unknown>)
48
+ }
@@ -5,6 +5,7 @@ export { isIterable } from './_types'
5
5
  */
6
6
  export { ifilter } from './filter'
7
7
  export { ifind } from './find'
8
+ export { ifirst } from './first'
8
9
  export { iflat } from './flat'
9
10
  export { ihead } from './head'
10
11
  export { imap } from './map'
@@ -4,18 +4,18 @@ import { i as invariant, n as nonexhaustive } from './core.D4npX2q8.mjs';
4
4
  import { o } from './core.BgvIuGGU.mjs';
5
5
  import { u } from './core.DbRvwARP.mjs';
6
6
 
7
- function isOnStage(value, stage) {
8
- return value[_stage] === stage;
9
- }
10
- const _stage = "_stage";
11
- const _type = "_type";
12
-
13
7
  function n(e){return e==null}
14
8
 
15
9
  function t(...a){return u(e,a)}var e=(a,o)=>o.every(l=>l(a));
16
10
 
17
11
  function y(...a){return u(r,a)}var r=(a,o)=>o.some(e=>e(a));
18
12
 
13
+ function isOnStage(value, stage) {
14
+ return value[_stage] === stage;
15
+ }
16
+ const _stage = "_stage";
17
+ const _type = "_type";
18
+
19
19
  function flattenMarkdownOrString(value) {
20
20
  if (value === null || value === void 0) {
21
21
  return null;