atomic_view 0.1.9 → 0.1.10

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/atomic_view/controllers/chip_controller.js +8 -0
  3. data/app/assets/javascripts/atomic_view/controllers/command_palette_controller.js +96 -0
  4. data/app/assets/javascripts/atomic_view/controllers/dropdown_controller.js +42 -0
  5. data/app/assets/javascripts/atomic_view/controllers/modal_controller.js +23 -0
  6. data/app/assets/javascripts/atomic_view/controllers/theme_toggle_controller.js +45 -0
  7. data/app/assets/javascripts/atomic_view/controllers/toast_controller.js +20 -0
  8. data/app/assets/stylesheets/atomic_view/application.tailwind.css +0 -2
  9. data/app/assets/tailwind/atomic_view/engine.css +91 -28
  10. data/config/importmap.rb +45 -1
  11. data/lib/atomic_view/component.rb +1 -0
  12. data/lib/atomic_view/components/alert_component.erb +15 -0
  13. data/lib/atomic_view/components/alert_component.rb +63 -0
  14. data/lib/atomic_view/components/avatar_component.rb +63 -0
  15. data/lib/atomic_view/components/badge_component.rb +38 -0
  16. data/lib/atomic_view/components/button_component.rb +2 -23
  17. data/lib/atomic_view/components/card_component.rb +29 -0
  18. data/lib/atomic_view/components/chip_component.rb +56 -0
  19. data/lib/atomic_view/components/collection_check_boxes_component.erb +3 -0
  20. data/lib/atomic_view/components/collection_check_boxes_component.rb +25 -0
  21. data/lib/atomic_view/components/collection_radio_buttons_component.erb +3 -0
  22. data/lib/atomic_view/components/collection_radio_buttons_component.rb +25 -0
  23. data/lib/atomic_view/components/collection_select_component.erb +25 -0
  24. data/lib/atomic_view/components/collection_select_component.rb +21 -4
  25. data/lib/atomic_view/components/color_field_component.rb +3 -0
  26. data/lib/atomic_view/components/command_palette_component.erb +41 -0
  27. data/lib/atomic_view/components/command_palette_component.rb +88 -0
  28. data/lib/atomic_view/components/concerns/button_variants.rb +34 -0
  29. data/lib/atomic_view/components/concerns/field_chrome.rb +21 -0
  30. data/lib/atomic_view/components/concerns/section_support.rb +67 -0
  31. data/lib/atomic_view/components/date_field_component.rb +3 -9
  32. data/lib/atomic_view/components/date_select_component.rb +9 -0
  33. data/lib/atomic_view/components/datetime_local_field_component.rb +9 -0
  34. data/lib/atomic_view/components/datetime_select_component.rb +9 -0
  35. data/lib/atomic_view/components/dropdown_component.erb +11 -0
  36. data/lib/atomic_view/components/dropdown_component.rb +57 -0
  37. data/lib/atomic_view/components/empty_state_component.erb +10 -0
  38. data/lib/atomic_view/components/empty_state_component.rb +28 -0
  39. data/lib/atomic_view/components/field_component.erb +3 -3
  40. data/lib/atomic_view/components/field_component.rb +6 -46
  41. data/lib/atomic_view/components/grouped_collection_select_component.rb +5 -0
  42. data/lib/atomic_view/components/kbd_component.rb +31 -0
  43. data/lib/atomic_view/components/label_component.rb +16 -1
  44. data/lib/atomic_view/components/modal_component.erb +24 -0
  45. data/lib/atomic_view/components/modal_component.rb +57 -0
  46. data/lib/atomic_view/components/month_field_component.rb +9 -0
  47. data/lib/atomic_view/components/nav_item_component.erb +6 -0
  48. data/lib/atomic_view/components/nav_item_component.rb +36 -0
  49. data/lib/atomic_view/components/pagination_component.erb +19 -0
  50. data/lib/atomic_view/components/pagination_component.rb +81 -0
  51. data/lib/atomic_view/components/radio_button_component.rb +3 -0
  52. data/lib/atomic_view/components/range_field_component.rb +3 -0
  53. data/lib/atomic_view/components/segmented_control_component.erb +14 -0
  54. data/lib/atomic_view/components/segmented_control_component.rb +102 -0
  55. data/lib/atomic_view/components/select_component.erb +25 -0
  56. data/lib/atomic_view/components/select_component.rb +21 -7
  57. data/lib/atomic_view/components/submit_component.rb +2 -23
  58. data/lib/atomic_view/components/table_component.rb +52 -0
  59. data/lib/atomic_view/components/tabs_component.erb +5 -0
  60. data/lib/atomic_view/components/tabs_component.rb +74 -0
  61. data/lib/atomic_view/components/text_area_component.rb +5 -0
  62. data/lib/atomic_view/components/theme_toggle_component.rb +74 -0
  63. data/lib/atomic_view/components/time_select_component.rb +9 -0
  64. data/lib/atomic_view/components/time_zone_select_component.rb +5 -0
  65. data/lib/atomic_view/components/timeline_component/item_component.erb +10 -0
  66. data/lib/atomic_view/components/timeline_component/item_component.rb +50 -0
  67. data/lib/atomic_view/components/timeline_component.erb +10 -0
  68. data/lib/atomic_view/components/timeline_component.rb +61 -0
  69. data/lib/atomic_view/components/toast_component.erb +14 -0
  70. data/lib/atomic_view/components/toast_component.rb +68 -0
  71. data/lib/atomic_view/components/week_field_component.rb +9 -0
  72. data/lib/atomic_view/components/weekday_select_component.rb +5 -0
  73. data/lib/atomic_view/form_builder.rb +25 -0
  74. data/lib/atomic_view/version.rb +1 -1
  75. data/lib/atomic_view.rb +1 -0
  76. data/lib/generators/atomic_view/install_generator.rb +13 -12
  77. metadata +60 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b817e39386cd3776eac52dc194c42755d0c77dac42c237cc5783d5ed62aea76b
