@bagelink/vue 1.15.331 → 1.15.337

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.331",
4
+ "version": "1.15.337",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -17,6 +17,9 @@ const props = withDefaults(defineProps<{
17
17
  (theme name → `pair-*`, or a raw CSS color) and contrasts the glyph.
18
18
  Without `color`, just the circular shape. */
19
19
  round?: boolean
20
+ /** Soft circular chip: light tinted background + full-color glyph and border.
21
+ Uses `color`, and defaults to black when no color is provided. */
22
+ soft?: boolean
20
23
  weight?: number | string
21
24
  fontAwesome?: boolean
22
25
  fill?: boolean
@@ -159,12 +162,16 @@ const isRawCssColor = computed(() => {
159
162
  || /^(rgb|hsl|hwb|lab|lch|oklch|color)\(/i.test(c)
160
163
  })
161
164
 
165
+ const hasChip = computed(() => props.round || props.soft)
166
+
162
167
  const roundPairClass = computed(() => {
163
- if (!props.round || !props.color || isRawCssColor.value) { return undefined }
164
- return `pair-${props.color}`
168
+ if (!hasChip.value || isRawCssColor.value) { return undefined }
169
+ if (props.color) { return `pair-${props.color}` }
170
+ return props.soft ? 'pair-black' : undefined
165
171
  })
166
172
 
167
- const roundClass = computed(() => (props.round ? 'bgl_icon-round' : undefined))
173
+ const roundClass = computed(() => (hasChip.value ? 'bgl_icon-round' : undefined))
174
+ const softClass = computed(() => (props.soft ? 'soft' : undefined))
168
175
 
169
176
  const iconStyle = computed(() => {
170
177
  const style: Record<string, string> = {
@@ -177,11 +184,17 @@ const iconStyle = computed(() => {
177
184
  } else {
178
185
  style['font-variation-settings'] = `'wght' ${props.weight || 400}`
179
186
  }
180
- if (props.round) {
187
+ if (hasChip.value) {
181
188
  style.width = `calc(${computedSize.value}rem * 2)`
182
189
  style.height = `calc(${computedSize.value}rem * 2)`
183
190
  if (isRawCssColor.value && props.color) {
184
- style.backgroundColor = props.color
191
+ if (props.soft) {
192
+ style.backgroundColor = `color-mix(in srgb, ${props.color} 18%, transparent)`
193
+ style.color = props.color
194
+ style.border = `1px solid ${props.color}`
195
+ } else {
196
+ style.backgroundColor = props.color
197
+ }
185
198
  }
186
199
  } else if (props.color) {
187
200
  style.color = props.color
@@ -193,23 +206,24 @@ const iconStyle = computed(() => {
193
206
  <template>
194
207
  <Transition :name="transitionName" mode="out-in">
195
208
  <span
196
- v-if="iconRenderType === 'material'" :key="iconRender" class="bgl_icon-font notranslate"
197
- :class="[animateClass, roundClass, roundPairClass, { 'color-white keep-white': round && isRawCssColor }]"
209
+ v-if="iconRenderType === 'material'" :key="`material-${iconRender}`" class="bgl_icon-font notranslate"
210
+ :class="[animateClass, roundClass, roundPairClass, softClass, { 'color-white keep-white': round && !soft && isRawCssColor }]"
198
211
  :style="iconStyle" translate="no"
199
212
  >
200
213
  {{ iconRender }}
201
214
  </span>
202
215
  <span
203
- v-else-if="iconRenderType === 'font-awesome'" :key="iconRender" class="fa bgl_icon-font notranslate" :class="[
216
+ v-else-if="iconRenderType === 'font-awesome'" :key="`font-awesome-${iconRender}`" class="fa bgl_icon-font notranslate" :class="[
204
217
  `fa-${iconRender}`,
205
218
  animateClass,
206
219
  roundClass,
207
220
  roundPairClass,
221
+ softClass,
208
222
  {
209
223
  'fa-brands': isFaBrand,
210
224
  'fa-solid': fill,
211
225
  'far': !fill && !isFaBrand,
212
- 'color-white keep-white': round && isRawCssColor,
226
+ 'color-white keep-white': round && !soft && isRawCssColor,
213
227
  },
214
228
  ]" :style="iconStyle"
215
229
  translate="no"
@@ -230,7 +230,10 @@ function updateField(key: string, value: any, field: FieldBuilder) {
230
230
  }
231
231
 
232
232
  function isFieldEmpty(value: any, field: FieldBuilder): boolean {
233
- return value == null || value === '' || value === false
233
+ // `false` is a valid answer for boolean radio/toggle fields (for example a
234
+ // required Yes/No question). Only an unchecked checkbox uses false as its
235
+ // empty state.
236
+ return value == null || value === '' || (value === false && field._type === 'checkbox')
234
237
  || (Array.isArray(value) && value.length === 0)
235
238
  || (field._type === 'tel' && typeof value === 'string' && /^\+\d+$/.test(value))
236
239
  }
@@ -386,6 +389,13 @@ function getFieldProps(field: FieldBuilder, key: string) {
386
389
  return baseProps
387
390
  }
388
391
 
392
+ function getCustomFieldProps(field: FieldBuilder, key: string) {
393
+ return {
394
+ ...(field._component?.props ?? {}),
395
+ ...getFieldProps(field, key),
396
+ }
397
+ }
398
+
389
399
  function resolveComponent(componentRef: any): any {
390
400
  if (typeof componentRef === 'string') {
391
401
  if (props.components?.[componentRef] != null) {
@@ -445,6 +455,7 @@ async function handleSubmit(event?: Event): Promise<boolean> {
445
455
 
446
456
  if (!isValid) {
447
457
  event?.stopPropagation()
458
+ formElement.value?.reportValidity()
448
459
 
449
460
  const firstErrorKey = visibleFields.value.find(({ key }) => validationErrors.value[key])?.key
450
461
 
@@ -552,9 +563,7 @@ defineExpose({
552
563
  <template v-else-if="usesCustomComponent(field)">
553
564
  <component
554
565
  :is="resolveComponent(field._component!.component)"
555
- :model-value="getFieldValue(key)"
556
- v-bind="field._component!.props"
557
- @update:model-value="(val: any) => updateField(key, val, field)"
566
+ v-bind="getCustomFieldProps(field, key)"
558
567
  />
559
568
  </template>
560
569
 
@@ -759,6 +759,7 @@ interface JSONSchemaProperty {
759
759
  'title'?: string
760
760
  'description'?: string
761
761
  'default'?: any
762
+ 'const'?: any
762
763
  'enum'?: any[]
763
764
  'format'?: string
764
765
  'minimum'?: number
@@ -1065,6 +1066,27 @@ function createFieldFromProperty(
1065
1066
  field = $.text(config)
1066
1067
  }
1067
1068
 
1069
+ // Rendering is orthogonal to semantics: a checkbox may use a branded
1070
+ // component while remaining a checkbox for validation and serialization.
1071
+ if (
1072
+ xUi.component
1073
+ && fieldType !== 'component'
1074
+ && fieldType !== 'display'
1075
+ ) {
1076
+ field._component = {
1077
+ component: xUi.component,
1078
+ props: xUi.componentProps || {},
1079
+ }
1080
+ }
1081
+
1082
+ if (resolvedProp.const !== undefined) {
1083
+ field.validate(value =>
1084
+ value === resolvedProp.const
1085
+ ? true
1086
+ : config.requiredMessage || 'form.required'
1087
+ )
1088
+ }
1089
+
1068
1090
  // Apply class
1069
1091
  if (xUi.class) {
1070
1092
  field.class(xUi.class)
@@ -101,6 +101,7 @@ export interface FormField {
101
101
  export interface JSONSchemaProperty {
102
102
  type?: string | string[]
103
103
  items?: JSONSchemaProperty
104
+ const?: any
104
105
  title?: string
105
106
  description?: string
106
107
  format?: string
@@ -354,7 +355,12 @@ export function fieldsToSchema(fields: FormField[]): JSONSchemaObject {
354
355
  if (field.validation?.pattern !== undefined) prop.pattern = field.validation.pattern
355
356
 
356
357
  // Required
357
- if (field.required) required.push(field.key)
358
+ if (field.required) {
359
+ required.push(field.key)
360
+ // Required checkboxes represent explicit acceptance, not merely
361
+ // presence of a boolean key.
362
+ if (field.type === 'checkbox') prop.const = true
363
+ }
358
364
  }
359
365
 
360
366
  // Placeholder
package/src/i18n/index.ts CHANGED
@@ -19,6 +19,34 @@ const bagelinkLocales = {
19
19
  ru,
20
20
  } as const
21
21
 
22
+ function mergeMessages(
23
+ base: Record<string, unknown>,
24
+ overrides: Record<string, unknown>,
25
+ ): Record<string, unknown> {
26
+ const merged = { ...base }
27
+
28
+ for (const [key, value] of Object.entries(overrides)) {
29
+ const baseValue = merged[key]
30
+ if (
31
+ baseValue != null
32
+ && value != null
33
+ && typeof baseValue === 'object'
34
+ && typeof value === 'object'
35
+ && !Array.isArray(baseValue)
36
+ && !Array.isArray(value)
37
+ ) {
38
+ merged[key] = mergeMessages(
39
+ baseValue as Record<string, unknown>,
40
+ value as Record<string, unknown>,
41
+ )
42
+ } else {
43
+ merged[key] = value
44
+ }
45
+ }
46
+
47
+ return merged
48
+ }
49
+
22
50
  export interface CreateBagelI18nOptions {
23
51
  /** Active locale (default: 'en'). Same as vue-i18n's `locale`. */
24
52
  locale?: string
@@ -46,10 +74,10 @@ export function createI18n(options: CreateBagelI18nOptions = {}): I18n {
46
74
  const mergedMessages: Record<string, Record<string, unknown>> = {}
47
75
 
48
76
  for (const lang of supportedLocales) {
49
- mergedMessages[lang] = {
50
- ...(bagelinkLocales[lang as keyof typeof bagelinkLocales] || {}),
51
- ...(messages[lang] || {}),
52
- }
77
+ mergedMessages[lang] = mergeMessages(
78
+ bagelinkLocales[lang as keyof typeof bagelinkLocales] || {},
79
+ messages[lang] || {},
80
+ )
53
81
  }
54
82
 
55
83
  // Also include any user-provided locales not in bagelink's built-ins