@cloudron/pankow 4.2.3 → 4.3.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.
@@ -0,0 +1,188 @@
1
+ <script setup>
2
+
3
+ import { computed, useTemplateRef, ref } from 'vue';
4
+ import Menu from './Menu.vue';
5
+ import Button from './Button.vue';
6
+ import ButtonGroup from './ButtonGroup.vue';
7
+ import Icon from './Icon.vue';
8
+
9
+ const props = defineProps({
10
+ label: String,
11
+ subtext: String,
12
+ href: String,
13
+ actions: {
14
+ type: Array,
15
+ default: () => [],
16
+ },
17
+ });
18
+
19
+ const quickActions = computed(() => {
20
+ if (window.innerWidth <= 576) return [];
21
+
22
+ const visibleActions = props.actions.filter(a => !(typeof a.visible !== 'undefined' && !a.visible) && !a.separator);
23
+ if (visibleActions.length <= 2) return visibleActions;
24
+
25
+ return visibleActions.filter(a => a.quickAction);
26
+ });
27
+
28
+ const visibleActionCount = computed(() => {
29
+ return props.actions.filter(a => !(typeof a.visible !== 'undefined' && !a.visible) && !a.separator).length;
30
+ });
31
+
32
+ const isMenuOpen = ref(false);
33
+
34
+ const menuElement = useTemplateRef('menuElement');
35
+ function onMenu(event) {
36
+ isMenuOpen.value = true;
37
+ menuElement.value.open(event, event.currentTarget);
38
+ }
39
+
40
+ </script>
41
+
42
+ <template>
43
+ <component
44
+ :is="href ? 'a' : 'div'"
45
+ :href="href"
46
+ class="pankow-list-item"
47
+ :class="{ 'pankow-list-item-link': href }"
48
+ >
49
+ <div class="pankow-list-item-left">
50
+ <slot name="left" />
51
+ </div>
52
+ <div class="pankow-list-item-center">
53
+ <div class="pankow-list-item-label">{{ label }}</div>
54
+ <div v-if="subtext" class="pankow-list-item-subtext">{{ subtext }}</div>
55
+ </div>
56
+ <div class="pankow-list-item-right">
57
+ <Icon v-if="href" icon="fa-solid fa-chevron-right" class="pankow-list-item-chevron" />
58
+ <div v-else-if="visibleActionCount > 0" class="pankow-list-item-action-bar" :class="{ 'is-menu-open': isMenuOpen }">
59
+ <Menu ref="menuElement" :model="actions" @close="isMenuOpen = false" />
60
+ <ButtonGroup class="pankow-list-item-action-bar-quick-actions">
61
+ <Button tool v-for="quickAction in quickActions" :key="quickAction" :icon="quickAction.icon" @click="quickAction.action && quickAction.action()" :href="quickAction.href || null" :target="quickAction.target || null" v-tooltip.top="quickAction.label"/>
62
+ <Button tool @click.capture="onMenu($event)" icon="fa-solid fa-ellipsis" v-if="visibleActionCount > 0 && visibleActionCount !== quickActions.length"/>
63
+ </ButtonGroup>
64
+ <Button tool :plain="isMenuOpen ? null : true" secondary @click.capture="onMenu($event)" icon="fa-solid fa-ellipsis" v-if="visibleActionCount > 0" class="pankow-list-item-action-bar-menu-trigger"/>
65
+ </div>
66
+ </div>
67
+ </component>
68
+ </template>
69
+
70
+ <style scoped>
71
+
72
+ .pankow-list-item {
73
+ display: flex;
74
+ justify-content: space-between;
75
+ align-items: center;
76
+ gap: 10px;
77
+ padding: 12px 16px;
78
+ background: var(--pankow-body-background-color);
79
+ border-radius: var(--pankow-border-radius);
80
+ color: inherit;
81
+ transition: background-color 0.15s;
82
+ }
83
+
84
+ .pankow-list-item-link {
85
+ text-decoration: none;
86
+ cursor: pointer;
87
+ }
88
+
89
+ .pankow-list-item:hover {
90
+ background: var(--pankow-color-background-hover);
91
+ }
92
+
93
+ .pankow-list-item-left {
94
+ display: flex;
95
+ align-items: center;
96
+ flex-shrink: 0;
97
+ }
98
+
99
+ .pankow-list-item-center {
100
+ display: flex;
101
+ flex-direction: column;
102
+ min-width: 0;
103
+ flex: 1;
104
+ }
105
+
106
+ .pankow-list-item-label {
107
+ font-size: 15px;
108
+ font-weight: var(--pankow-font-weight-bold);
109
+ }
110
+
111
+ .pankow-list-item-subtext {
112
+ font-size: 13px;
113
+ color: var(--pankow-color-text-secondary);
114
+ overflow: hidden;
115
+ text-overflow: ellipsis;
116
+ }
117
+
118
+ .pankow-list-item-right {
119
+ flex-shrink: 0;
120
+ display: flex;
121
+ align-items: center;
122
+ }
123
+
124
+ .pankow-list-item-chevron {
125
+ font-size: 12px;
126
+ color: #999;
127
+ }
128
+
129
+ .pankow-list-item-action-bar {
130
+ display: flex;
131
+ gap: 5px;
132
+ justify-content: end;
133
+ min-height: 31px;
134
+ align-items: center;
135
+ min-width: 55px;
136
+ }
137
+
138
+ .pankow-list-item-action-bar-menu-trigger {
139
+ display: none;
140
+ }
141
+
142
+ .pankow-list-item-action-bar-quick-actions {
143
+ display: flex;
144
+ }
145
+
146
+ .pankow-list-item-action-bar .pankow-list-item-action-bar-quick-actions .pankow-button {
147
+ background-color: var(--pankow-input-background-color);
148
+ color: var(--pankow-color-text);
149
+ border: 1px solid transparent;
150
+ }
151
+
152
+ .pankow-list-item-action-bar .pankow-list-item-action-bar-quick-actions .pankow-button:hover {
153
+ color: var(--pankow-color-primary);
154
+ }
155
+
156
+ @media (hover: hover) {
157
+ .pankow-list-item-action-bar-quick-actions {
158
+ display: none;
159
+ }
160
+
161
+ .pankow-list-item-action-bar-menu-trigger {
162
+ display: flex;
163
+ }
164
+
165
+ .pankow-list-item:hover .pankow-list-item-action-bar-quick-actions {
166
+ display: flex;
167
+ }
168
+
169
+ .pankow-list-item:hover .pankow-list-item-action-bar-menu-trigger {
170
+ display: none;
171
+ }
172
+ }
173
+
174
+ @media (hover: none) {
175
+ .pankow-list-item-action-bar-quick-actions {
176
+ display: flex;
177
+ }
178
+
179
+ .pankow-list-item-action-bar-menu-trigger {
180
+ display: none;
181
+ }
182
+ }
183
+
184
+ .pankow-list-item-action-bar.is-menu-open .pankow-list-item-action-bar-menu-trigger {
185
+ display: block !important;
186
+ }
187
+
188
+ </style>
@@ -1,71 +1,19 @@
1
1
  <template>