4
- data.tar.gz: 0d20253d17fb2e627dc13dbc125e4e6d136bd8db4cc3bf14b4f3c40c3fec1d2f
3
+ metadata.gz: 8d4e1219dbfa172625d237cc3d8d164e89d5ea07970aba2d3ef7eeff274641b2
4
+ data.tar.gz: d247639eefd01fe823d75ba0dcf38424f2479275a2e1c616c7bfebd54e0e751d
5
5
  SHA512:
6
- metadata.gz: eeb1ce8e2fc53395b14d2e8c4e878d5e548e9fa040b6ab7848a0a33a0c052b361765188c73661c99633ca4a6aa1491b9460528e7a5f83abee12be31c58a55385
7
- data.tar.gz: 86f4e559785caee6915e93c61c6555b56300a5078b123bfdb396f3dde89bcfb2905eab0bf3cc4fa9cd0e6fe7444b8219d275438d16b8b6da30e8f3da6a20af18
6
+ metadata.gz: 0c3c20d67269e91d589827053856626a38c7e03bc649b11a3ff3090f6f94576a2b044e79644926e2b02d13aa23a756c91e1ce34d0e4841d6b08b5e802af1f90d
7
+ data.tar.gz: 2ab75bf246557bc2fcc9db8f09945eea2010bb8f6c2290398b2e4b6507891b5f7c308e917cd79d2b65a8161af835dc99138ca3d4a72b761bd70eeeb096e1a39e
@@ -0,0 +1,8 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ remove() {
5
+ this.element.classList.add("opacity-0", "transition-opacity")
6
+ setTimeout(() => this.element.remove(), 200)
7
+ }
8
+ }
@@ -0,0 +1,96 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["input", "row"]
5
+
6
+ highlightedIndex = null
7
+
8
+ connect() {
9
+ document.addEventListener("keydown", this.onGlobalKeydown)
10
+ this.element.addEventListener("close", this.onClose)
11
+ }
12
+
13
+ disconnect() {
14
+ document.removeEventListener("keydown", this.onGlobalKeydown)
15
+ this.element.removeEventListener("close", this.onClose)
16
+ }
17
+
18
+ onGlobalKeydown = (event) => {
19
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
20
+ event.preventDefault()
21
+ this.element.showModal()
22
+ }
23
+ }
24
+
25
+ onClose = () => {
26
+ this.inputTarget.value = ""
27
+ this.rowTargets.forEach((row) => row.classList.remove("hidden"))
28
+ this.clearHighlight()
29
+ }
30
+
31
+ filter() {
32
+ const query = this.inputTarget.value.trim().toLowerCase()
33
+
34
+ this.rowTargets.forEach((row) => {
35
+ const matches = row.dataset.searchText.includes(query)
36
+ row.classList.toggle("hidden", !matches)
37
+ })
38
+
39
+ this.clearHighlight()
40
+ }
41
+
42
+ navigate(event) {
43
+ if (event.key === "ArrowDown") {
44
+ event.preventDefault()
45
+ this.moveHighlight(1)
46
+ } else if (event.key === "ArrowUp") {
47
+ event.preventDefault()
48
+ this.moveHighlight(-1)
49
+ } else if (event.key === "Enter") {
50
+ this.activateHighlighted(event)
51
+ }
52
+ }
53
+
54
+ moveHighlight(delta) {
55
+ const visibleRows = this.visibleRows
56
+ if (visibleRows.length === 0) return
57
+
58
+ const nextIndex = this.nextIndex(visibleRows.length, delta)
59
+ this.highlight(visibleRows, nextIndex)
60
+ }
61
+
62
+ nextIndex(count, delta) {
63
+ if (this.highlightedIndex === null) return delta === 1 ? 0 : count - 1
64
+ return (this.highlightedIndex + delta + count) % count
65
+ }
66
+
67
+ highlight(visibleRows, index) {
68
+ visibleRows.forEach((row) => row.classList.remove("bg-offset"))
69
+
70
+ this.highlightedIndex = index
71
+ const row = visibleRows[index]
72
+ if (!row) return
73
+
74
+ row.classList.add("bg-offset")
75
+ row.scrollIntoView({ block: "nearest" })
76
+ }
77
+
78
+ activateHighlighted(event) {
79
+ if (this.highlightedIndex === null) return
80
+
81
+ const row = this.visibleRows[this.highlightedIndex]
82
+ if (!row) return
83
+
84
+ event.preventDefault()
85
+ row.click()
86
+ }
87
+
88
+ clearHighlight() {
89
+ this.rowTargets.forEach((row) => row.classList.remove("bg-offset"))
90
+ this.highlightedIndex = null
91
+ }
92
+
93
+ get visibleRows() {
94
+ return this.rowTargets.filter((row) => !row.classList.contains("hidden"))
95
+ }
96
+ }
@@ -0,0 +1,42 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { computePosition, offset, flip, shift, autoUpdate } from "@floating-ui/dom"
3
+
4
+ export default class extends Controller {
5
+ static targets = ["trigger", "menu"]
6
+
7
+ toggle() {
8
+ this.menuTarget.classList.contains("hidden") ? this.open() : this.close()
9
+ }
10
+
11
+ open() {
12
+ this.menuTarget.classList.remove("hidden")
13
+ this.cleanup = autoUpdate(this.triggerTarget, this.menuTarget, () => {
14
+ computePosition(this.triggerTarget, this.menuTarget, {
15
+ middleware: [offset(4), flip(), shift({ padding: 8 })]
16
+ }).then(({ x, y }) => {
17
+ Object.assign(this.menuTarget.style, { left: `${x}px`, top: `${y}px` })
18
+ })
19
+ })
20
+ document.addEventListener("click", this.onOutsideClick)
21
+ document.addEventListener("keydown", this.onKeydown)
22
+ }
23
+
24
+ close() {
25
+ this.menuTarget.classList.add("hidden")
26
+ if (this.cleanup) this.cleanup()
27
+ document.removeEventListener("click", this.onOutsideClick)
28
+ document.removeEventListener("keydown", this.onKeydown)
29
+ }
30
+
31
+ onOutsideClick = (event) => {
32
+ if (!this.element.contains(event.target)) this.close()
33
+ }
34
+
35
+ onKeydown = (event) => {
36
+ if (event.key === "Escape") this.close()
37
+ }
38
+
39
+ disconnect() {
40
+ this.close()
41
+ }
42
+ }
@@ -0,0 +1,23 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ open() {
5
+ this.element.showModal()
6
+ }
7
+
8
+ close() {
9
+ this.element.close()
10
+ }
11
+
12
+ closeOnBackdrop(event) {
13
+ if (event.target === this.element) this.close()
14
+ }
15
+
16
+ connect() {
17
+ this.element.addEventListener("click", this.closeOnBackdrop.bind(this))
18
+ }
19
+
20
+ disconnect() {
21
+ this.element.removeEventListener("click", this.closeOnBackdrop.bind(this))
22
+ }
23
+ }
@@ -0,0 +1,45 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ const STORAGE_KEY = "atomic-view-theme"
4
+
5
+ export default class extends Controller {
6
+ connect() {
7
+ this.apply(this.resolved())
8
+ }
9
+
10
+ toggle() {
11
+ this.apply(this.isDark ? "light" : "dark")
12
+ }
13
+
14
+ apply(theme) {
15
+ document.documentElement.classList.toggle("dark", theme === "dark")
16
+
17
+ try {
18
+ localStorage.setItem(STORAGE_KEY, theme)
19
+ } catch {
20
+ // localStorage unavailable (private browsing, etc.) -- the toggle
21
+ // still works for the current page load, it just won't persist.
22
+ }
23
+ }
24
+
25
+ resolved() {
26
+ return this.stored() ?? (this.systemPrefersDark() ? "dark" : "light")
27
+ }
28
+
29
+ stored() {
30
+ try {
31
+ const value = localStorage.getItem(STORAGE_KEY)
32
+ return value === "light" || value === "dark" ? value : null
33
+ } catch {
34
+ return null
35
+ }
36
+ }
37
+
38
+ systemPrefersDark() {
39
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false
40
+ }
41
+
42
+ get isDark() {
43
+ return document.documentElement.classList.contains("dark")
44
+ }
45
+ }
@@ -0,0 +1,20 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = { autoDismissMs: Number }
5
+
6
+ connect() {
7
+ if (this.autoDismissMsValue > 0) {
8
+ this.timeout = setTimeout(() => this.dismiss(), this.autoDismissMsValue)
9
+ }
10
+ }
11
+
12
+ disconnect() {
13
+ if (this.timeout) clearTimeout(this.timeout)
14
+ }
15
+
16
+ dismiss() {
17
+ this.element.classList.add("opacity-0", "transition-opacity")
18
+ setTimeout(() => this.element.remove(), 200)
19
+ }
20
+ }
@@ -5,8 +5,6 @@
5
5
  @plugin "@tailwindcss/forms";
