create-lupine 1.0.16 β†’ 1.0.18

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