keystone_ui 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/app/assets/javascripts/keystone_ui/accordion_controller.js +21 -0
- data/app/assets/javascripts/keystone_ui/auto_submit_controller.js +7 -0
- data/app/assets/javascripts/keystone_ui/chartjs.js +24 -0
- data/app/assets/javascripts/keystone_ui/clipboard_controller.js +25 -0
- data/app/assets/javascripts/keystone_ui/color_picker_controller.js +158 -0
- data/app/assets/javascripts/keystone_ui/column_picker_controller.js +47 -0
- data/app/assets/javascripts/keystone_ui/dismiss_controller.js +7 -0
- data/app/assets/javascripts/keystone_ui/dropdown_controller.js +16 -0
- data/app/assets/javascripts/keystone_ui/file_upload_controller.js +61 -0
- data/app/assets/javascripts/keystone_ui/index.js +33 -0
- data/app/assets/javascripts/keystone_ui/line_chart_controller.js +38 -0
- data/app/assets/javascripts/keystone_ui/modal_controller.js +19 -0
- data/app/assets/javascripts/keystone_ui/multi_select_controller.js +35 -0
- data/app/assets/javascripts/keystone_ui/stat_card_info_controller.js +9 -0
- data/app/assets/javascripts/keystone_ui/swipe_deck_controller.js +123 -0
- data/app/assets/javascripts/keystone_ui/tab_switcher_controller.js +19 -0
- data/app/assets/tailwind/keystone_ui_engine/color_picker.css +31 -0
- data/app/assets/tailwind/keystone_ui_engine/engine.css +7 -0
- data/app/assets/tailwind/keystone_ui_engine/nav.css +175 -0
- data/app/assets/tailwind/keystone_ui_engine/theme.css +32 -0
- data/app/components/keystone/ui/accordion_component.html.erb +19 -0
- data/app/components/keystone/ui/accordion_component.rb +51 -0
- data/app/components/keystone/ui/alert_component.html.erb +15 -0
- data/app/components/keystone/ui/alert_component.rb +58 -0
- data/app/components/keystone/ui/badge_component.html.erb +1 -0
- data/app/components/keystone/ui/badge_component.rb +28 -0
- data/app/components/keystone/ui/bottom_nav_component.html.erb +3 -0
- data/app/components/keystone/ui/bottom_nav_component.rb +13 -0
- data/app/components/keystone/ui/bottom_nav_item_component.html.erb +4 -0
- data/app/components/keystone/ui/bottom_nav_item_component.rb +24 -0
- data/app/components/keystone/ui/button_component.html.erb +1 -0
- data/app/components/keystone/ui/button_component.rb +57 -0
- data/app/components/keystone/ui/card_component.html.erb +9 -0
- data/app/components/keystone/ui/card_component.rb +33 -0
- data/app/components/keystone/ui/card_link_component.html.erb +3 -0
- data/app/components/keystone/ui/card_link_component.rb +25 -0
- data/app/components/keystone/ui/chart_card_component.html.erb +6 -0
- data/app/components/keystone/ui/chart_card_component.rb +35 -0
- data/app/components/keystone/ui/code_component.html.erb +6 -0
- data/app/components/keystone/ui/code_component.rb +40 -0
- data/app/components/keystone/ui/color_picker_component.html.erb +37 -0
- data/app/components/keystone/ui/color_picker_component.rb +31 -0
- data/app/components/keystone/ui/column.rb +21 -0
- data/app/components/keystone/ui/column_picker_component.html.erb +20 -0
- data/app/components/keystone/ui/column_picker_component.rb +35 -0
- data/app/components/keystone/ui/copy_button_component.html.erb +11 -0
- data/app/components/keystone/ui/copy_button_component.rb +39 -0
- data/app/components/keystone/ui/cta_banner_component.html.erb +9 -0
- data/app/components/keystone/ui/cta_banner_component.rb +39 -0
- data/app/components/keystone/ui/data_table_component.html.erb +54 -0
- data/app/components/keystone/ui/data_table_component.rb +173 -0
- data/app/components/keystone/ui/disclosure_component.html.erb +9 -0
- data/app/components/keystone/ui/disclosure_component.rb +46 -0
- data/app/components/keystone/ui/feature_grid_component.html.erb +17 -0
- data/app/components/keystone/ui/feature_grid_component.rb +55 -0
- data/app/components/keystone/ui/file_upload_component.html.erb +18 -0
- data/app/components/keystone/ui/file_upload_component.rb +85 -0
- data/app/components/keystone/ui/form_component.html.erb +3 -0
- data/app/components/keystone/ui/form_component.rb +27 -0
- data/app/components/keystone/ui/form_field_component.html.erb +36 -0
- data/app/components/keystone/ui/form_field_component.rb +83 -0
- data/app/components/keystone/ui/form_page_component.html.erb +10 -0
- data/app/components/keystone/ui/form_page_component.rb +21 -0
- data/app/components/keystone/ui/funnel_component.html.erb +14 -0
- data/app/components/keystone/ui/funnel_component.rb +85 -0
- data/app/components/keystone/ui/grid_component.html.erb +3 -0
- data/app/components/keystone/ui/grid_component.rb +38 -0
- data/app/components/keystone/ui/hero_component.html.erb +27 -0
- data/app/components/keystone/ui/hero_component.rb +74 -0
- data/app/components/keystone/ui/input_component.html.erb +1 -0
- data/app/components/keystone/ui/input_component.rb +57 -0
- data/app/components/keystone/ui/line_chart_component.html.erb +3 -0
- data/app/components/keystone/ui/line_chart_component.rb +48 -0
- data/app/components/keystone/ui/mobile_actions_component.html.erb +11 -0
- data/app/components/keystone/ui/mobile_actions_component.rb +21 -0
- data/app/components/keystone/ui/mobile_header_component.html.erb +11 -0
- data/app/components/keystone/ui/mobile_header_component.rb +38 -0
- data/app/components/keystone/ui/modal_component.html.erb +13 -0
- data/app/components/keystone/ui/modal_component.rb +68 -0
- data/app/components/keystone/ui/multi_select_component.html.erb +21 -0
- data/app/components/keystone/ui/multi_select_component.rb +40 -0
- data/app/components/keystone/ui/nav_dropdown_component.html.erb +11 -0
- data/app/components/keystone/ui/nav_dropdown_component.rb +33 -0
- data/app/components/keystone/ui/nav_item_component.html.erb +1 -0
- data/app/components/keystone/ui/nav_item_component.rb +21 -0
- data/app/components/keystone/ui/navbar_component.html.erb +34 -0
- data/app/components/keystone/ui/navbar_component.rb +32 -0
- data/app/components/keystone/ui/option_card_component.html.erb +9 -0
- data/app/components/keystone/ui/option_card_component.rb +28 -0
- data/app/components/keystone/ui/page_component.html.erb +3 -0
- data/app/components/keystone/ui/page_component.rb +40 -0
- data/app/components/keystone/ui/page_header_component.html.erb +17 -0
- data/app/components/keystone/ui/page_header_component.rb +42 -0
- data/app/components/keystone/ui/panel_component.html.erb +3 -0
- data/app/components/keystone/ui/panel_component.rb +25 -0
- data/app/components/keystone/ui/pipeline_component.html.erb +34 -0
- data/app/components/keystone/ui/pipeline_component.rb +48 -0
- data/app/components/keystone/ui/progress_component.html.erb +8 -0
- data/app/components/keystone/ui/progress_component.rb +35 -0
- data/app/components/keystone/ui/radio_card_component.html.erb +9 -0
- data/app/components/keystone/ui/radio_card_component.rb +42 -0
- data/app/components/keystone/ui/section_component.html.erb +18 -0
- data/app/components/keystone/ui/section_component.rb +32 -0
- data/app/components/keystone/ui/select_component.html.erb +8 -0
- data/app/components/keystone/ui/select_component.rb +38 -0
- data/app/components/keystone/ui/settings_link_component.html.erb +4 -0
- data/app/components/keystone/ui/settings_link_component.rb +26 -0
- data/app/components/keystone/ui/show_page_component.html.erb +4 -0
- data/app/components/keystone/ui/show_page_component.rb +21 -0
- data/app/components/keystone/ui/stat_card_component.html.erb +22 -0
- data/app/components/keystone/ui/stat_card_component.rb +92 -0
- data/app/components/keystone/ui/swipe_deck_component.html.erb +39 -0
- data/app/components/keystone/ui/swipe_deck_component.rb +52 -0
- data/app/components/keystone/ui/tab_switcher_component.html.erb +14 -0
- data/app/components/keystone/ui/tab_switcher_component.rb +33 -0
- data/app/components/keystone/ui/textarea_component.html.erb +1 -0
- data/app/components/keystone/ui/textarea_component.rb +38 -0
- data/app/helpers/keystone_ui_helper.rb +195 -0
- data/config/importmap.rb +19 -0
- data/lib/generators/keystone/install_generator.rb +90 -0
- data/lib/keystone_ui/configuration.rb +24 -0
- data/lib/keystone_ui/engine.rb +47 -0
- data/lib/keystone_ui/safelist.rb +106 -0
- data/lib/keystone_ui/version.rb +5 -0
- data/lib/keystone_ui.rb +9 -0
- data/the_local/agents/keystone_ui-develop.md +297 -0
- data/the_local/agents/keystone_ui-info.md +81 -0
- data/the_local/agents/keystone_ui-install.md +137 -0
- data/the_local/interface.yml +110 -0
- metadata +203 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = {
|
|
5
|
+
successMessage: { type: String, default: "Copied!" },
|
|
6
|
+
errorMessage: { type: String, default: "Failed!" }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async copy() {
|
|
10
|
+
const text = this.element.dataset.clipboardText
|
|
11
|
+
try {
|
|
12
|
+
await navigator.clipboard.writeText(text)
|
|
13
|
+
this.flash(this.successMessageValue)
|
|
14
|
+
} catch {
|
|
15
|
+
this.flash(this.errorMessageValue)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
flash(message) {
|
|
20
|
+
const label = this.element.querySelector("span") || this.element.lastChild
|
|
21
|
+
const original = label.textContent
|
|
22
|
+
label.textContent = message
|
|
23
|
+
setTimeout(() => { label.textContent = original }, 1500)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["swatch", "hexLabel", "input", "panel", "svArea", "svGradient", "svCursor", "hueSlider"]
|
|
5
|
+
static values = { value: String }
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
this.hue = 0
|
|
9
|
+
this.sat = 1
|
|
10
|
+
this.val = 1
|
|
11
|
+
this.dragging = false
|
|
12
|
+
|
|
13
|
+
if (this.valueValue) {
|
|
14
|
+
this.setFromHex(this.valueValue)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this.boundPointerMove = this.svMove.bind(this)
|
|
18
|
+
this.boundPointerUp = this.svUp.bind(this)
|
|
19
|
+
this.boundClickOutside = this.clickOutside.bind(this)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
disconnect() {
|
|
23
|
+
document.removeEventListener("pointermove", this.boundPointerMove)
|
|
24
|
+
document.removeEventListener("pointerup", this.boundPointerUp)
|
|
25
|
+
document.removeEventListener("click", this.boundClickOutside)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
toggle(event) {
|
|
29
|
+
event.stopPropagation()
|
|
30
|
+
const panel = this.panelTarget
|
|
31
|
+
const isHidden = panel.classList.contains("hidden")
|
|
32
|
+
|
|
33
|
+
if (isHidden) {
|
|
34
|
+
panel.classList.remove("hidden")
|
|
35
|
+
document.addEventListener("click", this.boundClickOutside)
|
|
36
|
+
} else {
|
|
37
|
+
this.close()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
close() {
|
|
42
|
+
this.panelTarget.classList.add("hidden")
|
|
43
|
+
document.removeEventListener("click", this.boundClickOutside)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
clickOutside(event) {
|
|
47
|
+
if (!this.element.contains(event.target)) {
|
|
48
|
+
this.close()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
hueChanged() {
|
|
53
|
+
this.hue = parseInt(this.hueSliderTarget.value)
|
|
54
|
+
this.updateSvGradient()
|
|
55
|
+
this.updateColor()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
svDown(event) {
|
|
59
|
+
event.preventDefault()
|
|
60
|
+
this.dragging = true
|
|
61
|
+
document.addEventListener("pointermove", this.boundPointerMove)
|
|
62
|
+
document.addEventListener("pointerup", this.boundPointerUp)
|
|
63
|
+
this.updateSvFromPointer(event)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
svMove(event) {
|
|
67
|
+
if (!this.dragging) return
|
|
68
|
+
event.preventDefault()
|
|
69
|
+
this.updateSvFromPointer(event)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
svUp() {
|
|
73
|
+
this.dragging = false
|
|
74
|
+
document.removeEventListener("pointermove", this.boundPointerMove)
|
|
75
|
+
document.removeEventListener("pointerup", this.boundPointerUp)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
updateSvFromPointer(event) {
|
|
79
|
+
const rect = this.svAreaTarget.getBoundingClientRect()
|
|
80
|
+
const x = Math.max(0, Math.min(event.clientX - rect.left, rect.width))
|
|
81
|
+
const y = Math.max(0, Math.min(event.clientY - rect.top, rect.height))
|
|
82
|
+
|
|
83
|
+
this.sat = x / rect.width
|
|
84
|
+
this.val = 1 - y / rect.height
|
|
85
|
+
|
|
86
|
+
this.svCursorTarget.style.left = `${(this.sat * 100)}%`
|
|
87
|
+
this.svCursorTarget.style.top = `${((1 - this.val) * 100)}%`
|
|
88
|
+
|
|
89
|
+
this.updateColor()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
updateColor() {
|
|
93
|
+
const hex = this.hsvToHex(this.hue, this.sat, this.val)
|
|
94
|
+
this.swatchTarget.style.backgroundColor = hex
|
|
95
|
+
this.hexLabelTarget.textContent = hex
|
|
96
|
+
this.inputTarget.value = hex
|
|
97
|
+
|
|
98
|
+
this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
updateSvGradient() {
|
|
102
|
+
const hueColor = `hsl(${this.hue}, 100%, 50%)`
|
|
103
|
+
this.svGradientTarget.style.background = `linear-gradient(to right, #fff, ${hueColor})`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setFromHex(hex) {
|
|
107
|
+
const { h, s, v } = this.hexToHsv(hex)
|
|
108
|
+
this.hue = h
|
|
109
|
+
this.sat = s
|
|
110
|
+
this.val = v
|
|
111
|
+
|
|
112
|
+
if (this.hasHueSliderTarget) this.hueSliderTarget.value = h
|
|
113
|
+
this.updateSvGradient()
|
|
114
|
+
|
|
115
|
+
if (this.hasSvCursorTarget) {
|
|
116
|
+
this.svCursorTarget.style.left = `${(s * 100)}%`
|
|
117
|
+
this.svCursorTarget.style.top = `${((1 - v) * 100)}%`
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
hsvToHex(h, s, v) {
|
|
122
|
+
const c = v * s
|
|
123
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
|
|
124
|
+
const m = v - c
|
|
125
|
+
let r, g, b
|
|
126
|
+
|
|
127
|
+
if (h < 60) { r = c; g = x; b = 0 }
|
|
128
|
+
else if (h < 120) { r = x; g = c; b = 0 }
|
|
129
|
+
else if (h < 180) { r = 0; g = c; b = x }
|
|
130
|
+
else if (h < 240) { r = 0; g = x; b = c }
|
|
131
|
+
else if (h < 300) { r = x; g = 0; b = c }
|
|
132
|
+
else { r = c; g = 0; b = x }
|
|
133
|
+
|
|
134
|
+
const toHex = (n) => Math.round((n + m) * 255).toString(16).padStart(2, "0")
|
|
135
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
hexToHsv(hex) {
|
|
139
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255
|
|
140
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255
|
|
141
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255
|
|
142
|
+
|
|
143
|
+
const max = Math.max(r, g, b)
|
|
144
|
+
const min = Math.min(r, g, b)
|
|
145
|
+
const d = max - min
|
|
146
|
+
|
|
147
|
+
let h = 0
|
|
148
|
+
if (d !== 0) {
|
|
149
|
+
if (max === r) h = 60 * (((g - b) / d) % 6)
|
|
150
|
+
else if (max === g) h = 60 * ((b - r) / d + 2)
|
|
151
|
+
else h = 60 * ((r - g) / d + 4)
|
|
152
|
+
}
|
|
153
|
+
if (h < 0) h += 360
|
|
154
|
+
|
|
155
|
+
const s = max === 0 ? 0 : d / max
|
|
156
|
+
return { h: Math.round(h), s, v: max }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["menu"]
|
|
5
|
+
static values = { saveUrl: String }
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
this._close = this.close.bind(this)
|
|
9
|
+
document.addEventListener("click", this._close)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
disconnect() {
|
|
13
|
+
document.removeEventListener("click", this._close)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
toggle(event) {
|
|
17
|
+
event.stopPropagation()
|
|
18
|
+
this.menuTarget.classList.toggle("hidden")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
close(event) {
|
|
22
|
+
if (!this.element.contains(event.target)) {
|
|
23
|
+
this.menuTarget.classList.add("hidden")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
save() {
|
|
28
|
+
const allCheckboxes = this.element.querySelectorAll("input[type=checkbox]")
|
|
29
|
+
const hiddenColumns = Array.from(allCheckboxes)
|
|
30
|
+
.filter(cb => !cb.checked)
|
|
31
|
+
.map(cb => cb.value)
|
|
32
|
+
|
|
33
|
+
if (this.hasSaveUrlValue) {
|
|
34
|
+
const token = document.querySelector('meta[name="csrf-token"]')?.content
|
|
35
|
+
fetch(this.saveUrlValue, {
|
|
36
|
+
method: "PATCH",
|
|
37
|
+
headers: {
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
"X-CSRF-Token": token
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify({ hidden_columns: hiddenColumns })
|
|
42
|
+
}).then(() => {
|
|
43
|
+
Turbo.visit(window.location.href, { action: "replace" })
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["menu"]
|
|
5
|
+
|
|
6
|
+
toggle(event) {
|
|
7
|
+
event.stopPropagation()
|
|
8
|
+
this.menuTarget.classList.toggle("hidden")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
hide(event) {
|
|
12
|
+
if (!this.element.contains(event.target)) {
|
|
13
|
+
this.menuTarget.classList.add("hidden")
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["input", "dropZone", "fileName"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this._onDragOver = this.dragOver.bind(this)
|
|
8
|
+
this._onDragLeave = this.dragLeave.bind(this)
|
|
9
|
+
this._onDrop = this.drop.bind(this)
|
|
10
|
+
|
|
11
|
+
this.dropZoneTarget.addEventListener("dragover", this._onDragOver)
|
|
12
|
+
this.dropZoneTarget.addEventListener("dragleave", this._onDragLeave)
|
|
13
|
+
this.dropZoneTarget.addEventListener("drop", this._onDrop)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
disconnect() {
|
|
17
|
+
this.dropZoneTarget.removeEventListener("dragover", this._onDragOver)
|
|
18
|
+
this.dropZoneTarget.removeEventListener("dragleave", this._onDragLeave)
|
|
19
|
+
this.dropZoneTarget.removeEventListener("drop", this._onDrop)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dragOver(event) {
|
|
23
|
+
event.preventDefault()
|
|
24
|
+
this.dropZoneTarget.classList.add("border-accent-500", "bg-accent-50", "dark:bg-accent-900/10")
|
|
25
|
+
this.dropZoneTarget.classList.remove("border-gray-300", "dark:border-zinc-600")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
dragLeave(event) {
|
|
29
|
+
event.preventDefault()
|
|
30
|
+
this.dropZoneTarget.classList.remove("border-accent-500", "bg-accent-50", "dark:bg-accent-900/10")
|
|
31
|
+
this.dropZoneTarget.classList.add("border-gray-300", "dark:border-zinc-600")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
drop(event) {
|
|
35
|
+
event.preventDefault()
|
|
36
|
+
this.dragLeave(event)
|
|
37
|
+
|
|
38
|
+
const files = event.dataTransfer.files
|
|
39
|
+
if (files.length > 0) {
|
|
40
|
+
this.inputTarget.files = files
|
|
41
|
+
this.updateFileName()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
select() {
|
|
46
|
+
this.updateFileName()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
updateFileName() {
|
|
50
|
+
if (!this.hasFileNameTarget) return
|
|
51
|
+
|
|
52
|
+
const files = this.inputTarget.files
|
|
53
|
+
if (files.length === 0) {
|
|
54
|
+
this.fileNameTarget.textContent = ""
|
|
55
|
+
} else if (files.length === 1) {
|
|
56
|
+
this.fileNameTarget.textContent = files[0].name
|
|
57
|
+
} else {
|
|
58
|
+
this.fileNameTarget.textContent = `${files.length} files selected`
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import ColorPickerController from "keystone_ui/color_picker_controller"
|
|
2
|
+
import MultiSelectController from "keystone_ui/multi_select_controller"
|
|
3
|
+
import SwipeDeckController from "keystone_ui/swipe_deck_controller"
|
|
4
|
+
import ColumnPickerController from "keystone_ui/column_picker_controller"
|
|
5
|
+
import FileUploadController from "keystone_ui/file_upload_controller"
|
|
6
|
+
import DropdownController from "keystone_ui/dropdown_controller"
|
|
7
|
+
import DismissController from "keystone_ui/dismiss_controller"
|
|
8
|
+
import ModalController from "keystone_ui/modal_controller"
|
|
9
|
+
import ClipboardController from "keystone_ui/clipboard_controller"
|
|
10
|
+
import TabSwitcherController from "keystone_ui/tab_switcher_controller"
|
|
11
|
+
import AccordionController from "keystone_ui/accordion_controller"
|
|
12
|
+
import StatCardInfoController from "keystone_ui/stat_card_info_controller"
|
|
13
|
+
import AutoSubmitController from "keystone_ui/auto_submit_controller"
|
|
14
|
+
import LineChartController from "keystone_ui/line_chart_controller"
|
|
15
|
+
|
|
16
|
+
export function registerControllers(application) {
|
|
17
|
+
application.register("color-picker", ColorPickerController)
|
|
18
|
+
application.register("multi-select", MultiSelectController)
|
|
19
|
+
application.register("swipe-deck", SwipeDeckController)
|
|
20
|
+
application.register("column-picker", ColumnPickerController)
|
|
21
|
+
application.register("file-upload", FileUploadController)
|
|
22
|
+
application.register("dropdown", DropdownController)
|
|
23
|
+
application.register("dismiss", DismissController)
|
|
24
|
+
application.register("modal", ModalController)
|
|
25
|
+
application.register("clipboard", ClipboardController)
|
|
26
|
+
application.register("tab-switcher", TabSwitcherController)
|
|
27
|
+
application.register("accordion", AccordionController)
|
|
28
|
+
application.register("stat-card-info", StatCardInfoController)
|
|
29
|
+
application.register("auto-submit", AutoSubmitController)
|
|
30
|
+
application.register("line-chart", LineChartController)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { ColorPickerController, MultiSelectController, SwipeDeckController, ColumnPickerController, FileUploadController, DropdownController, DismissController, ModalController, ClipboardController, TabSwitcherController, AccordionController, StatCardInfoController, AutoSubmitController, LineChartController }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { Chart, registerables } from "chart.js"
|
|
3
|
+
|
|
4
|
+
Chart.register(...registerables)
|
|
5
|
+
|
|
6
|
+
export default class extends Controller {
|
|
7
|
+
static targets = ["canvas"]
|
|
8
|
+
static values = { data: Object }
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.chart = new Chart(this.canvasTarget, {
|
|
12
|
+
type: "line",
|
|
13
|
+
data: this.resolveColors(this.dataValue),
|
|
14
|
+
options: {
|
|
15
|
+
responsive: true,
|
|
16
|
+
maintainAspectRatio: false,
|
|
17
|
+
interaction: { mode: "index", intersect: false }
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
disconnect() {
|
|
23
|
+
this.chart?.destroy()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
resolveColors(data) {
|
|
27
|
+
const styles = getComputedStyle(this.element)
|
|
28
|
+
const resolve = (color) => {
|
|
29
|
+
const match = typeof color === "string" && color.match(/^var\((--[^)]+)\)$/)
|
|
30
|
+
return match ? styles.getPropertyValue(match[1]).trim() || color : color
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
data.datasets.forEach((dataset) => {
|
|
34
|
+
if (dataset.borderColor) dataset.borderColor = resolve(dataset.borderColor)
|
|
35
|
+
})
|
|
36
|
+
return data
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["backdrop"]
|
|
5
|
+
|
|
6
|
+
open() {
|
|
7
|
+
this.backdropTarget.classList.remove("hidden")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
close() {
|
|
11
|
+
this.backdropTarget.classList.add("hidden")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
closeOnBackdrop(event) {
|
|
15
|
+
if (event.target === this.backdropTarget) {
|
|
16
|
+
this.close()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["menu", "label"]
|
|
5
|
+
static values = { label: String }
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
this._close = this.close.bind(this)
|
|
9
|
+
document.addEventListener("click", this._close)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
disconnect() {
|
|
13
|
+
document.removeEventListener("click", this._close)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
toggle(event) {
|
|
17
|
+
event.stopPropagation()
|
|
18
|
+
this.menuTarget.classList.toggle("hidden")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
close(event) {
|
|
22
|
+
if (!this.element.contains(event.target)) {
|
|
23
|
+
this.menuTarget.classList.add("hidden")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
updateLabel() {
|
|
28
|
+
const checked = this.element.querySelectorAll("input[type=checkbox]:checked")
|
|
29
|
+
if (checked.length === 0) {
|
|
30
|
+
this.labelTarget.textContent = `All ${this.labelValue}`
|
|
31
|
+
} else {
|
|
32
|
+
this.labelTarget.textContent = `${checked.length} selected`
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["card", "stack"]
|
|
5
|
+
static values = { emptyTitle: String, emptySubtitle: String }
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
this.currentIndex = 0
|
|
9
|
+
this.setupTouch()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
complete() {
|
|
13
|
+
const card = this.currentCard
|
|
14
|
+
if (!card) return
|
|
15
|
+
|
|
16
|
+
const itemId = card.dataset.itemId
|
|
17
|
+
const valueInput = card.querySelector("[data-swipe-deck-value]")
|
|
18
|
+
const value = valueInput ? valueInput.value : null
|
|
19
|
+
|
|
20
|
+
this.dispatch("complete", { detail: { itemId, value, card } })
|
|
21
|
+
this.animateOut(card, "right", "ring-2 ring-green-400")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
skip() {
|
|
25
|
+
const card = this.currentCard
|
|
26
|
+
if (!card) return
|
|
27
|
+
|
|
28
|
+
const itemId = card.dataset.itemId
|
|
29
|
+
this.dispatch("skip", { detail: { itemId, card } })
|
|
30
|
+
this.animateOut(card, "left", "ring-2 ring-red-400")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get currentCard() {
|
|
34
|
+
return this.cardTargets[this.currentIndex]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
animateOut(card, direction, colorClass) {
|
|
38
|
+
if (colorClass) {
|
|
39
|
+
colorClass.split(" ").forEach(c => card.classList.add(c))
|
|
40
|
+
}
|
|
41
|
+
const translateX = direction === "right" ? "150%" : "-150%"
|
|
42
|
+
const rotate = direction === "right" ? "15" : "-15"
|
|
43
|
+
card.style.transition = "transform 0.3s ease-out, opacity 0.3s ease-out"
|
|
44
|
+
card.style.transform = `translateX(${translateX}) rotate(${rotate}deg)`
|
|
45
|
+
card.style.opacity = "0"
|
|
46
|
+
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
card.style.display = "none"
|
|
49
|
+
this.currentIndex++
|
|
50
|
+
this.updateStack()
|
|
51
|
+
}, 300)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
updateStack() {
|
|
55
|
+
const remaining = this.cardTargets.slice(this.currentIndex)
|
|
56
|
+
remaining.forEach((card, index) => {
|
|
57
|
+
card.style.transition = "transform 0.3s ease-out"
|
|
58
|
+
card.style.transform = `scale(${1 - index * 0.05}) translateY(${index * 8}px)`
|
|
59
|
+
card.style.zIndex = remaining.length - index
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (remaining.length === 0) {
|
|
63
|
+
const title = this.emptyTitleValue || "All done!"
|
|
64
|
+
const subtitle = this.emptySubtitleValue
|
|
65
|
+
let html = `
|
|
66
|
+
<div class="text-center py-16" data-testid="swipe-empty">
|
|
67
|
+
<p class="text-2xl font-semibold text-gray-900 dark:text-white">${title}</p>`
|
|
68
|
+
if (subtitle) {
|
|
69
|
+
html += `
|
|
70
|
+
<p class="mt-2 text-gray-500 dark:text-gray-400">${subtitle}</p>`
|
|
71
|
+
}
|
|
72
|
+
html += `
|
|
73
|
+
</div>`
|
|
74
|
+
this.stackTarget.innerHTML = html
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
clearSwipeColor(card) {
|
|
79
|
+
card.classList.remove("ring-2", "ring-green-400", "ring-red-400")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setupTouch() {
|
|
83
|
+
let startX = 0
|
|
84
|
+
let currentX = 0
|
|
85
|
+
|
|
86
|
+
const isInput = (e) => e.target.closest("input, textarea, select")
|
|
87
|
+
|
|
88
|
+
this.element.addEventListener("touchstart", (e) => {
|
|
89
|
+
if (isInput(e)) return
|
|
90
|
+
startX = e.touches[0].clientX
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
this.element.addEventListener("touchmove", (e) => {
|
|
94
|
+
if (isInput(e) || !this.currentCard) return
|
|
95
|
+
currentX = e.touches[0].clientX
|
|
96
|
+
const diff = currentX - startX
|
|
97
|
+
this.currentCard.style.transition = "none"
|
|
98
|
+
this.currentCard.style.transform = `translateX(${diff}px) rotate(${diff * 0.1}deg)`
|
|
99
|
+
this.clearSwipeColor(this.currentCard)
|
|
100
|
+
if (Math.abs(diff) > 30) {
|
|
101
|
+
this.currentCard.classList.add("ring-2", diff > 0 ? "ring-green-400" : "ring-red-400")
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
this.element.addEventListener("touchend", (e) => {
|
|
106
|
+
if (isInput(e) || !this.currentCard) return
|
|
107
|
+
const diff = currentX - startX
|
|
108
|
+
if (Math.abs(diff) > 100) {
|
|
109
|
+
if (diff > 0) {
|
|
110
|
+
this.complete()
|
|
111
|
+
} else {
|
|
112
|
+
this.skip()
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
this.clearSwipeColor(this.currentCard)
|
|
116
|
+
this.currentCard.style.transition = "transform 0.3s ease-out"
|
|
117
|
+
this.currentCard.style.transform = ""
|
|
118
|
+
}
|
|
119
|
+
startX = 0
|
|
120
|
+
currentX = 0
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["tab"]
|
|
5
|
+
|
|
6
|
+
select(event) {
|
|
7
|
+
const index = parseInt(event.currentTarget.dataset.index)
|
|
8
|
+
|
|
9
|
+
this.tabTargets.forEach((tab) => {
|
|
10
|
+
if (parseInt(tab.dataset.index) === index) {
|
|
11
|
+
tab.setAttribute("data-active", "")
|
|
12
|
+
} else {
|
|
13
|
+
tab.removeAttribute("data-active")
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
this.dispatch("change", { detail: { index } })
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* Color picker hue slider styling */
|
|
2
|
+
input[type="range"].keystone-hue-slider {
|
|
3
|
+
-webkit-appearance: none;
|
|
4
|
+
appearance: none;
|
|
5
|
+
height: 0.75rem;
|
|
6
|
+
border-radius: 0.5rem;
|
|
7
|
+
background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
|
|
8
|
+
outline: none;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
input[type="range"].keystone-hue-slider::-webkit-slider-thumb {
|
|
12
|
+
-webkit-appearance: none;
|
|
13
|
+
appearance: none;
|
|
14
|
+
width: 1rem;
|
|
15
|
+
height: 1rem;
|
|
16
|
+
border-radius: 50%;
|
|
17
|
+
background: white;
|
|
18
|
+
border: 2px solid rgba(0, 0, 0, 0.3);
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
input[type="range"].keystone-hue-slider::-moz-range-thumb {
|
|
24
|
+
width: 1rem;
|
|
25
|
+
height: 1rem;
|
|
26
|
+
border-radius: 50%;
|
|
27
|
+
background: white;
|
|
28
|
+
border: 2px solid rgba(0, 0, 0, 0.3);
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
31
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/* Keystone UI components — tells Tailwind to scan for classes in component files */
|
|
2
|
+
@source "../../components/**/*.{erb,rb}";
|
|
3
|
+
|
|
4
|
+
/* Safelist: grid-cols classes built from static constants in GridComponent.
|
|
5
|
+
Belt-and-suspenders — ensures these classes survive JIT purging even if
|
|
6
|
+
the scanner misses the Ruby hash literals. */
|
|
7
|
+
@source inline("grid-cols-1 grid-cols-2 grid-cols-3 grid-cols-4 grid-cols-5 grid-cols-6 grid-cols-7 grid-cols-8 grid-cols-9 grid-cols-10 grid-cols-11 grid-cols-12 sm:grid-cols-1 sm:grid-cols-2 sm:grid-cols-3 sm:grid-cols-4 sm:grid-cols-5 sm:grid-cols-6 sm:grid-cols-7 sm:grid-cols-8 sm:grid-cols-9 sm:grid-cols-10 sm:grid-cols-11 sm:grid-cols-12 md:grid-cols-1 md:grid-cols-2 md:grid-cols-3 md:grid-cols-4 md:grid-cols-5 md:grid-cols-6 md:grid-cols-7 md:grid-cols-8 md:grid-cols-9 md:grid-cols-10 md:grid-cols-11 md:grid-cols-12 lg:grid-cols-1 lg:grid-cols-2 lg:grid-cols-3 lg:grid-cols-4 lg:grid-cols-5 lg:grid-cols-6 lg:grid-cols-7 lg:grid-cols-8 lg:grid-cols-9 lg:grid-cols-10 lg:grid-cols-11 lg:grid-cols-12");
|