atomic_view 0.1.9 → 0.1.11
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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/atomic_view/controllers/chip_controller.js +8 -0
- data/app/assets/javascripts/atomic_view/controllers/command_palette_controller.js +96 -0
- data/app/assets/javascripts/atomic_view/controllers/dropdown_controller.js +42 -0
- data/app/assets/javascripts/atomic_view/controllers/modal_controller.js +23 -0
- data/app/assets/javascripts/atomic_view/controllers/theme_toggle_controller.js +45 -0
- data/app/assets/javascripts/atomic_view/controllers/toast_controller.js +20 -0
- data/app/assets/stylesheets/atomic_view/application.tailwind.css +0 -2
- data/app/assets/tailwind/atomic_view/engine.css +91 -28
- data/config/importmap.rb +45 -1
- data/lib/atomic_view/component.rb +1 -0
- data/lib/atomic_view/components/alert_component.erb +15 -0
- data/lib/atomic_view/components/alert_component.rb +63 -0
- data/lib/atomic_view/components/avatar_component.rb +63 -0
- data/lib/atomic_view/components/badge_component.rb +38 -0
- data/lib/atomic_view/components/button_component.rb +2 -23
- data/lib/atomic_view/components/card_component.rb +29 -0
- data/lib/atomic_view/components/chip_component.rb +56 -0
- data/lib/atomic_view/components/collection_check_boxes_component.erb +3 -0
- data/lib/atomic_view/components/collection_check_boxes_component.rb +25 -0
- data/lib/atomic_view/components/collection_radio_buttons_component.erb +3 -0
- data/lib/atomic_view/components/collection_radio_buttons_component.rb +25 -0
- data/lib/atomic_view/components/collection_select_component.erb +25 -0
- data/lib/atomic_view/components/collection_select_component.rb +21 -4
- data/lib/atomic_view/components/color_field_component.rb +3 -0
- data/lib/atomic_view/components/command_palette_component.erb +41 -0
- data/lib/atomic_view/components/command_palette_component.rb +88 -0
- data/lib/atomic_view/components/concerns/button_variants.rb +34 -0
- data/lib/atomic_view/components/concerns/field_chrome.rb +21 -0
- data/lib/atomic_view/components/concerns/section_support.rb +67 -0
- data/lib/atomic_view/components/date_field_component.rb +3 -9
- data/lib/atomic_view/components/date_select_component.rb +9 -0
- data/lib/atomic_view/components/datetime_local_field_component.rb +9 -0
- data/lib/atomic_view/components/datetime_select_component.rb +9 -0
- data/lib/atomic_view/components/dropdown_component.erb +11 -0
- data/lib/atomic_view/components/dropdown_component.rb +57 -0
- data/lib/atomic_view/components/empty_state_component.erb +10 -0
- data/lib/atomic_view/components/empty_state_component.rb +28 -0
- data/lib/atomic_view/components/field_component.erb +3 -3
- data/lib/atomic_view/components/field_component.rb +6 -46
- data/lib/atomic_view/components/grouped_collection_select_component.rb +5 -0
- data/lib/atomic_view/components/kbd_component.rb +31 -0
- data/lib/atomic_view/components/label_component.rb +16 -1
- data/lib/atomic_view/components/modal_component.erb +24 -0
- data/lib/atomic_view/components/modal_component.rb +57 -0
- data/lib/atomic_view/components/month_field_component.rb +9 -0
- data/lib/atomic_view/components/navigation_tree_component/item_component.erb +23 -0
- data/lib/atomic_view/components/navigation_tree_component/item_component.rb +54 -0
- data/lib/atomic_view/components/navigation_tree_component.erb +8 -0
- data/lib/atomic_view/components/navigation_tree_component.rb +40 -0
- data/lib/atomic_view/components/pagination_component.erb +19 -0
- data/lib/atomic_view/components/pagination_component.rb +81 -0
- data/lib/atomic_view/components/radio_button_component.rb +3 -0
- data/lib/atomic_view/components/range_field_component.rb +3 -0
- data/lib/atomic_view/components/segmented_control_component.erb +14 -0
- data/lib/atomic_view/components/segmented_control_component.rb +102 -0
- data/lib/atomic_view/components/select_component.erb +25 -0
- data/lib/atomic_view/components/select_component.rb +21 -7
- data/lib/atomic_view/components/submit_component.rb +2 -23
- data/lib/atomic_view/components/table_component.rb +52 -0
- data/lib/atomic_view/components/tabs_component.erb +5 -0
- data/lib/atomic_view/components/tabs_component.rb +74 -0
- data/lib/atomic_view/components/text_area_component.rb +5 -0
- data/lib/atomic_view/components/theme_toggle_component.rb +74 -0
- data/lib/atomic_view/components/time_select_component.rb +9 -0
- data/lib/atomic_view/components/time_zone_select_component.rb +5 -0
- data/lib/atomic_view/components/timeline_component/item_component.erb +10 -0
- data/lib/atomic_view/components/timeline_component/item_component.rb +50 -0
- data/lib/atomic_view/components/timeline_component.erb +10 -0
- data/lib/atomic_view/components/timeline_component.rb +61 -0
- data/lib/atomic_view/components/toast_component.erb +14 -0
- data/lib/atomic_view/components/toast_component.rb +68 -0
- data/lib/atomic_view/components/week_field_component.rb +9 -0
- data/lib/atomic_view/components/weekday_select_component.rb +5 -0
- data/lib/atomic_view/form_builder.rb +25 -0
- data/lib/atomic_view/version.rb +1 -1
- data/lib/atomic_view.rb +1 -0
- data/lib/generators/atomic_view/install_generator.rb +13 -12
- metadata +62 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 205025ff313e6a3303b0b163adb40754247e189320e839fa949cf6c419fd41a8
|
|
4
|
+
data.tar.gz: 8b62841b297e0bb48e4a536e0dd6d74ac3cff1d1aa956975a547012354900bed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3c79d5697e7eaa4153b7b5b1087f31bc3b3a85032316784329b45ca6c6330f6f706901ffa2d5abf078d4b53d2e3ee2baca1317874fd79d54eac77b74984024e1
|
|
7
|
+
data.tar.gz: f67f35c7dcfc88d43cfd3d716f10bec7ce3c351f556d2c1eb1a3446a8582d85ef0cd4e9af3a2f129507e07d758659267f8253cc6bc6d6622d8e45a8baa844286
|
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
93
|
+
--color-border: var(--color-white);
|
|
94
|
+
--color-input: var(--color-white);
|
|
95
|
+
--color-ring: var(--color-white);
|
|
54
96
|
|
|
55
|
-
|
|
56
|
-
--color-primary-foreground: var(--color-neutral-800);
|
|
97
|
+
--color-foreground: var(--color-neutral-50);
|
|
57
98
|
|
|
58
|
-
|
|
59
|
-
|
|
99
|
+
--color-primary: var(--color-white);
|
|
100
|
+
--color-primary-foreground: var(--color-neutral-800);
|
|
60
101
|
|
|
61
|
-
|
|
62
|
-
|
|
102
|
+
--color-secondary: var(--color-neutral-800);
|
|
103
|
+
--color-secondary-foreground: var(--color-neutral-100);
|
|
63
104
|
|
|
64
|
-
|
|
65
|
-
|
|
105
|
+
--color-muted: var(--color-neutral-800);
|
|
106
|
+
--color-muted-foreground: var(--color-neutral-200);
|
|
66
107
|
|
|
67
|
-
|
|
68
|
-
|
|
108
|
+
--color-accent: var(--color-green-50);
|
|
109
|
+
--color-accent-foreground: var(--color-green-800);
|
|
69
110
|
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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(
|
|
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"
|
|
@@ -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
|