@bagelink/vue 1.15.267 → 1.15.271

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.
@@ -27,7 +27,7 @@ type DeepKeyOf<T> = T extends Primitive ? never : {
27
27
  /**
28
28
  * SCIM 2.0 comparison operators
29
29
  */
30
- export type ComparisonOperator = 'eq' | 'ne' | 'gt' | 'ge' | 'lt' | 'le' | 'co' | 'sw' | 'ew' | 'pr' | 'in' | 'nin';
30
+ export type ComparisonOperator = 'eq' | 'ne' | 'gt' | 'ge' | 'lt' | 'le' | 'co' | 'sw' | 'ew' | 'pr' | 'in' | 'nin' | 'between';
31
31
  /**
32
32
  * SCIM 2.0 logical operators
33
33
  */
@@ -103,6 +103,18 @@ export declare class QueryFilter<T extends Record<string, any>> {
103
103
  * Present check (field has a value)
104
104
  */
105
105
  pr<K extends DeepKeyOf<T>>(field: K): this;
106
+ /**
107
+ * In list — native membership / array overlap: field in ["a","b"]
108
+ */
109
+ in<K extends DeepKeyOf<T>>(field: K, values: Primitive[]): this;
110
+ /**
111
+ * Not in list: field nin ["a","b"]
112
+ */
113
+ nin<K extends DeepKeyOf<T>>(field: K, values: Primitive[]): this;
114
+ /**
115
+ * Inclusive range: field between [lo, hi]
116
+ */
117
+ between<K extends DeepKeyOf<T>>(field: K, lo: number | string, hi: number | string): this;
106
118
  /**
107
119
  * AND logical operator
108
120
  */
@@ -1 +1 @@
1
- {"version":3,"file":"queryFilter.d.ts","sourceRoot":"","sources":["../../src/utils/queryFilter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"queryFilter.d.ts","sourceRoot":"","sources":["../../src/utils/queryFilter.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.267",
4
+ "version": "1.15.271",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -66,6 +66,11 @@ function isComplete(c: { field: string, op: ComparisonOperator, value?: any }) {
66
66
  if (isQueryOptionField(c.field as string)) return c.value !== '' && c.value != null
67
67
  if (c.op === 'pr') return true
68
68
  if (c.op === 'in' || c.op === 'nin') return Array.isArray(c.value) && c.value.length > 0
69
+ if (c.op === 'between') {
70
+ return Array.isArray(c.value) && c.value.length === 2
71
+ && c.value[0] !== '' && c.value[0] != null
72
+ && c.value[1] !== '' && c.value[1] != null
73
+ }
69
74
  return c.value !== '' && c.value !== null && c.value !== undefined
70
75
  }
71
76
 
@@ -102,12 +107,16 @@ const currentTexts = computed(() => ({
102
107
  pr: $t('filter.operators.pr'),
103
108
  in: $t('filter.operators.in'),
104
109
  nin: $t('filter.operators.nin'),
110
+ between: $t('filter.operators.between'),
105
111
  },
106
112
  placeholders: {
107
113
  selectField: $t('filter.placeholders.selectField'),
108
114
  selectOption: $t('filter.placeholders.selectOption'),
109
115
  selectDate: $t('filter.placeholders.selectDate'),
110
116
  enterValue: $t('filter.placeholders.enterValue'),
117
+ from: $t('filter.placeholders.from'),
118
+ to: $t('filter.placeholders.to'),
119
+ commaSeparated: $t('filter.placeholders.commaSeparated'),
111
120
  },
112
121
  buttons: {
113
122
  addCondition: $t('filter.buttons.addCondition'),
@@ -157,23 +166,25 @@ const allOperatorOptions = computed(() => [
157
166
  { label: currentTexts.value.operators.pr, value: 'pr' as ComparisonOperator },
158
167
  { label: currentTexts.value.operators.in, value: 'in' as ComparisonOperator },
159
168
  { label: currentTexts.value.operators.nin, value: 'nin' as ComparisonOperator },
169
+ { label: currentTexts.value.operators.between, value: 'between' as ComparisonOperator },
160
170
  ])
161
171
 
162
172
  function getOperatorsForType(fieldType: string, hasOptions: boolean, isMultiple: boolean) {
173
+ const allow = (ops: string[]) => allOperatorOptions.value.filter(o => ops.includes(o.value))
163
174
  if (isMultiple && hasOptions) {
164
- return allOperatorOptions.value.filter(o => ['in', 'nin', 'pr'].includes(o.value))
175
+ return allow(['in', 'nin', 'pr'])
165
176
  }
166
177
  if (hasOptions) {
167
- return allOperatorOptions.value.filter(o => ['eq', 'ne', 'pr'].includes(o.value))
178
+ return allow(['eq', 'ne', 'in', 'nin', 'pr'])
168
179
  }
169
180
  switch (fieldType) {
170
181
  case 'boolean':
171
- return allOperatorOptions.value.filter(o => ['eq', 'ne', 'pr'].includes(o.value))
182
+ return allow(['eq', 'ne', 'pr'])
172
183
  case 'number':
173
184
  case 'date':
174
- return allOperatorOptions.value.filter(o => ['eq', 'ne', 'gt', 'ge', 'lt', 'le', 'pr'].includes(o.value))
185
+ return allow(['eq', 'ne', 'gt', 'ge', 'lt', 'le', 'between', 'in', 'nin', 'pr'])
175
186
  default:
176
- return allOperatorOptions.value.filter(o => ['eq', 'ne', 'co', 'sw', 'ew', 'pr'].includes(o.value))
187
+ return allow(['eq', 'ne', 'co', 'sw', 'ew', 'in', 'nin', 'pr'])
177
188
  }
178
189
  }
179
190
 
@@ -281,7 +292,13 @@ function updateConditionAtIndex(index: number, updates: Partial<{ field: string,
281
292
  const updated = { ...existing }
282
293
  if (updates.field !== undefined) updated.field = updates.field
283
294
  if (updates.op !== undefined) updated.op = updates.op
284
- if (updates.value !== undefined) updated.value = Array.isArray(updates.value) ? updates.value : parseValue(updates.value)
295
+ if (updates.value !== undefined) {
296
+ updated.value = Array.isArray(updates.value)
297
+ // Coerce each list/range element (e.g. "18" → 18) so numeric filters
298
+ // emit bare numbers in the bracket literal.
299
+ ? updates.value.map(v => parseValue(typeof v === 'string' ? v : String(v)))
300
+ : parseValue(updates.value)
301
+ }
285
302
  if (updates.connector !== undefined) updated.connector = updates.connector
286
303
 
287
304
  newModel[index] = updated
@@ -334,6 +351,38 @@ function isMultiOperator(operator: ComparisonOperator): boolean {
334
351
  return operator === 'in' || operator === 'nin'
335
352
  }
336
353
 
354
+ function isBetweenOperator(operator: ComparisonOperator): boolean {
355
+ return operator === 'between'
356
+ }
357
+
358
+ /** in/nin on a field WITHOUT predefined options → comma-separated free text. */
359
+ function isFreeListOperator(condition: UICondition): boolean {
360
+ return isMultiOperator(condition.operator) && !getFieldOptions(condition.field)
361
+ }
362
+
363
+ /** Convert an array value to comma-separated text for the free-list input. */
364
+ function listToText(value: string | string[]): string {
365
+ return Array.isArray(value) ? value.join(', ') : (value ?? '')
366
+ }
367
+
368
+ /** Split comma-separated free-list text into a trimmed string[]. */
369
+ function textToList(text: string): string[] {
370
+ return text.split(',').map(s => s.trim()).filter(s => s !== '')
371
+ }
372
+
373
+ /** Read one bound of a `between` value ([lo, hi]); index 0 = lo, 1 = hi. */
374
+ function betweenBound(condition: UICondition, index: 0 | 1): string {
375
+ return Array.isArray(condition.value) ? (condition.value[index] ?? '') : ''
376
+ }
377
+
378
+ /** Update one bound of a `between` value, preserving the other. */
379
+ function onBetweenBoundChange(id: string, index: 0 | 1, v: string) {
380
+ const cond = conditions.value.find(c => c.id === id)
381
+ const current = cond && Array.isArray(cond.value) ? [...cond.value] : ['', '']
382
+ current[index] = v
383
+ onValueChange(id, [current[0] ?? '', current[1] ?? ''])
384
+ }
385
+
337
386
  const activeFiltersCount = computed(() => {
338
387
  return conditions.value.filter((c: UICondition) => Boolean(c.field) && (c.operator === 'pr' || Boolean(c.value))).length
339
388
  })
@@ -364,11 +413,19 @@ function onOperatorChange(id: string, operator: ComparisonOperator) {
364
413
  const index = getConditionIndex(id)
365
414
  if (index < 0) return
366
415
  const current = conditions.value[index]
367
- const wasMulti = current?.operator === 'in' || current?.operator === 'nin'
368
- const isMulti = operator === 'in' || operator === 'nin'
416
+ const prevOp = current?.operator
369
417
  const updates: Parameters<typeof updateConditionAtIndex>[1] = { op: operator }
370
- if (wasMulti && !isMulti) updates.value = ''
371
- else if (!wasMulti && isMulti) updates.value = []
418
+
419
+ // Reset the value shape when the operator's expected value type changes.
420
+ const wasList = prevOp === 'in' || prevOp === 'nin'
421
+ const wasBetween = prevOp === 'between'
422
+ const isList = operator === 'in' || operator === 'nin'
423
+ const isBetween = operator === 'between'
424
+
425
+ if (isBetween && !wasBetween) updates.value = ['', '']
426
+ else if (isList && !wasList) updates.value = []
427
+ else if (!isList && !isBetween && (wasList || wasBetween)) updates.value = ''
428
+
372
429
  updateConditionAtIndex(index, updates)
373
430
  }
374
431
 
@@ -466,8 +523,43 @@ function onConnectorChange(id: string, connector: LogicalOperator) {
466
523
 
467
524
  <!-- Value input - type-specific -->
468
525
  <template v-if="needsValue(condition.operator)">
526
+ <!-- between: two type-aware bound inputs (min / max) -->
527
+ <div
528
+ v-if="isBetweenOperator(condition.operator)"
529
+ :key="`${condition.id}-${condition.field}-between`"
530
+ class="flex gap-025 align-items-center between-inputs"
531
+ >
532
+ <DateInput
533
+ v-if="getFieldType(condition.field) === 'date'"
534
+ :model-value="betweenBound(condition, 0)"
535
+ :placeholder="currentTexts.placeholders.from" class="m-0 light-input borderHover"
536
+ @update:model-value="(v: any) => onBetweenBoundChange(condition.id, 0, String(v))"
537
+ />
538
+ <TextInput
539
+ v-else
540
+ :model-value="betweenBound(condition, 0)" placeholder="0"
541
+ :type="getFieldType(condition.field) === 'number' ? 'number' : 'text'"
542
+ class="m-0 light-input borderHover"
543
+ @update:model-value="(v: string) => onBetweenBoundChange(condition.id, 0, v)"
544
+ />
545
+ <span class="opacity-4 txt-12">–</span>
546
+ <DateInput
547
+ v-if="getFieldType(condition.field) === 'date'"
548
+ :model-value="betweenBound(condition, 1)"
549
+ :placeholder="currentTexts.placeholders.to" class="m-0 light-input borderHover"
550
+ @update:model-value="(v: any) => onBetweenBoundChange(condition.id, 1, String(v))"
551
+ />
552
+ <TextInput
553
+ v-else
554
+ :model-value="betweenBound(condition, 1)" placeholder="0"
555
+ :type="getFieldType(condition.field) === 'number' ? 'number' : 'text'"
556
+ class="m-0 light-input borderHover"
557
+ @update:model-value="(v: string) => onBetweenBoundChange(condition.id, 1, v)"
558
+ />
559
+ </div>
560
+ <!-- in/nin with predefined options: multiselect -->
469
561
  <SelectInput
470
- v-if="getFieldOptions(condition.field) && isMultiOperator(condition.operator)"
562
+ v-else-if="getFieldOptions(condition.field) && isMultiOperator(condition.operator)"
471
563
  :key="`${condition.id}-${condition.field}-multi`"
472
564
  :model-value="Array.isArray(condition.value) ? condition.value : []"
473
565
  :options="getFieldOptions(condition.field)!"
@@ -475,6 +567,15 @@ function onConnectorChange(id: string, connector: LogicalOperator) {
475
567
  class="m-0 light-input borderHover" searchable multiselect
476
568
  @update:model-value="(v: string[]) => onValueChange(condition.id, v)"
477
569
  />
570
+ <!-- in/nin without options: comma-separated free-text list -->
571
+ <TextInput
572
+ v-else-if="isFreeListOperator(condition)"
573
+ :key="`${condition.id}-${condition.field}-freelist`"
574
+ :model-value="listToText(condition.value)"
575
+ :placeholder="currentTexts.placeholders.commaSeparated"
576
+ class="m-0 light-input borderHover"
577
+ @update:model-value="(v: string) => onValueChange(condition.id, textToList(v))"
578
+ />
478
579
  <SelectInput
479
580
  v-else-if="getFieldOptions(condition.field)"
480
581
  :key="`${condition.id}-${condition.field}-single`"
@@ -553,6 +654,17 @@ function onConnectorChange(id: string, connector: LogicalOperator) {
553
654
  --input-background-color: transparent !important;
554
655
  }
555
656
 
657
+ .between-inputs {
658
+ min-width: 0;
659
+ }
660
+ .between-inputs > * {
661
+ min-width: 0;
662
+ flex: 1 1 0;
663
+ }
664
+ .between-inputs > span {
665
+ flex: 0 0 auto;
666
+ }
667
+
556
668
  .and-or-select .selectinput-btn {
557
669
  border: none;
558
670
  width: 100%;
@@ -185,7 +185,7 @@ const headerFontSize = computed(() => {
185
185
  <table class="infinite-wrapper" :class="{ selecting: computedSelectedItems.length }">
186
186
  <thead class="row first-row">
187
187
  <th v-if="isSortable" class="col-drag" />
188
- <th v-if="isSelectable && !isSortable">
188
+ <th v-if="isSelectable" class="col-check">
189
189
  <input ref="allSelectorEl" type="checkbox" @click.stop @change="(e) => toggleSelectAll(e)">
190
190
  </th>
191
191
  <th
@@ -210,7 +210,7 @@ const headerFontSize = computed(() => {
210
210
  <td v-if="isSortable" class="col-drag">
211
211
  <Icon icon="drag_indicator" :size="1.2" class="drag-handle-col grab" />
212
212
  </td>
213
- <td v-if="isSelectable && !isSortable">
213
+ <td v-if="isSelectable" class="col-check">
214
214
  <div @click.stop>
215
215
  <input v-model="selectedItems" type="checkbox" :value="row?.id || ''">
216
216
  </div>
@@ -244,13 +244,32 @@ const headerFontSize = computed(() => {
244
244
  <style scoped>
245
245
 
246
246
  .col-drag {
247
- width: 2.5rem;
247
+ width: 1rem;
248
+ padding: 0 0.3rem 0 0;
249
+ text-align: center;
250
+ vertical-align: middle;
251
+ }
252
+
253
+ .col-drag .drag-handle-col {
254
+ display: flex;
255
+ align-items: center;
256
+ justify-content: center;
257
+ }
258
+
259
+ .col-check {
260
+ width: 1rem;
248
261
  padding: 0 0.25rem;
262
+ vertical-align: middle;
263
+ }
264
+
265
+ .col-check > div {
266
+ display: flex;
267
+ align-items: center;
249
268
  }
250
269
 
251
270
  .col-drag .bgl_icon-font {
252
271
  color: var(--bgl-black-tint);
253
- opacity: 0.5;
272
+ opacity: 0;
254
273
  transition: opacity 0.15s;
255
274
  }
256
275
 
@@ -434,7 +453,7 @@ background: var(--bgl-gray-light);
434
453
  }
435
454
 
436
455
  .row-item input[type='checkbox'] {
437
- margin-top: 0.45rem !important;
456
+ margin-top: 0 !important;
438
457
  accent-color: var(--bgl-accent-color);
439
458
  }
440
459
 
@@ -200,6 +200,34 @@ th {
200
200
  opacity: 0.8;
201
201
  }
202
202
 
203
+ /* Image alignment: full width (default) vs floated start/end.
204
+ Targets align-* classes directly (not .image-figure) so alignment survives
205
+ getCleanHTML(), which strips the editor-only .image-figure class from output. */
206
+ .richtext-editor-content figure.align-full {
207
+ float: none;
208
+ width: 100%;
209
+ margin-inline: auto;
210
+ }
211
+
212
+ .richtext-editor-content figure.align-start {
213
+ float: inline-start;
214
+ max-width: 50%;
215
+ margin-block: 0.25rem 0.75rem;
216
+ margin-inline: 0 1.5rem;
217
+ }
218
+
219
+ .richtext-editor-content figure.align-end {
220
+ float: inline-end;
221
+ max-width: 50%;
222
+ margin-block: 0.25rem 0.75rem;
223
+ margin-inline: 1.5rem 0;
224
+ }
225
+
226
+ .richtext-editor-content figure.align-start img,
227
+ .richtext-editor-content figure.align-end img {
228
+ width: 100%;
229
+ }
230
+
203
231
  /* Responsive iframe wrapper */
204
232
  .richtext-editor-content div:has(> iframe) {
205
233
  position: relative;
@@ -229,7 +229,8 @@ const imageForm = ref({
229
229
  width: '',
230
230
  height: '',
231
231
  credit: '',
232
- figcaption: false
232
+ figcaption: false,
233
+ align: 'full' // full, start, end
233
234
  })
234
235
  let pendingImageData: {
235
236
  selection: Selection
@@ -703,13 +704,18 @@ function openImageModal(existingImage: HTMLElement | null = null) {
703
704
  const figcaption = existingImage.querySelector('figcaption')
704
705
  const credit = existingImage.getAttribute('data-credit') || img?.getAttribute('data-credit') || ''
705
706
 
707
+ // Read alignment from the figure (or bare img) data-align attribute
708
+ const alignSource = existingImage.tagName === 'IMG' ? img : existingImage
709
+ const align = alignSource?.getAttribute('data-align') || 'full'
710
+
706
711
  imageForm.value = {
707
712
  src: img?.src || '',
708
713
  alt: img?.alt || '',
709
714
  width: img?.getAttribute('data-width') || img?.style.width || '',
710
715
  height: img?.getAttribute('data-height') || img?.style.height || '',
711
716
  credit,
712
- figcaption: !!figcaption
717
+ figcaption: !!figcaption,
718
+ align
713
719
  }
714
720
  } else {
715
721
  // Reset form for new image
@@ -719,7 +725,8 @@ function openImageModal(existingImage: HTMLElement | null = null) {
719
725
  width: '',
720
726
  height: '',
721
727
  credit: '',
722
- figcaption: false
728
+ figcaption: false,
729
+ align: 'full'
723
730
  }
724
731
  }
725
732
 
@@ -749,7 +756,9 @@ function submitImage() {
749
756
  img.style.width = `${imageForm.value.width}px`
750
757
  }
751
758
  } else {
752
- img.style.width = '100%'
759
+ // Floated images default to a partial width so text can wrap alongside;
760
+ // full-width images fill the container.
761
+ img.style.width = (imageForm.value.align === 'start' || imageForm.value.align === 'end') ? '45%' : '100%'
753
762
  }
754
763
 
755
764
  if (imageForm.value.height) {
@@ -842,6 +851,12 @@ function submitImage() {
842
851
  selection.addRange(range)
843
852
  }
844
853
 
854
+ // Apply alignment to the figure wrapper (full = full width, start/end = float)
855
+ const align = imageForm.value.align || 'full'
856
+ elementToUpdate.classList.remove('align-full', 'align-start', 'align-end')
857
+ elementToUpdate.classList.add(`align-${align}`)
858
+ elementToUpdate.setAttribute('data-align', align)
859
+
845
860
  updateContentWithHistory(doc)
846
861
  showImageModal.value = false
847
862
  pendingImageData = null
@@ -1765,7 +1780,9 @@ function getCleanHTML(doc: Document | null): string {
1765
1780
  // that remains after video-container conversion
1766
1781
  clone.querySelectorAll('.bgl_vid').forEach((el) => { el.remove() })
1767
1782
 
1768
- // Strip editor-only classes from <figure> elements so output is pure semantic HTML
1783
+ // Strip editor-only classes from <figure> elements so output is pure semantic HTML.
1784
+ // Keep alignment classes (align-full/align-start/align-end) so floated images
1785
+ // render correctly outside the editor.
1769
1786
  clone.querySelectorAll('figure').forEach((fig) => {
1770
1787
  fig.classList.remove('image-figure', 'video-figure', 'embed-figure')
1771
1788
  if (fig.classList.length === 0) { fig.removeAttribute('class') }
@@ -2887,6 +2904,33 @@ async function initEditor() {
2887
2904
  outline-offset: 2px;
2888
2905
  }
2889
2906
 
2907
+ /* ── Image alignment ───────────────────────────────────────── */
2908
+ /* Default: full width block. */
2909
+ figure.align-full {
2910
+ float: none;
2911
+ width: 100%;
2912
+ margin-inline: 0;
2913
+ }
2914
+
2915
+ figure.align-start {
2916
+ float: inline-start;
2917
+ max-width: 50%;
2918
+ margin-block: 0.25em 0.75em;
2919
+ margin-inline: 0 1.25em;
2920
+ }
2921
+
2922
+ figure.align-end {
2923
+ float: inline-end;
2924
+ max-width: 50%;
2925
+ margin-block: 0.25em 0.75em;
2926
+ margin-inline: 1.25em 0;
2927
+ }
2928
+
2929
+ figure.align-start img,
2930
+ figure.align-end img {
2931
+ width: 100%;
2932
+ }
2933
+
2890
2934
  /* Selected state — applied via JS on click, stripped by getCleanHTML() */
2891
2935
  .media-selected {
2892
2936
  outline: 2px solid #007bff !important;
@@ -3484,6 +3528,14 @@ const imgTransform = computed({
3484
3528
  help="Examples: auto, 300px"
3485
3529
  />
3486
3530
  </div>
3531
+ <SelectInput
3532
+ v-model="imageForm.align" label="Alignment" :options="[
3533
+ { value: 'full', label: 'Full width (default)' },
3534
+ { value: 'start', label: 'Float to start (text wraps)' },
3535
+ { value: 'end', label: 'Float to end (text wraps)' },
3536
+ ]"
3537
+ help="Full width fills the container. Start/End float the image so text flows alongside."
3538
+ />
3487
3539
  <TextInput v-model="imageForm.credit" label="Photo Credit" placeholder="Photographer name (optional)" />
3488
3540
  <CheckInput
3489
3541
  v-model="imageForm.figcaption" label="Show alt text as caption"
@@ -125,20 +125,20 @@ defineExpose({ close })
125
125
  <div v-if="showGrip" class="dialog-grip" aria-hidden="true" />
126
126
  <!-- Header -->
127
127
  <header
128
- v-if="title || dismissable" class="flex space-between flex-shrink-0 align-items-start gap-05"
129
- :class="{ 'border-bottom': !flat, 'border-none': !title, 'm_ps-1': title, 'p-05 m_p-05': !title, 'ps-1-5 pe-1 py-1 ': !thin && title, 'ps-1 pe-05 py-05': thin && title }"
128
+ v-if="title || $slots.header || dismissable" class="flex space-between flex-shrink-0 align-items-start gap-05"
129
+ :class="{ 'border-bottom': !flat, 'border-none': !title && !$slots.header, 'm_ps-1': title || $slots.header, 'p-05 m_p-05': !title && !$slots.header, 'ps-1-5 pe-1 py-1 ': !thin && (title || $slots.header), 'ps-1 pe-05 py-05': thin && (title || $slots.header) }"
130
130
  >
131
131
  <Btn
132
132
  v-if="dismissable && closePlacement === 'header-start'" flat icon="close" aria-label="Close"
133
133
  class="dialog-close relative z-2" thin @click="close"
134
134
  />
135
- <h3
136
- v-if="title" class="dialog-title m-0 txt20 semi w-100p"
137
- :class="{ 'txt-center balance': centerTitle, 'word-break-all': !centerTitle, 'ellipsis-1': ellipsisTitle }"
138
- >
139
- {{ title }}
140
- </h3>
141
- <div v-if="!title" class="flex-grow-1" />
135
+ <h3
136
+ v-if="title || $slots.header" class="dialog-title m-0 txt20 semi w-100p"
137
+ :class="{ 'txt-center balance': centerTitle, 'word-break-all': !centerTitle, 'ellipsis-1': ellipsisTitle }"
138
+ >
139
+ <slot name="header">{{ title }}</slot>
140
+ </h3>
141
+ <div v-if="!title && !$slots.header" class="flex-grow-1" />
142
142
  <Btn
143
143
  v-if="dismissable && closePlacement === 'header-end'" flat icon="close" aria-label="Close"
144
144
  class="dialog-close relative z-2" thin @click="close"
@@ -78,13 +78,17 @@
78
78
  "ew": "Ends with",
79
79
  "pr": "Present",
80
80
  "in": "In list",
81
- "nin": "Not in list"
81
+ "nin": "Not in list",
82
+ "between": "Between"
82
83
  },
83
84
  "placeholders": {
84
85
  "selectField": "Select field",
85
86
  "selectOption": "Select option",
86
87
  "selectDate": "Select date",
87
- "enterValue": "Value"
88
+ "enterValue": "Value",
89
+ "from": "From",
90
+ "to": "To",
91
+ "commaSeparated": "Comma-separated values"
88
92
  },
89
93
  "buttons": {
90
94
  "addCondition": "Add condition",
@@ -496,5 +500,8 @@
496
500
  "charts": {
497
501
  "total": "Total",
498
502
  "share": "Share"
503
+ },
504
+ "guidedHelp": {
505
+ "hint": "The walkthrough is always available on the question mark."
499
506
  }
500
507
  }
@@ -77,13 +77,17 @@
77
77
  "ew": "Termina con",
78
78
  "pr": "Presente",
79
79
  "in": "En lista",
80
- "nin": "No en lista"
80
+ "nin": "No en lista",
81
+ "between": "Entre"
81
82
  },
82
83
  "placeholders": {
83
84
  "selectField": "Seleccionar campo",
84
85
  "selectOption": "Seleccionar opción",
85
86
  "selectDate": "Seleccionar fecha",
86
- "enterValue": "Valor"
87
+ "enterValue": "Valor",
88
+ "from": "Desde",
89
+ "to": "Hasta",
90
+ "commaSeparated": "Valores separados por comas"
87
91
  },
88
92
  "buttons": {
89
93
  "addCondition": "Añadir condición",
@@ -270,5 +274,8 @@
270
274
  "addToCalendar": {
271
275
  "addTo": "Agregar a {name}",
272
276
  "addToCalendar": "Agregar al calendario"
277
+ },
278
+ "guidedHelp": {
279
+ "hint": "La guía siempre está disponible en el signo de interrogación."
273
280
  }
274
281
  }
@@ -77,13 +77,17 @@
77
77
  "ew": "Se termine par",
78
78
  "pr": "Présent",
79
79
  "in": "Dans la liste",
80
- "nin": "Pas dans la liste"
80
+ "nin": "Pas dans la liste",
81
+ "between": "Entre"
81
82
  },
82
83
  "placeholders": {
83
84
  "selectField": "Sélectionner un champ",
84
85
  "selectOption": "Sélectionner une option",
85
86
  "selectDate": "Sélectionner une date",
86
- "enterValue": "Valeur"
87
+ "enterValue": "Valeur",
88
+ "from": "De",
89
+ "to": "À",
90
+ "commaSeparated": "Valeurs séparées par des virgules"
87
91
  },
88
92
  "buttons": {
89
93
  "addCondition": "Ajouter une condition",
@@ -269,5 +273,8 @@
269
273
  "addToCalendar": {
270
274
  "addTo": "Ajouter à {name}",
271
275
  "addToCalendar": "Ajouter au calendrier"
276
+ },
277
+ "guidedHelp": {
278
+ "hint": "Le guide est toujours disponible sur le point d'interrogation."
272
279
  }
273
280
  }
@@ -78,13 +78,17 @@
78
78
  "ew": "מסתיים ב",
79
79
  "pr": "קיים",
80
80
  "in": "ברשימה",
81
- "nin": "לא ברשימה"
81
+ "nin": "לא ברשימה",
82
+ "between": "בין"
82
83
  },
83
84
  "placeholders": {
84
85
  "selectField": "בחר שדה",
85
86
  "selectOption": "בחירה",
86
87
  "selectDate": "בחירת תאריך",
87
- "enterValue": "ערך"
88
+ "enterValue": "ערך",
89
+ "from": "מ־",
90
+ "to": "עד",
91
+ "commaSeparated": "ערכים מופרדים בפסיקים"
88
92
  },
89
93
  "buttons": {
90
94
  "addCondition": "הוספת תנאי",
@@ -517,5 +521,8 @@
517
521
  "charts": {
518
522
  "total": "סה״כ",
519
523
  "share": "חלק"
524
+ },
525
+ "guidedHelp": {
526
+ "hint": "ההסבר תמיד זמין בסימן השאלה."
520
527
  }
521
528
  }