@gnome-ui/react 1.40.1 → 1.41.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/README.md +42 -0
- package/dist/components/AvatarRotator/AvatarRotator.cjs +2 -0
- package/dist/components/AvatarRotator/AvatarRotator.cjs.map +1 -0
- package/dist/components/AvatarRotator/AvatarRotator.d.ts +36 -0
- package/dist/components/AvatarRotator/AvatarRotator.js +84 -0
- package/dist/components/AvatarRotator/AvatarRotator.js.map +1 -0
- package/dist/components/AvatarRotator/AvatarRotator.module.css.cjs +2 -0
- package/dist/components/AvatarRotator/AvatarRotator.module.css.cjs.map +1 -0
- package/dist/components/AvatarRotator/AvatarRotator.module.css.js +13 -0
- package/dist/components/AvatarRotator/AvatarRotator.module.css.js.map +1 -0
- package/dist/components/AvatarRotator/index.d.ts +2 -0
- package/dist/components/AvatarRotator.cjs +1 -0
- package/dist/components/AvatarRotator.d.ts +2 -0
- package/dist/components/AvatarRotator.js +2 -0
- package/dist/components/Chip/Chip.cjs +1 -1
- package/dist/components/Chip/Chip.cjs.map +1 -1
- package/dist/components/Chip/Chip.js +19 -19
- package/dist/components/ContributionGraph/ContributionGraph.cjs +1 -1
- package/dist/components/ContributionGraph/ContributionGraph.cjs.map +1 -1
- package/dist/components/ContributionGraph/ContributionGraph.js +108 -117
- package/dist/components/ContributionGraph/ContributionGraph.js.map +1 -1
- package/dist/components/CountDownTimer/CountDownTimer.cjs +1 -1
- package/dist/components/CountDownTimer/CountDownTimer.cjs.map +1 -1
- package/dist/components/CountDownTimer/CountDownTimer.js +37 -28
- package/dist/components/CountDownTimer/CountDownTimer.js.map +1 -1
- package/dist/components/Dropdown/Dropdown.cjs +1 -1
- package/dist/components/Dropdown/Dropdown.cjs.map +1 -1
- package/dist/components/Dropdown/Dropdown.js +45 -46
- package/dist/components/Dropdown/Dropdown.js.map +1 -1
- package/dist/components/GnomeProvider/GnomeContext.cjs +2 -0
- package/dist/components/GnomeProvider/GnomeContext.cjs.map +1 -0
- package/dist/components/GnomeProvider/GnomeContext.d.ts +16 -0
- package/dist/components/GnomeProvider/GnomeContext.js +40 -0
- package/dist/components/GnomeProvider/GnomeContext.js.map +1 -0
- package/dist/components/GnomeProvider/GnomeProvider.cjs +2 -0
- package/dist/components/GnomeProvider/GnomeProvider.cjs.map +1 -0
- package/dist/components/GnomeProvider/GnomeProvider.d.ts +24 -0
- package/dist/components/GnomeProvider/GnomeProvider.js +25 -0
- package/dist/components/GnomeProvider/GnomeProvider.js.map +1 -0
- package/dist/components/PasswordEntryRow/PasswordEntryRow.cjs +1 -1
- package/dist/components/PasswordEntryRow/PasswordEntryRow.cjs.map +1 -1
- package/dist/components/PasswordEntryRow/PasswordEntryRow.js +11 -12
- package/dist/components/PasswordEntryRow/PasswordEntryRow.js.map +1 -1
- package/dist/components/SearchBar/SearchBar.cjs +1 -1
- package/dist/components/SearchBar/SearchBar.cjs.map +1 -1
- package/dist/components/SearchBar/SearchBar.js +38 -39
- package/dist/components/SearchBar/SearchBar.js.map +1 -1
- package/dist/components/Sidebar/SidebarSection.cjs +1 -1
- package/dist/components/Sidebar/SidebarSection.cjs.map +1 -1
- package/dist/components/Sidebar/SidebarSection.js +41 -42
- package/dist/components/Sidebar/SidebarSection.js.map +1 -1
- package/dist/components/Toast/Toast.cjs +1 -1
- package/dist/components/Toast/Toast.cjs.map +1 -1
- package/dist/components/Toast/Toast.js +18 -18
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks.cjs +1 -1
- package/dist/hooks.js +3 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +90 -87
- package/dist/style.css +1 -1
- package/package.json +16 -1
- package/dist/icons/dist/icons/Check.cjs +0 -2
- package/dist/icons/dist/icons/Check.cjs.map +0 -1
- package/dist/icons/dist/icons/Check.js +0 -12
- package/dist/icons/dist/icons/Check.js.map +0 -1
- package/dist/icons/dist/icons/Close.cjs +0 -2
- package/dist/icons/dist/icons/Close.cjs.map +0 -1
- package/dist/icons/dist/icons/Close.js +0 -12
- package/dist/icons/dist/icons/Close.js.map +0 -1
- package/dist/icons/dist/icons/PanDown.cjs +0 -2
- package/dist/icons/dist/icons/PanDown.cjs.map +0 -1
- package/dist/icons/dist/icons/PanDown.js +0 -12
- package/dist/icons/dist/icons/PanDown.js.map +0 -1
- package/dist/icons/dist/icons/PanUp.cjs +0 -2
- package/dist/icons/dist/icons/PanUp.cjs.map +0 -1
- package/dist/icons/dist/icons/PanUp.js +0 -12
- package/dist/icons/dist/icons/PanUp.js.map +0 -1
- package/dist/icons/dist/icons/Search.cjs +0 -2
- package/dist/icons/dist/icons/Search.cjs.map +0 -1
- package/dist/icons/dist/icons/Search.js +0 -12
- package/dist/icons/dist/icons/Search.js.map +0 -1
- package/dist/icons/dist/icons/ViewConceal.cjs +0 -2
- package/dist/icons/dist/icons/ViewConceal.cjs.map +0 -1
- package/dist/icons/dist/icons/ViewConceal.js +0 -12
- package/dist/icons/dist/icons/ViewConceal.js.map +0 -1
- package/dist/icons/dist/icons/ViewReveal.cjs +0 -2
- package/dist/icons/dist/icons/ViewReveal.cjs.map +0 -1
- package/dist/icons/dist/icons/ViewReveal.js +0 -12
- package/dist/icons/dist/icons/ViewReveal.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SearchBar.cjs","names":[],"sources":["../../../src/components/SearchBar/SearchBar.tsx"],"sourcesContent":["import {\n useRef,\n useEffect,\n useCallback,\n useId,\n useState,\n type KeyboardEvent,\n type InputHTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Search, Close } from \"@gnome-ui/icons\";\nimport { Icon } from \"../Icon\";\nimport { Spinner } from \"../Spinner\";\nimport styles from \"./SearchBar.module.css\";\n\nexport interface Suggestion {\n id: string;\n label: string;\n}\n\nexport interface SearchBarProps\n extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\"> {\n /**\n * Whether the search bar is visible/expanded.\n * When `false` the bar collapses to zero height and is hidden from AT.\n */\n open: boolean;\n /** Called when the user presses Escape or clicks the clear button with empty value. */\n onClose?: () => void;\n /** Called when the clear (×) button is clicked. Also clears the input value. */\n onClear?: () => void;\n /**\n * Content to render below the search bar (e.g. filter chips).\n * Only visible while `open` is true.\n */\n children?: ReactNode;\n /**\n * Removes the header-bar background so the bar blends into any surface.\n * Mirrors the `.inline` style class.\n */\n inline?: boolean;\n /** Suggestion items to show in the autocomplete popover. */\n suggestions?: Suggestion[];\n /** Called when the user selects a suggestion. */\n onSuggestionSelect?: (item: Suggestion) => void;\n /** Shows a spinner inside the suggestions popover while fetching async results. */\n loadingSuggestions?: boolean;\n /** Custom row renderer for each suggestion. */\n renderSuggestion?: (item: Suggestion) => ReactNode;\n /** `aria-label` for the suggestions listbox. Defaults to `\"Suggestions\"`. */\n suggestionsLabel?: string;\n}\n\ninterface DropdownPos {\n top: number;\n left: number;\n width: number;\n}\n\n/**\n * Collapsible search bar following the Adwaita `AdwSearchBar` pattern.\n *\n * - Slides in/out with a CSS height transition.\n * - Auto-focuses the input when `open` becomes `true`.\n * - Escape key calls `onClose`.\n * - Shows a clear (×) button whenever the input has a value.\n * - Optional `suggestions` / `loadingSuggestions` props show an autocomplete\n * popover anchored below the bar — follows the GTK4 pattern of\n * `AdwSearchBar` + `GtkPopover` + `GtkListView` (replaces `GtkEntryCompletion`).\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.SearchBar.html\n */\nexport function SearchBar({\n open,\n onClose,\n onClear,\n children,\n value,\n onChange,\n placeholder = \"Search…\",\n disabled,\n inline = false,\n className,\n suggestions,\n onSuggestionSelect,\n loadingSuggestions = false,\n renderSuggestion,\n suggestionsLabel = \"Suggestions\",\n ...props\n}: SearchBarProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const wrapperRef = useRef<HTMLDivElement>(null);\n const listboxRef = useRef<HTMLUListElement>(null);\n\n const listboxId = useId();\n const optionIdPrefix = useId();\n\n const [activeIndex, setActiveIndex] = useState(-1);\n const [dropdownPos, setDropdownPos] = useState<DropdownPos | null>(null);\n\n const hasSuggestions = (suggestions?.length ?? 0) > 0;\n const showDropdown = open && (loadingSuggestions || hasSuggestions);\n\n // Reset active index whenever the suggestion list changes\n useEffect(() => { setActiveIndex(-1); }, [suggestions]);\n\n // Auto-focus when opening\n useEffect(() => {\n if (open) {\n const id = requestAnimationFrame(() => inputRef.current?.focus());\n return () => cancelAnimationFrame(id);\n }\n }, [open]);\n\n // Position the suggestions dropdown below the wrapper\n useEffect(() => {\n if (!showDropdown || !wrapperRef.current) {\n setDropdownPos(null);\n return;\n }\n function updatePos() {\n if (!wrapperRef.current) return;\n const r = wrapperRef.current.getBoundingClientRect();\n setDropdownPos({ top: r.bottom, left: r.left, width: r.width });\n }\n updatePos();\n window.addEventListener(\"scroll\", updatePos, { passive: true, capture: true });\n window.addEventListener(\"resize\", updatePos, { passive: true });\n return () => {\n window.removeEventListener(\"scroll\", updatePos, { capture: true });\n window.removeEventListener(\"resize\", updatePos);\n };\n }, [showDropdown]);\n\n // Scroll the active option into view\n useEffect(() => {\n if (activeIndex < 0 || !listboxRef.current) return;\n (listboxRef.current.children[activeIndex] as HTMLElement | undefined)\n ?.scrollIntoView({ block: \"nearest\" });\n }, [activeIndex]);\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent<HTMLInputElement>) => {\n if (showDropdown && hasSuggestions && suggestions) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActiveIndex((i) => Math.min(i + 1, suggestions.length - 1));\n return;\n }\n if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActiveIndex((i) => Math.max(i - 1, -1));\n return;\n }\n if (e.key === \"Enter\" && activeIndex >= 0) {\n e.preventDefault();\n onSuggestionSelect?.(suggestions[activeIndex]);\n return;\n }\n }\n if (e.key === \"Escape\") {\n e.preventDefault();\n onClose?.();\n }\n props.onKeyDown?.(e);\n },\n [onClose, props, showDropdown, hasSuggestions, suggestions, activeIndex, onSuggestionSelect]\n );\n\n const hasValue = value !== undefined && value !== \"\";\n const activeOptionId = activeIndex >= 0 && suggestions?.[activeIndex]\n ? `${optionIdPrefix}-${suggestions[activeIndex].id}`\n : undefined;\n\n const dropdown = showDropdown && dropdownPos\n ? createPortal(\n <ul\n ref={listboxRef}\n role=\"listbox\"\n aria-label={suggestionsLabel}\n id={listboxId}\n className={styles.suggestionsPanel}\n style={{ top: dropdownPos.top, left: dropdownPos.left, width: dropdownPos.width }}\n onMouseDown={(e) => e.preventDefault()} // keep focus on input\n >\n {loadingSuggestions && (\n <li role=\"presentation\" className={styles.suggestionsLoading}>\n <Spinner size=\"sm\" label=\"Loading suggestions…\" />\n </li>\n )}\n {!loadingSuggestions &&\n suggestions?.map((item, index) => (\n <li\n key={item.id}\n id={`${optionIdPrefix}-${item.id}`}\n role=\"option\"\n aria-selected={index === activeIndex}\n className={[\n styles.suggestionItem,\n index === activeIndex ? styles.suggestionItemActive : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseLeave={() => setActiveIndex(-1)}\n onClick={() => onSuggestionSelect?.(item)}\n >\n {renderSuggestion ? renderSuggestion(item) : item.label}\n </li>\n ))}\n </ul>,\n document.body\n )\n : null;\n\n return (\n <>\n <div\n ref={wrapperRef}\n className={[styles.wrapper, open ? styles.open : null, className]\n .filter(Boolean)\n .join(\" \")}\n aria-hidden={!open}\n >\n <div\n className={[\n styles.bar,\n inline ? styles.inline : null,\n disabled ? styles.disabled : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n <span className={styles.searchIcon} aria-hidden>\n <Icon icon={Search} size=\"md\" />\n </span>\n\n <input\n ref={inputRef}\n type=\"search\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={showDropdown}\n aria-controls={showDropdown ? listboxId : undefined}\n aria-activedescendant={activeOptionId}\n value={value}\n onChange={onChange}\n placeholder={placeholder}\n disabled={disabled}\n tabIndex={open ? undefined : -1}\n className={styles.input}\n onKeyDown={handleKeyDown}\n {...props}\n />\n\n {hasValue && (\n <button\n type=\"button\"\n className={styles.clearButton}\n aria-label=\"Clear search\"\n tabIndex={open ? undefined : -1}\n disabled={disabled}\n onClick={() => {\n onClear?.();\n inputRef.current?.focus();\n }}\n >\n <Icon icon={Close} size=\"md\" />\n </button>\n )}\n </div>\n\n {children && <div className={styles.filterRow}>{children}</div>}\n </div>\n\n {typeof document !== \"undefined\" && dropdown}\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SearchBar.cjs","names":[],"sources":["../../../src/components/SearchBar/SearchBar.tsx"],"sourcesContent":["import {\n useRef,\n useEffect,\n useCallback,\n useId,\n useState,\n type KeyboardEvent,\n type InputHTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Search, Close } from \"@gnome-ui/icons\";\nimport { Icon } from \"../Icon\";\nimport { Spinner } from \"../Spinner\";\nimport styles from \"./SearchBar.module.css\";\n\nexport interface Suggestion {\n id: string;\n label: string;\n}\n\nexport interface SearchBarProps\n extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\"> {\n /**\n * Whether the search bar is visible/expanded.\n * When `false` the bar collapses to zero height and is hidden from AT.\n */\n open: boolean;\n /** Called when the user presses Escape or clicks the clear button with empty value. */\n onClose?: () => void;\n /** Called when the clear (×) button is clicked. Also clears the input value. */\n onClear?: () => void;\n /**\n * Content to render below the search bar (e.g. filter chips).\n * Only visible while `open` is true.\n */\n children?: ReactNode;\n /**\n * Removes the header-bar background so the bar blends into any surface.\n * Mirrors the `.inline` style class.\n */\n inline?: boolean;\n /** Suggestion items to show in the autocomplete popover. */\n suggestions?: Suggestion[];\n /** Called when the user selects a suggestion. */\n onSuggestionSelect?: (item: Suggestion) => void;\n /** Shows a spinner inside the suggestions popover while fetching async results. */\n loadingSuggestions?: boolean;\n /** Custom row renderer for each suggestion. */\n renderSuggestion?: (item: Suggestion) => ReactNode;\n /** `aria-label` for the suggestions listbox. Defaults to `\"Suggestions\"`. */\n suggestionsLabel?: string;\n}\n\ninterface DropdownPos {\n top: number;\n left: number;\n width: number;\n}\n\n/**\n * Collapsible search bar following the Adwaita `AdwSearchBar` pattern.\n *\n * - Slides in/out with a CSS height transition.\n * - Auto-focuses the input when `open` becomes `true`.\n * - Escape key calls `onClose`.\n * - Shows a clear (×) button whenever the input has a value.\n * - Optional `suggestions` / `loadingSuggestions` props show an autocomplete\n * popover anchored below the bar — follows the GTK4 pattern of\n * `AdwSearchBar` + `GtkPopover` + `GtkListView` (replaces `GtkEntryCompletion`).\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.SearchBar.html\n */\nexport function SearchBar({\n open,\n onClose,\n onClear,\n children,\n value,\n onChange,\n placeholder = \"Search…\",\n disabled,\n inline = false,\n className,\n suggestions,\n onSuggestionSelect,\n loadingSuggestions = false,\n renderSuggestion,\n suggestionsLabel = \"Suggestions\",\n ...props\n}: SearchBarProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const wrapperRef = useRef<HTMLDivElement>(null);\n const listboxRef = useRef<HTMLUListElement>(null);\n\n const listboxId = useId();\n const optionIdPrefix = useId();\n\n const [activeIndex, setActiveIndex] = useState(-1);\n const [dropdownPos, setDropdownPos] = useState<DropdownPos | null>(null);\n\n const hasSuggestions = (suggestions?.length ?? 0) > 0;\n const showDropdown = open && (loadingSuggestions || hasSuggestions);\n\n // Reset active index whenever the suggestion list changes\n useEffect(() => { setActiveIndex(-1); }, [suggestions]);\n\n // Auto-focus when opening\n useEffect(() => {\n if (open) {\n const id = requestAnimationFrame(() => inputRef.current?.focus());\n return () => cancelAnimationFrame(id);\n }\n }, [open]);\n\n // Position the suggestions dropdown below the wrapper\n useEffect(() => {\n if (!showDropdown || !wrapperRef.current) {\n setDropdownPos(null);\n return;\n }\n function updatePos() {\n if (!wrapperRef.current) return;\n const r = wrapperRef.current.getBoundingClientRect();\n setDropdownPos({ top: r.bottom, left: r.left, width: r.width });\n }\n updatePos();\n window.addEventListener(\"scroll\", updatePos, { passive: true, capture: true });\n window.addEventListener(\"resize\", updatePos, { passive: true });\n return () => {\n window.removeEventListener(\"scroll\", updatePos, { capture: true });\n window.removeEventListener(\"resize\", updatePos);\n };\n }, [showDropdown]);\n\n // Scroll the active option into view\n useEffect(() => {\n if (activeIndex < 0 || !listboxRef.current) return;\n (listboxRef.current.children[activeIndex] as HTMLElement | undefined)\n ?.scrollIntoView({ block: \"nearest\" });\n }, [activeIndex]);\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent<HTMLInputElement>) => {\n if (showDropdown && hasSuggestions && suggestions) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActiveIndex((i) => Math.min(i + 1, suggestions.length - 1));\n return;\n }\n if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActiveIndex((i) => Math.max(i - 1, -1));\n return;\n }\n if (e.key === \"Enter\" && activeIndex >= 0) {\n e.preventDefault();\n onSuggestionSelect?.(suggestions[activeIndex]);\n return;\n }\n }\n if (e.key === \"Escape\") {\n e.preventDefault();\n onClose?.();\n }\n props.onKeyDown?.(e);\n },\n [onClose, props, showDropdown, hasSuggestions, suggestions, activeIndex, onSuggestionSelect]\n );\n\n const hasValue = value !== undefined && value !== \"\";\n const activeOptionId = activeIndex >= 0 && suggestions?.[activeIndex]\n ? `${optionIdPrefix}-${suggestions[activeIndex].id}`\n : undefined;\n\n const dropdown = showDropdown && dropdownPos\n ? createPortal(\n <ul\n ref={listboxRef}\n role=\"listbox\"\n aria-label={suggestionsLabel}\n id={listboxId}\n className={styles.suggestionsPanel}\n style={{ top: dropdownPos.top, left: dropdownPos.left, width: dropdownPos.width }}\n onMouseDown={(e) => e.preventDefault()} // keep focus on input\n >\n {loadingSuggestions && (\n <li role=\"presentation\" className={styles.suggestionsLoading}>\n <Spinner size=\"sm\" label=\"Loading suggestions…\" />\n </li>\n )}\n {!loadingSuggestions &&\n suggestions?.map((item, index) => (\n <li\n key={item.id}\n id={`${optionIdPrefix}-${item.id}`}\n role=\"option\"\n aria-selected={index === activeIndex}\n className={[\n styles.suggestionItem,\n index === activeIndex ? styles.suggestionItemActive : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseLeave={() => setActiveIndex(-1)}\n onClick={() => onSuggestionSelect?.(item)}\n >\n {renderSuggestion ? renderSuggestion(item) : item.label}\n </li>\n ))}\n </ul>,\n document.body\n )\n : null;\n\n return (\n <>\n <div\n ref={wrapperRef}\n className={[styles.wrapper, open ? styles.open : null, className]\n .filter(Boolean)\n .join(\" \")}\n aria-hidden={!open}\n >\n <div\n className={[\n styles.bar,\n inline ? styles.inline : null,\n disabled ? styles.disabled : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n <span className={styles.searchIcon} aria-hidden>\n <Icon icon={Search} size=\"md\" />\n </span>\n\n <input\n ref={inputRef}\n type=\"search\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={showDropdown}\n aria-controls={showDropdown ? listboxId : undefined}\n aria-activedescendant={activeOptionId}\n value={value}\n onChange={onChange}\n placeholder={placeholder}\n disabled={disabled}\n tabIndex={open ? undefined : -1}\n className={styles.input}\n onKeyDown={handleKeyDown}\n {...props}\n />\n\n {hasValue && (\n <button\n type=\"button\"\n className={styles.clearButton}\n aria-label=\"Clear search\"\n tabIndex={open ? undefined : -1}\n disabled={disabled}\n onClick={() => {\n onClear?.();\n inputRef.current?.focus();\n }}\n >\n <Icon icon={Close} size=\"md\" />\n </button>\n )}\n </div>\n\n {children && <div className={styles.filterRow}>{children}</div>}\n </div>\n\n {typeof document !== \"undefined\" && dropdown}\n </>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import { Icon as e } from "../Icon/Icon.js";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { Fragment as d, jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
2
|
+
import { Spinner as t } from "../Spinner/Spinner.js";
|
|
3
|
+
import n from "./SearchBar.module.css.js";
|
|
4
|
+
import { useCallback as r, useEffect as i, useId as a, useRef as o, useState as s } from "react";
|
|
5
|
+
import { createPortal as c } from "react-dom";
|
|
6
|
+
import { Fragment as l, jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
7
|
+
import { Close as f, Search as p } from "@gnome-ui/icons";
|
|
9
8
|
//#region src/components/SearchBar/SearchBar.tsx
|
|
10
9
|
function m({ open: m, onClose: h, onClear: g, children: _, value: v, onChange: y, placeholder: b = "Search…", disabled: x, inline: S = !1, className: C, suggestions: w, onSuggestionSelect: T, loadingSuggestions: E = !1, renderSuggestion: D, suggestionsLabel: O = "Suggestions", ...k }) {
|
|
11
|
-
let A =
|
|
12
|
-
|
|
10
|
+
let A = o(null), j = o(null), M = o(null), N = a(), P = a(), [F, I] = s(-1), [L, R] = s(null), z = (w?.length ?? 0) > 0, B = m && (E || z);
|
|
11
|
+
i(() => {
|
|
13
12
|
I(-1);
|
|
14
|
-
}, [w]),
|
|
13
|
+
}, [w]), i(() => {
|
|
15
14
|
if (m) {
|
|
16
15
|
let e = requestAnimationFrame(() => A.current?.focus());
|
|
17
16
|
return () => cancelAnimationFrame(e);
|
|
18
17
|
}
|
|
19
|
-
}, [m]),
|
|
18
|
+
}, [m]), i(() => {
|
|
20
19
|
if (!B || !j.current) {
|
|
21
20
|
R(null);
|
|
22
21
|
return;
|
|
@@ -36,10 +35,10 @@ function m({ open: m, onClose: h, onClear: g, children: _, value: v, onChange: y
|
|
|
36
35
|
}), window.addEventListener("resize", e, { passive: !0 }), () => {
|
|
37
36
|
window.removeEventListener("scroll", e, { capture: !0 }), window.removeEventListener("resize", e);
|
|
38
37
|
};
|
|
39
|
-
}, [B]),
|
|
38
|
+
}, [B]), i(() => {
|
|
40
39
|
F < 0 || !M.current || M.current.children[F]?.scrollIntoView({ block: "nearest" });
|
|
41
40
|
}, [F]);
|
|
42
|
-
let V =
|
|
41
|
+
let V = r((e) => {
|
|
43
42
|
if (B && z && w) {
|
|
44
43
|
if (e.key === "ArrowDown") {
|
|
45
44
|
e.preventDefault(), I((e) => Math.min(e + 1, w.length - 1));
|
|
@@ -63,60 +62,60 @@ function m({ open: m, onClose: h, onClear: g, children: _, value: v, onChange: y
|
|
|
63
62
|
w,
|
|
64
63
|
F,
|
|
65
64
|
T
|
|
66
|
-
]), H = v !== void 0 && v !== "", U = F >= 0 && w?.[F] ? `${P}-${w[F].id}` : void 0, W = B && L ?
|
|
65
|
+
]), H = v !== void 0 && v !== "", U = F >= 0 && w?.[F] ? `${P}-${w[F].id}` : void 0, W = B && L ? c(/* @__PURE__ */ d("ul", {
|
|
67
66
|
ref: M,
|
|
68
67
|
role: "listbox",
|
|
69
68
|
"aria-label": O,
|
|
70
69
|
id: N,
|
|
71
|
-
className:
|
|
70
|
+
className: n.suggestionsPanel,
|
|
72
71
|
style: {
|
|
73
72
|
top: L.top,
|
|
74
73
|
left: L.left,
|
|
75
74
|
width: L.width
|
|
76
75
|
},
|
|
77
76
|
onMouseDown: (e) => e.preventDefault(),
|
|
78
|
-
children: [E && /* @__PURE__ */
|
|
77
|
+
children: [E && /* @__PURE__ */ u("li", {
|
|
79
78
|
role: "presentation",
|
|
80
|
-
className:
|
|
81
|
-
children: /* @__PURE__ */
|
|
79
|
+
className: n.suggestionsLoading,
|
|
80
|
+
children: /* @__PURE__ */ u(t, {
|
|
82
81
|
size: "sm",
|
|
83
82
|
label: "Loading suggestions…"
|
|
84
83
|
})
|
|
85
|
-
}), !E && w?.map((e, t) => /* @__PURE__ */
|
|
84
|
+
}), !E && w?.map((e, t) => /* @__PURE__ */ u("li", {
|
|
86
85
|
id: `${P}-${e.id}`,
|
|
87
86
|
role: "option",
|
|
88
87
|
"aria-selected": t === F,
|
|
89
|
-
className: [
|
|
88
|
+
className: [n.suggestionItem, t === F ? n.suggestionItemActive : null].filter(Boolean).join(" "),
|
|
90
89
|
onMouseEnter: () => I(t),
|
|
91
90
|
onMouseLeave: () => I(-1),
|
|
92
91
|
onClick: () => T?.(e),
|
|
93
92
|
children: D ? D(e) : e.label
|
|
94
93
|
}, e.id))]
|
|
95
94
|
}), document.body) : null;
|
|
96
|
-
return /* @__PURE__ */
|
|
95
|
+
return /* @__PURE__ */ d(l, { children: [/* @__PURE__ */ d("div", {
|
|
97
96
|
ref: j,
|
|
98
97
|
className: [
|
|
99
|
-
|
|
100
|
-
m ?
|
|
98
|
+
n.wrapper,
|
|
99
|
+
m ? n.open : null,
|
|
101
100
|
C
|
|
102
101
|
].filter(Boolean).join(" "),
|
|
103
102
|
"aria-hidden": !m,
|
|
104
|
-
children: [/* @__PURE__ */
|
|
103
|
+
children: [/* @__PURE__ */ d("div", {
|
|
105
104
|
className: [
|
|
106
|
-
|
|
107
|
-
S ?
|
|
108
|
-
x ?
|
|
105
|
+
n.bar,
|
|
106
|
+
S ? n.inline : null,
|
|
107
|
+
x ? n.disabled : null
|
|
109
108
|
].filter(Boolean).join(" "),
|
|
110
109
|
children: [
|
|
111
|
-
/* @__PURE__ */
|
|
112
|
-
className:
|
|
110
|
+
/* @__PURE__ */ u("span", {
|
|
111
|
+
className: n.searchIcon,
|
|
113
112
|
"aria-hidden": !0,
|
|
114
|
-
children: /* @__PURE__ */
|
|
115
|
-
icon:
|
|
113
|
+
children: /* @__PURE__ */ u(e, {
|
|
114
|
+
icon: p,
|
|
116
115
|
size: "md"
|
|
117
116
|
})
|
|
118
117
|
}),
|
|
119
|
-
/* @__PURE__ */
|
|
118
|
+
/* @__PURE__ */ u("input", {
|
|
120
119
|
ref: A,
|
|
121
120
|
type: "search",
|
|
122
121
|
role: "combobox",
|
|
@@ -129,27 +128,27 @@ function m({ open: m, onClose: h, onClear: g, children: _, value: v, onChange: y
|
|
|
129
128
|
placeholder: b,
|
|
130
129
|
disabled: x,
|
|
131
130
|
tabIndex: m ? void 0 : -1,
|
|
132
|
-
className:
|
|
131
|
+
className: n.input,
|
|
133
132
|
onKeyDown: V,
|
|
134
133
|
...k
|
|
135
134
|
}),
|
|
136
|
-
H && /* @__PURE__ */
|
|
135
|
+
H && /* @__PURE__ */ u("button", {
|
|
137
136
|
type: "button",
|
|
138
|
-
className:
|
|
137
|
+
className: n.clearButton,
|
|
139
138
|
"aria-label": "Clear search",
|
|
140
139
|
tabIndex: m ? void 0 : -1,
|
|
141
140
|
disabled: x,
|
|
142
141
|
onClick: () => {
|
|
143
142
|
g?.(), A.current?.focus();
|
|
144
143
|
},
|
|
145
|
-
children: /* @__PURE__ */
|
|
146
|
-
icon:
|
|
144
|
+
children: /* @__PURE__ */ u(e, {
|
|
145
|
+
icon: f,
|
|
147
146
|
size: "md"
|
|
148
147
|
})
|
|
149
148
|
})
|
|
150
149
|
]
|
|
151
|
-
}), _ && /* @__PURE__ */
|
|
152
|
-
className:
|
|
150
|
+
}), _ && /* @__PURE__ */ u("div", {
|
|
151
|
+
className: n.filterRow,
|
|
153
152
|
children: _
|
|
154
153
|
})]
|
|
155
154
|
}), typeof document < "u" && W] });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SearchBar.js","names":[],"sources":["../../../src/components/SearchBar/SearchBar.tsx"],"sourcesContent":["import {\n useRef,\n useEffect,\n useCallback,\n useId,\n useState,\n type KeyboardEvent,\n type InputHTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Search, Close } from \"@gnome-ui/icons\";\nimport { Icon } from \"../Icon\";\nimport { Spinner } from \"../Spinner\";\nimport styles from \"./SearchBar.module.css\";\n\nexport interface Suggestion {\n id: string;\n label: string;\n}\n\nexport interface SearchBarProps\n extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\"> {\n /**\n * Whether the search bar is visible/expanded.\n * When `false` the bar collapses to zero height and is hidden from AT.\n */\n open: boolean;\n /** Called when the user presses Escape or clicks the clear button with empty value. */\n onClose?: () => void;\n /** Called when the clear (×) button is clicked. Also clears the input value. */\n onClear?: () => void;\n /**\n * Content to render below the search bar (e.g. filter chips).\n * Only visible while `open` is true.\n */\n children?: ReactNode;\n /**\n * Removes the header-bar background so the bar blends into any surface.\n * Mirrors the `.inline` style class.\n */\n inline?: boolean;\n /** Suggestion items to show in the autocomplete popover. */\n suggestions?: Suggestion[];\n /** Called when the user selects a suggestion. */\n onSuggestionSelect?: (item: Suggestion) => void;\n /** Shows a spinner inside the suggestions popover while fetching async results. */\n loadingSuggestions?: boolean;\n /** Custom row renderer for each suggestion. */\n renderSuggestion?: (item: Suggestion) => ReactNode;\n /** `aria-label` for the suggestions listbox. Defaults to `\"Suggestions\"`. */\n suggestionsLabel?: string;\n}\n\ninterface DropdownPos {\n top: number;\n left: number;\n width: number;\n}\n\n/**\n * Collapsible search bar following the Adwaita `AdwSearchBar` pattern.\n *\n * - Slides in/out with a CSS height transition.\n * - Auto-focuses the input when `open` becomes `true`.\n * - Escape key calls `onClose`.\n * - Shows a clear (×) button whenever the input has a value.\n * - Optional `suggestions` / `loadingSuggestions` props show an autocomplete\n * popover anchored below the bar — follows the GTK4 pattern of\n * `AdwSearchBar` + `GtkPopover` + `GtkListView` (replaces `GtkEntryCompletion`).\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.SearchBar.html\n */\nexport function SearchBar({\n open,\n onClose,\n onClear,\n children,\n value,\n onChange,\n placeholder = \"Search…\",\n disabled,\n inline = false,\n className,\n suggestions,\n onSuggestionSelect,\n loadingSuggestions = false,\n renderSuggestion,\n suggestionsLabel = \"Suggestions\",\n ...props\n}: SearchBarProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const wrapperRef = useRef<HTMLDivElement>(null);\n const listboxRef = useRef<HTMLUListElement>(null);\n\n const listboxId = useId();\n const optionIdPrefix = useId();\n\n const [activeIndex, setActiveIndex] = useState(-1);\n const [dropdownPos, setDropdownPos] = useState<DropdownPos | null>(null);\n\n const hasSuggestions = (suggestions?.length ?? 0) > 0;\n const showDropdown = open && (loadingSuggestions || hasSuggestions);\n\n // Reset active index whenever the suggestion list changes\n useEffect(() => { setActiveIndex(-1); }, [suggestions]);\n\n // Auto-focus when opening\n useEffect(() => {\n if (open) {\n const id = requestAnimationFrame(() => inputRef.current?.focus());\n return () => cancelAnimationFrame(id);\n }\n }, [open]);\n\n // Position the suggestions dropdown below the wrapper\n useEffect(() => {\n if (!showDropdown || !wrapperRef.current) {\n setDropdownPos(null);\n return;\n }\n function updatePos() {\n if (!wrapperRef.current) return;\n const r = wrapperRef.current.getBoundingClientRect();\n setDropdownPos({ top: r.bottom, left: r.left, width: r.width });\n }\n updatePos();\n window.addEventListener(\"scroll\", updatePos, { passive: true, capture: true });\n window.addEventListener(\"resize\", updatePos, { passive: true });\n return () => {\n window.removeEventListener(\"scroll\", updatePos, { capture: true });\n window.removeEventListener(\"resize\", updatePos);\n };\n }, [showDropdown]);\n\n // Scroll the active option into view\n useEffect(() => {\n if (activeIndex < 0 || !listboxRef.current) return;\n (listboxRef.current.children[activeIndex] as HTMLElement | undefined)\n ?.scrollIntoView({ block: \"nearest\" });\n }, [activeIndex]);\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent<HTMLInputElement>) => {\n if (showDropdown && hasSuggestions && suggestions) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActiveIndex((i) => Math.min(i + 1, suggestions.length - 1));\n return;\n }\n if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActiveIndex((i) => Math.max(i - 1, -1));\n return;\n }\n if (e.key === \"Enter\" && activeIndex >= 0) {\n e.preventDefault();\n onSuggestionSelect?.(suggestions[activeIndex]);\n return;\n }\n }\n if (e.key === \"Escape\") {\n e.preventDefault();\n onClose?.();\n }\n props.onKeyDown?.(e);\n },\n [onClose, props, showDropdown, hasSuggestions, suggestions, activeIndex, onSuggestionSelect]\n );\n\n const hasValue = value !== undefined && value !== \"\";\n const activeOptionId = activeIndex >= 0 && suggestions?.[activeIndex]\n ? `${optionIdPrefix}-${suggestions[activeIndex].id}`\n : undefined;\n\n const dropdown = showDropdown && dropdownPos\n ? createPortal(\n <ul\n ref={listboxRef}\n role=\"listbox\"\n aria-label={suggestionsLabel}\n id={listboxId}\n className={styles.suggestionsPanel}\n style={{ top: dropdownPos.top, left: dropdownPos.left, width: dropdownPos.width }}\n onMouseDown={(e) => e.preventDefault()} // keep focus on input\n >\n {loadingSuggestions && (\n <li role=\"presentation\" className={styles.suggestionsLoading}>\n <Spinner size=\"sm\" label=\"Loading suggestions…\" />\n </li>\n )}\n {!loadingSuggestions &&\n suggestions?.map((item, index) => (\n <li\n key={item.id}\n id={`${optionIdPrefix}-${item.id}`}\n role=\"option\"\n aria-selected={index === activeIndex}\n className={[\n styles.suggestionItem,\n index === activeIndex ? styles.suggestionItemActive : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseLeave={() => setActiveIndex(-1)}\n onClick={() => onSuggestionSelect?.(item)}\n >\n {renderSuggestion ? renderSuggestion(item) : item.label}\n </li>\n ))}\n </ul>,\n document.body\n )\n : null;\n\n return (\n <>\n <div\n ref={wrapperRef}\n className={[styles.wrapper, open ? styles.open : null, className]\n .filter(Boolean)\n .join(\" \")}\n aria-hidden={!open}\n >\n <div\n className={[\n styles.bar,\n inline ? styles.inline : null,\n disabled ? styles.disabled : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n <span className={styles.searchIcon} aria-hidden>\n <Icon icon={Search} size=\"md\" />\n </span>\n\n <input\n ref={inputRef}\n type=\"search\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={showDropdown}\n aria-controls={showDropdown ? listboxId : undefined}\n aria-activedescendant={activeOptionId}\n value={value}\n onChange={onChange}\n placeholder={placeholder}\n disabled={disabled}\n tabIndex={open ? undefined : -1}\n className={styles.input}\n onKeyDown={handleKeyDown}\n {...props}\n />\n\n {hasValue && (\n <button\n type=\"button\"\n className={styles.clearButton}\n aria-label=\"Clear search\"\n tabIndex={open ? undefined : -1}\n disabled={disabled}\n onClick={() => {\n onClear?.();\n inputRef.current?.focus();\n }}\n >\n <Icon icon={Close} size=\"md\" />\n </button>\n )}\n </div>\n\n {children && <div className={styles.filterRow}>{children}</div>}\n </div>\n\n {typeof document !== \"undefined\" && dropdown}\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SearchBar.js","names":[],"sources":["../../../src/components/SearchBar/SearchBar.tsx"],"sourcesContent":["import {\n useRef,\n useEffect,\n useCallback,\n useId,\n useState,\n type KeyboardEvent,\n type InputHTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Search, Close } from \"@gnome-ui/icons\";\nimport { Icon } from \"../Icon\";\nimport { Spinner } from \"../Spinner\";\nimport styles from \"./SearchBar.module.css\";\n\nexport interface Suggestion {\n id: string;\n label: string;\n}\n\nexport interface SearchBarProps\n extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\"> {\n /**\n * Whether the search bar is visible/expanded.\n * When `false` the bar collapses to zero height and is hidden from AT.\n */\n open: boolean;\n /** Called when the user presses Escape or clicks the clear button with empty value. */\n onClose?: () => void;\n /** Called when the clear (×) button is clicked. Also clears the input value. */\n onClear?: () => void;\n /**\n * Content to render below the search bar (e.g. filter chips).\n * Only visible while `open` is true.\n */\n children?: ReactNode;\n /**\n * Removes the header-bar background so the bar blends into any surface.\n * Mirrors the `.inline` style class.\n */\n inline?: boolean;\n /** Suggestion items to show in the autocomplete popover. */\n suggestions?: Suggestion[];\n /** Called when the user selects a suggestion. */\n onSuggestionSelect?: (item: Suggestion) => void;\n /** Shows a spinner inside the suggestions popover while fetching async results. */\n loadingSuggestions?: boolean;\n /** Custom row renderer for each suggestion. */\n renderSuggestion?: (item: Suggestion) => ReactNode;\n /** `aria-label` for the suggestions listbox. Defaults to `\"Suggestions\"`. */\n suggestionsLabel?: string;\n}\n\ninterface DropdownPos {\n top: number;\n left: number;\n width: number;\n}\n\n/**\n * Collapsible search bar following the Adwaita `AdwSearchBar` pattern.\n *\n * - Slides in/out with a CSS height transition.\n * - Auto-focuses the input when `open` becomes `true`.\n * - Escape key calls `onClose`.\n * - Shows a clear (×) button whenever the input has a value.\n * - Optional `suggestions` / `loadingSuggestions` props show an autocomplete\n * popover anchored below the bar — follows the GTK4 pattern of\n * `AdwSearchBar` + `GtkPopover` + `GtkListView` (replaces `GtkEntryCompletion`).\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.SearchBar.html\n */\nexport function SearchBar({\n open,\n onClose,\n onClear,\n children,\n value,\n onChange,\n placeholder = \"Search…\",\n disabled,\n inline = false,\n className,\n suggestions,\n onSuggestionSelect,\n loadingSuggestions = false,\n renderSuggestion,\n suggestionsLabel = \"Suggestions\",\n ...props\n}: SearchBarProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const wrapperRef = useRef<HTMLDivElement>(null);\n const listboxRef = useRef<HTMLUListElement>(null);\n\n const listboxId = useId();\n const optionIdPrefix = useId();\n\n const [activeIndex, setActiveIndex] = useState(-1);\n const [dropdownPos, setDropdownPos] = useState<DropdownPos | null>(null);\n\n const hasSuggestions = (suggestions?.length ?? 0) > 0;\n const showDropdown = open && (loadingSuggestions || hasSuggestions);\n\n // Reset active index whenever the suggestion list changes\n useEffect(() => { setActiveIndex(-1); }, [suggestions]);\n\n // Auto-focus when opening\n useEffect(() => {\n if (open) {\n const id = requestAnimationFrame(() => inputRef.current?.focus());\n return () => cancelAnimationFrame(id);\n }\n }, [open]);\n\n // Position the suggestions dropdown below the wrapper\n useEffect(() => {\n if (!showDropdown || !wrapperRef.current) {\n setDropdownPos(null);\n return;\n }\n function updatePos() {\n if (!wrapperRef.current) return;\n const r = wrapperRef.current.getBoundingClientRect();\n setDropdownPos({ top: r.bottom, left: r.left, width: r.width });\n }\n updatePos();\n window.addEventListener(\"scroll\", updatePos, { passive: true, capture: true });\n window.addEventListener(\"resize\", updatePos, { passive: true });\n return () => {\n window.removeEventListener(\"scroll\", updatePos, { capture: true });\n window.removeEventListener(\"resize\", updatePos);\n };\n }, [showDropdown]);\n\n // Scroll the active option into view\n useEffect(() => {\n if (activeIndex < 0 || !listboxRef.current) return;\n (listboxRef.current.children[activeIndex] as HTMLElement | undefined)\n ?.scrollIntoView({ block: \"nearest\" });\n }, [activeIndex]);\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent<HTMLInputElement>) => {\n if (showDropdown && hasSuggestions && suggestions) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActiveIndex((i) => Math.min(i + 1, suggestions.length - 1));\n return;\n }\n if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActiveIndex((i) => Math.max(i - 1, -1));\n return;\n }\n if (e.key === \"Enter\" && activeIndex >= 0) {\n e.preventDefault();\n onSuggestionSelect?.(suggestions[activeIndex]);\n return;\n }\n }\n if (e.key === \"Escape\") {\n e.preventDefault();\n onClose?.();\n }\n props.onKeyDown?.(e);\n },\n [onClose, props, showDropdown, hasSuggestions, suggestions, activeIndex, onSuggestionSelect]\n );\n\n const hasValue = value !== undefined && value !== \"\";\n const activeOptionId = activeIndex >= 0 && suggestions?.[activeIndex]\n ? `${optionIdPrefix}-${suggestions[activeIndex].id}`\n : undefined;\n\n const dropdown = showDropdown && dropdownPos\n ? createPortal(\n <ul\n ref={listboxRef}\n role=\"listbox\"\n aria-label={suggestionsLabel}\n id={listboxId}\n className={styles.suggestionsPanel}\n style={{ top: dropdownPos.top, left: dropdownPos.left, width: dropdownPos.width }}\n onMouseDown={(e) => e.preventDefault()} // keep focus on input\n >\n {loadingSuggestions && (\n <li role=\"presentation\" className={styles.suggestionsLoading}>\n <Spinner size=\"sm\" label=\"Loading suggestions…\" />\n </li>\n )}\n {!loadingSuggestions &&\n suggestions?.map((item, index) => (\n <li\n key={item.id}\n id={`${optionIdPrefix}-${item.id}`}\n role=\"option\"\n aria-selected={index === activeIndex}\n className={[\n styles.suggestionItem,\n index === activeIndex ? styles.suggestionItemActive : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseLeave={() => setActiveIndex(-1)}\n onClick={() => onSuggestionSelect?.(item)}\n >\n {renderSuggestion ? renderSuggestion(item) : item.label}\n </li>\n ))}\n </ul>,\n document.body\n )\n : null;\n\n return (\n <>\n <div\n ref={wrapperRef}\n className={[styles.wrapper, open ? styles.open : null, className]\n .filter(Boolean)\n .join(\" \")}\n aria-hidden={!open}\n >\n <div\n className={[\n styles.bar,\n inline ? styles.inline : null,\n disabled ? styles.disabled : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n <span className={styles.searchIcon} aria-hidden>\n <Icon icon={Search} size=\"md\" />\n </span>\n\n <input\n ref={inputRef}\n type=\"search\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={showDropdown}\n aria-controls={showDropdown ? listboxId : undefined}\n aria-activedescendant={activeOptionId}\n value={value}\n onChange={onChange}\n placeholder={placeholder}\n disabled={disabled}\n tabIndex={open ? undefined : -1}\n className={styles.input}\n onKeyDown={handleKeyDown}\n {...props}\n />\n\n {hasValue && (\n <button\n type=\"button\"\n className={styles.clearButton}\n aria-label=\"Clear search\"\n tabIndex={open ? undefined : -1}\n disabled={disabled}\n onClick={() => {\n onClear?.();\n inputRef.current?.focus();\n }}\n >\n <Icon icon={Close} size=\"md\" />\n </button>\n )}\n </div>\n\n {children && <div className={styles.filterRow}>{children}</div>}\n </div>\n\n {typeof document !== \"undefined\" && dropdown}\n </>\n );\n}\n"],"mappings":";;;;;;;;AAyEA,SAAgB,EAAU,EACxB,SACA,YACA,YACA,aACA,UACA,aACA,iBAAc,WACd,aACA,YAAS,IACT,cACA,gBACA,uBACA,wBAAqB,IACrB,qBACA,sBAAmB,eACnB,GAAG,KACc;CACjB,IAAM,IAAa,EAAyB,KAAK,EAC3C,IAAa,EAAuB,KAAK,EACzC,IAAa,EAAyB,KAAK,EAE3C,IAAiB,GAAO,EACxB,IAAiB,GAAO,EAExB,CAAC,GAAc,KAAmB,EAAS,GAAG,EAC9C,CAAC,GAAc,KAAmB,EAA6B,KAAK,EAEpE,KAAkB,GAAa,UAAU,KAAK,GAC9C,IAAiB,MAAS,KAAsB;AAkCtD,CA/BA,QAAgB;AAAE,IAAe,GAAG;IAAK,CAAC,EAAY,CAAC,EAGvD,QAAgB;AACd,MAAI,GAAM;GACR,IAAM,IAAK,4BAA4B,EAAS,SAAS,OAAO,CAAC;AACjE,gBAAa,qBAAqB,EAAG;;IAEtC,CAAC,EAAK,CAAC,EAGV,QAAgB;AACd,MAAI,CAAC,KAAgB,CAAC,EAAW,SAAS;AACxC,KAAe,KAAK;AACpB;;EAEF,SAAS,IAAY;AACnB,OAAI,CAAC,EAAW,QAAS;GACzB,IAAM,IAAI,EAAW,QAAQ,uBAAuB;AACpD,KAAe;IAAE,KAAK,EAAE;IAAQ,MAAM,EAAE;IAAM,OAAO,EAAE;IAAO,CAAC;;AAKjE,SAHA,GAAW,EACX,OAAO,iBAAiB,UAAU,GAAW;GAAE,SAAS;GAAM,SAAS;GAAM,CAAC,EAC9E,OAAO,iBAAiB,UAAU,GAAW,EAAE,SAAS,IAAM,CAAC,QAClD;AAEX,GADA,OAAO,oBAAoB,UAAU,GAAW,EAAE,SAAS,IAAM,CAAC,EAClE,OAAO,oBAAoB,UAAU,EAAU;;IAEhD,CAAC,EAAa,CAAC,EAGlB,QAAgB;AACV,MAAc,KAAK,CAAC,EAAW,WAClC,EAAW,QAAQ,SAAS,IACzB,eAAe,EAAE,OAAO,WAAW,CAAC;IACvC,CAAC,EAAY,CAAC;CAEjB,IAAM,IAAgB,GACnB,MAAuC;AACtC,MAAI,KAAgB,KAAkB,GAAa;AACjD,OAAI,EAAE,QAAQ,aAAa;AAEzB,IADA,EAAE,gBAAgB,EAClB,GAAgB,MAAM,KAAK,IAAI,IAAI,GAAG,EAAY,SAAS,EAAE,CAAC;AAC9D;;AAEF,OAAI,EAAE,QAAQ,WAAW;AAEvB,IADA,EAAE,gBAAgB,EAClB,GAAgB,MAAM,KAAK,IAAI,IAAI,GAAG,GAAG,CAAC;AAC1C;;AAEF,OAAI,EAAE,QAAQ,WAAW,KAAe,GAAG;AAEzC,IADA,EAAE,gBAAgB,EAClB,IAAqB,EAAY,GAAa;AAC9C;;;AAOJ,EAJI,EAAE,QAAQ,aACZ,EAAE,gBAAgB,EAClB,KAAW,GAEb,EAAM,YAAY,EAAE;IAEtB;EAAC;EAAS;EAAO;EAAc;EAAgB;EAAa;EAAa;EAAmB,CAC7F,EAEK,IAAiB,MAAU,KAAA,KAAa,MAAU,IAClD,IAAiB,KAAe,KAAK,IAAc,KACrD,GAAG,EAAe,GAAG,EAAY,GAAa,OAC9C,KAAA,GAEE,IAAW,KAAgB,IAC7B,EACE,kBAAC,MAAD;EACE,KAAK;EACL,MAAK;EACL,cAAY;EACZ,IAAI;EACJ,WAAW,EAAO;EAClB,OAAO;GAAE,KAAK,EAAY;GAAK,MAAM,EAAY;GAAM,OAAO,EAAY;GAAO;EACjF,cAAc,MAAM,EAAE,gBAAgB;YAPxC,CASG,KACC,kBAAC,MAAD;GAAI,MAAK;GAAe,WAAW,EAAO;aACxC,kBAAC,GAAD;IAAS,MAAK;IAAK,OAAM;IAAyB,CAAA;GAC/C,CAAA,EAEN,CAAC,KACA,GAAa,KAAK,GAAM,MACtB,kBAAC,MAAD;GAEE,IAAI,GAAG,EAAe,GAAG,EAAK;GAC9B,MAAK;GACL,iBAAe,MAAU;GACzB,WAAW,CACT,EAAO,gBACP,MAAU,IAAc,EAAO,uBAAuB,KACvD,CACE,OAAO,QAAQ,CACf,KAAK,IAAI;GACZ,oBAAoB,EAAe,EAAM;GACzC,oBAAoB,EAAe,GAAG;GACtC,eAAe,IAAqB,EAAK;aAExC,IAAmB,EAAiB,EAAK,GAAG,EAAK;GAC/C,EAfE,EAAK,GAeP,CACL,CACD;KACL,SAAS,KACV,GACD;AAEJ,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;EACE,KAAK;EACL,WAAW;GAAC,EAAO;GAAS,IAAO,EAAO,OAAO;GAAM;GAAU,CAC9D,OAAO,QAAQ,CACf,KAAK,IAAI;EACZ,eAAa,CAAC;YALhB,CAOE,kBAAC,OAAD;GACE,WAAW;IACT,EAAO;IACP,IAAW,EAAO,SAAW;IAC7B,IAAW,EAAO,WAAW;IAC9B,CACE,OAAO,QAAQ,CACf,KAAK,IAAI;aAPd;IASE,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAY,eAAA;eAClC,kBAAC,GAAD;MAAM,MAAM;MAAQ,MAAK;MAAO,CAAA;KAC3B,CAAA;IAEP,kBAAC,SAAD;KACE,KAAK;KACL,MAAK;KACL,MAAK;KACL,qBAAkB;KAClB,iBAAe;KACf,iBAAe,IAAe,IAAY,KAAA;KAC1C,yBAAuB;KAChB;KACG;KACG;KACH;KACV,UAAU,IAAO,KAAA,IAAY;KAC7B,WAAW,EAAO;KAClB,WAAW;KACX,GAAI;KACJ,CAAA;IAED,KACC,kBAAC,UAAD;KACE,MAAK;KACL,WAAW,EAAO;KAClB,cAAW;KACX,UAAU,IAAO,KAAA,IAAY;KACnB;KACV,eAAe;AAEb,MADA,KAAW,EACX,EAAS,SAAS,OAAO;;eAG3B,kBAAC,GAAD;MAAM,MAAM;MAAO,MAAK;MAAO,CAAA;KACxB,CAAA;IAEP;MAEL,KAAY,kBAAC,OAAD;GAAK,WAAW,EAAO;GAAY;GAAe,CAAA,CAC3D;KAEL,OAAO,WAAa,OAAe,EACnC,EAAA,CAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require(`../Icon/Icon.cjs`),t=require(
|
|
1
|
+
const e=require(`../Icon/Icon.cjs`),t=require(`./Sidebar.module.css.cjs`),n=require(`./Sidebar.cjs`);let r=require(`react`),i=require(`react/jsx-runtime`),a=require(`@gnome-ui/icons`);function o(e,t){let n=0;return r.Children.forEach(e,e=>{if(!(0,r.isValidElement)(e))return;let i=e.props;typeof i.label==`string`?i.label.toLowerCase().includes(t.toLowerCase())&&n++:i.children&&(n+=o(i.children,t))}),n}var s=(0,r.forwardRef)(function({title:s,icon:c,collapsible:l=!1,defaultOpen:u=!0,open:d,onOpenChange:f,children:p,className:m,...h},g){let[_,v]=(0,r.useState)(u),y=d!==void 0,b=y?d:_,x=n.useSidebarCollapsed(),S=(0,r.useContext)(n.SidebarFilterContext),C=(0,r.useId)(),w=e=>{y||v(e),f?.(e)};if((0,r.useImperativeHandle)(g,()=>({expand:()=>w(!0),collapse:()=>w(!1),toggle:()=>w(!b)})),!(S.length===0||o(p,S)>0))return null;let T=x?!0:b,E=!!(s||c||l&&!x);return(0,i.jsxs)(`section`,{className:[t.default.section,m].filter(Boolean).join(` `),...h,children:[E&&(l&&!x?(0,i.jsxs)(`button`,{type:`button`,className:t.default.sectionHeader,onClick:()=>w(!b),"aria-expanded":b,"aria-controls":C,children:[c&&(0,i.jsx)(`span`,{className:t.default.sectionHeaderIcon,children:(0,i.jsx)(e.Icon,{icon:c,size:`sm`,"aria-hidden":!0})}),s&&(0,i.jsx)(`span`,{className:t.default.sectionTitle,children:s}),(0,i.jsx)(`span`,{className:t.default.sectionChevron,children:(0,i.jsx)(e.Icon,{icon:b?a.PanUp:a.PanDown,size:`sm`,"aria-hidden":!0})})]}):(0,i.jsxs)(`div`,{className:t.default.sectionHeader,children:[c&&(0,i.jsx)(`span`,{className:t.default.sectionHeaderIcon,children:(0,i.jsx)(e.Icon,{icon:c,size:`sm`,"aria-hidden":!0})}),s&&(0,i.jsx)(`span`,{className:t.default.sectionTitle,children:s})]})),(0,i.jsx)(`div`,{id:C,className:[t.default.sectionBody,T?null:t.default.sectionBodyCollapsed].filter(Boolean).join(` `),"aria-hidden":!T||void 0,children:(0,i.jsx)(`div`,{className:t.default.sectionBodyInner,children:(0,i.jsx)(`ul`,{role:`list`,className:t.default.list,children:p})})})]})});exports.SidebarSection=s;
|
|
2
2
|
//# sourceMappingURL=SidebarSection.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SidebarSection.cjs","names":[],"sources":["../../../src/components/Sidebar/SidebarSection.tsx"],"sourcesContent":["import {\n forwardRef,\n useImperativeHandle,\n useState,\n useContext,\n useId,\n isValidElement,\n Children,\n type HTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport type { IconDefinition } from \"@gnome-ui/icons\";\nimport { PanDown, PanUp } from \"@gnome-ui/icons\";\nimport { Icon } from \"../Icon\";\nimport { useSidebarCollapsed, SidebarFilterContext } from \"./Sidebar\";\nimport styles from \"./Sidebar.module.css\";\n\n// ─── Filter helper ────────────────────────────────────────────────────────────\n\nfunction countMatchingItems(children: ReactNode, filter: string): number {\n let count = 0;\n Children.forEach(children, (child) => {\n if (!isValidElement(child)) return;\n const props = child.props as Record<string, unknown>;\n if (typeof props.label === \"string\") {\n if (props.label.toLowerCase().includes(filter.toLowerCase())) count++;\n } else if (props.children) {\n count += countMatchingItems(props.children as ReactNode, filter);\n }\n });\n return count;\n}\n\n// ─── Handle ───────────────────────────────────────────────────────────────────\n\nexport interface SidebarSectionHandle {\n expand: () => void;\n collapse: () => void;\n toggle: () => void;\n}\n\n// ─── Props ────────────────────────────────────────────────────────────────────\n\nexport interface SidebarSectionProps extends HTMLAttributes<HTMLElement> {\n /**\n * Section heading. Rendered in small caps above the items.\n * Omit for an untitled section (e.g. the first group in a sidebar).\n */\n title?: string;\n /** Icon rendered left of the title. Same type as `SidebarItem.icon`. */\n icon?: IconDefinition;\n /** Whether the section body can be toggled open/closed. Defaults to `false`. */\n collapsible?: boolean;\n /** Initial open state when `collapsible` is true. Defaults to `true`. */\n defaultOpen?: boolean;\n /** Controlled open state. */\n open?: boolean;\n /** Called when open state changes. */\n onOpenChange?: (open: boolean) => void;\n children?: ReactNode;\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * Named group of `SidebarItem` entries inside a `Sidebar`.\n *\n * Sections are separated by a thin divider. The title and icon are optional.\n * When `collapsible` is true the body can be toggled via the header button or\n * imperatively via a `ref` (`expand`, `collapse`, `toggle`).\n *\n * In rail (icon-only) mode the body is always visible regardless of open state.\n * When a sidebar filter is active and no children match, the section is hidden.\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Sidebar.html\n */\nexport const SidebarSection = forwardRef<SidebarSectionHandle, SidebarSectionProps>(\n function SidebarSection(\n {\n title,\n icon,\n collapsible = false,\n defaultOpen = true,\n open: controlledOpen,\n onOpenChange,\n children,\n className,\n ...props\n },\n ref,\n ) {\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n\n const sidebarCollapsed = useSidebarCollapsed();\n const filterValue = useContext(SidebarFilterContext);\n const bodyId = useId();\n\n const setAndNotify = (next: boolean) => {\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n useImperativeHandle(ref, () => ({\n expand: () => setAndNotify(true),\n collapse: () => setAndNotify(false),\n toggle: () => setAndNotify(!open),\n }));\n\n // Hide section entirely when filter is active and no children match\n const hasVisibleChildren =\n filterValue.length === 0 || countMatchingItems(children, filterValue) > 0;\n\n if (!hasVisibleChildren) return null;\n\n // In rail mode the body is always visible\n const isOpen = sidebarCollapsed ? true : open;\n\n const showHeader = !!(title || icon || (collapsible && !sidebarCollapsed));\n\n return (\n <section\n className={[styles.section, className].filter(Boolean).join(\" \")}\n {...props}\n >\n {showHeader && (\n collapsible && !sidebarCollapsed ? (\n <button\n type=\"button\"\n className={styles.sectionHeader}\n onClick={() => setAndNotify(!open)}\n aria-expanded={open}\n aria-controls={bodyId}\n >\n {icon && (\n <span className={styles.sectionHeaderIcon}>\n <Icon icon={icon} size=\"sm\" aria-hidden />\n </span>\n )}\n {title && <span className={styles.sectionTitle}>{title}</span>}\n <span className={styles.sectionChevron}>\n <Icon icon={open ? PanUp : PanDown} size=\"sm\" aria-hidden />\n </span>\n </button>\n ) : (\n <div className={styles.sectionHeader}>\n {icon && (\n <span className={styles.sectionHeaderIcon}>\n <Icon icon={icon} size=\"sm\" aria-hidden />\n </span>\n )}\n {title && <span className={styles.sectionTitle}>{title}</span>}\n </div>\n )\n )}\n\n <div\n id={bodyId}\n className={[\n styles.sectionBody,\n !isOpen ? styles.sectionBodyCollapsed : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n aria-hidden={!isOpen || undefined}\n >\n <div className={styles.sectionBodyInner}>\n <ul role=\"list\" className={styles.list}>\n {children}\n </ul>\n </div>\n </div>\n </section>\n );\n },\n);\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"SidebarSection.cjs","names":[],"sources":["../../../src/components/Sidebar/SidebarSection.tsx"],"sourcesContent":["import {\n forwardRef,\n useImperativeHandle,\n useState,\n useContext,\n useId,\n isValidElement,\n Children,\n type HTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport type { IconDefinition } from \"@gnome-ui/icons\";\nimport { PanDown, PanUp } from \"@gnome-ui/icons\";\nimport { Icon } from \"../Icon\";\nimport { useSidebarCollapsed, SidebarFilterContext } from \"./Sidebar\";\nimport styles from \"./Sidebar.module.css\";\n\n// ─── Filter helper ────────────────────────────────────────────────────────────\n\nfunction countMatchingItems(children: ReactNode, filter: string): number {\n let count = 0;\n Children.forEach(children, (child) => {\n if (!isValidElement(child)) return;\n const props = child.props as Record<string, unknown>;\n if (typeof props.label === \"string\") {\n if (props.label.toLowerCase().includes(filter.toLowerCase())) count++;\n } else if (props.children) {\n count += countMatchingItems(props.children as ReactNode, filter);\n }\n });\n return count;\n}\n\n// ─── Handle ───────────────────────────────────────────────────────────────────\n\nexport interface SidebarSectionHandle {\n expand: () => void;\n collapse: () => void;\n toggle: () => void;\n}\n\n// ─── Props ────────────────────────────────────────────────────────────────────\n\nexport interface SidebarSectionProps extends HTMLAttributes<HTMLElement> {\n /**\n * Section heading. Rendered in small caps above the items.\n * Omit for an untitled section (e.g. the first group in a sidebar).\n */\n title?: string;\n /** Icon rendered left of the title. Same type as `SidebarItem.icon`. */\n icon?: IconDefinition;\n /** Whether the section body can be toggled open/closed. Defaults to `false`. */\n collapsible?: boolean;\n /** Initial open state when `collapsible` is true. Defaults to `true`. */\n defaultOpen?: boolean;\n /** Controlled open state. */\n open?: boolean;\n /** Called when open state changes. */\n onOpenChange?: (open: boolean) => void;\n children?: ReactNode;\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * Named group of `SidebarItem` entries inside a `Sidebar`.\n *\n * Sections are separated by a thin divider. The title and icon are optional.\n * When `collapsible` is true the body can be toggled via the header button or\n * imperatively via a `ref` (`expand`, `collapse`, `toggle`).\n *\n * In rail (icon-only) mode the body is always visible regardless of open state.\n * When a sidebar filter is active and no children match, the section is hidden.\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Sidebar.html\n */\nexport const SidebarSection = forwardRef<SidebarSectionHandle, SidebarSectionProps>(\n function SidebarSection(\n {\n title,\n icon,\n collapsible = false,\n defaultOpen = true,\n open: controlledOpen,\n onOpenChange,\n children,\n className,\n ...props\n },\n ref,\n ) {\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n\n const sidebarCollapsed = useSidebarCollapsed();\n const filterValue = useContext(SidebarFilterContext);\n const bodyId = useId();\n\n const setAndNotify = (next: boolean) => {\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n useImperativeHandle(ref, () => ({\n expand: () => setAndNotify(true),\n collapse: () => setAndNotify(false),\n toggle: () => setAndNotify(!open),\n }));\n\n // Hide section entirely when filter is active and no children match\n const hasVisibleChildren =\n filterValue.length === 0 || countMatchingItems(children, filterValue) > 0;\n\n if (!hasVisibleChildren) return null;\n\n // In rail mode the body is always visible\n const isOpen = sidebarCollapsed ? true : open;\n\n const showHeader = !!(title || icon || (collapsible && !sidebarCollapsed));\n\n return (\n <section\n className={[styles.section, className].filter(Boolean).join(\" \")}\n {...props}\n >\n {showHeader && (\n collapsible && !sidebarCollapsed ? (\n <button\n type=\"button\"\n className={styles.sectionHeader}\n onClick={() => setAndNotify(!open)}\n aria-expanded={open}\n aria-controls={bodyId}\n >\n {icon && (\n <span className={styles.sectionHeaderIcon}>\n <Icon icon={icon} size=\"sm\" aria-hidden />\n </span>\n )}\n {title && <span className={styles.sectionTitle}>{title}</span>}\n <span className={styles.sectionChevron}>\n <Icon icon={open ? PanUp : PanDown} size=\"sm\" aria-hidden />\n </span>\n </button>\n ) : (\n <div className={styles.sectionHeader}>\n {icon && (\n <span className={styles.sectionHeaderIcon}>\n <Icon icon={icon} size=\"sm\" aria-hidden />\n </span>\n )}\n {title && <span className={styles.sectionTitle}>{title}</span>}\n </div>\n )\n )}\n\n <div\n id={bodyId}\n className={[\n styles.sectionBody,\n !isOpen ? styles.sectionBodyCollapsed : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n aria-hidden={!isOpen || undefined}\n >\n <div className={styles.sectionBodyInner}>\n <ul role=\"list\" className={styles.list}>\n {children}\n </ul>\n </div>\n </div>\n </section>\n );\n },\n);\n"],"mappings":"wLAmBA,SAAS,EAAmB,EAAqB,EAAwB,CACvE,IAAI,EAAQ,EAUZ,OATA,EAAA,SAAS,QAAQ,EAAW,GAAU,CACpC,GAAI,EAAA,EAAA,EAAA,gBAAgB,EAAM,CAAE,OAC5B,IAAM,EAAQ,EAAM,MAChB,OAAO,EAAM,OAAU,SACrB,EAAM,MAAM,aAAa,CAAC,SAAS,EAAO,aAAa,CAAC,EAAE,IACrD,EAAM,WACf,GAAS,EAAmB,EAAM,SAAuB,EAAO,GAElE,CACK,EA8CT,IAAa,GAAA,EAAA,EAAA,YACX,SACE,CACE,QACA,OACA,cAAc,GACd,cAAc,GACd,KAAM,EACN,eACA,WACA,YACA,GAAG,GAEL,EACA,CACA,GAAM,CAAC,EAAc,IAAA,EAAA,EAAA,UAA4B,EAAY,CACvD,EAAe,IAAmB,IAAA,GAClC,EAAO,EAAe,EAAiB,EAEvC,EAAmB,EAAA,qBAAqB,CACxC,GAAA,EAAA,EAAA,YAAyB,EAAA,qBAAqB,CAC9C,GAAA,EAAA,EAAA,QAAgB,CAEhB,EAAgB,GAAkB,CACjC,GAAc,EAAgB,EAAK,CACxC,IAAe,EAAK,EAatB,IAVA,EAAA,EAAA,qBAAoB,OAAY,CAC9B,WAAc,EAAa,GAAK,CAChC,aAAgB,EAAa,GAAM,CACnC,WAAc,EAAa,CAAC,EAAK,CAClC,EAAE,CAMC,EAFF,EAAY,SAAW,GAAK,EAAmB,EAAU,EAAY,CAAG,GAEjD,OAAO,KAGhC,IAAM,EAAS,EAAmB,GAAO,EAEnC,EAAa,CAAC,EAAE,GAAS,GAAS,GAAe,CAAC,GAExD,OACE,EAAA,EAAA,MAAC,UAAD,CACE,UAAW,CAAC,EAAA,QAAO,QAAS,EAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI,CAChE,GAAI,WAFN,CAIG,IACC,GAAe,CAAC,GACd,EAAA,EAAA,MAAC,SAAD,CACE,KAAK,SACL,UAAW,EAAA,QAAO,cAClB,YAAe,EAAa,CAAC,EAAK,CAClC,gBAAe,EACf,gBAAe,WALjB,CAOG,IACC,EAAA,EAAA,KAAC,OAAD,CAAM,UAAW,EAAA,QAAO,4BACtB,EAAA,EAAA,KAAC,EAAA,KAAD,CAAY,OAAM,KAAK,KAAK,cAAA,GAAc,CAAA,CACrC,CAAA,CAER,IAAS,EAAA,EAAA,KAAC,OAAD,CAAM,UAAW,EAAA,QAAO,sBAAe,EAAa,CAAA,EAC9D,EAAA,EAAA,KAAC,OAAD,CAAM,UAAW,EAAA,QAAO,yBACtB,EAAA,EAAA,KAAC,EAAA,KAAD,CAAM,KAAM,EAAO,EAAA,MAAQ,EAAA,QAAS,KAAK,KAAK,cAAA,GAAc,CAAA,CACvD,CAAA,CACA,IAET,EAAA,EAAA,MAAC,MAAD,CAAK,UAAW,EAAA,QAAO,uBAAvB,CACG,IACC,EAAA,EAAA,KAAC,OAAD,CAAM,UAAW,EAAA,QAAO,4BACtB,EAAA,EAAA,KAAC,EAAA,KAAD,CAAY,OAAM,KAAK,KAAK,cAAA,GAAc,CAAA,CACrC,CAAA,CAER,IAAS,EAAA,EAAA,KAAC,OAAD,CAAM,UAAW,EAAA,QAAO,sBAAe,EAAa,CAAA,CAC1D,KAIV,EAAA,EAAA,KAAC,MAAD,CACE,GAAI,EACJ,UAAW,CACT,EAAA,QAAO,YACN,EAAuC,KAA9B,EAAA,QAAO,qBAClB,CACE,OAAO,QAAQ,CACf,KAAK,IAAI,CACZ,cAAa,CAAC,GAAU,IAAA,aAExB,EAAA,EAAA,KAAC,MAAD,CAAK,UAAW,EAAA,QAAO,2BACrB,EAAA,EAAA,KAAC,KAAD,CAAI,KAAK,OAAO,UAAW,EAAA,QAAO,KAC/B,WACE,CAAA,CACD,CAAA,CACF,CAAA,CACE,IAGf"}
|
|
@@ -1,82 +1,81 @@
|
|
|
1
1
|
import { Icon as e } from "../Icon/Icon.js";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
2
|
+
import t from "./Sidebar.module.css.js";
|
|
3
|
+
import { SidebarFilterContext as n, useSidebarCollapsed as r } from "./Sidebar.js";
|
|
4
|
+
import { Children as i, forwardRef as a, isValidElement as o, useContext as s, useId as c, useImperativeHandle as l, useState as u } from "react";
|
|
5
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
6
|
+
import { PanDown as p, PanUp as m } from "@gnome-ui/icons";
|
|
8
7
|
//#region src/components/Sidebar/SidebarSection.tsx
|
|
9
8
|
function h(e, t) {
|
|
10
9
|
let n = 0;
|
|
11
|
-
return
|
|
12
|
-
if (!
|
|
10
|
+
return i.forEach(e, (e) => {
|
|
11
|
+
if (!o(e)) return;
|
|
13
12
|
let r = e.props;
|
|
14
13
|
typeof r.label == "string" ? r.label.toLowerCase().includes(t.toLowerCase()) && n++ : r.children && (n += h(r.children, t));
|
|
15
14
|
}), n;
|
|
16
15
|
}
|
|
17
|
-
var g =
|
|
18
|
-
let [C, w] =
|
|
16
|
+
var g = a(function({ title: i, icon: a, collapsible: o = !1, defaultOpen: g = !0, open: _, onOpenChange: v, children: y, className: b, ...x }, S) {
|
|
17
|
+
let [C, w] = u(g), T = _ !== void 0, E = T ? _ : C, D = r(), O = s(n), k = c(), A = (e) => {
|
|
19
18
|
T || w(e), v?.(e);
|
|
20
19
|
};
|
|
21
|
-
if (
|
|
20
|
+
if (l(S, () => ({
|
|
22
21
|
expand: () => A(!0),
|
|
23
22
|
collapse: () => A(!1),
|
|
24
23
|
toggle: () => A(!E)
|
|
25
24
|
})), !(O.length === 0 || h(y, O) > 0)) return null;
|
|
26
|
-
let j = D ? !0 : E, M = !!(
|
|
27
|
-
return /* @__PURE__ */
|
|
28
|
-
className: [
|
|
25
|
+
let j = D ? !0 : E, M = !!(i || a || o && !D);
|
|
26
|
+
return /* @__PURE__ */ f("section", {
|
|
27
|
+
className: [t.section, b].filter(Boolean).join(" "),
|
|
29
28
|
...x,
|
|
30
|
-
children: [M && (
|
|
29
|
+
children: [M && (o && !D ? /* @__PURE__ */ f("button", {
|
|
31
30
|
type: "button",
|
|
32
|
-
className:
|
|
31
|
+
className: t.sectionHeader,
|
|
33
32
|
onClick: () => A(!E),
|
|
34
33
|
"aria-expanded": E,
|
|
35
34
|
"aria-controls": k,
|
|
36
35
|
children: [
|
|
37
|
-
|
|
38
|
-
className:
|
|
39
|
-
children: /* @__PURE__ */
|
|
40
|
-
icon:
|
|
36
|
+
a && /* @__PURE__ */ d("span", {
|
|
37
|
+
className: t.sectionHeaderIcon,
|
|
38
|
+
children: /* @__PURE__ */ d(e, {
|
|
39
|
+
icon: a,
|
|
41
40
|
size: "sm",
|
|
42
41
|
"aria-hidden": !0
|
|
43
42
|
})
|
|
44
43
|
}),
|
|
45
|
-
|
|
46
|
-
className:
|
|
47
|
-
children:
|
|
44
|
+
i && /* @__PURE__ */ d("span", {
|
|
45
|
+
className: t.sectionTitle,
|
|
46
|
+
children: i
|
|
48
47
|
}),
|
|
49
|
-
/* @__PURE__ */
|
|
50
|
-
className:
|
|
51
|
-
children: /* @__PURE__ */
|
|
52
|
-
icon: E ?
|
|
48
|
+
/* @__PURE__ */ d("span", {
|
|
49
|
+
className: t.sectionChevron,
|
|
50
|
+
children: /* @__PURE__ */ d(e, {
|
|
51
|
+
icon: E ? m : p,
|
|
53
52
|
size: "sm",
|
|
54
53
|
"aria-hidden": !0
|
|
55
54
|
})
|
|
56
55
|
})
|
|
57
56
|
]
|
|
58
|
-
}) : /* @__PURE__ */
|
|
59
|
-
className:
|
|
60
|
-
children: [
|
|
61
|
-
className:
|
|
62
|
-
children: /* @__PURE__ */
|
|
63
|
-
icon:
|
|
57
|
+
}) : /* @__PURE__ */ f("div", {
|
|
58
|
+
className: t.sectionHeader,
|
|
59
|
+
children: [a && /* @__PURE__ */ d("span", {
|
|
60
|
+
className: t.sectionHeaderIcon,
|
|
61
|
+
children: /* @__PURE__ */ d(e, {
|
|
62
|
+
icon: a,
|
|
64
63
|
size: "sm",
|
|
65
64
|
"aria-hidden": !0
|
|
66
65
|
})
|
|
67
|
-
}),
|
|
68
|
-
className:
|
|
69
|
-
children:
|
|
66
|
+
}), i && /* @__PURE__ */ d("span", {
|
|
67
|
+
className: t.sectionTitle,
|
|
68
|
+
children: i
|
|
70
69
|
})]
|
|
71
|
-
})), /* @__PURE__ */
|
|
70
|
+
})), /* @__PURE__ */ d("div", {
|
|
72
71
|
id: k,
|
|
73
|
-
className: [
|
|
72
|
+
className: [t.sectionBody, j ? null : t.sectionBodyCollapsed].filter(Boolean).join(" "),
|
|
74
73
|
"aria-hidden": !j || void 0,
|
|
75
|
-
children: /* @__PURE__ */
|
|
76
|
-
className:
|
|
77
|
-
children: /* @__PURE__ */
|
|
74
|
+
children: /* @__PURE__ */ d("div", {
|
|
75
|
+
className: t.sectionBodyInner,
|
|
76
|
+
children: /* @__PURE__ */ d("ul", {
|
|
78
77
|
role: "list",
|
|
79
|
-
className:
|
|
78
|
+
className: t.list,
|
|
80
79
|
children: y
|
|
81
80
|
})
|
|
82
81
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SidebarSection.js","names":[],"sources":["../../../src/components/Sidebar/SidebarSection.tsx"],"sourcesContent":["import {\n forwardRef,\n useImperativeHandle,\n useState,\n useContext,\n useId,\n isValidElement,\n Children,\n type HTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport type { IconDefinition } from \"@gnome-ui/icons\";\nimport { PanDown, PanUp } from \"@gnome-ui/icons\";\nimport { Icon } from \"../Icon\";\nimport { useSidebarCollapsed, SidebarFilterContext } from \"./Sidebar\";\nimport styles from \"./Sidebar.module.css\";\n\n// ─── Filter helper ────────────────────────────────────────────────────────────\n\nfunction countMatchingItems(children: ReactNode, filter: string): number {\n let count = 0;\n Children.forEach(children, (child) => {\n if (!isValidElement(child)) return;\n const props = child.props as Record<string, unknown>;\n if (typeof props.label === \"string\") {\n if (props.label.toLowerCase().includes(filter.toLowerCase())) count++;\n } else if (props.children) {\n count += countMatchingItems(props.children as ReactNode, filter);\n }\n });\n return count;\n}\n\n// ─── Handle ───────────────────────────────────────────────────────────────────\n\nexport interface SidebarSectionHandle {\n expand: () => void;\n collapse: () => void;\n toggle: () => void;\n}\n\n// ─── Props ────────────────────────────────────────────────────────────────────\n\nexport interface SidebarSectionProps extends HTMLAttributes<HTMLElement> {\n /**\n * Section heading. Rendered in small caps above the items.\n * Omit for an untitled section (e.g. the first group in a sidebar).\n */\n title?: string;\n /** Icon rendered left of the title. Same type as `SidebarItem.icon`. */\n icon?: IconDefinition;\n /** Whether the section body can be toggled open/closed. Defaults to `false`. */\n collapsible?: boolean;\n /** Initial open state when `collapsible` is true. Defaults to `true`. */\n defaultOpen?: boolean;\n /** Controlled open state. */\n open?: boolean;\n /** Called when open state changes. */\n onOpenChange?: (open: boolean) => void;\n children?: ReactNode;\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * Named group of `SidebarItem` entries inside a `Sidebar`.\n *\n * Sections are separated by a thin divider. The title and icon are optional.\n * When `collapsible` is true the body can be toggled via the header button or\n * imperatively via a `ref` (`expand`, `collapse`, `toggle`).\n *\n * In rail (icon-only) mode the body is always visible regardless of open state.\n * When a sidebar filter is active and no children match, the section is hidden.\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Sidebar.html\n */\nexport const SidebarSection = forwardRef<SidebarSectionHandle, SidebarSectionProps>(\n function SidebarSection(\n {\n title,\n icon,\n collapsible = false,\n defaultOpen = true,\n open: controlledOpen,\n onOpenChange,\n children,\n className,\n ...props\n },\n ref,\n ) {\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n\n const sidebarCollapsed = useSidebarCollapsed();\n const filterValue = useContext(SidebarFilterContext);\n const bodyId = useId();\n\n const setAndNotify = (next: boolean) => {\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n useImperativeHandle(ref, () => ({\n expand: () => setAndNotify(true),\n collapse: () => setAndNotify(false),\n toggle: () => setAndNotify(!open),\n }));\n\n // Hide section entirely when filter is active and no children match\n const hasVisibleChildren =\n filterValue.length === 0 || countMatchingItems(children, filterValue) > 0;\n\n if (!hasVisibleChildren) return null;\n\n // In rail mode the body is always visible\n const isOpen = sidebarCollapsed ? true : open;\n\n const showHeader = !!(title || icon || (collapsible && !sidebarCollapsed));\n\n return (\n <section\n className={[styles.section, className].filter(Boolean).join(\" \")}\n {...props}\n >\n {showHeader && (\n collapsible && !sidebarCollapsed ? (\n <button\n type=\"button\"\n className={styles.sectionHeader}\n onClick={() => setAndNotify(!open)}\n aria-expanded={open}\n aria-controls={bodyId}\n >\n {icon && (\n <span className={styles.sectionHeaderIcon}>\n <Icon icon={icon} size=\"sm\" aria-hidden />\n </span>\n )}\n {title && <span className={styles.sectionTitle}>{title}</span>}\n <span className={styles.sectionChevron}>\n <Icon icon={open ? PanUp : PanDown} size=\"sm\" aria-hidden />\n </span>\n </button>\n ) : (\n <div className={styles.sectionHeader}>\n {icon && (\n <span className={styles.sectionHeaderIcon}>\n <Icon icon={icon} size=\"sm\" aria-hidden />\n </span>\n )}\n {title && <span className={styles.sectionTitle}>{title}</span>}\n </div>\n )\n )}\n\n <div\n id={bodyId}\n className={[\n styles.sectionBody,\n !isOpen ? styles.sectionBodyCollapsed : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n aria-hidden={!isOpen || undefined}\n >\n <div className={styles.sectionBodyInner}>\n <ul role=\"list\" className={styles.list}>\n {children}\n </ul>\n </div>\n </div>\n </section>\n );\n },\n);\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"SidebarSection.js","names":[],"sources":["../../../src/components/Sidebar/SidebarSection.tsx"],"sourcesContent":["import {\n forwardRef,\n useImperativeHandle,\n useState,\n useContext,\n useId,\n isValidElement,\n Children,\n type HTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport type { IconDefinition } from \"@gnome-ui/icons\";\nimport { PanDown, PanUp } from \"@gnome-ui/icons\";\nimport { Icon } from \"../Icon\";\nimport { useSidebarCollapsed, SidebarFilterContext } from \"./Sidebar\";\nimport styles from \"./Sidebar.module.css\";\n\n// ─── Filter helper ────────────────────────────────────────────────────────────\n\nfunction countMatchingItems(children: ReactNode, filter: string): number {\n let count = 0;\n Children.forEach(children, (child) => {\n if (!isValidElement(child)) return;\n const props = child.props as Record<string, unknown>;\n if (typeof props.label === \"string\") {\n if (props.label.toLowerCase().includes(filter.toLowerCase())) count++;\n } else if (props.children) {\n count += countMatchingItems(props.children as ReactNode, filter);\n }\n });\n return count;\n}\n\n// ─── Handle ───────────────────────────────────────────────────────────────────\n\nexport interface SidebarSectionHandle {\n expand: () => void;\n collapse: () => void;\n toggle: () => void;\n}\n\n// ─── Props ────────────────────────────────────────────────────────────────────\n\nexport interface SidebarSectionProps extends HTMLAttributes<HTMLElement> {\n /**\n * Section heading. Rendered in small caps above the items.\n * Omit for an untitled section (e.g. the first group in a sidebar).\n */\n title?: string;\n /** Icon rendered left of the title. Same type as `SidebarItem.icon`. */\n icon?: IconDefinition;\n /** Whether the section body can be toggled open/closed. Defaults to `false`. */\n collapsible?: boolean;\n /** Initial open state when `collapsible` is true. Defaults to `true`. */\n defaultOpen?: boolean;\n /** Controlled open state. */\n open?: boolean;\n /** Called when open state changes. */\n onOpenChange?: (open: boolean) => void;\n children?: ReactNode;\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * Named group of `SidebarItem` entries inside a `Sidebar`.\n *\n * Sections are separated by a thin divider. The title and icon are optional.\n * When `collapsible` is true the body can be toggled via the header button or\n * imperatively via a `ref` (`expand`, `collapse`, `toggle`).\n *\n * In rail (icon-only) mode the body is always visible regardless of open state.\n * When a sidebar filter is active and no children match, the section is hidden.\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Sidebar.html\n */\nexport const SidebarSection = forwardRef<SidebarSectionHandle, SidebarSectionProps>(\n function SidebarSection(\n {\n title,\n icon,\n collapsible = false,\n defaultOpen = true,\n open: controlledOpen,\n onOpenChange,\n children,\n className,\n ...props\n },\n ref,\n ) {\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n\n const sidebarCollapsed = useSidebarCollapsed();\n const filterValue = useContext(SidebarFilterContext);\n const bodyId = useId();\n\n const setAndNotify = (next: boolean) => {\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n useImperativeHandle(ref, () => ({\n expand: () => setAndNotify(true),\n collapse: () => setAndNotify(false),\n toggle: () => setAndNotify(!open),\n }));\n\n // Hide section entirely when filter is active and no children match\n const hasVisibleChildren =\n filterValue.length === 0 || countMatchingItems(children, filterValue) > 0;\n\n if (!hasVisibleChildren) return null;\n\n // In rail mode the body is always visible\n const isOpen = sidebarCollapsed ? true : open;\n\n const showHeader = !!(title || icon || (collapsible && !sidebarCollapsed));\n\n return (\n <section\n className={[styles.section, className].filter(Boolean).join(\" \")}\n {...props}\n >\n {showHeader && (\n collapsible && !sidebarCollapsed ? (\n <button\n type=\"button\"\n className={styles.sectionHeader}\n onClick={() => setAndNotify(!open)}\n aria-expanded={open}\n aria-controls={bodyId}\n >\n {icon && (\n <span className={styles.sectionHeaderIcon}>\n <Icon icon={icon} size=\"sm\" aria-hidden />\n </span>\n )}\n {title && <span className={styles.sectionTitle}>{title}</span>}\n <span className={styles.sectionChevron}>\n <Icon icon={open ? PanUp : PanDown} size=\"sm\" aria-hidden />\n </span>\n </button>\n ) : (\n <div className={styles.sectionHeader}>\n {icon && (\n <span className={styles.sectionHeaderIcon}>\n <Icon icon={icon} size=\"sm\" aria-hidden />\n </span>\n )}\n {title && <span className={styles.sectionTitle}>{title}</span>}\n </div>\n )\n )}\n\n <div\n id={bodyId}\n className={[\n styles.sectionBody,\n !isOpen ? styles.sectionBodyCollapsed : null,\n ]\n .filter(Boolean)\n .join(\" \")}\n aria-hidden={!isOpen || undefined}\n >\n <div className={styles.sectionBodyInner}>\n <ul role=\"list\" className={styles.list}>\n {children}\n </ul>\n </div>\n </div>\n </section>\n );\n },\n);\n"],"mappings":";;;;;;;AAmBA,SAAS,EAAmB,GAAqB,GAAwB;CACvE,IAAI,IAAQ;AAUZ,QATA,EAAS,QAAQ,IAAW,MAAU;AACpC,MAAI,CAAC,EAAe,EAAM,CAAE;EAC5B,IAAM,IAAQ,EAAM;AACpB,EAAI,OAAO,EAAM,SAAU,WACrB,EAAM,MAAM,aAAa,CAAC,SAAS,EAAO,aAAa,CAAC,IAAE,MACrD,EAAM,aACf,KAAS,EAAmB,EAAM,UAAuB,EAAO;GAElE,EACK;;AA8CT,IAAa,IAAiB,EAC5B,SACE,EACE,UACA,SACA,iBAAc,IACd,iBAAc,IACd,MAAM,GACN,iBACA,aACA,cACA,GAAG,KAEL,GACA;CACA,IAAM,CAAC,GAAc,KAAmB,EAAS,EAAY,EACvD,IAAe,MAAmB,KAAA,GAClC,IAAO,IAAe,IAAiB,GAEvC,IAAmB,GAAqB,EACxC,IAAc,EAAW,EAAqB,EAC9C,IAAS,GAAO,EAEhB,KAAgB,MAAkB;AAEtC,EADK,KAAc,EAAgB,EAAK,EACxC,IAAe,EAAK;;AAatB,KAVA,EAAoB,UAAY;EAC9B,cAAc,EAAa,GAAK;EAChC,gBAAgB,EAAa,GAAM;EACnC,cAAc,EAAa,CAAC,EAAK;EAClC,EAAE,EAMC,EAFF,EAAY,WAAW,KAAK,EAAmB,GAAU,EAAY,GAAG,GAEjD,QAAO;CAGhC,IAAM,IAAS,IAAmB,KAAO,GAEnC,IAAa,CAAC,EAAE,KAAS,KAAS,KAAe,CAAC;AAExD,QACE,kBAAC,WAAD;EACE,WAAW,CAAC,EAAO,SAAS,EAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;EAChE,GAAI;YAFN,CAIG,MACC,KAAe,CAAC,IACd,kBAAC,UAAD;GACE,MAAK;GACL,WAAW,EAAO;GAClB,eAAe,EAAa,CAAC,EAAK;GAClC,iBAAe;GACf,iBAAe;aALjB;IAOG,KACC,kBAAC,QAAD;KAAM,WAAW,EAAO;eACtB,kBAAC,GAAD;MAAY;MAAM,MAAK;MAAK,eAAA;MAAc,CAAA;KACrC,CAAA;IAER,KAAS,kBAAC,QAAD;KAAM,WAAW,EAAO;eAAe;KAAa,CAAA;IAC9D,kBAAC,QAAD;KAAM,WAAW,EAAO;eACtB,kBAAC,GAAD;MAAM,MAAM,IAAO,IAAQ;MAAS,MAAK;MAAK,eAAA;MAAc,CAAA;KACvD,CAAA;IACA;OAET,kBAAC,OAAD;GAAK,WAAW,EAAO;aAAvB,CACG,KACC,kBAAC,QAAD;IAAM,WAAW,EAAO;cACtB,kBAAC,GAAD;KAAY;KAAM,MAAK;KAAK,eAAA;KAAc,CAAA;IACrC,CAAA,EAER,KAAS,kBAAC,QAAD;IAAM,WAAW,EAAO;cAAe;IAAa,CAAA,CAC1D;OAIV,kBAAC,OAAD;GACE,IAAI;GACJ,WAAW,CACT,EAAO,aACN,IAAuC,OAA9B,EAAO,qBAClB,CACE,OAAO,QAAQ,CACf,KAAK,IAAI;GACZ,eAAa,CAAC,KAAU,KAAA;aAExB,kBAAC,OAAD;IAAK,WAAW,EAAO;cACrB,kBAAC,MAAD;KAAI,MAAK;KAAO,WAAW,EAAO;KAC/B;KACE,CAAA;IACD,CAAA;GACF,CAAA,CACE;;EAGf"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require(`../Icon/Icon.cjs`),t=require(
|
|
1
|
+
const e=require(`../Icon/Icon.cjs`),t=require(`./Toast.module.css.cjs`);let n=require(`react`),r=require(`react/jsx-runtime`),i=require(`@gnome-ui/icons`);function a({title:a,duration:o=3e3,onDismiss:s,actionLabel:c,onAction:l,dismissible:u=!1,className:d,...f}){let p=(0,n.useRef)(null),m=(0,n.useRef)(o),h=(0,n.useRef)(0),g=()=>{p.current&&=(clearTimeout(p.current),null)},_=e=>{e<=0||!s||(g(),h.current=Date.now(),p.current=setTimeout(()=>s(),e))};(0,n.useEffect)(()=>(o>0&&(m.current=o,_(o)),g),[o]);let v=()=>{if(p.current){let e=Date.now()-h.current;m.current=Math.max(0,m.current-e),g()}},y=()=>{_(m.current)},b=()=>{g(),l?.(),s?.()};return(0,r.jsxs)(`div`,{role:`status`,"aria-live":`polite`,"aria-atomic":`true`,className:[t.default.toast,d].filter(Boolean).join(` `),onMouseEnter:v,onMouseLeave:y,onFocus:v,onBlur:y,...f,children:[(0,r.jsx)(`span`,{className:t.default.title,children:a}),(c||u)&&(0,r.jsxs)(`span`,{className:t.default.actions,children:[c&&(0,r.jsx)(`button`,{type:`button`,className:t.default.actionBtn,onClick:b,children:c}),u&&(0,r.jsx)(`button`,{type:`button`,className:t.default.dismissBtn,"aria-label":`Dismiss`,onClick:()=>{g(),s?.()},children:(0,r.jsx)(e.Icon,{icon:i.Close,size:`md`,"aria-hidden":!0})})]})]})}exports.Toast=a;
|
|
2
2
|
//# sourceMappingURL=Toast.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toast.cjs","names":[],"sources":["../../../src/components/Toast/Toast.tsx"],"sourcesContent":["import {\n useEffect,\n useRef,\n type HTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport { Close } from \"@gnome-ui/icons\";\nimport { Icon } from \"../Icon\";\nimport styles from \"./Toast.module.css\";\n\nexport interface ToastProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** The notification message. Keep it short — one sentence. */\n title: ReactNode;\n /**\n * Auto-dismiss timeout in milliseconds.\n * - Set to `0` to disable auto-dismiss (user must click action or dismiss).\n * - Defaults to `3000`.\n */\n duration?: number;\n /** Called when the toast should be removed — after timeout or user action. */\n onDismiss?: () => void;\n /** Label for the optional action button. */\n actionLabel?: string;\n /** Called when the user clicks the action button. */\n onAction?: () => void;\n /** Whether to show a manual dismiss (×) button. */\n dismissible?: boolean;\n}\n\n/**\n * Non-blocking temporary notification following the Adwaita `AdwToast` pattern.\n *\n * - Auto-dismisses after `duration` ms (default 3 s). Set `duration={0}` to disable.\n * - Timer pauses while the toast is hovered or focused.\n * - Use inside `<Toaster>` for correct positioning and stacking.\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Toast.html\n */\nexport function Toast({\n title,\n duration = 3000,\n onDismiss,\n actionLabel,\n onAction,\n dismissible = false,\n className,\n ...props\n}: ToastProps) {\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const remainingRef = useRef(duration);\n const startedAtRef = useRef<number>(0);\n\n const clearTimer = () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n\n const startTimer = (ms: number) => {\n if (ms <= 0 || !onDismiss) return;\n clearTimer();\n startedAtRef.current = Date.now();\n timerRef.current = setTimeout(() => onDismiss(), ms);\n };\n\n // Start auto-dismiss on mount\n useEffect(() => {\n if (duration > 0) {\n remainingRef.current = duration;\n startTimer(duration);\n }\n return clearTimer;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [duration]);\n\n // Pause timer on hover/focus\n const handleMouseEnter = () => {\n if (timerRef.current) {\n const elapsed = Date.now() - startedAtRef.current;\n remainingRef.current = Math.max(0, remainingRef.current - elapsed);\n clearTimer();\n }\n };\n\n const handleMouseLeave = () => {\n startTimer(remainingRef.current);\n };\n\n const handleAction = () => {\n clearTimer();\n onAction?.();\n onDismiss?.();\n };\n\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n className={[styles.toast, className].filter(Boolean).join(\" \")}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n onFocus={handleMouseEnter}\n onBlur={handleMouseLeave}\n {...props}\n >\n <span className={styles.title}>{title}</span>\n\n {(actionLabel || dismissible) && (\n <span className={styles.actions}>\n {actionLabel && (\n <button\n type=\"button\"\n className={styles.actionBtn}\n onClick={handleAction}\n >\n {actionLabel}\n </button>\n )}\n {dismissible && (\n <button\n type=\"button\"\n className={styles.dismissBtn}\n aria-label=\"Dismiss\"\n onClick={() => { clearTimer(); onDismiss?.(); }}\n >\n <Icon icon={Close} size=\"md\" aria-hidden />\n </button>\n )}\n </span>\n )}\n </div>\n );\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"Toast.cjs","names":[],"sources":["../../../src/components/Toast/Toast.tsx"],"sourcesContent":["import {\n useEffect,\n useRef,\n type HTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport { Close } from \"@gnome-ui/icons\";\nimport { Icon } from \"../Icon\";\nimport styles from \"./Toast.module.css\";\n\nexport interface ToastProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** The notification message. Keep it short — one sentence. */\n title: ReactNode;\n /**\n * Auto-dismiss timeout in milliseconds.\n * - Set to `0` to disable auto-dismiss (user must click action or dismiss).\n * - Defaults to `3000`.\n */\n duration?: number;\n /** Called when the toast should be removed — after timeout or user action. */\n onDismiss?: () => void;\n /** Label for the optional action button. */\n actionLabel?: string;\n /** Called when the user clicks the action button. */\n onAction?: () => void;\n /** Whether to show a manual dismiss (×) button. */\n dismissible?: boolean;\n}\n\n/**\n * Non-blocking temporary notification following the Adwaita `AdwToast` pattern.\n *\n * - Auto-dismisses after `duration` ms (default 3 s). Set `duration={0}` to disable.\n * - Timer pauses while the toast is hovered or focused.\n * - Use inside `<Toaster>` for correct positioning and stacking.\n *\n * @see https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Toast.html\n */\nexport function Toast({\n title,\n duration = 3000,\n onDismiss,\n actionLabel,\n onAction,\n dismissible = false,\n className,\n ...props\n}: ToastProps) {\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const remainingRef = useRef(duration);\n const startedAtRef = useRef<number>(0);\n\n const clearTimer = () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n\n const startTimer = (ms: number) => {\n if (ms <= 0 || !onDismiss) return;\n clearTimer();\n startedAtRef.current = Date.now();\n timerRef.current = setTimeout(() => onDismiss(), ms);\n };\n\n // Start auto-dismiss on mount\n useEffect(() => {\n if (duration > 0) {\n remainingRef.current = duration;\n startTimer(duration);\n }\n return clearTimer;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [duration]);\n\n // Pause timer on hover/focus\n const handleMouseEnter = () => {\n if (timerRef.current) {\n const elapsed = Date.now() - startedAtRef.current;\n remainingRef.current = Math.max(0, remainingRef.current - elapsed);\n clearTimer();\n }\n };\n\n const handleMouseLeave = () => {\n startTimer(remainingRef.current);\n };\n\n const handleAction = () => {\n clearTimer();\n onAction?.();\n onDismiss?.();\n };\n\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n className={[styles.toast, className].filter(Boolean).join(\" \")}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n onFocus={handleMouseEnter}\n onBlur={handleMouseLeave}\n {...props}\n >\n <span className={styles.title}>{title}</span>\n\n {(actionLabel || dismissible) && (\n <span className={styles.actions}>\n {actionLabel && (\n <button\n type=\"button\"\n className={styles.actionBtn}\n onClick={handleAction}\n >\n {actionLabel}\n </button>\n )}\n {dismissible && (\n <button\n type=\"button\"\n className={styles.dismissBtn}\n aria-label=\"Dismiss\"\n onClick={() => { clearTimer(); onDismiss?.(); }}\n >\n <Icon icon={Close} size=\"md\" aria-hidden />\n </button>\n )}\n </span>\n )}\n </div>\n );\n}\n"],"mappings":"2JAsCA,SAAgB,EAAM,CACpB,QACA,WAAW,IACX,YACA,cACA,WACA,cAAc,GACd,YACA,GAAG,GACU,CACb,IAAM,GAAA,EAAA,EAAA,QAAwD,KAAK,CAC7D,GAAA,EAAA,EAAA,QAAsB,EAAS,CAC/B,GAAA,EAAA,EAAA,QAA8B,EAAE,CAEhC,MAAmB,CACvB,AAEE,EAAS,WADT,aAAa,EAAS,QAAQ,CACX,OAIjB,EAAc,GAAe,CAC7B,GAAM,GAAK,CAAC,IAChB,GAAY,CACZ,EAAa,QAAU,KAAK,KAAK,CACjC,EAAS,QAAU,eAAiB,GAAW,CAAE,EAAG,IAItD,EAAA,EAAA,gBACM,EAAW,IACb,EAAa,QAAU,EACvB,EAAW,EAAS,EAEf,GAEN,CAAC,EAAS,CAAC,CAGd,IAAM,MAAyB,CAC7B,GAAI,EAAS,QAAS,CACpB,IAAM,EAAU,KAAK,KAAK,CAAG,EAAa,QAC1C,EAAa,QAAU,KAAK,IAAI,EAAG,EAAa,QAAU,EAAQ,CAClE,GAAY,GAIV,MAAyB,CAC7B,EAAW,EAAa,QAAQ,EAG5B,MAAqB,CACzB,GAAY,CACZ,KAAY,CACZ,KAAa,EAGf,OACE,EAAA,EAAA,MAAC,MAAD,CACE,KAAK,SACL,YAAU,SACV,cAAY,OACZ,UAAW,CAAC,EAAA,QAAO,MAAO,EAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI,CAC9D,aAAc,EACd,aAAc,EACd,QAAS,EACT,OAAQ,EACR,GAAI,WATN,EAWE,EAAA,EAAA,KAAC,OAAD,CAAM,UAAW,EAAA,QAAO,eAAQ,EAAa,CAAA,EAE3C,GAAe,KACf,EAAA,EAAA,MAAC,OAAD,CAAM,UAAW,EAAA,QAAO,iBAAxB,CACG,IACC,EAAA,EAAA,KAAC,SAAD,CACE,KAAK,SACL,UAAW,EAAA,QAAO,UAClB,QAAS,WAER,EACM,CAAA,CAEV,IACC,EAAA,EAAA,KAAC,SAAD,CACE,KAAK,SACL,UAAW,EAAA,QAAO,WAClB,aAAW,UACX,YAAe,CAAE,GAAY,CAAE,KAAa,YAE5C,EAAA,EAAA,KAAC,EAAA,KAAD,CAAM,KAAM,EAAA,MAAO,KAAK,KAAK,cAAA,GAAc,CAAA,CACpC,CAAA,CAEN,GAEL"}
|