@handamade/psi-react 0.8.1 → 0.9.1

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.
@@ -2,6 +2,10 @@ import type { ButtonHTMLAttributes, Ref } from "react";
2
2
  type Variant = "accent" | "neutral" | "ghost" | "danger";
3
3
  type Size = 24 | 32 | 40 | 48;
4
4
  export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
5
+ /** Accessible name for the icon-only control. Required: an IconButton has
6
+ * no text to name it, so omitting this ships an unnameable button (D60).
7
+ * If you also pass `aria-labelledby`, that wins per the accname algorithm. */
8
+ "aria-label": string;
5
9
  /** Visual variant. @default "neutral" */
6
10
  variant?: Variant;
7
11
  /** Square size in px (24 | 32 | 40 | 48). @default 32 */
@@ -1 +1 @@
1
- {"version":3,"file":"IconButton.d.ts","sourceRoot":"","sources":["../../src/IconButton/IconButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,oBAAoB,EAEpB,GAAG,EACJ,MAAM,OAAO,CAAC;AAGf,KAAK,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AACzD,KAAK,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAE9B,MAAM,WAAW,eACf,SAAQ,oBAAoB,CAAC,iBAAiB,CAAC;IAC/C,yCAAyC;IACzC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yDAAyD;IACzD,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ;kDAC8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,GAAG,CAAC,EAAE,GAAG,CAAC,iBAAiB,GAAG,iBAAiB,CAAC,CAAC;CAClD;AAgBD;gDACgD;AAChD,wBAAgB,UAAU,CAAC,EACzB,OAAmB,EACnB,IAAS,EACT,IAAI,EACJ,MAAM,EACN,GAAG,EACH,QAAQ,EACR,SAAS,EACT,OAAO,EACP,GAAG,EACH,GAAG,IAAI,EACR,EAAE,eAAe,+BA0CjB"}
1
+ {"version":3,"file":"IconButton.d.ts","sourceRoot":"","sources":["../../src/IconButton/IconButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,oBAAoB,EAEpB,GAAG,EACJ,MAAM,OAAO,CAAC;AAGf,KAAK,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AACzD,KAAK,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAE9B,MAAM,WAAW,eACf,SAAQ,oBAAoB,CAAC,iBAAiB,CAAC;IAC/C;;kFAE8E;IAC9E,YAAY,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yDAAyD;IACzD,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ;kDAC8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,GAAG,CAAC,EAAE,GAAG,CAAC,iBAAiB,GAAG,iBAAiB,CAAC,CAAC;CAClD;AAgBD;gDACgD;AAChD,wBAAgB,UAAU,CAAC,EACzB,OAAmB,EACnB,IAAS,EACT,IAAI,EACJ,MAAM,EACN,GAAG,EACH,QAAQ,EACR,SAAS,EACT,OAAO,EACP,GAAG,EACH,GAAG,IAAI,EACR,EAAE,eAAe,+BA0CjB"}
@@ -1,6 +1,12 @@
1
1
  import type { InputHTMLAttributes, Ref } from "react";
2
2
  type Size = 24 | 32 | 40 | 48;
3
- export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
3
+ /** Input types Psi's Input supports. Deliberately narrower than the native
4
+ * `HTMLInputTypeAttribute`: checkbox/radio/file/submit are separate controls
5
+ * (Checkbox, Switch, Button), not variants of a text field (D60). */
6
+ export type InputType = "text" | "search" | "email" | "tel" | "url" | "password" | "number" | "date";
7
+ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
8
+ /** Input type. @default "text" */
9
+ type?: InputType;
4
10
  /** Height in px (24 | 32 | 40 | 48). @default 32 */
5
11
  size?: Size;
6
12
  /** Show error styling. Inside a Field, the Field's error also lights this. @default false */
@@ -11,6 +17,6 @@ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
11
17
  /** Single-line text input with pixel-true heights (24–48) and an error state.
12
18
  * Inside a Field, id/aria-describedby/aria-invalid/required are wired
13
19
  * automatically (D49). */
14
- export declare function Input({ size, error, className, ref, ...rest }: InputProps): import("react").JSX.Element;
20
+ export declare function Input({ size, error, type, className, ref, ...rest }: InputProps): import("react").JSX.Element;
15
21
  export {};
16
22
  //# sourceMappingURL=Input.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Input.d.ts","sourceRoot":"","sources":["../../src/Input/Input.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAItD,KAAK,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAE9B,MAAM,WAAW,UAAW,SAAQ,mBAAmB,CAAC,gBAAgB,CAAC;IACvE,oDAAoD;IACpD,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,6FAA6F;IAC7F,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,yDAAyD;IACzD,GAAG,CAAC,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAC;CAC7B;AASD;;0BAE0B;AAC1B,wBAAgB,KAAK,CAAC,EAAE,IAAS,EAAE,KAAa,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,EAAE,UAAU,+BAqBtF"}
1
+ {"version":3,"file":"Input.d.ts","sourceRoot":"","sources":["../../src/Input/Input.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAItD,KAAK,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAE9B;;qEAEqE;AACrE,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,QAAQ,GACR,OAAO,GACP,KAAK,GACL,KAAK,GACL,UAAU,GACV,QAAQ,GACR,MAAM,CAAC;AAEX,MAAM,WAAW,UAAW,SAAQ,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACrF,kCAAkC;IAClC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,oDAAoD;IACpD,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,6FAA6F;IAC7F,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,yDAAyD;IACzD,GAAG,CAAC,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAC;CAC7B;AASD;;0BAE0B;AAC1B,wBAAgB,KAAK,CAAC,EAAE,IAAS,EAAE,KAAa,EAAE,IAAa,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,EAAE,UAAU,+BAsBrG"}
package/dist/index.js CHANGED
@@ -33,42 +33,42 @@ const O = "_button_c94w9_3", X = "_disabled_c94w9_30", J = "_size24_c94w9_44", Q
33
33
  function un({
34
34
  variant: e = "neutral",
35
35
  size: t = 32,
36
- href: i,
36
+ href: s,
37
37
  target: a,
38
38
  rel: c,
39
- disabled: o,
40
- className: r,
39
+ disabled: r,
40
+ className: o,
41
41
  onClick: l,
42
42
  ref: d,
43
- ...s
43
+ ...i
44
44
  }) {
45
45
  const u = [
46
46
  f.button,
47
47
  ue[e],
48
48
  de[t],
49
- i !== void 0 && o ? f.disabled : void 0,
50
- r
49
+ s !== void 0 && r ? f.disabled : void 0,
50
+ o
51
51
  ].filter(Boolean).join(" ");
52
- return i !== void 0 ? /* @__PURE__ */ n(
52
+ return s !== void 0 ? /* @__PURE__ */ n(
53
53
  "a",
54
54
  {
55
55
  ref: d,
56
56
  className: u,
57
- href: o ? void 0 : i,
57
+ href: r ? void 0 : s,
58
58
  target: a,
59
59
  rel: c,
60
- "aria-disabled": o || void 0,
61
- onClick: o ? void 0 : l,
62
- ...s
60
+ "aria-disabled": r || void 0,
61
+ onClick: r ? void 0 : l,
62
+ ...i
63
63
  }
64
64
  ) : /* @__PURE__ */ n(
65
65
  "button",
66
66
  {
67
67
  ref: d,
68
68
  className: u,
69
- disabled: o,
69
+ disabled: r,
70
70
  onClick: l,
71
- ...s
71
+ ...i
72
72
  }
73
73
  );
74
74
  }
@@ -97,42 +97,42 @@ const _e = "_iconButton_129ca_3", he = "_disabled_129ca_29", pe = "_size24_129ca
97
97
  function Ce({
98
98
  variant: e = "neutral",
99
99
  size: t = 32,
100
- href: i,
100
+ href: s,
101
101
  target: a,
102
102
  rel: c,
103
- disabled: o,
104
- className: r,
103
+ disabled: r,
104
+ className: o,
105
105
  onClick: l,
106
106
  ref: d,
107
- ...s
107
+ ...i
108
108
  }) {
109
109
  const u = [
110
110
  w.iconButton,
111
111
  ke[e],
112
112
  xe[t],
113
- i !== void 0 && o ? w.disabled : void 0,
114
- r
113
+ s !== void 0 && r ? w.disabled : void 0,
114
+ o
115
115
  ].filter(Boolean).join(" ");
116
- return i !== void 0 ? /* @__PURE__ */ n(
116
+ return s !== void 0 ? /* @__PURE__ */ n(
117
117
  "a",
118
118
  {
119
119
  ref: d,
120
120
  className: u,
121
- href: o ? void 0 : i,
121
+ href: r ? void 0 : s,
122
122
  target: a,
123
123
  rel: c,
124
- "aria-disabled": o || void 0,
125
- onClick: o ? void 0 : l,
126
- ...s
124
+ "aria-disabled": r || void 0,
125
+ onClick: r ? void 0 : l,
126
+ ...i
127
127
  }
128
128
  ) : /* @__PURE__ */ n(
129
129
  "button",
130
130
  {
131
131
  ref: d,
132
132
  className: u,
133
- disabled: o,
133
+ disabled: r,
134
134
  onClick: l,
135
- ...s
135
+ ...i
136
136
  }
137
137
  );
138
138
  }
@@ -147,19 +147,19 @@ const ze = "_card_1btb3_1", Le = "_stacked_1btb3_8", $e = "_featured_1btb3_10",
147
147
  function dn({
148
148
  variant: e = "stacked",
149
149
  media: t,
150
- hoverLift: i = !1,
150
+ hoverLift: s = !1,
151
151
  className: a,
152
152
  children: c,
153
- ref: o,
154
- ...r
153
+ ref: r,
154
+ ...o
155
155
  }) {
156
156
  const l = [
157
157
  $.card,
158
158
  e === "featured" ? $.featured : $.stacked,
159
- i ? $.hoverLift : void 0,
159
+ s ? $.hoverLift : void 0,
160
160
  a
161
161
  ].filter(Boolean).join(" ");
162
- return /* @__PURE__ */ h("div", { ref: o, className: l, ...r, children: [
162
+ return /* @__PURE__ */ h("div", { ref: r, className: l, ...o, children: [
163
163
  t != null && /* @__PURE__ */ n("div", { className: $.media, children: t }),
164
164
  /* @__PURE__ */ n("div", { className: $.body, children: c })
165
165
  ] });
@@ -168,9 +168,9 @@ const Ne = "_panel_jn8w4_1", Ie = "_padding16_jn8w4_8", q = {
168
168
  panel: Ne,
169
169
  padding16: Ie
170
170
  };
171
- function _n({ padding: e = 24, className: t, children: i, ref: a, ...c }) {
172
- const o = [q.panel, e === 16 ? q.padding16 : void 0, t].filter(Boolean).join(" ");
173
- return /* @__PURE__ */ n("div", { ref: a, className: o, ...c, children: i });
171
+ function _n({ padding: e = 24, className: t, children: s, ref: a, ...c }) {
172
+ const r = [q.panel, e === 16 ? q.padding16 : void 0, t].filter(Boolean).join(" ");
173
+ return /* @__PURE__ */ n("div", { ref: a, className: r, ...c, children: s });
174
174
  }
175
175
  const Me = "_navbar_1gyow_1", Ee = "_inner_1gyow_12", We = "_brand_1gyow_19", Ae = "_links_1gyow_20", De = "_actions_1gyow_50", M = {
176
176
  navbar: Me,
@@ -179,20 +179,20 @@ const Me = "_navbar_1gyow_1", Ee = "_inner_1gyow_12", We = "_brand_1gyow_19", Ae
179
179
  links: Ae,
180
180
  actions: De
181
181
  };
182
- function hn({ brand: e, actions: t, children: i, className: a, ref: c, ...o }) {
183
- const r = [M.navbar, a].filter(Boolean).join(" ");
184
- return /* @__PURE__ */ n("header", { ref: c, className: r, ...o, children: /* @__PURE__ */ h("div", { className: `psi-container ${M.inner}`, children: [
182
+ function hn({ brand: e, actions: t, children: s, className: a, ref: c, ...r }) {
183
+ const o = [M.navbar, a].filter(Boolean).join(" ");
184
+ return /* @__PURE__ */ n("header", { ref: c, className: o, ...r, children: /* @__PURE__ */ h("div", { className: `psi-container ${M.inner}`, children: [
185
185
  e != null && /* @__PURE__ */ n("div", { className: M.brand, children: e }),
186
- /* @__PURE__ */ n("nav", { className: M.links, children: i }),
186
+ /* @__PURE__ */ n("nav", { className: M.links, children: s }),
187
187
  t != null && /* @__PURE__ */ n("div", { className: M.actions, children: t })
188
188
  ] }) });
189
189
  }
190
190
  const He = "_frame_et4fc_1", Ve = {
191
191
  frame: He
192
192
  };
193
- function pn({ ratio: e, className: t, style: i, children: a, ref: c, ...o }) {
194
- const r = [Ve.frame, t].filter(Boolean).join(" ");
195
- return /* @__PURE__ */ n("div", { ref: c, className: r, style: { aspectRatio: String(e), ...i }, ...o, children: a });
193
+ function pn({ ratio: e, className: t, style: s, children: a, ref: c, ...r }) {
194
+ const o = [Ve.frame, t].filter(Boolean).join(" ");
195
+ return /* @__PURE__ */ n("div", { ref: c, className: o, style: { aspectRatio: String(e), ...s }, ...r, children: a });
196
196
  }
197
197
  const Pe = "_field_lplye_1", Te = "_label_lplye_10", Re = "_marker_lplye_16", qe = "_message_lplye_20", Ke = "_invalid_lplye_26", j = {
198
198
  field: Pe,
@@ -204,16 +204,16 @@ const Pe = "_field_lplye_1", Te = "_label_lplye_10", Re = "_marker_lplye_16", qe
204
204
  function gn({
205
205
  label: e,
206
206
  description: t,
207
- error: i,
207
+ error: s,
208
208
  required: a = !1,
209
209
  group: c = !1,
210
- htmlFor: o,
211
- className: r,
210
+ htmlFor: r,
211
+ className: o,
212
212
  children: l,
213
213
  ref: d,
214
- ...s
214
+ ...i
215
215
  }) {
216
- const u = H(), p = o ?? u, _ = !!i, b = _ ? i : t, m = b != null && b !== !1 ? `${p}-message` : void 0, L = [j.field, _ && j.invalid, r].filter(Boolean).join(" "), g = e != null ? /* @__PURE__ */ h(V, { children: [
216
+ const u = H(), p = r ?? u, _ = !!s, b = _ ? s : t, m = b != null && b !== !1 ? `${p}-message` : void 0, L = [j.field, _ && j.invalid, o].filter(Boolean).join(" "), g = e != null ? /* @__PURE__ */ h(V, { children: [
217
217
  e,
218
218
  a && /* @__PURE__ */ n("span", { "aria-hidden": "true", className: j.marker, children: " *" })
219
219
  ] }) : null, W = /* @__PURE__ */ h(V, { children: [
@@ -227,10 +227,10 @@ function gn({
227
227
  ref: d,
228
228
  className: L,
229
229
  "aria-describedby": m,
230
- ...s,
230
+ ...i,
231
231
  children: W
232
232
  }
233
- ) : /* @__PURE__ */ n("div", { ref: d, className: L, ...s, children: W });
233
+ ) : /* @__PURE__ */ n("div", { ref: d, className: L, ...i, children: W });
234
234
  }
235
235
  function fn({ size: e = 20, ...t }) {
236
236
  return /* @__PURE__ */ h("svg", { width: e, height: e, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", ...t, children: [
@@ -370,14 +370,14 @@ const Ue = "_dialog_l23jf_2", Ye = "_w400_l23jf_31", Ze = "_w560_l23jf_32", Ge =
370
370
  function Dn({
371
371
  open: e,
372
372
  onClose: t,
373
- title: i,
373
+ title: s,
374
374
  footer: a,
375
375
  width: c = 560,
376
- dismissible: o = !0,
377
- className: r,
376
+ dismissible: r = !0,
377
+ className: o,
378
378
  children: l,
379
379
  ref: d,
380
- ...s
380
+ ...i
381
381
  }) {
382
382
  const u = z(null), p = H(), _ = (g) => {
383
383
  u.current = g, typeof d == "function" ? d(g) : d && (d.current = g);
@@ -393,23 +393,23 @@ function Dn({
393
393
  };
394
394
  }, [e]);
395
395
  const b = (g) => {
396
- g.preventDefault(), o && t("esc");
396
+ g.preventDefault(), r && t("esc");
397
397
  }, m = (g) => {
398
- o && g.target === u.current && t("backdrop");
399
- }, L = [C.dialog, et[c], r].filter(Boolean).join(" ");
398
+ r && g.target === u.current && t("backdrop");
399
+ }, L = [C.dialog, et[c], o].filter(Boolean).join(" ");
400
400
  return /* @__PURE__ */ n(
401
401
  "dialog",
402
402
  {
403
403
  ref: _,
404
404
  className: L,
405
- "aria-labelledby": i != null ? p : void 0,
405
+ "aria-labelledby": s != null ? p : void 0,
406
406
  onCancel: b,
407
407
  onClick: m,
408
- ...s,
408
+ ...i,
409
409
  children: /* @__PURE__ */ h("div", { className: C.panel, children: [
410
- (i != null || o) && /* @__PURE__ */ h("div", { className: C.header, children: [
411
- i != null && /* @__PURE__ */ n("h2", { id: p, className: C.title, children: i }),
412
- o && /* @__PURE__ */ n(
410
+ (s != null || r) && /* @__PURE__ */ h("div", { className: C.header, children: [
411
+ s != null && /* @__PURE__ */ n("h2", { id: p, className: C.title, children: s }),
412
+ r && /* @__PURE__ */ n(
413
413
  Ce,
414
414
  {
415
415
  variant: "ghost",
@@ -439,18 +439,19 @@ const tt = "_input_hpei8_3", nt = "_size24_hpei8_37", ot = "_size32_hpei8_43", r
439
439
  40: S.size40,
440
440
  48: S.size48
441
441
  };
442
- function Hn({ size: e = 32, error: t = !1, className: i, ref: a, ...c }) {
443
- const o = P(T), r = t || ((o == null ? void 0 : o.invalid) ?? !1), l = [o == null ? void 0 : o.describedBy, c["aria-describedby"]].filter(Boolean).join(" ") || void 0, d = [S.input, at[e], r && S.error, i].filter(Boolean).join(" ");
442
+ function Hn({ size: e = 32, error: t = !1, type: s = "text", className: a, ref: c, ...r }) {
443
+ const o = P(T), l = t || ((o == null ? void 0 : o.invalid) ?? !1), d = [o == null ? void 0 : o.describedBy, r["aria-describedby"]].filter(Boolean).join(" ") || void 0, i = [S.input, at[e], l && S.error, a].filter(Boolean).join(" ");
444
444
  return /* @__PURE__ */ n(
445
445
  "input",
446
446
  {
447
- ref: a,
448
- className: d,
449
- id: c.id ?? (o == null ? void 0 : o.id),
450
- required: c.required ?? ((o == null ? void 0 : o.required) || void 0),
447
+ ref: c,
448
+ className: i,
449
+ id: r.id ?? (o == null ? void 0 : o.id),
450
+ required: r.required ?? ((o == null ? void 0 : o.required) || void 0),
451
451
  "aria-invalid": (o == null ? void 0 : o.invalid) || void 0,
452
- ...c,
453
- "aria-describedby": l
452
+ type: s,
453
+ ...r,
454
+ "aria-describedby": d
454
455
  }
455
456
  );
456
457
  }
@@ -470,26 +471,26 @@ const ct = "_select_4x9ck_3", lt = "_size24_4x9ck_37", ut = "_size32_4x9ck_45",
470
471
  function Vn({
471
472
  size: e = 32,
472
473
  error: t = !1,
473
- className: i,
474
+ className: s,
474
475
  ref: a,
475
476
  children: c,
476
- ...o
477
+ ...r
477
478
  }) {
478
- const r = P(T), l = t || ((r == null ? void 0 : r.invalid) ?? !1), d = [r == null ? void 0 : r.describedBy, o["aria-describedby"]].filter(Boolean).join(" ") || void 0, s = [
479
+ const o = P(T), l = t || ((o == null ? void 0 : o.invalid) ?? !1), d = [o == null ? void 0 : o.describedBy, r["aria-describedby"]].filter(Boolean).join(" ") || void 0, i = [
479
480
  N.select,
480
481
  pt[e],
481
482
  l && N.error,
482
- i
483
+ s
483
484
  ].filter(Boolean).join(" ");
484
485
  return /* @__PURE__ */ n(
485
486
  "select",
486
487
  {
487
488
  ref: a,
488
- className: s,
489
- id: o.id ?? (r == null ? void 0 : r.id),
490
- required: o.required ?? ((r == null ? void 0 : r.required) || void 0),
491
- "aria-invalid": (r == null ? void 0 : r.invalid) || void 0,
492
- ...o,
489
+ className: i,
490
+ id: r.id ?? (o == null ? void 0 : o.id),
491
+ required: r.required ?? ((o == null ? void 0 : o.required) || void 0),
492
+ "aria-invalid": (o == null ? void 0 : o.invalid) || void 0,
493
+ ...r,
493
494
  "aria-describedby": d,
494
495
  children: c
495
496
  }
@@ -505,19 +506,19 @@ const gt = "_label_1v4vv_3", ft = "_disabled_1v4vv_13", vt = "_input_1v4vv_20",
505
506
  function Pn({
506
507
  children: e,
507
508
  className: t,
508
- disabled: i,
509
+ disabled: s,
509
510
  ref: a,
510
511
  ...c
511
512
  }) {
512
- const o = [E.label, i && E.disabled, t].filter(Boolean).join(" ");
513
- return /* @__PURE__ */ h("label", { className: o, children: [
513
+ const r = [E.label, s && E.disabled, t].filter(Boolean).join(" ");
514
+ return /* @__PURE__ */ h("label", { className: r, children: [
514
515
  /* @__PURE__ */ n(
515
516
  "input",
516
517
  {
517
518
  ref: a,
518
519
  type: "checkbox",
519
520
  className: E.input,
520
- disabled: i,
521
+ disabled: s,
521
522
  ...c
522
523
  }
523
524
  ),
@@ -536,12 +537,12 @@ const mt = "_label_jru2c_3", wt = "_disabled_jru2c_13", kt = "_input_jru2c_20",
536
537
  function Tn({
537
538
  children: e,
538
539
  className: t,
539
- disabled: i,
540
+ disabled: s,
540
541
  ref: a,
541
542
  ...c
542
543
  }) {
543
- const o = [B.label, i && B.disabled, t].filter(Boolean).join(" ");
544
- return /* @__PURE__ */ h("label", { className: o, children: [
544
+ const r = [B.label, s && B.disabled, t].filter(Boolean).join(" ");
545
+ return /* @__PURE__ */ h("label", { className: r, children: [
545
546
  /* @__PURE__ */ n(
546
547
  "input",
547
548
  {
@@ -549,7 +550,7 @@ function Tn({
549
550
  type: "checkbox",
550
551
  role: "switch",
551
552
  className: B.input,
552
- disabled: i,
553
+ disabled: s,
553
554
  ...c
554
555
  }
555
556
  ),
@@ -586,21 +587,21 @@ const Lt = "_tag_3oysa_3", $t = "_dismiss_3oysa_17", jt = "_neutral_3oysa_43", B
586
587
  function Rn({
587
588
  variant: e = "neutral",
588
589
  subtle: t = !1,
589
- onDismiss: i,
590
+ onDismiss: s,
590
591
  className: a,
591
592
  children: c,
592
- ref: o,
593
- ...r
593
+ ref: r,
594
+ ...o
594
595
  }) {
595
596
  const l = t ? Vt : Ht, d = [y.tag, l[e], a].filter(Boolean).join(" ");
596
- return /* @__PURE__ */ h("span", { ref: o, className: d, ...r, children: [
597
+ return /* @__PURE__ */ h("span", { ref: r, className: d, ...o, children: [
597
598
  c,
598
- i && /* @__PURE__ */ n(
599
+ s && /* @__PURE__ */ n(
599
600
  "button",
600
601
  {
601
602
  type: "button",
602
603
  className: y.dismiss,
603
- onClick: i,
604
+ onClick: s,
604
605
  "aria-label": "Dismiss",
605
606
  children: /* @__PURE__ */ h(
606
607
  "svg",
@@ -640,27 +641,27 @@ const Pt = "_wrapper_1eu6z_3", Tt = "_tooltip_1eu6z_10", Rt = "_top_1eu6z_24", q
640
641
  function qn({
641
642
  content: e,
642
643
  children: t,
643
- placement: i = "top"
644
+ placement: s = "top"
644
645
  }) {
645
- const [a, c] = Z(!1), o = H(), r = z(void 0), l = k(() => {
646
- r.current !== void 0 && (window.clearTimeout(r.current), r.current = void 0);
647
- }, []), d = k(() => c(!0), []), s = k(() => c(!1), []);
646
+ const [a, c] = Z(!1), r = H(), o = z(void 0), l = k(() => {
647
+ o.current !== void 0 && (window.clearTimeout(o.current), o.current = void 0);
648
+ }, []), d = k(() => c(!0), []), i = k(() => c(!1), []);
648
649
  x(() => l, [l]), x(() => {
649
650
  if (!a) return;
650
651
  const p = (_) => {
651
- _.key === "Escape" && s();
652
+ _.key === "Escape" && i();
652
653
  };
653
654
  return document.addEventListener("keydown", p), () => document.removeEventListener("keydown", p);
654
- }, [a, s]);
655
+ }, [a, i]);
655
656
  const u = U(t, {
656
- "aria-describedby": a ? o : void 0,
657
+ "aria-describedby": a ? r : void 0,
657
658
  onMouseEnter: (p) => {
658
- l(), r.current = window.setTimeout(d, Yt);
659
+ l(), o.current = window.setTimeout(d, Yt);
659
660
  const _ = t.props.onMouseEnter;
660
661
  _ == null || _(p);
661
662
  },
662
663
  onMouseLeave: (p) => {
663
- l(), s();
664
+ l(), i();
664
665
  const _ = t.props.onMouseLeave;
665
666
  _ == null || _(p);
666
667
  },
@@ -670,7 +671,7 @@ function qn({
670
671
  _ == null || _(p);
671
672
  },
672
673
  onBlur: (p) => {
673
- l(), s();
674
+ l(), i();
674
675
  const _ = t.props.onBlur;
675
676
  _ == null || _(p);
676
677
  }
@@ -680,9 +681,9 @@ function qn({
680
681
  a && /* @__PURE__ */ n(
681
682
  "span",
682
683
  {
683
- id: o,
684
+ id: r,
684
685
  role: "tooltip",
685
- className: [I.tooltip, Ut[i]].filter(Boolean).join(" "),
686
+ className: [I.tooltip, Ut[s]].filter(Boolean).join(" "),
686
687
  children: e
687
688
  }
688
689
  )
@@ -694,9 +695,9 @@ const Zt = "_toolbar_1i848_1", Gt = "_gap8_1i848_7", Ot = "_gap12_1i848_8", Xt =
694
695
  gap12: Ot,
695
696
  gap16: Xt
696
697
  }, Jt = { 8: A.gap8, 12: A.gap12, 16: A.gap16 };
697
- function Kn({ gap: e = 8, className: t, children: i, ref: a, ...c }) {
698
- const o = [A.toolbar, Jt[e], t].filter(Boolean).join(" ");
699
- return /* @__PURE__ */ n("div", { ref: a, role: c["aria-label"] != null ? "group" : void 0, className: o, ...c, children: i });
698
+ function Kn({ gap: e = 8, className: t, children: s, ref: a, ...c }) {
699
+ const r = [A.toolbar, Jt[e], t].filter(Boolean).join(" ");
700
+ return /* @__PURE__ */ n("div", { ref: a, role: c["aria-label"] != null ? "group" : void 0, className: r, ...c, children: s });
700
701
  }
701
702
  const Qt = "_trigger_vpa0u_4", en = "_menu_vpa0u_16", tn = "_item_vpa0u_37", nn = "_separator_vpa0u_74", D = {
702
703
  trigger: Qt,
@@ -704,85 +705,85 @@ const Qt = "_trigger_vpa0u_4", en = "_menu_vpa0u_16", tn = "_item_vpa0u_37", nn
704
705
  item: tn,
705
706
  separator: nn
706
707
  }, on = 500;
707
- function rn({ popoverRef: e, triggerRef: t, open: i, onEsc: a }) {
708
- const c = z(""), o = z(0), r = k(() => {
709
- const s = e.current;
710
- return s ? Array.from(
711
- s.querySelectorAll('[data-psi-menu-item]:not([aria-disabled="true"])')
708
+ function rn({ popoverRef: e, triggerRef: t, open: s, onEsc: a }) {
709
+ const c = z(""), r = z(0), o = k(() => {
710
+ const i = e.current;
711
+ return i ? Array.from(
712
+ i.querySelectorAll('[data-psi-menu-item]:not([aria-disabled="true"])')
712
713
  ) : [];
713
714
  }, [e]), l = k(
714
- (s) => {
715
- if (s) {
716
- for (const u of r()) u.tabIndex = u === s ? 0 : -1;
717
- s.focus();
715
+ (i) => {
716
+ if (i) {
717
+ for (const u of o()) u.tabIndex = u === i ? 0 : -1;
718
+ i.focus();
718
719
  }
719
720
  },
720
- [r]
721
+ [o]
721
722
  ), d = k(() => {
722
723
  var u;
723
- const s = (u = t.current) == null ? void 0 : u.querySelector("button, a, [tabindex]");
724
- s == null || s.focus();
724
+ const i = (u = t.current) == null ? void 0 : u.querySelector("button, a, [tabindex]");
725
+ i == null || i.focus();
725
726
  }, [t]);
726
727
  return x(() => {
727
- if (!i) return;
728
- const s = r();
729
- return l(s[0]), () => {
728
+ if (!s) return;
729
+ const i = o();
730
+ return l(i[0]), () => {
730
731
  var u;
731
732
  (u = e.current) != null && u.contains(document.activeElement) && d();
732
733
  };
733
- }, [i, e, r, l, d]), k(
734
- (s) => {
735
- if (s.key === "Escape") {
736
- s.preventDefault(), a();
734
+ }, [s, e, o, l, d]), k(
735
+ (i) => {
736
+ if (i.key === "Escape") {
737
+ i.preventDefault(), a();
737
738
  return;
738
739
  }
739
- const u = r();
740
+ const u = o();
740
741
  if (u.length === 0) return;
741
742
  const p = u.indexOf(document.activeElement);
742
- switch (s.key) {
743
+ switch (i.key) {
743
744
  case "ArrowDown":
744
- s.preventDefault(), l(u[(p + 1 + u.length) % u.length]);
745
+ i.preventDefault(), l(u[(p + 1 + u.length) % u.length]);
745
746
  return;
746
747
  case "ArrowUp":
747
- s.preventDefault(), l(u[p <= 0 ? u.length - 1 : p - 1]);
748
+ i.preventDefault(), l(u[p <= 0 ? u.length - 1 : p - 1]);
748
749
  return;
749
750
  case "Home":
750
- s.preventDefault(), l(u[0]);
751
+ i.preventDefault(), l(u[0]);
751
752
  return;
752
753
  case "End":
753
- s.preventDefault(), l(u[u.length - 1]);
754
+ i.preventDefault(), l(u[u.length - 1]);
754
755
  return;
755
756
  }
756
- if (s.key.length !== 1 || s.metaKey || s.ctrlKey || s.altKey) return;
757
+ if (i.key.length !== 1 || i.metaKey || i.ctrlKey || i.altKey) return;
757
758
  const _ = Date.now();
758
- c.current = _ - o.current > on ? s.key.toLowerCase() : c.current + s.key.toLowerCase(), o.current = _;
759
+ c.current = _ - r.current > on ? i.key.toLowerCase() : c.current + i.key.toLowerCase(), r.current = _;
759
760
  const b = u.find(
760
761
  (m) => (m.textContent ?? "").trim().toLowerCase().startsWith(c.current)
761
762
  );
762
- b && (s.preventDefault(), l(b));
763
+ b && (i.preventDefault(), l(b));
763
764
  },
764
- [r, l, a]
765
+ [o, l, a]
765
766
  );
766
767
  }
767
768
  function sn() {
768
769
  return typeof CSS < "u" && CSS.supports("anchor-name", "--x");
769
770
  }
770
- function an({ popoverRef: e, triggerRef: t, open: i, placement: a, anchorName: c }) {
771
+ function an({ popoverRef: e, triggerRef: t, open: s, placement: a, anchorName: c }) {
771
772
  x(() => {
772
- const o = t.current, r = e.current;
773
- !o || !r || (o.style.setProperty("anchor-name", c), r.style.setProperty("position-anchor", c));
773
+ const r = t.current, o = e.current;
774
+ !r || !o || (r.style.setProperty("anchor-name", c), o.style.setProperty("position-anchor", c));
774
775
  }, [e, t, c]), x(() => {
775
- if (!i || sn()) return;
776
- const o = t.current, r = e.current;
777
- if (!o || !r) return;
776
+ if (!s || sn()) return;
777
+ const r = t.current, o = e.current;
778
+ if (!r || !o) return;
778
779
  const l = () => {
779
- const d = o.getBoundingClientRect(), s = a.startsWith("bottom"), u = a.endsWith("end"), p = getComputedStyle(o).direction === "rtl", _ = u !== p;
780
- r.style.position = "fixed", r.style.top = s ? `${d.bottom}px` : "", r.style.bottom = s ? "" : `${window.innerHeight - d.top}px`, r.style.left = _ ? "" : `${d.left}px`, r.style.right = _ ? `${window.innerWidth - d.right}px` : "";
780
+ const d = r.getBoundingClientRect(), i = a.startsWith("bottom"), u = a.endsWith("end"), p = getComputedStyle(r).direction === "rtl", _ = u !== p;
781
+ o.style.position = "fixed", o.style.top = i ? `${d.bottom}px` : "", o.style.bottom = i ? "" : `${window.innerHeight - d.top}px`, o.style.left = _ ? "" : `${d.left}px`, o.style.right = _ ? `${window.innerWidth - d.right}px` : "";
781
782
  };
782
783
  return l(), window.addEventListener("scroll", l, !0), window.addEventListener("resize", l), () => {
783
784
  window.removeEventListener("scroll", l, !0), window.removeEventListener("resize", l);
784
785
  };
785
- }, [i, a, e, t]);
786
+ }, [s, a, e, t]);
786
787
  }
787
788
  const Y = F({ close: () => {
788
789
  } });
@@ -792,17 +793,17 @@ function K(e) {
792
793
  function Fn({
793
794
  open: e,
794
795
  onClose: t,
795
- trigger: i,
796
+ trigger: s,
796
797
  placement: a = "bottom-start",
797
798
  children: c,
798
- className: o,
799
- ref: r,
799
+ className: r,
800
+ ref: o,
800
801
  ...l
801
802
  }) {
802
- const d = z(null), s = z(null), u = z(!1), p = `--psi-menu-${H().replace(/:/g, "")}`;
803
- an({ popoverRef: d, triggerRef: s, open: e, placement: a, anchorName: p });
803
+ const d = z(null), i = z(null), u = z(!1), p = `--psi-menu-${H().replace(/:/g, "")}`;
804
+ an({ popoverRef: d, triggerRef: i, open: e, placement: a, anchorName: p });
804
805
  const _ = (v) => {
805
- d.current = v, typeof r == "function" ? r(v) : r && (r.current = v);
806
+ d.current = v, typeof o == "function" ? o(v) : o && (o.current = v);
806
807
  };
807
808
  x(() => {
808
809
  const v = d.current;
@@ -821,7 +822,7 @@ function Fn({
821
822
  }
822
823
  }, [t, e]), m = rn({
823
824
  popoverRef: d,
824
- triggerRef: s,
825
+ triggerRef: i,
825
826
  open: e,
826
827
  onEsc: () => t("esc")
827
828
  }), L = k(
@@ -829,15 +830,15 @@ function Fn({
829
830
  t(v);
830
831
  },
831
832
  [t]
832
- ), g = G(i);
833
+ ), g = G(s);
833
834
  x(() => {
834
835
  g || console.warn(
835
836
  "Psi Menu: `trigger` must be a single React element that forwards props to a focusable node. Menu clones it to attach aria-haspopup and aria-expanded; a string, fragment or array cannot carry them, so the menu renders without its trigger ARIA."
836
837
  );
837
838
  }, [g]);
838
- const W = g ? U(i, { "aria-haspopup": "menu", "aria-expanded": e }) : i;
839
+ const W = g ? U(s, { "aria-haspopup": "menu", "aria-expanded": e }) : s;
839
840
  return /* @__PURE__ */ h(V, { children: [
840
- /* @__PURE__ */ n("div", { ref: s, className: D.trigger, "data-psi-menu-trigger": !0, children: W }),
841
+ /* @__PURE__ */ n("div", { ref: i, className: D.trigger, "data-psi-menu-trigger": !0, children: W }),
841
842
  /* @__PURE__ */ n(
842
843
  "div",
843
844
  {
@@ -847,7 +848,7 @@ function Fn({
847
848
  role: "menu",
848
849
  "data-psi-menu": !0,
849
850
  "data-placement": a,
850
- className: [D.menu, o].filter(Boolean).join(" "),
851
+ className: [D.menu, r].filter(Boolean).join(" "),
851
852
  onToggle: b,
852
853
  onKeyDown: m,
853
854
  children: /* @__PURE__ */ n(Y.Provider, { value: { close: L }, children: c })
@@ -855,7 +856,7 @@ function Fn({
855
856
  )
856
857
  ] });
857
858
  }
858
- function Un({ children: e, onSelect: t, variant: i = "neutral", disabled: a = !1 }) {
859
+ function Un({ children: e, onSelect: t, variant: s = "neutral", disabled: a = !1 }) {
859
860
  const { close: c } = P(Y);
860
861
  return /* @__PURE__ */ n(
861
862
  "button",
@@ -863,7 +864,7 @@ function Un({ children: e, onSelect: t, variant: i = "neutral", disabled: a = !1
863
864
  type: "button",
864
865
  role: "menuitem",
865
866
  "data-psi-menu-item": !0,
866
- "data-variant": i,
867
+ "data-variant": s,
867
868
  "aria-disabled": a || void 0,
868
869
  tabIndex: -1,
869
870
  className: D.item,
@@ -61,6 +61,13 @@
61
61
  "description": "Square icon-only button; same variant/size/href contract as Button. Give it an accessible name (`aria-label`).",
62
62
  "slots": [],
63
63
  "props": [
64
+ {
65
+ "name": "aria-label",
66
+ "type": "string",
67
+ "required": true,
68
+ "default": null,
69
+ "description": "Accessible name for the icon-only control. Required: an IconButton has\nno text to name it, so omitting this ships an unnameable button (D60).\nIf you also pass `aria-labelledby`, that wins per the accname algorithm."
70
+ },
64
71
  {
65
72
  "name": "variant",
66
73
  "type": "\"accent\" | \"neutral\" | \"ghost\" | \"danger\"",
@@ -194,6 +201,13 @@
194
201
  "description": "Single-line text input with pixel-true heights (24–48) and an error state. Inside a Field, id/aria-describedby/aria-invalid/required are wired automatically (D49).",
195
202
  "slots": [],
196
203
  "props": [
204
+ {
205
+ "name": "type",
206
+ "type": "\"number\" | \"text\" | \"search\" | \"email\" | \"tel\" | \"url\" | \"password\" | \"date\"",
207
+ "required": false,
208
+ "default": "text",
209
+ "description": "Input type."
210
+ },
197
211
  {
198
212
  "name": "size",
199
213
  "type": "24 | 32 | 40 | 48",
@@ -1,5 +1,215 @@
1
1
  {
2
2
  "patterns": [
3
+ {
4
+ "id": "action-feedback",
5
+ "intent": "Transient confirmation after a completed action, with an optional undo affordance",
6
+ "match": [
7
+ "toast",
8
+ "snackbar",
9
+ "action feedback",
10
+ "confirmation message",
11
+ "undo toast"
12
+ ],
13
+ "compose": {
14
+ "component": "Toast",
15
+ "props": {
16
+ "variant": "{param:variant}"
17
+ },
18
+ "slots": {
19
+ "body": [
20
+ "{content:message}"
21
+ ],
22
+ "action": [
23
+ {
24
+ "component": "Button",
25
+ "props": {
26
+ "variant": "ghost"
27
+ },
28
+ "content": "undo-label"
29
+ }
30
+ ]
31
+ }
32
+ },
33
+ "parameters": [
34
+ {
35
+ "key": "variant",
36
+ "ask": "What kind of outcome is being confirmed?",
37
+ "options": [
38
+ "success",
39
+ "danger",
40
+ "neutral"
41
+ ],
42
+ "default": "success"
43
+ }
44
+ ],
45
+ "content": {
46
+ "message": "[what just happened]",
47
+ "undo-label": "Undo"
48
+ },
49
+ "gaps": [
50
+ "Toast"
51
+ ],
52
+ "blocked": true,
53
+ "preset": null
54
+ },
55
+ {
56
+ "id": "bulk-action-bar",
57
+ "intent": "Bar that appears when table rows are selected: selection count, bulk actions, and a clear-selection control",
58
+ "match": [
59
+ "bulk actions",
60
+ "selection bar",
61
+ "batch actions",
62
+ "selected rows toolbar"
63
+ ],
64
+ "compose": {
65
+ "component": "Toolbar",
66
+ "props": {
67
+ "gap": 12
68
+ },
69
+ "slots": {
70
+ "body": [
71
+ {
72
+ "component": "Tag",
73
+ "props": {
74
+ "variant": "accent",
75
+ "subtle": true
76
+ },
77
+ "content": "count-label"
78
+ },
79
+ {
80
+ "component": "Button",
81
+ "props": {
82
+ "variant": "neutral"
83
+ },
84
+ "content": "export-label"
85
+ },
86
+ {
87
+ "component": "Button",
88
+ "props": {
89
+ "variant": "danger-subtle"
90
+ },
91
+ "content": "delete-label"
92
+ },
93
+ {
94
+ "component": "Button",
95
+ "props": {
96
+ "variant": "ghost"
97
+ },
98
+ "content": "clear-label"
99
+ }
100
+ ]
101
+ }
102
+ },
103
+ "parameters": [],
104
+ "content": {
105
+ "count-label": "[n] selected",
106
+ "export-label": "Export",
107
+ "delete-label": "Delete",
108
+ "clear-label": "Clear selection"
109
+ },
110
+ "gaps": [],
111
+ "blocked": false,
112
+ "preset": "<Toolbar\n gap={12}\n>\n <Tag subtle={true} variant=\"accent\">[n] selected</Tag>\n <Button variant=\"neutral\">Export</Button>\n <Button variant=\"danger-subtle\">Delete</Button>\n <Button variant=\"ghost\">Clear selection</Button>\n</Toolbar>\n"
113
+ },
114
+ {
115
+ "id": "data-table",
116
+ "intent": "Sortable, selectable table of records with numeric columns and a per-row actions menu — compose the row menu from the row-actions pattern",
117
+ "match": [
118
+ "data table",
119
+ "transactions table",
120
+ "sortable table",
121
+ "records table",
122
+ "list of transactions"
123
+ ],
124
+ "compose": {
125
+ "component": "Table",
126
+ "props": {
127
+ "selectable": true,
128
+ "sortable": true
129
+ },
130
+ "slots": {
131
+ "head": [
132
+ "{content:columns-note}"
133
+ ],
134
+ "body": [
135
+ "{content:rows-note}"
136
+ ]
137
+ }
138
+ },
139
+ "parameters": [],
140
+ "content": {
141
+ "columns-note": "[one header cell per column; numeric columns align right]",
142
+ "rows-note": "[one row per record; put the row-actions menu in the last cell]"
143
+ },
144
+ "gaps": [
145
+ "Table"
146
+ ],
147
+ "blocked": true,
148
+ "preset": null
149
+ },
150
+ {
151
+ "id": "date-range-filter",
152
+ "intent": "From/to date pair for narrowing a dated list — native date inputs, no calendar widget",
153
+ "match": [
154
+ "date range filter",
155
+ "from to date",
156
+ "date filter",
157
+ "period filter",
158
+ "filter by date"
159
+ ],
160
+ "compose": {
161
+ "component": "Toolbar",
162
+ "props": {
163
+ "gap": 12
164
+ },
165
+ "slots": {
166
+ "body": [
167
+ {
168
+ "component": "Field",
169
+ "slots": {
170
+ "label": [
171
+ "{content:from-label}"
172
+ ],
173
+ "body": [
174
+ {
175
+ "component": "Input",
176
+ "props": {
177
+ "type": "date",
178
+ "size": 32
179
+ }
180
+ }
181
+ ]
182
+ }
183
+ },
184
+ {
185
+ "component": "Field",
186
+ "slots": {
187
+ "label": [
188
+ "{content:to-label}"
189
+ ],
190
+ "body": [
191
+ {
192
+ "component": "Input",
193
+ "props": {
194
+ "type": "date",
195
+ "size": 32
196
+ }
197
+ }
198
+ ]
199
+ }
200
+ }
201
+ ]
202
+ }
203
+ },
204
+ "parameters": [],
205
+ "content": {
206
+ "from-label": "From",
207
+ "to-label": "To"
208
+ },
209
+ "gaps": [],
210
+ "blocked": false,
211
+ "preset": "<Toolbar\n gap={12}\n>\n <Field\n label=\"From\"\n >\n <Input size={32} type=\"date\" />\n </Field>\n <Field\n label=\"To\"\n >\n <Input size={32} type=\"date\" />\n </Field>\n</Toolbar>\n"
212
+ },
3
213
  {
4
214
  "id": "destructive-confirm",
5
215
  "intent": "Dialog confirming a destructive action",
@@ -59,6 +269,108 @@
59
269
  "blocked": false,
60
270
  "preset": "<Dialog\n footer={<>\n <Button size={32} variant=\"ghost\">Cancel</Button>\n <Button size={32} variant=\"danger\">[verb the object]</Button>\n </>}\n title=\"[Delete the object?]\"\n>\n [what is permanently lost]\n</Dialog>\n"
61
271
  },
272
+ {
273
+ "id": "detail-drawer",
274
+ "intent": "Side sheet showing one record's detail, opened from a table row, with its primary actions in a footer",
275
+ "match": [
276
+ "detail drawer",
277
+ "side sheet",
278
+ "row detail",
279
+ "slide-over",
280
+ "record detail panel"
281
+ ],
282
+ "compose": {
283
+ "component": "Drawer",
284
+ "props": {
285
+ "side": "{param:side}"
286
+ },
287
+ "slots": {
288
+ "title": [
289
+ "{content:title}"
290
+ ],
291
+ "body": [
292
+ "{content:body-note}"
293
+ ],
294
+ "footer": [
295
+ {
296
+ "component": "Button",
297
+ "props": {
298
+ "variant": "accent"
299
+ },
300
+ "content": "primary-label"
301
+ },
302
+ {
303
+ "component": "Button",
304
+ "props": {
305
+ "variant": "neutral"
306
+ },
307
+ "content": "dismiss-label"
308
+ }
309
+ ]
310
+ }
311
+ },
312
+ "parameters": [
313
+ {
314
+ "key": "side",
315
+ "ask": "Which edge should the drawer open from?",
316
+ "options": [
317
+ "end",
318
+ "start"
319
+ ],
320
+ "default": "end"
321
+ }
322
+ ],
323
+ "content": {
324
+ "title": "[record title]",
325
+ "body-note": "[key-value summary of the selected record]",
326
+ "primary-label": "Save",
327
+ "dismiss-label": "Close"
328
+ },
329
+ "gaps": [
330
+ "Drawer"
331
+ ],
332
+ "blocked": true,
333
+ "preset": null
334
+ },
335
+ {
336
+ "id": "empty-state",
337
+ "intent": "Placeholder shown when a list has no results, distinguishing an empty filter from an empty dataset",
338
+ "match": [
339
+ "empty state",
340
+ "no results",
341
+ "nothing here",
342
+ "zero state",
343
+ "no data"
344
+ ],
345
+ "compose": {
346
+ "component": "Panel",
347
+ "props": {
348
+ "padding": 24
349
+ },
350
+ "slots": {
351
+ "body": [
352
+ "{content:headline}",
353
+ "{content:explanation}",
354
+ {
355
+ "component": "Button",
356
+ "props": {
357
+ "variant": "neutral"
358
+ },
359
+ "content": "reset-label"
360
+ }
361
+ ]
362
+ }
363
+ },
364
+ "parameters": [],
365
+ "content": {
366
+ "headline": "[nothing matches these filters]",
367
+ "explanation": "[say whether the dataset is empty or the filters are too narrow]",
368
+ "reset-label": "Clear filters"
369
+ },
370
+ "gaps": [],
371
+ "blocked": false,
372
+ "preset": "<Panel\n padding={24}\n>\n [nothing matches these filters]\n [say whether the dataset is empty or the filters are too narrow]\n <Button variant=\"neutral\">Clear filters</Button>\n</Panel>\n"
373
+ },
62
374
  {
63
375
  "id": "filter-toolbar",
64
376
  "intent": "Toolbar of filter controls: search, category select, active-filter tags",
@@ -95,7 +407,7 @@
95
407
  },
96
408
  {
97
409
  "id": "row-actions",
98
- "intent": "List or table row with a labelled actions menu, including a destructive item — hand the destructive item's confirmation off to the destructive-confirm pattern. The trigger is a text Button, not an icon-only kebab: IconButton cannot carry an accessible name through the manifest, so it is not composable in a pattern yet",
410
+ "intent": "List or table row with an icon-only actions menu, including a destructive item — hand the destructive item's confirmation off to the destructive-confirm pattern",
99
411
  "match": [
100
412
  "row actions",
101
413
  "per-row actions",
@@ -112,11 +424,12 @@
112
424
  "slots": {
113
425
  "trigger": [
114
426
  {
115
- "component": "Button",
427
+ "component": "IconButton",
116
428
  "props": {
117
- "variant": "ghost"
429
+ "variant": "ghost",
430
+ "aria-label": "{content:trigger-label}"
118
431
  },
119
- "content": "trigger-label"
432
+ "content": "trigger-icon"
120
433
  }
121
434
  ],
122
435
  "body": [
@@ -154,13 +467,14 @@
154
467
  ],
155
468
  "content": {
156
469
  "trigger-label": "Actions",
470
+ "trigger-icon": "[icon]",
157
471
  "edit-label": "Edit",
158
472
  "duplicate-label": "Duplicate",
159
473
  "delete-label": "Delete"
160
474
  },
161
475
  "gaps": [],
162
476
  "blocked": false,
163
- "preset": "<Menu\n aria-label=\"Actions\"\n placement=\"bottom-end\"\n trigger={<Button variant=\"ghost\">Actions</Button>}\n>\n <MenuItem>Edit</MenuItem>\n <MenuItem>Duplicate</MenuItem>\n <MenuSeparator />\n <MenuItem variant=\"danger\">Delete</MenuItem>\n</Menu>\n"
477
+ "preset": "<Menu\n aria-label=\"Actions\"\n placement=\"bottom-end\"\n trigger={<IconButton aria-label=\"Actions\" variant=\"ghost\">[icon]</IconButton>}\n>\n <MenuItem>Edit</MenuItem>\n <MenuItem>Duplicate</MenuItem>\n <MenuSeparator />\n <MenuItem variant=\"danger\">Delete</MenuItem>\n</Menu>\n"
164
478
  },
165
479
  {
166
480
  "id": "settings-form-row",
@@ -201,6 +515,142 @@
201
515
  "gaps": [],
202
516
  "blocked": false,
203
517
  "preset": "<Field\n description=\"[one-line consequence of the setting]\"\n group={true}\n label=\"[setting name]\"\n>\n <Switch>[what turning it on does]</Switch>\n</Field>\n"
518
+ },
519
+ {
520
+ "id": "summary-tiles",
521
+ "intent": "Row of headline figures above a ledger — balances or totals, one Card per figure",
522
+ "match": [
523
+ "summary tiles",
524
+ "stat cards",
525
+ "kpi row",
526
+ "balance cards",
527
+ "totals row"
528
+ ],
529
+ "compose": {
530
+ "component": "Toolbar",
531
+ "props": {
532
+ "gap": 16
533
+ },
534
+ "slots": {
535
+ "body": [
536
+ {
537
+ "component": "Card",
538
+ "props": {
539
+ "variant": "stacked"
540
+ },
541
+ "content": "tile-note"
542
+ },
543
+ {
544
+ "component": "Card",
545
+ "props": {
546
+ "variant": "stacked"
547
+ },
548
+ "content": "tile-note"
549
+ }
550
+ ]
551
+ }
552
+ },
553
+ "parameters": [],
554
+ "content": {
555
+ "tile-note": "[label above, figure below]"
556
+ },
557
+ "gaps": [],
558
+ "blocked": false,
559
+ "preset": "<Toolbar\n gap={16}\n>\n <Card variant=\"stacked\">[label above, figure below]</Card>\n <Card variant=\"stacked\">[label above, figure below]</Card>\n</Toolbar>\n"
560
+ },
561
+ {
562
+ "id": "tabbed-workspace",
563
+ "intent": "Tab set switching between sibling views of the same workspace, such as accounts or saved filters",
564
+ "match": [
565
+ "tabs",
566
+ "tabbed view",
567
+ "switch between accounts",
568
+ "tab bar",
569
+ "segmented views"
570
+ ],
571
+ "compose": {
572
+ "component": "Tabs",
573
+ "props": {
574
+ "orientation": "{param:orientation}"
575
+ },
576
+ "slots": {
577
+ "list": [
578
+ "{content:tabs-note}"
579
+ ],
580
+ "panels": [
581
+ "{content:panels-note}"
582
+ ]
583
+ }
584
+ },
585
+ "parameters": [
586
+ {
587
+ "key": "orientation",
588
+ "ask": "How should the tab list be laid out?",
589
+ "options": [
590
+ "horizontal",
591
+ "vertical"
592
+ ],
593
+ "default": "horizontal"
594
+ }
595
+ ],
596
+ "content": {
597
+ "tabs-note": "[one tab per view; keep labels to one or two words]",
598
+ "panels-note": "[one panel per tab, in the same order]"
599
+ },
600
+ "gaps": [
601
+ "Tabs"
602
+ ],
603
+ "blocked": true,
604
+ "preset": null
605
+ },
606
+ {
607
+ "id": "table-pagination",
608
+ "intent": "Page-size selector and page range control beneath a data table",
609
+ "match": [
610
+ "pagination",
611
+ "table pagination",
612
+ "page controls",
613
+ "rows per page",
614
+ "paging"
615
+ ],
616
+ "compose": {
617
+ "component": "Toolbar",
618
+ "props": {
619
+ "gap": 12
620
+ },
621
+ "slots": {
622
+ "body": [
623
+ {
624
+ "component": "Field",
625
+ "slots": {
626
+ "label": [
627
+ "{content:page-size-label}"
628
+ ],
629
+ "body": [
630
+ {
631
+ "component": "Select",
632
+ "props": {
633
+ "size": 32
634
+ }
635
+ }
636
+ ]
637
+ }
638
+ },
639
+ {
640
+ "component": "Pagination"
641
+ }
642
+ ]
643
+ }
644
+ },
645
+ "parameters": [],
646
+ "content": {
647
+ "page-size-label": "Rows per page"
648
+ },
649
+ "gaps": [
650
+ "Pagination"
651
+ ],
652
+ "blocked": true,
653
+ "preset": null
204
654
  }
205
655
  ]
206
656
  }
@@ -7,6 +7,7 @@ Square icon-only button; same variant/size/href contract as Button. Give it an a
7
7
 
8
8
  | Prop | Type | Default | Required | Description |
9
9
  |---|---|---|---|---|
10
+ | `aria-label` | `string` | — | yes | Accessible name for the icon-only control. Required: an IconButton has no text to name it, so omitting this ships an unnameable button (D60). If you also pass `aria-labelledby`, that wins per the accname algorithm. |
10
11
  | `variant` | `"accent" \| "neutral" \| "ghost" \| "danger"` | neutral | no | Visual variant. |
11
12
  | `size` | `24 \| 32 \| 40 \| 48` | 32 | no | Square size in px (24 \| 32 \| 40 \| 48). |
12
13
  | `href` | `string` | — | no | Render as an anchor with this href (D33). disabled → aria-disabled, no href attribute, pointer-events: none. |
package/docs/Input.md CHANGED
@@ -7,6 +7,7 @@ Single-line text input with pixel-true heights (24–48) and an error state. Ins
7
7
 
8
8
  | Prop | Type | Default | Required | Description |
9
9
  |---|---|---|---|---|
10
+ | `type` | `"number" \| "text" \| "search" \| "email" \| "tel" \| "url" \| "password" \| "date"` | text | no | Input type. |
10
11
  | `size` | `24 \| 32 \| 40 \| 48` | 32 | no | Height in px (24 \| 32 \| 40 \| 48). |
11
12
  | `error` | `boolean` | false | no | Show error styling. Inside a Field, the Field's error also lights this. |
12
13
  | `ref` | `Ref<HTMLInputElement>` | — | no | Forwarded ref to the underlying `<input>` element. |
package/llms.txt CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Machine-readable artifacts
6
6
  - dist/manifest.json: full component/prop inventory with types and defaults
7
- - dist/patterns.json: 4 composition patterns (D47) — gap-annotated recipes with clarifying parameters; presets are generated JSX, never hand-written
7
+ - dist/patterns.json: 13 composition patterns (D47) — gap-annotated recipes with clarifying parameters; presets are generated JSX, never hand-written
8
8
  - docs/{Component}.md: per-component docs (props, theming, variant guidance)
9
9
 
10
10
  ## Rules for generated code
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handamade/psi-react",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,7 +40,7 @@
40
40
  "tsx": "^4.22.4",
41
41
  "vite": "^6.0.0",
42
42
  "vitest": "^3.2.6",
43
- "@handamade/psi-tokens": "0.8.1"
43
+ "@handamade/psi-tokens": "0.9.1"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "vite build && tsc -p tsconfig.build.json && tsx scripts/emit-manifest.ts && tsx scripts/emit-patterns.ts && tsx scripts/emit-docs.ts",