create-html-app

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Create HTML App

创建HTML应用

A Hubble HTML App is a folder-local
.html
file Hubble runs as a self-contained interactive UI. Opening it shows the app in the main content panel. Build any interactive experience — a tool, dashboard, or app — as a single
.html
file in the Folder.
To embed an HTML App inline inside a Markdown File, see
create-embed
.
Hubble HTML应用是一个存储在文件夹中的本地
.html
文件,Hubble会将其作为独立的交互式UI运行。打开该文件后,应用会显示在主内容面板中。你可以在文件夹中创建单个
.html
文件,构建任何交互式体验——比如工具、仪表板或应用。
若要在Markdown文件中嵌入HTML应用,请查看
create-embed

Runtime

运行时

Hubble provides the app runtime. Write plain HTML that assumes these globals are already available:
  • hubble
  • Alpine
  • Tailwind browser v4
  • Hubble theme tokens
Use Alpine and Tailwind by default. Do not add dependency
<script>
tags or set up package files, lockfiles, or
node_modules
.
HTML Apps run in a sandboxed iframe. Hubble allows:
  • allow-scripts
    : Alpine, Tailwind browser, and the Hubble runtime can execute.
  • allow-forms
    : native HTML form semantics work when paired with Alpine handlers such as
    @submit.prevent
    .
Hubble does not allow:
  • allow-same-origin
    : apps keep an opaque origin and cannot use app-origin storage, cookies, or same-origin access.
  • allow-top-navigation
    : apps cannot navigate the desktop app frame.
  • allow-popups
    /
    allow-popups-to-escape-sandbox
    : apps cannot open unsandboxed popup windows.
  • allow-downloads
    : apps cannot start downloads directly.
  • allow-modals
    : apps cannot use modal browser dialogs.
Hubble提供应用运行时环境。你可以编写普通HTML,默认以下全局对象已可用:
  • hubble
  • Alpine
  • Tailwind浏览器版v4
  • Hubble主题令牌
默认使用Alpine和Tailwind。无需添加依赖
<script>
标签,也无需配置包文件、锁定文件或
node_modules
HTML应用在沙箱化iframe中运行。Hubble允许以下权限:
  • allow-scripts
    :Alpine、Tailwind浏览器版和Hubble运行时可执行脚本。
  • allow-forms
    :配合Alpine处理器(如
    @submit.prevent
    )使用时,原生HTML表单语义可正常工作。
Hubble禁止以下权限:
  • allow-same-origin
    :应用保持不透明源,无法使用应用源存储、Cookie或同源访问。
  • allow-top-navigation
    :应用无法导航桌面应用框架。
  • allow-popups
    /
    allow-popups-to-escape-sandbox
    :应用无法打开未沙箱化的弹出窗口。
  • allow-downloads
    :应用无法直接启动下载。
  • allow-modals
    :应用无法使用模态浏览器对话框。

Files API

文件API

Apps reach Workspace files through
hubble.files
, an async broker rather than direct filesystem access. All paths are Workspace-relative and point to Markdown files.
await hubble.files.list("todos/*.md")
// → [{ name, path, modified_at, size }], sorted by path
await hubble.files.read("notes/today.md")
// → { path, body, properties }
await hubble.files.open("notes/today.md")
// → { path }, navigates the editor to the file
await hubble.files.create({ path, body, properties, open })
// → { path, body, properties }
await hubble.files.update(path, { body, properties })
// → { path, body, properties }, patch
await hubble.files.remove("notes/today.md")
// → { path }
Methods throw on failure; each has a
safe*
variant. See Files API for shapes, patch semantics, and error handling.
应用可通过
hubble.files
访问工作区文件,这是一个异步代理而非直接文件系统访问。所有路径均为工作区相对路径,指向Markdown文件。
await hubble.files.list("todos/*.md")
// → [{ name, path, modified_at, size }], sorted by path
await hubble.files.read("notes/today.md")
// → { path, body, properties }
await hubble.files.open("notes/today.md")
// → { path }, navigates the editor to the file
await hubble.files.create({ path, body, properties, open })
// → { path, body, properties }
await hubble.files.update(path, { body, properties })
// → { path, body, properties }, patch
await hubble.files.remove("notes/today.md")
// → { path }
方法执行失败时会抛出异常;每个方法都有对应的
safe*
变体。有关数据结构、补丁语义和错误处理,请查看Files API

Template

模板

<!doctype html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<title>Folder files</title>
	</head>
	<body class="m-0 bg-background p-3 font-sans text-foreground">
		<main
			class="grid gap-3 rounded-md border border-border bg-card p-3 text-card-foreground"
			x-data="{
				files: [],
				error: '',
				async init() {
					try {
						this.files = await hubble.files.list('**/*.md')
					} catch (error) {
						this.error = error.message || 'Could not load files'
					}
				},
			}"
		>
			<header class="flex items-center justify-between gap-3">
				<h1 class="m-0 text-base font-semibold">Folder files</h1>
				<span class="text-sm text-muted-foreground" x-text="`${files.length} files`"></span>
			</header>

			<p class="m-0 text-sm text-muted-foreground" x-show="!error && files.length === 0">
				Loading files...
			</p>
			<p class="m-0 text-sm text-destructive" x-show="error" x-text="error"></p>

			<ul class="m-0 grid list-none gap-2 p-0" x-show="!error && files.length > 0">
				<template x-for="file in files" :key="file.path">
					<li class="rounded-sm border border-border bg-background px-3 py-2">
						<span class="text-sm font-medium" x-text="file.path"></span>
					</li>
				</template>
			</ul>
		</main>
	</body>
