@kupola/kupola 1.2.0 → 1.4.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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +330 -286
  3. package/adapters/axios.d.ts +34 -0
  4. package/adapters/axios.js +122 -0
  5. package/adapters/navios-http.d.ts +110 -0
  6. package/adapters/navios-http.js +151 -0
  7. package/css/components-ext.css +157 -0
  8. package/css/components.css +69 -0
  9. package/css/theme-dark.css +5 -5
  10. package/css/utilities.css +119 -1
  11. package/dist/css/accessibility.css +119 -0
  12. package/dist/css/animations.css +224 -0
  13. package/dist/css/brand-themes.css +300 -0
  14. package/dist/css/colors_and_type.css +441 -0
  15. package/dist/css/components-ext.css +4165 -0
  16. package/dist/css/components.css +1483 -0
  17. package/dist/css/kupola.css +1 -12
  18. package/dist/css/kupola.min.css +1 -0
  19. package/dist/css/responsive.css +697 -0
  20. package/dist/css/scaffold.css +145 -0
  21. package/dist/css/states.css +316 -0
  22. package/dist/css/theme-dark.css +296 -0
  23. package/dist/css/theme-light.css +296 -0
  24. package/dist/css/utilities.css +171 -0
  25. package/dist/icons.svg +284 -0
  26. package/dist/kupola.cjs.js +17409 -257
  27. package/dist/kupola.cjs.js.map +1 -1
  28. package/dist/kupola.css +9000 -0
  29. package/dist/kupola.esm.js +17183 -7597
  30. package/dist/kupola.esm.js.map +1 -1
  31. package/dist/kupola.min.css +1 -0
  32. package/dist/kupola.min.js +2 -0
  33. package/dist/kupola.min.js.map +1 -0
  34. package/dist/kupola.umd.js +17415 -257
  35. package/dist/kupola.umd.js.map +1 -1
  36. package/dist/plugins/vite-plugin-kupola.js +120 -0
  37. package/dist/types/kupola.d.ts +421 -323
  38. package/js/calendar.js +334 -25
  39. package/js/carousel.js +182 -48
  40. package/js/collapse.js +148 -34
  41. package/js/color-picker.js +416 -108
  42. package/js/component.js +8 -19
  43. package/js/countdown.js +9 -8
  44. package/js/data-bind.js +73 -16
  45. package/js/datepicker.js +488 -110
  46. package/js/depends.js +710 -0
  47. package/js/dialog.js +4 -2
  48. package/js/drawer.js +172 -8
  49. package/js/dropdown.js +272 -17
  50. package/js/dynamic-tags.js +156 -40
  51. package/js/fileupload.js +9 -8
  52. package/js/form.js +280 -254
  53. package/js/global-events.js +281 -188
  54. package/js/heatmap.js +10 -7
  55. package/js/i18n.js +18 -10
  56. package/js/icons.js +141 -161
  57. package/js/image-preview.js +146 -2
  58. package/js/initializer.js +151 -71
  59. package/js/kupola-core.js +123 -45
  60. package/js/kupola-lifecycle.js +13 -11
  61. package/js/message.js +8 -1
  62. package/js/modal.js +207 -59
  63. package/js/notification.js +8 -1
  64. package/js/numberinput.js +9 -8
  65. package/js/pagination.js +263 -0
  66. package/js/registry.js +29 -12
  67. package/js/select.js +482 -27
  68. package/js/slide-captcha.js +11 -2
  69. package/js/slider.js +442 -25
  70. package/js/statcard.js +9 -7
  71. package/js/table.js +1210 -0
  72. package/js/tag.js +268 -14
  73. package/js/theme-standalone.js +88 -0
  74. package/js/theme.js +14 -43
  75. package/js/timepicker.js +335 -66
  76. package/js/tooltip.js +317 -86
  77. package/js/utils.js +6 -2
  78. package/js/validation.js +6 -2
  79. package/js/virtual-list.js +11 -7
  80. package/js/web-components.js +288 -0
  81. package/package.json +81 -67
  82. package/plugins/vite-plugin-kupola.js +120 -0
  83. package/scripts/build-css.cjs +113 -0
  84. package/scripts/build-svg-sprite.cjs +107 -0
  85. package/types/kupola.d.ts +421 -323
  86. package/CHANGELOG.md +0 -130
  87. package/INTEGRATION.md +0 -440
  88. package/PROJECT_SUMMARY.md +0 -312
  89. package/SKILL.md +0 -572
  90. package/dist/utils/utils/Kupola.cs +0 -77
  91. package/dist/utils/utils/Kupola.java +0 -104
  92. package/dist/utils/utils/kupola.go +0 -120
  93. package/dist/utils/utils/kupola.js +0 -63
  94. package/dist/utils/utils/kupola.py +0 -1392
  95. package/dist/utils/utils/kupola.rb +0 -69
  96. package/js/composition-api.js +0 -458
  97. package/js/error-handler.js +0 -181
  98. package/js/http.js +0 -419
  99. package/js/kupola-devtools.js +0 -598
  100. package/js/performance.js +0 -250
  101. package/js/router.js +0 -396
  102. package/js/security.js +0 -189
  103. package/js/test-utils.js +0 -251
  104. package/templates/base.html +0 -30
  105. package/templates/base_dashboard.html +0 -99
  106. package/utils/Kupola.cs +0 -77
  107. package/utils/Kupola.java +0 -104
  108. package/utils/kupola.go +0 -120
  109. package/utils/kupola.js +0 -63
  110. package/utils/kupola.py +0 -1392
  111. package/utils/kupola.rb +0 -69
