From 58ddc86635794405c247727196b420fff5e1fb25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Tue, 27 Jun 2023 14:44:36 +0200 Subject: [PATCH] Mostly working --- main.go | 4 +- static/css/main.css | 187 ++++++++++++++++++++++++++--- static/images/collapsed.svg | 74 ++++++++++++ static/images/expanded.svg | 65 +++++++++++ static/images/leaf.svg | 57 +++++++++ static/js/app.mjs | 137 +++++++++++++++++++++- static/js/node.mjs | 79 ++++++------- static/less/main.less | 226 +++++++++++++++++++++++++++++++++--- 8 files changed, 751 insertions(+), 78 deletions(-) create mode 100644 static/images/collapsed.svg create mode 100644 static/images/expanded.svg create mode 100644 static/images/leaf.svg diff --git a/main.go b/main.go index aab3b37..d120108 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,7 @@ import ( _ "embed" ) -const VERSION = "v0.1.5"; +const VERSION = "v0.2.1"; const LISTEN_HOST = "0.0.0.0"; const DB_SCHEMA = 6 @@ -496,7 +496,7 @@ func nodeDownload(w http.ResponseWriter, r *http.Request) {// {{{ } w.Header().Add("Content-Type", files[0].MIME) - w.Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, files[0].Filename)) + w.Header().Add("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, files[0].Filename)) w.Header().Add("Content-Length", strconv.Itoa(int(finfo.Size()))) read := 1 diff --git a/static/css/main.css b/static/css/main.css index 0d06d80..88eb3d9 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -16,13 +16,61 @@ body { font-family: 'Liberation Mono', monospace; font-size: 14pt; background-color: #fff; + height: 100%; } h1 { color: #abc837; } +.layout-crumbs { + grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank"; + grid-template-columns: 1fr; + grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr; + /* blank */ +} +.layout-crumbs #tree { + display: none; +} +.layout-tree { + display: grid; + grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree files" "tree blank"; + grid-template-columns: min-content 1fr; + grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr; + /* blank */ + color: #fff; + height: 100%; +} +.layout-tree-only { + display: grid; + grid-template-areas: "header" "tree"; + grid-template-columns: 1fr; + grid-template-rows: min-content /* header */ 1fr; + /* blank */ + color: #fff; + height: 100%; +} +.layout-tree-only #crumbs { + display: none; +} +.layout-tree-only .child-nodes { + display: none; +} +.layout-tree-only .node-name { + display: none; +} +.layout-tree-only .node-content { + display: none; +} +.layout-tree-only #file-section { + display: none; +} #app { display: grid; + grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree files" "tree blank"; + grid-template-columns: min-content 1fr; + grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr; + /* blank */ color: #fff; + height: 100%; } #blackout { position: absolute; @@ -109,16 +157,31 @@ h1 { } header { display: grid; - grid-template-columns: 1fr min-content min-content; + grid-area: header; + grid-template-columns: min-content 1fr min-content min-content; align-items: center; - background: #abc837; padding: 0px; color: #333c11; + background: linear-gradient(to right, #009fff, #ec2f4b); + background: linear-gradient(to right, #f5af19, #f12711); + background: linear-gradient(to right, #fdc830, #f37335); + background: linear-gradient(to right, #8a2387, #e94057, #f27121); + background: linear-gradient(to right, #659999, #f4791f); + background: linear-gradient(to right, #3e5151, #decba4); + color: #fff; } header.modified { background: #c84a37; color: #faedeb; } +header .tree { + padding-left: 16px; +} +header .tree img { + display: block; + height: 24px; + width: 24px; +} header .name { font-weight: bold; padding-left: 16px; @@ -135,12 +198,54 @@ header .menu { user-select: none; -webkit-tap-highlight-color: transparent; } +#tree { + grid-area: tree; + padding: 16px; + background-color: #333; + color: #ddd; + font-size: 0.85em; +} +#tree .node { + display: grid; + grid-template-columns: 24px min-content; + grid-template-rows: min-content 1fr; + margin-top: 12px; +} +#tree .node .expand-toggle img { + width: 16px; + height: 16px; +} +#tree .node .name { + white-space: nowrap; + cursor: pointer; +} +#tree .node .name:hover { + color: #ecbf00; +} +#tree .node .name.selected { + color: #ecbf00; + font-weight: bold; +} +#tree .node .children { + padding-left: 24px; + margin-left: 8px; + border-left: 1px solid #555; + grid-column: 1 / -1; +} +#tree .node .children.collapsed { + display: none; +} +#crumbs { + grid-area: crumbs; + background: linear-gradient(to right, #fdc830, #f37335); +} .crumbs { display: flex; flex-wrap: wrap; - padding: 16px; - background: #333; + padding: 8px 16px; + background: #505050; color: #fff; + box-shadow: 0px 5px 8px 0px rgba(128, 128, 128, 0.5); } .crumbs .crumb { margin-right: 8px; @@ -162,15 +267,15 @@ header .menu { margin-left: 0px; } .child-nodes { + grid-area: child-nodes; display: flex; flex-wrap: wrap; padding: 16px 16px 0px 16px; - background-color: #505050; } .child-nodes .child-node { padding: 8px; border-radius: 8px; - background-color: #2f2f2f; + background-color: #333; margin-right: 12px; margin-bottom: 16px; white-space: nowrap; @@ -180,33 +285,78 @@ header .menu { -webkit-tap-highlight-color: transparent; } .node-name { - margin: 32px 0 16px 0; + grid-area: name; background: #fff; color: #000; text-align: center; font-weight: bold; + margin-top: 32px; + margin-bottom: 32px; } .node-content { + grid-area: content; justify-self: center; - padding: 16px 32px; word-wrap: break-word; font-family: monospace; font-size: 0.85em; color: #333; - width: 900px; + width: calc(100% - 32px); + max-width: 900px; resize: none; border: none; outline: none; } .node-content:invalid { background: #f5f5f5; + padding-top: 16px; } -#file-section { +/* ============================================================= * + * Textarea replicates the height of an element expanding height * + * ============================================================= */ +.grow-wrap { + /* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */ + display: grid; + grid-area: content; +} +.grow-wrap::after { + /* Note the weird space! Needed to preventy jumpy behavior */ + content: attr(data-replicated-value) " "; + /* This is how textarea text behaves */ + width: calc(100% - 32px); + max-width: 900px; + white-space: pre-wrap; + word-wrap: break-word; + color: #f0f; + background: rgba(0, 255, 255, 0.5); justify-self: center; - width: 900px; - margin-top: 16px; + /* Hidden from view, clicks, and screen readers */ + visibility: hidden; +} +.grow-wrap > textarea { + /* You could leave this, but after a user resizes, then it ruins the auto sizing */ + resize: none; + /* Firefox shows scrollbar on growth, you can hide like this. */ + overflow: hidden; +} +.grow-wrap > textarea, +.grow-wrap::after { + /* Identical styling required!! */ + padding: 0.5rem; + font: inherit; + /* Place on top of each other */ + grid-area: 1 / 1 / 2 / 2; +} +/* ============================================================= */ +#file-section { + grid-area: files; + justify-self: center; + width: calc(100% - 32px); + max-width: 900px; padding: 32px; background: #f5f5f5; + border-radius: 8px; + margin-top: 32px; + margin-bottom: 32px; } #file-section .header { font-weight: bold; @@ -233,12 +383,17 @@ header .menu { white-space: nowrap; text-align: right; } -.tree { - padding: 16px; -} @media only screen and (max-width: 932px) { + #app { + grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank"; + grid-template-columns: 1fr; + grid-template-rows: min-content /* header */ min-content /* crumbs */ min-content /* child-nodes */ min-content /* name */ min-content /* content */ min-content /* files */ 1fr; + /* blank */ + } + #app #tree { + display: none; + } .node-content { - width: calc(100% - 32px); margin-left: 16px; padding: 16px; justify-self: start; diff --git a/static/images/collapsed.svg b/static/images/collapsed.svg new file mode 100644 index 0000000..8bd376f --- /dev/null +++ b/static/images/collapsed.svg @@ -0,0 +1,74 @@ + + + +image/svg+xml diff --git a/static/images/expanded.svg b/static/images/expanded.svg new file mode 100644 index 0000000..e1a6f66 --- /dev/null +++ b/static/images/expanded.svg @@ -0,0 +1,65 @@ + + + +image/svg+xml diff --git a/static/images/leaf.svg b/static/images/leaf.svg new file mode 100644 index 0000000..ed44541 --- /dev/null +++ b/static/images/leaf.svg @@ -0,0 +1,57 @@ + + + +image/svg+xml diff --git a/static/js/app.mjs b/static/js/app.mjs index a5ba406..c78b2d2 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -3,7 +3,7 @@ import { signal } from 'preact/signals' import { h, Component, render, createRef } from 'preact' import htm from 'htm' import { Session } from 'session' -import { NodeUI } from 'node' +import { Node, NodeUI } from 'node' const html = htm.bind(h) class App extends Component { @@ -19,10 +19,16 @@ class App extends Component { this.session = new Session(this) this.session.initialize() this.login = createRef() + this.tree = null this.nodeUI = createRef() this.nodeModified = signal(false) + + this.startNode = null + + this.setStartNode() }//}}} render() {//{{{ + console.log('render', 'app') if(!this.session.initialized) { return html`
Validating session
` } @@ -31,7 +37,10 @@ class App extends Component { return html`<${Login} ref=${this.login} />` } - return html`<${NodeUI} app=${this} ref=${this.nodeUI} />` + return html` + <${Tree} app=${this} ref=${this.tree} /> + <${NodeUI} app=${this} ref=${this.nodeUI} /> + ` }//}}} wsLoop() {//{{{ @@ -131,6 +140,11 @@ class App extends Component { break; } }//}}} + setStartNode() {//{{{ + let urlParams = new URLSearchParams(window.location.search) + let nodeID = urlParams.get('node') + this.startNode = new Node(this, nodeID ? parseInt(nodeID) : 0) + }//}}} } class Login extends Component { @@ -168,6 +182,125 @@ class Login extends Component { }//}}} } +class Tree extends Component { + constructor(props) {//{{{ + super(props) + this.treeNodes = {} + this.treeNodeComponents = {} + this.treeTrunk = [] + this.selectedTreeNode = null + + this.props.app.tree = this + + this.props.app.request('/node/tree', { StartNodeID: 0 }) + .then(res=>{ + // A tree of nodes is built. This requires the list of nodes + // returned from the server to be sorted in such a way that + // a parent node always appears before a child node. + // The server uses a recursive SQL query delivering this. + res.Nodes.forEach(nodeData=>{ + let node = new Node( + this, + nodeData.ID, + ) + node.Children = [] + node.Crumbs = [] + node.Files = [] + node.Level = nodeData.Level + node.Name = nodeData.Name + node.ParentID = nodeData.ParentID + node.Updated = nodeData.Updated + node.UserID = nodeData.UserID + + this.treeNodes[node.ID] = node + + if(node.ParentID == 0) + this.treeTrunk.push(node) + else if(this.treeNodes[node.ParentID] !== undefined) + this.treeNodes[node.ParentID].Children.push(node) + }) + // When starting with an explicit node value, expanding all nodes + // on its path gives the user a sense of location. Not necessarily working + // as the start node isn't guaranteed to have returned data yet. + this.crumbsUpdateNodes() + this.forceUpdate() + + }) + .catch(this.responseError) + }//}}} + render({ app }) {//{{{ + console.log('render', 'tree') + let renderedTreeTrunk = this.treeTrunk.map(node=>{ + this.treeNodeComponents[node.ID] = createRef() + return html`<${TreeNode} key=${"treenode_"+node.ID} tree=${this} node=${node} ref=${this.treeNodeComponents[node.ID]} selected=${node.ID == app.startNode.ID} />` + }) + return html`
${renderedTreeTrunk}
` + }//}}} + + setSelected(node) {//{{{ + if(this.selectedTreeNode) + this.selectedTreeNode.selected.value = false + + this.selectedTreeNode = this.treeNodeComponents[node.ID].current + this.selectedTreeNode.selected.value = true + this.selectedTreeNode.expanded.value = true + }//}}} + crumbsUpdateNodes(node) {//{{{ + this.props.app.startNode.Crumbs.forEach(crumb=>{ + // Start node is loaded before the tree. + let node = this.treeNodes[crumb.ID] + if(node) + node._expanded = true + + // Tree is done before the start node. + let component = this.treeNodeComponents[crumb.ID] + if(component && component.current) + component.current.expanded.value = true + }) + + // Will be undefined when called from tree initialization + // (as tree nodes aren't rendered yet) + if(node !== undefined) + this.setSelected(node) + }//}}} +} + +class TreeNode extends Component { + constructor(props) {//{{{ + super(props) + this.selected = signal(props.selected) + this.expanded = signal(this.props.node._expanded) + }//}}} + render({ tree, node }) {//{{{ + console.log('render', 'treenode', node.Name) + + let children = node.Children.map(node=>{ + tree.treeNodeComponents[node.ID] = createRef() + return html`<${TreeNode} key=${"treenode_"+node.ID} tree=${tree} node=${node} ref=${tree.treeNodeComponents[node.ID]} selected=${node.ID == tree.props.app.startNode.ID} />` + }) + + let expandImg = '' + if(node.Children.length == 0) + expandImg = html`` + else { + if(this.expanded.value) + expandImg = html`` + else + expandImg = html`` + } + + + let selected = (this.selected.value ? 'selected' : '') + + return html` +
+
this.expanded.value ^= true}>${expandImg}
+
window._app.current.nodeUI.current.goToNode(node.ID)}>${node.Name}
+
${children}
+
` + }//}}} +} + // Init{{{ window._app = createRef() window._resourceModels = [] diff --git a/static/js/node.mjs b/static/js/node.mjs index a72139e..3976e52 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -7,28 +7,26 @@ export class NodeUI extends Component { constructor() {//{{{ super() this.menu = signal(false) - this.tree = signal(null) this.node = signal(null) this.nodeContent = createRef() this.upload = signal(false) + window.addEventListener('popstate', evt=>{ if(evt.state && evt.state.hasOwnProperty('nodeID')) this.goToNode(evt.state.nodeID, true) else this.goToNode(0, true) }) + window.addEventListener('keydown', evt=>this.keyHandler(evt)) }//}}} render() {//{{{ + console.log('render', 'nodeUI') + if(this.node.value === null) return let node = this.node.value - let tree = this.tree.value - - let treeHTML = html`Tree` - if(tree !== null) - treeHTML = this.renderTree(tree) let crumbs = [ html`
this.goToNode(0)}>Start
` @@ -62,12 +60,15 @@ export class NodeUI extends Component { ${menu} ${upload}
this.saveNode()}> +
Notes
this.createNode(evt)}>+
-
${crumbs} +
+
${crumbs} +
${children.length > 0 ? html`
${children}
` : html``} @@ -80,12 +81,12 @@ export class NodeUI extends Component { ` }//}}} componentDidMount() {//{{{ - let urlParams = new URLSearchParams(window.location.search) - let nodeID = urlParams.get('node') - let root = new Node(this.props.app, nodeID ? parseInt(nodeID) : 0) - - root.retrieve(node=>{ + this.props.app.startNode.retrieve(node=>{ this.node.value = node + + // The tree isn't guaranteed to have loaded yet. This is also run from + // the tree code, in case the node hasn't loaded. + this.props.app.tree.crumbsUpdateNodes(node) }) }//}}} @@ -138,10 +139,17 @@ export class NodeUI extends Component { if(!dontPush) history.pushState({ nodeID }, '', `/?node=${nodeID}`) + + // New node is fetched in order to retrieve content and files. + // Such data is unnecessary to transfer for tree/navigational purposes. let node = new Node(this.props.app, nodeID) node.retrieve(node=>{ this.props.app.nodeModified.value = false this.node.value = node + + // Tree needs to know another node is selected, in order to render any + // previously selected node not selected. + this.props.app.tree.setSelected(node) }) }//}}} createNode(evt) {//{{{ @@ -174,12 +182,6 @@ export class NodeUI extends Component { this.menu.value = false }) }//}}} - retrieveTree() {//{{{ - this.node.value.children(children=>this.tree.value = children) - }//}}} - renderTree(tree) {//{{{ - return tree.map(node=>html`
${node.Name}
`) - }//}}} } class NodeContent extends Component { @@ -193,11 +195,14 @@ class NodeContent extends Component { }//}}} render({ content }) {//{{{ return html` - +
+ +
` }//}}} componentDidMount() {//{{{ this.resize() + window.addEventListener('resize', ()=>this.resize()) }//}}} componentDidUpdate() {//{{{ this.resize() @@ -207,9 +212,9 @@ class NodeContent extends Component { this.resize() }//}}} resize() {//{{{ - let textarea = this.contentDiv.current; - textarea.style.height = "auto"; - textarea.style.height = textarea.scrollHeight + 16 + "px"; + let textarea = document.getElementById('node-content') + if(textarea) + textarea.parentNode.dataset.replicatedValue = textarea.value }//}}} } @@ -249,17 +254,20 @@ class NodeFiles extends Component { }//}}} } -class Node { +export class Node { constructor(app, nodeID) {//{{{ - this.app = app - this.ID = nodeID - this.ParentID = 0 - this.UserID = 0 - this.Name = '' - this.Content = '' - this.Children = [] - this.Crumbs = [] - this.Files = [] + this.app = app + this.ID = nodeID + this.ParentID = 0 + this.UserID = 0 + this.Name = '' + this.Content = '' + this.Children = [] + this.Crumbs = [] + this.Files = [] + this._expanded = false // start value for the TreeNode component, + // it doesn't control it afterwards. + // Used to expand the crumbs upon site loading. }//}}} retrieve(callback) {//{{{ this.app.request('/node/retrieve', { ID: this.ID }) @@ -340,13 +348,6 @@ class Node { a.remove(); //afterwards we remove the element again }) }//}}} - children(callback) {//{{{ - this.app.request('/node/tree', { StartNodeID: this.ID }) - .then(res=>{ - callback(res.Nodes) - }) - .catch(this.app.responseError) - }//}}} } class Menu extends Component { diff --git a/static/less/main.less b/static/less/main.less index 6cedf73..1e6d581 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -19,15 +19,82 @@ html, body { font-size: @fontsize; background-color: @background; + height: 100%; } h1 { color: @accent_1; } -#app { +.layout-crumbs { + grid-template-areas: + "header" + "crumbs" + "child-nodes" + "name" + "content" + "files" + "blank" + ; + grid-template-columns: 1fr; + grid-template-rows: + min-content /* header */ + min-content /* crumbs */ + min-content /* child-nodes */ + min-content /* name */ + min-content /* content */ + min-content /* files */ + 1fr; /* blank */ + #tree { display: none } +} + +.layout-tree { display: grid; + grid-template-areas: + "header header" + "tree crumbs" + "tree child-nodes" + "tree name" + "tree content" + "tree files" + "tree blank" + ; + grid-template-columns: min-content 1fr; + grid-template-rows: + min-content /* header */ + min-content /* crumbs */ + min-content /* child-nodes */ + min-content /* name */ + min-content /* content */ + min-content /* files */ + 1fr; /* blank */ color: #fff; + height: 100%; +} + +.layout-tree-only { + display: grid; + grid-template-areas: + "header" + "tree" + ; + grid-template-columns: 1fr; + grid-template-rows: + min-content /* header */ + 1fr; /* blank */ + color: #fff; + height: 100%; + + #crumbs { display: none } + .child-nodes { display: none } + .node-name { display: none } + .node-content { display: none } + #file-section { display: none } +} + +#app { + .layout-tree(); + } #blackout { @@ -129,17 +196,34 @@ h1 { header { display: grid; - grid-template-columns: 1fr min-content min-content; + grid-area: header; + grid-template-columns: min-content 1fr min-content min-content; align-items: center; - background: @accent_1; + //background: @accent_1; padding: 0px; color: darken(@accent_1, 35%); + background: linear-gradient(to right, #009fff, #ec2f4b); + background: linear-gradient(to right, #f5af19, #f12711); + background: linear-gradient(to right, #fdc830, #f37335); + background: linear-gradient(to right, #8a2387, #e94057, #f27121); + background: linear-gradient(to right, #659999, #f4791f); + background: linear-gradient(to right, #3e5151, #decba4); + color: #fff; &.modified { background: @accent_3; color: lighten(@accent_3, 45%); } + .tree { + padding-left: 16px; + img { + display: block; + height: 24px; + width: 24px; + } + } + .name { font-weight: bold; padding-left: 16px; @@ -160,12 +244,68 @@ header { } } +#tree { + grid-area: tree; + padding: 16px; + background-color: #333; + color: #ddd; + font-size: 0.85em; + + .node { + display: grid; + grid-template-columns: 24px min-content; + grid-template-rows: + min-content + 1fr; + margin-top: 12px; + + + .expand-toggle { + img { + width: 16px; + height: 16px; + } + } + + .name { + white-space: nowrap; + cursor: pointer; + + &:hover { + color: @accent_2; + } + &.selected { + color: @accent_2; + font-weight: bold; + } + + } + + .children { + padding-left: 24px; + margin-left: 8px; + border-left: 1px solid #555; + grid-column: 1 / -1; + + &.collapsed { + display: none; + } + } + } +} + +#crumbs { + grid-area: crumbs; + background: linear-gradient(to right, #fdc830, #f37335); +} + .crumbs { display: flex; flex-wrap: wrap; - padding: 16px; - background: #333; + padding: 8px 16px; + background: #505050; color: #fff; + box-shadow: 0px 5px 8px 0px rgba(128, 128, 128, 0.5); .crumb { margin-right: 8px; @@ -192,16 +332,15 @@ header { } .child-nodes { + grid-area: child-nodes; display: flex; flex-wrap: wrap; - padding: 16px 16px 0px 16px; - background-color: #505050; .child-node { padding: 8px; border-radius: 8px; - background-color: #2f2f2f; + background-color: #333; margin-right: 12px; margin-bottom: 16px; white-space: nowrap; @@ -213,36 +352,86 @@ header { } .node-name { - margin: 32px 0 16px 0; + grid-area: name; background: #fff; color: #000; text-align: center; font-weight: bold; + margin-top: 32px; + margin-bottom: 32px; } .node-content { + grid-area: content; justify-self: center; - padding: 16px 32px; word-wrap: break-word; font-family: monospace; font-size: 0.85em; color: #333; - width: 900px; + width: calc(100% - 32px); + max-width: 900px; resize: none; border: none; outline: none; &:invalid { background: #f5f5f5; + padding-top: 16px; } } -#file-section { +/* ============================================================= * + * Textarea replicates the height of an element expanding height * + * ============================================================= */ +.grow-wrap { + /* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */ + display: grid; + grid-area: content; +} +.grow-wrap::after { + /* Note the weird space! Needed to preventy jumpy behavior */ + content: attr(data-replicated-value) " "; + + /* This is how textarea text behaves */ + width: calc(100% - 32px); + max-width: 900px; + white-space: pre-wrap; + word-wrap: break-word; + color: #f0f; + background: rgba(0, 255, 255, 0.5); justify-self: center; - width: 900px; - margin-top: 16px; + + /* Hidden from view, clicks, and screen readers */ + visibility: hidden; +} +.grow-wrap > textarea { + /* You could leave this, but after a user resizes, then it ruins the auto sizing */ + resize: none; + + /* Firefox shows scrollbar on growth, you can hide like this. */ + overflow: hidden; +} +.grow-wrap > textarea, +.grow-wrap::after { + /* Identical styling required!! */ + padding: 0.5rem; + font: inherit; + + /* Place on top of each other */ + grid-area: 1 / 1 / 2 / 2; +} +/* ============================================================= */ + +#file-section { + grid-area: files; + justify-self: center; + width: calc(100% - 32px); + max-width: 900px; padding: 32px; background: #f5f5f5; + border-radius: 8px; + margin-top: 32px; + margin-bottom: 32px; .header { font-weight: bold; @@ -275,13 +464,12 @@ header { } } -.tree { - padding: 16px; -} - @media only screen and (max-width: 932px) { + #app { + .layout-crumbs(); + } + .node-content { - width: calc(100% - 32px); margin-left: 16px; padding: 16px; justify-self: start;