4 changed files with 335 additions and 0 deletions
-
131src/components/SongButton.vue
-
55src/components/SongCard.vue
-
94src/components/SongInput.vue
-
55src/components/SongTag.vue
@ -0,0 +1,131 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
withDefaults( |
||||
|
defineProps<{ |
||||
|
type?: 'primary' | 'secondary' | 'text' | 'danger' |
||||
|
size?: 'small' | 'medium' | 'large' |
||||
|
disabled?: boolean |
||||
|
block?: boolean |
||||
|
loading?: boolean |
||||
|
}>(), |
||||
|
{ type: 'primary', size: 'medium', disabled: false, block: false, loading: false } |
||||
|
) |
||||
|
defineOptions({ name: 'SongButton' }) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<button |
||||
|
class="song-btn" |
||||
|
:class="[`song-btn--${type}`, `song-btn--${size}`, { 'song-btn--block': block }]" |
||||
|
:disabled="disabled || loading" |
||||
|
:aria-busy="loading" |
||||
|
> |
||||
|
<i v-if="loading" class="song-btn__ink" aria-hidden="true" /> |
||||
|
<slot /> |
||||
|
</button> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.song-btn { |
||||
|
display: inline-flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 6px; |
||||
|
font-family: var(--song-font-sans); |
||||
|
border: 1px solid transparent; |
||||
|
border-radius: var(--song-radius-m); |
||||
|
cursor: pointer; |
||||
|
transition: background-color var(--song-dur-1) var(--song-ease), |
||||
|
color var(--song-dur-1) var(--song-ease), |
||||
|
border-color var(--song-dur-1) var(--song-ease); |
||||
|
user-select: none; |
||||
|
} |
||||
|
|
||||
|
.song-btn--primary { |
||||
|
background: var(--song-btn-bg); |
||||
|
color: var(--song-btn-text); |
||||
|
} |
||||
|
|
||||
|
.song-btn--primary:hover:not(:disabled) { |
||||
|
filter: brightness(1.06); |
||||
|
} |
||||
|
|
||||
|
.song-btn--secondary { |
||||
|
background: transparent; |
||||
|
color: var(--ac-primary-ink); |
||||
|
border-color: var(--ac-border-strong); |
||||
|
} |
||||
|
|
||||
|
.song-btn--secondary:hover:not(:disabled) { |
||||
|
background: var(--song-primary-bg); |
||||
|
} |
||||
|
|
||||
|
.song-btn--text { |
||||
|
background: transparent; |
||||
|
color: var(--ac-primary-ink); |
||||
|
} |
||||
|
|
||||
|
.song-btn--text:hover:not(:disabled) { |
||||
|
background: var(--song-primary-bg); |
||||
|
} |
||||
|
|
||||
|
.song-btn--danger { |
||||
|
background: transparent; |
||||
|
color: var(--ac-danger-ink); |
||||
|
border-color: var(--ac-border-strong); |
||||
|
} |
||||
|
|
||||
|
.song-btn--danger:hover:not(:disabled) { |
||||
|
background: var(--song-danger-bg); |
||||
|
} |
||||
|
|
||||
|
.song-btn--small { |
||||
|
height: 28px; |
||||
|
padding: 0 12px; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.song-btn--medium { |
||||
|
height: 34px; |
||||
|
padding: 0 18px; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.song-btn--large { |
||||
|
height: 42px; |
||||
|
padding: 0 26px; |
||||
|
font-size: 15px; |
||||
|
font-family: var(--song-font-serif); |
||||
|
letter-spacing: 0.06em; |
||||
|
} |
||||
|
|
||||
|
.song-btn--block { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.song-btn:disabled { |
||||
|
opacity: 0.45; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
|
||||
|
.song-btn__ink { |
||||
|
width: 9px; |
||||
|
height: 9px; |
||||
|
border-radius: 50%; |
||||
|
background: currentColor; |
||||
|
opacity: 0.75; |
||||
|
animation: song-btn-ink-pulse 1.6s var(--song-ease) infinite; |
||||
|
} |
||||
|
|
||||
|
@keyframes song-btn-ink-pulse { |
||||
|
0%, |
||||
|
100% { |
||||
|
transform: scale(0.55); |
||||
|
opacity: 0.35; |
||||
|
} |
||||
|
|
||||
|
50% { |
||||
|
transform: scale(0.95); |
||||
|
opacity: 0.85; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,55 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
withDefaults( |
||||
|
defineProps<{ |
||||
|
title?: string |
||||
|
hover?: boolean |
||||
|
}>(), |
||||
|
{ title: '', hover: false } |
||||
|
) |
||||
|
defineOptions({ name: 'SongCard' }) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="song-card" :class="{ 'song-card--hover': hover }"> |
||||
|
<div v-if="title || $slots.header" class="song-card__head"> |
||||
|
<slot name="header"> |
||||
|
<span class="song-card__title">{{ title }}</span> |
||||
|
</slot> |
||||
|
</div> |
||||
|
<div class="song-card__body"> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.song-card { |
||||
|
background: var(--song-bg-elevated); |
||||
|
border: 1px solid var(--song-border); |
||||
|
border-radius: var(--song-radius-l); |
||||
|
box-shadow: var(--song-shadow-1); |
||||
|
transition: transform var(--song-dur-1) var(--song-ease), |
||||
|
box-shadow var(--song-dur-1) var(--song-ease); |
||||
|
} |
||||
|
|
||||
|
.song-card--hover:hover { |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: var(--song-shadow-2); |
||||
|
} |
||||
|
|
||||
|
.song-card__head { |
||||
|
padding: var(--song-space-4) var(--song-space-5); |
||||
|
border-bottom: 1px solid var(--song-border); |
||||
|
} |
||||
|
|
||||
|
.song-card__title { |
||||
|
font-family: var(--song-font-serif); |
||||
|
font-size: 18px; |
||||
|
line-height: 26px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.song-card__body { |
||||
|
padding: var(--song-space-5); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,94 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
withDefaults( |
||||
|
defineProps<{ |
||||
|
label?: string |
||||
|
modelValue?: string | number |
||||
|
placeholder?: string |
||||
|
type?: string |
||||
|
required?: boolean |
||||
|
rows?: number |
||||
|
textarea?: boolean |
||||
|
maxlength?: number |
||||
|
}>(), |
||||
|
{ label: '', modelValue: '', placeholder: '', type: 'text', required: false, rows: 3, textarea: false, maxlength: undefined } |
||||
|
) |
||||
|
defineEmits<{ 'update:modelValue': [value: string] }>() |
||||
|
defineOptions({ name: 'SongInput' }) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<label class="song-field"> |
||||
|
<span v-if="label" class="song-field__label"> |
||||
|
{{ label }}<span v-if="required" class="song-field__req">*</span> |
||||
|
</span> |
||||
|
<textarea |
||||
|
v-if="textarea" |
||||
|
class="song-field__input song-field__input--area" |
||||
|
:value="modelValue" |
||||
|
:placeholder="placeholder" |
||||
|
:rows="rows" |
||||
|
:maxlength="maxlength" |
||||
|
@input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)" |
||||
|
/> |
||||
|
<input |
||||
|
v-else |
||||
|
class="song-field__input" |
||||
|
:type="type" |
||||
|
:value="modelValue" |
||||
|
:placeholder="placeholder" |
||||
|
:maxlength="maxlength" |
||||
|
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)" |
||||
|
/> |
||||
|
</label> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.song-field { |
||||
|
display: block; |
||||
|
} |
||||
|
|
||||
|
.song-field__label { |
||||
|
display: block; |
||||
|
margin-bottom: 6px; |
||||
|
font-size: 13px; |
||||
|
color: var(--ac-text-secondary); |
||||
|
} |
||||
|
|
||||
|
.song-field__req { |
||||
|
color: var(--ac-danger-ink); |
||||
|
margin-left: 2px; |
||||
|
} |
||||
|
|
||||
|
.song-field__input { |
||||
|
width: 100%; |
||||
|
height: 34px; |
||||
|
padding: 0 12px; |
||||
|
font-family: var(--song-font-sans); |
||||
|
font-size: 14px; |
||||
|
color: var(--ac-text); |
||||
|
background: var(--song-bg-elevated); |
||||
|
border: 1px solid var(--ac-border-strong); |
||||
|
border-radius: var(--song-radius-m); |
||||
|
transition: border-color var(--song-dur-1) var(--song-ease); |
||||
|
} |
||||
|
|
||||
|
.song-field__input--area { |
||||
|
height: auto; |
||||
|
padding: 8px 12px; |
||||
|
resize: vertical; |
||||
|
line-height: 22px; |
||||
|
} |
||||
|
|
||||
|
.song-field__input::placeholder { |
||||
|
color: var(--ac-text-disabled); |
||||
|
} |
||||
|
|
||||
|
.song-field__input:hover { |
||||
|
border-color: var(--song-border-strong); |
||||
|
} |
||||
|
|
||||
|
.song-field__input:focus { |
||||
|
outline: none; |
||||
|
border-color: var(--song-primary); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,55 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
withDefaults( |
||||
|
defineProps<{ |
||||
|
type?: 'default' | 'primary' | 'success' | 'warning' | 'danger' |
||||
|
closable?: boolean |
||||
|
}>(), |
||||
|
{ type: 'default', closable: false } |
||||
|
) |
||||
|
defineOptions({ name: 'SongTag' }) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<span class="song-tag" :class="`song-tag--${type}`"> |
||||
|
<slot /> |
||||
|
</span> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.song-tag { |
||||
|
display: inline-flex; |
||||
|
align-items: center; |
||||
|
height: 22px; |
||||
|
padding: 0 10px; |
||||
|
font-size: 12px; |
||||
|
line-height: 20px; |
||||
|
border: 1px solid var(--song-border); |
||||
|
border-radius: var(--song-radius-s); |
||||
|
color: var(--ac-text-secondary); |
||||
|
background: transparent; |
||||
|
} |
||||
|
|
||||
|
.song-tag--primary { |
||||
|
color: var(--ac-primary-ink); |
||||
|
border-color: var(--ac-primary); |
||||
|
background: var(--ac-primary-bg); |
||||
|
} |
||||
|
|
||||
|
.song-tag--success { |
||||
|
color: var(--ac-success-ink); |
||||
|
border-color: var(--ac-success); |
||||
|
background: var(--ac-success-bg); |
||||
|
} |
||||
|
|
||||
|
.song-tag--warning { |
||||
|
color: var(--ac-warning-ink); |
||||
|
border-color: var(--ac-warning); |
||||
|
background: var(--ac-warning-bg); |
||||
|
} |
||||
|
|
||||
|
.song-tag--danger { |
||||
|
color: var(--ac-danger-ink); |
||||
|
border-color: var(--ac-danger); |
||||
|
background: var(--ac-danger-bg); |
||||
|
} |
||||
|
</style> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue