mirror of
https://github.com/fmhy/edit.git
synced 2026-03-11 08:55:38 +00:00
feat: improve feedback component, brand color picker
This commit is contained in:
parent
93ed69ddbf
commit
1622da5db9
8 changed files with 263 additions and 29 deletions
|
|
@ -29,8 +29,8 @@ onMounted(() => {
|
|||
>
|
||||
<ClientOnly>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<VPIconSun v-if="!isDark" class="sun" />
|
||||
<VPIconMoon v-else class="moon" />
|
||||
<div v-if="!isDark" class="sun text-xl i-ph-sun-duotone" />
|
||||
<div v-else class="moon text-xl i-ph-moon-duotone" />
|
||||
</Transition>
|
||||
</ClientOnly>
|
||||
</button>
|
||||
|
|
|
|||
88
docs/.vitepress/theme/components/ColorPicker.vue
Normal file
88
docs/.vitepress/theme/components/ColorPicker.vue
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<script setup lang="ts">
|
||||
import { colors } from '@fmhy/colors'
|
||||
import { useStorage, useStyleTag } from '@vueuse/core'
|
||||
import { watch } from 'vue'
|
||||
|
||||
const colorScales = [
|
||||
'50',
|
||||
'100',
|
||||
'200',
|
||||
'300',
|
||||
'400',
|
||||
'500',
|
||||
'600',
|
||||
'700',
|
||||
'800',
|
||||
'900',
|
||||
'950'
|
||||
] as const
|
||||
|
||||
type ColorNames = keyof typeof colors
|
||||
const selectedColor = useStorage<ColorNames>('preferred-color', 'swarm')
|
||||
|
||||
const colorOptions = Object.keys(colors).filter(
|
||||
(key) => typeof colors[key as keyof typeof colors] === 'object'
|
||||
) as Array<ColorNames>
|
||||
|
||||
const { css } = useStyleTag('', { id: 'brand-color' })
|
||||
|
||||
const updateThemeColor = (colorName: ColorNames) => {
|
||||
const colorSet = colors[colorName]
|
||||
|
||||
const cssVars = colorScales
|
||||
.map((scale) => `--vp-c-brand-${scale}: ${colorSet[scale]};`)
|
||||
.join('\n ')
|
||||
|
||||
css.value = `
|
||||
:root {
|
||||
${cssVars}
|
||||
--vp-c-brand-1: ${colorSet[500]};
|
||||
--vp-c-brand-2: ${colorSet[600]};
|
||||
--vp-c-brand-3: ${colorSet[800]};
|
||||
--vp-c-brand-soft: ${colorSet[400]};
|
||||
}
|
||||
|
||||
.dark {
|
||||
${cssVars}
|
||||
--vp-c-brand-1: ${colorSet[400]};
|
||||
--vp-c-brand-2: ${colorSet[500]};
|
||||
--vp-c-brand-3: ${colorSet[700]};
|
||||
--vp-c-brand-soft: ${colorSet[300]};
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
// Initialize theme color
|
||||
updateThemeColor(selectedColor.value)
|
||||
|
||||
watch(selectedColor, updateThemeColor)
|
||||
|
||||
const normalizeColorName = (colorName: string) =>
|
||||
colorName.replaceAll(/-/g, ' ').charAt(0).toUpperCase() +
|
||||
colorName.slice(1).replaceAll(/-/g, ' ')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div v-for="color in colorOptions" :key="color">
|
||||
<button
|
||||
:class="[
|
||||
'inline-block w-6 h-6 rounded-full transition-all duration-200'
|
||||
]"
|
||||
@click="selectedColor = color"
|
||||
:title="normalizeColorName(color)"
|
||||
>
|
||||
<span
|
||||
class="inline-block w-6 h-6 rounded-full"
|
||||
:style="{ backgroundColor: colors[color][500] }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-sm text-$vp-c-text-2">
|
||||
Selected: {{ normalizeColorName(selectedColor) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -69,10 +69,12 @@ const isDisabled = computed(() => {
|
|||
})
|
||||
|
||||
const router = useRouter()
|
||||
// prettier-ignore
|
||||
const feedback = reactive<
|
||||
Pick<FeedbackType, 'message' | 'page'> & Partial<Pick<FeedbackType, 'type'>>
|
||||
>({
|
||||
|
||||
const feedback = reactive<{
|
||||
message: string
|
||||
page: string
|
||||
type?: FeedbackType['type']
|
||||
}>({
|
||||
page: router.route.path,
|
||||
message: ''
|
||||
})
|
||||
|
|
@ -142,17 +144,45 @@ const toggleCard = () => (isCardShown.value = !isCardShown.value)
|
|||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
class="bg-$vp-c-default-soft text-primary px2 py1 border-$vp-c-default-soft hover:border-primary mt-2 select-none rounded border-2 border-solid font-bold transition-all duration-300"
|
||||
@click="toggleCard()"
|
||||
<div
|
||||
class="mt-2 p-4 border-2 border-solid bg-brand-50 border-brand-300 dark:bg-brand-950 dark:border-brand-800 rounded-xl col-span-3 transition-colors duration-250"
|
||||
>
|
||||
<span
|
||||
:class="
|
||||
isCardShown === false ? `i-lucide:mail mr-2` : `i-lucide:mail-x mr-2`
|
||||
"
|
||||
/>
|
||||
<span>Send Feedback</span>
|
||||
</button>
|
||||
<div class="flex items-start md:items-center gap-3">
|
||||
<div class="pt-1 md:pt-0">
|
||||
<div
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center bg-brand-500 dark:bg-brand-400"
|
||||
>
|
||||
<span
|
||||
:class="
|
||||
isCardShown === false
|
||||
? `i-lucide:mail w-6 h-6 text-white dark:text-brand-950`
|
||||
: `i-lucide:mail-x w-6 h-6 text-white dark:text-brand-950`
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex-grow flex items-start md:items-center gap-3 flex-col md:flex-row"
|
||||
>
|
||||
<div class="flex-grow">
|
||||
<div class="font-semibold text-brand-950 dark:text-brand-50">
|
||||
Got feedback?
|
||||
</div>
|
||||
<div class="text-sm text-brand-800 dark:text-brand-100">
|
||||
We'd love to know what you think about this page.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="inline-block text-center rounded-full px-4 py-2.5 text-sm font-medium border-2 border-solid text-brand-700 border-brand-300 dark:text-brand-100 dark:border-brand-800"
|
||||
@click="toggleCard()"
|
||||
>
|
||||
Share Feedback
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Transition name="fade" mode="out-in">
|
||||
|
|
@ -254,7 +284,7 @@ const toggleCard = () => (isCardShown.value = !isCardShown.value)
|
|||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="border border-div rounded-lg transition-colors duration-250 inline-block text-14px font-500 leading-1.5 px-3 py-3 text-center align-middle whitespace-nowrap disabled:opacity-50 text-text-2 bg-swarm-100 dark:bg-swarm-700 border-swarm-800 dark:border-swarm-700 disabled:bg-swarm-100 dark:disabled:bg-swarm-900 hover:border-swarm-900 dark:hover:border-swarm-800 hover:bg-swarm-200 dark:hover:bg-swarm-800"
|
||||
class="border border-div rounded-lg transition-colors duration-250 inline-block text-14px font-500 leading-1.5 px-3 py-3 text-center align-middle whitespace-nowrap disabled:opacity-50 text-text-2 bg-brand-100 dark:bg-brand-700 border-brand-800 dark:border-brand-700 disabled:bg-brand-100 dark:disabled:bg-brand-900 hover:border-brand-900 dark:hover:border-brand-800 hover:bg-brand-200 dark:hover:bg-brand-800"
|
||||
:disabled="isDisabled"
|
||||
@click="handleSubmit()"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import Field from './CardField.vue'
|
||||
import ColorPicker from './ColorPicker.vue'
|
||||
import InputField from './InputField.vue'
|
||||
import ToggleStarred from './ToggleStarred.vue'
|
||||
</script>
|
||||
|
|
@ -16,7 +17,7 @@ import ToggleStarred from './ToggleStarred.vue'
|
|||
<Field icon="i-twemoji-globe-with-meridians">Indexes</Field>
|
||||
<Field icon="i-twemoji-repeat-button">Storage Links</Field>
|
||||
<Field icon="i-twemoji-star">Recommendations</Field>
|
||||
<div class="align-center mb-4 flex justify-between">
|
||||
<div class="align-center mb-4 mt-4 flex justify-between">
|
||||
<div class="text-$vp-c-text-1 lh-relaxed text-sm font-bold">Options</div>
|
||||
</div>
|
||||
<InputField id="toggle-starred" label="Toggle Starred">
|
||||
|
|
@ -25,16 +26,6 @@ import ToggleStarred from './ToggleStarred.vue'
|
|||
</template>
|
||||
</InputField>
|
||||
|
||||
<Field icon="i-lucide:github">
|
||||
<a
|
||||
href="https://github.com/fmhy/edit"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Star FMHY on GitHub"
|
||||
class="text-primary underline font-bold"
|
||||
>
|
||||
Star on GitHub
|
||||
</a>
|
||||
</Field>
|
||||
<ColorPicker />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -257,6 +257,71 @@ #VPContent strong > a {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info.custom-block a {
|
||||
color: var(--vp-custom-block-info-text);
|
||||
font-weight: 500;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.info.custom-block a:hover {
|
||||
opacity: 0.7;
|
||||
color: var(--vp-custom-block-info-text-deep);
|
||||
}
|
||||
|
||||
.note.custom-block a {
|
||||
color: var(--vp-custom-block-info-text);
|
||||
font-weight: 500;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.note.custom-block a:hover {
|
||||
opacity: 0.7;
|
||||
color: var(--vp-custom-block-note-text-deep);
|
||||
}
|
||||
|
||||
.tip.custom-block a {
|
||||
color: var(--vp-custom-block-tip-text);
|
||||
font-weight: 500;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.tip.custom-block a:hover {
|
||||
opacity: 0.7;
|
||||
color: var(--vp-custom-block-tip-text-deep);
|
||||
}
|
||||
|
||||
.warning.custom-block a {
|
||||
color: var(--vp-custom-block-warning-text);
|
||||
font-weight: 500;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.warning.custom-block a:hover {
|
||||
opacity: 0.7;
|
||||
color: var(--vp-custom-block-warning-text-deep);
|
||||
}
|
||||
|
||||
.danger.custom-block a {
|
||||
color: var(--vp-custom-block-danger-text);
|
||||
font-weight: 500;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.danger.custom-block a:hover {
|
||||
opacity: 0.7;
|
||||
color: var(--vp-custom-block-danger-text-deep);
|
||||
}
|
||||
|
||||
.info.custom-block {
|
||||
--icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWluZm8iPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0iTTEyIDE2di00Ii8+PHBhdGggZD0iTTEyIDhoLjAxIi8+PC9zdmc+');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"docs:dev": "vitepress dev docs/",
|
||||
"docs:preview": "vitepress preview docs/",
|
||||
"format": "prettier -w --cache --check .",
|
||||
"licenser": "deno run --allow-read jsr:@kt3k/license-checker@3.3.1/main",
|
||||
"og:dev": "x-satori -t ./docs/.vitepress/hooks/Template.vue -c ./.vitepress/hooks/satoriConfig.ts --dev"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -31,6 +32,7 @@
|
|||
"nitropack": "^2.11.6",
|
||||
"nprogress": "^0.2.0",
|
||||
"pathe": "^2.0.1",
|
||||
"reka-ui": "^2.1.1",
|
||||
"unocss": "66.1.0-beta.3",
|
||||
"vitepress": "^1.6.3",
|
||||
"vue": "^3.5.13",
|
||||
|
|
@ -44,6 +46,7 @@
|
|||
"@iconify-json/heroicons-solid": "^1.2.0",
|
||||
"@iconify-json/lucide": "^1.2.10",
|
||||
"@iconify-json/mdi": "^1.2.1",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@iconify-json/simple-icons": "^1.2.12",
|
||||
"@iconify-json/twemoji": "^1.2.1",
|
||||
"@iconify/utils": "^2.3.0",
|
||||
|
|
|
|||
BIN
pnpm-lock.yaml
BIN
pnpm-lock.yaml
Binary file not shown.
|
|
@ -14,6 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Rule } from 'unocss'
|
||||
import { colors, shortcuts } from '@fmhy/colors'
|
||||
import {
|
||||
defineConfig,
|
||||
|
|
@ -23,6 +24,38 @@ import {
|
|||
transformerDirectives
|
||||
} from 'unocss'
|
||||
|
||||
const colorScales = [
|
||||
'50',
|
||||
'100',
|
||||
'200',
|
||||
'300',
|
||||
'400',
|
||||
'500',
|
||||
'600',
|
||||
'700',
|
||||
'800',
|
||||
'900',
|
||||
'950'
|
||||
] as const
|
||||
|
||||
const colorPattern = Object.keys(colors).join('|')
|
||||
const createColorRules = (type: 'text' | 'bg' | 'border'): Rule[] => {
|
||||
const property =
|
||||
type === 'text'
|
||||
? 'color'
|
||||
: type === 'bg'
|
||||
? 'background-color'
|
||||
: 'border-color'
|
||||
|
||||
return colorScales.map(
|
||||
(scale) =>
|
||||
[
|
||||
new RegExp(`^${type}-(${colorPattern})-${scale}$`),
|
||||
([, color]) => ({ [property]: colors[color][scale] })
|
||||
] as const
|
||||
)
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
content: {
|
||||
filesystem: ['.vitepress/config.mts', '.vitepress/constants.ts']
|
||||
|
|
@ -39,6 +72,30 @@ export default defineConfig({
|
|||
div: 'var(--vp-c-divider)'
|
||||
}
|
||||
},
|
||||
rules: [
|
||||
// Brand color utilities
|
||||
[
|
||||
/^brand-(\d+)$/,
|
||||
([, d]) => ({ color: `var(--vp-c-brand-${d})` })
|
||||
] as const,
|
||||
[
|
||||
/^bg-brand-(\d+)$/,
|
||||
([, d]) => ({ 'background-color': `var(--vp-c-brand-${d})` })
|
||||
] as const,
|
||||
[
|
||||
/^border-brand-(\d+)$/,
|
||||
([, d]) => ({ 'border-color': `var(--vp-c-brand-${d})` })
|
||||
] as const,
|
||||
[
|
||||
/^text-brand-(\d+)$/,
|
||||
([, d]) => ({ color: `var(--vp-c-brand-${d})` })
|
||||
] as const,
|
||||
|
||||
// Color scale utilities
|
||||
...createColorRules('text'),
|
||||
...createColorRules('bg'),
|
||||
...createColorRules('border')
|
||||
],
|
||||
presets: [
|
||||
presetUno(),
|
||||
presetAttributify(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue