kiso 0.4.3.pre → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e16c80be5816e217b497d96bbaca01d358ed7d9d9a76c885e0f3c0f4e482470
4
- data.tar.gz: 5017356a702d692ec3a08f86de357392a7bd93e4bee6b1fef4096c6433249fcc
3
+ metadata.gz: 4bf4475ba64978e58e6321ac224b3912cf463dae75462ee3372f86faf4b69799
4
+ data.tar.gz: ee8c23fdd9a81b0b4abd8a097371a4720b8327e2c719c6b1a5cb84ea21a22962
5
5
  SHA512:
6
- metadata.gz: a8ac36cb673b6f59b5fbd899e5c4117e525504d2d50a5b44bd08904c710b7bec469fa2f3f2c9b1fa4d349f2a0e965c98b394668fd7985b5a38210f93894aeb1b
7
- data.tar.gz: 9d1b05dda12c29966829fde3eb6f7a42f1f36b29973d5674a637350ffe90e73a345c34c648833cbea21665fe42260ddd224f7272e231299f2993a6ef20eb5250
6
+ metadata.gz: abdb896617cba586d6906624bb2e30434ad40ccd2c1a12d589284ca43149056724c4ecb6dad7cb4a27915f3dad846e64b2a12bc3efa5a0d0fe82378aaf6494ad
7
+ data.tar.gz: 00d0ce8bd9933e5f3500d3ac0dfa57a068ae8c3923371943e398122f179e508fe5c7bd39b308df15a92eda836e50a41693be5e295e5a7c1bd3f6aea1bf2a57af
@@ -0,0 +1,10 @@
1
+ /* Default display for buttons — lives in @layer components so utility classes
2
+ (including `hidden`) override via the higher-priority utilities layer. When
3
+ a utility like `hidden` is removed via JS, this rule restores inline-flex.
4
+ Fixes: https://github.com/steveclarke/kiso/issues/200 */
5
+
6
+ @layer components {
7
+ [data-slot="button"] {
8
+ display: inline-flex;
9
+ }
10
+ }
@@ -3,6 +3,7 @@
3
3
  that are awkward to express in ERB. Most styling lives in Ruby theme modules
4
4
  (lib/kiso/themes/) as computed Tailwind classes. */
5
5
 
6
+ @import "./button.css";
6
7
  @import "./checkbox.css";
7
8
  @import "./radio-group.css";
8
9
  @import "./color-mode.css";
@@ -10,6 +11,7 @@
10
11
  @import "./dialog.css";
11
12
  @import "./input-otp.css";
12
13
  @import "./slider.css";
14
+ @import "./tooltip.css";
13
15
 
14
16
  /* Scan Kiso's own files so host apps don't need to know the gem's internals.
15
17
  Paths are relative to THIS file (app/assets/tailwind/kiso/engine.css).
@@ -0,0 +1,48 @@
1
+ /* Tooltip entry/exit animations and arrow positioning. */
2
+
3
+ /* Tooltip root — inline-flex via @layer components so utilities can override */
4
+ @layer components {
5
+ [data-slot="tooltip"] {
6
+ display: inline-flex;
7
+ }
8
+ }
9
+
10
+ /* The browser UA stylesheet hides [popover]:not(:popover-open) with
11
+ display: none, but Tailwind's `flex` utility (author layer) overrides it.
12
+ Explicitly enforce hidden state for non-open popovers. */
13
+ [data-slot="tooltip-content"]:not(:popover-open) {
14
+ display: none;
15
+ }
16
+
17
+ /* Reset UA popover styles — browsers apply inset: 0, margin: auto, and
18
+ width/height: fit-content to [popover]:popover-open, which overrides
19
+ Floating UI's inline-style positioning. */
20
+ [data-slot="tooltip-content"] {
21
+ inset: unset;
22
+ margin: 0;
23
+ }
24
+
25
+ /* Entry animation — fade + scale from 95% */
26
+ [data-slot="tooltip-content"][data-state="open"] {
27
+ animation: kiso-tooltip-in 150ms ease-out;
28
+ }
29
+
30
+ /* Exit animation — fade out + scale to 95% */
31
+ [data-slot="tooltip-content"][data-state="closed"] {
32
+ animation: kiso-tooltip-out 100ms ease-in forwards;
33
+ }
34
+
35
+ @keyframes kiso-tooltip-in {
36
+ from { opacity: 0; transform: scale(0.95); }
37
+ }
38
+ @keyframes kiso-tooltip-out {
39
+ to { opacity: 0; transform: scale(0.95); }
40
+ }
41
+
42
+ /* Respect reduced motion */
43
+ @media (prefers-reduced-motion: reduce) {
44
+ [data-slot="tooltip-content"][data-state="open"],
45
+ [data-slot="tooltip-content"][data-state="closed"] {
46
+ animation: none;
47
+ }
48
+ }
@@ -0,0 +1,80 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Opens or closes a dialog from anywhere on the page by ID.
5
+ *
6
+ * Use when the trigger button lives outside the dialog's DOM scope
7
+ * (e.g., a toolbar button opening a confirmation dialog rendered
8
+ * elsewhere on the page).
9
+ *
10
+ * Works with both `kui(:dialog)` and `kui(:alert_dialog)` since
11
+ * both use the `kiso--dialog` Stimulus controller internally.
12
+ *
13
+ * @example
14
+ * <!-- Dialog rendered anywhere -->
15
+ * <dialog id="invite-dialog" data-controller="kiso--dialog" ...>
16
+ * ...
17
+ * </dialog>
18
+ *
19
+ * <!-- Trigger from a toolbar, sidebar, or any other scope -->
20
+ * <button data-controller="kiso--dialog-trigger"
21
+ * data-kiso--dialog-trigger-dialog-id-value="invite-dialog"
22
+ * data-action="kiso--dialog-trigger#open">
23
+ * Invite
24
+ * </button>
25
+ *
26
+ * @property {string} dialogIdValue - The DOM id of the target dialog element
27
+ *
28
+ * @fires kiso--dialog:open - Forwarded from the target dialog controller
29
+ * @fires kiso--dialog:close - Forwarded from the target dialog controller
30
+ */
31
+ export default class extends Controller {
32
+ static values = {
33
+ dialogId: String,
34
+ }
35
+
36
+ /**
37
+ * Opens the target dialog.
38
+ */
39
+ open() {
40
+ this._withDialogController((controller) => controller.open())
41
+ }
42
+
43
+ /**
44
+ * Closes the target dialog.
45
+ */
46
+ close() {
47
+ this._withDialogController((controller) => controller.close())
48
+ }
49
+
50
+ /**
51
+ * Toggles the target dialog open or closed.
52
+ */
53
+ toggle() {
54
+ this._withDialogController((controller) => {
55
+ if (controller.element.open) {
56
+ controller.close()
57
+ } else {
58
+ controller.open()
59
+ }
60
+ })
61
+ }
62
+
63
+ /**
64
+ * Finds the dialog element and its Stimulus controller, then
65
+ * calls the provided callback with the controller instance.
66
+ *
67
+ * @param {function} callback - Receives the dialog controller
68
+ * @private
69
+ */
70
+ _withDialogController(callback) {
71
+ const dialog = document.getElementById(this.dialogIdValue)
72
+ if (!dialog) return
73
+
74
+ const controller = this.application.getControllerForElementAndIdentifier(dialog, "kiso--dialog")
75
+
76
+ if (controller) {
77
+ callback(controller)
78
+ }
79
+ }
80
+ }
@@ -5,8 +5,19 @@ declare const KisoUi: {
5
5
  }
