diff --git a/admin/admin.css b/admin/admin.css index e1510a7..8b81c2b 100644 --- a/admin/admin.css +++ b/admin/admin.css @@ -1,135 +1,377 @@ :root { color-scheme: dark; - --bg: #07080a; - --shell: rgba(10, 10, 14, 0.96); - --sidebar: rgba(7, 8, 11, 0.9); - --surface: rgba(255, 255, 255, 0.034); - --surface-strong: rgba(255, 255, 255, 0.062); - --field: rgba(255, 255, 255, 0.045); - --text: rgba(255, 255, 255, 0.93); - --muted: rgba(255, 255, 255, 0.58); - --faint: rgba(255, 255, 255, 0.36); - --accent: #b9ff4a; - --accent-text: #071005; - --danger: #ff6f6f; - --radius-lg: 1.35rem; - --radius-md: 1.1rem; + --nodedc-component-accent-rgb: 247 248 244; + --nodedc-component-on-accent-rgb: 11 17 23; + --nodedc-on-accent-rgb: 11 17 23; + --launcher-radius-modal: 1.75rem; + --launcher-radius-card: 1.35rem; + --launcher-radius-control: 1.25rem; + --launcher-radius-circle: 999px; + --nodedc-shell-padding-x: 1.25rem; + --nodedc-shell-padding-top: 1rem; + --nodedc-shell-padding-bottom: 0.75rem; + --nodedc-shell-frame-gutter-x: 18px; + --nodedc-shell-height: 4.25rem; + --nodedc-shell-row-height: 3rem; + --nodedc-shell-logo-width: 7.25rem; + --nodedc-shell-logo-height: 1.79rem; + --nodedc-shell-pill-height: 3.45rem; + --nodedc-shell-pill-padding: 0.32rem; + --nodedc-shell-control-height: 2.78rem; + --admin-nav-width: clamp(20.75rem, 19.5vw, 22rem); + --admin-nav-pad: 1.1rem; + --admin-panel-gap: 1.25rem; + --admin-control-ring: 2.78rem; + --admin-control-inset: 5px; + --surface-base: rgba(8, 8, 11, 0.78); + --surface-strong: rgba(8, 8, 11, 0.9); + --surface-soft: rgba(255, 255, 255, 0.08); + --field: rgba(255, 255, 255, 0.06); + --text-primary: #f7f8f4; + --text-secondary: rgba(247, 248, 244, 0.72); + --text-muted: rgba(247, 248, 244, 0.48); + --danger: #ff7474; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } * { box-sizing: border-box; } -html { +html, +body { min-height: 100%; - background: var(--bg); + background: #050506; } body { margin: 0; min-height: 100vh; overflow: hidden; - background: - radial-gradient(circle at 14% 78%, rgba(185, 255, 74, 0.22), transparent 24rem), - radial-gradient(circle at 86% 18%, rgba(255, 255, 255, 0.08), transparent 18rem), - linear-gradient(145deg, #090a0c 0%, #050608 60%, #0b0c0f 100%); - color: var(--text); - font-family: - Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + color: var(--text-primary); + font-family: inherit; + font-synthesis: none; + -webkit-font-smoothing: antialiased; } button, input, -textarea { +textarea, +select { font: inherit; + outline: none; } button { - appearance: none; + border: 0; + cursor: pointer; +} + +a { + color: inherit; + text-decoration: none; +} + +button:disabled, +input:disabled, +textarea:disabled { + cursor: not-allowed; + opacity: 0.42; +} + +h1, +h2, +h3, +p { + margin: 0; + letter-spacing: 0; } .admin-backdrop { position: fixed; inset: 0; + background: #050506; +} + +.nodedc-expanded-toolbar-shell { + position: relative; + z-index: 80; + width: min(100%, calc(100vw - var(--nodedc-shell-frame-gutter-x))); + margin-inline: auto; + flex-shrink: 0; + padding: var(--nodedc-shell-padding-top) var(--nodedc-shell-padding-x) var(--nodedc-shell-padding-bottom); +} + +.nodedc-expanded-toolbar { + display: flex; + min-height: var(--nodedc-shell-height); + width: 100%; + flex-direction: column; +} + +.nodedc-expanded-toolbar-top { + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + min-height: var(--nodedc-shell-row-height); + width: 100%; + align-items: center; + gap: 1rem; +} + +.nodedc-expanded-toolbar-left, +.nodedc-expanded-toolbar-center, +.nodedc-expanded-toolbar-right { + display: flex; + min-width: 0; + align-items: center; + gap: 0.75rem; +} + +.nodedc-expanded-toolbar-left { + justify-content: flex-start; +} + +.nodedc-expanded-toolbar-center { + justify-content: center; +} + +.nodedc-expanded-toolbar-right { + justify-content: flex-end; +} + +.nodedc-expanded-brand-link { + display: inline-flex; + width: var(--nodedc-shell-logo-width); + height: var(--nodedc-shell-logo-height); + flex: 0 0 auto; + align-items: center; + justify-content: flex-start; + line-height: 0; +} + +.nodedc-expanded-brand-logo { + display: block; + width: 100%; + height: 100%; + object-fit: contain; +} + +.nodedc-expanded-workspace-button { + position: relative; + display: flex; + width: 3rem; + min-width: 3rem; + max-width: 3rem; + height: 3rem; + min-height: 3rem; + max-height: 3rem; + flex: 0 0 3rem; + align-items: center; + justify-content: center; + overflow: hidden; + border-radius: 999px; + background: #000; + padding: 0; +} + +.nodedc-expanded-workspace-avatar { + display: block; + width: 100%; + height: 100%; + border-radius: inherit; + object-fit: cover; +} + +.nodedc-expanded-nav-group, +.nodedc-expanded-user-group { + display: inline-flex; + min-height: var(--nodedc-shell-pill-height); + align-items: center; + gap: 0.18rem; + border-radius: 999px; + background: rgba(64, 64, 64, 0.48); + padding: var(--nodedc-shell-pill-padding); + user-select: none; +} + +.nodedc-expanded-user-group { + gap: 0.22rem; +} + +.nodedc-expanded-nav-button { + position: relative; + display: inline-flex; + min-height: var(--nodedc-shell-control-height); + align-items: center; + justify-content: center; + border-radius: 999px; + background: transparent; + color: rgba(255, 255, 255, 0.68); + padding: 0.2rem 1.22rem; + font-size: 0.74rem; + font-weight: 760; + line-height: 1; + white-space: nowrap; + transition: + background-color 160ms ease, + color 160ms ease, + opacity 160ms ease; +} + +.nodedc-expanded-nav-button:hover { + background: rgba(255, 255, 255, 0.07); + color: rgba(255, 255, 255, 0.96); +} + +.nodedc-expanded-nav-button[data-active="true"] { + background: rgba(255, 255, 255, 0.92); + color: rgba(8, 8, 10, 0.96); +} + +.nodedc-toolbar-icon-button { + display: inline-grid; + width: var(--nodedc-shell-control-height); + height: var(--nodedc-shell-control-height); + place-items: center; + border-radius: 999px; + background: transparent; + color: rgba(255, 255, 255, 0.72); + font-size: 0.82rem; +} + +.nodedc-expanded-profile-trigger { + display: inline-flex; + align-items: center; + gap: 0.22rem; + border-radius: 999px; +} + +.nodedc-expanded-user-avatar-button, +.nodedc-expanded-user-avatar { + display: grid; + width: var(--nodedc-shell-control-height); + height: var(--nodedc-shell-control-height); + place-items: center; + border-radius: 999px; +} + +.nodedc-expanded-user-avatar { background: - linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 32%), - rgba(0, 0, 0, 0.58); - backdrop-filter: blur(26px); + radial-gradient(circle at 70% 18%, rgba(255, 255, 255, 0.8), transparent 24%), + linear-gradient(135deg, #c8c9cb, #8d8f96 55%, #f1f1f1); + color: rgba(8, 8, 10, 0.96); + font-size: 0.74rem; + font-weight: 850; } .admin-shell { position: relative; z-index: 1; display: grid; - grid-template-columns: 296px minmax(0, 1fr); - width: min(88rem, calc(100vw - 7.5rem)); - height: min(88vh, 920px); - margin: 6vh auto; - overflow: hidden; - border-radius: 1.65rem; - background: var(--shell); - box-shadow: 0 28px 80px rgba(0, 0, 0, 0.42); + grid-template-columns: var(--admin-nav-width) minmax(0, 1fr); + gap: var(--admin-panel-gap); + width: min(94rem, calc(100vw - 2.5rem)); + height: min(calc(100vh - 6.65rem), 52rem); + margin: 0 auto 1.25rem; + overflow: visible; +} + +.sidebar, +.editor { + min-width: 0; + min-height: 0; + border-radius: var(--launcher-radius-card); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.012)), + rgba(10, 10, 13, 0.88); + box-shadow: 0 34px 110px rgba(0, 0, 0, 0.52); + backdrop-filter: blur(28px); + -webkit-backdrop-filter: blur(28px); } .sidebar { - min-height: 0; - overflow: auto; - padding: 1rem 0.75rem 1.25rem; - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent 26rem), - var(--sidebar); - box-shadow: inset -1px 0 rgba(255, 255, 255, 0.035); + position: relative; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: 1.05rem; + overflow: hidden; + padding: var(--admin-nav-pad); } .sidebar-head { + position: relative; display: grid; - gap: 1rem; - padding: 0.25rem 0 1.15rem; + gap: 1.05rem; + padding: 0.42rem 0 0; } -.back-link { - display: inline-flex; - align-items: center; - gap: 0.75rem; - width: max-content; - padding: 0.45rem 0.65rem; - color: var(--text); - font-size: 0.98rem; - text-decoration: none; +.sidebar-head h1 { + margin-top: 0.35rem; + font-size: 1.15rem; + line-height: 1.1; } -.workspace-card { +.admin-panel-close { + position: absolute; + top: var(--admin-control-inset); + right: var(--admin-control-inset); display: grid; - grid-template-columns: 2.9rem minmax(0, 1fr); + width: var(--admin-control-ring); + height: var(--admin-control-ring); + place-items: center; + border: 1px solid rgba(255, 255, 255, 0.22); + border-radius: var(--launcher-radius-circle); + background: transparent; + color: rgba(255, 255, 255, 0.8); + font-size: 1.25rem; + line-height: 1; +} + +.admin-panel-close:hover { + border-color: rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.07); + color: var(--text-primary); +} + +.workspace-card, +.admin-panel-client-select { + position: relative; + display: flex; + width: calc(100% + (var(--admin-nav-pad) * 2)); + min-height: calc(var(--admin-control-ring) + (var(--admin-control-inset) * 2)); + margin-inline: calc(var(--admin-nav-pad) * -1); align-items: center; - gap: 0.85rem; - min-height: 4rem; - padding: 0.75rem; - border-radius: 1.35rem; - background: - linear-gradient(135deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.028)), - rgba(255, 255, 255, 0.035); + gap: 0.65rem; + overflow: hidden; + border-radius: var(--launcher-radius-circle); + background: rgba(255, 255, 255, 0.04); + padding: var(--admin-control-inset); + color: rgba(255, 255, 255, 0.66); + opacity: 0.82; } .workspace-logo, -.empty-icon { +.empty-icon, +.admin-panel-client-select__icon, +.block-icon { display: grid; + width: var(--admin-control-ring); + height: var(--admin-control-ring); place-items: center; - width: 2.9rem; - aspect-ratio: 1; - border-radius: 1rem; - background: #050608; - color: var(--accent); - font-size: 0.78rem; - font-weight: 800; + flex: 0 0 auto; + border-radius: var(--launcher-radius-circle); + background: rgba(255, 255, 255, 0.055); + color: rgba(255, 255, 255, 0.58); + font-size: 0.7rem; + font-weight: 850; } .workspace-name { overflow: hidden; - color: var(--text); - font-size: 0.98rem; - font-weight: 800; - line-height: 1.15; + color: var(--text-primary); + font-size: 0.92rem; + font-weight: 820; text-overflow: ellipsis; white-space: nowrap; } @@ -142,79 +384,106 @@ button { .group-desc, .status, .empty-state p { - color: var(--muted); + color: var(--text-muted); } .workspace-role { margin-top: 0.12rem; - font-size: 0.82rem; + font-size: 0.72rem; + font-weight: 750; } .regions { display: grid; - gap: 1.45rem; - padding-bottom: 2rem; + align-content: start; + gap: 1.2rem; + min-height: 0; + overflow: auto; + padding-bottom: 1rem; } .region-title { - margin: 0 0 0.55rem; - padding: 0 0.65rem; - color: rgba(255, 255, 255, 0.64); - font-size: 0.72rem; - font-weight: 800; - letter-spacing: 0.22em; + margin: 0 0 0.48rem; + padding-inline: 0.35rem; + color: var(--text-muted); + font-size: 0.68rem; + font-weight: 850; + letter-spacing: 0.12em; text-transform: uppercase; } .block-list { display: grid; - gap: 0.45rem; + width: calc(100% + (var(--admin-nav-pad) * 2)); + margin-inline: calc(var(--admin-nav-pad) * -1); + gap: 0.22rem; } .block-btn { + position: relative; display: grid; + grid-template-columns: var(--admin-control-ring) minmax(0, 1fr); width: 100%; - min-height: 3rem; - border: 0; - border-radius: var(--radius-md); - background: transparent; - color: rgba(255, 255, 255, 0.74); - cursor: pointer; - gap: 0.22rem; - padding: 0.72rem 0.85rem; + min-height: calc(var(--admin-control-ring) + (var(--admin-control-inset) * 2)); + align-items: center; + gap: 0.86rem; + border-radius: var(--launcher-radius-circle); + background: rgba(255, 255, 255, 0.04); + color: rgba(255, 255, 255, 0.66); + opacity: 0.66; + padding: var(--admin-control-inset) 0.9rem var(--admin-control-inset) var(--admin-control-inset); text-align: left; + transition: + background 160ms ease, + color 160ms ease, + opacity 160ms ease, + transform 160ms ease, + box-shadow 160ms ease; } .block-btn:hover { - background: rgba(255, 255, 255, 0.04); - color: var(--text); + background: rgba(255, 255, 255, 0.085); + color: var(--text-primary); + opacity: 0.92; + transform: translateY(-1px); } .block-btn.active { - background: - linear-gradient(135deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.025)), - rgba(255, 255, 255, 0.05); - color: var(--accent); - box-shadow: inset 2px 0 var(--accent); + background: rgba(255, 255, 255, 0.115); + color: var(--text-primary); + opacity: 1; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.048), + 0 10px 22px rgba(0, 0, 0, 0.12); +} + +.block-btn.active .block-icon { + background: rgba(247, 248, 244, 0.96); + color: rgb(var(--nodedc-on-accent-rgb)); } .block-btn.disabled { - color: rgba(255, 255, 255, 0.3); + opacity: 0.3; +} + +.block-copy { + display: grid; + min-width: 0; + gap: 0.12rem; } .block-name { overflow: hidden; - font-size: 0.94rem; - font-weight: 780; - line-height: 1.18; + font-size: 0.91rem; + font-weight: 760; text-overflow: ellipsis; white-space: nowrap; } .block-meta { overflow: hidden; - font-size: 0.74rem; - line-height: 1.2; + font-size: 0.72rem; + font-weight: 650; text-overflow: ellipsis; white-space: nowrap; } @@ -222,9 +491,9 @@ button { .editor { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; - min-width: 0; - min-height: 0; - padding: 1.6rem 1.9rem 1.5rem; + overflow: hidden; + padding: 1rem; + font-size: 0.875rem; } .editor-head, @@ -241,34 +510,29 @@ button { .editor-head { justify-content: space-between; gap: 1rem; - padding: 0 0 1.25rem; -} - -h1, -h2, -h3 { - margin: 0; - letter-spacing: 0; + padding: 0.35rem 0.35rem 0.95rem; } .editor-head h1 { - color: rgba(255, 255, 255, 0.86); - font-size: 1.2rem; - font-weight: 820; + color: var(--text-muted); + font-size: 0.72rem; + font-weight: 850; + letter-spacing: 0.12em; line-height: 1.2; + text-transform: uppercase; } .breadcrumb { - margin-top: 0.4rem; - font-size: 0.75rem; - line-height: 1.3; + margin-top: 0.34rem; + font-size: 0.74rem; + line-height: 1.35; } .editor-head h2 { - margin-top: 1.35rem; - font-size: clamp(1.8rem, 3vw, 2.45rem); - font-weight: 740; - line-height: 1.02; + margin-top: 0.68rem; + font-size: 1.15rem; + font-weight: 850; + line-height: 1.12; } .actions, @@ -282,43 +546,37 @@ h3 { .ghost-btn, .danger-btn, .icon-btn { - min-height: 2.75rem; - border: 0; - border-radius: 1.25rem; - cursor: pointer; - padding: 0.72rem 1.08rem; - text-decoration: none; + display: inline-flex; + min-height: var(--nodedc-shell-control-height); + align-items: center; + justify-content: center; + gap: 0.45rem; + border-radius: var(--launcher-radius-circle); + padding: 0 1.05rem; + font-size: 0.78rem; + font-weight: 850; transition: - opacity 0.15s ease, - transform 0.15s ease, - background 0.15s ease; + background 160ms ease, + color 160ms ease, + transform 160ms ease, + opacity 160ms ease; } .primary-btn { - min-width: 12rem; - background: var(--accent); - color: var(--accent-text); - font-weight: 760; + min-width: 9rem; + background: rgba(255, 255, 255, 0.92); + color: rgba(8, 8, 10, 0.96); } .ghost-btn, .icon-btn { - background: - linear-gradient(135deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.025)), - rgba(255, 255, 255, 0.045); - color: var(--text); - font-weight: 700; + background: rgba(255, 255, 255, 0.075); + color: var(--text-primary); } .danger-btn { - background: rgba(255, 111, 111, 0.12); - color: #ffd2d2; - font-weight: 740; -} - -.icon-btn { - width: 2.75rem; - padding: 0; + background: transparent; + color: var(--text-primary); } .primary-btn:hover, @@ -328,15 +586,15 @@ h3 { transform: translateY(-1px); } -.primary-btn:disabled, -.ghost-btn:disabled, -.danger-btn:disabled, -.icon-btn:disabled, -input:disabled, -textarea:disabled { - cursor: not-allowed; - opacity: 0.42; - transform: none; +.ghost-btn:hover, +.icon-btn:hover, +.danger-btn:hover { + background: rgba(255, 255, 255, 0.12); +} + +.icon-btn { + width: var(--nodedc-shell-control-height); + padding: 0; } .empty-state { @@ -344,33 +602,30 @@ textarea:disabled { grid-template-columns: 3rem minmax(0, 1fr); align-self: start; gap: 1rem; - max-width: 44rem; - margin-top: 1.4rem; - padding: 1.25rem; - border-radius: var(--radius-lg); - background: - linear-gradient(135deg, rgba(255, 255, 255, 0.065), rgba(255, 255, 255, 0.026)), - var(--surface); - backdrop-filter: blur(18px); + margin: 1.4rem 0.35rem 0; + padding: 1.15rem; + border-radius: var(--launcher-radius-card); + background: rgba(255, 255, 255, 0.045); } .empty-state h3 { - font-size: 1.05rem; + font-size: 1.02rem; line-height: 1.25; } .empty-state p { - margin: 0.35rem 0 0; - font-size: 0.9rem; + margin-top: 0.35rem; + font-size: 0.84rem; + font-weight: 650; line-height: 1.45; } .block-form { + display: grid; min-height: 0; overflow: auto; - display: grid; - gap: 1.15rem; - padding: 0 0.1rem 5rem; + gap: 1rem; + padding: 0 0.2rem 5rem; scrollbar-color: rgba(255, 255, 255, 0.25) transparent; } @@ -380,20 +635,14 @@ textarea:disabled { .settings-card, .content-panel, -.json-details { - border-radius: var(--radius-lg); - background: - linear-gradient(135deg, rgba(255, 255, 255, 0.058), rgba(255, 255, 255, 0.018)), - var(--surface); - backdrop-filter: blur(18px); +.json-details, +.content-group { + border-radius: var(--launcher-radius-card); + background: rgba(255, 255, 255, 0.045); } .settings-card { - padding: 1.25rem; -} - -.block-meta-card { - margin-top: 0.1rem; + padding: 1rem; } .card-head, @@ -406,37 +655,38 @@ textarea:disabled { .card-head h3, .panel-head h3, .group-head h3 { - color: rgba(255, 255, 255, 0.9); + color: var(--text-primary); font-size: 1rem; - font-weight: 820; + font-weight: 850; line-height: 1.2; } .section-kicker { - margin-bottom: 0.35rem; - color: rgba(255, 255, 255, 0.58); + margin-bottom: 0.32rem; + color: var(--text-muted); font-size: 0.72rem; font-weight: 850; - letter-spacing: 0.22em; + letter-spacing: 0.08em; text-transform: uppercase; } .field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.85rem; - margin-top: 1.2rem; + gap: 0.75rem; + margin-top: 1rem; } .content-panel { display: grid; gap: 1rem; - padding: 1.2rem; + padding: 1rem; } .panel-note { - max-width: 22rem; + max-width: 24rem; font-size: 0.8rem; + font-weight: 650; line-height: 1.35; text-align: right; } @@ -449,32 +699,30 @@ textarea:disabled { .content-group { display: grid; gap: 1rem; - padding: 1.1rem; - border-radius: 1.25rem; - background: - linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.018)), - rgba(255, 255, 255, 0.026); + padding: 1rem; } .group-desc { - margin: 0.2rem 0 0; + margin-top: 0.22rem; font-size: 0.78rem; + font-weight: 650; line-height: 1.35; } .group-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.85rem; + gap: 0.75rem; } .field-control { display: grid; - gap: 0.45rem; min-width: 0; - color: rgba(255, 255, 255, 0.72); - font-size: 0.82rem; - font-weight: 730; + gap: 0.38rem; + color: var(--text-muted); + font-size: 0.72rem; + font-weight: 800; + text-transform: uppercase; } .field-control.wide { @@ -483,10 +731,10 @@ textarea:disabled { .field-label-row { display: flex; + min-width: 0; align-items: center; justify-content: space-between; gap: 0.75rem; - min-width: 0; } .field-label-text { @@ -499,11 +747,11 @@ textarea:disabled { flex: 0 0 auto; border-radius: 999px; background: rgba(255, 255, 255, 0.07); - color: rgba(255, 255, 255, 0.52); - font-size: 0.68rem; + color: var(--text-muted); + font-size: 0.64rem; font-weight: 820; letter-spacing: 0.04em; - padding: 0.2rem 0.45rem; + padding: 0.18rem 0.42rem; text-transform: uppercase; } @@ -512,6 +760,7 @@ textarea:disabled { font-size: 0.68rem; font-weight: 600; text-overflow: ellipsis; + text-transform: none; white-space: nowrap; } @@ -519,20 +768,16 @@ input, textarea { width: 100%; border: 0; - outline: 0; - border-radius: 1.18rem; - background: - linear-gradient(135deg, rgba(255, 255, 255, 0.058), rgba(255, 255, 255, 0.018)), - var(--field); - color: var(--text); - padding: 0.88rem 0.95rem; + border-radius: 1rem; + background: var(--field); + color: var(--text-primary); + padding: 0.75rem 0.85rem; } input:focus, textarea:focus { - box-shadow: - inset 0 0 0 1px rgba(185, 255, 74, 0.3), - 0 0 0 4px rgba(185, 255, 74, 0.055); + background: rgba(255, 255, 255, 0.1); + box-shadow: none; } textarea { @@ -543,9 +788,9 @@ textarea { .toggle-row { justify-content: flex-start; gap: 0.65rem; - color: rgba(255, 255, 255, 0.78); - font-size: 0.86rem; - font-weight: 780; + color: var(--text-secondary); + font-size: 0.82rem; + font-weight: 800; } .toggle-row input { @@ -569,28 +814,164 @@ textarea { } .toggle-row input:checked { - background: var(--accent); + background: rgba(255, 255, 255, 0.92); } .toggle-row input:checked::before { - background: #050608; + background: #050506; transform: translateX(1.22rem); } .empty-note { - color: var(--muted); - font-size: 0.9rem; + color: var(--text-muted); + font-size: 0.86rem; + font-weight: 650; line-height: 1.42; } +.service-media-field { + min-width: 0; +} + +.service-media-control { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + min-height: 3.35rem; + gap: 0.22rem; + overflow: hidden; + border-radius: var(--launcher-radius-circle); + background: rgba(255, 255, 255, 0.06); + padding: 0.25rem; +} + +.service-media-file-control { + display: flex; + min-width: 0; + height: 100%; + align-items: center; + gap: 0.7rem; + padding-left: 0.15rem; +} + +.service-media-file-control input[type="file"], +.service-media-hidden-input { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; +} + +.service-media-file-button { + display: inline-flex; + min-height: 2.78rem; + align-items: center; + justify-content: center; + border-radius: var(--launcher-radius-circle); + background: rgba(255, 255, 255, 0.92); + color: rgba(8, 8, 10, 0.96); + padding: 0 1rem; + font-size: 0.78rem; + font-weight: 850; + text-transform: none; + white-space: nowrap; + cursor: pointer; +} + +.service-media-file-button:hover { + background: #fff; +} + +.service-media-file-name { + min-width: 0; + overflow: hidden; + color: var(--text-muted); + font-size: 0.78rem; + font-weight: 650; + text-overflow: ellipsis; + text-transform: none; + white-space: nowrap; +} + +.service-media-url-input { + min-width: 0; + height: 100%; + border-radius: var(--launcher-radius-circle); + background: transparent; + padding: 0 0.82rem; +} + +.service-media-source-switch { + display: inline-flex; + align-items: center; + gap: 0.12rem; +} + +.service-media-source-button { + display: grid; + width: 2.78rem; + height: 2.78rem; + place-items: center; + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: var(--launcher-radius-circle); + background: transparent; + color: rgba(255, 255, 255, 0.66); +} + +.service-media-source-button:hover { + background: rgba(255, 255, 255, 0.07); + color: var(--text-primary); +} + +.service-media-source-button[data-active="true"] { + border-color: rgba(255, 255, 255, 0.92); + background: rgba(255, 255, 255, 0.92); + color: rgba(8, 8, 10, 0.96); +} + +.service-media-preview { + position: relative; + display: grid; + width: 2.78rem; + min-width: 2.78rem; + height: 2.78rem; + place-items: center; + overflow: hidden; + border-radius: var(--launcher-radius-circle); + background: rgba(255, 255, 255, 0.055); + color: rgba(255, 255, 255, 0.62); + font-size: 0.78rem; + text-transform: none; +} + +.service-media-preview img, +.service-media-preview video { + position: absolute; + inset: 0; + display: block; + width: 100%; + height: 100%; + border-radius: inherit; + object-fit: cover; +} + +.media-upload-error { + color: #ffd2d2; + font-size: 0.72rem; + font-weight: 750; + text-transform: none; +} + .json-details { - padding: 1rem 1.2rem; + padding: 1rem; } .json-details summary { cursor: pointer; - color: rgba(255, 255, 255, 0.74); - font-size: 0.9rem; + color: var(--text-secondary); + font-size: 0.86rem; font-weight: 800; } @@ -598,7 +979,7 @@ textarea { display: grid; gap: 0.7rem; margin-top: 1rem; - color: var(--muted); + color: var(--text-muted); font-size: 0.82rem; font-weight: 720; } @@ -612,9 +993,10 @@ textarea { .status { min-height: 1.25rem; - margin: 1rem 0 0; + margin: 0.8rem 0.35rem 0; overflow: hidden; font-size: 0.78rem; + font-weight: 650; line-height: 1.35; text-overflow: ellipsis; white-space: pre-wrap; @@ -625,17 +1007,37 @@ textarea { overflow: auto; } + .nodedc-expanded-toolbar-top { + grid-template-columns: 1fr; + justify-items: center; + } + + .nodedc-expanded-toolbar-center { + width: 100%; + justify-content: flex-start; + justify-self: stretch; + overflow-x: auto; + } + + .nodedc-expanded-nav-group { + max-width: 100%; + overflow-x: auto; + } + + .nodedc-expanded-toolbar-left, + .nodedc-expanded-toolbar-right { + display: none; + } + .admin-shell { grid-template-columns: 1fr; width: min(100vw - 1.5rem, 58rem); height: auto; - min-height: calc(100vh - 1.5rem); - margin: 0.75rem auto; + min-height: calc(100vh - 6rem); } .sidebar { max-height: 38vh; - box-shadow: inset 0 -1px rgba(255, 255, 255, 0.035); } .editor { @@ -653,15 +1055,17 @@ textarea { } @media (max-width: 720px) { - .admin-shell { - width: 100vw; - min-height: 100vh; - margin: 0; - border-radius: 0; + .nodedc-expanded-toolbar-shell { + width: 100%; + padding-inline: 0.75rem; } - .editor { - padding: 1.1rem; + .admin-shell { + width: 100vw; + min-height: calc(100vh - 5.25rem); + margin: 0; + gap: 0.75rem; + padding: 0 0.75rem 0.75rem; } .field-grid, @@ -670,7 +1074,8 @@ textarea { } .panel-head, - .card-head { + .card-head, + .group-head { align-items: flex-start; flex-direction: column; } @@ -678,4 +1083,12 @@ textarea { .panel-note { text-align: left; } + + .service-media-control { + grid-template-columns: minmax(0, 1fr) auto; + } + + .service-media-preview { + display: none; + } } diff --git a/admin/admin.js b/admin/admin.js index 8c1e4c5..14e7e14 100644 --- a/admin/admin.js +++ b/admin/admin.js @@ -132,6 +132,8 @@ const GROUP_COPY = { Контент: "Остальные typed-поля выбранного блока.", }; +const MEDIA_ACCEPT = "image/*,video/*,.gif,.webm,.mov,.mp4,.m4v,.avi,.mkv,.glb,.gltf,.svg,.avif,.webp,.woff,.woff2"; + function field(path, label, kind = "text", renderAs = kind === "html" ? "html" : "text", group = null, options = {}) { return { path, label, kind, renderAs, group: group || inferFieldGroup(path), ...options }; } @@ -247,6 +249,85 @@ function uniqueId(base) { return candidate; } +function truncateText(value, maxLength = 22) { + if (!value) return ""; + return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value; +} + +function fileNameFromUrl(value) { + if (!value) return "Файл не выбран"; + + try { + const clean = value.split("?")[0].split("#")[0]; + return decodeURIComponent(clean.slice(clean.lastIndexOf("/") + 1)) || value; + } catch { + return value; + } +} + +function safeBucketSegment(value) { + const safe = String(value || "site") + .normalize("NFKD") + .replace(/[^\w.-]+/g, "-") + .replace(/^-+|-+$/g, "") + .toLowerCase(); + + return safe || "site"; +} + +function adminPreviewUrl(value) { + if (!value) return ""; + if (/^(blob:|data:|https?:|\/)/i.test(value)) return value; + if (value.startsWith("./")) return `.${value}`; + return value; +} + +function mediaKindFromValue(value) { + if (!value) return null; + if (/\.(mp4|webm|mov|m4v|avi|mkv)(\?.*)?$/i.test(value)) return "video"; + if (/\.(png|jpe?g|gif|svg|avif|webp)(\?.*)?$/i.test(value)) return "image"; + return null; +} + +function uploadBucketForField(block, editableField) { + const path = editableField.path.toLowerCase(); + const currentValue = String(getByPath(block, editableField.path) ?? "").toLowerCase(); + const blockSegment = safeBucketSegment(block?.id || block?.type || "site"); + let root = "images"; + + if (path.includes("webgl") || currentValue.endsWith(".glb") || currentValue.endsWith(".gltf")) root = "models"; + if (path.includes("video") || path.includes("media") || path.includes("sliders") || path.includes("slideritems")) root = "media"; + if (path.includes("font") || /\.(woff2?|ttf|otf)(\?.*)?$/i.test(currentValue)) root = "fonts"; + + return `${root}/${blockSegment}`; +} + +function blockIcon(block) { + const id = block.id || ""; + const type = block.type || ""; + + if (id === "static-elements") return "SE"; + if (id.includes("hero")) return "H"; + if (id.includes("feature") || id.includes("video")) return "V"; + if (id.includes("component")) return "C"; + if (id.includes("modes")) return "AI"; + if (id.includes("faq")) return "Q"; + if (id.includes("pricing")) return "P"; + if (id.includes("waitlist")) return "W"; + if (id.includes("footer")) return "F"; + if (type.includes("Cta")) return "GO"; + return "B"; +} + +function readFileAsDataUrl(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.addEventListener("load", () => resolve(String(reader.result))); + reader.addEventListener("error", () => reject(reader.error ?? new Error("Не удалось прочитать файл"))); + reader.readAsDataURL(file); + }); +} + function labelForRegion(region) { if (region.id === "beforeCViz") return "Главная"; if (region.id === "cViz") return "Секции"; @@ -255,16 +336,22 @@ function labelForRegion(region) { function renderBlockButton({ list, block, meta, active, disabled, onClick }) { const button = document.createElement("button"); + const icon = document.createElement("span"); + const copy = document.createElement("span"); const name = document.createElement("span"); const description = document.createElement("span"); button.type = "button"; button.className = `block-btn${active ? " active" : ""}${disabled ? " disabled" : ""}`; + icon.className = "block-icon"; + icon.textContent = blockIcon(block); + copy.className = "block-copy"; name.className = "block-name"; name.textContent = block.adminLabel || block.id; description.className = "block-meta"; description.textContent = meta; - button.append(name, description); + copy.append(name, description); + button.append(icon, copy); button.addEventListener("click", onClick); list.append(button); } @@ -396,6 +483,210 @@ function groupEditableFields(block) { return groups; } +function renderMediaPreview(preview, value) { + preview.innerHTML = ""; + + const src = adminPreviewUrl(value); + const kind = mediaKindFromValue(value); + + if (!src) { + preview.textContent = "FILE"; + return; + } + + if (kind === "video") { + const video = document.createElement("video"); + video.src = src; + video.autoplay = true; + video.loop = true; + video.muted = true; + video.playsInline = true; + preview.append(video); + return; + } + + if (kind === "image") { + const image = document.createElement("img"); + image.src = src; + image.alt = ""; + image.loading = "lazy"; + image.addEventListener("error", () => { + preview.innerHTML = ""; + preview.textContent = "FILE"; + }); + preview.append(image); + return; + } + + preview.textContent = "FILE"; +} + +function updateMediaFieldValue({ block, editableField, hiddenInput, urlInput, fileName, preview, value }) { + hiddenInput.value = value; + urlInput.value = value; + fileName.textContent = truncateText(fileNameFromUrl(value), 28); + fileName.title = fileNameFromUrl(value); + setByPath(block, editableField.path, value); + renderMediaPreview(preview, value); + + if (activeBlock() === block) { + el.json.value = JSON.stringify(block, null, 2); + } +} + +function renderMediaField({ block, editableField, grid }) { + const value = getByPath(block, editableField.path) ?? ""; + const container = document.createElement("div"); + const labelRow = document.createElement("div"); + const labelText = document.createElement("span"); + const kind = document.createElement("span"); + const hiddenInput = document.createElement("input"); + const control = document.createElement("div"); + const fileControl = document.createElement("div"); + const fileLabel = document.createElement("label"); + const fileName = document.createElement("span"); + const fileInput = document.createElement("input"); + const urlInput = document.createElement("input"); + const sourceSwitch = document.createElement("div"); + const fileSourceButton = document.createElement("button"); + const urlSourceButton = document.createElement("button"); + const preview = document.createElement("div"); + const path = document.createElement("span"); + const error = document.createElement("span"); + const inputId = `media-${editableField.path.replace(/[^a-z0-9_-]+/gi, "-")}-${Math.random().toString(36).slice(2)}`; + let source = /^(https?:)?\/\//i.test(value) ? "url" : "file"; + + function setSource(nextSource) { + source = nextSource; + fileControl.hidden = source !== "file"; + urlInput.hidden = source !== "url"; + fileSourceButton.dataset.active = String(source === "file"); + urlSourceButton.dataset.active = String(source === "url"); + } + + container.className = "field-control wide service-media-field"; + labelRow.className = "field-label-row"; + labelText.className = "field-label-text"; + labelText.textContent = editableField.label || editableField.path; + kind.className = "field-kind"; + kind.textContent = editableField.kind || "media"; + labelRow.append(labelText, kind); + + hiddenInput.className = "service-media-hidden-input"; + hiddenInput.type = "hidden"; + hiddenInput.dataset.path = editableField.path; + hiddenInput.value = value; + + control.className = "service-media-control"; + + fileControl.className = "service-media-file-control"; + fileLabel.className = "service-media-file-button"; + fileLabel.htmlFor = inputId; + fileLabel.textContent = "Выберите файл"; + fileName.className = "service-media-file-name"; + fileName.textContent = truncateText(fileNameFromUrl(value), 28); + fileName.title = fileNameFromUrl(value); + fileInput.id = inputId; + fileInput.type = "file"; + fileInput.accept = MEDIA_ACCEPT; + fileControl.append(fileLabel, fileName, fileInput); + + urlInput.className = "service-media-url-input"; + urlInput.type = "text"; + urlInput.placeholder = "https://..."; + urlInput.autocomplete = "off"; + urlInput.value = value; + + sourceSwitch.className = "service-media-source-switch"; + sourceSwitch.setAttribute("aria-label", `${editableField.label || editableField.path}: источник`); + fileSourceButton.className = "service-media-source-button"; + fileSourceButton.type = "button"; + fileSourceButton.title = "Файл с диска"; + fileSourceButton.setAttribute("aria-label", "Файл с диска"); + fileSourceButton.textContent = "HD"; + urlSourceButton.className = "service-media-source-button"; + urlSourceButton.type = "button"; + urlSourceButton.title = "Внешняя ссылка"; + urlSourceButton.setAttribute("aria-label", "Внешняя ссылка"); + urlSourceButton.textContent = "URL"; + sourceSwitch.append(fileSourceButton, urlSourceButton); + + preview.className = "service-media-preview"; + renderMediaPreview(preview, value); + + path.className = "field-path"; + path.textContent = `${editableField.path} -> assets/uploads/${uploadBucketForField(block, editableField)}`; + error.className = "media-upload-error"; + + fileSourceButton.addEventListener("click", () => setSource("file")); + urlSourceButton.addEventListener("click", () => { + setSource("url"); + urlInput.focus(); + }); + + urlInput.addEventListener("input", () => { + updateMediaFieldValue({ + block, + editableField, + hiddenInput, + urlInput, + fileName, + preview, + value: urlInput.value, + }); + }); + + fileInput.addEventListener("change", async () => { + const file = fileInput.files?.[0]; + if (!file) return; + + error.textContent = ""; + fileName.textContent = "Сохраняем в assets..."; + + try { + const dataUrl = await readFileAsDataUrl(file); + const response = await fetch("/api/upload/asset", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + fileName: file.name, + mimeType: file.type || "application/octet-stream", + bucket: uploadBucketForField(block, editableField), + dataUrl, + }), + }); + const payload = await response.json(); + + if (!response.ok || !payload.ok) { + throw new Error(payload.error || "Файл не удалось сохранить"); + } + + updateMediaFieldValue({ + block, + editableField, + hiddenInput, + urlInput, + fileName, + preview, + value: payload.url, + }); + setSource("file"); + setStatus(`Файл сохранён: ${payload.url}. Нажмите «Сохранить JSON», чтобы записать путь в content/pages/home.json.`); + } catch (uploadError) { + error.textContent = uploadError.message; + fileName.textContent = truncateText(fileNameFromUrl(hiddenInput.value), 28); + setStatus(`Ошибка загрузки: ${uploadError.message}`); + } finally { + fileInput.value = ""; + } + }); + + setSource(source); + control.append(fileControl, urlInput, sourceSwitch, preview); + container.append(labelRow, hiddenInput, control, path, error); + grid.append(container); +} + function renderContentFields(block) { el.contentFields.innerHTML = ""; @@ -425,6 +716,11 @@ function renderContentFields(block) { grid.className = "group-grid"; for (const editableField of fields) { + if (editableField.kind === "media") { + renderMediaField({ block, editableField, grid }); + continue; + } + const label = document.createElement("label"); const labelRow = document.createElement("span"); const labelText = document.createElement("span"); diff --git a/admin/index.html b/admin/index.html index e41572b..ccdbad0 100644 --- a/admin/index.html +++ b/admin/index.html @@ -8,15 +8,56 @@
+