@statewalker/fsm-charts 0.0.2 → 0.0.4

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.
@@ -1,120 +1,7 @@
1
- export function buildStatechartCss({ prefix = "" } = {}): string {
2
- return `
3
- --state--fill: white;
4
- --state--stroke-color: silver;
5
- --state--stroke-width: 1px;
6
-
7
- --state-initial--fill: var(--state--stroke-color, silver);
8
- --state-initial--stroke-color: var(--state--stroke-color, none);
9
- --state-initial--stroke-width: var(--state--stroke-width, 0);
10
-
11
- --state-final--fill: var(--state--fill, white);
12
- --state-final--stroke-color: var(--state--stroke-color, silver);
13
- --state-final--stroke-width: var(--state--stroke-width, 1px);
14
-
15
- --state-label--color: gray;
16
- --state-label--font-size: 14px;
17
- --state-label--font-family: sans-serif;
18
-
19
- --transition--fill: rgba(255,255,255,0.7);
20
- --transition--stroke-color: none;
21
- --transition--stroke-width: 1;
22
-
23
- --transition-line--stroke-color: silver;
24
- --transition-line--stroke-width: 1px;
25
-
26
- --transition-label--color: gray;
27
- --transition-label--font-size: 12px;
28
- --transition-label--font-family: sans-serif;
29
-
30
- --transition-marker--fill: var(--transition-line--stroke-color, gray);
31
-
32
- .state-details {
33
-
34
- }
35
-
36
- .state-details {
37
- background-color: var(--state--fill, white);
38
- border: var(--state--stroke-width, 1px) solid var(--state--stroke-color, silver);
39
- border-radius: 0.5em;
40
- padding: 0.5em;
41
- }
42
-
43
- .state-details .state-label {
44
- cursor: pointer;
45
- color: var(--transition-label--color, gray);
46
- font-family: var(--state-label--font-family, sans-serif);
47
- font-size: var(--state-label--font-size, 14px);
48
- }
49
- .state-details .state-description {
50
- display: flex;
51
- flex-direction: column;
52
- gap: 0.5em;
53
- }
54
- .state-details .state-chart-container {
55
- max-width: 100%;
56
- overflow: auto;
57
- scrollbar-width: thin;
58
- }
1
+ import { serializeCss } from "../utils/serializeCss";
2
+ import { buildStatechartStyles } from "./buildStatechartStyles";
59
3
 
60
- .state-details .statechart {
61
-
62
- }
63
-
64
- .state-details .statechart .transition-marker {
65
- fill : var(--transition-marker--fill, silver);
66
- }
67
-
68
- .state-details .statechart .transitions {
69
- }
70
-
71
- .state-details .statechart .transitions .transition .transition-line {
72
- stroke: var(--transition-line--stroke-color, silver);
73
- stroke-width: var(--transition-line--stroke-width, 1px);
74
- }
75
-
76
- .state-details .statechart .transitions .transition .transition-box {
77
- fill: var(--transition--fill, rgba(255,255,255,0.7));
78
- stroke: var(--transition--stroke-color, none);
79
- stroke-width: var(--transition--stroke-width, 1);
80
- border-radius: 0.5em;
81
- }
82
-
83
- .state-details .statechart .transitions .transition .transition-label {
84
- fill: var(--transition-label--color, gray);
85
- font-size: var(--transition-label--font-size, 12px);
86
- font-family: var(--transition-label--font-family, sans-serif);
87
- }
88
-
89
- /* ------------------------------------------ */
90
- .state-details .statechart .states .state {
91
- }
92
- .state-details .statechart .states .state .state-box {
93
- fill: var(--state--fill, white);
94
- stroke: var(--state--stroke-color, silver);
95
- stroke-width: var(--state--stroke-width, 1px);
96
- border-radius: 0.5em;
97
- }
98
- .state-details .statechart .states .state .state-initial {
99
- fill: var(--state-initial--fill, gray);
100
- stroke: var(--state-initial--stroke-color, none);
101
- stroke-width: var(--state-initial--stroke-width, 0);
102
- }
103
- .state-details .statechart .states .state .state-final {
104
- fill: var(--state-final--fill, white);
105
- stroke: var(--state-final--stroke-color, silver);
106
- stroke-width: var(--state-final--stroke-width, 2);
107
- }
108
- .state-details .statechart .states .state .state-final.inner {
109
- fill: var(--state-final--stroke-color, silver);
110
- stroke: none;
111
- stroke-width: 0;
112
- }
113
- .state-details .statechart .states .state .state-label {
114
- fill: var(--state-label--color, gray);
115
- font-size: var(--state-label--font-size, 14px);
116
- font-family: var(--state-label--font-family, sans-serif);
117
- }
118
-
119
- `;
4
+ export function buildStatechartCss({ prefix = "" } = {}): string {
5
+ const cssObject = buildStatechartStyles({ root: prefix || ":root" });
6
+ return serializeCss(cssObject);
120
7
  }
@@ -0,0 +1,176 @@
1
+ import { CssTree } from "../utils/serializeCss";
2
+
3
+ function buildStateDetailsStyle() {
4
+ return {
5
+ ".state-details": {
6
+ backgroundColor: "var(--state-background-color, white)",
7
+ border:
8
+ "var(--state-border-width, 1px) solid var(--state-border-color, currentColor)",
9
+ borderRadius: "0.5em",
10
+ padding: "0.5em",
11
+ },
12
+ ".state-details__label": {
13
+ cursor: "pointer",
14
+ color: "var(--transition-label-color, currentColor)",
15
+ fontFamily: "var(--state-label-font-family, sans-serif)",
16
+ fontSize: "var(--state-label-font-size, 14px)",
17
+ },
18
+ ".state-details__description": {
19
+ display: "flex",
20
+ flexDirection: "column",
21
+ gap: "0.5em",
22
+ },
23
+ ".state-details__chart": {
24
+ maxWidth: "100%",
25
+ overflow: "auto",
26
+ scrollbarWidth: "thin",
27
+ },
28
+ //
29
+ ".state-details--selected": {
30
+ backgroundColor: "var(--state-selected-background-color, white)",
31
+ border:
32
+ "var(--state-selected-border-width, 1px) solid var(--state-selected-border-color, currentColor)",
33
+ },
34
+ ".state-details__label--selected": {
35
+ color: "var(--state-selected-label-color)",
36
+ },
37
+ };
38
+ }
39
+ export function buildStatechartStyles({
40
+ root = ":root",
41
+ }: { root?: string } = {}): CssTree {
42
+ return {
43
+ [root]: {
44
+ // --------------------------------------
45
+ // Normal states
46
+ "--state-background-color": "white",
47
+ "--state-border-color": "silver",
48
+ "--state-border-width": "1px",
49
+ "--state-label-color": "gray",
50
+ "--state-label-font-size": "14px",
51
+ "--state-label-font-family": "sans-serif",
52
+
53
+ // Selected states
54
+ "--state-selected-background-color": "#eee",
55
+ "--state-selected-border-color": "red",
56
+ "--state-selected-border-width": "2px",
57
+ "--state-selected-label-color": "red",
58
+
59
+ // --------------------------------------
60
+ // Normal transitions
61
+ "--transition-background-color": "rgba(255,255,255,0.7)",
62
+ "--transition-border-color": "none",
63
+ "--transition-border-width": "1px",
64
+ "--transition-line-color": "silver",
65
+ "--transition-line-width": "1px",
66
+ "--transition-label-color": "gray",
67
+ "--transition-label-font-size": "12px",
68
+ "--transition-label-font-family": "sans-serif",
69
+
70
+ // --------------------------------------
71
+ // Selected transitions
72
+ "--transition-selected-background-color": "#eee",
73
+ "--transition-selected-border-color": "none",
74
+ "--transition-selected-border-width": "0",
75
+ "--transition-selected-line-color": "red",
76
+ "--transition-selected-line-width": "1px",
77
+ "--transition-selected-label-color": "red",
78
+
79
+ // --------------------------------------
80
+ // --------------------------------------
81
+ ...buildStateDetailsStyle(),
82
+ ".transition": {},
83
+ ".transition--selected": {
84
+ "--transition-background-color":
85
+ "var(--transition-selected-background-color)",
86
+ "--transition-border-color": "var(--transition-selected-border-color)",
87
+ "--transition-border-width": "var(--transition-selected-border-width)",
88
+ "--transition-line-color": "var(--transition-selected-line-color)",
89
+ "--transition-line-width": "var(--transition-selected-line-width)",
90
+ "--transition-label-color": "var(--transition-selected-label-color)",
91
+ },
92
+ ".transition__marker": {
93
+ fill: "var(--transition-line-color, currentColor)",
94
+ stroke: "none",
95
+ },
96
+ ".transition__line": {
97
+ stroke: "var(--transition-line-color, currentColor)",
98
+ strokeWidth: "var(--transition-line-width, 1px)",
99
+ },
100
+ ".transition__box": {
101
+ fill: "var(--transition-background-color, rgba(255,255,255,0.7))",
102
+ stroke: "var(--transition-border-color, none)",
103
+ strokeWidth: "var(--transition-border-width, 1px)",
104
+ borderRadius: "0.5em",
105
+ },
106
+ ".transition__label": {
107
+ fill: "var(--transition-label-color, currentColor)",
108
+ fontSize: "var(--transition-label-font-size, 12px)",
109
+ fontFamily: "var(--transition-label-font-family, sans-serif)",
110
+ },
111
+
112
+ // ----------------------------------------------
113
+ // States
114
+
115
+ ".state": {},
116
+ ".state--selected": {
117
+ "--state-background-color": "var(--state-selected-background-color)",
118
+ "--state-border-color": "var(--state-selected-border-color)",
119
+ "--state-border-width": "var(--state-selected-border-width)",
120
+ "--state-label-color": "var(--state-selected-label-color)",
121
+ },
122
+
123
+ // --------------------------
124
+ // Panels
125
+ ".state__details": {
126
+ backgroundColor: "var(--state-background-color, white)",
127
+ border:
128
+ "var(--state-border-width, 1px) solid var(--state-border-color, currentColor)",
129
+ borderRadius: "0.5em",
130
+ padding: "0.5em",
131
+ },
132
+ ".state__description": {
133
+ display: "flex",
134
+ flexDirection: "column",
135
+ gap: "0.5em",
136
+ },
137
+ ".state__chart-container": {
138
+ maxWidth: "100%",
139
+ overflow: "auto",
140
+ scrollbarWidth: "thin",
141
+ },
142
+
143
+ // --------------------------
144
+ // Statecharts
145
+ ".state__box": {
146
+ fill: "var(--state-background-color, white)",
147
+ stroke: "var(--state-border-color, currentColor)",
148
+ strokeWidth: "var(--state-border-width, 1px)",
149
+ borderRadius: "0.5em",
150
+ },
151
+ ".state__label": {
152
+ color: "var(--state-label-color, currentColor)",
153
+ fill: "var(--state-label-color, currentColor)",
154
+ fontSize: "var(--state-label-font-size, 14px)",
155
+ fontFamily: "var(--state-label-font-family, sans-serif)",
156
+ cursor: "pointer",
157
+ },
158
+ ".state__initial": {
159
+ fill: "var(--state-border-color, currentColor)",
160
+ stroke: "var(--state-border-color, currentColor)",
161
+ strokeWidth: "var(--state-border-width, 2px)",
162
+ },
163
+ ".state__final": {
164
+ fill: "white",
165
+ stroke: "var(--state-border-color, currentColor)",
166
+ strokeWidth: "var(--state-border-width, 2px)",
167
+ // ---------------------------
168
+ "&.state__final--inner": {
169
+ fill: "var(--state-border-color, currentColor)",
170
+ stroke: "none",
171
+ strokeWidth: "0",
172
+ },
173
+ },
174
+ },
175
+ };
176
+ }
@@ -65,7 +65,7 @@ export function buildStatechartSvg({
65
65
  .transitions .transition .transition-label
66
66
  ---------------------------------------------------
67
67
  */
68
- println(`<g class="transitions">`);
68
+ println(`<g class="statechart__transitions">`);
69
69
 
70
70
  const markerAttrs = serializeAttrs({
71
71
  refX: 19,
@@ -73,7 +73,7 @@ export function buildStatechartSvg({
73
73
  markerWidth: 20,
74
74
  markerHeight: 14,
75
75
  orient: "auto",
76
- class: "transition-marker",
76
+ class: "transition__marker",
77
77
  stroke: "none",
78
78
  fill: "currentColor",
79
79
  });
@@ -119,7 +119,7 @@ export function buildStatechartSvg({
119
119
  base.lineEnd();
120
120
 
121
121
  const serializedAttrs = serializeAttrs({
122
- class: "transition-line",
122
+ class: "transition__line",
123
123
  fill: "none",
124
124
  stroke: "currentColor",
125
125
  strokeWidth: "1px",
@@ -144,12 +144,12 @@ export function buildStatechartSvg({
144
144
  fill: "rgba(255,255,255,0.7)",
145
145
  stroke: "none",
146
146
  strokeWidth: "1",
147
- class: "transition-box",
147
+ class: "transition__box",
148
148
  });
149
149
  println(` <rect ${rectAttrs} />`);
150
150
 
151
151
  const textAttrs = serializeAttrs({
152
- class: "transition-label",
152
+ class: "transition__label",
153
153
  "text-anchor": "middle",
154
154
  fill: "currentColor",
155
155
  x: Math.round(x + width / 2),
@@ -179,7 +179,7 @@ export function buildStatechartSvg({
179
179
  const initialStateRadius = 6;
180
180
  const finalStateRadius = initialStateRadius / 2;
181
181
 
182
- println(`<g class="states">`);
182
+ println(`<g class="statechart__states">`);
183
183
  for (const node of nodes) {
184
184
  const { key, id, state: text } = node;
185
185
  const { x, y, width, height } = getBoxParams({ ...node, padding: shift });
@@ -190,7 +190,7 @@ export function buildStatechartSvg({
190
190
  const cy = Math.round(y + height / 2);
191
191
  if (key === initialStateKey) {
192
192
  const serializedAttrs = serializeAttrs({
193
- class: "state-initial",
193
+ class: "state__initial",
194
194
  cx,
195
195
  cy,
196
196
  r: initialStateRadius,
@@ -201,7 +201,7 @@ export function buildStatechartSvg({
201
201
  println(` <circle ${serializedAttrs} />`);
202
202
  } else {
203
203
  let serializedAttrs = serializeAttrs({
204
- class: "state-final",
204
+ class: "state__final",
205
205
  cx,
206
206
  cy,
207
207
  r: initialStateRadius,
@@ -212,7 +212,7 @@ export function buildStatechartSvg({
212
212
  println(` <circle ${serializedAttrs} />`);
213
213
 
214
214
  serializedAttrs = serializeAttrs({
215
- class: "state-final inner",
215
+ class: "state__final state__final--inner",
216
216
  cx,
217
217
  cy,
218
218
  r: finalStateRadius,
@@ -233,12 +233,12 @@ export function buildStatechartSvg({
233
233
  fill: "none",
234
234
  stroke: "currentColor",
235
235
  strokeWidth: "2",
236
- class: "state-box",
236
+ class: "state__box",
237
237
  });
238
238
  println(` <rect ${rectAttrs} />`);
239
239
 
240
240
  const textAttrs = serializeAttrs({
241
- class: "state-label",
241
+ class: "state__label",
242
242
  "text-anchor": "middle",
243
243
  fill: "currentColor",
244
244
  x: Math.round(x + width / 2),
@@ -17,9 +17,9 @@ export function buildStatechartsPanel({
17
17
  render(statechart, println);
18
18
 
19
19
  function render(statechart: StateChart, println: (str: string) => void) {
20
- println(`<details class="state-details" data-state-id="${statechart.id}">`);
21
- println(` <summary class="state-label">${statechart.key}</summary>`);
22
- println(` <div class="state-description"><div class="state-chart-container">`);
20
+ println(` <details class="state-details" data-state-id="${statechart.id}">`);
21
+ println(` <summary class="state-details__label">${statechart.key}</summary>`);
22
+ println(` <div class="state-details__description"><div class="state-details__chart">`);
23
23
  const printChildren = (str: string) => println(` ${str}`);
24
24
  buildStatechartSvg({
25
25
  graph: statechart.graph,
@@ -28,11 +28,11 @@ export function buildStatechartsPanel({
28
28
  initialStateKey,
29
29
  finalStateKey,
30
30
  });
31
- println(` </div>`);
31
+ println(` </div>`);
32
32
  for (const child of statechart.children || []) {
33
33
  render(child, printChildren);
34
34
  }
35
- println(` </div>`);
36
- println(`</details>`);
35
+ println(` </div>`);
36
+ println(` </details>`);
37
37
  }
38
38
  }
package/src/svg/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./buildStatechartCss.ts";
2
2
  export * from "./buildStatechartsPanel.ts";
3
+ export * from "./buildStatechartStyles.ts";
3
4
  export * from "./buildStatechartSvg.ts";
@@ -0,0 +1,42 @@
1
+ import { toKebabCase } from "./toKebabCase.ts";
2
+
3
+ export interface CssTree extends Record<string, string | number | CssTree> {}
4
+
5
+ export function serializeCss(obj: CssTree) {
6
+ return visitStyle(obj).join("\n");
7
+
8
+ function visitStyle(
9
+ obj: CssTree,
10
+ lines: string[] = [],
11
+ prefixes: string[] = [""]
12
+ ) {
13
+ for (const [key, style] of Object.entries(obj)) {
14
+ const selectors = [];
15
+ for (const sel of key.split(",").map((_) => _.trim())) {
16
+ for (const prefix of prefixes) {
17
+ const selector = sel.replace(/^(.)/, (_, ch) =>
18
+ ch === "&" ? prefix : prefix.length > 0 ? prefix + " " + ch : ch
19
+ );
20
+ selectors.push(selector);
21
+ }
22
+ }
23
+ const selectorRules = [];
24
+ const extensions: string[] = [];
25
+ for (let [key, value] of Object.entries(style)) {
26
+ if (typeof value === "object") {
27
+ visitStyle({ [key]: value }, extensions, selectors);
28
+ } else {
29
+ key = toKebabCase(key);
30
+ selectorRules.push(` ${key}: ${value};`);
31
+ }
32
+ }
33
+ if (selectorRules.length) {
34
+ lines.push(`${selectors.join(",\n")} {`);
35
+ lines.push(...selectorRules);
36
+ lines.push(`}`);
37
+ }
38
+ lines.push(...extensions);
39
+ }
40
+ return lines;
41
+ }
42
+ }