@7pmlabs/design-system 2.0.1 → 2.0.3

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.
@@ -30,31 +30,36 @@ var f = [
30
30
  },
31
31
  setup(t) {
32
32
  u((e) => ({
33
- v46b8c47b: n.value.width,
34
- v7690d112: n.value.height,
35
- v3d749fa1: n.value.transform,
36
- v75dd342e: n.value.fill
33
+ v980e8a80: o.value.width,
34
+ v9d60b926: o.value.height,
35
+ fcd42934: o.value.transform,
36
+ v3752e149: o.value.fill
37
37
  }));
38
- let n = i(() => ({
38
+ let n = "/_design-system/icons".replace(/\/{2,}/g, "/"), o = i(() => ({
39
39
  width: t.width || `${r[t.size]}rem`,
40
40
  height: t.height || `${r[t.size]}rem`,
41
41
  transform: `rotate(${t.rotate}deg)`,
42
42
  fill: ["currentColor", ...Object.values(e)].includes(t.color) ? void 0 : t.color
43
- })), o = i(() => t.brand ? "brands" : t.variant), p = l("");
44
- return d(() => [t.icon, o.value], async () => {
45
- p.value = "";
46
- let e = `/node_modules/@7pmlabs/design-system/dist/assets/icons/${o.value}/${t.icon}.svg`;
43
+ })), p = i(() => t.brand ? "brands" : t.variant), m = Object.freeze({ __BICON_STATIC_ICONS_PLACEHOLDER__: "" }), h = l("");
44
+ return d(() => [t.icon, p.value], async () => {
45
+ let e = `${p.value}/${t.icon}`, r = m[e];
46
+ if (r !== void 0) {
47
+ h.value = r;
48
+ return;
49
+ }
50
+ h.value = "";
51
+ let i = `${n}/${e}.svg`;
47
52
  try {
48
- let n = await fetch(e);
49
- if (!n.ok) {
50
- console.warn(`[BIcon] Could not load icon '${t.icon}' from '${e}' (HTTP ${n.status})`);
53
+ let e = await fetch(i);
54
+ if (!e.ok) {
55
+ console.warn(`[BIcon] Could not load icon '${t.icon}' from '${i}' (HTTP ${e.status})`);
51
56
  return;
52
57
  }
53
- p.value = await n.text();
58
+ h.value = await e.text();
54
59
  } catch {
55
- console.warn(`[BIcon] Could not load icon '${t.icon}' from '${e}'`);
60
+ console.warn(`[BIcon] Could not load icon '${t.icon}' from '${i}'`);
56
61
  }
57
- }, { immediate: !0 }), (e, r) => (c(), a("span", {
62
+ }, { immediate: !0 }), (e, n) => (c(), a("span", {
58
63
  class: s(["b-icon", [{
59
64
  "b:fill-current": t.color === "currentColor",
60
65
  "b:fill-primary": t.color === "primary",
@@ -63,12 +68,12 @@ var f = [
63
68
  "b:fill-failure": t.color === "failure",
64
69
  "b:fill-warning": t.color === "warning",
65
70
  "b:fill-info": t.color === "info",
66
- "b-icon--color": !!n.value.fill
71
+ "b-icon--color": !!o.value.fill
67
72
  }]]),
68
73
  "aria-hidden": !!t.decorative || void 0,
69
74
  "aria-label": t.ariaLabel,
70
75
  "aria-labelledby": t.ariaLabelledby,
71
- innerHTML: p.value
76
+ innerHTML: h.value
72
77
  }, null, 10, f));
73
78
  }
74
79
  });
@@ -1 +1 @@
1
- {"version":3,"file":"design-system25.js","names":[],"sources":["../src/components/BIcon/BIcon.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { BIconBrandName, BIconName } from '@/components/BIcon/BIconEnum.ts';\nimport { BIconSizeMap } from '@/constants';\nimport { BCommonColor, BIconSize, BIconVariant } from '@/types';\nimport { computed, ref, watch } from 'vue';\n\nconst {\n icon,\n variant = BIconVariant.Regular,\n size = BIconSize.Medium,\n decorative = true,\n rotate = 0,\n brand = false,\n color = 'currentColor',\n width,\n height,\n} = defineProps<{\n /**\n * Icon name, should match the file name in the assets/icons folder\n * @example 'check', 'cross', 'arrow-right'\n */\n icon: `${BIconName}` | `${BIconBrandName}`;\n /**\n * Predefined icon variant, such as 'regular', 'solid', 'light', etc.\n * @default 'regular'\n */\n variant?: `${BIconVariant}`;\n /**\n * Predefined icon sizes\n * @default 'medium'\n */\n size?: `${BIconSize}`;\n /**\n * Custom color for the icon, can be a CSS color value or a theme color\n * @example 'currentColor', 'primary', 'secondary', '#808080', 'rgb(128, 128, 128)', 'hsl(0, 0%, 50%)'\n */\n color?: string | 'currentColor' | `${BCommonColor}`;\n /**\n * Custom width\n * @example '2rem', '24px', '1.5em'\n */\n width?: string;\n /**\n * Custom height\n * @example '2rem', '24px', '1.5em'\n */\n height?: string;\n /**\n * Whether icon is decorative (sets aria-hidden)\n * @default true\n */\n decorative?: boolean;\n /**\n * Accessible label for meaningful icons\n */\n ariaLabel?: string;\n /**\n * ID of element that labels this icon\n */\n ariaLabelledby?: string;\n /**\n * Icon rotation in degrees\n * @default 0\n */\n rotate?: number;\n /**\n * Whether the icon is a brand icon\n */\n brand?: boolean;\n}>();\n\nconst ICONS_BASE_URL = import.meta.env.DEV\n ? '/src/assets/icons'\n : `/node_modules/${__PACKAGE_NAME__}/dist/assets/icons`;\n\nconst svgStyle = computed(() => ({\n width: width || `${BIconSizeMap[size]}rem`,\n height: height || `${BIconSizeMap[size]}rem`,\n transform: `rotate(${rotate}deg)`,\n fill: ['currentColor', ...Object.values(BCommonColor)].includes(color) ? undefined : color,\n}));\n\nconst iconFolder = computed(() => (brand ? 'brands' : variant));\n\n/**\n * SVG markup fetched at runtime from the static assets folder.\n * No dynamic import() - icons are NOT bundled as JS chunks.\n * They are served as plain .svg files copied to dist/assets/icons by viteStaticCopy.\n */\nconst svgMarkup = ref<string>('');\n\nconst loadIcon = async () => {\n svgMarkup.value = '';\n const url = `${ICONS_BASE_URL}/${iconFolder.value}/${icon}.svg`;\n try {\n const res = await fetch(url);\n if (!res.ok) {\n console.warn(`[BIcon] Could not load icon '${icon}' from '${url}' (HTTP ${res.status})`);\n return;\n }\n svgMarkup.value = await res.text();\n } catch {\n console.warn(`[BIcon] Could not load icon '${icon}' from '${url}'`);\n }\n};\n\nwatch(() => [icon, iconFolder.value], loadIcon, { immediate: true });\n</script>\n\n<template>\n <!-- v-html renders the raw SVG markup inline so fill/color CSS works normally -->\n <span\n class=\"b-icon\"\n :class=\"[\n {\n 'b:fill-current': color === 'currentColor',\n 'b:fill-primary': color === 'primary',\n 'b:fill-secondary': color === 'secondary',\n 'b:fill-success': color === 'success',\n 'b:fill-failure': color === 'failure',\n 'b:fill-warning': color === 'warning',\n 'b:fill-info': color === 'info',\n 'b-icon--color': !!svgStyle.fill,\n },\n ]\"\n :aria-hidden=\"!!decorative || undefined\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n v-html=\"svgMarkup\"\n />\n</template>\n\n<style scoped>\n.b-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: v-bind('svgStyle.width');\n height: v-bind('svgStyle.height');\n transform: v-bind('svgStyle.transform');\n}\n\n/* Size the inner <svg> element to fill the wrapper span */\n.b-icon :deep(svg) {\n width: 100%;\n height: 100%;\n}\n\n.b-icon--color :deep(svg) {\n fill: v-bind('svgStyle.fill');\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuEA,IAIM,IAAW,SAAgB;GAC/B,OAAO,EAAA,SAAS,GAAG,EAAa,EAAA,MAAM;GACtC,QAAQ,EAAA,UAAU,GAAG,EAAa,EAAA,MAAM;GACxC,WAAW,UAAU,EAAA,OAAO;GAC5B,MAAM,CAAC,gBAAgB,GAAG,OAAO,OAAO,EAAa,CAAC,CAAC,SAAS,EAAA,MAAM,GAAG,KAAA,IAAY,EAAA;GACtF,EAAE,EAEG,IAAa,QAAgB,EAAA,QAAQ,WAAW,EAAA,QAAS,EAOzD,IAAY,EAAY,GAAG;SAiBjC,QAAY,CAAC,EAAA,MAAM,EAAW,MAAM,EAfnB,YAAY;AAC3B,KAAU,QAAQ;GAClB,IAAM,IAAM,0DAAqB,EAAW,MAAM,GAAG,EAAA,KAAK;AAC1D,OAAI;IACF,IAAM,IAAM,MAAM,MAAM,EAAI;AAC5B,QAAI,CAAC,EAAI,IAAI;AACX,aAAQ,KAAK,gCAAgC,EAAA,KAAK,UAAU,EAAI,UAAU,EAAI,OAAO,GAAG;AACxF;;AAEF,MAAU,QAAQ,MAAM,EAAI,MAAM;WAC5B;AACN,YAAQ,KAAK,gCAAgC,EAAA,KAAK,UAAU,EAAI,GAAG;;KAIvB,EAAE,WAAW,IAAM,CAAC,kBAKlE,EAkBE,QAAA;GAjBA,OAAK,EAAA,CAAC,UAAQ,CAAA;sBAC8B,EAAA,UAAK;sBAA+C,EAAA,UAAK;wBAA4C,EAAA,UAAK;sBAA4C,EAAA,UAAK;sBAA0C,EAAA,UAAK;sBAA0C,EAAA,UAAK;mBAAuC,EAAA,UAAK;uBAAwC,EAAA,MAAS;;GAYjY,eAAW,CAAA,CAAI,EAAA,cAAc,KAAA;GAC7B,cAAY,EAAA;GACZ,mBAAiB,EAAA;GAClB,WAAQ,EAAA"}
1
+ {"version":3,"file":"design-system25.js","names":[],"sources":["../src/components/BIcon/BIcon.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { BIconBrandName, BIconName } from '@/components/BIcon/BIconEnum.ts';\nimport { BIconSizeMap } from '@/constants';\nimport { BCommonColor, BIconSize, BIconVariant } from '@/types';\nimport { computed, ref, watch } from 'vue';\n\nconst {\n icon,\n variant = BIconVariant.Regular,\n size = BIconSize.Medium,\n decorative = true,\n rotate = 0,\n brand = false,\n color = 'currentColor',\n width,\n height,\n} = defineProps<{\n /**\n * Icon name, should match the file name in the assets/icons folder\n * @example 'check', 'cross', 'arrow-right'\n */\n icon: `${BIconName}` | `${BIconBrandName}`;\n /**\n * Predefined icon variant, such as 'regular', 'solid', 'light', etc.\n * @default 'regular'\n */\n variant?: `${BIconVariant}`;\n /**\n * Predefined icon sizes\n * @default 'medium'\n */\n size?: `${BIconSize}`;\n /**\n * Custom color for the icon, can be a CSS color value or a theme color\n * @example 'currentColor', 'primary', 'secondary', '#808080', 'rgb(128, 128, 128)', 'hsl(0, 0%, 50%)'\n */\n color?: string | 'currentColor' | `${BCommonColor}`;\n /**\n * Custom width\n * @example '2rem', '24px', '1.5em'\n */\n width?: string;\n /**\n * Custom height\n * @example '2rem', '24px', '1.5em'\n */\n height?: string;\n /**\n * Whether icon is decorative (sets aria-hidden)\n * @default true\n */\n decorative?: boolean;\n /**\n * Accessible label for meaningful icons\n */\n ariaLabel?: string;\n /**\n * ID of element that labels this icon\n */\n ariaLabelledby?: string;\n /**\n * Icon rotation in degrees\n * @default 0\n */\n rotate?: number;\n /**\n * Whether the icon is a brand icon\n */\n brand?: boolean;\n}>();\n\n// Resolved at build time by the consuming bundler. For lib-mode publishes this\n// bakes to '/' (default Vite base), preserving the historical `/_design-system/icons`\n// URL. For consumers that build from source with a non-root base (e.g. Storybook\n// deployed under a project subpath), the base is honored automatically.\nconst ICONS_BASE_URL = `${import.meta.env.BASE_URL}_design-system/icons`.replace(\n /\\/{2,}/g,\n '/',\n);\n\nconst svgStyle = computed(() => ({\n width: width || `${BIconSizeMap[size]}rem`,\n height: height || `${BIconSizeMap[size]}rem`,\n transform: `rotate(${rotate}deg)`,\n fill: ['currentColor', ...Object.values(BCommonColor)].includes(color) ? undefined : color,\n}));\n\nconst iconFolder = computed(() => (brand ? 'brands' : variant));\n\n/**\n * Map of `<variant>/<icon-name>` → raw SVG markup.\n *\n * In the published library this object literal is an inert sentinel — the\n * `__BICON_STATIC_ICONS_PLACEHOLDER__` key is never read at runtime. When the\n * `@7pmlabs/design-system/vite` plugin is in the consumer's pipeline, its\n * `transform` hook replaces this exact literal in the bundled BIcon module\n * with the AST-scanned set of statically-used icons, so they render in the\n * first frame without a network roundtrip.\n *\n * Consumers without the plugin keep the inert object and fall back to the\n * runtime fetch below — the build succeeds either way.\n */\nconst STATIC_ICONS: Readonly<Record<string, string>> = Object.freeze({\n __BICON_STATIC_ICONS_PLACEHOLDER__: '',\n});\n\nconst svgMarkup = ref<string>('');\n\nconst loadIcon = async () => {\n const key = `${iconFolder.value}/${icon}`;\n const inlined = STATIC_ICONS[key];\n if (inlined !== undefined) {\n svgMarkup.value = inlined;\n return;\n }\n svgMarkup.value = '';\n const url = `${ICONS_BASE_URL}/${key}.svg`;\n try {\n const res = await fetch(url);\n if (!res.ok) {\n console.warn(`[BIcon] Could not load icon '${icon}' from '${url}' (HTTP ${res.status})`);\n return;\n }\n svgMarkup.value = await res.text();\n } catch {\n console.warn(`[BIcon] Could not load icon '${icon}' from '${url}'`);\n }\n};\n\nwatch(() => [icon, iconFolder.value], loadIcon, { immediate: true });\n</script>\n\n<template>\n <!-- v-html renders the raw SVG markup inline so fill/color CSS works normally -->\n <span\n class=\"b-icon\"\n :class=\"[\n {\n 'b:fill-current': color === 'currentColor',\n 'b:fill-primary': color === 'primary',\n 'b:fill-secondary': color === 'secondary',\n 'b:fill-success': color === 'success',\n 'b:fill-failure': color === 'failure',\n 'b:fill-warning': color === 'warning',\n 'b:fill-info': color === 'info',\n 'b-icon--color': !!svgStyle.fill,\n },\n ]\"\n :aria-hidden=\"!!decorative || undefined\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n v-html=\"svgMarkup\"\n />\n</template>\n\n<style scoped>\n.b-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: v-bind('svgStyle.width');\n height: v-bind('svgStyle.height');\n transform: v-bind('svgStyle.transform');\n}\n\n/* Size the inner <svg> element to fill the wrapper span */\n.b-icon :deep(svg) {\n width: 100%;\n height: 100%;\n}\n\n.b-icon--color :deep(svg) {\n fill: v-bind('svgStyle.fill');\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2EA,IAAM,IAAiB,wBAAkD,QACvE,WACA,IACD,EAEK,IAAW,SAAgB;GAC/B,OAAO,EAAA,SAAS,GAAG,EAAa,EAAA,MAAM;GACtC,QAAQ,EAAA,UAAU,GAAG,EAAa,EAAA,MAAM;GACxC,WAAW,UAAU,EAAA,OAAO;GAC5B,MAAM,CAAC,gBAAgB,GAAG,OAAO,OAAO,EAAa,CAAC,CAAC,SAAS,EAAA,MAAM,GAAG,KAAA,IAAY,EAAA;GACtF,EAAE,EAEG,IAAa,QAAgB,EAAA,QAAQ,WAAW,EAAA,QAAS,EAezD,IAAiD,OAAO,OAAO,EACnE,oCAAoC,IACrC,CAAC,EAEI,IAAY,EAAY,GAAG;SAuBjC,QAAY,CAAC,EAAA,MAAM,EAAW,MAAM,EArBnB,YAAY;GAC3B,IAAM,IAAM,GAAG,EAAW,MAAM,GAAG,EAAA,QAC7B,IAAU,EAAa;AAC7B,OAAI,MAAY,KAAA,GAAW;AACzB,MAAU,QAAQ;AAClB;;AAEF,KAAU,QAAQ;GAClB,IAAM,IAAM,GAAG,EAAe,GAAG,EAAI;AACrC,OAAI;IACF,IAAM,IAAM,MAAM,MAAM,EAAI;AAC5B,QAAI,CAAC,EAAI,IAAI;AACX,aAAQ,KAAK,gCAAgC,EAAA,KAAK,UAAU,EAAI,UAAU,EAAI,OAAO,GAAG;AACxF;;AAEF,MAAU,QAAQ,MAAM,EAAI,MAAM;WAC5B;AACN,YAAQ,KAAK,gCAAgC,EAAA,KAAK,UAAU,EAAI,GAAG;;KAIvB,EAAE,WAAW,IAAM,CAAC,kBAKlE,EAkBE,QAAA;GAjBA,OAAK,EAAA,CAAC,UAAQ,CAAA;sBAC8B,EAAA,UAAK;sBAA+C,EAAA,UAAK;wBAA4C,EAAA,UAAK;sBAA4C,EAAA,UAAK;sBAA0C,EAAA,UAAK;sBAA0C,EAAA,UAAK;mBAAuC,EAAA,UAAK;uBAAwC,EAAA,MAAS;;GAYjY,eAAW,CAAA,CAAI,EAAA,cAAc,KAAA;GAC7B,cAAY,EAAA;GACZ,mBAAiB,EAAA;GAClB,WAAQ,EAAA"}
@@ -2,7 +2,7 @@ import e from "./design-system14.js";
2
2
  import t from "./design-system25.js";
3
3
  /* empty css */
4
4
  //#region src/components/BIcon/BIcon.vue
5
- var n = /* @__PURE__ */ e(t, [["__scopeId", "data-v-15f63dfa"]]);
5
+ var n = /* @__PURE__ */ e(t, [["__scopeId", "data-v-adf55fbd"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
8
 
@@ -1 +1 @@
1
- {"version":3,"file":"design-system27.js","names":[],"sources":["../src/components/BIcon/BIcon.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { BIconBrandName, BIconName } from '@/components/BIcon/BIconEnum.ts';\nimport { BIconSizeMap } from '@/constants';\nimport { BCommonColor, BIconSize, BIconVariant } from '@/types';\nimport { computed, ref, watch } from 'vue';\n\nconst {\n icon,\n variant = BIconVariant.Regular,\n size = BIconSize.Medium,\n decorative = true,\n rotate = 0,\n brand = false,\n color = 'currentColor',\n width,\n height,\n} = defineProps<{\n /**\n * Icon name, should match the file name in the assets/icons folder\n * @example 'check', 'cross', 'arrow-right'\n */\n icon: `${BIconName}` | `${BIconBrandName}`;\n /**\n * Predefined icon variant, such as 'regular', 'solid', 'light', etc.\n * @default 'regular'\n */\n variant?: `${BIconVariant}`;\n /**\n * Predefined icon sizes\n * @default 'medium'\n */\n size?: `${BIconSize}`;\n /**\n * Custom color for the icon, can be a CSS color value or a theme color\n * @example 'currentColor', 'primary', 'secondary', '#808080', 'rgb(128, 128, 128)', 'hsl(0, 0%, 50%)'\n */\n color?: string | 'currentColor' | `${BCommonColor}`;\n /**\n * Custom width\n * @example '2rem', '24px', '1.5em'\n */\n width?: string;\n /**\n * Custom height\n * @example '2rem', '24px', '1.5em'\n */\n height?: string;\n /**\n * Whether icon is decorative (sets aria-hidden)\n * @default true\n */\n decorative?: boolean;\n /**\n * Accessible label for meaningful icons\n */\n ariaLabel?: string;\n /**\n * ID of element that labels this icon\n */\n ariaLabelledby?: string;\n /**\n * Icon rotation in degrees\n * @default 0\n */\n rotate?: number;\n /**\n * Whether the icon is a brand icon\n */\n brand?: boolean;\n}>();\n\nconst ICONS_BASE_URL = import.meta.env.DEV\n ? '/src/assets/icons'\n : `/node_modules/${__PACKAGE_NAME__}/dist/assets/icons`;\n\nconst svgStyle = computed(() => ({\n width: width || `${BIconSizeMap[size]}rem`,\n height: height || `${BIconSizeMap[size]}rem`,\n transform: `rotate(${rotate}deg)`,\n fill: ['currentColor', ...Object.values(BCommonColor)].includes(color) ? undefined : color,\n}));\n\nconst iconFolder = computed(() => (brand ? 'brands' : variant));\n\n/**\n * SVG markup fetched at runtime from the static assets folder.\n * No dynamic import() - icons are NOT bundled as JS chunks.\n * They are served as plain .svg files copied to dist/assets/icons by viteStaticCopy.\n */\nconst svgMarkup = ref<string>('');\n\nconst loadIcon = async () => {\n svgMarkup.value = '';\n const url = `${ICONS_BASE_URL}/${iconFolder.value}/${icon}.svg`;\n try {\n const res = await fetch(url);\n if (!res.ok) {\n console.warn(`[BIcon] Could not load icon '${icon}' from '${url}' (HTTP ${res.status})`);\n return;\n }\n svgMarkup.value = await res.text();\n } catch {\n console.warn(`[BIcon] Could not load icon '${icon}' from '${url}'`);\n }\n};\n\nwatch(() => [icon, iconFolder.value], loadIcon, { immediate: true });\n</script>\n\n<template>\n <!-- v-html renders the raw SVG markup inline so fill/color CSS works normally -->\n <span\n class=\"b-icon\"\n :class=\"[\n {\n 'b:fill-current': color === 'currentColor',\n 'b:fill-primary': color === 'primary',\n 'b:fill-secondary': color === 'secondary',\n 'b:fill-success': color === 'success',\n 'b:fill-failure': color === 'failure',\n 'b:fill-warning': color === 'warning',\n 'b:fill-info': color === 'info',\n 'b-icon--color': !!svgStyle.fill,\n },\n ]\"\n :aria-hidden=\"!!decorative || undefined\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n v-html=\"svgMarkup\"\n />\n</template>\n\n<style scoped>\n.b-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: v-bind('svgStyle.width');\n height: v-bind('svgStyle.height');\n transform: v-bind('svgStyle.transform');\n}\n\n/* Size the inner <svg> element to fill the wrapper span */\n.b-icon :deep(svg) {\n width: 100%;\n height: 100%;\n}\n\n.b-icon--color :deep(svg) {\n fill: v-bind('svgStyle.fill');\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"design-system27.js","names":[],"sources":["../src/components/BIcon/BIcon.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { BIconBrandName, BIconName } from '@/components/BIcon/BIconEnum.ts';\nimport { BIconSizeMap } from '@/constants';\nimport { BCommonColor, BIconSize, BIconVariant } from '@/types';\nimport { computed, ref, watch } from 'vue';\n\nconst {\n icon,\n variant = BIconVariant.Regular,\n size = BIconSize.Medium,\n decorative = true,\n rotate = 0,\n brand = false,\n color = 'currentColor',\n width,\n height,\n} = defineProps<{\n /**\n * Icon name, should match the file name in the assets/icons folder\n * @example 'check', 'cross', 'arrow-right'\n */\n icon: `${BIconName}` | `${BIconBrandName}`;\n /**\n * Predefined icon variant, such as 'regular', 'solid', 'light', etc.\n * @default 'regular'\n */\n variant?: `${BIconVariant}`;\n /**\n * Predefined icon sizes\n * @default 'medium'\n */\n size?: `${BIconSize}`;\n /**\n * Custom color for the icon, can be a CSS color value or a theme color\n * @example 'currentColor', 'primary', 'secondary', '#808080', 'rgb(128, 128, 128)', 'hsl(0, 0%, 50%)'\n */\n color?: string | 'currentColor' | `${BCommonColor}`;\n /**\n * Custom width\n * @example '2rem', '24px', '1.5em'\n */\n width?: string;\n /**\n * Custom height\n * @example '2rem', '24px', '1.5em'\n */\n height?: string;\n /**\n * Whether icon is decorative (sets aria-hidden)\n * @default true\n */\n decorative?: boolean;\n /**\n * Accessible label for meaningful icons\n */\n ariaLabel?: string;\n /**\n * ID of element that labels this icon\n */\n ariaLabelledby?: string;\n /**\n * Icon rotation in degrees\n * @default 0\n */\n rotate?: number;\n /**\n * Whether the icon is a brand icon\n */\n brand?: boolean;\n}>();\n\n// Resolved at build time by the consuming bundler. For lib-mode publishes this\n// bakes to '/' (default Vite base), preserving the historical `/_design-system/icons`\n// URL. For consumers that build from source with a non-root base (e.g. Storybook\n// deployed under a project subpath), the base is honored automatically.\nconst ICONS_BASE_URL = `${import.meta.env.BASE_URL}_design-system/icons`.replace(\n /\\/{2,}/g,\n '/',\n);\n\nconst svgStyle = computed(() => ({\n width: width || `${BIconSizeMap[size]}rem`,\n height: height || `${BIconSizeMap[size]}rem`,\n transform: `rotate(${rotate}deg)`,\n fill: ['currentColor', ...Object.values(BCommonColor)].includes(color) ? undefined : color,\n}));\n\nconst iconFolder = computed(() => (brand ? 'brands' : variant));\n\n/**\n * Map of `<variant>/<icon-name>` → raw SVG markup.\n *\n * In the published library this object literal is an inert sentinel — the\n * `__BICON_STATIC_ICONS_PLACEHOLDER__` key is never read at runtime. When the\n * `@7pmlabs/design-system/vite` plugin is in the consumer's pipeline, its\n * `transform` hook replaces this exact literal in the bundled BIcon module\n * with the AST-scanned set of statically-used icons, so they render in the\n * first frame without a network roundtrip.\n *\n * Consumers without the plugin keep the inert object and fall back to the\n * runtime fetch below — the build succeeds either way.\n */\nconst STATIC_ICONS: Readonly<Record<string, string>> = Object.freeze({\n __BICON_STATIC_ICONS_PLACEHOLDER__: '',\n});\n\nconst svgMarkup = ref<string>('');\n\nconst loadIcon = async () => {\n const key = `${iconFolder.value}/${icon}`;\n const inlined = STATIC_ICONS[key];\n if (inlined !== undefined) {\n svgMarkup.value = inlined;\n return;\n }\n svgMarkup.value = '';\n const url = `${ICONS_BASE_URL}/${key}.svg`;\n try {\n const res = await fetch(url);\n if (!res.ok) {\n console.warn(`[BIcon] Could not load icon '${icon}' from '${url}' (HTTP ${res.status})`);\n return;\n }\n svgMarkup.value = await res.text();\n } catch {\n console.warn(`[BIcon] Could not load icon '${icon}' from '${url}'`);\n }\n};\n\nwatch(() => [icon, iconFolder.value], loadIcon, { immediate: true });\n</script>\n\n<template>\n <!-- v-html renders the raw SVG markup inline so fill/color CSS works normally -->\n <span\n class=\"b-icon\"\n :class=\"[\n {\n 'b:fill-current': color === 'currentColor',\n 'b:fill-primary': color === 'primary',\n 'b:fill-secondary': color === 'secondary',\n 'b:fill-success': color === 'success',\n 'b:fill-failure': color === 'failure',\n 'b:fill-warning': color === 'warning',\n 'b:fill-info': color === 'info',\n 'b-icon--color': !!svgStyle.fill,\n },\n ]\"\n :aria-hidden=\"!!decorative || undefined\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n v-html=\"svgMarkup\"\n />\n</template>\n\n<style scoped>\n.b-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: v-bind('svgStyle.width');\n height: v-bind('svgStyle.height');\n transform: v-bind('svgStyle.transform');\n}\n\n/* Size the inner <svg> element to fill the wrapper span */\n.b-icon :deep(svg) {\n width: 100%;\n height: 100%;\n}\n\n.b-icon--color :deep(svg) {\n fill: v-bind('svgStyle.fill');\n}\n</style>\n"],"mappings":""}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import type { Plugin } from 'vite';
2
+ export interface DesignSystemPluginOptions {
3
+ /**
4
+ * Source directory for SVG icons.
5
+ * Defaults to `<dir-of-@7pmlabs/design-system>/dist/assets/icons`,
6
+ * resolved from the consumer's `node_modules`.
7
+ */
8
+ iconsDir?: string;
9
+ /**
10
+ * URL prefix at which icons are served (runtime fallback).
11
+ * Must match `BIcon`'s hardcoded path. Default: `/_design-system/icons`.
12
+ */
13
+ iconsRoute?: string;
14
+ /**
15
+ * Whether to copy the icon directory into `outDir` during build (runtime fallback).
16
+ * Set to `false` when running inside the design system's own dev/storybook —
17
+ * we don't want to pollute the published `dist/`.
18
+ * Default: `true`.
19
+ */
20
+ emit?: boolean;
21
+ /**
22
+ * Project-relative roots (or absolute paths) to scan for static `<BIcon>`
23
+ * usage. Default: `['src']`.
24
+ */
25
+ include?: string[];
26
+ /**
27
+ * Variant assumed when `<BIcon icon="..." />` has no explicit `variant` prop.
28
+ * Must match `BIconVariant.Regular` in the lib. Default: `'regular'`.
29
+ */
30
+ defaultVariant?: string;
31
+ /**
32
+ * Keep the runtime fetch fallback (dev middleware + build-time copy) for
33
+ * icons that can't be statically resolved (dynamic `:icon` bindings, etc.).
34
+ * Default: `true`.
35
+ */
36
+ runtimeFallback?: boolean;
37
+ }
38
+ export declare function designSystem(options?: DesignSystemPluginOptions): Plugin;
39
+ export default designSystem;
@@ -0,0 +1,338 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { copyFile, mkdir, readdir, readFile, stat } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, extname, join, resolve } from 'node:path';
5
+ const DEFAULT_ROUTE = '/_design-system/icons';
6
+ // Sentinel literal shipped in the published BIcon module. The plugin's
7
+ // transform finds this exact pattern and replaces it with a populated map of
8
+ // statically-used icons. Consumers without this plugin keep the inert literal
9
+ // and fall back to the runtime fetch path inside BIcon.
10
+ const PLACEHOLDER_KEY = '__BICON_STATIC_ICONS_PLACEHOLDER__';
11
+ const PLACEHOLDER_RE = new RegExp(`Object\\.freeze\\(\\s*\\{\\s*(?:["']?)${PLACEHOLDER_KEY}\\1?\\s*:\\s*["']["']\\s*,?\\s*\\}\\s*\\)`);
12
+ const SCAN_EXTS = new Set(['.vue', '.tsx', '.jsx', '.ts', '.js', '.mts', '.mjs']);
13
+ const SKIP_DIRS = new Set([
14
+ 'node_modules',
15
+ 'dist',
16
+ '.nuxt',
17
+ '.output',
18
+ '.git',
19
+ '.cache',
20
+ '.vite',
21
+ ]);
22
+ // Matches an opening BIcon tag (PascalCase or kebab-case). The capture is the
23
+ // inner attribute string. `[^>]*?` is non-greedy and won't handle a `>` inside
24
+ // an attribute value — acceptable, that's vanishingly rare.
25
+ const TAG_RE = /<(?:BIcon|b-icon)\b([^>]*?)\/?>/g;
26
+ function makeStaticAttrRe(name) {
27
+ // Negative lookbehind excludes `:icon=`, `v-bind:icon=`, `@icon=`, `data-icon=`, etc.
28
+ return new RegExp(`(?<![\\w:@.-])${name}\\s*=\\s*"([^"]*)"`);
29
+ }
30
+ function makeBooleanAttrRe(name) {
31
+ return new RegExp(`(?<![\\w:@.-])${name}(?=[\\s/>])`);
32
+ }
33
+ function makeDynamicAttrRe(name) {
34
+ return new RegExp(`(?:^|\\s)(?::|v-bind:)${name}\\s*=`);
35
+ }
36
+ function getStaticAttr(attrs, name) {
37
+ const m = makeStaticAttrRe(name).exec(attrs);
38
+ return m ? m[1] : undefined;
39
+ }
40
+ function hasBooleanAttr(attrs, name) {
41
+ return makeBooleanAttrRe(name).test(attrs);
42
+ }
43
+ function hasDynamicAttr(attrs, name) {
44
+ if (makeDynamicAttrRe(name).test(attrs))
45
+ return true;
46
+ if (/(?:^|\s)v-bind\s*=/.test(attrs))
47
+ return true;
48
+ return false;
49
+ }
50
+ function scanCode(code, defaultVariant) {
51
+ const keys = new Set();
52
+ TAG_RE.lastIndex = 0;
53
+ let m;
54
+ while ((m = TAG_RE.exec(code)) !== null) {
55
+ const attrs = m[1] ?? '';
56
+ if (hasDynamicAttr(attrs, 'icon'))
57
+ continue;
58
+ const icon = getStaticAttr(attrs, 'icon');
59
+ if (!icon)
60
+ continue;
61
+ let variant;
62
+ if (hasBooleanAttr(attrs, 'brand') || getStaticAttr(attrs, 'brand') === 'true') {
63
+ variant = 'brands';
64
+ }
65
+ else if (hasDynamicAttr(attrs, 'brand') || hasDynamicAttr(attrs, 'variant')) {
66
+ continue;
67
+ }
68
+ else {
69
+ variant = getStaticAttr(attrs, 'variant') || defaultVariant;
70
+ }
71
+ keys.add(`${variant}/${icon}`);
72
+ }
73
+ return keys;
74
+ }
75
+ async function* walkDir(dir) {
76
+ let entries;
77
+ try {
78
+ entries = await readdir(dir, { withFileTypes: true });
79
+ }
80
+ catch {
81
+ return;
82
+ }
83
+ for (const entry of entries) {
84
+ if (entry.name.startsWith('.'))
85
+ continue;
86
+ if (SKIP_DIRS.has(entry.name))
87
+ continue;
88
+ const full = join(dir, entry.name);
89
+ if (entry.isDirectory()) {
90
+ yield* walkDir(full);
91
+ }
92
+ else if (entry.isFile() && SCAN_EXTS.has(extname(entry.name))) {
93
+ yield full;
94
+ }
95
+ }
96
+ }
97
+ function resolveDefaultIconsDir() {
98
+ const require = createRequire(import.meta.url);
99
+ const pkgPath = require.resolve('@7pmlabs/design-system/package.json');
100
+ return join(dirname(pkgPath), 'dist/assets/icons');
101
+ }
102
+ async function copyDir(src, dest) {
103
+ await mkdir(dest, { recursive: true });
104
+ const entries = await readdir(src, { withFileTypes: true });
105
+ await Promise.all(entries.map(async (entry) => {
106
+ const s = join(src, entry.name);
107
+ const d = join(dest, entry.name);
108
+ if (entry.isDirectory())
109
+ await copyDir(s, d);
110
+ else if (entry.isFile())
111
+ await copyFile(s, d);
112
+ }));
113
+ }
114
+ function normalizeRoute(route) {
115
+ let r = route.startsWith('/') ? route : `/${route}`;
116
+ if (r.endsWith('/'))
117
+ r = r.slice(0, -1);
118
+ return r;
119
+ }
120
+ function makeIconMiddleware(iconsDir) {
121
+ return async (req, res, next) => {
122
+ if (!req.url)
123
+ return next();
124
+ const relPath = decodeURIComponent(req.url.split('?')[0]);
125
+ const filePath = resolve(iconsDir, '.' + relPath);
126
+ if (!filePath.startsWith(iconsDir)) {
127
+ res.statusCode = 403;
128
+ res.end('Forbidden');
129
+ return;
130
+ }
131
+ try {
132
+ const st = await stat(filePath);
133
+ if (!st.isFile())
134
+ return next();
135
+ res.setHeader('Content-Type', 'image/svg+xml');
136
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
137
+ createReadStream(filePath).pipe(res);
138
+ }
139
+ catch {
140
+ next();
141
+ }
142
+ };
143
+ }
144
+ export function designSystem(options = {}) {
145
+ const route = normalizeRoute(options.iconsRoute ?? DEFAULT_ROUTE);
146
+ const emit = options.emit !== false;
147
+ const defaultVariant = options.defaultVariant ?? 'regular';
148
+ const runtimeFallback = options.runtimeFallback !== false;
149
+ let resolvedIconsDir;
150
+ let viteConfig;
151
+ let server;
152
+ // Per-source-file scan results — lets us update incrementally on edits
153
+ // in dev (`handleHotUpdate`) without re-reading every file.
154
+ const fileKeys = new Map();
155
+ const allKeys = new Set();
156
+ // Cached SVG content keyed by `<variant>/<name>`. Lazily filled.
157
+ const svgCache = new Map();
158
+ // Modules that contain the placeholder literal — invalidated when the
159
+ // icon set changes so they get re-transformed with the new map.
160
+ const consumerModules = new Set();
161
+ function getIconsDir() {
162
+ if (resolvedIconsDir)
163
+ return resolvedIconsDir;
164
+ const dir = options.iconsDir ?? resolveDefaultIconsDir();
165
+ resolvedIconsDir = resolve(dir);
166
+ return resolvedIconsDir;
167
+ }
168
+ function recomputeAllKeys() {
169
+ allKeys.clear();
170
+ for (const set of fileKeys.values()) {
171
+ for (const k of set)
172
+ allKeys.add(k);
173
+ }
174
+ }
175
+ function updateFileKeys(file, keys) {
176
+ const prev = fileKeys.get(file);
177
+ const same = prev && prev.size === keys.size && [...keys].every((k) => prev.has(k));
178
+ if (same)
179
+ return false;
180
+ if (keys.size === 0)
181
+ fileKeys.delete(file);
182
+ else
183
+ fileKeys.set(file, keys);
184
+ recomputeAllKeys();
185
+ return true;
186
+ }
187
+ async function loadSvg(key) {
188
+ if (svgCache.has(key))
189
+ return svgCache.get(key);
190
+ const iconsDir = getIconsDir();
191
+ const filePath = resolve(iconsDir, key + '.svg');
192
+ if (!filePath.startsWith(iconsDir))
193
+ return undefined;
194
+ try {
195
+ const content = await readFile(filePath, 'utf-8');
196
+ svgCache.set(key, content);
197
+ return content;
198
+ }
199
+ catch {
200
+ return undefined;
201
+ }
202
+ }
203
+ async function buildStaticIconsLiteral() {
204
+ const sortedKeys = [...allKeys].sort();
205
+ const entries = [];
206
+ for (const key of sortedKeys) {
207
+ const svg = await loadSvg(key);
208
+ if (svg === undefined)
209
+ continue;
210
+ entries.push(`${JSON.stringify(key)}:${JSON.stringify(svg)}`);
211
+ }
212
+ return `Object.freeze({${entries.join(',')}})`;
213
+ }
214
+ function invalidateConsumerModules() {
215
+ if (!server)
216
+ return;
217
+ for (const id of consumerModules) {
218
+ const mod = server.moduleGraph.getModuleById(id);
219
+ if (!mod)
220
+ continue;
221
+ server.moduleGraph.invalidateModule(mod);
222
+ void server.reloadModule(mod).catch(() => {
223
+ /* swallow — happens during teardown */
224
+ });
225
+ }
226
+ }
227
+ return {
228
+ name: '@7pmlabs/design-system',
229
+ configResolved(config) {
230
+ viteConfig = config;
231
+ },
232
+ async buildStart() {
233
+ if (!viteConfig)
234
+ return;
235
+ // Library builds publish the inert sentinel — no need to scan.
236
+ if (viteConfig.build.lib)
237
+ return;
238
+ const root = viteConfig.root;
239
+ const includes = options.include ?? ['src'];
240
+ fileKeys.clear();
241
+ consumerModules.clear();
242
+ for (const inc of includes) {
243
+ const dir = resolve(root, inc);
244
+ try {
245
+ const st = await stat(dir);
246
+ if (!st.isDirectory())
247
+ continue;
248
+ }
249
+ catch {
250
+ continue;
251
+ }
252
+ for await (const file of walkDir(dir)) {
253
+ try {
254
+ const code = await readFile(file, 'utf-8');
255
+ const keys = scanCode(code, defaultVariant);
256
+ if (keys.size > 0)
257
+ fileKeys.set(file, keys);
258
+ }
259
+ catch {
260
+ /* ignore unreadable */
261
+ }
262
+ }
263
+ }
264
+ recomputeAllKeys();
265
+ },
266
+ async transform(code, id) {
267
+ // Skip when this plugin is running inside a library build (the lib's
268
+ // own dev/build pipeline). We must NOT bake icons into the published
269
+ // BIcon module — consumers' copies of this plugin would then find the
270
+ // sentinel already replaced and silently fail to inline their own
271
+ // statically-used icons. The lib publishes with the inert literal
272
+ // intact; storybook/dev just pay the runtime-fetch tax during preview.
273
+ if (viteConfig?.build.lib)
274
+ return null;
275
+ // Inject the populated map into any module that carries the sentinel.
276
+ // In practice that's exactly the published BIcon module from the lib,
277
+ // but we don't hard-code its path — any module the lib ships in the
278
+ // future with the same sentinel will be patched the same way.
279
+ if (!code.includes(PLACEHOLDER_KEY))
280
+ return null;
281
+ consumerModules.add(id);
282
+ const replacement = await buildStaticIconsLiteral();
283
+ const next = code.replace(PLACEHOLDER_RE, replacement);
284
+ if (next === code)
285
+ return null;
286
+ return { code: next, map: null };
287
+ },
288
+ // Dev-only: when a consumer source file changes, re-scan it from disk
289
+ // (NOT from a `transform`-time `code` param — vite-plugin-vue would have
290
+ // compiled the template away by then) and invalidate the BIcon module
291
+ // so its sentinel is replaced with the updated map.
292
+ async handleHotUpdate({ file }) {
293
+ const cleanId = file.split('?')[0];
294
+ if (!SCAN_EXTS.has(extname(cleanId)))
295
+ return;
296
+ const norm = cleanId.replace(/\\/g, '/');
297
+ if (norm.includes('/node_modules/') ||
298
+ norm.includes('/.nuxt/') ||
299
+ norm.includes('/.output/')) {
300
+ return;
301
+ }
302
+ let code;
303
+ try {
304
+ code = await readFile(cleanId, 'utf-8');
305
+ }
306
+ catch {
307
+ return;
308
+ }
309
+ const keys = scanCode(code, defaultVariant);
310
+ if (updateFileKeys(cleanId, keys))
311
+ invalidateConsumerModules();
312
+ },
313
+ configureServer(s) {
314
+ server = s;
315
+ if (!runtimeFallback)
316
+ return;
317
+ const handler = makeIconMiddleware(getIconsDir());
318
+ s.middlewares.use(route, handler);
319
+ },
320
+ configurePreviewServer(s) {
321
+ if (!runtimeFallback)
322
+ return;
323
+ const handler = makeIconMiddleware(getIconsDir());
324
+ s.middlewares.use(route, handler);
325
+ },
326
+ async closeBundle() {
327
+ if (!emit || !runtimeFallback)
328
+ return;
329
+ if (!viteConfig || viteConfig.command !== 'build')
330
+ return;
331
+ const iconsDir = getIconsDir();
332
+ const outDir = resolve(viteConfig.root, viteConfig.build.outDir);
333
+ const dest = join(outDir, route);
334
+ await copyDir(iconsDir, dest);
335
+ },
336
+ };
337
+ }
338
+ export default designSystem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7pmlabs/design-system",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "type": "module",
5
5
  "homepage": "https://github.com/ngphanducthinh/design-system",
6
6
  "repository": {
@@ -13,7 +13,8 @@
13
13
  "scripts": {
14
14
  "dev": "vite",
15
15
  "build": "run-s lint-all build:lib",
16
- "build:lib": "vite build && npm run build:types",
16
+ "build:lib": "vite build && npm run build:vite-plugin && npm run build:types",
17
+ "build:vite-plugin": "tsc -p tsconfig.vite-plugin.json",
17
18
  "build:types": "vue-tsc --project tsconfig.types.json --declaration --emitDeclarationOnly --outDir dist/types",
18
19
  "lint-all": "run-p lint type-check",
19
20
  "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
@@ -103,10 +104,15 @@
103
104
  "import": "./dist/design-system.js",
104
105
  "types": "./dist/types/index.d.ts"
105
106
  },
107
+ "./vite": {
108
+ "import": "./dist/vite/index.js",
109
+ "types": "./dist/types/vite/index.d.ts"
110
+ },
106
111
  "./style.css": {
107
112
  "import": "./dist/design-system.css",
108
113
  "require": "./dist/design-system.css"
109
- }
114
+ },
115
+ "./package.json": "./package.json"
110
116
  },
111
117
  "sideEffects": [
112
118
  "dist/design-system.css",