shadcn-phlex 0.1.0
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 +7 -0
- data/README.md +195 -0
- data/app.css +20 -0
- data/css/shadcn-source.css +3 -0
- data/css/shadcn-tailwind.css +160 -0
- data/css/themes/mauve.css +62 -0
- data/css/themes/mist.css +62 -0
- data/css/themes/neutral.css +74 -0
- data/css/themes/olive.css +62 -0
- data/css/themes/stone.css +62 -0
- data/css/themes/taupe.css +62 -0
- data/css/themes/zinc.css +62 -0
- data/js/controllers/accordion_controller.js +135 -0
- data/js/controllers/checkbox_controller.js +52 -0
- data/js/controllers/collapsible_controller.js +85 -0
- data/js/controllers/combobox_controller.js +168 -0
- data/js/controllers/command_controller.js +171 -0
- data/js/controllers/context_menu_controller.js +132 -0
- data/js/controllers/dark_mode_controller.js +106 -0
- data/js/controllers/dialog_controller.js +205 -0
- data/js/controllers/drawer_controller.js +161 -0
- data/js/controllers/dropdown_menu_controller.js +189 -0
- data/js/controllers/hover_card_controller.js +85 -0
- data/js/controllers/index.js +89 -0
- data/js/controllers/menubar_controller.js +171 -0
- data/js/controllers/navigation_menu_controller.js +160 -0
- data/js/controllers/popover_controller.js +151 -0
- data/js/controllers/radio_group_controller.js +78 -0
- data/js/controllers/scroll_area_controller.js +117 -0
- data/js/controllers/select_controller.js +198 -0
- data/js/controllers/sheet_controller.js +130 -0
- data/js/controllers/slider_controller.js +142 -0
- data/js/controllers/switch_controller.js +40 -0
- data/js/controllers/tabs_controller.js +96 -0
- data/js/controllers/toast_controller.js +206 -0
- data/js/controllers/toggle_controller.js +30 -0
- data/js/controllers/toggle_group_controller.js +73 -0
- data/js/controllers/tooltip_controller.js +146 -0
- data/lib/generators/shadcn_phlex/component_generator.rb +79 -0
- data/lib/generators/shadcn_phlex/install_generator.rb +217 -0
- data/lib/shadcn/base.rb +27 -0
- data/lib/shadcn/engine.rb +24 -0
- data/lib/shadcn/kit.rb +1158 -0
- data/lib/shadcn/themes/accent_colors.rb +106 -0
- data/lib/shadcn/themes/base_colors.rb +313 -0
- data/lib/shadcn/ui/accordion.rb +135 -0
- data/lib/shadcn/ui/alert.rb +79 -0
- data/lib/shadcn/ui/alert_dialog.rb +220 -0
- data/lib/shadcn/ui/aspect_ratio.rb +35 -0
- data/lib/shadcn/ui/avatar.rb +134 -0
- data/lib/shadcn/ui/badge.rb +48 -0
- data/lib/shadcn/ui/breadcrumb.rb +180 -0
- data/lib/shadcn/ui/button.rb +63 -0
- data/lib/shadcn/ui/button_group.rb +58 -0
- data/lib/shadcn/ui/card.rb +133 -0
- data/lib/shadcn/ui/checkbox.rb +72 -0
- data/lib/shadcn/ui/collapsible.rb +76 -0
- data/lib/shadcn/ui/combobox.rb +229 -0
- data/lib/shadcn/ui/command.rb +256 -0
- data/lib/shadcn/ui/context_menu.rb +319 -0
- data/lib/shadcn/ui/dialog.rb +226 -0
- data/lib/shadcn/ui/direction.rb +23 -0
- data/lib/shadcn/ui/drawer.rb +217 -0
- data/lib/shadcn/ui/dropdown_menu.rb +384 -0
- data/lib/shadcn/ui/empty.rb +97 -0
- data/lib/shadcn/ui/field.rb +126 -0
- data/lib/shadcn/ui/hover_card.rb +75 -0
- data/lib/shadcn/ui/input.rb +36 -0
- data/lib/shadcn/ui/input_group.rb +32 -0
- data/lib/shadcn/ui/input_otp.rb +112 -0
- data/lib/shadcn/ui/item.rb +115 -0
- data/lib/shadcn/ui/kbd.rb +45 -0
- data/lib/shadcn/ui/label.rb +28 -0
- data/lib/shadcn/ui/menubar.rb +345 -0
- data/lib/shadcn/ui/native_select.rb +31 -0
- data/lib/shadcn/ui/navigation_menu.rb +238 -0
- data/lib/shadcn/ui/pagination.rb +224 -0
- data/lib/shadcn/ui/popover.rb +147 -0
- data/lib/shadcn/ui/progress.rb +40 -0
- data/lib/shadcn/ui/radio_group.rb +92 -0
- data/lib/shadcn/ui/resizable.rb +108 -0
- data/lib/shadcn/ui/scroll_area.rb +75 -0
- data/lib/shadcn/ui/select.rb +235 -0
- data/lib/shadcn/ui/separator.rb +36 -0
- data/lib/shadcn/ui/sheet.rb +231 -0
- data/lib/shadcn/ui/sidebar.rb +420 -0
- data/lib/shadcn/ui/skeleton.rb +23 -0
- data/lib/shadcn/ui/slider.rb +72 -0
- data/lib/shadcn/ui/sonner.rb +177 -0
- data/lib/shadcn/ui/spinner.rb +58 -0
- data/lib/shadcn/ui/switch.rb +75 -0
- data/lib/shadcn/ui/table.rb +154 -0
- data/lib/shadcn/ui/tabs.rb +154 -0
- data/lib/shadcn/ui/text_field.rb +146 -0
- data/lib/shadcn/ui/textarea.rb +32 -0
- data/lib/shadcn/ui/theme_toggle.rb +74 -0
- data/lib/shadcn/ui/toggle.rb +66 -0
- data/lib/shadcn/ui/toggle_group.rb +75 -0
- data/lib/shadcn/ui/tooltip.rb +78 -0
- data/lib/shadcn/ui/typography.rb +217 -0
- data/lib/shadcn/version.rb +5 -0
- data/lib/shadcn-phlex.rb +6 -0
- data/lib/shadcn.rb +80 -0
- data/package.json +14 -0
- data/skills/shadcn-phlex/SKILL.md +190 -0
- data/skills/shadcn-phlex/evals/evals.json +90 -0
- data/skills/shadcn-phlex/references/component-catalog.md +355 -0
- data/skills/shadcn-phlex/rules/composition.md +235 -0
- data/skills/shadcn-phlex/rules/forms.md +151 -0
- data/skills/shadcn-phlex/rules/helpers.md +54 -0
- data/skills/shadcn-phlex/rules/stimulus.md +61 -0
- data/skills/shadcn-phlex/rules/styling.md +177 -0
- metadata +209 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* shadcn theme: stone
|
|
3
|
+
* Compatible with ui.shadcn.com/themes — paste any theme here to swap.
|
|
4
|
+
*/
|
|
5
|
+
:root {
|
|
6
|
+
--radius: 0.625rem;
|
|
7
|
+
--background: oklch(1 0 75);
|
|
8
|
+
--foreground: oklch(0.147 0.004 49.25);
|
|
9
|
+
--card: oklch(1 0 75);
|
|
10
|
+
--card-foreground: oklch(0.147 0.004 49.25);
|
|
11
|
+
--popover: oklch(1 0 75);
|
|
12
|
+
--popover-foreground: oklch(0.147 0.004 49.25);
|
|
13
|
+
--primary: oklch(0.216 0.006 56.043);
|
|
14
|
+
--primary-foreground: oklch(0.985 0.001 106.423);
|
|
15
|
+
--secondary: oklch(0.97 0.001 106.424);
|
|
16
|
+
--secondary-foreground: oklch(0.216 0.006 56.043);
|
|
17
|
+
--muted: oklch(0.97 0.001 106.424);
|
|
18
|
+
--muted-foreground: oklch(0.553 0.013 58.071);
|
|
19
|
+
--accent: oklch(0.97 0.001 106.424);
|
|
20
|
+
--accent-foreground: oklch(0.216 0.006 56.043);
|
|
21
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
22
|
+
--border: oklch(0.923 0.003 48.717);
|
|
23
|
+
--input: oklch(0.923 0.003 48.717);
|
|
24
|
+
--ring: oklch(0.709 0.01 56.259);
|
|
25
|
+
--sidebar: oklch(0.985 0.001 106.423);
|
|
26
|
+
--sidebar-foreground: oklch(0.147 0.004 49.25);
|
|
27
|
+
--sidebar-primary: oklch(0.216 0.006 56.043);
|
|
28
|
+
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
|
|
29
|
+
--sidebar-accent: oklch(0.97 0.001 106.424);
|
|
30
|
+
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
|
|
31
|
+
--sidebar-border: oklch(0.923 0.003 48.717);
|
|
32
|
+
--sidebar-ring: oklch(0.709 0.01 56.259);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.dark {
|
|
36
|
+
--background: oklch(0.147 0.004 49.25);
|
|
37
|
+
--foreground: oklch(0.985 0.001 106.423);
|
|
38
|
+
--card: oklch(0.216 0.006 56.043);
|
|
39
|
+
--card-foreground: oklch(0.985 0.001 106.423);
|
|
40
|
+
--popover: oklch(0.216 0.006 56.043);
|
|
41
|
+
--popover-foreground: oklch(0.985 0.001 106.423);
|
|
42
|
+
--primary: oklch(0.923 0.003 48.717);
|
|
43
|
+
--primary-foreground: oklch(0.216 0.006 56.043);
|
|
44
|
+
--secondary: oklch(0.268 0.007 34.298);
|
|
45
|
+
--secondary-foreground: oklch(0.985 0.001 106.423);
|
|
46
|
+
--muted: oklch(0.268 0.007 34.298);
|
|
47
|
+
--muted-foreground: oklch(0.709 0.01 56.259);
|
|
48
|
+
--accent: oklch(0.268 0.007 34.298);
|
|
49
|
+
--accent-foreground: oklch(0.985 0.001 106.423);
|
|
50
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
51
|
+
--border: oklch(1 0 0 / 10%);
|
|
52
|
+
--input: oklch(1 0 0 / 15%);
|
|
53
|
+
--ring: oklch(0.553 0.013 58.071);
|
|
54
|
+
--sidebar: oklch(0.216 0.006 56.043);
|
|
55
|
+
--sidebar-foreground: oklch(0.985 0.001 106.423);
|
|
56
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
57
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
58
|
+
--sidebar-accent: oklch(0.268 0.007 34.298);
|
|
59
|
+
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
|
|
60
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
61
|
+
--sidebar-ring: oklch(0.553 0.013 58.071);
|
|
62
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* shadcn theme: taupe
|
|
3
|
+
* Compatible with ui.shadcn.com/themes — paste any theme here to swap.
|
|
4
|
+
*/
|
|
5
|
+
:root {
|
|
6
|
+
--radius: 0.625rem;
|
|
7
|
+
--background: oklch(1 0 0);
|
|
8
|
+
--foreground: oklch(0.15 0.008 75);
|
|
9
|
+
--card: oklch(1 0 0);
|
|
10
|
+
--card-foreground: oklch(0.15 0.008 75);
|
|
11
|
+
--popover: oklch(1 0 0);
|
|
12
|
+
--popover-foreground: oklch(0.15 0.008 75);
|
|
13
|
+
--primary: oklch(0.218 0.012 75);
|
|
14
|
+
--primary-foreground: oklch(0.985 0.002 75);
|
|
15
|
+
--secondary: oklch(0.968 0.003 75);
|
|
16
|
+
--secondary-foreground: oklch(0.218 0.012 75);
|
|
17
|
+
--muted: oklch(0.968 0.003 75);
|
|
18
|
+
--muted-foreground: oklch(0.556 0.016 75);
|
|
19
|
+
--accent: oklch(0.968 0.003 75);
|
|
20
|
+
--accent-foreground: oklch(0.218 0.012 75);
|
|
21
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
22
|
+
--border: oklch(0.923 0.005 75);
|
|
23
|
+
--input: oklch(0.923 0.005 75);
|
|
24
|
+
--ring: oklch(0.708 0.014 75);
|
|
25
|
+
--sidebar: oklch(0.985 0.002 75);
|
|
26
|
+
--sidebar-foreground: oklch(0.15 0.008 75);
|
|
27
|
+
--sidebar-primary: oklch(0.218 0.012 75);
|
|
28
|
+
--sidebar-primary-foreground: oklch(0.985 0.002 75);
|
|
29
|
+
--sidebar-accent: oklch(0.968 0.003 75);
|
|
30
|
+
--sidebar-accent-foreground: oklch(0.218 0.012 75);
|
|
31
|
+
--sidebar-border: oklch(0.923 0.005 75);
|
|
32
|
+
--sidebar-ring: oklch(0.708 0.014 75);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.dark {
|
|
36
|
+
--background: oklch(0.15 0.008 75);
|
|
37
|
+
--foreground: oklch(0.985 0.002 75);
|
|
38
|
+
--card: oklch(0.218 0.012 75);
|
|
39
|
+
--card-foreground: oklch(0.985 0.002 75);
|
|
40
|
+
--popover: oklch(0.218 0.012 75);
|
|
41
|
+
--popover-foreground: oklch(0.985 0.002 75);
|
|
42
|
+
--primary: oklch(0.923 0.005 75);
|
|
43
|
+
--primary-foreground: oklch(0.218 0.012 75);
|
|
44
|
+
--secondary: oklch(0.274 0.012 75);
|
|
45
|
+
--secondary-foreground: oklch(0.985 0.002 75);
|
|
46
|
+
--muted: oklch(0.274 0.012 75);
|
|
47
|
+
--muted-foreground: oklch(0.708 0.014 75);
|
|
48
|
+
--accent: oklch(0.274 0.012 75);
|
|
49
|
+
--accent-foreground: oklch(0.985 0.002 75);
|
|
50
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
51
|
+
--border: oklch(1 0 0 / 10%);
|
|
52
|
+
--input: oklch(1 0 0 / 15%);
|
|
53
|
+
--ring: oklch(0.556 0.016 75);
|
|
54
|
+
--sidebar: oklch(0.218 0.012 75);
|
|
55
|
+
--sidebar-foreground: oklch(0.985 0.002 75);
|
|
56
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
57
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
58
|
+
--sidebar-accent: oklch(0.274 0.012 75);
|
|
59
|
+
--sidebar-accent-foreground: oklch(0.985 0.002 75);
|
|
60
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
61
|
+
--sidebar-ring: oklch(0.556 0.016 75);
|
|
62
|
+
}
|
data/css/themes/zinc.css
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* shadcn theme: zinc
|
|
3
|
+
* Compatible with ui.shadcn.com/themes — paste any theme here to swap.
|
|
4
|
+
*/
|
|
5
|
+
:root {
|
|
6
|
+
--radius: 0.625rem;
|
|
7
|
+
--background: oklch(1 0 0);
|
|
8
|
+
--foreground: oklch(0.141 0.005 285.823);
|
|
9
|
+
--card: oklch(1 0 0);
|
|
10
|
+
--card-foreground: oklch(0.141 0.005 285.823);
|
|
11
|
+
--popover: oklch(1 0 0);
|
|
12
|
+
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
13
|
+
--primary: oklch(0.21 0.006 285.885);
|
|
14
|
+
--primary-foreground: oklch(0.985 0.002 247.839);
|
|
15
|
+
--secondary: oklch(0.967 0.001 264.542);
|
|
16
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
17
|
+
--muted: oklch(0.967 0.001 264.542);
|
|
18
|
+
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
19
|
+
--accent: oklch(0.967 0.001 264.542);
|
|
20
|
+
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
21
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
22
|
+
--border: oklch(0.92 0.004 264.532);
|
|
23
|
+
--input: oklch(0.92 0.004 264.532);
|
|
24
|
+
--ring: oklch(0.705 0.015 286.067);
|
|
25
|
+
--sidebar: oklch(0.985 0 0);
|
|
26
|
+
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
27
|
+
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
28
|
+
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
|
29
|
+
--sidebar-accent: oklch(0.967 0.001 264.542);
|
|
30
|
+
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
31
|
+
--sidebar-border: oklch(0.92 0.004 264.532);
|
|
32
|
+
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.dark {
|
|
36
|
+
--background: oklch(0.141 0.005 285.823);
|
|
37
|
+
--foreground: oklch(0.985 0.002 247.839);
|
|
38
|
+
--card: oklch(0.21 0.006 285.885);
|
|
39
|
+
--card-foreground: oklch(0.985 0.002 247.839);
|
|
40
|
+
--popover: oklch(0.21 0.006 285.885);
|
|
41
|
+
--popover-foreground: oklch(0.985 0.002 247.839);
|
|
42
|
+
--primary: oklch(0.92 0.004 264.532);
|
|
43
|
+
--primary-foreground: oklch(0.21 0.006 285.885);
|
|
44
|
+
--secondary: oklch(0.274 0.006 286.033);
|
|
45
|
+
--secondary-foreground: oklch(0.985 0.002 247.839);
|
|
46
|
+
--muted: oklch(0.274 0.006 286.033);
|
|
47
|
+
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
48
|
+
--accent: oklch(0.274 0.006 286.033);
|
|
49
|
+
--accent-foreground: oklch(0.985 0.002 247.839);
|
|
50
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
51
|
+
--border: oklch(1 0 0 / 10%);
|
|
52
|
+
--input: oklch(1 0 0 / 15%);
|
|
53
|
+
--ring: oklch(0.552 0.016 285.938);
|
|
54
|
+
--sidebar: oklch(0.21 0.006 285.885);
|
|
55
|
+
--sidebar-foreground: oklch(0.985 0.002 247.839);
|
|
56
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
57
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
58
|
+
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
59
|
+
--sidebar-accent-foreground: oklch(0.985 0.002 247.839);
|
|
60
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
61
|
+
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
62
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Replicates Radix Accordion behavior
|
|
4
|
+
// Supports single (default) or multiple open items
|
|
5
|
+
// data-type="single" | "multiple"
|
|
6
|
+
// data-collapsible="true" allows closing all items in single mode
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
static targets = ["item", "trigger", "content"]
|
|
9
|
+
static values = {
|
|
10
|
+
type: { type: String, default: "single" },
|
|
11
|
+
collapsible: { type: Boolean, default: false },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
connect() {
|
|
15
|
+
this.itemTargets.forEach((item) => {
|
|
16
|
+
const content = item.querySelector('[data-slot="accordion-content"]')
|
|
17
|
+
if (content && item.dataset.state !== "open") {
|
|
18
|
+
content.hidden = true
|
|
19
|
+
content.dataset.state = "closed"
|
|
20
|
+
} else if (content && item.dataset.state === "open") {
|
|
21
|
+
content.hidden = false
|
|
22
|
+
content.dataset.state = "open"
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
toggle(event) {
|
|
28
|
+
const trigger = event.currentTarget
|
|
29
|
+
const item = trigger.closest('[data-slot="accordion-item"]')
|
|
30
|
+
if (!item) return
|
|
31
|
+
|
|
32
|
+
const isOpen = item.dataset.state === "open"
|
|
33
|
+
|
|
34
|
+
if (this.typeValue === "single") {
|
|
35
|
+
// Close all other items
|
|
36
|
+
this.itemTargets.forEach((otherItem) => {
|
|
37
|
+
if (otherItem !== item && otherItem.dataset.state === "open") {
|
|
38
|
+
this._closeItem(otherItem)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (isOpen && (this.collapsibleValue || this.typeValue === "multiple")) {
|
|
44
|
+
this._closeItem(item)
|
|
45
|
+
} else if (!isOpen) {
|
|
46
|
+
this._openItem(item)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
keydown(event) {
|
|
51
|
+
const triggers = this.triggerTargets
|
|
52
|
+
const index = triggers.indexOf(event.currentTarget)
|
|
53
|
+
let nextIndex
|
|
54
|
+
|
|
55
|
+
switch (event.key) {
|
|
56
|
+
case "ArrowDown":
|
|
57
|
+
event.preventDefault()
|
|
58
|
+
nextIndex = (index + 1) % triggers.length
|
|
59
|
+
triggers[nextIndex].focus()
|
|
60
|
+
break
|
|
61
|
+
case "ArrowUp":
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
nextIndex = (index - 1 + triggers.length) % triggers.length
|
|
64
|
+
triggers[nextIndex].focus()
|
|
65
|
+
break
|
|
66
|
+
case "Home":
|
|
67
|
+
event.preventDefault()
|
|
68
|
+
triggers[0].focus()
|
|
69
|
+
break
|
|
70
|
+
case "End":
|
|
71
|
+
event.preventDefault()
|
|
72
|
+
triggers[triggers.length - 1].focus()
|
|
73
|
+
break
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_openItem(item) {
|
|
78
|
+
const content = item.querySelector('[data-slot="accordion-content"]')
|
|
79
|
+
const trigger = item.querySelector('[data-slot="accordion-trigger"]')
|
|
80
|
+
if (!content) return
|
|
81
|
+
|
|
82
|
+
item.dataset.state = "open"
|
|
83
|
+
if (trigger) {
|
|
84
|
+
trigger.dataset.state = "open"
|
|
85
|
+
trigger.setAttribute("aria-expanded", "true")
|
|
86
|
+
}
|
|
87
|
+
content.dataset.state = "open"
|
|
88
|
+
content.hidden = false
|
|
89
|
+
|
|
90
|
+
// Animate open
|
|
91
|
+
const height = content.scrollHeight
|
|
92
|
+
content.style.height = "0px"
|
|
93
|
+
content.style.overflow = "hidden"
|
|
94
|
+
requestAnimationFrame(() => {
|
|
95
|
+
content.style.transition = "height 200ms ease-out"
|
|
96
|
+
content.style.height = `${height}px`
|
|
97
|
+
const onEnd = () => {
|
|
98
|
+
content.style.height = ""
|
|
99
|
+
content.style.overflow = ""
|
|
100
|
+
content.style.transition = ""
|
|
101
|
+
content.removeEventListener("transitionend", onEnd)
|
|
102
|
+
}
|
|
103
|
+
content.addEventListener("transitionend", onEnd)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_closeItem(item) {
|
|
108
|
+
const content = item.querySelector('[data-slot="accordion-content"]')
|
|
109
|
+
const trigger = item.querySelector('[data-slot="accordion-trigger"]')
|
|
110
|
+
if (!content) return
|
|
111
|
+
|
|
112
|
+
// Animate close
|
|
113
|
+
const height = content.scrollHeight
|
|
114
|
+
content.style.height = `${height}px`
|
|
115
|
+
content.style.overflow = "hidden"
|
|
116
|
+
requestAnimationFrame(() => {
|
|
117
|
+
content.style.transition = "height 200ms ease-out"
|
|
118
|
+
content.style.height = "0px"
|
|
119
|
+
const onEnd = () => {
|
|
120
|
+
content.hidden = true
|
|
121
|
+
content.style.height = ""
|
|
122
|
+
content.style.overflow = ""
|
|
123
|
+
content.style.transition = ""
|
|
124
|
+
item.dataset.state = "closed"
|
|
125
|
+
if (trigger) {
|
|
126
|
+
trigger.dataset.state = "closed"
|
|
127
|
+
trigger.setAttribute("aria-expanded", "false")
|
|
128
|
+
}
|
|
129
|
+
content.dataset.state = "closed"
|
|
130
|
+
content.removeEventListener("transitionend", onEnd)
|
|
131
|
+
}
|
|
132
|
+
content.addEventListener("transitionend", onEnd)
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Replicates Radix Checkbox behavior
|
|
4
|
+
// Toggle checked/unchecked/indeterminate, updates hidden input for forms
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["button", "indicator", "input"]
|
|
7
|
+
static values = {
|
|
8
|
+
checked: { type: String, default: "false" }, // "true", "false", "indeterminate"
|
|
9
|
+
disabled: { type: Boolean, default: false },
|
|
10
|
+
name: String,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
connect() {
|
|
14
|
+
this._syncState()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toggle() {
|
|
18
|
+
if (this.disabledValue) return
|
|
19
|
+
|
|
20
|
+
if (this.checkedValue === "true") {
|
|
21
|
+
this.checkedValue = "false"
|
|
22
|
+
} else {
|
|
23
|
+
this.checkedValue = "true"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.dispatch("change", { detail: { checked: this.checkedValue === "true" } })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
checkedValueChanged() { this._syncState() }
|
|
30
|
+
|
|
31
|
+
_syncState() {
|
|
32
|
+
const isChecked = this.checkedValue === "true"
|
|
33
|
+
const isIndeterminate = this.checkedValue === "indeterminate"
|
|
34
|
+
const state = isIndeterminate ? "indeterminate" : isChecked ? "checked" : "unchecked"
|
|
35
|
+
|
|
36
|
+
this.buttonTargets.forEach((btn) => {
|
|
37
|
+
btn.dataset.state = state
|
|
38
|
+
btn.setAttribute("aria-checked", isIndeterminate ? "mixed" : String(isChecked))
|
|
39
|
+
btn.setAttribute("data-disabled", String(this.disabledValue))
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
this.indicatorTargets.forEach((ind) => {
|
|
43
|
+
ind.hidden = !isChecked && !isIndeterminate
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Update hidden input for form submission
|
|
47
|
+
this.inputTargets.forEach((input) => {
|
|
48
|
+
input.value = isChecked ? "1" : "0"
|
|
49
|
+
input.checked = isChecked
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Replicates Radix Collapsible behavior
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = ["trigger", "content"]
|
|
6
|
+
static values = {
|
|
7
|
+
open: { type: Boolean, default: false },
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this._syncState()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
toggle() {
|
|
15
|
+
this.openValue = !this.openValue
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
open() {
|
|
19
|
+
this.openValue = true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
close() {
|
|
23
|
+
this.openValue = false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
openValueChanged() {
|
|
27
|
+
this._syncState()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_syncState() {
|
|
31
|
+
const state = this.openValue ? "open" : "closed"
|
|
32
|
+
this.element.dataset.state = state
|
|
33
|
+
|
|
34
|
+
this.triggerTargets.forEach((trigger) => {
|
|
35
|
+
trigger.dataset.state = state
|
|
36
|
+
trigger.setAttribute("aria-expanded", String(this.openValue))
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
this.contentTargets.forEach((content) => {
|
|
40
|
+
if (this.openValue) {
|
|
41
|
+
this._animateOpen(content)
|
|
42
|
+
} else {
|
|
43
|
+
this._animateClose(content)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_animateOpen(content) {
|
|
49
|
+
content.hidden = false
|
|
50
|
+
content.dataset.state = "open"
|
|
51
|
+
const height = content.scrollHeight
|
|
52
|
+
content.style.height = "0px"
|
|
53
|
+
content.style.overflow = "hidden"
|
|
54
|
+
requestAnimationFrame(() => {
|
|
55
|
+
content.style.transition = "height 200ms ease-out"
|
|
56
|
+
content.style.height = `${height}px`
|
|
57
|
+
const onEnd = () => {
|
|
58
|
+
content.style.height = ""
|
|
59
|
+
content.style.overflow = ""
|
|
60
|
+
content.style.transition = ""
|
|
61
|
+
content.removeEventListener("transitionend", onEnd)
|
|
62
|
+
}
|
|
63
|
+
content.addEventListener("transitionend", onEnd)
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_animateClose(content) {
|
|
68
|
+
const height = content.scrollHeight
|
|
69
|
+
content.dataset.state = "closed"
|
|
70
|
+
content.style.height = `${height}px`
|
|
71
|
+
content.style.overflow = "hidden"
|
|
72
|
+
requestAnimationFrame(() => {
|
|
73
|
+
content.style.transition = "height 200ms ease-out"
|
|
74
|
+
content.style.height = "0px"
|
|
75
|
+
const onEnd = () => {
|
|
76
|
+
content.hidden = true
|
|
77
|
+
content.style.height = ""
|
|
78
|
+
content.style.overflow = ""
|
|
79
|
+
content.style.transition = ""
|
|
80
|
+
content.removeEventListener("transitionend", onEnd)
|
|
81
|
+
}
|
|
82
|
+
content.addEventListener("transitionend", onEnd)
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Combobox: filterable select with search input
|
|
4
|
+
// Uses Stimulus declarative click@window for outside click (no flicker)
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["input", "content", "item", "empty", "hiddenInput", "value"]
|
|
7
|
+
static values = {
|
|
8
|
+
open: { type: Boolean, default: false },
|
|
9
|
+
value: { type: String, default: "" },
|
|
10
|
+
placeholder: { type: String, default: "Search..." },
|
|
11
|
+
emptyText: { type: String, default: "No results found." },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
connect() {
|
|
15
|
+
this._hideTimeouts = []
|
|
16
|
+
this._allItems = this.itemTargets.map((el) => ({
|
|
17
|
+
element: el,
|
|
18
|
+
value: el.dataset.value || "",
|
|
19
|
+
label: el.textContent.trim().toLowerCase(),
|
|
20
|
+
}))
|
|
21
|
+
this.contentTargets.forEach((el) => { el.dataset.state = "closed"; el.hidden = true })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
disconnect() {
|
|
25
|
+
this._hideTimeouts.forEach(id => clearTimeout(id))
|
|
26
|
+
this._hideTimeouts = []
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
toggle() { this.openValue = !this.openValue }
|
|
30
|
+
|
|
31
|
+
// Wired as click@window->shadcn--combobox#hide
|
|
32
|
+
hide(event) {
|
|
33
|
+
if (!this.openValue) return
|
|
34
|
+
if (event && event.target && this.element.contains(event.target)) return
|
|
35
|
+
this.openValue = false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Wired as keydown.esc@window->shadcn--combobox#hideOnEscape
|
|
39
|
+
hideOnEscape() {
|
|
40
|
+
if (!this.openValue) return
|
|
41
|
+
this.openValue = false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
close() { this.openValue = false }
|
|
45
|
+
|
|
46
|
+
filter(event) {
|
|
47
|
+
const query = event.currentTarget.value.toLowerCase()
|
|
48
|
+
let visibleCount = 0
|
|
49
|
+
|
|
50
|
+
this._allItems.forEach(({ element, label }) => {
|
|
51
|
+
const match = label.includes(query)
|
|
52
|
+
element.hidden = !match
|
|
53
|
+
if (match) visibleCount++
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
this.emptyTargets.forEach((el) => { el.hidden = visibleCount > 0 })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
selectItem(event) {
|
|
60
|
+
const item = event.currentTarget
|
|
61
|
+
if (item.dataset.disabled) return
|
|
62
|
+
|
|
63
|
+
const value = item.dataset.value
|
|
64
|
+
const label = item.textContent.trim()
|
|
65
|
+
|
|
66
|
+
this.valueValue = value
|
|
67
|
+
this.valueTargets.forEach((el) => { el.textContent = label })
|
|
68
|
+
this.inputTargets.forEach((el) => { el.value = "" })
|
|
69
|
+
|
|
70
|
+
this.dispatch("change", { detail: { value, label } })
|
|
71
|
+
this.openValue = false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
keydown(event) {
|
|
75
|
+
if (!this.openValue && (event.key === "ArrowDown" || event.key === "Enter")) {
|
|
76
|
+
event.preventDefault()
|
|
77
|
+
this.openValue = true
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
if (event.key === "Escape") { this.openValue = false; return }
|
|
81
|
+
|
|
82
|
+
const items = this._getVisibleItems()
|
|
83
|
+
const current = items.indexOf(document.activeElement)
|
|
84
|
+
|
|
85
|
+
switch (event.key) {
|
|
86
|
+
case "ArrowDown":
|
|
87
|
+
event.preventDefault()
|
|
88
|
+
if (current < items.length - 1) items[current + 1]?.focus()
|
|
89
|
+
else if (current === -1 && items.length > 0) items[0].focus()
|
|
90
|
+
break
|
|
91
|
+
case "ArrowUp":
|
|
92
|
+
event.preventDefault()
|
|
93
|
+
if (current > 0) items[current - 1]?.focus()
|
|
94
|
+
else if (this.hasInputTarget) this.inputTarget.focus()
|
|
95
|
+
break
|
|
96
|
+
case "Enter":
|
|
97
|
+
if (document.activeElement?.matches('[data-slot*="item"]')) {
|
|
98
|
+
event.preventDefault()
|
|
99
|
+
document.activeElement.click()
|
|
100
|
+
}
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
openValueChanged() {
|
|
106
|
+
if (!this._hideTimeouts) return
|
|
107
|
+
this._render()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
valueValueChanged() { this._syncValueState() }
|
|
111
|
+
|
|
112
|
+
_render() {
|
|
113
|
+
const open = this.openValue
|
|
114
|
+
|
|
115
|
+
this._hideTimeouts.forEach(id => clearTimeout(id))
|
|
116
|
+
this._hideTimeouts = []
|
|
117
|
+
|
|
118
|
+
this.contentTargets.forEach((el) => {
|
|
119
|
+
if (open) {
|
|
120
|
+
el.getAnimations().forEach(a => a.cancel())
|
|
121
|
+
el.hidden = false
|
|
122
|
+
el.dataset.state = "open"
|
|
123
|
+
requestAnimationFrame(() => {
|
|
124
|
+
this._position(el)
|
|
125
|
+
if (this.hasInputTarget) this.inputTarget.focus()
|
|
126
|
+
})
|
|
127
|
+
} else {
|
|
128
|
+
el.dataset.state = "closed"
|
|
129
|
+
// Reset filter
|
|
130
|
+
this._allItems.forEach(({ element }) => { element.hidden = false })
|
|
131
|
+
this.emptyTargets.forEach((em) => { em.hidden = true })
|
|
132
|
+
|
|
133
|
+
const animations = el.getAnimations()
|
|
134
|
+
if (animations.length > 0) {
|
|
135
|
+
Promise.all(animations.map(a => a.finished)).then(() => {
|
|
136
|
+
if (el.dataset.state === "closed") el.hidden = true
|
|
137
|
+
}).catch(() => {})
|
|
138
|
+
} else {
|
|
139
|
+
el.hidden = true
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_syncValueState() {
|
|
146
|
+
this.itemTargets.forEach((item) => {
|
|
147
|
+
const isSelected = item.dataset.value === this.valueValue
|
|
148
|
+
item.dataset.state = isSelected ? "checked" : "unchecked"
|
|
149
|
+
item.setAttribute("aria-selected", String(isSelected))
|
|
150
|
+
})
|
|
151
|
+
this.hiddenInputTargets.forEach((input) => { input.value = this.valueValue })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_position(content) {
|
|
155
|
+
// Use the trigger for positioning, not the root (which expands when content is visible)
|
|
156
|
+
const trigger = this.element.querySelector('[data-slot="combobox-trigger"]') || this.element
|
|
157
|
+
const rect = trigger.getBoundingClientRect()
|
|
158
|
+
content.style.position = "fixed"
|
|
159
|
+
content.style.zIndex = "50"
|
|
160
|
+
content.style.width = `${Math.max(rect.width, 200)}px`
|
|
161
|
+
content.style.top = `${rect.bottom + 4}px`
|
|
162
|
+
content.style.left = `${rect.left}px`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_getVisibleItems() {
|
|
166
|
+
return this.itemTargets.filter((el) => !el.hidden && !el.dataset.disabled)
|
|
167
|
+
}
|
|
168
|
+
}
|