@netless/fastboard-react 0.1.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 (163) hide show
  1. package/LICENSE.txt +21 -0
  2. package/dist/behaviors/style.d.ts +1 -0
  3. package/dist/components/Fastboard.d.ts +15 -0
  4. package/dist/components/PageControl/PageControl.d.ts +5 -0
  5. package/dist/components/PageControl/hooks.d.ts +9 -0
  6. package/dist/components/PageControl/index.d.ts +2 -0
  7. package/dist/components/PlayerControl/PlayerControl.d.ts +9 -0
  8. package/dist/components/PlayerControl/hooks.d.ts +11 -0
  9. package/dist/components/PlayerControl/icons/Loading.d.ts +3 -0
  10. package/dist/components/PlayerControl/icons/Pause.d.ts +3 -0
  11. package/dist/components/PlayerControl/icons/Play.d.ts +3 -0
  12. package/dist/components/PlayerControl/icons/index.d.ts +6 -0
  13. package/dist/components/PlayerControl/index.d.ts +2 -0
  14. package/dist/components/RedoUndo/RedoUndo.d.ts +5 -0
  15. package/dist/components/RedoUndo/hooks.d.ts +6 -0
  16. package/dist/components/RedoUndo/index.d.ts +2 -0
  17. package/dist/components/Toolbar/Content.d.ts +2 -0
  18. package/dist/components/Toolbar/Toolbar.d.ts +14 -0
  19. package/dist/components/Toolbar/components/ApplianceButtons.d.ts +7 -0
  20. package/dist/components/Toolbar/components/AppsButton.d.ts +6 -0
  21. package/dist/components/Toolbar/components/ColorBox.d.ts +2 -0
  22. package/dist/components/Toolbar/components/CutLine.d.ts +2 -0
  23. package/dist/components/Toolbar/components/Mask.d.ts +7 -0
  24. package/dist/components/Toolbar/components/PencilButton.d.ts +2 -0
  25. package/dist/components/Toolbar/components/ShapesButton.d.ts +3 -0
  26. package/dist/components/Toolbar/components/Slider.d.ts +2 -0
  27. package/dist/components/Toolbar/components/TextButton.d.ts +2 -0
  28. package/dist/components/Toolbar/components/UpDownButtons.d.ts +7 -0
  29. package/dist/components/Toolbar/const.d.ts +18 -0
  30. package/dist/components/Toolbar/hooks.d.ts +12 -0
  31. package/dist/components/Toolbar/icons/Apps.d.ts +3 -0
  32. package/dist/components/Toolbar/icons/Arrow.d.ts +3 -0
  33. package/dist/components/Toolbar/icons/Circle.d.ts +3 -0
  34. package/dist/components/Toolbar/icons/Clean.d.ts +3 -0
  35. package/dist/components/Toolbar/icons/Clicker.d.ts +3 -0
  36. package/dist/components/Toolbar/icons/Collapse.d.ts +3 -0
  37. package/dist/components/Toolbar/icons/Diamond.d.ts +3 -0
  38. package/dist/components/Toolbar/icons/Down.d.ts +3 -0
  39. package/dist/components/Toolbar/icons/Eraser.d.ts +3 -0
  40. package/dist/components/Toolbar/icons/Expand.d.ts +3 -0
  41. package/dist/components/Toolbar/icons/Line.d.ts +3 -0
  42. package/dist/components/Toolbar/icons/Pencil.d.ts +3 -0
  43. package/dist/components/Toolbar/icons/Rectangle.d.ts +3 -0
  44. package/dist/components/Toolbar/icons/Selector.d.ts +3 -0
  45. package/dist/components/Toolbar/icons/SpeechBalloon.d.ts +3 -0
  46. package/dist/components/Toolbar/icons/Star.d.ts +3 -0
  47. package/dist/components/Toolbar/icons/Text.d.ts +3 -0
  48. package/dist/components/Toolbar/icons/Triangle.d.ts +3 -0
  49. package/dist/components/Toolbar/icons/Up.d.ts +3 -0
  50. package/dist/components/Toolbar/icons/index.d.ts +22 -0
  51. package/dist/components/Toolbar/index.d.ts +2 -0
  52. package/dist/components/ZoomControl/ZoomControl.d.ts +5 -0
  53. package/dist/components/ZoomControl/hooks.d.ts +7 -0
  54. package/dist/components/ZoomControl/index.d.ts +2 -0
  55. package/dist/components/hooks.d.ts +13 -0
  56. package/dist/i18n/index.d.ts +12 -0
  57. package/dist/icons/ChevronLeft.d.ts +3 -0
  58. package/dist/icons/ChevronRight.d.ts +3 -0
  59. package/dist/icons/FilePlus.d.ts +3 -0
  60. package/dist/icons/Minus.d.ts +3 -0
  61. package/dist/icons/Plus.d.ts +3 -0
  62. package/dist/icons/Redo.d.ts +3 -0
  63. package/dist/icons/Reset.d.ts +3 -0
  64. package/dist/icons/Undo.d.ts +3 -0
  65. package/dist/icons/index.d.ts +7 -0
  66. package/dist/index.d.ts +9 -0
  67. package/dist/index.js +2116 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/index.mjs +2083 -0
  70. package/dist/index.mjs.map +1 -0
  71. package/dist/internal/helpers.d.ts +16 -0
  72. package/dist/internal/hooks.d.ts +3 -0
  73. package/dist/internal/index.d.ts +2 -0
  74. package/dist/theme.d.ts +16 -0
  75. package/dist/typings.d.ts +10 -0
  76. package/package.json +41 -0
  77. package/src/behaviors/style.ts +4 -0
  78. package/src/components/Fastboard.scss +41 -0
  79. package/src/components/Fastboard.tsx +97 -0
  80. package/src/components/PageControl/PageControl.scss +80 -0
  81. package/src/components/PageControl/PageControl.tsx +105 -0
  82. package/src/components/PageControl/hooks.ts +67 -0
  83. package/src/components/PageControl/index.ts +2 -0
  84. package/src/components/PlayerControl/PlayerControl.scss +145 -0
  85. package/src/components/PlayerControl/PlayerControl.tsx +131 -0
  86. package/src/components/PlayerControl/components/Button.tsx +45 -0
  87. package/src/components/PlayerControl/hooks.ts +88 -0
  88. package/src/components/PlayerControl/icons/Loading.tsx +13 -0
  89. package/src/components/PlayerControl/icons/Pause.tsx +13 -0
  90. package/src/components/PlayerControl/icons/Play.tsx +13 -0
  91. package/src/components/PlayerControl/icons/index.ts +10 -0
  92. package/src/components/PlayerControl/index.ts +2 -0
  93. package/src/components/RedoUndo/RedoUndo.scss +56 -0
  94. package/src/components/RedoUndo/RedoUndo.tsx +76 -0
  95. package/src/components/RedoUndo/hooks.ts +18 -0
  96. package/src/components/RedoUndo/index.ts +2 -0
  97. package/src/components/Toolbar/Content.tsx +74 -0
  98. package/src/components/Toolbar/Toolbar.scss +281 -0
  99. package/src/components/Toolbar/Toolbar.tsx +116 -0
  100. package/src/components/Toolbar/components/ApplianceButtons.tsx +108 -0
  101. package/src/components/Toolbar/components/AppsButton.tsx +101 -0
  102. package/src/components/Toolbar/components/Button.tsx +46 -0
  103. package/src/components/Toolbar/components/ColorBox.tsx +55 -0
  104. package/src/components/Toolbar/components/CutLine.tsx +8 -0
  105. package/src/components/Toolbar/components/Mask.tsx +44 -0
  106. package/src/components/Toolbar/components/PencilButton.tsx +66 -0
  107. package/src/components/Toolbar/components/ShapesButton.tsx +128 -0
  108. package/src/components/Toolbar/components/Slider.tsx +26 -0
  109. package/src/components/Toolbar/components/TextButton.tsx +62 -0
  110. package/src/components/Toolbar/components/UpDownButtons.tsx +49 -0
  111. package/src/components/Toolbar/components/assets/cocos.png +0 -0
  112. package/src/components/Toolbar/components/assets/collapsed.png +0 -0
  113. package/src/components/Toolbar/components/assets/countdown.png +0 -0
  114. package/src/components/Toolbar/components/assets/expanded.png +0 -0
  115. package/src/components/Toolbar/components/assets/geogebra.png +0 -0
  116. package/src/components/Toolbar/components/assets/vscode.png +0 -0
  117. package/src/components/Toolbar/const.ts +32 -0
  118. package/src/components/Toolbar/hooks.ts +68 -0
  119. package/src/components/Toolbar/icons/Apps.tsx +16 -0
  120. package/src/components/Toolbar/icons/Arrow.tsx +13 -0
  121. package/src/components/Toolbar/icons/Circle.tsx +13 -0
  122. package/src/components/Toolbar/icons/Clean.tsx +16 -0
  123. package/src/components/Toolbar/icons/Clicker.tsx +19 -0
  124. package/src/components/Toolbar/icons/Collapse.tsx +13 -0
  125. package/src/components/Toolbar/icons/Diamond.tsx +13 -0
  126. package/src/components/Toolbar/icons/Down.tsx +13 -0
  127. package/src/components/Toolbar/icons/Eraser.tsx +16 -0
  128. package/src/components/Toolbar/icons/Expand.tsx +13 -0
  129. package/src/components/Toolbar/icons/Line.tsx +13 -0
  130. package/src/components/Toolbar/icons/Pencil.tsx +16 -0
  131. package/src/components/Toolbar/icons/Rectangle.tsx +13 -0
  132. package/src/components/Toolbar/icons/Selector.tsx +13 -0
  133. package/src/components/Toolbar/icons/SpeechBalloon.tsx +17 -0
  134. package/src/components/Toolbar/icons/Star.tsx +17 -0
  135. package/src/components/Toolbar/icons/Text.tsx +13 -0
  136. package/src/components/Toolbar/icons/Triangle.tsx +13 -0
  137. package/src/components/Toolbar/icons/Up.tsx +13 -0
  138. package/src/components/Toolbar/icons/index.ts +42 -0
  139. package/src/components/Toolbar/index.ts +2 -0
  140. package/src/components/ZoomControl/ZoomControl.scss +80 -0
  141. package/src/components/ZoomControl/ZoomControl.tsx +94 -0
  142. package/src/components/ZoomControl/hooks.ts +52 -0
  143. package/src/components/ZoomControl/index.ts +2 -0
  144. package/src/components/hooks.ts +59 -0
  145. package/src/i18n/en.json +31 -0
  146. package/src/i18n/index.ts +29 -0
  147. package/src/i18n/zh-CN.json +32 -0
  148. package/src/icons/ChevronLeft.tsx +21 -0
  149. package/src/icons/ChevronRight.tsx +21 -0
  150. package/src/icons/FilePlus.tsx +18 -0
  151. package/src/icons/Minus.tsx +15 -0
  152. package/src/icons/Plus.tsx +15 -0
  153. package/src/icons/Redo.tsx +18 -0
  154. package/src/icons/Reset.tsx +19 -0
  155. package/src/icons/Undo.tsx +18 -0
  156. package/src/icons/index.tsx +11 -0
  157. package/src/index.ts +10 -0
  158. package/src/internal/helpers.ts +31 -0
  159. package/src/internal/hooks.ts +23 -0
  160. package/src/internal/index.ts +2 -0
  161. package/src/style.scss +29 -0
  162. package/src/theme.ts +36 -0
  163. package/src/typings.ts +15 -0
