@painekillers/tree-builder-viewer 0.0.0-stage → 1.0.1
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/index.js +2 -0
- package/package.json +9 -3
- package/search.js +120 -0
- package/theme.js +175 -0
- package/themes/dark.css +144 -0
- package/themes/default.css +488 -0
- package/tooltip.js +78 -0
- package/viewer.js +752 -0
- package/README.md +0 -3
package/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@painekillers/tree-builder-viewer",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./index.js",
|
|
7
|
+
"./themes/*": "./themes/*"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@painekillers/tree-builder-core": "^1.0.0"
|
|
11
|
+
}
|
|
6
12
|
}
|
package/search.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} SearchEntry
|
|
3
|
+
* @property {string} name
|
|
4
|
+
* @property {*} value
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export default class SearchIndex {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {SearchEntry[]} entries
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
constructor(entries) {
|
|
14
|
+
this.entries = entries
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
add(entry) {
|
|
18
|
+
this.entries.push(entry)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#jaroWinkler(a, b) {
|
|
22
|
+
if(a === b) return 1
|
|
23
|
+
|
|
24
|
+
if(!a.length || !b.length) return 0
|
|
25
|
+
|
|
26
|
+
const distance = Math.floor(Math.max(a.length, b.length) / 2) - 1
|
|
27
|
+
|
|
28
|
+
const aMatches = new Array(a.length).fill(false)
|
|
29
|
+
const bMatches = new Array(b.length).fill(false)
|
|
30
|
+
|
|
31
|
+
let matches = 0
|
|
32
|
+
|
|
33
|
+
for(let i = 0; i < a.length; i++) {
|
|
34
|
+
const start = Math.max(0, i - distance)
|
|
35
|
+
const end = Math.min(i + distance + 1, b.length)
|
|
36
|
+
|
|
37
|
+
for(let j = start; j < end; j++) {
|
|
38
|
+
if(bMatches[j] || a[i] !== b[j]) continue
|
|
39
|
+
|
|
40
|
+
aMatches[i] = true
|
|
41
|
+
bMatches[j] = true
|
|
42
|
+
matches++
|
|
43
|
+
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if(matches === 0) return 0
|
|
49
|
+
|
|
50
|
+
let k = 0
|
|
51
|
+
let transpositions = 0
|
|
52
|
+
|
|
53
|
+
for(let i = 0; i < a.length; i++) {
|
|
54
|
+
if(!aMatches[i]) continue
|
|
55
|
+
|
|
56
|
+
while(!bMatches[k]) {
|
|
57
|
+
k++
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if(a[i] !== b[k]) {
|
|
61
|
+
transpositions++
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
k++
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const jaro =
|
|
68
|
+
(
|
|
69
|
+
matches / a.length +
|
|
70
|
+
matches / b.length +
|
|
71
|
+
(matches - transpositions / 2) / matches
|
|
72
|
+
) / 3
|
|
73
|
+
|
|
74
|
+
const prefixLength = Math.min(4, a.length, b.length)
|
|
75
|
+
|
|
76
|
+
let prefix = 0
|
|
77
|
+
|
|
78
|
+
while(prefix < prefixLength && a[prefix] === b[prefix]) {
|
|
79
|
+
prefix++
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return jaro + prefix * 0.1 * (1 - jaro)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#score(entry, query) {
|
|
86
|
+
const a = query.toLowerCase()
|
|
87
|
+
const b = entry.name.toLowerCase()
|
|
88
|
+
|
|
89
|
+
if (a === b) return 1
|
|
90
|
+
|
|
91
|
+
let score = this.#jaroWinkler(a, b)
|
|
92
|
+
|
|
93
|
+
if (b.startsWith(a)) {
|
|
94
|
+
score += 0.15
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (b.length < a.length) {
|
|
98
|
+
const difference = a.length - b.length
|
|
99
|
+
score -= difference * 0.03
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return Math.max(0, Math.min(score, 0.99))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
search(query, limit = 10, minScore = 0.5) {
|
|
106
|
+
const results = []
|
|
107
|
+
|
|
108
|
+
for (const entry of this.entries) {
|
|
109
|
+
const score = this.#score(entry, query)
|
|
110
|
+
|
|
111
|
+
if (score < minScore) continue
|
|
112
|
+
|
|
113
|
+
results.push({ entry, score })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
results.sort((a, b) => b.score - a.score)
|
|
117
|
+
|
|
118
|
+
return results.slice(0, limit)
|
|
119
|
+
}
|
|
120
|
+
}
|
package/theme.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const loadTheme = async path =>
|
|
2
|
+
(await import(path)).default
|
|
3
|
+
|
|
4
|
+
const merge = (base, theme) => {
|
|
5
|
+
const result = {...base}
|
|
6
|
+
|
|
7
|
+
for (const [key, value] of Object.entries(theme)) {
|
|
8
|
+
if (
|
|
9
|
+
value &&
|
|
10
|
+
typeof value === "object" &&
|
|
11
|
+
!Array.isArray(value) &&
|
|
12
|
+
value.constructor === Object &&
|
|
13
|
+
base[key]?.constructor === Object
|
|
14
|
+
) {
|
|
15
|
+
result[key] = merge(
|
|
16
|
+
base[key],
|
|
17
|
+
value
|
|
18
|
+
)
|
|
19
|
+
} else {
|
|
20
|
+
result[key] = value
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return result
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const loadBuiltInTheme = async name => {
|
|
28
|
+
const path = new URL(
|
|
29
|
+
`./themes/${name.toLowerCase()}.css`,
|
|
30
|
+
import.meta.url
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
theme: await loadTheme(
|
|
35
|
+
`@painekillers/tree-builder-core/themes/${name.toLowerCase()}.js`
|
|
36
|
+
),
|
|
37
|
+
css: path.href
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const defaultTheme =
|
|
42
|
+
await loadBuiltInTheme("Default")
|
|
43
|
+
|
|
44
|
+
export class ThemeContainer {
|
|
45
|
+
#themes = {
|
|
46
|
+
Default: defaultTheme
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async load(name, jsPath, cssPath) {
|
|
50
|
+
if (
|
|
51
|
+
jsPath === undefined &&
|
|
52
|
+
cssPath === undefined
|
|
53
|
+
) {
|
|
54
|
+
const builtIn =
|
|
55
|
+
await loadBuiltInTheme(name)
|
|
56
|
+
|
|
57
|
+
this.#themes[name] = {
|
|
58
|
+
theme: merge(
|
|
59
|
+
defaultTheme.theme,
|
|
60
|
+
builtIn.theme
|
|
61
|
+
),
|
|
62
|
+
css: builtIn.css
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
jsPath === undefined ||
|
|
70
|
+
cssPath === undefined
|
|
71
|
+
) {
|
|
72
|
+
console.warn(
|
|
73
|
+
`Theme ${name} requires both a JS path and a CSS path`
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.#themes[name] = {
|
|
80
|
+
theme: merge(
|
|
81
|
+
defaultTheme.theme,
|
|
82
|
+
await loadTheme(jsPath)
|
|
83
|
+
),
|
|
84
|
+
css: cssPath
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get(name) {
|
|
89
|
+
return this.#themes[name]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get themes() {
|
|
93
|
+
return Object.fromEntries(
|
|
94
|
+
Object.entries(this.#themes)
|
|
95
|
+
.map(([name, value]) => [
|
|
96
|
+
name,
|
|
97
|
+
value.theme
|
|
98
|
+
])
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class ThemeApplier {
|
|
104
|
+
#container
|
|
105
|
+
#shadow
|
|
106
|
+
|
|
107
|
+
#current
|
|
108
|
+
|
|
109
|
+
#defaultCSS
|
|
110
|
+
#css
|
|
111
|
+
|
|
112
|
+
#subscription = []
|
|
113
|
+
|
|
114
|
+
constructor(container, shadow) {
|
|
115
|
+
this.#container = container
|
|
116
|
+
this.#shadow = shadow
|
|
117
|
+
|
|
118
|
+
const defaultTheme =
|
|
119
|
+
this.#container.get("Default")
|
|
120
|
+
|
|
121
|
+
this.#defaultCSS =
|
|
122
|
+
document.createElement("link")
|
|
123
|
+
|
|
124
|
+
this.#defaultCSS.rel = "stylesheet"
|
|
125
|
+
this.#defaultCSS.href =
|
|
126
|
+
defaultTheme.css
|
|
127
|
+
|
|
128
|
+
this.#css =
|
|
129
|
+
document.createElement("link")
|
|
130
|
+
|
|
131
|
+
this.#css.rel = "stylesheet"
|
|
132
|
+
this.#css.href =
|
|
133
|
+
defaultTheme.css
|
|
134
|
+
|
|
135
|
+
this.#shadow.append(
|
|
136
|
+
this.#defaultCSS,
|
|
137
|
+
this.#css
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
this.#current =
|
|
141
|
+
defaultTheme.theme
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async apply(name) {
|
|
145
|
+
const theme =
|
|
146
|
+
this.#container.get(name)
|
|
147
|
+
|
|
148
|
+
if (!theme) {
|
|
149
|
+
console.warn(
|
|
150
|
+
`Could not find theme with name ${name}`
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.#current = theme.theme
|
|
157
|
+
this.#css.href = theme.css
|
|
158
|
+
|
|
159
|
+
for (const func of this.#subscription) {
|
|
160
|
+
func()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
get current() {
|
|
165
|
+
return this.#current
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get container() {
|
|
169
|
+
return this.#container
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
subscribe(func) {
|
|
173
|
+
this.#subscription.push(func)
|
|
174
|
+
}
|
|
175
|
+
}
|
package/themes/dark.css
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
body {
|
|
2
|
+
color: #e4e5e7;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
#search-button {
|
|
6
|
+
border-color: #3a3b3e;
|
|
7
|
+
background: #2d2e31;
|
|
8
|
+
color: #e4e5e7;
|
|
9
|
+
|
|
10
|
+
box-shadow:
|
|
11
|
+
0 2px 4px rgba(0, 0, 0, 0.25);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#search-button:hover {
|
|
15
|
+
box-shadow:
|
|
16
|
+
0 4px 16px rgba(0, 0, 0, 0.35);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#search-menu {
|
|
20
|
+
border-color: #3a3b3e;
|
|
21
|
+
background: #2d2e31;
|
|
22
|
+
|
|
23
|
+
box-shadow:
|
|
24
|
+
0 8px 32px rgba(0, 0, 0, 0.35);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.menu-header button {
|
|
28
|
+
color: #e4e5e7;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.menu-header button:hover {
|
|
32
|
+
background: rgba(255, 255, 255, 0.06);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#search-input {
|
|
36
|
+
border-color: #3a3b3e;
|
|
37
|
+
background: #242528;
|
|
38
|
+
color: #e4e5e7;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#search-input::placeholder {
|
|
42
|
+
color: #77797d;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.search-entry {
|
|
46
|
+
border-color: #3a3b3e;
|
|
47
|
+
background: #242528;
|
|
48
|
+
|
|
49
|
+
box-shadow:
|
|
50
|
+
0 2px 8px rgba(0, 0, 0, 0.2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.search-entry:hover {
|
|
54
|
+
box-shadow:
|
|
55
|
+
0 4px 16px rgba(0, 0, 0, 0.3);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.search-entry-actions button {
|
|
59
|
+
border-color: #3a3b3e;
|
|
60
|
+
background: #2d2e31;
|
|
61
|
+
color: #e4e5e7;
|
|
62
|
+
|
|
63
|
+
box-shadow:
|
|
64
|
+
0 2px 6px rgba(0, 0, 0, 0.2);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.search-entry-actions button:hover:not(:disabled) {
|
|
68
|
+
background: #343539;
|
|
69
|
+
|
|
70
|
+
box-shadow:
|
|
71
|
+
0 4px 12px rgba(0, 0, 0, 0.3);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.search-entry-alias {
|
|
75
|
+
color: #888a8e;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#tooltip {
|
|
79
|
+
border-color: #3a3b3e;
|
|
80
|
+
background: #2d2e31;
|
|
81
|
+
color: #e4e5e7;
|
|
82
|
+
|
|
83
|
+
box-shadow:
|
|
84
|
+
0 4px 14px rgba(0, 0, 0, 0.3);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#tooltip::after {
|
|
88
|
+
border-color: #3a3b3e;
|
|
89
|
+
background: #2d2e31;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#theme-button {
|
|
93
|
+
border-color: #3a3b3e;
|
|
94
|
+
background: #2d2e31;
|
|
95
|
+
color: #e4e5e7;
|
|
96
|
+
|
|
97
|
+
box-shadow:
|
|
98
|
+
0 2px 4px rgba(0, 0, 0, 0.25);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#theme-button:hover {
|
|
102
|
+
box-shadow:
|
|
103
|
+
0 4px 16px rgba(0, 0, 0, 0.35);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#theme-menu {
|
|
107
|
+
border-color: #3a3b3e;
|
|
108
|
+
background: #2d2e31;
|
|
109
|
+
|
|
110
|
+
box-shadow:
|
|
111
|
+
0 8px 32px rgba(0, 0, 0, 0.35);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.theme-entry {
|
|
115
|
+
border-color: #3a3b3e;
|
|
116
|
+
color: #e4e5e7;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.theme-entry:hover {
|
|
120
|
+
border-color: #5a5c62;
|
|
121
|
+
|
|
122
|
+
box-shadow:
|
|
123
|
+
0 4px 12px rgba(0, 0, 0, 0.3);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.theme-entry.selected {
|
|
127
|
+
border-color: #6a6c72;
|
|
128
|
+
|
|
129
|
+
box-shadow:
|
|
130
|
+
0 0 0 2px rgba(255, 255, 255, 0.08);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.theme-name {
|
|
134
|
+
background: #242528;
|
|
135
|
+
color: #e4e5e7;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#node-menu {
|
|
139
|
+
border-color: #3a3b3e;
|
|
140
|
+
background: #2d2e31;
|
|
141
|
+
|
|
142
|
+
box-shadow:
|
|
143
|
+
0 8px 32px rgba(0, 0, 0, 0.35);
|
|
144
|
+
}
|