create-lupine 1.0.11 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -34,28 +34,15 @@ const ref: RefProps = {
|
|
|
34
34
|
// Unmounting: Cleanup
|
|
35
35
|
onUnload: async (el: Element) => {
|
|
36
36
|
// Cleanup (timers, sockets)
|
|
37
|
-
}
|
|
37
|
+
},
|
|
38
38
|
};
|
|
39
39
|
// Usage
|
|
40
|
-
<div ref={ref}>...</div
|
|
40
|
+
<div ref={ref}>...</div>;
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
### `CssProps` (Styling)
|
|
44
44
|
|
|
45
|
-
Supports nesting and media queries. **Prefer this over inline styles.**
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
import { MediaQueryRange } from "lupine.components";
|
|
49
|
-
|
|
50
|
-
const css: CssProps = {
|
|
51
|
-
display: "flex",
|
|
52
|
-
".child": { color: "var(--primary-color)" },
|
|
53
|
-
// Responsive
|
|
54
|
-
[MediaQueryRange.MobileBelow]: {
|
|
55
|
-
flexDirection: "column",
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
```
|
|
45
|
+
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.
|
|
59
46
|
|
|
60
47
|
## 3. Styles & Themes ("The Look")
|
|
61
48
|
|
|
@@ -73,41 +60,131 @@ const css: CssProps = {
|
|
|
73
60
|
- **Margins/Padding**: `m-auto`, `p-m`, `mt-s`, `pb-l` (s=small, m=medium, l=large).
|
|
74
61
|
- **Text**: `.text-center`, `.ellipsis`.
|
|
75
62
|
|
|
76
|
-
###
|
|
63
|
+
### The Component CSS & Ampersand (`&`) Pattern
|
|
64
|
+
|
|
65
|
+
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
|
+
|
|
67
|
+
When Lupine renders the component, it generates a unique ID (e.g., `l1234`) and replaces the `&` with this ID everywhere it's used.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
export const MyComponent = () => {
|
|
71
|
+
const ref: RefProps = {
|
|
72
|
+
onLoad: async () => {
|
|
73
|
+
// 3. Querying namespaced elements
|
|
74
|
+
const btn = ref.$('&-btn');
|
|
75
|
+
btn.innerHTML = 'Ready';
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const css: CssProps = {
|
|
80
|
+
// Top-level rules apply to the root component container itself
|
|
81
|
+
width: '100%',
|
|
82
|
+
padding: '1rem',
|
|
83
|
+
|
|
84
|
+
// 1. Defining namespaced sub-classes in CSS:
|
|
85
|
+
'.&-title': { fontWeight: 'bold' },
|
|
86
|
+
'.&-btn': {
|
|
87
|
+
// Nesting pseudo-classes and combination modifiers (no space after &)
|
|
88
|
+
'&:hover': { background: '#f0f0f0' },
|
|
89
|
+
'&.active': { color: 'var(--primary-accent-color)' },
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
// Setting css={css} safely bounds this style scope
|
|
95
|
+
<aside css={css} ref={ref}>
|
|
96
|
+
{/* 2. Applying namespaced classes in JSX */}
|
|
97
|
+
<div class='&-title'>Hello</div>
|
|
98
|
+
<button class='&-btn active'>Click Me</button>
|
|
99
|
+
</aside>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Key Takeaways for `&`**:
|
|
105
|
+
|
|
106
|
+
1. **In `CssProps`**: `'.&-item':` matches namespaced children. `'&:hover'` matches pseudo-classes on the element, and `'&.active'` combines with the current element.
|
|
107
|
+
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.
|
|
109
|
+
|
|
110
|
+
## 4. CSS Placement Strategies: `css={}` vs `bindGlobalStyle`
|
|
111
|
+
|
|
112
|
+
Lupine.js provides two main ways to inject component CSS. Choosing the right one is critical for performance and DOM cleanliness.
|
|
113
|
+
|
|
114
|
+
### Strategy A: The `css={}` Prop (Dynamic / Single-Use)
|
|
115
|
+
|
|
116
|
+
**Best for**: Pages, views, or high-level containers that are only rendered once per screen.
|
|
117
|
+
|
|
118
|
+
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.
|
|
77
119
|
|
|
78
|
-
|
|
120
|
+
**Pros**: Perfect isolation. You can safely style dynamic children easily.
|
|
121
|
+
**Cons**: If you render 100 items using `css={}`, you will inject 100 identical `<style>` blocks into the DOM, severely bloating the page.
|
|
79
122
|
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<div class='
|
|
85
|
-
|
|
86
|
-
<div class='setting-section-item-icon'><i class='ifc-icon ma-chevron-right'></i></div>
|
|
123
|
+
```typescript
|
|
124
|
+
export const MyUniquePage = () => {
|
|
125
|
+
const css = { '.&-container': { padding: '10px' } };
|
|
126
|
+
return (
|
|
127
|
+
<div css={css} class='&-container'>
|
|
128
|
+
...
|
|
87
129
|
</div>
|
|
88
|
-
|
|
89
|
-
|
|
130
|
+
);
|
|
131
|
+
};
|
|
90
132
|
```
|
|
91
133
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
134
|
+
### Strategy B: `bindGlobalStyle` (Reusable Components)
|
|
135
|
+
|
|
136
|
+
**Best for**: Reusable UI components (Buttons, Toggles, List Items, Modals) that will be rendered multiple times.
|
|
137
|
+
|
|
138
|
+
`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!
|
|
139
|
+
|
|
140
|
+
**How it works seamlessly with `&`**:
|
|
141
|
+
|
|
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);`
|
|
143
|
+
2. You bind the style block globally once: `bindGlobalStyle(globalCssId, css);`
|
|
144
|
+
3. You assign this ID to the component's `ref` so Lupine knows what to replace `&` with: `const ref: RefProps = { globalCssId };`
|
|
145
|
+
4. Use `class="&-item"` normally. Lupine replaces `&` with the identical `globalCssId` across all instances!
|
|
146
|
+
|
|
147
|
+
**Pros**: Highly efficient. Rendering 100 buttons only generates 1 style block. Completely safe from class name collisions.
|
|
148
|
+
**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.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { bindGlobalStyle, getGlobalStylesId, CssProps, RefProps } from 'lupine.web';
|
|
152
|
+
|
|
153
|
+
export const ToggleButton = (props: { color?: string }) => {
|
|
154
|
+
const css: CssProps = {
|
|
155
|
+
// 1. Define namespaced sub-classes
|
|
156
|
+
'.&-container': {
|
|
157
|
+
padding: '10px',
|
|
158
|
+
color: 'var(--primary-color)',
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// 2. Generate the ID and bind it globally (only happens once)
|
|
163
|
+
const tabGlobalCssId = getGlobalStylesId(css);
|
|
164
|
+
bindGlobalStyle(tabGlobalCssId, css);
|
|
165
|
+
|
|
166
|
+
// 3. Assign the global ID to the reference
|
|
167
|
+
const ref: RefProps = {
|
|
168
|
+
globalCssId: tabGlobalCssId,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
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
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
108
183
|
```
|
|
109
184
|
|
|
110
|
-
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 5. Common Patterns ("The Lupine Way")
|
|
111
188
|
|
|
112
189
|
### List / Search (No Re-render)
|
|
113
190
|
|
|
@@ -124,7 +201,13 @@ const MyPage = () => {
|
|
|
124
201
|
// 3. Render Function
|
|
125
202
|
const makeList = async () => {
|
|
126
203
|
const data = await fetchData(pageIndex);
|
|
127
|
-
return
|
|
204
|
+
return (
|
|
205
|
+
<div>
|
|
206
|
+
{data.map((item) => (
|
|
207
|
+
<Item item={item} />
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
128
211
|
};
|
|
129
212
|
|
|
130
213
|
// 4. Events
|
|
@@ -140,15 +223,15 @@ const MyPage = () => {
|
|
|
140
223
|
const ref: RefProps = {
|
|
141
224
|
onLoad: async () => {
|
|
142
225
|
listDom.value = await makeList();
|
|
143
|
-
}
|
|
226
|
+
},
|
|
144
227
|
};
|
|
145
228
|
|
|
146
229
|
return (
|
|
147
230
|
<div ref={ref}>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
231
|
+
<input class='search' />
|
|
232
|
+
<button onClick={onSearch}>Go</button>
|
|
233
|
+
{/* Embed Dynamic Content */}
|
|
234
|
+
{listDom.node}
|
|
152
235
|
</div>
|
|
153
236
|
);
|
|
154
237
|
};
|
|
@@ -172,23 +255,58 @@ const Parent = () => {
|
|
|
172
255
|
|
|
173
256
|
return (
|
|
174
257
|
<div>
|
|
175
|
-
|
|
176
|
-
|
|
258
|
+
<SliderFrame hook={sliderHook} />
|
|
259
|
+
<div onClick={() => openDetail(1)}>Click Me</div>
|
|
177
260
|
</div>
|
|
178
261
|
);
|
|
179
|
-
}
|
|
262
|
+
};
|
|
180
263
|
|
|
181
264
|
// Child Component
|
|
182
265
|
const DetailComponent = (props) => {
|
|
183
266
|
return (
|
|
184
|
-
<HeaderWithBackFrame
|
|
185
|
-
title="Detail Page"
|
|
186
|
-
onBack={(e) => props.sliderFrameHook.close!(e)}
|
|
187
|
-
>
|
|
267
|
+
<HeaderWithBackFrame title='Detail Page' onBack={(e) => props.sliderFrameHook.close!(e)}>
|
|
188
268
|
Content...
|
|
189
269
|
</HeaderWithBackFrame>
|
|
190
270
|
);
|
|
191
|
-
}
|
|
271
|
+
};
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Component Hooks (Imperative Control)
|
|
275
|
+
|
|
276
|
+
Instead of React's `useImperativeHandle` or lifting state up, Lupine components often use a `hook` pattern for parent-to-child communication and exposing methods.
|
|
277
|
+
|
|
278
|
+
1. **Parent** creates an empty object: `const myHook: MyComponentHookProps = {};`
|
|
279
|
+
2. **Parent** passes it to the child: `<MyComponent hook={myHook} />`
|
|
280
|
+
3. **Child** populates it during render:
|
|
281
|
+
```typescript
|
|
282
|
+
if (props.hook) {
|
|
283
|
+
props.hook.getValue = () => value;
|
|
284
|
+
props.hook.setValue = (val) => {
|
|
285
|
+
updateDOM(val);
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
4. **Parent** calls it later on demand: `console.log(myHook.getValue());`
|
|
290
|
+
|
|
291
|
+
**⚠️ 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`:
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
const Parent = () => {
|
|
295
|
+
const myHook: MyComponentHookProps = {};
|
|
296
|
+
|
|
297
|
+
const ref: RefProps = {
|
|
298
|
+
onLoad: async () => {
|
|
299
|
+
// Safe: Child has rendered and populated the hook
|
|
300
|
+
myHook.setValue('Hello');
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<div ref={ref}>
|
|
306
|
+
<MyComponent hook={myHook} />
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
};
|
|
192
310
|
```
|
|
193
311
|
|
|
194
312
|
## 5. Architecture Cheat Sheet
|
|
@@ -99,6 +99,7 @@ const watchServer = async (isDev, npmCmd, httpPort, serverRootPath) => {
|
|
|
99
99
|
treeShaking: true,
|
|
100
100
|
metafile: true,
|
|
101
101
|
external: ['better-sqlite3', 'nodemailer', 'pdfkit', 'sharp'],
|
|
102
|
+
loader: { '.svg': 'text', '.glsl': 'text', '.png': 'file', '.gif': 'file', '.html': 'text' },
|
|
102
103
|
minify: !isDev,
|
|
103
104
|
plugins: [watchServerPlugin(isDev, npmCmd, httpPort)],
|
|
104
105
|
});
|
|
@@ -214,6 +215,7 @@ const watchApi = async (saved, isDev, entryPoints) => {
|
|
|
214
215
|
treeShaking: true,
|
|
215
216
|
metafile: true,
|
|
216
217
|
external: ['better-sqlite3', 'nodemailer', 'pdfkit', 'sharp'],
|
|
218
|
+
loader: { '.svg': 'text', '.glsl': 'text', '.png': 'file', '.gif': 'file', '.html': 'text' },
|
|
217
219
|
minify: !isDev,
|
|
218
220
|
plugins: [watchApiPlugin(isDev, saved.httpPort)],
|
|
219
221
|
});
|