apptify 0.5.0 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -308
- package/dist/apptify.css +1 -1
- package/dist/apptify.js +2298 -2140
- package/dist/components/AppleCard.vue.d.ts +76 -0
- package/dist/components/button.d.ts +3 -3
- package/dist/components/content.d.ts +0 -90
- package/dist/components/foundation.d.ts +201 -129
- package/dist/components/link.d.ts +31 -0
- package/dist/components/overlays.d.ts +8 -2
- package/dist/core/context.d.ts +11 -0
- package/dist/core/overlay-surface.d.ts +6 -0
- package/dist/core/popup-motion.d.ts +5 -5
- package/dist/core/popup-placement.d.ts +3 -2
- package/dist/index.d.ts +144 -79
- package/docs/COMPONENTS.md +24 -10
- package/docs/INPUT-WORKSHOP.md +59 -0
- package/docs/PLAYGROUND-TEMPLATE.md +32 -55
- package/docs/VALIDATION.md +1 -1
- package/package.json +14 -1
- package/templates/playground/README.md +14 -10
- package/templates/playground/index.html +1 -1
- package/templates/playground/package.json +6 -2
- package/templates/playground/source-snapshot.json +139 -54
- package/templates/playground/{playground → src}/App.vue +5 -4
- package/templates/playground/{playground → src}/ComponentDemo.vue +8 -9
- package/templates/playground/{playground → src}/catalog.ts +2 -3
- package/templates/playground/src/components/ComponentIndex.vue +43 -0
- package/templates/playground/src/components/DirectoryDialog.vue +20 -0
- package/templates/playground/src/components/PageFooter.vue +7 -0
- package/templates/playground/{playground → src}/components/UserMenu.vue +11 -11
- package/templates/playground/src/editor/AppleInputExample.vue +78 -0
- package/templates/playground/src/editor/AppleInputPreview.vue +82 -0
- package/templates/playground/src/editor/ComponentDocumentLink.vue +19 -0
- package/templates/playground/src/editor/ComponentWorkshop.vue +81 -0
- package/templates/playground/src/editor/ConfigurableEditor.vue +35 -0
- package/templates/playground/src/editor/LiveVueEditor.vue +133 -0
- package/templates/playground/src/editor/apple-input-source.ts +2 -0
- package/templates/playground/src/editor/apple-input.ts +48 -0
- package/templates/playground/src/editor/contract.ts +14 -0
- package/templates/playground/src/editor/documents.ts +20 -0
- package/templates/playground/src/editor/input-playground.ts +59 -0
- package/templates/playground/src/editor/runtime.ts +5 -0
- package/templates/playground/src/editor/sections.ts +10 -0
- package/templates/playground/{playground → src}/style.css +40 -39
- package/templates/playground/{playground → src}/views/[...all].vue +2 -2
- package/templates/playground/src/views/component-docs/[component].vue +162 -0
- package/templates/playground/src/views/components.vue +148 -0
- package/templates/playground/src/views/index.vue +73 -0
- package/templates/playground/src/views/settings.vue +225 -0
- package/templates/playground/tooling/playground-editor.mjs +114 -0
- package/templates/playground/tooling/prepare-input-source.mjs +38 -0
- package/templates/playground/tsconfig.json +2 -2
- package/templates/playground/vite.config.ts +3 -1
- package/tooling/playground-editor.mjs +114 -0
- package/tooling/prepare-input-source.mjs +38 -0
- package/tooling/prepare-playground.mjs +45 -19
- package/tooling/test-create-apptify.mjs +86 -0
- package/tooling/test-playground-package.mjs +13 -7
- package/templates/playground/playground/components/ComponentIndex.vue +0 -62
- package/templates/playground/playground/components/PageFooter.vue +0 -3
- package/templates/playground/playground/views/components.vue +0 -126
- package/templates/playground/playground/views/index.vue +0 -75
- package/templates/playground/playground/views/settings.vue +0 -98
- /package/templates/playground/{playground → src}/components/not-found-paper-scene.vue +0 -0
- /package/templates/playground/{playground → src}/components/not-found-paper-story.ts +0 -0
- /package/templates/playground/{playground → src}/env.d.ts +0 -0
- /package/templates/playground/{playground → src}/main.ts +0 -0
- /package/templates/playground/{playground → src}/router/index.ts +0 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// Generated by tooling/prepare-input-source.mjs; do not edit the snapshot by hand.
|
|
2
|
+
export default "<!-- AppleInput 同源实现:src/components/forms.ts + src/styles/forms.css。\n只移除 TypeScript 类型并转接浏览器公开依赖,保留 Options API/render 实现。 -->\n<script>\nimport { defineComponent, h, useId } from 'vue';\nimport { ripple } from 'apptify';\nimport { Eye, EyeOff, LoaderCircle, X } from 'lucide-vue-next';\nconst fieldProps = {\n label: { type: String, default: '' },\n hint: { type: String, default: '' },\n error: { type: String, default: '' },\n disabled: Boolean,\n loading: Boolean,\n required: Boolean,\n motion: { type: String, default: 'inherit' },\n};\nfunction fieldId(vm) { return String(vm.$attrs.id || vm.inputId); }\nfunction controlAttrs(vm, hasLabel = !!vm.label) {\n return {\n ...vm.$attrs,\n id: fieldId(vm),\n disabled: vm.disabled || vm.loading,\n required: vm.required,\n 'aria-label': vm.$attrs['aria-label'] || vm.label || undefined,\n 'aria-labelledby': vm.$attrs['aria-labelledby'] || (!vm.$attrs['aria-label'] && hasLabel ? `${fieldId(vm)}-label` : undefined),\n 'aria-invalid': vm.error ? true : undefined,\n 'aria-busy': vm.loading || undefined,\n 'aria-describedby': [vm.$attrs['aria-describedby'], (vm.error || vm.hint) ? `${fieldId(vm)}-message` : ''].filter(Boolean).join(' ') || undefined,\n };\n}\nfunction field(vm, control, classes = '', error = vm.error) {\n return h('div', {\n class: ['apple-field', classes, { 'is-disabled': vm.disabled, 'has-error': !!error }],\n 'data-apple-motion': vm.motion,\n }, [\n vm.label ? h('span', { class: 'apple-field__label', id: `${fieldId(vm)}-label` }, [vm.label, vm.required ? h('span', { 'aria-hidden': true, class: 'apple-field__required' }, ' *') : null]) : null,\n control,\n error || vm.hint ? h('p', { id: `${fieldId(vm)}-message`, class: ['apple-field__message', { 'is-error': !!error }], role: error ? 'alert' : undefined }, error || vm.hint) : null,\n ]);\n}\nfunction spinner() { return h(LoaderCircle, { size: 18, class: 'apple-control__spinner', 'aria-hidden': true }); }\nfunction stringValue(event) { return event.target.value; }\nexport const AppleInput = defineComponent({\n name: 'AppleInput', inheritAttrs: false,\n props: { ...fieldProps, modelValue: { type: [String, Number], default: '' }, type: { type: String, default: 'text' }, placeholder: String, clearable: Boolean },\n emits: ['update:modelValue', 'change', 'clear'],\n data() { return { inputId: useId(), passwordVisible: false }; },\n methods: { focus() { this.$refs.input?.focus(); } },\n render() {\n const inactive = this.disabled || this.loading;\n return field(this, h('div', { class: 'apple-input-wrap' }, [\n this.$slots.prefix ? h('span', { class: 'apple-input__affix' }, this.$slots.prefix()) : null,\n h('input', { ...controlAttrs(this), ref: 'input', class: ['apple-control', this.$attrs.class], type: this.type === 'password' && this.passwordVisible ? 'text' : this.type, placeholder: this.placeholder, value: this.modelValue,\n onInput: (event) => this.$emit('update:modelValue', stringValue(event)), onChange: (event) => this.$emit('change', stringValue(event)),\n }),\n this.loading ? spinner() : null,\n !inactive && this.clearable && String(this.modelValue).length ? ripple(h('button', { type: 'button', class: 'apple-field__icon', 'aria-label': '清空', title: '清空', onClick: () => { this.$emit('update:modelValue', ''); this.$emit('clear'); this.focus(); } }, h(X, { size: 16 }))) : null,\n !inactive && this.type === 'password' ? ripple(h('button', { type: 'button', class: 'apple-field__icon', 'aria-label': this.passwordVisible ? '隐藏密码' : '显示密码', title: this.passwordVisible ? '隐藏密码' : '显示密码', 'aria-pressed': this.passwordVisible, onClick: () => { this.passwordVisible = !this.passwordVisible; } }, h(this.passwordVisible ? EyeOff : Eye, { size: 18 }))) : null,\n this.$slots.suffix ? h('span', { class: 'apple-input__affix' }, this.$slots.suffix()) : null,\n ]));\n },\n});\nexport default AppleInput;\n</script>\n\n<style>\n.apple-field { display: flex; flex-direction: column; gap: 8px; min-width: 0; color: var(--apple-text); font: inherit; }\n.apple-field__label { font-size: 14px; font-weight: 600; line-height: 1.4; overflow-wrap: anywhere; }\n.apple-field__required, .apple-field__message.is-error { color: var(--apple-danger); }\n.apple-field__message { margin: 0; color: var(--apple-secondary); font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }\n.apple-field__counter { align-self: flex-end; color: var(--apple-secondary); font-size: 12px; font-variant-numeric: tabular-nums; }\n.apple-control { box-sizing: border-box; min-width: 0; width: 100%; min-height: 48px; border: 1px solid var(--apple-border); border-radius: 8px; padding: 11px 14px; background: var(--apple-surface); color: var(--apple-text); font: inherit; font-size: 16px; line-height: 1.5; transition: border-color var(--apple-duration-fast, 120ms) ease, box-shadow var(--apple-duration-fast, 120ms) ease, background-color var(--apple-duration-fast, 120ms) ease, color var(--apple-duration-fast, 120ms) ease; }\n.apple-field .apple-control { font-size: 16px; }\n.apple-control::placeholder { color: var(--apple-secondary); opacity: 1; }\n.apple-control:hover:where(:not([data-apple-touch] *)):not(:disabled) { border-color: color-mix(in srgb, var(--apple-secondary) 60%, var(--apple-border)); }\n.apple-control:focus { border-color: var(--apple-accent); outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--apple-accent) 20%, transparent); }\n.apple-field.has-error .apple-control, .apple-field.has-error .apple-input-wrap { border-color: var(--apple-danger); }\n.apple-control:disabled, .apple-choice:has(input:disabled), .apple-switch-row:has(input:disabled), .apple-field .is-disabled { opacity: .5; cursor: not-allowed; }\n.apple-field .apple-control:disabled { background: var(--apple-surface-alt); border-color: var(--apple-surface-alt); }\n.apple-input-wrap { display: flex; align-items: center; min-width: 0; border: 1px solid var(--apple-border); border-radius: 8px; background: var(--apple-surface); transition: border-color var(--apple-duration-fast, 120ms) ease, box-shadow var(--apple-duration-fast, 120ms) ease, background-color var(--apple-duration-fast, 120ms) ease; }\n.apple-input-wrap:hover:where(:not([data-apple-touch] *)):not(:has(input:disabled)) { border-color: color-mix(in srgb, var(--apple-secondary) 60%, var(--apple-border)); }\n.apple-input-wrap:focus-within { border-color: var(--apple-accent); outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--apple-accent) 20%, transparent); }\n.apple-input-wrap > .apple-control, .apple-input-wrap > .apple-control:focus, .apple-input-wrap > .apple-control:focus-visible { border: 0; background: transparent; box-shadow: none; outline: none; }\n.apple-input-wrap > .apple-control:disabled { background: transparent; }\n.apple-field .apple-input-wrap:has(input:disabled) { background: var(--apple-surface-alt); border-color: var(--apple-surface-alt); }\n.apple-input__affix { display: inline-flex; align-items: center; flex-shrink: 0; max-width: 35%; padding-inline: 12px; color: var(--apple-secondary); overflow-wrap: anywhere; }\n.apple-input-wrap > .apple-input__affix:has(button) { align-self: stretch; align-items: stretch; gap: 0; max-width: none; padding: 0; }\n.apple-input-wrap > .apple-input__affix button { align-self: stretch; box-sizing: border-box; min-height: 44px; padding: 7px 12px; border-width: 1px; border-style: solid; border-radius: 0; line-height: 1.4; }\n.apple-input-wrap > .apple-input__affix button:not(.apple-button) { border-color: transparent; }\n.apple-input-wrap > .apple-input__affix .apple-button--icon, .apple-input-wrap > .apple-input__affix .apple-field__icon { width: 44px; height: auto; padding: 0; }\n.apple-input-wrap > .apple-field__icon { align-self: stretch; height: auto; min-height: 44px; padding: 0; border-radius: 0; }\n.apple-input-wrap > .apple-field__icon:first-child, .apple-input-wrap > .apple-input__affix:first-child > button:first-child { border-radius: 7px 0 0 7px; }\n.apple-input-wrap > .apple-field__icon:last-child, .apple-input-wrap > .apple-input__affix:last-child > button:last-child { border-radius: 0 7px 7px 0; }\n.apple-input__affix + .apple-control { padding-inline-start: 0; }\n.apple-field__icon { position: relative; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 44px; height: 44px; border: 0; border-radius: 6px; color: var(--apple-secondary); background: transparent; cursor: pointer; transition: background-color var(--apple-duration-fast, 120ms) ease, color var(--apple-duration-fast, 120ms) ease; }\n.apple-field__icon:hover:where(:not([data-apple-touch] *)):not(:disabled) { color: var(--apple-text); background: var(--apple-surface-alt); }\n.apple-field__icon:active:not(:disabled) { background: color-mix(in srgb, var(--apple-text) 15%, var(--apple-surface)); }\n.apple-field__icon:focus-visible, .apple-stepper__button:focus-visible, .apple-rate__star:focus-visible { outline: 3px solid var(--apple-accent); outline-offset: 2px; }\n.apple-field__icon:disabled, .apple-stepper__button:disabled { opacity: .4; cursor: not-allowed; }\n.apple-control__spinner { flex-shrink: 0; margin-inline: 12px; color: var(--apple-accent); animation: apple-control-spin .9s linear infinite; }\n</style>\n"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ComponentDocument, ApiDefinition } from './contract'
|
|
2
|
+
const definitions = {
|
|
3
|
+
name: 'AppleInput', source: 'src/components/forms.ts:21–89',
|
|
4
|
+
props: [
|
|
5
|
+
{ name: 'modelValue / v-model', type: 'string | number', default: "''", description: '输入框里显示的内容,用 v-model 绑定。初始值可以是文字或数字;用户输入后,得到的值都是字符串。例如输入 123,得到的是 "123",不是数字 123。清空后得到空字符串 ""。' },
|
|
6
|
+
{ name: 'type', type: 'string', default: "'text'", description: '设置输入类型,例如 text(普通文字)、password(密码)、number(数字)。密码类型会显示“显示密码”按钮,点击后可以查看或隐藏内容,输入的值不会改变。数字类型仍返回字符串。' },
|
|
7
|
+
{ name: 'placeholder', type: 'string', default: 'undefined', description: '输入框为空时显示的提示文字,开始输入后消失。例如“请输入姓名”。它不能代替字段名称,建议同时设置 label。' },
|
|
8
|
+
{ name: 'clearable', type: 'boolean', default: 'false', description: '设为 true 后,输入框有内容时会显示清空按钮,值为数字 0 时也会显示。点击按钮会清空内容、触发 clear 事件,并让光标回到输入框。禁用或加载时会隐藏清空按钮。' },
|
|
9
|
+
{ name: 'label', type: 'string', default: "''", description: '输入框上方的字段名称,例如“姓名”。字段名称只用于说明,点击输入框本身可进入编辑。屏幕阅读器默认使用它介绍这个字段;可以用 aria-label 单独设置朗读名称。' },
|
|
10
|
+
{ name: 'hint', type: 'string', default: "''", description: '输入框下方的帮助文字,例如“最多输入 20 个字”。如果设置了 error,会优先显示错误信息,暂时隐藏这段帮助。屏幕阅读器也能读到这段说明。' },
|
|
11
|
+
{ name: 'error', type: 'string', default: "''", description: '输入框下方的错误信息,例如“请输入姓名”,同时显示错误样式。把它设为空字符串就能恢复正常。组件只显示你传入的错误,不会自己检查内容,也不会阻止提交。' },
|
|
12
|
+
{ name: 'disabled', type: 'boolean', default: 'false', description: '设为 true 后,输入框不能编辑,清空和密码按钮也会隐藏。按 Tab 会跳过这个字段,提交 HTML 表单时也不会包含这个字段。' },
|
|
13
|
+
{ name: 'loading', type: 'boolean', default: 'false', description: '设为 true 后显示加载图标,表示正在处理。此时不能输入,清空和密码按钮会隐藏,只保留加载图标。处理结束后设为 false,就能继续操作。' },
|
|
14
|
+
{ name: 'required', type: 'boolean', default: 'false', description: '设为 true 后,字段名称旁显示星号。放在 HTML 表单中时,浏览器会检查是否填写;为空就不能正常提交。它不会自动设置 error,也不会生成自定义错误信息。' },
|
|
15
|
+
{ name: 'motion', type: "'inherit' | 'auto' | 'full' | 'reduced' | 'none'", default: "'inherit'", description: '设置这个输入框的动效偏好。默认 inherit,跟随外层设置;none 表示不使用动效,reduced 表示减少动效。这个设置不会改变输入内容或校验结果。' },
|
|
16
|
+
],
|
|
17
|
+
events: [
|
|
18
|
+
{ name: 'update:modelValue', type: '(value: string) => void', description: '输入内容变化时触发,事件参数就是最新的文字;点击清空时,参数是空字符串。使用 v-model 时通常不用手动处理。即使 type 是 number,参数也仍是字符串,需要数字时自行转换。' },
|
|
19
|
+
{ name: 'change', type: '(value: string) => void', description: '修改内容后,把光标移出输入框时通常会触发,事件参数是当前文字。它不会像 update:modelValue 那样每输入一个字就触发。点击组件的清空按钮不会主动触发它。' },
|
|
20
|
+
{ name: 'clear', type: '() => void', description: '点击清空按钮时触发,没有事件参数。触发前,组件已经发出清空内容的更新事件;随后会让光标回到输入框。可以用它处理清空后的操作。' },
|
|
21
|
+
],
|
|
22
|
+
slots: [
|
|
23
|
+
{ name: 'prefix', type: '() => VNode[](无 slot props)', description: '在输入框内容的左侧放自定义内容,例如 @ 符号或图标。插槽不会提供额外数据。如果放的是按钮,请给按钮设置清楚的名称;纯装饰图标可以用 aria-hidden 隐藏朗读。' },
|
|
24
|
+
{ name: 'suffix', type: '() => VNode[](无 slot props)', description: '在输入框右侧放自定义内容,例如单位、图标或操作按钮。它位于组件自带的加载、清空、密码按钮之后。插槽不会提供额外数据;直接放在组件里的内容不会显示,需使用具名插槽。' },
|
|
25
|
+
],
|
|
26
|
+
methods: [{ name: 'focus()', type: '() => void', description: '让光标进入输入框。先用 ref 获取组件,再调用 input.value.focus()。例如点击“编辑姓名”后自动聚焦。输入框被禁用时无法聚焦;组件没有提供 blur() 或 select() 方法。' }],
|
|
27
|
+
notes: [
|
|
28
|
+
'常见 HTML 输入属性可以直接使用,例如 maxlength 限制长度、autocomplete 自动填充。@focus、@blur、@keydown 也可以直接监听。这些沿用浏览器的行为,不是组件另设的接口。class 和 style 应用在内部输入框上。不要使用 readable。',
|
|
29
|
+
'不传 id 时,组件会自动生成,保证名称和输入框正确关联。帮助、错误、加载状态也会告知屏幕阅读器;这些状态跟随 hint、error、loading、required 和 disabled。',
|
|
30
|
+
'可以用 Tab 切换焦点,用键盘选中文字、复制和操作按钮。密码按钮会提示当前是显示还是隐藏;清空后光标回到输入框。如果不显示 label,请设置 aria-label 或 aria-labelledby,让屏幕阅读器知道字段名称。',
|
|
31
|
+
],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function entries(group: string, definitions: ApiDefinition[]) {
|
|
35
|
+
return definitions.map(entry => {
|
|
36
|
+
const id = entry.name === 'modelValue / v-model' ? 'model-value'
|
|
37
|
+
: entry.name === 'update:modelValue' ? 'update-model-value'
|
|
38
|
+
: entry.name === 'focus()' ? 'focus' : entry.name
|
|
39
|
+
return { ...entry, id: `${group}-${id}` }
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
export default {
|
|
43
|
+
...definitions,
|
|
44
|
+
props: entries('prop', definitions.props),
|
|
45
|
+
events: entries('event', definitions.events),
|
|
46
|
+
slots: entries('slot', definitions.slots),
|
|
47
|
+
methods: entries('method', definitions.methods),
|
|
48
|
+
} satisfies ComponentDocument
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Public API descriptions are required; component-specific runnable examples are optional. */
|
|
2
|
+
export interface VueExample { title: string; description: string; code: string }
|
|
3
|
+
export interface ApiDefinition { name: string; type: string; default?: string; description: string }
|
|
4
|
+
export interface ApiEntry extends ApiDefinition { id: string; example?: VueExample }
|
|
5
|
+
export interface ComponentDocument {
|
|
6
|
+
name: string
|
|
7
|
+
source: string
|
|
8
|
+
props: ApiEntry[]
|
|
9
|
+
events: ApiEntry[]
|
|
10
|
+
slots: ApiEntry[]
|
|
11
|
+
methods: ApiEntry[]
|
|
12
|
+
notes: string[]
|
|
13
|
+
implementation?: { files: Record<string, string>; activeFile: string; description: string }
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineAsyncComponent, type Component } from 'vue'
|
|
2
|
+
import type { ComponentDocument } from './contract'
|
|
3
|
+
/** Lightweight route directory. Each document is downloaded only on its own page. */
|
|
4
|
+
export interface DocumentRegistration {
|
|
5
|
+
name: string
|
|
6
|
+
label: string
|
|
7
|
+
description: string
|
|
8
|
+
group: string
|
|
9
|
+
preview?: Component
|
|
10
|
+
load: () => Promise<{ default: ComponentDocument }>
|
|
11
|
+
}
|
|
12
|
+
export const componentDocuments: Record<string, DocumentRegistration> = {
|
|
13
|
+
'apple-input': {
|
|
14
|
+
name: 'AppleInput', label: '输入框', group: '表单',
|
|
15
|
+
description: '从输入、反馈到辅助操作,逐条了解输入框的行为与接口。',
|
|
16
|
+
preview: defineAsyncComponent(() => import('./AppleInputPreview.vue')),
|
|
17
|
+
load: () => import('./apple-input'),
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
export const componentDocumentPath = (slug: string) => `/component-docs/${slug}`
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { icons } from 'lucide-vue-next'
|
|
2
|
+
import exampleSource from './AppleInputExample.vue?raw'
|
|
3
|
+
|
|
4
|
+
export function createInputConfig() {
|
|
5
|
+
return {
|
|
6
|
+
type: 'text', label: '姓名', placeholder: '怎么称呼你', hint: '用于在应用中显示你的名称。', error: '',
|
|
7
|
+
clearable: true, disabled: false, loading: false, required: false,
|
|
8
|
+
prefixMode: 'none', prefix: '', prefixIcon: 'Mail', suffixMode: 'none', suffix: '', suffixIcon: 'Search', motion: 'inherit' as 'inherit' | 'auto' | 'full' | 'reduced' | 'none',
|
|
9
|
+
name: 'name', ariaLabel: '', maxlength: '', autocomplete: 'off', min: '', max: '', step: '',
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export type InputConfig = ReturnType<typeof createInputConfig>
|
|
13
|
+
export function buildInputProps(config: InputConfig) {
|
|
14
|
+
return {
|
|
15
|
+
type: config.type, label: config.label, placeholder: config.placeholder, hint: config.hint, error: config.error,
|
|
16
|
+
clearable: config.clearable, disabled: config.disabled, loading: config.loading, required: config.required,
|
|
17
|
+
motion: config.motion, name: config.name || undefined,
|
|
18
|
+
'aria-label': config.ariaLabel || (!config.label ? '演示输入框' : undefined),
|
|
19
|
+
maxlength: config.maxlength !== '' && Number.isInteger(Number(config.maxlength)) && Number(config.maxlength) >= 0 ? Number(config.maxlength) : undefined,
|
|
20
|
+
autocomplete: config.autocomplete,
|
|
21
|
+
min: config.type === 'number' && config.min !== '' ? config.min : undefined,
|
|
22
|
+
max: config.type === 'number' && config.max !== '' ? config.max : undefined,
|
|
23
|
+
step: config.type === 'number' && Number(config.step) > 0 ? config.step : undefined,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const iconNames = new Map(Object.keys(icons).map(name => [name.toLowerCase(), name]))
|
|
27
|
+
export function resolveIconName(code: string): string | undefined {
|
|
28
|
+
return iconNames.get(code.trim().replace(/-/g, '').toLowerCase())
|
|
29
|
+
}
|
|
30
|
+
export function iconCodeError(code: string): string {
|
|
31
|
+
return resolveIconName(code) ? '' : '请输入有效图标代号,例如 Mail、Search、ArrowRight。'
|
|
32
|
+
}
|
|
33
|
+
// This Vue source is both the editable file and the compiled preview.
|
|
34
|
+
const literal = (value: unknown) => JSON.stringify(value).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029')
|
|
35
|
+
const attribute = (value: string) => value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>')
|
|
36
|
+
const text = (value: string) => attribute(value).replace(/{/g, '{').replace(/}/g, '}')
|
|
37
|
+
export function generateInputCode(config: InputConfig, value: string | number) {
|
|
38
|
+
const attrs = Object.entries(buildInputProps(config)).flatMap(([name, next]) => {
|
|
39
|
+
if (next === undefined || next === '' || next === false) return []
|
|
40
|
+
return [typeof next === 'boolean' ? ` ${name}` : typeof next === 'number' ? ` :${name}="${next}"` : ` ${name}="${attribute(next)}"`]
|
|
41
|
+
}).join('\n')
|
|
42
|
+
const prefixIcon = config.prefixMode === 'icon' ? resolveIconName(config.prefixIcon) : undefined
|
|
43
|
+
const suffixIcon = config.suffixMode === 'icon' ? resolveIconName(config.suffixIcon) : undefined
|
|
44
|
+
const usedIcons = [...new Set([prefixIcon, suffixIcon].filter((name): name is string => !!name))]
|
|
45
|
+
const affix = (slot: string, mode: string, value: string, icon?: string) => {
|
|
46
|
+
if (mode === 'icon' && icon) return `<template #${slot}><${icon} :size="18" aria-hidden="true" /></template>`
|
|
47
|
+
return mode === 'text' ? `<template #${slot}><span>${text(value)}</span></template>` : ''
|
|
48
|
+
}
|
|
49
|
+
const prefix = affix('prefix', config.prefixMode, config.prefix, prefixIcon)
|
|
50
|
+
const suffix = affix('suffix', config.suffixMode, config.suffix, suffixIcon)
|
|
51
|
+
return exampleSource
|
|
52
|
+
.replace("import { Mail } from 'lucide-vue-next'", () => usedIcons.length ? `import { ${usedIcons.join(', ')} } from 'lucide-vue-next'` : '')
|
|
53
|
+
.replace(/, Mail(?= })/, () => usedIcons.length ? ', ' + usedIcons.join(', ') : '')
|
|
54
|
+
.replace("value: 'Apptify'", () => `value: ${literal(value)}`)
|
|
55
|
+
.replace(/(v-model="value"\n)[\s\S]*?(\n\s+@update:model-value)/, (_, start, end) => `${start}${attrs}${end}`)
|
|
56
|
+
.replace('<!-- prefix -->', () => prefix)
|
|
57
|
+
.replace('<!-- suffix -->', () => suffix)
|
|
58
|
+
.replace(':disabled="false"', () => config.disabled || config.loading ? 'disabled' : '')
|
|
59
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentDocument } from './contract'
|
|
2
|
+
/** One section map drives both document content and its API navigation tree. */
|
|
3
|
+
export function documentSections(doc: ComponentDocument) {
|
|
4
|
+
return [
|
|
5
|
+
{ id: 'props', kicker: 'PROPS', title: '属性', entries: doc.props },
|
|
6
|
+
{ id: 'events', kicker: 'EVENTS', title: '事件', entries: doc.events },
|
|
7
|
+
{ id: 'slots', kicker: 'SLOTS', title: '插槽', entries: doc.slots },
|
|
8
|
+
{ id: 'methods', kicker: 'METHODS', title: '公开方法', entries: doc.methods },
|
|
9
|
+
].filter(section => section.entries.length)
|
|
10
|
+
}
|
|
@@ -5,6 +5,14 @@ button { font: inherit; }
|
|
|
5
5
|
#app > .apple-provider { min-height: 100dvh; }
|
|
6
6
|
.apple-auto-size__inner { position: relative; }
|
|
7
7
|
.route-page { display: contents; }
|
|
8
|
+
.site-footer { width: 100%; margin-inline: auto; margin-top: 16px; padding-inline: 48px; background: var(--apple-surface-alt); }
|
|
9
|
+
.route-page:has(.components-page) + .site-footer > .page-footer { margin-inline: auto; }
|
|
10
|
+
@media (max-width: 1100px) {
|
|
11
|
+
.site-footer { padding-inline: 28px; }
|
|
12
|
+
}
|
|
13
|
+
@media (max-width: 640px) {
|
|
14
|
+
.site-footer { padding-inline: 18px; }
|
|
15
|
+
}
|
|
8
16
|
.brand { display: inline-flex; align-items: center; gap: 10px; color: var(--apple-text); text-decoration: none; font-weight: 650; font-size: 20px; }
|
|
9
17
|
.brand-symbol { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; background: var(--apple-text); color: var(--apple-surface); font-weight: 600; font-size: 24px; line-height: 1; padding-bottom: 4px; font-family: Georgia,serif; }
|
|
10
18
|
.user-trigger { width: 38px; height: 38px; border: 1px solid var(--apple-border); border-radius: 50%; display: grid; place-items: center; background: var(--apple-surface); color: var(--apple-text); transition: background var(--apple-fast); }
|
|
@@ -47,9 +55,10 @@ button { font: inherit; }
|
|
|
47
55
|
.more-components { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 38px 0; }
|
|
48
56
|
.more-components h2 { margin: 0 0 8px; font-size: 19px; font-weight: 600; }
|
|
49
57
|
.more-components span { font-size: 12px; color: var(--apple-secondary); }
|
|
50
|
-
.page-footer { padding: 24px 0; border
|
|
58
|
+
.page-footer { padding: 24px 0; border: 0; font-size: 12px; color: var(--apple-secondary); place-items: center; text-align: center }
|
|
51
59
|
.specimen-grid { display: grid; grid-template-columns: 1.04fr 1fr 1fr; gap: 18px; }
|
|
52
|
-
.specimen { min-width: 0;
|
|
60
|
+
.specimen { min-width: 0; }
|
|
61
|
+
.specimen > .apple-card__body { height: 100%; display: flex; flex-direction: column; padding: 9px 12px 10px; }
|
|
53
62
|
.specimen-caption { display: flex; align-items: center; justify-content: space-between; color: var(--apple-secondary); font-size: 9px; margin-bottom: 22px; }
|
|
54
63
|
.specimen h2 { font-size: 21px; font-weight: 600; line-height: 1.3; margin: 0 0 25px; }
|
|
55
64
|
.button-specimens { display: flex; align-items: center; flex-direction: column; gap: 15px; padding: 9px 0 22px; }
|
|
@@ -62,10 +71,10 @@ button { font: inherit; }
|
|
|
62
71
|
.preference-demo .apple-field__label { display: none; }
|
|
63
72
|
.preference-demo .apple-slider .apple-field__label { display: block; }
|
|
64
73
|
.setting-line { display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid color-mix(in srgb,var(--apple-border) 55%,transparent); padding: 3px 0 13px; }
|
|
65
|
-
.setting-line > div:first-child { min-width: 0; }
|
|
74
|
+
.setting-line > div:first-child { flex: 1; min-width: 0; }
|
|
66
75
|
.setting-line strong { display: block; font-size: 12px; font-weight: 500; }
|
|
67
76
|
.setting-line small { display: block; font-size: 10px; color: var(--apple-secondary); margin-top: 4px; }
|
|
68
|
-
.setting-line .apple-field { flex-shrink: 0; }
|
|
77
|
+
.setting-line .apple-field { flex-shrink: 0; width: auto; }
|
|
69
78
|
.check-row { padding-top: 5px; }
|
|
70
79
|
.check-row .apple-choice__label { font-size: 11px; }
|
|
71
80
|
.form-specimen .apple-form { display: flex; flex-direction: column; gap: 13px; }
|
|
@@ -80,33 +89,29 @@ button { font: inherit; }
|
|
|
80
89
|
.feedback-actions { margin-top: auto; display: flex; flex-wrap: wrap; gap: 3px; padding-top: 25px; }
|
|
81
90
|
.feedback-actions .apple-button { font-size: 11px; }
|
|
82
91
|
|
|
83
|
-
.specimen > .apple-search { margin-bottom: 12px; }
|
|
84
|
-
.
|
|
85
|
-
.
|
|
86
|
-
.
|
|
87
|
-
.
|
|
88
|
-
.
|
|
89
|
-
.
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
.index
|
|
94
|
-
|
|
95
|
-
.component-index a:hover:where(:not([data-apple-touch] *)) { color: var(--apple-text); background: var(--apple-hover); }
|
|
96
|
-
.index-items a.active { background: var(--apple-selected); color: var(--apple-text); font-weight: 500; }
|
|
97
|
-
.components-page { margin-left: 232px; width: calc(100% - 232px); max-width: none; padding: 42px 48px 0; }
|
|
98
|
-
.components-page > * { max-width: 1000px; margin-inline: auto; }
|
|
92
|
+
.specimen > .apple-card__body > .apple-search { margin-bottom: 12px; }
|
|
93
|
+
.directory-layout { display: grid; grid-template-columns: 224px minmax(0, 1fr); gap: 32px; align-items: start; }
|
|
94
|
+
.directory-index { position: sticky; top: 100px; max-height: calc(100dvh - 120px); overflow: auto; overscroll-behavior: contain; scrollbar-width: thin; padding-top: 16px; font-size: 13px; min-width: 0; }
|
|
95
|
+
.directory-index > span { color: var(--apple-secondary); font-size: 12px; display: block; margin: 0 0 12px 8px; }
|
|
96
|
+
.directory-index .apple-tree__row { min-height: 34px; }
|
|
97
|
+
.directory-index .apple-tree__row > span:last-child { min-width: 0; overflow-wrap: anywhere; }
|
|
98
|
+
.components-page { padding: 40px 40px 0; }
|
|
99
|
+
.components-content { min-width: 0; }
|
|
100
|
+
@media (max-width: 900px) {
|
|
101
|
+
.directory-layout { grid-template-columns: minmax(0, 1fr); gap: 20px; }
|
|
102
|
+
.directory-index { display: none; }
|
|
103
|
+
}
|
|
99
104
|
.feed-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding-bottom: 14px; border-bottom: 1px solid var(--apple-border); }
|
|
100
105
|
.feed-count { font-size: 12px; font-weight: 500; }
|
|
101
106
|
.feed-count small { margin-left: 7px; color: var(--apple-secondary); font-size: 10px; font-variant-numeric: tabular-nums; }
|
|
102
107
|
.preview-controls { display: flex; gap: 4px; }
|
|
103
|
-
.mobile-index-toggle { display: none; }
|
|
104
108
|
.feed-group { padding-bottom: 16px; }
|
|
105
109
|
.feed-group-heading { display: flex; align-items: baseline; gap: 12px; padding: 32px 0 20px; }
|
|
106
110
|
.feed-group-heading > span { color: var(--apple-secondary); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
107
111
|
.feed-group-heading h2 { margin: 0; font-size: 20px; font-weight: 600; }
|
|
108
112
|
.feed-group-heading small { margin-left: auto; font-size: 11px; color: var(--apple-secondary); }
|
|
109
|
-
.component-card { min-width: 0;
|
|
113
|
+
.component-card { min-width: 0; margin-bottom: 20px; }
|
|
114
|
+
.component-card > .apple-card__body { padding: 0; }
|
|
110
115
|
.component-card-heading { display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 12px; padding: 24px 28px 0; }
|
|
111
116
|
.component-card-heading h3 { font-size: 16px; font-weight: 600; margin: 0; }
|
|
112
117
|
.component-card-heading h3 a { display: inline-flex; align-items: center; gap: 8px; color: inherit; text-decoration: none; }
|
|
@@ -119,7 +124,6 @@ button { font: inherit; }
|
|
|
119
124
|
.detail-preview .component-demo--compact { max-width: 420px; }
|
|
120
125
|
.detail-preview .component-demo--media { max-width: 560px; }
|
|
121
126
|
.detail-preview.mobile-preview { max-width: 390px; margin-inline: auto; }
|
|
122
|
-
.component-card > .component-source { border-radius: 0 0 9px 9px; }
|
|
123
127
|
.component-source { border-top: 1px solid color-mix(in srgb,var(--apple-border) 45%,transparent); }
|
|
124
128
|
.code-view { padding: 4px 28px 20px; min-width: 0; }
|
|
125
129
|
.code-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; color: var(--apple-secondary); font-size: 12px; }
|
|
@@ -132,9 +136,10 @@ pre { padding: 18px; overflow: auto; background: var(--apple-surface-alt); borde
|
|
|
132
136
|
.component-demo .apple-form__fields { gap: 20px; }
|
|
133
137
|
.component-demo .apple-form__fields > .apple-button { justify-self: start; }
|
|
134
138
|
.component-demo .apple-grid { align-items: start; }
|
|
135
|
-
.demo-layout-tile {
|
|
136
|
-
.demo-layout-tile >
|
|
137
|
-
.demo-layout-tile >
|
|
139
|
+
.demo-layout-tile { min-height: 112px; }
|
|
140
|
+
.demo-layout-tile > .apple-card__body { display: flex; flex-direction: column; justify-content: center; gap: 12px; padding: 8px; }
|
|
141
|
+
.demo-layout-tile > .apple-card__body > span { color: var(--apple-secondary); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
142
|
+
.demo-layout-tile > .apple-card__body > strong { font-size: 16px; font-weight: 550; }
|
|
138
143
|
.component-demo > .apple-stack > .apple-provider { border-radius: 10px; }
|
|
139
144
|
.component-demo[data-component="AppleSpinner"] .apple-spinner { align-self: flex-start; }
|
|
140
145
|
.component-demo[data-component="AppleDivider"] .apple-divider:not(.apple-divider--vertical) { width: 100%; }
|
|
@@ -150,10 +155,10 @@ pre { padding: 18px; overflow: auto; background: var(--apple-surface-alt); borde
|
|
|
150
155
|
.demo-copy { margin: 0; padding: 8px 0; color: var(--apple-secondary); }
|
|
151
156
|
.demo-slide { display: block; width: 100%; aspect-ratio: 16/10; object-fit: cover; }
|
|
152
157
|
|
|
153
|
-
.demo-floating-preview { padding:
|
|
158
|
+
.demo-floating-preview > .apple-card__body { padding: 12px; }
|
|
154
159
|
.demo-floating-preview .apple-floating-group { position: relative; inset: auto; flex-direction: row; justify-content: center; padding: 12px 0; z-index: auto; }
|
|
155
|
-
.settings-
|
|
156
|
-
.settings-section
|
|
160
|
+
.settings-section { margin-bottom: 22px; }
|
|
161
|
+
.settings-section > .apple-card__body { padding: 16px; }
|
|
157
162
|
.settings-section-heading { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 26px; }
|
|
158
163
|
.settings-section-heading > svg { margin-top: 3px; color: var(--apple-secondary); }
|
|
159
164
|
.settings-section h2 { font-size: 19px; font-weight: 600; margin: 0; }
|
|
@@ -167,20 +172,16 @@ pre { padding: 18px; overflow: auto; background: var(--apple-surface-alt); borde
|
|
|
167
172
|
.theme-swatch i:last-child { width: 35%; }
|
|
168
173
|
.theme-label { display: flex; align-items: center; justify-content: space-between; gap: 4px; width: 100%; min-height: 20px; font-size: 12px; padding: 0 2px 3px; }
|
|
169
174
|
.theme-label svg { color: var(--apple-accent); flex-shrink: 0; }
|
|
170
|
-
.settings-section > .apple-segmented { max-width: 440px; }
|
|
175
|
+
.settings-section > .apple-card__body > .apple-segmented { max-width: 440px; }
|
|
171
176
|
.settings-note { color: var(--apple-secondary); font-size: 12px; line-height: 1.8; min-height: 22px; margin: 14px 0 24px; }
|
|
172
|
-
.motion-preview
|
|
177
|
+
.motion-preview > .apple-card__body { padding: 2px 8px 10px; }
|
|
173
178
|
.motion-preview-heading { display: flex; justify-content: space-between; align-items: center; gap: 12px; font-size: 12px; color: var(--apple-secondary); margin-bottom: 14px; }
|
|
174
179
|
.motion-preview-content p { margin: 18px 0 0; font-size: 13px; color: var(--apple-secondary); line-height: 1.8; }
|
|
175
180
|
.settings-reset { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--apple-secondary); font-size: 12px; margin: 26px 0 48px; }
|
|
176
181
|
.not-found-page { max-width: 740px; }
|
|
177
|
-
@media (min-width: 1600px) { .main-content.home-page { max-width: 1420px; } .specimen-grid { gap: 24px; } .specimen { padding:
|
|
178
|
-
@media (max-width: 1100px) { .main-content { padding-inline: 28px; } .components-page { padding-inline: 32px; } .
|
|
182
|
+
@media (min-width: 1600px) { .main-content.home-page { max-width: 1420px; } .specimen-grid { gap: 24px; } .specimen > .apple-card__body { padding: 14px 18px; } }
|
|
183
|
+
@media (max-width: 1100px) { .main-content { padding-inline: 28px; } .components-page { padding-inline: 32px; } .specimen-grid { grid-template-columns: 1fr 1fr; } }
|
|
179
184
|
@media (max-width: 800px) {
|
|
180
|
-
.sidebar { display: none; }
|
|
181
|
-
.components-page { margin-left: 0; width: 100%; }
|
|
182
|
-
.mobile-index-toggle { display: inline-flex; align-items: center; gap: 8px; min-height: 40px; padding: 0; border: 0; background: transparent; color: var(--apple-text); font-size: 12px; }
|
|
183
|
-
.feed-count { display: none; }
|
|
184
185
|
.theme-options { gap: 8px; }
|
|
185
186
|
}
|
|
186
187
|
@media (max-width: 640px) {
|
|
@@ -198,7 +199,7 @@ pre { padding: 18px; overflow: auto; background: var(--apple-surface-alt); borde
|
|
|
198
199
|
.mobile-break { display: block; }
|
|
199
200
|
.section-heading { align-items: flex-start; flex-direction: column; gap: 7px; padding-top: 22px; }
|
|
200
201
|
.specimen-grid { grid-template-columns: 1fr; gap: 16px; }
|
|
201
|
-
.specimen { padding:
|
|
202
|
+
.specimen > .apple-card__body { padding: 12px; }
|
|
202
203
|
.specimen h2 { font-size: 23px; }
|
|
203
204
|
.specimen-caption { font-size: 10px; }
|
|
204
205
|
.button-specimens > .apple-button { min-height: 44px; }
|
|
@@ -218,7 +219,7 @@ pre { padding: 18px; overflow: auto; background: var(--apple-surface-alt); borde
|
|
|
218
219
|
.detail-preview { padding: 28px 16px; }
|
|
219
220
|
.code-view { padding-inline: 16px; }
|
|
220
221
|
pre { padding: 14px; font-size: 11px; }
|
|
221
|
-
.settings-section { padding:
|
|
222
|
+
.settings-section > .apple-card__body { padding: 10px 6px; }
|
|
222
223
|
.theme-options { grid-template-columns: repeat(3,minmax(0,1fr)); gap: 12px; }
|
|
223
224
|
.theme-swatch { height: 68px; padding: 12px; }
|
|
224
225
|
.theme-label { font-size: 11px; }
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<router-link v-slot="{ href, navigate }" :to="homePath" custom>
|
|
13
13
|
<apple-button :href="href" @click="navigate">返回首页</apple-button>
|
|
14
14
|
</router-link>
|
|
15
|
-
<apple-
|
|
15
|
+
<apple-link as="button" @click="goBack"><ArrowLeft :size="16" aria-hidden="true" /> 返回上一页</apple-link>
|
|
16
16
|
</div>
|
|
17
17
|
</apple-card>
|
|
18
18
|
<NotFoundPaperScene class="paper-404-illustration" />
|
|
@@ -45,7 +45,7 @@ function goBack() {
|
|
|
45
45
|
.paper-404-heading h1 { font-size: clamp(26px, 2.7vw, 35px); line-height: 1.35; letter-spacing: -.035em; font-weight: 600; margin: 0 0 17px; }
|
|
46
46
|
.paper-404-copy { font-size: 13px; line-height: 1.95; color: var(--apple-secondary); margin: 0; text-wrap: pretty; }
|
|
47
47
|
.paper-404-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 27px; }
|
|
48
|
-
.paper-404-actions .apple-
|
|
48
|
+
.paper-404-actions .apple-link { font-size: 12px; padding-inline: 8px; color: var(--apple-secondary); }
|
|
49
49
|
.paper-404-illustration { min-width: 0; }
|
|
50
50
|
.paper-404-footnote { grid-column: 1 / -1; text-align: center; font-size: 10px; letter-spacing: .09em; color: var(--apple-secondary); margin: 48px 0 0; }
|
|
51
51
|
@media (max-width: 880px) {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<apple-container style="padding-top: 12px; padding-bottom: 24px">
|
|
3
|
+
<template v-if="registration">
|
|
4
|
+
<apple-breadcrumbs :items="breadcrumbs" label="组件文档路径" @click="navigateBreadcrumb" />
|
|
5
|
+
<header class="documentation-heading"><div><span class="page-kicker">{{ registration.group }} / COMPONENT API</span><h1>{{ registration.label }}<span>{{ registration.name }}</span></h1><p>{{ registration.description }}</p></div></header>
|
|
6
|
+
<div class="documentation-layout directory-layout">
|
|
7
|
+
<aside ref="documentationIndex" class="documentation-index directory-index" aria-label="API 导航"><span>本页接口</span><apple-tree v-model="selectedApi" v-model:expanded="expandedSections" :items="apiTree" label="组件 API 树" @select="navigateApi" /></aside>
|
|
8
|
+
<div ref="documentationContent" class="documentation-content">
|
|
9
|
+
<section class="component-overview" aria-label="组件演示"><component :is="registration.preview" v-if="registration.preview" /></section>
|
|
10
|
+
<ComponentWorkshop v-if="doc" :doc="doc" />
|
|
11
|
+
<apple-alert v-else-if="failed" tone="danger" title="文档加载失败"><apple-button variant="secondary" @click="loadDocument">重试</apple-button></apple-alert>
|
|
12
|
+
<apple-spinner v-else label="正在加载组件 API" />
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
<apple-empty v-else title="组件文档尚未开放" description="当前阶段仅开放 AppleInput 样板。"><router-link to="/components" custom v-slot="{ href, navigate }"><apple-link :href="href" @click="navigate">返回组件目录</apple-link></router-link></apple-empty>
|
|
17
|
+
</apple-container>
|
|
18
|
+
<DirectoryDialog v-if="registration" v-model="mobileIndex" title="本页接口">
|
|
19
|
+
<apple-tree v-model="selectedApi" v-model:expanded="expandedSections" :items="apiTree" label="组件 API 树" @select="navigateApi" />
|
|
20
|
+
</DirectoryDialog>
|
|
21
|
+
</template>
|
|
22
|
+
<script setup lang="ts">
|
|
23
|
+
import { computed, shallowRef, ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
24
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
25
|
+
import type { AppleItem, AppleTreeItem, AppleValue } from 'apptify'
|
|
26
|
+
import { componentDocuments } from '../../editor/documents'
|
|
27
|
+
import type { ComponentDocument } from '../../editor/contract'
|
|
28
|
+
import { documentSections } from '../../editor/sections'
|
|
29
|
+
import ComponentWorkshop from '../../editor/ComponentWorkshop.vue'
|
|
30
|
+
import DirectoryDialog from '../../components/DirectoryDialog.vue'
|
|
31
|
+
const route = useRoute()
|
|
32
|
+
const router = useRouter()
|
|
33
|
+
const breadcrumbs = computed(() => [
|
|
34
|
+
{ label: '组件', value: 'components', href: '/components' },
|
|
35
|
+
{ label: registration.value?.group || '组件', value: 'group', href: `/components#${String(route.params.component)}` },
|
|
36
|
+
{ label: registration.value?.label || '', value: 'current' },
|
|
37
|
+
])
|
|
38
|
+
function navigateBreadcrumb(item: AppleItem, event: MouseEvent) {
|
|
39
|
+
if (!item.href || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
|
|
40
|
+
event.preventDefault()
|
|
41
|
+
void router.push(item.href)
|
|
42
|
+
}
|
|
43
|
+
const registration = computed(() => componentDocuments[String(route.params.component)])
|
|
44
|
+
const doc = shallowRef<ComponentDocument>()
|
|
45
|
+
const failed = ref(false)
|
|
46
|
+
let loadVersion = 0
|
|
47
|
+
async function loadDocument() {
|
|
48
|
+
const version = ++loadVersion
|
|
49
|
+
doc.value = undefined; failed.value = false
|
|
50
|
+
const current = registration.value
|
|
51
|
+
if (!current) return
|
|
52
|
+
try { const result = await current.load(); if (version === loadVersion) doc.value = result.default }
|
|
53
|
+
catch { if (version === loadVersion) failed.value = true }
|
|
54
|
+
}
|
|
55
|
+
watch(registration, loadDocument, { immediate: true })
|
|
56
|
+
onBeforeUnmount(() => { loadVersion++ })
|
|
57
|
+
const documentationIndex = ref<HTMLElement>()
|
|
58
|
+
const documentationContent = ref<HTMLElement>()
|
|
59
|
+
const mobileIndex = ref(false)
|
|
60
|
+
const selectedApi = ref<AppleValue>('props')
|
|
61
|
+
const expandedSections = ref<AppleValue[]>(['props', 'events', 'slots', 'methods'])
|
|
62
|
+
const apiTree = computed<AppleTreeItem[]>(() => doc.value ? [
|
|
63
|
+
...documentSections(doc.value).map(section => ({
|
|
64
|
+
value: section.id, label: section.title,
|
|
65
|
+
children: section.entries.map(entry => ({ value: entry.id, label: entry.name })),
|
|
66
|
+
})),
|
|
67
|
+
...(doc.value.notes.length ? [{ value: 'accessibility', label: '交互与无障碍' }] : []),
|
|
68
|
+
] : [])
|
|
69
|
+
watch(() => [route.hash, doc.value] as const, () => {
|
|
70
|
+
const id = route.hash.slice(1)
|
|
71
|
+
const group = apiTree.value.find(item => item.children?.some(child => child.value === id))
|
|
72
|
+
if (!group && !apiTree.value.some(item => item.value === id)) return
|
|
73
|
+
selectApi(id)
|
|
74
|
+
}, { immediate: true })
|
|
75
|
+
function selectApi(id: AppleValue) {
|
|
76
|
+
selectedApi.value = id
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let scrollFrame = 0
|
|
80
|
+
let contentObserver: ResizeObserver | undefined
|
|
81
|
+
let indexObserver: ResizeObserver | undefined
|
|
82
|
+
function syncScrollSelection() {
|
|
83
|
+
scrollFrame = 0
|
|
84
|
+
const content = documentationContent.value
|
|
85
|
+
if (!content) return
|
|
86
|
+
const targets = apiTree.value.flatMap(section => [section, ...(section.children || [])])
|
|
87
|
+
.map(item => ({ id: item.value, element: document.getElementById(String(item.value)) }))
|
|
88
|
+
.filter(target => target.element && content.contains(target.element))
|
|
89
|
+
if (!targets.length) return
|
|
90
|
+
let active = targets[0].id
|
|
91
|
+
for (const target of targets) {
|
|
92
|
+
const offset = Math.max(100, parseFloat(getComputedStyle(target.element!).scrollMarginTop) || 0)
|
|
93
|
+
if (target.element!.getBoundingClientRect().top > offset + 1) break
|
|
94
|
+
active = target.id
|
|
95
|
+
}
|
|
96
|
+
// The final section may be too short to reach the reading line above the footer.
|
|
97
|
+
if (window.scrollY > 0 && window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2) {
|
|
98
|
+
active = targets[targets.length - 1].id
|
|
99
|
+
}
|
|
100
|
+
selectApi(active)
|
|
101
|
+
}
|
|
102
|
+
function queueScrollSelection() {
|
|
103
|
+
if (!scrollFrame) scrollFrame = requestAnimationFrame(syncScrollSelection)
|
|
104
|
+
}
|
|
105
|
+
function revealSelection() {
|
|
106
|
+
const index = documentationIndex.value
|
|
107
|
+
const row = index?.querySelector<HTMLElement>('.apple-tree__row.is-selected')
|
|
108
|
+
if (!index || !row) return
|
|
109
|
+
const bounds = index.getBoundingClientRect(), selected = row.getBoundingClientRect()
|
|
110
|
+
if (selected.top < bounds.top) index.scrollTop += selected.top - bounds.top
|
|
111
|
+
else if (selected.bottom > bounds.bottom) index.scrollTop += selected.bottom - bounds.bottom
|
|
112
|
+
}
|
|
113
|
+
watch([selectedApi, expandedSections], async () => {
|
|
114
|
+
await nextTick()
|
|
115
|
+
revealSelection()
|
|
116
|
+
}, { deep: true })
|
|
117
|
+
watch(doc, async () => {
|
|
118
|
+
await nextTick()
|
|
119
|
+
contentObserver?.disconnect()
|
|
120
|
+
if (documentationContent.value) contentObserver?.observe(documentationContent.value)
|
|
121
|
+
queueScrollSelection()
|
|
122
|
+
}, { flush: 'post' })
|
|
123
|
+
onMounted(() => {
|
|
124
|
+
window.addEventListener('scroll', queueScrollSelection, { passive: true })
|
|
125
|
+
window.addEventListener('resize', queueScrollSelection)
|
|
126
|
+
contentObserver = new ResizeObserver(queueScrollSelection)
|
|
127
|
+
if (documentationContent.value) contentObserver.observe(documentationContent.value)
|
|
128
|
+
indexObserver = new ResizeObserver(revealSelection)
|
|
129
|
+
const tree = documentationIndex.value?.querySelector<HTMLElement>('.apple-tree-size')
|
|
130
|
+
if (tree) indexObserver.observe(tree)
|
|
131
|
+
queueScrollSelection()
|
|
132
|
+
})
|
|
133
|
+
onBeforeUnmount(() => {
|
|
134
|
+
window.removeEventListener('scroll', queueScrollSelection)
|
|
135
|
+
window.removeEventListener('resize', queueScrollSelection)
|
|
136
|
+
contentObserver?.disconnect()
|
|
137
|
+
indexObserver?.disconnect()
|
|
138
|
+
cancelAnimationFrame(scrollFrame)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
async function navigateApi(item: AppleTreeItem) {
|
|
142
|
+
if (item.children?.length) return
|
|
143
|
+
mobileIndex.value = false
|
|
144
|
+
const id = String(item.value)
|
|
145
|
+
await nextTick()
|
|
146
|
+
await router.replace({ hash: `#${id}` })
|
|
147
|
+
await nextTick()
|
|
148
|
+
document.getElementById(id)?.scrollIntoView({ block: 'start' })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
</script>
|
|
152
|
+
<style scoped>
|
|
153
|
+
.documentation-page { max-width: 1440px; margin: 0 auto; padding: 40px; }
|
|
154
|
+
.documentation-page > :deep(.apple-link) { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; }
|
|
155
|
+
.documentation-heading { margin: 12px 0 36px 0; }
|
|
156
|
+
.documentation-heading h1 { margin: 12px 0; font-size: clamp(32px, 4vw, 48px); letter-spacing: -.04em; }
|
|
157
|
+
.documentation-heading h1 span { display: inline-block; margin-left: 18px; color: var(--apple-secondary); font-weight: 400; font-size: 24px; letter-spacing: -.02em; }
|
|
158
|
+
.documentation-heading p { color: var(--apple-secondary); font-size: 15px; line-height: 1.7; }
|
|
159
|
+
.documentation-content { min-width: 0; }
|
|
160
|
+
.component-overview { margin-bottom: 32px; }
|
|
161
|
+
@media (max-width: 720px) { .documentation-page { padding: 40px; } .documentation-heading { margin: 24px 0; } .documentation-heading h1 span { display: block; margin: 8px 0 0; font-size: 20px; } }
|
|
162
|
+
</style>
|