@moontra/moonui 6.19.0 → 6.19.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.
@@ -1,5 +1,6 @@
1
1
  import React from 'react'
2
2
  import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { renderToStaticMarkup } from 'react-dom/server'
3
4
  import '@testing-library/jest-dom'
4
5
  import {
5
6
  Table,
@@ -101,19 +102,21 @@ describe('Table Components', () => {
101
102
  render(
102
103
  <Table variant="striped" data-testid="table">
103
104
  <TableBody data-testid="tbody">
104
- <TableRow>
105
+ <TableRow data-testid="row">
105
106
  <TableCell>Test</TableCell>
106
107
  </TableRow>
107
108
  </TableBody>
108
109
  </Table>
109
110
  )
110
-
111
+
111
112
  const table = screen.getByTestId('table')
112
113
  expect(table).toHaveClass('dark:text-gray-200')
113
-
114
- // TableBody should have the striped class applied
114
+
115
115
  const tbody = screen.getByTestId('tbody')
116
116
  expect(tbody).toHaveClass('[&_tr:last-child]:border-0')
117
+
118
+ // #444: zebra SATIR düzeyinde uygulanır — tbody'de değil.
119
+ expect(screen.getByTestId('row')).toHaveClass('odd:bg-muted/50')
117
120
  })
118
121
 
119
122
  it('renders card variant correctly', () => {
@@ -761,21 +764,232 @@ describe('Table Components', () => {
761
764
  render(
762
765
  <Table variant="striped" data-testid="table">
763
766
  <TableBody data-testid="tbody">
764
- <TableRow>
767
+ <TableRow data-testid="row-1">
765
768
  <TableCell>Row 1</TableCell>
766
769
  </TableRow>
767
- <TableRow>
770
+ <TableRow data-testid="row-2">
768
771
  <TableCell>Row 2</TableCell>
769
772
  </TableRow>
770
773
  </TableBody>
771
774
  </Table>
772
775
  )
773
-
776
+
774
777
  const table = screen.getByTestId('table')
775
778
  expect(table).toHaveClass('dark:text-gray-200')
776
-
779
+
777
780
  const tbody = screen.getByTestId('tbody')
778
781
  expect(tbody).toHaveClass('[&_tr:last-child]:border-0')
782
+
783
+ // #444: her gövde satırı zebra sınıfını taşır; boyamayı `:nth-child(odd)` seçer.
784
+ expect(screen.getByTestId('row-1')).toHaveClass('odd:bg-muted/50')
785
+ expect(screen.getByTestId('row-2')).toHaveClass('odd:bg-muted/50')
786
+ })
787
+ })
788
+
789
+ /**
790
+ * #444 — `variant="striped"` HİÇ uygulanmıyordu.
791
+ *
792
+ * Kök neden: `Table`, çocuklarını gezip `child.type === "tbody"` diye STRING
793
+ * karşılaştırıyordu; `TableBody` bir `forwardRef` nesnesi olduğu için koşul daima
794
+ * false'tu. İkinci kusur: hedeflenen `even:[&>tr]:bg-muted/50` sınıfı tbody'nin
795
+ * kendi kardeş sırasına bakıyordu, satırlara değil.
796
+ *
797
+ * Bu blok "sessiz eşitliği" kilitler: striped çıktısı default'tan GERÇEKTEN
798
+ * farklı olmalı. Aşağıdaki testler düzeltme geri alındığında kırmızıya döner.
799
+ */
800
+ describe('Striped Variant (#444)', () => {
801
+ const rows = (
802
+ <>
803
+ <TableRow>
804
+ <TableCell>Row 1</TableCell>
805
+ </TableRow>
806
+ <TableRow>
807
+ <TableCell>Row 2</TableCell>
808
+ </TableRow>
809
+ <TableRow>
810
+ <TableCell>Row 3</TableCell>
811
+ </TableRow>
812
+ </>
813
+ )
814
+
815
+ const markupFor = (variant: 'default' | 'striped') =>
816
+ renderToStaticMarkup(
817
+ <Table variant={variant}>
818
+ <TableBody>{rows}</TableBody>
819
+ </Table>
820
+ )
821
+
822
+ it('produces markup that actually differs from the default variant', () => {
823
+ const striped = markupFor('striped')
824
+ const plain = markupFor('default')
825
+
826
+ // #444'ün ta kendisi: bu iki çıktı eskiden BİREBİR aynıydı.
827
+ expect(striped).not.toBe(plain)
828
+ expect(striped).toContain('odd:bg-muted/50')
829
+ expect(plain).not.toContain('odd:bg-muted/50')
830
+ })
831
+
832
+ it('never emits the old tbody-level selector', () => {
833
+ // Eski (çalışmayan) desen: tbody'nin kardeş sırasına bakan `even:` seçicisi.
834
+ expect(markupFor('striped')).not.toContain('even:[&>tr]:bg-muted/50')
835
+ })
836
+
837
+ it('applies the zebra class to rows, not to the tbody', () => {
838
+ render(
839
+ <Table variant="striped">
840
+ <TableBody data-testid="tbody">
841
+ <TableRow data-testid="row-1">
842
+ <TableCell>Row 1</TableCell>
843
+ </TableRow>
844
+ <TableRow data-testid="row-2">
845
+ <TableCell>Row 2</TableCell>
846
+ </TableRow>
847
+ </TableBody>
848
+ </Table>
849
+ )
850
+
851
+ expect(screen.getByTestId('tbody').className).not.toMatch(/bg-muted/)
852
+ expect(screen.getByTestId('row-1')).toHaveClass('odd:bg-muted/50')
853
+ expect(screen.getByTestId('row-2')).toHaveClass('odd:bg-muted/50')
854
+ })
855
+
856
+ it('does not stripe rows in non-striped variants', () => {
857
+ const { rerender } = render(
858
+ <Table variant="default">
859
+ <TableBody>
860
+ <TableRow data-testid="row">
861
+ <TableCell>Row</TableCell>
862
+ </TableRow>
863
+ </TableBody>
864
+ </Table>
865
+ )
866
+ expect(screen.getByTestId('row')).not.toHaveClass('odd:bg-muted/50')
867
+
868
+ rerender(
869
+ <Table variant="bordered">
870
+ <TableBody>
871
+ <TableRow data-testid="row">
872
+ <TableCell>Row</TableCell>
873
+ </TableRow>
874
+ </TableBody>
875
+ </Table>
876
+ )
877
+ expect(screen.getByTestId('row')).not.toHaveClass('odd:bg-muted/50')
878
+ })
879
+
880
+ it('leaves header and footer rows unstriped', () => {
881
+ render(
882
+ <Table variant="striped">
883
+ <TableHeader>
884
+ <TableRow data-testid="header-row">
885
+ <TableHead>Name</TableHead>
886
+ </TableRow>
887
+ </TableHeader>
888
+ <TableBody>
889
+ <TableRow data-testid="body-row">
890
+ <TableCell>John</TableCell>
891
+ </TableRow>
892
+ </TableBody>
893
+ <TableFooter>
894
+ <TableRow data-testid="footer-row">
895
+ <TableCell>Total</TableCell>
896
+ </TableRow>
897
+ </TableFooter>
898
+ </Table>
899
+ )
900
+
901
+ expect(screen.getByTestId('body-row')).toHaveClass('odd:bg-muted/50')
902
+ // Başlık satırı ve `bg-primary` zeminli altbilgi satırı zebraya girmez.
903
+ expect(screen.getByTestId('header-row')).not.toHaveClass('odd:bg-muted/50')
904
+ expect(screen.getByTestId('footer-row')).not.toHaveClass('odd:bg-muted/50')
905
+ })
906
+
907
+ it('reaches rows through fragments, .map() and wrapper components', () => {
908
+ // Eski `React.Children.map` + tip karşılaştırması bu yapıların hiçbirinde
909
+ // çalışamazdı; bağlam tabanlı çözüm derinlikten bağımsızdır.
910
+ const RowGroup = ({ items }: { items: string[] }) => (
911
+ <>
912
+ {items.map((item) => (
913
+ <TableRow key={item} data-testid={`row-${item}`}>
914
+ <TableCell>{item}</TableCell>
915
+ </TableRow>
916
+ ))}
917
+ </>
918
+ )
919
+
920
+ render(
921
+ <Table variant="striped">
922
+ <TableBody>
923
+ <RowGroup items={['a', 'b']} />
924
+ </TableBody>
925
+ </Table>
926
+ )
927
+
928
+ expect(screen.getByTestId('row-a')).toHaveClass('odd:bg-muted/50')
929
+ expect(screen.getByTestId('row-b')).toHaveClass('odd:bg-muted/50')
930
+ })
931
+
932
+ it('also stripes rows placed inside a raw <tbody>', () => {
933
+ render(
934
+ <Table variant="striped">
935
+ <tbody>
936
+ <TableRow data-testid="row">
937
+ <TableCell>Row</TableCell>
938
+ </TableRow>
939
+ </tbody>
940
+ </Table>
941
+ )
942
+
943
+ expect(screen.getByTestId('row')).toHaveClass('odd:bg-muted/50')
944
+ })
945
+
946
+ it('keeps hover feedback distinguishable from the stripe', () => {
947
+ render(
948
+ <Table variant="striped">
949
+ <TableBody>
950
+ <TableRow data-testid="row">
951
+ <TableCell>Row</TableCell>
952
+ </TableRow>
953
+ </TableBody>
954
+ </Table>
955
+ )
956
+
957
+ const row = screen.getByTestId('row')
958
+ // Varsayılan hover (`bg-muted/50`) şeritle AYNI renk olurdu → görünmez hover.
959
+ expect(row).toHaveClass('hover:bg-accent')
960
+ expect(row).not.toHaveClass('hover:bg-muted/50')
961
+ })
962
+
963
+ it('keeps the selected state distinct from the stripe colour', () => {
964
+ render(
965
+ <Table variant="striped">
966
+ <TableBody>
967
+ <TableRow data-testid="row" data-state="selected">
968
+ <TableCell>Row</TableCell>
969
+ </TableRow>
970
+ </TableBody>
971
+ </Table>
972
+ )
973
+
974
+ const row = screen.getByTestId('row')
975
+ // Şerit `bg-muted/50`, seçili `bg-muted` — tam opaklık BİLEREK şeritte
976
+ // kullanılmadı, yoksa seçili satır seçilmemişten ayırt edilemezdi.
977
+ expect(row).toHaveClass('data-[state=selected]:bg-muted')
978
+ expect(row).toHaveClass('odd:bg-muted/50')
979
+ })
980
+
981
+ it('still lets consumer classes win over the stripe', () => {
982
+ render(
983
+ <Table variant="striped">
984
+ <TableBody>
985
+ <TableRow className="bg-red-500" data-testid="row">
986
+ <TableCell>Row</TableCell>
987
+ </TableRow>
988
+ </TableBody>
989
+ </Table>
990
+ )
991
+
992
+ expect(screen.getByTestId('row')).toHaveClass('bg-red-500', 'odd:bg-muted/50')
779
993
  })
780
994
  })
781
995
 
@@ -84,8 +84,31 @@ const progressIndicatorVariants = cva(
84
84
  // Regresyon: __tests__/progress-secondary-contrast.test.ts
85
85
  secondary: "bg-[rgb(var(--secondary-500))]",
86
86
  success: "bg-success",
87
- warning: "bg-warning",
88
- error: "bg-error",
87
+ // #424 (#372'nin kardeşi): dolgu `bg-warning`/`bg-error` idi — track
88
+ // (`bg-warning/20` · `bg-error/20`) AYNI token'ın alfalı hâli olduğundan
89
+ // LIGHT temada ayrım kayboluyordu: warning 1.83 · error 2.89 (eşik 3:1,
90
+ // WCAG 1.4.11). DARK tema zaten temizdi (warning 6.97 · error 4.46).
91
+ //
92
+ // KÖK NEDEN: `--warning` ve `--error` tokens.css'te `.dark` bloğunda
93
+ // EZİLMİYOR — iki temada aynı renk. Kardeşleri `--success` (light 29% →
94
+ // dark 36%) ve `--primary` tema-uyarlanabilir olduğu için eşiği geçiyor;
95
+ // bu iki aile o uyarlamadan yoksun kaldığı için light'ta kalıyor.
96
+ //
97
+ // ÇÖZÜM: eksik tema-uyarlamasını component düzeyinde ver — light'ta
98
+ // skalanın -700 kademesine in, dark'ta semantik token'a DOKUNMA.
99
+ // Tema-KÖR bir -700 ataması dark'ı kırardı (docs kartında warning 2.50 ✗
100
+ // · error 2.23 ✗); `dark:` ayrımı tam olarak bunu önler ve dark render'ı
101
+ // bit-bit aynı bırakır. Aynı desen badge.tsx:51-63'te zaten kullanılıyor.
102
+ // Track alfaları BİLEREK `/20` kaldı: altı varyantın tamamı o konvansiyonu
103
+ // paylaşıyor ve alfayı düşürmek track↔zemin ayrımını zayıflatırdı
104
+ // (warning 1.162 → 1.081). Ölçülen sonuç — warning: light 4.24 ✓ /
105
+ // dark 6.97 ✓ (docs kartı #111827: 5.77) · error: light 4.95 ✓ /
106
+ // dark 4.46 ✓ (docs kartı 3.82). Skala token'ları RGB üçlüsüdür
107
+ // (semantiklerin HSL'inden farklı) → `rgb(var(--x-700))` kompozisyonu;
108
+ // `bg-warning-700` YAZILMADI, preset o sınıfı üretmez (#307 ölü-sınıf).
109
+ // Regresyon: __tests__/progress-warning-error-contrast.test.ts
110
+ warning: "bg-[rgb(var(--warning-700))] dark:bg-warning",
111
+ error: "bg-[rgb(var(--error-700))] dark:bg-error",
89
112
  },
90
113
  animation: {
91
114
  default: "transition-all duration-500",
@@ -72,10 +72,70 @@ const sliderRangeVariants = cva(
72
72
  // tüketici --secondary-500'ü ezerek özelleştirebilir.
73
73
  // Regresyon: __tests__/slider-secondary-contrast.test.ts
74
74
  secondary: "bg-[rgb(var(--secondary-500))]",
75
- accent: "bg-accent",
75
+ // #448 (#389/#407'nin `accent` kardeşi — ama ÇÖZÜM SINIFI farklı): range
76
+ // `bg-accent`, track `bg-accent/20` idi — aynı yüzey token'ının iki alfası.
77
+ // `--accent` HER İKİ temada da kusurluydu (warning/error'dan ağır: onlar
78
+ // yalnız light'ta kusurluydu): A ekseni light 1.123 · dark 1.303 · gerçek
79
+ // docs kartı (#111827) 1.162. Eşik 3:1 (WCAG 1.4.11 non-text).
80
+ //
81
+ // KÖK NEDEN: `--accent` bir YÜZEY token'ı — `hover:bg-accent` tinti olarak
82
+ // tasarlanmış ve iki temada da kendi zeminine BİTİŞİK duruyor (light
83
+ // rgb(235,239,245) beyaz üstünde, dark rgb(29,40,58) rgb(3,7,17) üstünde).
84
+ // Dolgu/kenarlık rolüne yapısal olarak uygun değil; bu yüzden track'i
85
+ // değiştirerek onarmak da MÜMKÜN DEĞİL (light'ta accent'ten 3:1 uzakta
86
+ // duran daha açık bir yüzey yok).
87
+ //
88
+ // #389'un tek-orta-ton kademe araması AYNEN uygulanamadı: tokens.css'te
89
+ // `--accent-50 … --accent-950` skalası YOK (skalası olan aileler:
90
+ // primary/secondary/success/warning/error/brand/brand-accent).
91
+ // `bg-accent-foreground` de elendi — `--accent-foreground` iki temada da
92
+ // `--primary` ile BİREBİR aynı değer (varyant primary klonuna dönerdi) ve
93
+ // 7 tema preset'inin 2'sinde beyaz (Creative/Nature), yani tek başına
94
+ // dolgu olamaz.
95
+ //
96
+ // ÇÖZÜM: sistemin TEK nötr skalası olan slate (`--secondary-*`) — `--accent`
97
+ // ile aynı hue ailesi (216° ↔ 215°), üstelik light temada `--accent` ile
98
+ // `--secondary` BİREBİR aynı token değerini taşıyor. Tema-kör tek kademe
99
+ // SEÇİLEMEDİ: bu ailede iki temayı birden geçen tek kademe 500'dür, o da
100
+ // `secondary` varyantının kendi rengi (ΔE*ab = 0.0 — iki varyant klon olurdu).
101
+ // Bu yüzden #442'nin `dark:` ayrımı deseni kullanıldı, tema başına ayrı kademe.
102
+ // Tam tarama (11 kademe × 2 tema × 3 zemin × 2 eksen): DARK'ta eşiği geçen VE
103
+ // kardeş varyantlardan ayırt edilebilen (ΔE*ab ≥ 10) TEK kademe 400
104
+ // (500 = secondary klonu · ≤300 `default`/`primary` ile çakışıyor · ≥600 eşiğin
105
+ // altında). LIGHT'ta {600,700,800} uygun; 700 kardeş ayrımını maksimize eder
106
+ // (ΔE 21.2 · 600→12.7 · 800→14.9).
107
+ // Ölçülen — A: light 1.123 → 10.081 ✓ · dark 1.303 → 7.533 ✓ · docs kartı
108
+ // 1.162 → 6.717 ✓. Track `bg-accent/20` DEĞİŞMEDİ (yedi varyantın ortak deseni).
109
+ // Regresyon: __tests__/slider-accent-contrast.test.ts
110
+ accent: "bg-[rgb(var(--secondary-700))] dark:bg-[rgb(var(--secondary-400))]",
76
111
  success: "bg-success",
77
- warning: "bg-warning",
78
- error: "bg-error",
112
+ // #442 (#424/PR #440'ın slider ikizi): range `bg-warning`/`bg-error` idi —
113
+ // track (`bg-warning/20` · `bg-error/20`) AYNI token'ın alfalı hâli olduğundan
114
+ // LIGHT temada ayrım kayboluyordu: warning 1.835 · error 2.893 (eşik 3:1,
115
+ // WCAG 1.4.11 non-text). DARK tema zaten temizdi (warning 6.97 · error 4.46).
116
+ //
117
+ // KÖK NEDEN: `--warning` ve `--error` tokens.css'in `.dark` bloğunda
118
+ // EZİLMİYOR — iki temada aynı renk. Kardeşleri tema-uyarlanabilir olduğu
119
+ // için geçiyor (`--success` light %29 → dark %36; ölçüm: success A ekseni
120
+ // light 3.73 / dark 4.83). Bu iki aile o uyarlamadan yoksun.
121
+ //
122
+ // ÇÖZÜM: eksik tema-uyarlamasını component düzeyinde ver — light'ta skalanın
123
+ // -700 kademesine in, dark'ta semantik token'a DOKUNMA (dark render bit-bit
124
+ // aynı kalır, regresyon yüzeyi sıfır). Tema-KÖR bir -700 ataması dark'ı
125
+ // kırardı: gerçek docs kartı (#111827) üstünde warning 2.499 ✗ · error 2.232 ✗.
126
+ // Tam skala taraması (11 kademe × 2 aile × 3 zemin × 2 tema) track `/20` iken
127
+ // iki temayı birden geçen kademe kümesini BOŞ buluyor → `dark:` ayrımı ZORUNLU;
128
+ // #389'un tek-orta-ton (`--secondary-500`) çözümü burada uygulanamaz, çünkü
129
+ // orada `--secondary` temaya göre değişiyordu. Aynı `dark:` deseni badge.tsx:51-63
130
+ // ve progress.tsx:110-111'de zaten kullanılıyor.
131
+ // Ölçülen sonuç — warning: light 1.835 → 4.235 ✓ · error: light 2.893 → 4.950 ✓
132
+ // (dark değişmedi: 6.97 / 4.46; docs kartı 5.77 / 3.82).
133
+ // Skala token'ları RGB üçlüsüdür (semantiklerin HSL'inden farklı) →
134
+ // `rgb(var(--x-700))` kompozisyonu; `bg-warning-700` YAZILMADI, preset bu
135
+ // ailelere sayısal skala açmıyor (#307 ölü-sınıf tuzağı).
136
+ // Regresyon: __tests__/slider-warning-error-contrast.test.ts
137
+ warning: "bg-[rgb(var(--warning-700))] dark:bg-warning",
138
+ error: "bg-[rgb(var(--error-700))] dark:bg-error",
79
139
  },
80
140
  },
81
141
  defaultVariants: {
@@ -103,10 +163,36 @@ const sliderThumbVariants = cva(
103
163
  // aynı kademe: tutamaç ve dolgu tek renkte okunur.
104
164
  // Regresyon: __tests__/slider-secondary-contrast.test.ts
105
165
  secondary: "border-[rgb(var(--secondary-500))] bg-background",
106
- accent: "border-accent bg-background",
166
+ // #448 EKSEN B (range'den FARKLI eksen: bileşen↔sayfa-zemini) — tutamacın
167
+ // dolgusu `bg-background`, yani zeminin kendisi; ayırt edilebilirliğini
168
+ // YALNIZ kenarlık taşır → non-text UI bileşeni, WCAG 1.4.11 ≥3:1.
169
+ // `border-accent` iki temada da kayboluyordu: light 1.154 · dark 1.359 ·
170
+ // gerçek docs kartı 1.197. Range ile AYNI kademeler seçildi — #407'nin
171
+ // gerekçesi burada da geçerli: "tutamaç ve dolgu tek renkte okunur".
172
+ // Ölçülen — light 1.154 → 10.355 ✓ · dark 1.359 → 7.857 ✓ · docs kartı
173
+ // 1.197 → 6.919 ✓.
174
+ // Regresyon: __tests__/slider-accent-contrast.test.ts
175
+ accent: "border-[rgb(var(--secondary-700))] dark:border-[rgb(var(--secondary-400))] bg-background",
107
176
  success: "border-success bg-background",
108
- warning: "border-warning bg-background",
109
- error: "border-error bg-background",
177
+ // #442 EKSEN B (range'den FARKLI eksen: bileşen↔sayfa-zemini) — #407'de
178
+ // `secondary` için açılan eksenin warning/error karşılığı. Tutamacın dolgusu
179
+ // `bg-background`, yani zeminin kendisi; ayırt edilebilirliği YALNIZ kenarlık
180
+ // taşır → non-text UI bileşeni, WCAG 1.4.11 ≥3:1.
181
+ // ÖLÇÜLEN (eski değerler): warning light **2.133 ✗** · error light 3.781 ✓;
182
+ // dark ikisi de rahat geçiyordu (9.45 / 5.33).
183
+ //
184
+ // `warning` KUSURLUYDU, düzeltildi. `error` bu eksende zaten geçiyordu ama
185
+ // yine de aynı ifadeye çekildi: range light'ta -700'e indiği için kenarlık
186
+ // `border-error`da bırakılsaydı LIGHT temada dolgu rgb(185,28,28), tutamaç
187
+ // rgb(239,67,67) olurdu — düzeltmenin KENDİSİ iki-tonlu bir görünüm üretirdi.
188
+ // #407'nin gerekçesi birebir geçerli: "tutamaç ve dolgu tek renkte okunur".
189
+ // Değişim kontrastı düşürmüyor, artırıyor: error light 3.781 → 6.470.
190
+ // Dark'ta `dark:border-*` semantik token'ı koruduğu için render bit-bit aynı.
191
+ // Ölçülen sonuç — warning: light 2.133 → 4.923 ✓ · error: light 3.781 → 6.470 ✓
192
+ // (dark değişmedi: 9.45 / 5.33; docs kartı 8.32 / 4.69).
193
+ // Regresyon: __tests__/slider-warning-error-contrast.test.ts
194
+ warning: "border-[rgb(var(--warning-700))] dark:border-warning bg-background",
195
+ error: "border-[rgb(var(--error-700))] dark:border-error bg-background",
110
196
  },
111
197
  size: {
112
198
  sm: "h-3 w-3",
@@ -34,12 +34,51 @@ const switchVariants = cva(
34
34
  md: "h-6 w-11",
35
35
  lg: "h-7 w-14",
36
36
  },
37
+ // #454 — checked track'in İKİ ekseni de ölçüldü (eşik 3:1, WCAG 1.4.11
38
+ // non-text UI). Switch'te bu iki eksen AYNI sayıyı verir, çünkü thumb
39
+ // `bg-background` taşır (switchThumbVariants tabanı):
40
+ // A) checked track ↔ sayfa/kart zemini → kontrol "açık" okunuyor mu?
41
+ // B) thumb ↔ checked track → tutamacın KONUMU görülüyor mu?
42
+ // Ölçüm zeminleri: `--background` · `--card` · docs kabuğunun gerçek koyu
43
+ // kartı (#111827). Regresyon: __tests__/switch-variant-contrast.test.tsx
37
44
  variant: {
38
- primary: "data-[state=checked]:bg-primary",
39
- success: "data-[state=checked]:bg-success",
40
- warning: "data-[state=checked]:bg-warning",
41
- destructive: "data-[state=checked]:bg-error",
42
- secondary: "data-[state=checked]:bg-accent",
45
+ primary: "data-[state=checked]:bg-primary", // light 4.920 · dark 19.256 ✓
46
+ success: "data-[state=checked]:bg-success", // light 4.904 · dark 6.043 ✓
47
+ // #454 kardeş kusuru (#424/PR #440 progress · #442/PR #446 slider ile AYNI
48
+ // sınıf, üçüncü component): `--warning` tokens.css'in `.dark` bloğunda
49
+ // EZİLMİYOR, yani iki temada da aynı turuncu. Dark'ta zemin koyu olduğu
50
+ // için geçiyor (9.446), LIGHT'ta beyaz üstünde 2.133 ✗ — açık temada
51
+ // checked warning switch'in tutamacı da kayboluyordu (B ekseni 2.133).
52
+ // ÇÖZÜM (emsalle birebir aynı şekil): eksik tema-uyarlamasını component
53
+ // düzeyinde ver — light'ta skalanın -700 kademesine in, dark'ta semantik
54
+ // token'a DOKUNMA (dark render bit-bit aynı kalır, regresyon yüzeyi sıfır).
55
+ // Light'ta eşiği geçen kademe kümesi {700,800,900,950}; en açık olanı
56
+ // seçildi (turuncu kimliğini en çok koruyan). Ölçülen: 2.133 → 4.923 ✓.
57
+ warning:
58
+ "data-[state=checked]:bg-[rgb(var(--warning-700))] dark:data-[state=checked]:bg-warning",
59
+ destructive: "data-[state=checked]:bg-error", // light 3.781 · dark 5.328 ✓ (dokunulmadı)
60
+ // #454 asıl kusuru: `secondary` varyantı `bg-accent` kullanıyordu — ADI
61
+ // `secondary`, TOKEN'ı `accent`. `--accent` bir YÜZEY token'ı (`hover:bg-accent`
62
+ // tinti) ve iki temada da kendi zeminine bitişik: light rgb(235,239,245)
63
+ // beyaz üstünde 1.154 · dark rgb(29,40,58) 1.359 · docs kartı 1.197.
64
+ // Yani checked switch hem sayfadan hem de kendi tutamacından ayrışmıyordu:
65
+ // "açık" durum GÖRÜNMÜYORDU (switch'in tek işi durumu göstermek).
66
+ //
67
+ // AD ↔ TOKEN KARARI: varyant kendi ailesine, `--secondary-*` slate skalasına
68
+ // bağlandı. `bg-secondary` (semantik) BİLEREK seçilmedi — light'ta `--accent`
69
+ // ile BİREBİR aynı değer (1.154), dark'ta daha da kötü (1.007). Skala
70
+ // taraması (11 kademe × 2 tema × 3 zemin × 2 eksen + kardeş ΔE*ab ≥ 10 klon
71
+ // filtresi): light {500,600,700,800,900,950} · dark {300,400,500} → iki
72
+ // temayı birden geçen TEK kademe **500**, yani `dark:` ayrımına gerek yok.
73
+ // #448'in slider'daki 700/400 ayrımı burada GEREKMEZ ve İSTENMEZ: orada 500
74
+ // elenmişti çünkü slider'ın `secondary` varyantı zaten o rengi kullanıyordu
75
+ // (ΔE = 0.0 → klon). Switch'in `accent` adlı bir varyantı YOK, dolayısıyla
76
+ // 500 serbest ve kardeşlerden ayrık (ΔE: primary 69.5/51.6 · success
77
+ // 63.6/74.8 · warning 96.0 · destructive 85.0 — hepsi ≥ 10).
78
+ // Yan fayda: slider'ın `secondary` varyantı da #389/PR #406'da aynı kademeye
79
+ // sabitlenmişti → aynı varyant adı artık iki component'te aynı rengi veriyor.
80
+ // Ölçülen: A/B light 1.154 → 4.759 ✓ · dark 1.359 → 4.234 ✓ · kart 1.197 → 3.728 ✓.
81
+ secondary: "data-[state=checked]:bg-[rgb(var(--secondary-500))]",
43
82
  },
44
83
  },
45
84
  defaultVariants: {
@@ -26,6 +26,51 @@ interface TableSortContextValue {
26
26
 
27
27
  const TableSortContext = React.createContext<TableSortContextValue | null>(null);
28
28
 
29
+ /**
30
+ * Zebra (striped) bağlamı (#444).
31
+ *
32
+ * ESKİ HÂLİ İKİ AYRI NEDENLE ÇALIŞMIYORDU:
33
+ * 1. `Table`, çocuklarını gezip `child.type === "tbody"` diye STRING
34
+ * karşılaştırıyordu. `TableBody` bir `React.forwardRef` nesnesidir; bu koşul
35
+ * hiçbir zaman doğru olmaz → `striped` sınıfı HİÇ eklenmiyordu (ölçüldü:
36
+ * `variant="striped"` çıktısı `variant="default"` ile birebir aynıydı).
37
+ * 2. Koşul düzelse bile hedeflenen sınıf yanlıştı: `even:[&>tr]:bg-muted/50`
38
+ * içindeki `even:` tbody'nin KENDİ kardeş sırasına bakar (tek tbody varsa hiç
39
+ * eşleşmez), satırlara değil.
40
+ *
41
+ * Artık `Table` yalnızca bir bayrak YAYINLAR; zebra sınıfını `TableRow` kendi
42
+ * üstüne koyar. Böylece eleman-tipi karşılaştırması tamamen kalkar: satırlar
43
+ * fragment, `.map()`, ara sarmalayıcı bir component ya da düz `<tbody>` içinden
44
+ * gelse de çalışır. `TableHeader`/`TableFooter` bayrağı `false`'a çevirir —
45
+ * başlık satırı ve `bg-primary` zeminli altbilgi satırı çizgilenmemelidir.
46
+ */
47
+ const TableStripedContext = React.createContext(false);
48
+
49
+ /**
50
+ * Zebra + etkileşim yüzeyleri (#444) — ölçülmüş değerler, tokens.css'e dokunulmadan.
51
+ *
52
+ * `odd:bg-muted/50` → satır düzeyi zebra. `:nth-child(odd)` satırın kendi
53
+ * kardeş sırasına bakar, yani gerçekten 1./3./5. GÖVDE SATIRI boyanır.
54
+ *
55
+ * `hover:bg-accent` → zebra açıkken ZORUNLU: `TableRow`'un varsayılan hover'ı
56
+ * zaten `bg-muted/50`, yani şeritle AYNI renk; override olmasa çizgili satırlarda
57
+ * hover geri bildirimi görünmez olurdu. `--accent` bu tasarım sisteminin kanonik
58
+ * etkileşim yüzeyidir (button ghost/outline, dropdown item aynı token'ı kullanır).
59
+ * `cn()` = twMerge olduğu için `hover:bg-muted/50` ile çakışma CSS sırasına değil,
60
+ * deterministik olarak sona yazılan sınıfa göre çözülür.
61
+ *
62
+ * `bg-muted` (tam opaklık) BİLEREK seçilmedi: `data-[state=selected]:bg-muted` ile
63
+ * birebir aynı renk olurdu ve çizgili tabloda seçili satır seçilmemişten
64
+ * ayırt edilemezdi. Ölçülen CIE L* değerleri (zemin → şerit → seçili → hover):
65
+ * açık tema : 100 → 98.06 → 96.12 → 94.09 (şerit ΔL* 1.94, kontrast 1.05:1)
66
+ * koyu tema : 1.89 → 9.61 → 18.96 → 16.06 (şerit ΔL* 7.72, kontrast 1.17:1)
67
+ * Dört yüzey de her iki temada ayrı; hover her satır paritesinde görünür değişir.
68
+ * WCAG 1.4.11'in 3:1 eşiği zebraya UYGULANMAZ — şerit dekoratif bir tarama
69
+ * yardımıdır, içeriği anlamak için gerekli bir grafik ya da UI bileşeni değildir
70
+ * (veri hücre metninde; metin kontrastı her iki temada AA üstünde kalır).
71
+ */
72
+ const STRIPED_ROW_CLASSES = "odd:bg-muted/50 hover:bg-accent";
73
+
29
74
  /** asc → desc → null → asc döngüsü */
30
75
  function nextSortDirection(current: SortDirection): SortDirection {
31
76
  if (current === "asc") return "desc";
@@ -114,9 +159,10 @@ const Table = React.forwardRef<
114
159
  onRowSelectionChange: _onRowSelectionChange,
115
160
  disableRowSelection: _disableRowSelection,
116
161
  getRowId: _getRowId,
117
- ...props
162
+ children,
163
+ ...props
118
164
  }, ref) => {
119
- // Apply striped styles to the tbody via a class name
165
+ // #444: zebra bir bayrak olarak yayınlanır; sınıfı TableRow kendi üstüne koyar.
120
166
  const striped = variant === "striped";
121
167
 
122
168
  // #321: sıralama prop'ları alt başlıklara bağlam üzerinden ulaşır
@@ -124,21 +170,7 @@ const Table = React.forwardRef<
124
170
  () => ({ sortColumn, sortDirection, onSortChange }),
125
171
  [sortColumn, sortDirection, onSortChange]
126
172
  );
127
-
128
- // Çocukları güvenli bir şekilde işle ve tip kontrollerini doğru yap
129
- const childrenWithProps = React.Children.map(props.children, (child) => {
130
- if (React.isValidElement(child)) {
131
- if (child.type === "tbody") {
132
- // Tip güvenliği için props'ları düzgün şekilde ele alıyoruz
133
- const tbodyProps = child.props as Record<string, unknown>;
134
- return React.cloneElement(child as React.ReactElement<React.HTMLAttributes<HTMLTableSectionElement>>, {
135
- className: cn(tbodyProps.className as string | undefined, striped && "even:[&>tr]:bg-muted/50")
136
- });
137
- }
138
- }
139
- return child;
140
- });
141
-
173
+
142
174
  return (
143
175
  <div className="relative w-full overflow-auto">
144
176
  {/* Yükleniyor durumu için overlay */}
@@ -149,19 +181,20 @@ const Table = React.forwardRef<
149
181
  )}
150
182
 
151
183
  <TableSortContext.Provider value={sortContextValue}>
152
- <table
153
- ref={ref}
154
- className={cn(
155
- tableVariants({ variant, size }),
156
- loading && "opacity-70",
157
- className
158
- )}
159
- {...props}
160
- >
161
- {childrenWithProps}
162
- </table>
184
+ <TableStripedContext.Provider value={striped}>
185
+ <table
186
+ ref={ref}
187
+ className={cn(
188
+ tableVariants({ variant, size }),
189
+ loading && "opacity-70",
190
+ className
191
+ )}
192
+ {...props}
193
+ >
194
+ {children}
195
+ </table>
196
+ </TableStripedContext.Provider>
163
197
  </TableSortContext.Provider>
164
-
165
198
  </div>
166
199
  );
167
200
  });
