From c97f7b72daed4d167668ac703c59702522def49b Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 18 Nov 2022 02:54:18 +0900 Subject: [PATCH 1/4] Move `Avatar` + `AvatarStack` styles to PVC (#1607) Co-authored-by: simurai --- .changeset/perfect-jokes-switch.md | 5 + .../primer/beta/avatar/default.png | Bin 2339 -> 1873 bytes .../primer/beta/avatar/focused.png | Bin 2339 -> 1873 bytes app/components/primer/beta/avatar.pcss | 73 +++++++++ app/components/primer/beta/avatar_stack.pcss | 141 ++++++++++++++++++ app/components/primer/primer.pcss | 2 + demo/app/assets/stylesheets/application.css | 1 - previews/primer/beta/avatar_preview.rb | 66 +++++++- previews/primer/beta/avatar_stack_preview.rb | 66 ++++++-- test/css/component_specific_selectors_test.rb | 15 ++ 10 files changed, 351 insertions(+), 18 deletions(-) create mode 100644 .changeset/perfect-jokes-switch.md create mode 100644 app/components/primer/beta/avatar.pcss create mode 100644 app/components/primer/beta/avatar_stack.pcss diff --git a/.changeset/perfect-jokes-switch.md b/.changeset/perfect-jokes-switch.md new file mode 100644 index 0000000000..b9cffbf482 --- /dev/null +++ b/.changeset/perfect-jokes-switch.md @@ -0,0 +1,5 @@ +--- +"@primer/view-components": patch +--- + +Move `Avatar` + `AvatarStack` styles to PVC diff --git a/.playwright/screenshots/previews.test.ts-snapshots/primer/beta/avatar/default.png b/.playwright/screenshots/previews.test.ts-snapshots/primer/beta/avatar/default.png index b98347ffd25a791fe9c7966110614e5d4395234d..d7899d720f22096b21dbb12d89f3d67721188273 100644 GIT binary patch literal 1873 zcmb_d{a4aw6#v?BZLN}8Zf>ZnWo9;}PMT4sY%L`UaYAXN6Ppu`FT)g?FOa!$I$JJu zYAX1~IZJ4+lqMm1P|2CLM6e=`@8Vl9Lu3Pi5Vd{3wm)DyXXiZU`8>be&;5Mvx%WO- zKEq?Sz7BsK0D!I7xd+d`_xwXcL z!IIhwF8BW$Tu_C}#Ok{FR@oid)@^Ou3f?3}bZ@)z-kx`-?(O_!ZswcsF@e8FMC}Mz z``+&=_x!FHA2>G1>uOCBEc-Jz;r9O4yR8$dMU|?gj(7jlRQ2A%>Jbhlx08mnu;iAD z%_bIKAMdYd*#Zgyf=Y#JLz|%&;8qO02H_4I;0i+FroF%h5bwI~09-&!`gT(x2tZJ` z>&FmslQ!&v5EtaK4Z^+L{}`@ea~TKVDNLqc9o}!X0L|{6dd5F})4yo9b(!_dnlBVC zVIX4E@ro9NVzoNs09>F+6KF_n__7)W9;_zQrJd@dyS(iIYnIB+uP$`G$Yi8N0BN=Mo0T+W~LDc#N2O}Vd0j1 z$la9u;MYKgG1_0GYXz zlak^q7DuS+!)pBMb9b9MEcjyYuv$Tn@|r*LZ8sv;SFI*CHk5Ic?>qi5SB&RqS4r{l zij@^serG{ILfceK5=1wQ(PVAR%rrH`-uxW>RH~kD(3fw6jnvN@h(mvty8ID z30R%syWrYXim^$;vdGZK<=Vl;MVd~pM~23Ov4RO2_1w90WSSd98gA+vO&gkL5ZsYN z=$(fd`{;DKqtA!j-{{2aI3_+`8hT>(Aq#GGY(dtb*ZyR5<=Vp>&pa0KlCx1wv9j>4 zeU!&EQ?Ymt#5T1t*$6DyosFmVlgQ*~0Rc;@Y&SH^%+mrGv9iYt?f95dlxE8hZNx>T z7x=u76=4##!!my}^eLQIr^@ZdI1 zNHOqelVx#*;p%k6zd3|XDQ<@|*_ ze{H^wcjS3RAoc0JE465OJ(z)b1D^uUU~;lgdU`q^4#+0(zi56v=)2x+J3Vilo!Y^sqKEz^#kI;@ESipE0{gKujf=S&X;|ThreODLbp0s8Hekh7_3>9(B1*Y zlWqa1k3OoPh_hC`FJXfb<@PbAqo=)TWrZWbeK1)zF>-lH=Yc%JK*?E_rWGAO&Rk+F zSiWvvZUZZFpoU|3wFn{tW<8yknMw-MXrqN3JSc8!y&7rTXVdx9>n3jjP z1c!;rURVG%U--K$?rZUfKhG`p1;Mlgh2Ufru`=$T(TfKA$gCyXMk24rOfpRg)yl~J zd>>J#7N2>BLZ$wwRCc;Y_+ne6al%JG3mY$IK+ zcjwYvk6cxm@s{MC~k+!h?zBKOcx zFbZ(K1(e_1e*2$A7<)2i;7cJ_v4krPPKji)7p$T4cKhRb2Dq@m)B7@LmlF<%-r+>1 zO2IM6QTqxWBw(9@oPvP_kO8T&{ Sr5Y6iWyeO~k2N1n`u1<{F-*k( literal 2339 zcmcIki&v6q9{y@-Y_e>2$JH_?+ty27(vF&%lx`PO$V(DUg&t@oijV|@bi@l-PUB3j zW-3cW+C|AjD&_?hS-fmhrl#fv6-k{&f%Sp})u%v~|+tFOQtN8NKI; z7-{wD#ikSg+WX`tYX89sN6G8W=TG^_e@WCiIm@{OA1-&Mc!oPK$aXI1aD)Z6hP$!KcET%wV!v)TT;J^fgX{Co9FL&wTlzIg z1KK4Y`&@6k94o&{47&0gbk*>{6<>6@j6zt`lt6%mPCd_FG=bLxi?(Ey`0~1a?Q#mc zl!I{wWPaO);E?E2h2}Dq>LrVFa(8zZ49{kb!Ce6CNm~|n1C89&4vrn z*2Z5m4FZfC1KOm2du*F>SvSd8(Y4bj(wCIKz04M;8EN^1#KZ$^l#`eABO1p77Y==B z{fwbxpY5kmpYDAn9~lV~&oPd|p!bFzZ_LoPusjV014L!ecX_F++jk*G9$`@VX@h=C z|ic?(rJqfkQpq+d6CKG$b4KChxjmkarMkaGTA*U82l*G zLPt`hxKE(DPV+w!zlAUJBFhTcY$chxSQL~VNNqN%j_AZq7UZqlr4CAAr&Q4ljQEquF%)bAYV0~-bg}nC(g?v7rOpb|(*>R^` zF#OD@j9SGomFnQTLo>8bW74_#!~%59$hk0S^}TJQ9DNo`mu6n3266hX6k6JP5*Unw z$eOyk!bl<_vu(Es8{fqG0wxF@#GK8Lj8X1)s}jjMC;q0S9sGVgiOihjFg>%oWpncp z{Z~Cz$H@m=hR)WG#D-H(^`I|0Jp^Aysq1+$u51+Km|Qt$LA&M`?0}Iwb~&cmuKLUUOFic;kyo-(AZ$2F=8V@4P!y2x5OrO@UUrY8t@C z^svD_BCMtf3uzgf$K^T%`$h0z-~>X%UjVDD5={AvEuUv*eh=nX2!*PY^&X3>#I_*GK_Zj0U6pp)s5}C5+L_@yi68ojHZhld&YK z6Ca&hS#=rhMq~waYFDT6Y6G6>)4Qs%gq+$ajBHRuNTgiEd?Pqn|NFHJp3Vq91Q5Kr zQ|@?hxNZnCuDT^JIAvbEGNT2Nel}`)*C*8doJ@c7Vpd7)sJu(ULx@dghn@*=-hpuG zl9D$>Y49(L{}X&0xfOy)pxHE^dXS+%?#8B0{p1CLwgxrJNHliiO$+Z5)kX4&iC)3! zGW{E$UIJv-j<$q5F2%`vq7E+%uG+2WzBQRl;^gY07^+nHf0a=4%4Mh@U-XH9=l8<( zA6{%zgSA9`Op8Hbd{Y+0hTLe2OZzKyi}CUMpcv$%UCW{#_V7<~X!@xR>Uq{7Pj7#J z*`x1ZkpcZrHv_W^DNSP(8yl-g9s4BT1A{V7xiAA)7gQ{jSY6zeL_}zd5A!72W&|{3 z5N>+O(k!mRO(j{Mo}mueIm^@Mrap;jU9X<0@j~yZ^alIgU;hiKUVO0C?TiLFhE+`Gl;^>->nrWmwFZ>J<6Yjm;T7Gq@W@7e|NK}XxKd7`+2!?EH#<5}2AP!AjsExxIzH-?~ zYVh~5+e-iKm!Dr;vHTs<63Bk?T1CGMkYb7?{ZN`mN2wf*Gk+#xGM@iTj4 zD9_4d6@-!?4&t9~J1BTm^gn8=tMAenGl~uvCt>PqERcK#8V@K;m29Q8=tNTqvR*QI zeW`u=j#d4$GhKpScFntcCu^~hj251Ik;cQrqj7OfMAm=ae%9(Zy=0oc9=O9Ql;p=F z`Pw1Hg>i{NNY|40%-h%Q;;ML3sT5PruBfOe#Z@^Zxv#O7j^zO#oN z&1PmkfyoS_)0Mt<-Gj5D!8p-Q*?M@lJ;~n}*Z{AZYr{w3nz6%q?ZY2m7bjeC;}-?> Q021a!hD0709=&k$zd3MUPXGV_ diff --git a/.playwright/screenshots/previews.test.ts-snapshots/primer/beta/avatar/focused.png b/.playwright/screenshots/previews.test.ts-snapshots/primer/beta/avatar/focused.png index b98347ffd25a791fe9c7966110614e5d4395234d..d7899d720f22096b21dbb12d89f3d67721188273 100644 GIT binary patch literal 1873 zcmb_d{a4aw6#v?BZLN}8Zf>ZnWo9;}PMT4sY%L`UaYAXN6Ppu`FT)g?FOa!$I$JJu zYAX1~IZJ4+lqMm1P|2CLM6e=`@8Vl9Lu3Pi5Vd{3wm)DyXXiZU`8>be&;5Mvx%WO- zKEq?Sz7BsK0D!I7xd+d`_xwXcL z!IIhwF8BW$Tu_C}#Ok{FR@oid)@^Ou3f?3}bZ@)z-kx`-?(O_!ZswcsF@e8FMC}Mz z``+&=_x!FHA2>G1>uOCBEc-Jz;r9O4yR8$dMU|?gj(7jlRQ2A%>Jbhlx08mnu;iAD z%_bIKAMdYd*#Zgyf=Y#JLz|%&;8qO02H_4I;0i+FroF%h5bwI~09-&!`gT(x2tZJ` z>&FmslQ!&v5EtaK4Z^+L{}`@ea~TKVDNLqc9o}!X0L|{6dd5F})4yo9b(!_dnlBVC zVIX4E@ro9NVzoNs09>F+6KF_n__7)W9;_zQrJd@dyS(iIYnIB+uP$`G$Yi8N0BN=Mo0T+W~LDc#N2O}Vd0j1 z$la9u;MYKgG1_0GYXz zlak^q7DuS+!)pBMb9b9MEcjyYuv$Tn@|r*LZ8sv;SFI*CHk5Ic?>qi5SB&RqS4r{l zij@^serG{ILfceK5=1wQ(PVAR%rrH`-uxW>RH~kD(3fw6jnvN@h(mvty8ID z30R%syWrYXim^$;vdGZK<=Vl;MVd~pM~23Ov4RO2_1w90WSSd98gA+vO&gkL5ZsYN z=$(fd`{;DKqtA!j-{{2aI3_+`8hT>(Aq#GGY(dtb*ZyR5<=Vp>&pa0KlCx1wv9j>4 zeU!&EQ?Ymt#5T1t*$6DyosFmVlgQ*~0Rc;@Y&SH^%+mrGv9iYt?f95dlxE8hZNx>T z7x=u76=4##!!my}^eLQIr^@ZdI1 zNHOqelVx#*;p%k6zd3|XDQ<@|*_ ze{H^wcjS3RAoc0JE465OJ(z)b1D^uUU~;lgdU`q^4#+0(zi56v=)2x+J3Vilo!Y^sqKEz^#kI;@ESipE0{gKujf=S&X;|ThreODLbp0s8Hekh7_3>9(B1*Y zlWqa1k3OoPh_hC`FJXfb<@PbAqo=)TWrZWbeK1)zF>-lH=Yc%JK*?E_rWGAO&Rk+F zSiWvvZUZZFpoU|3wFn{tW<8yknMw-MXrqN3JSc8!y&7rTXVdx9>n3jjP z1c!;rURVG%U--K$?rZUfKhG`p1;Mlgh2Ufru`=$T(TfKA$gCyXMk24rOfpRg)yl~J zd>>J#7N2>BLZ$wwRCc;Y_+ne6al%JG3mY$IK+ zcjwYvk6cxm@s{MC~k+!h?zBKOcx zFbZ(K1(e_1e*2$A7<)2i;7cJ_v4krPPKji)7p$T4cKhRb2Dq@m)B7@LmlF<%-r+>1 zO2IM6QTqxWBw(9@oPvP_kO8T&{ Sr5Y6iWyeO~k2N1n`u1<{F-*k( literal 2339 zcmcIki&v6q9{y@-Y_e>2$JH_?+ty27(vF&%lx`PO$V(DUg&t@oijV|@bi@l-PUB3j zW-3cW+C|AjD&_?hS-fmhrl#fv6-k{&f%Sp})u%v~|+tFOQtN8NKI; z7-{wD#ikSg+WX`tYX89sN6G8W=TG^_e@WCiIm@{OA1-&Mc!oPK$aXI1aD)Z6hP$!KcET%wV!v)TT;J^fgX{Co9FL&wTlzIg z1KK4Y`&@6k94o&{47&0gbk*>{6<>6@j6zt`lt6%mPCd_FG=bLxi?(Ey`0~1a?Q#mc zl!I{wWPaO);E?E2h2}Dq>LrVFa(8zZ49{kb!Ce6CNm~|n1C89&4vrn z*2Z5m4FZfC1KOm2du*F>SvSd8(Y4bj(wCIKz04M;8EN^1#KZ$^l#`eABO1p77Y==B z{fwbxpY5kmpYDAn9~lV~&oPd|p!bFzZ_LoPusjV014L!ecX_F++jk*G9$`@VX@h=C z|ic?(rJqfkQpq+d6CKG$b4KChxjmkarMkaGTA*U82l*G zLPt`hxKE(DPV+w!zlAUJBFhTcY$chxSQL~VNNqN%j_AZq7UZqlr4CAAr&Q4ljQEquF%)bAYV0~-bg}nC(g?v7rOpb|(*>R^` zF#OD@j9SGomFnQTLo>8bW74_#!~%59$hk0S^}TJQ9DNo`mu6n3266hX6k6JP5*Unw z$eOyk!bl<_vu(Es8{fqG0wxF@#GK8Lj8X1)s}jjMC;q0S9sGVgiOihjFg>%oWpncp z{Z~Cz$H@m=hR)WG#D-H(^`I|0Jp^Aysq1+$u51+Km|Qt$LA&M`?0}Iwb~&cmuKLUUOFic;kyo-(AZ$2F=8V@4P!y2x5OrO@UUrY8t@C z^svD_BCMtf3uzgf$K^T%`$h0z-~>X%UjVDD5={AvEuUv*eh=nX2!*PY^&X3>#I_*GK_Zj0U6pp)s5}C5+L_@yi68ojHZhld&YK z6Ca&hS#=rhMq~waYFDT6Y6G6>)4Qs%gq+$ajBHRuNTgiEd?Pqn|NFHJp3Vq91Q5Kr zQ|@?hxNZnCuDT^JIAvbEGNT2Nel}`)*C*8doJ@c7Vpd7)sJu(ULx@dghn@*=-hpuG zl9D$>Y49(L{}X&0xfOy)pxHE^dXS+%?#8B0{p1CLwgxrJNHliiO$+Z5)kX4&iC)3! zGW{E$UIJv-j<$q5F2%`vq7E+%uG+2WzBQRl;^gY07^+nHf0a=4%4Mh@U-XH9=l8<( zA6{%zgSA9`Op8Hbd{Y+0hTLe2OZzKyi}CUMpcv$%UCW{#_V7<~X!@xR>Uq{7Pj7#J z*`x1ZkpcZrHv_W^DNSP(8yl-g9s4BT1A{V7xiAA)7gQ{jSY6zeL_}zd5A!72W&|{3 z5N>+O(k!mRO(j{Mo}mueIm^@Mrap;jU9X<0@j~yZ^alIgU;hiKUVO0C?TiLFhE+`Gl;^>->nrWmwFZ>J<6Yjm;T7Gq@W@7e|NK}XxKd7`+2!?EH#<5}2AP!AjsExxIzH-?~ zYVh~5+e-iKm!Dr;vHTs<63Bk?T1CGMkYb7?{ZN`mN2wf*Gk+#xGM@iTj4 zD9_4d6@-!?4&t9~J1BTm^gn8=tMAenGl~uvCt>PqERcK#8V@K;m29Q8=tNTqvR*QI zeW`u=j#d4$GhKpScFntcCu^~hj251Ik;cQrqj7OfMAm=ae%9(Zy=0oc9=O9Ql;p=F z`Pw1Hg>i{NNY|40%-h%Q;;ML3sT5PruBfOe#Z@^Zxv#O7j^zO#oN z&1PmkfyoS_)0Mt<-Gj5D!8p-Q*?M@lJ;~n}*Z{AZYr{w3nz6%q?ZY2m7bjeC;}-?> Q021a!hD0709=&k$zd3MUPXGV_ diff --git a/app/components/primer/beta/avatar.pcss b/app/components/primer/beta/avatar.pcss new file mode 100644 index 0000000000..20b4872b82 --- /dev/null +++ b/app/components/primer/beta/avatar.pcss @@ -0,0 +1,73 @@ +/* avatar */ + +.avatar { + display: inline-block; + overflow: hidden; /* Ensure page layout in Firefox should images fail to load */ + line-height: 1; + vertical-align: middle; + background-color: var(--color-avatar-bg); /* adds opaque bg to transparent avatars */ + border-radius: var(--primer-borderRadius-medium, 6px); + flex-shrink: 0; + box-shadow: 0 0 0 1px var(--color-avatar-border); +} + +.avatar-link { + float: left; + line-height: 1; +} + +/* User for example on /stars and /user for grids of avatars */ +.avatar-group-item { + display: inline-block; + margin-bottom: 3px; +} + +/* Border radius */ + +.avatar-1, +.avatar-2, +.avatar-small { + border-radius: var(--primer-borderRadius-small, 4px); +} + +/* Sizes */ + +.avatar-1 { + width: var(--base-size-16, 16px); + height: var(--base-size-16, 16px); +} + +.avatar-2 { + width: var(--base-size-20, 20px); + height: var(--base-size-20, 20px); +} + +.avatar-3 { + width: var(--base-size-24, 24px); + height: var(--base-size-24, 24px); +} + +.avatar-4 { + width: var(--base-size-28, 28px); + height: var(--base-size-28, 28px); +} + +.avatar-5 { + width: var(--base-size-32, 32px); + height: var(--base-size-32, 32px); +} + +.avatar-6 { + width: var(--base-size-40, 40px); + height: var(--base-size-40, 40px); +} + +.avatar-7 { + width: var(--base-size-48, 48px); + height: var(--base-size-48, 48px); +} + +.avatar-8 { + width: var(--base-size-64, 64px); + height: var(--base-size-64, 64px); +} diff --git a/app/components/primer/beta/avatar_stack.pcss b/app/components/primer/beta/avatar_stack.pcss new file mode 100644 index 0000000000..b725be43ce --- /dev/null +++ b/app/components/primer/beta/avatar_stack.pcss @@ -0,0 +1,141 @@ +/* AvatarStack */ + +/* Stacked avatars can be used to show who is participating in thread when +** there is limited space available. */ + +.AvatarStack { + position: relative; + min-width: 26px; + height: 20px; + + & .AvatarStack-body { + position: absolute; + } + + &.AvatarStack--two { + min-width: 36px; + } + + &.AvatarStack--three-plus { + min-width: 46px; + } +} + +.AvatarStack-body { + display: flex; + background: var(--color-canvas-default); + border-radius: 100px; + + & .avatar { + position: relative; + z-index: 2; + display: flex; + width: 20px; + height: 20px; + box-sizing: content-box; + margin-right: -11px; + background-color: var(--color-canvas-default); + border-right: var(--primer-borderWidth-thin, 1px) solid var(--color-canvas-default); + border-radius: var(--primer-borderRadius-small, 4px); + box-shadow: none; + transition: margin 0.1s ease-in-out; + + &:first-child { + z-index: 3; + } + + &:last-child { + z-index: 1; + border-right: 0; + } + + & img { + border-radius: var(--primer-borderRadius-small, 4px); + } + + /* Account for 4+ avatars */ + &:nth-child(n + 4) { + display: none; + opacity: 0; + } + } + + &:hover { + & .avatar { + margin-right: 3px; + } + + & .avatar:nth-child(n + 4) { + display: flex; + opacity: 1; + } + + & .avatar-more { + display: none !important; + } + } +} + +.avatar.avatar-more { + z-index: 1; + margin-right: 0; + background: var(--color-canvas-subtle); + + &::before, + &::after { + position: absolute; + display: block; + height: 20px; + content: ''; + border-radius: 2px; + outline: var(--primer-borderWidth-thin, 1px) solid var(--color-canvas-default); + } + + &::before { + width: 17px; + background: var(--color-avatar-stack-fade-more); + } + + &::after { + width: 14px; + background: var(--color-avatar-stack-fade); + } +} + +/* Right aligned variation */ + +.AvatarStack--right { + & .AvatarStack-body { + right: 0; + flex-direction: row-reverse; + + &:hover .avatar { + margin-right: 0; + margin-left: 3px; + } + + & .avatar:not(:last-child) { + border-left: 0; + } + } + + & .avatar.avatar-more { + background: var(--color-avatar-stack-fade); + + &::before { + width: 5px; + } + + &::after { + width: 2px; + background: var(--color-canvas-subtle); + } + } + + & .avatar { + margin-right: 0; + margin-left: -11px; + border-right: 0; + border-left: var(--primer-borderWidth-thin, 1px) solid var(--color-canvas-default); + } +} diff --git a/app/components/primer/primer.pcss b/app/components/primer/primer.pcss index 24301652fb..86dca8dc9c 100644 --- a/app/components/primer/primer.pcss +++ b/app/components/primer/primer.pcss @@ -3,6 +3,8 @@ @import "./alpha/banner.pcss"; @import "./alpha/toggle_switch.pcss"; @import "./alpha/segmented_control.pcss"; +@import "./beta/avatar.pcss"; +@import "./beta/avatar_stack.pcss"; @import "./beta/breadcrumbs.pcss"; @import "./beta/button.pcss"; @import "./beta/counter.pcss"; diff --git a/demo/app/assets/stylesheets/application.css b/demo/app/assets/stylesheets/application.css index 511398bb71..ad3d3578ff 100644 --- a/demo/app/assets/stylesheets/application.css +++ b/demo/app/assets/stylesheets/application.css @@ -16,7 +16,6 @@ *= require @primer/css/dist/overlay.css *= require @primer/css/dist/utilities.css *= require @primer/css/dist/autocomplete.css - *= require @primer/css/dist/avatars.css *= require @primer/css/dist/branch-name.css *= require @primer/css/dist/header.css *= require @primer/css/dist/loaders.css diff --git a/previews/primer/beta/avatar_preview.rb b/previews/primer/beta/avatar_preview.rb index 3f87ae3968..16e91562c2 100644 --- a/previews/primer/beta/avatar_preview.rb +++ b/previews/primer/beta/avatar_preview.rb @@ -13,14 +13,70 @@ def playground(size: 24, shape: :circle, href: nil) render(Primer::Beta::Avatar.new(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser", size: size, shape: shape, href: href)) end - # @label Default options + # @label Default + def default + render(Primer::Beta::Avatar.new(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser")) + end + + # @label As link # - # @param size [Integer] select [16, 20, 24, 32, 40, 48, 80] - # @param shape [Symbol] select [circle, square] # @param href [String] text - def default(size: 24, shape: :circle, href: nil) - render(Primer::Beta::Avatar.new(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser", size: size, shape: shape, href: href)) + def as_link(href: "#") + render(Primer::Beta::Avatar.new(href: href, src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser")) + end + + # @!group Sizes + # + # @label 16px + def size_16 + render(Primer::Beta::Avatar.new(size: 16, src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser")) + end + + # @label 20px + def size_20 + render(Primer::Beta::Avatar.new(size: 20, src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser")) + end + + # @label 24px + def size_24 + render(Primer::Beta::Avatar.new(size: 24, src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser")) + end + + # @label 32px + def size_32 + render(Primer::Beta::Avatar.new(size: 32, src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser")) end + + # @label 40px + def size_40 + render(Primer::Beta::Avatar.new(size: 40, src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser")) + end + + # @label 48px + def size_48 + render(Primer::Beta::Avatar.new(size: 48, src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser")) + end + + # @label 80px + def size_80 + render(Primer::Beta::Avatar.new(size: 80, src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser")) + end + # + # @!endgroup + + # @!group Shape + # + # @label Circle + def shape_circle + render(Primer::Beta::Avatar.new(shape: :circle, src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser")) + end + + # @label Square + def shape_square + render(Primer::Beta::Avatar.new(shape: :square, src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser")) + end + # + # @!endgroup end end end diff --git a/previews/primer/beta/avatar_stack_preview.rb b/previews/primer/beta/avatar_stack_preview.rb index 47b72efa16..1d27d8d4b5 100644 --- a/previews/primer/beta/avatar_stack_preview.rb +++ b/previews/primer/beta/avatar_stack_preview.rb @@ -19,23 +19,64 @@ def playground(number_of_avatars: 1, tag: :div, align: :left, tooltipped: false, end end - # @label Default options + # @label Default + def default + render(Primer::Beta::AvatarStack.new) do |c| + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + end + end + + # @!group Multiple avatars # - # @param number_of_avatars [Integer] number - # @param tag select [["div", div], ["span", span]] - # @param align select [["Left", left], ["Right", right]] - # @param tooltipped toggle - # @param tooltip_label text - def default(number_of_avatars: 1, tag: :div, align: :left, tooltipped: false, tooltip_label: "This is a tooltip!") - render(Primer::Beta::AvatarStack.new(tag: tag, align: align, tooltipped: tooltipped, body_arguments: { label: tooltip_label })) do |c| - Array.new(number_of_avatars || 1) do - c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") - end + # @label 1 avatar + def avatar_1 + render(Primer::Beta::AvatarStack.new) do |c| + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") end end - # @!group More Examples + # @label 2 avatars + def avatar_2 + render(Primer::Beta::AvatarStack.new) do |c| + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + end + end + # @label 3 avatars + def avatar_3 + render(Primer::Beta::AvatarStack.new) do |c| + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + end + end + + # @label 4 avatars + def avatar_4 + render(Primer::Beta::AvatarStack.new) do |c| + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + end + end + + # @label 5 avatars + def avatar_5 + render(Primer::Beta::AvatarStack.new) do |c| + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") + end + end + # + # @!endgroup + + # @!group More options + # # @label Align right def align_right render(Primer::Beta::AvatarStack.new(align: :right)) do |c| @@ -53,6 +94,7 @@ def with_tooltip c.avatar(src: Primer::ExampleImage::BASE64_SRC, alt: "@kittenuser") end end + # # @!endgroup end end diff --git a/test/css/component_specific_selectors_test.rb b/test/css/component_specific_selectors_test.rb index 05e0976c99..37957861c0 100644 --- a/test/css/component_specific_selectors_test.rb +++ b/test/css/component_specific_selectors_test.rb @@ -45,6 +45,21 @@ class ComponentSpecificSelectorsTest < Minitest::Test ".Button--iconOnly.Button--small", ".Button--iconOnly.Button--large" ], + Primer::Beta::Avatar => [ + ".avatar-link", + ".avatar-group-item", + ".avatar-1", + ".avatar-2", + ".avatar-3", + ".avatar-4", + ".avatar-5", + ".avatar-6", + ".avatar-7", + ".avatar-8" + ], + Primer::Beta::AvatarStack => [ + ".AvatarStack-body .avatar img" + ], Primer::Beta::Counter => [ "Counter .octicon" ], From 1ae607800ac8a136d98484bd5662710334bb6f09 Mon Sep 17 00:00:00 2001 From: Siddharth Kshetrapal Date: Thu, 17 Nov 2022 18:54:55 +0100 Subject: [PATCH 2/4] Update Banner link in nav after #1579 (#1610) --- docs/src/@primer/gatsby-theme-doctocat/nav.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/@primer/gatsby-theme-doctocat/nav.yml b/docs/src/@primer/gatsby-theme-doctocat/nav.yml index 42625ccea0..693f53c61b 100644 --- a/docs/src/@primer/gatsby-theme-doctocat/nav.yml +++ b/docs/src/@primer/gatsby-theme-doctocat/nav.yml @@ -24,7 +24,7 @@ - title: AvatarStack url: "/components/beta/avatarstack" - title: Banner - url: "/components/beta/banner" + url: "/components/alpha/banner" - title: Blankslate url: "/components/beta/blankslate" - title: BorderBox From 047674c872b5ca3fedd768cee8473b7d5e665c95 Mon Sep 17 00:00:00 2001 From: Salvatore Ferrucci Date: Thu, 17 Nov 2022 21:54:53 +0100 Subject: [PATCH 3/4] Don't export XBannerElement (#1611) --- .changeset/dull-rabbits-glow.md | 5 +++++ app/components/primer/alpha/x_banner.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/dull-rabbits-glow.md diff --git a/.changeset/dull-rabbits-glow.md b/.changeset/dull-rabbits-glow.md new file mode 100644 index 0000000000..adc73a8531 --- /dev/null +++ b/.changeset/dull-rabbits-glow.md @@ -0,0 +1,5 @@ +--- +'@primer/view-components': patch +--- + +Avoid double-registering of exported components diff --git a/app/components/primer/alpha/x_banner.ts b/app/components/primer/alpha/x_banner.ts index 881b38d841..079ab2a804 100644 --- a/app/components/primer/alpha/x_banner.ts +++ b/app/components/primer/alpha/x_banner.ts @@ -1,7 +1,7 @@ import {controller, target} from '@github/catalyst' @controller -export class XBannerElement extends HTMLElement { +class XBannerElement extends HTMLElement { @target titleText: HTMLElement dismiss() { From 75ad4765ccbac2b0b5e631877c26373d13760ed3 Mon Sep 17 00:00:00 2001 From: River Lynn Bailey Date: Thu, 17 Nov 2022 14:55:41 -0600 Subject: [PATCH 4/4] Tests to validate CSS used by components (#1608) Co-authored-by: mxriverlynn --- .changeset/stale-houses-sparkle.md | 5 + previews/primer/alpha/banner_preview.rb | 2 +- test/css/component_selector_use_test.rb | 105 ++++++++++++++++++ test/css/component_specific_selectors_test.rb | 27 +---- test/css/test_helper.rb | 28 +++++ 5 files changed, 140 insertions(+), 27 deletions(-) create mode 100644 .changeset/stale-houses-sparkle.md create mode 100644 test/css/component_selector_use_test.rb diff --git a/.changeset/stale-houses-sparkle.md b/.changeset/stale-houses-sparkle.md new file mode 100644 index 0000000000..c109b0ba4c --- /dev/null +++ b/.changeset/stale-houses-sparkle.md @@ -0,0 +1,5 @@ +--- +'@primer/view-components': patch +--- + +testing to ensure CSS classes used by PVC components are valid, according to the available selectors diff --git a/previews/primer/alpha/banner_preview.rb b/previews/primer/alpha/banner_preview.rb index 442215f359..7a6e172f11 100644 --- a/previews/primer/alpha/banner_preview.rb +++ b/previews/primer/alpha/banner_preview.rb @@ -48,7 +48,7 @@ def scheme_warning # # @!endgroup - # @label Dismissable + # @label Dismissable def dismissable render(Primer::Alpha::Banner.new(dismissable: true, reappear: true)) { "This is a dismissable banner." } end diff --git a/test/css/component_selector_use_test.rb b/test/css/component_selector_use_test.rb new file mode 100644 index 0000000000..7ef500ddad --- /dev/null +++ b/test/css/component_selector_use_test.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require_relative "./test_helper" +Dir["app/components/**/*.rb"].each { |file| require_relative "../../#{file}" } + +# rubocop:disable Style/WordArray +IGNORED_SELECTORS = { + :global => ["preview-wrap"], + Primer::Alpha::TabPanels => ["tabnav", "tabnav-tab", "tabnav-tabs"], + Primer::Beta::Button => ["Button--medium", "octicon", "octicon-search", "Button--invisible-noVisuals"], + Primer::Dropdown => ["btn", "details-overlay", "details-reset", "dropdown-menu-se", "octicon", "octicon-triangle-down"], + Primer::Beta::AvatarStack => ["AvatarStack", "AvatarStack-body", "avatar", "avatar-small", "tooltipped", "tooltipped-n"], + Primer::Beta::CloseButton => ["close-button", "octicon", "octicon-x"], + Primer::Alpha::TextField => ["FormControl", "FormControl-input", "FormControl-label"], + Primer::Alpha::Layout => ["Layout--sidebarPosition-flowRow-start", "Layout--sidebarPosition-start"], + Primer::Alpha::SegmentedControl => ["Button--invisible-noVisuals", "Button--medium", "SegmentedControl-item--selected", "octicon", "octicon-eye", "octicon-file-code", "octicon-people", "SegmentedControl--iconOnly", "btn-block"], + Primer::Beta::Truncate => ["Truncate-text"], + Primer::Alpha::Banner => ["Banner-message", "Banner-title", "Banner-visual", "flash", "octicon", "octicon-bell", "Banner-actions", "Button--medium", "Banner--error", "Banner--success", "Banner--warning", "Banner--full"], + Primer::Alpha::Tooltip => ["Button--medium", "octicon", "octicon-search"], + Primer::Beta::ButtonGroup => ["BtnGroup", "BtnGroup-item", "btn", "btn-danger", "btn-outline", "btn-primary"], + Primer::Alpha::ActionList => ["ActionList-sectionDivider", "ActionList-sectionDivider-title", "ActionListContent--visual16", "ActionListItem--hasSubItem", "ActionListWrap--subGroup", "ActionListContent--sizeLarge", "ActionListContent--sizeXLarge", "octicon", "octicon-star", "Button--medium", "ActionListItem--trailingActionHover", "ActionListItem--danger", "ActionListItem--navActive"], + Primer::Markdown => ["markdown-body"], + Primer::Beta::BorderBox => ["Box", "Box-body", "Box-footer", "Box-header", "Box-row"], + Primer::ClipboardCopy => ["octicon", "octicon-check", "octicon-copy"], + Primer::Alpha::NavList => ["ActionList-sectionDivider", "ActionList-sectionDivider-title", "ActionListItem--hasSubItem", "ActionListWrap--subGroup", "ActionList", "ActionList--subGroup", "ActionListContent--hasActiveSubItem", "ActionListContent--visual16", "ActionListItem--navActive", "ActionListItem--subItem", "ActionListItem-collapseIcon", "octicon", "octicon-chevron-down", "octicon-comment-discussion", "octicon-gear", "octicon-people", "Button--medium"], + Primer::Alpha::UnderlineNav => ["UnderlineNav", "UnderlineNav-actions", "UnderlineNav-body", "UnderlineNav-item", "btn", "octicon", "octicon-star"], + Primer::Beta::Avatar => ["avatar"], + Primer::Beta::Blankslate => ["btn", "btn-primary", "octicon", "octicon-shield", "Box"], + Primer::Alpha::Dialog => ["Overlay", "Overlay--hidden", "Overlay--motion-scaleFade", "Overlay--size-medium", "Overlay-actionWrap", "Overlay-backdrop--center", "Overlay-body", "Overlay-closeButton", "Overlay-footer", "Overlay-footer--alignEnd", "Overlay-footer--divided", "Overlay-header", "Overlay-headerContentWrap", "Overlay-title", "Overlay-titleWrap", "Overlay-whenNarrow", "btn", "btn-primary", "close-button", "octicon", "octicon-x"], + Primer::Alpha::UnderlinePanels => ["UnderlineNav", "UnderlineNav-body", "UnderlineNav-item"], + Primer::Alpha::ToggleSwitch => ["octicon", "octicon-alert", "ToggleSwitch--checked", "ToggleSwitch--disabled", "ToggleSwitch--small"], + Primer::Beta::Breadcrumbs => ["breadcrumb-item-selected"], + Primer::Alpha::ButtonMarketing => ["btn-mktg"], + Primer::Beta::AutoComplete => ["ActionList", "FormControl", "FormControl-input", "FormControl-input-leadingVisual", "FormControl-input-leadingVisualWrap", "FormControl-input-wrap", "FormControl-input-wrap--leadingVisual", "FormControl-label", "FormControl-medium", "Overlay", "Overlay--height-auto", "Overlay--width-auto", "Overlay-backdrop--anchor", "Overlay-body", "Overlay-body--paddingNone", "octicon", "octicon-search", "btn", "btn-primary"], + Primer::Beta::IconButton => ["Button--medium", "octicon", "octicon-x"], + Primer::Beta::Flash => ["flash", "octicon", "octicon-people"], + Primer::Alpha::HiddenTextExpander => ["ellipsis-expander", "hidden-text-expander"], + Primer::Beta::Details => ["details-overlay", "btn"], + Primer::Alpha::AutoComplete => [/.*/] +}.freeze +# rubocop:enable Style/WordArray + +# Test CSS Selectors Used By Components +# ---- +# +# ensure all of the classes used by components are valid, checking against the +# available selectors in component-specific CSS +class ComponentSelectorUseTest < Minitest::Test + include Primer::ComponentTestHelpers + include Primer::RenderPreview + + COMPONENT_SELECTORS = Dir["app/{components,lib/primer}/**/*.css.json"].map do |file| + data = JSON.parse(File.read(file)) + data["selectors"].map { |sel| sel.gsub('\n', "").strip } + end.flatten.uniq + + # these test methods are created dynamically so we can see all failures for + # all components and not error after the first component failure + Primer::Component.descendants.each do |component_class| + class_test_name = component_class.name.downcase.gsub("::", "_") + define_method("test_selectors_used_by_#{class_test_name}_are_valid") do + preview_class = get_preview_class(component_class) + next unless preview_class + + unmatched_selectors = [] + previews = preview_class.instance_methods(false) + previews.each do |preview| + preview_page = render_preview(preview, preview_klass: preview_class) + preview_selectors = find_selectors(component_class, preview_page).map { |sel| ".#{sel}" } + + unmatched_selectors << (preview_selectors - COMPONENT_SELECTORS) + end + unmatched_selectors = unmatched_selectors.flatten.compact.uniq + + assert unmatched_selectors.empty?, unmatched_selectors_message(component_class, unmatched_selectors) + end + end + + private + + def find_selectors(component_class, node) + selectors = node.classes || [] + + child_selectors = node.elements.map do |el| + find_selectors(component_class, el) + end + + flat_list = selectors.concat(child_selectors).flatten.uniq + filter_selectors(component_class, flat_list, IGNORED_SELECTORS) + end + + def unmatched_selectors_message(component_class, selectors) + class_name = component_class.name + selector_list = selectors.compact.sort.join("\n") + + msg = [] + msg << "PVC Component '#{class_name}' uses CSS selectors that are not found in the PVC source:" + msg << "" + msg << selector_list + msg << "" + msg << "If these CSS selectors are not provided by PVC, they may be ignored by updating 'IGNORED_SELECTORS' in #{__FILE__}" + + msg.join("\n") + end +end diff --git a/test/css/component_specific_selectors_test.rb b/test/css/component_specific_selectors_test.rb index 37957861c0..4aeabc9d62 100644 --- a/test/css/component_specific_selectors_test.rb +++ b/test/css/component_specific_selectors_test.rb @@ -101,7 +101,7 @@ class ComponentSpecificSelectorsTest < Minitest::Test preview_class = get_preview_class(component_class) next unless preview_class - selectors = get_component_selectors(component_class) + selectors = get_component_selectors(component_class, IGNORED_SELECTORS) previews = preview_class.instance_methods(false) matched_selectors = [] @@ -121,18 +121,6 @@ class ComponentSpecificSelectorsTest < Minitest::Test private - def get_preview_class(component_class) - name = component_class.name.gsub("Component", "") - prevew_name = "#{name}Preview" - Object.const_defined?(prevew_name) ? prevew_name.constantize : nil - end - - def get_component_selectors(component_class) - css_file = Object.const_source_location(component_class.to_s)[0].gsub(".rb", ".css.json") - css_data = File.exist?(css_file) ? JSON.parse(File.read(css_file)) : {} - filter_selectors(component_class, css_data["selectors"]) - end - def no_preview_for_selectors_message(preview_class, selectors) class_name = preview_class.name selector_list = selectors.join("\n") @@ -146,17 +134,4 @@ def no_preview_for_selectors_message(preview_class, selectors) msg.join("\n") end - - def filter_selectors(component_class, selectors) - filtered = (selectors || []).reject do |selector| - global_ignored = IGNORED_SELECTORS[:global].any? { |pattern| selector.match(pattern) } - - component_filter = IGNORED_SELECTORS[component_class] - component_ignored = component_filter ? component_filter.any? { |pattern| selector.match(pattern) } : false - - global_ignored || component_ignored - end - - filtered.flatten.uniq - end end diff --git a/test/css/test_helper.rb b/test/css/test_helper.rb index 553a235c5a..0297b26d62 100644 --- a/test/css/test_helper.rb +++ b/test/css/test_helper.rb @@ -35,4 +35,32 @@ def render_preview(name, preview_klass: nil, params: {}) Nokogiri::HTML.fragment(@rendered_content) end + + def get_preview_class(component_class) + name = component_class.name.gsub("Component", "") + prevew_name = "#{name}Preview" + Object.const_defined?(prevew_name) ? prevew_name.constantize : nil + end + + def get_component_selectors(component_class, ignore_list = {}) + css_file = Object.const_source_location(component_class.to_s)[0].gsub(".rb", ".css.json") + css_data = File.exist?(css_file) ? JSON.parse(File.read(css_file)) : {} + filter_selectors(component_class, css_data["selectors"], ignore_list) + end + + def filter_selectors(component_class, selectors, ignore_list) + ignore_list ||= {} + + filtered = (selectors || []).reject do |selector| + global_list = ignore_list[:global] || [] + global_ignored = global_list.any? { |pattern| selector.match(pattern) } + + component_filter = ignore_list[component_class] + component_ignored = component_filter ? component_filter.any? { |pattern| selector.match(pattern) } : false + + global_ignored || component_ignored + end + + filtered.flatten.uniq + end end