@_linked/primitives 1.3.2 → 1.4.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 +27 -0
- package/lib/cjs/components/Button.d.ts +7 -1
- package/lib/cjs/components/Button.js.map +1 -1
- package/lib/cjs/components/Button.module.css +50 -0
- package/lib/cjs/components/ConfirmDialog.d.ts +4 -1
- package/lib/cjs/components/ConfirmDialog.js +1 -1
- package/lib/cjs/components/ConfirmDialog.js.map +1 -1
- package/lib/cjs/components/ConfirmDialog.module.css +0 -12
- package/lib/cjs/package.json +1 -1
- package/lib/esm/components/Button.d.ts +7 -1
- package/lib/esm/components/Button.js.map +1 -1
- package/lib/esm/components/Button.module.css +50 -0
- package/lib/esm/components/ConfirmDialog.d.ts +4 -1
- package/lib/esm/components/ConfirmDialog.js +1 -1
- package/lib/esm/components/ConfirmDialog.js.map +1 -1
- package/lib/esm/components/ConfirmDialog.module.css +0 -12
- package/lib/esm/package.json +1 -1
- package/package.json +1 -1
- package/test/button-colour-is-a-prop.test.mjs +183 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @\_linked/primitives
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#39](https://github.com/linked-fw/primitives/pull/39) [`8130aa3`](https://github.com/linked-fw/primitives/commit/8130aa3f02682f1ced21d8767a6049d53a2744a6) Thanks [@flyon](https://github.com/flyon)! - `ConfirmDialog`'s `tone="danger"` was visually inert; `Button` now has a `danger` colour
|
|
8
|
+
|
|
9
|
+
`tone="danger"` applied a `.danger` class of ConfirmDialog's own stylesheet to the confirm
|
|
10
|
+
button. That class has specificity 0-1-0, while `Button` colours itself with compound
|
|
11
|
+
selectors — `.Root.primary.solid`, 0-3-0 — so the override never won. Measured computed
|
|
12
|
+
`background-color` on the danger button was `rgb(37, 99, 235)` (`--button-primary-bg`),
|
|
13
|
+
identical to a default confirm button. Every destructive confirmation therefore rendered in
|
|
14
|
+
the primary colour: the class was on the element, and the colour was not.
|
|
15
|
+
|
|
16
|
+
Colour is a `Button` prop, so that is now how tone travels: `Button`'s `color` union gains
|
|
17
|
+
`'danger'` with rules for all four variants, reading `--button-danger-*` and falling back to
|
|
18
|
+
the `--intent-danger-*` family, and `ConfirmDialog` passes `color="danger"` instead of a
|
|
19
|
+
class. Computed `background-color` is now `rgb(239, 68, 68)` (`--intent-danger-icon`), with
|
|
20
|
+
`--intent-danger-text` on hover.
|
|
21
|
+
|
|
22
|
+
`color="danger"` is new public API on `Button`. The `--confirm-dialog-danger-bg`,
|
|
23
|
+
`--confirm-dialog-danger-bg-hover`, `--confirm-dialog-danger-border` and
|
|
24
|
+
`--confirm-dialog-danger-text` custom properties are gone — they were read only by the rule
|
|
25
|
+
that never applied, so nothing that worked has been removed. Re-skin the destructive button
|
|
26
|
+
through `--button-danger-bg` / `--button-danger-bg-hover` / `--button-danger-border` /
|
|
27
|
+
`--button-danger-color`, or through `--intent-danger-*` to move every danger affordance at
|
|
28
|
+
once.
|
|
29
|
+
|
|
3
30
|
## 1.3.2
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
|
@@ -2,7 +2,13 @@ import * as React from 'react';
|
|
|
2
2
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
3
3
|
asChild?: boolean;
|
|
4
4
|
variant?: 'solid' | 'outline' | 'ghost' | 'link';
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Which colour family the button speaks in. `danger` is the destructive one — a
|
|
7
|
+
* statement about consequence, and a Button prop rather than a class a caller adds,
|
|
8
|
+
* because the colour classes here are compound (`.Root.primary.solid`) and a bare
|
|
9
|
+
* class handed in through `className` loses the cascade to them every time.
|
|
10
|
+
*/
|
|
11
|
+
color?: 'primary' | 'secondary' | 'tertiary' | 'danger';
|
|
6
12
|
size?: 'small' | 'medium' | 'large' | 'default';
|
|
7
13
|
}
|
|
8
14
|
declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.js","sourceRoot":"","sources":["../../../src/components/Button.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,6CAA+B;AAE/B,4EAAwC;AACxC,gEAAqD;AACrD,qDAA4C;
|
|
1
|
+
{"version":3,"file":"Button.js","sourceRoot":"","sources":["../../../src/components/Button.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,6CAA+B;AAE/B,4EAAwC;AACxC,gEAAqD;AACrD,qDAA4C;AAgB5C,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAC7B,CACE,EAOC,EACD,GAAG,EACH,EAAE;QATF,EACE,SAAS,EACT,OAAO,GAAG,OAAO,EACjB,KAAK,GAAG,SAAS,EACjB,IAAI,GAAG,SAAS,EAChB,OAAO,GAAG,KAAK,OAEhB,EADI,KAAK,cANV,oDAOC,CADS;IAIV,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,iBAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;IACvC,MAAM,UAAU,GAAG,KAAK,CAAC,QAAQ,CAAC;IAElC,OAAO,CACL,oBAAC,IAAI,kBACH,SAAS,EAAE,IAAA,eAAE,EACX,2BAAK,CAAC,IAAI,EACV,2BAAK,CAAC,OAAO,CAAC,EACd,2BAAK,CAAC,KAAK,CAAC,EACZ,2BAAK,CAAC,IAAI,CAAC,EACX,UAAU,IAAI,2BAAK,CAAC,QAAQ,EAC5B,SAAS,CACV,EACD,GAAG,EAAE,GAAG,IACJ,KAAK,EACT,CACH,CAAC;AACJ,CAAC,CACF,CAAC;AAGO,wBAAM;AAFf,MAAM,CAAC,WAAW,GAAG,QAAQ,CAAC"}
|
|
@@ -220,6 +220,56 @@
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/* Destructive. Reads the intent family rather than a red primitive, so an app that
|
|
224
|
+
restyles what "danger" looks like restyles this too; the literal is the last-resort
|
|
225
|
+
fallback for a theme that defines neither, same contract as Badge. */
|
|
226
|
+
&.danger {
|
|
227
|
+
&.solid {
|
|
228
|
+
background: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
229
|
+
color: var(--button-danger-color, var(--control-text-contrast, #fff));
|
|
230
|
+
border-color: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
231
|
+
|
|
232
|
+
&:hover {
|
|
233
|
+
background: var(
|
|
234
|
+
--button-danger-bg-hover,
|
|
235
|
+
var(--intent-danger-text, #b91c1c)
|
|
236
|
+
);
|
|
237
|
+
color: var(--button-danger-color-hover, var(--control-text-contrast, #fff));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
&.outline {
|
|
242
|
+
background: transparent;
|
|
243
|
+
color: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
244
|
+
border: 1px solid
|
|
245
|
+
var(--button-danger-border, var(--intent-danger-border, #dc2626));
|
|
246
|
+
|
|
247
|
+
&:hover {
|
|
248
|
+
background: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
249
|
+
color: var(--button-danger-color, var(--control-text-contrast, #fff));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
&.ghost {
|
|
254
|
+
background: transparent;
|
|
255
|
+
color: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
256
|
+
|
|
257
|
+
&:hover {
|
|
258
|
+
background: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
259
|
+
color: var(--button-danger-color, var(--control-text-contrast, #fff));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
&.link {
|
|
264
|
+
background: transparent;
|
|
265
|
+
color: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
266
|
+
|
|
267
|
+
&:hover {
|
|
268
|
+
text-decoration: underline;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
223
273
|
/* size: 'small' | 'medium' | 'large' || 'default'*/
|
|
224
274
|
&.small {
|
|
225
275
|
padding: var(--button-padding-sm-y) var(--button-padding-sm-x);
|
|
@@ -9,7 +9,10 @@ export interface ConfirmDialogProps {
|
|
|
9
9
|
cancelText?: string;
|
|
10
10
|
/**
|
|
11
11
|
* `danger` for a destructive action. Not a colour — a statement about consequence, which
|
|
12
|
-
* is what `--intent-danger-*` exists to express.
|
|
12
|
+
* is what `--intent-danger-*` exists to express. It reaches the confirm button as
|
|
13
|
+
* `<Button color="danger">`, so an app re-skins it through `--button-danger-*` (or the
|
|
14
|
+
* `--intent-danger-*` family behind it) rather than through a `--confirm-dialog-danger-*`
|
|
15
|
+
* of its own.
|
|
13
16
|
*/
|
|
14
17
|
tone?: 'default' | 'danger';
|
|
15
18
|
/** The confirm action is in flight: both buttons lock. */
|
|
@@ -63,6 +63,6 @@ function ConfirmDialog({ open, onOpenChange, onConfirm, title, message, confirmT
|
|
|
63
63
|
React.createElement(AlertDialog_js_1.AlertDialog.Footer, null,
|
|
64
64
|
React.createElement(AlertDialog_js_1.AlertDialog.Cancel, { asChild: true },
|
|
65
65
|
React.createElement(Button_js_1.Button, { variant: "ghost", disabled: isLoading }, cancelText)),
|
|
66
|
-
React.createElement(Button_js_1.Button, {
|
|
66
|
+
React.createElement(Button_js_1.Button, { color: tone === 'danger' ? 'danger' : 'primary', onClick: onConfirm, disabled: isLoading || confirmDisabled }, isLoading && loadingContent ? loadingContent : confirmText)))));
|
|
67
67
|
}
|
|
68
68
|
//# sourceMappingURL=ConfirmDialog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConfirmDialog.js","sourceRoot":"","sources":["../../../src/components/ConfirmDialog.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"ConfirmDialog.js","sourceRoot":"","sources":["../../../src/components/ConfirmDialog.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,sCAqDC;AAlGD,6CAA+B;AAC/B,gEAAmD;AACnD,qDAA6C;AAC7C,2CAAmC;AACnC,0FAA+C;AA6B/C;;;;;;;;;;;GAWG;AACH,SAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,KAAK,EACL,OAAO,EACP,WAAW,GAAG,SAAS,EACvB,UAAU,GAAG,QAAQ,EACrB,IAAI,GAAG,SAAS,EAChB,SAAS,GAAG,KAAK,EACjB,eAAe,GAAG,KAAK,EACvB,cAAc,EACd,IAAI,GAAG,KAAK,EACZ,SAAS,GACU;IACnB,OAAO,CACL,oBAAC,4BAAW,CAAC,IAAI,IAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY;QACtD,oBAAC,4BAAW,CAAC,OAAO,IAClB,SAAS,EAAE,IAAA,eAAE,EAAC,kCAAK,CAAC,OAAO,EAAE,IAAI,IAAI,kCAAK,CAAC,IAAI,EAAE,SAAS,CAAC;YAE3D,oBAAC,4BAAW,CAAC,MAAM;gBACjB,oBAAC,4BAAW,CAAC,KAAK,QAAE,KAAK,CAAqB;gBAI9C,oBAAC,4BAAW,CAAC,WAAW,IAAC,OAAO,EAAE,OAAO,OAAO,KAAK,QAAQ,IAC1D,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,iCAAM,OAAO,CAAO,CACrC,CACP;YACrB,oBAAC,4BAAW,CAAC,MAAM;gBACjB,oBAAC,4BAAW,CAAC,MAAM,IAAC,OAAO;oBACzB,oBAAC,kBAAM,IAAC,OAAO,EAAC,OAAO,EAAC,QAAQ,EAAE,SAAS,IACxC,UAAU,CACJ,CACU;gBAQrB,oBAAC,kBAAM,IACL,KAAK,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,EAC/C,OAAO,EAAE,SAAS,EAClB,QAAQ,EAAE,SAAS,IAAI,eAAe,IAErC,SAAS,IAAI,cAAc,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,WAAW,CACpD,CACU,CACD,CACL,CACpB,CAAC;AACJ,CAAC"}
|
|
@@ -8,15 +8,3 @@
|
|
|
8
8
|
.wide {
|
|
9
9
|
max-width: var(--confirm-dialog-width-wide, 56rem);
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
/* Destructive confirmation. Reads the intent family rather than a red primitive, so an app
|
|
13
|
-
that restyles what "danger" looks like restyles this too. */
|
|
14
|
-
.danger {
|
|
15
|
-
background: var(--confirm-dialog-danger-bg, var(--intent-danger-icon));
|
|
16
|
-
border-color: var(--confirm-dialog-danger-border, var(--intent-danger-border));
|
|
17
|
-
color: var(--confirm-dialog-danger-text, var(--color-white));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.danger:hover:not(:disabled) {
|
|
21
|
-
background: var(--confirm-dialog-danger-bg-hover, var(--intent-danger-text));
|
|
22
|
-
}
|
package/lib/cjs/package.json
CHANGED
|
@@ -2,7 +2,13 @@ import * as React from 'react';
|
|
|
2
2
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
3
3
|
asChild?: boolean;
|
|
4
4
|
variant?: 'solid' | 'outline' | 'ghost' | 'link';
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Which colour family the button speaks in. `danger` is the destructive one — a
|
|
7
|
+
* statement about consequence, and a Button prop rather than a class a caller adds,
|
|
8
|
+
* because the colour classes here are compound (`.Root.primary.solid`) and a bare
|
|
9
|
+
* class handed in through `className` loses the cascade to them every time.
|
|
10
|
+
*/
|
|
11
|
+
color?: 'primary' | 'secondary' | 'tertiary' | 'danger';
|
|
6
12
|
size?: 'small' | 'medium' | 'large' | 'default';
|
|
7
13
|
}
|
|
8
14
|
declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.js","sourceRoot":"","sources":["../../../src/components/Button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,MAAM,qBAAqB,CAAC;AACxC,OAAO,EAAE,EAAE,EAAE,MAAM,iCAAiC,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"Button.js","sourceRoot":"","sources":["../../../src/components/Button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,MAAM,qBAAqB,CAAC;AACxC,OAAO,EAAE,EAAE,EAAE,MAAM,iCAAiC,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAgB5C,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAC7B,CACE,EACE,SAAS,EACT,OAAO,GAAG,OAAO,EACjB,KAAK,GAAG,SAAS,EACjB,IAAI,GAAG,SAAS,EAChB,OAAO,GAAG,KAAK,EACf,GAAG,KAAK,EACT,EACD,GAAG,EACH,EAAE;IACF,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;IACvC,MAAM,UAAU,GAAG,KAAK,CAAC,QAAQ,CAAC;IAElC,OAAO,CACL,oBAAC,IAAI,IACH,SAAS,EAAE,EAAE,CACX,KAAK,CAAC,IAAI,EACV,KAAK,CAAC,OAAO,CAAC,EACd,KAAK,CAAC,KAAK,CAAC,EACZ,KAAK,CAAC,IAAI,CAAC,EACX,UAAU,IAAI,KAAK,CAAC,QAAQ,EAC5B,SAAS,CACV,EACD,GAAG,EAAE,GAAG,KACJ,KAAK,GACT,CACH,CAAC;AACJ,CAAC,CACF,CAAC;AACF,MAAM,CAAC,WAAW,GAAG,QAAQ,CAAC;AAE9B,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
|
@@ -220,6 +220,56 @@
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/* Destructive. Reads the intent family rather than a red primitive, so an app that
|
|
224
|
+
restyles what "danger" looks like restyles this too; the literal is the last-resort
|
|
225
|
+
fallback for a theme that defines neither, same contract as Badge. */
|
|
226
|
+
&.danger {
|
|
227
|
+
&.solid {
|
|
228
|
+
background: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
229
|
+
color: var(--button-danger-color, var(--control-text-contrast, #fff));
|
|
230
|
+
border-color: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
231
|
+
|
|
232
|
+
&:hover {
|
|
233
|
+
background: var(
|
|
234
|
+
--button-danger-bg-hover,
|
|
235
|
+
var(--intent-danger-text, #b91c1c)
|
|
236
|
+
);
|
|
237
|
+
color: var(--button-danger-color-hover, var(--control-text-contrast, #fff));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
&.outline {
|
|
242
|
+
background: transparent;
|
|
243
|
+
color: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
244
|
+
border: 1px solid
|
|
245
|
+
var(--button-danger-border, var(--intent-danger-border, #dc2626));
|
|
246
|
+
|
|
247
|
+
&:hover {
|
|
248
|
+
background: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
249
|
+
color: var(--button-danger-color, var(--control-text-contrast, #fff));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
&.ghost {
|
|
254
|
+
background: transparent;
|
|
255
|
+
color: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
256
|
+
|
|
257
|
+
&:hover {
|
|
258
|
+
background: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
259
|
+
color: var(--button-danger-color, var(--control-text-contrast, #fff));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
&.link {
|
|
264
|
+
background: transparent;
|
|
265
|
+
color: var(--button-danger-bg, var(--intent-danger-icon, #dc2626));
|
|
266
|
+
|
|
267
|
+
&:hover {
|
|
268
|
+
text-decoration: underline;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
223
273
|
/* size: 'small' | 'medium' | 'large' || 'default'*/
|
|
224
274
|
&.small {
|
|
225
275
|
padding: var(--button-padding-sm-y) var(--button-padding-sm-x);
|
|
@@ -9,7 +9,10 @@ export interface ConfirmDialogProps {
|
|
|
9
9
|
cancelText?: string;
|
|
10
10
|
/**
|
|
11
11
|
* `danger` for a destructive action. Not a colour — a statement about consequence, which
|
|
12
|
-
* is what `--intent-danger-*` exists to express.
|
|
12
|
+
* is what `--intent-danger-*` exists to express. It reaches the confirm button as
|
|
13
|
+
* `<Button color="danger">`, so an app re-skins it through `--button-danger-*` (or the
|
|
14
|
+
* `--intent-danger-*` family behind it) rather than through a `--confirm-dialog-danger-*`
|
|
15
|
+
* of its own.
|
|
13
16
|
*/
|
|
14
17
|
tone?: 'default' | 'danger';
|
|
15
18
|
/** The confirm action is in flight: both buttons lock. */
|
|
@@ -24,6 +24,6 @@ export function ConfirmDialog({ open, onOpenChange, onConfirm, title, message, c
|
|
|
24
24
|
React.createElement(AlertDialog.Footer, null,
|
|
25
25
|
React.createElement(AlertDialog.Cancel, { asChild: true },
|
|
26
26
|
React.createElement(Button, { variant: "ghost", disabled: isLoading }, cancelText)),
|
|
27
|
-
React.createElement(Button, {
|
|
27
|
+
React.createElement(Button, { color: tone === 'danger' ? 'danger' : 'primary', onClick: onConfirm, disabled: isLoading || confirmDisabled }, isLoading && loadingContent ? loadingContent : confirmText)))));
|
|
28
28
|
}
|
|
29
29
|
//# sourceMappingURL=ConfirmDialog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConfirmDialog.js","sourceRoot":"","sources":["../../../src/components/ConfirmDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAC,EAAE,EAAC,MAAM,iCAAiC,CAAC;AACnD,OAAO,EAAC,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AACnC,OAAO,KAAK,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"ConfirmDialog.js","sourceRoot":"","sources":["../../../src/components/ConfirmDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAC,EAAE,EAAC,MAAM,iCAAiC,CAAC;AACnD,OAAO,EAAC,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AACnC,OAAO,KAAK,MAAM,4BAA4B,CAAC;AA6B/C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,EAC5B,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,KAAK,EACL,OAAO,EACP,WAAW,GAAG,SAAS,EACvB,UAAU,GAAG,QAAQ,EACrB,IAAI,GAAG,SAAS,EAChB,SAAS,GAAG,KAAK,EACjB,eAAe,GAAG,KAAK,EACvB,cAAc,EACd,IAAI,GAAG,KAAK,EACZ,SAAS,GACU;IACnB,OAAO,CACL,oBAAC,WAAW,CAAC,IAAI,IAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY;QACtD,oBAAC,WAAW,CAAC,OAAO,IAClB,SAAS,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,SAAS,CAAC;YAE3D,oBAAC,WAAW,CAAC,MAAM;gBACjB,oBAAC,WAAW,CAAC,KAAK,QAAE,KAAK,CAAqB;gBAI9C,oBAAC,WAAW,CAAC,WAAW,IAAC,OAAO,EAAE,OAAO,OAAO,KAAK,QAAQ,IAC1D,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,iCAAM,OAAO,CAAO,CACrC,CACP;YACrB,oBAAC,WAAW,CAAC,MAAM;gBACjB,oBAAC,WAAW,CAAC,MAAM,IAAC,OAAO;oBACzB,oBAAC,MAAM,IAAC,OAAO,EAAC,OAAO,EAAC,QAAQ,EAAE,SAAS,IACxC,UAAU,CACJ,CACU;gBAQrB,oBAAC,MAAM,IACL,KAAK,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,EAC/C,OAAO,EAAE,SAAS,EAClB,QAAQ,EAAE,SAAS,IAAI,eAAe,IAErC,SAAS,IAAI,cAAc,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,WAAW,CACpD,CACU,CACD,CACL,CACpB,CAAC;AACJ,CAAC"}
|
|
@@ -8,15 +8,3 @@
|
|
|
8
8
|
.wide {
|
|
9
9
|
max-width: var(--confirm-dialog-width-wide, 56rem);
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
/* Destructive confirmation. Reads the intent family rather than a red primitive, so an app
|
|
13
|
-
that restyles what "danger" looks like restyles this too. */
|
|
14
|
-
.danger {
|
|
15
|
-
background: var(--confirm-dialog-danger-bg, var(--intent-danger-icon));
|
|
16
|
-
border-color: var(--confirm-dialog-danger-border, var(--intent-danger-border));
|
|
17
|
-
color: var(--confirm-dialog-danger-text, var(--color-white));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.danger:hover:not(:disabled) {
|
|
21
|
-
background: var(--confirm-dialog-danger-bg-hover, var(--intent-danger-text));
|
|
22
|
-
}
|
package/lib/esm/package.json
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
|
|
6
|
+
const dir = fileURLToPath(new URL('../src/components/', import.meta.url));
|
|
7
|
+
const read = (name) => readFileSync(dir + name, 'utf8');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Button colours itself with COMPOUND selectors — `.Root.primary.solid`, specificity
|
|
11
|
+
* 0-3-0. A sibling component that recolours a Button by handing it a bare class of its
|
|
12
|
+
* own module (0-1-0) therefore loses the cascade silently: the class is on the element,
|
|
13
|
+
* the DOM looks right, and the colour is Button's. That is exactly how `ConfirmDialog`'s
|
|
14
|
+
* `tone="danger"` came to be visually inert while looking correct in every review.
|
|
15
|
+
*
|
|
16
|
+
* These are STYLESHEET tests. They read the CSS as text and compare selector
|
|
17
|
+
* specificity; they do not render anything, so they prove that no competing
|
|
18
|
+
* lower-specificity declaration exists — NOT that the button comes out red. The rendered
|
|
19
|
+
* colour was measured in a browser (computed `background-color`) when this was fixed;
|
|
20
|
+
* this package has no DOM test runner to keep doing that.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Class selectors only, which is all a CSS module needs. Returns [a, b, c]. */
|
|
24
|
+
const specificity = (selector) => [
|
|
25
|
+
(selector.match(/#[\w-]+/g) ?? []).length,
|
|
26
|
+
(selector.match(/\.[\w-]+/g) ?? []).length +
|
|
27
|
+
(selector.match(/\[[^\]]+\]/g) ?? []).length +
|
|
28
|
+
(selector.match(/:(?!:)(?!hover|focus|active|disabled|not)[\w-]+/g) ?? [])
|
|
29
|
+
.length,
|
|
30
|
+
0,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const compare = (x, y) =>
|
|
34
|
+
x[0] - y[0] || x[1] - y[1] || x[2] - y[2];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Strip comments and statement at-rules, then walk `{}` keeping a stack of `&`-expanded
|
|
38
|
+
* selectors. Each entry is one rule with the declarations written directly in it.
|
|
39
|
+
*/
|
|
40
|
+
function rules(css) {
|
|
41
|
+
const source = css
|
|
42
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
43
|
+
.replace(/@(?:import|charset|mixin)[^;{]*;/g, '');
|
|
44
|
+
const out = [];
|
|
45
|
+
const stack = [];
|
|
46
|
+
let buffer = '';
|
|
47
|
+
for (const ch of source) {
|
|
48
|
+
if (ch === '{') {
|
|
49
|
+
const parents = stack.length ? stack[stack.length - 1].selectors : [''];
|
|
50
|
+
const selectors = buffer
|
|
51
|
+
.trim()
|
|
52
|
+
.split(',')
|
|
53
|
+
.map((s) => s.trim())
|
|
54
|
+
.flatMap((sel) =>
|
|
55
|
+
parents.map((parent) =>
|
|
56
|
+
sel.includes('&')
|
|
57
|
+
? sel.replace(/&/g, parent)
|
|
58
|
+
: parent
|
|
59
|
+
? `${parent} ${sel}`
|
|
60
|
+
: sel
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
const rule = { selectors, declarations: '' };
|
|
64
|
+
out.push(rule);
|
|
65
|
+
stack.push(rule);
|
|
66
|
+
buffer = '';
|
|
67
|
+
} else if (ch === '}') {
|
|
68
|
+
stack.pop();
|
|
69
|
+
buffer = '';
|
|
70
|
+
} else {
|
|
71
|
+
buffer += ch;
|
|
72
|
+
// A `;` at this depth closes a declaration of the innermost open rule.
|
|
73
|
+
if (ch === ';') {
|
|
74
|
+
if (stack.length) stack[stack.length - 1].declarations += buffer;
|
|
75
|
+
buffer = '';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Highest specificity at which `prop` is set anywhere in this stylesheet. */
|
|
83
|
+
function maxSpecificityFor(css, prop) {
|
|
84
|
+
let best = null;
|
|
85
|
+
for (const rule of rules(css)) {
|
|
86
|
+
if (!new RegExp(`(^|;)\\s*${prop}\\s*:`).test(rule.declarations)) continue;
|
|
87
|
+
for (const selector of rule.selectors) {
|
|
88
|
+
const s = specificity(selector);
|
|
89
|
+
if (!best || compare(s, best.specificity) > 0)
|
|
90
|
+
best = { specificity: s, selector: selector.trim() };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return best;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
test('every Button color in the union is actually styled', () => {
|
|
97
|
+
const source = read('Button.tsx');
|
|
98
|
+
const stylesheet = read('Button.module.css');
|
|
99
|
+
|
|
100
|
+
const union = source.match(/color\?:([^;]+);/);
|
|
101
|
+
assert.ok(union, 'color prop not found in Button.tsx');
|
|
102
|
+
|
|
103
|
+
const colors = [...union[1].matchAll(/'([a-z-]+)'/g)].map((m) => m[1]);
|
|
104
|
+
assert.ok(colors.includes('danger'), `expected a danger colour, got ${colors}`);
|
|
105
|
+
|
|
106
|
+
for (const color of colors) {
|
|
107
|
+
assert.match(
|
|
108
|
+
stylesheet,
|
|
109
|
+
new RegExp(`&\\.${color}\\s*\\{`),
|
|
110
|
+
`Button color "${color}" is accepted by the type but has no rules in Button.module.css`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('ConfirmDialog expresses tone through Button, not through a class of its own', () => {
|
|
116
|
+
const source = read('ConfirmDialog.tsx');
|
|
117
|
+
|
|
118
|
+
assert.match(
|
|
119
|
+
source,
|
|
120
|
+
/color=\{tone === 'danger' \? 'danger' : 'primary'\}/,
|
|
121
|
+
'ConfirmDialog no longer passes tone to Button as a colour prop'
|
|
122
|
+
);
|
|
123
|
+
assert.doesNotMatch(
|
|
124
|
+
source,
|
|
125
|
+
/className=\{cl\(tone === 'danger'/,
|
|
126
|
+
'ConfirmDialog is back to overriding Button with a bare class, which loses the cascade'
|
|
127
|
+
);
|
|
128
|
+
assert.equal(
|
|
129
|
+
maxSpecificityFor(read('ConfirmDialog.module.css'), 'background'),
|
|
130
|
+
null,
|
|
131
|
+
'ConfirmDialog.module.css sets a background again — it cannot outrank Button from here'
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* MultiSelect is a KNOWN, MEASURED instance of the same trap, left as-is deliberately:
|
|
137
|
+
* its `.Root .button` (0-2-0) sets `background-color` on a `<Button>` whose
|
|
138
|
+
* `.Root.primary.solid` (0-3-0) sets `background`, so `--multiselect-selected-bg` is
|
|
139
|
+
* inert and the chip renders in `--button-primary-bg`. Fixing it is a separate design
|
|
140
|
+
* decision (the chip also nests a `<button>` inside a `<button>`), so it is recorded
|
|
141
|
+
* here rather than silently swept into the ConfirmDialog fix.
|
|
142
|
+
*/
|
|
143
|
+
const KNOWN_INERT = new Set(['MultiSelect.tsx']);
|
|
144
|
+
|
|
145
|
+
test('no component recolours a Button with a class that cannot outrank it', () => {
|
|
146
|
+
const buttonBackground = maxSpecificityFor(read('Button.module.css'), 'background');
|
|
147
|
+
assert.ok(buttonBackground, 'Button.module.css no longer sets a background');
|
|
148
|
+
|
|
149
|
+
const offenders = [];
|
|
150
|
+
for (const file of readdirSync(dir).filter((f) => f.endsWith('.tsx'))) {
|
|
151
|
+
if (file === 'Button.tsx') continue;
|
|
152
|
+
const source = read(file);
|
|
153
|
+
const stylesheet = (() => {
|
|
154
|
+
try {
|
|
155
|
+
return read(file.replace(/\.tsx$/, '.module.css'));
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
})();
|
|
160
|
+
if (!stylesheet) continue;
|
|
161
|
+
|
|
162
|
+
for (const call of source.match(/<Button[\s\S]{0,400}?>/g) ?? []) {
|
|
163
|
+
for (const [, cls] of call.matchAll(/style\.([A-Za-z][\w]*)/g)) {
|
|
164
|
+
for (const prop of ['background', 'background-color', 'border-color']) {
|
|
165
|
+
const own = maxSpecificityFor(stylesheet, prop);
|
|
166
|
+
if (!own) continue;
|
|
167
|
+
if (!own.selector.includes(`.${cls}`)) continue;
|
|
168
|
+
if (compare(own.specificity, buttonBackground.specificity) < 0)
|
|
169
|
+
offenders.push(
|
|
170
|
+
`${file}: "${own.selector}" (${own.specificity.join('-')}) sets ${prop} on a ` +
|
|
171
|
+
`<Button> whose "${buttonBackground.selector}" ` +
|
|
172
|
+
`(${buttonBackground.specificity.join('-')}) wins — pass a Button prop instead`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const unexpected = offenders.filter(
|
|
180
|
+
(o) => !KNOWN_INERT.has(o.slice(0, o.indexOf(':')))
|
|
181
|
+
);
|
|
182
|
+
assert.deepEqual(unexpected, [], unexpected.join('\n'));
|
|
183
|
+
});
|