cronixui 1.1.2 → 1.1.3

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 (76) hide show
  1. package/README.md +1 -1
  2. package/package.json +71 -71
  3. package/packages/flutter/.qwen/settings.json +7 -0
  4. package/packages/flutter/pubspec.yaml +20 -20
  5. package/packages/go/cronixui/cronixui.go +926 -926
  6. package/packages/python/README.md +142 -0
  7. package/packages/python/cronixui/__init__.py +15 -6
  8. package/packages/python/cronixui/__pycache__/__init__.cpython-314.pyc +0 -0
  9. package/packages/python/cronixui/__pycache__/accordion.cpython-314.pyc +0 -0
  10. package/packages/python/cronixui/__pycache__/alert.cpython-314.pyc +0 -0
  11. package/packages/python/cronixui/__pycache__/avatar.cpython-314.pyc +0 -0
  12. package/packages/python/cronixui/__pycache__/badge.cpython-314.pyc +0 -0
  13. package/packages/python/cronixui/__pycache__/button.cpython-314.pyc +0 -0
  14. package/packages/python/cronixui/__pycache__/card.cpython-314.pyc +0 -0
  15. package/packages/python/cronixui/__pycache__/command_palette.cpython-314.pyc +0 -0
  16. package/packages/python/cronixui/__pycache__/core.cpython-314.pyc +0 -0
  17. package/packages/python/cronixui/__pycache__/dropdown.cpython-314.pyc +0 -0
  18. package/packages/python/cronixui/__pycache__/form.cpython-314.pyc +0 -0
  19. package/packages/python/cronixui/__pycache__/layout.cpython-314.pyc +0 -0
  20. package/packages/python/cronixui/__pycache__/list.cpython-314.pyc +0 -0
  21. package/packages/python/cronixui/__pycache__/loading.cpython-314.pyc +0 -0
  22. package/packages/python/cronixui/__pycache__/modal.cpython-314.pyc +0 -0
  23. package/packages/python/cronixui/__pycache__/nav.cpython-314.pyc +0 -0
  24. package/packages/python/cronixui/__pycache__/pagination.cpython-314.pyc +0 -0
  25. package/packages/python/cronixui/__pycache__/progress.cpython-314.pyc +0 -0
  26. package/packages/python/cronixui/__pycache__/search.cpython-314.pyc +0 -0
  27. package/packages/python/cronixui/__pycache__/table.cpython-314.pyc +0 -0
  28. package/packages/python/cronixui/__pycache__/tabs.cpython-314.pyc +0 -0
  29. package/packages/python/cronixui/__pycache__/toast.cpython-314.pyc +0 -0
  30. package/packages/python/cronixui/__pycache__/toggle.cpython-314.pyc +0 -0
  31. package/packages/python/cronixui/__pycache__/tokens.cpython-314.pyc +0 -0
  32. package/packages/python/cronixui/__pycache__/tooltip.cpython-314.pyc +0 -0
  33. package/packages/python/cronixui/alert.py +119 -36
  34. package/packages/python/cronixui/avatar.py +129 -22
  35. package/packages/python/cronixui/badge.py +161 -24
  36. package/packages/python/cronixui/button.py +96 -27
  37. package/packages/python/cronixui/card.py +206 -33
  38. package/packages/python/cronixui/core.py +212 -23
  39. package/packages/python/cronixui/form.py +552 -141
  40. package/packages/python/cronixui/layout.py +358 -96
  41. package/packages/python/cronixui/list.py +140 -37
  42. package/packages/python/cronixui/loading.py +107 -17
  43. package/packages/python/cronixui/progress.py +189 -47
  44. package/packages/python/cronixui/table.py +118 -31
  45. package/packages/python/cronixui/tooltip.py +117 -15
  46. package/packages/react/src/components/Accordion.tsx +82 -82
  47. package/packages/react/src/components/Button.tsx +47 -47
  48. package/packages/react/src/components/Card.tsx +69 -69
  49. package/packages/react/src/components/CommandPalette.tsx +131 -131
  50. package/packages/react/src/components/Dropdown.tsx +88 -88
  51. package/packages/react/src/components/FileInput.tsx +86 -86
  52. package/packages/react/src/components/FormGroup.tsx +36 -36
  53. package/packages/react/src/components/List.tsx +55 -55
  54. package/packages/react/src/components/Pagination.tsx +107 -107
  55. package/packages/react/src/components/Progress.tsx +49 -49
  56. package/packages/react/src/components/Search.tsx +95 -95
  57. package/packages/react/src/components/Sidebar.tsx +64 -64
  58. package/packages/react/src/components/Stack.tsx +69 -69
  59. package/packages/react/src/components/Table.tsx +90 -90
  60. package/packages/react/src/components/Toast.tsx +134 -134
  61. package/packages/react/src/components/Typography.tsx +66 -66
  62. package/packages/react/src/index.ts +40 -40
  63. package/packages/react/src/styles.css +2039 -2039
  64. package/packages/rust/cronixui/src/components/avatar.rs +85 -85
  65. package/packages/rust/cronixui/src/components/breadcrumb.rs +58 -58
  66. package/packages/rust/cronixui/src/components/card.rs +259 -259
  67. package/packages/rust/cronixui/src/components/command_palette.rs +254 -254
  68. package/packages/rust/cronixui/src/components/dropdown.rs +179 -179
  69. package/packages/rust/cronixui/src/components/file_input.rs +74 -74
  70. package/packages/rust/cronixui/src/components/mod.rs +51 -51
  71. package/packages/rust/cronixui/src/components/search.rs +185 -185
  72. package/packages/rust/cronixui/src/components/skeleton.rs +63 -63
  73. package/packages/rust/cronixui/src/components/table.rs +56 -56
  74. package/packages/rust/cronixui/src/lib.rs +128 -128
  75. package/packages/web/dist/cronixui.css +97 -93
  76. package/packages/web/dist/cronixui.min.css +1 -1
