@flytedan/flytebot-design-system 0.37.0 → 0.38.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/dist/index.d.cts CHANGED
@@ -19,8 +19,18 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
19
19
  loading?: boolean;
20
20
  block?: boolean;
21
21
  as?: "button" | "a";
22
- }
23
- declare function Button({ children, variant, size, icon, trailingIcon, arrow, loading, disabled, block, as, className, ...rest }: ButtonProps): React.JSX.Element;
22
+ /** When `disabled` (or `loading`), stay in the tab order instead of going natively
23
+ * `disabled`: renders `aria-disabled="true"` rather than the `disabled` attribute,
24
+ * keeps the `:focus-visible` ring, and swallows click/Enter/Space without ever
25
+ * submitting a form. Use whenever a button's disabled state can flip while it holds
26
+ * focus (e.g. a controlled panel re-seeding mid-edit) — a native `disabled` drops focus
27
+ * to `<body>` the instant it applies, and nothing can intercept or redirect that.
28
+ * Also makes `aria-disabled` always explicit ("true"/"false", never absent): since the
29
+ * browser can no longer tell assistive tech this button's state from `disabled` itself,
30
+ * the ARIA state has to say so on its own at every render, not only while disabled. */
31
+ focusableWhenDisabled?: boolean;
32
+ }
33
+ declare function Button({ children, variant, size, icon, trailingIcon, arrow, loading, disabled, focusableWhenDisabled, block, as, className, onClick, onKeyDown, ...rest }: ButtonProps): React.JSX.Element;
24
34
 
25
35
  /** A square action carrying only a Phosphor glyph. Always needs a label for a11y. */
26
36
  interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -2239,12 +2249,19 @@ interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement
2239
2249
  error?: string;
2240
2250
  required?: boolean;
2241
2251
  rows?: number;
2252
+ /** Read-only plus `aria-busy`, with the same visible look as `disabled` (the
2253
+ * `.fd-input-disabled` tokens) — for a value an in-flight request is about to replace
2254
+ * while the field stays reachable. Unlike `disabled`, `busy` never sets the native
2255
+ * `disabled` attribute: `readOnly` blocks editing without removing the control from the
2256
+ * tab order or dropping focus that was already on it, so a keyboard user's in-progress
2257
+ * action (e.g. Ctrl+Enter to resolve) still reaches it while busy. */
2258
+ busy?: boolean;
2242
2259
  /** Applied to the outer .fd-field element — this is where width belongs. */
2243
2260
  style?: React.CSSProperties;
2244
2261
  /** Applied to the outer .fd-field element, like `style` — the same rule as Input. */
2245
2262
  className?: string;
2246
2263
  }
2247
- declare function Textarea({ label, help, error, required, rows, disabled, id, className, style, ...rest }: TextareaProps): React.JSX.Element;
2264
+ declare function Textarea({ label, help, error, required, rows, disabled, busy, readOnly, id, className, style, ...rest }: TextareaProps): React.JSX.Element;
2248
2265
 
2249
2266
  /** Numeric field with thousands formatting while unfocused, +/- steppers (press once for
2250
2267
  * one step, press and hold to repeat with acceleration), ArrowUp/ArrowDown (Shift for
package/dist/index.d.ts CHANGED
@@ -19,8 +19,18 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
19
19
  loading?: boolean;
20
20
  block?: boolean;
21
21
  as?: "button" | "a";
22
- }
23
- declare function Button({ children, variant, size, icon, trailingIcon, arrow, loading, disabled, block, as, className, ...rest }: ButtonProps): React.JSX.Element;
22
+ /** When `disabled` (or `loading`), stay in the tab order instead of going natively
23
+ * `disabled`: renders `aria-disabled="true"` rather than the `disabled` attribute,
24
+ * keeps the `:focus-visible` ring, and swallows click/Enter/Space without ever
25
+ * submitting a form. Use whenever a button's disabled state can flip while it holds
26
+ * focus (e.g. a controlled panel re-seeding mid-edit) — a native `disabled` drops focus
27
+ * to `<body>` the instant it applies, and nothing can intercept or redirect that.
28
+ * Also makes `aria-disabled` always explicit ("true"/"false", never absent): since the
29
+ * browser can no longer tell assistive tech this button's state from `disabled` itself,
30
+ * the ARIA state has to say so on its own at every render, not only while disabled. */
31
+ focusableWhenDisabled?: boolean;
32
+ }
33
+ declare function Button({ children, variant, size, icon, trailingIcon, arrow, loading, disabled, focusableWhenDisabled, block, as, className, onClick, onKeyDown, ...rest }: ButtonProps): React.JSX.Element;
24
34
 
25
35
  /** A square action carrying only a Phosphor glyph. Always needs a label for a11y. */
26
36
  interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -2239,12 +2249,19 @@ interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement
2239
2249
  error?: string;
2240
2250
  required?: boolean;
