Skip to content

Commit

Permalink
Registry update of tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
Robbe95 committed Jan 2, 2024
1 parent fc575e1 commit 9660269
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 20 deletions.
66 changes: 66 additions & 0 deletions public/api/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,72 @@
"AppText"
]
},
{
"component": "AppTabs",
"name": "Tabs",
"files": [
{
"name": "AppTabs.vue",
"dir": "./src/components/tabs",
"placementDir": "tabs",
"type": "components",
"content": "<script setup lang=\"ts\" generic=\"TRouter extends true | false | undefined = false\">\nimport { computed, watch } from 'vue'\nimport type { RouteLocationRaw } from 'vue-router'\nimport { RouterLink, RouterView, useRoute } from 'vue-router'\nimport AppIcon from '@/components/icon/AppIcon.vue'\nimport AppTabsGroup from '@/components/tabs/AppTabsGroup.vue'\nimport AppTabsList from '@/components/tabs/AppTabsList.vue'\nimport AppTabsTab from '@/components/tabs/AppTabsTab.vue'\nimport type { Icon } from '@/icons'\nimport AppTabsPanels from '@/components/tabs/AppTabsPanels.vue'\nimport { useTabQuery } from '@/composables/tabs/useTabsQuery'\n\nexport interface TabWithRoutes {\n label: string\n icon?: Icon\n to: RouteLocationRaw\n}\n\nexport interface TabWithoutRoutes {\n label: string\n icon?: Icon\n to?: never\n}\n\ninterface Props<THasRoutes> {\n /**\n * If true, the tabs will use the router to determine which tab is active, to with a route is required.\n * If false, the tabs will use query params to determine which tab is active.\n * If undefined, the tabs will use query params if the router is not available.\n */\n isRouter?: THasRoutes\n /**\n * The tabs to display.\n * If using the router, the tabs must have a `to` property.\n * If not using the router, the tabs must not have a `to` property.\n */\n tabs: THasRoutes extends true ? TabWithRoutes[] : TabWithoutRoutes[]\n /**\n * The name of the tab query param, in case you use multiple tabs on the page.\n */\n tabId?: string\n}\n\nconst { isRouter = false, tabs, tabId = 'default' } = defineProps<Props<TRouter>>()\n\nconst selectedTab = defineModel<number>({\n local: true,\n default: 0,\n})\n\n// Need this or it errors because of bad generics\nconst allTabs = computed(() => {\n return tabs\n})\n\nconst isUsingRouter = computed<boolean>(() => {\n // @ts-expect-error Generics in Vue SFCs are not supported yet\n return !!isRouter || isRouter === ''\n})\n\nfunction changeTab(index: number): void {\n selectedTab.value = index\n}\n\nfunction isActive(index: number): boolean {\n return selectedTab.value === index\n}\n\nconst tabComponent = computed(() => {\n return isUsingRouter.value ? RouterLink : 'button'\n})\n\n// Router logic\nconst route = useRoute()\nwatch(() => route.path, () => {\n if (!isUsingRouter.value)\n return\n\n const tab = (allTabs.value as TabWithRoutes[]).find((tab) => {\n return route.matched.find((route) => {\n return tab.to === route.path\n })\n })\n if (tab)\n selectedTab.value = (allTabs.value as TabWithRoutes[]).indexOf(tab)\n})\n\n// Tab logic in query params\nif (!isUsingRouter.value)\n useTabQuery({ selectedTab, tabId })\n</script>\n\n<template>\n <AppTabsGroup :selected-index=\"selectedTab\" as=\"div\" @change=\"changeTab\">\n <AppTabsList>\n <AppTabsTab\n :is=\"tabComponent\"\n v-for=\"(tab, index) in allTabs\"\n :key=\"index\"\n :to=\"(tab as TabWithRoutes)?.to ?? undefined\"\n :is-active=\"isActive(index)\"\n >\n <div class=\"flex w-full min-w-max items-center justify-center space-x-2 text-center\">\n <span class=\"flex-none\">{{ tab.label }}</span>\n <AppIcon v-if=\"tab.icon\" class=\"flex-none\" :icon=\"tab.icon\" />\n </div>\n </AppTabsTab>\n </AppTabsList>\n <AppTabsPanels>\n <slot v-if=\"!isUsingRouter\" />\n <RouterView v-else />\n </AppTabsPanels>\n </AppTabsGroup>\n</template>\n"
},
{
"name": "AppTabsGroup.vue",
"dir": "./src/components/tabs",
"placementDir": "tabs",
"type": "components",
"content": "<script setup lang=\"ts\">\nimport { TabGroup } from '@headlessui/vue'\n\ndefineSlots<{\n /**\n * Slot inside of TabGroup, should contain all Tab logic.\n */\n default: void\n}>()\n</script>\n\n<template>\n <TabGroup>\n <slot />\n </TabGroup>\n</template>\n"
},
{
"name": "AppTabsList.vue",
"dir": "./src/components/tabs",
"placementDir": "tabs",
"type": "components",
"content": "<script setup lang=\"ts\">\nimport { TabList } from '@headlessui/vue'\n\ndefineSlots<{\n /**\n * Slot inside of TabList, should contain Tab elements.\n */\n default: void\n}>()\n</script>\n\n<template>\n <TabList class=\"flex w-full flex-wrap justify-stretch\">\n <slot />\n </TabList>\n</template>\n"
},
{
"name": "AppTabsPanel.vue",
"dir": "./src/components/tabs",
"placementDir": "tabs",
"type": "components",
"content": "<script setup lang=\"ts\">\nimport { TabPanel } from '@headlessui/vue'\n\ndefineSlots<{\n /**\n * Slot inside of TabPanel, should contain panel content.\n */\n default: void\n}>()\n</script>\n\n<template>\n <TabPanel class=\"text-foreground\">\n <slot />\n </TabPanel>\n</template>\n"
},
{
"name": "AppTabsPanels.vue",
"dir": "./src/components/tabs",
"placementDir": "tabs",
"type": "components",
"content": "<script setup lang=\"ts\">\nimport { TabPanels } from '@headlessui/vue'\n\ndefineSlots<{\n /**\n * Slot inside of TabPanel, should contain TabPanels.\n */\n default: void\n}>()\n</script>\n\n<template>\n <TabPanels>\n <slot />\n </TabPanels>\n</template>\n"
},
{
"name": "AppTabsTab.vue",
"dir": "./src/components/tabs",
"placementDir": "tabs",
"type": "components",
"content": "<script setup lang=\"ts\">\nimport { Tab } from '@headlessui/vue'\nimport { computed } from 'vue'\nimport type { RouteLocationRaw } from 'vue-router'\nimport { RouterLink } from 'vue-router'\nimport { tabVariants } from '@/components/tabs/appTabs.style'\nimport { fadeTransition } from '@/transitions'\n\nconst { to } = defineProps<{\n /**\n * Whether the tab is active.\n */\n isActive: boolean\n /**\n * Route name to link to.\n */\n to?: RouteLocationRaw\n\n}>()\ndefineSlots<{\n /**\n * Slot inside of Tab, should contain Tab.\n */\n default: void\n}>()\n\nconst isRouter = computed(() => !!to)\n</script>\n\n<template>\n <Component\n :is=\"isRouter ? RouterLink : Tab\"\n :to=\"to\"\n class=\"relative flex flex-1\"\n :class=\"tabVariants()\"\n >\n <slot />\n <Transition v-bind=\"fadeTransition\">\n <div v-if=\"isActive\" class=\"absolute bottom-0 left-0 h-0.5 w-full translate-y-px bg-primary\" />\n </Transition>\n </Component>\n</template>\n"
},
{
"name": "appTabs.style.ts",
"dir": "./src/components/tabs",
"placementDir": "tabs",
"type": "components",
"content": "import type { VariantProps } from '@/utils/tailwind/cva'\nimport { cva } from '@/utils/tailwind/cva'\n\nexport const tabVariants = cva(\n {\n base: 'rounded-t-button border border-transparent border-b-neutral-200 px-4 py-2 text-foreground outline-none duration-100 first-letter:rounded-t-button focus-visible:border-primary',\n variants: {\n },\n },\n)\n\nexport type TabProps = VariantProps<typeof tabVariants>\n"
},
{
"name": "useTabsQuery.ts",
"dir": "./src/composables/tabs",
"placementDir": "tabs",
"type": "composables",
"content": "import type { Ref } from 'vue'\nimport { watch } from 'vue'\nimport { useRouteQuery } from '@vueuse/router'\n\nexport interface TabQueryParams {\n tabId?: string | null\n selectedTab: Ref<number | null>\n}\n\nexport function useTabQuery({ tabId = null, selectedTab }: TabQueryParams): void {\n const query = useRouteQuery<string>(`tab-${tabId ?? 'default'}`)\n let isInitialized = false\n if (query !== null)\n selectedTab.value = Number(query.value)\n\n const setTabQuery = (value: number | null): void => {\n query.value = value?.toString() ?? ''\n }\n\n watch(query, (value) => {\n if (value === null)\n return\n isInitialized = true\n selectedTab.value = Number(value)\n }, { immediate: true })\n\n watch(selectedTab, (value) => {\n if (!isInitialized)\n return\n setTabQuery(value)\n })\n}\n"
}
],
"internalDependencies": [],
"dependencies": [
"@vueuse/router"
]
},
{
"component": "AppText",
"name": "Text",
Expand Down
4 changes: 1 addition & 3 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
import { computed, ref } from 'vue'
import type { TabWithRoutes, TabWithoutRoutes } from '@/components/tabs/AppTabs.vue'
import type { TabWithoutRoutes } from '@/components/tabs/AppTabs.vue'
import AppTabs from '@/components/tabs/AppTabs.vue'
import type { Icon } from '@/icons'
import AppTabsPanel from '@/components/tabs/AppTabsPanel.vue'
const selectedTab = ref(2)
Expand Down
3 changes: 1 addition & 2 deletions src/components/tabs/AppTabs.story.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { TabWithRoutes, TabWithoutRoutes } from '@/components/tabs/AppTabs.vue'
import type { TabWithRoutes } from '@/components/tabs/AppTabs.vue'
import AppTabs from '@/components/tabs/AppTabs.vue'
import type { Icon } from '@/icons'
import AppTabsPanel from '@/components/tabs/AppTabsPanel.vue'
const selectedTab = ref(2)
Expand Down
43 changes: 29 additions & 14 deletions src/components/tabs/AppTabs.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<script setup lang="ts" generic="TRouter extends true | false | undefined = false">
import { computed, watch } from 'vue'
import type { RouteLocationRaw, RouteRecordRaw } from 'vue-router'
import type { RouteLocationRaw } from 'vue-router'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import AppIcon from '@/components/icon/AppIcon.vue'
import AppTabsGroup from '@/components/tabs/AppTabsGroup.vue'
import AppTabsList from '@/components/tabs/AppTabsList.vue'
import AppTabsTab from '@/components/tabs/AppTabsTab.vue'
import type { Icon } from '@/icons'
import AppTabsPanels from '@/components/tabs/AppTabsPanels.vue'
import { useTabQuery } from '@/composables/tab/useTabQuery'
import { useTabQuery } from '@/composables/tabs/useTabsQuery'
export interface TabWithRoutes {
label: string
Expand All @@ -22,21 +22,40 @@ export interface TabWithoutRoutes {
to?: never
}
const { isRouter = false, tabs } = defineProps<Props<TRouter>>()
interface Props<THasRoutes> {
/**
* If true, the tabs will use the router to determine which tab is active, to with a route is required.
* If false, the tabs will use query params to determine which tab is active.
* If undefined, the tabs will use query params if the router is not available.
*/
isRouter?: THasRoutes
/**
* The tabs to display.
* If using the router, the tabs must have a `to` property.
* If not using the router, the tabs must not have a `to` property.
*/
tabs: THasRoutes extends true ? TabWithRoutes[] : TabWithoutRoutes[]
/**
* The name of the tab query param, in case you use multiple tabs on the page.
*/
tabId?: string
}
const { isRouter = false, tabs, tabId = 'default' } = defineProps<Props<TRouter>>()
const selectedTab = defineModel<number>({
local: true,
default: 0,
})
// Need this or it errors because of bad generics
const allTabs = computed(() => {
return tabs
})
const selectedTab = defineModel<number>({
local: true,
default: 0,
const isUsingRouter = computed<boolean>(() => {
// @ts-expect-error Generics in Vue SFCs are not supported yet
return !!isRouter || isRouter === ''
})
function changeTab(index: number): void {
Expand All @@ -48,12 +67,7 @@ function isActive(index: number): boolean {
}
const tabComponent = computed(() => {
return isRouter ? RouterLink : 'button'
})
const isUsingRouter = computed<boolean>(() => {
// @ts-expect-error Generics in Vue SFCs are not supported yet
return !!isRouter || isRouter === ''
return isUsingRouter.value ? RouterLink : 'button'
})
// Router logic
Expand All @@ -71,8 +85,9 @@ watch(() => route.path, () => {
selectedTab.value = (allTabs.value as TabWithRoutes[]).indexOf(tab)
})
// Tab logic in query params
if (!isUsingRouter.value)
useTabQuery({ selectedTab, tabId: 'default' })
useTabQuery({ selectedTab, tabId })
</script>

<template>
Expand Down
File renamed without changes.
2 changes: 2 additions & 0 deletions src/scripts/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { appHeightTransition } from './files/transitions/appHeightTransition'
import { appLoader } from './files/components/appLoader'
import { appSelect } from './files/components/appSelect'
import { appSwitch } from './files/components/appSwitch'
import { appTabs } from './files/components/appTabs'

import type { Component } from './componentsTypes'

Expand All @@ -27,6 +28,7 @@ export const components: Component[] = [
appLoader,
appSelect,
appSwitch,
appTabs,

appHeightTransition,
]
1 change: 1 addition & 0 deletions src/scripts/componentsTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export enum ComponentName {
APP_LOADER = 'AppLoader',
APP_SELECT = 'AppSelect',
APP_SWITCH = 'AppSwitch',
APP_TABS = 'AppTabs',

TRANSITIONS = 'Transitions',
TRANSITION_HEIGHT = 'TransitionHeight',
Expand Down
51 changes: 51 additions & 0 deletions src/scripts/files/components/appTabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Component } from '../../componentsTypes'
import { ComponentName, ComponentType } from '../../componentsTypes'

export const appTabs: Component = {
component: ComponentName.APP_TABS,
name: 'Tabs',
files: [
{
type: ComponentType.COMPONENTS,
path: './src/components/tabs/AppTabs.vue',
folder: 'tabs',
},
{
type: ComponentType.COMPONENTS,
path: './src/components/tabs/AppTabsGroup.vue',
folder: 'tabs',
},
{
type: ComponentType.COMPONENTS,
path: './src/components/tabs/AppTabsList.vue',
folder: 'tabs',
},
{
type: ComponentType.COMPONENTS,
path: './src/components/tabs/AppTabsPanel.vue',
folder: 'tabs',
},
{
type: ComponentType.COMPONENTS,
path: './src/components/tabs/AppTabsPanels.vue',
folder: 'tabs',
},
{
type: ComponentType.COMPONENTS,
path: './src/components/tabs/AppTabsTab.vue',
folder: 'tabs',
},
{
type: ComponentType.COMPONENTS,
path: './src/components/tabs/appTabs.style.ts',
folder: 'tabs',
},
{
type: ComponentType.COMPOSABLES,
path: './src/composables/tabs/useTabsQuery.ts',
folder: 'tabs',
},
],
internalDependencies: [],
dependencies: ['@vueuse/router'],
}
2 changes: 1 addition & 1 deletion src/scripts/utils/generateRegistryFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function generateRegistryFile(components: Component[], fileName: st
}
}
catch (error) {
console.error(`Error reading file ${file.path} in component \'${component.name}\'`)
console.error(`\x1B[31mError reading file ${file.path} in component \'${component.name}\'`)
}
return null
})
Expand Down

0 comments on commit 9660269

Please sign in to comment.