shadcn_view_components 0.2.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 +590 -0
- data/LICENSE +49 -0
- data/README.md +542 -0
- data/app/assets/javascripts/shadcn/aria_relationships.js +88 -0
- data/app/assets/javascripts/shadcn/controllers/accordion_controller.js +241 -0
- data/app/assets/javascripts/shadcn/controllers/calendar_controller.js +98 -0
- data/app/assets/javascripts/shadcn/controllers/carousel_controller.js +305 -0
- data/app/assets/javascripts/shadcn/controllers/checked_state_controller.js +121 -0
- data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +613 -0
- data/app/assets/javascripts/shadcn/controllers/command_controller.js +87 -0
- data/app/assets/javascripts/shadcn/controllers/dialog_controller.js +208 -0
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +145 -0
- data/app/assets/javascripts/shadcn/controllers/input_otp_controller.js +416 -0
- data/app/assets/javascripts/shadcn/controllers/menu_controller.js +286 -0
- data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +430 -0
- data/app/assets/javascripts/shadcn/controllers/message_scroller_controller.js +37 -0
- data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +254 -0
- data/app/assets/javascripts/shadcn/controllers/popover_controller.js +117 -0
- data/app/assets/javascripts/shadcn/controllers/resizable_controller.js +256 -0
- data/app/assets/javascripts/shadcn/controllers/select_controller.js +491 -0
- data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +30 -0
- data/app/assets/javascripts/shadcn/controllers/slider_controller.js +74 -0
- data/app/assets/javascripts/shadcn/controllers/tabs_controller.js +146 -0
- data/app/assets/javascripts/shadcn/controllers/toast_controller.js +81 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_controller.js +21 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.js +37 -0
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +123 -0
- data/app/assets/javascripts/shadcn/floating_position.js +660 -0
- data/app/assets/javascripts/shadcn/hide_after_exit.js +53 -0
- data/app/assets/javascripts/shadcn/index.js +65 -0
- data/app/assets/javascripts/shadcn/menu_popover.js +352 -0
- data/app/assets/javascripts/shadcn/package.json +17 -0
- data/app/assets/javascripts/shadcn/state_attrs.js +13 -0
- data/app/assets/stylesheets/shadcn/shadcn.css +766 -0
- data/app/assets/tailwind/shadcn_view_components/engine.css +11 -0
- data/app/components/shadcn/accordion.rb +121 -0
- data/app/components/shadcn/alert.rb +31 -0
- data/app/components/shadcn/alert_dialog.rb +165 -0
- data/app/components/shadcn/aspect_ratio.rb +34 -0
- data/app/components/shadcn/attachment.rb +86 -0
- data/app/components/shadcn/avatar.rb +55 -0
- data/app/components/shadcn/badge.rb +32 -0
- data/app/components/shadcn/base_component.rb +239 -0
- data/app/components/shadcn/breadcrumb.rb +70 -0
- data/app/components/shadcn/bubble.rb +48 -0
- data/app/components/shadcn/button.html.erb +1 -0
- data/app/components/shadcn/button.rb +41 -0
- data/app/components/shadcn/button_group.rb +41 -0
- data/app/components/shadcn/button_styled.rb +51 -0
- data/app/components/shadcn/calendar.rb +276 -0
- data/app/components/shadcn/card.rb +18 -0
- data/app/components/shadcn/carousel.rb +236 -0
- data/app/components/shadcn/chart.rb +57 -0
- data/app/components/shadcn/checkbox.rb +64 -0
- data/app/components/shadcn/collapsible.rb +25 -0
- data/app/components/shadcn/combobox.rb +552 -0
- data/app/components/shadcn/command.rb +149 -0
- data/app/components/shadcn/context_menu.rb +79 -0
- data/app/components/shadcn/dialog.rb +180 -0
- data/app/components/shadcn/direction_provider.rb +18 -0
- data/app/components/shadcn/drawer.rb +150 -0
- data/app/components/shadcn/dropdown_menu.rb +242 -0
- data/app/components/shadcn/empty.rb +40 -0
- data/app/components/shadcn/field.rb +106 -0
- data/app/components/shadcn/floating_position_options.rb +132 -0
- data/app/components/shadcn/form.rb +105 -0
- data/app/components/shadcn/hover_card.rb +66 -0
- data/app/components/shadcn/input.rb +13 -0
- data/app/components/shadcn/input_group/button.rb +48 -0
- data/app/components/shadcn/input_group.rb +51 -0
- data/app/components/shadcn/input_otp.rb +265 -0
- data/app/components/shadcn/item.rb +108 -0
- data/app/components/shadcn/kbd.rb +9 -0
- data/app/components/shadcn/label.rb +12 -0
- data/app/components/shadcn/marker.rb +37 -0
- data/app/components/shadcn/menubar.rb +110 -0
- data/app/components/shadcn/message.rb +29 -0
- data/app/components/shadcn/message_scroller.rb +52 -0
- data/app/components/shadcn/native_checked_state.rb +74 -0
- data/app/components/shadcn/native_select.rb +78 -0
- data/app/components/shadcn/navigation_menu.rb +122 -0
- data/app/components/shadcn/pagination.rb +160 -0
- data/app/components/shadcn/popover.rb +68 -0
- data/app/components/shadcn/preview_base.rb +107 -0
- data/app/components/shadcn/progress.rb +49 -0
- data/app/components/shadcn/radio_group.rb +74 -0
- data/app/components/shadcn/resizable.rb +77 -0
- data/app/components/shadcn/scroll_area.rb +59 -0
- data/app/components/shadcn/select.rb +356 -0
- data/app/components/shadcn/separator.rb +42 -0
- data/app/components/shadcn/sheet.rb +157 -0
- data/app/components/shadcn/sidebar.rb +332 -0
- data/app/components/shadcn/skeleton.rb +13 -0
- data/app/components/shadcn/slider.rb +279 -0
- data/app/components/shadcn/sonner.rb +30 -0
- data/app/components/shadcn/spinner.rb +32 -0
- data/app/components/shadcn/switch.rb +79 -0
- data/app/components/shadcn/table.rb +46 -0
- data/app/components/shadcn/tabs.rb +119 -0
- data/app/components/shadcn/textarea.rb +7 -0
- data/app/components/shadcn/toggle.rb +66 -0
- data/app/components/shadcn/toggle_group.rb +127 -0
- data/app/components/shadcn/tooltip.rb +74 -0
- data/config/importmap.rb +5 -0
- data/lib/generators/shadcn_view_components/install_generator.rb +182 -0
- data/lib/shadcn_view_components/classes.rb +89 -0
- data/lib/shadcn_view_components/component_naming.rb +21 -0
- data/lib/shadcn_view_components/contracts/calendar.rb +95 -0
- data/lib/shadcn_view_components/engine.rb +56 -0
- data/lib/shadcn_view_components/generated/contracts/accordion.rb +112 -0
- data/lib/shadcn_view_components/generated/contracts/alert-dialog.rb +300 -0
- data/lib/shadcn_view_components/generated/contracts/alert.rb +104 -0
- data/lib/shadcn_view_components/generated/contracts/aspect-ratio.rb +34 -0
- data/lib/shadcn_view_components/generated/contracts/attachment.rb +224 -0
- data/lib/shadcn_view_components/generated/contracts/avatar.rb +149 -0
- data/lib/shadcn_view_components/generated/contracts/badge.rb +39 -0
- data/lib/shadcn_view_components/generated/contracts/breadcrumb.rb +172 -0
- data/lib/shadcn_view_components/generated/contracts/bubble.rb +112 -0
- data/lib/shadcn_view_components/generated/contracts/button-group.rb +81 -0
- data/lib/shadcn_view_components/generated/contracts/button.rb +81 -0
- data/lib/shadcn_view_components/generated/contracts/card.rb +172 -0
- data/lib/shadcn_view_components/generated/contracts/carousel.rb +133 -0
- data/lib/shadcn_view_components/generated/contracts/chart.rb +109 -0
- data/lib/shadcn_view_components/generated/contracts/checkbox.rb +35 -0
- data/lib/shadcn_view_components/generated/contracts/collapsible.rb +89 -0
- data/lib/shadcn_view_components/generated/contracts/combobox.rb +368 -0
- data/lib/shadcn_view_components/generated/contracts/command.rb +222 -0
- data/lib/shadcn_view_components/generated/contracts/context-menu.rb +378 -0
- data/lib/shadcn_view_components/generated/contracts/dialog.rb +258 -0
- data/lib/shadcn_view_components/generated/contracts/direction.rb +34 -0
- data/lib/shadcn_view_components/generated/contracts/drawer.rb +280 -0
- data/lib/shadcn_view_components/generated/contracts/dropdown-menu.rb +380 -0
- data/lib/shadcn_view_components/generated/contracts/empty.rb +150 -0
- data/lib/shadcn_view_components/generated/contracts/field.rb +244 -0
- data/lib/shadcn_view_components/generated/contracts/form.rb +62 -0
- data/lib/shadcn_view_components/generated/contracts/hover-card.rb +90 -0
- data/lib/shadcn_view_components/generated/contracts/input-group.rb +155 -0
- data/lib/shadcn_view_components/generated/contracts/input-otp.rb +106 -0
- data/lib/shadcn_view_components/generated/contracts/input.rb +34 -0
- data/lib/shadcn_view_components/generated/contracts/item.rb +251 -0
- data/lib/shadcn_view_components/generated/contracts/kbd.rb +57 -0
- data/lib/shadcn_view_components/generated/contracts/label.rb +34 -0
- data/lib/shadcn_view_components/generated/contracts/marker.rb +82 -0
- data/lib/shadcn_view_components/generated/contracts/menubar.rb +394 -0
- data/lib/shadcn_view_components/generated/contracts/message-scroller.rb +149 -0
- data/lib/shadcn_view_components/generated/contracts/message.rb +149 -0
- data/lib/shadcn_view_components/generated/contracts/native-select.rb +82 -0
- data/lib/shadcn_view_components/generated/contracts/navigation-menu.rb +195 -0
- data/lib/shadcn_view_components/generated/contracts/pagination.rb +176 -0
- data/lib/shadcn_view_components/generated/contracts/popover.rb +159 -0
- data/lib/shadcn_view_components/generated/contracts/progress.rb +126 -0
- data/lib/shadcn_view_components/generated/contracts/radio-group.rb +58 -0
- data/lib/shadcn_view_components/generated/contracts/resizable.rb +85 -0
- data/lib/shadcn_view_components/generated/contracts/scroll-area.rb +59 -0
- data/lib/shadcn_view_components/generated/contracts/select.rb +245 -0
- data/lib/shadcn_view_components/generated/contracts/separator.rb +34 -0
- data/lib/shadcn_view_components/generated/contracts/sheet.rb +209 -0
- data/lib/shadcn_view_components/generated/contracts/sidebar.rb +558 -0
- data/lib/shadcn_view_components/generated/contracts/skeleton.rb +34 -0
- data/lib/shadcn_view_components/generated/contracts/slider.rb +38 -0
- data/lib/shadcn_view_components/generated/contracts/sonner.rb +36 -0
- data/lib/shadcn_view_components/generated/contracts/spinner.rb +34 -0
- data/lib/shadcn_view_components/generated/contracts/switch.rb +35 -0
- data/lib/shadcn_view_components/generated/contracts/table.rb +199 -0
- data/lib/shadcn_view_components/generated/contracts/tabs.rb +104 -0
- data/lib/shadcn_view_components/generated/contracts/textarea.rb +34 -0
- data/lib/shadcn_view_components/generated/contracts/toggle-group.rb +62 -0
- data/lib/shadcn_view_components/generated/contracts/toggle.rb +39 -0
- data/lib/shadcn_view_components/generated/contracts/tooltip.rb +116 -0
- data/lib/shadcn_view_components/numeric_property_validation.rb +124 -0
- data/lib/shadcn_view_components/property_contract_definitions.rb +92 -0
- data/lib/shadcn_view_components/property_contracts.rb +42 -0
- data/lib/shadcn_view_components/property_validation.rb +55 -0
- data/lib/shadcn_view_components/version.rb +6 -0
- data/lib/shadcn_view_components.rb +21 -0
- metadata +446 -0
data/LICENSE
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 supermomonga
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This distribution includes styles and component contracts derived from shadcn/ui.
|
|
26
|
+
Upstream source: https://github.com/shadcn-ui/ui
|
|
27
|
+
License source: https://github.com/shadcn-ui/ui/blob/1773ecfeeb4a04366978d353e69b5c7ded78dcb2/LICENSE.md
|
|
28
|
+
|
|
29
|
+
MIT License
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2023 shadcn
|
|
32
|
+
|
|
33
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
34
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
35
|
+
in the Software without restriction, including without limitation the rights
|
|
36
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
37
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
38
|
+
furnished to do so, subject to the following conditions:
|
|
39
|
+
|
|
40
|
+
The above copyright notice and this permission notice shall be included in all
|
|
41
|
+
copies or substantial portions of the Software.
|
|
42
|
+
|
|
43
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
44
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
45
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
46
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
47
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
48
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
49
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
# shadcn_view_components
|
|
2
|
+
|
|
3
|
+
[shadcn/ui](https://ui.shadcn.com/)(base-nova スタイル)を [Rails ViewComponent](https://viewcomponent.org/) + Stimulus として移植する Rails エンジンgem。
|
|
4
|
+
|
|
5
|
+
単なる一回の移植ではなく、**shadcn/ui のバージョンアップへの追従コストを最小化する**ことを第一の設計目標とする:
|
|
6
|
+
|
|
7
|
+
1. **決定論的な生成パイプライン** — upstream レジストリから機械的に導出できるもの(クラス文字列・バリアント定義・`data-slot` 構造・CSS変数)はすべて自動抽出・自動生成し、コミットされた生成物として扱う
|
|
8
|
+
2. **自動的な乖離検知** — upstream が変わった際に、適合試験(conformance tests)が互換性の崩れを自動検出する
|
|
9
|
+
3. **型付きコードベース** — `app/`・`lib/`をSorbet(`typed: strict`)で検査
|
|
10
|
+
|
|
11
|
+
クライアントサイドのふるまいは React/Radix を持ち込まず、**Stimulus + Hotwire + ネイティブHTML要素**による Rails 流の再実装。クラス名・`data-slot`・ARIA属性といった「見た目と構造の契約」は upstream 由来の生成物として維持されるため、視覚的な追従は自動化される。
|
|
12
|
+
|
|
13
|
+
設計の詳細は[ドキュメント案内](https://github.com/supermomonga/shadcn_view_components/blob/main/docs/reference/README.md)を参照。
|
|
14
|
+
|
|
15
|
+
## ステータス
|
|
16
|
+
|
|
17
|
+
Phase 0〜4 完了 — vendor manifestの63アイテムを追跡し、61アイテムを実装済み。`questionnaire`と`toast`は[理由付き非対応](https://github.com/supermomonga/shadcn_view_components/blob/main/docs/guides/unsupported-components.md)(代替と再評価条件を文書化)。calendarは個別契約として提供する。初期実装計画は[ロードマップ(履歴)](https://github.com/supermomonga/shadcn_view_components/blob/main/docs/history/initial-roadmap.md)を参照。
|
|
18
|
+
|
|
19
|
+
<!-- BEGIN GENERATED COMPONENT INVENTORY -->
|
|
20
|
+
- 提供範囲([適合試験registry](https://github.com/supermomonga/shadcn_view_components/blob/main/spec/conformance/registry.yml)から生成): **実装済み 61 アイテム / 描画可能な公開 ViewComponent 322 クラス**
|
|
21
|
+
- 実装済みアイテム:
|
|
22
|
+
`accordion`, `alert`, `alert-dialog`, `aspect-ratio`, `attachment`, `avatar`, `badge`, `breadcrumb`, `bubble`, `button`, `button-group`, `calendar`, `card`, `carousel`, `chart`, `checkbox`, `collapsible`, `combobox`, `command`, `context-menu`, `dialog`, `direction`, `drawer`, `dropdown-menu`, `empty`, `field`, `form`, `hover-card`, `input`, `input-group`, `input-otp`, `item`, `kbd`, `label`, `marker`, `menubar`, `message`, `message-scroller`, `native-select`, `navigation-menu`, `pagination`, `popover`, `progress`, `radio-group`, `resizable`, `scroll-area`, `select`, `separator`, `sheet`, `sidebar`, `skeleton`, `slider`, `sonner`, `spinner`, `switch`, `table`, `tabs`, `textarea`, `toggle`, `toggle-group`, `tooltip`
|
|
23
|
+
- 意図的に非対応のアイテム:
|
|
24
|
+
- `questionnaire`: 具体的な共通要件なしにRails版の状態管理を定義すると、upstreamと異なる独自仕様を保守することになるため。 代替: `form`, `field`, `input`, `radio-group`, `checkbox`, `button`, `progress`。
|
|
25
|
+
- `toast`: 独立したToastは既存のSonner通知基盤と責務が重複し、通知APIを二重に保守することになるため。 代替: `sonner`, `alert`。
|
|
26
|
+
<!-- END GENERATED COMPONENT INVENTORY -->
|
|
27
|
+
|
|
28
|
+
全クラスのinitializer、slot、HTML属性の適用先、フォーム送信、状態、JavaScript要件、upstreamとの差異は[コンポーネントAPIリファレンス](https://github.com/supermomonga/shadcn_view_components/blob/main/docs/reference/components/README.md)で確認できる。
|
|
29
|
+
意図的に非対応のコンポーネント(`questionnaire` / `toast`)の理由、代替手段(サーバー主導の複数ステップフォーム、Sonner通知)、再評価条件は[非対応コンポーネントと代替](https://github.com/supermomonga/shadcn_view_components/blob/main/docs/guides/unsupported-components.md)にまとめている。
|
|
30
|
+
|
|
31
|
+
- calendar は react-day-picker の実行時クラス合成のため静的抽出の対象外。
|
|
32
|
+
契約は lib/shadcn_view_components/contracts/calendar.rb に個別契約として保守し、
|
|
33
|
+
コンポーネントは月テーブル(年月ナビ・日付ボタン)として提供する
|
|
34
|
+
- インタラクティブふるまい(05 §3 ネイティブ最優先): toggle/toggle-group は Stimulus、
|
|
35
|
+
accordion/collapsible は `<details>`/`<summary>`、dialog系は `<dialog>` + showModal、
|
|
36
|
+
popover/tooltip/menu は Popover API
|
|
37
|
+
- DropdownMenu / ContextMenuはARIA menu、Menubarは複数のARIA menuを束ねるmenubar、
|
|
38
|
+
NavigationMenuはネイティブの`nav` / リンクとして、それぞれ独立したキーボード操作を提供する
|
|
39
|
+
- upstream 出所: `vendor/shadcn/manifest.json` が唯一の真実の源(現在: shadcn@4.19.0 系)
|
|
40
|
+
|
|
41
|
+
### 公開コンポーネントの境界
|
|
42
|
+
|
|
43
|
+
公開コンポーネントとして管理するクラスは、すべて`.new`して描画できるViewComponentである。
|
|
44
|
+
`Shadcn::Chart`、`Shadcn::Form`、`Shadcn::Resizable`、`Shadcn::Sonner`は名前空間であり、
|
|
45
|
+
描画には上記一覧の配下クラスを使う。公開コンポーネントの一覧は
|
|
46
|
+
`spec/conformance/registry.yml`で管理し、各クラスの契約と最小構成での描画を自動検証する。
|
|
47
|
+
基底クラスと内部ナビゲーション用クラスは公開APIに含まれない。
|
|
48
|
+
|
|
49
|
+
## サポート範囲
|
|
50
|
+
|
|
51
|
+
正本は[support matrix](https://github.com/supermomonga/shadcn_view_components/blob/main/docs/reference/support-matrix.md)。gemspec・CIも同じ範囲を表す。
|
|
52
|
+
|
|
53
|
+
| 種別 | 対象 |
|
|
54
|
+
|---|---|
|
|
55
|
+
| Ruby | 4.0系 |
|
|
56
|
+
| Rails | 8.1系(`~> 8.1`。major updateは検証後に緩和) |
|
|
57
|
+
| ViewComponent | 4.1以降の4系(4.0系はRails 8.1と組み合わせ不能なため対象外) |
|
|
58
|
+
| Tailwind CSS | v4(`tailwindcss-rails ~> 4.3`) |
|
|
59
|
+
| tailwind_merge | 1.5系 |
|
|
60
|
+
| ブラウザ | Baseline 2024以上(拘束条件はPopover API: Chrome/Edge 114+ / Safari 17+ / Firefox 125+)。polyfillは提供しない |
|
|
61
|
+
|
|
62
|
+
CIは最新リリース版で全検査を実行し、下限組み合わせ(`gemfiles/minimum.gemfile`)は
|
|
63
|
+
`rspec` / `tailwind-build` job内の軽量spec再実行で検証する。Node・pnpm等の
|
|
64
|
+
開発toolchainの要件もsupport matrixを参照。
|
|
65
|
+
|
|
66
|
+
## インストール
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# Gemfile
|
|
70
|
+
gem "shadcn_view_components"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Ruby 4.0系・Rails 8.1系([サポート範囲](#サポート範囲)参照)と、Tailwind CSS v4および
|
|
74
|
+
`tailwindcss-rails >= 4.3`が必須。`tailwindcss-rails`は本gemの
|
|
75
|
+
実行時依存として導入される。ホストに標準入力がまだない場合は、先に作成する:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bundle install
|
|
79
|
+
bin/rails tailwindcss:install
|
|
80
|
+
npm install tw-animate-css # または pnpm add / yarn add
|
|
81
|
+
bin/rails generate shadcn_view_components:install
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
インストーラは`app/assets/tailwind/application.css`だけを対象に、次の固定importを
|
|
85
|
+
冪等に配置する。標準入力がない場合は`tailwindcss:install`の実行を求めて失敗する:
|
|
86
|
+
|
|
87
|
+
```css
|
|
88
|
+
@import "tailwindcss";
|
|
89
|
+
@import "tw-animate-css";
|
|
90
|
+
|
|
91
|
+
/* shadcn_view_components */
|
|
92
|
+
@import "../builds/tailwind/shadcn_view_components";
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`tailwindcss:build`と`tailwindcss:watch`は、先に`tailwindcss:engines`を実行して
|
|
96
|
+
`app/assets/builds/tailwind/shadcn_view_components.css`を自動生成する。このwrapperが
|
|
97
|
+
gem同梱のEngine CSSを読み、生成契約、Calendar個別契約、ViewComponent、配布JavaScriptを
|
|
98
|
+
gem内部の相対`@source`で走査する。ホストCSSにgemの物理パスや`@source`は保存されず、
|
|
99
|
+
gem更新後にTailwindパス更新のためgeneratorを再実行する必要もない。
|
|
100
|
+
|
|
101
|
+
`app/assets/builds/tailwind/shadcn_view_components.css`は生成物なので直接編集しない。
|
|
102
|
+
`tw-animate-css`はホスト入力から解決するため、上記のnpm依存は必須。
|
|
103
|
+
|
|
104
|
+
### JS(インタラクティブコンポーネント利用時)
|
|
105
|
+
|
|
106
|
+
importmap-rails利用時はエンジンが自動pinする:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
// app/javascript/application.js
|
|
110
|
+
import { register } from "@supermomonga/shadcn-view-components"
|
|
111
|
+
register(application)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
自動pinを使えないimportmap-rails環境では、次を `config/importmap.rb` に追加する:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
pin_all_from ShadcnViewComponents::Engine.root.join("app/assets/javascripts/shadcn"),
|
|
118
|
+
under: "@supermomonga/shadcn-view-components",
|
|
119
|
+
to: "shadcn"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
jsbundling-rails等のbundlerを使う場合は、generatorでESM packageをrepository内の
|
|
123
|
+
安定した相対pathへ同期し、そのlocal dependencyを追加する:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
bin/rails generate shadcn_view_components:install --javascript=bundler
|
|
127
|
+
pnpm add ./vendor/shadcn_view_components/javascript
|
|
128
|
+
# npm install ./vendor/shadcn_view_components/javascript でも可
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`@hotwired/stimulus` はホスト側のdependencyを使う。登録コードはimportmapと同じで、
|
|
132
|
+
`import { register } from "@supermomonga/shadcn-view-components"` に統一される。
|
|
133
|
+
gem更新後はgeneratorとpackage managerのinstallを再実行し、同期されたpackageをcommitする。
|
|
134
|
+
|
|
135
|
+
## 使い方
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# 基本
|
|
139
|
+
render(Shadcn::Button.new) { "保存" }
|
|
140
|
+
|
|
141
|
+
# バリアント
|
|
142
|
+
render(Shadcn::Button.new(variant: :destructive, size: :sm, disabled: true)) { "削除" }
|
|
143
|
+
|
|
144
|
+
# リンクボタン(asChild代替 — タグ差し替え。data-slot・クラスは維持)
|
|
145
|
+
render(Shadcn::Button.new(tag: :a, href: post_path(post))) { "詳細" }
|
|
146
|
+
|
|
147
|
+
# クラスだけ欲しい場面(自前要素に適用)
|
|
148
|
+
Shadcn::Button.classes(variant: :link)
|
|
149
|
+
|
|
150
|
+
# 複合(ERBでは自然に書ける)
|
|
151
|
+
<%= render(Shadcn::Card.new) do %>
|
|
152
|
+
<%= render(Shadcn::Card::Header.new) do %>
|
|
153
|
+
<%= render(Shadcn::Card::Title.new) { "タイトル" } %>
|
|
154
|
+
<% end %>
|
|
155
|
+
<%= render(Shadcn::Card::Content.new) { "本文" } %>
|
|
156
|
+
<% end %>
|
|
157
|
+
|
|
158
|
+
# 浮動要素の配置(Popover / Tooltip / HoverCard / Menu系Contentで共通)
|
|
159
|
+
render(Shadcn::Popover::Content.new(
|
|
160
|
+
side: :right,
|
|
161
|
+
align: :start,
|
|
162
|
+
side_offset: 8,
|
|
163
|
+
align_offset: 0,
|
|
164
|
+
collision_padding: 5
|
|
165
|
+
)) { "内容" }
|
|
166
|
+
|
|
167
|
+
# JSで操作する装飾Select。default_valueはhidden inputへ入り、nameでフォーム送信される
|
|
168
|
+
render(Shadcn::Select.new(name: "framework", default_value: "rails")) do
|
|
169
|
+
safe_join([
|
|
170
|
+
render(Shadcn::Select::Trigger.new) do
|
|
171
|
+
render(Shadcn::Select::Value.new(placeholder: "選択してください"))
|
|
172
|
+
end,
|
|
173
|
+
render(Shadcn::Select::Content.new) do
|
|
174
|
+
safe_join([
|
|
175
|
+
render(Shadcn::Select::Item.new(value: "rails")) { "Ruby on Rails" },
|
|
176
|
+
render(Shadcn::Select::Item.new(value: "hanami")) { "Hanami" }
|
|
177
|
+
])
|
|
178
|
+
end
|
|
179
|
+
])
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### ComboboxをRailsフォームで使う
|
|
184
|
+
|
|
185
|
+
`Combobox`ルートが確定値を持ち、`Input` / `ChipsInput`は候補を検索する表示用inputとして働く。
|
|
186
|
+
`name:`、`default_value:`、`disabled:`、`required:`、`form:`はルートへ渡す。
|
|
187
|
+
候補の表示ラベルと送信値は`Item`の本文と`value:`で分ける。
|
|
188
|
+
|
|
189
|
+
```erb
|
|
190
|
+
<%= render(Shadcn::Combobox.new(
|
|
191
|
+
name: "profile[framework]",
|
|
192
|
+
default_value: "rails",
|
|
193
|
+
required: true
|
|
194
|
+
)) do %>
|
|
195
|
+
<%= render(Shadcn::Combobox::Input.new(placeholder: "検索…")) %>
|
|
196
|
+
<%= render(Shadcn::Combobox::Content.new) do %>
|
|
197
|
+
<%= render(Shadcn::Combobox::List.new) do %>
|
|
198
|
+
<%= render(Shadcn::Combobox::Item.new(value: "rails")) { "Ruby on Rails" } %>
|
|
199
|
+
<%= render(Shadcn::Combobox::Item.new(value: "hanami")) { "Hanami" } %>
|
|
200
|
+
<% end %>
|
|
201
|
+
<% end %>
|
|
202
|
+
<% end %>
|
|
203
|
+
|
|
204
|
+
<%= render(Shadcn::Combobox.new(
|
|
205
|
+
name: "profile[tags][]",
|
|
206
|
+
default_value: %w[rails hanami],
|
|
207
|
+
multiple: true
|
|
208
|
+
)) do %>
|
|
209
|
+
<%= render(Shadcn::Combobox::Chips.new) do %>
|
|
210
|
+
<%= render(Shadcn::Combobox::Chip.new(value: "rails")) { "Rails" } %>
|
|
211
|
+
<%= render(Shadcn::Combobox::Chip.new(value: "hanami")) { "Hanami" } %>
|
|
212
|
+
<%= render(Shadcn::Combobox::ChipsInput.new(placeholder: "追加…")) %>
|
|
213
|
+
<% end %>
|
|
214
|
+
<% end %>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
複数値の`[]`は自動付与しないため、Railsの配列パラメータには上例のような`name:`を指定する。
|
|
218
|
+
単一選択を空にすると空文字を送信する。chipsをすべて削除した場合もRailsへキーを送るため、
|
|
219
|
+
空文字のhidden inputを有効にする。受信側では`Array(params.dig(:profile, :tags)).compact_blank`のように
|
|
220
|
+
空文字を除いて空配列へ正規化する。空の自由入力と既存値の重複は無視される。
|
|
221
|
+
単一選択では未確定の検索文字列だけでは送信値を変えず、検索入力を空にしたときに空文字へ更新する。
|
|
222
|
+
確定値が実際に変わったときだけ、ルート内の
|
|
223
|
+
`select[data-slot="combobox-form-control"]`から、bubblingする`input`、`change`の順で発火する。
|
|
224
|
+
初期描画、Turboによる再接続、同じ値の再選択では発火しない。バリデーションエラー時は、
|
|
225
|
+
サーバへ届いた値を`default_value:`へ戻して再描画する。
|
|
226
|
+
|
|
227
|
+
### InputOTPをフォームで使う
|
|
228
|
+
|
|
229
|
+
`InputOTP`は実際の`input[type="text"]`を値と選択範囲の唯一の情報源にし、各桁の`Slot`を
|
|
230
|
+
表示用として同期する。ブロックを省略すると、`length:`個の`Slot`を1つの`Group`に入れた
|
|
231
|
+
標準構成を自動で描画する。`id`、`name`、`form`、`required`、`disabled`、ARIA、data、
|
|
232
|
+
イベント属性は実inputへ渡るため、Railsフォーム、`label[for]`、エラー要素と直接結び付けられる。
|
|
233
|
+
|
|
234
|
+
```erb
|
|
235
|
+
<%= render(Shadcn::Form::Item.new(invalid: @code_error.present?)) do %>
|
|
236
|
+
<%= render(Shadcn::Field::Label.new(for: "verification-code")) { "認証コード" } %>
|
|
237
|
+
<%= render(Shadcn::InputOTP.new(
|
|
238
|
+
id: "verification-code",
|
|
239
|
+
name: "verification[code]",
|
|
240
|
+
length: 6,
|
|
241
|
+
value: params.dig(:verification, :code),
|
|
242
|
+
pattern: '^\d+$',
|
|
243
|
+
required: true,
|
|
244
|
+
aria: {
|
|
245
|
+
invalid: @code_error.present?.to_s,
|
|
246
|
+
describedby: ("verification-code-error" if @code_error.present?)
|
|
247
|
+
}
|
|
248
|
+
)) %>
|
|
249
|
+
<%= render(Shadcn::Form::Error.new(id: "verification-code-error", message: @code_error)) %>
|
|
250
|
+
<% end %>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`inputmode: "numeric"`はモバイル端末へ数字キーボードを示すヒントであり、文字種を制限しない。
|
|
254
|
+
数字だけに制限する場合は、上例のようにupstreamの`REGEXP_ONLY_DIGITS`と同じ
|
|
255
|
+
`pattern: '^\d+$'`を指定する。patternに一致しない入力・貼り付けは、不正文字だけを除くのではなく
|
|
256
|
+
変更全体を拒否して直前の値を保つ。patternを省略すれば任意の文字を入力できる。
|
|
257
|
+
入力中の判定はJavaScriptの`RegExp`を使うため、`pattern`には`^`と`$`を含めて全体一致を明示する。
|
|
258
|
+
これにより、HTMLのpattern制約によるフォーム送信時の全体一致判定とも結果が一致する。
|
|
259
|
+
|
|
260
|
+
独自の区切り方が必要な場合は、ブロック内に`Group`、`Slot.new(index:)`、`Separator`を明示する。
|
|
261
|
+
`container_class:`は実inputを覆う表示コンテナへ、`class:`は実inputへ追加される。初期値、入力、削除、
|
|
262
|
+
貼り付け、one-time-codeの自動入力、caretはStimulusが同じ実inputから各Slotへ反映する。
|
|
263
|
+
JavaScriptが無効な場合は、同梱の`noscript`スタイルによって実input自体を通常のテキスト欄として表示する。
|
|
264
|
+
|
|
265
|
+
### Checkbox、RadioGroup、Switchをフォームで使う
|
|
266
|
+
|
|
267
|
+
`Checkbox`、`RadioGroup::Item`、`Switch`は、ネイティブinputの`checked`プロパティを状態の唯一の
|
|
268
|
+
情報源にする。`data-checked` / `data-unchecked`は見た目と外部コード向けの投影であり、初期描画、
|
|
269
|
+
利用者操作、フォームのreset、Turboによる再接続のたびに現在の`checked`へ同期される。プログラムから
|
|
270
|
+
`input.checked`を変更した場合、ブラウザは`change`イベントを自動では発火しないため、変更後に
|
|
271
|
+
bubblingする`change`イベントをdispatchする。
|
|
272
|
+
|
|
273
|
+
```erb
|
|
274
|
+
<%= render(Shadcn::Checkbox.new(
|
|
275
|
+
id: "terms",
|
|
276
|
+
name: "account[terms]",
|
|
277
|
+
value: "accepted",
|
|
278
|
+
required: true,
|
|
279
|
+
aria: { label: "利用規約に同意する" }
|
|
280
|
+
)) %>
|
|
281
|
+
|
|
282
|
+
<%= render(Shadcn::RadioGroup.new(aria: { label: "プラン" })) do %>
|
|
283
|
+
<%= render(Shadcn::RadioGroup::Item.new(name: "account[plan]", value: "free", checked: true, aria: { label: "無料" })) %>
|
|
284
|
+
<%= render(Shadcn::RadioGroup::Item.new(name: "account[plan]", value: "pro", aria: { label: "プロ" })) %>
|
|
285
|
+
<% end %>
|
|
286
|
+
|
|
287
|
+
<%= render(Shadcn::Switch.new(name: "account[notifications]", value: "enabled", aria: { label: "通知" })) %>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Radioの`name`、`value`、`checked`、`required`、`disabled`、`form`は`RadioGroup`ではなく各`Item`へ
|
|
291
|
+
指定する。同じフォーム所有者と`name`を持つItemは、ブラウザのネイティブな排他グループになる。
|
|
292
|
+
CheckboxとSwitchは未選択時にはフォーム値を送信せず、Radioは選択されたItemの値だけを送信する。
|
|
293
|
+
未選択値も必要な場合はRailsのフォームヘルパと同様にhidden inputを別途置く。`checked`属性は初期値と
|
|
294
|
+
reset後の復帰先を表す。JavaScriptが無効でも、選択・キーボード操作・フォーム送信・表示色と印の切替は
|
|
295
|
+
ネイティブinputと`:checked` CSSで動作する。
|
|
296
|
+
|
|
297
|
+
### Sliderをフォームで使う
|
|
298
|
+
|
|
299
|
+
`Slider`はネイティブの`input[type="range"]`を値の唯一の情報源にする。`id`、`name`、`form`、
|
|
300
|
+
`disabled`、`required`、`aria:`、`data:`、イベント属性は実際のinputへ渡り、`class:`、`style:`、
|
|
301
|
+
`tag:`だけが見た目を構成するRootへ渡る。
|
|
302
|
+
|
|
303
|
+
```erb
|
|
304
|
+
<%= render(Shadcn::Label.new(for: "volume")) { "音量" } %>
|
|
305
|
+
<%= render(Shadcn::Slider.new(
|
|
306
|
+
id: "volume",
|
|
307
|
+
name: "settings[volume]",
|
|
308
|
+
min: 0,
|
|
309
|
+
max: 100,
|
|
310
|
+
step: 5,
|
|
311
|
+
value: 40,
|
|
312
|
+
aria: { label: "音量" }
|
|
313
|
+
)) %>
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`min` / `max` / `step` / `value`は有限数として検証し、`max > min`、`step > 0`、値域、
|
|
317
|
+
`min`を基準にしたstepとの一致を満たさない値は`ArgumentError`にする。`value: nil`ではブラウザ標準の
|
|
318
|
+
step調整済み中間値を使う。`orientation: :vertical`ではRootへ高さを`style:`または`class:`で指定する。
|
|
319
|
+
ポインタ・矢印キー・Home / Endの値変更はネイティブinputへ委ね、Stimulusはrangeとthumbの表示だけを同期する。
|
|
320
|
+
|
|
321
|
+
### Carouselの向きと書字方向を指定する
|
|
322
|
+
|
|
323
|
+
`Carousel`はRootの`orientation:`と`direction:`をレイアウトと移動方向の唯一の指定箇所にする。
|
|
324
|
+
子の`Content`、`Item`、`Previous`、`Next`へ同じ値を繰り返し渡す必要はない。`direction:`はRootの
|
|
325
|
+
`dir`属性にも反映され、ブラウザのRTLレイアウトとcontrollerのスクロール位置正規化を一致させる。
|
|
326
|
+
|
|
327
|
+
```erb
|
|
328
|
+
<h2 id="recommendations-title">おすすめ</h2>
|
|
329
|
+
<%= render(Shadcn::Carousel.new(
|
|
330
|
+
orientation: :vertical,
|
|
331
|
+
direction: :rtl,
|
|
332
|
+
class: "h-80 max-w-xs",
|
|
333
|
+
aria: { labelledby: "recommendations-title" }
|
|
334
|
+
)) do %>
|
|
335
|
+
<%= render(Shadcn::Carousel::Content.new(class: "h-64")) do %>
|
|
336
|
+
<% recommendations.each_with_index do |recommendation, index| %>
|
|
337
|
+
<%= render(Shadcn::Carousel::Item.new(
|
|
338
|
+
aria: { label: "#{index + 1} of #{recommendations.size}" }
|
|
339
|
+
)) { recommendation.name } %>
|
|
340
|
+
<% end %>
|
|
341
|
+
<% end %>
|
|
342
|
+
<%= render(Shadcn::Carousel::Previous.new) %>
|
|
343
|
+
<%= render(Shadcn::Carousel::Next.new) %>
|
|
344
|
+
<% end %>
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
縦向きでは上例のように表示領域の高さを指定する。移動先はviewport幅・高さの固定量ではなく、
|
|
348
|
+
実際の各Itemの位置から決めるため、Itemの寸法が異なる場合やレスポンシブ変更後も同じAPIを使える。
|
|
349
|
+
横LTRでは左矢印が前、右矢印が次、横RTLではその対応が逆になり、縦向きは書字方向にかかわらず
|
|
350
|
+
上矢印が前、下矢印が次になる。Previous / Nextは現在の論理スクロール位置が先頭・末尾に達したとき、
|
|
351
|
+
ネイティブの`disabled`状態へ同期する。Rootは既定で`tabindex="0"`となるため、Rootへフォーカスして
|
|
352
|
+
矢印キーを使える。既存のフォーカス設計へ組み込む場合は`tabindex:`で明示的に上書きできる。
|
|
353
|
+
|
|
354
|
+
Rootには内容を表す`aria-label`または`aria-labelledby`を指定する。各Itemにも内容名、または上例の
|
|
355
|
+
`1 of N`のような位置を表す名前を指定できる。複数Itemが同時に見える構成もあるため、ライブラリは
|
|
356
|
+
Itemへ一律の`aria-current`や`aria-hidden`を付けない。
|
|
357
|
+
[WAI-ARIA Carousel Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/)に従い、Rootの
|
|
358
|
+
`role="region"` / `aria-roledescription="carousel"`、Itemの`role="group"` /
|
|
359
|
+
`aria-roledescription="slide"`、ネイティブボタンの操作を保つ。
|
|
360
|
+
|
|
361
|
+
### 複合コンポーネントのアクセシビリティ契約
|
|
362
|
+
|
|
363
|
+
JavaScriptを使う複合コンポーネントは、ルートごとに一意なIDを生成し、子要素間のARIA参照を
|
|
364
|
+
接続時に補完する。利用者が指定した`id`、`aria-controls`、`aria-labelledby`、
|
|
365
|
+
`aria-describedby`などは上書きしない。fragment cacheなどによって自動生成IDだけが重複した場合は、
|
|
366
|
+
そのルートと自動生成した参照先だけを再採番する。入れ子にした同種コンポーネントは、それぞれの
|
|
367
|
+
controllerが直近のルートだけを管理する。
|
|
368
|
+
Tabsの向きは`Shadcn::Tabs.new(orientation: :horizontal | :vertical)`へ指定し、Listの
|
|
369
|
+
`aria-orientation`と矢印キーの方向を同じ値から補完する。
|
|
370
|
+
|
|
371
|
+
| コンポーネント | ARIA参照・状態 | キーボード操作 |
|
|
372
|
+
| --- | --- | --- |
|
|
373
|
+
| Dialog / AlertDialog / Sheet / Drawer | Triggerと`dialog`を`aria-controls`で結び、TitleとDescriptionを`aria-labelledby` / `aria-describedby`で参照する。開閉時は`aria-expanded`を同期する | ネイティブ`<dialog>`のモーダルフォーカス管理、Tab巡回、Escapeで閉じてTriggerへ戻る |
|
|
374
|
+
| Tabs | TriggerとPanelを`aria-controls` / `aria-labelledby`で相互参照し、`aria-selected`、`tabindex`、`hidden`を同期する | 向きに応じた矢印キー、Home / Endで選択とフォーカスを移動する |
|
|
375
|
+
| Combobox | Input、Listbox、Optionを`aria-controls` / `aria-labelledby` / `aria-activedescendant`で結び、候補の`aria-selected`を同期する | Inputにフォーカスを保ち、矢印キー、Home / End、Enter、Escapeで候補を操作する |
|
|
376
|
+
| Select | Trigger、Listbox、Optionを`aria-controls` / `aria-labelledby` / `aria-activedescendant`で結び、開閉・選択状態を同期する | Triggerにフォーカスを保ち、矢印キー、Home / End、Enter / Space、Escape、Tabで操作する |
|
|
377
|
+
| Accordion | TriggerとContentを`aria-controls` / `aria-labelledby`で相互参照し、`aria-expanded`を同期する | ネイティブ`<summary>`のEnter / Space操作を保つ |
|
|
378
|
+
| Carousel | Rootを`region`、Itemを`group`として識別し、Previous / Nextの`disabled`を論理スクロール位置へ同期する | 横LTRは左 / 右、横RTLは右 / 左、縦は上 / 下矢印で前後へ移動する。ボタンはEnter / Spaceでも操作できる |
|
|
379
|
+
| Resizable | Handleを`separator`として前方Panelへ結び、`aria-valuemin` / `aria-valuemax` / `aria-valuenow`を同期する | 向きに応じた矢印キーで5%ずつ、Home / Endで最小値・最大値へ変更する |
|
|
380
|
+
| Calendar | GridをCaptionへ結び、日付セルの`aria-selected`、当日の`aria-current`、日付ボタンの読み上げ名を設定する | 表示中の月表内で矢印キーを日・週単位、Home / Endを行の先頭・末尾への移動に使う |
|
|
381
|
+
|
|
382
|
+
注意: 純Rubyのコード(`#call` 内等)で複数の子を `render` で連ねるときはブロックの
|
|
383
|
+
戻り値しか使われないため `safe_join([...])` で連結する(ERBでは出力バッファが連結するため不要)。
|
|
384
|
+
|
|
385
|
+
- バリアント値は Symbol / String 両方を受け付ける。契約に存在しない値は `ArgumentError`(fail-fast)
|
|
386
|
+
- data属性・ARIA・CSS値になる意味的プロパティも同様に描画前に検証する。たとえば
|
|
387
|
+
`orientation` は `horizontal/vertical`、Toggle状態は `off/on`、Sheetの `side` は
|
|
388
|
+
`top/right/bottom/left`、Toggle Groupの `type` は `single/multiple`
|
|
389
|
+
- 数値プロパティは有限の数値または厳密な数値文字列だけを受け付ける。`nil`、上下限、既定値を含む
|
|
390
|
+
個別の制約は `Shadcn::Progress.property_contract(:value)` のように確認できる
|
|
391
|
+
- 浮動要素の `side` は `top/right/bottom/left/inline-start/inline-end`、`align` は
|
|
392
|
+
`start/center/end`。画面端では実配置を反転・調整し、scrollやresizeにも追従する
|
|
393
|
+
- `Select` は単一値の装飾listboxで、Stimulus登録が必要。JS不要、`multiple`、ブラウザ標準の
|
|
394
|
+
制約検証が必要なフォームには、実際の`<select>`を描く`NativeSelect`を使う
|
|
395
|
+
- `class:` で渡した追加クラスは `tailwind_merge` により契約クラスと統合される(利用者の上書きが後勝ち)
|
|
396
|
+
- テーマのカスタマイズは CSS変数の上書きが唯一の公式経路(`@import` より後に書く)
|
|
397
|
+
|
|
398
|
+
## 開発
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
bin/setup # mise toolchain + Ruby + 全JavaScript依存を導入
|
|
402
|
+
# (Ruby 4.0 / Node 24 / pnpm 10.22.0 — support matrix参照)
|
|
403
|
+
bundle exec rake verify # CI必須検査を同じRake taskで順に実行
|
|
404
|
+
|
|
405
|
+
bundle exec rake verify:spec # component/conformance/contract/generator/request
|
|
406
|
+
bundle exec rake verify:system # ふるまい(Cuprite + Chrome)
|
|
407
|
+
bundle exec rake verify:parity # visual + animation parity
|
|
408
|
+
bundle exec rake verify:javascript # extractor + distributed JS lint/typecheck/DOM/bundle tests
|
|
409
|
+
bundle exec rake verify:sorbet # Sorbet + RBI freshness
|
|
410
|
+
bundle exec rake verify:generated # コード生成物と生成ドキュメントの決定性
|
|
411
|
+
bundle exec rake verify:tailwind # install 済み gem の consumer 検証と Tailwind build
|
|
412
|
+
bundle exec rake verify:rubocop # Ruby lint
|
|
413
|
+
mise run lookbook # プレビュー(http://localhost:9292/)
|
|
414
|
+
mise run build-css # Lookbook用の静的スタイル再生成
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
CI検査を追加する場合は、`.github/workflows/ci.yml`のjobへ対応する
|
|
418
|
+
`LOCAL_VERIFY_TASK`を宣言し、同じtaskを`verify:full`の依存に追加する。
|
|
419
|
+
契約specが全jobと`verify:full`の完全一致を検査するため、どちらか一方だけの追加はCIで失敗する。
|
|
420
|
+
検査コマンドをworkflowへ直接追加せず、対応する`verify:*` task内へ実装する。
|
|
421
|
+
このworkflowは検証専用とし、他のstepは`setup_*`または`artifact_*`のIDを付ける。
|
|
422
|
+
未分類stepは契約specが拒否し、releaseやdeployのjobは目的別のworkflowへ分ける。
|
|
423
|
+
通常CIとupstream同期は、依存関係の導入にも文書と同じ`bin/setup`を使用する。
|
|
424
|
+
|
|
425
|
+
Lookbook は dummy アプリのルートパス(`/`)で開く。プレビューツールバーには
|
|
426
|
+
**Theme トグルボタン(月/太陽アイコン)** があり、プレビューの
|
|
427
|
+
ライト/ダークを切り替えられる(選択はクッキーに永続化)。反映はプレビュー専用レイアウト
|
|
428
|
+
(`spec/dummy/app/views/layouts/preview.html.erb`)が `<html class="dark">` として行う。
|
|
429
|
+
ボタンは Lookbook の display option「theme」(select)のテンプレートを差し替えたもので
|
|
430
|
+
(`spec/dummy/config/initializers/lookbook_theme_toggle.rb` 参照)、動作経路は Lookbook 組み込みのままである。
|
|
431
|
+
|
|
432
|
+
### コンポーネント別テスト範囲
|
|
433
|
+
|
|
434
|
+
`spec/coverage/registry.yml` は、実装済みの全61 registry itemについて、最小描画、
|
|
435
|
+
Lookbookプレビュー、upstreamとの見た目比較、ブラウザ操作テストの対応を管理する正本である。
|
|
436
|
+
見た目比較またはブラウザ操作テストを行わない項目にも、機械検査できる除外理由を必ず記載する。
|
|
437
|
+
契約registryへ実装済みitemを追加したのにcoverage行がない場合、存在しないpreview・controller・
|
|
438
|
+
system specを指定した場合、あるいはsystem spec側の対象・確認項目と台帳が一致しない場合はCIが失敗する。
|
|
439
|
+
|
|
440
|
+
全公開exportの最小構成はcomponent contract specが実際に描画し、全preview exampleはrequest specが
|
|
441
|
+
HTTP描画する。操作を持つコンポーネントはsystem specの`component_coverage` metadataで、pointer、
|
|
442
|
+
keyboard、state、form、reset、reconnect、no-JS、accessibilityなど、実際に確認するふるまいを宣言する。
|
|
443
|
+
|
|
444
|
+
### 生成パイプライン
|
|
445
|
+
|
|
446
|
+
```
|
|
447
|
+
rake shadcn:sync # upstream レジストリを取得 → vendor/shadcn/(ネットワーク使用)
|
|
448
|
+
rake shadcn:generate # vendor から契約JSON + Ruby + CSS を生成(オフライン)
|
|
449
|
+
rake shadcn:update # sync + generate(追従作業のフルセット)
|
|
450
|
+
rake shadcn:check # 決定論性検証(一時ディレクトリ生成とコミット済み生成物のバイト比較)
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`shadcn:sync` は、upstream の内容と revision が前回から変わらない場合、manifest の
|
|
454
|
+
`fetched_at` / `checked_at` を保持する。同じ入力を再同期しても時刻だけの差分は作られない。
|
|
455
|
+
同期時は unversioned なregistry一式とGitHub release metadataを処理の前後で二度確認し、
|
|
456
|
+
内容が安定している場合だけ完成済みstaging directoryを `vendor/shadcn` へ置換する。404、
|
|
457
|
+
通信失敗、JSON/schema不正、同期中の変更では既存snapshotを変更せず、`[sync:<種別>]` と
|
|
458
|
+
`retryable=true|false` をエラーへ出す。
|
|
459
|
+
|
|
460
|
+
編集ポリシー(詳細は[アーキテクチャとリポジトリ構成](https://github.com/supermomonga/shadcn_view_components/blob/main/docs/reference/architecture.md#2-リポジトリ構成)):
|
|
461
|
+
|
|
462
|
+
| パス | 性質 | 編集 |
|
|
463
|
+
|---|---|---|
|
|
464
|
+
| `app/components/shadcn/` | 手書き | 可 |
|
|
465
|
+
| `app/assets/javascripts/shadcn/` | 手書き | 可 |
|
|
466
|
+
| `tools/extractor/` | 手書き(パイプライン本体) | 可 |
|
|
467
|
+
| `vendor/shadcn/` | スナップショット | `rake shadcn:sync` のみ |
|
|
468
|
+
| `gen/contracts/`, `lib/shadcn_view_components/generated/`, `app/assets/stylesheets/shadcn/` | **生成物** | 禁止(再生成) |
|
|
469
|
+
|
|
470
|
+
### 新規コンポーネント追加
|
|
471
|
+
|
|
472
|
+
1. `tools/extractor/config/targets.json` にアイテム名を追加 → `rake shadcn:generate`
|
|
473
|
+
2. `app/components/shadcn/<name>.rb` + `<name>.html.erb` を実装(クラスは `Classes.resolve` 経由のみ。04 §7のチェックリスト参照)
|
|
474
|
+
3. `spec/conformance/registry.yml` に1行追加(該当アイテムを非対応の `unsupported` から `exports` へ)→ 適合試験が自動的に全組み合わせを検証
|
|
475
|
+
4. `spec/coverage/registry.yml` にpreview、見た目比較、操作テストの対応を追加する。対象外にする検証には具体的な理由を書く
|
|
476
|
+
|
|
477
|
+
### 週次upstream追従
|
|
478
|
+
|
|
479
|
+
`.github/workflows/upstream-drift.yml` が毎週月曜 09:00 JST に `rake shadcn:update`
|
|
480
|
+
を実行する。差分があれば、Chromeを含む全依存を用意して
|
|
481
|
+
`bundle exec rake verify` の8検査を先に実行し、全て成功した場合だけ自動PR
|
|
482
|
+
(`chore/upstream-sync`) を作成する。検証失敗はworkflow自体の失敗となり、PRは作成しない。
|
|
483
|
+
|
|
484
|
+
PR作成は `github.token` に固定し、最初にdraftで作成する。同じhead branchを指定して
|
|
485
|
+
`CI` workflowを `workflow_dispatch` する。GitHubの再帰防止により、`github.token` が
|
|
486
|
+
作成したPRの `pull_request` eventが起動しない場合でも、PR headに必須の
|
|
487
|
+
8 status checkが付く。その8個が実際に作成された後だけreview readyにする。
|
|
488
|
+
PR本文とActions summaryには、upstreamの完全なcommit SHA、
|
|
489
|
+
registry snapshot hash、取得日時、先行検証の結果とworkflow URLを記録する。手動実行は
|
|
490
|
+
`workflow_dispatch` から行う。
|
|
491
|
+
|
|
492
|
+
リポジトリ設定ではActionsにPull Request作成を許可し、mainの必須checkを
|
|
493
|
+
`lint-ruby`, `lint-js`, `sorbet`, `rspec`, `system`, `parity`, `determinism`,
|
|
494
|
+
`tailwind-build` に固定する。専用PATやApp tokenは使用しない。
|
|
495
|
+
|
|
496
|
+
### 見た目のupstreamパリティ検証(visual parity)
|
|
497
|
+
|
|
498
|
+
各コンポーネントの見た目が元のshadcn/ui実装(vendor/shadcn のReact実装)と一致するかをピクセル比較で機械判定する仕組み。契約による「クラス文字列の一致」、コンポーネントspecによる「DOM構造の一致」の先にある最終段(「CSS適用結果を含めた見た目の一致」)を検証する。
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
bundle exec rake parity:run # または mise run parity(ハーネスを明示的に再ビルド)
|
|
502
|
+
PARITY_RATIO=0.01 bundle exec rake parity:run # 閾値を1%に緩和(既定 0.5%)
|
|
503
|
+
bundle exec rake parity:update # 追跡baselineを意図して更新するときだけ実行
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
仕組み: `vendor/shadcn` の tsx を `tools/visual-parity` で展開し、Vite + React で実際に描画(upstream側)。dummy の Lookbook プレビュー(うち側)と同じ Chromium(Cuprite)でスクリーンショットを撮り、pixelmatch で差分率を判定する。upstream側のスタイルは upstream 実アプリの globals.css 相当のみを抽出器が生成した `tools/visual-parity/src/upstream_theme.css`(トークン + npm `shadcn/tailwind.css` の verbatim取り込み)から与えられ、gem の `shadcn.css` とは独立している。これにより shadcn.css への移植漏れ・移植ミス(例: カスタムバリアント未定義でクラスが沈黙する)が upstream 側との差分として検出される(共有してしまうと両側が同じだけ壊れて差分が消えるため)。両側でアニメーションを停止し、同一ブラウザ・同一フォントで比較するため決定論的。各シナリオは **light/dark 両カラースキーム**で撮影する(dark は両側の `<html>` に `.dark` を付与。`dark:bg-destructive/60` 等の dark時ユーティリティや `.dark` トークンの差分はこのモードでしか検出できない)。
|
|
507
|
+
|
|
508
|
+
画像寸法がわずかに異なる場合は、各画像の右下にあるページ背景色で不足領域を補完する。
|
|
509
|
+
これによりlight/darkのどちらでも背景だけの寸法差を同じように扱い、余分な領域にある
|
|
510
|
+
実コンテンツは引き続き差分として検出する。
|
|
511
|
+
|
|
512
|
+
さらにアニメーションパリティ(`spec/visual/animation_parity_spec.rb`)では、両側のコンポーネントを開いた直後に WAAPI でアニメーションを取得・停止し、currentTime を同一チェックポイント(0/25/50/75/100%)に固定した上で補間値(opacity / transform / 高さ)とアニメーション名・持続時間・イージングを比較する。時間を仮想化するため実行タイミングに影響されない(drawer は upstream が vaul のJSバネ物理で動くため対象外)。
|
|
513
|
+
|
|
514
|
+
- 素の `bundle exec rspec` でも**常時実行される**。upstream参照サーバ(vite preview)のキャッシュビルドと起動・停止は `spec/support/parity_server.rb` が自動で行う(ビルド入力のハッシュが変わらなければ再ビルドを省略)。明示的に外したいときだけ `PARITY=0 bundle exec rspec`
|
|
515
|
+
- CI でも必須ジョブ(`parity`)として実行される
|
|
516
|
+
- 通常実行の成果物はgitignore済みの `tmp/visual-parity/run-<pid>/<demo>/<light|dark>/`
|
|
517
|
+
(ours.png / upstream.png / diff.png / report.json)へ出力する。CI失敗時は同じ内容を
|
|
518
|
+
`visual-parity-<run id>-<attempt>` artifactとして7日間保存する
|
|
519
|
+
- 追跡中の `spec/visual/baselines/` は通常テストから変更しない。
|
|
520
|
+
`bundle exec rake parity:update` を明示的に実行した場合だけ更新し、画像差分をレビューしてCommitする
|
|
521
|
+
- 対象は `spec/coverage/registry.yml` が管理する99シナリオ。各シナリオをlight/darkで比較し、対象外5アイテムは同一状態を比較できない設計差の理由を同registryへ記録する
|
|
522
|
+
|
|
523
|
+
**シナリオの追加手順**:
|
|
524
|
+
|
|
525
|
+
1. 対象コンポーネントの Lookbook プレビュー(`spec/dummy/app/components/previews/shadcn/*_preview.rb`)を用意
|
|
526
|
+
2. `tools/visual-parity/src/demos.tsx` に同じテキスト・props・並びのJSXデモを追加(キーは `<コンポーネント>/<シナリオ>`)
|
|
527
|
+
3. `spec/coverage/registry.yml` の対象itemの`parity.scenarios`にプレビューのパスとデモIDを追加
|
|
528
|
+
|
|
529
|
+
`tools/visual-parity/src/components/ui/`(展開したupstreamソース)と `dist/` は gitignore 済みで、`pnpm run unpack` / `vite build` が常に `vendor/shadcn` から再生成する(sha256検証つき)。
|
|
530
|
+
|
|
531
|
+
## トラブルシューティング
|
|
532
|
+
|
|
533
|
+
| 症状 | 原因 | 対処 |
|
|
534
|
+
|---|---|---|
|
|
535
|
+
| スタイルがまったく当たらない | Engine wrapperが未生成、または固定importがない | インストーラを実行し、`bin/rails tailwindcss:build`を実行 |
|
|
536
|
+
| ダークモードが効かない | `.dark` の付与先が `<html>` 以外 | `<html class="dark">` に付与 |
|
|
537
|
+
| 変数を上書きしたのに反映されない | 上書き位置が `@import` より前 | importより後に書く |
|
|
538
|
+
| クラスの競合が意図どおりに解決されない | `tailwind_merge` gemのバージョン差 | issueで報告 |
|
|
539
|
+
|
|
540
|
+
## ライセンス
|
|
541
|
+
|
|
542
|
+
[MIT License](LICENSE)。本プロジェクトと shadcn/ui 由来の配布物のライセンス本文は、gem に同梱した LICENSE を参照してください。
|