Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6f5f0b649 | |||
| 02df33ce6b | |||
| eebf29c3d5 | |||
| c519d4e74b |
@@ -1,22 +1,139 @@
|
||||
# Simple React App Kit
|
||||
|
||||
Simple React App Kit is a reusable React application kit built with TanStack
|
||||
Start, shadcn/ui, and workspace packages.
|
||||
Simple React App Kit is a production-oriented React workspace for building application interfaces without assembling the foundation from scratch. It includes a runnable TanStack Start application, shared shadcn/ui components, feature packages, internationalization, documentation, and tested integration patterns.
|
||||
|
||||
## Adding components
|
||||
[Open the documentation](https://react-app-kit.go-slim.dev) · [English documentation](https://react-app-kit.go-slim.dev/en-US/)
|
||||
|
||||
To add components to your app, run the following command at the root of your `web` app:
|
||||
## What you get
|
||||
|
||||
```bash
|
||||
pnpm dlx shadcn@latest add button -c apps/web
|
||||
- A TanStack Start application with routing, SSR, responsive layout, and light/dark themes.
|
||||
- Shared shadcn/ui primitives that remain editable source code.
|
||||
- Application-level navigation, notifications, chat, and appearance blocks.
|
||||
- Adapter-based Media and Search packages that do not prescribe a backend.
|
||||
- A composable Lexical editor package.
|
||||
- Type-safe application preferences and an internationalization workflow.
|
||||
- A bilingual MDX documentation site with SSG, search, and Cloudflare deployment.
|
||||
|
||||
## Quick start
|
||||
|
||||
Requirements:
|
||||
|
||||
- [Bun](https://bun.com/) 1.3.5 or newer
|
||||
- Node.js 22.19 or newer
|
||||
|
||||
Install dependencies and start the example application:
|
||||
|
||||
```sh
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
|
||||
This will place the ui components in the `packages/ui/src/components` directory.
|
||||
The development server prints the local URL when it is ready.
|
||||
|
||||
## Using components
|
||||
To run the documentation site separately:
|
||||
|
||||
To use the components in your app, import them from the `ui` package.
|
||||
```sh
|
||||
bun run --cwd docs dev
|
||||
```
|
||||
|
||||
## Workspace packages
|
||||
|
||||
The packages are private workspace packages by default. Use them inside this repository with `workspace:*`, or adapt and publish them under your own scope.
|
||||
|
||||
| Package | Use it for |
|
||||
| --- | --- |
|
||||
| `@workspace/ui` | Shared components, hooks, icons, design tokens, and global styles |
|
||||
| `@workspace/blocks` | Application shell, navigation, notifications, chat, and appearance controls |
|
||||
| `@workspace/media` | Media browsing, uploads, folders, asset selection, and storage adapters |
|
||||
| `@workspace/search` | Application search, grouped results, pagination, and search history |
|
||||
| `@workspace/lexical` | Rich-text editing, actions, media nodes, and editor presets |
|
||||
| `@workspace/i18n` | Locale runtime, catalogs, extraction, compilation, and development tools |
|
||||
| `@workspace/preferences` | Typed external state, persistence callbacks, SSR snapshots, and effects |
|
||||
|
||||
The intended boundary is simple: your application owns APIs, persistence, routing decisions, and business rules; workspace packages own reusable presentation and interaction behavior.
|
||||
|
||||
## Using a package
|
||||
|
||||
Add a workspace dependency to the consuming application:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@workspace/media": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Import the package stylesheet once when it provides one. This also lets Tailwind discover the utilities used by that package:
|
||||
|
||||
```css
|
||||
@import "@workspace/ui/globals.css";
|
||||
@import "@workspace/media/globals.css";
|
||||
```
|
||||
|
||||
Then import only the public API you need:
|
||||
|
||||
```tsx
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import {
|
||||
MediaLibrary,
|
||||
MediaProvider,
|
||||
type MediaAdapter,
|
||||
} from "@workspace/media"
|
||||
|
||||
export function MediaPage({ adapter }: { adapter: MediaAdapter }) {
|
||||
return (
|
||||
<MediaProvider adapter={adapter}>
|
||||
<MediaLibrary />
|
||||
</MediaProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Package-specific setup, adapters, locale catalogs, and progressive examples are documented on the [documentation site](https://react-app-kit.go-slim.dev).
|
||||
|
||||
## Adding shadcn/ui components
|
||||
|
||||
Run the shadcn CLI from the repository root:
|
||||
|
||||
```sh
|
||||
bunx shadcn@latest add button -c apps/web
|
||||
```
|
||||
|
||||
The workspace aliases place generated components in `packages/ui/src/components`. Import them through the UI package:
|
||||
|
||||
```tsx
|
||||
import { Button } from "@workspace/ui/components/button"
|
||||
```
|
||||
|
||||
Generated components belong to the repository and can be changed to match the product instead of being treated as an opaque dependency.
|
||||
|
||||
## Common commands
|
||||
|
||||
| Command | Purpose |
|
||||
| --- | --- |
|
||||
| `bun run dev` | Start the example web application |
|
||||
| `bun run build` | Build the web application |
|
||||
| `bun run check` | Check formatting, lint, and all workspace types |
|
||||
| `bun run format` | Format application and package source files |
|
||||
| `bun run lint` | Run Oxlint across the repository |
|
||||
| `bun run typecheck` | Type-check every workspace |
|
||||
| `bun run --cwd packages/media test` | Run tests for one package |
|
||||
| `bun run --cwd docs build` | Generate the static documentation site |
|
||||
| `bun run --cwd docs deploy` | Build and deploy the docs to Cloudflare |
|
||||
|
||||
## Project structure
|
||||
|
||||
```text
|
||||
apps/web/ Runnable TanStack Start application
|
||||
docs/ Documentation application and SSG pipeline
|
||||
packages/
|
||||
blocks/ Application-level interface blocks
|
||||
i18n/ Internationalization runtime and tooling
|
||||
lexical/ Rich-text editor
|
||||
media/ Media library and picker
|
||||
preferences/ Typed preference state
|
||||
search/ Application search
|
||||
ui/ Shared shadcn/ui components
|
||||
```
|
||||
|
||||
Documentation for each package lives beside its source under `packages/<name>/docs`, so examples and APIs evolve with the implementation.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"@workspace/blocks/appearance/locales/{locale}",
|
||||
"@workspace/blocks/chats/locales/{locale}",
|
||||
"@workspace/blocks/layout/locales/{locale}",
|
||||
"@workspace/blocks/media/locales/{locale}",
|
||||
"@workspace/media/locales/{locale}",
|
||||
"@workspace/blocks/navigation/locales/{locale}",
|
||||
"@workspace/blocks/notifications/locales/{locale}",
|
||||
"@workspace/search/locales/{locale}",
|
||||
@@ -16,6 +16,7 @@
|
||||
"include": [
|
||||
"src",
|
||||
"../../packages/blocks/src",
|
||||
"../../packages/media/src",
|
||||
"../../packages/search/src",
|
||||
"../../packages/ui/src"
|
||||
],
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@workspace/blocks": "workspace:*",
|
||||
"@workspace/i18n": "workspace:*",
|
||||
"@workspace/lexical": "workspace:*",
|
||||
"@workspace/media": "workspace:*",
|
||||
"@workspace/search": "workspace:*",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"react": "^19.2.6",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import "@workspace/ui/globals.css";
|
||||
@import "@workspace/blocks/globals.css";
|
||||
@import "@workspace/lexical/globals.css";
|
||||
@import "@workspace/media/globals.css";
|
||||
@import "@workspace/search/globals.css";
|
||||
|
||||
:root {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"@workspace/blocks": "workspace:*",
|
||||
"@workspace/i18n": "workspace:*",
|
||||
"@workspace/lexical": "workspace:*",
|
||||
"@workspace/media": "workspace:*",
|
||||
"@workspace/search": "workspace:*",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"react": "^19.2.6",
|
||||
@@ -55,6 +56,7 @@
|
||||
"dependencies": {
|
||||
"@icones/react": "^0.0.1",
|
||||
"@workspace/i18n": "workspace:*",
|
||||
"@workspace/media": "workspace:*",
|
||||
"@workspace/preferences": "workspace:*",
|
||||
"@workspace/search": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -165,6 +167,24 @@
|
||||
"vitest": "^4.1.10",
|
||||
},
|
||||
},
|
||||
"packages/media": {
|
||||
"name": "@workspace/media",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@workspace/i18n": "workspace:*",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"react": "^19.2.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"jsdom": "^30.0.1",
|
||||
"react-dom": "^19.2.6",
|
||||
"typescript": "~6",
|
||||
"vitest": "^4.1.10",
|
||||
},
|
||||
},
|
||||
"packages/preferences": {
|
||||
"name": "@workspace/preferences",
|
||||
"version": "0.0.0",
|
||||
@@ -985,6 +1005,8 @@
|
||||
|
||||
"@workspace/lexical": ["@workspace/lexical@workspace:packages/lexical"],
|
||||
|
||||
"@workspace/media": ["@workspace/media@workspace:packages/media"],
|
||||
|
||||
"@workspace/preferences": ["@workspace/preferences@workspace:packages/preferences"],
|
||||
|
||||
"@workspace/search": ["@workspace/search@workspace:packages/search"],
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>my-shadcn-ui Documentation</title>
|
||||
<title>Simple React App Kit Documentation</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"@icones/react": "^0.0.1",
|
||||
"@workspace/i18n": "workspace:*",
|
||||
"@workspace/media": "workspace:*",
|
||||
"@workspace/preferences": "workspace:*",
|
||||
"@workspace/search": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -31,8 +31,8 @@ const packageDefinitions: readonly PackageDefinition[] = [
|
||||
key: "blocks",
|
||||
label: "Blocks",
|
||||
description: {
|
||||
zh: "构建应用外壳,并接入导航、通知、聊天、媒体和外观控制。",
|
||||
us: "Build an application shell and add navigation, notifications, chat, media, and appearance controls.",
|
||||
zh: "构建应用外壳,并接入导航、通知、聊天和外观控制。",
|
||||
us: "Build an application shell and add navigation, notifications, chat, and appearance controls.",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -51,6 +51,14 @@ const packageDefinitions: readonly PackageDefinition[] = [
|
||||
us: "Build a rich-text editor progressively from its root and content surface to custom actions and media.",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "media",
|
||||
label: "Media",
|
||||
description: {
|
||||
zh: "通过存储 Adapter 构建媒体浏览、上传、目录管理和资源选择流程。",
|
||||
us: "Build media browsing, uploads, folder management, and asset selection with a storage adapter.",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "preferences",
|
||||
label: "Preferences",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import "@workspace/ui/globals.css";
|
||||
@import "@workspace/media/globals.css";
|
||||
@import "@workspace/search/globals.css";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@@ -10,9 +10,20 @@ export default function IndexPage() {
|
||||
return (
|
||||
<>
|
||||
<Container className="flex items-center justify-between gap-4 pt-8 pb-10 sm:pt-12 sm:pb-14 lg:pb-16">
|
||||
<h1 className="min-w-0 text-2xl font-semibold capitalize sm:text-3xl">
|
||||
my-shadcn-ui
|
||||
<h1 className="min-w-0 text-2xl font-semibold sm:text-3xl">
|
||||
<span>S</span>
|
||||
<span className="text-slate-300 dark:text-slate-400">imple</span>
|
||||
<span> </span>
|
||||
<span>R</span>
|
||||
<span className="text-slate-300 dark:text-slate-400">eact</span>
|
||||
<span> </span>
|
||||
<span>A</span>
|
||||
<span className="text-slate-300 dark:text-slate-400">pp</span>
|
||||
<span> </span>
|
||||
<span>K</span>
|
||||
<span className="text-slate-300 dark:text-slate-400">it</span>
|
||||
</h1>
|
||||
<span className="text-xs align-bottom">alpha</span>
|
||||
<Actions className="ml-auto" />
|
||||
</Container>
|
||||
<Container
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
title: Media library
|
||||
description: Supply a storage adapter and add browsing, upload, folders, and asset selection.
|
||||
order: 21
|
||||
toc:
|
||||
- id: implement-the-adapter
|
||||
title: Implement the adapter
|
||||
- id: provide-media
|
||||
title: Provide media
|
||||
- id: selection-flows
|
||||
title: Selection flows
|
||||
---
|
||||
|
||||
## Implement the adapter {#implement-the-adapter}
|
||||
|
||||
`MediaAdapter` is the media feature's complete persistence boundary. Implement its query, upload, folder, update, and delete operations with the product's storage service.
|
||||
|
||||
## Provide media {#provide-media}
|
||||
|
||||
Mount the adapter once around the media surfaces:
|
||||
|
||||
```tsx
|
||||
import { MediaLibrary, MediaProvider } from "@workspace/blocks/media"
|
||||
|
||||
;<MediaProvider adapter={mediaAdapter} notify={showNotice}>
|
||||
<MediaLibrary />
|
||||
</MediaProvider>
|
||||
```
|
||||
|
||||
The optional `notify` callback translates operational results into the application's toast or notification system.
|
||||
|
||||
## Selection flows {#selection-flows}
|
||||
|
||||
Use `MediaPickerDialog` for single or multiple asset selection. `readMediaDimensions`, `formatMediaFileSize`, and `defaultMediaReference` cover the common presentation work around uploads and previews.
|
||||
@@ -33,7 +33,7 @@ Import the block stylesheet once from the application entry:
|
||||
import "@workspace/blocks/globals.css"
|
||||
```
|
||||
|
||||
Keep feature imports on their explicit subpaths. For example, importing `@workspace/blocks/media` does not pull chat into the same module graph.
|
||||
Keep feature imports on their explicit subpaths. For example, importing `@workspace/blocks/notifications` does not pull chat into the same module graph. Media workflows are provided separately by `@workspace/media`.
|
||||
|
||||
## Application providers {#application-providers}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ toc:
|
||||
|
||||
## What you will build {#what-you-will-build}
|
||||
|
||||
This tutorial builds an application shell step by step. You will begin with the shared layout, connect navigation, then add notifications, chat, media, and appearance preferences.
|
||||
This tutorial builds an application shell step by step. You will begin with the shared layout, connect navigation, then add notifications, chat, and appearance preferences.
|
||||
|
||||
By the end, the application owns its data and business rules while `@workspace/blocks` owns the reusable presentation and interaction patterns.
|
||||
|
||||
@@ -29,5 +29,5 @@ Blocks sit above `@workspace/ui`. They combine low-level components into feature
|
||||
2. Create the application shell.
|
||||
3. Define navigation and route state.
|
||||
4. Connect notifications.
|
||||
5. Add chat and media workflows.
|
||||
5. Add chat workflows.
|
||||
6. Finish with persistent appearance controls.
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
title: 媒体库
|
||||
description: 提供存储 Adapter,并加入浏览、上传、文件夹和资源选择能力。
|
||||
order: 21
|
||||
toc:
|
||||
- id: implement-the-adapter
|
||||
title: 实现 Adapter
|
||||
- id: provide-media
|
||||
title: 提供媒体能力
|
||||
- id: selection-flows
|
||||
title: 选择流程
|
||||
---
|
||||
|
||||
## 实现 Adapter {#implement-the-adapter}
|
||||
|
||||
`MediaAdapter` 是媒体功能完整的持久化边界。通过产品存储服务实现查询、上传、文件夹、更新和删除操作。
|
||||
|
||||
## 提供媒体能力 {#provide-media}
|
||||
|
||||
在媒体界面外挂载一次 Adapter:
|
||||
|
||||
```tsx
|
||||
import { MediaLibrary, MediaProvider } from "@workspace/blocks/media"
|
||||
|
||||
;<MediaProvider adapter={mediaAdapter} notify={showNotice}>
|
||||
<MediaLibrary />
|
||||
</MediaProvider>
|
||||
```
|
||||
|
||||
可选的 `notify` 回调把操作结果转换为应用的 Toast 或通知。
|
||||
|
||||
## 选择流程 {#selection-flows}
|
||||
|
||||
单选或多选资源使用 `MediaPickerDialog`。`readMediaDimensions`、`formatMediaFileSize` 和 `defaultMediaReference` 覆盖上传与预览相关的常用展示工作。
|
||||
@@ -33,7 +33,7 @@ Blocks 使用 `@workspace/ui`、React Query 和工作区国际化运行时。工
|
||||
import "@workspace/blocks/globals.css"
|
||||
```
|
||||
|
||||
功能模块应始终从显式子路径导入。例如,导入 `@workspace/blocks/media` 不会把聊天功能加入同一个模块图。
|
||||
功能模块应始终从显式子路径导入。例如,导入 `@workspace/blocks/notifications` 不会把聊天功能加入同一个模块图。媒体工作流由独立的 `@workspace/media` 提供。
|
||||
|
||||
## 应用 Providers {#application-providers}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ toc:
|
||||
|
||||
## 将要构建的内容 {#what-you-will-build}
|
||||
|
||||
本教程会逐步构建一个应用外壳。你将从共享布局开始,接入导航,再添加通知、聊天、媒体和外观偏好。
|
||||
本教程会逐步构建一个应用外壳。你将从共享布局开始,接入导航,再添加通知、聊天和外观偏好。
|
||||
|
||||
完成后,应用负责数据和业务规则,`@workspace/blocks` 负责可复用的展示与交互模式。
|
||||
|
||||
@@ -29,5 +29,5 @@ Blocks 位于 `@workspace/ui` 之上。它们将底层组件组合成完整功
|
||||
2. 创建应用外壳。
|
||||
3. 定义导航和路由状态。
|
||||
4. 接入通知。
|
||||
5. 添加聊天和媒体工作流。
|
||||
5. 添加聊天工作流。
|
||||
6. 使用持久化外观控件完成应用。
|
||||
|
||||
@@ -45,9 +45,6 @@
|
||||
"./layout/locales": "./src/blocks/layout/locales/catalogs.ts",
|
||||
"./layout/locales/*": "./src/blocks/layout/locales/*.ts",
|
||||
"./layouts/vega": "./src/blocks/layouts/vega/index.tsx",
|
||||
"./media": "./src/blocks/media/index.ts",
|
||||
"./media/locales": "./src/blocks/media/locales/catalogs.ts",
|
||||
"./media/locales/*": "./src/blocks/media/locales/*.ts",
|
||||
"./navigation": "./src/blocks/navigation/index.ts",
|
||||
"./navigation/locales": "./src/blocks/navigation/locales/catalogs.ts",
|
||||
"./navigation/locales/*": "./src/blocks/navigation/locales/*.ts",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { mediaMessages } from "../messages"
|
||||
import {
|
||||
blockCatalogLocales,
|
||||
type BlockCatalogLocale,
|
||||
type BlockMessageCatalog,
|
||||
} from "../../../i18n/catalogs"
|
||||
|
||||
export { blockCatalogLocales as mediaCatalogLocales }
|
||||
export type MediaCatalogLocale = BlockCatalogLocale
|
||||
export type MediaMessageCatalog = BlockMessageCatalog<typeof mediaMessages>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: Storage adapter
|
||||
description: Connect queries, uploads, mutations, URL resolution, and pagination to product infrastructure.
|
||||
order: 20
|
||||
toc:
|
||||
- id: implement-the-contract
|
||||
title: Implement the contract
|
||||
- id: separate-display-and-reference-urls
|
||||
title: Separate display and reference URLs
|
||||
- id: preserve-query-semantics
|
||||
title: Preserve query semantics
|
||||
---
|
||||
|
||||
## Implement the contract {#implement-the-contract}
|
||||
|
||||
`MediaAdapter` is the package's complete persistence boundary. Implement its asset queries, folder operations, uploads, updates, and deletes with the product's storage service.
|
||||
|
||||
```ts
|
||||
import type { MediaAdapter } from "@workspace/media"
|
||||
|
||||
export const mediaAdapter: MediaAdapter = {
|
||||
listAssets: (query) => api.media.list(query),
|
||||
listFolders: () => api.media.listFolders(),
|
||||
upload: (input) => api.media.upload(input),
|
||||
createFolder: (input) => api.media.createFolder(input),
|
||||
updateFolder: (input) => api.media.updateFolder(input),
|
||||
deleteFolder: (id) => api.media.deleteFolder(id),
|
||||
moveAsset: (id, folderId) => api.media.move(id, folderId),
|
||||
renameAsset: (id, name) => api.media.rename(id, name),
|
||||
updateFavorite: (id, favorite) => api.media.favorite(id, favorite),
|
||||
deleteAsset: (id) => api.media.delete(id),
|
||||
}
|
||||
```
|
||||
|
||||
## Separate display and reference URLs {#separate-display-and-reference-urls}
|
||||
|
||||
Use `resolveUrl` for short-lived signed URLs or image transformations used while rendering. Use `resolveReference` for the value returned by the picker and stored in another document. When `resolveReference` is absent, images use `defaultMediaReference`, which preserves intrinsic dimensions in the URL.
|
||||
|
||||
## Preserve query semantics {#preserve-query-semantics}
|
||||
|
||||
Treat `MediaAssetQuery` as an immutable request. Apply `allowedKinds`, `filter`, `folder`, and `keyword` before pagination, then return `hasNextPage` based on the filtered result. This keeps desktop and mobile filters consistent.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Asset picker
|
||||
description: Reuse the media library as a controlled single- or multiple-selection dialog.
|
||||
order: 30
|
||||
toc:
|
||||
- id: open-the-picker
|
||||
title: Open the picker
|
||||
- id: constrain-selection
|
||||
title: Constrain selection
|
||||
- id: consume-results
|
||||
title: Consume results
|
||||
---
|
||||
|
||||
## Open the picker {#open-the-picker}
|
||||
|
||||
`MediaPickerDialog` reuses the same adapter and folder model as the full library:
|
||||
|
||||
```tsx
|
||||
import { MediaPickerDialog } from "@workspace/media"
|
||||
|
||||
;<MediaPickerDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onConfirm={(assets) => editor.insertImages(assets)}
|
||||
/>
|
||||
```
|
||||
|
||||
## Constrain selection {#constrain-selection}
|
||||
|
||||
Pass `allowedKinds={["image"]}` for an image-only workflow. The dialog selects one asset by default; pass `multiple` when the consumer accepts several assets, and provide `initialSelection` when editing an existing value.
|
||||
|
||||
## Consume results {#consume-results}
|
||||
|
||||
The confirmation callback receives complete `MediaAsset` records. Store the adapter-provided reference rather than a temporary preview URL when the result must survive signed-URL expiration.
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: Installation and setup
|
||||
description: Add Media, register its catalog, and connect the storage adapter.
|
||||
order: 10
|
||||
toc:
|
||||
- id: add-the-package
|
||||
title: Add the package
|
||||
- id: register-styles-and-catalogs
|
||||
title: Register styles and catalogs
|
||||
- id: provide-media
|
||||
title: Provide media
|
||||
---
|
||||
|
||||
## Add the package {#add-the-package}
|
||||
|
||||
Add Media to the consuming application:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@workspace/media": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Register styles and catalogs {#register-styles-and-catalogs}
|
||||
|
||||
Import the stylesheet once so Tailwind includes utilities used by the package:
|
||||
|
||||
```css
|
||||
@import "@workspace/media/globals.css";
|
||||
```
|
||||
|
||||
Add `@workspace/media/locales/{locale}` to the application's internationalization catalog sources.
|
||||
|
||||
## Provide media {#provide-media}
|
||||
|
||||
Mount the adapter once around every media surface that should share storage behavior and notifications:
|
||||
|
||||
```tsx
|
||||
import { MediaLibrary, MediaProvider } from "@workspace/media"
|
||||
|
||||
;<MediaProvider adapter={mediaAdapter} notify={showNotice}>
|
||||
<MediaLibrary />
|
||||
</MediaProvider>
|
||||
```
|
||||
|
||||
The optional `notify` callback turns operation results into the application's toast or notification system.
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Overview
|
||||
description: Understand the Media package, its storage boundary, and the workflows it owns.
|
||||
order: 1
|
||||
toc:
|
||||
- id: package-role
|
||||
title: Package role
|
||||
- id: capabilities
|
||||
title: Capabilities
|
||||
- id: design-boundary
|
||||
title: Design boundary
|
||||
---
|
||||
|
||||
## Package role {#package-role}
|
||||
|
||||
`@workspace/media` provides reusable media-library and asset-picker surfaces. The host application supplies persistence through `MediaAdapter`; Media owns browsing, filtering, pagination, uploads, folders, selection, and asset actions.
|
||||
|
||||
## Capabilities {#capabilities}
|
||||
|
||||
- Browse images and videos with search, filters, folders, favorites, and pagination.
|
||||
- Upload to local or cloud storage targets exposed by the adapter.
|
||||
- Create, rename, nest, and delete organizational folders.
|
||||
- Select one or multiple assets through `MediaPickerDialog`.
|
||||
- Resolve display URLs and stable references independently.
|
||||
- Ship `en-US` and `zh-Hans` message catalogs.
|
||||
|
||||
## Design boundary {#design-boundary}
|
||||
|
||||
Media does not choose an API, database, object store, CDN, or signing strategy. The application implements those decisions behind `MediaAdapter` and can replace them without changing the interface components.
|
||||
|
||||
> Folders classify assets inside the library. They do not need to mirror object-storage paths or change public URLs.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: 存储 Adapter
|
||||
description: 将查询、上传、变更、URL 解析和分页连接到产品基础设施。
|
||||
order: 20
|
||||
toc:
|
||||
- id: implement-the-contract
|
||||
title: 实现完整契约
|
||||
- id: separate-display-and-reference-urls
|
||||
title: 区分展示与引用 URL
|
||||
- id: preserve-query-semantics
|
||||
title: 保持查询语义
|
||||
---
|
||||
|
||||
## 实现完整契约 {#implement-the-contract}
|
||||
|
||||
`MediaAdapter` 是这个包完整的持久化边界。通过产品的存储服务实现资源查询、目录操作、上传、更新和删除。
|
||||
|
||||
```ts
|
||||
import type { MediaAdapter } from "@workspace/media"
|
||||
|
||||
export const mediaAdapter: MediaAdapter = {
|
||||
listAssets: (query) => api.media.list(query),
|
||||
listFolders: () => api.media.listFolders(),
|
||||
upload: (input) => api.media.upload(input),
|
||||
createFolder: (input) => api.media.createFolder(input),
|
||||
updateFolder: (input) => api.media.updateFolder(input),
|
||||
deleteFolder: (id) => api.media.deleteFolder(id),
|
||||
moveAsset: (id, folderId) => api.media.move(id, folderId),
|
||||
renameAsset: (id, name) => api.media.rename(id, name),
|
||||
updateFavorite: (id, favorite) => api.media.favorite(id, favorite),
|
||||
deleteAsset: (id) => api.media.delete(id),
|
||||
}
|
||||
```
|
||||
|
||||
## 区分展示与引用 URL {#separate-display-and-reference-urls}
|
||||
|
||||
使用 `resolveUrl` 返回渲染时需要的短期签名 URL 或图片转换地址。使用 `resolveReference` 返回选择器写入其他文档的值。未提供 `resolveReference` 时,图片会使用 `defaultMediaReference`,在 URL 中保留固有尺寸。
|
||||
|
||||
## 保持查询语义 {#preserve-query-semantics}
|
||||
|
||||
将 `MediaAssetQuery` 当作不可变请求。在分页前依次应用 `allowedKinds`、`filter`、`folder` 和 `keyword`,然后根据筛选后的结果返回 `hasNextPage`。这样可以保持桌面端与移动端筛选行为一致。
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: 资源选择器
|
||||
description: 将媒体库复用为受控的单选或多选对话框。
|
||||
order: 30
|
||||
toc:
|
||||
- id: open-the-picker
|
||||
title: 打开选择器
|
||||
- id: constrain-selection
|
||||
title: 限制选择范围
|
||||
- id: consume-results
|
||||
title: 使用选择结果
|
||||
---
|
||||
|
||||
## 打开选择器 {#open-the-picker}
|
||||
|
||||
`MediaPickerDialog` 会复用完整媒体库相同的 Adapter 和目录模型:
|
||||
|
||||
```tsx
|
||||
import { MediaPickerDialog } from "@workspace/media"
|
||||
|
||||
;<MediaPickerDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onConfirm={(assets) => editor.insertImages(assets)}
|
||||
/>
|
||||
```
|
||||
|
||||
## 限制选择范围 {#constrain-selection}
|
||||
|
||||
图片专用流程传入 `allowedKinds={["image"]}`。对话框默认单选;消费方可以接受多个资源时传入 `multiple`,编辑已有值时通过 `initialSelection` 恢复选择。
|
||||
|
||||
## 使用选择结果 {#consume-results}
|
||||
|
||||
确认回调会收到完整的 `MediaAsset` 记录。如果结果需要在签名 URL 失效后继续使用,应保存 Adapter 提供的稳定引用,而不是临时预览地址。
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: 安装与设置
|
||||
description: 添加 Media、注册词典并连接存储 Adapter。
|
||||
order: 10
|
||||
toc:
|
||||
- id: add-the-package
|
||||
title: 添加包
|
||||
- id: register-styles-and-catalogs
|
||||
title: 注册样式与词典
|
||||
- id: provide-media
|
||||
title: 提供媒体能力
|
||||
---
|
||||
|
||||
## 添加包 {#add-the-package}
|
||||
|
||||
在使用方应用中添加 Media:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@workspace/media": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注册样式与词典 {#register-styles-and-catalogs}
|
||||
|
||||
导入一次样式表,让 Tailwind 包含 Media 使用的工具类:
|
||||
|
||||
```css
|
||||
@import "@workspace/media/globals.css";
|
||||
```
|
||||
|
||||
将 `@workspace/media/locales/{locale}` 加入应用的国际化 catalog sources。
|
||||
|
||||
## 提供媒体能力 {#provide-media}
|
||||
|
||||
在需要共享存储行为和通知方式的媒体界面外统一挂载 Adapter:
|
||||
|
||||
```tsx
|
||||
import { MediaLibrary, MediaProvider } from "@workspace/media"
|
||||
|
||||
;<MediaProvider adapter={mediaAdapter} notify={showNotice}>
|
||||
<MediaLibrary />
|
||||
</MediaProvider>
|
||||
```
|
||||
|
||||
可选的 `notify` 回调会把操作结果接入应用自己的 Toast 或通知系统。
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: 概览
|
||||
description: 了解 Media 包、存储边界以及它负责的媒体工作流。
|
||||
order: 1
|
||||
toc:
|
||||
- id: package-role
|
||||
title: 包的职责
|
||||
- id: capabilities
|
||||
title: 功能范围
|
||||
- id: design-boundary
|
||||
title: 设计边界
|
||||
---
|
||||
|
||||
## 包的职责 {#package-role}
|
||||
|
||||
`@workspace/media` 提供可复用的媒体库和资源选择界面。宿主应用通过 `MediaAdapter` 提供持久化能力;Media 负责浏览、筛选、分页、上传、目录、选择和资源操作。
|
||||
|
||||
## 功能范围 {#capabilities}
|
||||
|
||||
- 通过搜索、筛选、目录、收藏和分页浏览图片与视频。
|
||||
- 上传到 Adapter 暴露的本地或云端存储目标。
|
||||
- 创建、重命名、嵌套和删除分类目录。
|
||||
- 通过 `MediaPickerDialog` 单选或多选资源。
|
||||
- 分别解析展示 URL 和稳定引用。
|
||||
- 提供 `en-US` 与 `zh-Hans` 消息词典。
|
||||
|
||||
## 设计边界 {#design-boundary}
|
||||
|
||||
Media 不会替应用选择 API、数据库、对象存储、CDN 或签名策略。应用在 `MediaAdapter` 后实现这些决策,替换实现时无需修改界面组件。
|
||||
|
||||
> 目录只用于媒体库内部分类,不必对应对象存储路径,也不需要改变公开 URL。
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@workspace/media",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@workspace/i18n": "workspace:*",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"react": "^19.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"jsdom": "^30.0.1",
|
||||
"react-dom": "^19.2.6",
|
||||
"typescript": "~6",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./globals.css": "./src/styles/globals.css",
|
||||
"./locales": "./src/locales/catalogs.ts",
|
||||
"./locales/*": "./src/locales/*.ts"
|
||||
}
|
||||
}
|
||||
+18
-2
@@ -1,6 +1,5 @@
|
||||
import { describe, it } from "vitest"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
|
||||
import { mediaMessages } from "../messages"
|
||||
import { mediaCatalogLocales } from "./catalogs"
|
||||
import { messages as enUS } from "./en-US"
|
||||
@@ -20,3 +19,20 @@ describe("media locale catalogs", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function expectCompleteCatalogs({
|
||||
catalogs,
|
||||
locales,
|
||||
messageIds,
|
||||
}: {
|
||||
catalogs: Readonly<Record<string, Readonly<Record<string, string>>>>
|
||||
locales: readonly string[]
|
||||
messageIds: readonly string[]
|
||||
}) {
|
||||
expect(Object.keys(catalogs)).toEqual([...locales])
|
||||
|
||||
const expectedIds = new Set(messageIds)
|
||||
for (const messages of Object.values(catalogs)) {
|
||||
expect(new Set(Object.keys(messages))).toEqual(expectedIds)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { mediaMessages } from "../messages"
|
||||
|
||||
export const mediaCatalogLocales = ["en-US", "zh-Hans"] as const
|
||||
|
||||
interface MessageDescriptorMap {
|
||||
readonly [key: string]: {
|
||||
readonly id: string
|
||||
}
|
||||
}
|
||||
|
||||
type MessageId<T extends MessageDescriptorMap> = T[keyof T]["id"]
|
||||
|
||||
export type MediaCatalogLocale = (typeof mediaCatalogLocales)[number]
|
||||
export type MediaMessageCatalog = Readonly<
|
||||
Record<MessageId<typeof mediaMessages>, string>
|
||||
>
|
||||
@@ -0,0 +1 @@
|
||||
@source "../";
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@workspace/i18n": ["../i18n/src/index.ts"],
|
||||
"@workspace/ui/*": ["../ui/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -21,4 +21,4 @@ This tutorial installs the styles, introduces component subpaths, builds a small
|
||||
|
||||
## UI or Blocks {#ui-or-blocks}
|
||||
|
||||
Use UI components when the application owns the workflow. Use `@workspace/blocks` when the interface also needs a complete feature contract such as navigation, media storage, search adapters, or notification queries.
|
||||
Use UI components when the application owns the workflow. Use `@workspace/blocks` for complete application-shell features such as navigation and notifications, and focused packages such as `@workspace/media` or `@workspace/search` for domain workflows.
|
||||
|
||||
@@ -21,4 +21,4 @@ toc:
|
||||
|
||||
## UI 还是 Blocks {#ui-or-blocks}
|
||||
|
||||
当应用自己拥有业务流程时使用 UI 组件。当界面还需要导航、媒体存储、搜索适配器或通知查询等完整功能契约时,使用 `@workspace/blocks`。
|
||||
当应用自己拥有业务流程时使用 UI 组件。导航和通知等完整应用外壳功能使用 `@workspace/blocks`,媒体与搜索等领域工作流使用 `@workspace/media` 或 `@workspace/search` 等专用包。
|
||||
|
||||
Reference in New Issue
Block a user