apptify 0.6.1 → 0.7.0
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 +1 -1
- package/THIRD_PARTY_LICENSES.txt +23 -0
- package/dist/apptify.css +1 -1
- package/dist/apptify.js +2395 -2451
- package/dist/components/AppleCard.vue.d.ts +1 -1
- package/dist/components/content.d.ts +81 -20
- package/dist/components/forms.d.ts +2 -2
- package/dist/components/foundation.d.ts +133 -11
- package/dist/components/mobile-image-viewer.d.ts +35 -12
- package/dist/components/navibar.d.ts +2 -2
- package/dist/components/overlays.d.ts +10 -59
- package/dist/components/scrollbar.d.ts +88 -0
- package/dist/components/speed-dial.d.ts +157 -0
- package/dist/core/context.d.ts +11 -0
- package/dist/core/device.d.ts +2 -0
- package/dist/core/image-gallery-motion.d.ts +2 -1
- package/dist/core/image-motion.d.ts +1 -1
- package/dist/core/layers.d.ts +1 -0
- package/dist/core/motion-driver.d.ts +40 -0
- package/dist/core/motion.d.ts +1 -1
- package/dist/core/ripple.d.ts +16 -0
- package/dist/core/scroll-motion.d.ts +6 -0
- package/dist/core/scrollbar.d.ts +14 -0
- package/dist/core/speed-dial.d.ts +21 -0
- package/dist/index.d.ts +237 -26
- package/docs/COMPONENTS.md +10 -6
- package/package.json +5 -7
- package/templates/playground/README.md +1 -1
- package/templates/playground/package.json +1 -2
- package/templates/playground/source-snapshot.json +299 -66
- package/templates/playground/src/App.vue +1 -0
- package/templates/playground/src/ComponentDemo.vue +30 -21
- package/templates/playground/src/catalog.ts +70 -65
- package/templates/playground/src/components/ComponentIndex.vue +10 -14
- package/templates/playground/src/components/MasonryFeed.vue +52 -0
- package/templates/playground/src/components/UserMenu.vue +1 -1
- package/templates/playground/src/components/forbidden-paper-scene.vue +367 -0
- package/templates/playground/src/components/forbidden-paper-story.ts +233 -0
- package/templates/playground/src/editor/ComponentPlayground.vue +89 -0
- package/templates/playground/src/editor/ConfigurableEditor.vue +1 -1
- package/templates/playground/src/editor/apple-input-source.ts +1 -1
- package/templates/playground/src/editor/component-metadata.ts +72 -0
- package/templates/playground/src/editor/component-playground.ts +162 -0
- package/templates/playground/src/editor/contract.ts +1 -1
- package/templates/playground/src/editor/documents.ts +11 -2
- package/templates/playground/src/editor/handbook.ts +22 -0
- package/templates/playground/src/editor/handbooks/apple-accordion.ts +66 -0
- package/templates/playground/src/editor/handbooks/apple-action-sheet.ts +72 -0
- package/templates/playground/src/editor/handbooks/apple-alert.ts +67 -0
- package/templates/playground/src/editor/handbooks/apple-aside.ts +32 -0
- package/templates/playground/src/editor/handbooks/apple-auto-size.ts +32 -0
- package/templates/playground/src/editor/handbooks/apple-autocomplete.ts +114 -0
- package/templates/playground/src/editor/handbooks/apple-avatar-group.ts +50 -0
- package/templates/playground/src/editor/handbooks/apple-avatar.ts +56 -0
- package/templates/playground/src/editor/handbooks/apple-back-top.ts +62 -0
- package/templates/playground/src/editor/handbooks/apple-badge.ts +62 -0
- package/templates/playground/src/editor/handbooks/apple-breadcrumbs.ts +38 -0
- package/templates/playground/src/editor/handbooks/apple-button.ts +92 -0
- package/templates/playground/src/editor/handbooks/apple-card.ts +112 -0
- package/templates/playground/src/editor/handbooks/apple-cascader.ts +102 -0
- package/templates/playground/src/editor/handbooks/apple-checkbox.ts +85 -0
- package/templates/playground/src/editor/handbooks/apple-color-picker.ts +79 -0
- package/templates/playground/src/editor/handbooks/apple-container.ts +26 -0
- package/templates/playground/src/editor/handbooks/apple-date-picker.ts +104 -0
- package/templates/playground/src/editor/handbooks/apple-dialog.ts +151 -0
- package/templates/playground/src/editor/handbooks/apple-divider.ts +38 -0
- package/templates/playground/src/editor/handbooks/apple-drawer.ts +151 -0
- package/templates/playground/src/editor/handbooks/apple-empty.ts +43 -0
- package/templates/playground/src/editor/handbooks/apple-floating-group.ts +51 -0
- package/templates/playground/src/editor/handbooks/apple-form-field.ts +69 -0
- package/templates/playground/src/editor/handbooks/apple-form.ts +76 -0
- package/templates/playground/src/editor/handbooks/apple-grid.ts +32 -0
- package/templates/playground/src/editor/handbooks/apple-image.ts +122 -0
- package/templates/playground/src/editor/handbooks/apple-infinite-scroll.ts +73 -0
- package/templates/playground/src/editor/handbooks/apple-link.ts +68 -0
- package/templates/playground/src/editor/handbooks/apple-list.ts +67 -0
- package/templates/playground/src/editor/handbooks/apple-menu.ts +72 -0
- package/templates/playground/src/editor/handbooks/apple-navibar.ts +100 -0
- package/templates/playground/src/editor/handbooks/apple-otp-input.ts +108 -0
- package/templates/playground/src/editor/handbooks/apple-overlay-host.ts +13 -0
- package/templates/playground/src/editor/handbooks/apple-pagination.ts +61 -0
- package/templates/playground/src/editor/handbooks/apple-popover.ts +112 -0
- package/templates/playground/src/editor/handbooks/apple-progress.ts +56 -0
- package/templates/playground/src/editor/handbooks/apple-provider.ts +32 -0
- package/templates/playground/src/editor/handbooks/apple-pull-refresh.ts +61 -0
- package/templates/playground/src/editor/handbooks/apple-radio-group.ts +91 -0
- package/templates/playground/src/editor/handbooks/apple-rate.ts +97 -0
- package/templates/playground/src/editor/handbooks/apple-scroll-bar.ts +15 -0
- package/templates/playground/src/editor/handbooks/apple-search.ts +49 -0
- package/templates/playground/src/editor/handbooks/apple-segmented-control.ts +85 -0
- package/templates/playground/src/editor/handbooks/apple-select.ts +91 -0
- package/templates/playground/src/editor/handbooks/apple-sheet.ts +151 -0
- package/templates/playground/src/editor/handbooks/apple-skeleton.ts +74 -0
- package/templates/playground/src/editor/handbooks/apple-slider.ts +103 -0
- package/templates/playground/src/editor/handbooks/apple-snackbar.ts +89 -0
- package/templates/playground/src/editor/handbooks/apple-speed-dial-item.ts +16 -0
- package/templates/playground/src/editor/handbooks/apple-speed-dial.ts +13 -0
- package/templates/playground/src/editor/handbooks/apple-spinner.ts +38 -0
- package/templates/playground/src/editor/handbooks/apple-stack.ts +44 -0
- package/templates/playground/src/editor/handbooks/apple-statistic.ts +69 -0
- package/templates/playground/src/editor/handbooks/apple-stepper.ts +91 -0
- package/templates/playground/src/editor/handbooks/apple-steps.ts +61 -0
- package/templates/playground/src/editor/handbooks/apple-switch.ts +79 -0
- package/templates/playground/src/editor/handbooks/apple-tab-bar.ts +66 -0
- package/templates/playground/src/editor/handbooks/apple-table.ts +176 -0
- package/templates/playground/src/editor/handbooks/apple-tabs.ts +72 -0
- package/templates/playground/src/editor/handbooks/apple-tag.ts +56 -0
- package/templates/playground/src/editor/handbooks/apple-textarea.ts +109 -0
- package/templates/playground/src/editor/handbooks/apple-timeline.ts +44 -0
- package/templates/playground/src/editor/handbooks/apple-tooltip.ts +45 -0
- package/templates/playground/src/editor/handbooks/apple-transition.ts +44 -0
- package/templates/playground/src/editor/handbooks/apple-tree.ts +84 -0
- package/templates/playground/src/editor/handbooks/apple-upload.ts +126 -0
- package/templates/playground/src/editor/playground-examples.ts +105 -0
- package/templates/playground/src/images.ts +3 -0
- package/templates/playground/src/router/index.ts +19 -2
- package/templates/playground/src/style.css +4 -4
- package/templates/playground/src/views/403.vue +65 -0
- package/templates/playground/src/views/component-docs/[component].vue +11 -14
- package/templates/playground/src/views/components.vue +11 -41
- package/templates/playground/src/views/index.vue +3 -1
- package/templates/playground/src/views/settings.vue +5 -2
- package/tooling/create-playground.mjs +2 -2
- package/tooling/prepare-playground.mjs +3 -3
- package/tooling/test-create-apptify.mjs +3 -1
- package/tooling/test-playground-package.mjs +10 -0
- package/dist/components/gallery-scrollbar.d.ts +0 -46
- package/dist/core/desktop-image-enter.d.ts +0 -6
- package/dist/core/desktop-image-return.d.ts +0 -1
- package/templates/playground/src/components/DirectoryDialog.vue +0 -20
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div data-testid="component-playground" :data-component="doc.name">
|
|
3
|
+
<LiveVueEditor :files="files" :baseline-files="baselineFiles" active-file="App.vue" preview-surface reset-label="恢复当前配置" @restore="reset">
|
|
4
|
+
<template #configuration>
|
|
5
|
+
<section class="configuration" role="region" :aria-label="`${doc.name} 配置`">
|
|
6
|
+
<div class="config-heading"><h3>配置 {{ doc.name }}</h3><AppleLink as="button" @click="reset">重置</AppleLink></div>
|
|
7
|
+
<div class="config-grid">
|
|
8
|
+
<div v-for="entry in doc.props" :key="entry.name" class="prop-control" :data-prop-control="entry.name">
|
|
9
|
+
<AppleCheckbox v-if="entry.default === 'undefined' && !entry.required" v-model="config.props[entry.name].enabled" :label="`使用 ${entry.name}`" />
|
|
10
|
+
<AppleSwitch v-if="controls[entry.name].kind === 'boolean'" :model-value="config.props[entry.name].value === true" @update:model-value="config.props[entry.name].value = $event" :label="entry.name" :disabled="!config.props[entry.name].enabled" />
|
|
11
|
+
<AppleSelect v-else-if="controls[entry.name].kind === 'select'" :model-value="String(config.props[entry.name].value)" @update:model-value="config.props[entry.name].value = String($event)" :items="controls[entry.name].options.map(value => ({ label: value, value }))" :label="entry.name" :disabled="!config.props[entry.name].enabled" />
|
|
12
|
+
<AppleTextarea v-else-if="['json', 'expression'].includes(controls[entry.name].kind)" :model-value="String(config.props[entry.name].value)" @update:model-value="config.props[entry.name].value = String($event)" :label="entry.name" :hint="controls[entry.name].kind === 'json' ? 'JSON' : 'JavaScript 表达式'" :rows="3" :error="errors[entry.name] || ''" :disabled="!config.props[entry.name].enabled" />
|
|
13
|
+
<AppleInput v-else :model-value="String(config.props[entry.name].value)" @update:model-value="config.props[entry.name].value = String($event)" :label="entry.name" :error="errors[entry.name] || ''" :disabled="!config.props[entry.name].enabled" />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<AppleAccordion v-if="doc.slots.length" :items="[{ label: '插槽', value: 'slots' }]">
|
|
17
|
+
<template #item>
|
|
18
|
+
<div class="slot-controls">
|
|
19
|
+
<div v-for="entry in doc.slots" :key="entry.name" class="slot-control">
|
|
20
|
+
<AppleSwitch v-model="config.slots[entry.name].enabled" :label="entry.name" />
|
|
21
|
+
<template v-if="config.slots[entry.name].enabled">
|
|
22
|
+
<AppleInput v-if="entry.name.includes('${')" v-model="config.slots[entry.name].name" :label="`${entry.name} 名称`" />
|
|
23
|
+
<AppleTextarea v-model="config.slots[entry.name].code" :label="`${entry.name} 模板`" :rows="4" />
|
|
24
|
+
</template>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
28
|
+
</AppleAccordion>
|
|
29
|
+
<AppleAlert v-if="compileError" tone="danger" title="配置有误" :message="compileError" />
|
|
30
|
+
</section>
|
|
31
|
+
</template>
|
|
32
|
+
</LiveVueEditor>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import { computed, defineAsyncComponent, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue'
|
|
38
|
+
import { parse, compileScript, compileTemplate } from 'vue/compiler-sfc'
|
|
39
|
+
import { AppleAccordion, AppleAlert, AppleCheckbox, AppleInput, AppleLink, AppleSelect, AppleSwitch, AppleTextarea } from 'apptify'
|
|
40
|
+
import type { ComponentDocument } from './contract'
|
|
41
|
+
import { createPlaygroundConfig, generateComponentCode, propControl, propExpression } from './component-playground'
|
|
42
|
+
|
|
43
|
+
const props = defineProps<{ doc: ComponentDocument }>()
|
|
44
|
+
const LiveVueEditor = defineAsyncComponent(() => import('./LiveVueEditor.vue'))
|
|
45
|
+
const config = reactive(createPlaygroundConfig(props.doc))
|
|
46
|
+
const baselineFiles = { 'App.vue': generateComponentCode(props.doc, createPlaygroundConfig(props.doc)) }
|
|
47
|
+
const files = shallowRef(baselineFiles)
|
|
48
|
+
const errors = ref<Record<string, string>>({})
|
|
49
|
+
const compileError = ref('')
|
|
50
|
+
const controls = computed(() => Object.fromEntries(props.doc.props.map(entry => [entry.name, propControl(entry)])))
|
|
51
|
+
let updateTimer: ReturnType<typeof setTimeout> | undefined
|
|
52
|
+
|
|
53
|
+
function update() {
|
|
54
|
+
errors.value = {}
|
|
55
|
+
for (const entry of props.doc.props) {
|
|
56
|
+
if (!config.props[entry.name].enabled) continue
|
|
57
|
+
try { propExpression(entry, config.props[entry.name]) }
|
|
58
|
+
catch (error) { errors.value[entry.name] = error instanceof Error ? error.message : String(error) }
|
|
59
|
+
}
|
|
60
|
+
if (Object.keys(errors.value).length) { compileError.value = '请修正标记的属性。'; return }
|
|
61
|
+
try {
|
|
62
|
+
const code = generateComponentCode(props.doc, config)
|
|
63
|
+
const { descriptor, errors: parseErrors } = parse(code, { filename: 'App.vue' })
|
|
64
|
+
if (parseErrors.length) throw parseErrors[0]
|
|
65
|
+
const script = compileScript(descriptor, { id: 'component-playground' })
|
|
66
|
+
const template = compileTemplate({ source: descriptor.template!.content, filename: 'App.vue', id: 'component-playground', compilerOptions: { bindingMetadata: script.bindings } })
|
|
67
|
+
if (template.errors.length) throw template.errors[0]
|
|
68
|
+
compileError.value = ''
|
|
69
|
+
files.value = { 'App.vue': code }
|
|
70
|
+
} catch (error) { compileError.value = error instanceof Error ? error.message : String(error) }
|
|
71
|
+
}
|
|
72
|
+
watch(config, () => { clearTimeout(updateTimer); updateTimer = setTimeout(update, 150) }, { deep: true })
|
|
73
|
+
function reset() {
|
|
74
|
+
clearTimeout(updateTimer)
|
|
75
|
+
Object.assign(config, createPlaygroundConfig(props.doc))
|
|
76
|
+
update()
|
|
77
|
+
}
|
|
78
|
+
onBeforeUnmount(() => clearTimeout(updateTimer))
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<style scoped>
|
|
82
|
+
.configuration, .slot-controls, .slot-control { display: grid; gap: 20px; min-width: 0; }
|
|
83
|
+
.config-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
84
|
+
.config-heading h3 { margin: 0; font-size: 16px; overflow-wrap: anywhere; }
|
|
85
|
+
.config-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
|
|
86
|
+
.prop-control { display: grid; align-content: start; gap: 10px; min-width: 0; }
|
|
87
|
+
.slot-control + .slot-control { border-top: 1px solid var(--apple-border); padding-top: 20px; }
|
|
88
|
+
@media (max-width: 1100px) { .config-grid { grid-template-columns: minmax(0, 1fr); } }
|
|
89
|
+
</style>
|
|
@@ -26,7 +26,7 @@ defineExpose({ getEditorIns: () => editor.value?.getEditorIns() })
|
|
|
26
26
|
.controls-pane { flex: 1; min-height: 0; overflow: auto; padding: 20px; }
|
|
27
27
|
/* Vue REPL resets every descendant button, including the library select trigger. */
|
|
28
28
|
.controls-pane :deep(.apple-select) { border: 1px solid var(--apple-border); background: var(--apple-surface); }
|
|
29
|
-
.controls-pane :deep(.apple-select:hover:
|
|
29
|
+
.controls-pane :deep(.apple-select:hover:not(:disabled)) { border-color: color-mix(in srgb, var(--apple-secondary) 60%, var(--apple-border)); }
|
|
30
30
|
.controls-pane :deep(.apple-select:active:not(:disabled)) { background: color-mix(in srgb, var(--apple-text) 7%, var(--apple-surface)); }
|
|
31
31
|
.controls-pane :deep(.apple-select:focus) { border-color: var(--apple-accent); }
|
|
32
32
|
.controls-pane :deep(.apple-field.has-error .apple-select) { border-color: var(--apple-danger); }
|
|
@@ -1,2 +1,2 @@
|
|
|
1
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:
|
|
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: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: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: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,72 @@
|
|
|
1
|
+
export interface ComponentMetadata { label: string; group: string; description: string }
|
|
2
|
+
|
|
3
|
+
export const componentMetadata: Record<string, ComponentMetadata> = {
|
|
4
|
+
AppleScrollBar: { label: '滚动条', group: '基础', description: '悬浮在内容之上。' },
|
|
5
|
+
AppleButton: {"label":"按钮","group":"基础","description":"清晰的操作层级。"},
|
|
6
|
+
AppleLink: {"label":"链接","group":"基础","description":"让下一步清楚可见。"},
|
|
7
|
+
AppleSearch: {"label":"搜索","group":"基础","description":"快速找到关心的内容。"},
|
|
8
|
+
AppleProvider: {"label":"主题容器","group":"基础","description":"独立区域,也有统一的设计语言。"},
|
|
9
|
+
AppleOverlayHost: {"label":"弹层宿主","group":"基础","description":"显示当前上下文的弹层与通知。"},
|
|
10
|
+
AppleContainer: {"label":"内容容器","group":"基础","description":"在不同屏幕上保留合适的内容宽度。"},
|
|
11
|
+
AppleStack: {"label":"弹性布局","group":"基础","description":"有序排列,自然换行。"},
|
|
12
|
+
AppleGrid: {"label":"网格布局","group":"基础","description":"让列数随内容空间变化。"},
|
|
13
|
+
AppleCard: {"label":"卡片","group":"基础","description":"来自 Apple Store 的产品表达。"},
|
|
14
|
+
AppleImage: {"label":"图片与预览","group":"基础","description":"让值得细看的内容更近一步。"},
|
|
15
|
+
AppleAvatar: {"label":"头像","group":"基础","description":"为每一位用户保留位置。"},
|
|
16
|
+
AppleAvatarGroup: {"label":"头像组","group":"基础","description":"一起参与的人,一眼可见。"},
|
|
17
|
+
AppleBadge: {"label":"徽标","group":"基础","description":"不打扰,也不遗漏。"},
|
|
18
|
+
AppleTag: {"label":"标签","group":"基础","description":"轻量的分类与状态。"},
|
|
19
|
+
AppleDivider: {"label":"分隔线","group":"基础","description":"恰到好处的分界。"},
|
|
20
|
+
AppleInput: {"label":"输入框","group":"表单","description":"每一次输入,都自然流畅。"},
|
|
21
|
+
AppleTextarea: {"label":"多行输入","group":"表单","description":"为更长的想法留白。"},
|
|
22
|
+
AppleSelect: {"label":"选择器","group":"表单","description":"从容找到你的选择。"},
|
|
23
|
+
AppleAutocomplete: {"label":"自动补全","group":"表单","description":"想到哪里,找到哪里。"},
|
|
24
|
+
AppleCheckbox: {"label":"复选框","group":"表单","description":"一个,或不止一个。"},
|
|
25
|
+
AppleRadioGroup: {"label":"单选框","group":"表单","description":"专注于一个明确的选择。"},
|
|
26
|
+
AppleSwitch: {"label":"开关","group":"表单","description":"简单的一开一关。"},
|
|
27
|
+
AppleSlider: {"label":"滑块","group":"表单","description":"把控制权交给指尖。"},
|
|
28
|
+
AppleStepper: {"label":"步进器","group":"表单","description":"精确一点,也轻松一点。"},
|
|
29
|
+
AppleSegmentedControl: {"label":"分段控制","group":"表单","description":"在相邻的视图间切换。"},
|
|
30
|
+
AppleDatePicker: {"label":"日期与时间","group":"表单","description":"为值得记住的日子。"},
|
|
31
|
+
AppleColorPicker: {"label":"颜色选择","group":"表单","description":"找到属于你的颜色。"},
|
|
32
|
+
AppleUpload: {"label":"文件上传","group":"表单","description":"拖入文件,继续创作。"},
|
|
33
|
+
AppleOtpInput: {"label":"验证码","group":"表单","description":"验证步骤,也可以很顺手。"},
|
|
34
|
+
AppleCascader: {"label":"级联选择","group":"表单","description":"从整体,找到细节。"},
|
|
35
|
+
AppleRate: {"label":"评分","group":"表单","description":"记录真实的感受。"},
|
|
36
|
+
AppleForm: {"label":"表单验证","group":"表单","description":"把完整提交做好。"},
|
|
37
|
+
AppleFormField: {"label":"表单字段","group":"表单","description":"为自定义控件补齐标签与验证信息。"},
|
|
38
|
+
AppleNavibar: {"label":"顶部导航栏","group":"导航","description":"随可用宽度折叠的顶部导航。"},
|
|
39
|
+
AppleAside: {"label":"侧栏容器","group":"导航","description":"图片归位后,导航自然恢复。"},
|
|
40
|
+
AppleTabs: {"label":"标签页","group":"导航","description":"内容各归其位。"},
|
|
41
|
+
AppleTabBar: {"label":"页签栏","group":"导航","description":"相邻页面,自然衔接。"},
|
|
42
|
+
AppleBreadcrumbs: {"label":"面包屑","group":"导航","description":"始终知道身在何处。"},
|
|
43
|
+
ApplePagination: {"label":"分页","group":"导航","description":"一步步,轻松浏览。"},
|
|
44
|
+
AppleSteps: {"label":"步骤条","group":"导航","description":"每一步,都有进展。"},
|
|
45
|
+
AppleAccordion: {"label":"折叠面板","group":"数据展示","description":"需要时,再展开。"},
|
|
46
|
+
AppleTable: {"label":"数据表格","group":"数据展示","description":"复杂信息,清晰呈现。"},
|
|
47
|
+
AppleTree: {"label":"树形结构","group":"数据展示","description":"有层次,也有条理。"},
|
|
48
|
+
AppleList: {"label":"列表","group":"数据展示","description":"内容优先的日常界面。"},
|
|
49
|
+
AppleTimeline: {"label":"时间线","group":"数据展示","description":"把发生的事串联起来。"},
|
|
50
|
+
AppleStatistic: {"label":"统计数值","group":"数据展示","description":"重要数据,一目了然。"},
|
|
51
|
+
AppleDialog: {"label":"对话框","group":"反馈","description":"重要的决定,值得专注。"},
|
|
52
|
+
AppleDrawer: {"label":"抽屉","group":"反馈","description":"让补充信息就近展开。"},
|
|
53
|
+
AppleSheet: {"label":"底部面板","group":"反馈","description":"为拇指设计的临时空间。"},
|
|
54
|
+
AppleSnackbar: {"label":"轻提示","group":"反馈","description":"及时回应,不必打断。"},
|
|
55
|
+
AppleAlert: {"label":"提示信息","group":"反馈","description":"让状态清楚可见。"},
|
|
56
|
+
AppleProgress: {"label":"进度条","group":"反馈","description":"等待,也能心中有数。"},
|
|
57
|
+
AppleSpinner: {"label":"加载中","group":"反馈","description":"给等待一个轻巧的回应。"},
|
|
58
|
+
AppleSkeleton: {"label":"骨架屏","group":"反馈","description":"内容到达之前。"},
|
|
59
|
+
AppleEmpty: {"label":"空状态","group":"反馈","description":"没有内容,也不空洞。"},
|
|
60
|
+
ApplePopover: {"label":"气泡面板","group":"反馈","description":"让更多操作触手可及。"},
|
|
61
|
+
AppleTooltip: {"label":"文字提示","group":"反馈","description":"在需要的时候多说一句。"},
|
|
62
|
+
AppleMenu: {"label":"下拉菜单","group":"反馈","description":"按需展开的操作列表。"},
|
|
63
|
+
AppleActionSheet: {"label":"动作面板","group":"移动交互","description":"熟悉、直接的移动操作。"},
|
|
64
|
+
ApplePullRefresh: {"label":"下拉刷新","group":"移动交互","description":"轻轻下拉,获取新内容。"},
|
|
65
|
+
AppleBackTop: {"label":"回到顶部","group":"移动交互","description":"回到开始,毫不费力。"},
|
|
66
|
+
AppleFloatingGroup: {"label":"悬浮操作组","group":"移动交互","description":"常用操作,触手可及。"},
|
|
67
|
+
AppleSpeedDial: {"label":"全局快捷操作","group":"移动交互","description":"目录与常用操作,集中在一处。"},
|
|
68
|
+
AppleSpeedDialItem: {"label":"快捷操作项","group":"移动交互","description":"页面声明,全局呈现。"},
|
|
69
|
+
AppleAutoSize: {"label":"自适应尺寸","group":"基础","description":"内容变化,布局随行。"},
|
|
70
|
+
AppleTransition: {"label":"动效容器","group":"基础","description":"为页面保留连续感。"},
|
|
71
|
+
AppleInfiniteScroll: {"label":"无限滚动","group":"移动交互","description":"浏览,不必被翻页打断。"},
|
|
72
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { ApiEntry, ComponentDocument } from './contract'
|
|
2
|
+
import { playgroundExample, slotExample } from './playground-examples'
|
|
3
|
+
|
|
4
|
+
export type ControlKind = 'boolean' | 'text' | 'number' | 'select' | 'json' | 'expression'
|
|
5
|
+
export interface PropControl { name: string; kind: ControlKind; options: string[]; required: boolean }
|
|
6
|
+
export interface PropValue { enabled: boolean; value: string | boolean }
|
|
7
|
+
export interface SlotValue { enabled: boolean; name: string; scope?: string; code: string }
|
|
8
|
+
export interface PlaygroundConfig { props: Record<string, PropValue>; slots: Record<string, SlotValue> }
|
|
9
|
+
|
|
10
|
+
export function propControl(entry: ApiEntry): PropControl {
|
|
11
|
+
const options = [...entry.type.matchAll(/'([^']+)'/g)].map(match => match[1])
|
|
12
|
+
const enumType = options.length > 0 && /^('[^']+'\s*(\|\s*('[^']+'|undefined)\s*)*)$/.test(entry.type)
|
|
13
|
+
const kind: ControlKind = entry.type === 'boolean' ? 'boolean'
|
|
14
|
+
: enumType ? 'select' : entry.type === 'string' ? 'text' : entry.type === 'number' ? 'number'
|
|
15
|
+
: /=>|Component|File\[\]/.test(entry.type) ? 'expression'
|
|
16
|
+
: /\[\]|Record</.test(entry.type) && !entry.type.includes('undefined') ? 'json' : 'expression'
|
|
17
|
+
return { name: entry.name, kind, options, required: !!entry.required }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function editableValue(control: PropControl, expression: string): string | boolean {
|
|
21
|
+
if (control.kind === 'boolean') return expression === 'true'
|
|
22
|
+
if (control.kind === 'text' || control.kind === 'select') {
|
|
23
|
+
if (expression === 'undefined') return ''
|
|
24
|
+
try { return String(JSON.parse(expression)) } catch { return expression.replace(/^'|'$/g, '') }
|
|
25
|
+
}
|
|
26
|
+
if (control.kind === 'json') {
|
|
27
|
+
try { return JSON.stringify(JSON.parse(expression), null, 2) } catch { return expression === 'undefined' ? '[]' : expression }
|
|
28
|
+
}
|
|
29
|
+
return expression === 'undefined' ? '' : expression
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createPlaygroundConfig(doc: ComponentDocument): PlaygroundConfig {
|
|
33
|
+
const example = playgroundExample(doc.name)
|
|
34
|
+
return {
|
|
35
|
+
props: Object.fromEntries(doc.props.map(entry => {
|
|
36
|
+
const expression = example.props?.[entry.name] ?? entry.default ?? 'undefined'
|
|
37
|
+
return [entry.name, { enabled: expression !== 'undefined' || !!entry.required, value: editableValue(propControl(entry), expression) }]
|
|
38
|
+
})),
|
|
39
|
+
slots: Object.fromEntries(doc.slots.map(entry => {
|
|
40
|
+
const example = slotExample(doc.name, entry.name)
|
|
41
|
+
const name = entry.name.replace('${value}', 'first').replace('${key}', 'name')
|
|
42
|
+
return [entry.name, { ...example, enabled: !!example.enabled, name }]
|
|
43
|
+
})),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const scriptLiteral = (value: unknown) => JSON.stringify(value).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029')
|
|
48
|
+
const attr = (value: string) => value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>')
|
|
49
|
+
const kebab = (value: string) => value.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
50
|
+
|
|
51
|
+
export function propExpression(entry: ApiEntry, input: PropValue): string {
|
|
52
|
+
const control = propControl(entry)
|
|
53
|
+
if (control.kind === 'boolean') return String(input.value === true)
|
|
54
|
+
if (control.kind === 'text' || control.kind === 'select') return scriptLiteral(String(input.value))
|
|
55
|
+
if (control.kind === 'json') return scriptLiteral(JSON.parse(String(input.value)))
|
|
56
|
+
const expression = String(input.value).trim()
|
|
57
|
+
if (control.kind === 'number' && !/^-?(?:Infinity|(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)$/i.test(expression)) throw new Error(`${entry.name}:请输入数字或 Infinity。`)
|
|
58
|
+
if (!expression) throw new Error(`${entry.name}:请输入 JavaScript 表达式,或关闭该属性。`)
|
|
59
|
+
return expression.replace(/<\/script/gi, '<\\/script')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function generateComponentCode(doc: ComponentDocument, config: PlaygroundConfig): string {
|
|
63
|
+
const example = playgroundExample(doc.name)
|
|
64
|
+
const enabledProps = doc.props.filter(entry => config.props[entry.name].enabled)
|
|
65
|
+
const expressions = enabledProps.map(entry => ` ${scriptLiteral(entry.name)}: (${propExpression(entry, config.props[entry.name])})`).join(',\n')
|
|
66
|
+
const bindings = enabledProps.map(entry => ` :${kebab(entry.name)}="props.${entry.name}"`)
|
|
67
|
+
const listeners = doc.events.map(entry => {
|
|
68
|
+
let handler = `(...args) => record('${entry.name}', ...args)`
|
|
69
|
+
if (entry.name.startsWith('update:')) handler = `(value) => { props.${entry.name.slice(7)} = value; record('${entry.name}', value) }`
|
|
70
|
+
if (['refresh', 'load', 'retry'].includes(entry.name)) handler = `(done) => finishBatch('${entry.name}', done)`
|
|
71
|
+
if (doc.name === 'AppleForm' && entry.name === 'reset') handler = `() => { fieldValue = ''; record('reset') }`
|
|
72
|
+
return ` @${kebab(entry.name)}="${attr(handler)}"`
|
|
73
|
+
})
|
|
74
|
+
const slots = Object.values(config.slots).filter(slot => slot.enabled).map(slot => {
|
|
75
|
+
if (!/^[\w-]+$/.test(slot.name)) throw new Error('插槽名称只能包含字母、数字、下划线和连字符。')
|
|
76
|
+
return ` <template #${slot.name}${slot.scope ? `="${attr(slot.scope)}"` : ''}>${slot.code}</template>`
|
|
77
|
+
}).join('\n')
|
|
78
|
+
const component = ` <${doc.name}\n ref="demo"\n${[...bindings, ...listeners].join('\n')}\n >\n${slots}\n </${doc.name}>`
|
|
79
|
+
const names = [...new Set([doc.name, 'AppleButton', 'AppleInput', 'AppleLink', 'AppleTextarea'])]
|
|
80
|
+
const methods = doc.methods.map(method => ({ label: method.name, expression: `$refs.demo.${method.name.split('(')[0]}()` }))
|
|
81
|
+
const modal = ['AppleDialog', 'AppleDrawer', 'AppleSheet', 'AppleActionSheet', 'AppleSnackbar', 'AppleAlert'].includes(doc.name)
|
|
82
|
+
const actions = [...(modal ? [{ label: '打开 / 重新显示', expression: 'props.modelValue = true' }] : []), ...methods, ...(example.actions || [])]
|
|
83
|
+
const actionButtons = actions.length ? ` <div class="actions">${actions.map(action => `<AppleLink as="button" @click="${attr(action.expression)}">${attr(action.label)}</AppleLink>`).join('')}</div>` : ''
|
|
84
|
+
const modelProps = doc.props.filter(prop => doc.events.some(event => event.name === `update:${prop.name}`))
|
|
85
|
+
const modelReadout = modelProps.map(prop => `<output>${prop.name} = {{ display(props.${prop.name}) }}</output>`).join('\n ')
|
|
86
|
+
return `<script>
|
|
87
|
+
import { ${names.join(', ')} } from 'apptify'
|
|
88
|
+
import { markRaw } from 'vue'
|
|
89
|
+
import { Mail, Star, Check, Search, ArrowRight } from 'lucide-vue-next'
|
|
90
|
+
|
|
91
|
+
;[Mail, Star, Check, Search, ArrowRight].forEach(markRaw)
|
|
92
|
+
|
|
93
|
+
export default {
|
|
94
|
+
components: { ${names.join(', ')}, Mail, Star, Check, Search, ArrowRight },
|
|
95
|
+
data() {
|
|
96
|
+
return {
|
|
97
|
+
props: {
|
|
98
|
+
${expressions}
|
|
99
|
+
},
|
|
100
|
+
fieldValue: 'Apptify', expanded: false, batches: 0, events: [], eventId: 0,
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
computed: {
|
|
104
|
+
eventLog() { return this.events.join('\\n') },
|
|
105
|
+
},
|
|
106
|
+
methods: {
|
|
107
|
+
display(value) {
|
|
108
|
+
if (value === undefined) return 'undefined'
|
|
109
|
+
const seen = new WeakSet()
|
|
110
|
+
return JSON.stringify(value, (_, current) => {
|
|
111
|
+
if (typeof current === 'function') return '[function]'
|
|
112
|
+
if (current instanceof FormData) return Object.fromEntries(current.entries())
|
|
113
|
+
if (current instanceof File) return { name: current.name, size: current.size, type: current.type }
|
|
114
|
+
if (current instanceof Event) return { type: current.type }
|
|
115
|
+
if (current && typeof current === 'object') { if (seen.has(current)) return '[circular]'; seen.add(current) }
|
|
116
|
+
return current
|
|
117
|
+
})
|
|
118
|
+
},
|
|
119
|
+
record(name, ...args) {
|
|
120
|
+
this.events.push(++this.eventId + '. ' + name + ' ' + (args.length ? args.map(this.display).join(' · ') : '无参数'))
|
|
121
|
+
if (this.events.length > 12) this.events.shift()
|
|
122
|
+
},
|
|
123
|
+
finishBatch(name, done) {
|
|
124
|
+
this.record(name, done)
|
|
125
|
+
this.batches++
|
|
126
|
+
if (name !== 'refresh' && this.batches >= 3) this.props.finished = true
|
|
127
|
+
if (name === 'retry') this.props.error = false
|
|
128
|
+
if (typeof done === 'function') done()
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
<\/script>
|
|
133
|
+
|
|
134
|
+
<template>
|
|
135
|
+
<div class="example">
|
|
136
|
+
<section class="component-section" aria-label="${doc.name} 示例">
|
|
137
|
+
${example.before || ''}
|
|
138
|
+
${actionButtons}
|
|
139
|
+
${component}
|
|
140
|
+
${modelReadout}
|
|
141
|
+
</section>
|
|
142
|
+
<section v-if="${doc.events.length > 0}" class="events-section" aria-label="事件记录">
|
|
143
|
+
<div class="event-heading"><h3>事件记录</h3><AppleLink as="button" @click="events = []">清除记录</AppleLink></div>
|
|
144
|
+
<AppleTextarea :model-value="eventLog" readonly :rows="6" :resize="false" aria-label="事件记录内容" placeholder="等待操作…" />
|
|
145
|
+
</section>
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|
|
148
|
+
|
|
149
|
+
<style scoped>
|
|
150
|
+
.example { display: grid; gap: 28px; padding: 28px 24px; min-width: 0; }
|
|
151
|
+
.component-section { display: grid; gap: 20px; min-width: 0; }
|
|
152
|
+
.actions { display: flex; flex-wrap: wrap; gap: 20px; }
|
|
153
|
+
.events-section { border-top: 1px solid var(--apple-border); padding-top: 20px; min-width: 0; }
|
|
154
|
+
.event-heading { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
|
155
|
+
h3 { margin: 0; font-size: 14px; }
|
|
156
|
+
output { color: var(--apple-secondary); font: 12px/1.6 ui-monospace, monospace; overflow-wrap: anywhere; }
|
|
157
|
+
.tile { padding: 20px; border-radius: 8px; background: var(--apple-surface-alt); min-width: 0; }
|
|
158
|
+
.scroll-demo { height: 220px; overflow: auto; padding: 16px; border: 1px solid var(--apple-border); border-radius: 8px; }
|
|
159
|
+
.example :deep(.apple-provider) { padding: 20px; display: grid; gap: 16px; border-radius: 8px; }
|
|
160
|
+
@media (max-width: 480px) { .example { padding: 24px 16px; } }
|
|
161
|
+
</style>`
|
|
162
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Public API descriptions are required; component-specific runnable examples are optional. */
|
|
2
2
|
export interface VueExample { title: string; description: string; code: string }
|
|
3
3
|
export interface ApiDefinition { name: string; type: string; default?: string; description: string }
|
|
4
|
-
export interface ApiEntry extends ApiDefinition { id: string; example?: VueExample }
|
|
4
|
+
export interface ApiEntry extends ApiDefinition { id: string; required?: boolean; example?: VueExample }
|
|
5
5
|
export interface ComponentDocument {
|
|
6
6
|
name: string
|
|
7
7
|
source: string
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineAsyncComponent, type Component } from 'vue'
|
|
2
2
|
import type { ComponentDocument } from './contract'
|
|
3
|
+
import { componentMetadata } from './component-metadata'
|
|
3
4
|
/** Lightweight route directory. Each document is downloaded only on its own page. */
|
|
4
5
|
export interface DocumentRegistration {
|
|
5
6
|
name: string
|
|
@@ -9,12 +10,20 @@ export interface DocumentRegistration {
|
|
|
9
10
|
preview?: Component
|
|
10
11
|
load: () => Promise<{ default: ComponentDocument }>
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
+
const handbooks = import.meta.glob<{ default: ComponentDocument }>('./handbooks/apple-*.ts')
|
|
14
|
+
export const componentDocuments: Record<string, DocumentRegistration> = Object.fromEntries(Object.entries(componentMetadata).map(([name, item]) => {
|
|
15
|
+
const slug = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
|
16
|
+
return [slug, {
|
|
17
|
+
name, ...item,
|
|
18
|
+
load: handbooks[`./handbooks/${slug}.ts`],
|
|
19
|
+
}]
|
|
20
|
+
}))
|
|
21
|
+
Object.assign(componentDocuments, {
|
|
13
22
|
'apple-input': {
|
|
14
23
|
name: 'AppleInput', label: '输入框', group: '表单',
|
|
15
24
|
description: '从输入、反馈到辅助操作,逐条了解输入框的行为与接口。',
|
|
16
25
|
preview: defineAsyncComponent(() => import('./AppleInputPreview.vue')),
|
|
17
26
|
load: () => import('./apple-input'),
|
|
18
27
|
},
|
|
19
|
-
}
|
|
28
|
+
} satisfies Record<string, DocumentRegistration>)
|
|
20
29
|
export const componentDocumentPath = (slug: string) => `/component-docs/${slug}`
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ApiDefinition, ComponentDocument } from './contract'
|
|
2
|
+
|
|
3
|
+
type Definition = Omit<ComponentDocument, 'props' | 'events' | 'slots' | 'methods'> & {
|
|
4
|
+
props: (ApiDefinition & { required?: boolean })[]
|
|
5
|
+
events: ApiDefinition[]
|
|
6
|
+
slots: ApiDefinition[]
|
|
7
|
+
methods: ApiDefinition[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function defineHandbook(definition: Definition): ComponentDocument {
|
|
11
|
+
const entries = (group: string, list: ApiDefinition[]) => list.map(entry => ({
|
|
12
|
+
...entry,
|
|
13
|
+
id: `${group}-${entry.name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/[^\w-]+/g, '-').replace(/-+$/, '').toLowerCase()}`,
|
|
14
|
+
}))
|
|
15
|
+
return {
|
|
16
|
+
...definition,
|
|
17
|
+
props: entries('prop', definition.props),
|
|
18
|
+
events: entries('event', definition.events),
|
|
19
|
+
slots: entries('slot', definition.slots),
|
|
20
|
+
methods: entries('method', definition.methods),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { defineHandbook } from '../handbook'
|
|
2
|
+
|
|
3
|
+
export default defineHandbook({
|
|
4
|
+
"name": "AppleAccordion",
|
|
5
|
+
"source": "src/components/content.ts",
|
|
6
|
+
"props": [
|
|
7
|
+
{
|
|
8
|
+
"name": "motion",
|
|
9
|
+
"type": "'inherit' | 'auto' | 'full' | 'reduced' | 'none'",
|
|
10
|
+
"default": "\"inherit\"",
|
|
11
|
+
"description": "局部动效偏好。inherit 跟随外层;全局与系统的减少动效设置仍是上限。"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "items",
|
|
15
|
+
"type": "AppleItem[]",
|
|
16
|
+
"default": "[]",
|
|
17
|
+
"description": "按顺序显示的选项。每项 value 唯一,保留字符串和数字的类型区别。结构:{ label: string; value: string | number; disabled?: boolean; description?: string; href?: string; content?: string }。"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "modelValue",
|
|
21
|
+
"type": "string | number | (string | number)[] | undefined",
|
|
22
|
+
"default": "undefined",
|
|
23
|
+
"description": "展开项 value;multiple=true 时使用 value 数组。单选收起最后一项时发出 undefined。省略模型时内部管理。"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "multiple",
|
|
27
|
+
"type": "boolean",
|
|
28
|
+
"default": "false",
|
|
29
|
+
"description": "允许同时展开多项;开启时更新事件返回数组。"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "disabled",
|
|
33
|
+
"type": "boolean",
|
|
34
|
+
"default": "false",
|
|
35
|
+
"description": "禁用控件的交互。选项自身的 disabled 仍然独立生效。"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"events": [
|
|
39
|
+
{
|
|
40
|
+
"name": "update:modelValue",
|
|
41
|
+
"type": "(value: string | number | (string | number)[] | undefined) => void",
|
|
42
|
+
"description": "交互更新时发出新模型值;可使用 v-model 接收。"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "change",
|
|
46
|
+
"type": "(value: string | number | (string | number)[] | undefined) => void",
|
|
47
|
+
"description": "用户提交选择或调整时发出当前值;直接从父组件赋值不会主动发出此事件。"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"slots": [
|
|
51
|
+
{
|
|
52
|
+
"name": "item",
|
|
53
|
+
"type": "({ item: AppleItem, open: boolean }) => VNode[]",
|
|
54
|
+
"description": "通用折叠项正文,替代 item.content。"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "item-${value}",
|
|
58
|
+
"type": "({ item: AppleItem, open: boolean }) => VNode[]",
|
|
59
|
+
"description": "具体项的正文,优先于通用 item 插槽。"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"methods": [],
|
|
63
|
+
"notes": [
|
|
64
|
+
"使用真实内容与可访问名称;交互控件可通过 Tab 和原生键盘方式操作,动效遵循外层偏好。"
|
|
65
|
+
]
|
|
66
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { defineHandbook } from '../handbook'
|
|
2
|
+
|
|
3
|
+
export default defineHandbook({
|
|
4
|
+
"name": "AppleActionSheet",
|
|
5
|
+
"source": "src/components/overlays.ts",
|
|
6
|
+
"props": [
|
|
7
|
+
{
|
|
8
|
+
"name": "modelValue",
|
|
9
|
+
"type": "boolean",
|
|
10
|
+
"default": "false",
|
|
11
|
+
"description": "是否打开动作面板,默认 false。"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "title",
|
|
15
|
+
"type": "string",
|
|
16
|
+
"default": "\"\"",
|
|
17
|
+
"description": "标题文字。"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "message",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"default": "\"\"",
|
|
23
|
+
"description": "动作列表上方的说明文字,与 default 插槽内容同时显示。"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "items",
|
|
27
|
+
"type": "AppleMenuItem[]",
|
|
28
|
+
"default": "[]",
|
|
29
|
+
"description": "动作数组:{ label, value: string|number, disabled?, danger?, description?, icon?: Component }。default 插槽替代整个动作列表。"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "cancelText",
|
|
33
|
+
"type": "string",
|
|
34
|
+
"default": "\"取消\"",
|
|
35
|
+
"description": "取消按钮文字,空字符串隐藏按钮;取消返回 undefined 和 cancel 原因。"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "motion",
|
|
39
|
+
"type": "'inherit' | 'auto' | 'full' | 'reduced' | 'none'",
|
|
40
|
+
"default": "\"inherit\"",
|
|
41
|
+
"description": "局部动效偏好。inherit 跟随外层;全局与系统的减少动效设置仍是上限。"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"events": [
|
|
45
|
+
{
|
|
46
|
+
"name": "update:modelValue",
|
|
47
|
+
"type": "(value: boolean) => void",
|
|
48
|
+
"description": "交互更新时发出新模型值;可使用 v-model 接收。"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "select",
|
|
52
|
+
"type": "(value: string | number, item: AppleMenuItem) => void",
|
|
53
|
+
"description": "选中可用动作时发出 value 与完整项,随后更新打开状态并发出 close。"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "close",
|
|
57
|
+
"type": "(value: unknown, reason: 'select' | 'close' | 'cancel' | 'escape' | 'backdrop') => void",
|
|
58
|
+
"description": "选中动作时 value 为动作值;取消和背景/Escape 关闭时通常为 undefined。自定义 close(value) 保留传入值。"
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"slots": [
|
|
62
|
+
{
|
|
63
|
+
"name": "default",
|
|
64
|
+
"type": "({ select: (item: AppleMenuItem) => void, close: (value?: unknown) => void }) => VNode[]",
|
|
65
|
+
"description": "替代默认动作列表;调用 select(item) 保留选择事件与关闭行为,取消按钮仍保留。"
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"methods": [],
|
|
69
|
+
"notes": [
|
|
70
|
+
"自定义 default 插槽覆盖动作列表,使用 select(item) 保留结果回传;取消按钮仍保留。"
|
|
71
|
+
]
|
|
72
|
+
})
|