@kupola/kupola 1.2.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.
- package/CHANGELOG.md +130 -0
- package/INTEGRATION.md +440 -0
- package/PROJECT_SUMMARY.md +312 -0
- package/README.md +286 -0
- package/SKILL.md +572 -0
- package/css/accessibility.css +119 -0
- package/css/animations.css +224 -0
- package/css/brand-themes.css +300 -0
- package/css/colors_and_type.css +441 -0
- package/css/components-ext.css +4165 -0
- package/css/components.css +1483 -0
- package/css/kupola.css +12 -0
- package/css/responsive.css +697 -0
- package/css/scaffold.css +145 -0
- package/css/states.css +316 -0
- package/css/theme-dark.css +296 -0
- package/css/theme-light.css +296 -0
- package/css/utilities.css +171 -0
- package/dist/css/kupola.css +12 -0
- package/dist/icons/icons/activity.svg +3 -0
- package/dist/icons/icons/alert-circle.svg +1 -0
- package/dist/icons/icons/alert-triangle.svg +1 -0
- package/dist/icons/icons/alert.d27e24.svg +1 -0
- package/dist/icons/icons/alert.svg +1 -0
- package/dist/icons/icons/apple.svg +1 -0
- package/dist/icons/icons/arrow-down.svg +1 -0
- package/dist/icons/icons/arrow-expand.svg +1 -0
- package/dist/icons/icons/arrow-left.svg +1 -0
- package/dist/icons/icons/arrow-minimize.svg +1 -0
- package/dist/icons/icons/arrow-right-to-line.svg +1 -0
- package/dist/icons/icons/arrow-right.svg +1 -0
- package/dist/icons/icons/arrow-up-right.svg +1 -0
- package/dist/icons/icons/arrow-up.0c0c0d.svg +1 -0
- package/dist/icons/icons/arrow-up.svg +1 -0
- package/dist/icons/icons/at.svg +1 -0
- package/dist/icons/icons/atom.0c0c0d.svg +1 -0
- package/dist/icons/icons/atom.svg +1 -0
- package/dist/icons/icons/bar-chart-2.svg +1 -0
- package/dist/icons/icons/bar.svg +1 -0
- package/dist/icons/icons/bell.svg +1 -0
- package/dist/icons/icons/box.svg +1 -0
- package/dist/icons/icons/bug.svg +1 -0
- package/dist/icons/icons/button.svg +1 -0
- package/dist/icons/icons/calendar.svg +1 -0
- package/dist/icons/icons/check-circle.33c192.svg +1 -0
- package/dist/icons/icons/check-circle.svg +1 -0
- package/dist/icons/icons/check-square.svg +4 -0
- package/dist/icons/icons/check.d1d3db.svg +1 -0
- package/dist/icons/icons/check.svg +1 -0
- package/dist/icons/icons/chevron-down.svg +1 -0
- package/dist/icons/icons/chevron-left.svg +1 -0
- package/dist/icons/icons/chevron-right.svg +1 -0
- package/dist/icons/icons/chevron-up.svg +1 -0
- package/dist/icons/icons/circle.svg +1 -0
- package/dist/icons/icons/clock.svg +1 -0
- package/dist/icons/icons/cmd.svg +1 -0
- package/dist/icons/icons/code.svg +1 -0
- package/dist/icons/icons/columns.svg +1 -0
- package/dist/icons/icons/copy.svg +1 -0
- package/dist/icons/icons/cpu.svg +1 -0
- package/dist/icons/icons/dashboard.0c0c0d.svg +1 -0
- package/dist/icons/icons/dashboard.svg +1 -0
- package/dist/icons/icons/dollar.svg +1 -0
- package/dist/icons/icons/download.svg +1 -0
- package/dist/icons/icons/edit.svg +1 -0
- package/dist/icons/icons/extensions.svg +1 -0
- package/dist/icons/icons/external.svg +1 -0
- package/dist/icons/icons/eye.svg +1 -0
- package/dist/icons/icons/file-text.svg +1 -0
- package/dist/icons/icons/file.svg +1 -0
- package/dist/icons/icons/files.svg +1 -0
- package/dist/icons/icons/filter.svg +1 -0
- package/dist/icons/icons/folder-open.svg +4 -0
- package/dist/icons/icons/folder.svg +1 -0
- package/dist/icons/icons/gear.svg +1 -0
- package/dist/icons/icons/git.svg +1 -0
- package/dist/icons/icons/github.svg +1 -0
- package/dist/icons/icons/globe.svg +1 -0
- package/dist/icons/icons/grid-2x2.svg +1 -0
- package/dist/icons/icons/hash.svg +1 -0
- package/dist/icons/icons/heart.svg +1 -0
- package/dist/icons/icons/help-circle.svg +5 -0
- package/dist/icons/icons/help.svg +1 -0
- package/dist/icons/icons/home.svg +1 -0
- package/dist/icons/icons/icons.svg +153 -0
- package/dist/icons/icons/image.svg +1 -0
- package/dist/icons/icons/info-circle.svg +1 -0
- package/dist/icons/icons/info-square.svg +1 -0
- package/dist/icons/icons/info.387bff.svg +1 -0
- package/dist/icons/icons/info.svg +1 -0
- package/dist/icons/icons/input.svg +1 -0
- package/dist/icons/icons/key.svg +1 -0
- package/dist/icons/icons/layers.svg +1 -0
- package/dist/icons/icons/layout-grid.svg +6 -0
- package/dist/icons/icons/layout.svg +1 -0
- package/dist/icons/icons/link.svg +1 -0
- package/dist/icons/icons/list.svg +1 -0
- package/dist/icons/icons/lock.svg +1 -0
- package/dist/icons/icons/log-out.svg +1 -0
- package/dist/icons/icons/logo.svg +1 -0
- package/dist/icons/icons/mail.svg +1 -0
- package/dist/icons/icons/menu.svg +1 -0
- package/dist/icons/icons/message-circle.svg +1 -0
- package/dist/icons/icons/message-plus.svg +1 -0
- package/dist/icons/icons/message-square.svg +1 -0
- package/dist/icons/icons/mic.svg +1 -0
- package/dist/icons/icons/minus.svg +1 -0
- package/dist/icons/icons/moon.svg +1 -0
- package/dist/icons/icons/more-h.svg +1 -0
- package/dist/icons/icons/mouse.svg +1 -0
- package/dist/icons/icons/palette.svg +1 -0
- package/dist/icons/icons/panel-bottom.svg +1 -0
- package/dist/icons/icons/pause.svg +1 -0
- package/dist/icons/icons/play.svg +1 -0
- package/dist/icons/icons/plug.svg +1 -0
- package/dist/icons/icons/plus.0c0c0d.svg +1 -0
- package/dist/icons/icons/plus.svg +1 -0
- package/dist/icons/icons/refresh.svg +1 -0
- package/dist/icons/icons/ruler.svg +1 -0
- package/dist/icons/icons/scroll-text.svg +1 -0
- package/dist/icons/icons/search-menu.svg +1 -0
- package/dist/icons/icons/search.svg +1 -0
- package/dist/icons/icons/send.0c0c0d.svg +1 -0
- package/dist/icons/icons/send.svg +1 -0
- package/dist/icons/icons/settings.0c0c0d.svg +1 -0
- package/dist/icons/icons/settings.svg +1 -0
- package/dist/icons/icons/shield.svg +1 -0
- package/dist/icons/icons/sidebar-left.svg +1 -0
- package/dist/icons/icons/sidebar-right.svg +1 -0
- package/dist/icons/icons/sliders.svg +1 -0
- package/dist/icons/icons/sparkles.32f08c.svg +1 -0
- package/dist/icons/icons/sparkles.svg +1 -0
- package/dist/icons/icons/square.svg +1 -0
- package/dist/icons/icons/star.svg +1 -0
- package/dist/icons/icons/sun.svg +1 -0
- package/dist/icons/icons/table.svg +1 -0
- package/dist/icons/icons/terminal.svg +1 -0
- package/dist/icons/icons/trash.svg +1 -0
- package/dist/icons/icons/trending-down.svg +1 -0
- package/dist/icons/icons/trending-up.svg +1 -0
- package/dist/icons/icons/type.svg +1 -0
- package/dist/icons/icons/upload.svg +1 -0
- package/dist/icons/icons/user-circle.svg +1 -0
- package/dist/icons/icons/user-plus.svg +6 -0
- package/dist/icons/icons/user.svg +1 -0
- package/dist/icons/icons/users.svg +1 -0
- package/dist/icons/icons/warning.svg +1 -0
- package/dist/icons/icons/wrench.svg +1 -0
- package/dist/icons/icons/x-circle.svg +1 -0
- package/dist/icons/icons/x.svg +1 -0
- package/dist/icons/icons/zap.0c0c0d.svg +1 -0
- package/dist/icons/icons/zap.svg +1 -0
- package/dist/kupola.cjs.js +258 -0
- package/dist/kupola.cjs.js.map +1 -0
- package/dist/kupola.esm.js +7640 -0
- package/dist/kupola.esm.js.map +1 -0
- package/dist/kupola.umd.js +258 -0
- package/dist/kupola.umd.js.map +1 -0
- package/dist/types/kupola.d.ts +1359 -0
- package/dist/utils/utils/Kupola.cs +77 -0
- package/dist/utils/utils/Kupola.java +104 -0
- package/dist/utils/utils/kupola.go +120 -0
- package/dist/utils/utils/kupola.js +63 -0
- package/dist/utils/utils/kupola.py +1392 -0
- package/dist/utils/utils/kupola.rb +69 -0
- package/icons/activity.svg +3 -0
- package/icons/alert-circle.svg +1 -0
- package/icons/alert-triangle.svg +1 -0
- package/icons/alert.d27e24.svg +1 -0
- package/icons/alert.svg +1 -0
- package/icons/apple.svg +1 -0
- package/icons/arrow-down.svg +1 -0
- package/icons/arrow-expand.svg +1 -0
- package/icons/arrow-left.svg +1 -0
- package/icons/arrow-minimize.svg +1 -0
- package/icons/arrow-right-to-line.svg +1 -0
- package/icons/arrow-right.svg +1 -0
- package/icons/arrow-up-right.svg +1 -0
- package/icons/arrow-up.0c0c0d.svg +1 -0
- package/icons/arrow-up.svg +1 -0
- package/icons/at.svg +1 -0
- package/icons/atom.0c0c0d.svg +1 -0
- package/icons/atom.svg +1 -0
- package/icons/bar-chart-2.svg +1 -0
- package/icons/bar.svg +1 -0
- package/icons/bell.svg +1 -0
- package/icons/box.svg +1 -0
- package/icons/bug.svg +1 -0
- package/icons/button.svg +1 -0
- package/icons/calendar.svg +1 -0
- package/icons/check-circle.33c192.svg +1 -0
- package/icons/check-circle.svg +1 -0
- package/icons/check-square.svg +4 -0
- package/icons/check.d1d3db.svg +1 -0
- package/icons/check.svg +1 -0
- package/icons/chevron-down.svg +1 -0
- package/icons/chevron-left.svg +1 -0
- package/icons/chevron-right.svg +1 -0
- package/icons/chevron-up.svg +1 -0
- package/icons/circle.svg +1 -0
- package/icons/clock.svg +1 -0
- package/icons/cmd.svg +1 -0
- package/icons/code.svg +1 -0
- package/icons/columns.svg +1 -0
- package/icons/copy.svg +1 -0
- package/icons/cpu.svg +1 -0
- package/icons/dashboard.0c0c0d.svg +1 -0
- package/icons/dashboard.svg +1 -0
- package/icons/dollar.svg +1 -0
- package/icons/download.svg +1 -0
- package/icons/edit.svg +1 -0
- package/icons/extensions.svg +1 -0
- package/icons/external.svg +1 -0
- package/icons/eye.svg +1 -0
- package/icons/file-text.svg +1 -0
- package/icons/file.svg +1 -0
- package/icons/files.svg +1 -0
- package/icons/filter.svg +1 -0
- package/icons/folder-open.svg +4 -0
- package/icons/folder.svg +1 -0
- package/icons/gear.svg +1 -0
- package/icons/git.svg +1 -0
- package/icons/github.svg +1 -0
- package/icons/globe.svg +1 -0
- package/icons/grid-2x2.svg +1 -0
- package/icons/hash.svg +1 -0
- package/icons/heart.svg +1 -0
- package/icons/help-circle.svg +5 -0
- package/icons/help.svg +1 -0
- package/icons/home.svg +1 -0
- package/icons/icons.svg +153 -0
- package/icons/image.svg +1 -0
- package/icons/info-circle.svg +1 -0
- package/icons/info-square.svg +1 -0
- package/icons/info.387bff.svg +1 -0
- package/icons/info.svg +1 -0
- package/icons/input.svg +1 -0
- package/icons/key.svg +1 -0
- package/icons/layers.svg +1 -0
- package/icons/layout-grid.svg +6 -0
- package/icons/layout.svg +1 -0
- package/icons/link.svg +1 -0
- package/icons/list.svg +1 -0
- package/icons/lock.svg +1 -0
- package/icons/log-out.svg +1 -0
- package/icons/logo.svg +1 -0
- package/icons/mail.svg +1 -0
- package/icons/menu.svg +1 -0
- package/icons/message-circle.svg +1 -0
- package/icons/message-plus.svg +1 -0
- package/icons/message-square.svg +1 -0
- package/icons/mic.svg +1 -0
- package/icons/minus.svg +1 -0
- package/icons/moon.svg +1 -0
- package/icons/more-h.svg +1 -0
- package/icons/mouse.svg +1 -0
- package/icons/palette.svg +1 -0
- package/icons/panel-bottom.svg +1 -0
- package/icons/pause.svg +1 -0
- package/icons/play.svg +1 -0
- package/icons/plug.svg +1 -0
- package/icons/plus.0c0c0d.svg +1 -0
- package/icons/plus.svg +1 -0
- package/icons/refresh.svg +1 -0
- package/icons/ruler.svg +1 -0
- package/icons/scroll-text.svg +1 -0
- package/icons/search-menu.svg +1 -0
- package/icons/search.svg +1 -0
- package/icons/send.0c0c0d.svg +1 -0
- package/icons/send.svg +1 -0
- package/icons/settings.0c0c0d.svg +1 -0
- package/icons/settings.svg +1 -0
- package/icons/shield.svg +1 -0
- package/icons/sidebar-left.svg +1 -0
- package/icons/sidebar-right.svg +1 -0
- package/icons/sliders.svg +1 -0
- package/icons/sparkles.32f08c.svg +1 -0
- package/icons/sparkles.svg +1 -0
- package/icons/square.svg +1 -0
- package/icons/star.svg +1 -0
- package/icons/sun.svg +1 -0
- package/icons/table.svg +1 -0
- package/icons/terminal.svg +1 -0
- package/icons/trash.svg +1 -0
- package/icons/trending-down.svg +1 -0
- package/icons/trending-up.svg +1 -0
- package/icons/type.svg +1 -0
- package/icons/upload.svg +1 -0
- package/icons/user-circle.svg +1 -0
- package/icons/user-plus.svg +6 -0
- package/icons/user.svg +1 -0
- package/icons/users.svg +1 -0
- package/icons/warning.svg +1 -0
- package/icons/wrench.svg +1 -0
- package/icons/x-circle.svg +1 -0
- package/icons/x.svg +1 -0
- package/icons/zap.0c0c0d.svg +1 -0
- package/icons/zap.svg +1 -0
- package/js/calendar.js +184 -0
- package/js/carousel.js +173 -0
- package/js/collapse.js +127 -0
- package/js/color-picker.js +144 -0
- package/js/component.js +276 -0
- package/js/composition-api.js +458 -0
- package/js/countdown.js +144 -0
- package/js/data-bind.js +1286 -0
- package/js/datepicker.js +270 -0
- package/js/dialog.js +131 -0
- package/js/drawer.js +118 -0
- package/js/dropdown.js +122 -0
- package/js/dynamic-tags.js +156 -0
- package/js/error-handler.js +181 -0
- package/js/fileupload.js +392 -0
- package/js/form.js +291 -0
- package/js/global-events.js +203 -0
- package/js/heatmap.js +405 -0
- package/js/http.js +419 -0
- package/js/i18n.js +228 -0
- package/js/icons.js +165 -0
- package/js/image-preview.js +218 -0
- package/js/initializer.js +162 -0
- package/js/kupola-core.js +66 -0
- package/js/kupola-devtools.js +598 -0
- package/js/kupola-lifecycle.js +510 -0
- package/js/message.js +60 -0
- package/js/modal.js +191 -0
- package/js/notification.js +73 -0
- package/js/numberinput.js +146 -0
- package/js/performance.js +250 -0
- package/js/registry.js +205 -0
- package/js/router.js +396 -0
- package/js/security.js +189 -0
- package/js/select.js +143 -0
- package/js/slide-captcha.js +504 -0
- package/js/slider.js +94 -0
- package/js/statcard.js +219 -0
- package/js/tag.js +83 -0
- package/js/test-utils.js +251 -0
- package/js/theme.js +363 -0
- package/js/timepicker.js +238 -0
- package/js/tooltip.js +122 -0
- package/js/utils.js +1458 -0
- package/js/validation.js +553 -0
- package/js/virtual-list.js +593 -0
- package/package.json +105 -0
- package/templates/base.html +30 -0
- package/templates/base_dashboard.html +99 -0
- package/types/kupola.d.ts +1359 -0
- package/utils/Kupola.cs +77 -0
- package/utils/Kupola.java +104 -0
- package/utils/kupola.go +120 -0
- package/utils/kupola.js +63 -0
- package/utils/kupola.py +1392 -0
- package/utils/kupola.rb +69 -0
|
@@ -0,0 +1,1392 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
COMPONENTS_DIR = os.path.join(os.path.dirname(__file__), '..', 'components')
|
|
4
|
+
|
|
5
|
+
def render_component(name, **kwargs):
|
|
6
|
+
"""Render a Nimbus component HTML template.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
name: Component name (e.g., 'button', 'alert', 'card')
|
|
10
|
+
**kwargs: Context variables to substitute in the template
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
str: Rendered HTML string
|
|
14
|
+
"""
|
|
15
|
+
template_path = os.path.join(COMPONENTS_DIR, f'{name}.html')
|
|
16
|
+
if not os.path.exists(template_path):
|
|
17
|
+
return f'<span class="ds-alert ds-alert--danger">Component "{name}" not found</span>'
|
|
18
|
+
|
|
19
|
+
with open(template_path, 'r', encoding='utf-8') as f:
|
|
20
|
+
content = f.read()
|
|
21
|
+
|
|
22
|
+
for key, value in kwargs.items():
|
|
23
|
+
content = content.replace(f'{{{{ {key} }}}}', str(value))
|
|
24
|
+
content = content.replace(f'{{{{{key}}}}}', str(value))
|
|
25
|
+
|
|
26
|
+
return content
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def nimbus_css_link(path='/nimbus/css/nimbus.css'):
|
|
30
|
+
"""Generate CSS link tag for Nimbus."""
|
|
31
|
+
return f'<link rel="stylesheet" href="{path}" />'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def nimbus_icons_script(path='/nimbus/js/icons.js'):
|
|
35
|
+
"""Generate script tag for icons."""
|
|
36
|
+
return f'<script src="{path}"></script>'
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def nimbus_alert(title, description, type='info'):
|
|
40
|
+
"""Generate an alert component.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
title: Alert title
|
|
44
|
+
description: Alert description
|
|
45
|
+
type: 'info', 'success', 'warning', 'danger'
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
str: Alert HTML
|
|
49
|
+
"""
|
|
50
|
+
icon_map = {
|
|
51
|
+
'info': 'info',
|
|
52
|
+
'success': 'check-circle',
|
|
53
|
+
'warning': 'alert',
|
|
54
|
+
'danger': 'x-circle'
|
|
55
|
+
}
|
|
56
|
+
icon = icon_map.get(type, 'info')
|
|
57
|
+
|
|
58
|
+
return f'''<div class="ds-alert ds-alert--{type}">
|
|
59
|
+
<span class="ds-alert__icon"><img src="/nimbus/icons/{icon}.svg" width="16" height="16" alt="{icon}" class="icon"></span>
|
|
60
|
+
<div>
|
|
61
|
+
<div class="ds-alert__title">{title}</div>
|
|
62
|
+
<div class="ds-alert__desc">{description}</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>'''
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def nimbus_card(title, description, tags=None, actions=None):
|
|
68
|
+
"""Generate a card component.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
title: Card title
|
|
72
|
+
description: Card description
|
|
73
|
+
tags: List of tags (e.g., ['New', 'Feature'])
|
|
74
|
+
actions: List of action tuples (label, variant)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
str: Card HTML
|
|
78
|
+
"""
|
|
79
|
+
tags_html = ''
|
|
80
|
+
if tags:
|
|
81
|
+
tags_html = '<div style="display: flex; align-items: center; gap: var(--spacer-8); margin-bottom: var(--spacer-12);">'
|
|
82
|
+
for tag in tags:
|
|
83
|
+
tags_html += f'<span class="ds-tag">{tag}</span>'
|
|
84
|
+
tags_html += '</div>'
|
|
85
|
+
|
|
86
|
+
actions_html = ''
|
|
87
|
+
if actions:
|
|
88
|
+
actions_html = '<div style="display: flex; gap: var(--spacer-8); margin-top: var(--spacer-16);">'
|
|
89
|
+
for label, variant in actions:
|
|
90
|
+
actions_html += f'<button class="ds-btn ds-btn--{variant} ds-btn--sm">{label}</button>'
|
|
91
|
+
actions_html += '</div>'
|
|
92
|
+
|
|
93
|
+
return f'''<div class="ds-card">
|
|
94
|
+
{tags_html}
|
|
95
|
+
<div class="ds-card__title">{title}</div>
|
|
96
|
+
<p class="ds-card__desc">{description}</p>
|
|
97
|
+
{actions_html}
|
|
98
|
+
</div>'''
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def nimbus_tag(label, type='default'):
|
|
102
|
+
"""Generate a tag component.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
label: Tag text
|
|
106
|
+
type: 'default', 'success', 'warning', 'danger', 'primary'
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
str: Tag HTML
|
|
110
|
+
"""
|
|
111
|
+
type_class = f' ds-tag--{type}' if type != 'default' else ''
|
|
112
|
+
return f'<span class="ds-tag{type_class}">{label}</span>'
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def nimbus_button(label, variant='primary', size='md', icon=None, loading=False, disabled=False):
|
|
116
|
+
"""Generate a button component.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
label: Button text
|
|
120
|
+
variant: 'brand', 'primary', 'secondary', 'tertiary', 'danger-strong', 'danger-subtle', 'warning', 'link'
|
|
121
|
+
size: 'sm', 'md', 'lg'
|
|
122
|
+
icon: Icon name (e.g., 'check', 'download')
|
|
123
|
+
loading: Whether to show loading state
|
|
124
|
+
disabled: Whether button is disabled
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
str: Button HTML
|
|
128
|
+
"""
|
|
129
|
+
icon_html = ''
|
|
130
|
+
if icon:
|
|
131
|
+
icon_html = f'<img src="/nimbus/icons/{icon}.svg" width="14" height="14" alt="{icon}" class="icon">'
|
|
132
|
+
|
|
133
|
+
loading_class = ' ds-btn--loading' if loading else ''
|
|
134
|
+
disabled_attr = ' disabled' if disabled else ''
|
|
135
|
+
|
|
136
|
+
return f'''<button class="ds-btn ds-btn--{variant} ds-btn--{size}{loading_class}"{disabled_attr}>
|
|
137
|
+
{icon_html}
|
|
138
|
+
<span>{label}</span>
|
|
139
|
+
</button>'''
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def nimbus_input(placeholder='', type='text', icon=None, value='', error=False, disabled=False):
|
|
143
|
+
"""Generate an input component.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
placeholder: Input placeholder
|
|
147
|
+
type: Input type
|
|
148
|
+
icon: Icon name
|
|
149
|
+
value: Input value
|
|
150
|
+
error: Whether to show error state
|
|
151
|
+
disabled: Whether input is disabled
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
str: Input HTML
|
|
155
|
+
"""
|
|
156
|
+
icon_html = ''
|
|
157
|
+
if icon:
|
|
158
|
+
icon_html = f'<span class="ds-input__icon"><img src="/nimbus/icons/{icon}.svg" width="14" height="14" alt="{icon}" class="icon"></span>'
|
|
159
|
+
|
|
160
|
+
error_class = ' ds-input--error' if error else ''
|
|
161
|
+
disabled_attr = ' disabled' if disabled else ''
|
|
162
|
+
|
|
163
|
+
return f'''<div class="ds-input{error_class}">
|
|
164
|
+
{icon_html}
|
|
165
|
+
<input type="{type}" placeholder="{placeholder}" value="{value}"{disabled_attr} />
|
|
166
|
+
</div>'''
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def nimbus_dialog(title, body, confirm_text='Confirm', cancel_text='Cancel', variant='primary'):
|
|
170
|
+
"""Generate a dialog component.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
title: Dialog title
|
|
174
|
+
body: Dialog content
|
|
175
|
+
confirm_text: Confirm button text
|
|
176
|
+
cancel_text: Cancel button text
|
|
177
|
+
variant: Confirm button variant
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
str: Dialog HTML
|
|
181
|
+
"""
|
|
182
|
+
return f'''<div class="ds-backdrop" style="border-radius: var(--radius-4);">
|
|
183
|
+
<div class="ds-dialog">
|
|
184
|
+
<div class="ds-dialog__head">
|
|
185
|
+
<span class="ds-dialog__title">{title}</span>
|
|
186
|
+
<button class="ds-dialog__close" aria-label="Close">
|
|
187
|
+
<img src="/nimbus/icons/x.svg" width="14" height="14" alt="x" class="icon">
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="ds-dialog__body">
|
|
191
|
+
{body}
|
|
192
|
+
</div>
|
|
193
|
+
<div class="ds-dialog__foot">
|
|
194
|
+
<button class="ds-btn ds-btn--ghost">{cancel_text}</button>
|
|
195
|
+
<button class="ds-btn ds-btn--{variant}">{confirm_text}</button>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>'''
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def nimbus_pagination(current_page, total_pages, page_size=None, total_items=None):
|
|
202
|
+
"""Generate a pagination component.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
current_page: Current page number
|
|
206
|
+
total_pages: Total number of pages
|
|
207
|
+
page_size: Items per page (optional)
|
|
208
|
+
total_items: Total number of items (optional)
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
str: Pagination HTML
|
|
212
|
+
"""
|
|
213
|
+
pages = []
|
|
214
|
+
|
|
215
|
+
if current_page > 1:
|
|
216
|
+
pages.append(f'<button class="ds-pagination__item" aria-label="Previous"><img src="/nimbus/icons/chevron-right.svg" width="12" height="12" alt="chevron-right" class="icon" style="transform: rotate(180deg);"></button>')
|
|
217
|
+
|
|
218
|
+
for i in range(1, min(total_pages + 1, 7)):
|
|
219
|
+
if i == 1 or i == total_pages or (i >= current_page - 1 and i <= current_page + 1):
|
|
220
|
+
active_class = ' is-active' if i == current_page else ''
|
|
221
|
+
pages.append(f'<button class="ds-pagination__item{active_class} num">{i}</button>')
|
|
222
|
+
elif i == 2 and current_page > 3:
|
|
223
|
+
pages.append('<button class="ds-pagination__item" disabled>…</button>')
|
|
224
|
+
elif i == total_pages - 1 and current_page < total_pages - 2:
|
|
225
|
+
pages.append('<button class="ds-pagination__item" disabled>…</button>')
|
|
226
|
+
|
|
227
|
+
if current_page < total_pages:
|
|
228
|
+
pages.append(f'<button class="ds-pagination__item" aria-label="Next"><img src="/nimbus/icons/chevron-right.svg" width="12" height="12" alt="chevron-right" class="icon"></button>')
|
|
229
|
+
|
|
230
|
+
info_html = ''
|
|
231
|
+
if page_size and total_items:
|
|
232
|
+
start = (current_page - 1) * page_size + 1
|
|
233
|
+
end = min(current_page * page_size, total_items)
|
|
234
|
+
info_html = f'<span class="text-tertiary num" style="font-size: var(--body-sm-font-size);">{start}–{end} of {total_items}</span>'
|
|
235
|
+
|
|
236
|
+
return f'''<div style="display: flex; align-items: center; gap: var(--spacer-16);">
|
|
237
|
+
<div class="ds-pagination">
|
|
238
|
+
{''.join(pages)}
|
|
239
|
+
</div>
|
|
240
|
+
{info_html}
|
|
241
|
+
</div>'''
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def nimbus_stat_card(label, value, delta=None, delta_type='up'):
|
|
245
|
+
"""Generate a stat card component.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
label: Stat label
|
|
249
|
+
value: Stat value
|
|
250
|
+
delta: Delta value (e.g., '+12.4%')
|
|
251
|
+
delta_type: 'up' or 'down'
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
str: Stat card HTML
|
|
255
|
+
"""
|
|
256
|
+
delta_html = ''
|
|
257
|
+
if delta:
|
|
258
|
+
delta_icon = 'trending-up' if delta_type == 'up' else 'trending-down'
|
|
259
|
+
delta_html = f'''<span class="ds-statcard__delta is-{delta_type}">
|
|
260
|
+
<img src="/nimbus/icons/{delta_icon}.svg" width="16" height="16" alt="{delta_icon}" class="icon" />
|
|
261
|
+
{delta}
|
|
262
|
+
</span>'''
|
|
263
|
+
|
|
264
|
+
return f'''<div class="ds-statcard">
|
|
265
|
+
<span class="ds-statcard__label">{label}</span>
|
|
266
|
+
<span class="ds-statcard__value">{value}</span>
|
|
267
|
+
{delta_html}
|
|
268
|
+
</div>'''
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def nimbus_nav_list(groups):
|
|
272
|
+
"""Generate a navigation list component.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
groups: List of groups, each group is a dict with 'title' and 'items'
|
|
276
|
+
items is a list of dicts with 'label', 'icon', 'badge' (optional), 'active' (optional)
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
str: Navigation list HTML
|
|
280
|
+
"""
|
|
281
|
+
groups_html = []
|
|
282
|
+
|
|
283
|
+
for group in groups:
|
|
284
|
+
items_html = []
|
|
285
|
+
for item in group.get('items', []):
|
|
286
|
+
active_class = ' is-active' if item.get('active') else ''
|
|
287
|
+
badge_html = f'<span class="ds-navlist__badge">{item.get("badge")}</span>' if item.get('badge') else ''
|
|
288
|
+
|
|
289
|
+
items_html.append(f'''<a class="ds-navlist__item{active_class}">
|
|
290
|
+
<img src="/nimbus/icons/{item.get("icon", "circle")}.svg" width="18" height="18" alt="{item.get("icon")}" class="icon" />
|
|
291
|
+
<span class="ds-navlist__label">{item.get("label")}</span>
|
|
292
|
+
{badge_html}
|
|
293
|
+
</a>''')
|
|
294
|
+
|
|
295
|
+
groups_html.append(f'''<div class="ds-navlist__group">
|
|
296
|
+
<div class="ds-navlist__group-title">{group.get("title")}</div>
|
|
297
|
+
{''.join(items_html)}
|
|
298
|
+
</div>''')
|
|
299
|
+
|
|
300
|
+
return f'<nav class="ds-navlist">{''.join(groups_html)}</nav>'
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def nimbus_file_tree(items, depth=0):
|
|
304
|
+
"""Generate a file tree component.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
items: List of items, each item is a dict with 'label', 'icon', 'type' ('file' or 'folder'),
|
|
308
|
+
'children' (optional for folders), 'active' (optional)
|
|
309
|
+
depth: Current depth level
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
str: File tree HTML
|
|
313
|
+
"""
|
|
314
|
+
rows_html = []
|
|
315
|
+
|
|
316
|
+
for item in items:
|
|
317
|
+
active_class = ' is-active' if item.get('active') else ''
|
|
318
|
+
is_folder = item.get('type') == 'folder'
|
|
319
|
+
is_expanded = item.get('expanded', True)
|
|
320
|
+
|
|
321
|
+
chevron_icon = 'chevron-down' if (is_folder and is_expanded) else 'chevron-right'
|
|
322
|
+
chevron_class = ' ds-filetree__chevron--leaf' if not is_folder else ''
|
|
323
|
+
|
|
324
|
+
icon_class = f' ds-filetree__icon--{item.get("icon")}' if item.get('icon') else ' ds-filetree__icon--folder'
|
|
325
|
+
icon_name = 'folder' if is_folder else 'file-text'
|
|
326
|
+
|
|
327
|
+
rows_html.append(f'''<div class="ds-filetree__row{active_class}" data-depth="{depth}">
|
|
328
|
+
<img src="/nimbus/icons/{chevron_icon}.svg" width="14" height="14" alt="{chevron_icon}" class="icon ds-filetree__chevron{chevron_class}" />
|
|
329
|
+
<img src="/nimbus/icons/{icon_name}.svg" width="16" height="16" alt="{icon_name}" class="icon{icon_class}" />
|
|
330
|
+
<span class="ds-filetree__label">{item.get("label")}</span>
|
|
331
|
+
</div>''')
|
|
332
|
+
|
|
333
|
+
if is_folder and is_expanded and item.get('children'):
|
|
334
|
+
rows_html.append(nimbus_file_tree(item.get('children'), depth + 1))
|
|
335
|
+
|
|
336
|
+
return ''.join(rows_html)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def nimbus_theme_script(path='/nimbus/js/theme.js'):
|
|
340
|
+
"""Generate script tag for theme switching."""
|
|
341
|
+
return f'<script src="{path}"></script>'
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def nimbus_theme_toggle():
|
|
345
|
+
"""Generate theme toggle button."""
|
|
346
|
+
return '''<button data-theme-toggle class="ds-btn ds-btn--ghost ds-btn--sm ds-btn--icon" style="position: fixed; top: 16px; right: 16px; z-index: 9999;">
|
|
347
|
+
<img class="theme-icon" src="/nimbus/icons/sun.svg" width="14" height="14" alt="Toggle theme">
|
|
348
|
+
</button>'''
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
BRAND_OPTIONS = [
|
|
352
|
+
{'id': 'green', 'name': '翠绿', 'color': '#32F08C'},
|
|
353
|
+
{'id': 'xionghuang', 'name': '雄黄', 'color': '#FF9900'},
|
|
354
|
+
{'id': 'jianghuang', 'name': '姜黄', 'color': '#E2C027'},
|
|
355
|
+
{'id': 'lanlv', 'name': '蓝绿', 'color': '#12A182'},
|
|
356
|
+
{'id': 'kongquelan', 'name': '孔雀蓝', 'color': '#0EB0C9'},
|
|
357
|
+
{'id': 'meiguizi', 'name': '玫瑰紫', 'color': '#BA2F7B'},
|
|
358
|
+
{'id': 'shihong', 'name': '柿红', 'color': '#F2481B'},
|
|
359
|
+
{'id': 'quhong', 'name': '紫云', 'color': '#B1A6CC'},
|
|
360
|
+
{'id': 'shanchahong', 'name': '山茶红', 'color': '#F05A46'},
|
|
361
|
+
{'id': 'zengqing', 'name': '曾青', 'color': '#535164'},
|
|
362
|
+
{'id': 'roulan', 'name': '柔蓝', 'color': '#106898'}
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def nimbus_brand_toggle(brand='green'):
|
|
367
|
+
"""Generate brand color toggle button.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
brand: Current brand color ID
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
str: Brand toggle button HTML
|
|
374
|
+
"""
|
|
375
|
+
brand_info = next((b for b in BRAND_OPTIONS if b['id'] == brand), BRAND_OPTIONS[0])
|
|
376
|
+
return f'''<button data-brand-toggle class="ds-btn ds-btn--ghost ds-btn--sm" style="position: fixed; top: 16px; right: 56px; z-index: 9999; display: flex; align-items: center; gap: 6px;">
|
|
377
|
+
<span class="brand-icon" style="width: 12px; height: 12px; border-radius: 50%; background-color: {brand_info['color']};"></span>
|
|
378
|
+
<span class="brand-name" style="font-size: 11px;">{brand_info['name']}</span>
|
|
379
|
+
</button>'''
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def nimbus_brand_picker():
|
|
383
|
+
"""Generate brand color picker dropdown.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
str: Brand picker HTML
|
|
387
|
+
"""
|
|
388
|
+
buttons_html = []
|
|
389
|
+
for brand in BRAND_OPTIONS:
|
|
390
|
+
buttons_html.append(f'''<button class="ds-menu__item" data-brand-btn="{brand['id']}" style="display: flex; align-items: center; gap: 8px;">
|
|
391
|
+
<span style="width: 12px; height: 12px; border-radius: 50%; background-color: {brand['color']};"></span>
|
|
392
|
+
<span>{brand['name']}</span>
|
|
393
|
+
</button>''')
|
|
394
|
+
|
|
395
|
+
return f'''<div class="ds-menu" id="brand-picker" style="position: fixed; top: 56px; right: 16px; z-index: 9998; display: none;">
|
|
396
|
+
{''.join(buttons_html)}
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
<script>
|
|
401
|
+
const brandToggle = document.querySelector('[data-brand-toggle]');
|
|
402
|
+
const brandPicker = document.getElementById('brand-picker');
|
|
403
|
+
|
|
404
|
+
brandToggle.addEventListener('click', (e) => {{
|
|
405
|
+
e.stopPropagation();
|
|
406
|
+
brandPicker.style.display = brandPicker.style.display === 'none' ? 'block' : 'none';
|
|
407
|
+
}});
|
|
408
|
+
|
|
409
|
+
document.addEventListener('click', () => {{
|
|
410
|
+
brandPicker.style.display = 'none';
|
|
411
|
+
}});
|
|
412
|
+
|
|
413
|
+
brandPicker.addEventListener('click', (e) => {{
|
|
414
|
+
e.stopPropagation();
|
|
415
|
+
brandPicker.style.display = 'none';
|
|
416
|
+
}});
|
|
417
|
+
</script>'''
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def nimbus_html_tag(theme='dark', brand='green'):
|
|
421
|
+
"""Generate HTML tag with theme and brand attributes.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
theme: 'dark' or 'light'
|
|
425
|
+
brand: Brand color ID
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
str: HTML tag
|
|
429
|
+
"""
|
|
430
|
+
return f'<html lang="zh-CN" data-theme="{theme}" data-brand="{brand}">'
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def nimbus_checkbox(label, checked=False, disabled=False, name=None, value=None):
|
|
434
|
+
"""Generate a checkbox component.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
label: Checkbox label text
|
|
438
|
+
checked: Whether checkbox is checked
|
|
439
|
+
disabled: Whether checkbox is disabled
|
|
440
|
+
name: Input name attribute
|
|
441
|
+
value: Input value attribute
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
str: Checkbox HTML
|
|
445
|
+
"""
|
|
446
|
+
checked_attr = ' checked' if checked else ''
|
|
447
|
+
disabled_attr = ' disabled' if disabled else ''
|
|
448
|
+
name_attr = f' name="{name}"' if name else ''
|
|
449
|
+
value_attr = f' value="{value}"' if value else ''
|
|
450
|
+
|
|
451
|
+
return f'''<label class="ds-checkbox">
|
|
452
|
+
<input type="checkbox"{checked_attr}{disabled_attr}{name_attr}{value_attr}>
|
|
453
|
+
<span class="ds-checkbox__box"></span>
|
|
454
|
+
<span class="ds-checkbox__label">{label}</span>
|
|
455
|
+
</label>'''
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def nimbus_radio(label, checked=False, disabled=False, name=None, value=None):
|
|
459
|
+
"""Generate a radio component.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
label: Radio label text
|
|
463
|
+
checked: Whether radio is checked
|
|
464
|
+
disabled: Whether radio is disabled
|
|
465
|
+
name: Input name attribute
|
|
466
|
+
value: Input value attribute
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
str: Radio HTML
|
|
470
|
+
"""
|
|
471
|
+
checked_attr = ' checked' if checked else ''
|
|
472
|
+
disabled_attr = ' disabled' if disabled else ''
|
|
473
|
+
name_attr = f' name="{name}"' if name else ''
|
|
474
|
+
value_attr = f' value="{value}"' if value else ''
|
|
475
|
+
|
|
476
|
+
return f'''<label class="ds-radio">
|
|
477
|
+
<input type="radio"{checked_attr}{disabled_attr}{name_attr}{value_attr}>
|
|
478
|
+
<span class="ds-radio__dot"></span>
|
|
479
|
+
<span class="ds-radio__label">{label}</span>
|
|
480
|
+
</label>'''
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def nimbus_switch(checked=False, disabled=False, name=None):
|
|
484
|
+
"""Generate a switch component.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
checked: Whether switch is checked
|
|
488
|
+
disabled: Whether switch is disabled
|
|
489
|
+
name: Input name attribute
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
str: Switch HTML
|
|
493
|
+
"""
|
|
494
|
+
checked_attr = ' checked' if checked else ''
|
|
495
|
+
disabled_attr = ' disabled' if disabled else ''
|
|
496
|
+
name_attr = f' name="{name}"' if name else ''
|
|
497
|
+
|
|
498
|
+
return f'''<label class="ds-switch">
|
|
499
|
+
<input type="checkbox"{checked_attr}{disabled_attr}{name_attr}>
|
|
500
|
+
<span class="ds-switch__thumb"></span>
|
|
501
|
+
</label>'''
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def nimbus_select(options, value=None, placeholder='Select option'):
|
|
505
|
+
"""Generate a select component.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
options: List of options (dict with 'value' and 'label')
|
|
509
|
+
value: Selected value
|
|
510
|
+
placeholder: Placeholder text
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
str: Select HTML
|
|
514
|
+
"""
|
|
515
|
+
selected_label = placeholder
|
|
516
|
+
items_html = []
|
|
517
|
+
|
|
518
|
+
for opt in options:
|
|
519
|
+
is_active = opt['value'] == value
|
|
520
|
+
if is_active:
|
|
521
|
+
selected_label = opt['label']
|
|
522
|
+
|
|
523
|
+
items_html.append(f'<button class="ds-select__item{" is-active" if is_active else ""}" data-value="{opt["value"]}">{opt["label"]}</button>')
|
|
524
|
+
|
|
525
|
+
return f'''<div class="ds-select">
|
|
526
|
+
<button class="ds-select__trigger" data-select-trigger>
|
|
527
|
+
<span>{selected_label}</span>
|
|
528
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter">
|
|
529
|
+
<polyline points="6 9 12 15 18 9"/>
|
|
530
|
+
</svg>
|
|
531
|
+
</button>
|
|
532
|
+
<div class="ds-select__menu">
|
|
533
|
+
{''.join(items_html)}
|
|
534
|
+
</div>
|
|
535
|
+
</div>'''
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def nimbus_badge(text=None, type='default', dot=False):
|
|
539
|
+
"""Generate a badge component.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
text: Badge text
|
|
543
|
+
type: 'default', 'brand', 'success', 'warning', 'error'
|
|
544
|
+
dot: Whether to show as a dot
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
str: Badge HTML
|
|
548
|
+
"""
|
|
549
|
+
type_class = f' ds-badge--{type}' if type != 'default' else ''
|
|
550
|
+
dot_class = ' ds-badge--dot' if dot else ''
|
|
551
|
+
|
|
552
|
+
return f'<span class="ds-badge{type_class}{dot_class}">{text or ""}</span>'
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def nimbus_tooltip(text, position='top'):
|
|
556
|
+
"""Generate a tooltip component.
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
text: Tooltip text
|
|
560
|
+
position: 'top', 'bottom', 'left', 'right'
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
str: Tooltip HTML attribute
|
|
564
|
+
"""
|
|
565
|
+
return f'data-tooltip="{text}" data-tooltip-position="{position}"'
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def nimbus_spin(size='md', text=None):
|
|
569
|
+
"""Generate a spin/loading component.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
size: 'sm', 'md', 'lg'
|
|
573
|
+
text: Loading text
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
str: Spin HTML
|
|
577
|
+
"""
|
|
578
|
+
size_class = f' ds-spin--{size}' if size != 'md' else ''
|
|
579
|
+
text_html = f'<span class="ds-spin__text">{text}</span>' if text else ''
|
|
580
|
+
|
|
581
|
+
return f'''<div class="ds-spin{size_class}">
|
|
582
|
+
<div class="ds-spin__loader"></div>
|
|
583
|
+
{text_html}
|
|
584
|
+
</div>'''
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def nimbus_progress(percent=0, status='default', size='md', indeterminate=False):
|
|
588
|
+
"""Generate a progress component.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
percent: Progress percentage (0-100)
|
|
592
|
+
status: 'default', 'error', 'warning'
|
|
593
|
+
size: 'sm', 'md'
|
|
594
|
+
indeterminate: Whether to show indeterminate animation
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
str: Progress HTML
|
|
598
|
+
"""
|
|
599
|
+
size_class = ' ds-progress--sm' if size == 'sm' else ''
|
|
600
|
+
indeterminate_class = ' ds-progress--indeterminate' if indeterminate else ''
|
|
601
|
+
status_class = f' is-{status}' if status != 'default' else ''
|
|
602
|
+
width_style = f' style="width: {percent}%;"' if not indeterminate else ''
|
|
603
|
+
|
|
604
|
+
return f'''<div class="ds-progress{size_class}{indeterminate_class}">
|
|
605
|
+
<div class="ds-progress__bar{status_class}"{width_style}></div>
|
|
606
|
+
</div>'''
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def nimbus_empty(title='No Data', description=None, icon='box'):
|
|
610
|
+
"""Generate an empty state component.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
title: Empty state title
|
|
614
|
+
description: Empty state description
|
|
615
|
+
icon: Icon name
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
str: Empty HTML
|
|
619
|
+
"""
|
|
620
|
+
description_html = f'<div class="ds-empty__description">{description}</div>' if description else ''
|
|
621
|
+
|
|
622
|
+
return f'''<div class="ds-empty">
|
|
623
|
+
<div class="ds-empty__icon">
|
|
624
|
+
<img src="/nimbus/icons/{icon}.svg" width="48" height="48" alt="{icon}" class="icon">
|
|
625
|
+
</div>
|
|
626
|
+
<div class="ds-empty__title">{title}</div>
|
|
627
|
+
{description_html}
|
|
628
|
+
</div>'''
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def nimbus_divider(text=None, vertical=False):
|
|
632
|
+
"""Generate a divider component.
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
text: Divider text
|
|
636
|
+
vertical: Whether to show vertical divider
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
str: Divider HTML
|
|
640
|
+
"""
|
|
641
|
+
vertical_class = ' ds-divider--vertical' if vertical else ''
|
|
642
|
+
text_html = f'<span class="ds-divider__text">{text}</span>' if text else ''
|
|
643
|
+
|
|
644
|
+
return f'<div class="ds-divider{vertical_class}">{text_html}</div>'
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def nimbus_tooltip_script(path='/nimbus/js/tooltip.js'):
|
|
648
|
+
"""Generate script tag for tooltip functionality."""
|
|
649
|
+
return f'<script src="{path}"></script>'
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def nimbus_select_script(path='/nimbus/js/select.js'):
|
|
653
|
+
"""Generate script tag for select functionality."""
|
|
654
|
+
return f'<script src="{path}"></script>'
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
# ===== Dashboard Utilities =====
|
|
658
|
+
|
|
659
|
+
def nimbus_dashboard_header(logo_text='Nimbus', center_items=None, right_items=None):
|
|
660
|
+
"""Generate dashboard header.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
logo_text: Logo text
|
|
664
|
+
center_items: List of center menu items (dict with 'text', 'variant')
|
|
665
|
+
right_items: List of right icon buttons (dict with 'icon', 'badge', 'title')
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
str: Header HTML
|
|
669
|
+
"""
|
|
670
|
+
center_html = ''
|
|
671
|
+
if center_items and isinstance(center_items, list):
|
|
672
|
+
center_html = ''.join([
|
|
673
|
+
f'<button class="ds-btn ds-btn--{item.get("variant", "ghost")} ds-btn--sm">{item.get("text")}</button>'
|
|
674
|
+
for item in center_items
|
|
675
|
+
])
|
|
676
|
+
|
|
677
|
+
right_html = ''
|
|
678
|
+
if right_items and isinstance(right_items, list):
|
|
679
|
+
right_html = ''.join([
|
|
680
|
+
f'''<button class="ds-btn ds-btn--ghost ds-btn--sm ds-btn--icon" title="{item.get("title", "")}">
|
|
681
|
+
<img src="/nimbus/icons/{item.get("icon")}.svg" width="16" height="16" alt="{item.get("icon")}" class="icon">
|
|
682
|
+
{f'<span class="ds-btn__badge">{item.get("badge")}</span>' if item.get("badge") else ''}
|
|
683
|
+
</button>'''
|
|
684
|
+
for item in right_items
|
|
685
|
+
])
|
|
686
|
+
|
|
687
|
+
return f'''<header class="ds-dashboard__header">
|
|
688
|
+
<div class="ds-dashboard__header-left">
|
|
689
|
+
<div class="ds-dashboard__logo">
|
|
690
|
+
<div class="ds-dashboard__logo-icon"></div>
|
|
691
|
+
<span>{logo_text}</span>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
<div class="ds-dashboard__header-center">
|
|
695
|
+
{center_html}
|
|
696
|
+
</div>
|
|
697
|
+
<div class="ds-dashboard__header-right">
|
|
698
|
+
{right_html}
|
|
699
|
+
<div class="ds-avatar" title="User">
|
|
700
|
+
<img src="/nimbus/icons/user.svg" width="14" height="14" alt="user" class="icon">
|
|
701
|
+
</div>
|
|
702
|
+
</div>
|
|
703
|
+
</header>'''
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def nimbus_dashboard_sidebar(items=None):
|
|
707
|
+
"""Generate dashboard sidebar.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
items: List of sidebar items (dict with 'title', 'icon', 'active', 'badge')
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
str: Sidebar HTML
|
|
714
|
+
"""
|
|
715
|
+
if not items or not isinstance(items, list):
|
|
716
|
+
items = [
|
|
717
|
+
{'title': 'Dashboard', 'icon': 'layout-grid', 'active': True},
|
|
718
|
+
{'title': 'Projects', 'icon': 'layers'},
|
|
719
|
+
{'title': 'Tasks', 'icon': 'check-square'},
|
|
720
|
+
{'title': 'Files', 'icon': 'folder-open'},
|
|
721
|
+
{'title': 'Calendar', 'icon': 'calendar'},
|
|
722
|
+
{'title': 'Messages', 'icon': 'mail', 'badge': '5'},
|
|
723
|
+
]
|
|
724
|
+
|
|
725
|
+
items_html = ''.join([
|
|
726
|
+
f'''<div class="ds-dashboard__sidebar-item{item.get("active") and " is-active" or ""}" data-title="{item.get("title")}">
|
|
727
|
+
<img src="/nimbus/icons/{item.get("icon")}.svg" width="18" height="18" alt="{item.get("title")}" class="icon">
|
|
728
|
+
{f'<span class="ds-navlist__badge" style="position: absolute; top: -4px; right: -4px; font-size: 10px; padding: 1px 4px;">{item.get("badge")}</span>' if item.get("badge") else ''}
|
|
729
|
+
</div>'''
|
|
730
|
+
for item in items
|
|
731
|
+
])
|
|
732
|
+
|
|
733
|
+
return f'''<aside class="ds-dashboard__sidebar">
|
|
734
|
+
<nav class="ds-dashboard__sidebar-nav">
|
|
735
|
+
{items_html}
|
|
736
|
+
</nav>
|
|
737
|
+
</aside>'''
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def nimbus_dashboard_footer(left_items=None, right_items=None):
|
|
741
|
+
"""Generate dashboard footer/status bar.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
left_items: List of left status items (dict with 'text', 'type', 'status')
|
|
745
|
+
right_items: List of right status items (dict with 'text')
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
str: Footer HTML
|
|
749
|
+
"""
|
|
750
|
+
if not left_items or not isinstance(left_items, list):
|
|
751
|
+
left_items = [
|
|
752
|
+
{'text': 'Connected', 'type': 'dot', 'status': 'success'},
|
|
753
|
+
{'text': 'v1.0.0'},
|
|
754
|
+
]
|
|
755
|
+
|
|
756
|
+
if not right_items or not isinstance(right_items, list):
|
|
757
|
+
right_items = [
|
|
758
|
+
{'text': 'Branch: main'},
|
|
759
|
+
{'text': 'Last sync: 2 min ago'},
|
|
760
|
+
]
|
|
761
|
+
|
|
762
|
+
left_html = ''.join([
|
|
763
|
+
f'''<div class="ds-dashboard__status-item">
|
|
764
|
+
{f'<span class="ds-dashboard__status-dot{item.get("status") and " is-" + item.get("status") or ""}"></span>' if item.get("type") == 'dot' else ''}
|
|
765
|
+
<span>{item.get("text")}</span>
|
|
766
|
+
</div>'''
|
|
767
|
+
for item in left_items
|
|
768
|
+
])
|
|
769
|
+
|
|
770
|
+
right_html = ''.join([
|
|
771
|
+
f'''<div class="ds-dashboard__status-item">
|
|
772
|
+
<span>{item.get("text")}</span>
|
|
773
|
+
</div>'''
|
|
774
|
+
for item in right_items
|
|
775
|
+
])
|
|
776
|
+
|
|
777
|
+
return f'''<footer class="ds-dashboard__footer">
|
|
778
|
+
<div class="ds-dashboard__footer-left">
|
|
779
|
+
{left_html}
|
|
780
|
+
</div>
|
|
781
|
+
<div class="ds-dashboard__footer-right">
|
|
782
|
+
{right_html}
|
|
783
|
+
</div>
|
|
784
|
+
</footer>'''
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def nimbus_dashboard_layout(logo_text='Nimbus', header_center=None, header_right=None, sidebar_items=None, footer_left=None, footer_right=None):
|
|
788
|
+
"""Generate complete dashboard layout.
|
|
789
|
+
|
|
790
|
+
Args:
|
|
791
|
+
logo_text: Logo text
|
|
792
|
+
header_center: Center menu items
|
|
793
|
+
header_right: Right icon buttons
|
|
794
|
+
sidebar_items: Sidebar navigation items
|
|
795
|
+
footer_left: Left status items
|
|
796
|
+
footer_right: Right status items
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
str: Complete dashboard layout HTML
|
|
800
|
+
"""
|
|
801
|
+
return f'''<div class="ds-dashboard">
|
|
802
|
+
{nimbus_dashboard_header(logo_text, header_center, header_right)}
|
|
803
|
+
<div class="ds-dashboard__main">
|
|
804
|
+
{nimbus_dashboard_sidebar(sidebar_items)}
|
|
805
|
+
<main class="ds-dashboard__content">
|
|
806
|
+
{{% block content %}}{{% endblock %}}
|
|
807
|
+
</main>
|
|
808
|
+
</div>
|
|
809
|
+
{nimbus_dashboard_footer(footer_left, footer_right)}
|
|
810
|
+
</div>'''
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def nimbus_dashboard_script(path='/nimbus/dashboard/dashboard.js'):
|
|
814
|
+
"""Generate dashboard script tag."""
|
|
815
|
+
return f'<script src="{path}"></script>'
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def nimbus_dashboard_css(path='/nimbus/dashboard/dashboard.css'):
|
|
819
|
+
"""Generate dashboard CSS link tag."""
|
|
820
|
+
return f'<link rel="stylesheet" href="{path}">'
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def nimbus_dashboard_stat_card(label, value, delta=None, delta_type='up'):
|
|
824
|
+
"""Generate stat card for dashboard.
|
|
825
|
+
|
|
826
|
+
Args:
|
|
827
|
+
label: Stat label
|
|
828
|
+
value: Stat value
|
|
829
|
+
delta: Delta value
|
|
830
|
+
delta_type: 'up' or 'down'
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
str: Stat card HTML
|
|
834
|
+
"""
|
|
835
|
+
delta_html = ''
|
|
836
|
+
if delta:
|
|
837
|
+
delta_icon = 'trending-up' if delta_type == 'up' else 'trending-down'
|
|
838
|
+
delta_html = f'''<span class="ds-statcard__delta is-{delta_type}">
|
|
839
|
+
<img src="/nimbus/icons/{delta_icon}.svg" width="16" height="16" alt="{delta_icon}" class="icon" />
|
|
840
|
+
{delta}
|
|
841
|
+
</span>'''
|
|
842
|
+
|
|
843
|
+
return f'''<div class="ds-statcard">
|
|
844
|
+
<span class="ds-statcard__label">{label}</span>
|
|
845
|
+
<span class="ds-statcard__value">{value}</span>
|
|
846
|
+
{delta_html}
|
|
847
|
+
</div>'''
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def nimbus_dashboard_progress_bar(label, percentage, color='brand'):
|
|
851
|
+
"""Generate progress bar for dashboard.
|
|
852
|
+
|
|
853
|
+
Args:
|
|
854
|
+
label: Progress label
|
|
855
|
+
percentage: Progress percentage (0-100)
|
|
856
|
+
color: Color variant
|
|
857
|
+
|
|
858
|
+
Returns:
|
|
859
|
+
str: Progress bar HTML
|
|
860
|
+
"""
|
|
861
|
+
return f'''<div>
|
|
862
|
+
<div class="flex justify-between mb-4">
|
|
863
|
+
<span class="text-sm">{label}</span>
|
|
864
|
+
<span class="text-sm text-tertiary">{percentage}%</span>
|
|
865
|
+
</div>
|
|
866
|
+
<div class="h-2 bg-overlay-l2 rounded-full overflow-hidden">
|
|
867
|
+
<div class="h-full bg-{color} rounded-full" style="width: {percentage}%;"></div>
|
|
868
|
+
</div>
|
|
869
|
+
</div>'''
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def nimbus_activity_rail(items=None):
|
|
873
|
+
"""Generate activity rail component.
|
|
874
|
+
|
|
875
|
+
Args:
|
|
876
|
+
items: List of rail items (dict with 'icon', 'active', 'label')
|
|
877
|
+
|
|
878
|
+
Returns:
|
|
879
|
+
str: Activity rail HTML
|
|
880
|
+
"""
|
|
881
|
+
if not items or not isinstance(items, list):
|
|
882
|
+
items = [
|
|
883
|
+
{'icon': 'layout', 'active': True, 'label': 'Explorer'},
|
|
884
|
+
{'icon': 'search', 'label': 'Search'},
|
|
885
|
+
{'icon': 'git', 'label': 'Source Control'},
|
|
886
|
+
{'icon': 'play', 'label': 'Run and Debug'},
|
|
887
|
+
{'icon': 'extensions', 'label': 'Extensions'},
|
|
888
|
+
]
|
|
889
|
+
|
|
890
|
+
items_html = ''.join([
|
|
891
|
+
f'''<button class="ds-activityrail__item{item.get("active") and " is-active" or ""}" aria-label="{item.get("label")}" title="{item.get("label")}">
|
|
892
|
+
<img src="/nimbus/icons/{item.get("icon")}.svg" width="16" height="16" alt="{item.get("label")}" class="icon">
|
|
893
|
+
</button>'''
|
|
894
|
+
for item in items
|
|
895
|
+
])
|
|
896
|
+
|
|
897
|
+
return f'''<div class="ds-activityrail">
|
|
898
|
+
<nav class="ds-activityrail__nav">
|
|
899
|
+
{items_html}
|
|
900
|
+
</nav>
|
|
901
|
+
<div class="ds-activityrail__divider"></div>
|
|
902
|
+
<div class="ds-activityrail__bottom">
|
|
903
|
+
<button class="ds-activityrail__item" aria-label="Settings" title="Settings">
|
|
904
|
+
<img src="/nimbus/icons/settings.svg" width="16" height="16" alt="settings" class="icon">
|
|
905
|
+
</button>
|
|
906
|
+
</div>
|
|
907
|
+
</div>'''
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def nimbus_chat_composer(model='GPT-4'):
|
|
911
|
+
"""Generate chat composer component.
|
|
912
|
+
|
|
913
|
+
Args:
|
|
914
|
+
model: Model name display
|
|
915
|
+
|
|
916
|
+
Returns:
|
|
917
|
+
str: Chat composer HTML
|
|
918
|
+
"""
|
|
919
|
+
return f'''<div class="ds-chatcomposer">
|
|
920
|
+
<div class="ds-chatcomposer__toolbar">
|
|
921
|
+
<button class="ds-btn ds-btn--ghost ds-btn--sm ds-btn--icon" aria-label="Insert code">
|
|
922
|
+
<img src="/nimbus/icons/code.svg" width="14" height="14" alt="code" class="icon">
|
|
923
|
+
</button>
|
|
924
|
+
<button class="ds-btn ds-btn--ghost ds-btn--sm ds-btn--icon" aria-label="Insert image">
|
|
925
|
+
<img src="/nimbus/icons/image.svg" width="14" height="14" alt="image" class="icon">
|
|
926
|
+
</button>
|
|
927
|
+
<button class="ds-btn ds-btn--ghost ds-btn--sm ds-btn--icon" aria-label="Insert link">
|
|
928
|
+
<img src="/nimbus/icons/link.svg" width="14" height="14" alt="link" class="icon">
|
|
929
|
+
</button>
|
|
930
|
+
<button class="ds-btn ds-btn--ghost ds-btn--sm ds-btn--icon" aria-label="Insert file">
|
|
931
|
+
<img src="/nimbus/icons/file.svg" width="14" height="14" alt="file" class="icon">
|
|
932
|
+
</button>
|
|
933
|
+
</div>
|
|
934
|
+
<div class="ds-chatcomposer__input-area">
|
|
935
|
+
<span class="ds-chatcomposer__model">{model}</span>
|
|
936
|
+
<input type="text" class="ds-chatcomposer__input" placeholder="Ask a question..." aria-label="Chat input">
|
|
937
|
+
<button class="ds-btn ds-btn--ghost ds-btn--sm ds-btn--icon" aria-label="Voice input">
|
|
938
|
+
<img src="/nimbus/icons/mic.svg" width="14" height="14" alt="mic" class="icon">
|
|
939
|
+
</button>
|
|
940
|
+
<button class="ds-btn ds-btn--brand ds-btn--sm ds-btn--icon" aria-label="Send message">
|
|
941
|
+
<img src="/nimbus/icons/send.svg" width="14" height="14" alt="send" class="icon">
|
|
942
|
+
</button>
|
|
943
|
+
</div>
|
|
944
|
+
</div>'''
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def nimbus_editor_tabs(tabs=None):
|
|
948
|
+
"""Generate editor tabs component.
|
|
949
|
+
|
|
950
|
+
Args:
|
|
951
|
+
tabs: List of tabs (dict with 'name', 'active')
|
|
952
|
+
|
|
953
|
+
Returns:
|
|
954
|
+
str: Editor tabs HTML
|
|
955
|
+
"""
|
|
956
|
+
if not tabs or not isinstance(tabs, list):
|
|
957
|
+
tabs = [
|
|
958
|
+
{'name': 'main.py', 'active': True},
|
|
959
|
+
{'name': 'app.py'},
|
|
960
|
+
{'name': 'config.yaml'},
|
|
961
|
+
]
|
|
962
|
+
|
|
963
|
+
tabs_html = ''.join([
|
|
964
|
+
f'''<div class="ds-editortabs__tab{item.get("active") and " is-active" or ""}">
|
|
965
|
+
<img src="/nimbus/icons/file-text.svg" width="12" height="12" alt="file" class="icon">
|
|
966
|
+
<span>{item.get("name")}</span>
|
|
967
|
+
<button class="ds-editortabs__close" aria-label="Close tab">
|
|
968
|
+
<img src="/nimbus/icons/x.svg" width="10" height="10" alt="close" class="icon">
|
|
969
|
+
</button>
|
|
970
|
+
</div>'''
|
|
971
|
+
for item in tabs
|
|
972
|
+
])
|
|
973
|
+
|
|
974
|
+
return f'''<div class="ds-editortabs">
|
|
975
|
+
{tabs_html}
|
|
976
|
+
<button class="ds-editortabs__new" aria-label="New tab">
|
|
977
|
+
<img src="/nimbus/icons/plus.svg" width="12" height="12" alt="new" class="icon">
|
|
978
|
+
</button>
|
|
979
|
+
</div>'''
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def nimbus_kbd(key):
|
|
983
|
+
"""Generate keyboard key component.
|
|
984
|
+
|
|
985
|
+
Args:
|
|
986
|
+
key: Key name
|
|
987
|
+
|
|
988
|
+
Returns:
|
|
989
|
+
str: KBD HTML
|
|
990
|
+
"""
|
|
991
|
+
return f'<span class="ds-kbd">{key}</span>'
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def nimbus_kbd_shortcut(keys):
|
|
995
|
+
"""Generate keyboard shortcut combination.
|
|
996
|
+
|
|
997
|
+
Args:
|
|
998
|
+
keys: List of key names
|
|
999
|
+
|
|
1000
|
+
Returns:
|
|
1001
|
+
str: Shortcut HTML
|
|
1002
|
+
"""
|
|
1003
|
+
return ' + '.join([f'<span class="ds-kbd">{key}</span>' for key in keys])
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def nimbus_page_header(title, subtitle=None, actions=None):
|
|
1007
|
+
"""Generate page header component.
|
|
1008
|
+
|
|
1009
|
+
Args:
|
|
1010
|
+
title: Page title
|
|
1011
|
+
subtitle: Page subtitle
|
|
1012
|
+
actions: List of action tuples (label, variant)
|
|
1013
|
+
|
|
1014
|
+
Returns:
|
|
1015
|
+
str: Page header HTML
|
|
1016
|
+
"""
|
|
1017
|
+
subtitle_html = f'<p class="ds-pageheader__subtitle">{subtitle}</p>' if subtitle else ''
|
|
1018
|
+
|
|
1019
|
+
actions_html = ''
|
|
1020
|
+
if actions:
|
|
1021
|
+
actions_html = '<div class="ds-pageheader__actions">' + ''.join([
|
|
1022
|
+
f'<button class="ds-btn ds-btn--{variant} ds-btn--sm">{label}</button>'
|
|
1023
|
+
for label, variant in actions
|
|
1024
|
+
]) + '</div>'
|
|
1025
|
+
|
|
1026
|
+
return f'''<div class="ds-pageheader">
|
|
1027
|
+
<div class="ds-pageheader__title">
|
|
1028
|
+
<h1>{title}</h1>
|
|
1029
|
+
{subtitle_html}
|
|
1030
|
+
</div>
|
|
1031
|
+
{actions_html}
|
|
1032
|
+
</div>'''
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def nimbus_setting_row(label, description, type='switch', value=True, options=None):
|
|
1036
|
+
"""Generate setting row component.
|
|
1037
|
+
|
|
1038
|
+
Args:
|
|
1039
|
+
label: Setting label
|
|
1040
|
+
description: Setting description
|
|
1041
|
+
type: 'switch', 'select', 'button', 'input'
|
|
1042
|
+
value: Current value
|
|
1043
|
+
options: List of options for select type
|
|
1044
|
+
|
|
1045
|
+
Returns:
|
|
1046
|
+
str: Setting row HTML
|
|
1047
|
+
"""
|
|
1048
|
+
control_html = ''
|
|
1049
|
+
|
|
1050
|
+
if type == 'switch':
|
|
1051
|
+
control_html = f'''<label class="ds-toggle" role="switch" aria-checked="{str(value).lower()}">
|
|
1052
|
+
<input type="checkbox"{' checked' if value else ''}>
|
|
1053
|
+
<span class="ds-toggle__track"></span>
|
|
1054
|
+
</label>'''
|
|
1055
|
+
elif type == 'select' and options:
|
|
1056
|
+
options_html = ''.join([f'<option>{opt}</option>' for opt in options])
|
|
1057
|
+
control_html = f'<select class="ds-input" aria-label="{label}">{options_html}</select>'
|
|
1058
|
+
elif type == 'button':
|
|
1059
|
+
control_html = '<button class="ds-btn ds-btn--secondary ds-btn--sm">Configure</button>'
|
|
1060
|
+
else:
|
|
1061
|
+
control_html = f'<input type="text" class="ds-input" value="{value}" aria-label="{label}">'
|
|
1062
|
+
|
|
1063
|
+
return f'''<div class="ds-settingrow">
|
|
1064
|
+
<div class="ds-settingrow__info">
|
|
1065
|
+
<span class="ds-settingrow__title">{label}</span>
|
|
1066
|
+
<span class="ds-settingrow__description">{description}</span>
|
|
1067
|
+
</div>
|
|
1068
|
+
{control_html}
|
|
1069
|
+
</div>'''
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
def nimbus_setting_group(title, rows=None):
|
|
1073
|
+
"""Generate setting group component.
|
|
1074
|
+
|
|
1075
|
+
Args:
|
|
1076
|
+
title: Group title
|
|
1077
|
+
rows: List of setting rows (dict with label, description, type, value)
|
|
1078
|
+
|
|
1079
|
+
Returns:
|
|
1080
|
+
str: Setting group HTML
|
|
1081
|
+
"""
|
|
1082
|
+
if not rows or not isinstance(rows, list):
|
|
1083
|
+
rows = [
|
|
1084
|
+
{'label': 'Theme', 'description': 'Choose your preferred color scheme', 'type': 'select', 'options': ['Dark', 'Light', 'System']},
|
|
1085
|
+
{'label': 'Notifications', 'description': 'Receive email notifications', 'type': 'switch', 'value': True},
|
|
1086
|
+
]
|
|
1087
|
+
|
|
1088
|
+
rows_html = ''.join([
|
|
1089
|
+
nimbus_setting_row(
|
|
1090
|
+
label=row.get('label'),
|
|
1091
|
+
description=row.get('description'),
|
|
1092
|
+
type=row.get('type', 'switch'),
|
|
1093
|
+
value=row.get('value', True),
|
|
1094
|
+
options=row.get('options')
|
|
1095
|
+
)
|
|
1096
|
+
for row in rows
|
|
1097
|
+
])
|
|
1098
|
+
|
|
1099
|
+
return f'''<div class="ds-settingrow__group">
|
|
1100
|
+
<div class="ds-settingrow__grouplabel">{title}</div>
|
|
1101
|
+
{rows_html}
|
|
1102
|
+
</div>'''
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
def nimbus_status_bar(left_items=None, right_items=None):
|
|
1106
|
+
"""Generate status bar component.
|
|
1107
|
+
|
|
1108
|
+
Args:
|
|
1109
|
+
left_items: List of left items (dict with 'icon', 'text', 'title')
|
|
1110
|
+
right_items: List of right items (dict with 'text', 'title')
|
|
1111
|
+
|
|
1112
|
+
Returns:
|
|
1113
|
+
str: Status bar HTML
|
|
1114
|
+
"""
|
|
1115
|
+
if not left_items or not isinstance(left_items, list):
|
|
1116
|
+
left_items = [
|
|
1117
|
+
{'icon': 'git', 'text': 'main', 'title': 'Branch main'},
|
|
1118
|
+
{'icon': 'check', 'text': 'Synced', 'title': 'Synchronized'},
|
|
1119
|
+
]
|
|
1120
|
+
|
|
1121
|
+
if not right_items or not isinstance(right_items, list):
|
|
1122
|
+
right_items = [
|
|
1123
|
+
{'text': 'Ln 42, Col 15', 'title': 'Line 42, Column 15'},
|
|
1124
|
+
{'text': 'UTF-8', 'title': 'UTF-8 encoding'},
|
|
1125
|
+
{'text': 'Python', 'title': 'Python language'},
|
|
1126
|
+
]
|
|
1127
|
+
|
|
1128
|
+
left_html = ''.join([
|
|
1129
|
+
f'''<button class="ds-statusbar__item" aria-label="{item.get("title")}">
|
|
1130
|
+
<img src="/nimbus/icons/{item.get("icon")}.svg" width="12" height="12" alt="{item.get("title")}" class="icon">
|
|
1131
|
+
<span>{item.get("text")}</span>
|
|
1132
|
+
</button>'''
|
|
1133
|
+
for item in left_items
|
|
1134
|
+
])
|
|
1135
|
+
|
|
1136
|
+
right_html = ''.join([
|
|
1137
|
+
f'''<button class="ds-statusbar__item" aria-label="{item.get("title")}">
|
|
1138
|
+
<span>{item.get("text")}</span>
|
|
1139
|
+
</button>'''
|
|
1140
|
+
for item in right_items
|
|
1141
|
+
])
|
|
1142
|
+
|
|
1143
|
+
return f'''<div class="ds-statusbar">
|
|
1144
|
+
<div class="ds-statusbar__left">
|
|
1145
|
+
{left_html}
|
|
1146
|
+
</div>
|
|
1147
|
+
<div class="ds-statusbar__right">
|
|
1148
|
+
{right_html}
|
|
1149
|
+
</div>
|
|
1150
|
+
</div>'''
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
def nimbus_table_panel(headers=None, rows=None, title=None, actions=None):
|
|
1154
|
+
"""Generate table panel component.
|
|
1155
|
+
|
|
1156
|
+
Args:
|
|
1157
|
+
headers: List of column headers
|
|
1158
|
+
rows: List of rows (each row is a list of cell values)
|
|
1159
|
+
title: Panel title
|
|
1160
|
+
actions: List of action tuples (label, variant)
|
|
1161
|
+
|
|
1162
|
+
Returns:
|
|
1163
|
+
str: Table panel HTML
|
|
1164
|
+
"""
|
|
1165
|
+
if not headers:
|
|
1166
|
+
headers = ['Name', 'Type', 'Size', 'Modified']
|
|
1167
|
+
|
|
1168
|
+
if not rows:
|
|
1169
|
+
rows = [
|
|
1170
|
+
['main.py', 'Python', '4.2 KB', '2 hours ago'],
|
|
1171
|
+
['app.py', 'Python', '2.1 KB', '1 day ago'],
|
|
1172
|
+
]
|
|
1173
|
+
|
|
1174
|
+
headers_html = ''.join([f'<th>{h}</th>' for h in headers])
|
|
1175
|
+
rows_html = ''.join([
|
|
1176
|
+
'<tr>' + ''.join([f'<td>{cell}</td>' for cell in row]) + '</tr>'
|
|
1177
|
+
for row in rows
|
|
1178
|
+
])
|
|
1179
|
+
|
|
1180
|
+
actions_html = ''
|
|
1181
|
+
if actions:
|
|
1182
|
+
actions_html = '<div class="ds-tablepanel__actions">' + ''.join([
|
|
1183
|
+
f'<button class="ds-btn ds-btn--{variant} ds-btn--sm">{label}</button>'
|
|
1184
|
+
for label, variant in actions
|
|
1185
|
+
]) + '</div>'
|
|
1186
|
+
|
|
1187
|
+
title_html = f'<div class="ds-card__title">{title}</div>' if title else ''
|
|
1188
|
+
|
|
1189
|
+
return f'''<div class="ds-tablepanel">
|
|
1190
|
+
<div class="ds-tablepanel__toolbar">
|
|
1191
|
+
<div class="ds-tablepanel__filters">
|
|
1192
|
+
<button class="ds-btn ds-btn--ghost ds-btn--sm">
|
|
1193
|
+
<img src="/nimbus/icons/search.svg" width="12" height="12" alt="search" class="icon">
|
|
1194
|
+
<span>Search</span>
|
|
1195
|
+
</button>
|
|
1196
|
+
<button class="ds-btn ds-btn--ghost ds-btn--sm">
|
|
1197
|
+
<img src="/nimbus/icons/filter.svg" width="12" height="12" alt="filter" class="icon">
|
|
1198
|
+
<span>Filter</span>
|
|
1199
|
+
</button>
|
|
1200
|
+
</div>
|
|
1201
|
+
{actions_html}
|
|
1202
|
+
</div>
|
|
1203
|
+
{title_html}
|
|
1204
|
+
<table class="ds-table ds-tablepanel__table">
|
|
1205
|
+
<thead>
|
|
1206
|
+
<tr>{headers_html}</tr>
|
|
1207
|
+
</thead>
|
|
1208
|
+
<tbody>{rows_html}</tbody>
|
|
1209
|
+
</table>
|
|
1210
|
+
<div class="ds-tablepanel__footer">
|
|
1211
|
+
<span>1-{len(rows)} of {len(rows)} items</span>
|
|
1212
|
+
{nimbus_pagination(1, 1)}
|
|
1213
|
+
</div>
|
|
1214
|
+
</div>'''
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def nimbus_workbench_titlebar(project_name='My Project', actions=None):
|
|
1218
|
+
"""Generate workbench titlebar component.
|
|
1219
|
+
|
|
1220
|
+
Args:
|
|
1221
|
+
project_name: Project name
|
|
1222
|
+
actions: List of action icons
|
|
1223
|
+
|
|
1224
|
+
Returns:
|
|
1225
|
+
str: Titlebar HTML
|
|
1226
|
+
"""
|
|
1227
|
+
if not actions or not isinstance(actions, list):
|
|
1228
|
+
actions = ['save', 'undo', 'redo', 'search', 'settings']
|
|
1229
|
+
|
|
1230
|
+
actions_html = ''.join([
|
|
1231
|
+
f'''<button class="ds-btn ds-btn--ghost ds-btn--sm ds-btn--icon" aria-label="{action.title()}">
|
|
1232
|
+
<img src="/nimbus/icons/{action}.svg" width="14" height="14" alt="{action}" class="icon">
|
|
1233
|
+
</button>'''
|
|
1234
|
+
for action in actions
|
|
1235
|
+
])
|
|
1236
|
+
|
|
1237
|
+
return f'''<div class="ds-titlebar">
|
|
1238
|
+
<div class="ds-titlebar__left">
|
|
1239
|
+
<div class="ds-titlebar__traffic">
|
|
1240
|
+
<button class="ds-titlebar__traffic-btn ds-titlebar__traffic-btn--close" aria-label="Close window"></button>
|
|
1241
|
+
<button class="ds-titlebar__traffic-btn ds-titlebar__traffic-btn--minimize" aria-label="Minimize window"></button>
|
|
1242
|
+
<button class="ds-titlebar__traffic-btn ds-titlebar__traffic-btn--maximize" aria-label="Maximize window"></button>
|
|
1243
|
+
</div>
|
|
1244
|
+
<button class="ds-titlebar__project" aria-label="Select project">
|
|
1245
|
+
<span>{project_name}</span>
|
|
1246
|
+
<img src="/nimbus/icons/chevron-down.svg" width="12" height="12" alt="chevron" class="icon">
|
|
1247
|
+
</button>
|
|
1248
|
+
</div>
|
|
1249
|
+
<div class="ds-titlebar__right">
|
|
1250
|
+
{actions_html}
|
|
1251
|
+
</div>
|
|
1252
|
+
</div>'''
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def nimbus_progress_circle(value, label=''):
|
|
1256
|
+
"""Generate progress circle component.
|
|
1257
|
+
|
|
1258
|
+
Args:
|
|
1259
|
+
value: Progress percentage (0-100)
|
|
1260
|
+
label: Label text
|
|
1261
|
+
|
|
1262
|
+
Returns:
|
|
1263
|
+
str: Progress circle HTML
|
|
1264
|
+
"""
|
|
1265
|
+
circumference = 2 * 3.14159 * 45
|
|
1266
|
+
offset = circumference - (value / 100) * circumference
|
|
1267
|
+
|
|
1268
|
+
return f'''<div class="ds-progress-circle" role="progressbar" aria-valuenow="{value}" aria-valuemin="0" aria-valuemax="100">
|
|
1269
|
+
<svg class="ds-progress-circle__svg" viewBox="0 0 100 100">
|
|
1270
|
+
<circle class="ds-progress-circle__track" cx="50" cy="50" r="45" fill="none" stroke-width="8" />
|
|
1271
|
+
<circle class="ds-progress-circle__bar" cx="50" cy="50" r="45" fill="none" stroke-width="8" stroke-linecap="round" style="stroke-dasharray: {circumference}; stroke-dashoffset: {offset};" />
|
|
1272
|
+
</svg>
|
|
1273
|
+
<span class="ds-progress-circle__value">{value}%</span>
|
|
1274
|
+
<span class="ds-progress-circle__label">{label}</span>
|
|
1275
|
+
</div>'''
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
def nimbus_slider(label, value=50, min=0, max=100):
|
|
1279
|
+
"""Generate slider component.
|
|
1280
|
+
|
|
1281
|
+
Args:
|
|
1282
|
+
label: Slider label
|
|
1283
|
+
value: Current value
|
|
1284
|
+
min: Minimum value
|
|
1285
|
+
max: Maximum value
|
|
1286
|
+
|
|
1287
|
+
Returns:
|
|
1288
|
+
str: Slider HTML
|
|
1289
|
+
"""
|
|
1290
|
+
percentage = ((value - min) / (max - min)) * 100
|
|
1291
|
+
|
|
1292
|
+
return f'''<div class="ds-slider" role="slider" aria-valuenow="{value}" aria-valuemin="{min}" aria-valuemax="{max}">
|
|
1293
|
+
<span class="ds-slider__label">{label}</span>
|
|
1294
|
+
<div class="ds-slider__track">
|
|
1295
|
+
<div class="ds-slider__fill" style="width: {percentage}%;"></div>
|
|
1296
|
+
<input type="range" class="ds-slider__input" min="{min}" max="{max}" value="{value}" />
|
|
1297
|
+
</div>
|
|
1298
|
+
<span class="ds-slider__value">{value}</span>
|
|
1299
|
+
</div>'''
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
def nimbus_date_picker():
|
|
1303
|
+
"""Generate date picker component.
|
|
1304
|
+
|
|
1305
|
+
Returns:
|
|
1306
|
+
str: Date picker HTML
|
|
1307
|
+
"""
|
|
1308
|
+
return '''<div class="ds-datepicker" role="combobox" aria-haspopup="dialog" aria-expanded="false">
|
|
1309
|
+
<div class="ds-datepicker__input-wrap">
|
|
1310
|
+
<input type="text" class="ds-datepicker__input" placeholder="Select date" aria-label="Select date" readonly />
|
|
1311
|
+
<button class="ds-datepicker__icon" aria-label="Open calendar">
|
|
1312
|
+
<img src="/nimbus/icons/calendar.svg" width="16" height="16" alt="calendar" class="icon" />
|
|
1313
|
+
</button>
|
|
1314
|
+
</div>
|
|
1315
|
+
<div class="ds-datepicker__calendar" hidden>
|
|
1316
|
+
<div class="ds-datepicker__header">
|
|
1317
|
+
<button class="ds-datepicker__nav ds-datepicker__nav--prev" aria-label="Previous month">
|
|
1318
|
+
<img src="/nimbus/icons/chevron-left.svg" width="14" height="14" alt="previous" class="icon" />
|
|
1319
|
+
</button>
|
|
1320
|
+
<span class="ds-datepicker__title"></span>
|
|
1321
|
+
<button class="ds-datepicker__nav ds-datepicker__nav--next" aria-label="Next month">
|
|
1322
|
+
<img src="/nimbus/icons/chevron-right.svg" width="14" height="14" alt="next" class="icon" />
|
|
1323
|
+
</button>
|
|
1324
|
+
</div>
|
|
1325
|
+
<div class="ds-datepicker__weekdays">
|
|
1326
|
+
<span>日</span><span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span>
|
|
1327
|
+
</div>
|
|
1328
|
+
<div class="ds-datepicker__days"></div>
|
|
1329
|
+
</div>
|
|
1330
|
+
</div>'''
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
def nimbus_file_upload():
|
|
1334
|
+
"""Generate file upload component.
|
|
1335
|
+
|
|
1336
|
+
Returns:
|
|
1337
|
+
str: File upload HTML
|
|
1338
|
+
"""
|
|
1339
|
+
return '''<div class="ds-fileupload" role="region" aria-label="File upload">
|
|
1340
|
+
<div class="ds-fileupload__dropzone">
|
|
1341
|
+
<div class="ds-fileupload__icon">
|
|
1342
|
+
<img src="/nimbus/icons/upload.svg" width="32" height="32" alt="upload" class="icon" />
|
|
1343
|
+
</div>
|
|
1344
|
+
<div class="ds-fileupload__text">
|
|
1345
|
+
<span class="ds-fileupload__title">Drag and drop files here</span>
|
|
1346
|
+
<span class="ds-fileupload__subtitle">or click to browse</span>
|
|
1347
|
+
</div>
|
|
1348
|
+
<input type="file" class="ds-fileupload__input" multiple />
|
|
1349
|
+
</div>
|
|
1350
|
+
<div class="ds-fileupload__list" hidden></div>
|
|
1351
|
+
</div>'''
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
def nimbus_timeline(items=None):
|
|
1355
|
+
"""Generate timeline component.
|
|
1356
|
+
|
|
1357
|
+
Args:
|
|
1358
|
+
items: List of timeline items (dict with time, title, description)
|
|
1359
|
+
|
|
1360
|
+
Returns:
|
|
1361
|
+
str: Timeline HTML
|
|
1362
|
+
"""
|
|
1363
|
+
if not items or not isinstance(items, list):
|
|
1364
|
+
items = [
|
|
1365
|
+
{'time': '2 hours ago', 'title': 'Task completed', 'description': 'Design Review was marked as complete'},
|
|
1366
|
+
{'time': '5 hours ago', 'title': 'Project created', 'description': 'New Website project was created'},
|
|
1367
|
+
{'time': '1 day ago', 'title': 'File updated', 'description': 'API Integration was updated'},
|
|
1368
|
+
]
|
|
1369
|
+
|
|
1370
|
+
items_html = ''.join([
|
|
1371
|
+
f'''<div class="ds-timeline__item" role="listitem">
|
|
1372
|
+
<div class="ds-timeline__marker"></div>
|
|
1373
|
+
<div class="ds-timeline__content">
|
|
1374
|
+
<span class="ds-timeline__time">{item.get("time")}</span>
|
|
1375
|
+
<span class="ds-timeline__title">{item.get("title")}</span>
|
|
1376
|
+
<span class="ds-timeline__description">{item.get("description")}</span>
|
|
1377
|
+
</div>
|
|
1378
|
+
</div>'''
|
|
1379
|
+
for item in items
|
|
1380
|
+
])
|
|
1381
|
+
|
|
1382
|
+
return f'<div class="ds-timeline" role="list">{items_html}</div>'
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
def nimbus_validation_script(path='/nimbus/js/validation.js'):
|
|
1386
|
+
"""Generate validation script tag."""
|
|
1387
|
+
return f'<script src="{path}"></script>'
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def nimbus_data_bind_script(path='/nimbus/js/data-bind.js'):
|
|
1391
|
+
"""Generate data binding script tag."""
|
|
1392
|
+
return f'<script src="{path}"></script>'
|