shadcn_view_components 0.2.1 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a817d07fcea9d79467f32d8ba3a0c2cd58fcafd2fac59664f50963bd099e551
4
- data.tar.gz: 2c7d7f5b36b0c2ce319a3f159e98c0b685f02467b0c2737a5e2bcb6a39a1fe84
3
+ metadata.gz: 37380475ab9f7c6dd604b818376c3b9ef757d145a5c5d29e832eb5a503f9268a
4
+ data.tar.gz: 142bc9dea6e0ef39f223c3d4d1404de9707e9a7e6ea07b2f872572d3b6000bb9
5
5
  SHA512:
6
- metadata.gz: 310c43a4aa66d4a2446553e0de95c888f2722bf9d09c6acfebe91f997ea7d25209331480f37f04fcef300f4c4ac00c2ae25f7f9421004635e5c3a1cb4f9b1b06
7
- data.tar.gz: 46b8be5f13e80117e632d1c464fdb709f8c30fd124446f8939985cdc446add6ed3534633fba1a967b3b593c4463d02c47a49f3cf8eb7bbf27bc165aff86e90b1
6
+ metadata.gz: cc6b3e0426da0209f6024f1910bda0d555459e1ea4a7a33bfb29268bdf71b163a3829ce225aa0f71841c336202ef25d8db56a000162c843bfe1e49ffe5d6ca6b
7
+ data.tar.gz: 3e5d0d6d1c29cd72a0001351ec83aceaf7487ba41cd1f652633aaf6a19f07a52d34d71312845a41ae46620ce908961648c03e97c78c635025a5890b9d90e0dd3
data/README.md CHANGED
@@ -499,15 +499,15 @@ registry snapshot hash、取得日時、先行検証の結果とworkflow URLを
499
499
 
500
500
  ```bash
501
501
  bundle exec rake parity:run # または mise run parity(ハーネスを明示的に再ビルド)
502
- PARITY_RATIO=0.01 bundle exec rake parity:run # 閾値を1%に緩和(既定 0.5%)
503
502
  bundle exec rake parity:update # 追跡baselineを意図して更新するときだけ実行
504
503
  ```
