diff --git a/components.json b/components.json new file mode 100644 index 0000000..2f375bc --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/app/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/package.json b/package.json index 7d1c012..e29f603 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "react-window": "^1.8.11", "redux": "^4.2.1", "tailwind-merge": "^3.3.1", + "tailwindcss-animate": "^1.0.7", "use-debounce": "^10.0.5" }, "devDependencies": { diff --git a/src/app/index.css b/src/app/index.css index 1a4037e..d924e24 100644 --- a/src/app/index.css +++ b/src/app/index.css @@ -10,20 +10,25 @@ --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; - --primary: 221.2 83.2% 53.3%; + --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96%; - --secondary-foreground: 222.2 84% 4.9%; - --muted: 210 40% 96%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96%; - --accent-foreground: 222.2 84% 4.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; - --ring: 221.2 83.2% 53.3%; + --ring: 222.2 84% 4.9%; --radius: 0.5rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; } .dark { @@ -34,7 +39,7 @@ --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; --primary: 210 40% 98%; - --primary-foreground: 222.2 84% 4.9%; + --primary-foreground: 222.2 47.4% 11.2%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; @@ -46,6 +51,11 @@ --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; } } diff --git a/src/component/BasicElements/SelectElement.tsx b/src/component/BasicElements/SelectElement.tsx index 78ed474..fd38661 100644 --- a/src/component/BasicElements/SelectElement.tsx +++ b/src/component/BasicElements/SelectElement.tsx @@ -3,6 +3,7 @@ import { Input } from '@/component/ui/input' import { Select, SelectContent, + SelectGroup, SelectItem, SelectTrigger, SelectValue, @@ -104,30 +105,34 @@ export const selectDefinition: ElementDefinition = { ) }, Preview: ({ config }) => ( -
-
Предпросмотр
-
- -
-
- ), - Render: ({ config, value, onChange }) => ( - + - {config.options.map((option, index) => ( - - {option.label} - - ))} + + {config.options.map((option, index) => ( + + {option.label} + + ))} + + + + ), + Render: ({ config, value, onChange }) => ( + ), diff --git a/src/component/DualSpreadsheet/DualSpreadsheet.tsx b/src/component/DualSpreadsheet/DualSpreadsheet.tsx index 6fc008b..6f3b95a 100644 --- a/src/component/DualSpreadsheet/DualSpreadsheet.tsx +++ b/src/component/DualSpreadsheet/DualSpreadsheet.tsx @@ -41,6 +41,9 @@ const DualSpreadsheet: FC = ({ templateId, onEngineReady, enableAutoSave = true, + excelFileName, + isExcelLoading = false, + onExcelFileChange, }) => { // Мемоизируем конфигурацию листов const sheetsConfig = useMemo( @@ -345,11 +348,7 @@ const DualSpreadsheet: FC = ({ }, [isEditing]) // Синхронизация между formula bar и ячейкой при редактировании - useEffect(() => { - if (isEditing && formulaBarRef.current) { - formulaBarRef.current.value = editingValue - } - }, [editingValue, isEditing]) + // Теперь полагаемся на controlled компонент через formulaBarValue // Обновление подсветки ячеек при вводе формулы useEffect(() => { @@ -774,7 +773,10 @@ const DualSpreadsheet: FC = ({ // Обычный клик - очищаем множественное выделение setMultiSelection([]) setSelectedCell({ row, col }) - containerRefs.current[activeSheet]?.focus({ preventScroll: true }) + // Не возвращаем фокус на контейнер, если он сейчас на formula bar + if (document.activeElement !== formulaBarRef.current) { + containerRefs.current[activeSheet]?.focus({ preventScroll: true }) + } }, [isEditing, stopEditing, activeSheet, selectedCell, copiedCells] ) @@ -919,8 +921,30 @@ const DualSpreadsheet: FC = ({ const handleFormulaBarFocus = useCallback(() => { if (selectedCell && !isEditing) { + // Получаем позицию клика, если она была сохранена + const clickPosition = formulaBarRef.current?.dataset.clickPosition + const cursorPosition = clickPosition + ? parseInt(clickPosition) + : formulaBarRef.current?.selectionStart || 0 + setEditingSource('formula') startEditing() + + // Восстанавливаем позицию курсора после того как React обновит value + setTimeout(() => { + if (formulaBarRef.current) { + if (document.activeElement !== formulaBarRef.current) { + formulaBarRef.current.focus() + } + // Восстанавливаем вычисленную позицию курсора + formulaBarRef.current.setSelectionRange( + cursorPosition, + cursorPosition + ) + // Очищаем сохраненную позицию + delete formulaBarRef.current.dataset.clickPosition + } + }, 0) } }, [selectedCell, isEditing, startEditing]) @@ -1155,6 +1179,9 @@ const DualSpreadsheet: FC = ({ lastSaveError={lastSaveError} onManualSave={manualSave} onClearError={clearError} + excelFileName={excelFileName} + isExcelLoading={isExcelLoading} + onExcelFileChange={onExcelFileChange} />
) => { return ( -
-
-
+
+
+ {/* Адрес ячейки */} +
{selectedCell ? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}` : 'A1'}
+ + {/* Строка формул */}
onValueChange(e.target.value)} onFocus={onFocus} onBlur={e => { - // Проверяем, не переходит ли фокус в ячейку редактирования const relatedTarget = e.relatedTarget as HTMLElement if ( relatedTarget && relatedTarget.closest('input[type="text"]') ) { - return // Не завершаем редактирование + return } onBlur(e) }} onKeyDown={onKeyDown} + onMouseDown={e => { + // Если поле не в режиме редактирования, нужно запустить редактирование + // но сохранить позицию курсора + if (!isEditing) { + // Вычисляем позицию клика вручную + const input = e.currentTarget + const rect = input.getBoundingClientRect() + const clickX = + e.clientX - + rect.left - + parseInt(getComputedStyle(input).paddingLeft) + + // Создаем временный range для определения позиции + const tempSpan = document.createElement('span') + tempSpan.textContent = input.value + tempSpan.style.cssText = getComputedStyle(input).cssText + tempSpan.style.position = 'absolute' + tempSpan.style.visibility = 'hidden' + tempSpan.style.whiteSpace = 'pre' + document.body.appendChild(tempSpan) + + let position = 0 + let bestDistance = Infinity + + for (let i = 0; i <= input.value.length; i++) { + tempSpan.textContent = input.value.substring(0, i) + const distance = Math.abs(tempSpan.offsetWidth - clickX) + if (distance < bestDistance) { + bestDistance = distance + position = i + } + } + + document.body.removeChild(tempSpan) + + // Сохраняем вычисленную позицию в data-атрибуте + input.dataset.clickPosition = position.toString() + + // Запускаем редактирование + onFocus() + } + }} /> +
+ + {/* Правая группа: индикаторы и действия */} +
+ {/* Индикатор процесса */} {(isCalculating || isSaving || isLoading) && (
@@ -100,6 +151,7 @@ export const FormulaBar = memo(
)} + {/* Ошибка сохранения */} {lastSaveError && (
@@ -120,15 +172,15 @@ export const FormulaBar = memo( className="text-xs text-destructive" title={lastSaveError} > - Ошибка сохранения + Ошибка {onClearError && ( - -
- } - {/* Кнопка ручного сохранения */} + + {/* Кнопка сохранения */} {onManualSave && ( )} + + {/* ExcelUploadPanel */} + {onExcelFileChange && ( + {}} + onFileChange={onExcelFileChange} + /> + )}
diff --git a/src/component/DualSpreadsheet/types.ts b/src/component/DualSpreadsheet/types.ts index c35cc0b..a9d7a82 100644 --- a/src/component/DualSpreadsheet/types.ts +++ b/src/component/DualSpreadsheet/types.ts @@ -53,6 +53,10 @@ export interface DualSpreadsheetProps { templateId?: string // Добавляем ID шаблона для сохранения onEngineReady?: (engine: any) => void // Коллбэк, который отдаёт родителю методы движка enableAutoSave?: boolean // Возможность отключить автосохранение + // Пропсы для ExcelUploadPanel + excelFileName?: string + isExcelLoading?: boolean + onExcelFileChange?: (e: React.ChangeEvent) => void } export interface CellProps { @@ -122,6 +126,10 @@ export interface FormulaBarProps { lastSaveError?: string | null onManualSave?: () => void onClearError?: () => void + // Пропсы для ExcelUploadPanel + excelFileName?: string + isExcelLoading?: boolean + onExcelFileChange?: (e: React.ChangeEvent) => void } export interface CellRendererData { diff --git a/src/component/TemplateManager/ExcelUploadPanel.tsx b/src/component/TemplateManager/ExcelUploadPanel.tsx index d971f85..658d741 100644 --- a/src/component/TemplateManager/ExcelUploadPanel.tsx +++ b/src/component/TemplateManager/ExcelUploadPanel.tsx @@ -1,4 +1,5 @@ import { Button } from '@/component/ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '@/component/ui/popover' import { FileText, Upload } from 'lucide-react' import React, { useRef } from 'react' @@ -18,47 +19,82 @@ export const ExcelUploadPanel: React.FC = ({ const fileInputRef = useRef(null) return ( -
- - -
- - + +
+ + {isLoading ? ( +
+ ) : ( + + Шаблон + + )} + + + +
+
+

Excel файл

+

+ Загрузите или обновите Excel файл для шаблона +

+
- -
+ {fileName && ( +
+ +
+ + + {fileName} + +
+
+ )} + +
+ + +
+
+ + ) } diff --git a/src/component/TemplateManager/FormattingToolbar.tsx b/src/component/TemplateManager/FormattingToolbar.tsx index afaf548..69a57b2 100644 --- a/src/component/TemplateManager/FormattingToolbar.tsx +++ b/src/component/TemplateManager/FormattingToolbar.tsx @@ -18,6 +18,7 @@ export const FormattingToolbar: React.FC = () => (
{[Bold, Italic, Underline].map((Icon, i) => ( + +
{/* Кнопки справа */}
- {/* Центрированные кнопки */} -
- - -
diff --git a/tailwind.config.js b/tailwind.config.js index ede53a0..1bad2f1 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,49 +1,57 @@ /** @type {import('tailwindcss').Config} */ export default { - content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + darkMode: ['class'], + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], theme: { - extend: { - colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)', - }, - }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + } + } }, - plugins: [], + plugins: [require("tailwindcss-animate")], } diff --git a/yarn.lock b/yarn.lock index 30e9e37..b3a3330 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3744,6 +3744,11 @@ tailwind-merge@^3.3.1: resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.3.1.tgz#a7e7db7c714f6020319e626ecfb7e7dac8393a4b" integrity sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g== +tailwindcss-animate@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4" + integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA== + tailwindcss@^3.1, tailwindcss@^3.3.2: version "3.4.17" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63"