@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 ADDED
@@ -0,0 +1,2 @@
1
+ export { ThemeContainer } from "./theme.js"
2
+ export { default as Viewer } from "./viewer.js"
package/package.json CHANGED
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "name": "@painekillers/tree-builder-viewer",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }
@@ -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
+ }