diff --git a/docs/app/(home)/page.tsx b/docs/app/(home)/page.tsx index 832f112..5598564 100644 --- a/docs/app/(home)/page.tsx +++ b/docs/app/(home)/page.tsx @@ -1,8 +1,15 @@ -/** biome-ignore-all lint/a11y/useButtonType: */ -/** biome-ignore-all lint/suspicious/noArrayIndexKey: */ +/** biome-ignore-all lint/suspicious/noArrayIndexKey: Would need to look into this trivial issue */ "use client"; +import defaultMdxComponents from "fumadocs-ui/mdx"; import { cn } from "fumadocs-ui/utils/cn"; +import { + BlocksIcon, + GitMergeIcon, + HomeIcon, + SailboatIcon, + ZapIcon, +} from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { useState } from "react"; @@ -12,6 +19,7 @@ import CkanDevstallerDemo from "./ckan-devstaller-demo.gif"; export default function HomePage() { const gridColor = "color-mix(in oklab, var(--color-fd-primary) 10%, transparent)"; + const { Card, Cards } = defaultMdxComponents; return ( <>
-
+
+ + } + href="/docs/quick-start" + title="Quick start" + > + Get started with ckan-devstaller and install CKAN within minutes + + } href="/docs/builder" title="Builder"> + Customize your installation with an interactive web GUI + + } + href="/docs/reference/installation-architecture" + title="Installation architecture" + > + Learn about where files are installed after running + ckan-devstaller + + } + href="https://github.com/dathere/ckan-devstaller" + title="Source code" + > + View the source code of ckan-devstaller on GitHub + +
@@ -59,9 +94,12 @@ function Hero() { />

ckan-devstaller

- ckan-devstaller + + ckan-devstaller{" "} + +
- Launch CKAN dev instances within minutes + Launch CKAN dev instances within minutes.

ckan-devstaller is a command-line tool to automate installing CKAN for @@ -81,7 +119,7 @@ function Hero() { buttonVariants({ size: "lg", className: "rounded-full" }), )} > - Getting Started + Get Started +

+
+

ckan-devstaller command

+ +
+              ./ckan-devstaller
+            
+
+
+
+
+

Configuration options

+

Presets

+ + } + title="CKAN-only" + > + Installs CKAN with ckan-compose. + + } title="CKAN and the DataStore extension"> + Installs CKAN and the DataStore extension. + + } + title="CKAN, DataStore, ckanext-scheming, and DataPusher+ extensions" + > + Installs CKAN, the DataStore extension, the ckanext-scheming + extension, and the DataPusher+ extension. + + +

CKAN version

+ + } title="2.11.3"> + } title="2.10.8"> + } title="Other"> + +

SSH capability

+ + } title="Enable SSH"> + Installs openssh-server and net-tools. + + +

CKAN extensions

