@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.
@@ -0,0 +1,53 @@
1
+ // Page component with directional enter/exit transitions
2
+
3
+ import { Div } from '../native/div'
4
+ import { IRouteEvent } from '../../lib/router'
5
+ import { waitFor } from '../../utils/wait'
6
+ import styles from './page.module.css'
7
+
8
+ const TRANSITION_DURATION = 160
9
+
10
+ export const Page = () => {
11
+ const base = Div()
12
+ base.addClass(styles.page) // Initial state is hidden below (in CSS)
13
+
14
+ // Enter: direction based on isBack flag
15
+ base.on('enter', async ({ isBack }: IRouteEvent) => {
16
+ await waitFor(10)
17
+ // Clear all state classes
18
+ base.removeClass(
19
+ styles.exitUp,
20
+ styles.exitDown,
21
+ styles.hiddenBelow,
22
+ styles.hiddenAbove
23
+ )
24
+ // Add enter class (same visual result, but positioned correctly before)
25
+ base.addClass(isBack ? styles.enterDown : styles.enterUp)
26
+ })
27
+
28
+ // Exit: direction based on isBack flag
29
+ base.on('exit', async ({ isBack }: IRouteEvent) => {
30
+ base.removeClass(styles.enterUp, styles.enterDown)
31
+ if (isBack) {
32
+ // Back: current page goes DOWN
33
+ base.addClass(styles.exitDown)
34
+ } else {
35
+ // Forward: current page goes UP
36
+ base.addClass(styles.exitUp)
37
+ }
38
+ await waitFor(TRANSITION_DURATION)
39
+
40
+ // Position for next enter
41
+ if (isBack) {
42
+ // After exiting down, position below for next forward enter
43
+ base.addClass(styles.hiddenBelow)
44
+ } else {
45
+ // After exiting up, position above for next back enter
46
+ base.addClass(styles.hiddenAbove)
47
+ }
48
+ })
49
+
50
+ return base
51
+ }
52
+
53
+ export default Page
@@ -0,0 +1,45 @@
1
+ import { nextId } from '../utils/id'
2
+ import { createAppender, IAppender } from '../utils/appender'
3
+ import { createStyler, IStyler } from '../utils/styler'
4
+ import { createEmitter, IEmitter } from '../utils/emitter'
5
+ import { initMounter } from '../utils/mounter'
6
+
7
+ initMounter()
8
+
9
+ export function Base<K extends keyof HTMLElementTagNameMap>(name: K = 'div' as K): IBaseComponent<K> {
10
+ const id = nextId()
11
+ const el = document.createElement(name)
12
+ el.setAttribute('data-base-id', id)
13
+
14
+ const component: any = { id, el, isMounted: false, parent: undefined }
15
+
16
+ Object.assign(component, createEmitter(), createAppender(component), createStyler(component))
17
+
18
+ return component
19
+ }
20
+
21
+ export function BaseSVG<K extends keyof SVGElementTagNameMap>(name: K = 'svg' as K): IBaseSVGComponent<K> {
22
+ const id = nextId()
23
+ const el = document.createElementNS('http://www.w3.org/2000/svg', name)
24
+ el.setAttribute('data-base-id', id)
25
+
26
+ const component: any = { id, el, isMounted: false, parent: undefined }
27
+
28
+ Object.assign(component, createEmitter(), createAppender(component), createStyler(component))
29
+
30
+ return component
31
+ }
32
+
33
+ export interface IBaseComponent<K extends keyof HTMLElementTagNameMap = 'div'> extends IEmitter, IAppender, IStyler {
34
+ id: string
35
+ el: HTMLElementTagNameMap[K]
36
+ parent?: IBaseComponent<any>
37
+ isMounted: boolean
38
+ }
39
+
40
+ export interface IBaseSVGComponent<K extends keyof SVGElementTagNameMap = 'svg'> extends IEmitter, IAppender, IStyler {
41
+ id: string
42
+ el: SVGElementTagNameMap[K]
43
+ parent?: IBaseComponent<any> | IBaseSVGComponent<any>
44
+ isMounted: boolean
45
+ }
@@ -0,0 +1,15 @@
1
+ import { Base } from '../base'
2
+
3
+ export const A = (href = '#', text = '', target = '_self') => {
4
+ const base = Base('a')
5
+ base.el.href = href
6
+ base.el.target = target
7
+ if (text) base.el.textContent = text
8
+
9
+ return Object.assign(base, {
10
+ text(t: string) { base.el.textContent = t },
11
+ href(h: string) { base.el.href = h },
12
+ target(t: string) { base.el.target = t },
13
+ download(filename: string) { base.el.download = filename }
14
+ })
15
+ }
@@ -0,0 +1,17 @@
1
+ import { Base } from '../base'
2
+
3
+ export const Button = (text = '') => {
4
+ const base = Base('button')
5
+
6
+ base.style({ cursor: 'pointer' })
7
+ base.el.textContent = text
8
+ base.el.onclick = () => base.emit('click')
9
+
10
+ return Object.assign(base, {
11
+ focus() { base.el.focus() },
12
+ blur() { base.el.blur() },
13
+ disable() { base.el.disabled = true },
14
+ enable() { base.el.disabled = false },
15
+ text(t: string) { base.el.textContent = t }
16
+ })
17
+ }
@@ -0,0 +1,15 @@
1
+ import { Base } from '../base'
2
+
3
+ export const Canvas = (width: number, height: number) => {
4
+ const base = Base('canvas')
5
+ base.el.width = width
6
+ base.el.height = height
7
+
8
+ return Object.assign(base, {
9
+ ctx: base.el.getContext('2d')!,
10
+ resize(w: number, h: number) {
11
+ base.el.width = w
12
+ base.el.height = h
13
+ }
14
+ })
15
+ }
@@ -0,0 +1,11 @@
1
+ import { Base } from '../base'
2
+
3
+ export const Div = (content = '') => {
4
+ const base = Base('div')
5
+ if (content) base.el.innerHTML = content
6
+
7
+ return Object.assign(base, {
8
+ text(t: string) { base.el.textContent = t },
9
+ html(h: string) { base.el.innerHTML = h }
10
+ })
11
+ }
@@ -0,0 +1,17 @@
1
+ import { Base } from '../base'
2
+
3
+ const createHeading = (tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6', content = '') => {
4
+ const base = Base(tag)
5
+ if (content) base.el.textContent = content
6
+
7
+ return Object.assign(base, {
8
+ text(t: string) { base.el.textContent = t }
9
+ })
10
+ }
11
+
12
+ export const H1 = (content = '') => createHeading('h1', content)
13
+ export const H2 = (content = '') => createHeading('h2', content)
14
+ export const H3 = (content = '') => createHeading('h3', content)
15
+ export const H4 = (content = '') => createHeading('h4', content)
16
+ export const H5 = (content = '') => createHeading('h5', content)
17
+ export const H6 = (content = '') => createHeading('h6', content)
@@ -0,0 +1,17 @@
1
+ import { Base } from '../base'
2
+
3
+ const PLACEHOLDER = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
4
+
5
+ export const Img = (src = '', options: { width?: number; height?: number; alt?: string } = {}) => {
6
+ const base = Base('img')
7
+
8
+ base.el.src = src || PLACEHOLDER
9
+ if (options.alt) base.el.alt = options.alt
10
+ if (options.width) base.style({ width: options.width + 'px' })
11
+ if (options.height) base.style({ height: options.height + 'px' })
12
+
13
+ return Object.assign(base, {
14
+ src(s: string) { base.el.src = s },
15
+ alt(a: string) { base.el.alt = a }
16
+ })
17
+ }
@@ -0,0 +1,29 @@
1
+ import { Base } from '../base'
2
+
3
+ export const Input = (placeholder = '', type = 'text', options: { value?: string; accept?: string } = {}) => {
4
+ const base = type === 'textarea' ? Base('textarea') : Base('input')
5
+
6
+ base.el.setAttribute('type', type)
7
+ base.el.setAttribute('placeholder', placeholder)
8
+ if (options.accept) base.el.setAttribute('accept', options.accept)
9
+ if (options.value) base.el.value = options.value
10
+
11
+ // Events
12
+ base.el.onblur = () => base.emit('blur')
13
+ base.el.onfocus = () => base.emit('focus')
14
+ base.el.oninput = () => base.emit('input', base.el.value)
15
+ base.el.onkeydown = (e: KeyboardEvent) => {
16
+ if (e.key === 'Enter') base.emit('enter', base.el.value)
17
+ if (e.key === 'Escape') base.emit('escape')
18
+ base.emit('keydown', { key: e.key, value: base.el.value })
19
+ }
20
+
21
+ return Object.assign(base, {
22
+ focus() { base.el.focus() },
23
+ blur() { base.el.blur() },
24
+ select() { base.el.select() },
25
+ value() { return base.el.value },
26
+ setValue(v: string) { base.el.value = v },
27
+ clear() { base.el.value = '' }
28
+ })
29
+ }
@@ -0,0 +1,8 @@
1
+ import { Base } from '../base'
2
+
3
+ export const Option = (value: string, text: string) => {
4
+ const base = Base('option')
5
+ base.el.value = value
6
+ base.el.textContent = text
7
+ return base
8
+ }
@@ -0,0 +1,11 @@
1
+ import { Base } from '../base'
2
+
3
+ export const P = (content = '') => {
4
+ const base = Base('p')
5
+ if (content) base.el.textContent = content
6
+
7
+ return Object.assign(base, {
8
+ text(t: string) { base.el.textContent = t },
9
+ html(h: string) { base.el.innerHTML = h }
10
+ })
11
+ }
@@ -0,0 +1,15 @@
1
+ import { Base } from '../base'
2
+ import { Option } from './option'
3
+
4
+ export const Select = (options: { value: string; text: string }[] = []) => {
5
+ const base = Base('select')
6
+
7
+ options.forEach(({ value, text }) => base.append(Option(value, text)))
8
+ base.el.onchange = () => base.emit('change', base.el.value)
9
+
10
+ return Object.assign(base, {
11
+ add(value: string, text: string) { base.append(Option(value, text)) },
12
+ value() { return base.el.value },
13
+ setValue(v: string) { base.el.value = v }
14
+ })
15
+ }
@@ -0,0 +1,11 @@
1
+ import { Base } from '../base'
2
+
3
+ export const Span = (content = '') => {
4
+ const base = Base('span')
5
+ if (content) base.el.textContent = content
6
+
7
+ return Object.assign(base, {
8
+ text(t: string) { base.el.textContent = t },
9
+ html(h: string) { base.el.innerHTML = h }
10
+ })
11
+ }
@@ -0,0 +1,18 @@
1
+ import { BaseSVG } from '../base'
2
+
3
+ export const SVG = (viewBox: string | number, height?: number) => {
4
+ const base = BaseSVG('svg')
5
+
6
+ if (typeof viewBox === 'string') {
7
+ base.el.setAttribute('viewBox', viewBox)
8
+ } else {
9
+ const h = height || viewBox
10
+ base.el.setAttribute('viewBox', `0 0 ${viewBox} ${h}`)
11
+ base.el.setAttribute('width', String(viewBox))
12
+ base.el.setAttribute('height', String(h))
13
+ }
14
+
15
+ return Object.assign(base, {
16
+ html(content: string) { base.el.innerHTML = content }
17
+ })
18
+ }
@@ -0,0 +1,17 @@
1
+ // Date formatting helpers
2
+
3
+ export const dateFormatter = (locale = 'en-US', options: Intl.DateTimeFormatOptions = {}) =>
4
+ new Intl.DateTimeFormat(locale, {
5
+ year: 'numeric',
6
+ month: 'long',
7
+ day: 'numeric',
8
+ ...options
9
+ })
10
+
11
+ // Persian/Jalali date formatter
12
+ export const jDateFormatter = dateFormatter('fa-IR', { weekday: 'long' })
13
+
14
+ // Common formatters
15
+ export const formatDate = (date: Date, locale = 'en-US') => dateFormatter(locale).format(date)
16
+ export const formatTime = (date: Date, locale = 'en-US') =>
17
+ new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit' }).format(date)
@@ -0,0 +1,13 @@
1
+ // Device detection helpers
2
+
3
+ export const isMobile = () =>
4
+ /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
5
+
6
+ export const isTouch = () =>
7
+ 'ontouchstart' in window || navigator.maxTouchPoints > 0
8
+
9
+ export const isIOS = () =>
10
+ /iPad|iPhone|iPod/.test(navigator.userAgent)
11
+
12
+ export const isAndroid = () =>
13
+ /Android/.test(navigator.userAgent)
@@ -0,0 +1,13 @@
1
+ // Number formatting helpers
2
+
3
+ export const formatNumber = (n: number, decimals?: number) => {
4
+ if (typeof n !== 'number') return String(n)
5
+ const fixed = decimals !== undefined ? n.toFixed(decimals) : n.toString()
6
+ return fixed.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
7
+ }
8
+
9
+ export const formatCurrency = (n: number, currency = 'USD', locale = 'en-US') =>
10
+ new Intl.NumberFormat(locale, { style: 'currency', currency }).format(n)
11
+
12
+ export const formatPercent = (n: number, decimals = 0) =>
13
+ (n * 100).toFixed(decimals) + '%'
@@ -0,0 +1,15 @@
1
+ // Locale and text direction helpers
2
+
3
+ const RTL_CHARS = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/
4
+
5
+ export const isRTL = (text: string) => RTL_CHARS.test(text)
6
+
7
+ // Persian/Arabic digits to Latin
8
+ export const toLatinDigits = (str: string) =>
9
+ str.replace(/[\u06F0-\u06F9\u0660-\u0669]/g, d =>
10
+ String(d.charCodeAt(0) - (d >= '۰' ? 0x06F0 : 0x0660))
11
+ )
12
+
13
+ // Latin digits to Persian
14
+ export const toPersianDigits = (str: string) =>
15
+ str.replace(/[0-9]/g, d => String.fromCharCode(0x06F0 + parseInt(d)))
@@ -0,0 +1,12 @@
1
+ // Validation regex patterns
2
+
3
+ export const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
4
+ export const URL = /^https?:\/\/[^\s/$.?#].[^\s]*$/i
5
+ export const PHONE = /^\+?[\d\s-()]+$/
6
+ export const PASSWORD_STRONG = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/
7
+
8
+ // Validators (return boolean)
9
+ export const isEmail = (s: string) => EMAIL.test(s)
10
+ export const isUrl = (s: string) => URL.test(s)
11
+ export const isPhone = (s: string) => PHONE.test(s)
12
+ export const isStrongPassword = (s: string) => PASSWORD_STRONG.test(s)
package/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ // Base - A minimal, imperative UI framework
2
+ // Main entry point
3
+
4
+ // Core component factory
5
+ export { Base, IBaseComponent } from './components/base'
6
+
7
+ // Native components
8
+ export { Div } from './components/native/div'
9
+ export { Span } from './components/native/span'
10
+ export { P } from './components/native/p'
11
+ export { H1, H2, H3, H4, H5, H6 } from './components/native/h'
12
+ export { Button } from './components/native/button'
13
+ export { Input } from './components/native/input'
14
+ export { A } from './components/native/a'
15
+ export { Img } from './components/native/img'
16
+ export { Select } from './components/native/select'
17
+ export { Option } from './components/native/option'
18
+ export { Canvas } from './components/native/canvas'
19
+ export { SVG } from './components/native/svg'
20
+
21
+ // Advanced components
22
+ export { Page } from './components/advanced/page'
23
+
24
+ // Libraries
25
+ export { default as router, IRouteEvent } from './lib/router'
26
+ export { default as http } from './lib/http'
27
+ export { default as ldb } from './lib/ldb'
28
+ export { default as createDB } from './lib/idb'
29
+ export { default as state } from './lib/state'
30
+
31
+ // Utilities
32
+ export { createEmitter, emitter, IEmitter } from './utils/emitter'
33
+ export { createStyler, injectCSS, IStyler } from './utils/styler'
34
+ export { waitFor } from './utils/wait'
35
+ export { withRipple } from './utils/ripple'
package/lib/http.ts ADDED
@@ -0,0 +1,96 @@
1
+ // HTTP client with request deduplication and progress support
2
+
3
+ type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
4
+
5
+ interface RequestOptions {
6
+ headers?: Record<string, string>
7
+ auth?: string
8
+ onProgress?: (loaded: number, total: number) => void
9
+ }
10
+
11
+ // Request queue for deduplication
12
+ const pending: Record<string, Promise<any>> = {}
13
+
14
+ const request = async <T = any>(
15
+ method: Method,
16
+ url: string,
17
+ body?: any,
18
+ options: RequestOptions = {}
19
+ ): Promise<{ status: number; data: T }> => {
20
+ const headers: Record<string, string> = {
21
+ 'Content-Type': 'application/json',
22
+ ...options.headers
23
+ }
24
+ if (options.auth) headers['Authorization'] = `Bearer ${options.auth}`
25
+
26
+ const res = await fetch(url, {
27
+ method,
28
+ headers,
29
+ credentials: 'include',
30
+ body: body ? JSON.stringify(body) : undefined
31
+ })
32
+
33
+ let data: T
34
+ const contentType = res.headers.get('content-type')
35
+ if (contentType?.includes('application/json')) {
36
+ data = await res.json()
37
+ } else {
38
+ data = await res.text() as any
39
+ }
40
+
41
+ return { status: res.status, data }
42
+ }
43
+
44
+ // Deduplicated request - same URL returns same promise
45
+ const dedupe = <T>(key: string, fn: () => Promise<T>): Promise<T> => {
46
+ if (!pending[key]) {
47
+ pending[key] = fn().finally(() => delete pending[key])
48
+ }
49
+ return pending[key]
50
+ }
51
+
52
+ // Public API
53
+ export const http = {
54
+ get: <T = any>(url: string, options?: RequestOptions) =>
55
+ dedupe(url, () => request<T>('GET', url, undefined, options)),
56
+
57
+ post: <T = any>(url: string, body?: any, options?: RequestOptions) =>
58
+ request<T>('POST', url, body, options),
59
+
60
+ put: <T = any>(url: string, body?: any, options?: RequestOptions) =>
61
+ request<T>('PUT', url, body, options),
62
+
63
+ patch: <T = any>(url: string, body?: any, options?: RequestOptions) =>
64
+ request<T>('PATCH', url, body, options),
65
+
66
+ delete: <T = any>(url: string, options?: RequestOptions) =>
67
+ request<T>('DELETE', url, undefined, options),
68
+
69
+ // Upload with progress (uses XHR for progress events)
70
+ upload: (url: string, file: File, options: RequestOptions = {}) => {
71
+ return new Promise<{ status: number; data: any }>((resolve, reject) => {
72
+ const xhr = new XMLHttpRequest()
73
+ xhr.open('POST', url)
74
+ if (options.auth) xhr.setRequestHeader('Authorization', `Bearer ${options.auth}`)
75
+ xhr.setRequestHeader('Content-Type', file.type)
76
+
77
+ if (options.onProgress) {
78
+ xhr.upload.onprogress = (e) => {
79
+ if (e.lengthComputable) options.onProgress!(e.loaded, e.total)
80
+ }
81
+ }
82
+
83
+ xhr.onload = () => {
84
+ try {
85
+ resolve({ status: xhr.status, data: JSON.parse(xhr.response) })
86
+ } catch {
87
+ resolve({ status: xhr.status, data: xhr.response })
88
+ }
89
+ }
90
+ xhr.onerror = () => reject(new Error('Upload failed'))
91
+ xhr.send(file)
92
+ })
93
+ }
94
+ }
95
+
96
+ export default http
package/lib/idb.ts ADDED
@@ -0,0 +1,135 @@
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
+ export default (dbName: string) => ({
40
+ // Get database info
41
+ async info(): Promise<{ version: number; objectStoreNames: DOMStringList }> {
42
+ const db = await openDB(dbName)
43
+ const result = { version: db.version, objectStoreNames: db.objectStoreNames }
44
+ db.close()
45
+ return result
46
+ },
47
+
48
+ // Create or upgrade store
49
+ async createStore(name: string, version: number, options: StoreOptions = {}) {
50
+ const opts = { keyPath: 'id', autoIncrement: true, indices: [], ...options }
51
+ return new Promise<void>((resolve, reject) => {
52
+ const req = indexedDB.open(dbName, version)
53
+ req.onupgradeneeded = (e) => {
54
+ const db = (e.target as IDBOpenDBRequest).result
55
+ if (!db.objectStoreNames.contains(name)) {
56
+ const store = db.createObjectStore(name, { keyPath: opts.keyPath, autoIncrement: opts.autoIncrement })
57
+ opts.indices!.forEach(idx => store.createIndex(idx, idx))
58
+ }
59
+ }
60
+ req.onsuccess = () => { req.result.close(); resolve() }
61
+ req.onerror = () => reject(req.error)
62
+ })
63
+ },
64
+
65
+ // CRUD operations
66
+ async save<T>(store: string, data: T | T[]): Promise<T | T[]> {
67
+ const items = Array.isArray(data) ? data : [data]
68
+ await withStore(dbName, store, 'readwrite', (s) => {
69
+ items.forEach(item => s.add(item))
70
+ })
71
+ return data
72
+ },
73
+
74
+ async get<T>(store: string, id: any): Promise<T | undefined> {
75
+ return withStore(dbName, store, 'readonly', s => s.get(id))
76
+ },
77
+
78
+ async all<T>(store: string): Promise<T[]> {
79
+ return withStore(dbName, store, 'readonly', s => s.getAll())
80
+ },
81
+
82
+ async update<T>(store: string, data: T): Promise<T> {
83
+ await withStore(dbName, store, 'readwrite', s => s.put(data))
84
+ return data
85
+ },
86
+
87
+ async delete(store: string, id: any): Promise<void> {
88
+ await withStore(dbName, store, 'readwrite', s => s.delete(id))
89
+ },
90
+
91
+ async clear(store: string): Promise<void> {
92
+ await withStore(dbName, store, 'readwrite', s => s.clear())
93
+ },
94
+
95
+ async count(store: string): Promise<number> {
96
+ return withStore(dbName, store, 'readonly', s => s.count())
97
+ },
98
+
99
+ // Query with options
100
+ async find<T>(store: string, options: {
101
+ index?: string
102
+ value?: any
103
+ limit?: number
104
+ reverse?: boolean
105
+ } = {}): Promise<T[]> {
106
+ const { index, value, limit = 1000, reverse = false } = options
107
+ const db = await openDB(dbName)
108
+
109
+ return new Promise((resolve, reject) => {
110
+ if (!db.objectStoreNames.contains(store)) {
111
+ db.close()
112
+ return resolve([])
113
+ }
114
+
115
+ const tx = db.transaction(store, 'readonly')
116
+ const s = tx.objectStore(store)
117
+ const source = index ? s.index(index) : s
118
+ const range = value !== undefined ? IDBKeyRange.only(value) : undefined
119
+ const req = source.openCursor(range, reverse ? 'prev' : 'next')
120
+
121
+ const results: T[] = []
122
+ req.onsuccess = () => {
123
+ const cursor = req.result
124
+ if (cursor && results.length < limit) {
125
+ results.push(cursor.value)
126
+ cursor.continue()
127
+ } else {
128
+ db.close()
129
+ resolve(results)
130
+ }
131
+ }
132
+ tx.onerror = () => { db.close(); reject(tx.error) }
133
+ })
134
+ }
135
+ })