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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lupine",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Scaffolding tool for Lupine.js projects",
5
5
  "bin": {
6
6
  "create-lupine": "index.js"
@@ -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
- ### Standard UI Components
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
- #### Settings Group (Mobile/Desktop Settings)
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
- ```tsx
81
- <div class='setting-section-group'>
82
- <div class='setting-section-title'>Section Title</div>
83
- <div class='setting-section-block'>
84
- <div class='setting-section-item' onClick={...}>
85
- <div class='setting-section-item-text'>My Option</div>
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
- </div>
89
- </div>
130
+ );
131
+ };
90
132
  ```
91
133
 
92
- #### Admin Edit Row (Form Layout)
93
-
94
- Standard label + input row pattern for admin pages.
95
-
96
- ```tsx
97
- <div class="row-box mt-m">
98
- <div class="label-class">Title: </div>
99
- <div class="flex-1">
100
- {/* width-100p ensures input fills the flex-1 container */}
101
- <input
102
- type="text"
103
- class="input-base my-input-class w-100p"
104
- value={item.title}
105
- />
106
- </div>
107
- </div>
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
- ## 4. Common Patterns ("The Lupine Way")
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 <div>{data.map(item => <Item item={item} />)}</div>;
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
- <input class="search" />
149
- <button onClick={onSearch}>Go</button>
150
- {/* Embed Dynamic Content */}
151
- {listDom.node}
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
- <SliderFrame hook={sliderHook} />
176
- <div onClick={() => openDetail(1)}>Click Me</div>
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
  });