@codesuma/baseline 1.0.18 → 1.1.0
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 +103 -7
- package/components/base.ts +0 -3
- package/index.ts +2 -2
- package/lib/idb.ts +223 -168
- package/lib/ldb.ts +3 -19
- package/lib/router.ts +128 -120
- package/package.json +34 -34
- package/utils/appender.ts +25 -6
- package/utils/styler.ts +19 -0
package/README.md
CHANGED
|
@@ -548,18 +548,28 @@ await db.createStore('users', 1, { keyPath: 'id', indices: ['email'] })
|
|
|
548
548
|
|
|
549
549
|
// CRUD
|
|
550
550
|
await db.save('users', { id: '1', name: 'John', email: 'john@example.com' })
|
|
551
|
+
await db.save('users', [item1, item2, item3]) // Batch save
|
|
551
552
|
const user = await db.get('users', '1')
|
|
552
553
|
const allUsers = await db.all('users')
|
|
553
|
-
await db.update('users', { id: '1', name: 'Jane' })
|
|
554
554
|
await db.delete('users', '1')
|
|
555
|
+
await db.clear('users')
|
|
556
|
+
await db.count('users')
|
|
555
557
|
|
|
556
|
-
// Query
|
|
558
|
+
// Query with options
|
|
557
559
|
const results = await db.find('users', {
|
|
558
560
|
index: 'email',
|
|
559
|
-
value: 'john@example.com',
|
|
561
|
+
value: 'john@example.com', // Exact match
|
|
560
562
|
limit: 10,
|
|
561
563
|
reverse: true
|
|
562
564
|
})
|
|
565
|
+
|
|
566
|
+
// Range queries
|
|
567
|
+
await db.find('products', { index: 'price', gt: 10, lt: 100 })
|
|
568
|
+
await db.find('products', { index: 'price', between: [10, 100] })
|
|
569
|
+
await db.find('products', { index: 'price', gte: 50 })
|
|
570
|
+
|
|
571
|
+
// Close connection when done (auto-closes after 2s idle)
|
|
572
|
+
db.close()
|
|
563
573
|
```
|
|
564
574
|
|
|
565
575
|
### Global State
|
|
@@ -630,25 +640,111 @@ if (isEmail(input.value())) { /* valid */ }
|
|
|
630
640
|
|
|
631
641
|
## Project Structure
|
|
632
642
|
|
|
643
|
+
This repository uses a mono-repo layout with npm workspaces:
|
|
644
|
+
|
|
645
|
+
```
|
|
646
|
+
baseline/
|
|
647
|
+
├── packages/
|
|
648
|
+
│ └── baseline/ # The npm package (@codesuma/baseline)
|
|
649
|
+
│ ├── components/
|
|
650
|
+
│ │ ├── base.ts # Core factory
|
|
651
|
+
│ │ ├── native/ # Div, Button, Input, etc.
|
|
652
|
+
│ │ └── advanced/ # Page (with transitions)
|
|
653
|
+
│ ├── lib/ # Router, HTTP, State, Storage
|
|
654
|
+
│ ├── utils/ # Emitter, Appender, Styler
|
|
655
|
+
│ ├── helpers/ # Date, Device, Format, Regex
|
|
656
|
+
│ ├── index.ts # Public API
|
|
657
|
+
│ ├── package.json
|
|
658
|
+
│ └── README.md
|
|
659
|
+
├── examples/
|
|
660
|
+
│ └── demo-app/ # Example app (not published)
|
|
661
|
+
│ ├── components/
|
|
662
|
+
│ ├── pages/
|
|
663
|
+
│ ├── services/
|
|
664
|
+
│ ├── app.ts
|
|
665
|
+
│ ├── index.ts
|
|
666
|
+
│ ├── build/ # Example app build output
|
|
667
|
+
│ ├── rollup.config.js
|
|
668
|
+
│ └── tsconfig.json
|
|
669
|
+
├── package.json # Root workspace config
|
|
670
|
+
└── .gitignore
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
For your own app, install the package and follow this structure:
|
|
674
|
+
|
|
633
675
|
```
|
|
634
676
|
my-app/
|
|
635
|
-
├── base/ # The framework (copy or npm install)
|
|
636
677
|
├── components/ # Your reusable components
|
|
637
678
|
│ └── card/
|
|
638
|
-
│
|
|
639
|
-
│
|
|
679
|
+
│ ├── index.module.css
|
|
680
|
+
│ └── index.ts
|
|
640
681
|
├── pages/ # Page components
|
|
641
682
|
│ ├── home/
|
|
642
683
|
│ │ └── index.ts
|
|
643
684
|
│ └── about/
|
|
644
685
|
│ └── index.ts
|
|
645
686
|
├── services/ # API, state management
|
|
646
|
-
├── styles/ # CSS files
|
|
647
687
|
├── app.ts # App setup with router
|
|
648
688
|
├── index.ts # Entry point
|
|
649
689
|
└── index.html
|
|
650
690
|
```
|
|
651
691
|
|
|
692
|
+
## Development
|
|
693
|
+
|
|
694
|
+
### Setup
|
|
695
|
+
|
|
696
|
+
```bash
|
|
697
|
+
# Clone the repo
|
|
698
|
+
git clone https://github.com/ardeshirvalipoor/base-ui.git
|
|
699
|
+
cd base-ui
|
|
700
|
+
|
|
701
|
+
# Install all workspace dependencies
|
|
702
|
+
npm install
|
|
703
|
+
|
|
704
|
+
# Build the example app (watches for changes)
|
|
705
|
+
npm run build:example
|
|
706
|
+
|
|
707
|
+
# Serve locally
|
|
708
|
+
npm run start:example
|
|
709
|
+
# Visit http://localhost:9876
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### Workspace Structure
|
|
713
|
+
|
|
714
|
+
This project uses [npm workspaces](https://docs.npmjs.com/cli/using-npm/workspaces):
|
|
715
|
+
|
|
716
|
+
- **`packages/baseline`** — The library (published to npm)
|
|
717
|
+
- **`examples/demo-app`** — Example app (not published, uses `workspace:*` dependency)
|
|
718
|
+
|
|
719
|
+
The example app imports `@codesuma/baseline` like any consumer would, but npm workspaces resolve it to the local `packages/baseline/` directory.
|
|
720
|
+
|
|
721
|
+
## Publishing to npm
|
|
722
|
+
|
|
723
|
+
This project uses [Changesets](https://github.com/changesets/changesets) across the monorepo for automated versioning and publishing.
|
|
724
|
+
|
|
725
|
+
```bash
|
|
726
|
+
# From the root directory:
|
|
727
|
+
|
|
728
|
+
# 1. Document your changes
|
|
729
|
+
npm run changeset
|
|
730
|
+
|
|
731
|
+
# 2. Bump version and generate CHANGELOG.md
|
|
732
|
+
npm run version-packages
|
|
733
|
+
|
|
734
|
+
# 3. Publish to npm
|
|
735
|
+
npm run release
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
Only the files listed in `package.json` → `files` are published:
|
|
739
|
+
- `components/**/*`
|
|
740
|
+
- `helpers/**/*`
|
|
741
|
+
- `lib/**/*`
|
|
742
|
+
- `utils/**/*`
|
|
743
|
+
- `index.ts`
|
|
744
|
+
- `README.md`
|
|
745
|
+
|
|
746
|
+
The `examples/` directory and build output are **never** included in the npm package.
|
|
747
|
+
|
|
652
748
|
## Philosophy
|
|
653
749
|
|
|
654
750
|
1. **Imperative over declarative** - You control the DOM directly
|
package/components/base.ts
CHANGED
|
@@ -2,9 +2,6 @@ import { nextId } from '../utils/id'
|
|
|
2
2
|
import { createAppender, IAppender } from '../utils/appender'
|
|
3
3
|
import { createStyler, IStyler } from '../utils/styler'
|
|
4
4
|
import { createEmitter, IEmitter } from '../utils/emitter'
|
|
5
|
-
import { initMounter } from '../utils/mounter'
|
|
6
|
-
|
|
7
|
-
initMounter()
|
|
8
5
|
|
|
9
6
|
export function Base<K extends keyof HTMLElementTagNameMap>(name: K = 'div' as K): IBaseComponent<K> {
|
|
10
7
|
const id = nextId()
|
package/index.ts
CHANGED
|
@@ -22,10 +22,10 @@ export { SVG } from './components/native/svg'
|
|
|
22
22
|
export { Page } from './components/advanced/page'
|
|
23
23
|
|
|
24
24
|
// Libraries
|
|
25
|
-
export { default as router, IRouteEvent } from './lib/router'
|
|
25
|
+
export { default as router, IRouteEvent, createRouter } from './lib/router'
|
|
26
26
|
export { default as http } from './lib/http'
|
|
27
27
|
export { default as ldb } from './lib/ldb'
|
|
28
|
-
export { default as idb } from './lib/idb'
|
|
28
|
+
export { default as idb, IDatabase } from './lib/idb'
|
|
29
29
|
export { default as state } from './lib/state'
|
|
30
30
|
|
|
31
31
|
// Utilities
|
package/lib/idb.ts
CHANGED
|
@@ -1,168 +1,223 @@
|
|
|
1
|
-
// Simplified IndexedDB wrapper
|
|
2
|
-
|
|
3
|
-
type StoreOptions = { keyPath?: string; autoIncrement?: boolean; indices?: string[] }
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
1
|
+
// Simplified IndexedDB wrapper with connection caching
|
|
2
|
+
|
|
3
|
+
type StoreOptions = { keyPath?: string; autoIncrement?: boolean; indices?: string[] }
|
|
4
|
+
|
|
5
|
+
interface FindOptions {
|
|
6
|
+
index?: string
|
|
7
|
+
value?: any
|
|
8
|
+
gt?: any
|
|
9
|
+
gte?: any
|
|
10
|
+
lt?: any
|
|
11
|
+
lte?: any
|
|
12
|
+
between?: [any, any]
|
|
13
|
+
limit?: number
|
|
14
|
+
reverse?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// --- Connection cache ---
|
|
18
|
+
// Reuses open connections per dbName, auto-closes after idle timeout
|
|
19
|
+
|
|
20
|
+
const connections: Record<string, IDBDatabase> = {}
|
|
21
|
+
const closeTimers: Record<string, ReturnType<typeof setTimeout>> = {}
|
|
22
|
+
const IDLE_TIMEOUT = 2000
|
|
23
|
+
|
|
24
|
+
const getConnection = (name: string, version?: number): Promise<IDBDatabase> => {
|
|
25
|
+
// Version upgrade always needs a fresh connection
|
|
26
|
+
if (version !== undefined) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const req = indexedDB.open(name, version)
|
|
29
|
+
req.onsuccess = () => resolve(req.result)
|
|
30
|
+
req.onerror = () => reject(req.error)
|
|
31
|
+
req.onupgradeneeded = (e) => {
|
|
32
|
+
// Return the db via the stored handler (set by createStore)
|
|
33
|
+
const handler = (req as any).__onupgrade
|
|
34
|
+
if (handler) handler(e)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Reuse cached connection
|
|
40
|
+
if (connections[name] && connections[name].version) {
|
|
41
|
+
resetIdleTimer(name)
|
|
42
|
+
return Promise.resolve(connections[name])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const req = indexedDB.open(name)
|
|
47
|
+
req.onsuccess = () => {
|
|
48
|
+
connections[name] = req.result
|
|
49
|
+
|
|
50
|
+
// Clean up cache if browser closes the connection
|
|
51
|
+
req.result.onclose = () => { delete connections[name] }
|
|
52
|
+
|
|
53
|
+
resetIdleTimer(name)
|
|
54
|
+
resolve(req.result)
|
|
55
|
+
}
|
|
56
|
+
req.onerror = () => reject(req.error)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const resetIdleTimer = (name: string) => {
|
|
61
|
+
if (closeTimers[name]) clearTimeout(closeTimers[name])
|
|
62
|
+
closeTimers[name] = setTimeout(() => {
|
|
63
|
+
if (connections[name]) {
|
|
64
|
+
connections[name].close()
|
|
65
|
+
delete connections[name]
|
|
66
|
+
}
|
|
67
|
+
delete closeTimers[name]
|
|
68
|
+
}, IDLE_TIMEOUT)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const closeConnection = (name: string) => {
|
|
72
|
+
if (closeTimers[name]) clearTimeout(closeTimers[name])
|
|
73
|
+
if (connections[name]) {
|
|
74
|
+
connections[name].close()
|
|
75
|
+
delete connections[name]
|
|
76
|
+
}
|
|
77
|
+
delete closeTimers[name]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Core transaction helper ---
|
|
81
|
+
|
|
82
|
+
const withStore = async <T>(
|
|
83
|
+
dbName: string,
|
|
84
|
+
storeName: string,
|
|
85
|
+
mode: IDBTransactionMode,
|
|
86
|
+
fn: (store: IDBObjectStore) => IDBRequest | void
|
|
87
|
+
): Promise<T> => {
|
|
88
|
+
const db = await getConnection(dbName)
|
|
89
|
+
|
|
90
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
91
|
+
throw new Error(`Store "${storeName}" does not exist in database "${dbName}"`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const tx = db.transaction(storeName, mode)
|
|
96
|
+
const store = tx.objectStore(storeName)
|
|
97
|
+
const req = fn(store)
|
|
98
|
+
|
|
99
|
+
tx.oncomplete = () => {
|
|
100
|
+
resolve(req ? (req.result as T) : (undefined as T))
|
|
101
|
+
}
|
|
102
|
+
tx.onerror = () => reject(tx.error)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Cursor-based query helper ---
|
|
107
|
+
|
|
108
|
+
const withCursor = async <T>(
|
|
109
|
+
dbName: string,
|
|
110
|
+
storeName: string,
|
|
111
|
+
options: FindOptions = {}
|
|
112
|
+
): Promise<T[]> => {
|
|
113
|
+
const { index, value, gt, gte, lt, lte, between, limit = 1000, reverse = false } = options
|
|
114
|
+
const db = await getConnection(dbName)
|
|
115
|
+
|
|
116
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
117
|
+
throw new Error(`Store "${storeName}" does not exist in database "${dbName}"`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const tx = db.transaction(storeName, 'readonly')
|
|
122
|
+
const store = tx.objectStore(storeName)
|
|
123
|
+
const source = index ? store.index(index) : store
|
|
124
|
+
|
|
125
|
+
// Build key range
|
|
126
|
+
let range: IDBKeyRange | undefined
|
|
127
|
+
if (value !== undefined) range = IDBKeyRange.only(value)
|
|
128
|
+
else if (between) range = IDBKeyRange.bound(between[0], between[1])
|
|
129
|
+
else if (gte !== undefined && lte !== undefined) range = IDBKeyRange.bound(gte, lte)
|
|
130
|
+
else if (gte !== undefined && lt !== undefined) range = IDBKeyRange.bound(gte, lt, false, true)
|
|
131
|
+
else if (gt !== undefined && lte !== undefined) range = IDBKeyRange.bound(gt, lte, true, false)
|
|
132
|
+
else if (gt !== undefined && lt !== undefined) range = IDBKeyRange.bound(gt, lt, true, true)
|
|
133
|
+
else if (gte !== undefined) range = IDBKeyRange.lowerBound(gte)
|
|
134
|
+
else if (gt !== undefined) range = IDBKeyRange.lowerBound(gt, true)
|
|
135
|
+
else if (lte !== undefined) range = IDBKeyRange.upperBound(lte)
|
|
136
|
+
else if (lt !== undefined) range = IDBKeyRange.upperBound(lt, true)
|
|
137
|
+
|
|
138
|
+
const req = source.openCursor(range, reverse ? 'prev' : 'next')
|
|
139
|
+
const results: T[] = []
|
|
140
|
+
|
|
141
|
+
req.onsuccess = () => {
|
|
142
|
+
const cursor = req.result
|
|
143
|
+
if (cursor && results.length < limit) {
|
|
144
|
+
results.push(cursor.value)
|
|
145
|
+
cursor.continue()
|
|
146
|
+
} else {
|
|
147
|
+
resolve(results)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
tx.onerror = () => reject(tx.error)
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- Database factory ---
|
|
155
|
+
|
|
156
|
+
function createDatabase(dbName: string) {
|
|
157
|
+
return {
|
|
158
|
+
info: async () => {
|
|
159
|
+
const db = await getConnection(dbName)
|
|
160
|
+
return { version: db.version, stores: Array.from(db.objectStoreNames) }
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
createStore: (name: string, version: number, options: StoreOptions = {}) => {
|
|
164
|
+
const opts = { keyPath: 'id', autoIncrement: true, indices: [] as string[], ...options }
|
|
165
|
+
|
|
166
|
+
// Close any cached connection (version upgrade requires a fresh open)
|
|
167
|
+
closeConnection(dbName)
|
|
168
|
+
|
|
169
|
+
return new Promise<void>((resolve, reject) => {
|
|
170
|
+
const req = indexedDB.open(dbName, version)
|
|
171
|
+
req.onupgradeneeded = (e) => {
|
|
172
|
+
const db = (e.target as IDBOpenDBRequest).result
|
|
173
|
+
if (!db.objectStoreNames.contains(name)) {
|
|
174
|
+
const store = db.createObjectStore(name, {
|
|
175
|
+
keyPath: opts.keyPath,
|
|
176
|
+
autoIncrement: opts.autoIncrement
|
|
177
|
+
})
|
|
178
|
+
opts.indices.forEach(idx => store.createIndex(idx, idx))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
req.onsuccess = () => {
|
|
182
|
+
// Cache the new connection
|
|
183
|
+
connections[dbName] = req.result
|
|
184
|
+
req.result.onclose = () => { delete connections[dbName] }
|
|
185
|
+
resetIdleTimer(dbName)
|
|
186
|
+
resolve()
|
|
187
|
+
}
|
|
188
|
+
req.onerror = () => reject(req.error)
|
|
189
|
+
})
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
save: <T>(store: string, data: T | T[]): Promise<T | T[]> => {
|
|
193
|
+
const items = Array.isArray(data) ? data : [data]
|
|
194
|
+
return withStore<T | T[]>(dbName, store, 'readwrite', (s) => {
|
|
195
|
+
for (const item of items) s.put(item)
|
|
196
|
+
}).then(() => data)
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
get: <T>(store: string, id: any): Promise<T | undefined> =>
|
|
200
|
+
withStore<T | undefined>(dbName, store, 'readonly', s => s.get(id)),
|
|
201
|
+
|
|
202
|
+
all: <T>(store: string): Promise<T[]> =>
|
|
203
|
+
withStore<T[]>(dbName, store, 'readonly', s => s.getAll()),
|
|
204
|
+
|
|
205
|
+
delete: (store: string, id: any): Promise<void> =>
|
|
206
|
+
withStore<void>(dbName, store, 'readwrite', s => s.delete(id)),
|
|
207
|
+
|
|
208
|
+
clear: (store: string): Promise<void> =>
|
|
209
|
+
withStore<void>(dbName, store, 'readwrite', s => s.clear()),
|
|
210
|
+
|
|
211
|
+
count: (store: string): Promise<number> =>
|
|
212
|
+
withStore<number>(dbName, store, 'readonly', s => s.count()),
|
|
213
|
+
|
|
214
|
+
find: <T>(store: string, options?: FindOptions): Promise<T[]> =>
|
|
215
|
+
withCursor<T>(dbName, store, options),
|
|
216
|
+
|
|
217
|
+
close: () => closeConnection(dbName),
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export type IDatabase = ReturnType<typeof createDatabase>
|
|
222
|
+
|
|
223
|
+
export default createDatabase
|
package/lib/ldb.ts
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
function
|
|
2
|
-
function save(...args: any[]): void
|
|
3
|
-
function save(...args: any[]) {
|
|
4
|
-
if (args.length > 1) {
|
|
5
|
-
let [key, value] = args
|
|
6
|
-
if (typeof value === 'object') value = JSON.stringify(value)
|
|
7
|
-
localStorage.setItem(key, value)
|
|
8
|
-
return
|
|
9
|
-
}
|
|
10
|
-
let [value] = args
|
|
1
|
+
function set(key: string, value: any) {
|
|
11
2
|
if (typeof value === 'object') value = JSON.stringify(value)
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
as(key: string) {
|
|
15
|
-
localStorage.setItem(key, value)
|
|
16
|
-
}
|
|
17
|
-
}
|
|
3
|
+
localStorage.setItem(key, value)
|
|
18
4
|
}
|
|
19
5
|
|
|
20
6
|
function get(key: string) {
|
|
@@ -36,9 +22,7 @@ function clear() {
|
|
|
36
22
|
|
|
37
23
|
export default {
|
|
38
24
|
get,
|
|
39
|
-
set
|
|
40
|
-
save,
|
|
25
|
+
set,
|
|
41
26
|
remove,
|
|
42
27
|
clear
|
|
43
28
|
}
|
|
44
|
-
|
package/lib/router.ts
CHANGED
|
@@ -20,15 +20,6 @@ interface RouteConfig {
|
|
|
20
20
|
|
|
21
21
|
type Routes = Record<string, RouteConfig | (() => IBaseComponent<any>)>
|
|
22
22
|
|
|
23
|
-
const emitter = createEmitter()
|
|
24
|
-
let currentPath = ''
|
|
25
|
-
let currentPage: IBaseComponent<any> | null = null
|
|
26
|
-
let viewContainer: IBaseComponent<any> | null = null
|
|
27
|
-
const pageCache: Record<string, IBaseComponent<any>> = {}
|
|
28
|
-
let routeConfigs: Routes = {}
|
|
29
|
-
let initialized = false
|
|
30
|
-
let isBackNavigation = false
|
|
31
|
-
|
|
32
23
|
// Extract params from path like /users/:id
|
|
33
24
|
const extractParams = (pattern: string, path: string): Record<string, string> | null => {
|
|
34
25
|
const patternParts = pattern.split('/')
|
|
@@ -46,138 +37,155 @@ const extractParams = (pattern: string, path: string): Record<string, string> |
|
|
|
46
37
|
return params
|
|
47
38
|
}
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
40
|
+
export function createRouter() {
|
|
41
|
+
const emitter = createEmitter()
|
|
42
|
+
let currentPath = ''
|
|
43
|
+
let currentPage: IBaseComponent<any> | null = null
|
|
44
|
+
let viewContainer: IBaseComponent<any> | null = null
|
|
45
|
+
const pageCache: Record<string, IBaseComponent<any>> = {}
|
|
46
|
+
let routeConfigs: Routes = {}
|
|
47
|
+
let initialized = false
|
|
48
|
+
let isBackNavigation = false
|
|
49
|
+
|
|
50
|
+
// Find matching route
|
|
51
|
+
const matchRoute = (path: string): { pattern: string; params: Record<string, string> } | null => {
|
|
52
|
+
if (routeConfigs[path]) return { pattern: path, params: {} }
|
|
53
|
+
for (const pattern of Object.keys(routeConfigs)) {
|
|
54
|
+
if (pattern === '*') continue // Skip fallback during normal matching
|
|
55
|
+
const params = extractParams(pattern, path)
|
|
56
|
+
if (params) return { pattern, params }
|
|
57
|
+
}
|
|
58
|
+
// Fallback to wildcard route
|
|
59
|
+
if (routeConfigs['*']) return { pattern: '*', params: {} }
|
|
60
|
+
return null
|
|
55
61
|
}
|
|
56
|
-
return null
|
|
57
|
-
}
|
|
58
62
|
|
|
59
|
-
// Get query params
|
|
60
|
-
const getQuery = (key?: string) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
63
|
+
// Get query params
|
|
64
|
+
const getQuery = (key?: string) => {
|
|
65
|
+
const params = new URLSearchParams(window.location.search)
|
|
66
|
+
return key ? params.get(key) : Object.fromEntries(params)
|
|
67
|
+
}
|
|
64
68
|
|
|
65
|
-
// Handle navigation
|
|
66
|
-
const navigate = async (to: string, data?: any) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// Handle navigation
|
|
70
|
+
const navigate = async (to: string, data?: any) => {
|
|
71
|
+
const url = new URL(to, window.location.origin)
|
|
72
|
+
const path = url.pathname
|
|
73
|
+
const from = currentPath
|
|
74
|
+
const isBack = isBackNavigation
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
if (path === currentPath) return
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
const match = matchRoute(path)
|
|
79
|
+
if (!match) {
|
|
80
|
+
console.warn(`No route found for: ${path}`)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
// Exit current page with direction — await async handlers
|
|
85
|
+
if (currentPage) {
|
|
86
|
+
await currentPage.emit('exit', {
|
|
87
|
+
params: {},
|
|
88
|
+
query: getQuery(),
|
|
89
|
+
from: currentPath,
|
|
90
|
+
to: path,
|
|
91
|
+
data,
|
|
92
|
+
isBack
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Update state
|
|
97
|
+
currentPath = path
|
|
98
|
+
if (!isBack) {
|
|
99
|
+
history.pushState(data, '', to)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Get or create page
|
|
103
|
+
let page = pageCache[path]
|
|
104
|
+
if (!page) {
|
|
105
|
+
const config = routeConfigs[match.pattern]
|
|
106
|
+
const pageFactory = typeof config === 'function' ? config : config.page
|
|
107
|
+
page = pageFactory()
|
|
108
|
+
pageCache[path] = page
|
|
109
|
+
if (viewContainer) {
|
|
110
|
+
viewContainer.append(page)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
currentPage = page
|
|
115
|
+
|
|
116
|
+
// Enter the new page with direction
|
|
117
|
+
page.emit('enter', {
|
|
118
|
+
params: match.params,
|
|
84
119
|
query: getQuery(),
|
|
85
|
-
from
|
|
120
|
+
from,
|
|
86
121
|
to: path,
|
|
87
122
|
data,
|
|
88
123
|
isBack
|
|
89
124
|
})
|
|
90
|
-
}
|
|
91
125
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
126
|
+
emitter.emit('change', {
|
|
127
|
+
path,
|
|
128
|
+
params: match.params,
|
|
129
|
+
query: getQuery(),
|
|
130
|
+
from,
|
|
131
|
+
to: path,
|
|
132
|
+
data,
|
|
133
|
+
isBack
|
|
134
|
+
})
|
|
97
135
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (!page) {
|
|
101
|
-
const config = routeConfigs[match.pattern]
|
|
102
|
-
const pageFactory = typeof config === 'function' ? config : config.page
|
|
103
|
-
page = pageFactory()
|
|
104
|
-
pageCache[path] = page
|
|
105
|
-
if (viewContainer) {
|
|
106
|
-
viewContainer.append(page)
|
|
107
|
-
}
|
|
136
|
+
// Reset back flag
|
|
137
|
+
isBackNavigation = false
|
|
108
138
|
}
|
|
109
139
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
emitter.emit('change', {
|
|
123
|
-
path,
|
|
124
|
-
params: match.params,
|
|
125
|
-
query: getQuery(),
|
|
126
|
-
from,
|
|
127
|
-
to: path,
|
|
128
|
-
data,
|
|
129
|
-
isBack
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
// Reset back flag
|
|
133
|
-
isBackNavigation = false
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Initialize router
|
|
137
|
-
const init = () => {
|
|
138
|
-
if (initialized) return
|
|
139
|
-
initialized = true
|
|
140
|
+
// Initialize router
|
|
141
|
+
const init = () => {
|
|
142
|
+
if (initialized) return
|
|
143
|
+
initialized = true
|
|
144
|
+
|
|
145
|
+
window.addEventListener('popstate', (e) => {
|
|
146
|
+
isBackNavigation = true // Mark as back navigation
|
|
147
|
+
const path = window.location.pathname
|
|
148
|
+
if (path !== currentPath) {
|
|
149
|
+
navigate(path, e.state)
|
|
150
|
+
}
|
|
151
|
+
})
|
|
140
152
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const path = window.location.pathname
|
|
144
|
-
if (path !== currentPath) {
|
|
145
|
-
navigate(path, e.state)
|
|
153
|
+
if (currentPath === '' && viewContainer) {
|
|
154
|
+
navigate(window.location.pathname + window.location.search)
|
|
146
155
|
}
|
|
147
|
-
}
|
|
156
|
+
}
|
|
148
157
|
|
|
149
|
-
|
|
150
|
-
|
|
158
|
+
// Configure routes
|
|
159
|
+
const routes = (config: Routes, view: IBaseComponent<any>) => {
|
|
160
|
+
routeConfigs = config
|
|
161
|
+
viewContainer = view
|
|
162
|
+
init()
|
|
151
163
|
}
|
|
152
|
-
}
|
|
153
164
|
|
|
154
|
-
//
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
// Manually destroy a cached page
|
|
166
|
+
const destroyPage = (path: string) => {
|
|
167
|
+
const page = pageCache[path]
|
|
168
|
+
if (page) {
|
|
169
|
+
page.remove()
|
|
170
|
+
delete pageCache[path]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
160
173
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
return {
|
|
175
|
+
routes,
|
|
176
|
+
init,
|
|
177
|
+
goto: (path: string, data?: any) => navigate(path, data),
|
|
178
|
+
back: () => history.back(),
|
|
179
|
+
forward: () => history.forward(),
|
|
180
|
+
getPath: () => currentPath,
|
|
181
|
+
getQuery,
|
|
182
|
+
getParams: () => matchRoute(currentPath)?.params || {},
|
|
183
|
+
destroyPage,
|
|
184
|
+
on: emitter.on.bind(emitter),
|
|
185
|
+
off: emitter.off.bind(emitter),
|
|
186
|
+
once: emitter.once.bind(emitter),
|
|
167
187
|
}
|
|
168
188
|
}
|
|
169
189
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
init,
|
|
173
|
-
goto: (path: string, data?: any) => navigate(path, data),
|
|
174
|
-
back: () => history.back(),
|
|
175
|
-
forward: () => history.forward(),
|
|
176
|
-
getPath: () => currentPath,
|
|
177
|
-
getQuery,
|
|
178
|
-
getParams: () => matchRoute(currentPath)?.params || {},
|
|
179
|
-
destroyPage,
|
|
180
|
-
on: emitter.on.bind(emitter),
|
|
181
|
-
off: emitter.off.bind(emitter),
|
|
182
|
-
once: emitter.once.bind(emitter),
|
|
183
|
-
}
|
|
190
|
+
// Default singleton for backward compatibility
|
|
191
|
+
export default createRouter()
|
package/package.json
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@codesuma/baseline",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "A minimal, imperative UI framework for building fast web apps. No virtual DOM, no magic, no dependencies.",
|
|
5
|
-
"main": "index.ts",
|
|
6
|
-
"types": "index.ts",
|
|
7
|
-
"files": [
|
|
8
|
-
"components/**/*",
|
|
9
|
-
"helpers/**/*",
|
|
10
|
-
"lib/**/*",
|
|
11
|
-
"utils/**/*",
|
|
12
|
-
"index.ts",
|
|
13
|
-
"README.md"
|
|
14
|
-
],
|
|
15
|
-
"keywords": [
|
|
16
|
-
"ui",
|
|
17
|
-
"framework",
|
|
18
|
-
"spa",
|
|
19
|
-
"router",
|
|
20
|
-
"imperative",
|
|
21
|
-
"minimal",
|
|
22
|
-
"typescript"
|
|
23
|
-
],
|
|
24
|
-
"author": "ardeshirvalipoor",
|
|
25
|
-
"license": "MIT",
|
|
26
|
-
"repository": {
|
|
27
|
-
"type": "git",
|
|
28
|
-
"url": "https://
|
|
29
|
-
},
|
|
30
|
-
"peerDependencies": {},
|
|
31
|
-
"publishConfig": {
|
|
32
|
-
"access": "public"
|
|
33
|
-
},
|
|
34
|
-
"devDependencies": {}
|
|
1
|
+
{
|
|
2
|
+
"name": "@codesuma/baseline",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A minimal, imperative UI framework for building fast web apps. No virtual DOM, no magic, no dependencies.",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"types": "index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"components/**/*",
|
|
9
|
+
"helpers/**/*",
|
|
10
|
+
"lib/**/*",
|
|
11
|
+
"utils/**/*",
|
|
12
|
+
"index.ts",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"ui",
|
|
17
|
+
"framework",
|
|
18
|
+
"spa",
|
|
19
|
+
"router",
|
|
20
|
+
"imperative",
|
|
21
|
+
"minimal",
|
|
22
|
+
"typescript"
|
|
23
|
+
],
|
|
24
|
+
"author": "ardeshirvalipoor",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/ardeshirvalipoor/base-ui.git"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {}
|
|
35
35
|
}
|
package/utils/appender.ts
CHANGED
|
@@ -33,16 +33,17 @@ const markMounted = (component: AnyComponent) => {
|
|
|
33
33
|
export function createAppender(base: AnyComponent): IAppender {
|
|
34
34
|
let children: AnyComponent[] = []
|
|
35
35
|
|
|
36
|
+
// Internal setter — used by child.remove() to update parent's children list
|
|
37
|
+
const setChildren = (c: AnyComponent[]) => { children = c }
|
|
38
|
+
|
|
36
39
|
// Register for root detection (will be cleaned up once mounted)
|
|
37
40
|
pendingRoots.add(base)
|
|
38
41
|
initObserver()
|
|
39
42
|
|
|
40
43
|
return {
|
|
41
|
-
children,
|
|
42
|
-
|
|
43
44
|
getChildren: () => children,
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
_setChildren: setChildren,
|
|
46
47
|
|
|
47
48
|
append(...args: (AnyComponent | false | null | undefined)[]) {
|
|
48
49
|
for (const c of args) {
|
|
@@ -116,8 +117,25 @@ export function createAppender(base: AnyComponent): IAppender {
|
|
|
116
117
|
remove() {
|
|
117
118
|
children.forEach(c => c.remove())
|
|
118
119
|
if (base.parent) {
|
|
119
|
-
base.parent.
|
|
120
|
+
const parentChildren = base.parent.getChildren().filter(c => c !== base)
|
|
121
|
+
base.parent._setChildren(parentChildren)
|
|
122
|
+
}
|
|
123
|
+
base.isMounted = false
|
|
124
|
+
base.emit('unmounted')
|
|
125
|
+
pendingRoots.delete(base)
|
|
126
|
+
base.el.remove()
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
destroy() {
|
|
130
|
+
children.forEach(c => {
|
|
131
|
+
if (typeof (c as any).destroy === 'function') (c as any).destroy()
|
|
132
|
+
else c.remove()
|
|
133
|
+
})
|
|
134
|
+
if (base.parent) {
|
|
135
|
+
const parentChildren = base.parent.getChildren().filter(c => c !== base)
|
|
136
|
+
base.parent._setChildren(parentChildren)
|
|
120
137
|
}
|
|
138
|
+
base.isMounted = false
|
|
121
139
|
base.emit('unmounted')
|
|
122
140
|
base.removeAllListeners()
|
|
123
141
|
pendingRoots.delete(base)
|
|
@@ -127,15 +145,16 @@ export function createAppender(base: AnyComponent): IAppender {
|
|
|
127
145
|
}
|
|
128
146
|
|
|
129
147
|
export interface IAppender {
|
|
130
|
-
children: AnyComponent[]
|
|
131
148
|
getChildren(): AnyComponent[]
|
|
132
|
-
|
|
149
|
+
/** @internal */
|
|
150
|
+
_setChildren(c: AnyComponent[]): void
|
|
133
151
|
append(...args: (AnyComponent | false | null | undefined)[]): AnyComponent
|
|
134
152
|
prepend(...args: AnyComponent[]): AnyComponent
|
|
135
153
|
appendBefore(ref: AnyComponent, ...args: AnyComponent[]): AnyComponent
|
|
136
154
|
appendAfter(ref: AnyComponent, ...args: AnyComponent[]): AnyComponent
|
|
137
155
|
empty(): void
|
|
138
156
|
remove(): void
|
|
157
|
+
destroy(): void
|
|
139
158
|
}
|
|
140
159
|
|
|
141
160
|
export default createAppender
|
package/utils/styler.ts
CHANGED
|
@@ -18,6 +18,22 @@ export function createStyler(base: Component): IStyler {
|
|
|
18
18
|
return base
|
|
19
19
|
},
|
|
20
20
|
|
|
21
|
+
attr(key: string, value?: string) {
|
|
22
|
+
if (value === undefined) return base.el.getAttribute(key)
|
|
23
|
+
base.el.setAttribute(key, value)
|
|
24
|
+
return base
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
text(content: string) {
|
|
28
|
+
base.el.textContent = content
|
|
29
|
+
return base
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
html(content: string) {
|
|
33
|
+
base.el.innerHTML = content
|
|
34
|
+
return base
|
|
35
|
+
},
|
|
36
|
+
|
|
21
37
|
addClass(...classes: string[]) {
|
|
22
38
|
base.el.classList.add.apply(base.el.classList, classes)
|
|
23
39
|
return base
|
|
@@ -48,6 +64,9 @@ export function injectCSS(id: string, css: string) {
|
|
|
48
64
|
|
|
49
65
|
export interface IStyler {
|
|
50
66
|
style(s: CS, delay?: number): Component
|
|
67
|
+
attr(key: string, value?: string): string | null | Component
|
|
68
|
+
text(content: string): Component
|
|
69
|
+
html(content: string): Component
|
|
51
70
|
addClass(...classes: string[]): Component
|
|
52
71
|
removeClass(...classes: string[]): Component
|
|
53
72
|
toggleClass(cls: string, force?: boolean): Component
|