Skip to content

Commit c80c8dc

Browse files
committed
Update the demo to use resizable sections and demo page components
1 parent 9f70ce7 commit c80c8dc

10 files changed

+718
-24
lines changed

demo/components/ComponentPage.vue

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<template>
2+
<div class="component-page">
3+
<!-- make sticky header -->
4+
<div class="component-page__heading">
5+
<HashLink>{{ title }}</HashLink>
6+
</div>
7+
8+
<template v-if="description || slots.description">
9+
<div class="component-page__description">
10+
<slot name="description">
11+
{{ description }}
12+
</slot>
13+
</div>
14+
</template>
15+
16+
<template v-if="validDemosArray.length">
17+
<div class="component-page__demos">
18+
<template v-for="demo in validDemosArray" :key="demo.title">
19+
<div class="component-page__demo">
20+
<template v-if="demo.title !== 'default'">
21+
<div class="component-page__hash-link">
22+
<HashLink :hash="demo.slotKey">
23+
{{ demo.title }}
24+
</HashLink>
25+
</div>
26+
</template>
27+
28+
<template v-if="demo.description">
29+
<div class="component-page__demo-description">
30+
{{ demo.description }}
31+
</div>
32+
</template>
33+
34+
<ResizableSection>
35+
<slot :name="demo.slotKey" />
36+
</ResizableSection>
37+
</div>
38+
</template>
39+
</div>
40+
</template>
41+
</div>
42+
</template>
43+
44+
<script lang="ts" setup>
45+
import { asArray, kebabCase } from '@prefecthq/prefect-design'
46+
import { computed, useSlots } from 'vue'
47+
import HashLink from '@/demo/components/HashLink.vue'
48+
import ResizableSection from '@/demo/components/ResizableSection.vue'
49+
50+
type DemoSection = {
51+
title: string,
52+
description?: string,
53+
}
54+
55+
const props = defineProps<{
56+
title: string,
57+
description?: string,
58+
demos?: DemoSection | DemoSection[],
59+
}>()
60+
61+
const slots = useSlots()
62+
63+
const validDemosArray = computed(() => {
64+
const demos = []
65+
66+
if (slots.default) {
67+
demos.push({ title: 'default', slotKey: 'default' })
68+
}
69+
70+
if (props.demos) {
71+
demos.push(...asArray(props.demos))
72+
}
73+
74+
return asArray(demos)
75+
.map(demo => ({
76+
...demo,
77+
slotKey: kebabCase(demo.title),
78+
}))
79+
.filter(({ slotKey }) => slotKey in slots)
80+
})
81+
</script>
82+
83+
<style>
84+
.component-page { @apply
85+
max-w-full
86+
}
87+
88+
.component-page__heading { @apply
89+
flex
90+
justify-between
91+
text-sm
92+
}
93+
94+
.component-page__heading .hash-link { @apply
95+
text-2xl
96+
}
97+
98+
.component-page__description .hash-link,
99+
.component-page__demos .hash-link { @apply
100+
text-lg
101+
}
102+
103+
.component-page__demo .hash-link { @apply
104+
text-base
105+
}
106+
107+
.component-page__demos { @apply
108+
max-w-full
109+
py-4
110+
}
111+
112+
.component-page__description,
113+
.component-page__demos,
114+
.component-page__demo { @apply
115+
mt-4
116+
mb-8
117+
}
118+
119+
.component-page__description,
120+
.component-page__demo-description { @apply
121+
text-foreground-300
122+
my-2
123+
text-sm
124+
}
125+
</style>

