@kvass/widgets 1.0.17 → 1.1.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.
@@ -246,19 +246,19 @@ onMounted(() => {
246
246
  .kvass-contact {
247
247
  // default variables
248
248
 
249
- --kvass-contact-default-background: #763c3c;
250
- --kvass-contact-default-spacing: 1.3rem;
249
+ --kvass-contact-default-background: #ffffff;
250
+ --kvass-contact-default-spacing: 2rem;
251
251
  --kvass-contact-default-border-radius: 4px;
252
252
  --kvass-contact-default-border-color: #eaeaea;
253
- --kvass-contact-default-border-width: 0px;
253
+ --kvass-contact-default-border-width: 1px;
254
254
  --kvass-contact-default-color: #222222;
255
- --kvass-contact-default-color-inverted: #e6e4e4;
255
+ --kvass-contact-default-color-inverted: #ffffff;
256
256
  --kvass-contact-default-max-width: 720px;
257
257
  --kvass-contact-default-primary: #1d56d8;
258
258
  --kvass-contact-default-error: #d81d1d;
259
259
  --kvass-contact-default-grid-columns: 1;
260
260
  --kvass-contact-default-disabled: #eaeaea;
261
- --kvass-contact-default-input-background: #984a4a;
261
+ --kvass-contact-default-input-background: #ffffff;
262
262
  --kvass-contact-default-outline-width: 2px;
263
263
  --kvass-contact-default-outline-offset: 0px;
264
264
  --kvass-contact-default-checkbox-size: 1em;
@@ -266,22 +266,11 @@ onMounted(() => {
266
266
  --kvass-contact-default-checkbox-border-width: var(
267
267
  --kvass-contact-default-border-width
268
268
  );
269
+
269
270
  --kvass-contact-default-checkbox-border-radius: var(
270
271
  --kvass-contact-default-border-radius
271
272
  );
272
273
 
273
- --kvass-contact-border-radius: 20px;
274
-
275
- --kvass-contact-border-width: 0;
276
- --kvass-contact-checkbox-border-width: 0;
277
- --kvass-contact-max-width: 440px;
278
- --kvass-contact-label-transform: 10px;
279
- --kvass-contact-form-spacing: 0rem;
280
- --kvass-contact-spacing: 1.3rem;
281
- --kvass-contact-field-input-tranform: 10px;
282
- --kvass-contact-label-weight: bold;
283
- --kvass-contact-success-label-font-size: 1.2em;
284
-
285
274
  background-color: var(
286
275
  --kvass-contact-background,
287
276
  var(--kvass-contact-default-background)
@@ -0,0 +1,69 @@
1
+ # kvass-img-comparison-slider
2
+
3
+ A simple, embeddable Web Component to compare images.
4
+
5
+ `https://unpkg.com/@kvass/widgets@latest/dist/img-comparison-slider.js`
6
+
7
+ ## Develop
8
+
9
+ To run in development mode, first install the neccessary packages.
10
+
11
+ ```
12
+ npm install
13
+ ```
14
+
15
+ Then, run in development mode.
16
+
17
+ ```
18
+ npm run dev
19
+ ```
20
+
21
+ Open `localhost:3000` in the browser of your choice, and you will see the form widget.
22
+
23
+ ## Build
24
+
25
+ To build the widget for production, run `build` instead of `dev`.
26
+
27
+ ```
28
+ npm run build
29
+ ```
30
+
31
+ To use the widget, use the `<kvass-img-comparison-slider />` element as shown here.
32
+
33
+ ```html
34
+ <kvass-img-comparison-slider
35
+ first-image="https://example.com/first-image,First image"
36
+ second-image="https://example.com/second-image,Second image"
37
+ options="direction:vertical,keyboard:disabled"
38
+ ></kvass-img-comparison-slider>
39
+ ```
40
+
41
+ ## Props
42
+
43
+ The component has several props for easy configuration.
44
+
45
+ | Name | Type | Description | Enums |
46
+ | :---------- | :------ | :-------------------------------------------------------- | :---------------------- |
47
+ | **options** | String | key:value pairs separated by comma | The following options: |
48
+ | value | Number | Position of the divider in percents. | `0...100` |
49
+ | hover | Boolean | Automatically slide on mouse over. | `false`, `true` |
50
+ | direction | String | Set slider direction. | `horiontal`, `vertical` |
51
+ | nonce | | Define nonce which gets passed to inline style. | |
52
+ | keyboard | String | Enable/disable slider position control with the keyboard. | `enabled`, `disabled` |
53
+ | handle | Boolean | Enable/disable dragging by handle only. | `false`, `true` |
54
+ | | | | |
55
+ | first-image | String | Image url and text, separated by a comma | |
56
+ | second-mage | String | Image url and text, separated by a comma | |
57
+ | handle-svg | String | The svg on the slider handle | |
58
+
59
+ ## Styling
60
+
61
+ The widget's styles are based on CSS custom properties, and can be overwritten.
62
+ These are the available CSS variables.
63
+
64
+ | Name | Description | Default |
65
+ | :------------------------ | :--------------------------------------------------------------------------------------- | :------ |
66
+ | `--divider-width` | Width of the vertical line separating both images | `1px` |
67
+ | `--divider-color` | Color of the vertical line separating the two images | `#fff` |
68
+ | `--divider-shadow` | Shadow cast by the vertical line separating the two images | `none` |
69
+ | `--handle-position-start` | Handle position on the divider axis. In case the handle must be displaced off the center | `50%` |
@@ -0,0 +1,139 @@
1
+ <script setup>
2
+ import { ImgComparisonSlider } from '@img-comparison-slider/vue';
3
+ import { computed } from 'vue';
4
+
5
+ const defaultOptions = {
6
+ value: 50,
7
+ hover: false,
8
+ direction: 'horizontal',
9
+ keyboard: 'enabled',
10
+ handle: false,
11
+ }
12
+
13
+ function createImageObject(imageString) {
14
+ if (!imageString) return {}
15
+
16
+ const split = imageString.split(',')
17
+ return {
18
+ url: split[0],
19
+ description: split[1]
20
+ }
21
+ }
22
+
23
+ const props = defineProps({
24
+ firstImage: {
25
+ type: String,
26
+ default: 'https://assets.kvass.no/641c0b087c49867b0b1065ec,Første bilde'
27
+ },
28
+ secondImage: {
29
+ type: String,
30
+ default: 'https://assets.kvass.no/641c0a8c7c49867b0b106570,Andre bilde'
31
+ },
32
+
33
+ options: {
34
+ type: String,
35
+ default: '',
36
+ },
37
+
38
+ handleSvg: {
39
+ type: String,
40
+ default:
41
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -3 16 6"><path stroke="#fff" d="M -3 -2 L -5 0 L -3 2 M -3 -2 L -3 2 M 3 -2 L 5 0 L 3 2 M 3 -2 L 3 2" stroke-width="1" fill="#fff" vector-effect="non-scaling-stroke"></path></svg>',
42
+ },
43
+ })
44
+
45
+ // images
46
+ const firstImage = createImageObject(props.firstImage)
47
+ const firstImageCaption = computed(() => firstImage.description)
48
+
49
+ const secondImage = createImageObject(props.secondImage)
50
+ const secondImageCaption = computed(() => secondImage.description)
51
+
52
+ // options
53
+ const options = computed(() => {
54
+ const entries = props.options.split(',').map(entry => entry.split(':'))
55
+ return Object.fromEntries(entries)
56
+ })
57
+ </script>
58
+
59
+ <template>
60
+ <ImgComparisonSlider
61
+ :style="`--first-image-caption: ${firstImageCaption}; --second-image-caption: ${secondImageCaption}`" tabindex="0"
62
+ class="img-comparison-slider" v-bind="{
63
+ ...defaultOptions,
64
+ ...options,
65
+ }">
66
+ <img slot="first" class="img-comparison-slider__image" :src="firstImage.url" />
67
+ <img slot="second" class="img-comparison-slider__image" :src="secondImage.url" />
68
+
69
+ <div slot="handle" class="handle">
70
+ <p class="handle__caption handle__caption--left">
71
+ {{ firstImage.description }}
72
+ </p>
73
+ <div class="handle__svg" v-html="handleSvg"></div>
74
+ <p class="handle__caption handle__caption--right">
75
+ {{ secondImage.description }}
76
+ </p>
77
+ </div>
78
+ </ImgComparisonSlider>
79
+ </template>
80
+
81
+ <style lang="scss">
82
+ .img-comparison-slider {
83
+ width: 100%;
84
+ height: 100%;
85
+
86
+ --divider-width: 4px;
87
+ --divider-color: black;
88
+
89
+ // aspect-ratio: var(--kvass-img-comparison-slider-aspect-ratio, $aspect-ratio);
90
+ // aspect-ratio: var(
91
+ // --kvass-img-comparison-slider-aspect-ratio,
92
+ // $aspect-ratio
93
+ // );
94
+
95
+ &__image {
96
+ width: 100%;
97
+ height: 100%;
98
+ object-fit: cover;
99
+ }
100
+
101
+ .handle {
102
+ display: flex;
103
+ flex-direction: row;
104
+
105
+ justify-content: center;
106
+ align-items: center;
107
+ flex-wrap: nowrap;
108
+ gap: 1rem;
109
+ min-width: 800px;
110
+
111
+ font-size: 1rem;
112
+
113
+ @media (max-width: 600px) {
114
+ font-size: 0.75rem;
115
+ }
116
+
117
+ &__caption {
118
+ color: white;
119
+ word-wrap: wrap;
120
+ width: 100%;
121
+
122
+ &--left {
123
+ text-align: right;
124
+ }
125
+ }
126
+
127
+ &__svg {
128
+ padding: 0.3em;
129
+ background-color: black;
130
+ border-radius: 100%;
131
+
132
+ svg {
133
+ width: 2.5em;
134
+ height: 2.5em;
135
+ }
136
+ }
137
+ }
138
+ }
139
+ </style>
@@ -0,0 +1,7 @@
1
+ import { defineCustomElement } from 'vue'
2
+ import ImgComparisonSlider from './components/ImgComparisonSlider.ce.vue'
3
+
4
+ customElements.define(
5
+ 'kvass-img-comparison-slider',
6
+ defineCustomElement(ImgComparisonSlider),
7
+ )
@@ -0,0 +1,308 @@
1
+ <template>
2
+ <!-- <Loader :value="promise"> -->
3
+ <div class="project-selector" :class="`project-selector--theme-${theme}`">
4
+ <div
5
+ v-if="!disableNav || !navItems.length"
6
+ class="project-selector__navigation"
7
+ >
8
+ <CategorySelector
9
+ class="project-selector__navigation-category"
10
+ v-if="navItems.length"
11
+ :items="navItems"
12
+ :value="category"
13
+ @input="
14
+ ($ev) => {
15
+ category = $ev
16
+ filterItems()
17
+ }
18
+ "
19
+ />
20
+
21
+ <ProjectTypeSelector
22
+ v-if="projectTypes.length > 1"
23
+ class="project-selector__navigation-project-type"
24
+ :items="projectTypes"
25
+ :value="projectType"
26
+ @input="
27
+ ($ev) => {
28
+ projectType = $ev.target.value
29
+ filterItems()
30
+ }
31
+ "
32
+ />
33
+ </div>
34
+
35
+ <transition-group
36
+ v-if="items && items.length"
37
+ tag="div"
38
+ name="list"
39
+ appear
40
+ class="project-selector__card"
41
+ >
42
+ <Card
43
+ v-for="(item, index) in items"
44
+ :disable-label="disableNav"
45
+ :key="index"
46
+ :item="item"
47
+ theme="border"
48
+ />
49
+ </transition-group>
50
+
51
+ <div class="project-selector__no-result" v-else>Ingen resultater</div>
52
+ </div>
53
+ <!-- </Loader> -->
54
+ </template>
55
+
56
+ <script>
57
+ export default {
58
+ created() {
59
+ this.fetch()
60
+ },
61
+ }
62
+ </script>
63
+
64
+ <script setup>
65
+ import { ref, computed } from 'vue'
66
+
67
+ import Card from './components/Card.ce.vue'
68
+ import CategorySelector from './components/CategorySelector.ce.vue'
69
+ import ProjectTypeSelector from './components/ProjectTypeSelector.ce.vue'
70
+ // import { LoaderComponent as Loader } from 'vue-elder-loader'
71
+
72
+ import { getProjects } from './api'
73
+
74
+ function getSortValue(type, value) {
75
+ switch (type) {
76
+ case 'status':
77
+ switch (value[type]) {
78
+ case 'sale':
79
+ return 2
80
+ case 'upcoming':
81
+ return 1
82
+ default:
83
+ return 0
84
+ }
85
+ case 'name':
86
+ return value[type]
87
+ }
88
+ }
89
+
90
+ const props = defineProps({
91
+ source: {
92
+ type: String,
93
+ default: 'https://feature.kvass.no',
94
+ },
95
+ startCategory: {
96
+ type: String,
97
+ default: 'all',
98
+ enums: ['all', 'sale', 'upcoming', 'development', 'sold'],
99
+ },
100
+ enabledCategories: {
101
+ type: String,
102
+ default: 'all,sale,upcoming,development,sold',
103
+ },
104
+ theme: {
105
+ type: String,
106
+ enum: ['default', 'tiles'],
107
+ default: 'default',
108
+ },
109
+ sortOn: {
110
+ type: String,
111
+ enum: ['status', 'name'],
112
+ default: 'status',
113
+ },
114
+ // triggerLabel: {
115
+ // type: String,
116
+ // default: 'Velg type',
117
+ // },
118
+ disableNav: {
119
+ type: Boolean,
120
+ default: false,
121
+ },
122
+ })
123
+
124
+ const category = ref('')
125
+ const projectType = ref('none')
126
+ const items = ref([])
127
+ var allItems = []
128
+ const promise = ref(null)
129
+
130
+ const navItems = computed(() => {
131
+ return [
132
+ ...props.enabledCategories.split(',').filter((i) => {
133
+ if (i === 'all') return true
134
+ return allItems.find((k) => k.status.includes(i))
135
+ }),
136
+ ]
137
+ })
138
+
139
+ const projectTypes = computed(() => {
140
+ let types = ['none'].concat(
141
+ allItems.map((i) => {
142
+ if (i.customFields && i.customFields['project-type'])
143
+ return i.customFields['project-type']
144
+ }),
145
+ )
146
+
147
+ return [...new Set(types || [])].filter(Boolean)
148
+ })
149
+
150
+ function filterItems() {
151
+ items.value = allItems
152
+ .filter((i) => {
153
+ if (category.value === 'all') return true
154
+ return i.status.includes(category.value)
155
+ })
156
+ .filter((i) => {
157
+ if (!projectTypes.length || projectType.value === 'none') return true
158
+ if (!i.customFields || !i.customFields['project-type']) return
159
+ return i.customFields['project-type'].includes(projectType.value)
160
+ })
161
+ }
162
+
163
+ function getFromSource() {
164
+ if (props.source) return getProjects(props.source)
165
+ return Promise.resolve()
166
+ }
167
+
168
+ function getStatus(item) {
169
+ if (item.status) return item.status
170
+ let total = item.stats.total
171
+ if (item.isPublished && total && !item.stats.sale) return 'sold'
172
+ if (item.isPublished && total) return 'sale'
173
+ if (item.isPublished && !total) return 'upcoming'
174
+ if (!item.isPublished) return 'development'
175
+ return 'development'
176
+ }
177
+
178
+ function fetch() {
179
+ promise.value = getFromSource()
180
+ .then(async (data) => {
181
+ let items = []
182
+ //read from global customItems
183
+ if (typeof customItems !== 'undefined') {
184
+ items =
185
+ typeof customItems === 'function' ? await customItems() : customItems
186
+ }
187
+ //remove kvass projects that is defined in customItems
188
+ let kvassProjects = data
189
+ ? data.Projects.filter((item) => {
190
+ if (items.find((i) => (i.id ? i.id.includes(item.id) : undefined)))
191
+ return
192
+ return item
193
+ })
194
+ : []
195
+
196
+ allItems = [...kvassProjects, ...items]
197
+
198
+ return (allItems = allItems
199
+ .map((item) => {
200
+ item.status = getStatus(item)
201
+ return {
202
+ intro: item.customFields ? item.customFields['project-intro'] : '',
203
+ ...item,
204
+ sortVale: getSortValue(props.sortOn, item),
205
+ url: item.url,
206
+ }
207
+ })
208
+ .sort((a, b) => {
209
+ switch (props.sortOn) {
210
+ case 'status':
211
+ if (a.sortVale < b.sortVale) return 1
212
+ if (a.sortVale > b.sortVale) return -1
213
+ }
214
+ })
215
+ .filter((i) => props.enabledCategories.split(',').includes(i.status)))
216
+ })
217
+ .then(() => {
218
+ category.value = props.startCategory
219
+ items.value = allItems
220
+ })
221
+ }
222
+ </script>
223
+
224
+ <style lang="scss">
225
+ @import './styles/_variables';
226
+ .project-selector {
227
+ $gap: 1.5rem;
228
+ &__navigation {
229
+ display: flex;
230
+ justify-content: var(--kvass-project-selector-nav-position, center);
231
+ padding: 0 2rem;
232
+ padding-bottom: 3rem;
233
+ gap: $gap;
234
+ @media (max-width: $kvass-project-selector-resposive) {
235
+ flex-direction: column-reverse;
236
+ justify-content: center;
237
+ gap: $gap - 1rem;
238
+ }
239
+ &-category {
240
+ display: flex;
241
+ justify-content: center;
242
+ gap: $gap;
243
+ @media (max-width: $kvass-project-selector-resposive) {
244
+ flex-direction: column;
245
+ gap: $gap - 1rem;
246
+ align-items: center;
247
+ }
248
+ }
249
+ }
250
+ &__card {
251
+ position: relative;
252
+ display: grid;
253
+ grid-template-columns: repeat(
254
+ var(--kvass-project-selector-grid-columns, 4),
255
+ 1fr
256
+ );
257
+ gap: var(--kvass-project-selector-grid-gap, 2rem);
258
+ @media (max-width: $kvass-project-selector-resposive) {
259
+ grid-template-columns: 1fr;
260
+ padding-top: 2rem;
261
+ }
262
+ }
263
+ &__no-result {
264
+ font-size: 1.2em;
265
+ text-align: center;
266
+ display: flex;
267
+ justify-content: center;
268
+ align-items: center;
269
+ min-height: 200px;
270
+ margin: 2rem 0;
271
+ background-color: GetVariable('light-grey');
272
+ @media (max-width: $kvass-project-selector-resposive) {
273
+ min-height: 100px;
274
+ }
275
+ }
276
+ .list {
277
+ &-leave-active {
278
+ position: absolute;
279
+ }
280
+ &-move,
281
+ &-enter-active,
282
+ &-leave-active {
283
+ transition: all 500ms ease;
284
+ }
285
+ &-enter {
286
+ transform: scale(0.95);
287
+ }
288
+ &-enter,
289
+ &-leave-to {
290
+ opacity: 0;
291
+ }
292
+ }
293
+ }
294
+ // tiles theme
295
+ .project-selector--theme-tiles {
296
+ .project-selector__card {
297
+ grid-template-columns: repeat(
298
+ var(--kvass-project-selector-grid-columns, 2),
299
+ 1fr
300
+ );
301
+ gap: var(--kvass-project-selector-grid-gap, 0rem);
302
+ @media (max-width: $kvass-project-selector-resposive) {
303
+ grid-template-columns: 1fr;
304
+ padding-top: 2rem;
305
+ }
306
+ }
307
+ }
308
+ </style>
@@ -0,0 +1,48 @@
1
+ function getProjects(url) {
2
+ return fetch(`${url}/api/graphql`, {
3
+ method: 'POST',
4
+ headers: {
5
+ 'Content-Type': 'application/json',
6
+ },
7
+ body: JSON.stringify({
8
+ query: `
9
+ query {
10
+ Projects {
11
+ id
12
+ name
13
+ url
14
+ isPublished
15
+ media {
16
+ cover {
17
+ url
18
+ type
19
+ }
20
+ gallery {
21
+ url
22
+ type
23
+ }
24
+ }
25
+ address {
26
+ street
27
+ city
28
+ county
29
+ postcode
30
+ location {
31
+ coordinates
32
+ }
33
+ }
34
+ stats {
35
+ total
36
+ sale
37
+ }
38
+ customFields(keys: ["project-intro", "project-type"])
39
+ }
40
+ }
41
+ `,
42
+ }),
43
+ })
44
+ .then((res) => res.json())
45
+ .then((res) => res.data)
46
+ }
47
+
48
+ export { getProjects }
@@ -0,0 +1 @@
1
+ <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="map-pin" class="svg-inline--fa fa-map-pin fa-w-9" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 512"><path fill="currentColor" d="M112 316.94v156.69l22.02 33.02c4.75 7.12 15.22 7.12 19.97 0L176 473.63V316.94c-10.39 1.92-21.06 3.06-32 3.06s-21.61-1.14-32-3.06zM144 0C64.47 0 0 64.47 0 144s64.47 144 144 144 144-64.47 144-144S223.53 0 144 0zm0 76c-37.5 0-68 30.5-68 68 0 6.62-5.38 12-12 12s-12-5.38-12-12c0-50.73 41.28-92 92-92 6.62 0 12 5.38 12 12s-5.38 12-12 12z"></path></svg>