6
6
  @plugin "@tailwindcss/typography";
7
7
 
8
- @custom-variant dark (&:where(.dark, .dark *));
9
-
10
8
  /*
11
9
  The default border color has changed to `currentColor` in Tailwind CSS v4,
12
10
  so we've added these compatibility styles to make sure everything still
@@ -1,11 +1,17 @@
1
1
  @import "tailwindcss" source(none);
2
+ @custom-variant dark (&:where(.dark, .dark *));
2
3
  @source "../../../views/**/*.{erb}";
3
4
  @source "../../../../lib/atomic_view/components/**/*.{erb,rb}";
5
+ @source "../../../../previews/**/*.{erb,rb}";
4
6
 
5
7
  @plugin "@tailwindcss/forms";
6
8
  @plugin "@tailwindcss/typography";
7
9
 
8
10
  @theme {
11
+ /* --color-offset = page canvas background
12
+ --color-surface = card/panel background
13
+ (Named "canvas"/"surface" in some design specs — kept as offset/surface
14
+ here since renaming would be a breaking change across every component.) */
9
15
  --color-surface: var(--color-white);
10
16
  --color-offset: var(--color-gray-50);
11
17
  --color-backdrop: oklch(0 0 0 / 0.5);
@@ -14,6 +20,11 @@
14
20
  --color-input: var(--color-gray-200);
15
21
  --color-ring: var(--color-gray-950);
16
22
 
23
+ --color-foreground: var(--color-neutral-900);
24
+
25
+ /* --color-primary is the brand action color (buttons, links, active states).
26
+ Use --color-foreground for body/label/input text — do not reuse
27
+ --color-primary for text color. */
17
28
  --color-primary: var(--color-neutral-800);
18
29
  --color-primary-foreground: var(--color-white);
19
30
 
@@ -29,6 +40,15 @@
29
40
  --color-destructive: var(--color-red-800);
30
41
  --color-destructive-foreground: var(--color-red-50);
31
42
 
43
+ --color-success: var(--color-emerald-800);
44
+ --color-success-foreground: var(--color-emerald-50);
45
+
46
+ --color-warning: var(--color-amber-800);
47
+ --color-warning-foreground: var(--color-amber-50);
48
+
49
+ --color-info: var(--color-blue-800);
50
+ --color-info-foreground: var(--color-blue-50);
51
+
32
52
  --color-disabled: var(--color-gray-50);
33
53
  --color-disabled-foreground: var(--color-zinc-500);
34
54
  --color-disabled-ring: var(--color-zinc-200);
@@ -40,45 +60,77 @@
40
60
 
41
61
  --color-placeholder: var(--color-zinc-500);
42
62
  --color-focus-ring: var(--color-neutral-700);
63
+
64
+ --radius-well: 4px;
65
+ --radius-btn: 6px;
66
+ --radius-card: 8px;
67
+ --radius-pill: 9999px;
68
+
69
+ --shadow-soft: 0 1px 2px rgb(0 0 0 / .06), 0 8px 24px -8px rgb(0 0 0 / .18);
70
+ --shadow-panel: 0 20px 40px -20px rgb(0 0 0 / .22);
71
+
72
+ /* Generic fallback stacks only — no brand fonts shipped by the gem.
73
+ A host app supplies its own @font-face/Google Fonts <link> and overrides
74
+ these three variables, e.g.:
75
+ --font-display: "Bitter", serif;
76
+ --font-body: "Manrope", sans-serif;
77
+ --font-mono: "JetBrains Mono", monospace; */
78
+ --font-display: ui-serif, Georgia, serif;
79
+ --font-body: ui-sans-serif, system-ui, sans-serif;
80
+ --font-mono: ui-monospace, monospace;
43
81
  }
