@usableapp/cardds 0.7.22 → 0.7.23

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
@@ -2,6 +2,14 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
4
4
 
5
+ ## 0.7.23 — 2026-09-29
6
+
7
+ ### Added
8
+ - `IconBtnGroup` (Lh 2026-09-29): up to FOUR icon-only `IconBtn`s in ONE frame. They share its width, with a rule between them in the frame's colour. At home in a `CardFoot` (card or sheet), where it takes the row. More than 4 → a dev warning.
9
+ - Plain: commands, each on its own `onClick`. Pressing dims the button; it never shrinks.
10
+ - `select="one"`: one on at a time. `select="many"`: each on or off by itself. A pressed button STAYS filled (`aria-pressed`). Each child names itself with `value`. The group takes `value` + `onChange`, or `defaultValue`.
11
+ - Knobs: `--icon-btn-group-h` · `-border` · `-radius` · `-bg` · `-ink` · `-rule` · `-on-bg` · `-on-ink`. Gallery: `/IconBtnGroup`.
12
+
5
13
  ## 0.7.22 — 2026-09-29
6
14
 
7
15
  ### Changed
package/css/actions.css CHANGED
@@ -93,6 +93,24 @@
93
93
  }
94
94
  .icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
95
95
  .icon-btn--sm { width: var(--icon-btn-sm-size, var(--tap-sm)); height: var(--icon-btn-sm-size, var(--tap-sm)); font-size: var(--fs-title); }
96
+ /* ---- icon button group: up to four icon-only commands in ONE frame (Lh 2026-09-29) ----
97
+ <div class="icon-btn-group" role="group"><button class="icon-btn" aria-label="…">…</button>…</div>
98
+ One pill as tall as a button, the buttons share its width, a hairline between them; in a foot it takes the row. */
99
+ .icon-btn-group {
100
+ display: flex; min-width: 0;
101
+ min-height: var(--icon-btn-group-h, var(--btn-h));
102
+ border: var(--border-w) solid var(--icon-btn-group-border, var(--outline));
103
+ border-radius: var(--icon-btn-group-radius, var(--r-chip));
104
+ background: var(--icon-btn-group-bg, var(--action-bg));
105
+ color: var(--icon-btn-group-ink, var(--action-ink));
106
+ overflow: clip;
107
+ }
108
+ .icon-btn-group > .icon-btn { flex: 1 1 0; min-width: 0; width: auto; height: auto; border: 0; border-radius: 0; background: transparent; color: inherit; box-shadow: none; }
109
+ .icon-btn-group > .icon-btn + .icon-btn { border-inline-start: var(--border-w) solid var(--icon-btn-group-rule, var(--icon-btn-group-border, var(--outline))); }
110
+ .card__foot > .icon-btn-group { flex: 1 1 0; }
111
+ .icon-btn-group > .icon-btn[aria-pressed="true"] { background: var(--icon-btn-group-on-bg, var(--accent)); color: var(--icon-btn-group-on-ink, var(--on-accent)); } /* `select`: a pressed button STAYS filled (Lh 2026-09-29) */
112
+ .icon-btn-group > .icon-btn:active:not(:disabled) { scale: 1; opacity: var(--press-dim); } /* a part of one frame DIMS, it never shrinks inside it */
113
+
96
114
  /* fab: the square floating action, bottom-right of a stage or screen */
97
115
  .fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3) + var(--safe-bottom)); box-shadow: var(--shadow-float); z-index: var(--z-fab); }
98
116
 
@@ -0,0 +1,19 @@
1
+ import { type ComponentProps } from 'react';
2
+ type Picked = string | null | string[];
3
+ export interface IconBtnGroupProps extends Omit<ComponentProps<'div'>, 'onChange' | 'defaultValue'> {
4
+ /** the group's accessible name (what these commands act on) */
5
+ label?: string;
6
+ /** buttons that STAY ON when pressed (Lh 2026-09-29): `one` — one on at a time, pressing another moves it (value: the on
7
+ * button's `value`, or null) · `many` — each on or off by itself (value: the on ones' values). None = plain commands. */
8
+ select?: 'one' | 'many';
9
+ /** which are on — each child IconBtn names itself with its own `value` */
10
+ value?: Picked;
11
+ defaultValue?: Picked;
12
+ /** asks the app: `one` → the pressed value · `many` → the new list */
13
+ onChange?: (value: Picked) => void;
14
+ }
15
+ /** IconBtnGroup — a panel of up to FOUR icon-only buttons in ONE frame, sharing its width, a rule between them in the frame's
16
+ * colour (Lh 2026-09-29). Each is an `IconBtn` (its `label` is its spoken name). Plain: each does its own thing on its onClick.
17
+ * With `select`, a pressed button stays filled (aria-pressed): `one` or `many`. At home in a card's or a sheet's foot. */
18
+ export declare function IconBtnGroup({ label, select, value, defaultValue, onChange, className, children, ...rest }: IconBtnGroupProps): import("react").JSX.Element;
19
+ export {};
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Children, cloneElement, isValidElement } from 'react';
3
+ import { cx } from '../cx.js';
4
+ import { useControlled, warnOnce } from '../state.js';
5
+ /** IconBtnGroup — a panel of up to FOUR icon-only buttons in ONE frame, sharing its width, a rule between them in the frame's
6
+ * colour (Lh 2026-09-29). Each is an `IconBtn` (its `label` is its spoken name). Plain: each does its own thing on its onClick.
7
+ * With `select`, a pressed button stays filled (aria-pressed): `one` or `many`. At home in a card's or a sheet's foot. */
8
+ export function IconBtnGroup({ label, select, value, defaultValue, onChange, className, children, ...rest }) {
9
+ if (Children.count(children) > 4)
10
+ warnOnce('icon-btn-group', 'cardds IconBtnGroup: more than 4 buttons — keep 4, put the rest in a Menu behind one of them');
11
+ const [shown, request] = useControlled(value, defaultValue ?? (select === 'many' ? [] : null), onChange);
12
+ const on = (v) => (Array.isArray(shown) ? shown.includes(v) : shown === v);
13
+ const items = !select ? children : Children.map(children, (c) => {
14
+ if (!isValidElement(c))
15
+ return c;
16
+ const el = c;
17
+ const v = el.props.value;
18
+ if (v == null)
19
+ return c;
20
+ return cloneElement(el, {
21
+ 'aria-pressed': on(v),
22
+ onClick: (e) => {
23
+ el.props.onClick?.(e);
24
+ if (select === 'one')
25
+ request(v);
26
+ else {
27
+ const list = Array.isArray(shown) ? shown : [];
28
+ request(on(v) ? list.filter((x) => x !== v) : [...list, v]);
29
+ }
30
+ },
31
+ });
32
+ });
33
+ return _jsx("div", { role: "group", "aria-label": label, className: cx('icon-btn-group', className), ...rest, children: items });
34
+ }
package/dist/cardds.css CHANGED
@@ -2117,6 +2117,24 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2117
2117
  }
