Skip to content

Commit 8275529

Browse files
authored
feat(stage-*): apply ripple grid to other pages (#751)
1 parent 3db6315 commit 8275529

File tree

11 files changed

+434
-175
lines changed

11 files changed

+434
-175
lines changed

apps/stage-web/src/pages/settings/system/index.vue

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
<script setup lang="ts">
2-
import { IconItem } from '@proj-airi/stage-ui/components'
2+
import { IconItem, RippleGrid } from '@proj-airi/stage-ui/components'
3+
import { useRippleGridState } from '@proj-airi/stage-ui/composables/use-ripple-grid-state'
34
import { computed } from 'vue'
45
import { useI18n } from 'vue-i18n'
56
67
const { t } = useI18n()
8+
const { lastClickedIndex, setLastClickedIndex } = useRippleGridState()
79
810
const settings = computed(() => [
911
{
@@ -31,21 +33,22 @@ const settings = computed(() => [
3133
<div flex="~ col gap-4" font-normal>
3234
<div />
3335
<div flex="~ col gap-4">
34-
<IconItem
35-
v-for="(setting, index) in settings"
36-
:key="setting.to"
37-
v-motion
38-
:initial="{ opacity: 0, y: 10 }"
39-
:enter="{ opacity: 1, y: 0 }"
40-
:duration="250"
41-
:style="{
42-
transitionDelay: `${index * 50}ms`, // delay between each item, unocss doesn't support dynamic generation of classes now
43-
}"
44-
:title="setting.title"
45-
:description="setting.description"
46-
:icon="setting.icon"
47-
:to="setting.to"
48-
/>
36+
<RippleGrid
37+
:items="settings"
38+
:get-key="item => item.to"
39+
:columns="1"
40+
:origin-index="lastClickedIndex"
41+
@item-click="({ globalIndex }) => setLastClickedIndex(globalIndex)"
42+
>
43+
<template #item="{ item }">
44+
<IconItem
45+
:title="item.title"
46+
:description="item.description"
47+
:icon="item.icon"
48+
:to="item.to"
49+
/>
50+
</template>
51+
</RippleGrid>
4952
</div>
5053
<div
5154
v-motion

packages/stage-pages/src/pages/settings/index.vue

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
2-
import { IconItem } from '@proj-airi/stage-ui/components'
2+
import { IconItem, RippleGrid } from '@proj-airi/stage-ui/components'
3+
import { useRippleGridState } from '@proj-airi/stage-ui/composables/use-ripple-grid-state'
34
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
45
import { computed, ref } from 'vue'
56
import { useI18n } from 'vue-i18n'
@@ -8,6 +9,7 @@ import { useRouter } from 'vue-router'
89
const router = useRouter()
910
const resolveAnimation = ref<() => void>()
1011
const { t } = useI18n()
12+
const { lastClickedIndex, setLastClickedIndex } = useRippleGridState()
1113
1214
const settingsStore = useSettings()
1315
@@ -72,23 +74,23 @@ const settings = computed(() => [
7274

7375
<template>
7476
<div flex="~ col gap-4" font-normal>
75-
<div />
76-
<div flex="~ col gap-4" pb-12>
77-
<IconItem
78-
v-for="(setting, index) in settings"
79-
:key="setting.to"
80-
v-motion
81-
:initial="{ opacity: 0, y: 10 }"
82-
:enter="{ opacity: 1, y: 0 }"
83-
:duration="250"
84-
:style="{
85-
transitionDelay: `${index * 50}ms`, // delay between each item, unocss doesn't support dynamic generation of classes now
86-
}"
87-
:title="setting.title"
88-
:description="setting.description"
89-
:icon="setting.icon"
90-
:to="setting.to"
91-
/>
77+
<div pb-12>
78+
<RippleGrid
79+
:items="settings"
80+
:get-key="item => item.to"
81+
:columns="1"
82+
:origin-index="lastClickedIndex"
83+
@item-click="({ globalIndex }) => setLastClickedIndex(globalIndex)"
84+
>
85+
<template #item="{ item }">
86+
<IconItem
87+
:title="item.title"
88+
:description="item.description"
89+
:icon="item.icon"
90+
:to="item.to"
91+
/>
92+
</template>
93+
</RippleGrid>
9294
</div>
9395
<div
9496
v-motion

packages/stage-pages/src/pages/settings/modules/index.vue

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
11
<script setup lang="ts">
2-
import { IconStatusItem } from '@proj-airi/stage-ui/components'
2+
import { IconStatusItem, RippleGrid } from '@proj-airi/stage-ui/components'
33
import { useModulesList } from '@proj-airi/stage-ui/composables/use-modules-list'
4+
import { useRippleGridState } from '@proj-airi/stage-ui/composables/use-ripple-grid-state'
45
56
const { modulesList } = useModulesList()
7+
const { lastClickedIndex, setLastClickedIndex } = useRippleGridState()
68
</script>
79

810
<template>
9-
<div grid="~ cols-1 sm:cols-2 gap-4">
10-
<IconStatusItem
11-
v-for="(module, index) of modulesList"
12-
:key="module.id"
13-
v-motion
14-
:initial="{ opacity: 0, y: 10 }"
15-
:enter="{ opacity: 1, y: 0 }"
16-
:duration="250 + index * 10"
17-
:delay="index * 50"
18-
:title="module.name"
19-
:description="module.description"
20-
:icon="module.icon"
21-
:icon-color="module.iconColor"
22-
:icon-image="module.iconImage"
23-
:to="module.to"
24-
:configured="module.configured"
25-
/>
11+
<div>
12+
<RippleGrid
13+
:items="modulesList"
14+
:columns="{ default: 1, sm: 2 }"
15+
:origin-index="lastClickedIndex"
16+
@item-click="({ globalIndex }) => setLastClickedIndex(globalIndex)"
17+
>
18+
<template #item="{ item: module }">
19+
<IconStatusItem
20+
:title="module.name"
21+
:description="module.description"
22+
:icon="module.icon"
23+
:icon-color="module.iconColor"
24+
:icon-image="module.iconImage"
25+
:to="module.to"
26+
:configured="module.configured"
27+
/>
28+
</template>
29+
</RippleGrid>
2630
</div>
2731
<div
2832
v-motion

packages/stage-pages/src/pages/settings/providers/index.vue

Lines changed: 35 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,23 @@
11
<script setup lang="ts">
2-
import type { ProviderMetadata } from '@proj-airi/stage-ui/stores/providers'
3-
import type { Ref } from 'vue'
4-
5-
import { IconStatusItem } from '@proj-airi/stage-ui/components'
2+
import { IconStatusItem, RippleGrid } from '@proj-airi/stage-ui/components'
3+
import { useRippleGridState } from '@proj-airi/stage-ui/composables/use-ripple-grid-state'
64
import { useScrollToHash } from '@proj-airi/stage-ui/composables/useScrollToHash'
75
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
8-
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
96
import { storeToRefs } from 'pinia'
107
import { computed } from 'vue'
11-
import { onBeforeRouteLeave, useRoute } from 'vue-router'
12-
13-
import { useProvidersPageStore } from './store'
14-
15-
interface ProviderBlock {
16-
id: string
17-
icon: string
18-
title: string
19-
description: string
20-
providersRef: Ref<ProviderMetadata[]>
21-
}
22-
23-
interface ProviderRenderData extends ProviderMetadata {
24-
renderIndex: number
25-
}
26-
27-
interface ComputedProviderBlock {
28-
id: string
29-
icon: string
30-
title: string
31-
description: string
32-
providers: ProviderRenderData[]
33-
}
8+
import { useRoute } from 'vue-router'
349
3510
const route = useRoute()
3611
const providersStore = useProvidersStore()
37-
const providersPageStore = useProvidersPageStore()
38-
const breakpoints = useBreakpoints(breakpointsTailwind)
12+
const { lastClickedIndex, setLastClickedIndex } = useRippleGridState()
3913
4014
const {
4115
allChatProvidersMetadata,
4216
allAudioSpeechProvidersMetadata,
4317
allAudioTranscriptionProvidersMetadata,
4418
} = storeToRefs(providersStore)
4519
46-
const { lastClickedProviderIndex } = storeToRefs(providersPageStore)
47-
48-
const providerBlocksConfig: ProviderBlock[] = [
20+
const providerBlocksConfig = [
4921
{
5022
id: 'chat',
5123
icon: 'i-solar:chat-square-like-bold-duotone',
@@ -69,7 +41,7 @@ const providerBlocksConfig: ProviderBlock[] = [
6941
},
7042
]
7143
72-
const providerBlocks = computed<ComputedProviderBlock[]>(() => {
44+
const providerBlocks = computed(() => {
7345
let globalIndex = 0
7446
return providerBlocksConfig.map(block => ({
7547
id: block.id,
@@ -83,48 +55,13 @@ const providerBlocks = computed<ComputedProviderBlock[]>(() => {
8355
}))
8456
})
8557
86-
const cols = computed(() => {
87-
if (breakpoints.greaterOrEqual('xl').value) {
88-
return 3
89-
}
90-
if (breakpoints.greaterOrEqual('sm').value) {
91-
return 2
92-
}
93-
return 1
94-
})
95-
96-
function getAnimationDelay(renderIndex: number) {
97-
const numCols = cols.value
98-
99-
const currentRow = Math.floor(renderIndex / numCols)
100-
const currentCol = renderIndex % numCols
101-
102-
const clickedRow = Math.floor(lastClickedProviderIndex.value / numCols)
103-
const clickedCol = lastClickedProviderIndex.value % numCols
104-
105-
// manhattan distance
106-
const distance = Math.abs(currentRow - clickedRow) + Math.abs(currentCol - clickedCol)
107-
108-
return distance * 80
109-
}
110-
111-
function handleProviderClick(renderIndex: number) {
112-
providersPageStore.setLastClickedProviderIndex(renderIndex)
113-
}
114-
11558
useScrollToHash(() => route.hash, {
11659
auto: true, // automatically react to route hash
11760
offset: 16, // header + margin spacing
11861
behavior: 'smooth', // smooth scroll animation
11962
maxRetries: 15, // retry if target element isn't ready
12063
retryDelay: 150, // wait between retries
12164
})
122-
123-
onBeforeRouteLeave((to, from) => {
124-
if (!to.path.startsWith('/settings/providers/')) {
125-
providersPageStore.resetLastClickedProviderIndex()
126-
}
127-
})
12865
</script>
12966

13067
<template>
@@ -145,47 +82,41 @@ onBeforeRouteLeave((to, from) => {
14582
</div>
14683
</div>
14784

148-
<template v-for="(block, blockIndex) in providerBlocks" :key="block.id">
149-
<div flex="~ row items-center gap-2" :class="{ 'my-5': blockIndex > 0 }">
150-
<div :id="block.id" :class="block.icon" text="neutral-500 dark:neutral-400 4xl" />
151-
<div>
85+
<RippleGrid
86+
:sections="providerBlocks"
87+
:get-items="block => block.providers"
88+
:columns="{ default: 1, sm: 2, xl: 3 }"
89+
:origin-index="lastClickedIndex"
90+
@item-click="({ globalIndex }) => setLastClickedIndex(globalIndex)"
91+
>
92+
<template #header="{ section: block }">
93+
<div flex="~ row items-center gap-2">
94+
<div :id="block.id" :class="block.icon" text="neutral-500 dark:neutral-400 4xl" />
15295
<div>
153-
<span text="neutral-300 dark:neutral-500 sm sm:base">{{ block.description }}</span>
154-
</div>
155-
<div flex text-nowrap text="2xl sm:3xl" font-normal>
15696
<div>
157-
{{ block.title }}
97+
<span text="neutral-300 dark:neutral-500 sm sm:base">{{ block.description }}</span>
98+
</div>
99+
<div flex text-nowrap text="2xl sm:3xl" font-normal>
100+
<div>
101+
{{ block.title }}
102+
</div>
158103
</div>
159104
</div>
160105
</div>
161-
</div>
162-
<div grid="~ cols-1 sm:cols-2 xl:cols-3 gap-4">
163-
<div
164-
v-for="provider of block.providers"
165-
:key="provider.id"
166-
v-motion
167-
:initial="{ opacity: 0, y: 10 }"
168-
:enter="{ opacity: 1,
169-
y: 0,
170-
transition: {
171-
duration: 250,
172-
delay: getAnimationDelay(provider.renderIndex),
173-
} }"
106+
</template>
107+
108+
<template #item="{ item: provider }">
109+
<IconStatusItem
110+
:title="provider.localizedName || 'Unknown'"
111+
:description="provider.localizedDescription"
112+
:icon="provider.icon"
113+
:icon-color="provider.iconColor"
114+
:icon-image="provider.iconImage"
174115
:to="`/settings/providers/${provider.category}/${provider.id}`"
175-
@click="handleProviderClick(provider.renderIndex)"
176-
>
177-
<IconStatusItem
178-
:title="provider.localizedName || 'Unknown'"
179-
:description="provider.localizedDescription"
180-
:icon="provider.icon"
181-
:icon-color="provider.iconColor"
182-
:icon-image="provider.iconImage"
183-
:to="`/settings/providers/${provider.category}/${provider.id}`"
184-
:configured="provider.configured"
185-
/>
186-
</div>
187-
</div>
188-
</template>
116+
:configured="provider.configured"
117+
/>
118+
</template>
119+
</RippleGrid>
189120
</div>
190121
<div
191122
v-motion

packages/stage-pages/src/pages/settings/providers/store.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

packages/stage-ui/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './data-pane'
22
export * from './gadgets'
33
export * from './graphics'
4+
export { default as RippleGrid } from './layout/ripple-grid/RippleGrid.vue'
45
export * from './layouts'
56
export * from './markdown'
67
export * from './menu'

0 commit comments

Comments
 (0)