keystone_ui 0.6.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 (131) hide show
  1. checksums.yaml +7 -0
  2. data/app/assets/javascripts/keystone_ui/accordion_controller.js +21 -0
  3. data/app/assets/javascripts/keystone_ui/auto_submit_controller.js +7 -0
  4. data/app/assets/javascripts/keystone_ui/chartjs.js +24 -0
  5. data/app/assets/javascripts/keystone_ui/clipboard_controller.js +25 -0
  6. data/app/assets/javascripts/keystone_ui/color_picker_controller.js +158 -0
  7. data/app/assets/javascripts/keystone_ui/column_picker_controller.js +47 -0
  8. data/app/assets/javascripts/keystone_ui/dismiss_controller.js +7 -0
  9. data/app/assets/javascripts/keystone_ui/dropdown_controller.js +16 -0
  10. data/app/assets/javascripts/keystone_ui/file_upload_controller.js +61 -0
  11. data/app/assets/javascripts/keystone_ui/index.js +33 -0
  12. data/app/assets/javascripts/keystone_ui/line_chart_controller.js +38 -0
  13. data/app/assets/javascripts/keystone_ui/modal_controller.js +19 -0
  14. data/app/assets/javascripts/keystone_ui/multi_select_controller.js +35 -0
  15. data/app/assets/javascripts/keystone_ui/stat_card_info_controller.js +9 -0
  16. data/app/assets/javascripts/keystone_ui/swipe_deck_controller.js +123 -0
  17. data/app/assets/javascripts/keystone_ui/tab_switcher_controller.js +19 -0
  18. data/app/assets/tailwind/keystone_ui_engine/color_picker.css +31 -0
  19. data/app/assets/tailwind/keystone_ui_engine/engine.css +7 -0
  20. data/app/assets/tailwind/keystone_ui_engine/nav.css +175 -0
  21. data/app/assets/tailwind/keystone_ui_engine/theme.css +32 -0
  22. data/app/components/keystone/ui/accordion_component.html.erb +19 -0
  23. data/app/components/keystone/ui/accordion_component.rb +51 -0
  24. data/app/components/keystone/ui/alert_component.html.erb +15 -0
  25. data/app/components/keystone/ui/alert_component.rb +58 -0
  26. data/app/components/keystone/ui/badge_component.html.erb +1 -0
  27. data/app/components/keystone/ui/badge_component.rb +28 -0
  28. data/app/components/keystone/ui/bottom_nav_component.html.erb +3 -0
  29. data/app/components/keystone/ui/bottom_nav_component.rb +13 -0
  30. data/app/components/keystone/ui/bottom_nav_item_component.html.erb +4 -0
  31. data/app/components/keystone/ui/bottom_nav_item_component.rb +24 -0
  32. data/app/components/keystone/ui/button_component.html.erb +1 -0
  33. data/app/components/keystone/ui/button_component.rb +57 -0
  34. data/app/components/keystone/ui/card_component.html.erb +9 -0
  35. data/app/components/keystone/ui/card_component.rb +33 -0
  36. data/app/components/keystone/ui/card_link_component.html.erb +3 -0
  37. data/app/components/keystone/ui/card_link_component.rb +25 -0
  38. data/app/components/keystone/ui/chart_card_component.html.erb +6 -0
  39. data/app/components/keystone/ui/chart_card_component.rb +35 -0
  40. data/app/components/keystone/ui/code_component.html.erb +6 -0
  41. data/app/components/keystone/ui/code_component.rb +40 -0
  42. data/app/components/keystone/ui/color_picker_component.html.erb +37 -0
  43. data/app/components/keystone/ui/color_picker_component.rb +31 -0
  44. data/app/components/keystone/ui/column.rb +21 -0
  45. data/app/components/keystone/ui/column_picker_component.html.erb +20 -0
  46. data/app/components/keystone/ui/column_picker_component.rb +35 -0
  47. data/app/components/keystone/ui/copy_button_component.html.erb +11 -0
  48. data/app/components/keystone/ui/copy_button_component.rb +39 -0
  49. data/app/components/keystone/ui/cta_banner_component.html.erb +9 -0
  50. data/app/components/keystone/ui/cta_banner_component.rb +39 -0
  51. data/app/components/keystone/ui/data_table_component.html.erb +54 -0
  52. data/app/components/keystone/ui/data_table_component.rb +173 -0
  53. data/app/components/keystone/ui/disclosure_component.html.erb +9 -0
  54. data/app/components/keystone/ui/disclosure_component.rb +46 -0
  55. data/app/components/keystone/ui/feature_grid_component.html.erb +17 -0
  56. data/app/components/keystone/ui/feature_grid_component.rb +55 -0
  57. data/app/components/keystone/ui/file_upload_component.html.erb +18 -0
  58. data/app/components/keystone/ui/file_upload_component.rb +85 -0
  59. data/app/components/keystone/ui/form_component.html.erb +3 -0
  60. data/app/components/keystone/ui/form_component.rb +27 -0
  61. data/app/components/keystone/ui/form_field_component.html.erb +36 -0
  62. data/app/components/keystone/ui/form_field_component.rb +83 -0
  63. data/app/components/keystone/ui/form_page_component.html.erb +10 -0
  64. data/app/components/keystone/ui/form_page_component.rb +21 -0
  65. data/app/components/keystone/ui/funnel_component.html.erb +14 -0
  66. data/app/components/keystone/ui/funnel_component.rb +85 -0
  67. data/app/components/keystone/ui/grid_component.html.erb +3 -0
  68. data/app/components/keystone/ui/grid_component.rb +38 -0
  69. data/app/components/keystone/ui/hero_component.html.erb +27 -0
  70. data/app/components/keystone/ui/hero_component.rb +74 -0
  71. data/app/components/keystone/ui/input_component.html.erb +1 -0
  72. data/app/components/keystone/ui/input_component.rb +57 -0
  73. data/app/components/keystone/ui/line_chart_component.html.erb +3 -0
  74. data/app/components/keystone/ui/line_chart_component.rb +48 -0
  75. data/app/components/keystone/ui/mobile_actions_component.html.erb +11 -0
  76. data/app/components/keystone/ui/mobile_actions_component.rb +21 -0
  77. data/app/components/keystone/ui/mobile_header_component.html.erb +11 -0
  78. data/app/components/keystone/ui/mobile_header_component.rb +38 -0
  79. data/app/components/keystone/ui/modal_component.html.erb +13 -0
  80. data/app/components/keystone/ui/modal_component.rb +68 -0
  81. data/app/components/keystone/ui/multi_select_component.html.erb +21 -0
  82. data/app/components/keystone/ui/multi_select_component.rb +40 -0
  83. data/app/components/keystone/ui/nav_dropdown_component.html.erb +11 -0
  84. data/app/components/keystone/ui/nav_dropdown_component.rb +33 -0
  85. data/app/components/keystone/ui/nav_item_component.html.erb +1 -0
  86. data/app/components/keystone/ui/nav_item_component.rb +21 -0
  87. data/app/components/keystone/ui/navbar_component.html.erb +34 -0
  88. data/app/components/keystone/ui/navbar_component.rb +32 -0
  89. data/app/components/keystone/ui/option_card_component.html.erb +9 -0
  90. data/app/components/keystone/ui/option_card_component.rb +28 -0
  91. data/app/components/keystone/ui/page_component.html.erb +3 -0
  92. data/app/components/keystone/ui/page_component.rb +40 -0
  93. data/app/components/keystone/ui/page_header_component.html.erb +17 -0
  94. data/app/components/keystone/ui/page_header_component.rb +42 -0
  95. data/app/components/keystone/ui/panel_component.html.erb +3 -0
  96. data/app/components/keystone/ui/panel_component.rb +25 -0
  97. data/app/components/keystone/ui/pipeline_component.html.erb +34 -0
  98. data/app/components/keystone/ui/pipeline_component.rb +48 -0
  99. data/app/components/keystone/ui/progress_component.html.erb +8 -0
  100. data/app/components/keystone/ui/progress_component.rb +35 -0
  101. data/app/components/keystone/ui/radio_card_component.html.erb +9 -0
  102. data/app/components/keystone/ui/radio_card_component.rb +42 -0
  103. data/app/components/keystone/ui/section_component.html.erb +18 -0
  104. data/app/components/keystone/ui/section_component.rb +32 -0
  105. data/app/components/keystone/ui/select_component.html.erb +8 -0
  106. data/app/components/keystone/ui/select_component.rb +38 -0
  107. data/app/components/keystone/ui/settings_link_component.html.erb +4 -0
  108. data/app/components/keystone/ui/settings_link_component.rb +26 -0
  109. data/app/components/keystone/ui/show_page_component.html.erb +4 -0
  110. data/app/components/keystone/ui/show_page_component.rb +21 -0
  111. data/app/components/keystone/ui/stat_card_component.html.erb +22 -0
  112. data/app/components/keystone/ui/stat_card_component.rb +92 -0
  113. data/app/components/keystone/ui/swipe_deck_component.html.erb +39 -0
  114. data/app/components/keystone/ui/swipe_deck_component.rb +52 -0
  115. data/app/components/keystone/ui/tab_switcher_component.html.erb +14 -0
  116. data/app/components/keystone/ui/tab_switcher_component.rb +33 -0
  117. data/app/components/keystone/ui/textarea_component.html.erb +1 -0
  118. data/app/components/keystone/ui/textarea_component.rb +38 -0
  119. data/app/helpers/keystone_ui_helper.rb +195 -0
  120. data/config/importmap.rb +19 -0
  121. data/lib/generators/keystone/install_generator.rb +90 -0
  122. data/lib/keystone_ui/configuration.rb +24 -0
  123. data/lib/keystone_ui/engine.rb +47 -0
  124. data/lib/keystone_ui/safelist.rb +106 -0
  125. data/lib/keystone_ui/version.rb +5 -0
  126. data/lib/keystone_ui.rb +9 -0
  127. data/the_local/agents/keystone_ui-develop.md +297 -0
  128. data/the_local/agents/keystone_ui-info.md +81 -0
  129. data/the_local/agents/keystone_ui-install.md +137 -0
  130. data/the_local/interface.yml +110 -0
  131. metadata +203 -0