2
- <teleport to="#app">
3
- <TransitionGroup name="pankow-notification-fade" :class="`pankow-notification-container ${position}`" tag="div">
4
- <div v-for="message in messages" key="id">
5
- <div class="pankow-notification" :[message.type]="''">
6
- {{ message.text }}
7
- <Icon @click="onClose(message.id)" icon="fa-solid fa-xmark" class="pankow-notification-close"/>
8
- </div>
2
+ <TransitionGroup name="pankow-notification-fade" :class="`pankow-notification-container ${position}`" tag="div">
3
+ <div v-for="message in messages" :key="message.id">
4
+ <div class="pankow-notification" :[message.type]="''">
5
+ {{ message.text }}
6
+ <Icon @click="removeNotify(message.id)" icon="fa-solid fa-xmark" class="pankow-notification-close"/>
9
7
  </div>
10
- </TransitionGroup>
11
- </teleport>
8
+ </div>
9
+ </TransitionGroup>
12
10
  </template>
13
11
 
14
- <script>
15
-
16
- import { uuidv4 } from '../utils.js';
12
+ <script setup>
17
13
 
14
+ import { messages, position, removeNotify } from '../notifyState.js';
18
15
  import Icon from './Icon.vue';
19
16
 
20
- export default {
21
- name: 'Notification',
22
- components: {
23
- Icon
24
- },
25
- props: {
26
- position: {
27
- type: String,
28
- default: 'top-center',
29
- validator(value, props) {
30
- return [ 'top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right' ].includes(value);
31
- }
32
- }
33
- },
34
- data() {
35
- return {
36
- messages: []
37
- }
38
- },
39
- methods: {
40
- raise(options) {
41
- const id = uuidv4();
42
- const text = typeof options === 'object' && 'text' in options ? options.text : options;
43
- const persistent = typeof options === 'object' && 'persistent' in options ? options.persistent : false;
44
- const timeout = typeof options === 'object' && 'timeout' in options ? options.timeout : 2000;
45
- const type = typeof options === 'object' && 'type' in options ? options.type : null;
46
-
47
- this.messages.push({ id, text, persistent, type });
48
-
49
- if (!persistent) {
50
- setTimeout(() => {
51
- const index = this.messages.findIndex((value) => value.id === id);
52
- if (index > -1) this.messages.splice(index, 1);
53
- }, timeout);
54
- }
55
- },
56
- onClose(id) {
57
- const index = this.messages.findIndex((value) => value.id === id);
58
- if (index > -1) this.messages.splice(index, 1);
59
- }
60
- },
61
- mounted() {
62
- if (!window.pankow) window.pankow = {};
63
- if (window.pankow.notify) return console.error('Pankow: Notification DOM node already exists! Only ever use a single one.');
64
-
65
- window.pankow.notify = (options) => { this.raise(options); }
66
- }
67
- };
68
-
69
17
  </script>
