Compare commits
9 Commits
64af8e6d95
...
9ae8be6a50
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ae8be6a50 | |||
| 9ce947075a | |||
| a3c6c9fa76 | |||
| 9121bb287c | |||
| 25cf8ab255 | |||
| b4e802fdec | |||
| 8481a897dc | |||
| 22d10902fb | |||
| 2c59ce43ad |
19
bun.lock
19
bun.lock
@@ -3,11 +3,18 @@
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "st-randomness-helpers",
|
||||
"dependencies": {
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"zod": "^4.0.17",
|
||||
"zustand": "^5.0.7",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.1.4",
|
||||
"@commitlint/cli": "^19.8.1",
|
||||
"@commitlint/config-conventional": "^19.8.1",
|
||||
"@types/bun": "latest",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"husky": "^9.1.7",
|
||||
"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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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/config-conventional": "^19.8.1",
|
||||
"@types/bun": "latest",
|
||||
"@types/react-dom": "^19.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": {
|
||||
"typescript": "^5"
|
||||
@@ -17,6 +22,6 @@
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
"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()
|
||||
console.log(context)
|
||||
// Initialize the React GUI
|
||||
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)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
|
||||
//Paths
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
5
types/global.d.ts
vendored
5
types/global.d.ts
vendored
@@ -1,6 +1,3 @@
|
||||
export {}
|
||||
|
||||
import '../../../../../public/global'
|
||||
|
||||
declare global {
|
||||
}
|
||||
declare global {}
|
||||
|
||||
Reference in New Issue
Block a user