@bagelink/vue 1.6.2 → 1.6.7

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.6.2",
4
+ "version": "1.6.7",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Neveh Allon",
@@ -58,6 +58,7 @@
58
58
  "bin/experimentalGenTypedRoutes.ts"
59
59
  ],
60
60
  "devDependencies": {
61
+ "swiper": "^12.0.3",
61
62
  "@types/leaflet": "^1.9.18",
62
63
  "@types/signature_pad": "^4.0.0",
63
64
  "@vue-macros/reactivity-transform": "^1.1.6",
@@ -0,0 +1,507 @@
1
+ <script setup lang="ts" generic="T = any">
2
+ import type { SwiperContainer } from 'swiper/element/bundle'
3
+ import type { AutoplayOptions, CoverflowEffectOptions, PaginationOptions, SwiperOptions } from 'swiper/types'
4
+ import { Icon } from '@bagelink/vue'
5
+ import { register } from 'swiper/element/bundle'
6
+ import { computed, onMounted, ref, watch } from 'vue'
7
+ import 'swiper/css'
8
+ import 'swiper/css/navigation'
9
+ import 'swiper/css/pagination'
10
+ import 'swiper/css/effect-fade'
11
+ import 'swiper/css/effect-coverflow'
12
+ import 'swiper/css/effect-cube'
13
+ import 'swiper/css/effect-flip'
14
+
15
+ type SwiperEffect = 'slide' | 'fade' | 'cube' | 'coverflow' | 'flip'
16
+ type SwiperDirection = 'horizontal' | 'vertical'
17
+ type SwiperVariant = 'default' | 'testimonial' | 'gallery' | 'cards' | 'coverflow' | 'hero'
18
+
19
+ interface VariantConfig {
20
+ slidesPerView?: number | 'auto'
21
+ spaceBetween?: number
22
+ effect?: SwiperEffect
23
+ loop?: boolean
24
+ autoplay?: boolean | AutoplayOptions
25
+ pagination?: boolean | PaginationOptions
26
+ navigation?: boolean
27
+ speed?: number
28
+ centeredSlides?: boolean
29
+ coverflowEffect?: Partial<CoverflowEffectOptions>
30
+ breakpoints?: SwiperOptions['breakpoints']
31
+ }
32
+
33
+ const props = withDefaults(
34
+ defineProps<{
35
+ // Items to render
36
+ items?: T[]
37
+
38
+ // Active index (v-model support)
39
+ index?: number
40
+
41
+ // Preset variant
42
+ variant?: SwiperVariant
43
+
44
+ // Slide dimensions
45
+ slideWidth?: number | string
46
+
47
+ // Basic swiper options
48
+ effect?: SwiperEffect
49
+ direction?: SwiperDirection
50
+ speed?: number
51
+ loop?: boolean
52
+ initialSlide?: number
53
+
54
+ // Slides display
55
+ slidesPerView?: number | 'auto'
56
+ spaceBetween?: number
57
+ centeredSlides?: boolean
58
+
59
+ // Interaction
60
+ grabCursor?: boolean
61
+ keyboard?: boolean
62
+ mousewheel?: boolean
63
+
64
+ // Auto height
65
+ autoHeight?: boolean
66
+
67
+ // Navigation
68
+ navigation?: boolean
69
+
70
+ // Pagination
71
+ pagination?: boolean | PaginationOptions
72
+
73
+ // Autoplay
74
+ autoplay?: boolean | AutoplayOptions
75
+
76
+ // Effect-specific options
77
+ coverflowEffect?: Partial<CoverflowEffectOptions>
78
+
79
+ // Responsive breakpoints
80
+ breakpoints?: SwiperOptions['breakpoints']
81
+
82
+ // Advanced: allow override with raw options
83
+ advancedOptions?: SwiperOptions
84
+ }>(),
85
+ {
86
+ items: undefined,
87
+ index: undefined,
88
+ variant: 'default',
89
+ slideWidth: undefined,
90
+ effect: undefined,
91
+ direction: 'horizontal',
92
+ speed: undefined,
93
+ loop: undefined,
94
+ initialSlide: 0,
95
+ slidesPerView: undefined,
96
+ spaceBetween: undefined,
97
+ centeredSlides: undefined,
98
+ grabCursor: true,
99
+ keyboard: true,
100
+ mousewheel: false,
101
+ autoHeight: false,
102
+ navigation: undefined,
103
+ pagination: undefined,
104
+ autoplay: undefined,
105
+ coverflowEffect: undefined,
106
+ breakpoints: undefined,
107
+ advancedOptions: undefined,
108
+ },
109
+ )
110
+
111
+ const emit = defineEmits<{
112
+ 'update:index': [index: number]
113
+ }>()
114
+
115
+ defineSlots<{
116
+ 'default'?: (props: { item: T, index: number, currentIndex: number }) => any
117
+ 'prev-button'?: () => any
118
+ 'next-button'?: () => any
119
+ 'navigation'?: (props: {
120
+ items: T[]
121
+ currentIndex: number
122
+ slideTo: (index: number) => void
123
+ prev: () => void
124
+ next: () => void
125
+ isFirst: boolean
126
+ isLast: boolean
127
+ }) => any
128
+ 'pagination'?: () => any
129
+ }>()
130
+
131
+ const variantPresets: Record<SwiperVariant, VariantConfig> = {
132
+ default: {},
133
+ testimonial: {
134
+ slidesPerView: 1,
135
+ autoplay: { delay: 5000, disableOnInteraction: false },
136
+ pagination: { clickable: true },
137
+ loop: true,
138
+ speed: 600,
139
+ },
140
+ gallery: {
141
+ slidesPerView: 1,
142
+ spaceBetween: 0,
143
+ navigation: true,
144
+ loop: true,
145
+ },
146
+ cards: {
147
+ slidesPerView: 3,
148
+ spaceBetween: 20,
149
+ navigation: true,
150
+ breakpoints: {
151
+ 320: { slidesPerView: 1, spaceBetween: 10 },
152
+ 768: { slidesPerView: 2, spaceBetween: 15 },
153
+ 1024: { slidesPerView: 3, spaceBetween: 20 },
154
+ },
155
+ },
156
+ coverflow: {
157
+ effect: 'coverflow',
158
+ slidesPerView: 'auto',
159
+ centeredSlides: true,
160
+ loop: true,
161
+ navigation: true,
162
+ coverflowEffect: {
163
+ rotate: -2,
164
+ stretch: 0,
165
+ depth: 100,
166
+ slideShadows: true,
167
+ modifier: 1,
168
+ scale: 0.98,
169
+ },
170
+ },
171
+ hero: {
172
+ slidesPerView: 1,
173
+ autoplay: { delay: 4000 },
174
+ pagination: { clickable: true },
175
+ loop: true,
176
+ speed: 800,
177
+ effect: 'fade',
178
+ },
179
+ }
180
+
181
+ register()
182
+
183
+ const swiperEl = $ref<SwiperContainer | null>(null)
184
+ const isFirstSlide = ref(true)
185
+ const isLastSlide = ref(false)
186
+ const currentIndex = ref(props.index ?? props.initialSlide ?? 0)
187
+ let isUpdatingFromExternal = false // Guard to prevent update loops
188
+
189
+ const slideStyles = $computed(() => {
190
+ const styles: Record<string, string> = {}
191
+
192
+ if (props.slideWidth !== undefined) {
193
+ styles.width = typeof props.slideWidth === 'number' ? `${props.slideWidth}px` : props.slideWidth
194
+ }
195
+
196
+ return styles
197
+ })
198
+
199
+ // Apply variant preset configuration
200
+ const variantConfig = $computed(() => variantPresets[props.variant])
201
+
202
+ // Smart default: if slideWidth is set and slidesPerView wasn't set,
203
+ // automatically use 'auto' to let slides determine their own width
204
+ const effectiveSlidesPerView = $computed(() => {
205
+ // Priority: explicit prop > variant config > smart default
206
+ if (props.slidesPerView !== undefined) {
207
+ return props.slidesPerView
208
+ }
209
+
210
+ if (variantConfig.slidesPerView !== undefined) {
211
+ return variantConfig.slidesPerView
212
+ }
213
+
214
+ // Smart default: if slideWidth is provided, use 'auto'
215
+ if (props.slideWidth !== undefined) {
216
+ return 'auto'
217
+ }
218
+
219
+ // Final fallback
220
+ return 1
221
+ })
222
+
223
+ const swiperParams = computed((): SwiperOptions => {
224
+ // Helper to get value with priority: prop > variant > default
225
+ const getValue = <K extends keyof VariantConfig>(
226
+ propValue: any,
227
+ variantKey: K,
228
+ defaultValue: any
229
+ ) => {
230
+ if (propValue !== undefined) return propValue
231
+ if (variantConfig[variantKey] !== undefined) return variantConfig[variantKey]
232
+ return defaultValue
233
+ }
234
+
235
+ const params: SwiperOptions = {
236
+ effect: getValue(props.effect, 'effect', 'slide'),
237
+ direction: props.direction ?? 'horizontal',
238
+ speed: getValue(props.speed, 'speed', 300),
239
+ loop: getValue(props.loop, 'loop', false),
240
+ initialSlide: props.index ?? props.initialSlide,
241
+ slidesPerView: effectiveSlidesPerView,
242
+ spaceBetween: getValue(props.spaceBetween, 'spaceBetween', 0),
243
+ centeredSlides: getValue(props.centeredSlides, 'centeredSlides', false),
244
+ grabCursor: props.grabCursor,
245
+ keyboard: props.keyboard ? { enabled: true } : false,
246
+ mousewheel: props.mousewheel ? { enabled: true } : false,
247
+ autoHeight: props.autoHeight,
248
+ }
249
+
250
+ // Navigation (priority: prop > variant > default)
251
+ const navigationEnabled = getValue(props.navigation, 'navigation', false)
252
+ if (navigationEnabled) {
253
+ params.navigation = true
254
+ }
255
+
256
+ // Pagination (priority: prop > variant > default)
257
+ const paginationConfig = getValue(props.pagination, 'pagination', false)
258
+ if (paginationConfig) {
259
+ params.pagination = typeof paginationConfig === 'boolean'
260
+ ? { clickable: true }
261
+ : paginationConfig
262
+ }
263
+
264
+ // Autoplay (priority: prop > variant > default)
265
+ const autoplayConfig = getValue(props.autoplay, 'autoplay', false)
266
+ if (autoplayConfig) {
267
+ params.autoplay = typeof autoplayConfig === 'boolean'
268
+ ? { delay: 3000 }
269
+ : autoplayConfig
270
+ }
271
+
272
+ // Effect-specific options (merge variant and explicit)
273
+ const effectType = params.effect || 'slide'
274
+ if (effectType === 'coverflow') {
275
+ const baseCoverflowEffect = {
276
+ rotate: 0,
277
+ stretch: 0,
278
+ depth: 100,
279
+ modifier: 1,
280
+ slideShadows: true,
281
+ }
282
+ params.coverflowEffect = {
283
+ ...baseCoverflowEffect,
284
+ ...variantConfig.coverflowEffect,
285
+ ...props.coverflowEffect,
286
+ }
287
+ }
288
+
289
+ // Breakpoints (priority: prop > variant)
290
+ const breakpointsConfig = getValue(props.breakpoints, 'breakpoints', undefined)
291
+ if (breakpointsConfig) {
292
+ params.breakpoints = breakpointsConfig
293
+ }
294
+
295
+ // Advanced options override
296
+ if (props.advancedOptions) {
297
+ return { ...params, ...props.advancedOptions }
298
+ }
299
+
300
+ return params
301
+ })
302
+
303
+ const swiperInstance = $computed(() => swiperEl?.swiper)
304
+
305
+ function updateNavigationState() {
306
+ if (swiperEl?.swiper && !isUpdatingFromExternal) {
307
+ isFirstSlide.value = swiperEl.swiper.isBeginning
308
+ isLastSlide.value = swiperEl.swiper.isEnd
309
+
310
+ // Emit index update for v-model support
311
+ // Use activeIndex for non-loop mode, realIndex for loop mode
312
+ const newIndex = props.loop ? swiperEl.swiper.realIndex : swiperEl.swiper.activeIndex
313
+ currentIndex.value = newIndex
314
+
315
+ // Emit to keep external state in sync
316
+ emit('update:index', newIndex)
317
+ }
318
+ }
319
+
320
+ function slideTo(index: number) {
321
+ if (!swiperEl?.swiper) return
322
+ swiperEl.swiper.slideTo(index)
323
+ }
324
+
325
+ function handleSlideNav(dir: 'prev' | 'next') {
326
+ if (!swiperEl?.swiper) return
327
+
328
+ if (dir === 'next') {
329
+ swiperEl.swiper.slideNext()
330
+ } else {
331
+ swiperEl.swiper.slidePrev()
332
+ }
333
+ }
334
+
335
+ onMounted(() => {
336
+ if (!swiperEl) return
337
+
338
+ // Add events to track slide changes for reactive state
339
+ const params = {
340
+ ...swiperParams.value,
341
+ on: {
342
+ ...swiperParams.value.on,
343
+ init: updateNavigationState,
344
+ slideChange: updateNavigationState,
345
+ slideChangeTransitionEnd: updateNavigationState,
346
+ reachBeginning: updateNavigationState,
347
+ reachEnd: updateNavigationState,
348
+ fromEdge: updateNavigationState,
349
+ },
350
+ }
351
+
352
+ Object.assign(swiperEl, params)
353
+ swiperEl.initialize()
354
+ })
355
+
356
+ watch(
357
+ () => props.initialSlide,
358
+ (index) => {
359
+ if (index !== undefined && swiperEl?.swiper) {
360
+ setTimeout(() => swiperEl.swiper.slideTo(index), 1)
361
+ }
362
+ },
363
+ { immediate: true },
364
+ )
365
+
366
+ // Watch for external index changes (v-model support)
367
+ watch(
368
+ () => props.index,
369
+ (newIndex) => {
370
+ if (newIndex !== undefined) {
371
+ currentIndex.value = newIndex
372
+
373
+ if (swiperEl?.swiper) {
374
+ const activeIndex = props.loop ? swiperEl.swiper.realIndex : swiperEl.swiper.activeIndex
375
+ if (newIndex !== activeIndex) {
376
+ isUpdatingFromExternal = true
377
+ swiperEl.swiper.slideTo(newIndex)
378
+ // Reset guard after a short delay to allow transition
379
+ setTimeout(() => {
380
+ isUpdatingFromExternal = false
381
+ }, 50)
382
+ }
383
+ }
384
+ }
385
+ },
386
+ )
387
+
388
+ // Watch for param changes and update swiper
389
+ watch(
390
+ swiperParams,
391
+ (newParams) => {
392
+ if (swiperEl?.swiper) {
393
+ Object.assign(swiperEl.swiper.params, newParams)
394
+ swiperEl.swiper.update()
395
+ }
396
+ },
397
+ { deep: true },
398
+ )
399
+
400
+ defineExpose({
401
+ swiper: swiperInstance,
402
+ slideNext: () => { handleSlideNav('next') },
403
+ slidePrev: () => { handleSlideNav('prev') },
404
+ slideTo,
405
+ update: () => swiperEl?.swiper?.update(),
406
+ isFirst: isFirstSlide,
407
+ isLast: isLastSlide,
408
+ currentIndex,
409
+ })
410
+ </script>
411
+
412
+ <template>
413
+ <div class="swi-wrap">
414
+ <swiper-container ref="swiperEl" class="swiper" init="false">
415
+ <swiper-slide v-for="(item, index) in items" :key="index" :style="slideStyles">
416
+ <slot :item="item" :index="index" :currentIndex="currentIndex" />
417
+ </swiper-slide>
418
+ </swiper-container>
419
+
420
+ <!-- Custom Navigation Slot -->
421
+ <slot
422
+ name="navigation" :items="items || []" :currentIndex="currentIndex" :slide-to="slideTo"
423
+ :prev="() => handleSlideNav('prev')" :next="() => handleSlideNav('next')" :is-first="isFirstSlide"
424
+ :is-last="isLastSlide"
425
+ >
426
+ <!-- Default Navigation -->
427
+ <div v-if="navigation" class="swi-ctrl">
428
+ <div class="swi-prev hover" @click="handleSlideNav('prev')">
429
+ <slot name="prev-button">
430
+ <Icon name="chevron_left" />
431
+ </slot>
432
+ </div>
433
+ <div class="swi-next hover" @click="handleSlideNav('next')">
434
+ <slot name="next-button">
435
+ <Icon name="chevron_right" />
436
+ </slot>
437
+ </div>
438
+ </div>
439
+ </slot>
440
+
441
+ <!-- Custom Pagination Slot -->
442
+ <slot v-if="pagination" name="pagination" />
443
+ </div>
444
+ </template>
445
+
446
+ <style>
447
+ .swiper {
448
+ width: 100%;
449
+ padding-top: 50px;
450
+ padding-bottom: 50px;
451
+ }
452
+
453
+ :root {
454
+ --swiper-navigation-color: white;
455
+ --swiper-pagination-color: white;
456
+ }
457
+
458
+ .swi-ctrl {
459
+ position: absolute;
460
+ top: 50%;
461
+ transform: translateY(-50%);
462
+ width: 100%;
463
+ z-index: 99;
464
+ display: flex;
465
+ justify-content: space-between;
466
+ padding: 0rem 4rem;
467
+ height: 0;
468
+ pointer-events: none;
469
+ }
470
+
471
+ .swi-ctrl>* {
472
+ pointer-events: auto;
473
+ }
474
+
475
+ .swi-ctrl img {
476
+ height: 20px;
477
+ }
478
+
479
+ .swi-wrap {
480
+ position: relative;
481
+ }
482
+
483
+ .swi-prev,
484
+ .swi-next {
485
+ background: var(--blue);
486
+ height: 40px;
487
+ width: 40px;
488
+ border-radius: 100%;
489
+ padding: 10px;
490
+ display: flex;
491
+ justify-content: center;
492
+ align-items: center;
493
+ cursor: pointer;
494
+ transition: opacity 0.3s ease;
495
+ }
496
+
497
+ .swi-prev:hover,
498
+ .swi-next:hover {
499
+ opacity: 0.8;
500
+ }
501
+
502
+ @media screen and (max-width: 900px) {
503
+ .swi-ctrl {
504
+ padding: 0rem 0.5rem;
505
+ }
506
+ }
507
+ </style>
@@ -7,7 +7,7 @@ import { useHighlight } from './useHighlight'
7
7
  const props = defineProps({
8
8
  language: { type: String as PropType<Language>, default: 'html' },
9
9
  readonly: { type: Boolean, default: false },
10
- modelValue: { type: String, default: '' },
10
+ modelValue: { type: [String, Object] as PropType<string | Record<string, any>>, default: '' },
11
11
  autodetect: { type: Boolean, default: true },
12
12
  ignoreIllegals: { type: Boolean, default: true },
13
13
  label: { type: String, default: '' },
@@ -17,13 +17,26 @@ const props = defineProps({
17
17
  })
18
18
 
19
19
  const emit = defineEmits(['update:modelValue'])
20
+
21
+ // Track whether the original modelValue was an object
22
+ const isObjectMode = computed(() => typeof props.modelValue === 'object')
23
+
24
+ // Helper to convert value to string for display
25
+ function valueToString(value: string | Record<string, any>): string {
26
+ if (typeof value === 'string') return value
27
+ if (typeof value === 'object') {
28
+ return JSON.stringify(value, null, 2)
29
+ }
30
+ return ''
31
+ }
32
+
20
33
  // State
21
- const code = ref(props.modelValue || '')
34
+ const code = ref(valueToString(props.modelValue))
22
35
  const editorRef = ref<HTMLDivElement>()
23
36
  const { loaded, loadHighlight, highlightCode, setTheme } = useHighlight(props.theme)
24
37
 
25
38
  const maxHeight = computed(() => {
26
- const h = props.height ?? '240px'
39
+ const h = props.height
27
40
  return h.match(/^\d+$/) ? `${h}px` : h
28
41
  })
29
42
 
@@ -39,11 +52,24 @@ const formattedCode = computed(() => {
39
52
  function handleInput(e: Event) {
40
53
  const target = e.target as HTMLTextAreaElement
41
54
  code.value = target.value
42
- emit('update:modelValue', code.value)
55
+
56
+ // If originally an object, try to parse and emit object
57
+ if (isObjectMode.value) {
58
+ try {
59
+ const parsed = JSON.parse(code.value)
60
+ emit('update:modelValue', parsed)
61
+ } catch {
62
+ // If parsing fails, emit the string as-is (user is still typing)
63
+ // This allows intermediate invalid JSON states while editing
64
+ emit('update:modelValue', code.value)
65
+ }
66
+ } else {
67
+ emit('update:modelValue', code.value)
68
+ }
43
69
  }
44
70
 
45
71
  function handleTab(event: KeyboardEvent) {
46
- if ('Tab' !== event.key) {return}
72
+ if (event.key === 'Tab') { return }
47
73
 
48
74
  event.preventDefault()
49
75
  const target = event.target as HTMLTextAreaElement
@@ -53,11 +79,24 @@ function handleTab(event: KeyboardEvent) {
53
79
  // Add tab or indent selected text
54
80
  const newValue = `${code.value.substring(0, start)} ${code.value.substring(end)}`
55
81
  code.value = newValue
56
- emit('update:modelValue', code.value)
82
+
83
+ // If originally an object, try to parse and emit object
84
+ if (isObjectMode.value) {
85
+ try {
86
+ const parsed = JSON.parse(code.value)
87
+ emit('update:modelValue', parsed)
88
+ } catch {
89
+ // If parsing fails, emit the string as-is
90
+ emit('update:modelValue', code.value)
91
+ }
92
+ } else {
93
+ emit('update:modelValue', code.value)
94
+ }
57
95
 
58
96
  // Move cursor position after the inserted tab
59
97
  setTimeout(() => {
60
- target.selectionStart = target.selectionEnd = start + 2
98
+ target.selectionStart = start + 2
99
+ target.selectionEnd = start + 2
61
100
  }, 0)
62
101
  }
63
102
 
@@ -68,8 +107,9 @@ onMounted(async () => {
68
107
 
69
108
  watch(() => props.theme, (t) => { setTheme(t) })
70
109
  watch(() => props.modelValue, (newVal) => {
71
- if (newVal !== undefined && newVal !== code.value) {
72
- code.value = newVal
110
+ const newCodeStr = valueToString(newVal)
111
+ if (newCodeStr !== code.value) {
112
+ code.value = newCodeStr
73
113
  }
74
114
  }, { immediate: true })
75
115
  </script>
@@ -41,6 +41,7 @@ export { default as Rating } from './Rating.vue'
41
41
  export { default as RouterWrapper } from './RouterWrapper.vue'
42
42
  export { default as Slider } from './Slider.vue'
43
43
  export { default as Spreadsheet } from './Spreadsheet/Index.vue'
44
+ export { default as Swiper } from './Swiper.vue'
44
45
  export { default as Title } from './Title.vue'
45
46
  export { default as ToolBar } from './ToolBar.vue'
46
47
  export { default as TopBar } from './TopBar.vue'
@@ -55,10 +55,66 @@
55
55
  border-radius: 100%;
56
56
  }
57
57
 
58
- .aspect-ratio-1 {
58
+ .aspect-ratio-1,
59
+ .ratio-1 {
59
60
  aspect-ratio: 1;
60
61
  }
61
62
 
63
+ .aspect-ratio-4-3,
64
+ .ratio-4-3 {
65
+ aspect-ratio: 4 / 3;
66
+ }
67
+
68
+ .aspect-ratio-3-4,
69
+ .ratio-3-4 {
70
+ aspect-ratio: 3 / 4;
71
+ }
72
+
73
+ .aspect-ratio-16-9,
74
+ .ratio-16-9 {
75
+ aspect-ratio: 16 / 9;
76
+ }
77
+
78
+ .aspect-ratio-9-16,
79
+ .ratio-9-16 {
80
+ aspect-ratio: 9 / 16;
81
+ }
82
+
83
+ .aspect-ratio-3-2,
84
+ .ratio-3-2 {
85
+ aspect-ratio: 3 / 2;
86
+ }
87
+
88
+ .aspect-ratio-2-3,
89
+ .ratio-2-3 {
90
+ aspect-ratio: 2 / 3;
91
+ }
92
+
93
+ .aspect-ratio-5-4,
94
+ .ratio-5-4 {
95
+ aspect-ratio: 5 / 4;
96
+ }
97
+
98
+ .aspect-ratio-4-5,
99
+ .ratio-4-5 {
100
+ aspect-ratio: 4 / 5;
101
+ }
102
+
103
+ .aspect-ratio-2-1,
104
+ .ratio-2-1 {
105
+ aspect-ratio: 2 / 1;
106
+ }
107
+
108
+ .aspect-ratio-1-2,
109
+ .ratio-1-2 {
110
+ aspect-ratio: 1 / 2;
111
+ }
112
+
113
+ .aspect-ratio-21-9,
114
+ .ratio-21-9 {
115
+ aspect-ratio: 21 / 9;
116
+ }
117
+
62
118
  .vertical-align-middle,
63
119
  .vertical-middle {
64
120
  vertical-align: middle;