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