ember-tribe 2.6.8 → 2.6.9

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.
@@ -0,0 +1,457 @@
1
+ <style>
2
+ /* ── Storylang Visualizer Styles ── */
3
+ .sl-root {
4
+ font-family: "Inter", "Segoe UI", system-ui, sans-serif;
5
+ background: #0d1117;
6
+ height: 100vh;
7
+ color: #e6edf3;
8
+ display: flex;
9
+ flex-direction: column;
10
+ overflow: hidden;
11
+ }
12
+
13
+ .sl-header {
14
+ padding: 28px 40px 20px;
15
+ border-bottom: 1px solid #21262d;
16
+ display: flex;
17
+ align-items: center;
18
+ gap: 20px;
19
+ flex-wrap: wrap;
20
+ flex-shrink: 0;
21
+ position: sticky;
22
+ top: 0;
23
+ z-index: 10;
24
+ background: #0d1117;
25
+ }
26
+
27
+ .sl-header h1 {
28
+ font-size: 1.25rem;
29
+ font-weight: 600;
30
+ letter-spacing: -0.01em;
31
+ color: #f0f6fc;
32
+ margin: 0;
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 10px;
36
+ }
37
+
38
+ .sl-header h1 svg {
39
+ opacity: 0.7;
40
+ }
41
+
42
+ .sl-legend {
43
+ display: flex;
44
+ gap: 14px;
45
+ flex-wrap: wrap;
46
+ margin-left: auto;
47
+ align-items: center;
48
+ }
49
+
50
+ .sl-legend-item {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 6px;
54
+ font-size: 0.72rem;
55
+ font-weight: 500;
56
+ color: #8b949e;
57
+ letter-spacing: 0.03em;
58
+ text-transform: capitalize;
59
+ }
60
+
61
+ .sl-legend-dot {
62
+ width: 8px;
63
+ height: 8px;
64
+ border-radius: 50%;
65
+ flex-shrink: 0;
66
+ }
67
+
68
+ .sl-body {
69
+ display: flex;
70
+ flex: 1;
71
+ overflow-y: auto;
72
+ overflow-x: hidden;
73
+ align-items: flex-start;
74
+ }
75
+
76
+ /* ── Arc Diagram ── */
77
+ .sl-diagram-wrap {
78
+ flex: 1;
79
+ overflow: visible;
80
+ padding: 30px 0;
81
+ }
82
+
83
+ .sl-svg-container {
84
+ display: flex;
85
+ justify-content: center;
86
+ }
87
+
88
+ /* ── Node dots & labels ── */
89
+ .sl-node-group {
90
+ cursor: pointer;
91
+ }
92
+
93
+ .sl-node-group:hover .sl-node-dot {
94
+ filter: brightness(1.3);
95
+ }
96
+
97
+ .sl-node-dot {
98
+ transition:
99
+ r 0.15s ease,
100
+ filter 0.15s ease;
101
+ }
102
+
103
+ .sl-node-label {
104
+ font-size: 11.5px;
105
+ fill: #8b949e;
106
+ transition: fill 0.15s ease;
107
+ dominant-baseline: middle;
108
+ font-family: "Inter", "Segoe UI", system-ui, sans-serif;
109
+ }
110
+
111
+ .sl-node-group.active .sl-node-label,
112
+ .sl-node-group:hover .sl-node-label {
113
+ fill: #e6edf3;
114
+ }
115
+
116
+ .sl-node-group.active .sl-node-dot {
117
+ filter: brightness(1.4) drop-shadow(0 0 6px currentColor);
118
+ }
119
+
120
+ .sl-node-group.dimmed .sl-node-dot {
121
+ opacity: 0.2;
122
+ }
123
+
124
+ .sl-node-group.dimmed .sl-node-label {
125
+ opacity: 0.2;
126
+ }
127
+
128
+ /* ── Arcs ── */
129
+ .sl-arc {
130
+ fill: none;
131
+ transition:
132
+ opacity 0.2s ease,
133
+ stroke-width 0.2s ease;
134
+ }
135
+
136
+ .sl-arc.dimmed {
137
+ opacity: 0.03;
138
+ }
139
+
140
+ .sl-arc.highlighted {
141
+ opacity: 0.85;
142
+ stroke-width: 1.8px;
143
+ }
144
+
145
+ .sl-arc.default {
146
+ opacity: 0.18;
147
+ stroke-width: 1px;
148
+ }
149
+
150
+ /* ── Type badge on axis ── */
151
+ .sl-type-label {
152
+ font-size: 9px;
153
+ font-weight: 600;
154
+ letter-spacing: 0.08em;
155
+ text-transform: uppercase;
156
+ fill: #30363d;
157
+ font-family: "Inter", "Segoe UI", system-ui, sans-serif;
158
+ }
159
+
160
+ /* ── Detail Panel ── */
161
+ .sl-panel {
162
+ width: 300px;
163
+ min-width: 300px;
164
+ border-left: 1px solid #21262d;
165
+ background: #0d1117;
166
+ overflow-y: auto;
167
+ padding: 0;
168
+ scrollbar-width: thin;
169
+ scrollbar-color: #21262d transparent;
170
+ display: flex;
171
+ flex-direction: column;
172
+ position: sticky;
173
+ top: 0;
174
+ max-height: 100vh;
175
+ align-self: flex-start;
176
+ }
177
+
178
+ .sl-panel::-webkit-scrollbar {
179
+ width: 6px;
180
+ }
181
+
182
+ .sl-panel::-webkit-scrollbar-thumb {
183
+ background: #21262d;
184
+ border-radius: 3px;
185
+ }
186
+
187
+ .sl-panel-empty {
188
+ display: flex;
189
+ flex-direction: column;
190
+ align-items: center;
191
+ justify-content: center;
192
+ height: 100%;
193
+ gap: 10px;
194
+ color: #30363d;
195
+ padding: 40px;
196
+ text-align: center;
197
+ }
198
+
199
+ .sl-panel-empty svg {
200
+ opacity: 0.3;
201
+ margin-bottom: 4px;
202
+ }
203
+
204
+ .sl-panel-empty p {
205
+ font-size: 0.82rem;
206
+ line-height: 1.5;
207
+ margin: 0;
208
+ }
209
+
210
+ .sl-panel-header {
211
+ padding: 20px 22px 14px;
212
+ border-bottom: 1px solid #21262d;
213
+ position: sticky;
214
+ top: 0;
215
+ background: #0d1117;
216
+ z-index: 1;
217
+ }
218
+
219
+ .sl-panel-badge {
220
+ display: inline-flex;
221
+ align-items: center;
222
+ gap: 5px;
223
+ padding: 2px 9px;
224
+ border-radius: 12px;
225
+ font-size: 0.7rem;
226
+ font-weight: 600;
227
+ letter-spacing: 0.05em;
228
+ text-transform: uppercase;
229
+ margin-bottom: 8px;
230
+ }
231
+
232
+ .sl-panel-title {
233
+ font-size: 0.95rem;
234
+ font-weight: 600;
235
+ color: #f0f6fc;
236
+ word-break: break-all;
237
+ margin: 0;
238
+ }
239
+
240
+ .sl-panel-body {
241
+ padding: 16px 22px;
242
+ flex: 1;
243
+ }
244
+
245
+ .sl-section {
246
+ margin-bottom: 20px;
247
+ }
248
+
249
+ .sl-section-title {
250
+ font-size: 0.68rem;
251
+ font-weight: 600;
252
+ letter-spacing: 0.1em;
253
+ text-transform: uppercase;
254
+ color: #484f58;
255
+ margin-bottom: 8px;
256
+ display: flex;
257
+ align-items: center;
258
+ gap: 6px;
259
+ }
260
+
261
+ .sl-section-title::after {
262
+ content: "";
263
+ flex: 1;
264
+ height: 1px;
265
+ background: #21262d;
266
+ }
267
+
268
+ .sl-tag-list {
269
+ display: flex;
270
+ flex-wrap: wrap;
271
+ gap: 5px;
272
+ }
273
+
274
+ .sl-tag {
275
+ padding: 2px 8px;
276
+ border-radius: 6px;
277
+ font-size: 0.72rem;
278
+ font-weight: 500;
279
+ background: #161b22;
280
+ border: 1px solid #21262d;
281
+ color: #8b949e;
282
+ font-family: "ui-monospace", "SFMono-Regular", monospace;
283
+ }
284
+
285
+ .sl-tag.linked {
286
+ cursor: pointer;
287
+ border-color: #30363d;
288
+ }
289
+
290
+ .sl-tag.linked:hover {
291
+ border-color: #58a6ff;
292
+ color: #58a6ff;
293
+ }
294
+
295
+ /* ── Connection count ── */
296
+ .sl-connection-count {
297
+ display: flex;
298
+ align-items: center;
299
+ gap: 8px;
300
+ padding: 10px 0;
301
+ font-size: 0.8rem;
302
+ color: #8b949e;
303
+ border-bottom: 1px solid #21262d;
304
+ margin-bottom: 14px;
305
+ }
306
+
307
+ .sl-conn-badge {
308
+ display: flex;
309
+ align-items: center;
310
+ gap: 4px;
311
+ padding: 2px 8px;
312
+ border-radius: 10px;
313
+ font-size: 0.72rem;
314
+ font-weight: 600;
315
+ }
316
+
317
+ /* ── Welcome / empty state ── */
318
+ .sl-welcome {
319
+ display: flex;
320
+ flex-direction: column;
321
+ align-items: center;
322
+ justify-content: center;
323
+ height: 100vh;
324
+ gap: 16px;
325
+ text-align: center;
326
+ padding: 40px;
327
+ }
328
+
329
+ .sl-welcome-logo {
330
+ width: 80px;
331
+ height: 80px;
332
+ margin-bottom: 8px;
333
+ opacity: 0.85;
334
+ }
335
+
336
+ .sl-welcome-title {
337
+ font-size: 1.6rem;
338
+ font-weight: 700;
339
+ color: #f0f6fc;
340
+ margin: 0;
341
+ letter-spacing: -0.02em;
342
+ }
343
+
344
+ .sl-welcome-sub {
345
+ font-size: 0.9rem;
346
+ color: #8b949e;
347
+ margin: 0;
348
+ }
349
+
350
+ .sl-welcome-hint {
351
+ font-size: 0.85rem;
352
+ color: #484f58;
353
+ margin: 0;
354
+ }
355
+
356
+ .sl-welcome-btn {
357
+ display: inline-flex;
358
+ align-items: center;
359
+ gap: 6px;
360
+ margin-top: 8px;
361
+ padding: 9px 22px;
362
+ border-radius: 8px;
363
+ background: #a78bfa;
364
+ color: #0d1117;
365
+ font-size: 0.875rem;
366
+ font-weight: 600;
367
+ text-decoration: none;
368
+ transition:
369
+ background 0.15s ease,
370
+ transform 0.1s ease;
371
+ }
372
+
373
+ .sl-welcome-btn:hover {
374
+ background: #c4b5fd;
375
+ transform: translateY(-1px);
376
+ }
377
+ </style>
378
+
379
+ {{#if this.isEmpty}}
380
+
381
+ {{! ── Welcome / empty state ── }}
382
+ <div
383
+ class="bg-white vh-100 w-100 d-flex align-items-center justify-content-center flex-column"
384
+ >
385
+ <h1 class="display-6">Welcome to Tribe</h1>
386
+ <p class="lead">You have successfully installed
387
+ <strong>ember-tribe</strong></p>
388
+ <a
389
+ href="https://tribe-framework.org"
390
+ class="btn btn-success"
391
+ target="_blank"
392
+ rel="noopener noreferrer"
393
+ >
394
+ Read more
395
+ </a>
396
+ </div>
397
+
398
+ {{else}}
399
+ <div class="sl-root">
400
+
401
+ {{! ── Header ── }}
402
+ <header class="sl-header">
403
+ <h1>
404
+ Storylang
405
+ </h1>
406
+
407
+ <div class="sl-legend">
408
+ {{#each this.legendItems as |item|}}
409
+ <div class="sl-legend-item">
410
+ <span
411
+ class="sl-legend-dot"
412
+ style="background:{{item.color}}"
413
+ ></span>
414
+ {{item.label}}
415
+ </div>
416
+ {{/each}}
417
+ </div>
418
+ </header>
419
+
420
+ <div class="sl-body">
421
+
422
+ {{! ── Arc Diagram ── }}
423
+ <div class="sl-diagram-wrap">
424
+ <div class="sl-svg-container">
425
+ <Storylang::ArcDiagram
426
+ @nodes={{this.filteredNodes}}
427
+ @edges={{this.allEdges}}
428
+ @selectedNode={{this.selectedNode}}
429
+ @onSelectNode={{this.selectNode}}
430
+ />
431
+ </div>
432
+ </div>
433
+
434
+ {{! ── Detail Panel ── }}
435
+ <aside class="sl-panel">
436
+ {{#if this.selectedNode}}
437
+ <Storylang::NodeDetail
438
+ @node={{this.selectedNode}}
439
+ @edges={{this.selectedNodeEdges}}
440
+ @onClose={{this.clearSelection}}
441
+ @onNavigate={{this.selectNode}}
442
+ @allNodes={{this.allNodes}}
443
+ />
444
+ {{else}}
445
+ <div class="sl-panel-empty">
446
+ <p>Click any node on the diagram to inspect its connections and
447
+ properties.</p>
448
+ </div>
449
+ {{/if}}
450
+ </aside>
451
+
452
+ </div>
453
+ </div>
454
+
455
+ {{/if}}
456
+
457
+ <div {{did-insert this.loadModel}}></div>
@@ -0,0 +1,177 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
4
+ import { TYPE_COLOR } from './arc-diagram';
5
+
6
+ // Canonical display order for node types
7
+ const TYPE_ORDER = [
8
+ 'route',
9
+ 'service',
10
+ 'type',
11
+ 'helper',
12
+ 'modifier',
13
+ 'component',
14
+ ];
15
+
16
+ export default class StorylangIndexComponent extends Component {
17
+ @tracked selectedNode = null;
18
+ @tracked model = null;
19
+ @tracked isLoading = true;
20
+ @tracked isEmpty = false;
21
+
22
+ @action
23
+ async loadModel() {
24
+ try {
25
+ const response = await fetch('/storylang.json');
26
+
27
+ if (!response.ok) {
28
+ this.isEmpty = true;
29
+ return;
30
+ }
31
+
32
+ let data;
33
+ try {
34
+ data = await response.json();
35
+ } catch {
36
+ this.isEmpty = true;
37
+ return;
38
+ }
39
+
40
+ // Treat missing or fully-empty data as the welcome state
41
+ const hasContent =
42
+ data &&
43
+ [
44
+ 'routes',
45
+ 'services',
46
+ 'types',
47
+ 'helpers',
48
+ 'modifiers',
49
+ 'components',
50
+ ].some((key) => Array.isArray(data[key]) && data[key].length > 0);
51
+
52
+ if (hasContent) {
53
+ this.model = data;
54
+ } else {
55
+ this.isEmpty = true;
56
+ }
57
+ } catch {
58
+ this.isEmpty = true;
59
+ } finally {
60
+ this.isLoading = false;
61
+ }
62
+ }
63
+
64
+ // ── Legend ────────────────────────────────────────────────────────────────
65
+
66
+ get legendItems() {
67
+ return TYPE_ORDER.map((t) => ({
68
+ type: t,
69
+ label: t + 's',
70
+ color: TYPE_COLOR[t] ?? '#8895a7',
71
+ }));
72
+ }
73
+
74
+ // ── Nodes ─────────────────────────────────────────────────────────────────
75
+
76
+ get allNodes() {
77
+ const model = this.model;
78
+ if (!model) return [];
79
+
80
+ const nodes = [];
81
+
82
+ const addNodes = (items, type) => {
83
+ if (!items) return;
84
+ items.forEach((item) => {
85
+ nodes.push({
86
+ id: `${type}:${item.slug}`,
87
+ slug: item.slug,
88
+ type,
89
+ data: item,
90
+ });
91
+ });
92
+ };
93
+
94
+ addNodes(model.routes, 'route');
95
+ addNodes(model.services, 'service');
96
+ addNodes(model.types, 'type');
97
+ addNodes(model.helpers, 'helper');
98
+ addNodes(model.modifiers, 'modifier');
99
+ addNodes(model.components, 'component');
100
+
101
+ return nodes;
102
+ }
103
+
104
+ get filteredNodes() {
105
+ return this.allNodes;
106
+ }
107
+
108
+ // ── Edges ─────────────────────────────────────────────────────────────────
109
+
110
+ get allEdges() {
111
+ const nodes = this.allNodes;
112
+ const nodeIndex = {};
113
+ nodes.forEach((n, i) => {
114
+ nodeIndex[n.slug] = i;
115
+ });
116
+
117
+ const edges = [];
118
+ const model = this.model;
119
+
120
+ const addEdges = (item, itemType) => {
121
+ const sourceSlug = item.slug;
122
+
123
+ if (item.services) {
124
+ item.services.forEach((svc) => {
125
+ edges.push({
126
+ source: sourceSlug,
127
+ target: svc,
128
+ kind: 'service',
129
+ sourceType: itemType,
130
+ });
131
+ });
132
+ }
133
+
134
+ if (item.components) {
135
+ item.components.forEach((comp) => {
136
+ edges.push({
137
+ source: sourceSlug,
138
+ target: comp,
139
+ kind: 'component',
140
+ sourceType: itemType,
141
+ });
142
+ });
143
+ }
144
+ };
145
+
146
+ (model?.components || []).forEach((c) => addEdges(c, 'component'));
147
+ (model?.routes || []).forEach((r) => addEdges(r, 'route'));
148
+ (model?.services || []).forEach((s) => addEdges(s, 'service'));
149
+
150
+ return edges;
151
+ }
152
+
153
+ get selectedNodeEdges() {
154
+ if (!this.selectedNode) return [];
155
+ return this.allEdges.filter(
156
+ (e) =>
157
+ e.source === this.selectedNode.slug ||
158
+ e.target === this.selectedNode.slug,
159
+ );
160
+ }
161
+
162
+ // ── Actions ───────────────────────────────────────────────────────────────
163
+
164
+ @action
165
+ selectNode(node) {
166
+ if (this.selectedNode?.id === node.id) {
167
+ this.selectedNode = null;
168
+ } else {
169
+ this.selectedNode = node;
170
+ }
171
+ }
172
+
173
+ @action
174
+ clearSelection() {
175
+ this.selectedNode = null;
176
+ }
177
+ }