view_primitives 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/CHANGELOG.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +198 -0
- data/lib/generators/view_primitives/add/add_generator.rb +110 -0
- data/lib/generators/view_primitives/add/templates/accordion/accordion_component.html.erb +10 -0
- data/lib/generators/view_primitives/add/templates/accordion/accordion_component.rb.tt +22 -0
- data/lib/generators/view_primitives/add/templates/accordion/accordion_controller.js +15 -0
- data/lib/generators/view_primitives/add/templates/accordion/accordion_item_component.rb.tt +47 -0
- data/lib/generators/view_primitives/add/templates/alert/alert_component.rb.tt +62 -0
- data/lib/generators/view_primitives/add/templates/alert_dialog/alert_dialog_component.rb.tt +55 -0
- data/lib/generators/view_primitives/add/templates/aspect_ratio/aspect_ratio_component.rb.tt +18 -0
- data/lib/generators/view_primitives/add/templates/audio/audio_component.rb.tt +51 -0
- data/lib/generators/view_primitives/add/templates/avatar/avatar_component.rb.tt +37 -0
- data/lib/generators/view_primitives/add/templates/badge/badge_component.rb.tt +35 -0
- data/lib/generators/view_primitives/add/templates/banner/banner_component.rb.tt +29 -0
- data/lib/generators/view_primitives/add/templates/bottom_nav/bottom_nav_component.rb.tt +38 -0
- data/lib/generators/view_primitives/add/templates/breadcrumb/breadcrumb_component.rb.tt +37 -0
- data/lib/generators/view_primitives/add/templates/button/button_component.rb.tt +61 -0
- data/lib/generators/view_primitives/add/templates/button_group/button_group_component.rb.tt +23 -0
- data/lib/generators/view_primitives/add/templates/calendar/calendar_component.rb.tt +121 -0
- data/lib/generators/view_primitives/add/templates/calendar/calendar_controller.js +86 -0
- data/lib/generators/view_primitives/add/templates/card/card_component.rb.tt +16 -0
- data/lib/generators/view_primitives/add/templates/card/card_content_component.rb.tt +16 -0
- data/lib/generators/view_primitives/add/templates/card/card_description_component.rb.tt +17 -0
- data/lib/generators/view_primitives/add/templates/card/card_footer_component.rb.tt +16 -0
- data/lib/generators/view_primitives/add/templates/card/card_header_component.rb.tt +17 -0
- data/lib/generators/view_primitives/add/templates/card/card_title_component.rb.tt +17 -0
- data/lib/generators/view_primitives/add/templates/carousel/carousel_component.rb.tt +102 -0
- data/lib/generators/view_primitives/add/templates/carousel/carousel_controller.js +48 -0
- data/lib/generators/view_primitives/add/templates/chart/chart_component.rb.tt +63 -0
- data/lib/generators/view_primitives/add/templates/chart/chart_controller.js +29 -0
- data/lib/generators/view_primitives/add/templates/chat_bubble/chat_bubble_component.rb.tt +53 -0
- data/lib/generators/view_primitives/add/templates/checkbox/checkbox_component.rb.tt +50 -0
- data/lib/generators/view_primitives/add/templates/collapsible/collapsible_component.rb.tt +31 -0
- data/lib/generators/view_primitives/add/templates/combobox/combobox_component.rb.tt +87 -0
- data/lib/generators/view_primitives/add/templates/combobox/combobox_controller.js +38 -0
- data/lib/generators/view_primitives/add/templates/command/command_component.rb.tt +85 -0
- data/lib/generators/view_primitives/add/templates/command/command_controller.js +50 -0
- data/lib/generators/view_primitives/add/templates/context_menu/context_menu_component.rb.tt +47 -0
- data/lib/generators/view_primitives/add/templates/context_menu/context_menu_controller.js +20 -0
- data/lib/generators/view_primitives/add/templates/data_table/data_table_component.rb.tt +163 -0
- data/lib/generators/view_primitives/add/templates/data_table/data_table_controller.js +115 -0
- data/lib/generators/view_primitives/add/templates/date_picker/date_picker_component.rb.tt +92 -0
- data/lib/generators/view_primitives/add/templates/date_picker/date_picker_controller.js +48 -0
- data/lib/generators/view_primitives/add/templates/device_mockup/device_mockup_component.rb.tt +65 -0
- data/lib/generators/view_primitives/add/templates/dialog/dialog_component.rb.tt +71 -0
- data/lib/generators/view_primitives/add/templates/dialog/dialog_controller.js +15 -0
- data/lib/generators/view_primitives/add/templates/drawer/drawer_component.rb.tt +62 -0
- data/lib/generators/view_primitives/add/templates/drawer/drawer_controller.js +15 -0
- data/lib/generators/view_primitives/add/templates/dropdown_menu/dropdown_controller.js +17 -0
- data/lib/generators/view_primitives/add/templates/dropdown_menu/dropdown_menu_component.rb.tt +53 -0
- data/lib/generators/view_primitives/add/templates/embed/embed_component.rb.tt +271 -0
- data/lib/generators/view_primitives/add/templates/embed/embed_controller.js +43 -0
- data/lib/generators/view_primitives/add/templates/figure/figure_component.rb.tt +24 -0
- data/lib/generators/view_primitives/add/templates/file_input/file_input_component.rb.tt +31 -0
- data/lib/generators/view_primitives/add/templates/floating_label/floating_label_component.rb.tt +54 -0
- data/lib/generators/view_primitives/add/templates/footer/footer_component.rb.tt +51 -0
- data/lib/generators/view_primitives/add/templates/form_field/form_field_component.rb.tt +51 -0
- data/lib/generators/view_primitives/add/templates/gallery/gallery_component.rb.tt +83 -0
- data/lib/generators/view_primitives/add/templates/gallery/gallery_controller.js +28 -0
- data/lib/generators/view_primitives/add/templates/hover_card/hover_card_component.rb.tt +43 -0
- data/lib/generators/view_primitives/add/templates/iframe/iframe_component.rb.tt +59 -0
- data/lib/generators/view_primitives/add/templates/image/image_component.rb.tt +38 -0
- data/lib/generators/view_primitives/add/templates/indicator/indicator_component.rb.tt +46 -0
- data/lib/generators/view_primitives/add/templates/input/input_component.rb.tt +28 -0
- data/lib/generators/view_primitives/add/templates/input_otp/input_otp_component.rb.tt +65 -0
- data/lib/generators/view_primitives/add/templates/input_otp/input_otp_controller.js +39 -0
- data/lib/generators/view_primitives/add/templates/kbd/kbd_component.rb.tt +21 -0
- data/lib/generators/view_primitives/add/templates/label/label_component.rb.tt +23 -0
- data/lib/generators/view_primitives/add/templates/list_group/list_group_component.rb.tt +16 -0
- data/lib/generators/view_primitives/add/templates/list_group/list_group_item_component.rb.tt +31 -0
- data/lib/generators/view_primitives/add/templates/map_area/map_area_component.rb.tt +72 -0
- data/lib/generators/view_primitives/add/templates/mega_menu/mega_menu_component.rb.tt +130 -0
- data/lib/generators/view_primitives/add/templates/mega_menu/mega_menu_controller.js +23 -0
- data/lib/generators/view_primitives/add/templates/menubar/menubar_component.rb.tt +33 -0
- data/lib/generators/view_primitives/add/templates/menubar/menubar_controller.js +34 -0
- data/lib/generators/view_primitives/add/templates/menubar/menubar_menu_component.rb.tt +34 -0
- data/lib/generators/view_primitives/add/templates/navbar/navbar_component.rb.tt +90 -0
- data/lib/generators/view_primitives/add/templates/navbar/navbar_controller.js +11 -0
- data/lib/generators/view_primitives/add/templates/navigation_menu/navigation_menu_component.rb.tt +132 -0
- data/lib/generators/view_primitives/add/templates/navigation_menu/navigation_menu_controller.js +25 -0
- data/lib/generators/view_primitives/add/templates/number_input/number_input_component.rb.tt +34 -0
- data/lib/generators/view_primitives/add/templates/pagination/pagination_component.rb.tt +97 -0
- data/lib/generators/view_primitives/add/templates/picture/picture_component.rb.tt +63 -0
- data/lib/generators/view_primitives/add/templates/popover/popover_component.rb.tt +56 -0
- data/lib/generators/view_primitives/add/templates/popover/popover_controller.js +17 -0
- data/lib/generators/view_primitives/add/templates/progress/progress_component.rb.tt +28 -0
- data/lib/generators/view_primitives/add/templates/qr_code/qr_code_component.rb.tt +39 -0
- data/lib/generators/view_primitives/add/templates/radio_group/radio_group_component.rb.tt +51 -0
- data/lib/generators/view_primitives/add/templates/range/range_component.rb.tt +40 -0
- data/lib/generators/view_primitives/add/templates/rating/rating_component.rb.tt +42 -0
- data/lib/generators/view_primitives/add/templates/rating_input/rating_controller.js +47 -0
- data/lib/generators/view_primitives/add/templates/rating_input/rating_input_component.rb.tt +79 -0
- data/lib/generators/view_primitives/add/templates/resizable/resizable_component.rb.tt +91 -0
- data/lib/generators/view_primitives/add/templates/resizable/resizable_controller.js +38 -0
- data/lib/generators/view_primitives/add/templates/scroll_area/scroll_area_component.rb.tt +41 -0
- data/lib/generators/view_primitives/add/templates/search_input/search_input_component.rb.tt +50 -0
- data/lib/generators/view_primitives/add/templates/select/select_component.rb.tt +45 -0
- data/lib/generators/view_primitives/add/templates/separator/separator_component.rb.tt +25 -0
- data/lib/generators/view_primitives/add/templates/sheet/sheet_component.rb.tt +78 -0
- data/lib/generators/view_primitives/add/templates/sheet/sheet_controller.js +15 -0
- data/lib/generators/view_primitives/add/templates/sidebar/sidebar_component.rb.tt +169 -0
- data/lib/generators/view_primitives/add/templates/sidebar/sidebar_controller.js +11 -0
- data/lib/generators/view_primitives/add/templates/skeleton/skeleton_component.rb.tt +16 -0
- data/lib/generators/view_primitives/add/templates/speed_dial/speed_dial_component.rb.tt +111 -0
- data/lib/generators/view_primitives/add/templates/speed_dial/speed_dial_controller.js +22 -0
- data/lib/generators/view_primitives/add/templates/spinner/spinner_component.rb.tt +27 -0
- data/lib/generators/view_primitives/add/templates/stepper/stepper_component.rb.tt +99 -0
- data/lib/generators/view_primitives/add/templates/switch/switch_component.rb.tt +51 -0
- data/lib/generators/view_primitives/add/templates/tabs/tabs_component.html.erb +50 -0
- data/lib/generators/view_primitives/add/templates/tabs/tabs_component.rb.tt +16 -0
- data/lib/generators/view_primitives/add/templates/tabs/tabs_controller.js +26 -0
- data/lib/generators/view_primitives/add/templates/tabs/tabs_item_component.rb.tt +15 -0
- data/lib/generators/view_primitives/add/templates/textarea/textarea_component.rb.tt +24 -0
- data/lib/generators/view_primitives/add/templates/timeline/timeline_component.rb.tt +78 -0
- data/lib/generators/view_primitives/add/templates/timepicker/timepicker_component.rb.tt +140 -0
- data/lib/generators/view_primitives/add/templates/timepicker/timepicker_controller.js +92 -0
- data/lib/generators/view_primitives/add/templates/toaster/toaster_component.rb.tt +152 -0
- data/lib/generators/view_primitives/add/templates/toaster/toaster_controller.js +88 -0
- data/lib/generators/view_primitives/add/templates/toggle/toggle_component.rb.tt +41 -0
- data/lib/generators/view_primitives/add/templates/toggle/toggle_controller.js +12 -0
- data/lib/generators/view_primitives/add/templates/toggle_group/toggle_group_component.rb.tt +32 -0
- data/lib/generators/view_primitives/add/templates/toggle_group/toggle_group_controller.js +38 -0
- data/lib/generators/view_primitives/add/templates/tooltip/tooltip_component.rb.tt +42 -0
- data/lib/generators/view_primitives/add/templates/video/video_component.rb.tt +92 -0
- data/lib/generators/view_primitives/add/templates/wysiwyg/wysiwyg_component.rb.tt +88 -0
- data/lib/generators/view_primitives/add/templates/wysiwyg/wysiwyg_controller.js +40 -0
- data/lib/generators/view_primitives/components.rb +62 -0
- data/lib/generators/view_primitives/detector.rb +43 -0
- data/lib/generators/view_primitives/install/install_generator.rb +65 -0
- data/lib/generators/view_primitives/install/templates/application_component.rb.tt +5 -0
- data/lib/generators/view_primitives/install/templates/view_primitives.css +67 -0
- data/lib/generators/view_primitives/list/list_generator.rb +25 -0
- data/lib/view_primitives/class_helper.rb +11 -0
- data/lib/view_primitives/component_helper.rb +20 -0
- data/lib/view_primitives/railtie.rb +21 -0
- data/lib/view_primitives/version.rb +5 -0
- data/lib/view_primitives.rb +12 -0
- metadata +267 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cae6ead2a2d30db140973eb5f5a925e2845a3d6dc4caae79bbd8a461fb56eb9f
|
|
4
|
+
data.tar.gz: 5ece5bd38e0e0db26522fb29fcec2f466396b58caff8a172a41f2bdc8b14ecaa
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7175d71aa115dce9d0f34f6275b6d05a8997e41328f40809aaee7b127337f011b4323ef9a984085f01f2e9d56cfdb822c593a1bf6188028d6e9eb76658774573
|
|
7
|
+
data.tar.gz: 35a018df2effe68ffccc863056e503f12a0fd3e101f38bba557f2e42247109091d9f2b50edea55e8f1da29ea06517109619f826428a70291329d26b03fe01cc8
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
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.1.0] - 2026-05-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
**Generators**
|
|
15
|
+
- `rails g view_primitives:install` — copies `ApplicationComponent`, CSS variables, prints Tailwind config
|
|
16
|
+
- `rails g view_primitives:add <component>` — copies component files into `app/components/ui/`; warns before overwriting
|
|
17
|
+
- `rails g view_primitives:list` — shows all available components with installed status
|
|
18
|
+
- `ui` helper available in controllers, views, and Action Mailer views
|
|
19
|
+
- Install generator checks `UI` inflection and detects existing Tailwind entry point
|
|
20
|
+
|
|
21
|
+
**Phase 1 — Foundation**
|
|
22
|
+
- Button — 6 variants, 4 sizes, defaults to `type="button"` inside forms
|
|
23
|
+
- Alert — informational banner with title/description slots and destructive variant
|
|
24
|
+
- Accordion — collapsible `<details>` sections; optional `exclusive:` Stimulus mode
|
|
25
|
+
|
|
26
|
+
**Phase 2 — Display**
|
|
27
|
+
- Badge, Avatar, Card, Separator, Label, Skeleton, Progress, Aspect Ratio, Spinner, KBD
|
|
28
|
+
- Rating — read-only star display
|
|
29
|
+
- Rating Input — interactive star rating with form/AJAX submission
|
|
30
|
+
- Indicator — status dot/count badge overlaid on an element
|
|
31
|
+
- List Group — bordered list with optional links and active state
|
|
32
|
+
- Banner — announcement strip with variants
|
|
33
|
+
- Button Group — visually joined row of buttons
|
|
34
|
+
|
|
35
|
+
**Phase 3 — Forms**
|
|
36
|
+
- Input, Textarea, Checkbox, Radio Group, Select, Switch, Toggle, Toggle Group
|
|
37
|
+
- Form Field — label + input + hint + error layout wrapper
|
|
38
|
+
- File Input, Search Input, Number Input, Range, Floating Label
|
|
39
|
+
|
|
40
|
+
**Phase 4 — Navigation**
|
|
41
|
+
- Tabs — array API + Stimulus slot API
|
|
42
|
+
- Breadcrumb, Pagination, Stepper, Bottom Navigation, Footer
|
|
43
|
+
- Navbar — responsive top bar with hamburger
|
|
44
|
+
- Navigation Menu — top-level nav with dropdown flyouts
|
|
45
|
+
- Mega Menu — full-width dropdown with grouped links and images
|
|
46
|
+
|
|
47
|
+
**Phase 5 — Overlays**
|
|
48
|
+
- Dialog, Alert Dialog, Sheet, Drawer, Popover, Tooltip, Hover Card
|
|
49
|
+
|
|
50
|
+
**Phase 6 — Menus**
|
|
51
|
+
- Dropdown Menu, Context Menu, Menubar, Command, Combobox
|
|
52
|
+
|
|
53
|
+
**Phase 7 — Complex**
|
|
54
|
+
- Calendar, Date Picker, Timepicker, Carousel, Data Table, Sidebar, Input OTP
|
|
55
|
+
- Collapsible, Scroll Area, Resizable
|
|
56
|
+
- Gallery — responsive image grid with optional lightbox
|
|
57
|
+
- Chat Bubble, Speed Dial, Device Mockup, QR Code
|
|
58
|
+
|
|
59
|
+
**Phase 8 — Advanced**
|
|
60
|
+
- Chart — Chart.js adapter (bar, line, pie, doughnut, radar, polar area)
|
|
61
|
+
- Toaster — stacked toast notifications (Sonner-style)
|
|
62
|
+
- Timeline — vertical timeline with event items
|
|
63
|
+
- WYSIWYG — rich-text editor with Trix (default) or Quill adapter
|
|
64
|
+
|
|
65
|
+
**Phase 9 — Media & Semantic HTML**
|
|
66
|
+
- Picture — `<picture>` + `<source>` for art direction and modern formats (AVIF/WebP)
|
|
67
|
+
- Video — `<video>` + `<source>` with poster, controls, and `<track>` captions
|
|
68
|
+
- Figure — `<figure>` + `<figcaption>` wrapper
|
|
69
|
+
- Image — responsive `<img>` with `srcset` / `sizes`
|
|
70
|
+
- Audio — `<audio>` + `<source>` with optional transcript link
|
|
71
|
+
- Iframe — sandboxed embed wrapper with required `title` and lazy loading
|
|
72
|
+
- Map / Area — image map with clickable `<area>` regions
|
|
73
|
+
- Embed — third-party embeds with automatic provider detection from URL; supports YouTube, Vimeo, Spotify, Google Maps, Yandex Maps, Loom, SoundCloud, X (Twitter), Telegram, Facebook
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- Removed public `component` helper — use `ui` for primitives, `render` for other namespaces
|
|
78
|
+
- `AddGenerator` copies files from template directories automatically (no per-component methods)
|
|
79
|
+
- `Components.supported` is derived from template directories, not a duplicated list
|
|
80
|
+
- Simplified `Detector` and `ComponentHelper`
|
|
81
|
+
- `view_primitives:add` exits with status 1 on unknown components; prints copy summary
|
|
82
|
+
- Requires `view_component >= 4.0` and Rails `>= 7.1`
|
|
83
|
+
|
|
84
|
+
[0.1.0]: https://github.com/alec-c4/view_primitives/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexey Poimtsev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# ViewPrimitives
|
|
2
|
+
|
|
3
|
+
A [shadcn/ui](https://ui.shadcn.com)-inspired component library for Rails built on [ViewComponent](https://viewcomponent.org).
|
|
4
|
+
|
|
5
|
+
> **Acknowledgements** — The visual design, CSS class choices, and component structure of ViewPrimitives are heavily inspired by [shadcn/ui](https://ui.shadcn.com) and its Svelte port [shadcn-svelte](https://www.shadcn-svelte.com). We are grateful to [@shadcn](https://github.com/shadcn) and all contributors for their outstanding open-source work. ViewPrimitives is an independent Rails adaptation and is not affiliated with or endorsed by the shadcn/ui project.
|
|
6
|
+
|
|
7
|
+
Components are **copied into your app** via a generator — not imported from a package. Tailwind classes live in your own files, so any Tailwind setup works out of the box: `tailwindcss-rails`, `cssbundling-rails`, Vite, esbuild — no configuration required.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Ruby >= 3.2 (developed with 4.0.5 — use [mise](https://mise.jdx.dev) or see `.ruby-version`)
|
|
12
|
+
- Rails >= 7.1 (required by ViewComponent 4)
|
|
13
|
+
- [ViewComponent](https://viewcomponent.org) >= 4.0
|
|
14
|
+
- [Tailwind CSS](https://tailwindcss.com) (any setup)
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add to your Gemfile:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem "view_primitives"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then run the install generator:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
rails g view_primitives:install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This will:
|
|
31
|
+
- Create `app/components/application_component.rb` with `ViewPrimitives::ClassHelper` included (skipped if you already have one — add `include ViewPrimitives::ClassHelper` manually)
|
|
32
|
+
- Create `app/assets/stylesheets/view_primitives.css` with the design token definitions (`@theme inline` + oklch light/dark theme)
|
|
33
|
+
|
|
34
|
+
Then import it in your Tailwind CSS entry point:
|
|
35
|
+
|
|
36
|
+
```css
|
|
37
|
+
/* tailwindcss-rails → app/assets/tailwind/application.css */
|
|
38
|
+
/* tailwind (legacy) → app/assets/stylesheets/application.tailwind.css */
|
|
39
|
+
/* cssbundling/Vite → app/javascript/application.css */
|
|
40
|
+
|
|
41
|
+
@import "./view_primitives";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The install generator auto-detects these entry points and injects the import when possible.
|
|
45
|
+
|
|
46
|
+
### UI namespace
|
|
47
|
+
|
|
48
|
+
Components live under `UI::` (files in `app/components/ui/`). The gem registers the `UI` acronym with ActiveSupport so `ui :button` resolves to `UI::ButtonComponent`.
|
|
49
|
+
|
|
50
|
+
That's it — no `tailwind.config.js` required. Tailwind 4 reads the `@theme inline` block directly from the CSS.
|
|
51
|
+
|
|
52
|
+
## Adding components
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
rails g view_primitives:list # available + installed status
|
|
56
|
+
rails g view_primitives:add button
|
|
57
|
+
rails g view_primitives:add button alert accordion # multiple at once
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Each component is copied into `app/components/ui/` as plain Ruby and ERB files you own and can modify freely. Re-running `add` overwrites existing files (a warning is printed). Unknown component names fail with a non-zero exit code.
|
|
61
|
+
|
|
62
|
+
## View helpers
|
|
63
|
+
|
|
64
|
+
ViewPrimitives adds the `ui` helper to views and mailers:
|
|
65
|
+
|
|
66
|
+
```erb
|
|
67
|
+
<%# Positional label — no block needed %>
|
|
68
|
+
<%= ui :button, "Save changes", variant: :outline %>
|
|
69
|
+
<%= ui :alert, title: "Heads up!", description: "Check your settings." %>
|
|
70
|
+
<%= ui :accordion, items: [{ title: "FAQ", content: "Answer here." }] %>
|
|
71
|
+
|
|
72
|
+
<%# Block — for icons, slots, or complex content %>
|
|
73
|
+
<%= ui :button do %><svg .../> Save<% end %>
|
|
74
|
+
<%= ui :alert do |a| %><% a.with_alert_title { "Note" } %><% end %>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`ui` is shorthand for `render UI::SomeComponent.new(...)`. For components outside `app/components/ui/`, use `render` as usual.
|
|
78
|
+
|
|
79
|
+
## Components
|
|
80
|
+
|
|
81
|
+
### Available now
|
|
82
|
+
|
|
83
|
+
| Component | Description | Docs |
|
|
84
|
+
|-----------|-------------|------|
|
|
85
|
+
| Button | Clickable element with 6 variants and 4 sizes | [docs](docs/components/button.md) |
|
|
86
|
+
| Alert | Informational banner with title and description slots | [docs](docs/components/alert.md) |
|
|
87
|
+
| Accordion | Collapsible sections via native `<details>`, optional exclusive mode | [docs](docs/components/accordion.md) |
|
|
88
|
+
| Badge | Small status label with variants | [docs](docs/components/badge.md) |
|
|
89
|
+
| Avatar | User avatar with image and initials fallback | [docs](docs/components/avatar.md) |
|
|
90
|
+
| Card | Container with header, content, and footer slots | [docs](docs/components/card.md) |
|
|
91
|
+
| Separator | Horizontal or vertical divider | [docs](docs/components/separator.md) |
|
|
92
|
+
| Label | Accessible form label | [docs](docs/components/label.md) |
|
|
93
|
+
| Skeleton | Loading placeholder with pulse animation | [docs](docs/components/skeleton.md) |
|
|
94
|
+
| Progress | Progress bar with value prop | [docs](docs/components/progress.md) |
|
|
95
|
+
| Aspect Ratio | Constrains child content to a given aspect ratio | [docs](docs/components/aspect_ratio.md) |
|
|
96
|
+
| Spinner | Animated loading indicator | [docs](docs/components/spinner.md) |
|
|
97
|
+
| KBD | Keyboard shortcut key display | [docs](docs/components/kbd.md) |
|
|
98
|
+
| Rating | Read-only star rating display | [docs](docs/components/rating.md) |
|
|
99
|
+
| Rating Input | Interactive star rating — form or AJAX submission | [docs](docs/components/rating_input.md) |
|
|
100
|
+
| Indicator | Status dot or count badge overlaid on an element | [docs](docs/components/indicator.md) |
|
|
101
|
+
| List Group | Bordered list with optional links and active state | [docs](docs/components/list_group.md) |
|
|
102
|
+
| Banner | Styled announcement strip with variants | [docs](docs/components/banner.md) |
|
|
103
|
+
| Button Group | Visually joined row of buttons | [docs](docs/components/button_group.md) |
|
|
104
|
+
| Input | Styled text input with ring/border | [docs](docs/components/input.md) |
|
|
105
|
+
| Textarea | Styled multi-line input | [docs](docs/components/textarea.md) |
|
|
106
|
+
| Checkbox | Accessible checkbox with optional label | [docs](docs/components/checkbox.md) |
|
|
107
|
+
| Radio Group | Group of radio inputs | [docs](docs/components/radio_group.md) |
|
|
108
|
+
| Select | Native styled select element | [docs](docs/components/select.md) |
|
|
109
|
+
| Switch | CSS-only on/off toggle | [docs](docs/components/switch.md) |
|
|
110
|
+
| Toggle | Single pressable toggle button | [docs](docs/components/toggle.md) |
|
|
111
|
+
| Toggle Group | Group of related toggles (single or multiple) | [docs](docs/components/toggle_group.md) |
|
|
112
|
+
| Form Field | Label + input + hint + error layout wrapper | [docs](docs/components/form_field.md) |
|
|
113
|
+
| File Input | Styled file upload input | [docs](docs/components/file_input.md) |
|
|
114
|
+
| Search Input | Text input with built-in search icon and clear button | [docs](docs/components/search_input.md) |
|
|
115
|
+
| Number Input | Text input with increment/decrement controls | [docs](docs/components/number_input.md) |
|
|
116
|
+
| Range | Styled range slider | [docs](docs/components/range.md) |
|
|
117
|
+
| Floating Label | Input with floating placeholder label | [docs](docs/components/floating_label.md) |
|
|
118
|
+
| Breadcrumb | Navigational breadcrumb trail with separator | [docs](docs/components/breadcrumb.md) |
|
|
119
|
+
| Pagination | Page number links with prev/next and ellipsis | [docs](docs/components/pagination.md) |
|
|
120
|
+
| Stepper | Multi-step progress indicator (horizontal + vertical) | [docs](docs/components/stepper.md) |
|
|
121
|
+
| Tabs | Tab bar with content panels (array API + slot API) | [docs](docs/components/tabs.md) |
|
|
122
|
+
| Navbar | Responsive top navigation bar with hamburger menu | [docs](docs/components/navbar.md) |
|
|
123
|
+
| Navigation Menu | Top-level navigation with optional dropdown flyouts | [docs](docs/components/navigation_menu.md) |
|
|
124
|
+
| Bottom Nav | Mobile-style tab bar fixed to the bottom | [docs](docs/components/bottom_nav.md) |
|
|
125
|
+
| Footer | Page footer with columns, links, and copyright | [docs](docs/components/footer.md) |
|
|
126
|
+
| Mega Menu | Full-width dropdown panel with grouped links and images | [docs](docs/components/mega_menu.md) |
|
|
127
|
+
| Dialog | Modal dialog with trigger, title, description, footer slots | [docs](docs/components/dialog.md) |
|
|
128
|
+
| Alert Dialog | Blocking confirmation dialog | [docs](docs/components/alert_dialog.md) |
|
|
129
|
+
| Sheet | Slide-in panel from any edge (left/right/top/bottom) | [docs](docs/components/sheet.md) |
|
|
130
|
+
| Drawer | Bottom sheet with drag handle — mobile drawer pattern | [docs](docs/components/drawer.md) |
|
|
131
|
+
| Popover | Floating panel anchored to a trigger | [docs](docs/components/popover.md) |
|
|
132
|
+
| Tooltip | Hover label — CSS-only, no JS | [docs](docs/components/tooltip.md) |
|
|
133
|
+
| Hover Card | Rich hover preview card — CSS-only, no JS | [docs](docs/components/hover_card.md) |
|
|
134
|
+
| Dropdown Menu | Trigger-anchored menu with items and separators | [docs](docs/components/dropdown_menu.md) |
|
|
135
|
+
| Context Menu | Right-click context menu positioned at cursor | [docs](docs/components/context_menu.md) |
|
|
136
|
+
| Menubar | Horizontal application-style menu bar | [docs](docs/components/menubar.md) |
|
|
137
|
+
| Command | Modal command palette with live search filtering | [docs](docs/components/command.md) |
|
|
138
|
+
| Combobox | Autocomplete select with live search | [docs](docs/components/combobox.md) |
|
|
139
|
+
| Calendar | Date picker calendar grid | [docs](docs/components/calendar.md) |
|
|
140
|
+
| Date Picker | Input that opens a Calendar popover | [docs](docs/components/date_picker.md) |
|
|
141
|
+
| Timepicker | Input for selecting a time value | [docs](docs/components/timepicker.md) |
|
|
142
|
+
| Carousel | Scrollable item carousel with prev/next controls | [docs](docs/components/carousel.md) |
|
|
143
|
+
| Data Table | Sortable, filterable table with pagination | [docs](docs/components/data_table.md) |
|
|
144
|
+
| Sidebar | Collapsible application sidebar with nav groups | [docs](docs/components/sidebar.md) |
|
|
145
|
+
| Input OTP | One-time-password digit input group | [docs](docs/components/input_otp.md) |
|
|
146
|
+
| Collapsible | Single collapsible section (simpler than Accordion) | [docs](docs/components/collapsible.md) |
|
|
147
|
+
| Resizable | Drag-to-resize panel layout | [docs](docs/components/resizable.md) |
|
|
148
|
+
| Scroll Area | Custom scrollbar container | [docs](docs/components/scroll_area.md) |
|
|
149
|
+
| Gallery | Responsive image grid with optional lightbox | [docs](docs/components/gallery.md) |
|
|
150
|
+
| Chat Bubble | Styled message bubble for chat or comment threads | [docs](docs/components/chat_bubble.md) |
|
|
151
|
+
| Speed Dial | Floating action button that expands into sub-actions | [docs](docs/components/speed_dial.md) |
|
|
152
|
+
| Device Mockup | Phone or browser frame for marketing screenshots | [docs](docs/components/device_mockup.md) |
|
|
153
|
+
| QR Code | QR code display from a given value | [docs](docs/components/qr_code.md) |
|
|
154
|
+
| Timeline | Vertical timeline with event items | [docs](docs/components/timeline.md) |
|
|
155
|
+
| Toaster | Stacked toast notifications (Sonner-style) | [docs](docs/components/toaster.md) |
|
|
156
|
+
| Chart | Chart.js adapter — bar, line, pie, doughnut, radar, polar area | [docs](docs/components/chart.md) |
|
|
157
|
+
| Picture | `<picture>` + `<source>` for art direction and modern formats (AVIF/WebP) | [docs](docs/components/picture.md) |
|
|
158
|
+
| Video | `<video>` + `<source>` with poster, controls, and caption tracks | [docs](docs/components/video.md) |
|
|
159
|
+
| Figure | `<figure>` + `<figcaption>` wrapper for media content | [docs](docs/components/figure.md) |
|
|
160
|
+
| Image | Responsive `<img>` with `srcset` / `sizes` | [docs](docs/components/image.md) |
|
|
161
|
+
| Audio | `<audio>` + `<source>` with optional transcript link | [docs](docs/components/audio.md) |
|
|
162
|
+
| Iframe | Sandboxed embed wrapper with required `title` and lazy loading | [docs](docs/components/iframe.md) |
|
|
163
|
+
| WYSIWYG | Rich-text editor — Trix (default) or Quill adapter | [docs](docs/components/wysiwyg.md) |
|
|
164
|
+
| Map / Area | Image map with clickable `<area>` regions | [docs](docs/components/map_area.md) |
|
|
165
|
+
| Embed | Third-party embeds — YouTube, Vimeo, Spotify, Google Maps, Yandex Maps, Loom, SoundCloud, X, Telegram, Facebook | [docs](docs/components/embed.md) |
|
|
166
|
+
|
|
167
|
+
See [ROADMAP.md](ROADMAP.md) for the full component list organised by phase.
|
|
168
|
+
|
|
169
|
+
## Customisation
|
|
170
|
+
|
|
171
|
+
See **[docs/customization.md](docs/customization.md)** for the full guide covering:
|
|
172
|
+
|
|
173
|
+
- Design tokens (OKLCH colors, radius) — change the whole palette in one file
|
|
174
|
+
- Editing component constants — add variants, change classes
|
|
175
|
+
- Per-instance `class:` overrides — append utilities without touching the file
|
|
176
|
+
- Full brand theming example
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
bin/setup # install dependencies
|
|
182
|
+
bundle exec rake # run tests + linter
|
|
183
|
+
bin/console # interactive prompt
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
To run tests against a specific Rails version:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
bundle exec appraisal rails-8.1 rake test
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Contributing
|
|
193
|
+
|
|
194
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/alec-c4/view_primitives.
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT License. See [LICENSE.txt](LICENSE.txt).
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../components"
|
|
4
|
+
require_relative "../detector"
|
|
5
|
+
|
|
6
|
+
module ViewPrimitives
|
|
7
|
+
module Generators
|
|
8
|
+
class AddGenerator < Rails::Generators::Base
|
|
9
|
+
include Detector
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
argument :components, type: :array
|
|
14
|
+
|
|
15
|
+
def copy_components
|
|
16
|
+
@copied = []
|
|
17
|
+
@unknown = []
|
|
18
|
+
|
|
19
|
+
components.each do |name|
|
|
20
|
+
if Components.supported.include?(name)
|
|
21
|
+
copy_component(name)
|
|
22
|
+
@copied << name
|
|
23
|
+
else
|
|
24
|
+
@unknown << name
|
|
25
|
+
say " Unknown component: #{name}. Supported: #{Components.supported.join(", ")}", :red
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def report_summary
|
|
31
|
+
say "" if @copied&.any? || @unknown&.any?
|
|
32
|
+
say " Copied: #{@copied.join(", ")}", :green if @copied&.any?
|
|
33
|
+
return if @unknown.blank?
|
|
34
|
+
|
|
35
|
+
say " Failed: #{@unknown.join(", ")} (unknown)", :red
|
|
36
|
+
say " Run `rails g view_primitives:list` to see all available components.", :cyan
|
|
37
|
+
abort
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def report_setup_notes
|
|
41
|
+
@copied&.each do |name|
|
|
42
|
+
note = Components::SETUP_NOTES[name]
|
|
43
|
+
next unless note
|
|
44
|
+
|
|
45
|
+
say ""
|
|
46
|
+
say " ── Setup required for #{name} ──────────────────────────", :yellow
|
|
47
|
+
note.each_line { |line| say " #{line.chomp}", :cyan }
|
|
48
|
+
say ""
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def template(source, *args, **options, &block)
|
|
53
|
+
destination = args.first || options[:to]
|
|
54
|
+
warn_overwrite(destination) if destination
|
|
55
|
+
super
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def copy_file(source, *args, **options)
|
|
59
|
+
destination = args.first || options[:to]
|
|
60
|
+
warn_overwrite(destination) if destination
|
|
61
|
+
super
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def copy_component(name)
|
|
67
|
+
dir = File.join(source_root, name)
|
|
68
|
+
Dir.each_child(dir).sort.each { |file| copy_template_file(name, file) }
|
|
69
|
+
copy_extra_stimulus(name)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def copy_template_file(component, file)
|
|
73
|
+
source = "#{component}/#{file}"
|
|
74
|
+
|
|
75
|
+
case file
|
|
76
|
+
when /\.rb\.tt\z/
|
|
77
|
+
template source, "app/components/ui/#{file.delete_suffix(".tt")}"
|
|
78
|
+
when /\.html\.erb\z/
|
|
79
|
+
copy_file source, "app/components/ui/#{file}"
|
|
80
|
+
when /_controller\.js\z/
|
|
81
|
+
copy_js_controller source, file.delete_suffix("_controller.js")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def copy_extra_stimulus(name)
|
|
86
|
+
config = Components::EXTRA_STIMULUS[name]
|
|
87
|
+
copy_js_controller(config[:source], config[:name]) if config
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def copy_js_controller(source, stimulus_name)
|
|
91
|
+
dir = js_controllers_dir
|
|
92
|
+
unless dir
|
|
93
|
+
say " Could not detect a JS controllers directory.", :yellow
|
|
94
|
+
say " Copy #{source} manually and register Stimulus `#{stimulus_name}`.", :cyan
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
dest = "#{dir}/#{stimulus_name}_controller.js"
|
|
99
|
+
copy_file source, dest
|
|
100
|
+
say " Stimulus `#{stimulus_name}` → #{dest}", :green
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def warn_overwrite(destination)
|
|
104
|
+
return unless File.exist?(File.join(destination_root, destination))
|
|
105
|
+
|
|
106
|
+
say " #{destination} already exists — overwriting.", :yellow
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<%= tag.div(class: "w-full", **wrapper_attrs) do %>
|
|
2
|
+
<% @items_data.each do |item| %>
|
|
3
|
+
<%= render UI::AccordionItemComponent.new(title: item[:title], open: item.fetch(:open, false)) do %>
|
|
4
|
+
<%= item[:content] %>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% items.each do |item| %>
|
|
8
|
+
<%= item %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% end %>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class AccordionComponent < ApplicationComponent
|
|
5
|
+
renders_many :items, "UI::AccordionItemComponent"
|
|
6
|
+
|
|
7
|
+
# items: array shorthand — each entry: { title:, content:, open: (optional) }
|
|
8
|
+
# exclusive: when true, opening one item closes all others via Stimulus
|
|
9
|
+
def initialize(items: nil, exclusive: false)
|
|
10
|
+
@items_data = Array(items)
|
|
11
|
+
@exclusive = exclusive
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def wrapper_attrs
|
|
17
|
+
return {} unless @exclusive
|
|
18
|
+
|
|
19
|
+
{ data: { controller: "accordion", action: "click->accordion#toggle" } }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
toggle(event) {
|
|
5
|
+
const summary = event.target.closest("summary")
|
|
6
|
+
if (!summary) return
|
|
7
|
+
|
|
8
|
+
const target = summary.closest("details")
|
|
9
|
+
if (!target || target.open) return
|
|
10
|
+
|
|
11
|
+
this.element.querySelectorAll("details[open]").forEach(item => {
|
|
12
|
+
if (item !== target) item.open = false
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class AccordionItemComponent < ApplicationComponent
|
|
5
|
+
SUMMARY_CLASSES = "flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium " \
|
|
6
|
+
"transition-all outline-none hover:underline cursor-pointer list-none " \
|
|
7
|
+
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " \
|
|
8
|
+
"disabled:pointer-events-none disabled:opacity-50"
|
|
9
|
+
|
|
10
|
+
def initialize(title:, open: false, **html_attrs)
|
|
11
|
+
@title = title
|
|
12
|
+
@open = open
|
|
13
|
+
@extra_class = html_attrs.delete(:class)
|
|
14
|
+
@html_attrs = html_attrs
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
content_tag(:details, details_content, **details_attrs)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def details_attrs
|
|
24
|
+
attrs = @html_attrs.merge(class: cn("border-b last:border-b-0 group", @extra_class))
|
|
25
|
+
attrs[:open] = true if @open
|
|
26
|
+
attrs
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def details_content
|
|
30
|
+
safe_join([
|
|
31
|
+
content_tag(:summary, summary_content, class: SUMMARY_CLASSES),
|
|
32
|
+
content_tag(:div, content, class: "pb-4 pt-0 text-sm")
|
|
33
|
+
])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def summary_content
|
|
37
|
+
safe_join([
|
|
38
|
+
@title,
|
|
39
|
+
content_tag(:svg, content_tag(:path, nil, d: "m6 9 6 6 6-6"),
|
|
40
|
+
xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24",
|
|
41
|
+
fill: "none", stroke: "currentColor", stroke_width: "2",
|
|
42
|
+
stroke_linecap: "round", stroke_linejoin: "round",
|
|
43
|
+
class: "pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200 group-open:rotate-180")
|
|
44
|
+
])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class AlertComponent < ApplicationComponent
|
|
5
|
+
OUTER_CLASSES = "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border " \
|
|
6
|
+
"px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] " \
|
|
7
|
+
"has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current"
|
|
8
|
+
|
|
9
|
+
VARIANTS = {
|
|
10
|
+
default: "bg-card text-card-foreground",
|
|
11
|
+
destructive: "bg-card text-destructive [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90"
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
renders_one :alert_title, "UI::AlertComponent::TitleComponent"
|
|
15
|
+
renders_one :alert_description, "UI::AlertComponent::DescriptionComponent"
|
|
16
|
+
|
|
17
|
+
# title: and description: are kwargs shorthands for plain-text content.
|
|
18
|
+
# Use slots (with_alert_title / with_alert_description) for rich HTML content.
|
|
19
|
+
# Slots take precedence over kwargs when both are provided.
|
|
20
|
+
def initialize(variant: :default, title: nil, description: nil)
|
|
21
|
+
@variant = variant.to_sym
|
|
22
|
+
@title = title
|
|
23
|
+
@description = description
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call
|
|
27
|
+
content_tag(:div, safe_join([resolved_title, resolved_description].compact),
|
|
28
|
+
role: "alert",
|
|
29
|
+
class: cn(OUTER_CLASSES, VARIANTS.fetch(@variant, VARIANTS[:default])))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class TitleComponent < ApplicationComponent
|
|
33
|
+
CLASSES = "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight"
|
|
34
|
+
|
|
35
|
+
def call
|
|
36
|
+
content_tag(:h5, content, class: CLASSES)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class DescriptionComponent < ApplicationComponent
|
|
41
|
+
CLASSES = "col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed"
|
|
42
|
+
|
|
43
|
+
def call
|
|
44
|
+
content_tag(:div, content, class: CLASSES, data: { slot: "alert-description" })
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def resolved_title
|
|
51
|
+
alert_title || (@title && content_tag(:h5, @title, class: TitleComponent::CLASSES))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def resolved_description
|
|
55
|
+
return alert_description if alert_description
|
|
56
|
+
|
|
57
|
+
@description && content_tag(:div, @description,
|
|
58
|
+
class: DescriptionComponent::CLASSES,
|
|
59
|
+
data: { slot: "alert-description" })
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class AlertDialogComponent < ApplicationComponent
|
|
5
|
+
renders_one :trigger
|
|
6
|
+
renders_one :footer
|
|
7
|
+
|
|
8
|
+
OVERLAY = "fixed inset-0 z-50 bg-black/80"
|
|
9
|
+
PANEL = "fixed left-[50%] top-[50%] z-50 w-full max-w-lg " \
|
|
10
|
+
"translate-x-[-50%] translate-y-[-50%] " \
|
|
11
|
+
"rounded-lg border bg-background p-6 shadow-lg"
|
|
12
|
+
|
|
13
|
+
def initialize(title: nil, description: nil, **html_attrs)
|
|
14
|
+
@title = title
|
|
15
|
+
@description = description
|
|
16
|
+
@extra_class = html_attrs.delete(:class)
|
|
17
|
+
@html_attrs = html_attrs
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
content_tag(:div, data: { controller: "dialog" }, **@html_attrs) do
|
|
22
|
+
concat content_tag(:span, trigger, data: { action: "click->dialog#open" }, class: "contents") if trigger
|
|
23
|
+
concat panel
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def panel
|
|
30
|
+
content_tag(:div, data: { dialog_target: "panel" }, hidden: true) do
|
|
31
|
+
concat content_tag(:div, nil,
|
|
32
|
+
class: OVERLAY,
|
|
33
|
+
"aria-hidden": "true")
|
|
34
|
+
concat content_tag(:div,
|
|
35
|
+
class: cn(PANEL, @extra_class),
|
|
36
|
+
role: "alertdialog",
|
|
37
|
+
"aria-modal": "true",
|
|
38
|
+
"aria-label": @title) {
|
|
39
|
+
concat header_area
|
|
40
|
+
concat content_tag(:div, content, class: "py-1 text-sm text-muted-foreground") unless content.blank?
|
|
41
|
+
concat content_tag(:div, footer, class: "mt-6 flex justify-end gap-2") if footer
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def header_area
|
|
47
|
+
return "" if @title.nil? && @description.nil?
|
|
48
|
+
|
|
49
|
+
content_tag(:div, class: "mb-4") do
|
|
50
|
+
concat content_tag(:h2, @title, class: "text-lg font-semibold leading-none tracking-tight") if @title
|
|
51
|
+
concat content_tag(:p, @description, class: "mt-2 text-sm text-muted-foreground") if @description
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class AspectRatioComponent < ApplicationComponent
|
|
5
|
+
def initialize(ratio: 1, **html_attrs)
|
|
6
|
+
@ratio = ratio
|
|
7
|
+
@extra_class = html_attrs.delete(:class)
|
|
8
|
+
@html_attrs = html_attrs
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
content_tag(:div, content,
|
|
13
|
+
style: "aspect-ratio: #{@ratio}",
|
|
14
|
+
class: cn("overflow-hidden", @extra_class),
|
|
15
|
+
**@html_attrs)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|