@@ -1,926 +1,926 @@
1
- package cronixui
2
-
3
- import (
4
- "image/color"
5
- "strings"
6
-
7
- "fyne.io/fyne/v2"
8
- "fyne.io/fyne/v2/app"
9
- "fyne.io/fyne/v2/canvas"
10
- "fyne.io/fyne/v2/container"
11
- "fyne.io/fyne/v2/layout"
12
- "fyne.io/fyne/v2/theme"
13
- "fyne.io/fyne/v2/widget"
14
- )
15
-
16
- const Version = "1.0.6"
17
-
18
- // =============================================================================
19
- // DESIGN TOKENS
20
- // =============================================================================
21
-
22
- type Colors struct {
23
- BG color.Color
24
- Surface color.Color
25
- Surface2 color.Color
26
- Surface3 color.Color
27
- Surface4 color.Color
28
- Text color.Color
29
- TextMuted color.Color
30
- TextDim color.Color
31
- Accent color.Color
32
- AccentHover color.Color
33
- AccentLight color.Color
34
- AccentGlow color.Color
35
- AccentText color.Color
36
- Success color.Color
37
- SuccessBorder color.Color
38
- SuccessText color.Color
39
- Warning color.Color
40
- WarningBorder color.Color
41
- WarningText color.Color
42
- Error color.Color
43
- ErrorBorder color.Color
44
- ErrorText color.Color
45
- Info color.Color
46
- InfoBorder color.Color
47
- InfoText color.Color
48
- Border color.Color
49
- BorderHover color.Color
50
- BorderFocus color.Color
51
- }
52
-
53
- func DefaultColors() *Colors {
54
- return &Colors{
55
- BG: color.RGBA{R: 10, G: 10, B: 10, A: 255},
56
- Surface: color.RGBA{R: 17, G: 17, B: 17, A: 255},
57
- Surface2: color.RGBA{R: 26, G: 26, B: 26, A: 255},
58
- Surface3: color.RGBA{R: 34, G: 34, B: 34, A: 255},
59
- Surface4: color.RGBA{R: 42, G: 42, B: 42, A: 255},
60
- Text: color.RGBA{R: 240, G: 237, B: 232, A: 255},
61
- TextMuted: color.RGBA{R: 240, G: 237, B: 232, A: 128},
62
- TextDim: color.RGBA{R: 240, G: 237, B: 232, A: 64},
63
- Accent: color.RGBA{R: 107, G: 35, B: 35, A: 255},
64
- AccentHover: color.RGBA{R: 125, G: 42, B: 42, A: 255},
65
- AccentLight: color.RGBA{R: 138, G: 53, B: 53, A: 255},
66
- AccentGlow: color.RGBA{R: 107, G: 35, B: 35, A: 77},
67
- AccentText: color.RGBA{R: 201, G: 122, B: 122, A: 255},
68
- Success: color.RGBA{R: 30, G: 80, B: 40, A: 255},
69
- SuccessBorder: color.RGBA{R: 60, G: 140, B: 70, A: 102},
70
- SuccessText: color.RGBA{R: 107, G: 196, B: 122, A: 255},
71
- Warning: color.RGBA{R: 80, G: 60, B: 20, A: 255},
72
- WarningBorder: color.RGBA{R: 150, G: 110, B: 30, A: 102},
73
- WarningText: color.RGBA{R: 196, G: 164, B: 58, A: 255},
74
- Error: color.RGBA{R: 80, G: 20, B: 20, A: 255},
75
- ErrorBorder: color.RGBA{R: 180, G: 60, B: 60, A: 102},
76
- ErrorText: color.RGBA{R: 196, G: 107, B: 107, A: 255},
77
- Info: color.RGBA{R: 20, G: 53, B: 80, A: 255},
78
- InfoBorder: color.RGBA{R: 60, G: 140, B: 200, A: 102},
79
- InfoText: color.RGBA{R: 107, G: 168, B: 196, A: 255},
80
- Border: color.RGBA{R: 255, G: 255, B: 255, A: 20},
81
- BorderHover: color.RGBA{R: 255, G: 255, B: 255, A: 38},
82
- BorderFocus: color.RGBA{R: 255, G: 255, B: 255, A: 64},
83
- }
84
- }
85
-
86
- type Typography struct {
87
- FontFamily string
88
- FontMono string
89
- FontSizeXS float32
90
- FontSizeSM float32
91
- FontSizeBase float32
92
- FontSizeMD float32
93
- FontSizeLG float32
94
- FontSizeXL float32
95
- FontSize2XL float32
96
- FontSize3XL float32
97
- }
98
-
99
- func DefaultTypography() *Typography {
100
- return &Typography{
101
- FontFamily: "Outfit",
102
- FontMono: "JetBrains Mono",
103
- FontSizeXS: 11,
104
- FontSizeSM: 12,
105
- FontSizeBase: 13,
106
- FontSizeMD: 14,
107
- FontSizeLG: 16,
108
- FontSizeXL: 20,
109
- FontSize2XL: 28,
110
- FontSize3XL: 36,
111
- }
112
- }
113
-
114
- type Spacing struct {
115
- Space1, Space2, Space3, Space4, Space5, Space6, Space8, Space10, Space12 float32
116
- }
117
-
118
- func DefaultSpacing() *Spacing {
119
- return &Spacing{4, 8, 12, 16, 20, 24, 32, 40, 48}
120
- }
121
-
122
- type RadiusTokens struct {
123
- SM, Default, LG, XL, Full float32
124
- }
125
-
126
- func DefaultRadiusTokens() *RadiusTokens {
127
- return &RadiusTokens{6, 10, 14, 20, 9999}
128
- }
129
-
130
- type Shadow struct {
131
- SM, Default, LG, Glow string
132
- }
133
-
134
- func DefaultShadow() *Shadow {
135
- return &Shadow{
136
- SM: "0 1px 2px rgba(0, 0, 0, 0.3)",
137
- Default: "0 4px 12px rgba(0, 0, 0, 0.4)",
138
- LG: "0 8px 24px rgba(0, 0, 0, 0.5)",
139
- Glow: "0 0 20px rgba(107, 35, 35, 0.3)",
140
- }
141
- }
142
-
143
- type Transition struct {
144
- Fast, Default, Slow string
145
- }
146
-
147
- func DefaultTransition() *Transition {
148
- return &Transition{"0.1s ease", "0.15s ease", "0.25s ease"}
149
- }
150
-
151
- type ZIndexTokens struct {
152
- Dropdown, Sticky, Fixed, ModalBackdrop, Modal, Popover, Tooltip, Toast int
153
- }
154
-
155
- func DefaultZIndexTokens() *ZIndexTokens {
156
- return &ZIndexTokens{100, 200, 300, 400, 500, 600, 700, 800}
157
- }
158
-
159
- type LayoutTokens struct {
160
- ContainerMax, SidebarWidth, HeaderHeight float32
161
- }
162
-
163
- func DefaultLayoutTokens() *LayoutTokens {
164
- return &LayoutTokens{1200, 260, 56}
165
- }
166
-
167
- // =============================================================================
168
- // THEME
169
- // =============================================================================
170
-
171
- type Theme struct {
172
- colors *Colors
173
- spacing *Spacing
174
- radius *RadiusTokens
175
- }
176
-
177
- func NewTheme() fyne.Theme {
178
- return &Theme{
179
- colors: DefaultColors(),
180
- spacing: DefaultSpacing(),
181
- radius: DefaultRadiusTokens(),
182
- }
183
- }
184
-
185
- func (t *Theme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
186
- switch name {
187
- case theme.ColorNameBackground:
188
- return t.colors.BG
189
- case theme.ColorNameButton:
190
- return t.colors.Surface2
191
- case theme.ColorNameDisabledButton:
192
- return t.colors.Surface3
193
- case theme.ColorNameInputBackground:
194
- return t.colors.Surface
195
- case theme.ColorNameInputBorder:
196
- return t.colors.Border
197
- case theme.ColorNameDisabled:
198
- return t.colors.TextDim
199
- case theme.ColorNameForeground:
200
- return t.colors.Text
201
- case theme.ColorNamePlaceHolder:
202
- return t.colors.TextMuted
203
- case theme.ColorNamePrimary:
204
- return t.colors.Accent
205
- case theme.ColorNameHover:
206
- return t.colors.Surface3
207
- case theme.ColorNamePressed:
208
- return t.colors.Surface4
209
- case theme.ColorNameScrollBar:
210
- return t.colors.Surface3
211
- case theme.ColorNameShadow:
212
- return color.RGBA{R: 0, G: 0, B: 0, A: 50}
213
- case theme.ColorNameSuccess:
214
- return t.colors.Success
215
- case theme.ColorNameWarning:
216
- return t.colors.Warning
217
- case theme.ColorNameError:
218
- return t.colors.Error
219
- default:
220
- return t.colors.Text
221
- }
222
- }
223
-
224
- func (t *Theme) Font(style fyne.TextStyle) fyne.Resource {
225
- return theme.DefaultTheme().Font(style)
226
- }
227
-
228
- func (t *Theme) Icon(name fyne.ThemeIconName) fyne.Resource {
229
- return theme.DefaultTheme().Icon(name)
230
- }
231
-
232
- func (t *Theme) Size(name fyne.ThemeSizeName) float32 {
233
- switch name {
234
- case theme.SizeNamePadding:
235
- return t.spacing.Space2
236
- case theme.SizeNameInlinePadding:
237
- return t.spacing.Space3
238
- case theme.SizeNameScrollBar:
239
- return 8
240
- case theme.SizeNameScrollBarSmall:
241
- return 4
242
- case theme.SizeNameSeparatorThickness:
243
- return 1
244
- case theme.SizeNameText:
245
- return t.spacing.Space4
246
- case theme.SizeNameHeadingText:
247
- return t.spacing.Space6
248
- case theme.SizeNameSubHeadingText:
249
- return t.spacing.Space5
250
- case theme.SizeNameCaptionText:
251
- return t.spacing.Space2
252
- case theme.SizeNameInputBorder:
253
- return 1
254
- case theme.SizeNameInputRadius:
255
- return t.radius.Default
256
- case theme.SizeNameSelectionRadius:
257
- return t.radius.SM
258
- default:
259
- return 0
260
- }
261
- }
262
-
263
- // =============================================================================
264
- // BUTTONS
265
- // =============================================================================
266
-
267
- type ButtonVariant int
268
-
269
- const (
270
- ButtonDefault ButtonVariant = iota
271
- ButtonPrimary
272
- ButtonGhost
273
- ButtonOutline
274
- ButtonDanger
275
- ButtonSuccess
276
- )
277
-
278
- func NewButton(text string, variant ButtonVariant, onTap func()) *widget.Button {
279
- btn := widget.NewButton(text, onTap)
280
- switch variant {
281
- case ButtonPrimary:
282
- btn.Importance = widget.HighImportance
283
- case ButtonDanger:
284
- btn.Importance = widget.DangerImportance
285
- case ButtonSuccess:
286
- btn.Importance = widget.HighImportance
287
- case ButtonGhost, ButtonOutline:
288
- btn.Importance = widget.LowImportance
289
- default:
290
- btn.Importance = widget.MediumImportance
291
- }
292
- return btn
293
- }
294
-
295
- func NewButtonGroup(buttons ...*widget.Button) *fyne.Container {
296
- return container.NewHBox(buttons...)
297
- }
298
-
299
- // =============================================================================
300
- // CARD
301
- // =============================================================================
302
-
303
- // CardOption is a function that configures a card.
304
- type CardOption func(*CardConfig)
305
-
306
- // CardConfig holds card configuration.
307
- type CardConfig struct {
308
- Title string
309
- Subtitle string
310
- Icon fyne.CanvasObject
311
- Footer fyne.CanvasObject
312
- Clickable bool
313
- }
314
-
315
- // WithCardTitle sets the card title.
316
- func WithCardTitle(title string) CardOption {
317
- return func(c *CardConfig) { c.Title = title }
318
- }
319
-
320
- // WithCardSubtitle sets the card subtitle.
321
- func WithCardSubtitle(subtitle string) CardOption {
322
- return func(c *CardConfig) { c.Subtitle = subtitle }
323
- }
324
-
325
- // WithCardIcon sets the card icon.
326
- func WithCardIcon(icon fyne.CanvasObject) CardOption {
327
- return func(c *CardConfig) { c.Icon = icon }
328
- }
329
-
330
- // WithCardFooter sets the card footer.
331
- func WithCardFooter(footer fyne.CanvasObject) CardOption {
332
- return func(c *CardConfig) { c.Footer = footer }
333
- }
334
-
335
- // WithCardClickable makes the card clickable.
336
- func WithCardClickable(clickable bool) CardOption {
337
- return func(c *CardConfig) { c.Clickable = clickable }
338
- }
339
-
340
- // NewCard creates a card widget with optional icon, title, subtitle, and footer.
341
- func NewCard(content fyne.CanvasObject, options ...CardOption) *fyne.Container {
342
- config := &CardConfig{}
343
- for _, opt := range options {
344
- opt(config)
345
- }
346
-
347
- c := DefaultColors()
348
-
349
- var headerObjects []fyne.CanvasObject
350
- if config.Icon != nil {
351
- headerObjects = append(headerObjects, config.Icon)
352
- }
353
- if config.Title != "" {
354
- titleText := canvas.NewText(config.Title, c.Text)
355
- titleText.TextSize = 14
356
- titleText.TextStyle = fyne.TextStyle{Bold: true}
357
- headerObjects = append(headerObjects, titleText)
358
- }
359
- if config.Subtitle != "" {
360
- subtitleText := canvas.NewText(config.Subtitle, c.TextMuted)
361
- subtitleText.TextSize = 12
362
- headerObjects = append(headerObjects, subtitleText)
363
- }
364
-
365
- var header *fyne.Container
366
- if len(headerObjects) > 0 {
367
- header = container.NewVBox(headerObjects...)
368
- }
369
-
370
- var body fyne.CanvasObject
371
- if header != nil {
372
- body = container.NewVBox(header, content)
373
- } else {
374
- body = content
375
- }
376
-
377
- var finalContent *fyne.Container
378
- if config.Footer != nil {
379
- finalContent = container.NewVBox(body, canvas.NewRectangle(c.Border), config.Footer)
380
- } else {
381
- if c, ok := body.(*fyne.Container); ok {
382
- finalContent = c
383
- } else {
384
- finalContent = container.NewMax(body)
385
- }
386
- }
387
-
388
- bg := canvas.NewRectangle(c.Surface)
389
- bg.StrokeColor = c.Border
390
- bg.StrokeWidth = 1
391
- bg.CornerRadius = 14
392
-
393
- return container.NewStack(bg, container.NewPadded(finalContent))
394
- }
395
-
396
- // =============================================================================
397
- // INPUTS
398
- // =============================================================================
399
-
400
- func NewInput(placeholder string) *widget.Entry {
401
- entry := widget.NewEntry()
402
- entry.SetPlaceHolder(placeholder)
403
- return entry
404
- }
405
-
406
- func NewTextarea(placeholder string) *widget.Entry {
407
- entry := widget.NewMultiLineEntry()
408
- entry.SetPlaceHolder(placeholder)
409
- return entry
410
- }
411
-
412
- func NewPassword(placeholder string) *widget.Entry {
413
- entry := widget.NewPasswordEntry()
414
- entry.SetPlaceHolder(placeholder)
415
- return entry
416
- }
417
-
418
- // =============================================================================
419
- // FORM ELEMENTS
420
- // =============================================================================
421
-
422
- func NewCheckbox(label string, onChanged func(bool)) *widget.Check {
423
- return widget.NewCheck(label, onChanged)
424
- }
425
-
426
- func NewRadio(options []string, onChanged func(string)) *widget.RadioGroup {
427
- return widget.NewRadioGroup(options, onChanged)
428
- }
429
-
430
- func NewSelect(options []string, onChanged func(string)) *widget.Select {
431
- return widget.NewSelect(options, onChanged)
432
- }
433
-
434
- func NewSlider(min, max float64) *widget.Slider {
435
- return widget.NewSlider(min, max)
436
- }
437
-
438
- // =============================================================================
439
- // PROGRESS & LOADING
440
- // =============================================================================
441
-
442
- func NewProgress() *widget.ProgressBar {
443
- return widget.NewProgressBar()
444
- }
445
-
446
- func NewProgressInfinite() *widget.ProgressBarInfinite {
447
- return widget.NewProgressBarInfinite()
448
- }
449
-
450
- // =============================================================================
451
- // TOGGLE
452
- // =============================================================================
453
-
454
- type Toggle struct {
455
- widget.BaseWidget
456
- on bool
457
- onToggle func(bool)
458
- }
459
-
460
- func NewToggle(onToggle func(bool)) *Toggle {
461
- t := &Toggle{onToggle: onToggle}
462
- t.ExtendBaseWidget(t)
463
- return t
464
- }
465
-
466
- func (t *Toggle) Toggle() {
467
- t.on = !t.on
468
- if t.onToggle != nil {
469
- t.onToggle(t.on)
470
- }
471
- t.Refresh()
472
- }
473
-
474
- func (t *Toggle) IsOn() bool { return t.on }
475
- func (t *Toggle) SetOn(value bool) { t.on = value; t.Refresh() }
476
-
477
- // =============================================================================
478
- // MODAL
479
- // =============================================================================
480
-
481
- type Modal struct {
482
- widget.BaseWidget
483
- content fyne.CanvasObject
484
- open bool
485
- window fyne.Window
486
- }
487
-
488
- func NewModal(content fyne.CanvasObject) *Modal {
489
- m := &Modal{content: content}
490
- m.ExtendBaseWidget(m)
491
- return m
492
- }
493
-
494
- func (m *Modal) Open(window fyne.Window) {
495
- m.window = window
496
- m.open = true
497
- m.Show()
498
- }
499
-
500
- func (m *Modal) Close() { m.open = false; m.Hide() }
501
- func (m *Modal) IsOpen() bool { return m.open }
502
- func (m *Modal) Show() {
503
- if m.window != nil {
504
- popup := widget.NewModalPopUp(m.content, m.window.Canvas())
505
- popup.Show()
506
- }
507
- }
508
- func (m *Modal) Hide() {}
509
-
510
- // =============================================================================
511
- // DROPDOWN
512
- // =============================================================================
513
-
514
- // Dropdown creates a dropdown selection widget using Fyne's Select widget.
515
- func NewDropdown(items []string, onSelect func(string)) *widget.Select {
516
- s := widget.NewSelect(items, onSelect)
517
- return s
518
- }
519
-
520
- // =============================================================================
521
- // TABS
522
- // =============================================================================
523
-
524
- func NewTabs(items ...*widget.TabItem) *container.AppTabs {
525
- return container.NewAppTabs(items...)
526
- }
527
-
528
- // =============================================================================
529
- // ACCORDION
530
- // =============================================================================
531
-
532
- type AccordionItem struct {
533
- Title string
534
- Content fyne.CanvasObject
535
- }
536
-
537
- type Accordion struct {
538
- widget.BaseWidget
539
- items []AccordionItem
540
- openIndices map[int]bool
541
- }
542
-
543
- func NewAccordion(items ...AccordionItem) *Accordion {
544
- a := &Accordion{items: items, openIndices: make(map[int]bool)}
545
- a.ExtendBaseWidget(a)
546
- return a
547
- }
548
-
549
- func (a *Accordion) Toggle(index int) { a.openIndices[index] = !a.openIndices[index]; a.Refresh() }
550
- func (a *Accordion) Open(index int) { a.openIndices[index] = true; a.Refresh() }
551
- func (a *Accordion) Close(index int) { delete(a.openIndices, index); a.Refresh() }
552
- func (a *Accordion) OpenAll() {
553
- for i := range a.items {
554
- a.openIndices[i] = true
555
- }
556
- a.Refresh()
557
- }
558
- func (a *Accordion) CloseAll() { a.openIndices = make(map[int]bool); a.Refresh() }
559
- func (a *Accordion) IsOpen(index int) bool { return a.openIndices[index] }
560
-
561
- // =============================================================================
562
- // PAGINATION
563
- // =============================================================================
564
-
565
- type Pagination struct {
566
- widget.BaseWidget
567
- total int
568
- current int
569
- onChange func(int)
570
- }
571
-
572
- func NewPagination(total, current int, onChange func(int)) *Pagination {
573
- p := &Pagination{total: total, current: current, onChange: onChange}
574
- p.ExtendBaseWidget(p)
575
- return p
576
- }
577
-
578
- func (p *Pagination) GoTo(page int) {
579
- if page >= 1 && page <= p.total {
580
- p.current = page
581
- if p.onChange != nil {
582
- p.onChange(page)
583
- }
584
- p.Refresh()
585
- }
586
- }
587
- func (p *Pagination) Next() {
588
- if p.current < p.total {
589
- p.GoTo(p.current + 1)
590
- }
591
- }
592
- func (p *Pagination) Prev() {
593
- if p.current > 1 {
594
- p.GoTo(p.current - 1)
595
- }
596
- }
597
- func (p *Pagination) Current() int { return p.current }
598
- func (p *Pagination) Total() int { return p.total }
599
-
600
- // =============================================================================
601
- // TOAST
602
- // =============================================================================
603
-
604
- type ToastType int
605
-
606
- const (
607
- ToastSuccess ToastType = iota
608
- ToastError
609
- ToastWarning
610
- ToastInfo
611
- )
612
-
613
- func ShowToast(window fyne.Window, message string, toastType ToastType) {
614
- c := DefaultColors()
615
- var bgColor color.Color
616
- var prefix string
617
- switch toastType {
618
- case ToastSuccess:
619
- bgColor = c.Success
620
- prefix = "✓ "
621
- case ToastError:
622
- bgColor = c.Error
623
- prefix = "✕ "
624
- case ToastWarning:
625
- bgColor = c.Warning
626
- prefix = "⚠ "
627
- default:
628
- bgColor = c.Info
629
- prefix = "ℹ "
630
- }
631
- label := canvas.NewText(prefix+message, c.Text)
632
- label.TextSize = 13
633
- bg := canvas.NewRectangle(bgColor)
634
- bg.CornerRadius = 10
635
- cont := container.NewStack(bg, container.NewPadded(label))
636
- popover := widget.NewPopUp(cont, window.Canvas())
637
- popover.Move(fyne.NewPos(window.Canvas().Size().Width-320, window.Canvas().Size().Height-60))
638
- }
639
-
640
- // =============================================================================
641
- // COMMAND PALETTE
642
- // =============================================================================
643
-
644
- type CommandItem struct {
645
- Title, Subtitle, Kbd string
646
- Action func()
647
- }
648
-
649
- // CommandPalette creates a modal command palette with search and filtering.
650
- func NewCommandPalette(window fyne.Window, items []CommandItem) *fyne.Container {
651
- c := DefaultColors()
652
- searchEntry := widget.NewEntry()
653
- searchEntry.SetPlaceHolder("Type to search commands...")
654
-
655
- list := widget.NewList(
656
- func() int { return len(items) },
657
- func() fyne.CanvasObject {
658
- return container.NewVBox(
659
- widget.NewLabel("Command"),
660
- widget.NewLabel("Description"),
661
- )
662
- },
663
- func(id widget.ListItemID, item fyne.CanvasObject) {
664
- if id < len(items) {
665
- vbox := item.(*fyne.Container)
666
- vbox.Objects[0].(*widget.Label).SetText(items[id].Title)
667
- vbox.Objects[1].(*widget.Label).SetText(items[id].Subtitle)
668
- }
669
- },
670
- )
671
-
672
- searchEntry.OnChanged = func(query string) {
673
- // Filter logic can be added here
674
- list.Refresh()
675
- }
676
-
677
- list.OnSelected = func(id widget.ListItemID) {
678
- if id < len(items) && items[id].Action != nil {
679
- items[id].Action()
680
- window.Hide()
681
- }
682
- }
683
-
684
- content := container.NewVBox(
685
- searchEntry,
686
- container.NewScroll(list),
687
- )
688
-
689
- bg := canvas.NewRectangle(c.Surface)
690
- bg.CornerRadius = 14
691
-
692
- return container.NewStack(bg, container.NewPadded(content))
693
- }
694
-
695
- // =============================================================================
696
- // SEARCH
697
- // =============================================================================
698
-
699
- type SearchItem struct {
700
- Title, Subtitle string
701
- Action func()
702
- }
703
-
704
- // Search creates a search widget with input and results list.
705
- func NewSearch(items []SearchItem, onSelect func(SearchItem)) *fyne.Container {
706
- c := DefaultColors()
707
- searchEntry := widget.NewEntry()
708
- searchEntry.SetPlaceHolder("Search...")
709
-
710
- results := make([]SearchItem, 0)
711
- resultsList := widget.NewList(
712
- func() int { return len(results) },
713
- func() fyne.CanvasObject {
714
- return container.NewVBox(
715
- widget.NewLabel("Title"),
716
- widget.NewLabel("Subtitle"),
717
- )
718
- },
719
- func(id widget.ListItemID, item fyne.CanvasObject) {
720
- if id < len(results) {
721
- vbox := item.(*fyne.Container)
722
- vbox.Objects[0].(*widget.Label).SetText(results[id].Title)
723
- vbox.Objects[1].(*widget.Label).SetText(results[id].Subtitle)
724
- }
725
- },
726
- )
727
-
728
- resultsList.OnSelected = func(id widget.ListItemID) {
729
- if id < len(results) && onSelect != nil {
730
- onSelect(results[id])
731
- }
732
- }
733
-
734
- searchEntry.OnChanged = func(query string) {
735
- results = make([]SearchItem, 0)
736
- if query == "" {
737
- resultsList.Refresh()
738
- return
739
- }
740
- for _, item := range items {
741
- // Simple case-insensitive search
742
- if containsIgnoreCase(item.Title, query) || containsIgnoreCase(item.Subtitle, query) {
743
- results = append(results, item)
744
- }
745
- }
746
- resultsList.Refresh()
747
- }
748
-
749
- content := container.NewVBox(
750
- searchEntry,
751
- container.NewScroll(resultsList),
752
- )
753
-
754
- return content
755
- }
756
-
757
- func containsIgnoreCase(s, substr string) bool {
758
- return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
759
- }
760
-
761
- // =============================================================================
762
- // TABLE
763
- // =============================================================================
764
-
765
- func NewTable(rows, cols int, cellFunc func(row, col int) fyne.CanvasObject) *widget.Table {
766
- return widget.NewTable(
767
- func() (int, int) { return rows, cols },
768
- func() fyne.CanvasObject { return widget.NewLabel("") },
769
- func(id widget.TableCellID, cell fyne.CanvasObject) {
770
- if obj := cellFunc(id.Row, id.Col); obj != nil {
771
- cell.(*widget.Label).SetText(obj.(*widget.Label).Text)
772
- }
773
- },
774
- )
775
- }
776
-
777
- // =============================================================================
778
- // LIST
779
- // =============================================================================
780
-
781
- type ListItem struct {
782
- Title, Subtitle string
783
- }
784
-
785
- func NewList(items []ListItem) *widget.List {
786
- return widget.NewList(
787
- func() int { return len(items) },
788
- func() fyne.CanvasObject { return widget.NewLabel("") },
789
- func(id widget.ListItemID, item fyne.CanvasObject) {
790
- item.(*widget.Label).SetText(items[id].Title)
791
- },
792
- )
793
- }
794
-
795
- // =============================================================================
796
- // LAYOUT HELPERS
797
- // =============================================================================
798
-
799
- func NewContainer(content fyne.CanvasObject) fyne.CanvasObject {
800
- return container.NewMax(content)
801
- }
802
-
803
- func NewFlex(objects ...fyne.CanvasObject) fyne.CanvasObject {
804
- return container.NewHBox(objects...)
805
- }
806
-
807
- func NewStack(objects ...fyne.CanvasObject) fyne.CanvasObject {
808
- return container.NewVBox(objects...)
809
- }
810
-
811
- func NewGrid(cols int, objects ...fyne.CanvasObject) fyne.CanvasObject {
812
- return container.NewGridWithColumns(cols, objects...)
813
- }
814
-
815
- func NewForm(items ...*widget.FormItem) *widget.Form {
816
- return widget.NewForm(items...)
817
- }
818
-
819
- // =============================================================================
820
- // NAV
821
- // =============================================================================
822
-
823
- // Nav creates a navigation sidebar widget.
824
- func NewNav(items []string, active int, onSelect func(int)) *widget.List {
825
- list := widget.NewList(
826
- func() int { return len(items) },
827
- func() fyne.CanvasObject {
828
- return widget.NewLabel("Nav Item")
829
- },
830
- func(id widget.ListItemID, item fyne.CanvasObject) {
831
- if id < len(items) {
832
- item.(*widget.Label).SetText(items[id])
833
- }
834
- },
835
- )
836
- list.OnSelected = func(id widget.ListItemID) {
837
- if onSelect != nil {
838
- onSelect(id)
839
- }
840
- }
841
- if active >= 0 && active < len(items) {
842
- list.Select(active)
843
- }
844
- return list
845
- }
846
-
847
- // =============================================================================
848
- // STAT
849
- // =============================================================================
850
-
851
- type StatDeltaType int
852
-
853
- const (
854
- StatDeltaUp StatDeltaType = iota
855
- StatDeltaDown
856
- )
857
-
858
- // Stat creates a stat/metric display widget with value, label, and optional delta.
859
- func NewStat(value string, label string, delta string, deltaType StatDeltaType) *fyne.Container {
860
- c := DefaultColors()
861
-
862
- valueLabel := canvas.NewText(value, c.Text)
863
- valueLabel.TextSize = 28
864
- valueLabel.TextStyle = fyne.TextStyle{Bold: true}
865
-
866
- labelWidget := canvas.NewText(label, c.TextMuted)
867
- labelWidget.TextSize = 11
868
-
869
- var deltaWidget *canvas.Text
870
- if delta != "" {
871
- deltaColor := c.SuccessText
872
- if deltaType == StatDeltaDown {
873
- deltaColor = c.ErrorText
874
- }
875
- prefix := "↑ "
876
- if deltaType == StatDeltaDown {
877
- prefix = "↓ "
878
- }
879
- deltaWidget = canvas.NewText(prefix+delta, deltaColor)
880
- deltaWidget.TextSize = 11
881
- }
882
-
883
- content := container.NewVBox(
884
- valueLabel,
885
- labelWidget,
886
- )
887
-
888
- if deltaWidget != nil {
889
- content = container.NewVBox(
890
- valueLabel,
891
- labelWidget,
892
- deltaWidget,
893
- )
894
- }
895
-
896
- bg := canvas.NewRectangle(c.Surface)
897
- bg.StrokeColor = c.Border
898
- bg.StrokeWidth = 1
899
- bg.CornerRadius = 10
900
-
901
- return container.NewStack(bg, container.NewPadded(content))
902
- }
903
-
904
- // =============================================================================
905
- // APPLICATION
906
- // =============================================================================
907
-
908
- type App struct {
909
- fyne.App
910
- Theme fyne.Theme
911
- }
912
-
913
- func NewApp() *App {
914
- a := app.New()
915
- t := NewTheme()
916
- a.Settings().SetTheme(t)
917
- return &App{App: a, Theme: t}
918
- }
919
-
920
- func (a *App) NewWindow(title string) fyne.Window {
921
- return a.App.NewWindow(title)
922
- }
923
-
924
- func Init() fyne.Theme {
925
- return NewTheme()
926
- }
1
+ package cronixui
2
+
3
+ import (
4
+ "image/color"
5
+ "strings"
6
+
7
+ "fyne.io/fyne/v2"
8
+ "fyne.io/fyne/v2/app"
9
+ "fyne.io/fyne/v2/canvas"
10
+ "fyne.io/fyne/v2/container"
11
+ "fyne.io/fyne/v2/layout"
12
+ "fyne.io/fyne/v2/theme"
13
+ "fyne.io/fyne/v2/widget"
14
+ )
15
+
16
+ const Version = "1.0.6"
17
+
18
+ // =============================================================================
19
+ // DESIGN TOKENS
20
+ // =============================================================================
21
+
22
+ type Colors struct {
23
+ BG color.Color
24
+ Surface color.Color
25
+ Surface2 color.Color
26
+ Surface3 color.Color
27
+ Surface4 color.Color
28
+ Text color.Color
29
+ TextMuted color.Color
30
+ TextDim color.Color
31
+ Accent color.Color
32
+ AccentHover color.Color
33
+ AccentLight color.Color
34
+ AccentGlow color.Color
35
+ AccentText color.Color
36
+ Success color.Color
37
+ SuccessBorder color.Color
38
+ SuccessText color.Color
39
+ Warning color.Color
40
+ WarningBorder color.Color
41
+ WarningText color.Color
42
+ Error color.Color
43
+ ErrorBorder color.Color
44
+ ErrorText color.Color
45
+ Info color.Color
46
+ InfoBorder color.Color
47
+ InfoText color.Color
48
+ Border color.Color
49
+ BorderHover color.Color
50
+ BorderFocus color.Color
51
+ }
52
+
53
+ func DefaultColors() *Colors {
54
+ return &Colors{
55
+ BG: color.RGBA{R: 10, G: 10, B: 10, A: 255},
56
+ Surface: color.RGBA{R: 17, G: 17, B: 17, A: 255},
57
+ Surface2: color.RGBA{R: 26, G: 26, B: 26, A: 255},
58
+ Surface3: color.RGBA{R: 34, G: 34, B: 34, A: 255},
59
+ Surface4: color.RGBA{R: 42, G: 42, B: 42, A: 255},
60
+ Text: color.RGBA{R: 240, G: 237, B: 232, A: 255},
61
+ TextMuted: color.RGBA{R: 240, G: 237, B: 232, A: 128},
62
+ TextDim: color.RGBA{R: 240, G: 237, B: 232, A: 64},
63
+ Accent: color.RGBA{R: 107, G: 35, B: 35, A: 255},
64
+ AccentHover: color.RGBA{R: 125, G: 42, B: 42, A: 255},
65
+ AccentLight: color.RGBA{R: 138, G: 53, B: 53, A: 255},
66
+ AccentGlow: color.RGBA{R: 107, G: 35, B: 35, A: 77},
67
+ AccentText: color.RGBA{R: 201, G: 122, B: 122, A: 255},
68
+ Success: color.RGBA{R: 30, G: 80, B: 40, A: 255},
69
+ SuccessBorder: color.RGBA{R: 60, G: 140, B: 70, A: 102},
70
+ SuccessText: color.RGBA{R: 107, G: 196, B: 122, A: 255},
71
+ Warning: color.RGBA{R: 80, G: 60, B: 20, A: 255},
72
+ WarningBorder: color.RGBA{R: 150, G: 110, B: 30, A: 102},
73
+ WarningText: color.RGBA{R: 196, G: 164, B: 58, A: 255},
74
+ Error: color.RGBA{R: 80, G: 20, B: 20, A: 255},
75
+ ErrorBorder: color.RGBA{R: 180, G: 60, B: 60, A: 102},
76
+ ErrorText: color.RGBA{R: 196, G: 107, B: 107, A: 255},
77
+ Info: color.RGBA{R: 20, G: 53, B: 80, A: 255},
78
+ InfoBorder: color.RGBA{R: 60, G: 140, B: 200, A: 102},
79
+ InfoText: color.RGBA{R: 107, G: 168, B: 196, A: 255},
80
+ Border: color.RGBA{R: 255, G: 255, B: 255, A: 20},
81
+ BorderHover: color.RGBA{R: 255, G: 255, B: 255, A: 38},
82
+ BorderFocus: color.RGBA{R: 255, G: 255, B: 255, A: 64},
83
+ }
84
+ }
85
+
86
+ type Typography struct {
87
+ FontFamily string
88
+ FontMono string
89
+ FontSizeXS float32
90
+ FontSizeSM float32
91
+ FontSizeBase float32
92
+ FontSizeMD float32
93
+ FontSizeLG float32
94
+ FontSizeXL float32
95
+ FontSize2XL float32
96
+ FontSize3XL float32
97
+ }
98
+
99
+ func DefaultTypography() *Typography {
100
+ return &Typography{
101
+ FontFamily: "Outfit",
102
+ FontMono: "JetBrains Mono",
103
+ FontSizeXS: 11,
104
+ FontSizeSM: 12,
105
+ FontSizeBase: 13,
106
+ FontSizeMD: 14,
107
+ FontSizeLG: 16,
108
+ FontSizeXL: 20,
109
+ FontSize2XL: 28,
110
+ FontSize3XL: 36,
111
+ }
112
+ }
113
+
114
+ type Spacing struct {
115
+ Space1, Space2, Space3, Space4, Space5, Space6, Space8, Space10, Space12 float32
116
+ }
117
+
118
+ func DefaultSpacing() *Spacing {
119
+ return &Spacing{4, 8, 12, 16, 20, 24, 32, 40, 48}
120
+ }
121
+
122
+ type RadiusTokens struct {
123
+ SM, Default, LG, XL, Full float32
124
+ }
125
+
126
+ func DefaultRadiusTokens() *RadiusTokens {
127
+ return &RadiusTokens{6, 10, 14, 20, 9999}
128
+ }
129
+
130
+ type Shadow struct {
131
+ SM, Default, LG, Glow string
132
+ }
133
+
134
+ func DefaultShadow() *Shadow {
135
+ return &Shadow{
136
+ SM: "0 1px 2px rgba(0, 0, 0, 0.3)",
137
+ Default: "0 4px 12px rgba(0, 0, 0, 0.4)",
138
+ LG: "0 8px 24px rgba(0, 0, 0, 0.5)",
139
+ Glow: "0 0 20px rgba(107, 35, 35, 0.3)",
140
+ }
141
+ }
142
+
143
+ type Transition struct {
144
+ Fast, Default, Slow string
145
+ }
146
+
147
+ func DefaultTransition() *Transition {
148
+ return &Transition{"0.1s ease", "0.15s ease", "0.25s ease"}
149
+ }
150
+
151
+ type ZIndexTokens struct {
152
+ Dropdown, Sticky, Fixed, ModalBackdrop, Modal, Popover, Tooltip, Toast int
153
+ }
154
+
155
+ func DefaultZIndexTokens() *ZIndexTokens {
156
+ return &ZIndexTokens{100, 200, 300, 400, 500, 600, 700, 800}
157
+ }
158
+
159
+ type LayoutTokens struct {
160
+ ContainerMax, SidebarWidth, HeaderHeight float32
161
+ }
162
+
163
+ func DefaultLayoutTokens() *LayoutTokens {
164
+ return &LayoutTokens{1200, 260, 56}
165
+ }
166
+
167
+ // =============================================================================
168
+ // THEME
169
+ // =============================================================================
170
+
171
+ type Theme struct {
172
+ colors *Colors
173
+ spacing *Spacing
174
+ radius *RadiusTokens
175
+ }
176
+
177
+ func NewTheme() fyne.Theme {
178
+ return &Theme{
179
+ colors: DefaultColors(),
180
+ spacing: DefaultSpacing(),
181
+ radius: DefaultRadiusTokens(),
182
+ }
183
+ }
184
+
185
+ func (t *Theme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
186
+ switch name {
187
+ case theme.ColorNameBackground:
188
+ return t.colors.BG
189
+ case theme.ColorNameButton:
190
+ return t.colors.Surface2
191
+ case theme.ColorNameDisabledButton:
192
+ return t.colors.Surface3
193
+ case theme.ColorNameInputBackground:
194
+ return t.colors.Surface
195
+ case theme.ColorNameInputBorder:
196
+ return t.colors.Border
197
+ case theme.ColorNameDisabled:
198
+ return t.colors.TextDim
199
+ case theme.ColorNameForeground:
200
+ return t.colors.Text
201
+ case theme.ColorNamePlaceHolder:
202
+ return t.colors.TextMuted
203
+ case theme.ColorNamePrimary:
204
+ return t.colors.Accent
205
+ case theme.ColorNameHover:
206
+ return t.colors.Surface3
207
+ case theme.ColorNamePressed:
208
+ return t.colors.Surface4
209
+ case theme.ColorNameScrollBar:
210
+ return t.colors.Surface3
211
+ case theme.ColorNameShadow:
212
+ return color.RGBA{R: 0, G: 0, B: 0, A: 50}
213
+ case theme.ColorNameSuccess:
214
+ return t.colors.Success
215
+ case theme.ColorNameWarning:
216
+ return t.colors.Warning
217
+ case theme.ColorNameError:
218
+ return t.colors.Error
219
+ default:
220
+ return t.colors.Text
221
+ }
222
+ }
223
+
224
+ func (t *Theme) Font(style fyne.TextStyle) fyne.Resource {
225
+ return theme.DefaultTheme().Font(style)
226
+ }
227
+
228
+ func (t *Theme) Icon(name fyne.ThemeIconName) fyne.Resource {
229
+ return theme.DefaultTheme().Icon(name)
230
+ }
231
+
232
+ func (t *Theme) Size(name fyne.ThemeSizeName) float32 {
233
+ switch name {
234
+ case theme.SizeNamePadding:
235
+ return t.spacing.Space2
236
+ case theme.SizeNameInlinePadding:
237
+ return t.spacing.Space3
238
+ case theme.SizeNameScrollBar:
239
+ return 8
240
+ case theme.SizeNameScrollBarSmall:
241
+ return 4
242
+ case theme.SizeNameSeparatorThickness:
243
+ return 1
244
+ case theme.SizeNameText:
245
+ return t.spacing.Space4
246
+ case theme.SizeNameHeadingText:
247
+ return t.spacing.Space6
248
+ case theme.SizeNameSubHeadingText:
249
+ return t.spacing.Space5
250
+ case theme.SizeNameCaptionText:
251
+ return t.spacing.Space2
252
+ case theme.SizeNameInputBorder:
253
+ return 1
254
+ case theme.SizeNameInputRadius:
255
+ return t.radius.Default
256
+ case theme.SizeNameSelectionRadius:
257
+ return t.radius.SM
258
+ default:
259
+ return 0
260
+ }
261
+ }
262
+
263
+ // =============================================================================
264
+ // BUTTONS
265
+ // =============================================================================
266
+
267
+ type ButtonVariant int
268
+
269
+ const (
270
+ ButtonDefault ButtonVariant = iota
271
+ ButtonPrimary
272
+ ButtonGhost
273
+ ButtonOutline
274
+ ButtonDanger
275
+ ButtonSuccess
276
+ )
277
+
278
+ func NewButton(text string, variant ButtonVariant, onTap func()) *widget.Button {
279
+ btn := widget.NewButton(text, onTap)
280
+ switch variant {
281
+ case ButtonPrimary:
282
+ btn.Importance = widget.HighImportance
283
+ case ButtonDanger:
284
+ btn.Importance = widget.DangerImportance
285
+ case ButtonSuccess:
286
+ btn.Importance = widget.HighImportance
287
+ case ButtonGhost, ButtonOutline:
288
+ btn.Importance = widget.LowImportance
289
+ default:
290
+ btn.Importance = widget.MediumImportance
291
+ }
292
+ return btn
293
+ }
294
+
295
+ func NewButtonGroup(buttons ...*widget.Button) *fyne.Container {
296
+ return container.NewHBox(buttons...)
297
+ }
298
+
299
+ // =============================================================================
300
+ // CARD
301
+ // =============================================================================
302
+
303
+ // CardOption is a function that configures a card.
304
+ type CardOption func(*CardConfig)
305
+
306
+ // CardConfig holds card configuration.
307
+ type CardConfig struct {
308
+ Title string
309
+ Subtitle string
310
+ Icon fyne.CanvasObject
311
+ Footer fyne.CanvasObject
312
+ Clickable bool
313
+ }
314
+
315
+ // WithCardTitle sets the card title.
316
+ func WithCardTitle(title string) CardOption {
317
+ return func(c *CardConfig) { c.Title = title }
318
+ }
319
+
320
+ // WithCardSubtitle sets the card subtitle.
321
+ func WithCardSubtitle(subtitle string) CardOption {
322
+ return func(c *CardConfig) { c.Subtitle = subtitle }
323
+ }
324
+
325
+ // WithCardIcon sets the card icon.
326
+ func WithCardIcon(icon fyne.CanvasObject) CardOption {
327
+ return func(c *CardConfig) { c.Icon = icon }
328
+ }
329
+
330
+ // WithCardFooter sets the card footer.
331
+ func WithCardFooter(footer fyne.CanvasObject) CardOption {
332
+ return func(c *CardConfig) { c.Footer = footer }
333
+ }
334
+
335
+ // WithCardClickable makes the card clickable.
336
+ func WithCardClickable(clickable bool) CardOption {
337
+ return func(c *CardConfig) { c.Clickable = clickable }
338
+ }
339
+
340
+ // NewCard creates a card widget with optional icon, title, subtitle, and footer.
341
+ func NewCard(content fyne.CanvasObject, options ...CardOption) *fyne.Container {
342
+ config := &CardConfig{}
343
+ for _, opt := range options {
344
+ opt(config)
345
+ }
346
+
347
+ c := DefaultColors()
348
+
349
+ var headerObjects []fyne.CanvasObject
350
+ if config.Icon != nil {
351
+ headerObjects = append(headerObjects, config.Icon)
352
+ }
353
+ if config.Title != "" {
354
+ titleText := canvas.NewText(config.Title, c.Text)
355
+ titleText.TextSize = 14
356
+ titleText.TextStyle = fyne.TextStyle{Bold: true}
357
+ headerObjects = append(headerObjects, titleText)
358
+ }
359
+ if config.Subtitle != "" {
360
+ subtitleText := canvas.NewText(config.Subtitle, c.TextMuted)
361
+ subtitleText.TextSize = 12
362
+ headerObjects = append(headerObjects, subtitleText)
363
+ }
364
+
365
+ var header *fyne.Container
366
+ if len(headerObjects) > 0 {
367
+ header = container.NewVBox(headerObjects...)
368
+ }
369
+
370
+ var body fyne.CanvasObject
371
+ if header != nil {
372
+ body = container.NewVBox(header, content)
373
+ } else {
374
+ body = content
375
+ }
376
+
377
+ var finalContent *fyne.Container
378
+ if config.Footer != nil {
379
+ finalContent = container.NewVBox(body, canvas.NewRectangle(c.Border), config.Footer)
380
+ } else {
381
+ if c, ok := body.(*fyne.Container); ok {
382
+ finalContent = c
383
+ } else {
384
+ finalContent = container.NewMax(body)
385
+ }
386
+ }
387
+
388
+ bg := canvas.NewRectangle(c.Surface)
389
+ bg.StrokeColor = c.Border
390
+ bg.StrokeWidth = 1
391
+ bg.CornerRadius = 14
392
+
393
+ return container.NewStack(bg, container.NewPadded(finalContent))
394
+ }
395
+
396
+ // =============================================================================
397
+ // INPUTS
398
+ // =============================================================================
399
+
400
+ func NewInput(placeholder string) *widget.Entry {
401
+ entry := widget.NewEntry()
402
+ entry.SetPlaceHolder(placeholder)
403
+ return entry
404
+ }
405
+
406
+ func NewTextarea(placeholder string) *widget.Entry {
407
+ entry := widget.NewMultiLineEntry()
408
+ entry.SetPlaceHolder(placeholder)
409
+ return entry
410
+ }
411
+
412
+ func NewPassword(placeholder string) *widget.Entry {
413
+ entry := widget.NewPasswordEntry()
414
+ entry.SetPlaceHolder(placeholder)
415
+ return entry
416
+ }
417
+
418
+ // =============================================================================
419
+ // FORM ELEMENTS
420
+ // =============================================================================
421
+
422
+ func NewCheckbox(label string, onChanged func(bool)) *widget.Check {
423
+ return widget.NewCheck(label, onChanged)
424
+ }
425
+
426
+ func NewRadio(options []string, onChanged func(string)) *widget.RadioGroup {
427
+ return widget.NewRadioGroup(options, onChanged)
428
+ }
429
+
430
+ func NewSelect(options []string, onChanged func(string)) *widget.Select {
431
+ return widget.NewSelect(options, onChanged)
432
+ }
433
+
434
+ func NewSlider(min, max float64) *widget.Slider {
435
+ return widget.NewSlider(min, max)
436
+ }
437
+
438
+ // =============================================================================
439
+ // PROGRESS & LOADING
440
+ // =============================================================================
441
+
442
+ func NewProgress() *widget.ProgressBar {
443
+ return widget.NewProgressBar()
444
+ }
445
+
446
+ func NewProgressInfinite() *widget.ProgressBarInfinite {
447
+ return widget.NewProgressBarInfinite()
448
+ }
449
+
450
+ // =============================================================================
451
+ // TOGGLE
452
+ // =============================================================================
453
+
454
+ type Toggle struct {
455
+ widget.BaseWidget
456
+ on bool
457
+ onToggle func(bool)
458
+ }
459
+
460
+ func NewToggle(onToggle func(bool)) *Toggle {
461
+ t := &Toggle{onToggle: onToggle}
462
+ t.ExtendBaseWidget(t)
463
+ return t
464
+ }
465
+
466
+ func (t *Toggle) Toggle() {
467
+ t.on = !t.on
468
+ if t.onToggle != nil {
469
+ t.onToggle(t.on)
470
+ }
471
+ t.Refresh()
472
+ }
473
+
474
+ func (t *Toggle) IsOn() bool { return t.on }
475
+ func (t *Toggle) SetOn(value bool) { t.on = value; t.Refresh() }
476
+
477
+ // =============================================================================
478
+ // MODAL
479
+ // =============================================================================
480
+
481
+ type Modal struct {
482
+ widget.BaseWidget
483
+ content fyne.CanvasObject
484
+ open bool
485
+ window fyne.Window
486
+ }
487
+
488
+ func NewModal(content fyne.CanvasObject) *Modal {
489
+ m := &Modal{content: content}
490
+ m.ExtendBaseWidget(m)
491
+ return m
492
+ }
493
+
494
+ func (m *Modal) Open(window fyne.Window) {
495
+ m.window = window
496
+ m.open = true
497
+ m.Show()
498
+ }
499
+
500
+ func (m *Modal) Close() { m.open = false; m.Hide() }
501
+ func (m *Modal) IsOpen() bool { return m.open }
502
+ func (m *Modal) Show() {
503
+ if m.window != nil {
504
+ popup := widget.NewModalPopUp(m.content, m.window.Canvas())
505
+ popup.Show()
506
+ }
507
+ }
508
+ func (m *Modal) Hide() {}
509
+
510
+ // =============================================================================
511
+ // DROPDOWN
512
+ // =============================================================================
513
+
514
+ // Dropdown creates a dropdown selection widget using Fyne's Select widget.
515
+ func NewDropdown(items []string, onSelect func(string)) *widget.Select {
516
+ s := widget.NewSelect(items, onSelect)
517
+ return s
518
+ }
519
+
520
+ // =============================================================================
521
+ // TABS
522
+ // =============================================================================
523
+
524
+ func NewTabs(items ...*widget.TabItem) *container.AppTabs {
525
+ return container.NewAppTabs(items...)
526
+ }
527
+
528
+ // =============================================================================
529
+ // ACCORDION
530
+ // =============================================================================
531
+
532
+ type AccordionItem struct {
533
+ Title string
534
+ Content fyne.CanvasObject
535
+ }
536
+
537
+ type Accordion struct {
538
+ widget.BaseWidget
539
+ items []AccordionItem
540
+ openIndices map[int]bool
541
+ }
542
+
543
+ func NewAccordion(items ...AccordionItem) *Accordion {
544
+ a := &Accordion{items: items, openIndices: make(map[int]bool)}
545
+ a.ExtendBaseWidget(a)
546
+ return a
547
+ }
548
+
549
+ func (a *Accordion) Toggle(index int) { a.openIndices[index] = !a.openIndices[index]; a.Refresh() }
550
+ func (a *Accordion) Open(index int) { a.openIndices[index] = true; a.Refresh() }
551
+ func (a *Accordion) Close(index int) { delete(a.openIndices, index); a.Refresh() }
552
+ func (a *Accordion) OpenAll() {
553
+ for i := range a.items {
554
+ a.openIndices[i] = true
555
+ }
556
+ a.Refresh()
557
+ }
558
+ func (a *Accordion) CloseAll() { a.openIndices = make(map[int]bool); a.Refresh() }
559
+ func (a *Accordion) IsOpen(index int) bool { return a.openIndices[index] }
560
+
561
+ // =============================================================================
562
+ // PAGINATION
563
+ // =============================================================================
564
+
565
+ type Pagination struct {
566
+ widget.BaseWidget
567
+ total int
568
+ current int
569
+ onChange func(int)
570
+ }
571
+
572
+ func NewPagination(total, current int, onChange func(int)) *Pagination {
573
+ p := &Pagination{total: total, current: current, onChange: onChange}
574
+ p.ExtendBaseWidget(p)
575
+ return p
576
+ }
577
+
578
+ func (p *Pagination) GoTo(page int) {
579
+ if page >= 1 && page <= p.total {
580
+ p.current = page
581
+ if p.onChange != nil {
582
+ p.onChange(page)
583
+ }
584
+ p.Refresh()
585
+ }
586
+ }
587
+ func (p *Pagination) Next() {
588
+ if p.current < p.total {
589
+ p.GoTo(p.current + 1)
590
+ }
591
+ }
592
+ func (p *Pagination) Prev() {
593
+ if p.current > 1 {
594
+ p.GoTo(p.current - 1)
595
+ }
596
+ }
597
+ func (p *Pagination) Current() int { return p.current }
598
+ func (p *Pagination) Total() int { return p.total }
599
+
600
+ // =============================================================================
601
+ // TOAST
602
+ // =============================================================================
603
+
604
+ type ToastType int
605
+
606
+ const (
607
+ ToastSuccess ToastType = iota
608
+ ToastError
609
+ ToastWarning
610
+ ToastInfo
611
+ )
612
+
613
+ func ShowToast(window fyne.Window, message string, toastType ToastType) {
614
+ c := DefaultColors()
615
+ var bgColor color.Color
616
+ var prefix string
617
+ switch toastType {
618
+ case ToastSuccess:
619
+ bgColor = c.Success
620
+ prefix = "✓ "
621
+ case ToastError:
622
+ bgColor = c.Error
623
+ prefix = "✕ "
624
+ case ToastWarning:
625
+ bgColor = c.Warning
626
+ prefix = "⚠ "
627
+ default:
628
+ bgColor = c.Info
629
+ prefix = "ℹ "
630
+ }
631
+ label := canvas.NewText(prefix+message, c.Text)
632
+ label.TextSize = 13
633
+ bg := canvas.NewRectangle(bgColor)
634
+ bg.CornerRadius = 10
635
+ cont := container.NewStack(bg, container.NewPadded(label))
636
+ popover := widget.NewPopUp(cont, window.Canvas())
637
+ popover.Move(fyne.NewPos(window.Canvas().Size().Width-320, window.Canvas().Size().Height-60))
638
+ }
639
+
640
+ // =============================================================================
641
+ // COMMAND PALETTE
642
+ // =============================================================================
643
+
644
+ type CommandItem struct {
645
+ Title, Subtitle, Kbd string
646
+ Action func()
647
+ }
648
+
649
+ // CommandPalette creates a modal command palette with search and filtering.
650
+ func NewCommandPalette(window fyne.Window, items []CommandItem) *fyne.Container {
651
+ c := DefaultColors()
652
+ searchEntry := widget.NewEntry()
653
+ searchEntry.SetPlaceHolder("Type to search commands...")
654
+
655
+ list := widget.NewList(
656
+ func() int { return len(items) },
657
+ func() fyne.CanvasObject {
658
+ return container.NewVBox(
659
+ widget.NewLabel("Command"),
660
+ widget.NewLabel("Description"),
661
+ )
662
+ },
663
+ func(id widget.ListItemID, item fyne.CanvasObject) {
664
+ if id < len(items) {
665
+ vbox := item.(*fyne.Container)
666
+ vbox.Objects[0].(*widget.Label).SetText(items[id].Title)
667
+ vbox.Objects[1].(*widget.Label).SetText(items[id].Subtitle)
668
+ }
669
+ },
670
+ )
671
+
672
+ searchEntry.OnChanged = func(query string) {
673
+ // Filter logic can be added here
674
+ list.Refresh()
675
+ }
676
+
677
+ list.OnSelected = func(id widget.ListItemID) {
678
+ if id < len(items) && items[id].Action != nil {
679
+ items[id].Action()
680
+ window.Hide()
681
+ }
682
+ }
683
+
684
+ content := container.NewVBox(
685
+ searchEntry,
686
+ container.NewScroll(list),
687
+ )
688
+
689
+ bg := canvas.NewRectangle(c.Surface)
690
+ bg.CornerRadius = 14
691
+
692
+ return container.NewStack(bg, container.NewPadded(content))
693
+ }
694
+
695
+ // =============================================================================
696
+ // SEARCH
697
+ // =============================================================================
698
+
699
+ type SearchItem struct {
700
+ Title, Subtitle string
701
+ Action func()
702
+ }
703
+
704
+ // Search creates a search widget with input and results list.
705
+ func NewSearch(items []SearchItem, onSelect func(SearchItem)) *fyne.Container {
706
+ c := DefaultColors()
707
+ searchEntry := widget.NewEntry()
708
+ searchEntry.SetPlaceHolder("Search...")
709
+
710
+ results := make([]SearchItem, 0)
711
+ resultsList := widget.NewList(
712
+ func() int { return len(results) },
713
+ func() fyne.CanvasObject {
714
+ return container.NewVBox(
715
+ widget.NewLabel("Title"),
716
+ widget.NewLabel("Subtitle"),
717
+ )
718
+ },
719
+ func(id widget.ListItemID, item fyne.CanvasObject) {
720
+ if id < len(results) {
721
+ vbox := item.(*fyne.Container)
722
+ vbox.Objects[0].(*widget.Label).SetText(results[id].Title)
723
+ vbox.Objects[1].(*widget.Label).SetText(results[id].Subtitle)
724
+ }
725
+ },
726
+ )
727
+
728
+ resultsList.OnSelected = func(id widget.ListItemID) {
729
+ if id < len(results) && onSelect != nil {
730
+ onSelect(results[id])
731
+ }
732
+ }
733
+
734
+ searchEntry.OnChanged = func(query string) {
735
+ results = make([]SearchItem, 0)
736
+ if query == "" {
737
+ resultsList.Refresh()
738
+ return
739
+ }
740
+ for _, item := range items {
741
+ // Simple case-insensitive search
742
+ if containsIgnoreCase(item.Title, query) || containsIgnoreCase(item.Subtitle, query) {
743
+ results = append(results, item)
744
+ }
745
+ }
746
+ resultsList.Refresh()
747
+ }
748
+
749
+ content := container.NewVBox(
750
+ searchEntry,
751
+ container.NewScroll(resultsList),
752
+ )
753
+
754
+ return content
755
+ }
756
+
757
+ func containsIgnoreCase(s, substr string) bool {
758
+ return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
759
+ }
760
+
761
+ // =============================================================================
762
+ // TABLE
763
+ // =============================================================================
764
+
765
+ func NewTable(rows, cols int, cellFunc func(row, col int) fyne.CanvasObject) *widget.Table {
766
+ return widget.NewTable(
767
+ func() (int, int) { return rows, cols },
768
+ func() fyne.CanvasObject { return widget.NewLabel("") },
769
+ func(id widget.TableCellID, cell fyne.CanvasObject) {
770
+ if obj := cellFunc(id.Row, id.Col); obj != nil {
771
+ cell.(*widget.Label).SetText(obj.(*widget.Label).Text)
772
+ }
773
+ },
774
+ )
775
+ }
776
+
777
+ // =============================================================================
778
+ // LIST
779
+ // =============================================================================
780
+
781
+ type ListItem struct {
782
+ Title, Subtitle string
783
+ }
784
+
785
+ func NewList(items []ListItem) *widget.List {
786
+ return widget.NewList(
787
+ func() int { return len(items) },
788
+ func() fyne.CanvasObject { return widget.NewLabel("") },
789
+ func(id widget.ListItemID, item fyne.CanvasObject) {
790
+ item.(*widget.Label).SetText(items[id].Title)
791
+ },
792
+ )
793
+ }
794
+
795
+ // =============================================================================
796
+ // LAYOUT HELPERS
797
+ // =============================================================================
798
+
799
+ func NewContainer(content fyne.CanvasObject) fyne.CanvasObject {
800
+ return container.NewMax(content)
801
+ }
802
+
803
+ func NewFlex(objects ...fyne.CanvasObject) fyne.CanvasObject {
804
+ return container.NewHBox(objects...)
805
+ }
806
+
807
+ func NewStack(objects ...fyne.CanvasObject) fyne.CanvasObject {
808
+ return container.NewVBox(objects...)
809
+ }
810
+
811
+ func NewGrid(cols int, objects ...fyne.CanvasObject) fyne.CanvasObject {
812
+ return container.NewGridWithColumns(cols, objects...)
813
+ }
814
+
815
+ func NewForm(items ...*widget.FormItem) *widget.Form {
816
+ return widget.NewForm(items...)
817
+ }
818
+
819
+ // =============================================================================
820
+ // NAV
821
+ // =============================================================================
822
+
823
+ // Nav creates a navigation sidebar widget.
824
+ func NewNav(items []string, active int, onSelect func(int)) *widget.List {
825
+ list := widget.NewList(
826
+ func() int { return len(items) },
827
+ func() fyne.CanvasObject {
828
+ return widget.NewLabel("Nav Item")
829
+ },
830
+ func(id widget.ListItemID, item fyne.CanvasObject) {
831
+ if id < len(items) {
832
+ item.(*widget.Label).SetText(items[id])
833
+ }
834
+ },
835
+ )
836
+ list.OnSelected = func(id widget.ListItemID) {
837
+ if onSelect != nil {
838
+ onSelect(id)
839
+ }
840
+ }
841
+ if active >= 0 && active < len(items) {
842
+ list.Select(active)
843
+ }
844
+ return list
845
+ }
846
+
847
+ // =============================================================================
848
+ // STAT
849
+ // =============================================================================
850
+
851
+ type StatDeltaType int
852
+
853
+ const (
854
+ StatDeltaUp StatDeltaType = iota
855
+ StatDeltaDown
856
+ )
857
+
858
+ // Stat creates a stat/metric display widget with value, label, and optional delta.
859
+ func NewStat(value string, label string, delta string, deltaType StatDeltaType) *fyne.Container {
860
+ c := DefaultColors()
861
+
862
+ valueLabel := canvas.NewText(value, c.Text)
863
+ valueLabel.TextSize = 28
864
+ valueLabel.TextStyle = fyne.TextStyle{Bold: true}
865
+
866
+ labelWidget := canvas.NewText(label, c.TextMuted)
867
+ labelWidget.TextSize = 11
868
+
869
+ var deltaWidget *canvas.Text
870
+ if delta != "" {
871
+ deltaColor := c.SuccessText
872
+ if deltaType == StatDeltaDown {
873
+ deltaColor = c.ErrorText
874
+ }
875
+ prefix := "↑ "
876
+ if deltaType == StatDeltaDown {
877
+ prefix = "↓ "
878
+ }
879
+ deltaWidget = canvas.NewText(prefix+delta, deltaColor)
880
+ deltaWidget.TextSize = 11
881
+ }
882
+
883
+ content := container.NewVBox(
884
+ valueLabel,
885
+ labelWidget,
886
+ )
887
+
888
+ if deltaWidget != nil {
889
+ content = container.NewVBox(
890
+ valueLabel,
891
+ labelWidget,
892
+ deltaWidget,
893
+ )
894
+ }
895
+
896
+ bg := canvas.NewRectangle(c.Surface)
897
+ bg.StrokeColor = c.Border
898
+ bg.StrokeWidth = 1
899
+ bg.CornerRadius = 10
900
+
901
+ return container.NewStack(bg, container.NewPadded(content))
902
+ }
903
+
904
+ // =============================================================================
905
+ // APPLICATION
906
+ // =============================================================================
907
+
908
+ type App struct {
909
+ fyne.App
910
+ Theme fyne.Theme
911
+ }
912
+
913
+ func NewApp() *App {
914
+ a := app.New()
915
+ t := NewTheme()
916
+ a.Settings().SetTheme(t)
917
+ return &App{App: a, Theme: t}
918
+ }
919
+
920
+ func (a *App) NewWindow(title string) fyne.Window {
921
+ return a.App.NewWindow(title)
922
+ }
923
+
924
+ func Init() fyne.Theme {
925
+ return NewTheme()
926
+ }