diff --git a/.yarn/cache/@cbor-extract-cbor-extract-win32-x64-npm-2.1.1-b206bdfc73-8.zip b/.yarn/cache/@cbor-extract-cbor-extract-win32-x64-npm-2.1.1-b206bdfc73-8.zip new file mode 100644 index 00000000000..d4141549c16 Binary files /dev/null and b/.yarn/cache/@cbor-extract-cbor-extract-win32-x64-npm-2.1.1-b206bdfc73-8.zip differ diff --git a/packages/api/package.json b/packages/api/package.json index 05fe68df77e..d2da9e5fbbb 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -16,7 +16,7 @@ }, "license": "AGPL-3.0", "scripts": { - "clean": "rm -fr dist", + "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"", "prestart": "yarn clean", "start": "tsc -p tsconfig.json --watch", "prebuild": "yarn clean", diff --git a/packages/snjs/package.json b/packages/snjs/package.json index 5eb2f89680b..bb4e1612b16 100644 --- a/packages/snjs/package.json +++ b/packages/snjs/package.json @@ -25,7 +25,7 @@ "scripts": { "start": "webpack -w --config webpack.dev.js", "start:test-server": "yarn node e2e-server.js", - "clean": "rm -fr dist", + "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"", "prebuild": "yarn clean", "build": "yarn tsc && webpack --config webpack.prod.js", "watch": "webpack --config webpack.prod.js --watch", diff --git a/packages/web/package.json b/packages/web/package.json index c317b761728..77160127920 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -10,7 +10,7 @@ ], "scripts": { "build": "yarn clean && yarn copy:components && webpack --config web.webpack.prod.js && yarn tsc", - "clean": "rm -fr dist", + "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"", "format": "prettier --write src/javascripts", "lint": "eslint src/javascripts && yarn tsc", "lint:fix": "eslint src/javascripts --fix", diff --git a/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx b/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx index 9778b436a58..69dde0e3fbb 100644 --- a/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx +++ b/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx @@ -8,11 +8,15 @@ import NoteView from '../NoteView/NoteView' import { NoteViewController } from '../NoteView/Controller/NoteViewController' import { FileViewController } from '../NoteView/Controller/FileViewController' import { WebApplication } from '@/Application/WebApplication' +import { TabBar } from './TabBar' type State = { showMultipleSelectedNotes: boolean showMultipleSelectedFiles: boolean controllers: (NoteViewController | FileViewController)[] + activeControllerIndex: number + splitControllerIndex: number | undefined + focusedPane: 'primary' | 'secondary' selectedFile: FileItem | undefined selectedPane?: AppPaneId isInMobileView?: boolean @@ -28,10 +32,14 @@ class NoteGroupView extends AbstractComponent { constructor(props: Props) { super(props, props.application) + const controllerGroup = props.application.itemControllerGroup this.state = { showMultipleSelectedNotes: false, showMultipleSelectedFiles: false, controllers: [], + activeControllerIndex: controllerGroup.activeControllerIndex || 0, + splitControllerIndex: controllerGroup.splitControllerIndex, + focusedPane: controllerGroup.focusedPane || 'primary', selectedFile: undefined, } } @@ -44,6 +52,9 @@ class NoteGroupView extends AbstractComponent { const controllers = controllerGroup.itemControllers this.setState({ controllers: controllers, + activeControllerIndex: controllerGroup.activeControllerIndex, + splitControllerIndex: controllerGroup.splitControllerIndex, + focusedPane: controllerGroup.focusedPane, }) }) @@ -91,23 +102,75 @@ class NoteGroupView extends AbstractComponent { !this.state.showMultipleSelectedNotes && !this.state.showMultipleSelectedFiles const hasControllers = this.state.controllers.length > 0 + const itemControllerGroup = this.application.itemControllerGroup + + const primaryController = this.state.controllers[this.state.activeControllerIndex] + const secondaryController = this.state.splitControllerIndex !== undefined + ? this.state.controllers[this.state.splitControllerIndex] + : undefined + + const renderController = (controller: NoteViewController | FileViewController) => { + return controller instanceof NoteViewController ? ( + + ) : ( + + ) + } return ( <> + {shouldNotShowMultipleSelectedItems && hasControllers && ( + + )} + {this.state.showMultipleSelectedNotes && } {this.state.showMultipleSelectedFiles && ( )} + {shouldNotShowMultipleSelectedItems && hasControllers && ( - <> - {this.state.controllers.map((controller) => { - return controller instanceof NoteViewController ? ( - - ) : ( - - ) - })} - +
+ {secondaryController !== undefined ? ( +
+
{ + if (itemControllerGroup.focusedPane !== 'primary') { + itemControllerGroup.focusedPane = 'primary' + itemControllerGroup.notifyObservers() + } + }} + > +
+ {primaryController && renderController(primaryController)} +
+
{ + if (itemControllerGroup.focusedPane !== 'secondary') { + itemControllerGroup.focusedPane = 'secondary' + itemControllerGroup.notifyObservers() + } + }} + > +
+ {renderController(secondaryController)} +
+
+ ) : ( + primaryController && renderController(primaryController) + )} +
)} ) diff --git a/packages/web/src/javascripts/Components/NoteGroupView/TabBar.tsx b/packages/web/src/javascripts/Components/NoteGroupView/TabBar.tsx new file mode 100644 index 00000000000..7e107c05166 --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteGroupView/TabBar.tsx @@ -0,0 +1,198 @@ +import React, { useState, useEffect, useRef } from 'react' +import { WebApplication } from '@/Application/WebApplication' +import { NoteViewController } from '../NoteView/Controller/NoteViewController' +import { FileViewController } from '../NoteView/Controller/FileViewController' + +type TabBarProps = { + application: WebApplication + activeControllerIndex: number + splitControllerIndex: number | undefined + focusedPane: 'primary' | 'secondary' + controllers: (NoteViewController | FileViewController)[] +} + +export const TabBar: React.FC = ({ + application, + activeControllerIndex, + splitControllerIndex, + focusedPane, + controllers, +}) => { + const itemControllerGroup = application.itemControllerGroup + const [contextMenu, setContextMenu] = useState<{ + x: number + y: number + index: number + } | null>(null) + + const menuRef = useRef(null) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setContextMenu(null) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + + if (controllers.length === 0) { + return null + } + + const handleTabClick = (index: number) => { + // If split screen is active, determine which pane we are clicking in + // For simplicity, clicking a tab will focus that index as the primary tab + if (splitControllerIndex !== undefined && index === splitControllerIndex) { + itemControllerGroup.focusedPane = 'secondary' + } else { + itemControllerGroup.activeControllerIndex = index + itemControllerGroup.focusedPane = 'primary' + } + itemControllerGroup.notifyObservers() + } + + const handleCloseClick = (e: React.MouseEvent, index: number) => { + e.stopPropagation() + itemControllerGroup.closeTab(index) + } + + const handleContextMenu = (e: React.MouseEvent, index: number) => { + e.preventDefault() + setContextMenu({ + x: e.clientX, + y: e.clientY, + index, + }) + } + + const handleCloseTab = () => { + if (contextMenu !== null) { + itemControllerGroup.closeTab(contextMenu.index) + setContextMenu(null) + } + } + + const handleCloseOthers = () => { + if (contextMenu !== null) { + const targetController = controllers[contextMenu.index] + for (const controller of [...controllers]) { + if (controller !== targetController) { + itemControllerGroup.closeItemController(controller, { notify: false }) + } + } + itemControllerGroup.activeControllerIndex = 0 + itemControllerGroup.splitControllerIndex = undefined + itemControllerGroup.focusedPane = 'primary' + itemControllerGroup.notifyObservers() + setContextMenu(null) + } + } + + const handleSplitTab = () => { + if (contextMenu !== null) { + itemControllerGroup.splitTab(contextMenu.index) + setContextMenu(null) + } + } + + const getTabTitle = (controller: NoteViewController | FileViewController) => { + if (controller instanceof NoteViewController) { + return controller.item?.title || 'Untitled note' + } else { + return (controller.item as any)?.title || (controller.item as any)?.name || 'Untitled file' + } + } + + return ( +
+
+ {controllers.map((controller, index) => { + const isPrimaryActive = activeControllerIndex === index + const isSecondaryActive = splitControllerIndex === index + const isFocused = (focusedPane === 'primary' && isPrimaryActive) || + (focusedPane === 'secondary' && isSecondaryActive) + const isOpened = isPrimaryActive || isSecondaryActive + + let tabClassName = 'tab-item' + if (isOpened) { + tabClassName += ' tab-item-active' + } + if (isFocused) { + tabClassName += ' tab-item-focused' + } + + return ( +
handleTabClick(index)} + onContextMenu={(e) => handleContextMenu(e, index)} + > + {getTabTitle(controller)} + {controllers.length > 1 && ( + + )} +
+ ) + })} + + +
+ +
+ +
+ + {contextMenu && ( +
+
+ Close Tab +
+
+ Close Other Tabs +
+
+ Split Screen +
+
+ )} +
+ ) +} diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts b/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts index 101c80404c4..11454b85cce 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts @@ -22,6 +22,10 @@ export class ItemGroupController { changeObservers: ItemControllerGroupChangeCallback[] = [] eventObservers: (() => void)[] = [] + public activeControllerIndex = 0 + public splitControllerIndex: number | undefined = undefined + public focusedPane: 'primary' | 'secondary' = 'primary' + constructor( private items: ItemManagerInterface, private mutator: MutatorClientInterface, @@ -42,20 +46,35 @@ export class ItemGroupController { this.changeObservers.length = 0 - for (const controller of this.itemControllers) { + for (const controller of [...this.itemControllers]) { this.closeItemController(controller, { notify: false }) } this.itemControllers.length = 0 } - async createItemController(context: { - file?: FileItem - note?: SNNote - templateOptions?: TemplateNoteViewControllerOptions - }): Promise { - if (this.activeItemViewController) { - this.closeItemController(this.activeItemViewController, { notify: false }) + async createItemController( + context: { + file?: FileItem + note?: SNNote + templateOptions?: TemplateNoteViewControllerOptions + }, + options: { openInNewTab?: boolean; forceNewTab?: boolean } = {} + ): Promise { + // Check if the item is already open in any existing tab + if (!options.forceNewTab && (context.note || context.file)) { + const targetUuid = context.note?.uuid || context.file?.uuid + const existingIndex = this.itemControllers.findIndex((c) => c.item?.uuid === targetUuid) + if (existingIndex !== -1) { + if (this.splitControllerIndex !== undefined && existingIndex === this.splitControllerIndex) { + this.focusedPane = 'secondary' + } else { + this.activeControllerIndex = existingIndex + this.focusedPane = 'primary' + } + this.notifyObservers() + return this.itemControllers[existingIndex] + } } let controller!: NoteViewController | FileViewController @@ -91,25 +110,160 @@ export class ItemGroupController { throw Error('Invalid input to createItemController') } - this.itemControllers.push(controller) - await controller.initialize() + const shouldNewTab = options.openInNewTab || this.itemControllers.length === 0 + + if (shouldNewTab) { + this.itemControllers.push(controller) + if (this.focusedPane === 'secondary' && this.splitControllerIndex !== undefined) { + this.splitControllerIndex = this.itemControllers.length - 1 + } else { + this.activeControllerIndex = this.itemControllers.length - 1 + this.focusedPane = 'primary' + } + } else { + const targetIndex = (this.focusedPane === 'secondary' && this.splitControllerIndex !== undefined) + ? this.splitControllerIndex + : this.activeControllerIndex + + const oldController = this.itemControllers[targetIndex] + if (oldController) { + if (oldController instanceof NoteViewController) { + oldController.syncOnlyIfLargeNote() + } + oldController.deinit() + } + + this.itemControllers[targetIndex] = controller + } + this.notifyObservers() return controller } + public selectControllerIndex(index: number): void { + if (index >= 0 && index < this.itemControllers.length) { + this.activeControllerIndex = index + this.focusedPane = 'primary' + this.notifyObservers() + } + } + + public async openNewTemplateTab(): Promise { + await this.createItemController({ + templateOptions: { + title: '', + autofocusBehavior: 'editor', + } + }, { openInNewTab: true }) + } + + public splitTab(index: number): void { + if (index >= 0 && index < this.itemControllers.length) { + this.splitControllerIndex = index + this.focusedPane = 'secondary' + this.notifyObservers() + } + } + + public toggleSplitScreen(): void { + if (this.splitControllerIndex !== undefined) { + this.splitControllerIndex = undefined + this.focusedPane = 'primary' + } else { + if (this.itemControllers.length > 1) { + const otherIndex = this.itemControllers.findIndex((_, idx) => idx !== this.activeControllerIndex) + this.splitControllerIndex = otherIndex !== -1 ? otherIndex : undefined + } else { + const currentController = this.itemControllers[this.activeControllerIndex] + if (currentController) { + void this.createItemController({ + note: currentController.item instanceof SNNote ? currentController.item : undefined, + file: currentController.item instanceof FileItem ? currentController.item : undefined, + }, { openInNewTab: true, forceNewTab: true }).then((newController) => { + const newIndex = this.itemControllers.indexOf(newController) + this.splitControllerIndex = newIndex + this.focusedPane = 'secondary' + this.notifyObservers() + }) + return + } + } + } + this.notifyObservers() + } + + public closeTab(index: number): void { + if (index < 0 || index >= this.itemControllers.length) { + return + } + + const controller = this.itemControllers[index] + if (controller instanceof NoteViewController) { + controller.syncOnlyIfLargeNote() + } + controller.deinit() + + this.itemControllers.splice(index, 1) + + if (this.activeControllerIndex >= this.itemControllers.length) { + this.activeControllerIndex = Math.max(0, this.itemControllers.length - 1) + } + + if (this.splitControllerIndex !== undefined) { + if (this.splitControllerIndex === index) { + this.splitControllerIndex = undefined + this.focusedPane = 'primary' + } else if (this.splitControllerIndex > index) { + this.splitControllerIndex-- + } + } + + if (this.itemControllers.length === 0) { + this.activeControllerIndex = 0 + this.splitControllerIndex = undefined + this.focusedPane = 'primary' + } + + this.notifyObservers() + } + public closeItemController( controller: NoteViewController | FileViewController, { notify = true }: { notify: boolean } = { notify: true }, ): void { + const index = this.itemControllers.indexOf(controller) + if (index === -1) { + return + } + if (controller instanceof NoteViewController) { controller.syncOnlyIfLargeNote() } controller.deinit() - removeFromArray(this.itemControllers, controller) + this.itemControllers.splice(index, 1) + + if (this.activeControllerIndex >= this.itemControllers.length) { + this.activeControllerIndex = Math.max(0, this.itemControllers.length - 1) + } + + if (this.splitControllerIndex !== undefined) { + if (this.splitControllerIndex === index) { + this.splitControllerIndex = undefined + this.focusedPane = 'primary' + } else if (this.splitControllerIndex > index) { + this.splitControllerIndex-- + } + } + + if (this.itemControllers.length === 0) { + this.activeControllerIndex = 0 + this.splitControllerIndex = undefined + this.focusedPane = 'primary' + } if (notify) { this.notifyObservers() @@ -125,7 +279,7 @@ export class ItemGroupController { } closeAllItemControllers(): void { - for (const controller of this.itemControllers) { + for (const controller of [...this.itemControllers]) { this.closeItemController(controller, { notify: false }) } @@ -133,7 +287,10 @@ export class ItemGroupController { } get activeItemViewController(): NoteViewController | FileViewController | undefined { - return this.itemControllers[0] + if (this.focusedPane === 'secondary' && this.splitControllerIndex !== undefined) { + return this.itemControllers[this.splitControllerIndex] + } + return this.itemControllers[this.activeControllerIndex] } /** @@ -152,7 +309,7 @@ export class ItemGroupController { } } - private notifyObservers(): void { + public notifyObservers(): void { for (const observer of this.changeObservers) { observer(this.activeItemViewController) } diff --git a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx index c5fee3d2e77..7737f455ee4 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx @@ -97,6 +97,27 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => { closeMenuAndToggleNotesList() }, [closeMenuAndToggleNotesList, notesController]) + const openInNewTab = useCallback(async () => { + if (notes[0]) { + await application.itemListController.openNote(notes[0].uuid, true) + closeMenu() + } + }, [application, notes, closeMenu]) + + const openInSplitScreen = useCallback(async () => { + if (notes[0]) { + const newController = await application.itemControllerGroup.createItemController( + { note: notes[0] }, + { openInNewTab: true } + ) + const newIndex = application.itemControllerGroup.itemControllers.indexOf(newController) + if (newIndex !== -1) { + application.itemControllerGroup.splitTab(newIndex) + } + closeMenu() + } + }, [application, notes, closeMenu]) + const openRevisionHistoryModal = useCallback(() => { application.historyModalController.openModal(notesController.firstSelectedNote) }, [application.historyModalController, notesController.firstSelectedNote]) @@ -150,6 +171,16 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => { <> {notes.length === 1 && ( <> + + + + Open in new tab + + + + Open in split screen + + diff --git a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts index e407a8262b4..e50413eb0a8 100644 --- a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts +++ b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts @@ -371,8 +371,8 @@ export class ItemListController return this.getActiveItemController()?.item } - async openNote(uuid: string): Promise { - if (this.activeControllerItem?.uuid === uuid) { + async openNote(uuid: string, openInNewTab?: boolean): Promise { + if (!openInNewTab && this.activeControllerItem?.uuid === uuid) { return } @@ -382,13 +382,13 @@ export class ItemListController return } - await this.itemControllerGroup.createItemController({ note }) + await this.itemControllerGroup.createItemController({ note }, { openInNewTab }) await this.publishCrossControllerEventSync(CrossControllerEvent.ActiveEditorChanged) } - async openFile(fileUuid: string): Promise { - if (this.getActiveItemController()?.item.uuid === fileUuid) { + async openFile(fileUuid: string, openInNewTab?: boolean): Promise { + if (!openInNewTab && this.getActiveItemController()?.item.uuid === fileUuid) { return } @@ -398,7 +398,7 @@ export class ItemListController return } - await this.itemControllerGroup.createItemController({ file }) + await this.itemControllerGroup.createItemController({ file }, { openInNewTab }) } setCompletedFullSync = (completed: boolean) => { @@ -1157,10 +1157,14 @@ export class ItemListController if (this.selectedItemsCount === 1) { const item = this.firstSelectedItem + const hasMeta = this.keyboardService.activeModifiers.has(KeyboardModifier.Meta) + const hasCtrl = this.keyboardService.activeModifiers.has(KeyboardModifier.Ctrl) + const openInNewTab = userTriggered && (hasMeta || hasCtrl) + if (item.content_type === ContentType.TYPES.Note) { - await this.openNote(item.uuid) + await this.openNote(item.uuid, openInNewTab) } else if (item.content_type === ContentType.TYPES.File) { - await this.openFile(item.uuid) + await this.openFile(item.uuid, openInNewTab) } this.recents.add(item.uuid) diff --git a/packages/web/src/stylesheets/_tabs.scss b/packages/web/src/stylesheets/_tabs.scss new file mode 100644 index 00000000000..efb64d3b47a --- /dev/null +++ b/packages/web/src/stylesheets/_tabs.scss @@ -0,0 +1,252 @@ +.tab-bar-container { + display: flex; + align-items: center; + justify-content: space-between; + background-color: var(--sn-stylekit-bg-contrast-low, #f4f4f5); + border-bottom: 1px solid var(--sn-stylekit-border-color, #e4e4e7); + padding: 0 8px; + height: 38px; + user-select: none; + z-index: 20; + + // Adapt to dark themes dynamically + .theme-dark & { + background-color: var(--sn-stylekit-bg-contrast-low, #18181b); + border-bottom: 1px solid var(--sn-stylekit-border-color, #27272a); + } +} + +.tab-bar-tabs { + display: flex; + overflow-x: auto; + height: 100%; + align-items: flex-end; + gap: 4px; + scrollbar-width: none; // Hide scrollbar for standard Firefox + + &::-webkit-scrollbar { + display: none; // Hide scrollbar for Chrome/Safari + } +} + +.tab-item { + display: flex; + align-items: center; + height: 32px; + padding: 0 12px; + border-radius: 6px 6px 0 0; + background-color: transparent; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + font-size: 13px; + max-width: 160px; + border: 1px solid transparent; + border-bottom: none; + color: var(--sn-stylekit-input-placeholder-color, #71717a); + position: relative; + + &:hover { + background-color: var(--sn-stylekit-bg-contrast-medium, rgba(0, 0, 0, 0.05)); + color: var(--sn-stylekit-text-color, #18181b); + + .theme-dark & { + background-color: var(--sn-stylekit-bg-contrast-medium, rgba(255, 255, 255, 0.05)); + color: var(--sn-stylekit-text-color, #f4f4f5); + } + } +} + +.tab-item-active { + background-color: var(--editor-background-color, #ffffff) !important; + color: var(--editor-foreground-color, #18181b) !important; + border-color: var(--sn-stylekit-border-color, #e4e4e7) !important; + border-bottom: 1px solid var(--editor-background-color, #ffffff) !important; + margin-bottom: -1px; + font-weight: 500; + z-index: 21; + + .theme-dark & { + background-color: var(--editor-background-color, #09090b) !important; + color: var(--editor-foreground-color, #f4f4f5) !important; + border-color: var(--sn-stylekit-border-color, #27272a) !important; + border-bottom: 1px solid var(--editor-background-color, #09090b) !important; + } +} + +.tab-item-focused { + border-top: 2px solid var(--sn-stylekit-primary-color, #3b82f6) !important; +} + +.tab-add-btn { + background: none; + border: none; + color: var(--sn-stylekit-input-placeholder-color, #71717a); + cursor: pointer; + height: 28px; + width: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + font-size: 14px; + margin-bottom: 2px; + align-self: center; + transition: all 0.2s; + + &:hover { + background-color: var(--sn-stylekit-bg-contrast-medium, rgba(0, 0, 0, 0.05)); + color: var(--sn-stylekit-text-color, #18181b); + + .theme-dark & { + background-color: var(--sn-stylekit-bg-contrast-medium, rgba(255, 255, 255, 0.05)); + color: var(--sn-stylekit-text-color, #f4f4f5); + } + } +} + +.tab-title-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-right: 8px; +} + +.tab-close-btn { + background: none; + border: none; + color: inherit; + cursor: pointer; + padding: 2px 4px; + border-radius: 4px; + font-size: 9px; + opacity: 0.4; + transition: opacity 0.2s, background-color 0.2s; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + opacity: 1; + background-color: rgba(0, 0, 0, 0.08); + + .theme-dark & { + background-color: rgba(255, 255, 255, 0.15); + } + } +} + +.tab-bar-actions { + display: flex; + align-items: center; + gap: 4px; + padding-left: 8px; + border-left: 1px solid var(--sn-stylekit-border-color, #e4e4e7); + + .theme-dark & { + border-left-color: var(--sn-stylekit-border-color, #27272a); + } +} + +.tab-action-btn { + background: none; + border: none; + color: var(--sn-stylekit-input-placeholder-color, #71717a); + cursor: pointer; + padding: 6px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + + &:hover { + background-color: var(--sn-stylekit-bg-contrast-medium, rgba(0, 0, 0, 0.05)); + color: var(--sn-stylekit-text-color, #18181b); + + .theme-dark & { + background-color: var(--sn-stylekit-bg-contrast-medium, rgba(255, 255, 255, 0.05)); + color: var(--sn-stylekit-text-color, #f4f4f5); + } + } +} + +.tab-action-btn-active { + color: var(--sn-stylekit-primary-color, #3b82f6) !important; +} + +.tab-context-menu { + position: fixed; + z-index: 10000; + background-color: var(--sn-stylekit-editor-background-color, #ffffff); + border: 1px solid var(--sn-stylekit-border-color, #e4e4e7); + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + border-radius: 6px; + padding: 4px 0; + min-width: 140px; + + .theme-dark & { + background-color: var(--sn-stylekit-editor-background-color, #18181b); + border-color: var(--sn-stylekit-border-color, #27272a); + } +} + +.context-menu-item { + padding: 8px 12px; + font-size: 13px; + cursor: pointer; + color: var(--sn-stylekit-text-color, #18181b); + transition: background-color 0.2s; + + &:hover { + background-color: var(--sn-stylekit-bg-contrast-medium, rgba(0, 0, 0, 0.05)); + + .theme-dark & { + background-color: var(--sn-stylekit-bg-contrast-medium, rgba(255, 255, 255, 0.05)); + } + } + + .theme-dark & { + color: var(--sn-stylekit-text-color, #f4f4f5); + } +} + +// Split pane layout styles +.editors-split-container { + display: flex; + width: 100%; + height: 100%; + flex-direction: row; + + .split-pane-wrapper { + flex: 1; + height: 100%; + min-width: 0; // prevent flex children from overflowing + position: relative; + display: flex; + flex-direction: column; + + &:not(:last-child) { + border-right: 1px solid var(--sn-stylekit-border-color, #e4e4e7); + + .theme-dark & { + border-right-color: var(--sn-stylekit-border-color, #27272a); + } + } + } + + .split-pane-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + border: 2px solid transparent; + transition: border-color 0.2s; + z-index: 10; + } + + .split-pane-focused .split-pane-overlay { + border-color: rgba(59, 130, 246, 0.2); // subtle light highlight + } +} diff --git a/packages/web/src/stylesheets/index.css.scss b/packages/web/src/stylesheets/index.css.scss index 41012126d02..603128f7eb6 100644 --- a/packages/web/src/stylesheets/index.css.scss +++ b/packages/web/src/stylesheets/index.css.scss @@ -9,6 +9,7 @@ @import 'navigation'; @import 'items-column'; @import 'editor'; +@import 'tabs'; @import 'menus'; @import 'modals'; @import 'stylekit-sub';