@gnome-ui/react 1.45.2 → 1.47.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.
Files changed (54) hide show
  1. package/README.md +1 -0
  2. package/dist/components/AboutDialog/AboutDialog.cjs +1 -1
  3. package/dist/components/AboutDialog/AboutDialog.cjs.map +1 -1
  4. package/dist/components/AboutDialog/AboutDialog.js +105 -106
  5. package/dist/components/AboutDialog/AboutDialog.js.map +1 -1
  6. package/dist/components/BottomSheet/BottomSheet.cjs +1 -1
  7. package/dist/components/BottomSheet/BottomSheet.cjs.map +1 -1
  8. package/dist/components/BottomSheet/BottomSheet.js +36 -36
  9. package/dist/components/BottomSheet/BottomSheet.js.map +1 -1
  10. package/dist/components/Dialog/Dialog.cjs +1 -1
  11. package/dist/components/Dialog/Dialog.cjs.map +1 -1
  12. package/dist/components/Dialog/Dialog.js +50 -51
  13. package/dist/components/Dialog/Dialog.js.map +1 -1
  14. package/dist/components/Dialog/dialogUtils.cjs +1 -1
  15. package/dist/components/Dialog/dialogUtils.cjs.map +1 -1
  16. package/dist/components/Dialog/dialogUtils.d.ts +6 -0
  17. package/dist/components/Dialog/dialogUtils.js +11 -2
  18. package/dist/components/Dialog/dialogUtils.js.map +1 -1
  19. package/dist/components/Dropdown/Dropdown.cjs +1 -1
  20. package/dist/components/Dropdown/Dropdown.cjs.map +1 -1
  21. package/dist/components/Dropdown/Dropdown.js +88 -84
  22. package/dist/components/Dropdown/Dropdown.js.map +1 -1
  23. package/dist/components/PasswordEntryRow/PasswordEntryRow.cjs +1 -1
  24. package/dist/components/PasswordEntryRow/PasswordEntryRow.cjs.map +1 -1
  25. package/dist/components/PasswordEntryRow/PasswordEntryRow.d.ts +1 -1
  26. package/dist/components/PasswordEntryRow/PasswordEntryRow.js +10 -9
  27. package/dist/components/PasswordEntryRow/PasswordEntryRow.js.map +1 -1
  28. package/dist/components/ScrollToTop/ScrollToTop.cjs +2 -0
  29. package/dist/components/ScrollToTop/ScrollToTop.cjs.map +1 -0
  30. package/dist/components/ScrollToTop/ScrollToTop.d.ts +74 -0
  31. package/dist/components/ScrollToTop/ScrollToTop.js +42 -0
  32. package/dist/components/ScrollToTop/ScrollToTop.js.map +1 -0
  33. package/dist/components/ScrollToTop/ScrollToTop.module.css.cjs +2 -0
  34. package/dist/components/ScrollToTop/ScrollToTop.module.css.cjs.map +1 -0
  35. package/dist/components/ScrollToTop/ScrollToTop.module.css.js +13 -0
  36. package/dist/components/ScrollToTop/ScrollToTop.module.css.js.map +1 -0
  37. package/dist/components/ScrollToTop/index.d.ts +2 -0
  38. package/dist/components/ScrollToTop/useScrollToTopVisibility.cjs +2 -0
  39. package/dist/components/ScrollToTop/useScrollToTopVisibility.cjs.map +1 -0
  40. package/dist/components/ScrollToTop/useScrollToTopVisibility.d.ts +21 -0
  41. package/dist/components/ScrollToTop/useScrollToTopVisibility.js +18 -0
  42. package/dist/components/ScrollToTop/useScrollToTopVisibility.js.map +1 -0
  43. package/dist/components/ScrollToTop.cjs +1 -0
  44. package/dist/components/ScrollToTop.d.ts +2 -0
  45. package/dist/components/ScrollToTop.js +2 -0
  46. package/dist/components/ShortcutsDialog/ShortcutsDialog.cjs +1 -1
  47. package/dist/components/ShortcutsDialog/ShortcutsDialog.cjs.map +1 -1
  48. package/dist/components/ShortcutsDialog/ShortcutsDialog.js +74 -74
  49. package/dist/components/ShortcutsDialog/ShortcutsDialog.js.map +1 -1
  50. package/dist/index.cjs +1 -1
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.js +40 -39
  53. package/dist/style.css +1 -1
  54. package/package.json +1 -1
@@ -1,149 +1,153 @@
1
1
  import { Icon as e } from "../Icon/Icon.js";
2
2
  import t from "./Dropdown.module.css.js";
3
- import { useCallback as n, useEffect as r, useId as i, useRef as a, useState as o } from "react";
4
- import { jsx as s, jsxs as c } from "react/jsx-runtime";
5
- import { Check as l, PanDown as u } from "@gnome-ui/icons";
3
+ import { useCallback as n, useEffect as r, useId as i, useLayoutEffect as a, useRef as o, useState as s } from "react";
4
+ import { jsx as c, jsxs as l } from "react/jsx-runtime";
5
+ import { Check as u, PanDown as d } from "@gnome-ui/icons";
6
6
  //#region src/components/Dropdown/Dropdown.tsx
