create-lupine 1.0.13 → 1.0.15

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.
Files changed (42) hide show
  1. package/index.js +32 -2
  2. package/package.json +1 -1
  3. package/templates/common/AI_CONTEXT.md +245 -50
  4. package/templates/common/dev/cp-folder.js +2 -6
  5. package/templates/cv-starter/web/src/index.html +1 -1
  6. package/templates/doc-starter/web/src/index.html +1 -1
  7. package/templates/hello-world/web/src/index.html +1 -1
  8. package/templates/hello-world/web/src/index.tsx +54 -15
  9. package/templates/hello-world/web/src/styles/global.css +63 -0
  10. package/templates/responsive-starter/api/package.json +6 -0
  11. package/templates/responsive-starter/api/resources/config_default.json +6 -0
  12. package/templates/responsive-starter/api/resources/install.sqlite.sql +4 -0
  13. package/templates/responsive-starter/api/src/index.ts +4 -0
  14. package/templates/responsive-starter/api/src/service/root-api.ts +18 -0
  15. package/templates/responsive-starter/lupine.json +23 -0
  16. package/templates/responsive-starter/web/assets/favicon.ico +0 -0
  17. package/templates/responsive-starter/web/package.json +6 -0
  18. package/templates/responsive-starter/web/src/app-icons.ts +72 -0
  19. package/templates/responsive-starter/web/src/components/input-history-component.tsx +93 -0
  20. package/templates/responsive-starter/web/src/components/mine-about-page.tsx +86 -0
  21. package/templates/responsive-starter/web/src/components/mine-premium-page.tsx +80 -0
  22. package/templates/responsive-starter/web/src/components/mine-settings-page.tsx +219 -0
  23. package/templates/responsive-starter/web/src/components/mobile-top-search-menu.tsx +119 -0
  24. package/templates/responsive-starter/web/src/components/note-detail.tsx +116 -0
  25. package/templates/responsive-starter/web/src/components/note-edit.tsx +154 -0
  26. package/templates/responsive-starter/web/src/components/note-search-page.tsx +193 -0
  27. package/templates/responsive-starter/web/src/components/search-input.tsx +95 -0
  28. package/templates/responsive-starter/web/src/components/side-menu-content.tsx +178 -0
  29. package/templates/responsive-starter/web/src/frames/app-responsive-frame.tsx +46 -0
  30. package/templates/responsive-starter/web/src/index.html +16 -0
  31. package/templates/responsive-starter/web/src/index.tsx +42 -0
  32. package/templates/responsive-starter/web/src/pages/about-page.tsx +43 -0
  33. package/templates/responsive-starter/web/src/pages/finance-page.tsx +46 -0
  34. package/templates/responsive-starter/web/src/pages/home-page.tsx +452 -0
  35. package/templates/responsive-starter/web/src/pages/mine-page.tsx +325 -0
  36. package/templates/responsive-starter/web/src/pages/tools-page.tsx +46 -0
  37. package/templates/responsive-starter/web/src/services/local-notes-service.ts +87 -0
  38. package/templates/responsive-starter/web/src/services/mine-service.ts +45 -0
  39. package/templates/responsive-starter/web/src/styles/app.css +0 -0
  40. package/templates/responsive-starter/web/src/styles/base-css.ts +65 -0
  41. package/templates/responsive-starter/web/src/styles/global.css +2143 -0
  42. package/templates/responsive-starter/web/src/styles/theme.ts +16 -0
package/index.js CHANGED
@@ -72,6 +72,22 @@ const TEMPLATES = [
72
72
  color: green,
73
73
  needsPress: true,
74
74
  },
75
+ {
76
+ name: 'responsive-starter-tabs',
77
+ dir: 'responsive-starter',
78
+ layout: 'tabs',
79
+ display: 'Responsive Starter (tabs)',
80
+ itemType: 'frontend',
81
+ color: green,
82
+ },
83
+ {
84
+ name: 'responsive-starter-sidemenu',
85
+ dir: 'responsive-starter',
86
+ layout: 'sidemenu',
87
+ display: 'Responsive Starter (sidemenu)',
88
+ itemType: 'frontend',
89
+ color: green,
90
+ },
75
91
  ];