@@ -0,0 +1,76 @@
1
+ import type { CommonProps, GenericIcon } from "../../typings";
2
+
3
+ import Tippy from "@tippyjs/react";
4
+ import clsx from "clsx";
5
+ import React from "react";
6
+
7
+ import { useTranslation } from "../../i18n";
8
+ import { Icon } from "../../icons";
9
+ import { Redo } from "../../icons/Redo";
10
+ import { Undo } from "../../icons/Undo";
11
+ import { TopOffset } from "../../theme";
12
+ import { useTheme, useWritable } from "../hooks";
13
+ import { useRedoUndo } from "./hooks";
14
+
15
+ export const name = "fastboard-redo-undo";
16
+
17
+ export type RedoUndoProps = CommonProps & GenericIcon<"undo" | "redo">;
18
+
19
+ export function RedoUndo({ theme, undoIcon, undoIconDisable, redoIcon, redoIconDisable }: RedoUndoProps) {
20
+ theme = useTheme(theme);
21
+ const { t } = useTranslation();
22
+
23
+ const writable = useWritable();
24
+ const { redoSteps, undoSteps, redo, undo } = useRedoUndo();
25
+
26
+ const disabled = !writable;
27
+
28
+ return (
29
+ <div className={clsx(name, theme)}>
30
+ <Tippy
31
+ className="fastboard-tip"
32
+ content={t("undo")}
33
+ theme={theme}
34
+ disabled={disabled}
35
+ placement="top"
36
+ delay={[1000, 400]}
37
+ duration={300}
38
+ offset={TopOffset}
39
+ >
40
+ <button
41
+ className={clsx(`${name}-btn`, "undo", theme)}
42
+ disabled={disabled || undoSteps === 0}
43
+ onClick={undo}
44
+ >
45
+ <Icon
46
+ fallback={<Undo theme={theme} />}
47
+ src={undoSteps === 0 ? undoIconDisable : undoIcon}
48
+ alt="[undo]"
49
+ />
50
+ </button>
51
+ </Tippy>
52
+ <Tippy
53
+ className="fastboard-tip"
54
+ content={t("redo")}
55
+ theme={theme}
56
+ disabled={disabled}
57
+ placement="top"
58
+ delay={[1000, 400]}
59
+ duration={300}
60
+ offset={TopOffset}
61
+ >
62
+ <button
63
+ className={clsx(`${name}-btn`, "redo", theme)}
64
+ disabled={disabled || redoSteps === 0}
65
+ onClick={redo}
66
+ >
67
+ <Icon
68
+ fallback={<Redo theme={theme} />}
69
+ src={redoSteps === 0 ? redoIconDisable : redoIcon}
70
+ alt="[redo]"
71
+ />
72
+ </button>
73
+ </Tippy>
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,18 @@
1
+ import { useCallback } from "react";
2
+ import { useFastboardApp, useFastboardValue } from "../hooks";
3
+
4
+ export function useRedoUndo() {
5
+ const app = useFastboardApp();
6
+ const undoSteps = useFastboardValue(app.canUndoSteps);
7
+ const redoSteps = useFastboardValue(app.canRedoSteps);
8
+
9
+ const undo = useCallback(() => {
10
+ app.undo();
11
+ }, [app]);
12
+
13
+ const redo = useCallback(() => {
14
+ app.redo();
15
+ }, [app]);
16
+
17
+ return { redoSteps, undoSteps, redo, undo };
18
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./hooks";
2
+ export { RedoUndo, type RedoUndoProps } from "./RedoUndo";
@@ -0,0 +1,74 @@
1
+ import clsx from "clsx";
2
+ import React, { useCallback, useEffect, useRef, useState } from "react";
3
+
4
+ import { clamp } from "../../internal";
5
+ import { CleanButton, ClickerButton, EraserButton, SelectorButton } from "./components/ApplianceButtons";
6
+ import { AppsButton } from "./components/AppsButton";
7
+ import { PencilButton } from "./components/PencilButton";
8
+ import { ShapesButton } from "./components/ShapesButton";
9
+ import { TextButton } from "./components/TextButton";
10
+ import { DownButton, UpButton } from "./components/UpDownButtons";
11
+ import { ItemHeight, ItemsCount, MaxHeight, MinHeight } from "./const";
12
+ import { name } from "./Toolbar";
13
+
14
+ export const Content = React.memo(() => {
15
+ const ref = useRef<HTMLDivElement>(null);
16
+ const [scrollTop, setScrollTop] = useState(0);
17
+ const [parentHeight, setParentHeight] = useState(0);
18
+
19
+ const needScroll = parentHeight < ItemHeight * ItemsCount + 48;
20
+ const sectionHeight = clamp(parentHeight - 48 * (needScroll ? 3 : 1), MinHeight, MaxHeight);
21
+ const scrollBuffer = Math.max(parentHeight - sectionHeight - 1, 0);
22
+ const disableScrollUp = scrollTop === 0;
23
+ const disableScrollDown = scrollTop === scrollBuffer;
24
+
25
+ const scrollTo = useCallback(
26
+ (height: number) => {
27
+ setScrollTop(clamp(scrollTop + height, 0, scrollBuffer));
28
+ },
29
+ [scrollBuffer, scrollTop]
30
+ );
31
+
32
+ useEffect(() => {
33
+ if (ref.current) {
34
+ ref.current.scrollTop = scrollTop;
35
+ }
36
+ }, [scrollTop]);
37
+
38
+ useEffect(() => {
39
+ const container = ref.current?.parentElement?.parentElement;
40
+ if (container) {
41
+ const { paddingTop, paddingBottom } = getComputedStyle(container);
42
+ const padding = parseInt(paddingTop) + parseInt(paddingBottom) || 0;
43
+ const resizeObserver = new ResizeObserver(() => {
44
+ setParentHeight(container.getBoundingClientRect().height - padding);
45
+ });
46
+ resizeObserver.observe(container);
47
+ return () => resizeObserver.disconnect();
48
+ }
49
+ }, []);
50
+
51
+ return (
52
+ <>
53
+ {needScroll && <UpButton scrollTo={scrollTo} disabled={disableScrollUp} />}
54
+ <div
55
+ ref={ref}
56
+ className={clsx(`${name}-section`)}
57
+ style={{
58
+ height: `${sectionHeight}px`,
59
+ overflow: needScroll ? "hidden" : "visible",
60
+ }}
61
+ >
62
+ <ClickerButton />
63
+ <SelectorButton />
64
+ <PencilButton />
65
+ <TextButton />
66
+ <ShapesButton />
67
+ <EraserButton />
68
+ <CleanButton />
69
+ <AppsButton />
70
+ </div>
71
+ {needScroll && <DownButton scrollTo={scrollTo} disabled={disableScrollDown} />}
72
+ </>
73
+ );
74
+ });
@@ -0,0 +1,281 @@
1
+ $name: "fastboard-toolbar";
2
+
3
+ .#{$name} {
4
+ display: flex;
5
+ align-items: center;
6
+ padding: 4px;
7
+ border-radius: 4px;
8
+ flex-direction: column;
9
+ gap: 4px;
10
+ position: absolute;
11
+ z-index: 100;
12
+ backdrop-filter: blur(2px);
13
+ -webkit-backdrop-filter: blur(2px);
14
+
15
+ .rc-slider {
16
+ padding: 6px 0;
17
+ }
18
+ .rc-slider-rail,
19
+ .rc-slider-track {
20
+ height: 2px;
21
+ }
22
+
23
+ .tippy-content {
24
+ padding: 8px;
25
+ }
26
+ .tippy-box {
27
+ border: 1px solid rgba(0, 0, 0, 0.15);
28
+ background-color: rgba($color: #333, $alpha: 0.95);
29
+ backdrop-filter: blur(2px);
30
+ -webkit-backdrop-filter: blur(2px);
31
+ }
32
+ .tippy-box[data-theme~="light"] {
33
+ background-color: rgba($color: #fff, $alpha: 0.95);
34
+ box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.25);
35
+ }
36
+
37
+ &.light {
38
+ color: #333;
39
+ background-color: rgba($color: #fff, $alpha: 0.85);
40
+ border: 1px solid rgba(0, 0, 0, 0.15);
41
+ }
42
+
43
+ &.expanded {
44
+ border: 1px solid rgba(0, 0, 0, 0.15);
45
+ }
46
+
47
+ &.dark {
48
+ color: #ddd;
49
+ background-color: rgba($color: #333, $alpha: 0.85);
50
+ }
51
+
52
+ &.expanded:hover {
53
+ box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.25);
54
+ transform: translate3d(0, 0, 0);
55
+ }
56
+
57
+ &.collapsed {
58
+ padding: 0;
59
+ background-color: transparent;
60
+ }
61
+
62
+ &-tooltip {
63
+ display: inline-flex;
64
+ align-items: center;
65
+ gap: 4px;
66
+ }
67
+
68
+ &-hotkey {
69
+ margin-right: -4px;
70
+ width: 24px;
71
+ height: 24px;
72
+ border-radius: 4px;
73
+ background-color: rgba($color: #fff, $alpha: 0.1);
74
+ display: inline-flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ }
78
+
79
+ &-btn {
80
+ appearance: none;
81
+ cursor: pointer;
82
+ margin: 0;
83
+ border: 0;
84
+ padding: 4px;
85
+ width: 32px;
86
+ height: 32px;
87
+ background-color: transparent;
88
+ border-radius: 4px;
89
+ font-size: 24px;
90
+ line-height: 1;
91
+ position: relative;
92
+
93
+ &-interactive {
94
+ display: inline-block;
95
+ width: 32px;
96
+ height: 32px;
97
+ }
98
+
99
+ svg,
100
+ img {
101
+ width: 1em;
102
+ height: 1em;
103
+ }
104
+
105
+ &:disabled {
106
+ opacity: 0.5;
107
+ cursor: not-allowed;
108
+ }
109
+
110
+ &.light:not(:disabled):hover {
111
+ background-color: rgba(51, 129, 255, 0.1);
112
+ }
113
+
114
+ &.dark:not(:disabled):hover {
115
+ background-color: rgba(51, 129, 255, 0.25);
116
+ }
117
+ }
118
+
119
+ &-triangle {
120
+ width: 0px;
121
+ height: 0px;
122
+ border-bottom: 4px solid;
123
+ border-left: 4px solid transparent;
124
+ position: absolute;
125
+ bottom: 0;
126
+ right: 0;
127
+ }
128
+
129
+ &-cut-line {
130
+ display: inline-block;
131
+ height: 0.5px;
132
+ width: 100%;
133
+
134
+ &.light {
135
+ background-color: #e7e7e7;
136
+ }
137
+
138
+ &.dark {
139
+ background-color: rgba(255, 255, 255, 0.15);
140
+ }
141
+ }
142
+
143
+ &-section {
144
+ display: inline-flex;
145
+ flex-flow: column nowrap;
146
+ gap: 4px;
147
+ scroll-behavior: smooth;
148
+
149
+ &.collapsed {
150
+ transform: translateX(-100%);
151
+ transition: 1s transform;
152
+ }
153
+ }
154
+
155
+ &-panel {
156
+ width: 136px - 8px * 2;
157
+ padding: 0;
158
+ display: flex;
159
+ flex-flow: column nowrap;
160
+ align-items: center;
161
+ gap: 8px;
162
+
163
+ &.apps {
164
+ width: 240px - 8px * 2;
165
+ }
166
+ }
167
+
168
+ &-color-box,
169
+ &-shapes {
170
+ display: grid;
171
+ grid-template-columns: repeat(4, 1fr);
172
+ gap: 8px;
173
+ align-items: center;
174
+ justify-items: center;
175
+
176
+ .#{$name}-btn {
177
+ padding: 0;
178
+ width: 24px;
179
+ height: 24px;
180
+ }
181
+ }
182
+
183
+ &-apps {
184
+ width: 100%;
185
+ display: grid;
186
+ grid-template-columns: repeat(3, 1fr);
187
+ gap: 8px;
188
+ align-items: center;
189
+ justify-items: center;
190
+
191
+ .#{$name}-btn {
192
+ width: 40px;
193
+ height: 40px;
194
+ font-size: 40px;
195
+ }
196
+ }
197
+
198
+ &-app-icon {
199
+ padding-top: 4px;
200
+ display: inline-flex;
201
+ flex-flow: column nowrap;
202
+ align-items: center;
203
+ gap: 4px;
204
+
205
+ .fastboard-toolbar-btn {
206
+ padding: 0;
207
+ }
208
+
209
+ &-text {
210
+ font-size: 12px;
211
+ color: #5d5d5d;
212
+ overflow: hidden;
213
+ white-space: nowrap;
214
+ text-overflow: ellipsis;
215
+ }
216
+ }
217
+
218
+ &-color-item {
219
+ width: 24px;
220
+ height: 24px;
221
+ border-radius: 4px;
222
+ cursor: pointer;
223
+
224
+ *.light:hover {
225
+ background-color: #f5f5f5;
226
+ }
227
+
228
+ *.dark:hover {
229
+ background-color: #333;
230
+ }
231
+ }
232
+
233
+ &-color-border {
234
+ width: 24px;
235
+ height: 24px;
236
+ border: 1px solid transparent;
237
+ border-radius: 4px;
238
+ display: inline-flex;
239
+ align-items: center;
240
+ justify-content: center;
241
+
242
+ &.active.light {
243
+ border: 1px solid rgba(51, 129, 255, 0.8);
244
+ }
245
+
246
+ &.active.dark {
247
+ border: 1px solid rgba(51, 129, 255, 0.8);
248
+ }
249
+ }
250
+
251
+ &-color-btn {
252
+ margin: 0;
253
+ border: 1px solid rgba(0, 0, 0, 0.24);
254
+ padding: 0;
255
+ appearance: none;
256
+ width: 16px;
257
+ height: 16px;
258
+ border-radius: 4px;
259
+ cursor: pointer;
260
+
261
+ &:focus-visible {
262
+ outline-offset: 2px;
263
+ }
264
+ }
265
+
266
+ &-mask-btn {
267
+ width: 17px;
268
+ height: 62px;
269
+ cursor: pointer;
270
+ &.dark {
271
+ filter: invert(0.8);
272
+ }
273
+ }
274
+
275
+ &-expand-btn {
276
+ display: flex;
277
+ align-items: center;
278
+ position: absolute;
279
+ left: 0;
280
+ }
281
+ }
@@ -0,0 +1,116 @@
1
+ import type { CommonProps, GenericIcon, Theme } from "../../typings";
2
+ import type { ToolbarHook } from "./hooks";
3
+
4
+ import clsx from "clsx";
5
+ import { AnimatePresence, motion } from "framer-motion";
6
+ import React, { createContext, useCallback, useEffect, useState } from "react";
7
+
8
+ import collapsePNG from "./components/assets/collapsed.png";
9
+ import expandPNG from "./components/assets/expanded.png";
10
+
11
+ import { Icon } from "../../icons";
12
+ import { useTheme } from "../hooks";
13
+ import { Mask } from "./components/Mask";
14
+ import { Content } from "./Content";
15
+ import { EmptyToolbarHook, useToolbar } from "./hooks";
16
+
17
+ export type ToolbarProps = CommonProps & {
18
+ icons?: GenericIcon<
19
+ | "clicker"
20
+ | "selector"
21
+ | "pencil"
22
+ | "eraser"
23
+ | "clean"
24
+ | "expand"
25
+ | "collapse"
26
+ | "up"
27
+ | "down"
28
+ | "text"
29
+ | "apps"
30
+ >;
31
+ };
32
+
33
+ type ToolbarContextType = ToolbarHook & {
34
+ theme: Theme;
35
+ icons?: ToolbarProps["icons"];
36
+ };
37
+
38
+ export const ToolbarContext = createContext<ToolbarContextType>({
39
+ theme: "light",
40
+ ...EmptyToolbarHook,
41
+ });
42
+
43
+ export const name = "fastboard-toolbar";
44
+
45
+ export const Toolbar = ({ theme, icons }: ToolbarProps) => {
46
+ theme = useTheme(theme);
47
+
48
+ const hook = useToolbar();
49
+ const [expanded, setExpanded] = useState(true);
50
+ const [toolbar, toolbarRef] = useState<HTMLDivElement | null>(null);
51
+ const [onHover, setOnHover] = useState(false);
52
+ const [delayedOnHover, setDelayedOnHover] = useState(false);
53
+ const [pointEvents, setPointEvents] = useState(true);
54
+ const disabled = !hook.writable;
55
+
56
+ const toggle = useCallback(() => {
57
+ setExpanded(e => !e);
58
+ }, []);
59
+
60
+ useEffect(() => {
61
+ const timer = setTimeout(() => {
62
+ setDelayedOnHover(onHover);
63
+ }, 400);
64
+ return () => clearTimeout(timer);
65
+ }, [onHover]);
66
+
67
+ return (
68
+ <ToolbarContext.Provider value={{ theme, icons, ...hook }}>
69
+ <AnimatePresence>
70
+ {expanded ? (
71
+ <motion.div
72
+ initial={{ x: -100 }}
73
+ animate={{ x: 0, transition: { duration: 0.5 } }}
74
+ key="toolbar"
75
+ ref={toolbarRef}
76
+ className={clsx(name, theme)}
77
+ onPointerEnter={() => {
78
+ expanded && setOnHover(true);
79
+ }}
80
+ onMouseLeave={() => setOnHover(false)}
81
+ exit={{ x: -100, transition: { duration: 0.5 } }}
82
+ onAnimationStart={() => setPointEvents(false)}
83
+ onAnimationComplete={() => setPointEvents(true)}
84
+ style={{ pointerEvents: pointEvents ? "auto" : "none" }}
85
+ >
86
+ <Content />
87
+ {expanded && (onHover || delayedOnHover) && (
88
+ <Mask toolbar={toolbar}>
89
+ <div onClick={toggle}>
90
+ <img draggable={false} className={clsx(`${name}-mask-btn`, theme)} src={collapsePNG} />
91
+ </div>
92
+ </Mask>
93
+ )}
94
+ </motion.div>
95
+ ) : (
96
+ <motion.div
97
+ className={clsx(`${name}-expand-btn`, theme)}
98
+ key="expand"
99
+ onClick={toggle}
100
+ initial={{ x: -100 }}
101
+ animate={{ x: 0, transition: { duration: 0.5 } }}
102
+ >
103
+ {!expanded && (
104
+ <Icon
105
+ fallback={
106
+ <img draggable={false} src={expandPNG} className={clsx(`${name}-mask-btn`, theme)} />
107
+ }
108
+ src={disabled ? icons?.expandIconDisable : icons?.expandIcon}
109
+ />
110
+ )}
111
+ </motion.div>
112
+ )}
113
+ </AnimatePresence>
114
+ </ToolbarContext.Provider>
115
+ );
116
+ };
@@ -0,0 +1,108 @@
1
+ import type { HotKey } from "white-web-sdk";
2
+
3
+ import React, { useCallback, useContext } from "react";
4
+ import { ApplianceNames } from "white-web-sdk";
5
+
6
+ import { useTranslation } from "../../../i18n";
7
+ import { Icon } from "../../../icons";
8
+ import { defaultHotKeys } from "../../../internal";
9
+ import { useFastboardApp } from "../../hooks";
10
+ import { Icons } from "../icons";
11
+ import { ToolbarContext } from "../Toolbar";
12
+ import { Button } from "./Button";
13
+
14
+ export function renderToolTip(text: string | undefined, hotkey?: HotKey) {
15
+ if (!(typeof hotkey === "string")) return text;
16
+ return (
17
+ <span className="fastboard-toolbar-tooltip">
18
+ <span>{text}</span>
19
+ <span className="fastboard-toolbar-hotkey">{hotkey.toUpperCase()}</span>
20
+ </span>
21
+ );
22
+ }
23
+
24
+ export function ClickerButton() {
25
+ const app = useFastboardApp();
26
+ const { t } = useTranslation();
27
+ const { theme, icons, writable, setAppliance, memberState } = useContext(ToolbarContext);
28
+
29
+ const changeAppliance = useCallback(() => setAppliance(ApplianceNames.clicker), [setAppliance]);
30
+
31
+ const shortcut = app.hotKeys?.changeToClick;
32
+ const appliance = memberState?.currentApplianceName;
33
+ const active = appliance === ApplianceNames.clicker;
34
+ const disabled = !writable;
35
+
36
+ return (
37
+ <Button content={renderToolTip(t("clicker"), shortcut)} onClick={changeAppliance} active={active}>
38
+ <Icon
39
+ fallback={<Icons.Clicker theme={theme} active={active} />}
40
+ src={disabled ? icons?.clickerIconDisable : icons?.clickerIcon}
41
+ alt="[clicker]"
42
+ />
43
+ </Button>
44
+ );
45
+ }
46
+
47
+ export function SelectorButton() {
48
+ const app = useFastboardApp();
49
+ const { t } = useTranslation();
50
+ const { theme, icons, writable, setAppliance, memberState } = useContext(ToolbarContext);
51
+
52
+ const changeAppliance = useCallback(() => setAppliance(ApplianceNames.selector), [setAppliance]);
53
+
54
+ const appliance = memberState?.currentApplianceName;
55
+ const active = appliance === ApplianceNames.selector;
56
+ const disabled = !writable;
57
+ const shortcut = (app.hotKeys || defaultHotKeys).changeToSelector;
58
+
59
+ return (
60
+ <Button content={renderToolTip(t("selector"), shortcut)} onClick={changeAppliance} active={active}>
61
+ <Icon
62
+ fallback={<Icons.Selector theme={theme} active={active} />}
63
+ src={disabled ? icons?.selectorIconDisable : icons?.selectorIcon}
64
+ alt="[selector]"
65
+ />
66
+ </Button>
67
+ );
68
+ }
69
+
70
+ export function EraserButton() {
71
+ const app = useFastboardApp();
72
+ const { t } = useTranslation();
73
+ const { theme, icons, writable, setAppliance, memberState } = useContext(ToolbarContext);
74
+
75
+ const changeAppliance = useCallback(() => setAppliance(ApplianceNames.eraser), [setAppliance]);
76
+
77
+ const appliance = memberState?.currentApplianceName;
78
+ const active = appliance === ApplianceNames.eraser;
79
+ const disabled = !writable;
80
+ const shortcut = (app?.hotKeys || defaultHotKeys).changeToEraser;
81
+
82
+ return (
83
+ <Button content={renderToolTip(t("eraser"), shortcut)} onClick={changeAppliance} active={active}>
84
+ <Icon
85
+ fallback={<Icons.Eraser theme={theme} active={active} />}
86
+ src={disabled ? icons?.eraserIconDisable : icons?.eraserIcon}
87
+ alt="[eraser]"
88
+ />
89
+ </Button>
90
+ );
91
+ }
92
+
93
+ export function CleanButton() {
94
+ const { t } = useTranslation();
95
+ const { theme, icons, writable, cleanCurrentScene } = useContext(ToolbarContext);
96
+
97
+ const disabled = !writable;
98
+
99
+ return (
100
+ <Button content={t("clean")} onClick={cleanCurrentScene}>
101
+ <Icon
102
+ fallback={<Icons.Clean theme={theme} />}
103
+ src={disabled ? icons?.cleanIconDisable : icons?.cleanIcon}
104
+ alt="[clean]"
105
+ />
106
+ </Button>
107
+ );
108
+ }