@mxml3gend/gloss 0.1.1 → 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.
- package/README.md +74 -0
- package/dist/baseline.js +118 -0
- package/dist/cache.js +78 -0
- package/dist/cacheMetrics.js +120 -0
- package/dist/check.js +510 -0
- package/dist/config.js +214 -10
- package/dist/fs.js +105 -6
- package/dist/gitDiff.js +113 -0
- package/dist/hooks.js +101 -0
- package/dist/index.js +437 -12
- package/dist/renameKeyUsage.js +4 -12
- package/dist/server.js +163 -9
- package/dist/translationKeys.js +20 -0
- package/dist/translationTree.js +42 -0
- package/dist/typegen.js +30 -0
- package/dist/ui/Gloss_logo.png +0 -0
- package/dist/ui/assets/index-BCr07xD_.js +21 -0
- package/dist/ui/assets/index-CjmLcA1x.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/logo_full.png +0 -0
- package/dist/usage.js +105 -22
- package/dist/usageExtractor.js +151 -0
- package/dist/usageScanner.js +110 -28
- package/dist/xliff.js +92 -0
- package/package.json +15 -5
- package/dist/ui/assets/index-CREq9Gop.css +0 -1
- package/dist/ui/assets/index-Dhb2pVPI.js +0 -10
package/dist/xliff.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { flattenObject } from "./translationTree.js";
|
|
2
|
+
const escapeXml = (value) => value
|
|
3
|
+
.replace(/&/g, "&")
|
|
4
|
+
.replace(/</g, "<")
|
|
5
|
+
.replace(/>/g, ">")
|
|
6
|
+
.replace(/"/g, """)
|
|
7
|
+
.replace(/'/g, "'");
|
|
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(/'/g, "'")
|
|
12
|
+
.replace(/"/g, '"')
|
|
13
|
+
.replace(/>/g, ">")
|
|
14
|
+
.replace(/</g, "<")
|
|
15
|
+
.replace(/&/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.
|
|
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",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
".": "./dist/index.js"
|
|
10
10
|
},
|
|
11
11
|
"bin": {
|
|
12
|
-
"gloss": "
|
|
12
|
+
"gloss": "dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
@@ -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",
|
|
@@ -39,6 +40,15 @@
|
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"cors": "^2.8.6",
|
|
41
42
|
"express": "^5.2.1",
|
|
42
|
-
"open": "^11.0.0"
|
|
43
|
-
|
|
43
|
+
"open": "^11.0.0",
|
|
44
|
+
"typescript": "^5.9.3"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/MXML3GEND/gloss.git"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/MXML3GEND/gloss/issues"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/MXML3GEND/gloss#readme"
|
|
44
54
|
}
|
|
@@ -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:space-between;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__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}.language-switch span{font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);padding-inline:.25rem}.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--muted{background:#f6f8fc;border-color:#dde4f2;color:#4d6285}.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__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--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}.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}.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)}.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}}
|