2241
2251
  rows?: number;
2252
+ /** Read-only plus `aria-busy`, with the same visible look as `disabled` (the
2253
+ * `.fd-input-disabled` tokens) — for a value an in-flight request is about to replace
2254
+ * while the field stays reachable. Unlike `disabled`, `busy` never sets the native
2255
+ * `disabled` attribute: `readOnly` blocks editing without removing the control from the
2256
+ * tab order or dropping focus that was already on it, so a keyboard user's in-progress
2257
+ * action (e.g. Ctrl+Enter to resolve) still reaches it while busy. */
2258
+ busy?: boolean;
2242
2259
  /** Applied to the outer .fd-field element — this is where width belongs. */
2243
2260
  style?: React.CSSProperties;
2244
2261
  /** Applied to the outer .fd-field element, like `style` — the same rule as Input. */
2245
2262
  className?: string;
2246
2263
  }
2247
- declare function Textarea({ label, help, error, required, rows, disabled, id, className, style, ...rest }: TextareaProps): React.JSX.Element;
2264
+ declare function Textarea({ label, help, error, required, rows, disabled, busy, readOnly, id, className, style, ...rest }: TextareaProps): React.JSX.Element;
2248
2265
 
2249
2266
  /** Numeric field with thousands formatting while unfocused, +/- steppers (press once for
2250
2267
  * one step, press and hold to repeat with acceleration), ArrowUp/ArrowDown (Shift for
package/dist/index.js CHANGED
@@ -8,6 +8,9 @@ var VARIANTS = {
8
8
  danger: "fd-btn-danger",
9
9
  link: "fd-btn-link"
10
10
  };
11
+ function isActivationKey(key) {
12
+ return key === "Enter" || key === " ";
13
+ }
11
14
  function Button({
12
15
  children,
13
16
  variant = "primary",
@@ -17,12 +20,16 @@ function Button({
17
20
  arrow = false,
18
21
  loading = false,
19
22
  disabled = false,
23
+ focusableWhenDisabled = false,
20
24
  block = false,
21
25
  as = "button",
22
26
  className = "",
27
+ onClick,
28
+ onKeyDown,
23
29
  ...rest
24
30
  }) {
25
31
  const Tag2 = as;
32
+ const inert = (disabled || loading) && focusableWhenDisabled;
26
33
  const cls = [
27
34
  "fd-btn",
28
35
  VARIANTS[variant] || VARIANTS.primary,
@@ -35,9 +42,16 @@ function Button({
35
42
  Tag2,
36
43
  {
37
44
  className: cls,
38
- disabled: Tag2 === "button" ? disabled || loading : void 0,
39
- "aria-disabled": disabled || loading ? "true" : void 0,
45
+ disabled: Tag2 === "button" ? (disabled || loading) && !focusableWhenDisabled : void 0,
46
+ "aria-disabled": focusableWhenDisabled ? String(disabled || loading) : disabled || loading ? "true" : void 0,
40
47
  "aria-busy": loading ? "true" : void 0,
48
+ onClick: inert ? (e) => {
49
+ e.preventDefault();
50
+ } : onClick,
51
+ onKeyDown: inert ? (e) => {
52
+ if (isActivationKey(e.key)) e.preventDefault();
53
+ onKeyDown?.(e);
54
+ } : onKeyDown,
41
55
  ...rest,
42
56
  children: [
43
57
  icon && !loading ? /* @__PURE__ */ jsx("i", { className: "ph ph-" + icon, "aria-hidden": "true" }) : null,