demo/components/HashLink.vue

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<template>
2+
<span :id="hash" class="hash-link" @click="goToRoute">
3+
<span class="hash-link__hash">
4+
#
5+
</span>
6+
<span class="hash-link__text">
7+
<slot />
8+
</span>
9+
</span>
10+
</template>
11+
12+
<script lang="ts" setup>
13+
import { useRouter } from 'vue-router'
14+
15+
const props = defineProps<{
16+
hash?: string,
17+
}>()
18+
19+
const router = useRouter()
20+
21+
function goToRoute(): void {
22+
const hash = props.hash ? `#${props.hash}` : undefined
23+
24+
router.push({ hash })
25+
}
26+
</script>
27+
28+
<style>
29+
.hash-link { @apply
30+
cursor-pointer
31+
}
32+
33+
.hash-link__hash { @apply
34+
opacity-0
35+
translate-x-2
36+
absolute
37+
text-prefect-600
38+
transition-all
39+
}
40+
41+
.hash-link__text { @apply
42+
inline-block
43+
transition-transform
44+
}
45+
46+
.hash-link:hover .hash-link__text { @apply
47+
translate-x-4
48+
}
49+
50+
.hash-link:hover .hash-link__hash { @apply
51+
opacity-100
52+
translate-x-0
53+
}
54+
</style>

demo/components/ResizableSection.vue

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<template>
2+
<div ref="container" class="resizable-section">
3+
<p-frame v-if="show" :style="styles.iframe" :class="classes.iframe" :body-class="classes.content">
4+
<div class="resizable-section__content">
5+
<slot />
6+
</div>
7+
</p-frame>
8+
9+
<div class="resizable-section__aside">
10+
<div class="resizable-section__handle" @mousedown="start" @mouseup="stop">
11+
<component :is="ResizeIcon" />
12+
</div>
13+
14+
<transition name="fade">
15+
<div v-if="dragging" class="resizable-section__px">
16+
{{ contentWidth }}px
17+
</div>
18+
</transition>
19+
</div>
20+
</div>
21+
</template>
22+
23+
<script lang="ts" setup>
24+
import { toPixels, useColorTheme } from '@prefecthq/prefect-design'
25+
import { computed, ref, nextTick } from 'vue'
26+
import ResizeIcon from '@/demo/components/ResizeIcon.svg'
27+
28+
const container = ref<HTMLDivElement>()
29+
const dragging = ref(false)
30+
const contentWidth = ref<number>()
31+
const minWidth = 200
32+
const handleWidth = 24
33+
const handleWidthPx = `${handleWidth}px`
34+
35+
const { value: colorTheme } = useColorTheme()
36+
const show = ref(true)
37+
38+
const classes = computed(() => ({
39+
content: {
40+
'dark': colorTheme.value === 'dark',
41+
'light': colorTheme.value === 'light',
42+
'bg-background-700': colorTheme.value === 'light',
43+
'bg-background-400': colorTheme.value === 'dark',
44+
},
45+
iframe: {
46+
'pointer-events-none': dragging.value,
47+
},
48+
}))
49+
50+
const styles = computed(() => ({
51+
iframe: {
52+
'width': contentWidth.value ? toPixels(contentWidth.value) : '100%',
53+
},
54+
}))
55+
56+
const start = (): void => {
57+
dragging.value = true
58+
window.addEventListener('mouseup', stop)
59+
window.addEventListener('mousemove', drag)
60+
}
61+
62+
const stop = (): void => {
63+
dragging.value = false
64+
window.removeEventListener('mousemove', drag)
65+
window.removeEventListener('mouseup', stop)
66+
}
67+
68+
const drag = (event: MouseEvent): void => {
69+
const { offsetLeft, offsetWidth } = container.value!
70+
const positionX = event.clientX - offsetLeft
71+
72+
contentWidth.value = Math.min(Math.max(positionX, minWidth), offsetWidth - handleWidth)
73+
}
74+
75+
// This code block allows the component to take advantage of HMR
76+
if (import.meta.hot) {
77+
const { hot } = import.meta
78+
79+
hot.on('vite:afterUpdate', async () => {
80+
show.value = false
81+
await nextTick()
82+
show.value = true
83+
})
84+
}
85+
</script>
86+
87+
<style>
88+
.resizable-section { @apply
89+
w-full
90+
flex
91+
relative
92+
border
93+
overflow-hidden
94+
bg-transparent
95+
rounded
96+
dark:border-background-600
97+
}
98+
99+
.resizable-section__content { @apply
100+
relative
101+
order-first
102+
max-w-full
103+
min-w-[200px]
104+
p-4
105+
text-foreground
106+
}
107+
108+
.resizable-section__aside { @apply
109+
bg-background-500
110+
grow
111+
}
112+
113+
.resizable-section__handle { @apply
114+
bg-background-600
115+
text-foreground-300
116+
w-[v-bind(handleWidthPx)]
117+
h-full
118+
flex
119+
justify-center
120+
items-center
121+
cursor-ew-resize;
122+
filter: drop-shadow(0 0 0.15rem rgba(0, 0, 0, 0.2));
123+
}
124+
125+
.resizable-section__px { @apply
126+
select-none
127+
bg-opacity-70
128+
bg-background
129+
text-foreground
130+
rounded
131+
px-2
132+
py-0.5
133+
absolute
134+
bottom-1
135+
left-1;
136+
}
137+
138+
.fade-enter-active,
139+
.fade-leave-active {
140+
transition: opacity 0.5s ease;
141+
}
142+
143+
.fade-enter-from,
144+
.fade-leave-to {
145+
opacity: 0;
146+
}
147+
</style>

