mirror of
https://github.com/elk-zone/elk.git
synced 2024-11-23 18:05:25 +03:00
feat: added a profile settings and settings nav (#432)
This commit is contained in:
parent
c8a7e6e7e7
commit
613c5315b3
23 changed files with 698 additions and 7 deletions
|
@ -62,6 +62,8 @@ watchEffect(() => {
|
||||||
namedFields.value = named
|
namedFields.value = named
|
||||||
iconFields.value = icons
|
iconFields.value = icons
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isSelf = $computed(() => currentUser.value?.account.id === account.id)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -91,6 +93,15 @@ watchEffect(() => {
|
||||||
<div absolute top-18 right-0 flex gap-2 items-center>
|
<div absolute top-18 right-0 flex gap-2 items-center>
|
||||||
<AccountMoreButton :account="account" :command="command" />
|
<AccountMoreButton :account="account" :command="command" />
|
||||||
<AccountFollowButton :account="account" :command="command" />
|
<AccountFollowButton :account="account" :command="command" />
|
||||||
|
<!-- Edit profile -->
|
||||||
|
<NuxtLink
|
||||||
|
v-if="isSelf"
|
||||||
|
to="/settings/profile/appearance"
|
||||||
|
gap-1 items-center border="1" rounded-full flex="~ gap2 center" font-500 w-30 h-fit py1
|
||||||
|
hover="border-primary text-primary bg-active"
|
||||||
|
>
|
||||||
|
{{ $t('settings.profile.appearance.title') }}
|
||||||
|
</NuxtLink>
|
||||||
<!-- <button flex gap-1 items-center w-full rounded op75 hover="op100 text-purple" group>
|
<!-- <button flex gap-1 items-center w-full rounded op75 hover="op100 text-purple" group>
|
||||||
<div rounded p2 group-hover="bg-rose/10">
|
<div rounded p2 group-hover="bg-rose/10">
|
||||||
<div i-ri:bell-line />
|
<div i-ri:bell-line />
|
||||||
|
|
109
components/common/CommonCropImage.vue
Normal file
109
components/common/CommonCropImage.vue
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Boundaries } from 'vue-advanced-cropper'
|
||||||
|
import { Cropper } from 'vue-advanced-cropper'
|
||||||
|
import 'vue-advanced-cropper/dist/style.css'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
/** Images to be cropped */
|
||||||
|
modelValue?: File
|
||||||
|
/** Crop frame aspect ratio (width/height), default 1/1 */
|
||||||
|
stencilAspectRatio?: number
|
||||||
|
/** The ratio of the longest edge of the cut box to the length of the cut screen, default 0.9, not more than 1 */
|
||||||
|
stencilSizePercentage?: number
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
stencilAspectRatio: 1 / 1,
|
||||||
|
stencilSizePercentage: 0.9,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: File): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const vmFile = useVModel(props, 'modelValue', emits, { passive: true })
|
||||||
|
|
||||||
|
const cropperDialog = ref(false)
|
||||||
|
|
||||||
|
const cropper = ref<InstanceType<typeof Cropper>>()
|
||||||
|
|
||||||
|
const cropperFlag = ref(false)
|
||||||
|
|
||||||
|
const cropperImage = reactive({
|
||||||
|
src: '',
|
||||||
|
type: 'image/jpg',
|
||||||
|
})
|
||||||
|
|
||||||
|
const stencilSize = ({ boundaries }: { boundaries: Boundaries }) => {
|
||||||
|
return {
|
||||||
|
width: boundaries.width * props.stencilSizePercentage,
|
||||||
|
height: boundaries.height * props.stencilSizePercentage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(vmFile, (file, _, onCleanup) => {
|
||||||
|
let expired = false
|
||||||
|
onCleanup(() => expired = true)
|
||||||
|
|
||||||
|
if (file && !cropperFlag.value) {
|
||||||
|
cropperDialog.value = true
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
reader.onload = (e) => {
|
||||||
|
if (expired)
|
||||||
|
return
|
||||||
|
cropperImage.src = e.target?.result as string
|
||||||
|
cropperImage.type = file.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cropperFlag.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
const cropImage = () => {
|
||||||
|
if (cropper.value && vmFile.value) {
|
||||||
|
cropperFlag.value = true
|
||||||
|
cropperDialog.value = false
|
||||||
|
const { canvas } = cropper.value.getResult()
|
||||||
|
canvas?.toBlob((blob) => {
|
||||||
|
vmFile.value = new File([blob as any], `cropped${vmFile.value?.name}` as string, { type: blob?.type })
|
||||||
|
}, cropperImage.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ModalDialog v-model="cropperDialog" :use-v-if="false" max-w-500px flex>
|
||||||
|
<div flex-1 w-0>
|
||||||
|
<div text-lg text-center my2 px3>
|
||||||
|
<h1>
|
||||||
|
{{ $t('action.edit') }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div aspect-ratio-1>
|
||||||
|
<Cropper
|
||||||
|
ref="cropper"
|
||||||
|
class="overflow-hidden w-full h-full"
|
||||||
|
:src="cropperImage.src"
|
||||||
|
:resize-image="{
|
||||||
|
adjustStencil: false,
|
||||||
|
}"
|
||||||
|
:stencil-size="stencilSize"
|
||||||
|
:stencil-props="{
|
||||||
|
aspectRatio: props.stencilAspectRatio,
|
||||||
|
movable: false,
|
||||||
|
resizable: false,
|
||||||
|
handlers: {},
|
||||||
|
}"
|
||||||
|
image-restriction="stencil"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div m-4>
|
||||||
|
<button
|
||||||
|
btn-solid w-full rounded text-sm
|
||||||
|
@click="cropImage()"
|
||||||
|
>
|
||||||
|
{{ $t('action.confirm') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalDialog>
|
||||||
|
</template>
|
121
components/common/CommonInputImage.vue
Normal file
121
components/common/CommonInputImage.vue
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
modelValue?: File
|
||||||
|
/** The image src before change */
|
||||||
|
original?: string
|
||||||
|
/** Allowed file types */
|
||||||
|
allowedFileTypes?: string[]
|
||||||
|
/** Allowed file size */
|
||||||
|
allowedFileSize?: number
|
||||||
|
|
||||||
|
imgClass?: string
|
||||||
|
|
||||||
|
loading?: boolean
|
||||||
|
}>(), {
|
||||||
|
allowedFileTypes: () => ['image/jpeg', 'image/png'],
|
||||||
|
allowedFileSize: 1024 * 1024 * 5, // 5 MB
|
||||||
|
})
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: File): void
|
||||||
|
(event: 'error', code: number, message: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const vmFile = useVModel(props, 'modelValue', emits, { passive: true })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const elInput = ref<HTMLInputElement>()
|
||||||
|
|
||||||
|
function clearInput() {
|
||||||
|
if (elInput.value)
|
||||||
|
elInput.value.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectImage(e: Event) {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
const image = target.files?.[0]
|
||||||
|
if (!image) {
|
||||||
|
vmFile.value = image
|
||||||
|
}
|
||||||
|
else if (!props.allowedFileTypes.includes(image.type)) {
|
||||||
|
emits('error', 1, t('error.unsupported_file_format'))
|
||||||
|
clearInput()
|
||||||
|
}
|
||||||
|
else if (image.size > props.allowedFileSize) {
|
||||||
|
emits('error', 2, t('error.file_size_cannot_exceed_n_mb', [5]))
|
||||||
|
clearInput()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
vmFile.value = image
|
||||||
|
clearInput()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultImage = computed(() => props.original || '')
|
||||||
|
/** Preview of selected images */
|
||||||
|
const previewImage = ref('')
|
||||||
|
/** The current images on display */
|
||||||
|
const imageSrc = computed<string>(() => previewImage.value || defaultImage.value)
|
||||||
|
|
||||||
|
// Update the preview image when the input file change
|
||||||
|
watch(vmFile, (image, _, onCleanup) => {
|
||||||
|
let expired = false
|
||||||
|
onCleanup(() => expired = true)
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(image)
|
||||||
|
reader.onload = (e) => {
|
||||||
|
if (expired)
|
||||||
|
return
|
||||||
|
previewImage.value = e.target?.result as string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
previewImage.value = ''
|
||||||
|
clearInput()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
clearInput,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<label
|
||||||
|
class="bg-slate-500/10 focus-within:(outline outline-primary)"
|
||||||
|
relative
|
||||||
|
flex justify-center items-center
|
||||||
|
cursor-pointer
|
||||||
|
of-hidden
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="imageSrc"
|
||||||
|
:src="imageSrc"
|
||||||
|
:class="imgClass || ''"
|
||||||
|
object-cover
|
||||||
|
w-full
|
||||||
|
h-full
|
||||||
|
>
|
||||||
|
<div absolute bg="black/50" text-white rounded-full text-xl w12 h12 flex justify-center items-center hover="bg-black/40 text-primary">
|
||||||
|
<div i-ri:upload-line />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
absolute inset-0
|
||||||
|
bg="black/30" text-white
|
||||||
|
flex justify-center items-center
|
||||||
|
>
|
||||||
|
<div class="i-ri:loader-4-line animate-spin animate-duration-[2.5s]" text-4xl />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="elInput"
|
||||||
|
type="file"
|
||||||
|
absolute opacity-0 inset-0 z--1
|
||||||
|
:accept="allowedFileTypes.join(',')"
|
||||||
|
@change="selectImage"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</template>
|
|
@ -21,5 +21,6 @@ const { notifications } = useNotifications()
|
||||||
<NavSideItem :text="$t('nav_side.conversations')" to="/conversations" icon="i-ri:at-line" user-only />
|
<NavSideItem :text="$t('nav_side.conversations')" to="/conversations" icon="i-ri:at-line" user-only />
|
||||||
<NavSideItem :text="$t('nav_side.favourites')" to="/favourites" icon="i-ri:heart-3-line" user-only />
|
<NavSideItem :text="$t('nav_side.favourites')" to="/favourites" icon="i-ri:heart-3-line" user-only />
|
||||||
<NavSideItem :text="$t('nav_side.bookmarks')" to="/bookmarks" icon="i-ri:bookmark-line " user-only />
|
<NavSideItem :text="$t('nav_side.bookmarks')" to="/bookmarks" icon="i-ri:bookmark-line " user-only />
|
||||||
|
<NavSideItem :text="$t('nav_side.settings')" to="/settings" icon="i-ri:settings-4-line " user-only />
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
60
components/settings/SettingsNavItem.vue
Normal file
60
components/settings/SettingsNavItem.vue
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const props = defineProps<{
|
||||||
|
text?: string
|
||||||
|
icon?: string
|
||||||
|
to: string | Record<string, string>
|
||||||
|
command?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
if (props.command) {
|
||||||
|
useCommand({
|
||||||
|
scope: 'Settings',
|
||||||
|
|
||||||
|
name: () => props.text ?? (typeof props.to === 'string' ? props.to as string : props.to.name),
|
||||||
|
icon: () => props.icon || '',
|
||||||
|
|
||||||
|
onActivate() {
|
||||||
|
router.push(props.to)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NuxtLink
|
||||||
|
:to="to"
|
||||||
|
exact-active-class="text-primary"
|
||||||
|
block w-full group focus:outline-none
|
||||||
|
@click="$scrollToTop"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
w-full flex w-fit px5 py3 md:gap2 gap4 items-center
|
||||||
|
transition-250 group-hover:bg-active
|
||||||
|
group-focus-visible:ring="2 current"
|
||||||
|
>
|
||||||
|
<div flex-1 flex items-center md:gap2 gap4>
|
||||||
|
<div
|
||||||
|
flex items-center justify-center
|
||||||
|
:class="$slots.description ? 'w-12 h-12' : ''"
|
||||||
|
>
|
||||||
|
<slot name="icon">
|
||||||
|
<div v-if="icon" :class="icon" md:text-size-inherit text-xl />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div space-y-1>
|
||||||
|
<p>
|
||||||
|
<slot>
|
||||||
|
<span>{{ text }}</span>
|
||||||
|
</slot>
|
||||||
|
</p>
|
||||||
|
<p v-if="$slots.description" text-sm text-secondary>
|
||||||
|
<slot name="description" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div i-ri:arrow-right-s-line text-xl text-secondary-light />
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
|
@ -44,6 +44,13 @@ const switchUser = (user: UserLogin) => {
|
||||||
icon="i-ri:user-add-line"
|
icon="i-ri:user-add-line"
|
||||||
@click="openSigninDialog"
|
@click="openSigninDialog"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<NuxtLink to="/settings">
|
||||||
|
<CommonDropdownItem
|
||||||
|
:text="$t('nav_side.settings')"
|
||||||
|
icon="i-ri:settings-4-line"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
<CommonDropdownItem
|
<CommonDropdownItem
|
||||||
v-if="isMastoInitialised && currentUser"
|
v-if="isMastoInitialised && currentUser"
|
||||||
:text="$t('user.sign_out_account', [getFullHandle(currentUser.account)])"
|
:text="$t('user.sign_out_account', [getFullHandle(currentUser.account)])"
|
||||||
|
|
|
@ -14,6 +14,7 @@ const scopes = [
|
||||||
'Account',
|
'Account',
|
||||||
'Languages',
|
'Languages',
|
||||||
'Switch account',
|
'Switch account',
|
||||||
|
'Settings',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type CommandScopeNames = typeof scopes[number]
|
export type CommandScopeNames = typeof scopes[number]
|
||||||
|
|
|
@ -103,6 +103,24 @@ async function loginTo(user?: Omit<UserLogin, 'account'> & { account?: AccountCr
|
||||||
return masto
|
return masto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setAccountInfo(userId: string, account: AccountCredentials) {
|
||||||
|
const index = getUsersIndexByUserId(userId)
|
||||||
|
if (index === -1)
|
||||||
|
return false
|
||||||
|
|
||||||
|
users.value[index].account = account
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pullMyAccountInfo() {
|
||||||
|
const me = await useMasto().accounts.verifyCredentials()
|
||||||
|
setAccountInfo(currentUserId.value, me)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUsersIndexByUserId(userId: string) {
|
||||||
|
return users.value.findIndex(u => u.account?.id === userId)
|
||||||
|
}
|
||||||
|
|
||||||
export async function removePushNotificationData(user: UserLogin, fromSWPushManager = true) {
|
export async function removePushNotificationData(user: UserLogin, fromSWPushManager = true) {
|
||||||
// clear push subscription
|
// clear push subscription
|
||||||
user.pushSubscription = undefined
|
user.pushSubscription = undefined
|
||||||
|
|
|
@ -31,6 +31,8 @@ export const useIsMac = () => computed(() =>
|
||||||
useRequestHeaders(['user-agent'])['user-agent']?.includes('Macintosh')
|
useRequestHeaders(['user-agent'])['user-agent']?.includes('Macintosh')
|
||||||
?? navigator?.platform?.includes('Mac') ?? false)
|
?? navigator?.platform?.includes('Mac') ?? false)
|
||||||
|
|
||||||
|
export const isEmptyObject = (object: Object) => Object.keys(object).length === 0
|
||||||
|
|
||||||
export function removeHTMLTags(str: string) {
|
export function removeHTMLTags(str: string) {
|
||||||
return str.replaceAll(HTMLTagRE, '')
|
return str.replaceAll(HTMLTagRE, '')
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,4 +9,3 @@ async function run() {
|
||||||
}
|
}
|
||||||
|
|
||||||
run()
|
run()
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const wideLayout = computed(() => route.meta.wideLayout ?? false)
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div h-full :class="{ zen: isZenMode }">
|
<div h-full :class="{ zen: isZenMode }">
|
||||||
<div v-if="isHydrated.value && showUserSwitcherSidebar" fixed h-full hidden lg:block bg-code border-r-1 border-base>
|
<div v-if="isHydrated.value && showUserSwitcherSidebar" fixed h-full hidden lg:block bg-code border-r-1 border-base>
|
||||||
|
@ -39,7 +45,7 @@
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<div class="w-full sm:w-600px min-h-screen sm:border-l sm:border-r border-base">
|
<div class="w-full min-h-screen" :class="wideLayout ? 'lg:w-full sm:w-600px' : 'sm:w-600px'" sm:border-l sm:border-r border-base>
|
||||||
<div min-h="[calc(100vh-3.5rem)]" sm:min-h-screen>
|
<div min-h="[calc(100vh-3.5rem)]" sm:min-h-screen>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,7 +54,7 @@
|
||||||
<NavBottom v-if="isHydrated.value" />
|
<NavBottom v-if="isHydrated.value" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<aside class="hidden sm:none lg:block w-1/4 zen-hide">
|
<aside v-if="!wideLayout" class="hidden sm:none lg:block w-1/4 zen-hide">
|
||||||
<div sticky top-0 h-screen flex="~ col" py3>
|
<div sticky top-0 h-screen flex="~ col" py3>
|
||||||
<slot name="right">
|
<slot name="right">
|
||||||
<SearchWidget />
|
<SearchWidget />
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
"clear_upload_failed": "Clear file upload errors",
|
"clear_upload_failed": "Clear file upload errors",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"compose": "Compose",
|
"compose": "Compose",
|
||||||
|
"confirm": "Confirm",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"enter_app": "Enter App",
|
"enter_app": "Enter App",
|
||||||
"favourite": "Favourite",
|
"favourite": "Favourite",
|
||||||
|
@ -55,6 +56,7 @@
|
||||||
"prev": "Prev",
|
"prev": "Prev",
|
||||||
"publish": "Publish!",
|
"publish": "Publish!",
|
||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
|
"save": "Save",
|
||||||
"save_changes": "Save changes",
|
"save_changes": "Save changes",
|
||||||
"sign_in": "Sign in",
|
"sign_in": "Sign in",
|
||||||
"switch_account": "Switch account",
|
"switch_account": "Switch account",
|
||||||
|
@ -95,8 +97,10 @@
|
||||||
"error": {
|
"error": {
|
||||||
"account_not_found": "Account {0} not found",
|
"account_not_found": "Account {0} not found",
|
||||||
"explore-list-empty": "Nothing is trending right now. Check back later!",
|
"explore-list-empty": "Nothing is trending right now. Check back later!",
|
||||||
|
"file_size_cannot_exceed_n_mb": "File size cannot exceed {0}MB",
|
||||||
"sign_in_error": "Cannot connect to the server.",
|
"sign_in_error": "Cannot connect to the server.",
|
||||||
"status_not_found": "Post not found"
|
"status_not_found": "Post not found",
|
||||||
|
"unsupported_file_format": "Unsupported file format"
|
||||||
},
|
},
|
||||||
"feature_flag": {
|
"feature_flag": {
|
||||||
"avatar_on_avatar": "Avatar on Avatar",
|
"avatar_on_avatar": "Avatar on Avatar",
|
||||||
|
@ -153,7 +157,8 @@
|
||||||
"local": "Local",
|
"local": "Local",
|
||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"search": "Search {0}"
|
"search": "Search {0}",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"nav_user": {
|
"nav_user": {
|
||||||
"sign_in_desc": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server."
|
"sign_in_desc": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server."
|
||||||
|
@ -219,6 +224,26 @@
|
||||||
"search": {
|
"search": {
|
||||||
"search_desc": "Search for people & hashtags"
|
"search_desc": "Search for people & hashtags"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"preferences": {
|
||||||
|
"label": "Preferences"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"appearance": {
|
||||||
|
"bio": "Bio",
|
||||||
|
"description": "Edit avatar, username, profile, etc.",
|
||||||
|
"display_name": "Display name",
|
||||||
|
"label": "Appearance",
|
||||||
|
"title": "Edit profile"
|
||||||
|
},
|
||||||
|
"featured_tags": {
|
||||||
|
"description": "People can browse your public posts under these hashtags.",
|
||||||
|
"label": "Featured hashtags"
|
||||||
|
},
|
||||||
|
"label": "Profile"
|
||||||
|
},
|
||||||
|
"select_a_settings": "Select a settings"
|
||||||
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"edited": "(Edited)",
|
"edited": "(Edited)",
|
||||||
"editing": "Editing",
|
"editing": "Editing",
|
||||||
|
|
|
@ -37,6 +37,8 @@
|
||||||
"boosted": "已转发",
|
"boosted": "已转发",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"compose": "撰写",
|
"compose": "撰写",
|
||||||
|
"confirm": "确认",
|
||||||
|
"edit": "编辑",
|
||||||
"enter_app": "进入应用",
|
"enter_app": "进入应用",
|
||||||
"favourite": "喜欢",
|
"favourite": "喜欢",
|
||||||
"favourited": "已喜欢",
|
"favourited": "已喜欢",
|
||||||
|
@ -45,6 +47,7 @@
|
||||||
"prev": "上一个",
|
"prev": "上一个",
|
||||||
"publish": "发布!",
|
"publish": "发布!",
|
||||||
"reply": "回复",
|
"reply": "回复",
|
||||||
|
"save": "保存",
|
||||||
"save_changes": "保存更改",
|
"save_changes": "保存更改",
|
||||||
"sign_in": "登鹿",
|
"sign_in": "登鹿",
|
||||||
"switch_account": "切换帐号",
|
"switch_account": "切换帐号",
|
||||||
|
@ -81,8 +84,10 @@
|
||||||
"error": {
|
"error": {
|
||||||
"account_not_found": "未找到用户 {0}",
|
"account_not_found": "未找到用户 {0}",
|
||||||
"explore-list-empty": "目前没有热门话题,稍后再来看看吧!",
|
"explore-list-empty": "目前没有热门话题,稍后再来看看吧!",
|
||||||
|
"file_size_cannot_exceed_n_mb": "文件大小不能超过 {0}MB",
|
||||||
"sign_in_error": "无法连接服务器",
|
"sign_in_error": "无法连接服务器",
|
||||||
"status_not_found": "未找到帖文"
|
"status_not_found": "未找到帖文",
|
||||||
|
"unsupported_file_format": "不支持的文件格式"
|
||||||
},
|
},
|
||||||
"feature_flag": {
|
"feature_flag": {
|
||||||
"avatar_on_avatar": "头像堆叠",
|
"avatar_on_avatar": "头像堆叠",
|
||||||
|
@ -139,7 +144,8 @@
|
||||||
"local": "本地",
|
"local": "本地",
|
||||||
"notifications": "通知",
|
"notifications": "通知",
|
||||||
"profile": "个人资料",
|
"profile": "个人资料",
|
||||||
"search": "搜索 {0}"
|
"search": "搜索 {0}",
|
||||||
|
"settings": "设置"
|
||||||
},
|
},
|
||||||
"nav_user": {
|
"nav_user": {
|
||||||
"sign_in_desc": "登录后可关注其他人或标签、点赞、分享和回复帖文,或与不同服务器上的账号交互。"
|
"sign_in_desc": "登录后可关注其他人或标签、点赞、分享和回复帖文,或与不同服务器上的账号交互。"
|
||||||
|
@ -201,6 +207,26 @@
|
||||||
"search": {
|
"search": {
|
||||||
"search_desc": "搜索用户或话题标签"
|
"search_desc": "搜索用户或话题标签"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"preferences": {
|
||||||
|
"label": "首选项"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"appearance": {
|
||||||
|
"bio": "简介",
|
||||||
|
"description": "编辑个人资料,例如头像、用户名、个人简介等。",
|
||||||
|
"display_name": "昵称",
|
||||||
|
"label": "外观",
|
||||||
|
"title": "编辑个人资料"
|
||||||
|
},
|
||||||
|
"featured_tags": {
|
||||||
|
"description": "人们可以在这些标签下浏览你的公共嘟文。",
|
||||||
|
"label": "精选的话题标签"
|
||||||
|
},
|
||||||
|
"label": "个人资料"
|
||||||
|
},
|
||||||
|
"select_a_settings": "在左侧选择一个设置"
|
||||||
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"edited": "(已编辑)",
|
"edited": "(已编辑)",
|
||||||
"editing": "编辑中",
|
"editing": "编辑中",
|
||||||
|
|
|
@ -47,11 +47,13 @@
|
||||||
"pinia": "^2.0.28",
|
"pinia": "^2.0.28",
|
||||||
"shiki": "^0.12.1",
|
"shiki": "^0.12.1",
|
||||||
"shiki-es": "^0.1.2",
|
"shiki-es": "^0.1.2",
|
||||||
|
"slimeform": "^0.8.0",
|
||||||
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log",
|
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log",
|
||||||
"tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store",
|
"tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"ufo": "^1.0.1",
|
"ufo": "^1.0.1",
|
||||||
"ultrahtml": "^1.2.0",
|
"ultrahtml": "^1.2.0",
|
||||||
|
"vue-advanced-cropper": "^2.8.6",
|
||||||
"vue-virtual-scroller": "2.0.0-beta.7"
|
"vue-virtual-scroller": "2.0.0-beta.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
41
pages/settings.vue
Normal file
41
pages/settings.vue
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
definePageMeta({
|
||||||
|
wideLayout: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const isRootPath = computedEager(() => route.name === 'settings')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div min-h-screen flex>
|
||||||
|
<div border="x base" :class="isRootPath ? 'block lg:flex-none flex-1' : 'hidden lg:block'">
|
||||||
|
<MainContent>
|
||||||
|
<template #title>
|
||||||
|
<div text-lg font-bold flex items-center gap-2 @click="$scrollToTop">
|
||||||
|
<div i-ri:settings-4-line />
|
||||||
|
<span>{{ $t('nav_side.settings') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div xl:w-97 lg:w-78 w-full>
|
||||||
|
<SettingsNavItem
|
||||||
|
command
|
||||||
|
icon="i-ri:user-line"
|
||||||
|
:text="$t('settings.profile.label')"
|
||||||
|
to="/settings/profile"
|
||||||
|
/>
|
||||||
|
<SettingsNavItem
|
||||||
|
command
|
||||||
|
icon="i-ri:settings-2-line"
|
||||||
|
:text="$t('settings.preferences.label')"
|
||||||
|
to="/settings/preferences"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</MainContent>
|
||||||
|
</div>
|
||||||
|
<div flex-1 :class="isRootPath ? 'hidden lg:block' : 'block'">
|
||||||
|
<NuxtPage />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
8
pages/settings/index.vue
Normal file
8
pages/settings/index.vue
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<template>
|
||||||
|
<div min-h-screen flex justify-center items-center>
|
||||||
|
<div text-center flex="~ col gap-2" items-center>
|
||||||
|
<div i-ri:settings-4-line text-5xl />
|
||||||
|
<span text-xl>{{ $t('settings.select_a_settings') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
21
pages/settings/preferences/index.vue
Normal file
21
pages/settings/preferences/index.vue
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const { lg } = breakpoints
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MainContent :back="!lg">
|
||||||
|
<template #title>
|
||||||
|
<div text-lg font-bold flex items-center gap-2 @click="$scrollToTop">
|
||||||
|
<span>{{ $t('settings.preferences.label') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div text-center mt-10>
|
||||||
|
<h1 text-4xl>
|
||||||
|
🚧
|
||||||
|
</h1>
|
||||||
|
<h3 text-xl>
|
||||||
|
{{ $t('settings.preferences.label') }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</MainContent>
|
||||||
|
</template>
|
135
pages/settings/profile/appearance.vue
Normal file
135
pages/settings/profile/appearance.vue
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { invoke } from '@vueuse/shared'
|
||||||
|
import { useForm } from 'slimeform'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
// Keep alive the form page will reduce raw data timeliness and its status timeliness
|
||||||
|
keepalive: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const my = $computed(() => currentUser.value?.account)
|
||||||
|
|
||||||
|
watch($$(my), (value) => {
|
||||||
|
if (!value)
|
||||||
|
router.push('/')
|
||||||
|
})
|
||||||
|
|
||||||
|
const onlineSrc = $computed(() => ({
|
||||||
|
avatar: my?.avatar || '',
|
||||||
|
header: my?.header || '',
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { form, reset, submitter, dirtyFields, isError } = useForm({
|
||||||
|
form: () => ({
|
||||||
|
displayName: my?.displayName ?? '',
|
||||||
|
note: my?.source.note.replaceAll('\r', '') ?? '',
|
||||||
|
|
||||||
|
avatar: null as null | File,
|
||||||
|
header: null as null | File,
|
||||||
|
|
||||||
|
// These look more like account and privacy settings than appearance settings
|
||||||
|
// discoverable: false,
|
||||||
|
// bot: false,
|
||||||
|
// locked: false,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Keep the information to be edited up to date
|
||||||
|
invoke(async () => {
|
||||||
|
await pullMyAccountInfo()
|
||||||
|
reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
const isCanSubmit = computed(() => !isError.value && !isEmptyObject(dirtyFields.value))
|
||||||
|
|
||||||
|
const { submit, submitting } = submitter(async ({ dirtyFields }) => {
|
||||||
|
const res = await useMasto().accounts.updateCredentials(dirtyFields.value)
|
||||||
|
.then(account => ({ account }))
|
||||||
|
.catch((error: Error) => ({ error }))
|
||||||
|
|
||||||
|
if ('error' in res) {
|
||||||
|
// TODO: Show error message
|
||||||
|
console.error('Error(updateCredentials):', res.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setAccountInfo(my!.id, res.account)
|
||||||
|
reset()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MainContent back>
|
||||||
|
<template #title>
|
||||||
|
<div text-lg font-bold flex items-center gap-2 @click="$scrollToTop">
|
||||||
|
<span>{{ $t('settings.profile.appearance.title') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<form space-y-5 @submit.prevent="submit">
|
||||||
|
<div>
|
||||||
|
<!-- banner -->
|
||||||
|
<div of-hidden bg="gray-500/20" aspect="3">
|
||||||
|
<CommonInputImage
|
||||||
|
ref="elInputImage"
|
||||||
|
v-model="form.header"
|
||||||
|
:original="onlineSrc.header"
|
||||||
|
w-full h-full
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CommonCropImage v-model="form.header" :stencil-aspect-ratio="3 / 1" />
|
||||||
|
|
||||||
|
<!-- avatar -->
|
||||||
|
<div px-4>
|
||||||
|
<CommonInputImage
|
||||||
|
v-model="form.avatar"
|
||||||
|
:original="onlineSrc.avatar"
|
||||||
|
mt--10
|
||||||
|
rounded-full border="bg-base 4"
|
||||||
|
w="sm:30 24" min-w="sm:30 24" h="sm:30 24"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CommonCropImage v-model="form.avatar" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div px4 py3 space-y-5>
|
||||||
|
<!-- display name -->
|
||||||
|
<label space-y-2 block>
|
||||||
|
<p font-medium>
|
||||||
|
{{ $t('settings.profile.appearance.display_name') }}
|
||||||
|
</p>
|
||||||
|
<input v-model="form.displayName" type="text" input-base>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- note -->
|
||||||
|
<label space-y-2 block>
|
||||||
|
<p font-medium>
|
||||||
|
{{ $t('settings.profile.appearance.bio') }}
|
||||||
|
</p>
|
||||||
|
<textarea v-model="form.note" maxlength="500" min-h-10ex input-base />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- submit -->
|
||||||
|
<div text-right>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
btn-solid rounded-full text-sm
|
||||||
|
flex-inline gap-x-2 items-center
|
||||||
|
:disabled="submitting || !isCanSubmit"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
inline-block
|
||||||
|
:class="submitting ? 'i-ri:loader-2-fill animate animate-spin' : 'i-ri:save-line'"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ $t('action.save') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</MainContent>
|
||||||
|
</template>
|
18
pages/settings/profile/featured-tags.vue
Normal file
18
pages/settings/profile/featured-tags.vue
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<template>
|
||||||
|
<MainContent back>
|
||||||
|
<template #title>
|
||||||
|
<div text-lg font-bold flex items-center gap-2 @click="$scrollToTop">
|
||||||
|
<div i-ri:test-tube-line />
|
||||||
|
<span>{{ $t('settings.profile.featured_tags.label') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div text-center mt-10>
|
||||||
|
<h1 text-4xl>
|
||||||
|
🚧
|
||||||
|
</h1>
|
||||||
|
<h3 text-xl>
|
||||||
|
{{ $t('settings.profile.featured_tags.label') }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</MainContent>
|
||||||
|
</template>
|
34
pages/settings/profile/index.vue
Normal file
34
pages/settings/profile/index.vue
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const { lg } = breakpoints
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MainContent :back="!lg">
|
||||||
|
<template #title>
|
||||||
|
<div text-lg font-bold flex items-center gap-2 @click="$scrollToTop">
|
||||||
|
<span>{{ $t('settings.profile.label') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<SettingsNavItem
|
||||||
|
command
|
||||||
|
icon="i-ri:user-settings-line"
|
||||||
|
:text="$t('settings.profile.appearance.label')"
|
||||||
|
to="/settings/profile/appearance"
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
{{ $t('settings.profile.appearance.description') }}
|
||||||
|
</template>
|
||||||
|
</SettingsNavItem>
|
||||||
|
<SettingsNavItem
|
||||||
|
command
|
||||||
|
icon="i-ri:hashtag"
|
||||||
|
:text="$t('settings.profile.featured_tags.label')"
|
||||||
|
to="/settings/profile/featured-tags"
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
{{ $t('settings.profile.featured_tags.description') }}
|
||||||
|
</template>
|
||||||
|
</SettingsNavItem>
|
||||||
|
</MainContent>
|
||||||
|
</template>
|
|
@ -56,6 +56,7 @@ specifiers:
|
||||||
shiki: ^0.12.1
|
shiki: ^0.12.1
|
||||||
shiki-es: ^0.1.2
|
shiki-es: ^0.1.2
|
||||||
simple-git-hooks: ^2.8.1
|
simple-git-hooks: ^2.8.1
|
||||||
|
slimeform: ^0.8.0
|
||||||
std-env: ^3.3.1
|
std-env: ^3.3.1
|
||||||
tauri-plugin-log-api: github:tauri-apps/tauri-plugin-log
|
tauri-plugin-log-api: github:tauri-apps/tauri-plugin-log
|
||||||
tauri-plugin-store-api: github:tauri-apps/tauri-plugin-store
|
tauri-plugin-store-api: github:tauri-apps/tauri-plugin-store
|
||||||
|
@ -68,6 +69,7 @@ specifiers:
|
||||||
vite-plugin-inspect: ^0.7.11
|
vite-plugin-inspect: ^0.7.11
|
||||||
vite-plugin-pwa: ^0.13.3
|
vite-plugin-pwa: ^0.13.3
|
||||||
vitest: ^0.26.2
|
vitest: ^0.26.2
|
||||||
|
vue-advanced-cropper: ^2.8.6
|
||||||
vue-tsc: ^1.0.16
|
vue-tsc: ^1.0.16
|
||||||
vue-virtual-scroller: 2.0.0-beta.7
|
vue-virtual-scroller: 2.0.0-beta.7
|
||||||
workbox-window: ^6.5.4
|
workbox-window: ^6.5.4
|
||||||
|
@ -97,11 +99,13 @@ dependencies:
|
||||||
pinia: 2.0.28_typescript@4.9.4
|
pinia: 2.0.28_typescript@4.9.4
|
||||||
shiki: 0.12.1
|
shiki: 0.12.1
|
||||||
shiki-es: 0.1.2
|
shiki-es: 0.1.2
|
||||||
|
slimeform: 0.8.0
|
||||||
tauri-plugin-log-api: github.com/tauri-apps/tauri-plugin-log/b58475bbc410fa78eb69276c62d0b64c91c07914
|
tauri-plugin-log-api: github.com/tauri-apps/tauri-plugin-log/b58475bbc410fa78eb69276c62d0b64c91c07914
|
||||||
tauri-plugin-store-api: github.com/tauri-apps/tauri-plugin-store/9bd993aa67766596638bbfd91e79a1bf8f632014
|
tauri-plugin-store-api: github.com/tauri-apps/tauri-plugin-store/9bd993aa67766596638bbfd91e79a1bf8f632014
|
||||||
tippy.js: 6.3.7
|
tippy.js: 6.3.7
|
||||||
ufo: 1.0.1
|
ufo: 1.0.1
|
||||||
ultrahtml: 1.2.0
|
ultrahtml: 1.2.0
|
||||||
|
vue-advanced-cropper: 2.8.6
|
||||||
vue-virtual-scroller: 2.0.0-beta.7
|
vue-virtual-scroller: 2.0.0-beta.7
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
@ -4163,6 +4167,10 @@ packages:
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/classnames/2.3.2:
|
||||||
|
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/clean-regexp/1.0.0:
|
/clean-regexp/1.0.0:
|
||||||
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
|
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -4544,6 +4552,10 @@ packages:
|
||||||
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/debounce/1.2.1:
|
||||||
|
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/debug/2.6.9:
|
/debug/2.6.9:
|
||||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -4758,6 +4770,10 @@ packages:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/easy-bem/1.1.1:
|
||||||
|
resolution: {integrity: sha512-GJRqdiy2h+EXy6a8E6R+ubmqUM08BK0FWNq41k24fup6045biQ8NXxoXimiwegMQvFFV3t1emADdGNL1TlS61A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/ee-first/1.1.1:
|
/ee-first/1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -8807,6 +8823,12 @@ packages:
|
||||||
is-fullwidth-code-point: 4.0.0
|
is-fullwidth-code-point: 4.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/slimeform/0.8.0:
|
||||||
|
resolution: {integrity: sha512-oh0GY3qPkN1ouH3TQex/+SbVsgGmJhZvgz8NqfECuMuSy7k0NOQNUudH/bebcAY7fIk5nVunMS2GPfo4UWwmDw==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: '>=3'
|
||||||
|
dev: false
|
||||||
|
|
||||||
/snake-case/3.0.4:
|
/snake-case/3.0.4:
|
||||||
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
|
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -10013,6 +10035,17 @@ packages:
|
||||||
resolution: {integrity: sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ==}
|
resolution: {integrity: sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/vue-advanced-cropper/2.8.6:
|
||||||
|
resolution: {integrity: sha512-R1vkXG/Vam3OEd3vMJsVSJkXUc9ejM9l/NzPcPvkyzKGHwF69c2v1lh2Kqj2A5MCqrTmk76bmzmWFuYj+AcwmA==}
|
||||||
|
engines: {node: '>=8', npm: '>=5'}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.0.0
|
||||||
|
dependencies:
|
||||||
|
classnames: 2.3.2
|
||||||
|
debounce: 1.2.1
|
||||||
|
easy-bem: 1.1.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/vue-bundle-renderer/1.0.0:
|
/vue-bundle-renderer/1.0.0:
|
||||||
resolution: {integrity: sha512-43vCqTgaMXfHhtR8/VcxxWD1DgtzyvNc4wNyG5NKCIH19O1z5G9ZCRXTGEA2wifVec5PU82CkRLD2sTK9NkTdA==}
|
resolution: {integrity: sha512-43vCqTgaMXfHhtR8/VcxxWD1DgtzyvNc4wNyG5NKCIH19O1z5G9ZCRXTGEA2wifVec5PU82CkRLD2sTK9NkTdA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
--c-primary-active: #C16929;
|
--c-primary-active: #C16929;
|
||||||
--c-primary-light: #EA9E441A;
|
--c-primary-light: #EA9E441A;
|
||||||
--c-border: #eee;
|
--c-border: #eee;
|
||||||
|
--c-border-dark: #dccfcf;
|
||||||
|
|
||||||
--c-bg-base: #fff;
|
--c-bg-base: #fff;
|
||||||
--c-bg-active: #f6f6f6;
|
--c-bg-active: #f6f6f6;
|
||||||
|
@ -18,10 +19,15 @@
|
||||||
--c-bg-btn-disabled: #a1a1a1;
|
--c-bg-btn-disabled: #a1a1a1;
|
||||||
--c-text-btn-disabled: #fff;
|
--c-text-btn-disabled: #fff;
|
||||||
--c-text-btn: #232323;
|
--c-text-btn: #232323;
|
||||||
|
|
||||||
|
--c-success: #67C23A;
|
||||||
|
--c-warning: #E6A23C;
|
||||||
|
--c-error: #F56C6C;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--c-border: #222;
|
--c-border: #222;
|
||||||
|
--c-border-dark: #545251;
|
||||||
|
|
||||||
--c-bg-base: #111;
|
--c-bg-base: #111;
|
||||||
--c-bg-active: #191919;
|
--c-bg-active: #191919;
|
||||||
|
|
|
@ -13,6 +13,7 @@ export default defineConfig({
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
'border-base': 'border-$c-border',
|
'border-base': 'border-$c-border',
|
||||||
|
'border-dark': 'border-$c-border-dark',
|
||||||
'border-strong': 'border-$c-text-base',
|
'border-strong': 'border-$c-text-base',
|
||||||
'border-bg-base': 'border-$c-bg-base',
|
'border-bg-base': 'border-$c-bg-base',
|
||||||
'border-primary-light': 'border-$c-primary-light',
|
'border-primary-light': 'border-$c-primary-light',
|
||||||
|
@ -38,6 +39,12 @@ export default defineConfig({
|
||||||
'btn-text': 'btn-base px-4 py-2 text-$c-primary hover:text-$c-primary-active',
|
'btn-text': 'btn-base px-4 py-2 text-$c-primary hover:text-$c-primary-active',
|
||||||
'btn-action-icon': 'btn-base hover:bg-active rounded-full h9 w9 flex items-center justify-center',
|
'btn-action-icon': 'btn-base hover:bg-active rounded-full h9 w9 flex items-center justify-center',
|
||||||
|
|
||||||
|
// input
|
||||||
|
'input-base-focus': 'focus:outline-none focus:border-$c-primary',
|
||||||
|
'input-base-disabled': 'disabled:pointer-events-none disabled:bg-gray-500/5 disabled:text-gray-500/50',
|
||||||
|
'input-base': 'p2 rounded w-full bg-transparent border border-dark input-base-focus input-base-disabled',
|
||||||
|
'input-error': 'border-$c-error focus:(outline-offset-0 outline-$c-error outline-1px)',
|
||||||
|
|
||||||
// link
|
// link
|
||||||
'text-link-rounded': 'focus:outline-none focus:ring-(2 primary inset) hover:bg-active rounded md:rounded-full px2 mx--2',
|
'text-link-rounded': 'focus:outline-none focus:ring-(2 primary inset) hover:bg-active rounded md:rounded-full px2 mx--2',
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue