Skip to content

Commit c35dc4e

Browse files
committed
fix: CountryFlag downloads only the shown flags from flag-icons instead of the whole 1.1 MB vue-flag set AdminForth/2005
1 parent b680be3 commit c35dc4e

8 files changed

Lines changed: 31 additions & 55 deletions

File tree

‎adminforth/design/adoption.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,6 @@ Which packages follow the [design concepts](./system-concepts.md): tokens instea
5353
## Targets
5454

5555
- If simple - get of flowbite, prefer pure AFCL + Tailwind, move features we luck to AFCL
56-
- Existing icon fonts apart lucide - try to get rid of them if not complex
56+
- Existing icon fonts apart lucide - try to get rid of them if not complex
57+
- Core uses only Lucide icons and `flag-icons` flags (`afcl/CountryFlag.vue`). `vue-flowbite` and `vue-heroicons` stay in `spa/package.json` while plugins import them (flowbite: bulk-ai-flow, clone-row, dashboard, email-invite, email-password-reset, foreign-inline-list, inline-create, json-editor, markdown, open-signup, two-factors-auth, user-soft-delete; heroicons: dashboard, markdown). After plugins rework it they should disappear.
58+
- i18n shows language flags with its own `flag-icon-css`, a 445 KB stylesheet loaded on every page: move `LanguageFlag.vue` to AFCL `CountryFlag`.

‎adminforth/design/system-concepts.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ Below `sm` (640px) a Dialog or Modal docks to the bottom edge as a sheet: full w
4040
- Don't add icons from other sets; replace flowbite, heroicons and humbleicons icons with Lucide when you touch a component. The old sets stay installed because apps and older plugins still import them.
4141
- Don't draw icons with inline `<svg>` or keep them as own `.vue` files, take the Lucide component. Inline SVG is only for graphics which are not icons: the two-tone `Spinner` ring, the `Toast` timer ring, charts.
4242

43+
## Country flags and lazy loading
44+
45+
- Flags are the 4:3 SVGs of `flag-icons`, shown by AFCL `<CountryFlag :countryCode="'ua'" />`. Core and plugins use this component and don't bring their own flag packages or flag CSS.
46+
- A set of assets (flags, illustrations) never goes into the main bundle as a whole. `CountryFlag` loads the map of flag URLs (`afcl/countryFlagUrls.ts`, 7 KB gzip) with a dynamic `import()` when the first flag is shown, then the browser downloads only the SVGs of the shown flags, 0.2–10 KB each. The former `@iconify-prerendered/vue-flag` brought 1.1 MB gzip with the first flag.
47+
- An eager `import.meta.glob` belongs to a module which is imported dynamically. `@/afcl` is a barrel, so in an AFCL component such glob makes the dev server load every matched file on every page. Glob assets with `query: '?url&no-inline'`, otherwise Vite inlines files under 4 KB into JS as data URIs, and with `exhaustive: true` when the files are in `node_modules`, which the glob skips by default.
48+
4349
## Moving off flowbite
4450

4551
flowbite is being removed step by step. Don't add new flowbite usages (`flowbite` JS components, `flowbite-datepicker`, `data-*-toggle` attributes). When you touch code which uses it, replace it with a native Vue implementation, usually an AFCL component. Example: the user menu moved from flowbite `Dropdown` to AFCL `DropdownMenu`.

‎adminforth/pnpm-lock.yaml‎

Lines changed: 0 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎adminforth/spa/package-lock.json‎

Lines changed: 0 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎adminforth/spa/package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"dependencies": {
1616
"@fontsource-variable/geist": "^5.3.0",
1717
"@fontsource-variable/geist-mono": "^5.3.0",
18-
"@iconify-prerendered/vue-flag": "^0.28.1748584105",
1918
"@iconify-prerendered/vue-flowbite": "^0.28.1754899090",
2019
"@iconify-prerendered/vue-heroicons": "^0.28.1721921294",
2120
"@iconify-prerendered/vue-humbleicons": "^0.28.1754108846",

‎adminforth/spa/pnpm-lock.yaml‎

Lines changed: 0 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,29 @@
11
<template>
22
<div class="aspect-[4/3] w-8 h-6 flex items-center justify-center">
3-
<component v-if="flagComponent" :is="flagComponent" class="flag-icon h-full object-contain rounded-sm"/>
4-
<span v-else-if="flagIcons && countryCode">{{ countryCode }}</span>
3+
<img v-if="flagUrl" :src="flagUrl" :alt="countryCode" class="flag-icon h-full object-contain rounded-sm"/>
4+
<span v-else-if="getFlagUrl && countryCode">{{ countryCode }}</span>
55
</div>
66
</template>
77

88
<script setup lang="ts">
9-
import { computed, shallowRef, type Component } from 'vue';
9+
import { computed, shallowRef } from 'vue';
1010
1111
const props = defineProps(['countryCode']);
1212
13-
// the flag set takes megabytes, so it is loaded when a flag is shown for the first time
14-
const flagIcons = shallowRef<Record<string, Component>>();
15-
import('@iconify-prerendered/vue-flag').then((icons) => {
16-
flagIcons.value = icons;
13+
// the map of flag URLs is loaded when a flag is shown for the first time
14+
const getFlagUrl = shallowRef<(countryCode: string) => string | undefined>();
15+
import('./countryFlagUrls').then((module) => {
16+
getFlagUrl.value = module.default;
1717
});
1818
19-
const flagComponent = computed(() => {
20-
if (!props.countryCode || !flagIcons.value) {
21-
return null;
22-
}
23-
const normalizedCode = props.countryCode.charAt(0).toUpperCase() + props.countryCode.slice(1).toLowerCase();
24-
return flagIcons.value[`Icon${normalizedCode}4x3`] || null; // 4:3 aspect ratio flags
25-
});
19+
const flagUrl = computed(() => props.countryCode && getFlagUrl.value?.(props.countryCode));
2620
</script>
2721

2822
<style scoped>
2923
.flag-icon {
30-
box-shadow: inset -0.3px -0.3px 0.3px 0px rgba(0 0 0 / 0.2),
24+
box-shadow: inset -0.3px -0.3px 0.3px 0px rgba(0 0 0 / 0.2),
3125
inset 0.3px 0.3px 0.3px 0px rgba(255 255 255 / 0.2),
3226
0px 0px 3px #00000030;
3327
width: auto;
3428
}
35-
</style>
29+
</style>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// URLs of the 4:3 flags of flag-icons: only the URLs get into the bundle, the browser downloads the SVG of a flag when it is shown.
2+
// no-inline keeps small flags from being inlined as data URIs, exhaustive lets the glob into node_modules
3+
const flagUrls = import.meta.glob<string>('/node_modules/flag-icons/flags/4x3/*.svg', {
4+
eager: true,
5+
query: '?url&no-inline',
6+
import: 'default',
7+
exhaustive: true,
8+
});
9+
10+
export default function getFlagUrl(countryCode: string): string | undefined {
11+
return flagUrls[`/node_modules/flag-icons/flags/4x3/${countryCode.toLowerCase()}.svg`];
12+
}

0 commit comments

Comments
 (0)