diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/cell.tsx b/services/web/frontend/js/features/source-editor/components/table-generator/cell.tsx index 19531f650d..eac5bc90f4 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/cell.tsx +++ b/services/web/frontend/js/features/source-editor/components/table-generator/cell.tsx @@ -36,6 +36,7 @@ export const Cell: FC<{ cellData: editingCellData, updateCellData: update, startEditing, + commitCellData, } = useEditingContext() const inputRef = useRef(null) @@ -167,6 +168,7 @@ export const Cell: FC<{ className="table-generator-cell-input" ref={inputRef} value={editingCellData.content} + onBlur={commitCellData} style={{ width: `inherit` }} onChange={e => { update(filterInput(e.target.value)) diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/contexts/selection-context.tsx b/services/web/frontend/js/features/source-editor/components/table-generator/contexts/selection-context.tsx index 0539408d93..1d9e11a98b 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/contexts/selection-context.tsx +++ b/services/web/frontend/js/features/source-editor/components/table-generator/contexts/selection-context.tsx @@ -333,6 +333,17 @@ export class TableSelection { const { minY, maxY } = this.normalized() return maxY - minY + 1 } + + maximumCellWidth(table: TableData) { + const { minX, maxX, minY, maxY } = this.normalized() + let maxWidth = 1 + for (let row = minY; row <= maxY; ++row) { + const start = table.getCellIndex(row, minX) + const end = table.getCellIndex(row, maxX) + maxWidth = Math.max(maxWidth, end - start + 1) + } + return maxWidth + } } const SelectionContext = createContext< diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/tabular.tsx b/services/web/frontend/js/features/source-editor/components/table-generator/tabular.tsx index 35c09b5687..e9c5934c50 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/tabular.tsx +++ b/services/web/frontend/js/features/source-editor/components/table-generator/tabular.tsx @@ -19,6 +19,7 @@ import { CodeMirrorViewContextProvider } from '../codemirror-editor' import { TableProvider } from './contexts/table-context' import { TabularProvider, useTabularContext } from './contexts/tabular-context' import Icon from '../../../../shared/components/icon' +import { BorderTheme } from './toolbar/commands' export type ColumnDefinition = { alignment: 'left' | 'center' | 'right' | 'paragraph' @@ -91,6 +92,56 @@ export class TableData { } throw new Error("Couldn't find cell boundaries") } + + getBorderTheme(): BorderTheme | null { + if (this.rows.length === 0 || this.columns.length === 0) { + return null + } + const lastRow = this.rows[this.rows.length - 1] + const hasBottomBorder = lastRow.borderBottom > 0 + const firstColumn = this.columns[0] + const hasLeftBorder = firstColumn.borderLeft > 0 + for (const row of this.rows) { + if (hasBottomBorder === (row.borderTop === 0)) { + return null + } + } + // If we had the first, we have verified that we have the rest + const hasAllRowBorders = hasBottomBorder + + for (const column of this.columns) { + if (hasLeftBorder === (column.borderRight === 0)) { + return null + } + } + + for (const row of this.rows) { + for (const cell of row.cells) { + if (cell.multiColumn) { + if (cell.multiColumn.columns.specification.length === 0) { + return null + } + const firstCell = cell.multiColumn.columns.specification[0] + if (hasLeftBorder === (firstCell.borderLeft === 0)) { + return null + } + for (const column of cell.multiColumn.columns.specification) { + if (hasLeftBorder === (column.borderRight === 0)) { + return null + } + } + } + } + } + // If we had the first, we have verified that we have the rest + const hasAllColumnBorders = hasLeftBorder + + if (hasAllRowBorders && hasAllColumnBorders) { + return BorderTheme.FULLY_BORDERED + } else { + return BorderTheme.NO_BORDERS + } + } } export type Positions = { diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/commands.ts b/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/commands.ts index 5e4370ebe0..4dc73b535e 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/commands.ts +++ b/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/commands.ts @@ -342,13 +342,21 @@ export const insertRow = ( below: boolean, table: TableData ) => { - // TODO: Handle borders const { maxY, minY } = selection.normalized() + const rowsToInsert = selection.height() const from = below ? positions.rowPositions[maxY].to : positions.rowPositions[minY].from const numberOfColumns = table.columns.length - const insert = `\n${' &'.repeat(numberOfColumns - 1)}\\\\` + const borderTheme = table.getBorderTheme() + const border = borderTheme === BorderTheme.FULLY_BORDERED ? '\\hline' : '' + const initialHline = + borderTheme === BorderTheme.FULLY_BORDERED && !below && minY === 0 + ? '\\hline' + : '' + const insert = `${initialHline}\n${' &'.repeat( + numberOfColumns - 1 + )}\\\\${border}`.repeat(rowsToInsert) view.dispatch({ changes: { from, to: from, insert } }) if (!below) { return selection @@ -366,9 +374,9 @@ export const insertColumn = ( after: boolean, table: TableData ) => { - // TODO: Handle borders const selection = initialSelection.explode(table) const { maxX, minX } = selection.normalized() + const columnsToInsert = selection.maximumCellWidth(table) const changes: ChangeSpec[] = [] const targetColumn = after ? maxX : minX for (let row = 0; row < positions.rowPositions.length; row++) { @@ -377,7 +385,7 @@ export const insertColumn = ( const from = after ? target.to : target.from changes.push({ from, - insert: ' &', + insert: ' &'.repeat(columnsToInsert), }) } @@ -386,12 +394,25 @@ export const insertColumn = ( positions.columnDeclarations.to ) const columnSpecification = parseColumnSpecifications(specification) - columnSpecification.splice(after ? maxX + 1 : minX, 0, { - alignment: 'left', - borderLeft: 0, - borderRight: 0, - content: 'l', - }) + const borderTheme = table.getBorderTheme() + const borderRight = borderTheme === BorderTheme.FULLY_BORDERED ? 1 : 0 + const targetIndex = after ? maxX + 1 : minX + columnSpecification.splice( + targetIndex, + 0, + ...Array.from({ length: columnsToInsert }, () => ({ + alignment: 'left' as const, + borderLeft: 0, + borderRight, + content: 'l', + })) + ) + if (targetIndex === 0 && borderTheme === BorderTheme.FULLY_BORDERED) { + columnSpecification[0].borderLeft = Math.max( + 1, + columnSpecification[0].borderLeft + ) + } changes.push({ from: positions.columnDeclarations.from, to: positions.columnDeclarations.to, @@ -581,8 +602,9 @@ export const mergeCells = ( cellContent.push(table.getCell(minY, i).content) } const content = cellContent.join(' ').trim() - // TODO: respect border theme - const preamble = '\\multicolumn{' + (maxX - minX + 1) + '}{c}{' + const border = + table.getBorderTheme() === BorderTheme.FULLY_BORDERED ? '|' : '' + const preamble = `\\multicolumn{${maxX - minX + 1}}{${border}c${border}}{` const postamble = '}' const { from } = table.getCell(minY, minX) const { to } = table.getCell(minY, maxX) diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/toolbar.tsx b/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/toolbar.tsx index 027b39117b..04136d398a 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/toolbar.tsx +++ b/services/web/frontend/js/features/source-editor/components/table-generator/toolbar/toolbar.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react' +import { memo, useMemo } from 'react' import { useSelectionContext } from '../contexts/selection-context' import { ToolbarButton } from './toolbar-button' import { ToolbarButtonMenu } from './toolbar-button-menu' @@ -20,19 +20,49 @@ import { import { useCodeMirrorViewContext } from '../../codemirror-editor' import { useTableContext } from '../contexts/table-context' +const borderThemeLabel = (theme: BorderTheme | null) => { + switch (theme) { + case BorderTheme.FULLY_BORDERED: + return 'All borders' + case BorderTheme.NO_BORDERS: + return 'No borders' + default: + return 'Custom borders' + } +} + export const Toolbar = memo(function Toolbar() { const { selection, setSelection } = useSelectionContext() const view = useCodeMirrorViewContext() const { positions, rowSeparators, cellSeparators, tableEnvironment, table } = useTableContext() + + const borderDropdownLabel = useMemo( + () => borderThemeLabel(table.getBorderTheme()), + [table] + ) + + const captionLabel = useMemo(() => { + if (!tableEnvironment?.caption) { + return 'No caption' + } + if (tableEnvironment.caption.from < positions.tabular.from) { + return 'Caption above' + } + return 'Caption below' + }, [tableEnvironment, positions.tabular.from]) + if (!selection) { return null } + const columnsToInsert = selection.maximumCellWidth(table) + const rowsToInsert = selection.height() + return (

@@ -236,7 +270,9 @@ export const Toolbar = memo(function Toolbar() { }} > - Insert row above + {rowsToInsert === 1 + ? 'Insert row above' + : `Insert ${rowsToInsert} rows above`}
@@ -260,6 +298,7 @@ export const Toolbar = memo(function Toolbar() { label="Delete table" command={() => { removeNodes(view, tableEnvironment?.table ?? positions.tabular) + view.focus() }} />
diff --git a/services/web/frontend/js/features/source-editor/components/table-generator/utils.ts b/services/web/frontend/js/features/source-editor/components/table-generator/utils.ts index 29af26d86a..4c89df9d8e 100644 --- a/services/web/frontend/js/features/source-editor/components/table-generator/utils.ts +++ b/services/web/frontend/js/features/source-editor/components/table-generator/utils.ts @@ -213,7 +213,7 @@ function parseTabularBody( if (!tabularArgument) { throw new Error('Invalid multicolumn definition: missing cell content') } - if (getLastCell()?.content) { + if (getLastCell()?.content.trim()) { throw new Error( 'Invalid multicolumn definition: multicolumn must be at the start of a cell' ) diff --git a/services/web/frontend/js/features/source-editor/components/toolbar/table-inserter-dropdown.tsx b/services/web/frontend/js/features/source-editor/components/toolbar/table-inserter-dropdown.tsx index c18734b79c..3ab5b3c0c6 100644 --- a/services/web/frontend/js/features/source-editor/components/toolbar/table-inserter-dropdown.tsx +++ b/services/web/frontend/js/features/source-editor/components/toolbar/table-inserter-dropdown.tsx @@ -109,7 +109,7 @@ const SizeGrid: FC<{ onMouseEnter={() => { setCurrentSize({ sizeX: x, sizeY: y }) }} - onMouseDown={() => onSizeSelected(x, y)} + onMouseUp={() => onSizeSelected(x, y)} /> ))} diff --git a/services/web/frontend/js/features/source-editor/extensions/toolbar/commands.ts b/services/web/frontend/js/features/source-editor/extensions/toolbar/commands.ts index 367f987e04..694adc2422 100644 --- a/services/web/frontend/js/features/source-editor/extensions/toolbar/commands.ts +++ b/services/web/frontend/js/features/source-editor/extensions/toolbar/commands.ts @@ -71,7 +71,7 @@ export const insertFigure: Command = view => { export const insertTable = (view: EditorView, sizeX: number, sizeY: number) => { const { state, dispatch } = view const { pos, suffix } = ensureEmptyLine(state, state.selection.main) - const template = `\n\\begin{table}{#{}} + const template = `\n\\begin{table}[#{}] \t\\centering \\begin{tabular}{${'c'.repeat(sizeX)}} ${('\t\t' + '#{} & #{}'.repeat(sizeX - 1) + '\\\\\n').repeat( diff --git a/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts b/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts index d4f3d43778..7c3eb8248f 100644 --- a/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts +++ b/services/web/frontend/js/features/source-editor/extensions/toolbar/toolbar-panel.ts @@ -255,6 +255,15 @@ export const toolbarPanel = () => [ }, }, }, + '&.overall-theme-dark .ol-cm-toolbar-table-grid-popover': { + color: '#fff', + }, + '&.overall-theme-dark .ol-cm-toolbar-table-grid': { + '& td.active': { + outlineColor: 'white', + background: 'rgb(125, 125, 125)', + }, + }, '.ol-cm-toolbar-table-grid': { borderCollapse: 'separate', tableLayout: 'fixed', @@ -281,6 +290,7 @@ export const toolbarPanel = () => [ '.ol-cm-toolbar-table-grid-popover': { padding: '8px', marginLeft: '80px', + backgroundColor: 'var(--editor-toolbar-bg)', }, }), ] diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/table-generator.ts b/services/web/frontend/js/features/source-editor/extensions/visual/table-generator.ts new file mode 100644 index 0000000000..fc8399818e --- /dev/null +++ b/services/web/frontend/js/features/source-editor/extensions/visual/table-generator.ts @@ -0,0 +1,424 @@ +import { EditorView } from '@codemirror/view' + +export const tableGeneratorTheme = EditorView.baseTheme({ + '&dark .table-generator': { + '--table-generator-active-border-color': '#ccc', + '--table-generator-coming-soon-background-color': '#41464f', + '--table-generator-coming-soon-color': '#fff', + '--table-generator-divider-color': 'rgba(125,125,125,0.3)', + '--table-generator-dropdown-divider-color': 'rgba(125,125,125,0.3)', + '--table-generator-focus-border-color': '#5d7498', + '--table-generator-inactive-border-color': '#888', + '--table-generator-selected-background-color': '#ffffff2a', + '--table-generator-selector-background-color': '#777', + '--table-generator-selector-hover-color': '#3265b2', + '--table-generator-toolbar-background': 'var(--editor-toolbar-bg)', + '--table-generator-toolbar-button-active-background': + 'rgba(125, 125, 125, 0.4)', + '--table-generator-toolbar-button-color': 'var(--toolbar-btn-color)', + '--table-generator-toolbar-button-hover-background': + 'rgba(125, 125, 125, 0.2)', + '--table-generator-toolbar-dropdown-border-color': 'rgba(125,125,125,0.3)', + '--table-generator-toolbar-dropdown-disabled-background': + 'rgba(125,125,125,0.3)', + '--table-generator-toolbar-dropdown-disabled-color': '#999', + '--table-generator-toolbar-shadow-color': '#1e253029', + }, + + '&light .table-generator': { + '--table-generator-active-border-color': '#666', + '--table-generator-coming-soon-background-color': 'var(--neutral-10)', + '--table-generator-coming-soon-color': 'var(--neutral-70)', + '--table-generator-divider-color': 'var(--neutral-20)', + '--table-generator-dropdown-divider-color': 'var(--neutral-20)', + '--table-generator-focus-border-color': '#97b6e5', + '--table-generator-inactive-border-color': '#dedede', + '--table-generator-selected-background-color': 'var(--blue-10)', + '--table-generator-selector-background-color': 'var(--neutral-30)', + '--table-generator-selector-hover-color': '#3265b2', + '--table-generator-toolbar-background': '#fff', + '--table-generator-toolbar-button-active-background': + 'rgba(47, 58, 76, 0.16)', + '--table-generator-toolbar-button-color': 'var(--neutral-70)', + '--table-generator-toolbar-button-hover-background': + 'rgba(47, 58, 76, 0.08)', + '--table-generator-toolbar-dropdown-border-color': 'var(--neutral-60)', + '--table-generator-toolbar-dropdown-disabled-background': '#f2f2f2', + '--table-generator-toolbar-dropdown-disabled-color': 'var(--neutral-40)', + '--table-generator-toolbar-shadow-color': '#1e253029', + }, + + '.table-generator': { + position: 'relative', + '--table-generator-inactive-border-width': '1px', + '--table-generator-active-border-width': '1px', + '--table-generator-selector-handle-buffer': '12px', + '--table-generator-focus-border-width': '2px', + '--table-generator-focus-negative-border-width': '-2px', + }, + + '.table-generator-cell.selected': { + 'background-color': 'var(--table-generator-selected-background-color)', + }, + + '.table-generator-cell:focus-visible': { + outline: '2px dotted var(--table-generator-focus-border-color)', + }, + + '.table-generator-cell': { + border: + 'var(--table-generator-inactive-border-width) dashed var(--table-generator-inactive-border-color)', + 'min-width': '40px', + height: '30px', + '&.selection-edge-top': { + '--shadow-top': + '0 var(--table-generator-focus-negative-border-width) 0 var(--table-generator-focus-border-color)', + }, + '&.selection-edge-bottom': { + '--shadow-bottom': + '0 var(--table-generator-focus-border-width) 0 var(--table-generator-focus-border-color)', + }, + '&.selection-edge-left': { + '--shadow-left': + 'var(--table-generator-focus-negative-border-width) 0 0 var(--table-generator-focus-border-color)', + }, + '&.selection-edge-right': { + '--shadow-right': + 'var(--table-generator-focus-border-width) 0 0 var(--table-generator-focus-border-color)', + }, + 'box-shadow': + 'var(--shadow-top, 0 0 0 transparent), var(--shadow-bottom, 0 0 0 transparent), var(--shadow-left, 0 0 0 transparent), var(--shadow-right, 0 0 0 transparent)', + '&.table-generator-cell-border-left': { + 'border-left-style': 'solid', + 'border-left-color': 'var(--table-generator-active-border-color)', + 'border-left-width': 'var(--table-generator-active-border-width)', + }, + + '&.table-generator-cell-border-right': { + 'border-right-style': 'solid', + 'border-right-color': 'var(--table-generator-active-border-color)', + 'border-right-width': 'var(--table-generator-active-border-width)', + }, + + '&.table-generator-row-border-top': { + 'border-top-style': 'solid', + 'border-top-color': 'var(--table-generator-active-border-color)', + 'border-top-width': 'var(--table-generator-active-border-width)', + }, + + '&.table-generator-row-border-bottom': { + 'border-bottom-style': 'solid', + 'border-bottom-color': 'var(--table-generator-active-border-color)', + 'border-bottom-width': 'var(--table-generator-active-border-width)', + }, + }, + + '.table-generator-table': { + 'table-layout': 'fixed', + 'max-width': '80%', + margin: '0 auto', + cursor: 'default', + + '& td': { + padding: '0 0.25em', + 'max-width': '200px', + + '&.alignment-left': { + 'text-align': 'left', + }, + '&.alignment-right': { + 'text-align': 'right', + }, + '&.alignment-center': { + 'text-align': 'center', + }, + '&.alignment-paragraph': { + 'text-align': 'justify', + }, + }, + + '& .table-generator-selector-cell': { + padding: '0', + border: 'none !important', + position: 'relative', + cursor: 'pointer', + + '&.row-selector': { + width: 'calc(var(--table-generator-selector-handle-buffer) + 8px)', + + '&::after': { + width: '4px', + height: 'calc(100% - 8px)', + }, + }, + + '&.column-selector': { + height: 'calc(var(--table-generator-selector-handle-buffer) + 8px)', + + '&::after': { + width: 'calc(100% - 8px)', + height: '4px', + }, + }, + + '&::after': { + content: '""', + display: 'block', + position: 'absolute', + bottom: '4px', + right: '4px', + width: 'calc(100% - 8px)', + height: 'calc(100% - 8px)', + 'background-color': 'var(--table-generator-selector-background-color)', + 'border-radius': '4px', + }, + + '&:hover::after': { + 'background-color': 'var(--table-generator-selector-hover-color)', + }, + + '&.fully-selected::after': { + 'background-color': 'var(--table-generator-selector-hover-color)', + }, + }, + }, + + '.table-generator-floating-toolbar': { + position: 'absolute', + top: '-36px', + left: '0', + right: '0', + margin: '0 auto', + 'z-index': '1', + 'border-radius': '4px', + width: 'max-content', + 'background-color': 'var(--table-generator-toolbar-background)', + 'box-shadow': '0px 2px 4px 0px var(--table-generator-toolbar-shadow-color)', + padding: '4px', + height: '40px', + display: 'flex', + }, + + '.table-generator-toolbar-button': { + display: 'inline-flex', + 'align-items': 'center', + 'justify-content': 'center', + margin: '0', + 'background-color': 'transparent', + border: 'none', + 'border-radius': '4px', + 'line-height': '1', + overflow: 'hidden', + color: 'var(--table-generator-toolbar-button-color)', + 'text-align': 'center', + padding: '4px', + + '&:not(first-child)': { + 'margin-left': '4px', + }, + '&:not(:last-child)': { + 'margin-right': '4px', + }, + + '& > span': { + 'font-size': '24px', + }, + + '&:hover, &:focus': { + 'background-color': + 'var(--table-generator-toolbar-button-hover-background)', + }, + + '&:active, &.active': { + 'background-color': + 'var(--table-generator-toolbar-button-active-background)', + }, + + '&:hover, &:focus, &:active, &.active': { + 'box-shadow': 'none', + }, + + '&[aria-disabled="true"]': { + '&:hover, &:focus, &:active, &.active': { + 'background-color': 'transparent', + }, + opacity: '0.2', + }, + }, + + '.table-generator-button-group': { + display: 'inline-flex', + 'align-items': 'center', + 'justify-content': 'center', + 'line-height': '1', + overflow: 'hidden', + '&:not(:first-child)': { + 'border-left': '1px solid var(--table-generator-divider-color)', + 'padding-left': '8px', + 'margin-left': '8px', + }, + }, + + '.table-generator-button-menu-popover': { + 'background-color': 'var(--table-generator-toolbar-background) !important', + '& .popover-content': { + padding: '4px', + }, + '& .list-group': { + margin: '0', + padding: '0', + }, + '& > .arrow': { + display: 'none', + }, + }, + + '.table-generator-cell-input': { + 'max-width': 'calc(200px - 0.5em)', + width: '100%', + 'background-color': 'transparent', + border: '1px solid var(--table-generator-toolbar-shadow-color)', + padding: '0', + }, + + '.table-generator-border-options-coming-soon': { + display: 'flex', + margin: '4px', + 'font-size': '12px', + background: 'var(--table-generator-coming-soon-background-color)', + color: 'var(--table-generator-coming-soon-color)', + padding: '8px', + gap: '6px', + 'align-items': 'flex-start', + 'max-width': '240px', + 'font-family': 'Lato', + + '& .info-icon': { + flex: ' 0 0 24px', + }, + }, + + '.table-generator-toolbar-dropdown-toggle': { + border: '1px solid var(--table-generator-toolbar-dropdown-border-color)', + 'box-shadow': 'none', + background: 'transparent', + 'white-space': 'nowrap', + color: 'var(--table-generator-toolbar-button-color)', + 'border-radius': '4px', + padding: '6px 8px', + gap: '8px', + 'min-width': '120px', + 'font-size': '14px', + display: 'flex', + 'align-items': 'center', + 'justify-content': 'space-between', + 'font-family': 'Lato', + + '&:not(:first-child)': { + 'margin-left': '8px', + }, + + '&[aria-disabled="true"]': { + 'background-color': + 'var(--table-generator-toolbar-dropdown-disabled-background)', + color: 'var(--table-generator-toolbar-dropdown-disabled-color)', + }, + }, + + '.table-generator-toolbar-dropdown-popover': { + 'max-width': '300px', + background: 'var(--table-generator-toolbar-background) !important', + + '& .popover-content': { + padding: '0', + }, + + '& > .arrow': { + display: 'none', + }, + }, + + '.table-generator-toolbar-dropdown-menu': { + display: 'flex', + 'flex-direction': 'column', + 'min-width': '200px', + + '& > button': { + border: 'none', + 'box-shadow': 'none', + background: 'transparent', + 'white-space': 'nowrap', + color: 'var(--table-generator-toolbar-button-color)', + 'border-radius': '0', + 'font-size': '14px', + display: 'flex', + 'align-items': 'center', + 'justify-content': 'space-between', + 'column-gap': '8px', + 'align-self': 'stretch', + padding: '12px 8px', + 'font-family': 'Lato', + + '& .table-generator-button-label': { + 'align-self': 'stretch', + flex: '1 0 auto', + 'text-align': 'left', + }, + + '&:hover, &:focus': { + 'background-color': + 'var(--table-generator-toolbar-button-hover-background)', + }, + + '&:active, &.active': { + 'background-color': + 'var(--table-generator-toolbar-button-active-background)', + }, + + '&:hover, &:focus, &:active, &.active': { + 'box-shadow': 'none', + }, + + '&[aria-disabled="true"]': { + '&:hover, &:focus, &:active, &.active': { + 'background-color': 'transparent', + }, + color: 'var(--table-generator-toolbar-dropdown-disabled-color)', + }, + }, + + '& > hr': { + background: 'var(--table-generator-dropdown-divider-color)', + margin: '2px 8px', + display: 'block', + 'box-sizing': 'content-box', + border: '0', + height: '1px', + }, + }, + + '.table-generator-error': { + background: + 'linear-gradient(0deg, #f9f1f1, #f9f1f1), linear-gradient(0deg, #f5beba, #f5beba)', + display: 'flex', + 'justify-content': 'space-between', + color: 'black', + border: '1px solid #f5beba', + 'font-family': 'Lato', + 'margin-bottom': '0', + '& .table-generator-error-message': { + flex: '1 0 auto', + }, + '& .table-generator-error-icon': { + color: '#b83a33', + 'margin-right': '12px', + }, + }, + + '.table-generator-filler-row': { + border: 'none !important', + '& td': { + 'min-width': '40px', + }, + }, +}) diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts index 7eb6316379..c74ca68889 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts @@ -26,6 +26,7 @@ import { toolbarPanel } from '../toolbar/toolbar-panel' import { selectDecoratedArgument } from './select-decorated-argument' import { pasteHtml } from './paste-html' import { commandTooltip } from '../command-tooltip' +import { tableGeneratorTheme } from './table-generator' type Options = { visual: boolean @@ -208,4 +209,5 @@ const extension = (options: Options) => [ showContentWhenParsed, figureModalPasteHandler(), isSplitTestEnabled('paste-html') ? pasteHtml : [], + isSplitTestEnabled('table-generator') ? tableGeneratorTheme : [], ] diff --git a/services/web/frontend/stylesheets/app/editor.less b/services/web/frontend/stylesheets/app/editor.less index 25227a7c95..0cd536455d 100644 --- a/services/web/frontend/stylesheets/app/editor.less +++ b/services/web/frontend/stylesheets/app/editor.less @@ -19,7 +19,6 @@ @import './editor/dictionary.less'; @import './editor/compile-button.less'; @import './editor/figure-modal.less'; -@import './editor/table-generator.less'; @ui-layout-toggler-def-height: 50px; @ui-resizer-size: 7px; diff --git a/services/web/frontend/stylesheets/app/editor/table-generator.less b/services/web/frontend/stylesheets/app/editor/table-generator.less deleted file mode 100644 index 302c35a6e3..0000000000 --- a/services/web/frontend/stylesheets/app/editor/table-generator.less +++ /dev/null @@ -1,397 +0,0 @@ -@table-generator-active-border-color: #666; -@table-generator-inactive-border-color: #dedede; -@table-generator-focus-border-width: 2px; -@table-generator-focus-negative-border-width: -2px; -@table-generator-focus-border-color: #97b6e5; -@table-generator-selector-hover-color: #3265b2; -@table-generator-selector-handle-buffer: 12px; -@table-generator-toolbar-shadow-color: #1e253029; -@table-generator-toolbar-background: #fff; -@table-generator-inactive-border-width: 1px; -@table-generator-active-border-width: 1px; - -.table-generator-cell { - border: @table-generator-inactive-border-width dashed - @table-generator-inactive-border-color; - min-width: 40px; - height: 30px; -} - -.table-generator-cell-border-left { - border-left-style: solid; - border-left-color: @table-generator-active-border-color; - border-left-width: @table-generator-active-border-width; -} - -.table-generator-cell-border-right { - border-right-style: solid; - border-right-color: @table-generator-active-border-color; - border-right-width: @table-generator-active-border-width; -} - -.table-generator-row-border-top { - border-top-style: solid; - border-top-color: @table-generator-active-border-color; - border-top-width: @table-generator-active-border-width; -} - -.table-generator-row-border-bottom { - border-bottom-style: solid; - border-bottom-color: @table-generator-active-border-color; - border-bottom-width: @table-generator-active-border-width; -} - -.table-generator-cell.selected { - background-color: @blue-10; -} - -.table-generator-cell:focus-visible { - outline: 2px dotted @table-generator-focus-border-color; -} - -.table-generator-cell { - &.selection-edge-top { - --shadow-top: 0 @table-generator-focus-negative-border-width 0 - @table-generator-focus-border-color; - } - &.selection-edge-bottom { - --shadow-bottom: 0 @table-generator-focus-border-width 0 - @table-generator-focus-border-color; - } - &.selection-edge-left { - --shadow-left: @table-generator-focus-negative-border-width 0 0 - @table-generator-focus-border-color; - } - &.selection-edge-right { - --shadow-right: @table-generator-focus-border-width 0 0 - @table-generator-focus-border-color; - } - box-shadow: var(--shadow-top, 0 0 0 transparent), - var(--shadow-bottom, 0 0 0 transparent), - var(--shadow-left, 0 0 0 transparent), - var(--shadow-right, 0 0 0 transparent); -} - -.table-generator-table { - table-layout: fixed; - max-width: 80%; - margin: 0 auto; - cursor: default; - - & td { - padding: 0 0.25em; - max-width: 200px; - - &.alignment-left { - text-align: left; - } - &.alignment-right { - text-align: right; - } - &.alignment-center { - text-align: center; - } - &.alignment-paragraph { - text-align: justify; - } - } - - .table-generator-selector-cell { - padding: 0; - border: none !important; - position: relative; - cursor: pointer; - - &.row-selector { - width: @table-generator-selector-handle-buffer + 8px; - - &::after { - width: 4px; - height: calc(100% - 8px); - } - } - - &.column-selector { - height: @table-generator-selector-handle-buffer + 8px; - - &::after { - width: calc(100% - 8px); - height: 4px; - } - } - - &::after { - content: ''; - display: block; - position: absolute; - bottom: 4px; - right: 4px; - width: calc(100% - 8px); - height: calc(100% - 8px); - background-color: @neutral-30; - border-radius: 4px; - } - - &:hover::after { - background-color: @neutral-40; - } - - &.fully-selected::after { - background-color: @table-generator-selector-hover-color; - } - } -} - -.table-generator { - position: relative; -} - -.table-generator-floating-toolbar { - position: absolute; - top: -36px; - left: 0; - right: 0; - margin: 0 auto; - z-index: 1; - border-radius: 4px; - width: max-content; - background-color: @table-generator-toolbar-background; - box-shadow: 0px 2px 4px 0px @table-generator-toolbar-shadow-color; - padding: 4px; - height: 40px; - display: flex; -} - -.table-generator-toolbar-button { - display: inline-flex; - align-items: center; - justify-content: center; - margin: 0; - background-color: transparent; - border: none; - border-radius: 4px; - line-height: 1; - overflow: hidden; - color: @neutral-70; - text-align: center; - padding: 4px; - - &:not(:first-child) { - margin-left: 4px; - } - &:not(:last-child) { - margin-right: 4px; - } - - & > span { - font-size: 24px; - } - - &:hover, - &:focus { - background-color: rgba(47, 58, 76, 0.08); - } - - &:active, - &.active { - background-color: rgba(47, 58, 76, 0.16); - } - - &:hover, - &:focus, - &:active, - &.active { - color: inherit; - box-shadow: none; - } - - &[aria-disabled='true'] { - &:hover, - &:focus, - &:active, - &.active { - background-color: transparent; - } - color: @neutral-40; - } -} - -.table-generator-button-group { - display: inline-flex; - align-items: center; - justify-content: center; - line-height: 1; - overflow: hidden; - &:not(:first-child) { - border-left: 1px solid @neutral-20; - padding-left: 8px; - margin-left: 8px; - } -} - -.table-generator-button-menu-popover { - .popover-content { - padding: 4px; - } - .list-group { - margin: 0; - padding: 0; - } - & > .arrow { - display: none; - } -} - -.table-generator-cell-input { - max-width: calc(200px - 0.5em); - width: 100%; - background-color: transparent; - border: 1px solid @table-generator-toolbar-shadow-color; - border: 0; - padding: 0; -} - -.table-generator-border-options-coming-soon { - display: flex; - margin: 4px; - font-size: 12px; - background: @neutral-10; - color: @neutral-70; - padding: 8px; - gap: 6px; - align-items: flex-start; - max-width: 240px; - - & .info-icon { - flex: 0 0 24px; - } -} - -.table-generator-toolbar-dropdown-toggle { - border: 1px solid @neutral-60; - box-shadow: none; - background: transparent; - white-space: nowrap; - color: inherit; - border-radius: 4px; - padding: 6px 8px; - gap: 8px; - min-width: 120px; - font-size: 14px; - display: flex; - align-items: center; - justify-content: space-between; - font-family: @font-family-sans-serif; - - &:not(:first-child) { - margin-left: 8px; - } - - &[aria-disabled='true'] { - background-color: #f2f2f2; - color: @neutral-40; - } -} - -.table-generator-toolbar-dropdown-popover { - max-width: 300px; - - .popover-content { - padding: 0; - } - - & > .arrow { - display: none; - } -} - -.table-generator-toolbar-dropdown-menu { - display: flex; - flex-direction: column; - min-width: 200px; - - & > button { - border: none; - box-shadow: none; - background: transparent; - white-space: nowrap; - color: inherit; - border-radius: 0; - font-size: 14px; - display: flex; - align-items: center; - justify-content: space-between; - column-gap: 8px; - align-self: stretch; - padding: 12px 8px; - font-family: @font-family-sans-serif; - - .table-generator-button-label { - align-self: stretch; - flex: 1 0 auto; - text-align: left; - } - - &:hover, - &:focus { - background-color: rgba(47, 58, 76, 0.08); - } - - &:active, - &.active { - background-color: rgba(47, 58, 76, 0.16); - } - - &:hover, - &:focus, - &:active, - &.active { - color: inherit; - box-shadow: none; - } - - &[aria-disabled='true'] { - &:hover, - &:focus, - &:active, - &.active { - background-color: transparent; - } - color: @neutral-40; - } - } - - & > hr { - background: @neutral-20; - margin: 2px 8px; - display: block; - box-sizing: content-box; - border: 0; - height: 1px; - } -} - -.table-generator-error { - background: linear-gradient(0deg, #f9f1f1, #f9f1f1), - linear-gradient(0deg, #f5beba, #f5beba); - display: flex; - justify-content: space-between; - color: black; - border: 1px solid #f5beba; - font-family: @font-family-sans-serif; - margin-bottom: 0; - .table-generator-error-message { - flex: 1 0 auto; - } - .table-generator-error-icon { - color: #b83a33; - margin-right: 12px; - } -} - -.table-generator-filler-row { - border: none !important; - td { - min-width: 40px; - } -} diff --git a/services/web/frontend/stylesheets/core/css-variables.less b/services/web/frontend/stylesheets/core/css-variables.less index 102e131732..6db86cd53a 100644 --- a/services/web/frontend/stylesheets/core/css-variables.less +++ b/services/web/frontend/stylesheets/core/css-variables.less @@ -23,4 +23,14 @@ --toolbar-btn-color: @toolbar-btn-color; --editor-toolbar-bg: @editor-toolbar-bg; + + --neutral-10: @neutral-10; + --neutral-20: @neutral-20; + --neutral-30: @neutral-30; + --neutral-40: @neutral-40; + --neutral-60: @neutral-60; + --neutral-70: @neutral-70; + --neutral-80: @neutral-80; + --neutral-90: @neutral-90; + --blue-10: @blue-10; }