Skip to content

Commit 7844d04

Browse files
committed
docs: AFCL reference is split into Buttons and Inputs, Layout and Overlays, Data Display and Loading and Charts pages, every example imports its components from @/afcl, previews load only these components and preload the font AdminForth/2005
1 parent 41b64c0 commit 7844d04

11 files changed

Lines changed: 3018 additions & 2727 deletions

File tree

‎adminforth/design/system-concepts.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Apps restyle and test AdminForth by `af-*` and `afcl-*` classes. A restyle never
2222

2323
- Build UI in core and plugins from AFCL components (`@/afcl`). A plugin may adjust a few classes of a component via `class` or props like `contentClass`.
2424
- When the same piece of UI shows up in several places, or a piece clearly is a component, add it to AFCL instead of copying markup. Follow AFCL conventions: typed props with `withDefaults`, slots for content, `v-model` for state, `defineExpose` for an imperative API, `mergeClasses` for class overrides, tokens for colors, an `afcl-*` root class.
25-
- A new AFCL component gets a section in the AFCL docs (`documentation/docs/tutorial/03-Customization/15-afcl.md`) and its states and variants in [system.html](./system.html). Docs examples are live, not screenshots: wrap the code blocks of an example into `<AfclPreview id="...">` and the docs build renders them with real AFCL (see `documentation/afcl-preview/vite.config.ts`).
25+
- A new AFCL component gets a section on the page of its category in the AFCL docs (`documentation/docs/tutorial/03-Customization/15-afcl/`, a link in its `index.md` as well) and its states and variants in [system.html](./system.html). Docs examples are live, not screenshots: wrap the code blocks of an example into `<AfclPreview id="...">` and the docs build renders them with real AFCL (see `documentation/afcl-preview/vite.config.ts`). Nothing is registered globally, an example imports every AFCL component it uses from `@/afcl`, otherwise the build fails.
2626
- Example: `DropdownMenu` and `DropdownMenuItem` replaced the hand-made menus of the list (⋯ in the header and in rows), the user menu and the import-export plugin.
2727

2828
## Disabled state

‎documentation/afcl-preview/index.html‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<meta charset="utf-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1" />
66
<title>AFCL preview</title>
7+
<!-- otherwise the font is found only when the example renders its text, and the first paint waits for it -->
8+
<link rel="preload" href="./node_modules/@fontsource-variable/geist/files/geist-latin-wght-normal.woff2" as="font" type="font/woff2" crossorigin />
79
</head>
810
<body class="bg-lightHtml text-lightListTableText dark:bg-darkHtml dark:text-darkListTableText">
911
<div id="app" class="p-4 sm:p-6"></div>

‎documentation/afcl-preview/main.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createApp, defineAsyncComponent } from 'vue';
22
import { createRouter, createMemoryHistory } from 'vue-router';
3-
import * as afcl from '@/afcl';
43
import { initI18n } from '@/i18n';
54
import { coreStore } from './stubs';
65
import '@fontsource-variable/geist';
@@ -24,14 +23,14 @@ syncTheme();
2423
new MutationObserver(syncTheme).observe(docsRoot, { attributeFilter: ['data-theme'] });
2524

2625
const app = createApp(defineAsyncComponent(loadExample));
27-
Object.entries(afcl).forEach(([name, component]) => typeof component === 'object' && app.component(name, component));
2826
app.use(createRouter({ history: createMemoryHistory(), routes: [{ path: '/:path(.*)*', component: { render: () => null } }] }));
2927
initI18n(app);
3028
app.mount('#app');
3129

3230
// the docs page sizes the iframe to the example, and asks for the height again once it listens
31+
// offsetHeight rounds a fractional height down as well, the frame then is a bit short and shows a scrollbar
3332
function postHeight() {
34-
window.parent.postMessage({ afclPreview: id, height: document.body.offsetHeight }, '*');
33+
window.parent.postMessage({ afclPreview: id, height: Math.ceil(document.body.getBoundingClientRect().height) }, '*');
3534
}
3635
new ResizeObserver(postHeight).observe(document.body);
3736
window.addEventListener('message', (e) => e.data === 'afclPreviewHeight' && postHeight());