6
6
 
7
7
  export default KisoUi
8
+ export const KisoAlertController: typeof Controller
8
9
  export const KisoComboboxController: typeof Controller
10
+ export const KisoCommandController: typeof Controller
11
+ export const KisoCommandDialogController: typeof Controller
12
+ export const KisoDialogController: typeof Controller
13
+ export const KisoDialogTriggerController: typeof Controller
9
14
  export const KisoDropdownMenuController: typeof Controller
15
+ export const KisoInputOtpController: typeof Controller
16
+ export const KisoPopoverController: typeof Controller
10
17
  export const KisoSelectController: typeof Controller
18
+ export const KisoSidebarController: typeof Controller
19
+ export const KisoSliderController: typeof Controller
20
+ export const KisoThemeController: typeof Controller
11
21
  export const KisoToggleController: typeof Controller
12
22
  export const KisoToggleGroupController: typeof Controller
23
+ export const KisoTooltipController: typeof Controller
@@ -3,14 +3,17 @@ import KisoComboboxController from "./combobox_controller.js"
3
3
  import KisoCommandController from "./command_controller.js"
4
4
  import KisoCommandDialogController from "./command_dialog_controller.js"
5
5
  import KisoDialogController from "./dialog_controller.js"
6
+ import KisoDialogTriggerController from "./dialog_trigger_controller.js"
6
7
  import KisoDropdownMenuController from "./dropdown_menu_controller.js"
7
8
  import KisoInputOtpController from "./input_otp_controller.js"
8
9
  import KisoPopoverController from "./popover_controller.js"
9
10
  import KisoSelectController from "./select_controller.js"
10
11
  import KisoSidebarController from "./sidebar_controller.js"
12
+ import KisoSliderController from "./slider_controller.js"
11
13
  import KisoThemeController from "./theme_controller.js"
12
14
  import KisoToggleController from "./toggle_controller.js"
13
15
  import KisoToggleGroupController from "./toggle_group_controller.js"
16
+ import KisoTooltipController from "./tooltip_controller.js"
14
17
 
