create-aziz-app 3.1.2 → 3.1.3
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 +658 -207
- package/bin/create-aziz-app.js +302 -302
- package/package.json +1 -1
- package/template/README.md +402 -402
- package/template/_framework/api.js +219 -219
- package/template/_framework/boot.js +33 -33
- package/template/_framework/hooks.js +367 -291
- package/template/_framework/next-response.js +128 -128
- package/template/_framework/route-match.js +145 -145
- package/template/_framework/router.js +465 -259
- package/template/_framework/runtime.js +264 -264
- package/template/app/layout.js +18 -18
- package/template/app/page.js +33 -33
- package/template/docs/01-getting-started.md +151 -151
- package/template/docs/02-project-structure.md +109 -109
- package/template/docs/03-pages-and-layouts.md +140 -140
- package/template/docs/04-routing.md +120 -120
- package/template/docs/05-hooks.md +169 -169
- package/template/docs/06-api-routes.md +210 -210
- package/template/docs/07-api-client.md +151 -151
- package/template/docs/08-styling.md +94 -94
- package/template/docs/09-imports-and-modules.md +97 -97
- package/template/docs/10-database-and-models.md +154 -154
- package/template/docs/11-authentication.md +171 -171
- package/template/docs/12-production-and-deployment.md +121 -121
- package/template/docs/13-internals.md +179 -179
- package/template/docs/README.md +67 -67
- package/template/index.html +20 -20
- package/template/package.json +16 -16
- package/template/scripts/dev-server.js +493 -381
- package/template/scripts/lib/api-router.js +525 -525
- package/template/scripts/lib/js-transform.js +213 -195
- package/template/scripts/lib/module-resolver.js +132 -132
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
# 07 · API Client
|
|
2
|
-
|
|
3
|
-
The API client is the browser-side companion to [API routes](./06-api-routes.md). It's a tiny, dependency-free `fetch` wrapper with JSON handling, timeouts, retries, and typed errors.
|
|
4
|
-
|
|
5
|
-
A global `api` is available everywhere:
|
|
6
|
-
|
|
7
|
-
```js
|
|
8
|
-
const users = await api.get('/api/users');
|
|
9
|
-
await api.post('/api/users', { name: 'Aziz' });
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
You can also import it explicitly:
|
|
13
|
-
|
|
14
|
-
```js
|
|
15
|
-
import api, { ApiError } from '/_framework/api.js';
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Methods
|
|
19
|
-
|
|
20
|
-
```js
|
|
21
|
-
await api.get('/api/users');
|
|
22
|
-
await api.post('/api/users', { name: 'Aziz' });
|
|
23
|
-
await api.put('/api/users?id=7', { name: 'B' });
|
|
24
|
-
await api.patch('/api/users/7', { email: 'new@x.com' });
|
|
25
|
-
await api.delete('/api/users/7');
|
|
26
|
-
await api.del('/api/users/7'); // alias
|
|
27
|
-
await api.head('/api/users'); // returns the raw Response
|
|
28
|
-
await api.form('/api/upload', formData);// multipart/form-data
|
|
29
|
-
await api.raw('/api/users'); // returns the raw Response object
|
|
30
|
-
|
|
31
|
-
// Low-level escape hatch:
|
|
32
|
-
await api.request('/api/users', { method: 'POST', body: { a: 1 } });
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### Options (all methods)
|
|
36
|
-
|
|
37
|
-
```js
|
|
38
|
-
await api.get('/api/search', {
|
|
39
|
-
query: { q: 'aziz', tag: ['a', 'b'], skip: undefined }, // → ?q=aziz&tag=a&tag=b
|
|
40
|
-
headers: { Authorization: 'Bearer …' },
|
|
41
|
-
timeout: 5000, // ms, 0 disables
|
|
42
|
-
retries: 2, // for 5xx/429 only
|
|
43
|
-
signal, // external AbortSignal
|
|
44
|
-
raw: false, // true → return the Response, not parsed data
|
|
45
|
-
});
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
- **`query`** serializes objects; arrays become repeated params; `undefined`/`null` are skipped.
|
|
49
|
-
- **`raw`** returns the untouched `Response` instead of parsed JSON.
|
|
50
|
-
|
|
51
|
-
## Responses
|
|
52
|
-
|
|
53
|
-
- JSON responses are parsed automatically (`Content-Type: application/json`).
|
|
54
|
-
- Text-ish responses (`text/*`, html, xml) return the text.
|
|
55
|
-
- **`204`/`205` return `null`.**
|
|
56
|
-
|
|
57
|
-
## Errors
|
|
58
|
-
|
|
59
|
-
Failures throw an **`ApiError`** with `status`, `data`, and `url`:
|
|
60
|
-
|
|
61
|
-
```js
|
|
62
|
-
import { ApiError } from '/_framework/api.js';
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
await api.post('/api/users', bad);
|
|
66
|
-
} catch (err) {
|
|
67
|
-
if (err instanceof ApiError) {
|
|
68
|
-
if (err.status === 400) console.warn(err.message, err.data);
|
|
69
|
-
else if (err.status === 0) console.error('Network/timeout:', err.message);
|
|
70
|
-
else console.error('Server error', err.status);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
- `err.message` is extracted from the server's `error` / `message` / `detail` field when present.
|
|
76
|
-
- `err.status === 0` means network failure or **timeout** (`"Request timed out after …ms"`).
|
|
77
|
-
|
|
78
|
-
## Retry policy
|
|
79
|
-
|
|
80
|
-
| Status | Retried? |
|
|
81
|
-
|--------|----------|
|
|
82
|
-
| `4xx` | **No** (client errors are your bug — retrying won't help) |
|
|
83
|
-
| `429` | Yes (rate limit) |
|
|
84
|
-
| `5xx` | Yes |
|
|
85
|
-
| Network / timeout | Yes |
|
|
86
|
-
|
|
87
|
-
Backoff is `200ms, 400ms, 800ms…`. `retries` defaults to `0`; set it per request or globally.
|
|
88
|
-
|
|
89
|
-
```js
|
|
90
|
-
api.configure({ retries: 1, timeout: 10000 });
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Configuration
|
|
94
|
-
|
|
95
|
-
```js
|
|
96
|
-
api.setBaseURL('https://api.example.com'); // prefix for relative paths
|
|
97
|
-
api.setHeaders({ Authorization: `Bearer ${token}` });
|
|
98
|
-
api.configure({
|
|
99
|
-
baseURL: 'https://api.example.com',
|
|
100
|
-
timeout: 15000,
|
|
101
|
-
retries: 0,
|
|
102
|
-
headers: { 'X-App': 'aziz' },
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
console.log(api.defaults); // { baseURL, timeout, retries, headers }
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Defaults are merged under each request's own headers, so per-call `headers` win.
|
|
109
|
-
|
|
110
|
-
## Declarative fetching: `useFetch`
|
|
111
|
-
|
|
112
|
-
For read-only data with loading/error state built in, use the [hook](./05-hooks.md#usefetch--useapi):
|
|
113
|
-
|
|
114
|
-
```js
|
|
115
|
-
const { data, loading, error, refetch } = useFetch('/api/users');
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Use **`api`** for imperative flows (POST/PUT, custom retries, non-JSON bodies); use **`useFetch`** for declarative reads.
|
|
119
|
-
|
|
120
|
-
## Full example: form → API → UI
|
|
121
|
-
|
|
122
|
-
```js
|
|
123
|
-
function Contact() {
|
|
124
|
-
const [form, setForm] = useState({ name: '', message: '' });
|
|
125
|
-
const [status, setStatus] = useState('idle');
|
|
126
|
-
|
|
127
|
-
async function submit(e) {
|
|
128
|
-
e.preventDefault();
|
|
129
|
-
setStatus('sending');
|
|
130
|
-
try {
|
|
131
|
-
await api.post('/api/contact', form);
|
|
132
|
-
setStatus('sent');
|
|
133
|
-
} catch (err) {
|
|
134
|
-
setStatus('error: ' + err.message);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<form onSubmit={submit}>
|
|
140
|
-
<input value={form.name} onInput={e => setForm({ ...form, name: e.target.value })} />
|
|
141
|
-
<textarea value={form.message} onInput={e => setForm({ ...form, message: e.target.value })} />
|
|
142
|
-
<button disabled={status === 'sending'}>Send</button>
|
|
143
|
-
<p>{status}</p>
|
|
144
|
-
</form>
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
## Continue
|
|
150
|
-
|
|
151
|
-
→ [Styling](./08-styling.md)
|
|
1
|
+
# 07 · API Client
|
|
2
|
+
|
|
3
|
+
The API client is the browser-side companion to [API routes](./06-api-routes.md). It's a tiny, dependency-free `fetch` wrapper with JSON handling, timeouts, retries, and typed errors.
|
|
4
|
+
|
|
5
|
+
A global `api` is available everywhere:
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
const users = await api.get('/api/users');
|
|
9
|
+
await api.post('/api/users', { name: 'Aziz' });
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
You can also import it explicitly:
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import api, { ApiError } from '/_framework/api.js';
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Methods
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
await api.get('/api/users');
|
|
22
|
+
await api.post('/api/users', { name: 'Aziz' });
|
|
23
|
+
await api.put('/api/users?id=7', { name: 'B' });
|
|
24
|
+
await api.patch('/api/users/7', { email: 'new@x.com' });
|
|
25
|
+
await api.delete('/api/users/7');
|
|
26
|
+
await api.del('/api/users/7'); // alias
|
|
27
|
+
await api.head('/api/users'); // returns the raw Response
|
|
28
|
+
await api.form('/api/upload', formData);// multipart/form-data
|
|
29
|
+
await api.raw('/api/users'); // returns the raw Response object
|
|
30
|
+
|
|
31
|
+
// Low-level escape hatch:
|
|
32
|
+
await api.request('/api/users', { method: 'POST', body: { a: 1 } });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Options (all methods)
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
await api.get('/api/search', {
|
|
39
|
+
query: { q: 'aziz', tag: ['a', 'b'], skip: undefined }, // → ?q=aziz&tag=a&tag=b
|
|
40
|
+
headers: { Authorization: 'Bearer …' },
|
|
41
|
+
timeout: 5000, // ms, 0 disables
|
|
42
|
+
retries: 2, // for 5xx/429 only
|
|
43
|
+
signal, // external AbortSignal
|
|
44
|
+
raw: false, // true → return the Response, not parsed data
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- **`query`** serializes objects; arrays become repeated params; `undefined`/`null` are skipped.
|
|
49
|
+
- **`raw`** returns the untouched `Response` instead of parsed JSON.
|
|
50
|
+
|
|
51
|
+
## Responses
|
|
52
|
+
|
|
53
|
+
- JSON responses are parsed automatically (`Content-Type: application/json`).
|
|
54
|
+
- Text-ish responses (`text/*`, html, xml) return the text.
|
|
55
|
+
- **`204`/`205` return `null`.**
|
|
56
|
+
|
|
57
|
+
## Errors
|
|
58
|
+
|
|
59
|
+
Failures throw an **`ApiError`** with `status`, `data`, and `url`:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { ApiError } from '/_framework/api.js';
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await api.post('/api/users', bad);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err instanceof ApiError) {
|
|
68
|
+
if (err.status === 400) console.warn(err.message, err.data);
|
|
69
|
+
else if (err.status === 0) console.error('Network/timeout:', err.message);
|
|
70
|
+
else console.error('Server error', err.status);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `err.message` is extracted from the server's `error` / `message` / `detail` field when present.
|
|
76
|
+
- `err.status === 0` means network failure or **timeout** (`"Request timed out after …ms"`).
|
|
77
|
+
|
|
78
|
+
## Retry policy
|
|
79
|
+
|
|
80
|
+
| Status | Retried? |
|
|
81
|
+
|--------|----------|
|
|
82
|
+
| `4xx` | **No** (client errors are your bug — retrying won't help) |
|
|
83
|
+
| `429` | Yes (rate limit) |
|
|
84
|
+
| `5xx` | Yes |
|
|
85
|
+
| Network / timeout | Yes |
|
|
86
|
+
|
|
87
|
+
Backoff is `200ms, 400ms, 800ms…`. `retries` defaults to `0`; set it per request or globally.
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
api.configure({ retries: 1, timeout: 10000 });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
api.setBaseURL('https://api.example.com'); // prefix for relative paths
|
|
97
|
+
api.setHeaders({ Authorization: `Bearer ${token}` });
|
|
98
|
+
api.configure({
|
|
99
|
+
baseURL: 'https://api.example.com',
|
|
100
|
+
timeout: 15000,
|
|
101
|
+
retries: 0,
|
|
102
|
+
headers: { 'X-App': 'aziz' },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
console.log(api.defaults); // { baseURL, timeout, retries, headers }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Defaults are merged under each request's own headers, so per-call `headers` win.
|
|
109
|
+
|
|
110
|
+
## Declarative fetching: `useFetch`
|
|
111
|
+
|
|
112
|
+
For read-only data with loading/error state built in, use the [hook](./05-hooks.md#usefetch--useapi):
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
const { data, loading, error, refetch } = useFetch('/api/users');
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Use **`api`** for imperative flows (POST/PUT, custom retries, non-JSON bodies); use **`useFetch`** for declarative reads.
|
|
119
|
+
|
|
120
|
+
## Full example: form → API → UI
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
function Contact() {
|
|
124
|
+
const [form, setForm] = useState({ name: '', message: '' });
|
|
125
|
+
const [status, setStatus] = useState('idle');
|
|
126
|
+
|
|
127
|
+
async function submit(e) {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
setStatus('sending');
|
|
130
|
+
try {
|
|
131
|
+
await api.post('/api/contact', form);
|
|
132
|
+
setStatus('sent');
|
|
133
|
+
} catch (err) {
|
|
134
|
+
setStatus('error: ' + err.message);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<form onSubmit={submit}>
|
|
140
|
+
<input value={form.name} onInput={e => setForm({ ...form, name: e.target.value })} />
|
|
141
|
+
<textarea value={form.message} onInput={e => setForm({ ...form, message: e.target.value })} />
|
|
142
|
+
<button disabled={status === 'sending'}>Send</button>
|
|
143
|
+
<p>{status}</p>
|
|
144
|
+
</form>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Continue
|
|
150
|
+
|
|
151
|
+
→ [Styling](./08-styling.md)
|
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
# 08 · Styling
|
|
2
|
-
|
|
3
|
-
Aziz.js ships plain CSS. You can also opt into Bootstrap or Tailwind from the CLI.
|
|
4
|
-
|
|
5
|
-
## Global stylesheet
|
|
6
|
-
|
|
7
|
-
`index.html` links your main stylesheet:
|
|
8
|
-
|
|
9
|
-
```html
|
|
10
|
-
<link rel="stylesheet" href="/styles/globals.css">
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Add more by editing `index.html` (or inject `<link>` tags dynamically):
|
|
14
|
-
|
|
15
|
-
```html
|
|
16
|
-
<head>
|
|
17
|
-
<link rel="stylesheet" href="/styles/globals.css">
|
|
18
|
-
<link rel="stylesheet" href="/styles/theme.css">
|
|
19
|
-
</head>
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Inline styles
|
|
23
|
-
|
|
24
|
-
Use the `style` prop with an object (React-style), or a string:
|
|
25
|
-
|
|
26
|
-
```jsx
|
|
27
|
-
<div style={{ padding: '40px', maxWidth: '900px', margin: '0 auto' }} />
|
|
28
|
-
<div style="color: red" />
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Class names
|
|
32
|
-
|
|
33
|
-
Both `class` and `className` work:
|
|
34
|
-
|
|
35
|
-
```jsx
|
|
36
|
-
<div class="card" />
|
|
37
|
-
<div className="card" />
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## CSS frameworks via the CLI
|
|
41
|
-
|
|
42
|
-
Pick one at creation time:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
npx create-aziz-app my-app --css bootstrap
|
|
46
|
-
npx create-aziz-app my-app --css tailwind
|
|
47
|
-
npx create-aziz-app my-app --css none
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
- **bootstrap** — adds Bootstrap 5 + Bootstrap Icons (CDN `<link>`/`<script>` injected into `index.html`).
|
|
51
|
-
- **tailwind** — adds the Tailwind CDN `<script>` plus a `tailwind.config` block with `primary`/`dark` colors.
|
|
52
|
-
- **none** — just your `globals.css`.
|
|
53
|
-
|
|
54
|
-
The flag injects tags into `index.html` before `</head>` / `</body>`. Because these are CDNs, an internet connection is required at runtime.
|
|
55
|
-
|
|
56
|
-
> Prefer to install CSS locally (e.g. `npm i tailwindcss` + a build step) if you need offline/production builds — the CDN approach is for speed in development.
|
|
57
|
-
|
|
58
|
-
## Scoped styles?
|
|
59
|
-
|
|
60
|
-
There's no built-in CSS Modules / scoped styles. Options:
|
|
61
|
-
|
|
62
|
-
1. **BEM or a naming convention** in `globals.css`.
|
|
63
|
-
2. **Inline `style` objects** for truly one-off layout.
|
|
64
|
-
3. **A `<style>` tag** rendered inside a component (works, but it's global unless you namespace the selectors).
|
|
65
|
-
|
|
66
|
-
## Theming with context
|
|
67
|
-
|
|
68
|
-
Because hooks and context are global, a dark-mode toggle is easy:
|
|
69
|
-
|
|
70
|
-
```js
|
|
71
|
-
const ThemeContext = createContext('light');
|
|
72
|
-
|
|
73
|
-
function Root({ children }) {
|
|
74
|
-
const [theme, setTheme] = useLocalStorage('theme', 'light');
|
|
75
|
-
return (
|
|
76
|
-
<ThemeContext.Provider value={theme}>
|
|
77
|
-
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
|
|
78
|
-
Toggle theme
|
|
79
|
-
</button>
|
|
80
|
-
{children}
|
|
81
|
-
</ThemeContext.Provider>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Set `document.body.dataset.theme = theme` and switch colors in CSS:
|
|
87
|
-
|
|
88
|
-
```css
|
|
89
|
-
[data-theme="dark"] { background: #111; color: #eee; }
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Continue
|
|
93
|
-
|
|
94
|
-
→ [Imports & Modules](./09-imports-and-modules.md)
|
|
1
|
+
# 08 · Styling
|
|
2
|
+
|
|
3
|
+
Aziz.js ships plain CSS. You can also opt into Bootstrap or Tailwind from the CLI.
|
|
4
|
+
|
|
5
|
+
## Global stylesheet
|
|
6
|
+
|
|
7
|
+
`index.html` links your main stylesheet:
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<link rel="stylesheet" href="/styles/globals.css">
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Add more by editing `index.html` (or inject `<link>` tags dynamically):
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<head>
|
|
17
|
+
<link rel="stylesheet" href="/styles/globals.css">
|
|
18
|
+
<link rel="stylesheet" href="/styles/theme.css">
|
|
19
|
+
</head>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Inline styles
|
|
23
|
+
|
|
24
|
+
Use the `style` prop with an object (React-style), or a string:
|
|
25
|
+
|
|
26
|
+
```jsx
|
|
27
|
+
<div style={{ padding: '40px', maxWidth: '900px', margin: '0 auto' }} />
|
|
28
|
+
<div style="color: red" />
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Class names
|
|
32
|
+
|
|
33
|
+
Both `class` and `className` work:
|
|
34
|
+
|
|
35
|
+
```jsx
|
|
36
|
+
<div class="card" />
|
|
37
|
+
<div className="card" />
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## CSS frameworks via the CLI
|
|
41
|
+
|
|
42
|
+
Pick one at creation time:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx create-aziz-app my-app --css bootstrap
|
|
46
|
+
npx create-aziz-app my-app --css tailwind
|
|
47
|
+
npx create-aziz-app my-app --css none
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- **bootstrap** — adds Bootstrap 5 + Bootstrap Icons (CDN `<link>`/`<script>` injected into `index.html`).
|
|
51
|
+
- **tailwind** — adds the Tailwind CDN `<script>` plus a `tailwind.config` block with `primary`/`dark` colors.
|
|
52
|
+
- **none** — just your `globals.css`.
|
|
53
|
+
|
|
54
|
+
The flag injects tags into `index.html` before `</head>` / `</body>`. Because these are CDNs, an internet connection is required at runtime.
|
|
55
|
+
|
|
56
|
+
> Prefer to install CSS locally (e.g. `npm i tailwindcss` + a build step) if you need offline/production builds — the CDN approach is for speed in development.
|
|
57
|
+
|
|
58
|
+
## Scoped styles?
|
|
59
|
+
|
|
60
|
+
There's no built-in CSS Modules / scoped styles. Options:
|
|
61
|
+
|
|
62
|
+
1. **BEM or a naming convention** in `globals.css`.
|
|
63
|
+
2. **Inline `style` objects** for truly one-off layout.
|
|
64
|
+
3. **A `<style>` tag** rendered inside a component (works, but it's global unless you namespace the selectors).
|
|
65
|
+
|
|
66
|
+
## Theming with context
|
|
67
|
+
|
|
68
|
+
Because hooks and context are global, a dark-mode toggle is easy:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
const ThemeContext = createContext('light');
|
|
72
|
+
|
|
73
|
+
function Root({ children }) {
|
|
74
|
+
const [theme, setTheme] = useLocalStorage('theme', 'light');
|
|
75
|
+
return (
|
|
76
|
+
<ThemeContext.Provider value={theme}>
|
|
77
|
+
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
|
|
78
|
+
Toggle theme
|
|
79
|
+
</button>
|
|
80
|
+
{children}
|
|
81
|
+
</ThemeContext.Provider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Set `document.body.dataset.theme = theme` and switch colors in CSS:
|
|
87
|
+
|
|
88
|
+
```css
|
|
89
|
+
[data-theme="dark"] { background: #111; color: #eee; }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Continue
|
|
93
|
+
|
|
94
|
+
→ [Imports & Modules](./09-imports-and-modules.md)
|