70
18
 
71
19
  <style>
@@ -14,39 +14,34 @@
14
14
  </div>
15
15
  </template>
16
16
 
17
- <script>
18
-
19
- export default {
20
- name: 'TabView',
21
- emits: [ 'changed' ],
22
- props: {
23
- tabs: {
24
- type: Object,
25
- default: {}
26
- },
27
- defaultActive: String,
28
- tabPosition: {
29
- type: String,
30
- default: 'top',
31
- }
32
- },
33
- data() {
34
- return {
35
- active: ''
36
- };
37
- },
38
- mounted() {
39
- this.active = this.defaultActive;
40
- },
41
- methods: {
42
- open(key) {
43
- if (this.active === key) return;
17
+ <script setup>
44
18
 
45
- this.active = key;
46
- this.$emit('changed', key);
47
- }
19
+ import { ref, onMounted } from 'vue';
20
+
21
+ const emit = defineEmits([ 'changed' ]);
22
+ const props = defineProps({
23
+ tabs: {
24
+ type: Object,
25
+ default: {}
26
+ },
27
+ defaultActive: String,
28
+ tabPosition: {
29
+ type: String,
30
+ default: 'top',
48
31
  }
49
- };
32
+ });
33
+
34
+ const active = ref('');
35
+
36
+ onMounted(() => {
37
+ active.value = props.defaultActive;
38
+ });
39
+
40
+ function open(key) {
41
+ if (active.value === key) return;
42
+ active.value = key;
43
+ emit('changed', key);
44
+ }
50
45
 
51
46
  </script>
52
47
 
package/fallbackImage.js CHANGED
@@ -1,20 +1,20 @@
1
- /*
2
-
3
- FallbackImage Directive
4
-
5
- ```
6
- import fallbackImage from 'pankow/fallbackImage';
7
-
8
- app.directive('fallback-image', fallbackImage);
9
-
10
- ...
11
-
12
- <im v-fallback-image="'/fallback.png'"/>
13
-
14
- ```
15
-
16
- */
17
-
1
+ /**
2
+ * FallbackImage Directive
3
+ *
4
+ * Usage:
5
+ * ```
6
+ * import fallbackImage from 'pankow/fallbackImage';
7
+ * app.directive('fallback-image', fallbackImage);
8
+ * // <img v-fallback-image="'/fallback.png'"/>
9
+ * ```
10
+ */
11
+
12
+ /**
13
+ * Vue directive `mounted` hook. Sets fallback src on image load error.
14
+ * @param {HTMLImageElement} el
15
+ * @param {import('vue').DirectiveBinding} binding - binding.value is the fallback URL
16
+ * @param {import('vue').VNode} vnode
17
+ */
18
18
  function mounted(el, binding, vnode) {
19
19
  el.addEventListener('error', () => {
20
20
  console.log('image loading failed, try to load', binding.value);
@@ -22,6 +22,10 @@ function mounted(el, binding, vnode) {
22
22
  });
23
23
  }
24
24
 
25
+ /**
26
+ * Vue directive for image fallback. Register via `app.directive('fallback-image', fallbackImage)`.
27
+ * @type {import('vue').Directive}
28
+ */
25
29
  const fallbackImage = {
26
30
  mounted
27
31
  };
package/fetcher.js CHANGED
@@ -1,4 +1,12 @@
1
1
 
2
+ /**
3
+ * Shared configuration for all HTTP requests.
4
+ * @typedef {Object} FetcherGlobalOptions
5
+ * @property {string} credentials - Fetch credentials mode
6
+ * @property {string} redirect - Fetch redirect mode
7
+ * @property {?function(Response|Error): void} errorHook - Called on non-2xx responses or network errors
8
+ */
9
+
2
10
  const globalOptions = {
3
11
  credentials: 'same-origin',
4
12
  redirect: 'error',
@@ -16,6 +24,7 @@ const globalOptions = {
16
24
  * @param {Object} query Plain key/value map serialized as the URL query string
17
25
  * @param {Object|FormData|null} body
18
26
  * @param {RequestInit} [options] Extra fetch options; `signal` is supported for cancellation
27
+ * @returns {Promise<{ status: number, body: any }>}
19
28
  */
20
29
  async function request(uri, method, headers, query, body, options) {
21
30
  let response, responseBody;
@@ -64,27 +73,60 @@ async function request(uri, method, headers, query, body, options) {
64
73
  return { status: response.status, body: responseBody };
65
74
  }
66
75
 
67
- /** @param {RequestInit} [options] Merged into `fetch()`; use `{ signal }` to abort. */
76
+ /**
77
+ * Sends a HEAD request.
78
+ * @param {string} uri
79
+ * @param {Object} [query={}] - Query parameters serialized as URL query string
80
+ * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
81
+ * @returns {Promise<{ status: number, body: * }>}
82
+ */
68
83
  async function head(uri, query = {}, options = {}) {
69
84
  return await request(uri, 'HEAD', {}, query, null, options);
70
85
  }
71
86
 
72
- /** @param {RequestInit} [options] Merged into `fetch()`; use `{ signal }` to abort. */
87
+ /**
88
+ * Sends a GET request.
89
+ * @param {string} uri
90
+ * @param {Object} [query={}] - Query parameters serialized as URL query string
91
+ * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
92
+ * @returns {Promise<{ status: number, body: * }>}
93
+ */
73
94
  async function get(uri, query = {}, options = {}) {
74
95
  return await request(uri, 'GET', {}, query, null, options);
75
96
  }
76
97
 
77
- /** @param {RequestInit} [options] Merged into `fetch()`; use `{ signal }` to abort. */
98
+ /**
99
+ * Sends a POST request with a JSON or FormData body.
100
+ * @param {string} uri
101
+ * @param {Object|FormData} body - Request body; objects are JSON-stringified
102
+ * @param {Object} [query={}] - Query parameters serialized as URL query string
103
+ * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
104
+ * @returns {Promise<{ status: number, body: * }>}
105
+ */
78
106
  async function post(uri, body, query = {}, options = {}) {
79
107
  return await request(uri, 'POST', {}, query, body, options);
80
108
  }
81
109
 
82
- /** @param {RequestInit} [options] Merged into `fetch()`; use `{ signal }` to abort. */
110
+ /**
111
+ * Sends a PUT request with a JSON or FormData body.
112
+ * @param {string} uri
113
+ * @param {Object|FormData} body - Request body; objects are JSON-stringified
114
+ * @param {Object} [query={}] - Query parameters serialized as URL query string
115
+ * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
116
+ * @returns {Promise<{ status: number, body: * }>}
117
+ */
83
118
  async function put(uri, body, query = {}, options = {}) {
84
119
  return await request(uri, 'PUT', {}, query, body, options);
85
120
  }
86
121
 
87
- /** @param {RequestInit} [options] Merged into `fetch()`; use `{ signal }` to abort. */
122
+ /**
123
+ * Sends a DELETE request with an optional body.
124
+ * @param {string} uri
125
+ * @param {Object|FormData} body - Request body; objects are JSON-stringified
126
+ * @param {Object} [query={}] - Query parameters serialized as URL query string
127
+ * @param {RequestInit} [options={}] - Merged into `fetch()`; use `{ signal }` to abort
128
+ * @returns {Promise<{ status: number, body: * }>}
129
+ */
88
130
  async function del(uri, body, query = {}, options = {}) {
89
131
  return await request(uri, 'DELETE', {}, query, body, options);
90
132
  }
package/gestures.js CHANGED
@@ -1,15 +1,19 @@
1
- /*
1
+ /**
2
+ * Swipe handler
3
+ *
4
+ * Usage:
5
+ * ```
6
+ * import { onSwipe } from 'pankow/gestures';
7
+ * onSwipe(element, (direction) => {});
8
+ * ```
9
+ */
2
10
 
3
- Swipe handler
4
-
5
- ```
6
- import { onSwipe } from 'pankow/guesture';
7
-
8
- onSwipe(element, (direction) => {});
9
-
10
- ```
11
-
12
- */
11
+ /**
12
+ * Registers touch event listeners on an element and calls the callback on swipe.
13
+ * @param {HTMLElement} elem
14
+ * @param {function('left'|'right'|'up'|'down'): void} callback
15
+ * @param {number} [threshold=50] - Minimum pixel distance to trigger a swipe
16
+ */
13
17
  function onSwipe(elem, callback, threshold = 50) {
14
18
  callback = typeof callback === 'function' ? callback : () => {};
15
19
 
package/index.js CHANGED
@@ -5,6 +5,8 @@ import "@fontsource/inter/500.css";
5
5
  import '@fortawesome/fontawesome-free/css/all.min.css';
6
6
  import './style.css';
7
7
 
8
+ import pankowPlugin from './plugin.js';
9
+
8
10
  // Order of import matters due to css rule matching!
9
11
  import BottomBar from './components/BottomBar.vue';
10
12
  import BreadCrumb from './components/BreadCrumb.vue';
@@ -21,6 +23,7 @@ import FileUploader from './components/FileUploader.vue';
21
23
  import FormGroup from './components/FormGroup.vue';
22
24
  import Icon from './components/Icon.vue';
23
25
  import InputDialog from './components/InputDialog.vue';
26
+ import ListItem from './components/ListItem.vue';
24
27
  import LoginView from './components/LoginView.vue';
25
28
  import MainLayout from './components/MainLayout.vue';
26
29
  import MaskedInput from './components/MaskedInput.vue';
@@ -72,6 +75,7 @@ export {
72
75
  FormGroup,
73
76
  InputGroup,
74
77
  Icon,
78
+ ListItem,
75
79
  InputDialog,
76
80
  LoginView,
77
81
  MainLayout,
@@ -111,3 +115,6 @@ export {
111
115
  export { CheckBox as Checkbox }; /* deprecated */
112
116
  export { RadioButton as Radiobutton }; /* deprecated */
113
117
  export { BreadCrumb as Breadcrumb }; /* deprecated */
118
+
119
+ export { useNotify } from './notifyState.js';
120
+ export default pankowPlugin;
package/notifyState.js ADDED
@@ -0,0 +1,48 @@
1
+ import { reactive, ref } from 'vue';
2
+ import { uuidv4 } from './utils.js';
3
+
4
+ const messages = reactive([]);
5
+ const position = ref('top-center');
6
+
7
+ function pushNotify(options) {
8
+ const id = uuidv4();
9
+ const text = typeof options === 'object' && 'text' in options ? options.text : options;
10
+ const persistent = typeof options === 'object' && 'persistent' in options ? options.persistent : false;
11
+ const timeout = typeof options === 'object' && 'timeout' in options ? options.timeout : 2000;
12
+ const type = typeof options === 'object' && 'type' in options ? options.type : null;
13
+
14
+ messages.push({ id, text, persistent, type });
15
+
16
+ if (!persistent) {
17
+ setTimeout(() => {
18
+ const index = messages.findIndex((value) => value.id === id);
19
+ if (index > -1) messages.splice(index, 1);
20
+ }, timeout);
21
+ }
22
+ }
23
+
24
+ function removeNotify(id) {
25
+ const index = messages.findIndex((value) => value.id === id);
26
+ if (index > -1) messages.splice(index, 1);
27
+ }
28
+
29
+ function useNotify() {
30
+ return { notify: pushNotify };
31
+ }
32
+
33
+ function setNotifyPosition(pos) {
34
+ position.value = pos;
35
+ }
36
+
37
+ if (typeof window !== 'undefined') {
38
+ if (!window.pankow) window.pankow = {};
39
+ if (window.pankow.notify) {
40
+ console.error('Pankow: Notification already registered! Only ever use a single one.');
41
+ } else {
42
+ window.pankow.notify = pushNotify;
43
+ }
44
+
45
+ window.pankow.setNotifyPosition = setNotifyPosition;
46
+ }
47
+
48
+ export { messages, position, pushNotify, removeNotify, setNotifyPosition, useNotify };
package/package.json CHANGED
@@ -1,10 +1,25 @@
1
1
  {
2
2
  "name": "@cloudron/pankow",
3
3
  "private": false,
4
- "version": "4.2.3",
5
- "description": "",
4
+ "version": "4.3.1",
5
+ "description": "Vue 3 UI component library for Cloudron apps",
6
6
  "main": "index.js",
7
7
  "types": "types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./index.js",
11
+ "types": "./types/index.d.ts"
12
+ },
13
+ "./*": "./*",
14
+ "./vite-plugin": "./vite-plugin.js"
15
+ },
16
+ "keywords": [
17
+ "vue",
18
+ "vue3",
19
+ "cloudron",
20
+ "ui",
21
+ "components"
22
+ ],
8
23
  "files": [
9
24
  "*.css",
10
25
  "*.js",
@@ -28,6 +43,9 @@
28
43
  "monaco-editor": "^0.55.1",
29
44
  "online-3d-viewer": "^0.18.0"
30
45
  },
46
+ "peerDependencies": {
47
+ "vue": "^3.5.0"
48
+ },
31
49
  "devDependencies": {
32
50
  "@highlightjs/vue-plugin": "^2.1.0",
33
51
  "@vitejs/plugin-vue": "^6.0.7",
package/plugin.js ADDED
@@ -0,0 +1,39 @@
1
+ import { createApp } from 'vue';
2
+ import tooltip from './tooltip.js';
3
+ import fallbackImage from './fallbackImage.js';
4
+ import fetcher from './fetcher.js';
5
+ import { setNotifyPosition } from './notifyState.js';
6
+ import Notification from './components/Notification.vue';
7
+
8
+ const CONTAINER_ID = 'pankow-notification-root';
9
+
10
+ export default {
11
+ install(app, options = {}) {
12
+ app.directive('tooltip', tooltip);
13
+ app.directive('fallback-image', fallbackImage);
14
+
15
+ if (options.fetcher) {
16
+ if (options.fetcher.credentials !== undefined) {
17
+ fetcher.globalOptions.credentials = options.fetcher.credentials;
18
+ }
19
+ if (options.fetcher.errorHook !== undefined) {
20
+ fetcher.globalOptions.errorHook = options.fetcher.errorHook;
21
+ }
22
+ }
23
+
24
+ if (options.notification?.position) {
25
+ setNotifyPosition(options.notification.position);
26
+ }
27
+
28
+ if (typeof document !== 'undefined') {
29
+ const existing = document.getElementById(CONTAINER_ID);
30
+ if (existing) existing.remove();
31
+
32
+ const container = document.createElement('div');
33
+ container.id = CONTAINER_ID;
34
+ document.body.appendChild(container);
35
+
36
+ createApp(Notification).mount(container);
37
+ }
38
+ }
39
+ };