smriti 0.5.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 (124) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +168 -0
  4. data/Rakefile +15 -0
  5. data/app/assets/images/smriti/android-chrome-192x192.png +0 -0
  6. data/app/assets/images/smriti/android-chrome-512x512.png +0 -0
  7. data/app/assets/images/smriti/apple-touch-icon.png +0 -0
  8. data/app/assets/images/smriti/favicon-16x16.png +0 -0
  9. data/app/assets/images/smriti/favicon-32x32.png +0 -0
  10. data/app/assets/images/smriti/favicon-48x48.png +0 -0
  11. data/app/assets/images/smriti/favicon.ico +0 -0
  12. data/app/assets/images/smriti/favicon.svg +18 -0
  13. data/app/assets/images/smriti/logo.svg +18 -0
  14. data/app/assets/images/smriti/mask-icon.svg +5 -0
  15. data/app/assets/stylesheets/smriti/application.css +1040 -0
  16. data/app/controllers/smriti/admin/application_controller.rb +135 -0
  17. data/app/controllers/smriti/admin/dashboard_controller.rb +32 -0
  18. data/app/controllers/smriti/admin/mat_view_definitions_controller.rb +372 -0
  19. data/app/controllers/smriti/admin/mat_view_runs_controller.rb +185 -0
  20. data/app/controllers/smriti/admin/preferences_controller.rb +91 -0
  21. data/app/helpers/smriti/admin/datatable_helper.rb +249 -0
  22. data/app/helpers/smriti/admin/localized_digit_helper.rb +70 -0
  23. data/app/helpers/smriti/admin/ui_helper.rb +539 -0
  24. data/app/javascript/smriti/application.js +8 -0
  25. data/app/javascript/smriti/controllers/application.js +10 -0
  26. data/app/javascript/smriti/controllers/body_setup_controller.js +120 -0
  27. data/app/javascript/smriti/controllers/datatable_controller.js +351 -0
  28. data/app/javascript/smriti/controllers/details_controller.js +200 -0
  29. data/app/javascript/smriti/controllers/drawer_controller.js +470 -0
  30. data/app/javascript/smriti/controllers/flash_controller.js +112 -0
  31. data/app/javascript/smriti/controllers/index.js +10 -0
  32. data/app/javascript/smriti/controllers/mv_confirm_controller.js +435 -0
  33. data/app/javascript/smriti/controllers/tabs_controller.js +184 -0
  34. data/app/javascript/smriti/controllers/tooltip_controller.js +525 -0
  35. data/app/javascript/smriti/controllers/turbo_frame_lifecycle_controller.js +342 -0
  36. data/app/jobs/smriti/application_job.rb +144 -0
  37. data/app/jobs/smriti/create_view_job.rb +87 -0
  38. data/app/jobs/smriti/delete_view_job.rb +89 -0
  39. data/app/jobs/smriti/refresh_view_job.rb +94 -0
  40. data/app/models/concerns/smriti_i18n.rb +139 -0
  41. data/app/models/concerns/smriti_paginate.rb +70 -0
  42. data/app/models/concerns/smriti_query_helper.rb +36 -0
  43. data/app/models/smriti/application_record.rb +39 -0
  44. data/app/models/smriti/mat_view_definition.rb +254 -0
  45. data/app/models/smriti/mat_view_run.rb +275 -0
  46. data/app/views/layouts/smriti/_footer.html.erb +47 -0
  47. data/app/views/layouts/smriti/_header.html.erb +25 -0
  48. data/app/views/layouts/smriti/admin.html.erb +47 -0
  49. data/app/views/layouts/smriti/turbo_frame.html.erb +3 -0
  50. data/app/views/smriti/admin/dashboard/index.html.erb +38 -0
  51. data/app/views/smriti/admin/mat_view_definitions/_definition_actions.html.erb +94 -0
  52. data/app/views/smriti/admin/mat_view_definitions/_dt-index-empty-row.html.erb +11 -0
  53. data/app/views/smriti/admin/mat_view_definitions/_dt-index-row.html.erb +27 -0
  54. data/app/views/smriti/admin/mat_view_definitions/empty.html.erb +1 -0
  55. data/app/views/smriti/admin/mat_view_definitions/form.html.erb +79 -0
  56. data/app/views/smriti/admin/mat_view_definitions/index.html.erb +10 -0
  57. data/app/views/smriti/admin/mat_view_definitions/show.html.erb +40 -0
  58. data/app/views/smriti/admin/mat_view_runs/_dt-index-empty-row.html.erb +11 -0
  59. data/app/views/smriti/admin/mat_view_runs/_dt-index-row.html.erb +41 -0
  60. data/app/views/smriti/admin/mat_view_runs/index.html.erb +1 -0
  61. data/app/views/smriti/admin/mat_view_runs/show.html.erb +64 -0
  62. data/app/views/smriti/admin/preferences/show.html.erb +49 -0
  63. data/app/views/smriti/admin/ui/_card.html.erb +15 -0
  64. data/app/views/smriti/admin/ui/_datatable.html.erb +34 -0
  65. data/app/views/smriti/admin/ui/_datatable_filters.html.erb +45 -0
  66. data/app/views/smriti/admin/ui/_datatable_tbody.html.erb +11 -0
  67. data/app/views/smriti/admin/ui/_datatable_tfoot.html.erb +70 -0
  68. data/app/views/smriti/admin/ui/_datatable_thead.html.erb +105 -0
  69. data/app/views/smriti/admin/ui/_details.html.erb +10 -0
  70. data/app/views/smriti/admin/ui/_flash.html.erb +6 -0
  71. data/app/views/smriti/admin/ui/_table.html.erb +8 -0
  72. data/config/importmap.rb +9 -0
  73. data/config/locales/ar.yml +223 -0
  74. data/config/locales/de.yml +230 -0
  75. data/config/locales/en-AU-ocker.yml +223 -0
  76. data/config/locales/en-AU.yml +202 -0
  77. data/config/locales/en-BORK.yml +225 -0
  78. data/config/locales/en-CA.yml +223 -0
  79. data/config/locales/en-GB.yml +223 -0
  80. data/config/locales/en-LOL.yml +219 -0
  81. data/config/locales/en-SCOT.yml +223 -0
  82. data/config/locales/en-SHAKESPEARE.yml +225 -0
  83. data/config/locales/en-US-pirate.yml +222 -0
  84. data/config/locales/en-US.yml +225 -0
  85. data/config/locales/en-YODA.yml +221 -0
  86. data/config/locales/en.yml +223 -0
  87. data/config/locales/es.yml +226 -0
  88. data/config/locales/fa.yml +223 -0
  89. data/config/locales/fr-CA.yml +227 -0
  90. data/config/locales/fr.yml +227 -0
  91. data/config/locales/he.yml +218 -0
  92. data/config/locales/hi.yml +223 -0
  93. data/config/locales/it.yml +225 -0
  94. data/config/locales/ja-JP.yml +215 -0
  95. data/config/locales/pt.yml +225 -0
  96. data/config/locales/ru.yml +228 -0
  97. data/config/locales/ur.yml +225 -0
  98. data/config/locales/zh-CN.yml +214 -0
  99. data/config/locales/zh-TW.yml +214 -0
  100. data/config/routes.rb +36 -0
  101. data/lib/ext/exception.rb +20 -0
  102. data/lib/generators/smriti/install/install_generator.rb +86 -0
  103. data/lib/generators/smriti/install/templates/create_mat_view_definitions.rb +29 -0
  104. data/lib/generators/smriti/install/templates/create_mat_view_runs.rb +32 -0
  105. data/lib/generators/smriti/install/templates/smriti_initializer.rb +23 -0
  106. data/lib/smriti/admin/auth_bridge.rb +93 -0
  107. data/lib/smriti/admin/default_auth.rb +62 -0
  108. data/lib/smriti/configuration.rb +58 -0
  109. data/lib/smriti/engine.rb +82 -0
  110. data/lib/smriti/helpers/ui_test_ids.rb +49 -0
  111. data/lib/smriti/jobs/adapter.rb +81 -0
  112. data/lib/smriti/service_response.rb +75 -0
  113. data/lib/smriti/services/base_service.rb +471 -0
  114. data/lib/smriti/services/check_matview_exists.rb +76 -0
  115. data/lib/smriti/services/concurrent_refresh.rb +94 -0
  116. data/lib/smriti/services/create_view.rb +173 -0
  117. data/lib/smriti/services/delete_view.rb +111 -0
  118. data/lib/smriti/services/regular_refresh.rb +90 -0
  119. data/lib/smriti/services/swap_refresh.rb +181 -0
  120. data/lib/smriti/version.rb +21 -0
  121. data/lib/smriti.rb +64 -0
  122. data/lib/tasks/helpers.rb +185 -0
  123. data/lib/tasks/smriti_tasks.rake +151 -0
  124. metadata +206 -0