‎documentation/afcl-preview/vite.config.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ const docsDir = path.join(previewDir, '../docs');
1515
const examplesDir = path.join(previewDir, 'examples');
1616
const spaDir = path.join(previewDir, '../../adminforth/spa');
1717
const spaSrc = path.join(spaDir, 'src');
18+
const afclDir = path.join(spaSrc, 'afcl');
1819
const stubs = path.join(previewDir, 'stubs.ts');
1920

2021
const PREVIEW_RE = /<AfclPreview id="([^"]+)"[^>]*>([\s\S]*?)<\/AfclPreview>/g;
2122
const CODE_BLOCK_RE = /```(\w+)[^\n]*\n([\s\S]*?)```/g;
2223
const DIFF_MARKER_RE = /^\s*\/\/\s*diff-(add|remove)\s*$/;
24+
const AFCL_IMPORT_RE = /^import \{([^}]+)\} from '@\/afcl';?$/gm;
25+
const COMPONENT_TAG_RE = /<([A-Z]\w*)/g;
2326

2427
// code blocks may show a change with //diff-add and //diff-remove lines, the example renders the result
2528
function applyDiff(code: string) {
@@ -39,19 +42,33 @@ function applyDiff(code: string) {
3942
}
4043

4144
// every <AfclPreview> of the docs becomes an SFC: its html code blocks form the template, other blocks the script
45+
// nothing is registered globally, an example imports its components from '@/afcl' like an app does. The SFC takes
46+
// each of them from its own file, so an iframe loads only these components and not all of AFCL
4247
function docsExamples(): Plugin {
4348
return {
4449
name: 'afcl-docs-examples',
4550
buildStart() {
4651
fs.rmSync(examplesDir, { recursive: true, force: true });
4752
fs.mkdirSync(examplesDir);
53+
const afclComponents = new Set(fs.readdirSync(afclDir).filter((file) => file.endsWith('.vue')).map((file) => path.basename(file, '.vue')));
4854
const mdFiles = fs.readdirSync(docsDir, { recursive: true, encoding: 'utf-8' }).filter((file) => file.endsWith('.md'));
4955
for (const file of mdFiles) {
5056
for (const [, id, body] of fs.readFileSync(path.join(docsDir, file), 'utf-8').matchAll(PREVIEW_RE)) {
5157
const blocks = [...body.matchAll(CODE_BLOCK_RE)];
5258
const code = (html: boolean) => blocks.filter(([, lang]) => (lang === 'html') === html).map(([, , src]) => applyDiff(src)).join('\n');
59+
const template = code(true);
60+
const imported: string[] = [];
61+
const script = code(false).replace(AFCL_IMPORT_RE, (_, names: string) => {
62+
imported.push(...names.split(',').map((name) => name.trim()));
63+
return '';
64+
});
65+
const notImported = [...template.matchAll(COMPONENT_TAG_RE)].map(([, name]) => name).find((name) => afclComponents.has(name) && !imported.includes(name));
66+
if (notImported) {
67+
this.error(`AFCL example "${id}" in ${file} uses <${notImported}> but does not import it from '@/afcl'`);
68+
}
69+
const imports = imported.map((name) => `import ${name} from '@/afcl/${name}.vue';\n`).join('');
5370
// 'wx' fails on a second example with the same id
54-
fs.writeFileSync(path.join(examplesDir, `${id}.vue`), `<script setup lang="ts">\n${code(false)}</script>\n\n<template>\n${code(true)}</template>\n`, { flag: 'wx' });
71+
fs.writeFileSync(path.join(examplesDir, `${id}.vue`), `<script setup lang="ts">\n${imports}${script}</script>\n\n<template>\n${template}</template>\n`, { flag: 'wx' });
5572
}
5673
}
5774
},

0 commit comments

Comments
 (0)