create-lupine 1.0.23 → 1.0.25
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 +15 -10
- package/package.json +1 -1
- package/templates/common/AI_CONTEXT.md +56 -80
package/index.js
CHANGED
|
@@ -6,17 +6,22 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
7
|
import { execSync } from 'node:child_process';
|
|
8
8
|
|
|
9
|
-
function getLatestVersion(pkgName, fallback) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
function getLatestVersion(pkgName, fallback, retries = 2) {
|
|
10
|
+
for (let i = 0; i <= retries; i++) {
|
|
11
|
+
try {
|
|
12
|
+
const version = execSync(`npm view ${pkgName} version`, {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
15
|
+
timeout: 10000,
|
|
16
|
+
}).trim();
|
|
17
|
+
return version ? `^${version}` : fallback;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
if (i === retries) {
|
|
20
|
+
throw new Error(`\x1b[31m✖ Failed to fetch latest version for ${pkgName} from npm registry after ${retries} retries. Please check your network connection.\x1b[0m`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
19
23
|
}
|
|
24
|
+
return fallback;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
function generateRandomString(length) {
|
package/package.json
CHANGED
|
@@ -76,23 +76,23 @@ Since Lupine.js uses a CSS-in-JS styling approach, when you need to define or ov
|
|
|
76
76
|
// 1. Separate theme variables into their own CSS object
|
|
77
77
|
const cssTheme: CssProps = {
|
|
78
78
|
'[data-theme="light" i]': {
|
|
79
|
-
|
|
79
|
+
'--my-comp-bg-color': '#e6e6e6',
|
|
80
80
|
},
|
|
81
81
|
'[data-theme="dark" i]': {
|
|
82
|
-
|
|
82
|
+
'--my-comp-bg-color': 'var(--primary-accent-color)',
|
|
83
83
|
},
|
|
84
84
|
};
|
|
85
85
|
// 2. Bind globally. Param 4 (noTopClassName) MUST be true to prevent injecting a namespace prefix.
|
|
86
|
-
bindGlobalStyle(
|
|
86
|
+
bindGlobalStyle('my-comp-theme', cssTheme, false, true);
|
|
87
87
|
|
|
88
88
|
// 3. Use the variable in your standard component styles
|
|
89
89
|
const css: CssProps = {
|
|
90
|
-
|
|
91
|
-
backgroundColor:
|
|
90
|
+
'.&-element': {
|
|
91
|
+
backgroundColor: 'var(--my-comp-bg-color)',
|
|
92
92
|
},
|
|
93
93
|
};
|
|
94
94
|
// Bind your component styles normally
|
|
95
|
-
bindGlobalStyle(
|
|
95
|
+
bindGlobalStyle('my-comp-main', css);
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
#### 🎨 Color Variable Semantics (CRITICAL FOR DARK MODE)
|
|
@@ -131,22 +131,22 @@ export const MyComponent = () => {
|
|
|
131
131
|
const ref: RefProps = {
|
|
132
132
|
onLoad: async () => {
|
|
133
133
|
// 3. Querying namespaced elements
|
|
134
|
-
const btn = ref.$(
|
|
135
|
-
btn.innerHTML =
|
|
134
|
+
const btn = ref.$('.&-btn');
|
|
135
|
+
btn.innerHTML = 'Ready';
|
|
136
136
|
},
|
|
137
137
|
};
|
|
138
138
|
|
|
139
139
|
const css: CssProps = {
|
|
140
140
|
// Top-level rules apply to the root component container itself
|
|
141
|
-
width:
|
|
142
|
-
padding:
|
|
141
|
+
width: '100%',
|
|
142
|
+
padding: '1rem',
|
|
143
143
|
|
|
144
144
|
// 1. Defining namespaced sub-classes in CSS:
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
'.&-title': { fontWeight: 'bold' },
|
|
146
|
+
'.&-btn': {
|
|
147
147
|
// Nesting pseudo-classes and combination modifiers (no space after &)
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
'&:hover': { background: '#f0f0f0' },
|
|
149
|
+
'&.active': { color: 'var(--primary-accent-color)' },
|
|
150
150
|
},
|
|
151
151
|
};
|
|
152
152
|
|
|
@@ -154,8 +154,8 @@ export const MyComponent = () => {
|
|
|
154
154
|
// Setting css={css} safely bounds this style scope
|
|
155
155
|
<aside css={css} ref={ref}>
|
|
156
156
|
{/* 2. Applying namespaced classes in JSX */}
|
|
157
|
-
<div class=
|
|
158
|
-
<button class=
|
|
157
|
+
<div class='&-title'>Hello</div>
|
|
158
|
+
<button class='&-btn active'>Click Me</button>
|
|
159
159
|
</aside>
|
|
160
160
|
);
|
|
161
161
|
};
|
|
@@ -214,24 +214,19 @@ If your component divides its logic so that some internal floating DOM elements
|
|
|
214
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()`:
|
|
215
215
|
|
|
216
216
|
```tsx
|
|
217
|
-
import {
|
|
218
|
-
globalStyleUniqueId,
|
|
219
|
-
HtmlVar,
|
|
220
|
-
RefProps,
|
|
221
|
-
CssProps,
|
|
222
|
-
} from "lupine.components";
|
|
217
|
+
import { globalStyleUniqueId, HtmlVar, RefProps, CssProps } from 'lupine.components';
|
|
223
218
|
|
|
224
219
|
export const HomePage = () => {
|
|
225
220
|
// 1. Generate a manual ID for the container scope beforehand
|
|
226
221
|
const cssId = globalStyleUniqueId();
|
|
227
222
|
|
|
228
|
-
const listDom = new HtmlVar(
|
|
223
|
+
const listDom = new HtmlVar('');
|
|
229
224
|
|
|
230
225
|
const renderList = () => {
|
|
231
226
|
// 2. Explicitly bind the inner detached DOM to the parent's globalCssId
|
|
232
227
|
listDom.value = (
|
|
233
|
-
<div ref={{ globalCssId: cssId }} class=
|
|
234
|
-
<div class=
|
|
228
|
+
<div ref={{ globalCssId: cssId }} class='&-bundle-container'>
|
|
229
|
+
<div class='&-bundle-name'>Basic Bundle</div>
|
|
235
230
|
</div>
|
|
236
231
|
);
|
|
237
232
|
};
|
|
@@ -240,7 +235,7 @@ export const HomePage = () => {
|
|
|
240
235
|
globalCssId: cssId, // 3. The parent registers the ID as well
|
|
241
236
|
onLoad: async () => renderList(),
|
|
242
237
|
};
|
|
243
|
-
const css: CssProps = {
|
|
238
|
+
const css: CssProps = { '.&-bundle-name': { color: 'red' } };
|
|
244
239
|
|
|
245
240
|
return (
|
|
246
241
|
<div css={css} ref={ref}>
|
|
@@ -253,24 +248,24 @@ export const HomePage = () => {
|
|
|
253
248
|
|
|
254
249
|
### Using `&` on Top-Level Tags
|
|
255
250
|
|
|
256
|
-
The following example illustrates how to correctly use `&` in the `class` of a top-level tag.
|
|
251
|
+
The following example illustrates how to correctly use `&` in the `class` of a top-level tag.
|
|
257
252
|
|
|
258
253
|
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
254
|
|
|
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.
|
|
255
|
+
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
256
|
|
|
262
257
|
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
258
|
|
|
264
259
|
Similarly, a nested selector like `"&.&-box .&-item"` will be compiled into `"g00.g00-box .g00-item, l01.l01-box .l01-item"`.
|
|
265
260
|
|
|
266
|
-
|
|
261
|
+
_(Alternatively, if you define the class without the `&-` prefix like `class="box"`, you would target it using `"&.box"`)._
|
|
262
|
+
|
|
267
263
|
```typescript
|
|
268
264
|
export const Component1 = () => {
|
|
269
|
-
|
|
270
265
|
const css: CssProps = {
|
|
271
266
|
color: 'red',
|
|
272
|
-
|
|
273
|
-
|
|
267
|
+
'&.&-box': { fontWeight: 'bold' },
|
|
268
|
+
'&.&-box .&-item': { backgroundColor: 'blue' },
|
|
274
269
|
};
|
|
275
270
|
const gCssId = getGlobalStylesId(css);
|
|
276
271
|
bindGlobalStyle(gCssId, css);
|
|
@@ -286,8 +281,6 @@ export const Component1 = () => {
|
|
|
286
281
|
};
|
|
287
282
|
```
|
|
288
283
|
|
|
289
|
-
|
|
290
|
-
|
|
291
284
|
---
|
|
292
285
|
|
|
293
286
|
## 5. Common Patterns ("The Lupine Way")
|
|
@@ -319,7 +312,7 @@ const MyPage = () => {
|
|
|
319
312
|
// 4. Events
|
|
320
313
|
const onSearch = async () => {
|
|
321
314
|
// Read directly from DOM
|
|
322
|
-
const query = ref.$(
|
|
315
|
+
const query = ref.$('input.&-search').value;
|
|
323
316
|
// Update logic var
|
|
324
317
|
pageIndex = 0;
|
|
325
318
|
// Update UI manually
|
|
@@ -334,7 +327,7 @@ const MyPage = () => {
|
|
|
334
327
|
|
|
335
328
|
return (
|
|
336
329
|
<div ref={ref}>
|
|
337
|
-
<input class=
|
|
330
|
+
<input class='&-search' />
|
|
338
331
|
<button onClick={onSearch}>Go</button>
|
|
339
332
|
{/* Embed Dynamic Content */}
|
|
340
333
|
{listDom.node}
|
|
@@ -352,11 +345,11 @@ When performing imperative or programmatic routing via JavaScript (e.g. clicking
|
|
|
352
345
|
Instead, import and use `initializePage`:
|
|
353
346
|
|
|
354
347
|
```typescript
|
|
355
|
-
import { initializePage } from
|
|
348
|
+
import { initializePage } from 'lupine.web';
|
|
356
349
|
|
|
357
350
|
const navigate = () => {
|
|
358
351
|
// CORRECT: Seamless SPA transition
|
|
359
|
-
initializePage(
|
|
352
|
+
initializePage('/play/diff01/1');
|
|
360
353
|
|
|
361
354
|
// ERROR / ANTI-PATTERN: Forces full browser reload unless explicitly desired
|
|
362
355
|
// window.location.href = '/play/diff01/1';
|
|
@@ -368,11 +361,7 @@ const navigate = () => {
|
|
|
368
361
|
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.
|
|
369
362
|
|
|
370
363
|
```typescript
|
|
371
|
-
import {
|
|
372
|
-
SliderFrame,
|
|
373
|
-
SliderFrameHookProps,
|
|
374
|
-
HeaderWithBackFrame,
|
|
375
|
-
} from "lupine.components";
|
|
364
|
+
import { SliderFrame, SliderFrameHookProps, HeaderWithBackFrame } from 'lupine.components';
|
|
376
365
|
|
|
377
366
|
// 1. Parent Component (or Level 1)
|
|
378
367
|
const Parent = () => {
|
|
@@ -381,9 +370,7 @@ const Parent = () => {
|
|
|
381
370
|
|
|
382
371
|
const openDetail = (id) => {
|
|
383
372
|
// Push new view onto stack
|
|
384
|
-
sliderHook.load!(
|
|
385
|
-
<DetailComponent id={id} parentSliderFrameHook={sliderHook} />,
|
|
386
|
-
);
|
|
373
|
+
sliderHook.load!(<DetailComponent id={id} parentSliderFrameHook={sliderHook} />);
|
|
387
374
|
};
|
|
388
375
|
|
|
389
376
|
return (
|
|
@@ -397,28 +384,17 @@ const Parent = () => {
|
|
|
397
384
|
};
|
|
398
385
|
|
|
399
386
|
// 2. Child Component (Level 2)
|
|
400
|
-
const DetailComponent = (props: {
|
|
401
|
-
id: number;
|
|
402
|
-
parentSliderFrameHook: SliderFrameHookProps;
|
|
403
|
-
}) => {
|
|
387
|
+
const DetailComponent = (props: { id: number; parentSliderFrameHook: SliderFrameHookProps }) => {
|
|
404
388
|
// Define hook for Level 3
|
|
405
389
|
const childSliderHook: SliderFrameHookProps = {};
|
|
406
390
|
|
|
407
391
|
const openDeeper = () => {
|
|
408
392
|
// Load Level 3 component into this component's placeholder
|
|
409
|
-
childSliderHook.load!(
|
|
410
|
-
<DetailComponent
|
|
411
|
-
id={props.id + 1}
|
|
412
|
-
parentSliderFrameHook={childSliderHook}
|
|
413
|
-
/>,
|
|
414
|
-
);
|
|
393
|
+
childSliderHook.load!(<DetailComponent id={props.id + 1} parentSliderFrameHook={childSliderHook} />);
|
|
415
394
|
};
|
|
416
395
|
|
|
417
396
|
return (
|
|
418
|
-
<HeaderWithBackFrame
|
|
419
|
-
title="Detail Page"
|
|
420
|
-
onBack={(e) => props.parentSliderFrameHook.close!(e)}
|
|
421
|
-
>
|
|
397
|
+
<HeaderWithBackFrame title='Detail Page' onBack={(e) => props.parentSliderFrameHook.close!(e)}>
|
|
422
398
|
{/* Placeholder for Level 3 */}
|
|
423
399
|
<SliderFrame hook={childSliderHook} />
|
|
424
400
|
|
|
@@ -454,7 +430,7 @@ const Parent = () => {
|
|
|
454
430
|
const ref: RefProps = {
|
|
455
431
|
onLoad: async () => {
|
|
456
432
|
// Safe: Child has rendered and populated the hook
|
|
457
|
-
myHook.setValue(
|
|
433
|
+
myHook.setValue('Hello');
|
|
458
434
|
},
|
|
459
435
|
};
|
|
460
436
|
|
|
@@ -507,14 +483,14 @@ You can import an `.svg` file (if your bundler supports it) or define a raw Data
|
|
|
507
483
|
|
|
508
484
|
```typescript
|
|
509
485
|
// Option A: Using bundler import
|
|
510
|
-
import githubIcon from
|
|
486
|
+
import githubIcon from 'github.svg';
|
|
511
487
|
|
|
512
488
|
// Option B: Raw Data URI string
|
|
513
489
|
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`;
|
|
514
490
|
|
|
515
491
|
export const DemoIcons = {
|
|
516
492
|
github: githubIcon,
|
|
517
|
-
|
|
493
|
+
'ma-close': closeSvgData,
|
|
518
494
|
};
|
|
519
495
|
```
|
|
520
496
|
|
|
@@ -524,9 +500,9 @@ Use the `-webkit-mask-image` and `maskImage` property wrapped in `url()` to appl
|
|
|
524
500
|
```typescript
|
|
525
501
|
const css: CssProps = {
|
|
526
502
|
// Target the specific system icon class you wish to override
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
maskImage: `url("${DemoIcons[
|
|
503
|
+
'.ifc-icon.ma-close': {
|
|
504
|
+
'-webkit-mask-image': `url("${DemoIcons['ma-close']}")`,
|
|
505
|
+
maskImage: `url("${DemoIcons['ma-close']}")`,
|
|
530
506
|
// If needed, specify mask sizing properties:
|
|
531
507
|
// maskRepeat: 'no-repeat',
|
|
532
508
|
// maskPosition: 'center',
|
|
@@ -623,12 +599,12 @@ For interactive lists, `createDragUtil()` from `lupine.components` handles compl
|
|
|
623
599
|
1. **Option Selection (`ActionSheetSelectPromise`)** (Replaces `confirm()` or complex choices):
|
|
624
600
|
|
|
625
601
|
```typescript
|
|
626
|
-
import { ActionSheetSelectPromise } from
|
|
602
|
+
import { ActionSheetSelectPromise } from 'lupine.components';
|
|
627
603
|
|
|
628
604
|
const index = await ActionSheetSelectPromise({
|
|
629
|
-
title:
|
|
630
|
-
options: [
|
|
631
|
-
cancelButtonText:
|
|
605
|
+
title: 'Delete this saved game?', // Optional
|
|
606
|
+
options: ['Delete', 'Edit'],
|
|
607
|
+
cancelButtonText: 'Cancel',
|
|
632
608
|
});
|
|
633
609
|
|
|
634
610
|
if (index === 0) {
|
|
@@ -642,25 +618,25 @@ For interactive lists, `createDragUtil()` from `lupine.components` handles compl
|
|
|
642
618
|
2. **Simple Messages (`ActionSheetMessagePromise`)** (Replaces `alert()`):
|
|
643
619
|
|
|
644
620
|
```typescript
|
|
645
|
-
import { ActionSheetMessagePromise } from
|
|
621
|
+
import { ActionSheetMessagePromise } from 'lupine.components';
|
|
646
622
|
|
|
647
623
|
await ActionSheetMessagePromise({
|
|
648
|
-
title:
|
|
649
|
-
message:
|
|
650
|
-
closeButtonText:
|
|
624
|
+
title: 'Success', // Optional
|
|
625
|
+
message: 'Your profile has been saved.',
|
|
626
|
+
closeButtonText: 'OK', // Optional, defaults to a close behavior
|
|
651
627
|
});
|
|
652
628
|
```
|
|
653
629
|
|
|
654
630
|
3. **User Input (`ActionSheetInputPromise`)** (Replaces `prompt()`):
|
|
655
631
|
|
|
656
632
|
```typescript
|
|
657
|
-
import { ActionSheetInputPromise } from
|
|
633
|
+
import { ActionSheetInputPromise } from 'lupine.components';
|
|
658
634
|
|
|
659
635
|
const value = await ActionSheetInputPromise({
|
|
660
|
-
title:
|
|
636
|
+
title: 'Enter your name',
|
|
661
637
|
// placeholder: 'Player 1', // Optional
|
|
662
|
-
confirmButtonText:
|
|
663
|
-
cancelButtonText:
|
|
638
|
+
confirmButtonText: 'Submit', // Optional
|
|
639
|
+
cancelButtonText: 'Cancel', // Optional
|
|
664
640
|
});
|
|
665
641
|
|
|
666
642
|
if (value !== null) {
|
|
@@ -680,12 +656,12 @@ When building mobile interfaces, users expect the physical hardware "Back" butto
|
|
|
680
656
|
**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`.
|
|
681
657
|
|
|
682
658
|
```typescript
|
|
683
|
-
import { backActionHelper } from
|
|
659
|
+
import { backActionHelper } from 'lupine.components';
|
|
684
660
|
|
|
685
661
|
export const MyCloseButton = ({ onClose }) => {
|
|
686
662
|
return (
|
|
687
663
|
<i
|
|
688
|
-
class=
|
|
664
|
+
class='ifc-icon ma-close'
|
|
689
665
|
// Generate a unique ID for the back stack
|
|
690
666
|
data-back-action={backActionHelper.genBackActionId()}
|
|
691
667
|
onClick={onClose}
|