@teipublisher/pb-components 2.26.1-next.3 → 3.0.0-next-4.2

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.
Files changed (262) hide show
  1. package/.github/workflows/docker-cypress.yml +53 -0
  2. package/.github/workflows/node.js.yml +70 -21
  3. package/.releaserc.json +7 -2
  4. package/CHANGELOG.md +363 -11
  5. package/Dockerfile +78 -70
  6. package/README.md +112 -4
  7. package/css/components.css +5 -5
  8. package/css/gridjs/mermaid.min.css +1 -1
  9. package/css/leaflet/Control.Geocoder.css +1 -126
  10. package/css/leaflet/images/layers.png +0 -0
  11. package/css/tify/tify.css +6 -5
  12. package/css/tom-select/tom-select.bootstrap4.min.css +1 -1
  13. package/css/tom-select/tom-select.bootstrap5.min.css +1 -1
  14. package/css/tom-select/tom-select.default.min.css +1 -1
  15. package/css/tom-select/tom-select.default.min.css.map +1 -0
  16. package/css/tom-select/tom-select.min.css +1 -1
  17. package/cypress.config.js +84 -0
  18. package/dist/api.html +1 -1
  19. package/dist/css/design-system.css +607 -0
  20. package/dist/demo/bundle-test.html +4 -3
  21. package/dist/demo/components.css +46 -1
  22. package/dist/demo/design-system.html +710 -0
  23. package/dist/demo/dts-client.html +2 -2
  24. package/dist/demo/pb-autocomplete.html +23 -11
  25. package/dist/demo/pb-autocomplete2.html +66 -55
  26. package/dist/demo/pb-autocomplete3.html +17 -8
  27. package/dist/demo/pb-blacklab-highlight.html +28 -11
  28. package/dist/demo/pb-blacklab-results.html +3 -2
  29. package/dist/demo/pb-browse-docs.html +24 -24
  30. package/dist/demo/pb-browse-docs2.html +3 -3
  31. package/dist/demo/pb-clipboard.html +32 -28
  32. package/dist/demo/pb-code-editor.html +6 -6
  33. package/dist/demo/pb-code-highlight.html +63 -63
  34. package/dist/demo/pb-codepen.html +1 -1
  35. package/dist/demo/pb-collapse.html +1 -1
  36. package/dist/demo/pb-collapse2.html +2 -2
  37. package/dist/demo/pb-combo-box.html +135 -130
  38. package/dist/demo/pb-custom-form.html +64 -55
  39. package/dist/demo/pb-dialog.html +12 -6
  40. package/dist/demo/pb-document.html +1 -1
  41. package/dist/demo/pb-download.html +68 -59
  42. package/dist/demo/pb-drawer.html +67 -46
  43. package/dist/demo/pb-drawer2.html +65 -58
  44. package/dist/demo/pb-edit-app.html +2 -2
  45. package/dist/demo/pb-edit-xml.html +1 -1
  46. package/dist/demo/pb-facsimile-2.html +26 -11
  47. package/dist/demo/pb-facsimile-3.html +25 -10
  48. package/dist/demo/pb-facsimile-dedup-test-2.html +48 -0
  49. package/dist/demo/pb-facsimile-dedup-test.html +48 -0
  50. package/dist/demo/pb-facsimile.html +4 -4
  51. package/dist/demo/pb-formula.html +1 -1
  52. package/dist/demo/pb-grid.html +22 -8
  53. package/dist/demo/pb-highlight.html +2 -2
  54. package/dist/demo/pb-i18n-simple.html +1 -0
  55. package/dist/demo/pb-i18n.html +15 -5
  56. package/dist/demo/pb-image-strip-standalone.html +2 -2
  57. package/dist/demo/pb-image-strip-view.html +2 -2
  58. package/dist/demo/pb-leaflet-map.html +3 -3
  59. package/dist/demo/pb-leaflet-map2.html +2 -2
  60. package/dist/demo/pb-leaflet-map3.html +3 -3
  61. package/dist/demo/pb-link.html +1 -1
  62. package/dist/demo/pb-load.html +2 -6
  63. package/dist/demo/pb-login.html +1 -3
  64. package/dist/demo/pb-manage-odds.html +9 -4
  65. package/dist/demo/pb-markdown.html +1 -1
  66. package/dist/demo/pb-media-query.html +2 -2
  67. package/dist/demo/pb-mei.html +2 -2
  68. package/dist/demo/pb-mei2.html +2 -2
  69. package/dist/demo/pb-message.html +2 -3
  70. package/dist/demo/pb-odd-editor.html +54 -52
  71. package/dist/demo/pb-page-header.html +27 -0
  72. package/dist/demo/pb-popover.html +1 -1
  73. package/dist/demo/pb-print-preview.html +2 -2
  74. package/dist/demo/pb-progress.html +4 -4
  75. package/dist/demo/pb-repeat.html +32 -36
  76. package/dist/demo/pb-search.html +16 -5
  77. package/dist/demo/pb-search2.html +4 -4
  78. package/dist/demo/pb-search3.html +3 -3
  79. package/dist/demo/pb-search4.html +3 -3
  80. package/dist/demo/pb-select-feature.html +4 -4
  81. package/dist/demo/pb-select-feature2.html +4 -4
  82. package/dist/demo/pb-select-feature3.html +2 -2
  83. package/dist/demo/pb-select-i18n.html +58 -53
  84. package/dist/demo/pb-select-odd.html +1 -1
  85. package/dist/demo/pb-select.html +190 -75
  86. package/dist/demo/pb-select2.html +91 -37
  87. package/dist/demo/pb-select3.html +109 -41
  88. package/dist/demo/pb-svg.html +1 -1
  89. package/dist/demo/pb-table-grid.html +26 -15
  90. package/dist/demo/pb-tabs.html +15 -7
  91. package/dist/demo/pb-tify.html +7 -7
  92. package/dist/demo/pb-timeline.html +1 -1
  93. package/dist/demo/pb-timeline2.html +1 -1
  94. package/dist/demo/pb-toggle-feature.html +26 -23
  95. package/dist/demo/pb-toggle-feature2.html +4 -4
  96. package/dist/demo/pb-toggle-feature3.html +2 -2
  97. package/dist/demo/pb-toggle-feature4.html +56 -54
  98. package/dist/demo/pb-version.html +2 -2
  99. package/dist/demo/pb-view.html +78 -40
  100. package/dist/demo/pb-view2.html +69 -46
  101. package/dist/demo/pb-view3.html +53 -48
  102. package/dist/demo/pb-view4.html +70 -49
  103. package/dist/demo/pb-zoom.html +2 -2
  104. package/dist/{es-global-bridge-d8ce175d.js → es-global-bridge-D8ZcUcx_.js} +0 -4
  105. package/dist/focus-mixin-VCsFap6b.js +768 -0
  106. package/dist/images/icons.svg +217 -0
  107. package/dist/jinn-codemirror-DETLdm08.js +1 -0
  108. package/dist/lib/openseadragon.min.js +80 -0
  109. package/dist/lib/openseadragon.min.js.map +1 -0
  110. package/dist/pb-code-editor.js +25 -20
  111. package/dist/pb-component-docs.js +414 -3225
  112. package/dist/pb-components-bundle.js +3046 -4402
  113. package/dist/pb-dialog-tklYGWfc.js +121 -0
  114. package/dist/pb-edit-app.js +208 -107
  115. package/dist/pb-elements.json +716 -249
  116. package/dist/pb-facsimile.js +46 -0
  117. package/dist/pb-i18n-C0NDma4h.js +1 -0
  118. package/dist/pb-leaflet-map.js +23 -23
  119. package/dist/pb-mei.js +152 -134
  120. package/dist/pb-mixin-DHoWQheB.js +1 -0
  121. package/dist/pb-odd-editor.js +1671 -1231
  122. package/dist/pb-tify.js +1 -27
  123. package/dist/unsafe-html-D5VGo9Oq.js +1 -0
  124. package/dist/urls-BEONu_g4.js +1 -0
  125. package/eslint.config.mjs +92 -0
  126. package/gh-pages.js +5 -3
  127. package/i18n/common/en.json +6 -0
  128. package/i18n/common/pl.json +2 -2
  129. package/images/icons.svg +217 -0
  130. package/index.html +0 -5
  131. package/lib/leaflet-src.js.map +1 -0
  132. package/lib/leaflet.markercluster-src.js.map +1 -0
  133. package/lib/openseadragon.min.js +6 -6
  134. package/package.json +56 -81
  135. package/pb-elements.json +716 -249
  136. package/rollup.config.mjs +312 -0
  137. package/src/assets/components.css +5 -5
  138. package/src/assets/design-system.css +607 -0
  139. package/src/authority/airtable.js +20 -21
  140. package/src/authority/anton.js +129 -129
  141. package/src/authority/custom.js +70 -27
  142. package/src/authority/geonames.js +38 -32
  143. package/src/authority/gnd.js +50 -42
  144. package/src/authority/kbga.js +136 -134
  145. package/src/authority/metagrid.js +44 -46
  146. package/src/authority/reconciliation.js +66 -68
  147. package/src/authority/registry.js +4 -4
  148. package/src/docs/demo-utils.js +91 -0
  149. package/src/docs/pb-component-docs.js +287 -147
  150. package/src/docs/pb-component-view.js +380 -273
  151. package/src/docs/pb-components-list.js +115 -51
  152. package/src/docs/pb-demo-snippet.js +199 -174
  153. package/src/dts-client.js +306 -303
  154. package/src/dts-select-endpoint.js +125 -85
  155. package/src/parse-date-service.js +184 -135
  156. package/src/pb-ajax.js +175 -173
  157. package/src/pb-authority-lookup.js +198 -158
  158. package/src/pb-autocomplete.js +731 -313
  159. package/src/pb-blacklab-highlight.js +266 -260
  160. package/src/pb-blacklab-results.js +230 -225
  161. package/src/pb-browse-docs.js +601 -484
  162. package/src/pb-browse.js +68 -65
  163. package/src/pb-clipboard.js +97 -76
  164. package/src/pb-code-editor.js +111 -103
  165. package/src/pb-code-highlight.js +234 -204
  166. package/src/pb-codepen.js +81 -73
  167. package/src/pb-collapse.js +265 -152
  168. package/src/pb-combo-box.js +191 -191
  169. package/src/pb-components-bundle.js +1 -7
  170. package/src/pb-components.js +2 -6
  171. package/src/pb-custom-form.js +230 -141
  172. package/src/pb-dialog.js +99 -63
  173. package/src/pb-document.js +118 -91
  174. package/src/pb-download.js +214 -198
  175. package/src/pb-drawer.js +146 -149
  176. package/src/pb-edit-app.js +471 -240
  177. package/src/pb-edit-xml.js +101 -98
  178. package/src/pb-events.js +126 -107
  179. package/src/pb-facs-link.js +130 -101
  180. package/src/pb-facsimile.js +494 -410
  181. package/src/pb-fetch.js +389 -0
  182. package/src/pb-formula.js +152 -154
  183. package/src/pb-geolocation.js +130 -132
  184. package/src/pb-grid-action.js +59 -56
  185. package/src/pb-grid.js +388 -228
  186. package/src/pb-highlight.js +142 -142
  187. package/src/pb-hotkeys.js +40 -42
  188. package/src/pb-i18n.js +115 -127
  189. package/src/pb-icon-button.js +108 -0
  190. package/src/pb-icon.js +283 -0
  191. package/src/pb-image-strip.js +85 -79
  192. package/src/pb-lang.js +142 -57
  193. package/src/pb-leaflet-map.js +551 -483
  194. package/src/pb-link.js +132 -126
  195. package/src/pb-load.js +495 -428
  196. package/src/pb-login.js +303 -248
  197. package/src/pb-manage-odds.js +384 -338
  198. package/src/pb-map-icon.js +90 -90
  199. package/src/pb-map-layer.js +86 -86
  200. package/src/pb-markdown.js +107 -110
  201. package/src/pb-media-query.js +75 -73
  202. package/src/pb-mei.js +523 -303
  203. package/src/pb-message.js +144 -98
  204. package/src/pb-mixin.js +268 -265
  205. package/src/pb-navigation.js +83 -96
  206. package/src/pb-observable.js +39 -39
  207. package/src/pb-odd-editor.js +1209 -948
  208. package/src/pb-odd-elementspec-editor.js +375 -310
  209. package/src/pb-odd-model-editor.js +1189 -941
  210. package/src/pb-odd-parameter-editor.js +269 -170
  211. package/src/pb-odd-rendition-editor.js +184 -131
  212. package/src/pb-page.js +451 -422
  213. package/src/pb-paginate.js +260 -178
  214. package/src/pb-panel.js +217 -183
  215. package/src/pb-popover-themes.js +16 -9
  216. package/src/pb-popover.js +297 -288
  217. package/src/pb-print-preview.js +128 -128
  218. package/src/pb-progress.js +52 -52
  219. package/src/pb-repeat.js +141 -108
  220. package/src/pb-restricted.js +85 -78
  221. package/src/pb-search.js +258 -230
  222. package/src/pb-select-feature.js +210 -126
  223. package/src/pb-select-odd.js +184 -118
  224. package/src/pb-select-template.js +113 -78
  225. package/src/pb-select.js +330 -229
  226. package/src/pb-split-list.js +181 -176
  227. package/src/pb-svg.js +81 -80
  228. package/src/pb-table-column.js +55 -55
  229. package/src/pb-table-grid.js +334 -205
  230. package/src/pb-tabs.js +238 -61
  231. package/src/pb-tify.js +3331 -126
  232. package/src/pb-timeline.js +394 -255
  233. package/src/pb-toggle-feature.js +196 -188
  234. package/src/pb-upload.js +201 -176
  235. package/src/pb-version.js +22 -34
  236. package/src/pb-view-annotate.js +138 -102
  237. package/src/pb-view.js +1722 -1272
  238. package/src/pb-zoom.js +144 -46
  239. package/src/search-result-service.js +256 -223
  240. package/src/seed-element.js +14 -22
  241. package/src/settings.js +4 -4
  242. package/src/theming.js +98 -91
  243. package/src/urls.js +403 -289
  244. package/src/utils.js +53 -51
  245. package/vite.config.js +86 -0
  246. package/.github/workflows/main.yml +0 -24
  247. package/.github/workflows/release.js.yml +0 -34
  248. package/css/pb-styles.css +0 -51
  249. package/dist/iron-form-3b8dcaa7.js +0 -210
  250. package/dist/jinn-codemirror-da0e2d1f.js +0 -1
  251. package/dist/paper-checkbox-515a5284.js +0 -1597
  252. package/dist/paper-icon-button-b1d31571.js +0 -398
  253. package/dist/paper-listbox-a3b7175c.js +0 -1265
  254. package/dist/pb-i18n-0611135a.js +0 -1
  255. package/dist/pb-mixin-b1caa22e.js +0 -158
  256. package/dist/polymer-hack.js +0 -1
  257. package/dist/vaadin-element-mixin-fe4a4883.js +0 -527
  258. package/lib/Control.Geocoder.min.js +0 -2
  259. package/lib/Control.Geocoder.min.js.map +0 -1
  260. package/src/assets/pb-styles.css +0 -51
  261. package/src/pb-light-dom.js +0 -41
  262. package/src/polymer-hack.js +0 -6
