create-lupine 1.0.16 β 1.0.18
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/package.json +1 -1
- package/templates/common/AI_CONTEXT.md +593 -526
- package/templates/common/tsconfig.json +128 -0
- package/templates/lupine-template-cv-starter/web/src/lupine-template-cv-starter/index.tsx +13 -18
- package/templates/lupine-template-doc-starter/web/src/lupine-template-doc-starter/index.tsx +13 -18
- package/templates/lupine-template-doc-starter/web/src/markdown-built/markdown-config.ts +25 -25
- package/templates/lupine-template-responsive-starter/web/src/frames/app-responsive-frame.tsx +11 -5
- package/templates/lupine-template-responsive-starter/web/src/index.html +1 -1
|
@@ -1,526 +1,593 @@
|
|
|
1
|
-
# AI Context for Lupine.js
|
|
2
|
-
|
|
3
|
-
**SYSTEM ROLE**: You are an expert developer in `lupine.js`, a custom TypeScript full-stack framework.
|
|
4
|
-
|
|
5
|
-
**π CRITICAL WARNINGS π**
|
|
6
|
-
|
|
7
|
-
1. **`useState` EXISTS but rerenders the whole component**: Use it for simple/small components. For complex or large components, prefer `HtmlVar` for surgical, partial updates.
|
|
8
|
-
2. **NO VIRTUAL DOM STATE by default**: Without `useState`, changing a variable DOES NOT re-render the component. You must manually update `HtmlVar.value`.
|
|
9
|
-
3. **NO CONTROLLED INPUTS**: Do not bind `value={state}`. Read values from DOM on submit.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## 1. Core Philosophy & Reactivity
|
|
14
|
-
|
|
15
|
-
- **`useState` β React-style local state (small/simple components)**:
|
|
16
|
-
|
|
17
|
-
- Import: `import { useState } from 'lupine.components';`
|
|
18
|
-
- Syntax: `const [value, setValue] = useState(initial);` β calling `setValue(...)` rerenders the **entire** component.
|
|
19
|
-
- β
**Use when**: The component is small, state drives most of the UI, and the React-style patterns feel natural.
|
|
20
|
-
- β οΈ **Avoid when**: The component is large/complex, or only a tiny portion of the UI needs to change (e.g. a progress counter, a list inside a page) β repeated full rerenders are wasteful.
|
|
21
|
-
- **`ref.onLoad` + useState**: `onLoad` is called **only on initial mount** (not on rerenders). It's the right place for async data fetch that populates state.
|
|
22
|
-
|
|
23
|
-
- **`HtmlVar` β Surgical partial updates (large/complex components)**:
|
|
24
|
-
|
|
25
|
-
- Use `HtmlVar` to wrap dynamic sections (lists, conditional renderings, async content).
|
|
26
|
-
- **Pattern**: `const dom = new HtmlVar(initialContent);` β JSX `{dom.node}` β `dom.value = updatedContent`.
|
|
27
|
-
- β
**Use when**: Only a small part of a large component changes (e.g. list inside a page, progress text), or state is updated by external hooks (`props.hook.onProgress`), or high-frequency updates (file upload progress).
|
|
28
|
-
- The rest of the component DOM is never touched β highly efficient.
|
|
29
|
-
|
|
30
|
-
- **Direct DOM Access**:
|
|
31
|
-
- Use `RefProps` to get reference to the component root.
|
|
32
|
-
- Use `ref.$(selector)` to find the first element, `ref.$all(selector)` to find all elements (inputs, containers).
|
|
33
|
-
- **Value Retrieval**: `const val = ref.$('input.my-class').value`.
|
|
34
|
-
|
|
35
|
-
## 2. Key Interfaces
|
|
36
|
-
|
|
37
|
-
### `RefProps` (Lifecycle & DOM)
|
|
38
|
-
|
|
39
|
-
```typescript
|
|
40
|
-
const ref: RefProps = {
|
|
41
|
-
// Mounted: Initialize data, timers, events
|
|
42
|
-
onLoad: async (el: Element) => {
|
|
43
|
-
await loadData();
|
|
44
|
-
// ref.$('.sub-element').addEventListener(...)
|
|
45
|
-
},
|
|
46
|
-
// Unmounting: Cleanup
|
|
47
|
-
onUnload: async (el: Element) => {
|
|
48
|
-
// Cleanup (timers, sockets)
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
// Usage
|
|
52
|
-
<div ref={ref}>...</div>;
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### `CssProps` (Styling)
|
|
56
|
-
|
|
57
|
-
Supports nesting and media queries. **Prefer this over inline styles.** Define your styles in a `CssProps` object and bind them to the component's root JSX using the `css={css}` property. Use the `&` ampersand pattern (explained below) to guarantee unique class scoping.
|
|
58
|
-
|
|
59
|
-
## 3. Styles & Themes ("The Look")
|
|
60
|
-
|
|
61
|
-
### Global Variables (Theming) & Dark Mode Compatibility
|
|
62
|
-
|
|
63
|
-
**NEVER hardcode colors** (e.g., `#000`, `#fff`, `#f0f0f0`). Always use CSS variables to support Dark/Light modes. If you must use a fallback, wrap it: `var(--primary-bg-color, #fff)`.
|
|
64
|
-
|
|
65
|
-
#### π¨ Color Variable Semantics (CRITICAL FOR DARK MODE)
|
|
66
|
-
|
|
67
|
-
1. **Backgrounds (`--primary-bg-color` vs `--secondary-bg-color`)**:
|
|
68
|
-
- `--primary-bg-color`: The lowest-level background (White in light mode, **Deep Black** in dark mode).
|
|
69
|
-
- `--secondary-bg-color`: An elevated background (Light gray in light mode, **Lighter Black/Gray** in dark mode).
|
|
70
|
-
- _β οΈ Dark Mode Trap_: If you place a floating panel/card on the main body, **do NOT** use `--primary-bg-color` for the panel. It will blend into the body's deep black and become invisible. Use `--secondary-bg-color` for elevated panels to ensure visual separation.
|
|
71
|
-
2. **Text Colors (`--primary-color` vs `--secondary-color`)**:
|
|
72
|
-
- `--primary-color`: The primary **TEXT** color. (Dark grey/black in light mode, **White** in dark mode).
|
|
73
|
-
- _β οΈ Dark Mode Trap_: **Never** use `--primary-color` as the background color for a blue "Primary Action Button". It will turn white in dark mode.
|
|
74
|
-
- Always explicitly declare `color: 'var(--primary-color, inherit)'` on cards/containers so child text properly flips white in dark mode.
|
|
75
|
-
3. **Action / Brand Colors (`--primary-accent-color`)**:
|
|
76
|
-
- `--primary-accent-color`: The vibrant brand color (e.g., Lupine Blue). Use this for the **backgrounds of primary buttons**, active tabs, slider fills, and highlights.
|
|
77
|
-
- When using this as a background, set the text color to `var(--primary-bg-color)` so it stays high-contrast (white) in both themes.
|
|
78
|
-
4. **Borders (`--primary-border` / `--secondary-border-color`)**:
|
|
79
|
-
- Replace all hardcoded `#eee`, `#ccc`, `#999` borders with these to ensure they darken appropriately in dark mode.
|
|
80
|
-
5. **Status Colors**:
|
|
81
|
-
- `--success-color`, `--warning-color`, `--error-color`, `--success-bg-color` (Use replacing hardcoded green/reds).
|
|
82
|
-
6. **Spacing**: `var(--space-m)` (8px), `var(--space-l)` (16px).
|
|
83
|
-
|
|
84
|
-
### Standard Utility Classes
|
|
85
|
-
|
|
86
|
-
- **Flexbox**: `.row-box` (flex row, align-center), `.col` (flex: 1).
|
|
87
|
-
- **Margins/Padding**: `m-auto`, `p-m`, `mt-s`, `pb-l` (s=small, m=medium, l=large).
|
|
88
|
-
- **Text**: `.text-center`, `.ellipsis`.
|
|
89
|
-
|
|
90
|
-
### The Component CSS & Ampersand (`&`) Pattern (must go with RefProps)
|
|
91
|
-
|
|
92
|
-
Lupine.js handles component-scoped CSS safely to avoid class collisions. The modern and **preferred** way to style components is to attach a `css={css}` prop to the root element and use the **Ampersand (`&`) Pattern**.
|
|
93
|
-
|
|
94
|
-
When Lupine renders the component, it generates a unique ID (e.g., `l1234`) and replaces the `&` with this ID everywhere it's used.
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
export const MyComponent = () => {
|
|
98
|
-
const ref: RefProps = {
|
|
99
|
-
onLoad: async () => {
|
|
100
|
-
// 3. Querying namespaced elements
|
|
101
|
-
const btn = ref.$('.&-btn');
|
|
102
|
-
btn.innerHTML = 'Ready';
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const css: CssProps = {
|
|
107
|
-
// Top-level rules apply to the root component container itself
|
|
108
|
-
width: '100%',
|
|
109
|
-
padding: '1rem',
|
|
110
|
-
|
|
111
|
-
// 1. Defining namespaced sub-classes in CSS:
|
|
112
|
-
'.&-title': { fontWeight: 'bold' },
|
|
113
|
-
'.&-btn': {
|
|
114
|
-
// Nesting pseudo-classes and combination modifiers (no space after &)
|
|
115
|
-
'&:hover': { background: '#f0f0f0' },
|
|
116
|
-
'&.active': { color: 'var(--primary-accent-color)' },
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
// Setting css={css} safely bounds this style scope
|
|
122
|
-
<aside css={css} ref={ref}>
|
|
123
|
-
{/* 2. Applying namespaced classes in JSX */}
|
|
124
|
-
<div class='&-title'>Hello</div>
|
|
125
|
-
<button class='&-btn active'>Click Me</button>
|
|
126
|
-
</aside>
|
|
127
|
-
);
|
|
128
|
-
};
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
**Key Takeaways for `&` and `RefProps`**:
|
|
132
|
-
|
|
133
|
-
1. **In `CssProps` Binding**: The top-level keys in `CssProps` (like `display: 'flex'`) apply **directly** to the root element (the one attached to `ref={ref}`). **Do not** wrap your root styles in an artificial `.&-container`.
|
|
134
|
-
- `'&.active'` applies to the root element when it has the `.active` native class.
|
|
135
|
-
- `'.&-item'` applies to _descendant_ elements that have `class="&-item"`.
|
|
136
|
-
2. **In JSX `class` attributes**: Add `class="&-item"`. You can still mix native classes: `class="row-box &-item"`.
|
|
137
|
-
3. **In DOM Queries**:
|
|
138
|
-
- **π¨ NEVER use `document.querySelector('.&-item')` or `element.querySelector('.&-item')`**. Standard browser APIs DO NOT understand the `&` symbol and will fail to find the element.
|
|
139
|
-
- Use **`ref.$('.&-item')`** (WITH leading dot) to get the first matching element. The underlying logic
|
|
140
|
-
- Use **`ref.$all('.&-item')`** to get a `NodeList` of all matching descendants within the component.
|
|
141
|
-
|
|
142
|
-
## 4. CSS Placement Strategies
|
|
143
|
-
|
|
144
|
-
Lupine.js provides two main ways to inject component CSS.
|
|
145
|
-
|
|
146
|
-
### Strategy A: The `css={}` Prop (Dynamic / Single-Use)
|
|
147
|
-
|
|
148
|
-
**Best for**: Pages, views, or high-level containers that are only rendered once per screen.
|
|
149
|
-
|
|
150
|
-
When you pass `css={css}` to a JSX element, Lupine automatically evaluates it and injects a new `<style>` tag directly
|
|
151
|
-
|
|
152
|
-
**
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
Create a `
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
-
|
|
512
|
-
|
|
513
|
-
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1
|
+
# AI Context for Lupine.js
|
|
2
|
+
|
|
3
|
+
**SYSTEM ROLE**: You are an expert developer in `lupine.js`, a custom TypeScript full-stack framework.
|
|
4
|
+
|
|
5
|
+
**π CRITICAL WARNINGS π**
|
|
6
|
+
|
|
7
|
+
1. **`useState` EXISTS but rerenders the whole component**: Use it for simple/small components. For complex or large components, prefer `HtmlVar` for surgical, partial updates. **WARNING**: Because `useState` causes the entire parent component to re-render, any uncontrolled inner components/DOM elements that haven't explicitly saved their transient state will be abruptly reset to their default props. If you encounter bugs where interactive components (like toggles, inputs, animations) unexpectedly revert to their original state and lose data upon clicking or typing elsewhere, always check if a `useState` trigger in the parent is causing an unintended full reload.
|
|
8
|
+
2. **NO VIRTUAL DOM STATE by default**: Without `useState`, changing a variable DOES NOT re-render the component. You must manually update `HtmlVar.value`.
|
|
9
|
+
3. **NO CONTROLLED INPUTS**: Do not bind `value={state}`. Read values from DOM on submit.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Core Philosophy & Reactivity
|
|
14
|
+
|
|
15
|
+
- **`useState` β React-style local state (small/simple components)**:
|
|
16
|
+
|
|
17
|
+
- Import: `import { useState } from 'lupine.components';`
|
|
18
|
+
- Syntax: `const [value, setValue] = useState(initial);` β calling `setValue(...)` rerenders the **entire** component.
|
|
19
|
+
- β
**Use when**: The component is small, state drives most of the UI, and the React-style patterns feel natural.
|
|
20
|
+
- β οΈ **Avoid when**: The component is large/complex, or only a tiny portion of the UI needs to change (e.g. a progress counter, a list inside a page) β repeated full rerenders are wasteful.
|
|
21
|
+
- **`ref.onLoad` + useState**: `onLoad` is called **only on initial mount** (not on rerenders). It's the right place for async data fetch that populates state.
|
|
22
|
+
|
|
23
|
+
- **`HtmlVar` β Surgical partial updates (large/complex components)**:
|
|
24
|
+
|
|
25
|
+
- Use `HtmlVar` to wrap dynamic sections (lists, conditional renderings, async content).
|
|
26
|
+
- **Pattern**: `const dom = new HtmlVar(initialContent);` β JSX `{dom.node}` β `dom.value = updatedContent`.
|
|
27
|
+
- β
**Use when**: Only a small part of a large component changes (e.g. list inside a page, progress text), or state is updated by external hooks (`props.hook.onProgress`), or high-frequency updates (file upload progress).
|
|
28
|
+
- The rest of the component DOM is never touched β highly efficient.
|
|
29
|
+
|
|
30
|
+
- **Direct DOM Access**:
|
|
31
|
+
- Use `RefProps` to get reference to the component root.
|
|
32
|
+
- Use `ref.$(selector)` to find the first element, `ref.$all(selector)` to find all elements (inputs, containers).
|
|
33
|
+
- **Value Retrieval**: `const val = ref.$('input.my-class').value`.
|
|
34
|
+
|
|
35
|
+
## 2. Key Interfaces
|
|
36
|
+
|
|
37
|
+
### `RefProps` (Lifecycle & DOM)
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const ref: RefProps = {
|
|
41
|
+
// Mounted: Initialize data, timers, events
|
|
42
|
+
onLoad: async (el: Element) => {
|
|
43
|
+
await loadData();
|
|
44
|
+
// ref.$('.sub-element').addEventListener(...)
|
|
45
|
+
},
|
|
46
|
+
// Unmounting: Cleanup
|
|
47
|
+
onUnload: async (el: Element) => {
|
|
48
|
+
// Cleanup (timers, sockets)
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
// Usage
|
|
52
|
+
<div ref={ref}>...</div>;
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### `CssProps` (Styling)
|
|
56
|
+
|
|
57
|
+
Supports nesting and media queries. **Prefer this over inline styles.** Define your styles in a `CssProps` object and bind them to the component's root JSX using the `css={css}` property. Use the `&` ampersand pattern (explained below) to guarantee unique class scoping.
|
|
58
|
+
|
|
59
|
+
## 3. Styles & Themes ("The Look")
|
|
60
|
+
|
|
61
|
+
### Global Variables (Theming) & Dark Mode Compatibility
|
|
62
|
+
|
|
63
|
+
**NEVER hardcode colors** (e.g., `#000`, `#fff`, `#f0f0f0`). Always use CSS variables to support Dark/Light modes. If you must use a fallback, wrap it: `var(--primary-bg-color, #fff)`.
|
|
64
|
+
|
|
65
|
+
#### π¨ Color Variable Semantics (CRITICAL FOR DARK MODE)
|
|
66
|
+
|
|
67
|
+
1. **Backgrounds (`--primary-bg-color` vs `--secondary-bg-color`)**:
|
|
68
|
+
- `--primary-bg-color`: The lowest-level background (White in light mode, **Deep Black** in dark mode).
|
|
69
|
+
- `--secondary-bg-color`: An elevated background (Light gray in light mode, **Lighter Black/Gray** in dark mode).
|
|
70
|
+
- _β οΈ Dark Mode Trap_: If you place a floating panel/card on the main body, **do NOT** use `--primary-bg-color` for the panel. It will blend into the body's deep black and become invisible. Use `--secondary-bg-color` for elevated panels to ensure visual separation.
|
|
71
|
+
2. **Text Colors (`--primary-color` vs `--secondary-color`)**:
|
|
72
|
+
- `--primary-color`: The primary **TEXT** color. (Dark grey/black in light mode, **White** in dark mode).
|
|
73
|
+
- _β οΈ Dark Mode Trap_: **Never** use `--primary-color` as the background color for a blue "Primary Action Button". It will turn white in dark mode.
|
|
74
|
+
- Always explicitly declare `color: 'var(--primary-color, inherit)'` on cards/containers so child text properly flips white in dark mode.
|
|
75
|
+
3. **Action / Brand Colors (`--primary-accent-color`)**:
|
|
76
|
+
- `--primary-accent-color`: The vibrant brand color (e.g., Lupine Blue). Use this for the **backgrounds of primary buttons**, active tabs, slider fills, and highlights.
|
|
77
|
+
- When using this as a background, set the text color to `var(--primary-bg-color)` so it stays high-contrast (white) in both themes.
|
|
78
|
+
4. **Borders (`--primary-border` / `--secondary-border-color`)**:
|
|
79
|
+
- Replace all hardcoded `#eee`, `#ccc`, `#999` borders with these to ensure they darken appropriately in dark mode.
|
|
80
|
+
5. **Status Colors**:
|
|
81
|
+
- `--success-color`, `--warning-color`, `--error-color`, `--success-bg-color` (Use replacing hardcoded green/reds).
|
|
82
|
+
6. **Spacing**: `var(--space-m)` (8px), `var(--space-l)` (16px).
|
|
83
|
+
|
|
84
|
+
### Standard Utility Classes
|
|
85
|
+
|
|
86
|
+
- **Flexbox**: `.row-box` (flex row, align-center), `.col` (flex: 1).
|
|
87
|
+
- **Margins/Padding**: `m-auto`, `p-m`, `mt-s`, `pb-l` (s=small, m=medium, l=large).
|
|
88
|
+
- **Text**: `.text-center`, `.ellipsis`.
|
|
89
|
+
|
|
90
|
+
### The Component CSS & Ampersand (`&`) Pattern (must go with RefProps)
|
|
91
|
+
|
|
92
|
+
Lupine.js handles component-scoped CSS safely to avoid class collisions. The modern and **preferred** way to style components is to attach a `css={css}` prop to the root element and use the **Ampersand (`&`) Pattern**.
|
|
93
|
+
|
|
94
|
+
When Lupine renders the component, it generates a unique ID (e.g., `l1234`) and replaces the `&` with this ID everywhere it's used.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
export const MyComponent = () => {
|
|
98
|
+
const ref: RefProps = {
|
|
99
|
+
onLoad: async () => {
|
|
100
|
+
// 3. Querying namespaced elements
|
|
101
|
+
const btn = ref.$('.&-btn');
|
|
102
|
+
btn.innerHTML = 'Ready';
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const css: CssProps = {
|
|
107
|
+
// Top-level rules apply to the root component container itself
|
|
108
|
+
width: '100%',
|
|
109
|
+
padding: '1rem',
|
|
110
|
+
|
|
111
|
+
// 1. Defining namespaced sub-classes in CSS:
|
|
112
|
+
'.&-title': { fontWeight: 'bold' },
|
|
113
|
+
'.&-btn': {
|
|
114
|
+
// Nesting pseudo-classes and combination modifiers (no space after &)
|
|
115
|
+
'&:hover': { background: '#f0f0f0' },
|
|
116
|
+
'&.active': { color: 'var(--primary-accent-color)' },
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
// Setting css={css} safely bounds this style scope
|
|
122
|
+
<aside css={css} ref={ref}>
|
|
123
|
+
{/* 2. Applying namespaced classes in JSX */}
|
|
124
|
+
<div class='&-title'>Hello</div>
|
|
125
|
+
<button class='&-btn active'>Click Me</button>
|
|
126
|
+
</aside>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Key Takeaways for `&` and `RefProps`**:
|
|
132
|
+
|
|
133
|
+
1. **In `CssProps` Binding**: The top-level keys in `CssProps` (like `display: 'flex'`) apply **directly** to the root element (the one attached to `ref={ref}`). **Do not** wrap your root styles in an artificial `.&-container`.
|
|
134
|
+
- `'&.active'` applies to the root element when it has the `.active` native class.
|
|
135
|
+
- `'.&-item'` applies to _descendant_ elements that have `class="&-item"`.
|
|
136
|
+
2. **In JSX `class` attributes**: Add `class="&-item"`. You can still mix native classes: `class="row-box &-item"`.
|
|
137
|
+
3. **In DOM Queries**:
|
|
138
|
+
- **π¨ NEVER use `document.querySelector('.&-item')` or `element.querySelector('.&-item')`**. Standard browser APIs DO NOT understand the `&` symbol and will fail to find the element.
|
|
139
|
+
- Use **`ref.$('.&-item')`** (WITH leading dot) to get the first matching element. The underlying logic replaces `&` with the generated CSS ID, so this correctly translates to querying `.l1234 .l1234-item` which safely finds descendants within the current component's isolated namespace.
|
|
140
|
+
- Use **`ref.$all('.&-item')`** to get a `NodeList` of all matching descendants within the component.
|
|
141
|
+
|
|
142
|
+
## 4. CSS Placement Strategies & Sharing Scopes
|
|
143
|
+
|
|
144
|
+
Lupine.js provides two main ways to inject component CSS (`css={}` vs `bindGlobalStyle`). Additionally, you must actively manage how dynamic separated DOM chunks share the same CSS Scope.
|
|
145
|
+
|
|
146
|
+
### Strategy A: The `css={}` Prop (Dynamic / Single-Use)
|
|
147
|
+
|
|
148
|
+
**Best for**: Pages, views, or high-level containers that are only rendered once per screen.
|
|
149
|
+
|
|
150
|
+
When you pass `css={css}` to a JSX element, Lupine automatically evaluates it and injects a new `<style>` tag directly wrapping that element.
|
|
151
|
+
- **Pros**: Perfect isolation.
|
|
152
|
+
- **Cons**: If you render 100 items using `css={}`, it will inject 100 identical `<style>` blocks into the DOM, severely bloating the page.
|
|
153
|
+
|
|
154
|
+
### Strategy B: `bindGlobalStyle` (Reusable Components)
|
|
155
|
+
|
|
156
|
+
**Best for**: Reusable UI components (Buttons, Toggles, List Items, Modals) that will be rendered multiple times.
|
|
157
|
+
|
|
158
|
+
`bindGlobalStyle`, combined with `getGlobalStylesId`, places the `<style>` block in the `<head>` of the document **exactly once**. All instances of the component share the same CSS class names, but those names are still guaranteed to be collision-free!
|
|
159
|
+
|
|
160
|
+
**How it works seamlessly with `&`**:
|
|
161
|
+
1. Generate an ID based on the `CssProps` content: `const globalCssId = getGlobalStylesId(css);`. (Call this *inside* the component!)
|
|
162
|
+
2. Bind the style block globally once: `bindGlobalStyle(globalCssId, css);`
|
|
163
|
+
3. Assign this ID to the component's `ref` to link the scope: `const ref: RefProps = { globalCssId };` / `<div ref={ref}>`
|
|
164
|
+
4. Use `class="&-item"` normally. Lupine replaces `&` with the identical `globalCssId` across all instances.
|
|
165
|
+
|
|
166
|
+
### β οΈ IMPORTANT: The "Static `CssProps`" Rule
|
|
167
|
+
|
|
168
|
+
Because `bindGlobalStyle` injects your `<style>` tags into the `<head>` globally, your `CssProps` definition **MUST** be entirely static.
|
|
169
|
+
|
|
170
|
+
**ANTI-PATTERN:** Putting variables (like `isVertical`, `size`, `color`) directly inside the `CssProps` object structure. Every time the component re-renders with a different prop, it will generate conflicting CSS IDs and the latter will overwrite the former's styles, corrupting the layout. Define one immutable `const css: CssProps = {...}` and handle visual variations by appending standard class names to your root element (`class={isVertical ? '&-vertical' : '&-horizontal'}`) and map those variations inside your static `CssProps`.
|
|
171
|
+
|
|
172
|
+
### π Sharing the same CSS scope (`globalCssId`) among Separated DOMs
|
|
173
|
+
|
|
174
|
+
If your component divides its logic so that some internal floating DOM elements are rendered dynamically later (e.g. through a function passed to `HtmlVar`) *separated* from the root return statement, the inner DOM will automatically generate a **new, mismatched** CSS ID if not linked. Its internal `class="&-item"` references will break and styles will fail to apply.
|
|
175
|
+
|
|
176
|
+
To force separated local DOM partitions to share the exact same `&` CSS Scope as their parent page, explicitly share a globally unique CSS ID using `globalStyleUniqueId()`:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import { globalStyleUniqueId, HtmlVar, RefProps, CssProps } from 'lupine.components';
|
|
180
|
+
|
|
181
|
+
export const HomePage = () => {
|
|
182
|
+
// 1. Generate a manual ID for the container scope beforehand
|
|
183
|
+
const cssId = globalStyleUniqueId();
|
|
184
|
+
|
|
185
|
+
const listDom = new HtmlVar('');
|
|
186
|
+
|
|
187
|
+
const renderList = () => {
|
|
188
|
+
// 2. Explicitly bind the inner detached DOM to the parent's globalCssId
|
|
189
|
+
listDom.value = (
|
|
190
|
+
<div ref={{ globalCssId: cssId }} class="&-bundle-container">
|
|
191
|
+
<div class="&-bundle-name">Basic Bundle</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const ref: RefProps = {
|
|
197
|
+
globalCssId: cssId, // 3. The parent registers the ID as well
|
|
198
|
+
onLoad: async () => renderList()
|
|
199
|
+
};
|
|
200
|
+
const css: CssProps = { '.&-bundle-name': { color: 'red' } };
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div css={css} ref={ref}>
|
|
204
|
+
{/* 4. The dynamically injected nodes will properly map their &- prefixes */}
|
|
205
|
+
{listDom.node}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
```
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## 5. Common Patterns ("The Lupine Way")
|
|
213
|
+
|
|
214
|
+
### List / Search (No Re-render)
|
|
215
|
+
|
|
216
|
+
**Pattern**: Create a render function (`makeList`) and assign its result to `HtmlVar`.
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const MyPage = () => {
|
|
220
|
+
// 1. Logic Variables (Not State)
|
|
221
|
+
let pageIndex = 0;
|
|
222
|
+
|
|
223
|
+
// 2. Dynamic Container
|
|
224
|
+
const listDom = new HtmlVar(<div>Loading...</div>);
|
|
225
|
+
|
|
226
|
+
// 3. Render Function
|
|
227
|
+
const makeList = async () => {
|
|
228
|
+
const data = await fetchData(pageIndex);
|
|
229
|
+
return (
|
|
230
|
+
<div>
|
|
231
|
+
{data.map((item) => (
|
|
232
|
+
<Item item={item} />
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// 4. Events
|
|
239
|
+
const onSearch = async () => {
|
|
240
|
+
// Read directly from DOM
|
|
241
|
+
const query = ref.$('input.&-search').value;
|
|
242
|
+
// Update logic var
|
|
243
|
+
pageIndex = 0;
|
|
244
|
+
// Update UI manually
|
|
245
|
+
listDom.value = await makeList();
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const ref: RefProps = {
|
|
249
|
+
onLoad: async () => {
|
|
250
|
+
listDom.value = await makeList();
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<div ref={ref}>
|
|
256
|
+
<input class='&-search' />
|
|
257
|
+
<button onClick={onSearch}>Go</button>
|
|
258
|
+
{/* Embed Dynamic Content */}
|
|
259
|
+
{listDom.node}
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
};
|
|
263
|
+
```
|
|
264
|
+
### Page Navigation (`initializePage` vs `<a>`)
|
|
265
|
+
|
|
266
|
+
In the Lupine.js system, all standard `<a>` HTML tags are automatically intercepted. If the link points to an internal route, Lupine safely binds it to `_lupineJs.initializePage(href)` behind the scenes to perform a seamless single-page application (SPA) transition without a full browser reload.
|
|
267
|
+
|
|
268
|
+
When performing imperative or programmatic routing via JavaScript (e.g. clicking a `<button>` or a `div`), **DO NOT** use `window.location.href = '/path'`, as this forces a harsh full-page reload.
|
|
269
|
+
|
|
270
|
+
Instead, import and use `initializePage`:
|
|
271
|
+
```typescript
|
|
272
|
+
import { initializePage } from 'lupine.web';
|
|
273
|
+
|
|
274
|
+
const navigate = () => {
|
|
275
|
+
// CORRECT: Seamless SPA transition
|
|
276
|
+
initializePage('/play/diff01/1');
|
|
277
|
+
|
|
278
|
+
// ERROR / ANTI-PATTERN: Forces full browser reload unless explicitly desired
|
|
279
|
+
// window.location.href = '/play/diff01/1';
|
|
280
|
+
};
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Mobile Navigation (`SliderFrame`)
|
|
284
|
+
|
|
285
|
+
Lupine uses a "Slide-over" model for navigation (Drill-down). To achieve infinite nesting (where a child page can open a grandchild page), each Component level simply needs to define its own `sliderHook` and its own `<SliderFrame>` tag to act as the placeholder for its children.
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import { SliderFrame, SliderFrameHookProps, HeaderWithBackFrame } from 'lupine.components';
|
|
289
|
+
|
|
290
|
+
// 1. Parent Component (or Level 1)
|
|
291
|
+
const Parent = () => {
|
|
292
|
+
// Define hook for Level 2
|
|
293
|
+
const sliderHook: SliderFrameHookProps = {};
|
|
294
|
+
|
|
295
|
+
const openDetail = (id) => {
|
|
296
|
+
// Push new view onto stack
|
|
297
|
+
sliderHook.load!(<DetailComponent id={id} parentSliderFrameHook={sliderHook} />);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<div>
|
|
302
|
+
{/* Placeholder for Level 2 */}
|
|
303
|
+
<SliderFrame hook={sliderHook} />
|
|
304
|
+
|
|
305
|
+
<div onClick={() => openDetail(1)}>Click Me</div>
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// 2. Child Component (Level 2)
|
|
311
|
+
const DetailComponent = (props: { id: number; parentSliderFrameHook: SliderFrameHookProps }) => {
|
|
312
|
+
// Define hook for Level 3
|
|
313
|
+
const childSliderHook: SliderFrameHookProps = {};
|
|
314
|
+
|
|
315
|
+
const openDeeper = () => {
|
|
316
|
+
// Load Level 3 component into this component's placeholder
|
|
317
|
+
childSliderHook.load!(<DetailComponent id={props.id + 1} parentSliderFrameHook={childSliderHook} />);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<HeaderWithBackFrame title='Detail Page' onBack={(e) => props.parentSliderFrameHook.close!(e)}>
|
|
322
|
+
{/* Placeholder for Level 3 */}
|
|
323
|
+
<SliderFrame hook={childSliderHook} />
|
|
324
|
+
|
|
325
|
+
<div onClick={openDeeper}>Go Deeper</div>
|
|
326
|
+
</HeaderWithBackFrame>
|
|
327
|
+
);
|
|
328
|
+
};
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Component Hooks (Imperative Control)
|
|
332
|
+
|
|
333
|
+
Instead of React's `useImperativeHandle` or lifting state up, Lupine components often use a `hook` pattern for parent-to-child communication and exposing methods.
|
|
334
|
+
|
|
335
|
+
1. **Parent** creates an empty object: `const myHook: MyComponentHookProps = {};`
|
|
336
|
+
2. **Parent** passes it to the child: `<MyComponent hook={myHook} />`
|
|
337
|
+
3. **Child** populates it during render:
|
|
338
|
+
```typescript
|
|
339
|
+
if (props.hook) {
|
|
340
|
+
props.hook.getValue = () => value;
|
|
341
|
+
props.hook.setValue = (val) => {
|
|
342
|
+
updateDOM(val);
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
4. **Parent** calls it later on demand: `console.log(myHook.getValue());`
|
|
347
|
+
|
|
348
|
+
**β οΈ CRITICAL HOOK TIMING**: Do not call hook methods in the parent's top-level execution scope before returning the child component. The child component populates or resets the hook _during_ its own render phase. If you call `myHook.setValue()` and then return `<MyComponent hook={myHook} />`, your changes will be ignored or the hook object will be overwritten. You **MUST** wait until the component is mounted to use the hook, typically via a parent `RefProps.onLoad`:
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
const Parent = () => {
|
|
352
|
+
const myHook: MyComponentHookProps = {};
|
|
353
|
+
|
|
354
|
+
const ref: RefProps = {
|
|
355
|
+
onLoad: async () => {
|
|
356
|
+
// Safe: Child has rendered and populated the hook
|
|
357
|
+
myHook.setValue('Hello');
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<div ref={ref}>
|
|
363
|
+
<MyComponent hook={myHook} />
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
};
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## 5. Architecture Cheat Sheet
|
|
370
|
+
|
|
371
|
+
- **`lupine.api` (Backend)**:
|
|
372
|
+
- `req.locals.json()` to get body.
|
|
373
|
+
- `apiCache.getDb().selectObject('$__table', ...)`
|
|
374
|
+
- `ApiHelper.sendJson(req, res, { status: 'ok' })`
|
|
375
|
+
- **`lupine.web` (Frontend)**:
|
|
376
|
+
- `NotificationMessage.sendMessage('Msg', NotificationColor.Success)`
|
|
377
|
+
- `getRenderPageProps().renderPageFunctions.fetchData('/api/...')`
|
|
378
|
+
- Retrieve dynamic URL parameters explicitly via `props.urlParameters['paramName']`.
|
|
379
|
+
- **Environment vs Database Config**:
|
|
380
|
+
- `webEnv('API_BASE_URL', '')`: Use this mapping to synchronously read statically injected environment variables defined in `.env` (like `WEB.xxx`).
|
|
381
|
+
- `await WebConfig.get('siteLogo')`: Use this for dynamic configurations. It works asynchronously by fetching data from the backend server first, then caches it for subsequent calls.
|
|
382
|
+
- **Path Parameter Syntax**:
|
|
383
|
+
- Mandatory parameters use `:` (e.g., `pageRouter.use('/page/:id', PlayPage)`).
|
|
384
|
+
- Fixed parameters use `/fixed-parameter/` (e.g., `pageRouter.use('/page/:id/detail/', PlayPage)`), `detail` is a fixed parameter.
|
|
385
|
+
- Optional parameters use `?` (e.g., `/page/:userId/?option1/?option2`). Once an optional parameter is declared, all subsequent route sections become optional (It's not a query string).
|
|
386
|
+
|
|
387
|
+
## 6. Coding Standards & Gotchas
|
|
388
|
+
|
|
389
|
+
- **`useState` vs `HtmlVar`**: `useState` exists (`import { useState } from 'lupine.components'`) and is elegant for small components. But it rerenders the **entire** component β for large/complex components or high-frequency updates, prefer `HtmlVar` for surgical partial updates. `useEffect`, `useReducer`, `useCallback`, `useContext` **do NOT exist**.
|
|
390
|
+
- **β `className`**: Use standard HTML `class`.
|
|
391
|
+
- **β οΈ `style={{}}`**: **Allowed** for simple or dynamic inline styles (e.g., `style={{ border: '1px solid red' }}`), but **prefer `css={CssProps}`** for structural/theme styling.
|
|
392
|
+
- **β
Native Events**: `onClick`, `onChange`, `onInput`, `onMouseMove` etc. are standard HTML events and **ARE ALLOWED**. Use them for triggering logic or callbacks (e.g., `onInput={(e) => updateOtherThing(e.target.value)}`).
|
|
393
|
+
- **β
Uncontrolled Inputs**: While you _can_ use `onInput` to track state, the default efficient pattern is often to read `ref.$('input').value` only when the user clicks "Save" or "Search".
|
|
394
|
+
|
|
395
|
+
## 7. System Icons & Customization
|
|
396
|
+
|
|
397
|
+
Lupine.components uses a set of built-in system icons (like `ma-close` and `mg-arrow_back_ios_new_outlined` found in components like `MobileHeaderWithBack`).
|
|
398
|
+
|
|
399
|
+
These icons rely on an icon font generated by [icons-font-customization](https://github.com/uuware/icons-font-customization). If you want to add or modify the standard icon font itself, refer to that repository.
|
|
400
|
+
|
|
401
|
+
### Overriding System Icons without Generating a Font
|
|
402
|
+
|
|
403
|
+
If you do not want to generate or modify the full icon font, you can easily override specific system icons directly via CSS using pure SVG strings or imported SVG files.
|
|
404
|
+
|
|
405
|
+
**1. Define your SVG Data URL:**
|
|
406
|
+
You can import an `.svg` file (if your bundler supports it) or define a raw Data URI string.
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
// Option A: Using bundler import
|
|
410
|
+
import githubIcon from 'github.svg';
|
|
411
|
+
|
|
412
|
+
// Option B: Raw Data URI string
|
|
413
|
+
const closeSvgData = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M18 6L6 18M6 6l12 12'/%3E%3C/svg%3E`;
|
|
414
|
+
|
|
415
|
+
export const DemoIcons = {
|
|
416
|
+
github: githubIcon,
|
|
417
|
+
'ma-close': closeSvgData,
|
|
418
|
+
};
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**2. Override the specific `.ifc-icon` via CSS Masking:**
|
|
422
|
+
Use the `-webkit-mask-image` and `maskImage` property wrapped in `url()` to apply the SVG data to the icon class. This ensures it inherits colors (`currentColor`) properly.
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
const css: CssProps = {
|
|
426
|
+
// Target the specific system icon class you wish to override
|
|
427
|
+
'.ifc-icon.ma-close': {
|
|
428
|
+
'-webkit-mask-image': `url("${DemoIcons['ma-close']}")`,
|
|
429
|
+
maskImage: `url("${DemoIcons['ma-close']}")`,
|
|
430
|
+
// If needed, specify mask sizing properties:
|
|
431
|
+
// maskRepeat: 'no-repeat',
|
|
432
|
+
// maskPosition: 'center',
|
|
433
|
+
// maskSize: 'contain',
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## 8. Cross-Platform App Bootstrapping Guidance
|
|
439
|
+
|
|
440
|
+
When creating a new Cross-Platform App using `lupine.js`, follow this standard procedure for scaffolding the entry point, navigation, and icons:
|
|
441
|
+
|
|
442
|
+
1. **Custom Navigation Icons (`app-icons.ts`)**:
|
|
443
|
+
Instead of using the default icon font, you should export SVG Data URIs for your app's specific icons from `app-icons.ts`. Use a `reduce` function to generate the appropriate `CssProps` with `-webkit-mask-image: url("' + svg + '")'` and `maskImage: 'url("' + svg + '")'` to override the `.ifc-icon.[icon-name]` classes. Avoid using backticks (\"\`\") when injecting SVG variables inside the maskImage URL to prevent escaping issues.
|
|
444
|
+
|
|
445
|
+
2. **Base Styles (`base-css.ts`)**:
|
|
446
|
+
Create a `styles/base-css.ts` file that imports the dynamic icon styles (from `app-icons.ts`), and defines any placeholder wrappers (e.g., `.user-page-placeholder` having `width: '100%', height: '100%'`). Use `bindGlobalStyle('comm-css', baseCss, false, true)` in the index file to register these.
|
|
447
|
+
|
|
448
|
+
3. **Global UI Frame (`app-responsive-frame.tsx`)**:
|
|
449
|
+
Use `ResponsiveFrame` along with `SliderFrame` (for drill-down navigation via `SliderFrameHookProps`) to define the app's skeleton.
|
|
450
|
+
|
|
451
|
+
- Define your top/bottom navigation menus based on your icons.
|
|
452
|
+
- Return `ResponsiveFrame` passing in the `mainContent`, menus, and ensure you provide all required properties like `mobileSideMenuContent: <></>` (even if empty) to satisfy TypeScript interfaces.
|
|
453
|
+
|
|
454
|
+
4. **Page Router Configuration (`index.tsx`)**:
|
|
455
|
+
Create a `PageRouter`. Bind the `AppResponsiveFrame` using `pageRouter.setFramePage({ component: AppResponsiveFrame, placeholderClassname: 'user-page-placeholder' })`. Then associate the routes (`pageRouter.use('*', HomePage)`) and finalize with `bindRouter(pageRouter)`.
|
|
456
|
+
|
|
457
|
+
5. **Local Storage Patterns**:
|
|
458
|
+
For pure frontend utility apps (compatible with browsers, Capacitor, and Electron), wrap `localStorage.getItem()` and `localStorage.setItem()` inside dedicated Service singletons (e.g., `LocalNotesService`). Always parse/serialize consistently and assign standard unique IDs (like `Date.now()`) for newly inserted records. Combine this with the `onLoad` pattern inside `RefProps` to fetch data immediately when components render, injecting it directly into an `HtmlVar` wrapping the list.
|
|
459
|
+
|
|
460
|
+
## 9. Standard Mobile App Layout & Interactions
|
|
461
|
+
|
|
462
|
+
When asked to "create a list page" or "initialize a standard mobile framework", rigorously apply this exact structural pattern based on the cross-platform starter app.
|
|
463
|
+
|
|
464
|
+
### A. The Global Root (`index.tsx` & `AppResponsiveFrame`)
|
|
465
|
+
|
|
466
|
+
- Use `bindTheme` to load global color tokens.
|
|
467
|
+
- Set the global frame with `pageRouter.setFramePage({ component: AppResponsiveFrame, placeholderClassname: 'user-page-placeholder' })`.
|
|
468
|
+
- **`AppResponsiveFrame`** handles the macro layout:
|
|
469
|
+
- It renders `<ResponsiveFrame>` wrapping `<main class='user-page-placeholder'></main>`.
|
|
470
|
+
- It contains the global left `mobileSideMenuContent` (typically abstracted into a `<SideMenuContent />` component).
|
|
471
|
+
|
|
472
|
+
### B. The Home / List Page (`HomePage`)
|
|
473
|
+
|
|
474
|
+
A standard mobile list page must employ:
|
|
475
|
+
|
|
476
|
+
1. **The Top Header (`MobileHeaderCenter`)**:
|
|
477
|
+
|
|
478
|
+
- Wrap the top bar in `<MobileHeaderCenter>`.
|
|
479
|
+
- Use `<MobileHeaderTitleIcon title='App Name' left={...} right={...} />`.
|
|
480
|
+
- The _left_ slot usually contains an empty spacer `<MobileHeaderEmptyIcon />`.
|
|
481
|
+
- The _right_ slot contains actions (e.g., Search icon, `<MobileTopSysIcon />` to open the Side Menu).
|
|
482
|
+
|
|
483
|
+
2. **The Scrollable Content Area**:
|
|
484
|
+
|
|
485
|
+
- Beneath the header, create a flex-grow scrollable div: `<div class='flex-1 overflow-y-auto padding-m'>`.
|
|
486
|
+
- Mount an `HtmlVar` instance here (`{dom.node}`) to dynamically bind the list data arrays fetched typically via `RefProps.onLoad`.
|
|
487
|
+
|
|
488
|
+
3. **Floating Action Button (FAB)**:
|
|
489
|
+
- Overlay a primary action button at `bottom: 24px`, `right: 24px` using `.fab-button` styled with `var(--primary-accent-color)`.
|
|
490
|
+
|
|
491
|
+
### C. Advanced Touch Interactions (`createDragUtil` in Lists)
|
|
492
|
+
|
|
493
|
+
For interactive lists, `createDragUtil()` from `lupine.components` handles complex gesture physics.
|
|
494
|
+
|
|
495
|
+
- **Swipe-to-Reveal (Horizontal)**:
|
|
496
|
+
|
|
497
|
+
- Render an absolute positioned `.actions-layer` (opacity: 0 initially) underneath the `.list-card`.
|
|
498
|
+
- When the card's `onTouchStart`/`onMouseDown` is triggered, attach `dragUtil` handlers.
|
|
499
|
+
- In `dragUtil.setOnMoveCallback`, translate the card `transform: translateX(...)` up to a negative boundary (e.g., -100px) and toggle the action layer's opacity to 1.
|
|
500
|
+
- Implement a global `resetSwipeMenus` function attached to `onMouseDown={handleBgTouch}` at the page root to ensure an exclusive accordion-like menu state (only one open at a time).
|
|
501
|
+
|
|
502
|
+
- **Drag-to-Reorder (Vertical)**:
|
|
503
|
+
- Define a distinct `.drag-handle` slot inside the card (e.g., `bs-list` icon).
|
|
504
|
+
- In `dragUtil.setOnMoveCallback`, when dragging this handle, apply `scale(1.02)` and elevated `boxShadow` to the grabbed card. Compare its `relativeY` pointer position against sibling card `offsetTop`s to execute live `insertBefore / insertAfter` DOM swaps.
|
|
505
|
+
- Conclude by saving the new DOM sibling ordering in `dragUtil.setOnMoveEndCallback`.
|
|
506
|
+
|
|
507
|
+
### D. Sub-Page Routing & Drill-Downs (`SliderFrame`)
|
|
508
|
+
|
|
509
|
+
- Slide-over interactions are mandatory for Search panels, Creation modals, and Details views.
|
|
510
|
+
- The `HomePage` must define a top-level `<SliderFrame hook={sliderFrameHook} />` inside its scroll area.
|
|
511
|
+
- Opening a child acts instantly via: `sliderFrameHook.load!(<MyChildPage sliderFrameHook={sliderFrameHook} />)`.
|
|
512
|
+
- **Inside the Child Component**:
|
|
513
|
+
- Must be wrapped with `<HeaderWithBackFrame title='Subpage' onBack={(e) => props.sliderFrameHook.close!(e)}>` to provide the standard top-left back chevron.
|
|
514
|
+
- **Nested SliderFrames**:
|
|
515
|
+
- When opening a sliding frame _from within_ another sliding frame (e.g., opening a Settings About page from the Settings root page), you **MUST** define a new local hook `const innerSliderHook: SliderFrameHookProps = {};` and mount a _new_ `<SliderFrame hook={innerSliderHook} />` inside the parent slider component.
|
|
516
|
+
- Using the parent's hook will replace the parent's content instead of sliding a new frame over it.
|
|
517
|
+
- Wrap multiple children in `<></>` or a `<div>` if they are direct children to satisfy single `VNode` rendering constraints.
|
|
518
|
+
|
|
519
|
+
### E. Dialogs & Action Sheets (Replacing Native Alerts)
|
|
520
|
+
|
|
521
|
+
**DO NOT USE browser native `alert()`, `confirm()`, or `prompt()`**. Instead, use the native `ActionSheet` promises from `lupine.components` for a modern, mobile-friendly overlay experience:
|
|
522
|
+
|
|
523
|
+
1. **Option Selection (`ActionSheetSelectPromise`)** (Replaces `confirm()` or complex choices):
|
|
524
|
+
```typescript
|
|
525
|
+
import { ActionSheetSelectPromise } from 'lupine.components';
|
|
526
|
+
|
|
527
|
+
const index = await ActionSheetSelectPromise({
|
|
528
|
+
title: 'Delete this saved game?', // Optional
|
|
529
|
+
options: ['Delete', 'Edit'],
|
|
530
|
+
cancelButtonText: 'Cancel',
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
if (index === 0) { /* User clicked Delete (Index of options array) */ }
|
|
534
|
+
if (index === -1) { /* User clicked Cancel or tapped background */ }
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
2. **Simple Messages (`ActionSheetMessagePromise`)** (Replaces `alert()`):
|
|
538
|
+
```typescript
|
|
539
|
+
import { ActionSheetMessagePromise } from 'lupine.components';
|
|
540
|
+
|
|
541
|
+
await ActionSheetMessagePromise({
|
|
542
|
+
title: 'Success', // Optional
|
|
543
|
+
message: 'Your profile has been saved.',
|
|
544
|
+
closeButtonText: 'OK' // Optional, defaults to a close behavior
|
|
545
|
+
});
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
3. **User Input (`ActionSheetInputPromise`)** (Replaces `prompt()`):
|
|
549
|
+
```typescript
|
|
550
|
+
import { ActionSheetInputPromise } from 'lupine.components';
|
|
551
|
+
|
|
552
|
+
const value = await ActionSheetInputPromise({
|
|
553
|
+
title: 'Enter your name',
|
|
554
|
+
// placeholder: 'Player 1', // Optional
|
|
555
|
+
confirmButtonText: 'Submit', // Optional
|
|
556
|
+
cancelButtonText: 'Cancel' // Optional
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
if (value !== null) { /* User submitted a string */ }
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
4. **Other Available Prompts (Investigate their API via `lupine.components` when needed)**:
|
|
563
|
+
- `ActionSheetMultiSelectPromise`: For multiple checkbox selections.
|
|
564
|
+
- `ActionSheetTimePicker`: For selecting a time.
|
|
565
|
+
- `ActionSheetDatePicker`: For selecting a date.
|
|
566
|
+
|
|
567
|
+
### F. Hardware Back Button Handling (`data-back-action`)
|
|
568
|
+
|
|
569
|
+
When building mobile interfaces, users expect the physical hardware "Back" button (or swipe-from-edge gesture) to gracefully dismiss overlays, dialogs, sliders, or menusβsimilar to pressing the `ESC` key on a desktop.
|
|
570
|
+
|
|
571
|
+
**The Rule**: Whenever you implement a cancel button, a close icon (`X`), or a back chevron (`<`) in a mobile overlay or frame, you **MUST** attach the `data-back-action` attribute using the `backActionHelper`.
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
import { backActionHelper } from 'lupine.components';
|
|
575
|
+
|
|
576
|
+
export const MyCloseButton = ({ onClose }) => {
|
|
577
|
+
return (
|
|
578
|
+
<i
|
|
579
|
+
class="ifc-icon ma-close"
|
|
580
|
+
// Generate a unique ID for the back stack
|
|
581
|
+
data-back-action={backActionHelper.genBackActionId()}
|
|
582
|
+
onClick={onClose}
|
|
583
|
+
></i>
|
|
584
|
+
);
|
|
585
|
+
};
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
**How it works**:
|
|
589
|
+
- When the hardware back button is pressed, the underlying system automatically queries the DOM for all elements with `[data-back-action^="bb-"]`.
|
|
590
|
+
- It finds the most recently created component (the top-most overlay) and automatically triggers a `.click()` event on it.
|
|
591
|
+
- **Dynamic Mounting vs Static**:
|
|
592
|
+
- For components that are injected and removed dynamically (like `<ActionSheet />` or `<FloatWindow />`), simply attaching the property to the React/JSX node is sufficient.
|
|
593
|
+
- For static components that always remain in the DOM but toggle visibility (like an off-canvas sidebar), you must dynamically add/remove the attribute in Javascript (`el.setAttribute` / `el.removeAttribute`) to prevent the back button from intercepting events when the menu is actually closed.
|