Unify the web UI on shared controls and let pipeline columns be managed on the board.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
variant?: 'primary' | 'ghost' | 'dashed' | 'icon';
|
||||
type?: 'button' | 'submit';
|
||||
disabled?: boolean;
|
||||
block?: boolean;
|
||||
}>(),
|
||||
{ variant: 'primary', type: 'button' },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:type="type"
|
||||
class="ui-btn"
|
||||
:class="[variant, { block }]"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
min-height: var(--control-h);
|
||||
padding: 0 0.95rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius);
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
font-size: 0.88rem;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ui-btn.block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui-btn.ghost {
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
.ui-btn.dashed {
|
||||
background: #fff;
|
||||
border: 1.5px dashed currentColor;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ui-btn.icon {
|
||||
width: var(--control-h);
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
color: var(--accent-deep);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.ui-btn:focus-visible {
|
||||
outline: 2px solid var(--accent-soft);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div class="ui-card">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--pad-modal);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { use } from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { LineChart, PieChart } from 'echarts/charts';
|
||||
import {
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
TooltipComponent,
|
||||
} from 'echarts/components';
|
||||
import VChart from 'vue-echarts';
|
||||
import type { ComposeOption } from 'echarts/core';
|
||||
import type { LineSeriesOption, PieSeriesOption } from 'echarts/charts';
|
||||
import type {
|
||||
GridComponentOption,
|
||||
LegendComponentOption,
|
||||
TooltipComponentOption,
|
||||
} from 'echarts/components';
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
LineChart,
|
||||
PieChart,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
]);
|
||||
|
||||
export type UiChartOption = ComposeOption<
|
||||
| LineSeriesOption
|
||||
| PieSeriesOption
|
||||
| GridComponentOption
|
||||
| TooltipComponentOption
|
||||
| LegendComponentOption
|
||||
>;
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
option: UiChartOption;
|
||||
height?: string;
|
||||
}>(),
|
||||
{ height: '220px' },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VChart class="ui-chart" :option="option" :style="{ height }" autoresize />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-chart {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import UiButton from './UiButton.vue';
|
||||
import UiSearch from './UiSearch.vue';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{ placeholder: 'جستجو...' },
|
||||
);
|
||||
|
||||
const search = defineModel<string>('search', { default: '' });
|
||||
const open = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="filter-bar">
|
||||
<div class="row">
|
||||
<slot name="before" />
|
||||
<UiSearch v-model="search" :placeholder="placeholder" class="search" />
|
||||
<UiButton type="button" @click="open = !open">
|
||||
<UiIcon name="filter" :size="16" />
|
||||
فیلترها
|
||||
</UiButton>
|
||||
</div>
|
||||
<div v-if="open" class="panel">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter-bar {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search {
|
||||
flex: 1 1 10rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
||||
gap: 0.65rem;
|
||||
padding: var(--pad-modal);
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
type?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
maxlength?: number;
|
||||
minlength?: number;
|
||||
}>(),
|
||||
{ type: 'text' },
|
||||
);
|
||||
|
||||
const model = defineModel<string>({ default: '' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
v-model="model"
|
||||
class="ui-input"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:maxlength="maxlength"
|
||||
:minlength="minlength"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: var(--control-h);
|
||||
padding: 0 0.85rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.ui-input:focus {
|
||||
outline: 2px solid var(--accent-soft);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.ui-input:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { onUnmounted, watch } from 'vue';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
title: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
document.body.style.overflow = open ? 'hidden' : '';
|
||||
},
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
|
||||
function onBackdrop(event: MouseEvent) {
|
||||
if (event.target === event.currentTarget) emit('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="open" class="backdrop" @click="onBackdrop">
|
||||
<section
|
||||
class="dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
:aria-label="title"
|
||||
@click.stop
|
||||
>
|
||||
<header class="head">
|
||||
<h2>{{ title }}</h2>
|
||||
<button type="button" class="close" aria-label="بستن" @click="emit('close')">
|
||||
<UiIcon name="x" :size="18" />
|
||||
</button>
|
||||
</header>
|
||||
<div class="body">
|
||||
<slot />
|
||||
</div>
|
||||
<footer v-if="$slots.footer" class="foot">
|
||||
<slot name="footer" />
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 80;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 1.25rem;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
|
||||
.dialog {
|
||||
width: min(880px, 100%);
|
||||
max-height: min(88vh, 880px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.16);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.head,
|
||||
.body,
|
||||
.foot {
|
||||
padding: var(--pad-modal);
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.head h2 {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.close {
|
||||
width: var(--control-h);
|
||||
height: var(--control-h);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
color: var(--muted);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.body {
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.foot {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding-top: 0;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{ placeholder: 'جستجو...' },
|
||||
);
|
||||
|
||||
const model = defineModel<string>({ default: '' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="ui-search">
|
||||
<span class="sr">جستجو</span>
|
||||
<input
|
||||
v-model="model"
|
||||
type="search"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
<UiIcon name="search" :size="16" />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
min-height: var(--control-h);
|
||||
padding: 0 0.75rem;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.ui-search input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.ui-search:focus-within {
|
||||
outline: 2px solid var(--accent-soft);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.sr {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts" generic="T extends string">
|
||||
defineProps<{
|
||||
options: Array<{ value: T; label: string; icon?: 'grid' | 'list' }>;
|
||||
}>();
|
||||
|
||||
const model = defineModel<T>({ required: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui-seg" role="group">
|
||||
<button
|
||||
v-for="opt in options"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
class="seg"
|
||||
:class="{ on: model === opt.value }"
|
||||
:aria-pressed="model === opt.value"
|
||||
@click="model = opt.value"
|
||||
>
|
||||
<slot name="icon" :option="opt" />
|
||||
<span class="seg-label">{{ opt.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-seg {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--control-h);
|
||||
padding: 3px;
|
||||
gap: 2px;
|
||||
background: #e8eef1;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.seg {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
min-height: calc(var(--control-h) - 6px);
|
||||
padding: 0 0.75rem;
|
||||
border: 0;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
background: transparent;
|
||||
color: #7b8794;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.seg.on {
|
||||
background: #fff;
|
||||
color: var(--accent-deep);
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.seg:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const model = defineModel<string>({ default: '' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="ui-select">
|
||||
<select v-model="model" :disabled="disabled">
|
||||
<slot />
|
||||
</select>
|
||||
<UiIcon class="chev" name="chevron" :size="16" />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-select {
|
||||
position: relative;
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-select select {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
min-height: var(--control-h);
|
||||
padding: 0 2rem 0 0.85rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-select select:focus {
|
||||
outline: 2px solid var(--accent-soft);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.chev {
|
||||
position: absolute;
|
||||
left: 0.65rem;
|
||||
top: 50%;
|
||||
color: var(--muted);
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%) rotate(-90deg);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user