Automatic SQL updates
This commit is contained in:
parent
1812873e33
commit
85a2d0683e
55
db.go
55
db.go
@ -6,17 +6,22 @@ import (
|
|||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
|
|
||||||
// Standard
|
// Standard
|
||||||
_ "database/sql"
|
"embed"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dbConn string
|
dbConn string
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
|
|
||||||
|
//go:embed sql/*
|
||||||
|
embedded embed.FS
|
||||||
)
|
)
|
||||||
|
|
||||||
func dbInit() {
|
func dbInit() (err error) {
|
||||||
dbConn = fmt.Sprintf(
|
dbConn = fmt.Sprintf(
|
||||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||||
config.Database.Host,
|
config.Database.Host,
|
||||||
@ -32,11 +37,51 @@ func dbInit() {
|
|||||||
config.Database.Port,
|
config.Database.Port,
|
||||||
)
|
)
|
||||||
|
|
||||||
var err error
|
|
||||||
db, err = sqlx.Connect("postgres", dbConn)
|
db, err = sqlx.Connect("postgres", dbConn)
|
||||||
if err != nil {
|
return
|
||||||
panic(err)
|
}
|
||||||
|
|
||||||
|
func dbUpdate() (err error) {
|
||||||
|
/* Current schema revision is read from database.
|
||||||
|
* Used to iterate through the embedded SQL updates
|
||||||
|
* up to the DB_SCHEMA version currently compiled
|
||||||
|
* program is made for. */
|
||||||
|
var rows *sqlx.Rows
|
||||||
|
var schemaStr string
|
||||||
|
var schema int
|
||||||
|
rows, err = db.Queryx(`SELECT value FROM _internal.db WHERE "key"='schema'`)
|
||||||
|
if err != nil { return }
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
return errors.New("Table _interval.db missing schema row")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = rows.Scan(&schemaStr); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run updates
|
||||||
|
schema, err = strconv.Atoi(schemaStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := (schema+1); i <= DB_SCHEMA; i++ {
|
||||||
|
log.Printf("\x1b[32mNotes\x1b[0m Upgrading SQL schema to revision %d\n", i)
|
||||||
|
sql, _ := embedded.ReadFile(
|
||||||
|
fmt.Sprintf("sql/%04d.sql", i),
|
||||||
|
)
|
||||||
|
_, err = db.Exec(string(sql))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = db.Exec(`UPDATE _internal.db SET "value"=$1 WHERE "key"='schema'`, i)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
69
main.go
69
main.go
@ -12,10 +12,12 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
_ "embed"
|
||||||
)
|
)
|
||||||
|
|
||||||
const VERSION = "v0.0.5";
|
const VERSION = "v0.0.5";
|
||||||
const LISTEN_HOST = "0.0.0.0";
|
const LISTEN_HOST = "0.0.0.0";
|
||||||
|
const DB_SCHEMA = 2
|
||||||
|
|
||||||
var (
|
var (
|
||||||
flagPort int
|
flagPort int
|
||||||
@ -49,7 +51,15 @@ func main() {// {{{
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
dbInit()
|
if err = dbInit(); err != nil {
|
||||||
|
fmt.Printf("%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err = dbUpdate(); err != nil {
|
||||||
|
fmt.Printf("%s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
connectionManager = NewConnectionManager()
|
connectionManager = NewConnectionManager()
|
||||||
go connectionManager.BroadcastLoop()
|
go connectionManager.BroadcastLoop()
|
||||||
|
|
||||||
@ -61,6 +71,8 @@ func main() {// {{{
|
|||||||
http.HandleFunc("/node/retrieve", nodeRetrieve)
|
http.HandleFunc("/node/retrieve", nodeRetrieve)
|
||||||
http.HandleFunc("/node/create", nodeCreate)
|
http.HandleFunc("/node/create", nodeCreate)
|
||||||
http.HandleFunc("/node/update", nodeUpdate)
|
http.HandleFunc("/node/update", nodeUpdate)
|
||||||
|
http.HandleFunc("/node/rename", nodeRename)
|
||||||
|
http.HandleFunc("/node/delete", nodeDelete)
|
||||||
http.HandleFunc("/ws", websocketHandler)
|
http.HandleFunc("/ws", websocketHandler)
|
||||||
http.HandleFunc("/", staticHandler)
|
http.HandleFunc("/", staticHandler)
|
||||||
|
|
||||||
@ -262,6 +274,61 @@ func nodeUpdate(w http.ResponseWriter, r *http.Request) {// {{{
|
|||||||
"OK": true,
|
"OK": true,
|
||||||
})
|
})
|
||||||
}// }}}
|
}// }}}
|
||||||
|
func nodeRename(w http.ResponseWriter, r *http.Request) {// {{{
|
||||||
|
var err error
|
||||||
|
var session Session
|
||||||
|
|
||||||
|
if session, _, err = ValidateSession(r, true); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
NodeID int
|
||||||
|
Name string
|
||||||
|
}{}
|
||||||
|
if err = parseRequest(r, &req); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = session.RenameNode(req.NodeID, req.Name)
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
})
|
||||||
|
}// }}}
|
||||||
|
func nodeDelete(w http.ResponseWriter, r *http.Request) {// {{{
|
||||||
|
var err error
|
||||||
|
var session Session
|
||||||
|
|
||||||
|
if session, _, err = ValidateSession(r, true); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
NodeID int
|
||||||
|
}{}
|
||||||
|
if err = parseRequest(r, &req); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = session.DeleteNode(req.NodeID)
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
})
|
||||||
|
}// }}}
|
||||||
|
|
||||||
func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{
|
func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{
|
||||||
// Append index.html if needed for further reading of the file
|
// Append index.html if needed for further reading of the file
|
||||||
|
35
node.go
35
node.go
@ -223,5 +223,40 @@ func (session Session) UpdateNode(nodeID int, content string) (err error) {// {{
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}// }}}
|
}// }}}
|
||||||
|
func (session Session) RenameNode(nodeID int, name string) (err error) {// {{{
|
||||||
|
_, err = db.Exec(`
|
||||||
|
UPDATE node SET name = $1 WHERE user_id = $2 AND id = $3
|
||||||
|
`,
|
||||||
|
name,
|
||||||
|
session.UserID,
|
||||||
|
nodeID,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}// }}}
|
||||||
|
func (session Session) DeleteNode(nodeID int) (err error) {// {{{
|
||||||
|
_, err = db.Exec(`
|
||||||
|
WITH RECURSIVE nodetree AS (
|
||||||
|
SELECT
|
||||||
|
id, parent_id
|
||||||
|
FROM node
|
||||||
|
WHERE
|
||||||
|
user_id = $1 AND id = $2
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
n.id, n.parent_id
|
||||||
|
FROM node n
|
||||||
|
INNER JOIN nodetree nt ON n.parent_id = nt.id
|
||||||
|
)
|
||||||
|
|
||||||
|
DELETE FROM node WHERE id IN (
|
||||||
|
SELECT id FROM nodetree
|
||||||
|
)`,
|
||||||
|
session.UserID,
|
||||||
|
nodeID,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}// }}}
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
@ -8,9 +8,8 @@
|
|||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0px;
|
border: 0px;
|
||||||
border-bottom: 1px solid #fff;
|
border-bottom: 1px solid #444;
|
||||||
font-size: 18pt;
|
font-size: 18pt;
|
||||||
color: #fff;
|
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
#login input:focus {
|
#login input:focus {
|
||||||
@ -20,11 +19,14 @@
|
|||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
border: 1px solid #666;
|
border: 1px solid #666;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #fff;
|
color: #444;
|
||||||
padding: 16px 32px;
|
padding: 16px 32px;
|
||||||
font-size: 0.8em;
|
font-size: 1em;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
#login button:hover {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
#login .auth-failed {
|
#login .auth-failed {
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
color: #a00;
|
color: #a00;
|
||||||
|
@ -24,9 +24,48 @@ h1 {
|
|||||||
display: grid;
|
display: grid;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
#menu-blackout {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
z-index: 1024;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
#menu-blackout.show {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
#menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
right: 24px;
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
border: 2px solid #000;
|
||||||
|
box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1025;
|
||||||
|
}
|
||||||
|
#menu.show {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
#menu .item {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #aaa;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
#menu .item:hover {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
#menu .item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
header {
|
header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr min-content;
|
grid-template-columns: 1fr min-content min-content;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #ecbf00;
|
background: #ecbf00;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
@ -45,6 +84,13 @@ header .add {
|
|||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
header .menu {
|
||||||
|
font-size: 1.25em;
|
||||||
|
padding-right: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
.crumbs {
|
.crumbs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -98,7 +144,7 @@ header .add {
|
|||||||
}
|
}
|
||||||
.node-content {
|
.node-content {
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
padding: 0px 32px 32px 32px;
|
padding: 0px 32px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
color: #333;
|
color: #333;
|
||||||
@ -107,9 +153,13 @@ header .add {
|
|||||||
.node-content[contenteditable] {
|
.node-content[contenteditable] {
|
||||||
outline: 0px solid transparent;
|
outline: 0px solid transparent;
|
||||||
}
|
}
|
||||||
|
.node-content:empty {
|
||||||
|
background: #ddd;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
@media only screen and (max-width: 100ex) {
|
@media only screen and (max-width: 100ex) {
|
||||||
.node-content {
|
.node-content {
|
||||||
width: initial;
|
width: 100%;
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ const html = htm.bind(h)
|
|||||||
export class NodeUI extends Component {
|
export class NodeUI extends Component {
|
||||||
constructor() {//{{{
|
constructor() {//{{{
|
||||||
super()
|
super()
|
||||||
|
this.menu = signal(false)
|
||||||
this.node = signal(null)
|
this.node = signal(null)
|
||||||
this.nodeContent = createRef()
|
this.nodeContent = createRef()
|
||||||
window.addEventListener('popstate', evt=>{
|
window.addEventListener('popstate', evt=>{
|
||||||
@ -42,14 +43,26 @@ export class NodeUI extends Component {
|
|||||||
modified = 'modified';
|
modified = 'modified';
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
<div id="menu-blackout" class="${this.menu.value ? 'show' : ''}" onclick=${()=>this.menu.value = false}></div>
|
||||||
|
<div id="menu" class="${this.menu.value ? 'show' : ''}">
|
||||||
|
<div class="item" onclick=${()=>this.renameNode()}>Rename</div>
|
||||||
|
<div class="item" onclick=${()=>this.deleteNode()}>Delete</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<header class="${modified}" onclick=${()=>this.saveNode()}>
|
<header class="${modified}" onclick=${()=>this.saveNode()}>
|
||||||
<div class="name">Notes</div>
|
<div class="name">Notes</div>
|
||||||
<div class="add" onclick=${()=>this.createNode()}>+</div>
|
<div class="add" onclick=${()=>this.createNode()}>+</div>
|
||||||
|
<div class="menu" onclick=${()=>this.showMenu()}>☰</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="crumbs">${crumbs}</crumbs>
|
<div class="crumbs">${crumbs}</crumbs>
|
||||||
|
|
||||||
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
||||||
|
|
||||||
|
${node.ID > 0 ? html`
|
||||||
<div class="node-name">${node.Name}</div>
|
<div class="node-name">${node.Name}</div>
|
||||||
<${NodeContent} key=${node.ID} content=${node.Content} ref=${this.nodeContent} />
|
<${NodeContent} key=${node.ID} content=${node.Content} ref=${this.nodeContent} />
|
||||||
|
` : html``}
|
||||||
`
|
`
|
||||||
}//}}}
|
}//}}}
|
||||||
componentDidMount() {//{{{
|
componentDidMount() {//{{{
|
||||||
@ -61,6 +74,11 @@ export class NodeUI extends Component {
|
|||||||
this.node.value = node
|
this.node.value = node
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
|
showMenu() {//{{{
|
||||||
|
this.menu.value = true
|
||||||
|
}//}}}
|
||||||
|
|
||||||
goToNode(nodeID, dontPush) {//{{{
|
goToNode(nodeID, dontPush) {//{{{
|
||||||
if(this.props.app.nodeModified.value) {
|
if(this.props.app.nodeModified.value) {
|
||||||
if(!confirm("Changes not saved. Do you want to discard changes?"))
|
if(!confirm("Changes not saved. Do you want to discard changes?"))
|
||||||
@ -89,7 +107,7 @@ export class NodeUI extends Component {
|
|||||||
})
|
})
|
||||||
.catch(this.props.app.responseError)
|
.catch(this.props.app.responseError)
|
||||||
}//}}}
|
}//}}}
|
||||||
saveNode() {
|
saveNode() {//{{{
|
||||||
let content = this.nodeContent.current.contentDiv.current.innerText
|
let content = this.nodeContent.current.contentDiv.current.innerText
|
||||||
this.props.app.request('/node/update', {
|
this.props.app.request('/node/update', {
|
||||||
NodeID: this.node.value.ID,
|
NodeID: this.node.value.ID,
|
||||||
@ -99,7 +117,35 @@ export class NodeUI extends Component {
|
|||||||
this.props.app.nodeModified.value = false
|
this.props.app.nodeModified.value = false
|
||||||
})
|
})
|
||||||
.catch(this.props.app.responseError)
|
.catch(this.props.app.responseError)
|
||||||
}
|
}//}}}
|
||||||
|
renameNode() {//{{{
|
||||||
|
let name = prompt("New name")
|
||||||
|
if(!name)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.props.app.request('/node/rename', {
|
||||||
|
Name: name.trim(),
|
||||||
|
NodeID: this.node.value.ID,
|
||||||
|
})
|
||||||
|
.then(_=>{
|
||||||
|
this.goToNode(this.node.value.ID)
|
||||||
|
this.menu.value = false
|
||||||
|
})
|
||||||
|
.catch(this.props.app.responseError)
|
||||||
|
}//}}}
|
||||||
|
deleteNode() {//{{{
|
||||||
|
if(!confirm("Do you want to delete this note and all sub-notes?"))
|
||||||
|
return
|
||||||
|
|
||||||
|
this.props.app.request('/node/delete', {
|
||||||
|
NodeID: this.node.value.ID,
|
||||||
|
})
|
||||||
|
.then(_=>{
|
||||||
|
this.goToNode(this.node.value.ParentID)
|
||||||
|
this.menu.value = false
|
||||||
|
})
|
||||||
|
.catch(this.props.app.responseError)
|
||||||
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NodeContent extends Component {
|
class NodeContent extends Component {
|
||||||
|
@ -10,10 +10,9 @@
|
|||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0px;
|
border: 0px;
|
||||||
border-bottom: 1px solid #fff;
|
border-bottom: 1px solid #444;
|
||||||
|
|
||||||
font-size: 18pt;
|
font-size: 18pt;
|
||||||
color: #fff;
|
|
||||||
background-color: @background;
|
background-color: @background;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@ -25,10 +24,14 @@
|
|||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
border: 1px solid #666;
|
border: 1px solid #666;
|
||||||
background: @background;
|
background: @background;
|
||||||
color: #fff;
|
color: #444;
|
||||||
padding: 16px 32px;
|
padding: 16px 32px;
|
||||||
font-size: 0.8em;
|
font-size: 1em;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-failed {
|
.auth-failed {
|
||||||
|
@ -30,9 +30,55 @@ h1 {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#menu-blackout {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
z-index: 1024;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
right: 24px;
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
border: 2px solid #000;
|
||||||
|
box-shadow: 5px 5px 8px 0px rgba(0,0,0,0.5);
|
||||||
|
z-index: 1025;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #aaa;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr min-content;
|
grid-template-columns: 1fr min-content min-content;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: @accent_1;
|
background: @accent_1;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
@ -53,6 +99,14 @@ header {
|
|||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
font-size: 1.25em;
|
||||||
|
padding-right: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.crumbs {
|
.crumbs {
|
||||||
@ -117,7 +171,7 @@ header {
|
|||||||
|
|
||||||
.node-content {
|
.node-content {
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
padding: 0px 32px 32px 32px;
|
padding: 0px 32px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
color: #333;
|
color: #333;
|
||||||
@ -126,11 +180,16 @@ header {
|
|||||||
&[contenteditable] {
|
&[contenteditable] {
|
||||||
outline: 0px solid transparent;
|
outline: 0px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
background: #ddd;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 100ex) {
|
@media only screen and (max-width: 100ex) {
|
||||||
.node-content {
|
.node-content {
|
||||||
width: initial;
|
width: 100%;
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user