@edux-design/chips 0.2.0 → 0.3.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,11 @@
1
1
  # @edux-design/chips
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add ActionChip as a distinct native button with native ref and activation, count and adornment content, and visual selected state. Preserve the existing div-based Chip and removal contract; do not introduce nested interactive controls.
8
+
3
9
  ## 0.2.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @edux-design/chips
2
2
 
3
- Token-driven filter chips that compose existing primitives (like `TagNumber`) without extra props. Each chip renders a pill with a count, label, and optional dismiss control that mirrors the spec in `chips.png`.
3
+ Token-driven compact chips. `Chip` renders an informational or removable div-root pill; `ActionChip` renders a native button for filter actions and overlay triggers. Both support label/count composition with existing primitives.
4
4
 
5
5
  ---
6
6
 
@@ -37,7 +37,9 @@ import { TagNumber } from "@edux-design/tags";
37
37
  export function Filters({ onRemove }) {
38
38
  return (
39
39
  <Chip label="Label" onRemove={onRemove}>
40
- <TagNumber variant="brand" tone="solid">999</TagNumber>
40
+ <TagNumber variant="brand" tone="solid">
41
+ 999
42
+ </TagNumber>
41
43
  </Chip>
42
44
  );
43
45
  }
@@ -45,13 +47,13 @@ export function Filters({ onRemove }) {
45
47
 
46
48
  ### Key Props
47
49
 
48
- | Prop | Description |
49
- | ---- | ----------- |
50
- | `label` | Primary text rendered inside the chip (truncated when needed). |
51
- | `size` | `"default"` (40px tall) or `"small"` (32px tall). |
52
- | `onRemove` | When provided, renders a close icon button that calls the handler. |
53
- | `removeLabel` | Accessible label for the close button. Defaults to `"Remove chip"`. |
54
- | `disabled` | Mutes colors, disables the close button, and removes pointer interactions. |
50
+ | Prop | Description |
51
+ | ------------- | -------------------------------------------------------------------------- |
52
+ | `label` | Primary text rendered inside the chip (truncated when needed). |
53
+ | `size` | `"default"` (40px tall) or `"small"` (32px tall). |
54
+ | `onRemove` | When provided, renders a close icon button that calls the handler. |
55
+ | `removeLabel` | Accessible label for the close button. Defaults to `"Remove chip"`. |
56
+ | `disabled` | Mutes colors, disables the close button, and removes pointer interactions. |
55
57
 
56
58
  `children` are rendered before the label, so you can compose `TagNumber` (or any other primitive) directly inside the chip without a dedicated prop.
57
59
 
@@ -74,3 +76,33 @@ Stories live under `src/demos/Chip.stories.jsx` with size + state examples pulle
74
76
  - Focus, hover, pressed, dragged, and inactive visuals are all handled via Tailwind utility classes wired to the shared design tokens.
75
77
  - The chip container uses `focus-within` so the focus ring appears when the remove button is tabbed to.
76
78
  - Feel free to slot other components ahead of the label; the layout simply spaces whatever you supply, so TagNumber composition stays entirely declarative.
79
+
80
+ ## Native action/filter trigger
81
+
82
+ ```jsx
83
+ import { ActionChip } from "@edux-design/chips";
84
+ import { TagNumber } from "@edux-design/tags";
85
+ import { Chevron } from "@edux-design/icons";
86
+
87
+ <ActionChip
88
+ label="Stages"
89
+ selected={selectedStages.length > 0}
90
+ count={
91
+ selectedStages.length > 0 ? (
92
+ <TagNumber>{selectedStages.length}</TagNumber>
93
+ ) : null
94
+ }
95
+ endAdornment={<Chevron aria-hidden="true" />}
96
+ onClick={openStageFilter}
97
+ />;
98
+ ```
99
+
100
+ ActionChip forwards native button props and an `HTMLButtonElement` ref, defaults
101
+ to `type="button"`, and composes with Menu/Popover triggers. `selected` is visual
102
+ filter state; popup expansion comes from `aria-expanded` or `data-state="open"`.
103
+ Set native `aria-pressed` only for actual toggle actions. Labels and slots must
104
+ contain noninteractive content; removal remains on the div-root Chip.
105
+
106
+ Its `default` and `small` minimum heights retain existing 40px/32px chip geometry.
107
+ See [ActionChip contract and examples](./docs/action-chip.md) and the
108
+ [read-only host inventory](./docs/action-chip-inventory.md).
package/dist/index.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ import React from 'react';
2
+
1
3
  /**
2
4
  * Token-driven chip that composes existing primitives (i.e. TagNumber) inside.
3
5
  *
@@ -24,4 +26,23 @@ type ChipProps = React.HTMLAttributes<HTMLDivElement> & {
24
26
  removeButtonProps?: Omit<React.ComponentPropsWithRef<"button">, "type" | "disabled" | "aria-label" | "aria-labelledby" | "children">;
25
27
  };
26
28
 
27
- export { Chip };
29
+ interface ActionChipProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
30
+ /** Visible label; children are used when label is undefined. Supply noninteractive content. */
31
+ label?: React.ReactNode;
32
+ /** Chip-specific geometry, matching Chip's existing 40px/32px minimum heights. */
33
+ size?: "default" | "small";
34
+ /** Visual filter state only. Does not imply aria-pressed or popup expansion. */
35
+ selected?: boolean;
36
+ /** Noninteractive count content before the label. The host owns its visibility and value. */
37
+ count?: React.ReactNode;
38
+ startAdornment?: React.ReactNode;
39
+ endAdornment?: React.ReactNode;
40
+ /** Use the separate div-root Chip for removal. */
41
+ onRemove?: never;
42
+ removeLabel?: never;
43
+ removeButtonProps?: never;
44
+ }
45
+ /** Native action/overlay trigger. The host owns filter state and popup behaviour. */
46
+ declare const ActionChip: React.ForwardRefExoticComponent<ActionChipProps & React.RefAttributes<HTMLButtonElement>>;
47
+
48
+ export { ActionChip, Chip };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import React from 'react';
2
+
1
3
  /**
2
4
  * Token-driven chip that composes existing primitives (i.e. TagNumber) inside.
3
5
  *
@@ -24,4 +26,23 @@ type ChipProps = React.HTMLAttributes<HTMLDivElement> & {
24
26
  removeButtonProps?: Omit<React.ComponentPropsWithRef<"button">, "type" | "disabled" | "aria-label" | "aria-labelledby" | "children">;
25
27
  };
26
28
 
27
- export { Chip };
29
+ interface ActionChipProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
30
+ /** Visible label; children are used when label is undefined. Supply noninteractive content. */
31
+ label?: React.ReactNode;
32
+ /** Chip-specific geometry, matching Chip's existing 40px/32px minimum heights. */
33
+ size?: "default" | "small";
34
+ /** Visual filter state only. Does not imply aria-pressed or popup expansion. */
35
+ selected?: boolean;
36
+ /** Noninteractive count content before the label. The host owns its visibility and value. */
37
+ count?: React.ReactNode;
38
+ startAdornment?: React.ReactNode;
39
+ endAdornment?: React.ReactNode;
40
+ /** Use the separate div-root Chip for removal. */
41
+ onRemove?: never;
42
+ removeLabel?: never;
43
+ removeButtonProps?: never;
44
+ }
45
+ /** Native action/overlay trigger. The host owns filter state and popup behaviour. */
46
+ declare const ActionChip: React.ForwardRefExoticComponent<ActionChipProps & React.RefAttributes<HTMLButtonElement>>;
47
+
48
+ export { ActionChip, Chip };
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  // src/index.js
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
+ ActionChip: () => ActionChip,
32
33
  Chip: () => Chip
