create-lupine 1.0.21 → 1.0.23
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/index.js +2 -2
- package/package.json +1 -1
- package/templates/common/AI_CONTEXT.md +165 -89
- package/templates/common/apps/server/src/app-loader.ts +2 -2
- package/templates/common/apps/server/src/index.ts +76 -81
- package/templates/common/apps/server/src/server-loader.ts +4 -0
- package/templates/common/dev/dev-watch.js +8 -8
- package/templates/common/apps/server/src/fetch-data.ts +0 -20
package/index.js
CHANGED
|
@@ -289,8 +289,8 @@ async function init() {
|
|
|
289
289
|
dev: 'node ./dev/dev-watch --env=.env.development --dev=1 --cmd=start-dev',
|
|
290
290
|
build: 'node ./dev/dev-watch --env=.env.production --dev=0 --obfuscate=0',
|
|
291
291
|
'build-mobile': 'node ./dev/dev-watch --env=.env.mobile --dev=0 --mobile=1',
|
|
292
|
-
'start-dev': 'node dist/server_root/server/
|
|
293
|
-
'start-production': 'node dist/server_root/server/
|
|
292
|
+
'start-dev': 'node dist/server_root/server/server-loader.js --env=.env.development',
|
|
293
|
+
'start-production': 'node dist/server_root/server/server-loader.js --env=.env.production',
|
|
294
294
|
format: 'prettier --write "**/*.{js,json,css,scss,md,html,yaml,ts,jsx,tsx}"',
|
|
295
295
|
},
|
|
296
296
|
dependencies: {
|
package/package.json
CHANGED
|
@@ -7,7 +7,6 @@ When performing multi-line code refactoring or replacement operations (`replace_
|
|
|
7
7
|
- The `StartLine` and `EndLine` range MUST be restricted strictly to the absolute minimum lines you intend to modify or delete.
|
|
8
8
|
- If you are merely inserting new code (e.g., adding a button or appending logic), target ONLY the immediately preceding line or bracket as your anchor. You are strictly forbidden from wrapping innocent, unmodified surrounding code into the `Replacement` payload. Violating this red line causes severe production accidents!
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
**SYSTEM ROLE**: You are an expert developer in `lupine.js`, a custom TypeScript full-stack framework.
|
|
12
11
|
|
|
13
12
|
**🛑 CRITICAL WARNINGS 🛑**
|
|
@@ -77,23 +76,23 @@ Since Lupine.js uses a CSS-in-JS styling approach, when you need to define or ov
|
|
|
77
76
|
// 1. Separate theme variables into their own CSS object
|
|
78
77
|
const cssTheme: CssProps = {
|
|
79
78
|
'[data-theme="light" i]': {
|
|
80
|
-
|
|
79
|
+
"--my-comp-bg-color": "#e6e6e6",
|
|
81
80
|
},
|
|
82
81
|
'[data-theme="dark" i]': {
|
|
83
|
-
|
|
82
|
+
"--my-comp-bg-color": "var(--primary-accent-color)",
|
|
84
83
|
},
|
|
85
84
|
};
|
|
86
85
|
// 2. Bind globally. Param 4 (noTopClassName) MUST be true to prevent injecting a namespace prefix.
|
|
87
|
-
bindGlobalStyle(
|
|
86
|
+
bindGlobalStyle("my-comp-theme", cssTheme, false, true);
|
|
88
87
|
|
|
89
88
|
// 3. Use the variable in your standard component styles
|
|
90
89
|
const css: CssProps = {
|
|
91
|
-
|
|
92
|
-
backgroundColor:
|
|
93
|
-
}
|
|
90
|
+
".&-element": {
|
|
91
|
+
backgroundColor: "var(--my-comp-bg-color)",
|
|
92
|
+
},
|
|
94
93
|
};
|
|
95
94
|
// Bind your component styles normally
|
|
96
|
-
bindGlobalStyle(
|
|
95
|
+
bindGlobalStyle("my-comp-main", css);
|
|
97
96
|
```
|
|
98
97
|
|
|
99
98
|
#### 🎨 Color Variable Semantics (CRITICAL FOR DARK MODE)
|
|
@@ -132,22 +131,22 @@ export const MyComponent = () => {
|
|
|
132
131
|
const ref: RefProps = {
|
|
133
132
|
onLoad: async () => {
|
|
134
133
|
// 3. Querying namespaced elements
|
|
135
|
-
const btn = ref.$(
|
|
136
|
-
btn.innerHTML =
|
|
134
|
+
const btn = ref.$(".&-btn");
|
|
135
|
+
btn.innerHTML = "Ready";
|
|
137
136
|
},
|
|
138
137
|
};
|
|
139
138
|
|
|
140
139
|
const css: CssProps = {
|
|
141
140
|
// Top-level rules apply to the root component container itself
|
|
142
|
-
width:
|
|
143
|
-
padding:
|
|
141
|
+
width: "100%",
|
|
142
|
+
padding: "1rem",
|
|
144
143
|
|
|
145
144
|
// 1. Defining namespaced sub-classes in CSS:
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
".&-title": { fontWeight: "bold" },
|
|
146
|
+
".&-btn": {
|
|
148
147
|
// Nesting pseudo-classes and combination modifiers (no space after &)
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
"&:hover": { background: "#f0f0f0" },
|
|
149
|
+
"&.active": { color: "var(--primary-accent-color)" },
|
|
151
150
|
},
|
|
152
151
|
};
|
|
153
152
|
|
|
@@ -155,8 +154,8 @@ export const MyComponent = () => {
|
|
|
155
154
|
// Setting css={css} safely bounds this style scope
|
|
156
155
|
<aside css={css} ref={ref}>
|
|
157
156
|
{/* 2. Applying namespaced classes in JSX */}
|
|
158
|
-
<div class=
|
|
159
|
-
<button class=
|
|
157
|
+
<div class="&-title">Hello</div>
|
|
158
|
+
<button class="&-btn active">Click Me</button>
|
|
160
159
|
</aside>
|
|
161
160
|
);
|
|
162
161
|
};
|
|
@@ -182,6 +181,7 @@ Lupine.js provides two main ways to inject component CSS (`css={}` vs `bindGloba
|
|
|
182
181
|
**Best for**: Pages, views, or high-level containers that are only rendered once per screen.
|
|
183
182
|
|
|
184
183
|
When you pass `css={css}` to a JSX element, Lupine automatically evaluates it and injects a new `<style>` tag directly wrapping that element.
|
|
184
|
+
|
|
185
185
|
- **Pros**: Perfect isolation.
|
|
186
186
|
- **Cons**: If you render 100 items using `css={}`, it will inject 100 identical `<style>` blocks into the DOM, severely bloating the page.
|
|
187
187
|
|
|
@@ -192,11 +192,15 @@ When you pass `css={css}` to a JSX element, Lupine automatically evaluates it an
|
|
|
192
192
|
`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!
|
|
193
193
|
|
|
194
194
|
**How it works seamlessly with `&`**:
|
|
195
|
-
|
|
195
|
+
|
|
196
|
+
1. Generate an ID based on the `CssProps` content: `const globalCssId = getGlobalStylesId(css);`. (Call this _inside_ the component!)
|
|
196
197
|
2. Bind the style block globally once: `bindGlobalStyle(globalCssId, css);`
|
|
197
198
|
3. Assign this ID to the component's `ref` to link the scope: `const ref: RefProps = { globalCssId };` / `<div ref={ref}>`
|
|
198
199
|
4. Use `class="&-item"` normally. Lupine replaces `&` with the identical `globalCssId` across all instances.
|
|
199
200
|
|
|
201
|
+
> [!WARNING]
|
|
202
|
+
> Because `getGlobalStylesId` relies on `getRequestContext()` data to correctly attach and track styles (especially across SSR and interactive client renders), getGlobalStylesId and `bindGlobalStyle` **MUST** be called inside the component function scope. Calling them at the file/module level will result in runtime errors.
|
|
203
|
+
|
|
200
204
|
### ⚠️ IMPORTANT: The "Static `CssProps`" Rule
|
|
201
205
|
|
|
202
206
|
Because `bindGlobalStyle` injects your `<style>` tags into the `<head>` globally, your `CssProps` definition **MUST** be entirely static.
|
|
@@ -205,42 +209,85 @@ Because `bindGlobalStyle` injects your `<style>` tags into the `<head>` globally
|
|
|
205
209
|
|
|
206
210
|
### 🔗 Sharing the same CSS scope (`globalCssId`) among Separated DOMs
|
|
207
211
|
|
|
208
|
-
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`)
|
|
212
|
+
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.
|
|
209
213
|
|
|
210
214
|
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()`:
|
|
211
215
|
|
|
212
216
|
```tsx
|
|
213
|
-
import {
|
|
217
|
+
import {
|
|
218
|
+
globalStyleUniqueId,
|
|
219
|
+
HtmlVar,
|
|
220
|
+
RefProps,
|
|
221
|
+
CssProps,
|
|
222
|
+
} from "lupine.components";
|
|
214
223
|
|
|
215
224
|
export const HomePage = () => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const listDom = new HtmlVar('');
|
|
220
|
-
|
|
221
|
-
const renderList = () => {
|
|
222
|
-
// 2. Explicitly bind the inner detached DOM to the parent's globalCssId
|
|
223
|
-
listDom.value = (
|
|
224
|
-
<div ref={{ globalCssId: cssId }} class="&-bundle-container">
|
|
225
|
-
<div class="&-bundle-name">Basic Bundle</div>
|
|
226
|
-
</div>
|
|
227
|
-
);
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const ref: RefProps = {
|
|
231
|
-
globalCssId: cssId, // 3. The parent registers the ID as well
|
|
232
|
-
onLoad: async () => renderList()
|
|
233
|
-
};
|
|
234
|
-
const css: CssProps = { '.&-bundle-name': { color: 'red' } };
|
|
225
|
+
// 1. Generate a manual ID for the container scope beforehand
|
|
226
|
+
const cssId = globalStyleUniqueId();
|
|
235
227
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
228
|
+
const listDom = new HtmlVar("");
|
|
229
|
+
|
|
230
|
+
const renderList = () => {
|
|
231
|
+
// 2. Explicitly bind the inner detached DOM to the parent's globalCssId
|
|
232
|
+
listDom.value = (
|
|
233
|
+
<div ref={{ globalCssId: cssId }} class="&-bundle-container">
|
|
234
|
+
<div class="&-bundle-name">Basic Bundle</div>
|
|
235
|
+
</div>
|
|
241
236
|
);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const ref: RefProps = {
|
|
240
|
+
globalCssId: cssId, // 3. The parent registers the ID as well
|
|
241
|
+
onLoad: async () => renderList(),
|
|
242
|
+
};
|
|
243
|
+
const css: CssProps = { ".&-bundle-name": { color: "red" } };
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div css={css} ref={ref}>
|
|
247
|
+
{/* 4. The dynamically injected nodes will properly map their &- prefixes */}
|
|
248
|
+
{listDom.node}
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
};
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Using `&` on Top-Level Tags
|
|
255
|
+
|
|
256
|
+
The following example illustrates how to correctly use `&` in the `class` of a top-level tag.
|
|
257
|
+
|
|
258
|
+
Generally, you should not need to use `&` classes on the top-level tag because you can reference the top-level tag directly via `ref.current`. For styling, the first-level styles defined directly under your `CssProps` object are automatically applied to the top-level tag (e.g., `color: 'red'` below).
|
|
259
|
+
|
|
260
|
+
However, when there is a special need to use an `&-` class prefix on the top-level tag, you must be careful: **`"&.&-box"`** is the correct syntax. This is because the standalone `&` selector is replaced by both the explicit `gCssId` and the CSS ID automatically generated for this top-level tag.
|
|
261
|
+
|
|
262
|
+
For instance, if `gCssId="g00"` and the auto-generated CSS ID applied by the `ref` is `"l01"`, then `"&.&-box"` compiles to `"g00.g00-box, l01.l01-box"`.
|
|
263
|
+
|
|
264
|
+
Similarly, a nested selector like `"&.&-box .&-item"` will be compiled into `"g00.g00-box .g00-item, l01.l01-box .l01-item"`.
|
|
265
|
+
|
|
266
|
+
*(Alternatively, if you define the class without the `&-` prefix like `class="box"`, you would target it using `"&.box"`).*
|
|
267
|
+
```typescript
|
|
268
|
+
export const Component1 = () => {
|
|
269
|
+
|
|
270
|
+
const css: CssProps = {
|
|
271
|
+
color: 'red',
|
|
272
|
+
"&.&-box": { fontWeight: 'bold' },
|
|
273
|
+
"&.&-box .&-item": { backgroundColor: 'blue' },
|
|
274
|
+
};
|
|
275
|
+
const gCssId = getGlobalStylesId(css);
|
|
276
|
+
bindGlobalStyle(gCssId, css);
|
|
277
|
+
|
|
278
|
+
const ref: RefProps = {
|
|
279
|
+
globalCssId: gCssId,
|
|
280
|
+
};
|
|
281
|
+
return (
|
|
282
|
+
<div ref={ref} class='&-box'>
|
|
283
|
+
<div class='&-item'>item</div>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
242
286
|
};
|
|
243
287
|
```
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
244
291
|
---
|
|
245
292
|
|
|
246
293
|
## 5. Common Patterns ("The Lupine Way")
|
|
@@ -272,7 +319,7 @@ const MyPage = () => {
|
|
|
272
319
|
// 4. Events
|
|
273
320
|
const onSearch = async () => {
|
|
274
321
|
// Read directly from DOM
|
|
275
|
-
const query = ref.$(
|
|
322
|
+
const query = ref.$("input.&-search").value;
|
|
276
323
|
// Update logic var
|
|
277
324
|
pageIndex = 0;
|
|
278
325
|
// Update UI manually
|
|
@@ -287,7 +334,7 @@ const MyPage = () => {
|
|
|
287
334
|
|
|
288
335
|
return (
|
|
289
336
|
<div ref={ref}>
|
|
290
|
-
<input class=
|
|
337
|
+
<input class="&-search" />
|
|
291
338
|
<button onClick={onSearch}>Go</button>
|
|
292
339
|
{/* Embed Dynamic Content */}
|
|
293
340
|
{listDom.node}
|
|
@@ -295,22 +342,24 @@ const MyPage = () => {
|
|
|
295
342
|
);
|
|
296
343
|
};
|
|
297
344
|
```
|
|
345
|
+
|
|
298
346
|
### Page Navigation (`initializePage` vs `<a>`)
|
|
299
347
|
|
|
300
348
|
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.
|
|
301
349
|
|
|
302
|
-
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.
|
|
350
|
+
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.
|
|
303
351
|
|
|
304
352
|
Instead, import and use `initializePage`:
|
|
353
|
+
|
|
305
354
|
```typescript
|
|
306
|
-
import { initializePage } from
|
|
355
|
+
import { initializePage } from "lupine.web";
|
|
307
356
|
|
|
308
357
|
const navigate = () => {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
358
|
+
// CORRECT: Seamless SPA transition
|
|
359
|
+
initializePage("/play/diff01/1");
|
|
360
|
+
|
|
361
|
+
// ERROR / ANTI-PATTERN: Forces full browser reload unless explicitly desired
|
|
362
|
+
// window.location.href = '/play/diff01/1';
|
|
314
363
|
};
|
|
315
364
|
```
|
|
316
365
|
|
|
@@ -319,7 +368,11 @@ const navigate = () => {
|
|
|
319
368
|
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.
|
|
320
369
|
|
|
321
370
|
```typescript
|
|
322
|
-
import {
|
|
371
|
+
import {
|
|
372
|
+
SliderFrame,
|
|
373
|
+
SliderFrameHookProps,
|
|
374
|
+
HeaderWithBackFrame,
|
|
375
|
+
} from "lupine.components";
|
|
323
376
|
|
|
324
377
|
// 1. Parent Component (or Level 1)
|
|
325
378
|
const Parent = () => {
|
|
@@ -328,7 +381,9 @@ const Parent = () => {
|
|
|
328
381
|
|
|
329
382
|
const openDetail = (id) => {
|
|
330
383
|
// Push new view onto stack
|
|
331
|
-
sliderHook.load!(
|
|
384
|
+
sliderHook.load!(
|
|
385
|
+
<DetailComponent id={id} parentSliderFrameHook={sliderHook} />,
|
|
386
|
+
);
|
|
332
387
|
};
|
|
333
388
|
|
|
334
389
|
return (
|
|
@@ -342,17 +397,28 @@ const Parent = () => {
|
|
|
342
397
|
};
|
|
343
398
|
|
|
344
399
|
// 2. Child Component (Level 2)
|
|
345
|
-
const DetailComponent = (props: {
|
|
400
|
+
const DetailComponent = (props: {
|
|
401
|
+
id: number;
|
|
402
|
+
parentSliderFrameHook: SliderFrameHookProps;
|
|
403
|
+
}) => {
|
|
346
404
|
// Define hook for Level 3
|
|
347
405
|
const childSliderHook: SliderFrameHookProps = {};
|
|
348
406
|
|
|
349
407
|
const openDeeper = () => {
|
|
350
408
|
// Load Level 3 component into this component's placeholder
|
|
351
|
-
childSliderHook.load!(
|
|
409
|
+
childSliderHook.load!(
|
|
410
|
+
<DetailComponent
|
|
411
|
+
id={props.id + 1}
|
|
412
|
+
parentSliderFrameHook={childSliderHook}
|
|
413
|
+
/>,
|
|
414
|
+
);
|
|
352
415
|
};
|
|
353
416
|
|
|
354
417
|
return (
|
|
355
|
-
<HeaderWithBackFrame
|
|
418
|
+
<HeaderWithBackFrame
|
|
419
|
+
title="Detail Page"
|
|
420
|
+
onBack={(e) => props.parentSliderFrameHook.close!(e)}
|
|
421
|
+
>
|
|
356
422
|
{/* Placeholder for Level 3 */}
|
|
357
423
|
<SliderFrame hook={childSliderHook} />
|
|
358
424
|
|
|
@@ -388,7 +454,7 @@ const Parent = () => {
|
|
|
388
454
|
const ref: RefProps = {
|
|
389
455
|
onLoad: async () => {
|
|
390
456
|
// Safe: Child has rendered and populated the hook
|
|
391
|
-
myHook.setValue(
|
|
457
|
+
myHook.setValue("Hello");
|
|
392
458
|
},
|
|
393
459
|
};
|
|
394
460
|
|
|
@@ -441,14 +507,14 @@ You can import an `.svg` file (if your bundler supports it) or define a raw Data
|
|
|
441
507
|
|
|
442
508
|
```typescript
|
|
443
509
|
// Option A: Using bundler import
|
|
444
|
-
import githubIcon from
|
|
510
|
+
import githubIcon from "github.svg";
|
|
445
511
|
|
|
446
512
|
// Option B: Raw Data URI string
|
|
447
513
|
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`;
|
|
448
514
|
|
|
449
515
|
export const DemoIcons = {
|
|
450
516
|
github: githubIcon,
|
|
451
|
-
|
|
517
|
+
"ma-close": closeSvgData,
|
|
452
518
|
};
|
|
453
519
|
```
|
|
454
520
|
|
|
@@ -458,9 +524,9 @@ Use the `-webkit-mask-image` and `maskImage` property wrapped in `url()` to appl
|
|
|
458
524
|
```typescript
|
|
459
525
|
const css: CssProps = {
|
|
460
526
|
// Target the specific system icon class you wish to override
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
maskImage: `url("${DemoIcons[
|
|
527
|
+
".ifc-icon.ma-close": {
|
|
528
|
+
"-webkit-mask-image": `url("${DemoIcons["ma-close"]}")`,
|
|
529
|
+
maskImage: `url("${DemoIcons["ma-close"]}")`,
|
|
464
530
|
// If needed, specify mask sizing properties:
|
|
465
531
|
// maskRepeat: 'no-repeat',
|
|
466
532
|
// maskPosition: 'center',
|
|
@@ -555,42 +621,51 @@ For interactive lists, `createDragUtil()` from `lupine.components` handles compl
|
|
|
555
621
|
**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:
|
|
556
622
|
|
|
557
623
|
1. **Option Selection (`ActionSheetSelectPromise`)** (Replaces `confirm()` or complex choices):
|
|
624
|
+
|
|
558
625
|
```typescript
|
|
559
|
-
import { ActionSheetSelectPromise } from
|
|
560
|
-
|
|
626
|
+
import { ActionSheetSelectPromise } from "lupine.components";
|
|
627
|
+
|
|
561
628
|
const index = await ActionSheetSelectPromise({
|
|
562
|
-
title:
|
|
563
|
-
options: [
|
|
564
|
-
cancelButtonText:
|
|
629
|
+
title: "Delete this saved game?", // Optional
|
|
630
|
+
options: ["Delete", "Edit"],
|
|
631
|
+
cancelButtonText: "Cancel",
|
|
565
632
|
});
|
|
566
|
-
|
|
567
|
-
if (index === 0) {
|
|
568
|
-
|
|
633
|
+
|
|
634
|
+
if (index === 0) {
|
|
635
|
+
/* User clicked Delete (Index of options array) */
|
|
636
|
+
}
|
|
637
|
+
if (index === -1) {
|
|
638
|
+
/* User clicked Cancel or tapped background */
|
|
639
|
+
}
|
|
569
640
|
```
|
|
570
641
|
|
|
571
642
|
2. **Simple Messages (`ActionSheetMessagePromise`)** (Replaces `alert()`):
|
|
643
|
+
|
|
572
644
|
```typescript
|
|
573
|
-
import { ActionSheetMessagePromise } from
|
|
574
|
-
|
|
645
|
+
import { ActionSheetMessagePromise } from "lupine.components";
|
|
646
|
+
|
|
575
647
|
await ActionSheetMessagePromise({
|
|
576
|
-
title:
|
|
577
|
-
message:
|
|
578
|
-
closeButtonText:
|
|
648
|
+
title: "Success", // Optional
|
|
649
|
+
message: "Your profile has been saved.",
|
|
650
|
+
closeButtonText: "OK", // Optional, defaults to a close behavior
|
|
579
651
|
});
|
|
580
652
|
```
|
|
581
653
|
|
|
582
654
|
3. **User Input (`ActionSheetInputPromise`)** (Replaces `prompt()`):
|
|
655
|
+
|
|
583
656
|
```typescript
|
|
584
|
-
import { ActionSheetInputPromise } from
|
|
585
|
-
|
|
657
|
+
import { ActionSheetInputPromise } from "lupine.components";
|
|
658
|
+
|
|
586
659
|
const value = await ActionSheetInputPromise({
|
|
587
|
-
title:
|
|
660
|
+
title: "Enter your name",
|
|
588
661
|
// placeholder: 'Player 1', // Optional
|
|
589
|
-
confirmButtonText:
|
|
590
|
-
cancelButtonText:
|
|
662
|
+
confirmButtonText: "Submit", // Optional
|
|
663
|
+
cancelButtonText: "Cancel", // Optional
|
|
591
664
|
});
|
|
592
|
-
|
|
593
|
-
if (value !== null) {
|
|
665
|
+
|
|
666
|
+
if (value !== null) {
|
|
667
|
+
/* User submitted a string */
|
|
668
|
+
}
|
|
594
669
|
```
|
|
595
670
|
|
|
596
671
|
4. **Other Available Prompts (Investigate their API via `lupine.components` when needed)**:
|
|
@@ -605,14 +680,14 @@ When building mobile interfaces, users expect the physical hardware "Back" butto
|
|
|
605
680
|
**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`.
|
|
606
681
|
|
|
607
682
|
```typescript
|
|
608
|
-
import { backActionHelper } from
|
|
683
|
+
import { backActionHelper } from "lupine.components";
|
|
609
684
|
|
|
610
685
|
export const MyCloseButton = ({ onClose }) => {
|
|
611
686
|
return (
|
|
612
|
-
<i
|
|
687
|
+
<i
|
|
613
688
|
class="ifc-icon ma-close"
|
|
614
689
|
// Generate a unique ID for the back stack
|
|
615
|
-
data-back-action={backActionHelper.genBackActionId()}
|
|
690
|
+
data-back-action={backActionHelper.genBackActionId()}
|
|
616
691
|
onClick={onClose}
|
|
617
692
|
></i>
|
|
618
693
|
);
|
|
@@ -620,8 +695,9 @@ export const MyCloseButton = ({ onClose }) => {
|
|
|
620
695
|
```
|
|
621
696
|
|
|
622
697
|
**How it works**:
|
|
698
|
+
|
|
623
699
|
- When the hardware back button is pressed, the underlying system automatically queries the DOM for all elements with `[data-back-action^="bb-"]`.
|
|
624
700
|
- It finds the most recently created component (the top-most overlay) and automatically triggers a `.click()` event on it.
|
|
625
|
-
- **Dynamic Mounting vs Static**:
|
|
701
|
+
- **Dynamic Mounting vs Static**:
|
|
626
702
|
- For components that are injected and removed dynamically (like `<ActionSheet />` or `<FloatWindow />`), simply attaching the property to the React/JSX node is sufficient.
|
|
627
703
|
- 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.
|
|
@@ -1,81 +1,76 @@
|
|
|
1
|
-
// initApp should be called before any other logics, so need to avoid `export default new Class()`
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import {
|
|
4
|
-
CryptoUtils,
|
|
5
|
-
HostToPathProps,
|
|
6
|
-
appStart,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
domainCerts,
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
};
|
|
81
|
-
initAndStartServer();
|
|
1
|
+
// initApp should be called before any other logics, so need to avoid `export default new Class()`
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
CryptoUtils,
|
|
5
|
+
HostToPathProps,
|
|
6
|
+
appStart,
|
|
7
|
+
getDefaultDbConfig,
|
|
8
|
+
loadEnv,
|
|
9
|
+
setAccessControlAllowHost,
|
|
10
|
+
} from 'lupine.api/server';
|
|
11
|
+
import { ServerEnvKeys } from './server-env-keys';
|
|
12
|
+
|
|
13
|
+
const initAndStartServer = async () => {
|
|
14
|
+
setAccessControlAllowHost(['localhost', '127.0.0.1']);
|
|
15
|
+
|
|
16
|
+
const envFile = process.argv.find((i) => i.startsWith('--env='))?.substring(6) || '.env';
|
|
17
|
+
// it can use "#!import file_name" to import another env file
|
|
18
|
+
await loadEnv(envFile);
|
|
19
|
+
|
|
20
|
+
const dbConfig = { ...getDefaultDbConfig() };
|
|
21
|
+
const serverRootPath = path.resolve(process.env[ServerEnvKeys.SERVER_ROOT_PATH]!);
|
|
22
|
+
const apps = (process.env[ServerEnvKeys.APPS] || '').split(',');
|
|
23
|
+
const webRootMap: HostToPathProps[] = [];
|
|
24
|
+
|
|
25
|
+
const domainCerts: Record<string, { key: string; cert: string }> = {};
|
|
26
|
+
for (const app of apps) {
|
|
27
|
+
const appHosts = process.env[`${ServerEnvKeys.DOMAINS}:${app}`] || '';
|
|
28
|
+
const dbFilename =
|
|
29
|
+
process.env[`${ServerEnvKeys.DB_FILENAME}:${app}`] || process.env[`${ServerEnvKeys.DB_FILENAME}`] || 'sqlite3.db';
|
|
30
|
+
webRootMap.push({
|
|
31
|
+
appName: app,
|
|
32
|
+
hosts: appHosts ? appHosts.split(',') : [],
|
|
33
|
+
// web, data, api folders should be created in building process
|
|
34
|
+
webPath: path.join(serverRootPath, app + '_web'),
|
|
35
|
+
dataPath: path.join(serverRootPath, app + '_data'),
|
|
36
|
+
apiPath: path.join(serverRootPath, app + '_api'),
|
|
37
|
+
dbType: process.env[`${ServerEnvKeys.DB_TYPE}:${app}`] || process.env[`${ServerEnvKeys.DB_TYPE}`] || 'sqlite',
|
|
38
|
+
dbConfig: { ...dbConfig, filename: dbFilename },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const appDomains = appHosts.split(',');
|
|
42
|
+
for (const domain of appDomains) {
|
|
43
|
+
domainCerts[domain] = {
|
|
44
|
+
key: process.env[`${ServerEnvKeys.SSL_KEY_PATH}:${app}`] || '',
|
|
45
|
+
cert: process.env[`${ServerEnvKeys.SSL_CRT_PATH}:${app}`] || '',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const bindIp = process.env[ServerEnvKeys.BIND_IP] || '::';
|
|
51
|
+
// 0 to disable http/https server
|
|
52
|
+
const httpPort = Number.parseInt(process.env[ServerEnvKeys.HTTP_PORT] || '8080');
|
|
53
|
+
const httpsPort = Number.parseInt(process.env[ServerEnvKeys.HTTPS_PORT] || '8443');
|
|
54
|
+
const sslKeyPath = process.env[ServerEnvKeys.SSL_KEY_PATH] || '';
|
|
55
|
+
const sslCrtPath = process.env[ServerEnvKeys.SSL_CRT_PATH] || '';
|
|
56
|
+
|
|
57
|
+
// Can't use log until initApp is called (after AppStart.start)
|
|
58
|
+
await appStart.start({
|
|
59
|
+
debug: process.env[ServerEnvKeys.NODE_ENV] === 'development',
|
|
60
|
+
devToken: CryptoUtils.sha256(process.env['DEV_TOKEN'] || ''),
|
|
61
|
+
appEnvFile: envFile,
|
|
62
|
+
apiConfig: {
|
|
63
|
+
serverRoot: `${serverRootPath}`,
|
|
64
|
+
webHostMap: webRootMap,
|
|
65
|
+
},
|
|
66
|
+
serverConfig: {
|
|
67
|
+
bindIp,
|
|
68
|
+
httpPort,
|
|
69
|
+
httpsPort,
|
|
70
|
+
sslKeyPath,
|
|
71
|
+
sslCrtPath,
|
|
72
|
+
domainCerts,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
initAndStartServer();
|
|
@@ -66,14 +66,14 @@ const watchServerPlugin = (isDev, npmCmd, httpPort) => {
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
// watch server code changes
|
|
69
|
-
const
|
|
69
|
+
const watchServerLoader = async (isDev, npmCmd, httpPort, serverRootPath) => {
|
|
70
70
|
const cmd = isDev ? esbuild.context : esbuild.build;
|
|
71
71
|
const ctx = await cmd({
|
|
72
|
-
entryPoints: ['apps/server/src/
|
|
72
|
+
entryPoints: ['apps/server/src/server-loader.ts'],
|
|
73
73
|
// outdir: path.join(serverRootPath, 'server'),
|
|
74
|
-
outfile: path.join(serverRootPath, 'server', '
|
|
74
|
+
outfile: path.join(serverRootPath, 'server', 'server-loader.js'),
|
|
75
75
|
platform: 'node',
|
|
76
|
-
sourcemap:
|
|
76
|
+
sourcemap: isDev ? 'inline' : false, // inline
|
|
77
77
|
format: 'cjs',
|
|
78
78
|
bundle: true,
|
|
79
79
|
treeShaking: true,
|
|
@@ -93,7 +93,7 @@ const watchServer = async (isDev, npmCmd, httpPort, serverRootPath) => {
|
|
|
93
93
|
entryPoints: ['apps/server/src/index.ts'],
|
|
94
94
|
outdir: path.join(serverRootPath, 'server'),
|
|
95
95
|
platform: 'node',
|
|
96
|
-
sourcemap:
|
|
96
|
+
sourcemap: isDev ? 'inline' : false, // inline
|
|
97
97
|
format: 'cjs',
|
|
98
98
|
bundle: true,
|
|
99
99
|
treeShaking: true,
|
|
@@ -167,7 +167,7 @@ const watchClient = async (saved, isDev, entryPoints, outbase) => {
|
|
|
167
167
|
outbase,
|
|
168
168
|
// entryNames: '[name]-[hash]',
|
|
169
169
|
platform: 'browser',
|
|
170
|
-
sourcemap:
|
|
170
|
+
sourcemap: isDev ? 'inline' : false, // inline
|
|
171
171
|
format: 'iife',
|
|
172
172
|
bundle: true,
|
|
173
173
|
treeShaking: true,
|
|
@@ -209,7 +209,7 @@ const watchApi = async (saved, isDev, entryPoints) => {
|
|
|
209
209
|
outdir: saved.outdirApi,
|
|
210
210
|
// outbase,
|
|
211
211
|
platform: 'node',
|
|
212
|
-
sourcemap:
|
|
212
|
+
sourcemap: isDev ? 'inline' : false, // inline
|
|
213
213
|
format: 'cjs', // iife, cjs
|
|
214
214
|
bundle: true,
|
|
215
215
|
treeShaking: true,
|
|
@@ -366,6 +366,6 @@ const start = async () => {
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
watchServer(isDev, npmCmd, httpPort, serverRootPath);
|
|
369
|
-
|
|
369
|
+
watchServerLoader(isDev, npmCmd, httpPort, serverRootPath);
|
|
370
370
|
};
|
|
371
371
|
start();
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { JsonObject } from 'lupine.api';
|
|
2
|
-
|
|
3
|
-
export const fetchData = async (urlWithoutHost: string, postData: string | JsonObject) => {
|
|
4
|
-
const url = process.env['API_BASE_URL'] + urlWithoutHost;
|
|
5
|
-
console.log('========fetchData', url);
|
|
6
|
-
|
|
7
|
-
const option = {
|
|
8
|
-
method: postData ? 'POST' : 'GET',
|
|
9
|
-
body: postData ? (typeof postData === 'string' ? postData : JSON.stringify(postData)) : undefined,
|
|
10
|
-
};
|
|
11
|
-
const data = await fetch(url, option);
|
|
12
|
-
// const json = await data.json();
|
|
13
|
-
const text = await data.text();
|
|
14
|
-
try {
|
|
15
|
-
const json = JSON.parse(text);
|
|
16
|
-
return { json };
|
|
17
|
-
} catch (e) {
|
|
18
|
-
return { text };
|
|
19
|
-
}
|
|
20
|
-
};
|