demo/components/ResizeIcon.svg

+9
Loading

demo/views/HistogramChartSection.vue

+27-17
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,39 @@
11
<template>
2-
<p-layout-default>
3-
<p-content class="histogram-chart-section">
4-
<div class="histogram-chart-section__controls">
5-
<p-label label="Buckets">
6-
<div class="flex gap-2">
7-
<p-number-input v-model="buckets" />
8-
<p-button primary icon="RefreshIcon" @click="getData" />
9-
</div>
10-
</p-label>
11-
</div>
12-
<div class="flex gap-2">
13-
<p-checkbox v-model="smooth" label="Smooth" />
14-
<p-checkbox v-model="showXAxis" label="Show X Axis" />
15-
<p-checkbox v-model="showYAxis" label="Show Y Axis" />
16-
</div>
2+
<ComponentPage
3+
title="Histogram Chart"
4+
:demos="[{ title: 'Mini Map' }]"
5+
>
6+
<template #description>
7+
<p-content class="histogram-chart-section">
8+
<div class="histogram-chart-section__controls">
9+
<p-label label="Buckets">
10+
<div class="flex gap-2">
11+
<p-number-input v-model="buckets" />
12+
<p-button primary icon="RefreshIcon" @click="getData" />
13+
</div>
14+
</p-label>
15+
</div>
16+
<div class="flex gap-2">
17+
<p-checkbox v-model="smooth" label="Smooth" />
18+
<p-checkbox v-model="showXAxis" label="Show X Axis" />
19+
<p-checkbox v-model="showYAxis" label="Show Y Axis" />
20+
</div>
21+
</p-content>
22+
</template>
1723

24+
<HistogramChart class="h-60" :data="data" :smooth="smooth" :options="{ showXAxis, showYAxis }" />
25+
26+
<template #mini-map>
1827
<HistogramChart :data="data" :smooth="smooth" :options="{ showXAxis, showYAxis }" />
19-
</p-content>
20-
</p-layout-default>
28+
</template>
29+
</ComponentPage>
2130
</template>
2231

2332
<script lang="ts" setup>
2433
import { BooleanRouteParam, NumberRouteParam, useRouteQueryParam } from '@prefecthq/vue-compositions'
2534
import { endOfWeek, startOfWeek } from 'date-fns'
2635
import { ref, watch } from 'vue'
36+
import ComponentPage from '../components/ComponentPage.vue'
2737
import { generateBarChartData } from '../data'
2838
import { HistogramChart, HistogramData } from '@/components/HistogramChart'
2939

0 commit comments

Comments
 (0)