Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist
public/llms.txt
public/llms-full.txt
22 changes: 22 additions & 0 deletions apps/docs/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# ConnectFX Docs App

## Purpose

`apps/docs` is the FoldKit documentation shell for ConnectFX. It is a static Vite app; framework behavior stays in `packages/`.

## Rules

- Keep the app as a presentation shell over `src/content/registry.ts`.
- Source prose from `docs/spec/`, `CONTEXT.md`, and `SPEC.md`. ConnectFX is specification-first and pre-implementation; pages must describe the specified contracts, not invent shipped APIs.
- Code examples illustrate the specified SDK shape. Snippets that import unimplemented `@connectfx/*` packages live in `src/snippets/` as illustrative `.txt`-style sources and are not executed.
- Do not add runtime behavior, persistence, provider calls, or host-product semantics here.
- Add FoldCN components through the local `../foldcn` registry and keep the generated `components.json` configuration.
- Do not use `Date.now`; use fixed timestamps in examples.

## Verification

```bash
bun run --cwd apps/docs test
bun run --cwd apps/docs typecheck
bun run --cwd apps/docs build
```
9 changes: 9 additions & 0 deletions apps/docs/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://foldcn.dev/schema/components.json",
"css": "src/styles.css",
"baseColor": "neutral",
"aliases": {
"ui": "src/components/ui",
"lib": "src/lib"
}
}
31 changes: 31 additions & 0 deletions apps/docs/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Connectfx</title>
<meta
name="description"
content="Connectfx is an embedded-first, Effect-native framework for durable external-system Connections. Connections, credentials, grants, health, event subscriptions, and managed-resource reconciliation compose into your own Effect application as layers."
/>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta property="og:title" content="Connectfx" />
<meta
property="og:description"
content="Connectfx is an embedded-first, Effect-native framework for durable external-system Connections. Connections, credentials, grants, health, event subscriptions, and managed-resource reconciliation compose into your own Effect application as layers."
/>
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Connectfx" />
<meta
name="twitter:description"
content="Connectfx is an embedded-first, Effect-native framework for durable external-system Connections. Connections, credentials, grants, health, event subscriptions, and managed-resource reconciliation compose into your own Effect application as layers."
/>
<script src="/theme-init.js"></script>
<link href="/src/styles.css" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/entry.ts"></script>
</body>
</html>
34 changes: 34 additions & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@connectfx/docs",
"version": "0.0.0",
"private": true,
"type": "module",
"packageManager": "bun@1.3.14",
"scripts": {
"build": "bun run generate:llms && vite build",
"dev": "vite",
"generate:llms": "bun --preload ./scripts/raw-plugin.ts scripts/generate-llms.ts",
"test": "vitest run --passWithNoTests && bun run check:snippets",
"check:snippets": "bun scripts/check-snippets.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@connectfx/core": "workspace:*",
"@foldkit/ui": "catalog:",
"class-variance-authority": "catalog:",
"clsx": "catalog:",
"effect": "catalog:",
"foldkit": "catalog:",
"tailwind-merge": "catalog:"
},
"devDependencies": {
"@foldkit/vite-plugin": "catalog:",
"@tailwindcss/vite": "catalog:",
"happy-dom": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vitest": "catalog:"
}
}
5 changes: 5 additions & 0 deletions apps/docs/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/docs/public/fonts/InterVariable.woff2
Binary file not shown.
Binary file not shown.
6 changes: 6 additions & 0 deletions apps/docs/public/theme-init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
;(function () {
var preference = localStorage.getItem("theme-preference")
var theme = preference ? JSON.parse(preference) : "System"
var isDark = theme === "Dark" || (theme === "System" && window.matchMedia("(prefers-color-scheme: dark)").matches)
if (isDark) document.documentElement.classList.add("dark")
})()
12 changes: 12 additions & 0 deletions apps/docs/railway.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://railway.com/railway.schema.json",
"build": {
"builder": "RAILPACK",
"buildCommand": "bun install --frozen-lockfile && bun turbo build --filter=@connectfx/docs"
},
"deploy": {
"healthcheckPath": "/",
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
}
}
32 changes: 32 additions & 0 deletions apps/docs/scripts/check-snippets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Glob } from "bun"
import { existsSync } from "node:fs"
import { dirname, join, relative } from "node:path"