33
34
  });
34
35
  module.exports = __toCommonJS(index_exports);
@@ -111,7 +112,7 @@ var Chip = (0, import_react.forwardRef)(function Chip2({
111
112
  onClick: (event) => {
112
113
  event.stopPropagation();
113
114
  if (disabled) return;
114
- onRemoveClick == null ? void 0 : onRemoveClick(event);
115
+ onRemoveClick?.(event);
115
116
  if (!event.defaultPrevented) onRemove(event);
116
117
  },
117
118
  className: (0, import_utils.cx)(
@@ -131,7 +132,69 @@ var Chip = (0, import_react.forwardRef)(function Chip2({
131
132
  }
132
133
  );
133
134
  });
135
+
136
+ // src/elements/ActionChip.tsx
137
+ var import_react2 = require("react");
138
+ var import_utils2 = require("@edux-design/utils");
139
+ var import_jsx_runtime2 = require("react/jsx-runtime");
140
+ var hasSlot = (node) => node != null && typeof node !== "boolean";
141
+ var sizes = {
142
+ default: "text-sm min-h-[40px] px-8 py-6 gap-8",
143
+ small: "text-xs min-h-32 px-8 py-4 gap-6"
144
+ };
145
+ var ActionChip = (0, import_react2.forwardRef)(
146
+ function ActionChip2({
147
+ label,
148
+ children,
149
+ size = "default",
150
+ selected = false,
151
+ count,
152
+ startAdornment,
153
+ endAdornment,
154
+ disabled = false,
155
+ type = "button",
156
+ className,
157
+ // Do not leak unsupported removal props onto the native button in JS hosts.
158
+ onRemove: _onRemove,
159
+ removeLabel: _removeLabel,
160
+ removeButtonProps: _removeButtonProps,
161
+ ...props
162
+ }, ref) {
163
+ const expanded = props["aria-expanded"] === true || props["aria-expanded"] === "true";
164
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
165
+ "button",
166
+ {
167
+ ...props,
168
+ ref,
169
+ type,
170
+ disabled,
171
+ "data-selected": selected ? "true" : void 0,
172
+ className: (0, import_utils2.cx)(
173
+ "inline-flex w-fit max-w-full min-w-0 items-center rounded-full border border-border-minimal bg-bg-base text-fg-base shadow-sm",
174
+ "transition-all duration-200 ease-out cursor-pointer",
175
+ "hover:border-border-primary-base hover:bg-bg-primary-subtle",
176
+ "active:border-border-primary-emphasis active:bg-bg-primary-subtle-90",
177
+ "focus-visible:shadow-focus focus-visible:border-border-emphasis focus-visible:outline-none",
178
+ "data-[state=open]:border-border-primary-base data-[state=open]:bg-bg-primary-subtle",
179
+ sizes[size],
180
+ (selected || expanded) && "border-border-primary-base bg-bg-primary-subtle",
181
+ disabled && "bg-bg-inactive text-fg-inactive border-border-subtle cursor-not-allowed pointer-events-none",
182
+ className
183
+ ),
184
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "flex min-w-0 flex-1 items-center gap-6", children: [
185
+ hasSlot(startAdornment) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "inline-flex shrink-0 items-center", children: startAdornment }),
186
+ hasSlot(count) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "inline-flex shrink-0 items-center", children: count }),
187
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "flex min-w-0 items-center gap-8", children: [
188
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "min-w-0 truncate text-left", children: label === void 0 ? children : label }),
189
+ hasSlot(endAdornment) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "inline-flex shrink-0 items-center", children: endAdornment })
190
+ ] })
191
+ ] })
192
+ }
193
+ );
194
+ }
195
+ );
134
196
  // Annotate the CommonJS export names for ESM import in node:
