@jungherz-de/notionkit-elements 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/components/nk-ai-input-row.js +57 -0
- package/dist/components/nk-ai-msg.js +67 -0
- package/dist/components/nk-ai-thread.js +14 -0
- package/dist/components/nk-app.js +23 -0
- package/dist/components/nk-avatar-group.js +35 -0
- package/dist/components/nk-banner.js +30 -0
- package/dist/components/nk-block-host.js +35 -0
- package/dist/components/nk-board-view.js +129 -0
- package/dist/components/nk-breadcrumb.js +78 -0
- package/dist/components/nk-btn.js +107 -0
- package/dist/components/nk-callout.js +36 -0
- package/dist/components/nk-check.js +77 -0
- package/dist/components/nk-cmdk.js +221 -0
- package/dist/components/nk-code.js +66 -0
- package/dist/components/nk-comment.js +41 -0
- package/dist/components/nk-comments.js +57 -0
- package/dist/components/nk-danger-zone.js +27 -0
- package/dist/components/nk-database.js +100 -0
- package/dist/components/nk-divider.js +12 -0
- package/dist/components/nk-emoji-picker.js +112 -0
- package/dist/components/nk-empty.js +36 -0
- package/dist/components/nk-field.js +50 -0
- package/dist/components/nk-filter-bar.js +78 -0
- package/dist/components/nk-heading.js +29 -0
- package/dist/components/nk-input.js +108 -0
- package/dist/components/nk-kbd.js +15 -0
- package/dist/components/nk-member-list.js +30 -0
- package/dist/components/nk-member-row.js +46 -0
- package/dist/components/nk-mention.js +31 -0
- package/dist/components/nk-menu-item.js +68 -0
- package/dist/components/nk-menu.js +45 -0
- package/dist/components/nk-modal.js +166 -0
- package/dist/components/nk-model-card.js +89 -0
- package/dist/components/nk-page-actions.js +15 -0
- package/dist/components/nk-page-cover.js +35 -0
- package/dist/components/nk-page-title.js +71 -0
- package/dist/components/nk-page.js +71 -0
- package/dist/components/nk-pop.js +90 -0
- package/dist/components/nk-profile-row.js +25 -0
- package/dist/components/nk-progress.js +43 -0
- package/dist/components/nk-quote.js +32 -0
- package/dist/components/nk-radio.js +160 -0
- package/dist/components/nk-section-label.js +42 -0
- package/dist/components/nk-segmented.js +99 -0
- package/dist/components/nk-select.js +127 -0
- package/dist/components/nk-settings-pane.js +40 -0
- package/dist/components/nk-settings-user.js +36 -0
- package/dist/components/nk-sidebar.js +81 -0
- package/dist/components/nk-skeleton.js +31 -0
- package/dist/components/nk-slider.js +85 -0
- package/dist/components/nk-stat.js +40 -0
- package/dist/components/nk-stats.js +14 -0
- package/dist/components/nk-switch.js +69 -0
- package/dist/components/nk-synced.js +26 -0
- package/dist/components/nk-tab.js +54 -0
- package/dist/components/nk-table-view.js +109 -0
- package/dist/components/nk-tabs.js +82 -0
- package/dist/components/nk-tag.js +32 -0
- package/dist/components/nk-template-btn.js +43 -0
- package/dist/components/nk-textarea.js +95 -0
- package/dist/components/nk-theme-toggle.js +67 -0
- package/dist/components/nk-toast.js +51 -0
- package/dist/components/nk-todo.js +73 -0
- package/dist/components/nk-toggle.js +56 -0
- package/dist/components/nk-topbar.js +24 -0
- package/dist/components/nk-tree-item.js +155 -0
- package/dist/components/nk-tree.js +123 -0
- package/dist/components/nk-workspace-switcher.js +82 -0
- package/dist/components/shared/base-6gEcoatG.js +317 -0
- package/dist/components/shared/focus-DXrK9oLe.js +47 -0
- package/dist/components/shared/property-cell-Dn91uFkf.js +92 -0
- package/dist/notionkit-elements.esm.js +4639 -0
- package/dist/notionkit-elements.js +4718 -0
- package/dist/notionkit-elements.min.js +1 -0
- package/package.json +72 -0
- package/src/base.js +296 -0
- package/src/components/content/nk-callout.js +35 -0
- package/src/components/content/nk-code.js +51 -0
- package/src/components/content/nk-divider.js +11 -0
- package/src/components/content/nk-heading.js +28 -0
- package/src/components/content/nk-kbd.js +14 -0
- package/src/components/content/nk-progress.js +42 -0
- package/src/components/content/nk-quote.js +31 -0
- package/src/components/content/nk-tag.js +31 -0
- package/src/components/content/nk-todo.js +72 -0
- package/src/components/content/nk-toggle.js +55 -0
- package/src/components/data/nk-ai-input-row.js +56 -0
- package/src/components/data/nk-ai-msg.js +66 -0
- package/src/components/data/nk-ai-thread.js +13 -0
- package/src/components/data/nk-board-view.js +128 -0
- package/src/components/data/nk-comment.js +40 -0
- package/src/components/data/nk-comments.js +56 -0
- package/src/components/data/nk-database.js +99 -0
- package/src/components/data/nk-filter-bar.js +77 -0
- package/src/components/data/nk-table-view.js +108 -0
- package/src/components/forms/nk-btn.js +106 -0
- package/src/components/forms/nk-check.js +76 -0
- package/src/components/forms/nk-field.js +49 -0
- package/src/components/forms/nk-input.js +107 -0
- package/src/components/forms/nk-radio.js +159 -0
- package/src/components/forms/nk-select.js +126 -0
- package/src/components/forms/nk-slider.js +84 -0
- package/src/components/forms/nk-switch.js +68 -0
- package/src/components/forms/nk-textarea.js +94 -0
- package/src/components/overlays/nk-cmdk.js +201 -0
- package/src/components/overlays/nk-emoji-picker.js +94 -0
- package/src/components/overlays/nk-menu-item.js +67 -0
- package/src/components/overlays/nk-menu.js +44 -0
- package/src/components/overlays/nk-modal.js +165 -0
- package/src/components/overlays/nk-pop.js +89 -0
- package/src/components/overlays/nk-settings-pane.js +39 -0
- package/src/components/overlays/nk-settings-user.js +35 -0
- package/src/components/overlays/nk-toast.js +50 -0
- package/src/components/page/nk-avatar-group.js +34 -0
- package/src/components/page/nk-banner.js +29 -0
- package/src/components/page/nk-block-host.js +34 -0
- package/src/components/page/nk-danger-zone.js +26 -0
- package/src/components/page/nk-empty.js +35 -0
- package/src/components/page/nk-member-list.js +29 -0
- package/src/components/page/nk-member-row.js +45 -0
- package/src/components/page/nk-mention.js +30 -0
- package/src/components/page/nk-model-card.js +88 -0
- package/src/components/page/nk-page-actions.js +14 -0
- package/src/components/page/nk-page-cover.js +34 -0
- package/src/components/page/nk-page-title.js +70 -0
- package/src/components/page/nk-page.js +70 -0
- package/src/components/page/nk-profile-row.js +24 -0
- package/src/components/page/nk-segmented.js +98 -0
- package/src/components/page/nk-skeleton.js +30 -0
- package/src/components/page/nk-stat.js +39 -0
- package/src/components/page/nk-stats.js +13 -0
- package/src/components/page/nk-synced.js +25 -0
- package/src/components/page/nk-tab.js +53 -0
- package/src/components/page/nk-tabs.js +81 -0
- package/src/components/page/nk-template-btn.js +42 -0
- package/src/components/shell/nk-app.js +22 -0
- package/src/components/shell/nk-breadcrumb.js +77 -0
- package/src/components/shell/nk-section-label.js +41 -0
- package/src/components/shell/nk-sidebar.js +80 -0
- package/src/components/shell/nk-theme-toggle.js +66 -0
- package/src/components/shell/nk-topbar.js +23 -0
- package/src/components/shell/nk-tree-item.js +154 -0
- package/src/components/shell/nk-tree.js +122 -0
- package/src/components/shell/nk-workspace-switcher.js +81 -0
- package/src/index.js +88 -0
- package/src/util/emoji.js +17 -0
- package/src/util/focus.js +45 -0
- package/src/util/fuzzy.js +19 -0
- package/src/util/highlight.js +14 -0
- package/src/util/property-cell.js +90 -0
|
@@ -0,0 +1,4718 @@
|
|
|
1
|
+
var NotionKitElements = (function (exports) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// NotionKit v1.1.1 – Constructable Stylesheet
|
|
5
|
+
// Auto-generated by build-styles-js.mjs – do not edit manually.
|
|
6
|
+
|
|
7
|
+
// The :root and [data-theme="dark"] blocks that declare --nk-* …
|
|
8
|
+
const tokensCss = ":root{color-scheme:light;--nk-bg:#ffffff;--nk-bg-sidebar:#f7f6f3;--nk-bg-hover:rgba(0,0,0,0.045);--nk-bg-active:rgba(0,0,0,0.06);--nk-bg-callout:#f1f1ef;--nk-bg-code:#f7f6f3;--nk-bg-card:#ffffff;--nk-text:#37352f;--nk-text-secondary:rgba(55,53,47,0.65);--nk-text-tertiary:rgba(55,53,47,0.45);--nk-border:rgba(55,53,47,0.09);--nk-border-strong:rgba(55,53,47,0.16);--nk-accent:#2383e2;--nk-on-accent:#ffffff;--nk-danger:#eb5757;--nk-tag-blue-bg:#e7f3f8;--nk-tag-blue-text:#337ea9;--nk-tag-green-bg:#edf3ec;--nk-tag-green-text:#448361;--nk-tag-orange-bg:#fbecdd;--nk-tag-orange-text:#d9730d;--nk-tag-purple-bg:#f6f3f9;--nk-tag-purple-text:#9065b0;--nk-decor-purple:#9065b0;--nk-decor-blue:#529cca;--nk-decor-orange:#d9730d;--nk-scrim:rgba(15,15,15,0.6);--nk-scrim-soft:rgba(15,15,15,0.5);--nk-sidebar-width:260px;--nk-radius:6px;--nk-font:ui-sans-serif,-apple-system,\"Segoe UI\",Inter,Helvetica,Arial,sans-serif;--nk-font-mono:ui-monospace,\"SF Mono\",Menlo,Consolas,monospace;--nk-shadow-menu:rgba(15,15,15,0.05) 0px 0px 0px 1px,rgba(15,15,15,0.1) 0px 3px 6px,rgba(15,15,15,0.2) 0px 9px 24px;--nk-shadow-card:0 1px 2px rgba(0,0,0,0.04);--nk-shadow-knob:0 1px 2px rgba(0,0,0,0.2);--nk-shadow-segment:0 1px 2px rgba(0,0,0,0.08)}[data-theme=dark]{color-scheme:dark;--nk-bg:#191919;--nk-bg-sidebar:#202020;--nk-bg-hover:rgba(255,255,255,0.055);--nk-bg-active:rgba(255,255,255,0.08);--nk-bg-callout:#252525;--nk-bg-code:#202020;--nk-bg-card:#202020;--nk-text:rgba(255,255,255,0.81);--nk-text-secondary:rgba(255,255,255,0.46);--nk-text-tertiary:rgba(255,255,255,0.28);--nk-border:rgba(255,255,255,0.094);--nk-border-strong:rgba(255,255,255,0.16);--nk-accent:#529CCA;--nk-tag-blue-bg:#133040;--nk-tag-blue-text:#529CCA;--nk-tag-green-bg:#173B2C;--nk-tag-green-text:#4DAB9A;--nk-tag-orange-bg:#40291B;--nk-tag-orange-text:#FFA344;--nk-tag-purple-bg:#2E2440;--nk-tag-purple-text:#9A6DD7;--nk-shadow-menu:rgba(15,15,15,0.2) 0px 0px 0px 1px,rgba(15,15,15,0.4) 0px 3px 6px,rgba(15,15,15,0.6) 0px 9px 24px;--nk-shadow-card:none}";
|
|
9
|
+
|
|
10
|
+
// … and everything else, in two pieces around where they were.
|
|
11
|
+
const componentsHead = "[class*=nk-],[class*=nk-] *,[class*=nk-] ::after,[class*=nk-] ::before,[class*=nk-]::after,[class*=nk-]::before{box-sizing:border-box}[class*=nk-] :where(h1,h2,h3,h4,h5,h6,p,figure,blockquote,dl,dd,pre,ul,ol,li){margin:0;padding:0}[class*=nk-] :where(ul,ol){list-style:none}:where(button,input,select,textarea)[class*=nk-],[class*=nk-] :where(button,input,select,textarea,option,optgroup,fieldset,legend){margin:0;padding:0}";
|
|
12
|
+
const componentsTail = ".nk-body{margin:0;min-height:100vh;font-family:var(--nk-font);font-size:14px;background:var(--nk-bg);color:var(--nk-text);-webkit-font-smoothing:antialiased;transition:background .25s,color .25s}.nk-app{display:flex;height:100vh;overflow:hidden}.nk-sidebar{width:var(--nk-sidebar-width);min-width:var(--nk-sidebar-width);background:var(--nk-bg-sidebar);display:flex;flex-direction:column;border-right:1px solid var(--nk-border);transition:background .25s}.nk-workspace{display:flex;align-items:center;gap:8px;padding:10px 12px;margin:6px 8px 2px;border-radius:var(--nk-radius);cursor:pointer;font-weight:500;user-select:none}.nk-workspace:hover{background:var(--nk-bg-hover)}.nk-workspace .avatar,.nk-workspace ::slotted(.avatar),.nk-workspace ::slotted([slot=avatar]){width:22px;height:22px;border-radius:4px;background:linear-gradient(135deg,var(--nk-decor-purple),var(--nk-decor-blue));color:var(--nk-on-accent);font-size:11px;font-weight:600;display:flex;align-items:center;justify-content:center}.nk-workspace .chev{margin-left:auto;color:var(--nk-text-tertiary);font-size:11px}.nk-sidebar-scroll{flex:1;overflow-y:auto;padding:4px 8px 16px}.nk-sidebar-scroll::-webkit-scrollbar{width:8px}.nk-sidebar-scroll::-webkit-scrollbar-thumb{background:var(--nk-border-strong);border-radius:4px}.nk-section-label{font-size:11px;font-weight:500;letter-spacing:.02em;color:var(--nk-text-tertiary);padding:14px 10px 4px;display:flex;justify-content:space-between;align-items:center}.nk-section-label .plus{cursor:pointer;border-radius:4px;padding:0 4px;font-size:13px;opacity:0;transition:opacity .15s}.nk-section-label:hover .plus{opacity:1}.nk-section-label .plus:hover{background:var(--nk-bg-hover)}.nk-sidebar-footer{padding:8px;border-top:1px solid var(--nk-border);color:var(--nk-text-secondary);font-size:13px}.nk-sidebar-footer .nk-tree-item{min-height:26px}.nk-main{flex:1;display:flex;flex-direction:column;min-width:0}.nk-topbar{height:45px;display:flex;align-items:center;padding:0 12px;gap:8px;flex-shrink:0}.nk-breadcrumb{display:flex;align-items:center;gap:4px;font-size:14px}.nk-breadcrumb .crumb,.nk-breadcrumb ::slotted(.crumb){padding:2px 6px;border-radius:4px;cursor:pointer;color:var(--nk-text-secondary);display:flex;align-items:center;gap:5px}.nk-breadcrumb .crumb:hover{background:var(--nk-bg-hover)}a.crumb{text-decoration:none}.nk-breadcrumb .crumb.current,.nk-breadcrumb ::slotted(.crumb.current){color:var(--nk-text)}.nk-breadcrumb .sep{color:var(--nk-text-tertiary);font-size:12px}.nk-topbar-actions{margin-left:auto;display:flex;align-items:center;gap:4px}.nk-topbar-btn{padding:4px 8px;border-radius:var(--nk-radius);cursor:pointer;color:var(--nk-text-secondary);font-size:14px;user-select:none;border:none;background:0 0;font-family:inherit}.nk-topbar-btn:hover{background:var(--nk-bg-hover)}.nk-topbar-btn ::slotted(svg),.nk-topbar-btn svg{width:16px;height:16px;flex-shrink:0;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.nk-share-btn{border:1px solid var(--nk-border-strong);font-size:13px;padding:3px 10px}.nk-theme-toggle{font-size:15px;width:30px;height:28px;padding:0}.nk-tree-item{display:flex;align-items:center;gap:6px;padding:3px 8px 3px 10px;border-radius:var(--nk-radius);cursor:pointer;user-select:none;color:var(--nk-text-secondary);position:relative;min-height:28px}.nk-tree-item:hover{background:var(--nk-bg-hover)}.nk-tree-item.active{background:var(--nk-bg-active);color:var(--nk-text);font-weight:500}.nk-tree-item.compact{min-height:26px}.nk-tree-item .icon,.nk-tree-item ::slotted(.icon),.nk-tree-item ::slotted([slot=icon]){font-size:15px;width:20px;text-align:center;flex-shrink:0}.nk-tree-item .label,.nk-tree-item ::slotted(.label){white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.nk-tree-item .actions{margin-left:auto;display:none;gap:2px;color:var(--nk-text-tertiary)}.nk-tree-item:hover .actions{display:flex}.nk-tree-item .actions span{padding:0 3px;border-radius:3px;font-size:12px}.nk-tree-item .actions span:hover{background:var(--nk-bg-active)}.nk-tree-children{margin-left:14px;border-left:1px solid var(--nk-border);padding-left:4px;overflow:hidden}.nk-tree-children.collapsed{display:none}.nk-toggle-arrow{width:16px;height:16px;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;font-size:9px;color:var(--nk-text-tertiary);border-radius:3px;transition:transform .15s}.nk-toggle-arrow.open{transform:rotate(90deg)}.nk-kbd-hint{margin-left:auto;display:flex;gap:3px;align-items:center}.nk-kbd{font-family:inherit;font-size:10.5px;background:var(--nk-bg-callout);border:1px solid var(--nk-border-strong);border-bottom-width:2px;border-radius:4px;padding:0 4px}.nk-page-scroll{flex:1;overflow-y:auto}.nk-page-scroll::-webkit-scrollbar{width:10px}.nk-page-scroll::-webkit-scrollbar-thumb{background:var(--nk-border-strong);border-radius:5px;border:2px solid var(--nk-bg)}.nk-cover{height:200px;background:radial-gradient(ellipse at 20% 40%,color-mix(in srgb,var(--nk-decor-purple) 35%,transparent),transparent 55%),radial-gradient(ellipse at 80% 20%,color-mix(in srgb,var(--nk-decor-blue) 35%,transparent),transparent 55%),radial-gradient(ellipse at 60% 90%,color-mix(in srgb,var(--nk-decor-orange) 25%,transparent),transparent 55%),var(--nk-bg-callout)}.nk-page{max-width:760px;margin:0 auto;padding:0 64px 120px}.nk-page ::slotted(.nk-page-icon),.nk-page ::slotted([slot=icon]),.nk-page-icon{font-size:64px;line-height:1;margin-top:-42px;cursor:pointer;user-select:none;display:inline-block}.nk-page-title{font-size:40px;font-weight:700;margin:8px 0 4px;outline:0;letter-spacing:-.01em}.nk-page-meta{color:var(--nk-text-tertiary);font-size:13px;margin-bottom:24px;display:flex;gap:16px}.nk-page p.lead{font-size:16px;line-height:1.6;color:var(--nk-text-secondary);margin-bottom:20px}.nk-page ::slotted(p.lead){font-size:16px;line-height:1.6;color:var(--nk-text-secondary);margin-bottom:20px!important}.nk-heading{font-size:22px;font-weight:600;margin:32px 0 10px;display:flex;align-items:center;gap:8px}.nk-callout{display:flex;gap:10px;align-items:flex-start;background:var(--nk-bg-callout);border-radius:var(--nk-radius);padding:14px 16px;margin:16px 0;line-height:1.55;font-size:14.5px}.nk-callout .c-icon,.nk-callout ::slotted(.c-icon),.nk-callout ::slotted([slot=icon]){font-size:17px}.nk-todo{display:flex;align-items:flex-start;gap:8px;padding:4px 0}.nk-todo input[type=checkbox]{appearance:none;width:16px;height:16px;margin-top:3px;border:1.4px solid var(--nk-border-strong);border-radius:2px;cursor:pointer;flex-shrink:0;position:relative;background:var(--nk-bg)}.nk-todo input[type=checkbox]:checked{background:var(--nk-accent);border-color:var(--nk-accent)}.nk-todo input[type=checkbox]:checked::after{content:\"✓\";color:var(--nk-on-accent);font-size:11px;position:absolute;inset:0;display:flex;align-items:center;justify-content:center}.nk-todo input:checked+span{text-decoration:line-through;color:var(--nk-text-tertiary)}.nk-todo input:disabled{opacity:.5;cursor:not-allowed}.nk-toggle{margin:6px 0}.nk-toggle summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:6px;padding:4px 6px;border-radius:var(--nk-radius);font-weight:500;user-select:none}.nk-toggle summary::-webkit-details-marker{display:none}.nk-toggle summary:hover{background:var(--nk-bg-hover)}.nk-toggle summary::before{content:\"▸\";color:var(--nk-text-tertiary);font-size:11px;transition:transform .15s;display:inline-block}.nk-toggle[open] summary::before{transform:rotate(90deg)}.nk-toggle .toggle-body,.nk-toggle ::slotted(.toggle-body){padding:4px 0 8px 26px;color:var(--nk-text-secondary);line-height:1.55}.nk-divider{border:none;border-top:1px solid var(--nk-border);margin:24px 0}.nk-quote{border-left:3px solid var(--nk-text);padding:2px 0 2px 14px;margin:16px 0;color:var(--nk-text-secondary);font-size:14.5px;line-height:1.55}.nk-quote .q-cite{display:block;margin-top:6px;font-size:12.5px;color:var(--nk-text-tertiary)}.nk-mention{display:inline-flex;align-items:center;gap:4px;padding:0 4px;border-radius:4px;font-weight:500;cursor:pointer;white-space:nowrap}.nk-mention:hover{background:var(--nk-bg-hover)}.nk-mention.person{color:var(--nk-text)}.nk-mention.person .mini-avatar,.nk-mention.person ::slotted(.mini-avatar),.nk-mention.person ::slotted([slot=avatar]){width:16px;height:16px;border-radius:50%;color:var(--nk-on-accent);font-size:8.5px;font-weight:600;display:inline-flex;align-items:center;justify-content:center}.nk-mention.page{color:var(--nk-text-secondary);text-decoration:underline;text-decoration-color:var(--nk-border-strong);text-underline-offset:2px}.nk-mention.date{color:var(--nk-tag-orange-text)}.nk-inline-code{font-family:var(--nk-font-mono);font-size:85%;color:var(--nk-danger);background:color-mix(in srgb,var(--nk-danger) 8%,transparent);border-radius:4px;padding:1px 5px}.nk-code{background:var(--nk-bg-code);border:1px solid var(--nk-border);border-radius:var(--nk-radius);padding:20px 24px;font-family:var(--nk-font-mono);font-size:13px;line-height:1.6;margin:14px 0;overflow-x:auto;color:var(--nk-text-secondary);white-space:pre;position:relative}.nk-code .lang{position:absolute;top:6px;right:10px;font-size:11px;color:var(--nk-text-tertiary);font-family:var(--nk-font)}.nk-code .tag,.nk-code ::slotted(.tag){color:var(--nk-accent)}.nk-code .attr,.nk-code ::slotted(.attr){color:var(--nk-tag-orange-text)}.nk-database{margin:12px 0 8px}.nk-db-tabs{display:flex;gap:2px;border-bottom:1px solid var(--nk-border);margin-bottom:0;font-size:13.5px}.nk-db-tab{padding:6px 10px;cursor:pointer;color:var(--nk-text-secondary);border-bottom:2px solid transparent;margin-bottom:-1px;user-select:none;display:flex;align-items:center;gap:6px}.nk-db-tab:hover{color:var(--nk-text)}.nk-db-tab.active{color:var(--nk-text);border-bottom-color:var(--nk-text);font-weight:500}.nk-db-tab .badge,.nk-db-tab ::slotted(.badge){background:var(--nk-bg-callout);border-radius:8px;font-size:11px;padding:0 6px;color:var(--nk-text-tertiary)}.nk-table-wrap{overflow-x:auto}.nk-table{width:100%;border-collapse:collapse;font-size:14px}.nk-table th{text-align:left;font-weight:400;color:var(--nk-text-tertiary);padding:8px 10px;border-bottom:1px solid var(--nk-border);font-size:13px;white-space:nowrap;user-select:none;cursor:pointer}.nk-table th:hover{background:var(--nk-bg-hover)}.nk-table th .th-icon,.nk-table th ::slotted(.th-icon){margin-right:5px;font-size:12px}.nk-table td{padding:7px 10px;border-bottom:1px solid var(--nk-border);white-space:nowrap;vertical-align:middle}.nk-table tbody tr:hover{background:var(--nk-bg-hover)}.nk-table .row-title{display:flex;align-items:center;gap:7px;font-weight:500;cursor:pointer}.nk-table .row-title:hover{text-decoration:underline;text-decoration-color:var(--nk-text-tertiary)}.nk-table .date-cell{color:var(--nk-text-secondary)}.nk-table .person-cell{display:flex;align-items:center;gap:6px;color:var(--nk-text-secondary)}.nk-table .person-cell .mini-avatar,.nk-table .person-cell ::slotted(.mini-avatar){width:20px;height:20px;border-radius:50%;font-size:10px;color:var(--nk-on-accent);display:inline-flex;align-items:center;justify-content:center;font-weight:600}.nk-new-row,.nk-table .new-row{color:var(--nk-text-tertiary);cursor:pointer;padding:8px 10px;display:flex;align-items:center;gap:7px;font-size:14px}.nk-new-row:hover,.nk-table .new-row:hover{background:var(--nk-bg-hover)}.nk-tag{display:inline-block;padding:1px 8px;border-radius:10px;font-size:12.5px;font-weight:450;line-height:1.6}.nk-tag.blue{background:var(--nk-tag-blue-bg);color:var(--nk-tag-blue-text)}.nk-tag.green{background:var(--nk-tag-green-bg);color:var(--nk-tag-green-text)}.nk-tag.orange{background:var(--nk-tag-orange-bg);color:var(--nk-tag-orange-text)}.nk-tag.purple{background:var(--nk-tag-purple-bg);color:var(--nk-tag-purple-text)}.nk-progress{width:110px;height:6px;border-radius:3px;background:var(--nk-tag-blue-bg);overflow:hidden;display:inline-block}.nk-progress>i{display:block;height:100%;background:var(--nk-tag-blue-text);border-radius:3px}.nk-progress-label{font-size:12px;color:var(--nk-text-tertiary);margin-left:7px}.nk-board{display:none;gap:12px;padding:14px 2px;overflow-x:auto}.nk-board.active{display:flex}.nk-board-col{min-width:220px;width:220px;flex-shrink:0}.nk-board-col-header{display:flex;align-items:center;gap:7px;margin-bottom:8px;font-size:13.5px;font-weight:500}.nk-board-col-header .count{color:var(--nk-text-tertiary);font-weight:400}.nk-card{background:var(--nk-bg-card);border:1px solid var(--nk-border);border-radius:8px;padding:10px 12px;margin-bottom:8px;cursor:grab;box-shadow:var(--nk-shadow-card);transition:border-color .15s}.nk-card:hover{border-color:var(--nk-border-strong)}.nk-card .card-title,.nk-card ::slotted(.card-title){font-weight:500;margin-bottom:7px;display:flex;gap:6px;align-items:center}.nk-card .card-meta,.nk-card ::slotted(.card-meta){display:flex;gap:6px;flex-wrap:wrap;align-items:center;font-size:12px;color:var(--nk-text-tertiary)}.nk-field{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:9px 0}.nk-field .f-label,.nk-field ::slotted(.f-label),.nk-field ::slotted([slot=label]){font-size:14px}.nk-field .f-desc,.nk-field ::slotted(.f-desc),.nk-field ::slotted([slot=desc]){font-size:12.5px;color:var(--nk-text-tertiary);margin-top:2px;line-height:1.45}.nk-field .f-control,.nk-field ::slotted(.f-control){flex-shrink:0}.nk-input,.nk-select,.nk-textarea{font-family:inherit;font-size:13.5px;color:var(--nk-text);background:var(--nk-bg);border:1px solid var(--nk-border-strong);border-radius:var(--nk-radius);padding:5px 10px;outline:0;transition:border-color .15s,box-shadow .15s;min-width:210px}.nk-input:focus,.nk-select:focus,.nk-textarea:focus{border-color:var(--nk-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--nk-accent) 25%,transparent)}.nk-input::placeholder,.nk-textarea::placeholder{color:var(--nk-text-tertiary)}.nk-input.wide,.nk-textarea.wide{width:100%}.nk-select.compact{min-width:120px}.nk-textarea{resize:vertical;min-height:70px;line-height:1.5}.nk-select{appearance:none;cursor:pointer;padding-right:26px;background-image:linear-gradient(45deg,transparent 50%,var(--nk-text-tertiary) 50%),linear-gradient(135deg,var(--nk-text-tertiary) 50%,transparent 50%);background-position:calc(100% - 14px) 55%,calc(100% - 10px) 55%;background-size:4px 4px;background-repeat:no-repeat}.nk-btn{font-family:inherit;font-size:13.5px;font-weight:500;border-radius:var(--nk-radius);cursor:pointer;padding:5px 12px;border:1px solid transparent;transition:background .15s,border-color .15s,opacity .15s;display:inline-flex;align-items:center;gap:6px}.nk-btn ::slotted(svg),.nk-btn svg{width:15px;height:15px;flex-shrink:0;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.nk-btn.primary{background:var(--nk-accent);color:var(--nk-on-accent)}.nk-btn.primary:hover{opacity:.88}.nk-btn.secondary{background:0 0;color:var(--nk-text);border-color:var(--nk-border-strong)}.nk-btn.secondary:hover{background:var(--nk-bg-hover)}.nk-btn.danger{background:0 0;color:var(--nk-danger);border-color:var(--nk-border-strong)}.nk-btn.danger:hover{background:color-mix(in srgb,var(--nk-danger) 8%,transparent)}.nk-btn.danger-solid{background:var(--nk-danger);color:var(--nk-on-accent)}.nk-btn.danger-solid:hover{opacity:.88}.nk-btn.small{padding:3px 9px;font-size:12.5px}a.nk-btn{text-decoration:none}.nk-btn:disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.nk-switch{position:relative;width:34px;height:20px;flex-shrink:0;appearance:none;border:none;cursor:pointer;border-radius:20px;background:var(--nk-border-strong);transition:background .2s}.nk-switch::after{content:\"\";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--nk-on-accent);transition:transform .2s;box-shadow:var(--nk-shadow-knob)}.nk-switch:checked,.nk-switch[aria-checked=true]{background:var(--nk-accent)}.nk-switch:checked::after,.nk-switch[aria-checked=true]::after{transform:translateX(14px)}.nk-switch:disabled{opacity:.5;cursor:not-allowed}.nk-check{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:14px;cursor:pointer}.nk-check input{appearance:none;width:16px;height:16px;flex-shrink:0;border:1.4px solid var(--nk-border-strong);border-radius:3px;cursor:pointer;position:relative;background:var(--nk-bg)}.nk-check input[type=radio]{border-radius:50%}.nk-check input:checked{background:var(--nk-accent);border-color:var(--nk-accent)}.nk-check input:checked::after{content:\"✓\";color:var(--nk-on-accent);font-size:10.5px;position:absolute;inset:0;display:flex;align-items:center;justify-content:center}.nk-check input[type=radio]:checked::after{content:\"●\";font-size:7px}.nk-check input:disabled{opacity:.5;cursor:not-allowed}.nk-check input:disabled+*{opacity:.6}.nk-slider{width:210px;accent-color:var(--nk-accent);cursor:pointer}.nk-slider-value{font-size:12.5px;color:var(--nk-text-tertiary);margin-top:3px}.nk-profile-row{display:flex;align-items:center;gap:14px;padding:8px 0}.nk-profile-row .big-avatar,.nk-profile-row ::slotted(.big-avatar),.nk-profile-row ::slotted([slot=avatar]){width:56px;height:56px;border-radius:50%;background:linear-gradient(135deg,var(--nk-decor-purple),var(--nk-decor-blue));color:var(--nk-on-accent);font-size:20px;font-weight:600;display:flex;align-items:center;justify-content:center}.nk-model-card{border:1px solid var(--nk-border-strong);border-radius:8px;padding:12px 14px;margin-bottom:10px;cursor:pointer;display:flex;align-items:flex-start;gap:11px;transition:border-color .15s,background .15s}.nk-model-card:hover{background:var(--nk-bg-hover)}.nk-model-card.selected{border-color:var(--nk-accent);background:color-mix(in srgb,var(--nk-accent) 6%,transparent)}.nk-model-card .m-radio{width:15px;height:15px;border-radius:50%;flex-shrink:0;margin-top:2px;border:1.4px solid var(--nk-border-strong);position:relative}.nk-model-card.selected .m-radio{border-color:var(--nk-accent)}.nk-model-card.selected .m-radio::after{content:\"\";position:absolute;inset:3px;border-radius:50%;background:var(--nk-accent)}.nk-model-card .m-name,.nk-model-card ::slotted(.m-name),.nk-model-card ::slotted([slot=title]){font-weight:500;font-size:14px;display:flex;align-items:center;gap:7px}.nk-model-card .m-desc,.nk-model-card ::slotted(.m-desc),.nk-model-card ::slotted([slot=desc]){font-size:12.5px;color:var(--nk-text-tertiary);margin-top:2px;line-height:1.45}.nk-danger-zone{border:1px solid color-mix(in srgb,var(--nk-danger) 40%,transparent);border-radius:8px;padding:14px 16px;margin-top:26px}.nk-danger-zone .dz-title{color:var(--nk-danger);font-weight:600;font-size:13px;margin-bottom:8px}.nk-member-row{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--nk-border)}.nk-member-list ::slotted(.nk-member-row:last-child),.nk-member-list>.nk-member-row:last-child,.nk-member-row.last{border-bottom:none}.nk-member-row .mini-avatar,.nk-member-row ::slotted(.mini-avatar),.nk-member-row ::slotted([slot=avatar]){width:28px;height:28px;border-radius:50%;color:var(--nk-on-accent);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;flex-shrink:0}.nk-member-row .m-mail{font-size:12px;color:var(--nk-text-tertiary)}.nk-member-row .nk-select{min-width:120px;margin-left:auto}.nk-member-row ::slotted([slot=role]){margin-left:auto}.nk-modal-backdrop{position:fixed;inset:0;background:var(--nk-scrim);display:flex;align-items:center;justify-content:center;z-index:100;opacity:0;pointer-events:none;transition:opacity .2s}.nk-modal-backdrop.open{opacity:1;pointer-events:auto}.nk-modal{width:min(960px,92vw);height:min(640px,86vh);background:var(--nk-bg);border-radius:10px;box-shadow:var(--nk-shadow-menu);display:flex;overflow:hidden;transform:scale(.98) translateY(6px);transition:transform .2s}.nk-modal-backdrop.open .nk-modal{transform:scale(1) translateY(0)}.nk-settings-nav{width:230px;min-width:230px;background:var(--nk-bg-sidebar);border-right:1px solid var(--nk-border);padding:10px 8px;overflow-y:auto;flex-shrink:0}.nk-settings-user{display:flex;align-items:center;gap:9px;padding:6px 10px 12px;margin-bottom:4px}.nk-settings-user .avatar,.nk-settings-user ::slotted(.avatar),.nk-settings-user ::slotted([slot=avatar]){width:34px;height:34px;border-radius:50%;background:linear-gradient(135deg,var(--nk-decor-purple),var(--nk-decor-blue));color:var(--nk-on-accent);font-weight:600;font-size:13px;display:flex;align-items:center;justify-content:center}.nk-settings-user .name{font-weight:500;font-size:13.5px}.nk-settings-user .mail{font-size:11.5px;color:var(--nk-text-tertiary)}.nk-settings-nav .nk-tree-item{min-height:27px}.nk-settings-nav .nk-tree-item.active{background:var(--nk-bg-active);color:var(--nk-text);font-weight:500}.nk-settings-content{flex:1;overflow-y:auto;padding:36px 48px 80px;min-width:0}.nk-settings-content::-webkit-scrollbar{width:10px}.nk-settings-content::-webkit-scrollbar-thumb{background:var(--nk-border-strong);border-radius:5px;border:2px solid var(--nk-bg)}.nk-settings-pane{display:none;max-width:520px}.nk-settings-pane.active{display:block}.nk-settings-pane>h2{font-size:16px;font-weight:600;padding-bottom:12px;border-bottom:1px solid var(--nk-border);margin-bottom:20px}.nk-settings-pane>h3{font-size:13px;font-weight:600;margin:26px 0 10px}.nk-settings-pane ::slotted(h2){font-size:16px;font-weight:600;border:0;border-bottom:1px solid var(--nk-border);padding:0 0 12px!important;margin:0 0 20px!important}.nk-settings-pane ::slotted(h3){font-size:13px;font-weight:600;padding:0!important;margin:26px 0 10px!important}.nk-pop{background:var(--nk-bg);border-radius:10px;box-shadow:var(--nk-shadow-menu);padding:10px;width:296px}.nk-emoji-search{width:100%;font-family:inherit;font-size:13px;color:var(--nk-text);background:var(--nk-bg-callout);border:none;border-radius:var(--nk-radius);padding:6px 10px;outline:0;margin-bottom:8px}.nk-emoji-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:2px}.nk-emoji-grid ::slotted(span),.nk-emoji-grid span{font-size:18px;text-align:center;padding:4px 0;border-radius:5px;cursor:pointer;user-select:none}.nk-emoji-grid span:hover{background:var(--nk-bg-hover)}.nk-emoji-cats{display:flex;gap:2px;margin-top:8px;padding-top:8px;border-top:1px solid var(--nk-border)}.nk-emoji-cats span{font-size:14px;padding:3px 7px;border-radius:5px;cursor:pointer;color:var(--nk-text-tertiary);filter:grayscale(1);opacity:.6}.nk-emoji-cats span.active,.nk-emoji-cats span:hover{filter:none;opacity:1;background:var(--nk-bg-hover)}.nk-menu{padding:5px;width:230px}.nk-menu-item{display:flex;align-items:center;gap:9px;padding:5px 9px;border-radius:5px;cursor:pointer;font-size:13.5px;user-select:none}.nk-menu-item:hover{background:var(--nk-bg-hover)}.nk-menu-item .m-icon,.nk-menu-item ::slotted(.m-icon),.nk-menu-item ::slotted([slot=icon]){width:17px;text-align:center;font-size:14px}.nk-menu-item .m-shortcut,.nk-menu-item ::slotted(.m-shortcut){margin-left:auto;font-size:11.5px;color:var(--nk-text-tertiary)}.nk-menu-item.danger{color:var(--nk-danger)}.nk-menu-sep{border-top:1px solid var(--nk-border);margin:5px 4px}.nk-menu-label{font-size:11px;color:var(--nk-text-tertiary);padding:5px 9px 2px}.nk-cmdk-backdrop{position:fixed;inset:0;background:var(--nk-scrim-soft);display:flex;justify-content:center;align-items:flex-start;padding-top:14vh;z-index:110;opacity:0;pointer-events:none;transition:opacity .18s}.nk-cmdk-backdrop.open{opacity:1;pointer-events:auto}.nk-cmdk{width:min(560px,92vw);background:var(--nk-bg);border-radius:12px;box-shadow:var(--nk-shadow-menu);overflow:hidden;transform:scale(.98);transition:transform .18s}.nk-cmdk-backdrop.open .nk-cmdk{transform:scale(1)}.nk-cmdk-input-row{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid var(--nk-border)}.nk-cmdk-input-row input{flex:1;border:none;outline:0;background:0 0;font-family:inherit;font-size:16px;color:var(--nk-text)}.nk-cmdk-input-row input::placeholder{color:var(--nk-text-tertiary)}.nk-cmdk-list{max-height:320px;overflow-y:auto;padding:6px}.nk-cmdk-group{font-size:11px;color:var(--nk-text-tertiary);padding:8px 10px 3px}.nk-cmdk-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:6px;cursor:pointer;font-size:14px;user-select:none}.nk-cmdk-item.selected{background:var(--nk-bg-active)}.nk-cmdk-item .m-icon,.nk-cmdk-item ::slotted(.m-icon),.nk-cmdk-item ::slotted([slot=icon]){width:17px;text-align:center;font-size:14px}.nk-cmdk-item .m-shortcut,.nk-cmdk-item ::slotted(.m-shortcut){margin-left:auto;font-size:11.5px;color:var(--nk-text-tertiary)}.nk-cmdk-empty{padding:30px;text-align:center;color:var(--nk-text-tertiary);font-size:13.5px}.nk-cmdk-footer{display:flex;gap:14px;padding:8px 16px;border-top:1px solid var(--nk-border);font-size:11.5px;color:var(--nk-text-tertiary)}.nk-toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(80px);background:var(--nk-text);color:var(--nk-bg);padding:8px 16px;border-radius:8px;font-size:13.5px;opacity:0;transition:.3s;pointer-events:none;box-shadow:var(--nk-shadow-menu);display:flex;gap:8px;align-items:center}.nk-toast.show{opacity:1;transform:translateX(-50%) translateY(0)}.nk-gallery-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));gap:20px 32px;margin:12px 0}.nk-g-item>h4{font-size:12px;font-weight:500;color:var(--nk-text-tertiary);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px}.nk-g-item ::slotted(h4){font-size:12px;font-weight:500;color:var(--nk-text-tertiary);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px!important}.nk-tabs{display:flex;gap:2px;border-bottom:1px solid var(--nk-border);font-size:14px}.nk-tab{padding:7px 11px;cursor:pointer;color:var(--nk-text-secondary);border-bottom:2px solid transparent;margin-bottom:-1px;user-select:none}.nk-tab:hover{color:var(--nk-text)}.nk-tab.active{color:var(--nk-text);border-bottom-color:var(--nk-text);font-weight:500}.nk-tab-panel{padding:12px 2px;color:var(--nk-text-secondary);line-height:1.55;font-size:14px}.nk-template-btn{display:flex;align-items:center;gap:8px;width:100%;font-family:inherit;font-size:14px;color:var(--nk-text-secondary);background:var(--nk-bg-callout);border:none;border-radius:var(--nk-radius);padding:8px 12px;cursor:pointer;margin-bottom:6px;text-align:left}.nk-template-btn:hover{background:var(--nk-bg-hover)}.nk-stats{display:flex;gap:10px}.nk-stat{flex:1;border:1px solid var(--nk-border);border-radius:8px;padding:12px 14px}.nk-stat .s-label,.nk-stat ::slotted(.s-label),.nk-stat ::slotted([slot=label]){font-size:12px;color:var(--nk-text-tertiary);margin-bottom:4px}.nk-stat .s-value,.nk-stat ::slotted(.s-value),.nk-stat ::slotted([slot=value]){font-size:22px;font-weight:600;letter-spacing:-.01em}.nk-stat .s-delta,.nk-stat ::slotted(.s-delta),.nk-stat ::slotted([slot=delta]){font-size:11.5px;margin-top:3px}.nk-stat .s-delta.up,.nk-stat ::slotted(.s-delta.up),.nk-stat ::slotted([slot=delta].up){color:var(--nk-tag-green-text)}.nk-stat .s-delta.down,.nk-stat ::slotted(.s-delta.down),.nk-stat ::slotted([slot=delta].down){color:var(--nk-danger)}.nk-synced{border:1.5px solid color-mix(in srgb,var(--nk-danger) 55%,transparent);border-radius:8px;padding:12px 14px;position:relative;margin:8px 0}.nk-synced .synced-badge{position:absolute;top:-9px;right:10px;font-size:11px;background:var(--nk-danger);color:var(--nk-on-accent);border-radius:8px;padding:1px 8px}.nk-segmented{display:inline-flex;background:var(--nk-bg-callout);border-radius:7px;padding:2px}.nk-segmented button{font-family:inherit;font-size:13px;border:none;background:0 0;color:var(--nk-text-secondary);padding:4px 12px;border-radius:5px;cursor:pointer}.nk-segmented ::slotted(button){font-family:inherit;font-size:13px;border:none;background:0 0;color:var(--nk-text-secondary);padding:4px 12px!important;border-radius:5px;cursor:pointer}.nk-segmented ::slotted(button.active),.nk-segmented button.active{background:var(--nk-bg);color:var(--nk-text);font-weight:500;box-shadow:var(--nk-shadow-segment)}.nk-banner{display:flex;align-items:center;gap:10px;font-size:13.5px;border-radius:8px;padding:9px 14px;margin-bottom:8px}.nk-banner.info{background:var(--nk-tag-blue-bg);color:var(--nk-tag-blue-text)}.nk-banner.success{background:var(--nk-tag-green-bg);color:var(--nk-tag-green-text)}.nk-banner.warning{background:var(--nk-tag-orange-bg);color:var(--nk-tag-orange-text)}.nk-banner .b-action,.nk-banner ::slotted(.b-action),.nk-banner ::slotted([slot=action]){margin-left:auto;font-weight:500;cursor:pointer;text-decoration:underline;text-underline-offset:2px}.nk-avatar-group{display:flex}.nk-avatar-group .mini-avatar,.nk-avatar-group ::slotted(.mini-avatar){width:26px;height:26px;border-radius:50%;color:var(--nk-on-accent);font-size:10.5px;font-weight:600;border:2px solid var(--nk-bg);display:flex;align-items:center;justify-content:center;margin-left:-7px}.nk-avatar-group .mini-avatar:first-child,.nk-avatar-group ::slotted(.mini-avatar:first-child){margin-left:0}.nk-avatar-group .more,.nk-avatar-group ::slotted(.more){background:var(--nk-bg-callout);color:var(--nk-text-secondary)}.nk-skeleton{border-radius:5px;margin-bottom:10px;background:linear-gradient(90deg,var(--nk-bg-callout) 25%,var(--nk-bg-hover) 50%,var(--nk-bg-callout) 75%);background-size:200% 100%;animation:1.6s infinite nk-shimmer}@keyframes nk-shimmer{to{background-position:-200% 0}}.nk-empty{text-align:center;padding:36px 20px;border:1.5px dashed var(--nk-border-strong);border-radius:10px;color:var(--nk-text-tertiary)}.nk-empty .e-icon,.nk-empty ::slotted(.e-icon),.nk-empty ::slotted([slot=icon]){font-size:30px;margin-bottom:8px}.nk-empty .e-title,.nk-empty ::slotted(.e-title),.nk-empty ::slotted([slot=title]){font-weight:500;color:var(--nk-text-secondary);margin-bottom:3px;font-size:14px}.nk-empty .e-desc,.nk-empty ::slotted(.e-desc),.nk-empty ::slotted([slot=desc]){font-size:12.5px;margin-bottom:12px}.nk-comments{border-left:1px solid var(--nk-border);padding:4px 0 4px 18px;margin:8px 0 8px 8px}.nk-comment{display:flex;gap:9px;margin-bottom:14px}.nk-comment .mini-avatar,.nk-comment ::slotted(.mini-avatar),.nk-comment ::slotted([slot=avatar]){width:24px;height:24px;border-radius:50%;color:var(--nk-on-accent);flex-shrink:0;font-size:10px;font-weight:600;display:flex;align-items:center;justify-content:center}.nk-comment .c-head,.nk-comment ::slotted(.c-head){font-size:12.5px;color:var(--nk-text-tertiary);margin-bottom:2px}.nk-comment .c-head b{color:var(--nk-text);font-weight:600}.nk-comment .c-body,.nk-comment ::slotted(.c-body){font-size:13.5px;line-height:1.5}.nk-comment-input{display:flex;gap:8px;align-items:center;margin-top:4px}.nk-comment-input input{min-width:0;flex:1}.nk-ai-thread{display:flex;flex-direction:column;gap:14px}.nk-ai-msg{display:flex;gap:9px}.nk-ai-msg .mini-avatar,.nk-ai-msg ::slotted(.mini-avatar),.nk-ai-msg ::slotted([slot=avatar]){width:24px;height:24px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:12px;background:var(--nk-bg-callout)}.nk-ai-msg.user .mini-avatar,.nk-ai-msg.user ::slotted(.mini-avatar),.nk-ai-msg.user ::slotted([slot=avatar]){background:linear-gradient(135deg,var(--nk-decor-purple),var(--nk-decor-blue));color:var(--nk-on-accent);font-size:9.5px;font-weight:600}.nk-ai-msg .a-body,.nk-ai-msg ::slotted(.a-body){font-size:14px;line-height:1.55;padding-top:2px}.nk-ai-msg .a-name,.nk-ai-msg ::slotted(.a-name){font-size:12px;font-weight:600;margin-bottom:2px}.nk-ai-msg .a-name span{color:var(--nk-text-tertiary);font-weight:400}.nk-ai-actions{display:flex;gap:4px;margin-top:6px}.nk-ai-actions button{font-family:inherit;font-size:11.5px;color:var(--nk-text-tertiary);background:0 0;border:1px solid var(--nk-border);border-radius:5px;padding:2px 8px;cursor:pointer}.nk-ai-actions ::slotted(button){font-family:inherit;font-size:11.5px;color:var(--nk-text-tertiary);background:0 0;border:1px solid var(--nk-border);border-radius:5px;padding:2px 8px!important;cursor:pointer}.nk-ai-actions ::slotted(button:hover),.nk-ai-actions button:hover{background:var(--nk-bg-hover);color:var(--nk-text)}.nk-ai-input-row{display:flex;align-items:center;gap:8px;margin-top:14px;border:1px solid var(--nk-border-strong);border-radius:10px;padding:8px 12px}.nk-ai-input-row:focus-within{border-color:var(--nk-accent)}.nk-ai-input-row input{flex:1;border:none;outline:0;background:0 0;font-family:inherit;font-size:14px;color:var(--nk-text);min-width:0}.nk-ai-input-row input::placeholder{color:var(--nk-text-tertiary)}.nk-ai-send{border:none;background:var(--nk-accent);color:var(--nk-on-accent);border-radius:6px;width:26px;height:26px;cursor:pointer;font-size:13px;flex-shrink:0}.nk-block-host{position:relative;border-radius:var(--nk-radius);margin:8px 0;padding:2px 0 2px 2px;transition:background .15s,box-shadow .15s}.nk-block-host:hover{background:var(--nk-bg-hover)}.nk-block-host:focus-within{background:0 0;box-shadow:0 0 0 2px color-mix(in srgb,var(--nk-accent) 25%,transparent)}.nk-block-host.nk-drop-target{box-shadow:0 -2px 0 0 var(--nk-accent);background:0 0}.nk-block-host .nk-block-handle,.nk-block-host ::slotted(.nk-block-handle){position:absolute;left:-26px;top:4px;width:18px;height:20px;border-radius:4px;display:flex;align-items:center;justify-content:center;color:var(--nk-text-tertiary);font-size:13px;cursor:grab;opacity:0;transition:opacity .15s;user-select:none}.nk-block-host:hover .nk-block-handle{opacity:1}.nk-block-host .nk-block-actions{position:absolute;display:flex;gap:1px;z-index:2;opacity:0;transition:opacity .15s;pointer-events:none}.nk-block-host .nk-block-actions.show{opacity:1;pointer-events:auto}.nk-block-host .nk-block-actions ::slotted(button),.nk-block-host .nk-block-actions button{width:20px;height:22px;padding:0;border:none;border-radius:4px;background:0 0;color:var(--nk-text-tertiary);font-family:inherit;font-size:14px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center}.nk-block-host .nk-block-actions button:hover{background:var(--nk-bg-hover);color:var(--nk-text)}.nk-block-host .nk-block-actions .drag{cursor:grab}.nk-block-host .nk-block-actions .drag:active{cursor:grabbing}.nk-block-host .ProseMirror{outline:0;font-family:var(--nk-font);font-size:14px;line-height:1.6;color:var(--nk-text);min-height:60px;padding:4px 2px}.nk-block-host .ProseMirror>*+*{margin-top:6px}.nk-block-host .ProseMirror h1{font-size:30px;font-weight:600;letter-spacing:-.01em;margin:24px 0 6px}.nk-block-host .ProseMirror h2{font-size:22px;font-weight:600;margin:20px 0 6px}.nk-block-host .ProseMirror h3{font-size:17px;font-weight:600;margin:16px 0 4px}.nk-block-host .ProseMirror p{margin:0}.nk-block-host .ProseMirror ol,.nk-block-host .ProseMirror ul{padding-left:24px;margin:4px 0}.nk-block-host .ProseMirror ul{list-style:disc}.nk-block-host .ProseMirror ol{list-style:decimal}.nk-block-host .ProseMirror li{margin:2px 0}.nk-block-host .ProseMirror li::marker{color:var(--nk-text-tertiary)}.nk-block-host .ProseMirror ul[data-type=taskList]{list-style:none;padding-left:2px}.nk-block-host .ProseMirror ul[data-type=taskList] li{display:flex;gap:8px;align-items:flex-start}.nk-block-host .ProseMirror ul[data-type=taskList] li>label{flex-shrink:0;margin-top:3px;user-select:none}.nk-block-host .ProseMirror ul[data-type=taskList] li>div{flex:1;min-width:0}.nk-block-host .ProseMirror ul[data-type=taskList] input[type=checkbox]{appearance:none;width:16px;height:16px;margin:0;border:1.4px solid var(--nk-border-strong);border-radius:2px;cursor:pointer;position:relative;background:var(--nk-bg);display:block}.nk-block-host .ProseMirror ul[data-type=taskList] input[type=checkbox]:checked{background:var(--nk-accent);border-color:var(--nk-accent)}.nk-block-host .ProseMirror ul[data-type=taskList] input[type=checkbox]:checked::after{content:\"✓\";color:var(--nk-on-accent);font-size:11px;position:absolute;inset:0;display:flex;align-items:center;justify-content:center}.nk-block-host .ProseMirror li[data-checked=true]>div{text-decoration:line-through;color:var(--nk-text-tertiary)}.nk-block-host .ProseMirror-gapcursor::after{border-top-color:var(--nk-text)}.nk-block-host .ProseMirror .ProseMirror-selectednode{background:color-mix(in srgb,var(--nk-accent) 8%,transparent)}.nk-block-host .ProseMirror blockquote{border-left:3px solid var(--nk-text);padding:2px 0 2px 14px;margin:12px 0;color:var(--nk-text-secondary)}.nk-block-host .ProseMirror code{font-family:var(--nk-font-mono);font-size:85%;color:var(--nk-danger);background:color-mix(in srgb,var(--nk-danger) 8%,transparent);border-radius:4px;padding:1px 5px}.nk-block-host .ProseMirror pre{background:var(--nk-bg-code);border:1px solid var(--nk-border);border-radius:var(--nk-radius);padding:16px 20px;margin:12px 0;font-family:var(--nk-font-mono);font-size:13px;line-height:1.6;color:var(--nk-text-secondary);overflow-x:auto}.nk-block-host .ProseMirror pre code{background:0 0;color:inherit;padding:0;font-size:inherit}.nk-block-host .ProseMirror hr{border:none;border-top:1px solid var(--nk-border);margin:20px 0}.nk-block-host .ProseMirror a{color:var(--nk-accent);text-decoration:underline;text-underline-offset:2px}.nk-block-host .ProseMirror-selectednode{box-shadow:0 0 0 2px color-mix(in srgb,var(--nk-accent) 35%,transparent);border-radius:3px}.nk-block-host .ProseMirror ::selection{background:color-mix(in srgb,var(--nk-accent) 25%,transparent)}.nk-block-host .ProseMirror .is-empty::before,.nk-block-host .ProseMirror p.is-editor-empty:first-child::before{content:attr(data-placeholder);color:var(--nk-text-tertiary);float:left;height:0;pointer-events:none}.nk-block-host .bn-container{font-family:var(--nk-font);color:var(--nk-text);background:0 0}.nk-block-host .bn-editor{padding:4px 2px}.nk-block-host .bn-block-content{font-size:14px;line-height:1.6}.nk-block-host .bn-side-menu{color:var(--nk-text-tertiary)}.nk-block-host .bn-drag-handle{border-radius:4px}.nk-block-host .bn-drag-handle:hover{background:var(--nk-bg-hover)}.nk-block-host .novel-editor{font-family:var(--nk-font);color:var(--nk-text)}.nk-block-host .novel-prose{max-width:none}.nk-block-host .bn-suggestion-menu,.nk-block-host .tippy-box,.nk-slash-menu{background:var(--nk-bg);border-radius:10px;box-shadow:var(--nk-shadow-menu);padding:6px;width:280px;max-height:320px;overflow-y:auto;font-family:var(--nk-font);font-size:13.5px;color:var(--nk-text)}.nk-slash-menu-label{font-size:11px;color:var(--nk-text-tertiary);padding:6px 9px 2px}.nk-slash-item{display:flex;align-items:center;gap:10px;padding:6px 9px;border-radius:5px;cursor:pointer;user-select:none}.nk-slash-item.selected,.nk-slash-item:hover{background:var(--nk-bg-hover)}.nk-slash-item .m-icon,.nk-slash-item ::slotted(.m-icon){width:28px;height:28px;flex-shrink:0;border-radius:4px;background:var(--nk-bg-callout);border:1px solid var(--nk-border);display:flex;align-items:center;justify-content:center;font-size:15px}.nk-slash-item .m-desc{font-size:11.5px;color:var(--nk-text-tertiary)}.nk-bubble-menu{display:flex;align-items:center;gap:2px;background:var(--nk-bg);border-radius:8px;box-shadow:var(--nk-shadow-menu);padding:4px}.nk-bubble-menu button{font-family:inherit;font-size:13px;border:none;background:0 0;color:var(--nk-text-secondary);padding:4px 8px;border-radius:5px;cursor:pointer}.nk-bubble-menu ::slotted(button){font-family:inherit;font-size:13px;border:none;background:0 0;color:var(--nk-text-secondary);padding:4px 8px!important;border-radius:5px;cursor:pointer}.nk-bubble-menu ::slotted(button:hover),.nk-bubble-menu button:hover{background:var(--nk-bg-hover);color:var(--nk-text)}.nk-bubble-menu ::slotted(button.active),.nk-bubble-menu button.active{color:var(--nk-accent)}@media (max-width:860px){.nk-sidebar{display:none}.nk-page{padding:0 24px 80px}.nk-settings-nav{width:60px;min-width:60px}.nk-settings-nav .label,.nk-settings-nav .nk-section-label,.nk-settings-user .u-text{display:none}.nk-settings-content{padding:24px}.nk-cmdk-backdrop{padding-top:6vh}.nk-cmdk{width:min(560px,96vw)}.nk-stats{flex-wrap:wrap}}@media (prefers-reduced-motion:reduce){.nk-skeleton{animation:none;background:var(--nk-bg-callout)}[class*=nk-],[class*=nk-]::after,[class*=nk-]::before{animation-duration:NaNs!important;animation-iteration-count:1!important;transition-duration:NaNs!important;scroll-behavior:auto!important}}\n/*# sourceMappingURL=notionkit.min.css.map */";
|
|
13
|
+
|
|
14
|
+
const componentsCss = componentsHead + componentsTail;
|
|
15
|
+
const css = componentsHead + tokensCss + componentsTail;
|
|
16
|
+
|
|
17
|
+
const nkSheet = new CSSStyleSheet();
|
|
18
|
+
nkSheet.replaceSync(css);
|
|
19
|
+
|
|
20
|
+
const tokensSheet = new CSSStyleSheet();
|
|
21
|
+
tokensSheet.replaceSync(tokensCss);
|
|
22
|
+
|
|
23
|
+
const componentsSheet = new CSSStyleSheet();
|
|
24
|
+
componentsSheet.replaceSync(componentsCss);
|
|
25
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// NotionKit Elements – base classes
|
|
28
|
+
//
|
|
29
|
+
// Every <nk-*> element is a thin shell around the markup notionkit.css already
|
|
30
|
+
// styles. The shadow root adopts the *components* half of that stylesheet and
|
|
31
|
+
// nothing else; the element ships no visual CSS of its own.
|
|
32
|
+
// ============================================================
|
|
33
|
+
|
|
34
|
+
// ── Design tokens ──
|
|
35
|
+
// The shadow roots deliberately adopt componentsSheet only. The full sheet
|
|
36
|
+
// would bring the :root / [data-theme] token blocks along, and those match the
|
|
37
|
+
// .nk-wrapper below — every --nk-* token would then be re-declared inside each
|
|
38
|
+
// shadow root, where a matching rule always beats an inherited value. A page's
|
|
39
|
+
// own `:root { --nk-accent: … }` would never arrive.
|
|
40
|
+
//
|
|
41
|
+
// So the token defaults go on the document once, and every shadow root
|
|
42
|
+
// inherits them like any other custom property. They sit in a cascade layer so
|
|
43
|
+
// an ordinary (unlayered) brand stylesheet — and notionkit.css itself, if the
|
|
44
|
+
// page loads it — wins over them, no matter the load order.
|
|
45
|
+
|
|
46
|
+
const TOKENS_INJECTED = '__nkDefaultTokensInjected';
|
|
47
|
+
|
|
48
|
+
function injectDefaultTokens() {
|
|
49
|
+
if (typeof document === 'undefined') return; // SSR / non-DOM
|
|
50
|
+
if (globalThis[TOKENS_INJECTED]) return; // another bundle copy did it
|
|
51
|
+
globalThis[TOKENS_INJECTED] = true;
|
|
52
|
+
|
|
53
|
+
const sheet = new CSSStyleSheet();
|
|
54
|
+
sheet.replaceSync(`@layer notionkit-defaults { ${tokensCss} }`);
|
|
55
|
+
// Append — never assign — so an app's own adopted sheets survive.
|
|
56
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
injectDefaultTokens();
|
|
60
|
+
|
|
61
|
+
// ── Global theme sync ──
|
|
62
|
+
// data-theme on <html> is the only source of truth. One MutationObserver
|
|
63
|
+
// mirrors it onto every registered instance; no element sets a theme itself.
|
|
64
|
+
|
|
65
|
+
const instances = new Set();
|
|
66
|
+
|
|
67
|
+
function getCurrentTheme() {
|
|
68
|
+
return document.documentElement.getAttribute('data-theme') || 'light';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function syncAllThemes() {
|
|
72
|
+
const theme = getCurrentTheme();
|
|
73
|
+
for (const instance of instances) instance._syncTheme(theme);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (typeof window !== 'undefined' && typeof MutationObserver !== 'undefined') {
|
|
77
|
+
new MutationObserver(syncAllThemes).observe(document.documentElement, {
|
|
78
|
+
attributes: true,
|
|
79
|
+
attributeFilter: ['data-theme']
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Host stylesheets ──
|
|
84
|
+
// Layout plumbing only: how the host box participates in the outer layout, and
|
|
85
|
+
// that `hidden` keeps working. :host([hidden]) has a higher specificity than
|
|
86
|
+
// :host, so it wins regardless of order. The wrapper is layout-transparent.
|
|
87
|
+
|
|
88
|
+
const hostSheets = new Map();
|
|
89
|
+
|
|
90
|
+
function hostSheetFor(display) {
|
|
91
|
+
let sheet = hostSheets.get(display);
|
|
92
|
+
if (!sheet) {
|
|
93
|
+
sheet = new CSSStyleSheet();
|
|
94
|
+
sheet.replaceSync(`
|
|
95
|
+
:host { display: ${display}; }
|
|
96
|
+
:host([hidden]) { display: none; }
|
|
97
|
+
.nk-wrapper { display: contents; }
|
|
98
|
+
`);
|
|
99
|
+
hostSheets.set(display, sheet);
|
|
100
|
+
}
|
|
101
|
+
return sheet;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Base class ──
|
|
105
|
+
|
|
106
|
+
class NkElement extends HTMLElement {
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* How the host takes part in the outer layout. Default 'contents': the host
|
|
110
|
+
* generates no box, so the inner .nk-* element sits in the parent's layout
|
|
111
|
+
* exactly where the class markup would – a button stays inline, a sidebar
|
|
112
|
+
* is a direct flex child of the app, a tree row and its children box are
|
|
113
|
+
* siblings. That is what makes the pixel parity with the class markup hold.
|
|
114
|
+
* Consequence: style the parent or the tokens, not the host; `hidden` on
|
|
115
|
+
* the host still works. Override with 'block' / 'inline-block' only when a
|
|
116
|
+
* host box is genuinely needed.
|
|
117
|
+
*/
|
|
118
|
+
static get display() { return 'contents'; }
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Optional CSSStyleSheet with component-specific host rules, adopted after
|
|
122
|
+
* the shared sheets. Positioning and sizing of the host only — never colour,
|
|
123
|
+
* spacing or typography; those belong to notionkit.css.
|
|
124
|
+
*/
|
|
125
|
+
static get hostStyles() { return null; }
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Opt in when the component *copies* light-DOM children into its shadow tree
|
|
129
|
+
* (options, breadcrumb crumbs). A MutationObserver then calls
|
|
130
|
+
* projectLightDom() again whenever those children change, so a framework
|
|
131
|
+
* that swaps them keeps the rendered element in step. Components that merely
|
|
132
|
+
* *slot* their children use `slotchange` instead.
|
|
133
|
+
*/
|
|
134
|
+
static get observesLightDom() { return false; }
|
|
135
|
+
|
|
136
|
+
/** What the light-DOM observer watches. Drop `subtree` for shallow copies. */
|
|
137
|
+
static get lightDomObserverInit() {
|
|
138
|
+
return { childList: true, subtree: true, characterData: true, attributes: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 'named' (default) or 'manual'. Manual assignment lets a component route
|
|
143
|
+
* light-DOM nodes itself – text nodes into a label slot, nested elements
|
|
144
|
+
* into a children slot – so <nk-tree-item icon="🏠">Home</nk-tree-item>
|
|
145
|
+
* reads naturally. With 'manual' the component calls assignSlots() on
|
|
146
|
+
* every childList change.
|
|
147
|
+
*/
|
|
148
|
+
static get slotAssignment() { return 'named'; }
|
|
149
|
+
|
|
150
|
+
static get observedAttributes() { return []; }
|
|
151
|
+
|
|
152
|
+
constructor() {
|
|
153
|
+
super();
|
|
154
|
+
this._initialized = false;
|
|
155
|
+
this._shadow = this.attachShadow({ mode: 'open', slotAssignment: this.constructor.slotAssignment });
|
|
156
|
+
const sheets = [componentsSheet, hostSheetFor(this.constructor.display)];
|
|
157
|
+
const extra = this.constructor.hostStyles;
|
|
158
|
+
if (extra) sheets.push(extra);
|
|
159
|
+
this._shadow.adoptedStyleSheets = sheets;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
connectedCallback() {
|
|
163
|
+
if (!this._initialized) {
|
|
164
|
+
this._initialized = true;
|
|
165
|
+
|
|
166
|
+
// The wrapper mirrors data-theme from <html> so [data-theme]-keyed
|
|
167
|
+
// component rules apply inside the root. Token *values* need no
|
|
168
|
+
// mirroring — they inherit.
|
|
169
|
+
this._wrapper = document.createElement('div');
|
|
170
|
+
this._wrapper.className = 'nk-wrapper';
|
|
171
|
+
this._wrapper.setAttribute('data-theme', getCurrentTheme());
|
|
172
|
+
this._shadow.appendChild(this._wrapper);
|
|
173
|
+
|
|
174
|
+
this.render();
|
|
175
|
+
} else {
|
|
176
|
+
this._wrapper.setAttribute('data-theme', getCurrentTheme());
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Everything below runs on every connect, not just the first. Moving an
|
|
180
|
+
// element in the DOM disconnects and reconnects it, and disconnectedCallback
|
|
181
|
+
// tears all of this down — without re-arming it here a moved element would
|
|
182
|
+
// keep its markup but silently stop reacting.
|
|
183
|
+
this.setupEvents();
|
|
184
|
+
instances.add(this);
|
|
185
|
+
|
|
186
|
+
if (this.constructor.observesLightDom) {
|
|
187
|
+
this._lightDomObserver ??= new MutationObserver(records => this.projectLightDom(records));
|
|
188
|
+
this._lightDomObserver.observe(this, this.constructor.lightDomObserverInit);
|
|
189
|
+
}
|
|
190
|
+
if (this.constructor.slotAssignment === 'manual') {
|
|
191
|
+
this.assignSlots();
|
|
192
|
+
this._slotObserver ??= new MutationObserver(() => this.assignSlots());
|
|
193
|
+
this._slotObserver.observe(this, { childList: true });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
disconnectedCallback() {
|
|
198
|
+
instances.delete(this);
|
|
199
|
+
this._lightDomObserver?.disconnect();
|
|
200
|
+
this._slotObserver?.disconnect();
|
|
201
|
+
this.teardownEvents();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
205
|
+
if (!this._initialized) return;
|
|
206
|
+
if (oldValue === newValue) return;
|
|
207
|
+
this.onAttributeChanged(name, oldValue, newValue);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
_syncTheme(theme) {
|
|
211
|
+
this._wrapper?.setAttribute('data-theme', theme);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Subclasses override to build the inner DOM inside this._wrapper. Runs once. */
|
|
215
|
+
render() {}
|
|
216
|
+
|
|
217
|
+
/** Subclasses override to attach event listeners. Runs on every connect. */
|
|
218
|
+
setupEvents() {}
|
|
219
|
+
|
|
220
|
+
/** Subclasses override to remove event listeners. Runs on every disconnect. */
|
|
221
|
+
teardownEvents() {}
|
|
222
|
+
|
|
223
|
+
/** Subclasses override to react to attribute changes after render(). */
|
|
224
|
+
onAttributeChanged(name, oldValue, newValue) {}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Subclasses that set observesLightDom override this to (re-)copy their
|
|
228
|
+
* light-DOM children into the shadow tree. Runs on every change to those
|
|
229
|
+
* children, so it has to be safe to call repeatedly.
|
|
230
|
+
*/
|
|
231
|
+
projectLightDom() {}
|
|
232
|
+
|
|
233
|
+
/** Components with manual slot assignment override this. */
|
|
234
|
+
assignSlots() {}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Escape hatch: re-copy the light-DOM children now. The observer covers the
|
|
238
|
+
* ordinary cases; this is for the ones it cannot see, so nobody has to reach
|
|
239
|
+
* into element.shadowRoot.
|
|
240
|
+
*/
|
|
241
|
+
refresh() { this.projectLightDom(); if (this.constructor.slotAssignment === 'manual') this.assignSlots(); }
|
|
242
|
+
|
|
243
|
+
// ── Utilities ──
|
|
244
|
+
|
|
245
|
+
getBoolAttr(name) {
|
|
246
|
+
return this.hasAttribute(name);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
setBoolAttr(name, value) {
|
|
250
|
+
if (value) this.setAttribute(name, '');
|
|
251
|
+
else this.removeAttribute(name);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
createElement(tag, classes = [], attrs = {}) {
|
|
255
|
+
const el = document.createElement(tag);
|
|
256
|
+
if (classes.length) el.classList.add(...classes);
|
|
257
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
258
|
+
if (val !== null && val !== undefined && val !== false) el.setAttribute(key, val === true ? '' : val);
|
|
259
|
+
}
|
|
260
|
+
return el;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Dispatches a composed, bubbling CustomEvent — the nk-* event contract. */
|
|
264
|
+
emit(eventName, detail = null) {
|
|
265
|
+
return this.dispatchEvent(new CustomEvent(eventName, {
|
|
266
|
+
bubbles: true,
|
|
267
|
+
composed: true,
|
|
268
|
+
cancelable: true,
|
|
269
|
+
detail
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Form-associated base class ──
|
|
275
|
+
// ElementInternals gives the element a place in the surrounding <form>:
|
|
276
|
+
// FormData, reset, validation and :disabled through <fieldset>.
|
|
277
|
+
|
|
278
|
+
class NkFormElement extends NkElement {
|
|
279
|
+
|
|
280
|
+
static formAssociated = true;
|
|
281
|
+
|
|
282
|
+
constructor() {
|
|
283
|
+
super();
|
|
284
|
+
this._internals = this.attachInternals();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
get form() { return this._internals.form; }
|
|
288
|
+
get validationMessage() { return this._internals.validationMessage; }
|
|
289
|
+
get validity() { return this._internals.validity; }
|
|
290
|
+
get willValidate() { return this._internals.willValidate; }
|
|
291
|
+
|
|
292
|
+
checkValidity() { return this._internals.checkValidity(); }
|
|
293
|
+
reportValidity() { return this._internals.reportValidity(); }
|
|
294
|
+
|
|
295
|
+
formResetCallback() { this.resetValue(); }
|
|
296
|
+
formStateRestoreCallback(state, mode) { this.restoreValue(state, mode); }
|
|
297
|
+
formDisabledCallback(disabled) { this.onFormDisabled(disabled); }
|
|
298
|
+
|
|
299
|
+
/** Subclasses override. */
|
|
300
|
+
resetValue() {}
|
|
301
|
+
restoreValue(state, mode) {}
|
|
302
|
+
onFormDisabled(disabled) {}
|
|
303
|
+
|
|
304
|
+
setFormValue(value, state) {
|
|
305
|
+
this._internals.setFormValue(value, state);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
setValidity(flags, message, anchor) {
|
|
309
|
+
this._internals.setValidity(flags, message, anchor);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Mirrors the native control's validity onto the host, so required works. */
|
|
313
|
+
syncValidityFrom(control) {
|
|
314
|
+
if (!control) return;
|
|
315
|
+
if (control.validity.valid) this._internals.setValidity({});
|
|
316
|
+
else this._internals.setValidity(control.validity, control.validationMessage, control);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// <nk-btn variant="primary" small>Save</nk-btn> → <button class="nk-btn primary small">
|
|
321
|
+
// <nk-btn href="/docs" variant="secondary">Docs</nk-btn> → <a class="nk-btn secondary">
|
|
322
|
+
// <nk-btn variant="topbar">⭐</nk-btn> → <button class="nk-topbar-btn">
|
|
323
|
+
//
|
|
324
|
+
// Modifier classes become attributes. A slotted <svg> is sized by the
|
|
325
|
+
// `.nk-btn ::slotted(svg)` twin; pass the icon itself, never wrapped.
|
|
326
|
+
const VARIANTS$1 = ['primary', 'secondary', 'danger', 'danger-solid', 'topbar', 'share'];
|
|
327
|
+
|
|
328
|
+
class NkBtn extends NkElement {
|
|
329
|
+
static get observedAttributes() { return ['variant', 'small', 'disabled', 'type', 'href', 'title', 'aria-label']; }
|
|
330
|
+
|
|
331
|
+
render() {
|
|
332
|
+
this._build();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
_build() {
|
|
336
|
+
const href = this.getAttribute('href');
|
|
337
|
+
const el = href ? this.createElement('a', [], { href }) : this.createElement('button', [], { type: this.getAttribute('type') || 'button' });
|
|
338
|
+
el.className = this._computeClasses().join(' ');
|
|
339
|
+
el.appendChild(document.createElement('slot'));
|
|
340
|
+
this._applyState(el);
|
|
341
|
+
if (this._btn) {
|
|
342
|
+
this._btn.removeEventListener('click', this._onClick);
|
|
343
|
+
this._btn.replaceWith(el);
|
|
344
|
+
} else {
|
|
345
|
+
this._wrapper.appendChild(el);
|
|
346
|
+
}
|
|
347
|
+
this._btn = el;
|
|
348
|
+
if (this._onClick) el.addEventListener('click', this._onClick);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
_applyState(el) {
|
|
352
|
+
const disabled = this.getBoolAttr('disabled');
|
|
353
|
+
if (el.tagName === 'BUTTON') el.disabled = disabled;
|
|
354
|
+
else el.setAttribute('aria-disabled', disabled ? 'true' : 'false');
|
|
355
|
+
const title = this.getAttribute('title');
|
|
356
|
+
title ? el.setAttribute('title', title) : el.removeAttribute('title');
|
|
357
|
+
const label = this.getAttribute('aria-label');
|
|
358
|
+
label ? el.setAttribute('aria-label', label) : el.removeAttribute('aria-label');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
setupEvents() {
|
|
362
|
+
this._onClick = (e) => {
|
|
363
|
+
if (this.getBoolAttr('disabled')) { e.preventDefault(); e.stopPropagation(); return; }
|
|
364
|
+
// A button inside a shadow root is not a submit button of the outer
|
|
365
|
+
// form; forward the intent to the form the host sits in.
|
|
366
|
+
const type = this.getAttribute('type');
|
|
367
|
+
if (type === 'submit' || type === 'reset') {
|
|
368
|
+
const form = this.closest('form');
|
|
369
|
+
if (form) type === 'submit' ? form.requestSubmit() : form.reset();
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
this._btn.addEventListener('click', this._onClick);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
teardownEvents() {
|
|
376
|
+
this._btn?.removeEventListener('click', this._onClick);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
onAttributeChanged(name) {
|
|
380
|
+
if (!this._btn) return;
|
|
381
|
+
switch (name) {
|
|
382
|
+
case 'href':
|
|
383
|
+
this._build();
|
|
384
|
+
break;
|
|
385
|
+
case 'variant':
|
|
386
|
+
case 'small':
|
|
387
|
+
this._btn.className = this._computeClasses().join(' ');
|
|
388
|
+
break;
|
|
389
|
+
case 'type':
|
|
390
|
+
if (this._btn.tagName === 'BUTTON') this._btn.setAttribute('type', this.getAttribute('type') || 'button');
|
|
391
|
+
break;
|
|
392
|
+
default:
|
|
393
|
+
this._applyState(this._btn);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
_computeClasses() {
|
|
398
|
+
const variant = this.getAttribute('variant');
|
|
399
|
+
if (variant === 'topbar') return ['nk-topbar-btn'];
|
|
400
|
+
if (variant === 'share') return ['nk-topbar-btn', 'nk-share-btn'];
|
|
401
|
+
const classes = ['nk-btn'];
|
|
402
|
+
if (VARIANTS$1.includes(variant)) classes.push(variant);
|
|
403
|
+
if (this.getBoolAttr('small')) classes.push('small');
|
|
404
|
+
return classes;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
focus(options) { this._btn?.focus(options); }
|
|
408
|
+
click() { this._btn?.click(); }
|
|
409
|
+
|
|
410
|
+
get variant() { return this.getAttribute('variant'); }
|
|
411
|
+
set variant(v) { v ? this.setAttribute('variant', v) : this.removeAttribute('variant'); }
|
|
412
|
+
get small() { return this.getBoolAttr('small'); }
|
|
413
|
+
set small(v) { this.setBoolAttr('small', v); }
|
|
414
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
415
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
416
|
+
get type() { return this.getAttribute('type') || 'button'; }
|
|
417
|
+
set type(v) { this.setAttribute('type', v); }
|
|
418
|
+
get href() { return this.getAttribute('href'); }
|
|
419
|
+
set href(v) { v ? this.setAttribute('href', v) : this.removeAttribute('href'); }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
customElements.define('nk-btn', NkBtn);
|
|
423
|
+
|
|
424
|
+
// <nk-input name="title" value="…" placeholder="…" wide></nk-input> → <input class="nk-input wide">
|
|
425
|
+
// Takes part in the surrounding <form> through ElementInternals.
|
|
426
|
+
const FORWARDED$2 = ['type', 'placeholder', 'name', 'min', 'max', 'step', 'autocomplete', 'pattern', 'maxlength', 'minlength', 'inputmode', 'list', 'aria-label'];
|
|
427
|
+
const BOOLEANS$1 = ['required', 'disabled', 'readonly'];
|
|
428
|
+
|
|
429
|
+
class NkInput extends NkFormElement {
|
|
430
|
+
static get observedAttributes() { return ['value', 'wide', ...FORWARDED$2, ...BOOLEANS$1]; }
|
|
431
|
+
|
|
432
|
+
render() {
|
|
433
|
+
this._input = this.createElement('input', ['nk-input']);
|
|
434
|
+
for (const attr of FORWARDED$2) this._forward(attr);
|
|
435
|
+
for (const attr of BOOLEANS$1) this._forward(attr);
|
|
436
|
+
this._input.value = this.getAttribute('value') ?? '';
|
|
437
|
+
this._defaultValue = this._input.value;
|
|
438
|
+
this._syncWide();
|
|
439
|
+
this._wrapper.appendChild(this._input);
|
|
440
|
+
this._syncFormValue();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
_forward(attr) {
|
|
444
|
+
if (BOOLEANS$1.includes(attr)) {
|
|
445
|
+
this._input[attr === 'readonly' ? 'readOnly' : attr] = this.getBoolAttr(attr);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const v = this.getAttribute(attr);
|
|
449
|
+
if (v === null) this._input.removeAttribute(attr);
|
|
450
|
+
else this._input.setAttribute(attr, v);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
_syncWide() {
|
|
454
|
+
this._input.classList.toggle('wide', this.getBoolAttr('wide'));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
setupEvents() {
|
|
458
|
+
this._onInput = () => {
|
|
459
|
+
this._syncFormValue();
|
|
460
|
+
this.emit('nk-input', { value: this._input.value, name: this.name });
|
|
461
|
+
};
|
|
462
|
+
this._onChange = () => {
|
|
463
|
+
this._syncFormValue();
|
|
464
|
+
this.emit('nk-change', { value: this._input.value, name: this.name });
|
|
465
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
466
|
+
};
|
|
467
|
+
this._input.addEventListener('input', this._onInput);
|
|
468
|
+
this._input.addEventListener('change', this._onChange);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
teardownEvents() {
|
|
472
|
+
this._input?.removeEventListener('input', this._onInput);
|
|
473
|
+
this._input?.removeEventListener('change', this._onChange);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
onAttributeChanged(name) {
|
|
477
|
+
if (!this._input) return;
|
|
478
|
+
if (name === 'value') { this._input.value = this.getAttribute('value') ?? ''; this._syncFormValue(); return; }
|
|
479
|
+
if (name === 'wide') { this._syncWide(); return; }
|
|
480
|
+
this._forward(name);
|
|
481
|
+
this._syncFormValue();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
_syncFormValue() {
|
|
485
|
+
this.setFormValue(this._input.value);
|
|
486
|
+
this.syncValidityFrom(this._input);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
resetValue() {
|
|
490
|
+
this._input.value = this.getAttribute('value') ?? '';
|
|
491
|
+
this._syncFormValue();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
restoreValue(state) {
|
|
495
|
+
if (typeof state === 'string') { this._input.value = state; this._syncFormValue(); }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
onFormDisabled(disabled) {
|
|
499
|
+
this._input.disabled = disabled || this.getBoolAttr('disabled');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
focus(options) { this._input?.focus(options); }
|
|
503
|
+
blur() { this._input?.blur(); }
|
|
504
|
+
select() { this._input?.select(); }
|
|
505
|
+
|
|
506
|
+
get value() { return this._input?.value ?? (this.getAttribute('value') ?? ''); }
|
|
507
|
+
set value(v) {
|
|
508
|
+
if (this._input) { this._input.value = v ?? ''; this._syncFormValue(); }
|
|
509
|
+
else this.setAttribute('value', v ?? '');
|
|
510
|
+
}
|
|
511
|
+
get name() { return this.getAttribute('name'); }
|
|
512
|
+
set name(v) { this.setAttribute('name', v); }
|
|
513
|
+
get type() { return this.getAttribute('type') || 'text'; }
|
|
514
|
+
set type(v) { this.setAttribute('type', v); }
|
|
515
|
+
get placeholder() { return this.getAttribute('placeholder'); }
|
|
516
|
+
set placeholder(v) { this.setAttribute('placeholder', v); }
|
|
517
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
518
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
519
|
+
get required() { return this.getBoolAttr('required'); }
|
|
520
|
+
set required(v) { this.setBoolAttr('required', v); }
|
|
521
|
+
get readonly() { return this.getBoolAttr('readonly'); }
|
|
522
|
+
set readonly(v) { this.setBoolAttr('readonly', v); }
|
|
523
|
+
get wide() { return this.getBoolAttr('wide'); }
|
|
524
|
+
set wide(v) { this.setBoolAttr('wide', v); }
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
customElements.define('nk-input', NkInput);
|
|
528
|
+
|
|
529
|
+
// <nk-textarea name="bio" rows="3" wide>Text</nk-textarea> → <textarea class="nk-textarea wide">
|
|
530
|
+
// The initial value is the `value` attribute, or the element's text content.
|
|
531
|
+
const FORWARDED$1 = ['placeholder', 'name', 'rows', 'maxlength', 'minlength', 'autocomplete', 'aria-label'];
|
|
532
|
+
const BOOLEANS = ['required', 'disabled', 'readonly'];
|
|
533
|
+
|
|
534
|
+
class NkTextarea extends NkFormElement {
|
|
535
|
+
static get observedAttributes() { return ['value', 'wide', ...FORWARDED$1, ...BOOLEANS]; }
|
|
536
|
+
|
|
537
|
+
render() {
|
|
538
|
+
this._input = this.createElement('textarea', ['nk-textarea']);
|
|
539
|
+
for (const attr of [...FORWARDED$1, ...BOOLEANS]) this._forward(attr);
|
|
540
|
+
this._input.value = this.getAttribute('value') ?? this.textContent.trim();
|
|
541
|
+
this._syncWide();
|
|
542
|
+
this._wrapper.appendChild(this._input);
|
|
543
|
+
this._syncFormValue();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
_forward(attr) {
|
|
547
|
+
if (BOOLEANS.includes(attr)) {
|
|
548
|
+
this._input[attr === 'readonly' ? 'readOnly' : attr] = this.getBoolAttr(attr);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const v = this.getAttribute(attr);
|
|
552
|
+
if (v === null) this._input.removeAttribute(attr);
|
|
553
|
+
else this._input.setAttribute(attr, v);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
_syncWide() { this._input.classList.toggle('wide', this.getBoolAttr('wide')); }
|
|
557
|
+
|
|
558
|
+
setupEvents() {
|
|
559
|
+
this._onInput = () => {
|
|
560
|
+
this._syncFormValue();
|
|
561
|
+
this.emit('nk-input', { value: this._input.value, name: this.name });
|
|
562
|
+
};
|
|
563
|
+
this._onChange = () => {
|
|
564
|
+
this._syncFormValue();
|
|
565
|
+
this.emit('nk-change', { value: this._input.value, name: this.name });
|
|
566
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
567
|
+
};
|
|
568
|
+
this._input.addEventListener('input', this._onInput);
|
|
569
|
+
this._input.addEventListener('change', this._onChange);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
teardownEvents() {
|
|
573
|
+
this._input?.removeEventListener('input', this._onInput);
|
|
574
|
+
this._input?.removeEventListener('change', this._onChange);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
onAttributeChanged(name) {
|
|
578
|
+
if (!this._input) return;
|
|
579
|
+
if (name === 'value') { this._input.value = this.getAttribute('value') ?? ''; this._syncFormValue(); return; }
|
|
580
|
+
if (name === 'wide') { this._syncWide(); return; }
|
|
581
|
+
this._forward(name);
|
|
582
|
+
this._syncFormValue();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
_syncFormValue() {
|
|
586
|
+
this.setFormValue(this._input.value);
|
|
587
|
+
this.syncValidityFrom(this._input);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
resetValue() {
|
|
591
|
+
this._input.value = this.getAttribute('value') ?? this.textContent.trim();
|
|
592
|
+
this._syncFormValue();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
restoreValue(state) {
|
|
596
|
+
if (typeof state === 'string') { this._input.value = state; this._syncFormValue(); }
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
onFormDisabled(disabled) { this._input.disabled = disabled || this.getBoolAttr('disabled'); }
|
|
600
|
+
|
|
601
|
+
focus(options) { this._input?.focus(options); }
|
|
602
|
+
blur() { this._input?.blur(); }
|
|
603
|
+
|
|
604
|
+
get value() { return this._input?.value ?? (this.getAttribute('value') ?? ''); }
|
|
605
|
+
set value(v) {
|
|
606
|
+
if (this._input) { this._input.value = v ?? ''; this._syncFormValue(); }
|
|
607
|
+
else this.setAttribute('value', v ?? '');
|
|
608
|
+
}
|
|
609
|
+
get name() { return this.getAttribute('name'); }
|
|
610
|
+
set name(v) { this.setAttribute('name', v); }
|
|
611
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
612
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
613
|
+
get required() { return this.getBoolAttr('required'); }
|
|
614
|
+
set required(v) { this.setBoolAttr('required', v); }
|
|
615
|
+
get wide() { return this.getBoolAttr('wide'); }
|
|
616
|
+
set wide(v) { this.setBoolAttr('wide', v); }
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
customElements.define('nk-textarea', NkTextarea);
|
|
620
|
+
|
|
621
|
+
// <nk-select name="role" value="editor">
|
|
622
|
+
// <option value="viewer">Viewer</option>
|
|
623
|
+
// <option value="editor">Editor</option>
|
|
624
|
+
// </nk-select>
|
|
625
|
+
//
|
|
626
|
+
// The light-DOM <option>s are copied into the shadow <select>. A
|
|
627
|
+
// MutationObserver keeps the copy in step when a framework swaps them; the
|
|
628
|
+
// empty string is a valid value; the live selection survives a rebuild.
|
|
629
|
+
class NkSelect extends NkFormElement {
|
|
630
|
+
static get observedAttributes() { return ['value', 'name', 'disabled', 'required', 'compact', 'aria-label']; }
|
|
631
|
+
static get observesLightDom() { return true; }
|
|
632
|
+
|
|
633
|
+
render() {
|
|
634
|
+
this._select = this.createElement('select', ['nk-select']);
|
|
635
|
+
this._forwardAll();
|
|
636
|
+
this._wrapper.appendChild(this._select);
|
|
637
|
+
this._defaultValue = this.getAttribute('value');
|
|
638
|
+
// Children may not be parsed yet when connectedCallback runs.
|
|
639
|
+
this.projectLightDom();
|
|
640
|
+
requestAnimationFrame(() => this.projectLightDom());
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
_forwardAll() {
|
|
644
|
+
const name = this.getAttribute('name');
|
|
645
|
+
name ? this._select.setAttribute('name', name) : this._select.removeAttribute('name');
|
|
646
|
+
const label = this.getAttribute('aria-label');
|
|
647
|
+
label ? this._select.setAttribute('aria-label', label) : this._select.removeAttribute('aria-label');
|
|
648
|
+
this._select.disabled = this.getBoolAttr('disabled');
|
|
649
|
+
this._select.required = this.getBoolAttr('required');
|
|
650
|
+
this._select.classList.toggle('compact', this.getBoolAttr('compact'));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
projectLightDom() {
|
|
654
|
+
if (!this._select) return;
|
|
655
|
+
// The options are pure data — the clones carry no listeners — so skipping
|
|
656
|
+
// an unchanged rebuild is safe, and it keeps an open dropdown from
|
|
657
|
+
// snapping shut on light-DOM churn elsewhere.
|
|
658
|
+
const nodes = [...this.children].filter(n => n.matches('option, optgroup'));
|
|
659
|
+
const signature = nodes.map(n => n.outerHTML).join('');
|
|
660
|
+
if (signature === this._optionSignature) return;
|
|
661
|
+
this._optionSignature = signature;
|
|
662
|
+
|
|
663
|
+
// innerHTML = '' drops the selection; remember it first. selectedIndex -1
|
|
664
|
+
// means "nothing selected", which is not an option whose value is "".
|
|
665
|
+
const previous = this._select.selectedIndex >= 0 ? this._select.value : null;
|
|
666
|
+
|
|
667
|
+
this._select.innerHTML = '';
|
|
668
|
+
for (const node of nodes) this._select.appendChild(node.cloneNode(true));
|
|
669
|
+
|
|
670
|
+
// Keep the live selection when it survived; otherwise the value attribute;
|
|
671
|
+
// otherwise the browser default (first option / `selected`).
|
|
672
|
+
if (!this._applyValue(previous)) this._applyValue(this.getAttribute('value'));
|
|
673
|
+
this._syncFormValue();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Selects `value` if an option carries it, and reports whether it did. */
|
|
677
|
+
_applyValue(value) {
|
|
678
|
+
if (value === null) return false;
|
|
679
|
+
if (![...this._select.options].some(o => o.value === value)) return false;
|
|
680
|
+
this._select.value = value;
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
setupEvents() {
|
|
685
|
+
this._onChange = () => {
|
|
686
|
+
this._syncFormValue();
|
|
687
|
+
this.emit('nk-change', { value: this._select.value, name: this.name });
|
|
688
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
689
|
+
};
|
|
690
|
+
this._select.addEventListener('change', this._onChange);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
teardownEvents() {
|
|
694
|
+
this._select?.removeEventListener('change', this._onChange);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
onAttributeChanged(name) {
|
|
698
|
+
if (!this._select) return;
|
|
699
|
+
if (name === 'value') {
|
|
700
|
+
this._applyValue(this.getAttribute('value'));
|
|
701
|
+
this._syncFormValue();
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
this._forwardAll();
|
|
705
|
+
this._syncFormValue();
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
_syncFormValue() {
|
|
709
|
+
this.setFormValue(this._select.value);
|
|
710
|
+
this.syncValidityFrom(this._select);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
resetValue() {
|
|
714
|
+
if (!this._applyValue(this._defaultValue)) this._select.selectedIndex = 0;
|
|
715
|
+
this._syncFormValue();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
restoreValue(state) {
|
|
719
|
+
if (typeof state === 'string') { this._applyValue(state); this._syncFormValue(); }
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
onFormDisabled(disabled) { this._select.disabled = disabled || this.getBoolAttr('disabled'); }
|
|
723
|
+
|
|
724
|
+
focus(options) { this._select?.focus(options); }
|
|
725
|
+
|
|
726
|
+
get value() { return this._select?.value ?? ''; }
|
|
727
|
+
set value(v) {
|
|
728
|
+
if (this._select) { this._select.value = v; this._syncFormValue(); }
|
|
729
|
+
else this.setAttribute('value', v);
|
|
730
|
+
}
|
|
731
|
+
get selectedIndex() { return this._select?.selectedIndex ?? -1; }
|
|
732
|
+
get options() { return this._select?.options ?? []; }
|
|
733
|
+
get name() { return this.getAttribute('name'); }
|
|
734
|
+
set name(v) { this.setAttribute('name', v); }
|
|
735
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
736
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
737
|
+
get required() { return this.getBoolAttr('required'); }
|
|
738
|
+
set required(v) { this.setBoolAttr('required', v); }
|
|
739
|
+
get compact() { return this.getBoolAttr('compact'); }
|
|
740
|
+
set compact(v) { this.setBoolAttr('compact', v); }
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
customElements.define('nk-select', NkSelect);
|
|
744
|
+
|
|
745
|
+
// <nk-switch name="notify" checked></nk-switch>
|
|
746
|
+
// → <button class="nk-switch" role="switch" aria-checked="true">
|
|
747
|
+
// notionkit.css styles the state through [aria-checked]; the toggling is here.
|
|
748
|
+
class NkSwitch extends NkFormElement {
|
|
749
|
+
static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'label']; }
|
|
750
|
+
|
|
751
|
+
render() {
|
|
752
|
+
this._btn = this.createElement('button', ['nk-switch'], { type: 'button', role: 'switch' });
|
|
753
|
+
this._defaultChecked = this.getBoolAttr('checked');
|
|
754
|
+
this._apply();
|
|
755
|
+
this._wrapper.appendChild(this._btn);
|
|
756
|
+
this._syncFormValue();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
_apply() {
|
|
760
|
+
this._btn.setAttribute('aria-checked', this.getBoolAttr('checked') ? 'true' : 'false');
|
|
761
|
+
this._btn.disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
|
|
762
|
+
const label = this.getAttribute('label');
|
|
763
|
+
label ? this._btn.setAttribute('aria-label', label) : this._btn.removeAttribute('aria-label');
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
setupEvents() {
|
|
767
|
+
this._onClick = () => this.toggle();
|
|
768
|
+
this._btn.addEventListener('click', this._onClick);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
teardownEvents() {
|
|
772
|
+
this._btn?.removeEventListener('click', this._onClick);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
toggle() {
|
|
776
|
+
if (this.getBoolAttr('disabled') || this._formDisabled) return;
|
|
777
|
+
this.checked = !this.checked;
|
|
778
|
+
this.emit('nk-change', { checked: this.checked, value: this.getAttribute('value') || 'on', name: this.name });
|
|
779
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
onAttributeChanged() {
|
|
783
|
+
if (!this._btn) return;
|
|
784
|
+
this._apply();
|
|
785
|
+
this._syncFormValue();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
_syncFormValue() {
|
|
789
|
+
const val = this.getAttribute('value') || 'on';
|
|
790
|
+
this.setFormValue(this.getBoolAttr('checked') ? val : null, this.getBoolAttr('checked') ? 'checked' : 'unchecked');
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
resetValue() { this.setBoolAttr('checked', this._defaultChecked); }
|
|
794
|
+
restoreValue(state) { if (state === 'checked' || state === 'unchecked') this.setBoolAttr('checked', state === 'checked'); }
|
|
795
|
+
onFormDisabled(disabled) { this._formDisabled = disabled; this._apply(); }
|
|
796
|
+
|
|
797
|
+
focus(options) { this._btn?.focus(options); }
|
|
798
|
+
|
|
799
|
+
get checked() { return this.getBoolAttr('checked'); }
|
|
800
|
+
set checked(v) { this.setBoolAttr('checked', v); }
|
|
801
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
802
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
803
|
+
get name() { return this.getAttribute('name'); }
|
|
804
|
+
set name(v) { this.setAttribute('name', v); }
|
|
805
|
+
get value() { return this.getAttribute('value') || 'on'; }
|
|
806
|
+
set value(v) { this.setAttribute('value', v); }
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
customElements.define('nk-switch', NkSwitch);
|
|
810
|
+
|
|
811
|
+
// <nk-check name="mail" value="weekly" checked>Weekly digest</nk-check>
|
|
812
|
+
// → <label class="nk-check"><input type="checkbox"> Weekly digest</label>
|
|
813
|
+
class NkCheck extends NkFormElement {
|
|
814
|
+
static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'indeterminate', 'required']; }
|
|
815
|
+
|
|
816
|
+
render() {
|
|
817
|
+
const label = this.createElement('label', ['nk-check']);
|
|
818
|
+
this._input = this.createElement('input', [], { type: 'checkbox' });
|
|
819
|
+
this._apply();
|
|
820
|
+
this._defaultChecked = this.getBoolAttr('checked');
|
|
821
|
+
label.appendChild(this._input);
|
|
822
|
+
label.appendChild(document.createElement('slot'));
|
|
823
|
+
this._wrapper.appendChild(label);
|
|
824
|
+
this._syncFormValue();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
_apply() {
|
|
828
|
+
this._input.checked = this.getBoolAttr('checked');
|
|
829
|
+
this._input.indeterminate = this.getBoolAttr('indeterminate');
|
|
830
|
+
this._input.disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
|
|
831
|
+
this._input.required = this.getBoolAttr('required');
|
|
832
|
+
const name = this.getAttribute('name');
|
|
833
|
+
name ? this._input.setAttribute('name', name) : this._input.removeAttribute('name');
|
|
834
|
+
this._input.value = this.getAttribute('value') || 'on';
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
setupEvents() {
|
|
838
|
+
this._onChange = () => {
|
|
839
|
+
this._syncing = true;
|
|
840
|
+
this.setBoolAttr('checked', this._input.checked);
|
|
841
|
+
this.removeAttribute('indeterminate');
|
|
842
|
+
this._syncing = false;
|
|
843
|
+
this._syncFormValue();
|
|
844
|
+
this.emit('nk-change', { checked: this._input.checked, value: this._input.value, name: this.name });
|
|
845
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
846
|
+
};
|
|
847
|
+
this._input.addEventListener('change', this._onChange);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
teardownEvents() {
|
|
851
|
+
this._input?.removeEventListener('change', this._onChange);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
onAttributeChanged() {
|
|
855
|
+
if (this._syncing || !this._input) return;
|
|
856
|
+
this._apply();
|
|
857
|
+
this._syncFormValue();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
_syncFormValue() {
|
|
861
|
+
this.setFormValue(this._input.checked ? this._input.value : null, this._input.checked ? 'checked' : 'unchecked');
|
|
862
|
+
this.syncValidityFrom(this._input);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
resetValue() { this.setBoolAttr('checked', this._defaultChecked); }
|
|
866
|
+
restoreValue(state) { if (state === 'checked' || state === 'unchecked') this.setBoolAttr('checked', state === 'checked'); }
|
|
867
|
+
onFormDisabled(disabled) { this._formDisabled = disabled; this._apply(); }
|
|
868
|
+
|
|
869
|
+
focus(options) { this._input?.focus(options); }
|
|
870
|
+
|
|
871
|
+
get checked() { return this._input?.checked ?? this.getBoolAttr('checked'); }
|
|
872
|
+
set checked(v) { this.setBoolAttr('checked', v); }
|
|
873
|
+
get indeterminate() { return this.getBoolAttr('indeterminate'); }
|
|
874
|
+
set indeterminate(v) { this.setBoolAttr('indeterminate', v); }
|
|
875
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
876
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
877
|
+
get name() { return this.getAttribute('name'); }
|
|
878
|
+
set name(v) { this.setAttribute('name', v); }
|
|
879
|
+
get value() { return this.getAttribute('value') || 'on'; }
|
|
880
|
+
set value(v) { this.setAttribute('value', v); }
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
customElements.define('nk-check', NkCheck);
|
|
884
|
+
|
|
885
|
+
// <nk-radio name="style" value="concise" checked>Concise</nk-radio>
|
|
886
|
+
// → <label class="nk-check"><input type="radio"> Concise</label>
|
|
887
|
+
//
|
|
888
|
+
// ── Grouping ──
|
|
889
|
+
// Every <nk-radio> keeps its <input type="radio"> in its own shadow root, and
|
|
890
|
+
// native radio grouping works per tree — it does not reach across shadow
|
|
891
|
+
// boundaries. Two <nk-radio name="x"> would therefore both stay checked. So
|
|
892
|
+
// the group is kept here, following the native definition: same `name`,
|
|
893
|
+
// same containing tree, same form owner. One tab stop per group, arrow keys
|
|
894
|
+
// move within it (wrapping, skipping disabled), and select as they go.
|
|
895
|
+
|
|
896
|
+
const ARROW_KEYS = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 };
|
|
897
|
+
|
|
898
|
+
/** Form owner, also for a peer that has not been upgraded yet. */
|
|
899
|
+
function ownerForm(el) {
|
|
900
|
+
return (el.form !== undefined ? el.form : el.closest('form')) ?? null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** Only the selected radio is a tab stop; arrow keys move within the group. */
|
|
904
|
+
function syncGroupTabIndex(group) {
|
|
905
|
+
const enabled = group.filter(el => !el.disabled);
|
|
906
|
+
if (!enabled.length) return;
|
|
907
|
+
const focusable = enabled.find(el => el.checked) || enabled[0];
|
|
908
|
+
for (const el of group) {
|
|
909
|
+
if (el._input) el._input.tabIndex = el === focusable ? 0 : -1;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
class NkRadio extends NkFormElement {
|
|
914
|
+
static get observedAttributes() { return ['checked', 'disabled', 'name', 'value', 'required']; }
|
|
915
|
+
|
|
916
|
+
render() {
|
|
917
|
+
const label = this.createElement('label', ['nk-check']);
|
|
918
|
+
this._input = this.createElement('input', [], { type: 'radio' });
|
|
919
|
+
// `name` on the inner input is inert (it is alone in its tree); it stays
|
|
920
|
+
// as a devtools courtesy. The group above is the real one.
|
|
921
|
+
this._apply();
|
|
922
|
+
this._defaultChecked = this.getBoolAttr('checked');
|
|
923
|
+
label.appendChild(this._input);
|
|
924
|
+
label.appendChild(document.createElement('slot'));
|
|
925
|
+
this._wrapper.appendChild(label);
|
|
926
|
+
this._syncFormValue();
|
|
927
|
+
// Elements upgrade in document order, so the last `checked` one in the
|
|
928
|
+
// markup wins the group — the same outcome native radios produce.
|
|
929
|
+
if (this._input.checked) this._uncheckPeers();
|
|
930
|
+
syncGroupTabIndex(this._group());
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
_apply() {
|
|
934
|
+
this._input.checked = this.getBoolAttr('checked');
|
|
935
|
+
this._input.disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
|
|
936
|
+
this._input.required = this.getBoolAttr('required');
|
|
937
|
+
const name = this.getAttribute('name');
|
|
938
|
+
name ? this._input.setAttribute('name', name) : this._input.removeAttribute('name');
|
|
939
|
+
this._input.value = this.getAttribute('value') || '';
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
setupEvents() {
|
|
943
|
+
this._onChange = () => this._applyChange();
|
|
944
|
+
this._onKeyDown = (e) => {
|
|
945
|
+
const dir = ARROW_KEYS[e.key];
|
|
946
|
+
if (!dir || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
947
|
+
const group = this._group().filter(el => !el.disabled);
|
|
948
|
+
if (group.length < 2) return;
|
|
949
|
+
e.preventDefault();
|
|
950
|
+
const next = group[(group.indexOf(this) + dir + group.length) % group.length];
|
|
951
|
+
next._input.focus();
|
|
952
|
+
next._input.checked = true;
|
|
953
|
+
next._applyChange();
|
|
954
|
+
};
|
|
955
|
+
this._input.addEventListener('change', this._onChange);
|
|
956
|
+
this._input.addEventListener('keydown', this._onKeyDown);
|
|
957
|
+
syncGroupTabIndex(this._group());
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
teardownEvents() {
|
|
961
|
+
this._input?.removeEventListener('change', this._onChange);
|
|
962
|
+
this._input?.removeEventListener('keydown', this._onKeyDown);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** Shared by user change and arrow-key selection, so both look identical. */
|
|
966
|
+
_applyChange() {
|
|
967
|
+
if (this._input.checked) this._uncheckPeers();
|
|
968
|
+
this._syncing = true;
|
|
969
|
+
this.setBoolAttr('checked', this._input.checked);
|
|
970
|
+
this._syncing = false;
|
|
971
|
+
this._syncFormValue();
|
|
972
|
+
syncGroupTabIndex(this._group());
|
|
973
|
+
this.emit('nk-change', { checked: this._input.checked, value: this._input.value, name: this.name });
|
|
974
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** Same name, same tree, same form owner — in document order, this element included. */
|
|
978
|
+
_group() {
|
|
979
|
+
const name = this.getAttribute('name');
|
|
980
|
+
const root = this.getRootNode();
|
|
981
|
+
if (!name || typeof root?.querySelectorAll !== 'function') return [this];
|
|
982
|
+
const form = ownerForm(this);
|
|
983
|
+
return [...root.querySelectorAll('nk-radio')].filter(
|
|
984
|
+
el => el.getAttribute('name') === name && ownerForm(el) === form
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
_uncheckPeers() {
|
|
989
|
+
for (const el of this._group()) {
|
|
990
|
+
if (el !== this && el.checked) el.checked = false;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
onAttributeChanged(name) {
|
|
995
|
+
if (this._syncing || !this._input) return;
|
|
996
|
+
this._apply();
|
|
997
|
+
if (name === 'checked' && this._input.checked) this._uncheckPeers();
|
|
998
|
+
this._syncFormValue();
|
|
999
|
+
syncGroupTabIndex(this._group());
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
_syncFormValue() {
|
|
1003
|
+
const val = this.getAttribute('value') || '';
|
|
1004
|
+
this.setFormValue(this._input.checked ? val : null, this._input.checked ? 'checked' : 'unchecked');
|
|
1005
|
+
this.syncValidityFrom(this._input);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
resetValue() {
|
|
1009
|
+
this._input.checked = this._defaultChecked;
|
|
1010
|
+
this.setBoolAttr('checked', this._defaultChecked);
|
|
1011
|
+
this._syncFormValue();
|
|
1012
|
+
syncGroupTabIndex(this._group());
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
restoreValue(state) {
|
|
1016
|
+
if (state === 'checked') this.checked = true;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
onFormDisabled(disabled) { this._formDisabled = disabled; this._apply(); syncGroupTabIndex(this._group()); }
|
|
1020
|
+
|
|
1021
|
+
focus(options) { this._input?.focus(options); }
|
|
1022
|
+
|
|
1023
|
+
get checked() { return this._input?.checked ?? this.getBoolAttr('checked'); }
|
|
1024
|
+
set checked(v) {
|
|
1025
|
+
if (this._input) this._input.checked = v;
|
|
1026
|
+
// Also covers the case where the attribute is already present, so
|
|
1027
|
+
// setBoolAttr stays silent and onAttributeChanged never runs.
|
|
1028
|
+
if (v) this._uncheckPeers();
|
|
1029
|
+
this.setBoolAttr('checked', v);
|
|
1030
|
+
if (this._input) { this._syncFormValue(); syncGroupTabIndex(this._group()); }
|
|
1031
|
+
}
|
|
1032
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
1033
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
1034
|
+
get name() { return this.getAttribute('name'); }
|
|
1035
|
+
set name(v) { this.setAttribute('name', v); }
|
|
1036
|
+
get value() { return this.getAttribute('value') || ''; }
|
|
1037
|
+
set value(v) { this.setAttribute('value', v); }
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
customElements.define('nk-radio', NkRadio);
|
|
1041
|
+
|
|
1042
|
+
// <nk-slider name="size" min="12" max="18" value="14" show-value></nk-slider>
|
|
1043
|
+
// → <input type="range" class="nk-slider"> <div class="nk-slider-value">14</div>
|
|
1044
|
+
const FORWARDED = ['name', 'min', 'max', 'step', 'aria-label'];
|
|
1045
|
+
|
|
1046
|
+
class NkSlider extends NkFormElement {
|
|
1047
|
+
static get observedAttributes() { return ['value', 'disabled', 'show-value', 'unit', ...FORWARDED]; }
|
|
1048
|
+
|
|
1049
|
+
render() {
|
|
1050
|
+
this._input = this.createElement('input', ['nk-slider'], { type: 'range' });
|
|
1051
|
+
this._out = this.createElement('div', ['nk-slider-value']);
|
|
1052
|
+
this._apply();
|
|
1053
|
+
this._input.value = this.getAttribute('value') ?? this._input.value;
|
|
1054
|
+
this._defaultValue = this._input.value;
|
|
1055
|
+
this._wrapper.appendChild(this._input);
|
|
1056
|
+
this._wrapper.appendChild(this._out);
|
|
1057
|
+
this._syncOut();
|
|
1058
|
+
this._syncFormValue();
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
_apply() {
|
|
1062
|
+
for (const attr of FORWARDED) {
|
|
1063
|
+
const v = this.getAttribute(attr);
|
|
1064
|
+
v === null ? this._input.removeAttribute(attr) : this._input.setAttribute(attr, v);
|
|
1065
|
+
}
|
|
1066
|
+
this._input.disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
|
|
1067
|
+
this._out.style.display = this.getBoolAttr('show-value') ? '' : 'none';
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
_syncOut() {
|
|
1071
|
+
this._out.textContent = `${this._input.value}${this.getAttribute('unit') || ''}`;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
setupEvents() {
|
|
1075
|
+
this._onInput = () => {
|
|
1076
|
+
this._syncOut();
|
|
1077
|
+
this._syncFormValue();
|
|
1078
|
+
this.emit('nk-input', { value: Number(this._input.value), name: this.name });
|
|
1079
|
+
};
|
|
1080
|
+
this._onChange = () => {
|
|
1081
|
+
this._syncFormValue();
|
|
1082
|
+
this.emit('nk-change', { value: Number(this._input.value), name: this.name });
|
|
1083
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1084
|
+
};
|
|
1085
|
+
this._input.addEventListener('input', this._onInput);
|
|
1086
|
+
this._input.addEventListener('change', this._onChange);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
teardownEvents() {
|
|
1090
|
+
this._input?.removeEventListener('input', this._onInput);
|
|
1091
|
+
this._input?.removeEventListener('change', this._onChange);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
onAttributeChanged(name) {
|
|
1095
|
+
if (!this._input) return;
|
|
1096
|
+
if (name === 'value') this._input.value = this.getAttribute('value') ?? '';
|
|
1097
|
+
else this._apply();
|
|
1098
|
+
this._syncOut();
|
|
1099
|
+
this._syncFormValue();
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
_syncFormValue() { this.setFormValue(this._input.value); }
|
|
1103
|
+
resetValue() { this._input.value = this._defaultValue; this._syncOut(); this._syncFormValue(); }
|
|
1104
|
+
restoreValue(state) { if (typeof state === 'string') { this._input.value = state; this._syncOut(); this._syncFormValue(); } }
|
|
1105
|
+
onFormDisabled(disabled) { this._formDisabled = disabled; this._apply(); }
|
|
1106
|
+
|
|
1107
|
+
focus(options) { this._input?.focus(options); }
|
|
1108
|
+
|
|
1109
|
+
get value() { return Number(this._input?.value ?? this.getAttribute('value')); }
|
|
1110
|
+
set value(v) {
|
|
1111
|
+
if (this._input) { this._input.value = v; this._syncOut(); this._syncFormValue(); }
|
|
1112
|
+
else this.setAttribute('value', v);
|
|
1113
|
+
}
|
|
1114
|
+
get name() { return this.getAttribute('name'); }
|
|
1115
|
+
set name(v) { this.setAttribute('name', v); }
|
|
1116
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
1117
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
1118
|
+
get showValue() { return this.getBoolAttr('show-value'); }
|
|
1119
|
+
set showValue(v) { this.setBoolAttr('show-value', v); }
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
customElements.define('nk-slider', NkSlider);
|
|
1123
|
+
|
|
1124
|
+
// <nk-field label="Display name" desc="Shown next to your comments">
|
|
1125
|
+
// <nk-input value="Marcel"></nk-input>
|
|
1126
|
+
// </nk-field>
|
|
1127
|
+
// → <div class="nk-field"><div><div class="f-label">…</div><div class="f-desc">…</div></div>
|
|
1128
|
+
// <div class="f-control">…</div></div>
|
|
1129
|
+
// Rich label / description content goes through slot="label" / slot="desc"
|
|
1130
|
+
// (twins `.nk-field ::slotted([slot="label"])` etc. in notionkit.css).
|
|
1131
|
+
class NkField extends NkElement {
|
|
1132
|
+
static get observedAttributes() { return ['label', 'desc']; }
|
|
1133
|
+
|
|
1134
|
+
render() {
|
|
1135
|
+
const field = this.createElement('div', ['nk-field']);
|
|
1136
|
+
const text = document.createElement('div');
|
|
1137
|
+
this._label = this.createElement('div', ['f-label']);
|
|
1138
|
+
const labelSlot = this.createElement('slot', [], { name: 'label' });
|
|
1139
|
+
labelSlot.appendChild(this._label);
|
|
1140
|
+
this._desc = this.createElement('div', ['f-desc']);
|
|
1141
|
+
const descSlot = this.createElement('slot', [], { name: 'desc' });
|
|
1142
|
+
descSlot.appendChild(this._desc);
|
|
1143
|
+
text.appendChild(labelSlot);
|
|
1144
|
+
text.appendChild(descSlot);
|
|
1145
|
+
const control = this.createElement('div', ['f-control']);
|
|
1146
|
+
control.appendChild(document.createElement('slot'));
|
|
1147
|
+
field.appendChild(text);
|
|
1148
|
+
field.appendChild(control);
|
|
1149
|
+
this._wrapper.appendChild(field);
|
|
1150
|
+
this._sync();
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
onAttributeChanged() { this._sync(); }
|
|
1154
|
+
|
|
1155
|
+
_sync() {
|
|
1156
|
+
if (!this._label) return;
|
|
1157
|
+
this._label.textContent = this.getAttribute('label') || '';
|
|
1158
|
+
const desc = this.getAttribute('desc');
|
|
1159
|
+
this._desc.textContent = desc || '';
|
|
1160
|
+
this._desc.style.display = desc ? '' : 'none';
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
get label() { return this.getAttribute('label'); }
|
|
1164
|
+
set label(v) { this.setAttribute('label', v); }
|
|
1165
|
+
get desc() { return this.getAttribute('desc'); }
|
|
1166
|
+
set desc(v) { v == null ? this.removeAttribute('desc') : this.setAttribute('desc', v); }
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
customElements.define('nk-field', NkField);
|
|
1170
|
+
|
|
1171
|
+
// <nk-tag color="blue">Text</nk-tag> — modifier class .blue becomes color="blue"
|
|
1172
|
+
const COLORS$1 = ['blue', 'green', 'orange', 'purple'];
|
|
1173
|
+
|
|
1174
|
+
class NkTag extends NkElement {
|
|
1175
|
+
static get observedAttributes() { return ['color']; }
|
|
1176
|
+
|
|
1177
|
+
render() {
|
|
1178
|
+
this._tag = this.createElement('span', this._computeClasses());
|
|
1179
|
+
this._tag.appendChild(document.createElement('slot'));
|
|
1180
|
+
this._wrapper.appendChild(this._tag);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
onAttributeChanged() {
|
|
1184
|
+
if (this._tag) this._tag.className = this._computeClasses().join(' ');
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
_computeClasses() {
|
|
1188
|
+
const classes = ['nk-tag'];
|
|
1189
|
+
const color = this.getAttribute('color');
|
|
1190
|
+
if (COLORS$1.includes(color)) classes.push(color);
|
|
1191
|
+
return classes;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
get color() { return this.getAttribute('color'); }
|
|
1195
|
+
set color(v) { v ? this.setAttribute('color', v) : this.removeAttribute('color'); }
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
customElements.define('nk-tag', NkTag);
|
|
1199
|
+
|
|
1200
|
+
// <nk-progress value="72" label="72%"></nk-progress>
|
|
1201
|
+
// The bar is `.nk-progress > i` — a child combinator, so both nodes live here.
|
|
1202
|
+
class NkProgress extends NkElement {
|
|
1203
|
+
static get observedAttributes() { return ['value', 'max', 'label']; }
|
|
1204
|
+
|
|
1205
|
+
render() {
|
|
1206
|
+
this._bar = this.createElement('span', ['nk-progress'], { role: 'progressbar' });
|
|
1207
|
+
this._fill = document.createElement('i');
|
|
1208
|
+
this._bar.appendChild(this._fill);
|
|
1209
|
+
this._label = this.createElement('span', ['nk-progress-label']);
|
|
1210
|
+
this._wrapper.appendChild(this._bar);
|
|
1211
|
+
this._wrapper.appendChild(this._label);
|
|
1212
|
+
this._sync();
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
onAttributeChanged() { this._sync(); }
|
|
1216
|
+
|
|
1217
|
+
_sync() {
|
|
1218
|
+
if (!this._bar) return;
|
|
1219
|
+
const max = Number(this.getAttribute('max')) || 100;
|
|
1220
|
+
const value = Math.min(max, Math.max(0, Number(this.getAttribute('value')) || 0));
|
|
1221
|
+
this._fill.style.width = `${(value / max) * 100}%`;
|
|
1222
|
+
this._bar.setAttribute('aria-valuenow', value);
|
|
1223
|
+
this._bar.setAttribute('aria-valuemin', 0);
|
|
1224
|
+
this._bar.setAttribute('aria-valuemax', max);
|
|
1225
|
+
const label = this.getAttribute('label');
|
|
1226
|
+
this._label.textContent = label ?? '';
|
|
1227
|
+
this._label.style.display = label === null ? 'none' : '';
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
get value() { return Number(this.getAttribute('value')) || 0; }
|
|
1231
|
+
set value(v) { this.setAttribute('value', v); }
|
|
1232
|
+
get max() { return Number(this.getAttribute('max')) || 100; }
|
|
1233
|
+
set max(v) { this.setAttribute('max', v); }
|
|
1234
|
+
get label() { return this.getAttribute('label'); }
|
|
1235
|
+
set label(v) { v == null ? this.removeAttribute('label') : this.setAttribute('label', v); }
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
customElements.define('nk-progress', NkProgress);
|
|
1239
|
+
|
|
1240
|
+
// <nk-callout icon="💡">text</nk-callout>
|
|
1241
|
+
// <nk-callout><span slot="icon">📌</span>text</nk-callout>
|
|
1242
|
+
// The .c-icon rule has ::slotted(.c-icon) / ::slotted([slot="icon"]) twins in
|
|
1243
|
+
// notionkit.css – those size an icon passed in from the light DOM. Pass the
|
|
1244
|
+
// icon node itself, never wrapped: ::slotted() only matches the assigned node.
|
|
1245
|
+
class NkCallout extends NkElement {
|
|
1246
|
+
static get observedAttributes() { return ['icon']; }
|
|
1247
|
+
|
|
1248
|
+
render() {
|
|
1249
|
+
this._box = this.createElement('div', ['nk-callout']);
|
|
1250
|
+
this._icon = this.createElement('span', ['c-icon']);
|
|
1251
|
+
const iconSlot = this.createElement('slot', [], { name: 'icon' });
|
|
1252
|
+
iconSlot.appendChild(this._icon);
|
|
1253
|
+
const body = document.createElement('div');
|
|
1254
|
+
body.appendChild(document.createElement('slot'));
|
|
1255
|
+
this._box.appendChild(iconSlot);
|
|
1256
|
+
this._box.appendChild(body);
|
|
1257
|
+
this._wrapper.appendChild(this._box);
|
|
1258
|
+
this._syncIcon();
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
onAttributeChanged() { this._syncIcon(); }
|
|
1262
|
+
|
|
1263
|
+
_syncIcon() {
|
|
1264
|
+
if (this._icon) this._icon.textContent = this.getAttribute('icon') || '💡';
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
get icon() { return this.getAttribute('icon'); }
|
|
1268
|
+
set icon(v) { v == null ? this.removeAttribute('icon') : this.setAttribute('icon', v); }
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
customElements.define('nk-callout', NkCallout);
|
|
1272
|
+
|
|
1273
|
+
// <nk-divider></nk-divider> → <hr class="nk-divider">
|
|
1274
|
+
class NkDivider extends NkElement {
|
|
1275
|
+
render() {
|
|
1276
|
+
this._wrapper.appendChild(this.createElement('hr', ['nk-divider']));
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
customElements.define('nk-divider', NkDivider);
|
|
1281
|
+
|
|
1282
|
+
// <nk-heading level="2">Section</nk-heading> → <h2 class="nk-heading">
|
|
1283
|
+
// The heading level is real markup (h1–h4), so the outline stays honest.
|
|
1284
|
+
class NkHeading extends NkElement {
|
|
1285
|
+
static get observedAttributes() { return ['level']; }
|
|
1286
|
+
|
|
1287
|
+
render() {
|
|
1288
|
+
this._build();
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
_build() {
|
|
1292
|
+
const level = Math.min(4, Math.max(1, Number(this.getAttribute('level')) || 2));
|
|
1293
|
+
const h = this.createElement(`h${level}`, ['nk-heading']);
|
|
1294
|
+
h.appendChild(document.createElement('slot'));
|
|
1295
|
+
if (this._heading) this._heading.replaceWith(h);
|
|
1296
|
+
else this._wrapper.appendChild(h);
|
|
1297
|
+
this._heading = h;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
onAttributeChanged() { this._build(); }
|
|
1301
|
+
|
|
1302
|
+
get level() { return Number(this.getAttribute('level')) || 2; }
|
|
1303
|
+
set level(v) { this.setAttribute('level', v); }
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
customElements.define('nk-heading', NkHeading);
|
|
1307
|
+
|
|
1308
|
+
// <nk-toggle label="Details" open>Body…</nk-toggle>
|
|
1309
|
+
// → <details class="nk-toggle" open><summary>Details</summary><div class="toggle-body">…</div></details>
|
|
1310
|
+
// The summary marker is a pseudo-element and cannot be styled on slotted
|
|
1311
|
+
// content, so the summary is rendered here (label attribute or slot="label").
|
|
1312
|
+
class NkToggle extends NkElement {
|
|
1313
|
+
static get observedAttributes() { return ['label', 'open']; }
|
|
1314
|
+
|
|
1315
|
+
render() {
|
|
1316
|
+
this._details = this.createElement('details', ['nk-toggle']);
|
|
1317
|
+
const summary = document.createElement('summary');
|
|
1318
|
+
this._label = document.createElement('span');
|
|
1319
|
+
const labelSlot = this.createElement('slot', [], { name: 'label' });
|
|
1320
|
+
labelSlot.appendChild(this._label);
|
|
1321
|
+
summary.appendChild(labelSlot);
|
|
1322
|
+
const body = this.createElement('div', ['toggle-body']);
|
|
1323
|
+
body.appendChild(document.createElement('slot'));
|
|
1324
|
+
this._details.appendChild(summary);
|
|
1325
|
+
this._details.appendChild(body);
|
|
1326
|
+
this._details.open = this.getBoolAttr('open');
|
|
1327
|
+
this._wrapper.appendChild(this._details);
|
|
1328
|
+
this._sync();
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
setupEvents() {
|
|
1332
|
+
this._onToggle = () => {
|
|
1333
|
+
this._syncing = true;
|
|
1334
|
+
this.setBoolAttr('open', this._details.open);
|
|
1335
|
+
this._syncing = false;
|
|
1336
|
+
this.emit('nk-toggle', { open: this._details.open });
|
|
1337
|
+
};
|
|
1338
|
+
this._details.addEventListener('toggle', this._onToggle);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
teardownEvents() {
|
|
1342
|
+
this._details?.removeEventListener('toggle', this._onToggle);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
onAttributeChanged(name) {
|
|
1346
|
+
if (!this._details) return;
|
|
1347
|
+
if (name === 'open') { if (!this._syncing) this._details.open = this.getBoolAttr('open'); return; }
|
|
1348
|
+
this._sync();
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
_sync() { this._label.textContent = this.getAttribute('label') || ''; }
|
|
1352
|
+
|
|
1353
|
+
get open() { return this.getBoolAttr('open'); }
|
|
1354
|
+
set open(v) { this.setBoolAttr('open', v); }
|
|
1355
|
+
get label() { return this.getAttribute('label'); }
|
|
1356
|
+
set label(v) { this.setAttribute('label', v); }
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
customElements.define('nk-toggle', NkToggle);
|
|
1360
|
+
|
|
1361
|
+
// <nk-todo name="task" value="1" checked>Write the docs</nk-todo>
|
|
1362
|
+
// → <label class="nk-todo"><input type="checkbox"><span>Write the docs</span></label>
|
|
1363
|
+
// `.nk-todo input:checked + span` strikes the text through — both siblings
|
|
1364
|
+
// are rendered here so the combinator works; the text is slotted into <span>.
|
|
1365
|
+
class NkTodo extends NkFormElement {
|
|
1366
|
+
static get observedAttributes() { return ['checked', 'disabled', 'name', 'value']; }
|
|
1367
|
+
|
|
1368
|
+
render() {
|
|
1369
|
+
const label = this.createElement('label', ['nk-todo']);
|
|
1370
|
+
this._input = this.createElement('input', [], { type: 'checkbox' });
|
|
1371
|
+
this._apply();
|
|
1372
|
+
this._defaultChecked = this.getBoolAttr('checked');
|
|
1373
|
+
const text = document.createElement('span');
|
|
1374
|
+
text.appendChild(document.createElement('slot'));
|
|
1375
|
+
label.appendChild(this._input);
|
|
1376
|
+
label.appendChild(text);
|
|
1377
|
+
this._wrapper.appendChild(label);
|
|
1378
|
+
this._syncFormValue();
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
_apply() {
|
|
1382
|
+
this._input.checked = this.getBoolAttr('checked');
|
|
1383
|
+
this._input.disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
|
|
1384
|
+
const name = this.getAttribute('name');
|
|
1385
|
+
name ? this._input.setAttribute('name', name) : this._input.removeAttribute('name');
|
|
1386
|
+
this._input.value = this.getAttribute('value') || 'on';
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
setupEvents() {
|
|
1390
|
+
this._onChange = () => {
|
|
1391
|
+
this._syncing = true;
|
|
1392
|
+
this.setBoolAttr('checked', this._input.checked);
|
|
1393
|
+
this._syncing = false;
|
|
1394
|
+
this._syncFormValue();
|
|
1395
|
+
this.emit('nk-change', { checked: this._input.checked, value: this._input.value, name: this.name });
|
|
1396
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1397
|
+
};
|
|
1398
|
+
this._input.addEventListener('change', this._onChange);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
teardownEvents() {
|
|
1402
|
+
this._input?.removeEventListener('change', this._onChange);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
onAttributeChanged() {
|
|
1406
|
+
if (this._syncing || !this._input) return;
|
|
1407
|
+
this._apply();
|
|
1408
|
+
this._syncFormValue();
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
_syncFormValue() {
|
|
1412
|
+
this.setFormValue(this._input.checked ? this._input.value : null, this._input.checked ? 'checked' : 'unchecked');
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
resetValue() { this.setBoolAttr('checked', this._defaultChecked); }
|
|
1416
|
+
restoreValue(state) { if (state === 'checked' || state === 'unchecked') this.setBoolAttr('checked', state === 'checked'); }
|
|
1417
|
+
onFormDisabled(disabled) { this._formDisabled = disabled; this._apply(); }
|
|
1418
|
+
|
|
1419
|
+
get checked() { return this._input?.checked ?? this.getBoolAttr('checked'); }
|
|
1420
|
+
set checked(v) { this.setBoolAttr('checked', v); }
|
|
1421
|
+
get disabled() { return this.getBoolAttr('disabled'); }
|
|
1422
|
+
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
1423
|
+
get name() { return this.getAttribute('name'); }
|
|
1424
|
+
set name(v) { this.setAttribute('name', v); }
|
|
1425
|
+
get value() { return this.getAttribute('value') || 'on'; }
|
|
1426
|
+
set value(v) { this.setAttribute('value', v); }
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
customElements.define('nk-todo', NkTodo);
|
|
1430
|
+
|
|
1431
|
+
// <nk-kbd>⌘</nk-kbd> <nk-kbd>K</nk-kbd> → <kbd class="nk-kbd">
|
|
1432
|
+
class NkKbd extends NkElement {
|
|
1433
|
+
|
|
1434
|
+
render() {
|
|
1435
|
+
const kbd = this.createElement('kbd', ['nk-kbd']);
|
|
1436
|
+
kbd.appendChild(document.createElement('slot'));
|
|
1437
|
+
this._wrapper.appendChild(kbd);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
customElements.define('nk-kbd', NkKbd);
|
|
1442
|
+
|
|
1443
|
+
// Minimal HTML highlighter emitting the .tag / .attr hooks .nk-code styles.
|
|
1444
|
+
// Mirrors tools/highlight.mjs in the NotionKit foundation – no third-party
|
|
1445
|
+
// highlighter, no other languages. Anything that is not HTML is escaped only.
|
|
1446
|
+
function escapeHtml(s) {
|
|
1447
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function highlightHtml(source) {
|
|
1451
|
+
return escapeHtml(source).replace(/<(\/?)([a-zA-Z][\w-]*)([^&]*?)(\/?)>/g, (m, close, tag, attrs, self) => {
|
|
1452
|
+
const attrHtml = attrs.replace(/([\w-]+)(=)("[^"]*"|'[^']*')?/g,
|
|
1453
|
+
(a, name, eq, val) => `<span class="attr">${name}</span>${eq}${val ?? ''}`);
|
|
1454
|
+
return `<${close}<span class="tag">${tag}</span>${attrHtml}${self}>`;
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// <nk-code lang="html"><div class="nk-btn">…</nk-code>
|
|
1459
|
+
// The block keeps whatever whitespace it is given (white-space: pre). Text
|
|
1460
|
+
// content is slotted as-is; add `highlight` to colour HTML tags/attributes,
|
|
1461
|
+
// which copies the text into the shadow tree (light DOM is then ignored).
|
|
1462
|
+
class NkCode extends NkElement {
|
|
1463
|
+
static get observedAttributes() { return ['lang', 'highlight']; }
|
|
1464
|
+
static get observesLightDom() { return true; }
|
|
1465
|
+
|
|
1466
|
+
render() {
|
|
1467
|
+
this._box = this.createElement('div', ['nk-code']);
|
|
1468
|
+
this._lang = this.createElement('span', ['lang']);
|
|
1469
|
+
this._slot = document.createElement('slot');
|
|
1470
|
+
this._pre = this.createElement('span', ['hl']);
|
|
1471
|
+
this._box.appendChild(this._lang);
|
|
1472
|
+
this._box.appendChild(this._slot);
|
|
1473
|
+
this._box.appendChild(this._pre);
|
|
1474
|
+
this._wrapper.appendChild(this._box);
|
|
1475
|
+
this._sync();
|
|
1476
|
+
this.projectLightDom();
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
onAttributeChanged() { this._sync(); this.projectLightDom(); }
|
|
1480
|
+
|
|
1481
|
+
_sync() {
|
|
1482
|
+
if (!this._lang) return;
|
|
1483
|
+
const lang = this.getAttribute('lang');
|
|
1484
|
+
this._lang.textContent = lang || '';
|
|
1485
|
+
this._lang.style.display = lang ? '' : 'none';
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
projectLightDom() {
|
|
1489
|
+
if (!this._pre) return;
|
|
1490
|
+
const highlight = this.hasAttribute('highlight');
|
|
1491
|
+
this._slot.style.display = highlight ? 'none' : '';
|
|
1492
|
+
this._pre.style.display = highlight ? '' : 'none';
|
|
1493
|
+
if (!highlight) return;
|
|
1494
|
+
const text = this.textContent.replace(/^\n/, '');
|
|
1495
|
+
if (text === this._lastText) return;
|
|
1496
|
+
this._lastText = text;
|
|
1497
|
+
this._pre.innerHTML = highlightHtml(text);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
get lang() { return this.getAttribute('lang'); }
|
|
1501
|
+
set lang(v) { v == null ? this.removeAttribute('lang') : this.setAttribute('lang', v); }
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
customElements.define('nk-code', NkCode);
|
|
1505
|
+
|
|
1506
|
+
// <nk-quote cite="Ada Lovelace">The Analytical Engine weaves…</nk-quote>
|
|
1507
|
+
// .q-cite has no ::slotted() twin, so the citation is rendered here.
|
|
1508
|
+
class NkQuote extends NkElement {
|
|
1509
|
+
static get observedAttributes() { return ['cite']; }
|
|
1510
|
+
|
|
1511
|
+
render() {
|
|
1512
|
+
this._quote = this.createElement('blockquote', ['nk-quote']);
|
|
1513
|
+
this._quote.appendChild(document.createElement('slot'));
|
|
1514
|
+
this._cite = this.createElement('cite', ['q-cite']);
|
|
1515
|
+
this._quote.appendChild(this._cite);
|
|
1516
|
+
this._wrapper.appendChild(this._quote);
|
|
1517
|
+
this._sync();
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
onAttributeChanged() { this._sync(); }
|
|
1521
|
+
|
|
1522
|
+
_sync() {
|
|
1523
|
+
if (!this._cite) return;
|
|
1524
|
+
const cite = this.getAttribute('cite');
|
|
1525
|
+
this._cite.textContent = cite || '';
|
|
1526
|
+
this._cite.style.display = cite ? '' : 'none';
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
get cite() { return this.getAttribute('cite'); }
|
|
1530
|
+
set cite(v) { v == null ? this.removeAttribute('cite') : this.setAttribute('cite', v); }
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
customElements.define('nk-quote', NkQuote);
|
|
1534
|
+
|
|
1535
|
+
// <nk-app>
|
|
1536
|
+
// <nk-sidebar slot="sidebar">…</nk-sidebar>
|
|
1537
|
+
// <nk-topbar>…</nk-topbar>
|
|
1538
|
+
// <nk-page>…</nk-page>
|
|
1539
|
+
// </nk-app>
|
|
1540
|
+
// → <div class="nk-app"><aside class="nk-sidebar">…</aside><main class="nk-main">…</main></div>
|
|
1541
|
+
// The outermost element of a workspace app: a full-height flex row.
|
|
1542
|
+
class NkApp extends NkElement {
|
|
1543
|
+
render() {
|
|
1544
|
+
const app = this.createElement('div', ['nk-app']);
|
|
1545
|
+
app.appendChild(this.createElement('slot', [], { name: 'sidebar' }));
|
|
1546
|
+
const main = this.createElement('main', ['nk-main']);
|
|
1547
|
+
main.appendChild(document.createElement('slot'));
|
|
1548
|
+
app.appendChild(main);
|
|
1549
|
+
this._wrapper.appendChild(app);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
customElements.define('nk-app', NkApp);
|
|
1554
|
+
|
|
1555
|
+
// <nk-sidebar slot="sidebar">
|
|
1556
|
+
// <nk-workspace-switcher slot="workspace" name="Acme"></nk-workspace-switcher>
|
|
1557
|
+
// <nk-tree>…</nk-tree>
|
|
1558
|
+
// <nk-tree-item slot="footer" icon="⚙️">Settings</nk-tree-item>
|
|
1559
|
+
// </nk-sidebar>
|
|
1560
|
+
// → <aside class="nk-sidebar"><div class="nk-workspace">…</div>
|
|
1561
|
+
// <div class="nk-sidebar-scroll">…</div><div class="nk-sidebar-footer">…</div></aside>
|
|
1562
|
+
//
|
|
1563
|
+
// The host is display:contents so the <aside> is a direct flex child of
|
|
1564
|
+
// .nk-app, exactly like the class markup. Below 860px notionkit.css hides the
|
|
1565
|
+
// aside; `open` brings it back as an off-canvas drawer (position only – the
|
|
1566
|
+
// look is still the stylesheet's).
|
|
1567
|
+
const drawerSheet = new CSSStyleSheet();
|
|
1568
|
+
drawerSheet.replaceSync(`
|
|
1569
|
+
@media (max-width: 860px) {
|
|
1570
|
+
:host([open]) .nk-sidebar { display: flex; position: fixed; inset: 0 auto 0 0; z-index: 60; }
|
|
1571
|
+
:host([open]) .nk-sidebar-backdrop { display: block; }
|
|
1572
|
+
}
|
|
1573
|
+
.nk-sidebar-backdrop { display: none; position: fixed; inset: 0; z-index: 59; background: var(--nk-scrim-soft); }
|
|
1574
|
+
`);
|
|
1575
|
+
|
|
1576
|
+
class NkSidebar extends NkElement {
|
|
1577
|
+
static get hostStyles() { return drawerSheet; }
|
|
1578
|
+
static get observedAttributes() { return ['open']; }
|
|
1579
|
+
|
|
1580
|
+
render() {
|
|
1581
|
+
this._backdrop = this.createElement('div', ['nk-sidebar-backdrop']);
|
|
1582
|
+
this._aside = this.createElement('aside', ['nk-sidebar']);
|
|
1583
|
+
this._aside.appendChild(this.createElement('slot', [], { name: 'workspace' }));
|
|
1584
|
+
const scroll = this.createElement('div', ['nk-sidebar-scroll']);
|
|
1585
|
+
scroll.appendChild(document.createElement('slot'));
|
|
1586
|
+
this._aside.appendChild(scroll);
|
|
1587
|
+
this._footer = this.createElement('div', ['nk-sidebar-footer']);
|
|
1588
|
+
this._footerSlot = this.createElement('slot', [], { name: 'footer' });
|
|
1589
|
+
this._footer.appendChild(this._footerSlot);
|
|
1590
|
+
this._aside.appendChild(this._footer);
|
|
1591
|
+
this._wrapper.appendChild(this._backdrop);
|
|
1592
|
+
this._wrapper.appendChild(this._aside);
|
|
1593
|
+
this._syncFooter();
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
_syncFooter() {
|
|
1597
|
+
const nodes = this._footerSlot.assignedElements();
|
|
1598
|
+
// The footer box carries padding and a border; only render it when used.
|
|
1599
|
+
this._footer.style.display = nodes.length ? '' : 'none';
|
|
1600
|
+
// Footer rows are 26px in the class version (.nk-sidebar-footer .nk-tree-item).
|
|
1601
|
+
for (const el of nodes) if (el.localName === 'nk-tree-item') el.setAttribute('compact', '');
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
setupEvents() {
|
|
1605
|
+
this._onSlot = () => this._syncFooter();
|
|
1606
|
+
this._onBackdrop = () => this.close();
|
|
1607
|
+
this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) this.close(); };
|
|
1608
|
+
this._footerSlot.addEventListener('slotchange', this._onSlot);
|
|
1609
|
+
this._backdrop.addEventListener('click', this._onBackdrop);
|
|
1610
|
+
document.addEventListener('keydown', this._onKey);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
teardownEvents() {
|
|
1614
|
+
this._footerSlot?.removeEventListener('slotchange', this._onSlot);
|
|
1615
|
+
this._backdrop?.removeEventListener('click', this._onBackdrop);
|
|
1616
|
+
document.removeEventListener('keydown', this._onKey);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
onAttributeChanged(name) {
|
|
1620
|
+
if (name === 'open') this.emit('nk-toggle', { open: this.getBoolAttr('open') });
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
show() { this.setBoolAttr('open', true); }
|
|
1624
|
+
close() { this.setBoolAttr('open', false); }
|
|
1625
|
+
toggle() { this.setBoolAttr('open', !this.getBoolAttr('open')); }
|
|
1626
|
+
|
|
1627
|
+
get open() { return this.getBoolAttr('open'); }
|
|
1628
|
+
set open(v) { this.setBoolAttr('open', v); }
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
customElements.define('nk-sidebar', NkSidebar);
|
|
1632
|
+
|
|
1633
|
+
// <nk-workspace-switcher slot="workspace" name="Acme" avatar="A">
|
|
1634
|
+
// <nk-menu slot="menu">…</nk-menu>
|
|
1635
|
+
// </nk-workspace-switcher>
|
|
1636
|
+
// → <div class="nk-workspace"><div class="avatar">A</div><span>Acme</span><span class="chev">⌄</span></div>
|
|
1637
|
+
// A click toggles `open` and shows whatever sits in the menu slot below the
|
|
1638
|
+
// row; clicking outside or pressing Escape closes it.
|
|
1639
|
+
const menuSheet = new CSSStyleSheet();
|
|
1640
|
+
menuSheet.replaceSync(`
|
|
1641
|
+
.nk-ws-anchor { position: relative; }
|
|
1642
|
+
.nk-ws-menu { display: none; position: absolute; left: 8px; top: calc(100% - 2px); z-index: 50; }
|
|
1643
|
+
:host([open]) .nk-ws-menu { display: block; }
|
|
1644
|
+
`);
|
|
1645
|
+
|
|
1646
|
+
class NkWorkspaceSwitcher extends NkElement {
|
|
1647
|
+
static get hostStyles() { return menuSheet; }
|
|
1648
|
+
static get observedAttributes() { return ['name', 'avatar', 'open']; }
|
|
1649
|
+
|
|
1650
|
+
render() {
|
|
1651
|
+
this._row = this.createElement('div', ['nk-workspace'], { role: 'button', tabindex: '0', 'aria-haspopup': 'menu' });
|
|
1652
|
+
this._avatar = this.createElement('div', ['avatar']);
|
|
1653
|
+
const avatarSlot = this.createElement('slot', [], { name: 'avatar' });
|
|
1654
|
+
avatarSlot.appendChild(this._avatar);
|
|
1655
|
+
this._name = document.createElement('span');
|
|
1656
|
+
const chev = this.createElement('span', ['chev']);
|
|
1657
|
+
chev.textContent = '⌄';
|
|
1658
|
+
this._row.append(avatarSlot, this._name, chev);
|
|
1659
|
+
this._menu = this.createElement('div', ['nk-ws-menu']);
|
|
1660
|
+
this._menu.appendChild(this.createElement('slot', [], { name: 'menu' }));
|
|
1661
|
+
const anchor = this.createElement('div', ['nk-ws-anchor']);
|
|
1662
|
+
anchor.append(this._row, this._menu);
|
|
1663
|
+
this._wrapper.appendChild(anchor);
|
|
1664
|
+
this._sync();
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
_sync() {
|
|
1668
|
+
const name = this.getAttribute('name') || '';
|
|
1669
|
+
this._name.textContent = name;
|
|
1670
|
+
this._avatar.textContent = this.getAttribute('avatar') || name.trim().charAt(0).toUpperCase();
|
|
1671
|
+
this._row.setAttribute('aria-expanded', this.getBoolAttr('open') ? 'true' : 'false');
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
setupEvents() {
|
|
1675
|
+
this._onClick = () => this.toggle();
|
|
1676
|
+
this._onKey = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.toggle(); } };
|
|
1677
|
+
this._onDocClick = (e) => { if (this.getBoolAttr('open') && !e.composedPath().includes(this)) this.close(); };
|
|
1678
|
+
this._onDocKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) this.close(); };
|
|
1679
|
+
this._onMenuSelect = () => this.close();
|
|
1680
|
+
this._row.addEventListener('click', this._onClick);
|
|
1681
|
+
this._row.addEventListener('keydown', this._onKey);
|
|
1682
|
+
this._menu.addEventListener('nk-select', this._onMenuSelect);
|
|
1683
|
+
document.addEventListener('click', this._onDocClick);
|
|
1684
|
+
document.addEventListener('keydown', this._onDocKey);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
teardownEvents() {
|
|
1688
|
+
this._row?.removeEventListener('click', this._onClick);
|
|
1689
|
+
this._row?.removeEventListener('keydown', this._onKey);
|
|
1690
|
+
this._menu?.removeEventListener('nk-select', this._onMenuSelect);
|
|
1691
|
+
document.removeEventListener('click', this._onDocClick);
|
|
1692
|
+
document.removeEventListener('keydown', this._onDocKey);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
onAttributeChanged(name) {
|
|
1696
|
+
this._sync();
|
|
1697
|
+
if (name === 'open') this.emit('nk-toggle', { open: this.getBoolAttr('open') });
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
show() { this.setBoolAttr('open', true); }
|
|
1701
|
+
close() { this.setBoolAttr('open', false); }
|
|
1702
|
+
toggle() { this.setBoolAttr('open', !this.getBoolAttr('open')); }
|
|
1703
|
+
|
|
1704
|
+
get name() { return this.getAttribute('name'); }
|
|
1705
|
+
set name(v) { this.setAttribute('name', v); }
|
|
1706
|
+
get open() { return this.getBoolAttr('open'); }
|
|
1707
|
+
set open(v) { this.setBoolAttr('open', v); }
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
customElements.define('nk-workspace-switcher', NkWorkspaceSwitcher);
|
|
1711
|
+
|
|
1712
|
+
// <nk-section-label addable>Favourites</nk-section-label>
|
|
1713
|
+
// → <div class="nk-section-label">Favourites <span class="plus">+</span></div>
|
|
1714
|
+
// The + appears on hover (a hover-parent rule, so it lives in the same tree)
|
|
1715
|
+
// and fires nk-action { action: 'add' }.
|
|
1716
|
+
class NkSectionLabel extends NkElement {
|
|
1717
|
+
static get observedAttributes() { return ['label', 'addable']; }
|
|
1718
|
+
|
|
1719
|
+
render() {
|
|
1720
|
+
this._box = this.createElement('div', ['nk-section-label']);
|
|
1721
|
+
this._text = document.createElement('span');
|
|
1722
|
+
this._text.appendChild(document.createElement('slot'));
|
|
1723
|
+
this._plus = this.createElement('span', ['plus'], { role: 'button', title: '+' });
|
|
1724
|
+
this._plus.textContent = '+';
|
|
1725
|
+
this._box.append(this._text, this._plus);
|
|
1726
|
+
this._wrapper.appendChild(this._box);
|
|
1727
|
+
this._sync();
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
_sync() {
|
|
1731
|
+
const label = this.getAttribute('label');
|
|
1732
|
+
if (label !== null) this._text.textContent = label;
|
|
1733
|
+
this._plus.style.display = this.getBoolAttr('addable') ? '' : 'none';
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
setupEvents() {
|
|
1737
|
+
this._onPlus = (e) => { e.stopPropagation(); this.emit('nk-action', { action: 'add' }); };
|
|
1738
|
+
this._plus.addEventListener('click', this._onPlus);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
teardownEvents() { this._plus?.removeEventListener('click', this._onPlus); }
|
|
1742
|
+
|
|
1743
|
+
onAttributeChanged() { this._sync(); }
|
|
1744
|
+
|
|
1745
|
+
get addable() { return this.getBoolAttr('addable'); }
|
|
1746
|
+
set addable(v) { this.setBoolAttr('addable', v); }
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
customElements.define('nk-section-label', NkSectionLabel);
|
|
1750
|
+
|
|
1751
|
+
// <nk-tree>
|
|
1752
|
+
// <nk-section-label addable>Favourites</nk-section-label>
|
|
1753
|
+
// <nk-tree-item icon="📊" open>Project overview
|
|
1754
|
+
// <nk-tree-item icon="🚀" active>NotionKit MVP</nk-tree-item>
|
|
1755
|
+
// </nk-tree-item>
|
|
1756
|
+
// </nk-tree>
|
|
1757
|
+
//
|
|
1758
|
+
// A container for tree items: keeps a single `active` item (listening to
|
|
1759
|
+
// nk-select from any depth), gives the group one tab stop with arrow-key
|
|
1760
|
+
// navigation, and can render items from data:
|
|
1761
|
+
// tree.data = [{ label, icon, value, href, open, active, children: [...] }]
|
|
1762
|
+
const KEYS$2 = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End']);
|
|
1763
|
+
|
|
1764
|
+
class NkTree extends NkElement {
|
|
1765
|
+
static get observedAttributes() { return ['manual']; }
|
|
1766
|
+
|
|
1767
|
+
render() {
|
|
1768
|
+
const box = this.createElement('div', [], { role: 'tree' });
|
|
1769
|
+
box.appendChild(document.createElement('slot'));
|
|
1770
|
+
this._wrapper.appendChild(box);
|
|
1771
|
+
this._box = box;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/** Makes `item` the single active item (called by the item after nk-select was not cancelled). */
|
|
1775
|
+
setActive(item) {
|
|
1776
|
+
if (this.getBoolAttr('manual')) return;
|
|
1777
|
+
for (const other of this.querySelectorAll('nk-tree-item[active]')) if (other !== item) other.removeAttribute('active');
|
|
1778
|
+
item.setAttribute('active', '');
|
|
1779
|
+
this._syncTabIndex(item);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
setupEvents() {
|
|
1783
|
+
this._onKey = (e) => {
|
|
1784
|
+
if (!KEYS$2.has(e.key) || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
1785
|
+
const item = e.target.closest?.('nk-tree-item');
|
|
1786
|
+
if (!item) return;
|
|
1787
|
+
const visible = this.visibleItems();
|
|
1788
|
+
const i = visible.indexOf(item);
|
|
1789
|
+
if (i < 0) return;
|
|
1790
|
+
let next = null;
|
|
1791
|
+
switch (e.key) {
|
|
1792
|
+
case 'ArrowDown': next = visible[i + 1]; break;
|
|
1793
|
+
case 'ArrowUp': next = visible[i - 1]; break;
|
|
1794
|
+
case 'Home': next = visible[0]; break;
|
|
1795
|
+
case 'End': next = visible[visible.length - 1]; break;
|
|
1796
|
+
case 'ArrowRight':
|
|
1797
|
+
if (item.hasChildren && !item.open) item.open = true;
|
|
1798
|
+
else if (item.hasChildren) next = item.querySelector('nk-tree-item');
|
|
1799
|
+
break;
|
|
1800
|
+
case 'ArrowLeft':
|
|
1801
|
+
if (item.hasChildren && item.open) item.open = false;
|
|
1802
|
+
else next = item.parentElement?.closest('nk-tree-item');
|
|
1803
|
+
break;
|
|
1804
|
+
}
|
|
1805
|
+
e.preventDefault();
|
|
1806
|
+
if (next) { this._syncTabIndex(next); next.focus(); }
|
|
1807
|
+
};
|
|
1808
|
+
this._onSlot = () => this._syncTabIndex();
|
|
1809
|
+
this.addEventListener('keydown', this._onKey);
|
|
1810
|
+
this._box.querySelector('slot').addEventListener('slotchange', this._onSlot);
|
|
1811
|
+
this._syncTabIndex();
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
teardownEvents() {
|
|
1815
|
+
this.removeEventListener('keydown', this._onKey);
|
|
1816
|
+
this._box?.querySelector('slot').removeEventListener('slotchange', this._onSlot);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
/** Items whose every ancestor item inside this tree is open – i.e. rendered. */
|
|
1820
|
+
visibleItems() {
|
|
1821
|
+
return [...this.querySelectorAll('nk-tree-item')].filter(item => {
|
|
1822
|
+
for (let p = item.parentElement?.closest('nk-tree-item'); p && this.contains(p); p = p.parentElement?.closest('nk-tree-item')) {
|
|
1823
|
+
if (!p.open) return false;
|
|
1824
|
+
}
|
|
1825
|
+
return true;
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/** Items upgrade after the tree; each one asks for a resync once rendered. */
|
|
1830
|
+
requestSync() {
|
|
1831
|
+
if (this._syncQueued) return;
|
|
1832
|
+
this._syncQueued = true;
|
|
1833
|
+
queueMicrotask(() => { this._syncQueued = false; this._syncTabIndex(); });
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
_syncTabIndex(focusable) {
|
|
1837
|
+
const items = [...this.querySelectorAll('nk-tree-item')];
|
|
1838
|
+
if (!items.length) return;
|
|
1839
|
+
const visible = this.visibleItems();
|
|
1840
|
+
const target = focusable || visible.find(i => i.active) || visible[0];
|
|
1841
|
+
// A method call, not a property write: assigning a property to an element
|
|
1842
|
+
// that has not been upgraded yet would create an own property that shadows
|
|
1843
|
+
// the accessor forever.
|
|
1844
|
+
for (const item of items) item.setTabbable?.(item === target);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/** Renders items from data into the light DOM (replacing existing items). */
|
|
1848
|
+
set data(list) {
|
|
1849
|
+
this._data = list;
|
|
1850
|
+
const build = (nodes) => nodes.map(n => {
|
|
1851
|
+
const item = document.createElement('nk-tree-item');
|
|
1852
|
+
if (n.icon) item.setAttribute('icon', n.icon);
|
|
1853
|
+
if (n.value != null) item.setAttribute('value', n.value);
|
|
1854
|
+
if (n.href) item.setAttribute('href', n.href);
|
|
1855
|
+
if (n.open) item.setAttribute('open', '');
|
|
1856
|
+
if (n.active) item.setAttribute('active', '');
|
|
1857
|
+
item.append(document.createTextNode(n.label ?? ''), ...build(n.children || []));
|
|
1858
|
+
return item;
|
|
1859
|
+
});
|
|
1860
|
+
for (const old of this.querySelectorAll(':scope > nk-tree-item')) old.remove();
|
|
1861
|
+
this.append(...build(list || []));
|
|
1862
|
+
}
|
|
1863
|
+
get data() { return this._data; }
|
|
1864
|
+
|
|
1865
|
+
get activeItem() { return this.querySelector('nk-tree-item[active]'); }
|
|
1866
|
+
get value() { return this.activeItem?.value ?? null; }
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
customElements.define('nk-tree', NkTree);
|
|
1870
|
+
|
|
1871
|
+
// <nk-tree-item icon="📊" value="overview" open>
|
|
1872
|
+
// Project overview
|
|
1873
|
+
// <nk-tree-item icon="🚀" value="mvp" active>NotionKit MVP</nk-tree-item>
|
|
1874
|
+
// </nk-tree-item>
|
|
1875
|
+
//
|
|
1876
|
+
// → <div class="nk-tree-item active"><span class="nk-toggle-arrow open">▸</span>
|
|
1877
|
+
// <span class="icon">📊</span><span class="label">…</span>
|
|
1878
|
+
// <span class="actions"><span>+</span><span>⋯</span></span></div>
|
|
1879
|
+
// <div class="nk-tree-children">…</div>
|
|
1880
|
+
//
|
|
1881
|
+
// Both halves – the row and the .nk-tree-children box that must be its next
|
|
1882
|
+
// sibling – are rendered here. Slots are assigned manually: text becomes the
|
|
1883
|
+
// label, nested <nk-tree-item>s become the children, `slot="icon"` /
|
|
1884
|
+
// `slot="end"` nodes go where they say. The hover actions are shadow-internal
|
|
1885
|
+
// (`.nk-tree-item:hover .actions` is a hover-parent rule) and report through
|
|
1886
|
+
// nk-action.
|
|
1887
|
+
class NkTreeItem extends NkElement {
|
|
1888
|
+
static get slotAssignment() { return 'manual'; }
|
|
1889
|
+
static get observedAttributes() { return ['label', 'icon', 'value', 'href', 'active', 'open', 'compact', 'no-actions']; }
|
|
1890
|
+
|
|
1891
|
+
render() {
|
|
1892
|
+
this._row = this.createElement('div', ['nk-tree-item'], { role: 'treeitem', tabindex: '-1' });
|
|
1893
|
+
this._arrow = this.createElement('span', ['nk-toggle-arrow'], { 'aria-hidden': 'true' });
|
|
1894
|
+
this._arrow.textContent = '▸';
|
|
1895
|
+
this._iconSlot = this.createElement('slot', [], { name: 'icon' });
|
|
1896
|
+
this._icon = this.createElement('span', ['icon']);
|
|
1897
|
+
this._iconSlot.appendChild(this._icon);
|
|
1898
|
+
this._label = this.createElement('span', ['label']);
|
|
1899
|
+
this._labelSlot = this.createElement('slot', [], { name: 'label' });
|
|
1900
|
+
this._labelText = document.createTextNode('');
|
|
1901
|
+
this._labelSlot.appendChild(this._labelText);
|
|
1902
|
+
this._label.appendChild(this._labelSlot);
|
|
1903
|
+
this._actions = this.createElement('span', ['actions']);
|
|
1904
|
+
for (const [action, glyph, title] of [['add', '+', 'Add'], ['more', '⋯', 'More']]) {
|
|
1905
|
+
const b = this.createElement('span', [], { 'data-action': action, role: 'button', title });
|
|
1906
|
+
b.textContent = glyph;
|
|
1907
|
+
this._actions.appendChild(b);
|
|
1908
|
+
}
|
|
1909
|
+
this._endSlot = this.createElement('slot', [], { name: 'end' });
|
|
1910
|
+
this._row.append(this._arrow, this._iconSlot, this._label, this._actions, this._endSlot);
|
|
1911
|
+
this._children = this.createElement('div', ['nk-tree-children'], { role: 'group' });
|
|
1912
|
+
this._childSlot = this.createElement('slot', [], { name: 'children' });
|
|
1913
|
+
this._children.appendChild(this._childSlot);
|
|
1914
|
+
this._wrapper.append(this._row, this._children);
|
|
1915
|
+
this._sync();
|
|
1916
|
+
this.closest('nk-tree')?.requestSync?.();
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
/** Routes light-DOM nodes: nested items → children, slot="icon"/"end" → there, the rest → label. */
|
|
1920
|
+
assignSlots() {
|
|
1921
|
+
if (!this._row) return;
|
|
1922
|
+
const icon = [], end = [], children = [], label = [];
|
|
1923
|
+
for (const node of this.childNodes) {
|
|
1924
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1925
|
+
const slot = node.getAttribute('slot');
|
|
1926
|
+
if (slot === 'icon') icon.push(node);
|
|
1927
|
+
else if (slot === 'end') end.push(node);
|
|
1928
|
+
else if (node.localName === 'nk-tree-item') children.push(node);
|
|
1929
|
+
else label.push(node);
|
|
1930
|
+
} else if (node.nodeType === Node.TEXT_NODE && node.data.trim()) {
|
|
1931
|
+
label.push(node);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
this._iconSlot.assign(...icon);
|
|
1935
|
+
this._endSlot.assign(...end);
|
|
1936
|
+
this._childSlot.assign(...children);
|
|
1937
|
+
this._labelSlot.assign(...(this.hasAttribute('label') ? [] : label));
|
|
1938
|
+
this._hasChildren = children.length > 0;
|
|
1939
|
+
this._hasEnd = end.length > 0;
|
|
1940
|
+
this._sync();
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
_sync() {
|
|
1944
|
+
const active = this.getBoolAttr('active'), open = this.getBoolAttr('open');
|
|
1945
|
+
this._row.classList.toggle('active', active);
|
|
1946
|
+
this._row.classList.toggle('compact', this.getBoolAttr('compact'));
|
|
1947
|
+
this._row.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
1948
|
+
this._arrow.classList.toggle('open', open);
|
|
1949
|
+
this._arrow.style.display = this._hasChildren ? '' : 'none';
|
|
1950
|
+
if (this._hasChildren) this._row.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
1951
|
+
else this._row.removeAttribute('aria-expanded');
|
|
1952
|
+
this._children.classList.toggle('collapsed', !open || !this._hasChildren);
|
|
1953
|
+
this._icon.textContent = this.getAttribute('icon') || '';
|
|
1954
|
+
this._icon.style.display = this.getAttribute('icon') ? '' : 'none';
|
|
1955
|
+
this._labelText.data = this.getAttribute('label') || '';
|
|
1956
|
+
// .actions is display:none until hover (a hover-parent rule); `hidden`
|
|
1957
|
+
// would lose to that rule, so the block is detached instead.
|
|
1958
|
+
const wantActions = !this.getBoolAttr('no-actions') && !this._hasEnd;
|
|
1959
|
+
if (wantActions && !this._actions.isConnected) this._row.insertBefore(this._actions, this._endSlot);
|
|
1960
|
+
if (!wantActions && this._actions.isConnected) this._actions.remove();
|
|
1961
|
+
const href = this.getAttribute('href');
|
|
1962
|
+
href ? this._row.setAttribute('data-href', href) : this._row.removeAttribute('data-href');
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
setupEvents() {
|
|
1966
|
+
this._onRowClick = (e) => {
|
|
1967
|
+
const action = e.target.closest?.('[data-action]');
|
|
1968
|
+
if (action) { e.stopPropagation(); this.emit('nk-action', { action: action.dataset.action, value: this.value, label: this.label }); return; }
|
|
1969
|
+
if (e.target === this._arrow || this._arrow.contains(e.target)) { e.stopPropagation(); this.toggle(); return; }
|
|
1970
|
+
this.select();
|
|
1971
|
+
};
|
|
1972
|
+
this._onKey = (e) => {
|
|
1973
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.select(); }
|
|
1974
|
+
};
|
|
1975
|
+
this._row.addEventListener('click', this._onRowClick);
|
|
1976
|
+
this._row.addEventListener('keydown', this._onKey);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
teardownEvents() {
|
|
1980
|
+
this._row?.removeEventListener('click', this._onRowClick);
|
|
1981
|
+
this._row?.removeEventListener('keydown', this._onKey);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
onAttributeChanged(name) {
|
|
1985
|
+
if (name === 'label') this.assignSlots();
|
|
1986
|
+
else this._sync();
|
|
1987
|
+
if (name === 'open') this.emit('nk-toggle', { open: this.getBoolAttr('open'), value: this.value });
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
/** Fires nk-select; a listener may preventDefault to keep the item from becoming active. */
|
|
1991
|
+
select() {
|
|
1992
|
+
const ok = this.emit('nk-select', { value: this.value, label: this.label, href: this.getAttribute('href'), item: this });
|
|
1993
|
+
if (!ok) return;
|
|
1994
|
+
const tree = this.closest('nk-tree');
|
|
1995
|
+
if (tree) tree.setActive(this);
|
|
1996
|
+
else this.setBoolAttr('active', true);
|
|
1997
|
+
if (this.getAttribute('href')) location.assign(this.getAttribute('href'));
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
toggle() { this.setBoolAttr('open', !this.getBoolAttr('open')); }
|
|
2001
|
+
focus(options) { this._row?.focus(options); }
|
|
2002
|
+
/** Roving tabindex, managed by <nk-tree>. */
|
|
2003
|
+
setTabbable(v) { if (this._row) this._row.tabIndex = v ? 0 : -1; }
|
|
2004
|
+
get tabbable() { return this._row?.tabIndex === 0; }
|
|
2005
|
+
get hasChildren() { return !!this._hasChildren; }
|
|
2006
|
+
|
|
2007
|
+
get label() { return this.getAttribute('label') ?? this._labelSlot?.assignedNodes().map(n => n.textContent).join('').trim() ?? ''; }
|
|
2008
|
+
set label(v) { this.setAttribute('label', v); }
|
|
2009
|
+
get value() { return this.getAttribute('value') ?? this.label; }
|
|
2010
|
+
set value(v) { this.setAttribute('value', v); }
|
|
2011
|
+
get icon() { return this.getAttribute('icon'); }
|
|
2012
|
+
set icon(v) { v == null ? this.removeAttribute('icon') : this.setAttribute('icon', v); }
|
|
2013
|
+
get active() { return this.getBoolAttr('active'); }
|
|
2014
|
+
set active(v) { this.setBoolAttr('active', v); }
|
|
2015
|
+
get open() { return this.getBoolAttr('open'); }
|
|
2016
|
+
set open(v) { this.setBoolAttr('open', v); }
|
|
2017
|
+
get compact() { return this.getBoolAttr('compact'); }
|
|
2018
|
+
set compact(v) { this.setBoolAttr('compact', v); }
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
customElements.define('nk-tree-item', NkTreeItem);
|
|
2022
|
+
|
|
2023
|
+
// <nk-topbar>
|
|
2024
|
+
// <nk-breadcrumb>…</nk-breadcrumb>
|
|
2025
|
+
// <nk-btn slot="actions" variant="share">Share</nk-btn>
|
|
2026
|
+
// <nk-theme-toggle slot="actions"></nk-theme-toggle>
|
|
2027
|
+
// </nk-topbar>
|
|
2028
|
+
// → <div class="nk-topbar">…<div class="nk-topbar-actions">…</div></div>
|
|
2029
|
+
// display:contents host, so the bar is a direct flex child of .nk-main.
|
|
2030
|
+
class NkTopbar extends NkElement {
|
|
2031
|
+
|
|
2032
|
+
render() {
|
|
2033
|
+
const bar = this.createElement('div', ['nk-topbar']);
|
|
2034
|
+
bar.appendChild(document.createElement('slot'));
|
|
2035
|
+
const actions = this.createElement('div', ['nk-topbar-actions']);
|
|
2036
|
+
actions.appendChild(this.createElement('slot', [], { name: 'actions' }));
|
|
2037
|
+
bar.appendChild(actions);
|
|
2038
|
+
this._wrapper.appendChild(bar);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
customElements.define('nk-topbar', NkTopbar);
|
|
2043
|
+
|
|
2044
|
+
// <nk-breadcrumb>
|
|
2045
|
+
// <a href="/overview">📊 Project overview</a>
|
|
2046
|
+
// <span>🚀 NotionKit MVP</span>
|
|
2047
|
+
// </nk-breadcrumb>
|
|
2048
|
+
// → <nav class="nk-breadcrumb"><a class="crumb">…</a><span class="sep">/</span><span class="crumb current">…</span></nav>
|
|
2049
|
+
//
|
|
2050
|
+
// The children are *cloned* into the shadow tree so the separators can be
|
|
2051
|
+
// interleaved and `.crumb:hover` (no ::slotted twin) applies. A
|
|
2052
|
+
// MutationObserver keeps the copy in step. Clicking a clone fires nk-select
|
|
2053
|
+
// and, unless prevented, forwards the click to the original – so an <a>
|
|
2054
|
+
// navigates exactly once and framework listeners see it.
|
|
2055
|
+
class NkBreadcrumb extends NkElement {
|
|
2056
|
+
static get observedAttributes() { return ['separator']; }
|
|
2057
|
+
static get observesLightDom() { return true; }
|
|
2058
|
+
|
|
2059
|
+
render() {
|
|
2060
|
+
this._nav = this.createElement('nav', ['nk-breadcrumb'], { 'aria-label': 'Breadcrumb' });
|
|
2061
|
+
this._wrapper.appendChild(this._nav);
|
|
2062
|
+
this.projectLightDom();
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
projectLightDom() {
|
|
2066
|
+
if (!this._nav) return;
|
|
2067
|
+
const originals = [...this.children];
|
|
2068
|
+
const sep = this.getAttribute('separator') ?? '/';
|
|
2069
|
+
const signature = originals.map(o => o.outerHTML).join('') + sep;
|
|
2070
|
+
if (signature === this._signature) return;
|
|
2071
|
+
this._signature = signature;
|
|
2072
|
+
this._nav.innerHTML = '';
|
|
2073
|
+
const explicitCurrent = originals.some(o => o.hasAttribute('current') || o.classList.contains('current'));
|
|
2074
|
+
originals.forEach((original, i) => {
|
|
2075
|
+
if (i > 0) {
|
|
2076
|
+
const s = this.createElement('span', ['sep'], { 'aria-hidden': 'true' });
|
|
2077
|
+
s.textContent = sep;
|
|
2078
|
+
this._nav.appendChild(s);
|
|
2079
|
+
}
|
|
2080
|
+
const clone = original.cloneNode(true);
|
|
2081
|
+
clone.removeAttribute('slot');
|
|
2082
|
+
clone.classList.add('crumb');
|
|
2083
|
+
const current = explicitCurrent ? (original.hasAttribute('current') || original.classList.contains('current')) : i === originals.length - 1;
|
|
2084
|
+
clone.classList.toggle('current', current);
|
|
2085
|
+
if (current) clone.setAttribute('aria-current', 'page');
|
|
2086
|
+
clone.dataset.index = i;
|
|
2087
|
+
this._nav.appendChild(clone);
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
setupEvents() {
|
|
2092
|
+
this._onClick = (e) => {
|
|
2093
|
+
const clone = e.target.closest('.crumb');
|
|
2094
|
+
if (!clone) return;
|
|
2095
|
+
e.preventDefault();
|
|
2096
|
+
const index = Number(clone.dataset.index);
|
|
2097
|
+
// Look the original up at click time: a framework may have replaced it.
|
|
2098
|
+
const original = this.children[index];
|
|
2099
|
+
const ok = this.emit('nk-select', {
|
|
2100
|
+
index, label: clone.textContent.trim(), href: original?.getAttribute('href') ?? null,
|
|
2101
|
+
value: original?.dataset.value ?? original?.getAttribute('href') ?? clone.textContent.trim(),
|
|
2102
|
+
current: clone.classList.contains('current'),
|
|
2103
|
+
});
|
|
2104
|
+
if (ok) original?.click();
|
|
2105
|
+
};
|
|
2106
|
+
this._nav.addEventListener('click', this._onClick);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
teardownEvents() { this._nav?.removeEventListener('click', this._onClick); }
|
|
2110
|
+
|
|
2111
|
+
onAttributeChanged() { this.projectLightDom(); }
|
|
2112
|
+
|
|
2113
|
+
get separator() { return this.getAttribute('separator') ?? '/'; }
|
|
2114
|
+
set separator(v) { this.setAttribute('separator', v); }
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
customElements.define('nk-breadcrumb', NkBreadcrumb);
|
|
2118
|
+
|
|
2119
|
+
// <nk-theme-toggle slot="actions"></nk-theme-toggle>
|
|
2120
|
+
// → <button class="nk-topbar-btn nk-theme-toggle">🌙</button>
|
|
2121
|
+
// Flips data-theme on <html> (the only theme source), remembers the choice in
|
|
2122
|
+
// localStorage (`storage-key`, default nk-theme), applies a stored or
|
|
2123
|
+
// system preference on first connect when <html> carries no theme yet, and
|
|
2124
|
+
// accepts postMessage({ nkTheme }) from a parent page (the landing page's
|
|
2125
|
+
// embedded demo uses that).
|
|
2126
|
+
class NkThemeToggle extends NkElement {
|
|
2127
|
+
static get observedAttributes() { return ['storage-key', 'title'];
|
|
2128
|
+
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
render() {
|
|
2132
|
+
this._btn = this.createElement('button', ['nk-topbar-btn', 'nk-theme-toggle'], { type: 'button' });
|
|
2133
|
+
this._btn.title = this.getAttribute('title') || 'Toggle light / dark';
|
|
2134
|
+
this._wrapper.appendChild(this._btn);
|
|
2135
|
+
const root = document.documentElement;
|
|
2136
|
+
let stored = null;
|
|
2137
|
+
try { stored = localStorage.getItem(this.storageKey); } catch {}
|
|
2138
|
+
if (stored === 'light' || stored === 'dark') root.setAttribute('data-theme', stored);
|
|
2139
|
+
else if (!root.hasAttribute('data-theme')) root.setAttribute('data-theme', matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
2140
|
+
this._syncGlyph();
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
_syncGlyph() {
|
|
2144
|
+
if (this._btn) this._btn.textContent = getCurrentTheme() === 'dark' ? '☀️' : '🌙';
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
_syncTheme(theme) {
|
|
2148
|
+
super._syncTheme(theme);
|
|
2149
|
+
this._syncGlyph();
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
setupEvents() {
|
|
2153
|
+
this._onClick = () => this.apply(getCurrentTheme() === 'dark' ? 'light' : 'dark');
|
|
2154
|
+
this._onMessage = (e) => { const t = e.data?.nkTheme; if (t === 'light' || t === 'dark') this.apply(t); };
|
|
2155
|
+
this._btn.addEventListener('click', this._onClick);
|
|
2156
|
+
window.addEventListener('message', this._onMessage);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
teardownEvents() {
|
|
2160
|
+
this._btn?.removeEventListener('click', this._onClick);
|
|
2161
|
+
window.removeEventListener('message', this._onMessage);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
onAttributeChanged(name) {
|
|
2165
|
+
if (name === 'title' && this._btn) this._btn.title = this.getAttribute('title') || 'Toggle light / dark';
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/** Sets the theme on <html>, persists it and reports it. */
|
|
2169
|
+
apply(theme) {
|
|
2170
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
2171
|
+
try { localStorage.setItem(this.storageKey, theme); } catch {}
|
|
2172
|
+
this._syncGlyph();
|
|
2173
|
+
this.emit('nk-change', { value: theme });
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
get storageKey() { return this.getAttribute('storage-key') || 'nk-theme'; }
|
|
2177
|
+
get value() { return getCurrentTheme(); }
|
|
2178
|
+
set value(v) { this.apply(v); }
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
customElements.define('nk-theme-toggle', NkThemeToggle);
|
|
2182
|
+
|
|
2183
|
+
// <nk-page icon="🚀" cover>
|
|
2184
|
+
// <nk-page-title>NotionKit MVP</nk-page-title>
|
|
2185
|
+
// <nk-page-actions>…</nk-page-actions>
|
|
2186
|
+
// <p class="lead">…</p>
|
|
2187
|
+
// …blocks…
|
|
2188
|
+
// </nk-page>
|
|
2189
|
+
// → <div class="nk-page-scroll"><div class="nk-cover"></div><div class="nk-page">
|
|
2190
|
+
// <div class="nk-page-icon">🚀</div>…</div></div>
|
|
2191
|
+
//
|
|
2192
|
+
// The host is display:contents, so .nk-page-scroll is the flex child of
|
|
2193
|
+
// .nk-main that scrolls. `narrow` drops the scroll wrapper for pages that are
|
|
2194
|
+
// the document itself (landing / docs skeleton). The page icon is rendered
|
|
2195
|
+
// here because its ::slotted twin is keyed on the parent (.nk-page).
|
|
2196
|
+
class NkPage extends NkElement {
|
|
2197
|
+
static get observedAttributes() { return ['icon', 'cover', 'narrow']; }
|
|
2198
|
+
|
|
2199
|
+
render() {
|
|
2200
|
+
this._page = this.createElement('div', ['nk-page']);
|
|
2201
|
+
this._icon = this.createElement('div', ['nk-page-icon'], { role: 'button', title: 'Change icon' });
|
|
2202
|
+
const iconSlot = this.createElement('slot', [], { name: 'icon' });
|
|
2203
|
+
iconSlot.appendChild(this._icon);
|
|
2204
|
+
this._page.appendChild(iconSlot);
|
|
2205
|
+
this._page.appendChild(document.createElement('slot'));
|
|
2206
|
+
this._cover = this.createElement('div', ['nk-cover']);
|
|
2207
|
+
this._coverSlot = this.createElement('slot', [], { name: 'cover' });
|
|
2208
|
+
this._coverSlot.appendChild(this._cover);
|
|
2209
|
+
this._build();
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
_build() {
|
|
2213
|
+
this._wrapper.replaceChildren();
|
|
2214
|
+
if (this.getBoolAttr('narrow')) {
|
|
2215
|
+
this._wrapper.append(this._coverSlot, this._page);
|
|
2216
|
+
} else {
|
|
2217
|
+
const scroll = this.createElement('div', ['nk-page-scroll']);
|
|
2218
|
+
scroll.append(this._coverSlot, this._page);
|
|
2219
|
+
this._wrapper.appendChild(scroll);
|
|
2220
|
+
}
|
|
2221
|
+
this._sync();
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
_sync() {
|
|
2225
|
+
const icon = this.getAttribute('icon');
|
|
2226
|
+
this._icon.textContent = icon || '';
|
|
2227
|
+
this._icon.style.display = icon ? '' : 'none';
|
|
2228
|
+
this._cover.style.display = this.getBoolAttr('cover') ? '' : 'none';
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
setupEvents() {
|
|
2232
|
+
this._onIcon = () => this.emit('nk-action', { action: 'icon', value: this.getAttribute('icon') });
|
|
2233
|
+
this._icon.addEventListener('click', this._onIcon);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
teardownEvents() { this._icon?.removeEventListener('click', this._onIcon); }
|
|
2237
|
+
|
|
2238
|
+
onAttributeChanged(name) {
|
|
2239
|
+
if (name === 'narrow') this._build();
|
|
2240
|
+
else this._sync();
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
get icon() { return this.getAttribute('icon'); }
|
|
2244
|
+
set icon(v) { v == null ? this.removeAttribute('icon') : this.setAttribute('icon', v); }
|
|
2245
|
+
get cover() { return this.getBoolAttr('cover'); }
|
|
2246
|
+
set cover(v) { this.setBoolAttr('cover', v); }
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
customElements.define('nk-page', NkPage);
|
|
2250
|
+
|
|
2251
|
+
// <nk-page-cover slot="cover"></nk-page-cover> → the token gradient
|
|
2252
|
+
// <nk-page-cover slot="cover" src="cover.jpg"></nk-page-cover> → an image
|
|
2253
|
+
class NkPageCover extends NkElement {
|
|
2254
|
+
static get observedAttributes() { return ['src']; }
|
|
2255
|
+
|
|
2256
|
+
render() {
|
|
2257
|
+
this._cover = this.createElement('div', ['nk-cover']);
|
|
2258
|
+
this._wrapper.appendChild(this._cover);
|
|
2259
|
+
this._sync();
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
_sync() {
|
|
2263
|
+
const src = this.getAttribute('src');
|
|
2264
|
+
if (src) {
|
|
2265
|
+
this._cover.style.backgroundImage = `url("${src}")`;
|
|
2266
|
+
this._cover.style.backgroundSize = 'cover';
|
|
2267
|
+
this._cover.style.backgroundPosition = 'center';
|
|
2268
|
+
} else {
|
|
2269
|
+
this._cover.style.backgroundImage = '';
|
|
2270
|
+
this._cover.style.backgroundSize = '';
|
|
2271
|
+
this._cover.style.backgroundPosition = '';
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
onAttributeChanged() { this._sync(); }
|
|
2276
|
+
|
|
2277
|
+
get src() { return this.getAttribute('src'); }
|
|
2278
|
+
set src(v) { v == null ? this.removeAttribute('src') : this.setAttribute('src', v); }
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
customElements.define('nk-page-cover', NkPageCover);
|
|
2282
|
+
|
|
2283
|
+
// <nk-page-title>NotionKit MVP</nk-page-title> → <h1 class="nk-page-title">
|
|
2284
|
+
// <nk-page-title editable placeholder="Untitled">…</nk-page-title>
|
|
2285
|
+
// With `editable` the text is copied into the shadow heading (contenteditable
|
|
2286
|
+
// cannot edit slotted nodes) and nk-change fires on blur / Enter.
|
|
2287
|
+
class NkPageTitle extends NkElement {
|
|
2288
|
+
static get observedAttributes() { return ['editable', 'placeholder', 'value']; }
|
|
2289
|
+
static get observesLightDom() { return true; }
|
|
2290
|
+
|
|
2291
|
+
render() {
|
|
2292
|
+
this._h1 = this.createElement('h1', ['nk-page-title']);
|
|
2293
|
+
this._slot = document.createElement('slot');
|
|
2294
|
+
this._h1.appendChild(this._slot);
|
|
2295
|
+
this._wrapper.appendChild(this._h1);
|
|
2296
|
+
this._sync();
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
_sync() {
|
|
2300
|
+
const editable = this.getBoolAttr('editable');
|
|
2301
|
+
if (editable) {
|
|
2302
|
+
if (this._slot.isConnected) this._slot.remove();
|
|
2303
|
+
this._h1.setAttribute('contenteditable', 'plaintext-only');
|
|
2304
|
+
this._h1.setAttribute('spellcheck', 'false');
|
|
2305
|
+
this._h1.setAttribute('role', 'textbox');
|
|
2306
|
+
this._h1.setAttribute('aria-label', 'Title');
|
|
2307
|
+
const text = this.getAttribute('value') ?? this.textContent.trim();
|
|
2308
|
+
if (this._h1.textContent !== text && document.activeElement !== this) this._h1.textContent = text;
|
|
2309
|
+
this._h1.setAttribute('data-placeholder', this.getAttribute('placeholder') || '');
|
|
2310
|
+
} else {
|
|
2311
|
+
this._h1.removeAttribute('contenteditable');
|
|
2312
|
+
this._h1.removeAttribute('role');
|
|
2313
|
+
if (this.hasAttribute('value')) { if (this._slot.isConnected) this._slot.remove(); this._h1.textContent = this.getAttribute('value'); }
|
|
2314
|
+
else if (!this._slot.isConnected) { this._h1.textContent = ''; this._h1.appendChild(this._slot); }
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
projectLightDom() { if (this._h1 && !this.hasAttribute('value')) this._sync(); }
|
|
2319
|
+
|
|
2320
|
+
setupEvents() {
|
|
2321
|
+
this._committed = this.value;
|
|
2322
|
+
this._onKey = (e) => { if (e.key === 'Enter') { e.preventDefault(); this._h1.blur(); } };
|
|
2323
|
+
this._onBlur = () => this._commit();
|
|
2324
|
+
this._h1.addEventListener('keydown', this._onKey);
|
|
2325
|
+
this._h1.addEventListener('blur', this._onBlur);
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
teardownEvents() {
|
|
2329
|
+
this._h1?.removeEventListener('keydown', this._onKey);
|
|
2330
|
+
this._h1?.removeEventListener('blur', this._onBlur);
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
_commit() {
|
|
2334
|
+
const value = this._h1.textContent.trim();
|
|
2335
|
+
if (value === this._committed) return;
|
|
2336
|
+
this._committed = value;
|
|
2337
|
+
this.emit('nk-change', { value });
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
onAttributeChanged() { this._sync(); }
|
|
2341
|
+
|
|
2342
|
+
get value() { return this.getBoolAttr('editable') ? (this._h1?.textContent.trim() ?? '') : (this.getAttribute('value') ?? this.textContent.trim()); }
|
|
2343
|
+
set value(v) { this.setAttribute('value', v); }
|
|
2344
|
+
get editable() { return this.getBoolAttr('editable'); }
|
|
2345
|
+
set editable(v) { this.setBoolAttr('editable', v); }
|
|
2346
|
+
focus(o) { this._h1?.focus(o); }
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
customElements.define('nk-page-title', NkPageTitle);
|
|
2350
|
+
|
|
2351
|
+
// <nk-page-actions><span>👤 Ada</span><span>📅 12 May</span><span>🏷️ <nk-tag color="purple">Design</nk-tag></span></nk-page-actions>
|
|
2352
|
+
// → <div class="nk-page-meta">…</div> – the meta row under the title
|
|
2353
|
+
class NkPageActions extends NkElement {
|
|
2354
|
+
render() {
|
|
2355
|
+
const row = this.createElement('div', ['nk-page-meta']);
|
|
2356
|
+
row.appendChild(document.createElement('slot'));
|
|
2357
|
+
this._wrapper.appendChild(row);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
customElements.define('nk-page-actions', NkPageActions);
|
|
2362
|
+
|
|
2363
|
+
// <nk-block-host handle><p>Editable content lives here.</p></nk-block-host>
|
|
2364
|
+
// → <div class="nk-block-host"><span class="nk-block-handle">⠿</span>…</div>
|
|
2365
|
+
// The optical shell for editor content (hover wash, focus ring, drop target).
|
|
2366
|
+
// Mount an editor into the light DOM; <nk-editor> (v1.1) does that for TipTap.
|
|
2367
|
+
class NkBlockHost extends NkElement {
|
|
2368
|
+
static get observedAttributes() { return ['handle', 'drop-target']; }
|
|
2369
|
+
|
|
2370
|
+
render() {
|
|
2371
|
+
this._box = this.createElement('div', ['nk-block-host']);
|
|
2372
|
+
this._handle = this.createElement('span', ['nk-block-handle'], { 'aria-hidden': 'true' });
|
|
2373
|
+
this._handle.textContent = '⠿';
|
|
2374
|
+
this._box.appendChild(this._handle);
|
|
2375
|
+
this._box.appendChild(document.createElement('slot'));
|
|
2376
|
+
this._wrapper.appendChild(this._box);
|
|
2377
|
+
this._sync();
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
_sync() {
|
|
2381
|
+
this._handle.style.display = this.getBoolAttr('handle') ? '' : 'none';
|
|
2382
|
+
this._box.classList.toggle('nk-drop-target', this.getBoolAttr('drop-target'));
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
onAttributeChanged() { this._sync(); }
|
|
2386
|
+
|
|
2387
|
+
get handle() { return this.getBoolAttr('handle'); }
|
|
2388
|
+
set handle(v) { this.setBoolAttr('handle', v); }
|
|
2389
|
+
get dropTarget() { return this.getBoolAttr('drop-target'); }
|
|
2390
|
+
set dropTarget(v) { this.setBoolAttr('drop-target', v); }
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
customElements.define('nk-block-host', NkBlockHost);
|
|
2394
|
+
|
|
2395
|
+
// <nk-banner variant="info">ℹ️ <span>Text</span><span slot="action">Open</span></nk-banner>
|
|
2396
|
+
// → <div class="nk-banner info">…<span class="b-action">Open</span></div>
|
|
2397
|
+
const VARIANTS = ['info', 'success', 'warning'];
|
|
2398
|
+
|
|
2399
|
+
class NkBanner extends NkElement {
|
|
2400
|
+
static get observedAttributes() { return ['variant']; }
|
|
2401
|
+
|
|
2402
|
+
render() {
|
|
2403
|
+
this._box = this.createElement('div', this._classes(), { role: 'status' });
|
|
2404
|
+
this._box.appendChild(document.createElement('slot'));
|
|
2405
|
+
this._box.appendChild(this.createElement('slot', [], { name: 'action' }));
|
|
2406
|
+
this._wrapper.appendChild(this._box);
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
_classes() {
|
|
2410
|
+
const v = this.getAttribute('variant');
|
|
2411
|
+
return ['nk-banner', ...(VARIANTS.includes(v) ? [v] : [])];
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
onAttributeChanged() { if (this._box) this._box.className = this._classes().join(' '); }
|
|
2415
|
+
|
|
2416
|
+
get variant() { return this.getAttribute('variant'); }
|
|
2417
|
+
set variant(v) { v ? this.setAttribute('variant', v) : this.removeAttribute('variant'); }
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
customElements.define('nk-banner', NkBanner);
|
|
2421
|
+
|
|
2422
|
+
// <nk-empty icon="🗂️" title="No entries yet" desc="Create the first one."><nk-btn variant="primary" small>+ New</nk-btn></nk-empty>
|
|
2423
|
+
// → <div class="nk-empty"><div class="e-icon">🗂️</div><div class="e-title">…</div><div class="e-desc">…</div>…</div>
|
|
2424
|
+
class NkEmpty extends NkElement {
|
|
2425
|
+
static get observedAttributes() { return ['icon', 'title', 'desc']; }
|
|
2426
|
+
|
|
2427
|
+
render() {
|
|
2428
|
+
const box = this.createElement('div', ['nk-empty']);
|
|
2429
|
+
this._parts = {};
|
|
2430
|
+
for (const [slot, cls] of [['icon', 'e-icon'], ['title', 'e-title'], ['desc', 'e-desc']]) {
|
|
2431
|
+
const el = this.createElement('div', [cls]);
|
|
2432
|
+
const s = this.createElement('slot', [], { name: slot });
|
|
2433
|
+
s.appendChild(el);
|
|
2434
|
+
box.appendChild(s);
|
|
2435
|
+
this._parts[slot] = el;
|
|
2436
|
+
}
|
|
2437
|
+
box.appendChild(document.createElement('slot'));
|
|
2438
|
+
this._wrapper.appendChild(box);
|
|
2439
|
+
this._sync();
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
_sync() {
|
|
2443
|
+
for (const [slot, el] of Object.entries(this._parts)) {
|
|
2444
|
+
const v = this.getAttribute(slot);
|
|
2445
|
+
el.textContent = v || '';
|
|
2446
|
+
el.style.display = v ? '' : 'none';
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
onAttributeChanged() { this._sync(); }
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
customElements.define('nk-empty', NkEmpty);
|
|
2454
|
+
|
|
2455
|
+
// <nk-skeleton height="18" width="60%"></nk-skeleton>
|
|
2456
|
+
// <nk-skeleton lines="3" widths="100%,85%,40%"></nk-skeleton>
|
|
2457
|
+
// → <div class="nk-skeleton" style="height:…;width:…"></div> × lines
|
|
2458
|
+
class NkSkeleton extends NkElement {
|
|
2459
|
+
static get observedAttributes() { return ['lines', 'height', 'width', 'widths']; }
|
|
2460
|
+
|
|
2461
|
+
render() { this._build(); }
|
|
2462
|
+
|
|
2463
|
+
_build() {
|
|
2464
|
+
this._wrapper.replaceChildren();
|
|
2465
|
+
const lines = Math.max(1, Number(this.getAttribute('lines')) || 1);
|
|
2466
|
+
const height = this.getAttribute('height') || '13';
|
|
2467
|
+
const widths = (this.getAttribute('widths') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
2468
|
+
const width = this.getAttribute('width');
|
|
2469
|
+
for (let i = 0; i < lines; i++) {
|
|
2470
|
+
const line = this.createElement('div', ['nk-skeleton'], { 'aria-hidden': 'true' });
|
|
2471
|
+
line.style.height = /^\d+(\.\d+)?$/.test(height) ? `${height}px` : height;
|
|
2472
|
+
const w = widths[i] ?? width;
|
|
2473
|
+
if (w) line.style.width = w;
|
|
2474
|
+
this._wrapper.appendChild(line);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
onAttributeChanged() { this._build(); }
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
customElements.define('nk-skeleton', NkSkeleton);
|
|
2482
|
+
|
|
2483
|
+
// <nk-synced badge="⟳ 3 places">…</nk-synced>
|
|
2484
|
+
// → <div class="nk-synced"><span class="synced-badge">⟳ 3 places</span>…</div>
|
|
2485
|
+
class NkSynced extends NkElement {
|
|
2486
|
+
static get observedAttributes() { return ['badge']; }
|
|
2487
|
+
|
|
2488
|
+
render() {
|
|
2489
|
+
const box = this.createElement('div', ['nk-synced']);
|
|
2490
|
+
this._badge = this.createElement('span', ['synced-badge']);
|
|
2491
|
+
box.appendChild(this._badge);
|
|
2492
|
+
box.appendChild(document.createElement('slot'));
|
|
2493
|
+
this._wrapper.appendChild(box);
|
|
2494
|
+
this._sync();
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
_sync() { this._badge.textContent = this.getAttribute('badge') ?? '⟳ synced'; }
|
|
2498
|
+
onAttributeChanged() { this._sync(); }
|
|
2499
|
+
|
|
2500
|
+
get badge() { return this.getAttribute('badge'); }
|
|
2501
|
+
set badge(v) { this.setAttribute('badge', v); }
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
customElements.define('nk-synced', NkSynced);
|
|
2505
|
+
|
|
2506
|
+
// <nk-tabs value="notes">
|
|
2507
|
+
// <nk-tab value="notes">📝 Notes</nk-tab>
|
|
2508
|
+
// <nk-tab value="tasks">✅ Tasks</nk-tab>
|
|
2509
|
+
// <div slot="panel" data-tab="notes" class="nk-tab-panel">…</div>
|
|
2510
|
+
// <div slot="panel" data-tab="tasks" class="nk-tab-panel">…</div>
|
|
2511
|
+
// </nk-tabs>
|
|
2512
|
+
// → <div class="nk-tabs" role="tablist">…</div> + the panels below it.
|
|
2513
|
+
// The tabs keep one tab active and hide every panel whose data-tab does not
|
|
2514
|
+
// match `value` (through the hidden attribute – the panels stay yours).
|
|
2515
|
+
const KEYS$1 = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
|
|
2516
|
+
|
|
2517
|
+
class NkTabs extends NkElement {
|
|
2518
|
+
static get observedAttributes() { return ['value']; }
|
|
2519
|
+
|
|
2520
|
+
render() {
|
|
2521
|
+
this._strip = this.createElement('div', ['nk-tabs'], { role: 'tablist' });
|
|
2522
|
+
this._tabSlot = document.createElement('slot');
|
|
2523
|
+
this._strip.appendChild(this._tabSlot);
|
|
2524
|
+
this._panelSlot = this.createElement('slot', [], { name: 'panel' });
|
|
2525
|
+
this._wrapper.append(this._strip, this._panelSlot);
|
|
2526
|
+
this._sync();
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
get tabs() { return [...this.querySelectorAll(':scope > nk-tab')]; }
|
|
2530
|
+
get panels() { return [...this.querySelectorAll(':scope > [slot="panel"]')]; }
|
|
2531
|
+
|
|
2532
|
+
_sync() {
|
|
2533
|
+
const tabs = this.tabs;
|
|
2534
|
+
if (!tabs.length) return;
|
|
2535
|
+
let value = this.getAttribute('value');
|
|
2536
|
+
if (value === null || !tabs.some(t => t.getAttribute('value') === value || (t.getAttribute('value') === null && t.textContent.trim() === value))) {
|
|
2537
|
+
const preset = tabs.find(t => t.hasAttribute('active')) || tabs[0];
|
|
2538
|
+
value = preset.getAttribute('value') ?? preset.textContent.trim();
|
|
2539
|
+
}
|
|
2540
|
+
for (const tab of tabs) {
|
|
2541
|
+
const v = tab.getAttribute('value') ?? tab.textContent.trim();
|
|
2542
|
+
if (v === value) tab.setAttribute('active', ''); else tab.removeAttribute('active');
|
|
2543
|
+
}
|
|
2544
|
+
for (const panel of this.panels) panel.hidden = panel.dataset.tab !== value;
|
|
2545
|
+
this._value = value;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
setupEvents() {
|
|
2549
|
+
this._onSlot = () => this._sync();
|
|
2550
|
+
this._onKey = (e) => {
|
|
2551
|
+
const dir = KEYS$1[e.key];
|
|
2552
|
+
if (!dir) return;
|
|
2553
|
+
const tabs = this.tabs.filter(t => !t.hasAttribute('disabled'));
|
|
2554
|
+
const i = tabs.indexOf(e.target.closest?.('nk-tab'));
|
|
2555
|
+
if (i < 0 || tabs.length < 2) return;
|
|
2556
|
+
e.preventDefault();
|
|
2557
|
+
const next = tabs[(i + dir + tabs.length) % tabs.length];
|
|
2558
|
+
next.select();
|
|
2559
|
+
next.focus();
|
|
2560
|
+
};
|
|
2561
|
+
this._tabSlot.addEventListener('slotchange', this._onSlot);
|
|
2562
|
+
this._panelSlot.addEventListener('slotchange', this._onSlot);
|
|
2563
|
+
this.addEventListener('keydown', this._onKey);
|
|
2564
|
+
this._sync();
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
teardownEvents() {
|
|
2568
|
+
this._tabSlot?.removeEventListener('slotchange', this._onSlot);
|
|
2569
|
+
this._panelSlot?.removeEventListener('slotchange', this._onSlot);
|
|
2570
|
+
this.removeEventListener('keydown', this._onKey);
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
onAttributeChanged() {
|
|
2574
|
+
const before = this._value;
|
|
2575
|
+
this._sync();
|
|
2576
|
+
if (this._value !== before) this.emit('nk-change', { value: this._value });
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
get value() { return this._value ?? this.getAttribute('value'); }
|
|
2580
|
+
set value(v) { this.setAttribute('value', v); }
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
customElements.define('nk-tabs', NkTabs);
|
|
2584
|
+
|
|
2585
|
+
// <nk-tab value="notes" active>📝 Notes</nk-tab> → <span class="nk-tab active" role="tab">
|
|
2586
|
+
class NkTab extends NkElement {
|
|
2587
|
+
static get observedAttributes() { return ['active', 'value', 'disabled']; }
|
|
2588
|
+
|
|
2589
|
+
render() {
|
|
2590
|
+
this._tab = this.createElement('span', ['nk-tab'], { role: 'tab' });
|
|
2591
|
+
this._tab.appendChild(document.createElement('slot'));
|
|
2592
|
+
this._wrapper.appendChild(this._tab);
|
|
2593
|
+
this._sync();
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
_sync() {
|
|
2597
|
+
const active = this.getBoolAttr('active');
|
|
2598
|
+
this._tab.classList.toggle('active', active);
|
|
2599
|
+
this._tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
2600
|
+
this._tab.tabIndex = active ? 0 : -1;
|
|
2601
|
+
this._tab.setAttribute('aria-disabled', this.getBoolAttr('disabled') ? 'true' : 'false');
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
setupEvents() {
|
|
2605
|
+
this._onClick = () => this.select();
|
|
2606
|
+
this._onKey = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.select(); } };
|
|
2607
|
+
this._tab.addEventListener('click', this._onClick);
|
|
2608
|
+
this._tab.addEventListener('keydown', this._onKey);
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
teardownEvents() {
|
|
2612
|
+
this._tab?.removeEventListener('click', this._onClick);
|
|
2613
|
+
this._tab?.removeEventListener('keydown', this._onKey);
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
select() {
|
|
2617
|
+
if (this.getBoolAttr('disabled')) return;
|
|
2618
|
+
const ok = this.emit('nk-select', { value: this.value, label: this.textContent.trim(), item: this });
|
|
2619
|
+
if (!ok) return;
|
|
2620
|
+
const tabs = this.closest('nk-tabs');
|
|
2621
|
+
if (tabs) tabs.value = this.value;
|
|
2622
|
+
else this.setBoolAttr('active', true);
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
onAttributeChanged() { this._sync(); }
|
|
2626
|
+
focus(o) { this._tab?.focus(o); }
|
|
2627
|
+
|
|
2628
|
+
get value() { return this.getAttribute('value') ?? this.textContent.trim(); }
|
|
2629
|
+
set value(v) { this.setAttribute('value', v); }
|
|
2630
|
+
get active() { return this.getBoolAttr('active'); }
|
|
2631
|
+
set active(v) { this.setBoolAttr('active', v); }
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
customElements.define('nk-tab', NkTab);
|
|
2635
|
+
|
|
2636
|
+
// <nk-segmented name="range" value="week">
|
|
2637
|
+
// <button value="week">Week</button><button value="month">Month</button>
|
|
2638
|
+
// </nk-segmented>
|
|
2639
|
+
// → <div class="nk-segmented">…</div> with .active on the chosen button.
|
|
2640
|
+
// The buttons stay in the light DOM (their look comes from the ::slotted
|
|
2641
|
+
// twins); the element sets type/role, moves .active and submits the value.
|
|
2642
|
+
const KEYS = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
|
|
2643
|
+
|
|
2644
|
+
class NkSegmented extends NkFormElement {
|
|
2645
|
+
static get observedAttributes() { return ['value', 'name', 'disabled']; }
|
|
2646
|
+
|
|
2647
|
+
render() {
|
|
2648
|
+
this._box = this.createElement('div', ['nk-segmented'], { role: 'radiogroup' });
|
|
2649
|
+
this._slot = document.createElement('slot');
|
|
2650
|
+
this._box.appendChild(this._slot);
|
|
2651
|
+
this._wrapper.appendChild(this._box);
|
|
2652
|
+
this._sync();
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
get buttons() { return this._slot?.assignedElements().filter(el => el.localName === 'button') ?? []; }
|
|
2656
|
+
_valueOf(btn) { return btn.getAttribute('value') ?? btn.textContent.trim(); }
|
|
2657
|
+
|
|
2658
|
+
_sync() {
|
|
2659
|
+
const buttons = this.buttons;
|
|
2660
|
+
if (!buttons.length) return;
|
|
2661
|
+
let value = this.getAttribute('value');
|
|
2662
|
+
if (value === null || !buttons.some(b => this._valueOf(b) === value)) {
|
|
2663
|
+
value = this._valueOf(buttons.find(b => b.classList.contains('active')) || buttons[0]);
|
|
2664
|
+
}
|
|
2665
|
+
if (this._defaultValue === undefined) this._defaultValue = value;
|
|
2666
|
+
const disabled = this.getBoolAttr('disabled') || !!this._formDisabled;
|
|
2667
|
+
for (const b of buttons) {
|
|
2668
|
+
const on = this._valueOf(b) === value;
|
|
2669
|
+
b.type = 'button';
|
|
2670
|
+
b.setAttribute('role', 'radio');
|
|
2671
|
+
b.setAttribute('aria-checked', on ? 'true' : 'false');
|
|
2672
|
+
b.classList.toggle('active', on);
|
|
2673
|
+
b.tabIndex = on ? 0 : -1;
|
|
2674
|
+
b.disabled = disabled;
|
|
2675
|
+
}
|
|
2676
|
+
this._value = value;
|
|
2677
|
+
this.setFormValue(value);
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
setupEvents() {
|
|
2681
|
+
this._onSlot = () => this._sync();
|
|
2682
|
+
this._onClick = (e) => {
|
|
2683
|
+
const btn = e.target.closest?.('button');
|
|
2684
|
+
if (!btn || !this.buttons.includes(btn) || btn.disabled) return;
|
|
2685
|
+
this._choose(this._valueOf(btn));
|
|
2686
|
+
};
|
|
2687
|
+
this._onKey = (e) => {
|
|
2688
|
+
const dir = KEYS[e.key];
|
|
2689
|
+
if (!dir) return;
|
|
2690
|
+
const buttons = this.buttons;
|
|
2691
|
+
const i = buttons.indexOf(e.target.closest?.('button'));
|
|
2692
|
+
if (i < 0 || buttons.length < 2) return;
|
|
2693
|
+
e.preventDefault();
|
|
2694
|
+
const next = buttons[(i + dir + buttons.length) % buttons.length];
|
|
2695
|
+
this._choose(this._valueOf(next));
|
|
2696
|
+
next.focus();
|
|
2697
|
+
};
|
|
2698
|
+
this._slot.addEventListener('slotchange', this._onSlot);
|
|
2699
|
+
this.addEventListener('click', this._onClick);
|
|
2700
|
+
this.addEventListener('keydown', this._onKey);
|
|
2701
|
+
this._sync();
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
teardownEvents() {
|
|
2705
|
+
this._slot?.removeEventListener('slotchange', this._onSlot);
|
|
2706
|
+
this.removeEventListener('click', this._onClick);
|
|
2707
|
+
this.removeEventListener('keydown', this._onKey);
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
_choose(value) {
|
|
2711
|
+
if (value === this._value) return;
|
|
2712
|
+
this._syncing = true;
|
|
2713
|
+
this.setAttribute('value', value);
|
|
2714
|
+
this._syncing = false;
|
|
2715
|
+
this._sync();
|
|
2716
|
+
this.emit('nk-change', { value, name: this.name });
|
|
2717
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
onAttributeChanged() { if (!this._syncing) this._sync(); }
|
|
2721
|
+
resetValue() { if (this._defaultValue !== undefined) this.setAttribute('value', this._defaultValue); }
|
|
2722
|
+
onFormDisabled(d) { this._formDisabled = d; this._sync(); }
|
|
2723
|
+
|
|
2724
|
+
get value() { return this._value ?? this.getAttribute('value'); }
|
|
2725
|
+
set value(v) { this.setAttribute('value', v); }
|
|
2726
|
+
get name() { return this.getAttribute('name'); }
|
|
2727
|
+
set name(v) { this.setAttribute('name', v); }
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
customElements.define('nk-segmented', NkSegmented);
|
|
2731
|
+
|
|
2732
|
+
// <nk-stats><nk-stat …></nk-stat>…</nk-stats> → <div class="nk-stats">
|
|
2733
|
+
class NkStats extends NkElement {
|
|
2734
|
+
render() {
|
|
2735
|
+
const row = this.createElement('div', ['nk-stats']);
|
|
2736
|
+
row.appendChild(document.createElement('slot'));
|
|
2737
|
+
this._wrapper.appendChild(row);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
customElements.define('nk-stats', NkStats);
|
|
2742
|
+
|
|
2743
|
+
// <nk-stat label="Active pages" value="128" delta="▲ 12 this week" trend="up"></nk-stat>
|
|
2744
|
+
// → <div class="nk-stat"><div class="s-label">…</div><div class="s-value">…</div><div class="s-delta up">…</div></div>
|
|
2745
|
+
class NkStat extends NkElement {
|
|
2746
|
+
static get observedAttributes() { return ['label', 'value', 'delta', 'trend']; }
|
|
2747
|
+
|
|
2748
|
+
render() {
|
|
2749
|
+
const box = this.createElement('div', ['nk-stat']);
|
|
2750
|
+
this._parts = {};
|
|
2751
|
+
for (const [slot, cls] of [['label', 's-label'], ['value', 's-value'], ['delta', 's-delta']]) {
|
|
2752
|
+
const el = this.createElement('div', [cls]);
|
|
2753
|
+
const s = this.createElement('slot', [], { name: slot });
|
|
2754
|
+
s.appendChild(el);
|
|
2755
|
+
box.appendChild(s);
|
|
2756
|
+
this._parts[slot] = el;
|
|
2757
|
+
}
|
|
2758
|
+
this._wrapper.appendChild(box);
|
|
2759
|
+
this._sync();
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
_sync() {
|
|
2763
|
+
for (const [slot, el] of Object.entries(this._parts)) {
|
|
2764
|
+
const v = this.getAttribute(slot);
|
|
2765
|
+
el.textContent = v ?? '';
|
|
2766
|
+
el.style.display = v === null ? 'none' : '';
|
|
2767
|
+
}
|
|
2768
|
+
const trend = this.getAttribute('trend');
|
|
2769
|
+
this._parts.delta.className = 's-delta' + (trend === 'up' || trend === 'down' ? ` ${trend}` : '');
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
onAttributeChanged() { this._sync(); }
|
|
2773
|
+
|
|
2774
|
+
get value() { return this.getAttribute('value'); }
|
|
2775
|
+
set value(v) { this.setAttribute('value', v); }
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
customElements.define('nk-stat', NkStat);
|
|
2779
|
+
|
|
2780
|
+
// <nk-avatar-group more="+2">
|
|
2781
|
+
// <span class="mini-avatar" style="background:#448361">SL</span>…
|
|
2782
|
+
// </nk-avatar-group>
|
|
2783
|
+
// → <div class="nk-avatar-group">…<span class="mini-avatar more">+2</span></div>
|
|
2784
|
+
// The avatars stay in the light DOM (::slotted twins size and overlap them);
|
|
2785
|
+
// the "more" bubble is rendered here from the attribute.
|
|
2786
|
+
class NkAvatarGroup extends NkElement {
|
|
2787
|
+
static get observedAttributes() { return ['more']; }
|
|
2788
|
+
|
|
2789
|
+
render() {
|
|
2790
|
+
const box = this.createElement('div', ['nk-avatar-group']);
|
|
2791
|
+
box.appendChild(document.createElement('slot'));
|
|
2792
|
+
this._more = this.createElement('span', ['mini-avatar', 'more']);
|
|
2793
|
+
box.appendChild(this._more);
|
|
2794
|
+
this._wrapper.appendChild(box);
|
|
2795
|
+
this._sync();
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
_sync() {
|
|
2799
|
+
const more = this.getAttribute('more');
|
|
2800
|
+
this._more.textContent = more || '';
|
|
2801
|
+
this._more.style.display = more ? '' : 'none';
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
onAttributeChanged() { this._sync(); }
|
|
2805
|
+
|
|
2806
|
+
get more() { return this.getAttribute('more'); }
|
|
2807
|
+
set more(v) { v == null ? this.removeAttribute('more') : this.setAttribute('more', v); }
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
customElements.define('nk-avatar-group', NkAvatarGroup);
|
|
2811
|
+
|
|
2812
|
+
// <nk-mention type="person"><span slot="avatar" class="mini-avatar" style="background:#448361">SL</span>Sara Lindt</nk-mention>
|
|
2813
|
+
// <nk-mention type="page">📄 Onboarding</nk-mention> <nk-mention type="date">📅 May 20</nk-mention>
|
|
2814
|
+
// → <span class="nk-mention person">…</span>
|
|
2815
|
+
const TYPES = ['person', 'page', 'date'];
|
|
2816
|
+
|
|
2817
|
+
class NkMention extends NkElement {
|
|
2818
|
+
static get observedAttributes() { return ['type']; }
|
|
2819
|
+
|
|
2820
|
+
render() {
|
|
2821
|
+
this._box = this.createElement('span', this._classes());
|
|
2822
|
+
this._box.appendChild(this.createElement('slot', [], { name: 'avatar' }));
|
|
2823
|
+
this._box.appendChild(document.createElement('slot'));
|
|
2824
|
+
this._wrapper.appendChild(this._box);
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
_classes() {
|
|
2828
|
+
const t = this.getAttribute('type');
|
|
2829
|
+
return ['nk-mention', ...(TYPES.includes(t) ? [t] : [])];
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
onAttributeChanged() { if (this._box) this._box.className = this._classes().join(' '); }
|
|
2833
|
+
|
|
2834
|
+
get type() { return this.getAttribute('type'); }
|
|
2835
|
+
set type(v) { v ? this.setAttribute('type', v) : this.removeAttribute('type'); }
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
customElements.define('nk-mention', NkMention);
|
|
2839
|
+
|
|
2840
|
+
// <nk-template-btn icon="📅" value="week-plan">Insert week plan</nk-template-btn>
|
|
2841
|
+
// → <button class="nk-template-btn">📅 Insert week plan</button>
|
|
2842
|
+
class NkTemplateBtn extends NkElement {
|
|
2843
|
+
static get observedAttributes() { return ['icon', 'value', 'disabled']; }
|
|
2844
|
+
|
|
2845
|
+
render() {
|
|
2846
|
+
this._btn = this.createElement('button', ['nk-template-btn'], { type: 'button' });
|
|
2847
|
+
// One flex item: "📅 Insert week plan" is a single text run in the class
|
|
2848
|
+
// markup, so icon, space and label must not become separate flex items.
|
|
2849
|
+
const run = document.createElement('span');
|
|
2850
|
+
this._icon = document.createElement('span');
|
|
2851
|
+
this._space = document.createTextNode(' ');
|
|
2852
|
+
run.append(this._icon, this._space, document.createElement('slot'));
|
|
2853
|
+
this._btn.appendChild(run);
|
|
2854
|
+
this._wrapper.appendChild(this._btn);
|
|
2855
|
+
this._sync();
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
_sync() {
|
|
2859
|
+
const icon = this.getAttribute('icon');
|
|
2860
|
+
this._icon.textContent = icon || '';
|
|
2861
|
+
this._space.data = icon ? ' ' : '';
|
|
2862
|
+
this._btn.disabled = this.getBoolAttr('disabled');
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
setupEvents() {
|
|
2866
|
+
this._onClick = () => this.emit('nk-select', { value: this.value, label: this.textContent.trim() });
|
|
2867
|
+
this._btn.addEventListener('click', this._onClick);
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
teardownEvents() { this._btn?.removeEventListener('click', this._onClick); }
|
|
2871
|
+
onAttributeChanged() { this._sync(); }
|
|
2872
|
+
focus(o) { this._btn?.focus(o); }
|
|
2873
|
+
|
|
2874
|
+
get value() { return this.getAttribute('value') ?? this.textContent.trim(); }
|
|
2875
|
+
set value(v) { this.setAttribute('value', v); }
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
customElements.define('nk-template-btn', NkTemplateBtn);
|
|
2879
|
+
|
|
2880
|
+
// <nk-model-card name="model" value="gpt" title="Mona Pro" desc="Best for long documents" selected></nk-model-card>
|
|
2881
|
+
// → <div class="nk-model-card selected"><div class="m-radio"></div><div><div class="m-name">…</div><div class="m-desc">…</div></div></div>
|
|
2882
|
+
// Cards with the same `name` in the same tree behave like radios and submit
|
|
2883
|
+
// the selected value with the form.
|
|
2884
|
+
class NkModelCard extends NkFormElement {
|
|
2885
|
+
static get observedAttributes() { return ['name', 'value', 'title', 'desc', 'selected', 'disabled']; }
|
|
2886
|
+
|
|
2887
|
+
render() {
|
|
2888
|
+
this._card = this.createElement('div', ['nk-model-card'], { role: 'radio', tabindex: '0' });
|
|
2889
|
+
this._card.appendChild(this.createElement('div', ['m-radio']));
|
|
2890
|
+
const text = document.createElement('div');
|
|
2891
|
+
this._name = this.createElement('div', ['m-name']);
|
|
2892
|
+
const titleSlot = this.createElement('slot', [], { name: 'title' });
|
|
2893
|
+
titleSlot.appendChild(this._name);
|
|
2894
|
+
this._desc = this.createElement('div', ['m-desc']);
|
|
2895
|
+
const descSlot = this.createElement('slot', [], { name: 'desc' });
|
|
2896
|
+
descSlot.appendChild(this._desc);
|
|
2897
|
+
text.append(titleSlot, descSlot);
|
|
2898
|
+
this._card.appendChild(text);
|
|
2899
|
+
this._wrapper.appendChild(this._card);
|
|
2900
|
+
this._defaultSelected = this.getBoolAttr('selected');
|
|
2901
|
+
this._sync();
|
|
2902
|
+
if (this.getBoolAttr('selected')) this._unselectPeers();
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
_sync() {
|
|
2906
|
+
const on = this.getBoolAttr('selected');
|
|
2907
|
+
this._card.classList.toggle('selected', on);
|
|
2908
|
+
this._card.setAttribute('aria-checked', on ? 'true' : 'false');
|
|
2909
|
+
this._card.setAttribute('aria-disabled', this.getBoolAttr('disabled') ? 'true' : 'false');
|
|
2910
|
+
this._name.textContent = this.getAttribute('title') || '';
|
|
2911
|
+
const desc = this.getAttribute('desc');
|
|
2912
|
+
this._desc.textContent = desc || '';
|
|
2913
|
+
this._desc.style.display = desc ? '' : 'none';
|
|
2914
|
+
this.setFormValue(on ? (this.getAttribute('value') || 'on') : null, on ? 'selected' : 'unselected');
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
_group() {
|
|
2918
|
+
const name = this.getAttribute('name');
|
|
2919
|
+
const root = this.getRootNode();
|
|
2920
|
+
if (!name || typeof root?.querySelectorAll !== 'function') return [this];
|
|
2921
|
+
return [...root.querySelectorAll('nk-model-card')].filter(el => el.getAttribute('name') === name);
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
_unselectPeers() {
|
|
2925
|
+
for (const el of this._group()) if (el !== this && el.hasAttribute('selected')) el.removeAttribute('selected');
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
setupEvents() {
|
|
2929
|
+
this._onClick = () => this.select();
|
|
2930
|
+
this._onKey = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.select(); } };
|
|
2931
|
+
this._card.addEventListener('click', this._onClick);
|
|
2932
|
+
this._card.addEventListener('keydown', this._onKey);
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
teardownEvents() {
|
|
2936
|
+
this._card?.removeEventListener('click', this._onClick);
|
|
2937
|
+
this._card?.removeEventListener('keydown', this._onKey);
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
select() {
|
|
2941
|
+
if (this.getBoolAttr('disabled') || this.getBoolAttr('selected')) return;
|
|
2942
|
+
this.setBoolAttr('selected', true);
|
|
2943
|
+
this.emit('nk-change', { value: this.value, name: this.name, checked: true });
|
|
2944
|
+
this.emit('nk-select', { value: this.value, label: this.getAttribute('title') });
|
|
2945
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
onAttributeChanged(name) {
|
|
2949
|
+
this._sync();
|
|
2950
|
+
if (name === 'selected' && this.getBoolAttr('selected')) this._unselectPeers();
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
resetValue() { this.setBoolAttr('selected', this._defaultSelected); }
|
|
2954
|
+
focus(o) { this._card?.focus(o); }
|
|
2955
|
+
|
|
2956
|
+
get selected() { return this.getBoolAttr('selected'); }
|
|
2957
|
+
set selected(v) { this.setBoolAttr('selected', v); }
|
|
2958
|
+
get value() { return this.getAttribute('value') || 'on'; }
|
|
2959
|
+
set value(v) { this.setAttribute('value', v); }
|
|
2960
|
+
get name() { return this.getAttribute('name'); }
|
|
2961
|
+
set name(v) { this.setAttribute('name', v); }
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
customElements.define('nk-model-card', NkModelCard);
|
|
2965
|
+
|
|
2966
|
+
// <nk-profile-row avatar="MK"><nk-btn variant="secondary" small>Change photo</nk-btn></nk-profile-row>
|
|
2967
|
+
// → <div class="nk-profile-row"><div class="big-avatar">MK</div>…</div>
|
|
2968
|
+
class NkProfileRow extends NkElement {
|
|
2969
|
+
static get observedAttributes() { return ['avatar']; }
|
|
2970
|
+
|
|
2971
|
+
render() {
|
|
2972
|
+
const row = this.createElement('div', ['nk-profile-row']);
|
|
2973
|
+
this._avatar = this.createElement('div', ['big-avatar']);
|
|
2974
|
+
const s = this.createElement('slot', [], { name: 'avatar' });
|
|
2975
|
+
s.appendChild(this._avatar);
|
|
2976
|
+
row.appendChild(s);
|
|
2977
|
+
row.appendChild(document.createElement('slot'));
|
|
2978
|
+
this._wrapper.appendChild(row);
|
|
2979
|
+
this._sync();
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
_sync() { this._avatar.textContent = this.getAttribute('avatar') || ''; }
|
|
2983
|
+
onAttributeChanged() { this._sync(); }
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
customElements.define('nk-profile-row', NkProfileRow);
|
|
2987
|
+
|
|
2988
|
+
// <nk-danger-zone title="Danger zone">…</nk-danger-zone>
|
|
2989
|
+
// → <div class="nk-danger-zone"><div class="dz-title">Danger zone</div>…</div>
|
|
2990
|
+
class NkDangerZone extends NkElement {
|
|
2991
|
+
static get observedAttributes() { return ['title']; }
|
|
2992
|
+
|
|
2993
|
+
render() {
|
|
2994
|
+
const box = this.createElement('div', ['nk-danger-zone']);
|
|
2995
|
+
this._title = this.createElement('div', ['dz-title']);
|
|
2996
|
+
box.appendChild(this._title);
|
|
2997
|
+
box.appendChild(document.createElement('slot'));
|
|
2998
|
+
this._wrapper.appendChild(box);
|
|
2999
|
+
this._sync();
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
_sync() {
|
|
3003
|
+
const t = this.getAttribute('title');
|
|
3004
|
+
this._title.textContent = t || '';
|
|
3005
|
+
this._title.style.display = t ? '' : 'none';
|
|
3006
|
+
}
|
|
3007
|
+
onAttributeChanged() { this._sync(); }
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
customElements.define('nk-danger-zone', NkDangerZone);
|
|
3011
|
+
|
|
3012
|
+
// <nk-member-list><nk-member-row …></nk-member-row>…</nk-member-list>
|
|
3013
|
+
// → <div class="nk-member-list">…</div>; the last row gets `last` (no border).
|
|
3014
|
+
class NkMemberList extends NkElement {
|
|
3015
|
+
render() {
|
|
3016
|
+
const box = this.createElement('div', ['nk-member-list']);
|
|
3017
|
+
this._slot = document.createElement('slot');
|
|
3018
|
+
box.appendChild(this._slot);
|
|
3019
|
+
this._wrapper.appendChild(box);
|
|
3020
|
+
this._mark();
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
_mark() {
|
|
3024
|
+
const rows = this._slot.assignedElements().filter(el => el.localName === 'nk-member-row');
|
|
3025
|
+
rows.forEach((row, i) => { if (i === rows.length - 1) row.setAttribute('last', ''); else row.removeAttribute('last'); });
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
setupEvents() {
|
|
3029
|
+
this._onSlot = () => this._mark();
|
|
3030
|
+
this._slot.addEventListener('slotchange', this._onSlot);
|
|
3031
|
+
this._mark();
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
teardownEvents() { this._slot?.removeEventListener('slotchange', this._onSlot); }
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
customElements.define('nk-member-list', NkMemberList);
|
|
3038
|
+
|
|
3039
|
+
// <nk-member-row name="Sara Lindt" mail="sara@…" avatar="SL" color="#448361">
|
|
3040
|
+
// <nk-select slot="role" compact>…</nk-select>
|
|
3041
|
+
// </nk-member-row>
|
|
3042
|
+
// → <div class="nk-member-row"><span class="mini-avatar">SL</span><div>Sara Lindt<div class="m-mail">…</div></div><select …></div>
|
|
3043
|
+
class NkMemberRow extends NkElement {
|
|
3044
|
+
static get observedAttributes() { return ['name', 'mail', 'avatar', 'color', 'last']; }
|
|
3045
|
+
|
|
3046
|
+
render() {
|
|
3047
|
+
this._row = this.createElement('div', ['nk-member-row']);
|
|
3048
|
+
this._avatar = this.createElement('span', ['mini-avatar']);
|
|
3049
|
+
const avatarSlot = this.createElement('slot', [], { name: 'avatar' });
|
|
3050
|
+
avatarSlot.appendChild(this._avatar);
|
|
3051
|
+
const text = document.createElement('div');
|
|
3052
|
+
this._name = document.createTextNode('');
|
|
3053
|
+
this._mail = this.createElement('div', ['m-mail']);
|
|
3054
|
+
text.append(this._name, this._mail);
|
|
3055
|
+
// `.nk-member-row .nk-select { margin-left: auto }` cannot reach a select
|
|
3056
|
+
// inside another shadow root, and a display:contents host takes no margin –
|
|
3057
|
+
// so the role slot sits in a box that carries that rule.
|
|
3058
|
+
const role = document.createElement('div');
|
|
3059
|
+
role.style.marginLeft = 'auto';
|
|
3060
|
+
role.appendChild(this.createElement('slot', [], { name: 'role' }));
|
|
3061
|
+
this._row.append(avatarSlot, text, role, document.createElement('slot'));
|
|
3062
|
+
this._wrapper.appendChild(this._row);
|
|
3063
|
+
this._sync();
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
_sync() {
|
|
3067
|
+
this._name.data = this.getAttribute('name') || '';
|
|
3068
|
+
const mail = this.getAttribute('mail');
|
|
3069
|
+
this._mail.textContent = mail || '';
|
|
3070
|
+
this._mail.style.display = mail ? '' : 'none';
|
|
3071
|
+
const avatar = this.getAttribute('avatar');
|
|
3072
|
+
this._avatar.textContent = avatar || (this.getAttribute('name') || '').split(/\s+/).map(w => w[0]).join('').slice(0, 2).toUpperCase();
|
|
3073
|
+
this._avatar.style.background = this.getAttribute('color') || 'var(--nk-text-tertiary)';
|
|
3074
|
+
this._row.classList.toggle('last', this.getBoolAttr('last'));
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
onAttributeChanged() { this._sync(); }
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
customElements.define('nk-member-row', NkMemberRow);
|
|
3081
|
+
|
|
3082
|
+
// Overlay plumbing shared by nk-modal and nk-cmdk: body scroll lock with a
|
|
3083
|
+
// refcount, and `inert` on everything outside the overlay so focus and
|
|
3084
|
+
// assistive technology stay inside – nested shadow roots included.
|
|
3085
|
+
let locks = 0, previousOverflow = '';
|
|
3086
|
+
|
|
3087
|
+
function lockScroll() {
|
|
3088
|
+
if (locks++ === 0) {
|
|
3089
|
+
previousOverflow = document.documentElement.style.overflow;
|
|
3090
|
+
document.documentElement.style.overflow = 'hidden';
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
function unlockScroll() {
|
|
3095
|
+
if (locks > 0 && --locks === 0) document.documentElement.style.overflow = previousOverflow;
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
/** Makes every sibling along `el`'s ancestor chain inert (shadow hosts included); returns the undo. */
|
|
3099
|
+
function inertOutside(el) {
|
|
3100
|
+
const made = [];
|
|
3101
|
+
let node = el;
|
|
3102
|
+
while (node && node !== document.body) {
|
|
3103
|
+
const parent = node.parentNode instanceof ShadowRoot ? node.parentNode : node.parentNode;
|
|
3104
|
+
if (!parent) break;
|
|
3105
|
+
for (const sib of parent.children) {
|
|
3106
|
+
if (sib === node || sib.inert || /^(SCRIPT|STYLE|LINK|TEMPLATE)$/.test(sib.tagName)) continue;
|
|
3107
|
+
sib.inert = true;
|
|
3108
|
+
made.push(sib);
|
|
3109
|
+
}
|
|
3110
|
+
node = parent instanceof ShadowRoot ? parent.host : parent;
|
|
3111
|
+
}
|
|
3112
|
+
return () => { for (const c of made) c.inert = false; };
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
/** The first tabbable element inside a light-DOM subtree, looking into shadow roots. */
|
|
3116
|
+
function firstFocusable(root) {
|
|
3117
|
+
const SEL = 'a[href], button:not([disabled]), input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"], [contenteditable="plaintext-only"]';
|
|
3118
|
+
const walk = (node) => {
|
|
3119
|
+
for (const el of node.querySelectorAll('*')) {
|
|
3120
|
+
if (el.matches(SEL) && el.getClientRects().length) return el;
|
|
3121
|
+
if (el.shadowRoot) { const inner = walk(el.shadowRoot); if (inner) return inner; }
|
|
3122
|
+
}
|
|
3123
|
+
return null;
|
|
3124
|
+
};
|
|
3125
|
+
return walk(root);
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
// <nk-modal id="settings">
|
|
3129
|
+
// <nk-settings-user slot="user" name="…" mail="…"></nk-settings-user>
|
|
3130
|
+
// <nk-settings-pane name="profile" group="Account" icon="👤" label="My profile" title="My profile" active>…</nk-settings-pane>
|
|
3131
|
+
// <nk-settings-pane name="members" group="Workspace" icon="👥" label="Members" title="Members">…</nk-settings-pane>
|
|
3132
|
+
// </nk-modal>
|
|
3133
|
+
// → <div class="nk-modal-backdrop open"><div class="nk-modal" role="dialog">
|
|
3134
|
+
// <nav class="nk-settings-nav">…user… section labels + tree rows…</nav>
|
|
3135
|
+
// <div class="nk-settings-content">…panes…</div></div></div>
|
|
3136
|
+
//
|
|
3137
|
+
// The modal renders the nav rows itself from the panes' label/icon/group, so
|
|
3138
|
+
// `.nk-settings-nav .nk-tree-item` and the 860px icon-rail rules match with
|
|
3139
|
+
// no JS breakpoint. Escape and the backdrop close it; focus moves in and
|
|
3140
|
+
// back out; the page behind is scroll-locked and inert. Put the element
|
|
3141
|
+
// directly under <body>: a transformed ancestor would trap the fixed backdrop.
|
|
3142
|
+
class NkModal extends NkElement {
|
|
3143
|
+
static get observedAttributes() { return ['open', 'pane']; }
|
|
3144
|
+
|
|
3145
|
+
render() {
|
|
3146
|
+
this._backdrop = this.createElement('div', ['nk-modal-backdrop']);
|
|
3147
|
+
this._backdrop.inert = true;
|
|
3148
|
+
this._box = this.createElement('div', ['nk-modal'], { role: 'dialog', 'aria-modal': 'true', tabindex: '-1' });
|
|
3149
|
+
this._nav = this.createElement('nav', ['nk-settings-nav'], { 'aria-label': 'Settings' });
|
|
3150
|
+
this._nav.appendChild(this.createElement('slot', [], { name: 'user' }));
|
|
3151
|
+
this._rows = document.createElement('div');
|
|
3152
|
+
this._nav.appendChild(this._rows);
|
|
3153
|
+
this._nav.appendChild(this.createElement('slot', [], { name: 'nav' }));
|
|
3154
|
+
this._content = this.createElement('div', ['nk-settings-content']);
|
|
3155
|
+
this._slot = document.createElement('slot');
|
|
3156
|
+
this._content.appendChild(this._slot);
|
|
3157
|
+
this._box.append(this._nav, this._content);
|
|
3158
|
+
this._backdrop.appendChild(this._box);
|
|
3159
|
+
this._wrapper.appendChild(this._backdrop);
|
|
3160
|
+
this._syncOpen();
|
|
3161
|
+
this.requestNav();
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
get panes() { return [...this.querySelectorAll(':scope > nk-settings-pane')]; }
|
|
3165
|
+
|
|
3166
|
+
/** Rebuilds the nav from the panes (debounced – panes upgrade after the modal). */
|
|
3167
|
+
requestNav() {
|
|
3168
|
+
if (this._navQueued) return;
|
|
3169
|
+
this._navQueued = true;
|
|
3170
|
+
queueMicrotask(() => { this._navQueued = false; this._buildNav(); });
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
_buildNav() {
|
|
3174
|
+
if (!this._rows) return;
|
|
3175
|
+
const panes = this.panes;
|
|
3176
|
+
const labelled = panes.filter(p => p.getAttribute('label'));
|
|
3177
|
+
this._rows.replaceChildren();
|
|
3178
|
+
this._nav.style.display = (labelled.length || this.querySelector(':scope > [slot="nav"], :scope > [slot="user"]')) ? '' : 'none';
|
|
3179
|
+
let lastGroup = null;
|
|
3180
|
+
for (const pane of labelled) {
|
|
3181
|
+
const group = pane.getAttribute('group');
|
|
3182
|
+
if (group && group !== lastGroup) {
|
|
3183
|
+
const label = this.createElement('div', ['nk-section-label']);
|
|
3184
|
+
label.textContent = group;
|
|
3185
|
+
this._rows.appendChild(label);
|
|
3186
|
+
lastGroup = group;
|
|
3187
|
+
}
|
|
3188
|
+
const row = this.createElement('div', ['nk-tree-item'], { role: 'tab', tabindex: '0', 'data-pane': pane.name });
|
|
3189
|
+
const icon = this.createElement('span', ['icon']);
|
|
3190
|
+
icon.textContent = pane.getAttribute('icon') || '';
|
|
3191
|
+
const text = this.createElement('span', ['label']);
|
|
3192
|
+
text.textContent = pane.getAttribute('label');
|
|
3193
|
+
row.append(icon, text);
|
|
3194
|
+
this._rows.appendChild(row);
|
|
3195
|
+
}
|
|
3196
|
+
this._syncActive();
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
/** Marks the active pane and its nav row without rebuilding the rows (keeps focus). */
|
|
3200
|
+
_syncActive() {
|
|
3201
|
+
const panes = this.panes;
|
|
3202
|
+
let current = this.getAttribute('pane');
|
|
3203
|
+
if (!panes.some(p => p.name === current)) current = (panes.find(p => p.hasAttribute('active')) || panes[0])?.name ?? null;
|
|
3204
|
+
for (const row of this._rows.querySelectorAll('[data-pane]')) {
|
|
3205
|
+
const on = row.dataset.pane === current;
|
|
3206
|
+
row.classList.toggle('active', on);
|
|
3207
|
+
row.setAttribute('aria-selected', on ? 'true' : 'false');
|
|
3208
|
+
}
|
|
3209
|
+
for (const pane of panes) pane.active = pane.name === current;
|
|
3210
|
+
this._current = current;
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
_syncOpen() {
|
|
3214
|
+
const open = this.getBoolAttr('open');
|
|
3215
|
+
this._backdrop.classList.toggle('open', open);
|
|
3216
|
+
this._backdrop.inert = !open;
|
|
3217
|
+
if (open === this._wasOpen) return;
|
|
3218
|
+
this._wasOpen = open;
|
|
3219
|
+
if (open) {
|
|
3220
|
+
this._returnFocus = document.activeElement;
|
|
3221
|
+
lockScroll();
|
|
3222
|
+
this._undoInert = inertOutside(this);
|
|
3223
|
+
requestAnimationFrame(() => {
|
|
3224
|
+
const target = this._rows.querySelector('.nk-tree-item.active') || firstFocusable(this) || this._box;
|
|
3225
|
+
target.focus({ preventScroll: true });
|
|
3226
|
+
});
|
|
3227
|
+
} else {
|
|
3228
|
+
unlockScroll();
|
|
3229
|
+
this._undoInert?.(); this._undoInert = null;
|
|
3230
|
+
const back = this._returnFocus;
|
|
3231
|
+
if (back && typeof back.focus === 'function' && back.isConnected) back.focus({ preventScroll: true });
|
|
3232
|
+
this._returnFocus = null;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
setupEvents() {
|
|
3237
|
+
this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(); };
|
|
3238
|
+
this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); } };
|
|
3239
|
+
this._onNavClick = (e) => { const row = e.target.closest('[data-pane]'); if (row) { row.focus(); this.pane = row.dataset.pane; } };
|
|
3240
|
+
this._onNavKey = (e) => {
|
|
3241
|
+
const row = e.target.closest?.('[data-pane]');
|
|
3242
|
+
if (!row) return;
|
|
3243
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.pane = row.dataset.pane; return; }
|
|
3244
|
+
const dir = { ArrowDown: 1, ArrowUp: -1 }[e.key];
|
|
3245
|
+
if (!dir) return;
|
|
3246
|
+
const rows = [...this._rows.querySelectorAll('[data-pane]')];
|
|
3247
|
+
const next = rows[(rows.indexOf(row) + dir + rows.length) % rows.length];
|
|
3248
|
+
e.preventDefault(); next.focus(); this.pane = next.dataset.pane;
|
|
3249
|
+
};
|
|
3250
|
+
this._onSlot = () => this.requestNav();
|
|
3251
|
+
this._backdrop.addEventListener('click', this._onBackdrop);
|
|
3252
|
+
document.addEventListener('keydown', this._onKey);
|
|
3253
|
+
this._rows.addEventListener('click', this._onNavClick);
|
|
3254
|
+
this._rows.addEventListener('keydown', this._onNavKey);
|
|
3255
|
+
this._slot.addEventListener('slotchange', this._onSlot);
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
teardownEvents() {
|
|
3259
|
+
this._backdrop?.removeEventListener('click', this._onBackdrop);
|
|
3260
|
+
document.removeEventListener('keydown', this._onKey);
|
|
3261
|
+
this._rows?.removeEventListener('click', this._onNavClick);
|
|
3262
|
+
this._rows?.removeEventListener('keydown', this._onNavKey);
|
|
3263
|
+
this._slot?.removeEventListener('slotchange', this._onSlot);
|
|
3264
|
+
if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
onAttributeChanged(name) {
|
|
3268
|
+
if (name === 'open') {
|
|
3269
|
+
this._syncOpen();
|
|
3270
|
+
this.emit('nk-toggle', { open: this.getBoolAttr('open') });
|
|
3271
|
+
} else if (name === 'pane') {
|
|
3272
|
+
const before = this._current;
|
|
3273
|
+
this._syncActive();
|
|
3274
|
+
if (this._current !== before) this.emit('nk-select', { value: this._current, label: this.panes.find(p => p.name === this._current)?.getAttribute('label') ?? this._current });
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
show(pane) { if (pane) this.setAttribute('pane', pane); this.setBoolAttr('open', true); }
|
|
3279
|
+
close() { this.setBoolAttr('open', false); }
|
|
3280
|
+
toggle() { this.setBoolAttr('open', !this.getBoolAttr('open')); }
|
|
3281
|
+
|
|
3282
|
+
get open() { return this.getBoolAttr('open'); }
|
|
3283
|
+
set open(v) { this.setBoolAttr('open', v); }
|
|
3284
|
+
get pane() { return this._current ?? this.getAttribute('pane'); }
|
|
3285
|
+
set pane(v) { this.setAttribute('pane', v); }
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
customElements.define('nk-modal', NkModal);
|
|
3289
|
+
|
|
3290
|
+
// <nk-settings-pane name="profile" label="My profile" icon="👤" group="Account" title="My profile" active>…</nk-settings-pane>
|
|
3291
|
+
// → <section class="nk-settings-pane active"><h2>My profile</h2>…</section>
|
|
3292
|
+
// `label`, `icon` and `group` feed the nav that <nk-modal> renders; `title`
|
|
3293
|
+
// renders the pane heading (child-combinator rule, so it lives here). Slotted
|
|
3294
|
+
// <h2>/<h3> are styled by their twins.
|
|
3295
|
+
class NkSettingsPane extends NkElement {
|
|
3296
|
+
static get observedAttributes() { return ['name', 'label', 'icon', 'group', 'title', 'active']; }
|
|
3297
|
+
|
|
3298
|
+
render() {
|
|
3299
|
+
this._section = this.createElement('section', ['nk-settings-pane'], { role: 'tabpanel' });
|
|
3300
|
+
this._h2 = document.createElement('h2');
|
|
3301
|
+
this._section.appendChild(this._h2);
|
|
3302
|
+
this._section.appendChild(document.createElement('slot'));
|
|
3303
|
+
this._wrapper.appendChild(this._section);
|
|
3304
|
+
this._sync();
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
_sync() {
|
|
3308
|
+
const title = this.getAttribute('title');
|
|
3309
|
+
this._h2.textContent = title || '';
|
|
3310
|
+
this._h2.style.display = title ? '' : 'none';
|
|
3311
|
+
this._section.classList.toggle('active', this.getBoolAttr('active'));
|
|
3312
|
+
this._section.setAttribute('aria-label', this.getAttribute('label') || title || this.getAttribute('name') || '');
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
onAttributeChanged(name) {
|
|
3316
|
+
this._sync();
|
|
3317
|
+
if (['label', 'icon', 'group', 'name'].includes(name)) this.closest('nk-modal')?.requestNav?.();
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
get name() { return this.getAttribute('name') ?? this.getAttribute('label') ?? ''; }
|
|
3321
|
+
get active() { return this.getBoolAttr('active'); }
|
|
3322
|
+
set active(v) { this.setBoolAttr('active', v); }
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
customElements.define('nk-settings-pane', NkSettingsPane);
|
|
3326
|
+
|
|
3327
|
+
// <nk-settings-user slot="user" name="Marcel Karas" mail="marcel@…" avatar="MK"></nk-settings-user>
|
|
3328
|
+
// → <div class="nk-settings-user"><div class="avatar">MK</div><div class="u-text"><div class="name">…</div><div class="mail">…</div></div></div>
|
|
3329
|
+
class NkSettingsUser extends NkElement {
|
|
3330
|
+
static get observedAttributes() { return ['name', 'mail', 'avatar']; }
|
|
3331
|
+
|
|
3332
|
+
render() {
|
|
3333
|
+
const box = this.createElement('div', ['nk-settings-user']);
|
|
3334
|
+
this._avatar = this.createElement('div', ['avatar']);
|
|
3335
|
+
const s = this.createElement('slot', [], { name: 'avatar' });
|
|
3336
|
+
s.appendChild(this._avatar);
|
|
3337
|
+
const text = this.createElement('div', ['u-text']);
|
|
3338
|
+
this._name = this.createElement('div', ['name']);
|
|
3339
|
+
this._mail = this.createElement('div', ['mail']);
|
|
3340
|
+
text.append(this._name, this._mail);
|
|
3341
|
+
box.append(s, text);
|
|
3342
|
+
this._wrapper.appendChild(box);
|
|
3343
|
+
this._sync();
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
_sync() {
|
|
3347
|
+
const name = this.getAttribute('name') || '';
|
|
3348
|
+
this._name.textContent = name;
|
|
3349
|
+
const mail = this.getAttribute('mail');
|
|
3350
|
+
this._mail.textContent = mail || '';
|
|
3351
|
+
this._mail.style.display = mail ? '' : 'none';
|
|
3352
|
+
this._avatar.textContent = this.getAttribute('avatar') || name.split(/\s+/).map(w => w[0]).join('').slice(0, 2).toUpperCase();
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
onAttributeChanged() { this._sync(); }
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
customElements.define('nk-settings-user', NkSettingsUser);
|
|
3359
|
+
|
|
3360
|
+
// Subsequence scoring for the command palette: every query character must
|
|
3361
|
+
// appear in order; consecutive hits, word starts and an early first hit score
|
|
3362
|
+
// higher. Returns 0 when the query does not match.
|
|
3363
|
+
function fuzzyScore(query, text) {
|
|
3364
|
+
const q = query.trim().toLowerCase();
|
|
3365
|
+
if (!q) return 1;
|
|
3366
|
+
const t = text.toLowerCase();
|
|
3367
|
+
let ti = 0, score = 0, streak = 0;
|
|
3368
|
+
for (let qi = 0; qi < q.length; qi++) {
|
|
3369
|
+
const idx = t.indexOf(q[qi], ti);
|
|
3370
|
+
if (idx < 0) return 0;
|
|
3371
|
+
const wordStart = idx === 0 || /[\s\-_/.]/.test(t[idx - 1]);
|
|
3372
|
+
streak = idx === ti ? streak + 1 : 0;
|
|
3373
|
+
score += 1 + streak * 2 + (wordStart ? 3 : 0) - Math.min(3, (idx - ti) * 0.25);
|
|
3374
|
+
ti = idx + 1;
|
|
3375
|
+
}
|
|
3376
|
+
if (t.startsWith(q)) score += 10;
|
|
3377
|
+
return Math.max(0.01, score / (1 + t.length / 40));
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
// <nk-cmdk id="palette" placeholder="Search or type a command …"></nk-cmdk>
|
|
3381
|
+
// palette.commands = [
|
|
3382
|
+
// { group: 'Pages', items: [{ id: 'mvp', icon: '🚀', label: 'NotionKit MVP', keywords: 'project' }] },
|
|
3383
|
+
// { group: 'Actions', items: [{ id: 'theme', icon: '🌙', label: 'Toggle theme', shortcut: '⌘⇧L', action: () => … }] },
|
|
3384
|
+
// ];
|
|
3385
|
+
// → <div class="nk-cmdk-backdrop open"><div class="nk-cmdk"><div class="nk-cmdk-input-row">🔍<input><kbd>esc</kbd></div>
|
|
3386
|
+
// <div class="nk-cmdk-list">…</div><div class="nk-cmdk-footer">…</div></div></div>
|
|
3387
|
+
// mod+K toggles (attribute `hotkey`), ↑↓ move, Enter picks (nk-command +
|
|
3388
|
+
// item.action), Escape / backdrop close. Fuzzy subsequence search over
|
|
3389
|
+
// label + keywords. Put the element directly under <body>.
|
|
3390
|
+
class NkCmdk extends NkElement {
|
|
3391
|
+
static get observedAttributes() { return ['open', 'hotkey', 'placeholder']; }
|
|
3392
|
+
|
|
3393
|
+
render() {
|
|
3394
|
+
this._backdrop = this.createElement('div', ['nk-cmdk-backdrop']);
|
|
3395
|
+
this._backdrop.inert = true;
|
|
3396
|
+
this._box = this.createElement('div', ['nk-cmdk'], { role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Command palette' });
|
|
3397
|
+
const row = this.createElement('div', ['nk-cmdk-input-row']);
|
|
3398
|
+
const glass = document.createElement('span');
|
|
3399
|
+
glass.style.fontSize = '15px';
|
|
3400
|
+
glass.textContent = '🔍';
|
|
3401
|
+
this._input = this.createElement('input', [], { type: 'text', autocomplete: 'off', role: 'combobox', 'aria-expanded': 'true', 'aria-autocomplete': 'list' });
|
|
3402
|
+
const esc = this.createElement('kbd', ['nk-kbd']);
|
|
3403
|
+
esc.textContent = 'esc';
|
|
3404
|
+
row.append(glass, this._input, esc);
|
|
3405
|
+
this._list = this.createElement('div', ['nk-cmdk-list'], { role: 'listbox', id: 'list' });
|
|
3406
|
+
this._input.setAttribute('aria-controls', 'list');
|
|
3407
|
+
this._footer = this.createElement('div', ['nk-cmdk-footer']);
|
|
3408
|
+
const footerSlot = this.createElement('slot', [], { name: 'footer' });
|
|
3409
|
+
footerSlot.innerHTML = '<span><kbd class="nk-kbd">↑</kbd><kbd class="nk-kbd">↓</kbd> navigate</span><span><kbd class="nk-kbd">↵</kbd> open</span><span><kbd class="nk-kbd">⌘</kbd><kbd class="nk-kbd">K</kbd> toggle</span>';
|
|
3410
|
+
this._footer.appendChild(footerSlot);
|
|
3411
|
+
this._box.append(row, this._list, this._footer);
|
|
3412
|
+
this._backdrop.appendChild(this._box);
|
|
3413
|
+
this._wrapper.appendChild(this._backdrop);
|
|
3414
|
+
this._commands = [];
|
|
3415
|
+
this._index = 0;
|
|
3416
|
+
this._syncAttrs();
|
|
3417
|
+
this._renderList();
|
|
3418
|
+
this._syncOpen();
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
_syncAttrs() { this._input.placeholder = this.getAttribute('placeholder') || 'Search or type a command …'; }
|
|
3422
|
+
|
|
3423
|
+
/** Filtered, flat list in group order, best matches first within a group. */
|
|
3424
|
+
results() {
|
|
3425
|
+
const q = this._input?.value ?? '';
|
|
3426
|
+
const out = [];
|
|
3427
|
+
for (const group of this._commands) {
|
|
3428
|
+
const scored = (group.items || []).map(item => ({ item, group: group.group, score: fuzzyScore(q, `${item.label} ${item.keywords || ''}`) })).filter(r => r.score > 0);
|
|
3429
|
+
scored.sort((a, b) => b.score - a.score);
|
|
3430
|
+
out.push(...scored);
|
|
3431
|
+
}
|
|
3432
|
+
return out;
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
_renderList() {
|
|
3436
|
+
if (!this._list) return;
|
|
3437
|
+
const results = this.results();
|
|
3438
|
+
this._index = Math.min(this._index, Math.max(0, results.length - 1));
|
|
3439
|
+
this._list.replaceChildren();
|
|
3440
|
+
if (!results.length) {
|
|
3441
|
+
const empty = this.createElement('div', ['nk-cmdk-empty']);
|
|
3442
|
+
empty.textContent = `No results for “${this._input.value}”`;
|
|
3443
|
+
this._list.appendChild(empty);
|
|
3444
|
+
this._input.removeAttribute('aria-activedescendant');
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
let lastGroup = null;
|
|
3448
|
+
results.forEach((r, i) => {
|
|
3449
|
+
if (r.group && r.group !== lastGroup) {
|
|
3450
|
+
const g = this.createElement('div', ['nk-cmdk-group']);
|
|
3451
|
+
g.textContent = r.group;
|
|
3452
|
+
this._list.appendChild(g);
|
|
3453
|
+
lastGroup = r.group;
|
|
3454
|
+
}
|
|
3455
|
+
const el = this.createElement('div', ['nk-cmdk-item'], { role: 'option', id: `opt-${i}`, 'data-index': i });
|
|
3456
|
+
el.classList.toggle('selected', i === this._index);
|
|
3457
|
+
el.setAttribute('aria-selected', i === this._index ? 'true' : 'false');
|
|
3458
|
+
const icon = this.createElement('span', ['m-icon']);
|
|
3459
|
+
icon.textContent = r.item.icon || '';
|
|
3460
|
+
const label = document.createElement('span');
|
|
3461
|
+
label.textContent = r.item.label;
|
|
3462
|
+
el.append(icon, label);
|
|
3463
|
+
if (r.item.shortcut) { const sc = this.createElement('span', ['m-shortcut']); sc.textContent = r.item.shortcut; el.appendChild(sc); }
|
|
3464
|
+
this._list.appendChild(el);
|
|
3465
|
+
});
|
|
3466
|
+
this._input.setAttribute('aria-activedescendant', `opt-${this._index}`);
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
_move(dir) {
|
|
3470
|
+
const n = this.results().length;
|
|
3471
|
+
if (!n) return;
|
|
3472
|
+
this._index = (this._index + dir + n) % n;
|
|
3473
|
+
for (const el of this._list.querySelectorAll('.nk-cmdk-item')) {
|
|
3474
|
+
const on = Number(el.dataset.index) === this._index;
|
|
3475
|
+
el.classList.toggle('selected', on);
|
|
3476
|
+
el.setAttribute('aria-selected', on ? 'true' : 'false');
|
|
3477
|
+
if (on) el.scrollIntoView({ block: 'nearest' });
|
|
3478
|
+
}
|
|
3479
|
+
this._input.setAttribute('aria-activedescendant', `opt-${this._index}`);
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
pick(index = this._index) {
|
|
3483
|
+
const r = this.results()[index];
|
|
3484
|
+
if (!r) return;
|
|
3485
|
+
const query = this._input.value;
|
|
3486
|
+
this.close();
|
|
3487
|
+
const ok = this.emit('nk-command', { id: r.item.id ?? r.item.label, item: r.item, query });
|
|
3488
|
+
if (ok && typeof r.item.action === 'function') r.item.action(r.item);
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
_matchesHotkey(e) {
|
|
3492
|
+
const spec = (this.getAttribute('hotkey') || 'mod+k').toLowerCase().split('+');
|
|
3493
|
+
const key = spec.pop();
|
|
3494
|
+
const mod = spec.includes('mod') ? (e.metaKey || e.ctrlKey) : true;
|
|
3495
|
+
const shift = spec.includes('shift') ? e.shiftKey : !e.shiftKey;
|
|
3496
|
+
const alt = spec.includes('alt') ? e.altKey : !e.altKey;
|
|
3497
|
+
return mod && shift && alt && e.key.toLowerCase() === key;
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
_syncOpen() {
|
|
3501
|
+
const open = this.getBoolAttr('open');
|
|
3502
|
+
this._backdrop.classList.toggle('open', open);
|
|
3503
|
+
this._backdrop.inert = !open;
|
|
3504
|
+
if (open === this._wasOpen) return;
|
|
3505
|
+
this._wasOpen = open;
|
|
3506
|
+
if (open) {
|
|
3507
|
+
this._returnFocus = document.activeElement;
|
|
3508
|
+
this._input.value = '';
|
|
3509
|
+
this._index = 0;
|
|
3510
|
+
this._renderList();
|
|
3511
|
+
lockScroll();
|
|
3512
|
+
this._undoInert = inertOutside(this);
|
|
3513
|
+
requestAnimationFrame(() => this._input.focus({ preventScroll: true }));
|
|
3514
|
+
} else {
|
|
3515
|
+
unlockScroll();
|
|
3516
|
+
this._undoInert?.(); this._undoInert = null;
|
|
3517
|
+
const back = this._returnFocus;
|
|
3518
|
+
if (back && typeof back.focus === 'function' && back.isConnected) back.focus({ preventScroll: true });
|
|
3519
|
+
this._returnFocus = null;
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
setupEvents() {
|
|
3524
|
+
this._onDocKey = (e) => {
|
|
3525
|
+
if (this._matchesHotkey(e)) { e.preventDefault(); this.toggle(); return; }
|
|
3526
|
+
if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); }
|
|
3527
|
+
};
|
|
3528
|
+
this._onInput = () => { this._index = 0; this._renderList(); };
|
|
3529
|
+
this._onInputKey = (e) => {
|
|
3530
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); this._move(1); }
|
|
3531
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); this._move(-1); }
|
|
3532
|
+
else if (e.key === 'Enter') { e.preventDefault(); this.pick(); }
|
|
3533
|
+
};
|
|
3534
|
+
this._onListClick = (e) => { const el = e.target.closest('.nk-cmdk-item'); if (el) this.pick(Number(el.dataset.index)); };
|
|
3535
|
+
this._onListMove = (e) => { const el = e.target.closest('.nk-cmdk-item'); if (el && Number(el.dataset.index) !== this._index) { this._index = Number(el.dataset.index); this._move(0); } };
|
|
3536
|
+
this._onListDown = (e) => e.preventDefault(); // keep the input focused
|
|
3537
|
+
this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(); };
|
|
3538
|
+
document.addEventListener('keydown', this._onDocKey);
|
|
3539
|
+
this._input.addEventListener('input', this._onInput);
|
|
3540
|
+
this._input.addEventListener('keydown', this._onInputKey);
|
|
3541
|
+
this._list.addEventListener('click', this._onListClick);
|
|
3542
|
+
this._list.addEventListener('mousemove', this._onListMove);
|
|
3543
|
+
this._list.addEventListener('mousedown', this._onListDown);
|
|
3544
|
+
this._backdrop.addEventListener('click', this._onBackdrop);
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
teardownEvents() {
|
|
3548
|
+
document.removeEventListener('keydown', this._onDocKey);
|
|
3549
|
+
this._input?.removeEventListener('input', this._onInput);
|
|
3550
|
+
this._input?.removeEventListener('keydown', this._onInputKey);
|
|
3551
|
+
this._list?.removeEventListener('click', this._onListClick);
|
|
3552
|
+
this._list?.removeEventListener('mousemove', this._onListMove);
|
|
3553
|
+
this._list?.removeEventListener('mousedown', this._onListDown);
|
|
3554
|
+
this._backdrop?.removeEventListener('click', this._onBackdrop);
|
|
3555
|
+
if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
onAttributeChanged(name) {
|
|
3559
|
+
if (name === 'open') { this._syncOpen(); this.emit('nk-toggle', { open: this.getBoolAttr('open') }); }
|
|
3560
|
+
else this._syncAttrs();
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
show() { this.setBoolAttr('open', true); }
|
|
3564
|
+
close() { this.setBoolAttr('open', false); }
|
|
3565
|
+
toggle() { this.setBoolAttr('open', !this.getBoolAttr('open')); }
|
|
3566
|
+
|
|
3567
|
+
get commands() { return this._commands; }
|
|
3568
|
+
set commands(list) { this._commands = Array.isArray(list) ? list : []; this._index = 0; this._renderList(); }
|
|
3569
|
+
get open() { return this.getBoolAttr('open'); }
|
|
3570
|
+
set open(v) { this.setBoolAttr('open', v); }
|
|
3571
|
+
get query() { return this._input?.value ?? ''; }
|
|
3572
|
+
set query(v) { if (this._input) { this._input.value = v; this._index = 0; this._renderList(); } }
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
customElements.define('nk-cmdk', NkCmdk);
|
|
3576
|
+
|
|
3577
|
+
// <nk-menu>
|
|
3578
|
+
// <nk-menu-item type="label">Page</nk-menu-item>
|
|
3579
|
+
// <nk-menu-item icon="✏️" shortcut="⌘E" value="rename">Rename</nk-menu-item>
|
|
3580
|
+
// <nk-menu-item type="separator"></nk-menu-item>
|
|
3581
|
+
// <nk-menu-item icon="🗑️" danger value="delete">Delete</nk-menu-item>
|
|
3582
|
+
// </nk-menu>
|
|
3583
|
+
// → <div class="nk-pop nk-menu" role="menu">…</div>
|
|
3584
|
+
// ↑↓ move between items, Enter selects, nk-select bubbles from the item.
|
|
3585
|
+
class NkMenu extends NkElement {
|
|
3586
|
+
render() {
|
|
3587
|
+
this._box = this.createElement('div', ['nk-pop', 'nk-menu'], { role: 'menu' });
|
|
3588
|
+
this._box.appendChild(document.createElement('slot'));
|
|
3589
|
+
this._wrapper.appendChild(this._box);
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
get items() { return [...this.querySelectorAll(':scope > nk-menu-item')].filter(i => i.selectable); }
|
|
3593
|
+
|
|
3594
|
+
setupEvents() {
|
|
3595
|
+
this._onKey = (e) => {
|
|
3596
|
+
const dir = { ArrowDown: 1, ArrowUp: -1 }[e.key];
|
|
3597
|
+
const current = e.target.closest?.('nk-menu-item');
|
|
3598
|
+
const items = this.items;
|
|
3599
|
+
if (!items.length) return;
|
|
3600
|
+
if (dir) {
|
|
3601
|
+
e.preventDefault();
|
|
3602
|
+
const i = items.indexOf(current);
|
|
3603
|
+
items[(i + dir + items.length) % items.length].focus();
|
|
3604
|
+
} else if ((e.key === 'Enter' || e.key === ' ') && current) {
|
|
3605
|
+
e.preventDefault(); current.select();
|
|
3606
|
+
}
|
|
3607
|
+
};
|
|
3608
|
+
this.addEventListener('keydown', this._onKey);
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
teardownEvents() { this.removeEventListener('keydown', this._onKey); }
|
|
3612
|
+
|
|
3613
|
+
/** Focuses the first item (called by a popover when it opens). */
|
|
3614
|
+
focusFirst() { this.items[0]?.focus(); }
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
customElements.define('nk-menu', NkMenu);
|
|
3618
|
+
|
|
3619
|
+
// <nk-menu-item icon="✏️" shortcut="⌘E" value="rename">Rename</nk-menu-item>
|
|
3620
|
+
// <nk-menu-item type="separator"></nk-menu-item> <nk-menu-item type="label">Danger</nk-menu-item>
|
|
3621
|
+
// <nk-menu-item icon="🗑️" danger value="delete">Delete</nk-menu-item>
|
|
3622
|
+
// → <div class="nk-menu-item danger"><span class="m-icon">🗑️</span>Delete<span class="m-shortcut">…</span></div>
|
|
3623
|
+
class NkMenuItem extends NkElement {
|
|
3624
|
+
static get observedAttributes() { return ['icon', 'shortcut', 'danger', 'value', 'type', 'disabled']; }
|
|
3625
|
+
|
|
3626
|
+
render() { this._build(); }
|
|
3627
|
+
|
|
3628
|
+
_build() {
|
|
3629
|
+
const type = this.getAttribute('type');
|
|
3630
|
+
let el;
|
|
3631
|
+
if (type === 'separator') {
|
|
3632
|
+
el = this.createElement('div', ['nk-menu-sep'], { role: 'separator' });
|
|
3633
|
+
} else if (type === 'label') {
|
|
3634
|
+
el = this.createElement('div', ['nk-menu-label']);
|
|
3635
|
+
el.appendChild(document.createElement('slot'));
|
|
3636
|
+
} else {
|
|
3637
|
+
el = this.createElement('div', ['nk-menu-item'], { role: 'menuitem', tabindex: '-1' });
|
|
3638
|
+
this._icon = this.createElement('span', ['m-icon']);
|
|
3639
|
+
const iconSlot = this.createElement('slot', [], { name: 'icon' });
|
|
3640
|
+
iconSlot.appendChild(this._icon);
|
|
3641
|
+
this._shortcut = this.createElement('span', ['m-shortcut']);
|
|
3642
|
+
el.append(iconSlot, document.createElement('slot'), this._shortcut);
|
|
3643
|
+
}
|
|
3644
|
+
if (this._el) { this._el.removeEventListener('click', this._onClick); this._el.replaceWith(el); } else this._wrapper.appendChild(el);
|
|
3645
|
+
this._el = el;
|
|
3646
|
+
if (this._onClick) el.addEventListener('click', this._onClick);
|
|
3647
|
+
this._sync();
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
_sync() {
|
|
3651
|
+
if (!this._el.classList.contains('nk-menu-item')) return;
|
|
3652
|
+
const icon = this.getAttribute('icon');
|
|
3653
|
+
this._icon.textContent = icon || '';
|
|
3654
|
+
this._icon.style.display = icon ? '' : 'none';
|
|
3655
|
+
const sc = this.getAttribute('shortcut');
|
|
3656
|
+
this._shortcut.textContent = sc || '';
|
|
3657
|
+
this._shortcut.style.display = sc ? '' : 'none';
|
|
3658
|
+
this._el.classList.toggle('danger', this.getBoolAttr('danger'));
|
|
3659
|
+
this._el.setAttribute('aria-disabled', this.getBoolAttr('disabled') ? 'true' : 'false');
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
setupEvents() {
|
|
3663
|
+
this._onClick = () => this.select();
|
|
3664
|
+
this._el.addEventListener('click', this._onClick);
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
teardownEvents() { this._el?.removeEventListener('click', this._onClick); }
|
|
3668
|
+
|
|
3669
|
+
select() {
|
|
3670
|
+
if (!this._el.classList.contains('nk-menu-item') || this.getBoolAttr('disabled')) return;
|
|
3671
|
+
this.emit('nk-select', { value: this.value, label: this.textContent.trim(), item: this });
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
onAttributeChanged(name) { name === 'type' ? this._build() : this._sync(); }
|
|
3675
|
+
focus(o) { this._el?.focus(o); }
|
|
3676
|
+
get selectable() { return this._el?.classList.contains('nk-menu-item') && !this.getBoolAttr('disabled'); }
|
|
3677
|
+
|
|
3678
|
+
get value() { return this.getAttribute('value') ?? this.textContent.trim(); }
|
|
3679
|
+
set value(v) { this.setAttribute('value', v); }
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
customElements.define('nk-menu-item', NkMenuItem);
|
|
3683
|
+
|
|
3684
|
+
// <nk-pop placement="bottom-start">
|
|
3685
|
+
// <nk-btn slot="trigger" variant="secondary">Options</nk-btn>
|
|
3686
|
+
// <nk-menu>…</nk-menu>
|
|
3687
|
+
// </nk-pop>
|
|
3688
|
+
// The trigger stays in the light DOM; a click on it toggles `open`, outside
|
|
3689
|
+
// clicks and Escape close. Content is wrapped in `.nk-pop` unless it brings
|
|
3690
|
+
// its own surface (nk-menu, nk-emoji-picker) or `bare` is set. The only
|
|
3691
|
+
// styling here is positioning – the surface look is the stylesheet's.
|
|
3692
|
+
const popSheet = new CSSStyleSheet();
|
|
3693
|
+
popSheet.replaceSync(`
|
|
3694
|
+
.nk-pop-anchor { position: relative; display: inline-block; }
|
|
3695
|
+
.nk-pop-float { display: none; position: absolute; z-index: 50; top: 100%; left: 0; margin-top: 4px; }
|
|
3696
|
+
:host([open]) .nk-pop-float { display: block; }
|
|
3697
|
+
:host([placement="bottom-end"]) .nk-pop-float { left: auto; right: 0; }
|
|
3698
|
+
:host([placement="top-start"]) .nk-pop-float { top: auto; bottom: 100%; margin: 0 0 4px; }
|
|
3699
|
+
:host([placement="top-end"]) .nk-pop-float { top: auto; bottom: 100%; left: auto; right: 0; margin: 0 0 4px; }
|
|
3700
|
+
`);
|
|
3701
|
+
const OWN_SURFACE = new Set(['nk-menu', 'nk-emoji-picker']);
|
|
3702
|
+
|
|
3703
|
+
class NkPop extends NkElement {
|
|
3704
|
+
static get hostStyles() { return popSheet; }
|
|
3705
|
+
static get observedAttributes() { return ['open', 'placement', 'bare']; }
|
|
3706
|
+
|
|
3707
|
+
render() {
|
|
3708
|
+
const anchor = this.createElement('div', ['nk-pop-anchor']);
|
|
3709
|
+
this._triggerSlot = this.createElement('slot', [], { name: 'trigger' });
|
|
3710
|
+
this._float = this.createElement('div', ['nk-pop-float']);
|
|
3711
|
+
this._surface = this.createElement('div', ['nk-pop']);
|
|
3712
|
+
this._slot = document.createElement('slot');
|
|
3713
|
+
this._surface.appendChild(this._slot);
|
|
3714
|
+
this._float.appendChild(this._surface);
|
|
3715
|
+
anchor.append(this._triggerSlot, this._float);
|
|
3716
|
+
this._wrapper.appendChild(anchor);
|
|
3717
|
+
this._syncSurface();
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
_syncSurface() {
|
|
3721
|
+
const bare = this.getBoolAttr('bare') || this._slot.assignedElements().some(el => OWN_SURFACE.has(el.localName));
|
|
3722
|
+
this._surface.className = bare ? '' : 'nk-pop';
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
setupEvents() {
|
|
3726
|
+
this._onTrigger = (e) => {
|
|
3727
|
+
if (!e.composedPath().some(n => n instanceof Element && n.getAttribute?.('slot') === 'trigger')) return;
|
|
3728
|
+
e.stopPropagation();
|
|
3729
|
+
this.toggle();
|
|
3730
|
+
};
|
|
3731
|
+
this._onDocClick = (e) => { if (this.getBoolAttr('open') && !e.composedPath().includes(this)) this.close(); };
|
|
3732
|
+
this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) this.close(); };
|
|
3733
|
+
this._onSelect = (e) => { if (e.target !== this) this.close(); };
|
|
3734
|
+
this._onSlot = () => this._syncSurface();
|
|
3735
|
+
this.addEventListener('click', this._onTrigger);
|
|
3736
|
+
document.addEventListener('click', this._onDocClick);
|
|
3737
|
+
document.addEventListener('keydown', this._onKey);
|
|
3738
|
+
this._float.addEventListener('nk-select', this._onSelect);
|
|
3739
|
+
this._slot.addEventListener('slotchange', this._onSlot);
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
teardownEvents() {
|
|
3743
|
+
this.removeEventListener('click', this._onTrigger);
|
|
3744
|
+
document.removeEventListener('click', this._onDocClick);
|
|
3745
|
+
document.removeEventListener('keydown', this._onKey);
|
|
3746
|
+
this._float?.removeEventListener('nk-select', this._onSelect);
|
|
3747
|
+
this._slot?.removeEventListener('slotchange', this._onSlot);
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
onAttributeChanged(name) {
|
|
3751
|
+
if (name === 'bare') this._syncSurface();
|
|
3752
|
+
if (name === 'open') {
|
|
3753
|
+
const open = this.getBoolAttr('open');
|
|
3754
|
+
for (const el of this._triggerSlot.assignedElements()) el.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
3755
|
+
this.emit('nk-toggle', { open });
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
show() { this.setBoolAttr('open', true); }
|
|
3760
|
+
close() { this.setBoolAttr('open', false); }
|
|
3761
|
+
toggle() { this.setBoolAttr('open', !this.getBoolAttr('open')); }
|
|
3762
|
+
|
|
3763
|
+
get open() { return this.getBoolAttr('open'); }
|
|
3764
|
+
set open(v) { this.setBoolAttr('open', v); }
|
|
3765
|
+
get placement() { return this.getAttribute('placement') || 'bottom-start'; }
|
|
3766
|
+
set placement(v) { this.setAttribute('placement', v); }
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
customElements.define('nk-pop', NkPop);
|
|
3770
|
+
|
|
3771
|
+
// Built-in emoji set for <nk-emoji-picker>: eight categories, names for
|
|
3772
|
+
// search. Override with `picker.emojis = [{ char, name, cat }]`.
|
|
3773
|
+
const C = (cat, icon, list) => list.map(([char, name]) => ({ char, name, cat, icon }));
|
|
3774
|
+
const EMOJI_CATEGORIES = [
|
|
3775
|
+
{ id: 'smileys', icon: '😀' }, { id: 'people', icon: '👋' }, { id: 'nature', icon: '🌿' }, { id: 'food', icon: '☕' },
|
|
3776
|
+
{ id: 'activity', icon: '🎯' }, { id: 'travel', icon: '🚀' }, { id: 'objects', icon: '💡' }, { id: 'symbols', icon: '✅' },
|
|
3777
|
+
];
|
|
3778
|
+
const EMOJI = [
|
|
3779
|
+
...C('smileys', '😀', [['😀', 'grinning'], ['😊', 'smile blush'], ['😂', 'joy laugh'], ['🙂', 'slight smile'], ['😉', 'wink'], ['😍', 'heart eyes love'], ['🤔', 'thinking'], ['😎', 'cool sunglasses'], ['🥳', 'party'], ['😴', 'sleep'], ['🤯', 'mind blown'], ['😅', 'sweat smile'], ['🙃', 'upside down'], ['😇', 'angel halo'], ['🤗', 'hug'], ['😢', 'cry sad']]),
|
|
3780
|
+
...C('people', '👋', [['👋', 'wave hello'], ['👍', 'thumbs up'], ['👎', 'thumbs down'], ['🙏', 'pray thanks'], ['👏', 'clap'], ['🤝', 'handshake deal'], ['💪', 'strong'], ['👀', 'eyes look'], ['🧠', 'brain knowledge'], ['❤️', 'heart love'], ['👤', 'person user'], ['👥', 'people team members'], ['🧑💻', 'developer'], ['🙋', 'raise hand'], ['✍️', 'writing'], ['🫶', 'heart hands']]),
|
|
3781
|
+
...C('nature', '🌿', [['🌿', 'leaf herb'], ['🌱', 'seedling'], ['🌳', 'tree'], ['🌸', 'blossom flower'], ['🌞', 'sun'], ['🌙', 'moon night'], ['⭐', 'star'], ['🔥', 'fire hot'], ['🌈', 'rainbow'], ['🌊', 'wave water'], ['🍀', 'clover luck'], ['🐝', 'bee'], ['🦋', 'butterfly'], ['🐢', 'turtle'], ['🌍', 'earth globe'], ['❄️', 'snow']]),
|
|
3782
|
+
...C('food', '☕', [['☕', 'coffee'], ['🍵', 'tea'], ['🍎', 'apple'], ['🍕', 'pizza'], ['🍰', 'cake'], ['🍪', 'cookie'], ['🥐', 'croissant'], ['🍔', 'burger'], ['🍜', 'noodles'], ['🍺', 'beer'], ['🍷', 'wine'], ['🥗', 'salad'], ['🍫', 'chocolate'], ['🍓', 'strawberry'], ['🥑', 'avocado'], ['🍩', 'donut']]),
|
|
3783
|
+
...C('activity', '🎯', [['🎯', 'target goal'], ['🏆', 'trophy win'], ['🎉', 'tada celebration'], ['🎨', 'art palette design'], ['🎙️', 'microphone podcast voice'], ['🎬', 'clapper movie'], ['🎮', 'game'], ['⚽', 'football'], ['🏃', 'run'], ['🧘', 'yoga calm'], ['🎵', 'music'], ['🎲', 'dice'], ['🏁', 'finish flag'], ['🎓', 'graduation learn'], ['🧩', 'puzzle piece'], ['🔁', 'repeat retro']]),
|
|
3784
|
+
...C('travel', '🚀', [['🚀', 'rocket launch'], ['✈️', 'plane travel'], ['🚗', 'car'], ['🚲', 'bike'], ['🏠', 'house home'], ['🏢', 'office building'], ['🗺️', 'map'], ['🧭', 'compass'], ['⛰️', 'mountain'], ['🏖️', 'beach'], ['🚉', 'station'], ['🛰️', 'satellite'], ['🌆', 'city'], ['🏕️', 'camping'], ['⛵', 'sailboat'], ['🛫', 'departure']]),
|
|
3785
|
+
...C('objects', '💡', [['💡', 'idea bulb'], ['📊', 'chart bar'], ['📈', 'chart up'], ['📅', 'calendar date'], ['📄', 'page document'], ['📁', 'folder'], ['🗃️', 'card box database'], ['📌', 'pin'], ['🔍', 'search'], ['🛠️', 'tools'], ['⚙️', 'gear settings'], ['🔔', 'bell notification'], ['💎', 'gem diamond'], ['📝', 'memo notes'], ['📎', 'paperclip files'], ['🔒', 'lock secure'], ['💬', 'speech comment'], ['📤', 'export outbox'], ['📥', 'inbox'], ['🗑️', 'trash bin'], ['🖥️', 'desktop'], ['📱', 'phone'], ['💻', 'laptop'], ['🧾', 'receipt']]),
|
|
3786
|
+
...C('symbols', '✅', [['✅', 'check done'], ['✨', 'sparkles ai magic'], ['❌', 'cross no'], ['⚠️', 'warning'], ['ℹ️', 'info'], ['❓', 'question'], ['➕', 'plus add'], ['➡️', 'arrow right'], ['🔴', 'red circle'], ['🟢', 'green circle'], ['🟡', 'yellow circle'], ['🔵', 'blue circle'], ['⏳', 'hourglass wait'], ['♻️', 'recycle'], ['🆕', 'new'], ['💯', 'hundred']]),
|
|
3787
|
+
];
|
|
3788
|
+
|
|
3789
|
+
// <nk-emoji-picker></nk-emoji-picker>
|
|
3790
|
+
// → <div class="nk-pop"><input class="nk-emoji-search"><div class="nk-emoji-grid">…</div><div class="nk-emoji-cats">…</div></div>
|
|
3791
|
+
// Built-in set with names for search; `picker.emojis = [{ char, name, cat }]`
|
|
3792
|
+
// replaces it. A click fires nk-select { emoji }.
|
|
3793
|
+
class NkEmojiPicker extends NkElement {
|
|
3794
|
+
static get observedAttributes() { return ['placeholder', 'value']; }
|
|
3795
|
+
|
|
3796
|
+
render() {
|
|
3797
|
+
this._box = this.createElement('div', ['nk-pop']);
|
|
3798
|
+
this._search = this.createElement('input', ['nk-emoji-search'], { type: 'search', autocomplete: 'off', 'aria-label': 'Search emoji' });
|
|
3799
|
+
this._grid = this.createElement('div', ['nk-emoji-grid'], { role: 'listbox' });
|
|
3800
|
+
this._cats = this.createElement('div', ['nk-emoji-cats'], { role: 'tablist' });
|
|
3801
|
+
this._box.append(this._search, this._grid, this._cats);
|
|
3802
|
+
this._wrapper.appendChild(this._box);
|
|
3803
|
+
this._emojis = EMOJI;
|
|
3804
|
+
this._cat = EMOJI_CATEGORIES[0].id;
|
|
3805
|
+
this._renderCats();
|
|
3806
|
+
this._renderGrid();
|
|
3807
|
+
this._syncAttrs();
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
_syncAttrs() { this._search.placeholder = this.getAttribute('placeholder') || 'Search…'; }
|
|
3811
|
+
|
|
3812
|
+
_renderCats() {
|
|
3813
|
+
this._cats.replaceChildren();
|
|
3814
|
+
const cats = this._categories();
|
|
3815
|
+
for (const cat of cats) {
|
|
3816
|
+
const el = this.createElement('span', [], { role: 'tab', 'data-cat': cat.id, title: cat.id });
|
|
3817
|
+
el.textContent = cat.icon;
|
|
3818
|
+
el.classList.toggle('active', cat.id === this._cat);
|
|
3819
|
+
this._cats.appendChild(el);
|
|
3820
|
+
}
|
|
3821
|
+
this._cats.style.display = cats.length > 1 ? '' : 'none';
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
_categories() {
|
|
3825
|
+
if (this._catList) return this._catList;
|
|
3826
|
+
const seen = new Map();
|
|
3827
|
+
for (const e of this._emojis) if (e.cat && !seen.has(e.cat)) seen.set(e.cat, { id: e.cat, icon: e.icon || EMOJI_CATEGORIES.find(c => c.id === e.cat)?.icon || e.char });
|
|
3828
|
+
return [...seen.values()];
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
_renderGrid() {
|
|
3832
|
+
const q = this._search.value.trim().toLowerCase();
|
|
3833
|
+
const list = q ? this._emojis.filter(e => (e.name || '').toLowerCase().includes(q) || e.char === q) : this._emojis.filter(e => !e.cat || e.cat === this._cat);
|
|
3834
|
+
this._grid.replaceChildren();
|
|
3835
|
+
for (const e of list.slice(0, 64)) {
|
|
3836
|
+
const s = this.createElement('span', [], { role: 'option', title: e.name || '', 'data-emoji': e.char });
|
|
3837
|
+
s.textContent = e.char;
|
|
3838
|
+
this._grid.appendChild(s);
|
|
3839
|
+
}
|
|
3840
|
+
for (const el of this._cats.children) el.classList.toggle('active', !q && el.dataset.cat === this._cat);
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
setupEvents() {
|
|
3844
|
+
this._onInput = () => this._renderGrid();
|
|
3845
|
+
this._onGrid = (e) => { const s = e.target.closest('[data-emoji]'); if (s) this.select(s.dataset.emoji); };
|
|
3846
|
+
this._onCats = (e) => { const c = e.target.closest('[data-cat]'); if (!c) return; this._cat = c.dataset.cat; this._search.value = ''; this._renderGrid(); };
|
|
3847
|
+
this._onKey = (e) => { if (e.key === 'Enter') { const first = this._grid.querySelector('[data-emoji]'); if (first) this.select(first.dataset.emoji); } };
|
|
3848
|
+
this._search.addEventListener('input', this._onInput);
|
|
3849
|
+
this._search.addEventListener('keydown', this._onKey);
|
|
3850
|
+
this._grid.addEventListener('click', this._onGrid);
|
|
3851
|
+
this._cats.addEventListener('click', this._onCats);
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
teardownEvents() {
|
|
3855
|
+
this._search?.removeEventListener('input', this._onInput);
|
|
3856
|
+
this._search?.removeEventListener('keydown', this._onKey);
|
|
3857
|
+
this._grid?.removeEventListener('click', this._onGrid);
|
|
3858
|
+
this._cats?.removeEventListener('click', this._onCats);
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
select(emoji) {
|
|
3862
|
+
this.setAttribute('value', emoji);
|
|
3863
|
+
this.emit('nk-select', { emoji, value: emoji });
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
onAttributeChanged() { this._syncAttrs(); }
|
|
3867
|
+
focus(o) { this._search?.focus(o); }
|
|
3868
|
+
|
|
3869
|
+
/** Category strip override: [{ id, icon }]. Emojis without `cat` show in every category. */
|
|
3870
|
+
get categories() { return this._categories(); }
|
|
3871
|
+
set categories(list) { this._catList = Array.isArray(list) && list.length ? list : null; this._cat = this._categories()[0]?.id; this._renderCats(); this._renderGrid(); }
|
|
3872
|
+
get emojis() { return this._emojis; }
|
|
3873
|
+
set emojis(list) { this._emojis = Array.isArray(list) ? list : EMOJI; this._cat = this._categories()[0]?.id; this._renderCats(); this._renderGrid(); }
|
|
3874
|
+
get value() { return this.getAttribute('value'); }
|
|
3875
|
+
set value(v) { this.setAttribute('value', v); }
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
customElements.define('nk-emoji-picker', NkEmojiPicker);
|
|
3879
|
+
|
|
3880
|
+
// <nk-toast id="toast"></nk-toast> … toast.show('Saved');
|
|
3881
|
+
// → <div class="nk-toast show">✓ Saved</div>
|
|
3882
|
+
// One node, bottom-centred, auto-hides after `duration` ms (default 2200).
|
|
3883
|
+
class NkToast extends NkElement {
|
|
3884
|
+
static get observedAttributes() { return ['open', 'duration', 'icon']; }
|
|
3885
|
+
|
|
3886
|
+
render() {
|
|
3887
|
+
this._box = this.createElement('div', ['nk-toast'], { role: 'status', 'aria-live': 'polite' });
|
|
3888
|
+
this._icon = document.createElement('span');
|
|
3889
|
+
this._msg = document.createElement('span');
|
|
3890
|
+
this._box.append(this._icon, this._msg, document.createElement('slot'));
|
|
3891
|
+
this._wrapper.appendChild(this._box);
|
|
3892
|
+
this._sync();
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
_sync() {
|
|
3896
|
+
this._box.classList.toggle('show', this.getBoolAttr('open'));
|
|
3897
|
+
const icon = this.getAttribute('icon') ?? '✓';
|
|
3898
|
+
this._icon.textContent = icon;
|
|
3899
|
+
this._icon.style.display = icon ? '' : 'none';
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
/** Shows `message` (or the slotted content when omitted) and hides it again after `duration`. */
|
|
3903
|
+
show(message, { duration } = {}) {
|
|
3904
|
+
if (message !== undefined) this._msg.textContent = message;
|
|
3905
|
+
clearTimeout(this._timer);
|
|
3906
|
+
this.setBoolAttr('open', true);
|
|
3907
|
+
const ms = duration ?? Number(this.getAttribute('duration')) ?? 2200;
|
|
3908
|
+
if (ms > 0) this._timer = setTimeout(() => this.close(), ms || 2200);
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
close() { clearTimeout(this._timer); this.setBoolAttr('open', false); }
|
|
3912
|
+
|
|
3913
|
+
onAttributeChanged(name) {
|
|
3914
|
+
this._sync();
|
|
3915
|
+
if (name === 'open') this.emit('nk-toggle', { open: this.getBoolAttr('open') });
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
disconnectedCallback() { super.disconnectedCallback(); clearTimeout(this._timer); }
|
|
3919
|
+
|
|
3920
|
+
get open() { return this.getBoolAttr('open'); }
|
|
3921
|
+
set open(v) { this.setBoolAttr('open', v); }
|
|
3922
|
+
get message() { return this._msg?.textContent ?? ''; }
|
|
3923
|
+
set message(v) { if (this._msg) this._msg.textContent = v; }
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
customElements.define('nk-toast', NkToast);
|
|
3927
|
+
|
|
3928
|
+
// <nk-database id="db" view="table" add-view>
|
|
3929
|
+
// <nk-table-view name="table" label="▦ Table" count></nk-table-view>
|
|
3930
|
+
// <nk-board-view name="board" label="▤ Board" group-by="status"></nk-board-view>
|
|
3931
|
+
// </nk-database>
|
|
3932
|
+
// db.columns = [{ key: 'name', label: 'Name', type: 'text', icon: '📄', title: true }, { key: 'status', type: 'select', options: [...] }, …];
|
|
3933
|
+
// db.rows = [{ id: 1, icon: '🧭', name: '…', status: 'done', … }];
|
|
3934
|
+
// → <div class="nk-database"><div class="nk-db-tabs">…</div>…active view…</div>
|
|
3935
|
+
// The tabs come from the child views; data is pushed into every view.
|
|
3936
|
+
class NkDatabase extends NkElement {
|
|
3937
|
+
static get observedAttributes() { return ['view', 'add-view']; }
|
|
3938
|
+
|
|
3939
|
+
render() {
|
|
3940
|
+
this._box = this.createElement('div', ['nk-database']);
|
|
3941
|
+
this._tabs = this.createElement('div', ['nk-db-tabs'], { role: 'tablist' });
|
|
3942
|
+
this._slot = document.createElement('slot');
|
|
3943
|
+
this._box.append(this._tabs, this._slot);
|
|
3944
|
+
this._wrapper.appendChild(this._box);
|
|
3945
|
+
this._columns = []; this._rows = [];
|
|
3946
|
+
this._sync();
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
get views() { return this._slot?.assignedElements().filter(el => /^nk-[a-z]+-view$/.test(el.localName)) ?? []; }
|
|
3950
|
+
|
|
3951
|
+
_sync() {
|
|
3952
|
+
if (!this._tabs) return;
|
|
3953
|
+
const views = this.views;
|
|
3954
|
+
let current = this.getAttribute('view');
|
|
3955
|
+
if (!views.some(v => v.name === current)) current = views[0]?.name ?? null;
|
|
3956
|
+
this._tabs.replaceChildren();
|
|
3957
|
+
for (const v of views) {
|
|
3958
|
+
const tab = this.createElement('span', ['nk-db-tab'], { role: 'tab', 'data-view': v.name, tabindex: '0' });
|
|
3959
|
+
tab.classList.toggle('active', v.name === current);
|
|
3960
|
+
tab.setAttribute('aria-selected', v.name === current ? 'true' : 'false');
|
|
3961
|
+
tab.append(document.createTextNode(v.getAttribute('label') || v.name));
|
|
3962
|
+
const badge = v.hasAttribute('count') ? String(this._rows.length) : v.getAttribute('badge');
|
|
3963
|
+
if (badge) { const b = this.createElement('span', ['badge']); b.textContent = badge; tab.appendChild(b); }
|
|
3964
|
+
this._tabs.appendChild(tab);
|
|
3965
|
+
v.hidden = v.name !== current;
|
|
3966
|
+
// A method call, not a property write: a view that has not been upgraded
|
|
3967
|
+
// yet would otherwise get an own `data` property that shadows its setter.
|
|
3968
|
+
v.setData?.(this._columns, this._rows);
|
|
3969
|
+
}
|
|
3970
|
+
if (this.getBoolAttr('add-view')) {
|
|
3971
|
+
const add = this.createElement('span', ['nk-db-tab'], { role: 'button', 'data-add': '', tabindex: '0' });
|
|
3972
|
+
add.style.color = 'var(--nk-text-tertiary)';
|
|
3973
|
+
add.textContent = '+';
|
|
3974
|
+
this._tabs.appendChild(add);
|
|
3975
|
+
}
|
|
3976
|
+
this._tabs.style.display = views.length ? '' : 'none';
|
|
3977
|
+
this._current = current;
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
/** Views upgrade after the database; each one asks for a resync once rendered. */
|
|
3981
|
+
requestSync() {
|
|
3982
|
+
if (this._syncQueued) return;
|
|
3983
|
+
this._syncQueued = true;
|
|
3984
|
+
queueMicrotask(() => { this._syncQueued = false; this._sync(); });
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
setupEvents() {
|
|
3988
|
+
this._onClick = (e) => {
|
|
3989
|
+
const tab = e.target.closest('[data-view]');
|
|
3990
|
+
if (tab) { this.view = tab.dataset.view; return; }
|
|
3991
|
+
if (e.target.closest('[data-add]')) this.emit('nk-action', { action: 'add-view' });
|
|
3992
|
+
};
|
|
3993
|
+
this._onKey = (e) => { if ((e.key === 'Enter' || e.key === ' ') && e.target.closest('[data-view],[data-add]')) { e.preventDefault(); e.target.click(); } };
|
|
3994
|
+
this._onSlot = () => this._sync();
|
|
3995
|
+
this._tabs.addEventListener('click', this._onClick);
|
|
3996
|
+
this._tabs.addEventListener('keydown', this._onKey);
|
|
3997
|
+
this._slot.addEventListener('slotchange', this._onSlot);
|
|
3998
|
+
this._sync();
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
teardownEvents() {
|
|
4002
|
+
this._tabs?.removeEventListener('click', this._onClick);
|
|
4003
|
+
this._tabs?.removeEventListener('keydown', this._onKey);
|
|
4004
|
+
this._slot?.removeEventListener('slotchange', this._onSlot);
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
onAttributeChanged(name) {
|
|
4008
|
+
const before = this._current;
|
|
4009
|
+
this._sync();
|
|
4010
|
+
if (name === 'view' && this._current !== before) this.emit('nk-view-change', { view: this._current });
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
get columns() { return this._columns; }
|
|
4014
|
+
set columns(v) { this._columns = Array.isArray(v) ? v : []; this._sync(); }
|
|
4015
|
+
get rows() { return this._rows; }
|
|
4016
|
+
set rows(v) { this._rows = Array.isArray(v) ? v : []; this._sync(); }
|
|
4017
|
+
get view() { return this._current ?? this.getAttribute('view'); }
|
|
4018
|
+
set view(v) { this.setAttribute('view', v); }
|
|
4019
|
+
/** Re-renders every view after in-place row mutations. */
|
|
4020
|
+
refresh() { this._sync(); }
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
customElements.define('nk-database', NkDatabase);
|
|
4024
|
+
|
|
4025
|
+
// Polymorphic property renderer for database views. Returns plain DOM built
|
|
4026
|
+
// from NotionKit classes – deliberately not a custom element: every cell rule
|
|
4027
|
+
// starts with `.nk-table`, so a cell inside its own shadow root would never
|
|
4028
|
+
// be styled. Exported for consumers who render tables themselves.
|
|
4029
|
+
//
|
|
4030
|
+
// column: { key, label, type, icon, options: [{ value, label, color }], title }
|
|
4031
|
+
// types: text | select | multi-select | date | person | checkbox | url | number | progress
|
|
4032
|
+
const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
|
|
4033
|
+
const COLORS = ['blue', 'green', 'orange', 'purple'];
|
|
4034
|
+
|
|
4035
|
+
function optionFor(column, value) {
|
|
4036
|
+
const opts = column.options || [];
|
|
4037
|
+
return opts.find(o => o.value === value) || opts.find(o => o.label === value) || null;
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
function tagFor(column, value) {
|
|
4041
|
+
const opt = optionFor(column, value);
|
|
4042
|
+
const color = opt?.color && COLORS.includes(opt.color) ? opt.color : 'blue';
|
|
4043
|
+
return el('span', `nk-tag ${color}`, opt?.label ?? String(value));
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
function renderPropertyCell(column, value, row = {}) {
|
|
4047
|
+
const type = column.type || 'text';
|
|
4048
|
+
if (value === undefined || value === null || value === '') {
|
|
4049
|
+
if (type === 'checkbox') return checkbox(column, false, row);
|
|
4050
|
+
if (type === 'progress') return progress(0);
|
|
4051
|
+
return el('span', type === 'person' ? 'person-cell' : null, type === 'text' && column.title ? '' : '—');
|
|
4052
|
+
}
|
|
4053
|
+
switch (type) {
|
|
4054
|
+
case 'select': return tagFor(column, value);
|
|
4055
|
+
case 'multi-select': {
|
|
4056
|
+
const wrap = el('span');
|
|
4057
|
+
wrap.style.display = 'inline-flex'; wrap.style.gap = '4px'; wrap.style.flexWrap = 'wrap';
|
|
4058
|
+
for (const v of Array.isArray(value) ? value : String(value).split(',').map(s => s.trim()).filter(Boolean)) wrap.appendChild(tagFor(column, v));
|
|
4059
|
+
return wrap;
|
|
4060
|
+
}
|
|
4061
|
+
case 'date': return el('span', 'date-cell', String(value));
|
|
4062
|
+
case 'person': {
|
|
4063
|
+
const cell = el('span', 'person-cell');
|
|
4064
|
+
const p = typeof value === 'string' ? { name: value } : value;
|
|
4065
|
+
const avatar = el('span', 'mini-avatar', p.initials || p.avatar || (p.name || '').split(/\s+/).map(w => w[0]).join('').slice(0, 2).toUpperCase());
|
|
4066
|
+
avatar.style.background = p.color || 'var(--nk-text-tertiary)';
|
|
4067
|
+
cell.append(avatar, document.createTextNode(' ' + (p.name || '')));
|
|
4068
|
+
return cell;
|
|
4069
|
+
}
|
|
4070
|
+
case 'checkbox': return checkbox(column, !!value, row);
|
|
4071
|
+
case 'url': { const a = el('a', null, String(value).replace(/^https?:\/\//, '')); a.href = String(value); a.target = '_blank'; a.rel = 'noopener'; return a; }
|
|
4072
|
+
case 'number': return el('span', null, typeof value === 'number' ? value.toLocaleString() : String(value));
|
|
4073
|
+
case 'progress': return progress(Number(value) || 0);
|
|
4074
|
+
default: {
|
|
4075
|
+
if (column.title) {
|
|
4076
|
+
const t = el('span', 'row-title');
|
|
4077
|
+
if (row.icon) t.append(document.createTextNode(row.icon + ' '));
|
|
4078
|
+
t.append(document.createTextNode(String(value)));
|
|
4079
|
+
return t;
|
|
4080
|
+
}
|
|
4081
|
+
return el('span', null, String(value));
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
|
|
4086
|
+
function checkbox(column, checked, row) {
|
|
4087
|
+
const label = el('label', 'nk-check');
|
|
4088
|
+
const input = el('input');
|
|
4089
|
+
input.type = 'checkbox'; input.checked = checked;
|
|
4090
|
+
input.dataset.key = column.key;
|
|
4091
|
+
if (row.id != null) input.dataset.rowId = row.id;
|
|
4092
|
+
label.appendChild(input);
|
|
4093
|
+
return label;
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
function progress(value) {
|
|
4097
|
+
const frag = document.createDocumentFragment();
|
|
4098
|
+
const bar = el('span', 'nk-progress');
|
|
4099
|
+
const fill = el('i'); fill.style.width = `${Math.max(0, Math.min(100, value))}%`;
|
|
4100
|
+
bar.appendChild(fill);
|
|
4101
|
+
frag.append(bar, el('span', 'nk-progress-label', `${Math.round(value)}%`));
|
|
4102
|
+
return frag;
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
/** Sort comparator for a column type. */
|
|
4106
|
+
function compareBy(column, dir = 1) {
|
|
4107
|
+
const type = column.type || 'text';
|
|
4108
|
+
return (a, b) => {
|
|
4109
|
+
let x = a[column.key], y = b[column.key];
|
|
4110
|
+
if (type === 'person') { x = x?.name ?? x ?? ''; y = y?.name ?? y ?? ''; }
|
|
4111
|
+
if (type === 'number' || type === 'progress' || type === 'checkbox') return ((Number(x) || 0) - (Number(y) || 0)) * dir;
|
|
4112
|
+
return String(x ?? '').localeCompare(String(y ?? ''), undefined, { numeric: true }) * dir;
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
// <nk-table-view name="table" label="▦ Table" count new-row sortable></nk-table-view>
|
|
4117
|
+
// Standalone: view.columns = […]; view.rows = […]; Inside <nk-database> the
|
|
4118
|
+
// database pushes `data`. → <div class="nk-table-wrap"><table class="nk-table">…</table><div class="nk-new-row">+ New page</div></div>
|
|
4119
|
+
// Cells are plain markup from renderPropertyCell(); rows fire nk-select,
|
|
4120
|
+
// checkboxes nk-change, headers nk-action { action: 'sort' } (and sort locally
|
|
4121
|
+
// with `sortable`), the add row nk-action { action: 'new-row' }.
|
|
4122
|
+
class NkTableView extends NkElement {
|
|
4123
|
+
static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'new-row', 'new-row-label', 'sortable', 'sort-key', 'sort-dir']; }
|
|
4124
|
+
|
|
4125
|
+
render() {
|
|
4126
|
+
this._wrap = this.createElement('div', ['nk-table-wrap']);
|
|
4127
|
+
this._table = this.createElement('table', ['nk-table']);
|
|
4128
|
+
this._thead = document.createElement('thead');
|
|
4129
|
+
this._tbody = document.createElement('tbody');
|
|
4130
|
+
this._table.append(this._thead, this._tbody);
|
|
4131
|
+
this._newRow = this.createElement('div', ['nk-new-row'], { role: 'button', tabindex: '0' });
|
|
4132
|
+
this._wrap.append(this._table, this._newRow);
|
|
4133
|
+
this._wrapper.appendChild(this._wrap);
|
|
4134
|
+
this._columns ??= []; this._rows ??= [];
|
|
4135
|
+
this._render();
|
|
4136
|
+
this.closest('nk-database')?.requestSync?.();
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
_sortedRows() {
|
|
4140
|
+
const key = this.getAttribute('sort-key');
|
|
4141
|
+
const col = this._columns.find(c => c.key === key);
|
|
4142
|
+
if (!col) return this._rows;
|
|
4143
|
+
return [...this._rows].sort(compareBy(col, this.getAttribute('sort-dir') === 'desc' ? -1 : 1));
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
_render() {
|
|
4147
|
+
if (!this._thead) return;
|
|
4148
|
+
const tr = document.createElement('tr');
|
|
4149
|
+
for (const col of this._columns) {
|
|
4150
|
+
const th = this.createElement('th', [], { 'data-key': col.key, scope: 'col' });
|
|
4151
|
+
if (col.icon) { const i = this.createElement('span', ['th-icon']); i.textContent = col.icon; th.appendChild(i); }
|
|
4152
|
+
th.appendChild(document.createTextNode(col.label ?? col.key));
|
|
4153
|
+
if (col.key === this.getAttribute('sort-key')) th.appendChild(document.createTextNode(this.getAttribute('sort-dir') === 'desc' ? ' ↓' : ' ↑'));
|
|
4154
|
+
if (col.width) th.style.width = col.width;
|
|
4155
|
+
tr.appendChild(th);
|
|
4156
|
+
}
|
|
4157
|
+
this._thead.replaceChildren(tr);
|
|
4158
|
+
this._tbody.replaceChildren();
|
|
4159
|
+
for (const row of this._sortedRows()) {
|
|
4160
|
+
const r = this.createElement('tr', [], { 'data-id': row.id ?? '' });
|
|
4161
|
+
for (const col of this._columns) {
|
|
4162
|
+
const td = document.createElement('td');
|
|
4163
|
+
td.appendChild(renderPropertyCell(col, row[col.key], row));
|
|
4164
|
+
r.appendChild(td);
|
|
4165
|
+
}
|
|
4166
|
+
this._tbody.appendChild(r);
|
|
4167
|
+
}
|
|
4168
|
+
this._newRow.textContent = this.getAttribute('new-row-label') || '+ New page';
|
|
4169
|
+
this._newRow.style.display = this.getBoolAttr('new-row') ? '' : 'none';
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
setupEvents() {
|
|
4173
|
+
this._onClick = (e) => {
|
|
4174
|
+
if (this._newRow.contains(e.target)) { this.emit('nk-action', { action: 'new-row' }); return; }
|
|
4175
|
+
const th = e.target.closest('th[data-key]');
|
|
4176
|
+
if (th) {
|
|
4177
|
+
const key = th.dataset.key;
|
|
4178
|
+
const dir = this.getAttribute('sort-key') === key && this.getAttribute('sort-dir') !== 'desc' ? 'desc' : 'asc';
|
|
4179
|
+
const ok = this.emit('nk-action', { action: 'sort', key, value: dir });
|
|
4180
|
+
if (ok && this.getBoolAttr('sortable')) { this.setAttribute('sort-key', key); this.setAttribute('sort-dir', dir); }
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
4183
|
+
if (e.target.closest('input[type=checkbox], a')) return;
|
|
4184
|
+
const tr = e.target.closest('tr[data-id]');
|
|
4185
|
+
if (tr) { const row = this._rowById(tr.dataset.id); this.emit('nk-select', { row, id: row?.id, value: row?.id }); }
|
|
4186
|
+
};
|
|
4187
|
+
this._onChange = (e) => {
|
|
4188
|
+
const input = e.target;
|
|
4189
|
+
if (input.type !== 'checkbox') return;
|
|
4190
|
+
const row = this._rowById(input.dataset.rowId);
|
|
4191
|
+
if (row) row[input.dataset.key] = input.checked;
|
|
4192
|
+
this.emit('nk-change', { row, key: input.dataset.key, value: input.checked });
|
|
4193
|
+
};
|
|
4194
|
+
this._onKey = (e) => { if ((e.key === 'Enter' || e.key === ' ') && e.target === this._newRow) { e.preventDefault(); this.emit('nk-action', { action: 'new-row' }); } };
|
|
4195
|
+
this._wrap.addEventListener('click', this._onClick);
|
|
4196
|
+
this._wrap.addEventListener('change', this._onChange);
|
|
4197
|
+
this._wrap.addEventListener('keydown', this._onKey);
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
teardownEvents() {
|
|
4201
|
+
this._wrap?.removeEventListener('click', this._onClick);
|
|
4202
|
+
this._wrap?.removeEventListener('change', this._onChange);
|
|
4203
|
+
this._wrap?.removeEventListener('keydown', this._onKey);
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
_rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
|
|
4207
|
+
onAttributeChanged() { this._render(); }
|
|
4208
|
+
|
|
4209
|
+
get name() { return this.getAttribute('name') || 'table'; }
|
|
4210
|
+
get columns() { return this._columns; }
|
|
4211
|
+
set columns(v) { this._columns = Array.isArray(v) ? v : []; this._render(); }
|
|
4212
|
+
get rows() { return this._rows; }
|
|
4213
|
+
set rows(v) { this._rows = Array.isArray(v) ? v : []; this._render(); }
|
|
4214
|
+
set data({ columns, rows }) { this.setData(columns, rows); }
|
|
4215
|
+
setData(columns, rows) { this._columns = columns || []; this._rows = rows || []; this._render(); }
|
|
4216
|
+
refresh() { this._render(); }
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
customElements.define('nk-table-view', NkTableView);
|
|
4220
|
+
|
|
4221
|
+
// <nk-board-view name="board" label="▤ Board" group-by="status" new-row></nk-board-view>
|
|
4222
|
+
// Groups rows by a select column (default: the first select column); one
|
|
4223
|
+
// column per option, in option order. Cards show the title column and the
|
|
4224
|
+
// `meta-keys` (default: date and progress columns). Drag a card onto another
|
|
4225
|
+
// column to change its value – the row is updated and nk-change fires.
|
|
4226
|
+
// → <div class="nk-board active"><div class="nk-board-col">…</div>…</div>
|
|
4227
|
+
class NkBoardView extends NkElement {
|
|
4228
|
+
static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'group-by', 'title-key', 'meta-keys', 'new-row']; }
|
|
4229
|
+
|
|
4230
|
+
render() {
|
|
4231
|
+
this._board = this.createElement('div', ['nk-board', 'active']);
|
|
4232
|
+
this._wrapper.appendChild(this._board);
|
|
4233
|
+
this._columns ??= []; this._rows ??= [];
|
|
4234
|
+
this._render();
|
|
4235
|
+
this.closest('nk-database')?.requestSync?.();
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4238
|
+
_groupColumn() {
|
|
4239
|
+
const key = this.getAttribute('group-by');
|
|
4240
|
+
return this._columns.find(c => c.key === key) || this._columns.find(c => c.type === 'select') || null;
|
|
4241
|
+
}
|
|
4242
|
+
_titleColumn() {
|
|
4243
|
+
const key = this.getAttribute('title-key');
|
|
4244
|
+
return this._columns.find(c => c.key === key) || this._columns.find(c => c.title) || this._columns[0] || null;
|
|
4245
|
+
}
|
|
4246
|
+
_metaColumns() {
|
|
4247
|
+
const keys = (this.getAttribute('meta-keys') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
4248
|
+
if (keys.length) return keys.map(k => this._columns.find(c => c.key === k)).filter(Boolean);
|
|
4249
|
+
return this._columns.filter(c => c.type === 'date' || c.type === 'progress');
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
_render() {
|
|
4253
|
+
if (!this._board) return;
|
|
4254
|
+
this._board.replaceChildren();
|
|
4255
|
+
const group = this._groupColumn(), title = this._titleColumn();
|
|
4256
|
+
if (!group) return;
|
|
4257
|
+
const options = group.options?.length ? group.options : [...new Set(this._rows.map(r => r[group.key]))].map(v => ({ value: v, label: String(v) }));
|
|
4258
|
+
for (const opt of options) {
|
|
4259
|
+
const col = this.createElement('div', ['nk-board-col'], { 'data-value': opt.value });
|
|
4260
|
+
const header = this.createElement('div', ['nk-board-col-header']);
|
|
4261
|
+
const cards = this._rows.filter(r => r[group.key] === opt.value || r[group.key] === opt.label);
|
|
4262
|
+
const count = this.createElement('span', ['count']);
|
|
4263
|
+
count.textContent = String(cards.length);
|
|
4264
|
+
header.append(tagFor(group, opt.value), count);
|
|
4265
|
+
col.appendChild(header);
|
|
4266
|
+
for (const row of cards) {
|
|
4267
|
+
const card = this.createElement('div', ['nk-card'], { draggable: 'true', 'data-id': row.id ?? '', tabindex: '0' });
|
|
4268
|
+
const t = this.createElement('div', ['card-title']);
|
|
4269
|
+
t.textContent = `${row.icon ? row.icon + ' ' : ''}${title ? row[title.key] ?? '' : ''}`;
|
|
4270
|
+
const meta = this.createElement('div', ['card-meta']);
|
|
4271
|
+
for (const c of this._metaColumns()) {
|
|
4272
|
+
const v = row[c.key];
|
|
4273
|
+
if (v === undefined || v === null || v === '') continue;
|
|
4274
|
+
const s = document.createElement('span');
|
|
4275
|
+
if (c.type === 'progress') s.textContent = `▰ ${v}%`;
|
|
4276
|
+
else if (c.type === 'date') s.textContent = `📅 ${v}`;
|
|
4277
|
+
else s.appendChild(renderPropertyCell(c, v, row));
|
|
4278
|
+
meta.appendChild(s);
|
|
4279
|
+
}
|
|
4280
|
+
card.append(t, meta);
|
|
4281
|
+
col.appendChild(card);
|
|
4282
|
+
}
|
|
4283
|
+
if (this.getBoolAttr('new-row')) {
|
|
4284
|
+
const add = this.createElement('div', ['nk-new-row'], { role: 'button', tabindex: '0', 'data-add': opt.value });
|
|
4285
|
+
add.style.padding = '6px 10px';
|
|
4286
|
+
add.textContent = '+';
|
|
4287
|
+
col.appendChild(add);
|
|
4288
|
+
}
|
|
4289
|
+
this._board.appendChild(col);
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
setupEvents() {
|
|
4294
|
+
this._onClick = (e) => {
|
|
4295
|
+
const add = e.target.closest('[data-add]');
|
|
4296
|
+
if (add) { this.emit('nk-action', { action: 'new-row', value: add.dataset.add }); return; }
|
|
4297
|
+
const card = e.target.closest('.nk-card');
|
|
4298
|
+
if (card) { const row = this._rowById(card.dataset.id); this.emit('nk-select', { row, id: row?.id, value: row?.id }); }
|
|
4299
|
+
};
|
|
4300
|
+
this._onDragStart = (e) => { const card = e.target.closest?.('.nk-card'); if (!card) return; this._dragId = card.dataset.id; e.dataTransfer.effectAllowed = 'move'; try { e.dataTransfer.setData('text/plain', card.dataset.id); } catch {} };
|
|
4301
|
+
this._onDragOver = (e) => { if (this._dragId != null && e.target.closest('.nk-board-col')) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } };
|
|
4302
|
+
this._onDrop = (e) => {
|
|
4303
|
+
const col = e.target.closest('.nk-board-col');
|
|
4304
|
+
if (!col || this._dragId == null) return;
|
|
4305
|
+
e.preventDefault();
|
|
4306
|
+
this.move(this._dragId, col.dataset.value);
|
|
4307
|
+
this._dragId = null;
|
|
4308
|
+
};
|
|
4309
|
+
this._board.addEventListener('click', this._onClick);
|
|
4310
|
+
this._board.addEventListener('dragstart', this._onDragStart);
|
|
4311
|
+
this._board.addEventListener('dragover', this._onDragOver);
|
|
4312
|
+
this._board.addEventListener('drop', this._onDrop);
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4315
|
+
teardownEvents() {
|
|
4316
|
+
this._board?.removeEventListener('click', this._onClick);
|
|
4317
|
+
this._board?.removeEventListener('dragstart', this._onDragStart);
|
|
4318
|
+
this._board?.removeEventListener('dragover', this._onDragOver);
|
|
4319
|
+
this._board?.removeEventListener('drop', this._onDrop);
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
/** Moves a row into another column (sets the group value) and reports it. */
|
|
4323
|
+
move(id, value) {
|
|
4324
|
+
const row = this._rowById(id), group = this._groupColumn();
|
|
4325
|
+
if (!row || !group || row[group.key] === value) return;
|
|
4326
|
+
row[group.key] = value;
|
|
4327
|
+
this._render();
|
|
4328
|
+
this.emit('nk-change', { row, key: group.key, value });
|
|
4329
|
+
}
|
|
4330
|
+
|
|
4331
|
+
_rowById(id) { return this._rows.find(r => String(r.id) === String(id)); }
|
|
4332
|
+
onAttributeChanged() { this._render(); }
|
|
4333
|
+
|
|
4334
|
+
get name() { return this.getAttribute('name') || 'board'; }
|
|
4335
|
+
get columns() { return this._columns; }
|
|
4336
|
+
set columns(v) { this._columns = Array.isArray(v) ? v : []; this._render(); }
|
|
4337
|
+
get rows() { return this._rows; }
|
|
4338
|
+
set rows(v) { this._rows = Array.isArray(v) ? v : []; this._render(); }
|
|
4339
|
+
set data({ columns, rows }) { this.setData(columns, rows); }
|
|
4340
|
+
setData(columns, rows) { this._columns = columns || []; this._rows = rows || []; this._render(); }
|
|
4341
|
+
refresh() { this._render(); }
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
customElements.define('nk-board-view', NkBoardView);
|
|
4345
|
+
|
|
4346
|
+
// <nk-filter-bar search placeholder="Search rows …"></nk-filter-bar>
|
|
4347
|
+
// bar.filters = [{ key: 'status', label: 'Status: Done', value: 'done' }];
|
|
4348
|
+
// Composed from existing classes (nk-btn secondary small, nk-input, nk-tag):
|
|
4349
|
+
// filter / sort buttons fire nk-action, chips remove on ×, search and chip
|
|
4350
|
+
// changes fire nk-change { filters, search }. The wrapper is layout only.
|
|
4351
|
+
class NkFilterBar extends NkElement {
|
|
4352
|
+
static get observedAttributes() { return ['search', 'placeholder', 'no-filter', 'no-sort', 'value']; }
|
|
4353
|
+
|
|
4354
|
+
render() {
|
|
4355
|
+
this._bar = document.createElement('div');
|
|
4356
|
+
this._bar.style.cssText = 'display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:8px 0';
|
|
4357
|
+
this._filterBtn = this.createElement('button', ['nk-btn', 'secondary', 'small'], { type: 'button', 'data-action': 'filter' });
|
|
4358
|
+
this._filterBtn.textContent = '⚲ Filter';
|
|
4359
|
+
this._sortBtn = this.createElement('button', ['nk-btn', 'secondary', 'small'], { type: 'button', 'data-action': 'sort' });
|
|
4360
|
+
this._sortBtn.textContent = '↕ Sort';
|
|
4361
|
+
this._chips = document.createElement('span');
|
|
4362
|
+
this._chips.style.cssText = 'display:inline-flex;gap:4px;flex-wrap:wrap';
|
|
4363
|
+
this._input = this.createElement('input', ['nk-input'], { type: 'search', autocomplete: 'off' });
|
|
4364
|
+
this._input.style.marginLeft = 'auto';
|
|
4365
|
+
this._bar.append(this._filterBtn, this._sortBtn, this._chips, document.createElement('slot'), this._input);
|
|
4366
|
+
this._wrapper.appendChild(this._bar);
|
|
4367
|
+
this._filters = [];
|
|
4368
|
+
this._sync();
|
|
4369
|
+
}
|
|
4370
|
+
|
|
4371
|
+
_sync() {
|
|
4372
|
+
this._filterBtn.style.display = this.getBoolAttr('no-filter') ? 'none' : '';
|
|
4373
|
+
this._sortBtn.style.display = this.getBoolAttr('no-sort') ? 'none' : '';
|
|
4374
|
+
this._input.style.display = this.getBoolAttr('search') ? '' : 'none';
|
|
4375
|
+
this._input.placeholder = this.getAttribute('placeholder') || 'Search …';
|
|
4376
|
+
if (this.hasAttribute('value') && this._input.value !== this.getAttribute('value')) this._input.value = this.getAttribute('value');
|
|
4377
|
+
this._chips.replaceChildren();
|
|
4378
|
+
this._filters.forEach((f, i) => {
|
|
4379
|
+
const chip = this.createElement('span', ['nk-tag', f.color || 'blue'], { 'data-index': i });
|
|
4380
|
+
chip.style.cursor = 'pointer';
|
|
4381
|
+
chip.textContent = `${f.label ?? `${f.key}: ${f.value}`} ×`;
|
|
4382
|
+
chip.title = 'Remove filter';
|
|
4383
|
+
this._chips.appendChild(chip);
|
|
4384
|
+
});
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
setupEvents() {
|
|
4388
|
+
this._onClick = (e) => {
|
|
4389
|
+
const btn = e.target.closest('[data-action]');
|
|
4390
|
+
if (btn) { this.emit('nk-action', { action: btn.dataset.action }); return; }
|
|
4391
|
+
const chip = e.target.closest('[data-index]');
|
|
4392
|
+
if (chip) { this._filters.splice(Number(chip.dataset.index), 1); this._sync(); this._emitChange(); }
|
|
4393
|
+
};
|
|
4394
|
+
this._onInput = () => { clearTimeout(this._debounce); this._debounce = setTimeout(() => this._emitChange(), 150); };
|
|
4395
|
+
this._bar.addEventListener('click', this._onClick);
|
|
4396
|
+
this._input.addEventListener('input', this._onInput);
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
teardownEvents() {
|
|
4400
|
+
this._bar?.removeEventListener('click', this._onClick);
|
|
4401
|
+
this._input?.removeEventListener('input', this._onInput);
|
|
4402
|
+
clearTimeout(this._debounce);
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
_emitChange() { this.emit('nk-change', { filters: [...this._filters], search: this._input.value }); }
|
|
4406
|
+
onAttributeChanged() { this._sync(); }
|
|
4407
|
+
|
|
4408
|
+
get filters() { return this._filters; }
|
|
4409
|
+
set filters(v) { this._filters = Array.isArray(v) ? v : []; this._sync(); }
|
|
4410
|
+
get value() { return this._input?.value ?? ''; }
|
|
4411
|
+
set value(v) { if (this._input) this._input.value = v ?? ''; }
|
|
4412
|
+
/** Applies filters + search to rows: equality per filter key, substring search over every string field. */
|
|
4413
|
+
apply(rows) {
|
|
4414
|
+
const q = this.value.trim().toLowerCase();
|
|
4415
|
+
return rows.filter(r => this._filters.every(f => r[f.key] === f.value) && (!q || Object.values(r).some(v => String(typeof v === 'object' && v ? v.name ?? '' : v ?? '').toLowerCase().includes(q))));
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
|
|
4419
|
+
customElements.define('nk-filter-bar', NkFilterBar);
|
|
4420
|
+
|
|
4421
|
+
// <nk-comments placeholder="Comment …" send-label="Send">
|
|
4422
|
+
// <nk-comment …>…</nk-comment>
|
|
4423
|
+
// </nk-comments>
|
|
4424
|
+
// → <div class="nk-comments">…<div class="nk-comment-input"><input class="nk-input"><button class="nk-btn primary small">Send</button></div></div>
|
|
4425
|
+
// Enter or the button fires nk-submit { text } and clears the field.
|
|
4426
|
+
class NkComments extends NkElement {
|
|
4427
|
+
static get observedAttributes() { return ['placeholder', 'send-label', 'no-input', 'disabled']; }
|
|
4428
|
+
|
|
4429
|
+
render() {
|
|
4430
|
+
const box = this.createElement('div', ['nk-comments']);
|
|
4431
|
+
box.appendChild(document.createElement('slot'));
|
|
4432
|
+
this._row = this.createElement('div', ['nk-comment-input']);
|
|
4433
|
+
this._input = this.createElement('input', ['nk-input'], { type: 'text', 'aria-label': 'Comment' });
|
|
4434
|
+
this._btn = this.createElement('button', ['nk-btn', 'primary', 'small'], { type: 'button' });
|
|
4435
|
+
this._row.append(this._input, this._btn);
|
|
4436
|
+
box.appendChild(this._row);
|
|
4437
|
+
this._wrapper.appendChild(box);
|
|
4438
|
+
this._sync();
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
_sync() {
|
|
4442
|
+
this._input.placeholder = this.getAttribute('placeholder') || 'Comment …';
|
|
4443
|
+
this._btn.textContent = this.getAttribute('send-label') || 'Send';
|
|
4444
|
+
this._row.style.display = this.getBoolAttr('no-input') ? 'none' : '';
|
|
4445
|
+
this._input.disabled = this._btn.disabled = this.getBoolAttr('disabled');
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
setupEvents() {
|
|
4449
|
+
this._onSend = () => this.submit();
|
|
4450
|
+
this._onKey = (e) => { if (e.key === 'Enter') { e.preventDefault(); this.submit(); } };
|
|
4451
|
+
this._btn.addEventListener('click', this._onSend);
|
|
4452
|
+
this._input.addEventListener('keydown', this._onKey);
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
teardownEvents() {
|
|
4456
|
+
this._btn?.removeEventListener('click', this._onSend);
|
|
4457
|
+
this._input?.removeEventListener('keydown', this._onKey);
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
submit() {
|
|
4461
|
+
const text = this._input.value.trim();
|
|
4462
|
+
if (!text) return;
|
|
4463
|
+
const ok = this.emit('nk-submit', { text });
|
|
4464
|
+
if (ok) this._input.value = '';
|
|
4465
|
+
}
|
|
4466
|
+
|
|
4467
|
+
onAttributeChanged() { this._sync(); }
|
|
4468
|
+
focus(o) { this._input?.focus(o); }
|
|
4469
|
+
get value() { return this._input?.value ?? ''; }
|
|
4470
|
+
set value(v) { if (this._input) this._input.value = v ?? ''; }
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
customElements.define('nk-comments', NkComments);
|
|
4474
|
+
|
|
4475
|
+
// <nk-comment author="Sara Lindt" time="1 hr ago" avatar="SL" color="#448361">The board view feels close.</nk-comment>
|
|
4476
|
+
// → <div class="nk-comment"><span class="mini-avatar">SL</span><div><div class="c-head"><b>Sara Lindt</b> · 1 hr ago</div><div class="c-body">…</div></div></div>
|
|
4477
|
+
// slot="head" adds content after the name (e.g. an <nk-tag>).
|
|
4478
|
+
class NkComment extends NkElement {
|
|
4479
|
+
static get observedAttributes() { return ['author', 'time', 'avatar', 'color']; }
|
|
4480
|
+
|
|
4481
|
+
render() {
|
|
4482
|
+
const box = this.createElement('div', ['nk-comment']);
|
|
4483
|
+
this._avatar = this.createElement('span', ['mini-avatar']);
|
|
4484
|
+
const avatarSlot = this.createElement('slot', [], { name: 'avatar' });
|
|
4485
|
+
avatarSlot.appendChild(this._avatar);
|
|
4486
|
+
const text = document.createElement('div');
|
|
4487
|
+
this._head = this.createElement('div', ['c-head']);
|
|
4488
|
+
this._author = document.createElement('b');
|
|
4489
|
+
this._time = document.createTextNode('');
|
|
4490
|
+
this._head.append(this._author, this.createElement('slot', [], { name: 'head' }), this._time);
|
|
4491
|
+
const body = this.createElement('div', ['c-body']);
|
|
4492
|
+
body.appendChild(document.createElement('slot'));
|
|
4493
|
+
text.append(this._head, body);
|
|
4494
|
+
box.append(avatarSlot, text);
|
|
4495
|
+
this._wrapper.appendChild(box);
|
|
4496
|
+
this._sync();
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
_sync() {
|
|
4500
|
+
const author = this.getAttribute('author') || '';
|
|
4501
|
+
this._author.textContent = author;
|
|
4502
|
+
const time = this.getAttribute('time');
|
|
4503
|
+
this._time.data = time ? ` · ${time}` : '';
|
|
4504
|
+
this._avatar.textContent = this.getAttribute('avatar') || author.split(/\s+/).map(w => w[0]).join('').slice(0, 2).toUpperCase();
|
|
4505
|
+
this._avatar.style.background = this.getAttribute('color') || 'var(--nk-text-tertiary)';
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4508
|
+
onAttributeChanged() { this._sync(); }
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
customElements.define('nk-comment', NkComment);
|
|
4512
|
+
|
|
4513
|
+
// <nk-ai-thread><nk-ai-msg …>…</nk-ai-msg>…</nk-ai-thread> → <div class="nk-ai-thread">
|
|
4514
|
+
class NkAiThread extends NkElement {
|
|
4515
|
+
render() {
|
|
4516
|
+
const box = this.createElement('div', ['nk-ai-thread'], { role: 'log', 'aria-live': 'polite' });
|
|
4517
|
+
box.appendChild(document.createElement('slot'));
|
|
4518
|
+
this._wrapper.appendChild(box);
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
customElements.define('nk-ai-thread', NkAiThread);
|
|
4523
|
+
|
|
4524
|
+
// <nk-ai-msg role="user" name="You" avatar="MK">Summarise the open tasks.</nk-ai-msg>
|
|
4525
|
+
// <nk-ai-msg role="assistant" name="Mona" badge="· AI" avatar="✨">
|
|
4526
|
+
// Two tasks are open …
|
|
4527
|
+
// <button slot="actions" value="copy">📋 Copy</button><button slot="actions" value="retry">↻ Rephrase</button>
|
|
4528
|
+
// </nk-ai-msg>
|
|
4529
|
+
// → <div class="nk-ai-msg user"><span class="mini-avatar">MK</span><div class="a-body"><div class="a-name">You</div>…<div class="nk-ai-actions">…</div></div></div>
|
|
4530
|
+
// Action buttons stay in the light DOM (slotted twins) and fire nk-action { action: value }.
|
|
4531
|
+
class NkAiMsg extends NkElement {
|
|
4532
|
+
static get observedAttributes() { return ['role', 'name', 'badge', 'avatar', 'color']; }
|
|
4533
|
+
|
|
4534
|
+
render() {
|
|
4535
|
+
this._box = this.createElement('div', ['nk-ai-msg']);
|
|
4536
|
+
this._avatar = this.createElement('span', ['mini-avatar']);
|
|
4537
|
+
const avatarSlot = this.createElement('slot', [], { name: 'avatar' });
|
|
4538
|
+
avatarSlot.appendChild(this._avatar);
|
|
4539
|
+
const body = this.createElement('div', ['a-body']);
|
|
4540
|
+
this._name = this.createElement('div', ['a-name']);
|
|
4541
|
+
this._nameText = document.createTextNode('');
|
|
4542
|
+
this._badge = document.createElement('span');
|
|
4543
|
+
this._name.append(this._nameText, this._badge);
|
|
4544
|
+
this._actions = this.createElement('div', ['nk-ai-actions']);
|
|
4545
|
+
this._actionSlot = this.createElement('slot', [], { name: 'actions' });
|
|
4546
|
+
this._actions.appendChild(this._actionSlot);
|
|
4547
|
+
body.append(this._name, document.createElement('slot'), this._actions);
|
|
4548
|
+
this._box.append(avatarSlot, body);
|
|
4549
|
+
this._wrapper.appendChild(this._box);
|
|
4550
|
+
this._sync();
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4553
|
+
_sync() {
|
|
4554
|
+
const user = this.getAttribute('role') === 'user';
|
|
4555
|
+
this._box.classList.toggle('user', user);
|
|
4556
|
+
const name = this.getAttribute('name');
|
|
4557
|
+
this._nameText.data = name || '';
|
|
4558
|
+
const badge = this.getAttribute('badge');
|
|
4559
|
+
this._badge.textContent = badge ? ` ${badge}` : '';
|
|
4560
|
+
this._name.style.display = name ? '' : 'none';
|
|
4561
|
+
this._avatar.textContent = this.getAttribute('avatar') || (user ? (name || 'U').slice(0, 2).toUpperCase() : '✨');
|
|
4562
|
+
const color = this.getAttribute('color');
|
|
4563
|
+
this._avatar.style.background = color || '';
|
|
4564
|
+
this._actions.style.display = this._actionSlot.assignedElements().length ? '' : 'none';
|
|
4565
|
+
}
|
|
4566
|
+
|
|
4567
|
+
setupEvents() {
|
|
4568
|
+
this._onSlot = () => this._sync();
|
|
4569
|
+
this._onClick = (e) => {
|
|
4570
|
+
const btn = e.target.closest?.('button');
|
|
4571
|
+
if (btn && this._actionSlot.assignedElements().includes(btn)) this.emit('nk-action', { action: btn.value || btn.textContent.trim(), value: btn.value || btn.textContent.trim() });
|
|
4572
|
+
};
|
|
4573
|
+
this._actionSlot.addEventListener('slotchange', this._onSlot);
|
|
4574
|
+
this.addEventListener('click', this._onClick);
|
|
4575
|
+
this._sync();
|
|
4576
|
+
}
|
|
4577
|
+
|
|
4578
|
+
teardownEvents() {
|
|
4579
|
+
this._actionSlot?.removeEventListener('slotchange', this._onSlot);
|
|
4580
|
+
this.removeEventListener('click', this._onClick);
|
|
4581
|
+
}
|
|
4582
|
+
|
|
4583
|
+
onAttributeChanged() { this._sync(); }
|
|
4584
|
+
}
|
|
4585
|
+
|
|
4586
|
+
customElements.define('nk-ai-msg', NkAiMsg);
|
|
4587
|
+
|
|
4588
|
+
// <nk-ai-input-row placeholder="Ask Mona something …"></nk-ai-input-row>
|
|
4589
|
+
// → <div class="nk-ai-input-row"><span>✨</span><input><button class="nk-ai-send">↑</button></div>
|
|
4590
|
+
// Enter or the send button fires nk-submit { text } and clears the field.
|
|
4591
|
+
class NkAiInputRow extends NkElement {
|
|
4592
|
+
static get observedAttributes() { return ['placeholder', 'value', 'disabled', 'icon']; }
|
|
4593
|
+
|
|
4594
|
+
render() {
|
|
4595
|
+
this._row = this.createElement('div', ['nk-ai-input-row']);
|
|
4596
|
+
this._icon = document.createElement('span');
|
|
4597
|
+
this._icon.style.fontSize = '14px';
|
|
4598
|
+
this._input = this.createElement('input', [], { type: 'text', 'aria-label': 'Message' });
|
|
4599
|
+
this._btn = this.createElement('button', ['nk-ai-send'], { type: 'button', 'aria-label': 'Send' });
|
|
4600
|
+
this._btn.textContent = '↑';
|
|
4601
|
+
this._row.append(this._icon, this._input, this._btn);
|
|
4602
|
+
this._wrapper.appendChild(this._row);
|
|
4603
|
+
this._sync();
|
|
4604
|
+
}
|
|
4605
|
+
|
|
4606
|
+
_sync() {
|
|
4607
|
+
this._input.placeholder = this.getAttribute('placeholder') || 'Ask something …';
|
|
4608
|
+
if (this.hasAttribute('value') && this._input.value !== this.getAttribute('value')) this._input.value = this.getAttribute('value');
|
|
4609
|
+
this._input.disabled = this._btn.disabled = this.getBoolAttr('disabled');
|
|
4610
|
+
const icon = this.getAttribute('icon') ?? '✨';
|
|
4611
|
+
this._icon.textContent = icon;
|
|
4612
|
+
this._icon.style.display = icon ? '' : 'none';
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
setupEvents() {
|
|
4616
|
+
this._onSend = () => this.submit();
|
|
4617
|
+
this._onKey = (e) => { if (e.key === 'Enter') { e.preventDefault(); this.submit(); } };
|
|
4618
|
+
this._btn.addEventListener('click', this._onSend);
|
|
4619
|
+
this._input.addEventListener('keydown', this._onKey);
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4622
|
+
teardownEvents() {
|
|
4623
|
+
this._btn?.removeEventListener('click', this._onSend);
|
|
4624
|
+
this._input?.removeEventListener('keydown', this._onKey);
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
submit() {
|
|
4628
|
+
const text = this._input.value.trim();
|
|
4629
|
+
if (!text || this.getBoolAttr('disabled')) return;
|
|
4630
|
+
const ok = this.emit('nk-submit', { text });
|
|
4631
|
+
if (ok) this._input.value = '';
|
|
4632
|
+
}
|
|
4633
|
+
|
|
4634
|
+
onAttributeChanged() { this._sync(); }
|
|
4635
|
+
focus(o) { this._input?.focus(o); }
|
|
4636
|
+
get value() { return this._input?.value ?? ''; }
|
|
4637
|
+
set value(v) { if (this._input) this._input.value = v ?? ''; }
|
|
4638
|
+
}
|
|
4639
|
+
|
|
4640
|
+
customElements.define('nk-ai-input-row', NkAiInputRow);
|
|
4641
|
+
|
|
4642
|
+
exports.NkAiInputRow = NkAiInputRow;
|
|
4643
|
+
exports.NkAiMsg = NkAiMsg;
|
|
4644
|
+
exports.NkAiThread = NkAiThread;
|
|
4645
|
+
exports.NkApp = NkApp;
|
|
4646
|
+
exports.NkAvatarGroup = NkAvatarGroup;
|
|
4647
|
+
exports.NkBanner = NkBanner;
|
|
4648
|
+
exports.NkBlockHost = NkBlockHost;
|
|
4649
|
+
exports.NkBoardView = NkBoardView;
|
|
4650
|
+
exports.NkBreadcrumb = NkBreadcrumb;
|
|
4651
|
+
exports.NkBtn = NkBtn;
|
|
4652
|
+
exports.NkCallout = NkCallout;
|
|
4653
|
+
exports.NkCheck = NkCheck;
|
|
4654
|
+
exports.NkCmdk = NkCmdk;
|
|
4655
|
+
exports.NkCode = NkCode;
|
|
4656
|
+
exports.NkComment = NkComment;
|
|
4657
|
+
exports.NkComments = NkComments;
|
|
4658
|
+
exports.NkDangerZone = NkDangerZone;
|
|
4659
|
+
exports.NkDatabase = NkDatabase;
|
|
4660
|
+
exports.NkDivider = NkDivider;
|
|
4661
|
+
exports.NkElement = NkElement;
|
|
4662
|
+
exports.NkEmojiPicker = NkEmojiPicker;
|
|
4663
|
+
exports.NkEmpty = NkEmpty;
|
|
4664
|
+
exports.NkField = NkField;
|
|
4665
|
+
exports.NkFilterBar = NkFilterBar;
|
|
4666
|
+
exports.NkFormElement = NkFormElement;
|
|
4667
|
+
exports.NkHeading = NkHeading;
|
|
4668
|
+
exports.NkInput = NkInput;
|
|
4669
|
+
exports.NkKbd = NkKbd;
|
|
4670
|
+
exports.NkMemberList = NkMemberList;
|
|
4671
|
+
exports.NkMemberRow = NkMemberRow;
|
|
4672
|
+
exports.NkMention = NkMention;
|
|
4673
|
+
exports.NkMenu = NkMenu;
|
|
4674
|
+
exports.NkMenuItem = NkMenuItem;
|
|
4675
|
+
exports.NkModal = NkModal;
|
|
4676
|
+
exports.NkModelCard = NkModelCard;
|
|
4677
|
+
exports.NkPage = NkPage;
|
|
4678
|
+
exports.NkPageActions = NkPageActions;
|
|
4679
|
+
exports.NkPageCover = NkPageCover;
|
|
4680
|
+
exports.NkPageTitle = NkPageTitle;
|
|
4681
|
+
exports.NkPop = NkPop;
|
|
4682
|
+
exports.NkProfileRow = NkProfileRow;
|
|
4683
|
+
exports.NkProgress = NkProgress;
|
|
4684
|
+
exports.NkQuote = NkQuote;
|
|
4685
|
+
exports.NkRadio = NkRadio;
|
|
4686
|
+
exports.NkSectionLabel = NkSectionLabel;
|
|
4687
|
+
exports.NkSegmented = NkSegmented;
|
|
4688
|
+
exports.NkSelect = NkSelect;
|
|
4689
|
+
exports.NkSettingsPane = NkSettingsPane;
|
|
4690
|
+
exports.NkSettingsUser = NkSettingsUser;
|
|
4691
|
+
exports.NkSidebar = NkSidebar;
|
|
4692
|
+
exports.NkSkeleton = NkSkeleton;
|
|
4693
|
+
exports.NkSlider = NkSlider;
|
|
4694
|
+
exports.NkStat = NkStat;
|
|
4695
|
+
exports.NkStats = NkStats;
|
|
4696
|
+
exports.NkSwitch = NkSwitch;
|
|
4697
|
+
exports.NkSynced = NkSynced;
|
|
4698
|
+
exports.NkTab = NkTab;
|
|
4699
|
+
exports.NkTableView = NkTableView;
|
|
4700
|
+
exports.NkTabs = NkTabs;
|
|
4701
|
+
exports.NkTag = NkTag;
|
|
4702
|
+
exports.NkTemplateBtn = NkTemplateBtn;
|
|
4703
|
+
exports.NkTextarea = NkTextarea;
|
|
4704
|
+
exports.NkThemeToggle = NkThemeToggle;
|
|
4705
|
+
exports.NkToast = NkToast;
|
|
4706
|
+
exports.NkTodo = NkTodo;
|
|
4707
|
+
exports.NkToggle = NkToggle;
|
|
4708
|
+
exports.NkTopbar = NkTopbar;
|
|
4709
|
+
exports.NkTree = NkTree;
|
|
4710
|
+
exports.NkTreeItem = NkTreeItem;
|
|
4711
|
+
exports.NkWorkspaceSwitcher = NkWorkspaceSwitcher;
|
|
4712
|
+
exports.compareBy = compareBy;
|
|
4713
|
+
exports.renderPropertyCell = renderPropertyCell;
|
|
4714
|
+
exports.tagFor = tagFor;
|
|
4715
|
+
|
|
4716
|
+
return exports;
|
|
4717
|
+
|
|
4718
|
+
})({});
|