@hadoku/task-ui-components 4.23.14 → 4.23.15
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemePicker.d.ts","sourceRoot":"","sources":["../../src/components/ThemePicker.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"ThemePicker.d.ts","sourceRoot":"","sources":["../../src/components/ThemePicker.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAuD,MAAM,OAAO,CAAA;AAE3E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAIhD,OAAO,qBAAqB,CAAA;AAE5B;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,YAAY,EACZ,MAAM,EACN,aAAa,EACb,aAAa,EACb,QAAQ,EACR,eAAe,EACf,YAAY,EACZ,SAAc,EACd,iBAA0B,EAC3B,EAAE,gBAAgB,qBAsIlB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useState, useLayoutEffect } from "react";
|
|
2
|
+
import { useRef, useState, useLayoutEffect, useEffect } from "react";
|
|
3
3
|
import { Tooltip } from "./Tooltip.js";
|
|
4
4
|
import { getFallbackIcon, SettingsIcon } from "./ThemeIcons.js";
|
|
5
5
|
import { MoonIcon } from "@wolffm/themes";
|
|
@@ -16,6 +16,9 @@ function ThemePicker({
|
|
|
16
16
|
dropdownPlacement = "auto"
|
|
17
17
|
}) {
|
|
18
18
|
const dropdownRef = useRef(null);
|
|
19
|
+
const toggleRef = useRef(null);
|
|
20
|
+
const onToggleRef = useRef(onToggle);
|
|
21
|
+
onToggleRef.current = onToggle;
|
|
19
22
|
const [resolvedPlacement, setResolvedPlacement] = useState("right");
|
|
20
23
|
useLayoutEffect(() => {
|
|
21
24
|
if (!isOpen) return;
|
|
@@ -34,15 +37,32 @@ function ThemePicker({
|
|
|
34
37
|
});
|
|
35
38
|
return () => cancelAnimationFrame(frameId);
|
|
36
39
|
}, [isOpen, dropdownPlacement]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!isOpen) return;
|
|
42
|
+
const dropdown = dropdownRef.current;
|
|
43
|
+
const target = dropdown?.querySelector(".theme-pill__btn.active") ?? dropdown?.querySelector("button");
|
|
44
|
+
target?.focus();
|
|
45
|
+
const onKey = (e) => {
|
|
46
|
+
if (!isCloseKey(e)) return;
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
onToggleRef.current();
|
|
49
|
+
toggleRef.current?.focus();
|
|
50
|
+
};
|
|
51
|
+
window.addEventListener("keydown", onKey);
|
|
52
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
53
|
+
}, [isOpen]);
|
|
37
54
|
const currentIcon = getThemeIcon ? getThemeIcon(currentTheme) : /* @__PURE__ */ jsx(MoonIcon, {});
|
|
38
55
|
const openLeftClass = resolvedPlacement === "left" ? "theme-picker--open-left" : "";
|
|
39
56
|
return /* @__PURE__ */ jsxs("div", { className: `theme-picker ${openLeftClass} ${className}`.trim(), children: [
|
|
40
57
|
/* @__PURE__ */ jsx(Tooltip, { content: "Choose theme", children: /* @__PURE__ */ jsx(
|
|
41
58
|
"button",
|
|
42
59
|
{
|
|
60
|
+
ref: toggleRef,
|
|
43
61
|
className: "app-header__icon-btn theme-toggle-btn",
|
|
44
62
|
onClick: onToggle,
|
|
45
63
|
"aria-label": "Choose theme",
|
|
64
|
+
"aria-expanded": isOpen,
|
|
65
|
+
"aria-haspopup": "true",
|
|
46
66
|
children: currentIcon
|
|
47
67
|
}
|
|
48
68
|
) }),
|
|
@@ -95,6 +115,9 @@ function ThemePicker({
|
|
|
95
115
|
isOpen && /* @__PURE__ */ jsx("div", { className: "theme-picker__overlay", onClick: onToggle })
|
|
96
116
|
] });
|
|
97
117
|
}
|
|
118
|
+
function isCloseKey(e) {
|
|
119
|
+
return e.key === "Escape" || e.key === "GoBack" || e.key === "BrowserBack" || e.keyCode === 461 || e.keyCode === 10009;
|
|
120
|
+
}
|
|
98
121
|
export {
|
|
99
122
|
ThemePicker
|
|
100
123
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemePicker.js","sources":["../../src/components/ThemePicker.tsx"],"sourcesContent":["/**\n * ThemePicker component\n * A beautiful theme picker with light/dark variants for each theme family\n * Designed to work with any theme system via configuration\n */\n\nimport React, { useRef, useLayoutEffect, useState } from 'react'\nimport { Tooltip } from './Tooltip'\nimport type { ThemePickerProps } from '../types'\nimport { SettingsIcon, getFallbackIcon } from './ThemeIcons'\n// MoonIcon is a theme-family icon and lives with the theme model now.\nimport { MoonIcon } from '@wolffm/themes'\nimport '../theme-picker.css'\n\n/**\n * ThemePicker - Dropdown theme selector with optional settings button\n *\n * @example\n * ```tsx\n * import { ThemePicker } from '@wolffm/task-ui-components'\n * import '@wolffm/task-ui-components/theme-picker.css'\n *\n * <ThemePicker\n * currentTheme=\"dark\"\n * isOpen={showPicker}\n * themeFamilies={MY_THEME_FAMILIES}\n * onThemeChange={setTheme}\n * onToggle={() => setShowPicker(!showPicker)}\n * onSettingsClick={() => openSettings()}\n * />\n * ```\n */\nexport function ThemePicker({\n currentTheme,\n isOpen,\n themeFamilies,\n onThemeChange,\n onToggle,\n onSettingsClick,\n getThemeIcon,\n className = '',\n dropdownPlacement = 'auto'\n}: ThemePickerProps) {\n const dropdownRef = useRef<HTMLDivElement>(null)\n const [resolvedPlacement, setResolvedPlacement] = useState<'left' | 'right'>('right')\n\n // Edge detection: when 'auto', measure the dropdown after render and flip if it overflows\n useLayoutEffect(() => {\n if (!isOpen) return\n\n if (dropdownPlacement !== 'auto') {\n setResolvedPlacement(dropdownPlacement)\n return\n }\n\n // Start with right (default), then check if it overflows\n setResolvedPlacement('right')\n\n // Use rAF to measure after the browser has laid out the dropdown\n const frameId = requestAnimationFrame(() => {\n const el = dropdownRef.current\n if (!el) return\n const rect = el.getBoundingClientRect()\n if (rect.right > window.innerWidth) {\n setResolvedPlacement('left')\n }\n })\n\n return () => cancelAnimationFrame(frameId)\n }, [isOpen, dropdownPlacement])\n\n // Get icon for current theme (use provided function or fallback to MoonIcon)\n const currentIcon = getThemeIcon ? getThemeIcon(currentTheme) : <MoonIcon />\n\n const openLeftClass = resolvedPlacement === 'left' ? 'theme-picker--open-left' : ''\n\n return (\n <div className={`theme-picker ${openLeftClass} ${className}`.trim()}>\n <Tooltip content=\"Choose theme\">\n <button\n className=\"app-header__icon-btn theme-toggle-btn\"\n onClick={onToggle}\n aria-label=\"Choose theme\"\n >\n {currentIcon}\n </button>\n </Tooltip>\n {isOpen && (\n <div\n ref={dropdownRef}\n className=\"theme-picker__dropdown\"\n onClick={e => e.stopPropagation()}\n >\n <div className=\"theme-picker__main\">\n <div className=\"theme-picker__pills\">\n {themeFamilies.map((family, idx) => {\n // Use provided icons or fallback to generic shapes\n const lightIcon = family.lightIcon ?? getFallbackIcon(idx)\n const darkIcon = family.darkIcon ?? getFallbackIcon(idx)\n\n return (\n <div key={idx} className=\"theme-pill\">\n {/* Light variant button */}\n <Tooltip content={family.lightLabel}>\n <button\n className={`theme-pill__btn theme-pill__btn--light ${currentTheme === family.lightTheme ? 'active' : ''}`}\n onClick={() => onThemeChange(family.lightTheme)}\n aria-label={family.lightLabel}\n >\n <div className=\"theme-pill__icon\">{lightIcon}</div>\n </button>\n </Tooltip>\n\n {/* Dark variant button */}\n <Tooltip content={family.darkLabel}>\n <button\n className={`theme-pill__btn theme-pill__btn--dark ${currentTheme === family.darkTheme ? 'active' : ''}`}\n onClick={() => onThemeChange(family.darkTheme)}\n aria-label={family.darkLabel}\n >\n <div className=\"theme-pill__icon\">{darkIcon}</div>\n </button>\n </Tooltip>\n </div>\n )\n })}\n </div>\n {/* Settings button - separate column on the right */}\n {onSettingsClick && (\n <Tooltip content=\"Settings\">\n <button\n className=\"theme-picker__settings-icon\"\n onClick={() => {\n onSettingsClick()\n onToggle() // Close picker after opening settings\n }}\n aria-label=\"Settings\"\n >\n <SettingsIcon />\n </button>\n </Tooltip>\n )}\n </div>\n </div>\n )}\n {isOpen && <div className=\"theme-picker__overlay\" onClick={onToggle} />}\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;AAgCO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,oBAAoB;AACtB,GAAqB;AACnB,QAAM,cAAc,OAAuB,IAAI;AAC/C,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAA2B,OAAO;AAGpF,kBAAgB,MAAM;AACpB,QAAI,CAAC,OAAQ;AAEb,QAAI,sBAAsB,QAAQ;AAChC,2BAAqB,iBAAiB;AACtC;AAAA,IACF;AAGA,yBAAqB,OAAO;AAG5B,UAAM,UAAU,sBAAsB,MAAM;AAC1C,YAAM,KAAK,YAAY;AACvB,UAAI,CAAC,GAAI;AACT,YAAM,OAAO,GAAG,sBAAA;AAChB,UAAI,KAAK,QAAQ,OAAO,YAAY;AAClC,6BAAqB,MAAM;AAAA,MAC7B;AAAA,IACF,CAAC;AAED,WAAO,MAAM,qBAAqB,OAAO;AAAA,EAC3C,GAAG,CAAC,QAAQ,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"ThemePicker.js","sources":["../../src/components/ThemePicker.tsx"],"sourcesContent":["/**\n * ThemePicker component\n * A beautiful theme picker with light/dark variants for each theme family\n * Designed to work with any theme system via configuration\n */\n\nimport React, { useEffect, useRef, useLayoutEffect, useState } from 'react'\nimport { Tooltip } from './Tooltip'\nimport type { ThemePickerProps } from '../types'\nimport { SettingsIcon, getFallbackIcon } from './ThemeIcons'\n// MoonIcon is a theme-family icon and lives with the theme model now.\nimport { MoonIcon } from '@wolffm/themes'\nimport '../theme-picker.css'\n\n/**\n * ThemePicker - Dropdown theme selector with optional settings button\n *\n * @example\n * ```tsx\n * import { ThemePicker } from '@wolffm/task-ui-components'\n * import '@wolffm/task-ui-components/theme-picker.css'\n *\n * <ThemePicker\n * currentTheme=\"dark\"\n * isOpen={showPicker}\n * themeFamilies={MY_THEME_FAMILIES}\n * onThemeChange={setTheme}\n * onToggle={() => setShowPicker(!showPicker)}\n * onSettingsClick={() => openSettings()}\n * />\n * ```\n */\nexport function ThemePicker({\n currentTheme,\n isOpen,\n themeFamilies,\n onThemeChange,\n onToggle,\n onSettingsClick,\n getThemeIcon,\n className = '',\n dropdownPlacement = 'auto'\n}: ThemePickerProps) {\n const dropdownRef = useRef<HTMLDivElement>(null)\n const toggleRef = useRef<HTMLButtonElement>(null)\n // The latest onToggle, so the open/close effect below runs on isOpen alone:\n // callers pass a new arrow every render, and re-running it would steal focus.\n const onToggleRef = useRef(onToggle)\n onToggleRef.current = onToggle\n const [resolvedPlacement, setResolvedPlacement] = useState<'left' | 'right'>('right')\n\n // Edge detection: when 'auto', measure the dropdown after render and flip if it overflows\n useLayoutEffect(() => {\n if (!isOpen) return\n\n if (dropdownPlacement !== 'auto') {\n setResolvedPlacement(dropdownPlacement)\n return\n }\n\n // Start with right (default), then check if it overflows\n setResolvedPlacement('right')\n\n // Use rAF to measure after the browser has laid out the dropdown\n const frameId = requestAnimationFrame(() => {\n const el = dropdownRef.current\n if (!el) return\n const rect = el.getBoundingClientRect()\n if (rect.right > window.innerWidth) {\n setResolvedPlacement('left')\n }\n })\n\n return () => cancelAnimationFrame(frameId)\n }, [isOpen, dropdownPlacement])\n\n // Keyboard and TV remote: opening moves focus into the dropdown (onto the\n // theme in use), and Escape or a TV's Back key closes it and hands focus back\n // to the toggle. Without this a remote could open the picker and never leave.\n useEffect(() => {\n if (!isOpen) return\n const dropdown = dropdownRef.current\n const target =\n dropdown?.querySelector<HTMLButtonElement>('.theme-pill__btn.active') ??\n dropdown?.querySelector<HTMLButtonElement>('button')\n target?.focus()\n const onKey = (e: KeyboardEvent) => {\n if (!isCloseKey(e)) return\n e.preventDefault()\n onToggleRef.current()\n toggleRef.current?.focus()\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [isOpen])\n\n // Get icon for current theme (use provided function or fallback to MoonIcon)\n const currentIcon = getThemeIcon ? getThemeIcon(currentTheme) : <MoonIcon />\n\n const openLeftClass = resolvedPlacement === 'left' ? 'theme-picker--open-left' : ''\n\n return (\n <div className={`theme-picker ${openLeftClass} ${className}`.trim()}>\n <Tooltip content=\"Choose theme\">\n <button\n ref={toggleRef}\n className=\"app-header__icon-btn theme-toggle-btn\"\n onClick={onToggle}\n aria-label=\"Choose theme\"\n aria-expanded={isOpen}\n aria-haspopup=\"true\"\n >\n {currentIcon}\n </button>\n </Tooltip>\n {isOpen && (\n <div\n ref={dropdownRef}\n className=\"theme-picker__dropdown\"\n onClick={e => e.stopPropagation()}\n >\n <div className=\"theme-picker__main\">\n <div className=\"theme-picker__pills\">\n {themeFamilies.map((family, idx) => {\n // Use provided icons or fallback to generic shapes\n const lightIcon = family.lightIcon ?? getFallbackIcon(idx)\n const darkIcon = family.darkIcon ?? getFallbackIcon(idx)\n\n return (\n <div key={idx} className=\"theme-pill\">\n {/* Light variant button */}\n <Tooltip content={family.lightLabel}>\n <button\n className={`theme-pill__btn theme-pill__btn--light ${currentTheme === family.lightTheme ? 'active' : ''}`}\n onClick={() => onThemeChange(family.lightTheme)}\n aria-label={family.lightLabel}\n >\n <div className=\"theme-pill__icon\">{lightIcon}</div>\n </button>\n </Tooltip>\n\n {/* Dark variant button */}\n <Tooltip content={family.darkLabel}>\n <button\n className={`theme-pill__btn theme-pill__btn--dark ${currentTheme === family.darkTheme ? 'active' : ''}`}\n onClick={() => onThemeChange(family.darkTheme)}\n aria-label={family.darkLabel}\n >\n <div className=\"theme-pill__icon\">{darkIcon}</div>\n </button>\n </Tooltip>\n </div>\n )\n })}\n </div>\n {/* Settings button - separate column on the right */}\n {onSettingsClick && (\n <Tooltip content=\"Settings\">\n <button\n className=\"theme-picker__settings-icon\"\n onClick={() => {\n onSettingsClick()\n onToggle() // Close picker after opening settings\n }}\n aria-label=\"Settings\"\n >\n <SettingsIcon />\n </button>\n </Tooltip>\n )}\n </div>\n </div>\n )}\n {isOpen && <div className=\"theme-picker__overlay\" onClick={onToggle} />}\n </div>\n )\n}\n\n/** Escape, or a TV remote's Back: GoBack/BrowserBack, webOS 461, Tizen 10009. */\nfunction isCloseKey(e: KeyboardEvent): boolean {\n return (\n e.key === 'Escape' ||\n e.key === 'GoBack' ||\n e.key === 'BrowserBack' ||\n e.keyCode === 461 ||\n e.keyCode === 10009\n )\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hadoku/task-ui-components",
|
|
3
|
-
"version": "4.23.
|
|
4
|
-
"gitHead": "
|
|
3
|
+
"version": "4.23.15",
|
|
4
|
+
"gitHead": "0d94c5ddb49d5f9eae0d8f36504010489218fe40",
|
|
5
5
|
"description": "Reusable UI components with Bento grid and metallic gradient support",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Designed to work with any theme system via configuration
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, { useRef, useLayoutEffect, useState } from 'react'
|
|
7
|
+
import React, { useEffect, useRef, useLayoutEffect, useState } from 'react'
|
|
8
8
|
import { Tooltip } from './Tooltip'
|
|
9
9
|
import type { ThemePickerProps } from '../types'
|
|
10
10
|
import { SettingsIcon, getFallbackIcon } from './ThemeIcons'
|
|
@@ -42,6 +42,11 @@ export function ThemePicker({
|
|
|
42
42
|
dropdownPlacement = 'auto'
|
|
43
43
|
}: ThemePickerProps) {
|
|
44
44
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
45
|
+
const toggleRef = useRef<HTMLButtonElement>(null)
|
|
46
|
+
// The latest onToggle, so the open/close effect below runs on isOpen alone:
|
|
47
|
+
// callers pass a new arrow every render, and re-running it would steal focus.
|
|
48
|
+
const onToggleRef = useRef(onToggle)
|
|
49
|
+
onToggleRef.current = onToggle
|
|
45
50
|
const [resolvedPlacement, setResolvedPlacement] = useState<'left' | 'right'>('right')
|
|
46
51
|
|
|
47
52
|
// Edge detection: when 'auto', measure the dropdown after render and flip if it overflows
|
|
@@ -69,6 +74,26 @@ export function ThemePicker({
|
|
|
69
74
|
return () => cancelAnimationFrame(frameId)
|
|
70
75
|
}, [isOpen, dropdownPlacement])
|
|
71
76
|
|
|
77
|
+
// Keyboard and TV remote: opening moves focus into the dropdown (onto the
|
|
78
|
+
// theme in use), and Escape or a TV's Back key closes it and hands focus back
|
|
79
|
+
// to the toggle. Without this a remote could open the picker and never leave.
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!isOpen) return
|
|
82
|
+
const dropdown = dropdownRef.current
|
|
83
|
+
const target =
|
|
84
|
+
dropdown?.querySelector<HTMLButtonElement>('.theme-pill__btn.active') ??
|
|
85
|
+
dropdown?.querySelector<HTMLButtonElement>('button')
|
|
86
|
+
target?.focus()
|
|
87
|
+
const onKey = (e: KeyboardEvent) => {
|
|
88
|
+
if (!isCloseKey(e)) return
|
|
89
|
+
e.preventDefault()
|
|
90
|
+
onToggleRef.current()
|
|
91
|
+
toggleRef.current?.focus()
|
|
92
|
+
}
|
|
93
|
+
window.addEventListener('keydown', onKey)
|
|
94
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
95
|
+
}, [isOpen])
|
|
96
|
+
|
|
72
97
|
// Get icon for current theme (use provided function or fallback to MoonIcon)
|
|
73
98
|
const currentIcon = getThemeIcon ? getThemeIcon(currentTheme) : <MoonIcon />
|
|
74
99
|
|
|
@@ -78,9 +103,12 @@ export function ThemePicker({
|
|
|
78
103
|
<div className={`theme-picker ${openLeftClass} ${className}`.trim()}>
|
|
79
104
|
<Tooltip content="Choose theme">
|
|
80
105
|
<button
|
|
106
|
+
ref={toggleRef}
|
|
81
107
|
className="app-header__icon-btn theme-toggle-btn"
|
|
82
108
|
onClick={onToggle}
|
|
83
109
|
aria-label="Choose theme"
|
|
110
|
+
aria-expanded={isOpen}
|
|
111
|
+
aria-haspopup="true"
|
|
84
112
|
>
|
|
85
113
|
{currentIcon}
|
|
86
114
|
</button>
|
|
@@ -147,3 +175,14 @@ export function ThemePicker({
|
|
|
147
175
|
</div>
|
|
148
176
|
)
|
|
149
177
|
}
|
|
178
|
+
|
|
179
|
+
/** Escape, or a TV remote's Back: GoBack/BrowserBack, webOS 461, Tizen 10009. */
|
|
180
|
+
function isCloseKey(e: KeyboardEvent): boolean {
|
|
181
|
+
return (
|
|
182
|
+
e.key === 'Escape' ||
|
|
183
|
+
e.key === 'GoBack' ||
|
|
184
|
+
e.key === 'BrowserBack' ||
|
|
185
|
+
e.keyCode === 461 ||
|
|
186
|
+
e.keyCode === 10009
|
|
187
|
+
)
|
|
188
|
+
}
|