create-lupine 1.0.13 → 1.0.14
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 +229 -45
- package/templates/common/dev/cp-folder.js +2 -6
- package/templates/cv-starter/web/src/index.html +1 -1
- package/templates/doc-starter/web/src/index.html +1 -1
- package/templates/hello-world/web/src/index.html +1 -1
- package/templates/hello-world/web/src/styles/global.css +63 -0
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
- **Pattern**: `const dom = new HtmlVar(initialContent);` -> JSX `{dom.node}` -> `dom.value = updatedContent`.
|
|
18
18
|
- **Direct DOM Access**:
|
|
19
19
|
- Use `RefProps` to get reference to the component root.
|
|
20
|
-
- Use `ref.$(selector)` to find elements (inputs, containers).
|
|
20
|
+
- Use `ref.$(selector)` to find the first element, `ref.$all(selector)` to find all elements (inputs, containers).
|
|
21
21
|
- **Value Retrieval**: `const val = ref.$('input.my-class').value`.
|
|
22
22
|
|
|
23
23
|
## 2. Key Interfaces
|
|
@@ -46,13 +46,28 @@ Supports nesting and media queries. **Prefer this over inline styles.** Define y
|
|
|
46
46
|
|
|
47
47
|
## 3. Styles & Themes ("The Look")
|
|
48
48
|
|
|
49
|
-
### Global Variables (Theming)
|
|
50
|
-
|
|
51
|
-
**NEVER hardcode colors** (e.g., `#000`). Always use CSS variables to support Dark/Light modes.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
### Global Variables (Theming) & Dark Mode Compatibility
|
|
50
|
+
|
|
51
|
+
**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)`.
|
|
52
|
+
|
|
53
|
+
#### 🎨 Color Variable Semantics (CRITICAL FOR DARK MODE)
|
|
54
|
+
|
|
55
|
+
1. **Backgrounds (`--primary-bg-color` vs `--secondary-bg-color`)**:
|
|
56
|
+
- `--primary-bg-color`: The lowest-level background (White in light mode, **Deep Black** in dark mode).
|
|
57
|
+
- `--secondary-bg-color`: An elevated background (Light gray in light mode, **Lighter Black/Gray** in dark mode).
|
|
58
|
+
- _⚠️ 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.
|
|
59
|
+
2. **Text Colors (`--primary-color` vs `--secondary-color`)**:
|
|
60
|
+
- `--primary-color`: The primary **TEXT** color. (Dark grey/black in light mode, **White** in dark mode).
|
|
61
|
+
- _⚠️ Dark Mode Trap_: **Never** use `--primary-color` as the background color for a blue "Primary Action Button". It will turn white in dark mode.
|
|
62
|
+
- Always explicitly declare `color: 'var(--primary-color, inherit)'` on cards/containers so child text properly flips white in dark mode.
|
|
63
|
+
3. **Action / Brand Colors (`--primary-accent-color`)**:
|
|
64
|
+
- `--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.
|
|
65
|
+
- When using this as a background, set the text color to `var(--primary-bg-color)` so it stays high-contrast (white) in both themes.
|
|
66
|
+
4. **Borders (`--primary-border` / `--secondary-border-color`)**:
|
|
67
|
+
- Replace all hardcoded `#eee`, `#ccc`, `#999` borders with these to ensure they darken appropriately in dark mode.
|
|
68
|
+
5. **Status Colors**:
|
|
69
|
+
- `--success-color`, `--warning-color`, `--error-color`, `--success-bg-color` (Use replacing hardcoded green/reds).
|
|
70
|
+
6. **Spacing**: `var(--space-m)` (8px), `var(--space-l)` (16px).
|
|
56
71
|
|
|
57
72
|
### Standard Utility Classes
|
|
58
73
|
|
|
@@ -60,7 +75,7 @@ Supports nesting and media queries. **Prefer this over inline styles.** Define y
|
|
|
60
75
|
- **Margins/Padding**: `m-auto`, `p-m`, `mt-s`, `pb-l` (s=small, m=medium, l=large).
|
|
61
76
|
- **Text**: `.text-center`, `.ellipsis`.
|
|
62
77
|
|
|
63
|
-
### The Component CSS & Ampersand (`&`) Pattern
|
|
78
|
+
### The Component CSS & Ampersand (`&`) Pattern (must go with RefProps)
|
|
64
79
|
|
|
65
80
|
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**.
|
|
66
81
|
|
|
@@ -71,7 +86,7 @@ export const MyComponent = () => {
|
|
|
71
86
|
const ref: RefProps = {
|
|
72
87
|
onLoad: async () => {
|
|
73
88
|
// 3. Querying namespaced elements
|
|
74
|
-
const btn = ref.$('
|
|
89
|
+
const btn = ref.$('.&-btn');
|
|
75
90
|
btn.innerHTML = 'Ready';
|
|
76
91
|
},
|
|
77
92
|
};
|
|
@@ -101,11 +116,15 @@ export const MyComponent = () => {
|
|
|
101
116
|
};
|
|
102
117
|
```
|
|
103
118
|
|
|
104
|
-
**Key Takeaways for
|
|
119
|
+
**Key Takeaways for `&` and `RefProps`**:
|
|
105
120
|
|
|
106
|
-
1. **In `CssProps
|
|
121
|
+
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`.
|
|
122
|
+
- `'&.active'` applies to the root element when it has the `.active` native class.
|
|
123
|
+
- `'.&-item'` applies to _descendant_ elements that have `class="&-item"`.
|
|
107
124
|
2. **In JSX `class` attributes**: Add `class="&-item"`. You can still mix native classes: `class="row-box &-item"`.
|
|
108
|
-
3. **In
|
|
125
|
+
3. **In DOM Queries**:
|
|
126
|
+
- Use **`ref.$('.&-item')`** (WITH leading dot) to get the first matching element. The underlying logic simply replaces `&` with the generated ID (e.g. `l1234`), so this correctly translates to querying `.l1234 .l1234-item` which safely finds descendants within the current component's isolated namespace.
|
|
127
|
+
- Use **`ref.$all('.&-item')`** to get a `NodeList` of all matching descendants within the component.
|
|
109
128
|
|
|
110
129
|
## 4. CSS Placement Strategies: `css={}` vs `bindGlobalStyle`
|
|
111
130
|
|
|
@@ -122,12 +141,9 @@ When you pass `css={css}` to a JSX element, Lupine automatically evaluates it an
|
|
|
122
141
|
|
|
123
142
|
```typescript
|
|
124
143
|
export const MyUniquePage = () => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
...
|
|
129
|
-
</div>
|
|
130
|
-
);
|
|
144
|
+
// Styles apply directly to the root element
|
|
145
|
+
const css = { padding: '10px' };
|
|
146
|
+
return <div css={css}>...</div>;
|
|
131
147
|
};
|
|
132
148
|
```
|
|
133
149
|
|
|
@@ -139,7 +155,7 @@ export const MyUniquePage = () => {
|
|
|
139
155
|
|
|
140
156
|
**How it works seamlessly with `&`**:
|
|
141
157
|
|
|
142
|
-
1. You generate a unique ID based on the `CssProps` content (this ID remains identical for every instance of the component): `const globalCssId = getGlobalStylesId(css)
|
|
158
|
+
1. You generate a unique ID based on the `CssProps` content (this ID remains identical for every instance of the component): `const globalCssId = getGlobalStylesId(css);`. **CRITICAL**: This MUST be called _inside_ the component function body, not at the module root level, because it depends on the rendering context (themes, SSR scope) being fully initialized.
|
|
143
159
|
2. You bind the style block globally once: `bindGlobalStyle(globalCssId, css);`
|
|
144
160
|
3. You assign this ID to the component's `ref` so Lupine knows what to replace `&` with: `const ref: RefProps = { globalCssId };`
|
|
145
161
|
4. Use `class="&-item"` normally. Lupine replaces `&` with the identical `globalCssId` across all instances!
|
|
@@ -150,38 +166,69 @@ export const MyUniquePage = () => {
|
|
|
150
166
|
```typescript
|
|
151
167
|
import { bindGlobalStyle, getGlobalStylesId, CssProps, RefProps } from 'lupine.web';
|
|
152
168
|
|
|
153
|
-
export const ToggleButton = (props: { color?: string }) => {
|
|
169
|
+
export const ToggleButton = (props: { color?: string; disabled?: boolean }) => {
|
|
154
170
|
const css: CssProps = {
|
|
155
|
-
// 1.
|
|
156
|
-
'.&-container'
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
171
|
+
// 1. Top-level properties apply directly to the root element.
|
|
172
|
+
// Do NOT wrap these in '.&-container'
|
|
173
|
+
padding: '10px',
|
|
174
|
+
color: 'var(--primary-color)',
|
|
175
|
+
|
|
176
|
+
// 2. You can mix nested modifiers for the root element:
|
|
177
|
+
'&.disabled': { opacity: '0.5' },
|
|
178
|
+
|
|
179
|
+
// 3. Or target specific inner children:
|
|
180
|
+
'.&-inner': { fontWeight: 'bold' },
|
|
160
181
|
};
|
|
161
182
|
|
|
162
|
-
//
|
|
183
|
+
// 4. Generate the ID and bind it globally (Call INSIDE the component!)
|
|
163
184
|
const tabGlobalCssId = getGlobalStylesId(css);
|
|
164
185
|
bindGlobalStyle(tabGlobalCssId, css);
|
|
165
186
|
|
|
166
|
-
//
|
|
187
|
+
// 5. Assign the global ID to the reference
|
|
167
188
|
const ref: RefProps = {
|
|
168
189
|
globalCssId: tabGlobalCssId,
|
|
169
190
|
};
|
|
170
191
|
|
|
171
192
|
return (
|
|
172
|
-
<div
|
|
173
|
-
|
|
174
|
-
class='&-container'
|
|
175
|
-
ref={ref}
|
|
176
|
-
// Handle instance-specific differences with inline style!
|
|
177
|
-
style={{ backgroundColor: props.color }}
|
|
178
|
-
>
|
|
179
|
-
Click Me
|
|
193
|
+
<div ref={ref} class={props.disabled ? 'disabled' : ''} style={{ backgroundColor: props.color }}>
|
|
194
|
+
<span class='&-inner'>Click Me</span>
|
|
180
195
|
</div>
|
|
181
196
|
);
|
|
182
197
|
};
|
|
183
198
|
```
|
|
184
199
|
|
|
200
|
+
### ⚠️ IMPORTANT: Hardcoded Namespace IDs vs `getGlobalStylesId`
|
|
201
|
+
|
|
202
|
+
While using `getGlobalStylesId` combined with the `&` pattern is the most robust approach, you may sometimes use a hardcoded string as the namespace ID: `bindGlobalStyle('my-custom-component', css)`.
|
|
203
|
+
|
|
204
|
+
If you do this, Lupine will generate CSS targeting `.my-custom-component`. Therefore, **you MUST ensure that your root JSX element explicitly includes this exact class name**, otherwise the top-level CSS properties will fail to apply.
|
|
205
|
+
|
|
206
|
+
**CORRECT Example (Hardcoded ID):**
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
const css: CssProps = { padding: '10px' };
|
|
210
|
+
// ID is 'my-custom-component'
|
|
211
|
+
bindGlobalStyle('my-custom-component', css);
|
|
212
|
+
|
|
213
|
+
// Root element MUST have class='my-custom-component'
|
|
214
|
+
return <div class='my-custom-component'>...</div>;
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### ⚠️ IMPORTANT: The "Static `CssProps`" Rule
|
|
218
|
+
|
|
219
|
+
Because `bindGlobalStyle(getGlobalStylesId(css), css)` injects your `<style>` tags into the `<head>` of the document **globally**, your `CssProps` definition **MUST** be entirely static and shared across all instances of a component.
|
|
220
|
+
|
|
221
|
+
**ANTI-PATTERN:** Putting React variables (like `isVertical`, `size`, `color`) directly inside the `CssProps` object structure.
|
|
222
|
+
|
|
223
|
+
- **Why it's bad:** Every time the component re-renders with a different prop, `getGlobalStylesId` calculates a brand new hash. `bindGlobalStyle` then injects a duplicate `<style>` block into the head. If a user places two `Slider`s on the same page (one vertical, one horizontal), they will generate conflicting CSS IDs and the latter will aggressively overwrite the former's styles, corrupting the layout.
|
|
224
|
+
|
|
225
|
+
**CORRECT PATTERN:** Define one immutable `const css: CssProps = {...}` outside of any reactive dependencies.
|
|
226
|
+
|
|
227
|
+
- Handle visual variations by appending standard class names to your root element (`class={isVertical ? 'vertical' : 'horizontal'}`).
|
|
228
|
+
- In your static CSS definition, target these root modifier classes by prefixing the namespace `&` with the modifier: `&.vertical .&-track` or `&.horizontal .&-fill`.
|
|
229
|
+
- If you need precise, dynamic position coordinates (e.g. mouse tracking, progress percentages), calculate them in your JavaScript hook and inject them into standard DOM elements using `el.style.setProperty('--my-var', val)` or direct assignment, while your CSS relies purely on standard layout rules.
|
|
230
|
+
- Since css supports nested selectors (CSS-in-JS), the efficient approach is to define all CSS in one css prop on the top-level/root tag of a component, using class names on children. Avoid spreading inline css props across child elements.
|
|
231
|
+
|
|
185
232
|
---
|
|
186
233
|
|
|
187
234
|
## 5. Common Patterns ("The Lupine Way")
|
|
@@ -213,7 +260,7 @@ const MyPage = () => {
|
|
|
213
260
|
// 4. Events
|
|
214
261
|
const onSearch = async () => {
|
|
215
262
|
// Read directly from DOM
|
|
216
|
-
const query = ref.$('input
|
|
263
|
+
const query = ref.$('input.&-search').value;
|
|
217
264
|
// Update logic var
|
|
218
265
|
pageIndex = 0;
|
|
219
266
|
// Update UI manually
|
|
@@ -228,7 +275,7 @@ const MyPage = () => {
|
|
|
228
275
|
|
|
229
276
|
return (
|
|
230
277
|
<div ref={ref}>
|
|
231
|
-
<input class='search' />
|
|
278
|
+
<input class='&-search' />
|
|
232
279
|
<button onClick={onSearch}>Go</button>
|
|
233
280
|
{/* Embed Dynamic Content */}
|
|
234
281
|
{listDom.node}
|
|
@@ -239,33 +286,47 @@ const MyPage = () => {
|
|
|
239
286
|
|
|
240
287
|
### Mobile Navigation (`SliderFrame`)
|
|
241
288
|
|
|
242
|
-
Lupine uses a "Slide-over" model for navigation (Drill-down).
|
|
289
|
+
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.
|
|
243
290
|
|
|
244
291
|
```typescript
|
|
245
292
|
import { SliderFrame, SliderFrameHookProps, HeaderWithBackFrame } from 'lupine.components';
|
|
246
293
|
|
|
247
|
-
// Parent Component
|
|
294
|
+
// 1. Parent Component (or Level 1)
|
|
248
295
|
const Parent = () => {
|
|
296
|
+
// Define hook for Level 2
|
|
249
297
|
const sliderHook: SliderFrameHookProps = {};
|
|
250
298
|
|
|
251
299
|
const openDetail = (id) => {
|
|
252
300
|
// Push new view onto stack
|
|
253
|
-
sliderHook.load!(<DetailComponent id={id}
|
|
301
|
+
sliderHook.load!(<DetailComponent id={id} parentSliderFrameHook={sliderHook} />);
|
|
254
302
|
};
|
|
255
303
|
|
|
256
304
|
return (
|
|
257
305
|
<div>
|
|
306
|
+
{/* Placeholder for Level 2 */}
|
|
258
307
|
<SliderFrame hook={sliderHook} />
|
|
308
|
+
|
|
259
309
|
<div onClick={() => openDetail(1)}>Click Me</div>
|
|
260
310
|
</div>
|
|
261
311
|
);
|
|
262
312
|
};
|
|
263
313
|
|
|
264
|
-
// Child Component
|
|
265
|
-
const DetailComponent = (props) => {
|
|
314
|
+
// 2. Child Component (Level 2)
|
|
315
|
+
const DetailComponent = (props: { id: number; parentSliderFrameHook: SliderFrameHookProps }) => {
|
|
316
|
+
// Define hook for Level 3
|
|
317
|
+
const childSliderHook: SliderFrameHookProps = {};
|
|
318
|
+
|
|
319
|
+
const openDeeper = () => {
|
|
320
|
+
// Load Level 3 component into this component's placeholder
|
|
321
|
+
childSliderHook.load!(<DetailComponent id={props.id + 1} parentSliderFrameHook={childSliderHook} />);
|
|
322
|
+
};
|
|
323
|
+
|
|
266
324
|
return (
|
|
267
|
-
<HeaderWithBackFrame title='Detail Page' onBack={(e) => props.
|
|
268
|
-
|
|
325
|
+
<HeaderWithBackFrame title='Detail Page' onBack={(e) => props.parentSliderFrameHook.close!(e)}>
|
|
326
|
+
{/* Placeholder for Level 3 */}
|
|
327
|
+
<SliderFrame hook={childSliderHook} />
|
|
328
|
+
|
|
329
|
+
<div onClick={openDeeper}>Go Deeper</div>
|
|
269
330
|
</HeaderWithBackFrame>
|
|
270
331
|
);
|
|
271
332
|
};
|
|
@@ -326,3 +387,126 @@ const Parent = () => {
|
|
|
326
387
|
- **⚠️ `style={{}}`**: **Allowed** for simple or dynamic inline styles (e.g., `style={{ border: '1px solid red' }}`), but **prefer `css={CssProps}`** for structural/theme styling.
|
|
327
388
|
- **✅ 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)}`).
|
|
328
389
|
- **✅ 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".
|
|
390
|
+
|
|
391
|
+
## 7. System Icons & Customization
|
|
392
|
+
|
|
393
|
+
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`).
|
|
394
|
+
|
|
395
|
+
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.
|
|
396
|
+
|
|
397
|
+
### Overriding System Icons without Generating a Font
|
|
398
|
+
|
|
399
|
+
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.
|
|
400
|
+
|
|
401
|
+
**1. Define your SVG Data URL:**
|
|
402
|
+
You can import an `.svg` file (if your bundler supports it) or define a raw Data URI string.
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
// Option A: Using bundler import
|
|
406
|
+
import githubIcon from 'github.svg';
|
|
407
|
+
|
|
408
|
+
// Option B: Raw Data URI string
|
|
409
|
+
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`;
|
|
410
|
+
|
|
411
|
+
export const DemoIcons = {
|
|
412
|
+
github: githubIcon,
|
|
413
|
+
'ma-close': closeSvgData,
|
|
414
|
+
};
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**2. Override the specific `.ifc-icon` via CSS Masking:**
|
|
418
|
+
Use the `maskImage` property wrapped in `url()` to apply the SVG data to the icon class. This ensures it inherits colors (`currentColor`) properly.
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
const css: CssProps = {
|
|
422
|
+
// Target the specific system icon class you wish to override
|
|
423
|
+
'.ifc-icon.ma-close': {
|
|
424
|
+
maskImage: `url("${DemoIcons['ma-close']}")`,
|
|
425
|
+
// If needed, specify mask sizing properties:
|
|
426
|
+
// maskRepeat: 'no-repeat',
|
|
427
|
+
// maskPosition: 'center',
|
|
428
|
+
// maskSize: 'contain',
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## 8. Cross-Platform App Bootstrapping Guidance
|
|
434
|
+
|
|
435
|
+
When creating a new Cross-Platform App using `lupine.js`, follow this standard procedure for scaffolding the entry point, navigation, and icons:
|
|
436
|
+
|
|
437
|
+
1. **Custom Navigation Icons (`app-icons.ts`)**:
|
|
438
|
+
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 `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.
|
|
439
|
+
|
|
440
|
+
2. **Base Styles (`base-css.ts`)**:
|
|
441
|
+
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.
|
|
442
|
+
|
|
443
|
+
3. **Global UI Frame (`app-responsive-frame.tsx`)**:
|
|
444
|
+
Use `ResponsiveFrame` along with `SliderFrame` (for drill-down navigation via `SliderFrameHookProps`) to define the app's skeleton.
|
|
445
|
+
|
|
446
|
+
- Define your top/bottom navigation menus based on your icons.
|
|
447
|
+
- Return `ResponsiveFrame` passing in the `mainContent`, menus, and ensure you provide all required properties like `mobileSideMenuContent: <></>` (even if empty) to satisfy TypeScript interfaces.
|
|
448
|
+
|
|
449
|
+
4. **Page Router Configuration (`index.tsx`)**:
|
|
450
|
+
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)`.
|
|
451
|
+
|
|
452
|
+
5. **Local Storage Patterns**:
|
|
453
|
+
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.
|
|
454
|
+
|
|
455
|
+
## 9. Standard Mobile App Layout & Interactions
|
|
456
|
+
|
|
457
|
+
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.
|
|
458
|
+
|
|
459
|
+
### A. The Global Root (`index.tsx` & `AppResponsiveFrame`)
|
|
460
|
+
|
|
461
|
+
- Use `bindTheme` to load global color tokens.
|
|
462
|
+
- Set the global frame with `pageRouter.setFramePage({ component: AppResponsiveFrame, placeholderClassname: 'user-page-placeholder' })`.
|
|
463
|
+
- **`AppResponsiveFrame`** handles the macro layout:
|
|
464
|
+
- It renders `<ResponsiveFrame>` wrapping `<main class='user-page-placeholder'></main>`.
|
|
465
|
+
- It contains the global left `mobileSideMenuContent` (typically abstracted into a `<SideMenuContent />` component).
|
|
466
|
+
|
|
467
|
+
### B. The Home / List Page (`HomePage`)
|
|
468
|
+
|
|
469
|
+
A standard mobile list page must employ:
|
|
470
|
+
|
|
471
|
+
1. **The Top Header (`MobileHeaderCenter`)**:
|
|
472
|
+
|
|
473
|
+
- Wrap the top bar in `<MobileHeaderCenter>`.
|
|
474
|
+
- Use `<MobileHeaderTitleIcon title='App Name' left={...} right={...} />`.
|
|
475
|
+
- The _left_ slot usually contains an empty spacer `<MobileHeaderEmptyIcon />`.
|
|
476
|
+
- The _right_ slot contains actions (e.g., Search icon, `<MobileTopSysIcon />` to open the Side Menu).
|
|
477
|
+
|
|
478
|
+
2. **The Scrollable Content Area**:
|
|
479
|
+
|
|
480
|
+
- Beneath the header, create a flex-grow scrollable div: `<div class='flex-1 overflow-y-auto padding-m'>`.
|
|
481
|
+
- Mount an `HtmlVar` instance here (`{dom.node}`) to dynamically bind the list data arrays fetched typically via `RefProps.onLoad`.
|
|
482
|
+
|
|
483
|
+
3. **Floating Action Button (FAB)**:
|
|
484
|
+
- Overlay a primary action button at `bottom: 24px`, `right: 24px` using `.fab-button` styled with `var(--primary-accent-color)`.
|
|
485
|
+
|
|
486
|
+
### C. Advanced Touch Interactions (`createDragUtil` in Lists)
|
|
487
|
+
|
|
488
|
+
For interactive lists, `createDragUtil()` from `lupine.components` handles complex gesture physics.
|
|
489
|
+
|
|
490
|
+
- **Swipe-to-Reveal (Horizontal)**:
|
|
491
|
+
|
|
492
|
+
- Render an absolute positioned `.actions-layer` (opacity: 0 initially) underneath the `.list-card`.
|
|
493
|
+
- When the card's `onTouchStart`/`onMouseDown` is triggered, attach `dragUtil` handlers.
|
|
494
|
+
- 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.
|
|
495
|
+
- 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).
|
|
496
|
+
|
|
497
|
+
- **Drag-to-Reorder (Vertical)**:
|
|
498
|
+
- Define a distinct `.drag-handle` slot inside the card (e.g., `bs-list` icon).
|
|
499
|
+
- 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.
|
|
500
|
+
- Conclude by saving the new DOM sibling ordering in `dragUtil.setOnMoveEndCallback`.
|
|
501
|
+
|
|
502
|
+
### D. Sub-Page Routing & Drill-Downs (`SliderFrame`)
|
|
503
|
+
|
|
504
|
+
- Slide-over interactions are mandatory for Search panels, Creation modals, and Details views.
|
|
505
|
+
- The `HomePage` must define a top-level `<SliderFrame hook={sliderFrameHook} />` inside its scroll area.
|
|
506
|
+
- Opening a child acts instantly via: `sliderFrameHook.load!(<MyChildPage sliderFrameHook={sliderFrameHook} />)`.
|
|
507
|
+
- **Inside the Child Component**:
|
|
508
|
+
- Must be wrapped with `<HeaderWithBackFrame title='Subpage' onBack={(e) => props.sliderFrameHook.close!(e)}>` to provide the standard top-left back chevron.
|
|
509
|
+
- **Nested SliderFrames**:
|
|
510
|
+
- 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.
|
|
511
|
+
- Using the parent's hook will replace the parent's content instead of sliding a new frame over it.
|
|
512
|
+
- Wrap multiple children in `<></>` or a `<div>` if they are direct children to satisfy single `VNode` rendering constraints.
|
|
@@ -5,18 +5,14 @@ function copyRecursiveSync(src, dest) {
|
|
|
5
5
|
const stats = fs.statSync(src);
|
|
6
6
|
const isDirectory = stats.isDirectory();
|
|
7
7
|
if (isDirectory) {
|
|
8
|
-
|
|
9
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
10
|
-
}
|
|
8
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
11
9
|
fs.readdirSync(src).forEach((childItemName) => {
|
|
12
10
|
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName));
|
|
13
11
|
});
|
|
14
12
|
} else {
|
|
15
13
|
// Ensure parent directory exists for file copy (just in case)
|
|
16
14
|
const destDir = path.dirname(dest);
|
|
17
|
-
|
|
18
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
19
|
-
}
|
|
15
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
20
16
|
fs.copyFileSync(src, dest);
|
|
21
17
|
}
|
|
22
18
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<title><!--META-TITLE--></title>
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
7
7
|
<!--META-ENV-START-->
|
|
8
8
|
<!--META-ENV-END-->
|
|
9
9
|
<link rel="shortcut icon" href="{SUBDIR}/assets/favicon.ico?t={hash}" type="image/x-icon" />
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<title><!--META-TITLE--></title>
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no" />
|
|
7
7
|
<!--META-ENV-START-->
|
|
8
8
|
<!--META-ENV-END-->
|
|
9
9
|
<link rel="shortcut icon" href="{SUBDIR}/assets/favicon.ico?t={hash}" type="image/x-icon" />
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<title><!--META-TITLE--></title>
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no" />
|
|
7
7
|
<!--META-ENV-START-->
|
|
8
8
|
<!--META-ENV-END-->
|
|
9
9
|
<link rel="shortcut icon" href="{SUBDIR}/assets/favicon.ico?t={hash}" type="image/x-icon" />
|
|
@@ -785,6 +785,66 @@ body {
|
|
|
785
785
|
font-weight: var(--font-weight-base);
|
|
786
786
|
}
|
|
787
787
|
|
|
788
|
+
.safe-area-pt {
|
|
789
|
+
padding-top: constant(safe-area-inset-top);
|
|
790
|
+
padding-top: env(safe-area-inset-top);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.safe-area-pb {
|
|
794
|
+
padding-bottom: constant(safe-area-inset-bottom);
|
|
795
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.safe-area-pl {
|
|
799
|
+
padding-left: constant(safe-area-inset-left);
|
|
800
|
+
padding-left: env(safe-area-inset-left);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.safe-area-pr {
|
|
804
|
+
padding-right: constant(safe-area-inset-right);
|
|
805
|
+
padding-right: env(safe-area-inset-right);
|
|
806
|
+
}
|
|
807
|
+
.safe-area-pall {
|
|
808
|
+
padding-top: constant(safe-area-inset-top);
|
|
809
|
+
padding-top: env(safe-area-inset-top);
|
|
810
|
+
padding-bottom: constant(safe-area-inset-bottom);
|
|
811
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
812
|
+
padding-left: constant(safe-area-inset-left);
|
|
813
|
+
padding-left: env(safe-area-inset-left);
|
|
814
|
+
padding-right: constant(safe-area-inset-right);
|
|
815
|
+
padding-right: env(safe-area-inset-right);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.safe-area-mt {
|
|
819
|
+
margin-top: constant(safe-area-inset-top);
|
|
820
|
+
margin-top: env(safe-area-inset-top);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.safe-area-mb {
|
|
824
|
+
margin-bottom: constant(safe-area-inset-bottom);
|
|
825
|
+
margin-bottom: env(safe-area-inset-bottom);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.safe-area-ml {
|
|
829
|
+
margin-left: constant(safe-area-inset-left);
|
|
830
|
+
margin-left: env(safe-area-inset-left);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
.safe-area-mr {
|
|
834
|
+
margin-right: constant(safe-area-inset-right);
|
|
835
|
+
margin-right: env(safe-area-inset-right);
|
|
836
|
+
}
|
|
837
|
+
.safe-area-mall {
|
|
838
|
+
margin-top: constant(safe-area-inset-top);
|
|
839
|
+
margin-top: env(safe-area-inset-top);
|
|
840
|
+
margin-bottom: constant(safe-area-inset-bottom);
|
|
841
|
+
margin-bottom: env(safe-area-inset-bottom);
|
|
842
|
+
margin-left: constant(safe-area-inset-left);
|
|
843
|
+
margin-left: env(safe-area-inset-left);
|
|
844
|
+
margin-right: constant(safe-area-inset-right);
|
|
845
|
+
margin-right: env(safe-area-inset-right);
|
|
846
|
+
}
|
|
847
|
+
|
|
788
848
|
input.base-css[type='text'],
|
|
789
849
|
input.base-css[type='password'],
|
|
790
850
|
input.base-css[type='email'],
|
|
@@ -888,6 +948,9 @@ input.base-css[type='text'][disabled='true']::placeholder {
|
|
|
888
948
|
}
|
|
889
949
|
|
|
890
950
|
.button-base {
|
|
951
|
+
display: flex;
|
|
952
|
+
align-items: center;
|
|
953
|
+
justify-content: center;
|
|
891
954
|
line-height: 1.1;
|
|
892
955
|
height: var(--button-height);
|
|
893
956
|
cursor: pointer;
|