diff --git a/.oxlintrc.json b/.oxlintrc.json index cfd91fb..2c89af7 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -12,6 +12,13 @@ "no-console": "error", "no-unused-vars": "error", "no-shadow": "error", + "no-restricted-syntax": [ + "error", + { + "selector": "CallExpression[callee.type='MemberExpression'][callee.property.name=/^(then|catch|finally)$/]", + "message": "Prefer async/await over promise chains (.then/.catch/.finally)." + } + ], "import/no-cycle": "error", "import/no-duplicates": "error", "import/no-self-import": "error", diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0000cc4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,86 @@ +--- +name: knighted-develop-agent +description: Specialist coding agent for @knighted/develop (CDN-first browser playground for @knighted/jsx and @knighted/css). +--- + +You are a specialist engineer for the @knighted/develop package. Focus on playground runtime and UX in src/, plus build helpers in scripts/. Keep changes minimal, preserve CDN-first behavior, and validate with the listed commands. + +## Commands (run early and often) + +Repo root commands: + +- Install: npm install +- Dev server: npm run dev +- Build prep + import map generation: npm run build +- Build (esm primary CDN): npm run build:esm +- Build (jspmGa primary CDN): npm run build:jspm +- Build (importMap primary CDN): npm run build:importmap-mode +- Preview dist output: npm run preview +- Lint: npm run lint +- Format write: npm run prettier + +## Project knowledge + +Tech stack: + +- Node.js + npm +- ESM only (type: module) +- Browser-first runtime loaded from CDN +- @knighted/jsx runtime (DOM + React paths) +- @knighted/css browser compiler (CSS, Modules, Less, Sass) +- jspm for import map generation + +Repository structure: + +- src/ - app UI, CDN loader, bootstrap, styles +- scripts/ - build helper scripts for dist/import map preparation +- docs/ - package-specific docs + +## Code style and conventions + +- Preserve current project formatting: single quotes, no semicolons, print width 90, arrowParens avoid. +- Keep UI changes intentional and lightweight; avoid broad visual rewrites unless requested. +- Keep runtime logic defensive for flaky/slow CDN conditions. +- Preserve progressive loading behavior (lazy-load optional compilers/runtime pieces where possible). +- Do not introduce bundler-only assumptions into src/ runtime code. +- Prefer async/await over promise chains. +- Do not use IIFE, find another pattern instead. + +## CDN and runtime expectations + +- Keep dependency loading compatible with existing provider/fallback model in src/cdn.js. +- Prefer extending existing CDN import key patterns instead of ad hoc dynamic imports. +- Maintain graceful fallback behavior when CDN modules fail to load. +- Keep the app usable in local dev without requiring a local bundle step. + +## Testing and validation expectations + +- Run npm run lint after JavaScript edits. +- Run npm run build when touching scripts/, bootstrap, or CDN wiring. +- For UI behavior changes, validate manually through npm run dev in both render modes and at least one non-css style mode. + +## Git workflow + +- Keep changes focused to the smallest surface area. +- Update docs when behavior or developer workflow changes. +- Do not reformat unrelated files. + +## Boundaries + +Always: + +- Keep changes localized to @knighted/develop. +- Preserve ESM compatibility and browser execution. +- Preserve CDN-first loading and fallback behavior. + +Ask first: + +- Adding or upgrading dependencies. +- Changing build output contract or import-map format. +- Changing public behavior documented in README/docs. + +Never: + +- Commit secrets or credentials. +- Edit generated output folders unless explicitly requested. +- Modify node_modules or lockfiles unless explicitly requested. diff --git a/docs/build-and-deploy.md b/docs/build-and-deploy.md index 316acef..1a40d18 100644 --- a/docs/build-and-deploy.md +++ b/docs/build-and-deploy.md @@ -87,6 +87,10 @@ npm run preview ## Notes +Related docs: + +- `docs/code-mirror.md` for CodeMirror CDN integration rules, fallback behavior, and validation checklist. + - In production, the preferred/default mode is import-map-based resolution (`window.__KNIGHTED_PRIMARY_CDN__ = "importMap"`). - In `importMap` mode, runtime resolution is import-map first; if a specifier is missing from the generated map, runtime falls back through the CDN provider chain configured in `src/cdn.js`. diff --git a/docs/code-mirror.md b/docs/code-mirror.md new file mode 100644 index 0000000..89f65d6 --- /dev/null +++ b/docs/code-mirror.md @@ -0,0 +1,110 @@ +# CodeMirror Integration + +This document defines how CodeMirror is integrated in @knighted/develop and what constraints must be preserved when changing editor behavior. + +## Scope + +CodeMirror is used for both authoring panels: + +- Component panel (JSX source) +- Styles panel (CSS, CSS Modules, Less, Sass source) + +The integration is CDN-first and must keep textarea fallback behavior. + +## Integration Files + +- `src/cdn.js`: CDN import keys and provider candidates +- `src/editor-codemirror.js`: shared CodeMirror runtime + editor factory +- `src/app.js`: editor initialization, fallback handling, and value wiring +- `src/styles.css`: editor host styling + +## Runtime Model + +The app initializes CodeMirror asynchronously. + +- On success: both textareas are hidden and CodeMirror views are mounted. +- On failure: textareas remain active and the app keeps rendering normally. + +This fallback is required. Editor failures must never block rendering. + +## CDN Rules + +CodeMirror packages are loaded with `importFromCdnWithFallback` and entries in `cdnImportSpecs`. + +### Important: esm.sh specifier strategy + +Use unversioned `esm` specifiers for the CodeMirror package group: + +- `@codemirror/state` +- `@codemirror/view` +- `@codemirror/commands` +- `@codemirror/autocomplete` +- `@codemirror/language` +- `@codemirror/lang-javascript` +- `@codemirror/lang-css` + +Reason: this lets esm.sh resolve one compatible dependency graph. Mixing pinned versions can load multiple `@codemirror/state` instances and trigger: + +- `Unrecognized extension value in extension set ([object Object])` + +Keep `jspmGa` candidates as fallback entries. + +## Editor Behavior Baseline + +`src/editor-codemirror.js` should continue to include these extensions: + +- line numbers +- active line and gutter highlight +- bracket matching +- close brackets +- autocompletion +- syntax highlighting +- history keymap +- default keymap +- completion keymap +- close-bracket keymap +- `indentOnInput` +- tab size and indent unit + +Language mapping should remain: + +- component editor: `javascript-jsx` +- styles editor: + - `css` and `module` -> css language + - `less` -> less language + - `sass` -> sass language + +## App Wiring Requirements + +In `src/app.js`: + +- Keep `getJsxSource()` and `getCssSource()` abstraction so both CodeMirror and textarea fallback paths work. +- Keep `initializeCodeEditors()` non-blocking (`void initializeCodeEditors()`). +- Keep style language reconfiguration on style mode change. +- Keep textarea input listeners in place for fallback mode. + +## Validation Checklist + +When modifying editor integration: + +1. Run `npm run lint`. +2. Run `npm run dev` and verify: + - CodeMirror mounts in both panels. + - Textareas are hidden on success. + - Auto-close and indentation work while typing. + - Style mode change reconfigures language and still renders. + - Fallback path works if a CodeMirror import fails. +3. Run `npm run build` when CDN import keys are changed. + +## Troubleshooting + +If the UI still looks like plain textarea behavior: + +1. Check for `.cm-editor` nodes in devtools. +2. Check whether `textarea.source-textarea--hidden` is present. +3. Check status text for editor fallback message. +4. Hard reload to clear cached CDN module responses. +5. Inspect console for duplicate-state error: + - `Unrecognized extension value in extension set ([object Object])` + +If duplicate-state error returns, first verify `esm` CodeMirror specifiers in `src/cdn.js` are still unversioned for the full package group. diff --git a/src/app.js b/src/app.js index b775215..989877b 100644 --- a/src/app.js +++ b/src/app.js @@ -1,4 +1,5 @@ import { cdnImports, importFromCdnWithFallback } from './cdn.js' +import { createCodeMirrorEditor } from './editor-codemirror.js' const statusNode = document.getElementById('status') const renderMode = document.getElementById('render-mode') @@ -65,6 +66,10 @@ button:focus-visible { jsxEditor.value = defaultJsx cssEditor.value = defaultCss +let jsxCodeEditor = null +let cssCodeEditor = null +let getJsxSource = () => jsxEditor.value +let getCssSource = () => cssEditor.value let scheduled = null let reactRoot = null let reactRuntime = null @@ -85,6 +90,54 @@ const styleLabels = { sass: 'Sass', } +const getStyleEditorLanguage = mode => { + if (mode === 'less') return 'less' + if (mode === 'sass') return 'sass' + return 'css' +} + +const createEditorHost = textarea => { + const host = document.createElement('div') + host.className = 'editor-host' + textarea.before(host) + return host +} + +const initializeCodeEditors = async () => { + const jsxHost = createEditorHost(jsxEditor) + const cssHost = createEditorHost(cssEditor) + + try { + const [nextJsxEditor, nextCssEditor] = await Promise.all([ + createCodeMirrorEditor({ + parent: jsxHost, + value: defaultJsx, + language: 'javascript-jsx', + onChange: maybeRender, + }), + createCodeMirrorEditor({ + parent: cssHost, + value: defaultCss, + language: getStyleEditorLanguage(styleMode.value), + onChange: maybeRender, + }), + ]) + + jsxCodeEditor = nextJsxEditor + cssCodeEditor = nextCssEditor + getJsxSource = () => jsxCodeEditor.getValue() + getCssSource = () => cssCodeEditor.getValue() + + jsxEditor.classList.add('source-textarea--hidden') + cssEditor.classList.add('source-textarea--hidden') + } catch (error) { + jsxHost.remove() + cssHost.remove() + const message = error instanceof Error ? error.message : String(error) + setStatus(`Editor fallback: ${message}`) + } +} + const ensureCoreRuntime = async () => { if (coreRuntime) return coreRuntime @@ -423,7 +476,8 @@ const ensureLightningCssWasm = async () => { const compileStyles = async () => { const { cssFromSource } = await ensureCoreRuntime() const dialect = styleMode.value - const cacheKey = `${dialect}\u0000${cssEditor.value}` + const cssSource = getCssSource() + const cacheKey = `${dialect}\u0000${cssSource}` if (compiledStylesCache.key === cacheKey && compiledStylesCache.value) { return compiledStylesCache.value } @@ -432,7 +486,7 @@ const compileStyles = async () => { setStyleCompiling(shouldShowSpinner) if (!shouldShowSpinner) { - const output = { css: cssEditor.value, moduleExports: null } + const output = { css: cssSource, moduleExports: null } compiledStylesCache = { key: cacheKey, value: output, @@ -459,7 +513,7 @@ const compileStyles = async () => { options.lightningcss = await ensureLightningCssWasm() } - const result = await cssFromSource(cssEditor.value, options) + const result = await cssFromSource(cssSource, options) if (!result.ok) { throw new Error(result.error.message) } @@ -486,7 +540,7 @@ const compileStyles = async () => { const evaluateUserModule = async (helpers = {}) => { const { jsx, transpileJsxSource } = await ensureCoreRuntime() - const userCode = jsxEditor.value + const userCode = getJsxSource() .replace(/^\s*export\s+default\s+function\b/gm, '__defaultExport = function') .replace(/^\s*export\s+default\s+class\b/gm, '__defaultExport = class') .replace(/^\s*export\s+default\s+/gm, '__defaultExport = ') @@ -633,7 +687,12 @@ const maybeRender = () => { } renderMode.addEventListener('change', maybeRender) -styleMode.addEventListener('change', maybeRender) +styleMode.addEventListener('change', () => { + if (cssCodeEditor) { + cssCodeEditor.setLanguage(getStyleEditorLanguage(styleMode.value)) + } + maybeRender() +}) shadowToggle.addEventListener('change', maybeRender) autoRenderToggle.addEventListener('change', () => { if (autoRenderToggle.checked) { @@ -646,4 +705,5 @@ cssEditor.addEventListener('input', maybeRender) setStyleCompiling(false) setCdnLoading(true) +void initializeCodeEditors() renderPreview() diff --git a/src/cdn.js b/src/cdn.js index f79525f..6c51575 100644 --- a/src/cdn.js +++ b/src/cdn.js @@ -93,6 +93,46 @@ export const cdnImportSpecs = { esm: '@parcel/css-wasm', jspmGa: 'npm:@parcel/css-wasm', }, + codemirrorState: { + importMap: '@codemirror/state', + esm: '@codemirror/state', + jspmGa: 'npm:@codemirror/state@6.5.2', + }, + codemirrorView: { + importMap: '@codemirror/view', + esm: '@codemirror/view', + jspmGa: 'npm:@codemirror/view@6.38.6', + }, + codemirrorCommands: { + importMap: '@codemirror/commands', + esm: '@codemirror/commands', + jspmGa: 'npm:@codemirror/commands@6.10.0', + }, + codemirrorAutocomplete: { + importMap: '@codemirror/autocomplete', + esm: '@codemirror/autocomplete', + jspmGa: 'npm:@codemirror/autocomplete@6.20.0', + }, + codemirrorLanguage: { + importMap: '@codemirror/language', + esm: '@codemirror/language', + jspmGa: 'npm:@codemirror/language@6.11.3', + }, + codemirrorLezerHighlight: { + importMap: '@lezer/highlight', + esm: '@lezer/highlight', + jspmGa: 'npm:@lezer/highlight@1.2.3', + }, + codemirrorLangJavascript: { + importMap: '@codemirror/lang-javascript', + esm: '@codemirror/lang-javascript', + jspmGa: 'npm:@codemirror/lang-javascript@6.2.4', + }, + codemirrorLangCss: { + importMap: '@codemirror/lang-css', + esm: '@codemirror/lang-css', + jspmGa: 'npm:@codemirror/lang-css@6.3.1', + }, } const getProviderPriority = () => { diff --git a/src/editor-codemirror.js b/src/editor-codemirror.js new file mode 100644 index 0000000..8a59fcb --- /dev/null +++ b/src/editor-codemirror.js @@ -0,0 +1,320 @@ +import { cdnImports, importFromCdnWithFallback } from './cdn.js' + +let codeMirrorRuntime = null +let codeMirrorRuntimePromise = null + +const resolveLanguageExtension = (runtime, language) => { + if (language === 'javascript-jsx') { + return runtime.javascript({ jsx: true }) + } + + if (language === 'less' && typeof runtime.less === 'function') { + return runtime.less() + } + + if (language === 'sass' && typeof runtime.sass === 'function') { + return runtime.sass() + } + + return runtime.css() +} + +const loadCodeMirrorRuntime = async () => { + const [ + state, + view, + commands, + autocomplete, + language, + lezerHighlight, + langJavascript, + langCss, + ] = await Promise.all([ + importFromCdnWithFallback(cdnImports.codemirrorState), + importFromCdnWithFallback(cdnImports.codemirrorView), + importFromCdnWithFallback(cdnImports.codemirrorCommands), + importFromCdnWithFallback(cdnImports.codemirrorAutocomplete), + importFromCdnWithFallback(cdnImports.codemirrorLanguage), + importFromCdnWithFallback(cdnImports.codemirrorLezerHighlight), + importFromCdnWithFallback(cdnImports.codemirrorLangJavascript), + importFromCdnWithFallback(cdnImports.codemirrorLangCss), + ]) + + const runtime = { + Compartment: state.module.Compartment, + EditorState: state.module.EditorState, + EditorView: view.module.EditorView, + keymap: view.module.keymap, + lineNumbers: view.module.lineNumbers, + highlightActiveLineGutter: view.module.highlightActiveLineGutter, + drawSelection: view.module.drawSelection, + highlightActiveLine: view.module.highlightActiveLine, + highlightSpecialChars: view.module.highlightSpecialChars, + defaultKeymap: commands.module.defaultKeymap, + history: commands.module.history, + historyKeymap: commands.module.historyKeymap, + indentWithTab: commands.module.indentWithTab, + closeBrackets: autocomplete.module.closeBrackets, + closeBracketsKeymap: autocomplete.module.closeBracketsKeymap, + autocompletion: autocomplete.module.autocompletion, + completionKeymap: autocomplete.module.completionKeymap, + HighlightStyle: language.module.HighlightStyle, + syntaxHighlighting: language.module.syntaxHighlighting, + bracketMatching: language.module.bracketMatching, + indentOnInput: language.module.indentOnInput, + indentUnit: language.module.indentUnit, + tags: lezerHighlight.module.tags, + javascript: langJavascript.module.javascript, + css: langCss.module.css, + less: langCss.module.less, + sass: langCss.module.sass, + } + + if ( + typeof runtime.Compartment !== 'function' || + typeof runtime.EditorState !== 'function' || + typeof runtime.EditorView !== 'function' || + typeof runtime.keymap?.of !== 'function' || + typeof runtime.history !== 'function' || + typeof runtime.closeBrackets !== 'function' || + !Array.isArray(runtime.closeBracketsKeymap) || + typeof runtime.autocompletion !== 'function' || + !Array.isArray(runtime.completionKeymap) || + !runtime.HighlightStyle || + typeof runtime.syntaxHighlighting !== 'function' || + typeof runtime.indentOnInput !== 'function' || + !runtime.indentUnit || + !runtime.tags || + typeof runtime.javascript !== 'function' || + typeof runtime.css !== 'function' + ) { + throw new Error('CodeMirror runtime did not expose expected APIs.') + } + + return runtime +} + +const ensureCodeMirrorRuntime = async () => { + if (codeMirrorRuntime) return codeMirrorRuntime + if (!codeMirrorRuntimePromise) { + codeMirrorRuntimePromise = loadCodeMirrorRuntime() + } + + try { + const runtime = await codeMirrorRuntimePromise + codeMirrorRuntime = runtime + return runtime + } catch (error) { + codeMirrorRuntimePromise = null + throw error + } +} + +export const createCodeMirrorEditor = async ({ + parent, + value, + language, + onChange, + onFocus, +}) => { + const runtime = await ensureCodeMirrorRuntime() + const languageCompartment = new runtime.Compartment() + const editorHighlightStyle = runtime.HighlightStyle.define([ + { tag: runtime.tags.keyword, color: '#ff7fb3', fontWeight: '600' }, + { tag: [runtime.tags.name, runtime.tags.deleted], color: '#e7ecf9' }, + { + tag: [runtime.tags.character, runtime.tags.propertyName, runtime.tags.macroName], + color: '#3fd6a6', + }, + { + tag: [runtime.tags.function(runtime.tags.variableName), runtime.tags.labelName], + color: '#8dc8ff', + }, + { + tag: [ + runtime.tags.color, + runtime.tags.constant(runtime.tags.name), + runtime.tags.standard(runtime.tags.name), + ], + color: '#7fd7ff', + }, + { + tag: [runtime.tags.definition(runtime.tags.name), runtime.tags.separator], + color: '#dce4f6', + }, + { + tag: [runtime.tags.className, runtime.tags.typeName], + color: '#8eb8ff', + fontWeight: '600', + }, + { + tag: [ + runtime.tags.number, + runtime.tags.changed, + runtime.tags.annotation, + runtime.tags.modifier, + runtime.tags.self, + runtime.tags.namespace, + ], + color: '#ffcb82', + }, + { + tag: [runtime.tags.operator, runtime.tags.operatorKeyword], + color: '#d5def0', + }, + { + tag: [runtime.tags.string, runtime.tags.special(runtime.tags.string)], + color: '#ffd38e', + }, + { + tag: [runtime.tags.meta, runtime.tags.comment], + color: '#94a2bb', + fontStyle: 'italic', + }, + { + tag: runtime.tags.strong, + fontWeight: '700', + }, + { + tag: runtime.tags.emphasis, + fontStyle: 'italic', + }, + { + tag: runtime.tags.link, + color: '#88b6ff', + textDecoration: 'underline', + }, + { + tag: runtime.tags.heading, + color: '#f2f5ff', + fontWeight: '700', + }, + { + tag: [ + runtime.tags.atom, + runtime.tags.bool, + runtime.tags.special(runtime.tags.variableName), + ], + color: '#b8a8ff', + }, + { + tag: runtime.tags.invalid, + color: '#ff8fa1', + textDecoration: 'underline wavy #ff8fa1', + }, + ]) + const editorTheme = runtime.EditorView.theme({ + '&': { + height: '100%', + backgroundColor: 'transparent', + color: '#edf2ff', + fontSize: '0.9rem', + fontFamily: "'JetBrains Mono', 'Fira Code', monospace", + }, + '.cm-scroller': { + overflow: 'auto', + lineHeight: '1.5', + }, + '.cm-content': { + padding: '16px 18px', + minHeight: '100%', + caretColor: '#f1f5ff', + }, + '.cm-gutters': { + backgroundColor: 'rgba(255, 255, 255, 0.045)', + borderRight: '1px solid rgba(255, 255, 255, 0.13)', + color: '#98a8c4', + }, + '.cm-lineNumbers .cm-gutterElement': { + padding: '0 10px 0 14px', + }, + '&.cm-focused .cm-cursor': { + borderLeftColor: '#f1f5ff', + }, + '&.cm-focused .cm-selectionBackground, ::selection': { + backgroundColor: 'rgba(122, 107, 255, 0.36)', + }, + '&.cm-focused .cm-activeLine': { + backgroundColor: 'rgba(255, 255, 255, 0.08)', + }, + '&.cm-focused': { + outline: '1px solid rgba(122, 107, 255, 0.62)', + }, + '.cm-tooltip': { + backgroundColor: '#1b2233', + color: '#edf2ff', + border: '1px solid rgba(152, 168, 196, 0.32)', + }, + '.cm-tooltip-autocomplete > ul > li': { + color: '#dce6fa', + }, + '.cm-tooltip-autocomplete > ul > li[aria-selected]': { + backgroundColor: 'rgba(122, 107, 255, 0.34)', + color: '#f4f7ff', + }, + }) + const updateListener = runtime.EditorView.updateListener.of(update => { + if (update.docChanged && typeof onChange === 'function') { + onChange(update.state.doc.toString()) + } + + if (update.focusChanged && update.view.hasFocus && typeof onFocus === 'function') { + onFocus() + } + }) + const state = runtime.EditorState.create({ + doc: value, + extensions: [ + runtime.EditorState.tabSize.of(2), + runtime.indentUnit.of(' '), + runtime.lineNumbers(), + runtime.highlightSpecialChars(), + runtime.history(), + runtime.drawSelection(), + runtime.highlightActiveLine(), + runtime.highlightActiveLineGutter(), + runtime.bracketMatching(), + runtime.closeBrackets(), + runtime.autocompletion(), + runtime.indentOnInput(), + runtime.syntaxHighlighting(editorHighlightStyle), + runtime.EditorView.lineWrapping, + runtime.keymap.of([ + runtime.indentWithTab, + ...runtime.closeBracketsKeymap, + ...runtime.completionKeymap, + ...runtime.defaultKeymap, + ...runtime.historyKeymap, + ]), + languageCompartment.of(resolveLanguageExtension(runtime, language)), + editorTheme, + updateListener, + ], + }) + const view = new runtime.EditorView({ + state, + parent, + }) + + return { + getValue: () => view.state.doc.toString(), + setValue: nextValue => { + view.dispatch({ + changes: { + from: 0, + to: view.state.doc.length, + insert: nextValue, + }, + }) + }, + setLanguage: nextLanguage => { + view.dispatch({ + effects: languageCompartment.reconfigure( + resolveLanguageExtension(runtime, nextLanguage), + ), + }) + }, + focus: () => view.focus(), + destroy: () => view.destroy(), + } +} diff --git a/src/styles.css b/src/styles.css index 8a76db7..75fc4c8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -74,10 +74,14 @@ body { .component-panel { grid-area: component; + max-height: min(64vh, 620px); + min-height: 0; } .styles-panel { grid-area: styles; + max-height: min(64vh, 620px); + min-height: 0; } .preview-panel { @@ -105,7 +109,7 @@ body { } .panel.preview { - min-height: 500px; + min-height: 280px; } .panel-header { @@ -181,6 +185,33 @@ textarea:focus { outline: none; } +.source-textarea--hidden { + display: none; +} + +.editor-host { + flex: 1; + min-height: 0; + overflow: hidden; +} + +.editor-host .cm-editor { + height: 100%; +} + +.editor-host .cm-scroller { + font-family: 'JetBrains Mono', 'Fira Code', monospace; + overflow: auto; +} + +@media (max-width: 900px) { + .component-panel, + .styles-panel { + max-height: none; + min-height: 360px; + } +} + .panel-footer { padding: 10px 18px 16px; font-size: 0.85rem; @@ -188,7 +219,8 @@ textarea:focus { } .preview-host { - flex: 1; + flex: 0 1 auto; + min-height: 180px; padding: 18px; overflow: auto; position: relative;