apptify 0.5.1 → 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 +3 -0
- 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 +23 -9
- package/docs/INPUT-WORKSHOP.md +59 -0
- package/docs/PLAYGROUND-TEMPLATE.md +9 -9
- package/package.json +5 -1
- package/templates/playground/README.md +4 -4
- package/templates/playground/index.html +1 -1
- package/templates/playground/package.json +6 -2
- package/templates/playground/source-snapshot.json +138 -53
- 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 +27 -14
- package/tooling/test-create-apptify.mjs +15 -5
- 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,38 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import ts from 'typescript'
|
|
4
|
+
import postcss from 'postcss'
|
|
5
|
+
|
|
6
|
+
// Extract the actual AppleInput declarations with TypeScript's AST. Preserve
|
|
7
|
+
// implementation logic; only erase TS types and route dependencies to public ESM.
|
|
8
|
+
export async function prepareInputSource() {
|
|
9
|
+
const root = new URL('../', import.meta.url)
|
|
10
|
+
const source = await readFile(new URL('src/components/forms.ts', root), 'utf8')
|
|
11
|
+
const ast = ts.createSourceFile('forms.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
12
|
+
const names = ['fieldProps', 'FieldVm', 'fieldId', 'controlAttrs', 'field', 'spinner', 'stringValue', 'AppleInput']
|
|
13
|
+
const declarations = new Map()
|
|
14
|
+
for (const statement of ast.statements) {
|
|
15
|
+
const name = ts.isVariableStatement(statement) ? statement.declarationList.declarations[0]?.name.getText(ast) : statement.name?.getText(ast)
|
|
16
|
+
if (names.includes(name)) declarations.set(name, statement.getText(ast))
|
|
17
|
+
}
|
|
18
|
+
for (const name of names) if (!declarations.has(name)) throw new Error(`AppleInput source changed: missing ${name}`)
|
|
19
|
+
const script = [
|
|
20
|
+
"import { defineComponent, h, useId, type PropType, type Slots, type VNodeChild } from 'vue'",
|
|
21
|
+
"import { ripple } from 'apptify'",
|
|
22
|
+
"import { Eye, EyeOff, LoaderCircle, X } from 'lucide-vue-next'",
|
|
23
|
+
...names.map(name => declarations.get(name)),
|
|
24
|
+
'export default AppleInput',
|
|
25
|
+
].join('\n\n')
|
|
26
|
+
const js = ts.transpileModule(script, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText
|
|
27
|
+
const cssAst = postcss.parse(await readFile(new URL('src/styles/forms.css', root), 'utf8'))
|
|
28
|
+
const boundary = cssAst.nodes.findIndex(node => node.type === 'rule' && node.selector === '.apple-textarea')
|
|
29
|
+
if (boundary < 0) throw new Error('Input CSS section changed: review source extraction')
|
|
30
|
+
const css = cssAst.nodes.slice(0, boundary).map(node => node.toString()).join('\n')
|
|
31
|
+
const code = `<!-- AppleInput 同源实现:src/components/forms.ts + src/styles/forms.css。\n只移除 TypeScript 类型并转接浏览器公开依赖,保留 Options API/render 实现。 -->\n<script>\n${js}</script>\n\n<style>\n${css}\n</style>\n`
|
|
32
|
+
const destination = new URL('playground/editor/apple-input-source.ts', root)
|
|
33
|
+
const snapshot = '// Generated by tooling/prepare-input-source.mjs; do not edit the snapshot by hand.\nexport default ' + JSON.stringify(code) + '\n'
|
|
34
|
+
// Do not trigger a hot update during dev-server startup for unchanged data.
|
|
35
|
+
const previous = await readFile(destination, 'utf8').catch(() => undefined)
|
|
36
|
+
if (previous !== snapshot) await writeFile(destination, snapshot)
|
|
37
|
+
}
|
|
38
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) await prepareInputSource()
|
|
@@ -3,22 +3,32 @@ import { basename, join, relative } from 'node:path'
|
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { createHash } from 'node:crypto'
|
|
5
5
|
|
|
6
|
+
import { prepareInputSource } from './prepare-input-source.mjs'
|
|
7
|
+
await prepareInputSource()
|
|
6
8
|
const root = fileURLToPath(new URL('../', import.meta.url))
|
|
7
9
|
const output = join(root, 'templates/playground')
|
|
8
10
|
const library = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'))
|
|
9
11
|
await rm(output, { recursive: true, force: true })
|
|
10
12
|
await mkdir(output, { recursive: true })
|
|
11
13
|
const copyOptions = { recursive: true, filter: source => basename(source) !== '.DS_Store' }
|
|
12
|
-
await cp(join(root, 'playground'), join(output, '
|
|
14
|
+
await cp(join(root, 'playground'), join(output, 'src'), copyOptions)
|
|
13
15
|
await cp(join(root, 'public'), join(output, 'public'), copyOptions)
|
|
14
|
-
await
|
|
16
|
+
await writeFile(join(output, 'index.html'), (await readFile(join(root, 'index.html'), 'utf8')).replace('/playground/main.ts', '/src/main.ts'))
|
|
17
|
+
await mkdir(join(output, 'tooling'), { recursive: true })
|
|
18
|
+
await cp(join(root, 'tooling/prepare-input-source.mjs'), join(output, 'tooling/prepare-input-source.mjs'))
|
|
19
|
+
await cp(join(root, 'tooling/playground-editor.mjs'), join(output, 'tooling/playground-editor.mjs'))
|
|
15
20
|
|
|
16
|
-
const snapshot = { createdAt: new Date().toISOString(), libraryVersion: library.version, files: {} }
|
|
21
|
+
const snapshot = { createdAt: new Date().toISOString(), libraryVersion: library.version, files: {}, sourcePaths: {} }
|
|
17
22
|
async function record(directory) {
|
|
18
23
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
19
24
|
const path = join(directory, entry.name)
|
|
20
25
|
if (entry.isDirectory()) await record(path)
|
|
21
|
-
else
|
|
26
|
+
else {
|
|
27
|
+
const key = relative(output, path).replaceAll('\\', '/')
|
|
28
|
+
const sourcePath = key.startsWith('src/') ? 'playground/' + key.slice(4) : key
|
|
29
|
+
snapshot.sourcePaths[key] = sourcePath
|
|
30
|
+
snapshot.files[key] = createHash('sha256').update(await readFile(join(root, sourcePath))).digest('hex')
|
|
31
|
+
}
|
|
22
32
|
}
|
|
23
33
|
}
|
|
24
34
|
await record(output)
|
|
@@ -32,14 +42,15 @@ async function adapt(directory) {
|
|
|
32
42
|
let source = await readFile(path, 'utf8')
|
|
33
43
|
source = source.replace(/(['"])(?:\.\.\/)+src(?:\/(?:index|core\/motion|core\/context|components\/content))?\1/g, "'apptify'")
|
|
34
44
|
if (/(['"])(?:\.\.\/)+src(?:\/[^'"]*)?\1/.test(source)) throw new Error('Unmapped private library import: ' + path)
|
|
45
|
+
if (path === join(output, 'src/editor/runtime.ts')) source = "import 'apptify/style.css'\n" + source
|
|
35
46
|
if (entry.name === 'main.ts') source = "import 'apptify/style.css'\n" + source
|
|
36
|
-
if (path === join(output, '
|
|
47
|
+
if (path === join(output, 'src/App.vue')) {
|
|
37
48
|
const navigationEnd = ']\n\nfunction navigate'
|
|
38
49
|
if (!source.includes(navigationEnd)) throw new Error('App navigation layout changed; update template adaptation')
|
|
39
50
|
source = source.replace('to="/" aria-label="Apptify 首页"', ':to="homePath" aria-label="Apptify 首页"')
|
|
40
51
|
source = source.replace(navigationEnd, "].filter(item => router.getRoutes().some(page => page.path === item.href))\nconst homePath = navigation[0]?.href ?? '/'\n\nfunction navigate")
|
|
41
52
|
}
|
|
42
|
-
if (path === join(output, '
|
|
53
|
+
if (path === join(output, 'src/views/[...all].vue')) {
|
|
43
54
|
source = source.replace('to="/" custom', ':to="homePath" custom')
|
|
44
55
|
source = source.replace('const router = useRouter()', "const router = useRouter()\nconst homePath = router.getRoutes().some(page => page.path === '/') ? '/' : '/settings'")
|
|
45
56
|
source = source.replace("else void router.push('/')", 'else void router.push(homePath)')
|
|
@@ -48,7 +59,7 @@ async function adapt(directory) {
|
|
|
48
59
|
}
|
|
49
60
|
}
|
|
50
61
|
}
|
|
51
|
-
await adapt(join(output, '
|
|
62
|
+
await adapt(join(output, 'src'))
|
|
52
63
|
const manifest = {
|
|
53
64
|
name: 'apptify-app', version: '0.0.0', private: true, type: 'module',
|
|
54
65
|
scripts: { dev: 'vite', typecheck: 'vue-tsc --noEmit', build: 'vue-tsc --noEmit && vite build', preview: 'vite preview --host 127.0.0.1' },
|
|
@@ -57,19 +68,21 @@ const manifest = {
|
|
|
57
68
|
'vue-router': library.devDependencies['vue-router'], vuetify: library.peerDependencies.vuetify,
|
|
58
69
|
'lucide-vue-next': library.dependencies['lucide-vue-next'],
|
|
59
70
|
},
|
|
60
|
-
devDependencies: Object.fromEntries(['@vitejs/plugin-vue', 'typescript', 'vite', 'vite-plugin-pages', 'vue-tsc'].map(name => [name, library.devDependencies[name]])),
|
|
71
|
+
devDependencies: Object.fromEntries(['@vitejs/plugin-vue', 'typescript', 'vite', 'vite-plugin-pages', 'vue-tsc', '@vue/repl', 'es-module-shims', 'esbuild', 'postcss'].map(name => [name, library.devDependencies[name]])),
|
|
61
72
|
}
|
|
62
73
|
await writeFile(join(output, 'package.json'), JSON.stringify(manifest, null, 2) + '\n')
|
|
63
74
|
const tsconfig = JSON.parse(await readFile(join(root, 'tsconfig.json'), 'utf8'))
|
|
64
|
-
tsconfig.include = ['
|
|
75
|
+
tsconfig.include = ['src/**/*.ts', 'src/**/*.vue']
|
|
65
76
|
await writeFile(join(output, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2) + '\n')
|
|
66
77
|
await writeFile(join(output, 'vite.config.ts'), [
|
|
67
78
|
"import { defineConfig } from 'vite'",
|
|
68
79
|
"import vue from '@vitejs/plugin-vue'",
|
|
69
80
|
"import apptifyRoutes from 'apptify/vite'",
|
|
81
|
+
"import playgroundEditor from './tooling/playground-editor.mjs'",
|
|
70
82
|
'',
|
|
71
83
|
'export default defineConfig({',
|
|
72
|
-
"
|
|
84
|
+
" optimizeDeps: { exclude: ['@vue/repl'] },",
|
|
85
|
+
" plugins: [playgroundEditor({ appDir: 'src', libraryDir: null }), apptifyRoutes(), vue()],",
|
|
73
86
|
" server: { host: '127.0.0.1' },",
|
|
74
87
|
" build: { outDir: 'dist' },",
|
|
75
88
|
'})', '',
|
|
@@ -88,10 +101,10 @@ await writeFile(join(output, 'README.md'), [
|
|
|
88
101
|
"",
|
|
89
102
|
"检查:`npm run typecheck`、`npm run build`。",
|
|
90
103
|
"",
|
|
91
|
-
"- 页面:`
|
|
92
|
-
"- 布局与导航:`
|
|
93
|
-
"- 账号表单:`
|
|
94
|
-
"- 样式:`
|
|
104
|
+
"- 页面:`src/views/`,文件路由自动生成。",
|
|
105
|
+
"- 布局与导航:`src/App.vue` 的 `navigation` 数组。",
|
|
106
|
+
"- 账号表单:`src/components/UserMenu.vue`,需要接入自己的后端。",
|
|
107
|
+
"- 样式:`src/style.css`;图片与来源:`public/images/`。",
|
|
95
108
|
"",
|
|
96
109
|
"可删除 `views/index.vue` 和 `views/components.vue`,再添加业务页面;删除后重启开发服务。删除组件总览后,可一并删除 `ComponentDemo.vue`、`catalog.ts` 和 `components/ComponentIndex.vue`。",
|
|
97
110
|
"",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
@@ -30,13 +30,14 @@ try {
|
|
|
30
30
|
await unpack(root, library)
|
|
31
31
|
await mkdir(join(installer, 'node_modules/.bin'))
|
|
32
32
|
await symlink('../create-apptify/bin/create-apptify.mjs', join(installer, 'node_modules/.bin/create-apptify'))
|
|
33
|
-
await writeFile(join(installer, 'package.json'),
|
|
33
|
+
await writeFile(join(installer, 'package.json'), JSON.stringify({ private: true, dependencies: { 'create-apptify': packed.version } }) + '\n')
|
|
34
34
|
|
|
35
|
-
const run = args => spawnSync(process.execPath, [cli, ...args], { cwd
|
|
35
|
+
const run = (args, cwd = installer) => spawnSync(process.execPath, [cli, ...args], { cwd, env, encoding: 'utf8' })
|
|
36
36
|
const help = run(['--help'])
|
|
37
37
|
assert.equal(help.status, 0, help.stderr)
|
|
38
38
|
assert.match(help.stdout, /npm create apptify@latest/)
|
|
39
39
|
assert.equal(run([]).status, 1)
|
|
40
|
+
assert.match(run([]).stderr, /Destination must be empty/)
|
|
40
41
|
assert.equal(run(['--unknown']).status, 1)
|
|
41
42
|
assert.equal(run(['one', 'two']).status, 1)
|
|
42
43
|
|
|
@@ -46,7 +47,10 @@ try {
|
|
|
46
47
|
const manifest = JSON.parse(await readFile(join(app, 'package.json'), 'utf8'))
|
|
47
48
|
assert.match(manifest.dependencies.apptify, /^file:\.\/vendor\/apptify-/)
|
|
48
49
|
assert.match(await readFile(join(app, '.gitignore'), 'utf8'), /node_modules/)
|
|
49
|
-
await readFile(join(app, '
|
|
50
|
+
await readFile(join(app, 'src/App.vue'))
|
|
51
|
+
assert(!(await readdir(app)).includes('playground'), 'Generated app must use src/')
|
|
52
|
+
assert.match(await readFile(join(app, 'index.html'), 'utf8'), /src="\/src\/main\.ts"/)
|
|
53
|
+
assert.deepEqual(JSON.parse(await readFile(join(app, 'tsconfig.json'), 'utf8')).include, ['src/**/*.ts', 'src/**/*.vue'])
|
|
50
54
|
const archive = join(app, manifest.dependencies.apptify.slice('file:./'.length))
|
|
51
55
|
const portableManifest = JSON.parse(execFileSync('tar', ['-xOf', archive, 'package/package.json'], { encoding: 'utf8' }))
|
|
52
56
|
assert.equal(portableManifest.name, 'apptify')
|
|
@@ -65,7 +69,13 @@ try {
|
|
|
65
69
|
await mkdir(empty)
|
|
66
70
|
const copied = run([empty])
|
|
67
71
|
assert.equal(copied.status, 0, copied.stderr)
|
|
68
|
-
await readFile(join(empty, '
|
|
72
|
+
await readFile(join(empty, 'src/App.vue'))
|
|
73
|
+
const current = join(scratch, 'current-directory')
|
|
74
|
+
await mkdir(current)
|
|
75
|
+
const defaultTarget = run([], current)
|
|
76
|
+
assert.equal(defaultTarget.status, 0, defaultTarget.stderr)
|
|
77
|
+
await readFile(join(current, 'src/router/index.ts'))
|
|
78
|
+
assert.match(JSON.parse(await readFile(join(current, 'package.json'), 'utf8')).dependencies.apptify, /^file:\.\/vendor\//)
|
|
69
79
|
await rm(installer, { recursive: true, force: true })
|
|
70
80
|
execFileSync('tar', ['-t', '-f', archive])
|
|
71
81
|
console.log('create-apptify: packed CLI, npm create, portable archive, help, argument validation and overwrite protection passed.')
|
|
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'
|
|
|
5
5
|
import { createHash } from 'node:crypto'
|
|
6
6
|
import { execFileSync, spawn } from 'node:child_process'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
|
-
import { chromium } from '@playwright/test'
|
|
8
|
+
import { chromium, expect } from '@playwright/test'
|
|
9
9
|
|
|
10
10
|
const root = fileURLToPath(new URL('../', import.meta.url))
|
|
11
11
|
const scratch = await mkdtemp(join(tmpdir(), 'apptify-package-'))
|
|
@@ -123,6 +123,12 @@ try {
|
|
|
123
123
|
assert.equal(await image.evaluate(async image => { await image.decode(); return image.naturalWidth > 0 }), true)
|
|
124
124
|
await page.locator('.component-index [aria-current]').first().waitFor()
|
|
125
125
|
})
|
|
126
|
+
await checkPage(server.url, '/component-docs/apple-input', async page => {
|
|
127
|
+
const frame = page.frameLocator('[data-testid="input-playground"] .vue-repl iframe')
|
|
128
|
+
await expect(frame.locator('.apple-input-wrap input').first()).toBeVisible()
|
|
129
|
+
await frame.locator('.apple-input-wrap input').first().fill('Template src preview')
|
|
130
|
+
await expect(frame.locator('.apple-input-wrap input').first()).toHaveValue('Template src preview')
|
|
131
|
+
})
|
|
126
132
|
await checkPage(server.url, '/settings', async page => {
|
|
127
133
|
await page.setViewportSize({ width: 320, height: 900 })
|
|
128
134
|
await page.getByRole('button', { name: '深色', exact: true }).click()
|
|
@@ -150,12 +156,12 @@ try {
|
|
|
150
156
|
server.stop()
|
|
151
157
|
server = undefined
|
|
152
158
|
|
|
153
|
-
await rm(join(app, '
|
|
154
|
-
await rm(join(app, '
|
|
159
|
+
await rm(join(app, 'src/views/index.vue'))
|
|
160
|
+
await rm(join(app, 'src/views/components.vue'))
|
|
155
161
|
// These files only served the removed overview page. Deleting them must be optional.
|
|
156
|
-
await rm(join(app, '
|
|
157
|
-
await rm(join(app, '
|
|
158
|
-
await rm(join(app, '
|
|
162
|
+
await rm(join(app, 'src/ComponentDemo.vue'))
|
|
163
|
+
await rm(join(app, 'src/catalog.ts'))
|
|
164
|
+
await rm(join(app, 'src/components/ComponentIndex.vue'))
|
|
159
165
|
run(['run', 'typecheck'], app)
|
|
160
166
|
run(['run', 'build'], app)
|
|
161
167
|
server = await serve()
|
|
@@ -178,7 +184,7 @@ try {
|
|
|
178
184
|
await browser.close()
|
|
179
185
|
}
|
|
180
186
|
for (const [path, hash] of Object.entries(snapshot.files)) {
|
|
181
|
-
assert.equal(createHash('sha256').update(await readFile(join(root, path))).digest('hex'), hash, 'Source changed during packing; rerun for its latest snapshot: ' + path)
|
|
187
|
+
assert.equal(createHash('sha256').update(await readFile(join(root, snapshot.sourcePaths?.[path] ?? path))).digest('hex'), hash, 'Source changed during packing; rerun for its latest snapshot: ' + path)
|
|
182
188
|
}
|
|
183
189
|
for (const [path, hash] of Object.entries(snapshot.buildSources || {})) {
|
|
184
190
|
assert.equal(createHash('sha256').update(await readFile(join(root, path))).digest('hex'), hash, 'Build source changed after this archive was created; pack the latest version: ' + path)
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<nav class="component-index" aria-label="组件定位">
|
|
3
|
-
<AppleTree :items="treeItems" :model-value="activeId || activeGroup" :expanded="expandedGroups" label="组件目录树" @update:expanded="expanded = $event" @select="navigate">
|
|
4
|
-
<template #item="{ item }">
|
|
5
|
-
<router-link :to="`/components#${item.value}`" :class="['index-link', { 'index-group': item.children, active: activeId === item.value || (item.children && activeGroup === item.value) }]" :aria-current="activeId === item.value ? 'location' : undefined" tabindex="-1" @click.prevent>
|
|
6
|
-
<component :is="icons[item.label]" v-if="item.children" :size="15" aria-hidden="true" />
|
|
7
|
-
<span>{{ item.label }}</span><small v-if="item.children">{{ item.children.length }}</small>
|
|
8
|
-
</router-link>
|
|
9
|
-
</template>
|
|
10
|
-
</AppleTree>
|
|
11
|
-
<p v-if="!treeItems.length" class="index-empty" role="status">未找到组件,请尝试其他关键词。</p>
|
|
12
|
-
</nav>
|
|
13
|
-
</template>
|
|
14
|
-
|
|
15
|
-
<script setup lang="ts">
|
|
16
|
-
import { computed, ref, watch } from 'vue'
|
|
17
|
-
import { useRoute, useRouter } from 'vue-router'
|
|
18
|
-
import { Bell, Box, MousePointer2, Smartphone, Table2, TextCursorInput } from 'lucide-vue-next'
|
|
19
|
-
import { AppleTree } from 'apptify'
|
|
20
|
-
import type { AppleTreeItem, AppleValue } from 'apptify'
|
|
21
|
-
import { sections, componentId } from '../catalog'
|
|
22
|
-
|
|
23
|
-
const props = defineProps<{ activeGroup: string; activeId: string; search?: string }>()
|
|
24
|
-
const emit = defineEmits<{ navigate: [] }>()
|
|
25
|
-
const router = useRouter()
|
|
26
|
-
const route = useRoute()
|
|
27
|
-
const expanded = ref<AppleValue[]>([props.activeGroup])
|
|
28
|
-
const icons: Record<string, typeof Box> = { '基础': Box, '表单': TextCursorInput, '导航': MousePointer2, '数据展示': Table2, '反馈': Bell, '移动交互': Smartphone }
|
|
29
|
-
const query = computed(() => props.search?.trim().toLocaleLowerCase() ?? '')
|
|
30
|
-
const treeItems = computed<AppleTreeItem[]>(() => sections.flatMap(section => {
|
|
31
|
-
const groupMatches = section.label.toLocaleLowerCase().includes(query.value)
|
|
32
|
-
const items = section.items.filter(item => groupMatches || `${item.label} ${item.name} ${componentId(item.name)} ${item.description}`.toLocaleLowerCase().includes(query.value))
|
|
33
|
-
return items.length ? [{ label: section.label, value: section.id, children: items.map(item => ({ label: item.label, value: componentId(item.name) })) }] : []
|
|
34
|
-
}))
|
|
35
|
-
const expandedGroups = computed(() => query.value ? treeItems.value.map(item => item.value) : expanded.value)
|
|
36
|
-
watch(() => props.activeGroup, group => {
|
|
37
|
-
if (!expanded.value.includes(group)) expanded.value = [...expanded.value, group]
|
|
38
|
-
})
|
|
39
|
-
function navigate(item: AppleTreeItem) {
|
|
40
|
-
const hash = `#${item.value}`
|
|
41
|
-
if (route.hash === hash) {
|
|
42
|
-
const target = document.getElementById(String(item.value))
|
|
43
|
-
if (target) window.scrollTo({ top: target.getBoundingClientRect().top + window.scrollY - 96, behavior: 'instant' })
|
|
44
|
-
} else void router.push(`/components${hash}`)
|
|
45
|
-
emit('navigate')
|
|
46
|
-
}
|
|
47
|
-
</script>
|
|
48
|
-
|
|
49
|
-
<style scoped>
|
|
50
|
-
.component-index { min-width: 0; }
|
|
51
|
-
.component-index :deep(.apple-tree__row) { min-height: 38px; gap: 0; font-size: 12px; border-radius: 6px; }
|
|
52
|
-
.component-index :deep(.apple-tree__toggle), .component-index :deep(.apple-tree__spacer) { flex-basis: 24px; width: 24px; height: 34px; }
|
|
53
|
-
.component-index .index-link { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; padding: 8px 4px; font-size: 12px; color: inherit; text-decoration: none; }
|
|
54
|
-
.component-index .index-group { border-radius: 0; }
|
|
55
|
-
/* The tree row owns the fade; avoid a second, smaller hover background on its link. */
|
|
56
|
-
.component-index .index-link:hover { background: transparent; }
|
|
57
|
-
.index-link > span { flex: 1; overflow-wrap: anywhere; }
|
|
58
|
-
.index-link > svg { flex-shrink: 0; }
|
|
59
|
-
.index-link.active { color: var(--apple-text); font-weight: 600; }
|
|
60
|
-
.index-link small { font-size: 10px; color: var(--apple-secondary); font-variant-numeric: tabular-nums; }
|
|
61
|
-
.index-empty { margin: 12px 8px; font-size: 12px; line-height: 1.7; color: var(--apple-secondary); }
|
|
62
|
-
</style>
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<apple-aside class="sidebar" aria-label="组件目录">
|
|
3
|
-
<div class="sidebar-heading"><strong>组件目录</strong><span>点击定位,直接预览</span></div>
|
|
4
|
-
<div class="directory-search"><apple-search v-model="directorySearch" label="搜索组件目录" placeholder="名称、标签或描述" /></div>
|
|
5
|
-
<ComponentIndex :active-group="activeGroup" :active-id="activeId" :search="directorySearch" />
|
|
6
|
-
</apple-aside>
|
|
7
|
-
<main class="main-content components-page">
|
|
8
|
-
<header class="page-heading"><div><div class="page-kicker">COMPONENTS</div><h1>组件<span>。</span></h1><p>{{ catalog.length }} 个组件,完整呈现。向下浏览,直接体验。</p></div></header>
|
|
9
|
-
<div class="feed-toolbar">
|
|
10
|
-
<apple-button class="mobile-index-toggle" variant="ghost" size="small" :icon="PanelLeft" @click="mobileIndex = true">组件目录</apple-button>
|
|
11
|
-
<span class="feed-count">全部组件 <small>{{ catalog.length }}</small></span>
|
|
12
|
-
<div class="preview-controls" role="group" aria-label="预览尺寸">
|
|
13
|
-
<apple-button variant="ghost" size="small" icon-only :icon="Monitor" :class="{ active: previewMode === 'desktop' }" :aria-pressed="previewMode === 'desktop'" label="桌面预览" @click="previewMode = 'desktop'" />
|
|
14
|
-
<apple-button variant="ghost" size="small" icon-only :icon="Smartphone" :class="{ active: previewMode === 'mobile' }" :aria-pressed="previewMode === 'mobile'" label="手机预览" @click="previewMode = 'mobile'" />
|
|
15
|
-
</div>
|
|
16
|
-
</div>
|
|
17
|
-
<div ref="feed" class="component-feed">
|
|
18
|
-
<section v-for="(section, index) in sections" :id="section.id" :key="section.id" class="feed-group feed-anchor" :data-group="section.id" :aria-labelledby="`${section.id}-heading`">
|
|
19
|
-
<header class="feed-group-heading"><span>0{{ index + 1 }}</span><h2 :id="`${section.id}-heading`">{{ section.label }}</h2><small>{{ section.items.length }} 个组件</small></header>
|
|
20
|
-
<article v-for="item in section.items" :id="componentId(item.name)" :key="item.name" class="component-card feed-anchor" :data-group="section.id" :aria-labelledby="`${componentId(item.name)}-heading`">
|
|
21
|
-
<header class="component-card-heading"><div><h3 :id="`${componentId(item.name)}-heading`"><router-link :to="`#${componentId(item.name)}`">{{ item.label }}<Hash :size="14" /></router-link></h3><p>{{ item.description }}</p></div><code>{{ componentId(item.name) }}</code></header>
|
|
22
|
-
<section class="detail-preview" :class="{ 'mobile-preview': previewMode === 'mobile' }" :aria-label="`${item.label}预览`"><ComponentDemo :name="item.name" @navigate="navigateGroup" /></section>
|
|
23
|
-
<apple-accordion class="component-source" :items="sourceSections">
|
|
24
|
-
<template #item>
|
|
25
|
-
<div class="code-view"><div class="code-toolbar"><span>使用示例</span><apple-button variant="ghost" size="small" @click="copy(item.code)">复制代码</apple-button></div><pre><code>{{ item.code }}</code></pre><h4>接口</h4><p class="api-line">{{ item.api }}</p></div>
|
|
26
|
-
</template>
|
|
27
|
-
</apple-accordion>
|
|
28
|
-
</article>
|
|
29
|
-
</section>
|
|
30
|
-
</div>
|
|
31
|
-
<PageFooter />
|
|
32
|
-
</main>
|
|
33
|
-
<apple-drawer v-model="mobileIndex" title="组件目录" placement="left" width="290px">
|
|
34
|
-
<div class="directory-search"><apple-search v-model="directorySearch" label="搜索组件目录" placeholder="名称、标签或描述" /></div>
|
|
35
|
-
<ComponentIndex :active-group="activeGroup" :active-id="activeId" :search="directorySearch" @navigate="mobileIndex = false" />
|
|
36
|
-
</apple-drawer>
|
|
37
|
-
</template>
|
|
38
|
-
|
|
39
|
-
<script setup lang="ts">
|
|
40
|
-
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
41
|
-
import { useRoute, useRouter } from 'vue-router'
|
|
42
|
-
import { Hash, Monitor, PanelLeft, Smartphone } from 'lucide-vue-next'
|
|
43
|
-
import { useApple } from 'apptify'
|
|
44
|
-
import { catalog, sections, componentId } from '../catalog'
|
|
45
|
-
import ComponentDemo from '../ComponentDemo.vue'
|
|
46
|
-
import ComponentIndex from '../components/ComponentIndex.vue'
|
|
47
|
-
import PageFooter from '../components/PageFooter.vue'
|
|
48
|
-
|
|
49
|
-
const route = useRoute()
|
|
50
|
-
const router = useRouter()
|
|
51
|
-
const apple = useApple()
|
|
52
|
-
const feed = ref<HTMLElement>()
|
|
53
|
-
const activeGroup = ref(sections[0].id)
|
|
54
|
-
const activeId = ref('')
|
|
55
|
-
const previewMode = ref('desktop')
|
|
56
|
-
const mobileIndex = ref(false)
|
|
57
|
-
const directorySearch = ref('')
|
|
58
|
-
const sourceSections = [{ label: '代码与 API', value: 'source' }]
|
|
59
|
-
let anchors: HTMLElement[] = []
|
|
60
|
-
let frame = 0
|
|
61
|
-
let observer: ResizeObserver | undefined
|
|
62
|
-
|
|
63
|
-
function updatePosition() {
|
|
64
|
-
frame = 0
|
|
65
|
-
let current: HTMLElement | undefined
|
|
66
|
-
for (const anchor of anchors) {
|
|
67
|
-
if (anchor.getBoundingClientRect().top > 128) break
|
|
68
|
-
if (!current || Math.abs(anchor.getBoundingClientRect().top - current.getBoundingClientRect().top) > 1) current = anchor
|
|
69
|
-
else if (anchor.id === route.hash.slice(1) || (current.id !== route.hash.slice(1) && anchor.id === activeId.value)) current = anchor
|
|
70
|
-
}
|
|
71
|
-
activeId.value = current?.id ?? ''
|
|
72
|
-
activeGroup.value = current?.dataset.group ?? sections[0].id
|
|
73
|
-
}
|
|
74
|
-
function schedulePosition() {
|
|
75
|
-
if (!frame) frame = requestAnimationFrame(updatePosition)
|
|
76
|
-
}
|
|
77
|
-
function navigateGroup(label: string) {
|
|
78
|
-
const section = sections.find(section => section.label === label)
|
|
79
|
-
void router.push(section ? `/components#${section.id}` : '/components')
|
|
80
|
-
}
|
|
81
|
-
async function copy(value: string) {
|
|
82
|
-
try { await navigator.clipboard.writeText(value); apple.notify('已复制', { tone: 'success' }) }
|
|
83
|
-
catch { apple.notify('无法访问剪贴板,请手动选择代码复制', { tone: 'warning' }) }
|
|
84
|
-
}
|
|
85
|
-
watch(() => route.hash, () => { mobileIndex.value = false; schedulePosition() })
|
|
86
|
-
watch(activeId, async () => {
|
|
87
|
-
await nextTick()
|
|
88
|
-
const sidebar = document.querySelector<HTMLElement>('.sidebar')
|
|
89
|
-
const current = sidebar?.querySelector<HTMLElement>('[aria-current="location"]')
|
|
90
|
-
if (!sidebar || !current) return
|
|
91
|
-
const item = current.getBoundingClientRect(), bounds = sidebar.getBoundingClientRect()
|
|
92
|
-
if (item.top < bounds.top + 16) sidebar.scrollTop -= bounds.top + 16 - item.top
|
|
93
|
-
else if (item.bottom > bounds.bottom - 16) sidebar.scrollTop += item.bottom - bounds.bottom + 16
|
|
94
|
-
})
|
|
95
|
-
onMounted(() => {
|
|
96
|
-
anchors = Array.from(feed.value!.querySelectorAll<HTMLElement>('.feed-anchor'))
|
|
97
|
-
window.addEventListener('scroll', schedulePosition, { passive: true })
|
|
98
|
-
window.addEventListener('resize', schedulePosition)
|
|
99
|
-
observer = new ResizeObserver(schedulePosition)
|
|
100
|
-
observer.observe(feed.value!)
|
|
101
|
-
schedulePosition()
|
|
102
|
-
})
|
|
103
|
-
onBeforeUnmount(() => {
|
|
104
|
-
window.removeEventListener('scroll', schedulePosition)
|
|
105
|
-
window.removeEventListener('resize', schedulePosition)
|
|
106
|
-
observer?.disconnect()
|
|
107
|
-
cancelAnimationFrame(frame)
|
|
108
|
-
})
|
|
109
|
-
</script>
|
|
110
|
-
|
|
111
|
-
<style scoped>
|
|
112
|
-
.directory-search { min-width: 0; margin: 0 4px 16px; }
|
|
113
|
-
.directory-search :deep(.apple-field__label) { font-size: 11px; }
|
|
114
|
-
.directory-search :deep(input) { min-width: 0; font-size: 12px; }
|
|
115
|
-
.components-page > * { max-width: none; }
|
|
116
|
-
.feed-group { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 400px), 1fr)); gap: 20px; align-items: start; }
|
|
117
|
-
.feed-group-heading { grid-column: 1 / -1; padding-bottom: 0; }
|
|
118
|
-
.component-card { margin-bottom: 0; }
|
|
119
|
-
.component-source :deep(.apple-accordion__item) { border: 0; }
|
|
120
|
-
.component-source :deep(.apple-accordion__item > h3) { font-size: 11px; font-weight: 400; }
|
|
121
|
-
.component-source :deep(.apple-accordion__item > h3 > button) { min-height: 44px; gap: 8px; padding: 13px 28px; color: var(--apple-secondary); }
|
|
122
|
-
.component-source :deep(.apple-accordion__content) { padding: 0; }
|
|
123
|
-
@media (max-width: 800px) {
|
|
124
|
-
.component-source :deep(.apple-accordion__item > h3 > button) { padding-inline: 18px; }
|
|
125
|
-
}
|
|
126
|
-
</style>
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<main class="main-content home-page">
|
|
3
|
-
<section class="home-intro">
|
|
4
|
-
<div class="page-kicker">APPTIFY / 界面与交互</div>
|
|
5
|
-
<h1>让界面自然,<br /><span>让细节动人。</span></h1>
|
|
6
|
-
<p>从一个按钮,到完整体验。<br class="mobile-break" /> 简洁的组件,恰到好处的交互。</p>
|
|
7
|
-
<div class="home-actions">
|
|
8
|
-
<router-link v-slot="{ href, navigate }" to="/components" custom>
|
|
9
|
-
<apple-button :href="href" @click="navigate">浏览全部组件 <ArrowRight :size="17" aria-hidden="true" /></apple-button>
|
|
10
|
-
</router-link>
|
|
11
|
-
<router-link v-slot="{ href, navigate }" to="/settings" custom>
|
|
12
|
-
<apple-link :href="href" class="home-link" @click="navigate">调整外观与动效 <ArrowUpRight :size="16" aria-hidden="true" /></apple-link>
|
|
13
|
-
</router-link>
|
|
14
|
-
</div>
|
|
15
|
-
</section>
|
|
16
|
-
<section aria-label="精选交互示例">
|
|
17
|
-
<div class="section-heading"><h2>细节,即刻体验。</h2><span>精选交互示例 · 直接试一试</span></div>
|
|
18
|
-
<div class="specimen-grid">
|
|
19
|
-
<section class="specimen"><div class="specimen-caption"><span>01 / 导航</span></div><h2>内容,各归其位。</h2><apple-search v-model="settingQuery" placeholder="搜索设置" label="搜索设置" /><apple-list v-model="setting" :items="filteredSettings" selectable label="设置导航示例" /><p v-if="!filteredSettings.length" class="search-empty" role="status">没有匹配的设置,试试其他关键词。</p></section>
|
|
20
|
-
<section class="specimen"><div class="specimen-caption"><span>02 / 控制</span></div><h2>点到,即止。</h2><div class="button-specimens"><apple-button :icon="arrowIcon" @click="$apple.notify('操作已完成',{tone:'success'})">继续探索</apple-button><apple-button variant="secondary" @click="$apple.notify('已加入收藏')">加入收藏</apple-button><apple-button variant="outline" @click="$router.push('/components#apple-button')">了解更多</apple-button><div class="icon-button-row"><apple-button variant="secondary" :icon="plusIcon" icon-only label="添加" @click="$apple.notify('已添加')" /><apple-button :variant="liked?'primary':'secondary'" :icon="heartIcon" icon-only :label="liked ? '取消收藏' : '收藏'" :aria-pressed="liked" @click="liked=!liked" /><apple-button variant="secondary" :icon="downloadIcon" icon-only label="浏览组件示例" @click="$router.push('/components')" /></div></div><div class="specimen-foot"><code>apple-button</code><span>5 种样式</span></div></section>
|
|
21
|
-
<section class="specimen"><div class="specimen-caption"><span>03 / 偏好</span></div><h2>恰好,是你的。</h2><div class="preference-demo"><apple-segmented-control v-model="period" :items="periods" label="统计周期" /><div class="setting-line"><div><strong>消息通知</strong><small>不错过重要的事</small></div><apple-switch v-model="notices" aria-label="消息通知" /></div><div class="setting-line"><div><strong>自动同步</strong><small>在所有设备上保持一致</small></div><apple-switch v-model="sync" aria-label="自动同步" /></div><apple-slider v-model="volume" label="音量" /><div class="check-row"><apple-checkbox v-model="agreed" label="记住我的偏好" /></div></div></section>
|
|
22
|
-
<section class="specimen form-specimen"><div class="specimen-caption"><span>04 / 输入</span></div><h2>开始一段对话。</h2><apple-form @submit="$apple.notify(`你好,${name || '新朋友'}!`,{tone:'success'})"><apple-input v-model="name" label="你的名字" placeholder="怎么称呼你" autocomplete="name" clearable required /><apple-input v-model="email" label="电子邮箱" type="email" placeholder="name@example.com" autocomplete="email" required /><apple-button type="submit">加入我们<ArrowRight :size="15" /></apple-button></apple-form></section>
|
|
23
|
-
<section class="specimen media-specimen"><div class="specimen-caption"><span>05 / 媒体</span></div><h2>靠近一点,再一点。</h2><apple-image :gallery="[{ src: '/images/lake.jpg', alt: '山间湖泊和木屋' }, { src: '/images/airpods-max-orange.jpg', alt: '橙色耳机' }]" aspect-ratio="4/3" /><div class="specimen-foot"><code>apple-image</code><span>山间的一刻</span></div></section>
|
|
24
|
-
<section class="specimen feedback-specimen"><div class="specimen-caption"><span>06 / 反馈</span></div><h2>每一步,都有回应。</h2><apple-alert title="所有更改已保存" tone="success" /><div class="progress-demo"><div><span>正在同步</span><span>{{progress}}%</span></div><apple-progress :model-value="progress" label="正在同步" /></div><apple-avatar-group :items="[{name:'林初'},{name:'Alex'},{name:'Taylor'},{name:'Quinn'}]" /><div class="feedback-actions"><apple-button variant="secondary" size="small" @click="openDemoDialog">打开对话框</apple-button><apple-button variant="ghost" size="small" @click="$apple.notify('所有更改已保存',{tone:'success'});progress=Math.min(100,progress+10)">显示提示</apple-button></div></section>
|
|
25
|
-
</div>
|
|
26
|
-
</section>
|
|
27
|
-
<section class="more-components">
|
|
28
|
-
<div><h2>更多组件,同样顺手。</h2><span>{{ catalog.length }} 个组件,预览与交互都在一页之中。</span></div>
|
|
29
|
-
<router-link v-slot="{ href, navigate }" to="/components" custom>
|
|
30
|
-
<apple-link :href="href" class="home-link" @click="navigate">查看全部组件 <ArrowRight :size="16" aria-hidden="true" /></apple-link>
|
|
31
|
-
</router-link>
|
|
32
|
-
</section>
|
|
33
|
-
<PageFooter />
|
|
34
|
-
</main>
|
|
35
|
-
</template>
|
|
36
|
-
|
|
37
|
-
<script lang="ts">
|
|
38
|
-
import { defineComponent, markRaw } from 'vue'
|
|
39
|
-
import { ArrowRight, ArrowUpRight, Download, Heart, Plus } from 'lucide-vue-next'
|
|
40
|
-
import { catalog } from '../catalog'
|
|
41
|
-
import PageFooter from '../components/PageFooter.vue'
|
|
42
|
-
|
|
43
|
-
export default defineComponent({
|
|
44
|
-
components: { ArrowRight, ArrowUpRight, PageFooter },
|
|
45
|
-
data() { return {
|
|
46
|
-
catalog, arrowIcon: markRaw(ArrowRight), plusIcon: markRaw(Plus), heartIcon: markRaw(Heart), downloadIcon: markRaw(Download),
|
|
47
|
-
period: 'month', periods: [{label:'日',value:'day'},{label:'周',value:'week'},{label:'月',value:'month'}],
|
|
48
|
-
notices: true, sync: false, agreed: true, volume: 64, progress: 68, name: '', email: '', liked: false,
|
|
49
|
-
settingQuery: '', setting: 'profile', overviewSettings: [
|
|
50
|
-
{label:'个人资料',value:'profile',description:'姓名、头像和联系方式'},
|
|
51
|
-
{label:'通知',value:'notices',description:'推送和邮件偏好'},
|
|
52
|
-
{label:'隐私与安全',value:'privacy',description:'管理你的账户安全设置'},
|
|
53
|
-
],
|
|
54
|
-
} },
|
|
55
|
-
computed: {
|
|
56
|
-
filteredSettings() {
|
|
57
|
-
const query = this.settingQuery.trim()
|
|
58
|
-
return this.overviewSettings.filter(item => item.label.includes(query) || item.description.includes(query))
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
methods: {
|
|
62
|
-
openDemoDialog() { this.$apple.dialog({title:'一切,就从这里开始。',message:'试试这些组件,找到适合你的交互。',confirmText:'好的',cancelText:'稍后'}) },
|
|
63
|
-
},
|
|
64
|
-
})
|
|
65
|
-
</script>
|
|
66
|
-
|
|
67
|
-
<style scoped>
|
|
68
|
-
.home-link { min-height: 44px; gap: 9px; font-size: 13px; font-weight: 500; }
|
|
69
|
-
.search-empty { margin: 12px 0 0; color: var(--apple-secondary); font-size: 12px; line-height: 1.7; }
|
|
70
|
-
/* Keep the library's control sizes in the compact specimens. */
|
|
71
|
-
.button-specimens > .apple-button { min-height: 44px; font-size: inherit; }
|
|
72
|
-
.icon-button-row .apple-button { width: 44px; height: 44px; min-height: 44px; }
|
|
73
|
-
.form-specimen .apple-form .apple-button { min-height: 44px; font-size: inherit; }
|
|
74
|
-
.feedback-actions .apple-button { font-size: 13px; }
|
|
75
|
-
</style>
|