+
+const OkuTabTrigger = TabTrigger as typeof TabTrigger & (new () => { $props: _TabsProps })
+
+export { OkuTabTrigger }
+
+export type { TabsTriggerProps }
diff --git a/packages/components/tabs/src/tabs.ts b/packages/components/tabs/src/tabs.ts
new file mode 100644
index 000000000..b0f00078e
--- /dev/null
+++ b/packages/components/tabs/src/tabs.ts
@@ -0,0 +1,199 @@
+import type { ElementType, IPrimitiveProps, InstanceTypeRef, MergeProps, RefElement } from '@oku-ui/primitive'
+import { Primitive } from '@oku-ui/primitive'
+import { computed, defineComponent, h, toRefs, useModel } from 'vue'
+import type { ComputedRef, PropType } from 'vue'
+import type { Scope } from '@oku-ui/provide'
+import { createProvideScope } from '@oku-ui/provide'
+import { createRovingFocusGroupScope } from '@oku-ui/roving-focus'
+import { useControllable, useForwardRef, useId } from '@oku-ui/use-composable'
+import { useDirection } from '@oku-ui/direction'
+
+const TAB_NAME = 'OkuTab' as const
+
+type TabsElement = ElementType<'div'>
+export type _TabsEl = HTMLDivElement
+
+export type ScopedPropsInterface = P & { scopeTabs?: Scope }
+export const ScopedProps = {
+ scopeTabs: {
+ type: Object as PropType,
+ required: false,
+ },
+}
+
+type Orientation = 'horizontal' | 'vertical'
+type Direction = 'ltr' | 'rtl'
+/**
+ * Whether a tab is activated automatically or manually.
+ * @defaultValue automatic
+ * */
+type ActivationMode = 'automatic' | 'manual'
+interface TabsProps extends ScopedPropsInterface {
+ /** The value for the selected tab, if controlled */
+ value?: string
+ /**
+ * The default value of the tab.
+ * @default 'tab1'
+ * @type string
+ * @example
+ * ```vue
+ *
+ // ...
+ *
+ * ```
+ * @see link-to-oku-docs/tab
+ */
+ defaultValue?: string
+ /**
+ * The callback function that is called when the tab value changes.
+ * @default () => {}
+ * @type (value: string) => void
+ * @example
+ * ```vue
+ * console.log(value)}>
+ // ...
+ *
+ * */
+ onValueChange?: (value: string) => void
+ /**
+ * The orientation of the tabs.
+ * @default 'horizontal'
+ * @type 'horizontal' | 'vertical'
+ * @example
+ * ```vue
+ *
+ // ...
+ *
+ * ```
+ * @see link-to-oku-docs/tab
+ * */
+ orientation?: Orientation
+ /**
+ * The direction of navigation between toolbar items.
+ */
+ dir?: Direction
+ /**
+ * Whether a tab is activated automatically or manually.
+ * @defaultValue automatic
+ * */
+ activationMode?: ActivationMode
+}
+
+interface TabsProvideValue {
+ baseId: string
+ value?: ComputedRef
+ onValueChange: (value: string) => void
+ orientation?: TabsProps['orientation']
+ dir?: TabsProps['dir']
+ activationMode?: TabsProps['activationMode']
+}
+
+export const [createTabsProvider, _createTabsScope] = createProvideScope(TAB_NAME, [
+ createRovingFocusGroupScope,
+])
+
+export const [TabsProvider, useTabsInject]
+ = createTabsProvider(TAB_NAME)
+
+export const useRovingFocusGroupScope = createRovingFocusGroupScope()
+
+const Tabs = defineComponent({
+ name: TAB_NAME,
+ inheritAttrs: false,
+ props: {
+ value: {
+ type: String as PropType,
+ required: false,
+ },
+ defaultValue: {
+ type: String as PropType,
+ default: undefined,
+ },
+ orientation: {
+ type: String as PropType,
+ default: 'horizontal',
+ },
+ dir: {
+ type: String as PropType,
+ default: 'ltr',
+ required: false,
+ },
+ activationMode: {
+ type: String as PropType,
+ default: 'automatic',
+ required: false,
+ },
+ modelValue: {
+ type: String as PropType,
+ required: false,
+ },
+ onValueChange: {
+ type: Function as PropType<(value: string) => void>,
+ required: false,
+ },
+ asChild: {
+ type: Boolean as PropType,
+ default: false,
+ },
+ ...ScopedProps,
+ },
+ emits: ['update:modelValue'],
+ setup(props, { slots, emit, attrs }) {
+ const {
+ value: valueProp,
+ onValueChange,
+ defaultValue,
+ orientation,
+ dir,
+ activationMode,
+ } = toRefs(props)
+
+ const direction = useDirection(dir.value)
+
+ const forwardedRef = useForwardRef()
+
+ const modelValue = useModel(props, 'modelValue')
+
+ const { state, updateValue } = useControllable({
+ prop: computed(() => modelValue.value ?? valueProp.value),
+ defaultProp: computed(() => defaultValue.value),
+ onChange: (result: any) => {
+ emit('update:modelValue', result)
+ onValueChange.value?.(result)
+ },
+ })
+
+ TabsProvider({
+ onValueChange: updateValue,
+ orientation: orientation.value,
+ dir: direction,
+ value: state,
+ activationMode: activationMode.value,
+ baseId: useId(),
+ scope: props.scopeTabs,
+ })
+
+ return () =>
+ h(
+ Primitive.div,
+ {
+ 'dir': direction,
+ 'data-orientation': props.orientation,
+ 'ref': forwardedRef,
+ ...attrs,
+ 'asChild': props.asChild,
+ }, slots,
+ )
+ },
+})
+
+type _TabsProps = MergeProps
+export type IstanceTabsType = InstanceTypeRef
+
+type TabsRef = RefElement
+
+const OkuTabs = Tabs as typeof Tabs & (new () => { $props: _TabsProps })
+
+export { OkuTabs }
+
+export type { TabsProps, TabsRef }
diff --git a/packages/components/tabs/src/utils.ts b/packages/components/tabs/src/utils.ts
new file mode 100644
index 000000000..a865868d7
--- /dev/null
+++ b/packages/components/tabs/src/utils.ts
@@ -0,0 +1,7 @@
+export function makeTriggerId(baseId: string, value: string) {
+ return `${baseId}-trigger-${value}`
+}
+
+export function makeContentId(baseId: string, value: string) {
+ return `${baseId}-content-${value}`
+}
diff --git a/packages/components/tabs/tsconfig.json b/packages/components/tabs/tsconfig.json
new file mode 100644
index 000000000..b8dfa9041
--- /dev/null
+++ b/packages/components/tabs/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "tsconfig/node16.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist"
+ },
+ "include": [
+ "src"
+ ]
+}
diff --git a/packages/components/tabs/tsup.config.ts b/packages/components/tabs/tsup.config.ts
new file mode 100644
index 000000000..a2f7a0d8b
--- /dev/null
+++ b/packages/components/tabs/tsup.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'tsup'
+import pkg from './package.json'
+
+const external = [
+ ...Object.keys(pkg.dependencies || {}),
+ ...Object.keys(pkg.peerDependencies || {}),
+]
+
+export default defineConfig((options) => {
+ return [
+ {
+ ...options,
+ entryPoints: ['src/index.ts'],
+ external,
+ dts: true,
+ clean: true,
+ target: 'node16',
+ format: ['esm'],
+ outExtension: () => ({ js: '.mjs' }),
+ },
+ ]
+})
diff --git a/packages/core/use-composable/src/index.ts b/packages/core/use-composable/src/index.ts
index bf791cd93..3a9b04522 100644
--- a/packages/core/use-composable/src/index.ts
+++ b/packages/core/use-composable/src/index.ts
@@ -10,5 +10,4 @@ export { useComposedRefs } from './useComposedRefs'
export { useForwardRef } from './useForwardRef'
export { useEscapeKeydown } from './useEscapeKeydown'
export type { MaybeComputedElementRef } from './unrefElement'
-export { useRect } from './use-rect'
export { computedEager, syncRef, computedAsync }
diff --git a/playground/nuxt3/package.json b/playground/nuxt3/package.json
index 3a5cc9eb8..f120bdc14 100644
--- a/playground/nuxt3/package.json
+++ b/playground/nuxt3/package.json
@@ -22,6 +22,7 @@
"@oku-ui/separator": "workspace:^",
"@oku-ui/slot": "workspace:^",
"@oku-ui/switch": "workspace:^",
+ "@oku-ui/tabs": "workspace:^",
"@oku-ui/visually-hidden": "workspace:^"
},
"devDependencies": {
diff --git a/playground/nuxt3/pages/index.vue b/playground/nuxt3/pages/index.vue
index fb7139b86..540ad87e7 100644
--- a/playground/nuxt3/pages/index.vue
+++ b/playground/nuxt3/pages/index.vue
@@ -56,6 +56,10 @@ const pages: Page[] = [
name: 'roving-focus',
path: '/roving-focus',
},
+ {
+ name: 'OkuTabs',
+ path: '/tabs',
+ },
]
diff --git a/playground/nuxt3/pages/tabs.vue b/playground/nuxt3/pages/tabs.vue
new file mode 100644
index 000000000..17f56e5f8
--- /dev/null
+++ b/playground/nuxt3/pages/tabs.vue
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/playground/vue3/package.json b/playground/vue3/package.json
index 627aa713c..1c01e3663 100644
--- a/playground/vue3/package.json
+++ b/playground/vue3/package.json
@@ -20,6 +20,7 @@
"@oku-ui/separator": "workspace:^",
"@oku-ui/slot": "workspace:^",
"@oku-ui/switch": "workspace:^",
+ "@oku-ui/tabs": "workspace:^",
"vite-plugin-pages": "^0.31.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b4371c9a2..5c10aa737 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -70,6 +70,9 @@ importers:
'@oku-ui/switch':
specifier: workspace:^
version: link:packages/components/switch
+ '@oku-ui/tabs':
+ specifier: workspace:^
+ version: link:packages/components/tabs
'@oku-ui/toggle':
specifier: workspace:^
version: link:packages/components/toggle
@@ -515,6 +518,37 @@ importers:
specifier: workspace:^
version: link:../../tsconfig
+ packages/components/tabs:
+ dependencies:
+ '@oku-ui/direction':
+ specifier: latest
+ version: link:../direction
+ '@oku-ui/presence':
+ specifier: latest
+ version: link:../presence
+ '@oku-ui/primitive':
+ specifier: latest
+ version: link:../../core/primitive
+ '@oku-ui/provide':
+ specifier: latest
+ version: link:../../core/provide
+ '@oku-ui/roving-focus':
+ specifier: latest
+ version: link:../roving-focus
+ '@oku-ui/use-composable':
+ specifier: latest
+ version: link:../../core/use-composable
+ '@oku-ui/utils':
+ specifier: latest
+ version: link:../../core/utils
+ vue:
+ specifier: ^3.3.4
+ version: 3.3.4
+ devDependencies:
+ tsconfig:
+ specifier: workspace:^
+ version: link:../../tsconfig
+
packages/components/toggle:
dependencies:
'@oku-ui/primitive':
@@ -657,6 +691,9 @@ importers:
'@oku-ui/switch':
specifier: workspace:^
version: link:../../packages/components/switch
+ '@oku-ui/tabs':
+ specifier: workspace:^
+ version: link:../../packages/components/tabs
'@oku-ui/visually-hidden':
specifier: workspace:^
version: link:../../packages/components/visually-hidden
@@ -700,6 +737,9 @@ importers:
'@oku-ui/switch':
specifier: workspace:^
version: link:../../packages/components/switch
+ '@oku-ui/tabs':
+ specifier: workspace:^
+ version: link:../../packages/components/tabs
vite-plugin-pages:
specifier: ^0.31.0
version: 0.31.0(vite@4.3.5)