@flytedan/flytebot-design-system 0.37.0 → 0.39.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"` only while actually disabled/loading (never
24
+ * the `disabled` attribute), keeps the `:focus-visible` ring, and swallows
25
+ * click/Enter/Space — including never letting a click bubble to an ancestor's own
26
+ * handler — without ever submitting a form. Use whenever a button's disabled state can
27
+ * flip while it holds focus (e.g. a controlled panel re-seeding mid-edit) — a native
28
+ * `disabled` drops focus to `<body>` the instant it applies, and nothing can intercept or
29
+ * redirect that. Once enabled again, this behaves exactly like a plain button: no
30
+ * `aria-disabled` attribute at all. */
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"` only while actually disabled/loading (never
24
+ * the `disabled` attribute), keeps the `:focus-visible` ring, and swallows
25
+ * click/Enter/Space — including never letting a click bubble to an ancestor's own
26
+ * handler — without ever submitting a form. Use whenever a button's disabled state can
27
+ * flip while it holds focus (e.g. a controlled panel re-seeding mid-edit) — a native
28
+ * `disabled` drops focus to `<body>` the instant it applies, and nothing can intercept or
29
+ * redirect that. Once enabled again, this behaves exactly like a plain button: no
30
+ * `aria-disabled` attribute at all. */
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
@@ -17,12 +17,16 @@ function Button({
17
17
  arrow = false,
18
18
  loading = false,
19
19
  disabled = false,
20
+ focusableWhenDisabled = false,
20
21
  block = false,
21
22
  as = "button",
22
23
  className = "",
24
+ onClick,
25
+ onKeyDown,
23
26
  ...rest
24
27
  }) {
25
28
  const Tag2 = as;
29
+ const inert = (disabled || loading) && focusableWhenDisabled;
26
30
  const cls = [
27
31
  "fd-btn",
28
32
  VARIANTS[variant] || VARIANTS.primary,
@@ -35,9 +39,14 @@ function Button({
35
39
  Tag2,
36
40
  {
37
41
  className: cls,
38
- disabled: Tag2 === "button" ? disabled || loading : void 0,
42
+ disabled: Tag2 === "button" ? (disabled || loading) && !focusableWhenDisabled : void 0,
39
43
  "aria-disabled": disabled || loading ? "true" : void 0,
40
44
  "aria-busy": loading ? "true" : void 0,
45
+ onClick: inert ? (e) => {
46
+ e.preventDefault();
47
+ e.stopPropagation();
48
+ } : onClick,
49
+ onKeyDown,
41
50
  ...rest,
42
51
  children: [
43
52
  icon && !loading ? /* @__PURE__ */ jsx("i", { className: "ph ph-" + icon, "aria-hidden": "true" }) : null,
@@ -3196,12 +3205,23 @@ function DatePicker({ label, help, error, required = false, disabled = false, ra
3196
3205
 
3197
3206
  // src/components/forms/Textarea.tsx
3198
3207
  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 }) {
3208
+ function Textarea({ label, help, error, required = false, rows = 4, disabled = false, busy = false, readOnly, id, className = "", style, ...rest }) {
3200
3209
  const fieldId = useFieldId(id);
3201
- const box = ["fd-input", "fd-input-textarea", error ? "fd-input-error" : "", disabled ? "fd-input-disabled" : ""].filter(Boolean).join(" ");
3210
+ const box = ["fd-input", "fd-input-textarea", error ? "fd-input-error" : "", disabled || busy ? "fd-input-disabled" : ""].filter(Boolean).join(" ");
3202
3211
  return /* @__PURE__ */ jsxs38("div", { className: ["fd-field", className].filter(Boolean).join(" "), style, children: [
3203
3212
  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 }) }),
3213
+ /* @__PURE__ */ jsx44("div", { className: box, children: /* @__PURE__ */ jsx44(
3214
+ "textarea",
3215
+ {
3216
+ id: fieldId,
3217
+ rows,
3218
+ disabled,
3219
+ readOnly: busy || readOnly,
3220
+ "aria-busy": busy || void 0,
3221
+ "aria-invalid": error ? "true" : void 0,
3222
+ ...rest
3223
+ }
3224
+ ) }),
3205
3225
  error ? /* @__PURE__ */ jsxs38("span", { className: "fd-field-error", children: [
3206
3226
  /* @__PURE__ */ jsx44("i", { className: "ph ph-warning-circle", "aria-hidden": "true" }),
3207
3227
  error
@@ -3576,8 +3596,7 @@ function MultiSelectPanel({
3576
3596
  rows: 4,
3577
3597
  placeholder: field.pastePlaceholder || field.placeholder || "Paste names, one per line or comma-separated",
3578
3598
  value: pasteText,
3579
- readOnly: pasteLoading,
3580
- "aria-busy": pasteLoading || void 0,
3599
+ busy: pasteLoading,
3581
3600
  onChange: (e) => setPasteText(e.target.value),
3582
3601
  onKeyDown: (e) => {
3583
3602
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
@@ -3639,19 +3658,9 @@ function MultiSelectPanel({
3639
3658
  {
3640
3659
  size: "sm",
3641
3660
  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
- },
3661
+ disabled: resolveDisabled,
3662
+ focusableWhenDisabled: true,
3663
+ onClick: doResolve,
3655
3664
  children: pasteLoading ? "Resolving\u2026" : "Resolve"
3656
3665
  }
3657
3666
  ) : void 0;
@@ -3788,16 +3797,6 @@ function RangePanel({
3788
3797
  setDraft(next);
3789
3798
  publish();
3790
3799
  };
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
3800
  return /* @__PURE__ */ jsx45(
3802
3801
  Popover,
3803
3802
  {
@@ -3808,7 +3807,7 @@ function RangePanel({
3808
3807
  offset: 6,
3809
3808
  width: PANEL_WIDTH.range,
3810
3809
  label: field.label,
3811
- footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", "aria-disabled": doneDisabled, onClick: doneClick, onKeyDown: doneKeyDown, children: "Done" }),
3810
+ footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", disabled: doneDisabled, focusableWhenDisabled: true, onClick: onClose, children: "Done" }),
3812
3811
  children: /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
3813
3812
  /* @__PURE__ */ jsx45("span", { ref: minWrapRef, children: /* @__PURE__ */ jsx45(
3814
3813
  NumberInput,
@@ -3923,16 +3922,12 @@ function DatePanel({
3923
3922
  };
3924
3923
  const doneDisabled = kind === "withinDays" && !validity.allValid();
3925
3924
  const confirm = () => {
3926
- if (doneDisabled) return;
3927
3925
  if (kind === "withinDays") {
3928
3926
  const shown = shownWithinDays();
3929
3927
  if (shown) onCommit(shown);
3930
3928
  }
3931
3929
  onClose();
3932
3930
  };
3933
- const doneKeyDown = (e) => {
3934
- if (doneDisabled && (e.key === "Enter" || e.key === " ")) e.preventDefault();
3935
- };
3936
3931
  return /* @__PURE__ */ jsx45(
3937
3932
  Popover,
3938
3933
  {
@@ -3943,7 +3938,7 @@ function DatePanel({
3943
3938
  offset: 6,
3944
3939
  width: PANEL_WIDTH.date,
3945
3940
  label: field.label,
3946
- footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", "aria-disabled": doneDisabled, onClick: confirm, onKeyDown: doneKeyDown, children: "Done" }),
3941
+ footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", disabled: doneDisabled, focusableWhenDisabled: true, onClick: confirm, children: "Done" }),
3947
3942
  children: /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
3948
3943
  /* @__PURE__ */ jsx45(
3949
3944
  SegmentedControl,