@cloudron/pankow 4.2.2 → 4.3.0

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,7 +1,7 @@
1
1
  <script setup>
2
2
 
3
3
  import { computed } from 'vue';
4
- import Checkbox from './Checkbox.vue';
4
+ import CheckBox from './CheckBox.vue';
5
5
  import Icon from './Icon.vue';
6
6
 
7
7
  const emit = defineEmits(['activated']);
@@ -43,7 +43,7 @@ function onActivated() {
43
43
 
44
44
  <template>
45
45
  <div class="pankow-menu-item" :class="{'pankow-menu-item-disabled': isDisabled }" @click.stop="onActivated" :separator="item.separator" v-if="isVisible" tabindex="0" @keydown.enter.stop="onActivated" @keydown.space.stop="onActivated">
46
- <Checkbox v-model="item.checked" v-if="item.checkbox" style="margin-right: 4px; display: inline-flex;"/>
46
+ <CheckBox v-model="item.checked" v-if="item.checkbox" style="margin-right: 4px; display: inline-flex;"/>
47
47
  <span v-if="!item.separator" :style="{ width: (hasIcons && !item.checkbox) ? '29px' : 0 }"><Icon v-show="hasIcons" :icon="item.icon"/></span>{{ item.label }}
48
48
  <div class="pankow-menu-item-separator-line" v-if="item.separator"></div>
49
49
  </div>
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
2
 
3
3
  import { computed } from 'vue';
4
- import Checkbox from './Checkbox.vue';
4
+ import CheckBox from './CheckBox.vue';
5
5
  import Icon from './Icon.vue';
6
6
 
7
7
  const emit = defineEmits(['activated']);
@@ -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>
@@ -7,37 +7,21 @@
7
7
  </span>
8
8
  </template>
9
9
 
10
- <script>
10
+ <script setup>
11
11
 
12
12
  import { uuidv4 } from '../utils.js';
13
13
 
14
- export default {
15
- name: 'Radiobutton',
16
- props: {
17
- modelValue: String,
18
- value: String,
19
- label: String,
20
- id: String,
21
- name: String,
22
- disabled: null
23
- },
24
- emits: ['update:modelValue'],
25
- computed: {
26
- internalValue: {
27
- get() {
28
- return this.modelValue
29
- },
30
- set(internalValue) {
31
- this.$emit('update:modelValue', internalValue)
32
- }
33
- }
34
- },
35
- data () {
36
- return {
37
- internalId: uuidv4()
38
- }
39
- }
40
- };
14
+ defineProps({
15
+ value: String,
16
+ label: String,
17
+ id: String,
18
+ name: String,
19
+ disabled: null
20
+ });
21
+
22
+ const internalValue = defineModel({ type: String });
23
+
24
+ const internalId = uuidv4();
41
25
 
42
26
  </script>
43
27
 
@@ -0,0 +1,113 @@
1
+ <script setup>
2
+
3
+ import { useTemplateRef, ref, onMounted, onUnmounted } from 'vue';
4
+
5
+ const mobileFilterBar = useTemplateRef('mobileFilterBar');
6
+
7
+ const isMobile = ref(false);
8
+
9
+ defineProps({
10
+ title: String,
11
+ });
12
+
13
+ function checkForMobile() {
14
+ isMobile.value = window.innerWidth <= 576;
15
+ }
16
+
17
+ onMounted(() => {
18
+ checkForMobile();
19
+ window.addEventListener('resize', checkForMobile);
20
+ });
21
+
22
+ onUnmounted(() => {
23
+ window.removeEventListener('resize', checkForMobile);
24
+ });
25
+
26
+ </script>
27
+
28
+ <template>
29
+ <div class="pankow-section-item">
30
+ <h2 class="pankow-section-item-header">
31
+ <div>
32
+ <div class="pankow-section-item-header-title-text">
33
+ <slot name="header-title">{{ title }}</slot>
34
+ <slot name="header-title-extra"></slot>
35
+ </div>
36
+ </div>
37
+ <div>
38
+ <Teleport :disabled="!isMobile" :to="mobileFilterBar">
39
+ <slot name="filter-bar"></slot>
40
+ </Teleport>
41
+ <slot name="header-buttons"></slot>
42
+ </div>
43
+ </h2>
44
+ <div class="pankow-section-item-mobile-filter-bar" v-show="isMobile && $slots['filter-bar']" ref="mobileFilterBar"></div>
45
+ <hr class="pankow-section-item-divider"/>
46
+ <div class="pankow-section-item-body">
47
+ <slot></slot>
48
+ </div>
49
+ </div>
50
+ </template>
51
+
52
+ <style>
53
+
54
+ .pankow-section-item {
55
+ margin-bottom: 20px;
56
+ position: relative;
57
+ }
58
+
59
+ .pankow-section-item-header {
60
+ display: flex;
61
+ flex-wrap: wrap;
62
+ justify-content: space-between;
63
+ align-items: center;
64
+ gap: 5px;
65
+ padding-left: 15px;
66
+ padding-right: 15px;
67
+ padding-top: 5px;
68
+ margin-top: 0;
69
+ margin-bottom: 0;
70
+ font-size: 24px;
71
+ font-weight: var(--pankow-font-weight-bold);
72
+ }
73
+
74
+ .pankow-section-item-header > div {
75
+ display: flex;
76
+ gap: 6px;
77
+ flex-wrap: wrap;
78
+ align-items: center;
79
+ }
80
+
81
+ .pankow-section-item-header-title-text {
82
+ display: flex;
83
+ gap: 6px;
84
+ align-items: baseline;
85
+ }
86
+
87
+ .pankow-section-item-divider {
88
+ border: 0;
89
+ border-top: 1px solid #d8dee4;
90
+ margin-top: 10px;
91
+ margin-bottom: 5px;
92
+ }
93
+
94
+ @media (prefers-color-scheme: dark) {
95
+ .pankow-section-item-divider {
96
+ border-top-color: #495057;
97
+ }
98
+ }
99
+
100
+ .pankow-section-item-body {
101
+ position: relative;
102
+ margin-bottom: 15px;
103
+ padding: 10px 15px 25px;
104
+ }
105
+
106
+ .pankow-section-item-mobile-filter-bar {
107
+ display: flex;
108
+ gap: 6px;
109
+ flex-wrap: wrap;
110
+ margin: 10px 15px;
111
+ }
112
+
113
+ </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