135
197
  0 && (module.exports = {
198
+ ActionChip,
136
199
  Chip
137
200
  });
package/dist/index.mjs CHANGED
@@ -76,7 +76,7 @@ var Chip = forwardRef(function Chip2({
76
76
  onClick: (event) => {
77
77
  event.stopPropagation();
78
78
  if (disabled) return;
79
- onRemoveClick == null ? void 0 : onRemoveClick(event);
79
+ onRemoveClick?.(event);
80
80
  if (!event.defaultPrevented) onRemove(event);
81
81
  },
82
82
  className: cx(
@@ -96,6 +96,68 @@ var Chip = forwardRef(function Chip2({
96
96
  }
97
97
  );
98
98
  });
99
+
100
+ // src/elements/ActionChip.tsx
101
+ import { forwardRef as forwardRef2 } from "react";
102
+ import { cx as cx2 } from "@edux-design/utils";
103
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
104
+ var hasSlot = (node) => node != null && typeof node !== "boolean";
105
+ var sizes = {
106
+ default: "text-sm min-h-[40px] px-8 py-6 gap-8",
107
+ small: "text-xs min-h-32 px-8 py-4 gap-6"
108
+ };
109
+ var ActionChip = forwardRef2(
110
+ function ActionChip2({
111
+ label,
112
+ children,
113
+ size = "default",
114
+ selected = false,
115
+ count,
116
+ startAdornment,
117
+ endAdornment,
118
+ disabled = false,
119
+ type = "button",
120
+ className,
121
+ // Do not leak unsupported removal props onto the native button in JS hosts.
122
+ onRemove: _onRemove,
123
+ removeLabel: _removeLabel,
124
+ removeButtonProps: _removeButtonProps,
125
+ ...props
126
+ }, ref) {
127
+ const expanded = props["aria-expanded"] === true || props["aria-expanded"] === "true";
128
+ return /* @__PURE__ */ jsx2(
129
+ "button",
130
+ {
131
+ ...props,
132
+ ref,
133
+ type,
134
+ disabled,
135
+ "data-selected": selected ? "true" : void 0,
136
+ className: cx2(
137
+ "inline-flex w-fit max-w-full min-w-0 items-center rounded-full border border-border-minimal bg-bg-base text-fg-base shadow-sm",
138
+ "transition-all duration-200 ease-out cursor-pointer",
139
+ "hover:border-border-primary-base hover:bg-bg-primary-subtle",
140
+ "active:border-border-primary-emphasis active:bg-bg-primary-subtle-90",
141
+ "focus-visible:shadow-focus focus-visible:border-border-emphasis focus-visible:outline-none",
142
+ "data-[state=open]:border-border-primary-base data-[state=open]:bg-bg-primary-subtle",
143
+ sizes[size],
144
+ (selected || expanded) && "border-border-primary-base bg-bg-primary-subtle",
145
+ disabled && "bg-bg-inactive text-fg-inactive border-border-subtle cursor-not-allowed pointer-events-none",
146
+ className
147
+ ),
148
+ children: /* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 flex-1 items-center gap-6", children: [
149
+ hasSlot(startAdornment) && /* @__PURE__ */ jsx2("span", { className: "inline-flex shrink-0 items-center", children: startAdornment }),
150
+ hasSlot(count) && /* @__PURE__ */ jsx2("span", { className: "inline-flex shrink-0 items-center", children: count }),
151
+ /* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 items-center gap-8", children: [
152
+ /* @__PURE__ */ jsx2("span", { className: "min-w-0 truncate text-left", children: label === void 0 ? children : label }),
153
+ hasSlot(endAdornment) && /* @__PURE__ */ jsx2("span", { className: "inline-flex shrink-0 items-center", children: endAdornment })
154
+ ] })
155
+ ] })
156
+ }
157
+ );
158
+ }
159
+ );
99
160
  export {
161
+ ActionChip,
100
162
  Chip
101
163
  };