package/src/pb-i18n.js CHANGED
@@ -1,167 +1,155 @@
1
- import { LitElement, html } from 'lit-element';
2
- import { directive, NodePart, AttributePart } from "lit-html";
3
-
4
- // Cache created lit-html parts, so we can update translations
5
- const partCache = new Map();
1
+ import { LitElement, html } from 'lit';
6
2
 
7
3
  // The currently used i18next translation function
8
- let _translate;
4
+ let _translateFn;
5
+ // Bump whenever the translation function or resources change
6
+ let _i18nVersion = 0;
7
+ // Shared, tiny in-memory cache per-version (key+options)
8
+ const _i18nCache = new Map();
9
9
 
10
- /**
10
+ /**
11
11
  * Called by pb-page before the first pb-i18n-update
12
12
  * to make sure the translation function is set.
13
13
  */
14
14
  export function initTranslation(translate) {
15
- _translate = translate;
16
- }
17
-
18
- function isConnected(part) {
19
- if (part instanceof NodePart) {
20
- return part.startNode.isConnected;
21
-
22
- }
23
- if (part instanceof AttributePart) {
24
- return part.committer.element.isConnected;
25
-
26
- }
27
- return part.element.isConnected;
15
+ _translateFn = translate;
16
+ _i18nVersion++;
17
+ _i18nCache.clear();
28
18
  }
