@cloudron/pankow 4.4.1 → 4.4.3

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.
@@ -1,15 +1,18 @@
1
1
  <script setup>
2
2
 
3
3
  import { copyToClipboard } from '../utils';
4
- import Button from './Button.vue';
5
4
 
6
5
  const props = defineProps({
7
6
  value: String,
8
7
  doneHint: String,
8
+ multiline: {
9
+ type: Boolean,
10
+ default: false
11
+ },
9
12
  });
10
13
 
11
14
  function onClipboard() {
12
- copyToClipboard(props.value);
15
+ copyToClipboard(props.value, { multiline: props.multiline });
13
16
  window.pankow.notify({
14
17
  text: props.doneHint || 'Copied',
15
18
  type: 'success',
@@ -6,10 +6,14 @@ import Button from './Button.vue';
6
6
  const props = defineProps({
7
7
  value: String,
8
8
  doneHint: String,
9
+ multiline: {
10
+ type: Boolean,
11
+ default: false
12
+ },
9
13
  });
10
14
 
11
15
  function onClipboard() {
12
- copyToClipboard(props.value);
16
+ copyToClipboard(props.value, { multiline: props.multiline });
13
17
  window.pankow.notify({
14
18
  text: props.doneHint || 'Copied',
15
19
  type: 'success',
@@ -2,6 +2,11 @@
2
2
  import { ref, computed, watch, onMounted, onUnmounted, useTemplateRef } from 'vue';
3
3
 
4
4
  const props = defineProps({
5
+ orientation: {
6
+ type: String,
7
+ default: 'horizontal'
8
+ },
9
+ // horizontal mode props
5
10
  leftWidth: {
6
11
  type: Number,
7
12
  default: 50
@@ -13,40 +18,88 @@ const props = defineProps({
13
18
  minRightWidth: {
14
19
  type: Number,
15
20
  default: 20
21
+ },
22
+ // vertical mode props
23
+ topHeight: {
24
+ type: Number,
25
+ default: 50
26
+ },
27
+ minTopHeight: {
28
+ type: Number,
29
+ default: 20
30
+ },
31
+ minBottomHeight: {
32
+ type: Number,
33
+ default: 20
34
+ },
35
+ opaque: {
36
+ type: Boolean,
37
+ default: false
16
38
  }
17
39
  });
18
40
 
19
- const emit = defineEmits(['update:leftWidth']);
41
+ const emit = defineEmits(['update:leftWidth', 'update:topHeight']);
42
+
43
+ const isHorizontal = computed(() => props.orientation === 'horizontal');
20
44
 
21
45
  const containerRef = useTemplateRef('container');
22
46
  const isDragging = ref(false);
23
- const dragStartX = ref(0);
24
- const dragStartWidth = ref(0);
25
- const currentLeftWidth = ref(props.leftWidth);
47
+ const dragStartCoord = ref(0);
48
+ const dragStartSize = ref(0);
49
+
50
+ const currentFirstSize = ref(
51
+ isHorizontal.value ? props.leftWidth : props.topHeight
52
+ );
26
53
 
27
54
  watch(() => props.leftWidth, (val) => {
55
+ if (!isDragging.value && isHorizontal.value) {
56
+ currentFirstSize.value = val;
57
+ }
58
+ });
59
+
60
+ watch(() => props.topHeight, (val) => {
61
+ if (!isDragging.value && !isHorizontal.value) {
62
+ currentFirstSize.value = val;
63
+ }
64
+ });
65
+
66
+ watch(() => props.orientation, () => {
28
67
  if (!isDragging.value) {
29
- currentLeftWidth.value = val;
68
+ currentFirstSize.value = isHorizontal.value ? props.leftWidth : props.topHeight;
30
69
  }
31
70
  });
32
71
 
72
+ const minFirst = computed(() =>
73
+ isHorizontal.value ? props.minLeftWidth : props.minTopHeight
74
+ );
75
+
76
+ const minSecond = computed(() =>
77
+ isHorizontal.value ? props.minRightWidth : props.minBottomHeight
78
+ );
79
+
33
80
  function startDrag(e) {
34
81
  isDragging.value = true;
35
- dragStartX.value = e.clientX ?? e.touches?.[0]?.clientX ?? 0;
36
- dragStartWidth.value = currentLeftWidth.value;
37
- document.body.style.cursor = 'col-resize';
82
+ dragStartCoord.value = isHorizontal.value
83
+ ? (e.clientX ?? e.touches?.[0]?.clientX ?? 0)
84
+ : (e.clientY ?? e.touches?.[0]?.clientY ?? 0);
85
+ dragStartSize.value = currentFirstSize.value;
86
+ document.body.style.cursor = isHorizontal.value ? 'col-resize' : 'row-resize';
38
87
  document.body.style.userSelect = 'none';
39
88
  e.preventDefault();
40
89
  }
41
90
 
42
91
  function onDrag(e) {
43
92
  if (!isDragging.value || !containerRef.value) return;
44
- const clientX = e.clientX ?? e.touches?.[0]?.clientX ?? 0;
45
- const containerWidth = containerRef.value.getBoundingClientRect().width;
46
- if (containerWidth === 0) return;
47
- const delta = ((clientX - dragStartX.value) / containerWidth) * 100;
48
- const newWidth = dragStartWidth.value + delta;
49
- currentLeftWidth.value = Math.max(props.minLeftWidth, Math.min(100 - props.minRightWidth, newWidth));
93
+ const coord = isHorizontal.value
94
+ ? (e.clientX ?? e.touches?.[0]?.clientX ?? 0)
95
+ : (e.clientY ?? e.touches?.[0]?.clientY ?? 0);
96
+ const containerSize = isHorizontal.value
97
+ ? containerRef.value.getBoundingClientRect().width
98
+ : containerRef.value.getBoundingClientRect().height;
99
+ if (containerSize === 0) return;
100
+ const delta = ((coord - dragStartCoord.value) / containerSize) * 100;
101
+ const newSize = dragStartSize.value + delta;
102
+ currentFirstSize.value = Math.max(minFirst.value, Math.min(100 - minSecond.value, newSize));
50
103
  }
51
104
 
52
105
  function stopDrag() {
@@ -54,7 +107,11 @@ function stopDrag() {
54
107
  isDragging.value = false;
55
108
  document.body.style.cursor = '';
56
109
  document.body.style.userSelect = '';
57
- emit('update:leftWidth', Math.round(currentLeftWidth.value));
110
+ if (isHorizontal.value) {
111
+ emit('update:leftWidth', Math.round(currentFirstSize.value));
112
+ } else {
113
+ emit('update:topHeight', Math.round(currentFirstSize.value));
114
+ }
58
115
  }
59
116
 
60
117
  onMounted(() => {
@@ -71,21 +128,35 @@ onUnmounted(() => {
71
128
  document.removeEventListener('touchend', stopDrag);
72
129
  });
73
130
 
74
- const leftStyle = computed(() => ({
75
- flex: `0 0 ${currentLeftWidth.value}%`,
76
- maxWidth: `${currentLeftWidth.value}%`
77
- }));
131
+ const firstStyle = computed(() => {
132
+ const size = currentFirstSize.value;
133
+ return isHorizontal.value
134
+ ? { flex: `0 0 ${size}%`, maxWidth: `${size}%` }
135
+ : { flex: `0 0 ${size}%`, maxHeight: `${size}%` };
136
+ });
78
137
  </script>
79
138
 
80
139
  <template>
81
- <div ref="container" class="pankow-split-layout" :class="{ 'pankow-split-layout-dragging': isDragging }">
82
- <div class="pankow-split-layout-left" :style="leftStyle">
140
+ <div
141
+ ref="container"
142
+ class="pankow-split-layout"
143
+ :class="{
144
+ 'pankow-split-layout-dragging': isDragging,
145
+ 'pankow-split-layout-vertical': !isHorizontal,
146
+ 'pankow-split-layout-divider-opaque': opaque
147
+ }"
148
+ >
149
+ <div class="pankow-split-layout-first" :style="firstStyle">
83
150
  <slot name="left" />
84
151
  </div>
85
- <div class="pankow-split-layout-divider" @mousedown="startDrag" @touchstart.prevent="startDrag">
152
+ <div
153
+ class="pankow-split-layout-divider"
154
+ @mousedown="startDrag"
155
+ @touchstart.prevent="startDrag"
156
+ >
86
157
  <div class="pankow-split-layout-handle"><span></span><span></span></div>
87
158
  </div>
88
- <div class="pankow-split-layout-right">
159
+ <div class="pankow-split-layout-second">
89
160
  <slot name="right" />
90
161
  </div>
91
162
  </div>
@@ -100,15 +171,22 @@ const leftStyle = computed(() => ({
100
171
  overflow: hidden;
101
172
  }
102
173
 
103
- .pankow-split-layout-left {
104
- height: 100%;
174
+ .pankow-split-layout-vertical {
175
+ flex-direction: column;
176
+ }
177
+
178
+ .pankow-split-layout-first {
105
179
  min-width: 0;
106
180
  overflow: hidden;
107
181
  }
108
182
 
183
+ .pankow-split-layout-vertical .pankow-split-layout-first {
184
+ height: auto;
185
+ min-height: 0;
186
+ min-width: 0;
187
+ }
188
+
109
189
  .pankow-split-layout-divider {
110
- width: 6px;
111
- cursor: col-resize;
112
190
  background: transparent;
113
191
  position: relative;
114
192
  flex-shrink: 0;
@@ -117,6 +195,20 @@ const leftStyle = computed(() => ({
117
195
  justify-content: center;
118
196
  }
119
197
 
198
+ .pankow-split-layout:not(.pankow-split-layout-vertical) .pankow-split-layout-divider {
199
+ width: 6px;
200
+ cursor: col-resize;
201
+ }
202
+
203
+ .pankow-split-layout-vertical .pankow-split-layout-divider {
204
+ height: 6px;
205
+ cursor: row-resize;
206
+ }
207
+
208
+ .pankow-split-layout-divider-opaque .pankow-split-layout-divider {
209
+ background: var(--pankow-color-background);
210
+ }
211
+
120
212
  .pankow-split-layout-divider:hover,
121
213
  .pankow-split-layout-dragging .pankow-split-layout-divider {
122
214
  background: var(--pankow-input-border-color);
@@ -128,13 +220,20 @@ const leftStyle = computed(() => ({
128
220
  left: 50%;
129
221
  transform: translate(-50%, -50%);
130
222
  display: flex;
131
- flex-direction: column;
132
223
  gap: 2px;
133
224
  opacity: 0;
134
225
  transition: opacity 0.15s;
135
226
  pointer-events: none;
136
227
  }
137
228
 
229
+ .pankow-split-layout:not(.pankow-split-layout-vertical) .pankow-split-layout-handle {
230
+ flex-direction: column;
231
+ }
232
+
233
+ .pankow-split-layout-vertical .pankow-split-layout-handle {
234
+ flex-direction: row;
235
+ }
236
+
138
237
  .pankow-split-layout-divider:hover .pankow-split-layout-handle,
139
238
  .pankow-split-layout-dragging .pankow-split-layout-handle {
140
239
  opacity: 0.5;
@@ -142,21 +241,34 @@ const leftStyle = computed(() => ({
142
241
 
143
242
  .pankow-split-layout-handle span {
144
243
  display: block;
145
- width: 2px;
146
- height: 16px;
147
244
  border-radius: 1px;
148
245
  background: var(--pankow-color-text);
149
246
  }
150
247
 
151
- .pankow-split-layout-right {
248
+ .pankow-split-layout:not(.pankow-split-layout-vertical) .pankow-split-layout-handle span {
249
+ width: 2px;
250
+ height: 16px;
251
+ }
252
+
253
+ .pankow-split-layout-vertical .pankow-split-layout-handle span {
254
+ width: 16px;
255
+ height: 2px;
256
+ }
257
+
258
+ .pankow-split-layout-second {
152
259
  flex: 1;
153
- height: 100%;
154
260
  min-width: 0;
155
261
  overflow: hidden;
156
262
  }
157
263
 
158
- .pankow-split-layout-dragging .pankow-split-layout-left,
159
- .pankow-split-layout-dragging .pankow-split-layout-right {
264
+ .pankow-split-layout-vertical .pankow-split-layout-second {
265
+ height: auto;
266
+ min-height: 0;
267
+ min-width: 0;
268
+ }
269
+
270
+ .pankow-split-layout-dragging .pankow-split-layout-first,
271
+ .pankow-split-layout-dragging .pankow-split-layout-second {
160
272
  pointer-events: none;
161
273
  }
162
274
  </style>
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <Transition name="pankow-version-banner">
3
+ <div v-if="mismatch" class="pankow-version-banner">
4
+ <span>A new version is available.</span>
5
+ <button class="pankow-version-banner-reload" @click="reload">Reload</button>
6
+ <button class="pankow-version-banner-close" @click="dismiss" aria-label="Dismiss">&#x2715;</button>
7
+ </div>
8
+ </Transition>
9
+ </template>
10
+
11
+ <script setup>
12
+ import { ref, onMounted, onUnmounted } from 'vue';
13
+
14
+ const mismatch = ref(false);
15
+
16
+ function setMismatch() {
17
+ mismatch.value = true;
18
+ }
19
+
20
+ function reload() {
21
+ window.location.reload(true);
22
+ }
23
+
24
+ function dismiss() {
25
+ mismatch.value = false;
26
+ }
27
+
28
+ function checkFlag() {
29
+ if (window.__pankow_version_mismatch) setMismatch();
30
+ }
31
+
32
+ onMounted(() => {
33
+ if (window.__pankow_version_mismatch) setMismatch();
34
+ window.addEventListener('pankow:version-mismatch', checkFlag);
35
+ });
36
+
37
+ onUnmounted(() => {
38
+ window.removeEventListener('pankow:version-mismatch', checkFlag);
39
+ });
40
+ </script>
41
+
42
+ <style>
43
+ .pankow-version-banner {
44
+ position: fixed;
45
+ top: 0;
46
+ left: 0;
47
+ right: 0;
48
+ z-index: 30002;
49
+ display: flex;
50
+ justify-content: center;
51
+ align-items: center;
52
+ gap: 16px;
53
+ padding: 10px 24px;
54
+ background: var(--pankow-color-primary);
55
+ color: white;
56
+ font-family: var(--pankow-font-family, Inter, sans-serif);
57
+ font-size: 13px;
58
+ font-weight: 500;
59
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
60
+ }
61
+
62
+ .pankow-version-banner-reload {
63
+ background: rgba(255, 255, 255, 0.2);
64
+ border: 1px solid rgba(255, 255, 255, 0.4);
65
+ color: white;
66
+ padding: 4px 12px;
67
+ border-radius: var(--pankow-border-radius, 4px);
68
+ cursor: pointer;
69
+ font: inherit;
70
+ }
71
+
72
+ .pankow-version-banner-reload:hover {
73
+ background: rgba(255, 255, 255, 0.3);
74
+ }
75
+
76
+ .pankow-version-banner-close {
77
+ background: none;
78
+ border: none;
79
+ color: rgba(255, 255, 255, 0.7);
80
+ cursor: pointer;
81
+ font-size: 14px;
82
+ padding: 0 4px;
83
+ line-height: 1;
84
+ }
85
+
86
+ .pankow-version-banner-close:hover {
87
+ color: white;
88
+ }
89
+
90
+ .pankow-version-banner-enter-active,
91
+ .pankow-version-banner-leave-active {
92
+ transition: transform 0.3s ease;
93
+ }
94
+
95
+ .pankow-version-banner-enter-from,
96
+ .pankow-version-banner-leave-to {
97
+ transform: translateY(-100%);
98
+ }
99
+ </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cloudron/pankow",
3
3
  "private": false,
4
- "version": "4.4.1",
4
+ "version": "4.4.3",
5
5
  "description": "Vue 3 UI component library for Cloudron apps",
6
6
  "main": "index.js",
7
7
  "types": "types/index.d.ts",
package/plugin.js CHANGED
@@ -5,8 +5,10 @@ import textOverflow from './text-overflow.js';
5
5
  import fetcher from './fetcher.js';
6
6
  import { setNotifyPosition } from './notifyState.js';
7
7
  import Notification from './components/Notification.vue';
8
+ import VersionCheckBanner from './components/VersionCheckBanner.vue';
8
9
 
9
- const CONTAINER_ID = 'pankow-notification-root';
10
+ const NOTIFICATION_CONTAINER_ID = 'pankow-notification-root';
11
+ const VERSION_BANNER_CONTAINER_ID = 'pankow-version-banner-root';
10
12
 
11
13
  export default {
12
14
  install(app, options = {}) {
@@ -28,14 +30,25 @@ export default {
28
30
  }
29
31
 
30
32
  if (typeof document !== 'undefined' && !import.meta.env.SSR) {
31
- const existing = document.getElementById(CONTAINER_ID);
32
- if (existing) existing.remove();
33
+ const existingNotification = document.getElementById(NOTIFICATION_CONTAINER_ID);
34
+ if (existingNotification) existingNotification.remove();
33
35
 
34
- const container = document.createElement('div');
35
- container.id = CONTAINER_ID;
36
- document.body.appendChild(container);
36
+ const notificationContainer = document.createElement('div');
37
+ notificationContainer.id = NOTIFICATION_CONTAINER_ID;
38
+ document.body.appendChild(notificationContainer);
37
39
 
38
- createApp(Notification).mount(container);
40
+ createApp(Notification).mount(notificationContainer);
41
+ }
42
+
43
+ if (typeof document !== 'undefined' && !import.meta.env.SSR) {
44
+ const existingBanner = document.getElementById(VERSION_BANNER_CONTAINER_ID);
45
+ if (existingBanner) existingBanner.remove();
46
+
47
+ const bannerContainer = document.createElement('div');
48
+ bannerContainer.id = VERSION_BANNER_CONTAINER_ID;
49
+ document.body.appendChild(bannerContainer);
50
+
51
+ createApp(VersionCheckBanner).mount(bannerContainer);
39
52
  }
40
53
  }
41
54
  };
package/types/utils.d.ts CHANGED
@@ -127,12 +127,15 @@ declare function getExtension(entry: {
127
127
  fileName: string;
128
128
  }): string;
129
129
  /**
130
- * Copies a string to the system clipboard using a temporary input element.
130
+ * Copies a string to the system clipboard using a temporary form element.
131
131
  * @param {string} value
132
+ * @param {{ multiline?: boolean }} [options] - When multiline is true, uses a textarea so newlines are preserved
132
133
  * @returns {void}
133
134
  * @deprecated Prefer the native `navigator.clipboard.writeText()` API.
134
135
  */
135
- declare function copyToClipboard(value: string): void;
136
+ declare function copyToClipboard(value: string, options?: {
137
+ multiline?: boolean;
138
+ }): void;
136
139
  /**
137
140
  * Parses the current page's URL query string into a key/value object.
138
141
  * @returns {Object<string, string>}
package/utils.js CHANGED
@@ -332,13 +332,15 @@ function getExtension(entry) {
332
332
  }
333
333
 
334
334
  /**
335
- * Copies a string to the system clipboard using a temporary input element.
335
+ * Copies a string to the system clipboard using a temporary form element.
336
336
  * @param {string} value
337
+ * @param {{ multiline?: boolean }} [options] - When multiline is true, uses a textarea so newlines are preserved
337
338
  * @returns {void}
338
339
  * @deprecated Prefer the native `navigator.clipboard.writeText()` API.
339
340
  */
340
- function copyToClipboard(value) {
341
- var elem = document.createElement('input');
341
+ function copyToClipboard(value, options) {
342
+ var multiline = options && options.multiline;
343
+ var elem = document.createElement(multiline ? 'textarea' : 'input');
342
344
  elem.value = value;
343
345
  document.body.append(elem);
344
346
  elem.select();
package/vite-plugin.js CHANGED
@@ -1,12 +1,44 @@
1
1
  import { dirname, resolve } from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
- import { existsSync } from 'node:fs';
3
+ import { existsSync, writeFileSync } from 'node:fs';
4
4
 
5
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
6
 
7
7
  const PANKOW_PACKAGE = '@cloudron/pankow';
8
8
 
9
- export default function pankowPlugin() {
9
+ const versionCheckScript = `<script>
10
+ (function () {
11
+ var meta = document.querySelector('meta[name="build-id"]');
12
+ if (!meta) return;
13
+
14
+ var id = meta.content;
15
+
16
+ function check() {
17
+ fetch('/build.json')
18
+ .then(function (r) { return r.ok ? r.json() : null; })
19
+ .then(function (d) {
20
+ if (d && d.buildId && d.buildId !== id) {
21
+ window.__pankow_version_mismatch = true;
22
+ window.dispatchEvent(new CustomEvent('pankow:version-mismatch'));
23
+ }
24
+ })
25
+ .catch(function () {});
26
+ }
27
+
28
+ setInterval(check, 30000);
29
+
30
+ document.addEventListener('visibilitychange', function () {
31
+ if (document.visibilityState === 'visible') check();
32
+ });
33
+
34
+ setTimeout(check, 5000);
35
+ })();
36
+ </script>`;
37
+
38
+ export default function pankowPlugin({ versionCheck = false } = {}) {
39
+ let buildId = null;
40
+ let config = null;
41
+
10
42
  return {
11
43
  name: 'vite-plugin-pankow',
12
44
  config(config) {
@@ -21,6 +53,9 @@ export default function pankowPlugin() {
21
53
  },
22
54
  };
23
55
  },
56
+ configResolved(resolvedConfig) {
57
+ config = resolvedConfig;
58
+ },
24
59
  configureServer(server) {
25
60
  const allow = server.config.server.fs.allow;
26
61
  if (Array.isArray(allow) && !allow.includes(__dirname)) {
@@ -36,5 +71,16 @@ export default function pankowPlugin() {
36
71
  }
37
72
  }
38
73
  },
74
+ transformIndexHtml(html) {
75
+ if (!versionCheck) return html;
76
+ buildId = Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
77
+ return html.replace('</head>', `<meta name="build-id" content="${buildId}">\n${versionCheckScript}\n</head>`);
78
+ },
79
+ closeBundle() {
80
+ if (!versionCheck || !buildId || !config) return;
81
+ const outDir = config.build.outDir || 'dist';
82
+ const outPath = resolve(config.root, outDir, 'build.json');
83
+ writeFileSync(outPath, JSON.stringify({ buildId }));
84
+ },
39
85
  };
40
86
  }