@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 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
- └── index.module.css # CSS module
639
- └── index.ts # Component
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
@@ -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
- const openDB = (name: string, version?: number): Promise<IDBDatabase> =>
6
- new Promise((resolve, reject) => {
7
- const req = indexedDB.open(name, version)
8
- req.onsuccess = () => resolve(req.result)
9
- req.onerror = () => reject(req.error)
10
- })
11
-
12
- const withStore = async <T>(
13
- dbName: string,
14
- storeName: string,
15
- mode: IDBTransactionMode,
16
- fn: (store: IDBObjectStore, db: IDBDatabase) => IDBRequest | void
17
- ): Promise<T> => {
18
- const db = await openDB(dbName)
19
- return new Promise((resolve, reject) => {
20
- if (!db.objectStoreNames.contains(storeName)) {
21
- db.close()
22
- return resolve([] as any)
23
- }
24
- const tx = db.transaction(storeName, mode)
25
- const store = tx.objectStore(storeName)
26
- const req = fn(store, db)
27
-
28
- tx.oncomplete = () => {
29
- db.close()
30
- resolve(req ? (req.result as T) : (undefined as any))
31
- }
32
- tx.onerror = () => {
33
- db.close()
34
- reject(tx.error)
35
- }
36
- })
37
- }
38
-
39
- // Get database info
40
- async function info(dbName: string): Promise<{ version: number; objectStoreNames: DOMStringList }> {
41
- const db = await openDB(dbName)
42
- const result = { version: db.version, objectStoreNames: db.objectStoreNames }
43
- db.close()
44
- return result
45
- }
46
-
47
- // Create or upgrade store
48
- async function createStore(dbName: string, name: string, version: number, options: StoreOptions = {}) {
49
- const opts = { keyPath: 'id', autoIncrement: true, indices: [], ...options }
50
- return new Promise<void>((resolve, reject) => {
51
- const req = indexedDB.open(dbName, version)
52
- req.onupgradeneeded = (e) => {
53
- const db = (e.target as IDBOpenDBRequest).result
54
- if (!db.objectStoreNames.contains(name)) {
55
- const store = db.createObjectStore(name, { keyPath: opts.keyPath, autoIncrement: opts.autoIncrement })
56
- opts.indices!.forEach(idx => store.createIndex(idx, idx))
57
- }
58
- }
59
- req.onsuccess = () => { req.result.close(); resolve() }
60
- req.onerror = () => reject(req.error)
61
- })
62
- }
63
-
64
- // CRUD operations
65
- async function save<T>(dbName: string, store: string, data: T | T[]): Promise<T | T[]> {
66
- const items = Array.isArray(data) ? data : [data]
67
- await withStore(dbName, store, 'readwrite', (s) => {
68
- items.forEach(item => s.add(item))
69
- })
70
- return data
71
- }
72
-
73
- async function get<T>(dbName: string, store: string, id: any): Promise<T | undefined> {
74
- return withStore(dbName, store, 'readonly', s => s.get(id))
75
- }
76
-
77
- async function all<T>(dbName: string, store: string): Promise<T[]> {
78
- return withStore(dbName, store, 'readonly', s => s.getAll())
79
- }
80
-
81
- async function update<T>(dbName: string, store: string, data: T): Promise<T> {
82
- await withStore(dbName, store, 'readwrite', s => s.put(data))
83
- return data
84
- }
85
-
86
- async function del(dbName: string, store: string, id: any): Promise<void> {
87
- await withStore(dbName, store, 'readwrite', s => s.delete(id))
88
- }
89
-
90
- async function clear(dbName: string, store: string): Promise<void> {
91
- await withStore(dbName, store, 'readwrite', s => s.clear())
92
- }
93
-
94
- async function count(dbName: string, store: string): Promise<number> {
95
- return withStore(dbName, store, 'readonly', s => s.count())
96
- }
97
-
98
- // Query with options
99
- async function find<T>(dbName: string, store: string, options: {
100
- index?: string
101
- value?: any
102
- limit?: number
103
- reverse?: boolean
104
- } = {}): Promise<T[]> {
105
- const { index, value, limit = 1000, reverse = false } = options
106
- const db = await openDB(dbName)
107
-
108
- return new Promise((resolve, reject) => {
109
- if (!db.objectStoreNames.contains(store)) {
110
- db.close()
111
- return resolve([])
112
- }
113
-
114
- const tx = db.transaction(store, 'readonly')
115
- const s = tx.objectStore(store)
116
- const source = index ? s.index(index) : s
117
- const range = value !== undefined ? IDBKeyRange.only(value) : undefined
118
- const req = source.openCursor(range, reverse ? 'prev' : 'next')
119
-
120
- const results: T[] = []
121
- req.onsuccess = () => {
122
- const cursor = req.result
123
- if (cursor && results.length < limit) {
124
- results.push(cursor.value)
125
- cursor.continue()
126
- } else {
127
- db.close()
128
- resolve(results)
129
- }
130
- }
131
- tx.onerror = () => { db.close(); reject(tx.error) }
132
- })
133
- }
134
-
135
- // Add this factory function at the end, before export default
136
-
137
- function createDatabase(dbName: string) {
138
- return {
139
- info: () => info(dbName),
140
- createStore: (name: string, version: number, options?: StoreOptions) =>
141
- createStore(dbName, name, version, options),
142
- save: <T>(store: string, data: T | T[]) => save<T>(dbName, store, data),
143
- get: <T>(store: string, id: any) => get<T>(dbName, store, id),
144
- all: <T>(store: string) => all<T>(dbName, store),
145
- update: <T>(store: string, data: T) => update<T>(dbName, store, data),
146
- delete: (store: string, id: any) => del(dbName, store, id),
147
- clear: (store: string) => clear(dbName, store),
148
- count: (store: string) => count(dbName, store),
149
- find: <T>(store: string, options?: Parameters<typeof find>[2]) =>
150
- find<T>(dbName, store, options),
151
- }
152
- }
153
-
154
- // Make the export callable AND have the raw methods
155
- const idb = Object.assign(createDatabase, {
156
- info,
157
- createStore,
158
- save,
159
- get,
160
- all,
161
- update,
162
- delete: del,
163
- clear,
164
- count,
165
- find
166
- })
167
-
168
- export default idb
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 save(value: any): { as: (key: any) => void }
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: save,
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
- // Find matching route
50
- const matchRoute = (path: string): { pattern: string; params: Record<string, string> } | null => {
51
- if (routeConfigs[path]) return { pattern: path, params: {} }
52
- for (const pattern of Object.keys(routeConfigs)) {
53
- const params = extractParams(pattern, path)
54
- if (params) return { pattern, params }
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
- const params = new URLSearchParams(window.location.search)
62
- return key ? params.get(key) : Object.fromEntries(params)
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
- const url = new URL(to, window.location.origin)
68
- const path = url.pathname
69
- const from = currentPath
70
- const isBack = isBackNavigation
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
- if (path === currentPath) return
76
+ if (path === currentPath) return
73
77
 
74
- const match = matchRoute(path)
75
- if (!match) {
76
- console.warn(`No route found for: ${path}`)
77
- return
78
- }
78
+ const match = matchRoute(path)
79
+ if (!match) {
80
+ console.warn(`No route found for: ${path}`)
81
+ return
82
+ }
79
83
 
80
- // Exit current page with direction
81
- if (currentPage) {
82
- currentPage.emit('exit', {
83
- params: {},
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: currentPath,
120
+ from,
86
121
  to: path,
87
122
  data,
88
123
  isBack
89
124
  })
90
- }
91
125
 
92
- // Update state
93
- currentPath = path
94
- if (!isBack) {
95
- history.pushState(data, '', to)
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
- // Get or create page
99
- let page = pageCache[path]
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
- currentPage = page
111
-
112
- // Enter the new page with direction
113
- page.emit('enter', {
114
- params: match.params,
115
- query: getQuery(),
116
- from,
117
- to: path,
118
- data,
119
- isBack
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
- window.addEventListener('popstate', (e) => {
142
- isBackNavigation = true // Mark as back navigation
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
- if (currentPath === '' && viewContainer) {
150
- navigate(window.location.pathname + window.location.search)
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
- // Configure routes
155
- const routes = (config: Routes, view: IBaseComponent<any>) => {
156
- routeConfigs = config
157
- viewContainer = view
158
- init()
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
- // Manually destroy a cached page
162
- const destroyPage = (path: string) => {
163
- const page = pageCache[path]
164
- if (page) {
165
- page.remove()
166
- delete pageCache[path]
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
- export default {
171
- routes,
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.18",
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://gith ub.com/ardeshirvalipoor/base-ui.git"
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
- setChildren: (c: AnyComponent[]) => { children = c },
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.setChildren(base.parent.getChildren().filter(c => c !== base))
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
- setChildren(c: AnyComponent[]): void
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