From aa40d6d5aeff547439c5ee8f4e20dab18de07c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Wed, 22 Apr 2026 21:22:39 +0200 Subject: [PATCH 1/4] docs: Extend security and installation docs --- .vscode/settings.json | 5 +- cli.json | 14 ++ content/docs/advanced/security.mdx | 18 +- content/docs/configuration/env-variables.mdx | 1 - content/docs/setup/installation.mdx | 65 ++++++- package-lock.json | 1 + package.json | 1 + src/components/steps.tsx | 9 + src/components/tabs.tsx | 190 +++++++++++++++++++ src/components/ui/tabs.tsx | 142 ++++++++++++++ src/lib/cn.ts | 1 + src/lib/merge-refs.ts | 13 ++ src/styles/app.css | 1 + 13 files changed, 451 insertions(+), 10 deletions(-) create mode 100644 cli.json create mode 100644 src/components/steps.tsx create mode 100644 src/components/tabs.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/lib/cn.ts create mode 100644 src/lib/merge-refs.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 2f2ac25..6cca1ad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,7 @@ { "editor.formatOnSave": true, - "editor.defaultFormatter": "oxc.oxc-vscode" + "editor.defaultFormatter": "oxc.oxc-vscode", + "[typescriptreact]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + } } diff --git a/cli.json b/cli.json new file mode 100644 index 0000000..d8dff3e --- /dev/null +++ b/cli.json @@ -0,0 +1,14 @@ +{ + "$schema": "node_modules/@fumadocs/cli/dist/schema.json", + "aliases": { + "uiDir": "./components/ui", + "componentsDir": "./components", + "layoutDir": "./layouts", + "cssDir": "./styles", + "libDir": "./lib" + }, + "baseDir": "src", + "uiLibrary": "radix-ui", + "framework": "tanstack-start", + "commands": {} +} diff --git a/content/docs/advanced/security.mdx b/content/docs/advanced/security.mdx index e052658..d16b6fa 100644 --- a/content/docs/advanced/security.mdx +++ b/content/docs/advanced/security.mdx @@ -1,6 +1,20 @@ --- title: Security -description: Secure your OrcaCD deployment with hardening +description: Secure your OrcaCD deployment --- -TODO: +OrcaCD is designed to be safe by default, but of course, there are always additional steps you can take to further secure your deployment. Here are some best practices to consider: + +## Harden your Deployment + +- Disable password authentication for the Hub and use a secure OIDC provider instead, that enforces strong authentication methods, including secure multi-factor authentication (MFA). +- Always run the hub behind a secure reverse proxy and ensure that all communicationis encrypted using TLS. +- Make sure to configure the `TRUSTED_PROXIES` environment variable correctly to prevent IP spoofing attacks. + +## Why is it safe by default? + +A big focus of OrcaCD next to ease of use is security. Here are some of the measures we have taken to achieve this: + +- All sensitive data is stored encrypted in the database using a modern encryption algorithm ([AEGIS-256](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-aegis-aead-18)). +- Messages between the Hub and the Agents are encrypted with the same algorithm. The key is computed using the quantum-resistant [ML-KEM](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.pdf) algorithm in combination with [X25519](https://www.rfc-editor.org/rfc/rfc7748.html). +- We take all security issues seriously. You can find our security policy [here](https://github.com/OrcaCD/orca-cd/blob/main/SECURITY.md). diff --git a/content/docs/configuration/env-variables.mdx b/content/docs/configuration/env-variables.mdx index 6d042a1..eb2b060 100644 --- a/content/docs/configuration/env-variables.mdx +++ b/content/docs/configuration/env-variables.mdx @@ -4,7 +4,6 @@ description: Complete reference for all OrcaCD configuration options --- Below are all the environment variables supported by OrcaCD. These should be configured in your `.env` file. - Be cautious when modifying environment variables that are not recommended to change. ## General diff --git a/content/docs/setup/installation.mdx b/content/docs/setup/installation.mdx index 6aeeb4b..c8791b6 100644 --- a/content/docs/setup/installation.mdx +++ b/content/docs/setup/installation.mdx @@ -1,19 +1,72 @@ --- title: Installation -description: Get OrcaCD running quickly with Docker installation +description: Get OrcaCD running quickly with Docker --- -## Installation with Docker +import { Step, Steps } from "fumadocs-ui/components/steps"; +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; -1. Download the [`docker-compose.yml`](https://raw.githubusercontent.com/OrcaCD/orca-cd/main/docker-compose.yml) and [`.env`](https://raw.githubusercontent.com/OrcaCD/orca-cd/main/.env.example) file: + + -```bash +## Start Hub and Agent + +Download the [`docker-compose.yml`](https://raw.githubusercontent.com/OrcaCD/orca-cd/main/docker-compose.yml) and [`.env`](https://raw.githubusercontent.com/OrcaCD/orca-cd/main/.env.example) file: + +```bash tab="curl" curl -o docker-compose.yml https://raw.githubusercontent.com/OrcaCD/orca-cd/main/docker-compose.yml curl -o .env https://raw.githubusercontent.com/OrcaCD/orca-cd/main/.env.example ``` -2. Edit the `.env` file so that it fits your needs. See the environment variables section for more information. +```bash tab="wget" +wget -O docker-compose.yml https://raw.githubusercontent.com/OrcaCD/orca-cd/main/docker-compose.yml +wget -O .env https://raw.githubusercontent.com/OrcaCD/orca-cd/main/.env.example +``` + +Edit the `.env` file according to the instructions in the file. You can also customize other environment variables as needed. +See the [environment variables page](../configuration/env-variables) for more details. + + + Remove the Agent part from the compose file if you don't want to deploy it to the same machine as + the Hub. + + +Start the Hub and the Agent: + +```bash +docker compose up -d +``` + + + -3. Run `docker compose up -d` +## Configure your Reverse Proxy + +See the [reverse proxy guide](../guides/reverse-proxy) for instructions on how to configure a reverse proxy for your Hub. + + + + +## Create an Admin Account Create an admin account on `https:///login` + +Todo: Add image + + + + +## Connect your first Agent + +Navigate to the Agents page and click "Add Agent". Follow the instructions and copy the token and add it as `AUTH_TOKEN` to the `.env` file of your Agent deployment and restart the Agent. + +Todo: Add image + + + +## Start Deploying + +Add your first repository and create your first deployment. + + + diff --git a/package-lock.json b/package-lock.json index 1179e86..724ec02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "hasInstallScript": true, "dependencies": { "@orama/orama": "^3.1.18", + "@radix-ui/react-tabs": "^1.1.13", "@tanstack/react-router": "1.168.23", "@tanstack/react-router-devtools": "1.166.13", "@tanstack/react-start": "1.167.42", diff --git a/package.json b/package.json index d9b3484..b8d524e 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@orama/orama": "^3.1.18", + "@radix-ui/react-tabs": "^1.1.13", "@tanstack/react-router": "1.168.23", "@tanstack/react-router-devtools": "1.166.13", "@tanstack/react-start": "1.167.42", diff --git a/src/components/steps.tsx b/src/components/steps.tsx new file mode 100644 index 0000000..0e79b76 --- /dev/null +++ b/src/components/steps.tsx @@ -0,0 +1,9 @@ +import type { ReactNode } from "react"; + +export function Steps({ children }: { children: ReactNode }) { + return
{children}
; +} + +export function Step({ children }: { children: ReactNode }) { + return
{children}
; +} diff --git a/src/components/tabs.tsx b/src/components/tabs.tsx new file mode 100644 index 0000000..44b582e --- /dev/null +++ b/src/components/tabs.tsx @@ -0,0 +1,190 @@ +"use client"; + +import * as React from "react"; +import { + type ComponentProps, + createContext, + type ReactNode, + useContext, + useEffect, + useId, + useMemo, + useState, +} from "react"; +import { cn } from "../lib/cn"; +import * as Unstyled from "./ui/tabs"; + +type CollectionKey = string | symbol; + +export interface TabsProps extends Omit< + ComponentProps, + "value" | "onValueChange" +> { + /** + * Use simple mode instead of advanced usage as documented in https://radix-ui.com/primitives/docs/components/tabs. + */ + items?: string[]; + + /** + * Shortcut for `defaultValue` when `items` is provided. + * + * @defaultValue 0 + */ + defaultIndex?: number; + + /** + * Additional label in tabs list when `items` is provided. + */ + label?: ReactNode; +} + +const TabsContext = createContext<{ + items?: string[]; + collection: CollectionKey[]; +} | null>(null); + +function useTabContext() { + const ctx = useContext(TabsContext); + if (!ctx) throw new Error("You must wrap your component in "); + return ctx; +} + +export function TabsList(props: React.ComponentPropsWithRef) { + return ( + + ); +} + +export function TabsTrigger(props: React.ComponentPropsWithRef) { + return ( + + ); +} + +export function Tabs({ + ref, + className, + items, + label, + defaultIndex = 0, + defaultValue = items ? escapeValue(items[defaultIndex]) : undefined, + ...props +}: TabsProps) { + const [value, setValue] = useState(defaultValue); + const collection = useMemo(() => [], []); + + return ( + { + if (items && !items.some((item) => escapeValue(item) === v)) return; + setValue(v); + }} + {...props} + > + {items && ( + + {label && {label}} + {items.map((item) => ( + + {item} + + ))} + + )} + ({ items, collection }), [collection, items])}> + {props.children} + + + ); +} + +export interface TabProps extends Omit, "value"> { + /** + * Value of tab, detect from index if unspecified. + */ + value?: string; +} + +export function Tab({ value, ...props }: TabProps) { + const { items } = useTabContext(); + const resolved = + value ?? + // eslint-disable-next-line react-hooks/rules-of-hooks -- `value` is not supposed to change + items?.at(useCollectionIndex()); + if (!resolved) + throw new Error( + "Failed to resolve tab `value`, please pass a `value` prop to the Tab component.", + ); + + return ( + + {props.children} + + ); +} + +export function TabsContent({ + value, + className, + ...props +}: ComponentProps) { + return ( + figure:only-child]:-m-4 [&>figure:only-child]:border-none", + className, + )} + {...props} + > + {props.children} + + ); +} + +/** + * Inspired by Headless UI. + * + * Return the index of children, this is made possible by registering the order of render from children using React context. + * This is supposed by work with pre-rendering & pure client-side rendering. + */ +function useCollectionIndex() { + const key = useId(); + const { collection } = useTabContext(); + + useEffect(() => { + return () => { + const idx = collection.indexOf(key); + if (idx !== -1) collection.splice(idx, 1); + }; + }, [key, collection]); + + if (!collection.includes(key)) collection.push(key); + return collection.indexOf(key); +} + +/** + * only escape whitespaces in values in simple mode + */ +function escapeValue(v: string): string { + return v.toLowerCase().replace(/\s/, "-"); +} diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..16f9dfd --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { + type ComponentProps, + createContext, + use, + useEffectEvent, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; +import * as Primitive from "@radix-ui/react-tabs"; +import { mergeRefs } from "../../lib/merge-refs"; + +type ChangeListener = (v: string) => void; +const listeners = new Map>(); + +export interface TabsProps extends ComponentProps { + /** + * Identifier for Sharing value of tabs + */ + groupId?: string; + + /** + * Enable persistent + */ + persist?: boolean; + + /** + * If true, updates the URL hash based on the tab's id + */ + updateAnchor?: boolean; +} + +const TabsContext = createContext<{ + valueToIdMap: Map; +} | null>(null); + +function useTabContext() { + const ctx = use(TabsContext); + if (!ctx) throw new Error("You must wrap your component in "); + return ctx; +} + +export const TabsList = Primitive.TabsList; + +export const TabsTrigger = Primitive.TabsTrigger; + +export function Tabs({ + ref, + groupId, + persist = false, + updateAnchor = false, + defaultValue, + value: _value, + onValueChange: _onValueChange, + ...props +}: TabsProps) { + const tabsRef = useRef(null); + const valueToIdMap = useMemo(() => new Map(), []); + const [value, setValue] = + _value === undefined + ? // eslint-disable-next-line react-hooks/rules-of-hooks -- not supposed to change controlled/uncontrolled + useState(defaultValue) + : // eslint-disable-next-line react-hooks/rules-of-hooks -- not supposed to change controlled/uncontrolled + [_value, useEffectEvent((v: string) => _onValueChange?.(v))]; + + useLayoutEffect(() => { + if (!groupId) return; + let previous = sessionStorage.getItem(groupId); + if (persist) previous ??= localStorage.getItem(groupId); + if (previous) setValue(previous); + + const groupListeners = listeners.get(groupId) ?? new Set(); + groupListeners.add(setValue); + listeners.set(groupId, groupListeners); + return () => { + groupListeners.delete(setValue); + }; + }, [groupId, persist, setValue]); + + useLayoutEffect(() => { + const hash = window.location.hash.slice(1); + if (!hash) return; + + for (const [value, id] of valueToIdMap.entries()) { + if (id === hash) { + setValue(value); + tabsRef.current?.scrollIntoView(); + break; + } + } + }, [setValue, valueToIdMap]); + + return ( + { + if (updateAnchor) { + const id = valueToIdMap.get(v); + + if (id) { + window.history.replaceState(null, "", `#${id}`); + } + } + + if (groupId) { + const groupListeners = listeners.get(groupId); + if (groupListeners) { + for (const listener of groupListeners) listener(v); + } + + sessionStorage.setItem(groupId, v); + if (persist) localStorage.setItem(groupId, v); + } else { + setValue(v); + } + }} + {...props} + > + ({ valueToIdMap }), [valueToIdMap])}> + {props.children} + + + ); +} + +export function TabsContent({ value, ...props }: ComponentProps) { + const { valueToIdMap } = useTabContext(); + + if (props.id) { + valueToIdMap.set(value, props.id); + } + + return ( + + {props.children} + + ); +} diff --git a/src/lib/cn.ts b/src/lib/cn.ts new file mode 100644 index 0000000..8e473da --- /dev/null +++ b/src/lib/cn.ts @@ -0,0 +1 @@ +export { twMerge as cn } from "tailwind-merge"; diff --git a/src/lib/merge-refs.ts b/src/lib/merge-refs.ts new file mode 100644 index 0000000..04e1d17 --- /dev/null +++ b/src/lib/merge-refs.ts @@ -0,0 +1,13 @@ +import type * as React from "react"; + +export function mergeRefs(...refs: (React.Ref | undefined)[]): React.RefCallback { + return (value) => { + refs.forEach((ref) => { + if (typeof ref === "function") { + ref(value); + } else if (ref) { + ref.current = value; + } + }); + }; +} diff --git a/src/styles/app.css b/src/styles/app.css index 8d375ca..81cf3de 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -8,6 +8,7 @@ .dark { --color-fd-primary: oklch(0.68 0.15 237); + --color-fd-background: hsl(0, 0%, 11%); } button:not([disabled]), From 3b6ede6ee7c197a5e621387d81923c0c0b5a6efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Wed, 22 Apr 2026 21:28:10 +0200 Subject: [PATCH 2/4] fix: Linting --- src/components/tabs.tsx | 19 ++++++++++++++----- src/components/ui/tabs.tsx | 28 +++++++++++++++++++++------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/components/tabs.tsx b/src/components/tabs.tsx index 44b582e..bbdef6b 100644 --- a/src/components/tabs.tsx +++ b/src/components/tabs.tsx @@ -45,7 +45,9 @@ const TabsContext = createContext<{ function useTabContext() { const ctx = useContext(TabsContext); - if (!ctx) throw new Error("You must wrap your component in "); + if (!ctx) { + throw new Error("You must wrap your component in "); + } return ctx; } @@ -94,7 +96,9 @@ export function Tabs({ )} value={value} onValueChange={(v: string) => { - if (items && !items.some((item) => escapeValue(item) === v)) return; + if (items && !items.some((item) => escapeValue(item) === v)) { + return; + } setValue(v); }} {...props} @@ -129,10 +133,11 @@ export function Tab({ value, ...props }: TabProps) { value ?? // eslint-disable-next-line react-hooks/rules-of-hooks -- `value` is not supposed to change items?.at(useCollectionIndex()); - if (!resolved) + if (!resolved) { throw new Error( "Failed to resolve tab `value`, please pass a `value` prop to the Tab component.", ); + } return ( @@ -174,11 +179,15 @@ function useCollectionIndex() { useEffect(() => { return () => { const idx = collection.indexOf(key); - if (idx !== -1) collection.splice(idx, 1); + if (idx !== -1) { + collection.splice(idx, 1); + } }; }, [key, collection]); - if (!collection.includes(key)) collection.push(key); + if (!collection.includes(key)) { + collection.push(key); + } return collection.indexOf(key); } diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 16f9dfd..dfb23db 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -39,7 +39,9 @@ const TabsContext = createContext<{ function useTabContext() { const ctx = use(TabsContext); - if (!ctx) throw new Error("You must wrap your component in "); + if (!ctx) { + throw new Error("You must wrap your component in "); + } return ctx; } @@ -67,10 +69,16 @@ export function Tabs({ [_value, useEffectEvent((v: string) => _onValueChange?.(v))]; useLayoutEffect(() => { - if (!groupId) return; + if (!groupId) { + return; + } let previous = sessionStorage.getItem(groupId); - if (persist) previous ??= localStorage.getItem(groupId); - if (previous) setValue(previous); + if (persist) { + previous ??= localStorage.getItem(groupId); + } + if (previous) { + setValue(previous); + } const groupListeners = listeners.get(groupId) ?? new Set(); groupListeners.add(setValue); @@ -82,7 +90,9 @@ export function Tabs({ useLayoutEffect(() => { const hash = window.location.hash.slice(1); - if (!hash) return; + if (!hash) { + return; + } for (const [value, id] of valueToIdMap.entries()) { if (id === hash) { @@ -109,11 +119,15 @@ export function Tabs({ if (groupId) { const groupListeners = listeners.get(groupId); if (groupListeners) { - for (const listener of groupListeners) listener(v); + for (const listener of groupListeners) { + listener(v); + } } sessionStorage.setItem(groupId, v); - if (persist) localStorage.setItem(groupId, v); + if (persist) { + localStorage.setItem(groupId, v); + } } else { setValue(v); } From a72a9dbbcc951b7b10ce06dd3b2b20051fae63f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Wed, 22 Apr 2026 22:41:21 +0200 Subject: [PATCH 3/4] fix: Use markdown version of components --- content/docs/setup/installation.mdx | 30 +---- src/components/steps.tsx | 9 -- src/components/tabs.tsx | 199 ---------------------------- src/components/ui/tabs.tsx | 156 ---------------------- src/lib/cn.ts | 1 - src/lib/merge-refs.ts | 13 -- 6 files changed, 5 insertions(+), 403 deletions(-) delete mode 100644 src/components/steps.tsx delete mode 100644 src/components/tabs.tsx delete mode 100644 src/components/ui/tabs.tsx delete mode 100644 src/lib/cn.ts delete mode 100644 src/lib/merge-refs.ts diff --git a/content/docs/setup/installation.mdx b/content/docs/setup/installation.mdx index c8791b6..2ee457e 100644 --- a/content/docs/setup/installation.mdx +++ b/content/docs/setup/installation.mdx @@ -3,13 +3,7 @@ title: Installation description: Get OrcaCD running quickly with Docker --- -import { Step, Steps } from "fumadocs-ui/components/steps"; -import { Tab, Tabs } from "fumadocs-ui/components/tabs"; - - - - -## Start Hub and Agent +## Start Hub and Agent [step] Download the [`docker-compose.yml`](https://raw.githubusercontent.com/OrcaCD/orca-cd/main/docker-compose.yml) and [`.env`](https://raw.githubusercontent.com/OrcaCD/orca-cd/main/.env.example) file: @@ -37,36 +31,22 @@ Start the Hub and the Agent: docker compose up -d ``` - - - -## Configure your Reverse Proxy +## Configure your Reverse Proxy [step] See the [reverse proxy guide](../guides/reverse-proxy) for instructions on how to configure a reverse proxy for your Hub. - - - -## Create an Admin Account +## Create an Admin Account [step] Create an admin account on `https:///login` Todo: Add image - - - -## Connect your first Agent +## Connect your first Agent [step] Navigate to the Agents page and click "Add Agent". Follow the instructions and copy the token and add it as `AUTH_TOKEN` to the `.env` file of your Agent deployment and restart the Agent. Todo: Add image - - -## Start Deploying +## Start Deploying [step] Add your first repository and create your first deployment. - - - diff --git a/src/components/steps.tsx b/src/components/steps.tsx deleted file mode 100644 index 0e79b76..0000000 --- a/src/components/steps.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { ReactNode } from "react"; - -export function Steps({ children }: { children: ReactNode }) { - return
{children}
; -} - -export function Step({ children }: { children: ReactNode }) { - return
{children}
; -} diff --git a/src/components/tabs.tsx b/src/components/tabs.tsx deleted file mode 100644 index bbdef6b..0000000 --- a/src/components/tabs.tsx +++ /dev/null @@ -1,199 +0,0 @@ -"use client"; - -import * as React from "react"; -import { - type ComponentProps, - createContext, - type ReactNode, - useContext, - useEffect, - useId, - useMemo, - useState, -} from "react"; -import { cn } from "../lib/cn"; -import * as Unstyled from "./ui/tabs"; - -type CollectionKey = string | symbol; - -export interface TabsProps extends Omit< - ComponentProps, - "value" | "onValueChange" -> { - /** - * Use simple mode instead of advanced usage as documented in https://radix-ui.com/primitives/docs/components/tabs. - */ - items?: string[]; - - /** - * Shortcut for `defaultValue` when `items` is provided. - * - * @defaultValue 0 - */ - defaultIndex?: number; - - /** - * Additional label in tabs list when `items` is provided. - */ - label?: ReactNode; -} - -const TabsContext = createContext<{ - items?: string[]; - collection: CollectionKey[]; -} | null>(null); - -function useTabContext() { - const ctx = useContext(TabsContext); - if (!ctx) { - throw new Error("You must wrap your component in "); - } - return ctx; -} - -export function TabsList(props: React.ComponentPropsWithRef) { - return ( - - ); -} - -export function TabsTrigger(props: React.ComponentPropsWithRef) { - return ( - - ); -} - -export function Tabs({ - ref, - className, - items, - label, - defaultIndex = 0, - defaultValue = items ? escapeValue(items[defaultIndex]) : undefined, - ...props -}: TabsProps) { - const [value, setValue] = useState(defaultValue); - const collection = useMemo(() => [], []); - - return ( - { - if (items && !items.some((item) => escapeValue(item) === v)) { - return; - } - setValue(v); - }} - {...props} - > - {items && ( - - {label && {label}} - {items.map((item) => ( - - {item} - - ))} - - )} - ({ items, collection }), [collection, items])}> - {props.children} - - - ); -} - -export interface TabProps extends Omit, "value"> { - /** - * Value of tab, detect from index if unspecified. - */ - value?: string; -} - -export function Tab({ value, ...props }: TabProps) { - const { items } = useTabContext(); - const resolved = - value ?? - // eslint-disable-next-line react-hooks/rules-of-hooks -- `value` is not supposed to change - items?.at(useCollectionIndex()); - if (!resolved) { - throw new Error( - "Failed to resolve tab `value`, please pass a `value` prop to the Tab component.", - ); - } - - return ( - - {props.children} - - ); -} - -export function TabsContent({ - value, - className, - ...props -}: ComponentProps) { - return ( - figure:only-child]:-m-4 [&>figure:only-child]:border-none", - className, - )} - {...props} - > - {props.children} - - ); -} - -/** - * Inspired by Headless UI. - * - * Return the index of children, this is made possible by registering the order of render from children using React context. - * This is supposed by work with pre-rendering & pure client-side rendering. - */ -function useCollectionIndex() { - const key = useId(); - const { collection } = useTabContext(); - - useEffect(() => { - return () => { - const idx = collection.indexOf(key); - if (idx !== -1) { - collection.splice(idx, 1); - } - }; - }, [key, collection]); - - if (!collection.includes(key)) { - collection.push(key); - } - return collection.indexOf(key); -} - -/** - * only escape whitespaces in values in simple mode - */ -function escapeValue(v: string): string { - return v.toLowerCase().replace(/\s/, "-"); -} diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx deleted file mode 100644 index dfb23db..0000000 --- a/src/components/ui/tabs.tsx +++ /dev/null @@ -1,156 +0,0 @@ -"use client"; - -import { - type ComponentProps, - createContext, - use, - useEffectEvent, - useLayoutEffect, - useMemo, - useRef, - useState, -} from "react"; -import * as Primitive from "@radix-ui/react-tabs"; -import { mergeRefs } from "../../lib/merge-refs"; - -type ChangeListener = (v: string) => void; -const listeners = new Map>(); - -export interface TabsProps extends ComponentProps { - /** - * Identifier for Sharing value of tabs - */ - groupId?: string; - - /** - * Enable persistent - */ - persist?: boolean; - - /** - * If true, updates the URL hash based on the tab's id - */ - updateAnchor?: boolean; -} - -const TabsContext = createContext<{ - valueToIdMap: Map; -} | null>(null); - -function useTabContext() { - const ctx = use(TabsContext); - if (!ctx) { - throw new Error("You must wrap your component in "); - } - return ctx; -} - -export const TabsList = Primitive.TabsList; - -export const TabsTrigger = Primitive.TabsTrigger; - -export function Tabs({ - ref, - groupId, - persist = false, - updateAnchor = false, - defaultValue, - value: _value, - onValueChange: _onValueChange, - ...props -}: TabsProps) { - const tabsRef = useRef(null); - const valueToIdMap = useMemo(() => new Map(), []); - const [value, setValue] = - _value === undefined - ? // eslint-disable-next-line react-hooks/rules-of-hooks -- not supposed to change controlled/uncontrolled - useState(defaultValue) - : // eslint-disable-next-line react-hooks/rules-of-hooks -- not supposed to change controlled/uncontrolled - [_value, useEffectEvent((v: string) => _onValueChange?.(v))]; - - useLayoutEffect(() => { - if (!groupId) { - return; - } - let previous = sessionStorage.getItem(groupId); - if (persist) { - previous ??= localStorage.getItem(groupId); - } - if (previous) { - setValue(previous); - } - - const groupListeners = listeners.get(groupId) ?? new Set(); - groupListeners.add(setValue); - listeners.set(groupId, groupListeners); - return () => { - groupListeners.delete(setValue); - }; - }, [groupId, persist, setValue]); - - useLayoutEffect(() => { - const hash = window.location.hash.slice(1); - if (!hash) { - return; - } - - for (const [value, id] of valueToIdMap.entries()) { - if (id === hash) { - setValue(value); - tabsRef.current?.scrollIntoView(); - break; - } - } - }, [setValue, valueToIdMap]); - - return ( - { - if (updateAnchor) { - const id = valueToIdMap.get(v); - - if (id) { - window.history.replaceState(null, "", `#${id}`); - } - } - - if (groupId) { - const groupListeners = listeners.get(groupId); - if (groupListeners) { - for (const listener of groupListeners) { - listener(v); - } - } - - sessionStorage.setItem(groupId, v); - if (persist) { - localStorage.setItem(groupId, v); - } - } else { - setValue(v); - } - }} - {...props} - > - ({ valueToIdMap }), [valueToIdMap])}> - {props.children} - - - ); -} - -export function TabsContent({ value, ...props }: ComponentProps) { - const { valueToIdMap } = useTabContext(); - - if (props.id) { - valueToIdMap.set(value, props.id); - } - - return ( - - {props.children} - - ); -} diff --git a/src/lib/cn.ts b/src/lib/cn.ts deleted file mode 100644 index 8e473da..0000000 --- a/src/lib/cn.ts +++ /dev/null @@ -1 +0,0 @@ -export { twMerge as cn } from "tailwind-merge"; diff --git a/src/lib/merge-refs.ts b/src/lib/merge-refs.ts deleted file mode 100644 index 04e1d17..0000000 --- a/src/lib/merge-refs.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type * as React from "react"; - -export function mergeRefs(...refs: (React.Ref | undefined)[]): React.RefCallback { - return (value) => { - refs.forEach((ref) => { - if (typeof ref === "function") { - ref(value); - } else if (ref) { - ref.current = value; - } - }); - }; -} From aa29da299cb5f6b794f6a2cf1127f41136b265e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Wed, 22 Apr 2026 22:50:49 +0200 Subject: [PATCH 4/4] fix: Revert unused changes --- cli.json | 14 -------------- package-lock.json | 1 - package.json | 1 - 3 files changed, 16 deletions(-) delete mode 100644 cli.json diff --git a/cli.json b/cli.json deleted file mode 100644 index d8dff3e..0000000 --- a/cli.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "node_modules/@fumadocs/cli/dist/schema.json", - "aliases": { - "uiDir": "./components/ui", - "componentsDir": "./components", - "layoutDir": "./layouts", - "cssDir": "./styles", - "libDir": "./lib" - }, - "baseDir": "src", - "uiLibrary": "radix-ui", - "framework": "tanstack-start", - "commands": {} -} diff --git a/package-lock.json b/package-lock.json index 724ec02..1179e86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "hasInstallScript": true, "dependencies": { "@orama/orama": "^3.1.18", - "@radix-ui/react-tabs": "^1.1.13", "@tanstack/react-router": "1.168.23", "@tanstack/react-router-devtools": "1.166.13", "@tanstack/react-start": "1.167.42", diff --git a/package.json b/package.json index b8d524e..d9b3484 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ }, "dependencies": { "@orama/orama": "^3.1.18", - "@radix-ui/react-tabs": "^1.1.13", "@tanstack/react-router": "1.168.23", "@tanstack/react-router-devtools": "1.166.13", "@tanstack/react-start": "1.167.42",