const snippetsRoot = join(import.meta.dir, "..", "src", "snippets")
const failures: Array<string> = []
let checked = 0

if (!existsSync(snippetsRoot)) {
console.log("check-snippets: 0 runnable snippets verified")
process.exit(0)
}

for await (const expectedPath of new Glob("**/*.expected.txt").scan({ cwd: snippetsRoot, absolute: true })) {
const snippetPath = expectedPath.replace(/\.expected\.txt$/, ".ts")
const label = relative(snippetsRoot, snippetPath)
if (!(await Bun.file(snippetPath).exists())) {
failures.push(`${label}: expected-output file has no sibling snippet`)
continue
}
const expected = (await Bun.file(expectedPath).text()).trimEnd()
const run = Bun.spawnSync(["bun", snippetPath], { cwd: dirname(snippetPath), stdout: "pipe", stderr: "pipe" })
checked += 1
if (run.exitCode !== 0) failures.push(`${label}: exited ${run.exitCode}\n${run.stderr.toString().trimEnd()}`)
else if (run.stdout.toString().trimEnd() !== expected) failures.push(`${label}: stdout drifted`)
}

if (failures.length > 0) {
console.error(failures.join("\n\n"))
process.exit(1)
}
console.log(`check-snippets: ${checked} runnable snippets verified`)
4 changes: 4 additions & 0 deletions apps/docs/scripts/generate-llms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { llmsFull, llmsIndex } from "../src/content/registry"

await Bun.write(new URL("../public/llms.txt", import.meta.url), `${llmsIndex()}\n`)
await Bun.write(new URL("../public/llms-full.txt", import.meta.url), `${llmsFull()}\n`)
16 changes: 16 additions & 0 deletions apps/docs/scripts/raw-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { plugin } from "bun"
import { dirname, resolve } from "node:path"

plugin({
name: "raw-imports",
setup(build) {
build.onResolve({ filter: /\?raw$/ }, (args) => ({
path: resolve(dirname(args.importer), args.path.slice(0, -"?raw".length)),
namespace: "raw",
}))
build.onLoad({ filter: /.*/, namespace: "raw" }, async (args) => ({
contents: `export default ${JSON.stringify(await Bun.file(args.path).text())}`,
loader: "js",
}))
},
})
41 changes: 41 additions & 0 deletions apps/docs/src/app/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Option } from "effect"
import { Runtime } from "foldkit"
import { Url } from "foldkit/url"

import * as Dialog from "@/components/ui/dialog"
import * as Sheet from "@/components/ui/sheet"

import { legacyRedirects } from "../content/registry"
import { toPath, urlToRoute } from "../route/route"
import type { Message } from "./message"
import type { Model } from "./model"
import { initialSearchCommand } from "./searchPalette"
import { LoadSidebarGroups, LoadThemePreference, RedirectLegacy } from "./update"

export const init: Runtime.RoutingApplicationInit<Model, Message> = (url: Url) => {
const route = urlToRoute(url)
const redirectTarget = legacyRedirects.get(toPath(route))
return [
{
route,
url,
searchDialog: Dialog.init({
id: "search-dialog",
isAnimated: false,
focusSelector: '[data-slot="command-input"]',
}),
searchCommand: initialSearchCommand(),
mobileNavigation: Sheet.init({ id: "mobile-navigation", isAnimated: true }),
copiedCode: Option.none(),
themePreference: "System",
maybeActiveSectionId: Option.none(),
openSidebarGroups: {},
isMobileTocOpen: false,
},
[
LoadThemePreference(),
LoadSidebarGroups(),
...(redirectTarget === undefined ? [] : [RedirectLegacy({ url: redirectTarget })]),
],
]
}
62 changes: 62 additions & 0 deletions apps/docs/src/app/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Schema } from "effect"
import { m } from "foldkit/message"
import { UrlRequest } from "foldkit/navigation"
import { Url } from "foldkit/url"

import * as Command from "@/components/ui/command"
import * as Dialog from "@/components/ui/dialog"
import * as Sheet from "@/components/ui/sheet"

import { SidebarGroups } from "../layout/sidebarStorage"
import { ThemePreference } from "./model"