505
504
 
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` トークンの差分はこのモードでしか検出できない)。
505
+ 仕組み: `vendor/shadcn` の tsx を `tools/visual-parity` で展開し、Vite + React で実際に描画(upstream側)。dummy の Lookbook プレビュー(うち側)と同じ Chromium(Cuprite)でスクリーンショットを撮り、**画像寸法と復号後の全RGBA値の完全一致**を判定する。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
506
 
508
- 画像寸法がわずかに異なる場合は、各画像の右下にあるページ背景色で不足領域を補完する。
509
- これによりlight/darkのどちらでも背景だけの寸法差を同じように扱い、余分な領域にある
510
- 実コンテンツは引き続き差分として検出する。
507
+ 既定の許容差分は0px。画像寸法が異なる場合も失敗する。実装上避けられない差分だけ、
508
+ `spec/coverage/registry.yml` に理由と許容する矩形領域を記録する。現在の例外は、
509
+ ネイティブCSSスクロールバーを使うScrollAreaの右端だけ。領域外のRGBAは常に完全一致を要求する。
510
+ CIの`parity` jobはUbuntu 24.04とChromiumのrevisionを固定し、`system` jobは最新Chromeで動作を検証する。
511
511
 
512
512
  さらにアニメーションパリティ(`spec/visual/animation_parity_spec.rb`)では、両側のコンポーネントを開いた直後に WAAPI でアニメーションを取得・停止し、currentTime を同一チェックポイント(0/25/50/75/100%)に固定した上で補間値(opacity / transform / 高さ)とアニメーション名・持続時間・イージングを比較する。時間を仮想化するため実行タイミングに影響されない(drawer は upstream が vaul のJSバネ物理で動くため対象外)。
513
513
 
@@ -18,31 +18,36 @@ export default class SliderController extends Controller {
18
18
  const input = this.input
19
19
  if (!input) return
20
20
 
21
- const percentage = this.percentage(input)
22
21
  const vertical = this.element.dataset.orientation === "vertical"
22
+ const thumb = this.thumbs[0]
23
+ if (!thumb) return
24
+
25
+ const ratio = this.ratio(input)
26
+ const thumbSize = vertical ? thumb.offsetHeight : thumb.offsetWidth
27
+ const offset = Math.round((0.5 - ratio) * thumbSize * 1000) / 1000
28
+ const position = `calc(${ratio * 100}% + ${offset}px)`
23
29
 
24
30
  for (const range of this.ranges) {
25
- range.style.width = vertical ? "" : percentage
26
- range.style.height = vertical ? percentage : ""
31
+ range.style.width = vertical ? "" : position
32
+ range.style.height = vertical ? position : ""
27
33
  }
28
34
 
29
35
  for (const thumb of this.thumbs) {
30
- thumb.style.left = vertical ? "50%" : percentage
31
- thumb.style.bottom = vertical ? percentage : ""
36
+ thumb.style.left = vertical ? "50%" : position
37
+ thumb.style.bottom = vertical ? position : ""
32
38
  }
33
39
  }
34
40
 
35
41
  /** @param {HTMLInputElement} input */
36
- percentage(input) {
42
+ ratio(input) {
37
43
  const min = this.numberOr(input.min, DEFAULT_MIN)
38
44
  const max = this.numberOr(input.max, DEFAULT_MAX)
39
- if (max <= min) return "0%"
45
+ if (max <= min) return 0
40
46
 
41
47
  const value = Number.isFinite(input.valueAsNumber)
42
48
  ? input.valueAsNumber
43
49
  : this.numberOr(input.value, min)
44
- const ratio = Math.min(1, Math.max(0, (value - min) / (max - min)))
45
- return `${ratio * 100}%`
50
+ return Math.min(1, Math.max(0, (value - min) / (max - min)))
46
51
  }
47
52
 
48
53
  /** @param {string} value @param {number} fallback */
@@ -36,9 +36,12 @@ module Shadcn
36
36
  sig { override.returns(String) }
37
37
  def call
38
38
  content_tag(:div, **html_attributes) do
39
- # ナビはキャプション行に絶対配置で重なる(upstream rdp と同じ構造)
40
- navigation = Navigation.new(month: @month, caption_id: caption_id, month_path: @month_path)
41
- safe_join([render(navigation), month_grid])
39
+ # upstreamのmonthsはpadding内の位置基準。ナビのabsolute位置も
40
+ # ルートの外縁ではなくこの内容領域に合わせる。
41
+ content_tag(:div, class: "relative flex flex-col gap-4") do
42
+ navigation = Navigation.new(month: @month, caption_id: caption_id, month_path: @month_path)
43
+ safe_join([render(navigation), month_grid])
44
+ end
42
45
  end
43
46
  end
44
47
 
@@ -69,7 +69,9 @@ module Shadcn
69
69
  merge_nested(attributes, :data, { slot: contract_root_slot[:name] })
70
70
  content_tag(tag, **attributes) do
71
71
  extra = [ORIENTATION_CLASS, @user_class].compact.join(" ")
72
- content_tag(:div, class: self.class.classes(extra:)) { content }
72
+ # Emblaは初期位置にもtranslate3d(0,0,0)を付ける。描画レイヤーを
73
+ # 揃えることで、角丸と文字のアンチエイリアスもupstreamと一致する。
74
+ content_tag(:div, class: self.class.classes(extra:), style: "transform: translate3d(0px, 0px, 0px)") { content }
73
75
  end
74
76
  end
75
77
 
@@ -102,9 +104,7 @@ module Shadcn
102
104
  ORIENTATION_CLASS = "group-data-vertical/carousel:inset-y-auto " \
103
105
  "group-data-vertical/carousel:left-1/2 group-data-vertical/carousel:my-0 " \
104
106
  "group-data-vertical/carousel:-translate-x-1/2 group-data-vertical/carousel:rotate-90"
105
- RTL_ICON_CLASS = "group-[[data-orientation=horizontal][data-direction=rtl]]/carousel:rotate-180"
106
-
107
- private_constant :ORIENTATION_CLASS, :RTL_ICON_CLASS
107
+ private_constant :ORIENTATION_CLASS
108
108
 
109
109
  private
110
110
 
@@ -169,7 +169,7 @@ module Shadcn
169
169
  "stroke-linejoin": "round",
170
170
  width: "16",
171
171
  height: "16",
172
- class: "size-4 #{RTL_ICON_CLASS}",
172
+ class: "size-4",
173
173
  aria: { hidden: "true" }
174
174
  ) do
175
175
  raw(paths)
@@ -8,9 +8,13 @@ module Shadcn
8
8
  NATIVE_STATE_CLASS = T.let([
9
9
  "not-checked:border-input not-checked:bg-transparent dark:not-checked:bg-input/30",
10
10
  "checked:border-primary checked:bg-primary checked:text-primary-foreground",
11
- "group-has-[:focus-visible]/field-label:checked:border-primary dark:checked:bg-primary"
11
+ "group-has-[:focus-visible]/field-label:checked:border-primary dark:checked:bg-primary",
12
+ "disabled:opacity-100"
12
13
  ].join(" ").freeze, String)
13
14
 
15
+ # input の選択時の文字色は兄弟の indicator に継承されない。
16
+ INDICATOR_STATE_CLASS = "pointer-events-none absolute inset-0 hidden peer-checked:grid peer-checked:text-primary-foreground"
17
+
14
18
  # 契約タグは CheckboxPrimitive.Root。ネイティブな input[type=checkbox] として描く
15
19
  # (JS無しで動作 — 05-stimulus-hotwire §3「checkbox = input + CSS」)。
16
20
  # base-nova の契約クラスは data-checked / data-unchecked(属性の存在)を参照する。
@@ -44,7 +48,7 @@ module Shadcn
44
48
  sig { returns(String) }
45
49
  def indicator_element
46
50
  contract_class = T.cast(contract_slot("checkbox-indicator").dig(:static_attributes, :class), T.nilable(String))
47
- content_tag(:span, data: { slot: "checkbox-indicator" }, aria: { hidden: "true" }, class: [contract_class, "pointer-events-none absolute inset-0 hidden peer-checked:grid"].compact.join(" ")) do
51
+ content_tag(:span, data: { slot: "checkbox-indicator" }, aria: { hidden: "true" }, class: [contract_class, INDICATOR_STATE_CLASS].compact.join(" ")) do
48
52
  content_tag(
49
53
  :svg,
50
54
  xmlns: "http://www.w3.org/2000/svg",
@@ -58,15 +58,9 @@ module Shadcn
58
58
  class: [indicator_class, "pointer-events-none absolute inset-0 hidden peer-checked:flex"].compact.join(" ")
59
59
  ) do
60
60
  content_tag(
61
- :svg,
62
- xmlns: "http://www.w3.org/2000/svg",
63
- viewBox: "0 0 24 24",
64
- fill: "none",
65
- stroke: "none",
66
- class: "absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-current"
67
- ) do
68
- raw(%(<circle cx="12" cy="12" r="10"/>))
69
- end
61
+ :span,
62
+ class: "absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground"
63
+ ) { "".html_safe }
70
64
  end
71
65
  end
72
66
  end
@@ -25,7 +25,10 @@ module Shadcn
25
25
  def html_attributes
26
26
  attributes = super
27
27
  attributes[:role] = "group"
28
- merge_style(attributes, { display: "flex" })
28
+ # groupロールにaria-orientationは許可されない。縦配置はCSSで指定する。
29
+ style = { display: "flex" }
30
+ style[:flex_direction] = "column" if @orientation == "vertical"
31
+ merge_style(attributes, style)
29
32
  merge_nested(attributes, :data, { controller: CONTROLLER, orientation: @orientation })
30
33
  attributes
31
34
  end
@@ -158,7 +158,10 @@ module Shadcn
158
158
  attributes = @html_args.merge(class: self.class.classes(extra: @user_class))
159
159
  attributes[:role] = "presentation" unless attributes.key?(:role)
160
160
  attributes[:popover] = "auto" unless attributes.key?(:popover)
161
- merge_style(attributes, display: "flex", flex_direction: "column", overflow: "hidden")
161
+ # upstreamのPositionerはtransformを持つ描画レイヤー。ネイティブpopoverは
162
+ # top layerへ移るため、自身にも同じレイヤーを作り色の合成を揃える。
163
+ merge_style(attributes, display: "flex", flex_direction: "column", overflow: "hidden",
164
+ transform: "translate3d(0px, 0px, 0px)")
162
165
  merge_floating_position_data(
163
166
  attributes,
164
167
  slot: "select-content",
@@ -202,6 +205,9 @@ module Shadcn
202
205
  attributes = super
203
206
  attributes[:role] = "option"
204
207
  attributes[:tabindex] = "-1"
208
+ # upstreamは現在項目をfocusしてfocus:*を適用する。こちらはトリガーに
209
+ # focusを保持するため、同じ強調をdata-highlightedで反映する。
210
+ attributes[:class] = [attributes[:class], "data-[highlighted=true]:bg-accent data-[highlighted=true]:text-accent-foreground"].compact.join(" ")
205
211
  merge_nested(attributes, :aria, { selected: "false" }.merge(@disabled ? { disabled: "true" } : {}))
206
212
  item_data = {
207
213
  value: @value,
@@ -46,18 +46,18 @@ module Shadcn
46
46
  sig { returns(String) }
47
47
  def range_style
48
48
  if vertical?
49
- "position: absolute; bottom: 0; height: #{percentage}%"
49
+ "position: absolute; bottom: 0; height: #{edge_aligned_position}"
50
50
  else
51
- "position: absolute; left: 0; width: #{percentage}%"
51
+ "position: absolute; left: 0; width: #{edge_aligned_position}"
52
52
  end
53
53
  end
54
54
 
55
55
  sig { returns(String) }
56
56
  def thumb_style
57
57
  if vertical?
58
- "position: absolute; bottom: #{percentage}%; left: 50%; transform: translate(-50%, 50%)"
58
+ "position: absolute; bottom: #{edge_aligned_position}; left: 50%; transform: translate(-50%, 50%)"
59
59
  else
60
- "position: absolute; left: #{percentage}%; top: 50%; transform: translate(-50%, -50%)"
60
+ "position: absolute; left: #{edge_aligned_position}; top: 50%; transform: translate(-50%, -50%)"
61
61
  end
62
62
  end
63
63
 
@@ -90,8 +90,20 @@ module Shadcn
90
90
 
91
91
  sig { returns(String) }
92
92
  def percentage
93
- result = ((effective_value - decimal(@min)) / (decimal(@max) - decimal(@min))) * 100
94
- format("%.12g", result.to_f)
93
+ format("%.12g", (fraction * 100).to_f)
94
+ end
95
+
96
+ # upstreamのthumbAlignment="edge"は12pxのつまみ中心を、両端6pxを
97
+ # 除いた範囲内で動かす。rangeの終端も同じ位置に合わせる。
98
+ sig { returns(String) }
99
+ def edge_aligned_position
100
+ offset = 6 - (12 * fraction)
101
+ "calc(#{percentage}% + #{format('%.12g', offset.to_f)}px)"
102
+ end
103
+
104
+ sig { returns(Rational) }
105
+ def fraction
106
+ (effective_value - decimal(@min)) / (decimal(@max) - decimal(@min))
95
107
  end
96
108
 
97
109
  sig { returns(Rational) }
@@ -25,7 +25,7 @@ module Shadcn
25
25
  "stroke-linejoin": "round",
26
26
  **html_attributes
27
27
  ) do
28
- raw(%(<path d="M21 12a9 9 0 1 1-9-9"/>))
28
+ raw(%(<path d="M21 12a9 9 0 1 1-6.219-8.56"/>))
29
29
  end
30
30
  end
31
31
  end
@@ -37,7 +37,10 @@ module Shadcn
37
37
 
38
38
  sig { override.returns(String) }
39
39
  def call
40
- content_tag(:span, class: "relative inline-flex") do
40
+ # 装飾をflex内の通常要素にして、upstreamと同じ行内ボックスの基準位置を使う。
41
+ # inputは外枠に重ね、ネイティブのチェック状態とフォーム値を保持する。
42
+ wrapper_size = @size == "sm" ? "h-[14px] w-[24px]" : "h-[18.4px] w-[32px]"
43
+ content_tag(:span, class: "relative inline-flex shrink-0 items-center rounded-full border border-transparent #{wrapper_size}") do
41
44
  safe_join([input_element, thumb_element])
42
45
  end
43
46
  end
@@ -51,6 +54,8 @@ module Shadcn
51
54
  # 背景/角丸の上にOS標準の箱を塗る(dark時は白箱として視覚差になる)ため消す。
52
55
  # 契約の data-[state] 系背景クラスを素の要素に効かせるために必要
53
56
  attributes[:class] = [attributes[:class], "appearance-none", NATIVE_STATE_CLASS].compact.join(" ")
57
+ # 外枠の透明な1px borderを覆い、上流のrootと同じ描画位置に置く。
58
+ merge_style(attributes, position: "absolute", top: "-1px", left: "-1px")
54
59
  merge_native_checked_state(attributes, checked: native_checked?(@html_args))
55
60
  void_tag("input", **attributes)
56
61
  end
@@ -63,7 +68,7 @@ module Shadcn
63
68
  # 発火しない。sizeはコンポーネント側で明示する(契約のdata-size値と同じ)
64
69
  size_class = @size == "sm" ? "size-3" : "size-4"
65
70
  decoration = [
66
- "pointer-events-none absolute left-0.5 top-1/2 -translate-y-1/2 translate-x-0",
71
+ "pointer-events-none relative",
67
72
  "peer-checked:translate-x-[calc(100%-2px)] peer-not-checked:translate-x-0",
68
73
  "dark:peer-checked:bg-primary-foreground dark:peer-not-checked:bg-foreground transition-transform",
69
74
  size_class
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module ShadcnViewComponents
5
- VERSION = "0.2.1"
5
+ VERSION = "0.2.2"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shadcn_view_components
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - supermomonga
@@ -441,7 +441,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
441
441
  - !ruby/object:Gem::Version
442
442
  version: '0'
443
443
  requirements: []
444
- rubygems_version: 4.0.16
444
+ rubygems_version: 4.0.20
445
445
  specification_version: 4
446
446
  summary: shadcn/ui components as Rails ViewComponents with a deterministic sync pipeline
447
447
  test_files: []