ember-tribe 2.6.6 → 2.6.8
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/README.md +416 -236
- package/blueprints/ember-tribe/files/storylang +63 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,18 @@ The addon automatically configures following essential packages:
|
|
|
26
26
|
|
|
27
27
|
**Ember Addons:** `ember-cli-dotenv`, `ember-cli-sass`, `ember-modifier`, `ember-composable-helpers`, `ember-truth-helpers`, `ember-file-upload` , `ember-power-select`
|
|
28
28
|
|
|
29
|
-
**NPM Packages:** `bootstrap`, `@popperjs/core`, `animate.css`, `video.js`, `swiper`, `howler`
|
|
29
|
+
**NPM Packages:** `bootstrap`, `@popperjs/core`, `animate.css`, `video.js`, `swiper`, `howler`, `sortablejs`, `papaparse`
|
|
30
|
+
|
|
31
|
+
**Built-in features that can be used in routes and components**:
|
|
32
|
+
|
|
33
|
+
- **Layout**: `table`, `figure`, `accordion`, `card`, `list-group`, `navbar`, `nav`, `tab`, `breadcrumb`
|
|
34
|
+
- **Interactive**: `button`, `button-group`, `dropdown`, `modal`, `collapse`, `offcanvas`, `pagination`, `popover`, `tooltip`, `swiper-carousel`, `videojs-player`, `howlerjs-player`, `input-field`, `input-group`, `textarea`, `checkbox`, `radio`, `range`, `select`, `multi-select`, `date`, `file-uploader`, `alert`, `badge`, `toast`, `placeholder`, `progress`, `spinner`, `scrollspy`
|
|
35
|
+
|
|
36
|
+
**Preinstalled services in ember-tribe**:
|
|
37
|
+
|
|
38
|
+
- `store`: Ember Data store for CRUD operations
|
|
39
|
+
- `router`: Ember router service for navigation
|
|
40
|
+
- `types`: Automatic model generation from backend tracks
|
|
30
41
|
|
|
31
42
|
---
|
|
32
43
|
|
|
@@ -50,6 +61,106 @@ installer.sh
|
|
|
50
61
|
|
|
51
62
|
---
|
|
52
63
|
|
|
64
|
+
## Best Practices for AI generated code
|
|
65
|
+
|
|
66
|
+
These rules are **mandatory** for all Tribe-compatible code. Follow them strictly and do not deviate unless explicitly instructed.
|
|
67
|
+
|
|
68
|
+
### General Rules
|
|
69
|
+
|
|
70
|
+
1. **Bootstrap 5.x — Required Foundation**
|
|
71
|
+
Use Bootstrap 5.x as the sole design system for all layout, spacing, and responsive behaviour. Do not introduce custom CSS frameworks or utility libraries that conflict with Bootstrap. Follow Bootstrap conventions strictly.
|
|
72
|
+
|
|
73
|
+
2. **Backend Field Access**
|
|
74
|
+
Always access backend fields through the `modules` object — e.g. `object.modules.field_name`. Never access backend fields directly.
|
|
75
|
+
|
|
76
|
+
3. **npm Packages over Ember Addons**
|
|
77
|
+
When an npm package and an Ember addon offer equivalent functionality, always prefer the npm package for better long-term compatibility.
|
|
78
|
+
|
|
79
|
+
4. **Icons — FontAwesome 6.x Only**
|
|
80
|
+
Use FontAwesome 6.x for all icons. Do not use any other icon library unless the project description explicitly specifies one.
|
|
81
|
+
|
|
82
|
+
5. **Animations — Subtle and Purposeful**
|
|
83
|
+
If animations are needed, use `animate.css`. Keep animations subtle — prefer fades and minimal slides. Avoid anything that feels flashy or distracting.
|
|
84
|
+
|
|
85
|
+
6. **EmberData Caching**
|
|
86
|
+
When data has already been loaded into the store, retrieve it with `peekRecord` instead of making a new network request.
|
|
87
|
+
|
|
88
|
+
7. **Backend Filtering over Frontend Filtering**
|
|
89
|
+
For sorting and filtering data, always use `this.store.query` with backend query parameters. Do not filter or sort arrays on the frontend when the backend can do it.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### Storylang Architecture Rules
|
|
94
|
+
|
|
95
|
+
Follow this strict order of thinking when designing any feature:
|
|
96
|
+
|
|
97
|
+
> **Understand Types → Routes → Controllers → Helpers → Modifiers → Services → Components**
|
|
98
|
+
|
|
99
|
+
Always begin by understanding your data types, then define the routes that load that data, then wire up controllers to handle user actions, then extract reusable template logic into helpers, then isolate DOM behaviour into modifiers, then move app-wide logic into services, and finally — only when the project's scale warrants it — extract repeatable UI into components.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
**Types**
|
|
104
|
+
|
|
105
|
+
11. **Start by Understanding Your Data**
|
|
106
|
+
Before writing any code, read the project description and `types.json` to understand the data model. Every architectural decision that follows — which routes to create, which services to build, whether components are even needed — depends on a clear understanding of the underlying types.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
**Routes**
|
|
111
|
+
|
|
112
|
+
8. **Route Naming**
|
|
113
|
+
Match route names to user mental models. Use consistent, predictable naming conventions so that routes are self-documenting.
|
|
114
|
+
|
|
115
|
+
9. **Routes Are for Fetching, Not Logic**
|
|
116
|
+
Routes should primarily perform read/fetch operations and pass data down to components or services. Keep JavaScript in routes to a minimum — business logic belongs in components and services, not routes.
|
|
117
|
+
|
|
118
|
+
10. **Route Parameters**
|
|
119
|
+
Keep `get_vars` minimal and meaningful. Load only the data types that each specific route actually needs — avoid over-fetching.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
**Controllers**
|
|
124
|
+
|
|
125
|
+
12. **Controllers Bridge Routes and Templates**
|
|
126
|
+
Controllers sit between routes and templates, handling query parameters, user actions, and transient UI state that belongs to a specific route. Keep controllers focused — they are not a place for business logic or data fetching.
|
|
127
|
+
|
|
128
|
+
13. **Keep Controllers Thin**
|
|
129
|
+
Delegate complex logic to services. A controller should primarily expose tracked properties and actions that the corresponding template needs directly.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
**Helpers**
|
|
134
|
+
|
|
135
|
+
17. **Helpers Must Be Pure and Stateless**
|
|
136
|
+
A helper receives input and returns output — nothing else. Helpers must have no side effects and must not interact with the store, services, or DOM.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
**Modifiers**
|
|
141
|
+
|
|
142
|
+
18. **Modifiers Own All DOM Interaction**
|
|
143
|
+
Any direct DOM manipulation or third-party library initialisation must live in a modifier.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
**Services**
|
|
148
|
+
|
|
149
|
+
15. **Services Are the Core Logic Layer**
|
|
150
|
+
Services hold the primary business logic of the application. They interact with both routes and components and are the single source of truth for app-wide behaviour.
|
|
151
|
+
|
|
152
|
+
16. **Keep Services Stateless When Possible**
|
|
153
|
+
Avoid storing transient state in services. Where services must depend on one another, use dependency injection.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
**Components**
|
|
158
|
+
|
|
159
|
+
14. **Components are not always required**
|
|
160
|
+
Before creating components, assess the scale of the project from its description. On small projects, fewer files means higher code readability — collapsing template logic directly into route templates is often the right call. On larger projects, the opposite is true: extracting repeatable UI into named components improves clarity, maintainability, and testability. Make this decision deliberately at the start, not as an afterthought.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
53
164
|
## Storylang CLI
|
|
54
165
|
|
|
55
166
|
ember-tribe ships with a command-line tool called `storylang` that synchronises the `config/storylang.json` specification with the actual Ember project files.
|
|
@@ -96,33 +207,18 @@ The storylang.json file contains seven main sections:
|
|
|
96
207
|
|
|
97
208
|
```json
|
|
98
209
|
{
|
|
99
|
-
"implementation_approach": "...",
|
|
100
210
|
"types": [...],
|
|
101
|
-
"components": [...],
|
|
102
211
|
"routes": [...],
|
|
103
|
-
"services": [...],
|
|
104
212
|
"helpers": [...],
|
|
105
|
-
"modifiers": [...]
|
|
213
|
+
"modifiers": [...],
|
|
214
|
+
"services": [...],
|
|
215
|
+
"components": [...]
|
|
106
216
|
}
|
|
107
217
|
```
|
|
108
218
|
|
|
109
219
|
### Section Definitions
|
|
110
220
|
|
|
111
|
-
### 1.
|
|
112
|
-
|
|
113
|
-
**Purpose**: Provides a high-level technical overview of how the frontend interface would work.
|
|
114
|
-
|
|
115
|
-
**Format**:
|
|
116
|
-
|
|
117
|
-
```json
|
|
118
|
-
{
|
|
119
|
-
"implementation_approach": "Two-paragraph description explaining technical approach and key functionality."
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
---
|
|
124
|
-
|
|
125
|
-
### 2. Types
|
|
221
|
+
### 1. Types
|
|
126
222
|
|
|
127
223
|
**Purpose**: Declares which data types from `types.json` and maps them to the components, routes, services, helpers and modifiers that consume them. This creates a traceable link between your data layer and your UI implementation.
|
|
128
224
|
|
|
@@ -147,52 +243,66 @@ The storylang.json file contains seven main sections:
|
|
|
147
243
|
|
|
148
244
|
---
|
|
149
245
|
|
|
150
|
-
###
|
|
246
|
+
### 2. Routes
|
|
151
247
|
|
|
152
|
-
**Purpose**: Defines
|
|
248
|
+
**Purpose**: Defines the application's routes and their requirements.
|
|
153
249
|
|
|
154
250
|
**Format**:
|
|
155
251
|
|
|
156
252
|
```json
|
|
157
253
|
{
|
|
158
|
-
"
|
|
254
|
+
"routes": [
|
|
159
255
|
{
|
|
160
|
-
"name": "
|
|
161
|
-
"type": "component-type",
|
|
256
|
+
"name": "route-name", //should match Ember router.js
|
|
162
257
|
"tracked_vars": [{ "<variableName>": "<dataType>" }],
|
|
163
|
-
"
|
|
258
|
+
"get_vars": [{ "<paramName>": "<dataType>" }],
|
|
164
259
|
"actions": ["action1", "action2"],
|
|
165
|
-
"helpers": ["helper1"
|
|
166
|
-
"
|
|
167
|
-
"
|
|
260
|
+
"helpers": ["helper1"],
|
|
261
|
+
"services": ["service1"],
|
|
262
|
+
"components": ["component1", "component2"],
|
|
263
|
+
"types": ["type1", "type2"]
|
|
168
264
|
}
|
|
169
265
|
]
|
|
170
266
|
}
|
|
171
267
|
```
|
|
172
268
|
|
|
173
|
-
|
|
269
|
+
---
|
|
174
270
|
|
|
175
|
-
|
|
176
|
-
|
|
271
|
+
### 3. Helpers
|
|
272
|
+
|
|
273
|
+
**Purpose**: Defines custom template helpers — pure functions used in templates to format, compute or transform data for display.
|
|
274
|
+
|
|
275
|
+
**Format**:
|
|
276
|
+
|
|
277
|
+
```json
|
|
278
|
+
{
|
|
279
|
+
"helpers": [
|
|
280
|
+
{
|
|
281
|
+
"name": "helper-name",
|
|
282
|
+
"description": "What this helper does",
|
|
283
|
+
"input_args": [{ "<argumentName>": "<dataType>" }],
|
|
284
|
+
"return": "<dataType>"
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
}
|
|
288
|
+
```
|
|
177
289
|
|
|
178
290
|
**Example**:
|
|
179
291
|
|
|
180
292
|
```json
|
|
181
293
|
{
|
|
182
|
-
"
|
|
294
|
+
"helpers": [
|
|
183
295
|
{
|
|
184
|
-
"name": "
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
"modifiers": ["tooltip"],
|
|
195
|
-
"services": ["store", "router"]
|
|
296
|
+
"name": "format-date",
|
|
297
|
+
"description": "Formats a raw ISO date string into a human-readable date",
|
|
298
|
+
"input_args": [{ "isoString": "string" }, { "format": "string" }],
|
|
299
|
+
"return": "string"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"name": "truncate-text",
|
|
303
|
+
"description": "Truncates a string to a given character limit and appends an ellipsis",
|
|
304
|
+
"input_args": [{ "text": "string" }, { "limit": "int" }],
|
|
305
|
+
"return": "string"
|
|
196
306
|
}
|
|
197
307
|
]
|
|
198
308
|
}
|
|
@@ -200,24 +310,41 @@ The storylang.json file contains seven main sections:
|
|
|
200
310
|
|
|
201
311
|
---
|
|
202
312
|
|
|
203
|
-
### 4.
|
|
313
|
+
### 4. Modifiers
|
|
204
314
|
|
|
205
|
-
**Purpose**: Defines
|
|
315
|
+
**Purpose**: Defines custom Ember modifiers — functions that directly interact with DOM elements to attach behaviour, third-party libraries or event listeners.
|
|
206
316
|
|
|
207
317
|
**Format**:
|
|
208
318
|
|
|
209
319
|
```json
|
|
210
320
|
{
|
|
211
|
-
"
|
|
321
|
+
"modifiers": [
|
|
212
322
|
{
|
|
213
|
-
"name": "
|
|
214
|
-
"
|
|
215
|
-
"
|
|
216
|
-
"
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
323
|
+
"name": "modifier-name",
|
|
324
|
+
"description": "What DOM behaviour this modifier applies",
|
|
325
|
+
"input_args": [{ "<argumentName>": "<dataType>" }],
|
|
326
|
+
"services": ["service1"]
|
|
327
|
+
}
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Example**:
|
|
333
|
+
|
|
334
|
+
```json
|
|
335
|
+
{
|
|
336
|
+
"modifiers": [
|
|
337
|
+
{
|
|
338
|
+
"name": "tooltip",
|
|
339
|
+
"description": "Initialises a Bootstrap tooltip on the target element using the provided label",
|
|
340
|
+
"input_args": [{ "label": "string" }, { "placement": "string" }],
|
|
341
|
+
"services": []
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
"name": "autofocus",
|
|
345
|
+
"description": "Sets focus on the target element when it is inserted into the DOM",
|
|
346
|
+
"input_args": [],
|
|
347
|
+
"services": []
|
|
221
348
|
}
|
|
222
349
|
]
|
|
223
350
|
}
|
|
@@ -245,18 +372,6 @@ The storylang.json file contains seven main sections:
|
|
|
245
372
|
}
|
|
246
373
|
```
|
|
247
374
|
|
|
248
|
-
**Built-in Services in ember-tribe**:
|
|
249
|
-
|
|
250
|
-
- `store`: Ember Data store for CRUD operations
|
|
251
|
-
- `router`: Ember router service for navigation
|
|
252
|
-
- `types`: Automatic model generation from backend tracks
|
|
253
|
-
- `bootstrap`: Bootstrap CSS framework with Popper.js
|
|
254
|
-
- `papaparse`: CSV parsing library
|
|
255
|
-
- `sortablejs`: Drag-and-drop sorting
|
|
256
|
-
- `swiperjs`: Touch slider/carousel
|
|
257
|
-
- `videojs`: Video player
|
|
258
|
-
- `howlerjs`: Audio player
|
|
259
|
-
|
|
260
375
|
**Example**:
|
|
261
376
|
|
|
262
377
|
```json
|
|
@@ -282,97 +397,47 @@ The storylang.json file contains seven main sections:
|
|
|
282
397
|
|
|
283
398
|
---
|
|
284
399
|
|
|
285
|
-
### 6.
|
|
286
|
-
|
|
287
|
-
**Purpose**: Defines custom template helpers — pure functions used in templates to format, compute or transform data for display.
|
|
288
|
-
|
|
289
|
-
**Format**:
|
|
290
|
-
|
|
291
|
-
```json
|
|
292
|
-
{
|
|
293
|
-
"helpers": [
|
|
294
|
-
{
|
|
295
|
-
"name": "helper-name",
|
|
296
|
-
"description": "What this helper does",
|
|
297
|
-
"args": [{ "<argumentName>": "<dataType>" }],
|
|
298
|
-
"return": "<dataType>"
|
|
299
|
-
}
|
|
300
|
-
]
|
|
301
|
-
}
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
**Helper Properties**:
|
|
305
|
-
|
|
306
|
-
- `name`: Kebab-case name of the helper
|
|
307
|
-
- `description`: Brief description of what the helper computes or transforms
|
|
308
|
-
- `args`: Input arguments the helper accepts
|
|
309
|
-
- `return`: The data type the helper outputs
|
|
310
|
-
|
|
311
|
-
**Example**:
|
|
312
|
-
|
|
313
|
-
```json
|
|
314
|
-
{
|
|
315
|
-
"helpers": [
|
|
316
|
-
{
|
|
317
|
-
"name": "format-date",
|
|
318
|
-
"description": "Formats a raw ISO date string into a human-readable date",
|
|
319
|
-
"args": [{ "isoString": "string" }, { "format": "string" }],
|
|
320
|
-
"return": "string"
|
|
321
|
-
},
|
|
322
|
-
{
|
|
323
|
-
"name": "truncate-text",
|
|
324
|
-
"description": "Truncates a string to a given character limit and appends an ellipsis",
|
|
325
|
-
"args": [{ "text": "string" }, { "limit": "int" }],
|
|
326
|
-
"return": "string"
|
|
327
|
-
}
|
|
328
|
-
]
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
---
|
|
333
|
-
|
|
334
|
-
### 7. Modifiers
|
|
400
|
+
### 6. Components
|
|
335
401
|
|
|
336
|
-
**Purpose**: Defines
|
|
402
|
+
**Purpose**: Defines reusable UI components that will be built for the application.
|
|
337
403
|
|
|
338
404
|
**Format**:
|
|
339
405
|
|
|
340
406
|
```json
|
|
341
407
|
{
|
|
342
|
-
"
|
|
408
|
+
"components": [
|
|
343
409
|
{
|
|
344
|
-
"name": "
|
|
345
|
-
"
|
|
346
|
-
"
|
|
347
|
-
"
|
|
410
|
+
"name": "component-name",
|
|
411
|
+
"type": "component-type",
|
|
412
|
+
"tracked_vars": [{ "<variableName>": "<dataType>" }],
|
|
413
|
+
"inherited_args": [{ "<argumentName>": "<argType>" }],
|
|
414
|
+
"actions": ["action1", "action2"],
|
|
415
|
+
"helpers": ["helper1", "helper2"],
|
|
416
|
+
"modifiers": ["modifier1"],
|
|
417
|
+
"services": ["service1", "service2"]
|
|
348
418
|
}
|
|
349
419
|
]
|
|
350
420
|
}
|
|
351
421
|
```
|
|
352
422
|
|
|
353
|
-
**Modifier Properties**:
|
|
354
|
-
|
|
355
|
-
- `name`: Kebab-case name of the modifier
|
|
356
|
-
- `description`: Brief description of the DOM behaviour it attaches
|
|
357
|
-
- `args`: Arguments passed into the modifier from the template
|
|
358
|
-
- `services`: Ember services injected if needed
|
|
359
|
-
|
|
360
423
|
**Example**:
|
|
361
424
|
|
|
362
425
|
```json
|
|
363
426
|
{
|
|
364
|
-
"
|
|
365
|
-
{
|
|
366
|
-
"name": "tooltip",
|
|
367
|
-
"description": "Initialises a Bootstrap tooltip on the target element using the provided label",
|
|
368
|
-
"args": [{ "label": "string" }, { "placement": "string" }],
|
|
369
|
-
"services": []
|
|
370
|
-
},
|
|
427
|
+
"components": [
|
|
371
428
|
{
|
|
372
|
-
"name": "
|
|
373
|
-
"
|
|
374
|
-
"
|
|
375
|
-
"
|
|
429
|
+
"name": "file-summary-card",
|
|
430
|
+
"type": "card",
|
|
431
|
+
"tracked_vars": [{ "isSelected": "bool" }, { "isExpanded": "bool" }],
|
|
432
|
+
"inherited_args": [
|
|
433
|
+
{ "file": "var" },
|
|
434
|
+
{ "onEdit": "action" },
|
|
435
|
+
{ "onDelete": "action" }
|
|
436
|
+
],
|
|
437
|
+
"actions": ["toggleSelection", "expandDetails", "editFile", "deleteFile"],
|
|
438
|
+
"helpers": ["formatDate", "truncateText"],
|
|
439
|
+
"modifiers": ["tooltip"],
|
|
440
|
+
"services": ["store", "router"]
|
|
376
441
|
}
|
|
377
442
|
]
|
|
378
443
|
}
|
|
@@ -409,25 +474,6 @@ The storylang.json file contains seven main sections:
|
|
|
409
474
|
|
|
410
475
|
---
|
|
411
476
|
|
|
412
|
-
### Storylang Best Practices
|
|
413
|
-
|
|
414
|
-
**Always begin your thought process with routes → then move repeatable template logic into components → then move repeatable app-wide logic from components and routes to services → then extract reusable template and component functions into helpers → then extract template and component DOM behaviour into modifiers**
|
|
415
|
-
|
|
416
|
-
1. **Start with Routes**: Match route names to user mental models and use consistent naming conventions
|
|
417
|
-
2. **Minimize Route Logic**: Preferably fetch (read) type data in routes and then pass that data to components or services. Other than fetching type data, minimize the use of javascript in routes — Javascript is meant to be in components and services more than in routes
|
|
418
|
-
3. **Route Parameters**: Keep get_vars minimal and meaningful, and load only necessary types for each route
|
|
419
|
-
4. **Component Focus**: Keep components focused on single responsibilities and use descriptive, kebab-case names. Names should indicate which built-in ember-tribe component to use.
|
|
420
|
-
5. **Data Flow**: Receive backend data down from routes (via inherited_args) rather than fetching (reading) in components
|
|
421
|
-
6. **Component Actions**: Non-read functions — create, update, delete — can all happen well at component-level
|
|
422
|
-
7. **Service Integration**: Use services directly in both components and routes for app-wide logic
|
|
423
|
-
8. **Service Architecture**: Keep services stateless when possible and use dependency injection for service composition
|
|
424
|
-
9. **Service Role**: Services interact with both routes and components and store the core logic of the application
|
|
425
|
-
10. **Helpers**: Keep helpers pure and stateless — they should only receive input and return output with no side effects
|
|
426
|
-
11. **Modifiers**: Use modifiers to isolate all direct DOM manipulation and third-party library initialisation from component logic
|
|
427
|
-
12. **Types Mapping**: Populate the `types` section as you define your routes and components — it is your traceability layer between data types and UI
|
|
428
|
-
|
|
429
|
-
---
|
|
430
|
-
|
|
431
477
|
## Ember-Tribe Development Guide
|
|
432
478
|
|
|
433
479
|
### Required File Outputs
|
|
@@ -615,66 +661,6 @@ post.destroyRecord(); // => DELETE request
|
|
|
615
661
|
|
|
616
662
|
---
|
|
617
663
|
|
|
618
|
-
## Component Architecture
|
|
619
|
-
|
|
620
|
-
### Component Structure
|
|
621
|
-
|
|
622
|
-
```javascript
|
|
623
|
-
// Component class
|
|
624
|
-
import Component from '@glimmer/component';
|
|
625
|
-
import { tracked } from '@glimmer/tracking';
|
|
626
|
-
import { action } from '@ember/object';
|
|
627
|
-
import { service } from '@ember/service';
|
|
628
|
-
|
|
629
|
-
export default class FileCardComponent extends Component {
|
|
630
|
-
@service store;
|
|
631
|
-
@tracked isSelected = false;
|
|
632
|
-
|
|
633
|
-
@action
|
|
634
|
-
toggleSelection() {
|
|
635
|
-
this.isSelected = !this.isSelected;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
### Template Patterns
|
|
641
|
-
|
|
642
|
-
```handlebars
|
|
643
|
-
<div
|
|
644
|
-
class='card {{if this.isSelected "border-primary"}}'
|
|
645
|
-
{{on 'click' this.toggleSelection}}
|
|
646
|
-
>
|
|
647
|
-
<div class='card-body'>
|
|
648
|
-
<h5 class='card-title'>{{@file.modules.title}}</h5>
|
|
649
|
-
<p class='card-text'>{{@file.modules.description}}</p>
|
|
650
|
-
</div>
|
|
651
|
-
</div>
|
|
652
|
-
```
|
|
653
|
-
|
|
654
|
-
---
|
|
655
|
-
|
|
656
|
-
## Services Integration
|
|
657
|
-
|
|
658
|
-
Make services based on storylang.json service definitions:
|
|
659
|
-
|
|
660
|
-
```javascript
|
|
661
|
-
// app/services/visualization-builder.js
|
|
662
|
-
import Service from '@ember/service';
|
|
663
|
-
import { tracked } from '@glimmer/tracking';
|
|
664
|
-
import { service } from '@ember/service';
|
|
665
|
-
|
|
666
|
-
export default class VisualizationBuilderService extends Service {
|
|
667
|
-
@service store;
|
|
668
|
-
@tracked supportedTypes = ['network', 'tree', 'sankey'];
|
|
669
|
-
|
|
670
|
-
buildVisualization(files, type, config) {
|
|
671
|
-
// Service logic implementation
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
---
|
|
677
|
-
|
|
678
664
|
## Route Generation
|
|
679
665
|
|
|
680
666
|
### Route Creation
|
|
@@ -936,9 +922,69 @@ export default class CartContentsComponent extends Component {
|
|
|
936
922
|
|
|
937
923
|
---
|
|
938
924
|
|
|
939
|
-
##
|
|
925
|
+
## Services Integration
|
|
940
926
|
|
|
941
|
-
|
|
927
|
+
Make services based on storylang.json service definitions:
|
|
928
|
+
|
|
929
|
+
```javascript
|
|
930
|
+
// app/services/visualization-builder.js
|
|
931
|
+
import Service from '@ember/service';
|
|
932
|
+
import { tracked } from '@glimmer/tracking';
|
|
933
|
+
import { service } from '@ember/service';
|
|
934
|
+
|
|
935
|
+
export default class VisualizationBuilderService extends Service {
|
|
936
|
+
@service store;
|
|
937
|
+
@tracked supportedTypes = ['network', 'tree', 'sankey'];
|
|
938
|
+
|
|
939
|
+
buildVisualization(files, type, config) {
|
|
940
|
+
// Service logic implementation
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
---
|
|
946
|
+
|
|
947
|
+
## Component Architecture
|
|
948
|
+
|
|
949
|
+
### Component Structure
|
|
950
|
+
|
|
951
|
+
```javascript
|
|
952
|
+
// Component class
|
|
953
|
+
import Component from '@glimmer/component';
|
|
954
|
+
import { tracked } from '@glimmer/tracking';
|
|
955
|
+
import { action } from '@ember/object';
|
|
956
|
+
import { service } from '@ember/service';
|
|
957
|
+
|
|
958
|
+
export default class FileCardComponent extends Component {
|
|
959
|
+
@service store;
|
|
960
|
+
@tracked isSelected = false;
|
|
961
|
+
|
|
962
|
+
@action
|
|
963
|
+
toggleSelection() {
|
|
964
|
+
this.isSelected = !this.isSelected;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
### Template Patterns
|
|
970
|
+
|
|
971
|
+
```handlebars
|
|
972
|
+
<div
|
|
973
|
+
class='card {{if this.isSelected "border-primary"}}'
|
|
974
|
+
{{on 'click' this.toggleSelection}}
|
|
975
|
+
>
|
|
976
|
+
<div class='card-body'>
|
|
977
|
+
<h5 class='card-title'>{{@file.modules.title}}</h5>
|
|
978
|
+
<p class='card-text'>{{@file.modules.description}}</p>
|
|
979
|
+
</div>
|
|
980
|
+
</div>
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
---
|
|
984
|
+
|
|
985
|
+
## Forms and Input Fields
|
|
986
|
+
|
|
987
|
+
### File upload javascript example
|
|
942
988
|
|
|
943
989
|
```javascript
|
|
944
990
|
import ENV from '<your-application-name>/config/environment';
|
|
@@ -968,6 +1014,153 @@ async uploadFile(file) {
|
|
|
968
1014
|
}
|
|
969
1015
|
```
|
|
970
1016
|
|
|
1017
|
+
### Input and Textarea fields
|
|
1018
|
+
|
|
1019
|
+
Use Ember's built-in `<Input>` component instead of a raw `<input>` tag — it automatically updates bound state via `@value`.
|
|
1020
|
+
|
|
1021
|
+
```handlebars
|
|
1022
|
+
<div class="mb-3">
|
|
1023
|
+
<label for="input-name" class="form-label">Name:</label>
|
|
1024
|
+
<Input
|
|
1025
|
+
id="input-name"
|
|
1026
|
+
class="form-control"
|
|
1027
|
+
@type="text"
|
|
1028
|
+
@value={{this.name}}
|
|
1029
|
+
disabled={{this.isReadOnly}}
|
|
1030
|
+
maxlength="50"
|
|
1031
|
+
placeholder="Enter your name"
|
|
1032
|
+
/>
|
|
1033
|
+
</div>
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
```javascript
|
|
1037
|
+
import Component from '@glimmer/component';
|
|
1038
|
+
import { tracked } from '@glimmer/tracking';
|
|
1039
|
+
|
|
1040
|
+
export default class ExampleComponent extends Component {
|
|
1041
|
+
@tracked name = '';
|
|
1042
|
+
@tracked isReadOnly = false;
|
|
1043
|
+
}
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
```handlebars
|
|
1047
|
+
<div class="form-check mb-3">
|
|
1048
|
+
<Input
|
|
1049
|
+
id="admin-checkbox"
|
|
1050
|
+
class="form-check-input"
|
|
1051
|
+
@type="checkbox"
|
|
1052
|
+
@checked={{this.isAdmin}}
|
|
1053
|
+
{{on "input" this.validateRole}}
|
|
1054
|
+
/>
|
|
1055
|
+
<label for="admin-checkbox" class="form-check-label">Is Admin?</label>
|
|
1056
|
+
</div>
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
```handlebars
|
|
1060
|
+
<div class="mb-3">
|
|
1061
|
+
<label for="user-comment" class="form-label">Comment:</label>
|
|
1062
|
+
<Textarea
|
|
1063
|
+
id="user-comment"
|
|
1064
|
+
class="form-control"
|
|
1065
|
+
@value={{this.userComment}}
|
|
1066
|
+
rows="6"
|
|
1067
|
+
/>
|
|
1068
|
+
</div>
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
**Key rules for `<Input>` and `<Textarea>`:**
|
|
1072
|
+
- `@value`, `@type`, and `@checked` must be passed as **arguments** (with `@`).
|
|
1073
|
+
- Use the `{{on}}` modifier for event handling (e.g. `{{on "input" this.handler}}`).
|
|
1074
|
+
- Bootstrap styles `form-control` correctly when `disabled` is present
|
|
1075
|
+
|
|
1076
|
+
### ember-power-select example
|
|
1077
|
+
|
|
1078
|
+
`ember-power-select` is the recommended way to implement searchable, single, and multi-select dropdowns in ember-tribe. It is pre-installed and works alongside Bootstrap 5.x. Use it wherever a native `<select>` would be insufficient — e.g. when you need search/filter, async options, or multi-select.
|
|
1079
|
+
|
|
1080
|
+
**Single select (Bootstrap-compatible wrapper):**
|
|
1081
|
+
|
|
1082
|
+
```handlebars
|
|
1083
|
+
<div class="mb-3">
|
|
1084
|
+
<label class="form-label">Assign Category:</label>
|
|
1085
|
+
<div class="form-control p-0 border-0">
|
|
1086
|
+
<PowerSelect
|
|
1087
|
+
@options={{this.categories}}
|
|
1088
|
+
@selected={{this.selectedCategory}}
|
|
1089
|
+
@onChange={{this.handleCategoryChange}}
|
|
1090
|
+
@placeholder="Select a category"
|
|
1091
|
+
as |category|
|
|
1092
|
+
>
|
|
1093
|
+
{{category.name}}
|
|
1094
|
+
</PowerSelect>
|
|
1095
|
+
</div>
|
|
1096
|
+
</div>
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
```javascript
|
|
1100
|
+
import Component from '@glimmer/component';
|
|
1101
|
+
import { tracked } from '@glimmer/tracking';
|
|
1102
|
+
import { action } from '@ember/object';
|
|
1103
|
+
|
|
1104
|
+
export default class ExampleComponent extends Component {
|
|
1105
|
+
@tracked selectedCategory = null;
|
|
1106
|
+
|
|
1107
|
+
categories = [
|
|
1108
|
+
{ id: 1, name: 'Design' },
|
|
1109
|
+
{ id: 2, name: 'Engineering' },
|
|
1110
|
+
{ id: 3, name: 'Marketing' },
|
|
1111
|
+
];
|
|
1112
|
+
|
|
1113
|
+
@action
|
|
1114
|
+
handleCategoryChange(category) {
|
|
1115
|
+
this.selectedCategory = category;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
**Multi-select variant:**
|
|
1121
|
+
|
|
1122
|
+
```handlebars
|
|
1123
|
+
<div class="mb-3">
|
|
1124
|
+
<label class="form-label">Assign Tags:</label>
|
|
1125
|
+
<div class="form-control p-0 border-0">
|
|
1126
|
+
<PowerSelectMultiple
|
|
1127
|
+
@options={{this.availableTags}}
|
|
1128
|
+
@selected={{this.selectedTags}}
|
|
1129
|
+
@onChange={{this.handleTagsChange}}
|
|
1130
|
+
@placeholder="Select tags"
|
|
1131
|
+
as |tag|
|
|
1132
|
+
>
|
|
1133
|
+
{{tag.label}}
|
|
1134
|
+
</PowerSelectMultiple>
|
|
1135
|
+
</div>
|
|
1136
|
+
</div>
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
**Async options loaded from the store:**
|
|
1140
|
+
|
|
1141
|
+
```handlebars
|
|
1142
|
+
<div class="mb-3">
|
|
1143
|
+
<label class="form-label">Select Project:</label>
|
|
1144
|
+
<div class="form-control p-0 border-0">
|
|
1145
|
+
<PowerSelect
|
|
1146
|
+
@options={{this.projects}}
|
|
1147
|
+
@selected={{this.selectedProject}}
|
|
1148
|
+
@onChange={{this.handleProjectChange}}
|
|
1149
|
+
@searchField="name"
|
|
1150
|
+
@placeholder="Search projects..."
|
|
1151
|
+
as |project|
|
|
1152
|
+
>
|
|
1153
|
+
{{project.modules.name}}
|
|
1154
|
+
</PowerSelect>
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
**Key rules for `<PowerSelect>`:**
|
|
1160
|
+
- `@options`, `@selected`, and `@onChange` are always required arguments.
|
|
1161
|
+
- Use `@searchField` to specify which object property drives the built-in search filter.
|
|
1162
|
+
- For multi-select, use `<PowerSelectMultiple>` — the `@onChange` callback receives the full updated array, so assign it directly to your tracked property.
|
|
1163
|
+
|
|
971
1164
|
---
|
|
972
1165
|
|
|
973
1166
|
## Deploying to Junction (Self-Hosted)
|
|
@@ -985,19 +1178,6 @@ You can then upload the `dist/` folder to [Junction (open source)](http://localh
|
|
|
985
1178
|
|
|
986
1179
|
---
|
|
987
1180
|
|
|
988
|
-
## Best Practices
|
|
989
|
-
|
|
990
|
-
1. **Module Access**: Remember to use `modules.field_name` for backend fields
|
|
991
|
-
2. **npm packages vs ember addons**: Prioritize npm packages over ember addons for better compatibility, when both are offering similar functionality
|
|
992
|
-
3. **Minimal Controllers**: Logic should ideally reside in components and services
|
|
993
|
-
4. **Bootstrap 5.x Foundation**: Responsive, accessible design system
|
|
994
|
-
5. **Use FontAwesome 6.x**: Comprehensive icon library
|
|
995
|
-
6. **Animations**: If required, use animate.css for enhanced user experience. Prefer subtle animations (fadeIn, slideIn). Avoid overwhelming users with excessive animation
|
|
996
|
-
7. **Access Cache**: Leverage EmberData caching with `peekRecord`
|
|
997
|
-
8. **Avoid array manipulations of backend data**: Use backend filtering over frontend array manipulation
|
|
998
|
-
|
|
999
|
-
---
|
|
1000
|
-
|
|
1001
1181
|
# License
|
|
1002
1182
|
|
|
1003
1183
|
This project is licensed under the [GNU GPL v3 License](LICENSE.md).
|
|
@@ -109,7 +109,7 @@ function parseHbsFile(filePath) {
|
|
|
109
109
|
const helpers = [...helpersSet];
|
|
110
110
|
|
|
111
111
|
const modifiersSet = new Set(
|
|
112
|
-
[...src.matchAll(/\{\{([\w-]+-modifier|
|
|
112
|
+
[...src.matchAll(/\{\{([\w-]+-modifier|tooltip|[\w-]+)\s/g)]
|
|
113
113
|
.map(([, name]) => name)
|
|
114
114
|
.filter((n) => !helpers.includes(n) && !builtinHelpers.has(n))
|
|
115
115
|
);
|
|
@@ -199,6 +199,65 @@ function buildTypes(routes, components) {
|
|
|
199
199
|
return [...typeMap.entries()].map(([slug, used_in]) => ({ slug, used_in }));
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
function compactJSON(value, indent = 2) {
|
|
203
|
+
function isLeafArray(arr) {
|
|
204
|
+
if (!Array.isArray(arr) || arr.length === 0) return false;
|
|
205
|
+
return arr.every((item) => {
|
|
206
|
+
if (typeof item === 'string') return true;
|
|
207
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
208
|
+
const keys = Object.keys(item);
|
|
209
|
+
return keys.length === 1 && typeof item[keys[0]] === 'string';
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function serialize(val, depth) {
|
|
216
|
+
const pad = ' '.repeat(indent * depth);
|
|
217
|
+
const childPad = ' '.repeat(indent * (depth + 1));
|
|
218
|
+
|
|
219
|
+
if (val === null) return 'null';
|
|
220
|
+
if (typeof val !== 'object') return JSON.stringify(val);
|
|
221
|
+
|
|
222
|
+
if (Array.isArray(val)) {
|
|
223
|
+
if (val.length === 0) return '[]';
|
|
224
|
+
if (isLeafArray(val)) {
|
|
225
|
+
const items = val.map((item) => {
|
|
226
|
+
if (typeof item === 'string') return JSON.stringify(item);
|
|
227
|
+
const k = Object.keys(item)[0];
|
|
228
|
+
return `{ ${JSON.stringify(k)}: ${JSON.stringify(item[k])} }`;
|
|
229
|
+
});
|
|
230
|
+
return '[' + items.join(', ') + ']';
|
|
231
|
+
}
|
|
232
|
+
const items = val.map((item) => childPad + serialize(item, depth + 1));
|
|
233
|
+
return '[\n' + items.join(',\n') + '\n' + pad + ']';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const keys = Object.keys(val);
|
|
237
|
+
if (keys.length === 0) return '{}';
|
|
238
|
+
const entries = keys.map((k) => childPad + JSON.stringify(k) + ': ' + serialize(val[k], depth + 1));
|
|
239
|
+
return '{\n' + entries.join(',\n') + '\n' + pad + '}';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return serialize(value, 0);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function stripEmpty(obj) {
|
|
246
|
+
if (Array.isArray(obj)) {
|
|
247
|
+
return obj.map(stripEmpty);
|
|
248
|
+
}
|
|
249
|
+
if (obj !== null && typeof obj === 'object') {
|
|
250
|
+
const out = {};
|
|
251
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
252
|
+
if (Array.isArray(v) && v.length === 0) continue;
|
|
253
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0) continue;
|
|
254
|
+
out[k] = stripEmpty(v);
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
return obj;
|
|
259
|
+
}
|
|
260
|
+
|
|
202
261
|
function mergeByKey(existing, scanned, key) {
|
|
203
262
|
const existingMap = new Map(existing.map((e) => [e[key], e]));
|
|
204
263
|
const scannedMap = new Map(scanned.map((s) => [s[key], s]));
|
|
@@ -217,11 +276,11 @@ function mergeByKey(existing, scanned, key) {
|
|
|
217
276
|
const outputFile = path.join(configDir, 'storylang.json');
|
|
218
277
|
|
|
219
278
|
if (!fs.existsSync(appDir)) {
|
|
220
|
-
console.error(`Could not find app/ directory at ${appDir}. Make sure you are running from the
|
|
279
|
+
console.error(`Could not find app/ directory at ${appDir}. Make sure you are running from the folder of your Ember project.`);
|
|
221
280
|
process.exit(1);
|
|
222
281
|
}
|
|
223
282
|
|
|
224
|
-
console.log('
|
|
283
|
+
console.log('Storylang — scanning project files…\n');
|
|
225
284
|
|
|
226
285
|
const components = buildComponents(appDir);
|
|
227
286
|
const routes = buildRoutes(appDir);
|
|
@@ -245,7 +304,7 @@ function mergeByKey(existing, scanned, key) {
|
|
|
245
304
|
};
|
|
246
305
|
|
|
247
306
|
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
248
|
-
fs.writeFileSync(outputFile,
|
|
307
|
+
fs.writeFileSync(outputFile, compactJSON(stripEmpty(merged)) + '\n', 'utf8');
|
|
249
308
|
|
|
250
309
|
console.log(`✅ config/storylang.json updated`);
|
|
251
310
|
console.log(` routes: ${routes.length}`);
|