@setsquare/ui 0.2.4 → 0.5.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.
- package/CHANGELOG.md +132 -0
- package/NOTICE +1 -1
- package/README.md +17 -7
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/page-items.d.ts +10 -0
- package/dist/lib/page-items.d.ts.map +1 -0
- package/dist/lib/page-items.js +26 -0
- package/dist/lib/page-items.js.map +1 -0
- package/dist/lint/index.d.ts +5 -0
- package/dist/lint/index.d.ts.map +1 -1
- package/dist/lint/index.js +84 -3
- package/dist/lint/index.js.map +1 -1
- package/dist/shell/sidebar.d.ts +23 -0
- package/dist/shell/sidebar.d.ts.map +1 -1
- package/dist/shell/sidebar.js +22 -3
- package/dist/shell/sidebar.js.map +1 -1
- package/dist/styles/theme.css +550 -0
- package/dist/styles/tokens.css +17 -0
- package/dist/testing/index.d.ts +57 -2
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +276 -11
- package/dist/testing/index.js.map +1 -1
- package/dist/tokens.json +45 -0
- package/dist/ui/breadcrumbs.d.ts +9 -1
- package/dist/ui/breadcrumbs.d.ts.map +1 -1
- package/dist/ui/breadcrumbs.js +3 -3
- package/dist/ui/breadcrumbs.js.map +1 -1
- package/dist/ui/button-styles.d.ts.map +1 -1
- package/dist/ui/button-styles.js +16 -6
- package/dist/ui/button-styles.js.map +1 -1
- package/dist/ui/combo-box.d.ts +6 -1
- package/dist/ui/combo-box.d.ts.map +1 -1
- package/dist/ui/combo-box.js +2 -2
- package/dist/ui/combo-box.js.map +1 -1
- package/dist/ui/drop-indicator.d.ts +21 -0
- package/dist/ui/drop-indicator.d.ts.map +1 -0
- package/dist/ui/drop-indicator.js +20 -0
- package/dist/ui/drop-indicator.js.map +1 -0
- package/dist/ui/feature-card.d.ts.map +1 -1
- package/dist/ui/feature-card.js.map +1 -1
- package/dist/ui/grid-list.d.ts +10 -3
- package/dist/ui/grid-list.d.ts.map +1 -1
- package/dist/ui/grid-list.js +18 -7
- package/dist/ui/grid-list.js.map +1 -1
- package/dist/ui/pagination.d.ts +69 -14
- package/dist/ui/pagination.d.ts.map +1 -1
- package/dist/ui/pagination.js +177 -32
- package/dist/ui/pagination.js.map +1 -1
- package/dist/ui/switch.d.ts +2 -1
- package/dist/ui/switch.d.ts.map +1 -1
- package/dist/ui/switch.js +5 -2
- package/dist/ui/switch.js.map +1 -1
- package/dist/ui/tabs.d.ts +82 -15
- package/dist/ui/tabs.d.ts.map +1 -1
- package/dist/ui/tabs.js +274 -16
- package/dist/ui/tabs.js.map +1 -1
- package/dist/ui/time-of-day-icon.d.ts.map +1 -1
- package/dist/ui/time-of-day-icon.js.map +1 -1
- package/dist/ui/toolbar.d.ts +22 -0
- package/dist/ui/toolbar.d.ts.map +1 -0
- package/dist/ui/toolbar.js +22 -0
- package/dist/ui/toolbar.js.map +1 -0
- package/dist/ui/tree.d.ts +98 -0
- package/dist/ui/tree.d.ts.map +1 -0
- package/dist/ui/tree.js +75 -0
- package/dist/ui/tree.js.map +1 -0
- package/package.json +21 -25
- package/templates/next/page-transition.tsx +1 -1
package/dist/styles/theme.css
CHANGED
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
--color-border: var(--border);
|
|
39
39
|
--color-input: var(--input);
|
|
40
40
|
--color-ring: var(--ring);
|
|
41
|
+
--color-edge: var(--edge);
|
|
42
|
+
--color-edge-selected: var(--edge-selected);
|
|
43
|
+
--color-canvas-mark: var(--canvas-mark);
|
|
44
|
+
--color-canvas-mark-halo: var(--canvas-mark-halo);
|
|
41
45
|
--color-destructive: var(--destructive);
|
|
42
46
|
--color-destructive-foreground: var(--destructive-foreground);
|
|
43
47
|
--color-destructive-subtle: var(--destructive-subtle);
|
|
@@ -164,6 +168,20 @@
|
|
|
164
168
|
--site-gutter: clamp(20px, calc(3.2 * var(--site-vw, 1vw)), 44px);
|
|
165
169
|
--section-y: clamp(56px, calc(7 * var(--site-vw, 1vw)), 112px);
|
|
166
170
|
--section-y-tight: clamp(40px, calc(5 * var(--site-vw, 1vw)), 80px);
|
|
171
|
+
/* A preview isn't the page, so its shell leaves the page's view-transition names (site-header) to the page's
|
|
172
|
+
own. A name may be on one element only: two, and the browser abandons every page transition. Important,
|
|
173
|
+
to win over the name the header sets inline. */
|
|
174
|
+
& * {
|
|
175
|
+
view-transition-name: none !important;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* The same for a dashboard shell, or a part of one, shown inside a page that has its own, such as a documentation
|
|
180
|
+
site: its header, sidebar and right panel leave shell-header, shell-sidebar and shell-panel to the page's. */
|
|
181
|
+
@utility shell-preview {
|
|
182
|
+
& * {
|
|
183
|
+
view-transition-name: none !important;
|
|
184
|
+
}
|
|
167
185
|
}
|
|
168
186
|
|
|
169
187
|
/* The content width of a public page, centred, with the gutter either side. */
|
|
@@ -259,6 +277,7 @@
|
|
|
259
277
|
}
|
|
260
278
|
}
|
|
261
279
|
@utility card-surface {
|
|
280
|
+
--tabs-surface: var(--card);
|
|
262
281
|
position: relative;
|
|
263
282
|
padding: var(--space-6);
|
|
264
283
|
border: 1px solid var(--border);
|
|
@@ -482,6 +501,537 @@
|
|
|
482
501
|
@media (prefers-reduced-motion: reduce) { animation: none; }
|
|
483
502
|
}
|
|
484
503
|
|
|
504
|
+
/* ---------- Tabs: icons, and the selected tab opens to show its name ----------
|
|
505
|
+
Tab (the target) > tabs-paint (the pill) > tabs-icon and tabs-name. The paint's data-display says what a closed
|
|
506
|
+
tab shows: expanding (the name opens when selected), text, or icon. Widths and heights animate at AA only; at
|
|
507
|
+
AAA the name still fades in (opacity is not motion), and under reduced motion nothing animates. */
|
|
508
|
+
@keyframes setsquare-tab-pop { 0% { scale: 0.82; } 60% { scale: 1.08; } 100% { scale: 1; } }
|
|
509
|
+
@keyframes setsquare-badge-beat { 0%, 40%, 100% { scale: 1; } 15% { scale: 1.15; } }
|
|
510
|
+
@keyframes setsquare-badge-halo { 0% { scale: 0.5; opacity: 0.6; } 100% { scale: 1; opacity: 0; } }
|
|
511
|
+
@keyframes setsquare-badge-glow { 0%, 100% { opacity: 0; } 50% { opacity: 0.45; } }
|
|
512
|
+
@keyframes setsquare-badge-out { to { opacity: 0; } }
|
|
513
|
+
@keyframes setsquare-tip-below { from { opacity: 0; translate: -50% calc(var(--motion-distance) * -0.5); } }
|
|
514
|
+
@keyframes setsquare-tip-beside { from { opacity: 0; translate: calc(var(--motion-distance) * -0.5) -50%; } }
|
|
515
|
+
@keyframes setsquare-panel-in { from { opacity: 0; translate: var(--tabs-from, 0 0); } }
|
|
516
|
+
@utility tabs-list {
|
|
517
|
+
position: relative;
|
|
518
|
+
display: flex;
|
|
519
|
+
flex-wrap: wrap;
|
|
520
|
+
align-items: center;
|
|
521
|
+
gap: 4px;
|
|
522
|
+
/* The rail: 104px of icon tiles, so it never changes width; the selected tile grows downwards. */
|
|
523
|
+
&[data-orientation='vertical'] {
|
|
524
|
+
flex: none;
|
|
525
|
+
flex-direction: column;
|
|
526
|
+
flex-wrap: nowrap;
|
|
527
|
+
align-items: stretch;
|
|
528
|
+
width: 104px;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
@utility tabs-paint {
|
|
532
|
+
/* The badge's cut-out is the tab's own surface: the page (or --tabs-surface), the hover ground, the fill. */
|
|
533
|
+
--tabs-cut: var(--tabs-surface, var(--background));
|
|
534
|
+
position: relative;
|
|
535
|
+
display: inline-grid;
|
|
536
|
+
grid-template-columns: 20px 0fr;
|
|
537
|
+
align-items: center;
|
|
538
|
+
column-gap: 0;
|
|
539
|
+
height: var(--control-height);
|
|
540
|
+
padding: 0 8px;
|
|
541
|
+
border-radius: 999px;
|
|
542
|
+
font-size: 14px;
|
|
543
|
+
line-height: 20px;
|
|
544
|
+
font-weight: 500;
|
|
545
|
+
color: var(--muted-foreground);
|
|
546
|
+
white-space: nowrap;
|
|
547
|
+
transition: grid-template-columns 280ms var(--ease-standard), column-gap 280ms var(--ease-standard),
|
|
548
|
+
padding 280ms var(--ease-standard), background-color var(--duration-fast) var(--ease-standard),
|
|
549
|
+
color var(--duration-fast) var(--ease-standard);
|
|
550
|
+
[data-hovered] > & {
|
|
551
|
+
--tabs-cut: var(--accent);
|
|
552
|
+
background-color: var(--accent);
|
|
553
|
+
color: var(--foreground);
|
|
554
|
+
}
|
|
555
|
+
[data-selected] > & {
|
|
556
|
+
--tabs-cut: var(--brand-subtle);
|
|
557
|
+
grid-template-columns: 20px 1fr;
|
|
558
|
+
column-gap: 8px;
|
|
559
|
+
padding: 0 14px 0 10px;
|
|
560
|
+
background-color: var(--brand-subtle);
|
|
561
|
+
color: var(--foreground);
|
|
562
|
+
}
|
|
563
|
+
/* Text has no icon column: the name is the only one. */
|
|
564
|
+
&[data-display='text'] {
|
|
565
|
+
grid-template-columns: 1fr;
|
|
566
|
+
column-gap: 0;
|
|
567
|
+
padding: 0 14px;
|
|
568
|
+
}
|
|
569
|
+
[data-selected] > &[data-display='icon'] {
|
|
570
|
+
grid-template-columns: 20px 0fr;
|
|
571
|
+
column-gap: 0;
|
|
572
|
+
padding: 0 8px;
|
|
573
|
+
}
|
|
574
|
+
/* Where the name doesn't open to mark the selected tab, a brand marker does: selection never rests on the fill
|
|
575
|
+
alone (1.4.1, 1.4.11). */
|
|
576
|
+
[data-selected] > &:is([data-display='text'], [data-display='icon'])::after {
|
|
577
|
+
content: '';
|
|
578
|
+
position: absolute;
|
|
579
|
+
left: 10px;
|
|
580
|
+
right: 10px;
|
|
581
|
+
bottom: 3px;
|
|
582
|
+
height: 3px;
|
|
583
|
+
border-radius: 2px;
|
|
584
|
+
background-color: var(--brand);
|
|
585
|
+
}
|
|
586
|
+
@variant aaa {
|
|
587
|
+
transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
|
|
588
|
+
}
|
|
589
|
+
[role='tablist'][data-orientation='vertical'] & {
|
|
590
|
+
display: grid;
|
|
591
|
+
grid-template-columns: none;
|
|
592
|
+
grid-template-rows: 20px 0fr;
|
|
593
|
+
justify-items: center;
|
|
594
|
+
row-gap: 0;
|
|
595
|
+
width: 100%;
|
|
596
|
+
height: auto;
|
|
597
|
+
min-height: var(--control-height);
|
|
598
|
+
padding: 8px 4px;
|
|
599
|
+
border-radius: var(--radius-lg);
|
|
600
|
+
white-space: normal;
|
|
601
|
+
transition: grid-template-rows 280ms var(--ease-standard), row-gap 280ms var(--ease-standard),
|
|
602
|
+
background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
|
|
603
|
+
@variant aaa {
|
|
604
|
+
transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
[role='tablist'][data-orientation='vertical'] [data-selected] > & {
|
|
608
|
+
grid-template-rows: 20px 1fr;
|
|
609
|
+
row-gap: 6px;
|
|
610
|
+
}
|
|
611
|
+
[role='tablist'][data-orientation='vertical'] &[data-display='text'] {
|
|
612
|
+
grid-template-rows: 1fr;
|
|
613
|
+
row-gap: 0;
|
|
614
|
+
}
|
|
615
|
+
[role='tablist'][data-orientation='vertical'] [data-selected] > &[data-display='icon'] {
|
|
616
|
+
grid-template-rows: 20px 0fr;
|
|
617
|
+
row-gap: 0;
|
|
618
|
+
}
|
|
619
|
+
[role='tablist'][data-orientation='vertical'] [data-selected] > &:is([data-display='text'], [data-display='icon'])::after {
|
|
620
|
+
left: 3px;
|
|
621
|
+
right: auto;
|
|
622
|
+
top: 10px;
|
|
623
|
+
bottom: 10px;
|
|
624
|
+
width: 3px;
|
|
625
|
+
height: auto;
|
|
626
|
+
}
|
|
627
|
+
@media (prefers-reduced-motion: reduce) {
|
|
628
|
+
transition: none !important;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
@utility tabs-icon {
|
|
632
|
+
position: relative;
|
|
633
|
+
display: block;
|
|
634
|
+
width: 20px;
|
|
635
|
+
height: 20px;
|
|
636
|
+
& > :not(.tabs-badge) {
|
|
637
|
+
display: block;
|
|
638
|
+
}
|
|
639
|
+
/* The selected tab's icon turns brand and pops (at AA). */
|
|
640
|
+
[data-selected] & > :not(.tabs-badge) {
|
|
641
|
+
color: var(--brand);
|
|
642
|
+
animation: setsquare-tab-pop 300ms var(--ease-standard);
|
|
643
|
+
@variant aaa {
|
|
644
|
+
animation: none;
|
|
645
|
+
}
|
|
646
|
+
@media (prefers-reduced-motion: reduce) {
|
|
647
|
+
animation: none;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
@utility tabs-name {
|
|
652
|
+
min-width: 0;
|
|
653
|
+
/* Clipped sideways only, so an important badge's ring after the name isn't cut off above or below. */
|
|
654
|
+
overflow-x: clip;
|
|
655
|
+
overflow-y: visible;
|
|
656
|
+
opacity: 0;
|
|
657
|
+
transition: opacity 160ms var(--ease-standard);
|
|
658
|
+
[data-selected] & {
|
|
659
|
+
opacity: 1;
|
|
660
|
+
transition-delay: 80ms;
|
|
661
|
+
}
|
|
662
|
+
[data-display='text'] > & {
|
|
663
|
+
opacity: 1;
|
|
664
|
+
}
|
|
665
|
+
[data-selected] [data-display='icon'] > & {
|
|
666
|
+
opacity: 0;
|
|
667
|
+
}
|
|
668
|
+
/* In the rail it fills its row, which opens from nothing, so a closing name is clipped below the icon rather
|
|
669
|
+
than spilling up over it. */
|
|
670
|
+
[role='tablist'][data-orientation='vertical'] & {
|
|
671
|
+
align-self: stretch;
|
|
672
|
+
width: 100%;
|
|
673
|
+
min-height: 0;
|
|
674
|
+
overflow: hidden;
|
|
675
|
+
}
|
|
676
|
+
[role='tablist'][data-orientation='vertical'] [data-display='text'] > & {
|
|
677
|
+
overflow: visible;
|
|
678
|
+
}
|
|
679
|
+
@media (prefers-reduced-motion: reduce) {
|
|
680
|
+
transition: none !important;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
@utility tabs-name-row {
|
|
684
|
+
display: inline-flex;
|
|
685
|
+
align-items: center;
|
|
686
|
+
gap: 6px;
|
|
687
|
+
/* In the rail, names wrap and hyphenate inside the tile (1.4.12). */
|
|
688
|
+
[role='tablist'][data-orientation='vertical'] & {
|
|
689
|
+
display: block;
|
|
690
|
+
text-align: center;
|
|
691
|
+
font-size: 13px;
|
|
692
|
+
line-height: 16px;
|
|
693
|
+
hyphens: auto;
|
|
694
|
+
overflow-wrap: anywhere;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/* The name, over a hidden bold copy that holds the bold width from the start. */
|
|
698
|
+
@utility tabs-name-text {
|
|
699
|
+
display: inline-grid;
|
|
700
|
+
& > * {
|
|
701
|
+
grid-area: 1 / 1;
|
|
702
|
+
}
|
|
703
|
+
& > :last-child {
|
|
704
|
+
font-weight: 700;
|
|
705
|
+
visibility: hidden;
|
|
706
|
+
}
|
|
707
|
+
[data-selected] & > :first-child {
|
|
708
|
+
font-weight: 700;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
@utility tabs-count {
|
|
712
|
+
font-size: 13px;
|
|
713
|
+
line-height: 20px;
|
|
714
|
+
font-weight: 500;
|
|
715
|
+
color: var(--muted-foreground);
|
|
716
|
+
font-variant-numeric: tabular-nums;
|
|
717
|
+
[role='tablist'][data-orientation='vertical'] & {
|
|
718
|
+
display: none;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/* A badge: pinned to the icon's top-right while the tab is closed, anchored by its right edge so a wide count
|
|
722
|
+
grows back over its own icon and never reaches the next tab, with a 2px cut-out in the tab's own surface. After
|
|
723
|
+
the name when the name shows beside the icon, so it never sits over text. */
|
|
724
|
+
@utility tabs-badge {
|
|
725
|
+
position: absolute;
|
|
726
|
+
top: -8px;
|
|
727
|
+
right: -8px;
|
|
728
|
+
isolation: isolate;
|
|
729
|
+
display: inline-flex;
|
|
730
|
+
align-items: center;
|
|
731
|
+
justify-content: center;
|
|
732
|
+
min-width: 18px;
|
|
733
|
+
height: 18px;
|
|
734
|
+
padding: 0 5px;
|
|
735
|
+
border-radius: 999px;
|
|
736
|
+
background-color: var(--foreground);
|
|
737
|
+
color: var(--background);
|
|
738
|
+
font-size: 13px;
|
|
739
|
+
line-height: 18px;
|
|
740
|
+
font-weight: 700;
|
|
741
|
+
font-variant-numeric: tabular-nums;
|
|
742
|
+
white-space: nowrap;
|
|
743
|
+
box-shadow: 0 0 0 2px var(--tabs-cut, var(--background));
|
|
744
|
+
pointer-events: none;
|
|
745
|
+
&[data-dot] {
|
|
746
|
+
top: -3px;
|
|
747
|
+
right: -4px;
|
|
748
|
+
min-width: 10px;
|
|
749
|
+
width: 10px;
|
|
750
|
+
height: 10px;
|
|
751
|
+
padding: 0;
|
|
752
|
+
}
|
|
753
|
+
&[data-place='inline'] {
|
|
754
|
+
position: relative;
|
|
755
|
+
top: auto;
|
|
756
|
+
right: auto;
|
|
757
|
+
box-shadow: none;
|
|
758
|
+
}
|
|
759
|
+
[role='tablist'][data-orientation='vertical'] &[data-place='inline'] {
|
|
760
|
+
margin-left: 6px;
|
|
761
|
+
vertical-align: middle;
|
|
762
|
+
}
|
|
763
|
+
/* Important: the destructive fill and a ring that stays, so importance never rests on colour or motion alone. */
|
|
764
|
+
&[data-important] {
|
|
765
|
+
background-color: var(--destructive);
|
|
766
|
+
color: var(--destructive-foreground);
|
|
767
|
+
}
|
|
768
|
+
&[data-important]::after {
|
|
769
|
+
content: '';
|
|
770
|
+
position: absolute;
|
|
771
|
+
inset: -4px;
|
|
772
|
+
border: 2px solid var(--destructive);
|
|
773
|
+
border-radius: inherit;
|
|
774
|
+
pointer-events: none;
|
|
775
|
+
}
|
|
776
|
+
/* The halo the pulse swells: 8px past the badge and no further, so it stays off the next tab and any text. */
|
|
777
|
+
&[data-important]::before {
|
|
778
|
+
content: '';
|
|
779
|
+
position: absolute;
|
|
780
|
+
inset: -8px;
|
|
781
|
+
z-index: -1;
|
|
782
|
+
border-radius: 999px;
|
|
783
|
+
background-color: var(--destructive);
|
|
784
|
+
opacity: 0;
|
|
785
|
+
pointer-events: none;
|
|
786
|
+
}
|
|
787
|
+
/* After a name: 10px from the text, a 5px halo and 6px of room on its right. */
|
|
788
|
+
&[data-important][data-place='inline'] {
|
|
789
|
+
margin: 0 6px 0 4px;
|
|
790
|
+
}
|
|
791
|
+
[role='tablist'][data-orientation='vertical'] &[data-important][data-place='inline'] {
|
|
792
|
+
margin-left: 10px;
|
|
793
|
+
}
|
|
794
|
+
&[data-important][data-place='inline']::after {
|
|
795
|
+
inset: -3px;
|
|
796
|
+
}
|
|
797
|
+
&[data-important][data-place='inline']::before {
|
|
798
|
+
inset: -5px;
|
|
799
|
+
}
|
|
800
|
+
/* The pulse: three times in 4.5s, so it stops by itself inside the 5 seconds 2.2.2 allows. At AAA nothing
|
|
801
|
+
grows: the halo glows in and out at its full size. */
|
|
802
|
+
&[data-pulse] {
|
|
803
|
+
animation: setsquare-badge-beat 1500ms var(--ease-standard) 3;
|
|
804
|
+
}
|
|
805
|
+
&[data-pulse]::before {
|
|
806
|
+
animation: setsquare-badge-halo 1500ms cubic-bezier(0, 0, 0.2, 1) 3;
|
|
807
|
+
}
|
|
808
|
+
@variant aaa {
|
|
809
|
+
&[data-pulse] {
|
|
810
|
+
animation: none;
|
|
811
|
+
}
|
|
812
|
+
&[data-pulse]::before {
|
|
813
|
+
animation: setsquare-badge-glow 1500ms ease-in-out 3;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/* Back on the icon as its tab closes: it waits for the name to fade, so it never lands on the text. */
|
|
817
|
+
&[data-returning] {
|
|
818
|
+
animation: setsquare-fade var(--duration-fast) var(--ease-standard) 200ms both;
|
|
819
|
+
}
|
|
820
|
+
&[data-clearing] {
|
|
821
|
+
animation: setsquare-badge-out 300ms var(--ease-standard) 700ms both;
|
|
822
|
+
}
|
|
823
|
+
/* Clearing and its tab closed before it went: it doesn't come back to the icon just to leave. */
|
|
824
|
+
&[data-clearing][data-returning] {
|
|
825
|
+
opacity: 0;
|
|
826
|
+
animation: none;
|
|
827
|
+
}
|
|
828
|
+
@media (prefers-reduced-motion: reduce) {
|
|
829
|
+
&, &::before {
|
|
830
|
+
animation: none !important;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/* A tooltip for a tab whose name isn't showing. It sits inside the tab, so the pointer can move onto it across an
|
|
835
|
+
8px bridge without leaving. Hidden from assistive tech: the name is the tab's own. */
|
|
836
|
+
@utility tabs-tip {
|
|
837
|
+
position: absolute;
|
|
838
|
+
z-index: var(--z-popover);
|
|
839
|
+
padding: 4px 8px;
|
|
840
|
+
border-radius: var(--radius-md);
|
|
841
|
+
background-color: var(--foreground);
|
|
842
|
+
color: var(--background);
|
|
843
|
+
font-size: 13px;
|
|
844
|
+
line-height: 20px;
|
|
845
|
+
font-weight: 500;
|
|
846
|
+
white-space: nowrap;
|
|
847
|
+
box-shadow: var(--shadow-lg);
|
|
848
|
+
&::before {
|
|
849
|
+
content: '';
|
|
850
|
+
position: absolute;
|
|
851
|
+
}
|
|
852
|
+
&[data-side='bottom'] {
|
|
853
|
+
top: calc(100% + 6px);
|
|
854
|
+
left: 50%;
|
|
855
|
+
translate: -50% 0;
|
|
856
|
+
animation: setsquare-tip-below var(--duration-fast) var(--ease-standard) both;
|
|
857
|
+
}
|
|
858
|
+
&[data-side='bottom']::before {
|
|
859
|
+
left: 0;
|
|
860
|
+
right: 0;
|
|
861
|
+
top: -8px;
|
|
862
|
+
height: 8px;
|
|
863
|
+
}
|
|
864
|
+
&[data-side='right'] {
|
|
865
|
+
left: calc(100% + 6px);
|
|
866
|
+
top: 50%;
|
|
867
|
+
translate: 0 -50%;
|
|
868
|
+
animation: setsquare-tip-beside var(--duration-fast) var(--ease-standard) both;
|
|
869
|
+
}
|
|
870
|
+
&[data-side='right']::before {
|
|
871
|
+
top: 0;
|
|
872
|
+
bottom: 0;
|
|
873
|
+
left: -8px;
|
|
874
|
+
width: 8px;
|
|
875
|
+
}
|
|
876
|
+
@media (prefers-reduced-motion: reduce) {
|
|
877
|
+
animation: none !important;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
/* A panel comes in from the side the selection moved towards: --motion-distance and a fade, a fade alone at AAA. */
|
|
881
|
+
@utility tabs-panel {
|
|
882
|
+
&[data-travel] {
|
|
883
|
+
animation: setsquare-panel-in var(--duration-enter) var(--ease-standard) both;
|
|
884
|
+
}
|
|
885
|
+
&[data-travel='forward'] {
|
|
886
|
+
--tabs-from: var(--motion-distance) 0;
|
|
887
|
+
}
|
|
888
|
+
&[data-travel='back'] {
|
|
889
|
+
--tabs-from: calc(var(--motion-distance) * -1) 0;
|
|
890
|
+
}
|
|
891
|
+
[data-orientation='vertical'] > &[data-travel='forward'] {
|
|
892
|
+
--tabs-from: 0 var(--motion-distance);
|
|
893
|
+
}
|
|
894
|
+
[data-orientation='vertical'] > &[data-travel='back'] {
|
|
895
|
+
--tabs-from: 0 calc(var(--motion-distance) * -1);
|
|
896
|
+
}
|
|
897
|
+
@media (prefers-reduced-motion: reduce) {
|
|
898
|
+
animation: none !important;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/* ---------- Pagination: the results footer ----------
|
|
903
|
+
The page links' paint (Layer 2), the range's figures coming in from the way you paged, the Go button arriving,
|
|
904
|
+
and the mark on a control whose page is slow to come. At AAA all of it fades; under reduced motion none of it
|
|
905
|
+
moves, and the pending mark holds still. */
|
|
906
|
+
@keyframes setsquare-current-pop { 0% { scale: 0.86; } 60% { scale: 1.04; } 100% { scale: 1; } }
|
|
907
|
+
@keyframes setsquare-roll-in { from { opacity: 0; translate: 0 var(--pagination-from, 0); } }
|
|
908
|
+
@keyframes setsquare-go-in { from { opacity: 0; translate: calc(var(--motion-distance) * -0.75) 0; } }
|
|
909
|
+
@keyframes setsquare-pending { 0%, 100% { opacity: 0.2; scale: 0.4 1; } 50% { opacity: 1; scale: 1 1; } }
|
|
910
|
+
@keyframes setsquare-pending-fade { 0%, 100% { opacity: 0.2; } 50% { opacity: 1; } }
|
|
911
|
+
@keyframes setsquare-pending-hold { to { opacity: 0.6; scale: 1 1; } }
|
|
912
|
+
@utility pagination-paint {
|
|
913
|
+
position: relative;
|
|
914
|
+
display: inline-flex;
|
|
915
|
+
align-items: center;
|
|
916
|
+
justify-content: center;
|
|
917
|
+
gap: 4px;
|
|
918
|
+
height: var(--control-height);
|
|
919
|
+
min-width: var(--control-height);
|
|
920
|
+
padding: 0 8px;
|
|
921
|
+
border-radius: var(--radius-md);
|
|
922
|
+
font-size: 14px;
|
|
923
|
+
line-height: 20px;
|
|
924
|
+
font-weight: 500;
|
|
925
|
+
font-variant-numeric: tabular-nums;
|
|
926
|
+
white-space: nowrap;
|
|
927
|
+
color: var(--foreground);
|
|
928
|
+
transition: background-color var(--duration-fast) var(--ease-standard);
|
|
929
|
+
[data-hovered] > & {
|
|
930
|
+
background-color: var(--accent);
|
|
931
|
+
}
|
|
932
|
+
[aria-current='page'] > & {
|
|
933
|
+
background-color: var(--primary);
|
|
934
|
+
color: var(--primary-foreground);
|
|
935
|
+
font-weight: 700;
|
|
936
|
+
}
|
|
937
|
+
/* The new current page pops, at AA, once the page has changed. */
|
|
938
|
+
[data-moved] [aria-current='page'] > & {
|
|
939
|
+
animation: setsquare-current-pop 240ms var(--ease-standard);
|
|
940
|
+
}
|
|
941
|
+
[data-pending] > &::after {
|
|
942
|
+
content: '';
|
|
943
|
+
position: absolute;
|
|
944
|
+
left: 8px;
|
|
945
|
+
right: 8px;
|
|
946
|
+
bottom: 4px;
|
|
947
|
+
height: 2px;
|
|
948
|
+
border-radius: 2px;
|
|
949
|
+
background-color: currentColor;
|
|
950
|
+
opacity: 0;
|
|
951
|
+
/* Five beats, then it holds still: it stops by itself inside the five seconds 2.2.2 allows. */
|
|
952
|
+
animation:
|
|
953
|
+
setsquare-pending 900ms var(--ease-standard) var(--duration-loading-delay) 5,
|
|
954
|
+
setsquare-pending-hold 0s calc(var(--duration-loading-delay) + 4500ms) forwards;
|
|
955
|
+
}
|
|
956
|
+
@variant aaa {
|
|
957
|
+
[data-moved] [aria-current='page'] > & {
|
|
958
|
+
animation: none;
|
|
959
|
+
}
|
|
960
|
+
[data-pending] > &::after {
|
|
961
|
+
animation-name: setsquare-pending-fade, setsquare-pending-hold;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
@media (prefers-reduced-motion: reduce) {
|
|
965
|
+
transition: none;
|
|
966
|
+
[data-moved] [aria-current='page'] > & {
|
|
967
|
+
animation: none;
|
|
968
|
+
}
|
|
969
|
+
[data-pending] > &::after {
|
|
970
|
+
animation: none;
|
|
971
|
+
opacity: 0.6;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
/* The page field's mark when its page is slow: the same underline, under the number. */
|
|
976
|
+
@utility pagination-field-pending {
|
|
977
|
+
position: relative;
|
|
978
|
+
&::after {
|
|
979
|
+
content: '';
|
|
980
|
+
position: absolute;
|
|
981
|
+
left: calc(var(--control-pad-x) + 2px);
|
|
982
|
+
right: calc(var(--control-pad-x) + 2px);
|
|
983
|
+
bottom: calc(var(--field-band, 0px) + 4px);
|
|
984
|
+
height: 2px;
|
|
985
|
+
border-radius: 2px;
|
|
986
|
+
background-color: currentColor;
|
|
987
|
+
opacity: 0;
|
|
988
|
+
pointer-events: none;
|
|
989
|
+
animation:
|
|
990
|
+
setsquare-pending 900ms var(--ease-standard) var(--duration-loading-delay) 5,
|
|
991
|
+
setsquare-pending-hold 0s calc(var(--duration-loading-delay) + 4500ms) forwards;
|
|
992
|
+
@media (prefers-reduced-motion: reduce) {
|
|
993
|
+
animation: none;
|
|
994
|
+
opacity: 0.6;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
@variant aaa {
|
|
998
|
+
&::after {
|
|
999
|
+
animation-name: setsquare-pending-fade, setsquare-pending-hold;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/* The range's figures come in from the way you paged: up for later pages, down for earlier ones. */
|
|
1004
|
+
@utility pagination-roll {
|
|
1005
|
+
display: inline-block;
|
|
1006
|
+
&[data-direction] {
|
|
1007
|
+
animation: setsquare-roll-in 220ms var(--ease-standard);
|
|
1008
|
+
}
|
|
1009
|
+
&[data-direction='forward'] {
|
|
1010
|
+
--pagination-from: calc(var(--motion-distance) * 0.75);
|
|
1011
|
+
}
|
|
1012
|
+
&[data-direction='back'] {
|
|
1013
|
+
--pagination-from: calc(var(--motion-distance) * -0.75);
|
|
1014
|
+
}
|
|
1015
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1016
|
+
animation: none !important;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
@utility pagination-go {
|
|
1020
|
+
animation: setsquare-go-in 180ms var(--ease-standard) both;
|
|
1021
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1022
|
+
animation: none;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/* A new page's rows fade in (no movement, at every level). Key the rows by page so they are new elements. */
|
|
1026
|
+
@utility rows-arrive {
|
|
1027
|
+
& > * {
|
|
1028
|
+
animation: setsquare-fade 200ms var(--ease-standard);
|
|
1029
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1030
|
+
animation: none;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
485
1035
|
/* ---------- FeatureCard's ground: dots fading out from the icon, and one ripple across them ----------
|
|
486
1036
|
The dots sit behind the title and the muted text, so they are light enough for that text to keep 4.5:1 on a
|
|
487
1037
|
dot as well as on the card (scripts/lib/contrast.mjs measures it on the darkest dot). At AAA there are none:
|
package/dist/styles/tokens.css
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
--sidebar-width-aaa: 256px;
|
|
55
55
|
--sidebar-width-collapsed-aa: 56px;
|
|
56
56
|
--sidebar-width-collapsed-aaa: 72px;
|
|
57
|
+
--tree-indent: 16px;
|
|
57
58
|
--rail-width-aa: 72px;
|
|
58
59
|
--rail-width-aaa: 88px;
|
|
59
60
|
--subnav-width-aa: 224px;
|
|
@@ -152,6 +153,10 @@
|
|
|
152
153
|
--border: #e0e4e9;
|
|
153
154
|
--input: #7a838e;
|
|
154
155
|
--ring: var(--brand);
|
|
156
|
+
--edge: var(--input);
|
|
157
|
+
--edge-selected: var(--brand);
|
|
158
|
+
--canvas-mark: #22369f;
|
|
159
|
+
--canvas-mark-halo: #ffffff;
|
|
155
160
|
--destructive: #bf2a2a;
|
|
156
161
|
--destructive-foreground: #ffffff;
|
|
157
162
|
--destructive-subtle: #fdecec;
|
|
@@ -193,6 +198,10 @@
|
|
|
193
198
|
--border: #28303a;
|
|
194
199
|
--input: #6b7582;
|
|
195
200
|
--ring: var(--brand);
|
|
201
|
+
--edge: var(--input);
|
|
202
|
+
--edge-selected: var(--brand);
|
|
203
|
+
--canvas-mark: #22369f;
|
|
204
|
+
--canvas-mark-halo: #ffffff;
|
|
196
205
|
--destructive: #f47a70;
|
|
197
206
|
--destructive-foreground: #0e1116;
|
|
198
207
|
--destructive-subtle: #3a1616;
|
|
@@ -234,6 +243,10 @@
|
|
|
234
243
|
--border: #7f8792;
|
|
235
244
|
--input: #5d6570;
|
|
236
245
|
--ring: var(--brand);
|
|
246
|
+
--edge: var(--input);
|
|
247
|
+
--edge-selected: var(--brand);
|
|
248
|
+
--canvas-mark: #22369f;
|
|
249
|
+
--canvas-mark-halo: #ffffff;
|
|
237
250
|
--destructive: #8c1818;
|
|
238
251
|
--destructive-foreground: #ffffff;
|
|
239
252
|
--destructive-subtle: #fbe7e7;
|
|
@@ -275,6 +288,10 @@
|
|
|
275
288
|
--border: #6c7580;
|
|
276
289
|
--input: #8c96a3;
|
|
277
290
|
--ring: var(--brand);
|
|
291
|
+
--edge: var(--input);
|
|
292
|
+
--edge-selected: var(--brand);
|
|
293
|
+
--canvas-mark: #22369f;
|
|
294
|
+
--canvas-mark-halo: #ffffff;
|
|
278
295
|
--destructive: #ffa39b;
|
|
279
296
|
--destructive-foreground: #07090c;
|
|
280
297
|
--destructive-subtle: #3a1313;
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -126,6 +126,7 @@ export interface ScreenshotPage extends Evaluates {
|
|
|
126
126
|
};
|
|
127
127
|
animations?: 'disabled' | 'allow';
|
|
128
128
|
caret?: 'hide' | 'initial';
|
|
129
|
+
scale?: 'css' | 'device';
|
|
129
130
|
}): Promise<Uint8Array>;
|
|
130
131
|
}
|
|
131
132
|
export interface InvisibleFocus {
|
|
@@ -157,6 +158,59 @@ export interface FocusVisibleOptions {
|
|
|
157
158
|
export declare function findInvisibleFocus(page: ScreenshotPage, options?: FocusVisibleOptions): Promise<InvisibleFocus[]>;
|
|
158
159
|
/** Throws, listing every control whose focus cannot be seen, unless there are none. `trapped` ones are skipped. */
|
|
159
160
|
export declare function expectFocusVisible(page: ScreenshotPage, options?: FocusVisibleOptions): Promise<void>;
|
|
161
|
+
export interface WeakFocus {
|
|
162
|
+
element: string;
|
|
163
|
+
/** CSS px² of the indicator whose colour changed by at least 3:1 when the control took focus. */
|
|
164
|
+
area: number;
|
|
165
|
+
/**
|
|
166
|
+
* CSS px² the indicator needs: a 2 CSS px thick perimeter of the unfocused component, 4h + 4w − (16 − 4π)r for a
|
|
167
|
+
* rectangle with corner radius r (W3C's Understanding 2.4.13, read on 9 October 2026).
|
|
168
|
+
*/
|
|
169
|
+
needed: number;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Focus appearance (WCAG 2.4.13, AAA): for every control a keyboard can reach, the pixels that change colour by at
|
|
173
|
+
* least 3:1 when it takes focus must cover at least the area of a 2 CSS px thick perimeter of the unfocused
|
|
174
|
+
* component: 4h + 4w − (16 − 4π)r CSS px² for a rectangle with corner radius r (W3C's formulas, read on 9 October
|
|
175
|
+
* 2026). The component is its visible presentation, which W3C gauges by the indicator itself: the part whose outline
|
|
176
|
+
* or box shadow changes on focus (a three-layer control's paint), or the control when none does. Screenshots are
|
|
177
|
+
* taken at CSS scale, with
|
|
178
|
+
* focus away and then on the control, as `findInvisibleFocus` takes them. Controls the browser draws the indicator
|
|
179
|
+
* for and the author hasn't touched are exempt in WCAG, and aren't told apart here.
|
|
180
|
+
*/
|
|
181
|
+
export declare function findWeakFocusIndicators(page: ScreenshotPage, options?: FocusVisibleOptions): Promise<WeakFocus[]>;
|
|
182
|
+
/** Throws, listing every control whose focus indicator is too small or too faint for 2.4.13, unless there are none. */
|
|
183
|
+
export declare function expectFocusAppearance(page: ScreenshotPage, options?: FocusVisibleOptions): Promise<void>;
|
|
184
|
+
export interface ObscuredFocus {
|
|
185
|
+
element: string;
|
|
186
|
+
/** How much of it other content covered, of the points sampled across it: 1 is all of it. */
|
|
187
|
+
covered: number;
|
|
188
|
+
}
|
|
189
|
+
export interface FocusObscuredOptions {
|
|
190
|
+
/** `aa`: fails a control that's entirely hidden (2.4.11). `aaa`: fails one that's hidden at all (2.4.12). */
|
|
191
|
+
level?: 'aa' | 'aaa';
|
|
192
|
+
/** How many Tab presses to follow. Default 80. */
|
|
193
|
+
stops?: number;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Focus not obscured (WCAG 2.4.11 at AA, 2.4.12 at AAA): presses Tab, and at each stop samples a grid of points
|
|
197
|
+
* across the focused control. A point is covered when what's on top there is neither the control, something inside
|
|
198
|
+
* it, nor something it's inside. At AA a control fails when every point is covered; at AAA when any is. A visually
|
|
199
|
+
* hidden input is measured by the control around it, and only the part of a control that's in view is sampled:
|
|
200
|
+
* what the window or a scroll container cuts off isn't covered by other content. Elements with
|
|
201
|
+
* `pointer-events: none` can't be seen by the sampling, so a covering layer that lets clicks through goes unnoticed.
|
|
202
|
+
*/
|
|
203
|
+
export declare function findObscuredFocus(page: Evaluates & {
|
|
204
|
+
keyboard: {
|
|
205
|
+
press(key: string): Promise<void>;
|
|
206
|
+
};
|
|
207
|
+
}, options?: FocusObscuredOptions): Promise<ObscuredFocus[]>;
|
|
208
|
+
/** Throws, listing every tab stop whose focused control is hidden by other content, unless there are none. */
|
|
209
|
+
export declare function expectFocusNotObscured(page: Evaluates & {
|
|
210
|
+
keyboard: {
|
|
211
|
+
press(key: string): Promise<void>;
|
|
212
|
+
};
|
|
213
|
+
}, options?: FocusObscuredOptions): Promise<void>;
|
|
160
214
|
export interface LinkFinding {
|
|
161
215
|
/** The link, described as `tag "text"`. */
|
|
162
216
|
element: string;
|
|
@@ -285,8 +339,9 @@ export interface TextSpacingOptions {
|
|
|
285
339
|
* reports every box whose content now runs out of it further than it did before. Measuring the difference
|
|
286
340
|
* leaves alone anything that was already clipped on purpose (visually hidden text, a decorative bleed).
|
|
287
341
|
* Scroll containers are left out too, since their content can still be scrolled to, and so is text that
|
|
288
|
-
* spills from a box inside a scroller that scrolls that way (code in a code block)
|
|
289
|
-
*
|
|
342
|
+
* spills from a box inside a scroller that scrolls that way (code in a code block), or inside a pane marked
|
|
343
|
+
* `data-setsquare-pannable`: one people move with pan controls instead of scrollbars, such as a React Flow
|
|
344
|
+
* diagram, which lays itself out again once its nodes have grown. Only the innermost box is reported. Takes the spacing off again before returning. Returns [] when everything still fits.
|
|
290
345
|
*/
|
|
291
346
|
export declare function findClippedText(options?: TextSpacingOptions, root?: ParentNode): ClippedText[];
|
|
292
347
|
/** Throws, listing every finding, unless all the text still fits with WCAG 1.4.12's spacing applied. */
|