15
18
  const KisoUi = {
16
19
  start(application) {
@@ -19,14 +22,17 @@ const KisoUi = {
19
22
  application.register("kiso--command", KisoCommandController)
20
23
  application.register("kiso--command-dialog", KisoCommandDialogController)
21
24
  application.register("kiso--dialog", KisoDialogController)
25
+ application.register("kiso--dialog-trigger", KisoDialogTriggerController)
22
26
  application.register("kiso--dropdown-menu", KisoDropdownMenuController)
23
27
  application.register("kiso--input-otp", KisoInputOtpController)
24
28
  application.register("kiso--popover", KisoPopoverController)
25
29
  application.register("kiso--select", KisoSelectController)
26
30
  application.register("kiso--sidebar", KisoSidebarController)
31
+ application.register("kiso--slider", KisoSliderController)
27
32
  application.register("kiso--theme", KisoThemeController)
28
33
  application.register("kiso--toggle", KisoToggleController)
29
34
  application.register("kiso--toggle-group", KisoToggleGroupController)
35
+ application.register("kiso--tooltip", KisoTooltipController)
30
36
  },
31
37
  }
32
38
 
@@ -37,12 +43,15 @@ export {
37
43
  KisoCommandController,
38
44
  KisoCommandDialogController,
39
45
  KisoDialogController,
46
+ KisoDialogTriggerController,
40
47
  KisoDropdownMenuController,
41
48
  KisoInputOtpController,
42
49
  KisoPopoverController,
43
50
  KisoSelectController,
44
51
  KisoSidebarController,
52
+ KisoSliderController,
45
53
  KisoThemeController,
46
54
  KisoToggleController,
47
55
  KisoToggleGroupController,
56
+ KisoTooltipController,
48
57
  }
@@ -0,0 +1,212 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { startPositioning } from "kiso-ui/utils/positioning"
3
+
4
+ /**
5
+ * Tooltip controller — shows a floating tooltip on hover or keyboard focus.
6
+ * Uses Floating UI for positioning and the native `[popover]` attribute for
7
+ * top-layer rendering (no z-index issues).
8
+ *
9
+ * @example
10
+ * <div data-controller="kiso--tooltip"
11
+ * data-kiso--tooltip-side-value="top"
12
+ * data-kiso--tooltip-align-value="center"
13
+ * data-kiso--tooltip-delay-value="0"
14
+ * data-slot="tooltip">
15
+ * <div data-kiso--tooltip-target="trigger"
16
+ * data-action="mouseenter->kiso--tooltip#show mouseleave->kiso--tooltip#hide
17
+ * focusin->kiso--tooltip#show focusout->kiso--tooltip#hide"
18
+ * data-slot="tooltip-trigger">
19
+ * <button type="button">Hover me</button>
20
+ * </div>
21
+ * <div data-kiso--tooltip-target="content"
22
+ * data-slot="tooltip-content"
23
+ * popover="manual"
24
+ * role="tooltip">
25
+ * Tooltip text
26
+ * <div data-kiso--tooltip-target="arrow" data-slot="tooltip-arrow"></div>
27
+ * </div>
28
+ * </div>
29
+ *
30
+ * @property {HTMLElement} triggerTarget - Element that activates the tooltip on hover/focus
31
+ * @property {HTMLElement} contentTarget - The floating tooltip panel (popover)
32
+ * @property {HTMLElement} arrowTarget - Arrow element positioned by Floating UI
33
+ *
34
+ * @fires kiso--tooltip:show - When the tooltip becomes visible
35
+ * @fires kiso--tooltip:hide - When the tooltip is hidden
36
+ */
37
+ export default class extends Controller {
38
+ static targets = ["trigger", "content", "arrow"]
39
+ static values = {
40
+ side: { type: String, default: "top" },
41
+ align: { type: String, default: "center" },
42
+ delay: { type: Number, default: 0 },
43
+ }
44
+
45
+ connect() {
46
+ this._open = false
47
+
48
+ // Link trigger to tooltip content via aria-describedby
49
+ this._triggerEl =
50
+ this.triggerTarget.querySelector("button, a, [tabindex]") || this.triggerTarget
51
+ this._tooltipId = this.contentTarget.id || `tooltip-${crypto.randomUUID().slice(0, 8)}`
52
+ this.contentTarget.id = this._tooltipId
53
+ this._triggerEl.setAttribute("aria-describedby", this._tooltipId)
54
+ }
55
+
56
+ disconnect() {
57
+ this._cleanupPosition?.()
58
+ this._clearTimers()
59
+ }
60
+
61
+ /**
62
+ * Shows the tooltip after the configured delay.
63
+ * Called on mouseenter and focusin on the trigger.
64
+ */
65
+ show() {
66
+ this._clearHideTimeout()
67
+
68
+ if (this._open) return
69
+
70
+ const delay = this.delayValue
71
+ if (delay > 0) {
72
+ this._showTimeout = setTimeout(() => this._doShow(), delay)
73
+ } else {
74
+ this._doShow()
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Hides the tooltip with a small delay to allow the mouse to cross
80
+ * the gap between trigger and tooltip content.
81
+ * Called on mouseleave and focusout on the trigger.
82
+ */
83
+ hide() {
84
+ this._clearShowTimeout()
85
+
86
+ if (!this._open) return
87
+
88
+ // Small delay lets the mouse cross the offset gap to the content
89
+ this._hideTimeout = setTimeout(() => this._doHide(), 100)
90
+ }
91
+
92
+ /**
93
+ * Cancels a pending hide when the mouse enters the tooltip content.
94
+ * Keeps the tooltip visible while the user reads it.
95
+ */
96
+ contentMouseEnter() {
97
+ this._clearHideTimeout()
98
+ }
99
+
100
+ /**
101
+ * Hides the tooltip when the mouse leaves the tooltip content.
102
+ */
103
+ contentMouseLeave() {
104
+ this._doHide()
105
+ }
106
+
107
+ // --- Private ---
108
+
109
+ /**
110
+ * Shows the tooltip immediately: opens the popover, positions it,
111
+ * and triggers the entry animation.
112
+ *
113
+ * @private
114
+ */
115
+ _doShow() {
116
+ if (this._open) return
117
+ this._open = true
118
+
119
+ this.contentTarget.showPopover()
120
+ this.contentTarget.setAttribute("data-state", "open")
121
+
122
+ const placement = this._buildPlacement()
123
+ const options = {
124
+ placement,
125
+ strategy: "fixed",
126
+ offset: 8,
127
+ }
128
+ if (this.hasArrowTarget) {
129
+ options.arrow = this.arrowTarget
130
+ }
131
+
132
+ this._cleanupPosition = startPositioning(this.triggerTarget, this.contentTarget, options)
133
+
134
+ this.dispatch("show")
135
+ }
136
+
137
+ /**
138
+ * Hides the tooltip: triggers exit animation, then hides the popover.
139
+ *
140
+ * @private
141
+ */
142
+ _doHide() {
143
+ if (!this._open) return
144
+ this._open = false
145
+
146
+ this._cleanupPosition?.()
147
+ this._cleanupPosition = null
148
+ this._clearTimers()
149
+
150
+ this.contentTarget.setAttribute("data-state", "closed")
151
+
152
+ // Hide after exit animation completes
153
+ this._animHandler = () => {
154
+ this.contentTarget.hidePopover()
155
+ this.contentTarget.removeEventListener("animationend", this._animHandler)
156
+ this._animHandler = null
157
+ }
158
+ this.contentTarget.addEventListener("animationend", this._animHandler)
159
+
160
+ // Fallback: hide immediately if no animation runs
161
+ this._closeTimeout = setTimeout(() => {
162
+ if (!this._open && this.contentTarget.matches(":popover-open")) {
163
+ this.contentTarget.hidePopover()
164
+ }
165
+ }, 150)
166
+
167
+ this.dispatch("hide")
168
+ }
169
+
170
+ /**
171
+ * Builds a Floating UI placement string from side and align values.
172
+ *
173
+ * @returns {string} e.g. "top", "top-start", "bottom-end"
174
+ * @private
175
+ */
176
+ _buildPlacement() {
177
+ const side = this.sideValue
178
+ const align = this.alignValue
179
+ if (align === "center") return side
180
+ return `${side}-${align}`
181
+ }
182
+
183
+ /** @private */
184
+ _clearShowTimeout() {
185
+ if (this._showTimeout) {
186
+ clearTimeout(this._showTimeout)
187
+ this._showTimeout = null
188
+ }
189
+ }
190
+
191
+ /** @private */
192
+ _clearHideTimeout() {
193
+ if (this._hideTimeout) {
194
+ clearTimeout(this._hideTimeout)
195
+ this._hideTimeout = null
196
+ }
197
+ }
198
+
199
+ /** @private */
200
+ _clearTimers() {
201
+ this._clearShowTimeout()
202
+ this._clearHideTimeout()
203
+ if (this._closeTimeout) {
204
+ clearTimeout(this._closeTimeout)
205
+ this._closeTimeout = null
206
+ }
207
+ if (this._animHandler) {
208
+ this.contentTarget.removeEventListener("animationend", this._animHandler)
209
+ this._animHandler = null
210
+ }
211
+ }
212
+ }
@@ -2,12 +2,20 @@
2
2
  * Shared positioning utilities for floating components.
3
3
  * Wraps Floating UI for smart positioning with flip, shift, and auto-update.
4
4
  *
5
- * Used by select, combobox, popover, and dropdown_menu controllers.
5
+ * Used by select, combobox, popover, dropdown_menu, and tooltip controllers.
6
6
  *
7
7
  * @module utils/positioning
8
8
  */
9
9
 
10
- import { autoUpdate, computePosition, flip, offset, shift, size } from "@floating-ui/dom"
10
+ import {
11
+ arrow as arrowMiddleware,
12
+ autoUpdate,
13
+ computePosition,
14
+ flip,
15
+ offset,
16
+ shift,
17
+ size,
18
+ } from "@floating-ui/dom"
11
19
 
12
20
  /**
13
21
  * Starts positioning a floating element relative to a reference element.
@@ -24,6 +32,7 @@ import { autoUpdate, computePosition, flip, offset, shift, size } from "@floatin
24
32
  * @param {number} [options.offset=4] - Pixel gap between reference and floating element
25
33
  * @param {"absolute"|"fixed"} [options.strategy="absolute"] - CSS positioning strategy
26
34
  * @param {boolean} [options.matchWidth=false] - Set floating element minWidth to reference width
35
+ * @param {HTMLElement|null} [options.arrow=null] - Arrow element to position via Floating UI arrow middleware
27
36
  * @returns {Function} Cleanup function — call on close or disconnect to remove listeners
28
37
  *
29
38
  * @example
@@ -49,6 +58,7 @@ export function startPositioning(reference, floating, options = {}) {
49
58
  offset: offsetDistance = 4,
50
59
  strategy = "absolute",
51
60
  matchWidth = false,
61
+ arrow: arrowElement = null,
52
62
  } = options
53
63
 
54
64
  const middleware = [offset(offsetDistance), flip(), shift({ padding: 8 })]
@@ -63,12 +73,19 @@ export function startPositioning(reference, floating, options = {}) {
63
73
  )
64
74
  }
65
75
 
76
+ if (arrowElement) {
77
+ middleware.push(arrowMiddleware({ element: arrowElement }))
78
+ }
79
+
80
+ /** @type {Record<string, string>} Maps placement side to the opposite side where the arrow attaches */
81
+ const ARROW_STATIC_SIDE = { top: "bottom", right: "left", bottom: "top", left: "right" }
82
+
66
83
  const update = () => {
67
84
  computePosition(reference, floating, {
68
85
  placement,
69
86
  strategy,
70
87
  middleware,
71
- }).then(({ x, y, placement: finalPlacement }) => {
88
+ }).then(({ x, y, placement: finalPlacement, middlewareData }) => {
72
89
  Object.assign(floating.style, {
73
90
  position: strategy,
74
91
  left: `${x}px`,
@@ -79,6 +96,20 @@ export function startPositioning(reference, floating, options = {}) {
79
96
  if (floating.dataset.side !== side) {
80
97
  floating.dataset.side = side
81
98
  }
99
+
100
+ // Position arrow element if provided
101
+ if (arrowElement && middlewareData.arrow) {
102
+ const { x: arrowX, y: arrowY } = middlewareData.arrow
103
+ const staticSide = ARROW_STATIC_SIDE[side]
104
+
105
+ Object.assign(arrowElement.style, {
106
+ left: arrowX != null ? `${arrowX}px` : "",
107
+ top: arrowY != null ? `${arrowY}px` : "",
108
+ right: "",
109
+ bottom: "",
110
+ [staticSide]: "-4px",
111
+ })
112
+ }
82
113
  })
83
114
  }
84
115
 
@@ -1,6 +1,7 @@
1
- <%# locals: (src: nil, alt: "", text: nil, size: :md, ui: {}, css_classes: "", **component_options) %>
1
+ <%# locals: (src: nil, alt: "", text: nil, size: :md, color: nil, ui: {}, css_classes: "", **component_options) %>
2
2
  <%= content_tag :span,
3
3
  class: Kiso::Themes::Avatar.render(size: size, class: css_classes),
4
+ style: (color ? "background-color: #{color};" : nil),
4
5
  data: kiso_prepare_options(component_options, slot: "avatar", size: size),
5
6
  **component_options do %>
6
7
  <% content = capture { yield }.presence %>
@@ -8,7 +9,9 @@
8
9
  <%= content %>
9
10
  <% else %>
10
11
  <% if text.present? %>
11
- <%= tag.span class: Kiso::Themes::AvatarFallback.render(size: size, class: ui[:fallback]),
12
+ <% fallback_classes = ui[:fallback].to_s %>
13
+ <% fallback_classes = "bg-transparent text-inherit #{fallback_classes}" if color %>
14
+ <%= tag.span class: Kiso::Themes::AvatarFallback.render(size: size, class: fallback_classes),
12
15
  data: { slot: "avatar-fallback" } do %>
13
16
  <%= text %>
14
17
  <% end %>
@@ -0,0 +1,7 @@
1
+ <%# locals: (label: nil, css_classes: "", **component_options) %>
2
+ <%= kiso_component_icon(:spinner,
3
+ role: "status",
4
+ aria: { label: label || t("kiso.spinner.loading") },
5
+ class: Kiso::Themes::Spinner.render(class: css_classes),
6
+ data: kiso_prepare_options(component_options, slot: "spinner"),
7
+ **component_options) %>
@@ -0,0 +1,25 @@
1
+ <%# locals: (text: nil, kbds: nil, side: :top, align: :center, delay: 0, ui: {}, css_classes: "", **component_options) %>
2
+ <%= tag.div(
3
+ class: css_classes.presence,
4
+ data: kiso_prepare_options(component_options, slot: "tooltip",
5
+ controller: "kiso--tooltip",
6
+ kiso__tooltip_side_value: side,
7
+ kiso__tooltip_align_value: align,
8
+ kiso__tooltip_delay_value: delay),
9
+ **component_options) do %>
10
+ <% if text %>
11
+ <%= kui(:tooltip, :trigger) { yield } %>
12
+ <%= kui(:tooltip, :content, css_classes: ui[:content].to_s) do %>
13
+ <span data-slot="tooltip-text"><%= text %></span>
14
+ <% if kbds.present? %>
15
+ <span data-slot="tooltip-kbds" class="hidden lg:inline-flex items-center shrink-0 gap-0.5">
16
+ <% Array(kbds).each do |kbd| %>
17
+ <%= kui(:kbd, size: :sm) { kbd } %>
18
+ <% end %>
19
+ </span>
20
+ <% end %>
21
+ <% end %>
22
+ <% else %>
23
+ <%= yield %>
24
+ <% end %>
25
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <%# locals: (css_classes: "", ui: {}, **component_options) %>
2
+ <%= tag.div(
3
+ class: Kiso::Themes::TooltipContent.render(class: css_classes),
4
+ role: "tooltip",
5
+ popover: "manual",
6
+ data: kiso_prepare_options(component_options, slot: "tooltip-content",
7
+ kiso__tooltip_target: "content",
8
+ action: "mouseenter->kiso--tooltip#contentMouseEnter mouseleave->kiso--tooltip#contentMouseLeave"),
9
+ **component_options) do %>
10
+ <%= yield %>
11
+ <%= tag.div(
12
+ class: Kiso::Themes::TooltipArrow.render(class: ui[:arrow].to_s),
13
+ data: { slot: "tooltip-arrow", kiso__tooltip_target: "arrow" }) %>
14
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <%# locals: (css_classes: "", **component_options) %>
2
+ <%= tag.div(
3
+ class: css_classes.presence,
4
+ data: kiso_prepare_options(component_options, slot: "tooltip-trigger",
5
+ kiso__tooltip_target: "trigger",
6
+ action: "mouseenter->kiso--tooltip#show mouseleave->kiso--tooltip#hide focusin->kiso--tooltip#show focusout->kiso--tooltip#hide"),
7
+ **component_options) do %>
8
+ <%= yield %>
9
+ <% end %>
@@ -24,6 +24,10 @@ en:
24
24
  collapse: "Collapse sidebar"
25
25
  dialog:
26
26
  close: "Close"
27
+ spinner:
28
+ loading: "Loading"
29
+ tooltip:
30
+ label: "Tooltip"
27
31
  pagination:
28
32
  label: "pagination"
29
33
  more_pages: "More pages"
@@ -87,7 +87,8 @@ module Kiso
87
87
  menu: "menu",
88
88
  minus: "minus",
89
89
  panel_left_close: "panel-left-close",
90
- panel_left_open: "panel-left-open"
90
+ panel_left_open: "panel-left-open",
91
+ spinner: "loader-circle"
91
92
  }
92
93
  end
93
94
  end
@@ -130,7 +130,10 @@ module Kiso
130
130
  nav_item: {base: "rounded-full"},
131
131
 
132
132
  # Nav item badge: rounded-md → rounded-full
133
- nav_item_badge: {base: "rounded-full"}
133
+ nav_item_badge: {base: "rounded-full"},
134
+
135
+ # Tooltip content: rounded-md → rounded-xl
136
+ tooltip_content: {base: "rounded-xl"}
134
137
  }.freeze
135
138
  end
136
139
  end
@@ -172,7 +172,10 @@ module Kiso
172
172
  nav_item: {base: "rounded-none"},
173
173
 
174
174
  # Nav item badge: rounded-md → rounded-none
175
- nav_item_badge: {base: "rounded-none"}
175
+ nav_item_badge: {base: "rounded-none"},
176
+
177
+ # Tooltip content: rounded-md → rounded-none
178
+ tooltip_content: {base: "rounded-none"}
176
179
  }.freeze
177
180
  end
178
181
  end
@@ -4,6 +4,7 @@ module Kiso
4
4
  base: "group/avatar relative flex shrink-0 rounded-full select-none items-center justify-center bg-muted",
5
5
  variants: {
6
6
  size: {
7
+ xs: "size-5",
7
8
  sm: "size-6",
8
9
  md: "size-8",
9
10
  lg: "size-10"
@@ -20,6 +21,7 @@ module Kiso
20
21
  base: "flex size-full items-center justify-center rounded-full bg-muted text-muted-foreground font-medium",
21
22
  variants: {
22
23
  size: {
24
+ xs: "text-[10px]",
23
25
  sm: "text-xs",
24
26
  md: "text-sm",
25
27
  lg: "text-base"
@@ -31,6 +33,7 @@ module Kiso
31
33
  AvatarBadge = ClassVariants.build(
32
34
  base: "bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 " \
33
35
  "inline-flex items-center justify-center rounded-full ring-2 select-none " \
36
+ "group-data-[size=xs]/avatar:size-1.5 group-data-[size=xs]/avatar:[&>svg]:hidden " \
34
37
  "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden " \
35
38
  "group-data-[size=md]/avatar:size-2.5 group-data-[size=md]/avatar:[&>svg]:size-2 " \
36
39
  "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2"
@@ -44,9 +47,11 @@ module Kiso
44
47
  AvatarGroupCount = ClassVariants.build(
45
48
  base: "bg-muted text-muted-foreground ring-background relative flex size-8 shrink-0 " \
46
49
  "items-center justify-center rounded-full text-sm ring-2 " \
50
+ "group-has-data-[size=xs]/avatar-group:size-5 " \
47
51
  "group-has-data-[size=lg]/avatar-group:size-10 " \
48
52
  "group-has-data-[size=sm]/avatar-group:size-6 " \
49
53
  "[&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 " \
54
+ "group-has-data-[size=xs]/avatar-group:[&>svg]:size-2.5 " \
50
55
  "group-has-data-[size=sm]/avatar-group:[&>svg]:size-3"
51
56
  )
52
57
  end
@@ -14,7 +14,7 @@ module Kiso
14
14
  # - +size+ — :xs, :sm, :md (default), :lg, :xl
15
15
  # - +block+ — +true+ for full-width, +false+ (default)
16
16
  Button = ClassVariants.build(
17
- base: "inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap shrink-0 " \
17
+ base: "items-center justify-center gap-2 font-medium whitespace-nowrap shrink-0 " \
18
18
  "transition-all " \
19
19
  "focus-visible:outline-2 focus-visible:outline-offset-2 " \
20
20
  "disabled:pointer-events-none disabled:opacity-50 " \
@@ -0,0 +1,16 @@
1
+ module Kiso
2
+ module Themes
3
+ # Spinning loading indicator.
4
+ #
5
+ # @example
6
+ # Spinner.render
7
+ #
8
+ # No variants — size is controlled via +css_classes:+. Inherits
9
+ # +currentColor+ from the parent context.
10
+ #
11
+ # shadcn base: size-4 animate-spin
12
+ Spinner = ClassVariants.build(
13
+ base: "animate-spin size-4"
14
+ )
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ module Kiso
2
+ module Themes
3
+ # Floating tooltip content panel with inverted colors.
4
+ #
5
+ # @example
6
+ # TooltipContent.render
7
+ #
8
+ # No variants — tooltip always uses inverted colors. Inherits position
9
+ # from Floating UI via the Stimulus controller.
10
+ #
11
+ # shadcn base: bg-foreground text-background px-3 py-1.5 text-xs rounded-md
12
+ # Kiso: bg-inverted text-inverted-foreground (semantic equivalents)
13
+ TooltipContent = ClassVariants.build(
14
+ base: "bg-inverted text-inverted-foreground px-3 py-1.5 text-xs rounded-md " \
15
+ "flex items-center gap-1.5 select-none w-max max-w-xs"
16
+ )
17
+
18
+ # Arrow element pointing from tooltip content to the trigger.
19
+ #
20
+ # Positioned by Floating UI arrow middleware. CSS rotates 45° to form
21
+ # a diamond, with half hidden behind the tooltip edge.
22
+ TooltipArrow = ClassVariants.build(
23
+ base: "absolute size-2 rotate-45 bg-inverted"
24
+ )
25
+ end
26
+ end
data/lib/kiso/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Kiso
4
4
  # @return [String] the current gem version
5
- VERSION = "0.4.3.pre"
5
+ VERSION = "0.5.0.pre"
6
6
  end
data/lib/kiso.rb CHANGED
@@ -49,6 +49,8 @@ require "kiso/themes/nav"
49
49
  require "kiso/themes/page"
50
50
  require "kiso/themes/avatar"
51
51
  require "kiso/themes/skeleton"
52
+ require "kiso/themes/spinner"
53
+ require "kiso/themes/tooltip"
52
54
  require "kiso/themes/slider"
53
55
  require "kiso/themes/layout"
54
56
  require "kiso/icons"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kiso
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3.pre
4
+ version: 0.5.0.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Clarke
@@ -73,13 +73,13 @@ executables: []
73
73
  extensions: []
74
74
  extra_rdoc_files: []
75
75
  files:
76
- - CHANGELOG.md
77
76
  - MIT-LICENSE
78
77
  - README.md
79
78
  - Rakefile
80
79
  - app/assets/fonts/kiso/GeistMonoVF.woff2
81
80
  - app/assets/fonts/kiso/GeistVF.woff2
82
81
  - app/assets/fonts/kiso/OFL.txt
82
+ - app/assets/tailwind/kiso/button.css
83
83
  - app/assets/tailwind/kiso/checkbox.css
84
84
  - app/assets/tailwind/kiso/color-mode.css
85
85
  - app/assets/tailwind/kiso/dashboard.css
@@ -93,6 +93,7 @@ files:
93
93
  - app/assets/tailwind/kiso/palettes/zinc.css
94
94
  - app/assets/tailwind/kiso/radio-group.css
95
95
  - app/assets/tailwind/kiso/slider.css
96
+ - app/assets/tailwind/kiso/tooltip.css
96
97
  - app/helpers/kiso/app_component_helper.rb
97
98
  - app/helpers/kiso/component_helper.rb
98
99
  - app/helpers/kiso/icon_helper.rb
@@ -103,6 +104,7 @@ files:
103
104
  - app/javascript/controllers/kiso/command_controller.js
104
105
  - app/javascript/controllers/kiso/command_dialog_controller.js
105
106
  - app/javascript/controllers/kiso/dialog_controller.js
107
+ - app/javascript/controllers/kiso/dialog_trigger_controller.js
106
108
  - app/javascript/controllers/kiso/dropdown_menu_controller.js
107
109
  - app/javascript/controllers/kiso/index.d.ts
108
110
  - app/javascript/controllers/kiso/index.js
@@ -114,6 +116,7 @@ files:
114
116
  - app/javascript/controllers/kiso/theme_controller.js
115
117
  - app/javascript/controllers/kiso/toggle_controller.js
116
118
  - app/javascript/controllers/kiso/toggle_group_controller.js
119
+ - app/javascript/controllers/kiso/tooltip_controller.js
117
120
  - app/javascript/kiso/utils/focusable.js
118
121
  - app/javascript/kiso/utils/highlight.js
119
122
  - app/javascript/kiso/utils/positioning.js
@@ -168,6 +171,7 @@ files:
168
171
  - app/views/kiso/components/_separator.html.erb
169
172
  - app/views/kiso/components/_skeleton.html.erb
170
173
  - app/views/kiso/components/_slider.html.erb
174
+ - app/views/kiso/components/_spinner.html.erb
171
175
  - app/views/kiso/components/_stats_card.html.erb
172
176
  - app/views/kiso/components/_stats_grid.html.erb
173
177
  - app/views/kiso/components/_switch.html.erb
@@ -175,6 +179,7 @@ files:
175
179
  - app/views/kiso/components/_textarea.html.erb
176
180
  - app/views/kiso/components/_toggle.html.erb
177
181
  - app/views/kiso/components/_toggle_group.html.erb
182
+ - app/views/kiso/components/_tooltip.html.erb
178
183
  - app/views/kiso/components/alert/_actions.html.erb
179
184
  - app/views/kiso/components/alert/_description.html.erb
180
185
  - app/views/kiso/components/alert/_title.html.erb
@@ -320,6 +325,8 @@ files:
320
325
  - app/views/kiso/components/table/_header.html.erb
321
326
  - app/views/kiso/components/table/_row.html.erb
322
327
  - app/views/kiso/components/toggle_group/_item.html.erb
328
+ - app/views/kiso/components/tooltip/_content.html.erb
329
+ - app/views/kiso/components/tooltip/_trigger.html.erb
323
330
  - config/deploy.docs.yml
324
331
  - config/deploy.yml
325
332
  - config/importmap.rb
@@ -380,12 +387,14 @@ files:
380
387
  - lib/kiso/themes/shared.rb
381
388
  - lib/kiso/themes/skeleton.rb
382
389
  - lib/kiso/themes/slider.rb
390
+ - lib/kiso/themes/spinner.rb
383
391
  - lib/kiso/themes/stats_card.rb
384
392
  - lib/kiso/themes/switch.rb
385
393
  - lib/kiso/themes/table.rb
386
394
  - lib/kiso/themes/textarea.rb
387
395
  - lib/kiso/themes/toggle.rb
388
396
  - lib/kiso/themes/toggle_group.rb
397
+ - lib/kiso/themes/tooltip.rb
389
398
  - lib/kiso/version.rb
390
399
  - lib/tasks/kiso.rake
391
400
  homepage: https://github.com/steveclarke/kiso
@@ -394,7 +403,7 @@ licenses:
394
403
  metadata:
395
404
  homepage_uri: https://github.com/steveclarke/kiso
396
405
  source_code_uri: https://github.com/steveclarke/kiso
397
- changelog_uri: https://github.com/steveclarke/kiso/blob/master/CHANGELOG.md
406
+ changelog_uri: https://github.com/steveclarke/kiso/releases
398
407
  bug_tracker_uri: https://github.com/steveclarke/kiso/issues
399
408
  rubygems_mfa_required: 'true'
400
409
  allowed_push_host: https://rubygems.org
data/CHANGELOG.md DELETED
@@ -1,138 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ## [0.4.3.pre] - 2026-03-08
11
-
12
- ### Fixed
13
-
14
- - `scope:` values now merge into the parent partial's kwargs — previously, scope was pushed onto the stack for sub-parts but not merged into the parent's own strict locals, causing `ActionView::StrictLocalsError`
15
- - `kui_tag` / `appui_tag` now render correctly without a block — self-closing elements (e.g., status dots, icons) were rendering the options hash as text content instead of HTML attributes
16
-
17
- ## [0.4.2.pre] - 2026-03-08
18
-
19
- ### Added
20
-
21
- - `scope:` prop for sharing domain locals from parent components to sub-parts — `appui(:room_card, scope: { room: room })` makes `room:` available to all sub-parts automatically without repeating it on every call. Explicit kwargs on sub-part calls override scope values. One level deep only (parent to its own sub-parts).
22
-
23
- ## [0.4.1.pre] - 2026-03-08
24
-
25
- ### Added
26
-
27
- - `center:` variant on App layout component — `kui(:app, center: true)` applies full-viewport centering for login pages and similar single-focus layouts
28
- - `kui_tag` / `appui_tag` helpers — collapse the common `content_tag` + `kiso_prepare_options` + theme rendering boilerplate into a single call; component generator templates now use `appui_tag` by default
29
- - Shade scale auto-wiring — host apps defining `--color-primary-50` through `--color-primary-950` in their Tailwind `@theme` block now automatically feed into Kiso's semantic color tokens (shade 500 for light mode, 400 for dark mode, matching Nuxt UI conventions)
30
-
31
- ### Fixed
32
-
33
- - Palette CSS files are now importable in host apps — a `kiso:palettes` rake task generates CSS stubs so `@import "../builds/tailwind/kiso/palettes/blue.css"` resolves correctly
34
-
35
- ## [0.4.0.pre] - 2026-03-08
36
-
37
- ### Added
38
-
39
- - Layout component family: App, Container (4 size variants), Header, Footer, Main
40
- - Page component family: Page (grid with sidebars), PageHeader, PageBody, PageSection, PageGrid, PageCard (4 variants)
41
- - `appui()` helper for host app components with `app/themes/` and `app/views/components/`
42
- - `kiso:framework_component` generator for scaffolding engine components
43
- - `kiso:component` generator for scaffolding host app components
44
- - Theme presets: `apply_preset(:rounded)` and `apply_preset(:sharp)`
45
- - 5 OKLCH color palettes (zinc, blue, green, orange, violet)
46
- - i18n support: all component strings use `t()` with `config/locales/en.yml`
47
- - [Building Your Own Components](/guide/building-components) guide — how to wrap Kiso components with domain logic and build standalone components with `appui()`, themes, and sub-parts
48
- - [Detailed release notes](/releases/batch-merge) with upgrade guide and examples for all new features
49
-
50
- ## [0.3.0.pre] - 2026-03-03
51
-
52
- ### Added
53
-
54
- - 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.
55
- - 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`.
56
- - AspectRatio component — lightweight wrapper that applies an aspect ratio via inline style. Accepts any `ratio:` value (defaults to 16:9).
57
- - 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.
58
- - Empty component `:actions` slot for placing buttons below the description.
59
- - Button `method:` prop — renders a Rails `button_to` form for DELETE/POST/PUT/PATCH actions while preserving all Button styling.
60
- - Icons guide added to documentation site.
61
-
62
- ### Fixed
63
-
64
- - InputOTP slots missing visible border when a separator is placed inside a group.
65
- - Sidebar header and footer now use `flex-col` layout matching shadcn structure.
66
-
67
- ## [0.2.2.pre] - 2026-03-03
68
-
69
- ### Fixed
70
-
71
- - Dashboard layout rendering — components called without a block inside a layout (e.g., sidebar toggle, collapse) would capture the entire page template via ERB yield bubbling, breaking the dashboard grid. The `kui()` helper now passes an empty proc to prevent yield from reaching the layout.
72
- - Dashboard toggle and collapse icon sizing — SVG icons now render at the correct size via `[&>svg]:size-4`.
73
-
74
- ## [0.2.1.pre] - 2026-03-03
75
-
76
- ### Fixed
77
-
78
- - Propshaft `stylesheet_link_tag :app` compatibility — the Rails 8.1 default `:app` symbol caused Propshaft to serve `tailwindcss-rails` engine CSS stubs directly to the browser, resulting in 404 errors for absolute filesystem paths. Kiso now filters these build-time intermediates from Propshaft's stylesheet resolution automatically. Host apps using either `:app` or explicit `"tailwind"` work correctly.
79
-
80
- ## [0.2.0.pre] - 2026-03-03
81
-
82
- ### Added
83
-
84
- - InputOTP component — one-time password input with individual character slots, auto-advance, paste support, and mobile SMS autofill via `autocomplete="one-time-code"`. Stimulus controller distributes a single transparent input to visual slot divs. Sub-parts: group, slot, separator. Dispatches `change` and `complete` events for auto-submit workflows.
85
- - SelectNative component — styled native `<select>` with chevron icon overlay. Variant × size axes matching Input (outline/soft/ghost, sm/md/lg). No JavaScript required.
86
- - Sidebar state variants — `kui-sidebar-open:` and `kui-sidebar-closed:` custom Tailwind variants for showing/hiding any element based on sidebar open/closed state. Composable with breakpoints (e.g., `kui-sidebar-open:lg:hidden`).
87
- - Custom toggle icon override — sidebar toggle and collapse buttons accept a block to replace the default icon.
88
- - Auto body base styles — engine CSS now applies `bg-background text-foreground antialiased` on `<body>` via `@layer base`, so host apps no longer need to add these classes manually.
89
-
90
- ## [0.1.1.pre] - 2026-03-03
91
-
92
- ### Added
93
-
94
- - Dashboard layout system — sidebar, navbar, panel, toolbar, and nav components with cookie-persisted sidebar state
95
- - Avatar component with image, fallback, badge, and group support
96
- - Form components — Field, Label, Input, Textarea, InputGroup, Checkbox, RadioGroup, Switch, Select, Combobox
97
- - Overlay components — Popover, DropdownMenu, Command palette
98
- - Navigation components — Breadcrumb, Pagination
99
- - Element components — Kbd, Toggle, ToggleGroup
100
- - Dark mode system — `kiso_theme_script` helper, ColorModeButton, ColorModeSelect
101
- - Floating UI positioning for popovers and dropdowns
102
- - Global theme overrides via `Kiso.configure`
103
- - Configurable default icons via `kiso_component_icon`
104
- - Getting Started guide
105
-
106
- ### Changed
107
-
108
- - Renamed `kiso()` helper to `kui()` to avoid Rails route proxy collision
109
- - Renamed `empty_state` to `empty` to match shadcn naming
110
- - Adopted `data-slot` convention from shadcn v4
111
-
112
- ## [0.1.0.pre] - 2026-02-25
113
-
114
- ### Added
115
-
116
- - Core engine with `kui()` component helper and `kiso_prepare_options` builder
117
- - `class_variants` + `tailwind_merge` integration for variant definitions
118
- - Theme CSS with 7 palettes, surface tokens, and dark mode
119
- - Badge component (color × variant × size, pill shape, SVG handling)
120
- - Alert component (color × variant, CSS Grid layout, title/description sub-parts)
121
- - Button component (6 variants, smart tag, 5 sizes, icon support)
122
- - Card component (3 variants, 6 sub-parts, shadcn gap-6/py-6 spacing)
123
- - Separator component (horizontal/vertical, decorative prop)
124
- - Empty State component (5 sub-parts, media variant)
125
- - Lookbook component previews
126
- - Bridgetown documentation site
127
-
128
- [Unreleased]: https://github.com/steveclarke/kiso/compare/v0.4.3.pre...HEAD
129
- [0.4.3.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.4.3.pre
130
- [0.4.2.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.4.2.pre
131
- [0.4.1.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.4.1.pre
132
- [0.4.0.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.4.0.pre
133
- [0.3.0.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.3.0.pre
134
- [0.2.2.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.2.2.pre
135
- [0.2.1.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.2.1.pre
136
- [0.2.0.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.2.0.pre
137
- [0.1.1.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.1.1.pre
138
- [0.1.0.pre]: https://github.com/steveclarke/kiso/releases/tag/v0.1.0.pre