baldur 0.1.1

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 (164) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +21 -0
  4. data/README.md +318 -0
  5. data/TODO.md +6 -0
  6. data/app/assets/javascripts/baldur/controllers/accordion_controller.js +148 -0
  7. data/app/assets/javascripts/baldur/controllers/alert_controller.js +209 -0
  8. data/app/assets/javascripts/baldur/controllers/date_field_controller.js +558 -0
  9. data/app/assets/javascripts/baldur/controllers/details_menu_controller.js +30 -0
  10. data/app/assets/javascripts/baldur/controllers/form_submit_controller.js +7 -0
  11. data/app/assets/javascripts/baldur/controllers/marketing_pricing_controller.js +47 -0
  12. data/app/assets/javascripts/baldur/controllers/marketing_tabs_controller.js +118 -0
  13. data/app/assets/javascripts/baldur/controllers/menu_select_controller.js +401 -0
  14. data/app/assets/javascripts/baldur/controllers/mobile_sidebar_controller.js +13 -0
  15. data/app/assets/javascripts/baldur/controllers/modal_controller.js +149 -0
  16. data/app/assets/javascripts/baldur/controllers/panel_right_controller.js +1 -0
  17. data/app/assets/javascripts/baldur/controllers/panel_secondary_controller.js +129 -0
  18. data/app/assets/javascripts/baldur/controllers/segmented_tabs_controller.js +38 -0
  19. data/app/assets/javascripts/baldur/controllers/sidebar_controller.js +77 -0
  20. data/app/assets/javascripts/baldur/controllers/smooth_scroll_controller.js +29 -0
  21. data/app/assets/javascripts/baldur/controllers/snackbar_controller.js +158 -0
  22. data/app/assets/javascripts/baldur/controllers/table_disclosure_controller.js +46 -0
  23. data/app/assets/javascripts/baldur/controllers/theme_controller.js +90 -0
  24. data/app/assets/javascripts/baldur/controllers/tooltip_controller.js +136 -0
  25. data/app/assets/javascripts/baldur/lib/animation-helpers.js +56 -0
  26. data/app/assets/javascripts/baldur/lib/dom-helpers.js +80 -0
  27. data/app/assets/javascripts/baldur/lib/field-validation-helpers.js +36 -0
  28. data/app/assets/javascripts/baldur/lib/focus-management.js +89 -0
  29. data/app/assets/javascripts/baldur/lib/formatting-helpers.js +100 -0
  30. data/app/assets/javascripts/baldur/lib/lucide.js +20 -0
  31. data/app/assets/javascripts/baldur/lib/snackbar.js +50 -0
  32. data/app/assets/javascripts/baldur/lib/storage-helpers.js +50 -0
  33. data/app/assets/stylesheets/baldur/application/components/alert.css +226 -0
  34. data/app/assets/stylesheets/baldur/application/components/app_bar.css +41 -0
  35. data/app/assets/stylesheets/baldur/application/components/button.css +173 -0
  36. data/app/assets/stylesheets/baldur/application/components/card.css +63 -0
  37. data/app/assets/stylesheets/baldur/application/components/chart.css +40 -0
  38. data/app/assets/stylesheets/baldur/application/components/chip.css +51 -0
  39. data/app/assets/stylesheets/baldur/application/components/dialog.css +81 -0
  40. data/app/assets/stylesheets/baldur/application/components/forms.css +624 -0
  41. data/app/assets/stylesheets/baldur/application/components/layout.css +2 -0
  42. data/app/assets/stylesheets/baldur/application/components/list.css +15 -0
  43. data/app/assets/stylesheets/baldur/application/components/menu.css +300 -0
  44. data/app/assets/stylesheets/baldur/application/components/panel-right.css +1 -0
  45. data/app/assets/stylesheets/baldur/application/components/panel-secondary.css +71 -0
  46. data/app/assets/stylesheets/baldur/application/components/progress.css +84 -0
  47. data/app/assets/stylesheets/baldur/application/components/segmented-buttons.css +117 -0
  48. data/app/assets/stylesheets/baldur/application/components/settings-nav.css +84 -0
  49. data/app/assets/stylesheets/baldur/application/components/sidebar.css +123 -0
  50. data/app/assets/stylesheets/baldur/application/components/snackbar.css +179 -0
  51. data/app/assets/stylesheets/baldur/application/components/stepper.css +124 -0
  52. data/app/assets/stylesheets/baldur/application/components/switch.css +105 -0
  53. data/app/assets/stylesheets/baldur/application/components/table.css +331 -0
  54. data/app/assets/stylesheets/baldur/application/components/timeline.css +184 -0
  55. data/app/assets/stylesheets/baldur/application/components/utilities.css +180 -0
  56. data/app/assets/stylesheets/baldur/application/global.css +125 -0
  57. data/app/assets/stylesheets/baldur/application/marketing/layout.css +36 -0
  58. data/app/assets/stylesheets/baldur/application/motion.css +125 -0
  59. data/app/assets/stylesheets/baldur/application/theme.css +329 -0
  60. data/app/assets/stylesheets/baldur/theme/dark.css +90 -0
  61. data/app/assets/stylesheets/baldur/theme/light.css +82 -0
  62. data/app/assets/stylesheets/baldur.css +27 -0
  63. data/app/assets/stylesheets/baldur_panel_right.css +1 -0
  64. data/app/assets/stylesheets/baldur_panel_secondary.css +1 -0
  65. data/app/assets/tailwind/baldur/engine.css +5 -0
  66. data/app/helpers/baldur/compatibility/ui_aliases.rb +7 -0
  67. data/app/helpers/baldur/marketing_helper.rb +121 -0
  68. data/app/helpers/baldur/optional/auth_page_helper.rb +17 -0
  69. data/app/helpers/baldur/optional/google_auth_helper.rb +16 -0
  70. data/app/helpers/baldur/optional/panel_right_helper.rb +7 -0
  71. data/app/helpers/baldur/optional/panel_secondary_helper.rb +26 -0
  72. data/app/helpers/baldur/render_helper.rb +13 -0
  73. data/app/helpers/baldur/ui_helper.rb +217 -0
  74. data/app/helpers/baldur/ui_helper_feedback.rb +93 -0
  75. data/app/helpers/baldur/ui_helper_forms.rb +230 -0
  76. data/app/helpers/baldur/ui_helper_unavailable.rb +98 -0
  77. data/app/views/baldur/components/_accordion.html.erb +30 -0
  78. data/app/views/baldur/components/_action_row.html.erb +6 -0
  79. data/app/views/baldur/components/_alert.html.erb +61 -0
  80. data/app/views/baldur/components/_badge.html.erb +25 -0
  81. data/app/views/baldur/components/_button.html.erb +81 -0
  82. data/app/views/baldur/components/_card.html.erb +40 -0
  83. data/app/views/baldur/components/_chart_card.html.erb +42 -0
  84. data/app/views/baldur/components/_checkbox.html.erb +27 -0
  85. data/app/views/baldur/components/_date_field.html.erb +43 -0
  86. data/app/views/baldur/components/_google_sign_in_button.html.erb +1 -0
  87. data/app/views/baldur/components/_kebab_menu.html.erb +36 -0
  88. data/app/views/baldur/components/_kpi.html.erb +45 -0
  89. data/app/views/baldur/components/_menu_select.html.erb +78 -0
  90. data/app/views/baldur/components/_modal.html.erb +54 -0
  91. data/app/views/baldur/components/_pagination.html.erb +61 -0
  92. data/app/views/baldur/components/_segmented_buttons.html.erb +51 -0
  93. data/app/views/baldur/components/_settings_nav.html.erb +41 -0
  94. data/app/views/baldur/components/_snackbar.html.erb +42 -0
  95. data/app/views/baldur/components/_snackbar_stack.html.erb +13 -0
  96. data/app/views/baldur/components/_stepper.html.erb +39 -0
  97. data/app/views/baldur/components/_table.html.erb +117 -0
  98. data/app/views/baldur/components/_table_card.html.erb +86 -0
  99. data/app/views/baldur/components/_table_footer.html.erb +68 -0
  100. data/app/views/baldur/components/_text_field.html.erb +33 -0
  101. data/app/views/baldur/components/_tooltip.html.erb +73 -0
  102. data/app/views/baldur/marketing/_cta_banner.html.erb +20 -0
  103. data/app/views/baldur/marketing/_faq_section.html.erb +37 -0
  104. data/app/views/baldur/marketing/_features_section.html.erb +67 -0
  105. data/app/views/baldur/marketing/_footer.html.erb +38 -0
  106. data/app/views/baldur/marketing/_hero_section.html.erb +259 -0
  107. data/app/views/baldur/marketing/_pricing_tables.html.erb +99 -0
  108. data/app/views/baldur/marketing/_testimonials_section.html.erb +80 -0
  109. data/app/views/baldur/marketing/_top_nav.html.erb +28 -0
  110. data/app/views/baldur/optional/_auth_page.html.erb +21 -0
  111. data/app/views/baldur/optional/_google_sign_in_button.html.erb +19 -0
  112. data/app/views/baldur/optional/_panel_right.html.erb +1 -0
  113. data/app/views/baldur/optional/_panel_secondary.html.erb +34 -0
  114. data/baldur.gemspec +30 -0
  115. data/config/importmap.rb +2 -0
  116. data/lib/baldur/configuration.rb +24 -0
  117. data/lib/baldur/engine.rb +10 -0
  118. data/lib/baldur/version.rb +3 -0
  119. data/lib/baldur.rb +17 -0
  120. data/lib/generators/baldur/install/install_generator.rb +113 -0
  121. data/lib/generators/baldur/install/templates/baldur_initializer.rb +19 -0
  122. data/lib/generators/baldur/install/templates/fonts.css +14 -0
  123. data/lib/generators/baldur/install/templates/theme.css +27 -0
  124. data/lib/generators/baldur/install/templates/ui_helper.rb +4 -0
  125. data/lib/generators/baldur/install_google_auth/install_google_auth_generator.rb +15 -0
  126. data/lib/generators/baldur/install_panel_right/install_panel_right_generator.rb +9 -0
  127. data/lib/generators/baldur/install_panel_secondary/install_panel_secondary_generator.rb +21 -0
  128. data/script/verify_host_install +111 -0
  129. data/test/gemspec_test.rb +11 -0
  130. data/test/install_generator_test.rb +35 -0
  131. data/test/install_panel_secondary_generator_test.rb +21 -0
  132. data/test/marketing_helper_test.rb +38 -0
  133. data/test/run_all.rb +3 -0
  134. data/test/test_helper.rb +9 -0
  135. data/test/tmp/install_generator/app/assets/stylesheets/fonts.css +14 -0
  136. data/test/tmp/install_generator/app/assets/stylesheets/theme.css +27 -0
  137. data/test/tmp/install_generator/app/assets/tailwind/application.css +4 -0
  138. data/test/tmp/install_generator/app/helpers/ui_helper.rb +4 -0
  139. data/test/tmp/install_generator/app/javascript/controllers/accordion_controller.js +1 -0
  140. data/test/tmp/install_generator/app/javascript/controllers/date_field_controller.js +1 -0
  141. data/test/tmp/install_generator/app/javascript/controllers/details_menu_controller.js +1 -0
  142. data/test/tmp/install_generator/app/javascript/controllers/form_submit_controller.js +1 -0
  143. data/test/tmp/install_generator/app/javascript/controllers/marketing_pricing_controller.js +1 -0
  144. data/test/tmp/install_generator/app/javascript/controllers/marketing_tabs_controller.js +1 -0
  145. data/test/tmp/install_generator/app/javascript/controllers/menu_select_controller.js +1 -0
  146. data/test/tmp/install_generator/app/javascript/controllers/modal_controller.js +1 -0
  147. data/test/tmp/install_generator/app/javascript/controllers/segmented_tabs_controller.js +1 -0
  148. data/test/tmp/install_generator/app/javascript/controllers/sidebar_controller.js +1 -0
  149. data/test/tmp/install_generator/app/javascript/controllers/smooth_scroll_controller.js +1 -0
  150. data/test/tmp/install_generator/app/javascript/controllers/snackbar_controller.js +1 -0
  151. data/test/tmp/install_generator/app/javascript/controllers/theme_controller.js +1 -0
  152. data/test/tmp/install_generator/app/javascript/controllers/tooltip_controller.js +1 -0
  153. data/test/tmp/install_generator/app/javascript/lib/animation-helpers.js +1 -0
  154. data/test/tmp/install_generator/app/javascript/lib/dom-helpers.js +1 -0
  155. data/test/tmp/install_generator/app/javascript/lib/field-validation-helpers.js +1 -0
  156. data/test/tmp/install_generator/app/javascript/lib/focus-management.js +1 -0
  157. data/test/tmp/install_generator/app/javascript/lib/formatting-helpers.js +1 -0
  158. data/test/tmp/install_generator/app/javascript/lib/snackbar.js +1 -0
  159. data/test/tmp/install_generator/app/javascript/lib/storage-helpers.js +1 -0
  160. data/test/tmp/install_generator/config/initializers/baldur.rb +19 -0
  161. data/test/tmp/install_panel_secondary_generator/app/assets/tailwind/application.css +2 -0
  162. data/test/tmp/install_panel_secondary_generator/app/helpers/panel_secondary_helper.rb +3 -0
  163. data/test/tmp/install_panel_secondary_generator/app/javascript/controllers/panel_secondary_controller.js +1 -0
  164. metadata +259 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Animation & Motion Helpers