+ + } title="ckanext-scheming"> + } title="ckanext-gztr"> + } title="DataStore"> + } title="DataPusher+"> + } title="ckanext-spatial"> + +
+
+ ); +} diff --git a/docs/components/codeblock.tsx b/docs/components/codeblock.tsx new file mode 100644 index 0000000..bb23807 --- /dev/null +++ b/docs/components/codeblock.tsx @@ -0,0 +1,261 @@ +'use client'; +import { Check, Clipboard } from 'lucide-react'; +import { + type ComponentProps, + createContext, + type HTMLAttributes, + type ReactNode, + type RefObject, + useContext, + useMemo, + useRef, +} from 'react'; +import { cn } from '../lib/cn'; +import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button'; +import { buttonVariants } from './ui/button'; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from './tabs.unstyled'; +import { mergeRefs } from '../lib/merge-refs'; + +export interface CodeBlockProps extends ComponentProps<'figure'> { + /** + * Icon of code block + * + * When passed as a string, it assumes the value is the HTML of icon + */ + icon?: ReactNode; + + /** + * Allow to copy code with copy button + * + * @defaultValue true + */ + allowCopy?: boolean; + + /** + * Keep original background color generated by Shiki or Rehype Code + * + * @defaultValue false + */ + keepBackground?: boolean; + + viewportProps?: HTMLAttributes; + + /** + * show line numbers + */ + 'data-line-numbers'?: boolean; + + /** + * @defaultValue 1 + */ + 'data-line-numbers-start'?: number; + + Actions?: (props: { className?: string; children?: ReactNode }) => ReactNode; +} + +const TabsContext = createContext<{ + containerRef: RefObject; + nested: boolean; +} | null>(null); + +export function Pre(props: ComponentProps<'pre'>) { + return ( +
+      {props.children}
+    
+ ); +} + +export function CodeBlock({ + ref, + title, + allowCopy = true, + keepBackground = false, + icon, + viewportProps = {}, + children, + Actions = (props) => ( +
+ ), + ...props +}: CodeBlockProps) { + const inTab = useContext(TabsContext) !== null; + const areaRef = useRef(null); + + return ( +
+ {title ? ( +
+ {typeof icon === 'string' ? ( +
+ ) : ( + icon + )} +
{title}
+ {Actions({ + className: '-me-2', + children: allowCopy && , + })} +
+ ) : ( + Actions({ + className: + 'absolute top-2 right-2 z-2 backdrop-blur-lg rounded-lg text-fd-muted-foreground', + children: allowCopy && , + }) + )} +
+ {children} +
+
+ ); +} + +function CopyButton({ + className, + containerRef, + ...props +}: ComponentProps<'button'> & { + containerRef: RefObject; +}) { + const [checked, onClick] = useCopyButton(() => { + const pre = containerRef.current?.getElementsByTagName('pre').item(0); + if (!pre) return; + + const clone = pre.cloneNode(true) as HTMLElement; + clone.querySelectorAll('.nd-copy-ignore').forEach((node) => { + node.replaceWith('\n'); + }); + + void navigator.clipboard.writeText(clone.textContent ?? ''); + }); + + return ( + + ); +} + +export function CodeBlockTabs({ ref, ...props }: ComponentProps) { + const containerRef = useRef(null); + const nested = useContext(TabsContext) !== null; + + return ( + + ({ + containerRef, + nested, + }), + [nested], + )} + > + {props.children} + + + ); +} + +export function CodeBlockTabsList(props: ComponentProps) { + return ( + + {props.children} + + ); +} + +export function CodeBlockTabsTrigger({ + children, + ...props +}: ComponentProps) { + return ( + +
+ {children} + + ); +} + +// TODO: currently Vite RSC plugin has problem with `asChild` due to children is automatically wrapped in , maybe revisit this in future +export function CodeBlockTab(props: ComponentProps) { + return ; +} diff --git a/docs/components/tabs.tsx b/docs/components/tabs.tsx new file mode 100644 index 0000000..54c41a3 --- /dev/null +++ b/docs/components/tabs.tsx @@ -0,0 +1,202 @@ +'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 './tabs.unstyled'; + +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 const TabsList = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); +TabsList.displayName = 'TabsList'; + +export const TabsTrigger = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); +TabsTrigger.displayName = 'TabsTrigger'; + +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/docs/components/tabs.unstyled.tsx b/docs/components/tabs.unstyled.tsx new file mode 100644 index 0000000..783095f --- /dev/null +++ b/docs/components/tabs.unstyled.tsx @@ -0,0 +1,163 @@ +'use client'; + +import { + type ComponentProps, + createContext, + useContext, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react'; +import * as Primitive from '@radix-ui/react-tabs'; +import { mergeRefs } from '../lib/merge-refs'; +import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event'; + +type ChangeListener = (v: string) => void; +const listeners = new Map(); + +function addChangeListener(id: string, listener: ChangeListener): void { + const list = listeners.get(id) ?? []; + list.push(listener); + listeners.set(id, list); +} + +function removeChangeListener(id: string, listener: ChangeListener): void { + const list = listeners.get(id) ?? []; + listeners.set( + id, + list.filter((item) => item !== listener), + ); +} + +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 = useContext(TabsContext); + if (!ctx) throw new Error('You must wrap your component in '); + return ctx; +} + +export const TabsList = Primitive.TabsList; + +export const TabsTrigger = Primitive.TabsTrigger; + +/** + * @internal You better not use it + */ +export function Tabs({ + ref, + groupId, + persist = false, + updateAnchor = false, + defaultValue, + value: _value, + onValueChange: _onValueChange, + ...props +}: TabsProps) { + const tabsRef = useRef(null); + const [value, setValue] = + _value === undefined + ? // eslint-disable-next-line react-hooks/rules-of-hooks -- not supposed to change controlled/uncontrolled + useState(defaultValue) + : [_value, _onValueChange ?? (() => undefined)]; + + const onChange = useEffectEvent((v: string) => setValue(v)); + const valueToIdMap = useMemo(() => new Map(), []); + + useLayoutEffect(() => { + if (!groupId) return; + const previous = persist + ? localStorage.getItem(groupId) + : sessionStorage.getItem(groupId); + + if (previous) onChange(previous); + addChangeListener(groupId, onChange); + return () => { + removeChangeListener(groupId, onChange); + }; + }, [groupId, persist]); + + useLayoutEffect(() => { + const hash = window.location.hash.slice(1); + if (!hash) return; + + for (const [value, id] of valueToIdMap.entries()) { + if (id === hash) { + onChange(value); + tabsRef.current?.scrollIntoView(); + break; + } + } + }, [valueToIdMap]); + + return ( + { + if (updateAnchor) { + const id = valueToIdMap.get(v); + + if (id) { + window.history.replaceState(null, '', `#${id}`); + } + } + + if (groupId) { + listeners.get(groupId)?.forEach((item) => { + item(v); + }); + + if (persist) localStorage.setItem(groupId, v); + else sessionStorage.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/docs/content/docs/builder.mdx b/docs/content/docs/builder.mdx new file mode 100644 index 0000000..e0704a0 --- /dev/null +++ b/docs/content/docs/builder.mdx @@ -0,0 +1,8 @@ +--- +title: Builder +description: Customize your CKAN installation before running ckan-devstaller. +--- + +import Builder from "@/components/builder"; + + diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx index 4d61230..f0abbcd 100644 --- a/docs/content/docs/index.mdx +++ b/docs/content/docs/index.mdx @@ -1,11 +1,11 @@ --- title: Quick Start -description: Getting Started with ckan-devstaller +description: Get started with ckan-devstaller and install CKAN within minutes. --- ckan-devstaller attempts to install a CKAN instance from source along with [ckan-compose](https://github.com/tino097/ckan-compose) and other optional features, intended for development use in a new Ubuntu 22.04 instance. -Make sure `ckan-devstaller` is run in a **new** Ubuntu 22.04 instanceof. Do NOT run `ckan-devstaller` in an existing instance that is important for your usage. +Make sure `ckan-devstaller` is run in a **new** Ubuntu 22.04 instance. Do NOT run `ckan-devstaller` in an existing instance that is important for your usage. import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; @@ -40,11 +40,18 @@ wget -O - https://github.com/dathere/ckan-devstaller/releases/download/0.2.1/ins ## Learn more -import { BlocksIcon, GitMergeIcon, Trash2Icon } from 'lucide-react'; +import { BlocksIcon, HomeIcon, GitMergeIcon, Trash2Icon } from 'lucide-react'; } + href="/docs/builder" + title="Builder" + > + Customize your installation with an interactive web GUI + + } href="/docs/reference/installation-architecture" title="Installation architecture" > diff --git a/docs/package.json b/docs/package.json index 7d3909d..812b351 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,6 +13,7 @@ "dependencies": { "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "fumadocs-core": "15.8.1", "fumadocs-mdx": "12.0.1", @@ -24,14 +25,14 @@ "tailwind-merge": "^3.3.1" }, "devDependencies": { + "@biomejs/biome": "2.2.5", + "@tailwindcss/postcss": "^4.1.13", + "@types/mdx": "^2.0.13", "@types/node": "24.5.2", "@types/react": "^19.1.14", "@types/react-dom": "^19.1.9", - "typescript": "^5.9.2", - "@types/mdx": "^2.0.13", - "@tailwindcss/postcss": "^4.1.13", - "tailwindcss": "^4.1.13", "postcss": "^8.5.6", - "@biomejs/biome": "^2.2.4" + "tailwindcss": "^4.1.13", + "typescript": "^5.9.2" } } \ No newline at end of file