cumstack 1.0.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.
@@ -0,0 +1,199 @@
1
+ // iso 639-1 language code lookup table
2
+ // maps 2-letter language codes to their native names
3
+
4
+ export const LANGUAGE_CODES = {
5
+ aa: 'Afaraf',
6
+ ab: 'Аҧсуа',
7
+ ae: 'Avesta',
8
+ af: 'Afrikaans',
9
+ ak: 'Akan',
10
+ am: 'አማርኛ',
11
+ an: 'Aragonés',
12
+ ar: 'العربية',
13
+ as: 'অসমীয়া',
14
+ av: 'Авар',
15
+ ay: 'Aymar',
16
+ az: 'Azərbaycan',
17
+ ba: 'Башҡорт',
18
+ be: 'Беларуская',
19
+ bg: 'Български',
20
+ bh: 'भोजपुरी',
21
+ bi: 'Bislama',
22
+ bm: 'Bamanankan',
23
+ bn: 'বাংলা',
24
+ bo: 'བོད་ཡིག',
25
+ br: 'Brezhoneg',
26
+ bs: 'Bosanski',
27
+ ca: 'Català',
28
+ ce: 'Нохчийн',
29
+ ch: 'Chamoru',
30
+ co: 'Corsu',
31
+ cr: 'ᓀᐦᐃᔭᐍᐏᐣ',
32
+ cs: 'Čeština',
33
+ cu: 'Словѣньскъ',
34
+ cv: 'Чӑваш',
35
+ cy: 'Cymraeg',
36
+ da: 'Dansk',
37
+ de: 'Deutsch',
38
+ dv: 'ދިވެހި',
39
+ dz: 'རྫོང་ཁ',
40
+ ee: 'Eʋegbe',
41
+ el: 'Ελληνικά',
42
+ en: 'English',
43
+ eo: 'Esperanto',
44
+ es: 'Español',
45
+ et: 'Eesti',
46
+ eu: 'Euskara',
47
+ fa: 'فارسی',
48
+ ff: 'Fulfulde',
49
+ fi: 'Suomi',
50
+ fj: 'Vosa Vakaviti',
51
+ fo: 'Føroyskt',
52
+ fr: 'Français',
53
+ fy: 'Frysk',
54
+ ga: 'Gaeilge',
55
+ gd: 'Gàidhlig',
56
+ gl: 'Galego',
57
+ gn: "Avañe'ẽ",
58
+ gu: 'ગુજરાતી',
59
+ gv: 'Gaelg',
60
+ ha: 'هَوُسَ',
61
+ he: 'עברית',
62
+ hi: 'हिन्दी',
63
+ ho: 'Hiri Motu',
64
+ hr: 'Hrvatski',
65
+ ht: 'Kreyòl',
66
+ hu: 'Magyar',
67
+ hy: 'Հայերեն',
68
+ hz: 'Otjiherero',
69
+ ia: 'Interlingua',
70
+ id: 'Bahasa Indonesia',
71
+ ie: 'Interlingue',
72
+ ig: 'Igbo',
73
+ ii: 'ꆈꌠꉙ',
74
+ ik: 'Iñupiatun',
75
+ io: 'Ido',
76
+ is: 'Íslenska',
77
+ it: 'Italiano',
78
+ iu: 'ᐃᓄᒃᑎᑐᑦ',
79
+ ja: '日本語',
80
+ jv: 'Basa Jawa',
81
+ ka: 'ქართული',
82
+ kg: 'Kikongo',
83
+ ki: 'Gĩkũyũ',
84
+ kj: 'Kuanyama',
85
+ kk: 'Қазақша',
86
+ kl: 'Kalaallisut',
87
+ km: 'ភាសាខ្មែរ',
88
+ kn: 'ಕನ್ನಡ',
89
+ ko: '한국어',
90
+ kr: 'Kanuri',
91
+ ks: 'कश्मीरी',
92
+ ku: 'Kurdî',
93
+ kv: 'Коми',
94
+ kw: 'Kernewek',
95
+ ky: 'Кыргызча',
96
+ la: 'Latina',
97
+ lb: 'Lëtzebuergesch',
98
+ lg: 'Luganda',
99
+ li: 'Limburgs',
100
+ ln: 'Lingála',
101
+ lo: 'ລາວ',
102
+ lt: 'Lietuvių',
103
+ lu: 'Tshiluba',
104
+ lv: 'Latviešu',
105
+ mg: 'Malagasy',
106
+ mh: 'Kajin M̧ajeļ',
107
+ mi: 'Te Reo Māori',
108
+ mk: 'Македонски',
109
+ ml: 'മലയാളം',
110
+ mn: 'Монгол',
111
+ mr: 'मराठी',
112
+ ms: 'Bahasa Melayu',
113
+ mt: 'Malti',
114
+ my: 'ဗမာစာ',
115
+ na: 'Dorerin Naoero',
116
+ nb: 'Norsk Bokmål',
117
+ nd: 'isiNdebele',
118
+ ne: 'नेपाली',
119
+ ng: 'Owambo',
120
+ nl: 'Nederlands',
121
+ nn: 'Norsk Nynorsk',
122
+ no: 'Norsk',
123
+ nr: 'isiNdebele',
124
+ nv: 'Diné bizaad',
125
+ ny: 'ChiCheŵa',
126
+ oc: 'Occitan',
127
+ oj: 'ᐊᓂᔑᓈᐯᒧᐎᓐ',
128
+ om: 'Oromoo',
129
+ or: 'ଓଡ଼ିଆ',
130
+ os: 'Ирон',
131
+ pa: 'ਪੰਜਾਬੀ',
132
+ pi: 'पाऴि',
133
+ pl: 'Polski',
134
+ ps: 'پښتو',
135
+ pt: 'Português',
136
+ qu: 'Runa Simi',
137
+ rm: 'Rumantsch',
138
+ rn: 'Ikirundi',
139
+ ro: 'Română',
140
+ ru: 'Русский',
141
+ rw: 'Ikinyarwanda',
142
+ sa: 'संस्कृतम्',
143
+ sc: 'Sardu',
144
+ sd: 'سنڌي',
145
+ se: 'Davvisámegiella',
146
+ sg: 'Sängö',
147
+ si: 'සිංහල',
148
+ sk: 'Slovenčina',
149
+ sl: 'Slovenščina',
150
+ sm: 'Gagana Samoa',
151
+ sn: 'chiShona',
152
+ so: 'Soomaali',
153
+ sq: 'Shqip',
154
+ sr: 'Српски',
155
+ ss: 'SiSwati',
156
+ st: 'Sesotho',
157
+ su: 'Basa Sunda',
158
+ sv: 'Svenska',
159
+ sw: 'Kiswahili',
160
+ ta: 'தமிழ்',
161
+ te: 'తెలుగు',
162
+ tg: 'Тоҷикӣ',
163
+ th: 'ไทย',
164
+ ti: 'ትግርኛ',
165
+ tk: 'Türkmençe',
166
+ tl: 'Tagalog',
167
+ tn: 'Setswana',
168
+ to: 'Lea Faka-Tonga',
169
+ tr: 'Türkçe',
170
+ ts: 'Xitsonga',
171
+ tt: 'Татарча',
172
+ tw: 'Twi',
173
+ ty: 'Reo Tahiti',
174
+ ug: 'ئۇيغۇرچە',
175
+ uk: 'Українська',
176
+ ur: 'اردو',
177
+ uz: 'Oʻzbekcha',
178
+ ve: 'Tshivenḓa',
179
+ vi: 'Tiếng Việt',
180
+ vo: 'Volapük',
181
+ wa: 'Walon',
182
+ wo: 'Wolof',
183
+ xh: 'isiXhosa',
184
+ yi: 'ייִדיש',
185
+ yo: 'Yorùbá',
186
+ za: 'Saɯ cueŋƅ',
187
+ zh: '中文',
188
+ zu: 'isiZulu',
189
+ };
190
+
191
+ // check if language code is valid
192
+ export function isValidLanguageCode(code) {
193
+ return code.length === 2 && LANGUAGE_CODES.hasOwnProperty(code);
194
+ }
195
+
196
+ // get native name for language code
197
+ export function getLanguageName(code) {
198
+ return LANGUAGE_CODES[code] || code;
199
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * cumstack Reactivity System
3
+ * signal-based fine-grained reactivity (shared between client and server)
4
+ */
5
+
6
+ let currentEffect = null;
7
+ const effectStack = [];
8
+ let batchDepth = 0;
9
+ let pending = null;
10
+ let schedule = null;
11
+
12
+ /**
13
+ * create a reactive signal
14
+ * @template T
15
+ * @param {T} initial - Initial value
16
+ * @returns {[() => T, (next: T | ((prev: T) => T)) => void]} Tuple of [getter, setter]
17
+ */
18
+ export function createMoan(initial) {
19
+ let value = initial;
20
+ const subscribers = new Set();
21
+ const read = () => {
22
+ if (currentEffect) subscribers.add(currentEffect);
23
+ return value;
24
+ };
25
+ const write = (next) => {
26
+ const newValue = typeof next === 'function' ? next(value) : next;
27
+ if (Object.is(value, newValue)) return;
28
+ value = newValue;
29
+ const effects = [...subscribers];
30
+ for (const effect of effects) if (subscribers.has(effect)) (schedule || ((e) => e()))(effect);
31
+ };
32
+ return [read, write];
33
+ }
34
+
35
+ /**
36
+ * create an effect that runs when dependencies change
37
+ * @param {() => void} fn - Effect function
38
+ * @returns {() => void} Dispose function to stop the effect
39
+ */
40
+ export function onClimax(fn) {
41
+ let disposed = false;
42
+ const cleanup = [];
43
+ const effect = () => {
44
+ if (disposed) return;
45
+ effectStack.push(effect);
46
+ currentEffect = effect;
47
+ try {
48
+ // run any previous cleanup
49
+ cleanup.forEach((c) => c());
50
+ cleanup.length = 0;
51
+ const result = fn();
52
+ if (typeof result === 'function') cleanup.push(result);
53
+ } catch (err) {
54
+ console.error('Effect error:', err);
55
+ } finally {
56
+ effectStack.pop();
57
+ currentEffect = effectStack[effectStack.length - 1] || null;
58
+ }
59
+ };
60
+ batch(effect);
61
+ return () => {
62
+ disposed = true;
63
+ cleanup.forEach((c) => c());
64
+ cleanup.length = 0;
65
+ };
66
+ }
67
+
68
+ /**
69
+ * create a computed value
70
+ * @template T
71
+ * @param {() => T} fn - Computation function
72
+ * @returns {() => T} Getter for the computed value
73
+ */
74
+ export function knotMemo(fn) {
75
+ let initialized = false;
76
+ let cached;
77
+ const [signal, setSignal] = createMoan();
78
+ onClimax(() => {
79
+ const result = fn();
80
+ if (!initialized || !Object.is(result, cached)) {
81
+ cached = result;
82
+ untrack(() => batch(() => setSignal(result)));
83
+ initialized = true;
84
+ }
85
+ });
86
+ return signal;
87
+ }
88
+
89
+ /**
90
+ * create a resource for async data fetching
91
+ * @template T
92
+ * @param {(signal: AbortSignal) => Promise<T>} fetcher - Async function to fetch data
93
+ * @param {(() => any) | null} deps - Optional signal to trigger refetch
94
+ * @param {T | null} initialValue - Initial value before data loads
95
+ * @returns {Object} Resource object with data, loading, error states and methods
96
+ */
97
+ export function loadShot(fetcher, deps = null, initialValue = null) {
98
+ const [data, setData] = createMoan(initialValue);
99
+ const [loading, setLoading] = createMoan(false);
100
+ const [error, setError] = createMoan(null);
101
+ let controller = null;
102
+ const refetch = async () => {
103
+ if (controller) controller.abort();
104
+ controller = new AbortController();
105
+ const signal = controller.signal;
106
+ batch(() => {
107
+ setLoading(true);
108
+ setError(null);
109
+ });
110
+ try {
111
+ const result = await fetcher(signal);
112
+ batch(() => {
113
+ if (!signal.aborted) setData(result);
114
+ });
115
+ } catch (err) {
116
+ batch(() => {
117
+ if (!signal.aborted) setError(err);
118
+ });
119
+ } finally {
120
+ batch(() => {
121
+ if (!signal.aborted) setLoading(false);
122
+ });
123
+ }
124
+ };
125
+ if (deps) {
126
+ onClimax(() => {
127
+ const _ = deps();
128
+ refetch();
129
+ });
130
+ } else queueMicrotask(refetch);
131
+ const dispose = () => controller?.abort();
132
+ return {
133
+ data,
134
+ loading,
135
+ error,
136
+ refetch,
137
+ dispose,
138
+ idle: () => !loading() && data() === initialValue,
139
+ success: () => !loading() && !error(),
140
+ hasError: () => !!error(),
141
+ };
142
+ }
143
+
144
+ /**
145
+ * batch multiple updates together
146
+ * @param {Function} fn - Function to run in batch
147
+ */
148
+ export function batch(fn) {
149
+ if (batchDepth++ === 0) pending = new Set();
150
+ const prev = schedule;
151
+ schedule = (e) => pending.add(e);
152
+ try {
153
+ fn(schedule);
154
+ } finally {
155
+ schedule = prev;
156
+ if (--batchDepth === 0) {
157
+ const effects = pending;
158
+ pending = null;
159
+ for (const e of effects) e();
160
+ }
161
+ }
162
+ }
163
+
164
+ // external scheduling
165
+ batch.schedule = (fn) => {
166
+ if (batchDepth > 0) pending.add(fn);
167
+ else fn();
168
+ };
169
+
170
+ /**
171
+ * run function without tracking dependencies
172
+ * @template T
173
+ * @param {() => T} fn - Function to run without tracking
174
+ * @returns {T} Result of the function
175
+ */
176
+ export const untrack = (fn) => {
177
+ const prev = currentEffect;
178
+ currentEffect = null;
179
+ try {
180
+ return fn();
181
+ } finally {
182
+ currentEffect = prev;
183
+ }
184
+ };
185
+
186
+ /**
187
+ * get current location (pathname, search, hash)
188
+ * returns a reactive signal that updates on navigation
189
+ * @returns {Object} Location object with location signal, active function, and dispose method
190
+ */
191
+ let locationInstance = null;
192
+ export function useLocation() {
193
+ if (typeof window === 'undefined') {
194
+ return {
195
+ location: () => ({ pathname: '/', search: '', hash: '' }),
196
+ active: () => false,
197
+ dispose: () => {},
198
+ };
199
+ }
200
+
201
+ // return singleton instance if already created
202
+ if (locationInstance) {
203
+ locationInstance.refCount++;
204
+ return {
205
+ location: locationInstance.location,
206
+ active: locationInstance.active,
207
+ dispose: () => {
208
+ if (--locationInstance.refCount === 0) {
209
+ locationInstance.dispose();
210
+ locationInstance = null;
211
+ }
212
+ },
213
+ };
214
+ }
215
+
216
+ // patch history methods once
217
+ if (!window.__locationPatched) {
218
+ ['pushState', 'replaceState'].forEach((method) => {
219
+ const orig = history[method];
220
+ history[method] = function (...args) {
221
+ const result = orig.apply(this, args);
222
+ window.dispatchEvent(new Event(method.toLowerCase()));
223
+ return result;
224
+ };
225
+ });
226
+ window.__locationPatched = true;
227
+ }
228
+
229
+ const getLoc = () => ({
230
+ pathname: window.location.pathname,
231
+ search: window.location.search,
232
+ hash: window.location.hash,
233
+ });
234
+ const [location, setLocation] = createMoan(getLoc());
235
+ const update = () => batch(() => setLocation(getLoc()));
236
+ const events = ['popstate', 'pushstate', 'replacestate'];
237
+ events.forEach((e) => window.addEventListener(e, update));
238
+
239
+ const active = (path) => location().pathname === path;
240
+ const dispose = () => events.forEach((e) => window.removeEventListener(e, update));
241
+
242
+ locationInstance = {
243
+ location,
244
+ active,
245
+ dispose,
246
+ refCount: 1,
247
+ };
248
+
249
+ return {
250
+ location,
251
+ active,
252
+ dispose: () => {
253
+ if (--locationInstance.refCount === 0) {
254
+ locationInstance.dispose();
255
+ locationInstance = null;
256
+ }
257
+ },
258
+ };
259
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * cumstack Router
3
+ * simple manual routing with params and navigation
4
+ */
5
+
6
+ import { createMoan } from './reactivity.js';
7
+
8
+ /**
9
+ * parse route pattern to regex with params
10
+ * @param {string} pattern - Route pattern like "/user/:id"
11
+ * @returns {{ regex: RegExp, params: string[] }}
12
+ */
13
+ function parseRoute(pattern) {
14
+ const params = [];
15
+ const regexPattern = pattern.replace(/\*/g, '.*').replace(/:(\w+)/g, (_, param) => {
16
+ params.push(param);
17
+ return '([^/]+)';
18
+ });
19
+ return {
20
+ regex: new RegExp(`^${regexPattern}$`),
21
+ params,
22
+ };
23
+ }
24
+
25
+ /**
26
+ * match a path against a pattern
27
+ * @param {string} path - Current path
28
+ * @param {string} pattern - Route pattern
29
+ * @returns {{ matched: boolean, params: Record<string, string> }}
30
+ */
31
+ export function matchRoute(path, pattern) {
32
+ const { regex, params: paramNames } = parseRoute(pattern);
33
+ const match = path.match(regex);
34
+ if (!match) return { matched: false, params: {} };
35
+ const params = {};
36
+ paramNames.forEach((name, index) => (params[name] = match[index + 1]));
37
+ return { matched: true, params };
38
+ }
39
+
40
+ /**
41
+ * create a router instance
42
+ */
43
+ export function createRouter() {
44
+ const routes = new Map();
45
+ const [currentPath, setCurrentPath] = createMoan(typeof window !== 'undefined' ? window.location.pathname : '/');
46
+ const [currentParams, setCurrentParams] = createMoan({});
47
+
48
+ /**
49
+ * register a route
50
+ * @param {string} pattern - Route pattern
51
+ * @param {Function} handler - Route handler
52
+ */
53
+ function register(pattern, handler) {
54
+ routes.set(pattern, handler);
55
+ }
56
+
57
+ /**
58
+ * navigate to a path
59
+ * @param {string} path - Target path
60
+ * @param {Object} options - Navigation options
61
+ */
62
+ function navigate(path, options = {}) {
63
+ if (typeof window === 'undefined') return;
64
+ const { replace = false, state = null } = options;
65
+ if (replace) window.history.replaceState(state, '', path);
66
+ else window.history.pushState(state, '', path);
67
+ setCurrentPath(path);
68
+ matchCurrentRoute();
69
+ }
70
+
71
+ /**
72
+ * match current route and extract params
73
+ */
74
+ function matchCurrentRoute() {
75
+ const path = currentPath();
76
+ for (const [pattern, handler] of routes) {
77
+ const { matched, params } = matchRoute(path, pattern);
78
+ if (matched) {
79
+ setCurrentParams(params);
80
+ return { handler, params };
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * handle browser navigation events
88
+ */
89
+ function handlePopState() {
90
+ setCurrentPath(window.location.pathname);
91
+ matchCurrentRoute();
92
+ }
93
+
94
+ /**
95
+ * initialize router
96
+ */
97
+ function init() {
98
+ if (typeof window !== 'undefined') {
99
+ window.addEventListener('popstate', handlePopState);
100
+ matchCurrentRoute();
101
+ }
102
+ }
103
+
104
+ return {
105
+ register,
106
+ navigate,
107
+ init,
108
+ currentPath,
109
+ currentParams,
110
+ matchRoute: matchCurrentRoute,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * create a link component helper
116
+ * @param {string} href - Twink href
117
+ * @param {Object} options - Twink options
118
+ */
119
+ export function createTwink(href, options = {}) {
120
+ return {
121
+ href,
122
+ onClick: (e) => {
123
+ if (options.onClick) options.onClick(e);
124
+ if (!e.defaultPrevented && !e.metaKey && !e.ctrlKey && !e.shiftKey && e.button === 0) {
125
+ e.preventDefault();
126
+ options.navigate?.(href);
127
+ }
128
+ },
129
+ };
130
+ }
131
+
132
+ /**
133
+ * parse query string
134
+ * @param {string} search - Query string
135
+ * @returns {Record<string, string>}
136
+ */
137
+ export function parseQuery(search) {
138
+ const params = new URLSearchParams(search);
139
+ const result = {};
140
+ for (const [key, value] of params) result[key] = value;
141
+ return result;
142
+ }
143
+
144
+ /**
145
+ * build query string
146
+ * @param {Record<string, string>} params - Query params
147
+ * @returns {string}
148
+ */
149
+ export function buildQuery(params) {
150
+ const searchParams = new URLSearchParams(params);
151
+ const query = searchParams.toString();
152
+ return query ? `?${query}` : '';
153
+ }