7
- var d = ({ options: d, value: f, onChange: p, placeholder: m = "Select an option", disabled: h, className: g, ..._ }) => {
8
- let [v, y] = o(!1), [b, x] = o(-1), [S, C] = o(!1), w = i(), T = i(), E = a(null), D = a(null), O = d.find((e) => e.value === f), k = n(() => {
9
- if (!E.current) return;
10
- let e = E.current.getBoundingClientRect(), t = window.innerHeight - e.bottom, n = Math.min(d.length * 48 + 8, 280);
11
- C(t < n && e.top > n);
12
- }, [d.length]), A = n(() => {
13
- if (h) return;
14
- k(), y(!0);
15
- let e = d.findIndex((e) => e.value === f && !e.disabled);
16
- x(e >= 0 ? e : d.findIndex((e) => !e.disabled));
7
+ var f = ({ options: f, value: p, onChange: m, placeholder: h = "Select an option", disabled: g, className: _, ...v }) => {
8
+ let [y, b] = s(!1), [x, S] = s(-1), [C, w] = s(!1), T = i(), E = i(), D = o(null), O = o(null), k = f.find((e) => e.value === p), A = n(() => {
9
+ if (!D.current) return;
10
+ let e = D.current.getBoundingClientRect(), t = window.innerHeight - e.bottom, n = Math.min(f.length * 48 + 8, 280);
11
+ w(t < n && e.top > n);
12
+ }, [f.length]), j = n(() => {
13
+ if (g) return;
14
+ A(), b(!0);
15
+ let e = f.findIndex((e) => e.value === p && !e.disabled);
16
+ S(e >= 0 ? e : f.findIndex((e) => !e.disabled));
17
17
  }, [
18
- h,
19
- k,
20
- d,
21
- f
22
- ]), j = n(() => {
23
- y(!1), x(-1), E.current?.focus();
24
- }, []), M = n((e) => {
25
- e.disabled || (p?.(e.value), j());
26
- }, [p, j]);
27
- r(() => {
28
- !v || b < 0 || !D.current || D.current.children[b]?.scrollIntoView({ block: "nearest" });
29
- }, [b, v]), r(() => {
30
- if (!v) return;
18
+ g,
19
+ A,
20
+ f,
21
+ p
22
+ ]), M = n(() => {
23
+ b(!1), S(-1), D.current?.focus();
24
+ }, []), N = n((e) => {
25
+ e.disabled || (m?.(e.value), M());
26
+ }, [m, M]);
27
+ a(() => {
28
+ if (!y || !D.current || !O.current) return;
29
+ let e = D.current.getBoundingClientRect(), t = O.current.offsetHeight;
30
+ w(window.innerHeight - e.bottom < t && e.top > t);
31
+ }, [y]), r(() => {
32
+ !y || x < 0 || !O.current || O.current.children[x]?.scrollIntoView({ block: "nearest" });
33
+ }, [x, y]), r(() => {
34
+ if (!y) return;
31
35
  let e = (e) => {
32
- !E.current?.contains(e.target) && !D.current?.contains(e.target) && j();
36
+ !D.current?.contains(e.target) && !O.current?.contains(e.target) && M();
33
37
  };
34
38
  return document.addEventListener("mousedown", e), () => document.removeEventListener("mousedown", e);
35
- }, [v, j]);
36
- let N = n((e) => {
39
+ }, [y, M]);
40
+ let P = n((e) => {
37
41
  switch (e.key) {
38
42
  case "Enter":
39
43
  case " ":
40
44
  case "ArrowDown":
41
- e.preventDefault(), A();
45
+ e.preventDefault(), j();
42
46
  break;
43
47
  case "ArrowUp":
44
- e.preventDefault(), k(), y(!0), x(d.reduce((e, t, n) => t.disabled ? e : n, -1));
48
+ e.preventDefault(), A(), b(!0), S(f.reduce((e, t, n) => t.disabled ? e : n, -1));
45
49
  break;
46
50
  }
47
51
  }, [
52
+ j,
48
53
  A,
49
- k,
50
- d
51
- ]), P = n((e) => {
52
- let t = d.map((e, t) => ({
54
+ f
55
+ ]), F = n((e) => {
56
+ let t = f.map((e, t) => ({
53
57
  o: e,
54
58
  i: t
55
- })).filter(({ o: e }) => !e.disabled).map(({ i: e }) => e), n = t.indexOf(b);
59
+ })).filter(({ o: e }) => !e.disabled).map(({ i: e }) => e), n = t.indexOf(x);
56
60
  switch (e.key) {
57
61
  case "ArrowDown": {
58
62
  e.preventDefault();
59
63
  let r = t[Math.min(n + 1, t.length - 1)];
60
- x(r ?? b);
64
+ S(r ?? x);
61
65
  break;
62
66
  }
63
67
  case "ArrowUp": {
64
68
  e.preventDefault();
65
69
  let r = t[Math.max(n - 1, 0)];
66
- x(r ?? b);
70
+ S(r ?? x);
67
71
  break;
68
72
  }
69
73
  case "Home":
70
- e.preventDefault(), x(t[0] ?? -1);
74
+ e.preventDefault(), S(t[0] ?? -1);
71
75
  break;
72
76
  case "End":
73
- e.preventDefault(), x(t[t.length - 1] ?? -1);
77
+ e.preventDefault(), S(t[t.length - 1] ?? -1);
74
78
  break;
75
79
  case "Enter":
76
80
  case " ":
77
- e.preventDefault(), b >= 0 && M(d[b]);
81
+ e.preventDefault(), x >= 0 && N(f[x]);
78
82
  break;
79
83
  case "Escape":
80
84
  case "Tab":
81
- j();
85
+ M();
82
86
  break;
83
87
  }
84
88
  }, [
85
- d,
86
- b,
87
- M,
88
- j
89
+ f,
90
+ x,
91
+ N,
92
+ M
89
93
  ]);
90
- return /* @__PURE__ */ c("div", {
91
- className: [t.wrapper, g].filter(Boolean).join(" "),
92
- ..._,
93
- children: [/* @__PURE__ */ c("button", {
94
- ref: E,
95
- id: w,
94
+ return /* @__PURE__ */ l("div", {
95
+ className: [t.wrapper, _].filter(Boolean).join(" "),
96
+ ...v,
97
+ children: [/* @__PURE__ */ l("button", {
98
+ ref: D,
99
+ id: T,
96
100
  type: "button",
97
101
  role: "combobox",
98
102
  "aria-haspopup": "listbox",
99
- "aria-expanded": v,
100
- "aria-controls": T,
101
- "aria-activedescendant": v && b >= 0 ? `${T}-opt-${b}` : void 0,
102
- disabled: h,
103
- className: [t.trigger, v ? t.triggerOpen : null].filter(Boolean).join(" "),
104
- onClick: () => v ? j() : A(),
105
- onKeyDown: N,
106
- children: [/* @__PURE__ */ s("span", {
107
- className: [t.triggerLabel, O ? null : t.placeholder].filter(Boolean).join(" "),
108
- children: O?.label ?? m
109
- }), /* @__PURE__ */ s(e, {
110
- icon: u,
103
+ "aria-expanded": y,
104
+ "aria-controls": E,
105
+ "aria-activedescendant": y && x >= 0 ? `${E}-opt-${x}` : void 0,
106
+ disabled: g,
107
+ className: [t.trigger, y ? t.triggerOpen : null].filter(Boolean).join(" "),
108
+ onClick: () => y ? M() : j(),
109
+ onKeyDown: P,
110
+ children: [/* @__PURE__ */ c("span", {
111
+ className: [t.triggerLabel, k ? null : t.placeholder].filter(Boolean).join(" "),
112
+ children: k?.label ?? h
113
+ }), /* @__PURE__ */ c(e, {
114
+ icon: d,
111
115
  size: "md",
112
116
  "aria-hidden": !0,
113
- className: [t.chevron, v ? t.chevronOpen : null].filter(Boolean).join(" ")
117
+ className: [t.chevron, y ? t.chevronOpen : null].filter(Boolean).join(" ")
114
118
  })]
115
- }), v && /* @__PURE__ */ s("ul", {
116
- ref: D,
117
- id: T,
119
+ }), y && /* @__PURE__ */ c("ul", {
120
+ ref: O,
121
+ id: E,
118
122
  role: "listbox",
119
- "aria-labelledby": w,
123
+ "aria-labelledby": T,
120
124
  tabIndex: -1,
121
- className: [t.list, S ? t.listUp : t.listDown].filter(Boolean).join(" "),
122
- onKeyDown: P,
123
- children: d.map((n, r) => /* @__PURE__ */ c("li", {
124
- id: `${T}-opt-${r}`,
125
+ className: [t.list, C ? t.listUp : t.listDown].filter(Boolean).join(" "),
126
+ onKeyDown: F,
127
+ children: f.map((n, r) => /* @__PURE__ */ l("li", {
128
+ id: `${E}-opt-${r}`,
125
129
  role: "option",
126
- "aria-selected": n.value === f,
130
+ "aria-selected": n.value === p,
127
131
  "aria-disabled": n.disabled,
128
132
  className: [
129
133
  t.option,
130
- n.value === f ? t.optionSelected : null,
131
- r === b ? t.optionActive : null,
134
+ n.value === p ? t.optionSelected : null,
135
+ r === x ? t.optionActive : null,
132
136
  n.disabled ? t.optionDisabled : null
133
137
  ].filter(Boolean).join(" "),
134
- onMouseEnter: () => !n.disabled && x(r),
135
- onClick: () => M(n),
136
- children: [/* @__PURE__ */ c("span", {
138
+ onMouseEnter: () => !n.disabled && S(r),
139
+ onClick: () => N(n),
140
+ children: [/* @__PURE__ */ l("span", {
137
141
  className: t.optionText,
138
- children: [/* @__PURE__ */ s("span", {
142
+ children: [/* @__PURE__ */ c("span", {
139
143
  className: t.optionLabel,
140
144
  children: n.label
141
- }), n.description && /* @__PURE__ */ s("span", {
145
+ }), n.description && /* @__PURE__ */ c("span", {
142
146
  className: t.optionDesc,
143
147
  children: n.description
144
148
  })]
145
- }), n.value === f && /* @__PURE__ */ s(e, {
146
- icon: l,
149
+ }), n.value === p && /* @__PURE__ */ c(e, {
150
+ icon: u,
147
151
  size: "md",
148
152
  "aria-hidden": !0,
149
153
  className: t.checkIcon
@@ -153,6 +157,6 @@ var d = ({ options: d, value: f, onChange: p, placeholder: m = "Select an option
153
157
  });
154
158
  };
155
159
  //#endregion
156
- export { d as Dropdown };
160
+ export { f as Dropdown };
157
161
 
158
162
  //# sourceMappingURL=Dropdown.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Dropdown.js","names":[],"sources":["../../../src/components/Dropdown/Dropdown.tsx"],"sourcesContent":["import { Check, PanDown } from '@gnome-ui/icons';\nimport {\n type HTMLAttributes,\n type KeyboardEvent,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n} from 'react';\n\nimport { Icon } from '../Icon';\n\nimport styles from './Dropdown.module.css';\n\nexport interface DropdownOption<V extends string = string> {\n /** The value submitted / returned on selection. */\n value: V;\n /** Display label shown in the list and trigger. */\n label: string;\n /** Optional descriptive text shown below the label. */\n description?: string;\n /** Whether the option is selectable. */\n disabled?: boolean;\n}\n\nexport interface DropdownProps<V extends string = string>\n extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {\n /** The list of selectable options. */\n options: DropdownOption<V>[];\n /** The currently selected value. */\n value?: V;\n /** Called when the user selects an option. */\n onChange?: (value: V) => void;\n /** Placeholder shown when no option is selected. */\n placeholder?: string;\n /** Accessible label for the control (used as `aria-label`). */\n 'aria-label'?: string;\n /** Disables the entire control. */\n disabled?: boolean;\n}\n\n/**\n * Expandable option list following the Adwaita combo-row / drop-down pattern.\n *\n * - Fully keyboard-navigable: Space/Enter opens; ↑/↓ navigate; Enter selects; Escape closes.\n * - Uses `role=\"combobox\"` + `role=\"listbox\"` + `role=\"option\"` ARIA pattern.\n * - Closes when clicking outside or pressing Escape.\n * - The list flips above the trigger when there is not enough space below.\n *\n * @see https://developer.gnome.org/hig/patterns/controls/drop-down-lists.html\n */\nexport const Dropdown = <V extends string = string>({\n options,\n value,\n onChange,\n placeholder = 'Select an option',\n disabled,\n className,\n ...props\n}: DropdownProps<V>) => {\n const [open, setOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState<number>(-1);\n const [flipUp, setFlipUp] = useState(false);\n\n const triggerId = useId();\n const listboxId = useId();\n const triggerRef = useRef<HTMLButtonElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n\n const selected = options.find((o) => o.value === value);\n\n // Compute flip direction when opening\n const computeFlip = useCallback(() => {\n if (!triggerRef.current) {\n return;\n }\n\n const rect = triggerRef.current.getBoundingClientRect();\n const spaceBelow = window.innerHeight - rect.bottom;\n const listH = Math.min(options.length * 48 + 8, 280);\n\n setFlipUp(spaceBelow < listH && rect.top > listH);\n }, [options.length]);\n\n const openList = useCallback(() => {\n if (disabled) {\n return;\n }\n\n computeFlip();\n setOpen(true);\n const idx = options.findIndex((o) => o.value === value && !o.disabled);\n\n setActiveIndex(idx >= 0 ? idx : options.findIndex((o) => !o.disabled));\n }, [disabled, computeFlip, options, value]);\n\n const closeList = useCallback(() => {\n setOpen(false);\n setActiveIndex(-1);\n triggerRef.current?.focus();\n }, []);\n\n const selectOption = useCallback(\n (opt: DropdownOption<V>) => {\n if (opt.disabled) {\n return;\n }\n\n onChange?.(opt.value);\n closeList();\n },\n [onChange, closeList],\n );\n\n // Scroll active item into view\n useEffect(() => {\n if (!open || activeIndex < 0 || !listRef.current) {\n return;\n }\n\n const item = listRef.current.children[activeIndex] as HTMLElement | undefined;\n\n item?.scrollIntoView({ block: 'nearest' });\n }, [activeIndex, open]);\n\n // Close on outside click\n useEffect(() => {\n if (!open) {\n return;\n }\n\n const handler = (e: MouseEvent) => {\n if (\n !triggerRef.current?.contains(e.target as Node) &&\n !listRef.current?.contains(e.target as Node)\n ) {\n closeList();\n }\n };\n\n document.addEventListener('mousedown', handler);\n\n return () => document.removeEventListener('mousedown', handler);\n }, [open, closeList]);\n\n // Trigger keyboard handler\n const handleTriggerKeyDown = useCallback(\n (e: KeyboardEvent<HTMLButtonElement>) => {\n switch (e.key) {\n case 'Enter':\n case ' ':\n case 'ArrowDown':\n e.preventDefault();\n openList();\n break;\n case 'ArrowUp':\n e.preventDefault();\n computeFlip();\n setOpen(true);\n setActiveIndex(options.reduce((last, o, i) => (!o.disabled ? i : last), -1));\n break;\n }\n },\n [openList, computeFlip, options],\n );\n\n // Listbox keyboard handler\n const handleListKeyDown = useCallback(\n (e: KeyboardEvent<HTMLUListElement>) => {\n const enabledIndexes = options\n .map((o, i) => ({ o, i }))\n .filter(({ o }) => !o.disabled)\n .map(({ i }) => i);\n\n const currentPos = enabledIndexes.indexOf(activeIndex);\n\n switch (e.key) {\n case 'ArrowDown': {\n e.preventDefault();\n const next = enabledIndexes[Math.min(currentPos + 1, enabledIndexes.length - 1)];\n\n setActiveIndex(next ?? activeIndex);\n break;\n }\n case 'ArrowUp': {\n e.preventDefault();\n const prev = enabledIndexes[Math.max(currentPos - 1, 0)];\n\n setActiveIndex(prev ?? activeIndex);\n break;\n }\n case 'Home':\n e.preventDefault();\n setActiveIndex(enabledIndexes[0] ?? -1);\n break;\n case 'End':\n e.preventDefault();\n setActiveIndex(enabledIndexes[enabledIndexes.length - 1] ?? -1);\n break;\n case 'Enter':\n case ' ': {\n e.preventDefault();\n if (activeIndex >= 0) {\n selectOption(options[activeIndex]);\n }\n\n break;\n }\n case 'Escape':\n case 'Tab':\n closeList();\n break;\n }\n },\n [options, activeIndex, selectOption, closeList],\n );\n\n return (\n <div className={[styles.wrapper, className].filter(Boolean).join(' ')} {...props}>\n {/* Trigger button */}\n <button\n ref={triggerRef}\n id={triggerId}\n type=\"button\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-activedescendant={\n open && activeIndex >= 0 ? `${listboxId}-opt-${activeIndex}` : undefined\n }\n disabled={disabled}\n className={[styles.trigger, open ? styles.triggerOpen : null].filter(Boolean).join(' ')}\n onClick={() => (open ? closeList() : openList())}\n onKeyDown={handleTriggerKeyDown}\n >\n <span\n className={[styles.triggerLabel, !selected ? styles.placeholder : null]\n .filter(Boolean)\n .join(' ')}\n >\n {selected?.label ?? placeholder}\n </span>\n <Icon\n icon={PanDown}\n size=\"md\"\n aria-hidden\n className={[styles.chevron, open ? styles.chevronOpen : null].filter(Boolean).join(' ')}\n />\n </button>\n\n {/* Listbox */}\n {open && (\n <ul\n ref={listRef}\n id={listboxId}\n role=\"listbox\"\n aria-labelledby={triggerId}\n tabIndex={-1}\n className={[styles.list, flipUp ? styles.listUp : styles.listDown]\n .filter(Boolean)\n .join(' ')}\n onKeyDown={handleListKeyDown}\n >\n {options.map((opt, i) => (\n <li\n key={opt.value}\n id={`${listboxId}-opt-${i}`}\n role=\"option\"\n aria-selected={opt.value === value}\n aria-disabled={opt.disabled}\n className={[\n styles.option,\n opt.value === value ? styles.optionSelected : null,\n i === activeIndex ? styles.optionActive : null,\n opt.disabled ? styles.optionDisabled : null,\n ]\n .filter(Boolean)\n .join(' ')}\n onMouseEnter={() => !opt.disabled && setActiveIndex(i)}\n onClick={() => selectOption(opt)}\n >\n <span className={styles.optionText}>\n <span className={styles.optionLabel}>{opt.label}</span>\n {opt.description && <span className={styles.optionDesc}>{opt.description}</span>}\n </span>\n {opt.value === value && (\n <Icon icon={Check} size=\"md\" aria-hidden className={styles.checkIcon} />\n )}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"Dropdown.js","names":[],"sources":["../../../src/components/Dropdown/Dropdown.tsx"],"sourcesContent":["import { Check, PanDown } from '@gnome-ui/icons';\nimport {\n type HTMLAttributes,\n type KeyboardEvent,\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react';\n\nimport { Icon } from '../Icon';\n\nimport styles from './Dropdown.module.css';\n\nexport interface DropdownOption<V extends string = string> {\n /** The value submitted / returned on selection. */\n value: V;\n /** Display label shown in the list and trigger. */\n label: string;\n /** Optional descriptive text shown below the label. */\n description?: string;\n /** Whether the option is selectable. */\n disabled?: boolean;\n}\n\nexport interface DropdownProps<V extends string = string>\n extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {\n /** The list of selectable options. */\n options: DropdownOption<V>[];\n /** The currently selected value. */\n value?: V;\n /** Called when the user selects an option. */\n onChange?: (value: V) => void;\n /** Placeholder shown when no option is selected. */\n placeholder?: string;\n /** Accessible label for the control (used as `aria-label`). */\n 'aria-label'?: string;\n /** Disables the entire control. */\n disabled?: boolean;\n}\n\n/**\n * Expandable option list following the Adwaita combo-row / drop-down pattern.\n *\n * - Fully keyboard-navigable: Space/Enter opens; ↑/↓ navigate; Enter selects; Escape closes.\n * - Uses `role=\"combobox\"` + `role=\"listbox\"` + `role=\"option\"` ARIA pattern.\n * - Closes when clicking outside or pressing Escape.\n * - The list flips above the trigger when there is not enough space below.\n *\n * @see https://developer.gnome.org/hig/patterns/controls/drop-down-lists.html\n */\nexport const Dropdown = <V extends string = string>({\n options,\n value,\n onChange,\n placeholder = 'Select an option',\n disabled,\n className,\n ...props\n}: DropdownProps<V>) => {\n const [open, setOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState<number>(-1);\n const [flipUp, setFlipUp] = useState(false);\n\n const triggerId = useId();\n const listboxId = useId();\n const triggerRef = useRef<HTMLButtonElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n\n const selected = options.find((o) => o.value === value);\n\n // Compute flip direction when opening\n const computeFlip = useCallback(() => {\n if (!triggerRef.current) {\n return;\n }\n\n const rect = triggerRef.current.getBoundingClientRect();\n const spaceBelow = window.innerHeight - rect.bottom;\n const listH = Math.min(options.length * 48 + 8, 280);\n\n setFlipUp(spaceBelow < listH && rect.top > listH);\n }, [options.length]);\n\n const openList = useCallback(() => {\n if (disabled) {\n return;\n }\n\n computeFlip();\n setOpen(true);\n const idx = options.findIndex((o) => o.value === value && !o.disabled);\n\n setActiveIndex(idx >= 0 ? idx : options.findIndex((o) => !o.disabled));\n }, [disabled, computeFlip, options, value]);\n\n const closeList = useCallback(() => {\n setOpen(false);\n setActiveIndex(-1);\n triggerRef.current?.focus();\n }, []);\n\n const selectOption = useCallback(\n (opt: DropdownOption<V>) => {\n if (opt.disabled) {\n return;\n }\n\n onChange?.(opt.value);\n closeList();\n },\n [onChange, closeList],\n );\n\n // Refine the flip with the real list height once it has rendered — the\n // pre-open estimate can't account for option descriptions or wrapped labels.\n useLayoutEffect(() => {\n if (!open || !triggerRef.current || !listRef.current) {\n return;\n }\n\n const rect = triggerRef.current.getBoundingClientRect();\n const listH = listRef.current.offsetHeight;\n\n setFlipUp(window.innerHeight - rect.bottom < listH && rect.top > listH);\n }, [open]);\n\n // Scroll active item into view\n useEffect(() => {\n if (!open || activeIndex < 0 || !listRef.current) {\n return;\n }\n\n const item = listRef.current.children[activeIndex] as HTMLElement | undefined;\n\n item?.scrollIntoView({ block: 'nearest' });\n }, [activeIndex, open]);\n\n // Close on outside click\n useEffect(() => {\n if (!open) {\n return;\n }\n\n const handler = (e: MouseEvent) => {\n if (\n !triggerRef.current?.contains(e.target as Node) &&\n !listRef.current?.contains(e.target as Node)\n ) {\n closeList();\n }\n };\n\n document.addEventListener('mousedown', handler);\n\n return () => document.removeEventListener('mousedown', handler);\n }, [open, closeList]);\n\n // Trigger keyboard handler\n const handleTriggerKeyDown = useCallback(\n (e: KeyboardEvent<HTMLButtonElement>) => {\n switch (e.key) {\n case 'Enter':\n case ' ':\n case 'ArrowDown':\n e.preventDefault();\n openList();\n break;\n case 'ArrowUp':\n e.preventDefault();\n computeFlip();\n setOpen(true);\n setActiveIndex(options.reduce((last, o, i) => (!o.disabled ? i : last), -1));\n break;\n }\n },\n [openList, computeFlip, options],\n );\n\n // Listbox keyboard handler\n const handleListKeyDown = useCallback(\n (e: KeyboardEvent<HTMLUListElement>) => {\n const enabledIndexes = options\n .map((o, i) => ({ o, i }))\n .filter(({ o }) => !o.disabled)\n .map(({ i }) => i);\n\n const currentPos = enabledIndexes.indexOf(activeIndex);\n\n switch (e.key) {\n case 'ArrowDown': {\n e.preventDefault();\n const next = enabledIndexes[Math.min(currentPos + 1, enabledIndexes.length - 1)];\n\n setActiveIndex(next ?? activeIndex);\n break;\n }\n case 'ArrowUp': {\n e.preventDefault();\n const prev = enabledIndexes[Math.max(currentPos - 1, 0)];\n\n setActiveIndex(prev ?? activeIndex);\n break;\n }\n case 'Home':\n e.preventDefault();\n setActiveIndex(enabledIndexes[0] ?? -1);\n break;\n case 'End':\n e.preventDefault();\n setActiveIndex(enabledIndexes[enabledIndexes.length - 1] ?? -1);\n break;\n case 'Enter':\n case ' ': {\n e.preventDefault();\n if (activeIndex >= 0) {\n selectOption(options[activeIndex]);\n }\n\n break;\n }\n case 'Escape':\n case 'Tab':\n closeList();\n break;\n }\n },\n [options, activeIndex, selectOption, closeList],\n );\n\n return (\n <div className={[styles.wrapper, className].filter(Boolean).join(' ')} {...props}>\n {/* Trigger button */}\n <button\n ref={triggerRef}\n id={triggerId}\n type=\"button\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-activedescendant={\n open && activeIndex >= 0 ? `${listboxId}-opt-${activeIndex}` : undefined\n }\n disabled={disabled}\n className={[styles.trigger, open ? styles.triggerOpen : null].filter(Boolean).join(' ')}\n onClick={() => (open ? closeList() : openList())}\n onKeyDown={handleTriggerKeyDown}\n >\n <span\n className={[styles.triggerLabel, !selected ? styles.placeholder : null]\n .filter(Boolean)\n .join(' ')}\n >\n {selected?.label ?? placeholder}\n </span>\n <Icon\n icon={PanDown}\n size=\"md\"\n aria-hidden\n className={[styles.chevron, open ? styles.chevronOpen : null].filter(Boolean).join(' ')}\n />\n </button>\n\n {/* Listbox */}\n {open && (\n <ul\n ref={listRef}\n id={listboxId}\n role=\"listbox\"\n aria-labelledby={triggerId}\n tabIndex={-1}\n className={[styles.list, flipUp ? styles.listUp : styles.listDown]\n .filter(Boolean)\n .join(' ')}\n onKeyDown={handleListKeyDown}\n >\n {options.map((opt, i) => (\n <li\n key={opt.value}\n id={`${listboxId}-opt-${i}`}\n role=\"option\"\n aria-selected={opt.value === value}\n aria-disabled={opt.disabled}\n className={[\n styles.option,\n opt.value === value ? styles.optionSelected : null,\n i === activeIndex ? styles.optionActive : null,\n opt.disabled ? styles.optionDisabled : null,\n ]\n .filter(Boolean)\n .join(' ')}\n onMouseEnter={() => !opt.disabled && setActiveIndex(i)}\n onClick={() => selectOption(opt)}\n >\n <span className={styles.optionText}>\n <span className={styles.optionLabel}>{opt.label}</span>\n {opt.description && <span className={styles.optionDesc}>{opt.description}</span>}\n </span>\n {opt.value === value && (\n <Icon icon={Check} size=\"md\" aria-hidden className={styles.checkIcon} />\n )}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../Icon/Icon.cjs"),t=require("../EntryRow/EntryRow.cjs"),n=require("./PasswordEntryRow.module.css.cjs");let r=require("react"),i=require("react/jsx-runtime"),a=require("@gnome-ui/icons");var o=({trailing:o,disabled:s,...c})=>{let[l,u]=(0,r.useState)(!1),d=(0,i.jsx)(`button`,{type:`button`,className:n.default.revealButton,onClick:e=>{e.stopPropagation(),u(e=>!e)},"aria-label":l?`Conceal password`:`Reveal password`,"aria-pressed":l,disabled:s,children:(0,i.jsx)(e.Icon,{icon:l?a.ViewConceal:a.ViewReveal,size:`md`,"aria-hidden":!0})});return(0,i.jsx)(t.EntryRow,{...c,type:l?`text`:`password`,disabled:s,trailing:(0,i.jsxs)(i.Fragment,{children:[o,d]})})};exports.PasswordEntryRow=o;
1
+ const e=require("../Icon/Icon.cjs"),t=require("../EntryRow/EntryRow.cjs"),n=require("./PasswordEntryRow.module.css.cjs");let r=require("react"),i=require("react/jsx-runtime"),a=require("@gnome-ui/icons");var o=({trailing:o,disabled:s,autoComplete:c=`current-password`,...l})=>{let[u,d]=(0,r.useState)(!1),f=(0,i.jsx)(`button`,{type:`button`,className:n.default.revealButton,onClick:e=>{e.stopPropagation(),d(e=>!e)},"aria-label":u?`Conceal password`:`Reveal password`,"aria-pressed":u,disabled:s,children:(0,i.jsx)(e.Icon,{icon:u?a.ViewConceal:a.ViewReveal,size:`md`,"aria-hidden":!0})});return(0,i.jsx)(t.EntryRow,{...l,autoComplete:c,type:u?`text`:`password`,disabled:s,trailing:(0,i.jsxs)(i.Fragment,{children:[o,f]})})};exports.PasswordEntryRow=o;
2
2
  //# sourceMappingURL=PasswordEntryRow.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"PasswordEntryRow.cjs","names":[],"sources":["../../../src/components/PasswordEntryRow/PasswordEntryRow.tsx"],"sourcesContent":["import { ViewConceal, ViewReveal } from '@gnome-ui/icons';\nimport { type ReactNode, useState } from 'react';\n\nimport { EntryRow, type EntryRowProps } from '../EntryRow';\nimport { Icon } from '../Icon';\n\nimport styles from './PasswordEntryRow.module.css';\n\nexport interface PasswordEntryRowProps extends Omit<EntryRowProps, 'type' | 'trailing'> {\n /** Additional trailing widgets placed before the reveal button. */\n trailing?: ReactNode;\n}\n\n/**\n * Password entry row with a built-in reveal/conceal toggle.\n *\n * Mirrors `AdwPasswordEntryRow` — an `EntryRow` variant that defaults to\n * `type=\"password\"` and provides a trailing icon button to reveal or conceal\n * the entered text. Use inside a `BoxedList` for password settings fields.\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.PasswordEntryRow.html\n */\nexport const PasswordEntryRow = ({ trailing, disabled, ...props }: PasswordEntryRowProps) => {\n const [revealed, setRevealed] = useState(false);\n\n const revealButton = (\n <button\n type=\"button\"\n className={styles.revealButton}\n onClick={(e) => {\n e.stopPropagation();\n setRevealed((v) => !v);\n }}\n aria-label={revealed ? 'Conceal password' : 'Reveal password'}\n aria-pressed={revealed}\n disabled={disabled}\n >\n <Icon icon={revealed ? ViewConceal : ViewReveal} size=\"md\" aria-hidden />\n </button>\n );\n\n return (\n <EntryRow\n {...props}\n type={revealed ? 'text' : 'password'}\n disabled={disabled}\n trailing={\n <>\n {trailing}\n {revealButton}\n </>\n }\n />\n );\n};\n"],"mappings":"4MAsBA,IAAa,GAAoB,CAAE,WAAU,WAAU,GAAG,KAAmC,CAC3F,GAAM,CAAC,EAAU,IAAA,EAAA,EAAA,UAAwB,EAAK,EAExC,GACJ,EAAA,EAAA,KAAC,SAAD,CACE,KAAK,SACL,UAAW,EAAA,QAAO,aAClB,QAAU,GAAM,CACd,EAAE,gBAAgB,EAClB,EAAa,GAAM,CAAC,CAAC,CACvB,EACA,aAAY,EAAW,mBAAqB,kBAC5C,eAAc,EACJ,qBAEV,EAAA,EAAA,KAAC,EAAA,KAAD,CAAM,KAAM,EAAW,EAAA,YAAc,EAAA,WAAY,KAAK,KAAK,cAAA,EAAa,CAAA,CAClE,CAAA,EAGV,OACE,EAAA,EAAA,KAAC,EAAA,SAAD,CACE,GAAI,EACJ,KAAM,EAAW,OAAS,WAChB,WACV,UACE,EAAA,EAAA,MAAA,EAAA,SAAA,CAAA,SAAA,CACG,EACA,CACD,CAAA,CAAA,CAEL,CAAA,CAEL"}
1
+ {"version":3,"file":"PasswordEntryRow.cjs","names":[],"sources":["../../../src/components/PasswordEntryRow/PasswordEntryRow.tsx"],"sourcesContent":["import { ViewConceal, ViewReveal } from '@gnome-ui/icons';\nimport { type ReactNode, useState } from 'react';\n\nimport { EntryRow, type EntryRowProps } from '../EntryRow';\nimport { Icon } from '../Icon';\n\nimport styles from './PasswordEntryRow.module.css';\n\nexport interface PasswordEntryRowProps extends Omit<EntryRowProps, 'type' | 'trailing'> {\n /** Additional trailing widgets placed before the reveal button. */\n trailing?: ReactNode;\n}\n\n/**\n * Password entry row with a built-in reveal/conceal toggle.\n *\n * Mirrors `AdwPasswordEntryRow` — an `EntryRow` variant that defaults to\n * `type=\"password\"` and provides a trailing icon button to reveal or conceal\n * the entered text. Use inside a `BoxedList` for password settings fields.\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.PasswordEntryRow.html\n */\nexport const PasswordEntryRow = ({\n trailing,\n disabled,\n // \"current-password\" lets password managers and mobile keyboards offer the\n // saved credential; pass \"new-password\" for registration / change forms.\n autoComplete = 'current-password',\n ...props\n}: PasswordEntryRowProps) => {\n const [revealed, setRevealed] = useState(false);\n\n const revealButton = (\n <button\n type=\"button\"\n className={styles.revealButton}\n onClick={(e) => {\n e.stopPropagation();\n setRevealed((v) => !v);\n }}\n aria-label={revealed ? 'Conceal password' : 'Reveal password'}\n aria-pressed={revealed}\n disabled={disabled}\n >\n <Icon icon={revealed ? ViewConceal : ViewReveal} size=\"md\" aria-hidden />\n </button>\n );\n\n return (\n <EntryRow\n {...props}\n autoComplete={autoComplete}\n type={revealed ? 'text' : 'password'}\n disabled={disabled}\n trailing={\n <>\n {trailing}\n {revealButton}\n </>\n }\n />\n );\n};\n"],"mappings":"4MAsBA,IAAa,GAAoB,CAC/B,WACA,WAGA,eAAe,mBACf,GAAG,KACwB,CAC3B,GAAM,CAAC,EAAU,IAAA,EAAA,EAAA,UAAwB,EAAK,EAExC,GACJ,EAAA,EAAA,KAAC,SAAD,CACE,KAAK,SACL,UAAW,EAAA,QAAO,aAClB,QAAU,GAAM,CACd,EAAE,gBAAgB,EAClB,EAAa,GAAM,CAAC,CAAC,CACvB,EACA,aAAY,EAAW,mBAAqB,kBAC5C,eAAc,EACJ,qBAEV,EAAA,EAAA,KAAC,EAAA,KAAD,CAAM,KAAM,EAAW,EAAA,YAAc,EAAA,WAAY,KAAK,KAAK,cAAA,EAAa,CAAA,CAClE,CAAA,EAGV,OACE,EAAA,EAAA,KAAC,EAAA,SAAD,CACE,GAAI,EACU,eACd,KAAM,EAAW,OAAS,WAChB,WACV,UACE,EAAA,EAAA,MAAA,EAAA,SAAA,CAAA,SAAA,CACG,EACA,CACD,CAAA,CAAA,CAEL,CAAA,CAEL"}
@@ -13,4 +13,4 @@ export interface PasswordEntryRowProps extends Omit<EntryRowProps, 'type' | 'tra
13
13
  *
14
14
  * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.PasswordEntryRow.html
15
15
  */
16
- export declare const PasswordEntryRow: ({ trailing, disabled, ...props }: PasswordEntryRowProps) => import("react/jsx-runtime").JSX.Element;
16
+ export declare const PasswordEntryRow: ({ trailing, disabled, autoComplete, ...props }: PasswordEntryRowProps) => import("react/jsx-runtime").JSX.Element;
@@ -5,27 +5,28 @@ import { useState as r } from "react";
5
5
  import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
6
6
  import { ViewConceal as s, ViewReveal as c } from "@gnome-ui/icons";
7
7
  //#region src/components/PasswordEntryRow/PasswordEntryRow.tsx
8
- var l = ({ trailing: l, disabled: u, ...d }) => {
9
- let [f, p] = r(!1), m = /* @__PURE__ */ a("button", {
8
+ var l = ({ trailing: l, disabled: u, autoComplete: d = "current-password", ...f }) => {
9
+ let [p, m] = r(!1), h = /* @__PURE__ */ a("button", {
10
10
  type: "button",
11
11
  className: n.revealButton,
12
12
  onClick: (e) => {
13
- e.stopPropagation(), p((e) => !e);
13
+ e.stopPropagation(), m((e) => !e);
14
14
  },
15
- "aria-label": f ? "Conceal password" : "Reveal password",
16
- "aria-pressed": f,
15
+ "aria-label": p ? "Conceal password" : "Reveal password",
16
+ "aria-pressed": p,
17
17
  disabled: u,
18
18
  children: /* @__PURE__ */ a(e, {
19
- icon: f ? s : c,
19
+ icon: p ? s : c,
20
20
  size: "md",
21
21
  "aria-hidden": !0
22
22
  })
23
23
  });
24
24
  return /* @__PURE__ */ a(t, {
25
- ...d,
26
- type: f ? "text" : "password",
25
+ ...f,
26
+ autoComplete: d,
27
+ type: p ? "text" : "password",
27
28
  disabled: u,
28
- trailing: /* @__PURE__ */ o(i, { children: [l, m] })
29
+ trailing: /* @__PURE__ */ o(i, { children: [l, h] })
29
30
  });
30
31
  };
31
32
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"PasswordEntryRow.js","names":[],"sources":["../../../src/components/PasswordEntryRow/PasswordEntryRow.tsx"],"sourcesContent":["import { ViewConceal, ViewReveal } from '@gnome-ui/icons';\nimport { type ReactNode, useState } from 'react';\n\nimport { EntryRow, type EntryRowProps } from '../EntryRow';\nimport { Icon } from '../Icon';\n\nimport styles from './PasswordEntryRow.module.css';\n\nexport interface PasswordEntryRowProps extends Omit<EntryRowProps, 'type' | 'trailing'> {\n /** Additional trailing widgets placed before the reveal button. */\n trailing?: ReactNode;\n}\n\n/**\n * Password entry row with a built-in reveal/conceal toggle.\n *\n * Mirrors `AdwPasswordEntryRow` — an `EntryRow` variant that defaults to\n * `type=\"password\"` and provides a trailing icon button to reveal or conceal\n * the entered text. Use inside a `BoxedList` for password settings fields.\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.PasswordEntryRow.html\n */\nexport const PasswordEntryRow = ({ trailing, disabled, ...props }: PasswordEntryRowProps) => {\n const [revealed, setRevealed] = useState(false);\n\n const revealButton = (\n <button\n type=\"button\"\n className={styles.revealButton}\n onClick={(e) => {\n e.stopPropagation();\n setRevealed((v) => !v);\n }}\n aria-label={revealed ? 'Conceal password' : 'Reveal password'}\n aria-pressed={revealed}\n disabled={disabled}\n >\n <Icon icon={revealed ? ViewConceal : ViewReveal} size=\"md\" aria-hidden />\n </button>\n );\n\n return (\n <EntryRow\n {...props}\n type={revealed ? 'text' : 'password'}\n disabled={disabled}\n trailing={\n <>\n {trailing}\n {revealButton}\n </>\n }\n />\n );\n};\n"],"mappings":";;;;;;;AAsBA,IAAa,KAAoB,EAAE,aAAU,aAAU,GAAG,QAAmC;CAC3F,IAAM,CAAC,GAAU,KAAe,EAAS,EAAK,GAExC,IACJ,kBAAC,UAAD;EACE,MAAK;EACL,WAAW,EAAO;EAClB,UAAU,MAAM;GAEd,AADA,EAAE,gBAAgB,GAClB,GAAa,MAAM,CAAC,CAAC;EACvB;EACA,cAAY,IAAW,qBAAqB;EAC5C,gBAAc;EACJ;YAEV,kBAAC,GAAD;GAAM,MAAM,IAAW,IAAc;GAAY,MAAK;GAAK,eAAA;EAAa,CAAA;CAClE,CAAA;CAGV,OACE,kBAAC,GAAD;EACE,GAAI;EACJ,MAAM,IAAW,SAAS;EAChB;EACV,UACE,kBAAA,GAAA,EAAA,UAAA,CACG,GACA,CACD,EAAA,CAAA;CAEL,CAAA;AAEL"}
1
+ {"version":3,"file":"PasswordEntryRow.js","names":[],"sources":["../../../src/components/PasswordEntryRow/PasswordEntryRow.tsx"],"sourcesContent":["import { ViewConceal, ViewReveal } from '@gnome-ui/icons';\nimport { type ReactNode, useState } from 'react';\n\nimport { EntryRow, type EntryRowProps } from '../EntryRow';\nimport { Icon } from '../Icon';\n\nimport styles from './PasswordEntryRow.module.css';\n\nexport interface PasswordEntryRowProps extends Omit<EntryRowProps, 'type' | 'trailing'> {\n /** Additional trailing widgets placed before the reveal button. */\n trailing?: ReactNode;\n}\n\n/**\n * Password entry row with a built-in reveal/conceal toggle.\n *\n * Mirrors `AdwPasswordEntryRow` — an `EntryRow` variant that defaults to\n * `type=\"password\"` and provides a trailing icon button to reveal or conceal\n * the entered text. Use inside a `BoxedList` for password settings fields.\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.PasswordEntryRow.html\n */\nexport const PasswordEntryRow = ({\n trailing,\n disabled,\n // \"current-password\" lets password managers and mobile keyboards offer the\n // saved credential; pass \"new-password\" for registration / change forms.\n autoComplete = 'current-password',\n ...props\n}: PasswordEntryRowProps) => {\n const [revealed, setRevealed] = useState(false);\n\n const revealButton = (\n <button\n type=\"button\"\n className={styles.revealButton}\n onClick={(e) => {\n e.stopPropagation();\n setRevealed((v) => !v);\n }}\n aria-label={revealed ? 'Conceal password' : 'Reveal password'}\n aria-pressed={revealed}\n disabled={disabled}\n >\n <Icon icon={revealed ? ViewConceal : ViewReveal} size=\"md\" aria-hidden />\n </button>\n );\n\n return (\n <EntryRow\n {...props}\n autoComplete={autoComplete}\n type={revealed ? 'text' : 'password'}\n disabled={disabled}\n trailing={\n <>\n {trailing}\n {revealButton}\n </>\n }\n />\n );\n};\n"],"mappings":";;;;;;;AAsBA,IAAa,KAAoB,EAC/B,aACA,aAGA,kBAAe,oBACf,GAAG,QACwB;CAC3B,IAAM,CAAC,GAAU,KAAe,EAAS,EAAK,GAExC,IACJ,kBAAC,UAAD;EACE,MAAK;EACL,WAAW,EAAO;EAClB,UAAU,MAAM;GAEd,AADA,EAAE,gBAAgB,GAClB,GAAa,MAAM,CAAC,CAAC;EACvB;EACA,cAAY,IAAW,qBAAqB;EAC5C,gBAAc;EACJ;YAEV,kBAAC,GAAD;GAAM,MAAM,IAAW,IAAc;GAAY,MAAK;GAAK,eAAA;EAAa,CAAA;CAClE,CAAA;CAGV,OACE,kBAAC,GAAD;EACE,GAAI;EACU;EACd,MAAM,IAAW,SAAS;EAChB;EACV,UACE,kBAAA,GAAA,EAAA,UAAA,CACG,GACA,CACD,EAAA,CAAA;CAEL,CAAA;AAEL"}
@@ -0,0 +1,2 @@
1
+ const e=require("../IconButton/IconButton.cjs"),t=require("./ScrollToTop.module.css.cjs"),n=require("./useScrollToTopVisibility.cjs");let r=require("react"),i=require("react/jsx-runtime"),a=require("@gnome-ui/icons");var o={"bottom-right":t.default.bottom_right,"bottom-left":t.default.bottom_left,"bottom-center":t.default.bottom_center,"top-right":t.default.top_right,"top-left":t.default.top_left,"top-center":t.default.top_center},s=(0,r.forwardRef)(function({visible:r=`auto`,position:s=`bottom-right`,threshold:c=300,scrollTarget:l,className:u,style:d},f){return n.useScrollToTopVisibility({visible:r,threshold:c,scrollTarget:l})?(0,i.jsx)(`div`,{ref:f,className:[t.default.root,o[s],u].filter(Boolean).join(` `),style:d,children:(0,i.jsx)(e.IconButton,{icon:a.GoUp,label:`Scroll to top`,variant:`osd`,onClick:()=>(l??window).scrollTo({top:0,behavior:`smooth`})})}):null});exports.ScrollToTop=s;
2
+ //# sourceMappingURL=ScrollToTop.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScrollToTop.cjs","names":[],"sources":["../../../src/components/ScrollToTop/ScrollToTop.tsx"],"sourcesContent":["import { GoUp } from '@gnome-ui/icons';\nimport { type CSSProperties, forwardRef } from 'react';\n\nimport { IconButton } from '../IconButton';\nimport styles from './ScrollToTop.module.css';\nimport { useScrollToTopVisibility } from './useScrollToTopVisibility';\n\n/**\n * Controls when the button is rendered.\n *\n * - `\"auto\"` — hidden until the user scrolls past `threshold` pixels (default).\n * - `\"always\"` — always rendered regardless of scroll position.\n */\nexport type ScrollToTopVisible = 'always' | 'auto';\n\n/**\n * Anchor corner or edge for the fixed-position container.\n * The button is inset `--gnome-space-4` (24 px) from each named edge.\n */\nexport type ScrollToTopPosition =\n | 'bottom-right'\n | 'bottom-left'\n | 'bottom-center'\n | 'top-right'\n | 'top-left'\n | 'top-center';\n\nexport interface ScrollToTopProps {\n /**\n * Controls when the button is visible.\n *\n * - `\"auto\"` (default) — shown only after the user scrolls past `threshold` pixels.\n * Hides again when they scroll back above the threshold.\n * - `\"always\"` — permanently visible.\n */\n visible?: ScrollToTopVisible;\n /**\n * Corner or edge where the button is anchored inside the viewport.\n *\n * Default: `\"bottom-right\"`.\n */\n position?: ScrollToTopPosition;\n /**\n * Number of pixels the user must scroll before the button appears.\n * Only relevant when `visible=\"auto\"`.\n *\n * Default: `300`.\n */\n threshold?: number;\n /**\n * Scrollable element to observe and scroll back to the top.\n * Pass an `HTMLElement` when the scrollable area is a container other than\n * the page (e.g. a drawer, a split-view pane, or a custom scroll region).\n *\n * Defaults to `window`.\n */\n scrollTarget?: HTMLElement | Window;\n /** Additional CSS class applied to the root positioning container. */\n className?: string;\n /** Inline styles applied to the root positioning container. */\n style?: CSSProperties;\n}\n\nconst POSITION_CLASS: Record<ScrollToTopPosition, string> = {\n 'bottom-right': styles.bottom_right,\n 'bottom-left': styles.bottom_left,\n 'bottom-center': styles.bottom_center,\n 'top-right': styles.top_right,\n 'top-left': styles.top_left,\n 'top-center': styles.top_center,\n};\n\n/**\n * Fixed-position OSD button that scrolls the page (or a container) back to the top.\n *\n * - Renders a circular `IconButton` with the `GoUp` icon at the chosen `position`.\n * - When `visible=\"auto\"` (default), the button appears only after the user scrolls\n * past `threshold` pixels and hides again when they scroll back above it.\n * - The button is semi-transparent at rest (`opacity: 0.5`) and fully opaque on\n * hover or keyboard focus, so it does not obscure underlying content.\n * - Clicking triggers a smooth scroll to the top via `scrollTo({ top: 0, behavior: \"smooth\" })`.\n * - Forwards `ref` to the root `<div>` positioning container, not the inner `<button>`.\n *\n * @example\n * // Minimal — appears after 300 px of scroll, anchored bottom-right\n * <ScrollToTop />\n *\n * @example\n * // Always visible, anchored bottom-left\n * <ScrollToTop visible=\"always\" position=\"bottom-left\" />\n *\n * @example\n * // Observe a custom scrollable container\n * const ref = useRef<HTMLDivElement>(null);\n * <div ref={ref} style={{ overflowY: 'auto', height: 400 }}>...</div>\n * <ScrollToTop scrollTarget={ref.current ?? undefined} threshold={150} />\n */\nexport const ScrollToTop = forwardRef<HTMLDivElement, ScrollToTopProps>(function ScrollToTop(\n { visible = 'auto', position = 'bottom-right', threshold = 300, scrollTarget, className, style },\n ref,\n) {\n const isVisible = useScrollToTopVisibility({ visible, threshold, scrollTarget });\n\n if (!isVisible) {\n return null;\n }\n\n return (\n <div\n ref={ref}\n className={[styles.root, POSITION_CLASS[position], className].filter(Boolean).join(' ')}\n style={style}\n >\n <IconButton\n icon={GoUp}\n label=\"Scroll to top\"\n variant=\"osd\"\n onClick={() => (scrollTarget ?? window).scrollTo({ top: 0, behavior: 'smooth' })}\n />\n </div>\n );\n});\n"],"mappings":"yNA+DA,IAAM,EAAsD,CAC1D,eAAgB,EAAA,QAAO,aACvB,cAAe,EAAA,QAAO,YACtB,gBAAiB,EAAA,QAAO,cACxB,YAAa,EAAA,QAAO,UACpB,WAAY,EAAA,QAAO,SACnB,aAAc,EAAA,QAAO,UACvB,EA2Ba,GAAA,EAAA,EAAA,YAA2D,SACtE,CAAE,UAAU,OAAQ,WAAW,eAAgB,YAAY,IAAK,eAAc,YAAW,SACzF,EACA,CAOA,OANkB,EAAA,yBAAyB,CAAE,UAAS,YAAW,cAAa,CAEzE,GAKH,EAAA,EAAA,KAAC,MAAD,CACO,MACL,UAAW,CAAC,EAAA,QAAO,KAAM,EAAe,GAAW,CAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,EAC/E,kBAEP,EAAA,EAAA,KAAC,EAAA,WAAD,CACE,KAAM,EAAA,KACN,MAAM,gBACN,QAAQ,MACR,aAAgB,GAAgB,QAAQ,SAAS,CAAE,IAAK,EAAG,SAAU,QAAS,CAAC,CAChF,CAAA,CACE,CAAA,EAfE,IAiBX,CAAC"}
@@ -0,0 +1,74 @@
1
+ import { CSSProperties } from 'react';
2
+ /**
3
+ * Controls when the button is rendered.
4
+ *
5
+ * - `"auto"` — hidden until the user scrolls past `threshold` pixels (default).
6
+ * - `"always"` — always rendered regardless of scroll position.
7
+ */
8
+ export type ScrollToTopVisible = 'always' | 'auto';
9
+ /**
10
+ * Anchor corner or edge for the fixed-position container.
11
+ * The button is inset `--gnome-space-4` (24 px) from each named edge.
12
+ */
13
+ export type ScrollToTopPosition = 'bottom-right' | 'bottom-left' | 'bottom-center' | 'top-right' | 'top-left' | 'top-center';
14
+ export interface ScrollToTopProps {
15
+ /**
16
+ * Controls when the button is visible.
17
+ *
18
+ * - `"auto"` (default) — shown only after the user scrolls past `threshold` pixels.
19
+ * Hides again when they scroll back above the threshold.
20
+ * - `"always"` — permanently visible.
21
+ */
22
+ visible?: ScrollToTopVisible;
23
+ /**
24
+ * Corner or edge where the button is anchored inside the viewport.
25
+ *
26
+ * Default: `"bottom-right"`.
27
+ */
28
+ position?: ScrollToTopPosition;
29
+ /**
30
+ * Number of pixels the user must scroll before the button appears.
31
+ * Only relevant when `visible="auto"`.
32
+ *
33
+ * Default: `300`.
34
+ */
35
+ threshold?: number;
36
+ /**
37
+ * Scrollable element to observe and scroll back to the top.
38
+ * Pass an `HTMLElement` when the scrollable area is a container other than
39
+ * the page (e.g. a drawer, a split-view pane, or a custom scroll region).
40
+ *
41
+ * Defaults to `window`.
42
+ */
43
+ scrollTarget?: HTMLElement | Window;
44
+ /** Additional CSS class applied to the root positioning container. */
45
+ className?: string;
46
+ /** Inline styles applied to the root positioning container. */
47
+ style?: CSSProperties;
48
+ }
49
+ /**
50
+ * Fixed-position OSD button that scrolls the page (or a container) back to the top.
51
+ *
52
+ * - Renders a circular `IconButton` with the `GoUp` icon at the chosen `position`.
53
+ * - When `visible="auto"` (default), the button appears only after the user scrolls
54
+ * past `threshold` pixels and hides again when they scroll back above it.
55
+ * - The button is semi-transparent at rest (`opacity: 0.5`) and fully opaque on
56
+ * hover or keyboard focus, so it does not obscure underlying content.
57
+ * - Clicking triggers a smooth scroll to the top via `scrollTo({ top: 0, behavior: "smooth" })`.
58
+ * - Forwards `ref` to the root `<div>` positioning container, not the inner `<button>`.
59
+ *
60
+ * @example
61
+ * // Minimal — appears after 300 px of scroll, anchored bottom-right
62
+ * <ScrollToTop />
63
+ *
64
+ * @example
65
+ * // Always visible, anchored bottom-left
66
+ * <ScrollToTop visible="always" position="bottom-left" />
67
+ *
68
+ * @example
69
+ * // Observe a custom scrollable container
70
+ * const ref = useRef<HTMLDivElement>(null);
71
+ * <div ref={ref} style={{ overflowY: 'auto', height: 400 }}>...</div>
72
+ * <ScrollToTop scrollTarget={ref.current ?? undefined} threshold={150} />
73
+ */
74
+ export declare const ScrollToTop: import('react').ForwardRefExoticComponent<ScrollToTopProps & import('react').RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,42 @@
1
+ import { IconButton as e } from "../IconButton/IconButton.js";
2
+ import t from "./ScrollToTop.module.css.js";
3
+ import { useScrollToTopVisibility as n } from "./useScrollToTopVisibility.js";
4
+ import { forwardRef as r } from "react";
5
+ import { jsx as i } from "react/jsx-runtime";
6
+ import { GoUp as a } from "@gnome-ui/icons";
7
+ //#region src/components/ScrollToTop/ScrollToTop.tsx
8
+ var o = {
9
+ "bottom-right": t.bottom_right,
10
+ "bottom-left": t.bottom_left,
11
+ "bottom-center": t.bottom_center,
12
+ "top-right": t.top_right,
13
+ "top-left": t.top_left,
14
+ "top-center": t.top_center
15
+ }, s = r(function({ visible: r = "auto", position: s = "bottom-right", threshold: c = 300, scrollTarget: l, className: u, style: d }, f) {
16
+ return n({
17
+ visible: r,
18
+ threshold: c,
19
+ scrollTarget: l
20
+ }) ? /* @__PURE__ */ i("div", {
21
+ ref: f,
22
+ className: [
23
+ t.root,
24
+ o[s],
25
+ u
26
+ ].filter(Boolean).join(" "),
27
+ style: d,
28
+ children: /* @__PURE__ */ i(e, {
29
+ icon: a,
30
+ label: "Scroll to top",
31
+ variant: "osd",
32
+ onClick: () => (l ?? window).scrollTo({
33
+ top: 0,
34
+ behavior: "smooth"
35
+ })
36
+ })
37
+ }) : null;
38
+ });
39
+ //#endregion
40
+ export { s as ScrollToTop };
41
+
42
+ //# sourceMappingURL=ScrollToTop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScrollToTop.js","names":[],"sources":["../../../src/components/ScrollToTop/ScrollToTop.tsx"],"sourcesContent":["import { GoUp } from '@gnome-ui/icons';\nimport { type CSSProperties, forwardRef } from 'react';\n\nimport { IconButton } from '../IconButton';\nimport styles from './ScrollToTop.module.css';\nimport { useScrollToTopVisibility } from './useScrollToTopVisibility';\n\n/**\n * Controls when the button is rendered.\n *\n * - `\"auto\"` — hidden until the user scrolls past `threshold` pixels (default).\n * - `\"always\"` — always rendered regardless of scroll position.\n */\nexport type ScrollToTopVisible = 'always' | 'auto';\n\n/**\n * Anchor corner or edge for the fixed-position container.\n * The button is inset `--gnome-space-4` (24 px) from each named edge.\n */\nexport type ScrollToTopPosition =\n | 'bottom-right'\n | 'bottom-left'\n | 'bottom-center'\n | 'top-right'\n | 'top-left'\n | 'top-center';\n\nexport interface ScrollToTopProps {\n /**\n * Controls when the button is visible.\n *\n * - `\"auto\"` (default) — shown only after the user scrolls past `threshold` pixels.\n * Hides again when they scroll back above the threshold.\n * - `\"always\"` — permanently visible.\n */\n visible?: ScrollToTopVisible;\n /**\n * Corner or edge where the button is anchored inside the viewport.\n *\n * Default: `\"bottom-right\"`.\n */\n position?: ScrollToTopPosition;\n /**\n * Number of pixels the user must scroll before the button appears.\n * Only relevant when `visible=\"auto\"`.\n *\n * Default: `300`.\n */\n threshold?: number;\n /**\n * Scrollable element to observe and scroll back to the top.\n * Pass an `HTMLElement` when the scrollable area is a container other than\n * the page (e.g. a drawer, a split-view pane, or a custom scroll region).\n *\n * Defaults to `window`.\n */\n scrollTarget?: HTMLElement | Window;\n /** Additional CSS class applied to the root positioning container. */\n className?: string;\n /** Inline styles applied to the root positioning container. */\n style?: CSSProperties;\n}\n\nconst POSITION_CLASS: Record<ScrollToTopPosition, string> = {\n 'bottom-right': styles.bottom_right,\n 'bottom-left': styles.bottom_left,\n 'bottom-center': styles.bottom_center,\n 'top-right': styles.top_right,\n 'top-left': styles.top_left,\n 'top-center': styles.top_center,\n};\n\n/**\n * Fixed-position OSD button that scrolls the page (or a container) back to the top.\n *\n * - Renders a circular `IconButton` with the `GoUp` icon at the chosen `position`.\n * - When `visible=\"auto\"` (default), the button appears only after the user scrolls\n * past `threshold` pixels and hides again when they scroll back above it.\n * - The button is semi-transparent at rest (`opacity: 0.5`) and fully opaque on\n * hover or keyboard focus, so it does not obscure underlying content.\n * - Clicking triggers a smooth scroll to the top via `scrollTo({ top: 0, behavior: \"smooth\" })`.\n * - Forwards `ref` to the root `<div>` positioning container, not the inner `<button>`.\n *\n * @example\n * // Minimal — appears after 300 px of scroll, anchored bottom-right\n * <ScrollToTop />\n *\n * @example\n * // Always visible, anchored bottom-left\n * <ScrollToTop visible=\"always\" position=\"bottom-left\" />\n *\n * @example\n * // Observe a custom scrollable container\n * const ref = useRef<HTMLDivElement>(null);\n * <div ref={ref} style={{ overflowY: 'auto', height: 400 }}>...</div>\n * <ScrollToTop scrollTarget={ref.current ?? undefined} threshold={150} />\n */\nexport const ScrollToTop = forwardRef<HTMLDivElement, ScrollToTopProps>(function ScrollToTop(\n { visible = 'auto', position = 'bottom-right', threshold = 300, scrollTarget, className, style },\n ref,\n) {\n const isVisible = useScrollToTopVisibility({ visible, threshold, scrollTarget });\n\n if (!isVisible) {\n return null;\n }\n\n return (\n <div\n ref={ref}\n className={[styles.root, POSITION_CLASS[position], className].filter(Boolean).join(' ')}\n style={style}\n >\n <IconButton\n icon={GoUp}\n label=\"Scroll to top\"\n variant=\"osd\"\n onClick={() => (scrollTarget ?? window).scrollTo({ top: 0, behavior: 'smooth' })}\n />\n </div>\n );\n});\n"],"mappings":";;;;;;;AA+DA,IAAM,IAAsD;CAC1D,gBAAgB,EAAO;CACvB,eAAe,EAAO;CACtB,iBAAiB,EAAO;CACxB,aAAa,EAAO;CACpB,YAAY,EAAO;CACnB,cAAc,EAAO;AACvB,GA2Ba,IAAc,EAA6C,SACtE,EAAE,aAAU,QAAQ,cAAW,gBAAgB,eAAY,KAAK,iBAAc,cAAW,YACzF,GACA;CAOA,OANkB,EAAyB;EAAE;EAAS;EAAW;CAAa,CAEzE,IAKH,kBAAC,OAAD;EACO;EACL,WAAW;GAAC,EAAO;GAAM,EAAe;GAAW;EAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;EAC/E;YAEP,kBAAC,GAAD;GACE,MAAM;GACN,OAAM;GACN,SAAQ;GACR,gBAAgB,KAAgB,QAAQ,SAAS;IAAE,KAAK;IAAG,UAAU;GAAS,CAAC;EAChF,CAAA;CACE,CAAA,IAfE;AAiBX,CAAC"}
@@ -0,0 +1,2 @@
1
+ var e={root:`_root_1qshr_1`,bottom_right:`_bottom_right_1qshr_8`,bottom_left:`_bottom_left_1qshr_13`,bottom_center:`_bottom_center_1qshr_18`,top_right:`_top_right_1qshr_24`,top_left:`_top_left_1qshr_29`,top_center:`_top_center_1qshr_34`};exports.default=e;
2
+ //# sourceMappingURL=ScrollToTop.module.css.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScrollToTop.module.css.cjs","names":[],"sources":["../../../src/components/ScrollToTop/ScrollToTop.module.css"],"sourcesContent":[".root {\n position: fixed;\n z-index: 9999;\n pointer-events: none;\n}\n\n/* Position variants — 24px edge spacing via --gnome-space-4 */\n.bottom_right {\n bottom: var(--gnome-space-4, 24px);\n right: var(--gnome-space-4, 24px);\n}\n\n.bottom_left {\n bottom: var(--gnome-space-4, 24px);\n left: var(--gnome-space-4, 24px);\n}\n\n.bottom_center {\n bottom: var(--gnome-space-4, 24px);\n left: 50%;\n transform: translateX(-50%);\n}\n\n.top_right {\n top: var(--gnome-space-4, 24px);\n right: var(--gnome-space-4, 24px);\n}\n\n.top_left {\n top: var(--gnome-space-4, 24px);\n left: var(--gnome-space-4, 24px);\n}\n\n.top_center {\n top: var(--gnome-space-4, 24px);\n left: 50%;\n transform: translateX(-50%);\n}\n\n/* Re-enable pointer events and apply hover opacity on the button */\n.root > button {\n pointer-events: auto;\n opacity: 0.5;\n transition: opacity var(--gnome-duration-fast, 100ms) var(--gnome-easing-default, ease);\n}\n\n.root > button:hover,\n.root > button:focus-visible {\n opacity: 1;\n}\n"],"mappings":""}
@@ -0,0 +1,13 @@
1
+ var e = {
2
+ root: "_root_1qshr_1",
3
+ bottom_right: "_bottom_right_1qshr_8",
4
+ bottom_left: "_bottom_left_1qshr_13",
5
+ bottom_center: "_bottom_center_1qshr_18",
6
+ top_right: "_top_right_1qshr_24",
7
+ top_left: "_top_left_1qshr_29",
8
+ top_center: "_top_center_1qshr_34"
9
+ };
10
+ //#endregion
11
+ export { e as default };
12
+
13
+ //# sourceMappingURL=ScrollToTop.module.css.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScrollToTop.module.css.js","names":[],"sources":["../../../src/components/ScrollToTop/ScrollToTop.module.css"],"sourcesContent":[".root {\n position: fixed;\n z-index: 9999;\n pointer-events: none;\n}\n\n/* Position variants — 24px edge spacing via --gnome-space-4 */\n.bottom_right {\n bottom: var(--gnome-space-4, 24px);\n right: var(--gnome-space-4, 24px);\n}\n\n.bottom_left {\n bottom: var(--gnome-space-4, 24px);\n left: var(--gnome-space-4, 24px);\n}\n\n.bottom_center {\n bottom: var(--gnome-space-4, 24px);\n left: 50%;\n transform: translateX(-50%);\n}\n\n.top_right {\n top: var(--gnome-space-4, 24px);\n right: var(--gnome-space-4, 24px);\n}\n\n.top_left {\n top: var(--gnome-space-4, 24px);\n left: var(--gnome-space-4, 24px);\n}\n\n.top_center {\n top: var(--gnome-space-4, 24px);\n left: 50%;\n transform: translateX(-50%);\n}\n\n/* Re-enable pointer events and apply hover opacity on the button */\n.root > button {\n pointer-events: auto;\n opacity: 0.5;\n transition: opacity var(--gnome-duration-fast, 100ms) var(--gnome-easing-default, ease);\n}\n\n.root > button:hover,\n.root > button:focus-visible {\n opacity: 1;\n}\n"],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export type { ScrollToTopPosition, ScrollToTopProps, ScrollToTopVisible } from './ScrollToTop';
2
+ export { ScrollToTop } from './ScrollToTop';
@@ -0,0 +1,2 @@
1
+ let e=require("react");function t({visible:t,threshold:n,scrollTarget:r}){let[i,a]=(0,e.useState)(!1);return(0,e.useEffect)(()=>{if(t===`always`)return;let e=r??window,i=()=>e===window?window.scrollY:e.scrollTop,o=()=>a(i()>n);return o(),e.addEventListener(`scroll`,o,{passive:!0}),()=>e.removeEventListener(`scroll`,o)},[t,n,r]),t===`always`||i}exports.useScrollToTopVisibility=t;
2
+ //# sourceMappingURL=useScrollToTopVisibility.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useScrollToTopVisibility.cjs","names":[],"sources":["../../../src/components/ScrollToTop/useScrollToTopVisibility.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\nimport type { ScrollToTopVisible } from './ScrollToTop';\n\ninterface UseScrollToTopVisibilityOptions {\n /** See {@link ScrollToTopVisible}. */\n visible: ScrollToTopVisible;\n /** Pixels scrolled before returning `true` in `\"auto\"` mode. */\n threshold: number;\n /** Element to observe. Defaults to `window` when `undefined`. */\n scrollTarget?: HTMLElement | Window;\n}\n\n/**\n * Returns `true` when the `ScrollToTop` button should be rendered.\n *\n * - `visible === \"always\"` → always `true`; no scroll listener is attached.\n * - `visible === \"auto\"` → attaches a passive `scroll` listener on `scrollTarget`\n * (or `window`) and returns `true` once the scroll offset exceeds `threshold`.\n * Reverts to `false` when the user scrolls back below the threshold.\n *\n * This is an internal hook — not exported from the package.\n */\nexport function useScrollToTopVisibility({\n visible,\n threshold,\n scrollTarget,\n}: UseScrollToTopVisibilityOptions): boolean {\n const [isScrolledPast, setIsScrolledPast] = useState(false);\n\n useEffect(() => {\n if (visible === 'always') {\n return;\n }\n\n const target = scrollTarget ?? window;\n\n const getScrollY = () =>\n target === window ? window.scrollY : (target as HTMLElement).scrollTop;\n\n const check = () => setIsScrolledPast(getScrollY() > threshold);\n\n check();\n target.addEventListener('scroll', check, { passive: true });\n return () => target.removeEventListener('scroll', check);\n }, [visible, threshold, scrollTarget]);\n\n return visible === 'always' || isScrolledPast;\n}\n"],"mappings":"uBAuBA,SAAgB,EAAyB,CACvC,UACA,YACA,gBAC2C,CAC3C,GAAM,CAAC,EAAgB,IAAA,EAAA,EAAA,UAA8B,EAAK,EAmB1D,OAjBA,EAAA,EAAA,eAAgB,CACd,GAAI,IAAY,SACd,OAGF,IAAM,EAAS,GAAgB,OAEzB,MACJ,IAAW,OAAS,OAAO,QAAW,EAAuB,UAEzD,MAAc,EAAkB,EAAW,EAAI,CAAS,EAI9D,OAFA,EAAM,EACN,EAAO,iBAAiB,SAAU,EAAO,CAAE,QAAS,EAAK,CAAC,MAC7C,EAAO,oBAAoB,SAAU,CAAK,CACzD,EAAG,CAAC,EAAS,EAAW,CAAY,CAAC,EAE9B,IAAY,UAAY,CACjC"}
@@ -0,0 +1,21 @@
1
+ import { ScrollToTopVisible } from './ScrollToTop';
2
+ interface UseScrollToTopVisibilityOptions {
3
+ /** See {@link ScrollToTopVisible}. */
4
+ visible: ScrollToTopVisible;
5
+ /** Pixels scrolled before returning `true` in `"auto"` mode. */
6
+ threshold: number;
7
+ /** Element to observe. Defaults to `window` when `undefined`. */
8
+ scrollTarget?: HTMLElement | Window;
9
+ }
10
+ /**
11
+ * Returns `true` when the `ScrollToTop` button should be rendered.
12
+ *
13
+ * - `visible === "always"` → always `true`; no scroll listener is attached.
14
+ * - `visible === "auto"` → attaches a passive `scroll` listener on `scrollTarget`
15
+ * (or `window`) and returns `true` once the scroll offset exceeds `threshold`.
16
+ * Reverts to `false` when the user scrolls back below the threshold.
17
+ *
18
+ * This is an internal hook — not exported from the package.
19
+ */
20
+ export declare function useScrollToTopVisibility({ visible, threshold, scrollTarget, }: UseScrollToTopVisibilityOptions): boolean;
21
+ export {};