29
19
 
30
- function removeDisconnectedParts() {
31
- Object.keys(partCache).forEach((part) => {
32
- if (!isConnected(part)) {
33
- partCache.delete(part);
34
- }
35
- });
36
- }
37
-
38
- function whenIdle(cb) {
39
- "requestIdleCallback" in window ? window.requestIdleCallback(cb) : setTimeout(cb)
40
- }
41
-
42
- function updatePart(part, cb) {
43
-
44
- // Grab the new value
45
- const newValue = cb();
46
-
47
- // Only set the value if it has changed
48
- if (part.value === newValue) {
49
- return;
50
- }
51
-
52
- // Set the new value
53
- part.setValue(newValue);
54
- part.commit();
55
- }
56
-
57
- function updateParts(options) {
58
- _translate = options.t;
59
- partCache.forEach((cb, part) => {
60
- if (isConnected(part)) {
61
- updatePart(part, cb);
62
- }
63
- });
64
- }
20
+ // No-op helpers kept for backward compatibility of API surface
65
21
 
66
22
  /**
67
23
  * Translate the given string using i18n.
68
- *
24
+ *
69
25
  * @param {String} key The key to translate
70
26
  * @param {Object} [value] Optional object containing interpolation values
71
- * @returns
27
+ * @returns {string}
72
28
  */
