@decky/ui 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +504 -0
  2. package/README.md +27 -0
  3. package/dist/class-mapper.d.ts +7 -0
  4. package/dist/class-mapper.js +26 -0
  5. package/dist/components/Button.d.ts +5 -0
  6. package/dist/components/Button.js +2 -0
  7. package/dist/components/ButtonItem.d.ts +7 -0
  8. package/dist/components/ButtonItem.js +3 -0
  9. package/dist/components/Carousel.d.ts +19 -0
  10. package/dist/components/Carousel.js +2 -0
  11. package/dist/components/ControlsList.d.ts +6 -0
  12. package/dist/components/ControlsList.js +2 -0
  13. package/dist/components/Dialog.d.ts +32 -0
  14. package/dist/components/Dialog.js +19 -0
  15. package/dist/components/DialogCheckbox.d.ts +17 -0
  16. package/dist/components/DialogCheckbox.js +14 -0
  17. package/dist/components/Dropdown.d.ts +34 -0
  18. package/dist/components/Dropdown.js +3 -0
  19. package/dist/components/Field.d.ts +22 -0
  20. package/dist/components/Field.js +2 -0
  21. package/dist/components/FocusRing.d.ts +9 -0
  22. package/dist/components/FocusRing.js +2 -0
  23. package/dist/components/Focusable.d.ts +12 -0
  24. package/dist/components/Focusable.js +2 -0
  25. package/dist/components/FooterLegend.d.ts +66 -0
  26. package/dist/components/FooterLegend.js +32 -0
  27. package/dist/components/Item.d.ts +11 -0
  28. package/dist/components/Item.js +1 -0
  29. package/dist/components/Marquee.d.ts +14 -0
  30. package/dist/components/Marquee.js +2 -0
  31. package/dist/components/Menu.d.ts +29 -0
  32. package/dist/components/Menu.js +8 -0
  33. package/dist/components/Modal.d.ts +52 -0
  34. package/dist/components/Modal.js +29 -0
  35. package/dist/components/Panel.d.ts +11 -0
  36. package/dist/components/Panel.js +4 -0
  37. package/dist/components/ProgressBar.d.ts +21 -0
  38. package/dist/components/ProgressBar.js +4 -0
  39. package/dist/components/Scroll.d.ts +7 -0
  40. package/dist/components/Scroll.js +5 -0
  41. package/dist/components/SidebarNavigation.d.ts +21 -0
  42. package/dist/components/SidebarNavigation.js +2 -0
  43. package/dist/components/SliderField.d.ts +26 -0
  44. package/dist/components/SliderField.js +2 -0
  45. package/dist/components/Spinner.d.ts +2 -0
  46. package/dist/components/Spinner.js +2 -0
  47. package/dist/components/SteamSpinner.d.ts +2 -0
  48. package/dist/components/SteamSpinner.js +2 -0
  49. package/dist/components/Tabs.d.ts +16 -0
  50. package/dist/components/Tabs.js +48 -0
  51. package/dist/components/TextField.d.ts +22 -0
  52. package/dist/components/TextField.js +2 -0
  53. package/dist/components/Toggle.d.ts +8 -0
  54. package/dist/components/Toggle.js +2 -0
  55. package/dist/components/ToggleField.d.ts +9 -0
  56. package/dist/components/ToggleField.js +2 -0
  57. package/dist/components/index.d.ts +25 -0
  58. package/dist/components/index.js +25 -0
  59. package/dist/custom-components/ColorPickerModal.d.ts +12 -0
  60. package/dist/custom-components/ColorPickerModal.js +87 -0
  61. package/dist/custom-components/ReorderableList.d.ts +26 -0
  62. package/dist/custom-components/ReorderableList.js +84 -0
  63. package/dist/custom-components/SuspensefulImage.d.ts +7 -0
  64. package/dist/custom-components/SuspensefulImage.js +27 -0
  65. package/dist/custom-components/index.d.ts +3 -0
  66. package/dist/custom-components/index.js +3 -0
  67. package/dist/custom-hooks/index.d.ts +1 -0
  68. package/dist/custom-hooks/index.js +1 -0
  69. package/dist/custom-hooks/useQuickAccessVisible.d.ts +1 -0
  70. package/dist/custom-hooks/useQuickAccessVisible.js +22 -0
  71. package/dist/deck-hooks/index.d.ts +1 -0
  72. package/dist/deck-hooks/index.js +1 -0
  73. package/dist/deck-hooks/useParams.d.ts +1 -0
  74. package/dist/deck-hooks/useParams.js +2 -0
  75. package/dist/globals/SteamClient.d.ts +307 -0
  76. package/dist/globals/SteamClient.js +1 -0
  77. package/dist/globals/index.d.ts +2 -0
  78. package/dist/globals/index.js +2 -0
  79. package/dist/globals/stores.d.ts +50 -0
  80. package/dist/globals/stores.js +1 -0
  81. package/dist/index.d.ts +10 -0
  82. package/dist/index.js +10 -0
  83. package/dist/logger.d.ts +17 -0
  84. package/dist/logger.js +46 -0
  85. package/dist/modules/Router.d.ts +110 -0
  86. package/dist/modules/Router.js +102 -0
  87. package/dist/modules/index.d.ts +1 -0
  88. package/dist/modules/index.js +1 -0
  89. package/dist/plugin.d.ts +68 -0
  90. package/dist/plugin.js +5 -0
  91. package/dist/utils/index.d.ts +12 -0
  92. package/dist/utils/index.js +23 -0
  93. package/dist/utils/patcher.d.ts +18 -0
  94. package/dist/utils/patcher.js +103 -0
  95. package/dist/utils/react.d.ts +18 -0
  96. package/dist/utils/react.js +74 -0
  97. package/dist/utils/static-classes.d.ts +52 -0
  98. package/dist/utils/static-classes.js +28 -0
  99. package/dist/webpack.d.ts +21 -0
  100. package/dist/webpack.js +102 -0
  101. package/package.json +82 -0
  102. package/src/class-mapper.ts +34 -0
  103. package/src/components/Button.tsx +8 -0
  104. package/src/components/ButtonItem.tsx +14 -0
  105. package/src/components/Carousel.ts +25 -0
  106. package/src/components/ControlsList.tsx +13 -0
  107. package/src/components/Dialog.tsx +86 -0
  108. package/src/components/DialogCheckbox.tsx +36 -0
  109. package/src/components/Dropdown.tsx +49 -0
  110. package/src/components/Field.tsx +28 -0
  111. package/src/components/FocusRing.ts +15 -0
  112. package/src/components/Focusable.tsx +18 -0
  113. package/src/components/FooterLegend.ts +67 -0
  114. package/src/components/Item.tsx +12 -0
  115. package/src/components/Marquee.tsx +20 -0
  116. package/src/components/Menu.tsx +57 -0
  117. package/src/components/Modal.tsx +119 -0
  118. package/src/components/Panel.tsx +26 -0
  119. package/src/components/ProgressBar.tsx +35 -0
  120. package/src/components/Scroll.tsx +15 -0
  121. package/src/components/SidebarNavigation.tsx +28 -0
  122. package/src/components/SliderField.tsx +33 -0
  123. package/src/components/Spinner.tsx +8 -0
  124. package/src/components/SteamSpinner.tsx +7 -0
  125. package/src/components/Tabs.tsx +127 -0
  126. package/src/components/TextField.tsx +28 -0
  127. package/src/components/Toggle.tsx +14 -0
  128. package/src/components/ToggleField.tsx +15 -0
  129. package/src/components/index.ts +25 -0
  130. package/src/custom-components/ColorPickerModal.tsx +132 -0
  131. package/src/custom-components/ReorderableList.tsx +183 -0
  132. package/src/custom-components/SuspensefulImage.tsx +44 -0
  133. package/src/custom-components/index.ts +3 -0
  134. package/src/custom-hooks/index.ts +1 -0
  135. package/src/custom-hooks/useQuickAccessVisible.tsx +63 -0
  136. package/src/deck-hooks/index.ts +1 -0
  137. package/src/deck-hooks/useParams.ts +15 -0
  138. package/src/globals/SteamClient.ts +321 -0
  139. package/src/globals/index.ts +2 -0
  140. package/src/globals/stores.ts +50 -0
  141. package/src/index.ts +11 -0
  142. package/src/logger.ts +77 -0
  143. package/src/modules/Router.tsx +174 -0
  144. package/src/modules/index.ts +1 -0
  145. package/src/plugin.tsx +96 -0
  146. package/src/utils/index.ts +43 -0
  147. package/src/utils/patcher.ts +160 -0
  148. package/src/utils/react.ts +119 -0
  149. package/src/utils/static-classes.ts +1104 -0
  150. package/src/webpack.ts +136 -0