2118
2118
  .icon-btn--invert { background: var(--icon-btn-invert-bg, var(--accent)); color: var(--icon-btn-invert-ink, var(--on-accent)); border-color: transparent; }
2119
2119
  .icon-btn--sm { width: var(--icon-btn-sm-size, var(--tap-sm)); height: var(--icon-btn-sm-size, var(--tap-sm)); font-size: var(--fs-title); }
2120
+ /* ---- icon button group: up to four icon-only commands in ONE frame (Lh 2026-09-29) ----
2121
+ <div class="icon-btn-group" role="group"><button class="icon-btn" aria-label="…">…</button>…</div>
2122
+ One pill as tall as a button, the buttons share its width, a hairline between them; in a foot it takes the row. */
2123
+ .icon-btn-group {
2124
+ display: flex; min-width: 0;
2125
+ min-height: var(--icon-btn-group-h, var(--btn-h));
2126
+ border: var(--border-w) solid var(--icon-btn-group-border, var(--outline));
2127
+ border-radius: var(--icon-btn-group-radius, var(--r-chip));
2128
+ background: var(--icon-btn-group-bg, var(--action-bg));
2129
+ color: var(--icon-btn-group-ink, var(--action-ink));
2130
+ overflow: clip;
2131
+ }
2132
+ .icon-btn-group > .icon-btn { flex: 1 1 0; min-width: 0; width: auto; height: auto; border: 0; border-radius: 0; background: transparent; color: inherit; box-shadow: none; }
2133
+ .icon-btn-group > .icon-btn + .icon-btn { border-inline-start: var(--border-w) solid var(--icon-btn-group-rule, var(--icon-btn-group-border, var(--outline))); }
2134
+ .card__foot > .icon-btn-group { flex: 1 1 0; }
2135
+ .icon-btn-group > .icon-btn[aria-pressed="true"] { background: var(--icon-btn-group-on-bg, var(--accent)); color: var(--icon-btn-group-on-ink, var(--on-accent)); } /* `select`: a pressed button STAYS filled (Lh 2026-09-29) */
2136
+ .icon-btn-group > .icon-btn:active:not(:disabled) { scale: 1; opacity: var(--press-dim); } /* a part of one frame DIMS, it never shrinks inside it */
2137
+
2120
2138
  /* fab: the square floating action, bottom-right of a stage or screen */
2121
2139
  .fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3) + var(--safe-bottom)); box-shadow: var(--shadow-float); z-index: var(--z-fab); }
2122
2140
 
package/dist/index.d.ts CHANGED
@@ -33,6 +33,7 @@ export * from './sheets/SheetSteps.js';
33
33
  export * from './sheets/Drawer.js';
34
34
  export * from './actions/Btn.js';
35
35
  export * from './actions/IconBtn.js';
36
+ export * from './actions/IconBtnGroup.js';
36
37
  export * from './actions/Chip.js';
37
38
  export * from './actions/Dot.js';
38
39
  export * from './actions/Dropdown.js';
package/dist/index.js CHANGED
@@ -37,6 +37,7 @@ export * from './sheets/SheetSteps.js';
37
37
  export * from './sheets/Drawer.js';
38
38
  export * from './actions/Btn.js';
39
39
  export * from './actions/IconBtn.js';
40
+ export * from './actions/IconBtnGroup.js';
40
41
  export * from './actions/Chip.js';
41
42
  export * from './actions/Dot.js';
42
43
  export * from './actions/Dropdown.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.22",
3
+ "version": "0.7.23",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",