@conduction/nextcloud-vue 2.94.0 → 2.95.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.
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +46 -28
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +17 -10
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js.map +1 -1
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +56 -16
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js +9 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js.map +1 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js +240 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js +65 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnDeviceFramesWidget/index.js +30 -0
- package/dist/esm/components/CnDeviceFramesWidget/index.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +17 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +30 -31
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js +6 -0
- package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js.map +1 -0
- package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js +90 -0
- package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +48 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +20 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +230 -25
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +447 -221
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +18 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +18 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +356 -114
- package/dist/esm/utils/libraryWidgetKeys.js +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +6964 -5943
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +356 -114
- package/l10n/en.json +18 -1
- package/l10n/nl.json +18 -1
- package/package.json +1 -1
- package/src/components/CnAppRoot/CnAppRoot.vue +20 -0
- package/src/components/CnBrandBar/CnBrandBar.vue +5 -4
- package/src/components/CnDetailPage/CnDetailPage.vue +55 -15
- package/src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue +360 -0
- package/src/components/CnDeviceFramesWidget/index.js +29 -0
- package/src/components/CnFactsWidget/CnFactsWidget.vue +46 -2
- package/src/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue +107 -0
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +81 -1
- package/src/components/CnRegisterMapping/CnRegisterMapping.md +10 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +511 -28
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/index.js +1 -0
- package/src/index.js +1 -0
- package/src/schemas/app-manifest-v2.schema.json +6 -1
- package/src/utils/libraryWidgetKeys.js +1 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import script from './CnDeviceFramesWidget.vue2.js';
|
|
2
|
+
import { render } from './CnDeviceFramesWidget.vue3.js';
|
|
3
|
+
|
|
4
|
+
script.render = render;
|
|
5
|
+
script.__scopeId = "data-v-05f76090";
|
|
6
|
+
const __sfc_main = script;
|
|
7
|
+
|
|
8
|
+
export { __sfc_main as default };
|
|
9
|
+
//# sourceMappingURL=CnDeviceFramesWidget.vue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnDeviceFramesWidget.vue.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { markRaw } from 'vue';
|
|
2
|
+
import CloudCheckOutline from 'vue-material-design-icons/CloudCheckOutline.vue';
|
|
3
|
+
import CloudOffOutline from 'vue-material-design-icons/CloudOffOutline.vue';
|
|
4
|
+
import InformationOutline from 'vue-material-design-icons/InformationOutline.vue';
|
|
5
|
+
import __sfc_main from '../CnSectionsWidget/CnSectionsWidget.vue.js';
|
|
6
|
+
|
|
7
|
+
/** Counter for unique heading ids across instances. */
|
|
8
|
+
let instanceCounter = 0;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* CnDeviceFramesWidget - the `device-frames` widget: several small pages side
|
|
12
|
+
* by side, each in a phone frame with its own header (board DqInspectie,
|
|
13
|
+
* "Inspectie in het veld": the day offline, a checklist question with a
|
|
14
|
+
* photo, and the sync queue).
|
|
15
|
+
*
|
|
16
|
+
* Each frame stacks widgets of the surface, as the `sections` widget does,
|
|
17
|
+
* under a header with the frame's title and an optional status line. The
|
|
18
|
+
* status can follow the connection ("Offline · synced at 07:42" while the
|
|
19
|
+
* device is offline). The frames wrap onto the next row when the page is
|
|
20
|
+
* narrower, and a frame takes the full width on a phone.
|
|
21
|
+
*
|
|
22
|
+
* ```json
|
|
23
|
+
* {
|
|
24
|
+
* "type": "device-frames",
|
|
25
|
+
* "content": {
|
|
26
|
+
* "width": 350,
|
|
27
|
+
* "status": { "followConnection": true, "offlineText": "Offline · synced at 07:42", "onlineText": "Online" },
|
|
28
|
+
* "frames": [
|
|
29
|
+
* { "key": "today", "title": "Today", "widgetIds": ["today-runs", "offline-note"] },
|
|
30
|
+
* { "key": "question", "title": "Kerkplein 7", "widgetIds": ["checklist-run"] },
|
|
31
|
+
* { "key": "sync", "title": "Synchronise", "widgetIds": ["sync-queue"] }
|
|
32
|
+
* ]
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @spec openspec/changes/device-frames-widget/specs/device-frames-widget/spec.md#requirement-a-page-can-show-several-small-pages-as-phone-frames
|
|
38
|
+
*/
|
|
39
|
+
var script = {
|
|
40
|
+
name: 'CnDeviceFramesWidget',
|
|
41
|
+
|
|
42
|
+
components: { CnSectionsWidget: __sfc_main },
|
|
43
|
+
|
|
44
|
+
inject: {
|
|
45
|
+
cnTranslate: { default: () => (key) => key },
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
props: {
|
|
49
|
+
/**
|
|
50
|
+
* The widget configuration.
|
|
51
|
+
*
|
|
52
|
+
* - `frames` — `[{ key?, title, widgetIds? | sections?, status? }]`, in order. `widgetIds` names
|
|
53
|
+
* widgets of the surface, stacked; `sections` takes the `sections` widget's entries instead.
|
|
54
|
+
* - `status` — the status line of every frame (a frame's own `status` wins): `{ text?, tone?,
|
|
55
|
+
* followConnection?, onlineText?, offlineText? }`. With `followConnection` the line reads
|
|
56
|
+
* `offlineText` while the device is offline and `onlineText` otherwise. `tone` is `warning`
|
|
57
|
+
* (default while offline), `success` or `info`.
|
|
58
|
+
* - `width` — the frame width in pixels (default 350).
|
|
59
|
+
*
|
|
60
|
+
* @type {{frames?: Array<object>, status?: object, width?: number}}
|
|
61
|
+
*/
|
|
62
|
+
content: {
|
|
63
|
+
type: Object,
|
|
64
|
+
default: () => ({}),
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/** Sibling widget definitions on the surface, for `widgetIds`. */
|
|
68
|
+
availableWidgets: {
|
|
69
|
+
type: Array,
|
|
70
|
+
default: () => [],
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/** The bound record's id, forwarded to the children. */
|
|
74
|
+
objectId: {
|
|
75
|
+
type: [String, Number],
|
|
76
|
+
default: '',
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/** The loaded record, forwarded to the children. */
|
|
80
|
+
objectData: {
|
|
81
|
+
type: Object,
|
|
82
|
+
default: null,
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/** The resolved object-type slug. */
|
|
86
|
+
objectType: {
|
|
87
|
+
type: String,
|
|
88
|
+
default: '',
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/** The resolved JSON Schema object. */
|
|
92
|
+
schemaObject: {
|
|
93
|
+
type: Object,
|
|
94
|
+
default: null,
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/** OpenRegister register slug of the surface. */
|
|
98
|
+
register: {
|
|
99
|
+
type: [String, Object],
|
|
100
|
+
default: '',
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/** OpenRegister schema slug of the surface. */
|
|
104
|
+
schema: {
|
|
105
|
+
type: [String, Object],
|
|
106
|
+
default: '',
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/** The effective object store. */
|
|
110
|
+
store: {
|
|
111
|
+
type: Object,
|
|
112
|
+
default: null,
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/** Rendering surface forwarded to the children. */
|
|
116
|
+
surface: {
|
|
117
|
+
type: String,
|
|
118
|
+
default: 'app-dashboard',
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/** Object context forwarded to integration children. */
|
|
122
|
+
integrationContext: {
|
|
123
|
+
type: Object,
|
|
124
|
+
default: null,
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/** The consumer's component registry, for custom child widget types. */
|
|
128
|
+
cnRegistry: {
|
|
129
|
+
type: Object,
|
|
130
|
+
default: () => ({}),
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
data() {
|
|
135
|
+
instanceCounter += 1;
|
|
136
|
+
return {
|
|
137
|
+
uid: instanceCounter,
|
|
138
|
+
online: typeof navigator === 'undefined' || navigator.onLine !== false,
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
computed: {
|
|
143
|
+
/** @return {object} The frame width. */
|
|
144
|
+
frameStyle() {
|
|
145
|
+
const width = Number(this.content?.width);
|
|
146
|
+
return { '--cn-device-frame-width': `${Number.isFinite(width) && width >= 240 ? width : 350}px` }
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The frames to draw: a key, a translated title, the sections and the
|
|
151
|
+
* status line.
|
|
152
|
+
*
|
|
153
|
+
* @return {Array<{key: string, title: string, sections: Array<object>, status: object|null}>}
|
|
154
|
+
*/
|
|
155
|
+
frames() {
|
|
156
|
+
const list = Array.isArray(this.content?.frames) ? this.content.frames : [];
|
|
157
|
+
return list
|
|
158
|
+
.filter((frame) => frame && typeof frame === 'object')
|
|
159
|
+
.map((frame, index) => {
|
|
160
|
+
const sections = Array.isArray(frame.sections)
|
|
161
|
+
? frame.sections
|
|
162
|
+
: (Array.isArray(frame.widgetIds) ? frame.widgetIds : []).filter((id) => typeof id === 'string' && id !== '').map((widgetId) => ({ widgetId }));
|
|
163
|
+
return {
|
|
164
|
+
key: String(frame.key || `frame-${index}`).replace(/[^\w-]/g, '-'),
|
|
165
|
+
title: this.tr(frame.title || ''),
|
|
166
|
+
sections,
|
|
167
|
+
status: this.statusOf(frame.status || this.content?.status || null),
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
mounted() {
|
|
174
|
+
if (typeof window !== 'undefined') {
|
|
175
|
+
window.addEventListener('online', this.onConnection);
|
|
176
|
+
window.addEventListener('offline', this.onConnection);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
beforeUnmount() {
|
|
181
|
+
if (typeof window !== 'undefined') {
|
|
182
|
+
window.removeEventListener('online', this.onConnection);
|
|
183
|
+
window.removeEventListener('offline', this.onConnection);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
methods: {
|
|
188
|
+
/**
|
|
189
|
+
* Translate through the app's translate.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} key The source string.
|
|
192
|
+
* @return {string}
|
|
193
|
+
*/
|
|
194
|
+
tr(key) {
|
|
195
|
+
return key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The status line of a frame, or null.
|
|
200
|
+
*
|
|
201
|
+
* @param {object|null} status The configured status.
|
|
202
|
+
* @return {{text: string, tone: string, icon: object}|null}
|
|
203
|
+
*/
|
|
204
|
+
statusOf(status) {
|
|
205
|
+
if (!status || typeof status !== 'object') {
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
let text = status.text;
|
|
209
|
+
let tone = status.tone;
|
|
210
|
+
if (status.followConnection === true) {
|
|
211
|
+
text = this.online ? status.onlineText : status.offlineText;
|
|
212
|
+
tone = tone || (this.online ? 'success' : 'warning');
|
|
213
|
+
}
|
|
214
|
+
if (typeof text !== 'string' || text === '') {
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
const known = ['warning', 'success', 'info'].includes(tone) ? tone : 'info';
|
|
218
|
+
const icon = known === 'warning' ? CloudOffOutline : (known === 'success' ? CloudCheckOutline : InformationOutline);
|
|
219
|
+
return { text: this.tr(text), tone: known, icon: markRaw(icon) }
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
/** Follow the browser's connection state. */
|
|
223
|
+
onConnection() {
|
|
224
|
+
this.online = typeof navigator === 'undefined' || navigator.onLine !== false;
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The id of a frame's heading.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} key The frame key.
|
|
231
|
+
* @return {string}
|
|
232
|
+
*/
|
|
233
|
+
headingId(key) {
|
|
234
|
+
return `cn-device-frame-${this.uid}-${key}`
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export { script as default };
|
|
240
|
+
//# sourceMappingURL=CnDeviceFramesWidget.vue2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnDeviceFramesWidget.vue2.js","sources":["../../../../src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-device-frames-widget\" data-testid=\"cn-device-frames-widget\">\n\t\t<section\n\t\t\tv-for=\"frame in frames\"\n\t\t\t:key=\"frame.key\"\n\t\t\tclass=\"cn-device-frames-widget__frame\"\n\t\t\t:style=\"frameStyle\"\n\t\t\t:aria-labelledby=\"headingId(frame.key)\"\n\t\t\t:data-testid=\"`cn-device-frame-${frame.key}`\">\n\t\t\t<header class=\"cn-device-frames-widget__head\">\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"frame.status\"\n\t\t\t\t\tclass=\"cn-device-frames-widget__status\"\n\t\t\t\t\t:class=\"`cn-device-frames-widget__status--${frame.status.tone}`\"\n\t\t\t\t\tdata-testid=\"cn-device-frame-status\">\n\t\t\t\t\t<component :is=\"frame.status.icon\" :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t{{ frame.status.text }}\n\t\t\t\t</span>\n\t\t\t\t<h3 :id=\"headingId(frame.key)\" class=\"cn-device-frames-widget__title\">\n\t\t\t\t\t{{ frame.title }}\n\t\t\t\t</h3>\n\t\t\t</header>\n\t\t\t<div class=\"cn-device-frames-widget__body\">\n\t\t\t\t<CnSectionsWidget\n\t\t\t\t\t:content=\"{ sections: frame.sections, cards: false, hideEmpty: true }\"\n\t\t\t\t\t:availableWidgets=\"availableWidgets\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:objectData=\"objectData\"\n\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t:cnRegistry=\"cnRegistry\" />\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { markRaw } from 'vue'\nimport CloudCheckOutline from 'vue-material-design-icons/CloudCheckOutline.vue'\nimport CloudOffOutline from 'vue-material-design-icons/CloudOffOutline.vue'\nimport InformationOutline from 'vue-material-design-icons/InformationOutline.vue'\nimport CnSectionsWidget from '../CnSectionsWidget/CnSectionsWidget.vue'\n\n/** Counter for unique heading ids across instances. */\nlet instanceCounter = 0\n\n/**\n * CnDeviceFramesWidget - the `device-frames` widget: several small pages side\n * by side, each in a phone frame with its own header (board DqInspectie,\n * \"Inspectie in het veld\": the day offline, a checklist question with a\n * photo, and the sync queue).\n *\n * Each frame stacks widgets of the surface, as the `sections` widget does,\n * under a header with the frame's title and an optional status line. The\n * status can follow the connection (\"Offline · synced at 07:42\" while the\n * device is offline). The frames wrap onto the next row when the page is\n * narrower, and a frame takes the full width on a phone.\n *\n * ```json\n * {\n * \"type\": \"device-frames\",\n * \"content\": {\n * \"width\": 350,\n * \"status\": { \"followConnection\": true, \"offlineText\": \"Offline · synced at 07:42\", \"onlineText\": \"Online\" },\n * \"frames\": [\n * { \"key\": \"today\", \"title\": \"Today\", \"widgetIds\": [\"today-runs\", \"offline-note\"] },\n * { \"key\": \"question\", \"title\": \"Kerkplein 7\", \"widgetIds\": [\"checklist-run\"] },\n * { \"key\": \"sync\", \"title\": \"Synchronise\", \"widgetIds\": [\"sync-queue\"] }\n * ]\n * }\n * }\n * ```\n *\n * @spec openspec/changes/device-frames-widget/specs/device-frames-widget/spec.md#requirement-a-page-can-show-several-small-pages-as-phone-frames\n */\nexport default {\n\tname: 'CnDeviceFramesWidget',\n\n\tcomponents: { CnSectionsWidget },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `frames` — `[{ key?, title, widgetIds? | sections?, status? }]`, in order. `widgetIds` names\n\t\t * widgets of the surface, stacked; `sections` takes the `sections` widget's entries instead.\n\t\t * - `status` — the status line of every frame (a frame's own `status` wins): `{ text?, tone?,\n\t\t * followConnection?, onlineText?, offlineText? }`. With `followConnection` the line reads\n\t\t * `offlineText` while the device is offline and `onlineText` otherwise. `tone` is `warning`\n\t\t * (default while offline), `success` or `info`.\n\t\t * - `width` — the frame width in pixels (default 350).\n\t\t *\n\t\t * @type {{frames?: Array<object>, status?: object, width?: number}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Sibling widget definitions on the surface, for `widgetIds`. */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id, forwarded to the children. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, forwarded to the children. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to the children. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'app-dashboard',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tdata() {\n\t\tinstanceCounter += 1\n\t\treturn {\n\t\t\tuid: instanceCounter,\n\t\t\tonline: typeof navigator === 'undefined' || navigator.onLine !== false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object} The frame width. */\n\t\tframeStyle() {\n\t\t\tconst width = Number(this.content?.width)\n\t\t\treturn { '--cn-device-frame-width': `${Number.isFinite(width) && width >= 240 ? width : 350}px` }\n\t\t},\n\n\t\t/**\n\t\t * The frames to draw: a key, a translated title, the sections and the\n\t\t * status line.\n\t\t *\n\t\t * @return {Array<{key: string, title: string, sections: Array<object>, status: object|null}>}\n\t\t */\n\t\tframes() {\n\t\t\tconst list = Array.isArray(this.content?.frames) ? this.content.frames : []\n\t\t\treturn list\n\t\t\t\t.filter((frame) => frame && typeof frame === 'object')\n\t\t\t\t.map((frame, index) => {\n\t\t\t\t\tconst sections = Array.isArray(frame.sections)\n\t\t\t\t\t\t? frame.sections\n\t\t\t\t\t\t: (Array.isArray(frame.widgetIds) ? frame.widgetIds : []).filter((id) => typeof id === 'string' && id !== '').map((widgetId) => ({ widgetId }))\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(frame.key || `frame-${index}`).replace(/[^\\w-]/g, '-'),\n\t\t\t\t\t\ttitle: this.tr(frame.title || ''),\n\t\t\t\t\t\tsections,\n\t\t\t\t\t\tstatus: this.statusOf(frame.status || this.content?.status || null),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.addEventListener('online', this.onConnection)\n\t\t\twindow.addEventListener('offline', this.onConnection)\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.removeEventListener('online', this.onConnection)\n\t\t\twindow.removeEventListener('offline', this.onConnection)\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the app's translate.\n\t\t *\n\t\t * @param {string} key The source string.\n\t\t * @return {string}\n\t\t */\n\t\ttr(key) {\n\t\t\treturn key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')\n\t\t},\n\n\t\t/**\n\t\t * The status line of a frame, or null.\n\t\t *\n\t\t * @param {object|null} status The configured status.\n\t\t * @return {{text: string, tone: string, icon: object}|null}\n\t\t */\n\t\tstatusOf(status) {\n\t\t\tif (!status || typeof status !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet text = status.text\n\t\t\tlet tone = status.tone\n\t\t\tif (status.followConnection === true) {\n\t\t\t\ttext = this.online ? status.onlineText : status.offlineText\n\t\t\t\ttone = tone || (this.online ? 'success' : 'warning')\n\t\t\t}\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst known = ['warning', 'success', 'info'].includes(tone) ? tone : 'info'\n\t\t\tconst icon = known === 'warning' ? CloudOffOutline : (known === 'success' ? CloudCheckOutline : InformationOutline)\n\t\t\treturn { text: this.tr(text), tone: known, icon: markRaw(icon) }\n\t\t},\n\n\t\t/** Follow the browser's connection state. */\n\t\tonConnection() {\n\t\t\tthis.online = typeof navigator === 'undefined' || navigator.onLine !== false\n\t\t},\n\n\t\t/**\n\t\t * The id of a frame's heading.\n\t\t *\n\t\t * @param {string} key The frame key.\n\t\t * @return {string}\n\t\t */\n\t\theadingId(key) {\n\t\t\treturn `cn-device-frame-${this.uid}-${key}`\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-device-frames-widget {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-start;\n\tgap: 20px;\n}\n\n.cn-device-frames-widget__frame {\n\tdisplay: flex;\n\tflex: none;\n\tflex-direction: column;\n\twidth: var(--cn-device-frame-width, 350px);\n\tmax-width: 100%;\n\tmin-height: 600px;\n\tbox-sizing: border-box;\n\toverflow: hidden;\n\tborder: 10px solid var(--color-main-text);\n\tborder-radius: 36px;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-device-frames-widget__head {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n\tpadding: 14px 18px 10px;\n\tborder-bottom: 1px solid var(--color-border);\n\tbackground: var(--color-main-background);\n}\n\n.cn-device-frames-widget__status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n}\n\n.cn-device-frames-widget__status--warning {\n\tcolor: var(--color-warning-text, var(--color-main-text));\n}\n\n.cn-device-frames-widget__status--success {\n\tcolor: var(--color-success-text, var(--color-main-text));\n}\n\n.cn-device-frames-widget__status--info {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-device-frames-widget__title {\n\tmargin: 0;\n\tfont-size: 20px;\n\tfont-weight: 700;\n}\n\n.cn-device-frames-widget__body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 14px;\n}\n\n/* On a phone the frame is the screen: no bezel, the full width. */\n@media (max-width: 640px) {\n\t.cn-device-frames-widget__frame {\n\t\twidth: 100%;\n\t\tmin-height: 0;\n\t\tborder-width: 0;\n\t\tborder-radius: var(--cn-board-card-radius, 12px);\n\t}\n}\n</style>\n"],"names":["CnSectionsWidget"],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { resolveComponent, openBlock, createElementBlock, Fragment, renderList, normalizeStyle, createElementVNode, normalizeClass, createBlock, resolveDynamicComponent, createTextVNode, toDisplayString, createCommentVNode, createVNode } from 'vue';
|
|
2
|
+
|
|
3
|
+
const _hoisted_1 = {
|
|
4
|
+
class: "cn-device-frames-widget",
|
|
5
|
+
"data-testid": "cn-device-frames-widget"
|
|
6
|
+
};
|
|
7
|
+
const _hoisted_2 = ["aria-labelledby", "data-testid"];
|
|
8
|
+
const _hoisted_3 = { class: "cn-device-frames-widget__head" };
|
|
9
|
+
const _hoisted_4 = ["id"];
|
|
10
|
+
const _hoisted_5 = { class: "cn-device-frames-widget__body" };
|
|
11
|
+
|
|
12
|
+
function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
13
|
+
const _component_CnSectionsWidget = resolveComponent("CnSectionsWidget");
|
|
14
|
+
|
|
15
|
+
return (openBlock(), createElementBlock("div", _hoisted_1, [
|
|
16
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList($options.frames, (frame) => {
|
|
17
|
+
return (openBlock(), createElementBlock("section", {
|
|
18
|
+
key: frame.key,
|
|
19
|
+
class: "cn-device-frames-widget__frame",
|
|
20
|
+
style: normalizeStyle($options.frameStyle),
|
|
21
|
+
"aria-labelledby": $options.headingId(frame.key),
|
|
22
|
+
"data-testid": `cn-device-frame-${frame.key}`
|
|
23
|
+
}, [
|
|
24
|
+
createElementVNode("header", _hoisted_3, [
|
|
25
|
+
(frame.status)
|
|
26
|
+
? (openBlock(), createElementBlock("span", {
|
|
27
|
+
key: 0,
|
|
28
|
+
class: normalizeClass(["cn-device-frames-widget__status", `cn-device-frames-widget__status--${frame.status.tone}`]),
|
|
29
|
+
"data-testid": "cn-device-frame-status"
|
|
30
|
+
}, [
|
|
31
|
+
(openBlock(), createBlock(resolveDynamicComponent(frame.status.icon), {
|
|
32
|
+
size: 16,
|
|
33
|
+
"aria-hidden": "true"
|
|
34
|
+
})),
|
|
35
|
+
createTextVNode(" " + toDisplayString(frame.status.text), 1 /* TEXT */)
|
|
36
|
+
], 2 /* CLASS */))
|
|
37
|
+
: createCommentVNode("v-if", true),
|
|
38
|
+
createElementVNode("h3", {
|
|
39
|
+
id: $options.headingId(frame.key),
|
|
40
|
+
class: "cn-device-frames-widget__title"
|
|
41
|
+
}, toDisplayString(frame.title), 9 /* TEXT, PROPS */, _hoisted_4)
|
|
42
|
+
]),
|
|
43
|
+
createElementVNode("div", _hoisted_5, [
|
|
44
|
+
createVNode(_component_CnSectionsWidget, {
|
|
45
|
+
content: { sections: frame.sections, cards: false, hideEmpty: true },
|
|
46
|
+
availableWidgets: $props.availableWidgets,
|
|
47
|
+
objectId: $props.objectId,
|
|
48
|
+
objectData: $props.objectData,
|
|
49
|
+
objectType: $props.objectType,
|
|
50
|
+
schemaObject: $props.schemaObject,
|
|
51
|
+
register: $props.register,
|
|
52
|
+
schema: $props.schema,
|
|
53
|
+
store: $props.store,
|
|
54
|
+
surface: $props.surface,
|
|
55
|
+
integrationContext: $props.integrationContext,
|
|
56
|
+
cnRegistry: $props.cnRegistry
|
|
57
|
+
}, null, 8 /* PROPS */, ["content", "availableWidgets", "objectId", "objectData", "objectType", "schemaObject", "register", "schema", "store", "surface", "integrationContext", "cnRegistry"])
|
|
58
|
+
])
|
|
59
|
+
], 12 /* STYLE, PROPS */, _hoisted_2))
|
|
60
|
+
}), 128 /* KEYED_FRAGMENT */))
|
|
61
|
+
]))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { render };
|
|
65
|
+
//# sourceMappingURL=CnDeviceFramesWidget.vue3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnDeviceFramesWidget.vue3.js","sources":["../../../../src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue?vue&type=template&id=05f76090&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-device-frames-widget\" data-testid=\"cn-device-frames-widget\">\n\t\t<section\n\t\t\tv-for=\"frame in frames\"\n\t\t\t:key=\"frame.key\"\n\t\t\tclass=\"cn-device-frames-widget__frame\"\n\t\t\t:style=\"frameStyle\"\n\t\t\t:aria-labelledby=\"headingId(frame.key)\"\n\t\t\t:data-testid=\"`cn-device-frame-${frame.key}`\">\n\t\t\t<header class=\"cn-device-frames-widget__head\">\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"frame.status\"\n\t\t\t\t\tclass=\"cn-device-frames-widget__status\"\n\t\t\t\t\t:class=\"`cn-device-frames-widget__status--${frame.status.tone}`\"\n\t\t\t\t\tdata-testid=\"cn-device-frame-status\">\n\t\t\t\t\t<component :is=\"frame.status.icon\" :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t{{ frame.status.text }}\n\t\t\t\t</span>\n\t\t\t\t<h3 :id=\"headingId(frame.key)\" class=\"cn-device-frames-widget__title\">\n\t\t\t\t\t{{ frame.title }}\n\t\t\t\t</h3>\n\t\t\t</header>\n\t\t\t<div class=\"cn-device-frames-widget__body\">\n\t\t\t\t<CnSectionsWidget\n\t\t\t\t\t:content=\"{ sections: frame.sections, cards: false, hideEmpty: true }\"\n\t\t\t\t\t:availableWidgets=\"availableWidgets\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:objectData=\"objectData\"\n\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t:cnRegistry=\"cnRegistry\" />\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { markRaw } from 'vue'\nimport CloudCheckOutline from 'vue-material-design-icons/CloudCheckOutline.vue'\nimport CloudOffOutline from 'vue-material-design-icons/CloudOffOutline.vue'\nimport InformationOutline from 'vue-material-design-icons/InformationOutline.vue'\nimport CnSectionsWidget from '../CnSectionsWidget/CnSectionsWidget.vue'\n\n/** Counter for unique heading ids across instances. */\nlet instanceCounter = 0\n\n/**\n * CnDeviceFramesWidget - the `device-frames` widget: several small pages side\n * by side, each in a phone frame with its own header (board DqInspectie,\n * \"Inspectie in het veld\": the day offline, a checklist question with a\n * photo, and the sync queue).\n *\n * Each frame stacks widgets of the surface, as the `sections` widget does,\n * under a header with the frame's title and an optional status line. The\n * status can follow the connection (\"Offline · synced at 07:42\" while the\n * device is offline). The frames wrap onto the next row when the page is\n * narrower, and a frame takes the full width on a phone.\n *\n * ```json\n * {\n * \"type\": \"device-frames\",\n * \"content\": {\n * \"width\": 350,\n * \"status\": { \"followConnection\": true, \"offlineText\": \"Offline · synced at 07:42\", \"onlineText\": \"Online\" },\n * \"frames\": [\n * { \"key\": \"today\", \"title\": \"Today\", \"widgetIds\": [\"today-runs\", \"offline-note\"] },\n * { \"key\": \"question\", \"title\": \"Kerkplein 7\", \"widgetIds\": [\"checklist-run\"] },\n * { \"key\": \"sync\", \"title\": \"Synchronise\", \"widgetIds\": [\"sync-queue\"] }\n * ]\n * }\n * }\n * ```\n *\n * @spec openspec/changes/device-frames-widget/specs/device-frames-widget/spec.md#requirement-a-page-can-show-several-small-pages-as-phone-frames\n */\nexport default {\n\tname: 'CnDeviceFramesWidget',\n\n\tcomponents: { CnSectionsWidget },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `frames` — `[{ key?, title, widgetIds? | sections?, status? }]`, in order. `widgetIds` names\n\t\t * widgets of the surface, stacked; `sections` takes the `sections` widget's entries instead.\n\t\t * - `status` — the status line of every frame (a frame's own `status` wins): `{ text?, tone?,\n\t\t * followConnection?, onlineText?, offlineText? }`. With `followConnection` the line reads\n\t\t * `offlineText` while the device is offline and `onlineText` otherwise. `tone` is `warning`\n\t\t * (default while offline), `success` or `info`.\n\t\t * - `width` — the frame width in pixels (default 350).\n\t\t *\n\t\t * @type {{frames?: Array<object>, status?: object, width?: number}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Sibling widget definitions on the surface, for `widgetIds`. */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id, forwarded to the children. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, forwarded to the children. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to the children. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'app-dashboard',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tdata() {\n\t\tinstanceCounter += 1\n\t\treturn {\n\t\t\tuid: instanceCounter,\n\t\t\tonline: typeof navigator === 'undefined' || navigator.onLine !== false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object} The frame width. */\n\t\tframeStyle() {\n\t\t\tconst width = Number(this.content?.width)\n\t\t\treturn { '--cn-device-frame-width': `${Number.isFinite(width) && width >= 240 ? width : 350}px` }\n\t\t},\n\n\t\t/**\n\t\t * The frames to draw: a key, a translated title, the sections and the\n\t\t * status line.\n\t\t *\n\t\t * @return {Array<{key: string, title: string, sections: Array<object>, status: object|null}>}\n\t\t */\n\t\tframes() {\n\t\t\tconst list = Array.isArray(this.content?.frames) ? this.content.frames : []\n\t\t\treturn list\n\t\t\t\t.filter((frame) => frame && typeof frame === 'object')\n\t\t\t\t.map((frame, index) => {\n\t\t\t\t\tconst sections = Array.isArray(frame.sections)\n\t\t\t\t\t\t? frame.sections\n\t\t\t\t\t\t: (Array.isArray(frame.widgetIds) ? frame.widgetIds : []).filter((id) => typeof id === 'string' && id !== '').map((widgetId) => ({ widgetId }))\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(frame.key || `frame-${index}`).replace(/[^\\w-]/g, '-'),\n\t\t\t\t\t\ttitle: this.tr(frame.title || ''),\n\t\t\t\t\t\tsections,\n\t\t\t\t\t\tstatus: this.statusOf(frame.status || this.content?.status || null),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.addEventListener('online', this.onConnection)\n\t\t\twindow.addEventListener('offline', this.onConnection)\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.removeEventListener('online', this.onConnection)\n\t\t\twindow.removeEventListener('offline', this.onConnection)\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the app's translate.\n\t\t *\n\t\t * @param {string} key The source string.\n\t\t * @return {string}\n\t\t */\n\t\ttr(key) {\n\t\t\treturn key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')\n\t\t},\n\n\t\t/**\n\t\t * The status line of a frame, or null.\n\t\t *\n\t\t * @param {object|null} status The configured status.\n\t\t * @return {{text: string, tone: string, icon: object}|null}\n\t\t */\n\t\tstatusOf(status) {\n\t\t\tif (!status || typeof status !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet text = status.text\n\t\t\tlet tone = status.tone\n\t\t\tif (status.followConnection === true) {\n\t\t\t\ttext = this.online ? status.onlineText : status.offlineText\n\t\t\t\ttone = tone || (this.online ? 'success' : 'warning')\n\t\t\t}\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst known = ['warning', 'success', 'info'].includes(tone) ? tone : 'info'\n\t\t\tconst icon = known === 'warning' ? CloudOffOutline : (known === 'success' ? CloudCheckOutline : InformationOutline)\n\t\t\treturn { text: this.tr(text), tone: known, icon: markRaw(icon) }\n\t\t},\n\n\t\t/** Follow the browser's connection state. */\n\t\tonConnection() {\n\t\t\tthis.online = typeof navigator === 'undefined' || navigator.onLine !== false\n\t\t},\n\n\t\t/**\n\t\t * The id of a frame's heading.\n\t\t *\n\t\t * @param {string} key The frame key.\n\t\t * @return {string}\n\t\t */\n\t\theadingId(key) {\n\t\t\treturn `cn-device-frame-${this.uid}-${key}`\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-device-frames-widget {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-start;\n\tgap: 20px;\n}\n\n.cn-device-frames-widget__frame {\n\tdisplay: flex;\n\tflex: none;\n\tflex-direction: column;\n\twidth: var(--cn-device-frame-width, 350px);\n\tmax-width: 100%;\n\tmin-height: 600px;\n\tbox-sizing: border-box;\n\toverflow: hidden;\n\tborder: 10px solid var(--color-main-text);\n\tborder-radius: 36px;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-device-frames-widget__head {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n\tpadding: 14px 18px 10px;\n\tborder-bottom: 1px solid var(--color-border);\n\tbackground: var(--color-main-background);\n}\n\n.cn-device-frames-widget__status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n}\n\n.cn-device-frames-widget__status--warning {\n\tcolor: var(--color-warning-text, var(--color-main-text));\n}\n\n.cn-device-frames-widget__status--success {\n\tcolor: var(--color-success-text, var(--color-main-text));\n}\n\n.cn-device-frames-widget__status--info {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-device-frames-widget__title {\n\tmargin: 0;\n\tfont-size: 20px;\n\tfont-weight: 700;\n}\n\n.cn-device-frames-widget__body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 14px;\n}\n\n/* On a phone the frame is the screen: no bezel, the full width. */\n@media (max-width: 640px) {\n\t.cn-device-frames-widget__frame {\n\t\twidth: 100%;\n\t\tmin-height: 0;\n\t\tborder-width: 0;\n\t\tborder-radius: var(--cn-board-card-radius, 12px);\n\t}\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createElementVNode","_createBlock","_resolveDynamicComponent","_createTextVNode","_toDisplayString"],"mappings":";;;AAWM,EAAA,KAAK,EAAC,yBAAS;AAAA,EAAA,aAAA,EAAA,yBAAA;;;AAQV,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,+BAAwB,GAAA;;AAajC,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,+BAAc,GAAA;;;;;AArBzB,EAAA,QAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;sBACAA,8CACgB,QAAM,CAAA,MAAA,EAAA,CAAf,KAAK,KAAA;2BADZA;QAEC,GAAG,EAAE,KAAK,CAAC,GAAG;aACV,EAAA,gCAAA;AACH,QAAA,KAAI,iBAAE,QAAU,CAAA,UAAA,CAAA;AAChB,QAAA,iBAAc,EAAE,QAAA,CAAA,SAAS,CAAC,KAAK,CAAC,GAAG,CAAA;QACnC,aAAU,EAAA,CAAA,gBAAA,EAAqB,KAAK,CAAC,GAAG,CAAA,CAAA;;AACxC,QAAAC,kBAAA,CAYO,UAZP,UAYO,EAAA;AAVD,UAAA,CAAA,KAAK,CAAC,MAAM;4BADlBD,kBAOM,CAAA,MAAA,EAAA;;AALN,gBAAA,KAAK,kBAAA,iCAAA,EAAA,CAAA,iCAAA,EACuC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAA,CAAA,CAAA,CAAA;AAC7D,gBAAA,aAAW,EAAC,wBAAI;;AAChB,iBAAAD,SAAA,EAAA,EAAAG,WAAA,CAAGC,uBAAA,CAAA,KAAA,CAAA,MAAA,CAAA,IAAA,CAAA,EAAA;AAAA,kBAAA,IAAA,EAAA,EAAA;AAAA,kBAAA,aAAA,EAAA,MAAA;;AAAA,gBAAAC,eAAA,CAAA,GACH,GAAGC,eAAA,CAAA,KAAG,CAAA,MAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;;;UAEPJ,kBAEK,CAAA,IAAA,EAAA;AAFA,YAAA,EAAE,EAAE,QAAA,CAAA,SAAO,CAAA,KAAA,CAAA,GAAA,CAAA;AAAA,YAAA,KAAA,EAAA,gCAAA;AACZ,WAAA,EAAAI,eAAA,CAAA,KAAK,CAAC,KAAK,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;;;;YAKb,OAAO,EAAA,EAAA,QAAA,EAAc,KAAK,CAAA,QAAA,EAAA,KAAA,EAAA,KAAA,EAAA,SAAA,EAAA,IAAA,EAAA;AAC1B,YAAA,gBAAgB,EAAE,MAAA,CAAA,gBAAA;AAClB,YAAA,QAAQ,EAAE,MAAI,CAAA,QAAA;AACd,YAAA,UAAU,EAAE,MAAA,CAAA,UAAA;AACZ,YAAA,UAAU,EAAE,MAAI,CAAA,UAAA;AAChB,YAAA,YAAY,EAAE,MAAY,CAAA,YAAA;AAC1B,YAAA,QAAQ,EAAE,MAAQ,CAAA,QAAA;AAClB,YAAA,MAAE,EAAA,MAAA,CAAA,MAAA;AACF,YAAA,KAAK,EAAA,MAAA,CAAA,KAAA;AACL,YAAA,OAAA,EAAA,MAAA,CAAA,OAAA;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import __sfc_main from './CnDeviceFramesWidget.vue.js';
|
|
2
|
+
import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
6
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
7
|
+
*
|
|
8
|
+
* CnDeviceFramesWidget — self-registers the `device-frames` widget type
|
|
9
|
+
* (several small pages side by side, each in a phone frame) into the shared
|
|
10
|
+
* dashboardWidgetRegistry at module load. A container: it renders other
|
|
11
|
+
* widgets of the surface, so the host hands it their definitions.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
registerDashboardWidget('device-frames', {
|
|
16
|
+
renderer: __sfc_main,
|
|
17
|
+
form: null,
|
|
18
|
+
defaultContent: {
|
|
19
|
+
frames: [],
|
|
20
|
+
width: 350,
|
|
21
|
+
},
|
|
22
|
+
displayName: 'Device frames',
|
|
23
|
+
icon: 'CellphoneText',
|
|
24
|
+
container: true,
|
|
25
|
+
ownsTitle: true,
|
|
26
|
+
surfaces: ['app-dashboard', 'detail-page'],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export { __sfc_main as CnDeviceFramesWidget, __sfc_main as default };
|
|
30
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/components/CnDeviceFramesWidget/index.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * CnDeviceFramesWidget — self-registers the `device-frames` widget type\n * (several small pages side by side, each in a phone frame) into the shared\n * dashboardWidgetRegistry at module load. A container: it renders other\n * widgets of the surface, so the host hands it their definitions.\n */\n\nimport CnDeviceFramesWidget from './CnDeviceFramesWidget.vue'\nimport { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'\n\nregisterDashboardWidget('device-frames', {\n\trenderer: CnDeviceFramesWidget,\n\tform: null,\n\tdefaultContent: {\n\t\tframes: [],\n\t\twidth: 350,\n\t},\n\tdisplayName: 'Device frames',\n\ticon: 'CellphoneText',\n\tcontainer: true,\n\townsTitle: true,\n\tsurfaces: ['app-dashboard', 'detail-page'],\n})\n\nexport { CnDeviceFramesWidget }\nexport default CnDeviceFramesWidget\n"],"names":["CnDeviceFramesWidget"],"mappings":";;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA;AACA,uBAAuB,CAAC,eAAe,EAAE;AACzC,CAAC,QAAQ,EAAEA,UAAoB;AAC/B,CAAC,IAAI,EAAE,IAAI;AACX,CAAC,cAAc,EAAE;AACjB,EAAE,MAAM,EAAE,EAAE;AACZ,EAAE,KAAK,EAAE,GAAG;AACZ,EAAE;AACF,CAAC,WAAW,EAAE,eAAe;AAC7B,CAAC,IAAI,EAAE,eAAe;AACtB,CAAC,SAAS,EAAE,IAAI;AAChB,CAAC,SAAS,EAAE,IAAI;AAChB,CAAC,QAAQ,EAAE,CAAC,eAAe,EAAE,aAAa,CAAC;AAC3C,CAAC;;;;"}
|
|
@@ -3,7 +3,7 @@ export { FACTS_REVEAL_CHANNEL } from './CnFactsWidget.vue2.js';
|
|
|
3
3
|
import { render } from './CnFactsWidget.vue3.js';
|
|
4
4
|
|
|
5
5
|
script.render = render;
|
|
6
|
-
script.__scopeId = "data-v-
|
|
6
|
+
script.__scopeId = "data-v-c2fcc10e";
|
|
7
7
|
const __sfc_main = script;
|
|
8
8
|
|
|
9
9
|
export { __sfc_main as default };
|
|
@@ -135,6 +135,7 @@ var script = {
|
|
|
135
135
|
* - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object. An entry with `from: "object"` reads the current object even then (a client card that also shows the ticket's channel).
|
|
136
136
|
* - `link` — `{ label, route? | href? }` at the foot.
|
|
137
137
|
* - `hideEmpty` — drop an entry whose value is empty (default true).
|
|
138
|
+
* - `columns` — `2` lays the rows out in two columns with the label above the value (board DqContact); a row with `parts` joins them like a line (an address on one row).
|
|
138
139
|
* - `emptyText` — shown when no entry has a value.
|
|
139
140
|
*
|
|
140
141
|
* @type {{fields?: Array<object>, source?: object, link?: object, hideEmpty?: boolean, emptyText?: string}}
|
|
@@ -177,6 +178,16 @@ var script = {
|
|
|
177
178
|
},
|
|
178
179
|
|
|
179
180
|
computed: {
|
|
181
|
+
/**
|
|
182
|
+
* Whether the rows sit in two columns, label above value.
|
|
183
|
+
*
|
|
184
|
+
* @spec openspec/changes/facts-columns/specs/facts-widget/spec.md#requirement-facts-rows-can-sit-in-two-columns
|
|
185
|
+
* @return {boolean}
|
|
186
|
+
*/
|
|
187
|
+
twoColumns() {
|
|
188
|
+
return Number(this.content?.columns) === 2
|
|
189
|
+
},
|
|
190
|
+
|
|
180
191
|
/**
|
|
181
192
|
* The token context: the detail object (prop, else the injected context)
|
|
182
193
|
* and the page workspace.
|
|
@@ -413,7 +424,7 @@ var script = {
|
|
|
413
424
|
if (kind === 'lead') {
|
|
414
425
|
return this.partsValue(record, field.title)
|
|
415
426
|
}
|
|
416
|
-
if ((kind === 'line' || kind === 'text') && Array.isArray(field.parts)) {
|
|
427
|
+
if ((kind === 'line' || kind === 'text' || kind === 'row') && Array.isArray(field.parts)) {
|
|
417
428
|
return this.lineValue(record, field)
|
|
418
429
|
}
|
|
419
430
|
if ((kind === 'line' || kind === 'text') && !field.path && typeof field.text === 'string') {
|
|
@@ -791,6 +802,11 @@ var script = {
|
|
|
791
802
|
if (!spec.path && typeof spec.text === 'string') {
|
|
792
803
|
return this.tr(spec.text)
|
|
793
804
|
}
|
|
805
|
+
// A group of parts with its own separator ("Parkstraat 24" inside
|
|
806
|
+
// "Parkstraat 24, 3421 AB Zuiddrecht").
|
|
807
|
+
if (!spec.path && Array.isArray(spec.parts)) {
|
|
808
|
+
return this.lineValue(record, spec)
|
|
809
|
+
}
|
|
794
810
|
const value = this.format(readPath(record, spec.path), spec, record);
|
|
795
811
|
if (value === '') {
|
|
796
812
|
return ''
|