@_linked/primitives 1.3.2 → 1.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 CHANGED
@@ -1,5 +1,47 @@
1
1
  # @\_linked/primitives
2
2
 
3
+ ## 1.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#41](https://github.com/linked-fw/primitives/pull/41) [`cb999cc`](https://github.com/linked-fw/primitives/commit/cb999cc24a80dc95773458075a38fdf0d07ddd8b) Thanks [@flyon](https://github.com/flyon)! - Require `@_linked/core@^2.22.8` (was `^2.0.1`), and pin it in the lockfile.
8
+
9
+ The declared range was wide enough that the resolved core depended on whatever the
10
+ consumer — or this repo's own CI, via `package-lock.json` — happened to install. Core
11
+ decides how a shape's IRI is minted, so a stale core made this package emit legacy
12
+ `data.lincd.org` IRIs instead of the arch-02 `linked.cm` scheme. Which IRIs a published
13
+ package produces should not be a function of the installer's dependency tree.
14
+
15
+ Minor rather than patch: this raises the minimum core a consumer must resolve, so it
16
+ changes what gets installed rather than only what this package does internally.
17
+
18
+ ## 1.4.0
19
+
20
+ ### Minor Changes
21
+
22
+ - [#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
23
+
24
+ `tone="danger"` applied a `.danger` class of ConfirmDialog's own stylesheet to the confirm
25
+ button. That class has specificity 0-1-0, while `Button` colours itself with compound
26
+ selectors — `.Root.primary.solid`, 0-3-0 — so the override never won. Measured computed
27
+ `background-color` on the danger button was `rgb(37, 99, 235)` (`--button-primary-bg`),
28
+ identical to a default confirm button. Every destructive confirmation therefore rendered in
29
+ the primary colour: the class was on the element, and the colour was not.
30
+
31
+ Colour is a `Button` prop, so that is now how tone travels: `Button`'s `color` union gains
32
+ `'danger'` with rules for all four variants, reading `--button-danger-*` and falling back to
33
+ the `--intent-danger-*` family, and `ConfirmDialog` passes `color="danger"` instead of a
34
+ class. Computed `background-color` is now `rgb(239, 68, 68)` (`--intent-danger-icon`), with
35
+ `--intent-danger-text` on hover.
36
+
37
+ `color="danger"` is new public API on `Button`. The `--confirm-dialog-danger-bg`,
38
+ `--confirm-dialog-danger-bg-hover`, `--confirm-dialog-danger-border` and
39
+ `--confirm-dialog-danger-text` custom properties are gone — they were read only by the rule
40
+ that never applied, so nothing that worked has been removed. Re-skin the destructive button
41
+ through `--button-danger-bg` / `--button-danger-bg-hover` / `--button-danger-border` /
42
+ `--button-danger-color`, or through `--intent-danger-*` to move every danger affordance at
43
+ once.
44
+
3
45
  ## 1.3.2
4
46
 
5
47
  ### 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
- color?: 'primary' | 'secondary' | 'tertiary';
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;AAU5C,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"}
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, { className: (0, ClassNames_1.cl)(tone === 'danger' && ConfirmDialog_module_css_1.default.danger), onClick: onConfirm, disabled: isLoading || confirmDisabled }, isLoading && loadingContent ? loadingContent : confirmText)))));
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,sCAiDC;AA3FD,6CAA+B;AAC/B,gEAAmD;AACnD,qDAA6C;AAC7C,2CAAmC;AACnC,0FAA+C;AA0B/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;gBAIrB,oBAAC,kBAAM,IACL,SAAS,EAAE,IAAA,eAAE,EAAC,IAAI,KAAK,QAAQ,IAAI,kCAAK,CAAC,MAAM,CAAC,EAChD,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"}
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
- }
@@ -10,7 +10,7 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/linked-fw/primitives.git"
12
12
  },
13
- "version": "1.3.2",
13
+ "version": "1.5.0",
14
14
  "packageManager": "npm@11.19.1",
15
15
  "linkedPackage": true,
16
16
  "license": "ISC",
@@ -31,7 +31,7 @@
31
31
  "web3"
32
32
  ],
33
33
  "dependencies": {
34
- "@_linked/core": "^2.0.1",
34
+ "@_linked/core": "^2.22.8",
35
35
  "@_linked/css": "^0.2.0",
36
36
  "@_linked/react": "^1.2",
37
37
  "@radix-ui/react-accordion": "^1.1.2",
@@ -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
- color?: 'primary' | 'secondary' | 'tertiary';
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;AAU5C,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"}
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, { className: cl(tone === 'danger' && style.danger), onClick: onConfirm, disabled: isLoading || confirmDisabled }, isLoading && loadingContent ? loadingContent : confirmText)))));
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;AA0B/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;gBAIrB,oBAAC,MAAM,IACL,SAAS,EAAE,EAAE,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,CAAC,EAChD,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"}
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
- }
@@ -10,7 +10,7 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/linked-fw/primitives.git"
12
12
  },
13
- "version": "1.3.2",
13
+ "version": "1.5.0",
14
14
  "packageManager": "npm@11.19.1",
15
15
  "linkedPackage": true,
16
16
  "license": "ISC",
@@ -31,7 +31,7 @@
31
31
  "web3"
32
32
  ],
33
33
  "dependencies": {
34
- "@_linked/core": "^2.0.1",
34
+ "@_linked/core": "^2.22.8",
35
35
  "@_linked/css": "^0.2.0",
36
36
  "@_linked/react": "^1.2",
37
37
  "@radix-ui/react-accordion": "^1.1.2",
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/linked-fw/primitives.git"
12
12
  },
13
- "version": "1.3.2",
13
+ "version": "1.5.0",
14
14
  "packageManager": "npm@11.19.1",
15
15
  "linkedPackage": true,
16
16
  "main": "lib/cjs/index.js",
@@ -33,7 +33,7 @@
33
33
  "web3"
34
34
  ],
35
35
  "dependencies": {
36
- "@_linked/core": "^2.0.1",
36
+ "@_linked/core": "^2.22.8",
37
37
  "@_linked/css": "^0.2.0",
38
38
  "@_linked/react": "^1.2",
39
39
  "@radix-ui/react-accordion": "^1.1.2",
@@ -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
+ });