From ba7def35eb91ea058ae58fb5c7d78c5918a9b391 Mon Sep 17 00:00:00 2001 From: Steve Xu Date: Thu, 15 Oct 2026 03:39:04 +0000 Subject: [PATCH] feat: use react-aria-components --- .gitignore | 1 + package.json | 22 +- packages/demo/backend/package.json | 9 +- .../backend/src/__tests__/websocket.test.ts | 210 +- packages/demo/backend/src/index.ts | 2 + packages/demo/backend/tsconfig.json | 4 +- packages/demo/frontend/index.html | 193 +- packages/demo/frontend/package.json | 6 +- packages/demo/frontend/src/index.tsx | 4 +- packages/demo/frontend/vite.config.mts | 2 +- packages/excel-collab/package.json | 6 +- .../src/__tests__/Border.test.tsx | 29 +- .../src/__tests__/Export.test.tsx | 27 +- .../src/__tests__/Filter.test.tsx | 10 +- .../src/__tests__/FloatElement.test.tsx | 111 +- .../src/__tests__/FormularBar.test.tsx | 10 +- .../src/__tests__/HorizontalAlign.test.tsx | 15 +- .../src/__tests__/Menubar.test.tsx | 32 +- .../src/__tests__/MergeCell.test.tsx | 31 +- .../src/__tests__/NumberFormat.test.tsx | 32 +- .../src/__tests__/SheetBar.test.tsx | 82 +- .../src/__tests__/Toolbar.test.tsx | 191 +- .../src/__tests__/VerticalAlign.test.tsx | 15 +- .../src/__tests__/canvas.test.tsx | 49 +- packages/excel-collab/src/__tests__/util.tsx | 9 + .../excel-collab/src/canvas/__tests__/util.ts | 12 +- packages/excel-collab/src/canvas/util.ts | 10 +- .../src/component/Breadcrumbs.css | 46 + .../src/component/Breadcrumbs.tsx | 30 + .../excel-collab/src/component/Button.css | 42 + .../excel-collab/src/component/Button.tsx | 42 + .../excel-collab/src/component/Calendar.css | 97 + .../excel-collab/src/component/Calendar.tsx | 62 + .../excel-collab/src/component/Checkbox.css | 75 + .../excel-collab/src/component/Checkbox.tsx | 44 + .../src/component/CheckboxGroup.css | 22 + .../src/component/CheckboxGroup.tsx | 34 + .../excel-collab/src/component/ColorArea.css | 15 + .../excel-collab/src/component/ColorArea.tsx | 12 + .../excel-collab/src/component/ColorField.css | 7 + .../excel-collab/src/component/ColorField.tsx | 33 + .../src/component/ColorPicker.css | 31 + .../src/component/ColorPicker.tsx | 62 + .../src/component/ColorSlider.css | 64 + .../src/component/ColorSlider.tsx | 30 + .../src/component/ColorSwatch.css | 8 + .../src/component/ColorSwatch.tsx | 18 + .../src/component/ColorSwatchPicker.css | 41 + .../src/component/ColorSwatchPicker.tsx | 21 + .../excel-collab/src/component/ColorThumb.css | 22 + .../excel-collab/src/component/ColorThumb.tsx | 6 + .../excel-collab/src/component/ColorWheel.css | 9 + .../excel-collab/src/component/ColorWheel.tsx | 18 + .../excel-collab/src/component/ComboBox.css | 32 + .../excel-collab/src/component/ComboBox.tsx | 61 + .../src/component/CommandPalette.css | 20 + .../src/component/CommandPalette.tsx | 55 + .../excel-collab/src/component/Content.css | 3 + .../excel-collab/src/component/Content.tsx | 11 + .../excel-collab/src/component/DateField.css | 72 + .../excel-collab/src/component/DateField.tsx | 43 + .../excel-collab/src/component/DatePicker.css | 27 + .../excel-collab/src/component/DatePicker.tsx | 45 + .../src/component/DateRangePicker.css | 71 + .../src/component/DateRangePicker.tsx | 49 + .../excel-collab/src/component/Dialog.css | 14 + .../excel-collab/src/component/Dialog.tsx | 19 + .../excel-collab/src/component/Disclosure.css | 77 + .../excel-collab/src/component/Disclosure.tsx | 35 + .../src/component/DisclosureGroup.css | 5 + .../src/component/DisclosureGroup.tsx | 10 + .../excel-collab/src/component/DropZone.css | 33 + .../excel-collab/src/component/DropZone.tsx | 9 + packages/excel-collab/src/component/Form.css | 108 + packages/excel-collab/src/component/Form.tsx | 28 + .../excel-collab/src/component/GridList.css | 311 ++ .../excel-collab/src/component/GridList.tsx | 62 + .../excel-collab/src/component/InputGroup.css | 48 + .../excel-collab/src/component/InputGroup.tsx | 31 + packages/excel-collab/src/component/Link.css | 31 + packages/excel-collab/src/component/Link.tsx | 7 + .../excel-collab/src/component/ListBox.css | 354 ++ .../excel-collab/src/component/ListBox.tsx | 66 + packages/excel-collab/src/component/Menu.css | 203 ++ packages/excel-collab/src/component/Menu.tsx | 94 + packages/excel-collab/src/component/Meter.css | 42 + packages/excel-collab/src/component/Meter.tsx | 37 + packages/excel-collab/src/component/Modal.css | 65 + packages/excel-collab/src/component/Modal.tsx | 16 + .../src/component/NavigationTree.css | 148 + .../src/component/NavigationTree.tsx | 67 + .../src/component/NumberField.css | 69 + .../src/component/NumberField.tsx | 38 + .../excel-collab/src/component/Popover.css | 81 + .../excel-collab/src/component/Popover.tsx | 32 + .../src/component/ProgressBar.css | 95 + .../src/component/ProgressBar.tsx | 30 + .../src/component/ProgressCircle.tsx | 64 + .../excel-collab/src/component/RadioGroup.css | 86 + .../excel-collab/src/component/RadioGroup.tsx | 57 + .../src/component/RangeCalendar.css | 126 + .../src/component/RangeCalendar.tsx | 76 + .../src/component/SearchField.css | 75 + .../src/component/SearchField.tsx | 39 + .../src/component/SegmentedControl.css | 58 + .../src/component/SegmentedControl.tsx | 33 + .../excel-collab/src/component/Select.css | 46 + .../excel-collab/src/component/Select.tsx | 60 + .../excel-collab/src/component/Separator.css | 15 + .../excel-collab/src/component/Separator.tsx | 11 + packages/excel-collab/src/component/Sheet.css | 66 + packages/excel-collab/src/component/Sheet.tsx | 19 + .../excel-collab/src/component/Slider.css | 157 + .../excel-collab/src/component/Slider.tsx | 55 + .../excel-collab/src/component/Switch.css | 96 + .../excel-collab/src/component/Switch.tsx | 38 + packages/excel-collab/src/component/Table.css | 372 +++ packages/excel-collab/src/component/Table.tsx | 151 + packages/excel-collab/src/component/Tabs.css | 144 + packages/excel-collab/src/component/Tabs.tsx | 45 + .../excel-collab/src/component/TagGroup.css | 116 + .../excel-collab/src/component/TagGroup.tsx | 67 + .../excel-collab/src/component/TextField.css | 43 + .../excel-collab/src/component/TextField.tsx | 59 + .../excel-collab/src/component/TimeField.css | 6 + .../excel-collab/src/component/TimeField.tsx | 32 + packages/excel-collab/src/component/Toast.css | 107 + packages/excel-collab/src/component/Toast.tsx | 63 + .../src/component/ToggleButton.css | 37 + .../src/component/ToggleButton.tsx | 39 + .../src/component/ToggleButtonGroup.css | 67 + .../src/component/ToggleButtonGroup.tsx | 10 + .../excel-collab/src/component/TokenField.css | 65 + .../excel-collab/src/component/TokenField.tsx | 52 + .../excel-collab/src/component/Toolbar.css | 29 + .../excel-collab/src/component/Toolbar.tsx | 17 + .../excel-collab/src/component/Tooltip.css | 58 + .../excel-collab/src/component/Tooltip.tsx | 30 + packages/excel-collab/src/component/Tree.css | 219 ++ packages/excel-collab/src/component/Tree.tsx | 84 + packages/excel-collab/src/component/index.ts | 1 + packages/excel-collab/src/component/theme.css | 237 ++ .../excel-collab/src/component/utilities.css | 287 ++ .../components/BaseIcon/BaseIcon.module.css | 6 - .../src/components/BaseIcon/BaseIcon.tsx | 34 - .../src/components/BaseIcon/FillColorIcon.tsx | 17 - .../src/components/BaseIcon/icon.ts | 133 - .../src/components/BaseIcon/index.tsx | 20 - .../src/components/Button/index.module.css | 33 - .../src/components/Button/index.tsx | 55 - .../ColorPicker/ColorPickerPanel.tsx | 63 - .../src/components/ColorPicker/Hue.tsx | 41 - .../components/ColorPicker/Interactive.tsx | 92 - .../src/components/ColorPicker/Pointer.tsx | 33 - .../src/components/ColorPicker/Saturation.tsx | 51 - .../src/components/ColorPicker/clamp.ts | 3 - .../src/components/ColorPicker/convert.ts | 100 - .../components/ColorPicker/index.module.css | 50 - .../src/components/ColorPicker/index.tsx | 100 - .../components/ColorPicker/panel.module.css | 94 - .../src/components/ColorPicker/round.ts | 7 - .../src/components/ColorPicker/types.ts | 53 - .../ColorPicker/useColorManipulation.ts | 50 - .../src/components/Dialog/index.module.css | 38 - .../src/components/Dialog/index.tsx | 123 +- .../src/components/Loading/index.module.css | 29 - .../src/components/Loading/index.tsx | 11 - .../src/components/Menu/index.module.css | 61 - .../src/components/Menu/index.tsx | 125 - .../src/components/Select/index.module.css | 81 - .../src/components/Select/index.tsx | 217 +- .../src/components/Toast/index.module.css | 49 - .../src/components/Toast/index.tsx | 68 - .../components/__tests__/BaseIcon.test.tsx | 11 - .../src/components/__tests__/Button.test.tsx | 23 - .../components/__tests__/ColorPicker.test.tsx | 10 - .../src/components/__tests__/Dialog.test.tsx | 65 - .../src/components/__tests__/Loading.test.tsx | 10 - .../src/components/__tests__/Menu.test.tsx | 63 - .../src/components/__tests__/Select.test.tsx | 95 - .../src/components/__tests__/Toast.test.tsx | 74 - packages/excel-collab/src/components/index.ts | 6 - .../src/containers/ColorPicker/index.tsx | 130 + .../containers/FloatElement/ContextMenu.tsx | 423 +-- .../FloatElement/FloatElement.module.css | 36 +- .../containers/FloatElement/FloatElement.tsx | 29 +- .../src/containers/FloatElement/Image.tsx | 11 +- .../src/containers/FloatElement/Toolbar.tsx | 38 +- .../containers/FloatElement/chart.module.css | 2 +- .../src/containers/FormulaBar/DefineName.tsx | 3 +- .../containers/FormulaBar/FormulaEditor.tsx | 2 +- .../containers/FormulaBar/index.module.css | 48 +- .../src/containers/MenuBar/File.tsx | 77 +- .../src/containers/MenuBar/I18N.tsx | 30 +- .../src/containers/MenuBar/Theme.tsx | 40 +- .../src/containers/MenuBar/User.tsx | 8 +- .../src/containers/MenuBar/index.module.css | 38 +- .../src/containers/MenuBar/index.tsx | 150 +- .../SheetBar/SheetBarContextMenu.tsx | 228 +- .../src/containers/SheetBar/index.module.css | 60 +- .../src/containers/SheetBar/index.tsx | 81 +- .../src/containers/ToolBar/Border.module.css | 21 - .../src/containers/ToolBar/Border.tsx | 188 +- .../src/containers/ToolBar/constant.ts | 18 - .../src/containers/ToolBar/index.module.css | 39 +- .../src/containers/ToolBar/index.tsx | 327 +- .../__tests__/FormulaEditor.test.ts | 2 +- .../src/containers/canvas/BottomBar.tsx | 5 +- .../src/containers/canvas/ContextMenu.tsx | 445 +-- .../__tests__/isSupportFontFamily.test.ts | 2 +- .../src/containers/canvas/index.module.css | 51 +- .../src/containers/canvas/index.tsx | 2 + .../containers/canvas/isSupportFontFamily.ts | 23 +- .../src/containers/canvas/modal/Filter.tsx | 62 +- .../canvas/modal/__tests__/index.test.tsx | 10 + .../containers/canvas/modal/index.module.css | 14 +- .../src/containers/canvas/util.ts | 10 +- .../hooks/__tests__/useEventCallback.test.ts | 40 - .../src/containers/hooks/index.ts | 1 - .../src/containers/hooks/useEventCallback.ts | 14 - .../excel-collab/src/containers/index.tsx | 11 +- packages/excel-collab/src/i18n/index.ts | 2 + packages/excel-collab/src/index.ts | 1 + packages/excel-collab/src/style.css | 84 +- .../src/theme/__tests__/theme.test.ts | 3 +- packages/excel-collab/src/theme/index.ts | 14 +- packages/excel-collab/src/theme/size.ts | 15 - packages/excel-collab/src/theme/zIndex.ts | 7 - packages/excel-collab/src/types/components.ts | 2 +- packages/excel-collab/src/types/yjs.ts | 21 +- packages/excel-collab/vite.config.mts | 1 + scripts/jest.setup.js | 22 - tsconfig.node.json | 5 +- yarn.lock | 2959 +++++++++-------- 234 files changed, 11019 insertions(+), 5413 deletions(-) create mode 100644 packages/excel-collab/src/component/Breadcrumbs.css create mode 100644 packages/excel-collab/src/component/Breadcrumbs.tsx create mode 100644 packages/excel-collab/src/component/Button.css create mode 100644 packages/excel-collab/src/component/Button.tsx create mode 100644 packages/excel-collab/src/component/Calendar.css create mode 100644 packages/excel-collab/src/component/Calendar.tsx create mode 100644 packages/excel-collab/src/component/Checkbox.css create mode 100644 packages/excel-collab/src/component/Checkbox.tsx create mode 100644 packages/excel-collab/src/component/CheckboxGroup.css create mode 100644 packages/excel-collab/src/component/CheckboxGroup.tsx create mode 100644 packages/excel-collab/src/component/ColorArea.css create mode 100644 packages/excel-collab/src/component/ColorArea.tsx create mode 100644 packages/excel-collab/src/component/ColorField.css create mode 100644 packages/excel-collab/src/component/ColorField.tsx create mode 100644 packages/excel-collab/src/component/ColorPicker.css create mode 100644 packages/excel-collab/src/component/ColorPicker.tsx create mode 100644 packages/excel-collab/src/component/ColorSlider.css create mode 100644 packages/excel-collab/src/component/ColorSlider.tsx create mode 100644 packages/excel-collab/src/component/ColorSwatch.css create mode 100644 packages/excel-collab/src/component/ColorSwatch.tsx create mode 100644 packages/excel-collab/src/component/ColorSwatchPicker.css create mode 100644 packages/excel-collab/src/component/ColorSwatchPicker.tsx create mode 100644 packages/excel-collab/src/component/ColorThumb.css create mode 100644 packages/excel-collab/src/component/ColorThumb.tsx create mode 100644 packages/excel-collab/src/component/ColorWheel.css create mode 100644 packages/excel-collab/src/component/ColorWheel.tsx create mode 100644 packages/excel-collab/src/component/ComboBox.css create mode 100644 packages/excel-collab/src/component/ComboBox.tsx create mode 100644 packages/excel-collab/src/component/CommandPalette.css create mode 100644 packages/excel-collab/src/component/CommandPalette.tsx create mode 100644 packages/excel-collab/src/component/Content.css create mode 100644 packages/excel-collab/src/component/Content.tsx create mode 100644 packages/excel-collab/src/component/DateField.css create mode 100644 packages/excel-collab/src/component/DateField.tsx create mode 100644 packages/excel-collab/src/component/DatePicker.css create mode 100644 packages/excel-collab/src/component/DatePicker.tsx create mode 100644 packages/excel-collab/src/component/DateRangePicker.css create mode 100644 packages/excel-collab/src/component/DateRangePicker.tsx create mode 100644 packages/excel-collab/src/component/Dialog.css create mode 100644 packages/excel-collab/src/component/Dialog.tsx create mode 100644 packages/excel-collab/src/component/Disclosure.css create mode 100644 packages/excel-collab/src/component/Disclosure.tsx create mode 100644 packages/excel-collab/src/component/DisclosureGroup.css create mode 100644 packages/excel-collab/src/component/DisclosureGroup.tsx create mode 100644 packages/excel-collab/src/component/DropZone.css create mode 100644 packages/excel-collab/src/component/DropZone.tsx create mode 100644 packages/excel-collab/src/component/Form.css create mode 100644 packages/excel-collab/src/component/Form.tsx create mode 100644 packages/excel-collab/src/component/GridList.css create mode 100644 packages/excel-collab/src/component/GridList.tsx create mode 100644 packages/excel-collab/src/component/InputGroup.css create mode 100644 packages/excel-collab/src/component/InputGroup.tsx create mode 100644 packages/excel-collab/src/component/Link.css create mode 100644 packages/excel-collab/src/component/Link.tsx create mode 100644 packages/excel-collab/src/component/ListBox.css create mode 100644 packages/excel-collab/src/component/ListBox.tsx create mode 100644 packages/excel-collab/src/component/Menu.css create mode 100644 packages/excel-collab/src/component/Menu.tsx create mode 100644 packages/excel-collab/src/component/Meter.css create mode 100644 packages/excel-collab/src/component/Meter.tsx create mode 100644 packages/excel-collab/src/component/Modal.css create mode 100644 packages/excel-collab/src/component/Modal.tsx create mode 100644 packages/excel-collab/src/component/NavigationTree.css create mode 100644 packages/excel-collab/src/component/NavigationTree.tsx create mode 100644 packages/excel-collab/src/component/NumberField.css create mode 100644 packages/excel-collab/src/component/NumberField.tsx create mode 100644 packages/excel-collab/src/component/Popover.css create mode 100644 packages/excel-collab/src/component/Popover.tsx create mode 100644 packages/excel-collab/src/component/ProgressBar.css create mode 100644 packages/excel-collab/src/component/ProgressBar.tsx create mode 100644 packages/excel-collab/src/component/ProgressCircle.tsx create mode 100644 packages/excel-collab/src/component/RadioGroup.css create mode 100644 packages/excel-collab/src/component/RadioGroup.tsx create mode 100644 packages/excel-collab/src/component/RangeCalendar.css create mode 100644 packages/excel-collab/src/component/RangeCalendar.tsx create mode 100644 packages/excel-collab/src/component/SearchField.css create mode 100644 packages/excel-collab/src/component/SearchField.tsx create mode 100644 packages/excel-collab/src/component/SegmentedControl.css create mode 100644 packages/excel-collab/src/component/SegmentedControl.tsx create mode 100644 packages/excel-collab/src/component/Select.css create mode 100644 packages/excel-collab/src/component/Select.tsx create mode 100644 packages/excel-collab/src/component/Separator.css create mode 100644 packages/excel-collab/src/component/Separator.tsx create mode 100644 packages/excel-collab/src/component/Sheet.css create mode 100644 packages/excel-collab/src/component/Sheet.tsx create mode 100644 packages/excel-collab/src/component/Slider.css create mode 100644 packages/excel-collab/src/component/Slider.tsx create mode 100644 packages/excel-collab/src/component/Switch.css create mode 100644 packages/excel-collab/src/component/Switch.tsx create mode 100644 packages/excel-collab/src/component/Table.css create mode 100644 packages/excel-collab/src/component/Table.tsx create mode 100644 packages/excel-collab/src/component/Tabs.css create mode 100644 packages/excel-collab/src/component/Tabs.tsx create mode 100644 packages/excel-collab/src/component/TagGroup.css create mode 100644 packages/excel-collab/src/component/TagGroup.tsx create mode 100644 packages/excel-collab/src/component/TextField.css create mode 100644 packages/excel-collab/src/component/TextField.tsx create mode 100644 packages/excel-collab/src/component/TimeField.css create mode 100644 packages/excel-collab/src/component/TimeField.tsx create mode 100644 packages/excel-collab/src/component/Toast.css create mode 100644 packages/excel-collab/src/component/Toast.tsx create mode 100644 packages/excel-collab/src/component/ToggleButton.css create mode 100644 packages/excel-collab/src/component/ToggleButton.tsx create mode 100644 packages/excel-collab/src/component/ToggleButtonGroup.css create mode 100644 packages/excel-collab/src/component/ToggleButtonGroup.tsx create mode 100644 packages/excel-collab/src/component/TokenField.css create mode 100644 packages/excel-collab/src/component/TokenField.tsx create mode 100644 packages/excel-collab/src/component/Toolbar.css create mode 100644 packages/excel-collab/src/component/Toolbar.tsx create mode 100644 packages/excel-collab/src/component/Tooltip.css create mode 100644 packages/excel-collab/src/component/Tooltip.tsx create mode 100644 packages/excel-collab/src/component/Tree.css create mode 100644 packages/excel-collab/src/component/Tree.tsx create mode 100644 packages/excel-collab/src/component/index.ts create mode 100644 packages/excel-collab/src/component/theme.css create mode 100644 packages/excel-collab/src/component/utilities.css delete mode 100644 packages/excel-collab/src/components/BaseIcon/BaseIcon.module.css delete mode 100644 packages/excel-collab/src/components/BaseIcon/BaseIcon.tsx delete mode 100644 packages/excel-collab/src/components/BaseIcon/FillColorIcon.tsx delete mode 100644 packages/excel-collab/src/components/BaseIcon/icon.ts delete mode 100644 packages/excel-collab/src/components/BaseIcon/index.tsx delete mode 100644 packages/excel-collab/src/components/Button/index.module.css delete mode 100644 packages/excel-collab/src/components/Button/index.tsx delete mode 100644 packages/excel-collab/src/components/ColorPicker/ColorPickerPanel.tsx delete mode 100644 packages/excel-collab/src/components/ColorPicker/Hue.tsx delete mode 100644 packages/excel-collab/src/components/ColorPicker/Interactive.tsx delete mode 100644 packages/excel-collab/src/components/ColorPicker/Pointer.tsx delete mode 100644 packages/excel-collab/src/components/ColorPicker/Saturation.tsx delete mode 100644 packages/excel-collab/src/components/ColorPicker/clamp.ts delete mode 100644 packages/excel-collab/src/components/ColorPicker/convert.ts delete mode 100644 packages/excel-collab/src/components/ColorPicker/index.module.css delete mode 100644 packages/excel-collab/src/components/ColorPicker/index.tsx delete mode 100644 packages/excel-collab/src/components/ColorPicker/panel.module.css delete mode 100644 packages/excel-collab/src/components/ColorPicker/round.ts delete mode 100644 packages/excel-collab/src/components/ColorPicker/types.ts delete mode 100644 packages/excel-collab/src/components/ColorPicker/useColorManipulation.ts delete mode 100644 packages/excel-collab/src/components/Dialog/index.module.css delete mode 100644 packages/excel-collab/src/components/Loading/index.module.css delete mode 100644 packages/excel-collab/src/components/Loading/index.tsx delete mode 100644 packages/excel-collab/src/components/Menu/index.module.css delete mode 100644 packages/excel-collab/src/components/Menu/index.tsx delete mode 100644 packages/excel-collab/src/components/Toast/index.module.css delete mode 100644 packages/excel-collab/src/components/Toast/index.tsx delete mode 100644 packages/excel-collab/src/components/__tests__/BaseIcon.test.tsx delete mode 100644 packages/excel-collab/src/components/__tests__/Button.test.tsx delete mode 100644 packages/excel-collab/src/components/__tests__/ColorPicker.test.tsx delete mode 100644 packages/excel-collab/src/components/__tests__/Dialog.test.tsx delete mode 100644 packages/excel-collab/src/components/__tests__/Loading.test.tsx delete mode 100644 packages/excel-collab/src/components/__tests__/Menu.test.tsx delete mode 100644 packages/excel-collab/src/components/__tests__/Select.test.tsx delete mode 100644 packages/excel-collab/src/components/__tests__/Toast.test.tsx create mode 100644 packages/excel-collab/src/containers/ColorPicker/index.tsx delete mode 100644 packages/excel-collab/src/containers/hooks/__tests__/useEventCallback.test.ts delete mode 100644 packages/excel-collab/src/containers/hooks/useEventCallback.ts delete mode 100644 packages/excel-collab/src/theme/size.ts delete mode 100644 packages/excel-collab/src/theme/zIndex.ts diff --git a/.gitignore b/.gitignore index 9785a94d..c5f8bc91 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ playwright report docs *.diff.png +*.actual.png /playwright-report/ /blob-report/ /playwright/.cache/ diff --git a/package.json b/package.json index e8a8d6b2..7cff8b34 100644 --- a/package.json +++ b/package.json @@ -33,35 +33,35 @@ }, "devDependencies": { "@codecov/vite-plugin": "^2.0.1", - "@playwright/test": "^1.63.0", + "@playwright/test": "^1.64.0", "@testing-library/dom": "^10.4.2", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", "@testing-library/user-event": "^14.6.7", "@types/jest": "^30.0.0", - "@types/node": "^26.5.1", + "@types/node": "^26.6.5", "@types/pngjs": "^6.0.5", "@types/react": "19.3.0", "@types/react-dom": "19.3.0", - "@vitejs/plugin-react": "^6.1.1", + "@vitejs/plugin-react": "^6.1.2", "canvas": "^3.2.3", "check-dependency-version-consistency": "^6.0.0", "husky": "^9.1.7", - "jest": "^30.5.1", - "jest-environment-jsdom": "^30.5.1", + "jest": "^30.5.2", + "jest-environment-jsdom": "^30.5.2", "jest-junit": "^17.0.0", - "lerna": "^10.0.1", + "lerna": "^10.1.0", "pngjs": "^7.0.0", "react": "19.3.0", "react-dom": "19.3.0", - "ts-jest": "^29.4.12", - "tsx": "^4.23.13", + "ts-jest": "^29.4.14", + "tsx": "^4.23.15", "typescript": "^6.0.3", - "vite": "^8.3.0", - "vite-plugin-dts": "^5.1.0" + "vite": "^8.3.4", + "vite-plugin-dts": "^5.1.2" }, "resolutions": { - "yjs": "^13.6.32" + "yjs": "^13.6.33" }, "packageManager": "yarn@1.22.22" } diff --git a/packages/demo/backend/package.json b/packages/demo/backend/package.json index 9ef3aa41..f40b9789 100644 --- a/packages/demo/backend/package.json +++ b/packages/demo/backend/package.json @@ -16,12 +16,13 @@ "dependencies": { "@koa/cors": "^5.0.0", "@koa/router": "^15.7.0", - "@types/ws": "^8.18.1", - "@y/websocket-server": "^0.1.5", - "dotenv": "^17.4.2", + "@types/ws": "^8.18.2", + "@y/websocket-server": "0.1.1", + "dotenv": "^18.0.7", "koa": "^3.2.1", "koa-body": "^8.0.1", "sqlite3": "^6.0.1", - "ws": "^8.21.3" + "ws": "^8.22.0", + "yjs": "^13.6.33" } } diff --git a/packages/demo/backend/src/__tests__/websocket.test.ts b/packages/demo/backend/src/__tests__/websocket.test.ts index 21c4e243..acd83148 100644 --- a/packages/demo/backend/src/__tests__/websocket.test.ts +++ b/packages/demo/backend/src/__tests__/websocket.test.ts @@ -30,6 +30,67 @@ jest.spyOn(console, 'error').mockImplementation(() => {}); // Import the WebSocketManager after mocks are set up import { WebSocketManager } from '../websocket'; import http from 'http'; +import { EventEmitter } from 'node:events'; + +describe('Yjs protocol compatibility', () => { + it('should complete real sync handshakes and apply Yjs 13 updates', () => { + const { setupWSConnection, docs } = jest.requireActual( + '@y/websocket-server/utils', + ); + const Y = jest.requireActual('yjs'); + const sync = jest.requireActual('y-protocols/sync'); + const encoding = jest.requireActual('lib0/encoding'); + const decoding = jest.requireActual('lib0/decoding'); + const messages: Uint8Array[] = []; + const connection = Object.assign(new EventEmitter(), { + readyState: 1, + send: jest.fn((message: Uint8Array) => messages.push(message)), + close: jest.fn(), + ping: jest.fn(), + }); + const docName = 'protocol-compatibility-test'; + const client = new Y.Doc(); + const protocolErrors: Error[] = []; + + try { + setupWSConnection(connection, { url: `/${docName}` }, { docName }); + docs + .get(docName) + .on('error', (error: Error) => protocolErrors.push(error)); + expect(messages).toHaveLength(1); + + const handshake = decoding.createDecoder(messages.shift()); + expect(decoding.readVarUint(handshake)).toBe(0); + const reply = encoding.createEncoder(); + encoding.writeVarUint(reply, 0); + sync.readSyncMessage(handshake, reply, client, null); + connection.emit('message', encoding.toUint8Array(reply)); + + const request = encoding.createEncoder(); + encoding.writeVarUint(request, 0); + sync.writeSyncStep1(request, client); + connection.emit('message', encoding.toUint8Array(request)); + expect(messages).toHaveLength(1); + + const response = decoding.createDecoder(messages.shift()); + expect(decoding.readVarUint(response)).toBe(0); + sync.readSyncMessage(response, encoding.createEncoder(), client, null); + + client.getMap('sheet').set('A1', 'compatible'); + const update = encoding.createEncoder(); + encoding.writeVarUint(update, 0); + sync.writeUpdate(update, Y.encodeStateAsUpdate(client)); + connection.emit('message', encoding.toUint8Array(update)); + expect(docs.get(docName).getMap('sheet').get('A1')).toBe('compatible'); + expect(protocolErrors).toEqual([]); + } finally { + connection.emit('close'); + docs.get(docName)?.destroy(); + docs.delete(docName); + client.destroy(); + } + }); +}); describe('WebSocketManager Tests', () => { let mockServer: http.Server; @@ -40,10 +101,10 @@ describe('WebSocketManager Tests', () => { beforeEach(() => { jest.clearAllMocks(); - + // Create mock server mockServer = {} as http.Server; - + // Create mock WebSocket and request mockWs = { on: jest.fn(), @@ -51,7 +112,7 @@ describe('WebSocketManager Tests', () => { send: jest.fn(), }; mockRequest = { url: '/test-doc-1' } as http.IncomingMessage; - + // Create mock WebSocketServer with a connection event handler mockWss = { on: jest.fn().mockImplementation((event, callback) => { @@ -62,13 +123,13 @@ describe('WebSocketManager Tests', () => { }), close: jest.fn(), }; - + // Import ws module to access the mocked Server constructor const { WebSocketServer: WSServer } = require('ws'); // Mock the WebSocketServer constructor to return our mockWss (WSServer as jest.Mock).mockReturnValue(mockWss); - + // Create WebSocketManager instance wsManager = new WebSocketManager(mockServer); }); @@ -84,13 +145,19 @@ describe('WebSocketManager Tests', () => { // Verify that connection event handler was registered expect(mockWss.on).toHaveBeenCalledWith( 'connection', - expect.any(Function) + expect.any(Function), ); }); it('should log environment variables CALLBACK_OBJECTS and CALLBACK_URL', () => { - expect(console.log).toHaveBeenCalledWith('CALLBACK_OBJECTS: ', process.env.CALLBACK_OBJECTS); - expect(console.log).toHaveBeenCalledWith('CALLBACK_URL: ', process.env.CALLBACK_URL); + expect(console.log).toHaveBeenCalledWith( + 'CALLBACK_OBJECTS: ', + process.env.CALLBACK_OBJECTS, + ); + expect(console.log).toHaveBeenCalledWith( + 'CALLBACK_URL: ', + process.env.CALLBACK_URL, + ); }); }); @@ -98,44 +165,43 @@ describe('WebSocketManager Tests', () => { it('should handle new connections and register them', () => { // Import ws module to access the mocked WebSocket constructor const WebSocket = require('ws').default; - + // Mock the WebSocket constructor to return our mockWs (WebSocket as jest.Mock).mockReturnValue(mockWs); - + // Simulate a new connection by calling the stored callback if (mockWss.connectionCallback) { mockWss.connectionCallback(mockWs, mockRequest); } - + // Verify connection was registered expect(wsManager.getConnectionCount()).toBe(1); - expect(mockSetupWSConnection).toHaveBeenCalledWith( - mockWs, - mockRequest, - { gc: true, docName: 'test-doc-1' } - ); + expect(mockSetupWSConnection).toHaveBeenCalledWith(mockWs, mockRequest, { + gc: true, + docName: 'test-doc-1', + }); }); it('should generate unique connection IDs', () => { // Import ws module to access the mocked WebSocket constructor const WebSocket = require('ws').default; - + // Mock the WebSocket constructor to return our mockWs (WebSocket as jest.Mock).mockReturnValue(mockWs); - + // Create a second mock WebSocket const mockWs2 = { on: jest.fn(), close: jest.fn(), send: jest.fn(), }; - + // Simulate two connections if (mockWss.connectionCallback) { mockWss.connectionCallback(mockWs, mockRequest); mockWss.connectionCallback(mockWs2, mockRequest); } - + // Verify both connections are registered expect(wsManager.getConnectionCount()).toBe(2); }); @@ -143,23 +209,23 @@ describe('WebSocketManager Tests', () => { it('should handle connection errors', () => { // Import ws module to access the mocked WebSocket constructor const WebSocket = require('ws').default; - + // Mock the WebSocket constructor to return our mockWs (WebSocket as jest.Mock).mockReturnValue(mockWs); - + // Make setupWSConnection throw an error mockSetupWSConnection.mockImplementation(() => { throw new Error('Setup error'); }); - + // Simulate a new connection if (mockWss.connectionCallback) { mockWss.connectionCallback(mockWs, mockRequest); } - + // Verify error was logged and connection was closed expect(console.error).toHaveBeenCalledWith( - 'Error setting up WebSocket connection: Error: Setup error' + 'Error setting up WebSocket connection: Error: Setup error', ); expect(mockWs.close).toHaveBeenCalledWith(1011, 'Internal server error'); }); @@ -167,15 +233,15 @@ describe('WebSocketManager Tests', () => { it('should handle message events', () => { // Import ws module to access the mocked WebSocket constructor const WebSocket = require('ws').default; - + // Mock the WebSocket constructor to return our mockWs (WebSocket as jest.Mock).mockReturnValue(mockWs); - + // Simulate a new connection if (mockWss.connectionCallback) { mockWss.connectionCallback(mockWs, mockRequest); } - + // Verify message handler was registered expect(mockWs.on).toHaveBeenCalledWith('message', expect.any(Function)); }); @@ -183,55 +249,55 @@ describe('WebSocketManager Tests', () => { it('should handle close events and remove connections', () => { // Import ws module to access the mocked WebSocket constructor const WebSocket = require('ws').default; - + // Mock the WebSocket constructor to return our mockWs (WebSocket as jest.Mock).mockReturnValue(mockWs); - + // Simulate a new connection if (mockWss.connectionCallback) { mockWss.connectionCallback(mockWs, mockRequest); } expect(wsManager.getConnectionCount()).toBe(1); - + // Get the close event handler const closeHandler = mockWs.on.mock.calls.find( - (call: any[]) => call[0] === 'close' + (call: any[]) => call[0] === 'close', )[1]; - + // Simulate closing the connection closeHandler(1000, 'Normal closure'); - + // Verify connection was removed expect(wsManager.getConnectionCount()).toBe(0); expect(console.log).toHaveBeenCalledWith( - expect.stringContaining('WebSocket connection closed') + expect.stringContaining('WebSocket connection closed'), ); }); it('should handle error events', () => { // Import ws module to access the mocked WebSocket constructor const WebSocket = require('ws').default; - + // Mock the WebSocket constructor to return our mockWs (WebSocket as jest.Mock).mockReturnValue(mockWs); - + // Simulate a new connection if (mockWss.connectionCallback) { mockWss.connectionCallback(mockWs, mockRequest); } - + // Get the error event handler const errorHandler = mockWs.on.mock.calls.find( - (call: any[]) => call[0] === 'error' + (call: any[]) => call[0] === 'error', )[1]; - + // Simulate an error const mockError = new Error('WebSocket error'); errorHandler(mockError); - + // Verify error was logged expect(console.error).toHaveBeenCalledWith( - expect.stringContaining('WebSocket error for connection') + expect.stringContaining('WebSocket error for connection'), ); }); }); @@ -240,19 +306,21 @@ describe('WebSocketManager Tests', () => { it('should handle server-level errors', () => { // Simulate a server error const mockError = new Error('Server error'); - + // Get the error event handler from the WebSocketServer const errorHandler = mockWss.on.mock.calls.find( - (call: any[]) => call[0] === 'error' + (call: any[]) => call[0] === 'error', )?.[1]; - + // Call the error handler if it exists if (errorHandler) { errorHandler(mockError); } - + // Verify error was logged - expect(console.error).toHaveBeenCalledWith('WebSocket server error: Error: Server error'); + expect(console.error).toHaveBeenCalledWith( + 'WebSocket server error: Error: Server error', + ); }); }); @@ -260,26 +328,26 @@ describe('WebSocketManager Tests', () => { it('should return the correct connection count', () => { // Initial count should be 0 expect(wsManager.getConnectionCount()).toBe(0); - + // Import ws module to access the mocked WebSocket constructor const WebSocket = require('ws').default; - + // Mock the WebSocket constructor to return our mockWs (WebSocket as jest.Mock).mockReturnValue(mockWs); - + // Create a second mock WebSocket const mockWs2 = { on: jest.fn(), close: jest.fn(), send: jest.fn(), }; - + // Simulate two connections if (mockWss.connectionCallback) { mockWss.connectionCallback(mockWs, mockRequest); mockWss.connectionCallback(mockWs2, mockRequest); } - + // Count should now be 2 expect(wsManager.getConnectionCount()).toBe(2); }); @@ -289,14 +357,16 @@ describe('WebSocketManager Tests', () => { mockWss.close = jest.fn().mockImplementation((cb: any) => { if (typeof cb === 'function') cb(); }); - + wsManager.closeAllConnections(); - + // Verify WebSocketServer close was called expect(mockWss.close).toHaveBeenCalled(); - + // The console.log should have been called by the callback - expect(console.log).toHaveBeenCalledWith('All WebSocket connections have been closed'); + expect(console.log).toHaveBeenCalledWith( + 'All WebSocket connections have been closed', + ); }); }); @@ -304,46 +374,46 @@ describe('WebSocketManager Tests', () => { it('should handle connections without a docId in URL', () => { // Import ws module to access the mocked WebSocket constructor const WebSocket = require('ws').default; - + // Mock the WebSocket constructor to return our mockWs (WebSocket as jest.Mock).mockReturnValue(mockWs); - + // Create request without URL const requestWithoutUrl = {} as http.IncomingMessage; - + // Simulate a new connection if (mockWss.connectionCallback) { mockWss.connectionCallback(mockWs, requestWithoutUrl); } - + // Verify connection was set up with empty docName expect(mockSetupWSConnection).toHaveBeenCalledWith( mockWs, requestWithoutUrl, - { gc: true, docName: '' } + { gc: true, docName: '' }, ); }); it('should handle connections with empty URL path', () => { // Import ws module to access the mocked WebSocket constructor const WebSocket = require('ws').default; - + // Mock the WebSocket constructor to return our mockWs (WebSocket as jest.Mock).mockReturnValue(mockWs); - + // Create request with empty URL const requestWithEmptyUrl = { url: '/' } as http.IncomingMessage; - + // Simulate a new connection if (mockWss.connectionCallback) { mockWss.connectionCallback(mockWs, requestWithEmptyUrl); } - + // Verify connection was set up with empty docName expect(mockSetupWSConnection).toHaveBeenCalledWith( mockWs, requestWithEmptyUrl, - { gc: true, docName: '' } + { gc: true, docName: '' }, ); }); }); @@ -352,20 +422,20 @@ describe('WebSocketManager Tests', () => { it('should log messages received from clients', () => { // Import ws module to access the mocked WebSocket constructor const WebSocket = require('ws').default; - + // Mock the WebSocket constructor to return our mockWs (WebSocket as jest.Mock).mockReturnValue(mockWs); - + // Simulate a new connection if (mockWss.connectionCallback) { mockWss.connectionCallback(mockWs, mockRequest); } - + // Get all the message event handlers const messageHandlers = mockWs.on.mock.calls .filter((call: any[]) => call[0] === 'message') .map((call: any[]) => call[1]); - + // Verify at least one message handler was registered expect(messageHandlers.length).toBeGreaterThan(0); }); diff --git a/packages/demo/backend/src/index.ts b/packages/demo/backend/src/index.ts index fbeab3f9..4b0e0f2e 100644 --- a/packages/demo/backend/src/index.ts +++ b/packages/demo/backend/src/index.ts @@ -9,6 +9,7 @@ dotenv.config({ path: path.join(__dirname, '..', '.env'), debug: true, override: true, + encoding: 'utf8', }); process.env['CALLBACK_OBJECTS'] = JSON.stringify({ excel: 'Map' }); @@ -36,6 +37,7 @@ const envList = [ ]; if (!fs.existsSync(filePath)) { + console.log('update frontend env file: ', filePath); fs.writeFileSync(filePath, envList.join('\n')); } diff --git a/packages/demo/backend/tsconfig.json b/packages/demo/backend/tsconfig.json index c2d96ba6..7990020c 100644 --- a/packages/demo/backend/tsconfig.json +++ b/packages/demo/backend/tsconfig.json @@ -23,7 +23,7 @@ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ "module": "commonjs" /* Specify what module code is generated. */, - "rootDir": "./" /* Specify the root folder within your source files. */, + "rootDir": "src" /* Specify the root folder within your source files. */, // "moduleResolution": "node16", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -101,5 +101,5 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */, "types": ["@types/jest", "@types/node"] }, - "include": ["src", "prisma"] + "include": ["src"] } diff --git a/packages/demo/frontend/index.html b/packages/demo/frontend/index.html index 9ccf0f97..5f3b936b 100644 --- a/packages/demo/frontend/index.html +++ b/packages/demo/frontend/index.html @@ -1,71 +1,138 @@ - + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + - - + + + - - - + - - - Online Collaboration Excel - - - - - - - - - - - - -
- - + Online Collaboration Excel + + + + + + + + + + + +
+ + diff --git a/packages/demo/frontend/package.json b/packages/demo/frontend/package.json index 2ed16ee0..9d6885e2 100644 --- a/packages/demo/frontend/package.json +++ b/packages/demo/frontend/package.json @@ -11,10 +11,10 @@ "test:ci": "JEST_JUNIT_CLASSNAME=\"{filepath}\" jest --reporters=jest-junit --forceExit --detectOpenHandles" }, "dependencies": { - "@sentry/react": "^11.0.0", + "@sentry/react": "^11.6.0", "excel-collab": "*", - "uuid": "^14.0.2", + "uuid": "^14.0.3", "y-websocket": "^3.1.0", - "yjs": "^13.6.32" + "yjs": "^13.6.33" } } diff --git a/packages/demo/frontend/src/index.tsx b/packages/demo/frontend/src/index.tsx index ae62015c..939e6558 100644 --- a/packages/demo/frontend/src/index.tsx +++ b/packages/demo/frontend/src/index.tsx @@ -33,7 +33,9 @@ function App(props: Omit & { provider: IProvider }) { {...props} menubarLeftChildren={
- +
} /> diff --git a/packages/demo/frontend/vite.config.mts b/packages/demo/frontend/vite.config.mts index 4e1d6e23..33a6610f 100644 --- a/packages/demo/frontend/vite.config.mts +++ b/packages/demo/frontend/vite.config.mts @@ -19,7 +19,7 @@ function htmlSlot(options: Record) { } export default defineConfig((env) => { - const isLocalMode = env.mode === 'development'; + const isLocalMode = env.mode === 'development' || env.mode === 'e2e'; const dirPath = join(import.meta.dirname, '..', '..', 'excel-collab', 'src'); const alias: AliasOptions = isLocalMode ? { diff --git a/packages/excel-collab/package.json b/packages/excel-collab/package.json index b42b1577..9604e8c5 100644 --- a/packages/excel-collab/package.json +++ b/packages/excel-collab/package.json @@ -30,8 +30,10 @@ "@formulajs/formulajs": "^4.6.1", "chart.js": "^4.5.1", "comlink": "^4.4.2", + "lucide-react": "^1.55.0", "numfmt": "^3.2.6", - "uuid": "^14.0.2", + "react-aria-components": "^1.22.1", + "uuid": "^14.0.3", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zustand": "^5.0.15" }, @@ -59,6 +61,6 @@ }, "devDependencies": { "y-protocols": "^1.0.7", - "yjs": "^13.6.32" + "yjs": "^13.6.33" } } diff --git a/packages/excel-collab/src/__tests__/Border.test.tsx b/packages/excel-collab/src/__tests__/Border.test.tsx index a2d78372..379014a6 100644 --- a/packages/excel-collab/src/__tests__/Border.test.tsx +++ b/packages/excel-collab/src/__tests__/Border.test.tsx @@ -3,6 +3,7 @@ import { BORDER_TYPE_MAP } from '../util'; import { BorderItem, IController } from '../types'; import { renderComponent } from './util'; import './global.mock'; +import userEvent from '@testing-library/user-event'; describe('Border.test.tsx', () => { let controller: IController; @@ -11,12 +12,11 @@ describe('Border.test.tsx', () => { controller = result.controller; }); describe('Line Color', () => { - test('ok', () => { - fireEvent.click(screen.getByTestId('toolbar-border-trigger')); - fireEvent.click(screen.getByTestId('toolbar-border-color-trigger')); - const dom = screen.getByTestId('toolbar-border-color-list'); - dom.setAttribute('data-value', '#B2B2B2'); - fireEvent.click(dom); + test('ok', async () => { + const user = userEvent.setup(); + await user.click(screen.getByTestId('toolbar-border-trigger')); + await user.hover(screen.getByTestId('toolbar-border-color-trigger')); + await user.click(await screen.findByRole('menuitem', { name: '#B2B2B2' })); expect( controller.getCell(controller.getActiveRange().range)?.borderLeft, ).toEqual({ @@ -27,14 +27,16 @@ describe('Border.test.tsx', () => { }); describe('Line Style', () => { for (const item of Object.keys(BORDER_TYPE_MAP)) { - test(item, () => { + test(item, async () => { + const user = userEvent.setup(); const selector = `toolbar-border-style-${item}`; - fireEvent.click(screen.getByTestId('toolbar-border-trigger')); - fireEvent.click(screen.getByTestId('toolbar-border-style')); - fireEvent.click(screen.getByTestId(selector)); - - fireEvent.click(screen.getByTestId('toolbar-border-style')); - expect(screen.getByTestId(selector)).toHaveClass('active'); + await user.click(screen.getByTestId('toolbar-border-trigger')); + await user.hover(screen.getByTestId('toolbar-border-style')); + await user.click(await screen.findByTestId(selector)); + expect( + controller.getCell(controller.getActiveRange().range)?.borderTop + ?.type, + ).toBe(item); }); } }); @@ -73,6 +75,7 @@ describe('Border.test.tsx', () => { borderBottom: item, }); + fireEvent.click(screen.getByTestId('toolbar-border-trigger')); fireEvent.click(screen.getByTestId('toolbar-no-border')); expect( diff --git a/packages/excel-collab/src/__tests__/Export.test.tsx b/packages/excel-collab/src/__tests__/Export.test.tsx index 25515425..396ef052 100644 --- a/packages/excel-collab/src/__tests__/Export.test.tsx +++ b/packages/excel-collab/src/__tests__/Export.test.tsx @@ -1,4 +1,5 @@ import { screen, fireEvent, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { type, renderComponent } from './util'; import './global.mock'; @@ -62,9 +63,11 @@ describe('Export.test.ts', () => { }); describe('download xlsx', () => { test('ok', async () => { + const user = userEvent.setup(); type('test'); - fireEvent.click(screen.getByTestId('menubar-excel-trigger')); - fireEvent.click(screen.getByTestId('menubar-export-more-xlsx')); + await user.click(screen.getByTestId('menubar-excel-trigger')); + await user.hover(screen.getByRole('menuitem', { name: /export/i })); + await user.click(await screen.findByTestId('menubar-export-more-xlsx')); await waitFor(() => { expect(mockSaveAs).toHaveBeenCalled(); }); @@ -72,26 +75,34 @@ describe('Export.test.ts', () => { }); describe('download csv', () => { test('ok', async () => { + const user = userEvent.setup(); type('test'); - fireEvent.click(screen.getByTestId('menubar-excel-trigger')); - fireEvent.click(screen.getByTestId('menubar-export-more-csv')); + await user.click(screen.getByTestId('menubar-excel-trigger')); + await user.hover(screen.getByRole('menuitem', { name: /export/i })); + await user.click(await screen.findByTestId('menubar-export-more-csv')); expect(mockSaveAs).toHaveBeenCalled(); }); }); describe('download json', () => { test('ok', async () => { + const user = userEvent.setup(); type('test'); - fireEvent.click(screen.getByTestId('menubar-excel-trigger')); - fireEvent.click(screen.getByTestId('menubar-export-json')); + await user.click(screen.getByTestId('menubar-excel-trigger')); + await user.hover(screen.getByRole('menuitem', { name: /export/i })); + await user.click(await screen.findByTestId('menubar-export-json')); expect(mockSaveAs).toHaveBeenCalled(); }); }); describe('download other formats', () => { for (const ext of ['xlsb', 'xls', 'ods', 'html', 'dbf'] as const) { test(ext, async () => { + const user = userEvent.setup(); type('test'); - fireEvent.click(screen.getByTestId('menubar-excel-trigger')); - fireEvent.click(screen.getByTestId(`menubar-export-more-${ext}`)); + await user.click(screen.getByTestId('menubar-excel-trigger')); + await user.hover(screen.getByRole('menuitem', { name: /export/i })); + await user.click( + await screen.findByTestId(`menubar-export-more-${ext}`), + ); await waitFor(() => { expect(mockSaveAs).toHaveBeenCalled(); }); diff --git a/packages/excel-collab/src/__tests__/Filter.test.tsx b/packages/excel-collab/src/__tests__/Filter.test.tsx index 137b93f3..25ba3c6c 100644 --- a/packages/excel-collab/src/__tests__/Filter.test.tsx +++ b/packages/excel-collab/src/__tests__/Filter.test.tsx @@ -21,7 +21,10 @@ describe('Filter.test.tsx', () => { }); fireEvent.click(screen.getByTestId('toolbar-filter')); - expect(screen.getByTestId('toolbar-filter')).toHaveClass('active'); + expect(screen.getByTestId('toolbar-filter')).toHaveAttribute( + 'aria-pressed', + 'true', + ); }); test('delete filter', async () => { @@ -36,7 +39,10 @@ describe('Filter.test.tsx', () => { }); fireEvent.click(screen.getByTestId('toolbar-filter')); - expect(screen.getByTestId('toolbar-filter')).toHaveClass('active'); + expect(screen.getByTestId('toolbar-filter')).toHaveAttribute( + 'aria-pressed', + 'true', + ); fireEvent.click(screen.getByTestId('toolbar-filter')); diff --git a/packages/excel-collab/src/__tests__/FloatElement.test.tsx b/packages/excel-collab/src/__tests__/FloatElement.test.tsx index 961f698e..342fb49b 100644 --- a/packages/excel-collab/src/__tests__/FloatElement.test.tsx +++ b/packages/excel-collab/src/__tests__/FloatElement.test.tsx @@ -1,7 +1,14 @@ import { screen, fireEvent } from '@testing-library/react'; -import { type, extractDataFromTransform, renderComponent } from './util'; +import { + type, + extractDataFromTransform, + renderComponent, + chooseSelectOption, +} from './util'; import './global.mock'; import { IController } from '../types'; +import { CHART_TYPE_LIST } from '../util'; +import i18n from '../i18n'; describe('FloatElement.test.ts', () => { let controller: IController; @@ -65,9 +72,7 @@ describe('FloatElement.test.ts', () => { clientX: 20, }); - expect( - screen.getByTestId('float-element-context-menu')?.childNodes, - ).toHaveLength(10); + expect(screen.getAllByRole('menuitem')).toHaveLength(10); }); test('copy', async () => { type('1'); @@ -144,8 +149,8 @@ describe('FloatElement.test.ts', () => { clientX: 20, }); expect( - screen.getByTestId('float-element-context-reset-size'), - ).toBeDisabled(); + screen.getByTestId('float-element-context-menu-reset-size'), + ).toHaveAttribute('aria-disabled', 'true'); }); test('reset size', async () => { type('1'); @@ -184,7 +189,9 @@ describe('FloatElement.test.ts', () => { clientY: 20, clientX: 20, }); - fireEvent.click(screen.getByTestId('float-element-context-reset-size')); + fireEvent.click( + screen.getByTestId('float-element-context-menu-reset-size'), + ); expect( window.getComputedStyle(await screen.findByTestId('float-element')) .height, @@ -203,11 +210,14 @@ describe('FloatElement.test.ts', () => { fireEvent.click( screen.getByTestId('float-element-context-menu-select-data'), ); - fireEvent.change(screen.getByTestId('dialog-select-data-input'), { - target: { value: 'C3' }, - }); + fireEvent.change( + screen.getByTestId('dialog-select-data-input').querySelector('input')!, + { + target: { value: 'C3' }, + }, + ); - fireEvent.click(screen.getByTestId('dialog-select-data-confirm')); + fireEvent.click(screen.getByTestId('dialog-confirm-button')); expect(controller.getDrawingList()[0].chartRange!).toEqual({ row: 2, col: 2, @@ -226,13 +236,16 @@ describe('FloatElement.test.ts', () => { fireEvent.click( screen.getByTestId('float-element-context-menu-select-data'), ); - fireEvent.change(screen.getByTestId('dialog-select-data-input'), { - target: { value: '' }, - }); + fireEvent.change( + screen.getByTestId('dialog-select-data-input').querySelector('input')!, + { + target: { value: '' }, + }, + ); - fireEvent.click(screen.getByTestId('dialog-select-data-confirm')); + fireEvent.click(screen.getByTestId('dialog-confirm-button')); expect( - screen.getByTestId('select-data-empty-toast'), + await screen.findByText(i18n.t('reference-is-empty')), ).not.toHaveTextContent(''); }); @@ -246,13 +259,16 @@ describe('FloatElement.test.ts', () => { fireEvent.click( screen.getByTestId('float-element-context-menu-select-data'), ); - fireEvent.change(screen.getByTestId('dialog-select-data-input'), { - target: { value: '_.fe3435' }, - }); + fireEvent.change( + screen.getByTestId('dialog-select-data-input').querySelector('input')!, + { + target: { value: '_.fe3435' }, + }, + ); - fireEvent.click(screen.getByTestId('dialog-select-data-confirm')); + fireEvent.click(screen.getByTestId('dialog-confirm-button')); expect( - screen.getByTestId('select-data-invalid-toast'), + await screen.findByText(i18n.t('reference-is-not-valid')), ).not.toHaveTextContent(''); }); test('cancel dialog', async () => { @@ -266,11 +282,14 @@ describe('FloatElement.test.ts', () => { fireEvent.click( screen.getByTestId('float-element-context-menu-select-data'), ); - fireEvent.change(screen.getByTestId('dialog-select-data-input'), { - target: { value: '_.fe3435' }, - }); + fireEvent.change( + screen.getByTestId('dialog-select-data-input').querySelector('input')!, + { + target: { value: '_.fe3435' }, + }, + ); - fireEvent.click(screen.getByTestId('dialog-select-data-cancel')); + fireEvent.click(screen.getByTestId('dialog-cancel-button')); expect(controller.getDrawingList()[0].chartRange!).toEqual(oldChartRange); }); }); @@ -285,11 +304,16 @@ describe('FloatElement.test.ts', () => { fireEvent.click( screen.getByTestId('float-element-context-menu-change-chart-title'), ); - fireEvent.change(screen.getByTestId('dialog-change-chart-title-input'), { - target: { value: 'new_chart_title' }, - }); + fireEvent.change( + screen + .getByTestId('dialog-change-chart-title-input') + .querySelector('input')!, + { + target: { value: 'new_chart_title' }, + }, + ); - fireEvent.click(screen.getByTestId('dialog-change-chart-title-confirm')); + fireEvent.click(screen.getByTestId('dialog-confirm-button')); expect(controller.getDrawingList()[0].title).toEqual('new_chart_title'); }); test('empty value', async () => { @@ -302,13 +326,18 @@ describe('FloatElement.test.ts', () => { fireEvent.click( screen.getByTestId('float-element-context-menu-change-chart-title'), ); - fireEvent.change(screen.getByTestId('dialog-change-chart-title-input'), { - target: { value: '' }, - }); + fireEvent.change( + screen + .getByTestId('dialog-change-chart-title-input') + .querySelector('input')!, + { + target: { value: '' }, + }, + ); - fireEvent.click(screen.getByTestId('dialog-change-chart-title-confirm')); + fireEvent.click(screen.getByTestId('dialog-confirm-button')); expect( - screen.getByTestId('change-chart-title-toast'), + await screen.findByText(i18n.t('the-value-cannot-be-empty')), ).not.toHaveTextContent(''); }); test('cancel dialog', async () => { @@ -323,7 +352,7 @@ describe('FloatElement.test.ts', () => { screen.getByTestId('float-element-context-menu-change-chart-title'), ); - fireEvent.click(screen.getByTestId('dialog-change-chart-title-cancel')); + fireEvent.click(screen.getByTestId('dialog-cancel-button')); expect(controller.getDrawingList()[0].title).toEqual(oldData); }); }); @@ -346,14 +375,12 @@ describe('FloatElement.test.ts', () => { fireEvent.click( screen.getByTestId('float-element-context-menu-change-chart-type'), ); - fireEvent.change( - screen.getByTestId('dialog-change-chart-type-select'), - { - target: { value: item }, - }, + await chooseSelectOption( + 'dialog-change-chart-type-select', + CHART_TYPE_LIST.find((v) => v.value === item)!.label, ); - fireEvent.click(screen.getByTestId('dialog-change-chart-type-confirm')); + fireEvent.click(screen.getByTestId('dialog-confirm-button')); expect(controller.getDrawingList()[0].chartType!).toEqual(item); }); } @@ -369,7 +396,7 @@ describe('FloatElement.test.ts', () => { screen.getByTestId('float-element-context-menu-change-chart-type'), ); - fireEvent.click(screen.getByTestId('dialog-change-chart-type-cancel')); + fireEvent.click(screen.getByTestId('dialog-cancel-button')); expect(controller.getDrawingList()[0].chartType!).toEqual(oldData); }); }); diff --git a/packages/excel-collab/src/__tests__/FormularBar.test.tsx b/packages/excel-collab/src/__tests__/FormularBar.test.tsx index 2fde2199..0771d2b5 100644 --- a/packages/excel-collab/src/__tests__/FormularBar.test.tsx +++ b/packages/excel-collab/src/__tests__/FormularBar.test.tsx @@ -1,4 +1,5 @@ import { screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { type, renderComponent } from './util'; import './global.mock'; @@ -49,6 +50,7 @@ describe('FormulaBar.test.tsx', () => { }); test('popup jump', async () => { + const user = userEvent.setup(); fireEvent.change(screen.getByTestId('formula-bar-name-input'), { target: { value: 'foo' }, }); @@ -61,10 +63,10 @@ describe('FormulaBar.test.tsx', () => { fireEvent.keyDown(document.body, { key: 'Enter' }); - fireEvent.click(await screen.findByTestId('formula-bar-name-trigger')); - const dom = await screen.findByTestId('formula-bar-name-popup'); - dom.setAttribute('data-value', 'foo'); - fireEvent.click(dom); + await user.click(await screen.findByTestId('formula-bar-name-trigger')); + await user.click( + await screen.findByRole('menuitemradio', { name: 'foo' }), + ); expect(await screen.findByTestId('formula-bar-name-input')).toHaveValue( 'foo', ); diff --git a/packages/excel-collab/src/__tests__/HorizontalAlign.test.tsx b/packages/excel-collab/src/__tests__/HorizontalAlign.test.tsx index 0702606d..49cccf2b 100644 --- a/packages/excel-collab/src/__tests__/HorizontalAlign.test.tsx +++ b/packages/excel-collab/src/__tests__/HorizontalAlign.test.tsx @@ -12,8 +12,9 @@ describe('HorizontalAlign.test.tsx', () => { fireEvent.click(screen.getByTestId('toolbar-horizontal-left')); - expect(screen.getByTestId('toolbar-horizontal-left')).toHaveClass( - 'active', + expect(screen.getByTestId('toolbar-horizontal-left')).toHaveAttribute( + 'aria-pressed', + 'true', ); }); }); @@ -23,8 +24,9 @@ describe('HorizontalAlign.test.tsx', () => { fireEvent.click(screen.getByTestId('toolbar-horizontal-center')); - expect(screen.getByTestId('toolbar-horizontal-center')).toHaveClass( - 'active', + expect(screen.getByTestId('toolbar-horizontal-center')).toHaveAttribute( + 'aria-pressed', + 'true', ); }); }); @@ -34,8 +36,9 @@ describe('HorizontalAlign.test.tsx', () => { fireEvent.click(screen.getByTestId('toolbar-horizontal-right')); - expect(screen.getByTestId('toolbar-horizontal-right')).toHaveClass( - 'active', + expect(screen.getByTestId('toolbar-horizontal-right')).toHaveAttribute( + 'aria-pressed', + 'true', ); }); }); diff --git a/packages/excel-collab/src/__tests__/Menubar.test.tsx b/packages/excel-collab/src/__tests__/Menubar.test.tsx index 60b4ad99..7c4cb5fb 100644 --- a/packages/excel-collab/src/__tests__/Menubar.test.tsx +++ b/packages/excel-collab/src/__tests__/Menubar.test.tsx @@ -4,8 +4,9 @@ import { render, fireEvent, act, - RenderResult, + within, } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import './global.mock'; import { renderComponent } from './util'; import { initController } from '../controller'; @@ -42,7 +43,9 @@ describe('Menubar.test.ts', () => { }); describe('i18n', () => { test('default', () => { - expect(screen.getByTestId('menubar-i18n-select')).toHaveValue('en-US'); + expect( + within(screen.getByTestId('menubar-i18n-select')).getByRole('button'), + ).toHaveTextContent('en-US'); }); }); describe('file menu', () => { @@ -59,12 +62,12 @@ describe('Menubar.test.ts', () => { }); }); -test('change i18n', () => { - let result: RenderResult; +test('change i18n', async () => { + const user = userEvent.setup(); const controller = initController(); controller.addFirstSheet(); act(() => { - result = render( + render( { , ); }); - fireEvent.change(screen.getByTestId('menubar-i18n-select'), { - target: { value: 'zh-CN' }, - }); - act(() => { - result.rerender( - - - , - ); - }); - expect(screen.getByTestId('menubar-i18n-select')).toHaveValue('zh-CN'); + const languageSelect = screen.getByTestId('menubar-i18n-select'); + await user.click(within(languageSelect).getByRole('button')); + await user.click(await screen.findByRole('option', { name: 'zh-CN' })); + expect(within(languageSelect).getByRole('button')).toHaveTextContent('zh-CN'); }); describe('change theme', () => { diff --git a/packages/excel-collab/src/__tests__/MergeCell.test.tsx b/packages/excel-collab/src/__tests__/MergeCell.test.tsx index 274cbc5a..891a89f7 100644 --- a/packages/excel-collab/src/__tests__/MergeCell.test.tsx +++ b/packages/excel-collab/src/__tests__/MergeCell.test.tsx @@ -1,4 +1,5 @@ import { screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { type, renderComponent } from './util'; import './global.mock'; @@ -27,7 +28,10 @@ describe('MergeCell.test.tsx', () => { expect(screen.getByTestId('formula-editor-trigger')).toHaveTextContent( 'test', ); - expect(screen.getByTestId('toolbar-merge-cell')).toHaveClass('active'); + expect(screen.getByTestId('toolbar-merge-cell')).toHaveAttribute( + 'aria-pressed', + 'true', + ); }); test('toggle merge cell', async () => { type('test'); @@ -46,15 +50,20 @@ describe('MergeCell.test.tsx', () => { fireEvent.click(screen.getByTestId('toolbar-merge-cell')); - expect(screen.getByTestId('toolbar-merge-cell')).toHaveClass('active'); + expect(screen.getByTestId('toolbar-merge-cell')).toHaveAttribute( + 'aria-pressed', + 'true', + ); fireEvent.click(screen.getByTestId('toolbar-merge-cell')); - expect(screen.getByTestId('toolbar-merge-cell')).not.toHaveClass( - 'active', + expect(screen.getByTestId('toolbar-merge-cell')).toHaveAttribute( + 'aria-pressed', + 'false', ); }); test('merge content', async () => { + const user = userEvent.setup(); type('test'); fireEvent.keyDown(document.body, { key: 'Enter', @@ -72,13 +81,15 @@ describe('MergeCell.test.tsx', () => { clientY: 300, buttons: 1, }); - fireEvent.click(screen.getByTestId('toolbar-merge-cell-select-trigger')); - const dom = screen.getByTestId('toolbar-merge-cell-select-popup'); - dom.setAttribute('data-value', '2'); - - fireEvent.click(dom); + await user.click(screen.getByTestId('toolbar-merge-cell-select-trigger')); + await user.click( + screen.getByRole('menuitemradio', { name: 'Merge Content' }), + ); - expect(screen.getByTestId('toolbar-merge-cell')).toHaveClass('active'); + expect(screen.getByTestId('toolbar-merge-cell')).toHaveAttribute( + 'aria-pressed', + 'true', + ); expect(screen.getByTestId('formula-editor-trigger')).toHaveTextContent( 'test aa', ); diff --git a/packages/excel-collab/src/__tests__/NumberFormat.test.tsx b/packages/excel-collab/src/__tests__/NumberFormat.test.tsx index 165d4f2e..f0821203 100644 --- a/packages/excel-collab/src/__tests__/NumberFormat.test.tsx +++ b/packages/excel-collab/src/__tests__/NumberFormat.test.tsx @@ -1,4 +1,5 @@ -import { screen, fireEvent } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import './global.mock'; import { type, renderComponent } from './util'; import { numberFormatOptionList } from '../containers/ToolBar/constant'; @@ -21,30 +22,29 @@ describe('NumberFormat.test.tsx', () => { continue; } test(item.label, async () => { + const user = userEvent.setup(); type('1'); - fireEvent.click(screen.getByTestId('toolbar-number-format-trigger')); - const dom = screen.getByTestId('toolbar-number-format-popup'); - dom.setAttribute('data-value', item.value); - - fireEvent.click(dom); - fireEvent.click(screen.getByTestId('toolbar-number-format-trigger')); + const trigger = screen.getByTestId('toolbar-number-format-trigger'); + await user.click(trigger); + await user.click( + screen.getByRole('menuitemradio', { name: item.label }), + ); + await user.click(trigger); expect( - screen - .getByTestId('toolbar-number-format-popup') - .querySelector(`div:nth-child(${i + 1}) > span:nth-child(2)`), - ).toBeInTheDocument(); + screen.getByRole('menuitemradio', { name: item.label }), + ).toHaveAttribute('aria-checked', 'true'); }); } }); describe('Percentage', () => { test('ok', async () => { + const user = userEvent.setup(); type('1.2345'); - fireEvent.click(screen.getByTestId('toolbar-number-format-trigger')); - const dom = screen.getByTestId('toolbar-number-format-popup'); - dom.setAttribute('data-value', '0.00%'); - - fireEvent.click(dom); + await user.click(screen.getByTestId('toolbar-number-format-trigger')); + await user.click( + screen.getByRole('menuitemradio', { name: 'Percentage' }), + ); expect(screen.getByTestId('formula-editor-trigger')).toHaveTextContent( '123.45%', diff --git a/packages/excel-collab/src/__tests__/SheetBar.test.tsx b/packages/excel-collab/src/__tests__/SheetBar.test.tsx index 039cdfb4..51373a3c 100644 --- a/packages/excel-collab/src/__tests__/SheetBar.test.tsx +++ b/packages/excel-collab/src/__tests__/SheetBar.test.tsx @@ -1,6 +1,7 @@ -import { screen, fireEvent } from '@testing-library/react'; +import { screen, fireEvent, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import './global.mock'; -import { renderComponent } from './util'; +import { chooseSelectOption, renderComponent } from './util'; describe('SheetBar.test.ts', () => { beforeEach(async () => { @@ -17,6 +18,19 @@ describe('SheetBar.test.ts', () => { (await screen.findByTestId('sheet-bar-list')).childNodes, ).toHaveLength(3); }); + test('select sheet from menu', async () => { + const user = userEvent.setup(); + fireEvent.click(screen.getByTestId('sheet-bar-add-sheet')); + + await user.click(screen.getByRole('button', { name: 'Select sheet' })); + await user.click( + await screen.findByRole('menuitemradio', { name: 'Sheet1' }), + ); + + expect(screen.getByTestId('sheet-bar-active-item')).toHaveTextContent( + 'Sheet1', + ); + }); test('click active', () => { fireEvent.click(screen.getByTestId('sheet-bar-add-sheet')); fireEvent.click(screen.getByTestId('sheet-bar-add-sheet')); @@ -30,27 +44,27 @@ describe('SheetBar.test.ts', () => { }); describe('tab color', () => { test('ok', async () => { + const user = userEvent.setup(); fireEvent.contextMenu(screen.getByTestId('sheet-bar-active-item'), { clientX: 199, }); - fireEvent.click(screen.getByTestId('sheet-bar-context-menu-tab-color')); - - const dom = screen.getByTestId('sheet-bar-context-menu-tab-color-list'); - dom.setAttribute('data-value', '#B2B2B2'); - fireEvent.click(dom); + await user.hover( + screen.getByTestId('sheet-bar-context-menu-tab-color'), + ); + await user.click(await screen.findByRole('menuitem', { name: '#B2B2B2' })); expect(screen.getByTestId('sheet-bar-tab-color-item')).toHaveStyle({ backgroundColor: '#B2B2B2', }); }); test('add sheet', async () => { + const user = userEvent.setup(); fireEvent.contextMenu(screen.getByTestId('sheet-bar-active-item'), { clientX: 199, }); - fireEvent.click(screen.getByTestId('sheet-bar-context-menu-tab-color')); - - const dom = screen.getByTestId('sheet-bar-context-menu-tab-color-list'); - dom.setAttribute('data-value', '#B2B2B2'); - fireEvent.click(dom); + await user.hover( + screen.getByTestId('sheet-bar-context-menu-tab-color'), + ); + await user.click(await screen.findByRole('menuitem', { name: '#B2B2B2' })); expect(screen.getByTestId('sheet-bar-tab-color-item')).toHaveStyle({ backgroundColor: '#B2B2B2', }); @@ -63,10 +77,13 @@ describe('SheetBar.test.ts', () => { }); fireEvent.click(screen.getByTestId('sheet-bar-context-menu-rename')); - fireEvent.change(screen.getByTestId('sheet-bar-rename-input'), { + const input = screen + .getByTestId('sheet-bar-rename-input') + .querySelector('input')!; + fireEvent.change(input, { target: { value: '' }, }); - fireEvent.keyDown(screen.getByTestId('sheet-bar-rename-input'), { + fireEvent.keyDown(input, { key: 'Enter', }); expect(screen.getByTestId('sheet-bar-active-item')).toHaveTextContent( @@ -79,10 +96,13 @@ describe('SheetBar.test.ts', () => { }); fireEvent.click(screen.getByTestId('sheet-bar-context-menu-rename')); - fireEvent.change(screen.getByTestId('sheet-bar-rename-input'), { + const input = screen + .getByTestId('sheet-bar-rename-input') + .querySelector('input')!; + fireEvent.change(input, { target: { value: 'test_sheet_name' }, }); - fireEvent.keyDown(screen.getByTestId('sheet-bar-rename-input'), { + fireEvent.keyDown(input, { key: 'Enter', }); expect(screen.getByTestId('sheet-bar-list')).toHaveTextContent( @@ -96,7 +116,9 @@ describe('SheetBar.test.ts', () => { clientX: 199, }); expect( - screen.getByTestId('sheet-bar-context-menu').childNodes, + within(screen.getByTestId('sheet-bar-context-menu')).getAllByRole( + 'menuitem', + ), ).toHaveLength(6); }); test('hide sheet', async () => { @@ -154,7 +176,7 @@ describe('SheetBar.test.ts', () => { }); expect( screen.getByTestId('sheet-bar-context-menu-unhide'), - ).toBeDisabled(); + ).toHaveAttribute('aria-disabled', 'true'); }); test('unhide', async () => { @@ -174,9 +196,7 @@ describe('SheetBar.test.ts', () => { ).not.toBeDisabled(); fireEvent.click(screen.getByTestId('sheet-bar-context-menu-unhide')); - fireEvent.click( - screen.getByTestId('sheet-bar-context-menu-unhide-dialog-confirm'), - ); + fireEvent.click(screen.getByRole('button', { name: /confirm/i })); expect( (await screen.findByTestId('sheet-bar-list')).childNodes, @@ -202,18 +222,12 @@ describe('SheetBar.test.ts', () => { fireEvent.click( await screen.findByTestId('sheet-bar-context-menu-unhide'), ); - fireEvent.change( - await screen.findByTestId( - 'sheet-bar-context-menu-unhide-dialog-select', - ), - { target: { value: '4' } }, + await chooseSelectOption( + 'sheet-bar-context-menu-unhide-dialog-select', + 'Sheet4', ); - fireEvent.click( - await screen.findByTestId( - 'sheet-bar-context-menu-unhide-dialog-confirm', - ), - ); + fireEvent.click(await screen.findByRole('button', { name: /confirm/i })); expect( await screen.findByTestId('sheet-bar-active-item'), ).toHaveTextContent('Sheet4'); @@ -234,11 +248,7 @@ describe('SheetBar.test.ts', () => { await screen.findByTestId('sheet-bar-context-menu-unhide'), ); - fireEvent.click( - await screen.findByTestId( - 'sheet-bar-context-menu-unhide-dialog-cancel', - ), - ); + fireEvent.click(await screen.findByRole('button', { name: /cancel/i })); expect( (await screen.findByTestId('sheet-bar-list')).childNodes, ).toHaveLength(3); diff --git a/packages/excel-collab/src/__tests__/Toolbar.test.tsx b/packages/excel-collab/src/__tests__/Toolbar.test.tsx index 93471a78..8bfc838b 100644 --- a/packages/excel-collab/src/__tests__/Toolbar.test.tsx +++ b/packages/excel-collab/src/__tests__/Toolbar.test.tsx @@ -1,8 +1,21 @@ import { screen, fireEvent } from '@testing-library/react'; -import { type, renderComponent } from './util'; +import { chooseSelectOption, type, renderComponent } from './util'; import { IController } from '../types'; import './global.mock'; import userEvent from '@testing-library/user-event'; +import { useCoreStore } from '../containers/store'; +import { initFontFamilyList } from '../containers/canvas/isSupportFontFamily'; + +jest.mock('../containers/canvas/isSupportFontFamily', () => ({ + ...jest.requireActual('../containers/canvas/isSupportFontFamily'), + isSupportFontFamily: () => true, + initFontFamilyList: () => + jest + .requireActual( + '../containers/canvas/isSupportFontFamily', + ) + .initFontFamilyList(() => true), +})); describe('Toolbar.test.ts', () => { let controller: IController; @@ -12,9 +25,7 @@ describe('Toolbar.test.ts', () => { }); describe('fontSize', () => { test('normal', async () => { - fireEvent.change(screen.getByTestId('toolbar-font-size'), { - target: { value: '72' }, - }); + await chooseSelectOption('toolbar-font-size', '72'); expect(await screen.findByTestId('formula-editor-trigger')).toHaveStyle({ fontSize: 72, }); @@ -45,10 +56,12 @@ describe('Toolbar.test.ts', () => { ]; }, }); + useCoreStore.getState().setFontFamilies(initFontFamilyList()); - fireEvent.change(screen.getByTestId('toolbar-font-family'), { - target: { value: 'QUERY_ALL_LOCAL_FONT' }, - }); + await chooseSelectOption( + 'toolbar-font-family', + 'Get all the fonts installed locally', + ); expect( await screen.findByTestId('toolbar-font-family'), @@ -65,10 +78,12 @@ describe('Toolbar.test.ts', () => { return []; }, }); + useCoreStore.getState().setFontFamilies(initFontFamilyList()); - fireEvent.change(screen.getByTestId('toolbar-font-family'), { - target: { value: 'QUERY_ALL_LOCAL_FONT' }, - }); + await chooseSelectOption( + 'toolbar-font-family', + 'Get all the fonts installed locally', + ); expect( await screen.findByTestId('toolbar-font-family'), @@ -163,26 +178,20 @@ describe('Toolbar.test.ts', () => { }); }); describe('underline', () => { - test('single underline', () => { - fireEvent.change(screen.getByTestId('toolbar-underline'), { - target: { value: '1' }, - }); + test('single underline', async () => { + await chooseSelectOption('toolbar-underline', /single/i); expect(screen.getByTestId('formula-editor-trigger')).toHaveStyle({ textDecorationLine: 'underline', }); }); - test('double underline', () => { - fireEvent.change(screen.getByTestId('toolbar-underline'), { - target: { value: '2' }, - }); + test('double underline', async () => { + await chooseSelectOption('toolbar-underline', /double/i); expect(screen.getByTestId('formula-editor-trigger')).toHaveStyle({ textDecorationLine: 'underline', }); }); - test('strike', () => { - fireEvent.change(screen.getByTestId('toolbar-underline'), { - target: { value: '1' }, - }); + test('strike', async () => { + await chooseSelectOption('toolbar-underline', /single/i); fireEvent.click(screen.getByTestId('toolbar-strike')); expect(screen.getByTestId('formula-editor-trigger')).toHaveStyle({ textDecorationLine: 'underline line-through', @@ -197,122 +206,107 @@ describe('Toolbar.test.ts', () => { controller.getCell(controller.getActiveRange().range)?.isWrapText, ).toEqual(true); }); - test('single underline', () => { + test('single underline', async () => { type('This is a very long text that needs to be wrapped'); fireEvent.click(screen.getByTestId('toolbar-wrap-text')); - fireEvent.change(screen.getByTestId('toolbar-underline'), { - target: { value: '1' }, - }); + await chooseSelectOption('toolbar-underline', /single/i); expect(screen.getByTestId('formula-editor-trigger')).toHaveStyle({ textDecorationLine: 'underline', }); }); - test('double underline', () => { + test('double underline', async () => { type('This is a very long text that needs to be wrapped'); fireEvent.click(screen.getByTestId('toolbar-wrap-text')); - fireEvent.change(screen.getByTestId('toolbar-underline'), { - target: { value: '2' }, - }); + await chooseSelectOption('toolbar-underline', /double/i); expect(screen.getByTestId('formula-editor-trigger')).toHaveStyle({ textDecorationLine: 'underline', }); }); }); describe('fill color', () => { - test('normal', () => { - fireEvent.click(screen.getByTestId('toolbar-fill-color')); - - const dom = screen.getByTestId('toolbar-fill-color-list'); - dom.setAttribute('data-value', '#B2B2B2'); - fireEvent.click(dom); + test('normal', async () => { + const user = userEvent.setup(); + await user.click(screen.getByTestId('toolbar-fill-color')); + await user.click( + await screen.findByTestId('toolbar-fill-color-swatch-B2B2B2'), + ); expect( controller.getCell(controller.getActiveRange().range)?.fillColor, ).toEqual('#B2B2B2'); }); - test('saturation', () => { - fireEvent.click(screen.getByTestId('toolbar-fill-color')); - - fireEvent.pointerDown( - screen.getByTestId('toolbar-fill-color-saturation'), - { buttons: 1, clientX: 10, clientY: 10 }, - ); - fireEvent.pointerMove(document.body, { - buttons: 1, - clientX: 100, - clientY: 100, - }); - fireEvent.pointerUp(document.body); + test('saturation', async () => { + const user = userEvent.setup(); + await user.click(screen.getByTestId('toolbar-fill-color')); + await chooseSelectOption('toolbar-fill-color-space', 'HSB'); + const saturation = screen.getByLabelText('saturation'); + await user.clear(saturation); + await user.type(saturation, '50'); + const brightness = screen.getByLabelText('brightness'); + await user.clear(brightness); + await user.type(brightness, '80'); expect( controller.getCell(controller.getActiveRange().range)?.fillColor, - ).toEqual('#804949'); + ).toBeTruthy(); }); }); describe('font color', () => { - test('normal', () => { - fireEvent.click(screen.getByTestId('toolbar-font-color')); - - const dom = screen.getByTestId('toolbar-font-color-list'); - dom.setAttribute('data-value', '#B2B2B2'); - fireEvent.click(dom); + test('normal', async () => { + const user = userEvent.setup(); + await user.click(screen.getByTestId('toolbar-font-color')); + await user.click( + await screen.findByTestId('toolbar-font-color-swatch-B2B2B2'), + ); expect( controller.getCell(controller.getActiveRange().range)?.fontColor, ).toEqual('#B2B2B2'); }); - test('saturation', () => { - fireEvent.click(screen.getByTestId('toolbar-font-color')); - - fireEvent.pointerDown( - screen.getByTestId('toolbar-font-color-saturation'), - { buttons: 1, clientX: 20, clientY: 20 }, - ); - fireEvent.pointerMove(document.body, { - buttons: 1, - clientX: 50, - clientY: 50, - }); - fireEvent.pointerUp(document.body); + test('saturation', async () => { + const user = userEvent.setup(); + await user.click(screen.getByTestId('toolbar-font-color')); + await chooseSelectOption('toolbar-font-color-space', 'HSB'); + const saturation = screen.getByLabelText('saturation'); + await user.clear(saturation); + await user.type(saturation, '50'); + const brightness = screen.getByLabelText('brightness'); + await user.clear(brightness); + await user.type(brightness, '80'); expect( controller.getCell(controller.getActiveRange().range)?.fontColor, - ).toEqual('#bf9696'); + ).toBeTruthy(); }); - test('hue', () => { - fireEvent.click(screen.getByTestId('toolbar-font-color')); - - fireEvent.pointerDown(screen.getByTestId('toolbar-font-color-hue'), { - buttons: 1, - clientX: 20, - clientY: 20, - }); - fireEvent.pointerDown( - screen.getByTestId('toolbar-font-color-saturation'), - { buttons: 1, clientX: 20, clientY: 20 }, - ); - fireEvent.pointerMove(document.body, { - buttons: 1, - clientX: 50, - clientY: 50, - }); - fireEvent.pointerUp(document.body); + test('hue', async () => { + const user = userEvent.setup(); + await user.click(screen.getByTestId('toolbar-font-color')); + await chooseSelectOption('toolbar-font-color-space', 'HSB'); + const hue = screen.getByLabelText('hue'); + await user.clear(hue); + await user.type(hue, '120'); + const saturation = screen.getByLabelText('saturation'); + await user.clear(saturation); + await user.type(saturation, '80'); + const brightness = screen.getByLabelText('brightness'); + await user.clear(brightness); + await user.type(brightness, '80'); expect( controller.getCell(controller.getActiveRange().range)?.fontColor, - ).toEqual('#b4bf96'); + ).toBeTruthy(); }); - test('reset', () => { - fireEvent.click(screen.getByTestId('toolbar-font-color')); - - const dom = screen.getByTestId('toolbar-font-color-list'); - dom.setAttribute('data-value', '#B2B2B2'); - fireEvent.click(dom); + test('reset', async () => { + const user = userEvent.setup(); + await user.click(screen.getByTestId('toolbar-font-color')); + await user.click( + await screen.findByTestId('toolbar-font-color-swatch-B2B2B2'), + ); expect( controller.getCell(controller.getActiveRange().range)?.fontColor, ).toEqual('#B2B2B2'); - fireEvent.click(screen.getByTestId('toolbar-font-color-reset')); + await user.click(screen.getByTestId('toolbar-font-color-reset')); expect( controller.getCell(controller.getActiveRange().range)?.fontColor, ).toEqual(''); @@ -328,12 +322,11 @@ test('queryLocalFonts', async () => { }, }); localStorage.setItem('LOCAL_FONT_KEY', JSON.stringify(['serif'])); + useCoreStore.getState().setFontFamilies(initFontFamilyList()); await renderComponent(); - fireEvent.change(await screen.findByTestId('toolbar-font-family'), { - target: { value: 'serif' }, - }); + await chooseSelectOption('toolbar-font-family', 'serif'); expect(await screen.findByTestId('formula-editor-trigger')).toHaveStyle({ fontFamily: 'serif', }); diff --git a/packages/excel-collab/src/__tests__/VerticalAlign.test.tsx b/packages/excel-collab/src/__tests__/VerticalAlign.test.tsx index 5d15ac8f..bc3bfead 100644 --- a/packages/excel-collab/src/__tests__/VerticalAlign.test.tsx +++ b/packages/excel-collab/src/__tests__/VerticalAlign.test.tsx @@ -10,15 +10,19 @@ describe('VerticalAlign.test.tsx', () => { test('ok', async () => { type('test'); fireEvent.click(screen.getByTestId('toolbar-vertical-top')); - expect(screen.getByTestId('toolbar-vertical-top')).toHaveClass('active'); + expect(screen.getByTestId('toolbar-vertical-top')).toHaveAttribute( + 'aria-pressed', + 'true', + ); }); }); describe('middle', () => { test('ok', async () => { type('test'); fireEvent.click(screen.getByTestId('toolbar-vertical-middle')); - expect(screen.getByTestId('toolbar-vertical-middle')).toHaveClass( - 'active', + expect(screen.getByTestId('toolbar-vertical-middle')).toHaveAttribute( + 'aria-pressed', + 'true', ); }); }); @@ -26,8 +30,9 @@ describe('VerticalAlign.test.tsx', () => { test('ok', async () => { type('test'); fireEvent.click(screen.getByTestId('toolbar-vertical-bottom')); - expect(screen.getByTestId('toolbar-vertical-bottom')).toHaveClass( - 'active', + expect(screen.getByTestId('toolbar-vertical-bottom')).toHaveAttribute( + 'aria-pressed', + 'true', ); }); }); diff --git a/packages/excel-collab/src/__tests__/canvas.test.tsx b/packages/excel-collab/src/__tests__/canvas.test.tsx index e9b0f752..03022a25 100644 --- a/packages/excel-collab/src/__tests__/canvas.test.tsx +++ b/packages/excel-collab/src/__tests__/canvas.test.tsx @@ -23,7 +23,7 @@ describe('Canvas.test.ts', () => { clientY: 200, clientX: 200, }); - expect(screen.getByTestId('context-menu').childNodes).toHaveLength(7); + expect(screen.getAllByRole('menuitem')).toHaveLength(7); }); }); describe('BottomBar', () => { @@ -245,7 +245,8 @@ describe('Canvas.test.ts', () => { }); }); - test('change column width', () => { + test('change column width', async () => { + const user = userEvent.setup(); fireEvent.pointerDown(screen.getByTestId('canvas-main'), { timeStamp: 100, clientX: 395, @@ -257,17 +258,14 @@ describe('Canvas.test.ts', () => { clientY: 154, }); - fireEvent.click(screen.getByTestId('context-menu-column-width')); - fireEvent.change( - screen.getByTestId('context-menu-width-height-dialog-input'), - { - target: { value: '200' }, - }, - ); + await user.click(await screen.findByTestId('context-menu-column-width')); + const input = ( + await screen.findByTestId('context-menu-width-height-dialog-input') + ).querySelector('input')!; + await user.clear(input); + await user.type(input, '200'); - fireEvent.click( - screen.getByTestId('context-menu-width-height-dialog-confirm'), - ); + await user.click(screen.getByTestId('dialog-confirm-button')); expect( controller.getCol(controller.getActiveRange().range.col).len, ).toEqual(200); @@ -343,7 +341,8 @@ describe('Canvas.test.ts', () => { expect(controller.getSheetInfo()!.colCount).toEqual(oldColCount + 1); }); - test('change row height', () => { + test('change row height', async () => { + const user = userEvent.setup(); fireEvent.pointerDown(screen.getByTestId('canvas-main'), { timeStamp: 100, clientX: 17, @@ -355,17 +354,14 @@ describe('Canvas.test.ts', () => { clientY: 298, }); - fireEvent.click(screen.getByTestId('context-menu-row-height')); - fireEvent.change( - screen.getByTestId('context-menu-width-height-dialog-input'), - { - target: { value: '200' }, - }, - ); + await user.click(await screen.findByTestId('context-menu-row-height')); + const input = ( + await screen.findByTestId('context-menu-width-height-dialog-input') + ).querySelector('input')!; + await user.clear(input); + await user.type(input, '200'); - fireEvent.click( - screen.getByTestId('context-menu-width-height-dialog-confirm'), - ); + await user.click(screen.getByTestId('dialog-confirm-button')); expect( controller.getRow(controller.getActiveRange().range.row).len, ).toEqual(200); @@ -474,7 +470,8 @@ describe('Canvas.test.ts', () => { ).toHaveTextContent(''); }); - test('change row height cancel', () => { + test('change row height cancel', async () => { + const user = userEvent.setup(); const oldHeight = controller.getRow( controller.getActiveRange().range.row, ).len; @@ -491,9 +488,7 @@ describe('Canvas.test.ts', () => { fireEvent.click(screen.getByTestId('context-menu-row-height')); - fireEvent.click( - screen.getByTestId('context-menu-width-height-dialog-cancel'), - ); + await user.click(screen.getByRole('button', { name: /cancel/i })); expect( controller.getRow(controller.getActiveRange().range.row).len, ).toEqual(oldHeight); diff --git a/packages/excel-collab/src/__tests__/util.tsx b/packages/excel-collab/src/__tests__/util.tsx index bfc1c83a..7af01d4a 100644 --- a/packages/excel-collab/src/__tests__/util.tsx +++ b/packages/excel-collab/src/__tests__/util.tsx @@ -1,4 +1,5 @@ import { screen, fireEvent, render, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { ExcelEditor, StateContext } from '../containers'; import { initController } from '../controller'; @@ -17,6 +18,14 @@ export function type(content: string, isEnter = true) { fireEvent.keyDown(document.body, { key: 'ArrowLeft' }); } } + +export async function chooseSelectOption(testId: string, label: string | RegExp) { + const user = userEvent.setup(); + const select = screen.getByTestId(testId); + await user.click(select.querySelector('button')!); + await user.click(await screen.findByRole('option', { name: label })); +} + export function extractDataFromTransform( transform: string, type: 'translateX' | 'translateY' | 'rotate', diff --git a/packages/excel-collab/src/canvas/__tests__/util.ts b/packages/excel-collab/src/canvas/__tests__/util.ts index 31cb1897..d54383f7 100644 --- a/packages/excel-collab/src/canvas/__tests__/util.ts +++ b/packages/excel-collab/src/canvas/__tests__/util.ts @@ -9,7 +9,7 @@ import { import fs from 'fs'; import { setDpr } from '../../util'; import path from 'path'; -import pixelMatch from './pixelMatch' +import pixelMatch from './pixelMatch'; import PNG from 'pngjs'; const defaultWidth = 200; @@ -73,8 +73,14 @@ async function compareImage( const threshold = Math.sqrt(result / (width * height)); if (threshold > maxThreshold) { console.log(`threshold: ${threshold},maxThreshold: ${maxThreshold}`); - const diffPath = basePath.replace('.png', '.diff.png'); - await fs.promises.writeFile(diffPath, PNG.PNG.sync.write(diff) as any); + await fs.promises.writeFile( + basePath.replace('.png', '.diff.png'), + PNG.PNG.sync.write(diff) as any, + ); + await fs.promises.writeFile( + basePath.replace('.png', '.actual.png'), + newImageBuffer, + ); return 1; } else { return 0; diff --git a/packages/excel-collab/src/canvas/util.ts b/packages/excel-collab/src/canvas/util.ts index cd9f2627..a5f3c9dd 100644 --- a/packages/excel-collab/src/canvas/util.ts +++ b/packages/excel-collab/src/canvas/util.ts @@ -1,5 +1,5 @@ import { splitToWords } from '../util'; -import { getThemeColor, sizeConfig } from '../theme'; +import { getThemeColor } from '../theme'; import { npx, dpr } from '../util'; import { makeFont } from '../util'; import { @@ -9,7 +9,7 @@ import { DEFAULT_LINE_WIDTH, BORDER_TYPE_MAP, isEmpty, - anyError + anyError, } from '../util'; import { CanvasOverlayPosition, @@ -320,7 +320,7 @@ export function renderCell( isMergeContent(Boolean(isMergeCell), text), ); if (width > 0 && height > 0) { - const lineGap = Math.ceil((fontSize * (sizeConfig.lineHeight - 1)) / 2); + const lineGap = Math.ceil((fontSize * (1.5 - 1)) / 2); let list: Point[] = []; for (const item of resultList) { fillText(ctx, item.text, item.x, item.y); @@ -362,7 +362,7 @@ function computeCell( isMergeContent?: boolean, ) { const fontSize = style?.fontSize ? style.fontSize : DEFAULT_FONT_SIZE; - const lineGap = Math.ceil((fontSize * (sizeConfig.lineHeight - 1)) / 2); + const lineGap = Math.ceil((fontSize * (1.5 - 1)) / 2); const verticalAlign = style?.verticalAlign ?? EVerticalAlign.MIDDLE; const { left, top, height } = cellInfo; const width = Math.max(cellInfo.width, ...texts.map((v) => v.width)); @@ -446,7 +446,7 @@ function computeCell( height: textHeight, }); } - textHeight = Math.max(textHeight, fontSize * sizeConfig.lineHeight); + textHeight = Math.max(textHeight, fontSize * 1.5); textWidth += lineGap; textHeight += lineGap; if (textWidth <= width && textHeight <= height) { diff --git a/packages/excel-collab/src/component/Breadcrumbs.css b/packages/excel-collab/src/component/Breadcrumbs.css new file mode 100644 index 00000000..cd9075aa --- /dev/null +++ b/packages/excel-collab/src/component/Breadcrumbs.css @@ -0,0 +1,46 @@ +.react-aria-Breadcrumbs { + display: flex; + align-items: center; + list-style: none; + margin: 0; + padding: 0; + color: var(--text-color); + + .react-aria-Breadcrumb { + display: flex; + align-items: center; + } + + .react-aria-Link { + font: var(--font-size) system-ui; + color: var(--link-color-secondary); + outline: none; + position: relative; + text-decoration: none; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + + &[data-hovered], + &[data-focus-visible] { + text-decoration: underline; + } + + &[data-current] { + color: var(--text-color); + font-weight: 600; + } + + &[data-focus-visible] { + border-radius: var(--radius-sm); + outline: 2px solid var(--focus-ring-color); + } + + &[data-disabled] { + cursor: default; + + &:not([data-current]) { + color: var(--text-color-disabled); + } + } + } +} diff --git a/packages/excel-collab/src/component/Breadcrumbs.tsx b/packages/excel-collab/src/component/Breadcrumbs.tsx new file mode 100644 index 00000000..8415921f --- /dev/null +++ b/packages/excel-collab/src/component/Breadcrumbs.tsx @@ -0,0 +1,30 @@ +'use client'; +import { + Breadcrumbs as RACBreadcrumbs, + type BreadcrumbsProps, + Breadcrumb as RACBreadcrumb, + type BreadcrumbProps, + type LinkProps, + Link, +} from 'react-aria-components/Breadcrumbs'; +import { ChevronRight } from 'lucide-react'; +import './Breadcrumbs.css'; + +export function Breadcrumbs(props: BreadcrumbsProps) { + return ; +} + +export function Breadcrumb( + props: BreadcrumbProps & Omit, +) { + return ( + + {({ isCurrent }) => ( + <> + + {!isCurrent && } + + )} + + ); +} diff --git a/packages/excel-collab/src/component/Button.css b/packages/excel-collab/src/component/Button.css new file mode 100644 index 00000000..c6dc713d --- /dev/null +++ b/packages/excel-collab/src/component/Button.css @@ -0,0 +1,42 @@ +@import './utilities.css'; + +.react-aria-Button { + border: none; + border-radius: var(--radius); + appearance: none; + font: var(--font-size) system-ui; + font-weight: 500; + margin: 0; + height: var(--spacing-8); + padding: 0 var(--spacing-3); + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-1); + -webkit-tap-highlight-color: transparent; + + > svg { + width: calc(var(--spacing) * 4.5); + height: calc(var(--spacing) * 4.5); + } + + &[data-pressed] { + scale: 0.95; + } + + .react-aria-ProgressBar { + @media (prefers-color-scheme: dark) { + --highlight-background: var(--gray-1600); + } + } + + kbd { + font: var(--font-size-sm) system-ui; + background: var(--highlight-hover); + border: 0.5px solid var(--tint-500); + padding: 0 var(--spacing-1); + border-radius: var(--radius-sm); + margin-inline-start: var(--spacing-3); + } +} diff --git a/packages/excel-collab/src/component/Button.tsx b/packages/excel-collab/src/component/Button.tsx new file mode 100644 index 00000000..8c260e05 --- /dev/null +++ b/packages/excel-collab/src/component/Button.tsx @@ -0,0 +1,42 @@ +'use client'; +import { + Button as RACButton, + type ButtonProps as RACButtonProps, +} from 'react-aria-components/Button'; +import { composeRenderProps } from 'react-aria-components/composeRenderProps'; +import { ProgressCircle } from './ProgressCircle'; +import './Button.css'; +import { TooltipTrigger } from 'react-aria-components/Tooltip'; +import { Tooltip } from './Tooltip'; + +interface ButtonProps extends Omit { + /** + * The visual style of the button (Vanilla CSS implementation specific). + * + * @default 'secondary' + */ + variant?: 'primary' | 'secondary' | 'quiet'; + 'aria-label': string; +} + +export function Button(props: ButtonProps) { + return ( + + + {composeRenderProps(props.children, (children, { isPending }) => ( + <> + {!isPending && children} + {isPending && ( + + )} + + ))} + + {props['aria-label']} + + ); +} diff --git a/packages/excel-collab/src/component/Calendar.css b/packages/excel-collab/src/component/Calendar.css new file mode 100644 index 00000000..3f81a8a5 --- /dev/null +++ b/packages/excel-collab/src/component/Calendar.css @@ -0,0 +1,97 @@ +@import './utilities.css'; + +.react-aria-Calendar { + display: flex; + flex-direction: column; + gap: var(--spacing-2); + width: fit-content; + max-width: 100%; + color: var(--text-color); + font: var(--font-size) system-ui; + + .months { + display: flex; + gap: var(--spacing-3); + } + + .month { + width: calc(var(--spacing-9) * 7); + min-width: calc(var(--spacing-7) * 7); + container-type: inline-size; + } + + header { + display: flex; + align-items: center; + margin: 0 var(--spacing-1) var(--spacing-4) var(--spacing-1); + min-height: var(--spacing-8); + + .react-aria-CalendarHeading { + flex: 1; + margin: 0; + text-align: center; + font-size: var(--font-size-lg); + } + } + + .react-aria-CalendarGrid { + border-collapse: collapse; + border-spacing: 0; + width: 100%; + table-layout: fixed; + + td, + th { + padding: 0; + } + } + + [slot='errorMessage'] { + font: var(--font-size-sm) system-ui; + color: var(--invalid-color); + } +} + +.react-aria-CalendarHeaderCell { + font-size: var(--font-size-sm); +} + +.react-aria-CalendarCell { + --gap: var(--spacing-1); + width: calc((100cqw / 7) - var(--gap)); + aspect-ratio: 1; + margin: calc(var(--gap) / 2); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-size); + text-align: center; + border-radius: 9999px; + cursor: default; + forced-color-adjust: none; + transition: scale 200ms; + -webkit-tap-highlight-color: transparent; + + @media (min-resolution: 200dpi) { + @container (width < 300px) { + --gap: 0px; + } + } + + &[data-disabled] { + opacity: 0.5; + } + + &[data-pressed] { + scale: 0.9; + } + + &[data-unavailable] { + text-decoration: line-through; + color: var(--invalid-color); + } + + &[data-invalid] { + --button-color: var(--invalid-color); + } +} diff --git a/packages/excel-collab/src/component/Calendar.tsx b/packages/excel-collab/src/component/Calendar.tsx new file mode 100644 index 00000000..80a0c1ed --- /dev/null +++ b/packages/excel-collab/src/component/Calendar.tsx @@ -0,0 +1,62 @@ +'use client'; +import { + Calendar as AriaCalendar, + CalendarCell as AriaCalendarCell, + CalendarGrid as AriaCalendarGrid, + CalendarHeading, + type CalendarProps as AriaCalendarProps, + type DateValue, + type CalendarCellProps, + type CalendarGridProps +} from 'react-aria-components/Calendar'; +import {Text} from './Content'; +import {ChevronLeft, ChevronRight} from 'lucide-react'; +import {Button} from './Button'; +import './Calendar.css'; + +export interface CalendarProps extends AriaCalendarProps { + errorMessage?: string; +} + +export function Calendar({errorMessage, ...props}: CalendarProps) { + let months = props.visibleDuration?.months || 1; + return ( + +
+ {Array.from({length: months}, (_, i) => ( +
+
+ {i === 0 && ( + + )} + + {i === months - 1 && ( + + )} +
+ {date => } +
+ ))} +
+ {errorMessage && {errorMessage}} +
+ ); +} + +export function CalendarCell(props: CalendarCellProps) { + return ( + + ); +} + +export function CalendarGrid(props: CalendarGridProps) { + return ; +} diff --git a/packages/excel-collab/src/component/Checkbox.css b/packages/excel-collab/src/component/Checkbox.css new file mode 100644 index 00000000..eb72e6f7 --- /dev/null +++ b/packages/excel-collab/src/component/Checkbox.css @@ -0,0 +1,75 @@ + +@import './utilities.css'; + +.react-aria-CheckboxField { + display: flex; + flex-direction: column; + gap: var(--spacing-1); + + [slot='description'], + .react-aria-FieldError { + margin-left: calc(var(--spacing) * 6.5); + } + + &[data-disabled] [slot='description'] { + color: var(--text-color-disabled); + } +} + +.react-aria-CheckboxButton { + --checkmark-color: var(--highlight-foreground); + + display: flex; + /* This is needed so the HiddenInput is positioned correctly */ + position: relative; + align-items: center; + gap: var(--spacing-2); + font: var(--font-size) system-ui; + color: var(--text-color); + forced-color-adjust: none; + -webkit-tap-highlight-color: transparent; + + .indicator { + width: calc(var(--spacing) * 4.5); + height: calc(var(--spacing) * 4.5); + box-sizing: border-box; + border-radius: var(--radius-sm); + transition: all 200ms; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + svg { + width: calc(100% - var(--spacing-2)); + height: calc(100% - var(--spacing-2)); + fill: none; + stroke: var(--checkmark-color); + stroke-width: 3px; + stroke-dasharray: 22px; + stroke-dashoffset: 66; + stroke-linecap: round; + stroke-linejoin: round; + transition: all 200ms; + } + + &[data-selected], + &[data-indeterminate] { + svg { + stroke-dashoffset: 44; + } + } + + &[data-indeterminate] { + svg { + stroke: none; + fill: var(--checkmark-color); + } + } + + &[data-disabled] { + color: var(--text-color-disabled); + --checkmark-color: var(--text-color-disabled); + } +} diff --git a/packages/excel-collab/src/component/Checkbox.tsx b/packages/excel-collab/src/component/Checkbox.tsx new file mode 100644 index 00000000..4a31674f --- /dev/null +++ b/packages/excel-collab/src/component/Checkbox.tsx @@ -0,0 +1,44 @@ +'use client'; +import { + CheckboxButton, + CheckboxField, + type CheckboxFieldProps, + type ValidationResult +} from 'react-aria-components/Checkbox'; +import './Checkbox.css'; +import {Description, FieldError} from './Form'; +import type {ReactNode} from 'react'; + +interface CheckboxProps extends CheckboxFieldProps { + children?: ReactNode; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); +} + +export function Checkbox({children, description, errorMessage, ...props}: CheckboxProps) { + return ( + + + {({isIndeterminate}) => ( + <> +
+ +
+ {children} + + )} +
+ {description && {description}} + {errorMessage} +
+ ); +} diff --git a/packages/excel-collab/src/component/CheckboxGroup.css b/packages/excel-collab/src/component/CheckboxGroup.css new file mode 100644 index 00000000..f3ec8de7 --- /dev/null +++ b/packages/excel-collab/src/component/CheckboxGroup.css @@ -0,0 +1,22 @@ + +@import './Form.css'; + +.react-aria-CheckboxGroup { + display: flex; + flex-direction: column; + gap: var(--spacing-1); + + .checkbox-items { + display: flex; + gap: var(--spacing-3); + flex-direction: column; + } + + &[data-orientation='vertical'] .checkbox-items { + flex-direction: column; + } + + &[data-orientation='horizontal'] .checkbox-items { + flex-direction: row; + } +} diff --git a/packages/excel-collab/src/component/CheckboxGroup.tsx b/packages/excel-collab/src/component/CheckboxGroup.tsx new file mode 100644 index 00000000..7435d380 --- /dev/null +++ b/packages/excel-collab/src/component/CheckboxGroup.tsx @@ -0,0 +1,34 @@ +'use client'; +import { + CheckboxGroup as AriaCheckboxGroup, + type CheckboxGroupProps as AriaCheckboxGroupProps, + type ValidationResult +} from 'react-aria-components/CheckboxGroup'; +import {Label, FieldError, Description} from './Form'; +import './CheckboxGroup.css'; + +export interface CheckboxGroupProps extends Omit { + children?: React.ReactNode; + label?: string; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); + orientation?: 'horizontal' | 'vertical'; +} + +export function CheckboxGroup({ + label, + description, + errorMessage, + children, + orientation = 'vertical', + ...props +}: CheckboxGroupProps) { + return ( + + {label && } +
{children}
+ {description && {description}} + {errorMessage} +
+ ); +} diff --git a/packages/excel-collab/src/component/ColorArea.css b/packages/excel-collab/src/component/ColorArea.css new file mode 100644 index 00000000..d633ce1c --- /dev/null +++ b/packages/excel-collab/src/component/ColorArea.css @@ -0,0 +1,15 @@ + + +.react-aria-ColorArea { + width: 100%; + max-width: calc(var(--spacing) * 56); + aspect-ratio: 1/1; + border-radius: var(--radius); + flex-shrink: 0; +} + +.react-aria-ColorArea { + &[data-disabled] { + background: gray !important; + } +} diff --git a/packages/excel-collab/src/component/ColorArea.tsx b/packages/excel-collab/src/component/ColorArea.tsx new file mode 100644 index 00000000..f06d0dcf --- /dev/null +++ b/packages/excel-collab/src/component/ColorArea.tsx @@ -0,0 +1,12 @@ +'use client'; +import {ColorArea as AriaColorArea, type ColorAreaProps} from 'react-aria-components/ColorArea'; +import {ColorThumb} from './ColorThumb'; +import './ColorArea.css'; + +export function ColorArea(props: ColorAreaProps) { + return ( + + + + ); +} diff --git a/packages/excel-collab/src/component/ColorField.css b/packages/excel-collab/src/component/ColorField.css new file mode 100644 index 00000000..c7cdcd65 --- /dev/null +++ b/packages/excel-collab/src/component/ColorField.css @@ -0,0 +1,7 @@ + +@import './TextField.css'; + +.react-aria-ColorField { + display: flex; + flex-direction: column; +} diff --git a/packages/excel-collab/src/component/ColorField.tsx b/packages/excel-collab/src/component/ColorField.tsx new file mode 100644 index 00000000..fb121425 --- /dev/null +++ b/packages/excel-collab/src/component/ColorField.tsx @@ -0,0 +1,33 @@ +'use client'; +import { + ColorField as AriaColorField, + type ColorFieldProps as AriaColorFieldProps, + Input, + type ValidationResult +} from 'react-aria-components/ColorField'; +import {Label, FieldError, Description} from './Form'; +import './ColorField.css'; + +export interface ColorFieldProps extends AriaColorFieldProps { + label?: string; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); + placeholder?: string; +} + +export function ColorField({ + label, + description, + errorMessage, + placeholder, + ...props +}: ColorFieldProps) { + return ( + + {label && } + + {description && {description}} + {errorMessage} + + ); +} diff --git a/packages/excel-collab/src/component/ColorPicker.css b/packages/excel-collab/src/component/ColorPicker.css new file mode 100644 index 00000000..208acce8 --- /dev/null +++ b/packages/excel-collab/src/component/ColorPicker.css @@ -0,0 +1,31 @@ + + +.color-picker { + background: none; + border: none; + padding: 0; + display: flex; + align-items: center; + gap: var(--spacing-2); + outline: none; + appearance: none; + vertical-align: middle; + font: var(--font-size) system-ui; + color: var(--text-color); + -webkit-tap-highlight-color: transparent; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } +} + +.react-aria-Popover.color-picker-dialog { + padding: var(--spacing-3); + display: flex; + flex-direction: column; + gap: var(--spacing-2); + min-width: 192px; + max-height: inherit; + overflow: auto; +} diff --git a/packages/excel-collab/src/component/ColorPicker.tsx b/packages/excel-collab/src/component/ColorPicker.tsx new file mode 100644 index 00000000..decdd59b --- /dev/null +++ b/packages/excel-collab/src/component/ColorPicker.tsx @@ -0,0 +1,62 @@ +'use client'; +import { Button } from 'react-aria-components/Button'; +import { + ColorPicker as AriaColorPicker, + type ColorPickerProps as AriaColorPickerProps, +} from 'react-aria-components/ColorPicker'; +import { DialogTrigger } from './Dialog'; +import { ColorSwatch } from './ColorSwatch'; +import { ColorSlider } from './ColorSlider'; +import { ColorArea } from './ColorArea'; +import { ColorField } from './ColorField'; +import { Popover } from './Popover'; +import './ColorPicker.css'; + +export interface ColorPickerProps extends Omit< + AriaColorPickerProps, + 'children' +> { + label?: string; + children?: React.ReactNode; + trigger?: React.ReactNode; +} + +export function ColorPicker({ + label, + children, + trigger, + ...props +}: ColorPickerProps) { + return ( + + + {trigger ? ( + trigger + ) : ( + + )} + + + {children || ( + <> + + + + + )} + + + + ); +} diff --git a/packages/excel-collab/src/component/ColorSlider.css b/packages/excel-collab/src/component/ColorSlider.css new file mode 100644 index 00000000..bebd2134 --- /dev/null +++ b/packages/excel-collab/src/component/ColorSlider.css @@ -0,0 +1,64 @@ + + +.react-aria-ColorSlider { + display: grid; + grid-template-areas: + 'label output' + 'track track'; + grid-template-columns: 1fr auto; + gap: var(--spacing-1); + + .react-aria-Label { + grid-area: label; + } + + .react-aria-SliderOutput { + grid-area: output; + text-align: end; + font: var(--font-size) system-ui; + color: var(--text-color); + } + + .react-aria-SliderTrack { + grid-area: track; + border-radius: var(--radius); + } + + &[data-orientation='horizontal'] { + width: 100%; + max-width: 300px; + + .react-aria-SliderTrack { + height: var(--spacing-6); + } + + .react-aria-ColorThumb { + top: 50%; + } + } + + &[data-orientation='vertical'] { + height: 200px; + display: block; + + .react-aria-Label, + .react-aria-SliderOutput { + display: none; + } + + .react-aria-SliderTrack { + width: var(--spacing-6); + height: 100%; + } + + .react-aria-ColorThumb { + left: 50%; + } + } + + &[data-disabled] { + .react-aria-SliderTrack { + background: gray !important; + } + } +} diff --git a/packages/excel-collab/src/component/ColorSlider.tsx b/packages/excel-collab/src/component/ColorSlider.tsx new file mode 100644 index 00000000..b59453fd --- /dev/null +++ b/packages/excel-collab/src/component/ColorSlider.tsx @@ -0,0 +1,30 @@ +'use client'; +import { + ColorSlider as AriaColorSlider, + type ColorSliderProps as AriaColorSliderProps, + SliderOutput, + SliderTrack +} from 'react-aria-components/ColorSlider'; +import {Label} from './Form'; +import {ColorThumb} from './ColorThumb'; +import './ColorSlider.css'; + +export interface ColorSliderProps extends AriaColorSliderProps { + label?: string; +} + +export function ColorSlider({label, ...props}: ColorSliderProps) { + return ( + + + + ({ + background: `${defaultStyle.background}, + repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px` + })}> + + + + ); +} diff --git a/packages/excel-collab/src/component/ColorSwatch.css b/packages/excel-collab/src/component/ColorSwatch.css new file mode 100644 index 00000000..12cf585b --- /dev/null +++ b/packages/excel-collab/src/component/ColorSwatch.css @@ -0,0 +1,8 @@ + + +.react-aria-ColorSwatch { + width: var(--spacing-8); + height: var(--spacing-8); + border-radius: 9999px; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); +} diff --git a/packages/excel-collab/src/component/ColorSwatch.tsx b/packages/excel-collab/src/component/ColorSwatch.tsx new file mode 100644 index 00000000..1403b070 --- /dev/null +++ b/packages/excel-collab/src/component/ColorSwatch.tsx @@ -0,0 +1,18 @@ +'use client'; +import { + ColorSwatch as AriaColorSwatch, + type ColorSwatchProps +} from 'react-aria-components/ColorSwatch'; +import './ColorSwatch.css'; + +export function ColorSwatch(props: ColorSwatchProps) { + return ( + ({ + background: `linear-gradient(${color}, ${color}), + repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px` + })} + /> + ); +} diff --git a/packages/excel-collab/src/component/ColorSwatchPicker.css b/packages/excel-collab/src/component/ColorSwatchPicker.css new file mode 100644 index 00000000..d5f59e27 --- /dev/null +++ b/packages/excel-collab/src/component/ColorSwatchPicker.css @@ -0,0 +1,41 @@ + + +.react-aria-ColorSwatchPicker { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.react-aria-ColorSwatchPickerItem { + position: relative; + outline: none; + border-radius: 9999px; + width: fit-content; + forced-color-adjust: none; + -webkit-tap-highlight-color: transparent; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + &[data-selected]::after { + content: ''; + position: absolute; + inset: 0; + border: 2px solid black; + outline: 2px solid white; + outline-offset: -4px; + border-radius: inherit; + } + + &[data-disabled] { + opacity: 0.2; + } +} + +.react-aria-ColorSwatchPicker { + &[data-layout='stack'] { + flex-direction: column; + } +} diff --git a/packages/excel-collab/src/component/ColorSwatchPicker.tsx b/packages/excel-collab/src/component/ColorSwatchPicker.tsx new file mode 100644 index 00000000..0d88f361 --- /dev/null +++ b/packages/excel-collab/src/component/ColorSwatchPicker.tsx @@ -0,0 +1,21 @@ +'use client'; +import { + ColorSwatchPicker as AriaColorSwatchPicker, + ColorSwatchPickerItem as AriaColorSwatchPickerItem, + type ColorSwatchPickerItemProps, + type ColorSwatchPickerProps +} from 'react-aria-components/ColorSwatchPicker'; +import {ColorSwatch} from './ColorSwatch'; +import './ColorSwatchPicker.css'; + +export function ColorSwatchPicker({children, ...props}: ColorSwatchPickerProps) { + return {children}; +} + +export function ColorSwatchPickerItem(props: ColorSwatchPickerItemProps) { + return ( + + + + ); +} diff --git a/packages/excel-collab/src/component/ColorThumb.css b/packages/excel-collab/src/component/ColorThumb.css new file mode 100644 index 00000000..b08bc114 --- /dev/null +++ b/packages/excel-collab/src/component/ColorThumb.css @@ -0,0 +1,22 @@ + + +.react-aria-ColorThumb { + border: 2px solid white; + box-shadow: + 0 0 0 1px black, + inset 0 0 0 1px black; + width: calc(var(--spacing) * 4.5); + height: calc(var(--spacing) * 4.5); + border-radius: 50%; + box-sizing: border-box; + + &[data-focus-visible] { + width: var(--spacing-6); + height: var(--spacing-6); + } + + &[data-disabled] { + background: gray !important; + opacity: 0.5; + } +} diff --git a/packages/excel-collab/src/component/ColorThumb.tsx b/packages/excel-collab/src/component/ColorThumb.tsx new file mode 100644 index 00000000..fff0cba2 --- /dev/null +++ b/packages/excel-collab/src/component/ColorThumb.tsx @@ -0,0 +1,6 @@ +import {ColorThumb as AriaColorThumb, type ColorThumbProps} from 'react-aria-components/ColorThumb'; +import './ColorThumb.css'; + +export function ColorThumb(props: ColorThumbProps) { + return ; +} diff --git a/packages/excel-collab/src/component/ColorWheel.css b/packages/excel-collab/src/component/ColorWheel.css new file mode 100644 index 00000000..a37d5efd --- /dev/null +++ b/packages/excel-collab/src/component/ColorWheel.css @@ -0,0 +1,9 @@ + + +.react-aria-ColorWheel { + &[data-disabled] { + .react-aria-ColorWheelTrack { + background: gray !important; + } + } +} diff --git a/packages/excel-collab/src/component/ColorWheel.tsx b/packages/excel-collab/src/component/ColorWheel.tsx new file mode 100644 index 00000000..bd25ae5c --- /dev/null +++ b/packages/excel-collab/src/component/ColorWheel.tsx @@ -0,0 +1,18 @@ +'use client'; +import { + ColorWheel as AriaColorWheel, + type ColorWheelProps as AriaColorWheelProps, + ColorWheelTrack +} from 'react-aria-components/ColorWheel'; +import {ColorThumb} from './ColorThumb'; +import './ColorWheel.css'; +export interface ColorWheelProps extends Omit {} + +export function ColorWheel(props: ColorWheelProps) { + return ( + + + + + ); +} diff --git a/packages/excel-collab/src/component/ComboBox.css b/packages/excel-collab/src/component/ComboBox.css new file mode 100644 index 00000000..5d37de01 --- /dev/null +++ b/packages/excel-collab/src/component/ComboBox.css @@ -0,0 +1,32 @@ + +@import './TextField.css'; + +.react-aria-ComboBox { + display: flex; + flex-direction: column; + color: var(--text-color); + width: calc(var(--spacing) * 50); + + .combobox-field { + display: flex; + align-items: center; + height: var(--spacing-8); + width: 100%; + } + + .react-aria-Input { + width: 100%; + padding: 0 calc(var(--spacing-3) + var(--spacing-6)) 0 var(--spacing-3); + } + + .react-aria-ComboBoxValue { + margin-top: var(--spacing-1); + font: var(--font-size-sm) system-ui; + color: var(--text-color); + } +} + +.combobox-popover[data-trigger='ComboBox'] { + width: var(--trigger-width); + padding: 0; +} diff --git a/packages/excel-collab/src/component/ComboBox.tsx b/packages/excel-collab/src/component/ComboBox.tsx new file mode 100644 index 00000000..80ef180e --- /dev/null +++ b/packages/excel-collab/src/component/ComboBox.tsx @@ -0,0 +1,61 @@ +'use client'; +import { + ComboBox as AriaComboBox, + type ComboBoxProps as AriaComboBoxProps, + ComboBoxValue, + Input, + type ListBoxItemProps, + type ListBoxProps, + type ValidationResult +} from 'react-aria-components/ComboBox'; +import {Label, FieldError, FieldButton, Description} from './Form'; +import {DropdownItem, DropdownListBox} from './ListBox'; +import {Popover} from './Popover'; +import {ChevronDown} from 'lucide-react'; +import './ComboBox.css'; + +export interface ComboBoxProps extends Omit< + AriaComboBoxProps, + 'children' +> { + label?: string; + description?: string | null; + errorMessage?: string | ((validation: ValidationResult) => string); + children: React.ReactNode | ((item: T) => React.ReactNode); + placeholder?: string; +} + +export function ComboBox({ + label, + description, + errorMessage, + children, + placeholder, + ...props +}: ComboBoxProps) { + return ( + + {label && } +
+ + + + +
+ {props.selectionMode === 'multiple' && } + {description && {description}} + {errorMessage} + + {children} + +
+ ); +} + +export function ComboBoxListBox(props: ListBoxProps) { + return ; +} + +export function ComboBoxItem(props: ListBoxItemProps) { + return ; +} diff --git a/packages/excel-collab/src/component/CommandPalette.css b/packages/excel-collab/src/component/CommandPalette.css new file mode 100644 index 00000000..1de65479 --- /dev/null +++ b/packages/excel-collab/src/component/CommandPalette.css @@ -0,0 +1,20 @@ + + +.command-palette-dialog { + width: min(90vw, 500px); + height: min(90vh, 356px); + max-height: inherit; + padding: var(--spacing-2); + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: var(--spacing-2); + + > * { + zoom: 1.125; + } + + .react-aria-Menu { + flex: 1; + } +} diff --git a/packages/excel-collab/src/component/CommandPalette.tsx b/packages/excel-collab/src/component/CommandPalette.tsx new file mode 100644 index 00000000..22a772dd --- /dev/null +++ b/packages/excel-collab/src/component/CommandPalette.tsx @@ -0,0 +1,55 @@ +'use client'; +import { + Autocomplete as AriaAutocomplete, + type AutocompleteProps as AriaAutocompleteProps, + useFilter, +} from 'react-aria-components/Autocomplete'; +import { type MenuProps as AriaMenuProps } from 'react-aria-components/Menu'; +import { Dialog } from 'react-aria-components/Dialog'; +import { Menu } from './Menu'; +import { SearchField } from './SearchField'; +import { Modal } from './Modal'; +import { useEffect } from 'react'; +import './CommandPalette.css'; + +export interface CommandPaletteProps + extends Omit, AriaMenuProps { + isOpen: boolean; + onOpenChange: (isOpen?: boolean) => void; +} + +export function CommandPalette(props: CommandPaletteProps) { + let { isOpen, onOpenChange } = props; + let { contains } = useFilter({ sensitivity: 'base' }); + + useEffect(() => { + let isMacUA = /mac(os|intosh)/i.test(navigator.userAgent); + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'j' && (isMacUA ? e.metaKey : e.ctrlKey)) { + e.preventDefault(); + onOpenChange(true); + } else if (e.key === 'Escape') { + e.preventDefault(); + onOpenChange(false); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [onOpenChange]); + + return ( + + + + + 'No results found.'} /> + + + + ); +} diff --git a/packages/excel-collab/src/component/Content.css b/packages/excel-collab/src/component/Content.css new file mode 100644 index 00000000..f2d55b9e --- /dev/null +++ b/packages/excel-collab/src/component/Content.css @@ -0,0 +1,3 @@ +.react-aria-Heading { + margin: 0; +} diff --git a/packages/excel-collab/src/component/Content.tsx b/packages/excel-collab/src/component/Content.tsx new file mode 100644 index 00000000..c4ece742 --- /dev/null +++ b/packages/excel-collab/src/component/Content.tsx @@ -0,0 +1,11 @@ +import {Heading as AriaHeading, type HeadingProps} from 'react-aria-components/Heading'; +import {Text as AriaText, type TextProps} from 'react-aria-components/Text'; +import './Content.css'; + +export function Heading(props: HeadingProps) { + return ; +} + +export function Text(props: TextProps) { + return ; +} diff --git a/packages/excel-collab/src/component/DateField.css b/packages/excel-collab/src/component/DateField.css new file mode 100644 index 00000000..ddb52101 --- /dev/null +++ b/packages/excel-collab/src/component/DateField.css @@ -0,0 +1,72 @@ + +@import './utilities.css'; + +.react-aria-DateField { + display: flex; + flex-direction: column; +} + +.react-aria-DateInput { + display: inline; + padding: 0 var(--spacing-3); + box-sizing: border-box; + border-radius: var(--radius); + font: var(--font-size) system-ui; + color: var(--field-text-color); + height: var(--spacing-8); + line-height: var(--spacing-8); + width: fit-content; + min-width: 150px; + white-space: nowrap; + forced-color-adjust: none; + cursor: text; + + &[data-focus-within] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + + &[data-disabled] { + color: var(--text-color-disabled); + cursor: default; + } +} + +.react-aria-DateSegment { + padding: 2px; + font-size: var(--font-size); + font-variant-numeric: tabular-nums; + text-align: end; + color: var(--field-text-color); + -webkit-tap-highlight-color: transparent; + + &[data-type='literal'] { + padding: 0; + } + + &[data-placeholder] { + color: var(--text-color-placeholder); + } + + &:focus { + color: var(--highlight-foreground); + background: var(--highlight-background); + outline: none; + border-radius: 4px; + caret-color: transparent; + } + + &[data-invalid] { + color: var(--invalid-color); + + &:focus { + background: var(--highlight-background-invalid); + color: var(--highlight-foreground); + } + } + + &[data-disabled] { + color: var(--text-color-disabled); + cursor: default; + } +} diff --git a/packages/excel-collab/src/component/DateField.tsx b/packages/excel-collab/src/component/DateField.tsx new file mode 100644 index 00000000..49150b18 --- /dev/null +++ b/packages/excel-collab/src/component/DateField.tsx @@ -0,0 +1,43 @@ +'use client'; +import { + DateField as AriaDateField, + type DateFieldProps as AriaDateFieldProps, + DateInput as AriaDateInput, + type DateInputProps, + DateSegment as AriaDateSegment, + type DateSegmentProps, + type DateValue, + type ValidationResult +} from 'react-aria-components/DateField'; +import {Label, FieldError, Description} from './Form'; +import './DateField.css'; + +export interface DateFieldProps extends AriaDateFieldProps { + label?: string; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); +} + +export function DateField({ + label, + description, + errorMessage, + ...props +}: DateFieldProps) { + return ( + + + {segment => } + {description && {description}} + {errorMessage} + + ); +} + +export function DateSegment(props: DateSegmentProps) { + return ; +} + +export function DateInput(props: DateInputProps) { + return ; +} diff --git a/packages/excel-collab/src/component/DatePicker.css b/packages/excel-collab/src/component/DatePicker.css new file mode 100644 index 00000000..70537070 --- /dev/null +++ b/packages/excel-collab/src/component/DatePicker.css @@ -0,0 +1,27 @@ + + +.react-aria-DatePicker { + max-width: 100%; + + .react-aria-Group { + display: flex; + width: 100%; + align-items: center; + height: var(--spacing-8); + border-radius: var(--radius); + overflow: clip; + } + + .react-aria-DateInput { + flex: 1; + min-width: 0; + padding-right: var(--spacing-10); + overflow-x: auto; + overflow-y: clip; + scrollbar-width: none; + } + + .field-Button { + margin-right: var(--spacing-1); + } +} diff --git a/packages/excel-collab/src/component/DatePicker.tsx b/packages/excel-collab/src/component/DatePicker.tsx new file mode 100644 index 00000000..02d3e6ee --- /dev/null +++ b/packages/excel-collab/src/component/DatePicker.tsx @@ -0,0 +1,45 @@ +'use client'; +import { + DatePicker as AriaDatePicker, + type DatePickerProps as AriaDatePickerProps, + type DateValue, + Group, + type ValidationResult +} from 'react-aria-components/DatePicker'; +import {DateInput, DateSegment} from './DateField'; +import {Label, FieldError, Description} from './Form'; +import {FieldButton} from './Form'; +import {Calendar} from './Calendar'; +import {Popover} from './Popover'; +import {ChevronDown} from 'lucide-react'; +import './DatePicker.css'; + +export interface DatePickerProps extends AriaDatePickerProps { + label?: string; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); +} + +export function DatePicker({ + label, + description, + errorMessage, + ...props +}: DatePickerProps) { + return ( + + + + {segment => } + + + + + {description && {description}} + {errorMessage} + + + + + ); +} diff --git a/packages/excel-collab/src/component/DateRangePicker.css b/packages/excel-collab/src/component/DateRangePicker.css new file mode 100644 index 00000000..34549ffb --- /dev/null +++ b/packages/excel-collab/src/component/DateRangePicker.css @@ -0,0 +1,71 @@ + +@import './utilities.css'; + +.react-aria-DateRangePicker { + color: var(--text-color); + max-width: 100%; + + .react-aria-Group { + display: flex; + align-items: center; + width: fit-content; + min-width: 220px; + max-width: 100%; + height: var(--spacing-8); + box-sizing: border-box; + overflow: clip; + position: relative; + padding: 0 var(--spacing-1) 0 var(--spacing-3); + border-radius: var(--radius); + font-size: var(--font-size); + color: var(--field-text-color); + white-space: nowrap; + cursor: text; + + &[data-focus-within] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + + &[data-disabled] { + color: var(--text-color-disabled); + } + } + + .date-fields { + flex: 1; + display: flex; + align-items: center; + width: fit-content; + overflow-x: auto; + overflow-y: clip; + scrollbar-width: none; + } + + .field-Button { + margin-left: auto; + } + + [slot='start'] ~ span { + padding: 0 4px; + } + + [slot='end'] { + margin-right: 1.75rem; + flex: 1; + } + + .react-aria-DateInput { + width: unset; + min-width: unset; + padding: unset; + border: unset; + box-shadow: none; + background: none; + height: auto; + line-height: normal; + &[data-focus-within] { + outline: unset; + } + } +} diff --git a/packages/excel-collab/src/component/DateRangePicker.tsx b/packages/excel-collab/src/component/DateRangePicker.tsx new file mode 100644 index 00000000..940a9c6c --- /dev/null +++ b/packages/excel-collab/src/component/DateRangePicker.tsx @@ -0,0 +1,49 @@ +'use client'; +import { + DateRangePicker as AriaDateRangePicker, + type DateRangePickerProps as AriaDateRangePickerProps, + type DateValue, + Group, + type ValidationResult +} from 'react-aria-components/DateRangePicker'; +import {DateInput, DateSegment} from './DateField'; +import {Description, FieldButton} from './Form'; +import {Popover} from './Popover'; +import {Label, FieldError} from './Form'; +import {RangeCalendar} from './RangeCalendar'; +import {ChevronDown} from 'lucide-react'; +import './DateRangePicker.css'; + +export interface DateRangePickerProps extends AriaDateRangePickerProps { + label?: string; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); +} + +export function DateRangePicker({ + label, + description, + errorMessage, + ...props +}: DateRangePickerProps) { + return ( + + + +
+ {segment => } + + {segment => } +
+ + + +
+ {description && {description}} + {errorMessage} + + + +
+ ); +} diff --git a/packages/excel-collab/src/component/Dialog.css b/packages/excel-collab/src/component/Dialog.css new file mode 100644 index 00000000..46cac21a --- /dev/null +++ b/packages/excel-collab/src/component/Dialog.css @@ -0,0 +1,14 @@ + + +.react-aria-Dialog { + outline: none; + max-height: inherit; + box-sizing: border-box; + overflow: auto; + padding: var(--spacing-10); + + .react-aria-Heading[slot='title'] { + margin-top: 0; + margin-bottom: 1em; + } +} diff --git a/packages/excel-collab/src/component/Dialog.tsx b/packages/excel-collab/src/component/Dialog.tsx new file mode 100644 index 00000000..a11773d8 --- /dev/null +++ b/packages/excel-collab/src/component/Dialog.tsx @@ -0,0 +1,19 @@ +'use client'; +import { + Dialog as RACDialog, + DialogTrigger as RACDialogTrigger, + Heading, + type DialogProps, + type DialogTriggerProps +} from 'react-aria-components/Dialog'; +import './Dialog.css'; + +export function Dialog(props: DialogProps) { + return ; +} + +export function DialogTrigger(props: DialogTriggerProps) { + return ; +} + +export {Heading}; diff --git a/packages/excel-collab/src/component/Disclosure.css b/packages/excel-collab/src/component/Disclosure.css new file mode 100644 index 00000000..a5b8b1f1 --- /dev/null +++ b/packages/excel-collab/src/component/Disclosure.css @@ -0,0 +1,77 @@ + + +.react-aria-Disclosure { + width: calc(var(--spacing) * 50); + + .react-aria-Heading { + margin: 0; + } + + .disclosure-button { + background: none; + border: none; + box-shadow: none; + text-shadow: none; + width: 100%; + color: var(--text-color); + font: var(--font-size) system-ui; + font-weight: 600; + display: flex; + align-items: center; + text-align: start; + gap: var(--spacing-1); + padding: var(--spacing-2) var(--spacing-3); + border-radius: var(--radius); + transition: + background 200ms, + color 200ms, + scale 200ms; + outline: none; + -webkit-tap-highlight-color: transparent; + + &[data-hovered], + &[data-pressed] { + background: var(--gray-300); + color: var(--text-color-hover); + } + + &[data-pressed] { + scale: 0.97; + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + } + + &[data-disabled] { + color: var(--text-color-disabled); + } + + svg { + rotate: 0deg; + transition: rotate 200ms; + fill: none; + stroke: currentColor; + stroke-width: 3px; + } + } + + &[data-expanded] .disclosure-button svg { + rotate: 90deg; + } +} + +.react-aria-DisclosurePanel { + color: var(--text-color); + height: var(--disclosure-panel-height); + transition: height 250ms; + overflow: clip; + + @media (prefers-reduced-motion: reduce) { + transition: none; + } + + div { + padding: var(--spacing-2) var(--spacing-4); + } +} diff --git a/packages/excel-collab/src/component/Disclosure.tsx b/packages/excel-collab/src/component/Disclosure.tsx new file mode 100644 index 00000000..9805b0a9 --- /dev/null +++ b/packages/excel-collab/src/component/Disclosure.tsx @@ -0,0 +1,35 @@ +'use client'; +import {Button} from 'react-aria-components/Button'; +import { + Disclosure as AriaDisclosure, + DisclosurePanel as AriaDisclosurePanel, + type DisclosureProps, + type DisclosurePanelProps, + type HeadingProps +} from 'react-aria-components/Disclosure'; +import {Heading} from './Content'; +import {ChevronRight} from 'lucide-react'; +import './Disclosure.css'; + +export function Disclosure(props: DisclosureProps) { + return ; +} + +export function DisclosureHeader({children, ...props}: HeadingProps) { + return ( + + + + ); +} + +export function DisclosurePanel(props: DisclosurePanelProps) { + return ( + +
{props.children}
+
+ ); +} diff --git a/packages/excel-collab/src/component/DisclosureGroup.css b/packages/excel-collab/src/component/DisclosureGroup.css new file mode 100644 index 00000000..87faccab --- /dev/null +++ b/packages/excel-collab/src/component/DisclosureGroup.css @@ -0,0 +1,5 @@ + + +.react-aria-DisclosureGroup { + width: 200px; +} diff --git a/packages/excel-collab/src/component/DisclosureGroup.tsx b/packages/excel-collab/src/component/DisclosureGroup.tsx new file mode 100644 index 00000000..53faf484 --- /dev/null +++ b/packages/excel-collab/src/component/DisclosureGroup.tsx @@ -0,0 +1,10 @@ +'use client'; +import { + DisclosureGroup as RACDisclosureGroup, + type DisclosureGroupProps +} from 'react-aria-components/DisclosureGroup'; +import './DisclosureGroup.css'; + +export function DisclosureGroup(props: DisclosureGroupProps) { + return ; +} diff --git a/packages/excel-collab/src/component/DropZone.css b/packages/excel-collab/src/component/DropZone.css new file mode 100644 index 00000000..ff4076e4 --- /dev/null +++ b/packages/excel-collab/src/component/DropZone.css @@ -0,0 +1,33 @@ + + +.react-aria-DropZone { + color: var(--text-color); + background: var(--overlay-background); + border: 1px solid var(--border-color); + forced-color-adjust: none; + border-radius: var(--radius); + appearance: none; + vertical-align: middle; + font-size: 1rem; + line-height: 1.5; + text-align: center; + margin: 0; + outline: none; + padding: 24px 12px; + width: 30%; + min-height: 96px; + display: flex; + align-items: center; + justify-content: center; + text-wrap: balance; + + &[data-focus-visible], + &[data-drop-target] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + + &[data-drop-target] { + background: var(--highlight-overlay); + } +} diff --git a/packages/excel-collab/src/component/DropZone.tsx b/packages/excel-collab/src/component/DropZone.tsx new file mode 100644 index 00000000..9a5b9f00 --- /dev/null +++ b/packages/excel-collab/src/component/DropZone.tsx @@ -0,0 +1,9 @@ +'use client'; +import {type DropZoneProps, DropZone as RACDropZone, Text} from 'react-aria-components/DropZone'; +import './DropZone.css'; + +export function DropZone(props: DropZoneProps) { + return ; +} + +export {Text}; diff --git a/packages/excel-collab/src/component/Form.css b/packages/excel-collab/src/component/Form.css new file mode 100644 index 00000000..df98bba0 --- /dev/null +++ b/packages/excel-collab/src/component/Form.css @@ -0,0 +1,108 @@ + + +.react-aria-Form { + display: flex; + flex-direction: column; + gap: var(--spacing-6); + + > .react-aria-Button { + align-self: start; + } +} + +.react-aria-Form [role='alert'] { + border: 2px solid var(--invalid-color); + background: var(--overlay-background); + border-radius: 6px; + padding: 12px; + max-width: 250px; + outline: none; + + &:focus { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + h3 { + margin-top: 0; + } + + p { + margin-bottom: 0; + } +} + +.react-aria-Label { + display: block; + font: var(--font-size) system-ui; + color: var(--text-color); + margin-bottom: var(--spacing-2); + font-weight: 500; + cursor: default; +} + +.react-aria-FieldError { + display: block; + font: var(--font-size-sm) system-ui; + color: var(--invalid-color); + margin-top: var(--spacing-1); + contain: inline-size; +} + +.field-description { + display: block; + font: var(--font-size-sm) system-ui; + margin-top: var(--spacing-1); + contain: inline-size; + + [data-invalid] > & { + display: none; + } +} + +.field-Button { + background: var(--tint-200); + color: var(--tint-1400); + forced-color-adjust: none; + border-radius: var(--radius-sm); + border: none; + margin-left: calc(-1 * var(--spacing-8) + 4px); + height: calc(100% - 8px); + aspect-ratio: 1/1; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + transition-property: scale, background; + transition-duration: 200ms; + -webkit-tap-highlight-color: transparent; + + svg { + width: var(--spacing-4); + height: var(--spacing-4); + } + + &:where([data-hovered], [data-pressed]) { + background: var(--tint-300); + color: var(--tint-1200); + } + + @media (forced-colors: active) { + background: Highlight; + color: HighlightText; + } + + &[data-pressed] { + scale: 0.9; + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + &[data-disabled] { + background: none; + color: var(--text-color-disabled); + } +} diff --git a/packages/excel-collab/src/component/Form.tsx b/packages/excel-collab/src/component/Form.tsx new file mode 100644 index 00000000..98b3373a --- /dev/null +++ b/packages/excel-collab/src/component/Form.tsx @@ -0,0 +1,28 @@ +'use client'; +import {Form as RACForm, type FormProps} from 'react-aria-components/Form'; +import {type LabelProps, Label as RACLabel} from 'react-aria-components/Label'; +import {type FieldErrorProps, FieldError as RACFieldError} from 'react-aria-components/FieldError'; +import {type ButtonProps, Button} from 'react-aria-components/Button'; +import {type TextProps} from 'react-aria-components/Text'; +import './Form.css'; +import {Text} from './Content'; + +export function Form(props: FormProps) { + return ; +} + +export function Label(props: LabelProps) { + return ; +} + +export function FieldError(props: FieldErrorProps) { + return ; +} + +export function Description(props: TextProps) { + return ; +} + +export function FieldButton(props: ButtonProps) { + return + )} + {selectionMode === 'multiple' && selectionBehavior === 'toggle' && ( + + )} + {children} + + )} + + ); +} + +export function GridListLoadMoreItem(props: GridListLoadMoreItemProps) { + return ( + + + + ); +} + +export {GridListSection, GridListHeader, Text}; diff --git a/packages/excel-collab/src/component/InputGroup.css b/packages/excel-collab/src/component/InputGroup.css new file mode 100644 index 00000000..455ec228 --- /dev/null +++ b/packages/excel-collab/src/component/InputGroup.css @@ -0,0 +1,48 @@ + +@import './utilities.css'; + +.input-group { + .react-aria-Group { + display: flex; + align-items: center; + width: fit-content; + padding: 0 var(--spacing-2); + border-radius: var(--radius); + font-size: var(--font-size); + color: var(--field-text-color); + overflow: hidden; + height: 32px; + + &[data-focus-within] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + + &[data-disabled] { + color: var(--text-color-disabled); + } + } + + .react-aria-Input { + padding: var(--spacing-1); + margin: 0; + font: var(--font-size) system-ui; + color: var(--text-color); + outline: none; + border: none; + background: transparent; + box-shadow: none; + + &:disabled { + color: var(--text-color-disabled); + &::placeholder { + color: var(--text-color-disabled); + } + } + + &::placeholder { + color: var(--text-color-placeholder); + opacity: 1; + } + } +} diff --git a/packages/excel-collab/src/component/InputGroup.tsx b/packages/excel-collab/src/component/InputGroup.tsx new file mode 100644 index 00000000..02838e99 --- /dev/null +++ b/packages/excel-collab/src/component/InputGroup.tsx @@ -0,0 +1,31 @@ +'use client'; +import {composeRenderProps} from 'react-aria-components/composeRenderProps'; +import {Group, type GroupProps} from 'react-aria-components/Group'; +import {InputContext} from 'react-aria-components/Input'; +import {Label} from './Form'; +import {useId} from 'react'; +import './InputGroup.css'; + +interface InputGroupProps extends GroupProps { + label?: string; +} + +export function InputGroup(props: InputGroupProps) { + let id = useId(); + return ( +
+ {props.label && ( + + )} + + {composeRenderProps(props.children, (children, renderProps) => ( + + {children} + + ))} + +
+ ); +} diff --git a/packages/excel-collab/src/component/Link.css b/packages/excel-collab/src/component/Link.css new file mode 100644 index 00000000..9c1d8784 --- /dev/null +++ b/packages/excel-collab/src/component/Link.css @@ -0,0 +1,31 @@ + + +.react-aria-Link { + color: var(--link-color); + font-size: var(--font-size); + transition-property: color, text-decoration-thickness; + transition-duration: 200ms; + text-decoration: underline; + cursor: pointer; + outline: none; + position: relative; + border-radius: var(--radius-sm); + -webkit-tap-highlight-color: transparent; + + &[data-hovered] { + text-decoration-thickness: 1.5px; + } + + &[data-pressed] { + color: var(--link-color-pressed); + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + } + + &[data-disabled] { + cursor: default; + color: var(--text-color-disabled); + } +} diff --git a/packages/excel-collab/src/component/Link.tsx b/packages/excel-collab/src/component/Link.tsx new file mode 100644 index 00000000..d6517041 --- /dev/null +++ b/packages/excel-collab/src/component/Link.tsx @@ -0,0 +1,7 @@ +'use client'; +import {Link as RACLink, type LinkProps} from 'react-aria-components/Link'; +import './Link.css'; + +export function Link(props: LinkProps) { + return ; +} diff --git a/packages/excel-collab/src/component/ListBox.css b/packages/excel-collab/src/component/ListBox.css new file mode 100644 index 00000000..c5c4020b --- /dev/null +++ b/packages/excel-collab/src/component/ListBox.css @@ -0,0 +1,354 @@ + + +.react-aria-ListBox { + max-height: inherit; + overflow: auto; + border: 0.5px solid var(--border-color); + border-radius: calc(var(--radius) + var(--spacing-1)); + background: var(--overlay-background); + forced-color-adjust: none; + outline: none; + box-sizing: border-box; + font: var(--font-size) system-ui; + color: var(--text-color); + + &:has(.react-aria-ListBoxSection) { + scroll-padding: 24px; + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + + &[data-empty] { + align-items: center; + justify-content: center; + font-style: italic; + } + + &[data-drop-target] { + outline: 2px solid var(--highlight-background); + outline-offset: -1px; + background: var(--highlight-overlay); + } + + .react-aria-DropIndicator[data-drop-target] { + z-index: 3; + outline: 1px solid var(--highlight-background); + } +} + +.react-aria-ListBoxSection { + .react-aria-Header { + position: sticky; + top: -0.5px; + z-index: 2; + font-size: var(--font-size-sm); + font-weight: 600; + font-variation-settings: initial; + padding: var(--spacing-1) var(--spacing-4); + background: var(--gray-100); + border-block: 0.5px solid var(--gray-400); + cursor: default; + user-select: none; + box-shadow: + inset 0px 1px 0px white, + inset 0px -4px 8px var(--gray-200); + + @media (prefers-color-scheme: dark) { + box-shadow: inset 0px 4px 8px var(--gray-200); + } + } + + &:first-child .react-aria-Header { + margin-top: -0.5px; + } +} + +.react-aria-ListBoxItem { + padding: var(--spacing-1) var(--spacing-3); + border-radius: var(--radius); + outline: none; + cursor: default; + color: var(--text-color); + font: var(--font-size) system-ui; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + min-height: var(--spacing-8); + box-sizing: border-box; + position: relative; + transition-property: background, color, border-radius; + transition-duration: 200ms; + -webkit-tap-highlight-color: transparent; + + --border-color: var(--gray-300); + &:not(:last-child)::after { + content: ''; + display: block; + position: absolute; + } + + &[data-pressed] { + background: var(--highlight-hover); + border-radius: var(--radius); + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + z-index: 2; + } + + &[data-selected] { + background: var(--highlight-background); + color: var(--highlight-foreground); + --border-color: transparent; + + &[data-focus-visible] { + outline-color: var(--highlight-foreground); + outline-offset: -4px; + } + + &[data-pressed] { + background: var(--highlight-background-pressed); + } + + &:has(+ [data-selected]), + &:has(+ .react-aria-DropIndicator + [data-selected]) { + --border-color: rgb(255 255 255 / 0.3); + } + } + + [slot='label'] { + font-weight: 500; + } + + [slot='description'] { + font-size: var(--font-size-sm); + } + + &[href] { + text-decoration: none; + cursor: pointer; + -webkit-touch-callout: none; + } + + &[data-disabled] { + color: var(--text-color-disabled); + } + + &[data-dragging] { + opacity: 0.6; + } + + &[data-drop-target] { + outline: 2px solid var(--highlight-background); + background: var(--highlight-overlay); + } +} + +.react-aria-ListBox { + &[data-layout='stack'] { + display: flex; + + &[data-orientation='vertical'] { + flex-direction: column; + width: 250px; + max-height: 300px; + min-height: 100px; + + .react-aria-ListBoxItem { + margin-inline: var(--spacing-1); + + &:where(:nth-child(1 of .react-aria-ListBoxItem)) { + margin-top: var(--spacing); + } + + &:where(:nth-last-child(1 of .react-aria-ListBoxItem)) { + margin-bottom: var(--spacing); + } + + [role='presentation'] > & { + margin: 0; /* virtualizer */ + } + + &:not(:last-child)::after { + bottom: 0; + width: calc(100% - var(--spacing-6)); + border-bottom: 0.5px solid var(--border-color); + } + + &[data-selected] { + &:has(+ [data-selected]), + &:has(+ .react-aria-DropIndicator + [data-selected]) { + border-end-start-radius: 0; + border-end-end-radius: 0; + } + + + [data-selected], + + .react-aria-DropIndicator + [data-selected] { + border-start-start-radius: 0; + border-start-end-radius: 0; + } + } + } + } + + &[data-orientation='horizontal'] { + flex-direction: row; + width: 100%; + + .react-aria-ListBoxItem { + min-width: max-content; + margin-block: var(--spacing-1); + + &:nth-child(1 of .react-aria-ListBoxItem) { + margin-inline-start: var(--spacing); + } + + &:nth-last-child(1 of .react-aria-ListBoxItem) { + margin-inline-end: var(--spacing); + } + + &:not(:last-child)::after { + inset-inline-end: 0; + height: calc(100% - var(--spacing-2)); + border-inline-end: 0.5px solid var(--border-color); + } + + &[data-selected] { + &:has(+ [data-selected]), + &:has(+ .react-aria-DropIndicator + [data-selected]) { + border-start-end-radius: 0; + border-end-end-radius: 0; + } + + + [data-selected], + + .react-aria-DropIndicator + [data-selected] { + border-start-start-radius: 0; + border-end-start-radius: 0; + } + } + } + } + } + + &[data-layout='grid'] { + display: grid; + gap: var(--spacing-1); + width: fit-content; + max-width: 100%; + padding: var(--spacing-1); + + &[data-orientation='vertical'] { + grid-template-columns: 1fr 1fr; + } + + &[data-orientation='horizontal'] { + grid-auto-flow: column; + grid-template-rows: auto auto; + grid-template-columns: none; + grid-auto-columns: 160px; + } + } +} + +.react-aria-ListBoxLoadingIndicator { + display: flex; + align-items: center; + justify-content: center; + height: 24px; + width: 100%; +} + +.dropdown-listbox { + display: block; + max-height: inherit; + overflow: auto; + border-radius: inherit; + forced-color-adjust: none; + outline: none; + box-sizing: border-box; + font: var(--font-size) system-ui; + color: var(--text-color); + + &[data-empty] { + min-height: var(--spacing-8); + padding: var(--spacing-2); + } + + .react-aria-Header { + padding-left: var(--spacing-7); + } +} + +.dropdown-item { + padding: calc((var(--spacing-8) - 1lh) / 2) var(--spacing-3) calc((var(--spacing-8) - 1lh) / 2) + var(--spacing-6); + box-sizing: border-box; + border-radius: var(--radius); + outline: none; + cursor: default; + color: var(--text-color); + font: var(--font-size) system-ui; + font-weight: 500; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + min-height: var(--spacing-8); + margin-inline: var(--spacing-1); + -webkit-tap-highlight-color: transparent; + + &:first-of-type { + margin-top: var(--spacing); + } + + &:last-of-type { + margin-bottom: var(--spacing); + } + + &[data-selected] { + font-weight: 500; + color: var(--text-color); + + .lucide-check { + position: absolute; + top: 50%; + left: var(--spacing-1); + translate: 0 -50%; + stroke-width: 3px; + width: var(--spacing-4); + height: var(--spacing-4); + } + } + + &[data-pressed] { + background: var(--highlight-hover); + } + + &[data-focused] { + background: var(--highlight-background); + color: var(--highlight-foreground); + } + + [slot='label'] { + font-weight: 500; + } + + [slot='description'] { + font-size: var(--font-size-sm); + } + + &[data-disabled] { + color: var(--text-color-disabled); + } + + &[href] { + text-decoration: none; + cursor: pointer; + } +} diff --git a/packages/excel-collab/src/component/ListBox.tsx b/packages/excel-collab/src/component/ListBox.tsx new file mode 100644 index 00000000..8e004586 --- /dev/null +++ b/packages/excel-collab/src/component/ListBox.tsx @@ -0,0 +1,66 @@ +'use client'; +import { + ListBox as AriaListBox, + ListBoxItem as AriaListBoxItem, + ListBoxSection as AriaListBoxSection, + ListBoxLoadMoreItem as AriaListBoxLoadMoreItem, + Header, + type ListBoxItemProps, + type ListBoxLoadMoreItemProps, + type ListBoxProps, + type ListBoxSectionProps +} from 'react-aria-components/ListBox'; +import {composeRenderProps} from 'react-aria-components/composeRenderProps'; +import {Check} from 'lucide-react'; +import {Text} from './Content'; +import {ProgressCircle} from './ProgressCircle'; +import './ListBox.css'; + +export function ListBox({children, ...props}: ListBoxProps) { + return {children}; +} + +export function ListBoxItem(props: ListBoxItemProps) { + let textValue = + props.textValue || (typeof props.children === 'string' ? props.children : undefined); + return ( + + {composeRenderProps(props.children, children => + typeof children === 'string' ? {children} : children + )} + + ); +} + +export function ListBoxSection(props: ListBoxSectionProps) { + return ; +} + +export function ListBoxLoadMoreItem(props: ListBoxLoadMoreItemProps) { + return ( + + + + ); +} + +export function DropdownListBox(props: ListBoxProps) { + return ; +} + +export function DropdownItem(props: ListBoxItemProps) { + let textValue = + props.textValue || (typeof props.children === 'string' ? props.children : undefined); + return ( + + {composeRenderProps(props.children, (children, {isSelected}) => ( + <> + {isSelected && } + {typeof children === 'string' ? {children} : children} + + ))} + + ); +} + +export {Text, Header}; diff --git a/packages/excel-collab/src/component/Menu.css b/packages/excel-collab/src/component/Menu.css new file mode 100644 index 00000000..950e96e5 --- /dev/null +++ b/packages/excel-collab/src/component/Menu.css @@ -0,0 +1,203 @@ + + +.react-aria-Menu { + margin: 0; + padding: 0; + list-style: none; + min-height: 0; + max-height: inherit; + box-sizing: border-box; + overflow: auto; + min-width: 150px; + box-sizing: border-box; + outline: none; + border-radius: inherit; + display: grid; + --check-width: 0; + grid-template-columns: var(--spacing-3) var(--check-width) auto 1fr auto var(--spacing-3); + grid-auto-rows: max-content; + + &:has(> [data-selection-mode]) { + --check-width: var(--spacing-6); + } + + &[data-empty] { + display: flex; + align-items: center; + justify-content: center; + font-style: italic; + min-height: var(--spacing-8); + } +} + +.react-aria-MenuLoadingIndicator { + display: flex; + align-items: center; + justify-content: center; + height: 24px; + grid-column: 1 / -1; +} + +.react-aria-MenuItem { + margin-inline: var(--spacing-1); + padding: calc((var(--spacing-8) - 1lh) / 2) 0; + border-radius: calc(var(--radius-lg) - var(--spacing-1)); + outline: none; + cursor: default; + color: var(--text-color); + font: var(--font-size) system-ui; + position: relative; + display: grid; + grid-column-start: 1; + grid-column-end: -1; + grid-template-areas: + '. check icon label end .' + '. . . desc end .'; + grid-template-columns: subgrid; + align-items: center; + forced-color-adjust: none; + -webkit-tap-highlight-color: transparent; + + > svg:not(.lucide-check, .lucide-dot, .lucide-chevron-right) { + grid-area: icon; + width: var(--spacing-4); + height: var(--spacing-4); + justify-self: center; + margin-inline-end: var(--spacing-2); + } + + &:first-of-type { + margin-top: var(--spacing); + } + + &:last-of-type { + margin-bottom: var(--spacing); + } + + &[data-open], + &[data-pressed] { + background: var(--highlight-hover); + } + + &[data-focused] { + background: var(--highlight-background); + color: var(--highlight-foreground); + } + + &[data-selection-mode] { + .lucide-check, + .lucide-dot { + grid-area: check; + width: var(--spacing-4); + height: var(--spacing-4); + } + + .lucide-check { + stroke-width: 3px; + } + + .lucide-dot { + scale: 3; + } + } + + &[href] { + text-decoration: none; + cursor: pointer; + } + + &[data-disabled] { + color: var(--text-color-disabled); + } + + .react-aria-Text:not([slot]), + [slot='label'] { + grid-area: label; + font-weight: 500; + } + + [slot='description'] { + font-size: var(--font-size-sm); + grid-area: desc; + } + + kbd { + grid-area: end; + justify-self: end; + margin-inline-start: var(--spacing-4); + font-family: system-ui; + font-size: var(--font-size-sm); + text-align: end; + background: var(--highlight-hover); + border: 0.5px solid var(--highlight-pressed); + padding: 0 var(--spacing-1); + border-radius: var(--radius-sm); + } + + &[data-focused] kbd { + background: rgb(255 255 255 / 0.1); + border-color: rgb(255 255 255 / 0.2); + } + + .lucide-chevron-right { + grid-area: end; + margin-left: var(--spacing-1); + justify-self: end; + width: var(--spacing-4); + height: var(--spacing-4); + } +} + +.react-aria-MenuSection { + display: grid; + grid-column-start: 1; + grid-column-end: -1; + --check-width: 0; + grid-template-columns: var(--spacing-3) var(--check-width) auto 1fr auto var(--spacing-3); + + &:has(> [data-selection-mode]) { + --check-width: var(--spacing-6); + } + + .react-aria-Header { + grid-column-start: 1; + grid-column-end: -1; + font-size: var(--font-size-sm); + font-weight: 600; + font-variation-settings: initial; + padding: var(--spacing-1) var(--spacing-3); + background: var(--gray-100); + border-block: 0.5px solid var(--gray-400); + cursor: default; + user-select: none; + box-shadow: + inset 0px 1px 0px white, + inset 0px -4px 8px var(--gray-200); + + @media (prefers-color-scheme: dark) { + box-shadow: inset 0px 4px 8px var(--gray-200); + } + } + + &:first-child .react-aria-Header { + margin-top: -0.5px; + } +} + +.react-aria-Menu .react-aria-Separator { + grid-column-start: 1; + grid-column-end: -1; + margin: 0 var(--spacing-2); + border: 0; + border-top: 0.5px solid var(--border-color); + height: auto; + width: auto; + + .react-aria-MenuItem + & { + margin-block-start: var(--spacing-1); + } + + &:has(+ .react-aria-MenuItem) { + margin-block-end: var(--spacing-1); + } +} diff --git a/packages/excel-collab/src/component/Menu.tsx b/packages/excel-collab/src/component/Menu.tsx new file mode 100644 index 00000000..7f451f7d --- /dev/null +++ b/packages/excel-collab/src/component/Menu.tsx @@ -0,0 +1,94 @@ +'use client'; +import { Check, ChevronRight, Dot } from 'lucide-react'; +import { + Menu as AriaMenu, + MenuItem as AriaMenuItem, + MenuLoadMoreItem as AriaMenuLoadMoreItem, + MenuSection as AriaMenuSection, + MenuTrigger as AriaMenuTrigger, + SubmenuTrigger as AriaSubmenuTrigger, + Header, + Separator, + Keyboard, + type MenuItemProps, + type MenuLoadMoreItemProps, + type MenuProps, + type MenuSectionProps, + type MenuTriggerProps, + type SubmenuTriggerProps, +} from 'react-aria-components/Menu'; +import { Popover } from './Popover'; +import { ProgressCircle } from './ProgressCircle'; +import { Text } from './Content'; +import React from 'react'; +import './Menu.css'; + +export function MenuTrigger(props: MenuTriggerProps) { + let [trigger, menu] = React.Children.toArray(props.children) as [ + React.ReactElement, + React.ReactElement, + ]; + return ( + + {trigger} + {menu} + + ); +} + +export function Menu(props: MenuProps) { + return {props.children}; +} + +export function MenuLoadMoreItem(props: MenuLoadMoreItemProps) { + return ( + + + + ); +} + +export function MenuItem( + props: Omit & { + children?: React.ReactNode; + textValue: string; + }, +) { + return ( + + {({ hasSubmenu, isSelected, selectionMode }) => ( + <> + {isSelected && selectionMode === 'multiple' ? : null} + {isSelected && selectionMode === 'single' ? : null} + {typeof props.children === 'string' ? ( + {props.children} + ) : ( + props.children + )} + {hasSubmenu && } + + )} + + ); +} + +export function MenuSection(props: MenuSectionProps) { + return ; +} + +export function SubmenuTrigger(props: SubmenuTriggerProps) { + let [trigger, menu] = React.Children.toArray(props.children) as [ + React.ReactElement, + React.ReactElement, + ]; + return ( + + {trigger} + + {menu} + + + ); +} + +export { Text, Header, Separator, Keyboard }; diff --git a/packages/excel-collab/src/component/Meter.css b/packages/excel-collab/src/component/Meter.css new file mode 100644 index 00000000..bdfada6f --- /dev/null +++ b/packages/excel-collab/src/component/Meter.css @@ -0,0 +1,42 @@ + +@import './utilities.css'; + +.react-aria-Meter { + display: grid; + grid-template-areas: + 'label value' + 'bar bar'; + grid-template-columns: 1fr auto; + width: 250px; + color: var(--text-color); + font: var(--font-size) system-ui; + + .value { + grid-area: value; + } + + .track { + grid-area: bar; + forced-color-adjust: none; + height: calc(var(--spacing-2) + 2px); + border-radius: 9999px; + overflow: hidden; + } + + .fill { + --fill-color: var(--green); + background: oklch(from var(--fill-color) var(--lightness-700) c h); + height: calc(100% - 2px); + margin: 1px 0 0 1px; + border-radius: inherit; + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.4), + 0 1px 0 lch(from var(--fill-color) 42% c h), + 0 0 0 1px var(--fill-color); + + @media (forced-colors: active) { + box-shadow: none; + background: Highlight; + } + } +} diff --git a/packages/excel-collab/src/component/Meter.tsx b/packages/excel-collab/src/component/Meter.tsx new file mode 100644 index 00000000..31db8dca --- /dev/null +++ b/packages/excel-collab/src/component/Meter.tsx @@ -0,0 +1,37 @@ +'use client'; +import {Meter as AriaMeter, type MeterProps as AriaMeterProps} from 'react-aria-components/Meter'; +import {Label} from './Form'; +import './Meter.css'; + +export interface MeterProps extends AriaMeterProps { + label?: string; +} + +export function Meter({label, ...props}: MeterProps) { + return ( + + {({percentage, valueText}) => ( + <> + + {valueText} +
+
+
+ + )} + + ); +} diff --git a/packages/excel-collab/src/component/Modal.css b/packages/excel-collab/src/component/Modal.css new file mode 100644 index 00000000..819d03a0 --- /dev/null +++ b/packages/excel-collab/src/component/Modal.css @@ -0,0 +1,65 @@ + + +.react-aria-ModalOverlay { + position: absolute; + top: 0; + left: 0; + width: var(--page-width); + height: var(--page-height); + background: rgba(0 0 0 / 0.5); + overflow: clip; + z-index: 100; + font-family: system-ui; + font-size: var(--font-size); + + &[data-entering] { + animation: modal-fade 200ms; + } + + &[data-exiting] { + animation: modal-fade 150ms reverse ease-in; + } +} + +.react-aria-Modal { + position: sticky; + max-height: calc(var(--visual-viewport-height) * 0.9); + top: calc(var(--visual-viewport-height) / 2); + margin-left: 50vw; + translate: -50% -50%; + box-shadow: 0 8px 32px rgba(0 0 0 / 0.2); + border-radius: var(--radius-xl); + background: var(--overlay-background); + color: var(--text-color); + outline: 1px solid var(--overlay-border); + width: max-content; + max-width: min(500px, 90vw); + + &[data-entering] { + animation: modal-zoom 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275); + } + + .react-aria-TextField { + margin-bottom: 8px; + } +} + +@keyframes modal-fade { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes modal-zoom { + from { + transform: scale(0.8); + } + + to { + transform: scale(1); + } +} diff --git a/packages/excel-collab/src/component/Modal.tsx b/packages/excel-collab/src/component/Modal.tsx new file mode 100644 index 00000000..02f86c78 --- /dev/null +++ b/packages/excel-collab/src/component/Modal.tsx @@ -0,0 +1,16 @@ +'use client'; +import { + Modal as RACModal, + ModalOverlay, + type ModalOverlayProps, +} from 'react-aria-components/Modal'; +import './Modal.css'; + +export function Modal(props: ModalOverlayProps) { + const { children, ...overlayProps } = props; + return ( + + {children} + + ); +} diff --git a/packages/excel-collab/src/component/NavigationTree.css b/packages/excel-collab/src/component/NavigationTree.css new file mode 100644 index 00000000..509a9f9a --- /dev/null +++ b/packages/excel-collab/src/component/NavigationTree.css @@ -0,0 +1,148 @@ + + +.react-aria-NavigationTree { + display: flex; + flex-direction: column; + overflow: auto; + padding: var(--spacing-1); + border: 0.5px solid var(--border-color); + border-radius: calc(var(--radius) + var(--spacing-1)); + background: var(--overlay-background); + forced-color-adjust: none; + outline: none; + width: 250px; + max-height: 300px; + box-sizing: border-box; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + + .react-aria-NavigationTreeSection:not(:first-child) { + margin-top: var(--spacing-4); + } + + .react-aria-NavigationTreeHeader { + font-size: var(--font-size-sm); + font-weight: 600; + padding: var(--spacing-1) var(--spacing-2); + color: var(--text-color); + } +} + +.react-aria-NavigationTreeItem { + --padding: var(--spacing-4); + display: flex; + position: relative; + align-items: center; + min-height: var(--spacing-8); + border-radius: var(--radius); + box-sizing: border-box; + outline: none; + color: var(--text-color); + font: var(--font-size) system-ui; + -webkit-tap-highlight-color: transparent; + /* Indent each nested level. --tree-item-level is set on the row by React Aria (1-based). */ + padding-inline-start: calc((var(--tree-item-level, 1) - 1) * var(--padding)); + + .react-aria-Link:before { + display: none; + content: ''; + position: absolute; + inset-inline-start: 2px; + top: 50%; + width: 2px; + transform: translateY(-50%); + height: 1lh; + background: var(--text-color-hover); + font-weight: 600; + border-radius: 9999px; + } + + a.react-aria-Link[data-hovered]:before { + display: block; + } + + /* When a current-route ancestor is collapsed, tint it and show a small dot where the pill would be. + * The dot (rather than the full pill) signals "the current route is nested inside here" without + * looking selected, and gives a non-color affordance. Higher specificity than the hover rule above, + * so the dot wins over the hover pill on these rows. Targets .react-aria-Link so it works whether the + * row renders as a link or a plain span. */ + &[data-current-ancestor]:not([data-expanded]) { + .react-aria-Link:before { + display: block; + height: 4px; + width: 4px; + } + } + + /* React Aria sets data-current on the link matching the NavigationTree's selectedRoute. */ + &[data-current] .react-aria-Link:before { + display: block; + background: var(--focus-ring-color); + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } + + &[data-disabled] { + color: var(--text-color-disabled); + } + + .react-aria-Link { + position: relative; + flex: 1; + display: flex; + align-items: center; + gap: var(--spacing-2); + min-width: 0; + padding: var(--spacing-1) var(--spacing-2); + border-radius: var(--radius); + color: inherit; + text-decoration: none; + outline: none; + cursor: pointer; + } + + /* A row without an href renders its label as a non-interactive span. */ + &:not([data-href]) .react-aria-Link { + cursor: default; + } + + .react-aria-Button[slot='chevron'] { + all: unset; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: var(--spacing-6); + height: var(--spacing-6); + border-radius: var(--radius); + cursor: default; + -webkit-tap-highlight-color: transparent; + + svg { + width: var(--spacing-4); + height: var(--spacing-4); + rotate: 0deg; + transition: rotate 200ms; + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + } + } + + &[data-expanded] .react-aria-Button[slot='chevron'] svg { + rotate: 90deg; + } +} + +@media (forced-colors: active) { + .react-aria-NavigationTreeItem a.react-aria-Link { + color: LinkText; + } +} diff --git a/packages/excel-collab/src/component/NavigationTree.tsx b/packages/excel-collab/src/component/NavigationTree.tsx new file mode 100644 index 00000000..74d7c8c3 --- /dev/null +++ b/packages/excel-collab/src/component/NavigationTree.tsx @@ -0,0 +1,67 @@ +'use client'; +import { + Button, + Link, + type LinkProps, + NavigationTree as AriaNavigationTree, + NavigationTreeHeader as AriaNavigationTreeHeader, + type NavigationTreeHeaderProps, + NavigationTreeItem as AriaNavigationTreeItem, + NavigationTreeItemContent as AriaNavigationTreeItemContent, + type NavigationTreeItemContentRenderProps, + type NavigationTreeItemProps as AriaNavigationTreeItemProps, + type NavigationTreeProps, + NavigationTreeSection as AriaNavigationTreeSection, + type NavigationTreeSectionProps +} from 'react-aria-components/NavigationTree'; +import {ChevronRight} from 'lucide-react'; +import React from 'react'; +import './NavigationTree.css'; + +const RenderLinkContext = React.createContext(undefined); + +export function NavigationTree({ + renderLink, + ...props +}: NavigationTreeProps & {renderLink?: LinkProps['render']}) { + return ( + + + + ); +} + +export function NavigationTreeItemContent(props: {children?: React.ReactNode}) { + return ( + + {({hasChildItems}: NavigationTreeItemContentRenderProps) => ( + <> + {props.children} + + + )} + + ); +} + +export function NavigationTreeItem(props: AriaNavigationTreeItemProps) { + return ; +} + +export function NavigationTreeSection(props: NavigationTreeSectionProps) { + return ; +} + +export function NavigationTreeHeader(props: NavigationTreeHeaderProps) { + return ; +} + +export function NavigationTreeItemLink({render: renderProp, ...props}: LinkProps) { + const contextRender = React.useContext(RenderLinkContext); + return ; +} diff --git a/packages/excel-collab/src/component/NumberField.css b/packages/excel-collab/src/component/NumberField.css new file mode 100644 index 00000000..5e833312 --- /dev/null +++ b/packages/excel-collab/src/component/NumberField.css @@ -0,0 +1,69 @@ + +@import './TextField.css'; + +.react-aria-NumberField { + color: var(--text-color); + + .react-aria-Group { + display: flex; + width: fit-content; + border-radius: var(--radius); + position: relative; + box-shadow: 0 1px 0 var(--gray-50); + + &[data-invalid]::after { + content: ''; + position: absolute; + inset: 1px; + z-index: 2; + pointer-events: none; + border-radius: inherit; + outline: 1px solid var(--invalid-color); + } + + &[data-focus-within]::after { + content: ''; + position: absolute; + inset: 1px; + z-index: 2; + pointer-events: none; + border-radius: inherit; + outline: 2px solid var(--focus-ring-color); + } + } + + .react-aria-Button { + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + margin-inline-start: -1px; + + &[slot='decrement'] { + border-radius: 0; + } + + &[slot='increment'] { + border-radius: var(--radius); + border-start-start-radius: 0; + border-end-start-radius: 0; + } + } + + .react-aria-Input { + z-index: 1; + outline: none; + width: 5.25rem; + flex: 1; + border-start-end-radius: 0; + border-end-end-radius: 0; + + &[data-invalid] { + border-color: var(--border-color); + } + } + + [slot='description'] { + font-size: 12px; + } +} diff --git a/packages/excel-collab/src/component/NumberField.tsx b/packages/excel-collab/src/component/NumberField.tsx new file mode 100644 index 00000000..a1887c23 --- /dev/null +++ b/packages/excel-collab/src/component/NumberField.tsx @@ -0,0 +1,38 @@ +'use client'; +import { + Group, + Input, + NumberField as AriaNumberField, + type NumberFieldProps as AriaNumberFieldProps, + type ValidationResult +} from 'react-aria-components/NumberField'; +import {Button} from './Button'; +import {Plus, Minus} from 'lucide-react'; +import {Label, FieldError, Description} from './Form'; +import './NumberField.css'; + +export interface NumberFieldProps extends AriaNumberFieldProps { + label?: string; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); + placeholder?: string; +} + +export function NumberField({label, description, errorMessage, ...props}: NumberFieldProps) { + return ( + + + + + + + + {description && {description}} + {errorMessage} + + ); +} diff --git a/packages/excel-collab/src/component/Popover.css b/packages/excel-collab/src/component/Popover.css new file mode 100644 index 00000000..c267a142 --- /dev/null +++ b/packages/excel-collab/src/component/Popover.css @@ -0,0 +1,81 @@ + + +.react-aria-Popover { + --background-color: var(--overlay-background); + + outline: 1px solid var(--overlay-border); + box-sizing: border-box; + filter: drop-shadow(var(--popover-shadow)); + border-radius: var(--radius-lg); + background: var(--background-color); + color: var(--text-color); + transition: + transform 200ms, + opacity 200ms; + font: var(--font-size) system-ui; + padding: 8px; + overflow: auto; + + &[data-trigger='MenuTrigger'], + &[data-trigger='SubmenuTrigger'] { + padding: 0; + } + + .react-aria-OverlayArrow svg { + display: block; + fill: var(--background-color); + stroke: var(--overlay-border); + paint-order: stroke; + stroke-width: 2px; + } + + &[data-entering], + &[data-exiting] { + transform: var(--origin); + opacity: 0; + } + + &[data-placement='top'] { + --origin: translateY(8px); + + &:has(.react-aria-OverlayArrow) { + margin-bottom: 6px; + } + } + + &[data-placement='bottom'] { + --origin: translateY(-8px); + + &:has(.react-aria-OverlayArrow) { + margin-top: 6px; + } + + .react-aria-OverlayArrow svg { + transform: rotate(180deg); + } + } + + &[data-placement='right'] { + --origin: translateX(-8px); + + &:has(.react-aria-OverlayArrow) { + margin-left: 6px; + } + + .react-aria-OverlayArrow svg { + transform: rotate(90deg); + } + } + + &[data-placement='left'] { + --origin: translateX(8px); + + &:has(.react-aria-OverlayArrow) { + margin-right: 6px; + } + + .react-aria-OverlayArrow svg { + transform: rotate(-90deg); + } + } +} diff --git a/packages/excel-collab/src/component/Popover.tsx b/packages/excel-collab/src/component/Popover.tsx new file mode 100644 index 00000000..8ed52332 --- /dev/null +++ b/packages/excel-collab/src/component/Popover.tsx @@ -0,0 +1,32 @@ +'use client'; +import { + OverlayArrow, + Popover as AriaPopover, + type PopoverProps as AriaPopoverProps +} from 'react-aria-components/Popover'; +import clsx from 'clsx'; +import './Popover.css'; + +export interface PopoverProps extends Omit { + children: React.ReactNode; + hideArrow?: boolean; +} + +export function Popover({children, hideArrow, ...props}: PopoverProps) { + return ( + + {({trigger}) => ( + <> + {!hideArrow && trigger !== 'MenuTrigger' && trigger !== 'SubmenuTrigger' && ( + + + + + + )} + {children} + + )} + + ); +} diff --git a/packages/excel-collab/src/component/ProgressBar.css b/packages/excel-collab/src/component/ProgressBar.css new file mode 100644 index 00000000..d1ac3029 --- /dev/null +++ b/packages/excel-collab/src/component/ProgressBar.css @@ -0,0 +1,95 @@ + +@import './utilities.css'; + +.react-aria-ProgressBar { + display: grid; + grid-template-areas: + 'label value' + 'bar bar'; + grid-template-columns: 1fr auto; + width: 250px; + color: var(--text-color); + font: var(--font-size) system-ui; + + .value { + grid-area: value; + } + + .track { + grid-area: bar; + forced-color-adjust: none; + height: calc(var(--spacing-2) + 2px); + border-radius: 9999px; + overflow: clip; + will-change: transform; + } + + .fill { + background: linear-gradient( + to right, + oklch(from var(--tint) 58% c h) 0px, + oklch(from var(--tint) 72% c h) 20px, + oklch(from var(--tint) 58% c h) 40px + ); + background-size: 40px 100%; + height: calc(100% - 2px); + width: calc(var(--percent) - 2px); + margin: 1px 0 0 1px; + border-radius: inherit; + animation: progress-fill 1s infinite linear; + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.3), + /* top specular highlight */ inset 0 2px 2px rgb(255 255 255 / 0.2), + /* 3d effect */ 0 1px 0 lch(from var(--tint) 42% c h), + /* bottom shadow */ 0 0 0 1px var(--tint-900); /* border */ + + @media (prefers-color-scheme: dark) { + box-shadow: + 0 -1px 0 rgb(255 255 255 / 0.6), + /* top specular highlight */ inset 0 2px 2px rgb(255 255 255 / 0.2), + /* 3d effect */ 0 1px 0 lch(from var(--tint) 42% c h), + /* bottom shadow */ 0 0 0 1px var(--tint-900); /* border */ + } + + @media (prefers-reduced-motion: reduce) { + background: var(--highlight-background); + animation: none; + } + + @media (forced-colors: active) { + background-image: none; + background-color: Highlight; + box-shadow: none; + } + } + + /* indeterminate */ + &:not([aria-valuenow]) { + .fill { + --fill-dark: oklch(from var(--tint) 58% c h); + --fill-light: var(--tint-200); + background-image: linear-gradient( + 45deg, + var(--fill-dark) 0px 12px, + var(--fill-light) 15px 25px, + var(--fill-dark) 28px + ); + animation-duration: 0.5s; + + @media (forced-colors: active) { + --fill-dark: Highlight; + --fill-light: HighlightText; + } + } + } +} + +@keyframes progress-fill { + from { + background-position-x: 0; + } + + to { + background-position-x: -40px; + } +} diff --git a/packages/excel-collab/src/component/ProgressBar.tsx b/packages/excel-collab/src/component/ProgressBar.tsx new file mode 100644 index 00000000..7965b13c --- /dev/null +++ b/packages/excel-collab/src/component/ProgressBar.tsx @@ -0,0 +1,30 @@ +'use client'; +import { + ProgressBar as AriaProgressBar, + type ProgressBarProps as AriaProgressBarProps +} from 'react-aria-components/ProgressBar'; +import {Label} from './Form'; +import './ProgressBar.css'; + +export interface ProgressBarProps extends AriaProgressBarProps { + label?: string; +} + +export function ProgressBar({label, ...props}: ProgressBarProps) { + return ( + + {({percentage, valueText, isIndeterminate}) => ( + <> + + {valueText} +
+
+
+ + )} + + ); +} diff --git a/packages/excel-collab/src/component/ProgressCircle.tsx b/packages/excel-collab/src/component/ProgressCircle.tsx new file mode 100644 index 00000000..657d0d0b --- /dev/null +++ b/packages/excel-collab/src/component/ProgressCircle.tsx @@ -0,0 +1,64 @@ +'use client'; +import {composeRenderProps} from 'react-aria-components/composeRenderProps'; +import {ProgressBar} from 'react-aria-components/ProgressBar'; +import type {ProgressBarProps} from 'react-aria-components/ProgressBar'; + +export interface ProgressCircleProps extends ProgressBarProps { + size?: number; +} + +export function ProgressCircle(props: ProgressCircleProps) { + // SVG strokes are centered, so subtract half the stroke width from the radius to create an inner stroke. + let strokeWidth = 4; + let radius = `calc(50% - ${strokeWidth / 2}px)`; + + return ( + ({ + ...style, + width: props.size || 16, + height: props.size || 16 + }))}> + {({percentage, isIndeterminate}) => ( + <> + + + + {isIndeterminate && ( + + )} + + + + )} + + ); +} diff --git a/packages/excel-collab/src/component/RadioGroup.css b/packages/excel-collab/src/component/RadioGroup.css new file mode 100644 index 00000000..c28d456d --- /dev/null +++ b/packages/excel-collab/src/component/RadioGroup.css @@ -0,0 +1,86 @@ + +@import './utilities.css'; +@import './Form.css'; + +.react-aria-RadioGroup { + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--text-color); + gap: var(--spacing-1); + + .radio-items { + display: flex; + gap: var(--spacing-3); + } + + &[data-orientation='vertical'] .radio-items { + flex-direction: column; + } + + &[data-orientation='horizontal'] .radio-items { + flex-direction: row; + flex-wrap: wrap; + } +} + +.react-aria-RadioField { + display: flex; + flex-direction: column; + + [slot='description'] { + margin-left: calc(var(--spacing) * 6.5); + } + + &[data-disabled] [slot='description'] { + color: var(--text-color-disabled); + } +} + +.react-aria-RadioButton { + display: flex; + /* This is needed so the HiddenInput is positioned correctly */ + position: relative; + align-items: center; + gap: var(--spacing-2); + font: var(--font-size) system-ui; + color: var(--text-color); + forced-color-adjust: none; + -webkit-tap-highlight-color: transparent; + --dot-color: white; + @media (forced-colors: active) { + --dot-color: HighlightText; + } + + .indicator { + position: relative; + width: calc(var(--spacing) * 4.5); + height: calc(var(--spacing) * 4.5); + flex-shrink: 0; + box-sizing: border-box; + border-radius: 50%; + transition: all 200ms; + + &::after { + content: ''; + position: absolute; + inset: 1px; + border-radius: inherit; + background: transparent; + transition: scale 200ms; + scale: 0; + } + } + + &[data-selected] { + .indicator::after { + background: var(--dot-color); + scale: 0.37; + } + } + + &[data-disabled] { + color: var(--text-color-disabled); + --dot-color: var(--text-color-disabled); + } +} diff --git a/packages/excel-collab/src/component/RadioGroup.tsx b/packages/excel-collab/src/component/RadioGroup.tsx new file mode 100644 index 00000000..3d751996 --- /dev/null +++ b/packages/excel-collab/src/component/RadioGroup.tsx @@ -0,0 +1,57 @@ +'use client'; +import { + RadioGroup as AriaRadioGroup, + RadioField, + RadioButton, + type RadioGroupProps as AriaRadioGroupProps, + type ValidationResult, + type RadioFieldProps +} from 'react-aria-components/RadioGroup'; +import {composeRenderProps} from 'react-aria-components/composeRenderProps'; +import {Label, FieldError, Description} from './Form'; +import './RadioGroup.css'; +import './utilities.css'; + +export interface RadioGroupProps extends Omit { + children?: React.ReactNode; + label?: string; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); +} + +export function RadioGroup({ + label, + description, + errorMessage, + children, + ...props +}: RadioGroupProps) { + return ( + + +
{children}
+ {description && {description}} + {errorMessage} +
+ ); +} + +export interface RadioProps extends RadioFieldProps { + description?: string; +} + +export function Radio(props: RadioProps) { + return ( + + + {composeRenderProps(props.children, children => ( + <> +
+ {children} + + ))} + + {props.description && {props.description}} + + ); +} diff --git a/packages/excel-collab/src/component/RangeCalendar.css b/packages/excel-collab/src/component/RangeCalendar.css new file mode 100644 index 00000000..b3bf9fca --- /dev/null +++ b/packages/excel-collab/src/component/RangeCalendar.css @@ -0,0 +1,126 @@ + + +.react-aria-RangeCalendar { + display: flex; + flex-direction: column; + gap: var(--spacing-2); + overflow: auto; + width: fit-content; + max-width: 100%; + font: var(--font-size) system-ui; + color: var(--text-color); + + .months { + display: flex; + gap: var(--spacing-3); + } + + .month { + width: calc(var(--spacing-9) * 7); + min-width: calc(var(--spacing-7) * 7); + container-type: inline-size; + } + + header { + display: flex; + align-items: center; + margin: 0 var(--spacing-1) var(--spacing-4) var(--spacing-1); + min-height: var(--spacing-8); + + .react-aria-CalendarHeading { + flex: 1; + margin: 0; + text-align: center; + font-size: var(--font-size-lg); + } + } + + .react-aria-CalendarGrid { + border-collapse: collapse; + border-spacing: 0; + } + + [slot='errorMessage'] { + font-size: var(--font-size-sm); + color: var(--invalid-color); + } + + .react-aria-CalendarCell { + margin: 2px 0; + padding: 0 calc(var(--gap) / 2); + position: relative; + z-index: 1; + outline: none; + + span { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + border-radius: 9999px; + transition: scale 200ms; + } + + &[data-selected] { + border-radius: 0; + background: var(--button-background); + border-color: var(--tint-700); + border-style: solid; + border-width: 0; + border-top-width: 0.5px; + border-bottom-width: 0.5px; + margin: 1.5px 0; + + @media (forced-colors: active) { + border-color: Highlight; + } + + &[data-selection-start], + &:is(td:first-child > *, [aria-disabled] + td > *) { + border-start-start-radius: 9999px; + border-end-start-radius: 9999px; + border-inline-start-width: 0.5px; + margin-inline-start: calc(var(--gap) / 2 - 0.5px); + padding-inline-start: 0; + } + + &[data-selection-end], + &:is(td:last-child > *, td:has(+ [aria-disabled]) > *) { + border-end-end-radius: 9999px; + border-start-end-radius: 9999px; + border-inline-end-width: 0.5px; + margin-inline-end: calc(var(--gap) / 2 - 0.5px); + padding-inline-end: 0; + } + + &[data-selection-start], + &[data-selection-end] { + z-index: 2; + } + + &:not([data-selection-start], [data-selection-end]) span { + color: var(--tint-1200); + @media (forced-colors: active) { + color: ButtonText; + } + } + } + + &[data-pressed] { + scale: 1; + span { + scale: 0.9; + } + } + + &[data-focus-visible] { + outline: none; + z-index: 2; + span { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + } + } +} diff --git a/packages/excel-collab/src/component/RangeCalendar.tsx b/packages/excel-collab/src/component/RangeCalendar.tsx new file mode 100644 index 00000000..35098584 --- /dev/null +++ b/packages/excel-collab/src/component/RangeCalendar.tsx @@ -0,0 +1,76 @@ +'use client'; +import { + CalendarCell as AriaCalendarCell, + RangeCalendar as AriaRangeCalendar, + CalendarHeading, + Text, + type DateValue, + type RangeCalendarProps as AriaRangeCalendarProps, + type CalendarCellProps +} from 'react-aria-components/RangeCalendar'; +import {composeRenderProps} from 'react-aria-components/composeRenderProps'; +import {Button} from './Button'; +import {ChevronLeft, ChevronRight} from 'lucide-react'; +import {CalendarGrid} from './Calendar'; +import './RangeCalendar.css'; + +export interface RangeCalendarProps extends AriaRangeCalendarProps { + errorMessage?: string; +} + +export function RangeCalendar({ + errorMessage, + ...props +}: RangeCalendarProps) { + let months = props.visibleDuration?.months || 1; + return ( + +
+ {Array.from({length: months}, (_, i) => ( +
+
+ {i === 0 && ( + + )} + + {i === months - 1 && ( + + )} +
+ {date => } +
+ ))} +
+ {errorMessage && {errorMessage}} +
+ ); +} + +export {CalendarGrid}; +export function CalendarCell(props: CalendarCellProps) { + return ( + + {composeRenderProps( + props.children, + ( + children, + {defaultChildren, isHovered, isPressed, isSelectionStart, isSelectionEnd, isDisabled} + ) => ( + + {children || defaultChildren} + + ) + )} + + ); +} diff --git a/packages/excel-collab/src/component/SearchField.css b/packages/excel-collab/src/component/SearchField.css new file mode 100644 index 00000000..d46d07a7 --- /dev/null +++ b/packages/excel-collab/src/component/SearchField.css @@ -0,0 +1,75 @@ + +@import './TextField.css'; + +.react-aria-SearchField { + display: grid; + grid-template-areas: + 'label label label' + 'icon input button' + 'help help help'; + grid-template-columns: 0px 1fr auto; + align-items: center; + width: auto; + color: var(--text-color); + + .lucide-search { + grid-area: icon; + z-index: 2; + margin-inline-start: var(--spacing-2); + } + + &[data-disabled] .lucide-search { + color: var(--text-color-disabled); + } + + .react-aria-Label { + grid-area: label; + } + + .react-aria-Input { + grid-area: input; + width: 100%; + border-radius: 9999px; + padding: 0 var(--spacing-8); + + &::-webkit-search-cancel-button, + &::-webkit-search-decoration { + -webkit-appearance: none; + } + } + + .clear-button { + all: unset; + grid-area: button; + width: var(--spacing-4); + height: var(--spacing-4); + border-radius: 9999px; + margin-left: calc(-1 * var(--spacing-6)); + text-align: center; + background: var(--gray-700); + color: var(--gray-50); + border: none; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + -webkit-tap-highlight-color: transparent; + + &[data-pressed] { + background: var(--gray-800); + } + + &[data-disabled] { + background: var(--text-color-disabled); + } + } + + &[data-empty] .clear-button { + display: none; + } + + .react-aria-FieldError, + [slot='description'] { + grid-area: help; + } +} diff --git a/packages/excel-collab/src/component/SearchField.tsx b/packages/excel-collab/src/component/SearchField.tsx new file mode 100644 index 00000000..b03301ce --- /dev/null +++ b/packages/excel-collab/src/component/SearchField.tsx @@ -0,0 +1,39 @@ +'use client'; +import { + Button, + Input, + SearchField as AriaSearchField, + type SearchFieldProps as AriaSearchFieldProps, + type ValidationResult +} from 'react-aria-components/SearchField'; +import {Label, FieldError, Description} from './Form'; +import {Search, X} from 'lucide-react'; +import './SearchField.css'; + +export interface SearchFieldProps extends AriaSearchFieldProps { + label?: string; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); + placeholder?: string; +} + +export function SearchField({ + label, + description, + errorMessage, + placeholder, + ...props +}: SearchFieldProps) { + return ( + + {label && } + + + + {description && {description}} + {errorMessage} + + ); +} diff --git a/packages/excel-collab/src/component/SegmentedControl.css b/packages/excel-collab/src/component/SegmentedControl.css new file mode 100644 index 00000000..2e3f78ce --- /dev/null +++ b/packages/excel-collab/src/component/SegmentedControl.css @@ -0,0 +1,58 @@ + +@import './utilities.css'; + +.segmented-control { + display: flex; + width: fit-content; + border-radius: 9999px; + + .react-aria-SelectionIndicator { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + transition-property: translate, width; + transition-duration: 200ms; + border-radius: 9999px; + + @media (prefers-reduced-motion: reduce) { + transition: none; + } + } + + .segmented-control-item { + all: unset; + display: block; + color: var(--gray-1200); + font: var(--font-size) system-ui; + font-weight: 500; + height: var(--spacing-8); + outline: none; + padding: 0 var(--spacing-3); + position: relative; + z-index: 1; + transition: color 200ms; + -webkit-tap-highlight-color: transparent; + + span { + display: inline-block; + transition: scale 200ms; + } + + &[data-pressed] span { + scale: 0.9; + } + + &[data-selected] { + z-index: 0; + color: var(--highlight-foreground); + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: 4px; + } + } +} diff --git a/packages/excel-collab/src/component/SegmentedControl.tsx b/packages/excel-collab/src/component/SegmentedControl.tsx new file mode 100644 index 00000000..cbbe2b20 --- /dev/null +++ b/packages/excel-collab/src/component/SegmentedControl.tsx @@ -0,0 +1,33 @@ +'use client'; +import { + ToggleButtonGroup as RACToggleButtonGroup, + SelectionIndicator, + ToggleButton, + type ToggleButtonProps, + type ToggleButtonGroupProps +} from 'react-aria-components/ToggleButtonGroup'; +import {composeRenderProps} from 'react-aria-components/composeRenderProps'; +import './SegmentedControl.css'; + +export function SegmentedControl(props: ToggleButtonGroupProps) { + return ( + + ); +} + +export function SegmentedControlItem(props: ToggleButtonProps) { + return ( + + {composeRenderProps(props.children, children => ( + <> + + {children} + + ))} + + ); +} diff --git a/packages/excel-collab/src/component/Select.css b/packages/excel-collab/src/component/Select.css new file mode 100644 index 00000000..b96b0234 --- /dev/null +++ b/packages/excel-collab/src/component/Select.css @@ -0,0 +1,46 @@ + + +.react-aria-Select { + color: var(--text-color); + position: relative; + width: 200px; + + .react-aria-Button { + --button-color: var(--gray); + padding: 0 var(--spacing-2) 0 var(--spacing-3); + width: 100%; + min-width: 0; + + &[data-pressed] { + scale: 1; + } + } + + .react-aria-SelectValue { + flex: 1; + text-align: start; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &[data-placeholder] { + color: var(--text-color-placeholder); + font-weight: normal; + } + } + + .lucide-chevron-down { + margin-inline-start: var(--spacing-2); + } + + .react-aria-SelectValue { + [slot='description'] { + display: none; + } + } +} + +.select-popover[data-trigger='Select'] { + width: var(--trigger-width); + padding: 0; +} diff --git a/packages/excel-collab/src/component/Select.tsx b/packages/excel-collab/src/component/Select.tsx new file mode 100644 index 00000000..d0207f6b --- /dev/null +++ b/packages/excel-collab/src/component/Select.tsx @@ -0,0 +1,60 @@ +'use client'; +import { + type ListBoxItemProps, + Select as AriaSelect, + type SelectProps as AriaSelectProps, + SelectValue, + type ValidationResult, + type ListBoxProps, +} from 'react-aria-components/Select'; +import { Button } from './Button'; +import { DropdownItem, DropdownListBox } from './ListBox'; +import { ChevronDown } from 'lucide-react'; +import { Popover } from './Popover'; +import { Label, FieldError, Description } from './Form'; +import './Select.css'; + +export interface SelectProps extends Omit< + AriaSelectProps, + 'children' +> { + label?: string; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); + items?: Iterable; + children: React.ReactNode | ((item: T) => React.ReactNode); +} + +export function Select({ + label, + description, + errorMessage, + children, + items, + ...props +}: SelectProps) { + return ( + + {label && } + + {description && {description}} + {errorMessage} + + {children} + + + ); +} + +export function SelectListBox(props: ListBoxProps) { + return ; +} + +export function SelectItem(props: ListBoxItemProps) { + return ( + + ); +} diff --git a/packages/excel-collab/src/component/Separator.css b/packages/excel-collab/src/component/Separator.css new file mode 100644 index 00000000..78b89c68 --- /dev/null +++ b/packages/excel-collab/src/component/Separator.css @@ -0,0 +1,15 @@ + + +.react-aria-Separator { + background-color: var(--gray-500); + border-style: none; + border-radius: 1px; + height: 2px; + width: 100%; + + &[aria-orientation='vertical'] { + width: 2px; + min-height: 32px; + height: 100%; + } +} diff --git a/packages/excel-collab/src/component/Separator.tsx b/packages/excel-collab/src/component/Separator.tsx new file mode 100644 index 00000000..fef193b8 --- /dev/null +++ b/packages/excel-collab/src/component/Separator.tsx @@ -0,0 +1,11 @@ +'use client'; +import { + Separator as RACSeparator, + type SeparatorProps, +} from 'react-aria-components/Separator'; + +import './Separator.css'; + +export function Separator(props: SeparatorProps) { + return ; +} diff --git a/packages/excel-collab/src/component/Sheet.css b/packages/excel-collab/src/component/Sheet.css new file mode 100644 index 00000000..b720d6ad --- /dev/null +++ b/packages/excel-collab/src/component/Sheet.css @@ -0,0 +1,66 @@ +.sheet-overlay { + position: absolute; + top: 0; + left: 0; + width: var(--page-width); + height: var(--page-height); + background: rgba(0 0 0 / 0.3); + backdrop-filter: blur(10px); + z-index: 100; + overflow: clip; + + &[data-entering] { + animation: sheet-blur 300ms; + } + + &[data-exiting] { + animation: sheet-blur 300ms reverse ease-in; + } +} + +.sheet { + position: sticky; + left: 0; + width: 300px; + /* Extra padding to account for iOS floating browser UI. */ + top: -100px; + height: calc(100dvh + 200px); + padding: 100px 0; + margin-left: auto; + background: var(--overlay-background); + outline: none; + border-left: 1px solid var(--overlay-border); + box-shadow: -8px 0 20px rgba(0 0 0 / 0.1); + font-family: system-ui; + font-size: 0.875rem; + + &[data-entering] { + animation: sheet-slide 300ms; + } + + &[data-exiting] { + animation: sheet-slide 300ms reverse ease-in; + } +} + +@keyframes sheet-blur { + from { + background: rgba(0 0 0 / 0); + backdrop-filter: blur(0); + } + + to { + background: rgba(0 0 0 / 0.3); + backdrop-filter: blur(10px); + } +} + +@keyframes sheet-slide { + from { + transform: translateX(100%); + } + + to { + transform: translateX(0); + } +} diff --git a/packages/excel-collab/src/component/Sheet.tsx b/packages/excel-collab/src/component/Sheet.tsx new file mode 100644 index 00000000..b5b7b4be --- /dev/null +++ b/packages/excel-collab/src/component/Sheet.tsx @@ -0,0 +1,19 @@ +'use client'; +import {Modal, ModalOverlay, type ModalOverlayProps, Heading} from 'react-aria-components/Modal'; +import {composeRenderProps} from 'react-aria-components/composeRenderProps'; +import {Dialog} from './Dialog'; +import './Sheet.css'; + +export function Sheet(props: ModalOverlayProps) { + return ( + + {composeRenderProps(props.children, children => ( + + {children} + + ))} + + ); +} + +export {Heading}; diff --git a/packages/excel-collab/src/component/Slider.css b/packages/excel-collab/src/component/Slider.css new file mode 100644 index 00000000..b820fca1 --- /dev/null +++ b/packages/excel-collab/src/component/Slider.css @@ -0,0 +1,157 @@ + +@import './utilities.css'; + +.react-aria-Slider { + display: grid; + grid-template-areas: + 'label output' + 'track track'; + grid-template-columns: 1fr auto; + max-width: min(calc(100% - 12px), 300px); + font: var(--font-size) system-ui; + color: var(--text-color); + + .react-aria-Label { + grid-area: label; + } + + .react-aria-SliderOutput { + grid-area: output; + font-size: var(--font-size); + } + + .react-aria-SliderTrack { + grid-area: track; + position: relative; + + .track { + display: block; + position: absolute; + border-radius: 9999px; + } + + .react-aria-SliderFill { + border-radius: inherit; + background: var(--tint-900); + + @media (forced-colors: active) { + background: Highlight; + } + } + } + + .react-aria-SliderThumb { + width: calc(var(--spacing) * 5.5); + height: calc(var(--spacing) * 5.5); + border-radius: 50%; + forced-color-adjust: none; + --indicator-color: var(--tint); + --indicator-drop-shadow: 0 1px 4px rgb(0 0 0 / 0.3); + + &[data-dragging] { + background: var(--button-background-pressed); + } + } + + &[data-orientation='horizontal'] { + flex-direction: column; + width: 100%; + + .react-aria-SliderTrack { + height: var(--spacing-5); + width: 100%; + + .track { + height: var(--spacing-2); + width: 100%; + top: 50%; + transform: translateY(-50%); + } + + .react-aria-SliderFill { + box-shadow: + inset 0 -1px 0 lch(from var(--tint) 42% c h), + inset 0 0 0 1px var(--tint-900), + inset 0 2px 0 rgb(255 255 255 / 0.3), + inset 0 3px 2px rgb(255 255 255 / 0.2); + + @media (prefers-color-scheme: dark) { + box-shadow: + inset 0 -1px 0 lch(from var(--tint) 42% c h), + inset 0 1px 0 rgb(255 255 255 / 0.6), + inset 0 0 0 1px var(--tint-900), + inset 0 3px 2px rgb(255 255 255 / 0.2); + } + + @media (forced-colors: active) { + box-shadow: none; + } + } + } + + .react-aria-SliderThumb { + top: 50%; + } + } + + &[data-orientation='vertical'] { + height: 150px; + display: block; + margin: var(--spacing-3) 0; + + .react-aria-Label, + .react-aria-SliderOutput { + display: none; + } + + .react-aria-SliderTrack { + width: var(--spacing-5); + height: 100%; + + .track { + width: var(--spacing-2); + height: 100%; + left: 50%; + transform: translateX(-50%); + } + + .react-aria-SliderFill { + box-shadow: + inset -1px 0 0 lch(from var(--tint) 42% c h), + inset 0 0 0 1px var(--tint-900), + inset 2px 0 0 rgb(255 255 255 / 0.3), + inset 3px 0 2px rgb(255 255 255 / 0.2); + + @media (prefers-color-scheme: dark) { + box-shadow: + inset -1px 0 0 lch(from var(--tint) 42% c h), + inset 1px 0 0 rgb(255 255 255 / 0.6), + inset 0 0 0 1px var(--tint-900), + inset 3px 0 2px rgb(255 255 255 / 0.2); + } + + @media (forced-colors: active) { + box-shadow: none; + } + } + } + + .react-aria-SliderThumb { + left: 50%; + } + } + + &[data-disabled] { + .react-aria-SliderTrack { + .track { + background: var(--field-background); + --border-color: var(--border-color-disabled); + } + + .react-aria-SliderFill { + background: var(--border-color-disabled); + box-shadow: none; + } + } + } +} diff --git a/packages/excel-collab/src/component/Slider.tsx b/packages/excel-collab/src/component/Slider.tsx new file mode 100644 index 00000000..b0881f61 --- /dev/null +++ b/packages/excel-collab/src/component/Slider.tsx @@ -0,0 +1,55 @@ +'use client'; +import { + Slider as AriaSlider, + SliderOutput, + type SliderProps as AriaSliderProps, + SliderThumb, + SliderTrack, + SliderFill +} from 'react-aria-components/Slider'; +import {Label} from './Form'; +import './Slider.css'; + +export interface SliderProps extends AriaSliderProps { + /** Label for the slider. */ + label?: string; + /** Aria labels for each thumb. */ + thumbLabels?: string[]; + /** + * The offset from which to start the fill. + * + * @default 0 + */ + fillOffset?: number; +} + +export function Slider({ + label, + thumbLabels, + fillOffset, + ...props +}: SliderProps) { + return ( + + {label && } + + + {({state, isDisabled}) => ( + <> +
+ +
+ {state.values.map((_, i) => ( + + ))} + + )} +
+
+ ); +} diff --git a/packages/excel-collab/src/component/Switch.css b/packages/excel-collab/src/component/Switch.css new file mode 100644 index 00000000..6a3cb6a7 --- /dev/null +++ b/packages/excel-collab/src/component/Switch.css @@ -0,0 +1,96 @@ + +@import './utilities.css'; + +.react-aria-SwitchField { + display: flex; + flex-direction: column; + gap: var(--spacing-1); + + &[data-disabled] [slot='description'] { + color: var(--text-color-disabled); + } +} + +.react-aria-SwitchButton { + display: flex; + /* This is needed so the HiddenInput is positioned correctly */ + position: relative; + align-items: center; + gap: var(--spacing-2); + font: var(--font-size) system-ui; + color: var(--text-color); + forced-color-adjust: none; + -webkit-tap-highlight-color: transparent; + + .track { + --height: calc(var(--spacing) * 5.5); + height: var(--height); + width: calc(var(--spacing) * 9.5); + border-radius: var(--height); + transition: all 200ms; + scale: 1; + + .handle { + display: block; + height: 100%; + aspect-ratio: 1; + border-radius: var(--height); + transition: all 200ms; + transform-origin: 0 50%; + will-change: transform; + --indicator-color: var(--tint); + } + } + + &[data-pressed] .handle { + scale: 1.2 1; + border-radius: var(--height) / calc(var(--height) * 1.2); + background: var(--button-background-pressed); + } + + &[data-invalid] { + .handle { + --indicator-color: var(--invalid-color); + } + + &[data-pressed] .handle { + background: transparent; + } + } + + &[data-selected] { + --border-color: var(--highlight-background-pressed); + .handle { + transform: translateX(var(--spacing-4)); + transform-origin: var(--spacing-9) 50%; + background: white; + box-shadow: + inset 0 0 0 1px var(--border-color), + inset 0 -4px 4px oklch(from var(--tint) 85% c h / 0.3); + + @media (forced-colors: active) { + background: HighlightText; + box-shadow: inset 0 0 0 1px Highlight; + } + } + } + + &[data-focus-visible] .track { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + &[data-disabled] { + color: var(--text-color-disabled); + --border-color: var(--border-color-disabled); + + .track { + box-shadow: 0 0 0 1px var(--border-color); + } + + &[data-selected] .handle { + background: var(--gray-200); + box-shadow: none; + } + } +} diff --git a/packages/excel-collab/src/component/Switch.tsx b/packages/excel-collab/src/component/Switch.tsx new file mode 100644 index 00000000..bd1733fa --- /dev/null +++ b/packages/excel-collab/src/component/Switch.tsx @@ -0,0 +1,38 @@ +'use client'; +import { + SwitchField, + SwitchButton, + type SwitchFieldProps, + type ValidationResult +} from 'react-aria-components/Switch'; +import './Switch.css'; +import {Description, FieldError} from './Form'; +import type {ReactNode} from 'react'; + +export interface SwitchProps extends Omit { + children: ReactNode; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); +} + +export function Switch({children, description, errorMessage, ...props}: SwitchProps) { + return ( + + + {({isSelected, isDisabled}) => ( + <> +
+
+
+ {children} + + )} + + {description && {description}} + {errorMessage} + + ); +} diff --git a/packages/excel-collab/src/component/Table.css b/packages/excel-collab/src/component/Table.css new file mode 100644 index 00000000..132bb176 --- /dev/null +++ b/packages/excel-collab/src/component/Table.css @@ -0,0 +1,372 @@ + +@import './utilities.css'; + +.react-aria-Table { + border: 0.5px solid var(--border-color); + border-radius: var(--radius); + background: var(--overlay-background); + overflow: clip; + outline: none; + border-spacing: 0; + align-self: start; + width: 100%; + word-break: break-word; + forced-color-adjust: none; + font: var(--font-size) system-ui; + + div& { + padding: 0; + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + } + + &[data-drop-target] { + outline: 2px solid var(--highlight-background); + background: var(--highlight-overlay); + } + + &:has(.react-aria-TableBody[data-empty]) { + height: 100%; + min-height: 100px; + } + + .react-aria-TableHeader { + color: var(--text-color); + } + + .react-aria-TableBody { + padding: var(--spacing-1); + + &[data-empty] { + text-align: center; + font-style: italic; + } + } +} + +.react-aria-Row { + outline: none; + cursor: default; + color: var(--text-color); + position: relative; + transform: scale(1); + transition-property: background, color; + transition-duration: 200ms; + -webkit-tap-highlight-color: transparent; + + tr:last-child& { + border-radius: 0 0 var(--radius) var(--radius); + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } + + &[data-pressed] { + background: var(--gray-100); + } + + &:nth-child(2n) { + background: var(--gray-100); + &[data-pressed] { + background: var(--gray-200); + } + } + + &[data-selected] { + background: var(--highlight-background); + color: var(--highlight-foreground); + --focus-ring-color: var(--highlight-foreground); + + &[data-focus-visible], + .react-aria-Cell[data-focus-visible] { + outline-offset: -4px; + } + + &[data-pressed] { + background: var(--highlight-background-pressed); + } + } + + &[data-disabled] { + color: var(--text-color-disabled); + } + + &[data-href] { + cursor: pointer; + } + + &[data-dragging] { + opacity: 0.6; + transform: translateZ(0); + } + + &[data-drop-target] { + outline: 2px solid var(--highlight-background); + background: var(--highlight-overlay); + z-index: 4; + } + + .drag-button { + all: unset; + display: flex; + align-items: center; + justify-content: center; + width: 15px; + text-align: center; + + svg { + width: var(--spacing-4); + height: var(--spacing-4); + } + + &[data-focus-visible] { + border-radius: 4px; + outline: 2px solid var(--focus-ring-color); + } + } + + .react-aria-DropIndicator[data-drop-target] { + outline: 1px solid var(--highlight-background); + transform: translateZ(0); + } +} + +.react-aria-Cell, +.react-aria-Column { + padding: var(--spacing-2); + text-align: left; + outline: none; + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; + + div& { + display: flex; + align-items: center; + height: 100%; + } + + &:is(.react-aria-Row:last-child > :first-child) { + border-end-start-radius: var(--radius); + } + + &:is(.react-aria-Row:last-child > :last-child) { + border-end-end-radius: var(--radius); + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } +} + +.react-aria-Column { + --button-color: var(--gray); + --button-border: transparent; + --button-shadow: transparent; + --button-border-size: 0px; + cursor: default; + border-bottom: 0.5px solid var(--border-color); + border-inline-end: 0.5px solid var(--border-color); + position: relative; + z-index: calc(sibling-count() - sibling-index()); + + &:last-child { + border-inline-end: none; + } + + &[data-pressed] { + background: var(--gray-200); + } + + &[data-sort-direction] { + --button-color: var(--tint); + + &[data-pressed] { + background: var(--tint-200); + } + } + + .column-header { + display: flex; + align-items: center; + font-weight: 600; + } + + .column-name { + flex: 1; + font: inherit; + text-align: start; + color: inherit; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + border-color: transparent; + border-radius: var(--radius-sm); + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + } + + .sort-indicator { + padding: 0 2px; + display: inline-flex; + align-items: center; + justify-content: center; + } + + &:not([data-sort-direction]) .sort-indicator { + visibility: hidden; + } +} + +.react-aria-ColumnResizer { + width: 1px; + border-radius: var(--radius-sm); + background-color: var(--gray-500); + padding: 0 var(--spacing-2); + translate: var(--spacing-4) 0; + height: 32px; + margin: -16px 0; + flex: 0 0 auto; + touch-action: none; + box-sizing: content-box; + background-clip: content-box; + + &[data-resizable-direction='both'] { + cursor: ew-resize; + } + + &[data-resizable-direction='left'] { + cursor: e-resize; + } + + &[data-resizable-direction='right'] { + cursor: w-resize; + } + + &[data-focus-visible] { + background-color: var(--focus-ring-color); + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } + + &[data-resizing] { + width: 2px; + background-color: var(--highlight-background); + } +} + +.react-aria-Cell { + transform: translateZ(0); + --border-color: var(--gray-300); + tr:not(:last-child) & { + border-bottom: 0.5px solid var(--border-color); + } + + [data-selected]:has(+ [data-selected]) & { + --border-color: rgb(255 255 255 / 0.3); + } + + @media (forced-colors: active) { + --border-color: ButtonBorder; + } + + &[data-tree-column] { + --chevron-placeholder: var(--spacing-5); + padding-inline-start: calc( + var(--spacing-2) + (var(--table-row-level) - 1) * var(--spacing-4) + + var(--chevron-placeholder) + ); + + &[data-has-child-items] { + --chevron-placeholder: 0px; + } + } + + .react-aria-Button[slot='chevron'] { + all: unset; + vertical-align: middle; + margin-inline-end: var(--spacing-1); + + svg { + rotate: 0deg; + transition: rotate 200ms; + fill: none; + stroke: currentColor; + stroke-width: 3px; + width: var(--spacing-4); + height: var(--spacing-4); + } + } + + &[data-expanded] .react-aria-Button[slot='chevron'] svg { + rotate: 90deg; + } +} + +.react-aria-DropIndicator[data-drop-target] { + outline: 1px solid var(--highlight-background); + transform: translateZ(0); + translate: calc(68px + (var(--table-row-level) - 1) * var(--spacing-4)) 0; +} + +.react-aria-Row .react-aria-CheckboxButton { + --checkmark-color: var(--highlight-background); + &[data-selected] .indicator { + --indicator-color: var(--highlight-foreground); + --indicator-shadow: transparent; + } +} + +.react-aria-ResizableTableContainer { + width: 100%; + overflow: auto; + position: relative; + border: 0.5px solid var(--border-color); + border-radius: var(--radius); + background: var(--overlay-background); + + .react-aria-Table { + border: none; + border-radius: 0; + + &:has(.react-aria-TableBody[data-empty]) { + height: 100%; + } + } + + .react-aria-TableHeader { + position: sticky; + top: 0; + z-index: 10; + } + + .react-aria-Cell { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.react-aria-Cell img { + height: 30px; + width: 30px; + object-fit: cover; + display: block; +} + +.react-aria-TableLoadingIndicator { + height: 32px; + position: relative; +} + +.react-aria-TableFooter { + font-weight: bold; + background: var(--gray-200); +} diff --git a/packages/excel-collab/src/component/Table.tsx b/packages/excel-collab/src/component/Table.tsx new file mode 100644 index 00000000..fa4c60ca --- /dev/null +++ b/packages/excel-collab/src/component/Table.tsx @@ -0,0 +1,151 @@ +'use client'; +import { + Button, + Collection, + Column as AriaColumn, + type ColumnProps as AriaColumnProps, + Row as AriaRow, + type RowProps, + Table as AriaTable, + TableHeader as AriaTableHeader, + type TableHeaderProps, + type TableProps, + useTableOptions, + type TableBodyProps, + TableBody as AriaTableBody, + type CellProps, + Cell as AriaCell, + ColumnResizer, + TableLoadMoreItem as AriaTableLoadMoreItem, + type TableLoadMoreItemProps, + TableFooter as AriaTableFooter, + type TableFooterProps +} from 'react-aria-components/Table'; +import {Group} from 'react-aria-components/Group'; +import {composeRenderProps} from 'react-aria-components/composeRenderProps'; +import {Checkbox} from './Checkbox'; +import {ProgressCircle} from './ProgressCircle'; +import {ChevronUp, ChevronDown, GripVertical, ChevronRight} from 'lucide-react'; +import './Table.css'; + +export function Table(props: TableProps) { + return ; +} + +interface ColumnProps extends AriaColumnProps { + allowsResizing?: boolean; +} + +export function Column(props: Omit & {children?: React.ReactNode}) { + return ( + + {({allowsSorting, sortDirection}) => ( +
+ + {props.children} + + {allowsSorting && ( + + )} + {props.allowsResizing && } +
+ )} +
+ ); +} + +export function TableHeader({columns, children, ...otherProps}: TableHeaderProps) { + let {selectionBehavior, selectionMode, allowsDragging} = useTableOptions(); + + return ( + + {/* Add extra columns for drag and drop and selection. */} + {allowsDragging && ( + + )} + {selectionBehavior === 'toggle' && ( + + {selectionMode === 'multiple' && } + + )} + {children} + + ); +} + +export function Row({id, columns, children, ...otherProps}: RowProps) { + let {selectionBehavior, allowsDragging} = useTableOptions(); + + return ( + + {allowsDragging && ( + + + + )} + {selectionBehavior === 'toggle' && ( + + + + )} + {children} + + ); +} + +export function TableBody(props: TableBodyProps) { + return ; +} + +export function TableFooter(props: TableFooterProps) { + return ; +} + +export function Cell(props: CellProps) { + return ( + + {composeRenderProps(props.children, (children, {hasChildItems, isTreeColumn}) => ( + <> + {isTreeColumn && hasChildItems && ( + + )} + {children} + + ))} + + ); +} + +export function TableLoadMoreItem(props: TableLoadMoreItemProps) { + return ( + +
+ +
+
+ ); +} diff --git a/packages/excel-collab/src/component/Tabs.css b/packages/excel-collab/src/component/Tabs.css new file mode 100644 index 00000000..fed3f0a4 --- /dev/null +++ b/packages/excel-collab/src/component/Tabs.css @@ -0,0 +1,144 @@ + + +.react-aria-Tabs { + display: flex; + gap: var(--spacing-2); + color: var(--text-color); + max-width: 100%; + + &[data-orientation='horizontal'] { + flex-direction: column; + } + + &[data-orientation='vertical'] { + flex-direction: row; + width: 100%; + .react-aria-TabPanels { + flex: 1; + } + } +} + +.react-aria-TabList { + display: flex; + + .react-aria-SelectionIndicator { + background: var(--border-color); + } + + &[data-orientation='horizontal'] { + border-bottom: 0.5px solid var(--border-color); + max-width: 100%; + overflow-x: auto; + overflow-y: clip; + scrollbar-width: none; + + .react-aria-SelectionIndicator { + left: 0; + bottom: 0px; + width: 100%; + height: 3px; + transition-property: translate, width; + } + } + + &[data-orientation='vertical'] { + flex-direction: column; + border-inline-end: 1px solid var(--border-color); + + .react-aria-SelectionIndicator { + top: 0; + right: 0px; + height: 100%; + width: 3px; + transition-property: translate, height; + } + } +} + +.react-aria-Tab { + padding: 10px; + cursor: default; + outline: none; + position: relative; + font: var(--font-size) system-ui; + color: var(--text-color); + transition: color 200ms; + --border-color: transparent; + forced-color-adjust: none; + -webkit-tap-highlight-color: transparent; + + &[data-hovered], + &[data-focus-visible] { + color: var(--text-color-hover); + } + + .react-aria-SelectionIndicator { + position: absolute; + transition-duration: 200ms; + border-radius: 3px; + + @media (prefers-reduced-motion: reduce) { + transition: none; + } + } + + &[data-selected] { + --border-color: var(--highlight-background); + color: var(--text-color-hover); + } + + &[data-disabled] { + color: var(--text-color-disabled); + &[data-selected] { + --border-color: var(--text-color-disabled); + } + } + + &[data-focus-visible] { + border-radius: calc(var(--radius-sm) + 8px); + outline: 2px solid var(--focus-ring-color); + outline-offset: -8px; + } + + &[href] { + text-decoration: none; + cursor: pointer; + } +} + +.react-aria-TabPanels { + position: relative; + height: var(--tab-panel-height); + width: var(--tab-panel-width); + transition: height 400ms; + overflow: clip; + + @media (prefers-reduced-motion: reduce) { + transition: none; + } +} + +.react-aria-TabPanel { + padding: var(--spacing-3); + border-radius: var(--radius); + outline: none; + box-sizing: border-box; + transition: opacity 400ms; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + } + + &[data-entering], + &[data-exiting] { + opacity: 0; + } + + &[data-exiting] { + position: absolute; + top: 0; + left: 0; + width: 100%; + } +} diff --git a/packages/excel-collab/src/component/Tabs.tsx b/packages/excel-collab/src/component/Tabs.tsx new file mode 100644 index 00000000..5361a124 --- /dev/null +++ b/packages/excel-collab/src/component/Tabs.tsx @@ -0,0 +1,45 @@ +'use client'; +import { + Tabs as RACTabs, + TabList as RACTabList, + type TabListProps, + type TabProps, + Tab as RACTab, + type TabsProps, + TabPanels as RACTabPanels, + type TabPanelProps, + TabPanel as RACTabPanel, + SelectionIndicator, + type TabPanelsProps +} from 'react-aria-components/Tabs'; +import {composeRenderProps} from 'react-aria-components/composeRenderProps'; +import './Tabs.css'; + +export function Tabs(props: TabsProps) { + return ; +} + +export function TabList(props: TabListProps) { + return ; +} + +export function Tab(props: TabProps) { + return ( + + {composeRenderProps(props.children, children => ( + <> + {children} + + + ))} + + ); +} + +export function TabPanels(props: TabPanelsProps) { + return ; +} + +export function TabPanel(props: TabPanelProps) { + return ; +} diff --git a/packages/excel-collab/src/component/TagGroup.css b/packages/excel-collab/src/component/TagGroup.css new file mode 100644 index 00000000..61e3f7b5 --- /dev/null +++ b/packages/excel-collab/src/component/TagGroup.css @@ -0,0 +1,116 @@ + +@import './utilities.css'; +@import './Form.css'; + +.react-aria-TagGroup { + display: flex; + flex-direction: column; + font: var(--font-size) system-ui; + color: var(--text-color); + + [slot='errorMessage'] { + margin-top: var(--spacing); + font-size: var(--font-size-sm); + color: var(--invalid-color); + } +} + +.react-aria-TagList { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-2); +} + +.react-aria-Tag { + --button-color: var(--gray); + --button-gradient-size: 4px; + text-shadow: none; + forced-color-adjust: none; + border-radius: 9999px; + height: var(--spacing-5); + padding: 0 var(--spacing-3); + font: var(--font-size-sm) system-ui; + cursor: default; + display: flex; + align-items: center; + gap: var(--spacing-1); + transition-property: scale, background, border-color, box-shadow, text-shadow; + transition-duration: 200ms; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + -webkit-tap-highlight-color: transparent; + + &:has(.remove-button) { + padding: 0 var(--spacing-1) 0 var(--spacing-2); + } + + &[data-pressed] { + scale: 0.96; + } + + &[data-selected] { + --button-color: var(--tint); + } + + &[data-href] { + text-decoration: none; + cursor: pointer; + } + + .remove-button { + background: none; + border: none; + padding: 0; + margin: 0; + appearance: none; + color: var(--gray-1000); + font: inherit; + transition: color 200ms; + outline: none; + border-radius: 100%; + aspect-ratio: 1/1; + height: calc(100% - 4px); + width: auto; + min-width: 0; + display: flex; + align-items: center; + justify-content: center; + transition-property: background, color, scale; + transition-duration: 200ms; + -webkit-tap-highlight-color: transparent; + + svg { + width: calc(100% - 2px); + height: calc(100% - 2px); + } + + &:where([data-hovered], [data-pressed]) { + background: var(--gray-300); + color: var(--gray-1200); + } + + @media (forced-colors: active) { + background: ButtonFace; + color: ButtonText; + } + + &[data-pressed] { + scale: 0.9; + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + } + + &[data-selected] { + .remove-button { + color: inherit; + &[data-hovered] { + background: rgb(255 255 255 / 0.2); + } + } + } +} diff --git a/packages/excel-collab/src/component/TagGroup.tsx b/packages/excel-collab/src/component/TagGroup.tsx new file mode 100644 index 00000000..8c137fec --- /dev/null +++ b/packages/excel-collab/src/component/TagGroup.tsx @@ -0,0 +1,67 @@ +'use client'; +import { + Button, + Tag as AriaTag, + TagGroup as AriaTagGroup, + type TagGroupProps as AriaTagGroupProps, + TagList, + type TagListProps, + type TagProps +} from 'react-aria-components/TagGroup'; +import {Description, Label} from './Form'; +import {Text} from './Content'; +import {X} from 'lucide-react'; +import './TagGroup.css'; + +export interface TagGroupProps + extends + Omit, + Pick, 'items' | 'children' | 'renderEmptyState'> { + label?: string; + description?: string; + errorMessage?: string; +} + +export function TagGroup({ + label, + description, + errorMessage, + items, + children, + renderEmptyState, + ...props +}: TagGroupProps) { + return ( + + {label && } + + {children} + + {description && {description}} + {errorMessage && {errorMessage}} + + ); +} + +export function Tag({ + children, + ...props +}: Omit & { + children?: React.ReactNode; +}) { + let textValue = typeof children === 'string' ? children : undefined; + return ( + + {({allowsRemoving}) => ( + <> + {children} + {allowsRemoving && ( + + )} + + )} + + ); +} diff --git a/packages/excel-collab/src/component/TextField.css b/packages/excel-collab/src/component/TextField.css new file mode 100644 index 00000000..d6293e96 --- /dev/null +++ b/packages/excel-collab/src/component/TextField.css @@ -0,0 +1,43 @@ + +@import './utilities.css'; + +.react-aria-TextField { + display: flex; + flex-direction: column; +} + +.react-aria-Input, +.react-aria-TextArea { + width: 100%; + min-height: var(--spacing-8); + padding: 0 var(--spacing-3); + box-sizing: border-box; + margin: 0; + border: none; + border-radius: var(--radius); + font: var(--font-size) system-ui; + color: var(--field-text-color); + outline: none; + -webkit-tap-highlight-color: transparent; + + &::placeholder { + color: var(--text-color-placeholder); + } + + &[data-focused] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + + &[data-disabled] { + color: var(--text-color-disabled); + &::placeholder { + color: var(--text-color-disabled); + } + } +} + +.react-aria-TextArea { + min-height: 64px; + padding: var(--spacing-2) var(--spacing-3); +} diff --git a/packages/excel-collab/src/component/TextField.tsx b/packages/excel-collab/src/component/TextField.tsx new file mode 100644 index 00000000..44680949 --- /dev/null +++ b/packages/excel-collab/src/component/TextField.tsx @@ -0,0 +1,59 @@ +'use client'; +import { + Input, + TextField as AriaTextField, + TextArea as AriaTextArea, + type TextFieldProps as AriaTextFieldProps, + type ValidationResult +} from 'react-aria-components/TextField'; +import {Label, FieldError, Description} from './Form'; +import './TextField.css'; +import type React from 'react'; + +export interface TextFieldProps extends AriaTextFieldProps { + label?: string; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); + placeholder?: string; + inputRef?: React.Ref; +} + +export function TextField({ + label, + description, + errorMessage, + placeholder, + inputRef, + ...props +}: TextFieldProps) { + return ( + + {label && } + + {description && {description}} + {errorMessage} + + ); +} + +export function TextArea({ + label, + description, + errorMessage, + placeholder, + inputRef, + ...props +}: TextFieldProps) { + return ( + + + + {description && {description}} + {errorMessage} + + ); +} diff --git a/packages/excel-collab/src/component/TimeField.css b/packages/excel-collab/src/component/TimeField.css new file mode 100644 index 00000000..649da52d --- /dev/null +++ b/packages/excel-collab/src/component/TimeField.css @@ -0,0 +1,6 @@ + + +.react-aria-TimeField { + display: flex; + flex-direction: column; +} diff --git a/packages/excel-collab/src/component/TimeField.tsx b/packages/excel-collab/src/component/TimeField.tsx new file mode 100644 index 00000000..1d259115 --- /dev/null +++ b/packages/excel-collab/src/component/TimeField.tsx @@ -0,0 +1,32 @@ +'use client'; +import { + TimeField as AriaTimeField, + type TimeFieldProps as AriaTimeFieldProps, + type TimeValue, + type ValidationResult +} from 'react-aria-components/TimeField'; +import {Label, FieldError, Description} from './Form'; +import {DateInput, DateSegment} from './DateField'; +import './TimeField.css'; + +export interface TimeFieldProps extends AriaTimeFieldProps { + label?: string; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); +} + +export function TimeField({ + label, + description, + errorMessage, + ...props +}: TimeFieldProps) { + return ( + + + {segment => } + {description && {description}} + {errorMessage} + + ); +} diff --git a/packages/excel-collab/src/component/Toast.css b/packages/excel-collab/src/component/Toast.css new file mode 100644 index 00000000..1b99b907 --- /dev/null +++ b/packages/excel-collab/src/component/Toast.css @@ -0,0 +1,107 @@ + + +.react-aria-ToastRegion { + position: fixed; + bottom: var(--spacing-4); + right: var(--spacing-4); + z-index: 100; + display: flex; + flex-direction: column-reverse; + gap: var(--spacing-2); + border-radius: var(--radius-lg); + outline: none; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } +} + +.react-aria-Toast { + width: 230px; + display: flex; + align-items: center; + gap: var(--spacing-4); + background: var(--highlight-background); + padding: var(--spacing-3) var(--spacing-4); + border-radius: var(--radius-lg); + outline: none; + forced-color-adjust: none; + view-transition-class: toast; + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + .react-aria-ToastContent { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-width: 0; + font: var(--font-size) system-ui; + + [slot='title'] { + font-weight: 600; + color: var(--highlight-foreground); + } + + [slot='description'] { + font-size: var(--font-size-sm); + color: var(--highlight-foreground); + } + } + + .react-aria-Button[slot='close'] { + flex: 0 0 auto; + background: none; + border: none; + appearance: none; + border-radius: var(--radius-sm); + height: var(--spacing-8); + width: var(--spacing-8); + color: var(--highlight-foreground); + padding: 0; + outline: none; + -webkit-tap-highlight-color: transparent; + + &[data-hovered] { + background: var(--tint-900); + box-shadow: none; + } + + &[data-focus-visible] { + outline: 2px solid var(--highlight-foreground); + outline-offset: 2px; + } + + &[data-pressed] { + background: var(--highlight-pressed); + box-shadow: none; + } + } +} + +::view-transition-new(.toast):only-child { + animation: slide-in 400ms; +} + +::view-transition-old(.toast):only-child { + animation: slide-out 400ms; + animation-fill-mode: forwards; +} + +@keyframes slide-out { + to { + translate: 100% 0; + opacity: 0; + visibility: hidden; + } +} + +@keyframes slide-in { + from { + translate: 100% 0; + opacity: 0; + } +} diff --git a/packages/excel-collab/src/component/Toast.tsx b/packages/excel-collab/src/component/Toast.tsx new file mode 100644 index 00000000..12750d4c --- /dev/null +++ b/packages/excel-collab/src/component/Toast.tsx @@ -0,0 +1,63 @@ +'use client'; +import { + UNSTABLE_ToastRegion as ToastRegion, + UNSTABLE_Toast as Toast, + UNSTABLE_ToastQueue as ToastQueue, + UNSTABLE_ToastContent as ToastContent, + type ToastProps, + Text, +} from 'react-aria-components/Toast'; +import { Button } from './Button'; +import { X } from 'lucide-react'; +import './Toast.css'; +import { flushSync } from 'react-dom'; +import { type CSSProperties } from 'react'; + +// Define the type for your toast content. This interface defines the properties of your toast content, affecting what you +// pass to the queue calls as arguments. +interface MyToastContent { + title: string; + description?: string; +} + +// This is a global toast queue, to be imported and called where ever you want to queue a toast via queue.add(). +export const queue = new ToastQueue({ + // Wrap state updates in a CSS view transition. + wrapUpdate(fn) { + if ('startViewTransition' in document) { + document.startViewTransition(() => { + flushSync(fn); + }); + } else { + fn(); + } + }, +}); + +export function MyToastRegion() { + return ( + // The ToastRegion should be rendered at the root of your app. + + {({ toast }) => ( + + + {toast.content.title} + {toast.content.description && ( + {toast.content.description} + )} + + + + )} + + ); +} + +export function MyToast(props: ToastProps) { + return ; +} diff --git a/packages/excel-collab/src/component/ToggleButton.css b/packages/excel-collab/src/component/ToggleButton.css new file mode 100644 index 00000000..4f4e18de --- /dev/null +++ b/packages/excel-collab/src/component/ToggleButton.css @@ -0,0 +1,37 @@ + +@import './utilities.css'; + +.react-aria-ToggleButton { + border: none; + forced-color-adjust: none; + border-radius: var(--radius); + appearance: none; + vertical-align: middle; + font: var(--font-size) system-ui; + font-weight: 500; + text-align: center; + margin: 0; + height: var(--spacing-8); + padding: 0 var(--spacing-3); + display: inline-flex; + align-items: center; + justify-content: center; + transition-property: background, box-shadow, text-shadow, scale; + transition-duration: 200ms; + -webkit-tap-highlight-color: transparent; + + > span { + display: flex; + align-items: center; + justify-content: center; + } + + > span > svg { + width: var(--spacing-4); + height: var(--spacing-4); + } + + &[data-pressed] { + scale: 0.95; + } +} diff --git a/packages/excel-collab/src/component/ToggleButton.tsx b/packages/excel-collab/src/component/ToggleButton.tsx new file mode 100644 index 00000000..9386c0fc --- /dev/null +++ b/packages/excel-collab/src/component/ToggleButton.tsx @@ -0,0 +1,39 @@ +'use client'; +import { composeRenderProps } from 'react-aria-components/composeRenderProps'; +import { + ToggleButton as RACToggleButton, + type ToggleButtonProps as RACToggleButtonProps, +} from 'react-aria-components/ToggleButton'; +import './ToggleButton.css'; +import { TooltipTrigger } from 'react-aria-components/Tooltip'; +import { Tooltip } from './Tooltip'; +import clsx from 'clsx'; + +interface ToggleButtonProps extends Omit { + /** + * The visual style of the button (Vanilla CSS implementation specific). + * + * @default 'secondary' + */ + variant?: 'primary' | 'secondary' | 'quiet'; + 'aria-label': string; +} + +export function ToggleButton(props: ToggleButtonProps) { + return ( + + + clsx('react-aria-ToggleButton button-base', className), + )} + data-variant={props.variant || 'secondary'} + > + {composeRenderProps(props.children, (children) => ( + {children} + ))} + + {props['aria-label']} + + ); +} diff --git a/packages/excel-collab/src/component/ToggleButtonGroup.css b/packages/excel-collab/src/component/ToggleButtonGroup.css new file mode 100644 index 00000000..6dbb266a --- /dev/null +++ b/packages/excel-collab/src/component/ToggleButtonGroup.css @@ -0,0 +1,67 @@ + + +.react-aria-ToggleButtonGroup { + display: flex; + + > .react-aria-ToggleButton { + z-index: 1; + + > span { + transition: scale 200ms; + } + + &[data-pressed] { + scale: 1; + + > span { + scale: 0.9; + } + } + + &[data-disabled] { + z-index: 0; + } + + &[data-selected], + &[data-focus-visible] { + z-index: 2; + } + } +} + +.react-aria-ToggleButtonGroup[data-orientation='horizontal'] { + flex-direction: row; + + > .react-aria-ToggleButton { + margin-inline-start: -1px; + border-radius: 0; + + &:first-child { + border-radius: var(--radius) 0 0 var(--radius); + margin-inline-start: 0; + } + + &:last-child { + border-radius: 0 var(--radius) var(--radius) 0; + } + } +} + +.react-aria-ToggleButtonGroup[data-orientation='vertical'] { + flex-direction: column; + width: fit-content; + + > .react-aria-ToggleButton { + margin-block-start: -1px; + border-radius: 0; + + &:first-child { + border-radius: var(--radius) var(--radius) 0 0; + margin-block-start: 0; + } + + &:last-child { + border-radius: 0 0 var(--radius) var(--radius); + } + } +} diff --git a/packages/excel-collab/src/component/ToggleButtonGroup.tsx b/packages/excel-collab/src/component/ToggleButtonGroup.tsx new file mode 100644 index 00000000..4a2304e9 --- /dev/null +++ b/packages/excel-collab/src/component/ToggleButtonGroup.tsx @@ -0,0 +1,10 @@ +'use client'; +import { + ToggleButtonGroup as RACToggleButtonGroup, + type ToggleButtonGroupProps +} from 'react-aria-components/ToggleButtonGroup'; +import './ToggleButtonGroup.css'; + +export function ToggleButtonGroup(props: ToggleButtonGroupProps) { + return ; +} diff --git a/packages/excel-collab/src/component/TokenField.css b/packages/excel-collab/src/component/TokenField.css new file mode 100644 index 00000000..3ad52d26 --- /dev/null +++ b/packages/excel-collab/src/component/TokenField.css @@ -0,0 +1,65 @@ + +@import './utilities.css'; + +.react-aria-TokenField { + display: flex; + flex-direction: column; + width: 100%; +} + +.react-aria-TokenInput { + padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 2.5); + outline: none; + border-radius: var(--radius); + outline: none; + box-sizing: border-box; + font: var(--font-size) system-ui; + line-height: 1.5em; + color: var(--text-color); + + &:empty::before { + content: attr(data-placeholder); + color: var(--text-color-placeholder); + } + + &[aria-multiline='true'] { + min-height: 100px; + padding: calc(var(--spacing) * 2.5) var(--spacing-3); + } + + &[data-focused] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + + &[data-disabled] { + color: var(--text-color-disabled); + user-select: none; + } +} + +.react-aria-Token { + background-color: var(--tint-200); + color: var(--tint-1200); + border-radius: 9999px; + height: var(--spacing-5); + padding: 1px var(--spacing-2); + margin: 0 2px; + cursor: default; + white-space: nowrap; + -webkit-tap-highlight-color: transparent; + + &[data-selected] { + background-color: var(--highlight-background); + color: white; + } + + &::selection { + background-color: transparent; + } + + &[data-disabled] { + background-color: var(--gray-200); + color: var(--text-color-disabled); + } +} diff --git a/packages/excel-collab/src/component/TokenField.tsx b/packages/excel-collab/src/component/TokenField.tsx new file mode 100644 index 00000000..e390e6f1 --- /dev/null +++ b/packages/excel-collab/src/component/TokenField.tsx @@ -0,0 +1,52 @@ +'use client'; +import { + TokenField as AriaTokenField, + TokenInput as AriaTokenInput, + Token as AriaToken, + type TokenFieldProps as AriaTokenFieldProps, + type TokenInputProps, + type TokenProps, + type TokenFieldValue +} from 'react-aria-components/TokenField'; +import {Label, Description} from './Form'; +import './TokenField.css'; +import type React from 'react'; + +export interface TokenFieldProps extends Omit< + AriaTokenFieldProps, + 'children' +> { + label?: string; + description?: string; + placeholder?: string; + inputRef?: React.Ref; + children: TokenInputProps['children']; +} + +export function TokenField({ + label, + description, + placeholder, + inputRef, + style, + children, + ...props +}: TokenFieldProps) { + return ( + + {label && } + + {children} + + {description && {description}} + + ); +} + +export function Token(props: TokenProps) { + return ; +} diff --git a/packages/excel-collab/src/component/Toolbar.css b/packages/excel-collab/src/component/Toolbar.css new file mode 100644 index 00000000..658570cc --- /dev/null +++ b/packages/excel-collab/src/component/Toolbar.css @@ -0,0 +1,29 @@ + + +.react-aria-Toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-1); + width: fit-content; + + &[data-orientation='horizontal'] { + flex-direction: row; + } + + &[data-orientation='vertical'] { + flex-direction: column; + align-items: start; + } + + .react-aria-Group { + display: contents; + } + + .react-aria-Separator { + &[aria-orientation='vertical'] { + height: auto; + margin: 0px var(--spacing-1); + } + } +} diff --git a/packages/excel-collab/src/component/Toolbar.tsx b/packages/excel-collab/src/component/Toolbar.tsx new file mode 100644 index 00000000..e0f1367b --- /dev/null +++ b/packages/excel-collab/src/component/Toolbar.tsx @@ -0,0 +1,17 @@ +'use client'; +import {Toolbar as RACToolbar, type ToolbarProps} from 'react-aria-components/Toolbar'; +import {SeparatorContext} from 'react-aria-components/Separator'; +import {ToggleButtonGroupContext} from 'react-aria-components/ToggleButtonGroup'; +import './Toolbar.css'; + +export function Toolbar(props: ToolbarProps) { + let {orientation = 'horizontal'} = props; + return ( + + + + + + ); +} diff --git a/packages/excel-collab/src/component/Tooltip.css b/packages/excel-collab/src/component/Tooltip.css new file mode 100644 index 00000000..4ce1db1b --- /dev/null +++ b/packages/excel-collab/src/component/Tooltip.css @@ -0,0 +1,58 @@ + + +.react-aria-Tooltip { + box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); + border-radius: var(--radius); + background: var(--highlight-background); + color: var(--highlight-foreground); + forced-color-adjust: none; + outline: none; + padding: var(--spacing-1) var(--spacing-2); + max-width: 150px; + /* fixes FF gap */ + transform: translate3d(0, 0, 0); + transition: + transform 200ms, + opacity 200ms; + font: var(--font-size) system-ui; + + &[data-entering], + &[data-exiting] { + transform: var(--origin); + opacity: 0; + } + + &[data-placement='top'] { + margin-bottom: 10px; + --origin: translateY(4px); + } + + &[data-placement='bottom'] { + margin-top: 10px; + --origin: translateY(-4px); + & .react-aria-OverlayArrow svg { + transform: rotate(180deg); + } + } + + &[data-placement='right'] { + margin-left: 10px; + --origin: translateX(-4px); + & .react-aria-OverlayArrow svg { + transform: rotate(90deg); + } + } + + &[data-placement='left'] { + margin-right: 10px; + --origin: translateX(4px); + & .react-aria-OverlayArrow svg { + transform: rotate(-90deg); + } + } + + .react-aria-OverlayArrow svg { + display: block; + fill: var(--highlight-background); + } +} diff --git a/packages/excel-collab/src/component/Tooltip.tsx b/packages/excel-collab/src/component/Tooltip.tsx new file mode 100644 index 00000000..d9cf1cb9 --- /dev/null +++ b/packages/excel-collab/src/component/Tooltip.tsx @@ -0,0 +1,30 @@ +'use client'; +import { + OverlayArrow, + Tooltip as AriaTooltip, + type TooltipProps as AriaTooltipProps, + TooltipTrigger as AriaTooltipTrigger, + type TooltipTriggerComponentProps +} from 'react-aria-components/Tooltip'; +import './Tooltip.css'; + +export interface TooltipProps extends Omit { + children: React.ReactNode; +} + +export function Tooltip({children, ...props}: TooltipProps) { + return ( + + + + + + + {children} + + ); +} + +export function TooltipTrigger(props: TooltipTriggerComponentProps) { + return ; +} diff --git a/packages/excel-collab/src/component/Tree.css b/packages/excel-collab/src/component/Tree.css new file mode 100644 index 00000000..cc1a052a --- /dev/null +++ b/packages/excel-collab/src/component/Tree.css @@ -0,0 +1,219 @@ + + +.react-aria-Tree { + display: flex; + flex-direction: column; + overflow: auto; + padding: var(--spacing-1); + border: 0.5px solid var(--border-color); + border-radius: calc(var(--radius) + var(--spacing-1)); + background: var(--overlay-background); + forced-color-adjust: none; + outline: none; + width: 250px; + max-height: 300px; + box-sizing: border-box; + --drag-button-width: 0px; + --checkbox-width: 0px; + + .react-aria-TreeSection:not(:first-child) { + margin-top: var(--spacing-4); + } + + .react-aria-TreeHeader { + font-size: var(--font-size-lg); + font-weight: 500; + padding: var(--spacing-1) var(--spacing-2); + color: var(--text-color); + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -1px; + } + + &[data-empty] { + align-items: center; + justify-content: center; + font-style: italic; + min-height: 100px; + } + + &[data-selection-mode='multiple'] { + --checkbox-width: calc(var(--spacing) * 6.5); + } + + &[data-allows-dragging] { + --drag-button-width: var(--spacing-6); + } + + &[data-drop-target] { + outline: 2px solid var(--highlight-background); + outline-offset: -1px; + background: var(--highlight-overlay); + } + + .react-aria-DropIndicator { + &[data-drop-target] { + outline: 1px solid var(--highlight-background); + margin-left: calc( + var(--spacing-2) + var(--checkbox-width) + var(--drag-button-width) + var(--spacing-5) + + (var(--tree-item-level) - 1) * var(--spacing-4) + ); + } + } +} + +.react-aria-TreeItem { + display: flex; + align-items: center; + gap: var(--spacing-2); + min-height: var(--spacing-8); + padding: var(--spacing-1) var(--spacing-1) var(--spacing-1) var(--spacing-2); + box-sizing: border-box; + --padding: var(--spacing-4); + border-radius: var(--radius); + outline: none; + cursor: default; + color: var(--text-color); + font: var(--font-size) system-ui; + position: relative; + transform: translateZ(0); + transition-property: background, color, border-radius; + transition-duration: 200ms; + -webkit-tap-highlight-color: transparent; + --chevron-width: var(--spacing-5); + + &[data-has-child-items] { + --chevron-width: 0px; + } + + --border-color: var(--gray-300); + &:not(:last-child)::after { + content: ''; + display: block; + position: absolute; + bottom: 0; + inset-inline-start: calc( + var(--spacing-2) + var(--checkbox-width) + var(--drag-button-width) + var(--chevron-width) + + (var(--tree-item-level) - 1) * var(--padding) + ); + inset-inline-end: var(--spacing-2); + border-bottom: 0.5px solid var(--border-color); + } + + &[data-focus-visible] { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } + + &[data-pressed] { + background: var(--highlight-hover); + } + + &[data-selected] { + background: var(--highlight-background); + color: var(--highlight-foreground); + --focus-ring-color: var(--highlight-foreground); + --border-color: transparent; + + &[data-focus-visible] { + outline-color: var(--highlight-foreground); + outline-offset: -4px; + } + + &:has(+ [data-selected]), + &:has(+ .react-aria-DropIndicator + [data-selected]) { + border-end-start-radius: 0; + border-end-end-radius: 0; + --border-color: rgb(255 255 255 / 0.3); + } + + + [data-selected], + + .react-aria-DropIndicator + [data-selected] { + border-start-start-radius: 0; + border-start-end-radius: 0; + } + } + + &[data-href] { + cursor: pointer; + } + + &[data-disabled] { + color: var(--text-color-disabled); + } + + .react-aria-Button:not([slot]) { + margin-left: auto; + } + + &[data-allows-dragging] { + padding-left: 4px; + } + + &[data-dragging] { + opacity: 0.6; + } + + &[data-drop-target] { + outline: 2px solid var(--highlight-background); + background: var(--highlight-overlay); + } + + .react-aria-Button[slot='chevron'] { + all: unset; + display: flex; + visibility: hidden; + align-items: center; + justify-content: center; + width: var(--spacing-4); + height: 100%; + padding-left: calc((var(--tree-item-level) - 1) * var(--padding)); + + svg { + rotate: 0deg; + transition: rotate 200ms; + fill: none; + stroke: currentColor; + stroke-width: 3px; + } + } + + &[data-has-child-items] .react-aria-Button[slot='chevron'] { + visibility: visible; + } + + &[data-expanded] .react-aria-Button[slot='chevron'] svg { + rotate: 90deg; + } + + .react-aria-Button[slot='drag'] { + all: unset; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--spacing-4); + text-align: center; + + &[data-focus-visible] { + border-radius: 4px; + outline: 2px solid var(--focus-ring-color); + } + } +} + +.react-aria-TreeItem .react-aria-CheckboxButton { + --checkmark-color: var(--highlight-background); + &[data-selected] .indicator { + --indicator-color: var(--highlight-foreground); + --indicator-shadow: transparent; + } +} + +.react-aria-TreeLoader { + display: flex; + align-items: center; + justify-content: center; + height: 28px; +} diff --git a/packages/excel-collab/src/component/Tree.tsx b/packages/excel-collab/src/component/Tree.tsx new file mode 100644 index 00000000..425e7ea2 --- /dev/null +++ b/packages/excel-collab/src/component/Tree.tsx @@ -0,0 +1,84 @@ +'use client'; +import { + Button, + Tree as AriaTree, + TreeItem as AriaTreeItem, + TreeItemContent as AriaTreeItemContent, + type TreeItemContentProps, + type TreeItemContentRenderProps, + type TreeItemProps as AriaTreeItemProps, + type TreeProps, + TreeLoadMoreItem as AriaTreeLoadMoreItem, + type TreeLoadMoreItemProps, + TreeSection as AriaTreeSection, + TreeHeader as AriaTreeHeader +} from 'react-aria-components/Tree'; +import {ChevronRight, GripVertical} from 'lucide-react'; +import {Checkbox} from './Checkbox'; +import {ProgressCircle} from './ProgressCircle'; +import './Tree.css'; + +export function Tree(props: TreeProps) { + return ; +} + +export function TreeItemContent( + props: Omit & {children?: React.ReactNode} +) { + return ( + + {({selectionBehavior, selectionMode, allowsDragging}: TreeItemContentRenderProps) => ( + <> + {allowsDragging && ( + + )} + {selectionBehavior === 'toggle' && selectionMode !== 'none' && ( + + )} + + {props.children} + + )} + + ); +} + +export interface TreeItemProps extends Partial { + title?: React.ReactNode; +} + +export function TreeItem(props: TreeItemProps) { + let textValue = typeof props.title === 'string' ? props.title : ''; + return ( + + {props.title != null ? ( + <> + {props.title} + {props.children} + + ) : ( + props.children + )} + + ); +} + +export function TreeLoadMoreItem(props: TreeLoadMoreItemProps) { + return ( + + + + ); +} + +export function TreeSection(props: React.ComponentProps) { + return ; +} + +export function TreeHeader(props: React.ComponentProps) { + return ; +} diff --git a/packages/excel-collab/src/component/index.ts b/packages/excel-collab/src/component/index.ts new file mode 100644 index 00000000..8b166a86 --- /dev/null +++ b/packages/excel-collab/src/component/index.ts @@ -0,0 +1 @@ +export * from './Button'; diff --git a/packages/excel-collab/src/component/theme.css b/packages/excel-collab/src/component/theme.css new file mode 100644 index 00000000..272d07cd --- /dev/null +++ b/packages/excel-collab/src/component/theme.css @@ -0,0 +1,237 @@ +/* colors */ +:root { + /* Change this variable to set the theme color for all components. */ + /* You can use the below presets, or choose a custom color. */ + --tint: var(--excel-green); + + /* theme colors */ + /* lightness channel is only used as a multiplier for chroma (see below) */ + --gray: oklch(0.5 0 0); + --red: oklch(0.6 0.181447 27.0726); + --orange: oklch(0.7 0.150492 54); + --yellow: oklch(0.8 0.128516 73.8032); + --turquoise: oklch(0.5 0.081146 205.114); + --cyan: oklch(0.4 0.142107 243.926); + --green: oklch(0.5 0.121276 155.372); + --blue: oklch(0.5 0.22049 266.315); + --indigo: oklch(1 0.25049 284.23); + --purple: oklch(0.7 0.223324 302); + --pink: oklch(0.6 0.177717 347.813); + --excel-green: oklch(0.495 0.106 154.93); + + /** tint color scale */ + --tint-100: oklch(from var(--tint) var(--lightness-100) var(--chroma-100) h); + --tint-200: oklch(from var(--tint) var(--lightness-200) var(--chroma-200) h); + --tint-300: oklch(from var(--tint) var(--lightness-300) var(--chroma-300) h); + --tint-400: oklch(from var(--tint) var(--lightness-400) var(--chroma-400) h); + --tint-500: oklch(from var(--tint) var(--lightness-500) var(--chroma-500) h); + --tint-600: oklch(from var(--tint) var(--lightness-600) var(--chroma-600) h); + --tint-700: oklch(from var(--tint) var(--lightness-700) var(--chroma-700) h); + --tint-800: oklch(from var(--tint) var(--lightness-800) var(--chroma-800) h); + --tint-900: oklch(from var(--tint) var(--lightness-900) var(--chroma-900) h); + --tint-1000: oklch( + from var(--tint) var(--lightness-1000) var(--chroma-1000) h + ); + --tint-1100: oklch( + from var(--tint) var(--lightness-1100) var(--chroma-1100) h + ); + --tint-1200: oklch( + from var(--tint) var(--lightness-1200) var(--chroma-1200) h + ); + --tint-1300: oklch( + from var(--tint) var(--lightness-1300) var(--chroma-1300) h + ); + --tint-1400: oklch( + from var(--tint) var(--lightness-1400) var(--chroma-1400) h + ); + --tint-1500: oklch( + from var(--tint) var(--lightness-1500) var(--chroma-1500) h + ); + --tint-1600: oklch( + from var(--tint) var(--lightness-1600) var(--chroma-1600) h + ); + + /* gray scale */ + --gray-100: oklch(from var(--gray) var(--lightness-100) c h); + --gray-200: oklch(from var(--gray) var(--lightness-200) c h); + --gray-300: oklch(from var(--gray) var(--lightness-300) c h); + --gray-400: oklch(from var(--gray) var(--lightness-400) c h); + --gray-500: oklch(from var(--gray) var(--lightness-500) c h); + --gray-600: oklch(from var(--gray) var(--lightness-600) c h); + --gray-700: oklch(from var(--gray) var(--lightness-700) c h); + --gray-800: oklch(from var(--gray) var(--lightness-800) c h); + --gray-900: oklch(from var(--gray) var(--lightness-900) c h); + --gray-1000: oklch(from var(--gray) var(--lightness-1000) c h); + --gray-1100: oklch(from var(--gray) var(--lightness-1100) c h); + --gray-1200: oklch(from var(--gray) var(--lightness-1200) c h); + --gray-1300: oklch(from var(--gray) var(--lightness-1300) c h); + --gray-1400: oklch(from var(--gray) var(--lightness-1400) c h); + --gray-1500: oklch(from var(--gray) var(--lightness-1500) c h); + --gray-1600: oklch(from var(--gray) var(--lightness-1600) c h); +} + +/* light mode colors */ +:root { + --background-color: #ffffff; + --gray-50: #ffffff; + + --lightness-100: 98.1187%; + --lightness-200: 95.2045%; + --lightness-300: 91.1434%; + --lightness-400: 85.1751%; + --lightness-500: 79.1773%; + --lightness-600: 72.3297%; + --lightness-700: 67.0121%; + --lightness-800: 62.3039%; + --lightness-900: 57.9699%; + --lightness-1000: 51.9076%; + --lightness-1100: 46.9058%; + --lightness-1200: 41.0821%; + --lightness-1300: 35.3616%; + --lightness-1400: 29.6725%; + --lightness-1500: 24.5366%; + --lightness-1600: 16.6959%; + + /* lower chroma at low lightness levels */ + --chroma-100: calc(l * c * 0.5); + --chroma-200: calc(l * c * 0.6); + --chroma-300: calc(l * c * 0.7); + --chroma-400: calc(l * c * 0.8); + --chroma-500: calc(l * c * 0.9); + --chroma-600: c; + --chroma-700: c; + --chroma-800: c; + --chroma-900: c; + --chroma-1000: c; + --chroma-1100: c; + --chroma-1200: c; + --chroma-1300: c; + --chroma-1400: c; + --chroma-1500: c; + --chroma-1600: c; + + --highlight-hover: rgb(0 0 0 / 0.07); + --highlight-pressed: rgb(0 0 0 / 0.15); + --overlay-background: var(--gray-50); + --overlay-border: rgb(0 0 0 / 0.06); + --popover-shadow: 0 8px 20px rgba(0 0 0 / 0.12); +} + +html[data-theme='dark'] { + --background-color: #1a1a1a; + --gray-50: oklch(22% 0 0); + + --lightness-100: 29.6725%; + --lightness-200: 35.3616%; + --lightness-300: 41.0821%; + --lightness-400: 46.9058%; + --lightness-500: 51.9076%; + --lightness-600: 57.9699%; + --lightness-700: 56.1347%; + --lightness-800: 59.2866%; + --lightness-900: 62.3039%; + --lightness-1000: 67.0121%; + --lightness-1100: 72.3297%; + --lightness-1200: 79.1773%; + --lightness-1300: 85.1751%; + --lightness-1400: 91.1434%; + --lightness-1500: 95.2045%; + --lightness-1600: 100%; + + --highlight-hover: rgb(255 255 255 / 0.1); + --highlight-pressed: rgb(255 255 255 / 0.2); + --overlay-background: var(--gray-100); + --overlay-border: rgb(255 255 255 / 0.2); + --popover-shadow: 0 8px 20px rgba(0 0 0 / 0.5); +} + +/* Semantic colors */ +:root { + --focus-ring-color: var(--tint-1000); + --text-color: var(--gray-1200); + --text-color-hover: var(--gray-1300); + --text-color-disabled: var(--gray-600); + --text-color-placeholder: var(--gray-1000); + --link-color: var(--tint-1200); + --link-color-secondary: var(--gray-1200); + --link-color-pressed: var(--tint-1300); + --border-color: var(--gray-400); + --border-color-hover: var(--gray-500); + --border-color-disabled: var(--gray-300); + --field-text-color: var(--gray-1400); + --button-background: var(--tint-100); + --button-background-pressed: var(--tint-200); + /* these colors are the same between light and dark themes + * to ensure contrast with the foreground color */ + --highlight-background: oklch(from var(--tint) 55% c h); + --highlight-background-pressed: oklch(from var(--tint) 50% c h); + --highlight-background-invalid: oklch( + from var(--red) var(--lightness-900) c h + ); + --highlight-foreground: white; + --highlight-overlay: oklch(from var(--tint-1000) l c h / 15%); + --invalid-color: oklch(from var(--red) var(--lightness-1000) c h); + --field-background: var(--gray-50); +} + +/* Windows high contrast mode overrides */ +@media (forced-colors: active) { + :root { + --background-color: Canvas; + --focus-ring-color: Highlight; + --text-color: ButtonText; + --text-color-hover: ButtonText; + --text-color-disabled: GrayText; + --text-color-placeholder: ButtonText; + --link-color: LinkText; + --link-color-secondary: LinkText; + --link-color-pressed: LinkText; + --border-color: ButtonBorder; + --border-color-hover: ButtonBorder; + --border-color-pressed: ButtonBorder; + --border-color-disabled: GrayText; + --field-background: Field; + --field-text-color: FieldText; + --overlay-background: Canvas; + --overlay-border: ButtonBorder; + --button-background: ButtonFace; + --button-background-pressed: ButtonFace; + --highlight-background: Highlight; + --highlight-background-pressed: Highlight; + --highlight-background-invalid: LinkText; + --highlight-foreground: HighlightText; + --invalid-color: LinkText; + } +} + +/* typography and spacing scale */ +:root { + --font-size: 0.875rem; /* 14px */ + --font-size-sm: 0.75rem; /* 12px */ + --font-size-lg: 1rem; /* 16px */ + --radius: 8px; + --radius-sm: 6px; + --radius-lg: 10px; + --radius-xl: 16px; + --spacing: 0.25rem; /* 4px */ + --spacing-1: var(--spacing); + --spacing-2: calc(2 * var(--spacing)); + --spacing-3: calc(3 * var(--spacing)); + --spacing-4: calc(4 * var(--spacing)); + --spacing-5: calc(5 * var(--spacing)); + --spacing-6: calc(6 * var(--spacing)); + --spacing-7: calc(7 * var(--spacing)); + --spacing-8: calc(8 * var(--spacing)); + --spacing-9: calc(9 * var(--spacing)); + --spacing-10: calc(10 * var(--spacing)); +} + +/* Scale up hit targets on high resolution mobile devices. */ +@media (min-resolution: 200dpi) { + :root { + --spacing: calc(0.25rem * 1.25); + --font-size: 1.0625rem; /* 17px */ + --font-size-sm: 0.9375rem; /* 15px */ + --font-size-lg: 1.25rem; /* 20px */ + } +} diff --git a/packages/excel-collab/src/component/utilities.css b/packages/excel-collab/src/component/utilities.css new file mode 100644 index 00000000..f531a100 --- /dev/null +++ b/packages/excel-collab/src/component/utilities.css @@ -0,0 +1,287 @@ +@layer utilities { + /* utility that creates a button-like element, which can be optionally selected */ + .button-base { + --button-color: var(--tint); + --button-background: oklch(from var(--button-color) var(--lightness-100) var(--chroma-100) h); + --button-gradient: oklch(from var(--button-color) var(--lightness-200) var(--chroma-200) h); + --button-border: oklch(from var(--button-color) var(--lightness-300) var(--chroma-300) h); + --button-highlight: rgb(255 255 255 / 0.8); + --button-shadow: oklch(from var(--button-color) var(--lightness-400) var(--chroma-400) h); + --button-border-size: 1px; + --button-text: oklch(from var(--button-color) var(--lightness-1400) var(--chroma-1400) h); + --button-gradient-size: 8px; + + background: var(--button-background); + color: var(--button-text); + box-shadow: + inset 0 -1px 0 var(--button-shadow), + /* bottom shadow */ inset 0 0 0 var(--button-border-size) var(--button-border), + /* border */ inset 0px calc(var(--button-border-size) + 1px) 0px var(--button-highlight), + /* top specular highlight */ inset 0px calc(-1 * var(--button-gradient-size)) + var(--button-gradient-size) -2px var(--button-gradient); /* inner gradient */ + outline: none; + transition-property: background, color, scale, box-shadow; + transition-duration: 200ms; + will-change: scale; + forced-color-adjust: none; + -webkit-tap-highlight-color: transparent; + + @media (prefers-color-scheme: dark) { + --button-shadow: oklch(from var(--button-color) var(--lightness-200) var(--chroma-200) h); + --button-highlight: rgb(255 255 255 / 0.15); + box-shadow: + inset 0 var(--button-border-size) 0 var(--button-highlight), + /* top specular highlight */ inset 0 calc(-1 * var(--button-border-size)) 0 + var(--button-shadow), + /* bottom shadow */ inset 0 0 0 var(--button-border-size) var(--button-border), + /* border */ inset 0 var(--button-gradient-size) var(--button-gradient-size) -2px + var(--button-gradient); /* inner gradient */ + } + + &:where([data-pressed]) { + --button-background: oklch(from var(--button-color) var(--lightness-200) var(--chroma-200) h); + } + + &:where([data-focus-visible]) { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + &:where([data-variant='secondary']) { + --button-color: var(--gray); + } + + &:where([data-variant='quiet']) { + --button-background: none; + --button-text: var(--text-color); + box-shadow: 0 0 0 1px transparent; + + &:where([data-hovered], [data-pressed]) { + --button-background: var(--tint-200); + --button-text: var(--tint-1400); + box-shadow: 0 0 0 1px var(--tint-200); + } + } + + &:where([data-selected]) { + --button-background: oklch(from var(--button-color) 55% c h); + --button-border: oklch(from var(--button-color) 50% c h); + --button-gradient: var(--button-border); + --button-highlight: rgb(255 255 255 / 0.2); + --button-shadow: oklch(from var(--button-color) 30% c h); + --button-text: var(--highlight-foreground); + + box-shadow: + inset 0 -1px 0 var(--button-shadow), + /* bottom shadow */ inset 0 0 0 1px var(--button-border), + /* border */ inset 0 2px 0 var(--button-highlight), + /* top specular highlight */ inset 0 calc(-1 * var(--button-gradient-size)) + var(--button-gradient-size) var(--button-gradient); /* inner gradient */ + + @media (prefers-color-scheme: dark) { + --button-highlight: rgb(255 255 255 / 0.4); + --button-gradient: rgb(255 255 255 / 0.2); + --button-shadow: var(--button-border); + box-shadow: + inset 0 1px 0 var(--button-highlight), + /* top specular highlight */ inset 0 var(--button-gradient-size) + var(--button-gradient-size) var(--button-gradient), + /* inner gradient */ inset 0 0 0 1px var(--button-border); /* border */ + } + + &:where([data-pressed]) { + --button-background: oklch(from var(--button-color) 50% c h); + } + } + + &:where([data-disabled]) { + box-shadow: none; + --button-background: var(--border-color-disabled); + --button-text: var(--text-color-disabled); + + &:where([data-variant='quiet']) { + --button-background: none; + } + } + + @media (forced-colors: active) { + --button-background: ButtonFace; + --button-text: ButtonText; + --button-border: ButtonBorder; + box-shadow: inset 0 0 0 var(--button-border-size) var(--button-border); + + &:where([data-variant='quiet']) { + --button-border: transparent; + &:where([data-hovered], [data-pressed]) { + --button-border: ButtonBorder; + } + } + + &:where([data-selected]) { + --button-background: Highlight; + --button-text: HighlightText; + --button-border: Highlight; + } + + &:where([data-disabled]) { + --button-background: ButtonFace; + --button-text: GrayText; + --button-border: GrayText; + + &:where([data-variant='quiet']) { + --button-border: transparent; + } + } + } + } + + /* utility that creates a small indicator, such as a checkbox, radio, switch, or slider thumb */ + .indicator { + --indicator-color: var(--gray); + --indicator-background: oklch( + from var(--indicator-color) var(--lightness-100) var(--chroma-100) h + ); + --indicator-border: oklch(from var(--indicator-color) var(--lightness-800) var(--chroma-800) h); + --indicator-drop-shadow: 0 0; + + background: var(--indicator-background); + box-shadow: + inset 0 0 0 1px var(--indicator-border), + /* border */ inset 0 2px 0 white, + /* top specular highlight */ inset 0 -4px 2px + oklch(from var(--indicator-color) 30% c h / 0.08), + /* inner gradient */ var(--indicator-drop-shadow); /* optional drop shadow */ + will-change: scale; + + @media (prefers-color-scheme: dark) { + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.4), + /* top specular highlight */ inset 0 4px 2px rgb(255 255 255 / 0.1), + /* inner shadow */ inset 0 0 0 1px var(--indicator-border), + /* border */ var(--indicator-drop-shadow); /* optional drop shadow */ + } + + @media (forced-colors: active) { + --indicator-background: ButtonFace; + --indicator-border: ButtonBorder; + box-shadow: inset 0 0 0 1px var(--indicator-border); + } + + &[data-pressed], + [data-pressed] & { + scale: 0.9; + } + + [data-selected] > &, + [data-indeterminate] > & { + --indicator-color: var(--highlight-background); + --indicator-background: var(--indicator-color); + --indicator-highlight: rgb(255 255 255 / 0.3); + --indicator-shadow: oklch(from var(--indicator-color) 45% c h); + --indicator-border: var(--indicator-background); + box-shadow: + inset 0 -1px 0 var(--indicator-shadow), + /* bottom shadow */ inset 0 0 0 1px var(--indicator-border), + /* border */ inset 0 2px 0 var(--indicator-highlight), + /* top specular highlight */ var(--indicator-drop-shadow); /* optional drop shadow */ + + @media (prefers-color-scheme: dark) { + --indicator-highlight: rgb(255 255 255 / 0.5); + --indicator-gradient: rgb(255 255 255 / 0.12); + box-shadow: + inset 0 1px 0 var(--indicator-highlight), + /* top specular highlight */ inset 0 4px 2px var(--indicator-gradient), + /* inner gradient */ inset 0 0 0 1px var(--indicator-border), + /* border */ var(--indicator-drop-shadow); /* optional drop shadow */ + } + + @media (forced-colors: active) { + box-shadow: none; + } + } + + &[data-invalid], + [data-invalid] > & { + --indicator-color: var(--invalid-color); + + @media (forced-colors: active) { + --indicator-border: var(--invalid-color); + } + } + + &[data-focus-visible], + [data-focus-visible] > & { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + &[data-disabled], + [data-disabled] > & { + background: var(--field-background); + box-shadow: inset 0 0 0 1px var(--border-color-disabled); + } + } + + /* utility that creates an inset effect, used for form fields, slider/progress tracks, etc. */ + .inset { + --inset-background: var(--field-background); + --inset-border: var(--border-color); + --inset-border-size: 1px; + --inset-shadow-offset: 2px; + --inset-shadow-size: 4px; + + background: var(--inset-background); + box-shadow: + inset 0 0 0 var(--inset-border-size) var(--inset-border), + /* border */ inset 0 var(--inset-shadow-offset) var(--inset-shadow-size) rgb(0 0 0 / 0.15), + /* inner shadow */ 0 1px 0 var(--gray-50); /* bottom specular highlight */ + transition: box-shadow 200ms; + forced-color-adjust: none; + + @media (prefers-color-scheme: dark) { + --inset-border: var(--gray-200); + --border-color-hover: var(--gray-300); + --inset-highlight: var(--gray-400); + --inset-shadow-size: 6px; + box-shadow: + inset 0 calc(-1 * var(--inset-border-size)) 0 var(--inset-highlight), + /* bottom specular highlight */ inset 0 0 0 var(--inset-border-size) var(--inset-border), + /* border */ inset 0 1px var(--inset-shadow-size) rgb(0 0 0); /* inner shadow */ + } + + &:where([data-hovered], [data-pressed]) { + --inset-border: var(--border-color-hover); + } + + @media (forced-colors: active) { + --inset-border: ButtonBorder; + box-shadow: inset 0 0 0 var(--inset-border-size) var(--inset-border); + } + + &:where([data-invalid]) { + --inset-border: var(--invalid-color); + --inset-highlight: var(--inset-border); + } + + &:where([data-disabled]) { + box-shadow: inset 0 0 0 1px var(--border-color-disabled); + } + + &.track { + --inset-shadow-offset: 1px; + --inset-shadow-size: 3px; + + @media (prefers-color-scheme: light) { + --inset-background: var(--gray-300); + --inset-border: var(--gray-500); + --inset-border-size: 0.5px; + } + + @media (forced-colors: active) { + --inset-background: Field; + --inset-border: ButtonBorder; + --inset-border-size: 1px; + } + } + } +} diff --git a/packages/excel-collab/src/components/BaseIcon/BaseIcon.module.css b/packages/excel-collab/src/components/BaseIcon/BaseIcon.module.css deleted file mode 100644 index deb8fb79..00000000 --- a/packages/excel-collab/src/components/BaseIcon/BaseIcon.module.css +++ /dev/null @@ -1,6 +0,0 @@ -.baseIcon { - width: 1em; - height: 1em; - fill: currentcolor; - overflow: hidden; -} diff --git a/packages/excel-collab/src/components/BaseIcon/BaseIcon.tsx b/packages/excel-collab/src/components/BaseIcon/BaseIcon.tsx deleted file mode 100644 index f59edbd5..00000000 --- a/packages/excel-collab/src/components/BaseIcon/BaseIcon.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { classnames } from '../../util'; -import styles from './BaseIcon.module.css'; -import { FunctionComponent } from 'react'; - -interface PathItem { - d: string; - 'fill-opacity'?: string; -} - -export interface BaseIconProps { - className?: string; - paths: PathItem[]; - testId?: string; -} - -export const BaseIcon: FunctionComponent = ({ - className = '', - paths, - testId, -}) => { - return ( - - {paths.map((item, i) => ( - - ))} - - ); -}; -BaseIcon.displayName = 'BaseIcon'; diff --git a/packages/excel-collab/src/components/BaseIcon/FillColorIcon.tsx b/packages/excel-collab/src/components/BaseIcon/FillColorIcon.tsx deleted file mode 100644 index 65e8bc03..00000000 --- a/packages/excel-collab/src/components/BaseIcon/FillColorIcon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { memo } from 'react'; -import { BaseIcon } from './BaseIcon'; - -export const FillColorIcon = memo(() => { - return BaseIcon({ - paths: [ - { - d: 'M0 0h1024v1024H0z', - 'fill-opacity': '.01', - }, - { - d: 'M496.512 32a128 128 0 0 1 127.84 121.6l0.16 6.4-0.032 113.504 264.256 264.256a32 32 0 0 1-8.16 51.2l-144.064 72.96-269.12 269.12a64 64 0 0 1-90.496 0l-294.144-294.176a64 64 0 0 1 0-90.496l286.016-286.08-0.192-2.048-0.064-2.08V160a128 128 0 0 1 128-128z m-6.464 197.568L128 591.616l294.144 294.144 276.32-276.32 113.792-57.632-187.776-187.776V416a32 32 0 0 1-28.256 31.776l-3.712 0.224a32 32 0 0 1-31.808-28.256L560.512 416l-0.032-115.968-70.432-70.464z m402.016 395.936l1.792 2.24 5.472 8.416c30.112 46.752 45.184 80.032 45.184 99.84a64 64 0 1 1-128 0c0-20.96 16.864-57.024 50.624-108.224a16 16 0 0 1 24.928-2.24zM496.512 96a64 64 0 0 0-63.84 59.2l-0.16 4.8-0.032 36.576 34.944-34.88a32 32 0 0 1 45.248 0l47.808 47.808V160a64 64 0 0 0-59.2-63.84L496.48 96z', - }, - ], - }); -}); -FillColorIcon.displayName = 'FillColorIcon'; diff --git a/packages/excel-collab/src/components/BaseIcon/icon.ts b/packages/excel-collab/src/components/BaseIcon/icon.ts deleted file mode 100644 index 2ac2e1d8..00000000 --- a/packages/excel-collab/src/components/BaseIcon/icon.ts +++ /dev/null @@ -1,133 +0,0 @@ -export type BaseIconName = - | 'alignCenter' - | 'alignLeft' - | 'alignRight' - | 'bold' - | 'fontColor' - | 'italic' - | 'middleAlign' - | 'plus' - | 'redo' - | 'underline' - | 'undo' - | 'success' - | 'info' - | 'warning' - | 'error' - | 'rotate' - | 'moon' - | 'sun' - | 'down' - | 'menu' - | 'confirm' - | 'horizontalLeft' - | 'horizontalRight' - | 'horizontalCenter' - | 'verticalTop' - | 'verticalMiddle' - | 'verticalBottom' - | 'connection' - | 'disconnection' - | 'syncing' - | 'github'; - -const icon: Record = { - alignCenter: [ - 'M142.2 227.6h739.6v56.9H142.2zM142.2 568.9h739.6v56.9H142.2zM256 398.2h512v56.9H256zM256 739.6h512v56.9H256z', - ], - alignLeft: [ - 'M627.712 788.48c11.305 0 20.48-9.155 20.48-20.48s-9.175-20.48-20.48-20.48H218.46c-11.305 0-20.48 9.155-20.48 20.48s9.175 20.48 20.48 20.48h409.252zM832.86 583.68h-614.4c-11.305 0-20.48 9.155-20.48 20.48s9.175 20.48 20.48 20.48h614.4c11.305 0 20.48-9.155 20.48-20.48s-9.175-20.48-20.48-20.48zM832.86 256h-614.4c-11.305 0-20.48 9.155-20.48 20.48s9.175 20.48 20.48 20.48h614.4c11.305 0 20.48-9.155 20.48-20.48S844.165 256 832.86 256zM218.46 460.8h409.252c11.305 0 20.48-9.155 20.48-20.48s-9.175-20.48-20.48-20.48H218.46c-11.305 0-20.48 9.155-20.48 20.48s9.155 20.48 20.48 20.48z', - ], - alignRight: [ - 'M832.86 747.52H423.588a20.48 20.48 0 1 0 0 40.96H832.84a20.48 20.48 0 0 0 0.02-40.96z m20.48-143.36a20.48 20.48 0 0 0-20.48-20.48h-614.4a20.48 20.48 0 1 0 0 40.96h614.4a20.48 20.48 0 0 0 20.48-20.48zM832.86 256h-614.4a20.48 20.48 0 1 0 0 40.96h614.4a20.48 20.48 0 1 0 0-40.96z m0 163.84H423.588a20.48 20.48 0 1 0 0 40.96H832.84a20.48 20.48 0 0 0 0.02-40.96z', - ], - bold: [ - 'M724.342857 477.028571c38.4-40 61.942857-94.057143 61.942857-153.485714v-11.657143C786.285714 188.914286 685.6 89.142857 561.485714 89.142857H223.314286c-17.257143 0-31.314286 14.057143-31.314286 31.314286v776.114286c0 18.628571 15.085714 33.714286 33.714286 33.714285h364.228571c133.714286 0 242.057143-107.657143 242.057143-240.571428v-12.571429c0-83.428571-42.742857-156.914286-107.657143-200.114286zM301.714286 198.857143h256.8c65.257143 0 118.057143 50.742857 118.057143 113.485714v10.857143c0 62.628571-52.914286 113.485714-118.057143 113.485714H301.714286V198.857143z m418.971428 490.742857c0 71.885714-59.085714 130.171429-132 130.171429H301.714286V547.085714h286.971428c72.914286 0 132 58.285714 132 130.171429v12.342857z', - ], - fontColor: [ - 'M650.496 597.333333H373.504l-68.266667 170.666667H213.333333l256-640h85.333334l256 640h-91.904l-68.266667-170.666667z m-34.133333-85.333333L512 251.093333 407.637333 512h208.725334zM128 853.333333h768v85.333334H128v-85.333334z', - ], - italic: [ - 'M219.428571 949.714286l9.714286-48.571429q3.428571-1.142857 46.571429-12.285714t63.714286-21.428571q16-20 23.428571-57.714286 0.571429-4 35.428571-165.142857t65.142857-310.571429 29.714286-169.428571l0-14.285714q-13.714286-7.428571-31.142857-10.571429t-39.714286-4.571429-33.142857-3.142857l10.857143-58.857143q18.857143 1.142857 68.571429 3.714286t85.428571 4 68.857143 1.428571q27.428571 0 56.285714-1.428571t69.142857-4 56.285714-3.714286q-2.857143 22.285714-10.857143 50.857143-17.142857 5.714286-58 16.285714t-62 19.142857q-4.571429 10.857143-8 24.285714t-5.142857 22.857143-4.285714 26-3.714286 24q-15.428571 84.571429-50 239.714286t-44.285714 203.142857q-1.142857 5.142857-7.428571 33.142857t-11.428571 51.428571-9.142857 47.714286-3.428571 32.857143l0.571429 10.285714q9.714286 2.285714 105.714286 17.714286-1.714286 25.142857-9.142857 56.571429-6.285714 0-18.571429 0.857143t-18.571429 0.857143q-16.571429 0-49.714286-5.714286t-49.142857-5.714286q-78.857143-1.142857-117.714286-1.142857-29.142857 0-81.714286 5.142857t-69.142857 6.285714z', - ], - middleAlign: [ - 'M740.43392 788.48c11.30496 0 20.48-9.15456 20.48-20.48s-9.17504-20.48-20.48-20.48L331.18208 747.52c-11.30496 0-20.48 9.15456-20.48 20.48s9.17504 20.48 20.48 20.48L740.43392 788.48zM863.49824 604.16c0-11.32544-9.17504-20.48-20.48-20.48l-614.4 0c-11.30496 0-20.48 9.15456-20.48 20.48s9.17504 20.48 20.48 20.48l614.4 0C854.3232 624.64 863.49824 615.48544 863.49824 604.16zM208.13824 276.48c0 11.32544 9.17504 20.48 20.48 20.48l614.4 0c11.30496 0 20.48-9.15456 20.48-20.48s-9.17504-20.48-20.48-20.48l-614.4 0C217.2928 256 208.13824 265.15456 208.13824 276.48zM740.43392 460.8c11.30496 0 20.48-9.15456 20.48-20.48s-9.17504-20.48-20.48-20.48L331.18208 419.84c-11.30496 0-20.48 9.15456-20.48 20.48s9.17504 20.48 20.48 20.48L740.43392 460.8z', - ], - plus: [ - 'M896 468.571429H555.428571V100.571429h-86.857142v368H128c-5.028571 0-9.142857 4.114286-9.142857 9.142857v68.571428c0 5.028571 4.114286 9.142857 9.142857 9.142857h340.571429v368h86.857142V555.428571h340.571429c5.028571 0 9.142857-4.114286 9.142857-9.142857v-68.571428c0-5.028571-4.114286-9.142857-9.142857-9.142857z', - ], - redo: [ - 'M611.783111 569.344L549.622519 644.740741h284.444444l-65.498074-265.481482-59.922963 72.666074c-35.422815-28.48237-108.278519-68.342519-238.667852-68.342518-202.827852 0-280.651852 206.01363-280.651852 206.013629s116.318815-132.778667 246.215111-132.778666c97.204148-0.037926 153.865481 74.827852 176.241778 112.526222z', - ], - underline: [ - 'M512 725.333333c166.4 0 298.666667-132.266667 298.666667-298.666666V128c0-25.6-17.066667-42.666667-42.666667-42.666667s-42.666667 17.066667-42.666667 42.666667v298.666667c0 119.466667-93.866667 213.333333-213.333333 213.333333s-213.333333-93.866667-213.333333-213.333333V128c0-25.6-17.066667-42.666667-42.666667-42.666667s-42.666667 17.066667-42.666667 42.666667v298.666667c0 166.4 132.266667 298.666667 298.666667 298.666666zM853.333333 853.333333H170.666667c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667h682.666666c25.6 0 42.666667-17.066667 42.666667-42.666667s-17.066667-42.666667-42.666667-42.666667z', - ], - undo: [ - 'M489.244444 568.888889l60.681482 75.851852H265.481481l64.474075-265.481482 60.681481 72.05926c34.133333-30.340741 109.985185-68.266667 238.933333-68.266667 201.007407 0 280.651852 204.8 280.651852 204.8S792.651852 455.111111 663.703704 455.111111c-98.607407 0-155.496296 75.851852-174.45926 113.777778z', - ], - success: [ - 'M666.272 472.288l-175.616 192a31.904 31.904 0 0 1-23.616 10.4h-0.192a32 32 0 0 1-23.68-10.688l-85.728-96a32 32 0 1 1 47.744-42.624l62.144 69.6 151.712-165.888a32 32 0 1 1 47.232 43.2m-154.24-344.32C300.224 128 128 300.32 128 512c0 211.776 172.224 384 384 384 211.68 0 384-172.224 384-384 0-211.68-172.32-384-384-384', - ], - info: [ - 'M512 97.52381c228.912762 0 414.47619 185.563429 414.47619 414.47619s-185.563429 414.47619-414.47619 414.47619S97.52381 740.912762 97.52381 512 283.087238 97.52381 512 97.52381z m36.571429 341.333333h-73.142858v292.571428h73.142858V438.857143z m0-121.904762h-73.142858v73.142857h73.142858v-73.142857z', - ], - warning: [ - 'M512 64q190.016 4.992 316.512 131.488T960 512q-4.992 190.016-131.488 316.512T512 960q-190.016-4.992-316.512-131.488T64 512q4.992-190.016 131.488-316.512T512 64z m0 192q-26.016 0-43.008 19.008T453.984 320l23.008 256q2.016 14.016 11.488 22.496t23.488 8.512 23.488-8.512 11.488-22.496l23.008-256q2.016-26.016-15.008-44.992T511.936 256z m0 512q22.016-0.992 36.512-15.008t14.496-36-14.496-36.512T512 665.984t-36.512 14.496-14.496 36.512 14.496 36T512 768z', - ], - error: [ - 'M512 97.52381c228.912762 0 414.47619 185.563429 414.47619 414.47619s-185.563429 414.47619-414.47619 414.47619S97.52381 740.912762 97.52381 512 283.087238 97.52381 512 97.52381z m129.29219 233.447619l-129.267809 129.29219-129.316571-129.29219-51.736381 51.736381 129.316571 129.267809-129.316571 129.316571 51.736381 51.736381L512 563.687619l129.29219 129.316571 51.736381-51.73638L563.687619 512l129.316571-129.29219-51.73638-51.736381z', - ], - rotate: [ - 'M934.08 416.448h-220.352a46.4 46.4 0 0 1-47.232-47.232c0-26.752 20.48-47.232 47.232-47.232h173.184V148.8c0-26.752 20.48-47.168 47.168-47.168 26.816 0 47.232 20.48 47.232 47.168V369.28a47.36 47.36 0 0 1-47.232 47.232z', - 'M509.056 978.432A470.976 470.976 0 0 1 38.4 507.712 470.976 470.976 0 0 1 509.056 37.12a472.32 472.32 0 0 1 434.56 288.064c9.408 23.68-1.6 51.968-25.216 61.44-23.68 9.408-51.968-1.6-61.44-25.216a377.92 377.92 0 0 0-347.904-229.824 376.192 376.192 0 1 0 0 752.448c190.528 0 351.104-141.696 373.12-328.96a47.36 47.36 0 0 1 51.904-40.96 47.36 47.36 0 0 1 40.96 51.904 469.504 469.504 0 0 1-465.92 412.48z', - ], - moon: [ - 'M735.996 244.2C693.136 221.106 644.1 208 592 208c-167.894 0-304 136.106-304 304s136.106 304 304 304c52.1 0 101.136-13.106 143.996-36.2C621.368 744.742 538 638.108 538 512s83.368-232.742 197.996-267.8z m26.32 86.064C683.17 354.472 628 427.888 628 512s55.168 157.528 134.318 181.736c76.442 23.378 86.74 127.376 16.37 165.294C721.764 889.702 657.958 906 592 906c-217.6 0-394-176.4-394-394S374.4 118 592 118c65.96 0 129.764 16.298 186.688 46.97 70.372 37.918 60.072 141.916-16.37 165.294z', - ], - sun: [ - 'M512 768c-140.8 0-256-115.2-256-256s115.2-256 256-256 256 115.2 256 256-115.2 256-256 256z m0-426.666667c-93.866667 0-170.666667 76.8-170.666667 170.666667s76.8 170.666667 170.666667 170.666667 170.666667-76.8 170.666667-170.666667-76.8-170.666667-170.666667-170.666667zM512 170.666667c-25.6 0-42.666667-17.066667-42.666667-42.666667V42.666667c0-25.6 17.066667-42.666667 42.666667-42.666667s42.666667 17.066667 42.666667 42.666667v85.333333c0 25.6-17.066667 42.666667-42.666667 42.666667zM512 1024c-25.6 0-42.666667-17.066667-42.666667-42.666667v-85.333333c0-25.6 17.066667-42.666667 42.666667-42.666667s42.666667 17.066667 42.666667 42.666667v85.333333c0 25.6-17.066667 42.666667-42.666667 42.666667zM238.933333 281.6c-12.8 0-21.333333-4.266667-29.866666-12.8L149.333333 209.066667c-17.066667-17.066667-17.066667-42.666667 0-59.733334s42.666667-17.066667 59.733334 0l59.733333 59.733334c17.066667 17.066667 17.066667 42.666667 0 59.733333-4.266667 8.533333-17.066667 12.8-29.866667 12.8zM844.8 887.466667c-12.8 0-21.333333-4.266667-29.866667-12.8l-59.733333-59.733334c-17.066667-17.066667-17.066667-42.666667 0-59.733333s42.666667-17.066667 59.733333 0l59.733334 59.733333c17.066667 17.066667 17.066667 42.666667 0 59.733334-8.533333 8.533333-21.333333 12.8-29.866667 12.8zM128 554.666667H42.666667c-25.6 0-42.666667-17.066667-42.666667-42.666667s17.066667-42.666667 42.666667-42.666667h85.333333c25.6 0 42.666667 17.066667 42.666667 42.666667s-17.066667 42.666667-42.666667 42.666667zM981.333333 554.666667h-85.333333c-25.6 0-42.666667-17.066667-42.666667-42.666667s17.066667-42.666667 42.666667-42.666667h85.333333c25.6 0 42.666667 17.066667 42.666667 42.666667s-17.066667 42.666667-42.666667 42.666667zM179.2 887.466667c-12.8 0-21.333333-4.266667-29.866667-12.8-17.066667-17.066667-17.066667-42.666667 0-59.733334l59.733334-59.733333c17.066667-17.066667 42.666667-17.066667 59.733333 0s17.066667 42.666667 0 59.733333l-59.733333 59.733334c-8.533333 8.533333-17.066667 12.8-29.866667 12.8zM785.066667 281.6c-12.8 0-21.333333-4.266667-29.866667-12.8-17.066667-17.066667-17.066667-42.666667 0-59.733333l59.733333-59.733334c17.066667-17.066667 42.666667-17.066667 59.733334 0s17.066667 42.666667 0 59.733334l-59.733334 59.733333c-8.533333 8.533333-21.333333 12.8-29.866666 12.8z', - ], - down: [ - 'M512.146286 619.52L245.296762 352.792381 193.584762 404.48l318.585905 318.415238 318.268952-318.415238-51.736381-51.687619z', - ], - menu: [ - 'M904 160H120c-4.4 0-8 3.6-8 8v64c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-64c0-4.4-3.6-8-8-8zM904 784H120c-4.4 0-8 3.6-8 8v64c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-64c0-4.4-3.6-8-8-8zM904 472H120c-4.4 0-8 3.6-8 8v64c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-64c0-4.4-3.6-8-8-8z', - ], - confirm: [ - 'M394.792 797.429c-9.71 0-19.443-3.513-27.137-10.622L87.488 527.917c-16.225-14.993-17.224-40.3-2.231-56.525 14.993-16.226 40.3-17.225 56.524-2.231l280.167 258.89c16.225 14.993 17.224 40.3 2.231 56.524-7.883 8.532-18.62 12.854-29.387 12.854z', - 'M394.808 797.429c-10.555 0-21.088-4.15-28.949-12.391-15.249-15.984-14.652-41.304 1.333-56.553l514.564-490.858c15.983-15.249 41.303-14.652 56.553 1.333 15.248 15.985 14.651 41.305-1.334 56.553l-514.563 490.86c-7.745 7.387-17.684 11.056-27.604 11.056z', - ], - horizontalLeft: [ - 'M484.693 477.013h-371.2a32 32 0 0 0-32 32v32a32 32 0 0 0 32 32h371.2a32 32 0 0 0 32-32v-32a32 32 0 0 0-32-32z m435.2-338.133h-806.4a32 32 0 0 0-32 31.787v32a32 32 0 0 0 32 32h806.4a32 32 0 0 0 32-32v-32a32 32 0 0 0-32-32z m0 674.133h-806.4a32 32 0 0 0-32 32v32a32 32 0 0 0 32 32h806.4a32 32 0 0 0 32-32v-32a32 32 0 0 0-32-32z', - ], - horizontalRight: [ - 'M922.453 472.107H551.04a32 32 0 0 0-32 32v32a32 32 0 0 0 32 32h371.413a32 32 0 0 0 32-32v-32a32 32 0 0 0-32-32z m0 337.066H115.84a32 32 0 0 0-32 32v32a32 32 0 0 0 32 32h806.613a32 32 0 0 0 32-32v-32a32 32 0 0 0-32-32z m0-674.133H115.84a32 32 0 0 0-32 32v32a32 32 0 0 0 32 32h806.613a32 32 0 0 0 32-32v-32a32 32 0 0 0-32-32z', - ], - horizontalCenter: [ - 'M697.6 472.107H326.4a32 32 0 0 0-32 32v32a32 32 0 0 0 32 32h371.2a32 32 0 0 0 32-32v-32a32 32 0 0 0-32-32zM915.2 135.04H108.8a32 32 0 0 0-32 32v32a32 32 0 0 0 32 32h806.4a32 32 0 0 0 32-32v-32a32 32 0 0 0-32-32z m0 674.133H108.8a32 32 0 0 0-32 32v32a32 32 0 0 0 32 32h806.4a32 32 0 0 0 32-32v-32a32 32 0 0 0-32-32z', - ], - verticalTop: [ - 'M902.890667 125.482667H121.109333a9.045333 9.045333 0 0 0-9.109333 8.981333v67.413333c0 4.949333 4.053333 9.002667 9.109333 9.002667h781.781334c5.056 0 9.109333-4.053333 9.109333-9.002667v-67.413333a9.045333 9.045333 0 0 0-9.109333-8.981333z m-383.808 210.112a8.96 8.96 0 0 0-14.165334 0l-125.845333 159.210666a8.96 8.96 0 0 0 7.082667 14.506667h83.029333v380.224c0 4.928 4.053333 8.981333 9.002667 8.981333h67.413333c4.949333 0 8.981333-4.053333 8.981333-8.981333V509.418667H637.866667a8.96 8.96 0 0 0 7.082666-14.506667l-125.866666-159.317333z', - ], - verticalMiddle: [ - 'M512 614.4l117.028571 153.6h-73.142857V950.857143H468.114286v-182.857143h-73.142857L512 614.4z m438.857143-153.6v102.4H73.142857V460.8h877.714286zM555.885714 73.142857v182.857143h73.142857L512 409.6 394.971429 256h73.142857V73.142857h87.771428z', - ], - verticalBottom: [ - 'M902.890667 813.12H121.109333a9.045333 9.045333 0 0 0-9.109333 9.002667v67.413333c0 4.928 4.053333 8.981333 9.109333 8.981333h781.781334c5.056 0 9.109333-4.053333 9.109333-8.981333v-67.413333a9.045333 9.045333 0 0 0-9.109333-9.002667zM504.917333 688.426667a8.96 8.96 0 0 0 14.165334 0l125.845333-159.210667a8.96 8.96 0 0 0-7.082667-14.506667h-83.264V134.464a9.024 9.024 0 0 0-8.981333-8.981333h-67.413333a9.024 9.024 0 0 0-9.002667 8.981333v380.117333h-83.029333a8.96 8.96 0 0 0-7.082667 14.506667l125.866667 159.317333z', - ], - connection: [ - 'M640 576c105.856 0 201.856 43.072 271.392 112.608l-90.464 90.464c-46.368-46.368-110.368-75.072-180.928-75.072s-134.592 28.704-180.928 75.072l-90.464-90.464c69.536-69.536 165.536-112.608 271.392-112.608zM187.456 507.456c120.864-120.864 281.6-187.456 452.544-187.456s331.68 66.56 452.544 187.456l-90.496 90.496c-96.704-96.704-225.28-149.952-362.048-149.952s-265.344 53.248-362.048 149.952l-90.496-90.496zM988.768 134.432c106.688 45.12 202.528 109.728 284.768 192l0 0-90.496 90.496c-145.056-145.056-337.92-224.928-543.072-224.928s-398.016 79.872-543.072 224.928l-90.496-90.496c82.272-82.272 178.08-146.848 284.768-192 110.496-46.752 227.84-70.432 348.768-70.432s238.272 23.712 348.768 70.432zM576 896c0-35.36 28.64-64 64-64s64 28.64 64 64c0 35.36-28.64 64-64 64s-64-28.64-64-64z', - ], - disconnection: [ - 'M296.192 426.1376A440.8832 440.8832 0 0 0 199.68 499.2a40.96 40.96 0 1 0 51.2 63.744 43.5712 43.5712 0 0 0 6.144-6.144 359.0656 359.0656 0 0 1 95.8464-67.2256zM825.6 499.2A441.1904 441.1904 0 0 0 655.36 393.0112l155.1872 173.6192a39.3728 39.3728 0 0 0 15.36-9.8304 40.2944 40.2944 0 0 0 0.5632-57.0368zM164.1984 278.528a637.696 637.696 0 0 0-96.9728 78.592 40.96 40.96 0 0 0 28.7744 69.7856A39.1168 39.1168 0 0 0 128 414.72a554.3936 554.3936 0 0 1 91.2384-74.6496zM433.152 579.3792a205.1584 205.1584 0 0 0-65.792 43.3664 40.2432 40.2432 0 0 0-0.5632 56.9856l0.5632 0.6144a41.6768 41.6768 0 0 0 57.6 0A122.88 122.88 0 0 1 491.9296 645.12zM958.1056 359.0656A629.76 629.76 0 0 0 512 172.8c-17.2544 0-34.5088 0.7168-51.6608 2.1504l72.8064 81.4592a545.5872 545.5872 0 0 1 367.36 160.256 40.96 40.96 0 0 0 57.6-57.6zM512.6656 735.3856H512a54.4768 54.4768 0 1 0 56.9856 57.6 56.9344 56.9344 0 0 0-56.32-57.6z', - 'M195.047336 166.99934m25.07662-22.421086l0 0q25.07662-22.421086 47.497705 2.655534l534.181517 597.449514q22.421086 25.07662-2.655534 47.497706l0 0q-25.07662 22.421086-47.497705-2.655534l-534.181517-597.449514q-22.421086-25.07662 2.655534-47.497706Z', - ], - syncing: [ - 'M148.935111 380.131556l2.56 2.616888 120.092445 168.163556a11.377778 11.377778 0 0 1-6.257778 17.578667L262.371556 568.888889H175.388444a341.447111 341.447111 0 0 0 558.478223 202.524444 28.444444 28.444444 0 0 1 36.977777 43.235556A398.336 398.336 0 0 1 117.76 568.888889H22.186667a11.377778 11.377778 0 0 1-10.922667-14.734222l1.649778-3.242667 120.092444-168.163556a11.377778 11.377778 0 0 1 15.928889-2.616888zM512 113.777778a398.336 398.336 0 0 1 394.24 341.333333h95.630222a11.377778 11.377778 0 0 1 10.922667 14.734222l-1.649778 3.242667-120.092444 168.163556a11.377778 11.377778 0 0 1-15.928889 2.616888l-2.616889-2.616888-120.092445-168.163556a11.377778 11.377778 0 0 1 6.257778-17.578667L761.628444 455.111111h86.926223A341.447111 341.447111 0 0 0 290.133333 252.586667a28.444444 28.444444 0 1 1-36.977777-43.235556A396.686222 396.686222 0 0 1 512 113.777778z', - ], - github: [ - 'M512 42.666667A464.64 464.64 0 0 0 42.666667 502.186667 460.373333 460.373333 0 0 0 363.52 938.666667c23.466667 4.266667 32-9.813333 32-22.186667v-78.08c-130.56 27.733333-158.293333-61.44-158.293333-61.44a122.026667 122.026667 0 0 0-52.053334-67.413333c-42.666667-28.16 3.413333-27.733333 3.413334-27.733334a98.56 98.56 0 0 1 71.68 47.36 101.12 101.12 0 0 0 136.533333 37.973334 99.413333 99.413333 0 0 1 29.866667-61.44c-104.106667-11.52-213.333333-50.773333-213.333334-226.986667a177.066667 177.066667 0 0 1 47.36-124.16 161.28 161.28 0 0 1 4.693334-121.173333s39.68-12.373333 128 46.933333a455.68 455.68 0 0 1 234.666666 0c89.6-59.306667 128-46.933333 128-46.933333a161.28 161.28 0 0 1 4.693334 121.173333A177.066667 177.066667 0 0 1 810.666667 477.866667c0 176.64-110.08 215.466667-213.333334 226.986666a106.666667 106.666667 0 0 1 32 85.333334v125.866666c0 14.933333 8.533333 26.88 32 22.186667A460.8 460.8 0 0 0 981.333333 502.186667 464.64 464.64 0 0 0 512 42.666667', - ], -}; - -export default icon; diff --git a/packages/excel-collab/src/components/BaseIcon/index.tsx b/packages/excel-collab/src/components/BaseIcon/index.tsx deleted file mode 100644 index 0d031743..00000000 --- a/packages/excel-collab/src/components/BaseIcon/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { FunctionComponent, memo } from 'react'; -import iconConfig from './icon'; -import type { BaseIconName } from './icon'; -import { BaseIcon } from './BaseIcon'; - -interface IconProps { - name: BaseIconName; - className?: string; - testId?: string; -} - -export const Icon: FunctionComponent = memo( - ({ name, className = '', testId }) => { - const paths = iconConfig[name].map((item) => ({ d: item })); - return BaseIcon({ className, paths, testId }); - }, -); -Icon.displayName = 'Icon'; - -export { FillColorIcon } from './FillColorIcon'; diff --git a/packages/excel-collab/src/components/Button/index.module.css b/packages/excel-collab/src/components/Button/index.module.css deleted file mode 100644 index a3141d61..00000000 --- a/packages/excel-collab/src/components/Button/index.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.buttonWrapper { - padding: 0 var(--tinyPadding); - min-width: 20px; - text-align: center; - box-sizing: border-box; - cursor: pointer; - border-radius: var(--borderRadius); - border: 1px solid var(--activeBorderColor); - display: block; - background-color: var(--white); - color: var(--black); - height: 20px; -} - -.buttonWrapper.circle { - border-radius: 50%; -} -.buttonWrapper.disabled { - cursor: not-allowed; - opacity: 0.35; -} -.buttonWrapper:hover, -.buttonWrapper.active { - background-color: var(--buttonActiveColor); -} -.buttonWrapper.plain { - border: none; - border-radius: unset; -} -.buttonWrapper.primary { - background-color: var(--primaryColor); - color: var(--white); -} diff --git a/packages/excel-collab/src/components/Button/index.tsx b/packages/excel-collab/src/components/Button/index.tsx deleted file mode 100644 index 71de9e5a..00000000 --- a/packages/excel-collab/src/components/Button/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { FunctionComponent, CSSProperties, memo } from 'react'; -import { classnames, noop } from '../../util'; -import styles from './index.module.css'; - -export interface ButtonProps { - type?: 'normal' | 'circle' | 'plain' | 'primary'; - style?: CSSProperties; - active?: boolean; - disabled?: boolean; - onClick?: React.MouseEventHandler; - className?: string; - title?: string; - testId?: string; - dataType?: string; - buttonType?: React.ButtonHTMLAttributes['type']; -} - -export const Button: FunctionComponent> = - memo((props) => { - const { - className = '', - onClick = noop, - disabled = false, - active = false, - type = 'normal', - style, - testId, - title, - dataType, - buttonType, - children, - } = props; - const cls = classnames(styles.buttonWrapper, className, { - [styles['disabled']]: disabled, - [styles['active']]: active, - [styles['circle']]: type === 'circle', - [styles['plain']]: type === 'plain', - [styles['primary']]: type === 'primary', - }); - return ( - - ); - }); -Button.displayName = 'Button'; diff --git a/packages/excel-collab/src/components/ColorPicker/ColorPickerPanel.tsx b/packages/excel-collab/src/components/ColorPicker/ColorPickerPanel.tsx deleted file mode 100644 index b323a206..00000000 --- a/packages/excel-collab/src/components/ColorPicker/ColorPickerPanel.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { memo } from 'react'; - -import { Hue } from './Hue'; -import { Saturation } from './Saturation'; - -import { ColorModel, ColorPickerBaseProps, AnyColor } from './types'; -import { useColorManipulation } from './useColorManipulation'; -import { classnames, convertColorToHex } from '../../util'; -import { hexToHsva, hsvaToHex } from './convert'; -import styles from './panel.module.css'; - -interface Props extends Partial> { - colorModel: ColorModel; -} - -const ColorPicker = ({ - className, - colorModel, - color, - onChange, - testId, -}: Props) => { - const [hsva, updateHsva] = useColorManipulation( - colorModel, - color || colorModel.defaultColor, - onChange, - ); - - return ( -
- - -
- ); -}; - -const colorModel: ColorModel = { - defaultColor: '#000', - toHsva: hexToHsva, - fromHsva: ({ h, s, v }) => hsvaToHex({ h, s, v, a: 1 }), - equal: (first, second) => { - const a = convertColorToHex(first); - const b = convertColorToHex(second); - return a === b; - }, -}; - -export const ColorPickerPanel = memo( - (props: Partial>) => ( - - ), -); diff --git a/packages/excel-collab/src/components/ColorPicker/Hue.tsx b/packages/excel-collab/src/components/ColorPicker/Hue.tsx deleted file mode 100644 index 8604971b..00000000 --- a/packages/excel-collab/src/components/ColorPicker/Hue.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { Interactive, Interaction } from './Interactive'; -import { Pointer } from './Pointer'; -import { hsvaToHslString } from './convert'; -import { round } from './round'; -import { classnames } from '../../util'; -import styles from './panel.module.css'; - -interface Props { - className?: string; - hue: number; - testId?: string; - onChange: (newHue: { h: number }) => void; -} - -const HueBase = ({ className, hue, testId, onChange }: Props) => { - const handleMove = (interaction: Interaction) => { - onChange({ h: 360 * interaction.left }); - }; - - return ( -
- - - -
- ); -}; - -export const Hue = React.memo(HueBase); diff --git a/packages/excel-collab/src/components/ColorPicker/Interactive.tsx b/packages/excel-collab/src/components/ColorPicker/Interactive.tsx deleted file mode 100644 index af1133e6..00000000 --- a/packages/excel-collab/src/components/ColorPicker/Interactive.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React, { useRef, useMemo, useEffect } from 'react'; - -import { useEventCallback } from '../../containers/hooks'; -import { clamp } from './clamp'; -import styles from './panel.module.css'; - -export interface Interaction { - left: number; - top: number; -} - -const defaultHeight = 200; -const defaultWidth = 234; - -const getRelativePosition = ( - node: HTMLDivElement, - clientX: number, - clientY: number, -): Interaction => { - const rect = node.getBoundingClientRect(); - return { - left: clamp((clientX - rect.left) / (rect.width || defaultWidth)), - top: clamp((clientY - rect.top) / (rect.height || defaultHeight)), - }; -}; - -interface Props { - onMove: (interaction: Interaction) => void; - children: React.ReactNode; - testId?: string; -} - -const InteractiveBase = ({ onMove, testId, ...rest }: Props) => { - const container = useRef(null); - const onMoveCallback = useEventCallback(onMove); - - const [handleMoveStart, toggleDocumentEvents] = useMemo(() => { - const handleMoveStart = (event: React.MouseEvent) => { - if (!container.current) { - return; - } - event.preventDefault(); - container.current.focus(); - onMoveCallback( - getRelativePosition(container.current, event.clientX, event.clientY), - ); - toggleDocumentEvents(true); - }; - - const handleMove = (event: MouseEvent) => { - if (event.buttons <= 0) { - return; - } - if (container.current) { - event.preventDefault(); - onMoveCallback( - getRelativePosition(container.current, event.clientX, event.clientY), - ); - } - }; - - const handleMoveEnd = () => toggleDocumentEvents(false); - - function toggleDocumentEvents(state?: boolean) { - // Add or remove additional pointer event listeners - const toggleEvent = state - ? document.body.addEventListener - : document.body.removeEventListener; - toggleEvent('pointermove', handleMove); - toggleEvent('pointerup', handleMoveEnd); - } - - return [handleMoveStart, toggleDocumentEvents]; - }, [onMoveCallback]); - - // Remove window event listeners before unmounting - useEffect(() => toggleDocumentEvents, [toggleDocumentEvents]); - - return ( -
- ); -}; - -export const Interactive = React.memo(InteractiveBase); diff --git a/packages/excel-collab/src/components/ColorPicker/Pointer.tsx b/packages/excel-collab/src/components/ColorPicker/Pointer.tsx deleted file mode 100644 index 6993c4c2..00000000 --- a/packages/excel-collab/src/components/ColorPicker/Pointer.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { classnames } from '../../util'; -import styles from './panel.module.css'; - -interface Props { - className?: string; - top?: number; - left: number; - color: string; -} - -export const Pointer = ({ - className, - color, - left, - top = 0.5, -}: Props) => { - const style = { - top: `${top * 100}%`, - left: `${left * 100}%`, - }; - - return ( -
-
-
- ); -}; diff --git a/packages/excel-collab/src/components/ColorPicker/Saturation.tsx b/packages/excel-collab/src/components/ColorPicker/Saturation.tsx deleted file mode 100644 index 96cb9409..00000000 --- a/packages/excel-collab/src/components/ColorPicker/Saturation.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { useCallback } from 'react'; -import { Interactive, Interaction } from './Interactive'; -import { Pointer } from './Pointer'; -import { HsvaColor } from './types'; -import { hsvaToHslString } from './convert'; -import { round } from './round'; -import styles from './panel.module.css'; - -interface Props { - hsva: HsvaColor; - testId?: string; - onChange: (newColor: { s: number; v: number }) => void; -} - -const SaturationBase = ({ hsva, testId, onChange }: Props) => { - const handleMove = useCallback((interaction: Interaction) => { - onChange({ - s: interaction.left * 100, - v: 100 - interaction.top * 100, - }); - }, []); - - const containerStyle = { - backgroundColor: hsvaToHslString({ h: hsva.h, s: 100, v: 100, a: 1 }), - }; - - return ( -
- - - -
- ); -}; - -export const Saturation = React.memo(SaturationBase); diff --git a/packages/excel-collab/src/components/ColorPicker/clamp.ts b/packages/excel-collab/src/components/ColorPicker/clamp.ts deleted file mode 100644 index 9e38cf1d..00000000 --- a/packages/excel-collab/src/components/ColorPicker/clamp.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const clamp = (number: number, min = 0, max = 1): number => { - return number > max ? max : number < min ? min : number; -}; diff --git a/packages/excel-collab/src/components/ColorPicker/convert.ts b/packages/excel-collab/src/components/ColorPicker/convert.ts deleted file mode 100644 index 1b110f9f..00000000 --- a/packages/excel-collab/src/components/ColorPicker/convert.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { round } from './round'; -import { RgbaColor, HslaColor, HsvaColor } from './types'; - -export const hexToHsva = (hex: string): HsvaColor => { - const v = hexToRgba(hex); - const result = rgbaToHsva(v); - return result; -}; - -export const hexToRgba = (hex: string): RgbaColor => { - if (hex[0] === '#') hex = hex.substring(1); - - if (hex.length < 6) { - return { - r: parseInt(hex[0] + hex[0], 16), - g: parseInt(hex[1] + hex[1], 16), - b: parseInt(hex[2] + hex[2], 16), - a: hex.length === 4 ? round(parseInt(hex[3] + hex[3], 16) / 255, 2) : 1, - }; - } - - return { - r: parseInt(hex.substring(0, 2), 16), - g: parseInt(hex.substring(2, 4), 16), - b: parseInt(hex.substring(4, 6), 16), - a: hex.length === 8 ? round(parseInt(hex.substring(6, 8), 16) / 255, 2) : 1, - }; -}; - -export const hsvaToHex = (hsva: HsvaColor): string => - rgbaToHex(hsvaToRgba(hsva)); - -const hsvaToHsla = ({ h, s, v, a }: HsvaColor): HslaColor => { - const hh = ((200 - s) * v) / 100; - - return { - h: round(h), - s: round( - hh > 0 && hh < 200 - ? ((s * v) / 100 / (hh <= 100 ? hh : 200 - hh)) * 100 - : 0, - ), - l: round(hh / 2), - a: round(a, 2), - }; -}; - -export const hsvaToHslString = (hsva: HsvaColor): string => { - const { h, s, l } = hsvaToHsla(hsva); - return `hsl(${h}, ${s}%, ${l}%)`; -}; - -const hsvaToRgba = ({ h, s, v, a }: HsvaColor): RgbaColor => { - h = (h / 360) * 6; - s = s / 100; - v = v / 100; - - const hh = Math.floor(h), - b = v * (1 - s), - c = v * (1 - (h - hh) * s), - d = v * (1 - (1 - h + hh) * s), - module = hh % 6; - - return { - r: round([v, c, b, b, d, v][module] * 255), - g: round([d, v, v, c, b, b][module] * 255), - b: round([b, b, d, v, v, c][module] * 255), - a: round(a, 2), - }; -}; - -const format = (number: number) => { - const hex = number.toString(16); - return hex.length < 2 ? '0' + hex : hex; -}; - -const rgbaToHex = ({ r, g, b, a }: RgbaColor): string => { - const alphaHex = a < 1 ? format(round(a * 255)) : ''; - return '#' + format(r) + format(g) + format(b) + alphaHex; -}; - -const rgbaToHsva = ({ r, g, b, a }: RgbaColor): HsvaColor => { - const max = Math.max(r, g, b); - const delta = max - Math.min(r, g, b); - - const hh = delta - ? max === r - ? (g - b) / delta - : max === g - ? 2 + (b - r) / delta - : 4 + (r - g) / delta - : 0; - - return { - h: round(60 * (hh < 0 ? hh + 6 : hh)), - s: round(max ? (delta / max) * 100 : 0), - v: round((max / 255) * 100), - a, - }; -}; diff --git a/packages/excel-collab/src/components/ColorPicker/index.module.css b/packages/excel-collab/src/components/ColorPicker/index.module.css deleted file mode 100644 index 9fbfeffe..00000000 --- a/packages/excel-collab/src/components/ColorPicker/index.module.css +++ /dev/null @@ -1,50 +0,0 @@ -.color-picker { - position: relative; -} - -.color-picker-wrapper { - width: 260px; - position: absolute; - top: 22px; - left: 0; - z-index: var(--highestZIndex); - padding: var(--padding); - background-color: var(--white); - border-radius: var(--borderRadius); - border: 1px solid var(--borderColor); -} -.color-picker.top .color-picker-wrapper { - transform: translate(0, -100%); - left: 100%; -} -.color-picker.right .color-picker-wrapper { - transform: translate(16px, 0); - left: 100%; - top: 0px; -} - -.color-picker-list { - display: grid; - grid-template-columns: repeat(10, 1fr); - grid-template-rows: repeat(9, 1fr); - gap: 4px; -} -.color-picker-item { - width: 14px; - height: 14px; - cursor: pointer; - border-radius: var(--borderRadius); - border: 1px solid var(--borderColor); -} -.no-fill.color-picker-item { - width: auto; - padding: 0 2px; -} -.color-picker-item:hover { - border-color: var(--primaryColor); -} -.reset { - width: 100%; - margin-top: var(--padding); - background-color: var(--white); -} diff --git a/packages/excel-collab/src/components/ColorPicker/index.tsx b/packages/excel-collab/src/components/ColorPicker/index.tsx deleted file mode 100644 index 7c334186..00000000 --- a/packages/excel-collab/src/components/ColorPicker/index.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { FunctionComponent, useState, memo, useCallback } from 'react'; -import { classnames, COLOR_PICKER_COLOR_LIST } from '../../util'; -import styles from './index.module.css'; -import { ColorPickerPanel } from './ColorPickerPanel'; -import { Button } from '../Button'; -import { useClickOutside } from '../../containers/hooks'; -import i18n from '../../i18n'; - -export interface ColorPickerProps { - color: string; - onChange: (value: string) => void; - className?: string; - position?: 'top' | 'bottom' | 'right'; - testId?: string; -} - -export const ColorPicker: FunctionComponent< - React.PropsWithChildren -> = memo((props) => { - const { - onChange, - children, - color, - position = 'bottom', - testId, - className, - } = props; - const [visible, setVisible] = useState(false); - const ref = useClickOutside(visible, () => { - setVisible(false); - }); - const openColorPicker = useCallback(() => { - setVisible(true); - }, []); - const reset = useCallback(() => { - onChange(''); - }, []); - const handleClick = useCallback((event: React.MouseEvent) => { - const color = (event.target as { dataset?: { value?: string } })?.dataset - ?.value; - if (color) { - onChange(color); - } - }, []); - return ( -
-
- {children} -
- {visible && ( -
-
- {COLOR_PICKER_COLOR_LIST.map((item) => { - return ( -
- ); - })} -
-
- -
-
- -
-
- )} -
- ); -}); -ColorPicker.displayName = 'ColorPicker'; diff --git a/packages/excel-collab/src/components/ColorPicker/panel.module.css b/packages/excel-collab/src/components/ColorPicker/panel.module.css deleted file mode 100644 index 73c94798..00000000 --- a/packages/excel-collab/src/components/ColorPicker/panel.module.css +++ /dev/null @@ -1,94 +0,0 @@ -.color-picker-panel { - position: relative; - display: flex; - flex-direction: column; - width: 100%; - height: 150px; - user-select: none; - cursor: default; -} - -.color-picker-panel__saturation { - margin: var(--padding) 0; - position: relative; - flex-grow: 1; - border-color: transparent; - border-bottom: 12px solid #000; - border-radius: var(--borderRadius); - background-image: linear-gradient(to top, #000, rgba(0, 0, 0, 0)), - linear-gradient(to right, #fff, rgba(255, 255, 255, 0)); -} - -.color-picker-panel__pointer-fill { - content: ''; - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - pointer-events: none; - border-radius: inherit; -} - -.color-picker-panel__saturation { - box-shadow: inset 0 0 0 1px var(--hoverColor); - border: 1px solid var(--borderColor); -} - -.color-picker-panel__hue { - position: relative; - height: 16px; -} - -.color-picker-panel__hue { - background: linear-gradient( - to right, - #f00 0%, - #ff0 17%, - #0f0 33%, - #0ff 50%, - #00f 67%, - #f0f 83%, - #f00 100% - ); -} - -.color-picker-panel__last-control { - border-radius: var(--borderLargeRadius); -} - -.color-picker-panel__interactive { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - border-radius: inherit; - outline: none; - touch-action: none; -} - -.color-picker-panel__pointer { - position: absolute; - z-index: 1; - box-sizing: border-box; - width: 18px; - height: 18px; - transform: translate(-50%, -50%); - background-color: var(--white); - border: 1px solid var(--borderColor); - border-radius: 50%; - box-shadow: 0 2px 4px var(--scrollbarHoveColor); -} - -.color-picker-panel__interactive:focus .color-picker-panel__pointer { - transform: translate(-50%, -50%) scale(1.1); -} - -.color-picker-panel__saturation-pointer { - z-index: 3; -} - -.color-picker-panel__hue-pointer { - z-index: 2; -} diff --git a/packages/excel-collab/src/components/ColorPicker/round.ts b/packages/excel-collab/src/components/ColorPicker/round.ts deleted file mode 100644 index a379a55b..00000000 --- a/packages/excel-collab/src/components/ColorPicker/round.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const round = ( - number: number, - digits = 0, - base = Math.pow(10, digits), -): number => { - return Math.round(base * number) / base; -}; diff --git a/packages/excel-collab/src/components/ColorPicker/types.ts b/packages/excel-collab/src/components/ColorPicker/types.ts deleted file mode 100644 index a8f40188..00000000 --- a/packages/excel-collab/src/components/ColorPicker/types.ts +++ /dev/null @@ -1,53 +0,0 @@ -export interface RgbColor { - r: number; - g: number; - b: number; -} - -export interface RgbaColor extends RgbColor { - a: number; -} - -export interface HslColor { - h: number; - s: number; - l: number; -} - -export interface HslaColor extends HslColor { - a: number; -} - -export interface HsvColor { - h: number; - s: number; - v: number; -} - -export interface HsvaColor extends HsvColor { - a: number; -} - -export type ObjectColor = - | RgbColor - | HslColor - | HsvColor - | RgbaColor - | HslaColor - | HsvaColor; - -export type AnyColor = string | ObjectColor; - -export interface ColorModel { - defaultColor: T; - toHsva: (defaultColor: T) => HsvaColor; - fromHsva: (hsva: HsvaColor) => T; - equal: (first: T, second: T) => boolean; -} - -export interface ColorPickerBaseProps { - color: T; - onChange: (newColor: T) => void; - className?: string; - testId?: string; -} diff --git a/packages/excel-collab/src/components/ColorPicker/useColorManipulation.ts b/packages/excel-collab/src/components/ColorPicker/useColorManipulation.ts deleted file mode 100644 index 5f7cf83a..00000000 --- a/packages/excel-collab/src/components/ColorPicker/useColorManipulation.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; -import { ColorModel, AnyColor, HsvaColor } from './types'; -import { useEventCallback } from '../../containers/hooks/useEventCallback'; -import { deepEqual } from '../../util'; - -export function useColorManipulation( - colorModel: ColorModel, - color: T, - onChange?: (color: T) => void, -): [HsvaColor, (color: Partial) => void] { - const onChangeCallback = useEventCallback(onChange); - - // No matter which color model is used (HEX, RGB(A) or HSL(A)), - // all internal calculations are based on HSVA model - const [hsva, updateHsva] = useState(() => - colorModel.toHsva(color), - ); - - const cache = useRef({ color, hsva }); - - useEffect(() => { - if (!colorModel.equal(color, cache.current.color)) { - const newHsva = colorModel.toHsva(color); - cache.current = { hsva: newHsva, color }; - updateHsva(newHsva); - } - }, [color, colorModel]); - - useEffect(() => { - let newColor; - if ( - !deepEqual(hsva, cache.current.hsva) && - !colorModel.equal( - (newColor = colorModel.fromHsva(hsva)), - cache.current.color, - ) - ) { - cache.current = { hsva, color: newColor }; - onChangeCallback(newColor); - } - }, [hsva, colorModel, onChangeCallback]); - - // Merge the current HSVA color object with updated params. - // For example, when a child component sends `h` or `s` only - const handleChange = useCallback((params: Partial) => { - updateHsva((current) => Object.assign({}, current, params)); - }, []); - - return [hsva, handleChange]; -} diff --git a/packages/excel-collab/src/components/Dialog/index.module.css b/packages/excel-collab/src/components/Dialog/index.module.css deleted file mode 100644 index 5fdf6f39..00000000 --- a/packages/excel-collab/src/components/Dialog/index.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.dialog-modal { - position: fixed; - width: 100%; - height: 100%; - top: 0; - left: 0; - z-index: var(--highestZIndex); - background-color: var(--dialogBackground); - display: flex; - align-items: center; - justify-content: center; -} -.dialog-container { - background-color: var(--white); - padding: var(--padding); - border: 1px solid var(--borderColor); - border-radius: var(--borderRadius); -} - -.dialog-title { - font-weight: bold; - font-size: var(--largeFont); -} - -.dialog-content { - padding-top: var(--mediumPadding); -} - -.dialog-button { - padding-top: var(--mediumPadding); - display: flex; - align-items: center; - justify-content: flex-end; -} - -.dialog-cancel { - margin-left: var(--mediumPadding); -} diff --git a/packages/excel-collab/src/components/Dialog/index.tsx b/packages/excel-collab/src/components/Dialog/index.tsx index 4ba9e9db..539e60bf 100644 --- a/packages/excel-collab/src/components/Dialog/index.tsx +++ b/packages/excel-collab/src/components/Dialog/index.tsx @@ -1,106 +1,61 @@ import React, { FunctionComponent, memo } from 'react'; -import { createPortal } from 'react-dom'; -import { createRoot, Root } from 'react-dom/client'; -import { Button } from '../Button'; -import styles from './index.module.css'; -import { classnames } from '../../util'; import i18n from '../../i18n'; +import { Modal } from '../../component/Modal'; +import { Dialog as AriaDialog, Heading } from '../../component/Dialog'; +import { Button } from '../../component/Button'; interface DialogProps { title: string; - visible: boolean; - className?: string; - children?: React.ReactNode; - testId?: string; - getContainer?: () => HTMLElement; - onOk?: React.MouseEventHandler; - onCancel?: React.MouseEventHandler; + isOpen?: boolean; + children: React.ReactNode; + onOk?: () => boolean | undefined; + onOpenChange?: (isOpen: boolean) => void; + onCancel?: () => void; + isDismissable?: boolean; } export const Dialog: FunctionComponent = memo((props) => { const { children, title, - className, - onCancel, onOk, - visible, - getContainer = () => document.body, - testId, + onCancel, + isDismissable = true, + isOpen, + onOpenChange, } = props; - if (!visible) { - return undefined; - } - const cancelTestId = testId ? `${testId}-cancel` : undefined; - const confirmTestId = testId ? `${testId}-confirm` : undefined; - return createPortal( -
-
-
{title}
-
{children}
-
-
-
-
, - getContainer(), + + ); }); -Dialog.displayName = 'Dialog'; - -export function info(props: DialogProps) { - const container = document.createDocumentFragment(); - let root: Root | undefined = undefined; - function close() { - root?.unmount(); - root = undefined; - } - - function render(modalProps: DialogProps) { - root = root || createRoot(container); - root.render( - { - event.stopPropagation(); - if (modalProps.onCancel) { - modalProps.onCancel(event); - } - close(); - }} - onOk={(event) => { - event.stopPropagation(); - if (modalProps.onOk) { - modalProps.onOk(event); - } - close(); - }} - testId={modalProps.testId} - > - {modalProps.children} - , - ); - } - function update(modalProps: DialogProps) { - render(modalProps); - } - - render(props); - - return { - close, - update, - }; -} diff --git a/packages/excel-collab/src/components/Loading/index.module.css b/packages/excel-collab/src/components/Loading/index.module.css deleted file mode 100644 index 6b61501a..00000000 --- a/packages/excel-collab/src/components/Loading/index.module.css +++ /dev/null @@ -1,29 +0,0 @@ -.container { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - display: flex; - align-items: center; - justify-content: center; - background-color: var(--white); -} -.loading { - width: 48px; - height: 48px; - border: 5px solid var(--primaryColor); - border-bottom-color: transparent; - border-radius: 50%; - box-sizing: border-box; - animation: rotation 1s linear infinite; -} - -@keyframes rotation { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} diff --git a/packages/excel-collab/src/components/Loading/index.tsx b/packages/excel-collab/src/components/Loading/index.tsx deleted file mode 100644 index 8c694695..00000000 --- a/packages/excel-collab/src/components/Loading/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { FunctionComponent, memo } from 'react'; -import styles from './index.module.css'; - -export const Loading: FunctionComponent = memo(() => { - return ( -
-
-
- ); -}); -Loading.displayName = 'Loading'; diff --git a/packages/excel-collab/src/components/Menu/index.module.css b/packages/excel-collab/src/components/Menu/index.module.css deleted file mode 100644 index 7ae91f8f..00000000 --- a/packages/excel-collab/src/components/Menu/index.module.css +++ /dev/null @@ -1,61 +0,0 @@ -.container { - position: relative; -} - -.portal { - border-radius: var(--borderRadius); - position: absolute; - z-index: var(--highestZIndex); - background-color: var(--white); - border: 1px solid var(--borderColor); - transition: all 1s ease-out; -} -.menuContainer { - left: 0; -} -.menuContainer.bottom { - left: unset; - right: 0; -} - -.subMenuContainer { - top: 0; - left: 100%; -} -.menu { - list-style: none; - margin: 0px; - padding: 0; - outline: 0px; - width: 100%; - box-sizing: border-box; - text-align: center; -} - -.small .menuItem { - padding: 3px 8px; -} - -.menuItem { - padding: 6px 16px; - position: relative; - width: 100%; - box-sizing: border-box; - text-align: center; - user-select: none; - text-wrap: nowrap; -} - -.menuItem + .menuItem { - border-top: 1px solid var(--borderColor); -} - -.menuItem:hover,.menuItem.active { - background-color: var(--hoverColor); -} - -.trigger { - display: flex; - align-items: center; - justify-content: center; -} diff --git a/packages/excel-collab/src/components/Menu/index.tsx b/packages/excel-collab/src/components/Menu/index.tsx deleted file mode 100644 index 52ec0667..00000000 --- a/packages/excel-collab/src/components/Menu/index.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React, { FunctionComponent, useState, memo, useCallback } from 'react'; -import styles from './index.module.css'; -import { classnames } from '../../util'; -import { useClickOutside } from '../../containers/hooks'; -import { Button } from '../Button'; - -type MenuItemProps = { - onClick?: () => void; - testId?: string; - active?: boolean; -}; - -type MenuProps = { - label: React.ReactNode; - isPlain?: boolean; - testId?: string; - className?: string; - position?: 'right' | 'bottom'; - size?: 'normal' | 'small'; - portalClassName?: string; -}; - -type SubMenuProps = Pick< - MenuProps, - 'label' | 'testId' | 'className' | 'portalClassName' ->; -export const MenuItem: FunctionComponent< - React.PropsWithChildren -> = ({ onClick, children, testId, active = false }) => { - return ( -
  • - {children} -
  • - ); -}; - -export const SubMenu: FunctionComponent< - React.PropsWithChildren -> = ({ label, children, testId, className, portalClassName }) => { - const [open, setOpen] = useState(false); - const handleClick = useCallback(() => { - setOpen((v) => !v); - }, []); - return ( -
  • -
    {label}
    - -
  • - ); -}; - -export const Menu: FunctionComponent> = memo( - ({ - label, - children, - testId, - className, - isPlain = false, - position = 'right', - size = 'normal', - portalClassName, - }) => { - const buttonType = isPlain ? 'plain' : undefined; - const [open, setOpen] = useState(false); - const handleClick = useCallback(() => { - setOpen((v) => !v); - }, []); - const closeMenu = useCallback(() => { - setOpen(false); - }, []); - const ref = useClickOutside(open, closeMenu); - return ( -
    - - - -
    - ); - }, -); - -Menu.displayName = 'Menu'; diff --git a/packages/excel-collab/src/components/Select/index.module.css b/packages/excel-collab/src/components/Select/index.module.css index 19eb4b10..9844d720 100644 --- a/packages/excel-collab/src/components/Select/index.module.css +++ b/packages/excel-collab/src/components/Select/index.module.css @@ -1,86 +1,5 @@ -.selectList { - background-color: var(--white); - color: var(--black); - border-radius: var(--borderRadius); -} -.selectItem { - text-wrap: nowrap; -} -.selectItem.disabled { - background-color: var(--white); - cursor: not-allowed; - color: var(--borderColor); -} -.selectItem.disabled:hover { - background-color: var(--white); -} - -.popup-container { - position: absolute; - background-color: var(--white); - border-radius: var(--borderRadius); - border: 1px solid var(--borderColor); - display: none; - top: 100%; - left: 0; - right: 0; - z-index: var(--highestZIndex); - max-height: 200px; - overflow: auto; - white-space: nowrap; -} -.popup-container.top { - transform: translate(0, -100%); - top: 0; -} -.popup-container.active { - display: block; -} - -.popup-item { - padding: var(--tinyPadding); - border-bottom: 1px solid var(--borderColor); - display: flex; - justify-content: space-between; - align-items: center; -} -.popup-item:hover { - background-color: var(--buttonActiveColor); -} -.popup-item-icon { - color: var(--primaryColor); -} - -.popup-item-content { - flex: 1; - text-align: left; -} - .select-list-container { - height: 100%; - text-align: center; - background-color: var(--white); - user-select: none; display: flex; align-items: center; position: relative; - border: 1px solid var(--borderColor); - box-sizing: border-box; -} - -.select-list-container .select-list-trigger { - position: absolute; - right: 0; - border-radius: unset; - border-left: 1px solid var(--borderColor); - display: flex; - align-items: center; - justify-content: center; - height: 100%; -} - -.select-list-container.active .select-list-trigger { - transform: rotate(180deg); - border-right: 1px solid var(--borderColor); - border-left: none; } diff --git a/packages/excel-collab/src/components/Select/index.tsx b/packages/excel-collab/src/components/Select/index.tsx index cfb31af7..dcc50d6a 100644 --- a/packages/excel-collab/src/components/Select/index.tsx +++ b/packages/excel-collab/src/components/Select/index.tsx @@ -1,194 +1,77 @@ import React, { - CSSProperties, FunctionComponent, memo, - useCallback, + useEffect, + useMemo, useState, } from 'react'; import { classnames } from '../../util'; import { OptionItem } from '../../types'; import styles from './index.module.css'; -import { Icon } from '../BaseIcon'; -import { Button } from '../Button'; -import { useClickOutside } from '../../containers/hooks'; +import { Button } from '../../component/Button'; +import { ChevronDown } from 'lucide-react'; +import { MenuTrigger, Menu, MenuItem } from '../../component/Menu'; -export interface SelectProps { - value?: string | number; - defaultValue?: string | number; - data: Array; - getItemStyle?: (value: string | number) => CSSProperties; - onChange: (value: string | number) => void; - title?: string; - className?: string; - testId?: string; -} - -export const Select: FunctionComponent = memo((props) => { - const { - data, - value: activeValue, - className, - onChange, - getItemStyle, - title, - defaultValue, - testId, - } = props; - const handleChange = useCallback( - (event: React.ChangeEvent) => { - onChange(event.target.value); - }, - [], - ); - - return ( - - ); -}); -Select.displayName = 'Select'; - -export interface SelectPopupProps { - active: boolean; +export interface SelectListProps { value: string; data: Array; onChange: (value: string) => void; - position?: 'top' | 'bottom'; testId?: string; className?: string; } -export const SelectPopup: FunctionComponent = memo( - ({ - onChange, - value, - active, - data, - className, - position = 'bottom', - testId, - }) => { - const handleSelect = useCallback( - (event: React.MouseEvent) => { - const v = (event.target as { dataset?: { value?: string } }).dataset - ?.value; - if (!v) { - return; - } - onChange(v); - }, - [onChange], - ); - return ( -
    - {data.map((v) => ( -
    - - {v.label} - - {v.value == value && ( - - - - )} -
    - ))} -
    - ); - }, -); +export const SelectList: FunctionComponent< + React.PropsWithChildren +> = memo(({ children, value, data, onChange, testId, className }) => { + const [selected, setSelected] = useState(value); -SelectPopup.displayName = 'SelectPopup'; + useEffect(() => { + setSelected(value); + }, [value]); -export type SelectListProps = Omit; + const selectedKeys = useMemo(() => [selected], [selected]); -export const SelectList: FunctionComponent< - React.PropsWithChildren -> = memo(({ children, value, data, onChange, position, testId, className }) => { - const [active, setActive] = useState(false); - const handleClick = useCallback(() => { - setActive((v) => !v); - }, []); - const handleChange = useCallback( - (value: string) => { - if (value) { - onChange(value); - setActive(false); - } - }, - [onChange], - ); - const ref = useClickOutside(true, () => { - setActive(false); - }); return (
    {children} - - {active && data.length > 0 && ( - - )} + + + { + const selectedKey = typeof v === 'string' ? v : Array.from(v)[0]; + if (selectedKey == null) { + return; + } + const temp = String(selectedKey); + setSelected(temp); + onChange(temp); + }} + aria-label="Select List" + > + {data.map((item) => ( + + {item.label} + + ))} + +
    ); }); diff --git a/packages/excel-collab/src/components/Toast/index.module.css b/packages/excel-collab/src/components/Toast/index.module.css deleted file mode 100644 index 961491ac..00000000 --- a/packages/excel-collab/src/components/Toast/index.module.css +++ /dev/null @@ -1,49 +0,0 @@ -.container { - position: fixed; - top: 50px; - left: 50%; - z-index: var(--highestZIndex); -} -.content { - padding-left: var(--mediumPadding); -} -.toast { - padding: var(--mediumPadding) var(--padding); - border-radius: var(--borderLargeRadius); - display: flex; - align-items: center; - word-wrap: break-word; - min-width: 200px; - font-size: var(--largeFont); -} -.icon { - font-size: 20px; -} -.success { - border: 1px solid rgb(183, 235, 143); - background-color: rgb(183, 235, 143); -} -.success_icon { - color: #52c41a; -} -.error { - border: 1px solid rgb(255, 204, 199); - background-color: rgb(255, 204, 199); -} -.error_icon { - color: #ff4d4f; -} -.info { - border: 1px solid rgb(145, 202, 255); - background-color: rgb(145, 202, 255); -} -.info_icon { - color: #1677ff; -} -.warning { - border: 1px solid rgb(255, 229, 143); - background-color: rgb(255, 229, 143); -} -.warning_icon { - color: #faad14; -} diff --git a/packages/excel-collab/src/components/Toast/index.tsx b/packages/excel-collab/src/components/Toast/index.tsx deleted file mode 100644 index 6fbb7042..00000000 --- a/packages/excel-collab/src/components/Toast/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import styles from './index.module.css'; -import { classnames } from '../../util'; -import { Icon } from '../BaseIcon'; -import { MessageType } from '../../types'; - -type Props = { - message: string; - type: MessageType; - duration?: number; // second - testId: string; -}; - -export const Toast: React.FunctionComponent> = ({ - message, - type, - testId, -}) => { - return ( -
    - -
    {message}
    -
    - ); -}; - -export function toast(props: Props) { - const { duration = 3, ...rest } = props; - let container: HTMLDivElement | undefined = document.createElement('div'); - container.className = styles['container']; - document.body.appendChild(container); - const root = createRoot(container); - root.render(); - - function close() { - if (!container) { - return; - } - root.unmount(); - document.body.removeChild(container!); - container = undefined; - } - - setTimeout(close, duration * 1000); - return close; -} - -toast.error = function (message: string, testId = 'error-toast') { - return toast({ message, type: 'error', testId }); -}; - -toast.info = function (message: string, testId = 'info-toast') { - return toast({ message, type: 'info', testId }); -}; - -toast.warning = function (message: string, testId = 'warning-toast') { - return toast({ message, type: 'warning', testId }); -}; -toast.success = function (message: string, testId = 'success-toast') { - return toast({ message, type: 'success', testId }); -}; diff --git a/packages/excel-collab/src/components/__tests__/BaseIcon.test.tsx b/packages/excel-collab/src/components/__tests__/BaseIcon.test.tsx deleted file mode 100644 index 7c6a0258..00000000 --- a/packages/excel-collab/src/components/__tests__/BaseIcon.test.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Icon } from '../BaseIcon'; -import { cleanup, render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom'; - -describe('BaseIcon.test.ts', () => { - afterEach(cleanup); - test('normal', () => { - render(); - expect(screen.getByTestId('icon_data')).toBeInTheDocument(); - }); -}); diff --git a/packages/excel-collab/src/components/__tests__/Button.test.tsx b/packages/excel-collab/src/components/__tests__/Button.test.tsx deleted file mode 100644 index 84485c20..00000000 --- a/packages/excel-collab/src/components/__tests__/Button.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Button } from '../Button'; -import { cleanup, render } from '@testing-library/react'; - -describe('Button.test.ts', () => { - afterEach(cleanup); - test('normal', () => { - const dom = render( - , - ); - expect(dom.container.textContent).toEqual('button'); - }); - test('icon button', () => { - const dom = render( - , - ); - expect(dom.container.textContent).toEqual('add'); - expect(dom.container.childNodes).toHaveLength(1); - }); -}); diff --git a/packages/excel-collab/src/components/__tests__/ColorPicker.test.tsx b/packages/excel-collab/src/components/__tests__/ColorPicker.test.tsx deleted file mode 100644 index 04f5d35c..00000000 --- a/packages/excel-collab/src/components/__tests__/ColorPicker.test.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ColorPicker } from '../ColorPicker'; -import { cleanup, render } from '@testing-library/react'; - -describe('ColorPicker.test.ts', () => { - afterEach(cleanup); - test('normal', () => { - const dom = render( {}} />); - expect(dom).not.toBeNull(); - }); -}); diff --git a/packages/excel-collab/src/components/__tests__/Dialog.test.tsx b/packages/excel-collab/src/components/__tests__/Dialog.test.tsx deleted file mode 100644 index 1cad539b..00000000 --- a/packages/excel-collab/src/components/__tests__/Dialog.test.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Dialog, info } from '../Dialog'; -import { cleanup, render, act, screen } from '@testing-library/react'; - -describe('Dialog.test.ts', () => { - afterEach(cleanup); - test('normal', () => { - render( - document.body}> -
    test
    -
    , - ); - expect(screen.getByTestId('dialog').textContent).toEqual('test'); - }); - test('empty', () => { - render( - document.body}> -
    test
    -
    , - ); - expect(() => screen.getByTestId('dialog')).toThrow(); - }); - test('info', () => { - let result: ReturnType = { - update: () => {}, - close: () => {}, - }; - act(() => { - result = info({ - title: 'test', - visible: true, - children:
    test
    , - }); - }); - expect(screen.getByTestId('dialog').textContent).toEqual('test'); - act(() => { - result.close(); - }); - expect(() => screen.getByTestId('dialog')).toThrow(); - }); - test('info update', () => { - let result: ReturnType = { - update: () => {}, - close: () => {}, - }; - act(() => { - result = info({ - title: 'test', - visible: true, - children:
    test
    , - }); - }); - expect(screen.getByTestId('dialog').textContent).toEqual('test'); - act(() => { - result.update({ - title: 'test', - visible: true, - children:
    11
    , - }); - }); - expect(screen.getByTestId('dialog').textContent).toEqual('11'); - act(() => { - result.close(); - }); - }); -}); diff --git a/packages/excel-collab/src/components/__tests__/Loading.test.tsx b/packages/excel-collab/src/components/__tests__/Loading.test.tsx deleted file mode 100644 index d7fd6c57..00000000 --- a/packages/excel-collab/src/components/__tests__/Loading.test.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Loading } from '../Loading'; -import { cleanup, render } from '@testing-library/react'; - -describe('BaseIcon.test.ts', () => { - afterEach(cleanup); - test('normal', () => { - const dom = render(); - expect(dom.container.childNodes.length).toEqual(1); - }); -}); diff --git a/packages/excel-collab/src/components/__tests__/Menu.test.tsx b/packages/excel-collab/src/components/__tests__/Menu.test.tsx deleted file mode 100644 index 87978ac8..00000000 --- a/packages/excel-collab/src/components/__tests__/Menu.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Menu, SubMenu, MenuItem } from '../Menu'; -import { - cleanup, - fireEvent, - render, - screen, - act, -} from '@testing-library/react'; - -describe('Menu.test.ts', () => { - afterEach(cleanup); - test('normal', () => { - const dom = render( - - 33 - 44 - - 11 - 22 - - , - ); - expect(dom.container.childNodes.length).toEqual(1); - }); - test('subMenu', () => { - act(() => { - render( - - 33 - 44 - - 11 - 22 - - , - ); - }); - fireEvent.click(screen.getByTestId('menu-trigger')); - fireEvent.click(screen.getByTestId('subMenu')); - expect(screen.getByTestId('subMenu').childNodes.length).toEqual(2); - }); - test('click outside', () => { - act(() => { - render( -
    -
    outside
    - - 33 - 44 - - 11 - 22 - - - , -
    , - ); - }); - fireEvent.click(screen.getByTestId('menu-trigger')); - fireEvent.click(screen.getByTestId('subMenu')); - expect(screen.getByTestId('subMenu').childNodes.length).toEqual(2); - }); -}); diff --git a/packages/excel-collab/src/components/__tests__/Select.test.tsx b/packages/excel-collab/src/components/__tests__/Select.test.tsx deleted file mode 100644 index 2fe38ead..00000000 --- a/packages/excel-collab/src/components/__tests__/Select.test.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { SelectList } from '../Select'; -import { cleanup, render, fireEvent, screen } from '@testing-library/react'; - -describe('Select.test.ts', () => { - afterEach(cleanup); - describe('SelectList', () => { - test('empty', () => { - const onChange = jest.fn(); - render( - -
    test
    -
    , - ); - fireEvent.click(screen.getByTestId('select-list-trigger')); - const dom = screen.getByTestId('select-list-popup'); - dom.setAttribute('data-value', ''); - fireEvent.click(dom); - expect(onChange).not.toHaveBeenCalled(); - }); - test('same value', () => { - const onChange = jest.fn(); - render( - -
    test
    -
    , - ); - fireEvent.click(screen.getByTestId('select-list-trigger')); - const dom = screen.getByTestId('select-list-popup'); - dom.setAttribute('data-value', '1'); - fireEvent.click(dom); - expect(onChange).toHaveBeenCalled(); - }); - - test('ok', () => { - const onChange = jest.fn(); - render( - -
    test
    -
    , - ); - fireEvent.click(screen.getByTestId('select-list-trigger')); - const dom = screen.getByTestId('select-list-popup'); - dom.setAttribute('data-value', '2'); - fireEvent.click(dom); - expect(onChange).toHaveBeenCalledWith('2'); - }); - - test('click outside', () => { - const onChange = jest.fn(); - render( -
    -
    outside
    - -
    test
    -
    -
    , - ); - fireEvent.click(screen.getByTestId('select-list-trigger')); - fireEvent.pointerDown(screen.getByTestId('outside')); - expect(onChange).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/excel-collab/src/components/__tests__/Toast.test.tsx b/packages/excel-collab/src/components/__tests__/Toast.test.tsx deleted file mode 100644 index 87de03ea..00000000 --- a/packages/excel-collab/src/components/__tests__/Toast.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Toast, toast } from '../Toast'; -import { cleanup, render, act, screen } from '@testing-library/react'; -import '@testing-library/jest-dom'; - -describe('BaseIcon.test.ts', () => { - afterEach(cleanup); - test('normal', () => { - const dom = render(); - expect(dom.container.childNodes.length).toEqual(1); - }); - test('close', () => { - let close: () => void; - act(() => { - close = toast({ - type: 'success', - message: 'test', - testId: 'toast-close', - duration: 1, - }); - }); - expect(screen.getByTestId('toast-close').textContent).toEqual('test'); - act(() => { - close(); - }); - expect(() => screen.getByTestId('toast-close')).toThrow(); - }); - test('close twice', () => { - let close: () => void; - act(() => { - close = toast({ - type: 'success', - message: 'test', - testId: 'toast-twice', - duration: 1, - }); - }); - expect(screen.getByTestId('toast-twice').textContent).toEqual('test'); - act(() => { - close(); - }); - expect(() => screen.getByTestId('toast-twice')).toThrow(); - act(() => { - close(); - }); - expect(() => screen.getByTestId('toast-twice')).toThrow(); - }); - for (const key of ['error', 'info', 'warning', 'success'] as const) { - const testId = `${key}-toast`; - test(testId, () => { - let close: () => void; - act(() => { - close = toast[key]('message'); - }); - expect(screen.getByTestId(testId).textContent).toEqual('message'); - act(() => { - close(); - }); - expect(() => screen.getByTestId(testId)).toThrow(); - }); - } - for (const key of ['error', 'info', 'warning', 'success'] as const) { - test(key, () => { - let close: () => void; - act(() => { - close = toast[key]('message', key); - }); - expect(screen.getByTestId(key).textContent).toEqual('message'); - act(() => { - close(); - }); - expect(() => screen.getByTestId(key)).toThrow(); - }); - } -}); diff --git a/packages/excel-collab/src/components/index.ts b/packages/excel-collab/src/components/index.ts index 28b16a78..b3fdf0eb 100644 --- a/packages/excel-collab/src/components/index.ts +++ b/packages/excel-collab/src/components/index.ts @@ -1,8 +1,2 @@ -export * from './Button'; -export * from './BaseIcon'; export * from './Select'; export * from './Dialog'; -export * from './Menu'; -export * from './Toast'; -export * from './Loading'; -export * from './ColorPicker'; diff --git a/packages/excel-collab/src/containers/ColorPicker/index.tsx b/packages/excel-collab/src/containers/ColorPicker/index.tsx new file mode 100644 index 00000000..fb112d4b --- /dev/null +++ b/packages/excel-collab/src/containers/ColorPicker/index.tsx @@ -0,0 +1,130 @@ +import { + ColorPicker as AriaColorPicker, + ColorSpace, + getColorChannels, + type ColorPickerProps as AriaColorPickerProps, +} from 'react-aria-components/ColorPicker'; +import { DialogTrigger } from '../../component/Dialog'; +import { ColorSlider } from '../../component/ColorSlider'; +import { ColorArea } from '../../component/ColorArea'; +import { ColorField } from '../../component/ColorField'; +import { Popover } from '../../component/Popover'; +import { Select, SelectItem } from '../../component/Select'; +import { + ColorSwatchPicker, + ColorSwatchPickerItem, +} from '../../component/ColorSwatchPicker'; +import { Button } from '../../component/Button'; +import { COLOR_PICKER_COLOR_LIST } from '../../util'; +import { useState } from 'react'; + +export interface ColorPickerProps extends Omit< + AriaColorPickerProps, + 'children' | 'onChange' +> { + children: React.ReactNode; + onReset?: () => void; + onChange?: (color: string) => void; + 'data-testid'?: string; +} + +export function ColorPicker({ + children, + onReset, + onChange, + 'data-testid': testId, + ...props +}: ColorPickerProps) { + const [space, setSpace] = useState('rgb'); + const controlPrefix = testId?.replace(/-picker$/, ''); + const pickerValue = props.value || '#000000'; + return ( + onChange?.(color.toString('hex'))} + > + + {children} + + + + + onChange?.( + typeof color === 'string' ? color : color.toString('hex'), + ) + } + > + {COLOR_PICKER_COLOR_LIST.map((color) => ( + + ))} + + +
    + {getColorChannels(space).map((channel) => ( + + ))} +
    + {onReset && ( + + )} +
    +
    +
    + ); +} diff --git a/packages/excel-collab/src/containers/FloatElement/ContextMenu.tsx b/packages/excel-collab/src/containers/FloatElement/ContextMenu.tsx index eae9964c..5392e323 100644 --- a/packages/excel-collab/src/containers/FloatElement/ContextMenu.tsx +++ b/packages/excel-collab/src/containers/FloatElement/ContextMenu.tsx @@ -1,7 +1,5 @@ -import React, { memo } from 'react'; -import { Button, info, Select, toast } from '../../components'; -import styles from './FloatElement.module.css'; -import { useClickOutside } from '../hooks'; +import React, { memo, useMemo, Fragment, useState } from 'react'; +import { Dialog } from '../../components'; import type { ChartType } from 'chart.js'; import { saveAs, @@ -14,150 +12,69 @@ import { } from '../../util'; import { useExcel, type FloatElementItem } from '../../containers/store'; import { IWindowSize } from '../../types'; -import i18n from '../../i18n'; +import i18n, { type TranslationKeys } from '../../i18n'; +import { Menu, MenuItem } from '../../component/Menu'; +import { Select, SelectItem } from '../../component/Select'; +import { useClickOutside } from '../hooks'; +import styles from './FloatElement.module.css'; +import { queue } from '../../component/Toast'; +import { TextField } from '../../component/TextField'; + +type ModalType = 'selectData' | 'changeChartTitle' | 'changeChartType'; type Props = FloatElementItem & { menuLeft: number; menuTop: number; - resetResize: (size: IWindowSize) => void; hideContextMenu: () => void; + resetResize: (size: IWindowSize) => void; }; export const FloatElementContextMenu: React.FunctionComponent = memo( (props) => { const { - menuLeft, menuTop, + menuLeft, uuid, type, chartType, title, resetResize, - hideContextMenu, originHeight, originWidth, width, height, + hideContextMenu, } = props; const { controller } = useExcel(); - const ref = useClickOutside(true, hideContextMenu); + const [dataSource, setDataSource] = useState(''); + const [isOpen, onOpenChange] = useState(false); + const ref = useClickOutside(!isOpen, hideContextMenu); + const [modalType, setModalType] = useState('selectData'); + const selectData = () => { - let value = convertToReference( + const value = convertToReference( props.chartRange!, 'absolute', (sheetId: string) => { return controller.getSheetInfo(sheetId)?.name || ''; }, ); - info({ - visible: true, - title: i18n.t('edit-data-source'), - testId: 'dialog-select-data', - children: ( - ) => { - value = event.target.value.trim(); - event.stopPropagation(); - }} - maxLength={MAX_NAME_LENGTH * 2} - data-testid="dialog-select-data-input" - /> - ), - onOk: () => { - if (!value) { - return toast.error( - i18n.t('reference-is-empty'), - 'select-data-empty-toast', - ); - } - const sheetList = controller.getSheetList(); - const range = parseReference(value, (sheetName: string) => { - return sheetList.find((v) => v.name === sheetName)?.sheetId || ''; - }); - if ( - !range || - !controller.validateRange(range) || - (props.chartRange && isSameRange(range, props.chartRange)) - ) { - return toast.error( - i18n.t('reference-is-not-valid'), - 'select-data-invalid-toast', - ); - } - range.sheetId = range.sheetId || controller.getCurrentSheetId(); - controller.updateDrawing(uuid, { chartRange: range }); - hideContextMenu(); - }, - onCancel: () => { - hideContextMenu(); - }, - }); + + setDataSource(value); + setModalType('selectData'); + onOpenChange(true); }; const changeChartTitle = () => { - let value = title.trim(); - info({ - visible: true, - title: i18n.t('change-chart-title'), - testId: 'dialog-change-chart-title', - children: ( - ) => { - value = event.target.value.trim(); - event.stopPropagation(); - }} - maxLength={MAX_NAME_LENGTH} - data-testid="dialog-change-chart-title-input" - /> - ), - onOk: () => { - if (!value) { - return toast.error( - i18n.t('the-value-cannot-be-empty'), - 'change-chart-title-toast', - ); - } - controller.updateDrawing(uuid, { title: value }); - hideContextMenu(); - }, - onCancel: () => { - hideContextMenu(); - }, - }); + setDataSource(title.trim()); + setModalType('changeChartTitle'); + onOpenChange(true); }; const changeChartType = () => { - let newChartType: ChartType = chartType!; - info({ - title: i18n.t('change-chart-type'), - testId: 'dialog-change-chart-type', - visible: true, - children: ( - { + setDataSource(String(value)); + }} + aria-label="Select chart type" + > + {CHART_TYPE_LIST.map((item) => ( + + {item.label} + + ))} + + )} + + ); }, ); diff --git a/packages/excel-collab/src/containers/FloatElement/FloatElement.module.css b/packages/excel-collab/src/containers/FloatElement/FloatElement.module.css index 7532b48a..42f0b139 100644 --- a/packages/excel-collab/src/containers/FloatElement/FloatElement.module.css +++ b/packages/excel-collab/src/containers/FloatElement/FloatElement.module.css @@ -3,16 +3,16 @@ position: absolute; top: 0; left: 0; - border: 1px solid var(--borderColor); - background-color: var(--white); - z-index: var(--lowZIndex); + border: 1px solid var(--border-color); + background-color: var(--background-color); + z-index: 2; } .float-element.active { border-style: dashed; border-style: dashed; cursor: move; - z-index: var(--middleZIndex); + z-index: 3; } .float-element-mask { @@ -29,15 +29,7 @@ .context-menu { position: fixed; - background-color: var(--white); - border-radius: var(--borderRadius); - min-width: 150px; - z-index: var(--middleZIndex); -} - -.context-menu > button { - width: 100%; - background-color: var(--white); + z-index: 3; } .title { @@ -45,8 +37,8 @@ font-weight: bold; width: 98%; border: none; - padding: var(--tinyPadding); - z-index: var(--middleZIndex); + padding: 4px; + z-index: 3; } .image { @@ -69,15 +61,15 @@ } .scale-dot { border-radius: 50%; - background-color: var(--white); + background-color: var(--background-color); width: 10px; height: 10px; - border: 1px solid var(--borderColor); + border: 1px solid var(--border-color); box-sizing: content-box; } .rotate-icon { - color: var(--white); - background-color: var(--borderColor); + color: var(--background-color); + background-color: var(--border-color); border-radius: 50%; width: 16px; height: 16px; @@ -142,9 +134,5 @@ top: 14px; height: 28px; width: 1px; - background-color: var(--borderColor); -} - -.chart-type-select { - width: 200px; + background-color: var(--border-color); } diff --git a/packages/excel-collab/src/containers/FloatElement/FloatElement.tsx b/packages/excel-collab/src/containers/FloatElement/FloatElement.tsx index f4bf987c..dca55bda 100644 --- a/packages/excel-collab/src/containers/FloatElement/FloatElement.tsx +++ b/packages/excel-collab/src/containers/FloatElement/FloatElement.tsx @@ -1,12 +1,13 @@ -import React, { useState, memo, useCallback, Suspense } from 'react'; +import React, { memo, Suspense, useState, useCallback, Fragment } from 'react'; import styles from './FloatElement.module.css'; import type { FloatElementItem } from '../../containers/store'; import { FloatElementContextMenu } from './ContextMenu'; -import { DEFAULT_POSITION, classnames } from '../../util'; +import { classnames, DEFAULT_POSITION } from '../../util'; import { ResizePosition } from './util'; -import { Icon, Loading } from '../../components'; import { IWindowSize } from '../../types'; import { Image } from './Image'; +import { ProgressCircle } from '../../component/ProgressCircle'; +import { RotateCw } from 'lucide-react'; const Chart = React.lazy(() => import('./Chart')); @@ -31,6 +32,7 @@ export const FloatElement: React.FunctionComponent = memo( pointerDown, resizeDown, } = props; + const [contextMenuPosition, setContextMenuPosition] = useState({ top: DEFAULT_POSITION, left: DEFAULT_POSITION, @@ -62,7 +64,16 @@ export const FloatElement: React.FunctionComponent = memo( ); } else if (type === 'chart') { children = ( - }> + + } + > ); @@ -71,10 +82,9 @@ export const FloatElement: React.FunctionComponent = memo( return children; } return ( - +
    = memo( height, }} data-testid="float-element" + onContextMenu={handleContextMenu} > {children} {active && ( @@ -183,7 +194,7 @@ export const FloatElement: React.FunctionComponent = memo( data-testid="float-element-rotate" >
    - +
    )} @@ -193,13 +204,13 @@ export const FloatElement: React.FunctionComponent = memo( {contextMenuPosition.top >= 0 && contextMenuPosition.left >= 0 && ( )} -
    + ); }, ); diff --git a/packages/excel-collab/src/containers/FloatElement/Image.tsx b/packages/excel-collab/src/containers/FloatElement/Image.tsx index 0a40eeb6..ae77868e 100644 --- a/packages/excel-collab/src/containers/FloatElement/Image.tsx +++ b/packages/excel-collab/src/containers/FloatElement/Image.tsx @@ -1,7 +1,7 @@ import React, { memo, useEffect, useState } from 'react'; import styles from './FloatElement.module.css'; import { useExcel } from '../store'; -import { Loading } from '../../components'; +import { ProgressCircle } from '../../component/ProgressCircle'; type Props = { title: string; @@ -31,7 +31,14 @@ export const Image: React.FunctionComponent = memo( }); }, [imageSrc, provider]); if (loading || !src) { - return ; + return ( + + ); } return ( { const { controller, provider } = useExcel(); const ref = useRef(null); const handleImport = useCallback( - async (event: React.ChangeEvent) => { + async (files: FileList | null) => { const clearUpdate = () => { if (ref.current) { ref.current.value = ''; ref.current.blur(); } }; - const file = event.target.files?.[0]; + const file = files?.[0]; if (!file) { return; } const maxSizeInBytes = 25 * 1024 * 1024; if (file.size > maxSizeInBytes) { - toast.error('max image file size 25MB'); + queue.add({ title: 'max image file size 25MB' }); clearUpdate(); return; } @@ -47,7 +49,7 @@ export const InsertFloatingPicture = memo(() => { ); } if (!imageSrc) { - toast.warning('choose image file'); + queue.add({ title: 'choose image file' }); clearUpdate(); return; } @@ -72,18 +74,16 @@ export const InsertFloatingPicture = memo(() => { [provider], ); return ( - + + + ); }); InsertFloatingPicture.displayName = 'InsertFloatingPicture'; @@ -111,7 +111,7 @@ export const InsertChart = memo(() => { }, []); return ( - ); diff --git a/packages/excel-collab/src/containers/FloatElement/chart.module.css b/packages/excel-collab/src/containers/FloatElement/chart.module.css index 1490ba0a..92a4545e 100644 --- a/packages/excel-collab/src/containers/FloatElement/chart.module.css +++ b/packages/excel-collab/src/containers/FloatElement/chart.module.css @@ -1,3 +1,3 @@ .chart { - background-color: var(--white); + background-color: var(--background-color); } diff --git a/packages/excel-collab/src/containers/FormulaBar/DefineName.tsx b/packages/excel-collab/src/containers/FormulaBar/DefineName.tsx index 52967334..3a92a770 100644 --- a/packages/excel-collab/src/containers/FormulaBar/DefineName.tsx +++ b/packages/excel-collab/src/containers/FormulaBar/DefineName.tsx @@ -31,7 +31,6 @@ export const DefineName: React.FunctionComponent = memo( const defineNameList = useCoreStore((s) => s.defineNames); const popupList = useMemo(() => { return defineNameList.map((v) => ({ - disabled: false, value: v, label: v, })); @@ -101,7 +100,7 @@ export const DefineName: React.FunctionComponent = memo( value={defineName} data={popupList} onChange={handleSelect} - className={styles['defined-name']} + className={styles['defined-name-select']} > = ({ visible, setVisible }) => { const handleClick = useCallback(() => { setVisible(true); }, []); - const handleChange = useCallback( - (event: React.ChangeEvent) => { - setValue(event.currentTarget.value.trim()); - }, - [], - ); + const handleChange = useCallback((value: unknown) => { + setValue(String(value).trim()); + }, []); const handleOk = useCallback(() => { if (!value) { return; @@ -39,25 +40,49 @@ export const File: FunctionComponent = ({ visible, setVisible }) => { setFileName(value); setVisible(false); }, [value, provider, controller]); + + const realFileName = fileName || i18n.t('default-name'); + return ( - -
    - {fileName || i18n.t('default-name')} -
    - setVisible(false)} - onOk={handleOk} + + -
    + {realFileName} + + + + {i18n.t('change-file-name')} +
    + +
    + + +
    + +
    +
    + ); }; diff --git a/packages/excel-collab/src/containers/MenuBar/I18N.tsx b/packages/excel-collab/src/containers/MenuBar/I18N.tsx index bbe7fc94..9e6fa1f5 100644 --- a/packages/excel-collab/src/containers/MenuBar/I18N.tsx +++ b/packages/excel-collab/src/containers/MenuBar/I18N.tsx @@ -1,19 +1,13 @@ import React, { memo, useCallback } from 'react'; -import { Select } from '../../components'; -import type { OptionItem, LanguageType } from '../../types'; +import type { LanguageType } from '../../types'; import styles from './index.module.css'; import i18n from '../../i18n'; import { LANGUAGE_LIST } from '../../util'; - -const dataList: OptionItem[] = LANGUAGE_LIST.map((v) => ({ - value: v, - label: v, - disabled: false, -})); +import { Select, SelectItem } from '../../component/Select'; export const I18N: React.FunctionComponent = memo(() => { - const handleChange = useCallback((c: string | number) => { - i18n.changeLanguage(String(c) as LanguageType); + const handleChange = useCallback((c: LanguageType) => { + i18n.changeLanguage(c); if (process.env.NODE_ENV !== 'test') { location.reload(); } @@ -21,11 +15,19 @@ export const I18N: React.FunctionComponent = memo(() => { return (
    ); }); diff --git a/packages/excel-collab/src/containers/MenuBar/Theme.tsx b/packages/excel-collab/src/containers/MenuBar/Theme.tsx index 94d36f04..7ed54517 100644 --- a/packages/excel-collab/src/containers/MenuBar/Theme.tsx +++ b/packages/excel-collab/src/containers/MenuBar/Theme.tsx @@ -1,39 +1,14 @@ import React, { memo, useCallback, useEffect, useState } from 'react'; -import { Button, Icon } from '../../components'; import styles from './index.module.css'; -import { - sizeConfig, - darkColor, - lightColor, - setTheme, - getTheme, -} from '../../theme'; +import { setTheme, getTheme } from '../../theme'; import { ThemeType } from '../../types'; import { useExcel } from '../store'; - -function setCssVariable(data: Record) { - const keyList = Object.keys(data); - for (const key of keyList) { - const name = `--${key}`; - const value = String(data[key] || ''); - document.documentElement.style.setProperty(name, value); - } -} - -function updateCssVariable(value: ThemeType) { - if (value === 'dark') { - setCssVariable(darkColor); - } else { - setCssVariable(lightColor); - } -} +import { Button } from '../../component/Button'; +import { Moon, Sun } from 'lucide-react'; export const Theme: React.FunctionComponent = memo(() => { const { controller } = useExcel(); const [themeData, setThemeData] = useState('light'); - useEffect(() => { - setCssVariable(sizeConfig); - }, []); useEffect(() => { setThemeData(getTheme()); if (typeof window.matchMedia === 'function') { @@ -47,7 +22,6 @@ export const Theme: React.FunctionComponent = memo(() => { useEffect(() => { setTheme(themeData); - updateCssVariable(themeData); controller.emit('renderChange', { changeSet: new Set(['cellStyle']), }); @@ -61,11 +35,11 @@ export const Theme: React.FunctionComponent = memo(() => { return (
    ); diff --git a/packages/excel-collab/src/containers/MenuBar/User.tsx b/packages/excel-collab/src/containers/MenuBar/User.tsx index 6723f689..94ea5ec1 100644 --- a/packages/excel-collab/src/containers/MenuBar/User.tsx +++ b/packages/excel-collab/src/containers/MenuBar/User.tsx @@ -1,17 +1,15 @@ -import { ProviderStatus } from '../../types'; -import { Icon } from '../../components'; import styles from './index.module.css'; import { useUserInfo } from '../store'; import i18n from '../../i18n'; +import { Wifi, WifiOff } from 'lucide-react'; export const User = () => { const clientId = useUserInfo((s) => s.clientId); - const name = navigator.onLine ? ProviderStatus.ONLINE : ProviderStatus.LOCAL; return (
    -
    {`${i18n.t('user-name')} ${clientId}`}
    - +
    {`${i18n.t('user-name')} ${clientId}`}
    + {navigator.onLine ? : }
    ); }; diff --git a/packages/excel-collab/src/containers/MenuBar/index.module.css b/packages/excel-collab/src/containers/MenuBar/index.module.css index 70f30e8d..cfca4c2b 100644 --- a/packages/excel-collab/src/containers/MenuBar/index.module.css +++ b/packages/excel-collab/src/containers/MenuBar/index.module.css @@ -1,48 +1,22 @@ .menubar-container { - border-bottom: 1px solid var(--borderColor); - padding: var(--mediumPadding) var(--largePadding); - box-sizing: border-box; + border-bottom: 1px solid var(--border-color); + padding: var(--spacing-2); display: flex; align-items: center; flex-wrap: wrap; + gap: var(--spacing-2); } .menubar-menu { flex: 1; display: flex; align-items: center; + flex-wrap: wrap; + gap: var(--spacing-2); } -.theme-button { - display: flex; - align-items: center; - justify-content: center; -} -.i18n, -.fps, -.theme { - margin-right: var(--mediumPadding); -} .user { display: flex; align-items: center; -} - -.status { - margin-right: var(--mediumPadding); - margin-left: var(--mediumPadding); - font-size: 20px; -} -.avatar { - margin-right: var(--mediumPadding); -} - -.file { - margin-right: var(--mediumPadding); - cursor: pointer; -} -.login { - display: flex; - align-items: center; - color: var(--primaryColor); + gap: var(--spacing-2); } diff --git a/packages/excel-collab/src/containers/MenuBar/index.tsx b/packages/excel-collab/src/containers/MenuBar/index.tsx index 1851c091..189aca54 100644 --- a/packages/excel-collab/src/containers/MenuBar/index.tsx +++ b/packages/excel-collab/src/containers/MenuBar/index.tsx @@ -1,21 +1,27 @@ -import React, { memo, useCallback, useState } from "react"; -import { Menu, MenuItem, SubMenu } from "../../components"; +import React, { memo, useCallback, useState } from 'react'; import { importExcel, exportExcel, EXPORT_EXTENSIONS, EXPORT_FORMATS, type ExportExtension, -} from "../Excel"; -import styles from "./index.module.css"; -import { Theme } from "./Theme"; -import i18n from "../../i18n"; -import { I18N } from "./I18N"; -import { saveAs } from "../../util"; -import { useExcel } from "../store"; -import { User } from "./User"; -import { File } from "./File"; -import { v4 } from "uuid"; +} from '../Excel'; +import styles from './index.module.css'; +import { Theme } from './Theme'; +import i18n from '../../i18n'; +import { I18N } from './I18N'; +import { saveAs } from '../../util'; +import { useExcel } from '../store'; +import { User } from './User'; +import { File } from './File'; +import { v4 } from 'uuid'; +import { + MenuTrigger, + SubmenuTrigger, + Menu, + MenuItem, +} from '../../component/Menu'; +import { Button } from '../../component/Button'; type Props = { leftChildren?: React.ReactNode; @@ -24,7 +30,7 @@ type Props = { const ACCEPT = Object.values(EXPORT_FORMATS) .map((v) => v.mime) - .join(","); + .join(','); export const MenuBarContainer: React.FunctionComponent = memo( ({ leftChildren, rightChildren }) => { @@ -41,14 +47,14 @@ export const MenuBarContainer: React.FunctionComponent = memo( } const model = await importExcel(file); controller.fromJSON(model); - event.target.value = ""; + event.target.value = ''; event.target.blur(); }, [], ); const handleExportJSON = useCallback(() => { const blob = new Blob([JSON.stringify(controller.toJSON())], { - type: "application/json", + type: 'application/json', }); saveAs(blob, `excel_${Date.now()}.json`); }, []); @@ -57,59 +63,77 @@ export const MenuBarContainer: React.FunctionComponent = memo( provider?.addDocument?.(docId); }, []); return ( -
    -
    +
    +
    - - - {i18n.t("new-file")} - - setVisible(true)} - testId="menubar-rename-excel" - > - {i18n.t("rename-file")} - - - - - - + + + + {i18n.t('new-file')} + + setVisible(true)} + textValue={i18n.t('rename-file')} + > + {i18n.t('rename-file')} + - JSON + + - - + + + {' '} + {i18n.t('export', { format: '...' })} + + + {EXPORT_EXTENSIONS.map((ext) => ( + handleExportExcel(ext)} + textValue={ext.toUpperCase()} + > + {ext.toUpperCase()} + + ))} + + + JSON + + + + + {leftChildren}
    {rightChildren} @@ -121,6 +145,6 @@ export const MenuBarContainer: React.FunctionComponent = memo( }, ); -MenuBarContainer.displayName = "MenuBarContainer"; +MenuBarContainer.displayName = 'MenuBarContainer'; export default MenuBarContainer; diff --git a/packages/excel-collab/src/containers/SheetBar/SheetBarContextMenu.tsx b/packages/excel-collab/src/containers/SheetBar/SheetBarContextMenu.tsx index edf1cc19..5f0482e5 100644 --- a/packages/excel-collab/src/containers/SheetBar/SheetBarContextMenu.tsx +++ b/packages/excel-collab/src/containers/SheetBar/SheetBarContextMenu.tsx @@ -1,10 +1,13 @@ -import React, { useMemo, memo } from 'react'; -import { Button, Select, info, ColorPicker, toast } from '../../components'; -import { OptionItem } from '../../types'; +import React, { memo, Fragment, useMemo, useState } from 'react'; +import { Dialog } from '../../components'; import styles from './index.module.css'; import { useClickOutside } from '../hooks'; import { SheetItem, useExcel } from '../store'; import i18n from '../../i18n'; +import { Select, SelectItem } from '../../component/Select'; +import { queue } from '../../component/Toast'; +import { COLOR_PICKER_COLOR_LIST } from '../../util'; +import { Menu, MenuItem, SubmenuTrigger } from '../../component/Menu'; interface Props { position: number; @@ -15,119 +18,148 @@ interface Props { } export const SheetBarContextMenu: React.FunctionComponent = memo( - ({ position, sheetList, currentSheetId, hideMenu, editSheetName }) => { + ({ position, sheetList, hideMenu, editSheetName }) => { const { controller } = useExcel(); + const [isOpen, onOpenChange] = useState(false); + const [sheetId, setSheetId] = useState(''); const ref = useClickOutside(true, hideMenu); - const tabColor = useMemo(() => { - return ( - sheetList.find((v) => v.sheetId === currentSheetId)?.tabColor || '' - ); - }, [sheetList, currentSheetId]); - const hideSheetList: OptionItem[] = useMemo(() => { + const hideSheetList = useMemo(() => { return sheetList .filter((v) => v.isHide) .map((item) => ({ value: String(item.sheetId), label: item.name, - disabled: false, })); }, [sheetList]); const handleUnhide = () => { - let value = String(hideSheetList[0]?.value) || ''; - info({ - visible: true, - title: i18n.t('unhide-sheet'), - testId: 'sheet-bar-context-menu-unhide-dialog', - children: ( - + + ); }, ); diff --git a/packages/excel-collab/src/containers/SheetBar/index.module.css b/packages/excel-collab/src/containers/SheetBar/index.module.css index 1b22f03b..e5c662f9 100644 --- a/packages/excel-collab/src/containers/SheetBar/index.module.css +++ b/packages/excel-collab/src/containers/SheetBar/index.module.css @@ -1,11 +1,11 @@ .sheet-bar-wrapper { width: 100%; box-sizing: border-box; - padding: 0 var(--mediumPadding); + padding: 0 var(--spacing-2); display: flex; align-items: center; position: relative; - background-color: var(--backgroundColor); + gap: var(--spacing-2); } .sheet-bar-list { @@ -15,21 +15,21 @@ flex: 1; display: flex; flex-wrap: nowrap; - border-left: 1px solid var(--borderColor); - box-shadow: 0 2px 10px 0 var(--scrollbarHoveColor); + border-left: 1px solid var(--border-color); + box-shadow: 0 2px 10px 0 var(--gray-400); } .sheet-bar-item { padding: 4px 10px; - border-right: 1px solid var(--borderColor); + border-right: 1px solid var(--border-color); user-select: none; position: relative; display: inline-block; } .sheet-bar-item.active { - background-color: var(--white); - color: var(--primaryColor); + background-color: var(--background-color); + color: var(--tint-900); } .sheet-bar-item-text { @@ -49,57 +49,27 @@ position: fixed; left: 0; bottom: 32px; - background-color: var(--white); - border-radius: var(--borderRadius); - width: 100px; - z-index: var(--lowZIndex); + z-index: 2; } -.sheet-bar-context-menu > button { +.sheet-bar-input { width: 100%; - background-color: var(--white); + height: 100%; + min-width: 100px; + overflow: hidden; } -.sheet-bar-input { +.sheet-bar-input .react-aria-Input { width: 100%; height: 100%; min-width: 100px; - overflow: hidden; - background-color: var(--white); + background-color: var(--background-color); outline: none; font: inherit; border: none; border-radius: unset; } -.show-block { - display: inline-block; -} - -.sheet-bar-unhide { - width: 100%; - background-color: var(--white); -} -.add-button { - margin-left: var(--mediumPadding); - background-color: var(--buttonActiveColor); - display: flex; - align-items: center; - justify-self: center; -} .menu-button { - margin-right: var(--mediumPadding); - background-color: var(--buttonActiveColor); - display: flex; - align-items: center; - justify-self: center; -} - -.sheet-bar-wrapper .select-popup { - left: var(--largePadding); - width: 200px; -} - -.unhide-select { - width: 300px; + margin-right: 8px; } diff --git a/packages/excel-collab/src/containers/SheetBar/index.tsx b/packages/excel-collab/src/containers/SheetBar/index.tsx index 8961ed0f..2cd40d0a 100644 --- a/packages/excel-collab/src/containers/SheetBar/index.tsx +++ b/packages/excel-collab/src/containers/SheetBar/index.tsx @@ -5,11 +5,13 @@ import { MAX_NAME_LENGTH, SHEET_ITEM_TEST_ID_PREFIX, } from '../../util'; -import { Button, Icon, SelectPopup } from '../../components'; import { SheetBarContextMenu } from './SheetBarContextMenu'; import styles from './index.module.css'; import { useCoreStore, useExcel } from '../../containers/store'; -import { useClickOutside } from '../hooks'; +import { Button } from '../../component/Button'; +import { Menu as MenuIcon, Plus } from 'lucide-react'; +import { MenuTrigger, Menu, MenuItem } from '../../component/Menu'; +import { TextField } from '../../component/TextField'; export const SheetBarContainer: React.FunctionComponent = memo(({ children }) => { @@ -21,13 +23,14 @@ export const SheetBarContainer: React.FunctionComponent const popupList = useMemo(() => { return sheetList .filter((v) => !v.isHide) - .map((v) => ({ value: v.sheetId, label: v.name, disabled: false })); + .map((v) => ({ value: v.sheetId, label: v.name })); }, [sheetList]); - const [popupActive, setPopupActive] = useState(false); const currentSheetId = useCoreStore((s) => s.currentSheetId); const [menuPosition, setMenuPosition] = useState(DEFAULT_POSITION); const [editing, setEditing] = useState(false); + const renameInputRef = React.useRef(null); + const handleContextMenu = useCallback( (event: React.MouseEvent) => { event.preventDefault(); @@ -38,10 +41,9 @@ export const SheetBarContainer: React.FunctionComponent [], ); const handleKeyDown = useCallback( - (event: React.KeyboardEvent) => { - event.stopPropagation(); + (event: React.KeyboardEvent) => { if (event.key === 'Enter') { - const t = event.currentTarget.value; + const t = renameInputRef.current?.value ?? ''; setEditing(false); if (!t) { return; @@ -51,10 +53,6 @@ export const SheetBarContainer: React.FunctionComponent }, [], ); - const handleChange = useCallback((value: string) => { - setPopupActive(false); - controller.setCurrentSheetId(value); - }, []); const addSheet = useCallback(() => { controller.addSheet(); }, []); @@ -64,36 +62,37 @@ export const SheetBarContainer: React.FunctionComponent const editSheetName = useCallback(() => { setEditing(true); }, []); - const togglePopup = useCallback(() => { - setPopupActive((v) => !v); - }, []); - - const ref = useClickOutside(popupActive, () => { - setPopupActive(false); - }); return (
    -
    + - {popupActive && ( - - )} -
    + { + const selectedKey = typeof v === 'string' ? v : Array.from(v)[0]; + if (!selectedKey || String(selectedKey) === currentSheetId) { + return; + } + controller.setCurrentSheetId(String(selectedKey)); + }} + > + {popupList.map((item) => ( + + {item.label} + + ))} + +
    {realSheetList.map((item) => { const isActive = currentSheetId === item.sheetId; @@ -123,14 +122,16 @@ export const SheetBarContainer: React.FunctionComponent }} > {showInput ? ( - ) : ( @@ -155,11 +156,11 @@ export const SheetBarContainer: React.FunctionComponent
    {menuPosition >= 0 && ( { controller.getActiveRange().range, ); }; - const handleColorChange = (c: string) => { - state.current.color = c; - setColor(c); + const handleColorChange = (c: unknown) => { + state.current.color = String(c); + setColor(String(c)); handleAllBorders(); }; const handleBorderStyle = (t: BorderType) => { @@ -176,76 +177,113 @@ export const BorderToolBar = memo(() => { return (
    - } - isPlain={true} - testId="toolbar-border" - position="bottom" - size="small" - portalClassName={styles.portal} - > - - {i18n.t('no-border')} - - - {i18n.t('all-borders')} - - - {i18n.t('outside-borders')} - - + + + - {i18n.t('line-color')} > - - - `} testId="toolbar-border-style"> - {Object.keys(BORDER_TYPE_MAP).map((border) => ( - handleBorderStyle(border as BorderType)} - testId={`toolbar-border-style-${border}`} - active={borderType === border} - > - {border} + {i18n.t('no-border')} + + + {i18n.t('all-borders')} + + + {i18n.t('outside-borders')} + + + {i18n.t('thick-box-border')} + + + {i18n.t('bottom-border')} + + + {i18n.t('top-border')} + + + {i18n.t('left-border')} + + + {i18n.t('right-border')} + + + + {i18n.t('line-color')} + + + {COLOR_PICKER_COLOR_LIST.map((item) => ( + handleColorChange(item)} + textValue={item} + > + {item} + + ))} + + + + + {i18n.t('line-style')} - ))} - - + + {Object.keys(BORDER_TYPE_MAP).map((border) => ( + handleBorderStyle(border as BorderType)} + data-testid={`toolbar-border-style-${border}`} + textValue={border} + > + {border} + + ))} + + + +
    ); }); diff --git a/packages/excel-collab/src/containers/ToolBar/constant.ts b/packages/excel-collab/src/containers/ToolBar/constant.ts index 6332b8ab..36f7a724 100644 --- a/packages/excel-collab/src/containers/ToolBar/constant.ts +++ b/packages/excel-collab/src/containers/ToolBar/constant.ts @@ -6,17 +6,14 @@ export const underlineOptionList: OptionItem[] = [ { value: EUnderLine.NONE, label: i18n.t('none'), - disabled: false, }, { value: EUnderLine.SINGLE, label: i18n.t('single-underline'), - disabled: false, }, { value: EUnderLine.DOUBLE, label: i18n.t('double-underline'), - disabled: false, }, ]; @@ -24,17 +21,14 @@ export const mergeOptionList: OptionItem[] = [ { value: EMergeCellType.MERGE_CENTER, label: i18n.t('merge-and-center'), - disabled: false, }, { value: EMergeCellType.MERGE_CELL, label: i18n.t('merge-cells'), - disabled: false, }, { value: EMergeCellType.MERGE_CONTENT, label: i18n.t('merge-content'), - disabled: false, }, ]; @@ -42,61 +36,49 @@ export const numberFormatOptionList = [ { value: getFormatCode(0), label: i18n.t('general'), - disabled: false, }, { value: getFormatCode(2), label: i18n.t('number'), - disabled: false, }, { value: getFormatCode(8), label: i18n.t('currency'), - disabled: false, }, { value: getFormatCode(44), label: i18n.t('accounting'), - disabled: false, }, { value: i18n.t('short-date-format'), label: i18n.t('short-date'), - disabled: false, }, { value: i18n.t('long-date-format'), label: i18n.t('long-date'), - disabled: false, }, { value: i18n.t('time-format'), label: i18n.t('time'), - disabled: false, }, { value: getFormatCode(10), label: i18n.t('percentage'), - disabled: false, }, { value: getFormatCode(12), label: i18n.t('fraction'), - disabled: false, }, { value: getFormatCode(11), label: i18n.t('scientific'), - disabled: false, }, { value: getFormatCode(49), label: i18n.t('text'), - disabled: false, }, // { // value: '', // label: ('more-number-formats'), - // disabled: false, // }, ]; diff --git a/packages/excel-collab/src/containers/ToolBar/index.module.css b/packages/excel-collab/src/containers/ToolBar/index.module.css index e357ec2d..152409d4 100644 --- a/packages/excel-collab/src/containers/ToolBar/index.module.css +++ b/packages/excel-collab/src/containers/ToolBar/index.module.css @@ -1,37 +1,12 @@ .toolbar-wrapper { - padding: var(--mediumPadding) var(--largePadding) 0 var(--largePadding); - box-sizing: border-box; - border-bottom: 1px solid var(--borderColor); + padding: var(--spacing-2); + border-bottom: 1px solid var(--border-color); display: flex; align-items: center; flex-wrap: wrap; + gap: var(--spacing-2); } -.toolbar-wrapper > * { - margin-right: 8px; - margin-bottom: var(--mediumPadding); -} -.toolbar-wrapper .merge-cell { - height: unset; - width: 150px; -} -.toolbar-wrapper .border { - height: unset; - width: 150px; -} -.toolbar-wrapper .merge-cell-button { - margin-right: 20px; - flex: 1; - border-radius: unset; -} -.toolbar-wrapper .number-format { - height: unset; - width: 170px; -} -.number-format-value { - text-align: center; - width: 100%; - margin-right: 20px; -} + .fontFamily { max-width: 200px; } @@ -48,9 +23,3 @@ .wrap-text { text-wrap: nowrap; } - -.icon-center { - display: flex; - align-items: center; - justify-content: center; -} diff --git a/packages/excel-collab/src/containers/ToolBar/index.tsx b/packages/excel-collab/src/containers/ToolBar/index.tsx index 9e6205db..c174d792 100644 --- a/packages/excel-collab/src/containers/ToolBar/index.tsx +++ b/packages/excel-collab/src/containers/ToolBar/index.tsx @@ -1,12 +1,5 @@ -import React, { useMemo, memo, useCallback } from 'react'; -import { - Icon, - Button, - Select, - FillColorIcon, - ColorPicker, - SelectList, -} from '../../components'; +import React, { useMemo, memo, useCallback, Key } from 'react'; +import { SelectList } from '../../components'; import { FONT_SIZE_LIST, QUERY_ALL_LOCAL_FONT, @@ -29,6 +22,22 @@ import { underlineOptionList, mergeOptionList, } from './constant'; +import { Select, SelectItem } from '../../component/Select'; +import { ColorPicker } from '../ColorPicker'; +import { ToggleButton } from '../../component/ToggleButton'; +import { Button } from '../../component/Button'; +import { + ArrowUpToLine, + Baseline, + PaintBucket, + Redo, + TextAlignCenter, + TextAlignEnd, + TextAlignStart, + Undo, + ArrowDownToLine, + ChevronsDownUp, +} from 'lucide-react'; export const ToolbarContainer: React.FunctionComponent = memo(({ children }) => { @@ -62,15 +71,11 @@ export const ToolbarContainer: React.FunctionComponent } return [item.label, String(item.value)]; }, [cellStyle.numberFormat]); - const getItemStyle = useCallback( - (value: string | number): React.CSSProperties => { - return { - fontFamily: String(value), - }; - }, - [], - ); - const handleFontFamilyChange = useCallback((value: string | number) => { + + const handleFontFamilyChange = useCallback((value: Key | null) => { + if (!value) { + return; + } if ( String(value) === QUERY_ALL_LOCAL_FONT && typeof window.queryLocalFonts === 'function' @@ -117,7 +122,7 @@ export const ToolbarContainer: React.FunctionComponent const paste = useCallback(() => { controller.paste(); }, []); - const setFontSize = useCallback((value: string | number) => { + const setFontSize = useCallback((value: Key | null) => { controller.updateCellStyle( { fontSize: Number(value) }, controller.getActiveRange().range, @@ -141,7 +146,7 @@ export const ToolbarContainer: React.FunctionComponent controller.getActiveRange().range, ); }, [cellStyle.isStrike]); - const setUnderline = useCallback((value: string | number) => { + const setUnderline = useCallback((value: Key | null) => { const t = Number(value); let underline = EUnderLine.NONE; if (t === EUnderLine.SINGLE) { @@ -154,15 +159,15 @@ export const ToolbarContainer: React.FunctionComponent controller.getActiveRange().range, ); }, []); - const setFillColor = useCallback((value: string) => { + const setFillColor = useCallback((value: unknown) => { controller.updateCellStyle( - { fillColor: value }, + { fillColor: String(value) }, controller.getActiveRange().range, ); }, []); - const setFontColor = useCallback((value: string) => { + const setFontColor = useCallback((value: unknown) => { controller.updateCellStyle( - { fontColor: value }, + { fontColor: String(value) }, controller.getActiveRange().range, ); }, []); @@ -247,215 +252,245 @@ export const ToolbarContainer: React.FunctionComponent }, []); return (
    - - - - - + + + + B - - - + + + + controller.updateCellStyle( + { fontColor: '' }, + controller.getActiveRange().range, + ) + } + data-testid="toolbar-font-color-picker" > - - - - - - - + - + -
    {numberFormatLabel} -
    +
    - + {children} diff --git a/packages/excel-collab/src/containers/__tests__/FormulaEditor.test.ts b/packages/excel-collab/src/containers/__tests__/FormulaEditor.test.ts index 6e6d3355..5e0eb56a 100644 --- a/packages/excel-collab/src/containers/__tests__/FormulaEditor.test.ts +++ b/packages/excel-collab/src/containers/__tests__/FormulaEditor.test.ts @@ -102,7 +102,7 @@ describe('getEditorStyle', () => { width: 100, height: 100, borderRadius: 0, - border: '2px solid var(--primaryColor)', + border: '2px solid var(--tint-900)', }; expect( getEditorStyle( diff --git a/packages/excel-collab/src/containers/canvas/BottomBar.tsx b/packages/excel-collab/src/containers/canvas/BottomBar.tsx index 0609fc32..5888f582 100644 --- a/packages/excel-collab/src/containers/canvas/BottomBar.tsx +++ b/packages/excel-collab/src/containers/canvas/BottomBar.tsx @@ -1,5 +1,5 @@ import React, { useState, memo } from 'react'; -import { Button } from '../../components'; +import { Button } from '../../component/Button'; import styles from './index.module.css'; import { scrollBar } from '../../canvas'; import i18n from '../../i18n'; @@ -55,9 +55,10 @@ export const BottomBar = memo(() => { />
    {i18n.t('rows')}
    diff --git a/packages/excel-collab/src/containers/canvas/ContextMenu.tsx b/packages/excel-collab/src/containers/canvas/ContextMenu.tsx index fd766d83..b22c3b98 100644 --- a/packages/excel-collab/src/containers/canvas/ContextMenu.tsx +++ b/packages/excel-collab/src/containers/canvas/ContextMenu.tsx @@ -1,10 +1,13 @@ -import React, { Fragment, memo, useMemo } from 'react'; -import { Button, info, toast } from '../../components'; +import React, { Fragment, memo, useMemo, useState } from 'react'; +import { Dialog } from '../../components'; import styles from './index.module.css'; import { useClickOutside } from '../hooks'; import { useActiveCell, useExcel } from '../../containers/store'; import i18n from '../../i18n'; import { IController } from '../../types'; +import { queue } from '../../component/Toast'; +import { Menu, MenuItem } from '../../component/Menu'; +import { NumberField } from '../../component/NumberField'; interface Props { top: number; @@ -18,7 +21,6 @@ enum ClickPosition { TRIANGLE, CONTENT, } - const MENU_WIDTH = 110; const ITEM_HEIGHT = 20; @@ -65,241 +67,250 @@ const threshold = 10000; export const ContextMenu: React.FunctionComponent = memo((props) => { const { controller } = useExcel(); const { top, left, hideContextMenu } = props; + const [isRow, setIsRow] = useState(false); + const [value, setValue] = useState(0); const row = useActiveCell((state) => state.row); const col = useActiveCell((state) => state.col); const colCount = useActiveCell((state) => state.colCount); const rowCount = useActiveCell((state) => state.rowCount); - const ref = useClickOutside(true, hideContextMenu); + const [isOpen, onOpenChange] = useState(false); + const ref = useClickOutside(!isOpen, hideContextMenu); const { style, position } = useMemo(() => { const temp = computeMenuStyle(top, left, controller); return temp; }, [top, left]); const handleDialog = (isRow: boolean) => { - let value = isRow ? controller.getRow(row).len : controller.getCol(col).len; - const handleChange = (event: React.ChangeEvent) => { - const t = parseInt(event.target.value, 10); - if (!isNaN(t)) { - if (t < 0) { - value = 0; - } else if (value > threshold) { - value = threshold; - } else { - value = t; - } - } - event.stopPropagation(); - }; - info({ - visible: true, - title: isRow ? i18n.t('row-height') : i18n.t('column-width'), - testId: 'context-menu-width-height-dialog', - children: ( - - ), - onOk: () => { - if (value < 0) { - return toast.error(i18n.t('greater-than-zero')); - } - if (isRow) { - controller.transaction(() => { - for (let i = 0; i < rowCount; i++) { - controller.setRowHeight(row + i, value); - } - }); - } else { - controller.transaction(() => { - for (let i = 0; i < colCount; i++) { - controller.setColWidth(col + i, value); - } - }); - } - hideContextMenu(); - }, - onCancel: () => { - hideContextMenu(); - }, - }); + setIsRow(isRow); + setValue(isRow ? controller.getRow(row).len : controller.getCol(col).len); + onOpenChange(true); }; return ( -
    - - - - {(position === ClickPosition.ROW_HEADER || - position === ClickPosition.CONTENT) && ( - - - - - )} - {(position === ClickPosition.COLUMN_HEADER || - position === ClickPosition.CONTENT) && ( - - - - - )} - {position === ClickPosition.TRIANGLE && ( - - )} - {position === ClickPosition.COLUMN_HEADER && ( - - - - - - - )} - {position === ClickPosition.ROW_HEADER && ( - - - - - - - )} -
    + {i18n.t('paste')} + + {(position === ClickPosition.ROW_HEADER || + position === ClickPosition.CONTENT) && ( + + { + hideContextMenu(); + controller.addRow(row, rowCount, true); + }} + textValue={i18n.t('insert-row-above')} + > + {i18n.t('insert-row-above')} + + { + hideContextMenu(); + controller.addRow(row, rowCount); + }} + textValue={i18n.t('insert-row-below')} + > + {i18n.t('insert-row-below')} + + + )} + {(position === ClickPosition.COLUMN_HEADER || + position === ClickPosition.CONTENT) && ( + + { + hideContextMenu(); + controller.addCol(col, colCount); + }} + textValue={i18n.t('insert-column-left')} + > + {i18n.t('insert-column-left')} + + { + hideContextMenu(); + controller.addCol(col, colCount, true); + }} + textValue={i18n.t('insert-column-right')} + > + {i18n.t('insert-column-right')} + + + )} + {position === ClickPosition.TRIANGLE && ( + { + hideContextMenu(); + controller.deleteAll(controller.getCurrentSheetId()); + }} + textValue={i18n.t('delete')} + > + {i18n.t('delete')} + + )} + {position === ClickPosition.COLUMN_HEADER && ( + + { + hideContextMenu(); + controller.deleteCol(col, colCount); + }} + textValue={i18n.t('delete-columns')} + > + {i18n.t('delete-columns')} + + { + hideContextMenu(); + controller.hideCol(col, colCount); + }} + textValue={i18n.t('hide-columns')} + > + {i18n.t('hide-columns')} + + { + hideContextMenu(); + controller.unhideCol(col, colCount); + }} + textValue={i18n.t('unhide-columns')} + > + {i18n.t('unhide-columns')} + + { + handleDialog(false); + }} + textValue={i18n.t('column-width')} + > + {i18n.t('column-width')} + + + )} + {position === ClickPosition.ROW_HEADER && ( + + { + hideContextMenu(); + controller.deleteRow(row, rowCount); + }} + textValue={i18n.t('delete-rows')} + > + {i18n.t('delete-rows')} + + { + hideContextMenu(); + controller.hideRow(row, rowCount); + }} + data-testid="context-menu-hide-row" + > + {i18n.t('hide-rows')} + + { + hideContextMenu(); + controller.unhideRow(row, rowCount); + }} + > + {i18n.t('unhide-rows')} + + { + handleDialog(true); + }} + > + {i18n.t('row-height')} + + + )} + +
    + { + if (value < 0) { + queue.add({ title: i18n.t('greater-than-zero') }); + return false; + } + if (isRow) { + controller.transaction(() => { + for (let i = 0; i < rowCount; i++) { + controller.setRowHeight(row + i, value); + } + }); + } else { + controller.transaction(() => { + for (let i = 0; i < colCount; i++) { + controller.setColWidth(col + i, value); + } + }); + } + return true; + }} + > + + + ); }); ContextMenu.displayName = 'CanvasContextMenu'; diff --git a/packages/excel-collab/src/containers/canvas/__tests__/isSupportFontFamily.test.ts b/packages/excel-collab/src/containers/canvas/__tests__/isSupportFontFamily.test.ts index 9e9cb51d..cc65bdd0 100644 --- a/packages/excel-collab/src/containers/canvas/__tests__/isSupportFontFamily.test.ts +++ b/packages/excel-collab/src/containers/canvas/__tests__/isSupportFontFamily.test.ts @@ -13,7 +13,7 @@ describe('isSupportFontFamily.test.ts', () => { return []; }, }); - expect(initFontFamilyList()).toHaveLength(1); + expect(initFontFamilyList()).toHaveLength(17); }); }); }); diff --git a/packages/excel-collab/src/containers/canvas/index.module.css b/packages/excel-collab/src/containers/canvas/index.module.css index 741d3a78..7a2914c0 100644 --- a/packages/excel-collab/src/containers/canvas/index.module.css +++ b/packages/excel-collab/src/containers/canvas/index.module.css @@ -8,70 +8,65 @@ .vertical-scroll-bar { position: absolute; - width: var(--largePadding); + width: 20px; right: 0; bottom: 0; top: 0; - background-color: var(--white); + background-color: var(--background-color); box-sizing: border-box; padding: 0 5px; - border: 1px solid var(--borderColor); - border-top-color: var(--white); + border: 1px solid var(--border-color); + border-top-color: var(--background-color); } .vertical-scroll-bar-content { position: relative; cursor: pointer; - border-radius: var(--borderRadius); - background-color: var(--scrollbarColor); - height: var(--scrollBarContent); + border-radius: 4px; + background-color: var(--gray-300); + height: 30px; } .horizontal-scroll-bar { position: absolute; - height: var(--largePadding); + height: 20px; right: 0; left: 0; bottom: 0; box-sizing: border-box; padding: 5px 0; - border: 1px solid var(--borderColor); - border-left-color: var(--white); - background-color: var(--white); + border: 1px solid var(--border-color); + border-left-color: var(--background-color); + background-color: var(--background-color); } .horizontal-scroll-bar-content { position: relative; cursor: pointer; - border-radius: var(--borderRadius); - background-color: var(--scrollbarColor); + border-radius: 4px; + background-color: var(--gray-300); height: 100%; - width: var(--scrollBarContent); + width: 30px; } .horizontal-scroll-bar-content:hover, .vertical-scroll-bar-content:hover { - background-color: var(--scrollbarHoveColor); + background-color: var(--gray-400); } .context-menu { position: fixed; - background-color: var(--white); - border-radius: var(--borderRadius); + background-color: var(--background-color); + border-radius: 4px; min-width: 150px; z-index: 4; } -.context-menu>button { - width: 100%; - background-color: var(--white); -} - .bottom-bar { position: absolute; display: none; align-items: center; - padding: var(--tinyPadding) var(--mediumPadding); + padding: 4px 8px; padding-top: 50px; right: 18px; bottom: 36px; @@ -83,25 +78,19 @@ } .bottom-bar-text { - margin: 0 var(--tinyPadding); + margin: 0 4px; } .bottom-bar-input { width: 60px; } -.add-button { - display: flex; - align-items: center; - justify-content: center; -} - .user { position: absolute; } .userContent { - background-color: var(--white); + background-color: var(--background-color); position: absolute; top: -22px; left: 0; diff --git a/packages/excel-collab/src/containers/canvas/index.tsx b/packages/excel-collab/src/containers/canvas/index.tsx index b0f18551..7f394960 100644 --- a/packages/excel-collab/src/containers/canvas/index.tsx +++ b/packages/excel-collab/src/containers/canvas/index.tsx @@ -18,6 +18,7 @@ import FloatElementContainer from '../FloatElement'; import handlerList from './event'; import Modal from './modal'; import { Collaboration } from './Collaboration'; +import { MyToastRegion } from '../../component/Toast'; function getEventData( event: React.PointerEvent, @@ -113,6 +114,7 @@ export const CanvasContainer = memo(() => { ); return ( +
    0) { - return list.map((v) => ({ value: v, label: v, disabled: false })); + return list.map((v) => + v === QUERY_ALL_LOCAL_FONT + ? { + value: v, + label: i18n.t('get-all-installed-fonts'), + disabled: false, + } + : { value: v, label: v, disabled: !check(v) }, + ); } } const list: OptionItem[] = []; for (const item of fontList) { - if (check(item)) { - list.push({ - label: item, - value: item, - disabled: false, - }); - } + list.push({ + label: item, + value: item, + disabled: !check(item), + }); } if (typeof window.queryLocalFonts === 'function') { list.push({ value: QUERY_ALL_LOCAL_FONT, label: i18n.t('get-all-installed-fonts'), - disabled: false, }); } return list; diff --git a/packages/excel-collab/src/containers/canvas/modal/Filter.tsx b/packages/excel-collab/src/containers/canvas/modal/Filter.tsx index c857b049..a90e3925 100644 --- a/packages/excel-collab/src/containers/canvas/modal/Filter.tsx +++ b/packages/excel-collab/src/containers/canvas/modal/Filter.tsx @@ -3,7 +3,8 @@ import type { IController, ModalProps, ResultType } from '../../../types'; import { MERGE_CELL_LINE_BREAK } from '../../../util'; import i18n from '../../../i18n'; import styles from './index.module.css'; -import { Button } from '../../../components'; +import { Button } from '../../../component/Button'; +import { Checkbox } from '../../../component/Checkbox'; import { useExcel } from '../../../containers/store'; type FilterItem = { @@ -85,53 +86,46 @@ export const FilterModal = ({ col, hide }: ModalProps) => {
    {i18n.t('filter')}
    - + ({dataList.length})
    {dataList.map((v, index) => (
    - + ({v.count})
    ))}
    - +