@mxml3gend/gloss 0.1.2 → 0.1.3

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.
@@ -0,0 +1 @@
1
+ :root{--text-primary: #0f2343;--text-secondary: #3f5478;--text-muted: #5b7196;--surface-card: #ffffff;--surface-border: #d6e0f0;font-family:Manrope,Avenir Next,Segoe UI,sans-serif;line-height:1.45;font-weight:500;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,*:before,*:after{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh;background:radial-gradient(circle at 8% 0%,rgba(255,186,132,.26),transparent 42%),radial-gradient(circle at 90% 18%,rgba(120,184,255,.28),transparent 40%),linear-gradient(180deg,#f3f7ff,#f8f1e8)}#root{width:100%}.gloss-app{--space-1: .5rem;--space-2: .75rem;--space-3: 1rem;--space-4: 1.5rem;--space-5: 2rem;--state-danger-text: #842a34;--state-danger-border: #edc7cc;--state-danger-bg: #fff5f6;--state-warning-text: #75511f;--state-warning-border: #ead5ab;--state-warning-bg: #fff9ef;--state-info-text: #2a4f85;--state-info-border: #c7d8f6;--state-info-bg: #eef4ff;--state-success-text: #1f6943;--state-success-border: #b9e0c8;--state-success-bg: #eef8f2;--state-missing-row-bg: #fffaf8;--state-partial-row-bg: #fffdf7;--state-highlight-row-bg: #f2f7ff;--state-missing-cell-bg: #fffdf9;--state-dirty-cell-bg: #f2f7ff;--state-dirty-missing-cell-bg: #fff9f0;--state-unused-bg: #f8f9fc;max-width:1360px;margin:0 auto;padding:var(--space-4) var(--space-3) calc(var(--space-5) + var(--space-2));color:var(--text-primary)}.workspace-shell{margin-top:var(--space-3);display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--space-2);align-items:start}.action-sidebar{position:sticky;top:calc(var(--space-4) + .25rem);border:1px solid #d7e2f5;border-radius:14px;background:linear-gradient(180deg,#f9fbff,#f3f7ff);padding:.5rem;display:grid;gap:.55rem;box-shadow:0 8px 18px #1f3a6914}.action-sidebar__group{display:grid;gap:.4rem}.action-sidebar__btn{width:2.35rem;height:2.35rem;border:1px solid #c7d6ee;border-radius:10px;background:#fff;color:#274d88;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background .12s ease,border-color .12s ease,transform .12s ease}.action-sidebar__btn svg{width:1.1rem;height:1.1rem}.action-sidebar__btn:hover{background:#edf4ff;border-color:#88a8db;transform:translateY(-.5px)}.action-sidebar__btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.action-sidebar__btn.is-active{background:#e7efff;border-color:#5b7fc5;color:#1e3f75}.action-sidebar__btn--saving{animation:sidebar-pulse 1.1s ease-in-out infinite}@keyframes sidebar-pulse{0%{box-shadow:0 0 #2957b13d}70%{box-shadow:0 0 0 8px #2957b100}to{box-shadow:0 0 #2957b100}}.app-header{position:sticky;top:0;z-index:20;padding:var(--space-3);border-radius:16px;border:1px solid #d9e3f6;background:linear-gradient(140deg,#fdfefff7,#f8fafffa 55%,#f4f8fffa);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);box-shadow:0 10px 24px #16264612}.app-header__main{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:var(--space-3);align-items:center}.app-header__brand{min-width:0}.app-header__logo{display:block;width:min(380px,100%);height:auto}.app-header__controls{display:grid;justify-items:end;gap:var(--space-2)}.app-header__stats{display:flex;flex-wrap:wrap;gap:var(--space-1);justify-self:center;align-items:center;margin-top:0}.stat-chip{display:flex;align-items:baseline;gap:.45rem;padding:.4rem .68rem;border-radius:999px;background:#fffffff5;border:1px solid rgba(21,37,66,.09)}.stat-chip span{font-size:.75rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em}.stat-chip strong{font-size:1rem}.language-switch{display:inline-flex;align-items:center;gap:.45rem;padding:.32rem .4rem;border-radius:999px;border:1px solid var(--surface-border);background:#fffffff0}.support-cards{margin-top:var(--space-2);display:grid;gap:var(--space-1);grid-template-columns:repeat(auto-fit,minmax(240px,1fr));opacity:.88}.support-card{border-radius:12px;border:1px solid #e2e9f7;background:#fbfcff;padding:var(--space-2) var(--space-3)}.support-card h2{margin:0;font-size:.95rem}.support-card p{margin:var(--space-1) 0 0;color:var(--text-secondary);font-size:.88rem;line-height:1.45}.editor-shell{margin-top:0;border-radius:18px;border:1px solid #dbe4f5;background:#fdfefe;padding:var(--space-3);box-shadow:0 16px 30px #0f234c12;display:grid;gap:var(--space-2)}.status-bar{margin:0;border-radius:12px;border:1px solid #dbe5f7;background:#f8fbff;padding:.625rem .75rem;display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);flex-wrap:wrap}.status-bar__main{margin:0;font-weight:600;font-size:.9rem}.status-bar__main--error{color:var(--state-danger-text)}.status-bar__main--warning{color:var(--state-warning-text);display:flex;align-items:center;gap:var(--space-1);flex-wrap:wrap}.status-bar__main--success{color:var(--state-success-text)}.status-bar__main--info{color:var(--text-secondary)}.status-bar__meta{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.status-chip{display:inline-flex;align-items:center;gap:.35rem;border-radius:999px;border:1px solid #cad8f2;background:#f0f5ff;color:#29476f;font-size:.76rem;font-weight:600;padding:.18rem .5rem}.status-chip[type=button]{cursor:pointer}.status-chip--muted{background:#f6f8fc;border-color:#dde4f2;color:#4d6285}.status-chip--warning{background:var(--state-warning-bg);border-color:var(--state-warning-border);color:var(--state-warning-text)}.status-chip--info{background:var(--state-info-bg);border-color:var(--state-info-border);color:var(--state-info-text)}.status-chip__action{opacity:.85;font-size:.72rem;border-left:1px solid currentColor;padding-left:.4rem}.status-bar__details{width:100%;border:1px solid #e0e8f7;border-radius:10px;background:#fff;padding:.6rem .75rem}.status-bar__details strong{display:block;margin-bottom:.45rem;color:#2f4b75;font-size:.82rem}.status-bar__details p{margin:0;color:var(--text-secondary);font-size:.84rem}.status-bar__details ul{margin:0;padding-left:1rem;display:grid;gap:.3rem}.status-bar__details li{color:#35527c;font-size:.82rem;line-height:1.35}.loading-state{margin:20vh auto 0;max-width:280px;border-radius:12px;border:1px solid #d5e2fb;background:#f7faff;padding:var(--space-3);text-align:center;color:var(--text-secondary)}.notice{margin:0 0 .7rem;padding:.6rem .75rem;border-radius:10px;border:1px solid transparent}.notice--error{color:var(--state-danger-text);background:var(--state-danger-bg);border-color:var(--state-danger-border)}.notice--success{color:var(--state-success-text);background:var(--state-success-bg);border-color:var(--state-success-border)}.notice--warning{color:var(--state-warning-text);background:var(--state-warning-bg);border-color:var(--state-warning-border)}.notice--stale{color:var(--state-warning-text);background:#fff8f0;border-color:#ecd8b6;display:flex;align-items:center;gap:.6rem;flex-wrap:wrap}.editor-tabs{display:flex;gap:var(--space-1);margin:0;padding:.25rem;border:1px solid #e2e9f6;border-radius:11px;background:#f7faff}.translations-workspace{display:grid;gap:var(--space-2)}.workspace-content-shell{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--space-2);align-items:start}.workspace-content-shell--with-drawer{grid-template-columns:minmax(0,1fr) minmax(320px,420px)}.editor-controls{display:grid;gap:var(--space-1);padding:var(--space-1);border-radius:14px;border:1px solid #e1e8f6;background:linear-gradient(180deg,#f9fbff,#f6f9ff)}.toolbar{display:grid;gap:.55rem;margin:0;padding:var(--space-2);border:1px solid #e4ebf8;border-radius:12px;background:#fff}.toolbar__top{display:grid;grid-template-columns:auto minmax(280px,1fr) auto;align-items:center;gap:.55rem}.toolbar__mode-switch{display:inline-flex;align-items:center;gap:.4rem}.toolbar__mode-group{display:grid;gap:.28rem}.toolbar__mode-hint{margin:0;color:#5e7395;font-size:.76rem;line-height:1.3}.toolbar__dsl-hint{margin-top:.05rem;font-size:.74rem;color:#61789e}.toolbar__field{display:grid;gap:.32rem;min-width:160px}.toolbar__field--search{min-width:0}.toolbar__field--search input{width:100%;min-height:2.3rem}.toolbar__field span{font-size:.75rem;font-weight:600;color:#5f7394;text-transform:uppercase;letter-spacing:.06em}.toolbar__actions{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:.45rem}.toolbar__translate-progress{margin:0;font-size:.82rem;color:#506687}.toolbar__chips{display:flex;align-items:center;flex-wrap:wrap;gap:.45rem}.toolbar__token-list{display:flex;flex-wrap:wrap;gap:.4rem}.toolbar__token{display:inline-flex;align-items:center;gap:.35rem;border:1px solid #c8d8f2;border-radius:999px;background:#fff;color:#2b476f;padding:.22rem .52rem;font-size:.74rem;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;cursor:pointer}.toolbar__token:hover{background:#f4f8ff;border-color:#9eb8e6}.toolbar__dsl-toggle{border:1px solid #d1ddf3;border-radius:999px;background:#f7faff;color:#36537e;font-size:.72rem;font-weight:700;letter-spacing:.03em;padding:.28rem .52rem;cursor:help}.filter-chip{border:1px solid #d5e1f5;border-radius:999px;background:#f7faff;color:#274368;font-size:.78rem;font-weight:600;padding:.3rem .58rem;cursor:pointer}.filter-chip.is-active{border-color:#87a9e3;background:#e8f0ff;color:#1d3f79}.filter-chip:disabled{opacity:.58;cursor:not-allowed}.filter-chip--clear{background:#fff;color:#516b93;border-color:#c8d8f2}.toolbar__advanced{width:100%;border:1px solid #dbe5f7;border-radius:12px;background:#f8fbff;padding:.7rem;display:grid;gap:.6rem}.toolbar__advanced-top{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.5rem}.toolbar__advanced-head{display:flex;align-items:center;justify-content:space-between;gap:.6rem;flex-wrap:wrap}.toolbar__advanced-head strong{font-size:.84rem;color:#2f4b75}.toolbar__advanced-actions{display:flex;gap:.4rem;flex-wrap:wrap}.toolbar__advanced-empty{margin:0;color:var(--text-secondary);font-size:.84rem}.toolbar__git-warning{margin:0;font-size:.82rem;color:#8f3c45}.toolbar__rules{display:grid;gap:.45rem}.toolbar__rule{display:grid;grid-template-columns:minmax(140px,1fr) minmax(140px,1fr) minmax(140px,1fr) auto;gap:.45rem;align-items:end;padding:.55rem;border:1px solid #d9e4f8;border-radius:10px;background:#fff}.toolbar__sort{display:flex;align-items:end;gap:.45rem;flex-wrap:wrap;padding-top:.25rem;border-top:1px dashed #d4dff4}.add-key-form{display:flex;align-items:flex-end;gap:var(--space-1);flex-wrap:wrap;margin:0;padding:var(--space-2);border:1px solid #e4ebf8;border-radius:12px;background:#fff}.add-key-form input{min-width:260px;flex:1 1 280px}.add-key-form__error{margin:0;color:#8f3c45;font-size:.82rem;padding:0 .125rem}.editor-main{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--space-2);align-items:start}.editor-main--translate{grid-template-columns:minmax(0,1fr)}.editor-explorers{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-2)}.editor-content{min-width:0;border:1px solid #dfe8f6;border-radius:15px;background:#fcfdff;padding:var(--space-2);box-shadow:inset 0 1px #fff}.analysis-drawer{min-width:0;max-height:74vh;border:1px solid #d9e5f7;border-radius:14px;background:#f8fbff;padding:.6rem;display:grid;grid-template-rows:auto minmax(0,1fr);gap:.55rem;overflow:hidden}.analysis-drawer__header{display:flex;align-items:center;justify-content:space-between;gap:.6rem}.analysis-drawer__title{margin:0;font-size:.82rem;color:var(--text-muted);font-weight:700;letter-spacing:.05em;text-transform:uppercase}.analysis-drawer__body{min-height:0;overflow:auto}.analysis-drawer .issues-panel,.analysis-drawer .duplicates-panel,.analysis-drawer .usage-details-panel{border:0;border-radius:0;background:transparent;padding:0}.usage-details-panel{display:grid;gap:.75rem}.usage-details-panel__meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:.5rem;margin:0}.usage-details-panel__meta div{border:1px solid #dbe5f7;border-radius:10px;background:#fff;padding:.5rem .6rem}.usage-details-panel__meta dt{margin:0;font-size:.72rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--text-muted)}.usage-details-panel__meta dd{margin:.18rem 0 0;color:var(--text-primary);overflow-wrap:anywhere;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.78rem}.usage-details-panel__section{border:1px solid #dce7f9;border-radius:10px;background:#fff;padding:.55rem .65rem}.usage-details-panel__title{margin:0;color:#365179;font-size:.78rem;font-weight:700}.usage-details-panel__empty{margin:.5rem 0 0;color:var(--text-secondary);font-size:.84rem}.file-tree,.namespace-tree{border:1px solid #d8e3f4;border-radius:14px;background:linear-gradient(180deg,#f8faff,#f2f7ff);padding:var(--space-2);max-height:32vh;overflow:auto;box-shadow:inset 0 1px #fff}.namespace-tree__title{margin:0 0 var(--space-1);font-size:.78rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);position:sticky;top:-.5rem;z-index:2;background:linear-gradient(180deg,#f8faff,#f8fafff5);padding:.45rem 0 .35rem}.namespace-tree__all-btn,.namespace-tree__node{width:100%;text-align:left;border:1px solid transparent;border-radius:9px;background:transparent;color:var(--text-primary);font:inherit;cursor:pointer}.namespace-tree__all-btn{padding:.42rem .54rem}.namespace-tree__row{display:grid;grid-template-columns:auto minmax(0,1fr);gap:.3rem;align-items:center}.namespace-tree__toggle{border:1px solid transparent;background:transparent;color:#50688f;cursor:pointer;border-radius:7px;width:1.4rem;height:1.4rem;padding:0;display:inline-flex;align-items:center;justify-content:center}.namespace-tree__node{display:flex;justify-content:space-between;align-items:center;gap:.45rem;padding:.34rem .46rem}.namespace-tree__name{overflow:hidden;text-overflow:ellipsis}.namespace-tree__meta{color:var(--text-muted);font-size:.7rem;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace}.namespace-tree__all-btn:hover,.namespace-tree__node:hover,.namespace-tree__toggle:hover{background:#e9f1ff}.namespace-tree__all-btn.is-selected,.namespace-tree__node.is-selected{border-color:#8caadd;background:#e4edff;box-shadow:inset 3px 0 #466fcf}.namespace-tree__list{list-style:none;margin:var(--space-1) 0 0;padding:0;display:grid;gap:.1rem}.namespace-tree__item{margin:0}.namespace-tree__empty{margin:.6rem 0 0;padding:.55rem .62rem;border:1px dashed #d8e4f6;border-radius:10px;background:#f8fbff;color:var(--text-secondary);font-size:.84rem;line-height:1.42}.file-tree__title{margin:0 0 var(--space-1);font-size:.78rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);position:sticky;top:-.5rem;z-index:2;background:linear-gradient(180deg,#f8faff,#f8fafff5);padding:.45rem 0 .35rem}.file-tree__all-btn,.file-tree__folder-btn,.file-tree__file-btn{width:100%;text-align:left;border:1px solid transparent;border-radius:9px;background:transparent;padding:.42rem .54rem;color:var(--text-primary);font:inherit;cursor:pointer}.file-tree__all-btn,.file-tree__file-btn{display:flex;align-items:center;justify-content:space-between;gap:.5rem}.file-tree__folder-btn{display:inline-flex;align-items:center;gap:.38rem;color:#26456f;font-weight:600}.file-tree__caret{width:.8rem;color:#50688f}.file-tree__folder-name{overflow:hidden;text-overflow:ellipsis}.file-tree__all-btn:hover,.file-tree__folder-btn:hover,.file-tree__file-btn:hover{background:#e9f1ff}.file-tree__all-btn.is-selected,.file-tree__file-btn.is-selected{border-color:#8caadd;background:#e4edff;box-shadow:inset 3px 0 #466fcf}.file-tree__file-name{overflow:hidden;text-overflow:ellipsis}.file-tree__file-count{color:var(--text-muted);font-size:.72rem;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;background:#edf2fd;border:1px solid #d4e0f5;border-radius:999px;padding:.1rem .38rem}.file-tree__list{list-style:none;margin:var(--space-1) 0 0;padding:0;display:grid;gap:.1rem}.file-tree__item{margin:0}.file-tree__empty{margin:.6rem 0 0;padding:.55rem .62rem;border:1px dashed #d8e4f6;border-radius:10px;background:#f8fbff;color:var(--text-secondary);font-size:.84rem;line-height:1.42}.issues-panel{border:1px solid var(--surface-border);border-radius:12px;background:#f8fafe;padding:.75rem;display:grid;gap:.75rem}.issues-panel__header{display:flex;align-items:center;justify-content:space-between;gap:.65rem;flex-wrap:wrap}.issues-panel__actions{display:flex;flex-wrap:wrap;gap:.4rem}.issues-panel__title{margin:0;font-size:.95rem;font-weight:700}.issues-panel__count{color:var(--text-muted);font-size:.8rem;font-weight:600}.issues-panel__list{list-style:none;margin:0;padding:0;display:grid;gap:.5rem}.issue-row{display:grid;grid-template-columns:auto minmax(220px,max-content) minmax(0,1fr) auto;align-items:center;gap:.55rem;border:1px solid #e1e8f7;border-radius:10px;background:#fff;padding:.5rem .6rem}.issue-row__badge{display:inline-flex;align-items:center;border-radius:999px;border:1px solid #d0dcf3;background:#f5f8ff;color:#32527e;font-size:.7rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:.15rem .45rem}.issue-row__key{border:0;background:transparent;color:#1c4b98;font:inherit;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;text-align:left;padding:0;cursor:pointer;text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}.issue-row__key--passive{color:#3f5578;text-decoration:none;cursor:default}.issue-row__meta{color:var(--text-secondary);font-size:.82rem;overflow-wrap:anywhere}.issue-row__actions{display:inline-flex;justify-content:flex-end;align-items:center;gap:.35rem;flex-wrap:wrap}.issue-row--missing{border-color:#eadbb8;background:#fffbf4}.issue-row--placeholder_mismatch{border-color:#ecd8b4;background:#fffaf3}.issue-row--invalid_key{border-color:#efcfd4;background:#fff8f9}.issue-row--unused{border-color:#dde4f2;background:var(--state-unused-bg)}.issue-row--hardcoded_text{border-color:#ead7c2;background:#fffaf6}.duplicates-panel{border:1px solid var(--surface-border);border-radius:12px;background:#f8fafe;padding:.7rem}.duplicates-panel__title{margin:0 0 .65rem;font-size:.95rem;font-weight:700}.duplicates-panel__list{display:grid;gap:.75rem}.duplicate-locale{border:1px solid #d7e2f7;border-radius:10px;background:#fff;padding:.55rem}.duplicate-locale__title{margin:0 0 .45rem;font-size:.78rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted)}.duplicate-group{border:1px solid #e3ebfb;border-radius:9px;padding:.5rem;margin-top:.45rem}.duplicate-group__top{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:.5rem;align-items:center}.duplicate-group__value{overflow-wrap:anywhere}.duplicate-group__count{font-size:.78rem;color:var(--text-muted)}.duplicate-group__keys{margin:.45rem 0 0;padding-left:1.2rem;display:grid;gap:.2rem;color:var(--text-secondary);font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.78rem}.table-wrap{border-radius:12px;border:1px solid #d8e3f4;background:#fff;overflow:auto;max-height:76vh}.grid-table{width:100%;min-width:1080px;border-collapse:collapse}.grid-table th{text-align:left;padding:.62rem .66rem;border-bottom:1px solid #d9e3f5;background:#f7faff;position:sticky;top:0;z-index:2;color:#3a557f;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em}.grid-table td{padding:.56rem .6rem;border-bottom:1px solid #edf1f8;vertical-align:top}.grid-table tr.row-state--none>td{background:var(--state-missing-row-bg)}.grid-table tr.row-state--partial>td{background:var(--state-partial-row-bg)}.grid-table tr.row-state--highlighted>td{background:var(--state-highlight-row-bg)}.grid-table tr.row-state--none td.value-cell--dirty,.grid-table tr.row-state--partial td.value-cell--dirty{background:var(--state-dirty-cell-bg)}.grid-table tr.row-state--none td.value-cell--dirty-missing,.grid-table tr.row-state--partial td.value-cell--dirty-missing{background:var(--state-dirty-missing-cell-bg)}.grid-table tbody tr:not(.usage-files-row):not(.virtual-spacer):not(.namespace-group-row):hover>td{background-color:#f8fbff}.namespace-group-row td{padding:0;border-bottom:1px solid #dfe8f6;background:#f7faff}.namespace-group-toggle{width:100%;border:0;background:transparent;padding:.56rem .66rem;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:.45rem;color:#2e4f84;font:inherit;font-weight:600;cursor:pointer;text-align:left}.namespace-group-toggle:hover{background:#edf4ff}.namespace-group-toggle__caret{color:#4d6b9d;width:.9rem}.namespace-group-toggle__label{font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.82rem}.namespace-group-toggle__count{color:var(--text-muted);font-size:.74rem;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace}.key-col{width:300px;min-width:300px;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.8rem;color:#1f3659;border-right:1px solid #d7e2f4;background-clip:padding-box}.key-col__label{display:inline;overflow-wrap:anywhere}.key-col__dirty-dot{margin-left:.25rem;color:#2a63c7;font-weight:700}.key-col--translate{border-right:1px solid #dce6f7}.key-col--unused{color:#677186}.key-col--file-selected{box-shadow:inset 2px 0 #4a74d9}.key-col--placeholder-warning{box-shadow:inset 0 -2px #d39a3f}.usage-col{width:106px;min-width:106px}.usage-cell{color:var(--text-secondary);white-space:nowrap;text-align:center}.usage-cell--unused{background:var(--state-unused-bg)}.usage-toggle{border:0;background:none;padding:0;font:inherit;color:#1848a3;cursor:pointer;text-decoration:underline;text-underline-offset:2px;font-weight:600}.usage-tag{font-size:.7rem;font-weight:600;color:#667289;text-transform:uppercase;letter-spacing:.03em}.usage-files-row td{background:#f8fbff;padding-top:.7rem;padding-bottom:.8rem}.virtual-spacer td{border-bottom:0;padding:0}.usage-files{font-size:.82rem;color:var(--text-secondary);border:1px solid #dde6f6;border-radius:10px;background:#fff;padding:.6rem .7rem}.usage-files strong{margin-right:.45rem;color:var(--text-primary)}.usage-files-list{margin:.45rem 0 0;padding-left:1.2rem;display:grid;gap:.26rem;color:var(--text-primary);font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.78rem}.key-diff-block{margin-top:.7rem;padding-top:.55rem;border-top:1px dashed #d7e3f8}.key-diff-line{overflow-wrap:anywhere}.status-col{width:168px;min-width:168px;color:#4a5f80;font-size:.79rem;line-height:1.35}.status-col__summary{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap}.status-col__changed{color:#2f5d9b;font-weight:600}.status-col__tags{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.2rem}.status-inline-tag{display:inline-flex;align-items:center;border-radius:999px;padding:.1rem .42rem;font-size:.7rem;font-weight:700;letter-spacing:.03em}.status-inline-tag--warning{color:var(--state-warning-text);background:var(--state-warning-bg);border:1px solid var(--state-warning-border)}.status-inline-tag--info{color:var(--state-info-text);background:var(--state-info-bg);border:1px solid var(--state-info-border)}.locale-col,.locale-cell{min-width:260px}.value-cell{background:transparent}.value-cell--missing{background:var(--state-missing-cell-bg)}.value-cell--dirty{background:var(--state-dirty-cell-bg)}.value-cell--dirty-missing{background:var(--state-dirty-missing-cell-bg)}.value-cell--active{box-shadow:inset 0 0 0 2px #2c5ecf42}.value-input{box-sizing:border-box;width:100%;resize:none;overflow:hidden;min-height:2.05rem;line-height:1.32;border:1px solid #d6e2f4;border-radius:9px;background:#fff;padding:.4rem .5rem;font:inherit;color:var(--text-primary);transition:border-color .12s ease,box-shadow .12s ease,background .12s ease}.value-input:focus{outline:none;border-color:#7ca2e0;box-shadow:0 0 0 2px #5586d824}.value-input--dirty{border-color:#0e5fd8;box-shadow:0 0 0 2px #0e5fd814}.actions-col{width:1%;min-width:0;white-space:nowrap;text-align:right}.row-actions{display:flex;gap:.4rem;flex-wrap:nowrap}.row-action-icon-btn{width:2rem;height:2rem;padding:0}.row-action-icon-btn svg{width:1.05rem;height:1.05rem}.rename-form{display:flex;gap:.4rem;flex-wrap:wrap;align-items:center}.inline-error{color:#9a241f;width:100%;font-size:.82rem}.empty-state{margin:1rem auto;padding:2.1rem 1.35rem;border:1px dashed #dbe4f5;border-radius:12px;background:linear-gradient(180deg,#fbfdff,#f6f9ff);text-align:center;max-width:700px;color:var(--text-secondary);line-height:1.48;font-size:.92rem}.empty-state--workspace{margin:1.2rem auto;padding:2.4rem 1.5rem}.empty-state--panel{margin:0;max-width:none;text-align:left;padding:1rem .92rem;border-style:solid;border-color:#dce7f8;background:#fff;font-size:.86rem;line-height:1.42}.footer-actions{margin-top:var(--space-2);padding:var(--space-2);border-top:1px solid #e2e9f6;border-radius:12px;background:linear-gradient(180deg,#f9fbff,#f6f9ff);display:grid;grid-template-columns:minmax(0,1fr);align-items:start;gap:var(--space-2)}.footer-actions__meta{min-width:0}.footer-actions__summary{margin:0;color:var(--text-secondary);font-size:.86rem;line-height:1.42;max-width:68ch}.footer-actions__xliff{margin-top:.55rem;display:flex;align-items:end;flex-wrap:wrap;gap:.45rem}.footer-actions__xliff-label{font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted)}.footer-actions__xliff-field{display:grid;gap:.18rem}.footer-actions__xliff-field span{font-size:.7rem;color:var(--text-muted)}.footer-actions__file-input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}.footer-actions__links{margin-top:.45rem;display:flex;flex-wrap:wrap;gap:.45rem}.btn{border-radius:10px;border:1px solid transparent;padding:.5rem .8rem;font-size:.9rem;font-weight:600;text-decoration:none;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:transform .12s ease,box-shadow .12s ease,background .12s ease}.btn:hover{transform:translateY(-.5px)}.btn:disabled{opacity:.55;cursor:not-allowed;transform:none}.btn--primary{color:#fff;background:linear-gradient(135deg,#245fdf,#1b49be);box-shadow:0 10px 18px #1f59d942}.btn--ghost{color:#1d355c;border-color:#c2d0ea;background:#f6f9ff}.btn--support{color:#fff;background:linear-gradient(135deg,#ff7849,#f14c4c);box-shadow:0 7px 16px #f14c4c3b}.btn--danger{color:#9c212a;border-color:#eec1c7;background:#fff6f7}.btn--small{padding:.4rem .6rem;font-size:.8rem}.is-active{border-color:#4d73d7;background:#eaf1ff}input,textarea,select{border-radius:9px;border:1px solid #c4d2ea;background:#fff;color:var(--text-primary);padding:.48rem .6rem;font:inherit}input:focus,textarea:focus,select:focus{outline:none;border-color:#1f5eff;box-shadow:0 0 0 3px #1f5eff24}.modal-overlay{position:fixed;inset:0;background:#0e162480;display:flex;align-items:center;justify-content:center;padding:1rem;z-index:1000}.modal-dialog{width:min(480px,100%);border-radius:14px;border:1px solid var(--surface-border);background:var(--surface-card);box-shadow:0 24px 44px #0e1f4847;padding:1rem;display:grid;gap:.75rem}.modal-dialog__message{margin:0;color:var(--text-primary);white-space:pre-line}.modal-dialog__actions{display:flex;justify-content:flex-end;gap:.5rem}.toast-stack{position:fixed;right:1rem;bottom:1rem;z-index:1100}.toast{margin:0;border-radius:10px;border:1px solid #bde0cb;background:#edf9f1;color:#1e6f45;padding:.55rem .7rem;font-size:.86rem;font-weight:600;box-shadow:0 10px 24px #10361f24}.toast--success{border-color:#bde0cb;background:#edf9f1;color:#1e6f45}@media(max-width:860px){.gloss-app{padding:var(--space-3) var(--space-2) calc(var(--space-5) + var(--space-2))}.workspace-shell{margin-top:var(--space-2);grid-template-columns:1fr}.action-sidebar{position:static;grid-template-columns:repeat(2,auto);justify-content:start;align-items:start}.action-sidebar__group{grid-auto-flow:column;grid-auto-columns:min-content}.app-header{position:static}.app-header__main{grid-template-columns:1fr}.app-header__controls{justify-items:start}.editor-shell{padding:var(--space-2)}.status-bar{align-items:flex-start}.editor-controls{padding:.5rem}.toolbar__top,.toolbar__actions,.toolbar__chips,.toolbar__field,.toolbar__field--search,.toolbar__rule{width:100%}.toolbar__top{grid-template-columns:1fr}.toolbar__rule{grid-template-columns:1fr;align-items:stretch}.toolbar__field input,.toolbar__field select,.toolbar__actions .btn,.add-key-form input{width:100%}.editor-main,.workspace-content-shell--with-drawer{grid-template-columns:1fr}.analysis-drawer{max-height:none}.editor-explorers{grid-template-columns:1fr}.namespace-tree,.file-tree{max-height:none}.footer-actions,.issue-row{grid-template-columns:1fr;align-items:stretch}.issue-row__actions{justify-content:flex-start}.toast-stack{left:.75rem;right:.75rem;bottom:.75rem}}
@@ -11,8 +11,8 @@
11
11
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
12
12
  <meta name="apple-mobile-web-app-title" content="Gloss" />
13
13
  <link rel="manifest" href="/site.webmanifest" />
14
- <script type="module" crossorigin src="/assets/index-DfgO64nU.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-CgyZVU2h.css">
14
+ <script type="module" crossorigin src="/assets/index-BCr07xD_.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-CjmLcA1x.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
package/dist/usage.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { updateCacheMetrics } from "./cacheMetrics.js";
3
4
  import { createScanMatcher } from "./scanFilters.js";
4
5
  import { extractTranslationKeys } from "./usageExtractor.js";
5
6
  const SUPPORTED_EXTENSIONS = [
@@ -33,6 +34,66 @@ const hasSkippedPathSegment = (relativePath) => normalizePath(relativePath)
33
34
  .split("/")
34
35
  .some((segment) => SKIP_DIRECTORIES.has(segment));
35
36
  const isSupportedFile = (name) => SUPPORTED_EXTENSIONS.some((extension) => name.endsWith(extension));
37
+ const fileSignature = (mtimeMs, size) => `${mtimeMs}:${size}`;
38
+ const keyUsageCache = new Map();
39
+ const mtimeFromEntry = (entry) => {
40
+ if (typeof entry.mtimeMs === "number" && Number.isFinite(entry.mtimeMs)) {
41
+ return entry.mtimeMs;
42
+ }
43
+ const parsed = Number.parseFloat(entry.signature.split(":")[0] ?? "");
44
+ return Number.isFinite(parsed) ? parsed : null;
45
+ };
46
+ const sizeFromEntry = (entry) => {
47
+ if (typeof entry.sizeBytes === "number" && Number.isFinite(entry.sizeBytes)) {
48
+ return entry.sizeBytes;
49
+ }
50
+ const parsed = Number.parseInt(entry.signature.split(":")[1] ?? "", 10);
51
+ return Number.isFinite(parsed) ? parsed : 0;
52
+ };
53
+ const summarizeCacheEntries = (entries) => {
54
+ let fileCount = 0;
55
+ let totalSizeBytes = 0;
56
+ let oldestMtimeMs = null;
57
+ for (const entry of entries) {
58
+ fileCount += 1;
59
+ totalSizeBytes += sizeFromEntry(entry);
60
+ const mtime = mtimeFromEntry(entry);
61
+ if (mtime !== null) {
62
+ oldestMtimeMs = oldestMtimeMs === null ? mtime : Math.min(oldestMtimeMs, mtime);
63
+ }
64
+ }
65
+ return { fileCount, totalSizeBytes, oldestMtimeMs };
66
+ };
67
+ export const keyUsageCacheKey = (cfg) => {
68
+ const root = projectRoot();
69
+ const i18nDirectory = translationsDir(cfg);
70
+ return `${path.resolve(root)}::${path.resolve(i18nDirectory)}::${JSON.stringify(cfg.scan ?? {})}`;
71
+ };
72
+ export const getKeyUsageCacheStatus = (cfg) => {
73
+ const cacheKey = keyUsageCacheKey(cfg);
74
+ const bucket = keyUsageCache.get(cacheKey);
75
+ if (!bucket) {
76
+ return {
77
+ cacheKey,
78
+ fileCount: 0,
79
+ totalSizeBytes: 0,
80
+ oldestMtimeMs: null,
81
+ };
82
+ }
83
+ return {
84
+ cacheKey,
85
+ ...summarizeCacheEntries(bucket.files.values()),
86
+ };
87
+ };
88
+ export const clearKeyUsageCache = () => {
89
+ const bucketCount = keyUsageCache.size;
90
+ let fileCount = 0;
91
+ for (const bucket of keyUsageCache.values()) {
92
+ fileCount += bucket.files.size;
93
+ }
94
+ keyUsageCache.clear();
95
+ return { bucketCount, fileCount };
96
+ };
36
97
  const extractRelativeImports = (content) => {
37
98
  const imports = new Set();
38
99
  const importRegexes = [
@@ -102,7 +163,7 @@ const isPageFile = (relativePath) => {
102
163
  isNextAppPage ||
103
164
  isSvelteKitPage);
104
165
  };
105
- const collectFiles = async (directory, projectDir, shouldScanFile, cfg, files) => {
166
+ const collectFiles = async (directory, projectDir, shouldScanFile, cfg, previousFiles, nextFiles, files) => {
106
167
  const entries = await fs.readdir(directory, { withFileTypes: true });
107
168
  for (const entry of entries) {
108
169
  if (entry.name.startsWith(".")) {
@@ -113,7 +174,7 @@ const collectFiles = async (directory, projectDir, shouldScanFile, cfg, files) =
113
174
  if (SKIP_DIRECTORIES.has(entry.name)) {
114
175
  continue;
115
176
  }
116
- await collectFiles(fullPath, projectDir, shouldScanFile, cfg, files);
177
+ await collectFiles(fullPath, projectDir, shouldScanFile, cfg, previousFiles, nextFiles, files);
117
178
  continue;
118
179
  }
119
180
  if (!entry.isFile() || !isSupportedFile(entry.name)) {
@@ -126,16 +187,38 @@ const collectFiles = async (directory, projectDir, shouldScanFile, cfg, files) =
126
187
  if (!shouldScanFile(relativePath)) {
127
188
  continue;
128
189
  }
190
+ const stat = await fs.stat(fullPath);
191
+ const signature = fileSignature(stat.mtimeMs, stat.size);
192
+ const cached = previousFiles?.get(relativePath);
193
+ if (cached && cached.signature === signature) {
194
+ nextFiles.set(relativePath, cached);
195
+ files.push({
196
+ filePath: fullPath,
197
+ relativePath,
198
+ keys: new Set(cached.keys),
199
+ imports: [...cached.imports],
200
+ });
201
+ continue;
202
+ }
129
203
  const content = await fs.readFile(fullPath, "utf8");
204
+ const keys = extractTranslationKeys(content, fullPath, cfg.scan?.mode);
205
+ const imports = extractRelativeImports(content);
206
+ nextFiles.set(relativePath, {
207
+ signature,
208
+ keys,
209
+ imports,
210
+ mtimeMs: stat.mtimeMs,
211
+ sizeBytes: stat.size,
212
+ });
130
213
  files.push({
131
214
  filePath: fullPath,
132
215
  relativePath,
133
- keys: new Set(extractTranslationKeys(content, fullPath, cfg.scan?.mode)),
134
- imports: extractRelativeImports(content),
216
+ keys: new Set(keys),
217
+ imports,
135
218
  });
136
219
  }
137
220
  };
138
- export async function buildKeyUsageMap(cfg) {
221
+ export async function buildKeyUsageMap(cfg, options) {
139
222
  const root = projectRoot();
140
223
  const i18nDirectory = translationsDir(cfg);
141
224
  const candidateRoots = [
@@ -146,6 +229,10 @@ export async function buildKeyUsageMap(cfg) {
146
229
  path.join(root, "routes"),
147
230
  ];
148
231
  const sourceRoots = Array.from(new Set(candidateRoots.filter((candidate) => path.resolve(candidate) !== root)));
232
+ const useCache = options?.useCache !== false;
233
+ const cacheKey = keyUsageCacheKey(cfg);
234
+ const previousCache = useCache ? keyUsageCache.get(cacheKey) : undefined;
235
+ const nextFileCache = new Map();
149
236
  const files = [];
150
237
  const shouldScanFile = createScanMatcher(cfg.scan);
151
238
  for (const sourceRoot of sourceRoots) {
@@ -153,7 +240,21 @@ export async function buildKeyUsageMap(cfg) {
153
240
  if (!stat?.isDirectory()) {
154
241
  continue;
155
242
  }
156
- await collectFiles(sourceRoot, root, shouldScanFile, cfg, files);
243
+ await collectFiles(sourceRoot, root, shouldScanFile, cfg, previousCache?.files, nextFileCache, files);
244
+ }
245
+ if (useCache) {
246
+ keyUsageCache.set(cacheKey, { files: nextFileCache });
247
+ const summary = summarizeCacheEntries(nextFileCache.values());
248
+ try {
249
+ await updateCacheMetrics(projectRoot(), "keyUsage", {
250
+ cacheKey,
251
+ ...summary,
252
+ updatedAt: new Date().toISOString(),
253
+ });
254
+ }
255
+ catch {
256
+ // Non-fatal: cache metrics are observability only.
257
+ }
157
258
  }
158
259
  const fileByPath = new Map(files.map((file) => [path.resolve(file.filePath), file]));
159
260
  const adjacency = new Map();
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { updateCacheMetrics } from "./cacheMetrics.js";
3
4
  import { createScanMatcher } from "./scanFilters.js";
4
5
  import { extractTranslationKeys } from "./usageExtractor.js";
5
6
  const IGNORED_DIRECTORIES = new Set([
@@ -17,6 +18,7 @@ const IGNORED_DIRECTORIES = new Set([
17
18
  const SCANNED_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
18
19
  const projectRoot = () => process.env.INIT_CWD || process.cwd();
19
20
  const normalizePath = (filePath) => filePath.split(path.sep).join("/");
21
+ const usageScannerCache = new Map();
20
22
  const isScannableFile = (fileName) => SCANNED_EXTENSIONS.has(path.extname(fileName));
21
23
  const hasIgnoredPathSegment = (relativePath) => normalizePath(relativePath)
22
24
  .split("/")
@@ -42,9 +44,69 @@ export const inferUsageRoot = (cfg) => {
42
44
  }
43
45
  return parentDirectory;
44
46
  };
45
- export async function scanUsage(rootDir = projectRoot(), scan) {
46
- const usage = {};
47
- const seenFilesByKey = new Map();
47
+ export const usageScannerCacheKey = (rootDir, scan) => {
48
+ const normalizedRoot = path.resolve(rootDir);
49
+ return `${normalizedRoot}::${JSON.stringify(scan ?? {})}`;
50
+ };
51
+ const fileSignature = (mtimeMs, size) => `${mtimeMs}:${size}`;
52
+ const mtimeFromEntry = (entry) => {
53
+ if (typeof entry.mtimeMs === "number" && Number.isFinite(entry.mtimeMs)) {
54
+ return entry.mtimeMs;
55
+ }
56
+ const parsed = Number.parseFloat(entry.signature.split(":")[0] ?? "");
57
+ return Number.isFinite(parsed) ? parsed : null;
58
+ };
59
+ const sizeFromEntry = (entry) => {
60
+ if (typeof entry.sizeBytes === "number" && Number.isFinite(entry.sizeBytes)) {
61
+ return entry.sizeBytes;
62
+ }
63
+ const parsed = Number.parseInt(entry.signature.split(":")[1] ?? "", 10);
64
+ return Number.isFinite(parsed) ? parsed : 0;
65
+ };
66
+ const summarizeCacheEntries = (entries) => {
67
+ let fileCount = 0;
68
+ let totalSizeBytes = 0;
69
+ let oldestMtimeMs = null;
70
+ for (const entry of entries) {
71
+ fileCount += 1;
72
+ totalSizeBytes += sizeFromEntry(entry);
73
+ const mtime = mtimeFromEntry(entry);
74
+ if (mtime !== null) {
75
+ oldestMtimeMs = oldestMtimeMs === null ? mtime : Math.min(oldestMtimeMs, mtime);
76
+ }
77
+ }
78
+ return { fileCount, totalSizeBytes, oldestMtimeMs };
79
+ };
80
+ export const getUsageScannerCacheStatus = (rootDir, scan) => {
81
+ const cacheKey = usageScannerCacheKey(rootDir, scan);
82
+ const bucket = usageScannerCache.get(cacheKey);
83
+ if (!bucket) {
84
+ return {
85
+ cacheKey,
86
+ fileCount: 0,
87
+ totalSizeBytes: 0,
88
+ oldestMtimeMs: null,
89
+ };
90
+ }
91
+ return {
92
+ cacheKey,
93
+ ...summarizeCacheEntries(bucket.files.values()),
94
+ };
95
+ };
96
+ export const clearUsageScannerCache = () => {
97
+ const bucketCount = usageScannerCache.size;
98
+ let fileCount = 0;
99
+ for (const bucket of usageScannerCache.values()) {
100
+ fileCount += bucket.files.size;
101
+ }
102
+ usageScannerCache.clear();
103
+ return { bucketCount, fileCount };
104
+ };
105
+ export async function scanUsage(rootDir = projectRoot(), scan, options) {
106
+ const useCache = options?.useCache !== false;
107
+ const cacheKey = usageScannerCacheKey(rootDir, scan);
108
+ const previousCache = useCache ? usageScannerCache.get(cacheKey) : undefined;
109
+ const nextFiles = new Map();
48
110
  const shouldScanFile = createScanMatcher(scan);
49
111
  const scanDirectory = async (directory) => {
50
112
  const entries = await fs.readdir(directory, { withFileTypes: true });
@@ -67,23 +129,54 @@ export async function scanUsage(rootDir = projectRoot(), scan) {
67
129
  if (!shouldScanFile(relativePath)) {
68
130
  continue;
69
131
  }
132
+ const stat = await fs.stat(fullPath);
133
+ const signature = fileSignature(stat.mtimeMs, stat.size);
134
+ const cached = previousCache?.files.get(relativePath);
135
+ if (cached && cached.signature === signature) {
136
+ nextFiles.set(relativePath, cached);
137
+ continue;
138
+ }
70
139
  const source = await fs.readFile(fullPath, "utf8");
71
140
  const keys = extractTranslationKeys(source, fullPath, scan?.mode);
72
- for (const key of keys) {
73
- if (!usage[key]) {
74
- usage[key] = { count: 0, files: [] };
75
- seenFilesByKey.set(key, new Set());
76
- }
77
- usage[key].count += 1;
78
- const fileSet = seenFilesByKey.get(key);
79
- if (fileSet && !fileSet.has(relativePath)) {
80
- fileSet.add(relativePath);
81
- usage[key].files.push(relativePath);
82
- }
83
- }
141
+ nextFiles.set(relativePath, {
142
+ signature,
143
+ keys,
144
+ mtimeMs: stat.mtimeMs,
145
+ sizeBytes: stat.size,
146
+ });
84
147
  }
85
148
  };
86
149
  await scanDirectory(rootDir);
150
+ if (useCache) {
151
+ usageScannerCache.set(cacheKey, { files: nextFiles });
152
+ const summary = summarizeCacheEntries(nextFiles.values());
153
+ try {
154
+ await updateCacheMetrics(projectRoot(), "usageScanner", {
155
+ cacheKey,
156
+ ...summary,
157
+ updatedAt: new Date().toISOString(),
158
+ });
159
+ }
160
+ catch {
161
+ // Non-fatal: cache metrics are observability only.
162
+ }
163
+ }
164
+ const usage = {};
165
+ const seenFilesByKey = new Map();
166
+ for (const [relativePath, fileData] of nextFiles.entries()) {
167
+ for (const key of fileData.keys) {
168
+ if (!usage[key]) {
169
+ usage[key] = { count: 0, files: [] };
170
+ seenFilesByKey.set(key, new Set());
171
+ }
172
+ usage[key].count += 1;
173
+ const fileSet = seenFilesByKey.get(key);
174
+ if (fileSet && !fileSet.has(relativePath)) {
175
+ fileSet.add(relativePath);
176
+ usage[key].files.push(relativePath);
177
+ }
178
+ }
179
+ }
87
180
  for (const value of Object.values(usage)) {
88
181
  value.files.sort((left, right) => left.localeCompare(right));
89
182
  }
package/dist/xliff.js ADDED
@@ -0,0 +1,92 @@
1
+ import { flattenObject } from "./translationTree.js";
2
+ const escapeXml = (value) => value
3
+ .replace(/&/g, "&amp;")
4
+ .replace(/</g, "&lt;")
5
+ .replace(/>/g, "&gt;")
6
+ .replace(/"/g, "&quot;")
7
+ .replace(/'/g, "&apos;");
8
+ const decodeXml = (value) => value
9
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16)))
10
+ .replace(/&#([0-9]+);/g, (_, num) => String.fromCodePoint(Number.parseInt(num, 10)))
11
+ .replace(/&apos;/g, "'")
12
+ .replace(/&quot;/g, '"')
13
+ .replace(/&gt;/g, ">")
14
+ .replace(/&lt;/g, "<")
15
+ .replace(/&amp;/g, "&");
16
+ const normalizeXmlText = (value) => {
17
+ const cdataUnwrapped = value.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, (_, inner) => inner);
18
+ const withoutTags = cdataUnwrapped.replace(/<[^>]*>/g, "");
19
+ return decodeXml(withoutTags);
20
+ };
21
+ const readTagContent = (block, tag) => {
22
+ const regex = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i");
23
+ const match = regex.exec(block);
24
+ if (!match) {
25
+ return "";
26
+ }
27
+ return normalizeXmlText(match[1]);
28
+ };
29
+ const readBlockId = (attrs) => {
30
+ const match = /\bid\s*=\s*["']([^"']+)["']/i.exec(attrs);
31
+ return match ? decodeXml(match[1].trim()) : "";
32
+ };
33
+ const collectBlocks = (xml, tagName) => {
34
+ const regex = new RegExp(`<${tagName}\\b([^>]*)>([\\s\\S]*?)<\\/${tagName}>`, "gi");
35
+ const blocks = [];
36
+ let match = regex.exec(xml);
37
+ while (match) {
38
+ const id = readBlockId(match[1] ?? "");
39
+ if (id) {
40
+ blocks.push({ id, content: match[2] ?? "" });
41
+ }
42
+ match = regex.exec(xml);
43
+ }
44
+ return blocks;
45
+ };
46
+ export const buildXliffDocument = ({ translations, locales, sourceLocale, targetLocale, }) => {
47
+ const flattenedByLocale = {};
48
+ for (const locale of locales) {
49
+ flattenedByLocale[locale] = flattenObject(translations[locale] ?? {});
50
+ }
51
+ const keySet = new Set();
52
+ for (const values of Object.values(flattenedByLocale)) {
53
+ for (const key of Object.keys(values)) {
54
+ keySet.add(key);
55
+ }
56
+ }
57
+ const keys = Array.from(keySet).sort((left, right) => left.localeCompare(right));
58
+ const units = keys
59
+ .map((key) => {
60
+ const sourceValue = flattenedByLocale[sourceLocale]?.[key] ?? "";
61
+ const targetValue = flattenedByLocale[targetLocale]?.[key] ?? "";
62
+ return [
63
+ ` <trans-unit id="${escapeXml(key)}">`,
64
+ ` <source>${escapeXml(sourceValue)}</source>`,
65
+ ` <target>${escapeXml(targetValue)}</target>`,
66
+ " </trans-unit>",
67
+ ].join("\n");
68
+ })
69
+ .join("\n");
70
+ return [
71
+ '<?xml version="1.0" encoding="UTF-8"?>',
72
+ '<xliff version="1.2">',
73
+ ` <file source-language="${escapeXml(sourceLocale)}" target-language="${escapeXml(targetLocale)}" datatype="plaintext" original="gloss">`,
74
+ " <body>",
75
+ units,
76
+ " </body>",
77
+ " </file>",
78
+ "</xliff>",
79
+ "",
80
+ ].join("\n");
81
+ };
82
+ export const parseXliffTargets = (content) => {
83
+ const updates = {};
84
+ const blocks = [...collectBlocks(content, "trans-unit"), ...collectBlocks(content, "unit")];
85
+ for (const block of blocks) {
86
+ const target = readTagContent(block.content, "target");
87
+ const source = readTagContent(block.content, "source");
88
+ const value = target.length > 0 ? target : source;
89
+ updates[block.id] = value;
90
+ }
91
+ return updates;
92
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxml3gend/gloss",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Local-first CLI + web app for managing i18n translation files",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,7 +27,8 @@
27
27
  "dev:server": "tsx src/devServer.ts",
28
28
  "build": "tsc -p tsconfig.json && node ./scripts/copy-ui-dist.mjs",
29
29
  "prepublishOnly": "npm run build",
30
- "test": "npm run build && node --test tests/**/*.test.mjs"
30
+ "test": "npm run build && node --test tests/**/*.test.mjs",
31
+ "test:perf": "npm run build && node --test tests/performance.regression.test.mjs"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@types/cors": "^2.8.19",
@@ -1 +0,0 @@
1
- :root{--text-primary: #0f2343;--text-secondary: #3f5478;--text-muted: #5b7196;--surface-card: #ffffff;--surface-border: #d6e0f0;font-family:Manrope,Avenir Next,Segoe UI,sans-serif;line-height:1.45;font-weight:500;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,*:before,*:after{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh;background:radial-gradient(circle at 8% 0%,rgba(255,186,132,.26),transparent 42%),radial-gradient(circle at 90% 18%,rgba(120,184,255,.28),transparent 40%),linear-gradient(180deg,#f3f7ff,#f8f1e8)}#root{width:100%}.gloss-app{--space-1: .5rem;--space-2: .75rem;--space-3: 1rem;--space-4: 1.5rem;--space-5: 2rem;max-width:1360px;margin:0 auto;padding:var(--space-4) var(--space-3) calc(var(--space-5) + var(--space-2));color:var(--text-primary)}.hero{padding:var(--space-3) var(--space-4);border-radius:16px;border:1px solid #d9e3f6;background:linear-gradient(140deg,#fdfeff,#f8faff 55%,#f4f8ff);box-shadow:0 10px 24px #16264612}.hero__top{display:flex;justify-content:flex-end;align-items:center;gap:1rem;flex-wrap:wrap}.hero__eyebrow{margin:0;text-transform:uppercase;letter-spacing:.08em;font-size:.75rem;color:var(--text-muted)}.hero__title{margin:.4rem 0 0;font-size:clamp(2.1rem,5vw,3.5rem);line-height:1;letter-spacing:-.02em}.hero__logo{display:block;width:min(420px,100%);height:auto;margin:.4rem 0 0}.hero__summary{margin:var(--space-2) 0 var(--space-2);max-width:72ch;color:var(--text-secondary);line-height:1.45}.hero__stats{display:flex;flex-wrap:wrap;gap:var(--space-1);margin-bottom:var(--space-3)}.stat-chip{display:flex;align-items:baseline;gap:.45rem;padding:.4rem .68rem;border-radius:999px;background:#fffffff5;border:1px solid rgba(21,37,66,.09)}.stat-chip span{font-size:.75rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em}.stat-chip strong{font-size:1rem}.hero__actions{display:flex;flex-wrap:wrap;gap:.625rem}.language-switch{display:inline-flex;align-items:center;gap:.45rem;padding:.32rem .4rem;border-radius:999px;border:1px solid var(--surface-border);background:#fffffff0}.support-cards{margin-top:var(--space-2);display:grid;gap:var(--space-1);grid-template-columns:repeat(auto-fit,minmax(240px,1fr));opacity:.88}.support-card{border-radius:12px;border:1px solid #e2e9f7;background:#fbfcff;padding:var(--space-2) var(--space-3)}.support-card h2{margin:0;font-size:.95rem}.support-card p{margin:var(--space-1) 0 0;color:var(--text-secondary);font-size:.88rem;line-height:1.45}.editor-shell{margin-top:var(--space-3);border-radius:18px;border:1px solid #dbe4f5;background:#fdfefe;padding:var(--space-3);box-shadow:0 16px 30px #0f234c12;display:grid;gap:var(--space-2)}.status-bar{margin:0;border-radius:12px;border:1px solid #dbe5f7;background:#f8fbff;padding:.625rem .75rem;display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);flex-wrap:wrap}.status-bar__main{margin:0;font-weight:600;font-size:.9rem}.status-bar__main--error{color:#8f2935}.status-bar__main--warning{color:#7b5b23;display:flex;align-items:center;gap:var(--space-1);flex-wrap:wrap}.status-bar__main--success{color:#1d6a42}.status-bar__main--info{color:var(--text-secondary)}.status-bar__meta{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.status-chip{display:inline-flex;align-items:center;gap:.35rem;border-radius:999px;border:1px solid #cad8f2;background:#f0f5ff;color:#29476f;font-size:.76rem;font-weight:600;padding:.18rem .5rem}.status-chip[type=button]{cursor:pointer}.status-chip--muted{background:#f6f8fc;border-color:#dde4f2;color:#4d6285}.status-chip--warning{background:#fff5de;border-color:#f4d090;color:#7a4f08}.status-chip__action{opacity:.85;font-size:.72rem;border-left:1px solid currentColor;padding-left:.4rem}.status-bar__details{width:100%;border:1px solid #e0e8f7;border-radius:10px;background:#fff;padding:.6rem .75rem}.status-bar__details strong{display:block;margin-bottom:.45rem;color:#2f4b75;font-size:.82rem}.status-bar__details p{margin:0;color:var(--text-secondary);font-size:.84rem}.status-bar__details ul{margin:0;padding-left:1rem;display:grid;gap:.3rem}.status-bar__details li{color:#35527c;font-size:.82rem;line-height:1.35}.loading-state{margin:20vh auto 0;max-width:280px;border-radius:12px;border:1px solid #d5e2fb;background:#f7faff;padding:var(--space-3);text-align:center;color:var(--text-secondary)}.notice{margin:0 0 .7rem;padding:.6rem .75rem;border-radius:10px;border:1px solid transparent}.notice--error{color:#901f1a;background:#fff3f2;border-color:#f7cdc8}.notice--success{color:#0d6837;background:#edf8f0;border-color:#b6e4c3}.notice--warning{color:#7a4f08;background:#fff5de;border-color:#f4d090}.notice--stale{color:#7a2b31;background:#fff2f4;border-color:#f4c0c9;display:flex;align-items:center;gap:.6rem;flex-wrap:wrap}.editor-tabs{display:flex;gap:var(--space-1);margin:0;padding:.25rem;border:1px solid #e2e9f6;border-radius:11px;background:#f7faff}.translations-workspace{display:grid;gap:var(--space-2)}.editor-controls{display:grid;gap:var(--space-1);padding:var(--space-1);border-radius:14px;border:1px solid #e1e8f6;background:linear-gradient(180deg,#f9fbff,#f6f9ff)}.toolbar{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--space-1);flex-wrap:wrap;margin:0;padding:var(--space-2);border:1px solid #e4ebf8;border-radius:12px;background:#fff}.toolbar__primary{flex:1 1 280px;min-width:240px}.toolbar__secondary{display:flex;align-items:flex-end;gap:.5rem;flex-wrap:wrap}.toolbar__field{display:grid;gap:.32rem;min-width:160px}.toolbar__field--search input{width:100%}.toolbar__field span{font-size:.75rem;font-weight:600;color:#5f7394;text-transform:uppercase;letter-spacing:.06em}.toolbar__toggle{display:flex;align-items:center;gap:.4rem;font-size:.88rem;padding:.42rem .6rem;border:1px solid #dbe5f7;border-radius:10px;background:#fdfefe}.toolbar__advanced{width:100%;border:1px solid #dbe5f7;border-radius:12px;background:#f8fbff;padding:.7rem;display:grid;gap:.6rem}.toolbar__advanced-head{display:flex;align-items:center;justify-content:space-between;gap:.6rem;flex-wrap:wrap}.toolbar__advanced-head strong{font-size:.84rem;color:#2f4b75}.toolbar__advanced-actions{display:flex;gap:.4rem;flex-wrap:wrap}.toolbar__advanced-empty{margin:0;color:var(--text-secondary);font-size:.84rem}.toolbar__git-warning{margin:0;font-size:.82rem;color:#8f3c45}.toolbar__rules{display:grid;gap:.45rem}.toolbar__rule{display:grid;grid-template-columns:minmax(140px,1fr) minmax(140px,1fr) minmax(140px,1fr) auto;gap:.45rem;align-items:end;padding:.55rem;border:1px solid #d9e4f8;border-radius:10px;background:#fff}.toolbar__sort{display:flex;align-items:end;gap:.45rem;flex-wrap:wrap;padding-top:.25rem;border-top:1px dashed #d4dff4}.add-key-form{display:flex;align-items:flex-end;gap:var(--space-1);flex-wrap:wrap;margin:0;padding:var(--space-2);border:1px solid #e4ebf8;border-radius:12px;background:#fff}.add-key-form input{min-width:260px;flex:1 1 280px}.add-key-form__error{margin:0;color:#8f3c45;font-size:.82rem;padding:0 .125rem}.editor-main{display:grid;grid-template-columns:minmax(230px,280px) minmax(0,1fr);gap:var(--space-3);align-items:start}.editor-content{min-width:0;border:1px solid #dfe8f6;border-radius:15px;background:#fcfdff;padding:var(--space-2);box-shadow:inset 0 1px #fff}.file-tree{border:1px solid #d8e3f4;border-radius:14px;background:linear-gradient(180deg,#f8faff,#f2f7ff);padding:var(--space-2);max-height:74vh;overflow:auto;box-shadow:inset 0 1px #fff}.file-tree__title{margin:0 0 var(--space-1);font-size:.78rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);position:sticky;top:-.5rem;z-index:2;background:linear-gradient(180deg,#f8faff,#f8fafff5);padding:.45rem 0 .35rem}.file-tree__all-btn,.file-tree__folder-btn,.file-tree__file-btn{width:100%;text-align:left;border:1px solid transparent;border-radius:9px;background:transparent;padding:.42rem .54rem;color:var(--text-primary);font:inherit;cursor:pointer}.file-tree__all-btn,.file-tree__file-btn{display:flex;align-items:center;justify-content:space-between;gap:.5rem}.file-tree__folder-btn{display:inline-flex;align-items:center;gap:.38rem;color:#26456f;font-weight:600}.file-tree__caret{width:.8rem;color:#50688f}.file-tree__folder-name{overflow:hidden;text-overflow:ellipsis}.file-tree__all-btn:hover,.file-tree__folder-btn:hover,.file-tree__file-btn:hover{background:#e9f1ff}.file-tree__all-btn.is-selected,.file-tree__file-btn.is-selected{border-color:#8caadd;background:#e4edff;box-shadow:inset 3px 0 #466fcf}.file-tree__file-name{overflow:hidden;text-overflow:ellipsis}.file-tree__file-count{color:var(--text-muted);font-size:.72rem;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;background:#edf2fd;border:1px solid #d4e0f5;border-radius:999px;padding:.1rem .38rem}.file-tree__list{list-style:none;margin:var(--space-1) 0 0;padding:0;display:grid;gap:.1rem}.file-tree__item{margin:0}.file-tree__empty{margin:.55rem 0 0;color:var(--text-secondary);font-size:.88rem}.duplicates-panel{border:1px solid var(--surface-border);border-radius:12px;background:#f8fafe;padding:.7rem}.duplicates-panel__title{margin:0 0 .65rem;font-size:.95rem;font-weight:700}.duplicates-panel__list{display:grid;gap:.75rem}.duplicate-locale{border:1px solid #d7e2f7;border-radius:10px;background:#fff;padding:.55rem}.duplicate-locale__title{margin:0 0 .45rem;font-size:.78rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted)}.duplicate-group{border:1px solid #e3ebfb;border-radius:9px;padding:.5rem;margin-top:.45rem}.duplicate-group__top{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:.5rem;align-items:center}.duplicate-group__value{overflow-wrap:anywhere}.duplicate-group__count{font-size:.78rem;color:var(--text-muted)}.duplicate-group__keys{margin:.45rem 0 0;padding-left:1.2rem;display:grid;gap:.2rem;color:var(--text-secondary);font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.78rem}.table-wrap{border-radius:12px;border:1px solid #d8e3f4;background:#fff;overflow:auto;max-height:74vh}.grid-table{width:100%;min-width:1180px;border-collapse:collapse}.grid-table th{text-align:left;padding:.78rem .72rem;border-bottom:1px solid #d9e3f5;background:#f6f9ff;position:sticky;top:0;z-index:2;color:#3a557f;font-size:.76rem;text-transform:uppercase;letter-spacing:.06em}.grid-table td{padding:.72rem .66rem;border-bottom:1px solid #edf1f8;vertical-align:top}.grid-table tr.row-state--none>td{background:#fff7f6}.grid-table tr.row-state--partial>td{background:#fffaf2}.grid-table tr.row-state--highlighted>td{background:#edf4ff}.grid-table tr.row-state--none td.value-cell--dirty,.grid-table tr.row-state--partial td.value-cell--dirty{background:#e9f2ff}.grid-table tr.row-state--none td.value-cell--dirty-missing,.grid-table tr.row-state--partial td.value-cell--dirty-missing{background:#fff2df}.grid-table tbody tr:not(.usage-files-row):not(.virtual-spacer):hover>td{background-color:#f7fafe}.key-col{width:320px;min-width:320px;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.83rem;color:#1f3659;border-right:1px solid #d7e2f4;background-clip:padding-box}.key-col--unused{color:#7a5b2f}.key-col--file-selected{box-shadow:inset 3px 0 #4a74d9}.key-col--placeholder-warning{box-shadow:inset 0 -2px #e59f35}.usage-col{width:130px}.usage-cell{color:var(--text-secondary);white-space:nowrap}.usage-cell--unused{background:#fffaf3}.usage-toggle{border:0;background:none;padding:0;font:inherit;color:#1848a3;cursor:pointer;text-decoration:underline;text-underline-offset:2px}.usage-tag{font-size:.74rem;font-weight:600;color:#8b6a39;text-transform:lowercase}.usage-files-row td{background:#f8fbff;padding-top:.7rem;padding-bottom:.8rem}.virtual-spacer td{border-bottom:0;padding:0}.usage-files{font-size:.82rem;color:var(--text-secondary);border:1px solid #dde6f6;border-radius:10px;background:#fff;padding:.6rem .7rem}.usage-files strong{margin-right:.45rem;color:var(--text-primary)}.usage-files-list{margin:.45rem 0 0;padding-left:1.2rem;display:grid;gap:.26rem;color:var(--text-primary);font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.78rem}.key-diff-block{margin-top:.7rem;padding-top:.55rem;border-top:1px dashed #d7e3f8}.key-diff-line{overflow-wrap:anywhere}.status-col{width:176px;min-width:176px;color:#4a5f80;font-size:.84rem;line-height:1.4}.status-col__tags{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.25rem}.status-inline-tag{display:inline-flex;align-items:center;border-radius:999px;padding:.1rem .42rem;font-size:.7rem;font-weight:700;letter-spacing:.03em}.status-inline-tag--warning{color:#8a5a15;background:#fff4df;border:1px solid #efd2a1}.status-inline-tag--info{color:#214f98;background:#edf4ff;border:1px solid #c8daf8}.locale-col,.locale-cell{min-width:220px}.value-cell{background:transparent}.value-cell--missing{background:#fffaf4}.value-cell--dirty{background:#e9f2ff}.value-cell--dirty-missing{background:#fff3e3}.value-cell--active{box-shadow:inset 0 0 0 2px #2c5ecf57}.value-input{box-sizing:border-box;width:100%;resize:vertical;min-height:2.25rem;line-height:1.35}.value-input--dirty{border-color:#0e5fd8;box-shadow:0 0 0 2px #0e5fd814}.actions-col{width:220px;min-width:220px}.row-actions{display:flex;gap:.4rem;flex-wrap:wrap}.rename-form{display:flex;gap:.4rem;flex-wrap:wrap;align-items:center}.inline-error{color:#9a241f;width:100%;font-size:.82rem}.empty-state{margin:1rem auto;padding:2.25rem 1.25rem;border:1px dashed #dbe4f5;border-radius:12px;background:linear-gradient(180deg,#fbfdff,#f6f9ff);text-align:center;max-width:680px;color:var(--text-secondary);line-height:1.5}.footer-actions{margin-top:var(--space-2);padding:var(--space-2) 0 0;border-top:1px solid #e2e9f6;display:flex;justify-content:flex-end;position:sticky;bottom:0;background:linear-gradient(180deg,#fdfeff00,#fdfeff 32%);z-index:3}.footer-actions .btn--primary{min-width:152px;min-height:2.5rem;font-size:.95rem}.btn{border-radius:10px;border:1px solid transparent;padding:.5rem .8rem;font-size:.9rem;font-weight:600;text-decoration:none;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:transform .12s ease,box-shadow .12s ease,background .12s ease}.btn:hover{transform:translateY(-.5px)}.btn:disabled{opacity:.55;cursor:not-allowed;transform:none}.btn--primary{color:#fff;background:linear-gradient(135deg,#245fdf,#1b49be);box-shadow:0 10px 18px #1f59d942}.btn--ghost{color:#1d355c;border-color:#c2d0ea;background:#f6f9ff}.btn--support{color:#fff;background:linear-gradient(135deg,#ff7849,#f14c4c);box-shadow:0 7px 16px #f14c4c3b}.btn--danger{color:#9c212a;border-color:#eec1c7;background:#fff6f7}.btn--small{padding:.4rem .6rem;font-size:.8rem}.is-active{border-color:#4d73d7;background:#eaf1ff}input,textarea,select{border-radius:9px;border:1px solid #c4d2ea;background:#fff;color:var(--text-primary);padding:.48rem .6rem;font:inherit}input:focus,textarea:focus,select:focus{outline:none;border-color:#1f5eff;box-shadow:0 0 0 3px #1f5eff24}.modal-overlay{position:fixed;inset:0;background:#0e162480;display:flex;align-items:center;justify-content:center;padding:1rem;z-index:1000}.modal-dialog{width:min(480px,100%);border-radius:14px;border:1px solid var(--surface-border);background:var(--surface-card);box-shadow:0 24px 44px #0e1f4847;padding:1rem;display:grid;gap:.75rem}.modal-dialog__message{margin:0;color:var(--text-primary);white-space:pre-line}.modal-dialog__actions{display:flex;justify-content:flex-end;gap:.5rem}@media(max-width:860px){.gloss-app{padding:var(--space-3) var(--space-2) calc(var(--space-5) + var(--space-2))}.hero{padding:var(--space-3)}.hero__top{align-items:flex-start}.editor-shell{padding:var(--space-2)}.status-bar{align-items:flex-start}.editor-controls{padding:.5rem}.toolbar__primary,.toolbar__secondary,.toolbar__field,.toolbar__field--search,.toolbar__rule{width:100%}.toolbar__rule{grid-template-columns:1fr;align-items:stretch}.toolbar__field input,.toolbar__field select,.toolbar__secondary .btn,.add-key-form input{width:100%}.editor-main{grid-template-columns:1fr}.file-tree{max-height:none}}