@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/lib/ldb.ts ADDED
@@ -0,0 +1,44 @@
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
11
+ if (typeof value === 'object') value = JSON.stringify(value)
12
+
13
+ return {
14
+ as(key: string) {
15
+ localStorage.setItem(key, value)
16
+ }
17
+ }
18
+ }
19
+
20
+ function get(key: string) {
21
+ const raw = String(localStorage.getItem(key) || '')
22
+ try {
23
+ return JSON.parse(raw)
24
+ } catch (err) {
25
+ return raw
26
+ }
27
+ }
28
+
29
+ function remove(key: string) {
30
+ localStorage.removeItem(key)
31
+ }
32
+
33
+ function clear() {
34
+ localStorage.clear()
35
+ }
36
+
37
+ export default {
38
+ get,
39
+ set: save,
40
+ save,
41
+ remove,
42
+ clear
43
+ }
44
+
package/lib/router.ts ADDED
@@ -0,0 +1,179 @@
1
+ // Router - SPA navigation with page lifecycle (Hide/Show pattern)
2
+ // All pages stay in DOM, only visibility toggles for smooth transitions
3
+
4
+ import { createEmitter } from '../utils/emitter'
5
+ import { IBaseComponent } from '../components/base'
6
+
7
+ // Event payload for enter/exit events
8
+ export interface IRouteEvent {
9
+ params: Record<string, string>
10
+ query: Record<string, string>
11
+ from: string
12
+ data?: any
13
+ isBack?: boolean // True if navigating via back button
14
+ }
15
+
16
+ interface RouteConfig {
17
+ page: () => IBaseComponent<any>
18
+ }
19
+
20
+ type Routes = Record<string, RouteConfig | (() => IBaseComponent<any>)>
21
+
22
+ const emitter = createEmitter()
23
+ let currentPath = ''
24
+ let currentPage: IBaseComponent<any> | null = null
25
+ let viewContainer: IBaseComponent<any> | null = null
26
+ const pageCache: Record<string, IBaseComponent<any>> = {}
27
+ let routeConfigs: Routes = {}
28
+ let initialized = false
29
+ let isBackNavigation = false
30
+
31
+ // Extract params from path like /users/:id
32
+ const extractParams = (pattern: string, path: string): Record<string, string> | null => {
33
+ const patternParts = pattern.split('/')
34
+ const pathParts = path.split('/')
35
+ if (patternParts.length !== pathParts.length) return null
36
+
37
+ const params: Record<string, string> = {}
38
+ for (let i = 0; i < patternParts.length; i++) {
39
+ if (patternParts[i].startsWith(':')) {
40
+ params[patternParts[i].slice(1)] = pathParts[i]
41
+ } else if (patternParts[i] !== pathParts[i]) {
42
+ return null
43
+ }
44
+ }
45
+ return params
46
+ }
47
+
48
+ // Find matching route
49
+ const matchRoute = (path: string): { pattern: string; params: Record<string, string> } | null => {
50
+ if (routeConfigs[path]) return { pattern: path, params: {} }
51
+ for (const pattern of Object.keys(routeConfigs)) {
52
+ const params = extractParams(pattern, path)
53
+ if (params) return { pattern, params }
54
+ }
55
+ return null
56
+ }
57
+
58
+ // Get query params
59
+ const getQuery = (key?: string) => {
60
+ const params = new URLSearchParams(window.location.search)
61
+ return key ? params.get(key) : Object.fromEntries(params)
62
+ }
63
+
64
+ // Handle navigation
65
+ const navigate = async (to: string, data?: any) => {
66
+ const url = new URL(to, window.location.origin)
67
+ const path = url.pathname
68
+ const from = currentPath
69
+ const isBack = isBackNavigation
70
+
71
+ if (path === currentPath) return
72
+
73
+ const match = matchRoute(path)
74
+ if (!match) {
75
+ console.warn(`No route found for: ${path}`)
76
+ return
77
+ }
78
+
79
+ // Exit current page with direction
80
+ if (currentPage) {
81
+ currentPage.emit('exit', {
82
+ params: {},
83
+ query: getQuery(),
84
+ from: currentPath,
85
+ data,
86
+ isBack
87
+ })
88
+ }
89
+
90
+ // Update state
91
+ currentPath = path
92
+ if (!isBack) {
93
+ history.pushState(data, '', to)
94
+ }
95
+
96
+ // Get or create page
97
+ let page = pageCache[path]
98
+ if (!page) {
99
+ const config = routeConfigs[match.pattern]
100
+ const pageFactory = typeof config === 'function' ? config : config.page
101
+ page = pageFactory()
102
+ pageCache[path] = page
103
+ if (viewContainer) {
104
+ viewContainer.append(page)
105
+ }
106
+ }
107
+
108
+ currentPage = page
109
+
110
+ // Enter the new page with direction
111
+ page.emit('enter', {
112
+ params: match.params,
113
+ query: getQuery(),
114
+ from,
115
+ data,
116
+ isBack
117
+ })
118
+
119
+ emitter.emit('change', {
120
+ path,
121
+ params: match.params,
122
+ query: getQuery(),
123
+ from,
124
+ data,
125
+ isBack
126
+ })
127
+
128
+ // Reset back flag
129
+ isBackNavigation = false
130
+ }
131
+
132
+ // Initialize router
133
+ const init = () => {
134
+ if (initialized) return
135
+ initialized = true
136
+
137
+ window.addEventListener('popstate', (e) => {
138
+ isBackNavigation = true // Mark as back navigation
139
+ const path = window.location.pathname
140
+ if (path !== currentPath) {
141
+ navigate(path, e.state)
142
+ }
143
+ })
144
+
145
+ if (currentPath === '' && viewContainer) {
146
+ navigate(window.location.pathname + window.location.search)
147
+ }
148
+ }
149
+
150
+ // Configure routes
151
+ const routes = (config: Routes, view: IBaseComponent<any>) => {
152
+ routeConfigs = config
153
+ viewContainer = view
154
+ init()
155
+ }
156
+
157
+ // Manually destroy a cached page
158
+ const destroyPage = (path: string) => {
159
+ const page = pageCache[path]
160
+ if (page) {
161
+ page.remove()
162
+ delete pageCache[path]
163
+ }
164
+ }
165
+
166
+ export default {
167
+ routes,
168
+ init,
169
+ goto: (path: string, data?: any) => navigate(path, data),
170
+ back: () => history.back(),
171
+ forward: () => history.forward(),
172
+ getPath: () => currentPath,
173
+ getQuery,
174
+ getParams: () => matchRoute(currentPath)?.params || {},
175
+ destroyPage,
176
+ on: emitter.on.bind(emitter),
177
+ off: emitter.off.bind(emitter),
178
+ once: emitter.once.bind(emitter),
179
+ }
package/lib/state.ts ADDED
@@ -0,0 +1,35 @@
1
+ // Global state with optional change notifications
2
+
3
+ import { createEmitter } from '../utils/emitter'
4
+
5
+ const data: Record<string, any> = {}
6
+ const emitter = createEmitter()
7
+
8
+ export const get = <T = any>(key: string): T | undefined => data[key]
9
+
10
+ export const set = <T>(key: string, value: T): T => {
11
+ data[key] = value
12
+ emitter.emit(key, value) // Emit specific key
13
+ emitter.emit('change', { key, value }) // Emit generic 'change'
14
+ return value
15
+ }
16
+
17
+ export const remove = (key: string) => {
18
+ delete data[key]
19
+ emitter.emit(key, undefined)
20
+ emitter.emit('change', { key, value: undefined })
21
+ }
22
+
23
+ export const clear = () => {
24
+ Object.keys(data).forEach(k => delete data[k])
25
+ emitter.emit('change', { key: '*', value: undefined })
26
+ }
27
+
28
+ export const all = () => ({ ...data })
29
+
30
+ // Re-export emitter methods for subscriptions
31
+ export const on = emitter.on.bind(emitter)
32
+ export const off = emitter.off.bind(emitter)
33
+ export const once = emitter.once.bind(emitter)
34
+
35
+ export default { get, set, remove, clear, all, on, off, once }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@codesuma/baseline",
3
+ "version": "1.0.1",
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
+ }
@@ -0,0 +1,141 @@
1
+ import { IBaseComponent, IBaseSVGComponent } from '../components/base'
2
+
3
+ type AnyComponent = IBaseComponent<any> | IBaseSVGComponent<any>
4
+
5
+ // Lightweight observer - only checks if element entered document.body
6
+ let observer: MutationObserver | null = null
7
+ const pendingRoots: Set<AnyComponent> = new Set()
8
+
9
+ const initObserver = () => {
10
+ if (observer) return
11
+ observer = new MutationObserver(() => {
12
+ pendingRoots.forEach(component => {
13
+ if (document.body.contains(component.el)) {
14
+ pendingRoots.delete(component)
15
+ markMounted(component)
16
+ }
17
+ })
18
+ if (pendingRoots.size === 0 && observer) {
19
+ observer.disconnect()
20
+ observer = null
21
+ }
22
+ })
23
+ observer.observe(document.body, { childList: true, subtree: true })
24
+ }
25
+
26
+ const markMounted = (component: AnyComponent) => {
27
+ if (component.isMounted) return
28
+ component.isMounted = true
29
+ component.emit('mounted')
30
+ component.getChildren().forEach(markMounted)
31
+ }
32
+
33
+ export function createAppender(base: AnyComponent): IAppender {
34
+ let children: AnyComponent[] = []
35
+
36
+ // Register for root detection (will be cleaned up once mounted)
37
+ pendingRoots.add(base)
38
+ initObserver()
39
+
40
+ return {
41
+ children,
42
+
43
+ getChildren: () => children,
44
+
45
+ setChildren: (c: AnyComponent[]) => { children = c },
46
+
47
+ append(...args: (AnyComponent | false | null | undefined)[]) {
48
+ for (const c of args) {
49
+ if (!c) continue
50
+ if (c instanceof Promise) {
51
+ c.then(resolved => {
52
+ if (!resolved?.el) return
53
+ base.el.appendChild(resolved.el)
54
+ children.push(resolved)
55
+ resolved.parent = base
56
+ pendingRoots.delete(resolved) // No longer pending
57
+ if (base.isMounted) markMounted(resolved)
58
+ })
59
+ continue
60
+ }
61
+ if (!c.el) continue
62
+ base.el.appendChild(c.el)
63
+ children.push(c)
64
+ c.parent = base
65
+ pendingRoots.delete(c) // No longer pending - has parent
66
+ if (base.isMounted) markMounted(c)
67
+ }
68
+ base.emit('append', args)
69
+ return base
70
+ },
71
+
72
+ prepend(...args: AnyComponent[]) {
73
+ for (const c of args) {
74
+ if (!c?.el) continue
75
+ base.el.insertBefore(c.el, base.el.firstChild)
76
+ children.unshift(c)
77
+ c.parent = base
78
+ pendingRoots.delete(c)
79
+ if (base.isMounted) markMounted(c)
80
+ }
81
+ base.emit('prepend', args)
82
+ return base
83
+ },
84
+
85
+ appendBefore(ref: AnyComponent, ...args: AnyComponent[]) {
86
+ for (const c of args) {
87
+ if (!c?.el) continue
88
+ base.el.insertBefore(c.el, ref.el)
89
+ const idx = children.indexOf(ref)
90
+ children.splice(idx, 0, c)
91
+ c.parent = base
92
+ pendingRoots.delete(c)
93
+ if (base.isMounted) markMounted(c)
94
+ }
95
+ return base
96
+ },
97
+
98
+ appendAfter(ref: AnyComponent, ...args: AnyComponent[]) {
99
+ for (const c of args) {
100
+ if (!c?.el) continue
101
+ base.el.insertBefore(c.el, ref.el.nextSibling)
102
+ const idx = children.indexOf(ref)
103
+ children.splice(idx + 1, 0, c)
104
+ c.parent = base
105
+ pendingRoots.delete(c)
106
+ if (base.isMounted) markMounted(c)
107
+ }
108
+ return base
109
+ },
110
+
111
+ empty() {
112
+ children.forEach(c => c.remove())
113
+ children = []
114
+ },
115
+
116
+ remove() {
117
+ children.forEach(c => c.remove())
118
+ if (base.parent) {
119
+ base.parent.setChildren(base.parent.getChildren().filter(c => c !== base))
120
+ }
121
+ base.removeAllListeners()
122
+ pendingRoots.delete(base)
123
+ base.el.remove()
124
+ base.emit('unmounted')
125
+ }
126
+ }
127
+ }
128
+
129
+ export interface IAppender {
130
+ children: AnyComponent[]
131
+ getChildren(): AnyComponent[]
132
+ setChildren(c: AnyComponent[]): void
133
+ append(...args: (AnyComponent | false | null | undefined)[]): AnyComponent
134
+ prepend(...args: AnyComponent[]): AnyComponent
135
+ appendBefore(ref: AnyComponent, ...args: AnyComponent[]): AnyComponent
136
+ appendAfter(ref: AnyComponent, ...args: AnyComponent[]): AnyComponent
137
+ empty(): void
138
+ remove(): void
139
+ }
140
+
141
+ export default createAppender
@@ -0,0 +1,63 @@
1
+ export function createEmitter<T extends Record<string, any> = Record<string, any>>(): IEmitter<T> {
2
+ const listeners: Record<string, Function[]> = {}
3
+
4
+ return {
5
+ on(event, handler) {
6
+ const events = Array.isArray(event) ? event : [event]
7
+ for (const e of events) {
8
+ const key = e as string
9
+ listeners[key] = listeners[key] || []
10
+ listeners[key].push(handler)
11
+ }
12
+ return this
13
+ },
14
+
15
+ once(event, handler) {
16
+ const wrapper = (payload: any) => {
17
+ handler(payload)
18
+ this.off(event, wrapper as any)
19
+ }
20
+ return this.on(event, wrapper as any)
21
+ },
22
+
23
+ off(event, handler) {
24
+ const key = event as string
25
+ if (!handler) {
26
+ delete listeners[key]
27
+ } else if (listeners[key]) {
28
+ listeners[key] = listeners[key].filter(h => h !== handler)
29
+ }
30
+ return this
31
+ },
32
+
33
+ emit(event, payload) {
34
+ const key = event as string
35
+ if (listeners[key]) {
36
+ for (const handler of listeners[key]) {
37
+ handler(payload)
38
+ }
39
+ }
40
+ return this
41
+ },
42
+
43
+ removeAllListeners() {
44
+ for (const key in listeners) delete listeners[key]
45
+ },
46
+
47
+ getListeners() {
48
+ return listeners
49
+ }
50
+ } as IEmitter<T>
51
+ }
52
+
53
+ // Global emitter for framework internals (mounter, theme, db-ready)
54
+ export const emitter = createEmitter()
55
+
56
+ export interface IEmitter<T extends Record<string, any> = Record<string, any>> {
57
+ on<K extends keyof T>(event: K | K[], handler: (payload: T[K]) => void): this
58
+ once<K extends keyof T>(event: K, handler: (payload: T[K]) => void): this
59
+ off<K extends keyof T>(event: K, handler?: (payload: T[K]) => void): this
60
+ emit<K extends keyof T>(event: K, payload?: T[K]): this
61
+ removeAllListeners(): void
62
+ getListeners(): Record<string, Function[]>
63
+ }
package/utils/id.ts ADDED
@@ -0,0 +1,13 @@
1
+ export const idGenerator = (id = 0) => () => (id++).toString()
2
+ export const nextId = idGenerator()
3
+
4
+ export const shortUUID = () => {
5
+ return (new Date().valueOf().toString(36).slice(2) + Math.random().toString(36).slice(2)).slice(-12)
6
+ }
7
+
8
+ // Credits: https://stackoverflow.com/users/109538/broofa
9
+ export const uuidv4 = () => {
10
+ return (String(1e7) + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
11
+ (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
12
+ )
13
+ }
@@ -0,0 +1,8 @@
1
+ // mounter.ts is now deprecated - mounted detection is handled by appender.ts
2
+ // This file kept for backward compatibility only
3
+
4
+ export function initMounter() {
5
+ // No-op - appender handles mounting now
6
+ }
7
+
8
+ export default { observe: initMounter }
@@ -0,0 +1,59 @@
1
+ // Ripple effect - Material Design style touch feedback
2
+
3
+ import { Base, IBaseComponent } from '../components/base'
4
+ import { injectCSS } from '../utils/styler'
5
+
6
+ const RIPPLE_CSS = `
7
+ [data-ripple] .ripple {
8
+ position: absolute;
9
+ border-radius: 50%;
10
+ background: currentColor;
11
+ opacity: 0.1;
12
+ transform: scale(0);
13
+ pointer-events: none;
14
+ }
15
+ [data-ripple] .ripple.animate {
16
+ animation: ripple 1s ease-out;
17
+ }
18
+ @keyframes ripple {
19
+ to { transform: scale(4); opacity: 0; }
20
+ }
21
+ `
22
+
23
+ export const withRipple = <T extends IBaseComponent<any>>(component: T): T => {
24
+ injectCSS('base-ripple', RIPPLE_CSS)
25
+
26
+ component.el.setAttribute('data-ripple', '')
27
+
28
+ const createRipple = (x: number, y: number) => {
29
+ const ripple = Base('span')
30
+ ripple.el.className = 'ripple'
31
+
32
+ const rect = component.el.getBoundingClientRect()
33
+ const size = Math.max(rect.width, rect.height) * 2
34
+
35
+ ripple.style({
36
+ width: size + 'px',
37
+ height: size + 'px',
38
+ left: (x - rect.left - size / 2) + 'px',
39
+ top: (y - rect.top - size / 2) + 'px'
40
+ })
41
+
42
+ component.el.appendChild(ripple.el)
43
+ ripple.el.classList.add('animate')
44
+
45
+ setTimeout(() => ripple.el.remove(), 600)
46
+ }
47
+
48
+ component.el.addEventListener('touchstart', (e: TouchEvent) => {
49
+ createRipple(e.touches[0].clientX, e.touches[0].clientY)
50
+ }, { passive: true })
51
+
52
+ component.el.addEventListener('mousedown', (e: MouseEvent) => {
53
+ createRipple(e.clientX, e.clientY)
54
+ })
55
+
56
+ return component
57
+ }
58
+
59
+ export default withRipple
@@ -0,0 +1,56 @@
1
+ import { IBaseComponent, IBaseSVGComponent } from '../components/base'
2
+
3
+ type Component = IBaseComponent<any> | IBaseSVGComponent<any>
4
+ type StyleObj = Partial<CSSStyleDeclaration> & Record<string, any>
5
+
6
+ export function createStyler(base: Component): IStyler {
7
+ return {
8
+ style(s: StyleObj, delay?: number) {
9
+ const apply = () => {
10
+ const keys = Object.keys(s)
11
+ for (let i = 0; i < keys.length; i++) {
12
+ const k = keys[i]
13
+ const v = s[k]
14
+ ; (base.el.style as any)[k] = typeof v === 'function' ? v() : v
15
+ }
16
+ }
17
+ delay ? setTimeout(apply, delay) : apply()
18
+ return base
19
+ },
20
+
21
+ addClass(...classes: string[]) {
22
+ base.el.classList.add.apply(base.el.classList, classes)
23
+ return base
24
+ },
25
+
26
+ removeClass(...classes: string[]) {
27
+ base.el.classList.remove.apply(base.el.classList, classes)
28
+ return base
29
+ },
30
+
31
+ toggleClass(cls: string, force?: boolean) {
32
+ base.el.classList.toggle(cls, force)
33
+ return base
34
+ }
35
+ }
36
+ }
37
+
38
+ // Inject CSS once - for components that need self-contained styles
39
+ const injected: Record<string, boolean> = {}
40
+ export function injectCSS(id: string, css: string) {
41
+ if (injected[id]) return
42
+ injected[id] = true
43
+ const el = document.createElement('style')
44
+ el.id = id
45
+ el.textContent = css
46
+ document.head.appendChild(el)
47
+ }
48
+
49
+ export interface IStyler {
50
+ style(s: StyleObj, delay?: number): Component
51
+ addClass(...classes: string[]): Component
52
+ removeClass(...classes: string[]): Component
53
+ toggleClass(cls: string, force?: boolean): Component
54
+ }
55
+
56
+ export default createStyler
package/utils/wait.ts ADDED
@@ -0,0 +1,3 @@
1
+ export const waitFor = (time: number) => {
2
+ return new Promise(resolve => setTimeout(resolve, time, true))
3
+ }