@c8y/bootstrap 1024.5.1 → 1024.8.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.
@@ -0,0 +1,50 @@
1
+ /** Context path of the tenant-owned hosted app that stores icon definitions. */
2
+ export declare const ICON_OPTIONS_CONTEXT_PATH = "public-icon-options";
3
+ /** Filename of the icon manifest inside the hosted app. */
4
+ export declare const ICON_OPTIONS_FILE = "icons.json";
5
+ export type CustomIconDefinition = {
6
+ /** Unique name used as CSS class suffix. E.g. 'my-icon' → 'dlt-c8y-icon-my-icon' or 'c8y-icon-my-icon' */
7
+ name: string;
8
+ /** 'dlt' = single-color (dlt-c8y-icon-*), 'c8y' = duocolor (c8y-icon-*) */
9
+ family: IconFamily;
10
+ /** Display group in the icon selector */
11
+ category: string;
12
+ /** Indicates whether a secondary SVG file exists; used for URL-reference CSS without loading the file */
13
+ hasSecondary?: boolean;
14
+ /** When true, this icon replaces a built-in */
15
+ isOverride: boolean;
16
+ /** Name of the built-in icon being replaced */
17
+ overriddenIconName?: string;
18
+ /** Transient in-memory SVG content (never persisted to icons.json). When present,
19
+ * the CSS embeds it as a data URI — renders immediately, no dependency on the
20
+ * hosted app having propagated the file yet. */
21
+ svgContentPrimary?: string;
22
+ /** Transient in-memory light-layer SVG content — see svgContentPrimary. */
23
+ svgContentSecondary?: string;
24
+ };
25
+ export type CustomIconSet = {
26
+ icons: Array<CustomIconDefinition>;
27
+ lastModified?: string;
28
+ };
29
+ /** Determines CSS rendering strategy and class prefix for a custom icon. */
30
+ export type IconFamily = 'dlt' | 'c8y';
31
+ type FetchFn = (url: string, options?: RequestInit) => Promise<Pick<Response, 'status' | 'json'>>;
32
+ export declare function getIconManifestPath(): string;
33
+ export declare function getIconSet(fetchFn?: FetchFn): Promise<CustomIconSet>;
34
+ /**
35
+ * Fetches custom icons from the tenant's hosted app and injects their CSS into
36
+ * the document before Angular starts — ensures icons render correctly on first paint.
37
+ * Called in parallel during bootstrap alongside branding and options loading.
38
+ *
39
+ * CSS is embedded in icons.json as a pre-built data-URI style string.
40
+ * C8Y hosted apps only reliably serve .json files — separate CSS/SVG files return 404.
41
+ */
42
+ export declare function loadCustomIconStyles(fetchFn?: FetchFn): Promise<void>;
43
+ /** Builds and injects the CSS for the given icon set. The style tag is always
44
+ * written — an empty set clears previously injected rules, so removed overrides
45
+ * revert to the built-in rendering immediately. */
46
+ export declare function applyCustomIconStyles(set: CustomIconSet): void;
47
+ export declare function convertRelativePathToAbsolutePath(relativePath: string): string;
48
+ export declare function getIconFileName(icon: CustomIconDefinition, isSecondary?: boolean): string;
49
+ export declare function buildCssForCustomIcon(icon: CustomIconDefinition, lastModified?: string): string;
50
+ export {};
@@ -0,0 +1,130 @@
1
+ const STYLE_TAG_ID = 'c8y-custom-icons';
2
+ /** Context path of the tenant-owned hosted app that stores icon definitions. */
3
+ export const ICON_OPTIONS_CONTEXT_PATH = 'public-icon-options';
4
+ /** Filename of the icon manifest inside the hosted app. */
5
+ export const ICON_OPTIONS_FILE = 'icons.json';
6
+ export function getIconManifestPath() {
7
+ return convertRelativePathToAbsolutePath(ICON_OPTIONS_FILE);
8
+ }
9
+ export async function getIconSet(fetchFn) {
10
+ const url = getIconManifestPath();
11
+ const response = await (fetchFn ?? fetch)(url, { cache: 'no-cache' });
12
+ if (response.status === 200) {
13
+ const set = await response.json();
14
+ return set;
15
+ }
16
+ if (response.status === 404) {
17
+ return { icons: [] };
18
+ }
19
+ throw new Error(`Failed to load custom icons: HTTP ${response.status}`);
20
+ }
21
+ /**
22
+ * Fetches custom icons from the tenant's hosted app and injects their CSS into
23
+ * the document before Angular starts — ensures icons render correctly on first paint.
24
+ * Called in parallel during bootstrap alongside branding and options loading.
25
+ *
26
+ * CSS is embedded in icons.json as a pre-built data-URI style string.
27
+ * C8Y hosted apps only reliably serve .json files — separate CSS/SVG files return 404.
28
+ */
29
+ export async function loadCustomIconStyles(fetchFn) {
30
+ try {
31
+ const set = await getIconSet(fetchFn);
32
+ applyCustomIconStyles(set);
33
+ }
34
+ catch (e) {
35
+ // Network error or malformed JSON — non-fatal, app continues without custom icon CSS
36
+ console.warn('[c8y] Failed to load custom icon styles:', e);
37
+ }
38
+ }
39
+ /** Builds and injects the CSS for the given icon set. The style tag is always
40
+ * written — an empty set clears previously injected rules, so removed overrides
41
+ * revert to the built-in rendering immediately. */
42
+ export function applyCustomIconStyles(set) {
43
+ const css = Array.isArray(set?.icons)
44
+ ? set.icons
45
+ .map(icon => buildCssForCustomIcon(icon, set.lastModified))
46
+ .filter(Boolean)
47
+ .join('\n')
48
+ : '';
49
+ injectStyleTag(css);
50
+ }
51
+ export function convertRelativePathToAbsolutePath(relativePath) {
52
+ const basePath = `/apps/public/${ICON_OPTIONS_CONTEXT_PATH}/`;
53
+ return `${basePath}${relativePath}`;
54
+ }
55
+ export function getIconFileName(icon, isSecondary = false) {
56
+ // Duocolor names already carry the 'c8y-' prefix — strip it so the family
57
+ // prefix isn't doubled in the file name (c8y-c8y-*.svg).
58
+ const name = icon.family === 'c8y' && icon.name.startsWith('c8y-') ? icon.name.slice(4) : icon.name;
59
+ return `${icon.family}-${name}-${isSecondary ? 'secondary' : 'primary'}.svg`;
60
+ }
61
+ /** Percent-encodes an SVG string for use in a CSS `url()` data URI. */
62
+ function toDataUri(svg) {
63
+ return `data:image/svg+xml,${encodeURIComponent(svg.trim().replace(/\s+/g, ' '))}`;
64
+ }
65
+ /** Source for a mask-image url(): in-memory SVG content as a data URI when available,
66
+ * otherwise the (cache-busted) hosted-app file URL. */
67
+ function iconSource(icon, isSecondary, lastModified) {
68
+ const content = isSecondary ? icon.svgContentSecondary : icon.svgContentPrimary;
69
+ if (content) {
70
+ return toDataUri(content);
71
+ }
72
+ let fileName = getIconFileName(icon, isSecondary);
73
+ if (lastModified) {
74
+ // Append lastModified as cache-buster query param to ensure updated icons are loaded after saving
75
+ fileName += `?v=${encodeURIComponent(lastModified)}`;
76
+ }
77
+ return convertRelativePathToAbsolutePath(fileName);
78
+ }
79
+ export function buildCssForCustomIcon(icon, lastModified) {
80
+ const absolutePathToPrimary = iconSource(icon, false, lastModified);
81
+ if (icon.family === 'dlt') {
82
+ const beforeBase = ` content:"";
83
+ display:inline-block;
84
+ width:1.1em;
85
+ height:1.1em;
86
+ line-height:1;
87
+ vertical-align:top;
88
+ background-color:currentColor;
89
+ mask-size:contain;
90
+ mask-repeat:no-repeat;
91
+ mask-position:center;
92
+ -webkit-mask-size:contain;
93
+ -webkit-mask-repeat:no-repeat;
94
+ -webkit-mask-position:center;`;
95
+ return `
96
+ .c8y-icon.dlt-c8y-icon-${icon.name}::before{
97
+ ${beforeBase}
98
+ mask-image:url("${absolutePathToPrimary}");
99
+ -webkit-mask-image:url("${absolutePathToPrimary}");
100
+ }`;
101
+ }
102
+ const cssName = icon.name.startsWith('c8y-') ? icon.name.slice(4) : icon.name;
103
+ const primaryCss = `
104
+ .c8y-icon.c8y-icon-${cssName}::before{
105
+ mask-image:url("${absolutePathToPrimary}");
106
+ -webkit-mask-image:url("${absolutePathToPrimary}");
107
+ }`;
108
+ if (!icon.hasSecondary) {
109
+ return primaryCss;
110
+ }
111
+ const absolutePathToSecondary = iconSource(icon, true, lastModified);
112
+ return `
113
+ ${primaryCss}
114
+
115
+ .c8y-icon.c8y-icon-${cssName}::after{
116
+ mask-image:url("${absolutePathToSecondary}");
117
+ -webkit-mask-image:url("${absolutePathToSecondary}");
118
+ --_c8y-after-bg:currentColor;
119
+ }`;
120
+ }
121
+ function injectStyleTag(css) {
122
+ let tag = document.getElementById(STYLE_TAG_ID);
123
+ if (!tag) {
124
+ tag = document.createElement('style');
125
+ tag.id = STYLE_TAG_ID;
126
+ document.head.appendChild(tag);
127
+ }
128
+ tag.textContent = css;
129
+ }
130
+ //# sourceMappingURL=custom-icon-bootstrap.js.map
package/dist/index.d.ts CHANGED
@@ -4,3 +4,4 @@ export * from './login-check/login-check';
4
4
  export * from './plugins/plugins';