73
29
  export function get(key, value) {
74
- if (_translate) {
75
- return _translate(key, value);
76
- }
77
- return key;
30
+ if (_translateFn) return _translateFn(key, value);
31
+ return key;
78
32
  }
79
33
 
80
- export const langChanged = directive((cb) => (part) => {
81
- partCache.set(part, cb);
82
- updatePart(part, cb);
83
- });
84
-
85
34
  /**
86
35
  * lit-html directive to translate a given key into the target language
87
36
  * using i18next.
88
- *
89
- * @param {String} key The key to translate
90
- * @param {Object} [value] Optional object containing interpolation values
37
+ *
38
+ * @param {String} key
39
+ * @param {Object} [value]
91
40
  */
92
- export const translate = (key, value) => langChanged(() => get(key, value));
93
-
94
- document.addEventListener('pb-i18n-update', (ev) => {
95
- updateParts(ev.detail);
96
- });
97
-
98
- // start a background task to garbage collect parts
99
- setInterval(() => whenIdle(() => removeDisconnectedParts()), 1000 * 60);
41
+ export const translate = (key, value) => get(key, value);
42
+
43
+ // Keep _translateFn in sync on updates (guard for non-browser envs)
44
+ if (typeof document !== 'undefined') {
45
+ document.addEventListener('pb-i18n-update', ev => {
46
+ const next = ev.detail?.t;
47
+ if (next && next !== _translateFn) {
48
+ _translateFn = next;
49
+ }
50
+ // Invalidate cache regardless (resources may have changed)
51
+ _i18nVersion++;
52
+ _i18nCache.clear();
53
+ });
54
+ }
100
55
 
