ssr-emotion-react 1.0.0-beta.5 → 1.0.1
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/README.md +126 -57
- package/dist/astro-integration.js +48 -48
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
A natural and powerful Zero-Runtime CSS-in-JS solution et ses React 🍅
|
|
4
4
|
|
|
5
|
-
## LIVE DEMO with Astro
|
|
6
|
-
|
|
7
|
-
準備中...
|
|
8
|
-
|
|
9
5
|
## Installation
|
|
10
6
|
|
|
11
7
|
### 🚀 Astro
|
|
@@ -39,8 +35,9 @@ Now, you can use not only Astro components (`.astro`) but also React JSX compone
|
|
|
39
35
|
|
|
40
36
|
> **Note:** React JSX components in Astro Islands
|
|
41
37
|
>
|
|
42
|
-
> * **No directive (
|
|
43
|
-
>
|
|
38
|
+
> * **No directive (SSR Only)**: Rendered as just static HTML tags. It results in zero client-side JavaScript. (I used to think there wasn't much point in writing static content in JSX components instead of just using Astro components. It seemed like standard Astro components was more than enough. **However, I've realized one major advantage:** SSR Emotion — the ultimate SSR Zero-Runtime CSS-in-JS solution, seamlessly integrated with Astro. By using React JSX components, your styles are automatically extracted into static CSS during the build process. This means you can enjoy the full power of CSS-in-JS while still shipping zero bytes of JS to the browser. In this regard, it's a significant upgrade over standard Astro components.)
|
|
39
|
+
>
|
|
40
|
+
> * `client:only="react"` **(CSR Only)**: As you know, this is the standard mode where Emotion is used, and this plugin does nothing. It skips server-side rendering and runs entirely in the browser.
|
|
44
41
|
>
|
|
45
42
|
> * `client:load`(and others like `client:visible` or `client:idle`) **(SSR Hydration)**: Despite its cool and flashy name, "SSR Hydration" is not that complicated: it just creates a static HTML skeleton first, and once the JS is ready, the engine takes over the DOM as if it had been there from the start. If you are particular about the visual transition—like ensuring there is no layout shift by pre-setting an image's height—you might want to take control to make the swap feel completely natural.
|
|
46
43
|
|
|
@@ -52,24 +49,24 @@ This plugin wasn't originally built for React. It was first conceived as a core
|
|
|
52
49
|
While developing [Potate](https://github.com/uniho/potate), I discovered a way to handle SSR [Emotion](https://emotion.sh/docs/@emotion/css) and Hydration that felt more "correct" for the Astro era: **The Full DOM Replacement strategy.** It worked so flawlessly in Potate that I decided to bring this lineage back to the "ancestor," React. By applying Potate's philosophy to React, we've eliminated the historical complexities of Emotion SSR and the fragility of standard React hydration.
|
|
53
50
|
|
|
54
51
|
|
|
55
|
-
## 💎
|
|
52
|
+
## 💎 SSR Only
|
|
53
|
+
|
|
54
|
+
You don't need to learn any special properties or complex setups. It just works with the [Emotion `css()` function](https://emotion.sh/docs/@emotion/css). It feels completely natural, even in Astro's **"No directive" (SSR Only)** mode.
|
|
56
55
|
|
|
57
56
|
* **Zero Runtime by default:** No `Emotion` library is shipped to the browser. It delivers a pure Zero-JS experience.
|
|
58
57
|
* **Familiar DX:** Use the full expressive power of the [Emotion `css()` function](https://emotion.sh/docs/@emotion/css) that you already know.
|
|
59
58
|
* **Decoupled Asset Delivery:** Styles are moved to separate `.css` files to allow for flexible cache strategies. By using unique filenames (cache busting), we ensure that updates are immediately reflected even when long-term caching is enabled on the server.
|
|
60
|
-
* **Hydration Stability:** No overhead for style re-calculation. Interactive Islands remain stable and fully dynamic without visual flickering during the hydration process.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## 🛠 How it looks
|
|
64
59
|
|
|
65
|
-
|
|
60
|
+
### Example: SSR Only Styling
|
|
66
61
|
|
|
67
62
|
While you can use [`css()`](https://emotion.sh/docs/@emotion/css) directly, you can also create reusable functions like `flexCol()` (which we call **"The Patterns"**).
|
|
68
63
|
|
|
69
64
|
```jsx
|
|
65
|
+
// src/components/StaticBox.jsx
|
|
66
|
+
|
|
70
67
|
import { css } from '@emotion/css'
|
|
71
68
|
|
|
72
|
-
export
|
|
69
|
+
export default props => (
|
|
73
70
|
<div class={flexCol({
|
|
74
71
|
color: 'hotpink',
|
|
75
72
|
'&:hover': { color: 'deeppink' }
|
|
@@ -85,10 +82,27 @@ const flexCol = (...args) => css({
|
|
|
85
82
|
|
|
86
83
|
```
|
|
87
84
|
|
|
85
|
+
```astro
|
|
86
|
+
---
|
|
87
|
+
// src/pages/index.astro
|
|
88
|
+
|
|
89
|
+
import Layout from '../layouts/Layout.astro'
|
|
90
|
+
import StaticBox from '../components/StaticBox'
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
<Layout>
|
|
94
|
+
<StaticBox />
|
|
95
|
+
</Layout>
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 🌗 SSR Hydration (SSR + CSR)
|
|
88
100
|
|
|
89
|
-
|
|
101
|
+
In Astro, Island components (`client:load` and others) get the best of both worlds.
|
|
90
102
|
|
|
91
|
-
|
|
103
|
+
* **Hydration Stability:** No overhead for style re-calculation. Interactive Islands remain stable and fully dynamic without visual flickering during the hydration process.
|
|
104
|
+
* **Unlimited Flexibility:** Need to change colors based on user input or mouse position? Just pass the props/state to css() like you always do.
|
|
105
|
+
* **Zero Learning Curve:** If you know how to use useEffect and Emotion, you already know how to build dynamic Islands with React.
|
|
92
106
|
|
|
93
107
|
### How it works
|
|
94
108
|
|
|
@@ -107,12 +121,12 @@ You can easily change styles when the component "wakes up" in the browser:
|
|
|
107
121
|
```jsx
|
|
108
122
|
// src/components/InteractiveBox.jsx
|
|
109
123
|
|
|
110
|
-
export
|
|
111
|
-
const [isLoaded, setIsLoaded] = useState(false)
|
|
124
|
+
export default props => {
|
|
125
|
+
const [isLoaded, setIsLoaded] = useState(false)
|
|
112
126
|
|
|
113
127
|
useEffect(() => {
|
|
114
|
-
setIsLoaded(true)
|
|
115
|
-
}, [])
|
|
128
|
+
setIsLoaded(true) // Triggers once JS is active
|
|
129
|
+
}, [])
|
|
116
130
|
|
|
117
131
|
return (
|
|
118
132
|
<div class={css({
|
|
@@ -123,8 +137,8 @@ export const InteractiveBox = () => {
|
|
|
123
137
|
})}>
|
|
124
138
|
{isLoaded ? 'I am Interactive!' : 'I am Static HTML'}
|
|
125
139
|
</div>
|
|
126
|
-
)
|
|
127
|
-
}
|
|
140
|
+
)
|
|
141
|
+
}
|
|
128
142
|
|
|
129
143
|
```
|
|
130
144
|
|
|
@@ -132,9 +146,9 @@ export const InteractiveBox = () => {
|
|
|
132
146
|
---
|
|
133
147
|
// src/pages/index.astro
|
|
134
148
|
|
|
135
|
-
import Layout from '../layouts/Layout.astro'
|
|
136
|
-
import StaticBox from '../components/StaticBox'
|
|
137
|
-
import InteractiveBox from '../components/InteractiveBox'
|
|
149
|
+
import Layout from '../layouts/Layout.astro'
|
|
150
|
+
import StaticBox from '../components/StaticBox'
|
|
151
|
+
import InteractiveBox from '../components/InteractiveBox'
|
|
138
152
|
---
|
|
139
153
|
|
|
140
154
|
<Layout>
|
|
@@ -144,13 +158,6 @@ import InteractiveBox from '../components/InteractiveBox';
|
|
|
144
158
|
|
|
145
159
|
```
|
|
146
160
|
|
|
147
|
-
### Key Benefits
|
|
148
|
-
|
|
149
|
-
* **No FOUC (Flash of Unstyled Content):** Since the "Server Side" style is already in the static CSS, there's no flickering.
|
|
150
|
-
* **Unlimited Flexibility:** Need to change colors based on user input or mouse position? Just pass the props/state to css() like you always do.
|
|
151
|
-
* **Zero Learning Curve:** If you know how to use useEffect and Emotion, you already know how to build dynamic Islands with React.
|
|
152
|
-
|
|
153
|
-
|
|
154
161
|
## 🛠 The Patterns
|
|
155
162
|
|
|
156
163
|
We refer to reusable CSS logic as "The Patterns".
|
|
@@ -176,7 +183,9 @@ const linkOverlay = (...args) => css({
|
|
|
176
183
|
},
|
|
177
184
|
}, ...args)
|
|
178
185
|
|
|
179
|
-
|
|
186
|
+
:
|
|
187
|
+
|
|
188
|
+
const MyComponent = props => (
|
|
180
189
|
// The parent must have position: relative
|
|
181
190
|
<div class={css({ position: 'relative', border: '1px solid #ccc', padding: '1rem' })}>
|
|
182
191
|
<img src="https://via.placeholder.com/150" alt="placeholder" />
|
|
@@ -202,11 +211,13 @@ const isBP = value => value in BP
|
|
|
202
211
|
const _gt = bp => `(min-width: ${isBP(bp) ? BP[bp] : bp})`
|
|
203
212
|
const _lt = bp => `(max-width: ${isBP(bp) ? BP[bp] : bp})`
|
|
204
213
|
|
|
205
|
-
const gt = (bp, ...args) => css({[`@media ${_gt(bp)}`]: args})
|
|
206
|
-
const lt = (bp, ...args) => css({[`@media ${_lt(bp)}`]: args})
|
|
207
|
-
const bw = (min, max, ...args) => css({[`@media ${_gt(min)} and ${_lt(max)}`]: args})
|
|
214
|
+
const gt = (bp, ...args) => css({[`@media ${_gt(bp)}`]: css(...args)})
|
|
215
|
+
const lt = (bp, ...args) => css({[`@media ${_lt(bp)}`]: css(...args)})
|
|
216
|
+
const bw = (min, max, ...args) => css({[`@media ${_gt(min)} and ${_lt(max)}`]: css(...args)})
|
|
208
217
|
|
|
209
|
-
|
|
218
|
+
:
|
|
219
|
+
|
|
220
|
+
const MyComponent = props => (
|
|
210
221
|
<div class={css(
|
|
211
222
|
{ color: 'black' }, // default css
|
|
212
223
|
bw('sm', '75rem', { color: 'blue' }), // between
|
|
@@ -234,7 +245,7 @@ export const styled = (Tag) => (style, ...values) => props => {
|
|
|
234
245
|
const makeClassName = (style, ...values) =>
|
|
235
246
|
typeof style == 'function' ? makeClassName(style(props)) : css(style, ...values);
|
|
236
247
|
|
|
237
|
-
const {sx, className, 'class': _class, children, ...wosx} = props;
|
|
248
|
+
const {as: As, sx, className, 'class': _class, children, ...wosx} = props;
|
|
238
249
|
|
|
239
250
|
// cleanup transient props
|
|
240
251
|
Object.keys(wosx).forEach(key => {
|
|
@@ -246,7 +257,8 @@ export const styled = (Tag) => (style, ...values) => props => {
|
|
|
246
257
|
className: cx(makeClassName(style, ...values), makeClassName(sx), _class, className),
|
|
247
258
|
};
|
|
248
259
|
|
|
249
|
-
|
|
260
|
+
const T = As || Tag;
|
|
261
|
+
return (<T {...newProps}>{children}</T>);
|
|
250
262
|
};
|
|
251
263
|
|
|
252
264
|
```
|
|
@@ -255,26 +267,23 @@ What is the sx prop? For those unfamiliar with libraries like [MUI, the sx prop]
|
|
|
255
267
|
|
|
256
268
|
In this implementation, you can pass raw style objects to the sx prop without wrapping them in `css()` or "The Patterns" functions.
|
|
257
269
|
|
|
258
|
-
However,
|
|
270
|
+
However, defining a `styled` component inside a render function is **a pitfall** because it creates a new component identity every time, forcing React to re-mount.
|
|
271
|
+
I personally prefer the approach shown below. In any case, how you choose to implement this is entirely up to you.
|
|
259
272
|
|
|
260
273
|
```js
|
|
261
|
-
// the-sx-prop.
|
|
274
|
+
// the-sx-prop.jsx
|
|
262
275
|
|
|
263
276
|
import {css, cx} from '@emotion/css'
|
|
264
277
|
|
|
265
|
-
const sx = (props, ...
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
_css.push(arg);
|
|
273
|
-
}
|
|
278
|
+
export const sx = (props, style, ...values) => {
|
|
279
|
+
let result = (props && typeof props === 'object' ? props : {});
|
|
280
|
+
if (typeof style === 'function') {
|
|
281
|
+
result = {...style(result), ...result};
|
|
282
|
+
result.className = cx(css(result?.$css, ...values), result.className);
|
|
283
|
+
} else {
|
|
284
|
+
result.className = cx(css(style, ...values), result.className);
|
|
274
285
|
}
|
|
275
286
|
|
|
276
|
-
result.className = cx(css(_css), result.className);
|
|
277
|
-
|
|
278
287
|
// cleanup transient props
|
|
279
288
|
Object.keys(result).forEach(key => {
|
|
280
289
|
if (key.startsWith('$')) delete result[key];
|
|
@@ -283,10 +292,16 @@ const sx = (props, ...styles) => {
|
|
|
283
292
|
return result;
|
|
284
293
|
}
|
|
285
294
|
|
|
286
|
-
|
|
295
|
+
// Factory for component-scoped sx functions (adds `.css()` automatically)
|
|
296
|
+
sx._factory = (genCSS) => {
|
|
297
|
+
const f = (props, ...styles) => sx(props || {}, genCSS, ...styles);
|
|
298
|
+
f.css = (...styles) => f({}, ...styles); // style only
|
|
299
|
+
// f.curry = (props) => (...values) => f(props || {}, ...values); // currying
|
|
300
|
+
return f;
|
|
301
|
+
}
|
|
287
302
|
|
|
288
303
|
// My button style
|
|
289
|
-
sx.button =
|
|
304
|
+
sx.button = sx._factory(props => {
|
|
290
305
|
const style = {
|
|
291
306
|
// default is text button
|
|
292
307
|
padding: '8px 16px',
|
|
@@ -305,7 +320,7 @@ sx.button = (props, ...styles) => sx(props, ...styles, props => {
|
|
|
305
320
|
style.boxShadow = 'var(--style-shadows-level1)';
|
|
306
321
|
}
|
|
307
322
|
|
|
308
|
-
return [
|
|
323
|
+
return {$css: [
|
|
309
324
|
css`
|
|
310
325
|
line-height: 1;
|
|
311
326
|
display: inline-flex;
|
|
@@ -316,26 +331,80 @@ sx.button = (props, ...styles) => sx(props, ...styles, props => {
|
|
|
316
331
|
}
|
|
317
332
|
`,
|
|
318
333
|
style,
|
|
319
|
-
];
|
|
334
|
+
]};
|
|
320
335
|
});
|
|
321
336
|
|
|
322
337
|
```
|
|
323
338
|
|
|
324
339
|
```jsx
|
|
325
|
-
import sx from './the-sx-prop'
|
|
340
|
+
import {sx} from './the-sx-prop'
|
|
326
341
|
|
|
327
342
|
export default props => {
|
|
328
343
|
return (<>
|
|
329
344
|
<button {...sx.button({}, {margin: '1rem'})}>Buuton1</button>
|
|
345
|
+
<button {...sx.button.css({margin: '1rem'})}>Buuton1</button>
|
|
330
346
|
<button {...sx.button({$elevated: true}, {margin: '1rem'})}>Button2</button>
|
|
331
347
|
<div {...sx.button({$elevated: true}, {margin: '1rem'})}>Button3</div>
|
|
332
|
-
<button disabled {...sx.button({
|
|
348
|
+
<button disabled {...sx.button.css({margin: '1rem'})}>Button4</button>
|
|
333
349
|
<button {...sx.button({disabled: true}, {margin: '1rem'})}>Button5</button>
|
|
334
350
|
</>);
|
|
335
351
|
}
|
|
336
352
|
|
|
337
353
|
```
|
|
338
354
|
|
|
355
|
+
Furthermore, by creating a component like the one below, you evolve into a **Super Saiyan** (for those who aren't familiar, it's like a classic Superman).
|
|
356
|
+
|
|
357
|
+
```js
|
|
358
|
+
// the-sx-prop.jsx
|
|
359
|
+
|
|
360
|
+
:
|
|
361
|
+
:
|
|
362
|
+
|
|
363
|
+
export const As = ({as: Tag = 'div', children, ...props}) => <Tag {...props}>{children}</Tag>;
|
|
364
|
+
|
|
365
|
+
// My list style
|
|
366
|
+
sx.ul = sx._factory(props => ({
|
|
367
|
+
as: 'ul',
|
|
368
|
+
$css: css`
|
|
369
|
+
& > li {
|
|
370
|
+
position: relative;
|
|
371
|
+
padding-left: 1.5rem;
|
|
372
|
+
|
|
373
|
+
&:before {
|
|
374
|
+
content: "\\2022";
|
|
375
|
+
position: absolute;
|
|
376
|
+
width: 1.5rem;
|
|
377
|
+
left: 0; top: 0;
|
|
378
|
+
text-align: center;
|
|
379
|
+
}
|
|
380
|
+
& + li, & > ul {
|
|
381
|
+
margin-top: .25rem;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
& > ul {
|
|
385
|
+
margin-left: 1rem;
|
|
386
|
+
}
|
|
387
|
+
`}));
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
```jsx
|
|
392
|
+
import {sx, As} from './the-sx-prop'
|
|
393
|
+
|
|
394
|
+
const MyComponent = props => {
|
|
395
|
+
return (
|
|
396
|
+
<As {...sx.ul.css({ padding: '20px', border: '1px solid #ccc' })}>
|
|
397
|
+
<li>Is it Flexible?</li>
|
|
398
|
+
<li>Is it Dynamic?</li>
|
|
399
|
+
<li>Is it Polymorphic?</li>
|
|
400
|
+
<li>No, it's <strong>As</strong>!</li>
|
|
401
|
+
</As>
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
|
|
339
408
|
## 🚫 Won't Do
|
|
340
409
|
|
|
341
410
|
The following features are not planned for the core roadmap (though contributors are welcome to explore them):
|