@attrus-ds/core 1.0.19 → 1.1.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.
@@ -142,9 +142,11 @@
142
142
  .btn-primary:focus-visible { box-shadow: var(--shadow-focus); }
143
143
 
144
144
 
145
- /* SECONDARY — soft neutral fill + hairline border */
145
+ /* SECONDARY — hairline border, no resting fill. The fill arrives on hover:
146
+ a filled secondary at rest competed with the primary for weight, and on a
147
+ subtle surface it read as a disabled block rather than a button. */
146
148
  .btn-secondary {
147
- background: var(--color-background-subtle);
149
+ background: transparent;
148
150
  color: var(--color-foreground-default);
149
151
  border-color: var(--color-border-default);
150
152
  }
@@ -216,7 +218,7 @@
216
218
 
217
219
  /* DANGER OUTLINE — destructive contextual */
218
220
  .btn-danger-outline {
219
- background: var(--color-background-surface);
221
+ background: transparent;
220
222
  color: var(--color-danger-500);
221
223
  border-color: var(--color-feedback-danger-border);
222
224
  }
@@ -361,7 +363,8 @@
361
363
  in a segmented control, and styling from it keeps markup and look in step. */
362
364
  .btn-group .btn[aria-pressed="true"] {
363
365
  box-shadow: inset 0 0 0 999px color-mix(in srgb, currentColor 14%, transparent);
364
- font-weight: var(--w-bold);
366
+ /* No weight change: the fill already marks the selection, and a bolder label
367
+ widens the segment on toggle, nudging every neighbour sideways. */
365
368
  /* The overlay stops at the border, and the next segment overlaps this one by a
366
369
  hairline — so without this the selected segment ended in a 1px light strip
367
370
  (its own untinted border, covered by the neighbour's fill). Lifting it above
@@ -1302,3 +1302,121 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
1302
1302
  background: color-mix(in srgb, var(--color-pure-white) 6%, transparent);
1303
1303
  border-color: var(--color-border-inverse);
1304
1304
  }
1305
+
1306
+ /* ----------------------------------------------------------------
1307
+ INSET ACTION BUTTON — .fix-btn reads as a button, not a label
1308
+ ----------------------------------------------------------------
1309
+ A flush segment (divider + field fill) is the right language for a
1310
+ SELECTOR: it qualifies the value, so it belongs to the field's surface, and
1311
+ the chevron already says "dropdown". But the same treatment on an ACTION
1312
+ made it read as a static label glued to the field. An action is its own
1313
+ control, so it sits INSIDE the field as a button: inset from the edge,
1314
+ its own border and radius, its own fill. .fix-select is untouched.
1315
+ The inset is 6px on every side; the button's radius is the field's minus
1316
+ that inset, so the two curves stay concentric.
1317
+ ---------------------------------------------------------------- */
1318
+ :is(.group, .pill-search) > .fix-btn {
1319
+ --fix-inset: 6px;
1320
+ align-self: stretch;
1321
+ height: auto;
1322
+ margin: var(--fix-inset);
1323
+ padding: 0 var(--space-3);
1324
+ border: var(--border-hairline) solid var(--color-border-default);
1325
+ /* Concentric (field radius − inset) comes out ~2px on a 6px field, which
1326
+ reads as a square chip. Floor it at --radius-xs so it still reads as a
1327
+ button; the curves stay visually parallel at this size. */
1328
+ border-radius: max(var(--radius-xs), calc(var(--shape-control) - var(--fix-inset)));
1329
+ background: var(--color-background-surface);
1330
+ color: var(--color-foreground-default);
1331
+ box-shadow: none;
1332
+ }
1333
+ /* the flush-segment dividers do not apply to a button that has its own border */
1334
+ :is(.group, .pill-search) > .fix-btn.l,
1335
+ :is(.group, .pill-search) > .fix-btn.r { border: var(--border-hairline) solid var(--color-border-default); }
1336
+ :is(.group, .pill-search) > .fix-btn.l { margin-right: 0; }
1337
+ :is(.group, .pill-search) > .fix-btn.r { margin-left: 0; }
1338
+ :is(.group, .pill-search) > .fix-btn:hover { background: var(--color-background-hover); border-color: var(--color-border-strong); color: var(--color-foreground-default); }
1339
+ :is(.group, .pill-search) > .fix-btn:active { background: var(--color-background-press); }
1340
+ :is(.group, .pill-search) > .fix-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--color-border-focus); }
1341
+ :is(.group, .pill-search) > .fix-btn.is-primary {
1342
+ background: var(--color-brand-solid);
1343
+ border-color: transparent;
1344
+ color: var(--color-foreground-on-solid);
1345
+ }
1346
+ :is(.group, .pill-search) > .fix-btn.is-primary:hover { background: var(--color-brand-solid-hover); border-color: transparent; color: var(--color-foreground-on-solid); }
1347
+ :is(.group, .pill-search) > .fix-btn.is-primary:focus-visible { box-shadow: 0 0 0 2px var(--color-border-focus); }
1348
+
1349
+ /* Pill field → pill button (concentric), and the end-padding the flush
1350
+ segment needed to clear the round cap is no longer needed */
1351
+ :is(.group.is-pill, .pill-search) > .fix-btn,
1352
+ :is(.group.is-pill, .pill-search) > .fix-btn:first-child,
1353
+ :is(.group.is-pill, .pill-search) > .fix-btn:last-child { border-radius: var(--radius-pill); padding: 0 var(--space-4); }
1354
+ /* the pill clips its children for the flush selector; a clipped focus ring
1355
+ on the inset button would be cut, so the ring is drawn inside instead */
1356
+ .pill-search > .fix-btn:focus-visible,
1357
+ .pill-search > .fix-btn.is-primary:focus-visible { box-shadow: inset 0 0 0 2px var(--color-border-focus); }
1358
+ .pill-search > .fix-btn.is-primary:focus-visible { box-shadow: inset 0 0 0 2px var(--color-foreground-on-solid), 0 0 0 0 transparent; }
1359
+
1360
+ /* Underline field → a compact bordered button on the rule, not floating text */
1361
+ .group.underline > .fix-btn {
1362
+ align-self: center;
1363
+ height: calc(100% - 12px);
1364
+ margin: 0 0 0 var(--space-2);
1365
+ padding: 0 var(--space-3);
1366
+ border: var(--border-hairline) solid var(--color-border-default);
1367
+ border-radius: var(--shape-control);
1368
+ background: var(--color-background-surface);
1369
+ color: var(--color-foreground-default);
1370
+ font-weight: var(--w-regular);
1371
+ text-decoration: none;
1372
+ }
1373
+ .group.underline > .fix-btn:first-child { margin: 0 var(--space-2) 0 0; }
1374
+ .group.underline > .fix-btn:last-child { margin-right: 0; }
1375
+ .group.underline > .fix-btn.is-primary { background: var(--color-brand-solid); border-color: transparent; color: var(--color-foreground-on-solid); font-weight: var(--w-regular); }
1376
+ .group.underline > .fix-btn.is-primary:hover { background: var(--color-brand-solid-hover); color: var(--color-foreground-on-solid); }
1377
+ .group.underline > .fix-btn:focus-visible,
1378
+ .group.underline > .fix-btn.is-primary:focus-visible { box-shadow: 0 0 0 2px var(--color-border-focus); text-decoration: none; }
1379
+
1380
+ /* on-inverse keeps the same shape, tinted for dark surfaces */
1381
+ .group.on-inverse > .fix-btn:not(.is-primary),
1382
+ .pill-search.on-inverse > .fix-btn:not(.is-primary) {
1383
+ background: color-mix(in srgb, var(--color-pure-white) 8%, transparent);
1384
+ border-color: var(--color-border-inverse);
1385
+ color: var(--color-foreground-inverse-default);
1386
+ }
1387
+
1388
+ /* ----------------------------------------------------------------
1389
+ ADDON VARIANTS — the inset button speaks the Button vocabulary
1390
+ ----------------------------------------------------------------
1391
+ .fix-btn takes the same emphasis ladder as .btn, with the same tokens, so
1392
+ "Apply" inside a field and "Apply" beside it read identically:
1393
+ (none) / .is-secondary neutral surface + hairline border (default)
1394
+ .is-primary brand fill — THE action of this field
1395
+ .is-tertiary soft neutral fill, borderless — quiet affirmative
1396
+ .is-ghost nothing until hover — secondary tools (copy, clear)
1397
+ .is-danger destructive solid
1398
+ .is-danger-outline destructive, contextual
1399
+ .is-success strong positive (rare)
1400
+ Not offered: .link (it would read as a label again — the problem this button
1401
+ exists to solve) and .inverse (the field's own on-inverse handles dark).
1402
+ ---------------------------------------------------------------- */
1403
+ :is(.group, .pill-search) > .fix-btn.is-tertiary {
1404
+ background: color-mix(in srgb, var(--color-foreground-default) 7%, transparent);
1405
+ border-color: transparent;
1406
+ color: var(--color-foreground-default);
1407
+ }
1408
+ :is(.group, .pill-search) > .fix-btn.is-tertiary:hover { background: color-mix(in srgb, var(--color-foreground-default) 12%, transparent); border-color: transparent; }
1409
+ :is(.group, .pill-search) > .fix-btn.is-ghost { background: transparent; border-color: transparent; color: var(--color-foreground-secondary); }
1410
+ :is(.group, .pill-search) > .fix-btn.is-ghost:hover { background: var(--color-background-hover); border-color: transparent; color: var(--color-foreground-default); }
1411
+ :is(.group, .pill-search) > .fix-btn.is-danger { background: var(--color-danger-500); border-color: transparent; color: var(--color-foreground-on-solid); }
1412
+ :is(.group, .pill-search) > .fix-btn.is-danger:hover { background: var(--color-danger-700); border-color: transparent; color: var(--color-foreground-on-solid); }
1413
+ :is(.group, .pill-search) > .fix-btn.is-danger-outline { background: var(--color-background-surface); border-color: var(--color-feedback-danger-border); color: var(--color-danger-500); }
1414
+ :is(.group, .pill-search) > .fix-btn.is-danger-outline:hover { background: var(--color-feedback-danger-background); border-color: var(--color-danger-500); color: var(--color-danger-500); }
1415
+ :is(.group, .pill-search) > .fix-btn.is-success { background: var(--color-success-500); border-color: transparent; color: var(--color-foreground-on-solid); }
1416
+ :is(.group, .pill-search) > .fix-btn.is-success:hover { background: var(--color-success-700); border-color: transparent; color: var(--color-foreground-on-solid); }
1417
+ /* the underline rules above set a surface fill; restate the variants there */
1418
+ .group.underline > .fix-btn.is-tertiary { background: color-mix(in srgb, var(--color-foreground-default) 7%, transparent); border-color: transparent; }
1419
+ .group.underline > .fix-btn.is-ghost { background: transparent; border-color: transparent; color: var(--color-foreground-secondary); }
1420
+ .group.underline > .fix-btn.is-danger { background: var(--color-danger-500); border-color: transparent; color: var(--color-foreground-on-solid); }
1421
+ .group.underline > .fix-btn.is-danger-outline { background: var(--color-background-surface); border-color: var(--color-feedback-danger-border); color: var(--color-danger-500); }
1422
+ .group.underline > .fix-btn.is-success { background: var(--color-success-500); border-color: transparent; color: var(--color-foreground-on-solid); }
@@ -3,9 +3,15 @@ import * as React from 'react';
3
3
  export type InputSize = 'sm' | 'md' | 'lg';