5
5
  export * from './providers/providers';
6
6
  export * from './metadata/metadata';
7
+ export * from './icon-customization/custom-icon-bootstrap';
package/dist/index.js CHANGED
@@ -4,4 +4,5 @@ export * from './login-check/login-check';
4
4
  export * from './plugins/plugins';
5
5
  export * from './providers/providers';
6
6
  export * from './metadata/metadata';
7
+ export * from './icon-customization/custom-icon-bootstrap';
7
8
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,mBAAmB,CAAC;AAClC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,mBAAmB,CAAC;AAClC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4CAA4C,CAAC"}
@@ -1,8 +1,12 @@
1
1
  import { bootstrapGetRemotes, loadRemotes } from '../plugins/plugins';
2
2
  import { ensureLoggedInIfRequired } from '../login-check/login-check';
3
3
  import { loadOptions, applyOptions } from '../resolvers/options-resolver';
4
+ import { loadCustomIconStyles } from '../icon-customization/custom-icon-bootstrap';
4
5
  export async function loadMetaDataAndPerformBootstrap(loadBootstrapModule) {
5
- let options = await loadOptions();
6
+ let [options] = await Promise.all([
7
+ loadOptions(),
8
+ loadCustomIconStyles().catch(() => undefined) // non-critical, must not block bootstrap
9
+ ]);
6
10
  options = await applyOptions({
7
11
  ...options
8
12
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c8y/bootstrap",
3
- "version": "1024.5.1",
3
+ "version": "1024.8.3",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Cumulocity",
6
6
  "description": "Bootstrap layer",