76
92
 
77
93
  async function init() {
@@ -136,7 +152,10 @@ async function init() {
136
152
  template = selected.name;
137
153
  }
138
154
 
139
- const templateDir = path.resolve(fileURLToPath(import.meta.url), '../templates', template);
155
+ const templateObj = TEMPLATES.find((t) => t.name === template);
156
+ const templateDirName = templateObj?.dir || template;
157
+
158
+ const templateDir = path.resolve(fileURLToPath(import.meta.url), '../templates', templateDirName);
140
159
  const commonDir = path.resolve(fileURLToPath(import.meta.url), '../templates', 'common');
141
160
 
142
161
  // ... (copy logic remains same)
@@ -157,6 +176,18 @@ async function init() {
157
176
 
158
177
  copyDir(templateDir, targetAppDir);
159
178
 
179
+ if (templateObj?.layout) {
180
+ const frameFile = path.join(targetAppDir, 'web/src/frames/app-responsive-frame.tsx');
181
+ if (fs.existsSync(frameFile)) {
182
+ let content = fs.readFileSync(frameFile, 'utf-8');
183
+ content = content.replace(
184
+ /const DEFAULT_LAYOUT = '.*';/,
185
+ `const DEFAULT_LAYOUT = '${templateObj.layout}';`
186
+ );
187
+ fs.writeFileSync(frameFile, content);
188
+ }
189
+ }
190
+
160
191
  changePkgName(path.join(targetAppDir, 'lupine.json'), appName);
161
192
  changePkgName(path.join(targetAppDir, 'api', 'package.json'), appName + '-api');
162
193
  changePkgName(path.join(targetAppDir, 'web', 'package.json'), appName + '-web');
@@ -209,7 +240,6 @@ async function init() {
209
240
  'lupine.web': latestWeb,
210
241
  };
211
242
 
212
- const templateObj = TEMPLATES.find((t) => t.name === template);
213
243
  if (templateObj && templateObj.needsPress) {
214
244
  pkg.dependencies['lupine.press'] = latestPress;
215
245
  pkg.devDependencies['gray-matter'] = '^4.0.3';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lupine",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Scaffolding tool for Lupine.js projects",
5
5
  "bin": {
6
6
  "create-lupine": "index.js"
@@ -4,20 +4,30 @@
4
4
 
5
5
  **🛑 CRITICAL WARNINGS 🛑**
6
6
 
7
- 1. **NO REACT HOOKS**: `useState`, `useEffect`, `useReducer`, `useCallback`, `useContext` **DO NOT EXIST**.
8
- 2. **NO VIRTUAL DOM STATE**: Changing a variable DOES NOT re-render the component. You must manually update `HtmlVar.value`.
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
9
  3. **NO CONTROLLED INPUTS**: Do not bind `value={state}`. Read values from DOM on submit.
10
10
 
11
11
  ---
12
12
 
13
13
  ## 1. Core Philosophy & Reactivity
14
14
 
15
- - **`HtmlVar` is the "State"**:
15
+ - **`useState` React-style local state (small/simple components)**:
16
+ - Import: `import { useState } from 'lupine.components';`
17
+ - Syntax: `const [value, setValue] = useState(initial);` — calling `setValue(...)` rerenders the **entire** component.
18
+ - ✅ **Use when**: The component is small, state drives most of the UI, and the React-style patterns feel natural.
19
+ - ⚠️ **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.
20
+ - **`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.
21
+
22
+ - **`HtmlVar` — Surgical partial updates (large/complex components)**:
16
23
  - Use `HtmlVar` to wrap dynamic sections (lists, conditional renderings, async content).
17
- - **Pattern**: `const dom = new HtmlVar(initialContent);` -> JSX `{dom.node}` -> `dom.value = updatedContent`.
24
+ - **Pattern**: `const dom = new HtmlVar(initialContent);` JSX `{dom.node}` `dom.value = updatedContent`.
25
+ - ✅ **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).
26
+ - The rest of the component DOM is never touched — highly efficient.
27
+
18
28
  - **Direct DOM Access**:
19
29
  - Use `RefProps` to get reference to the component root.
20
- - Use `ref.$(selector)` to find elements (inputs, containers).
30
+ - Use `ref.$(selector)` to find the first element, `ref.$all(selector)` to find all elements (inputs, containers).
21
31
  - **Value Retrieval**: `const val = ref.$('input.my-class').value`.
22
32
 
23
33
  ## 2. Key Interfaces
@@ -46,13 +56,28 @@ Supports nesting and media queries. **Prefer this over inline styles.** Define y
46
56
 
47
57
  ## 3. Styles & Themes ("The Look")
48
58
 
49
- ### Global Variables (Theming)
50
-
51
- **NEVER hardcode colors** (e.g., `#000`). Always use CSS variables to support Dark/Light modes.
52
-
53
- - **Colors**: `var(--primary-color)`, `var(--primary-bg-color)`, `var(--secondary-color)`, `var(--error-color)`.
54
- - **Borders**: `var(--primary-border)`, `var(--border-radius-m)`.
55
- - **Spacing**: `var(--space-m)` (8px), `var(--space-l)` (16px).
59
+ ### Global Variables (Theming) & Dark Mode Compatibility
60
+
61
+ **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)`.
62
+
63
+ #### 🎨 Color Variable Semantics (CRITICAL FOR DARK MODE)
64
+
65
+ 1. **Backgrounds (`--primary-bg-color` vs `--secondary-bg-color`)**:
66
+ - `--primary-bg-color`: The lowest-level background (White in light mode, **Deep Black** in dark mode).
67
+ - `--secondary-bg-color`: An elevated background (Light gray in light mode, **Lighter Black/Gray** in dark mode).
68
+ - _⚠️ 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.
69
+ 2. **Text Colors (`--primary-color` vs `--secondary-color`)**:
70
+ - `--primary-color`: The primary **TEXT** color. (Dark grey/black in light mode, **White** in dark mode).
71
+ - _⚠️ Dark Mode Trap_: **Never** use `--primary-color` as the background color for a blue "Primary Action Button". It will turn white in dark mode.
72
+ - Always explicitly declare `color: 'var(--primary-color, inherit)'` on cards/containers so child text properly flips white in dark mode.
73
+ 3. **Action / Brand Colors (`--primary-accent-color`)**:
74
+ - `--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.
75
+ - When using this as a background, set the text color to `var(--primary-bg-color)` so it stays high-contrast (white) in both themes.
76
+ 4. **Borders (`--primary-border` / `--secondary-border-color`)**:
77
+ - Replace all hardcoded `#eee`, `#ccc`, `#999` borders with these to ensure they darken appropriately in dark mode.
78
+ 5. **Status Colors**:
79
+ - `--success-color`, `--warning-color`, `--error-color`, `--success-bg-color` (Use replacing hardcoded green/reds).
80
+ 6. **Spacing**: `var(--space-m)` (8px), `var(--space-l)` (16px).
56
81
 
57
82
  ### Standard Utility Classes
58
83
 
@@ -60,7 +85,7 @@ Supports nesting and media queries. **Prefer this over inline styles.** Define y
60
85
  - **Margins/Padding**: `m-auto`, `p-m`, `mt-s`, `pb-l` (s=small, m=medium, l=large).
61
86
  - **Text**: `.text-center`, `.ellipsis`.
62
87
 
63
- ### The Component CSS & Ampersand (`&`) Pattern
88
+ ### The Component CSS & Ampersand (`&`) Pattern (must go with RefProps)
64
89
 
65
90
  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
91
 
@@ -71,7 +96,7 @@ export const MyComponent = () => {
71
96
  const ref: RefProps = {
72
97
  onLoad: async () => {
73
98
  // 3. Querying namespaced elements
74
- const btn = ref.$('&-btn');
99
+ const btn = ref.$('.&-btn');
75
100
  btn.innerHTML = 'Ready';
76
101
  },
77
102
  };
@@ -101,11 +126,16 @@ export const MyComponent = () => {
101
126
  };
102
127
  ```
103
128
 
104
- **Key Takeaways for `&`**:
129
+ **Key Takeaways for `&` and `RefProps`**:
105
130
 
106
- 1. **In `CssProps`**: `'.&-item':` matches namespaced children. `'&:hover'` matches pseudo-classes on the element, and `'&.active'` combines with the current element.
131
+ 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`.
132
+ - `'&.active'` applies to the root element when it has the `.active` native class.
133
+ - `'.&-item'` applies to _descendant_ elements that have `class="&-item"`.
107
134
  2. **In JSX `class` attributes**: Add `class="&-item"`. You can still mix native classes: `class="row-box &-item"`.
108
- 3. **In `RefProps` Queries**: `ref.$('&-item')` compiles precisely to `el.querySelector('.l1234-item')`. If you don't use `&`, doing `ref.$('.btn')` will search _all descendants_ (`el.querySelector('.l1234 .btn')`), which might accidentally leak into child components! Prefixing with `&` guarantees you are querying the strict namespace.
135
+ 3. **In DOM Queries**:
136
+ - **🚨 NEVER use `document.querySelector('.&-item')` or `element.querySelector('.&-item')`**. Standard browser APIs DO NOT understand the `&` symbol and will fail to find the element.
137
+ - 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.
138
+ - Use **`ref.$all('.&-item')`** to get a `NodeList` of all matching descendants within the component.
109
139
 
110
140
  ## 4. CSS Placement Strategies: `css={}` vs `bindGlobalStyle`
111
141
 
@@ -122,12 +152,9 @@ When you pass `css={css}` to a JSX element, Lupine automatically evaluates it an
122
152
 
123
153
  ```typescript
124
154
  export const MyUniquePage = () => {
125
- const css = { '.&-container': { padding: '10px' } };
126
- return (
127
- <div css={css} class='&-container'>
128
- ...
129
- </div>
130
- );
155
+ // Styles apply directly to the root element
156
+ const css = { padding: '10px' };
157
+ return <div css={css}>...</div>;
131
158
  };
132
159
  ```
133
160
 
@@ -139,7 +166,7 @@ export const MyUniquePage = () => {
139
166
 
140
167
  **How it works seamlessly with `&`**:
141
168
 
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);`
169
+ 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
170
  2. You bind the style block globally once: `bindGlobalStyle(globalCssId, css);`
144
171
  3. You assign this ID to the component's `ref` so Lupine knows what to replace `&` with: `const ref: RefProps = { globalCssId };`
145
172
  4. Use `class="&-item"` normally. Lupine replaces `&` with the identical `globalCssId` across all instances!
@@ -150,38 +177,69 @@ export const MyUniquePage = () => {
150
177
  ```typescript
151
178
  import { bindGlobalStyle, getGlobalStylesId, CssProps, RefProps } from 'lupine.web';
152
179
 
153
- export const ToggleButton = (props: { color?: string }) => {
180
+ export const ToggleButton = (props: { color?: string; disabled?: boolean }) => {
154
181
  const css: CssProps = {
155
- // 1. Define namespaced sub-classes
156
- '.&-container': {
157
- padding: '10px',
158
- color: 'var(--primary-color)',
159
- },
182
+ // 1. Top-level properties apply directly to the root element.
183
+ // Do NOT wrap these in '.&-container'
184
+ padding: '10px',
185
+ color: 'var(--primary-color)',
186
+
187
+ // 2. You can mix nested modifiers for the root element:
188
+ '&.disabled': { opacity: '0.5' },
189
+
190
+ // 3. Or target specific inner children:
191
+ '.&-inner': { fontWeight: 'bold' },
160
192
  };
161
193
 
162
- // 2. Generate the ID and bind it globally (only happens once)
194
+ // 4. Generate the ID and bind it globally (Call INSIDE the component!)
163
195
  const tabGlobalCssId = getGlobalStylesId(css);
164
196
  bindGlobalStyle(tabGlobalCssId, css);
165
197
 
166
- // 3. Assign the global ID to the reference
198
+ // 5. Assign the global ID to the reference
167
199
  const ref: RefProps = {
168
200
  globalCssId: tabGlobalCssId,
169
201
  };
170
202
 
171
203
  return (
172
- <div
173
- // 4. Use the `&` pattern safely!
174
- class='&-container'
175
- ref={ref}
176
- // Handle instance-specific differences with inline style!
177
- style={{ backgroundColor: props.color }}
178
- >
179
- Click Me
204
+ <div ref={ref} class={props.disabled ? 'disabled' : ''} style={{ backgroundColor: props.color }}>
205
+ <span class='&-inner'>Click Me</span>
180
206
  </div>
181
207
  );
182
208
  };
183
209
  ```
184
210
 
211
+ ### ⚠️ IMPORTANT: Hardcoded Namespace IDs vs `getGlobalStylesId`
212
+
213
+ 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)`.
214
+
215
+ 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.
216
+
217
+ **CORRECT Example (Hardcoded ID):**
218
+
219
+ ```typescript
220
+ const css: CssProps = { padding: '10px' };
221
+ // ID is 'my-custom-component'
222
+ bindGlobalStyle('my-custom-component', css);
223
+
224
+ // Root element MUST have class='my-custom-component'
225
+ return <div class='my-custom-component'>...</div>;
226
+ ```
227
+
228
+ ### ⚠️ IMPORTANT: The "Static `CssProps`" Rule
229
+
230
+ 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.
231
+
232
+ **ANTI-PATTERN:** Putting React variables (like `isVertical`, `size`, `color`) directly inside the `CssProps` object structure.
233
+
234
+ - **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.
235
+
236
+ **CORRECT PATTERN:** Define one immutable `const css: CssProps = {...}` outside of any reactive dependencies.
237
+
238
+ - Handle visual variations by appending standard class names to your root element (`class={isVertical ? 'vertical' : 'horizontal'}`).
239
+ - In your static CSS definition, target these root modifier classes by prefixing the namespace `&` with the modifier: `&.vertical .&-track` or `&.horizontal .&-fill`.
240
+ - 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.
241
+ - 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.
242
+
185
243
  ---
186
244
 
187
245
  ## 5. Common Patterns ("The Lupine Way")
@@ -213,7 +271,7 @@ const MyPage = () => {
213
271
  // 4. Events
214
272
  const onSearch = async () => {
215
273
  // Read directly from DOM
216
- const query = ref.$('input.search').value;
274
+ const query = ref.$('input.&-search').value;
217
275
  // Update logic var
218
276
  pageIndex = 0;
219
277
  // Update UI manually
@@ -228,7 +286,7 @@ const MyPage = () => {
228
286
 
229
287
  return (
230
288
  <div ref={ref}>
231
- <input class='search' />
289
+ <input class='&-search' />
232
290
  <button onClick={onSearch}>Go</button>
233
291
  {/* Embed Dynamic Content */}
234
292
  {listDom.node}
@@ -239,33 +297,47 @@ const MyPage = () => {
239
297
 
240
298
  ### Mobile Navigation (`SliderFrame`)
241
299
 
242
- Lupine uses a "Slide-over" model for navigation (Drill-down).
300
+ 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
301
 
244
302
  ```typescript
245
303
  import { SliderFrame, SliderFrameHookProps, HeaderWithBackFrame } from 'lupine.components';
246
304
 
247
- // Parent Component
305
+ // 1. Parent Component (or Level 1)
248
306
  const Parent = () => {
307
+ // Define hook for Level 2
249
308
  const sliderHook: SliderFrameHookProps = {};
250
309
 
251
310
  const openDetail = (id) => {
252
311
  // Push new view onto stack
253
- sliderHook.load!(<DetailComponent id={id} sliderFrameHook={sliderHook} />);
312
+ sliderHook.load!(<DetailComponent id={id} parentSliderFrameHook={sliderHook} />);
254
313
  };
255
314
 
256
315
  return (
257
316
  <div>
317
+ {/* Placeholder for Level 2 */}
258
318
  <SliderFrame hook={sliderHook} />
319
+
259
320
  <div onClick={() => openDetail(1)}>Click Me</div>
260
321
  </div>
261
322
  );
262
323
  };
263
324
 
264
- // Child Component
265
- const DetailComponent = (props) => {
325
+ // 2. Child Component (Level 2)
326
+ const DetailComponent = (props: { id: number; parentSliderFrameHook: SliderFrameHookProps }) => {
327
+ // Define hook for Level 3
328
+ const childSliderHook: SliderFrameHookProps = {};
329
+
330
+ const openDeeper = () => {
331
+ // Load Level 3 component into this component's placeholder
332
+ childSliderHook.load!(<DetailComponent id={props.id + 1} parentSliderFrameHook={childSliderHook} />);
333
+ };
334
+
266
335
  return (
267
- <HeaderWithBackFrame title='Detail Page' onBack={(e) => props.sliderFrameHook.close!(e)}>
268
- Content...
336
+ <HeaderWithBackFrame title='Detail Page' onBack={(e) => props.parentSliderFrameHook.close!(e)}>
337
+ {/* Placeholder for Level 3 */}
338
+ <SliderFrame hook={childSliderHook} />
339
+
340
+ <div onClick={openDeeper}>Go Deeper</div>
269
341
  </HeaderWithBackFrame>
270
342
  );
271
343
  };
@@ -321,8 +393,131 @@ const Parent = () => {
321
393
 
322
394
  ## 6. Coding Standards & Gotchas
323
395
 
324
- - **❌ React Hooks**: `useState`, `useEffect` **DO NOT EXIST**. Use `HtmlVar` and `RefProps`.
396
+ - **`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**.
325
397
  - **❌ `className`**: Use standard HTML `class`.
326
398
  - **⚠️ `style={{}}`**: **Allowed** for simple or dynamic inline styles (e.g., `style={{ border: '1px solid red' }}`), but **prefer `css={CssProps}`** for structural/theme styling.
327
399
  - **✅ 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
400
  - **✅ 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".
401
+
402
+ ## 7. System Icons & Customization
403
+
404
+ 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`).
405
+
406
+ 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.
407
+
408
+ ### Overriding System Icons without Generating a Font
409
+
410
+ 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.
411
+
412
+ **1. Define your SVG Data URL:**
413
+ You can import an `.svg` file (if your bundler supports it) or define a raw Data URI string.
414
+
415
+ ```typescript
416
+ // Option A: Using bundler import
417
+ import githubIcon from 'github.svg';
418
+
419
+ // Option B: Raw Data URI string
420
+ 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`;
421
+
422
+ export const DemoIcons = {
423
+ github: githubIcon,
424
+ 'ma-close': closeSvgData,
425
+ };
426
+ ```
427
+
428
+ **2. Override the specific `.ifc-icon` via CSS Masking:**
429
+ Use the `maskImage` property wrapped in `url()` to apply the SVG data to the icon class. This ensures it inherits colors (`currentColor`) properly.
430
+
431
+ ```typescript
432
+ const css: CssProps = {
433
+ // Target the specific system icon class you wish to override
434
+ '.ifc-icon.ma-close': {
435
+ maskImage: `url("${DemoIcons['ma-close']}")`,
436
+ // If needed, specify mask sizing properties:
437
+ // maskRepeat: 'no-repeat',
438
+ // maskPosition: 'center',
439
+ // maskSize: 'contain',
440
+ },
441
+ };
442
+ ```
443
+
444
+ ## 8. Cross-Platform App Bootstrapping Guidance
445
+
446
+ When creating a new Cross-Platform App using `lupine.js`, follow this standard procedure for scaffolding the entry point, navigation, and icons:
447
+
448
+ 1. **Custom Navigation Icons (`app-icons.ts`)**:
449
+ 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.
450
+
451
+ 2. **Base Styles (`base-css.ts`)**:
452
+ 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.
453
+
454
+ 3. **Global UI Frame (`app-responsive-frame.tsx`)**:
455
+ Use `ResponsiveFrame` along with `SliderFrame` (for drill-down navigation via `SliderFrameHookProps`) to define the app's skeleton.
456
+
457
+ - Define your top/bottom navigation menus based on your icons.
458
+ - Return `ResponsiveFrame` passing in the `mainContent`, menus, and ensure you provide all required properties like `mobileSideMenuContent: <></>` (even if empty) to satisfy TypeScript interfaces.
459
+
460
+ 4. **Page Router Configuration (`index.tsx`)**:
461
+ 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)`.
462
+
463
+ 5. **Local Storage Patterns**:
464
+ 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.
465
+
466
+ ## 9. Standard Mobile App Layout & Interactions
467
+
468
+ 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.
469
+
470
+ ### A. The Global Root (`index.tsx` & `AppResponsiveFrame`)
471
+
472
+ - Use `bindTheme` to load global color tokens.
473
+ - Set the global frame with `pageRouter.setFramePage({ component: AppResponsiveFrame, placeholderClassname: 'user-page-placeholder' })`.
474
+ - **`AppResponsiveFrame`** handles the macro layout:
475
+ - It renders `<ResponsiveFrame>` wrapping `<main class='user-page-placeholder'></main>`.
476
+ - It contains the global left `mobileSideMenuContent` (typically abstracted into a `<SideMenuContent />` component).
477
+
478
+ ### B. The Home / List Page (`HomePage`)
479
+
480
+ A standard mobile list page must employ:
481
+
482
+ 1. **The Top Header (`MobileHeaderCenter`)**:
483
+
484
+ - Wrap the top bar in `<MobileHeaderCenter>`.
485
+ - Use `<MobileHeaderTitleIcon title='App Name' left={...} right={...} />`.
486
+ - The _left_ slot usually contains an empty spacer `<MobileHeaderEmptyIcon />`.
487
+ - The _right_ slot contains actions (e.g., Search icon, `<MobileTopSysIcon />` to open the Side Menu).
488
+
489
+ 2. **The Scrollable Content Area**:
490
+
491
+ - Beneath the header, create a flex-grow scrollable div: `<div class='flex-1 overflow-y-auto padding-m'>`.
492
+ - Mount an `HtmlVar` instance here (`{dom.node}`) to dynamically bind the list data arrays fetched typically via `RefProps.onLoad`.
493
+
494
+ 3. **Floating Action Button (FAB)**:
495
+ - Overlay a primary action button at `bottom: 24px`, `right: 24px` using `.fab-button` styled with `var(--primary-accent-color)`.
496
+
497
+ ### C. Advanced Touch Interactions (`createDragUtil` in Lists)
498
+
499
+ For interactive lists, `createDragUtil()` from `lupine.components` handles complex gesture physics.
500
+
501
+ - **Swipe-to-Reveal (Horizontal)**:
502
+
503
+ - Render an absolute positioned `.actions-layer` (opacity: 0 initially) underneath the `.list-card`.
504
+ - When the card's `onTouchStart`/`onMouseDown` is triggered, attach `dragUtil` handlers.
505
+ - 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.
506
+ - 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).
507
+
508
+ - **Drag-to-Reorder (Vertical)**:
509
+ - Define a distinct `.drag-handle` slot inside the card (e.g., `bs-list` icon).
510
+ - 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.
511
+ - Conclude by saving the new DOM sibling ordering in `dragUtil.setOnMoveEndCallback`.
512
+
513
+ ### D. Sub-Page Routing & Drill-Downs (`SliderFrame`)
514
+
515
+ - Slide-over interactions are mandatory for Search panels, Creation modals, and Details views.
516
+ - The `HomePage` must define a top-level `<SliderFrame hook={sliderFrameHook} />` inside its scroll area.
517
+ - Opening a child acts instantly via: `sliderFrameHook.load!(<MyChildPage sliderFrameHook={sliderFrameHook} />)`.
518
+ - **Inside the Child Component**:
519
+ - Must be wrapped with `<HeaderWithBackFrame title='Subpage' onBack={(e) => props.sliderFrameHook.close!(e)}>` to provide the standard top-left back chevron.
520
+ - **Nested SliderFrames**:
521
+ - 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.
522
+ - Using the parent's hook will replace the parent's content instead of sliding a new frame over it.
523
+ - 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
- if (!fs.existsSync(dest)) {
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
- if (!fs.existsSync(destDir)) {
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" />
@@ -2,32 +2,71 @@
2
2
  import './styles/global.css';
3
3
  import './styles/app.css';
4
4
 
5
- import { bindRouter, CssProps, debugWatch, HtmlVar, isFrontEnd, PageRouter, webEnv } from 'lupine.components';
5
+ import { bindRouter, CssProps, isFrontEnd, PageRouter, webEnv, debugWatch, useState } from 'lupine.components';
6
+
7
+ // ── useState demo component ─────────────────────────────────────
8
+ const Counter = () => {
9
+ const [count, setCount] = useState(0);
10
+ const [name, setName] = useState('World');
11
+ const [show, setShow] = useState(true);
6
12
 
7
- const HelloPage = () => {
8
13
  const css: CssProps = {
9
- padding: '20px',
10
- h1: {
11
- color: 'blue',
12
- '&:hover': {
13
- color: 'red',
14
- },
15
- },
14
+ display: 'inline-block',
15
+ padding: '24px',
16
+ border: '2px solid #4f8ef7',
17
+ borderRadius: '12px',
18
+ margin: '20px',
19
+ minWidth: '280px',
20
+ fontFamily: 'monospace',
16
21
  };
17
- const dom = new HtmlVar('0');
22
+
18
23
  return (
19
- <div css={css} style={{ textAlign: 'center' }}>
20
- <h1>Hello World</h1>
21
- <p>This is a simplified Lupine.js project.</p>
22
- <p>{dom.node}</p>
23
- <button onClick={() => (dom.value = (Number(dom.value) + 1).toString())}>Increment</button>
24
+ <div css={css}>
25
+ {/* number state */}
26
+ <p>
27
+ count = <strong>{count}</strong>
28
+ </p>
29
+ <button onClick={() => setCount(count - 1)}>−</button> <button onClick={() => setCount(count + 1)}>+</button>{' '}
30
+ {/* functional update test */}
31
+ <button onClick={() => setCount((n) => n * 2)}>×2</button>
32
+ {/* string state */}
33
+ <p style={{ marginTop: '12px' }}>
34
+ Hello, <strong>{name}</strong>!
35
+ </p>
36
+ <button onClick={() => setName(name === 'World' ? 'Lupine' : 'World')}>Toggle name</button>
37
+ {/* boolean state + multiple setState calls should batch into one rerender */}
38
+ <p style={{ marginTop: '12px' }}>
39
+ <button
40
+ onClick={() => {
41
+ // 3 consecutive setState calls — should batch into 1 rerender
42
+ setCount(count + 10);
43
+ setName('Batch!');
44
+ setShow(!show);
45
+ }}
46
+ >
47
+ Batch
48
+ </button>
49
+ </p>
50
+ {show && <p>✅ true</p>}
51
+ {!show && <p>❌ false</p>}
24
52
  </div>
25
53
  );
26
54
  };
27
55
 
56
+ const HelloPage = () => (
57
+ <div css={{ textAlign: 'center', padding: '20px' }}>
58
+ <h1>Demo</h1>
59
+ <Counter />
60
+ <Counter /> {/* two independent instances — state is isolated */}
61
+ </div>
62
+ );
63
+ // ────────────────────────────────────────────────────────────────
64
+
65
+ // #if DEV==='1'
28
66
  if (isFrontEnd() && webEnv('NODE_ENV', '') === 'development') {
29
67
  debugWatch(webEnv('API_PORT', 0));
30
68
  }
69
+ // #endif
31
70
 
32
71
  const pageRouter = new PageRouter();
33
72
  pageRouter.use('/', HelloPage);