kiso 0.1.0.pre → 0.2.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 +36 -2
- data/README.md +67 -27
- data/Rakefile +8 -0
- data/app/assets/tailwind/kiso/checkbox.css +18 -0
- data/app/assets/tailwind/kiso/color-mode.css +9 -0
- data/app/assets/tailwind/kiso/dashboard.css +194 -0
- data/app/assets/tailwind/kiso/engine.css +117 -0
- data/app/assets/tailwind/kiso/input-otp.css +10 -0
- data/app/assets/tailwind/kiso/radio-group.css +17 -0
- data/app/helpers/kiso/component_helper.rb +46 -27
- data/app/helpers/kiso/icon_helper.rb +53 -9
- data/app/helpers/kiso/theme_helper.rb +38 -0
- data/app/javascript/controllers/kiso/combobox_controller.js +616 -0
- data/app/javascript/controllers/kiso/command_controller.js +184 -0
- data/app/javascript/controllers/kiso/command_dialog_controller.js +104 -0
- data/app/javascript/controllers/kiso/dropdown_menu_controller.js +684 -0
- data/app/javascript/controllers/kiso/index.d.ts +12 -0
- data/app/javascript/controllers/kiso/index.js +42 -0
- data/app/javascript/controllers/kiso/input_otp_controller.js +195 -0
- data/app/javascript/controllers/kiso/popover_controller.js +254 -0
- data/app/javascript/controllers/kiso/select_controller.js +307 -0
- data/app/javascript/controllers/kiso/sidebar_controller.js +84 -0
- data/app/javascript/controllers/kiso/theme_controller.js +89 -0
- data/app/javascript/controllers/kiso/toggle_controller.js +24 -0
- data/app/javascript/controllers/kiso/toggle_group_controller.js +128 -0
- data/app/javascript/kiso/utils/focusable.js +8 -0
- data/app/javascript/kiso/utils/highlight.js +43 -0
- data/app/javascript/kiso/utils/positioning.js +86 -0
- data/app/javascript/kiso/vendor/floating-ui-core.js +1 -0
- data/app/javascript/kiso/vendor/floating-ui-dom.js +1 -0
- data/app/views/kiso/components/_alert.html.erb +1 -1
- data/app/views/kiso/components/_avatar.html.erb +23 -0
- data/app/views/kiso/components/_badge.html.erb +1 -1
- data/app/views/kiso/components/_breadcrumb.html.erb +8 -0
- data/app/views/kiso/components/_button.html.erb +1 -1
- data/app/views/kiso/components/_card.html.erb +1 -1
- data/app/views/kiso/components/_checkbox.html.erb +7 -0
- data/app/views/kiso/components/_color_mode_button.html.erb +14 -0
- data/app/views/kiso/components/_color_mode_select.html.erb +24 -0
- data/app/views/kiso/components/_combobox.html.erb +12 -0
- data/app/views/kiso/components/_command.html.erb +7 -0
- data/app/views/kiso/components/_dashboard_group.html.erb +14 -0
- data/app/views/kiso/components/_dashboard_navbar.html.erb +7 -0
- data/app/views/kiso/components/_dashboard_panel.html.erb +7 -0
- data/app/views/kiso/components/_dashboard_sidebar.html.erb +11 -0
- data/app/views/kiso/components/_dashboard_toolbar.html.erb +7 -0
- data/app/views/kiso/components/_dropdown_menu.html.erb +7 -0
- data/app/views/kiso/components/{_empty_state.html.erb → _empty.html.erb} +2 -2
- data/app/views/kiso/components/_field.html.erb +12 -0
- data/app/views/kiso/components/_field_group.html.erb +7 -0
- data/app/views/kiso/components/_field_set.html.erb +7 -0
- data/app/views/kiso/components/_input.html.erb +8 -0
- data/app/views/kiso/components/_input_group.html.erb +8 -0
- data/app/views/kiso/components/_input_otp.html.erb +22 -0
- data/app/views/kiso/components/_kbd.html.erb +7 -0
- data/app/views/kiso/components/_label.html.erb +5 -0
- data/app/views/kiso/components/_nav.html.erb +7 -0
- data/app/views/kiso/components/_pagination.html.erb +9 -0
- data/app/views/kiso/components/_popover.html.erb +8 -0
- data/app/views/kiso/components/_radio_group.html.erb +8 -0
- data/app/views/kiso/components/_select.html.erb +8 -0
- data/app/views/kiso/components/_select_native.html.erb +16 -0
- data/app/views/kiso/components/_separator.html.erb +1 -1
- data/app/views/kiso/components/_stats_card.html.erb +1 -1
- data/app/views/kiso/components/_stats_grid.html.erb +1 -1
- data/app/views/kiso/components/_switch.html.erb +10 -0
- data/app/views/kiso/components/_table.html.erb +2 -1
- data/app/views/kiso/components/_textarea.html.erb +9 -0
- data/app/views/kiso/components/_toggle.html.erb +12 -0
- data/app/views/kiso/components/_toggle_group.html.erb +12 -0
- data/app/views/kiso/components/alert/_description.html.erb +1 -1
- data/app/views/kiso/components/alert/_title.html.erb +1 -1
- data/app/views/kiso/components/avatar/_badge.html.erb +7 -0
- data/app/views/kiso/components/avatar/_fallback.html.erb +7 -0
- data/app/views/kiso/components/avatar/_group.html.erb +7 -0
- data/app/views/kiso/components/avatar/_group_count.html.erb +7 -0
- data/app/views/kiso/components/avatar/_image.html.erb +6 -0
- data/app/views/kiso/components/breadcrumb/_ellipsis.html.erb +10 -0
- data/app/views/kiso/components/breadcrumb/_item.html.erb +7 -0
- data/app/views/kiso/components/breadcrumb/_link.html.erb +7 -0
- data/app/views/kiso/components/breadcrumb/_list.html.erb +7 -0
- data/app/views/kiso/components/breadcrumb/_page.html.erb +9 -0
- data/app/views/kiso/components/breadcrumb/_separator.html.erb +9 -0
- data/app/views/kiso/components/card/_action.html.erb +7 -0
- data/app/views/kiso/components/card/_content.html.erb +1 -1
- data/app/views/kiso/components/card/_description.html.erb +1 -1
- data/app/views/kiso/components/card/_footer.html.erb +1 -1
- data/app/views/kiso/components/card/_header.html.erb +1 -1
- data/app/views/kiso/components/card/_title.html.erb +1 -1
- data/app/views/kiso/components/combobox/_chip.html.erb +19 -0
- data/app/views/kiso/components/combobox/_chips.html.erb +20 -0
- data/app/views/kiso/components/combobox/_chips_input.html.erb +10 -0
- data/app/views/kiso/components/combobox/_content.html.erb +9 -0
- data/app/views/kiso/components/combobox/_empty.html.erb +9 -0
- data/app/views/kiso/components/combobox/_group.html.erb +8 -0
- data/app/views/kiso/components/combobox/_input.html.erb +23 -0
- data/app/views/kiso/components/combobox/_item.html.erb +19 -0
- data/app/views/kiso/components/combobox/_label.html.erb +7 -0
- data/app/views/kiso/components/combobox/_list.html.erb +10 -0
- data/app/views/kiso/components/combobox/_separator.html.erb +6 -0
- data/app/views/kiso/components/command/_dialog.html.erb +11 -0
- data/app/views/kiso/components/command/_empty.html.erb +9 -0
- data/app/views/kiso/components/command/_group.html.erb +14 -0
- data/app/views/kiso/components/command/_input.html.erb +16 -0
- data/app/views/kiso/components/command/_item.html.erb +13 -0
- data/app/views/kiso/components/command/_list.html.erb +10 -0
- data/app/views/kiso/components/command/_separator.html.erb +7 -0
- data/app/views/kiso/components/command/_shortcut.html.erb +7 -0
- data/app/views/kiso/components/dashboard_navbar/_toggle.html.erb +11 -0
- data/app/views/kiso/components/dashboard_sidebar/_collapse.html.erb +12 -0
- data/app/views/kiso/components/dashboard_sidebar/_footer.html.erb +7 -0
- data/app/views/kiso/components/dashboard_sidebar/_header.html.erb +7 -0
- data/app/views/kiso/components/dashboard_sidebar/_toggle.html.erb +11 -0
- data/app/views/kiso/components/dashboard_toolbar/_left.html.erb +7 -0
- data/app/views/kiso/components/dashboard_toolbar/_right.html.erb +7 -0
- data/app/views/kiso/components/dropdown_menu/_checkbox_item.html.erb +18 -0
- data/app/views/kiso/components/dropdown_menu/_content.html.erb +10 -0
- data/app/views/kiso/components/dropdown_menu/_group.html.erb +8 -0
- data/app/views/kiso/components/dropdown_menu/_item.html.erb +15 -0
- data/app/views/kiso/components/dropdown_menu/_label.html.erb +8 -0
- data/app/views/kiso/components/dropdown_menu/_radio_group.html.erb +10 -0
- data/app/views/kiso/components/dropdown_menu/_radio_item.html.erb +19 -0
- data/app/views/kiso/components/dropdown_menu/_separator.html.erb +6 -0
- data/app/views/kiso/components/dropdown_menu/_shortcut.html.erb +7 -0
- data/app/views/kiso/components/dropdown_menu/_sub.html.erb +8 -0
- data/app/views/kiso/components/dropdown_menu/_sub_content.html.erb +10 -0
- data/app/views/kiso/components/dropdown_menu/_sub_trigger.html.erb +12 -0
- data/app/views/kiso/components/dropdown_menu/_trigger.html.erb +9 -0
- data/app/views/kiso/components/empty/_content.html.erb +7 -0
- data/app/views/kiso/components/empty/_description.html.erb +7 -0
- data/app/views/kiso/components/empty/_header.html.erb +7 -0
- data/app/views/kiso/components/empty/_media.html.erb +7 -0
- data/app/views/kiso/components/empty/_title.html.erb +7 -0
- data/app/views/kiso/components/field/_content.html.erb +7 -0
- data/app/views/kiso/components/field/_description.html.erb +7 -0
- data/app/views/kiso/components/field/_error.html.erb +22 -0
- data/app/views/kiso/components/field/_label.html.erb +5 -0
- data/app/views/kiso/components/field/_separator.html.erb +15 -0
- data/app/views/kiso/components/field/_title.html.erb +7 -0
- data/app/views/kiso/components/field_set/_legend.html.erb +9 -0
- data/app/views/kiso/components/input_group/_addon.html.erb +7 -0
- data/app/views/kiso/components/input_otp/_group.html.erb +7 -0
- data/app/views/kiso/components/input_otp/_separator.html.erb +8 -0
- data/app/views/kiso/components/input_otp/_slot.html.erb +11 -0
- data/app/views/kiso/components/kbd/_group.html.erb +7 -0
- data/app/views/kiso/components/nav/_item.html.erb +15 -0
- data/app/views/kiso/components/nav/_section.html.erb +37 -0
- data/app/views/kiso/components/nav/_section_title.html.erb +7 -0
- data/app/views/kiso/components/pagination/_content.html.erb +7 -0
- data/app/views/kiso/components/pagination/_ellipsis.html.erb +9 -0
- data/app/views/kiso/components/pagination/_item.html.erb +7 -0
- data/app/views/kiso/components/pagination/_link.html.erb +9 -0
- data/app/views/kiso/components/pagination/_next.html.erb +12 -0
- data/app/views/kiso/components/pagination/_previous.html.erb +12 -0
- data/app/views/kiso/components/popover/_anchor.html.erb +8 -0
- data/app/views/kiso/components/popover/_content.html.erb +11 -0
- data/app/views/kiso/components/popover/_description.html.erb +7 -0
- data/app/views/kiso/components/popover/_header.html.erb +7 -0
- data/app/views/kiso/components/popover/_title.html.erb +7 -0
- data/app/views/kiso/components/popover/_trigger.html.erb +9 -0
- data/app/views/kiso/components/radio_group/_item.html.erb +6 -0
- data/app/views/kiso/components/select/_content.html.erb +10 -0
- data/app/views/kiso/components/select/_group.html.erb +8 -0
- data/app/views/kiso/components/select/_item.html.erb +19 -0
- data/app/views/kiso/components/select/_label.html.erb +7 -0
- data/app/views/kiso/components/select/_separator.html.erb +6 -0
- data/app/views/kiso/components/select/_trigger.html.erb +13 -0
- data/app/views/kiso/components/select/_value.html.erb +11 -0
- data/app/views/kiso/components/stats_card/_description.html.erb +1 -1
- data/app/views/kiso/components/stats_card/_header.html.erb +1 -1
- data/app/views/kiso/components/stats_card/_label.html.erb +1 -1
- data/app/views/kiso/components/stats_card/_value.html.erb +1 -1
- data/app/views/kiso/components/table/_body.html.erb +1 -1
- data/app/views/kiso/components/table/_caption.html.erb +1 -1
- data/app/views/kiso/components/table/_cell.html.erb +1 -1
- data/app/views/kiso/components/table/_footer.html.erb +1 -1
- data/app/views/kiso/components/table/_head.html.erb +1 -1
- data/app/views/kiso/components/table/_header.html.erb +1 -1
- data/app/views/kiso/components/table/_row.html.erb +1 -1
- data/app/views/kiso/components/toggle_group/_item.html.erb +13 -0
- data/config/deploy.docs.yml +31 -0
- data/config/deploy.yml +34 -0
- data/config/importmap.rb +10 -0
- data/lib/kiso/cli/base.rb +15 -0
- data/lib/kiso/cli/icons.rb +2 -1
- data/lib/kiso/cli/main.rb +6 -0
- data/lib/kiso/cli/make.rb +22 -12
- data/lib/kiso/configuration.rb +54 -0
- data/lib/kiso/engine.rb +36 -1
- data/lib/kiso/theme_overrides.rb +130 -0
- data/lib/kiso/themes/alert.rb +16 -1
- data/lib/kiso/themes/avatar.rb +53 -0
- data/lib/kiso/themes/badge.rb +15 -5
- data/lib/kiso/themes/breadcrumb.rb +44 -0
- data/lib/kiso/themes/button.rb +15 -2
- data/lib/kiso/themes/card.rb +18 -2
- data/lib/kiso/themes/checkbox.rb +33 -0
- data/lib/kiso/themes/color_mode_button.rb +15 -0
- data/lib/kiso/themes/color_mode_select.rb +7 -0
- data/lib/kiso/themes/combobox.rb +97 -0
- data/lib/kiso/themes/command.rb +79 -0
- data/lib/kiso/themes/dashboard.rb +51 -0
- data/lib/kiso/themes/dropdown_menu.rb +108 -0
- data/lib/kiso/themes/empty.rb +54 -0
- data/lib/kiso/themes/field.rb +76 -0
- data/lib/kiso/themes/field_group.rb +15 -0
- data/lib/kiso/themes/field_set.rb +32 -0
- data/lib/kiso/themes/input.rb +33 -0
- data/lib/kiso/themes/input_group.rb +39 -0
- data/lib/kiso/themes/input_otp.rb +46 -0
- data/lib/kiso/themes/kbd.rb +31 -0
- data/lib/kiso/themes/label.rb +16 -0
- data/lib/kiso/themes/nav.rb +27 -0
- data/lib/kiso/themes/pagination.rb +73 -0
- data/lib/kiso/themes/popover.rb +32 -0
- data/lib/kiso/themes/radio_group.rb +43 -0
- data/lib/kiso/themes/select.rb +78 -0
- data/lib/kiso/themes/select_native.rb +49 -0
- data/lib/kiso/themes/separator.rb +8 -2
- data/lib/kiso/themes/shared.rb +51 -0
- data/lib/kiso/themes/stats_card.rb +26 -14
- data/lib/kiso/themes/switch.rb +56 -0
- data/lib/kiso/themes/table.rb +18 -15
- data/lib/kiso/themes/textarea.rb +33 -0
- data/lib/kiso/themes/toggle.rb +71 -0
- data/lib/kiso/themes/toggle_group.rb +13 -0
- data/lib/kiso/version.rb +4 -1
- data/lib/kiso.rb +70 -2
- metadata +183 -22
- data/app/views/kiso/components/empty_state/_content.html.erb +0 -7
- data/app/views/kiso/components/empty_state/_description.html.erb +0 -7
- data/app/views/kiso/components/empty_state/_header.html.erb +0 -7
- data/app/views/kiso/components/empty_state/_media.html.erb +0 -7
- data/app/views/kiso/components/empty_state/_title.html.erb +0 -7
- data/lib/kiso/themes/empty_state.rb +0 -42
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import KisoComboboxController from "./combobox_controller.js"
|
|
2
|
+
import KisoCommandController from "./command_controller.js"
|
|
3
|
+
import KisoCommandDialogController from "./command_dialog_controller.js"
|
|
4
|
+
import KisoDropdownMenuController from "./dropdown_menu_controller.js"
|
|
5
|
+
import KisoInputOtpController from "./input_otp_controller.js"
|
|
6
|
+
import KisoPopoverController from "./popover_controller.js"
|
|
7
|
+
import KisoSelectController from "./select_controller.js"
|
|
8
|
+
import KisoSidebarController from "./sidebar_controller.js"
|
|
9
|
+
import KisoThemeController from "./theme_controller.js"
|
|
10
|
+
import KisoToggleController from "./toggle_controller.js"
|
|
11
|
+
import KisoToggleGroupController from "./toggle_group_controller.js"
|
|
12
|
+
|
|
13
|
+
const KisoUi = {
|
|
14
|
+
start(application) {
|
|
15
|
+
application.register("kiso--combobox", KisoComboboxController)
|
|
16
|
+
application.register("kiso--command", KisoCommandController)
|
|
17
|
+
application.register("kiso--command-dialog", KisoCommandDialogController)
|
|
18
|
+
application.register("kiso--dropdown-menu", KisoDropdownMenuController)
|
|
19
|
+
application.register("kiso--input-otp", KisoInputOtpController)
|
|
20
|
+
application.register("kiso--popover", KisoPopoverController)
|
|
21
|
+
application.register("kiso--select", KisoSelectController)
|
|
22
|
+
application.register("kiso--sidebar", KisoSidebarController)
|
|
23
|
+
application.register("kiso--theme", KisoThemeController)
|
|
24
|
+
application.register("kiso--toggle", KisoToggleController)
|
|
25
|
+
application.register("kiso--toggle-group", KisoToggleGroupController)
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default KisoUi
|
|
30
|
+
export {
|
|
31
|
+
KisoComboboxController,
|
|
32
|
+
KisoCommandController,
|
|
33
|
+
KisoCommandDialogController,
|
|
34
|
+
KisoDropdownMenuController,
|
|
35
|
+
KisoInputOtpController,
|
|
36
|
+
KisoPopoverController,
|
|
37
|
+
KisoSelectController,
|
|
38
|
+
KisoSidebarController,
|
|
39
|
+
KisoThemeController,
|
|
40
|
+
KisoToggleController,
|
|
41
|
+
KisoToggleGroupController,
|
|
42
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages a one-time password input with individual visual character slots.
|
|
5
|
+
*
|
|
6
|
+
* A single transparent `<input>` overlays the visual slots. The browser handles
|
|
7
|
+
* all native input behavior (focus, paste, mobile autofill). This controller
|
|
8
|
+
* distributes the input value to slot elements and tracks the active slot via
|
|
9
|
+
* `selectionStart`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <div data-controller="kiso--input-otp"
|
|
13
|
+
* data-kiso--input-otp-length-value="6"
|
|
14
|
+
* data-kiso--input-otp-pattern-value="\\d">
|
|
15
|
+
* <input data-kiso--input-otp-target="input"
|
|
16
|
+
* type="text" maxlength="6" autocomplete="one-time-code"
|
|
17
|
+
* class="absolute inset-0 z-10 w-full h-full opacity-0">
|
|
18
|
+
* <div data-slot="input-otp-group">
|
|
19
|
+
* <div data-kiso--input-otp-target="slot" data-slot="input-otp-slot">
|
|
20
|
+
* <span data-slot="input-otp-slot-char"></span>
|
|
21
|
+
* <div data-slot="input-otp-caret" hidden>
|
|
22
|
+
* <div class="bg-foreground h-4 w-px animate-caret-blink"></div>
|
|
23
|
+
* </div>
|
|
24
|
+
* </div>
|
|
25
|
+
* </div>
|
|
26
|
+
* </div>
|
|
27
|
+
*
|
|
28
|
+
* @property {HTMLInputElement} inputTarget - The transparent real input element
|
|
29
|
+
* @property {HTMLElement[]} slotTargets - Visual slot div elements
|
|
30
|
+
* @property {Number} lengthValue - Expected OTP code length
|
|
31
|
+
* @property {String} patternValue - Regex pattern for allowed characters (default: digits only)
|
|
32
|
+
*
|
|
33
|
+
* @fires kiso--input-otp:change - When the OTP value changes.
|
|
34
|
+
* Detail: `{ value: string }`
|
|
35
|
+
* @fires kiso--input-otp:complete - When all slots are filled.
|
|
36
|
+
* Detail: `{ value: string }`
|
|
37
|
+
*/
|
|
38
|
+
export default class extends Controller {
|
|
39
|
+
static targets = ["input", "slot"]
|
|
40
|
+
static values = {
|
|
41
|
+
length: { type: Number, default: 6 },
|
|
42
|
+
pattern: { type: String, default: "\\d" },
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Binds event listeners and syncs slots from any pre-filled value.
|
|
47
|
+
*/
|
|
48
|
+
connect() {
|
|
49
|
+
this._handleInput = this._handleInput.bind(this)
|
|
50
|
+
this._handleKeydown = this._handleKeydown.bind(this)
|
|
51
|
+
this._handleFocus = this._handleFocus.bind(this)
|
|
52
|
+
this._handleBlur = this._handleBlur.bind(this)
|
|
53
|
+
this._handleClick = this._handleClick.bind(this)
|
|
54
|
+
|
|
55
|
+
this.inputTarget.addEventListener("input", this._handleInput)
|
|
56
|
+
this.inputTarget.addEventListener("keydown", this._handleKeydown)
|
|
57
|
+
this.inputTarget.addEventListener("focus", this._handleFocus)
|
|
58
|
+
this.inputTarget.addEventListener("blur", this._handleBlur)
|
|
59
|
+
this.element.addEventListener("click", this._handleClick)
|
|
60
|
+
|
|
61
|
+
this._syncSlots()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Removes event listeners.
|
|
66
|
+
*/
|
|
67
|
+
disconnect() {
|
|
68
|
+
this.inputTarget.removeEventListener("input", this._handleInput)
|
|
69
|
+
this.inputTarget.removeEventListener("keydown", this._handleKeydown)
|
|
70
|
+
this.inputTarget.removeEventListener("focus", this._handleFocus)
|
|
71
|
+
this.inputTarget.removeEventListener("blur", this._handleBlur)
|
|
72
|
+
this.element.removeEventListener("click", this._handleClick)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Filters input against the pattern, syncs slots, and dispatches events.
|
|
77
|
+
*
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
_handleInput() {
|
|
81
|
+
const regex = new RegExp(this.patternValue)
|
|
82
|
+
const filtered = this.inputTarget.value
|
|
83
|
+
.split("")
|
|
84
|
+
.filter((char) => regex.test(char))
|
|
85
|
+
.join("")
|
|
86
|
+
.slice(0, this.lengthValue)
|
|
87
|
+
|
|
88
|
+
if (filtered !== this.inputTarget.value) {
|
|
89
|
+
this.inputTarget.value = filtered
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this._syncSlots()
|
|
93
|
+
this._updateActiveSlot()
|
|
94
|
+
|
|
95
|
+
this.dispatch("change", { detail: { value: filtered } })
|
|
96
|
+
|
|
97
|
+
if (filtered.length === this.lengthValue) {
|
|
98
|
+
this.dispatch("complete", { detail: { value: filtered } })
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Re-syncs the active slot after keydown events that may change selection
|
|
104
|
+
* (arrow keys, backspace, delete, home, end).
|
|
105
|
+
*
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
_handleKeydown() {
|
|
109
|
+
requestAnimationFrame(() => this._updateActiveSlot())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Shows the caret on the active slot when the input receives focus.
|
|
114
|
+
*
|
|
115
|
+
* @private
|
|
116
|
+
*/
|
|
117
|
+
_handleFocus() {
|
|
118
|
+
this._updateActiveSlot()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Clears all active states and hides all carets on blur.
|
|
123
|
+
*
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
_handleBlur() {
|
|
127
|
+
this._clearActiveSlots()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Focuses the transparent input when clicking anywhere on the component.
|
|
132
|
+
* Places the cursor at the end of the current value.
|
|
133
|
+
*
|
|
134
|
+
* @private
|
|
135
|
+
*/
|
|
136
|
+
_handleClick() {
|
|
137
|
+
this.inputTarget.focus()
|
|
138
|
+
const len = this.inputTarget.value.length
|
|
139
|
+
this.inputTarget.setSelectionRange(len, len)
|
|
140
|
+
this._updateActiveSlot()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Distributes input value characters to visual slot elements.
|
|
145
|
+
* Each slot's `[data-slot="input-otp-slot-char"]` span gets its character.
|
|
146
|
+
*
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
_syncSlots() {
|
|
150
|
+
const value = this.inputTarget.value
|
|
151
|
+
this.slotTargets.forEach((slot, index) => {
|
|
152
|
+
const charEl = slot.querySelector("[data-slot='input-otp-slot-char']")
|
|
153
|
+
if (charEl) {
|
|
154
|
+
charEl.textContent = value[index] || ""
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Sets `data-active="true"` on the slot matching the cursor position.
|
|
161
|
+
* Shows the blinking caret on the active empty slot.
|
|
162
|
+
*
|
|
163
|
+
* @private
|
|
164
|
+
*/
|
|
165
|
+
_updateActiveSlot() {
|
|
166
|
+
if (document.activeElement !== this.inputTarget) return
|
|
167
|
+
|
|
168
|
+
const pos = Math.min(this.inputTarget.selectionStart ?? 0, this.lengthValue - 1)
|
|
169
|
+
|
|
170
|
+
this.slotTargets.forEach((slot, index) => {
|
|
171
|
+
const isActive = index === pos
|
|
172
|
+
const charEl = slot.querySelector("[data-slot='input-otp-slot-char']")
|
|
173
|
+
const caretEl = slot.querySelector("[data-slot='input-otp-caret']")
|
|
174
|
+
const hasChar = charEl && charEl.textContent !== ""
|
|
175
|
+
|
|
176
|
+
slot.dataset.active = isActive
|
|
177
|
+
if (caretEl) {
|
|
178
|
+
caretEl.hidden = !(isActive && !hasChar)
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Removes all active states and hides all carets.
|
|
185
|
+
*
|
|
186
|
+
* @private
|
|
187
|
+
*/
|
|
188
|
+
_clearActiveSlots() {
|
|
189
|
+
this.slotTargets.forEach((slot) => {
|
|
190
|
+
delete slot.dataset.active
|
|
191
|
+
const caretEl = slot.querySelector("[data-slot='input-otp-caret']")
|
|
192
|
+
if (caretEl) caretEl.hidden = true
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { FOCUSABLE_SELECTOR } from "kiso-ui/utils/focusable"
|
|
3
|
+
import { startPositioning } from "kiso-ui/utils/positioning"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Popover controller — toggles a floating panel anchored to a trigger element.
|
|
7
|
+
* Supports configurable alignment (start, center, end), focus trapping,
|
|
8
|
+
* and close-on-outside-click / Escape.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <div data-controller="kiso--popover" data-slot="popover">
|
|
12
|
+
* <button data-kiso--popover-target="trigger"
|
|
13
|
+
* data-action="click->kiso--popover#toggle keydown->kiso--popover#triggerKeydown">
|
|
14
|
+
* Open
|
|
15
|
+
* </button>
|
|
16
|
+
* <div data-kiso--popover-target="content" data-align="center" role="dialog" hidden>
|
|
17
|
+
* Content here
|
|
18
|
+
* </div>
|
|
19
|
+
* </div>
|
|
20
|
+
*
|
|
21
|
+
* @property {HTMLElement} triggerTarget - Button that opens/closes the popover
|
|
22
|
+
* @property {HTMLElement} contentTarget - The floating panel
|
|
23
|
+
* @property {HTMLElement} [anchorTarget] - Optional alternate positioning reference
|
|
24
|
+
*/
|
|
25
|
+
export default class extends Controller {
|
|
26
|
+
static targets = ["trigger", "content", "anchor"]
|
|
27
|
+
|
|
28
|
+
connect() {
|
|
29
|
+
this._open = false
|
|
30
|
+
this._handleOutsideClick = this._handleOutsideClick.bind(this)
|
|
31
|
+
this._handleKeydown = this._handleKeydown.bind(this)
|
|
32
|
+
|
|
33
|
+
// Set ARIA attrs on the interactive element inside the trigger wrapper
|
|
34
|
+
this._triggerButton =
|
|
35
|
+
this.triggerTarget.querySelector("button, [tabindex]") || this.triggerTarget
|
|
36
|
+
this._triggerButton.setAttribute("aria-haspopup", "dialog")
|
|
37
|
+
this._triggerButton.setAttribute("aria-expanded", "false")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
disconnect() {
|
|
41
|
+
this._cleanupPosition?.()
|
|
42
|
+
this._clearCloseTimers()
|
|
43
|
+
this._removeGlobalListeners()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Toggles the popover open or closed.
|
|
48
|
+
* Skips if the click was auto-generated by a Space keyup (already handled in triggerKeydown).
|
|
49
|
+
*
|
|
50
|
+
* @param {Event} event
|
|
51
|
+
*/
|
|
52
|
+
toggle(event) {
|
|
53
|
+
if (this._ignoreNextClick) {
|
|
54
|
+
this._ignoreNextClick = false
|
|
55
|
+
event.preventDefault()
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
event.preventDefault()
|
|
59
|
+
if (this._open) {
|
|
60
|
+
this.close()
|
|
61
|
+
} else {
|
|
62
|
+
this.open()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Opens the popover, positions it relative to the trigger (or anchor),
|
|
68
|
+
* and focuses the first focusable element inside.
|
|
69
|
+
*/
|
|
70
|
+
open() {
|
|
71
|
+
if (this._open) return
|
|
72
|
+
|
|
73
|
+
this._clearCloseTimers()
|
|
74
|
+
this._open = true
|
|
75
|
+
this.contentTarget.hidden = false
|
|
76
|
+
this.contentTarget.setAttribute("data-state", "open")
|
|
77
|
+
this._triggerButton.setAttribute("aria-expanded", "true")
|
|
78
|
+
this.triggerTarget.setAttribute("data-state", "open")
|
|
79
|
+
this._positionContent()
|
|
80
|
+
this._addGlobalListeners()
|
|
81
|
+
|
|
82
|
+
// Focus the first focusable element inside the content
|
|
83
|
+
this._focusRaf = requestAnimationFrame(() => {
|
|
84
|
+
const focusable = this.contentTarget.querySelector(FOCUSABLE_SELECTOR)
|
|
85
|
+
if (focusable) {
|
|
86
|
+
focusable.focus()
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Closes the popover with a closing animation, then hides it.
|
|
93
|
+
* Returns focus to the trigger button.
|
|
94
|
+
*/
|
|
95
|
+
close() {
|
|
96
|
+
if (!this._open) return
|
|
97
|
+
|
|
98
|
+
this._cleanupPosition?.()
|
|
99
|
+
this._cleanupPosition = null
|
|
100
|
+
this._clearCloseTimers()
|
|
101
|
+
this._open = false
|
|
102
|
+
this.contentTarget.setAttribute("data-state", "closed")
|
|
103
|
+
this._triggerButton.setAttribute("aria-expanded", "false")
|
|
104
|
+
this.triggerTarget.setAttribute("data-state", "closed")
|
|
105
|
+
|
|
106
|
+
// Hide after animation completes
|
|
107
|
+
this._animationEndHandler = () => {
|
|
108
|
+
this.contentTarget.hidden = true
|
|
109
|
+
this.contentTarget.removeEventListener("animationend", this._animationEndHandler)
|
|
110
|
+
}
|
|
111
|
+
this.contentTarget.addEventListener("animationend", this._animationEndHandler)
|
|
112
|
+
|
|
113
|
+
// Fallback: if no animation, hide immediately
|
|
114
|
+
this._closeTimeout = setTimeout(() => {
|
|
115
|
+
if (this.contentTarget.getAttribute("data-state") === "closed") {
|
|
116
|
+
this.contentTarget.hidden = true
|
|
117
|
+
}
|
|
118
|
+
}, 200)
|
|
119
|
+
|
|
120
|
+
this._removeGlobalListeners()
|
|
121
|
+
// Focus the button inside the trigger wrapper, or the trigger itself
|
|
122
|
+
const btn = this.triggerTarget.querySelector("button, [tabindex]")
|
|
123
|
+
;(btn || this.triggerTarget).focus()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Opens the popover on ArrowDown, Space, or Enter when trigger is focused.
|
|
128
|
+
*
|
|
129
|
+
* @param {KeyboardEvent} event
|
|
130
|
+
*/
|
|
131
|
+
triggerKeydown(event) {
|
|
132
|
+
switch (event.key) {
|
|
133
|
+
case "ArrowDown":
|
|
134
|
+
case " ":
|
|
135
|
+
case "Enter":
|
|
136
|
+
event.preventDefault()
|
|
137
|
+
if (!this._open) {
|
|
138
|
+
this._ignoreNextClick = true
|
|
139
|
+
this.open()
|
|
140
|
+
}
|
|
141
|
+
break
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- Private ---
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Cancels any pending close timers and animation handlers.
|
|
149
|
+
*
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
152
|
+
_clearCloseTimers() {
|
|
153
|
+
if (this._closeTimeout) {
|
|
154
|
+
clearTimeout(this._closeTimeout)
|
|
155
|
+
this._closeTimeout = null
|
|
156
|
+
}
|
|
157
|
+
if (this._focusRaf) {
|
|
158
|
+
cancelAnimationFrame(this._focusRaf)
|
|
159
|
+
this._focusRaf = null
|
|
160
|
+
}
|
|
161
|
+
if (this._animationEndHandler) {
|
|
162
|
+
this.contentTarget.removeEventListener("animationend", this._animationEndHandler)
|
|
163
|
+
this._animationEndHandler = null
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @private */
|
|
168
|
+
static _alignToPlacement = {
|
|
169
|
+
start: "bottom-start",
|
|
170
|
+
center: "bottom",
|
|
171
|
+
end: "bottom-end",
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Positions the content panel relative to the reference element (trigger or anchor).
|
|
176
|
+
* Respects `data-align` on the content element: "start", "end", or "center" (default).
|
|
177
|
+
* Starts auto-updating on scroll/resize.
|
|
178
|
+
*
|
|
179
|
+
* @private
|
|
180
|
+
*/
|
|
181
|
+
_positionContent() {
|
|
182
|
+
const reference = this.hasAnchorTarget ? this.anchorTarget : this.triggerTarget
|
|
183
|
+
const align = this.contentTarget.dataset.align || "center"
|
|
184
|
+
const placement = this.constructor._alignToPlacement[align] || "bottom"
|
|
185
|
+
|
|
186
|
+
this._cleanupPosition = startPositioning(reference, this.contentTarget, {
|
|
187
|
+
placement,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Closes the popover when clicking outside the component.
|
|
193
|
+
*
|
|
194
|
+
* @param {MouseEvent} event
|
|
195
|
+
* @private
|
|
196
|
+
*/
|
|
197
|
+
_handleOutsideClick(event) {
|
|
198
|
+
if (!this.element.contains(event.target)) {
|
|
199
|
+
this.close()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Handles Escape to close and Tab to trap focus within the popover content.
|
|
205
|
+
*
|
|
206
|
+
* @param {KeyboardEvent} event
|
|
207
|
+
* @private
|
|
208
|
+
*/
|
|
209
|
+
_handleKeydown(event) {
|
|
210
|
+
if (!this._open) return
|
|
211
|
+
|
|
212
|
+
if (event.key === "Escape") {
|
|
213
|
+
event.preventDefault()
|
|
214
|
+
this.close()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Trap focus inside content
|
|
218
|
+
if (event.key === "Tab") {
|
|
219
|
+
const focusableElements = this.contentTarget.querySelectorAll(FOCUSABLE_SELECTOR)
|
|
220
|
+
|
|
221
|
+
if (focusableElements.length === 0) {
|
|
222
|
+
event.preventDefault()
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const first = focusableElements[0]
|
|
227
|
+
const last = focusableElements[focusableElements.length - 1]
|
|
228
|
+
|
|
229
|
+
if (event.shiftKey) {
|
|
230
|
+
if (document.activeElement === first) {
|
|
231
|
+
event.preventDefault()
|
|
232
|
+
last.focus()
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
if (document.activeElement === last) {
|
|
236
|
+
event.preventDefault()
|
|
237
|
+
first.focus()
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** @private */
|
|
244
|
+
_addGlobalListeners() {
|
|
245
|
+
document.addEventListener("click", this._handleOutsideClick, true)
|
|
246
|
+
document.addEventListener("keydown", this._handleKeydown)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** @private */
|
|
250
|
+
_removeGlobalListeners() {
|
|
251
|
+
document.removeEventListener("click", this._handleOutsideClick, true)
|
|
252
|
+
document.removeEventListener("keydown", this._handleKeydown)
|
|
253
|
+
}
|
|
254
|
+
}
|