@@ -0,0 +1,119 @@
1
+ import { FC, ReactNode } from 'react';
2
+
3
+ import { findSP } from '../utils';
4
+ import { Export, findModule, findModuleByExport, findModuleExport } from '../webpack';
5
+
6
+ // All of the popout options + strTitle are related. Proper usage is not yet known...
7
+ export interface ShowModalProps {
8
+ browserContext?: unknown;
9
+ bForcePopOut?: boolean;
10
+ bHideActionIcons?: boolean;
11
+ bHideMainWindowForPopouts?: boolean;
12
+ bNeverPopOut?: boolean;
13
+ fnOnClose?: () => void; // Seems to be the same as "closeModal" callback, but only when the modal is a popout. Will no longer work after "Update" invocation!
14
+ popupHeight?: number;
15
+ popupWidth?: number;
16
+ promiseRenderComplete?: Promise<void>; // Invoked once the render is complete. Currently, it seems to be used as image loading success/error callback...
17
+ strTitle?: string;
18
+ }
19
+
20
+ export interface ShowModalResult {
21
+ // This method will not invoke any of the variations of "closeModal" callbacks!
22
+ Close: () => void;
23
+
24
+ // This method will replace the modal element completely and will not update the callback chains,
25
+ // meaning that "closeModal" and etc. will not automatically close the modal anymore (also "fnOnClose"
26
+ // will not be even called upon close anymore)! You have to manually call the "Close" method when, for example,
27
+ // the "closeModal" is invoked in the newly updated modal:
28
+ // <ModalRoot closeModal={() => { console.log("ABOUT TO CLOSE"); showModalRes.Close(); }} />
29
+ Update: (modal: ReactNode) => void;
30
+ }
31
+
32
+ const showModalRaw: (
33
+ modal: ReactNode,
34
+ parent?: EventTarget,
35
+ title?: string,
36
+ props?: ShowModalProps,
37
+ unknown1?: unknown,
38
+ hideActions?: { bHideActions?: boolean },
39
+ modalManager?: unknown,
40
+ ) => ShowModalResult = findModuleExport(
41
+ (e: Export) =>
42
+ typeof e === 'function' && e.toString().includes('props.bDisableBackgroundDismiss') && !e?.prototype?.Cancel,
43
+ );
44
+
45
+ export const showModal = (
46
+ modal: ReactNode,
47
+ parent?: EventTarget,
48
+ props: ShowModalProps = {
49
+ strTitle: 'Decky Dialog',
50
+ bHideMainWindowForPopouts: false,
51
+ },
52
+ ): ShowModalResult => {
53
+ return showModalRaw(modal, parent || findSP(), props.strTitle, props, undefined, {
54
+ bHideActions: props.bHideActionIcons,
55
+ });
56
+ };
57
+
58
+ export interface ModalRootProps {
59
+ children?: ReactNode;
60
+ onCancel?(): void;
61
+ closeModal?(): void;
62
+ onOK?(): void;
63
+ onEscKeypress?(): void;
64
+ className?: string;
65
+ modalClassName?: string;
66
+ bAllowFullSize?: boolean;
67
+ bDestructiveWarning?: boolean;
68
+ bDisableBackgroundDismiss?: boolean;
69
+ bHideCloseIcon?: boolean;
70
+ bOKDisabled?: boolean;
71
+ bCancelDisabled?: boolean;
72
+ }
73
+
74
+ export interface ConfirmModalProps extends ModalRootProps {
75
+ onMiddleButton?(): void; // setting this prop will enable the middle button
76
+ strTitle?: ReactNode;
77
+ strDescription?: ReactNode;
78
+ strOKButtonText?: ReactNode;
79
+ strCancelButtonText?: ReactNode;
80
+ strMiddleButtonText?: ReactNode;
81
+ bAlertDialog?: boolean; // This will open a modal with only OK button enabled
82
+ bMiddleDisabled?: boolean;
83
+ }
84
+
85
+ export const ConfirmModal = findModuleExport(
86
+ (e: Export) => !e?.prototype?.OK && e?.prototype?.Cancel && e?.prototype?.render,
87
+ ) as FC<ConfirmModalProps>;
88
+
89
+ export const ModalRoot = Object.values(
90
+ findModule((m: any) => {
91
+ if (typeof m !== 'object') return false;
92
+
93
+ for (let prop in m) {
94
+ if (m[prop]?.m_mapModalManager && Object.values(m)?.find((x: any) => x?.type)) {
95
+ return true;
96
+ }
97
+ }
98
+
99
+ return false;
100
+ }) || {},
101
+ )?.find((x: any) => x?.type?.toString()?.includes('((function(){')) as FC<ModalRootProps>;
102
+
103
+ interface SimpleModalProps {
104
+ active?: boolean;
105
+ children: ReactNode;
106
+ }
107
+
108
+ const ModalModule = findModuleByExport((e: Export) => e?.toString().includes('.ModalPosition,fallback:'), 5);
109
+
110
+ const ModalModuleProps = ModalModule ? Object.values(ModalModule) : [];
111
+
112
+ export const SimpleModal = ModalModuleProps.find((prop) => {
113
+ const string = prop?.toString();
114
+ return string?.includes('.ShowPortalModal()') && string?.includes('.OnElementReadyCallbacks.Register(');
115
+ }) as FC<SimpleModalProps>;
116
+
117
+ export const ModalPosition = ModalModuleProps.find((prop) =>
118
+ prop?.toString().includes('.ModalPosition,fallback:'),
119
+ ) as FC<SimpleModalProps>;
@@ -0,0 +1,26 @@
1
+ import { FC, ReactNode } from 'react';
2
+
3
+ import { Export, findModuleDetailsByExport } from '../webpack';
4
+
5
+ // TODO where did this go?
6
+ // export const Panel: FC<{ children?: ReactNode; }> = findModuleExport((e: Export) => {
7
+ // if (typeof mod !== 'object' || !mod.__esModule) return undefined;
8
+ // return mod.Panel;
9
+ // });
10
+
11
+ export interface PanelSectionProps {
12
+ title?: string;
13
+ spinner?: boolean;
14
+ children?: ReactNode;
15
+ }
16
+
17
+ const [mod, panelSection] = findModuleDetailsByExport((e: Export) => e.toString()?.includes('.PanelSection'));
18
+
19
+ export const PanelSection = panelSection as FC<PanelSectionProps>;
20
+
21
+ export interface PanelSectionRowProps {
22
+ children?: ReactNode;
23
+ }
24
+ export const PanelSectionRow = Object.values(mod).filter(
25
+ (exp: any) => !exp?.toString()?.includes('.PanelSection'),
26
+ )[0] as FC<PanelSectionRowProps>;
@@ -0,0 +1,35 @@
1
+ import { ReactNode, VFC } from 'react';
2
+
3
+ import { Export, findModuleExport } from '../webpack';
4
+ import { ItemProps } from './Item';
5
+
6
+ export interface ProgressBarItemProps extends ItemProps {
7
+ indeterminate?: boolean;
8
+ nTransitionSec?: number;
9
+ nProgress?: number;
10
+ focusable?: boolean;
11
+ }
12
+
13
+ export interface ProgressBarProps {
14
+ indeterminate?: boolean;
15
+ nTransitionSec?: number;
16
+ nProgress?: number;
17
+ focusable?: boolean;
18
+ }
19
+
20
+ export interface ProgressBarWithInfoProps extends ProgressBarItemProps {
21
+ sTimeRemaining?: ReactNode;
22
+ sOperationText?: ReactNode;
23
+ }
24
+
25
+ export const ProgressBar = findModuleExport((e: Export) =>
26
+ e?.toString()?.includes('.ProgressBar,"standard"=='),
27
+ ) as VFC<ProgressBarProps>;
28
+
29
+ export const ProgressBarWithInfo = findModuleExport((e: Export) =>
30
+ e?.toString()?.includes('.ProgressBarFieldStatus},'),
31
+ ) as VFC<ProgressBarWithInfoProps>;
32
+
33
+ export const ProgressBarItem = findModuleExport((e: Export) =>
34
+ e?.toString()?.includes('"indeterminate","nTransitionSec"'),
35
+ ) as VFC<ProgressBarItemProps>;
@@ -0,0 +1,15 @@
1
+ import { FC, ReactNode } from 'react';
2
+
3
+ import { Export, findModuleByExport, findModuleExport } from '../webpack';
4
+
5
+ const ScrollingModule = findModuleByExport((e: Export) => e?.render?.toString?.().includes('{case"x":'));
6
+
7
+ const ScrollingModuleProps = ScrollingModule ? Object.values(ScrollingModule) : [];
8
+
9
+ export const ScrollPanel = ScrollingModuleProps.find((prop: any) =>
10
+ prop?.render?.toString?.().includes('{case"x":'),
11
+ ) as FC<{ children?: ReactNode }>;
12
+
13
+ export const ScrollPanelGroup: FC<{ children?: ReactNode }> = findModuleExport((e: Export) =>
14
+ e?.render?.toString().includes('.FocusVisibleChild()),[])'),
15
+ );
@@ -0,0 +1,28 @@
1
+ import { ReactNode, VFC } from 'react';
2
+
3
+ import { Export, findModuleExport } from '../webpack';
4
+
5
+ export interface SidebarNavigationPage {
6
+ title: ReactNode;
7
+ content: ReactNode;
8
+ icon?: ReactNode;
9
+ visible?: boolean;
10
+ hideTitle?: boolean;
11
+ identifier?: string;
12
+ route?: string;
13
+ link?: string;
14
+ padding?: 'none' | 'compact';
15
+ }
16
+
17
+ export interface SidebarNavigationProps {
18
+ title?: string;
19
+ pages: (SidebarNavigationPage | 'separator')[];
20
+ showTitle?: boolean;
21
+ disableRouteReporting?: boolean;
22
+ page?: string;
23
+ onPageRequested?: (page: string) => void;
24
+ }
25
+
26
+ export const SidebarNavigation = findModuleExport((e: Export) =>
27
+ e?.toString()?.includes('"disableRouteReporting"'),
28
+ ) as VFC<SidebarNavigationProps>;
@@ -0,0 +1,33 @@
1
+ import { FC } from 'react';
2
+
3
+ import { CommonUIModule } from '../webpack';
4
+ import { ItemProps } from './Item';
5
+
6
+ export interface NotchLabel {
7
+ notchIndex: number;
8
+ label: string;
9
+ value?: number;
10
+ }
11
+
12
+ export interface SliderFieldProps extends ItemProps {
13
+ value: number;
14
+ min?: number;
15
+ max?: number;
16
+ step?: number;
17
+ notchCount?: number;
18
+ notchLabels?: NotchLabel[];
19
+ notchTicksVisible?: boolean;
20
+ showValue?: boolean;
21
+ resetValue?: number;
22
+ disabled?: boolean;
23
+ editableValue?: boolean;
24
+ validValues?: 'steps' | 'range' | ((value: number) => boolean);
25
+ valueSuffix?: string;
26
+ minimumDpadGranularity?: number;
27
+ onChange?(value: number): void;
28
+ className?: string;
29
+ }
30
+
31
+ export const SliderField = Object.values(CommonUIModule).find((mod: any) =>
32
+ mod?.toString()?.includes('SliderField,fallback'),
33
+ ) as FC<SliderFieldProps>;
@@ -0,0 +1,8 @@
1
+ import { FC, SVGAttributes } from 'react';
2
+
3
+ import { IconsModule } from '../webpack';
4
+
5
+ // TODO type this and other icons?
6
+ export const Spinner = Object.values(IconsModule).find(
7
+ (mod: any) => mod?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(mod.toString()),
8
+ ) as FC<SVGAttributes<SVGElement>>;
@@ -0,0 +1,7 @@
1
+ import { FC, SVGAttributes } from 'react';
2
+
3
+ import { Export, findModuleExport } from '../webpack';
4
+
5
+ export const SteamSpinner = findModuleExport(
6
+ (e: Export) => e?.toString?.()?.includes('Steam Spinner') && e?.toString?.()?.includes('src'),
7
+ ) as FC<SVGAttributes<SVGElement>>;
@@ -0,0 +1,127 @@
1
+ import { FC, ReactNode, createElement, useEffect, useState } from 'react';
2
+
3
+ import { fakeRenderComponent, findInReactTree, sleep } from '../utils';
4
+ import { Export, findModuleByExport } from '../webpack';
5
+ import { FooterLegendProps } from './FooterLegend';
6
+ import { SteamSpinner } from './SteamSpinner';
7
+
8
+ /**
9
+ * Individual tab objects for the Tabs component
10
+ *
11
+ * `id` ID of this tab, can be used with activeTab to auto-focus a given tab
12
+ * `title` Title shown in the header bar
13
+ * `renderTabAddon` Return a {@link ReactNode} to render it next to the tab title, i.e. the counts for each tab on the Media page
14
+ * `content` Content of the tab
15
+ * `footer` Sets up button handlers and labels
16
+ */
17
+ export interface Tab {
18
+ id: string;
19
+ title: string;
20
+ renderTabAddon?: () => ReactNode;
21
+ content: ReactNode;
22
+ footer?: FooterLegendProps;
23
+ }
24
+
25
+ /**
26
+ * Props for the {@link Tabs}
27
+ *
28
+ * `tabs` array of {@link Tab}
29
+ * `activeTab` tab currently active, needs to be one of the tabs {@link Tab.id}, must be set using a `useState` in the `onShowTab` handler
30
+ * `onShowTab` Called when the active tab should change, needs to set `activeTab`. See example.
31
+ * `autoFocusContents` Whether to automatically focus the tab contents or not.
32
+ * `footer` Sets up button handlers and labels
33
+ *
34
+ * @example
35
+ * const Component: FC = () => {
36
+ * const [currentTab, setCurrentTab] = useState<string>("Tab1");
37
+ *
38
+ * return (
39
+ * <Tabs
40
+ * title="Theme Manager"
41
+ * activeTab={currentTabRoute}
42
+ * onShowTab={(tabID: string) => {
43
+ * setCurrentTabRoute(tabID);
44
+ * }}
45
+ * tabs={[
46
+ * {
47
+ * title: "Tab 1",
48
+ * content: <Tab1Component />,
49
+ * id: "Tab1",
50
+ * },
51
+ * {
52
+ * title: "Tab 2",
53
+ * content: <Tab2Component />,
54
+ * id: "Tab2",
55
+ * },
56
+ * ]}
57
+ * />
58
+ * );
59
+ * };
60
+ */
61
+ export interface TabsProps {
62
+ tabs: Tab[];
63
+ activeTab: string;
64
+ onShowTab: (tab: string) => void;
65
+ autoFocusContents?: boolean;
66
+ }
67
+
68
+ let tabsComponent: any;
69
+
70
+ const getTabs = async () => {
71
+ if (tabsComponent) return tabsComponent;
72
+ // @ts-ignore
73
+ while (!window?.DeckyPluginLoader?.routerHook?.routes) {
74
+ console.debug('[DFL:Tabs]: Waiting for Decky router...');
75
+ await sleep(500);
76
+ }
77
+ return (tabsComponent = fakeRenderComponent(
78
+ () => {
79
+ return findInReactTree(
80
+ findInReactTree(
81
+ // @ts-ignore
82
+ window.DeckyPluginLoader.routerHook.routes
83
+ .find((x: any) => x.props.path == '/library/app/:appid/achievements')
84
+ .props.children.type(),
85
+ (x) => x?.props?.scrollTabsTop,
86
+ ).type({ appid: 1 }),
87
+ (x) => x?.props?.tabs,
88
+ ).type;
89
+ },
90
+ {
91
+ useRef: () => ({ current: { reaction: { track: () => {} } } }),
92
+ useContext: () => ({ match: { params: { appid: 1 } } }),
93
+ useMemo: () => ({ data: {} }),
94
+ },
95
+ ));
96
+ };
97
+
98
+ let oldTabs: any;
99
+
100
+ try {
101
+ const oldTabsModule = findModuleByExport((e: Export) => e.Unbleed);
102
+ if (oldTabsModule)
103
+ oldTabs = Object.values(oldTabsModule).find((x: any) => x?.type?.toString()?.includes('((function(){'));
104
+ } catch (e) {
105
+ console.error('Error finding oldTabs:', e);
106
+ }
107
+
108
+ /**
109
+ * Tabs component as used in the library and media tabs. See {@link TabsProps}.
110
+ * Unlike other components in `decky-frontend-lib`, this requires Decky Loader to be running.
111
+ */
112
+ export const Tabs = (oldTabs ||
113
+ ((props: TabsProps) => {
114
+ const found = tabsComponent;
115
+ const [tc, setTC] = useState<FC<TabsProps>>(found);
116
+ useEffect(() => {
117
+ if (found) return;
118
+ (async () => {
119
+ console.debug('[DFL:Tabs]: Finding component...');
120
+ const t = await getTabs();
121
+ console.debug('[DFL:Tabs]: Found!');
122
+ setTC(t);
123
+ })();
124
+ }, []);
125
+ console.log('tc', tc);
126
+ return tc ? createElement(tc, props) : <SteamSpinner />;
127
+ })) as FC<TabsProps>;
@@ -0,0 +1,28 @@
1
+ import { ChangeEventHandler, HTMLAttributes, ReactNode, VFC } from 'react';
2
+
3
+ import { CommonUIModule, Module } from '../webpack';
4
+
5
+ export interface TextFieldProps extends HTMLAttributes<HTMLInputElement> {
6
+ label?: ReactNode;
7
+ requiredLabel?: ReactNode;
8
+ description?: ReactNode;
9
+ disabled?: boolean;
10
+ bShowCopyAction?: boolean;
11
+ bShowClearAction?: boolean;
12
+ bAlwaysShowClearAction?: boolean;
13
+ bIsPassword?: boolean;
14
+ rangeMin?: number;
15
+ rangeMax?: number;
16
+ mustBeNumeric?: boolean;
17
+ mustBeURL?: boolean;
18
+ mustBeEmail?: boolean;
19
+ focusOnMount?: boolean;
20
+ tooltip?: string;
21
+ inlineControls?: ReactNode;
22
+ onChange?: ChangeEventHandler<HTMLInputElement>;
23
+ value?: string;
24
+ }
25
+
26
+ export const TextField = Object.values(CommonUIModule).find(
27
+ (mod: Module) => mod?.validateUrl && mod?.validateEmail,
28
+ ) as VFC<TextFieldProps>;
@@ -0,0 +1,14 @@
1
+ import { FC } from 'react';
2
+
3
+ import { CommonUIModule } from '../webpack';
4
+
5
+ export interface ToggleProps {
6
+ value: boolean;
7
+ disabled?: boolean;
8
+ onChange?(checked: boolean): void;
9
+ navRef?: any; // TODO figure out what this is
10
+ }
11
+
12
+ export const Toggle = Object.values(CommonUIModule).find((mod: any) =>
13
+ mod?.render?.toString()?.includes('.ToggleOff)'),
14
+ ) as FC<ToggleProps>;
@@ -0,0 +1,15 @@
1
+ import { FC } from 'react';
2
+
3
+ import { CommonUIModule } from '../webpack';
4
+ import { ItemProps } from './Item';
5
+
6
+ export interface ToggleFieldProps extends ItemProps {
7
+ highlightOnFocus?: boolean;
8
+ checked: boolean;
9
+ disabled?: boolean;
10
+ onChange?(checked: boolean): void;
11
+ }
12
+
13
+ export const ToggleField = Object.values(CommonUIModule).find((mod: any) =>
14
+ mod?.render?.toString()?.includes('ToggleField,fallback'),
15
+ ) as FC<ToggleFieldProps>;
@@ -0,0 +1,25 @@
1
+ export * from './Button';
2
+ export * from './ButtonItem';
3
+ export * from './Carousel';
4
+ export * from './ControlsList';
5
+ export * from './Dialog';
6
+ export * from './DialogCheckbox';
7
+ export * from './Dropdown';
8
+ export * from './Field';
9
+ export * from './Focusable';
10
+ export * from './FocusRing';
11
+ export * from './FooterLegend';
12
+ export * from './Marquee';
13
+ export * from './Menu';
14
+ export * from './Modal';
15
+ export * from './Panel';
16
+ export * from './ProgressBar';
17
+ export * from './SidebarNavigation';
18
+ export * from './SliderField';
19
+ export * from './Spinner';
20
+ export * from './SteamSpinner';
21
+ export * from './Tabs';
22
+ export * from './TextField';
23
+ export * from './Toggle';
24
+ export * from './ToggleField';
25
+ export * from './Scroll';
@@ -0,0 +1,132 @@
1
+ import { CSSProperties, FC, useState } from 'react';
2
+
3
+ import { ConfirmModal, SliderField } from '../components';
4
+ import { gamepadSliderClasses } from '../utils/static-classes';
5
+
6
+ interface ColorPickerModalProps {
7
+ closeModal: () => void;
8
+ onConfirm?(HSLString: string): any;
9
+ title?: string;
10
+ defaultH?: number;
11
+ defaultS?: number;
12
+ defaultL?: number;
13
+ defaultA?: number;
14
+ }
15
+
16
+ export const ColorPickerModal: FC<ColorPickerModalProps> = ({
17
+ closeModal,
18
+ onConfirm = () => {},
19
+ title = 'Color Picker',
20
+ defaultH = 0,
21
+ defaultS = 100,
22
+ defaultL = 50,
23
+ defaultA = 1,
24
+ }) => {
25
+ const [H, setH] = useState<number>(defaultH);
26
+ const [S, setS] = useState<number>(defaultS);
27
+ const [L, setL] = useState<number>(defaultL);
28
+ const [A, setA] = useState<number>(defaultA);
29
+
30
+ const colorPickerCSSVars = {
31
+ '--decky-color-picker-hvalue': `${H}`,
32
+ '--decky-color-picker-svalue': `${S}%`,
33
+ '--decky-color-picker-lvalue': `${L}%`,
34
+ '--decky-color-picker-avalue': `${A}`,
35
+ } as CSSProperties;
36
+
37
+ return (
38
+ <ConfirmModal
39
+ bAllowFullSize
40
+ onCancel={closeModal}
41
+ onOK={() => {
42
+ onConfirm(`hsla(${H}, ${S}%, ${L}%, ${A})`);
43
+ closeModal();
44
+ }}
45
+ >
46
+ <style>
47
+ {`
48
+ /* This removes the cyan track color that is behind the slider head */
49
+ .ColorPicker_Container .${gamepadSliderClasses.SliderTrack} {
50
+ --left-track-color: #0000;
51
+ /* This is for compatibility with the "Colored Toggles" CSSLoader Theme*/
52
+ --colored-toggles-main-color: #0000;
53
+ }
54
+
55
+ .ColorPicker_HSlider .${gamepadSliderClasses.SliderTrack} {
56
+ background: linear-gradient(
57
+ 270deg,
58
+ hsla(360, var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue)),
59
+ hsla(270, var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue)),
60
+ hsla(180, var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue)),
61
+ hsla(90, var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue)),
62
+ hsla(0, var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue))
63
+ );
64
+ }
65
+
66
+ .ColorPicker_SSlider .${gamepadSliderClasses.SliderTrack} {
67
+ background: linear-gradient(
68
+ 90deg,
69
+ hsla(var(--decky-color-picker-hvalue), 0%, var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue)),
70
+ hsla(var(--decky-color-picker-hvalue), 100%, var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue))
71
+ );
72
+ }
73
+
74
+ .ColorPicker_LSlider .${gamepadSliderClasses.SliderTrack} {
75
+ background: linear-gradient(
76
+ 90deg,
77
+ hsla(var(--decky-color-picker-hvalue), var(--decky-color-picker-svalue), 0%, var(--decky-color-picker-avalue)),
78
+ hsla(var(--decky-color-picker-hvalue), var(--decky-color-picker-svalue), 50%, var(--decky-color-picker-avalue)),
79
+ hsla(var(--decky-color-picker-hvalue), var(--decky-color-picker-svalue), 100%, var(--decky-color-picker-avalue))
80
+ );
81
+ }
82
+
83
+ .ColorPicker_ASlider .${gamepadSliderClasses.SliderTrack} {
84
+ background: linear-gradient(
85
+ 90deg,
86
+ hsla(var(--decky-color-picker-hvalue), var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), 0),
87
+ hsla(var(--decky-color-picker-hvalue), var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), 1)
88
+ );
89
+ }
90
+ `}
91
+ </style>
92
+ <div
93
+ className="ColorPicker_ColorDisplayContainer"
94
+ style={{
95
+ display: 'flex',
96
+ justifyContent: 'space-between',
97
+ alignItems: 'center',
98
+ marginBottom: '1em',
99
+ // theres a large header by default on the modal, so this just pushes it up into that unused space
100
+ marginTop: '-2.5em',
101
+ }}
102
+ >
103
+ <div>
104
+ <span style={{ fontSize: '1.5em' }}>
105
+ <b>{title}</b>
106
+ </span>
107
+ </div>
108
+ <div
109
+ style={{
110
+ backgroundColor: `hsla(${H}, ${S}%, ${L}%, ${A})`,
111
+ width: '40px',
112
+ height: '40px',
113
+ }}
114
+ ></div>
115
+ </div>
116
+ <div className="ColorPicker_Container" style={colorPickerCSSVars}>
117
+ <div className="ColorPicker_HSlider">
118
+ <SliderField showValue editableValue label="Hue" value={H} min={0} max={360} onChange={setH} />
119
+ </div>
120
+ <div className="ColorPicker_SSlider">
121
+ <SliderField showValue editableValue label="Saturation" value={S} min={0} max={100} onChange={setS} />
122
+ </div>
123
+ <div className="ColorPicker_LSlider">
124
+ <SliderField showValue editableValue label="Lightness" value={L} min={0} max={100} onChange={setL} />
125
+ </div>
126
+ <div className="ColorPicker_ASlider">
127
+ <SliderField showValue editableValue label="Alpha" value={A} step={0.1} min={0} max={1} onChange={setA} />
128
+ </div>
129
+ </div>
130
+ </ConfirmModal>
131
+ );
132
+ };