@@ -0,0 +1,539 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Codevedas Inc. 2025-present
4
+ #
5
+ # This source code is licensed under the MIT license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+
8
+ module Smriti
9
+ module Admin
10
+ # Smriti::Admin::UiHelper
11
+ # -------------------------
12
+ # View helper methods for the Smriti admin UI.
13
+ #
14
+ # Responsibilities:
15
+ # - Provides consistent button, link, drawer, badge, and icon components.
16
+ # - Wraps standard Rails helpers (`link_to`, `button_to`, `button_tag`, etc.)
17
+ # with Smriti-specific styling and Stimulus integration.
18
+ # - Defines inline SVG icon snippets for use across the admin dashboard.
19
+ #
20
+ # Key Components:
21
+ # - Buttons: {#mv_button_link}, {#mv_button_to}, {#mv_drawer_link}, {#mv_drawer_action_button}
22
+ # - Links: {#mv_link_to}
23
+ # - Badges: {#mv_badge}
24
+ # - Icons: {#mv_icon} with private `svg_icon_*` methods
25
+ #
26
+ module UiHelper
27
+ private
28
+
29
+ # Builds CSS classes for a Smriti-styled button.
30
+ #
31
+ # @api private
32
+ #
33
+ # @param variant [Symbol] one of `:primary`, `:secondary`, `:ghost`, `:negative`
34
+ # @param size [Symbol] one of `:sm`, `:md`, `:lg`
35
+ # @return [String] concatenated CSS class string
36
+ def mv_button_classes(variant, size)
37
+ [
38
+ 'mv-btn',
39
+ "mv-btn--#{variant}",
40
+ "mv-btn--#{size}"
41
+ ].join(' ')
42
+ end
43
+
44
+ # Renders a styled link button.
45
+ #
46
+ # @api private
47
+ #
48
+ # @param href [String] target URL
49
+ # @param opts [Hash] options for styling, data attributes, etc.
50
+ # @yield link body content
51
+ # @return [String] HTML-safe link tag
52
+ def mv_button_link(href, opts = {}, &)
53
+ link_to capture(&), href, **link_options(assign_test_id(opts))
54
+ end
55
+
56
+ # Renders a button link that opens a drawer via Stimulus.
57
+ #
58
+ # @api private
59
+ #
60
+ # @param drawer_url [String] URL to load into the drawer
61
+ # @param drawer_title [String] title for the drawer
62
+ # @param args [Hash] additional options
63
+ # @yield button body
64
+ # @return [String] HTML-safe link tag
65
+ def mv_drawer_link(drawer_url, drawer_title, args = {}, &)
66
+ data = { action: 'click->drawer#open', drawer_title: drawer_title, drawer_url: drawer_url }
67
+ args[:data] = (args[:data] || {}).merge(data)
68
+ mv_button_link '#', assign_test_id(args), &
69
+ end
70
+
71
+ # Renders a styled `button_to` element.
72
+ #
73
+ # @api private
74
+ #
75
+ # @param href [String] target URL
76
+ # @param opts [Hash] options for styling, data attributes, etc.
77
+ # @yield button content
78
+ # @return [String] HTML-safe button tag
79
+ def mv_button_to(href, opts = {}, &)
80
+ button_to href, **link_options(assign_test_id(opts)) do
81
+ capture(&)
82
+ end
83
+ end
84
+
85
+ # Renders a drawer action button with optional tooltip.
86
+ #
87
+ # @api private
88
+ #
89
+ # @param label [String] ARIA label for accessibility
90
+ # @param action [String] Stimulus action method
91
+ # @param tooltip [String, nil] optional tooltip text
92
+ # @param tooltip_placement [String, nil] placement of tooltip (default: "top")
93
+ # @param args_orig [Hash] additional HTML options
94
+ # @yield button content
95
+ # @return [String] HTML-safe button tag
96
+ def mv_drawer_action_button(label, action, tooltip = nil, tooltip_placement = nil, args_org = {}, &)
97
+ args = assign_test_id(args_org)
98
+ data = { action: "drawer##{action}" }
99
+ data = data.merge(args[:data] || {})
100
+ if tooltip
101
+ data[:controller] = 'tooltip'
102
+ data[:'tooltip-text-value'] = tooltip
103
+ data[:'tooltip-placement'] = tooltip_placement || 'top'
104
+ end
105
+
106
+ args[:data] = data
107
+
108
+ button_tag(type: 'button', class: 'mv-drawer-action', 'aria-label': label, **args, &)
109
+ end
110
+
111
+ # Renders a styled external or internal link, with optional tooltip.
112
+ #
113
+ # @api private
114
+ #
115
+ # @param text [String, nil] link text (nil if using block form)
116
+ # @param url [String, nil] target URL
117
+ # @param args_orig [Hash] HTML options (supports `:tooltip`, `:is_blank`)
118
+ # @yield link body when block form is used
119
+ # @return [String] HTML-safe link tag
120
+ def mv_link_to(text = nil, url = nil, args_orig = nil, &block)
121
+ args = assign_test_id(args_orig || {})
122
+ if block_given?
123
+ args = assign_test_id(url || {})
124
+ url = text
125
+ end
126
+
127
+ tooltip = args.fetch(:tooltip, nil)
128
+ is_blank = args.fetch(:is_blank, true)
129
+ underline = args.fetch(:underline, true)
130
+ args_to_apply = args.except(:tooltip, :is_blank)
131
+ if is_blank
132
+ args_to_apply[:target] = '_blank'
133
+ args_to_apply[:rel] = 'noopener noreferrer'
134
+ end
135
+ args_to_apply[:class] = 'underline' if underline
136
+ if tooltip
137
+ args_to_apply[:'data-controller'] = 'tooltip'
138
+ args_to_apply[:'data-tooltip-text-value'] = tooltip
139
+ end
140
+
141
+ if block_given?
142
+ link_to url, args_to_apply do
143
+ capture(&block)
144
+ end
145
+ else
146
+ link_to text, url, **args_to_apply
147
+ end
148
+ end
149
+
150
+ # Renders a tab link for the tabs component.
151
+ #
152
+ # @api private
153
+ #
154
+ # @param tab_name [String] unique name of the tab
155
+ # @param args_org [Hash] additional HTML options
156
+ #
157
+ # @yield tab link content
158
+ #
159
+ # @return [String] HTML-safe link tag
160
+ def mv_tab_link(tab_name, args_org = {}, &)
161
+ args = assign_test_id(args_org)
162
+ classes = ['mv-tab']
163
+ selected = args.delete(:selected)
164
+ classes << (selected ? 'mv-tab--on' : '')
165
+ args[:class] = [args[:class], classes.compact.join(' ')].compact.join(' ').strip
166
+ args[:data] = args.fetch(:data, {}).merge({ action: 'click->tabs#show', 'tabs-target': 'link', name: tab_name })
167
+ link_to '#', **args, &
168
+ end
169
+
170
+ # Renders a badge element styled by status.
171
+ #
172
+ # @api private
173
+ #
174
+ # @param status [String, Symbol] status name (`success`, `running`, `failed`, etc.)
175
+ # @param text [String] text to display inside the badge
176
+ # @return [String] HTML-safe span tag with badge classes
177
+ def mv_badge(status, text)
178
+ klass = case status.to_s.downcase
179
+ when 'success' then 'mv-chip mv-chip--success'
180
+ when 'running' then 'mv-chip mv-chip--running'
181
+ when 'failed' then 'mv-chip mv-chip--failed'
182
+ else 'mv-chip'
183
+ end
184
+ content_tag(:span, text, class: klass)
185
+ end
186
+
187
+ # Renders an inline SVG icon.
188
+ #
189
+ # @api private
190
+ #
191
+ # @param name [String, Symbol] icon name (method suffix after `svg_icon_`)
192
+ # @param size [Integer] icon width/height in pixels
193
+ # @param class_name [String, nil] optional extra CSS class
194
+ # @return [String] HTML-safe SVG element
195
+ def mv_icon(name, size: 16, class_name: nil)
196
+ icon_method_name = :"svg_icon_#{name}"
197
+ content_tag :svg,
198
+ class: ['mv-icon', class_name].compact.join(' '),
199
+ xmlns: 'http://www.w3.org/2000/svg',
200
+ width: size, height: size, viewBox: '0 0 24 24',
201
+ fill: 'none', stroke: 'currentColor',
202
+ 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round',
203
+ 'aria-hidden': 'true' do
204
+ respond_to?(icon_method_name, true) ? raw(send(icon_method_name)) : ''.html_safe
205
+ end
206
+ end
207
+
208
+ # Renders a styled submit button.
209
+ #
210
+ # @api private
211
+ #
212
+ # @param opts [Hash] options for styling, data attributes, etc.
213
+ # @yield button content
214
+ # @return [String] HTML-safe button tag
215
+ def mv_submit_button(opts = {}, &)
216
+ opts = link_options(assign_test_id(opts))
217
+ opts[:type] = 'submit'
218
+ opts.delete(:method)
219
+ button_tag(capture(&), **opts)
220
+ end
221
+
222
+ # Renders a styled generic button.
223
+ #
224
+ # @api private
225
+ #
226
+ # @param opts [Hash] options for styling, data attributes, etc.
227
+ # @yield button content
228
+ # @return [String] HTML-safe button tag
229
+ def mv_button(opts = {}, &)
230
+ opts = link_options(assign_test_id(opts))
231
+ opts[:type] = 'button'
232
+ opts.delete(:method)
233
+ button_tag(capture(&), **opts)
234
+ end
235
+
236
+ # Renders a styled cancel button.
237
+ #
238
+ # @api private
239
+ #
240
+ # @param opts [Hash] options for styling, data attributes, etc.
241
+ # @yield button content
242
+ # @return [String] HTML-safe button tag
243
+ def mv_cancel_button(opts = {}, &)
244
+ mv_button(opts.merge(variant: :secondary), &)
245
+ end
246
+
247
+ # Builds standardized options hash for button/link helpers.
248
+ #
249
+ # @api private
250
+ #
251
+ # @param opts [Hash] options including :variant, :size, :method, :tooltip, etc.
252
+ # @return [Hash] merged HTML attributes (class, method, data)
253
+ def link_options(opts)
254
+ variant = opts.fetch(:variant, :primary)
255
+ size = opts.fetch(:size, :md)
256
+ classes = opts[:class] ? " #{opts.delete(:class)}" : ''
257
+ method = opts.fetch(:method, :get)
258
+ disabled = opts.fetch(:disabled, false)
259
+ underline = opts[:underline] ? ' underline' : ''
260
+
261
+ tip = opts[:tooltip]
262
+ tooltip = if tip
263
+ {
264
+ controller: 'tooltip',
265
+ 'tooltip-text-value': tip,
266
+ 'tooltip-placement': opts.fetch(:tooltip_placement, 'top')
267
+ }
268
+ else
269
+ {}
270
+ end
271
+
272
+ html_data = (opts[:data] || {}).dup.merge(tooltip)
273
+ html_data[:'turbo-confirm'] = opts.fetch(:confirm, nil)
274
+ { class: "#{mv_button_classes(variant, size)}#{underline}#{classes}", method: method, disabled: disabled, data: html_data }
275
+ end
276
+
277
+ # SVG markup for a right-pointing arrow icon
278
+ #
279
+ # @api private
280
+ #
281
+ # @return [String] SVG markup
282
+ def svg_icon_arrow_right
283
+ <<~SVG.strip.freeze
284
+ <polyline points="9 18 15 12 9 6"/>
285
+ SVG
286
+ end
287
+
288
+ # SVG markup for a left-pointing arrow icon
289
+ #
290
+ # @api private
291
+ #
292
+ # @return [String] SVG markup
293
+ def svg_icon_arrow_left
294
+ <<~SVG.strip.freeze
295
+ <polyline points="15 18 9 12 15 6"/>
296
+ SVG
297
+ end
298
+
299
+ # SVG markup for a left-pointing double-arrow icon
300
+ #
301
+ # @api private
302
+ #
303
+ # @return [String] SVG markup
304
+ def svg_icon_double_arrow_left
305
+ <<~SVG.strip.freeze
306
+ <polyline points="11 17 6 12 11 7"/>
307
+ <polyline points="18 17 13 12 18 7"/>
308
+ SVG
309
+ end
310
+
311
+ # SVG markup for a right-pointing double-arrow icon
312
+ #
313
+ # @api private
314
+ #
315
+ # @return [String] SVG markup
316
+ def svg_icon_double_arrow_right
317
+ <<~SVG.strip.freeze
318
+ <polyline points="13 17 18 12 13 7"/>
319
+ <polyline points="6 17 11 12 6 7"/>
320
+ SVG
321
+ end
322
+
323
+ # SVG markup for a refresh/reload icon
324
+ #
325
+ # @api private
326
+ #
327
+ # @return [String] SVG markup
328
+ def svg_icon_refresh
329
+ <<~SVG.strip.freeze
330
+ <path d="M21 12a9 9 0 1 1-2.64-6.36"/>
331
+ <polyline points="23 4 23 10 17 10"/>
332
+ SVG
333
+ end
334
+
335
+ # SVG markup for a trash/delete (bin) icon
336
+ #
337
+ # @api private
338
+ #
339
+ # @return [String] SVG markup
340
+ def svg_icon_trash
341
+ <<~SVG.strip.freeze
342
+ <polyline points="3 6 5 6 21 6"/>
343
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
344
+ <path d="M10 11v6"/>
345
+ <path d="M14 11v6"/>
346
+ <path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/>
347
+ SVG
348
+ end
349
+
350
+ # SVG markup for a hammer/tool icon
351
+ #
352
+ # @api private
353
+ #
354
+ # @return [String] SVG markup
355
+ def svg_icon_hammer
356
+ <<~SVG.strip.freeze
357
+ <path d="M14 4l7 7"/>
358
+ <path d="M5 15l7-7 3 3-7 7H5v-3z"/>
359
+ SVG
360
+ end
361
+
362
+ # SVG markup for an “X in a circle” (close/error) icon
363
+ #
364
+ # @api private
365
+ #
366
+ # @return [String] SVG markup
367
+ def svg_icon_x_circle
368
+ <<~SVG.strip.freeze
369
+ <circle cx="12" cy="12" r="10"/>
370
+ <path d="M15 9l-6 6"/>
371
+ <path d="M9 9l6 6"/>
372
+ SVG
373
+ end
374
+
375
+ # SVG markup for a “plus in a circle” (add) icon
376
+ #
377
+ # @api private
378
+ #
379
+ # @return [String] SVG markup
380
+ def svg_icon_plus_circle
381
+ <<~SVG.strip.freeze
382
+ <circle cx="12" cy="12" r="10"/>
383
+ <line x1="12" y1="8" x2="12" y2="16"/>
384
+ <line x1="8" y1="12" x2="16" y2="12"/>
385
+ SVG
386
+ end
387
+
388
+ # SVG markup for a checkmark-in-circle (success) icon
389
+ #
390
+ # @api private
391
+ #
392
+ # @return [String] SVG markup
393
+ def svg_icon_check_circle
394
+ <<~SVG.strip.freeze
395
+ <circle cx="12" cy="12" r="10"/>
396
+ <polyline points="9 12 12 15 16 9"/>
397
+ SVG
398
+ end
399
+
400
+ # SVG markup for an alert/warning triangle icon
401
+ #
402
+ # @api private
403
+ #
404
+ # @return [String] SVG markup
405
+ def svg_icon_alert
406
+ <<~SVG.strip.freeze
407
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
408
+ <line x1="12" y1="9" x2="12" y2="13"/>
409
+ <line x1="12" y1="17" x2="12.01" y2="17"/>
410
+ SVG
411
+ end
412
+
413
+ # SVG markup for a history/clock icon
414
+ #
415
+ # @api private
416
+ #
417
+ # @return [String] SVG markup
418
+ def svg_icon_history
419
+ <<~SVG.strip.freeze
420
+ <path d="M3 3v5h5"/>
421
+ <path d="M3.05 13a9 9 0 1 0 .5-5.5"/>
422
+ <path d="M12 7v5l3 3"/>
423
+ SVG
424
+ end
425
+
426
+ # SVG markup for an edit/pencil icon
427
+ #
428
+ # @api private
429
+ #
430
+ # @return [String] SVG markup
431
+ def svg_icon_edit
432
+ <<~SVG.strip.freeze
433
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
434
+ <path d="M18.5 2.5a2.1 2.1 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
435
+ SVG
436
+ end
437
+
438
+ # SVG markup for a stacked-layers icon
439
+ #
440
+ # @api private
441
+ #
442
+ # @return [String] SVG markup
443
+ def svg_icon_layers
444
+ <<~SVG.strip.freeze
445
+ <polygon points="12 2 2 7 12 12 22 7 12 2"/>
446
+ <polyline points="2 17 12 22 22 17"/>
447
+ <polyline points="2 12 12 17 22 12"/>
448
+ SVG
449
+ end
450
+
451
+ # SVG markup for a database/cylinder icon
452
+ #
453
+ # @api private
454
+ #
455
+ # @return [String] SVG markup
456
+ def svg_icon_database
457
+ <<~SVG.strip.freeze
458
+ <ellipse cx="12" cy="5" rx="9" ry="3"/>
459
+ <path d="M3 5v6c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/>
460
+ <path d="M3 11v6c0 1.66 4.03 3 9 3s9-1.34 9-3v-6"/>
461
+ SVG
462
+ end
463
+
464
+ # SVG markup for a gear/settings icon
465
+ #
466
+ # @api private
467
+ #
468
+ # @return [String] SVG markup
469
+ def svg_icon_gear
470
+ <<~SVG.strip.freeze
471
+ <circle cx="12" cy="12" r="3"></circle>
472
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
473
+ SVG
474
+ end
475
+
476
+ # SVG markup for a sort ascending icon
477
+ #
478
+ # @api private
479
+ #
480
+ # @return [String] SVG markup
481
+ def svg_icon_sort_asc
482
+ <<~SVG.strip.freeze
483
+ <line x1="6" y1="6" x2="12" y2="6"/>
484
+ <line x1="6" y1="12" x2="16" y2="12"/>
485
+ <line x1="6" y1="18" x2="20" y2="18"/>
486
+ SVG
487
+ end
488
+
489
+ # SVG markup for a sort descending icon
490
+ #
491
+ # @api private
492
+ #
493
+ # @return [String] SVG markup
494
+ def svg_icon_sort_desc
495
+ <<~SVG.strip.freeze
496
+ <line x1="6" y1="6" x2="20" y2="6"/>
497
+ <line x1="6" y1="12" x2="16" y2="12"/>
498
+ <line x1="6" y1="18" x2="12" y2="18"/>
499
+ SVG
500
+ end
501
+
502
+ # SVG markup for a neutral sort icon (no particular order)
503
+ #
504
+ # @api private
505
+ #
506
+ # @return [String] SVG markup
507
+ def svg_icon_sort_neutral
508
+ # three lines: top line is greater, middle is lower then top, bottom is lower then middle
509
+ <<~SVG.strip.freeze
510
+ <line x1="6" y1="6" x2="20" y2="6"/>
511
+ <line x1="6" y1="12" x2="20" y2="12"/>
512
+ <line x1="6" y1="18" x2="20" y2="18"/>
513
+ SVG
514
+ end
515
+
516
+ # Maps a symbolic test ID to its actual string value for data attributes.
517
+ # if `:testid` key is not present, returns original args unchanged.
518
+ #
519
+ # @api private
520
+ #
521
+ # @param args [Hash] original options hash
522
+ # @return [Hash] modified options hash with `data-testid` set
523
+ #
524
+ # @example
525
+ # assign_test_id(class: 'btn', testid: :HEADER_LINK)
526
+ # # => { class: 'btn', data: { testid: 'header_link' } }
527
+ #
528
+ def assign_test_id(args = {})
529
+ return args unless args[:testid].present?
530
+
531
+ testid_constant = args.delete(:testid)
532
+ testid_identifier = args.delete(:testid_identifier) || ''
533
+ args_data = args[:data] || {}
534
+ args_data[:testid] = "#{Smriti::Helpers::UiTestIds.const_get(testid_constant)}-#{testid_identifier}".chomp('-')
535
+ args.merge data: args_data
536
+ end
537
+ end
538
+ end
539
+ end
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright Codevedas Inc. 2025-present
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import "@hotwired/turbo-rails";
8
+ import "smriti/controllers";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Copyright Codevedas Inc. 2025-present
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import { Application } from "@hotwired/stimulus";
8
+ const application = Application.start();
9
+ window.Stimulus = application;
10
+ export { application };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Copyright Codevedas Inc. 2025-present
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ /**
9
+ * Stimulus Controller: ThemeAndTimezoneController
10
+ * -----------------------------------------------
11
+ * Manages browser timezone cookie and applied theme attributes.
12
+ *
13
+ * Responsibilities:
14
+ * - Detects and stores the browser’s timezone in a cookie.
15
+ * - Determines and applies the appropriate UI theme (light/dark)
16
+ * based on user setting or system preference.
17
+ * - Ensures consistency between `data-theme` and `data-applied-theme`
18
+ * attributes on the `<html>` element.
19
+ *
20
+ * Key Components:
21
+ * - Timezone: `_ensureTimezoneCookie`, `_cookieMatches`, `_writeCookie`
22
+ * - Theme: `_ensureAppliedTheme`, `_applyTheme`
23
+ */
24
+
25
+ import { Controller } from "@hotwired/stimulus";
26
+
27
+ /**
28
+ * @class ThemeAndTimezoneController
29
+ * @extends Controller
30
+ */
31
+ export default class extends Controller {
32
+ /**
33
+ * Static values configuration for Stimulus values API.
34
+ * @property {string} timezoneCookie - Name of the timezone cookie.
35
+ * @property {string} timezoneCookiePath - Path for the timezone cookie.
36
+ * @property {string} themeAttribute - Attribute containing desired theme.
37
+ * @property {string} appliedThemeAttribute - Attribute to store applied theme.
38
+ */
39
+ static values = {
40
+ timezoneCookie: { type: String, default: "browser_tz" },
41
+ timezoneCookiePath: { type: String, default: "/" },
42
+ themeAttribute: { type: String, default: "data-theme" },
43
+ appliedThemeAttribute: { type: String, default: "data-applied-theme" },
44
+ };
45
+
46
+ /**
47
+ * Initializes controller-level references.
48
+ */
49
+ initialize() {
50
+ /** @type {HTMLElement} Root HTML element */
51
+ this.htmlElement = document.documentElement;
52
+ }
53
+
54
+ /**
55
+ * Called when the controller is connected to the DOM.
56
+ * Ensures timezone and theme are properly set.
57
+ */
58
+ connect() {
59
+ this._ensureTimezoneCookie();
60
+ this._ensureAppliedTheme();
61
+ }
62
+
63
+ // ── Timezone ─────────────────────────────────────────────────────
64
+
65
+ /**
66
+ * Ensures that a cookie storing the browser's timezone is set.
67
+ */
68
+ _ensureTimezoneCookie() {
69
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
70
+ if (this._cookieMatches(tz)) return;
71
+ this._writeCookie(tz);
72
+ }
73
+
74
+ /**
75
+ * Checks whether the stored cookie already matches the given value.
76
+ * @param {string} value - Timezone string to check.
77
+ * @return {boolean} True if cookie already matches.
78
+ */
79
+ _cookieMatches(value) {
80
+ const needle = `${this.timezoneCookieValue}=${value}`;
81
+ return document.cookie.split(";").some((entry) => entry.trim() === needle);
82
+ }
83
+
84
+ /**
85
+ * Writes a cookie with the provided timezone value.
86
+ * @param {string} value - Timezone value (e.g., "America/New_York").
87
+ * @return {void}
88
+ */
89
+ _writeCookie(value) {
90
+ document.cookie = `${this.timezoneCookieValue}=${value}; path=${this.timezoneCookiePathValue}`;
91
+ }
92
+
93
+ // ── Theme ────────────────────────────────────────────────────────
94
+
95
+ /**
96
+ * Ensures that the applied theme matches either a user preference
97
+ * or the system’s color-scheme setting.
98
+ */
99
+ _ensureAppliedTheme() {
100
+ const setting = this.htmlElement.getAttribute(this.themeAttributeValue);
101
+ if (setting === "light" || setting === "dark") {
102
+ this._applyTheme(setting);
103
+ return;
104
+ }
105
+
106
+ const prefersDark = window.matchMedia(
107
+ "(prefers-color-scheme: dark)",
108
+ ).matches;
109
+ this._applyTheme(prefersDark ? "dark" : "light");
110
+ }
111
+
112
+ /**
113
+ * Applies a theme by updating the `data-applied-theme` attribute.
114
+ * @param {"light" | "dark"} theme - Theme name to apply.
115
+ * @return {void}
116
+ */
117
+ _applyTheme(theme) {
118
+ this.htmlElement.setAttribute(this.appliedThemeAttributeValue, theme);
119
+ }
120
+ }