@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.
Files changed (354) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/INTEGRATION.md +440 -0
  3. package/PROJECT_SUMMARY.md +312 -0
  4. package/README.md +286 -0
  5. package/SKILL.md +572 -0
  6. package/css/accessibility.css +119 -0
  7. package/css/animations.css +224 -0
  8. package/css/brand-themes.css +300 -0
  9. package/css/colors_and_type.css +441 -0
  10. package/css/components-ext.css +4165 -0
  11. package/css/components.css +1483 -0
  12. package/css/kupola.css +12 -0
  13. package/css/responsive.css +697 -0
  14. package/css/scaffold.css +145 -0
  15. package/css/states.css +316 -0
  16. package/css/theme-dark.css +296 -0
  17. package/css/theme-light.css +296 -0
  18. package/css/utilities.css +171 -0
  19. package/dist/css/kupola.css +12 -0
  20. package/dist/icons/icons/activity.svg +3 -0
  21. package/dist/icons/icons/alert-circle.svg +1 -0
  22. package/dist/icons/icons/alert-triangle.svg +1 -0
  23. package/dist/icons/icons/alert.d27e24.svg +1 -0
  24. package/dist/icons/icons/alert.svg +1 -0
  25. package/dist/icons/icons/apple.svg +1 -0
  26. package/dist/icons/icons/arrow-down.svg +1 -0
  27. package/dist/icons/icons/arrow-expand.svg +1 -0
  28. package/dist/icons/icons/arrow-left.svg +1 -0
  29. package/dist/icons/icons/arrow-minimize.svg +1 -0
  30. package/dist/icons/icons/arrow-right-to-line.svg +1 -0
  31. package/dist/icons/icons/arrow-right.svg +1 -0
  32. package/dist/icons/icons/arrow-up-right.svg +1 -0
  33. package/dist/icons/icons/arrow-up.0c0c0d.svg +1 -0
  34. package/dist/icons/icons/arrow-up.svg +1 -0
  35. package/dist/icons/icons/at.svg +1 -0
  36. package/dist/icons/icons/atom.0c0c0d.svg +1 -0
  37. package/dist/icons/icons/atom.svg +1 -0
  38. package/dist/icons/icons/bar-chart-2.svg +1 -0
  39. package/dist/icons/icons/bar.svg +1 -0
  40. package/dist/icons/icons/bell.svg +1 -0
  41. package/dist/icons/icons/box.svg +1 -0
  42. package/dist/icons/icons/bug.svg +1 -0
  43. package/dist/icons/icons/button.svg +1 -0
  44. package/dist/icons/icons/calendar.svg +1 -0
  45. package/dist/icons/icons/check-circle.33c192.svg +1 -0
  46. package/dist/icons/icons/check-circle.svg +1 -0
  47. package/dist/icons/icons/check-square.svg +4 -0
  48. package/dist/icons/icons/check.d1d3db.svg +1 -0
  49. package/dist/icons/icons/check.svg +1 -0
  50. package/dist/icons/icons/chevron-down.svg +1 -0
  51. package/dist/icons/icons/chevron-left.svg +1 -0
  52. package/dist/icons/icons/chevron-right.svg +1 -0
  53. package/dist/icons/icons/chevron-up.svg +1 -0
  54. package/dist/icons/icons/circle.svg +1 -0
  55. package/dist/icons/icons/clock.svg +1 -0
  56. package/dist/icons/icons/cmd.svg +1 -0
  57. package/dist/icons/icons/code.svg +1 -0
  58. package/dist/icons/icons/columns.svg +1 -0
  59. package/dist/icons/icons/copy.svg +1 -0
  60. package/dist/icons/icons/cpu.svg +1 -0
  61. package/dist/icons/icons/dashboard.0c0c0d.svg +1 -0
  62. package/dist/icons/icons/dashboard.svg +1 -0
  63. package/dist/icons/icons/dollar.svg +1 -0
  64. package/dist/icons/icons/download.svg +1 -0
  65. package/dist/icons/icons/edit.svg +1 -0
  66. package/dist/icons/icons/extensions.svg +1 -0
  67. package/dist/icons/icons/external.svg +1 -0
  68. package/dist/icons/icons/eye.svg +1 -0
  69. package/dist/icons/icons/file-text.svg +1 -0
  70. package/dist/icons/icons/file.svg +1 -0
  71. package/dist/icons/icons/files.svg +1 -0
  72. package/dist/icons/icons/filter.svg +1 -0
  73. package/dist/icons/icons/folder-open.svg +4 -0
  74. package/dist/icons/icons/folder.svg +1 -0
  75. package/dist/icons/icons/gear.svg +1 -0
  76. package/dist/icons/icons/git.svg +1 -0
  77. package/dist/icons/icons/github.svg +1 -0
  78. package/dist/icons/icons/globe.svg +1 -0
  79. package/dist/icons/icons/grid-2x2.svg +1 -0
  80. package/dist/icons/icons/hash.svg +1 -0
  81. package/dist/icons/icons/heart.svg +1 -0
  82. package/dist/icons/icons/help-circle.svg +5 -0
  83. package/dist/icons/icons/help.svg +1 -0
  84. package/dist/icons/icons/home.svg +1 -0
  85. package/dist/icons/icons/icons.svg +153 -0
  86. package/dist/icons/icons/image.svg +1 -0
  87. package/dist/icons/icons/info-circle.svg +1 -0
  88. package/dist/icons/icons/info-square.svg +1 -0
  89. package/dist/icons/icons/info.387bff.svg +1 -0
  90. package/dist/icons/icons/info.svg +1 -0
  91. package/dist/icons/icons/input.svg +1 -0
  92. package/dist/icons/icons/key.svg +1 -0
  93. package/dist/icons/icons/layers.svg +1 -0
  94. package/dist/icons/icons/layout-grid.svg +6 -0
  95. package/dist/icons/icons/layout.svg +1 -0
  96. package/dist/icons/icons/link.svg +1 -0
  97. package/dist/icons/icons/list.svg +1 -0
  98. package/dist/icons/icons/lock.svg +1 -0
  99. package/dist/icons/icons/log-out.svg +1 -0
  100. package/dist/icons/icons/logo.svg +1 -0
  101. package/dist/icons/icons/mail.svg +1 -0
  102. package/dist/icons/icons/menu.svg +1 -0
  103. package/dist/icons/icons/message-circle.svg +1 -0
  104. package/dist/icons/icons/message-plus.svg +1 -0
  105. package/dist/icons/icons/message-square.svg +1 -0
  106. package/dist/icons/icons/mic.svg +1 -0
  107. package/dist/icons/icons/minus.svg +1 -0
  108. package/dist/icons/icons/moon.svg +1 -0
  109. package/dist/icons/icons/more-h.svg +1 -0
  110. package/dist/icons/icons/mouse.svg +1 -0
  111. package/dist/icons/icons/palette.svg +1 -0
  112. package/dist/icons/icons/panel-bottom.svg +1 -0
  113. package/dist/icons/icons/pause.svg +1 -0
  114. package/dist/icons/icons/play.svg +1 -0
  115. package/dist/icons/icons/plug.svg +1 -0
  116. package/dist/icons/icons/plus.0c0c0d.svg +1 -0
  117. package/dist/icons/icons/plus.svg +1 -0
  118. package/dist/icons/icons/refresh.svg +1 -0
  119. package/dist/icons/icons/ruler.svg +1 -0
  120. package/dist/icons/icons/scroll-text.svg +1 -0
  121. package/dist/icons/icons/search-menu.svg +1 -0
  122. package/dist/icons/icons/search.svg +1 -0
  123. package/dist/icons/icons/send.0c0c0d.svg +1 -0
  124. package/dist/icons/icons/send.svg +1 -0
  125. package/dist/icons/icons/settings.0c0c0d.svg +1 -0
  126. package/dist/icons/icons/settings.svg +1 -0
  127. package/dist/icons/icons/shield.svg +1 -0
  128. package/dist/icons/icons/sidebar-left.svg +1 -0
  129. package/dist/icons/icons/sidebar-right.svg +1 -0
  130. package/dist/icons/icons/sliders.svg +1 -0
  131. package/dist/icons/icons/sparkles.32f08c.svg +1 -0
  132. package/dist/icons/icons/sparkles.svg +1 -0
  133. package/dist/icons/icons/square.svg +1 -0
  134. package/dist/icons/icons/star.svg +1 -0
  135. package/dist/icons/icons/sun.svg +1 -0
  136. package/dist/icons/icons/table.svg +1 -0
  137. package/dist/icons/icons/terminal.svg +1 -0
  138. package/dist/icons/icons/trash.svg +1 -0
  139. package/dist/icons/icons/trending-down.svg +1 -0
  140. package/dist/icons/icons/trending-up.svg +1 -0
  141. package/dist/icons/icons/type.svg +1 -0
  142. package/dist/icons/icons/upload.svg +1 -0
  143. package/dist/icons/icons/user-circle.svg +1 -0
  144. package/dist/icons/icons/user-plus.svg +6 -0
  145. package/dist/icons/icons/user.svg +1 -0
  146. package/dist/icons/icons/users.svg +1 -0
  147. package/dist/icons/icons/warning.svg +1 -0
  148. package/dist/icons/icons/wrench.svg +1 -0
  149. package/dist/icons/icons/x-circle.svg +1 -0
  150. package/dist/icons/icons/x.svg +1 -0
  151. package/dist/icons/icons/zap.0c0c0d.svg +1 -0
  152. package/dist/icons/icons/zap.svg +1 -0
  153. package/dist/kupola.cjs.js +258 -0
  154. package/dist/kupola.cjs.js.map +1 -0
  155. package/dist/kupola.esm.js +7640 -0
  156. package/dist/kupola.esm.js.map +1 -0
  157. package/dist/kupola.umd.js +258 -0
  158. package/dist/kupola.umd.js.map +1 -0
  159. package/dist/types/kupola.d.ts +1359 -0
  160. package/dist/utils/utils/Kupola.cs +77 -0
  161. package/dist/utils/utils/Kupola.java +104 -0
  162. package/dist/utils/utils/kupola.go +120 -0
  163. package/dist/utils/utils/kupola.js +63 -0
  164. package/dist/utils/utils/kupola.py +1392 -0
  165. package/dist/utils/utils/kupola.rb +69 -0
  166. package/icons/activity.svg +3 -0
  167. package/icons/alert-circle.svg +1 -0
  168. package/icons/alert-triangle.svg +1 -0
  169. package/icons/alert.d27e24.svg +1 -0
  170. package/icons/alert.svg +1 -0
  171. package/icons/apple.svg +1 -0
  172. package/icons/arrow-down.svg +1 -0
  173. package/icons/arrow-expand.svg +1 -0
  174. package/icons/arrow-left.svg +1 -0
  175. package/icons/arrow-minimize.svg +1 -0
  176. package/icons/arrow-right-to-line.svg +1 -0
  177. package/icons/arrow-right.svg +1 -0
  178. package/icons/arrow-up-right.svg +1 -0
  179. package/icons/arrow-up.0c0c0d.svg +1 -0
  180. package/icons/arrow-up.svg +1 -0
  181. package/icons/at.svg +1 -0
  182. package/icons/atom.0c0c0d.svg +1 -0
  183. package/icons/atom.svg +1 -0
  184. package/icons/bar-chart-2.svg +1 -0
  185. package/icons/bar.svg +1 -0
  186. package/icons/bell.svg +1 -0
  187. package/icons/box.svg +1 -0
  188. package/icons/bug.svg +1 -0
  189. package/icons/button.svg +1 -0
  190. package/icons/calendar.svg +1 -0
  191. package/icons/check-circle.33c192.svg +1 -0
  192. package/icons/check-circle.svg +1 -0
  193. package/icons/check-square.svg +4 -0
  194. package/icons/check.d1d3db.svg +1 -0
  195. package/icons/check.svg +1 -0
  196. package/icons/chevron-down.svg +1 -0
  197. package/icons/chevron-left.svg +1 -0
  198. package/icons/chevron-right.svg +1 -0
  199. package/icons/chevron-up.svg +1 -0
  200. package/icons/circle.svg +1 -0
  201. package/icons/clock.svg +1 -0
  202. package/icons/cmd.svg +1 -0
  203. package/icons/code.svg +1 -0
  204. package/icons/columns.svg +1 -0
  205. package/icons/copy.svg +1 -0
  206. package/icons/cpu.svg +1 -0
  207. package/icons/dashboard.0c0c0d.svg +1 -0
  208. package/icons/dashboard.svg +1 -0
  209. package/icons/dollar.svg +1 -0
  210. package/icons/download.svg +1 -0
  211. package/icons/edit.svg +1 -0
  212. package/icons/extensions.svg +1 -0
  213. package/icons/external.svg +1 -0
  214. package/icons/eye.svg +1 -0
  215. package/icons/file-text.svg +1 -0
  216. package/icons/file.svg +1 -0
  217. package/icons/files.svg +1 -0
  218. package/icons/filter.svg +1 -0
  219. package/icons/folder-open.svg +4 -0
  220. package/icons/folder.svg +1 -0
  221. package/icons/gear.svg +1 -0
  222. package/icons/git.svg +1 -0
  223. package/icons/github.svg +1 -0
  224. package/icons/globe.svg +1 -0
  225. package/icons/grid-2x2.svg +1 -0
  226. package/icons/hash.svg +1 -0
  227. package/icons/heart.svg +1 -0
  228. package/icons/help-circle.svg +5 -0
  229. package/icons/help.svg +1 -0
  230. package/icons/home.svg +1 -0
  231. package/icons/icons.svg +153 -0
  232. package/icons/image.svg +1 -0
  233. package/icons/info-circle.svg +1 -0
  234. package/icons/info-square.svg +1 -0
  235. package/icons/info.387bff.svg +1 -0
  236. package/icons/info.svg +1 -0
  237. package/icons/input.svg +1 -0
  238. package/icons/key.svg +1 -0
  239. package/icons/layers.svg +1 -0
  240. package/icons/layout-grid.svg +6 -0
  241. package/icons/layout.svg +1 -0
  242. package/icons/link.svg +1 -0
  243. package/icons/list.svg +1 -0
  244. package/icons/lock.svg +1 -0
  245. package/icons/log-out.svg +1 -0
  246. package/icons/logo.svg +1 -0
  247. package/icons/mail.svg +1 -0
  248. package/icons/menu.svg +1 -0
  249. package/icons/message-circle.svg +1 -0
  250. package/icons/message-plus.svg +1 -0
  251. package/icons/message-square.svg +1 -0
  252. package/icons/mic.svg +1 -0
  253. package/icons/minus.svg +1 -0
  254. package/icons/moon.svg +1 -0
  255. package/icons/more-h.svg +1 -0
  256. package/icons/mouse.svg +1 -0
  257. package/icons/palette.svg +1 -0
  258. package/icons/panel-bottom.svg +1 -0
  259. package/icons/pause.svg +1 -0
  260. package/icons/play.svg +1 -0
  261. package/icons/plug.svg +1 -0
  262. package/icons/plus.0c0c0d.svg +1 -0
  263. package/icons/plus.svg +1 -0
  264. package/icons/refresh.svg +1 -0
  265. package/icons/ruler.svg +1 -0
  266. package/icons/scroll-text.svg +1 -0
  267. package/icons/search-menu.svg +1 -0
  268. package/icons/search.svg +1 -0
  269. package/icons/send.0c0c0d.svg +1 -0
  270. package/icons/send.svg +1 -0
  271. package/icons/settings.0c0c0d.svg +1 -0
  272. package/icons/settings.svg +1 -0
  273. package/icons/shield.svg +1 -0
  274. package/icons/sidebar-left.svg +1 -0
  275. package/icons/sidebar-right.svg +1 -0
  276. package/icons/sliders.svg +1 -0
  277. package/icons/sparkles.32f08c.svg +1 -0
  278. package/icons/sparkles.svg +1 -0
  279. package/icons/square.svg +1 -0
  280. package/icons/star.svg +1 -0
  281. package/icons/sun.svg +1 -0
  282. package/icons/table.svg +1 -0
  283. package/icons/terminal.svg +1 -0
  284. package/icons/trash.svg +1 -0
  285. package/icons/trending-down.svg +1 -0
  286. package/icons/trending-up.svg +1 -0
  287. package/icons/type.svg +1 -0
  288. package/icons/upload.svg +1 -0
  289. package/icons/user-circle.svg +1 -0
  290. package/icons/user-plus.svg +6 -0
  291. package/icons/user.svg +1 -0
  292. package/icons/users.svg +1 -0
  293. package/icons/warning.svg +1 -0
  294. package/icons/wrench.svg +1 -0
  295. package/icons/x-circle.svg +1 -0
  296. package/icons/x.svg +1 -0
  297. package/icons/zap.0c0c0d.svg +1 -0
  298. package/icons/zap.svg +1 -0
  299. package/js/calendar.js +184 -0
  300. package/js/carousel.js +173 -0
  301. package/js/collapse.js +127 -0
  302. package/js/color-picker.js +144 -0
  303. package/js/component.js +276 -0
  304. package/js/composition-api.js +458 -0
  305. package/js/countdown.js +144 -0
  306. package/js/data-bind.js +1286 -0
  307. package/js/datepicker.js +270 -0
  308. package/js/dialog.js +131 -0
  309. package/js/drawer.js +118 -0
  310. package/js/dropdown.js +122 -0
  311. package/js/dynamic-tags.js +156 -0
  312. package/js/error-handler.js +181 -0
  313. package/js/fileupload.js +392 -0
  314. package/js/form.js +291 -0
  315. package/js/global-events.js +203 -0
  316. package/js/heatmap.js +405 -0
  317. package/js/http.js +419 -0
  318. package/js/i18n.js +228 -0
  319. package/js/icons.js +165 -0
  320. package/js/image-preview.js +218 -0
  321. package/js/initializer.js +162 -0
  322. package/js/kupola-core.js +66 -0
  323. package/js/kupola-devtools.js +598 -0
  324. package/js/kupola-lifecycle.js +510 -0
  325. package/js/message.js +60 -0
  326. package/js/modal.js +191 -0
  327. package/js/notification.js +73 -0
  328. package/js/numberinput.js +146 -0
  329. package/js/performance.js +250 -0
  330. package/js/registry.js +205 -0
  331. package/js/router.js +396 -0
  332. package/js/security.js +189 -0
  333. package/js/select.js +143 -0
  334. package/js/slide-captcha.js +504 -0
  335. package/js/slider.js +94 -0
  336. package/js/statcard.js +219 -0
  337. package/js/tag.js +83 -0
  338. package/js/test-utils.js +251 -0
  339. package/js/theme.js +363 -0
  340. package/js/timepicker.js +238 -0
  341. package/js/tooltip.js +122 -0
  342. package/js/utils.js +1458 -0
  343. package/js/validation.js +553 -0
  344. package/js/virtual-list.js +593 -0
  345. package/package.json +105 -0
  346. package/templates/base.html +30 -0
  347. package/templates/base_dashboard.html +99 -0
  348. package/types/kupola.d.ts +1359 -0
  349. package/utils/Kupola.cs +77 -0
  350. package/utils/Kupola.java +104 -0
  351. package/utils/kupola.go +120 -0
  352. package/utils/kupola.js +63 -0
  353. package/utils/kupola.py +1392 -0
  354. 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>'