@@ -0,0 +1,175 @@
1
+ /* Bottom navigation bar */
2
+ .bottom-nav {
3
+ background: var(--base-bg-low);
4
+ border-top: 1px solid var(--base-border-tertiary);
5
+ bottom: 0;
6
+ display: flex;
7
+ justify-content: space-around;
8
+ left: 0;
9
+ padding-bottom: env(safe-area-inset-bottom);
10
+ position: fixed;
11
+ right: 0;
12
+ z-index: 40;
13
+
14
+ @media (min-width: 1024px) {
15
+ display: none;
16
+ }
17
+
18
+ &::after {
19
+ content: '';
20
+ position: absolute;
21
+ top: 100%;
22
+ left: 0;
23
+ right: 0;
24
+ height: 100px;
25
+ background: inherit;
26
+ }
27
+ }
28
+
29
+ .bottom-nav-item {
30
+ align-items: center;
31
+ color: var(--base-text-tertiary);
32
+ display: flex;
33
+ flex: 1;
34
+ flex-direction: column;
35
+ gap: 2px;
36
+ justify-content: center;
37
+ min-height: 56px;
38
+ padding: 8px 4px;
39
+ text-decoration: none;
40
+
41
+ &:hover,
42
+ &:active {
43
+ color: var(--base-text-secondary);
44
+ }
45
+
46
+ &.active {
47
+ color: var(--text-primary);
48
+ }
49
+
50
+ svg {
51
+ height: 24px;
52
+ width: 24px;
53
+ }
54
+ }
55
+
56
+ .bottom-nav-label {
57
+ font-size: 10px;
58
+ font-weight: 500;
59
+ line-height: 1;
60
+ }
61
+
62
+ /* Nav dropdown for desktop area navigation */
63
+ .nav-dropdown {
64
+ position: relative;
65
+ width: 100%;
66
+
67
+ @media (min-width: 1024px) {
68
+ width: auto;
69
+ }
70
+ }
71
+
72
+ .nav-dropdown-trigger {
73
+ align-items: center;
74
+ color: var(--base-text);
75
+ display: flex;
76
+ font-size: 14px;
77
+ gap: 4px;
78
+ justify-content: space-between;
79
+ padding: 12px 16px;
80
+ text-wrap: nowrap;
81
+ width: 100%;
82
+
83
+ @media (min-width: 1024px) {
84
+ padding: 8px 4px;
85
+ width: auto;
86
+ }
87
+
88
+ &:hover {
89
+ color: var(--base-text-secondary);
90
+ }
91
+
92
+ &.active {
93
+ color: var(--text-primary);
94
+ font-weight: 600;
95
+
96
+ @media (min-width: 1024px) {
97
+ box-shadow: inset 0 -4px 0 0 var(--border-primary);
98
+ }
99
+ }
100
+ }
101
+
102
+ .nav-dropdown-caret {
103
+ height: 16px;
104
+ width: 16px;
105
+ transition: transform 0.2s ease;
106
+ }
107
+
108
+ .nav-dropdown:has(.nav-dropdown-menu:not(.hidden)) .nav-dropdown-caret {
109
+ transform: rotate(180deg);
110
+ }
111
+
112
+ .nav-dropdown-menu {
113
+ background: var(--base-bg-base);
114
+ position: relative;
115
+ width: 100%;
116
+ z-index: 50;
117
+
118
+ @media (min-width: 1024px) {
119
+ background: var(--base-bg-low);
120
+ border: 1px solid var(--base-border-tertiary);
121
+ border-radius: 8px;
122
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
123
+ min-width: 200px;
124
+ position: absolute;
125
+ top: 100%;
126
+ left: 0;
127
+ margin-top: 4px;
128
+ }
129
+
130
+ ul {
131
+ list-style: none;
132
+ margin: 0;
133
+ padding: 0;
134
+ }
135
+
136
+ li a,
137
+ & > a {
138
+ color: var(--base-text);
139
+ display: block;
140
+ font-size: 14px;
141
+ padding: 12px 16px 12px 32px;
142
+ text-decoration: none;
143
+ white-space: nowrap;
144
+
145
+ @media (min-width: 1024px) {
146
+ padding: 8px 16px;
147
+ }
148
+
149
+ &:hover {
150
+ background: var(--base-bg-hover);
151
+ color: var(--base-text-secondary);
152
+ }
153
+
154
+ &.active {
155
+ background: var(--base-bg-hover);
156
+ color: var(--text-primary);
157
+ font-weight: 600;
158
+ }
159
+ }
160
+ }
161
+
162
+ /* Add padding to main content for bottom nav on mobile */
163
+ @media (max-width: 1023px) {
164
+ main {
165
+ padding-bottom: calc(56px + env(safe-area-inset-bottom));
166
+ }
167
+
168
+ .hotwire-native .bottom-nav {
169
+ display: none;
170
+ }
171
+
172
+ .hotwire-native main {
173
+ padding-bottom: 0;
174
+ }
175
+ }
@@ -0,0 +1,32 @@
1
+ /* Keystone UI theme — defines accent and surface color tokens as CSS custom properties.
2
+ Apps override these variables to customize the color palette. */
3
+
4
+ @theme {
5
+ /* Accent — defaults to blue */
6
+ --color-accent-50: #eff6ff;
7
+ --color-accent-100: #dbeafe;
8
+ --color-accent-200: #bfdbfe;
9
+ --color-accent-300: #93c5fd;
10
+ --color-accent-400: #60a5fa;
11
+ --color-accent-500: #3b82f6;
12
+ --color-accent-600: #2563eb;
13
+ --color-accent-700: #1d4ed8;
14
+ --color-accent-800: #1e40af;
15
+ --color-accent-900: #1e3a8a;
16
+
17
+ /* Surface — defaults to zinc */
18
+ --color-surface-50: #fafafa;
19
+ --color-surface-100: #f4f4f5;
20
+ --color-surface-200: #e4e4e7;
21
+ --color-surface-300: #d4d4d8;
22
+ --color-surface-400: #a1a1aa;
23
+ --color-surface-500: #71717a;
24
+ --color-surface-600: #52525b;
25
+ --color-surface-700: #3f3f46;
26
+ --color-surface-800: #27272a;
27
+ --color-surface-900: #18181b;
28
+ --color-surface-950: #09090b;
29
+
30
+ /* Accent — darkest shade */
31
+ --color-accent-950: #172554;
32
+ }
@@ -0,0 +1,19 @@
1
+ <div class="<%= classes %>" data-controller="accordion">
2
+ <% items.each_with_index do |item, index| %>
3
+ <div class="<%= item_classes %>">
4
+ <button
5
+ type="button"
6
+ class="<%= button_classes %>"
7
+ data-action="click->accordion#toggle"
8
+ data-index="<%= index %>">
9
+ <span class="pr-4"><%= item[:question] %></span>
10
+ <span class="<%= icon_classes %>" data-accordion-target="icon" data-index="<%= index %>">
11
+ <%= caret_icon.html_safe %>
12
+ </span>
13
+ </button>
14
+ <div class="<%= answer_classes %>" data-accordion-target="answer" data-index="<%= index %>">
15
+ <p><%= item[:answer] %></p>
16
+ </div>
17
+ </div>
18
+ <% end %>
19
+ </div>
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class AccordionComponent < ViewComponent::Base
6
+ BASE_CLASSES = "flex flex-col gap-4"
7
+ ITEM_LAYOUT_CLASSES = "rounded-xl border"
8
+ BUTTON_LAYOUT_CLASSES = "flex w-full items-center justify-between px-6 py-4 text-left font-semibold transition"
9
+ ANSWER_LAYOUT_CLASSES = "hidden px-6 pb-4 text-sm"
10
+ ICON_LAYOUT_CLASSES = "shrink-0 transition-transform"
11
+
12
+ CARET_ICON = <<~SVG.freeze
13
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
14
+ SVG
15
+
16
+ attr_reader :items
17
+
18
+ def initialize(items: [])
19
+ @items = items
20
+ end
21
+
22
+ def classes
23
+ BASE_CLASSES
24
+ end
25
+
26
+ def item_classes
27
+ "#{ITEM_LAYOUT_CLASSES} border-surface-200 dark:border-surface-700"
28
+ end
29
+
30
+ def button_classes
31
+ "#{BUTTON_LAYOUT_CLASSES} text-surface-900 dark:text-white hover:text-accent-600 dark:hover:text-accent-400"
32
+ end
33
+
34
+ def answer_classes
35
+ "#{ANSWER_LAYOUT_CLASSES} text-surface-600 dark:text-surface-400"
36
+ end
37
+
38
+ def icon_classes
39
+ "#{ICON_LAYOUT_CLASSES} text-surface-400"
40
+ end
41
+
42
+ def caret_icon
43
+ CARET_ICON
44
+ end
45
+
46
+ def wrapper_data
47
+ { controller: "accordion" }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,15 @@
1
+ <div class="<%= classes %>" role="alert" <%= wrapper_data.map { |k, v| %(data-#{k}="#{v}") }.join(" ") %>>
2
+ <div class="<%= OUTER_CLASSES %>">
3
+ <div class="<%= INNER_CLASSES %>">
4
+ <% if title? %>
5
+ <p class="<%= TITLE_CLASSES %>"><%= title_text %></p>
6
+ <% end %>
7
+ <p class="<%= title? ? MESSAGE_WITH_TITLE_CLASSES : MESSAGE_CLASSES %>"><%= message_text %></p>
8
+ </div>
9
+ <% if dismissible? %>
10
+ <button type="button" class="<%= DISMISS_CLASSES %>" data-action="click->dismiss#close" aria-label="Dismiss">
11
+ &times;
12
+ </button>
13
+ <% end %>
14
+ </div>
15
+ </div>
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class AlertComponent < ViewComponent::Base
6
+ BASE_CLASSES = "rounded-md p-4"
7
+
8
+ TYPE_CLASSES = {
9
+ success: "bg-green-50 text-green-800 dark:bg-green-900/30 dark:text-green-300",
10
+ warning: "bg-yellow-50 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300",
11
+ error: "bg-red-50 text-red-800 dark:bg-red-900/30 dark:text-red-300"
12
+ }.freeze
13
+
14
+ OUTER_CLASSES = "flex"
15
+ INNER_CLASSES = "flex-1"
16
+ TITLE_CLASSES = "font-semibold"
17
+ MESSAGE_CLASSES = "text-sm"
18
+ MESSAGE_WITH_TITLE_CLASSES = "text-sm mt-1"
19
+ DISMISS_CLASSES = "ml-auto -mr-1.5 -mt-1.5 inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2 cursor-pointer"
20
+
21
+ def initialize(message:, type: :info, title: nil, dismissible: false)
22
+ @message = message
23
+ @type = type
24
+ @title = title
25
+ @dismissible = dismissible
26
+ end
27
+
28
+ def classes
29
+ type_css = if @type == :info
30
+ "bg-accent-50 text-accent-800 dark:bg-accent-900/30 dark:text-accent-300"
31
+ else
32
+ TYPE_CLASSES.fetch(@type)
33
+ end
34
+ "#{BASE_CLASSES} #{type_css}"
35
+ end
36
+
37
+ def message_text
38
+ @message
39
+ end
40
+
41
+ def title?
42
+ !@title.nil?
43
+ end
44
+
45
+ def title_text
46
+ @title
47
+ end
48
+
49
+ def dismissible?
50
+ @dismissible
51
+ end
52
+
53
+ def wrapper_data
54
+ dismissible? ? { controller: "dismiss" } : {}
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1 @@
1
+ <span class="<%= classes %>"><%= label %></span>
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class BadgeComponent < ViewComponent::Base
6
+ BASE_CLASSES = "inline-flex items-center rounded-full px-2 py-1 text-xs font-medium"
7
+
8
+ VARIANT_CLASSES = {
9
+ neutral: "bg-gray-100 text-gray-700 dark:bg-zinc-700 dark:text-gray-300",
10
+ success: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-400",
11
+ danger: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-400",
12
+ warning: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-400",
13
+ info: "bg-accent-100 text-accent-700 dark:bg-accent-900/50 dark:text-accent-400"
14
+ }.freeze
15
+
16
+ attr_reader :label
17
+
18
+ def initialize(label:, variant: :neutral)
19
+ @label = label
20
+ @variant = variant
21
+ end
22
+
23
+ def classes
24
+ "#{BASE_CLASSES} #{VARIANT_CLASSES.fetch(@variant)}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ <nav class="<%= nav_classes %>">
2
+ <%= content %>
3
+ </nav>
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class BottomNavComponent < ViewComponent::Base
6
+ NAV_CLASSES = "bottom-nav lg:hidden hotwire-native:hidden"
7
+
8
+ def nav_classes
9
+ NAV_CLASSES
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ <a href="<%= @href %>" class="<%= item_classes %>">
2
+ <%= @icon.html_safe %>
3
+ <span class="<%= LABEL_CLASSES %>"><%= @label %></span>
4
+ </a>
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class BottomNavItemComponent < ViewComponent::Base
6
+ attr_reader :label, :href, :icon
7
+
8
+ def initialize(label:, href:, icon:, active: false)
9
+ @label = label
10
+ @href = href
11
+ @icon = icon
12
+ @active = active
13
+ end
14
+
15
+ ITEM_BASE = "bottom-nav-item"
16
+ ACTIVE_CLASS = "active"
17
+ LABEL_CLASSES = "bottom-nav-label"
18
+
19
+ def item_classes
20
+ @active ? "#{ITEM_BASE} #{ACTIVE_CLASS}" : ITEM_BASE
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1 @@
1
+ <%= content_tag(tag_name, @label, **tag_options) %>
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class ButtonComponent < ViewComponent::Base
6
+ BASE_CLASSES = "inline-flex items-center justify-center font-semibold rounded-lg border-0 cursor-pointer no-underline"
7
+
8
+ VARIANT_CLASSES = {
9
+ secondary: "bg-gray-500 text-white hover:bg-gray-400",
10
+ danger: "bg-red-600 text-white hover:bg-red-500"
11
+ }.freeze
12
+
13
+ SIZE_CLASSES = {
14
+ sm: "text-sm px-3 py-1.5",
15
+ md: "text-base px-4 py-2",
16
+ lg: "text-lg px-5 py-3"
17
+ }.freeze
18
+
19
+ def initialize(label:, href: nil, variant: :primary, size: :md, type: :submit, data: nil)
20
+ @label = label
21
+ @href = href
22
+ @variant = variant
23
+ @size = size
24
+ @type = type.to_s
25
+ @data = data
26
+ end
27
+
28
+ def classes
29
+ variant_css = if @variant == :primary
30
+ "bg-accent-600 text-white hover:bg-accent-500"
31
+ else
32
+ VARIANT_CLASSES.fetch(@variant)
33
+ end
34
+ [ BASE_CLASSES, variant_css, SIZE_CLASSES.fetch(@size) ].join(" ")
35
+ end
36
+
37
+ def tag_name
38
+ button? ? :button : :a
39
+ end
40
+
41
+ def tag_options
42
+ options = { class: classes }
43
+ if button?
44
+ options[:type] = @type
45
+ else
46
+ options[:href] = @href
47
+ end
48
+ options[:data] = @data if @data
49
+ options
50
+ end
51
+
52
+ def button?
53
+ @href.nil?
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,9 @@
1
+ <div class="<%= card_classes %>">
2
+ <div class="<%= BODY_CLASSES %>">
3
+ <h3 class="<%= TITLE_CLASSES %>"><%= @title %></h3>
4
+ <p class="<%= SUMMARY_CLASSES %>"><%= @summary %></p>
5
+ </div>
6
+ <div class="<%= CTA_CLASSES %>">
7
+ <%= link_to @cta, @link, class: link_classes %>
8
+ </div>
9
+ </div>
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class CardComponent < ViewComponent::Base
6
+ CARD_CLASSES = "overflow-hidden rounded-lg border border-gray-200 bg-white dark:bg-zinc-900 dark:border-zinc-700"
7
+ CARD_EDGE_CLASSES = "overflow-hidden border-y border-gray-200 bg-white sm:rounded-lg sm:border-x dark:bg-zinc-900 dark:border-zinc-700"
8
+ BODY_CLASSES = "px-4 py-4 sm:px-6 sm:pt-6 sm:pb-4"
9
+ CTA_CLASSES = "px-4 pb-4 sm:px-6 sm:pb-6"
10
+ TITLE_CLASSES = "text-lg font-semibold text-gray-900 dark:text-white m-0"
11
+ SUMMARY_CLASSES = "mt-1 text-sm text-gray-500 dark:text-gray-400 mb-0"
12
+ LINK_BASE_CLASSES = "text-sm font-medium no-underline"
13
+
14
+ def initialize(title:, summary:, link:, cta: "Read more", edge_to_edge: false)
15
+ @title = title
16
+ @summary = summary
17
+ @link = link
18
+ @cta = cta
19
+ @edge_to_edge = edge_to_edge
20
+ end
21
+
22
+ def link_classes
23
+ "#{LINK_BASE_CLASSES} text-accent-600 hover:text-accent-900 dark:text-accent-400"
24
+ end
25
+
26
+ private
27
+
28
+ def card_classes
29
+ @edge_to_edge ? CARD_EDGE_CLASSES : CARD_CLASSES
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ <a href="<%= href %>" class="<%= classes %>">
2
+ <%= content %>
3
+ </a>
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class CardLinkComponent < ViewComponent::Base
6
+ BASE_CLASSES = "block rounded-lg border border-gray-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"
7
+ SHADOW_CLASS = "shadow-sm"
8
+ PADDING_CLASSES = { sm: "p-3", md: "p-4", lg: "p-6" }.freeze
9
+
10
+ attr_reader :href
11
+
12
+ def initialize(href:, padding: :md, shadow: true)
13
+ @href = href
14
+ @padding = padding
15
+ @shadow = shadow
16
+ end
17
+
18
+ def classes
19
+ tokens = [ BASE_CLASSES, "hover:border-accent-500/50", PADDING_CLASSES.fetch(@padding) ]
20
+ tokens << SHADOW_CLASS if @shadow
21
+ tokens.join(" ")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ <div class="<%= classes %>">
2
+ <h2 class="<%= title_classes %>"><%= title %></h2>
3
+ <div class="<%= chart_height_class %>">
4
+ <%= content %>
5
+ </div>
6
+ </div>
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class ChartCardComponent < ViewComponent::Base
6
+ CARD_CLASSES = "rounded-xl border border-gray-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800"
7
+ TITLE_CLASSES = "text-sm font-medium text-gray-500 dark:text-gray-400 mb-4"
8
+
9
+ HEIGHT_CLASSES = {
10
+ sm: "h-48",
11
+ md: "h-64",
12
+ lg: "h-96"
13
+ }.freeze
14
+
15
+ attr_reader :title
16
+
17
+ def initialize(title:, height: :md)
18
+ @title = title
19
+ @height = height
20
+ end
21
+
22
+ def classes
23
+ CARD_CLASSES
24
+ end
25
+
26
+ def title_classes
27
+ TITLE_CLASSES
28
+ end
29
+
30
+ def chart_height_class
31
+ HEIGHT_CLASSES.fetch(@height)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,6 @@
1
+ <div class="<%= wrapper_classes %>">
2
+ <% if caption? %>
3
+ <div class="<%= caption_classes %>"><%= caption %></div>
4
+ <% end %>
5
+ <pre class="<%= pre_classes %>"><code class="<%= language_class %>"><%= content %></code></pre>
6
+ </div>
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keystone
4
+ module Ui
5
+ class CodeComponent < ViewComponent::Base
6
+ WRAPPER_CLASSES = "overflow-hidden rounded-lg border border-surface-200 dark:border-surface-700"
7
+ CAPTION_CLASSES = "border-b border-surface-200 bg-surface-50 px-4 py-2 font-mono text-xs text-surface-500 dark:border-surface-700 dark:bg-surface-800 dark:text-surface-400"
8
+ PRE_CLASSES = "overflow-x-auto bg-surface-900 p-4 font-mono text-sm leading-relaxed text-surface-100 dark:bg-black"
9
+
10
+ attr_reader :language, :caption
11
+
12
+ def initialize(language: nil, caption: nil)
13
+ @language = language
14
+ @caption = caption
15
+ end
16
+
17
+ def caption?
18
+ !@caption.nil?
19
+ end
20
+
21
+ def wrapper_classes
22
+ WRAPPER_CLASSES
23
+ end
24
+
25
+ def caption_classes
26
+ CAPTION_CLASSES
27
+ end
28
+
29
+ def pre_classes
30
+ PRE_CLASSES
31
+ end
32
+
33
+ def language_class
34
+ return nil if @language.nil?
35
+
36
+ "language-#{@language}"
37
+ end
38
+ end
39
+ end
40
+ end