@heyramzi/ui 3.3.1 → 3.5.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/COMPONENTS.md +1 -0
- package/dist/components/copy/clipboard.js +3 -3
- package/dist/components/docs/index.js +1 -0
- package/dist/components/flag/codes.js +13 -0
- package/dist/components/flag/index.js +2 -0
- package/dist/slides/export/pdf.js +7 -0
- package/dist/vite/flags.js +46 -0
- package/package.json +7 -2
- package/src/charts/chart-plot.svelte +21 -18
- package/src/components/copy/clipboard.test.ts +17 -1
- package/src/components/copy/clipboard.ts +3 -3
- package/src/components/docs/document-view.svelte +232 -0
- package/src/components/docs/document-view.svelte.d.ts +29 -0
- package/src/components/docs/index.ts +1 -0
- package/src/components/flag/codes.test.ts +30 -0
- package/src/components/flag/codes.ts +25 -0
- package/src/components/flag/flag.svelte +51 -0
- package/src/components/flag/flag.svelte.d.ts +12 -0
- package/src/components/flag/flags/AD.svg +1 -0
- package/src/components/flag/flags/AE.svg +1 -0
- package/src/components/flag/flags/AF.svg +1 -0
- package/src/components/flag/flags/AG.svg +1 -0
- package/src/components/flag/flags/AI.svg +1 -0
- package/src/components/flag/flags/AL.svg +1 -0
- package/src/components/flag/flags/AM.svg +1 -0
- package/src/components/flag/flags/AO.svg +1 -0
- package/src/components/flag/flags/AQ.svg +1 -0
- package/src/components/flag/flags/AR.svg +1 -0
- package/src/components/flag/flags/AS.svg +1 -0
- package/src/components/flag/flags/AT.svg +1 -0
- package/src/components/flag/flags/AU.svg +1 -0
- package/src/components/flag/flags/AW.svg +1 -0
- package/src/components/flag/flags/AX.svg +1 -0
- package/src/components/flag/flags/AZ.svg +1 -0
- package/src/components/flag/flags/BA.svg +1 -0
- package/src/components/flag/flags/BB.svg +1 -0
- package/src/components/flag/flags/BD.svg +1 -0
- package/src/components/flag/flags/BE.svg +1 -0
- package/src/components/flag/flags/BF.svg +1 -0
- package/src/components/flag/flags/BG.svg +1 -0
- package/src/components/flag/flags/BH.svg +1 -0
- package/src/components/flag/flags/BI.svg +1 -0
- package/src/components/flag/flags/BJ.svg +1 -0
- package/src/components/flag/flags/BL.svg +1 -0
- package/src/components/flag/flags/BM.svg +1 -0
- package/src/components/flag/flags/BN.svg +1 -0
- package/src/components/flag/flags/BO.svg +1 -0
- package/src/components/flag/flags/BQ.svg +1 -0
- package/src/components/flag/flags/BR.svg +1 -0
- package/src/components/flag/flags/BS.svg +1 -0
- package/src/components/flag/flags/BT.svg +1 -0
- package/src/components/flag/flags/BV.svg +1 -0
- package/src/components/flag/flags/BW.svg +1 -0
- package/src/components/flag/flags/BY.svg +1 -0
- package/src/components/flag/flags/BZ.svg +1 -0
- package/src/components/flag/flags/CA.svg +1 -0
- package/src/components/flag/flags/CC.svg +1 -0
- package/src/components/flag/flags/CD.svg +1 -0
- package/src/components/flag/flags/CF.svg +1 -0
- package/src/components/flag/flags/CG.svg +1 -0
- package/src/components/flag/flags/CH.svg +1 -0
- package/src/components/flag/flags/CI.svg +1 -0
- package/src/components/flag/flags/CK.svg +1 -0
- package/src/components/flag/flags/CL.svg +1 -0
- package/src/components/flag/flags/CM.svg +1 -0
- package/src/components/flag/flags/CN.svg +1 -0
- package/src/components/flag/flags/CO.svg +1 -0
- package/src/components/flag/flags/CR.svg +1 -0
- package/src/components/flag/flags/CU.svg +1 -0
- package/src/components/flag/flags/CV.svg +1 -0
- package/src/components/flag/flags/CW.svg +1 -0
- package/src/components/flag/flags/CX.svg +1 -0
- package/src/components/flag/flags/CY.svg +1 -0
- package/src/components/flag/flags/CZ.svg +1 -0
- package/src/components/flag/flags/DE.svg +1 -0
- package/src/components/flag/flags/DJ.svg +1 -0
- package/src/components/flag/flags/DK.svg +1 -0
- package/src/components/flag/flags/DM.svg +1 -0
- package/src/components/flag/flags/DO.svg +1 -0
- package/src/components/flag/flags/DZ.svg +1 -0
- package/src/components/flag/flags/EC.svg +1 -0
- package/src/components/flag/flags/EE.svg +1 -0
- package/src/components/flag/flags/EG.svg +1 -0
- package/src/components/flag/flags/EH.svg +1 -0
- package/src/components/flag/flags/ER.svg +1 -0
- package/src/components/flag/flags/ES.svg +1 -0
- package/src/components/flag/flags/ET.svg +1 -0
- package/src/components/flag/flags/FI.svg +1 -0
- package/src/components/flag/flags/FJ.svg +1 -0
- package/src/components/flag/flags/FK.svg +1 -0
- package/src/components/flag/flags/FM.svg +1 -0
- package/src/components/flag/flags/FO.svg +1 -0
- package/src/components/flag/flags/FR.svg +1 -0
- package/src/components/flag/flags/GA.svg +1 -0
- package/src/components/flag/flags/GB.svg +1 -0
- package/src/components/flag/flags/GD.svg +1 -0
- package/src/components/flag/flags/GE.svg +1 -0
- package/src/components/flag/flags/GF.svg +1 -0
- package/src/components/flag/flags/GG.svg +1 -0
- package/src/components/flag/flags/GH.svg +1 -0
- package/src/components/flag/flags/GI.svg +1 -0
- package/src/components/flag/flags/GL.svg +1 -0
- package/src/components/flag/flags/GM.svg +1 -0
- package/src/components/flag/flags/GN.svg +1 -0
- package/src/components/flag/flags/GP.svg +1 -0
- package/src/components/flag/flags/GQ.svg +1 -0
- package/src/components/flag/flags/GR.svg +1 -0
- package/src/components/flag/flags/GS.svg +1 -0
- package/src/components/flag/flags/GT.svg +1 -0
- package/src/components/flag/flags/GU.svg +1 -0
- package/src/components/flag/flags/GW.svg +1 -0
- package/src/components/flag/flags/GY.svg +1 -0
- package/src/components/flag/flags/HK.svg +1 -0
- package/src/components/flag/flags/HM.svg +1 -0
- package/src/components/flag/flags/HN.svg +1 -0
- package/src/components/flag/flags/HR.svg +1 -0
- package/src/components/flag/flags/HT.svg +1 -0
- package/src/components/flag/flags/HU.svg +1 -0
- package/src/components/flag/flags/ID.svg +1 -0
- package/src/components/flag/flags/IE.svg +1 -0
- package/src/components/flag/flags/IL.svg +1 -0
- package/src/components/flag/flags/IM.svg +1 -0
- package/src/components/flag/flags/IN.svg +1 -0
- package/src/components/flag/flags/IO.svg +1 -0
- package/src/components/flag/flags/IQ.svg +1 -0
- package/src/components/flag/flags/IR.svg +1 -0
- package/src/components/flag/flags/IS.svg +1 -0
- package/src/components/flag/flags/IT.svg +1 -0
- package/src/components/flag/flags/JE.svg +1 -0
- package/src/components/flag/flags/JM.svg +1 -0
- package/src/components/flag/flags/JO.svg +1 -0
- package/src/components/flag/flags/JP.svg +1 -0
- package/src/components/flag/flags/KE.svg +1 -0
- package/src/components/flag/flags/KG.svg +1 -0
- package/src/components/flag/flags/KH.svg +1 -0
- package/src/components/flag/flags/KI.svg +1 -0
- package/src/components/flag/flags/KM.svg +1 -0
- package/src/components/flag/flags/KN.svg +1 -0
- package/src/components/flag/flags/KP.svg +1 -0
- package/src/components/flag/flags/KR.svg +1 -0
- package/src/components/flag/flags/KW.svg +1 -0
- package/src/components/flag/flags/KY.svg +1 -0
- package/src/components/flag/flags/KZ.svg +1 -0
- package/src/components/flag/flags/LA.svg +1 -0
- package/src/components/flag/flags/LB.svg +1 -0
- package/src/components/flag/flags/LC.svg +1 -0
- package/src/components/flag/flags/LI.svg +1 -0
- package/src/components/flag/flags/LICENSE +22 -0
- package/src/components/flag/flags/LK.svg +1 -0
- package/src/components/flag/flags/LR.svg +1 -0
- package/src/components/flag/flags/LS.svg +1 -0
- package/src/components/flag/flags/LT.svg +1 -0
- package/src/components/flag/flags/LU.svg +1 -0
- package/src/components/flag/flags/LV.svg +1 -0
- package/src/components/flag/flags/LY.svg +1 -0
- package/src/components/flag/flags/MA.svg +1 -0
- package/src/components/flag/flags/MC.svg +1 -0
- package/src/components/flag/flags/MD.svg +1 -0
- package/src/components/flag/flags/ME.svg +1 -0
- package/src/components/flag/flags/MF.svg +1 -0
- package/src/components/flag/flags/MG.svg +1 -0
- package/src/components/flag/flags/MH.svg +1 -0
- package/src/components/flag/flags/MK.svg +1 -0
- package/src/components/flag/flags/ML.svg +1 -0
- package/src/components/flag/flags/MM.svg +1 -0
- package/src/components/flag/flags/MN.svg +1 -0
- package/src/components/flag/flags/MO.svg +1 -0
- package/src/components/flag/flags/MP.svg +1 -0
- package/src/components/flag/flags/MQ.svg +1 -0
- package/src/components/flag/flags/MR.svg +1 -0
- package/src/components/flag/flags/MS.svg +1 -0
- package/src/components/flag/flags/MT.svg +1 -0
- package/src/components/flag/flags/MU.svg +1 -0
- package/src/components/flag/flags/MV.svg +1 -0
- package/src/components/flag/flags/MW.svg +1 -0
- package/src/components/flag/flags/MX.svg +1 -0
- package/src/components/flag/flags/MY.svg +1 -0
- package/src/components/flag/flags/MZ.svg +1 -0
- package/src/components/flag/flags/NA.svg +1 -0
- package/src/components/flag/flags/NC.svg +1 -0
- package/src/components/flag/flags/NE.svg +1 -0
- package/src/components/flag/flags/NF.svg +1 -0
- package/src/components/flag/flags/NG.svg +1 -0
- package/src/components/flag/flags/NI.svg +1 -0
- package/src/components/flag/flags/NL.svg +1 -0
- package/src/components/flag/flags/NO.svg +1 -0
- package/src/components/flag/flags/NP.svg +1 -0
- package/src/components/flag/flags/NR.svg +1 -0
- package/src/components/flag/flags/NU.svg +1 -0
- package/src/components/flag/flags/NZ.svg +1 -0
- package/src/components/flag/flags/OM.svg +1 -0
- package/src/components/flag/flags/PA.svg +1 -0
- package/src/components/flag/flags/PE.svg +1 -0
- package/src/components/flag/flags/PF.svg +1 -0
- package/src/components/flag/flags/PG.svg +1 -0
- package/src/components/flag/flags/PH.svg +1 -0
- package/src/components/flag/flags/PK.svg +1 -0
- package/src/components/flag/flags/PL.svg +1 -0
- package/src/components/flag/flags/PM.svg +1 -0
- package/src/components/flag/flags/PN.svg +1 -0
- package/src/components/flag/flags/PR.svg +1 -0
- package/src/components/flag/flags/PS.svg +1 -0
- package/src/components/flag/flags/PT.svg +1 -0
- package/src/components/flag/flags/PW.svg +1 -0
- package/src/components/flag/flags/PY.svg +1 -0
- package/src/components/flag/flags/QA.svg +1 -0
- package/src/components/flag/flags/RE.svg +1 -0
- package/src/components/flag/flags/RO.svg +1 -0
- package/src/components/flag/flags/RS.svg +1 -0
- package/src/components/flag/flags/RU.svg +1 -0
- package/src/components/flag/flags/RW.svg +1 -0
- package/src/components/flag/flags/SA.svg +1 -0
- package/src/components/flag/flags/SB.svg +1 -0
- package/src/components/flag/flags/SC.svg +1 -0
- package/src/components/flag/flags/SD.svg +1 -0
- package/src/components/flag/flags/SE.svg +1 -0
- package/src/components/flag/flags/SG.svg +1 -0
- package/src/components/flag/flags/SH.svg +1 -0
- package/src/components/flag/flags/SI.svg +1 -0
- package/src/components/flag/flags/SJ.svg +1 -0
- package/src/components/flag/flags/SK.svg +1 -0
- package/src/components/flag/flags/SL.svg +1 -0
- package/src/components/flag/flags/SM.svg +1 -0
- package/src/components/flag/flags/SN.svg +1 -0
- package/src/components/flag/flags/SO.svg +1 -0
- package/src/components/flag/flags/SR.svg +1 -0
- package/src/components/flag/flags/SS.svg +1 -0
- package/src/components/flag/flags/ST.svg +1 -0
- package/src/components/flag/flags/SV.svg +1 -0
- package/src/components/flag/flags/SX.svg +1 -0
- package/src/components/flag/flags/SY.svg +1 -0
- package/src/components/flag/flags/SZ.svg +1 -0
- package/src/components/flag/flags/TC.svg +1 -0
- package/src/components/flag/flags/TD.svg +1 -0
- package/src/components/flag/flags/TF.svg +1 -0
- package/src/components/flag/flags/TG.svg +1 -0
- package/src/components/flag/flags/TH.svg +1 -0
- package/src/components/flag/flags/TJ.svg +1 -0
- package/src/components/flag/flags/TK.svg +1 -0
- package/src/components/flag/flags/TL.svg +1 -0
- package/src/components/flag/flags/TM.svg +1 -0
- package/src/components/flag/flags/TN.svg +1 -0
- package/src/components/flag/flags/TO.svg +1 -0
- package/src/components/flag/flags/TR.svg +1 -0
- package/src/components/flag/flags/TT.svg +1 -0
- package/src/components/flag/flags/TV.svg +1 -0
- package/src/components/flag/flags/TW.svg +1 -0
- package/src/components/flag/flags/TZ.svg +1 -0
- package/src/components/flag/flags/UA.svg +1 -0
- package/src/components/flag/flags/UG.svg +1 -0
- package/src/components/flag/flags/UM.svg +1 -0
- package/src/components/flag/flags/US.svg +1 -0
- package/src/components/flag/flags/UY.svg +1 -0
- package/src/components/flag/flags/UZ.svg +1 -0
- package/src/components/flag/flags/VA.svg +1 -0
- package/src/components/flag/flags/VC.svg +1 -0
- package/src/components/flag/flags/VE.svg +1 -0
- package/src/components/flag/flags/VG.svg +1 -0
- package/src/components/flag/flags/VI.svg +1 -0
- package/src/components/flag/flags/VN.svg +1 -0
- package/src/components/flag/flags/VU.svg +1 -0
- package/src/components/flag/flags/WF.svg +1 -0
- package/src/components/flag/flags/WS.svg +1 -0
- package/src/components/flag/flags/XK.svg +1 -0
- package/src/components/flag/flags/YE.svg +1 -0
- package/src/components/flag/flags/YT.svg +1 -0
- package/src/components/flag/flags/ZA.svg +1 -0
- package/src/components/flag/flags/ZM.svg +1 -0
- package/src/components/flag/flags/ZW.svg +1 -0
- package/src/components/flag/index.ts +2 -0
- package/src/slides/export/pdf.ts +11 -0
- package/src/vite/flags.test.ts +38 -0
- package/src/vite/flags.ts +63 -0
package/COMPONENTS.md
CHANGED
|
@@ -63,6 +63,7 @@ Generic pieces built on the primitives, imported from `@heyramzi/ui/components/<
|
|
|
63
63
|
|
|
64
64
|
- `copy` — CopyButton, copyText, copyRichText, htmlToPlainText, Copied
|
|
65
65
|
- `data-table` — SortableHead, Pagination, InfiniteScroll, RowContextMenu, TableSkeleton, and the sort, page and filter helpers
|
|
66
|
+
- `flag` — Flag, FLAG_CODES, flagCode: a country flag from an ISO 3166-1 alpha-2 code (plus XK for Kosovo), 1em tall at 3:2, one lazy `<img>` of `/flags/<CODE>.svg` each. Add `flagAssets()` from `@heyramzi/ui/vite` to the app's Vite plugins to serve the 250 SVGs (country-flag-icons, MIT, licence beside them). Decorative unless `label` is set. An unknown code renders nothing; to keep a row's layout, wrap it in `<span class="inline-block aspect-[3/2] h-[1em]">`
|
|
66
67
|
- `hold-to-confirm` — HoldToConfirm, press and hold to confirm
|
|
67
68
|
- `language-flag` — LanguageFlag, LANGUAGE_FLAGS (code to `/flags/*.svg`, also a SegmentedFilter option's `image`)
|
|
68
69
|
- `list-page` — ListPageShell, MasterDetail, GroupedList, RailRow and the rest of the master-detail page kit, DetailPanel, ToolbarSearch, toolbarTriggerClass, NoticeBar, EmptyState, FilterPopover, setSectionHost
|
|
@@ -32,7 +32,7 @@ export function htmlToPlainText(html) {
|
|
|
32
32
|
if (typeof document === "undefined") {
|
|
33
33
|
return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<\/li>/gi, "\n").replace(/<li>/gi, "• ").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").trim();
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return
|
|
35
|
+
// A DOMParser document has no browsing context, so nothing in the markup loads or runs.
|
|
36
|
+
// An element from document.createElement belongs to the live page: an <img onerror> fires.
|
|
37
|
+
return new DOMParser().parseFromString(html, "text/html").body.textContent ?? "";
|
|
38
38
|
}
|
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
* A surface supplies a title and a body, and never lays a document out itself.
|
|
4
4
|
*/
|
|
5
5
|
export { default as DocPage } from "../../../src/components/docs/doc-page.svelte";
|
|
6
|
+
export { default as DocumentView } from "../../../src/components/docs/document-view.svelte";
|
|
6
7
|
export { default as OnThisPage } from "../../../src/components/docs/on-this-page.svelte";
|
|
7
8
|
export { ScrollSpy, READ_LINE } from "./scroll-spy.svelte.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Where the host serves the flag files. `flagAssets()` from `@heyramzi/ui/vite` serves them there. */
|
|
2
|
+
export const FLAG_BASE = "/flags";
|
|
3
|
+
/**
|
|
4
|
+
* The 249 ISO 3166-1 alpha-2 codes plus XK (Kosovo, user-assigned but in common use), each with
|
|
5
|
+
* a file in `./flags`, from country-flag-icons 1.6.20 (MIT).
|
|
6
|
+
*/
|
|
7
|
+
export const FLAG_CODES = ("AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR " + "BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ " + "EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW " + "GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY " + "KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV " + "MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY " + "QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG " + "TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS XK YE YT ZA ZM ZW").split(" ");
|
|
8
|
+
const KNOWN = new Set(FLAG_CODES);
|
|
9
|
+
/** The code in capitals when a flag exists for it, else null. */
|
|
10
|
+
export function flagCode(code) {
|
|
11
|
+
const upper = code?.trim().toUpperCase() ?? "";
|
|
12
|
+
return KNOWN.has(upper) ? upper : null;
|
|
13
|
+
}
|
|
@@ -38,6 +38,13 @@ export async function printPage(page, options = {}) {
|
|
|
38
38
|
}
|
|
39
39
|
await page.evaluate(() => document.fonts.ready.then(() => undefined));
|
|
40
40
|
if (options.settle) await page.waitForTimeout(options.settle);
|
|
41
|
+
// WHY: a lazy or still-loading image read as broken while the PDF printed it (testimonial photos, 11 Nov 2026).
|
|
42
|
+
await page.evaluate(() => Promise.all([...document.images].map((img) => {
|
|
43
|
+
img.loading = "eager";
|
|
44
|
+
if (img.complete) return undefined;
|
|
45
|
+
const loaded = img.decode().catch(() => undefined);
|
|
46
|
+
return Promise.race([loaded, new Promise((resolve) => setTimeout(resolve, 15e3))]);
|
|
47
|
+
})).then(() => undefined));
|
|
41
48
|
const brokenImages = await page.evaluate(() => [...document.images].filter((img) => !img.complete || img.naturalWidth === 0).map((img) => img.src));
|
|
42
49
|
const { size } = options;
|
|
43
50
|
const pdf = await page.pdf({
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
// From src/vite in the repo, or dist/vite in the published package.
|
|
5
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const FLAGS_DIR = [join(here, "../components/flag/flags"), join(here, "../../src/components/flag/flags")].find((dir) => existsSync(dir));
|
|
7
|
+
/**
|
|
8
|
+
* Serves the `Flag` SVGs at `/flags/<CODE>.svg`: from the package in dev and preview, and as
|
|
9
|
+
* one static file each in the client build. Add it to `plugins` in `vite.config.ts`.
|
|
10
|
+
*/
|
|
11
|
+
export function flagAssets(options = {}) {
|
|
12
|
+
const base = (options.base ?? "/flags").replace(/\/$/, "");
|
|
13
|
+
if (!FLAGS_DIR) throw new Error("@heyramzi/ui: the flag SVGs are missing from the package");
|
|
14
|
+
const dir = FLAGS_DIR;
|
|
15
|
+
const files = readdirSync(dir).filter((name) => /^[A-Z]{2}\.svg$/.test(name));
|
|
16
|
+
const known = new Set(files);
|
|
17
|
+
let ssrBuild = false;
|
|
18
|
+
// Returns nothing: Vite calls a function returned from configureServer as a post hook.
|
|
19
|
+
const serve = (server) => {
|
|
20
|
+
server.middlewares.use((req, res, next) => {
|
|
21
|
+
const path = req.url?.split("?")[0] ?? "";
|
|
22
|
+
const name = path.startsWith(`${base}/`) ? path.slice(base.length + 1) : "";
|
|
23
|
+
if (!known.has(name)) return next();
|
|
24
|
+
res.setHeader("Content-Type", "image/svg+xml");
|
|
25
|
+
res.setHeader("Cache-Control", "public, max-age=86400");
|
|
26
|
+
res.end(readFileSync(join(dir, name)));
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
name: "heyramzi-ui:flags",
|
|
31
|
+
configResolved(config) {
|
|
32
|
+
ssrBuild = Boolean(config.build.ssr);
|
|
33
|
+
},
|
|
34
|
+
configureServer: serve,
|
|
35
|
+
configurePreviewServer: serve,
|
|
36
|
+
generateBundle() {
|
|
37
|
+
const consumer = this.environment?.config?.consumer;
|
|
38
|
+
if (consumer ? consumer !== "client" : ssrBuild) return;
|
|
39
|
+
for (const name of files) this.emitFile({
|
|
40
|
+
type: "asset",
|
|
41
|
+
fileName: `${base.replace(/^\//, "")}/${name}`,
|
|
42
|
+
source: readFileSync(join(dir, name))
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyramzi/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Svelte 5 UI for apps: primitives, effects and animations, plus work (tasks and docs), charts, slides, an editor, onboarding and a canvas as subpaths.",
|
|
5
5
|
"homepage": "https://ui.heyramzi.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -115,6 +115,11 @@
|
|
|
115
115
|
"node": "./dist/effects/magnetic/magnetic.js",
|
|
116
116
|
"default": "./src/effects/magnetic/magnetic.ts"
|
|
117
117
|
},
|
|
118
|
+
"./vite": {
|
|
119
|
+
"types": "./src/vite/flags.ts",
|
|
120
|
+
"node": "./dist/vite/flags.js",
|
|
121
|
+
"default": "./src/vite/flags.ts"
|
|
122
|
+
},
|
|
118
123
|
"./icons/sf-symbols": {
|
|
119
124
|
"svelte": "./src/icons/sf-symbols.ts",
|
|
120
125
|
"types": "./src/icons/sf-symbols.ts",
|
|
@@ -389,6 +394,6 @@
|
|
|
389
394
|
"sync": "pnpm sync:transitions && pnpm sync:shadcn",
|
|
390
395
|
"sync:check": "node scripts/sync-transitions.mjs --check && node scripts/sync-shadcn.mjs --check",
|
|
391
396
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
392
|
-
"build": "node ../../../
|
|
397
|
+
"build": "node ../../../upsys/kit/scripts/build-dist.mjs"
|
|
393
398
|
}
|
|
394
399
|
}
|
|
@@ -82,25 +82,28 @@
|
|
|
82
82
|
{#if count > 0}
|
|
83
83
|
<p class="hr-sr-only" aria-live="polite">{announce}</p>
|
|
84
84
|
{/if}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
{#each table.columns as column, j (j)}
|
|
91
|
-
<th scope="col">{column}</th>
|
|
92
|
-
{/each}
|
|
93
|
-
</tr>
|
|
94
|
-
</thead>
|
|
95
|
-
<tbody>
|
|
96
|
-
{#each table.rows as row, i (i)}
|
|
85
|
+
<!-- A table ignores width: 1px and spills past the page, so the clipping box is a div. -->
|
|
86
|
+
<div class="hr-sr-only">
|
|
87
|
+
<table>
|
|
88
|
+
<caption>{label}</caption>
|
|
89
|
+
<thead>
|
|
97
90
|
<tr>
|
|
98
|
-
<th scope="
|
|
99
|
-
{#each
|
|
100
|
-
<
|
|
91
|
+
<th scope="col">{table.xLabel}</th>
|
|
92
|
+
{#each table.columns as column, j (j)}
|
|
93
|
+
<th scope="col">{column}</th>
|
|
101
94
|
{/each}
|
|
102
95
|
</tr>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
</thead>
|
|
97
|
+
<tbody>
|
|
98
|
+
{#each table.rows as row, i (i)}
|
|
99
|
+
<tr>
|
|
100
|
+
<th scope="row">{row.x}</th>
|
|
101
|
+
{#each row.cells as cell, j (j)}
|
|
102
|
+
<td>{cell}</td>
|
|
103
|
+
{/each}
|
|
104
|
+
</tr>
|
|
105
|
+
{/each}
|
|
106
|
+
</tbody>
|
|
107
|
+
</table>
|
|
108
|
+
</div>
|
|
106
109
|
{/if}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
|
|
2
|
-
import { copyRichText, copyText } from "./clipboard.js";
|
|
2
|
+
import { copyRichText, copyText, htmlToPlainText } from "./clipboard.js";
|
|
3
3
|
|
|
4
4
|
// Mock navigator.clipboard
|
|
5
5
|
const mockClipboard = {
|
|
@@ -204,3 +204,19 @@ describe("htmlToPlainText (server-side)", () => {
|
|
|
204
204
|
expect(mockWriteText).toHaveBeenCalledWith("Hello World & Friends <3>");
|
|
205
205
|
});
|
|
206
206
|
});
|
|
207
|
+
|
|
208
|
+
describe("htmlToPlainText (browser)", () => {
|
|
209
|
+
afterEach(() => vi.restoreAllMocks());
|
|
210
|
+
|
|
211
|
+
it("parses into an inert document, so an onerror in the markup never runs", async () => {
|
|
212
|
+
const win = window as Window & { __fired?: boolean };
|
|
213
|
+
win.__fired = false;
|
|
214
|
+
const createElement = vi.spyOn(document, "createElement");
|
|
215
|
+
expect(htmlToPlainText('<img src="x" onerror="window.__fired=true">hi')).toBe("hi");
|
|
216
|
+
await new Promise((done) => setTimeout(done, 10));
|
|
217
|
+
expect(win.__fired).toBe(false);
|
|
218
|
+
// jsdom loads no images, so the onerror can't fire here either way: the live-document
|
|
219
|
+
// element the old code built is what a real browser runs the handler on.
|
|
220
|
+
expect(createElement).not.toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -47,7 +47,7 @@ export function htmlToPlainText(html: string): string {
|
|
|
47
47
|
.replace(/>/g, ">")
|
|
48
48
|
.trim();
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
50
|
+
// A DOMParser document has no browsing context, so nothing in the markup loads or runs.
|
|
51
|
+
// An element from document.createElement belongs to the live page: an <img onerror> fires.
|
|
52
|
+
return new DOMParser().parseFromString(html, "text/html").body.textContent ?? "";
|
|
53
53
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import type { Component, Snippet } from "svelte";
|
|
6
|
+
import { untrack } from "svelte";
|
|
7
|
+
import DocPage from "./doc-page.svelte";
|
|
8
|
+
import type { DocHeading } from "./scroll-spy.svelte.js";
|
|
9
|
+
|
|
10
|
+
// =========================
|
|
11
|
+
// TYPES
|
|
12
|
+
// =========================
|
|
13
|
+
interface Props {
|
|
14
|
+
title: string;
|
|
15
|
+
/** Breadcrumb, as a reader would say it: "Marketing / Reports". */
|
|
16
|
+
section?: string;
|
|
17
|
+
/** Already worded and localised: "Updated 30 Jul 2026". */
|
|
18
|
+
updated?: string;
|
|
19
|
+
icon?: Component<{
|
|
20
|
+
class?: string;
|
|
21
|
+
weight?: "bold";
|
|
22
|
+
"aria-hidden"?: boolean | "true" | "false";
|
|
23
|
+
}>;
|
|
24
|
+
actions?: Snippet;
|
|
25
|
+
/** The body's h2s, anchored by id, for the "On this page" rail. */
|
|
26
|
+
headings?: DocHeading[];
|
|
27
|
+
contentsLabel: string;
|
|
28
|
+
/** The rendered body. Trusted HTML from the consumer's own parser, never user input. */
|
|
29
|
+
html: string;
|
|
30
|
+
/** Changes when another document is shown: the pane returns to the top and the body re-enters. */
|
|
31
|
+
resetKey: string;
|
|
32
|
+
measure?: "prose" | "wide";
|
|
33
|
+
/** Runs on the body after every swap (icons, diagrams). Returns its cleanup. */
|
|
34
|
+
enhance?: (root: HTMLElement) => void | (() => void);
|
|
35
|
+
class?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// =========================
|
|
39
|
+
// PROPS
|
|
40
|
+
// =========================
|
|
41
|
+
let {
|
|
42
|
+
title,
|
|
43
|
+
section,
|
|
44
|
+
updated,
|
|
45
|
+
icon,
|
|
46
|
+
actions,
|
|
47
|
+
headings = [],
|
|
48
|
+
contentsLabel,
|
|
49
|
+
html,
|
|
50
|
+
resetKey,
|
|
51
|
+
measure = "prose",
|
|
52
|
+
enhance,
|
|
53
|
+
class: className = "",
|
|
54
|
+
}: Props = $props();
|
|
55
|
+
|
|
56
|
+
// =========================
|
|
57
|
+
// STATE
|
|
58
|
+
// =========================
|
|
59
|
+
let body = $state<HTMLElement | null>(null);
|
|
60
|
+
|
|
61
|
+
// =========================
|
|
62
|
+
// EFFECTS
|
|
63
|
+
// =========================
|
|
64
|
+
$effect(() => {
|
|
65
|
+
void html;
|
|
66
|
+
const root = body;
|
|
67
|
+
if (!root || !enhance) return;
|
|
68
|
+
return untrack(() => enhance(root)) ?? undefined;
|
|
69
|
+
});
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<!-- =========================== -->
|
|
73
|
+
<!-- MARKUP -->
|
|
74
|
+
<!-- =========================== -->
|
|
75
|
+
<!-- Inline, not framed: a link is an ordinary anchor, so the router swaps only this body. -->
|
|
76
|
+
<DocPage
|
|
77
|
+
{title}
|
|
78
|
+
{section}
|
|
79
|
+
{updated}
|
|
80
|
+
{icon}
|
|
81
|
+
{actions}
|
|
82
|
+
{headings}
|
|
83
|
+
{contentsLabel}
|
|
84
|
+
{resetKey}
|
|
85
|
+
{measure}
|
|
86
|
+
>
|
|
87
|
+
{#key resetKey}
|
|
88
|
+
<div bind:this={body} class="document-view t-enter {className}">
|
|
89
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted, parsed server-side -->
|
|
90
|
+
{@html html}
|
|
91
|
+
</div>
|
|
92
|
+
{/key}
|
|
93
|
+
</DocPage>
|
|
94
|
+
|
|
95
|
+
<!-- =========================== -->
|
|
96
|
+
<!-- STYLES -->
|
|
97
|
+
<!-- =========================== -->
|
|
98
|
+
<!-- In the components layer at zero specificity, so a consumer's own document stylesheet wins without !important. -->
|
|
99
|
+
<style>
|
|
100
|
+
@layer theme, base, components, utilities;
|
|
101
|
+
|
|
102
|
+
@layer components {
|
|
103
|
+
:global {
|
|
104
|
+
:where(.document-view) {
|
|
105
|
+
color: var(--color-foreground);
|
|
106
|
+
font-size: 0.9375rem;
|
|
107
|
+
line-height: 1.65;
|
|
108
|
+
overflow-wrap: anywhere;
|
|
109
|
+
}
|
|
110
|
+
:where(.document-view) :where(h1, h2, h3, h4) {
|
|
111
|
+
font-family: var(--font-heading);
|
|
112
|
+
color: var(--color-foreground);
|
|
113
|
+
line-height: 1.25;
|
|
114
|
+
text-wrap: balance;
|
|
115
|
+
scroll-margin-top: 6rem;
|
|
116
|
+
}
|
|
117
|
+
:where(.document-view) :where(h1:first-child) {
|
|
118
|
+
display: none;
|
|
119
|
+
}
|
|
120
|
+
:where(.document-view) :where(h2) {
|
|
121
|
+
margin: 2.5rem 0 0.75rem;
|
|
122
|
+
font-size: 1.25rem;
|
|
123
|
+
font-weight: 700;
|
|
124
|
+
letter-spacing: -0.01em;
|
|
125
|
+
}
|
|
126
|
+
:where(.document-view) :where(h3) {
|
|
127
|
+
margin: 1.75rem 0 0.5rem;
|
|
128
|
+
font-size: 1rem;
|
|
129
|
+
font-weight: 600;
|
|
130
|
+
}
|
|
131
|
+
:where(.document-view) > :where(:first-child, h1:first-child + *) {
|
|
132
|
+
margin-top: 0;
|
|
133
|
+
}
|
|
134
|
+
:where(.document-view) :where(p, ul, ol, blockquote, pre, table) {
|
|
135
|
+
margin: 0 0 1rem;
|
|
136
|
+
}
|
|
137
|
+
:where(.document-view) :where(ul, ol) {
|
|
138
|
+
padding-left: 1.25rem;
|
|
139
|
+
}
|
|
140
|
+
:where(.document-view) :where(li + li) {
|
|
141
|
+
margin-top: 0.25rem;
|
|
142
|
+
}
|
|
143
|
+
:where(.document-view) :where(a) {
|
|
144
|
+
color: var(--color-brand);
|
|
145
|
+
text-decoration: underline;
|
|
146
|
+
text-decoration-color: color-mix(in oklch, currentColor 35%, transparent);
|
|
147
|
+
text-underline-offset: 0.16em;
|
|
148
|
+
transition: text-decoration-color var(--duration-fast) ease-out;
|
|
149
|
+
}
|
|
150
|
+
@media (hover: hover) and (pointer: fine) {
|
|
151
|
+
:where(.document-view) :where(a:hover) {
|
|
152
|
+
text-decoration-color: currentColor;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/* A wikilink to a page that does not exist yet reads as text, not as a dead link. */
|
|
156
|
+
:where(.document-view) :where(.wikilink.missing, a.missing) {
|
|
157
|
+
color: var(--color-muted-foreground);
|
|
158
|
+
text-decoration-style: dashed;
|
|
159
|
+
}
|
|
160
|
+
:where(.document-view) :where(code) {
|
|
161
|
+
font-family: var(--font-mono);
|
|
162
|
+
font-size: 0.85em;
|
|
163
|
+
padding: 0.1em 0.35em;
|
|
164
|
+
border-radius: var(--radius-sm, 0.375rem);
|
|
165
|
+
background: var(--color-muted);
|
|
166
|
+
}
|
|
167
|
+
:where(.document-view) :where(pre) {
|
|
168
|
+
overflow-x: auto;
|
|
169
|
+
padding: 0.875rem 1rem;
|
|
170
|
+
border: 1px solid var(--color-border);
|
|
171
|
+
border-radius: var(--radius-lg);
|
|
172
|
+
background: var(--color-muted);
|
|
173
|
+
font-size: 0.8125rem;
|
|
174
|
+
line-height: 1.55;
|
|
175
|
+
}
|
|
176
|
+
:where(.document-view) :where(pre code) {
|
|
177
|
+
padding: 0;
|
|
178
|
+
background: none;
|
|
179
|
+
font-size: inherit;
|
|
180
|
+
}
|
|
181
|
+
:where(.document-view) :where(blockquote) {
|
|
182
|
+
padding: 0.75rem 1rem;
|
|
183
|
+
border-radius: var(--radius-md);
|
|
184
|
+
background: var(--color-muted);
|
|
185
|
+
color: var(--color-muted-foreground);
|
|
186
|
+
}
|
|
187
|
+
:where(.document-view) :where(blockquote > :last-child) {
|
|
188
|
+
margin-bottom: 0;
|
|
189
|
+
}
|
|
190
|
+
:where(.document-view) :where(.callout) {
|
|
191
|
+
margin: 0 0 1rem;
|
|
192
|
+
padding: 0.75rem 1rem;
|
|
193
|
+
border: 1px solid var(--color-border);
|
|
194
|
+
border-radius: var(--radius-lg);
|
|
195
|
+
background: var(--color-card);
|
|
196
|
+
}
|
|
197
|
+
:where(.document-view) :where(table) {
|
|
198
|
+
display: block;
|
|
199
|
+
max-width: 100%;
|
|
200
|
+
overflow-x: auto;
|
|
201
|
+
border-collapse: collapse;
|
|
202
|
+
font-size: 0.875rem;
|
|
203
|
+
font-variant-numeric: tabular-nums;
|
|
204
|
+
}
|
|
205
|
+
:where(.document-view) :where(th, td) {
|
|
206
|
+
padding: 0.5rem 0.75rem;
|
|
207
|
+
border-bottom: 1px solid var(--color-border);
|
|
208
|
+
text-align: left;
|
|
209
|
+
vertical-align: top;
|
|
210
|
+
}
|
|
211
|
+
:where(.document-view) :where(th) {
|
|
212
|
+
font-weight: 600;
|
|
213
|
+
color: var(--color-muted-foreground);
|
|
214
|
+
}
|
|
215
|
+
:where(.document-view) :where(hr) {
|
|
216
|
+
margin: 2rem 0;
|
|
217
|
+
border: 0;
|
|
218
|
+
border-top: 1px solid var(--color-border);
|
|
219
|
+
}
|
|
220
|
+
:where(.document-view) :where(img) {
|
|
221
|
+
max-width: 100%;
|
|
222
|
+
height: auto;
|
|
223
|
+
border-radius: var(--radius-md);
|
|
224
|
+
}
|
|
225
|
+
@media (prefers-reduced-motion: reduce) {
|
|
226
|
+
:where(.document-view) :where(a) {
|
|
227
|
+
transition: none;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Component, Snippet } from "svelte";
|
|
2
|
+
import type { DocHeading } from "./scroll-spy.svelte.js";
|
|
3
|
+
interface Props {
|
|
4
|
+
title: string;
|
|
5
|
+
/** Breadcrumb, as a reader would say it: "Marketing / Reports". */
|
|
6
|
+
section?: string;
|
|
7
|
+
/** Already worded and localised: "Updated 30 Jul 2026". */
|
|
8
|
+
updated?: string;
|
|
9
|
+
icon?: Component<{
|
|
10
|
+
class?: string;
|
|
11
|
+
weight?: "bold";
|
|
12
|
+
"aria-hidden"?: boolean | "true" | "false";
|
|
13
|
+
}>;
|
|
14
|
+
actions?: Snippet;
|
|
15
|
+
/** The body's h2s, anchored by id, for the "On this page" rail. */
|
|
16
|
+
headings?: DocHeading[];
|
|
17
|
+
contentsLabel: string;
|
|
18
|
+
/** The rendered body. Trusted HTML from the consumer's own parser, never user input. */
|
|
19
|
+
html: string;
|
|
20
|
+
/** Changes when another document is shown: the pane returns to the top and the body re-enters. */
|
|
21
|
+
resetKey: string;
|
|
22
|
+
measure?: "prose" | "wide";
|
|
23
|
+
/** Runs on the body after every swap (icons, diagrams). Returns its cleanup. */
|
|
24
|
+
enhance?: (root: HTMLElement) => void | (() => void);
|
|
25
|
+
class?: string;
|
|
26
|
+
}
|
|
27
|
+
declare const $$Component: Component<Props, {}, "">;
|
|
28
|
+
type $$Component = ReturnType<typeof $$Component>;
|
|
29
|
+
export default $$Component;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* A surface supplies a title and a body, and never lays a document out itself.
|
|
4
4
|
*/
|
|
5
5
|
export { default as DocPage } from "./doc-page.svelte";
|
|
6
|
+
export { default as DocumentView } from "./document-view.svelte";
|
|
6
7
|
export { default as OnThisPage } from "./on-this-page.svelte";
|
|
7
8
|
export {
|
|
8
9
|
ScrollSpy,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
5
|
+
import { FLAG_CODES, flagCode } from "./codes.js";
|
|
6
|
+
|
|
7
|
+
const dir = join(dirname(fileURLToPath(import.meta.url)), "flags");
|
|
8
|
+
|
|
9
|
+
describe("flag codes", () => {
|
|
10
|
+
it("lists exactly the SVG files shipped", () => {
|
|
11
|
+
const files = readdirSync(dir)
|
|
12
|
+
.filter((name) => name.endsWith(".svg"))
|
|
13
|
+
.map((name) => name.slice(0, -4));
|
|
14
|
+
expect(FLAG_CODES.toSorted()).toEqual(files.toSorted());
|
|
15
|
+
expect(FLAG_CODES).toHaveLength(250);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("knows Kosovo, which ISO 3166-1 leaves user-assigned", () => {
|
|
19
|
+
expect(flagCode("xk")).toBe("XK");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("takes any case and refuses what it has no file for", () => {
|
|
23
|
+
expect(flagCode("jp")).toBe("JP");
|
|
24
|
+
expect(flagCode(" Pl ")).toBe("PL");
|
|
25
|
+
expect(flagCode("")).toBeNull();
|
|
26
|
+
expect(flagCode(undefined)).toBeNull();
|
|
27
|
+
expect(flagCode("ZZ")).toBeNull();
|
|
28
|
+
expect(flagCode("../FR")).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Where the host serves the flag files. `flagAssets()` from `@heyramzi/ui/vite` serves them there. */
|
|
2
|
+
export const FLAG_BASE = "/flags";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The 249 ISO 3166-1 alpha-2 codes plus XK (Kosovo, user-assigned but in common use), each with
|
|
6
|
+
* a file in `./flags`, from country-flag-icons 1.6.20 (MIT).
|
|
7
|
+
*/
|
|
8
|
+
export const FLAG_CODES: readonly string[] = (
|
|
9
|
+
"AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR " +
|
|
10
|
+
"BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ " +
|
|
11
|
+
"EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW " +
|
|
12
|
+
"GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY " +
|
|
13
|
+
"KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV " +
|
|
14
|
+
"MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY " +
|
|
15
|
+
"QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG " +
|
|
16
|
+
"TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS XK YE YT ZA ZM ZW"
|
|
17
|
+
).split(" ");
|
|
18
|
+
|
|
19
|
+
const KNOWN = new Set(FLAG_CODES);
|
|
20
|
+
|
|
21
|
+
/** The code in capitals when a flag exists for it, else null. */
|
|
22
|
+
export function flagCode(code: string | null | undefined): string | null {
|
|
23
|
+
const upper = code?.trim().toUpperCase() ?? "";
|
|
24
|
+
return KNOWN.has(upper) ? upper : null;
|
|
25
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// =========================
|
|
3
|
+
// IMPORTS
|
|
4
|
+
// =========================
|
|
5
|
+
import { cn } from "../../lib/utils.js";
|
|
6
|
+
import { FLAG_BASE, flagCode } from "./codes.js";
|
|
7
|
+
|
|
8
|
+
// =========================
|
|
9
|
+
// PROPS
|
|
10
|
+
// =========================
|
|
11
|
+
interface Props {
|
|
12
|
+
/** ISO 3166-1 alpha-2 code, or XK for Kosovo, any case. Unknown or empty renders nothing. */
|
|
13
|
+
code: string | null | undefined;
|
|
14
|
+
/** Names the flag for a reader. Leave it out when the country name sits beside it. */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Where the host serves `<CODE>.svg`. */
|
|
17
|
+
base?: string;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let { code, label, base = FLAG_BASE, class: className }: Props = $props();
|
|
22
|
+
|
|
23
|
+
// =========================
|
|
24
|
+
// STATE
|
|
25
|
+
// =========================
|
|
26
|
+
let file = $derived(flagCode(code));
|
|
27
|
+
// A file the host doesn't serve hides the flag instead of showing a broken image.
|
|
28
|
+
let failedFor = $state<string | null>(null);
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<!-- =========================== -->
|
|
32
|
+
<!-- MARKUP -->
|
|
33
|
+
<!-- =========================== -->
|
|
34
|
+
{#if file && failedFor !== file}
|
|
35
|
+
<img
|
|
36
|
+
src="{base}/{file}.svg"
|
|
37
|
+
alt={label ?? ""}
|
|
38
|
+
aria-hidden={label ? undefined : "true"}
|
|
39
|
+
width="3"
|
|
40
|
+
height="2"
|
|
41
|
+
loading="lazy"
|
|
42
|
+
decoding="async"
|
|
43
|
+
draggable="false"
|
|
44
|
+
onerror={() => (failedFor = file)}
|
|
45
|
+
class={cn(
|
|
46
|
+
"inline-block aspect-[3/2] h-[1em] w-auto max-w-none shrink-0 object-cover align-[-0.125em]",
|
|
47
|
+
"rounded-[min(var(--radius-sm),0.15em)] outline outline-1 -outline-offset-1 outline-black/10 dark:outline-white/15",
|
|
48
|
+
className,
|
|
49
|
+
)}
|
|
50
|
+
/>
|
|
51
|
+
{/if}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** ISO 3166-1 alpha-2 code, or XK for Kosovo, any case. Unknown or empty renders nothing. */
|
|
3
|
+
code: string | null | undefined;
|
|
4
|
+
/** Names the flag for a reader. Leave it out when the country name sits beside it. */
|
|
5
|
+
label?: string;
|
|
6
|
+
/** Where the host serves `<CODE>.svg`. */
|
|
7
|
+
base?: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const $$Component: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type $$Component = ReturnType<typeof $$Component>;
|
|
12
|
+
export default $$Component;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#fedd00" d="M0 0h513v342H0z"/><path fill="#10069f" d="M0 0h171v342H0z"/><path fill="#d50032" d="M342 0h171v342H342z"/><path fill="#D80027" d="M198.9 113.6H256v64.8h-57.1z"/><g stroke="#D80027" stroke-width="7"><path d="M267.6 113.6v64.7M284.2 113.6v64.7M300.8 113.6v64.7"/></g><g stroke="#D80027" stroke-width="5"><path d="M247.4 178.4v64.7M234.1 178.4v64.7M220.8 178.5v53.9M207.1 178.5v47.1"/></g><path fill="#FFDA44" d="m199.9 146 27.6-32.4 28.5 32.3-28.1 32.4z"/><path fill="#BC8B00" d="M182.2 95.9v92.2c0 34.3 27.3 54.2 48.6 64.5-.8 1.4 25.1 8.3 25.1 8.3s25.9-6.9 25.1-8.3c21.4-10.3 48.6-30.1 48.6-64.5V95.9H182.2zm129.9 92.2c0 16.9-10 29.4-32.8 43.9-8.6 5.5-17.5 9-23.3 11-5.8-2-14.7-5.5-23.3-11-22.8-14.5-32.8-28-32.8-43.9v-74.5h112.2v74.5z"/><path fill="#D80027" d="M264.4 188.9h29.5v8.6h-29.5zM264.4 206.5h29.5v8.6h-29.5z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><path fill="#00843d" d="M0 0h513v114H0z"/><path d="M0 228h513v114H0z"/><path fill="#c8102e" d="M0 0h171v342H0z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#D32011" d="M0 0h513v342H0z"/><path fill="#007A36" d="M342 0h171v342H342z"/><path d="M0 0h171v342H0z"/><path fill="#FFF" d="M256.5 131.3c-17.5 0-31.8 14.3-31.8 31.8v47.7h63.6v-47.7c0-17.7-14.3-31.8-31.8-31.8z"/><path fill="#FFF" d="M256.5 93c-43.1 0-78 34.9-78 78s34.9 77.9 78 77.9 78-34.9 78-78-34.9-77.9-78-77.9zm0 142.4c-35.6 0-64.5-28.9-64.5-64.5s28.9-64.5 64.5-64.5 64.5 29 64.5 64.6c0 35.6-28.9 64.4-64.5 64.4z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path d="M0 0h513v342H0z"/><path fill="#0072c6" d="M88.8 136.5c-2.2 12.9-3.4 26.2-3.4 39.8 0 13.6 1.2 27 3.4 39.8L256 228.3l167.2-12.1c2.2-12.9 3.4-26.2 3.4-39.8s-1.2-27-3.4-39.8"/><path fill="#FFF" d="M423.2 219H88.8c15.8 69.8 84.7 122.3 167.2 122.3S407.4 288.8 423.2 219z"/><path fill="#FFDA44" d="M365.9 136.5H146.1l44.9-21.1-23.9-43.5 48.8 9.4L222 32l34 36.2L290 32l6.1 49.3 48.8-9.4-23.9 43.5z"/><g fill="#ce1126"><path d="M256.5 342 0 0v342z"/><path d="M513 342V0L256 342z"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#012169" d="M0 0h513v342H0z"/><path fill="#FFF" d="M454.8 265.38c7.94-10.93 13.24-24.27 13.24-40.42V104.89a59.754 59.754 0 0 1-36.02 12.01c-19.64 0-37.07-9.43-48.03-24.01-10.95 14.58-28.39 24.01-48.03 24.01-13.52 0-25.99-4.47-36.02-12v120.06c0 16.16 5.3 29.5 13.24 40.42H454.8z"/><path fill="#9cf" d="M310.23 260.98C332.65 296.96 384 309 384 309s51.35-12.04 73.77-48.02H310.23z"/><g fill="#f90"><path d="M396.66 172.21c.32 1.42 8.72 10.17 17.56 20.15 1.39 1.57-8.74 12.63-6.91 12.59 4.75-.12 19.27-17.26 19.34 6.65.05 15.94-30 27.51-30 27.51h17.47l.65 14.96s4.97-13.34 7.59-16.57c8.1-9.97 20.18-30.07 34.59-27.1 14.41 2.97-13.66-13.92-13.66-13.92s-5.2-15.19-16.63-16.97c-10.06-1.57-14.29-2.51-26.64-7.3-1.68-.65-3.88-2.32-3.36 0z"/><path d="M359.69 198.69c1.07-.99 4.46-12.63 8.69-25.28.67-1.99 15.31 1.27 14.35-.3-2.47-4.06-24.58-8.08-3.9-20.08 13.79-8 38.82 12.27 38.82 12.27l-8.72-15.14 12.64-8.03s-14.05 2.35-18.14 1.69c-12.68-2.04-36.13-2.48-40.75-16.45-4.62-13.97-5.25 18.79-5.25 18.79s-10.56 12.08-6.4 22.88c3.66 9.5 4.96 13.64 6.97 26.73.29 1.79-.06 4.53 1.69 2.92z"/><path d="M395.67 219.87c-1.39-.43-13.16 2.49-26.22 5.18-2.06.42-6.59-13.88-7.47-12.26-2.27 4.17 5.36 25.31-15.41 13.45-13.84-7.91-8.89-39.73-8.89-39.73l-8.71 15.14-13.29-6.9s9.09 10.96 10.57 14.84c4.6 11.99 16 32.49 6.24 43.49-9.76 11.01 18.88-4.9 18.88-4.9s15.76 3.07 23-5.96c6.38-7.94 9.3-11.14 19.61-19.45 1.43-1.12 3.97-2.19 1.69-2.9z"/></g><path fill="#012169" d="M0 0h256.5v171H0z"/><g fill="#FFF"><path d="M256.5 0v30.6l-45.3 25.2h45.3v59.4h-59.2l59.2 32.9V171h-26.7l-73.7-40.9V171h-55.7v-48.7L12.8 171H0v-30.6l45.3-25.2H0V55.8h59.2L0 22.9V0h26.7l73.7 40.9V0h55.7v48.7L243.7 0z"/><path d="M156.1 115.2 256.5 171v-15.8l-72-40zM100.4 55.8 0 0v15.8l72 40z"/></g><g fill="#D80027"><path d="M144.3 0h-32.1v69.5H0v32h112.2V171h32.1v-69.5h112.2v-32H144.3z"/><path d="M156.1 115.2 256.5 171v-15.8l-72-40zM72 115.2l-72 40V171l100.4-55.8zM100.4 55.8 0 0v15.8l72 40zM184.5 55.8l72-40V0L156.1 55.8z"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 60"><path fill="#ED2024" d="M0 0h90v60H0z"/><path fill="#212121" d="M32.5 12.2v16l7.1 2.5-5 4.9 2.4 3 5.3-5.5 1.2 2.3-3 4.9 3.6 5.6-1.8 2.6 2.8 3.6 2.7-3.7-1.5-2.6 3-5.8-2.7-4.6 1.2-2.3 5.2 5.5 2.5-2.9-5.2-5.1 7.2-2.6V12.2l-5.2 1.9-.1 3.8-3.3.3v-2.6l1.7-2.3 5.6-2.2-2.3-.4 1.4-1.2.9.4-.8-1.4L54 9l-1-1.1L47.5 9l1.4 1.1-3.8 4.9-3.8-4.8 1.3-1.1-5-1.2L36.2 9l-1.5-.4-.8 1.4.9-.6 1.6 1.1-2.3.5 5.5 2 1.6 2.3v2.9l-3.3-.3v-3.6z"/><g fill="#212121"><path d="M26.2 25h6.4v2.5h-6.4zM26.2 20.9h6.4v2.7h-6.4zM26.2 16.7h6.4v2.6h-6.4zM26.2 12.7h6.4v2.5h-6.4z"/></g><g fill="#212121"><path d="M57.4 25h6.4v2.5h-6.4zM57.4 20.9h6.4v2.7h-6.4zM57.4 16.7h6.4v2.6h-6.4zM57.4 12.7h6.4v2.5h-6.4z"/></g><path fill="#212121" d="M53.4 36.1h6.4V38h-6.4zM30.2 36.1h6.4V38h-6.4z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#0052B4" d="M0 0h513v342H0z"/><path fill="#D80027" d="M0 0h513v114H0z"/><path fill="#FF9811" d="M0 228h513v114H0z"/></svg>
|