Просмотр исходного кода

Merge pull request #14428 from overleaf/mj-table-gen-cleanup

[visual] Table generator tweaks and improvements

GitOrigin-RevId: 621820c613924bc201ec85ea249edd6c9b6f88b2
Mathias Jakobsen 3 лет назад
Родитель
Сommit
8b5278c076

+ 2 - 0
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<HTMLInputElement>(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))

+ 11 - 0
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<

+ 51 - 0
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 = {

+ 34 - 12
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)

+ 46 - 7
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 (
     <div className="table-generator-floating-toolbar">
       <ToolbarDropdown
         id="table-generator-caption-dropdown"
-        label="Caption below"
+        label={captionLabel}
         disabled={!tableEnvironment}
       >
         <button
@@ -68,7 +98,7 @@ export const Toolbar = memo(function Toolbar() {
       </ToolbarDropdown>
       <ToolbarDropdown
         id="table-generator-borders-dropdown"
-        label="All borders"
+        label={borderDropdownLabel}
       >
         <button
           className="ol-cm-toolbar-menu-item"
@@ -209,7 +239,9 @@ export const Toolbar = memo(function Toolbar() {
             }}
           >
             <span className="table-generator-button-label">
-              Insert column left
+              {columnsToInsert === 1
+                ? 'Insert column left'
+                : `Insert ${columnsToInsert} columns left`}
             </span>
           </button>
           <button
@@ -223,7 +255,9 @@ export const Toolbar = memo(function Toolbar() {
             }}
           >
             <span className="table-generator-button-label">
-              Insert column right
+              {columnsToInsert === 1
+                ? 'Insert column right'
+                : `Insert ${columnsToInsert} columns right`}
             </span>
           </button>
           <hr />
@@ -236,7 +270,9 @@ export const Toolbar = memo(function Toolbar() {
             }}
           >
             <span className="table-generator-button-label">
-              Insert row above
+              {rowsToInsert === 1
+                ? 'Insert row above'
+                : `Insert ${rowsToInsert} rows above`}
             </span>
           </button>
           <button
@@ -248,7 +284,9 @@ export const Toolbar = memo(function Toolbar() {
             }}
           >
             <span className="table-generator-button-label">
-              Insert row below
+              {rowsToInsert === 1
+                ? 'Insert row below'
+                : `Insert ${rowsToInsert} rows below`}
             </span>
           </button>
         </ToolbarDropdown>
@@ -260,6 +298,7 @@ export const Toolbar = memo(function Toolbar() {
           label="Delete table"
           command={() => {
             removeNodes(view, tableEnvironment?.table ?? positions.tabular)
+            view.focus()
           }}
         />
       </div>

+ 1 - 1
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'
         )

+ 1 - 1
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)}
                 />
               ))}
             </tr>

+ 1 - 1
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(

+ 10 - 0
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)',
     },
   }),
 ]

+ 424 - 0
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',
+    },
+  },
+})

+ 2 - 0
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 : [],
 ]

+ 0 - 1
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;

+ 0 - 397
services/web/frontend/stylesheets/app/editor/table-generator.less

@@ -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;
-  }
-}

+ 10 - 0
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;
 }