Compare commits

...

4 Commits

Author SHA1 Message Date
Maofeng a6f5f0b649 docs: rewrite project readme 2026-09-20 16:49:28 +08:00
Maofeng 02df33ce6b style(docs): update homepage branding 2026-09-20 16:44:27 +08:00
Maofeng eebf29c3d5 docs(media): add standalone package guide 2026-09-20 16:44:22 +08:00
Maofeng c519d4e74b feat(media): extract workspace package 2026-09-20 16:44:15 +08:00
49 changed files with 579 additions and 107 deletions
+127 -10
View File
@@ -1,22 +1,139 @@
# Simple React App Kit # Simple React App Kit
Simple React App Kit is a reusable React application kit built with TanStack 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.
Start, shadcn/ui, and workspace packages.
## 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 - A TanStack Start application with routing, SSR, responsive layout, and light/dark themes.
pnpm dlx shadcn@latest add button -c apps/web - 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 ```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.
+2 -1
View File
@@ -7,7 +7,7 @@
"@workspace/blocks/appearance/locales/{locale}", "@workspace/blocks/appearance/locales/{locale}",
"@workspace/blocks/chats/locales/{locale}", "@workspace/blocks/chats/locales/{locale}",
"@workspace/blocks/layout/locales/{locale}", "@workspace/blocks/layout/locales/{locale}",
"@workspace/blocks/media/locales/{locale}", "@workspace/media/locales/{locale}",
"@workspace/blocks/navigation/locales/{locale}", "@workspace/blocks/navigation/locales/{locale}",
"@workspace/blocks/notifications/locales/{locale}", "@workspace/blocks/notifications/locales/{locale}",
"@workspace/search/locales/{locale}", "@workspace/search/locales/{locale}",
@@ -16,6 +16,7 @@
"include": [ "include": [
"src", "src",
"../../packages/blocks/src", "../../packages/blocks/src",
"../../packages/media/src",
"../../packages/search/src", "../../packages/search/src",
"../../packages/ui/src" "../../packages/ui/src"
], ],
+1
View File
@@ -29,6 +29,7 @@
"@workspace/blocks": "workspace:*", "@workspace/blocks": "workspace:*",
"@workspace/i18n": "workspace:*", "@workspace/i18n": "workspace:*",
"@workspace/lexical": "workspace:*", "@workspace/lexical": "workspace:*",
"@workspace/media": "workspace:*",
"@workspace/search": "workspace:*", "@workspace/search": "workspace:*",
"@workspace/ui": "workspace:*", "@workspace/ui": "workspace:*",
"react": "^19.2.6", "react": "^19.2.6",
+1
View File
@@ -1,6 +1,7 @@
@import "@workspace/ui/globals.css"; @import "@workspace/ui/globals.css";
@import "@workspace/blocks/globals.css"; @import "@workspace/blocks/globals.css";
@import "@workspace/lexical/globals.css"; @import "@workspace/lexical/globals.css";
@import "@workspace/media/globals.css";
@import "@workspace/search/globals.css"; @import "@workspace/search/globals.css";
:root { :root {
+22
View File
@@ -27,6 +27,7 @@
"@workspace/blocks": "workspace:*", "@workspace/blocks": "workspace:*",
"@workspace/i18n": "workspace:*", "@workspace/i18n": "workspace:*",
"@workspace/lexical": "workspace:*", "@workspace/lexical": "workspace:*",
"@workspace/media": "workspace:*",
"@workspace/search": "workspace:*", "@workspace/search": "workspace:*",
"@workspace/ui": "workspace:*", "@workspace/ui": "workspace:*",
"react": "^19.2.6", "react": "^19.2.6",
@@ -55,6 +56,7 @@
"dependencies": { "dependencies": {
"@icones/react": "^0.0.1", "@icones/react": "^0.0.1",
"@workspace/i18n": "workspace:*", "@workspace/i18n": "workspace:*",
"@workspace/media": "workspace:*",
"@workspace/preferences": "workspace:*", "@workspace/preferences": "workspace:*",
"@workspace/search": "workspace:*", "@workspace/search": "workspace:*",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -165,6 +167,24 @@
"vitest": "^4.1.10", "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": { "packages/preferences": {
"name": "@workspace/preferences", "name": "@workspace/preferences",
"version": "0.0.0", "version": "0.0.0",
@@ -985,6 +1005,8 @@
"@workspace/lexical": ["@workspace/lexical@workspace:packages/lexical"], "@workspace/lexical": ["@workspace/lexical@workspace:packages/lexical"],
"@workspace/media": ["@workspace/media@workspace:packages/media"],
"@workspace/preferences": ["@workspace/preferences@workspace:packages/preferences"], "@workspace/preferences": ["@workspace/preferences@workspace:packages/preferences"],
"@workspace/search": ["@workspace/search@workspace:packages/search"], "@workspace/search": ["@workspace/search@workspace:packages/search"],
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+1
View File
@@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"@icones/react": "^0.0.1", "@icones/react": "^0.0.1",
"@workspace/i18n": "workspace:*", "@workspace/i18n": "workspace:*",
"@workspace/media": "workspace:*",
"@workspace/preferences": "workspace:*", "@workspace/preferences": "workspace:*",
"@workspace/search": "workspace:*", "@workspace/search": "workspace:*",
"clsx": "^2.1.1", "clsx": "^2.1.1",
+10 -2
View File
@@ -31,8 +31,8 @@ const packageDefinitions: readonly PackageDefinition[] = [
key: "blocks", key: "blocks",
label: "Blocks", label: "Blocks",
description: { description: {
zh: "构建应用外壳,并接入导航、通知、聊天、媒体和外观控制。", zh: "构建应用外壳,并接入导航、通知、聊天和外观控制。",
us: "Build an application shell and add navigation, notifications, chat, media, and appearance controls.", 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.", 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", key: "preferences",
label: "Preferences", label: "Preferences",
+1
View File
@@ -1,4 +1,5 @@
@import "@workspace/ui/globals.css"; @import "@workspace/ui/globals.css";
@import "@workspace/media/globals.css";
@import "@workspace/search/globals.css"; @import "@workspace/search/globals.css";
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
+13 -2
View File
@@ -10,9 +10,20 @@ export default function IndexPage() {
return ( return (
<> <>
<Container className="flex items-center justify-between gap-4 pt-8 pb-10 sm:pt-12 sm:pb-14 lg:pb-16"> <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"> <h1 className="min-w-0 text-2xl font-semibold sm:text-3xl">
my-shadcn-ui <span>S</span>
<span className="text-slate-300 dark:text-slate-400">imple</span>
<span>&nbsp;</span>
<span>R</span>
<span className="text-slate-300 dark:text-slate-400">eact</span>
<span>&nbsp;</span>
<span>A</span>
<span className="text-slate-300 dark:text-slate-400">pp</span>
<span>&nbsp;</span>
<span>K</span>
<span className="text-slate-300 dark:text-slate-400">it</span>
</h1> </h1>
<span className="text-xs align-bottom">alpha</span>
<Actions className="ml-auto" /> <Actions className="ml-auto" />
</Container> </Container>
<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" 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} ## Application providers {#application-providers}
+2 -2
View File
@@ -13,7 +13,7 @@ toc:
## What you will build {#what-you-will-build} ## 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. 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. 2. Create the application shell.
3. Define navigation and route state. 3. Define navigation and route state.
4. Connect notifications. 4. Connect notifications.
5. Add chat and media workflows. 5. Add chat workflows.
6. Finish with persistent appearance controls. 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" import "@workspace/blocks/globals.css"
``` ```
功能模块应始终从显式子路径导入。例如,导入 `@workspace/blocks/media` 不会把聊天功能加入同一个模块图。 功能模块应始终从显式子路径导入。例如,导入 `@workspace/blocks/notifications` 不会把聊天功能加入同一个模块图。媒体工作流由独立的 `@workspace/media` 提供。
## 应用 Providers {#application-providers} ## 应用 Providers {#application-providers}
+2 -2
View File
@@ -13,7 +13,7 @@ toc:
## 将要构建的内容 {#what-you-will-build} ## 将要构建的内容 {#what-you-will-build}
本教程会逐步构建一个应用外壳。你将从共享布局开始,接入导航,再添加通知、聊天、媒体和外观偏好。 本教程会逐步构建一个应用外壳。你将从共享布局开始,接入导航,再添加通知、聊天和外观偏好。
完成后,应用负责数据和业务规则,`@workspace/blocks` 负责可复用的展示与交互模式。 完成后,应用负责数据和业务规则,`@workspace/blocks` 负责可复用的展示与交互模式。
@@ -29,5 +29,5 @@ Blocks 位于 `@workspace/ui` 之上。它们将底层组件组合成完整功
2. 创建应用外壳。 2. 创建应用外壳。
3. 定义导航和路由状态。 3. 定义导航和路由状态。
4. 接入通知。 4. 接入通知。
5. 添加聊天和媒体工作流。 5. 添加聊天工作流。
6. 使用持久化外观控件完成应用。 6. 使用持久化外观控件完成应用。
-3
View File
@@ -45,9 +45,6 @@
"./layout/locales": "./src/blocks/layout/locales/catalogs.ts", "./layout/locales": "./src/blocks/layout/locales/catalogs.ts",
"./layout/locales/*": "./src/blocks/layout/locales/*.ts", "./layout/locales/*": "./src/blocks/layout/locales/*.ts",
"./layouts/vega": "./src/blocks/layouts/vega/index.tsx", "./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": "./src/blocks/navigation/index.ts",
"./navigation/locales": "./src/blocks/navigation/locales/catalogs.ts", "./navigation/locales": "./src/blocks/navigation/locales/catalogs.ts",
"./navigation/locales/*": "./src/blocks/navigation/locales/*.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.
+48
View File
@@ -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.
+31
View File
@@ -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 或通知系统。
+31
View File
@@ -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。
+30
View File
@@ -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"
}
}
@@ -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 { mediaMessages } from "../messages"
import { mediaCatalogLocales } from "./catalogs" import { mediaCatalogLocales } from "./catalogs"
import { messages as enUS } from "./en-US" 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)
}
}
+16
View File
@@ -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>
>
+1
View File
@@ -0,0 +1 @@
@source "../";
+19
View File
@@ -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"]
}
+1 -1
View File
@@ -21,4 +21,4 @@ This tutorial installs the styles, introduces component subpaths, builds a small
## UI or Blocks {#ui-or-blocks} ## 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.
+1 -1
View File
@@ -21,4 +21,4 @@ toc:
## UI 还是 Blocks {#ui-or-blocks} ## UI 还是 Blocks {#ui-or-blocks}
当应用自己拥有业务流程时使用 UI 组件。当界面还需要导航、媒体存储、搜索适配器或通知查询等完整功能契约时,使用 `@workspace/blocks`。 当应用自己拥有业务流程时使用 UI 组件。导航和通知等完整应用外壳功能使用 `@workspace/blocks`,媒体与搜索等领域工作流使用 `@workspace/media` 或 `@workspace/search` 等专用包。