Show slash menu group headers

This commit is contained in:
rucki
2026-08-27 10:49:36 +02:00
parent 08ed9a92c1
commit 18591ce90f
6 changed files with 6 additions and 6 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
<textarea class="form-control" name="content" rows="5" x-model="text" @keydown="onKeydown($event)" @input="onInput($event)" @paste="handlePaste($event)">{{ item.content }}</textarea>
<textarea class="form-control" name="comment" rows="3" placeholder="{% trans 'Comment, e.g. same task already captured [[12]]' %}" @keydown="onKeydown($event)" @input="onInput($event)" @paste="handlePaste($event)">{{ item.comment }}</textarea>
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak>
<template x-for="(cmd, idx) in filtered" :key="cmd.token"><button type="button" class="list-group-item list-group-item-action" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span><span x-show="idx === 0 || filtered[idx-1].group !== cmd.group" class="float-end text-muted text-uppercase slash-group" x-text="cmd.group"></span></button></template>
<template x-for="(cmd, idx) in filtered" :key="cmd.token"><div><div x-show="cmd.group && (idx === 0 || filtered[idx-1].group !== cmd.group)" class="slash-group-header" x-text="cmd.group"></div><button type="button" class="list-group-item list-group-item-action w-100 text-start" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span></button></div></template>
</div>
<div class="row g-2">
<div class="col-md-4"><select class="form-select" name="visibility">
+1 -1
View File
@@ -17,7 +17,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
body{background:linear-gradient(180deg,#f8fafc 0,#f7f7f4 18rem);color:#1f2937}.shell{max-width:960px}.navbar{box-shadow:0 .35rem 1.25rem rgba(15,23,42,.06)}.navbar a{text-decoration:none}.nav-soft{color:#475569!important;font-weight:500;padding:.2rem .35rem;border-radius:.5rem}.nav-soft:hover{color:#0d6efd!important;background:#eff6ff}.user-menu-btn{min-width:9rem}.card{border:0}.item-card{border:0;border-radius:1.1rem;box-shadow:0 .45rem 1.4rem rgba(15,23,42,.07)!important}.filter-panel{padding:.25rem .15rem}.border{border-color:rgba(15,23,42,.10)!important}.text-bg-light{background:#f1f5f9!important;color:#475569!important}.btn{border-radius:.7rem}.form-control,.form-select{border-color:#e2e8f0;border-radius:.75rem}.content p:last-child{margin-bottom:0}.tag{font-size:.85rem}.slash-menu{position:absolute;z-index:20;top:100%;left:0;width:18rem}.slash-group{font-size:.65rem;letter-spacing:.04em;margin-left:.5rem}.done{opacity:.6}.done .content{text-decoration:line-through}.content img{max-width:100%;max-height:420px;border-radius:.5rem;border:1px solid #ddd;padding:.25rem;background:white}.overview-content{max-height:calc(var(--overview-lines) * 1.55em);overflow:hidden}.overview-content.expanded{max-height:none}.content{min-width:0;max-width:100%;overflow-wrap:anywhere}.content pre{position:relative;display:block;width:100%;max-width:100%;box-sizing:border-box;background:#1f2937;color:#f9fafb;border-radius:.6rem;padding:1rem;overflow-x:auto;overflow-y:auto;white-space:pre}.content pre code{display:block;color:inherit;white-space:pre;min-width:0}.copy-code{position:absolute;top:.4rem;right:.4rem;line-height:1}.copy-item{position:absolute;right:.75rem;bottom:.5rem;line-height:1;opacity:0;transition:opacity .15s}.item-card:hover .copy-item,.copy-item:focus{opacity:.7}.copy-item:hover{opacity:1!important}.mobile-create-buttons,.mobile-create-nav,.mobile-tags,.mobile-media,.mobile-tag-picker,.mobile-bottom-nav{display:none}.mobile-tag-popup{position:fixed;inset:auto .75rem 1rem .75rem;z-index:1050;max-height:55vh;overflow:auto}.mobile-tag-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.25);z-index:1040}.camera-input{max-width:13rem}@media (max-width: 767.98px){body{padding-bottom:4.25rem}.shell{max-width:100%}main.container{padding:.25rem .75rem 1rem}.navbar .container{gap:.5rem}.navbar-brand{font-size:1rem}.navbar .d-flex{gap:.5rem!important}.user-menu-btn{min-width:auto;max-width:9rem}.item-card{border-radius:.75rem}.item-actions{width:100%;justify-content:stretch!important}.item-action-btn{flex:1 1 0;min-width:0}.mobile-create-buttons,.mobile-tags,.mobile-media{display:flex!important}.mobile-bottom-nav{display:flex!important;position:fixed;left:.75rem;right:.75rem;bottom:.75rem;z-index:1060;background:white;border-radius:1rem;box-shadow:0 .5rem 1.5rem rgba(15,23,42,.18);padding:.35rem}.mobile-bottom-nav .btn{font-size:.8rem;padding:.45rem .25rem}.mobile-tag-picker{display:block!important}.desktop-quick-create{display:none!important}.mobile-create-nav{display:none!important}.desktop-file{display:none!important}.mobile-create-buttons{position:sticky;top:57px;z-index:10;background:#f7f7f4;padding:.35rem 0}.toolbar-filter{display:block;background:#f7f7f4;border-top:4px solid #fff;padding:.35rem .75rem .5rem;box-shadow:0 .4rem .9rem rgba(15,23,42,.05)}.mobile-filter-sidebar{display:none}.filter-panel{background:#f7f7f4;padding:0}.filter-panel-title{display:block;margin:0 0 .3rem!important;font-size:.82rem;color:#64748b;line-height:1}.filter-row{display:grid;grid-template-columns:repeat(5,minmax(0,1fr)) 2.25rem;gap:.25rem;width:100%}.filter-row .btn,.filter-row summary{width:100%;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-left:.1rem;padding-right:.1rem}.mobile-filter-toggle{position:absolute;opacity:0;pointer-events:none}.mobile-filter-more{grid-column:6;grid-row:1;position:relative;border:0;background:transparent;color:#475569;font-size:1.2rem;line-height:1;text-align:center;cursor:pointer}.mobile-filter-more .badge{position:absolute;top:-.15rem;right:.15rem;font-size:.55rem;line-height:1;padding:.12rem .25rem}.mobile-filter-details{display:none;grid-column:1/-1;grid-row:2;margin-top:.45rem;padding:.75rem;background:white;border-radius:1rem;box-shadow:0 .35rem 1rem rgba(15,23,42,.12)}.mobile-filter-toggle:checked ~ .mobile-filter-details{display:block}.quick-actions{flex-wrap:wrap}.quick-actions .form-control{min-width:100%}.slash-menu{width:calc(100vw - 1.5rem)}textarea.form-control-lg{font-size:1rem}.btn{touch-action:manipulation}.item-row{flex-direction:column}.item-actions{align-self:flex-end}.item-main{width:100%;min-width:0}.copy-item{opacity:.65}}
body{background:linear-gradient(180deg,#f8fafc 0,#f7f7f4 18rem);color:#1f2937}.shell{max-width:960px}.navbar{box-shadow:0 .35rem 1.25rem rgba(15,23,42,.06)}.navbar a{text-decoration:none}.nav-soft{color:#475569!important;font-weight:500;padding:.2rem .35rem;border-radius:.5rem}.nav-soft:hover{color:#0d6efd!important;background:#eff6ff}.user-menu-btn{min-width:9rem}.card{border:0}.item-card{border:0;border-radius:1.1rem;box-shadow:0 .45rem 1.4rem rgba(15,23,42,.07)!important}.filter-panel{padding:.25rem .15rem}.border{border-color:rgba(15,23,42,.10)!important}.text-bg-light{background:#f1f5f9!important;color:#475569!important}.btn{border-radius:.7rem}.form-control,.form-select{border-color:#e2e8f0;border-radius:.75rem}.content p:last-child{margin-bottom:0}.tag{font-size:.85rem}.slash-menu{position:absolute;z-index:20;top:100%;left:0;width:18rem}.slash-group-header{padding:.35rem .75rem .2rem;background:#f8fafc;color:#94a3b8;font-size:.65rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;border-top:1px solid #eef2f7}.slash-menu>div:first-of-type .slash-group-header{border-top:0}.slash-menu>div>.list-group-item{border-left:0;border-right:0;border-radius:0}.done{opacity:.6}.done .content{text-decoration:line-through}.content img{max-width:100%;max-height:420px;border-radius:.5rem;border:1px solid #ddd;padding:.25rem;background:white}.overview-content{max-height:calc(var(--overview-lines) * 1.55em);overflow:hidden}.overview-content.expanded{max-height:none}.content{min-width:0;max-width:100%;overflow-wrap:anywhere}.content pre{position:relative;display:block;width:100%;max-width:100%;box-sizing:border-box;background:#1f2937;color:#f9fafb;border-radius:.6rem;padding:1rem;overflow-x:auto;overflow-y:auto;white-space:pre}.content pre code{display:block;color:inherit;white-space:pre;min-width:0}.copy-code{position:absolute;top:.4rem;right:.4rem;line-height:1}.copy-item{position:absolute;right:.75rem;bottom:.5rem;line-height:1;opacity:0;transition:opacity .15s}.item-card:hover .copy-item,.copy-item:focus{opacity:.7}.copy-item:hover{opacity:1!important}.mobile-create-buttons,.mobile-create-nav,.mobile-tags,.mobile-media,.mobile-tag-picker,.mobile-bottom-nav{display:none}.mobile-tag-popup{position:fixed;inset:auto .75rem 1rem .75rem;z-index:1050;max-height:55vh;overflow:auto}.mobile-tag-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.25);z-index:1040}.camera-input{max-width:13rem}@media (max-width: 767.98px){body{padding-bottom:4.25rem}.shell{max-width:100%}main.container{padding:.25rem .75rem 1rem}.navbar .container{gap:.5rem}.navbar-brand{font-size:1rem}.navbar .d-flex{gap:.5rem!important}.user-menu-btn{min-width:auto;max-width:9rem}.item-card{border-radius:.75rem}.item-actions{width:100%;justify-content:stretch!important}.item-action-btn{flex:1 1 0;min-width:0}.mobile-create-buttons,.mobile-tags,.mobile-media{display:flex!important}.mobile-bottom-nav{display:flex!important;position:fixed;left:.75rem;right:.75rem;bottom:.75rem;z-index:1060;background:white;border-radius:1rem;box-shadow:0 .5rem 1.5rem rgba(15,23,42,.18);padding:.35rem}.mobile-bottom-nav .btn{font-size:.8rem;padding:.45rem .25rem}.mobile-tag-picker{display:block!important}.desktop-quick-create{display:none!important}.mobile-create-nav{display:none!important}.desktop-file{display:none!important}.mobile-create-buttons{position:sticky;top:57px;z-index:10;background:#f7f7f4;padding:.35rem 0}.toolbar-filter{display:block;background:#f7f7f4;border-top:4px solid #fff;padding:.35rem .75rem .5rem;box-shadow:0 .4rem .9rem rgba(15,23,42,.05)}.mobile-filter-sidebar{display:none}.filter-panel{background:#f7f7f4;padding:0}.filter-panel-title{display:block;margin:0 0 .3rem!important;font-size:.82rem;color:#64748b;line-height:1}.filter-row{display:grid;grid-template-columns:repeat(5,minmax(0,1fr)) 2.25rem;gap:.25rem;width:100%}.filter-row .btn,.filter-row summary{width:100%;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-left:.1rem;padding-right:.1rem}.mobile-filter-toggle{position:absolute;opacity:0;pointer-events:none}.mobile-filter-more{grid-column:6;grid-row:1;position:relative;border:0;background:transparent;color:#475569;font-size:1.2rem;line-height:1;text-align:center;cursor:pointer}.mobile-filter-more .badge{position:absolute;top:-.15rem;right:.15rem;font-size:.55rem;line-height:1;padding:.12rem .25rem}.mobile-filter-details{display:none;grid-column:1/-1;grid-row:2;margin-top:.45rem;padding:.75rem;background:white;border-radius:1rem;box-shadow:0 .35rem 1rem rgba(15,23,42,.12)}.mobile-filter-toggle:checked ~ .mobile-filter-details{display:block}.quick-actions{flex-wrap:wrap}.quick-actions .form-control{min-width:100%}.slash-menu{width:calc(100vw - 1.5rem)}textarea.form-control-lg{font-size:1rem}.btn{touch-action:manipulation}.item-row{flex-direction:column}.item-actions{align-self:flex-end}.item-main{width:100%;min-width:0}.copy-item{opacity:.65}}
</style>
</head>
<body>
+1 -1
View File
@@ -120,7 +120,7 @@
<details class="mobile-tag-picker mb-2"><summary class="btn btn-sm btn-outline-secondary">{% trans "Add tags" %}</summary><div class="mobile-tags flex-wrap gap-2 mt-2">{% for tag in tags %}<button type="button" class="btn btn-sm btn-outline-secondary" @click="addToken('#{{ tag.name|escapejs }}')">#{{ tag.name }}</button>{% endfor %}</div></details>
{{ form.content }}
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak>
<template x-for="(cmd, idx) in filtered" :key="cmd.token"><button type="button" class="list-group-item list-group-item-action" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span><span x-show="idx === 0 || filtered[idx-1].group !== cmd.group" class="float-end text-muted text-uppercase slash-group" x-text="cmd.group"></span></button></template>
<template x-for="(cmd, idx) in filtered" :key="cmd.token"><div><div x-show="cmd.group && (idx === 0 || filtered[idx-1].group !== cmd.group)" class="slash-group-header" x-text="cmd.group"></div><button type="button" class="list-group-item list-group-item-action w-100 text-start" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span></button></div></template>
</div>
<div class="d-flex align-items-center gap-2 mt-2 quick-actions">
<input class="form-control desktop-file" type="file" name="files" multiple accept="image/*,.pdf,.txt,.md" x-ref="fileInput" @change="previewFile($event)">
+1 -1
View File
@@ -18,7 +18,7 @@
<textarea class="form-control" name="content" rows="18" x-model="text" @keydown="onKeydown($event)" @input="onInput($event)" @paste="handlePaste($event)">{{ item.content }}</textarea>
<textarea class="form-control" name="comment" rows="5" placeholder="{% trans 'Comment' %}" @keydown="onKeydown($event)" @input="onInput($event)" @paste="handlePaste($event)">{{ item.comment }}</textarea>
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak>
<template x-for="(cmd, idx) in filtered" :key="cmd.token"><button type="button" class="list-group-item list-group-item-action" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span><span x-show="idx === 0 || filtered[idx-1].group !== cmd.group" class="float-end text-muted text-uppercase slash-group" x-text="cmd.group"></span></button></template>
<template x-for="(cmd, idx) in filtered" :key="cmd.token"><div><div x-show="cmd.group && (idx === 0 || filtered[idx-1].group !== cmd.group)" class="slash-group-header" x-text="cmd.group"></div><button type="button" class="list-group-item list-group-item-action w-100 text-start" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span></button></div></template>
</div>
<div class="row g-2">
<div class="col-md-4"><select class="form-select" name="visibility"><option value="private" {% if item.visibility == 'private' %}selected{% endif %}>{% trans "Private" %}</option><option value="public" {% if item.visibility == 'public' %}selected{% endif %}>{% trans "Public" %}</option></select></div>
+1 -1
View File
@@ -28,7 +28,7 @@
</div>
<details class="mobile-tag-picker mb-2"><summary class="btn btn-sm btn-outline-secondary">{% trans "Add tags" %}</summary><div class="mobile-tags flex-wrap gap-2 mt-2">{% for tag in tags %}<button type="button" class="btn btn-sm btn-outline-secondary" @click="addToken('#{{ tag.name|escapejs }}')">#{{ tag.name }}</button>{% endfor %}</div></details>
{{ form.content }}
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak><template x-for="(cmd, idx) in filtered" :key="cmd.token"><button type="button" class="list-group-item list-group-item-action" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span><span x-show="idx === 0 || filtered[idx-1].group !== cmd.group" class="float-end text-muted text-uppercase slash-group" x-text="cmd.group"></span></button></template></div>
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak><template x-for="(cmd, idx) in filtered" :key="cmd.token"><div><div x-show="cmd.group && (idx === 0 || filtered[idx-1].group !== cmd.group)" class="slash-group-header" x-text="cmd.group"></div><button type="button" class="list-group-item list-group-item-action w-100 text-start" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span></button></div></template></div>
<div class="d-flex align-items-center gap-2 mt-2 quick-actions"><input class="form-control desktop-file" type="file" name="files" multiple accept="image/*,.pdf,.txt,.md" x-ref="fileInput" @change="previewFile($event)"><div class="mobile-media gap-2 w-100"><label class="btn btn-outline-secondary flex-fill mb-0">{% trans "Gallery" %}<input class="d-none" type="file" name="files" multiple accept="image/*" @change="previewFile($event)"></label><label class="btn btn-outline-secondary flex-fill mb-0">{% trans "Photo" %}<input class="d-none" type="file" name="files" accept="image/*" capture="environment" @change="previewFile($event)"></label></div><button class="btn btn-dark px-4">{% trans "Save" %}</button></div>
</form>
<div id="items" class="vstack gap-3">
+1 -1
View File
@@ -17,7 +17,7 @@
</div>
<textarea class="form-control" name="content" rows="14" placeholder="Text … #tag [[ ![[" x-model="text" @keydown="onKeydown($event)" @input="onInput($event)" @paste="handlePaste($event)"></textarea>
<div class="slash-menu list-group shadow" x-show="open" @click.outside="open=false" x-cloak>
<template x-for="(cmd, idx) in filtered" :key="cmd.token"><button type="button" class="list-group-item list-group-item-action" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span><span x-show="idx === 0 || filtered[idx-1].group !== cmd.group" class="float-end text-muted text-uppercase slash-group" x-text="cmd.group"></span></button></template>
<template x-for="(cmd, idx) in filtered" :key="cmd.token"><div><div x-show="cmd.group && (idx === 0 || filtered[idx-1].group !== cmd.group)" class="slash-group-header" x-text="cmd.group"></div><button type="button" class="list-group-item list-group-item-action w-100 text-start" :class="{'active': idx === active}" @mouseenter="active=idx" @mousedown.prevent="insert(cmd.token)"><strong x-text="cmd.token"></strong> <span class="text-muted" x-text="cmd.label"></span></button></div></template>
</div>
<div>
<label class="form-label">{% trans "Add image" %}</label>