phlex_kit 0.5.0 → 0.6.1
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/README.md +26 -10
- data/app/assets/stylesheets/phlex_kit/_tokens.css +1 -0
- data/app/components/phlex_kit/accordion/accordion.css +2 -0
- data/app/components/phlex_kit/accordion/accordion.rb +1 -1
- data/app/components/phlex_kit/accordion/accordion_content.rb +3 -1
- data/app/components/phlex_kit/accordion/accordion_controller.js +26 -2
- data/app/components/phlex_kit/accordion/accordion_default_trigger.rb +20 -4
- data/app/components/phlex_kit/accordion/accordion_trigger.rb +18 -2
- data/app/components/phlex_kit/alert/alert.rb +1 -1
- data/app/components/phlex_kit/alert_dialog/alert_dialog_cancel.rb +1 -1
- data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +1 -1
- data/app/components/phlex_kit/attachment/attachment.rb +2 -2
- data/app/components/phlex_kit/attachment/attachment_media.rb +1 -1
- data/app/components/phlex_kit/avatar/avatar.rb +1 -1
- data/app/components/phlex_kit/badge/badge.rb +1 -1
- data/app/components/phlex_kit/button/button.rb +1 -1
- data/app/components/phlex_kit/button_group/button_group.css +38 -0
- data/app/components/phlex_kit/button_group/button_group.rb +1 -1
- data/app/components/phlex_kit/calendar/calendar_controller.js +12 -8
- data/app/components/phlex_kit/card/card.rb +1 -1
- data/app/components/phlex_kit/carousel/carousel.rb +1 -1
- data/app/components/phlex_kit/clipboard/clipboard_popover.rb +1 -1
- data/app/components/phlex_kit/combobox/combobox.css +21 -12
- data/app/components/phlex_kit/combobox/combobox.rb +19 -3
- data/app/components/phlex_kit/combobox/combobox_badge_trigger.rb +7 -2
- data/app/components/phlex_kit/combobox/combobox_checkbox.rb +3 -0
- data/app/components/phlex_kit/combobox/combobox_controller.js +85 -20
- data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +11 -2
- data/app/components/phlex_kit/combobox/combobox_item.rb +4 -1
- data/app/components/phlex_kit/combobox/combobox_list.rb +9 -2
- data/app/components/phlex_kit/combobox/combobox_popover.rb +8 -7
- data/app/components/phlex_kit/combobox/combobox_radio.rb +3 -0
- data/app/components/phlex_kit/combobox/combobox_search_input.rb +8 -3
- data/app/components/phlex_kit/command/command.rb +17 -2
- data/app/components/phlex_kit/command/command_controller.js +93 -3
- data/app/components/phlex_kit/command/command_dialog_content.rb +16 -4
- data/app/components/phlex_kit/command/command_input.rb +7 -2
- data/app/components/phlex_kit/command/command_item.rb +12 -3
- data/app/components/phlex_kit/command/command_list.rb +9 -2
- data/app/components/phlex_kit/context_menu/context_menu.css +25 -13
- data/app/components/phlex_kit/context_menu/context_menu_content.rb +1 -1
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +71 -12
- data/app/components/phlex_kit/context_menu/context_menu_item.rb +7 -3
- data/app/components/phlex_kit/context_menu/context_menu_label.rb +1 -1
- data/app/components/phlex_kit/context_menu/context_menu_trigger.rb +3 -1
- data/app/components/phlex_kit/data_table/data_table_column_visibility_controller.js +1 -1
- data/app/components/phlex_kit/data_table/data_table_controller.js +12 -1
- data/app/components/phlex_kit/data_table/data_table_pagination.rb +12 -10
- data/app/components/phlex_kit/data_table/data_table_selection_summary.rb +1 -0
- data/app/components/phlex_kit/data_table/data_table_sort_head.rb +5 -1
- data/app/components/phlex_kit/dialog/dialog_content.rb +12 -4
- data/app/components/phlex_kit/dialog/dialog_controller.js +13 -0
- data/app/components/phlex_kit/drawer/drawer_content.rb +2 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +37 -15
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_content.rb +4 -3
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +8 -11
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_item.rb +1 -1
- data/app/components/phlex_kit/empty/empty_media.rb +1 -1
- data/app/components/phlex_kit/field/field.css +5 -5
- data/app/components/phlex_kit/field/field.rb +1 -1
- data/app/components/phlex_kit/field/field_legend.rb +1 -1
- data/app/components/phlex_kit/hover_card/hover_card.css +27 -8
- data/app/components/phlex_kit/hover_card/hover_card.rb +2 -1
- data/app/components/phlex_kit/hover_card/hover_card_content.rb +2 -2
- data/app/components/phlex_kit/hover_card/hover_card_controller.js +19 -5
- data/app/components/phlex_kit/input_group/input_group_addon.rb +1 -1
- data/app/components/phlex_kit/input_otp/input_otp.rb +4 -1
- data/app/components/phlex_kit/input_otp/input_otp_controller.js +12 -1
- data/app/components/phlex_kit/item/item.rb +1 -1
- data/app/components/phlex_kit/item/item_media.rb +1 -1
- data/app/components/phlex_kit/link/link.rb +1 -1
- data/app/components/phlex_kit/marker/marker.rb +1 -1
- data/app/components/phlex_kit/menubar/menubar.css +36 -14
- data/app/components/phlex_kit/menubar/menubar.rb +1 -1
- data/app/components/phlex_kit/menubar/menubar_checkbox_item.rb +5 -2
- data/app/components/phlex_kit/menubar/menubar_content.rb +1 -1
- data/app/components/phlex_kit/menubar/menubar_controller.js +100 -12
- data/app/components/phlex_kit/menubar/menubar_item.rb +8 -5
- data/app/components/phlex_kit/menubar/menubar_radio_item.rb +5 -2
- data/app/components/phlex_kit/menubar/menubar_trigger.rb +1 -0
- data/app/components/phlex_kit/native_select/native_select.rb +1 -1
- data/app/components/phlex_kit/navigation_menu/navigation_menu.css +24 -7
- data/app/components/phlex_kit/navigation_menu/navigation_menu_content.rb +3 -1
- data/app/components/phlex_kit/popover/popover.css +24 -9
- data/app/components/phlex_kit/popover/popover.rb +2 -1
- data/app/components/phlex_kit/popover/popover_content.rb +6 -4
- data/app/components/phlex_kit/popover/popover_controller.js +12 -8
- data/app/components/phlex_kit/popover/popover_trigger.rb +1 -1
- data/app/components/phlex_kit/resizable/resizable_panel.rb +4 -2
- data/app/components/phlex_kit/select/select.css +27 -11
- data/app/components/phlex_kit/select/select_content.rb +6 -4
- data/app/components/phlex_kit/select/select_controller.js +21 -17
- data/app/components/phlex_kit/select/select_trigger.rb +1 -1
- data/app/components/phlex_kit/sheet/sheet_content.rb +2 -2
- data/app/components/phlex_kit/sheet/sheet_content_controller.js +68 -2
- data/app/components/phlex_kit/sheet/sheet_controller.js +28 -3
- data/app/components/phlex_kit/shortcut_key/shortcut_key.css +1 -1
- data/app/components/phlex_kit/sidebar/sidebar.rb +1 -1
- data/app/components/phlex_kit/sidebar/sidebar_wrapper.rb +1 -1
- data/app/components/phlex_kit/spinner/spinner.rb +1 -1
- data/app/components/phlex_kit/switch/switch.rb +7 -4
- data/app/components/phlex_kit/tabs/tabs.rb +1 -1
- data/app/components/phlex_kit/tabs/tabs_content.rb +4 -1
- data/app/components/phlex_kit/tabs/tabs_controller.js +60 -7
- data/app/components/phlex_kit/tabs/tabs_list.rb +2 -2
- data/app/components/phlex_kit/tabs/tabs_trigger.rb +14 -2
- data/app/components/phlex_kit/theme_toggle/theme_toggle.rb +11 -3
- data/app/components/phlex_kit/toast/toast_controller.js +17 -0
- data/app/components/phlex_kit/toast/toast_icon.rb +1 -1
- data/app/components/phlex_kit/toast/toast_item.rb +1 -1
- data/app/components/phlex_kit/toggle/toggle.rb +2 -2
- data/app/components/phlex_kit/toggle_group/toggle_group.rb +2 -2
- data/app/components/phlex_kit/toggle_group/toggle_group_item.rb +1 -1
- data/app/components/phlex_kit/tooltip/tooltip.css +40 -22
- data/app/components/phlex_kit/tooltip/tooltip.rb +3 -1
- data/app/components/phlex_kit/tooltip/tooltip_content.rb +1 -1
- data/app/components/phlex_kit/tooltip/tooltip_controller.js +32 -0
- data/app/components/phlex_kit/tooltip/tooltip_trigger.rb +8 -3
- data/app/components/phlex_kit/typography/text.rb +1 -1
- data/app/javascript/phlex_kit/controllers/index.js +2 -0
- data/lib/generators/phlex_kit/component/component_generator.rb +28 -0
- data/lib/generators/phlex_kit/install/templates/phlex_kit.rb +3 -0
- data/lib/phlex_kit/base_component.rb +12 -0
- data/lib/phlex_kit/engine.rb +11 -1
- data/lib/phlex_kit/icons/lucide.rb +3 -1
- data/lib/phlex_kit/icons/phosphor.rb +3 -1
- data/lib/phlex_kit/icons/remix.rb +3 -1
- data/lib/phlex_kit/icons/tabler.rb +3 -1
- data/lib/phlex_kit/version.rb +1 -1
- data/lib/phlex_kit.rb +5 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74f3d49a1a6adfab950d193db75d0cce12c02308c10fe69566d7a62ea0a835e3
|
|
4
|
+
data.tar.gz: 0b673ba98ab594e4f2783a32c83b792e849605cd43fb1033d2bd258dece4c789
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0e680e98ac74f15fa085a2828e7aa86041d67fa58ec82dd07d9f2912bc06c063ef58b5fcdcba5c44b037cc7b4070b01869b891da449ad8bcbb9ce4eb62facf4
|
|
7
|
+
data.tar.gz: ddea978e8b26eaf08dcb373df2c59bec15405f0c043ca349c8c6f6e1c20194f75712281d526f70bc0fc234fcb2aeb41a79f42386182494a6a597af3bb9868fe3
|
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
A [shadcn/ui](https://ui.shadcn.com)-grade component kit for
|
|
7
7
|
[Phlex](https://phlex.fun) + Rails, styled with **vanilla CSS + design tokens
|
|
8
8
|
instead of Tailwind**. The full [ruby_ui](https://ruby-ui.com) catalog plus
|
|
9
|
-
shadcn's own additions —
|
|
9
|
+
shadcn's own additions — 72 component families, 40 Stimulus controllers —
|
|
10
10
|
with the default theme lifted verbatim from shadcn/ui's current token system.
|
|
11
11
|
|
|
12
12
|
<picture>
|
|
@@ -21,7 +21,9 @@ switches, nav menu, command palette — at `/create` in [the docs site](#the-doc
|
|
|
21
21
|
|
|
22
22
|
- **No build step.** Components are plain Ruby classes with co-located vanilla
|
|
23
23
|
CSS, served precompiled-static through Propshaft. No Tailwind, no Node, no
|
|
24
|
-
PostCSS.
|
|
24
|
+
PostCSS. (Outside Rails there's no Zeitwerk to autoload them — after
|
|
25
|
+
`require "phlex_kit"`, require the component files you use, e.g.
|
|
26
|
+
`Dir[File.join(gem_dir, "app/components/phlex_kit/**/*.rb")].sort.each { require it }`.)
|
|
25
27
|
- **The shadcn look, themeable.** Every component reads `--pk-*` custom
|
|
26
28
|
properties. The default palette, radii, and control geometry match
|
|
27
29
|
ui.shadcn.com; redefine `:root` and the whole kit re-themes live — dark,
|
|
@@ -119,8 +121,8 @@ PhlexKit.configure { |c| c.define_ui_alias = true } # UI == PhlexKit
|
|
|
119
121
|
|
|
120
122
|
The gem ships shadcn/ui's default (neutral) token set — dark by default, light
|
|
121
123
|
via `<html data-theme="light">`, OS-following via `data-theme="system"`.
|
|
122
|
-
Override any token in your own stylesheet
|
|
123
|
-
gem's,
|
|
124
|
+
Override any token in your own stylesheet — the installer prepends the gem's
|
|
125
|
+
`@import`, so the gem's rules come first and yours, coming later, win:
|
|
124
126
|
|
|
125
127
|
```css
|
|
126
128
|
@import url("phlex_kit/phlex_kit.css");
|
|
@@ -135,10 +137,14 @@ The full token surface: `--pk-bg`, `--pk-surface`, `--pk-surface-2`,
|
|
|
135
137
|
`--pk-accent` (hover fills), `--pk-border`, `--pk-input` (control borders),
|
|
136
138
|
`--pk-ring` (focus rings), `--pk-text`, `--pk-text-2`, `--pk-muted`,
|
|
137
139
|
`--pk-brand`, `--pk-brand-ink`, `--pk-green`, `--pk-amber`, `--pk-red`,
|
|
138
|
-
`--pk-chart-1..5`, `--pk-radius`, plus `--pk-font-sans` / `--pk-font-
|
|
139
|
-
(system stacks by default — override them after an @font-face to adopt Geist
|
|
140
|
-
like shadcn, as the docs site does). Want a fully custom palette?
|
|
141
|
-
`_tokens.css` import
|
|
140
|
+
`--pk-chart-1..5`, `--pk-radius`, plus `--pk-font-sans` / `--pk-font-serif` /
|
|
141
|
+
`--pk-font-mono` (system stacks by default — override them after an @font-face to adopt Geist
|
|
142
|
+
like shadcn, as the docs site does). Want a fully custom palette? Override
|
|
143
|
+
every token in your stylesheet (the `_tokens.css` import lives inside the
|
|
144
|
+
gem's manifest, so you re-declare rather than remove). The complete reference —
|
|
145
|
+
defaults, shadcn equivalents, the optional `--pk-sidebar-*` overrides, and
|
|
146
|
+
every per-component knob (`--pk-card-spacing`, `--pk-cell-size`, …) — is in
|
|
147
|
+
[docs/07-TOKENS.md](docs/07-TOKENS.md).
|
|
142
148
|
|
|
143
149
|
Prefer a ready-made palette? The gem ships theme files under
|
|
144
150
|
`app/assets/stylesheets/phlex_kit/themes/` (`neutral`, `zinc`, `claude`) —
|
|
@@ -245,7 +251,16 @@ for another component reuses the same wiring — it's injected only once.
|
|
|
245
251
|
The dummy Rails app is a full shadcn-style docs site: a sidebar menu with one
|
|
246
252
|
page per component, every use case demoed with a Preview/Code toggle (the code
|
|
247
253
|
shown is extracted from the running example source, so it can never drift).
|
|
248
|
-
A one-page kitchen-sink gallery lives at /gallery
|
|
254
|
+
A one-page kitchen-sink gallery lives at /gallery, and /examples holds six
|
|
255
|
+
full-page admin screens (dashboard, orders, inbox, profile, reports, settings)
|
|
256
|
+
for a fictional commerce app — every visible element a kit component:
|
|
257
|
+
|
|
258
|
+
<picture>
|
|
259
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/examples-dashboard-dark.png">
|
|
260
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/images/examples-dashboard-light.png">
|
|
261
|
+
<img alt="The /examples admin dashboard — KPI cards, a revenue chart and a live activity feed inside the kit's sidebar shell" src="docs/images/examples-dashboard-light.png">
|
|
262
|
+
</picture>
|
|
263
|
+
|
|
249
264
|
|
|
250
265
|
```bash
|
|
251
266
|
git clone https://github.com/MatthewKennedy/phlex_kit && cd phlex_kit
|
|
@@ -281,7 +296,8 @@ alert dialog, aspect ratio, attachment, avatar (+ group), badge, breadcrumb,
|
|
|
281
296
|
bubble, button, button group, calendar, card, carousel, chart, checkbox,
|
|
282
297
|
clipboard, codeblock, collapsible, combobox (button/input/badge-chip
|
|
283
298
|
triggers), command palette, context menu, data table, date picker, dialog,
|
|
284
|
-
drawer, dropdown menu, empty,
|
|
299
|
+
drawer, dropdown menu, empty, field (set / group / label / description /
|
|
300
|
+
error), form + form field, hover card, input, input
|
|
285
301
|
group, input OTP, item, kbd, label, link, masked input, menubar, message,
|
|
286
302
|
message scroller, native select, navigation menu, pagination, popover,
|
|
287
303
|
marker, progress, radio button + radio group, resizable, scroll area, select,
|
|
@@ -134,6 +134,7 @@
|
|
|
134
134
|
--pk-chart-5: #1e40af;
|
|
135
135
|
--pk-radius: 0.625rem;
|
|
136
136
|
--pk-font-sans: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
137
|
+
--pk-font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
|
137
138
|
--pk-font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
138
139
|
color-scheme: dark;
|
|
139
140
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* Co-located with accordion.rb. Theme tokens from the global stylesheet. */
|
|
2
2
|
.pk-accordion { width: 100%; }
|
|
3
3
|
.pk-accordion-item { border-bottom: 1px solid var(--pk-border); }
|
|
4
|
+
/* APG: triggers live inside headings — the heading is a neutral wrapper. */
|
|
5
|
+
.pk-accordion-heading { margin: 0; display: flex; font-size: inherit; font-weight: inherit; line-height: inherit; }
|
|
4
6
|
.pk-accordion-trigger {
|
|
5
7
|
width: 100%; display: flex; flex: 1; align-items: center; justify-content: space-between;
|
|
6
8
|
padding-block: 1rem; font-size: .875rem; font-weight: 500; text-align: left;
|
|
@@ -13,7 +13,7 @@ module PhlexKit
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def view_template(&)
|
|
16
|
-
div(**mix({ class: "pk-accordion", data: { type: TYPES
|
|
16
|
+
div(**mix({ class: "pk-accordion", data: { type: fetch_option(TYPES, @type, :type) } }, @attrs), &)
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
end
|
|
@@ -2,7 +2,9 @@ module PhlexKit
|
|
|
2
2
|
class AccordionContent < BaseComponent
|
|
3
3
|
def initialize(**attrs) = (@attrs = attrs)
|
|
4
4
|
def view_template(&)
|
|
5
|
-
|
|
5
|
+
# role="region" + aria-labelledby → trigger id (wired by the controller
|
|
6
|
+
# in connect(), which also assigns the ids), per the APG accordion pattern.
|
|
7
|
+
div(**mix({ class: "pk-accordion-content", role: "region", data: { phlex_kit__accordion_target: "content", state: "closed" }, style: "height: 0px;", hidden: true }, @attrs), &)
|
|
6
8
|
end
|
|
7
9
|
end
|
|
8
10
|
end
|
|
@@ -3,8 +3,10 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
3
3
|
// Connects to data-controller="phlex-kit--accordion"
|
|
4
4
|
// Ported from ruby_ui's ruby-ui--accordion controller, reworked to use the native
|
|
5
5
|
// Web Animations API instead of the `motion` package (no extra JS dependency).
|
|
6
|
+
let uid = 0
|
|
7
|
+
|
|
6
8
|
export default class extends Controller {
|
|
7
|
-
static targets = ["icon", "content"]
|
|
9
|
+
static targets = ["icon", "content", "trigger"]
|
|
8
10
|
static values = {
|
|
9
11
|
open: { type: Boolean, default: false },
|
|
10
12
|
animationDuration: { type: Number, default: 150 },
|
|
@@ -12,6 +14,7 @@ export default class extends Controller {
|
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
connect() {
|
|
17
|
+
this.wireAria()
|
|
15
18
|
const d = this.animationDurationValue
|
|
16
19
|
this.animationDurationValue = 0
|
|
17
20
|
this.openValue ? this.open() : this.close()
|
|
@@ -35,7 +38,28 @@ export default class extends Controller {
|
|
|
35
38
|
if (ctrl && ctrl.openValue) ctrl.openValue = false
|
|
36
39
|
})
|
|
37
40
|
}
|
|
38
|
-
openValueChanged(isOpen) {
|
|
41
|
+
openValueChanged(isOpen) {
|
|
42
|
+
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", isOpen ? "true" : "false")
|
|
43
|
+
isOpen ? this.open() : this.close()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Pair the trigger with its content: assign per-instance ids (render time
|
|
47
|
+
// has no shared item value, so the parts can't derive them), wire
|
|
48
|
+
// aria-controls/aria-labelledby, and propagate a disabled item onto the
|
|
49
|
+
// actual button so it drops out of the tab order.
|
|
50
|
+
wireAria() {
|
|
51
|
+
if (!this.hasTriggerTarget) return
|
|
52
|
+
const id = ++uid
|
|
53
|
+
const trigger = this.triggerTarget
|
|
54
|
+
if (!trigger.id) trigger.id = `pk-accordion-trigger-${id}`
|
|
55
|
+
if (this.hasContentTarget) {
|
|
56
|
+
const content = this.contentTarget
|
|
57
|
+
if (!content.id) content.id = `pk-accordion-panel-${id}`
|
|
58
|
+
trigger.setAttribute("aria-controls", content.id)
|
|
59
|
+
content.setAttribute("aria-labelledby", trigger.id)
|
|
60
|
+
}
|
|
61
|
+
if ("disabled" in this.element.dataset) trigger.disabled = true
|
|
62
|
+
}
|
|
39
63
|
|
|
40
64
|
open() {
|
|
41
65
|
if (!this.hasContentTarget) return
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# Convenience trigger: label on the left, chevron icon on the right.
|
|
3
3
|
class AccordionDefaultTrigger < BaseComponent
|
|
4
|
-
def initialize(
|
|
4
|
+
def initialize(open: false, disabled: false, heading_level: 3, **attrs)
|
|
5
|
+
raise ArgumentError, "heading_level must be 1..6" unless (1..6).cover?(heading_level)
|
|
6
|
+
@open = open
|
|
7
|
+
@disabled = disabled
|
|
8
|
+
@heading_level = heading_level
|
|
9
|
+
@attrs = attrs
|
|
10
|
+
end
|
|
5
11
|
def view_template(&block)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
12
|
+
# APG accordion: each trigger is a button wrapped in a heading.
|
|
13
|
+
# aria-expanded reflects the render-time open state; the controller keeps
|
|
14
|
+
# it in sync and wires aria-controls to the content's id in connect().
|
|
15
|
+
base = {
|
|
16
|
+
type: "button", class: "pk-accordion-trigger",
|
|
17
|
+
aria_expanded: @open ? "true" : "false", disabled: @disabled,
|
|
18
|
+
data: { phlex_kit__accordion_target: "trigger", action: "click->phlex-kit--accordion#toggle" }
|
|
19
|
+
}
|
|
20
|
+
send(:"h#{@heading_level}", class: "pk-accordion-heading") do
|
|
21
|
+
button(**mix(base, @attrs)) do
|
|
22
|
+
span(class: "pk-accordion-trigger-label", &block)
|
|
23
|
+
render PhlexKit::AccordionIcon.new
|
|
24
|
+
end
|
|
9
25
|
end
|
|
10
26
|
end
|
|
11
27
|
end
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
class AccordionTrigger < BaseComponent
|
|
3
|
-
def initialize(
|
|
3
|
+
def initialize(open: false, disabled: false, heading_level: 3, **attrs)
|
|
4
|
+
raise ArgumentError, "heading_level must be 1..6" unless (1..6).cover?(heading_level)
|
|
5
|
+
@open = open
|
|
6
|
+
@disabled = disabled
|
|
7
|
+
@heading_level = heading_level
|
|
8
|
+
@attrs = attrs
|
|
9
|
+
end
|
|
4
10
|
def view_template(&)
|
|
5
|
-
|
|
11
|
+
# APG accordion: each trigger is a button wrapped in a heading.
|
|
12
|
+
# aria-expanded reflects the render-time open state; the controller keeps
|
|
13
|
+
# it in sync and wires aria-controls to the content's id in connect().
|
|
14
|
+
base = {
|
|
15
|
+
type: "button", class: "pk-accordion-trigger",
|
|
16
|
+
aria_expanded: @open ? "true" : "false", disabled: @disabled,
|
|
17
|
+
data: { phlex_kit__accordion_target: "trigger", action: "click->phlex-kit--accordion#toggle" }
|
|
18
|
+
}
|
|
19
|
+
send(:"h#{@heading_level}", class: "pk-accordion-heading") do
|
|
20
|
+
button(**mix(base, @attrs), &)
|
|
21
|
+
end
|
|
6
22
|
end
|
|
7
23
|
end
|
|
8
24
|
end
|
|
@@ -7,7 +7,7 @@ module PhlexKit
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def view_template(&block)
|
|
10
|
-
render PhlexKit::Button.new(variant: :outline, data: { action: "click->phlex-kit--alert-dialog#dismiss" },
|
|
10
|
+
render PhlexKit::Button.new(variant: :outline, **mix({ data: { action: "click->phlex-kit--alert-dialog#dismiss" } }, @attrs), &block)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
end
|
|
@@ -15,7 +15,7 @@ module PhlexKit
|
|
|
15
15
|
template(**mix({ data: { phlex_kit__alert_dialog_target: "content" } }, @attrs)) do
|
|
16
16
|
div(data: { controller: "phlex-kit--alert-dialog" }) do
|
|
17
17
|
div(class: "pk-alert-dialog-overlay", "aria-hidden": "true")
|
|
18
|
-
div(role: "alertdialog", "aria-modal": "true", class: [ "pk-alert-dialog-panel", SIZES
|
|
18
|
+
div(role: "alertdialog", "aria-modal": "true", class: [ "pk-alert-dialog-panel", fetch_option(SIZES, @size, :size) ].compact.join(" "), &block)
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -20,8 +20,8 @@ module PhlexKit
|
|
|
20
20
|
|
|
21
21
|
def view_template(&)
|
|
22
22
|
div(**mix({
|
|
23
|
-
class: [ "pk-attachment", SIZES
|
|
24
|
-
data: { state: STATES
|
|
23
|
+
class: [ "pk-attachment", fetch_option(SIZES, @size, :size), fetch_option(ORIENTATIONS, @orientation, :orientation) ].compact.join(" "),
|
|
24
|
+
data: { state: fetch_option(STATES, @state, :state) }
|
|
25
25
|
}, @attrs), &)
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -11,7 +11,7 @@ module PhlexKit
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def view_template(&)
|
|
14
|
-
div(**mix({ class: [ "pk-attachment-media", VARIANTS
|
|
14
|
+
div(**mix({ class: [ "pk-attachment-media", fetch_option(VARIANTS, @variant, :variant) ].compact.join(" ") }, @attrs), &)
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -50,7 +50,7 @@ module PhlexKit
|
|
|
50
50
|
private
|
|
51
51
|
|
|
52
52
|
def classes
|
|
53
|
-
[ "pk-badge", VARIANTS
|
|
53
|
+
[ "pk-badge", fetch_option(VARIANTS, @variant, :variant), fetch_option(SIZES, @size, :size) ].compact.join(" ")
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
end
|
|
@@ -52,7 +52,7 @@ module PhlexKit
|
|
|
52
52
|
private
|
|
53
53
|
|
|
54
54
|
def classes
|
|
55
|
-
[ "pk-button", VARIANTS
|
|
55
|
+
[ "pk-button", fetch_option(VARIANTS, @variant, :variant), fetch_option(SIZES, @size, :size), ("icon" if @icon) ].compact.join(" ")
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
end
|
|
@@ -36,6 +36,44 @@
|
|
|
36
36
|
/* Nested groups keep their own corners — undo the squaring the parent applied. */
|
|
37
37
|
.pk-button-group > .pk-button-group { border-radius: 0; border: 0; }
|
|
38
38
|
|
|
39
|
+
/* Composite children (dropdown menu, popover, hover card, tooltip, select,
|
|
40
|
+
combobox) wrap their trigger control in inert divs — shadcn's asChild
|
|
41
|
+
merges the trigger onto the button, so its direct-child rules just work;
|
|
42
|
+
ours land on the wrapper. Re-apply the corner/border rules to the control
|
|
43
|
+
inside: wrapper→trigger-div→.pk-button for the div-trigger components,
|
|
44
|
+
wrapper→trigger for select/combobox (their trigger IS the control). The
|
|
45
|
+
absolutely-positioned panels are untouched. */
|
|
46
|
+
.pk-button-group:not(.vertical) > :not(:first-child) > :is(.pk-dropdown-menu-trigger, .pk-popover-trigger, .pk-hover-card-trigger, .pk-tooltip-trigger) > .pk-button,
|
|
47
|
+
.pk-button-group:not(.vertical) > :not(:first-child) > :is(.pk-select-trigger, .pk-combobox-trigger) {
|
|
48
|
+
border-top-left-radius: 0;
|
|
49
|
+
border-bottom-left-radius: 0;
|
|
50
|
+
border-left: 0;
|
|
51
|
+
}
|
|
52
|
+
.pk-button-group:not(.vertical) > :not(:last-child) > :is(.pk-dropdown-menu-trigger, .pk-popover-trigger, .pk-hover-card-trigger, .pk-tooltip-trigger) > .pk-button,
|
|
53
|
+
.pk-button-group:not(.vertical) > :not(:last-child) > :is(.pk-select-trigger, .pk-combobox-trigger) {
|
|
54
|
+
border-top-right-radius: 0;
|
|
55
|
+
border-bottom-right-radius: 0;
|
|
56
|
+
}
|
|
57
|
+
.pk-button-group.vertical > :not(:first-child) > :is(.pk-dropdown-menu-trigger, .pk-popover-trigger, .pk-hover-card-trigger, .pk-tooltip-trigger) > .pk-button,
|
|
58
|
+
.pk-button-group.vertical > :not(:first-child) > :is(.pk-select-trigger, .pk-combobox-trigger) {
|
|
59
|
+
border-top-left-radius: 0;
|
|
60
|
+
border-top-right-radius: 0;
|
|
61
|
+
border-top: 0;
|
|
62
|
+
}
|
|
63
|
+
.pk-button-group.vertical > :not(:last-child) > :is(.pk-dropdown-menu-trigger, .pk-popover-trigger, .pk-hover-card-trigger, .pk-tooltip-trigger) > .pk-button,
|
|
64
|
+
.pk-button-group.vertical > :not(:last-child) > :is(.pk-select-trigger, .pk-combobox-trigger) {
|
|
65
|
+
border-bottom-left-radius: 0;
|
|
66
|
+
border-bottom-right-radius: 0;
|
|
67
|
+
}
|
|
68
|
+
/* .pk-select defaults to width:100%; as a group segment it hugs its trigger
|
|
69
|
+
and lets siblings (the input) take the leftover flex space — shadcn's
|
|
70
|
+
[&>[data-slot=select-trigger]]:w-fit. */
|
|
71
|
+
.pk-button-group > .pk-select { width: fit-content; }
|
|
72
|
+
|
|
73
|
+
/* A focused nested trigger lifts above its neighbor like a direct child. */
|
|
74
|
+
.pk-button-group :is(.pk-dropdown-menu-trigger, .pk-popover-trigger, .pk-hover-card-trigger, .pk-tooltip-trigger) > .pk-button:focus-visible,
|
|
75
|
+
.pk-button-group :is(.pk-select-trigger, .pk-combobox-trigger):focus-visible { position: relative; z-index: 10; }
|
|
76
|
+
|
|
39
77
|
.pk-button-group-text {
|
|
40
78
|
display: flex;
|
|
41
79
|
align-items: center;
|
|
@@ -13,7 +13,7 @@ module PhlexKit
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def view_template(&)
|
|
16
|
-
classes = [ "pk-button-group", ORIENTATIONS
|
|
16
|
+
classes = [ "pk-button-group", fetch_option(ORIENTATIONS, @orientation, :orientation) ].compact.join(" ")
|
|
17
17
|
div(**mix({ class: classes, role: "group" }, @attrs), &)
|
|
18
18
|
end
|
|
19
19
|
end
|
|
@@ -34,10 +34,10 @@ export default class extends Controller {
|
|
|
34
34
|
maxDate: { type: String, default: null },
|
|
35
35
|
disabledDates: { type: Array, default: [] },
|
|
36
36
|
weekNumbers: { type: Boolean, default: false },
|
|
37
|
-
viewDate
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
37
|
+
// Resolved lazily by viewDate() — a `new Date()` default here would be
|
|
38
|
+
// frozen at module load (stale "today" in long-lived tabs) and
|
|
39
|
+
// toISOString() is UTC (wrong day for non-UTC users near midnight).
|
|
40
|
+
viewDate: { type: String, default: "" },
|
|
41
41
|
format: { type: String, default: "yyyy-MM-dd" },
|
|
42
42
|
};
|
|
43
43
|
static outlets = ["phlex-kit--calendar-input"];
|
|
@@ -61,14 +61,14 @@ export default class extends Controller {
|
|
|
61
61
|
const date = this.viewDate();
|
|
62
62
|
date.setDate(2);
|
|
63
63
|
date.setMonth(Number(e.target.value));
|
|
64
|
-
this.viewDateValue =
|
|
64
|
+
this.viewDateValue = this.isoDate(date);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
setYear(e) {
|
|
68
68
|
const date = this.viewDate();
|
|
69
69
|
date.setDate(2);
|
|
70
70
|
date.setFullYear(Number(e.target.value));
|
|
71
|
-
this.viewDateValue =
|
|
71
|
+
this.viewDateValue = this.isoDate(date);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
selectDay(e) {
|
|
@@ -132,9 +132,13 @@ export default class extends Controller {
|
|
|
132
132
|
// update the viewDateValue to the selected date's month (this re-renders)
|
|
133
133
|
const newViewDate = new Date(selectedDate);
|
|
134
134
|
newViewDate.setDate(2); // avoid month-length/timezone edges
|
|
135
|
-
this.viewDateValue =
|
|
135
|
+
this.viewDateValue = this.isoDate(newViewDate);
|
|
136
136
|
|
|
137
137
|
this.updateCalendar();
|
|
138
|
+
|
|
139
|
+
// Stimulus fires valueChanged during value initialization too — a
|
|
140
|
+
// server-rendered selected_date must not dispatch a change event.
|
|
141
|
+
if (prevValue === undefined) return;
|
|
138
142
|
this.pushToOutlets();
|
|
139
143
|
this.dispatchChange();
|
|
140
144
|
}
|
|
@@ -180,7 +184,7 @@ export default class extends Controller {
|
|
|
180
184
|
const date = this.viewDate();
|
|
181
185
|
date.setDate(2); // avoid month-length/timezone edges
|
|
182
186
|
date.setMonth(date.getMonth() + adjustment);
|
|
183
|
-
return
|
|
187
|
+
return this.isoDate(date);
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
updateCalendar() {
|
|
@@ -27,7 +27,7 @@ module PhlexKit
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def view_template(&block)
|
|
30
|
-
div(**mix({ class: [ "pk-card", SIZES
|
|
30
|
+
div(**mix({ class: [ "pk-card", fetch_option(SIZES, @size, :size) ].compact.join(" ") }, @attrs), &block)
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
end
|
|
@@ -17,7 +17,7 @@ module PhlexKit
|
|
|
17
17
|
|
|
18
18
|
def view_template(&)
|
|
19
19
|
div(**mix({
|
|
20
|
-
class: [ "pk-carousel", ORIENTATIONS
|
|
20
|
+
class: [ "pk-carousel", fetch_option(ORIENTATIONS, @orientation, :orientation) ].join(" "),
|
|
21
21
|
role: "region",
|
|
22
22
|
aria: { roledescription: "carousel" },
|
|
23
23
|
data: {
|
|
@@ -6,7 +6,7 @@ module PhlexKit
|
|
|
6
6
|
@attrs = attrs
|
|
7
7
|
end
|
|
8
8
|
def view_template(&block)
|
|
9
|
-
div(class: "pk-clipboard-popover pk-hidden", data: { phlex_kit__clipboard_target: TARGETS
|
|
9
|
+
div(class: "pk-clipboard-popover pk-hidden", data: { phlex_kit__clipboard_target: fetch_option(TARGETS, @type, :target) }) do
|
|
10
10
|
div(**mix({ class: "pk-clipboard-popover-inner" }, @attrs), &block)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/* Co-located with combobox.rb — UI::Combobox. ruby_ui's structure + Stimulus
|
|
2
|
-
kept; the
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
kept; like upstream the panel is a native [popover] (manual), and CSS
|
|
3
|
+
anchor positioning replaces @floating-ui (Baseline 2026): the panel anchors
|
|
4
|
+
to the combobox wrapper (all three trigger layouts fill it), gets the
|
|
5
|
+
trigger width as a floor via anchor-size(), and flips above at the
|
|
6
|
+
viewport bottom via position-try-fallbacks. Tailwind → vanilla CSS on the
|
|
7
|
+
palette tokens. Geometry from ruby_ui (h-9 trigger, rounded-lg panel, p-1
|
|
8
|
+
list, px-2 py-1 label rows, group labels via ::before content(attr(label))).
|
|
6
9
|
Theme tokens come from the global stylesheet. */
|
|
7
|
-
.pk-combobox {
|
|
10
|
+
.pk-combobox { width: 100%; anchor-name: --pk-combobox; anchor-scope: --pk-combobox; }
|
|
8
11
|
|
|
9
12
|
/* Closed-state trigger (mirrors .pk-select-trigger geometry). */
|
|
10
13
|
.pk-combobox-trigger {
|
|
@@ -40,14 +43,17 @@
|
|
|
40
43
|
.pk-combobox-trigger-content { overflow: hidden; text-overflow: ellipsis; }
|
|
41
44
|
.pk-combobox-trigger-icon { flex: none; width: 1rem; height: 1rem; margin-left: .5rem; opacity: .5; }
|
|
42
45
|
|
|
43
|
-
/* Dropdown panel —
|
|
44
|
-
|
|
46
|
+
/* Dropdown panel — anchored below the combobox, flips above at the viewport
|
|
47
|
+
bottom. Width: trigger width with a 15rem floor (was JS
|
|
48
|
+
updatePopoverWidth, now pure CSS). */
|
|
45
49
|
.pk-combobox-popover {
|
|
46
|
-
position:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
margin
|
|
50
|
+
position: fixed;
|
|
51
|
+
position-anchor: --pk-combobox;
|
|
52
|
+
inset: auto;
|
|
53
|
+
position-area: block-end span-inline-end;
|
|
54
|
+
margin: .25rem 0 0;
|
|
55
|
+
position-try-fallbacks: flip-block;
|
|
56
|
+
width: max(anchor-size(width), 15rem);
|
|
51
57
|
border: 1px solid var(--pk-border);
|
|
52
58
|
border-radius: var(--pk-radius);
|
|
53
59
|
background: var(--pk-surface);
|
|
@@ -342,3 +348,6 @@
|
|
|
342
348
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
|
|
343
349
|
}
|
|
344
350
|
}
|
|
351
|
+
/* Hiding must survive author display values (a host class like
|
|
352
|
+
display:grid would beat the UA [popover] { display: none }). */
|
|
353
|
+
.pk-combobox-popover:not(:popover-open) { display: none; }
|
|
@@ -19,9 +19,10 @@ module PhlexKit
|
|
|
19
19
|
@attrs = attrs
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def view_template
|
|
22
|
+
def view_template
|
|
23
|
+
# role="combobox" lives on the interactive element (the trigger button /
|
|
24
|
+
# filter input), not this wrapper — see ComboboxTrigger and friends.
|
|
23
25
|
div(**mix({
|
|
24
|
-
role: "combobox",
|
|
25
26
|
class: "pk-combobox",
|
|
26
27
|
data: {
|
|
27
28
|
controller: "phlex-kit--combobox",
|
|
@@ -29,7 +30,22 @@ module PhlexKit
|
|
|
29
30
|
phlex_kit__combobox_auto_highlight_value: (@auto_highlight ? "true" : nil),
|
|
30
31
|
action: "turbo:morph@window->phlex-kit--combobox#updateTriggerContent click@window->phlex-kit--combobox#onClickOutside"
|
|
31
32
|
}
|
|
32
|
-
}, @attrs)
|
|
33
|
+
}, @attrs)) do
|
|
34
|
+
live_region
|
|
35
|
+
yield
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Screen-reader announcement of the filtered result count — the controller
|
|
42
|
+
# writes "N results" / "No results" into it from applyFilter.
|
|
43
|
+
def live_region
|
|
44
|
+
div(
|
|
45
|
+
class: "pk-sr-only",
|
|
46
|
+
aria: { live: "polite" },
|
|
47
|
+
data: { phlex_kit__combobox_target: "liveRegion" }
|
|
48
|
+
)
|
|
33
49
|
end
|
|
34
50
|
end
|
|
35
51
|
end
|
|
@@ -8,16 +8,19 @@ module PhlexKit
|
|
|
8
8
|
# PhlexKit's completion of it. Keyboard actions live here because focus stays
|
|
9
9
|
# in this field (see ComboboxInputTrigger). See combobox.rb.
|
|
10
10
|
class ComboboxBadgeTrigger < BaseComponent
|
|
11
|
-
def initialize(placeholder: "", clear_button: false, **attrs)
|
|
11
|
+
def initialize(placeholder: "", clear_button: false, list_id: nil, **attrs)
|
|
12
12
|
@placeholder = placeholder
|
|
13
13
|
@clear_button = clear_button
|
|
14
|
+
@list_id = list_id
|
|
14
15
|
@attrs = attrs
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def view_template
|
|
19
|
+
# The ARIA combobox role sits on the field itself (the interactive
|
|
20
|
+
# element), not this wrapper. Pass `list_id:` matching the ComboboxList
|
|
21
|
+
# id to wire aria-controls statically — otherwise the controller wires it.
|
|
18
22
|
div(**mix({
|
|
19
23
|
class: "pk-combobox-badge-trigger",
|
|
20
|
-
aria: { haspopup: "listbox", expanded: "false" },
|
|
21
24
|
data: {
|
|
22
25
|
placeholder: @placeholder,
|
|
23
26
|
phlex_kit__combobox_target: "trigger",
|
|
@@ -34,6 +37,8 @@ module PhlexKit
|
|
|
34
37
|
div(class: "pk-combobox-badge-container pk-hidden", data: { phlex_kit__combobox_target: "badgeContainer" })
|
|
35
38
|
input(
|
|
36
39
|
type: :text,
|
|
40
|
+
role: "combobox",
|
|
41
|
+
aria: { haspopup: "listbox", expanded: "false", autocomplete: "list", controls: @list_id },
|
|
37
42
|
placeholder: @placeholder,
|
|
38
43
|
autocomplete: "off",
|
|
39
44
|
autocorrect: "off",
|
|
@@ -10,6 +10,9 @@ module PhlexKit
|
|
|
10
10
|
def view_template
|
|
11
11
|
input(**mix({
|
|
12
12
|
type: :checkbox,
|
|
13
|
+
# The option label is the interactive surface (ARIA option); the real
|
|
14
|
+
# input must not be a second tab stop inside it.
|
|
15
|
+
tabindex: "-1",
|
|
13
16
|
class: "pk-combobox-checkbox",
|
|
14
17
|
data: {
|
|
15
18
|
phlex_kit__combobox_target: "input",
|