jabroni-outfit 1.3.0
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/LICENSE +21 -0
- package/README.md +3 -0
- package/biome.json +35 -0
- package/dist/jabroni-outfit.es.js +11065 -0
- package/dist/jabroni-outfit.umd.js +68 -0
- package/index.html +112 -0
- package/package.json +37 -0
- package/src/example.ts +14 -0
- package/src/index.ts +12 -0
- package/src/store/default-state.ts +33 -0
- package/src/store/persistent-state.ts +36 -0
- package/src/store/store.ts +50 -0
- package/src/store/types.ts +16 -0
- package/src/ui/default-scheme.ts +35 -0
- package/src/ui/index.ts +59 -0
- package/src/ui/scheme-parser.ts +44 -0
- package/src/ui/styles.ts +21 -0
- package/src/ui/types.ts +19 -0
- package/src/utils/index.ts +17 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +23 -0
- package/vite.config.js +30 -0
package/index.html
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Document</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
background: linear-gradient(180deg, black, coral);
|
|
11
|
+
height: 100vh;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
margin: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.codepen-container {
|
|
17
|
+
position: absolute;
|
|
18
|
+
left: 50%;
|
|
19
|
+
top: 50%;
|
|
20
|
+
transform: translate(-50%, -50%);
|
|
21
|
+
width: 60vw;
|
|
22
|
+
height: auto;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.vt323-regular {
|
|
26
|
+
font-family: "VT323", monospace;
|
|
27
|
+
font-weight: 400;
|
|
28
|
+
font-style: normal;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
main * {
|
|
33
|
+
color: #fedfdf;
|
|
34
|
+
text-align: center;
|
|
35
|
+
font-family: "VT323", monospace;
|
|
36
|
+
font-weight: 400;
|
|
37
|
+
margin: 1rem
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
main h1 {
|
|
41
|
+
font-size: 6rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
main h2 {
|
|
45
|
+
font-size: 3rem;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
main a {
|
|
49
|
+
color: darksalmon;
|
|
50
|
+
}
|
|
51
|
+
</style>
|
|
52
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
53
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
54
|
+
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
|
|
55
|
+
<!-- <script src="./dist/jabroni-outfit.umd.js"></script> -->
|
|
56
|
+
<!-- <script type="module" src="./src/example.ts"></script> -->
|
|
57
|
+
<!-- <script src="https://unpkg.com/jabroni-outfit@1.1.0/dist/jabroni-outfit.umd.js"></script> -->
|
|
58
|
+
</head>
|
|
59
|
+
|
|
60
|
+
<body>
|
|
61
|
+
<div class="codepen-container">
|
|
62
|
+
<main>
|
|
63
|
+
<h1>Jabroni Outfit</h1>
|
|
64
|
+
<h2>out-of-the-box gui and persistent-state library based on vue</h2>
|
|
65
|
+
<h3><a href="https://github.com/smartacephale/jabroni-outfit">https://github.com/smartacephale/jabroni-outfit</a>
|
|
66
|
+
</h3>
|
|
67
|
+
<br>
|
|
68
|
+
</main>
|
|
69
|
+
<iframe height="400px" style="width: 100%;" scrolling="no" title="Untitled"
|
|
70
|
+
src="https://codepen.io/smartacephale/embed/bGPawde?default-tab=&editable=true&theme-id=dark" frameborder="no"
|
|
71
|
+
loading="lazy" allowtransparency="true" allowfullscreen="true">
|
|
72
|
+
See the Pen <a href="https://codepen.io/smartacephale/pen/bGPawde">
|
|
73
|
+
Untitled</a> by smartacephale (<a href="https://codepen.io/smartacephale">@smartacephale</a>)
|
|
74
|
+
on <a href="https://codepen.io">CodePen</a>.
|
|
75
|
+
</iframe>
|
|
76
|
+
</div>
|
|
77
|
+
<script>
|
|
78
|
+
const {
|
|
79
|
+
JabroniOutfitStore,
|
|
80
|
+
JabroniOutfitUI,
|
|
81
|
+
defaultStateWithDurationAndPrivacy,
|
|
82
|
+
defaultSchemeWithPrivateFilter
|
|
83
|
+
} = window.jabronioutfit;
|
|
84
|
+
|
|
85
|
+
const myState = {
|
|
86
|
+
gradientColor1: { value: "red", persistent: false, watch: true },
|
|
87
|
+
gradientColor2: { value: "coral", persistent: false, watch: true },
|
|
88
|
+
gradientColor3: { value: "orange", persistent: false, watch: true },
|
|
89
|
+
gradientEnabled: { value: true, persistent: false, watch: true },
|
|
90
|
+
uiEnabled: { value: true, persistent: true, watch: true }
|
|
91
|
+
}
|
|
92
|
+
const store = new JabroniOutfitStore(myState);
|
|
93
|
+
|
|
94
|
+
const ui = new JabroniOutfitUI(store, {
|
|
95
|
+
gradientColor1: [{ type: "text", model: "stateLocale.gradientColor1", placeholder: "color", labelBefore: "color1" }],
|
|
96
|
+
gradientColor2: [{ type: "text", model: "stateLocale.gradientColor2", placeholder: "color", labelBefore: "color2" }],
|
|
97
|
+
gradientColor3: [{ type: "text", model: "stateLocale.gradientColor3", placeholder: "color", labelBefore: "color3" }],
|
|
98
|
+
gradientEnabled: [{ type: "checkbox", model: "stateLocale.gradientEnabled", labelBefore: "gradient enabled" }],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function drawGradient() {
|
|
102
|
+
const { gradientColor1, gradientColor2, gradientColor3, gradientEnabled } = store.stateLocale;
|
|
103
|
+
if (!gradientEnabled) { document.body.style.background = 'coral'; return; }
|
|
104
|
+
document.body.style.background = `radial-gradient(${gradientColor1}, ${gradientColor2}, ${gradientColor3})`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
drawGradient();
|
|
108
|
+
store.subscribe(drawGradient);
|
|
109
|
+
</script>
|
|
110
|
+
</body>
|
|
111
|
+
|
|
112
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jabroni-outfit",
|
|
3
|
+
"description": "out-of-the-box gui and persistent-state library based on vue",
|
|
4
|
+
"version": "1.3.0",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": ["gui", "tweaks", "ui", "persistent-state", "state-management"],
|
|
7
|
+
"author": "smartacephale atm.mormon@protonmail.com (https://github.com/smartacephale)",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"homepage": "https://github.com/smartacephale/jabroni-outfit#readme",
|
|
10
|
+
"main": "./dist/jabroni-outfit.umd.js",
|
|
11
|
+
"module": "./dist/jabroni-outfit.es.js",
|
|
12
|
+
"repository": "github:smartacephale/jabroni-outfit",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/smartacephale/jabroni-outfit/issues",
|
|
15
|
+
"email": "atm.mormon@protonmail.com"
|
|
16
|
+
},
|
|
17
|
+
"browser": "./dist/jabroni-outfit.es.js",
|
|
18
|
+
"unpkg": "./dist/jabroni-outfit.es.js",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./dist/jabroni-outfit.es.js",
|
|
22
|
+
"require": "./dist/jabroni-outfit.umd.cjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"dev": "vite",
|
|
27
|
+
"build": "tsc && vite build",
|
|
28
|
+
"preview": "vite preview"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"typescript": "^5.5.3",
|
|
32
|
+
"vite": "^5.4.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"vue": "^3.4.37"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/example.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defaultStateWithDurationAndPrivacy } from "./store/default-state";
|
|
2
|
+
import { JabroniOutfitStore } from "./store/store";
|
|
3
|
+
import { JabroniOutfitUI } from "./ui";
|
|
4
|
+
import { defaultSchemeWithPrivateFilter } from "./ui/default-scheme";
|
|
5
|
+
|
|
6
|
+
const store = new JabroniOutfitStore(defaultStateWithDurationAndPrivacy);
|
|
7
|
+
const ui = new JabroniOutfitUI(store, defaultSchemeWithPrivateFilter);
|
|
8
|
+
|
|
9
|
+
console.log(store, ui);
|
|
10
|
+
|
|
11
|
+
store.subscribe((subj) => {
|
|
12
|
+
const satisfy = /filter/gi.test(Object.keys(subj)[0]);
|
|
13
|
+
console.log({ ...subj, satisfy });
|
|
14
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { JabroniOutfitStore } from "./store/store";
|
|
2
|
+
export {
|
|
3
|
+
defaultStateWithDurationAndPrivacy,
|
|
4
|
+
defaultStateInclExclMiscPagination,
|
|
5
|
+
defaultStateWithDuration
|
|
6
|
+
} from "./store/default-state";
|
|
7
|
+
export { JabroniOutfitUI } from "./ui";
|
|
8
|
+
export {
|
|
9
|
+
defaultSchemeWithPrivateFilter,
|
|
10
|
+
DefaultScheme,
|
|
11
|
+
extendDefaultScheme
|
|
12
|
+
} from "./ui/default-scheme";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { StateOptions } from "./types";
|
|
2
|
+
|
|
3
|
+
export const stateOptions = {
|
|
4
|
+
EXCLUDE: {
|
|
5
|
+
filterExclude: { value: false, persistent: true, watch: true },
|
|
6
|
+
filterExcludeWords: { value: "", persistent: true, watch: "filterExclude" },
|
|
7
|
+
},
|
|
8
|
+
INCLUDE: {
|
|
9
|
+
filterInclude: { value: false, persistent: true, watch: true },
|
|
10
|
+
filterIncludeWords: { value: "", persistent: true, watch: "filterInclude" },
|
|
11
|
+
},
|
|
12
|
+
MISC: {
|
|
13
|
+
infiniteScrollEnabled: { value: true, persistent: true, watch: true },
|
|
14
|
+
uiEnabled: { value: true, persistent: true, watch: true }
|
|
15
|
+
},
|
|
16
|
+
DURATION_FILTER: {
|
|
17
|
+
filterDuration: { value: false, persistent: true, watch: true },
|
|
18
|
+
filterDurationFrom: { value: 0, type: 'number', persistent: true, watch: 'filterDuration' },
|
|
19
|
+
filterDurationTo: { value: 600, type: 'number', persistent: true, watch: 'filterDuration' }
|
|
20
|
+
},
|
|
21
|
+
PRIVACY_FILTER: {
|
|
22
|
+
filterPrivate: { value: false, persistent: true, watch: true },
|
|
23
|
+
filterPublic: { value: false, persistent: true, watch: true }
|
|
24
|
+
},
|
|
25
|
+
PAGINATION: {
|
|
26
|
+
pagIndexLast: { value: 1, persistent: false, watch: true },
|
|
27
|
+
pagIndexCur: { value: 1, persistent: false, watch: true }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const defaultStateInclExclMiscPagination: StateOptions = Object.assign({}, stateOptions.EXCLUDE, stateOptions.INCLUDE, stateOptions.MISC, stateOptions.PAGINATION);
|
|
32
|
+
export const defaultStateWithDuration: StateOptions = Object.assign({}, defaultStateInclExclMiscPagination, stateOptions.DURATION_FILTER);
|
|
33
|
+
export const defaultStateWithDurationAndPrivacy: StateOptions = Object.assign({}, defaultStateWithDuration, stateOptions.PRIVACY_FILTER);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Reactive, reactive, watch } from 'vue'
|
|
2
|
+
import type { RecordV } from './types';
|
|
3
|
+
|
|
4
|
+
export class PersistentState {
|
|
5
|
+
public state: Reactive<RecordV>;
|
|
6
|
+
|
|
7
|
+
constructor(state_: RecordV, private key = "state_acephale") {
|
|
8
|
+
this.key = key;
|
|
9
|
+
this.state = reactive(state_);
|
|
10
|
+
this.sync();
|
|
11
|
+
this.watchPersistence();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
sync() {
|
|
15
|
+
this.trySetFromLocalStorage();
|
|
16
|
+
window.addEventListener('focus', this.trySetFromLocalStorage);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
watchPersistence() {
|
|
20
|
+
watch(this.state, (value) => {
|
|
21
|
+
this.saveToLocalStorage(this.key, value);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
saveToLocalStorage(key: string, value: RecordV) {
|
|
26
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
trySetFromLocalStorage = () => {
|
|
30
|
+
const localStorageValue = localStorage.getItem(this.key);
|
|
31
|
+
if (localStorageValue !== null) {
|
|
32
|
+
const parsedState = JSON.parse(localStorageValue) as RecordV;
|
|
33
|
+
Object.assign(this.state, parsedState);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Reactive, reactive, watch } from "vue";
|
|
2
|
+
import type { NotifyApply, RecordV, StateOption, StateOptions } from "./types";
|
|
3
|
+
import { PersistentState } from "./persistent-state";
|
|
4
|
+
import { parseIntegerOr } from "../utils";
|
|
5
|
+
import { defaultStateInclExclMiscPagination } from "./default-state";
|
|
6
|
+
|
|
7
|
+
export class JabroniOutfitStore {
|
|
8
|
+
private callbacks: (NotifyApply)[] = [];
|
|
9
|
+
public state: Reactive<RecordV> | undefined;
|
|
10
|
+
public stateLocale: Reactive<RecordV> | undefined;
|
|
11
|
+
|
|
12
|
+
constructor(options: StateOptions = defaultStateInclExclMiscPagination) {
|
|
13
|
+
this.parseState(options);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
subscribe(callback: NotifyApply) {
|
|
17
|
+
this.callbacks.push(callback);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
notify(subject: RecordV) {
|
|
21
|
+
this.callbacks.forEach(cb => cb(subject));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
parseState = (st: StateOptions) => {
|
|
25
|
+
const persistent = {};
|
|
26
|
+
const nonpersistent = {};
|
|
27
|
+
|
|
28
|
+
Object.entries(st).forEach(([k, v]: [string, StateOption]) => {
|
|
29
|
+
((v.persistent ? persistent : nonpersistent) as RecordV)[k] = v.value;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
Object.assign(this, {
|
|
33
|
+
state: new PersistentState(persistent).state,
|
|
34
|
+
stateLocale: reactive(nonpersistent)
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
Object.entries(st).forEach(([k, v]: [string, StateOption]) => {
|
|
38
|
+
if (!v.watch) return;
|
|
39
|
+
const state = (v.persistent ? this.state : this.stateLocale) as Reactive<RecordV>;
|
|
40
|
+
watch(() => state[k], (a, b) => {
|
|
41
|
+
if (v.type === 'number') state[k] = parseIntegerOr(a as string, b as number);
|
|
42
|
+
const isWatchBool = typeof v.watch === 'boolean';
|
|
43
|
+
if (isWatchBool || state[v.watch as string]) {
|
|
44
|
+
const subject = isWatchBool ? k : v.watch as string;
|
|
45
|
+
this.notify({ [subject]: state[subject] } as RecordV);
|
|
46
|
+
}
|
|
47
|
+
}, { deep: true });
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Reactive } from "vue";
|
|
2
|
+
|
|
3
|
+
export interface StateOption {
|
|
4
|
+
value: string | number | boolean;
|
|
5
|
+
persistent: boolean;
|
|
6
|
+
watch: boolean | string;
|
|
7
|
+
type: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type StateOptions = Record<string, StateOption>
|
|
11
|
+
|
|
12
|
+
export type NotifyApply = (input: RecordV) => void;
|
|
13
|
+
|
|
14
|
+
export type RecordV = Record<string, string | number | boolean>;
|
|
15
|
+
|
|
16
|
+
export type StoreState = Reactive<RecordV>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Scheme } from "./types";
|
|
2
|
+
|
|
3
|
+
export const DefaultScheme: Scheme = {
|
|
4
|
+
excludeFilter: [
|
|
5
|
+
{ type: "checkbox", model: "state.filterExclude", label: "exclude" },
|
|
6
|
+
{ type: "text", model: "state.filterExcludeWords", placeholder: "regexp: word, word1|word2, f:full_word..." }
|
|
7
|
+
],
|
|
8
|
+
includeFilter: [
|
|
9
|
+
{ type: "checkbox", model: "state.filterInclude", label: "include" },
|
|
10
|
+
{ type: "text", model: "state.filterIncludeWords", placeholder: "regexp: word, word1|word2, f:full_word..." }
|
|
11
|
+
],
|
|
12
|
+
infiniteScroll: [
|
|
13
|
+
{ type: "checkbox", model: "state.infiniteScrollEnabled", label: "infinite scroll" },
|
|
14
|
+
{ type: "span", innerText: "{{ stateLocale.pagIndexCur }}/{{ stateLocale.pagIndexLast }}", "v-if": "stateLocale.pagIndexLast > 1" }
|
|
15
|
+
],
|
|
16
|
+
durationFilter: [
|
|
17
|
+
{ type: "checkbox", model: "state.filterDuration", label: "duration" },
|
|
18
|
+
{ type: "number", model: "state.filterDurationFrom", step: "10", min: "0", max: "72000", labelBefore: "from" },
|
|
19
|
+
{ type: "number", model: "state.filterDurationTo", step: "10", min: "0", max: "72000", labelBefore: "to" }
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const extendDefaultScheme = (newScheme: Scheme) =>
|
|
24
|
+
Object.entries(DefaultScheme).reduce((acc, [k, v], i) => {
|
|
25
|
+
if (i === 2) Object.assign(acc, newScheme);
|
|
26
|
+
Object.assign(acc, { [k]: v });
|
|
27
|
+
return acc;
|
|
28
|
+
}, {});
|
|
29
|
+
|
|
30
|
+
export const defaultSchemeWithPrivateFilter = extendDefaultScheme({
|
|
31
|
+
privateFilter: [
|
|
32
|
+
{ type: "checkbox", model: "state.filterPrivate", label: "private" },
|
|
33
|
+
{ type: "checkbox", model: "state.filterPublic", label: "public" },
|
|
34
|
+
]
|
|
35
|
+
});
|
package/src/ui/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createApp, reactive } from "vue";
|
|
2
|
+
import { parseDom } from "../utils";
|
|
3
|
+
import { DefaultScheme } from "./default-scheme";
|
|
4
|
+
import { schemeParser } from "./scheme-parser";
|
|
5
|
+
import type { Scheme } from "./types";
|
|
6
|
+
import type { JabroniOutfitStore } from "../store/store";
|
|
7
|
+
|
|
8
|
+
export class JabroniOutfitUI {
|
|
9
|
+
template = (parsed: string) => `
|
|
10
|
+
<div class="fixed bottom-0 right-0 z-9999 rounded bg-zinc-800 max-w-full p-2 m-2"
|
|
11
|
+
:class="state.hidden ? 'hover:bg-gray-600' : ''" v-if="state.uiEnabled">
|
|
12
|
+
|
|
13
|
+
<div class="flex items-center cursor-pointer py-1 px-2 m-1 rounded"
|
|
14
|
+
:class="!state.hidden ? 'hover:bg-zinc-900' : ''" @click="state.hidden = !state.hidden">
|
|
15
|
+
<span class="m-auto flex mono">
|
|
16
|
+
<span v-if="state.hidden && stateLocale.pagIndexLast > 1" class="px-3 py-2 mr-2 bg-gray-700 text-zinc-300 font-mono rounded ">
|
|
17
|
+
{{ stateLocale.pagIndexCur }}/{{ stateLocale.pagIndexLast }}
|
|
18
|
+
</span>
|
|
19
|
+
<svg v-if="state.hidden" class="h-7 w-7 fill-gray-600 stroke-gray-400" xmlns="http://www.w3.org/2000/svg"
|
|
20
|
+
fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
21
|
+
<path strokeLinecap="round" strokeLinejoin="round"
|
|
22
|
+
d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437 1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008Z" />
|
|
23
|
+
</svg>
|
|
24
|
+
<svg v-if="!state.hidden" class="h-7 w-7 fill-gray-600 stroke-gray-400" xmlns="http://www.w3.org/2000/svg"
|
|
25
|
+
fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
26
|
+
<path stroke-linecap="round" stroke-linejoin="round"
|
|
27
|
+
d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
|
28
|
+
</svg>
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<template v-if="!state.hidden">${parsed}</template>
|
|
33
|
+
</div>`;
|
|
34
|
+
|
|
35
|
+
constructor({ state, stateLocale }: JabroniOutfitStore, scheme: Scheme = DefaultScheme) {
|
|
36
|
+
const root = parseDom('<div id="tapermonkey-app" style="position: relative; z-index: 999999;"></div>');
|
|
37
|
+
document.body.appendChild(root);
|
|
38
|
+
|
|
39
|
+
const { parsedScheme, callbacks } = schemeParser(scheme);
|
|
40
|
+
const template = this.template(parsedScheme);
|
|
41
|
+
|
|
42
|
+
const app = createApp({
|
|
43
|
+
setup: () => {
|
|
44
|
+
const cbState = reactive(Object.keys(callbacks).reduce(
|
|
45
|
+
(acc, k) => { acc[k] = false; return acc }, {} as Record<string, boolean>));
|
|
46
|
+
Object.entries(callbacks).forEach(([k, v]) => {
|
|
47
|
+
callbacks[k] = async () => {
|
|
48
|
+
await v();
|
|
49
|
+
cbState[k] = true;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return { state, stateLocale, ...callbacks, cbState };
|
|
53
|
+
},
|
|
54
|
+
template
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
app.mount("#tapermonkey-app");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { componentStyles } from "./styles";
|
|
2
|
+
import type { Scheme, SchemeRowEl } from "./types";
|
|
3
|
+
|
|
4
|
+
export function schemeParser(scheme: Scheme) {
|
|
5
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
6
|
+
const callbacks: Record<string, any> = {};
|
|
7
|
+
|
|
8
|
+
const parseElement = ({ type, model, innerText, label, labelBefore, callback, ...rest }: SchemeRowEl) => {
|
|
9
|
+
if (type === 'button' && !callback) return "";
|
|
10
|
+
const isInput = /checkbox|text|number/.test(type);
|
|
11
|
+
const tag = isInput ? 'input' : type;
|
|
12
|
+
const id = isInput ? model : (Math.random() * 10000000 | 0).toString(16);
|
|
13
|
+
|
|
14
|
+
const props: Record<string, string | undefined> = {
|
|
15
|
+
id,
|
|
16
|
+
"v-model": isInput ? model : "",
|
|
17
|
+
type: isInput ? type : "",
|
|
18
|
+
"@click": callback ? `cb${id}` : "",
|
|
19
|
+
class: componentStyles[type],
|
|
20
|
+
...rest
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (callback) props[":style"] = `{ backgroundColor: cbState.cb${id} ? 'blue' : '' }`
|
|
24
|
+
if (callback) callbacks[`cb${id}`] = callback;
|
|
25
|
+
|
|
26
|
+
const propsToTags = Object.entries(props).filter(([_, v]) => v).map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
27
|
+
|
|
28
|
+
const dom = `<${tag} ${propsToTags}>${innerText || ''}${isInput ? '' : `</${tag}>`}`;
|
|
29
|
+
|
|
30
|
+
let res = dom;
|
|
31
|
+
if (label || labelBefore) {
|
|
32
|
+
const labelStr = `<label for="${props.id}" class="${labelBefore ? componentStyles.labelBefore : componentStyles.label}">${label || labelBefore}</label>`;
|
|
33
|
+
res = (labelBefore ? [labelStr, dom] : [dom, labelStr]).join("\n");
|
|
34
|
+
}
|
|
35
|
+
return res;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parsedScheme = Object.values(scheme).map(row => {
|
|
39
|
+
const parsedRow = row.map(r => parseElement(r)).join("\n");
|
|
40
|
+
return `<div class="${componentStyles.container}">${parsedRow}</div>`;
|
|
41
|
+
}).join("\n");
|
|
42
|
+
|
|
43
|
+
return { parsedScheme, callbacks };
|
|
44
|
+
}
|
package/src/ui/styles.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { GM_addStyle } from "../utils";
|
|
2
|
+
|
|
3
|
+
const style_ = `#tapermonkey-app *,#tapermonkey-app :before,#tapermonkey-app :after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}#tapermonkey-app :before,#tapermonkey-app :after{--tw-content: ""}#tapermonkey-app html,#tapermonkey-app :host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}#tapermonkey-app body{margin:0;line-height:inherit}#tapermonkey-app hr{height:0;color:inherit;border-top-width:1px}#tapermonkey-app abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}#tapermonkey-app h1,#tapermonkey-app h2,#tapermonkey-app h3,#tapermonkey-app h4,#tapermonkey-app h5,#tapermonkey-app h6{font-size:inherit;font-weight:inherit}#tapermonkey-app a{color:inherit;text-decoration:inherit}#tapermonkey-app b,#tapermonkey-app strong{font-weight:bolder}#tapermonkey-app code,#tapermonkey-app kbd,#tapermonkey-app samp,#tapermonkey-app pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}#tapermonkey-app small{font-size:80%}#tapermonkey-app sub,#tapermonkey-app sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}#tapermonkey-app sub{bottom:-.25em}#tapermonkey-app sup{top:-.5em}#tapermonkey-app table{text-indent:0;border-color:inherit;border-collapse:collapse}#tapermonkey-app button,#tapermonkey-app input,#tapermonkey-app optgroup,#tapermonkey-app select,#tapermonkey-app textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}#tapermonkey-app button,#tapermonkey-app select{text-transform:none}#tapermonkey-app button,#tapermonkey-app input:where([type=button]),#tapermonkey-app input:where([type=reset]),#tapermonkey-app input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}#tapermonkey-app :-moz-focusring{outline:auto}#tapermonkey-app :-moz-ui-invalid{box-shadow:none}#tapermonkey-app progress{vertical-align:baseline}#tapermonkey-app ::-webkit-inner-spin-button,#tapermonkey-app ::-webkit-outer-spin-button{height:auto}#tapermonkey-app [type=search]{-webkit-appearance:textfield;outline-offset:-2px}#tapermonkey-app ::-webkit-search-decoration{-webkit-appearance:none}#tapermonkey-app ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}#tapermonkey-app summary{display:list-item}#tapermonkey-app blockquote,#tapermonkey-app dl,#tapermonkey-app dd,#tapermonkey-app h1,#tapermonkey-app h2,#tapermonkey-app h3,#tapermonkey-app h4,#tapermonkey-app h5,#tapermonkey-app h6,#tapermonkey-app hr,#tapermonkey-app figure,#tapermonkey-app p,#tapermonkey-app pre{margin:0}#tapermonkey-app fieldset{margin:0;padding:0}#tapermonkey-app legend{padding:0}#tapermonkey-app ol,#tapermonkey-app ul,#tapermonkey-app menu{list-style:none;margin:0;padding:0}#tapermonkey-app dialog{padding:0}#tapermonkey-app textarea{resize:vertical}#tapermonkey-app input::-moz-placeholder,#tapermonkey-app textarea::-moz-placeholder{opacity:1;color:#9ca3af}#tapermonkey-app input::placeholder,#tapermonkey-app textarea::placeholder{opacity:1;color:#9ca3af}#tapermonkey-app button,#tapermonkey-app [role=button]{cursor:pointer}#tapermonkey-app :disabled{cursor:default}#tapermonkey-app img,#tapermonkey-app svg,#tapermonkey-app video,#tapermonkey-app canvas,#tapermonkey-app audio,#tapermonkey-app iframe,#tapermonkey-app embed,#tapermonkey-app object{display:block;vertical-align:middle}#tapermonkey-app img,#tapermonkey-app video{max-width:100%;height:auto}#tapermonkey-app [hidden]{display:none}#tapermonkey-app *,#tapermonkey-app :before,#tapermonkey-app :after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }#tapermonkey-app ::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }#tapermonkey-app .fixed{position:fixed}#tapermonkey-app .bottom-0{bottom:0}#tapermonkey-app .right-0{right:0}#tapermonkey-app .m-1{margin:.25rem}#tapermonkey-app .m-2{margin:.5rem}#tapermonkey-app .m-auto{margin:auto}#tapermonkey-app .mx-2{margin-left:.5rem;margin-right:.5rem}#tapermonkey-app .ml-2{margin-left:.5rem}#tapermonkey-app .ml-auto{margin-left:auto}#tapermonkey-app .mr-2{margin-right:.5rem}#tapermonkey-app .mr-4{margin-right:1rem}#tapermonkey-app .inline-block{display:inline-block}#tapermonkey-app .flex{display:flex}#tapermonkey-app .hidden{display:none}#tapermonkey-app .size-auto{width:auto;height:auto}#tapermonkey-app .h-7{height:1.75rem}#tapermonkey-app .h-8{height:2rem}#tapermonkey-app .w-24{width:6rem}#tapermonkey-app .w-7{width:1.75rem}#tapermonkey-app .w-full{width:100%}#tapermonkey-app .max-w-full{max-width:100%}#tapermonkey-app .cursor-pointer{cursor:pointer}#tapermonkey-app .items-center{align-items:center}#tapermonkey-app .rounded{border-radius:.25rem}#tapermonkey-app .rounded-sm{border-radius:.125rem}#tapermonkey-app .bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}#tapermonkey-app .bg-zinc-700{--tw-bg-opacity: 1;background-color:rgb(63 63 70 / var(--tw-bg-opacity))}#tapermonkey-app .bg-zinc-800{--tw-bg-opacity: 1;background-color:rgb(39 39 42 / var(--tw-bg-opacity))}#tapermonkey-app .bg-zinc-900{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity))}#tapermonkey-app .fill-gray-600{fill:#4b5563}#tapermonkey-app .stroke-gray-400{stroke:#9ca3af}#tapermonkey-app .p-2{padding:.5rem}#tapermonkey-app .px-2{padding-left:.5rem;padding-right:.5rem}#tapermonkey-app .px-3{padding-left:.75rem;padding-right:.75rem}#tapermonkey-app .py-1{padding-top:.25rem;padding-bottom:.25rem}#tapermonkey-app .py-2{padding-top:.5rem;padding-bottom:.5rem}#tapermonkey-app .font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}#tapermonkey-app .text-zinc-300{--tw-text-opacity: 1;color:rgb(212 212 216 / var(--tw-text-opacity))}#tapermonkey-app .accent-gray-700{accent-color:#374151}#tapermonkey-app .outline-none{outline:2px solid transparent;outline-offset:2px}#tapermonkey-app .invert{--tw-invert: invert(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.checked\:invert-0:checked{--tw-invert: invert(0);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:bg-gray-600:hover{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity))}.hover\:bg-zinc-600:hover{--tw-bg-opacity: 1;background-color:rgb(82 82 91 / var(--tw-bg-opacity))}.hover\:bg-zinc-900:hover{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity))}.focus\:outline-gray-600:focus{outline-color:#4b5563}`;
|
|
4
|
+
GM_addStyle(style_);
|
|
5
|
+
|
|
6
|
+
GM_addStyle(`
|
|
7
|
+
#tapermonkey-app .flex.items-center:first-child:hover { background: #3741512b; }
|
|
8
|
+
#tapermonkey-app input[type="text"]:hover { background: #52525b; }
|
|
9
|
+
#tapermonkey-app input[type="number"]:hover { background: #4b5563; }
|
|
10
|
+
#tapermonkey-app button:hover { background: #4b5563; }`);
|
|
11
|
+
|
|
12
|
+
export const componentStyles = {
|
|
13
|
+
container: "flex items-center bg-zinc-900 py-2 px-2 m-1 font-mono rounded",
|
|
14
|
+
text: "w-full h-8 text-zinc-300 px-3 py-2 mx-2 rounded-sm bg-zinc-700 outline-none focus:outline-gray-600 hover:bg-zinc-600",
|
|
15
|
+
checkbox: "mx-2 size-auto invert checked:invert-0 accent-gray-700",
|
|
16
|
+
number: "w-24 h-8 text-zinc-300 rounded px-3 py-2 bg-gray-700 hover:bg-gray-600 outline-none focus:outline-gray-600",
|
|
17
|
+
button: "mx-2 size-auto text-zinc-300 rounded px-3 py-2 bg-gray-700 hover:bg-gray-600 ml-auto",
|
|
18
|
+
span: "text-zinc-300 ml-auto mr-4",
|
|
19
|
+
label: "text-zinc-300 flex font-mono",
|
|
20
|
+
labelBefore: "text-zinc-300 mx-2 font-mono",
|
|
21
|
+
}
|
package/src/ui/types.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface SchemeRowEl {
|
|
2
|
+
type: 'checkbox' | 'text' | 'number' | 'span' | 'button';
|
|
3
|
+
model?: string,
|
|
4
|
+
label?: string,
|
|
5
|
+
labelBefore?: string,
|
|
6
|
+
innerText?: string,
|
|
7
|
+
max?: string,
|
|
8
|
+
min?: string,
|
|
9
|
+
step?: string,
|
|
10
|
+
"v-if"?: string,
|
|
11
|
+
placeholder?: string
|
|
12
|
+
callback?: () => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type SchemeRow = SchemeRowEl[];
|
|
16
|
+
|
|
17
|
+
export interface Scheme {
|
|
18
|
+
[key: string]: SchemeRow
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function parseIntegerOr(n: string, or: number) {
|
|
2
|
+
return Number.isInteger(Number.parseInt(n)) ? Number.parseInt(n) : or;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function GM_addStyle(css: string) {
|
|
6
|
+
const head = document.getElementsByTagName('head')[0];
|
|
7
|
+
if (!head) { return; }
|
|
8
|
+
const style = document.createElement('style');
|
|
9
|
+
style.type = 'text/css';
|
|
10
|
+
style.innerHTML = css.replace(/;/g, ' !important;');
|
|
11
|
+
head.appendChild(style);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseDom(html: string): HTMLElement {
|
|
15
|
+
const parsed = new DOMParser().parseFromString(html, 'text/html').body;
|
|
16
|
+
return parsed.children.length > 1 ? parsed : parsed.firstElementChild as HTMLElement;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
/* Linting */
|
|
17
|
+
"strict": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true
|
|
21
|
+
},
|
|
22
|
+
"include": ["src", "main.js"]
|
|
23
|
+
}
|
package/vite.config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
|
|
4
|
+
export default ({ mode }) => {
|
|
5
|
+
return defineConfig({
|
|
6
|
+
define: {
|
|
7
|
+
"process.env": {},
|
|
8
|
+
__VUE_OPTIONS_API__: false,
|
|
9
|
+
__VUE_PROD_DEVTOOLS__: false,
|
|
10
|
+
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
|
|
11
|
+
},
|
|
12
|
+
run: {
|
|
13
|
+
entry: path.resolve(__dirname, "./src/index.html")
|
|
14
|
+
},
|
|
15
|
+
resolve: {
|
|
16
|
+
alias: {
|
|
17
|
+
vue: 'vue/dist/vue.esm-bundler.js',
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
build: {
|
|
21
|
+
watch: false,
|
|
22
|
+
lib: {
|
|
23
|
+
minify: true,
|
|
24
|
+
entry: path.resolve(__dirname, "./src/index.ts"),
|
|
25
|
+
name: "jabronioutfit",
|
|
26
|
+
fileName: (format) => `jabroni-outfit.${format}.js`,
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
};
|