@flytedan/flytebot-design-system 0.36.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
@@ -3493,6 +3518,7 @@ function MultiSelectPanel({
3493
3518
  }, [mode2]);
3494
3519
  const doResolve = () => {
3495
3520
  if (!field.resolve) return;
3521
+ if (pasteLoading) return;
3496
3522
  const tokens = Array.from(new Set(pasteText.split(/[\n,]/).map((t) => t.trim()).filter(Boolean)));
3497
3523
  if (!tokens.length) return;
3498
3524
  setPasteLoading(true);
@@ -3575,7 +3601,7 @@ function MultiSelectPanel({
3575
3601
  rows: 4,
3576
3602
  placeholder: field.pastePlaceholder || field.placeholder || "Paste names, one per line or comma-separated",
3577
3603
  value: pasteText,
3578
- disabled: pasteLoading,
3604
+ busy: pasteLoading,
3579
3605
  onChange: (e) => setPasteText(e.target.value),
3580
3606
  onKeyDown: (e) => {
3581
3607
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
@@ -3637,19 +3663,9 @@ function MultiSelectPanel({
3637
3663
  {
3638
3664
  size: "sm",
3639
3665
  variant: "secondary",
3640
- "aria-disabled": resolveDisabled,
3641
- onClick: (e) => {
3642
- if (resolveDisabled) {
3643
- e.preventDefault();
3644
- return;
3645
- }
3646
- doResolve();
3647
- },
3648
- onKeyDown: (e) => {
3649
- if (resolveDisabled && (e.key === "Enter" || e.key === " ")) {
3650
- e.preventDefault();
3651
- }
3652
- },
3666
+ disabled: resolveDisabled,
3667
+ focusableWhenDisabled: true,
3668
+ onClick: doResolve,
3653
3669
  children: pasteLoading ? "Resolving\u2026" : "Resolve"
3654
3670
  }
3655
3671
  ) : void 0;
@@ -3796,7 +3812,7 @@ function RangePanel({
3796
3812
  offset: 6,
3797
3813
  width: PANEL_WIDTH.range,
3798
3814
  label: field.label,
3799
- footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: onClose, disabled: doneDisabled, children: "Done" }),
3815
+ footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", disabled: doneDisabled, focusableWhenDisabled: true, onClick: onClose, children: "Done" }),
3800
3816
  children: /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
3801
3817
  /* @__PURE__ */ jsx45("span", { ref: minWrapRef, children: /* @__PURE__ */ jsx45(
3802
3818
  NumberInput,
@@ -3911,7 +3927,6 @@ function DatePanel({
3911
3927
  };
3912
3928
  const doneDisabled = kind === "withinDays" && !validity.allValid();
3913
3929
  const confirm = () => {
3914
- if (doneDisabled) return;
3915
3930
  if (kind === "withinDays") {
3916
3931
  const shown = shownWithinDays();
3917
3932
  if (shown) onCommit(shown);
@@ -3928,7 +3943,7 @@ function DatePanel({
3928
3943
  offset: 6,
3929
3944
  width: PANEL_WIDTH.date,
3930
3945
  label: field.label,
3931
- footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: confirm, disabled: doneDisabled, children: "Done" }),
3946
+ footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", disabled: doneDisabled, focusableWhenDisabled: true, onClick: confirm, children: "Done" }),
3932
3947
  children: /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
3933
3948
  /* @__PURE__ */ jsx45(
3934
3949
  SegmentedControl,