Initial commit

This commit is contained in:
Никита Круглицкий
2024-11-26 16:15:12 +03:00
commit b3c99a667a
97 changed files with 14314 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
import type { InjectionKey } from 'vue'
import type { AccordionContext } from './types'
export const accordionContextKey: InjectionKey<AccordionContext> = Symbol('UI_ACCORDION')

View File

@@ -0,0 +1,78 @@
<template>
<div :class="[cn.b()]">
<slot />
</div>
</template>
<script setup lang="ts">
import type { AccordionActiveId, AccordionModelValue } from './types'
import { castArray } from 'lodash-es'
import { provide, ref, watch } from 'vue'
import { accordionContextKey } from './constants'
export interface Props {
multiple?: boolean
modelValue?: AccordionModelValue
}
defineOptions({
name: 'UiAccordion',
})
const props = withDefaults(defineProps<Props>(), {
multiple: false,
})
const emit = defineEmits<{
'update:modelValue': [value: Props['modelValue']]
}>()
const cn = useClassname('ui-accordion')
const activeItems = ref(castArray(props.modelValue))
function setActiveItems(_activeItems: AccordionActiveId[]) {
activeItems.value = _activeItems
const value = !props.multiple ? activeItems.value[0] : activeItems.value
emit('update:modelValue', value)
}
function handleItemClick(id: AccordionActiveId) {
if (!props.multiple) {
setActiveItems([activeItems.value[0] === id ? '' : id])
}
else {
const _activeItems = [...activeItems.value]
const index = _activeItems.indexOf(id)
if (index > -1)
_activeItems.splice(index, 1)
else
_activeItems.push(id)
setActiveItems(_activeItems)
}
}
function isActive(id: AccordionActiveId) {
return activeItems.value.includes(id)
}
watch(
() => props.modelValue,
value => (activeItems.value = castArray(value)),
{ deep: true },
)
provide(accordionContextKey, {
activeItems,
handleItemClick,
isActive,
})
</script>
<style lang="scss">
@use 'styles';
</style>

View File

@@ -0,0 +1,65 @@
<template>
<div :class="[cn.b(), cn.is('focused', focused), cn.is('active', isActive)]">
<button
:class="[cn.e('head')]"
type="button"
@focus="focused = true"
@blur="focused = false"
@click="handleClick"
@keydown.space.enter.stop.prevent="handleClick"
>
<slot name="title">
<h3 :class="[cn.e('title')]">
{{ title }}
</h3>
</slot>
<Component :is="isActive ? IMonoMinus : IMonoPlus" :class="cn.e('icon')" />
</button>
<UiCollapseTransition>
<div
v-show="isActive"
:class="[cn.e('wrapper')]"
>
<div :class="[cn.e('content')]">
<slot />
</div>
</div>
</UiCollapseTransition>
</div>
</template>
<script setup lang="ts">
import IMonoMinus from '~icons/mono/minus'
import IMonoPlus from '~icons/mono/plus'
import { computed, inject, ref } from 'vue'
import UiCollapseTransition from '~/components/ui/collapse-transition/index.vue'
import { accordionContextKey } from './constants'
export interface Props {
id?: string | number
title?: string
}
defineOptions({
name: 'UiAccordionItem',
})
const props = withDefaults(defineProps<Props>(), {
id: () => useId(),
title: '',
})
const accordion = inject(accordionContextKey)
const cn = useClassname('ui-accordion-item')
const focused = ref(false)
const isActive = computed(() => accordion?.isActive(props.id) ?? false)
function handleClick() {
accordion?.handleItemClick(props.id)
}
</script>

View File

@@ -0,0 +1,89 @@
@use '../../../styles/variables' as *;
@use '../../../styles/mixins' as *;
.ui-accordion {
display: flex;
flex-direction: column;
gap: 16px;
}
.ui-accordion-item {
$self: &;
border-radius: 12px;
background-color: $color-white;
transition: outline-color $transition-duration $transition-easing;
-webkit-tap-highlight-color: transparent;
outline: 1px solid $color-gray-500;
outline-offset: -1px;
&:focus,
&:hover,
&.is-active {
outline-width: 2px;
outline-offset: -2px;
}
&.is-active {
outline-color: $color-gray-600;
}
&__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
text-align: left;
border: none;
padding: 24px;
background: transparent;
transition: padding $transition-duration $transition-easing;
cursor: pointer;
outline: none;
user-select: none;
color: $color-black;
@include mobile {
padding: 16px;
}
#{$self}.is-active & {
padding: 24px 24px 4px;
@include mobile {
padding: 16px 16px 4px;
}
}
}
&__title {
@include font(18px, 600, 23px);
@include mobile {
@include font(13px, 600, 17px);
}
}
&__icon {
flex-shrink: 0;
font-size: 24px;
color: $color-gray-600;
}
&__wrapper {
will-change: height;
overflow: hidden;
}
&__content {
@include font(14px, 400, 18px);
color: $color-gray-700;
padding: 0 24px 24px;
@include mobile {
padding: 0 16px 16px;
}
}
}

View File

@@ -0,0 +1,10 @@
import type { Ref } from 'vue'
export type AccordionActiveId = string | number
export type AccordionModelValue = AccordionActiveId | AccordionActiveId[]
export interface AccordionContext {
activeItems: Ref<AccordionActiveId[]>
handleItemClick: (id: AccordionActiveId) => void
isActive: (id: AccordionActiveId) => boolean
}