@@ -170,8 +203,11 @@ Table.displayName = "Table";
170
203
  const TableHeader = React.forwardRef<
171
204
  HTMLTableSectionElement,
172
205
  React.HTMLAttributes<HTMLTableSectionElement>
173
- >(({ className, ...props }, ref) => (
174
- <thead ref={ref} className={cn("moonui-theme", "[&_tr]:border-b", className)} {...props} />
206
+ >(({ className, children, ...props }, ref) => (
207
+ <thead ref={ref} className={cn("moonui-theme", "[&_tr]:border-b", className)} {...props}>
208
+ {/* #444: başlık satırı zebraya dahil değildir */}
209
+ <TableStripedContext.Provider value={false}>{children}</TableStripedContext.Provider>
210
+ </thead>
175
211
  ));
176
212
  TableHeader.displayName = "TableHeader";
177
213
 
@@ -218,28 +254,37 @@ TableBody.displayName = "TableBody";
218
254
  const TableFooter = React.forwardRef<
219
255
  HTMLTableSectionElement,
220
256
  React.HTMLAttributes<HTMLTableSectionElement>
221
- >(({ className, ...props }, ref) => (
257
+ >(({ className, children, ...props }, ref) => (
222
258
  <tfoot
223
259
  ref={ref}
224
260
  className={cn("moonui-theme", "bg-primary text-primary-foreground font-medium", className)}
225
261
  {...props}
226
- />
262
+ >
263
+ {/* #444: altbilginin kendi `bg-primary` zemini var; şerit onu bozmamalı */}
264
+ <TableStripedContext.Provider value={false}>{children}</TableStripedContext.Provider>
265
+ </tfoot>
227
266
  ));
228
267
  TableFooter.displayName = "TableFooter";
229
268
 
230
269
  const TableRow = React.forwardRef<
231
270
  HTMLTableRowElement,
232
271
  React.HTMLAttributes<HTMLTableRowElement>
233
- >(({ className, ...props }, ref) => (
234
- <tr
235
- ref={ref}
236
- className={cn(
237
- "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
238
- className
239
- )}
240
- {...props}
241
- />
242
- ));
272
+ >(({ className, ...props }, ref) => {
273
+ // #444: zebra bayrağı Table'dan gelir; TableHeader/TableFooter içinde false'tur.
274
+ const striped = React.useContext(TableStripedContext);
275
+
276
+ return (
277
+ <tr
278
+ ref={ref}
279
+ className={cn(
280
+ "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
281
+ striped && STRIPED_ROW_CLASSES,
282
+ className
283
+ )}
284
+ {...props}
285
+ />
286
+ );
287
+ });
243
288
  TableRow.displayName = "TableRow";
244
289
 
245
290
  interface TableHeadProps extends React.ThHTMLAttributes<HTMLTableCellElement> {