@@ -0,0 +1,21 @@
1
+ # DS-20 host inventory
2
+
3
+ Read-only inventory captured 29 October 2026. No Payload source is changed by
4
+ this package work.
5
+
6
+ | Host | Current composition | Required behaviour |
7
+ | ---------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
8
+ | `ManuscriptFilterChip.jsx` | Div-root Chip with role, tab index, manual Enter/Space activation, TagNumber and Chevron | Native button ref/events, selected styling independent of popup expansion, count and adornment slots |
9
+ | `ManuScriptsHome.jsx` Stages | Controlled Menu, selected status count and filter-active visual | Native Menu trigger; count/filter state stays with host |
10
+ | `ManuScriptsHome.jsx` sort | Controlled Menu with dynamic sort label | Expansion visual without toggle-button semantics |
11
+ | `ManuscriptSeriesFilter.jsx` | Controlled Menu with selected series count and selected filter visual | Forwarded trigger ref/ARIA/events; selection remains controlled by host |
12
+ | `MetaDataHome.jsx` Status | Menu trigger wraps Chip in a div, count is selected count or total | Candidate native trigger adoption, preserving the host's count policy |
13
+ | `MetaDataHome.jsx` sort | Menu trigger wraps a dynamic-label Chip in a div | Candidate native trigger adoption |
14
+
15
+ Sources: audit round 2 README DS-20, review-1 section 3, review-3 R3-03;
16
+ `payload-client/src/features/Manuscripts/components/ManuscriptFilterChip.jsx`,
17
+ `ManuscriptSeriesFilter.jsx`, `elements/ManuScriptsHome.jsx`, and
18
+ `features/Metadata/pages/MetaDataHome.jsx` in the read-only Payload checkout.
19
+
20
+ The existing Chip root remains a div. Its removal button, callback composition,
21
+ ref types and disabled semantics are a separate contract from an action trigger.
@@ -0,0 +1,85 @@
1
+ # ActionChip
2
+
3
+ `ActionChip` is a native button for compact filter actions and Menu/Popover
4
+ triggers. The existing `Chip` remains a div with an optional removal button.
5
+
6
+ ```tsx
7
+ import { ActionChip } from "@edux-design/chips";
8
+ import { TagNumber } from "@edux-design/tags";
9
+ import { Chevron } from "@edux-design/icons";
10
+ import { Menu, MenuList, MenuItem } from "@edux-design/menus";
11
+
12
+ <Menu
13
+ button={
14
+ <ActionChip
15
+ label="Stages"
16
+ selected={selectedStages.length > 0}
17
+ count={
18
+ selectedStages.length > 0 ? (
19
+ <TagNumber variant="brand" tone="solid">
20
+ {selectedStages.length}
21
+ </TagNumber>
22
+ ) : null
23
+ }
24
+ endAdornment={<Chevron aria-hidden="true" />}
25
+ />
26
+ }
27
+ >
28
+ <MenuList>
29
+ <MenuItem onClick={chooseDraft}>Draft</MenuItem>
30
+ </MenuList>
31
+ </Menu>;
32
+ ```
33
+
34
+ The Menu clones its trigger and supplies popup ARIA, handlers and a composed
35
+ button ref. Popover uses `<PopoverTrigger asChild><ActionChip ... /></PopoverTrigger>`.
36
+ Both compositions retain native Enter/Space/click activation, handler cancellation
37
+ with `event.preventDefault()`, disabled behaviour, Escape dismissal and focus
38
+ restoration. No role, tab index or synthetic keyboard-to-click adapter is needed.
39
+
40
+ | Prop | Contract |
41
+ | ------------------------------------- | -------------------------------------------------------------------------------------------- |
42
+ | `ref` | `HTMLButtonElement`; Chip still accepts `HTMLDivElement` |
43
+ | `label` | Noninteractive React content; native `children` is the fallback when label is undefined |
44
+ | `count` | Noninteractive React content before label; host owns value, visibility and accessible naming |
45
+ | `startAdornment`, `endAdornment` | Noninteractive content, such as an icon or chevron |
46
+ | `selected` | Filter-applied visual and `data-selected="true"` only; never sets `aria-pressed` |
47
+ | `aria-expanded` / `data-state="open"` | Independently highlights an open popup |
48
+ | `aria-pressed` | Optional native attribute only when the action is an actual toggle |
49
+ | `disabled` | Native disabled button semantics and muted token colours |
50
+ | `type` | Native button type, default `"button"`; explicit submit/reset remain supported |
51
+ | `size` | `"default"` (40px minimum) or `"small"` (32px minimum), matching existing Chip geometry |
52
+ | Other native button props | Forwarded, including ARIA, form attributes and events |
53
+
54
+ `onRemove`, `removeLabel` and `removeButtonProps` are prohibited by the public
55
+ ActionChip type. Use `Chip` for removal. Do not place buttons, links, form fields,
56
+ removable Chips or other interactive descendants in any label/count/adornment
57
+ slot. Decorative icons should be hidden from assistive technology. Supply an
58
+ accessible name when there is no visible text; for a terse count, an explicit
59
+ `aria-label` can state the filter name and selected count together.
60
+
61
+ The chip minimum heights intentionally remain component-owned 40px and the
62
+ existing 32px spacing token; mapping to the control-size family would alter
63
+ established compact filter geometry. Existing spacing, colours, shadows, radii
64
+ and focus tokens are reused without changing the token package. The label
65
+ truncates in a constrained parent while count and adornments remain visible.
66
+
67
+ The host owns filter choices, loading/search, selection changes, popup state and
68
+ counts. This component does not manage pressed/open state or emit separate state
69
+ callbacks. The host should replace the old ManuscriptFilterChip's div keyboard
70
+ adapter with ActionChip when it adopts a released package; see
71
+ [action-chip-inventory.md](./action-chip-inventory.md) for the inspected hosts.
72
+
73
+ Verification: `src/elements/ActionChip.test.tsx`, strict public fixtures in
74
+ `test/action-chip-api.tsx`, CJS/SSR in `test/action-chip-public-runtime.cjs`,
75
+ four strict-a11y stories, and `node packages/chips/test/action-chip.browser.mjs`.
76
+ The browser suite compares both sizes against legacy Chip geometry in blue/teal,
77
+ light/dark; checks native once-only pointer/Enter/Space activation, real overlay
78
+ Escape/focus restoration, disabled state, and narrow long-label truncation.
79
+
80
+ The Menu composition story counts only true `onOpenChange` requests. Menu owns
81
+ its trigger opening policy, and may handle keyboard events before a native click
82
+ is emitted. Child `onClick` is not an overlay-open notification contract. The
83
+ standalone ActionChip tests separately verify one native click per pointer, Enter
84
+ or Space activation. Keyboard story sequences await Escape focus restoration
85
+ before sending the next activation key.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edux-design/chips",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css"