实现 codeBlock folder
类似:
- https://github.com/codesandbox/sandpack/issues/392
- Tailwindcss ui component docs
实现方法:
- 通过
prism-react-renderer获取到token - 自定义实现一个
transformTokens方法,将用于渲染的token数据转换为期望的格式,注意:大于xxx行、哪些需要默认展开、哪些需要默认折叠,高亮行数问题等 - 最后注入到
codeBlock组件中,然后再添加上folder的样式即可
// Helper.ts
import type Highlight from 'prism-react-renderer'
export type TransformTokens = Parameters<Highlight['props']['children']>[0]['tokens']
export type TransformTokensTypes = TransformTokens[0][0] & {
folderContent?: TransformTokens
summaryContent?: TransformTokens[0]
class?: string
index?: number
open?: boolean
}
const startFlag = ['{', '[']
const endFlag = ['}', ']']
const specialStartFlag = ['(']
const specialEndFlag = [')']
const defaultFoldFlagList = ['cn', 'HTMLAttributes']
const defaultShowFlagList = ['Component', 'forwardRef']
/**
* Transform tokens from `prism-react-renderer` to wrap them in folder structure
*
* @example
* transformTokens(tokens) -> wrap tokens in folder structure
*/
export function transformTokens(tokens: TransformTokens, folderLine = 10) {
const result: TransformTokens = []
let lastIndex = 0
let isShowFolder = false
let fold = false
tokens.forEach((token, index) => {
if (index < lastIndex)
return
let startToken: TransformTokens[0][0] = null as any
const mergedStarFlagList = [...startFlag]
token.forEach((t) => {
if (defaultFoldFlagList.some(text => t.content.includes(text))) {
// If cn then need to judge whether it is import token
if (t.content.includes('cn') && token.some(t => t.content === 'import'))
return
// If HTMLAttributes then need to judge whether it have start flag
if (
t.content.includes('HTMLAttributes')
&& !token.some(t => startFlag.includes(t.content))
)
return
fold = true
mergedStarFlagList.push(...specialStartFlag)
}
if (mergedStarFlagList.includes(t.content))
startToken = t
if (defaultShowFlagList.some(text => t.content.includes(text)))
isShowFolder = true
})
const mergedOptions = fold
? {
specialEndFlag,
specialStartFlag,
}
: undefined
const isFolder = checkIsFolder(token, mergedOptions)
if (isFolder && startToken) {
const endIndex = findEndIndex(tokens, index + 1, mergedOptions)
// Greater than or equal to folderLine then will folder otherwise it will show directly
if (endIndex !== -1 && (endIndex - index >= folderLine || isShowFolder || fold)) {
lastIndex = endIndex
const folder = tokens.slice(index + 1, endIndex)
const endToken = tokens[endIndex]
const ellipsisToken: TransformTokensTypes = {
types: ['ellipsis'],
content: '',
class: 'custom-folder ellipsis-token',
}
const copyContent: TransformTokensTypes = {
types: ['copy'],
content: '',
folderContent: folder,
class: 'custom-folder copy-token',
}
endToken.forEach((t, _, arr) => {
let className = ''
className += 'custom-folder'
if (t.content.trim() === '' && (arr.length === 3 || arr.length === 4)) {
// Add length check to sure it's added to } token
className += ' empty-token'
}
(t as TransformTokensTypes).class = className
})
startToken.types = ['folderStart'];
(startToken as TransformTokensTypes).folderContent = folder;
(startToken as TransformTokensTypes).summaryContent = [
...token,
ellipsisToken,
copyContent,
...endToken,
];
(startToken as TransformTokensTypes).index = index
isShowFolder && !fold && ((startToken as TransformTokensTypes).open = true)
result.push([startToken])
isShowFolder = false
fold = false
return
}
}
token.forEach((t) => {
(t as TransformTokensTypes).index = index
})
result.push(token)
})
return result
}
interface SpecialOptions {
specialStartFlag?: string[]
specialEndFlag?: string[]
}
function checkIsFolder(
token: TransformTokens[0],
{ specialStartFlag, specialEndFlag }: SpecialOptions = {},
) {
const stack: string[] = []
const mergedStarFlagList = specialStartFlag ? [...startFlag, ...specialStartFlag] : startFlag
const mergedEndFlagList = specialEndFlag ? [...endFlag, ...specialEndFlag] : endFlag
for (const t of token) {
if (mergedStarFlagList.includes(t.content))
stack.push(t.content)
else if (mergedEndFlagList.includes(t.content))
stack.pop()
}
return stack.length !== 0
}
function findEndIndex(
tokens: TransformTokens,
startIndex: number,
{ specialStartFlag, specialEndFlag }: SpecialOptions = {},
) {
const stack: string[] = ['flag']
const mergedStarFlagList = specialStartFlag ? [...startFlag, ...specialStartFlag] : startFlag
const mergedEndFlagList = specialEndFlag ? [...endFlag, ...specialEndFlag] : endFlag
for (let i = startIndex; i < tokens.length; i++) {
const token = tokens[i]
for (const line of token) {
const transformLine = line.content.replace(/\$/g, '')
if (mergedStarFlagList.includes(transformLine))
stack.push('flag')
else if (mergedEndFlagList.includes(transformLine))
stack.pop()
if (stack.length === 0)
return i
}
}
return -1
}nextui-cli 的同步文档脚本
name: sync docs
on:
push:
# branches:
# - main
paths:
- README.md
jobs:
sync-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
check-latest: true
node-version-file: .nvmrc
- name: Setup pnpm
uses: pnpm/action-setup@v2
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Set up Git
run: |
git config --global user.name 'winchesHe'
git config --global user.email '329487092@qq.com'
- name: Clone nextui repository
run: |
git clone https://github.com/nextui-org/nextui nextui --depth 1
- name: Run docs sync script
run: |
pnpm sync:docs
- name: Get version from package.json
id: get_version
run: |
VERSION=$(jq -r '.version' package.json)
echo "::set-output name=version::$VERSION"
- name: Commit changes to nextui repository
run: |
cd nextui
git add .
git commit -m "docs: sync api from nextui-cli v${{ steps.get_version.outputs.version }}"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.PAT }}
path: nextui
branch: sync-docs-${{ steps.get_version.outputs.version }}
title: 'docs: sync api from nextui-cli v${{ steps.get_version.outputs.version }}'
body: Sync api from nextui-cli.Item 组件
DropdownItem 组件实际就是 import {BaseItem} from "@nextui-org/aria-utils";
Menu 组件里会获取 state.collection 组件然后遍历来添加 Item 并通过 useMenuItem/useAriaMenuItem 来获取 Item 的属性
useSelectableItem控制可选列表的属性/状态,包括是否可以跳转import {useSelectableItem} from "@react-aria/selection";- 跳转用的
navigate在Provider里传入
Link 组件的核心在 import {useLink as useAriaLink} from "@react-aria/link"; 的 useLink 里,自己实现的onClick逻辑,与useSelectableItem不同
useSelectableItem 用在 Tabs,Menu 等组件上
打开 Code
src/utils/tiers.ts 文件
添加 supabase table
- 添加 supabase 的
sql文件 - 运行初始化命令
- supabase 运行在 docker 中
官网示例:https://supabase.com/docs/guides/local-development/overview
登录账号
任意输入邮箱,然后在 Inbucket 中接码
Docker 操作
查看docker 占用空间
docker system df清理docker占用空间
删除未使用的镜像,不加-a就删除从未使用过的镜像
docker image prune -a删除未使用的docker volume
docker volume prune删除每个未使用的容器、镜像、volumes和networks
docker system prune -a --volumesCodesandbox api 使用
在 CodeSandbox SDK 的语境里,有两个经常会混用却含义不同的词:
Sandbox
• 可以把它想成一台「云端开发机」或「远端容器」。
• 它有自己的文件系统、网络端口、终端、Git 仓库等资源。
• 只要 Sandbox 还在运行,你就可以随时连上去执行命令、读写文件、启动服务。
• 当它长时间无人连接时,平台会把它休眠(hibernate)来节省资源;再次连接时会被自动唤醒。Session(WebSocketSession)
• 这是「你 ↔️ Sandbox」之间的一条实时 WebSocket 连接。
• 一个 Sandbox 可以被多个 Session 同时连接(例如你在两个浏览器标签页里打开同一个项目)。
• Session 暴露了许多命名空间:fs、ports、tasks、git、terminals等;所有对 Sandbox 的操作最终都通过当前 Session 发消息给远端代理(agent)完成。
下面几个 API 的作用与对已有 Session 的影响:
• connect
- SDK 内部在创建
new WebSocketSession(...)时就会自动尝试连接,所以通常不需要手动调用。 - 若 Sandbox 正在休眠,
connect过程会先唤醒、再建立 WebSocket 通道。 - 只影响当前 Session;其他 Session 不受影响。
• disconnect
- 主动关闭当前 Session 的 WebSocket。
- Sandbox 本身并不会立刻被销毁;如果还有其他 Session 连接着,它仍保持运行;若所有连接都断开,则进入平台设定的休眠倒计时。
- 断开后,这个 Session 的各种命名空间 (
fs,git……) 都不能再用;调用会返回一个 Promise,等待底层完全关闭再 resolve。
• reconnect
- 在同一个
WebSocketSession实例上重新连接。 - 常用于网络抖动或用户临时掉线之后恢复;SDK 里也会自动做重连逻辑。
- 会复用同一个 Sandbox(通过 ID 识别),因此之前在 Sandbox 里运行的进程、端口、文件改动都还在。
- 同样只影响当前 Session,其他 Session 无感知。
• dispose
- 用于彻底清理
WebSocketSession对象本身:- 若尚未
disconnect,内部会先自动断连。 - 移除所有事件监听器、清掉定时器(如 keep-alive)、释放引用,方便 GC。
- 若尚未
- 一旦
dispose后,这个 Session 实例就等同于失效,不能再reconnect;若想再次连 Sandbox,需要新建一个新的WebSocketSession。 - 仍然不直接终止 Sandbox——除非这是最后一个连接,且平台休眠计时结束。
总结
• Sandbox = 远端环境本体,生命周期受「是否还有至少一个活跃 Session」+「休眠策略」控制。
• Session = 你跟 Sandbox 的一次实时连接,会话级别的资源清理只影响自己,不会干扰其他已连接的 Session。
• connect / reconnect 让当前 Session 处于可用状态,disconnect / dispose 让它不可用;Sandbox 是否继续运行取决于有没有别的 Session 还在连接。
Awesome 15docs