4
4
  export type InputStatus = 'default' | 'error' | 'success' | 'warning';
5
5
 
6
+ export type InputAddonVariant =
7
+ | 'primary' | 'secondary' | 'tertiary' | 'ghost'
8
+ | 'danger' | 'danger-outline' | 'success';
9
+
6
10
  export type InputAddon =
7
11
  | { type: 'select'; options: Array<string | { value: string; label: React.ReactNode }>; value?: string; onChange?: (value: string) => void; 'aria-label': string }
8
- | { type: 'button'; label: React.ReactNode; icon?: React.ReactNode; onClick?: () => void; primary?: boolean; disabled?: boolean; 'aria-label'?: string };
12
+ | { type: 'button'; label: React.ReactNode; icon?: React.ReactNode; onClick?: () => void; variant?: InputAddonVariant;
13
+ /** @deprecated use variant: 'primary' */
14
+ primary?: boolean; disabled?: boolean; 'aria-label'?: string };
9
15
 
10
16
  export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'> {
11
17
  /** Field label (renders the .field wrapper). */
@@ -22,6 +22,13 @@ export type InputStatus = 'default' | 'error' | 'success' | 'warning';
22
22
  `.fix-btn`). Static text belongs in `prefix`/`suffix`; use an addon only
23
23
  when the cell is a CONTROL — a qualifier the user picks, or an action that