3
+ * Consistent handling of transitions and timing across controllers
4
+ */
5
+
6
+ // Fetch motion timing from design tokens
7
+ export const getMotionTimings = () => {
8
+ try {
9
+ const computed = window.getComputedStyle(document.documentElement);
10
+ return {
11
+ fadeIn: parseFloat(computed.getPropertyValue("--motion-duration-short3")) || 150,
12
+ fadeOut: parseFloat(computed.getPropertyValue("--motion-duration-short2")) || 75,
13
+ standard: parseFloat(computed.getPropertyValue("--motion-duration-medium1")) || 300
14
+ };
15
+ } catch (error) {
16
+ return { fadeIn: 150, fadeOut: 75, standard: 300 };
17
+ }
18
+ };
19
+
20
+ export const requestFrameCallback = (fn) => {
21
+ return new Promise((resolve) => {
22
+ requestAnimationFrame(() => {
23
+ fn();
24
+ resolve();
25
+ });
26
+ });
27
+ };
28
+
29
+ export const debounceTimeout = (fn, delay) => {
30
+ let timeoutId = null;
31
+ return {
32
+ call: () => {
33
+ clearTimeout(timeoutId);
34
+ timeoutId = setTimeout(fn, delay);
35
+ },
36
+ clear: () => clearTimeout(timeoutId),
37
+ active: () => timeoutId !== null
38
+ };
39
+ };
40
+
41
+ export const smoothScroll = (element, options = {}) => {
42
+ const { behavior = "smooth", block = "nearest" } = options;
43
+ if (element && typeof element.scrollIntoView === "function") {
44
+ element.scrollIntoView({ behavior, block });
45
+ }
46
+ };
47
+
48
+ export const scheduleAnimation = (element, className, duration, cleanup = false) => {
49
+ return new Promise((resolve) => {
50
+ element.classList.add(className);
51
+ const timeout = setTimeout(() => {
52
+ if (cleanup) element.classList.remove(className);
53
+ resolve();
54
+ }, duration);
55
+ });
56
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * DOM Query & Manipulation Helpers
3
+ * Reduces boilerplate and centralizes DOM patterns used across controllers
4
+ */
5
+
6
+ export const queryAll = (selector, root = document) => {
7
+ try {
8
+ return Array.from(root.querySelectorAll(selector));
9
+ } catch (error) {
10
+ console.warn(`Invalid selector: ${selector}`, error);
11
+ return [];
12
+ }
13
+ };
14
+
15
+ export const query = (selector, root = document) => {
16
+ try {
17
+ return root.querySelector(selector);
18
+ } catch (error) {
19
+ console.warn(`Invalid selector: ${selector}`, error);
20
+ return null;
21
+ }
22
+ };
23
+
24
+ export const delegateEvent = (element, eventType, selector, handler) => {
25
+ element.addEventListener(eventType, (event) => {
26
+ const target = event.target.closest(selector);
27
+ if (target && element.contains(target)) {
28
+ handler.call(target, event);
29
+ }
30
+ });
31
+ };
32
+
33
+ export const setAttrs = (element, attrs) => {
34
+ if (!element) return;
35
+ Object.entries(attrs).forEach(([key, value]) => {
36
+ if (value === null || value === undefined) {
37
+ element.removeAttribute(key);
38
+ } else {
39
+ element.setAttribute(key, value);
40
+ }
41
+ });
42
+ };
43
+
44
+ export const toggleClass = (element, className, force) => {
45
+ if (!element) return;
46
+ element.classList.toggle(className, force);
47
+ };
48
+
49
+ export const addClass = (element, ...classes) => {
50
+ if (!element) return;
51
+ element.classList.add(...classes);
52
+ };
53
+
54
+ export const removeClass = (element, ...classes) => {
55
+ if (!element) return;
56
+ element.classList.remove(...classes);
57
+ };
58
+
59
+ export const replaceClass = (element, oldClass, newClass) => {
60
+ if (!element) return;
61
+ removeClass(element, oldClass);
62
+ addClass(element, newClass);
63
+ };
64
+
65
+ export const getDataset = (element, key) => {
66
+ return element?.dataset?.[key];
67
+ };
68
+
69
+ export const setDataset = (element, key, value) => {
70
+ if (!element) return;
71
+ element.dataset[key] = value;
72
+ };
73
+
74
+ export const closest = (element, selector) => {
75
+ return element?.closest(selector) || null;
76
+ };
77
+
78
+ export const matches = (element, selector) => {
79
+ return element?.matches(selector) ?? false;
80
+ };
@@ -0,0 +1,36 @@
1
+ import { query } from "baldur/lib/dom-helpers";
2
+
3
+ export function setFieldValidationMessage(fieldOrWrapper, message) {
4
+ const wrapper = resolveFieldWrapper(fieldOrWrapper);
5
+ if (!wrapper) return;
6
+
7
+ const support = query("[data-field-support]", wrapper);
8
+
9
+ if (message) {
10
+ wrapper.classList.add("is-invalid");
11
+ if (support) {
12
+ support.textContent = message;
13
+ support.removeAttribute("hidden");
14
+ }
15
+ return;
16
+ }
17
+
18
+ wrapper.classList.remove("is-invalid");
19
+ if (!support) return;
20
+
21
+ const defaultText = support.dataset.defaultSupport || "";
22
+ support.textContent = defaultText;
23
+
24
+ if (defaultText.length) {
25
+ support.removeAttribute("hidden");
26
+ } else {
27
+ support.setAttribute("hidden", "hidden");
28
+ }
29
+ }
30
+
31
+ function resolveFieldWrapper(fieldOrWrapper) {
32
+ if (!fieldOrWrapper) return null;
33
+ if (fieldOrWrapper.classList?.contains("field")) return fieldOrWrapper;
34
+ if (typeof fieldOrWrapper.closest !== "function") return null;
35
+ return fieldOrWrapper.closest(".field");
36
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Focus Management & Accessibility Helpers
3
+ * Ensures proper focus traps and ARIA updates across controllers
4
+ */
5
+
6
+ export const focusFirstFocusable = (container) => {
7
+ if (!container) return;
8
+
9
+ // Priority: autofocus > form inputs > buttons > any focusable
10
+ const autofocus = container.querySelector("[data-modal-autofocus]");
11
+ if (autofocus) {
12
+ setTimeout(() => autofocus.focus(), 10);
13
+ return;
14
+ }
15
+
16
+ const focusable = container.querySelector(
17
+ "input, select, textarea, button, [href], [tabindex]"
18
+ );
19
+ if (focusable) {
20
+ setTimeout(() => focusable.focus(), 10);
21
+ }
22
+ };
23
+
24
+ export const createFocusTrap = (element, onEscape) => {
25
+ const focusableElements = element.querySelectorAll(
26
+ "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])"
27
+ );
28
+
29
+ const firstElement = focusableElements[0];
30
+ const lastElement = focusableElements[focusableElements.length - 1];
31
+
32
+ const handleKeyDown = (event) => {
33
+ if (event.key === "Escape" && onEscape) {
34
+ onEscape(event);
35
+ return;
36
+ }
37
+
38
+ if (event.key !== "Tab") return;
39
+
40
+ if (event.shiftKey && document.activeElement === firstElement) {
41
+ event.preventDefault();
42
+ lastElement?.focus();
43
+ } else if (!event.shiftKey && document.activeElement === lastElement) {
44
+ event.preventDefault();
45
+ firstElement?.focus();
46
+ }
47
+ };
48
+
49
+ element.addEventListener("keydown", handleKeyDown);
50
+
51
+ return () => element.removeEventListener("keydown", handleKeyDown);
52
+ };
53
+
54
+ export const updateAriaLabel = (element, label) => {
55
+ if (element) {
56
+ element.setAttribute("aria-label", label);
57
+ }
58
+ };
59
+
60
+ export const updateAriaExpanded = (element, expanded) => {
61
+ if (element) {
62
+ element.setAttribute("aria-expanded", expanded ? "true" : "false");
63
+ }
64
+ };
65
+
66
+ export const updateAriaHidden = (element, hidden) => {
67
+ if (element) {
68
+ element.setAttribute("aria-hidden", hidden ? "true" : "false");
69
+ }
70
+ };
71
+
72
+ export const updateAriaChecked = (element, checked) => {
73
+ if (element) {
74
+ element.setAttribute("aria-checked", checked ? "true" : "false");
75
+ }
76
+ };
77
+
78
+ export const announceToScreenReader = (message, role = "status") => {
79
+ const announcement = document.createElement("div");
80
+ announcement.setAttribute("role", role);
81
+ announcement.setAttribute("aria-live", "polite");
82
+ announcement.setAttribute("aria-atomic", "true");
83
+ announcement.className = "sr-only";
84
+ announcement.textContent = message;
85
+
86
+ document.body.appendChild(announcement);
87
+
88
+ setTimeout(() => announcement.remove(), 3000);
89
+ };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Text Formatting & Validation Helpers
3
+ * Centralized formatting patterns used by multiple controllers
4
+ */
5
+
6
+ const ISO_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
7
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
8
+ const NAME_PATTERN = /^[a-zA-Z\s.'-]+$/;
9
+ const PIN_PATTERN = /^\d{6}$/;
10
+ const PAN_PATTERN = /^[A-Za-z]{5}[0-9]{4}[A-Za-z]$/;
11
+ const UPI_PATTERN = /^[A-Za-z0-9._-]{2,256}@[A-Za-z]{2,64}$/;
12
+
13
+ export const isValidIsoDate = (dateString) => {
14
+ return ISO_DATE_PATTERN.test(dateString);
15
+ };
16
+
17
+ export const parseDisplayDate = (text) => {
18
+ if (!text) return "";
19
+ const trimmed = text.trim();
20
+ if (!trimmed) return "";
21
+
22
+ const isoMatch = ISO_DATE_PATTERN.exec(trimmed);
23
+ if (isoMatch) return isoMatch[0];
24
+
25
+ const parsed = new Date(trimmed);
26
+ if (isNaN(parsed.getTime())) return null;
27
+
28
+ const year = parsed.getFullYear();
29
+ const month = (parsed.getMonth() + 1).toString().padStart(2, "0");
30
+ const day = parsed.getDate().toString().padStart(2, "0");
31
+ return `${year}-${month}-${day}`;
32
+ };
33
+
34
+ export const formatDateForDisplay = (isoValue) => {
35
+ if (!isoValue) return "";
36
+
37
+ const match = ISO_DATE_PATTERN.exec(isoValue);
38
+ if (!match) return isoValue;
39
+
40
+ const year = Number(match[1]);
41
+ const month = Number(match[2]) - 1;
42
+ const day = Number(match[3]);
43
+ const date = new Date(year, month, day);
44
+
45
+ if (isNaN(date.getTime())) return isoValue;
46
+
47
+ try {
48
+ return date.toLocaleDateString(undefined, {
49
+ year: "numeric",
50
+ month: "short",
51
+ day: "numeric"
52
+ });
53
+ } catch (error) {
54
+ return isoValue;
55
+ }
56
+ };
57
+
58
+ export const isValidEmail = (email) => {
59
+ return EMAIL_PATTERN.test(email);
60
+ };
61
+
62
+ export const isValidName = (name) => {
63
+ return NAME_PATTERN.test(name);
64
+ };
65
+
66
+ export const isValidPin = (pin) => {
67
+ return PIN_PATTERN.test(pin.replace(/\D/g, ""));
68
+ };
69
+
70
+ export const isValidPan = (pan) => {
71
+ return PAN_PATTERN.test(String(pan || "").trim());
72
+ };
73
+
74
+ export const isValidUpi = (upiId) => {
75
+ return UPI_PATTERN.test(String(upiId || "").trim());
76
+ };
77
+
78
+ export const extractDigits = (value, maxLength = null) => {
79
+ const digits = (value || "").replace(/\D+/g, "");
80
+ return maxLength ? digits.slice(0, maxLength) : digits;
81
+ };
82
+
83
+ export const formatMobileNumber = (digits, countryCode = "+91") => {
84
+ return digits ? `${countryCode} ${digits}` : "";
85
+ };
86
+
87
+ export const sanitizeField = (value, rule) => {
88
+ const text = String(value || "").trim();
89
+
90
+ switch (rule) {
91
+ case "name":
92
+ return text.replace(/[<>]/g, "").replace(/\s+/g, " ").trim();
93
+ case "email":
94
+ return text.replace(/[<>]/g, "").toLowerCase();
95
+ case "text":
96
+ return text.replace(/[<>]/g, "").replace(/\s+/g, " ").trim();
97
+ default:
98
+ return text;
99
+ }
100
+ };
@@ -0,0 +1,20 @@
1
+ const ICON_PATHS = {
2
+ "check-circle": '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><path d="m9 11 3 3L22 4"></path>',
3
+ "chevron-down": '<path d="m6 9 6 6 6-6"></path>',
4
+ "chevron-left": '<path d="m15 18-6-6 6-6"></path>',
5
+ "chevron-right": '<path d="m9 18 6-6-6-6"></path>',
6
+ "circle-alert": '<circle cx="12" cy="12" r="10"></circle><line x1="12" x2="12" y1="8" y2="12"></line><line x1="12" x2="12.01" y1="16" y2="16"></line>',
7
+ info: '<circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path>',
8
+ "triangle-alert": '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path>'
9
+ };
10
+
11
+ export function iconSvg(name, className = "") {
12
+ const paths = ICON_PATHS[name];
13
+
14
+ if (!paths) {
15
+ throw new Error(`Unknown icon ${name}`);
16
+ }
17
+
18
+ const classAttribute = className ? ` class="${className}"` : "";
19
+ return `<svg${classAttribute} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
20
+ }
@@ -0,0 +1,50 @@
1
+ import { iconSvg } from "baldur/lib/lucide";
2
+
3
+ const SNACKBAR_TITLES = {
4
+ success: "Success",
5
+ error: "Error",
6
+ warning: "Warning",
7
+ notice: "Notice"
8
+ };
9
+
10
+ const SNACKBAR_ICONS = {
11
+ success: "check-circle",
12
+ error: "circle-alert",
13
+ warning: "triangle-alert",
14
+ notice: "info"
15
+ };
16
+
17
+ function ensureStack() {
18
+ return document.querySelector("[data-baldur-snackbar-stack]");
19
+ }
20
+
21
+ function createSnackbarFromTemplate() {
22
+ const template = document.querySelector("[data-baldur-snackbar-template]");
23
+ const element = template?.content?.firstElementChild?.cloneNode(true);
24
+ return element instanceof HTMLElement ? element : null;
25
+ }
26
+
27
+ export function showSnackbar({
28
+ message,
29
+ title,
30
+ variant = "notice",
31
+ dismissLabel = "Dismiss"
32
+ } = {}) {
33
+ if (!message || !document?.body) return;
34
+
35
+ const resolvedVariant = ["success", "error", "warning", "notice"].includes(variant) ? variant : "notice";
36
+ const stack = ensureStack();
37
+ const snackbar = createSnackbarFromTemplate();
38
+ if (!stack || !snackbar) return;
39
+
40
+ const titleText = title || SNACKBAR_TITLES[resolvedVariant] || SNACKBAR_TITLES.notice;
41
+ const iconName = SNACKBAR_ICONS[resolvedVariant] || SNACKBAR_ICONS.notice;
42
+ snackbar.className = `snackbar snackbar--${resolvedVariant}`;
43
+ snackbar.dataset.snackbarVariant = resolvedVariant;
44
+ snackbar.querySelector("[data-snackbar-icon]").innerHTML = iconSvg(iconName, "h-5 w-5");
45
+ snackbar.querySelector("[data-snackbar-label]").textContent = titleText;
46
+ snackbar.querySelector("[data-snackbar-message]").textContent = String(message);
47
+ snackbar.querySelector("[data-snackbar-dismiss]").setAttribute("aria-label", dismissLabel);
48
+
49
+ stack.appendChild(snackbar);
50
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Local Storage Helpers
3
+ * Safe wrappers for localStorage with fallback handling
4
+ */
5
+
6
+ export const getFromStorage = (key, defaultValue = null) => {
7
+ try {
8
+ const value = localStorage.getItem(key);
9
+ return value ?? defaultValue;
10
+ } catch (error) {
11
+ console.warn(`Unable to read from localStorage:`, error);
12
+ return defaultValue;
13
+ }
14
+ };
15
+
16
+ export const saveToStorage = (key, value) => {
17
+ try {
18
+ localStorage.setItem(key, value);
19
+ return true;
20
+ } catch (error) {
21
+ console.warn(`Unable to write to localStorage:`, error);
22
+ return false;
23
+ }
24
+ };
25
+
26
+ export const removeFromStorage = (key) => {
27
+ try {
28
+ localStorage.removeItem(key);
29
+ return true;
30
+ } catch (error) {
31
+ console.warn(`Unable to remove from localStorage:`, error);
32
+ return false;
33
+ }
34
+ };
35
+
36
+ export const getSystemPreference = (preference, options = []) => {
37
+ if (!window.matchMedia) return options[0] || null;
38
+
39
+ try {
40
+ if (preference === "color-scheme") {
41
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
42
+ ? "dark"
43
+ : "light";
44
+ }
45
+ } catch (error) {
46
+ console.warn(`Unable to query system preference:`, error);
47
+ }
48
+
49
+ return options[0] || null;
50
+ };