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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -1
  3. data/app/assets/tailwind/kiso/dialog.css +46 -0
  4. data/app/assets/tailwind/kiso/engine.css +2 -0
  5. data/app/assets/tailwind/kiso/input-otp.css +17 -0
  6. data/app/assets/tailwind/kiso/slider.css +27 -0
  7. data/app/javascript/controllers/kiso/dialog_controller.js +140 -0
  8. data/app/javascript/controllers/kiso/index.js +3 -0
  9. data/app/javascript/controllers/kiso/slider_controller.js +276 -0
  10. data/app/views/kiso/components/_alert_dialog.html.erb +28 -0
  11. data/app/views/kiso/components/_aspect_ratio.html.erb +8 -0
  12. data/app/views/kiso/components/_button.html.erb +31 -17
  13. data/app/views/kiso/components/_dialog.html.erb +11 -0
  14. data/app/views/kiso/components/_slider.html.erb +42 -0
  15. data/app/views/kiso/components/alert_dialog/_action.html.erb +8 -0
  16. data/app/views/kiso/components/alert_dialog/_cancel.html.erb +8 -0
  17. data/app/views/kiso/components/alert_dialog/_description.html.erb +8 -0
  18. data/app/views/kiso/components/alert_dialog/_footer.html.erb +7 -0
  19. data/app/views/kiso/components/alert_dialog/_header.html.erb +7 -0
  20. data/app/views/kiso/components/alert_dialog/_media.html.erb +7 -0
  21. data/app/views/kiso/components/alert_dialog/_title.html.erb +8 -0
  22. data/app/views/kiso/components/dialog/_body.html.erb +7 -0
  23. data/app/views/kiso/components/dialog/_close.html.erb +10 -0
  24. data/app/views/kiso/components/dialog/_description.html.erb +7 -0
  25. data/app/views/kiso/components/dialog/_footer.html.erb +7 -0
  26. data/app/views/kiso/components/dialog/_header.html.erb +7 -0
  27. data/app/views/kiso/components/dialog/_title.html.erb +7 -0
  28. data/app/views/kiso/components/empty/_actions.html.erb +7 -0
  29. data/lib/kiso/themes/alert_dialog.rb +78 -0
  30. data/lib/kiso/themes/aspect_ratio.rb +16 -0
  31. data/lib/kiso/themes/dashboard.rb +2 -2
  32. data/lib/kiso/themes/dialog.rb +57 -0
  33. data/lib/kiso/themes/empty.rb +6 -1
  34. data/lib/kiso/themes/input_otp.rb +2 -2
  35. data/lib/kiso/themes/slider.rb +53 -0
  36. data/lib/kiso/version.rb +1 -1
  37. data/lib/kiso.rb +4 -0
  38. metadata +27 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cfca4f2a1e45d61e45de52c22a96428de18c04643860ea07c0913d2c29d3c397
4
- data.tar.gz: e46ba40a7fe00c0818958742e745058e6cec5990e2b195df085cc2b3e8242477
3
+ metadata.gz: 17ae0456b44e65c4a1f0c0fc7a0fb854daca4af8bf933d13d0fa2a3e78dc0b5d
4
+ data.tar.gz: 5dad006edca36a6d23822274557de73667eb8bd8aa69aa575037859ed1983742
5
5
  SHA512:
6
- metadata.gz: 8ca9dcd4857e5f4eb41086239901a6c347314432ba75189372642dba08d6975fe9caa5466635e94e013f26c858a84ff5af64e2701dd75a3ef946e560b3025011
7
- data.tar.gz: 75689ce3b7a06d3377d74ed37f7d5fe1d44c95d19549b6992e5c66bd3b78a8b07e924bbc2ba26a15f02b861f74986565c124369fb7648de5b5655b060dd2d4f6
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.2.2.pre...HEAD
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
- <% tag_name = href.present? ? :a : :button
5
- if tag_name == :a
6
- component_options[:href] = href
7
- component_options[:"aria-disabled"] = true if disabled
8
- else
9
- component_options[:type] = type
10
- component_options[:disabled] = true if disabled
11
- end %>
12
- <%= content_tag tag_name,
13
- class: Kiso::Themes::Button.render(
14
- color: color, variant: variant, size: size, block: block,
15
- class: css_classes),
16
- data: kiso_prepare_options(component_options, slot: "button"),
17
- **component_options do %>
18
- <%= yield %>
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 %>