@@ -3196,12 +3210,23 @@ function DatePicker({ label, help, error, required = false, disabled = false, ra
3196
3210
 
3197
3211
  // src/components/forms/Textarea.tsx
3198
3212
  import { jsx as jsx44, jsxs as jsxs38 } from "react/jsx-runtime";
3199
- function Textarea({ label, help, error, required = false, rows = 4, disabled = false, id, className = "", style, ...rest }) {
3213
+ function Textarea({ label, help, error, required = false, rows = 4, disabled = false, busy = false, readOnly, id, className = "", style, ...rest }) {
3200
3214
  const fieldId = useFieldId(id);
3201
- const box = ["fd-input", "fd-input-textarea", error ? "fd-input-error" : "", disabled ? "fd-input-disabled" : ""].filter(Boolean).join(" ");
3215
+ const box = ["fd-input", "fd-input-textarea", error ? "fd-input-error" : "", disabled || busy ? "fd-input-disabled" : ""].filter(Boolean).join(" ");
3202
3216
  return /* @__PURE__ */ jsxs38("div", { className: ["fd-field", className].filter(Boolean).join(" "), style, children: [
3203
3217
  label ? /* @__PURE__ */ jsx44(FieldLabel, { htmlFor: fieldId, required, children: label }) : null,
3204
- /* @__PURE__ */ jsx44("div", { className: box, children: /* @__PURE__ */ jsx44("textarea", { id: fieldId, rows, disabled, "aria-invalid": error ? "true" : void 0, ...rest }) }),
3218
+ /* @__PURE__ */ jsx44("div", { className: box, children: /* @__PURE__ */ jsx44(
3219
+ "textarea",
3220
+ {
3221
+ id: fieldId,
3222
+ rows,
3223
+ disabled,
3224
+ readOnly: busy || readOnly,
3225
+ "aria-busy": busy || void 0,
3226
+ "aria-invalid": error ? "true" : void 0,
3227
+ ...rest
3228
+ }
3229
+ ) }),
3205
3230
  error ? /* @__PURE__ */ jsxs38("span", { className: "fd-field-error", children: [
3206
3231
  /* @__PURE__ */ jsx44("i", { className: "ph ph-warning-circle", "aria-hidden": "true" }),
3207
3232
  error
@@ -3576,8 +3601,7 @@ function MultiSelectPanel({
3576
3601
  rows: 4,
3577
3602
  placeholder: field.pastePlaceholder || field.placeholder || "Paste names, one per line or comma-separated",
3578
3603
  value: pasteText,
3579
- readOnly: pasteLoading,
3580
- "aria-busy": pasteLoading || void 0,
3604
+ busy: pasteLoading,
3581
3605
  onChange: (e) => setPasteText(e.target.value),
3582
3606
  onKeyDown: (e) => {
3583
3607
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
@@ -3639,19 +3663,9 @@ function MultiSelectPanel({
3639
3663
  {
3640
3664
  size: "sm",
3641
3665
  variant: "secondary",
3642
- "aria-disabled": resolveDisabled,
3643
- onClick: (e) => {
3644
- if (resolveDisabled) {
3645
- e.preventDefault();
3646
- return;
3647
- }
3648
- doResolve();
3649
- },
3650
- onKeyDown: (e) => {
3651
- if (resolveDisabled && (e.key === "Enter" || e.key === " ")) {
3652
- e.preventDefault();
3653
- }
3654
- },
3666
+ disabled: resolveDisabled,
3667
+ focusableWhenDisabled: true,
3668
+ onClick: doResolve,
3655
3669
  children: pasteLoading ? "Resolving\u2026" : "Resolve"
3656
3670
  }
3657
3671
  ) : void 0;
@@ -3788,16 +3802,6 @@ function RangePanel({
3788
3802
  setDraft(next);
3789
3803
  publish();
3790
3804
  };
3791
- const doneClick = (e) => {
3792
- if (doneDisabled) {
3793
- e.preventDefault();
3794
- return;
3795
- }
3796
- onClose();
3797
- };
3798
- const doneKeyDown = (e) => {
3799
- if (doneDisabled && (e.key === "Enter" || e.key === " ")) e.preventDefault();
3800
- };
3801
3805
  return /* @__PURE__ */ jsx45(
3802
3806
  Popover,
3803
3807
  {
@@ -3808,7 +3812,7 @@ function RangePanel({
3808
3812
  offset: 6,
3809
3813
  width: PANEL_WIDTH.range,
3810
3814
  label: field.label,
3811
- footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", "aria-disabled": doneDisabled, onClick: doneClick, onKeyDown: doneKeyDown, children: "Done" }),
3815
+ footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", disabled: doneDisabled, focusableWhenDisabled: true, onClick: onClose, children: "Done" }),
3812
3816
  children: /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
3813
3817
  /* @__PURE__ */ jsx45("span", { ref: minWrapRef, children: /* @__PURE__ */ jsx45(
3814
3818
  NumberInput,
@@ -3923,16 +3927,12 @@ function DatePanel({
3923
3927
  };
3924
3928
  const doneDisabled = kind === "withinDays" && !validity.allValid();
3925
3929
  const confirm = () => {
3926
- if (doneDisabled) return;
3927
3930
  if (kind === "withinDays") {
3928
3931
  const shown = shownWithinDays();
3929
3932
  if (shown) onCommit(shown);
3930
3933
  }
3931
3934
  onClose();
3932
3935
  };
3933
- const doneKeyDown = (e) => {
3934
- if (doneDisabled && (e.key === "Enter" || e.key === " ")) e.preventDefault();
3935
- };
3936
3936
  return /* @__PURE__ */ jsx45(
3937
3937
  Popover,
3938
3938
  {
@@ -3943,7 +3943,7 @@ function DatePanel({
3943
3943
  offset: 6,
3944
3944
  width: PANEL_WIDTH.date,
3945
3945
  label: field.label,
3946
- footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", "aria-disabled": doneDisabled, onClick: confirm, onKeyDown: doneKeyDown, children: "Done" }),
3946
+ footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", disabled: doneDisabled, focusableWhenDisabled: true, onClick: confirm, children: "Done" }),
3947
3947
  children: /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
3948
3948
  /* @__PURE__ */ jsx45(
3949
3949
  SegmentedControl,