diff --git a/ui/src/styles/config.css b/ui/src/styles/config.css index aff3e88b4..641617b30 100644 --- a/ui/src/styles/config.css +++ b/ui/src/styles/config.css @@ -5,7 +5,7 @@ /* Layout Container */ .config-layout { display: grid; - grid-template-columns: 260px minmax(0, 1fr); + grid-template-columns: 240px minmax(0, 1fr); gap: 0; min-height: calc(100vh - 140px); margin: -16px; @@ -15,16 +15,18 @@ background: var(--panel); } -/* Sidebar */ +/* =========================================== + Sidebar + =========================================== */ .config-sidebar { display: flex; flex-direction: column; - background: linear-gradient(180deg, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.25)); + background: rgba(0, 0, 0, 0.2); border-right: 1px solid var(--border); } :root[data-theme="light"] .config-sidebar { - background: linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.6)); + background: rgba(0, 0, 0, 0.03); } .config-sidebar__header { @@ -36,11 +38,9 @@ } .config-sidebar__title { - font-family: var(--font-display); - font-size: 14px; font-weight: 600; - letter-spacing: 0.5px; - text-transform: uppercase; + font-size: 14px; + letter-spacing: 0.3px; } .config-sidebar__footer { @@ -53,11 +53,12 @@ .config-search { position: relative; padding: 12px; + border-bottom: 1px solid var(--border); } .config-search__icon { position: absolute; - left: 22px; + left: 24px; top: 50%; transform: translateY(-50%); width: 16px; @@ -68,22 +69,31 @@ .config-search__input { width: 100%; - padding: 10px 12px 10px 38px; + padding: 10px 32px 10px 40px; border: 1px solid var(--border); - border-radius: 10px; - background: rgba(0, 0, 0, 0.2); + border-radius: 8px; + background: rgba(0, 0, 0, 0.15); font-size: 13px; outline: none; - transition: border-color 150ms ease, box-shadow 150ms ease; + transition: border-color 150ms ease, box-shadow 150ms ease, background 150ms ease; +} + +.config-search__input::placeholder { + color: var(--muted); } .config-search__input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--focus); + background: rgba(0, 0, 0, 0.2); } :root[data-theme="light"] .config-search__input { - background: rgba(255, 255, 255, 0.9); + background: rgba(255, 255, 255, 0.8); +} + +:root[data-theme="light"] .config-search__input:focus { + background: #fff; } .config-search__clear { @@ -97,7 +107,8 @@ border-radius: 50%; background: rgba(255, 255, 255, 0.1); color: var(--muted); - font-size: 14px; + font-size: 16px; + line-height: 1; cursor: pointer; display: flex; align-items: center; @@ -120,21 +131,23 @@ .config-nav__item { display: flex; align-items: center; - gap: 10px; + gap: 12px; width: 100%; padding: 10px 12px; border: none; - border-radius: 10px; + border-radius: 8px; background: transparent; - color: var(--text); + color: var(--muted); font-size: 13px; + font-weight: 500; text-align: left; cursor: pointer; - transition: background 150ms ease, transform 100ms ease; + transition: background 150ms ease, color 150ms ease; } .config-nav__item:hover { - background: rgba(255, 255, 255, 0.06); + background: rgba(255, 255, 255, 0.05); + color: var(--text); } :root[data-theme="light"] .config-nav__item:hover { @@ -142,18 +155,24 @@ } .config-nav__item.active { - background: rgba(245, 159, 74, 0.15); + background: rgba(245, 159, 74, 0.12); color: var(--accent); } -.config-nav__item:active { - transform: scale(0.98); +.config-nav__icon { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; } -.config-nav__icon { - font-size: 16px; - width: 24px; - text-align: center; +.config-nav__icon svg { + width: 18px; + height: 18px; + stroke: currentColor; + fill: none; } .config-nav__label { @@ -166,27 +185,27 @@ /* Mode Toggle */ .config-mode-toggle { display: flex; - gap: 4px; - padding: 4px; + padding: 3px; background: rgba(0, 0, 0, 0.2); - border-radius: 10px; + border-radius: 8px; + border: 1px solid var(--border); } :root[data-theme="light"] .config-mode-toggle { - background: rgba(0, 0, 0, 0.08); + background: rgba(0, 0, 0, 0.06); } .config-mode-toggle__btn { flex: 1; padding: 8px 12px; border: none; - border-radius: 8px; + border-radius: 6px; background: transparent; color: var(--muted); font-size: 12px; - font-weight: 500; + font-weight: 600; cursor: pointer; - transition: background 150ms ease, color 150ms ease; + transition: background 150ms ease, color 150ms ease, box-shadow 150ms ease; } .config-mode-toggle__btn:hover { @@ -196,15 +215,17 @@ .config-mode-toggle__btn.active { background: rgba(255, 255, 255, 0.1); color: var(--text); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); } :root[data-theme="light"] .config-mode-toggle__btn.active { - background: rgba(255, 255, 255, 0.9); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } -/* Main Content */ +/* =========================================== + Main Content + =========================================== */ .config-main { display: flex; flex-direction: column; @@ -218,13 +239,13 @@ align-items: center; justify-content: space-between; gap: 12px; - padding: 12px 16px; - background: rgba(0, 0, 0, 0.1); + padding: 12px 20px; + background: rgba(0, 0, 0, 0.08); border-bottom: 1px solid var(--border); } :root[data-theme="light"] .config-actions { - background: rgba(0, 0, 0, 0.03); + background: rgba(0, 0, 0, 0.02); } .config-actions__left, @@ -235,40 +256,37 @@ } .config-changes-badge { - padding: 4px 10px; + padding: 5px 12px; border-radius: 999px; - background: rgba(245, 159, 74, 0.2); - border: 1px solid rgba(245, 159, 74, 0.4); + background: rgba(245, 159, 74, 0.15); + border: 1px solid rgba(245, 159, 74, 0.3); color: var(--accent); font-size: 12px; - font-weight: 500; + font-weight: 600; } .config-status { - font-size: 12px; + font-size: 13px; + color: var(--muted); } /* Diff Panel */ .config-diff { - margin: 12px 16px; - border: 1px solid var(--border); - border-radius: 12px; - background: rgba(0, 0, 0, 0.15); - overflow: hidden; -} - -:root[data-theme="light"] .config-diff { + margin: 16px 20px 0; + border: 1px solid rgba(245, 159, 74, 0.3); + border-radius: 10px; background: rgba(245, 159, 74, 0.05); + overflow: hidden; } .config-diff__summary { display: flex; align-items: center; justify-content: space-between; - padding: 10px 14px; + padding: 12px 16px; cursor: pointer; font-size: 13px; - font-weight: 500; + font-weight: 600; color: var(--accent); list-style: none; } @@ -283,46 +301,53 @@ transition: transform 200ms ease; } +.config-diff__chevron svg { + width: 100%; + height: 100%; +} + .config-diff[open] .config-diff__chevron { transform: rotate(180deg); } .config-diff__content { - padding: 0 14px 14px; + padding: 0 16px 16px; display: grid; gap: 8px; } .config-diff__item { - padding: 8px 10px; - border-radius: 8px; - background: rgba(0, 0, 0, 0.15); + display: flex; + align-items: baseline; + gap: 12px; + padding: 8px 12px; + border-radius: 6px; + background: rgba(0, 0, 0, 0.1); font-size: 12px; + font-family: var(--mono); } :root[data-theme="light"] .config-diff__item { - background: rgba(255, 255, 255, 0.8); + background: rgba(255, 255, 255, 0.6); } .config-diff__path { - font-family: var(--mono); font-weight: 600; - margin-bottom: 4px; color: var(--text); + flex-shrink: 0; } .config-diff__values { display: flex; - align-items: center; + align-items: baseline; gap: 8px; - font-family: var(--mono); - color: var(--muted); + min-width: 0; + flex-wrap: wrap; } .config-diff__from { color: var(--danger); - text-decoration: line-through; - opacity: 0.7; + opacity: 0.8; } .config-diff__arrow { @@ -337,13 +362,14 @@ .config-content { flex: 1; overflow-y: auto; - padding: 16px; + padding: 20px; } .config-raw-field textarea { - min-height: 400px; + min-height: 500px; font-family: var(--mono); font-size: 13px; + line-height: 1.5; } /* Loading State */ @@ -352,14 +378,14 @@ flex-direction: column; align-items: center; justify-content: center; - gap: 12px; - padding: 60px 20px; + gap: 16px; + padding: 80px 20px; color: var(--muted); } .config-loading__spinner { - width: 32px; - height: 32px; + width: 36px; + height: 36px; border: 3px solid var(--border); border-top-color: var(--accent); border-radius: 50%; @@ -376,45 +402,46 @@ flex-direction: column; align-items: center; justify-content: center; - gap: 12px; - padding: 60px 20px; + gap: 16px; + padding: 80px 20px; text-align: center; } .config-empty__icon { - font-size: 48px; - opacity: 0.5; + font-size: 56px; + opacity: 0.4; } .config-empty__text { color: var(--muted); - font-size: 14px; + font-size: 15px; } -/* Section Cards */ +/* =========================================== + Section Cards + =========================================== */ .config-form--modern { display: grid; - gap: 16px; + gap: 24px; } .config-section-card { border: 1px solid var(--border); - border-radius: 14px; - background: linear-gradient(160deg, rgba(255, 255, 255, 0.03), transparent 70%), - rgba(0, 0, 0, 0.12); + border-radius: 12px; + background: rgba(255, 255, 255, 0.02); overflow: hidden; } :root[data-theme="light"] .config-section-card { - background: linear-gradient(160deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.7) 70%); + background: rgba(255, 255, 255, 0.5); } .config-section-card__header { display: flex; align-items: flex-start; - gap: 12px; - padding: 16px; - background: rgba(255, 255, 255, 0.02); + gap: 14px; + padding: 18px 20px; + background: rgba(0, 0, 0, 0.06); border-bottom: 1px solid var(--border); } @@ -423,9 +450,15 @@ } .config-section-card__icon { - font-size: 24px; - line-height: 1; - padding-top: 2px; + width: 32px; + height: 32px; + color: var(--accent); + flex-shrink: 0; +} + +.config-section-card__icon svg { + width: 100%; + height: 100%; } .config-section-card__titles { @@ -435,9 +468,8 @@ .config-section-card__title { margin: 0; - font-size: 16px; + font-size: 17px; font-weight: 600; - letter-spacing: 0.2px; } .config-section-card__desc { @@ -448,99 +480,312 @@ } .config-section-card__content { - padding: 16px; + padding: 20px; } -/* Toggle Switch */ -.toggle-switch { - position: relative; - display: inline-flex; - cursor: pointer; +/* =========================================== + Form Fields + =========================================== */ +.cfg-fields { + display: grid; + gap: 20px; } -.toggle-switch input { - position: absolute; - opacity: 0; - width: 0; - height: 0; +.cfg-field { + display: grid; + gap: 6px; } -.toggle-switch__track { - position: relative; - width: 44px; - height: 24px; - background: rgba(255, 255, 255, 0.15); +.cfg-field--error { + padding: 12px; + border-radius: 8px; + background: rgba(255, 92, 92, 0.1); + border: 1px solid rgba(255, 92, 92, 0.3); +} + +.cfg-field__label { + font-size: 13px; + font-weight: 600; + color: var(--text); +} + +.cfg-field__help { + font-size: 12px; + color: var(--muted); + line-height: 1.4; +} + +.cfg-field__error { + font-size: 12px; + color: var(--danger); +} + +/* Text Input */ +.cfg-input-wrap { + display: flex; + gap: 8px; +} + +.cfg-input { + flex: 1; + padding: 10px 12px; border: 1px solid var(--border); - border-radius: 999px; - transition: background 200ms ease, border-color 200ms ease; + border-radius: 8px; + background: rgba(0, 0, 0, 0.12); + font-size: 14px; + outline: none; + transition: border-color 150ms ease, box-shadow 150ms ease, background 150ms ease; } -:root[data-theme="light"] .toggle-switch__track { - background: rgba(0, 0, 0, 0.1); +.cfg-input::placeholder { + color: var(--muted); + opacity: 0.7; } -.toggle-switch__thumb { - position: absolute; - top: 2px; - left: 2px; - width: 18px; - height: 18px; - background: var(--text); - border-radius: 50%; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); - transition: transform 200ms ease, background 200ms ease; -} - -.toggle-switch input:checked + .toggle-switch__track { - background: rgba(43, 217, 127, 0.3); - border-color: rgba(43, 217, 127, 0.5); -} - -.toggle-switch input:checked + .toggle-switch__track .toggle-switch__thumb { - transform: translateX(20px); - background: var(--ok); -} - -.toggle-switch input:focus + .toggle-switch__track { +.cfg-input:focus { + border-color: var(--accent); box-shadow: 0 0 0 3px var(--focus); + background: rgba(0, 0, 0, 0.18); } -.toggle-switch input:disabled + .toggle-switch__track { +:root[data-theme="light"] .cfg-input { + background: #fff; +} + +:root[data-theme="light"] .cfg-input:focus { + background: #fff; +} + +.cfg-input--sm { + padding: 8px 10px; + font-size: 13px; +} + +.cfg-input__reset { + padding: 8px 12px; + border: 1px solid var(--border); + border-radius: 8px; + background: rgba(255, 255, 255, 0.05); + color: var(--muted); + font-size: 14px; + cursor: pointer; + transition: background 150ms ease, color 150ms ease; +} + +.cfg-input__reset:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.1); + color: var(--text); +} + +.cfg-input__reset:disabled { opacity: 0.5; cursor: not-allowed; } -/* Field Row (for toggles) */ -.field-row { +/* Textarea */ +.cfg-textarea { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: 8px; + background: rgba(0, 0, 0, 0.12); + font-family: var(--mono); + font-size: 13px; + line-height: 1.5; + resize: vertical; + outline: none; + transition: border-color 150ms ease, box-shadow 150ms ease; +} + +.cfg-textarea:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--focus); +} + +:root[data-theme="light"] .cfg-textarea { + background: #fff; +} + +.cfg-textarea--sm { + padding: 8px 10px; + font-size: 12px; +} + +/* Number Input */ +.cfg-number { + display: inline-flex; + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; + background: rgba(0, 0, 0, 0.12); +} + +:root[data-theme="light"] .cfg-number { + background: #fff; +} + +.cfg-number__btn { + width: 40px; + border: none; + background: rgba(255, 255, 255, 0.05); + color: var(--text); + font-size: 18px; + font-weight: 300; + cursor: pointer; + transition: background 150ms ease; +} + +.cfg-number__btn:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.1); +} + +.cfg-number__btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +:root[data-theme="light"] .cfg-number__btn { + background: rgba(0, 0, 0, 0.03); +} + +:root[data-theme="light"] .cfg-number__btn:hover:not(:disabled) { + background: rgba(0, 0, 0, 0.06); +} + +.cfg-number__input { + width: 80px; + padding: 10px; + border: none; + border-left: 1px solid var(--border); + border-right: 1px solid var(--border); + background: transparent; + font-size: 14px; + text-align: center; + outline: none; + -moz-appearance: textfield; +} + +.cfg-number__input::-webkit-outer-spin-button, +.cfg-number__input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Select */ +.cfg-select { + padding: 10px 36px 10px 12px; + border: 1px solid var(--border); + border-radius: 8px; + background-color: rgba(0, 0, 0, 0.12); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 10px center; + font-size: 14px; + cursor: pointer; + outline: none; + appearance: none; + transition: border-color 150ms ease, box-shadow 150ms ease; +} + +.cfg-select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--focus); +} + +:root[data-theme="light"] .cfg-select { + background-color: #fff; +} + +/* Segmented Control */ +.cfg-segmented { + display: inline-flex; + padding: 3px; + border: 1px solid var(--border); + border-radius: 8px; + background: rgba(0, 0, 0, 0.12); +} + +:root[data-theme="light"] .cfg-segmented { + background: rgba(0, 0, 0, 0.04); +} + +.cfg-segmented__btn { + padding: 8px 16px; + border: none; + border-radius: 6px; + background: transparent; + color: var(--muted); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background 150ms ease, color 150ms ease, box-shadow 150ms ease; +} + +.cfg-segmented__btn:hover:not(:disabled):not(.active) { + color: var(--text); +} + +.cfg-segmented__btn.active { + background: rgba(255, 255, 255, 0.12); + color: var(--text); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +:root[data-theme="light"] .cfg-segmented__btn.active { + background: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.cfg-segmented__btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Toggle Row */ +.cfg-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; - padding: 12px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); + padding: 14px 16px; + border: 1px solid var(--border); + border-radius: 10px; + background: rgba(0, 0, 0, 0.06); + cursor: pointer; + transition: background 150ms ease, border-color 150ms ease; } -:root[data-theme="light"] .field-row { - border-bottom-color: rgba(0, 0, 0, 0.06); +.cfg-toggle-row:hover:not(.disabled) { + background: rgba(0, 0, 0, 0.1); + border-color: var(--border-strong); } -.field-row:last-child { - border-bottom: none; +.cfg-toggle-row.disabled { + opacity: 0.6; + cursor: not-allowed; } -.field-row__info { +:root[data-theme="light"] .cfg-toggle-row { + background: rgba(255, 255, 255, 0.5); +} + +:root[data-theme="light"] .cfg-toggle-row:hover:not(.disabled) { + background: rgba(255, 255, 255, 0.8); +} + +.cfg-toggle-row__content { flex: 1; min-width: 0; } -.field-row__label { +.cfg-toggle-row__label { display: block; font-size: 14px; font-weight: 500; color: var(--text); } -.field-row__help { +.cfg-toggle-row__help { display: block; margin-top: 2px; font-size: 12px; @@ -548,89 +793,411 @@ line-height: 1.4; } -/* Pills */ -.pill--sm { +/* Toggle Switch */ +.cfg-toggle { + position: relative; + flex-shrink: 0; +} + +.cfg-toggle input { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +.cfg-toggle__track { + display: block; + width: 48px; + height: 28px; + background: rgba(255, 255, 255, 0.12); + border: 1px solid var(--border); + border-radius: 999px; + position: relative; + transition: background 200ms ease, border-color 200ms ease; +} + +:root[data-theme="light"] .cfg-toggle__track { + background: rgba(0, 0, 0, 0.1); +} + +.cfg-toggle__track::after { + content: ""; + position: absolute; + top: 3px; + left: 3px; + width: 20px; + height: 20px; + background: var(--text); + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + transition: transform 200ms ease, background 200ms ease; +} + +.cfg-toggle input:checked + .cfg-toggle__track { + background: rgba(43, 217, 127, 0.25); + border-color: rgba(43, 217, 127, 0.5); +} + +.cfg-toggle input:checked + .cfg-toggle__track::after { + transform: translateX(20px); + background: var(--ok); +} + +.cfg-toggle input:focus + .cfg-toggle__track { + box-shadow: 0 0 0 3px var(--focus); +} + +/* Object (collapsible) */ +.cfg-object { + border: 1px solid var(--border); + border-radius: 10px; + background: rgba(0, 0, 0, 0.04); + overflow: hidden; +} + +:root[data-theme="light"] .cfg-object { + background: rgba(255, 255, 255, 0.4); +} + +.cfg-object__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + cursor: pointer; + list-style: none; + transition: background 150ms ease; +} + +.cfg-object__header:hover { + background: rgba(255, 255, 255, 0.03); +} + +:root[data-theme="light"] .cfg-object__header:hover { + background: rgba(0, 0, 0, 0.02); +} + +.cfg-object__header::-webkit-details-marker { + display: none; +} + +.cfg-object__title { + font-size: 14px; + font-weight: 600; + color: var(--text); +} + +.cfg-object__chevron { + width: 18px; + height: 18px; + color: var(--muted); + transition: transform 200ms ease; +} + +.cfg-object__chevron svg { + width: 100%; + height: 100%; +} + +.cfg-object[open] .cfg-object__chevron { + transform: rotate(180deg); +} + +.cfg-object__help { + padding: 0 16px 12px; + font-size: 12px; + color: var(--muted); + border-bottom: 1px solid var(--border); +} + +.cfg-object__content { + padding: 16px; + display: grid; + gap: 16px; +} + +/* Array */ +.cfg-array { + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; +} + +.cfg-array__header { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: rgba(0, 0, 0, 0.06); + border-bottom: 1px solid var(--border); +} + +:root[data-theme="light"] .cfg-array__header { + background: rgba(0, 0, 0, 0.02); +} + +.cfg-array__label { + flex: 1; + font-size: 14px; + font-weight: 600; + color: var(--text); +} + +.cfg-array__count { + font-size: 12px; + color: var(--muted); padding: 3px 8px; - font-size: 10px; + background: rgba(255, 255, 255, 0.06); + border-radius: 999px; +} + +:root[data-theme="light"] .cfg-array__count { + background: rgba(0, 0, 0, 0.06); +} + +.cfg-array__add { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border: 1px solid var(--border); + border-radius: 6px; + background: rgba(255, 255, 255, 0.05); + color: var(--text); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: background 150ms ease; +} + +.cfg-array__add:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.1); +} + +.cfg-array__add:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.cfg-array__add-icon { + width: 14px; + height: 14px; +} + +.cfg-array__add-icon svg { + width: 100%; + height: 100%; +} + +.cfg-array__help { + padding: 10px 16px; + font-size: 12px; + color: var(--muted); + border-bottom: 1px solid var(--border); +} + +.cfg-array__empty { + padding: 32px 16px; + text-align: center; + color: var(--muted); + font-size: 13px; +} + +.cfg-array__items { + display: grid; + gap: 1px; + background: var(--border); +} + +.cfg-array__item { + background: var(--panel); +} + +.cfg-array__item-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + background: rgba(0, 0, 0, 0.04); + border-bottom: 1px solid var(--border); +} + +:root[data-theme="light"] .cfg-array__item-header { + background: rgba(0, 0, 0, 0.02); +} + +.cfg-array__item-index { + font-size: 11px; + font-weight: 600; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.cfg-array__item-remove { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border: none; + border-radius: 6px; + background: transparent; + color: var(--muted); + cursor: pointer; + transition: background 150ms ease, color 150ms ease; +} + +.cfg-array__item-remove svg { + width: 16px; + height: 16px; +} + +.cfg-array__item-remove:hover:not(:disabled) { + background: rgba(255, 92, 92, 0.15); + color: var(--danger); +} + +.cfg-array__item-remove:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.cfg-array__item-content { + padding: 16px; +} + +/* Map (custom entries) */ +.cfg-map { + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; +} + +.cfg-map__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 16px; + background: rgba(0, 0, 0, 0.06); + border-bottom: 1px solid var(--border); +} + +:root[data-theme="light"] .cfg-map__header { + background: rgba(0, 0, 0, 0.02); +} + +.cfg-map__label { + font-size: 13px; + font-weight: 600; + color: var(--muted); +} + +.cfg-map__add { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border: 1px solid var(--border); + border-radius: 6px; + background: rgba(255, 255, 255, 0.05); + color: var(--text); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: background 150ms ease; +} + +.cfg-map__add:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.1); +} + +.cfg-map__add-icon { + width: 14px; + height: 14px; +} + +.cfg-map__add-icon svg { + width: 100%; + height: 100%; +} + +.cfg-map__empty { + padding: 24px 16px; + text-align: center; + color: var(--muted); + font-size: 13px; +} + +.cfg-map__items { + display: grid; + gap: 8px; + padding: 12px; +} + +.cfg-map__item { + display: grid; + grid-template-columns: 140px 1fr auto; + gap: 8px; + align-items: start; +} + +.cfg-map__item-key { + min-width: 0; +} + +.cfg-map__item-value { + min-width: 0; +} + +.cfg-map__item-remove { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: none; + border-radius: 6px; + background: transparent; + color: var(--muted); + cursor: pointer; + transition: background 150ms ease, color 150ms ease; +} + +.cfg-map__item-remove svg { + width: 16px; + height: 16px; +} + +.cfg-map__item-remove:hover:not(:disabled) { + background: rgba(255, 92, 92, 0.15); + color: var(--danger); +} + +/* Pill variants */ +.pill--sm { + padding: 4px 10px; + font-size: 11px; } .pill--ok { - border-color: rgba(43, 217, 127, 0.5); + border-color: rgba(43, 217, 127, 0.4); color: var(--ok); } .pill--danger { - border-color: rgba(255, 92, 92, 0.5); + border-color: rgba(255, 92, 92, 0.4); color: var(--danger); } -/* Improved Fieldset */ -.config-form--modern .fieldset { - border: 1px solid var(--border); - border-radius: 12px; - padding: 14px; - margin-top: 12px; - background: rgba(0, 0, 0, 0.08); -} - -:root[data-theme="light"] .config-form--modern .fieldset { - background: rgba(255, 255, 255, 0.5); -} - -.config-form--modern .fieldset > .legend { - font-weight: 600; - font-size: 13px; - color: var(--text); - margin-bottom: 12px; - padding-bottom: 10px; - border-bottom: 1px dashed var(--border); -} - -.config-form--modern .field { - margin-bottom: 12px; -} - -.config-form--modern .field:last-child { - margin-bottom: 0; -} - -.config-form--modern .field span { - font-size: 12px; - font-weight: 500; - color: var(--muted); -} - -.config-form--modern .field input, -.config-form--modern .field select, -.config-form--modern .field textarea { - font-size: 14px; -} - -/* Array improvements */ -.config-form--modern .array { - display: grid; - gap: 10px; -} - -.config-form--modern .array-item { - display: flex; - gap: 12px; - align-items: flex-start; - padding: 12px; - border: 1px solid var(--border); - border-radius: 10px; - background: rgba(0, 0, 0, 0.1); -} - -:root[data-theme="light"] .config-form--modern .array-item { - background: rgba(255, 255, 255, 0.6); -} - -/* Mobile Responsiveness */ +/* =========================================== + Mobile Responsiveness + =========================================== */ @media (max-width: 768px) { .config-layout { grid-template-columns: 1fr; - min-height: auto; } .config-sidebar { @@ -638,22 +1205,27 @@ border-bottom: 1px solid var(--border); } + .config-sidebar__header { + padding: 12px 16px; + } + .config-nav { display: flex; - flex-wrap: wrap; - gap: 6px; - padding: 12px; - max-height: none; - overflow: visible; + flex-wrap: nowrap; + gap: 4px; + padding: 8px 12px; + overflow-x: auto; + -webkit-overflow-scrolling: touch; } .config-nav__item { flex: 0 0 auto; padding: 8px 12px; + white-space: nowrap; } - .config-nav__icon { - width: auto; + .config-nav__label { + display: inline; } .config-sidebar__footer { @@ -662,47 +1234,67 @@ .config-actions { flex-wrap: wrap; + padding: 12px 16px; } - .config-actions__left { - width: 100%; - justify-content: center; - } - + .config-actions__left, .config-actions__right { width: 100%; justify-content: center; } - .config-diff__values { - flex-wrap: wrap; + .config-content { + padding: 16px; } - .field-row { - flex-direction: column; - align-items: flex-start; - gap: 10px; + .config-section-card__header { + padding: 14px 16px; } - .config-form--modern .array-item { - flex-direction: column; + .config-section-card__content { + padding: 16px; + } + + .cfg-toggle-row { + padding: 12px 14px; + } + + .cfg-map__item { + grid-template-columns: 1fr; + gap: 8px; + } + + .cfg-map__item-remove { + justify-self: end; } } @media (max-width: 480px) { + .config-nav__icon { + width: 24px; + height: 24px; + font-size: 16px; + } + .config-nav__label { display: none; } - .config-nav__item { - padding: 10px; + .config-section-card__icon { + width: 28px; + height: 28px; } - .config-section-card__header { - padding: 12px; + .config-section-card__title { + font-size: 15px; } - .config-section-card__content { - padding: 12px; + .cfg-segmented { + flex-wrap: wrap; + } + + .cfg-segmented__btn { + flex: 1 0 auto; + min-width: 60px; } } diff --git a/ui/src/ui/views/config-form.node.ts b/ui/src/ui/views/config-form.node.ts index 76a6e6195..6024c2407 100644 --- a/ui/src/ui/views/config-form.node.ts +++ b/ui/src/ui/views/config-form.node.ts @@ -26,6 +26,15 @@ function jsonValue(value: unknown): string { } } +// SVG Icons as template literals +const icons = { + chevronDown: html``, + plus: html``, + minus: html``, + trash: html``, + edit: html``, +}; + export function renderNode(params: { schema: JsonSchema; value: unknown; @@ -44,27 +53,29 @@ export function renderNode(params: { const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1))); const help = hint?.help ?? schema.description; const key = pathKey(path); + const hasDefault = schema.default !== undefined; + const isDefault = hasDefault && JSON.stringify(value) === JSON.stringify(schema.default); + const isEmpty = value === undefined || value === null || value === ""; if (unsupported.has(key)) { - return html`
- ${label}: unsupported schema node. Use Raw. + return html`
+
${label}
+
Unsupported schema node. Use Raw mode.
`; } + // Handle anyOf/oneOf unions if (schema.anyOf || schema.oneOf) { const variants = schema.anyOf ?? schema.oneOf ?? []; const nonNull = variants.filter( - (v) => - !( - v.type === "null" || - (Array.isArray(v.type) && v.type.includes("null")) - ), + (v) => !(v.type === "null" || (Array.isArray(v.type) && v.type.includes("null"))) ); if (nonNull.length === 1) { return renderNode({ ...params, schema: nonNull[0] }); } + // Check if it's a set of literal values (enum-like) const extractLiteral = (v: JsonSchema): unknown | undefined => { if (v.const !== undefined) return v.const; if (v.enum && v.enum.length === 1) return v.enum[0]; @@ -73,137 +84,314 @@ export function renderNode(params: { const literals = nonNull.map(extractLiteral); const allLiterals = literals.every((v) => v !== undefined); - if (allLiterals && literals.length > 0) { + if (allLiterals && literals.length > 0 && literals.length <= 5) { + // Use segmented control for small sets const resolvedValue = value ?? schema.default; - const currentIndex = literals.findIndex( - (lit) => - lit === resolvedValue || String(lit) === String(resolvedValue), - ); return html` - +
+ ${showLabel ? html`` : nothing} + ${help ? html`
${help}
` : nothing} +
+ ${literals.map((lit, idx) => html` + + `)} +
+
`; } + if (allLiterals && literals.length > 5) { + // Use dropdown for larger sets + return renderSelect({ ...params, options: literals.map(String), value: String(value ?? schema.default ?? "") }); + } + + // Handle mixed primitive types const primitiveTypes = new Set( - nonNull - .map((variant) => schemaType(variant)) - .filter((variant): variant is string => Boolean(variant)), + nonNull.map((variant) => schemaType(variant)).filter(Boolean) ); const normalizedTypes = new Set( - [...primitiveTypes].map((variant) => (variant === "integer" ? "number" : variant)), - ); - const primitiveOnly = [...normalizedTypes].every((variant) => - ["string", "number", "boolean"].includes(variant), + [...primitiveTypes].map((v) => (v === "integer" ? "number" : v)) ); - if (primitiveOnly && normalizedTypes.size > 0) { + if ([...normalizedTypes].every((v) => ["string", "number", "boolean"].includes(v as string))) { const hasString = normalizedTypes.has("string"); const hasNumber = normalizedTypes.has("number"); - const hasBoolean = normalizedTypes.has("boolean"); - - if (hasBoolean && normalizedTypes.size === 1) { - return renderNode({ - ...params, - schema: { ...schema, type: "boolean", anyOf: undefined, oneOf: undefined }, - }); - } - + if (hasString || hasNumber) { - const displayValue = value ?? schema.default ?? ""; - return html` - - `; + return renderTextInput({ + ...params, + inputType: hasNumber && !hasString ? "number" : "text", + }); } } } + // Enum - use segmented for small, dropdown for large if (schema.enum) { const options = schema.enum; - const resolvedValue = value ?? schema.default; - const currentIndex = options.findIndex( - (opt) => - opt === resolvedValue || String(opt) === String(resolvedValue), - ); - const unset = "__unset__"; + if (options.length <= 5) { + const resolvedValue = value ?? schema.default; + return html` +
+ ${showLabel ? html`` : nothing} + ${help ? html`
${help}
` : nothing} +
+ ${options.map((opt) => html` + + `)} +
+
+ `; + } + return renderSelect({ ...params, options: options.map(String), value: String(value ?? schema.default ?? "") }); + } + + // Object type - collapsible section + if (type === "object") { + return renderObject(params); + } + + // Array type + if (type === "array") { + return renderArray(params); + } + + // Boolean - toggle row + if (type === "boolean") { + const displayValue = typeof value === "boolean" ? value : typeof schema.default === "boolean" ? schema.default : false; return html` -