kimu-core 0.4.1 → 0.4.2
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/.editorconfig +116 -30
- package/.gitattributes +81 -11
- package/.github/FUNDING.yml +8 -8
- package/.github/kimu-copilot-instructions.md +3779 -3779
- package/.github/workflows/deploy-demo.yml +39 -39
- package/.nvmrc +1 -0
- package/.prettierignore +44 -0
- package/.prettierrc +16 -0
- package/FUNDING.md +31 -31
- package/icon.svg +10 -10
- package/package.json +9 -2
- package/scripts/minify-css-assets.js +82 -82
- package/src/core/index.ts +47 -47
- package/src/core/kimu-global-styles.ts +136 -136
- package/src/core/kimu-reactive.ts +196 -196
- package/src/modules-repository/api-axios/CHANGELOG.md +48 -48
- package/src/modules-repository/api-axios/QUICK-REFERENCE.md +178 -178
- package/src/modules-repository/api-axios/README.md +304 -304
- package/src/modules-repository/api-axios/api-axios-service.ts +355 -355
- package/src/modules-repository/api-axios/examples.ts +293 -293
- package/src/modules-repository/api-axios/index.ts +19 -19
- package/src/modules-repository/api-axios/interfaces.ts +71 -71
- package/src/modules-repository/api-axios/module.ts +41 -41
- package/src/modules-repository/api-core/CHANGELOG.md +42 -42
- package/src/modules-repository/api-core/QUICK-REFERENCE.md +192 -192
- package/src/modules-repository/api-core/README.md +435 -435
- package/src/modules-repository/api-core/api-core-service.ts +289 -289
- package/src/modules-repository/api-core/examples.ts +432 -432
- package/src/modules-repository/api-core/index.ts +8 -8
- package/src/modules-repository/api-core/interfaces.ts +83 -83
- package/src/modules-repository/api-core/module.ts +30 -30
- package/src/modules-repository/event-bus/README.md +273 -273
- package/src/modules-repository/event-bus/event-bus-service.ts +176 -176
- package/src/modules-repository/event-bus/module.ts +30 -30
- package/src/modules-repository/notification/README.md +423 -423
- package/src/modules-repository/notification/module.ts +30 -30
- package/src/modules-repository/notification/notification-service.ts +436 -436
- package/src/modules-repository/router/README.it.md +61 -10
- package/src/modules-repository/router/README.md +61 -10
- package/src/modules-repository/router/router-config.ts.example +61 -0
- package/src/modules-repository/router/router.ts +18 -0
- package/src/modules-repository/state/README.md +409 -409
- package/src/modules-repository/state/module.ts +30 -30
- package/src/modules-repository/state/state-service.ts +296 -296
- package/src/modules-repository/theme/README.md +311 -267
- package/src/modules-repository/theme/module.ts +30 -30
- package/src/modules-repository/theme/pre-build.js +40 -40
- package/src/modules-repository/theme/theme-service.ts +411 -389
- package/src/modules-repository/theme/themes/theme-cherry-blossom.css +78 -78
- package/src/modules-repository/theme/themes/theme-cozy.css +111 -111
- package/src/modules-repository/theme/themes/theme-cyberpunk.css +150 -150
- package/src/modules-repository/theme/themes/theme-dark.css +79 -79
- package/src/modules-repository/theme/themes/theme-forest.css +171 -171
- package/src/modules-repository/theme/themes/theme-gold.css +100 -100
- package/src/modules-repository/theme/themes/theme-high-contrast.css +126 -126
- package/src/modules-repository/theme/themes/theme-lava.css +101 -101
- package/src/modules-repository/theme/themes/theme-lavender.css +90 -90
- package/src/modules-repository/theme/themes/theme-light.css +79 -79
- package/src/modules-repository/theme/themes/theme-matrix.css +103 -103
- package/src/modules-repository/theme/themes/theme-midnight.css +81 -81
- package/src/modules-repository/theme/themes/theme-nord.css +94 -94
- package/src/modules-repository/theme/themes/theme-ocean.css +84 -84
- package/src/modules-repository/theme/themes/theme-retro80s.css +343 -343
- package/src/modules-repository/theme/themes/theme-sunset.css +62 -62
- package/src/modules-repository/theme/themes-config-default.json +19 -0
- package/src/modules-repository/theme/themes-config.d.ts +27 -27
- package/src/modules-repository/theme/{themes-config.json → themes-config.json.example} +223 -213
|
@@ -1,136 +1,136 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* KimuGlobalStyles manages global CSS files that should be injected into all extensions.
|
|
3
|
-
* This allows modules (like the theme module) to register additional global styles
|
|
4
|
-
* without hardcoding dependencies in the core framework.
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* - Core always injects 'assets/kimu-style.css' as the base style
|
|
8
|
-
* - Modules can register additional global CSS using registerGlobalStyle()
|
|
9
|
-
* - All registered styles are automatically injected into every extension's shadow root
|
|
10
|
-
*/
|
|
11
|
-
export class KimuGlobalStyles {
|
|
12
|
-
/** List of global CSS files to inject in all extensions */
|
|
13
|
-
private static _globalStyles: Array<{ id: string; path: string }> = [
|
|
14
|
-
// Default KIMU base styles - always included
|
|
15
|
-
{ id: 'kimu-style-default', path: 'assets/kimu-style.css' }
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Register a global CSS file to be injected in all extensions.
|
|
20
|
-
* This is useful for modules that want to provide global theming or styling.
|
|
21
|
-
*
|
|
22
|
-
* @param id - Unique identifier for the style (e.g., 'theme-dark', 'theme-light')
|
|
23
|
-
* @param path - Path to the CSS file relative to the base path
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* // In a theme module
|
|
27
|
-
* KimuGlobalStyles.registerGlobalStyle('theme-active', 'themes/theme-dark.css');
|
|
28
|
-
*/
|
|
29
|
-
static registerGlobalStyle(id: string, path: string): void {
|
|
30
|
-
// Check if already registered
|
|
31
|
-
const existing = this._globalStyles.find(s => s.id === id);
|
|
32
|
-
if (existing) {
|
|
33
|
-
// Update path if already registered
|
|
34
|
-
existing.path = path;
|
|
35
|
-
console.log(`[KimuGlobalStyles] Updated global style: ${id} -> ${path}`);
|
|
36
|
-
} else {
|
|
37
|
-
// Add new global style
|
|
38
|
-
this._globalStyles.push({ id, path });
|
|
39
|
-
console.log(`[KimuGlobalStyles] Registered global style: ${id} -> ${path}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Unregister a global CSS file.
|
|
45
|
-
*
|
|
46
|
-
* @param id - Unique identifier of the style to remove
|
|
47
|
-
*/
|
|
48
|
-
static unregisterGlobalStyle(id: string): void {
|
|
49
|
-
const index = this._globalStyles.findIndex(s => s.id === id);
|
|
50
|
-
if (index !== -1) {
|
|
51
|
-
this._globalStyles.splice(index, 1);
|
|
52
|
-
console.log(`[KimuGlobalStyles] Unregistered global style: ${id}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Get all registered global styles.
|
|
58
|
-
* Used internally by KimuComponentElement to inject styles into extensions.
|
|
59
|
-
*
|
|
60
|
-
* @returns Array of registered global styles
|
|
61
|
-
*/
|
|
62
|
-
static getGlobalStyles(): Array<{ id: string; path: string }> {
|
|
63
|
-
return [...this._globalStyles]; // Return a copy to prevent external modifications
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Clear all registered styles (useful for testing or reset scenarios).
|
|
68
|
-
* Note: This will also remove the default kimu-style.css.
|
|
69
|
-
*/
|
|
70
|
-
static clearAllStyles(): void {
|
|
71
|
-
this._globalStyles = [
|
|
72
|
-
{ id: 'kimu-style-default', path: 'assets/kimu-style.css' }
|
|
73
|
-
];
|
|
74
|
-
console.log('[KimuGlobalStyles] Cleared all global styles, keeping only default');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get the number of registered global styles.
|
|
79
|
-
*/
|
|
80
|
-
static getStyleCount(): number {
|
|
81
|
-
return this._globalStyles.length;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Check if a specific style ID is registered.
|
|
86
|
-
*/
|
|
87
|
-
static hasStyle(id: string): boolean {
|
|
88
|
-
return this._globalStyles.some(s => s.id === id);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Update a specific global style in all currently loaded extensions.
|
|
93
|
-
* This is useful when a theme changes and needs to be propagated to all extensions.
|
|
94
|
-
*
|
|
95
|
-
* @param id - Style ID to update (e.g., 'kimu-theme-active')
|
|
96
|
-
*
|
|
97
|
-
* Note: This method uses KimuEngine.injectStyle() to update styles in all shadow roots.
|
|
98
|
-
* It requires KimuEngine to be imported dynamically to avoid circular dependencies.
|
|
99
|
-
*/
|
|
100
|
-
static async updateStyleInAllExtensions(id: string): Promise<void> {
|
|
101
|
-
const style = this._globalStyles.find(s => s.id === id);
|
|
102
|
-
if (!style) {
|
|
103
|
-
console.warn(`[KimuGlobalStyles] Style "${id}" not found, cannot update`);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Find all custom elements (extensions) in the DOM
|
|
108
|
-
const allCustomElements = document.querySelectorAll('*');
|
|
109
|
-
const extensions: HTMLElement[] = [];
|
|
110
|
-
|
|
111
|
-
allCustomElements.forEach((el) => {
|
|
112
|
-
// Check if element is a custom element with shadow root
|
|
113
|
-
if (el.tagName.includes('-') && (el as any).shadowRoot) {
|
|
114
|
-
extensions.push(el as HTMLElement);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
if (extensions.length === 0) {
|
|
119
|
-
console.log('[KimuGlobalStyles] No extensions found to update');
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Dynamically import KimuEngine to avoid circular dependency
|
|
124
|
-
const { KimuEngine } = await import('./kimu-engine');
|
|
125
|
-
|
|
126
|
-
// Update style in all extensions
|
|
127
|
-
console.log(`[KimuGlobalStyles] Updating style "${id}" in ${extensions.length} extensions`);
|
|
128
|
-
for (const ext of extensions) {
|
|
129
|
-
try {
|
|
130
|
-
await KimuEngine.injectStyle(ext, style.path, style.id);
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.warn(`[KimuGlobalStyles] Failed to update style in ${ext.tagName}:`, error);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* KimuGlobalStyles manages global CSS files that should be injected into all extensions.
|
|
3
|
+
* This allows modules (like the theme module) to register additional global styles
|
|
4
|
+
* without hardcoding dependencies in the core framework.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* - Core always injects 'assets/kimu-style.css' as the base style
|
|
8
|
+
* - Modules can register additional global CSS using registerGlobalStyle()
|
|
9
|
+
* - All registered styles are automatically injected into every extension's shadow root
|
|
10
|
+
*/
|
|
11
|
+
export class KimuGlobalStyles {
|
|
12
|
+
/** List of global CSS files to inject in all extensions */
|
|
13
|
+
private static _globalStyles: Array<{ id: string; path: string }> = [
|
|
14
|
+
// Default KIMU base styles - always included
|
|
15
|
+
{ id: 'kimu-style-default', path: 'assets/kimu-style.css' }
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register a global CSS file to be injected in all extensions.
|
|
20
|
+
* This is useful for modules that want to provide global theming or styling.
|
|
21
|
+
*
|
|
22
|
+
* @param id - Unique identifier for the style (e.g., 'theme-dark', 'theme-light')
|
|
23
|
+
* @param path - Path to the CSS file relative to the base path
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // In a theme module
|
|
27
|
+
* KimuGlobalStyles.registerGlobalStyle('theme-active', 'themes/theme-dark.css');
|
|
28
|
+
*/
|
|
29
|
+
static registerGlobalStyle(id: string, path: string): void {
|
|
30
|
+
// Check if already registered
|
|
31
|
+
const existing = this._globalStyles.find(s => s.id === id);
|
|
32
|
+
if (existing) {
|
|
33
|
+
// Update path if already registered
|
|
34
|
+
existing.path = path;
|
|
35
|
+
console.log(`[KimuGlobalStyles] Updated global style: ${id} -> ${path}`);
|
|
36
|
+
} else {
|
|
37
|
+
// Add new global style
|
|
38
|
+
this._globalStyles.push({ id, path });
|
|
39
|
+
console.log(`[KimuGlobalStyles] Registered global style: ${id} -> ${path}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Unregister a global CSS file.
|
|
45
|
+
*
|
|
46
|
+
* @param id - Unique identifier of the style to remove
|
|
47
|
+
*/
|
|
48
|
+
static unregisterGlobalStyle(id: string): void {
|
|
49
|
+
const index = this._globalStyles.findIndex(s => s.id === id);
|
|
50
|
+
if (index !== -1) {
|
|
51
|
+
this._globalStyles.splice(index, 1);
|
|
52
|
+
console.log(`[KimuGlobalStyles] Unregistered global style: ${id}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get all registered global styles.
|
|
58
|
+
* Used internally by KimuComponentElement to inject styles into extensions.
|
|
59
|
+
*
|
|
60
|
+
* @returns Array of registered global styles
|
|
61
|
+
*/
|
|
62
|
+
static getGlobalStyles(): Array<{ id: string; path: string }> {
|
|
63
|
+
return [...this._globalStyles]; // Return a copy to prevent external modifications
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Clear all registered styles (useful for testing or reset scenarios).
|
|
68
|
+
* Note: This will also remove the default kimu-style.css.
|
|
69
|
+
*/
|
|
70
|
+
static clearAllStyles(): void {
|
|
71
|
+
this._globalStyles = [
|
|
72
|
+
{ id: 'kimu-style-default', path: 'assets/kimu-style.css' }
|
|
73
|
+
];
|
|
74
|
+
console.log('[KimuGlobalStyles] Cleared all global styles, keeping only default');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the number of registered global styles.
|
|
79
|
+
*/
|
|
80
|
+
static getStyleCount(): number {
|
|
81
|
+
return this._globalStyles.length;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if a specific style ID is registered.
|
|
86
|
+
*/
|
|
87
|
+
static hasStyle(id: string): boolean {
|
|
88
|
+
return this._globalStyles.some(s => s.id === id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Update a specific global style in all currently loaded extensions.
|
|
93
|
+
* This is useful when a theme changes and needs to be propagated to all extensions.
|
|
94
|
+
*
|
|
95
|
+
* @param id - Style ID to update (e.g., 'kimu-theme-active')
|
|
96
|
+
*
|
|
97
|
+
* Note: This method uses KimuEngine.injectStyle() to update styles in all shadow roots.
|
|
98
|
+
* It requires KimuEngine to be imported dynamically to avoid circular dependencies.
|
|
99
|
+
*/
|
|
100
|
+
static async updateStyleInAllExtensions(id: string): Promise<void> {
|
|
101
|
+
const style = this._globalStyles.find(s => s.id === id);
|
|
102
|
+
if (!style) {
|
|
103
|
+
console.warn(`[KimuGlobalStyles] Style "${id}" not found, cannot update`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Find all custom elements (extensions) in the DOM
|
|
108
|
+
const allCustomElements = document.querySelectorAll('*');
|
|
109
|
+
const extensions: HTMLElement[] = [];
|
|
110
|
+
|
|
111
|
+
allCustomElements.forEach((el) => {
|
|
112
|
+
// Check if element is a custom element with shadow root
|
|
113
|
+
if (el.tagName.includes('-') && (el as any).shadowRoot) {
|
|
114
|
+
extensions.push(el as HTMLElement);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (extensions.length === 0) {
|
|
119
|
+
console.log('[KimuGlobalStyles] No extensions found to update');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Dynamically import KimuEngine to avoid circular dependency
|
|
124
|
+
const { KimuEngine } = await import('./kimu-engine');
|
|
125
|
+
|
|
126
|
+
// Update style in all extensions
|
|
127
|
+
console.log(`[KimuGlobalStyles] Updating style "${id}" in ${extensions.length} extensions`);
|
|
128
|
+
for (const ext of extensions) {
|
|
129
|
+
try {
|
|
130
|
+
await KimuEngine.injectStyle(ext, style.path, style.id);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.warn(`[KimuGlobalStyles] Failed to update style in ${ext.tagName}:`, error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -1,196 +1,196 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* KIMU Reactive System - Optimized & Minimal
|
|
3
|
-
*
|
|
4
|
-
* Lightweight reactive property decorator for KIMU components.
|
|
5
|
-
* Designed for simplicity, performance, and minimal bundle size.
|
|
6
|
-
*
|
|
7
|
-
* Philosophy: KIMU is intentionally minimal. We focus on doing the essentials
|
|
8
|
-
* perfectly, not everything adequately. For complex reactivity needs, use Vue/React.
|
|
9
|
-
*
|
|
10
|
-
* Usage Example:
|
|
11
|
-
*
|
|
12
|
-
* ```typescript
|
|
13
|
-
* export class MyComponent extends KimuComponentElement {
|
|
14
|
-
* @property({ type: Number })
|
|
15
|
-
* counter = 0;
|
|
16
|
-
*
|
|
17
|
-
* @property({ type: String, reflect: true })
|
|
18
|
-
* label = 'Counter';
|
|
19
|
-
*
|
|
20
|
-
* increment() {
|
|
21
|
-
* this.counter++; // Auto re-render!
|
|
22
|
-
* }
|
|
23
|
-
* }
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Options for the @property decorator
|
|
29
|
-
*/
|
|
30
|
-
export interface PropertyOptions {
|
|
31
|
-
/**
|
|
32
|
-
* The type of the property. Used for type conversion.
|
|
33
|
-
* Supported types: String, Number, Boolean
|
|
34
|
-
*/
|
|
35
|
-
type?: StringConstructor | NumberConstructor | BooleanConstructor;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Whether the property should reflect to an attribute.
|
|
39
|
-
* If true, changes to the property will update the corresponding HTML attribute.
|
|
40
|
-
*/
|
|
41
|
-
reflect?: boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Convert property name to attribute name (camelCase to kebab-case)
|
|
46
|
-
*/
|
|
47
|
-
const toKebab = (str: string): string => str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Convert value to specified type
|
|
51
|
-
* @param value - Value to convert
|
|
52
|
-
* @param type - Target type constructor
|
|
53
|
-
* @returns Converted value
|
|
54
|
-
*/
|
|
55
|
-
function convert(value: any, type?: any): any {
|
|
56
|
-
if (!type) return value;
|
|
57
|
-
if (type === Number) return value == null ? 0 : Number(value) || 0;
|
|
58
|
-
if (type === Boolean) return value !== null && value !== 'false' && value !== false;
|
|
59
|
-
if (type === String) return value == null ? '' : String(value);
|
|
60
|
-
return value;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Reactive property decorator
|
|
65
|
-
*
|
|
66
|
-
* Transforms a class property into a reactive property that automatically
|
|
67
|
-
* triggers re-rendering when its value changes.
|
|
68
|
-
*
|
|
69
|
-
* @param options - Configuration options for the reactive property
|
|
70
|
-
* @returns Property decorator function
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```typescript
|
|
74
|
-
* export class Counter extends KimuComponentElement {
|
|
75
|
-
* @property({ type: Number })
|
|
76
|
-
* count = 0;
|
|
77
|
-
*
|
|
78
|
-
* @property({ type: String, reflect: true })
|
|
79
|
-
* label = 'Counter';
|
|
80
|
-
* }
|
|
81
|
-
* ```
|
|
82
|
-
*/
|
|
83
|
-
export function property(options: PropertyOptions = {}) {
|
|
84
|
-
return function (target: any, propertyKey: string) {
|
|
85
|
-
const privateKey = `__${propertyKey}`;
|
|
86
|
-
const { type, reflect = false } = options;
|
|
87
|
-
const attrName = toKebab(propertyKey);
|
|
88
|
-
|
|
89
|
-
// Store metadata for attribute observation
|
|
90
|
-
const ctor = target.constructor;
|
|
91
|
-
if (!ctor.__kimu_reactive_props__) {
|
|
92
|
-
ctor.__kimu_reactive_props__ = new Map();
|
|
93
|
-
ctor.__kimu_attr_to_prop__ = new Map(); // Inverse map for O(1) lookup
|
|
94
|
-
}
|
|
95
|
-
ctor.__kimu_reactive_props__.set(propertyKey, { type, reflect, attribute: attrName });
|
|
96
|
-
ctor.__kimu_attr_to_prop__.set(attrName, { propertyKey, type }); // Store inverse mapping
|
|
97
|
-
|
|
98
|
-
// Define the reactive property with optimized getter/setter
|
|
99
|
-
Object.defineProperty(target, propertyKey, {
|
|
100
|
-
get() {
|
|
101
|
-
return this[privateKey];
|
|
102
|
-
},
|
|
103
|
-
set(value: any) {
|
|
104
|
-
const oldValue = this[privateKey];
|
|
105
|
-
const newValue = type ? convert(value, type) : value;
|
|
106
|
-
|
|
107
|
-
// Early return if value hasn't changed
|
|
108
|
-
if (oldValue === newValue) return;
|
|
109
|
-
|
|
110
|
-
this[privateKey] = newValue;
|
|
111
|
-
|
|
112
|
-
// Reflect to attribute if enabled (optimized conditionals)
|
|
113
|
-
if (reflect && this instanceof HTMLElement) {
|
|
114
|
-
if (newValue == null || newValue === false) {
|
|
115
|
-
this.removeAttribute(attrName);
|
|
116
|
-
} else {
|
|
117
|
-
this.setAttribute(attrName, type === Boolean ? '' : String(newValue));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Batched re-render using RAF (prevents multiple renders in same frame)
|
|
122
|
-
// Call refresh directly - it already handles RAF batching internally
|
|
123
|
-
if (typeof this.refresh === 'function') {
|
|
124
|
-
this.refresh();
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
enumerable: true,
|
|
128
|
-
configurable: true
|
|
129
|
-
});
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Initialize reactive properties from attributes
|
|
136
|
-
*
|
|
137
|
-
* This should be called in the connectedCallback of components that use @property
|
|
138
|
-
*
|
|
139
|
-
* @param component - The component instance
|
|
140
|
-
*/
|
|
141
|
-
export function initReactiveProperties(component: any): void {
|
|
142
|
-
const props = component.constructor.__kimu_reactive_props__;
|
|
143
|
-
if (!props) return;
|
|
144
|
-
|
|
145
|
-
// Initialize properties from attributes
|
|
146
|
-
props.forEach((options: any, propertyKey: string) => {
|
|
147
|
-
const attrValue = component.getAttribute(options.attribute);
|
|
148
|
-
if (attrValue !== null) {
|
|
149
|
-
(component as any)[propertyKey] = convert(attrValue, options.type);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Get observed attributes for reactive properties
|
|
156
|
-
*
|
|
157
|
-
* @param componentClass - The component class
|
|
158
|
-
* @returns Array of observed attributes
|
|
159
|
-
*/
|
|
160
|
-
export function getObservedAttributes(componentClass: any): string[] {
|
|
161
|
-
const props = componentClass.__kimu_reactive_props__;
|
|
162
|
-
if (!props) return [];
|
|
163
|
-
|
|
164
|
-
return Array.from(props.values())
|
|
165
|
-
.map((opts: any) => opts.attribute)
|
|
166
|
-
.filter(Boolean);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Handle attribute changes for reactive properties
|
|
171
|
-
*
|
|
172
|
-
* This should be called from attributeChangedCallback
|
|
173
|
-
*
|
|
174
|
-
* @param component - The component instance
|
|
175
|
-
* @param name - Attribute name
|
|
176
|
-
* @param oldValue - Old attribute value
|
|
177
|
-
* @param newValue - New attribute value
|
|
178
|
-
*/
|
|
179
|
-
export function handleAttributeChange(
|
|
180
|
-
component: any,
|
|
181
|
-
name: string,
|
|
182
|
-
_oldValue: string | null,
|
|
183
|
-
newValue: string | null
|
|
184
|
-
): void {
|
|
185
|
-
const attrMap = component.constructor.__kimu_attr_to_prop__;
|
|
186
|
-
if (!attrMap) return;
|
|
187
|
-
|
|
188
|
-
// O(1) lookup using inverse map
|
|
189
|
-
const propInfo = attrMap.get(name);
|
|
190
|
-
if (propInfo) {
|
|
191
|
-
component[propInfo.propertyKey] = newValue !== null
|
|
192
|
-
? convert(newValue, propInfo.type)
|
|
193
|
-
: null;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
1
|
+
/**
|
|
2
|
+
* KIMU Reactive System - Optimized & Minimal
|
|
3
|
+
*
|
|
4
|
+
* Lightweight reactive property decorator for KIMU components.
|
|
5
|
+
* Designed for simplicity, performance, and minimal bundle size.
|
|
6
|
+
*
|
|
7
|
+
* Philosophy: KIMU is intentionally minimal. We focus on doing the essentials
|
|
8
|
+
* perfectly, not everything adequately. For complex reactivity needs, use Vue/React.
|
|
9
|
+
*
|
|
10
|
+
* Usage Example:
|
|
11
|
+
*
|
|
12
|
+
* ```typescript
|
|
13
|
+
* export class MyComponent extends KimuComponentElement {
|
|
14
|
+
* @property({ type: Number })
|
|
15
|
+
* counter = 0;
|
|
16
|
+
*
|
|
17
|
+
* @property({ type: String, reflect: true })
|
|
18
|
+
* label = 'Counter';
|
|
19
|
+
*
|
|
20
|
+
* increment() {
|
|
21
|
+
* this.counter++; // Auto re-render!
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options for the @property decorator
|
|
29
|
+
*/
|
|
30
|
+
export interface PropertyOptions {
|
|
31
|
+
/**
|
|
32
|
+
* The type of the property. Used for type conversion.
|
|
33
|
+
* Supported types: String, Number, Boolean
|
|
34
|
+
*/
|
|
35
|
+
type?: StringConstructor | NumberConstructor | BooleanConstructor;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether the property should reflect to an attribute.
|
|
39
|
+
* If true, changes to the property will update the corresponding HTML attribute.
|
|
40
|
+
*/
|
|
41
|
+
reflect?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Convert property name to attribute name (camelCase to kebab-case)
|
|
46
|
+
*/
|
|
47
|
+
const toKebab = (str: string): string => str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Convert value to specified type
|
|
51
|
+
* @param value - Value to convert
|
|
52
|
+
* @param type - Target type constructor
|
|
53
|
+
* @returns Converted value
|
|
54
|
+
*/
|
|
55
|
+
function convert(value: any, type?: any): any {
|
|
56
|
+
if (!type) return value;
|
|
57
|
+
if (type === Number) return value == null ? 0 : Number(value) || 0;
|
|
58
|
+
if (type === Boolean) return value !== null && value !== 'false' && value !== false;
|
|
59
|
+
if (type === String) return value == null ? '' : String(value);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Reactive property decorator
|
|
65
|
+
*
|
|
66
|
+
* Transforms a class property into a reactive property that automatically
|
|
67
|
+
* triggers re-rendering when its value changes.
|
|
68
|
+
*
|
|
69
|
+
* @param options - Configuration options for the reactive property
|
|
70
|
+
* @returns Property decorator function
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* export class Counter extends KimuComponentElement {
|
|
75
|
+
* @property({ type: Number })
|
|
76
|
+
* count = 0;
|
|
77
|
+
*
|
|
78
|
+
* @property({ type: String, reflect: true })
|
|
79
|
+
* label = 'Counter';
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function property(options: PropertyOptions = {}) {
|
|
84
|
+
return function (target: any, propertyKey: string) {
|
|
85
|
+
const privateKey = `__${propertyKey}`;
|
|
86
|
+
const { type, reflect = false } = options;
|
|
87
|
+
const attrName = toKebab(propertyKey);
|
|
88
|
+
|
|
89
|
+
// Store metadata for attribute observation
|
|
90
|
+
const ctor = target.constructor;
|
|
91
|
+
if (!ctor.__kimu_reactive_props__) {
|
|
92
|
+
ctor.__kimu_reactive_props__ = new Map();
|
|
93
|
+
ctor.__kimu_attr_to_prop__ = new Map(); // Inverse map for O(1) lookup
|
|
94
|
+
}
|
|
95
|
+
ctor.__kimu_reactive_props__.set(propertyKey, { type, reflect, attribute: attrName });
|
|
96
|
+
ctor.__kimu_attr_to_prop__.set(attrName, { propertyKey, type }); // Store inverse mapping
|
|
97
|
+
|
|
98
|
+
// Define the reactive property with optimized getter/setter
|
|
99
|
+
Object.defineProperty(target, propertyKey, {
|
|
100
|
+
get() {
|
|
101
|
+
return this[privateKey];
|
|
102
|
+
},
|
|
103
|
+
set(value: any) {
|
|
104
|
+
const oldValue = this[privateKey];
|
|
105
|
+
const newValue = type ? convert(value, type) : value;
|
|
106
|
+
|
|
107
|
+
// Early return if value hasn't changed
|
|
108
|
+
if (oldValue === newValue) return;
|
|
109
|
+
|
|
110
|
+
this[privateKey] = newValue;
|
|
111
|
+
|
|
112
|
+
// Reflect to attribute if enabled (optimized conditionals)
|
|
113
|
+
if (reflect && this instanceof HTMLElement) {
|
|
114
|
+
if (newValue == null || newValue === false) {
|
|
115
|
+
this.removeAttribute(attrName);
|
|
116
|
+
} else {
|
|
117
|
+
this.setAttribute(attrName, type === Boolean ? '' : String(newValue));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Batched re-render using RAF (prevents multiple renders in same frame)
|
|
122
|
+
// Call refresh directly - it already handles RAF batching internally
|
|
123
|
+
if (typeof this.refresh === 'function') {
|
|
124
|
+
this.refresh();
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
enumerable: true,
|
|
128
|
+
configurable: true
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Initialize reactive properties from attributes
|
|
136
|
+
*
|
|
137
|
+
* This should be called in the connectedCallback of components that use @property
|
|
138
|
+
*
|
|
139
|
+
* @param component - The component instance
|
|
140
|
+
*/
|
|
141
|
+
export function initReactiveProperties(component: any): void {
|
|
142
|
+
const props = component.constructor.__kimu_reactive_props__;
|
|
143
|
+
if (!props) return;
|
|
144
|
+
|
|
145
|
+
// Initialize properties from attributes
|
|
146
|
+
props.forEach((options: any, propertyKey: string) => {
|
|
147
|
+
const attrValue = component.getAttribute(options.attribute);
|
|
148
|
+
if (attrValue !== null) {
|
|
149
|
+
(component as any)[propertyKey] = convert(attrValue, options.type);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get observed attributes for reactive properties
|
|
156
|
+
*
|
|
157
|
+
* @param componentClass - The component class
|
|
158
|
+
* @returns Array of observed attributes
|
|
159
|
+
*/
|
|
160
|
+
export function getObservedAttributes(componentClass: any): string[] {
|
|
161
|
+
const props = componentClass.__kimu_reactive_props__;
|
|
162
|
+
if (!props) return [];
|
|
163
|
+
|
|
164
|
+
return Array.from(props.values())
|
|
165
|
+
.map((opts: any) => opts.attribute)
|
|
166
|
+
.filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Handle attribute changes for reactive properties
|
|
171
|
+
*
|
|
172
|
+
* This should be called from attributeChangedCallback
|
|
173
|
+
*
|
|
174
|
+
* @param component - The component instance
|
|
175
|
+
* @param name - Attribute name
|
|
176
|
+
* @param oldValue - Old attribute value
|
|
177
|
+
* @param newValue - New attribute value
|
|
178
|
+
*/
|
|
179
|
+
export function handleAttributeChange(
|
|
180
|
+
component: any,
|
|
181
|
+
name: string,
|
|
182
|
+
_oldValue: string | null,
|
|
183
|
+
newValue: string | null
|
|
184
|
+
): void {
|
|
185
|
+
const attrMap = component.constructor.__kimu_attr_to_prop__;
|
|
186
|
+
if (!attrMap) return;
|
|
187
|
+
|
|
188
|
+
// O(1) lookup using inverse map
|
|
189
|
+
const propInfo = attrMap.get(name);
|
|
190
|
+
if (propInfo) {
|
|
191
|
+
component[propInfo.propertyKey] = newValue !== null
|
|
192
|
+
? convert(newValue, propInfo.type)
|
|
193
|
+
: null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|