@designtools/docs 0.2.0 → 0.2.1
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 +6 -0
- package/dist/styles.css +54 -36
- package/dist/swatches.d.ts +7 -6
- package/dist/swatches.js +50 -22
- package/package.json +1 -1
- package/src/swatches.tsx +199 -141
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @designtools/docs
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a6d3829: The colour page gives its samples room: no panels round them, one hairline in each sample's own colour, the mode named once over each column, surfaces and utility colours as one row each with their full names and ratios, and the on-the-page table's verdicts as one quiet line (an icon, the ratio, the word) instead of wrapping pills.
|
|
8
|
+
|
|
3
9
|
## 0.2.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/dist/styles.css
CHANGED
|
@@ -751,6 +751,11 @@
|
|
|
751
751
|
height: 2.25rem;
|
|
752
752
|
}
|
|
753
753
|
|
|
754
|
+
:where([data-designtools]) .size-10:not([data-docs-content] *), .size-10:where([data-designtools]):not([data-docs-content] *) {
|
|
755
|
+
width: 2.5rem;
|
|
756
|
+
height: 2.5rem;
|
|
757
|
+
}
|
|
758
|
+
|
|
754
759
|
:where([data-designtools]) .size-14:not([data-docs-content] *), .size-14:where([data-designtools]):not([data-docs-content] *) {
|
|
755
760
|
width: 3.5rem;
|
|
756
761
|
height: 3.5rem;
|
|
@@ -818,6 +823,10 @@
|
|
|
818
823
|
height: 3rem;
|
|
819
824
|
}
|
|
820
825
|
|
|
826
|
+
:where([data-designtools]) .h-14:not([data-docs-content] *), .h-14:where([data-designtools]):not([data-docs-content] *) {
|
|
827
|
+
height: 3.5rem;
|
|
828
|
+
}
|
|
829
|
+
|
|
821
830
|
:where([data-designtools]) .h-16:not([data-docs-content] *), .h-16:where([data-designtools]):not([data-docs-content] *) {
|
|
822
831
|
height: 4rem;
|
|
823
832
|
}
|
|
@@ -906,14 +915,14 @@
|
|
|
906
915
|
min-height: 3rem;
|
|
907
916
|
}
|
|
908
917
|
|
|
909
|
-
:where([data-designtools]) .min-h-20:not([data-docs-content] *), .min-h-20:where([data-designtools]):not([data-docs-content] *) {
|
|
910
|
-
min-height: 5rem;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
918
|
:where([data-designtools]) .min-h-28:not([data-docs-content] *), .min-h-28:where([data-designtools]):not([data-docs-content] *) {
|
|
914
919
|
min-height: 7rem;
|
|
915
920
|
}
|
|
916
921
|
|
|
922
|
+
:where([data-designtools]) .min-h-36:not([data-docs-content] *), .min-h-36:where([data-designtools]):not([data-docs-content] *) {
|
|
923
|
+
min-height: 9rem;
|
|
924
|
+
}
|
|
925
|
+
|
|
917
926
|
:where([data-designtools]) .min-h-40:not([data-docs-content] *), .min-h-40:where([data-designtools]):not([data-docs-content] *) {
|
|
918
927
|
min-height: 10rem;
|
|
919
928
|
}
|
|
@@ -970,14 +979,14 @@
|
|
|
970
979
|
width: 2.75rem;
|
|
971
980
|
}
|
|
972
981
|
|
|
973
|
-
:where([data-designtools]) .w-14:not([data-docs-content] *), .w-14:where([data-designtools]):not([data-docs-content] *) {
|
|
974
|
-
width: 3.5rem;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
982
|
:where([data-designtools]) .w-28:not([data-docs-content] *), .w-28:where([data-designtools]):not([data-docs-content] *) {
|
|
978
983
|
width: 7rem;
|
|
979
984
|
}
|
|
980
985
|
|
|
986
|
+
:where([data-designtools]) .w-32:not([data-docs-content] *), .w-32:where([data-designtools]):not([data-docs-content] *) {
|
|
987
|
+
width: 8rem;
|
|
988
|
+
}
|
|
989
|
+
|
|
981
990
|
:where([data-designtools]) .w-48:not([data-docs-content] *), .w-48:where([data-designtools]):not([data-docs-content] *) {
|
|
982
991
|
width: 12rem;
|
|
983
992
|
}
|
|
@@ -1070,8 +1079,8 @@
|
|
|
1070
1079
|
min-width: 36rem;
|
|
1071
1080
|
}
|
|
1072
1081
|
|
|
1073
|
-
:where([data-designtools]) .min-w-\[
|
|
1074
|
-
min-width:
|
|
1082
|
+
:where([data-designtools]) .min-w-\[46rem\]:not([data-docs-content] *), .min-w-\[46rem\]:where([data-designtools]):not([data-docs-content] *) {
|
|
1083
|
+
min-width: 46rem;
|
|
1075
1084
|
}
|
|
1076
1085
|
|
|
1077
1086
|
:where([data-designtools]) .min-w-\[var\(--docs-target\,2\.75rem\)\]:not([data-docs-content] *), .min-w-\[var\(--docs-target\,2\.75rem\)\]:where([data-designtools]):not([data-docs-content] *) {
|
|
@@ -1090,6 +1099,10 @@
|
|
|
1090
1099
|
flex-shrink: 0;
|
|
1091
1100
|
}
|
|
1092
1101
|
|
|
1102
|
+
:where([data-designtools]) .table-fixed:not([data-docs-content] *), .table-fixed:where([data-designtools]):not([data-docs-content] *) {
|
|
1103
|
+
table-layout: fixed;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1093
1106
|
:where([data-designtools]) .border-collapse:not([data-docs-content] *), .border-collapse:where([data-designtools]):not([data-docs-content] *) {
|
|
1094
1107
|
border-collapse: collapse;
|
|
1095
1108
|
}
|
|
@@ -1507,10 +1520,6 @@
|
|
|
1507
1520
|
border-style: solid;
|
|
1508
1521
|
}
|
|
1509
1522
|
|
|
1510
|
-
:where([data-designtools]) .border-\(--border\):not([data-docs-content] *), .border-\(--border\):where([data-designtools]):not([data-docs-content] *) {
|
|
1511
|
-
border-color: var(--border);
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
1523
|
:where([data-designtools]) .border-border:not([data-docs-content] *), .border-border:where([data-designtools]):not([data-docs-content] *) {
|
|
1515
1524
|
border-color: var(--border, #e2e4e8);
|
|
1516
1525
|
}
|
|
@@ -1896,10 +1905,6 @@
|
|
|
1896
1905
|
padding-top: 1.5rem;
|
|
1897
1906
|
}
|
|
1898
1907
|
|
|
1899
|
-
:where([data-designtools]) .pt-8:not([data-docs-content] *), .pt-8:where([data-designtools]):not([data-docs-content] *) {
|
|
1900
|
-
padding-top: 2rem;
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
1908
|
:where([data-designtools]) .pt-10:not([data-docs-content] *), .pt-10:where([data-designtools]):not([data-docs-content] *) {
|
|
1904
1909
|
padding-top: 2.5rem;
|
|
1905
1910
|
}
|
|
@@ -1960,6 +1965,10 @@
|
|
|
1960
1965
|
padding-bottom: 1rem;
|
|
1961
1966
|
}
|
|
1962
1967
|
|
|
1968
|
+
:where([data-designtools]) .pb-5:not([data-docs-content] *), .pb-5:where([data-designtools]):not([data-docs-content] *) {
|
|
1969
|
+
padding-bottom: 1.25rem;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1963
1972
|
:where([data-designtools]) .pb-20:not([data-docs-content] *), .pb-20:where([data-designtools]):not([data-docs-content] *) {
|
|
1964
1973
|
padding-bottom: 5rem;
|
|
1965
1974
|
}
|
|
@@ -1996,6 +2005,10 @@
|
|
|
1996
2005
|
vertical-align: middle;
|
|
1997
2006
|
}
|
|
1998
2007
|
|
|
2008
|
+
:where([data-designtools]) .align-top:not([data-docs-content] *), .align-top:where([data-designtools]):not([data-docs-content] *) {
|
|
2009
|
+
vertical-align: top;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1999
2012
|
:where([data-designtools]) .\[font-family\:var\(--docs-display\,var\(--font-display\,var\(--font-sans\)\)\)\]:not([data-docs-content] *), .\[font-family\:var\(--docs-display\,var\(--font-display\,var\(--font-sans\)\)\)\]:where([data-designtools]):not([data-docs-content] *) {
|
|
2000
2013
|
font-family: var(--docs-display, var(--font-display, var(--font-sans)));
|
|
2001
2014
|
}
|
|
@@ -2326,8 +2339,8 @@
|
|
|
2326
2339
|
opacity: .7;
|
|
2327
2340
|
}
|
|
2328
2341
|
|
|
2329
|
-
:where([data-designtools]) .opacity-
|
|
2330
|
-
opacity: .
|
|
2342
|
+
:where([data-designtools]) .opacity-80:not([data-docs-content] *), .opacity-80:where([data-designtools]):not([data-docs-content] *) {
|
|
2343
|
+
opacity: .8;
|
|
2331
2344
|
}
|
|
2332
2345
|
|
|
2333
2346
|
:where([data-designtools]) .shadow:not([data-docs-content] *), .shadow:where([data-designtools]):not([data-docs-content] *) {
|
|
@@ -2415,6 +2428,20 @@
|
|
|
2415
2428
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
2416
2429
|
}
|
|
2417
2430
|
|
|
2431
|
+
:where([data-designtools]) .ring-\(--border\):not([data-docs-content] *), .ring-\(--border\):where([data-designtools]):not([data-docs-content] *) {
|
|
2432
|
+
--tw-ring-color: var(--border);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
:where([data-designtools]) .ring-\(--canvas-foreground\)\/15:not([data-docs-content] *), .ring-\(--canvas-foreground\)\/15:where([data-designtools]):not([data-docs-content] *) {
|
|
2436
|
+
--tw-ring-color: var(--canvas-foreground);
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
2440
|
+
:where([data-designtools]) .ring-\(--canvas-foreground\)\/15:not([data-docs-content] *), .ring-\(--canvas-foreground\)\/15:where([data-designtools]):not([data-docs-content] *) {
|
|
2441
|
+
--tw-ring-color: color-mix(in oklab, var(--canvas-foreground) 15%, transparent);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2418
2445
|
:where([data-designtools]) .ring-border:not([data-docs-content] *), .ring-border:where([data-designtools]):not([data-docs-content] *) {
|
|
2419
2446
|
--tw-ring-color: var(--border, #e2e4e8);
|
|
2420
2447
|
}
|
|
@@ -2423,13 +2450,13 @@
|
|
|
2423
2450
|
--tw-ring-color: var(--canvas, #fff);
|
|
2424
2451
|
}
|
|
2425
2452
|
|
|
2426
|
-
:where([data-designtools]) .ring-current\/
|
|
2453
|
+
:where([data-designtools]) .ring-current\/15:not([data-docs-content] *), .ring-current\/15:where([data-designtools]):not([data-docs-content] *) {
|
|
2427
2454
|
--tw-ring-color: currentcolor;
|
|
2428
2455
|
}
|
|
2429
2456
|
|
|
2430
2457
|
@supports (color: color-mix(in lab, red, red)) {
|
|
2431
|
-
:where([data-designtools]) .ring-current\/
|
|
2432
|
-
--tw-ring-color: color-mix(in oklab, currentcolor
|
|
2458
|
+
:where([data-designtools]) .ring-current\/15:not([data-docs-content] *), .ring-current\/15:where([data-designtools]):not([data-docs-content] *) {
|
|
2459
|
+
--tw-ring-color: color-mix(in oklab, currentcolor 15%, transparent);
|
|
2433
2460
|
}
|
|
2434
2461
|
}
|
|
2435
2462
|
|
|
@@ -2678,15 +2705,6 @@
|
|
|
2678
2705
|
margin-left: -.375rem;
|
|
2679
2706
|
}
|
|
2680
2707
|
|
|
2681
|
-
:where([data-designtools]) .first\:border-t-0:first-child:not([data-docs-content] *), .first\:border-t-0:first-child:where([data-designtools]):not([data-docs-content] *) {
|
|
2682
|
-
border-top-style: var(--tw-border-style);
|
|
2683
|
-
border-top-width: 0;
|
|
2684
|
-
}
|
|
2685
|
-
|
|
2686
|
-
:where([data-designtools]) .first\:pt-0:first-child:not([data-docs-content] *), .first\:pt-0:first-child:where([data-designtools]):not([data-docs-content] *) {
|
|
2687
|
-
padding-top: 0;
|
|
2688
|
-
}
|
|
2689
|
-
|
|
2690
2708
|
:where([data-designtools]) .first\:pt-1:first-child:not([data-docs-content] *), .first\:pt-1:first-child:where([data-designtools]):not([data-docs-content] *) {
|
|
2691
2709
|
padding-top: .25rem;
|
|
2692
2710
|
}
|
|
@@ -3040,6 +3058,10 @@
|
|
|
3040
3058
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
3041
3059
|
}
|
|
3042
3060
|
|
|
3061
|
+
:where([data-designtools]) .md\:grid-cols-\[9rem_repeat\(var\(--modes\)\,minmax\(0\,1fr\)\)\]:not([data-docs-content] *), .md\:grid-cols-\[9rem_repeat\(var\(--modes\)\,minmax\(0\,1fr\)\)\]:where([data-designtools]):not([data-docs-content] *) {
|
|
3062
|
+
grid-template-columns: 9rem repeat(var(--modes),minmax(0,1fr));
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3043
3065
|
:where([data-designtools]) .md\:grid-cols-\[11rem_minmax\(0\,1fr\)\]:not([data-docs-content] *), .md\:grid-cols-\[11rem_minmax\(0\,1fr\)\]:where([data-designtools]):not([data-docs-content] *) {
|
|
3044
3066
|
grid-template-columns: 11rem minmax(0, 1fr);
|
|
3045
3067
|
}
|
|
@@ -3159,10 +3181,6 @@
|
|
|
3159
3181
|
grid-template-columns: 3rem minmax(0, 1fr);
|
|
3160
3182
|
}
|
|
3161
3183
|
|
|
3162
|
-
:where([data-designtools]) .lg\:grid-cols-\[11rem_minmax\(0\,1fr\)\]:not([data-docs-content] *), .lg\:grid-cols-\[11rem_minmax\(0\,1fr\)\]:where([data-designtools]):not([data-docs-content] *) {
|
|
3163
|
-
grid-template-columns: 11rem minmax(0, 1fr);
|
|
3164
|
-
}
|
|
3165
|
-
|
|
3166
3184
|
:where([data-designtools]) .lg\:grid-cols-\[19rem_minmax\(0\,1fr\)\]:not([data-docs-content] *), .lg\:grid-cols-\[19rem_minmax\(0\,1fr\)\]:where([data-designtools]):not([data-docs-content] *) {
|
|
3167
3185
|
grid-template-columns: 19rem minmax(0, 1fr);
|
|
3168
3186
|
}
|
package/dist/swatches.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { type Mode } from "./lib/manifest.js";
|
|
2
2
|
import type { TokensManifest } from "./lib/manifest-types.js";
|
|
3
3
|
/**
|
|
4
|
-
* Colour, painted from the tokens rather than restated.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* every pair's contrast is measured on the colours
|
|
4
|
+
* Colour, painted from the tokens rather than restated. No panels and no cards around the samples:
|
|
5
|
+
* each sample is its own colour, with one hairline round it, set on the page in a light and a dark
|
|
6
|
+
* column. Each colour with a subdued pair shows its fill large, then subdued, hover and press; then
|
|
7
|
+
* the surfaces and the utility colours, one row each. Every sample sits inside its own `data-theme`,
|
|
8
|
+
* so dark is shown as the dark tokens resolve, and every pair's contrast is measured on the colours
|
|
9
|
+
* the browser actually paints.
|
|
9
10
|
*/
|
|
10
11
|
export declare function SemanticSwatches({ tokens, modes, }: {
|
|
11
12
|
tokens: TokensManifest;
|
|
@@ -15,7 +16,7 @@ export declare function SemanticSwatches({ tokens, modes, }: {
|
|
|
15
16
|
* Each colour on each background, per mode: a label is measured on its own fill, and this is
|
|
16
17
|
* everything else, red text on the page, a brand fill on a dark section. Backgrounds are the
|
|
17
18
|
* canvas, surface and overlay and every named `--surface-<name>`; colours are each semantic fill
|
|
18
|
-
* and muted text.
|
|
19
|
+
* and muted text. Text: fine as text (4.5:1); shape: fine as a shape only (3:1); none: neither.
|
|
19
20
|
* Measured on what the browser paints, inside each mode's `data-theme`.
|
|
20
21
|
*/
|
|
21
22
|
export declare function PageContrast({ tokens, modes }: {
|
package/dist/swatches.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, Square, X } from "lucide-react";
|
|
3
4
|
import { useEffect, useRef, useState } from "react";
|
|
4
5
|
import { contrastRatio, wcagGrade } from "./lib/contrast.js";
|
|
5
6
|
import { cx } from "./lib/cx.js";
|
|
6
7
|
import { ramps, semanticColours, valueIn } from "./lib/manifest.js";
|
|
7
8
|
/**
|
|
8
|
-
* Colour, painted from the tokens rather than restated.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* every pair's contrast is measured on the colours
|
|
9
|
+
* Colour, painted from the tokens rather than restated. No panels and no cards around the samples:
|
|
10
|
+
* each sample is its own colour, with one hairline round it, set on the page in a light and a dark
|
|
11
|
+
* column. Each colour with a subdued pair shows its fill large, then subdued, hover and press; then
|
|
12
|
+
* the surfaces and the utility colours, one row each. Every sample sits inside its own `data-theme`,
|
|
13
|
+
* so dark is shown as the dark tokens resolve, and every pair's contrast is measured on the colours
|
|
14
|
+
* the browser actually paints.
|
|
13
15
|
*/
|
|
14
16
|
export function SemanticSwatches({ tokens, modes = ["light", "dark"], }) {
|
|
15
17
|
const colours = semanticColours(tokens);
|
|
@@ -22,13 +24,17 @@ export function SemanticSwatches({ tokens, modes = ["light", "dark"], }) {
|
|
|
22
24
|
const token = byName.get(name)?.background;
|
|
23
25
|
return token && pointsAt(valueIn(token, mode));
|
|
24
26
|
};
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
const columns = cx("grid gap-x-8 gap-y-6", modes.length > 1 && "md:grid-cols-2");
|
|
28
|
+
const modeLabel = (mode) => _jsx("p", { className: "text-xs font-medium text-muted-foreground capitalize", children: mode });
|
|
29
|
+
const title = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
30
|
+
return (_jsxs("div", { "data-slot": "swatches", className: "flex flex-col gap-16", children: [_jsx("div", { className: "flex flex-col gap-14", children: families.map((f) => (_jsxs("section", { "data-slot": "swatch-family", className: "flex flex-col gap-5", children: [_jsxs("header", { className: "flex items-baseline gap-3", children: [_jsx("h3", { className: "text-2xl leading-tight tracking-tight", children: title(f.slice(2).replace(/-/g, " ")) }), _jsx("code", { className: "font-mono text-xs text-muted-foreground", children: f })] }), _jsx("div", { className: columns, children: modes.map((mode) => (_jsxs("div", { className: "flex flex-col gap-2.5", children: [modeLabel(mode), _jsxs("div", { "data-theme": mode, className: "flex flex-col gap-2.5", children: [_jsx(Chip, { name: f, label: "fill", step: step(f, mode), size: "lg" }), _jsxs("div", { className: "grid grid-cols-3 gap-2.5", children: [_jsx(Chip, { name: `${f}-subdued`, label: "subdued", step: step(`${f}-subdued`, mode) }), byName.has(`${f}-hover`) && _jsx(Chip, { name: `${f}-hover`, foreground: `${f}-foreground`, label: "hover" }), byName.has(`${f}-active`) && _jsx(Chip, { name: `${f}-active`, foreground: `${f}-foreground`, label: "press" })] })] })] }, mode))) })] }, f))) }), surfaces.length > 0 && (_jsx(Rows, { title: "Surfaces", lead: "The page, panels and popups, and the brand's own backgrounds, each with its text.", modes: modes, children: surfaces.map((name) => (_jsx(Row, { name: name.slice(2), modes: modes, children: (mode) => (_jsx("div", { "data-theme": mode, children: _jsx(Chip, { name: name, label: step(name, mode) ?? "", size: "wide" }) })) }, name))) })), utility.length > 0 && (_jsx(Rows, { title: "Lines, focus and shadow", lead: "Borders, input edges, the focus ring and the shadow colour.", modes: modes, children: utility.map((name) => (_jsx(Row, { name: name.slice(2), modes: modes, children: (mode) => (_jsxs("span", { className: "flex h-14 items-center gap-3", children: [_jsx("span", { "data-theme": mode, "aria-hidden": true, className: "size-10 shrink-0 rounded-lg ring-1 ring-(--canvas-foreground)/15 ring-inset", style: { background: `var(${name})` } }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: step(name, mode) })] })) }, name))) }))] }));
|
|
31
|
+
}
|
|
32
|
+
/** A titled list of rows, with the mode names once over the columns. */
|
|
33
|
+
function Rows({ title, lead, modes, children }) {
|
|
34
|
+
return (_jsxs("section", { className: "flex flex-col gap-5", children: [_jsxs("header", { className: "flex flex-col gap-1.5", children: [_jsx("h3", { className: "text-2xl leading-tight tracking-tight", children: title }), _jsx("p", { className: "text-sm text-muted-foreground", children: lead })] }), _jsxs("div", { children: [_jsxs("div", { "aria-hidden": true, className: "hidden gap-x-8 pb-3 text-xs font-medium text-muted-foreground md:grid md:grid-cols-[9rem_repeat(var(--modes),minmax(0,1fr))]", style: { "--modes": modes.length }, children: [_jsx("span", {}), modes.map((m) => (_jsx("span", { className: "capitalize", children: m }, m)))] }), children] })] }));
|
|
35
|
+
}
|
|
36
|
+
function Row({ name, modes, children }) {
|
|
37
|
+
return (_jsxs("div", { className: "grid items-center gap-x-8 gap-y-2 border-t border-border py-3 md:grid-cols-[9rem_repeat(var(--modes),minmax(0,1fr))]", style: { "--modes": modes.length }, children: [_jsx("code", { className: "font-mono text-sm", children: name }), modes.map((mode) => (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-xs text-muted-foreground capitalize md:hidden", children: mode }), children(mode)] }, mode)))] }));
|
|
32
38
|
}
|
|
33
39
|
/** `var(--color-primary-500)` → `primary-500`: the step it points at, which is what a person compares. */
|
|
34
40
|
function pointsAt(value) {
|
|
@@ -38,11 +44,11 @@ function pointsAt(value) {
|
|
|
38
44
|
return value?.startsWith("oklch(") ? "generated" : value;
|
|
39
45
|
}
|
|
40
46
|
/**
|
|
41
|
-
* One background with its foreground on it, labelled in that foreground, with the
|
|
42
|
-
*
|
|
43
|
-
*
|
|
47
|
+
* One background with its foreground on it, labelled in that foreground, with the contrast measured on
|
|
48
|
+
* what the browser paints. It reads its mode from the nearest `data-theme` around it. One hairline round
|
|
49
|
+
* it, so a white sample shows on a white page, and nothing else.
|
|
44
50
|
*/
|
|
45
|
-
function Chip({ name, foreground = `${name}-foreground`, label, step, size = "md",
|
|
51
|
+
function Chip({ name, foreground = `${name}-foreground`, label, step, size = "md", }) {
|
|
46
52
|
const chip = useRef(null);
|
|
47
53
|
const [ratio, setRatio] = useState(null);
|
|
48
54
|
useEffect(() => {
|
|
@@ -55,20 +61,42 @@ function Chip({ name, foreground = `${name}-foreground`, label, step, size = "md
|
|
|
55
61
|
const grade = ratio === null ? null : wcagGrade(ratio);
|
|
56
62
|
const fails = grade === "Fail";
|
|
57
63
|
const measured = grade && ratio !== null ? `${ratio.toFixed(1)}:1 ${grade}` : undefined;
|
|
58
|
-
|
|
64
|
+
const contrast = measured && (_jsx("span", { "data-slot": "swatch-contrast", className: cx("font-mono text-xs whitespace-nowrap", fails ? "rounded-sm bg-destructive px-1.5 py-0.5 font-medium text-destructive-foreground" : "opacity-80"), children: measured }));
|
|
65
|
+
const paint = { background: `var(${name})`, color: `var(${foreground})` };
|
|
66
|
+
// a hairline in the sample's own text colour: it shows a white sample on a white page and disappears into a strong fill
|
|
67
|
+
const base = "min-w-0 rounded-xl ring-1 ring-current/15 ring-inset";
|
|
68
|
+
const tip = `${name} with ${foreground}${step ? ` · ${step}` : ""}${measured ? ` · ${measured}` : ""}`;
|
|
69
|
+
if (size === "wide")
|
|
70
|
+
return (_jsxs("div", { ref: chip, "data-slot": "swatch", title: tip, className: cx(base, "flex h-14 items-center justify-between gap-4 px-4"), style: paint, children: [_jsxs("span", { className: "flex min-w-0 items-baseline gap-3", children: [_jsx("span", { className: "text-xl leading-none font-medium", children: "Aa" }), _jsx("span", { className: "font-mono text-xs opacity-80", children: label })] }), contrast] }));
|
|
71
|
+
if (size === "lg")
|
|
72
|
+
return (_jsxs("div", { ref: chip, "data-slot": "swatch", title: tip, className: cx(base, "flex min-h-36 flex-col justify-between gap-6 p-5"), style: paint, children: [_jsxs("span", { className: "flex items-start justify-between gap-3", children: [_jsx("span", { className: "text-5xl leading-none tracking-tight", children: "Aa" }), contrast] }), _jsxs("span", { className: "flex flex-col gap-0.5 text-sm leading-snug", children: [_jsx("span", { className: "font-medium", children: label }), step && _jsx("span", { className: "font-mono text-xs opacity-80", children: step })] })] }));
|
|
73
|
+
return (_jsxs("div", { ref: chip, "data-slot": "swatch", title: tip, className: cx(base, "flex min-h-28 flex-col justify-between gap-4 p-4"), style: paint, children: [_jsx("span", { className: "text-2xl leading-none font-medium", children: "Aa" }), _jsxs("span", { className: "flex flex-col gap-1 leading-snug", children: [_jsx("span", { className: "text-sm font-medium", children: label }), contrast] })] }));
|
|
59
74
|
}
|
|
60
75
|
/**
|
|
61
76
|
* Each colour on each background, per mode: a label is measured on its own fill, and this is
|
|
62
77
|
* everything else, red text on the page, a brand fill on a dark section. Backgrounds are the
|
|
63
78
|
* canvas, surface and overlay and every named `--surface-<name>`; colours are each semantic fill
|
|
64
|
-
* and muted text.
|
|
79
|
+
* and muted text. Text: fine as text (4.5:1); shape: fine as a shape only (3:1); none: neither.
|
|
65
80
|
* Measured on what the browser paints, inside each mode's `data-theme`.
|
|
66
81
|
*/
|
|
67
82
|
export function PageContrast({ tokens, modes = ["light", "dark"] }) {
|
|
68
83
|
const names = new Set(semanticColours(tokens).map((c) => c.name));
|
|
69
84
|
const colours = [...names].filter((n) => names.has(`${n}-subdued`)).concat(names.has("--muted") ? ["--muted-foreground"] : []);
|
|
70
85
|
const grounds = ["--canvas", "--surface", "--overlay", ...[...names].filter((n) => n.startsWith("--surface-") && !n.endsWith("-foreground") && n !== "--surface-foreground")];
|
|
71
|
-
return (
|
|
86
|
+
return (_jsxs("div", { "data-slot": "page-contrast", className: "flex flex-col gap-14", children: [_jsx("p", { className: "flex flex-wrap gap-x-6 gap-y-2 text-sm text-muted-foreground", children: [
|
|
87
|
+
["text", "fine as text, 4.5:1"],
|
|
88
|
+
["shape", "fine as a shape only, 3:1"],
|
|
89
|
+
["none", "neither"],
|
|
90
|
+
].map(([mark, words]) => (_jsxs("span", { className: "inline-flex items-center gap-2", children: [_jsx(Verdict, { mark: mark }), words] }, mark))) }), modes.map((mode) => (_jsx(PageGrid, { mode: mode, colours: colours, grounds: grounds }, mode)))] }));
|
|
91
|
+
}
|
|
92
|
+
const VERDICT = {
|
|
93
|
+
text: { Icon: Check, className: "text-success-subdued-foreground", label: "text" },
|
|
94
|
+
shape: { Icon: Square, className: "text-muted-foreground", label: "shape" },
|
|
95
|
+
none: { Icon: X, className: "text-destructive-subdued-foreground", label: "none" },
|
|
96
|
+
};
|
|
97
|
+
function Verdict({ mark, ratio }) {
|
|
98
|
+
const { Icon, className, label } = VERDICT[mark];
|
|
99
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1.5 whitespace-nowrap", children: [_jsx(Icon, { "aria-hidden": true, className: cx("size-3.5 shrink-0", className), strokeWidth: 2.5 }), ratio != null && _jsx("span", { className: "font-mono text-xs text-canvas-foreground tabular-nums", children: ratio.toFixed(1) }), _jsx("span", { className: cx(ratio != null && "text-xs text-muted-foreground"), children: ratio != null ? label : null })] }));
|
|
72
100
|
}
|
|
73
101
|
function PageGrid({ mode, colours, grounds }) {
|
|
74
102
|
const probe = useRef(null);
|
|
@@ -88,10 +116,10 @@ function PageGrid({ mode, colours, grounds }) {
|
|
|
88
116
|
out[`${c}|${g}`] = contrastRatio(painted[c], painted[g]);
|
|
89
117
|
setRatios(out);
|
|
90
118
|
}, [colours, grounds]);
|
|
91
|
-
return (_jsxs("div", { className: "-mx-1 overflow-x-auto px-1", tabIndex: 0, role: "region", "aria-label": `Colours on each background, ${mode} mode`, children: [_jsx("div", { ref: probe, "data-theme": mode, hidden: true }), _jsxs("table", { className: "w-full min-w-[
|
|
119
|
+
return (_jsxs("div", { className: "-mx-1 overflow-x-auto px-1 pb-2", tabIndex: 0, role: "region", "aria-label": `Colours on each background, ${mode} mode`, children: [_jsx("div", { ref: probe, "data-theme": mode, hidden: true }), _jsxs("table", { className: "w-full min-w-[46rem] table-fixed border-collapse text-left text-sm", children: [_jsx("caption", { className: "pb-5 text-left text-lg font-medium capitalize", children: mode }), _jsx("thead", { children: _jsxs("tr", { className: "text-xs text-muted-foreground", children: [_jsx("th", { scope: "col", className: "w-32 pr-4 pb-3 font-medium", children: "Colour" }), grounds.map((g) => (_jsx("th", { scope: "col", className: "px-2 pb-3 font-mono font-normal", children: g.slice(2) }, g)))] }) }), _jsx("tbody", { children: colours.map((c) => (_jsxs("tr", { className: "border-t border-border", children: [_jsx("th", { scope: "row", className: "py-4 pr-4 align-top font-mono text-xs font-medium break-words", children: c.slice(2) }), grounds.map((g) => {
|
|
92
120
|
const r = ratios[`${c}|${g}`];
|
|
93
|
-
const mark = r == null ?
|
|
94
|
-
return (_jsx("td", { className: "px-
|
|
121
|
+
const mark = r == null ? undefined : r >= 4.5 ? "text" : r >= 3 ? "shape" : "none";
|
|
122
|
+
return (_jsx("td", { className: "px-2 py-4 align-top", children: _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { "data-theme": mode, "aria-hidden": true, className: "flex h-12 items-center justify-center rounded-lg text-lg font-bold ring-1 ring-(--border) ring-inset", style: { background: `var(${g})`, color: `var(${c})` }, children: "Aa" }), mark && r != null && _jsx(Verdict, { mark: mark, ratio: r })] }) }, g));
|
|
95
123
|
})] }, c))) })] })] }));
|
|
96
124
|
}
|
|
97
125
|
/** The primitive ramps, one row per colour family, each step painted from its own token. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@designtools/docs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Design system docs as a package: pages, navigation, search, a workbench per component, rules, patterns, brand pages and markdown for agents, generated from the @designtools/manifest and themed by your tokens. One Next adapter serves them inside your app or as a standalone site.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/swatches.tsx
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Check, Square, X } from "lucide-react";
|
|
4
|
+
import { type CSSProperties, type ReactNode, useEffect, useRef, useState } from "react";
|
|
4
5
|
import { contrastRatio, wcagGrade } from "./lib/contrast";
|
|
5
6
|
import { cx } from "./lib/cx";
|
|
6
7
|
import { ramps, semanticColours, valueIn, type Mode } from "./lib/manifest";
|
|
7
8
|
import type { TokensManifest } from "./lib/manifest-types";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* Colour, painted from the tokens rather than restated.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* every pair's contrast is measured on the colours
|
|
11
|
+
* Colour, painted from the tokens rather than restated. No panels and no cards around the samples:
|
|
12
|
+
* each sample is its own colour, with one hairline round it, set on the page in a light and a dark
|
|
13
|
+
* column. Each colour with a subdued pair shows its fill large, then subdued, hover and press; then
|
|
14
|
+
* the surfaces and the utility colours, one row each. Every sample sits inside its own `data-theme`,
|
|
15
|
+
* so dark is shown as the dark tokens resolve, and every pair's contrast is measured on the colours
|
|
16
|
+
* the browser actually paints.
|
|
15
17
|
*/
|
|
16
18
|
export function SemanticSwatches({
|
|
17
19
|
tokens,
|
|
@@ -30,85 +32,107 @@ export function SemanticSwatches({
|
|
|
30
32
|
const token = byName.get(name)?.background;
|
|
31
33
|
return token && pointsAt(valueIn(token, mode));
|
|
32
34
|
};
|
|
35
|
+
const columns = cx("grid gap-x-8 gap-y-6", modes.length > 1 && "md:grid-cols-2");
|
|
36
|
+
const modeLabel = (mode: Mode) => <p className="text-xs font-medium text-muted-foreground capitalize">{mode}</p>;
|
|
37
|
+
const title = (text: string) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
return (
|
|
40
|
+
<div data-slot="swatches" className="flex flex-col gap-16">
|
|
41
|
+
<div className="flex flex-col gap-14">
|
|
42
|
+
{families.map((f) => (
|
|
43
|
+
<section key={f} data-slot="swatch-family" className="flex flex-col gap-5">
|
|
44
|
+
<header className="flex items-baseline gap-3">
|
|
45
|
+
<h3 className="text-2xl leading-tight tracking-tight">{title(f.slice(2).replace(/-/g, " "))}</h3>
|
|
46
|
+
<code className="font-mono text-xs text-muted-foreground">{f}</code>
|
|
47
|
+
</header>
|
|
48
|
+
<div className={columns}>
|
|
49
|
+
{modes.map((mode) => (
|
|
50
|
+
<div key={mode} className="flex flex-col gap-2.5">
|
|
51
|
+
{modeLabel(mode)}
|
|
52
|
+
{/* only the samples take the mode; the label is read on the page */}
|
|
53
|
+
<div data-theme={mode} className="flex flex-col gap-2.5">
|
|
54
|
+
<Chip name={f} label="fill" step={step(f, mode)} size="lg" />
|
|
55
|
+
<div className="grid grid-cols-3 gap-2.5">
|
|
56
|
+
<Chip name={`${f}-subdued`} label="subdued" step={step(`${f}-subdued`, mode)} />
|
|
57
|
+
{byName.has(`${f}-hover`) && <Chip name={`${f}-hover`} foreground={`${f}-foreground`} label="hover" />}
|
|
58
|
+
{byName.has(`${f}-active`) && <Chip name={`${f}-active`} foreground={`${f}-foreground`} label="press" />}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</section>
|
|
65
|
+
))}
|
|
43
66
|
</div>
|
|
44
|
-
{body}
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
67
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{modes.map((mode) => (
|
|
56
|
-
<div key={mode} data-theme={mode} data-slot="swatch-family" className={panel(mode)}>
|
|
57
|
-
{modeLabel(mode)}
|
|
58
|
-
<Chip name={f} label="fill" step={step(f, mode)} size="lg" />
|
|
59
|
-
<div className="grid grid-cols-3 gap-2">
|
|
60
|
-
<Chip name={`${f}-subdued`} label="subdued" step={step(`${f}-subdued`, mode)} />
|
|
61
|
-
{byName.has(`${f}-hover`) && <Chip name={`${f}-hover`} foreground={`${f}-foreground`} label="hover" />}
|
|
62
|
-
{byName.has(`${f}-active`) && <Chip name={`${f}-active`} foreground={`${f}-foreground`} label="press" />}
|
|
68
|
+
{surfaces.length > 0 && (
|
|
69
|
+
<Rows title="Surfaces" lead="The page, panels and popups, and the brand's own backgrounds, each with its text." modes={modes}>
|
|
70
|
+
{surfaces.map((name) => (
|
|
71
|
+
<Row key={name} name={name.slice(2)} modes={modes}>
|
|
72
|
+
{(mode) => (
|
|
73
|
+
<div data-theme={mode}>
|
|
74
|
+
<Chip name={name} label={step(name, mode) ?? ""} size="wide" />
|
|
63
75
|
</div>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
),
|
|
76
|
+
)}
|
|
77
|
+
</Row>
|
|
78
|
+
))}
|
|
79
|
+
</Rows>
|
|
69
80
|
)}
|
|
70
81
|
|
|
71
|
-
{
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
{utility.length > 0 && (
|
|
83
|
+
<Rows title="Lines, focus and shadow" lead="Borders, input edges, the focus ring and the shadow colour." modes={modes}>
|
|
84
|
+
{utility.map((name) => (
|
|
85
|
+
<Row key={name} name={name.slice(2)} modes={modes}>
|
|
86
|
+
{(mode) => (
|
|
87
|
+
<span className="flex h-14 items-center gap-3">
|
|
88
|
+
<span data-theme={mode} aria-hidden className="size-10 shrink-0 rounded-lg ring-1 ring-(--canvas-foreground)/15 ring-inset" style={{ background: `var(${name})` }} />
|
|
89
|
+
<span className="font-mono text-xs text-muted-foreground">{step(name, mode)}</span>
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
</Row>
|
|
93
|
+
))}
|
|
94
|
+
</Rows>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
88
99
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
100
|
+
/** A titled list of rows, with the mode names once over the columns. */
|
|
101
|
+
function Rows({ title, lead, modes, children }: { title: string; lead: string; modes: Mode[]; children: ReactNode }) {
|
|
102
|
+
return (
|
|
103
|
+
<section className="flex flex-col gap-5">
|
|
104
|
+
<header className="flex flex-col gap-1.5">
|
|
105
|
+
<h3 className="text-2xl leading-tight tracking-tight">{title}</h3>
|
|
106
|
+
<p className="text-sm text-muted-foreground">{lead}</p>
|
|
107
|
+
</header>
|
|
108
|
+
<div>
|
|
109
|
+
<div aria-hidden className="hidden gap-x-8 pb-3 text-xs font-medium text-muted-foreground md:grid md:grid-cols-[9rem_repeat(var(--modes),minmax(0,1fr))]" style={{ "--modes": modes.length } as CSSProperties}>
|
|
110
|
+
<span />
|
|
111
|
+
{modes.map((m) => (
|
|
112
|
+
<span key={m} className="capitalize">
|
|
113
|
+
{m}
|
|
114
|
+
</span>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
{children}
|
|
118
|
+
</div>
|
|
119
|
+
</section>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function Row({ name, modes, children }: { name: string; modes: Mode[]; children: (mode: Mode) => ReactNode }) {
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
className="grid items-center gap-x-8 gap-y-2 border-t border-border py-3 md:grid-cols-[9rem_repeat(var(--modes),minmax(0,1fr))]"
|
|
127
|
+
style={{ "--modes": modes.length } as CSSProperties}
|
|
128
|
+
>
|
|
129
|
+
<code className="font-mono text-sm">{name}</code>
|
|
130
|
+
{modes.map((mode) => (
|
|
131
|
+
<div key={mode} className="flex flex-col gap-1">
|
|
132
|
+
<span className="text-xs text-muted-foreground capitalize md:hidden">{mode}</span>
|
|
133
|
+
{children(mode)}
|
|
134
|
+
</div>
|
|
135
|
+
))}
|
|
112
136
|
</div>
|
|
113
137
|
);
|
|
114
138
|
}
|
|
@@ -121,9 +145,9 @@ function pointsAt(value: string | undefined): string | undefined {
|
|
|
121
145
|
}
|
|
122
146
|
|
|
123
147
|
/**
|
|
124
|
-
* One background with its foreground on it, labelled in that foreground, with the
|
|
125
|
-
*
|
|
126
|
-
*
|
|
148
|
+
* One background with its foreground on it, labelled in that foreground, with the contrast measured on
|
|
149
|
+
* what the browser paints. It reads its mode from the nearest `data-theme` around it. One hairline round
|
|
150
|
+
* it, so a white sample shows on a white page, and nothing else.
|
|
127
151
|
*/
|
|
128
152
|
function Chip({
|
|
129
153
|
name,
|
|
@@ -131,14 +155,12 @@ function Chip({
|
|
|
131
155
|
label,
|
|
132
156
|
step,
|
|
133
157
|
size = "md",
|
|
134
|
-
outlined,
|
|
135
158
|
}: {
|
|
136
159
|
name: string;
|
|
137
160
|
foreground?: string;
|
|
138
161
|
label: string;
|
|
139
162
|
step?: string;
|
|
140
|
-
size?: "md" | "lg";
|
|
141
|
-
outlined?: boolean;
|
|
163
|
+
size?: "md" | "lg" | "wide";
|
|
142
164
|
}) {
|
|
143
165
|
const chip = useRef<HTMLDivElement>(null);
|
|
144
166
|
const [ratio, setRatio] = useState<number | null>(null);
|
|
@@ -151,40 +173,45 @@ function Chip({
|
|
|
151
173
|
const grade = ratio === null ? null : wcagGrade(ratio);
|
|
152
174
|
const fails = grade === "Fail";
|
|
153
175
|
const measured = grade && ratio !== null ? `${ratio.toFixed(1)}:1 ${grade}` : undefined;
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
{measured}
|
|
171
|
-
</span>
|
|
172
|
-
)}
|
|
176
|
+
const contrast = measured && (
|
|
177
|
+
<span data-slot="swatch-contrast" className={cx("font-mono text-xs whitespace-nowrap", fails ? "rounded-sm bg-destructive px-1.5 py-0.5 font-medium text-destructive-foreground" : "opacity-80")}>
|
|
178
|
+
{measured}
|
|
179
|
+
</span>
|
|
180
|
+
);
|
|
181
|
+
const paint = { background: `var(${name})`, color: `var(${foreground})` };
|
|
182
|
+
// a hairline in the sample's own text colour: it shows a white sample on a white page and disappears into a strong fill
|
|
183
|
+
const base = "min-w-0 rounded-xl ring-1 ring-current/15 ring-inset";
|
|
184
|
+
const tip = `${name} with ${foreground}${step ? ` · ${step}` : ""}${measured ? ` · ${measured}` : ""}`;
|
|
185
|
+
|
|
186
|
+
if (size === "wide")
|
|
187
|
+
return (
|
|
188
|
+
<div ref={chip} data-slot="swatch" title={tip} className={cx(base, "flex h-14 items-center justify-between gap-4 px-4")} style={paint}>
|
|
189
|
+
<span className="flex min-w-0 items-baseline gap-3">
|
|
190
|
+
<span className="text-xl leading-none font-medium">Aa</span>
|
|
191
|
+
<span className="font-mono text-xs opacity-80">{label}</span>
|
|
173
192
|
</span>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
193
|
+
{contrast}
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
if (size === "lg")
|
|
197
|
+
return (
|
|
198
|
+
<div ref={chip} data-slot="swatch" title={tip} className={cx(base, "flex min-h-36 flex-col justify-between gap-6 p-5")} style={paint}>
|
|
199
|
+
<span className="flex items-start justify-between gap-3">
|
|
200
|
+
<span className="text-5xl leading-none tracking-tight">Aa</span>
|
|
201
|
+
{contrast}
|
|
202
|
+
</span>
|
|
203
|
+
<span className="flex flex-col gap-0.5 text-sm leading-snug">
|
|
204
|
+
<span className="font-medium">{label}</span>
|
|
205
|
+
{step && <span className="font-mono text-xs opacity-80">{step}</span>}
|
|
206
|
+
</span>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
return (
|
|
210
|
+
<div ref={chip} data-slot="swatch" title={tip} className={cx(base, "flex min-h-28 flex-col justify-between gap-4 p-4")} style={paint}>
|
|
211
|
+
<span className="text-2xl leading-none font-medium">Aa</span>
|
|
212
|
+
<span className="flex flex-col gap-1 leading-snug">
|
|
213
|
+
<span className="text-sm font-medium">{label}</span>
|
|
214
|
+
{contrast}
|
|
188
215
|
</span>
|
|
189
216
|
</div>
|
|
190
217
|
);
|
|
@@ -194,7 +221,7 @@ function Chip({
|
|
|
194
221
|
* Each colour on each background, per mode: a label is measured on its own fill, and this is
|
|
195
222
|
* everything else, red text on the page, a brand fill on a dark section. Backgrounds are the
|
|
196
223
|
* canvas, surface and overlay and every named `--surface-<name>`; colours are each semantic fill
|
|
197
|
-
* and muted text.
|
|
224
|
+
* and muted text. Text: fine as text (4.5:1); shape: fine as a shape only (3:1); none: neither.
|
|
198
225
|
* Measured on what the browser paints, inside each mode's `data-theme`.
|
|
199
226
|
*/
|
|
200
227
|
export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: TokensManifest; modes?: Mode[] }) {
|
|
@@ -202,7 +229,21 @@ export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: To
|
|
|
202
229
|
const colours = [...names].filter((n) => names.has(`${n}-subdued`)).concat(names.has("--muted") ? ["--muted-foreground"] : []);
|
|
203
230
|
const grounds = ["--canvas", "--surface", "--overlay", ...[...names].filter((n) => n.startsWith("--surface-") && !n.endsWith("-foreground") && n !== "--surface-foreground")];
|
|
204
231
|
return (
|
|
205
|
-
<div data-slot="page-contrast" className="flex flex-col gap-
|
|
232
|
+
<div data-slot="page-contrast" className="flex flex-col gap-14">
|
|
233
|
+
<p className="flex flex-wrap gap-x-6 gap-y-2 text-sm text-muted-foreground">
|
|
234
|
+
{(
|
|
235
|
+
[
|
|
236
|
+
["text", "fine as text, 4.5:1"],
|
|
237
|
+
["shape", "fine as a shape only, 3:1"],
|
|
238
|
+
["none", "neither"],
|
|
239
|
+
] as const
|
|
240
|
+
).map(([mark, words]) => (
|
|
241
|
+
<span key={mark} className="inline-flex items-center gap-2">
|
|
242
|
+
<Verdict mark={mark} />
|
|
243
|
+
{words}
|
|
244
|
+
</span>
|
|
245
|
+
))}
|
|
246
|
+
</p>
|
|
206
247
|
{modes.map((mode) => (
|
|
207
248
|
<PageGrid key={mode} mode={mode} colours={colours} grounds={grounds} />
|
|
208
249
|
))}
|
|
@@ -210,6 +251,23 @@ export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: To
|
|
|
210
251
|
);
|
|
211
252
|
}
|
|
212
253
|
|
|
254
|
+
const VERDICT = {
|
|
255
|
+
text: { Icon: Check, className: "text-success-subdued-foreground", label: "text" },
|
|
256
|
+
shape: { Icon: Square, className: "text-muted-foreground", label: "shape" },
|
|
257
|
+
none: { Icon: X, className: "text-destructive-subdued-foreground", label: "none" },
|
|
258
|
+
} as const;
|
|
259
|
+
|
|
260
|
+
function Verdict({ mark, ratio }: { mark: keyof typeof VERDICT; ratio?: number }) {
|
|
261
|
+
const { Icon, className, label } = VERDICT[mark];
|
|
262
|
+
return (
|
|
263
|
+
<span className="inline-flex items-center gap-1.5 whitespace-nowrap">
|
|
264
|
+
<Icon aria-hidden className={cx("size-3.5 shrink-0", className)} strokeWidth={2.5} />
|
|
265
|
+
{ratio != null && <span className="font-mono text-xs text-canvas-foreground tabular-nums">{ratio.toFixed(1)}</span>}
|
|
266
|
+
<span className={cx(ratio != null && "text-xs text-muted-foreground")}>{ratio != null ? label : null}</span>
|
|
267
|
+
</span>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
213
271
|
function PageGrid({ mode, colours, grounds }: { mode: Mode; colours: string[]; grounds: string[] }) {
|
|
214
272
|
const probe = useRef<HTMLDivElement>(null);
|
|
215
273
|
const [ratios, setRatios] = useState<Record<string, number | null>>({});
|
|
@@ -226,44 +284,44 @@ function PageGrid({ mode, colours, grounds }: { mode: Mode; colours: string[]; g
|
|
|
226
284
|
setRatios(out);
|
|
227
285
|
}, [colours, grounds]);
|
|
228
286
|
return (
|
|
229
|
-
<div className="-mx-1 overflow-x-auto px-1" tabIndex={0} role="region" aria-label={`Colours on each background, ${mode} mode`}>
|
|
287
|
+
<div className="-mx-1 overflow-x-auto px-1 pb-2" tabIndex={0} role="region" aria-label={`Colours on each background, ${mode} mode`}>
|
|
230
288
|
<div ref={probe} data-theme={mode} hidden />
|
|
231
|
-
<table className="w-full min-w-[
|
|
232
|
-
<caption className="pb-
|
|
289
|
+
<table className="w-full min-w-[46rem] table-fixed border-collapse text-left text-sm">
|
|
290
|
+
<caption className="pb-5 text-left text-lg font-medium capitalize">{mode}</caption>
|
|
233
291
|
<thead>
|
|
234
292
|
<tr className="text-xs text-muted-foreground">
|
|
235
|
-
<th scope="col" className="pr-4 pb-3 font-medium">
|
|
293
|
+
<th scope="col" className="w-32 pr-4 pb-3 font-medium">
|
|
294
|
+
Colour
|
|
295
|
+
</th>
|
|
236
296
|
{grounds.map((g) => (
|
|
237
|
-
<th key={g} scope="col" className="px-
|
|
297
|
+
<th key={g} scope="col" className="px-2 pb-3 font-mono font-normal">
|
|
298
|
+
{g.slice(2)}
|
|
299
|
+
</th>
|
|
238
300
|
))}
|
|
239
301
|
</tr>
|
|
240
302
|
</thead>
|
|
241
303
|
<tbody>
|
|
242
304
|
{colours.map((c) => (
|
|
243
305
|
<tr key={c} className="border-t border-border">
|
|
244
|
-
<th scope="row" className="py-
|
|
306
|
+
<th scope="row" className="py-4 pr-4 align-top font-mono text-xs font-medium break-words">
|
|
307
|
+
{c.slice(2)}
|
|
308
|
+
</th>
|
|
245
309
|
{grounds.map((g) => {
|
|
246
310
|
const r = ratios[`${c}|${g}`];
|
|
247
|
-
const mark = r == null ?
|
|
311
|
+
const mark = r == null ? undefined : r >= 4.5 ? "text" : r >= 3 ? "shape" : "none";
|
|
248
312
|
return (
|
|
249
|
-
<td key={g} className="px-
|
|
250
|
-
<div className="flex
|
|
313
|
+
<td key={g} className="px-2 py-4 align-top">
|
|
314
|
+
<div className="flex flex-col gap-2">
|
|
251
315
|
{/* only the sample takes the mode; the number is read on the page */}
|
|
252
|
-
<span
|
|
316
|
+
<span
|
|
317
|
+
data-theme={mode}
|
|
318
|
+
aria-hidden
|
|
319
|
+
className="flex h-12 items-center justify-center rounded-lg text-lg font-bold ring-1 ring-(--border) ring-inset"
|
|
320
|
+
style={{ background: `var(${g})`, color: `var(${c})` }}
|
|
321
|
+
>
|
|
253
322
|
Aa
|
|
254
323
|
</span>
|
|
255
|
-
{r != null &&
|
|
256
|
-
<span
|
|
257
|
-
className={cx(
|
|
258
|
-
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-mono text-xs",
|
|
259
|
-
mark === "text" && "bg-success-subdued text-success-subdued-foreground",
|
|
260
|
-
mark === "shape" && "bg-muted text-muted-foreground",
|
|
261
|
-
mark === "none" && "bg-destructive-subdued text-destructive-subdued-foreground",
|
|
262
|
-
)}
|
|
263
|
-
>
|
|
264
|
-
{r.toFixed(1)} {mark}
|
|
265
|
-
</span>
|
|
266
|
-
)}
|
|
324
|
+
{mark && r != null && <Verdict mark={mark} ratio={r} />}
|
|
267
325
|
</div>
|
|
268
326
|
</td>
|
|
269
327
|
);
|