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.
Files changed (140) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +84 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +198 -0
  5. data/lib/generators/view_primitives/add/add_generator.rb +110 -0
  6. data/lib/generators/view_primitives/add/templates/accordion/accordion_component.html.erb +10 -0
  7. data/lib/generators/view_primitives/add/templates/accordion/accordion_component.rb.tt +22 -0
  8. data/lib/generators/view_primitives/add/templates/accordion/accordion_controller.js +15 -0
  9. data/lib/generators/view_primitives/add/templates/accordion/accordion_item_component.rb.tt +47 -0
  10. data/lib/generators/view_primitives/add/templates/alert/alert_component.rb.tt +62 -0
  11. data/lib/generators/view_primitives/add/templates/alert_dialog/alert_dialog_component.rb.tt +55 -0
  12. data/lib/generators/view_primitives/add/templates/aspect_ratio/aspect_ratio_component.rb.tt +18 -0
  13. data/lib/generators/view_primitives/add/templates/audio/audio_component.rb.tt +51 -0
  14. data/lib/generators/view_primitives/add/templates/avatar/avatar_component.rb.tt +37 -0
  15. data/lib/generators/view_primitives/add/templates/badge/badge_component.rb.tt +35 -0
  16. data/lib/generators/view_primitives/add/templates/banner/banner_component.rb.tt +29 -0
  17. data/lib/generators/view_primitives/add/templates/bottom_nav/bottom_nav_component.rb.tt +38 -0
  18. data/lib/generators/view_primitives/add/templates/breadcrumb/breadcrumb_component.rb.tt +37 -0
  19. data/lib/generators/view_primitives/add/templates/button/button_component.rb.tt +61 -0
  20. data/lib/generators/view_primitives/add/templates/button_group/button_group_component.rb.tt +23 -0
  21. data/lib/generators/view_primitives/add/templates/calendar/calendar_component.rb.tt +121 -0
  22. data/lib/generators/view_primitives/add/templates/calendar/calendar_controller.js +86 -0
  23. data/lib/generators/view_primitives/add/templates/card/card_component.rb.tt +16 -0
  24. data/lib/generators/view_primitives/add/templates/card/card_content_component.rb.tt +16 -0
  25. data/lib/generators/view_primitives/add/templates/card/card_description_component.rb.tt +17 -0
  26. data/lib/generators/view_primitives/add/templates/card/card_footer_component.rb.tt +16 -0
  27. data/lib/generators/view_primitives/add/templates/card/card_header_component.rb.tt +17 -0
  28. data/lib/generators/view_primitives/add/templates/card/card_title_component.rb.tt +17 -0
  29. data/lib/generators/view_primitives/add/templates/carousel/carousel_component.rb.tt +102 -0
  30. data/lib/generators/view_primitives/add/templates/carousel/carousel_controller.js +48 -0
  31. data/lib/generators/view_primitives/add/templates/chart/chart_component.rb.tt +63 -0
  32. data/lib/generators/view_primitives/add/templates/chart/chart_controller.js +29 -0
  33. data/lib/generators/view_primitives/add/templates/chat_bubble/chat_bubble_component.rb.tt +53 -0
  34. data/lib/generators/view_primitives/add/templates/checkbox/checkbox_component.rb.tt +50 -0
  35. data/lib/generators/view_primitives/add/templates/collapsible/collapsible_component.rb.tt +31 -0
  36. data/lib/generators/view_primitives/add/templates/combobox/combobox_component.rb.tt +87 -0
  37. data/lib/generators/view_primitives/add/templates/combobox/combobox_controller.js +38 -0
  38. data/lib/generators/view_primitives/add/templates/command/command_component.rb.tt +85 -0
  39. data/lib/generators/view_primitives/add/templates/command/command_controller.js +50 -0
  40. data/lib/generators/view_primitives/add/templates/context_menu/context_menu_component.rb.tt +47 -0
  41. data/lib/generators/view_primitives/add/templates/context_menu/context_menu_controller.js +20 -0
  42. data/lib/generators/view_primitives/add/templates/data_table/data_table_component.rb.tt +163 -0
  43. data/lib/generators/view_primitives/add/templates/data_table/data_table_controller.js +115 -0
  44. data/lib/generators/view_primitives/add/templates/date_picker/date_picker_component.rb.tt +92 -0
  45. data/lib/generators/view_primitives/add/templates/date_picker/date_picker_controller.js +48 -0
  46. data/lib/generators/view_primitives/add/templates/device_mockup/device_mockup_component.rb.tt +65 -0
  47. data/lib/generators/view_primitives/add/templates/dialog/dialog_component.rb.tt +71 -0
  48. data/lib/generators/view_primitives/add/templates/dialog/dialog_controller.js +15 -0
  49. data/lib/generators/view_primitives/add/templates/drawer/drawer_component.rb.tt +62 -0
  50. data/lib/generators/view_primitives/add/templates/drawer/drawer_controller.js +15 -0
  51. data/lib/generators/view_primitives/add/templates/dropdown_menu/dropdown_controller.js +17 -0
  52. data/lib/generators/view_primitives/add/templates/dropdown_menu/dropdown_menu_component.rb.tt +53 -0
  53. data/lib/generators/view_primitives/add/templates/embed/embed_component.rb.tt +271 -0
  54. data/lib/generators/view_primitives/add/templates/embed/embed_controller.js +43 -0
  55. data/lib/generators/view_primitives/add/templates/figure/figure_component.rb.tt +24 -0
  56. data/lib/generators/view_primitives/add/templates/file_input/file_input_component.rb.tt +31 -0
  57. data/lib/generators/view_primitives/add/templates/floating_label/floating_label_component.rb.tt +54 -0
  58. data/lib/generators/view_primitives/add/templates/footer/footer_component.rb.tt +51 -0
  59. data/lib/generators/view_primitives/add/templates/form_field/form_field_component.rb.tt +51 -0
  60. data/lib/generators/view_primitives/add/templates/gallery/gallery_component.rb.tt +83 -0
  61. data/lib/generators/view_primitives/add/templates/gallery/gallery_controller.js +28 -0
  62. data/lib/generators/view_primitives/add/templates/hover_card/hover_card_component.rb.tt +43 -0
  63. data/lib/generators/view_primitives/add/templates/iframe/iframe_component.rb.tt +59 -0
  64. data/lib/generators/view_primitives/add/templates/image/image_component.rb.tt +38 -0
  65. data/lib/generators/view_primitives/add/templates/indicator/indicator_component.rb.tt +46 -0
  66. data/lib/generators/view_primitives/add/templates/input/input_component.rb.tt +28 -0
  67. data/lib/generators/view_primitives/add/templates/input_otp/input_otp_component.rb.tt +65 -0
  68. data/lib/generators/view_primitives/add/templates/input_otp/input_otp_controller.js +39 -0
  69. data/lib/generators/view_primitives/add/templates/kbd/kbd_component.rb.tt +21 -0
  70. data/lib/generators/view_primitives/add/templates/label/label_component.rb.tt +23 -0
  71. data/lib/generators/view_primitives/add/templates/list_group/list_group_component.rb.tt +16 -0
  72. data/lib/generators/view_primitives/add/templates/list_group/list_group_item_component.rb.tt +31 -0
  73. data/lib/generators/view_primitives/add/templates/map_area/map_area_component.rb.tt +72 -0
  74. data/lib/generators/view_primitives/add/templates/mega_menu/mega_menu_component.rb.tt +130 -0
  75. data/lib/generators/view_primitives/add/templates/mega_menu/mega_menu_controller.js +23 -0
  76. data/lib/generators/view_primitives/add/templates/menubar/menubar_component.rb.tt +33 -0
  77. data/lib/generators/view_primitives/add/templates/menubar/menubar_controller.js +34 -0
  78. data/lib/generators/view_primitives/add/templates/menubar/menubar_menu_component.rb.tt +34 -0
  79. data/lib/generators/view_primitives/add/templates/navbar/navbar_component.rb.tt +90 -0
  80. data/lib/generators/view_primitives/add/templates/navbar/navbar_controller.js +11 -0
  81. data/lib/generators/view_primitives/add/templates/navigation_menu/navigation_menu_component.rb.tt +132 -0
  82. data/lib/generators/view_primitives/add/templates/navigation_menu/navigation_menu_controller.js +25 -0
  83. data/lib/generators/view_primitives/add/templates/number_input/number_input_component.rb.tt +34 -0
  84. data/lib/generators/view_primitives/add/templates/pagination/pagination_component.rb.tt +97 -0
  85. data/lib/generators/view_primitives/add/templates/picture/picture_component.rb.tt +63 -0
  86. data/lib/generators/view_primitives/add/templates/popover/popover_component.rb.tt +56 -0
  87. data/lib/generators/view_primitives/add/templates/popover/popover_controller.js +17 -0
  88. data/lib/generators/view_primitives/add/templates/progress/progress_component.rb.tt +28 -0
  89. data/lib/generators/view_primitives/add/templates/qr_code/qr_code_component.rb.tt +39 -0
  90. data/lib/generators/view_primitives/add/templates/radio_group/radio_group_component.rb.tt +51 -0
  91. data/lib/generators/view_primitives/add/templates/range/range_component.rb.tt +40 -0
  92. data/lib/generators/view_primitives/add/templates/rating/rating_component.rb.tt +42 -0
  93. data/lib/generators/view_primitives/add/templates/rating_input/rating_controller.js +47 -0
  94. data/lib/generators/view_primitives/add/templates/rating_input/rating_input_component.rb.tt +79 -0
  95. data/lib/generators/view_primitives/add/templates/resizable/resizable_component.rb.tt +91 -0
  96. data/lib/generators/view_primitives/add/templates/resizable/resizable_controller.js +38 -0
  97. data/lib/generators/view_primitives/add/templates/scroll_area/scroll_area_component.rb.tt +41 -0
  98. data/lib/generators/view_primitives/add/templates/search_input/search_input_component.rb.tt +50 -0
  99. data/lib/generators/view_primitives/add/templates/select/select_component.rb.tt +45 -0
  100. data/lib/generators/view_primitives/add/templates/separator/separator_component.rb.tt +25 -0
  101. data/lib/generators/view_primitives/add/templates/sheet/sheet_component.rb.tt +78 -0
  102. data/lib/generators/view_primitives/add/templates/sheet/sheet_controller.js +15 -0
  103. data/lib/generators/view_primitives/add/templates/sidebar/sidebar_component.rb.tt +169 -0
  104. data/lib/generators/view_primitives/add/templates/sidebar/sidebar_controller.js +11 -0
  105. data/lib/generators/view_primitives/add/templates/skeleton/skeleton_component.rb.tt +16 -0
  106. data/lib/generators/view_primitives/add/templates/speed_dial/speed_dial_component.rb.tt +111 -0
  107. data/lib/generators/view_primitives/add/templates/speed_dial/speed_dial_controller.js +22 -0
  108. data/lib/generators/view_primitives/add/templates/spinner/spinner_component.rb.tt +27 -0
  109. data/lib/generators/view_primitives/add/templates/stepper/stepper_component.rb.tt +99 -0
  110. data/lib/generators/view_primitives/add/templates/switch/switch_component.rb.tt +51 -0
  111. data/lib/generators/view_primitives/add/templates/tabs/tabs_component.html.erb +50 -0
  112. data/lib/generators/view_primitives/add/templates/tabs/tabs_component.rb.tt +16 -0
  113. data/lib/generators/view_primitives/add/templates/tabs/tabs_controller.js +26 -0
  114. data/lib/generators/view_primitives/add/templates/tabs/tabs_item_component.rb.tt +15 -0
  115. data/lib/generators/view_primitives/add/templates/textarea/textarea_component.rb.tt +24 -0
  116. data/lib/generators/view_primitives/add/templates/timeline/timeline_component.rb.tt +78 -0
  117. data/lib/generators/view_primitives/add/templates/timepicker/timepicker_component.rb.tt +140 -0
  118. data/lib/generators/view_primitives/add/templates/timepicker/timepicker_controller.js +92 -0
  119. data/lib/generators/view_primitives/add/templates/toaster/toaster_component.rb.tt +152 -0
  120. data/lib/generators/view_primitives/add/templates/toaster/toaster_controller.js +88 -0
  121. data/lib/generators/view_primitives/add/templates/toggle/toggle_component.rb.tt +41 -0
  122. data/lib/generators/view_primitives/add/templates/toggle/toggle_controller.js +12 -0
  123. data/lib/generators/view_primitives/add/templates/toggle_group/toggle_group_component.rb.tt +32 -0
  124. data/lib/generators/view_primitives/add/templates/toggle_group/toggle_group_controller.js +38 -0
  125. data/lib/generators/view_primitives/add/templates/tooltip/tooltip_component.rb.tt +42 -0
  126. data/lib/generators/view_primitives/add/templates/video/video_component.rb.tt +92 -0
  127. data/lib/generators/view_primitives/add/templates/wysiwyg/wysiwyg_component.rb.tt +88 -0
  128. data/lib/generators/view_primitives/add/templates/wysiwyg/wysiwyg_controller.js +40 -0
  129. data/lib/generators/view_primitives/components.rb +62 -0
  130. data/lib/generators/view_primitives/detector.rb +43 -0
  131. data/lib/generators/view_primitives/install/install_generator.rb +65 -0
  132. data/lib/generators/view_primitives/install/templates/application_component.rb.tt +5 -0
  133. data/lib/generators/view_primitives/install/templates/view_primitives.css +67 -0
  134. data/lib/generators/view_primitives/list/list_generator.rb +25 -0
  135. data/lib/view_primitives/class_helper.rb +11 -0
  136. data/lib/view_primitives/component_helper.rb +20 -0
  137. data/lib/view_primitives/railtie.rb +21 -0
  138. data/lib/view_primitives/version.rb +5 -0
  139. data/lib/view_primitives.rb +12 -0
  140. 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