Compare commits
9 Commits
64af8e6d95
...
9ae8be6a50
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ae8be6a50 | |||
| 9ce947075a | |||
| a3c6c9fa76 | |||
| 9121bb287c | |||
| 25cf8ab255 | |||
| b4e802fdec | |||
| 8481a897dc | |||
| 22d10902fb | |||
| 2c59ce43ad |
70
biome.json
70
biome.json
@@ -1,37 +1,37 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
"useIgnoreFile": true
|
"useIgnoreFile": true
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignoreUnknown": false
|
"ignoreUnknown": false
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"indentStyle": "space",
|
"indentStyle": "space",
|
||||||
"indentWidth": 2
|
"indentWidth": 2
|
||||||
},
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true
|
"recommended": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"javascript": {
|
"javascript": {
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"quoteStyle": "single",
|
"quoteStyle": "single",
|
||||||
"trailingCommas": "none",
|
"trailingCommas": "none",
|
||||||
"semicolons": "asNeeded"
|
"semicolons": "asNeeded"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"assist": {
|
"assist": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"actions": {
|
"actions": {
|
||||||
"source": {
|
"source": {
|
||||||
"organizeImports": "on"
|
"organizeImports": "on"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
bun.lock
19
bun.lock
@@ -3,11 +3,18 @@
|
|||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "st-randomness-helpers",
|
"name": "st-randomness-helpers",
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"zod": "^4.0.17",
|
||||||
|
"zustand": "^5.0.7",
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.1.4",
|
"@biomejs/biome": "2.1.4",
|
||||||
"@commitlint/cli": "^19.8.1",
|
"@commitlint/cli": "^19.8.1",
|
||||||
"@commitlint/config-conventional": "^19.8.1",
|
"@commitlint/config-conventional": "^19.8.1",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
|
"@types/react-dom": "^19.1.7",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.1.5",
|
"lint-staged": "^16.1.5",
|
||||||
},
|
},
|
||||||
@@ -81,6 +88,8 @@
|
|||||||
|
|
||||||
"@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="],
|
"@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@19.1.7", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw=="],
|
||||||
|
|
||||||
"JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="],
|
"JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="],
|
||||||
|
|
||||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||||
@@ -255,6 +264,10 @@
|
|||||||
|
|
||||||
"pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
|
"pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
|
||||||
|
|
||||||
|
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="],
|
||||||
|
|
||||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
@@ -265,6 +278,8 @@
|
|||||||
|
|
||||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||||
|
|
||||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
@@ -305,6 +320,10 @@
|
|||||||
|
|
||||||
"yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="],
|
"yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.0.17", "", {}, "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ=="],
|
||||||
|
|
||||||
|
"zustand": ["zustand@5.0.7", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg=="],
|
||||||
|
|
||||||
"cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
"cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||||
|
|
||||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export default { extends: ['@commitlint/config-conventional'] };
|
export default { extends: ['@commitlint/config-conventional'] }
|
||||||
|
|||||||
1
dist/index.css
vendored
Normal file
1
dist/index.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.st-rnd-details{border:1px solid var(--color-neutral-6);background-color:var(--color-neutral-3);color:var(--color-neutral-12);border-radius:4px}.st-rnd-details[open] summary{background-color:var(--color-neutral-5)}.st-rnd-details>summary{cursor:pointer;padding:4px 8px;font-size:16px;font-weight:600;line-height:20px}.st-rnd-details>summary:hover{background-color:var(--color-neutral-4)}.st-rnd-details>div{padding:8px}.st-rnd-button{--btn-background-color:var(--color-primary-3);--btn-background-color-hover:var(--color-primary-4);--btn-background-color-active:var(--color-primary-5);--btn-border-color:var(--color-primary-6);--btn-color:var(--color-primary-12)}.st-rnd-button.button-neutral{--btn-background-color:var(--color-neutral-3);--btn-background-color-hover:var(--color-neutral-4);--btn-background-color-active:var(--color-neutral-5);--btn-border-color:var(--color-neutral-6);--btn-color:var(--color-neutral-12)}.st-rnd-button{display:inline-block;border:1px solid var(--btn-border-color);background-color:var(--btn-background-color);color:var(--btn-color);text-align:center;cursor:pointer;border-radius:4px;padding:4px 8px;font-weight:600}.st-rnd-button:hover{background-color:var(--btn-background-color-hover)}.st-rnd-button:active{background-color:var(--btn-background-color-active)}.st-rnd-button:disabled{background-color:color-mix(in srgb,var(--color-neutral-1)60%,var(--btn-background-color)40%);border-color:color-mix(in srgb,var(--color-neutral-1)60%,var(--btn-border-color)40%);color:color-mix(in srgb,var(--color-neutral-1)60%,var(--btn-color)40%);cursor:not-allowed}.st-rnd-button:disabled:hover{background-color:color-mix(in srgb,var(--color-neutral-1)60%,var(--btn-background-color)40%);border-color:color-mix(in srgb,var(--color-neutral-1)60%,var(--btn-border-color)40%);color:color-mix(in srgb,var(--color-neutral-1)60%,var(--btn-color)40%)}.st-rnd-button.button-icon{aspect-ratio:1;width:auto;height:auto;padding:4px;font-size:.8em}#st-rnd-upload-wordlist-container{display:flex;flex-direction:column}#st-rnd-wordlist-controls{display:flex;flex-direction:row;gap:8px}#st-rnd-wordlist-controls>select{flex-grow:1}#st-rnd-wordlist-controls>button{flex-shrink:0}.st-rnd-small-text{color:var(--color-neutral-11);font-size:.8em}.st-rnd-select{border:1px solid var(--color-neutral-6);background-color:var(--color-neutral-3);color:var(--color-neutral-12);border-radius:4px;padding:4px 8px}.st-rnd-select:active{background-color:var(--color-neutral-12)}.st-rnd-select:disabled{background-color:color-mix(in srgb,var(--color-neutral-1)60%,var(--color-neutral-3)40%);color:color-mix(in srgb,var(--color-neutral-1)60%,var(--color-neutral-12)40%);cursor:not-allowed}.st-rnd-select:disabled:active{background-color:color-mix(in srgb,var(--color-neutral-1)60%,var(--color-neutral-3)40%);color:color-mix(in srgb,var(--color-neutral-1)60%,var(--color-neutral-12)40%)}.st-rnd-dialog{border:1px solid var(--color-neutral-6);background-color:var(--color-neutral-1);color:var(--color-neutral-12);border-radius:4px;padding:8px}.st-rnd-dialog::backdrop{background-color:color-mix(in srgb,var(--color-neutral-2)80%,transparent 20%)}#st-rnd-wordlist-edit-dialog{position:relative;width:clamp(400px,100%,1200px)}#st-rnd-wordlist-edit-dialog>div{display:flex;flex-direction:column;gap:8px}#st-rnd-wordlist-edit-dialog #st-rnd-wordlist-edit-dialog-title{text-align:center;width:100%}#st-rnd-wordlist-edit-dialog #st-rnd-wordlist-edit-dialog-title>span{color:var(--color-primary-9)}#st-rnd-wordlist-edit-dialog #st-rnd-wordlist-actions{display:flex;justify-content:flex-end;gap:8px}.st-rnd-input{display:block;border:1px solid var(--color-neutral-6);background-color:var(--color-neutral-3);color:var(--color-neutral-12);border-radius:4px;width:100%;padding:4px 8px}.st-rnd-textarea{display:block;border:1px solid var(--color-neutral-6);background-color:var(--color-neutral-3);color:var(--color-neutral-12);border-radius:4px;width:100%;padding:4px 8px}:root{--color-neutral-1:#111113;--color-neutral-2:#18191b;--color-neutral-3:#212225;--color-neutral-4:#272a2d;--color-neutral-5:#2e3135;--color-neutral-6:#363a3f;--color-neutral-7:#43484e;--color-neutral-8:#5a6169;--color-neutral-9:#696e77;--color-neutral-10:#777b84;--color-neutral-11:#b0b4ba;--color-neutral-12:#edeef0;--color-primary-1:#13131e;--color-primary-2:#171625;--color-primary-3:#202248;--color-primary-4:#262a65;--color-primary-5:#303374;--color-primary-6:#3d3e82;--color-primary-7:#4a4a95;--color-primary-8:#5958b1;--color-primary-9:#5b5bd6;--color-primary-10:#6e6ade;--color-primary-11:#b1a9ff;--color-primary-12:#e0dffe}
|
||||||
239
dist/index.js
vendored
239
dist/index.js
vendored
File diff suppressed because one or more lines are too long
13
manifest.json
Normal file
13
manifest.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"display_name": "St Randomness Helpers",
|
||||||
|
"loading_order": 1,
|
||||||
|
"requires": [],
|
||||||
|
"optional": [],
|
||||||
|
"dependencies": [],
|
||||||
|
"author": "Your name",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"homePage": "",
|
||||||
|
"js": "dist/index.js",
|
||||||
|
"css": "dist/index.css",
|
||||||
|
"auto_update": true
|
||||||
|
}
|
||||||
@@ -8,8 +8,13 @@
|
|||||||
"@commitlint/cli": "^19.8.1",
|
"@commitlint/cli": "^19.8.1",
|
||||||
"@commitlint/config-conventional": "^19.8.1",
|
"@commitlint/config-conventional": "^19.8.1",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
|
"@types/react-dom": "^19.1.7",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.1.5"
|
"lint-staged": "^16.1.5",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"zod": "^4.0.17",
|
||||||
|
"zustand": "^5.0.7"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
@@ -17,6 +22,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"dev": "bun build --watch src/index.ts --outdir ./dist --target browser",
|
"dev": "bun build --watch src/index.ts --outdir ./dist --target browser",
|
||||||
"build": "bun build src/index.ts --outdir ./dist --target browser"
|
"build": "bun build src/index.ts --outdir ./dist --target browser --minify"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/Settings.tsx
Normal file
12
src/Settings.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Details from '@/components/ui/Details'
|
||||||
|
import WordLists from '@/components/WordLists'
|
||||||
|
|
||||||
|
const Settings = () => {
|
||||||
|
return (
|
||||||
|
<Details title="Randomness Helpers">
|
||||||
|
<WordLists />
|
||||||
|
</Details>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Settings
|
||||||
131
src/components/WordLists.tsx
Normal file
131
src/components/WordLists.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import UploadButton from '@/components/ui/UploadButton'
|
||||||
|
import '@/components/wordLists.css'
|
||||||
|
import {
|
||||||
|
type ChangeEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState
|
||||||
|
} from 'react'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
import Select from '@/components/ui/Select'
|
||||||
|
import EditWordList from '@/components/wordList/EditWordList'
|
||||||
|
import { useStore } from '@/store'
|
||||||
|
|
||||||
|
const WordLists = () => {
|
||||||
|
const wordLists = useStore(
|
||||||
|
useShallow((state) => Object.keys(state.wordLists))
|
||||||
|
)
|
||||||
|
const addWordList = useStore((state) => state.addWordList)
|
||||||
|
const removeWordList = useStore((state) => state.removeWordList)
|
||||||
|
const [selectedWordList, setSelectedWordList] = useState('')
|
||||||
|
|
||||||
|
const handleUpload: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const file = event.target.files?.[0]
|
||||||
|
if (file !== undefined && file.type === 'text/plain') {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const fileName = file.name.replace('.txt', '')
|
||||||
|
if (wordLists.includes(fileName)) {
|
||||||
|
alert(`A word list with the name ${fileName} already exists`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const text = e.target?.result
|
||||||
|
if (typeof text === 'string') {
|
||||||
|
const words = text.split('\n')
|
||||||
|
addWordList(fileName, words)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[addWordList, wordLists]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const selectedExist = wordLists.includes(selectedWordList)
|
||||||
|
if (!selectedExist) {
|
||||||
|
setSelectedWordList('')
|
||||||
|
}
|
||||||
|
}, [wordLists, selectedWordList])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<strong>Word Lists</strong>
|
||||||
|
<div id="st-rnd-wordlist-controls">
|
||||||
|
<Select
|
||||||
|
id="st-rnd-wordlist-select"
|
||||||
|
disabled={wordLists.length === 0}
|
||||||
|
value={selectedWordList}
|
||||||
|
onChange={(event) => {
|
||||||
|
const value = event.target.value
|
||||||
|
if (value !== '') {
|
||||||
|
setSelectedWordList(value)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
Select a word list
|
||||||
|
</option>
|
||||||
|
{wordLists.map((name) => (
|
||||||
|
<option key={name}>{name}</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<EditWordList currentWordList={selectedWordList} />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
icon
|
||||||
|
disabled={selectedWordList === ''}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedWordList !== '') {
|
||||||
|
const wordList = useStore.getState().wordLists[selectedWordList]
|
||||||
|
if (wordList === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([wordList.join('\n')], {
|
||||||
|
type: 'text/plain'
|
||||||
|
})
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `${selectedWordList}.txt`
|
||||||
|
link.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="fa fa-fw fa-download"></i>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
icon
|
||||||
|
disabled={selectedWordList === ''}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedWordList !== '') {
|
||||||
|
removeWordList(selectedWordList)
|
||||||
|
setSelectedWordList('')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="fa fa-fw fa-trash"></i>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="st-rnd-small-text">
|
||||||
|
Wordlists are txt files with one (or more) word per line, are used in
|
||||||
|
placeholders and macros.
|
||||||
|
</p>
|
||||||
|
<div id="st-rnd-upload-wordlist-container">
|
||||||
|
<UploadButton
|
||||||
|
id="upload-wordlist"
|
||||||
|
onChange={handleUpload}
|
||||||
|
accept=".txt"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WordLists
|
||||||
32
src/components/ui/Button.tsx
Normal file
32
src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { ButtonHTMLAttributes, DetailedHTMLProps, FC } from 'react'
|
||||||
|
import type { MergeOmitting } from '@/types/helpers'
|
||||||
|
import '@/components/ui/button.css'
|
||||||
|
|
||||||
|
export type ButtonProps = MergeOmitting<
|
||||||
|
DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>,
|
||||||
|
{
|
||||||
|
neutral?: boolean
|
||||||
|
icon?: boolean
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
const Button: FC<ButtonProps> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
neutral,
|
||||||
|
icon,
|
||||||
|
...buttonProps
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`st-rnd-button${neutral === true ? ' button-neutral' : ''}${icon === true ? ' button-icon' : ''}${
|
||||||
|
className ? ` ${className}` : ''
|
||||||
|
}`}
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button
|
||||||
24
src/components/ui/Details.tsx
Normal file
24
src/components/ui/Details.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import '@/components/ui/details.css'
|
||||||
|
import type { FC, ReactNode } from 'react'
|
||||||
|
import type { MergeOmitting } from '@/types/helpers'
|
||||||
|
|
||||||
|
export type DetailsProps = MergeOmitting<
|
||||||
|
React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDetailsElement>,
|
||||||
|
HTMLDetailsElement
|
||||||
|
>,
|
||||||
|
{
|
||||||
|
title: string
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
>
|
||||||
|
const Details: FC<DetailsProps> = ({ title, className, children }) => {
|
||||||
|
return (
|
||||||
|
<details className={`st-rnd-details${className ? ` ${className}` : ''}`}>
|
||||||
|
<summary>{title}</summary>
|
||||||
|
<div>{children}</div>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Details
|
||||||
28
src/components/ui/Dialog.tsx
Normal file
28
src/components/ui/Dialog.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import '@/components/ui/dialog.css'
|
||||||
|
import type {
|
||||||
|
DetailedHTMLProps,
|
||||||
|
DialogHTMLAttributes,
|
||||||
|
FC,
|
||||||
|
ReactNode
|
||||||
|
} from 'react'
|
||||||
|
import type { MergeOmitting } from '@/types/helpers'
|
||||||
|
|
||||||
|
export type DialogProps = MergeOmitting<
|
||||||
|
DetailedHTMLProps<DialogHTMLAttributes<HTMLDialogElement>, HTMLDialogElement>,
|
||||||
|
{
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
const Dialog: FC<DialogProps> = ({ children, className, ...dialogProps }) => {
|
||||||
|
return (
|
||||||
|
<dialog
|
||||||
|
className={`st-rnd-dialog${className ? ` ${className}` : ''}`}
|
||||||
|
{...dialogProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dialog
|
||||||
19
src/components/ui/Input.tsx
Normal file
19
src/components/ui/Input.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { DetailedHTMLProps, FC, InputHTMLAttributes } from 'react'
|
||||||
|
import type { MergeOmitting } from '@/types/helpers'
|
||||||
|
import '@/components/ui/input.css'
|
||||||
|
|
||||||
|
export type InputProps = MergeOmitting<
|
||||||
|
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
||||||
|
{ fullWidth?: boolean }
|
||||||
|
>
|
||||||
|
|
||||||
|
const Input: FC<InputProps> = ({ className, ...inputProps }) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className={`st-rnd-input${className ? ` ${className}` : ''}`}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Input
|
||||||
28
src/components/ui/Select.tsx
Normal file
28
src/components/ui/Select.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type {
|
||||||
|
DetailedHTMLProps,
|
||||||
|
FC,
|
||||||
|
ReactNode,
|
||||||
|
SelectHTMLAttributes
|
||||||
|
} from 'react'
|
||||||
|
import type { MergeOmitting } from '@/types/helpers'
|
||||||
|
import '@/components/ui/select.css'
|
||||||
|
|
||||||
|
type SelectProps = MergeOmitting<
|
||||||
|
DetailedHTMLProps<SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>,
|
||||||
|
{
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
const Select: FC<SelectProps> = ({ children, className, ...selectProps }) => {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
className={`st-rnd-select${className ? ` ${className}` : ''}`}
|
||||||
|
{...selectProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</select>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Select
|
||||||
24
src/components/ui/Textarea.tsx
Normal file
24
src/components/ui/Textarea.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { DetailedHTMLProps, FC, TextareaHTMLAttributes } from 'react'
|
||||||
|
import type { MergeOmitting } from '@/types/helpers'
|
||||||
|
import '@/components/ui/textarea.css'
|
||||||
|
|
||||||
|
export type TextareaProps = MergeOmitting<
|
||||||
|
DetailedHTMLProps<
|
||||||
|
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||||
|
HTMLTextAreaElement
|
||||||
|
>,
|
||||||
|
{
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
const Textarea: FC<TextareaProps> = ({ className, ...textareaProps }) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={`st-rnd-textarea${className ? ` ${className}` : ''}`}
|
||||||
|
{...textareaProps}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Textarea
|
||||||
23
src/components/ui/UploadButton.tsx
Normal file
23
src/components/ui/UploadButton.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import '@/components/ui/button.css'
|
||||||
|
import type { DetailedHTMLProps, FC, InputHTMLAttributes } from 'react'
|
||||||
|
import type { MergeOmitting } from '@/types/helpers'
|
||||||
|
|
||||||
|
export type UploadButtonProps = MergeOmitting<
|
||||||
|
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
||||||
|
{
|
||||||
|
id: string
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
const UploadButton: FC<UploadButtonProps> = ({ id, label, ...inputProps }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input type="file" id={id} {...inputProps} />
|
||||||
|
<label htmlFor={id} className={`st-rnd-button button-neutral`}>
|
||||||
|
{label || 'Upload'}
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UploadButton
|
||||||
81
src/components/ui/button.css
Normal file
81
src/components/ui/button.css
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
.st-rnd-button {
|
||||||
|
--btn-background-color: var(--color-primary-3);
|
||||||
|
--btn-background-color-hover: var(--color-primary-4);
|
||||||
|
--btn-background-color-active: var(--color-primary-5);
|
||||||
|
--btn-border-color: var(--color-primary-6);
|
||||||
|
--btn-color: var(--color-primary-12);
|
||||||
|
|
||||||
|
&.button-neutral {
|
||||||
|
--btn-background-color: var(--color-neutral-3);
|
||||||
|
--btn-background-color-hover: var(--color-neutral-4);
|
||||||
|
--btn-background-color-active: var(--color-neutral-5);
|
||||||
|
--btn-border-color: var(--color-neutral-6);
|
||||||
|
--btn-color: var(--color-neutral-12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-rnd-button {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
padding: 4px 8px;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--btn-border-color);
|
||||||
|
background-color: var(--btn-background-color);
|
||||||
|
color: var(--btn-color);
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--btn-background-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--btn-background-color-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-neutral-1) 60%,
|
||||||
|
var(--btn-background-color) 40%
|
||||||
|
);
|
||||||
|
border-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-neutral-1) 60%,
|
||||||
|
var(--btn-border-color) 40%
|
||||||
|
);
|
||||||
|
color: color-mix(in srgb, var(--color-neutral-1) 60%, var(--btn-color) 40%);
|
||||||
|
|
||||||
|
cursor: not-allowed;
|
||||||
|
&:hover {
|
||||||
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-neutral-1) 60%,
|
||||||
|
var(--btn-background-color) 40%
|
||||||
|
);
|
||||||
|
border-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-neutral-1) 60%,
|
||||||
|
var(--btn-border-color) 40%
|
||||||
|
);
|
||||||
|
color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-neutral-1) 60%,
|
||||||
|
var(--btn-color) 40%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.button-icon {
|
||||||
|
padding: 4px;
|
||||||
|
height: auto;
|
||||||
|
width: auto ;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/components/ui/details.css
Normal file
29
src/components/ui/details.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.st-rnd-details {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--color-neutral-6);
|
||||||
|
background-color: var(--color-neutral-3);
|
||||||
|
color: var(--color-neutral-12);
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
summary {
|
||||||
|
background-color: var(--color-neutral-5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> summary {
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-neutral-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/components/ui/dialog.css
Normal file
15
src/components/ui/dialog.css
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
.st-rnd-dialog {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--color-neutral-6);
|
||||||
|
padding: 8px;
|
||||||
|
background-color: var(--color-neutral-1);
|
||||||
|
color: var(--color-neutral-12);
|
||||||
|
|
||||||
|
&::backdrop {
|
||||||
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-neutral-2) 80%,
|
||||||
|
transparent 20%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/components/ui/input.css
Normal file
10
src/components/ui/input.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.st-rnd-input {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--color-neutral-6);
|
||||||
|
padding: 4px 8px;
|
||||||
|
background-color: var(--color-neutral-3);
|
||||||
|
color: var(--color-neutral-12);
|
||||||
|
}
|
||||||
38
src/components/ui/select.css
Normal file
38
src/components/ui/select.css
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
.st-rnd-select {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--color-neutral-6);
|
||||||
|
padding: 4px 8px;
|
||||||
|
background-color: var(--color-neutral-3);
|
||||||
|
color: var(--color-neutral-12);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--color-neutral-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-neutral-1) 60%,
|
||||||
|
var(--color-neutral-3) 40%
|
||||||
|
);
|
||||||
|
color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-neutral-1) 60%,
|
||||||
|
var(--color-neutral-12) 40%
|
||||||
|
);
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-neutral-1) 60%,
|
||||||
|
var(--color-neutral-3) 40%
|
||||||
|
);
|
||||||
|
color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-neutral-1) 60%,
|
||||||
|
var(--color-neutral-12) 40%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/components/ui/textarea.css
Normal file
9
src/components/ui/textarea.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.st-rnd-textarea {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--color-neutral-6);
|
||||||
|
background-color: var(--color-neutral-3);
|
||||||
|
color: var(--color-neutral-12);
|
||||||
|
}
|
||||||
114
src/components/wordList/EditWordList.tsx
Normal file
114
src/components/wordList/EditWordList.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { type FC, useRef, useState } from 'react'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import Dialog from '@/components/ui/Dialog'
|
||||||
|
import { useStore } from '@/store'
|
||||||
|
import '@/components/wordList/editWordlList.css'
|
||||||
|
import Input from '@/components/ui/Input'
|
||||||
|
import Textarea from '@/components/ui/Textarea'
|
||||||
|
|
||||||
|
export interface RenameWordListProps {
|
||||||
|
currentWordList: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditWordList: FC<RenameWordListProps> = ({ currentWordList }) => {
|
||||||
|
const [internalStatus, setInternalStatus] = useState<string>('')
|
||||||
|
const [newName, setNewName] = useState<string>('')
|
||||||
|
const dialogRef = useRef<HTMLDialogElement>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
icon
|
||||||
|
disabled={currentWordList === ''}
|
||||||
|
onClick={() => {
|
||||||
|
if (dialogRef.current === null || currentWordList === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const wordList = useStore.getState().wordLists[currentWordList]
|
||||||
|
if (wordList === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setNewName(currentWordList)
|
||||||
|
setInternalStatus(wordList.join('\n'))
|
||||||
|
dialogRef.current.showModal()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="fa fa-fw fa-pencil"></i>
|
||||||
|
</Button>
|
||||||
|
<Dialog id="st-rnd-wordlist-edit-dialog" ref={dialogRef}>
|
||||||
|
<div>
|
||||||
|
<h2 id="st-rnd-wordlist-edit-dialog-title">
|
||||||
|
Edit Word List: <span>{currentWordList}</span>
|
||||||
|
</h2>
|
||||||
|
<label htmlFor="st-rnd-wordlist-name">Name</label>
|
||||||
|
<p className="st-rnd-small-text">
|
||||||
|
Name of the word list, used in placeholder and macros.
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
id="st-rnd-wordlist-name"
|
||||||
|
type="text"
|
||||||
|
value={newName}
|
||||||
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="st-rnd-wordlist-words">Words</label>
|
||||||
|
<p className="st-rnd-small-text">
|
||||||
|
Words separated by newlines, words can be more than one word!
|
||||||
|
</p>
|
||||||
|
<Textarea
|
||||||
|
id="st-rnd-wordlist-words"
|
||||||
|
value={internalStatus}
|
||||||
|
onChange={(e) => setInternalStatus(e.target.value)}
|
||||||
|
rows={10}
|
||||||
|
/>
|
||||||
|
<div id="st-rnd-wordlist-actions">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
neutral
|
||||||
|
onClick={() => {
|
||||||
|
if (dialogRef.current === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dialogRef.current.close()
|
||||||
|
setInternalStatus('')
|
||||||
|
setNewName('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
currentWordList === '' ||
|
||||||
|
newName === undefined ||
|
||||||
|
internalStatus === undefined ||
|
||||||
|
dialogRef.current === null
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (newName === '') {
|
||||||
|
alert('Name cannot be empty')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const addWordList = useStore.getState().addWordList
|
||||||
|
if (currentWordList !== newName) {
|
||||||
|
const removeWordList = useStore.getState().removeWordList
|
||||||
|
removeWordList(currentWordList)
|
||||||
|
}
|
||||||
|
addWordList(newName, internalStatus.split('\n'))
|
||||||
|
dialogRef.current.close()
|
||||||
|
setInternalStatus('')
|
||||||
|
setNewName('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditWordList
|
||||||
24
src/components/wordList/editWordlList.css
Normal file
24
src/components/wordList/editWordlList.css
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#st-rnd-wordlist-edit-dialog {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
width: clamp(400px, 100%, 1200px);
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
#st-rnd-wordlist-edit-dialog-title {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
color: var(--color-primary-9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#st-rnd-wordlist-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/components/wordLists.css
Normal file
21
src/components/wordLists.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#st-rnd-upload-wordlist-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#st-rnd-wordlist-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
& > select {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
& > button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-rnd-small-text {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: var(--color-neutral-11);
|
||||||
|
}
|
||||||
1
src/constants.ts
Normal file
1
src/constants.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const MODULE_NAME = 'st-randomness-helpers'
|
||||||
27
src/gui.css
Normal file
27
src/gui.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
:root {
|
||||||
|
--color-neutral-1: #111113;
|
||||||
|
--color-neutral-2: #18191b;
|
||||||
|
--color-neutral-3: #212225;
|
||||||
|
--color-neutral-4: #272a2d;
|
||||||
|
--color-neutral-5: #2e3135;
|
||||||
|
--color-neutral-6: #363a3f;
|
||||||
|
--color-neutral-7: #43484e;
|
||||||
|
--color-neutral-8: #5a6169;
|
||||||
|
--color-neutral-9: #696e77;
|
||||||
|
--color-neutral-10: #777b84;
|
||||||
|
--color-neutral-11: #b0b4ba;
|
||||||
|
--color-neutral-12: #edeef0;
|
||||||
|
|
||||||
|
--color-primary-1: #13131e;
|
||||||
|
--color-primary-2: #171625;
|
||||||
|
--color-primary-3: #202248;
|
||||||
|
--color-primary-4: #262a65;
|
||||||
|
--color-primary-5: #303374;
|
||||||
|
--color-primary-6: #3d3e82;
|
||||||
|
--color-primary-7: #4a4a95;
|
||||||
|
--color-primary-8: #5958b1;
|
||||||
|
--color-primary-9: #5b5bd6;
|
||||||
|
--color-primary-10: #6e6ade;
|
||||||
|
--color-primary-11: #b1a9ff;
|
||||||
|
--color-primary-12: #e0dffe;
|
||||||
|
}
|
||||||
24
src/gui.tsx
Normal file
24
src/gui.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import Settings from '@/Settings'
|
||||||
|
import '@/gui.css'
|
||||||
|
|
||||||
|
const renderSettingsGui = () => {
|
||||||
|
const rootContainer = document.getElementById('extensions_settings')
|
||||||
|
|
||||||
|
if (rootContainer === null) {
|
||||||
|
throw new Error('[st-randomness-helpers] root container not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootElement = document.createElement('div')
|
||||||
|
|
||||||
|
rootContainer.appendChild(rootElement)
|
||||||
|
|
||||||
|
ReactDOM.createRoot(rootElement).render(
|
||||||
|
<StrictMode>
|
||||||
|
<Settings />
|
||||||
|
</StrictMode>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default renderSettingsGui
|
||||||
12
src/index.ts
12
src/index.ts
@@ -1,4 +1,10 @@
|
|||||||
console.log('Hello via Bun!')
|
import renderSettingsGui from '@/gui'
|
||||||
|
import initializeMacros from '@/macros/initializeMacros'
|
||||||
|
import initializePlaceholders from '@/placeholders/initializePlaceholders'
|
||||||
|
|
||||||
const context = SillyTavern.getContext()
|
// Initialize the React GUI
|
||||||
console.log(context)
|
renderSettingsGui()
|
||||||
|
// Setup the macros
|
||||||
|
initializeMacros()
|
||||||
|
// Setup the placeholders
|
||||||
|
initializePlaceholders()
|
||||||
|
|||||||
7
src/macros/initializeMacros.ts
Normal file
7
src/macros/initializeMacros.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { setupRandomWordMacros } from '@/services/randomWord'
|
||||||
|
|
||||||
|
const initializeMacros = () => {
|
||||||
|
setupRandomWordMacros()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default initializeMacros
|
||||||
7
src/placeholders/initializePlaceholders.ts
Normal file
7
src/placeholders/initializePlaceholders.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { setupRandomWordPlaceholders } from '@/services/randomWord'
|
||||||
|
|
||||||
|
const initializePlaceholders = () => {
|
||||||
|
setupRandomWordPlaceholders()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default initializePlaceholders
|
||||||
70
src/services/randomWord.ts
Normal file
70
src/services/randomWord.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { useStore } from '@/store'
|
||||||
|
import type { generationData } from '@/types/SillyTavern'
|
||||||
|
|
||||||
|
const registerRandomWordMacros = (dictName: string) => {
|
||||||
|
const { registerMacro } = SillyTavern.getContext()
|
||||||
|
const words = useStore.getState().wordLists[dictName]
|
||||||
|
if (words === undefined || words.length === 0) {
|
||||||
|
// If the word list is empty or undefined, don't register the macro
|
||||||
|
return
|
||||||
|
}
|
||||||
|
registerMacro(
|
||||||
|
`randomWord::${dictName}`,
|
||||||
|
() => {
|
||||||
|
const words = useStore.getState().wordLists[dictName] as string[]
|
||||||
|
return words[Math.floor(Math.random() * words.length)] as string
|
||||||
|
},
|
||||||
|
`Generates a random word from the word list '${dictName}'`
|
||||||
|
)
|
||||||
|
registerMacro(
|
||||||
|
`placeholder::randomWord::${dictName}`,
|
||||||
|
() => {
|
||||||
|
return `%%randomWord::${dictName}%%`
|
||||||
|
},
|
||||||
|
`Returns a placeholder for a random word from the word list '${dictName}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setupRandomWordMacros = () => {
|
||||||
|
for (const wordList of Object.keys(useStore.getState().wordLists)) {
|
||||||
|
registerRandomWordMacros(wordList)
|
||||||
|
}
|
||||||
|
|
||||||
|
useStore.subscribe(
|
||||||
|
(state) => Object.keys(state.wordLists),
|
||||||
|
(wordLists, oldWordLists) => {
|
||||||
|
const wordListsSet = new Set(wordLists)
|
||||||
|
const oldWordListsSet = new Set(oldWordLists)
|
||||||
|
const deletedWordLists = oldWordListsSet.difference(wordListsSet)
|
||||||
|
const newWordLists = wordListsSet.difference(oldWordListsSet)
|
||||||
|
|
||||||
|
const { unregisterMacro } = SillyTavern.getContext()
|
||||||
|
for (const wordList of deletedWordLists) {
|
||||||
|
unregisterMacro(`randomWord::${wordList}`)
|
||||||
|
unregisterMacro(`placeholder::randomWord::${wordList}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const wordList of newWordLists) {
|
||||||
|
registerRandomWordMacros(wordList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const randomWordPlaceholderRegex = /%%randomWord::([\w\W]*?)%%/g
|
||||||
|
|
||||||
|
export const setupRandomWordPlaceholders = () => {
|
||||||
|
const { eventTypes, eventSource } = SillyTavern.getContext()
|
||||||
|
eventSource.on(eventTypes.GENERATE_AFTER_DATA, (chat: generationData) => {
|
||||||
|
chat.prompt = chat.prompt.replaceAll(
|
||||||
|
randomWordPlaceholderRegex,
|
||||||
|
(_, wordList: string) => {
|
||||||
|
const words = useStore.getState().wordLists[wordList]
|
||||||
|
if (words === undefined || words.length === 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return words[Math.floor(Math.random() * words.length)] ?? ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
40
src/store/configSlice.ts
Normal file
40
src/store/configSlice.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { StateCreator } from 'zustand'
|
||||||
|
import { MODULE_NAME } from '@/constants'
|
||||||
|
|
||||||
|
export type WordList = string[]
|
||||||
|
|
||||||
|
export type WordLists = Record<string, string[]>
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
wordLists: WordLists
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigSlice extends Config {
|
||||||
|
addWordList: (name: string, words: string[]) => void
|
||||||
|
removeWordList: (name: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createConfigSlice: StateCreator<ConfigSlice> = (set) => {
|
||||||
|
const context = SillyTavern.getContext()
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: SillyTavern's extensionSettings is not properly typed
|
||||||
|
const extensionSettings: Record<string, any> = context.extensionSettings
|
||||||
|
|
||||||
|
if (extensionSettings[MODULE_NAME] === undefined) {
|
||||||
|
extensionSettings[MODULE_NAME] = {
|
||||||
|
wordLists: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = extensionSettings[MODULE_NAME]
|
||||||
|
|
||||||
|
return {
|
||||||
|
wordLists: settings.wordLists,
|
||||||
|
addWordList: (name, words) =>
|
||||||
|
set({ wordLists: { ...settings.wordLists, [name]: words } }),
|
||||||
|
removeWordList: (name) => {
|
||||||
|
const newWordLists = { ...settings.wordLists }
|
||||||
|
delete newWordLists[name]
|
||||||
|
set({ wordLists: newWordLists })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/store/index.ts
Normal file
19
src/store/index.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import { subscribeWithSelector } from 'zustand/middleware'
|
||||||
|
import { MODULE_NAME } from '@/constants'
|
||||||
|
import { type ConfigSlice, createConfigSlice } from '@/store/configSlice'
|
||||||
|
|
||||||
|
export const useStore = create<ConfigSlice>()(
|
||||||
|
subscribeWithSelector(createConfigSlice)
|
||||||
|
)
|
||||||
|
|
||||||
|
useStore.subscribe(
|
||||||
|
(state) => state.wordLists,
|
||||||
|
(wordLists) => {
|
||||||
|
const context = SillyTavern.getContext()
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: SillyTavern's extensionSettings is not properly typed
|
||||||
|
const extensionSettings: Record<string, any> = context.extensionSettings
|
||||||
|
extensionSettings[MODULE_NAME].wordLists = wordLists
|
||||||
|
context.saveSettingsDebounced()
|
||||||
|
}
|
||||||
|
)
|
||||||
4
src/types/SillyTavern.ts
Normal file
4
src/types/SillyTavern.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface generationData {
|
||||||
|
[key: string]: unknown
|
||||||
|
prompt: string
|
||||||
|
}
|
||||||
5
src/types/helpers.ts
Normal file
5
src/types/helpers.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type MergeOmitting<ReplaceableType, ReplacerType> = Omit<
|
||||||
|
ReplaceableType,
|
||||||
|
keyof ReplacerType
|
||||||
|
> &
|
||||||
|
ReplacerType
|
||||||
@@ -24,7 +24,12 @@
|
|||||||
// Some stricter flags (disabled by default)
|
// Some stricter flags (disabled by default)
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
|
||||||
|
//Paths
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
5
types/global.d.ts
vendored
5
types/global.d.ts
vendored
@@ -1,6 +1,3 @@
|
|||||||
export {}
|
|
||||||
|
|
||||||
import '../../../../../public/global'
|
import '../../../../../public/global'
|
||||||
|
|
||||||
declare global {
|
declare global {}
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user