101
56
  /**
102
57
  * Insert translated text somewhere on an HTML page. If no translation is found,
103
58
  * display the contained content.
104
59
  */
105
60
  export class PbI18n extends LitElement {
106
- static get properties() {
107
- return {
108
- ...super.properties,
109
- /**
110
- * The i18n key to use for looking up the translation.
111
- */
112
- key: {
113
- type: String
114
- },
115
- /**
116
- * Optional interpolation parameters to be passed to the
117
- * translation function
118
- */
119
- options: {
120
- type: Object
121
- },
122
- _translated: {
123
- type: String
124
- }
125
- };
61
+ static get properties() {
62
+ return {
63
+ ...super.properties,
64
+ /**
65
+ * The i18n key to use for looking up the translation.
66
+ */
67
+ key: { type: String },
68
+ /**
69
+ * Optional interpolation parameters to be passed to the
70
+ * translation function
71
+ */
72
+ options: { type: Object },
73
+ _translated: { type: String },
74
+ };
75
+ }
76
+
77
+ constructor() {
78
+ super();
79
+ this.key = 'missing-key';
80
+ this.options = null;
81
+ this._translated = null;
82
+ this._fallback = '';
83
+ // stable handler so we can remove it on disconnect
84
+ this._onI18nUpdate = () => this._recompute();
85
+ this._recomputeTimer = null;
86
+ }
87
+
88
+ connectedCallback() {
89
+ super.connectedCallback();
90
+
91
+ // Capture initial fallback then clear host content so Lit fully controls light DOM
92
+ if (!this._capturedFallback) {
93
+ this._fallback = this.innerHTML;
94
+ this._capturedFallback = true;
126
95
  }
96
+ this.innerHTML = '';
127
97
 
128
- constructor() {
129
- super();
130
- this.key = 'missing-key';
131
- this._options = null;
132
- this._translated = null;
98
+ if (typeof document !== 'undefined') {
99
+ document.addEventListener('pb-i18n-update', this._onI18nUpdate);
133
100
  }
134
101
 
135
- connectedCallback() {
136
- super.connectedCallback();
137
-
138
- this._fallback = this.innerHTML;
139
-
140
- document.addEventListener('pb-i18n-update', this._translate.bind(this));
102
+ this._scheduleRecompute();
103
+ }
141
104
 
142
- this._translate();
105
+ disconnectedCallback() {
106
+ if (typeof document !== 'undefined') {
107
+ document.removeEventListener('pb-i18n-update', this._onI18nUpdate);
143
108
  }
144
-
145
- set options(value) {
146
- this._options = value;
147
- this._translate();
109
+ if (this._recomputeTimer) {
110
+ clearTimeout(this._recomputeTimer);
111
+ this._recomputeTimer = null;
148
112
  }
113
+ super.disconnectedCallback();
114
+ }
149
115
 
150
- _translate() {
151
- const transl = get(this.key, this._options);
152
- if (transl && transl !== this.key) {
153
- this._translated = transl;
154
- } else {
155
- this._translated = null;
156
- }
116
+ updated(changed) {
117
+ if (changed.has('key') || changed.has('options')) {
118
+ this._scheduleRecompute();
157
119
  }
158
-
159
- render() {
160
- return this._translated ? this._translated : this._fallback;
120
+ }
121
+
122
+ _scheduleRecompute() {
123
+ if (this._recomputeTimer) return;
124
+ // Small debounce to coalesce multiple updates and event bursts
125
+ this._recomputeTimer = setTimeout(() => {
126
+ this._recomputeTimer = null;
127
+ this._recompute();
128
+ }, 20);
129
+ }
130
+
131
+ _recompute() {
132
+ const opts = this.options || {};
133
+ const cacheKey = `${_i18nVersion}::${this.key}::${JSON.stringify(opts)}`;
134
+ if (_i18nCache.has(cacheKey)) {
135
+ this._translated = _i18nCache.get(cacheKey);
136
+ return;
161
137
  }
162
138
 
163
- createRenderRoot() {
164
- return this;
165
- }
139
+ const result = get(this.key, opts);
140
+ const value = result && result !== this.key ? result : null;
141
+ _i18nCache.set(cacheKey, value);
142
+ this._translated = value;
143
+ }
144
+
145
+ render() {
146
+ // Return text (not HTML) so translations aren’t treated as markup
147
+ return this._translated ?? this._fallback;
148
+ }
149
+
150
+ // light DOM rendering (as before)
151
+ createRenderRoot() {
152
+ return this;
153
+ }
166
154
  }
167
- customElements.define('pb-i18n', PbI18n);
155
+ customElements.define('pb-i18n', PbI18n);
@@ -0,0 +1,108 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import './pb-icon.js';
3
+
4
+ const normalizeLabel = (value = '') => value.replace(/[-_]/g, ' ').trim();
5
+
6
+ export class PbIconButton extends LitElement {
7
+ static properties = {
8
+ icon: { type: String },
9
+ disabled: { type: Boolean, reflect: true },
10
+ label: { type: String },
11
+ toggles: { type: Boolean },
12
+ active: { type: Boolean, reflect: true },
13
+ };
14
+
15
+ constructor() {
16
+ super();
17
+ this.icon = '';
18
+ this.disabled = false;
19
+ this.label = '';
20
+ this.toggles = false;
21
+ this.active = false;
22
+ }
23
+
24
+ render() {
25
+ const hostTitle = this.title || '';
26
+ const iconLabel = this.icon
27
+ ? normalizeLabel(this.icon.includes(':') ? this.icon.split(':').pop() : this.icon)
28
+ : '';
29
+ const ariaLabel = this.label || hostTitle || iconLabel;
30
+
31
+ return html`
32
+ <button
33
+ class="pb-button pb-button--icon"
34
+ type="button"
35
+ title=${hostTitle}
36
+ aria-label=${ariaLabel}
37
+ ?disabled=${this.disabled}
38
+ aria-pressed=${this.toggles ? String(this.active) : undefined}
39
+ @click=${this._handleClick}
40
+ >
41
+ ${this.icon
42
+ ? html`<pb-icon icon="${this.icon}" decorative></pb-icon>`
43
+ : html`<slot></slot>`}
44
+ </button>
45
+ `;
46
+ }
47
+
48
+ _handleClick(event) {
49
+ if (this.disabled) {
50
+ event.preventDefault();
51
+ event.stopImmediatePropagation();
52
+ return;
53
+ }
54
+
55
+ if (this.toggles) {
56
+ this.active = !this.active;
57
+ this.dispatchEvent(
58
+ new CustomEvent('active-changed', {
59
+ detail: { value: this.active },
60
+ bubbles: true,
61
+ composed: true,
62
+ }),
63
+ );
64
+ }
65
+ }
66
+
67
+ static styles = css`
68
+ :host {
69
+ display: inline-flex;
70
+ box-sizing: border-box;
71
+ --pb-icon-button-size: 40px;
72
+ }
73
+
74
+ button {
75
+ width: var(--pb-icon-button-size);
76
+ height: var(--pb-icon-button-size);
77
+ border-radius: 999px;
78
+ border: none;
79
+ background: transparent;
80
+ color: inherit;
81
+ padding: 0;
82
+ cursor: pointer;
83
+ display: inline-flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ transition: background 0.2s ease-in-out;
87
+ }
88
+
89
+ button:hover:not(:disabled),
90
+ button:focus-visible {
91
+ background: var(--pb-icon-button-hover-background, rgba(0, 0, 0, 0.05));
92
+ outline: none;
93
+ }
94
+
95
+ button:disabled {
96
+ opacity: 0.4;
97
+ cursor: not-allowed;
98
+ }
99
+
100
+ :host([active]) button {
101
+ background: var(--pb-icon-button-active-background, rgba(0, 0, 0, 0.08));
102
+ }
103
+ `;
104
+ }
105
+
106
+ if (!customElements.get('pb-icon-button')) {
107
+ customElements.define('pb-icon-button', PbIconButton);
108
+ }
package/src/pb-icon.js ADDED
@@ -0,0 +1,283 @@
1
+ import { LitElement, html, css } from 'lit';
2
+
3
+ if (!customElements.__pbDefineGuard) {
4
+ const originalDefine = customElements.define.bind(customElements);
5
+ customElements.define = (name, constructor, options) => {
6
+ if (customElements.get(name)) {
7
+ return;
8
+ }
9
+ originalDefine(name, constructor, options);
10
+ };
11
+ customElements.__pbDefineGuard = true;
12
+ }
13
+
14
+ const polymerIconMap = {
15
+ 'icons:file-upload': 'upload',
16
+ 'icons:file-download': 'download',
17
+ 'icons:cloud-download': 'download',
18
+ 'icons:clear': 'close',
19
+ 'icons:check': 'check',
20
+ 'icons:error-outline': 'warning',
21
+ 'icons:folder-open': 'folder-open',
22
+ 'icons:arrow-upward': 'arrow-upward',
23
+ 'icons:arrow-forward': 'chevron-right',
24
+ 'icons:arrow-back': 'chevron-left',
25
+ 'icons:chevron-right': 'chevron-right',
26
+ 'icons:chevron-left': 'chevron-left',
27
+ 'icons:open-in-new': 'open-in-new',
28
+ 'icons:flag': 'flag',
29
+ 'icons:code': 'code',
30
+ 'icons:hourglass-empty': 'clock',
31
+ 'icons:delete': 'delete',
32
+ 'icons:create': 'edit',
33
+ 'icons:expand-more': 'expand-more',
34
+ 'icons:expand-less': 'expand-less',
35
+ 'icons:view-list': 'view-list',
36
+ 'icons:zoom-in': 'zoom-in',
37
+ 'icons:zoom-out': 'zoom-out',
38
+ 'icons:content-copy': 'copy',
39
+ 'content-copy': 'copy',
40
+ 'icons:content-paste': 'content-paste',
41
+ 'content-paste': 'content-paste',
42
+ 'icons:arrow-downward': 'arrow-downward',
43
+ 'icons:refresh': 'refresh',
44
+ 'icons:save': 'save',
45
+ };
46
+
47
+ /**
48
+ * A versatile icon component that replaces iron-icon and iron-icons.
49
+ * Supports both SVG sprite references and inline SVG content.
50
+ *
51
+ * @example
52
+ * <!-- Using icon name (requires icon sprite) -->
53
+ * <pb-icon icon="expand-more"></pb-icon>
54
+ *
55
+ * <!-- Using external sprite (default dist/images/icons.svg) -->
56
+ * <pb-icon icon="expand-more" sprite="dist/images/icons.svg"></pb-icon>
57
+ *
58
+ * <!-- Using inline SVG -->
59
+ * <pb-icon>
60
+ * <svg viewBox="0 0 24 24">
61
+ * <path d="M7 10l5 5 5-5z"/>
62
+ * </svg>
63
+ * </pb-icon>
64
+ *
65
+ * @slot - Inline SVG content when not using icon name
66
+ * @cssprop [--pb-icon-color] - Icon color (defaults to currentColor)
67
+ * @cssprop [--pb-icon-size] - Icon size (defaults to 1em)
68
+ */
69
+ export class PbIcon extends LitElement {
70
+ static get properties() {
71
+ return {
72
+ /**
73
+ * Icon name to display from the icon sprite.
74
+ * If not provided, expects SVG content in the default slot.
75
+ */
76
+ icon: { type: String },
77
+
78
+ /**
79
+ * Path or URL to the SVG sprite file. Optional.
80
+ * If not specified, defaults to 'dist/images/icons.svg'.
81
+ * If set to a string starting with '#', uses inline sprite.
82
+ */
83
+ sprite: { type: String },
84
+ /**
85
+ * Icon size. Can be a preset (xs, sm, md, lg, xl) or a custom size.
86
+ */
87
+ size: { type: String },
88
+
89
+ /**
90
+ * ARIA label for accessibility. Auto-generated from icon name if not provided.
91
+ */
92
+ label: { type: String },
93
+
94
+ /**
95
+ * Whether the icon is decorative (hidden from screen readers).
96
+ */
97
+ decorative: { type: Boolean },
98
+ };
99
+ }
100
+
101
+ constructor() {
102
+ super();
103
+ this.icon = '';
104
+ this.size = '';
105
+ this.label = '';
106
+ this.decorative = false;
107
+ // Default sprite path: try to determine the correct base URL
108
+ try {
109
+ const isDev =
110
+ (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.DEV) ||
111
+ (typeof location !== 'undefined' && /localhost|127\.0\.0\.1/.test(location.hostname));
112
+
113
+ if (isDev) {
114
+ // In dev mode, check if we're running in TEI Publisher context
115
+ const currentHost = window.location.hostname;
116
+ const currentPort = window.location.port;
117
+
118
+ // If we're on the TEI Publisher port (8080), use the app's resources path
119
+ if (currentPort === '8080' || currentHost.includes('tei-publisher')) {
120
+ this.sprite = '/exist/apps/tei-publisher/resources/images/icons.svg';
121
+ } else {
122
+ // Otherwise use the Vite dev server path
123
+ this.sprite = '/images/icons.svg';
124
+ }
125
+ } else {
126
+ this.sprite = '/dist/images/icons.svg';
127
+ }
128
+ } catch (_) {
129
+ this.sprite = '/dist/images/icons.svg';
130
+ }
131
+ }
132
+
133
+ render() {
134
+ const sizeClass = this._getSizeClass();
135
+ const ariaLabel = this._getAriaLabel();
136
+
137
+ return html`
138
+ <span
139
+ class="pb-icon ${sizeClass}"
140
+ role="img"
141
+ aria-label="${ariaLabel}"
142
+ aria-hidden="${this.decorative ? 'true' : 'false'}"
143
+ >
144
+ ${this.icon ? this._renderSpriteIcon() : this._renderSlotIcon()}
145
+ </span>
146
+ `;
147
+ }
148
+
149
+ _renderSpriteIcon() {
150
+ const iconName = this._normalizeIconName(this.icon);
151
+ if (!iconName) {
152
+ return html``;
153
+ }
154
+ const href =
155
+ this.sprite && !this.sprite.startsWith('#')
156
+ ? `${this.sprite}#icon-${iconName}`
157
+ : `#icon-${iconName}`;
158
+ return html`
159
+ <svg
160
+ width="1em"
161
+ height="1em"
162
+ viewBox="0 0 24 24"
163
+ aria-hidden="${this.decorative ? 'true' : 'false'}"
164
+ >
165
+ <use href="${href}"></use>
166
+ </svg>
167
+ `;
168
+ }
169
+
170
+ _renderSlotIcon() {
171
+ return html`<slot></slot>`;
172
+ }
173
+
174
+ _normalizeIconName(name) {
175
+ if (!name) return '';
176
+ const lower = name.toLowerCase();
177
+ if (polymerIconMap[lower]) {
178
+ return polymerIconMap[lower];
179
+ }
180
+ if (lower.includes(':')) {
181
+ return lower.split(':').pop();
182
+ }
183
+ return lower;
184
+ }
185
+
186
+ _getSizeClass() {
187
+ if (!this.size) return '';
188
+
189
+ const presetSizes = ['xs', 'sm', 'md', 'lg', 'xl'];
190
+ if (presetSizes.includes(this.size)) {
191
+ return `pb-icon--${this.size}`;
192
+ }
193
+
194
+ // Custom size - set CSS custom property
195
+ this.style.setProperty('--pb-icon-size', this.size);
196
+ return '';
197
+ }
198
+
199
+ _getAriaLabel() {
200
+ if (this.decorative) return '';
201
+ if (this.label) return this.label;
202
+ if (this.icon) {
203
+ const normalized = this._normalizeIconName(this.icon);
204
+ return normalized.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
205
+ }
206
+ return 'Icon';
207
+ }
208
+
209
+ static get styles() {
210
+ return css`
211
+ :host {
212
+ display: inline-flex;
213
+ align-items: center;
214
+ justify-content: center;
215
+ }
216
+
217
+ .pb-icon {
218
+ display: inline-flex;
219
+ align-items: center;
220
+ justify-content: center;
221
+ width: var(--pb-icon-size, 1em);
222
+ height: var(--pb-icon-size, 1em);
223
+ color: var(--pb-icon-color, currentColor);
224
+ fill: currentColor;
225
+ flex-shrink: 0;
226
+ user-select: none;
227
+ line-height: 1;
228
+ }
229
+
230
+ .pb-icon svg {
231
+ width: 100%;
232
+ height: 100%;
233
+ fill: inherit;
234
+ }
235
+
236
+ /* Size variants */
237
+ .pb-icon--xs {
238
+ font-size: 12px;
239
+ width: 12px;
240
+ height: 12px;
241
+ }
242
+
243
+ .pb-icon--sm {
244
+ font-size: 16px;
245
+ width: 16px;
246
+ height: 16px;
247
+ }
248
+
249
+ .pb-icon--md {
250
+ font-size: 20px;
251
+ width: 20px;
252
+ height: 20px;
253
+ }
254
+
255
+ .pb-icon--lg {
256
+ font-size: 24px;
257
+ width: 24px;
258
+ height: 24px;
259
+ }
260
+
261
+ .pb-icon--xl {
262
+ font-size: 32px;
263
+ width: 32px;
264
+ height: 32px;
265
+ }
266
+
267
+ /* Ensure slotted SVGs are properly sized */
268
+ ::slotted(svg) {
269
+ width: 100%;
270
+ height: 100%;
271
+ fill: currentColor;
272
+ }
273
+ `;
274
+ }
275
+ }
276
+
277
+ if (!customElements.get('pb-icon')) {
278
+ customElements.define('pb-icon', PbIcon);
279
+ }
280
+ if (!customElements.get('iron-icon')) {
281
+ class PbIronIcon extends PbIcon {}
282
+ customElements.define('iron-icon', PbIronIcon);
283
+ }