44
82
 
45
- @media (prefers-color-scheme: dark) {
46
- :root {
47
- --color-surface: var(--color-neutral-950);
48
- --color-offset: var(--color-neutral-900);
49
- --color-backdrop: oklch(0 0 0 / 0.5);
83
+ /* Explicit .dark class toggle only — no OS-preference auto-switching.
84
+ Host apps wanting OS-driven theming should flip the .dark class via a
85
+ small inline script keyed off matchMedia('(prefers-color-scheme: dark)'),
86
+ the same pattern Lookbook's own toggle uses
87
+ (test/dummy/app/views/layouts/application.html.erb). */
88
+ :root.dark {
89
+ --color-surface: var(--color-neutral-950);
90
+ --color-offset: var(--color-neutral-900);
91
+ --color-backdrop: oklch(0 0 0 / 0.5);
50
92
 
51
- --color-border: var(--color-white);
52
- --color-input: var(--color-white);
53
- --color-ring: var(--color-white);
93
+ --color-border: var(--color-white);
94
+ --color-input: var(--color-white);
95
+ --color-ring: var(--color-white);
54
96
 
55
- --color-primary: var(--color-white);
56
- --color-primary-foreground: var(--color-neutral-800);
97
+ --color-foreground: var(--color-neutral-50);
57
98
 
58
- --color-secondary: var(--color-neutral-800);
59
- --color-secondary-foreground: var(--color-neutral-100);
99
+ --color-primary: var(--color-white);
100
+ --color-primary-foreground: var(--color-neutral-800);
60
101
 
61
- --color-muted: var(--color-neutral-800);
62
- --color-muted-foreground: var(--color-neutral-200);
102
+ --color-secondary: var(--color-neutral-800);
103
+ --color-secondary-foreground: var(--color-neutral-100);
63
104
 
64
- --color-accent: var(--color-green-50);
65
- --color-accent-foreground: var(--color-green-800);
105
+ --color-muted: var(--color-neutral-800);
106
+ --color-muted-foreground: var(--color-neutral-200);
66
107
 
67
- --color-destructive: var(--color-red-800);
68
- --color-destructive-foreground: var(--color-red-50);
108
+ --color-accent: var(--color-green-50);
109
+ --color-accent-foreground: var(--color-green-800);
69
110
 
70
- --color-disabled: var(--color-neutral-800);
71
- --color-disabled-foreground: var(--color-neutral-400);
72
- --color-disabled-ring: var(--color-neutral-600);
111
+ --color-destructive: var(--color-red-800);
112
+ --color-destructive-foreground: var(--color-red-50);
73
113
 
74
- --color-error: var(--color-red-100);
75
- --color-error-ring: var(--color-red-400);
76
- --color-error-placeholder: var(--color-red-400);
77
- --color-error-focus-ring: var(--color-red-500);
114
+ --color-success: var(--color-emerald-50);
115
+ --color-success-foreground: var(--color-emerald-800);
78
116
 
79
- --color-placeholder: var(--color-neutral-400);
80
- --color-focus-ring: var(--color-white/20);
81
- }
117
+ --color-warning: var(--color-amber-50);
118
+ --color-warning-foreground: var(--color-amber-800);
119
+
120
+ --color-info: var(--color-blue-50);
121
+ --color-info-foreground: var(--color-blue-800);
122
+
123
+ --color-disabled: var(--color-neutral-800);
124
+ --color-disabled-foreground: var(--color-neutral-400);
125
+ --color-disabled-ring: var(--color-neutral-600);
126
+
127
+ --color-error: var(--color-red-100);
128
+ --color-error-ring: var(--color-red-400);
129
+ --color-error-placeholder: var(--color-red-400);
130
+ --color-error-focus-ring: var(--color-red-500);
131
+
132
+ --color-placeholder: var(--color-neutral-400);
133
+ --color-focus-ring: color-mix(in oklab, var(--color-white) 20%, transparent);
82
134
  }
83
135
 
84
136
  @layer base {
@@ -86,4 +138,15 @@
86
138
  [role="button"]:not(:disabled) {
87
139
  cursor: pointer;
88
140
  }
141
+
142
+ /* Hides the browser's native clear ("x") button on type="search" inputs
143
+ -- Tailwind's appearance-none doesn't reach this WebKit/Firefox-only
144
+ pseudo-element, so it has to be targeted directly. */
145
+ input[type="search"]::-webkit-search-cancel-button {
146
+ appearance: none;
147
+ }
148
+
149
+ input[type="search"] {
150
+ appearance: textfield;
151
+ }
89
152
  }
data/config/importmap.rb CHANGED
@@ -1,3 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- pin_all_from File.expand_path('../app/assets/javascripts', __dir__)
3
+ pin_all_from File.expand_path("../app/assets/javascripts", __dir__)
4
+
5
+ # Also pinned under "controllers/atomic_view" (Stimulus' own subfolder
6
+ # namespacing rule: each "/" in a controller's path becomes "--" in its
7
+ # identifier) so a host app's own `eagerLoadControllersFrom("controllers",
8
+ # application)` -- boilerplate every Rails + importmap + Stimulus app
9
+ # already has, generated by stimulus-rails' install task -- discovers and
10
+ # registers these controllers automatically, no host app setup required.
11
+ # "controllers/atomic_view/toast_controller" -> "atomic-view--toast",
12
+ # matching every atomic_view component's `data-controller` attribute.
13
+ #
14
+ # `to:` is required here alongside `under:` -- without it, `under:` doubles
15
+ # as both the import name prefix *and* the on-disk asset path prefix (they
16
+ # only look decoupled below because `to:` makes them so), so importmap-rails
17
+ # would look for the files at "controllers/atomic_view/*.js" (which doesn't
18
+ # exist) instead of their real location, silently drop every one as an
19
+ # unresolvable asset, and this pin would do nothing.
20
+ #
21
+ # The plain "atomic_view/controllers/<name>_controller" alias from the pin
22
+ # above still works too, for a host app that wants to `import` and
23
+ # `application.register(...)` a controller by hand instead.
24
+ pin_all_from File.expand_path("../app/assets/javascripts/atomic_view/controllers", __dir__),
25
+ under: "controllers/atomic_view", to: "atomic_view/controllers"
26
+
27
+ # `bin/importmap pin @floating-ui/dom` failed (the jspm.io generate API used
28
+ # by importmap-rails returned a 500), so this is pinned manually to
29
+ # jsDelivr's `+esm` bundle -- unlike jspm's `ga.jspm.io` build, jsDelivr's
30
+ # `+esm` output resolves its `@floating-ui/core`/`@floating-ui/utils`
31
+ # imports as same-origin absolute paths rather than bare specifiers, so it
32
+ # works as a single self-contained pin with no extra import map entries.
33
+ # Used by `dropdown_controller.js`.
34
+ pin "@floating-ui/dom", to: "https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.8.0/+esm"
35
+
36
+ # `bin/importmap pin local-time` vendors the package locally under
37
+ # "vendor/javascript" -- which works for a real Rails app, but this engine's
38
+ # `atomic_view.precompile` initializer (see `engine.rb`) never adds
39
+ # "vendor/javascript" to `app.config.assets.paths`, so a host app can't
40
+ # actually serve the vendored file. Pinned to jsDelivr's `+esm` bundle
41
+ # instead, same fix as `@floating-ui/dom` above. Used by `ItemComponent`'s
42
+ # `local_time_ago` calls (see `TimelineComponent`'s class docs) -- the host
43
+ # app still needs to `import LocalTime from "local-time"; LocalTime.start()`
44
+ # per local_time's own Importmap install docs for times to localize
45
+ # client-side; without that the server-rendered UTC fallback text just
46
+ # stays as-is.
47
+ pin "local-time", to: "https://cdn.jsdelivr.net/npm/local-time@3.0.3/+esm"
@@ -3,6 +3,7 @@
3
3
  module AtomicView
4
4
  class Component < ViewComponent::Base
5
5
  include Heroicons::Engine.helpers
6
+ include LocalTimeHelper
6
7
 
7
8
  def icon(name, variant: Heroicons.configuration.variant, options: {}, path_options: {})
8
9
  Heroicons::Icon.render(
@@ -0,0 +1,15 @@
1
+ <%= tag.div(**html_options, class: html_class, role: "alert") do %>
2
+ <div class="flex">
3
+ <div class="shrink-0">
4
+ <%= icon(icon_name, options: {class: icon_class}).to_s.html_safe %>
5
+ </div>
6
+ <div class="ml-2 min-w-0 flex-1">
7
+ <% if title.present? %>
8
+ <p class="text-sm font-medium <%= text_class %>"><%= title %></p>
9
+ <% end %>
10
+ <div class="<%= content_class %>">
11
+ <%= content %>
12
+ </div>
13
+ </div>
14
+ </div>
15
+ <% end %>
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ # Alert
6
+ #
7
+ # A persistent, inline banner for messages that belong in the page flow
8
+ # rather than a transient corner notification -- form-level validation
9
+ # summaries, page banners, billing warnings, etc. For a self-dismissing
10
+ # notification instead, see ToastComponent.
11
+ class AlertComponent < AtomicView::Component
12
+ attr_reader :variant, :title
13
+
14
+ VARIANTS = {
15
+ info: {icon: "information-circle", classes: "border-info bg-info/10", text_class: "text-info"},
16
+ success: {icon: "check-circle", classes: "border-success bg-success/10", text_class: "text-success"},
17
+ warning: {icon: "exclamation-triangle", classes: "border-warning bg-warning/10", text_class: "text-warning"},
18
+ error: {icon: "x-circle", classes: "border-destructive bg-destructive/10", text_class: "text-destructive"}
19
+ }.freeze
20
+
21
+ def initialize(variant: :info, title: nil, **options)
22
+ super()
23
+ @variant = variant
24
+ @title = title
25
+ @options = options
26
+ end
27
+
28
+ def html_options
29
+ @options.except(:class)
30
+ end
31
+
32
+ def html_class
33
+ class_names(base_classes, variant_config[:classes], @options[:class])
34
+ end
35
+
36
+ def icon_name
37
+ variant_config[:icon]
38
+ end
39
+
40
+ def icon_class
41
+ class_names("size-5 shrink-0", text_class)
42
+ end
43
+
44
+ def text_class
45
+ variant_config[:text_class]
46
+ end
47
+
48
+ def content_class
49
+ class_names("text-sm", text_class, "mt-1" => title.present?)
50
+ end
51
+
52
+ private
53
+
54
+ def variant_config
55
+ VARIANTS.fetch(variant, VARIANTS[:info])
56
+ end
57
+
58
+ def base_classes
59
+ "border-l-4 p-3"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ class AvatarComponent < AtomicView::Component
6
+ COLOR_PALETTE = [
7
+ "bg-[#264653] text-white",
8
+ "bg-[#2a9d8f] text-black",
9
+ "bg-[#e9c46a] text-black",
10
+ "bg-[#f4a261] text-black",
11
+ "bg-[#e76f51] text-black"
12
+ ].freeze
13
+
14
+ attr_reader :src, :initials, :size
15
+
16
+ def initialize(src: nil, initials: nil, size: :md, **options)
17
+ super()
18
+ @src = src
19
+ @initials = initials
20
+ @size = size
21
+ @options = options
22
+ end
23
+
24
+ def call
25
+ if src.present?
26
+ tag.img(**@options.except(:class), class: class_names("rounded-full object-cover", size_classes, @options[:class]), src: src)
27
+ else
28
+ tag.span(**@options.except(:class), class: class_names("flex items-center justify-center rounded-full font-medium", color_classes, size_classes, text_size_classes, @options[:class])) { initials }
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def color_classes
35
+ return "bg-offset text-foreground" if initials.blank?
36
+
37
+ COLOR_PALETTE[initials.sum % COLOR_PALETTE.length]
38
+ end
39
+
40
+ def size_classes
41
+ case size
42
+ when :sm
43
+ "size-6"
44
+ when :lg
45
+ "size-10"
46
+ else
47
+ "size-8"
48
+ end
49
+ end
50
+
51
+ def text_size_classes
52
+ case size
53
+ when :sm
54
+ "text-xs"
55
+ when :lg
56
+ "text-base"
57
+ else
58
+ "text-sm"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtomicView
4
+ module Components
5
+ class BadgeComponent < AtomicView::Component
6
+ attr_reader :variant
7
+
8
+ def initialize(variant: :default, **options)
9
+ super()
10
+ @variant = variant
11
+ @options = options
12
+ end
13
+
14
+ def call
15
+ tag.span(**@options.except(:class), class: class_names(base_classes, variant_classes, @options[:class])) { content }
16
+ end
17
+
18
+ private
19
+
20
+ def base_classes
21
+ "inline-flex items-center rounded-pill px-1.5 py-0.5 text-xs font-medium"
22
+ end
23
+
24
+ def variant_classes
25
+ case variant
26
+ when :secondary
27
+ "bg-secondary text-secondary-foreground"
28
+ when :destructive
29
+ "bg-destructive text-destructive-foreground"
30
+ when :outline
31
+ "bg-transparent border border-border text-foreground"
32
+ else
33
+ "bg-primary text-primary-foreground"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end