</html>
<!doctype html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<title>Folder files</title>
	</head>
	<body class="m-0 bg-background p-3 font-sans text-foreground">
		<main
			class="grid gap-3 rounded-md border border-border bg-card p-3 text-card-foreground"
			x-data="{
				files: [],
				error: '',
				async init() {
					try {
						this.files = await hubble.files.list('**/*.md')
					} catch (error) {
						this.error = error.message || 'Could not load files'
					}
				},
			}"
		>
			<header class="flex items-center justify-between gap-3">
				<h1 class="m-0 text-base font-semibold">Folder files</h1>
				<span class="text-sm text-muted-foreground" x-text="`${files.length} files`"></span>
			</header>

			<p class="m-0 text-sm text-muted-foreground" x-show="!error && files.length === 0">
				Loading files...
			</p>
			<p class="m-0 text-sm text-destructive" x-show="error" x-text="error"></p>

			<ul class="m-0 grid list-none gap-2 p-0" x-show="!error && files.length > 0">
				<template x-for="file in files" :key="file.path">
					<li class="rounded-sm border border-border bg-background px-3 py-2">
						<span class="text-sm font-medium" x-text="file.path"></span>
					</li>
				</template>
			</ul>
		</main>
	</body>
</html>

Structuring Logic

逻辑结构

Inline
x-data
is fine for simple state (a flag, a counter, a current tab). For anything more, like async loaders, multiple methods, or computed logic, register an
Alpine.data()
component in a
<script>
and reference it by name.
ts
<script>
	document.addEventListener('alpine:init', () => {
		Alpine.data('fileList', () => ({
			files: [],
			error: '',
			async init() {
				try {
					this.files = await hubble.files.list('**/*.md')
				} catch (error) {
					this.error = error.message || 'Could not load files'
				}
			},
		}))
	})
</script>

<main x-data="fileList"></main>
Why this beats large inline expressions:
  • Alpine parses
    x-data
    as a single expression. Multi-line bodies with arrow functions, object methods, or
    <
    /
    >
    /
    &
    get HTML-escaped or mis-parsed, surfacing as silent "Alpine Expression Error" failures.
  • Logic lives in real JavaScript with syntax highlighting and normal quoting, not inside an attribute string.
  • Components are reusable and testable, and keep markup readable.
Register components inside
alpine:init
so they exist before Alpine scans the DOM.
对于简单状态(如标志、计数器、当前标签页),使用内联
x-data
即可。若涉及更复杂的逻辑,比如异步加载器、多个方法或计算逻辑,请在
<script>
中注册
Alpine.data()
组件,并通过名称引用它。
ts
<script>
	document.addEventListener('alpine:init', () => {
		Alpine.data('fileList', () => ({
			files: [],
			error: '',
			async init() {
				try {
					this.files = await hubble.files.list('**/*.md')
				} catch (error) {
					this.error = error.message || 'Could not load files'
				}
			},
		}))
	})
</script>

<main x-data="fileList"></main>
这种方式优于大型内联表达式的原因:
  • Alpine会将
    x-data
    解析为单个表达式。包含箭头函数、对象方法或
    <
    /
    >
    /
    &
    的多行代码会被HTML转义或解析错误,导致无声的“Alpine表达式错误”。
  • 逻辑代码位于真实的JavaScript环境中,支持语法高亮和常规引号,无需写在属性字符串内。
  • 组件可复用、可测试,且能保持标记代码的可读性。
请在
alpine:init
事件中注册组件,确保Alpine扫描DOM前组件已存在。

Native Feel

原生体验

Use Hubble tokens through Tailwind classes so the app feels native:
  • Surfaces:
    bg-background
    ,
    bg-card
    ,
    bg-popover
  • Text:
    text-foreground
    ,
    text-muted-foreground
    ,
    text-card-foreground
  • Borders/rings:
    border-border
    ,
    border-input
    ,
    ring-ring
    ,
    focus-visible:ring-ring
  • Actions:
    bg-primary text-primary-foreground
    ,
    bg-secondary text-secondary-foreground
  • States:
    hover:bg-accent
    ,
    bg-selected text-selected-foreground
Keep spacing compact:
gap-2
,
gap-3
,
p-2
,
p-3
,
px-3
,
py-2
. Use
rounded-sm
or
rounded-md
for controls.
Avoid one-off palettes, oversized cards, hero layouts, and decorative gradients unless the user asks for a themed visual.
通过Tailwind类使用Hubble令牌,让应用具备原生质感:
  • 表面:
    bg-background
    ,
    bg-card
    ,
    bg-popover
  • 文本:
    text-foreground
    ,
    text-muted-foreground
    ,
    text-card-foreground
  • 边框/环形:
    border-border
    ,
    border-input
    ,
    ring-ring
    ,
    focus-visible:ring-ring
  • 操作:
    bg-primary text-primary-foreground
    ,
    bg-secondary text-secondary-foreground
  • 状态:
    hover:bg-accent
    ,
    bg-selected text-selected-foreground
保持紧凑间距:
gap-2
,
gap-3
,
p-2
,
p-3
,
px-3
,
py-2
。控件使用
rounded-sm
rounded-md
除非用户要求主题化视觉效果,否则请避免使用自定义调色板、超大卡片、英雄布局和装饰性渐变。

References

参考资料

Read only the patterns you need:
  • Files API
  • Buttons
  • Radio selects
  • Tabs
  • Forms
  • Lists and empty states
仅阅读你需要的模式即可:
  • Files API
  • Buttons
  • Radio selects
  • Tabs
  • Forms
  • Lists and empty states