24
24
  consumes this field. */
25
+ /** Button emphasis available to an addon. Same names and tokens as Button;
26
+ 'link' and 'inverse' are left out — a link inside a field reads as a label,
27
+ and dark surfaces are the field's own on-inverse job. */
28
+ export type InputAddonVariant =
29
+ | 'primary' | 'secondary' | 'tertiary' | 'ghost'
30
+ | 'danger' | 'danger-outline' | 'success';
31
+
25
32
  export type InputAddon =
26
33
  | {
27
34
  type: 'select';
@@ -36,7 +43,10 @@ export type InputAddon =
36
43
  label: React.ReactNode;
37
44
  icon?: React.ReactNode;
38
45
  onClick?: () => void;
39
- /** Filled — only when this is THE action of the field. */
46
+ /** Emphasis — the Button ladder, minus link/inverse. Default 'secondary'.
47
+ 'primary' only when this is THE action of the field. */
48
+ variant?: InputAddonVariant;
49
+ /** @deprecated use variant: 'primary' */
40
50
  primary?: boolean;
41
51
  disabled?: boolean;
42
52
  'aria-label'?: string;
@@ -103,7 +113,7 @@ function renderAddon(a: InputAddon, side: 'l' | 'r'): React.ReactNode {
103
113
  return (
104
114
  <button
105
115
  type="button"
106
- className={['fix-btn', side, a.primary ? 'is-primary' : ''].filter(Boolean).join(' ')}
116
+ className={['fix-btn', side, (() => { const v = a.variant || (a.primary ? 'primary' : 'secondary'); return v === 'secondary' ? '' : 'is-' + v; })()].filter(Boolean).join(' ')}
107
117
  onClick={a.onClick}
108
118
  disabled={a.disabled}
109
119
  aria-label={a['aria-label']}
@@ -63,7 +63,7 @@ function renderAddon(a: InputAddon, side: 'l' | 'r'): React.ReactNode {
63
63
  );
64
64
  }
65
65
  return (
66
- <button type="button" className={['fix-btn', side, a.primary ? 'is-primary' : ''].filter(Boolean).join(' ')}
66
+ <button type="button" className={['fix-btn', side, (() => { const v = a.variant || (a.primary ? 'primary' : 'secondary'); return v === 'secondary' ? '' : 'is-' + v; })()].filter(Boolean).join(' ')}
67
67
  onClick={a.onClick} disabled={a.disabled} aria-label={a['aria-label']}>
68
68
  {a.icon}
69
69
  {a.label}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attrus-ds/core",
3
- "version": "1.0.19",
3
+ "version": "1.1.0",
4
4
  "description": "ATTRUS / Cabiros design system — design tokens, component styles, brand assets, fonts and an optional typed React component layer.",
5
5
  "keywords": [
6
6
  "design-system",