@codesuma/baseline 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 +648 -0
- package/components/advanced/page.module.css +57 -0
- package/components/advanced/page.ts +53 -0
- package/components/base.ts +45 -0
- package/components/native/a.ts +15 -0
- package/components/native/button.ts +17 -0
- package/components/native/canvas.ts +15 -0
- package/components/native/div.ts +11 -0
- package/components/native/h.ts +17 -0
- package/components/native/img.ts +17 -0
- package/components/native/input.ts +29 -0
- package/components/native/option.ts +8 -0
- package/components/native/p.ts +11 -0
- package/components/native/select.ts +15 -0
- package/components/native/span.ts +11 -0
- package/components/native/svg.ts +18 -0
- package/helpers/date.ts +17 -0
- package/helpers/device.ts +13 -0
- package/helpers/format.ts +13 -0
- package/helpers/locales.ts +15 -0
- package/helpers/regex.ts +12 -0
- package/index.ts +35 -0
- package/lib/http.ts +96 -0
- package/lib/idb.ts +135 -0
- package/lib/ldb.ts +44 -0
- package/lib/router.ts +179 -0
- package/lib/state.ts +35 -0
- package/package.json +35 -0
- package/utils/appender.ts +141 -0
- package/utils/emitter.ts +63 -0
- package/utils/id.ts +13 -0
- package/utils/mounter.ts +8 -0
- package/utils/ripple.ts +59 -0
- package/utils/styler.ts +56 -0
- package/utils/wait.ts +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
# Base
|
|
2
|
+
|
|
3
|
+
A minimal, imperative UI framework for building fast web apps. No virtual DOM, no magic, no dependencies.
|
|
4
|
+
|
|
5
|
+
**~1000 lines** of TypeScript that gives you everything you need for SPAs.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @codesuma/baseline
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Why Base?
|
|
14
|
+
|
|
15
|
+
- **Predictable** - What you write is what happens. No hidden lifecycle, no reactivity magic.
|
|
16
|
+
- **Fast** - Direct DOM manipulation. No diffing algorithms.
|
|
17
|
+
- **Tiny** - The entire framework is smaller than most framework's "hello world" bundle.
|
|
18
|
+
- **Stable** - No dependencies means no breaking changes from upstream.
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { Div, Button, router } from '@nichobase/base'
|
|
24
|
+
|
|
25
|
+
const App = () => {
|
|
26
|
+
const app = Div()
|
|
27
|
+
const count = { value: 0 }
|
|
28
|
+
|
|
29
|
+
const counter = Div('0')
|
|
30
|
+
counter.style({ fontSize: '48px', textAlign: 'center' })
|
|
31
|
+
|
|
32
|
+
const btn = Button('Click me')
|
|
33
|
+
btn.on('click', () => {
|
|
34
|
+
count.value++
|
|
35
|
+
counter.text(String(count.value))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
app.style({ display: 'flex', flexDirection: 'column', gap: '20px', padding: '40px' })
|
|
39
|
+
app.append(counter, btn)
|
|
40
|
+
|
|
41
|
+
return app
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
document.body.appendChild(App().el)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Core Concepts
|
|
48
|
+
|
|
49
|
+
### Components
|
|
50
|
+
|
|
51
|
+
Every component is created with the `Base()` factory:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { Base } from 'base/components/base'
|
|
55
|
+
|
|
56
|
+
const card = Base('div') // Creates a <div>
|
|
57
|
+
card.el // The actual DOM element
|
|
58
|
+
card.id // Unique identifier
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or use pre-made native components:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { Div, Button, Input, Span, A, Img } from 'base/components/native'
|
|
65
|
+
|
|
66
|
+
const container = Div('Hello')
|
|
67
|
+
const btn = Button('Click')
|
|
68
|
+
const input = Input('Enter name...', 'text')
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Events
|
|
72
|
+
|
|
73
|
+
Components have a built-in event emitter:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const btn = Button()
|
|
77
|
+
|
|
78
|
+
// Listen to events
|
|
79
|
+
btn.on('click', () => console.log('clicked!'))
|
|
80
|
+
btn.on('mounted', () => console.log('in the DOM'))
|
|
81
|
+
|
|
82
|
+
// Listen once
|
|
83
|
+
btn.once('click', () => console.log('first click only'))
|
|
84
|
+
|
|
85
|
+
// Emit custom events
|
|
86
|
+
btn.emit('my-event', { data: 123 })
|
|
87
|
+
|
|
88
|
+
// Remove listener
|
|
89
|
+
btn.off('click', handler)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Styling
|
|
93
|
+
|
|
94
|
+
Two approaches: inline styles or CSS Modules.
|
|
95
|
+
|
|
96
|
+
#### Inline Styles
|
|
97
|
+
|
|
98
|
+
Use for dynamic values and simple components:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Inline styles
|
|
102
|
+
card.style({
|
|
103
|
+
opacity: '0.5',
|
|
104
|
+
transform: 'translateY(-10px)',
|
|
105
|
+
transition: 'all 0.3s ease',
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Chained styles
|
|
109
|
+
card.style({ opacity: '0' })
|
|
110
|
+
.style({ opacity: '1' })
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### CSS Modules (Recommended)
|
|
114
|
+
|
|
115
|
+
Use `.module.css` files for scoped, maintainable styles:
|
|
116
|
+
|
|
117
|
+
**card/index.module.css**
|
|
118
|
+
```css
|
|
119
|
+
.base {
|
|
120
|
+
background: #fff;
|
|
121
|
+
border-radius: 12px;
|
|
122
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
123
|
+
padding: 16px;
|
|
124
|
+
transition: all 0.3s ease;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.base:hover {
|
|
128
|
+
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
|
129
|
+
transform: translateY(-4px);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.dark {
|
|
133
|
+
background: #1a1a1a;
|
|
134
|
+
color: #fff;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**card/index.ts**
|
|
139
|
+
```typescript
|
|
140
|
+
import { Div } from '../../base/components/native/div'
|
|
141
|
+
import styles from './index.module.css'
|
|
142
|
+
|
|
143
|
+
export const Card = () => {
|
|
144
|
+
const base = Div()
|
|
145
|
+
base.addClass(styles.base)
|
|
146
|
+
|
|
147
|
+
// Toggle classes
|
|
148
|
+
base.addClass(styles.dark) // Add dark mode
|
|
149
|
+
base.removeClass(styles.dark) // Remove dark mode
|
|
150
|
+
base.toggleClass(styles.dark, isDark) // Conditional
|
|
151
|
+
|
|
152
|
+
return base
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### Project Structure with CSS Modules
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
components/
|
|
160
|
+
├── card/
|
|
161
|
+
│ ├── index.ts # Component logic
|
|
162
|
+
│ └── index.module.css # Scoped styles
|
|
163
|
+
├── button/
|
|
164
|
+
│ ├── index.ts
|
|
165
|
+
│ └── index.module.css
|
|
166
|
+
pages/
|
|
167
|
+
├── home/
|
|
168
|
+
│ ├── index.ts
|
|
169
|
+
│ └── index.module.css
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### Rollup Configuration
|
|
173
|
+
|
|
174
|
+
Ensure your `rollup.config.js` includes postcss:
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
import postcss from 'rollup-plugin-postcss'
|
|
178
|
+
|
|
179
|
+
export default {
|
|
180
|
+
plugins: [
|
|
181
|
+
postcss({
|
|
182
|
+
modules: true, // Enable CSS modules
|
|
183
|
+
extract: 'bundle.css', // Output file
|
|
184
|
+
minimize: true,
|
|
185
|
+
}),
|
|
186
|
+
],
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Link the CSS in your HTML:
|
|
191
|
+
```html
|
|
192
|
+
<link rel="stylesheet" href="/bundle.css">
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### DOM Tree
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
const list = Div()
|
|
199
|
+
const item1 = Div('First')
|
|
200
|
+
const item2 = Div('Second')
|
|
201
|
+
|
|
202
|
+
// Append children
|
|
203
|
+
list.append(item1, item2)
|
|
204
|
+
|
|
205
|
+
// Prepend
|
|
206
|
+
list.prepend(Div('Zero'))
|
|
207
|
+
|
|
208
|
+
// Conditional rendering
|
|
209
|
+
list.append(
|
|
210
|
+
isLoggedIn && UserProfile(),
|
|
211
|
+
showFooter ? Footer() : null
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
// Remove
|
|
215
|
+
item1.remove()
|
|
216
|
+
|
|
217
|
+
// Empty all children
|
|
218
|
+
list.empty()
|
|
219
|
+
|
|
220
|
+
// Access children
|
|
221
|
+
list.getChildren()
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Lifecycle
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
const card = Div()
|
|
228
|
+
|
|
229
|
+
card.on('mounted', () => {
|
|
230
|
+
// Called when component enters the DOM
|
|
231
|
+
console.log('Card is visible')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
card.on('unmounted', () => {
|
|
235
|
+
// Called when component is removed
|
|
236
|
+
console.log('Card removed')
|
|
237
|
+
})
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Router
|
|
241
|
+
|
|
242
|
+
Base includes a full-featured SPA router with page lifecycle management. Pages stay in DOM and visibility toggles for smooth transitions.
|
|
243
|
+
|
|
244
|
+
### Basic Setup
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { Div } from 'base/components/native/div'
|
|
248
|
+
import router from 'base/lib/router'
|
|
249
|
+
import { HomePage } from './pages/home'
|
|
250
|
+
import { AboutPage } from './pages/about'
|
|
251
|
+
import { UserPage } from './pages/user'
|
|
252
|
+
|
|
253
|
+
const view = Div() // Container for pages
|
|
254
|
+
const app = Div()
|
|
255
|
+
app.append(view)
|
|
256
|
+
|
|
257
|
+
// Configure routes - pages stay in DOM, visibility toggles
|
|
258
|
+
router.routes({
|
|
259
|
+
'/': HomePage,
|
|
260
|
+
'/about': AboutPage,
|
|
261
|
+
'/users/:id': UserPage,
|
|
262
|
+
}, view)
|
|
263
|
+
|
|
264
|
+
document.body.appendChild(app.el)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Route Configuration
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
router.routes({
|
|
271
|
+
// Simple routes
|
|
272
|
+
'/': HomePage,
|
|
273
|
+
'/about': AboutPage,
|
|
274
|
+
|
|
275
|
+
// Dynamic parameters
|
|
276
|
+
'/users/:id': UserPage,
|
|
277
|
+
'/posts/:postId/comments/:commentId': CommentPage,
|
|
278
|
+
}, view)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### How It Works
|
|
282
|
+
|
|
283
|
+
- All pages are created once and stay in DOM
|
|
284
|
+
- Navigation toggles visibility via CSS classes (`.enter` / `.exit`)
|
|
285
|
+
- Smooth transitions because both pages exist during animation
|
|
286
|
+
- State preserved (scroll position, form inputs)
|
|
287
|
+
|
|
288
|
+
### Page Component
|
|
289
|
+
|
|
290
|
+
Base includes a `Page` component with built-in CSS transitions for smooth page navigation:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { Page } from 'base/components/advanced/page'
|
|
294
|
+
import { IRouteEvent } from 'base/lib/router'
|
|
295
|
+
|
|
296
|
+
export const AboutPage = () => {
|
|
297
|
+
const page = Page() // Comes with enter/exit animations
|
|
298
|
+
|
|
299
|
+
// Add your content
|
|
300
|
+
page.append(header, body)
|
|
301
|
+
|
|
302
|
+
// Uses default animations - slide up on enter, slide up on exit
|
|
303
|
+
return page
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**Default animations:**
|
|
308
|
+
- Enter: Slides up from bottom, fades in
|
|
309
|
+
- Exit: Slides up, fades out
|
|
310
|
+
|
|
311
|
+
### Customizing Page Transitions
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import { Page } from 'base/components/advanced/page'
|
|
315
|
+
import { IRouteEvent } from 'base/lib/router'
|
|
316
|
+
|
|
317
|
+
export const HomePage = () => {
|
|
318
|
+
const page = Page()
|
|
319
|
+
|
|
320
|
+
// Override default enter - show instantly
|
|
321
|
+
page.off('enter')
|
|
322
|
+
page.on('enter', async ({ from }: IRouteEvent) => {
|
|
323
|
+
page.showInstant()
|
|
324
|
+
// Load data...
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
return page
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export const AboutPage = () => {
|
|
331
|
+
const page = Page()
|
|
332
|
+
|
|
333
|
+
// Custom exit - slide DOWN instead of up (for back navigation)
|
|
334
|
+
page.off('exit')
|
|
335
|
+
page.on('exit', async () => {
|
|
336
|
+
await page.exitDown()
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
return page
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Page methods:**
|
|
344
|
+
- `showInstant()` - Show page without animation
|
|
345
|
+
- `exitDown()` - Exit with downward slide (for back navigation)
|
|
346
|
+
|
|
347
|
+
### Page Lifecycle Events
|
|
348
|
+
|
|
349
|
+
Pages receive `enter` and `exit` events from the router:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
page.on('enter', async ({ params, query, from, data }: IRouteEvent) => {
|
|
353
|
+
// params: { id: '123' } for /users/:id
|
|
354
|
+
// query: { sort: 'name' } for ?sort=name
|
|
355
|
+
// from: '/home' - previous path
|
|
356
|
+
// data: custom data from router.goto()
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
page.on('exit', async () => {
|
|
360
|
+
// Called when navigating away
|
|
361
|
+
// Async supported - router waits for completion
|
|
362
|
+
})
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Navigation
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import router from 'base/lib/router'
|
|
369
|
+
|
|
370
|
+
// Navigate to path
|
|
371
|
+
router.goto('/about')
|
|
372
|
+
|
|
373
|
+
// With query params
|
|
374
|
+
router.goto('/search?q=hello&page=2')
|
|
375
|
+
|
|
376
|
+
// With custom data (available in enter event)
|
|
377
|
+
router.goto('/users/123', { fromNotification: true })
|
|
378
|
+
|
|
379
|
+
// Browser history
|
|
380
|
+
router.back()
|
|
381
|
+
router.forward()
|
|
382
|
+
|
|
383
|
+
// Get current state
|
|
384
|
+
router.getPath() // '/users/123'
|
|
385
|
+
router.getParams() // { id: '123' }
|
|
386
|
+
router.getQuery() // { sort: 'name' }
|
|
387
|
+
router.getQuery('sort') // 'name'
|
|
388
|
+
|
|
389
|
+
// For heavy pages - manually destroy to force recreation
|
|
390
|
+
router.destroyPage('/heavy-page')
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Resetting Heavy Pages
|
|
394
|
+
|
|
395
|
+
Since pages stay in DOM, you may want to reset state on exit for heavy pages:
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
page.on('exit', () => {
|
|
399
|
+
// Reset scroll position
|
|
400
|
+
page.el.scrollTop = 0
|
|
401
|
+
|
|
402
|
+
// Clear dynamic content
|
|
403
|
+
list.empty()
|
|
404
|
+
|
|
405
|
+
// Reset form inputs
|
|
406
|
+
input.val('')
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
// Or completely destroy the page to force recreation on next visit:
|
|
410
|
+
page.on('exit', () => {
|
|
411
|
+
router.destroyPage('/heavy-page')
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
### Listening to Route Changes
|
|
415
|
+
|
|
416
|
+
For global navigation handling:
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
router.on('change', ({ path, params, query, from, data }) => {
|
|
420
|
+
// Update navigation UI
|
|
421
|
+
updateActiveMenuItem(path)
|
|
422
|
+
|
|
423
|
+
// Analytics
|
|
424
|
+
trackPageView(path)
|
|
425
|
+
|
|
426
|
+
// Show/hide back button
|
|
427
|
+
backButton.style({ display: from ? 'block' : 'none' })
|
|
428
|
+
})
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Complete Example
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
// app.ts
|
|
435
|
+
import { Div } from './base/components/native/div'
|
|
436
|
+
import router from './base/lib/router'
|
|
437
|
+
import { FIXED, EASE } from './base/helpers/style'
|
|
438
|
+
import { HomePage } from './pages/home'
|
|
439
|
+
import { AboutPage } from './pages/about'
|
|
440
|
+
import { UserPage } from './pages/user'
|
|
441
|
+
|
|
442
|
+
const view = Div()
|
|
443
|
+
const app = Div()
|
|
444
|
+
const nav = Div()
|
|
445
|
+
|
|
446
|
+
// Navigation
|
|
447
|
+
const homeLink = Div('Home')
|
|
448
|
+
homeLink.on('click', () => router.goto('/'))
|
|
449
|
+
|
|
450
|
+
const aboutLink = Div('About')
|
|
451
|
+
aboutLink.on('click', () => router.goto('/about'))
|
|
452
|
+
|
|
453
|
+
nav.style({ display: 'flex', gap: '20px', padding: '10px' })
|
|
454
|
+
nav.append(homeLink, aboutLink)
|
|
455
|
+
|
|
456
|
+
app.style({ ...FIXED })
|
|
457
|
+
app.append(nav, view)
|
|
458
|
+
|
|
459
|
+
// Configure routes
|
|
460
|
+
router.routes({
|
|
461
|
+
'/': { page: HomePage, cache: true },
|
|
462
|
+
'/about': { page: AboutPage, cache: false },
|
|
463
|
+
'/users/:id': { page: UserPage, cache: true },
|
|
464
|
+
}, view)
|
|
465
|
+
|
|
466
|
+
export default app
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
// pages/user/index.ts
|
|
471
|
+
import { Div } from '../../base/components/native/div'
|
|
472
|
+
import http from '../../base/lib/http'
|
|
473
|
+
|
|
474
|
+
export const UserPage = () => {
|
|
475
|
+
const page = Div()
|
|
476
|
+
const name = Div()
|
|
477
|
+
const email = Div()
|
|
478
|
+
|
|
479
|
+
page.append(name, email)
|
|
480
|
+
|
|
481
|
+
page.on('enter', async ({ params }) => {
|
|
482
|
+
const { data } = await http.get(`/api/users/${params.id}`)
|
|
483
|
+
name.text(data.name)
|
|
484
|
+
email.text(data.email)
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
return page
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## HTTP Client
|
|
492
|
+
|
|
493
|
+
Built-in fetch wrapper with request deduplication:
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
import http from 'base/lib/http'
|
|
497
|
+
|
|
498
|
+
// GET (automatically deduplicated - same URL = same request)
|
|
499
|
+
const { status, data } = await http.get('/api/users')
|
|
500
|
+
|
|
501
|
+
// POST, PUT, PATCH, DELETE
|
|
502
|
+
await http.post('/api/users', { name: 'John' })
|
|
503
|
+
await http.put('/api/users/1', { name: 'Jane' })
|
|
504
|
+
await http.patch('/api/users/1', { active: true })
|
|
505
|
+
await http.delete('/api/users/1')
|
|
506
|
+
|
|
507
|
+
// With auth
|
|
508
|
+
await http.get('/api/me', { auth: 'my-token' })
|
|
509
|
+
|
|
510
|
+
// Upload with progress
|
|
511
|
+
await http.upload('/api/upload', file, {
|
|
512
|
+
onProgress: (loaded, total) => {
|
|
513
|
+
console.log(`${Math.round(loaded/total * 100)}%`)
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
## Storage
|
|
519
|
+
|
|
520
|
+
### localStorage
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
import ldb from 'base/lib/ldb'
|
|
524
|
+
|
|
525
|
+
ldb.set('user', { name: 'John' }) // Auto JSON stringify
|
|
526
|
+
ldb.get('user') // Auto JSON parse
|
|
527
|
+
ldb.remove('user')
|
|
528
|
+
ldb.clear()
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### IndexedDB
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
import createDB from 'base/lib/idb'
|
|
535
|
+
|
|
536
|
+
const db = createDB('my-app')
|
|
537
|
+
|
|
538
|
+
// Create store (run once on app init)
|
|
539
|
+
await db.createStore('users', 1, { keyPath: 'id', indices: ['email'] })
|
|
540
|
+
|
|
541
|
+
// CRUD
|
|
542
|
+
await db.save('users', { id: '1', name: 'John', email: 'john@example.com' })
|
|
543
|
+
const user = await db.get('users', '1')
|
|
544
|
+
const allUsers = await db.all('users')
|
|
545
|
+
await db.update('users', { id: '1', name: 'Jane' })
|
|
546
|
+
await db.delete('users', '1')
|
|
547
|
+
|
|
548
|
+
// Query
|
|
549
|
+
const results = await db.find('users', {
|
|
550
|
+
index: 'email',
|
|
551
|
+
value: 'john@example.com',
|
|
552
|
+
limit: 10,
|
|
553
|
+
reverse: true
|
|
554
|
+
})
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Global State
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
import state from 'base/lib/state'
|
|
561
|
+
|
|
562
|
+
// Simple get/set
|
|
563
|
+
state.set('user', { name: 'John', id: 123 })
|
|
564
|
+
state.get('user') // { name: 'John', id: 123 }
|
|
565
|
+
|
|
566
|
+
// Subscribe to changes
|
|
567
|
+
state.on('user', (user) => {
|
|
568
|
+
console.log('User changed:', user)
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
// Subscribe to any change
|
|
572
|
+
state.on('change', ({ key, value }) => {
|
|
573
|
+
console.log(`${key} changed`)
|
|
574
|
+
})
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
## Helpers
|
|
578
|
+
|
|
579
|
+
### Style Helpers
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
import { ABSOLUTE, FIXED, FLEX, CENTER, HIDE, SHOW, EASE, WH, X, Y, SCALE, ROTATE } from 'base/helpers/style'
|
|
583
|
+
|
|
584
|
+
card.style({ ...ABSOLUTE }) // position: absolute; inset: 0;
|
|
585
|
+
card.style({ ...CENTER }) // display: flex; align-items: center; justify-content: center;
|
|
586
|
+
card.style({ ...EASE(0.3) }) // transition: all 0.3s ease;
|
|
587
|
+
card.style({ ...WH(100, 50) }) // width: 100px; height: 50px;
|
|
588
|
+
card.style({ ...Y(-10) }) // transform: translateY(-10px);
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Ripple Effect
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
import { withRipple } from 'base/utils/ripple'
|
|
595
|
+
import { Button } from 'base/components/native/button'
|
|
596
|
+
|
|
597
|
+
const btn = withRipple(Button('Click me'))
|
|
598
|
+
// Now has Material Design ripple effect on click/touch
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Device Detection
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
import { isMobile, isTouch, isIOS, isAndroid } from 'base/helpers/device'
|
|
605
|
+
|
|
606
|
+
if (isMobile()) { /* mobile layout */ }
|
|
607
|
+
if (isTouch()) { /* touch interactions */ }
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Validation
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
import { isEmail, isUrl, isPhone, isStrongPassword } from 'base/helpers/regex'
|
|
614
|
+
|
|
615
|
+
if (isEmail(input.value())) { /* valid */ }
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
## Project Structure
|
|
619
|
+
|
|
620
|
+
```
|
|
621
|
+
my-app/
|
|
622
|
+
├── base/ # The framework (copy or npm install)
|
|
623
|
+
├── components/ # Your reusable components
|
|
624
|
+
│ └── card/
|
|
625
|
+
│ └── index.module.css # CSS module
|
|
626
|
+
│ └── index.ts # Component
|
|
627
|
+
├── pages/ # Page components
|
|
628
|
+
│ ├── home/
|
|
629
|
+
│ │ └── index.ts
|
|
630
|
+
│ └── about/
|
|
631
|
+
│ └── index.ts
|
|
632
|
+
├── services/ # API, state management
|
|
633
|
+
├── styles/ # CSS files
|
|
634
|
+
├── app.ts # App setup with router
|
|
635
|
+
├── index.ts # Entry point
|
|
636
|
+
└── index.html
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
## Philosophy
|
|
640
|
+
|
|
641
|
+
1. **Imperative over declarative** - You control the DOM directly
|
|
642
|
+
2. **Explicit over implicit** - No hidden state updates or re-renders
|
|
643
|
+
3. **Composition over inheritance** - Mix capabilities with Object.assign
|
|
644
|
+
4. **Simplicity over features** - Small API surface, easy to learn
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
MIT License
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
.page {
|
|
2
|
+
position: absolute;
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
top: 0;
|
|
7
|
+
left: 0;
|
|
8
|
+
right: 0;
|
|
9
|
+
bottom: 0;
|
|
10
|
+
padding-top: env(safe-area-inset-top);
|
|
11
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
12
|
+
background-color: #fff;
|
|
13
|
+
z-index: 100;
|
|
14
|
+
will-change: opacity, transform;
|
|
15
|
+
transition: opacity 0.16s ease, transform 0.16s ease;
|
|
16
|
+
transform: translateY(60px);
|
|
17
|
+
pointer-events: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Forward enter: from bottom */
|
|
21
|
+
.page.enterUp {
|
|
22
|
+
opacity: 1;
|
|
23
|
+
transform: translateY(0);
|
|
24
|
+
pointer-events: auto;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Back enter: from top */
|
|
28
|
+
.page.enterDown {
|
|
29
|
+
opacity: 1;
|
|
30
|
+
transform: translateY(0);
|
|
31
|
+
pointer-events: auto;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Forward exit: to top (hide above screen) */
|
|
35
|
+
.page.exitUp {
|
|
36
|
+
opacity: 0;
|
|
37
|
+
transform: translateY(-60px);
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Back exit: to bottom (hide below screen) */
|
|
42
|
+
.page.exitDown {
|
|
43
|
+
opacity: 0;
|
|
44
|
+
transform: translateY(60px);
|
|
45
|
+
pointer-events: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Initial hidden states for stacking */
|
|
49
|
+
.page.hiddenBelow {
|
|
50
|
+
transform: translateY(-60px);
|
|
51
|
+
opacity: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.page.hiddenAbove {
|
|
55
|
+
transform: translateY(60px);
|
|
56
|
+
opacity: 0;
|
|
57
|
+
}
|