export const ClickedLink = m("ClickedLink", { request: UrlRequest })
export const ChangedUrl = m("ChangedUrl", { url: Url })
export const CompletedNavigateInternal = m("CompletedNavigateInternal")
export const CompletedLoadExternal = m("CompletedLoadExternal")
export const PressedSearchShortcut = m("PressedSearchShortcut")
export const GotSearchDialogMessage = m("GotSearchDialogMessage", { message: Dialog.Message })
export const GotSearchCommandMessage = m("GotSearchCommandMessage", { message: Command.Message })
export const GotMobileNavigationMessage = m("GotMobileNavigationMessage", { message: Sheet.Message })
export const ClickedCopyCode = m("ClickedCopyCode", { source: Schema.String })
export const CompletedCopyCode = m("CompletedCopyCode", { source: Schema.String })
export const ClearedCopiedCode = m("ClearedCopiedCode", { source: Schema.String })
export const SelectedThemePreference = m("SelectedThemePreference", { preference: ThemePreference })
export const GotThemePreference = m("GotThemePreference", { preference: ThemePreference })
export const CompletedApplyTheme = m("CompletedApplyTheme")
export const CompletedSaveThemePreference = m("CompletedSaveThemePreference")
export const ChangedSystemTheme = m("ChangedSystemTheme", { theme: Schema.Literals(["Light", "Dark"]) })
export const ToggledSidebarGroup = m("ToggledSidebarGroup", { group: Schema.String })
export const GotSidebarGroups = m("GotSidebarGroups", { open: SidebarGroups })
export const CompletedSaveSidebarGroups = m("CompletedSaveSidebarGroups")
export const ChangedActiveSection = m("ChangedActiveSection", { sectionId: Schema.String })
export const ToggledMobileTableOfContents = m("ToggledMobileTableOfContents", { isOpen: Schema.Boolean })
export const ClickedMobileTableOfContentsLink = m("ClickedMobileTableOfContentsLink", { sectionId: Schema.String })
export const ClickedMobileNavigationTrigger = m("ClickedMobileNavigationTrigger")

export const Message = Schema.Union([
ClickedLink,
ChangedUrl,
CompletedNavigateInternal,
CompletedLoadExternal,
PressedSearchShortcut,
GotSearchDialogMessage,
GotSearchCommandMessage,
GotMobileNavigationMessage,
ClickedCopyCode,
CompletedCopyCode,
ClearedCopiedCode,
SelectedThemePreference,
GotThemePreference,
CompletedApplyTheme,
CompletedSaveThemePreference,
ChangedSystemTheme,
ToggledSidebarGroup,
GotSidebarGroups,
CompletedSaveSidebarGroups,
ChangedActiveSection,
ToggledMobileTableOfContents,
ClickedMobileTableOfContentsLink,
ClickedMobileNavigationTrigger,
])
export type Message = typeof Message.Type
25 changes: 25 additions & 0 deletions apps/docs/src/app/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Schema } from "effect"
import { Url } from "foldkit/url"

import * as Command from "@/components/ui/command"
import * as Dialog from "@/components/ui/dialog"
import * as Sheet from "@/components/ui/sheet"

import { SidebarGroups } from "../layout/sidebarStorage"
import { Route } from "../route/route"

export const ThemePreference = Schema.Literals(["Light", "System", "Dark"])
export type ThemePreference = typeof ThemePreference.Type
export const Model = Schema.Struct({
route: Route,
url: Url,
searchDialog: Dialog.Model,
searchCommand: Command.Model,
mobileNavigation: Sheet.Model,
copiedCode: Schema.Option(Schema.String),
themePreference: ThemePreference,
maybeActiveSectionId: Schema.Option(Schema.String),
openSidebarGroups: SidebarGroups,
isMobileTocOpen: Schema.Boolean,
})
export type Model = typeof Model.Type
9 changes: 9 additions & 0 deletions apps/docs/src/app/searchPalette.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Command from "@/components/ui/command"

import { defaultDocsPath, searchDocs, type SearchResult } from "../content/registry"

export type SearchItem = string
export const itemToPath = (item: SearchItem): string => (item.startsWith("/") ? item : defaultDocsPath)
export const SearchCommand: ReturnType<typeof Command.create<SearchItem>> = Command.create<SearchItem>()
export const initialSearchCommand = (): Command.Model => Command.init({ id: "search-command", isAnimated: false })
export const searchResultsFor = (inputValue: string): ReadonlyArray<SearchResult> => searchDocs(inputValue)
Loading
Loading