@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 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
+ }