kiso 0.2.2.pre → 0.3.0.pre
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/CHANGELOG.md +19 -1
- data/app/assets/tailwind/kiso/dialog.css +46 -0
- data/app/assets/tailwind/kiso/engine.css +2 -0
- data/app/assets/tailwind/kiso/input-otp.css +17 -0
- data/app/assets/tailwind/kiso/slider.css +27 -0
- data/app/javascript/controllers/kiso/dialog_controller.js +140 -0
- data/app/javascript/controllers/kiso/index.js +3 -0
- data/app/javascript/controllers/kiso/slider_controller.js +276 -0
- data/app/views/kiso/components/_alert_dialog.html.erb +28 -0
- data/app/views/kiso/components/_aspect_ratio.html.erb +8 -0
- data/app/views/kiso/components/_button.html.erb +31 -17
- data/app/views/kiso/components/_dialog.html.erb +11 -0
- data/app/views/kiso/components/_slider.html.erb +42 -0
- data/app/views/kiso/components/alert_dialog/_action.html.erb +8 -0
- data/app/views/kiso/components/alert_dialog/_cancel.html.erb +8 -0
- data/app/views/kiso/components/alert_dialog/_description.html.erb +8 -0
- data/app/views/kiso/components/alert_dialog/_footer.html.erb +7 -0
- data/app/views/kiso/components/alert_dialog/_header.html.erb +7 -0
- data/app/views/kiso/components/alert_dialog/_media.html.erb +7 -0
- data/app/views/kiso/components/alert_dialog/_title.html.erb +8 -0
- data/app/views/kiso/components/dialog/_body.html.erb +7 -0
- data/app/views/kiso/components/dialog/_close.html.erb +10 -0
- data/app/views/kiso/components/dialog/_description.html.erb +7 -0
- data/app/views/kiso/components/dialog/_footer.html.erb +7 -0
- data/app/views/kiso/components/dialog/_header.html.erb +7 -0
- data/app/views/kiso/components/dialog/_title.html.erb +7 -0
- data/app/views/kiso/components/empty/_actions.html.erb +7 -0
- data/lib/kiso/themes/alert_dialog.rb +78 -0
- data/lib/kiso/themes/aspect_ratio.rb +16 -0
- data/lib/kiso/themes/dashboard.rb +2 -2
- data/lib/kiso/themes/dialog.rb +57 -0
- data/lib/kiso/themes/empty.rb +6 -1
- data/lib/kiso/themes/input_otp.rb +2 -2
- data/lib/kiso/themes/slider.rb +53 -0
- data/lib/kiso/version.rb +1 -1
- data/lib/kiso.rb +4 -0
- metadata +27 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17ae0456b44e65c4a1f0c0fc7a0fb854daca4af8bf933d13d0fa2a3e78dc0b5d
|
|
4
|
+
data.tar.gz: 5dad006edca36a6d23822274557de73667eb8bd8aa69aa575037859ed1983742
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 490e5777fbc1fd030736c6c7866b3e39855699d7f71be28af41c9fc6bb2cbd7fd6b793d9d302e35886908ce663ddcc98e171ffd1c3419eebf8c6577073276c12
|
|
7
|
+
data.tar.gz: caa47f1734d26450d7163108cefa70438d75e8ef1201ffc1b4123eb4e5bb5e22aa5f7b545fddad657235d79d18e62aacb05818b6f0aa87922baa20c2d3d66bbb
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0.pre] - 2026-03-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Dialog component — modal dialog wrapping the native `<dialog>` element with `showModal()` for focus trapping and backdrop. Sub-parts: header, title, description, body, footer, close. Entry/exit CSS animations with reduced-motion support. Stimulus controller for programmatic open/close.
|
|
15
|
+
- Alert Dialog component — confirmation dialog that requires an explicit user action (`role="alertdialog"`). Cannot be dismissed by Escape or backdrop click. Sub-parts: header, title, description, media, footer, action, cancel. Size variants (default/sm) with responsive media grid layout. Auto-linked `aria-labelledby` and `aria-describedby`.
|
|
16
|
+
- AspectRatio component — lightweight wrapper that applies an aspect ratio via inline style. Accepts any `ratio:` value (defaults to 16:9).
|
|
17
|
+
- Slider component — range input with track, thumb, and fill styling. Supports min/max/step/value, three sizes (sm/md/lg), and disabled state. Stimulus controller for real-time value display.
|
|
18
|
+
- Empty component `:actions` slot for placing buttons below the description.
|
|
19
|
+
- Button `method:` prop — renders a Rails `button_to` form for DELETE/POST/PUT/PATCH actions while preserving all Button styling.
|
|
20
|
+
- Icons guide added to documentation site.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- InputOTP slots missing visible border when a separator is placed inside a group.
|
|
25
|
+
- Sidebar header and footer now use `flex-col` layout matching shadcn structure.
|
|
26
|
+
|
|
10
27
|
## [0.2.2.pre] - 2026-03-03
|
|
11
28
|
|
|
12
29
|
### Fixed
|
|
@@ -68,7 +85,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
68
85
|
- Lookbook component previews
|
|
69
86
|
- Bridgetown documentation site
|
|
70
87
|
|
|
71
|
-
[Unreleased]: https://github.com/steveclarke/kiso/compare/v0.
|
|
88
|
+
[Unreleased]: https://github.com/steveclarke/kiso/compare/v0.3.0.pre...HEAD
|
|
89
|
+
[0.3.0.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.3.0.pre
|
|
72
90
|
[0.2.2.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.2.2.pre
|
|
73
91
|
[0.2.1.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.2.1.pre
|
|
74
92
|
[0.2.0.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.2.0.pre
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* Dialog and Alert Dialog entry/exit animations and scroll lock. */
|
|
2
|
+
|
|
3
|
+
/* Entry */
|
|
4
|
+
:is(dialog[data-slot="dialog"], dialog[data-slot="alert-dialog"])[open] {
|
|
5
|
+
animation: kiso-dialog-backdrop-in 200ms ease-out;
|
|
6
|
+
}
|
|
7
|
+
:is(dialog[data-slot="dialog"], dialog[data-slot="alert-dialog"])[open] > :is([data-slot="dialog-content"], [data-slot="alert-dialog-content"]) {
|
|
8
|
+
animation: kiso-dialog-content-in 200ms ease-out;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* Exit — data-state="closing" set by Stimulus before calling .close() */
|
|
12
|
+
:is(dialog[data-slot="dialog"], dialog[data-slot="alert-dialog"])[data-state="closing"] {
|
|
13
|
+
animation: kiso-dialog-backdrop-out 200ms ease-in forwards;
|
|
14
|
+
}
|
|
15
|
+
:is(dialog[data-slot="dialog"], dialog[data-slot="alert-dialog"])[data-state="closing"] > :is([data-slot="dialog-content"], [data-slot="alert-dialog-content"]) {
|
|
16
|
+
animation: kiso-dialog-content-out 200ms ease-in forwards;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@keyframes kiso-dialog-backdrop-in {
|
|
20
|
+
from { opacity: 0; }
|
|
21
|
+
}
|
|
22
|
+
@keyframes kiso-dialog-backdrop-out {
|
|
23
|
+
to { opacity: 0; }
|
|
24
|
+
}
|
|
25
|
+
@keyframes kiso-dialog-content-in {
|
|
26
|
+
from { opacity: 0; transform: scale(0.95); }
|
|
27
|
+
}
|
|
28
|
+
@keyframes kiso-dialog-content-out {
|
|
29
|
+
to { opacity: 0; transform: scale(0.95); }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Scroll lock — prevent body scroll when modal dialog is open */
|
|
33
|
+
html:has(:is(dialog[data-slot="dialog"], dialog[data-slot="alert-dialog"])[open]:modal) {
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
scrollbar-gutter: stable;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Respect reduced motion */
|
|
39
|
+
@media (prefers-reduced-motion: reduce) {
|
|
40
|
+
:is(dialog[data-slot="dialog"], dialog[data-slot="alert-dialog"])[open],
|
|
41
|
+
:is(dialog[data-slot="dialog"], dialog[data-slot="alert-dialog"])[open] > :is([data-slot="dialog-content"], [data-slot="alert-dialog-content"]),
|
|
42
|
+
:is(dialog[data-slot="dialog"], dialog[data-slot="alert-dialog"])[data-state="closing"],
|
|
43
|
+
:is(dialog[data-slot="dialog"], dialog[data-slot="alert-dialog"])[data-state="closing"] > :is([data-slot="dialog-content"], [data-slot="alert-dialog-content"]) {
|
|
44
|
+
animation: none;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
@import "./radio-group.css";
|
|
8
8
|
@import "./color-mode.css";
|
|
9
9
|
@import "./dashboard.css";
|
|
10
|
+
@import "./dialog.css";
|
|
10
11
|
@import "./input-otp.css";
|
|
12
|
+
@import "./slider.css";
|
|
11
13
|
|
|
12
14
|
/* Scan Kiso's own files so host apps don't need to know the gem's internals.
|
|
13
15
|
Paths are relative to THIS file (app/assets/tailwind/kiso/engine.css).
|
|
@@ -8,3 +8,20 @@
|
|
|
8
8
|
0%, 70%, 100% { opacity: 1; }
|
|
9
9
|
20%, 50% { opacity: 0; }
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
/* Separator-adjacent rounding — when a separator sits inside a group,
|
|
13
|
+
round the slot edges next to it so each visual cluster has proper corners.
|
|
14
|
+
Also reset the negative margin so the slot after a separator starts fresh. */
|
|
15
|
+
|
|
16
|
+
@layer utilities {
|
|
17
|
+
[data-slot="input-otp-slot"]:has(+ [data-slot="input-otp-separator"]) {
|
|
18
|
+
border-top-right-radius: var(--radius-md);
|
|
19
|
+
border-bottom-right-radius: var(--radius-md);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
[data-slot="input-otp-separator"] + [data-slot="input-otp-slot"] {
|
|
23
|
+
margin-left: 0;
|
|
24
|
+
border-top-left-radius: var(--radius-md);
|
|
25
|
+
border-bottom-left-radius: var(--radius-md);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* Center the thumb over its left % position */
|
|
3
|
+
[data-slot="slider-thumb"] {
|
|
4
|
+
transform: translateX(-50%);
|
|
5
|
+
top: 50%;
|
|
6
|
+
margin-top: calc(-1 * var(--thumb-offset, 0.5rem));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* Size-specific thumb offsets (half the thumb size) */
|
|
10
|
+
[data-slot="slider"] :where([data-slot="slider-thumb"].size-3) {
|
|
11
|
+
--thumb-offset: 0.375rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
[data-slot="slider"] :where([data-slot="slider-thumb"].size-4) {
|
|
15
|
+
--thumb-offset: 0.5rem;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
[data-slot="slider"] :where([data-slot="slider-thumb"].size-5) {
|
|
19
|
+
--thumb-offset: 0.625rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Disabled state */
|
|
23
|
+
[data-slot="slider"]:has(input:disabled) {
|
|
24
|
+
opacity: 0.5;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dialog controller. Wraps the native `<dialog>` element with `showModal()`
|
|
5
|
+
* for proper focus trapping, backdrop, and Escape-to-close behavior.
|
|
6
|
+
*
|
|
7
|
+
* The `<dialog>` element itself is the controller root. It acts as the
|
|
8
|
+
* backdrop overlay (fixed inset-0 with bg-black/50). A content wrapper
|
|
9
|
+
* inside provides the centered panel.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <dialog data-controller="kiso--dialog"
|
|
13
|
+
* data-kiso--dialog-open-value="true"
|
|
14
|
+
* data-slot="dialog">
|
|
15
|
+
* <div data-slot="dialog-content">
|
|
16
|
+
* <button data-action="kiso--dialog#close" data-slot="dialog-close">×</button>
|
|
17
|
+
* <h2>Title</h2>
|
|
18
|
+
* <p>Description</p>
|
|
19
|
+
* </div>
|
|
20
|
+
* </dialog>
|
|
21
|
+
*
|
|
22
|
+
* <!-- Trigger from outside -->
|
|
23
|
+
* <button data-action="kiso--dialog#open">Open</button>
|
|
24
|
+
*
|
|
25
|
+
* @property {boolean} openValue - Whether the dialog should open on connect (default: false)
|
|
26
|
+
* @property {boolean} dismissableValue - Whether backdrop click and Escape close the dialog (default: true). Set to false for alert dialogs.
|
|
27
|
+
* @fires kiso--dialog:open - Dispatched when the dialog opens
|
|
28
|
+
* @fires kiso--dialog:close - Dispatched when the dialog closes
|
|
29
|
+
*/
|
|
30
|
+
export default class extends Controller {
|
|
31
|
+
static values = {
|
|
32
|
+
open: { type: Boolean, default: false },
|
|
33
|
+
dismissable: { type: Boolean, default: true },
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
connect() {
|
|
37
|
+
this._handleBackdropClick = this._handleBackdropClick.bind(this)
|
|
38
|
+
this._handleCancel = this._handleCancel.bind(this)
|
|
39
|
+
this.element.addEventListener("click", this._handleBackdropClick)
|
|
40
|
+
this.element.addEventListener("cancel", this._handleCancel)
|
|
41
|
+
|
|
42
|
+
if (this.openValue) {
|
|
43
|
+
this.open()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
disconnect() {
|
|
48
|
+
this._clearCloseTimers()
|
|
49
|
+
this.element.removeEventListener("click", this._handleBackdropClick)
|
|
50
|
+
this.element.removeEventListener("cancel", this._handleCancel)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Opens the dialog as a modal via `showModal()`.
|
|
55
|
+
*/
|
|
56
|
+
open() {
|
|
57
|
+
if (this.element.open) return
|
|
58
|
+
this.element.showModal()
|
|
59
|
+
this.dispatch("open")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Closes the dialog with an exit animation. Sets `data-state="closing"`
|
|
64
|
+
* to trigger the CSS animation, then calls `dialog.close()` after
|
|
65
|
+
* the animation completes.
|
|
66
|
+
*/
|
|
67
|
+
close() {
|
|
68
|
+
if (!this.element.open || this._isClosing) return
|
|
69
|
+
this._isClosing = true
|
|
70
|
+
|
|
71
|
+
this.element.dataset.state = "closing"
|
|
72
|
+
|
|
73
|
+
this._onCloseEnd = () => {
|
|
74
|
+
this._clearCloseTimers()
|
|
75
|
+
delete this.element.dataset.state
|
|
76
|
+
this._isClosing = false
|
|
77
|
+
this.element.close()
|
|
78
|
+
this.dispatch("close")
|
|
79
|
+
}
|
|
80
|
+
this.element.addEventListener("animationend", this._onCloseEnd)
|
|
81
|
+
|
|
82
|
+
// Fallback if no animation runs (e.g. prefers-reduced-motion)
|
|
83
|
+
this._closingTimeout = setTimeout(this._onCloseEnd, 250)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Responds to Stimulus Value changes for external open/close control.
|
|
88
|
+
*/
|
|
89
|
+
openValueChanged() {
|
|
90
|
+
// Skip during connect — handled in connect() directly
|
|
91
|
+
if (!this.element.isConnected) return
|
|
92
|
+
|
|
93
|
+
if (this.openValue) {
|
|
94
|
+
this.open()
|
|
95
|
+
} else {
|
|
96
|
+
this.close()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Removes pending close animation listeners and timeouts.
|
|
102
|
+
*
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
_clearCloseTimers() {
|
|
106
|
+
clearTimeout(this._closingTimeout)
|
|
107
|
+
if (this._onCloseEnd) {
|
|
108
|
+
this.element.removeEventListener("animationend", this._onCloseEnd)
|
|
109
|
+
this._onCloseEnd = null
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Closes the dialog when clicking the backdrop (the `<dialog>` element
|
|
115
|
+
* itself, outside the content panel).
|
|
116
|
+
*
|
|
117
|
+
* @param {MouseEvent} event
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
_handleBackdropClick(event) {
|
|
121
|
+
if (!this.dismissableValue) return
|
|
122
|
+
if (event.target === this.element) {
|
|
123
|
+
this.close()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Intercepts the native `cancel` event (fired on Escape) to route
|
|
129
|
+
* through our `close()` method for exit animation.
|
|
130
|
+
*
|
|
131
|
+
* @param {Event} event
|
|
132
|
+
* @private
|
|
133
|
+
*/
|
|
134
|
+
_handleCancel(event) {
|
|
135
|
+
event.preventDefault()
|
|
136
|
+
if (this.dismissableValue) {
|
|
137
|
+
this.close()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import KisoComboboxController from "./combobox_controller.js"
|
|
2
2
|
import KisoCommandController from "./command_controller.js"
|
|
3
3
|
import KisoCommandDialogController from "./command_dialog_controller.js"
|
|
4
|
+
import KisoDialogController from "./dialog_controller.js"
|
|
4
5
|
import KisoDropdownMenuController from "./dropdown_menu_controller.js"
|
|
5
6
|
import KisoInputOtpController from "./input_otp_controller.js"
|
|
6
7
|
import KisoPopoverController from "./popover_controller.js"
|
|
@@ -15,6 +16,7 @@ const KisoUi = {
|
|
|
15
16
|
application.register("kiso--combobox", KisoComboboxController)
|
|
16
17
|
application.register("kiso--command", KisoCommandController)
|
|
17
18
|
application.register("kiso--command-dialog", KisoCommandDialogController)
|
|
19
|
+
application.register("kiso--dialog", KisoDialogController)
|
|
18
20
|
application.register("kiso--dropdown-menu", KisoDropdownMenuController)
|
|
19
21
|
application.register("kiso--input-otp", KisoInputOtpController)
|
|
20
22
|
application.register("kiso--popover", KisoPopoverController)
|
|
@@ -31,6 +33,7 @@ export {
|
|
|
31
33
|
KisoComboboxController,
|
|
32
34
|
KisoCommandController,
|
|
33
35
|
KisoCommandDialogController,
|
|
36
|
+
KisoDialogController,
|
|
34
37
|
KisoDropdownMenuController,
|
|
35
38
|
KisoInputOtpController,
|
|
36
39
|
KisoPopoverController,
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Connects a visual slider (track, range, thumb) to a hidden `<input type="range">`
|
|
5
|
+
* for form submission and accessibility.
|
|
6
|
+
*
|
|
7
|
+
* The hidden input stores the value for form submission. The thumb element has
|
|
8
|
+
* `role="slider"` with ARIA attributes for screen readers. Mouse drag, touch drag,
|
|
9
|
+
* track click, and keyboard navigation all update both the visual position and the
|
|
10
|
+
* hidden input value.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <div data-controller="kiso--slider"
|
|
14
|
+
* data-kiso--slider-min-value="0"
|
|
15
|
+
* data-kiso--slider-max-value="100"
|
|
16
|
+
* data-kiso--slider-step-value="1">
|
|
17
|
+
* <input type="range" data-kiso--slider-target="input" class="sr-only"
|
|
18
|
+
* min="0" max="100" step="1" value="50">
|
|
19
|
+
* <div data-kiso--slider-target="track" data-slot="slider-track">
|
|
20
|
+
* <div data-kiso--slider-target="range" data-slot="slider-range"
|
|
21
|
+
* style="width: 50%"></div>
|
|
22
|
+
* </div>
|
|
23
|
+
* <div data-kiso--slider-target="thumb" data-slot="slider-thumb"
|
|
24
|
+
* role="slider" tabindex="0"
|
|
25
|
+
* aria-valuemin="0" aria-valuemax="100" aria-valuenow="50"
|
|
26
|
+
* style="position: absolute; left: 50%"></div>
|
|
27
|
+
* </div>
|
|
28
|
+
*
|
|
29
|
+
* @property {HTMLInputElement} inputTarget - The hidden range input for form submission
|
|
30
|
+
* @property {HTMLElement} trackTarget - The background track element
|
|
31
|
+
* @property {HTMLElement} rangeTarget - The filled range portion
|
|
32
|
+
* @property {HTMLElement} thumbTarget - The draggable thumb handle
|
|
33
|
+
* @property {Number} minValue - Minimum slider value
|
|
34
|
+
* @property {Number} maxValue - Maximum slider value
|
|
35
|
+
* @property {Number} stepValue - Step increment
|
|
36
|
+
*
|
|
37
|
+
* @fires kiso--slider:change - When the slider value changes.
|
|
38
|
+
* Detail: `{ value: number }`
|
|
39
|
+
*/
|
|
40
|
+
export default class extends Controller {
|
|
41
|
+
static targets = ["input", "track", "range", "thumb"]
|
|
42
|
+
static values = {
|
|
43
|
+
min: { type: Number, default: 0 },
|
|
44
|
+
max: { type: Number, default: 100 },
|
|
45
|
+
step: { type: Number, default: 1 },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Binds event listeners for mouse, touch, keyboard, and track click.
|
|
50
|
+
*/
|
|
51
|
+
connect() {
|
|
52
|
+
this._handleMouseDown = this._handleMouseDown.bind(this)
|
|
53
|
+
this._handleMouseMove = this._handleMouseMove.bind(this)
|
|
54
|
+
this._handleMouseUp = this._handleMouseUp.bind(this)
|
|
55
|
+
this._handleTouchStart = this._handleTouchStart.bind(this)
|
|
56
|
+
this._handleTouchMove = this._handleTouchMove.bind(this)
|
|
57
|
+
this._handleTouchEnd = this._handleTouchEnd.bind(this)
|
|
58
|
+
this._handleTrackClick = this._handleTrackClick.bind(this)
|
|
59
|
+
this._handleKeyDown = this._handleKeyDown.bind(this)
|
|
60
|
+
|
|
61
|
+
this.thumbTarget.addEventListener("mousedown", this._handleMouseDown)
|
|
62
|
+
this.thumbTarget.addEventListener("touchstart", this._handleTouchStart, { passive: false })
|
|
63
|
+
this.thumbTarget.addEventListener("keydown", this._handleKeyDown)
|
|
64
|
+
this.trackTarget.addEventListener("click", this._handleTrackClick)
|
|
65
|
+
|
|
66
|
+
this._dragging = false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Removes all event listeners.
|
|
71
|
+
*/
|
|
72
|
+
disconnect() {
|
|
73
|
+
this.thumbTarget.removeEventListener("mousedown", this._handleMouseDown)
|
|
74
|
+
this.thumbTarget.removeEventListener("touchstart", this._handleTouchStart)
|
|
75
|
+
this.thumbTarget.removeEventListener("keydown", this._handleKeyDown)
|
|
76
|
+
this.trackTarget.removeEventListener("click", this._handleTrackClick)
|
|
77
|
+
|
|
78
|
+
// Clean up any lingering global listeners from an interrupted drag
|
|
79
|
+
document.removeEventListener("mousemove", this._handleMouseMove)
|
|
80
|
+
document.removeEventListener("mouseup", this._handleMouseUp)
|
|
81
|
+
document.removeEventListener("touchmove", this._handleTouchMove)
|
|
82
|
+
document.removeEventListener("touchend", this._handleTouchEnd)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @returns {boolean} Whether the slider is disabled
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
get _isDisabled() {
|
|
90
|
+
return this.inputTarget.disabled
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Starts mouse drag tracking.
|
|
95
|
+
*
|
|
96
|
+
* @param {MouseEvent} event
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
_handleMouseDown(event) {
|
|
100
|
+
if (this._isDisabled) return
|
|
101
|
+
event.preventDefault()
|
|
102
|
+
|
|
103
|
+
this._dragging = true
|
|
104
|
+
document.addEventListener("mousemove", this._handleMouseMove)
|
|
105
|
+
document.addEventListener("mouseup", this._handleMouseUp)
|
|
106
|
+
this.thumbTarget.focus()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Updates slider position during mouse drag.
|
|
111
|
+
*
|
|
112
|
+
* @param {MouseEvent} event
|
|
113
|
+
* @private
|
|
114
|
+
*/
|
|
115
|
+
_handleMouseMove(event) {
|
|
116
|
+
if (!this._dragging) return
|
|
117
|
+
this._updateFromPointer(event.clientX)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Ends mouse drag tracking.
|
|
122
|
+
*
|
|
123
|
+
* @private
|
|
124
|
+
*/
|
|
125
|
+
_handleMouseUp() {
|
|
126
|
+
this._dragging = false
|
|
127
|
+
document.removeEventListener("mousemove", this._handleMouseMove)
|
|
128
|
+
document.removeEventListener("mouseup", this._handleMouseUp)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Starts touch drag tracking.
|
|
133
|
+
*
|
|
134
|
+
* @param {TouchEvent} event
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
_handleTouchStart(event) {
|
|
138
|
+
if (this._isDisabled) return
|
|
139
|
+
event.preventDefault()
|
|
140
|
+
|
|
141
|
+
this._dragging = true
|
|
142
|
+
document.addEventListener("touchmove", this._handleTouchMove, { passive: false })
|
|
143
|
+
document.addEventListener("touchend", this._handleTouchEnd)
|
|
144
|
+
this.thumbTarget.focus()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Updates slider position during touch drag.
|
|
149
|
+
*
|
|
150
|
+
* @param {TouchEvent} event
|
|
151
|
+
* @private
|
|
152
|
+
*/
|
|
153
|
+
_handleTouchMove(event) {
|
|
154
|
+
if (!this._dragging) return
|
|
155
|
+
event.preventDefault()
|
|
156
|
+
this._updateFromPointer(event.touches[0].clientX)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Ends touch drag tracking.
|
|
161
|
+
*
|
|
162
|
+
* @private
|
|
163
|
+
*/
|
|
164
|
+
_handleTouchEnd() {
|
|
165
|
+
this._dragging = false
|
|
166
|
+
document.removeEventListener("touchmove", this._handleTouchMove)
|
|
167
|
+
document.removeEventListener("touchend", this._handleTouchEnd)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Jumps the thumb to the clicked position on the track.
|
|
172
|
+
*
|
|
173
|
+
* @param {MouseEvent} event
|
|
174
|
+
* @private
|
|
175
|
+
*/
|
|
176
|
+
_handleTrackClick(event) {
|
|
177
|
+
if (this._isDisabled) return
|
|
178
|
+
this._updateFromPointer(event.clientX)
|
|
179
|
+
this.thumbTarget.focus()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Handles keyboard navigation on the thumb.
|
|
184
|
+
*
|
|
185
|
+
* Arrow keys adjust by step, Page Up/Down by 10x step, Home/End jump to min/max.
|
|
186
|
+
*
|
|
187
|
+
* @param {KeyboardEvent} event
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
_handleKeyDown(event) {
|
|
191
|
+
if (this._isDisabled) return
|
|
192
|
+
|
|
193
|
+
const current = parseFloat(this.inputTarget.value)
|
|
194
|
+
const step = this.stepValue
|
|
195
|
+
let newValue = current
|
|
196
|
+
|
|
197
|
+
switch (event.key) {
|
|
198
|
+
case "ArrowRight":
|
|
199
|
+
case "ArrowUp":
|
|
200
|
+
newValue = current + step
|
|
201
|
+
break
|
|
202
|
+
case "ArrowLeft":
|
|
203
|
+
case "ArrowDown":
|
|
204
|
+
newValue = current - step
|
|
205
|
+
break
|
|
206
|
+
case "PageUp":
|
|
207
|
+
newValue = current + step * 10
|
|
208
|
+
break
|
|
209
|
+
case "PageDown":
|
|
210
|
+
newValue = current - step * 10
|
|
211
|
+
break
|
|
212
|
+
case "Home":
|
|
213
|
+
newValue = this.minValue
|
|
214
|
+
break
|
|
215
|
+
case "End":
|
|
216
|
+
newValue = this.maxValue
|
|
217
|
+
break
|
|
218
|
+
default:
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
event.preventDefault()
|
|
223
|
+
this._setValue(newValue)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Calculates value from a pointer's X coordinate relative to the track.
|
|
228
|
+
*
|
|
229
|
+
* @param {number} clientX - The pointer's X coordinate
|
|
230
|
+
* @private
|
|
231
|
+
*/
|
|
232
|
+
_updateFromPointer(clientX) {
|
|
233
|
+
const rect = this.trackTarget.getBoundingClientRect()
|
|
234
|
+
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))
|
|
235
|
+
const rawValue = this.minValue + ratio * (this.maxValue - this.minValue)
|
|
236
|
+
this._setValue(rawValue)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Snaps a value to the nearest step, clamps to min/max, and updates
|
|
241
|
+
* the hidden input, visual position, and ARIA attributes.
|
|
242
|
+
*
|
|
243
|
+
* @param {number} rawValue - The unsnapped value
|
|
244
|
+
* @private
|
|
245
|
+
*/
|
|
246
|
+
_setValue(rawValue) {
|
|
247
|
+
const step = this.stepValue
|
|
248
|
+
const snapped = Math.round(rawValue / step) * step
|
|
249
|
+
const clamped = Math.max(this.minValue, Math.min(this.maxValue, snapped))
|
|
250
|
+
|
|
251
|
+
// Round to avoid floating point artifacts
|
|
252
|
+
const decimals = step.toString().includes(".") ? step.toString().split(".")[1].length : 0
|
|
253
|
+
const value = parseFloat(clamped.toFixed(decimals))
|
|
254
|
+
|
|
255
|
+
if (parseFloat(this.inputTarget.value) === value) return
|
|
256
|
+
|
|
257
|
+
this.inputTarget.value = value
|
|
258
|
+
this._updateVisual(value)
|
|
259
|
+
|
|
260
|
+
this.dispatch("change", { detail: { value } })
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Updates the visual position of the range and thumb elements,
|
|
265
|
+
* and syncs ARIA attributes.
|
|
266
|
+
*
|
|
267
|
+
* @param {number} value - The current slider value
|
|
268
|
+
* @private
|
|
269
|
+
*/
|
|
270
|
+
_updateVisual(value) {
|
|
271
|
+
const percent = ((value - this.minValue) / (this.maxValue - this.minValue)) * 100
|
|
272
|
+
this.rangeTarget.style.width = `${percent}%`
|
|
273
|
+
this.thumbTarget.style.left = `${percent}%`
|
|
274
|
+
this.thumbTarget.setAttribute("aria-valuenow", value)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<%# locals: (open: false, size: :default, css_classes: "", **component_options) %>
|
|
2
|
+
<%
|
|
3
|
+
# Store the dialog id on the view context so child sub-part partials
|
|
4
|
+
# (_title, _description) can auto-generate matching aria-labelledby /
|
|
5
|
+
# aria-describedby id attributes without requiring the caller to pass
|
|
6
|
+
# the id through each nested kui() call.
|
|
7
|
+
@_kiso_alert_dialog_id = component_options[:id]
|
|
8
|
+
aria = {}
|
|
9
|
+
if @_kiso_alert_dialog_id
|
|
10
|
+
aria[:labelledby] = "#{@_kiso_alert_dialog_id}-title"
|
|
11
|
+
aria[:describedby] = "#{@_kiso_alert_dialog_id}-description"
|
|
12
|
+
end
|
|
13
|
+
%>
|
|
14
|
+
<%= content_tag :dialog,
|
|
15
|
+
class: Kiso::Themes::AlertDialog.render(class: css_classes),
|
|
16
|
+
role: "alertdialog",
|
|
17
|
+
aria: aria,
|
|
18
|
+
data: kiso_prepare_options(component_options, slot: "alert-dialog",
|
|
19
|
+
controller: "kiso--dialog",
|
|
20
|
+
kiso__dialog_open_value: (open ? true : nil),
|
|
21
|
+
kiso__dialog_dismissable_value: false),
|
|
22
|
+
**component_options do %>
|
|
23
|
+
<div class="<%= Kiso::Themes::AlertDialogContent.render(size: size) %>"
|
|
24
|
+
data-slot="alert-dialog-content"
|
|
25
|
+
data-size="<%= size %>">
|
|
26
|
+
<%= yield %>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%# locals: (ratio: 16.0/9, css_classes: "", **component_options) %>
|
|
2
|
+
<%= content_tag :div,
|
|
3
|
+
style: "aspect-ratio: #{ratio}",
|
|
4
|
+
class: Kiso::Themes::AspectRatio.render(class: css_classes),
|
|
5
|
+
data: kiso_prepare_options(component_options, slot: "aspect-ratio"),
|
|
6
|
+
**component_options do %>
|
|
7
|
+
<%= yield %>
|
|
8
|
+
<% end %>
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
<%# locals: (color: :primary, variant: :solid, size: :md, block: false,
|
|
2
|
-
type: :button, href: nil, disabled: false,
|
|
3
|
-
css_classes: "", **component_options) %>
|
|
4
|
-
<%
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
2
|
+
type: :button, href: nil, method: nil, disabled: false,
|
|
3
|
+
form: {}, css_classes: "", **component_options) %>
|
|
4
|
+
<%
|
|
5
|
+
css = Kiso::Themes::Button.render(
|
|
6
|
+
color: color, variant: variant, size: size, block: block, class: css_classes)
|
|
7
|
+
data = kiso_prepare_options(component_options, slot: "button")
|
|
8
|
+
use_button_to = href.present? && method.present? && method.to_s != "get"
|
|
9
|
+
%>
|
|
10
|
+
<% if use_button_to %>
|
|
11
|
+
<%= button_to href,
|
|
12
|
+
method: method,
|
|
13
|
+
class: css,
|
|
14
|
+
form_class: "contents",
|
|
15
|
+
data: data,
|
|
16
|
+
disabled: disabled || nil,
|
|
17
|
+
form: form.presence,
|
|
18
|
+
**component_options do %>
|
|
19
|
+
<%= yield %>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% elsif href.present? %>
|
|
22
|
+
<% component_options[:href] = href
|
|
23
|
+
component_options[:"aria-disabled"] = true if disabled %>
|
|
24
|
+
<%= content_tag :a, class: css, data: data, **component_options do %>
|
|
25
|
+
<%= yield %>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% else %>
|
|
28
|
+
<% component_options[:type] = type
|
|
29
|
+
component_options[:disabled] = true if disabled %>
|
|
30
|
+
<%= content_tag :button, class: css, data: data, **component_options do %>
|
|
31
|
+
<%= yield %>
|
|
32
|
+
<% end %>
|
|
19
33
|
<% end %>
|