package/js/security.js DELETED
@@ -1,189 +0,0 @@
1
- class KupolaSecurity {
2
- constructor() {
3
- this.csrfToken = null;
4
- this.cspEnabled = false;
5
- }
6
-
7
- init() {
8
- this._initCSRF();
9
- this._initCSP();
10
- }
11
-
12
- _initCSRF() {
13
- const metaTag = document.querySelector('meta[name="csrf-token"]');
14
- if (metaTag) {
15
- this.csrfToken = metaTag.getAttribute('content');
16
- }
17
- }
18
-
19
- _initCSP() {
20
- const metaTag = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
21
- if (metaTag) {
22
- this.cspEnabled = true;
23
- }
24
- }
25
-
26
- getCSRFToken() {
27
- return this.csrfToken;
28
- }
29
-
30
- setCSRFToken(token) {
31
- this.csrfToken = token;
32
- const metaTag = document.querySelector('meta[name="csrf-token"]');
33
- if (metaTag) {
34
- metaTag.setAttribute('content', token);
35
- }
36
- }
37
-
38
- addCSRFHeader(config) {
39
- if (this.csrfToken) {
40
- config.headers = config.headers || {};
41
- config.headers['X-CSRF-Token'] = this.csrfToken;
42
- }
43
- return config;
44
- }
45
-
46
- escapeHtml(str) {
47
- if (!str || typeof str !== 'string') return str;
48
- const div = document.createElement('div');
49
- div.textContent = str;
50
- return div.innerHTML;
51
- }
52
-
53
- sanitize(html) {
54
- if (!html || typeof html !== 'string') return html;
55
-
56
- const allowedTags = [
57
- 'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'sub', 'sup',
58
- 'a', 'br', 'p', 'span', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
59
- 'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tr', 'td', 'th',
60
- 'img', 'video', 'audio', 'iframe'
61
- ];
62
-
63
- const allowedAttributes = [
64
- 'href', 'src', 'alt', 'title', 'class', 'id', 'data-*',
65
- 'width', 'height', 'frameborder', 'allowfullscreen'
66
- ];
67
-
68
- const tempDiv = document.createElement('div');
69
- tempDiv.innerHTML = html;
70
-
71
- const walk = (node) => {
72
- if (node.nodeType === Node.ELEMENT_NODE) {
73
- const tagName = node.tagName.toLowerCase();
74
- if (!allowedTags.includes(tagName)) {
75
- node.parentNode.replaceChild(document.createTextNode(node.textContent), node);
76
- return;
77
- }
78
-
79
- Array.from(node.attributes).forEach(attr => {
80
- const attrName = attr.name.toLowerCase();
81
- const isAllowed = allowedAttributes.includes(attrName) ||
82
- attrName.startsWith('data-');
83
-
84
- if (!isAllowed) {
85
- node.removeAttribute(attr.name);
86
- } else if (attrName === 'href' || attrName === 'src') {
87
- if (!this._isSafeUrl(attr.value)) {
88
- node.removeAttribute(attr.name);
89
- }
90
- }
91
- });
92
- }
93
-
94
- for (let i = node.childNodes.length - 1; i >= 0; i--) {
95
- walk(node.childNodes[i]);
96
- }
97
- };
98
-
99
- walk(tempDiv);
100
- return tempDiv.innerHTML;
101
- }
102
-
103
- _isSafeUrl(url) {
104
- try {
105
- const parsed = new URL(url);
106
- const safeProtocols = ['http:', 'https:', 'data:', 'mailto:', 'tel:'];
107
- return safeProtocols.includes(parsed.protocol);
108
- } catch {
109
- return false;
110
- }
111
- }
112
-
113
- validateEmail(email) {
114
- const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
115
- return re.test(email);
116
- }
117
-
118
- validateURL(url) {
119
- try {
120
- new URL(url);
121
- return true;
122
- } catch {
123
- return false;
124
- }
125
- }
126
-
127
- validatePhone(phone) {
128
- const re = /^[\d\s\-\+\(\)]{7,20}$/;
129
- return re.test(phone.replace(/\s/g, ''));
130
- }
131
-
132
- generateSecureHash(str) {
133
- if (!str || typeof str !== 'string') return '';
134
-
135
- let hash = 0;
136
- for (let i = 0; i < str.length; i++) {
137
- const char = str.charCodeAt(i);
138
- hash = ((hash << 5) - hash) + char;
139
- hash = hash & hash;
140
- }
141
- return Math.abs(hash).toString(36);
142
- }
143
-
144
- isXSSAttack(str) {
145
- if (!str || typeof str !== 'string') return false;
146
-
147
- const xssPatterns = [
148
- /<script[^>]*>.*?<\/script>/gi,
149
- /<img[^>]*onerror[^>]*>/gi,
150
- /javascript:/gi,
151
- /vbscript:/gi,
152
- /onload[^>]*=/gi,
153
- /onclick[^>]*=/gi,
154
- /onmouseover[^>]*=/gi,
155
- /<iframe[^>]*>/gi
156
- ];
157
-
158
- return xssPatterns.some(pattern => pattern.test(str));
159
- }
160
-
161
- protectForm(form) {
162
- if (!(form instanceof HTMLFormElement)) return;
163
-
164
- form.addEventListener('submit', (e) => {
165
- const inputs = form.querySelectorAll('input, textarea');
166
- let hasXSS = false;
167
-
168
- inputs.forEach(input => {
169
- if (this.isXSSAttack(input.value)) {
170
- hasXSS = true;
171
- input.classList.add('ds-input--error');
172
- } else {
173
- input.classList.remove('ds-input--error');
174
- }
175
- });
176
-
177
- if (hasXSS) {
178
- e.preventDefault();
179
- console.error('[KupolaSecurity] XSS attack detected in form');
180
- }
181
- });
182
- }
183
- }
184
-
185
- const kupolaSecurity = new KupolaSecurity();
186
- window.kupolaSecurity = kupolaSecurity;
187
- window.KupolaSecurity = KupolaSecurity;
188
-
189
- export default KupolaSecurity;
package/js/test-utils.js DELETED
@@ -1,251 +0,0 @@
1
- class KupolaTestUtils {
2
- static createElement(html) {
3
- const container = document.createElement('div');
4
- container.innerHTML = html;
5
- return container.firstElementChild;
6
- }
7
-
8
- static mountComponent(ComponentClass, props = {}, html = '') {
9
- const element = this.createElement(html);
10
- const component = new ComponentClass(element);
11
- Object.assign(component.props, props);
12
- return { component, element };
13
- }
14
-
15
- static async waitFor(condition, timeout = 1000) {
16
- const startTime = Date.now();
17
- return new Promise((resolve, reject) => {
18
- const check = async () => {
19
- try {
20
- const result = await condition();
21
- if (result) {
22
- resolve(result);
23
- } else if (Date.now() - startTime < timeout) {
24
- requestAnimationFrame(check);
25
- } else {
26
- reject(new Error('waitFor timeout'));
27
- }
28
- } catch (error) {
29
- if (Date.now() - startTime < timeout) {
30
- requestAnimationFrame(check);
31
- } else {
32
- reject(new Error('waitFor timeout'));
33
- }
34
- }
35
- };
36
- requestAnimationFrame(check);
37
- });
38
- }
39
-
40
- static async waitForElement(selector, parent = document, timeout = 1000) {
41
- return this.waitFor(() => parent.querySelector(selector), timeout);
42
- }
43
-
44
- static async waitForText(text, parent = document, timeout = 1000) {
45
- return this.waitFor(() => parent.textContent.includes(text), timeout);
46
- }
47
-
48
- static fireEvent(element, eventName, detail = {}) {
49
- const event = new CustomEvent(eventName, {
50
- bubbles: true,
51
- cancelable: true,
52
- detail
53
- });
54
- element.dispatchEvent(event);
55
- }
56
-
57
- static fireInput(element, value) {
58
- element.value = value;
59
- this.fireEvent(element, 'input');
60
- this.fireEvent(element, 'change');
61
- }
62
-
63
- static fireClick(element) {
64
- this.fireEvent(element, 'click');
65
- }
66
-
67
- static fireKeyDown(element, key) {
68
- const event = new KeyboardEvent('keydown', { key, bubbles: true });
69
- element.dispatchEvent(event);
70
- }
71
-
72
- static async nextTick() {
73
- return new Promise(resolve => requestAnimationFrame(resolve));
74
- }
75
-
76
- static mockDate(date) {
77
- const originalNow = Date.now;
78
- const originalDate = Date;
79
-
80
- Date.now = () => date.getTime();
81
- globalThis.Date = class extends Date {
82
- constructor(...args) {
83
- super(...args);
84
- if (args.length === 0) {
85
- return new Date(date);
86
- }
87
- return new originalDate(...args);
88
- }
89
- };
90
-
91
- return () => {
92
- Date.now = originalNow;
93
- globalThis.Date = originalDate;
94
- };
95
- }
96
-
97
- static mockFetch(mockResponses) {
98
- const originalFetch = fetch;
99
-
100
- globalThis.fetch = jest.fn(async (url, options) => {
101
- for (const mock of mockResponses) {
102
- if (typeof mock.url === 'string' && url === mock.url) {
103
- return mock.response();
104
- }
105
- if (mock.url instanceof RegExp && mock.url.test(url)) {
106
- return mock.response();
107
- }
108
- if (mock.method && options && options.method === mock.method) {
109
- return mock.response();
110
- }
111
- }
112
- return originalFetch(url, options);
113
- });
114
-
115
- return () => {
116
- globalThis.fetch = originalFetch;
117
- };
118
- }
119
-
120
- static async mountAndWait(ComponentClass, props = {}, html = '') {
121
- const { component, element } = this.mountComponent(ComponentClass, props, html);
122
- await component.mount();
123
- await this.nextTick();
124
- return { component, element };
125
- }
126
-
127
- static async unmountAndWait(component) {
128
- await component.unmount();
129
- await this.nextTick();
130
- }
131
-
132
- static getByText(parent, text) {
133
- const elements = parent.querySelectorAll('*');
134
- for (const element of elements) {
135
- if (element.textContent.trim() === text) {
136
- return element;
137
- }
138
- }
139
- throw new Error(`Element with text "${text}" not found`);
140
- }
141
-
142
- static queryByText(parent, text) {
143
- const elements = parent.querySelectorAll('*');
144
- for (const element of elements) {
145
- if (element.textContent.trim() === text) {
146
- return element;
147
- }
148
- }
149
- return null;
150
- }
151
-
152
- static getByTestId(parent, testId) {
153
- const element = parent.querySelector(`[data-testid="${testId}"]`);
154
- if (!element) {
155
- throw new Error(`Element with data-testid="${testId}" not found`);
156
- }
157
- return element;
158
- }
159
-
160
- static queryByTestId(parent, testId) {
161
- return parent.querySelector(`[data-testid="${testId}"]`);
162
- }
163
-
164
- static getByRole(parent, role) {
165
- const element = parent.querySelector(`[role="${role}"]`);
166
- if (!element) {
167
- throw new Error(`Element with role="${role}" not found`);
168
- }
169
- return element;
170
- }
171
-
172
- static queryByRole(parent, role) {
173
- return parent.querySelector(`[role="${role}"]`);
174
- }
175
-
176
- static async simulateDrag(element, startX, startY, endX, endY) {
177
- this.fireEvent(element, 'dragstart', {
178
- clientX: startX,
179
- clientY: startY
180
- });
181
-
182
- this.fireEvent(element, 'drag', {
183
- clientX: (startX + endX) / 2,
184
- clientY: (startY + endY) / 2
185
- });
186
-
187
- this.fireEvent(element, 'dragend', {
188
- clientX: endX,
189
- clientY: endY
190
- });
191
- }
192
-
193
- static async simulateTouch(element, startX, startY, endX, endY) {
194
- const touchStart = new TouchEvent('touchstart', {
195
- touches: [{ clientX: startX, clientY: startY }],
196
- bubbles: true
197
- });
198
- element.dispatchEvent(touchStart);
199
-
200
- const touchMove = new TouchEvent('touchmove', {
201
- touches: [{ clientX: (startX + endX) / 2, clientY: (startY + endY) / 2 }],
202
- bubbles: true
203
- });
204
- element.dispatchEvent(touchMove);
205
-
206
- const touchEnd = new TouchEvent('touchend', {
207
- touches: [{ clientX: endX, clientY: endY }],
208
- bubbles: true
209
- });
210
- element.dispatchEvent(touchEnd);
211
- }
212
-
213
- static async waitForState(component, stateKey, expectedValue) {
214
- return this.waitFor(() => {
215
- return component.state[stateKey] === expectedValue;
216
- });
217
- }
218
-
219
- static async waitForProps(component, propKey, expectedValue) {
220
- return this.waitFor(() => {
221
- return component.props[propKey] === expectedValue;
222
- });
223
- }
224
-
225
- static async waitForEmitted(component, eventName) {
226
- return this.waitFor(() => {
227
- return component._emitted && component._emitted[eventName];
228
- });
229
- }
230
-
231
- static spyOn(obj, methodName) {
232
- const original = obj[methodName];
233
- const spy = function(...args) {
234
- spy.called = true;
235
- spy.callCount = (spy.callCount || 0) + 1;
236
- spy.args = (spy.args || []).concat([args]);
237
- return original.apply(this, args);
238
- };
239
- obj[methodName] = spy;
240
- return () => {
241
- obj[methodName] = original;
242
- };
243
- }
244
-
245
- static async flushPromises() {
246
- return new Promise(resolve => setImmediate(resolve));
247
- }
248
- }
249
-
250
- window.KupolaTestUtils = KupolaTestUtils;
251
- window.testUtils = KupolaTestUtils;
@@ -1,30 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN" data-theme="{{ theme|default('dark') }}" data-brand="{{ brand|default('green') }}">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>{% block title %}{{ title|default('Nimbus') }}{% endblock %}</title>
7
-
8
- {% block head_css %}
9
- <link rel="stylesheet" href="{{ url_for('static', filename='nimbus/css/nimbus.css') }}">
10
- <link rel="stylesheet" href="{{ url_for('static', filename='nimbus/css/components-ext.css') }}">
11
- <link rel="stylesheet" href="{{ url_for('static', filename='nimbus/css/responsive.css') }}">
12
- <link rel="stylesheet" href="{{ url_for('static', filename='nimbus/css/accessibility.css') }}">
13
- <link rel="stylesheet" href="{{ url_for('static', filename='nimbus/css/animations.css') }}">
14
- {% endblock %}
15
-
16
- {% block head_js %}
17
- {% endblock %}
18
- </head>
19
- <body>
20
- {% block body %}
21
- {% endblock %}
22
-
23
- {% block body_js %}
24
- <script src="{{ url_for('static', filename='nimbus/js/icons.js') }}"></script>
25
- <script src="{{ url_for('static', filename='nimbus/js/theme.js') }}"></script>
26
- <script src="{{ url_for('static', filename='nimbus/js/validation.js') }}"></script>
27
- <script src="{{ url_for('static', filename='nimbus/js/data-bind.js') }}"></script>
28
- {% endblock %}
29
- </body>
30
- </html>
@@ -1,99 +0,0 @@
1
- {% extends "base.html" %}
2
-
3
- {% block head_css %}
4
- {{ super() }}
5
- <link rel="stylesheet" href="{{ url_for('static', filename='nimbus/dashboard/dashboard.css') }}">
6
- {% endblock %}
7
-
8
- {% block body %}
9
- <div class="ds-dashboard">
10
- <header class="ds-dashboard__header">
11
- <div class="ds-dashboard__header-left">
12
- <div class="ds-dashboard__logo">
13
- <div class="ds-dashboard__logo-icon"></div>
14
- <span>{{ logo|default('Nimbus') }}</span>
15
- </div>
16
- </div>
17
-
18
- <div class="ds-dashboard__header-center">
19
- {% block header_center %}{% endblock %}
20
- </div>
21
-
22
- <div class="ds-dashboard__header-right">
23
- {% block header_right %}
24
- <button class="ds-btn ds-btn--ghost ds-btn--sm ds-btn--icon" title="Search">
25
- <img src="{{ url_for('static', filename='nimbus/icons/search.svg') }}" width="16" height="16" alt="search" class="icon">
26
- </button>
27
- <button class="ds-btn ds-btn--ghost ds-btn--sm ds-btn--icon" title="Notifications">
28
- <img src="{{ url_for('static', filename='nimbus/icons/bell.svg') }}" width="16" height="16" alt="bell" class="icon">
29
- </button>
30
- <button class="ds-btn ds-btn--ghost ds-btn--sm ds-btn--icon" title="Theme" data-theme-toggle>
31
- <img class="theme-icon" src="{{ url_for('static', filename='nimbus/icons/sun.svg') }}" width="16" height="16" alt="theme" class="icon">
32
- </button>
33
- <button class="ds-btn ds-btn--ghost ds-btn--sm" data-brand-toggle title="Brand Color" style="display: flex; align-items: center; gap: 4px;">
34
- <span class="brand-icon" style="width: 10px; height: 10px; border-radius: 50%; background-color: #535164;"></span>
35
- <span class="brand-name" style="font-size: 10px;">曾青</span>
36
- </button>
37
- <div class="ds-avatar" title="User">
38
- <img src="{{ url_for('static', filename='nimbus/icons/user.svg') }}" width="14" height="14" alt="user" class="icon">
39
- </div>
40
- {% endblock %}
41
- </div>
42
- </header>
43
-
44
- <div class="ds-dashboard__main">
45
- <aside class="ds-dashboard__sidebar">
46
- {% block sidebar %}
47
- <nav class="ds-dashboard__sidebar-nav">
48
- <div class="ds-dashboard__sidebar-item is-active" data-title="Dashboard">
49
- <img src="{{ url_for('static', filename='nimbus/icons/layout-grid.svg') }}" width="18" height="18" alt="layout-grid" class="icon">
50
- </div>
51
- <div class="ds-dashboard__sidebar-item" data-title="Projects">
52
- <img src="{{ url_for('static', filename='nimbus/icons/layers.svg') }}" width="18" height="18" alt="layers" class="icon">
53
- </div>
54
- <div class="ds-dashboard__sidebar-item" data-title="Tasks">
55
- <img src="{{ url_for('static', filename='nimbus/icons/check-square.svg') }}" width="18" height="18" alt="check-square" class="icon">
56
- </div>
57
- <div class="ds-dashboard__sidebar-item" data-title="Files">
58
- <img src="{{ url_for('static', filename='nimbus/icons/folder-open.svg') }}" width="18" height="18" alt="folder-open" class="icon">
59
- </div>
60
- </nav>
61
- {% endblock %}
62
- </aside>
63
-
64
- <main class="ds-dashboard__content">
65
- {% block content %}{% endblock %}
66
- </main>
67
- </div>
68
-
69
- <footer class="ds-dashboard__footer">
70
- <div class="ds-dashboard__footer-left">
71
- {% block footer_left %}
72
- <div class="ds-dashboard__status-item">
73
- <span class="ds-dashboard__status-dot"></span>
74
- <span>Connected</span>
75
- </div>
76
- <div class="ds-dashboard__status-item">
77
- <span>v1.0.0</span>
78
- </div>
79
- {% endblock %}
80
- </div>
81
- <div class="ds-dashboard__footer-right">
82
- {% block footer_right %}
83
- <div class="ds-dashboard__status-item">
84
- <span>Last sync: 2 min ago</span>
85
- </div>
86
- {% endblock %}
87
- </div>
88
- </footer>
89
- </div>
90
-
91
- {% endblock %}
92
-
93
- {% block body_js %}
94
- {{ super() }}
95
- <script src="{{ url_for('static', filename='nimbus/dashboard/dashboard.js') }}"></script>
96
- <script>
97
- initTheme();
98
- </script>
99
- {% endblock %}
package/utils/Kupola.cs DELETED
@@ -1,77 +0,0 @@
1
- using System;
2
-
3
- namespace Kupola
4
- {
5
- public static class Components
6
- {
7
- public static string Button(string label, string variant = "brand", string size = "md")
8
- {
9
- return $"<button class=\"ds-btn ds-btn--{variant} ds-btn--{size}\">{label}</button>";
10
- }
11
-
12
- public static string Input(string label, string name, string placeholder, string validate = "")
13
- {
14
- string validateAttr = string.IsNullOrEmpty(validate) ? "" : $" data-validate=\"{validate}\"";
15
- return $@"<div class=""ds-form-item"">
16
- <label class=""ds-form-item__label"">{label}</label>
17
- <input type=""text"" class=""ds-input"" name=""{name}"" placeholder=""{placeholder}""{validateAttr}>
18
- </div>";
19
- }
20
-
21
- public static string Card(string title, string body, string footer = "")
22
- {
23
- string footerHtml = string.IsNullOrEmpty(footer) ? "" : $"<div class=\"ds-card__footer\">{footer}</div>";
24
- return $@"<div class=""ds-card"">
25
- <div class=""ds-card__header"">
26
- <h3 class=""ds-card__title"">{title}</h3>
27
- </div>
28
- <div class=""ds-card__body"">{body}</div>
29
- {footerHtml}
30
- </div>";
31
- }
32
-
33
- public static string StatCard(string title, string value, string trend = "", string trendType = "up")
34
- {
35
- string trendHtml = string.IsNullOrEmpty(trend) ? "" :
36
- $"<span class=\"ds-stat-card__trend {(trendType == "down" ? "ds-stat-card__trend--down" : "ds-stat-card__trend--up")}\">{trend}</span>";
37
- return $@"<div class=""ds-stat-card"">
38
- <div class=""ds-stat-card__title"">{title}</div>
39
- <div class=""ds-stat-card__value"">{value}</div>
40
- {trendHtml}
41
- </div>";
42
- }
43
-
44
- public static string Badge(string text, string variant = "default")
45
- {
46
- return $"<span class=\"ds-badge ds-badge--{variant}\">{text}</span>";
47
- }
48
-
49
- public static string Avatar(string initials, string size = "md", string variant = "default")
50
- {
51
- return $"<div class=\"ds-avatar ds-avatar--{size} ds-avatar--{variant}\">{initials}</div>";
52
- }
53
-
54
- public static string ThemeScript(string theme = "dark", string brand = "zengqing")
55
- {
56
- return $@"<script>
57
- if (typeof setTheme === 'function') setTheme('{theme}');
58
- if (typeof setBrand === 'function') setBrand('{brand}');
59
- </script>";
60
- }
61
-
62
- public static string DataBind(string key, string bindType)
63
- {
64
- return $"data-bind=\"{key}:{bindType}\"";
65
- }
66
-
67
- public static string DataComponent(string name)
68
- {
69
- return $"data-component=\"{name}\"";
70
- }
71
-
72
- public static string DataValidate(string rules)
73
- {
74
- return $"data-validate=\"{rules}\"";
75
- }
76
- }
77
- }