@sneat/components 0.27.22 → 0.27.23

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.
@@ -1,11 +1,39 @@
1
- import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
1
+ import { ChangeDetectionStrategy, Component, computed, inject, input, signal, } from '@angular/core';
2
+ import { IonIcon } from '@ionic/angular/ion-icon';
2
3
  import { IonItem } from '@ionic/angular/ion-item';
3
- import { IonItemDivider } from '@ionic/angular/ion-item-divider';
4
4
  import { IonLabel } from '@ionic/angular/ion-label';
5
5
  import { IonNote } from '@ionic/angular/ion-note';
6
6
  import { BUILD_INFO } from '@sneat/core-public';
7
+ import { addIcons } from 'ionicons';
8
+ import { chevronDownOutline, chevronUpOutline } from 'ionicons/icons';
7
9
  import { buildInfo as placeholderBuildInfo } from './build-info';
8
10
  import * as i0 from "@angular/core";
11
+ addIcons({ chevronDownOutline, chevronUpOutline });
12
+ // The template binds the icon `name` in kebab-case ('chevron-down-outline' /
13
+ // 'chevron-up-outline'), not the camelCase import names above: ionicons
14
+ // 8's runtime `getName()` lowercases whatever `name` it's given without
15
+ // inserting dashes (so a camelCase name arrives as e.g.
16
+ // "chevrondownoutline"), while `addIcons()` only ever registers the
17
+ // original camelCase key verbatim plus its kebab-case form — never an
18
+ // all-lowercase, no-dashes form. A camelCase `[name]` binding therefore
19
+ // never resolves (silent "[Ionicons Warning]: Could not load icon...").
20
+ // Verified empirically against
21
+ // node_modules/ionicons/dist/collection/components/icon/utils.js. Mirrors
22
+ // DataTug's local sibling of this component:
23
+ // datatug-apps' libs/datatug/main/src/lib/menu/build-info/menu-build-info.component.ts.
24
+ // Collapsible footer — a single collapsed-by-default row showing the app's
25
+ // copyright line, expandable to reveal the app version, short git hash, and
26
+ // UTC build timestamp. Founder ruling 2026-09-11: the previous
27
+ // always-expanded "App version" card (an ion-item-divider + two rows, one of
28
+ // them a readonly ion-input) took too much space in a side menu — collapse
29
+ // it behind one tappable row, using Ionic's own chevron icon for the
30
+ // expand/collapse control (no emoji), mirroring the requested
31
+ // "## copyright line [chevron] / --- Version / Build" shape.
32
+ //
33
+ // No ion-input: a readonly ion-input breaks Playwright locators
34
+ // (.inputValue() needs a native form control; ion-input is a Stencil
35
+ // shadow-DOM component) — plain ion-item/ion-label/ion-note here so
36
+ // build-info-version/build-info-hash are ordinary text nodes.
9
37
  export class AppVersionComponent {
10
38
  constructor() {
11
39
  // BUILD_INFO is optional so @sneat/components keeps working, unchanged,
@@ -15,12 +43,47 @@ export class AppVersionComponent {
15
43
  // a runtime DI contract rather than a compile-time import.
16
44
  this.buildInfo = inject(BUILD_INFO, { optional: true }) ?? placeholderBuildInfo;
17
45
  this.shortGitHash = this.buildInfo.gitHash.substring(0, 7);
46
+ /**
47
+ * Name of the copyright holder shown as a link in the collapsed row.
48
+ * Defaults to Sneat.Work — the copyright holder for every Sneat app, per
49
+ * founder ruling 2026-09-11. Override only for a genuinely different
50
+ * rights holder (e.g. a white-labeled deployment).
51
+ */
52
+ this.copyrightHolder = input('Sneat.Work', /* @ts-ignore */
53
+ ...(ngDevMode ? [{ debugName: "copyrightHolder" }] : /* istanbul ignore next */ []));
54
+ /** URL the `copyrightHolder` link points to. */
55
+ this.copyrightUrl = input('https://sneat.work', /* @ts-ignore */
56
+ ...(ngDevMode ? [{ debugName: "copyrightUrl" }] : /* istanbul ignore next */ []));
57
+ /** First year of the copyright range shown in the collapsed row. */
58
+ this.startYear = input(2020, /* @ts-ignore */
59
+ ...(ngDevMode ? [{ debugName: "startYear" }] : /* istanbul ignore next */ []));
60
+ this.expanded = signal(false, /* @ts-ignore */
61
+ ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
62
+ // The copyright range must end with the build year, never a hand-committed
63
+ // one, so it can't go stale. buildTimestamp is an ISO-8601 UTC instant
64
+ // once stamped (see @sneat/build-info's README), but stays the committed
65
+ // placeholder "timestamp t0be$et" whenever this renders off an unstamped
66
+ // build — new Date(placeholder) is Invalid Date, so fall back to the
67
+ // current year rather than rendering "NaN".
68
+ this.buildYear = computed(() => {
69
+ const stamped = new Date(this.buildInfo.buildTimestamp).getUTCFullYear();
70
+ return Number.isFinite(stamped) ? stamped : new Date().getUTCFullYear();
71
+ }, /* @ts-ignore */
72
+ ...(ngDevMode ? [{ debugName: "buildYear" }] : /* istanbul ignore next */ []));
73
+ }
74
+ toggle() {
75
+ this.expanded.update((expanded) => !expanded);
76
+ }
77
+ // The copyright link must open its target without also toggling the row —
78
+ // the ion-item's own (click) handler would otherwise also fire on bubble.
79
+ onLinkClick(event) {
80
+ event.stopPropagation();
18
81
  }
19
82
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AppVersionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
20
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.3", type: AppVersionComponent, isStandalone: true, selector: "sneat-app-version", ngImport: i0, template: "<ion-item-divider color=\"light\">\n <ion-label>App version</ion-label>\n</ion-item-divider>\n<ion-item lines=\"none\">\n <ion-label>Version</ion-label>\n <ion-note slot=\"end\" data-testid=\"build-info-version\">{{\n buildInfo.version\n }}</ion-note>\n</ion-item>\n<ion-item lines=\"none\">\n <ion-label>Build</ion-label>\n <ion-note\n slot=\"end\"\n data-testid=\"build-info-hash\"\n [title]=\"buildInfo.gitHash\"\n >{{ shortGitHash }} @ {{ buildInfo.buildTimestamp }}</ion-note\n >\n</ion-item>\n", dependencies: [{ kind: "component", type: IonItemDivider, selector: "ion-item-divider", inputs: ["color", "mode", "sticky"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
83
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: AppVersionComponent, isStandalone: true, selector: "sneat-app-version", inputs: { copyrightHolder: { classPropertyName: "copyrightHolder", publicName: "copyrightHolder", isSignal: true, isRequired: false, transformFunction: null }, copyrightUrl: { classPropertyName: "copyrightUrl", publicName: "copyrightUrl", isSignal: true, isRequired: false, transformFunction: null }, startYear: { classPropertyName: "startYear", publicName: "startYear", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ion-item\n button=\"true\"\n detail=\"false\"\n lines=\"none\"\n data-testid=\"build-info-toggle\"\n [attr.aria-expanded]=\"expanded()\"\n (click)=\"toggle()\"\n>\n <ion-label>\n {{ startYear() }} - {{ buildYear() }} &copy;\n <a\n [href]=\"copyrightUrl()\"\n target=\"_blank\"\n rel=\"noopener\"\n (click)=\"onLinkClick($event)\"\n >{{ copyrightHolder() }}</a\n >\n </ion-label>\n <ion-icon\n slot=\"end\"\n data-testid=\"build-info-chevron\"\n [name]=\"expanded() ? 'chevron-up-outline' : 'chevron-down-outline'\"\n ></ion-icon>\n</ion-item>\n@if (expanded()) {\n <ion-item lines=\"none\">\n <ion-label>\n <ion-note data-testid=\"build-info-version\"\n >Version v{{ buildInfo.version }}</ion-note\n >\n </ion-label>\n </ion-item>\n <ion-item lines=\"none\">\n <ion-label>\n <ion-note data-testid=\"build-info-hash\" [title]=\"buildInfo.gitHash\"\n >Build {{ shortGitHash }} &commat;\n {{ buildInfo.buildTimestamp }}</ion-note\n >\n </ion-label>\n </ion-item>\n}\n", dependencies: [{ kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
21
84
  }
22
85
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: AppVersionComponent, decorators: [{
23
86
  type: Component,
24
- args: [{ selector: 'sneat-app-version', changeDetection: ChangeDetectionStrategy.OnPush, imports: [IonItemDivider, IonLabel, IonItem, IonNote], template: "<ion-item-divider color=\"light\">\n <ion-label>App version</ion-label>\n</ion-item-divider>\n<ion-item lines=\"none\">\n <ion-label>Version</ion-label>\n <ion-note slot=\"end\" data-testid=\"build-info-version\">{{\n buildInfo.version\n }}</ion-note>\n</ion-item>\n<ion-item lines=\"none\">\n <ion-label>Build</ion-label>\n <ion-note\n slot=\"end\"\n data-testid=\"build-info-hash\"\n [title]=\"buildInfo.gitHash\"\n >{{ shortGitHash }} @ {{ buildInfo.buildTimestamp }}</ion-note\n >\n</ion-item>\n" }]
25
- }] });
87
+ args: [{ selector: 'sneat-app-version', changeDetection: ChangeDetectionStrategy.OnPush, imports: [IonIcon, IonItem, IonLabel, IonNote], template: "<ion-item\n button=\"true\"\n detail=\"false\"\n lines=\"none\"\n data-testid=\"build-info-toggle\"\n [attr.aria-expanded]=\"expanded()\"\n (click)=\"toggle()\"\n>\n <ion-label>\n {{ startYear() }} - {{ buildYear() }} &copy;\n <a\n [href]=\"copyrightUrl()\"\n target=\"_blank\"\n rel=\"noopener\"\n (click)=\"onLinkClick($event)\"\n >{{ copyrightHolder() }}</a\n >\n </ion-label>\n <ion-icon\n slot=\"end\"\n data-testid=\"build-info-chevron\"\n [name]=\"expanded() ? 'chevron-up-outline' : 'chevron-down-outline'\"\n ></ion-icon>\n</ion-item>\n@if (expanded()) {\n <ion-item lines=\"none\">\n <ion-label>\n <ion-note data-testid=\"build-info-version\"\n >Version v{{ buildInfo.version }}</ion-note\n >\n </ion-label>\n </ion-item>\n <ion-item lines=\"none\">\n <ion-label>\n <ion-note data-testid=\"build-info-hash\" [title]=\"buildInfo.gitHash\"\n >Build {{ shortGitHash }} &commat;\n {{ buildInfo.buildTimestamp }}</ion-note\n >\n </ion-label>\n </ion-item>\n}\n" }]
88
+ }], propDecorators: { copyrightHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyrightHolder", required: false }] }], copyrightUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyrightUrl", required: false }] }], startYear: [{ type: i0.Input, args: [{ isSignal: true, alias: "startYear", required: false }] }] } });
26
89
  //# sourceMappingURL=app-version.component.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"app-version.component.js","sourceRoot":"","sources":["../../../../../../libs/components/src/lib/app-version/app-version.component.ts","../../../../../../libs/components/src/lib/app-version/app-version.component.html"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,uBAAuB,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC3E,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAc,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,SAAS,IAAI,oBAAoB,EAAE,MAAM,cAAc,CAAC;;AAQjE,MAAM,OAAO,mBAAmB;IANhC;QAOE,wEAAwE;QACxE,qEAAqE;QACrE,8DAA8D;QAC9D,0EAA0E;QAC1E,2DAA2D;QACxC,cAAS,GAC1B,MAAM,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,IAAI,oBAAoB,CAAC;QAC9C,iBAAY,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;KAC1E;8GATY,mBAAmB;kGAAnB,mBAAmB,6ECdhC,2gBAkBA,4CDNY,cAAc,kGAAE,QAAQ,6FAAE,OAAO,0NAAE,OAAO;;2FAEzC,mBAAmB;kBAN/B,SAAS;+BACE,mBAAmB,mBAEZ,uBAAuB,CAAC,MAAM,WACtC,CAAC,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC","sourcesContent":["import { Component, ChangeDetectionStrategy, inject } from '@angular/core';\nimport { IonItem } from '@ionic/angular/ion-item';\nimport { IonItemDivider } from '@ionic/angular/ion-item-divider';\nimport { IonLabel } from '@ionic/angular/ion-label';\nimport { IonNote } from '@ionic/angular/ion-note';\nimport { BUILD_INFO, IBuildInfo } from '@sneat/core-public';\nimport { buildInfo as placeholderBuildInfo } from './build-info';\n\n@Component({\n selector: 'sneat-app-version',\n templateUrl: 'app-version.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [IonItemDivider, IonLabel, IonItem, IonNote],\n})\nexport class AppVersionComponent {\n // BUILD_INFO is optional so @sneat/components keeps working, unchanged,\n // for every consumer that has not called provideBuildInfo() yet — it\n // falls back to the never-restamped placeholders committed to\n // ./build-info.ts. See @sneat/core-public's build-info.ts for why this is\n // a runtime DI contract rather than a compile-time import.\n protected readonly buildInfo: IBuildInfo =\n inject(BUILD_INFO, { optional: true }) ?? placeholderBuildInfo;\n protected readonly shortGitHash = this.buildInfo.gitHash.substring(0, 7);\n}\n","<ion-item-divider color=\"light\">\n <ion-label>App version</ion-label>\n</ion-item-divider>\n<ion-item lines=\"none\">\n <ion-label>Version</ion-label>\n <ion-note slot=\"end\" data-testid=\"build-info-version\">{{\n buildInfo.version\n }}</ion-note>\n</ion-item>\n<ion-item lines=\"none\">\n <ion-label>Build</ion-label>\n <ion-note\n slot=\"end\"\n data-testid=\"build-info-hash\"\n [title]=\"buildInfo.gitHash\"\n >{{ shortGitHash }} @ {{ buildInfo.buildTimestamp }}</ion-note\n >\n</ion-item>\n"]}
1
+ {"version":3,"file":"app-version.component.js","sourceRoot":"","sources":["../../../../../../libs/components/src/lib/app-version/app-version.component.ts","../../../../../../libs/components/src/lib/app-version/app-version.component.html"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n computed,\n inject,\n input,\n signal,\n} from '@angular/core';\nimport { IonIcon } from '@ionic/angular/ion-icon';\nimport { IonItem } from '@ionic/angular/ion-item';\nimport { IonLabel } from '@ionic/angular/ion-label';\nimport { IonNote } from '@ionic/angular/ion-note';\nimport { BUILD_INFO, IBuildInfo } from '@sneat/core-public';\nimport { addIcons } from 'ionicons';\nimport { chevronDownOutline, chevronUpOutline } from 'ionicons/icons';\nimport { buildInfo as placeholderBuildInfo } from './build-info';\n\naddIcons({ chevronDownOutline, chevronUpOutline });\n// The template binds the icon `name` in kebab-case ('chevron-down-outline' /\n// 'chevron-up-outline'), not the camelCase import names above: ionicons\n// 8's runtime `getName()` lowercases whatever `name` it's given without\n// inserting dashes (so a camelCase name arrives as e.g.\n// \"chevrondownoutline\"), while `addIcons()` only ever registers the\n// original camelCase key verbatim plus its kebab-case form — never an\n// all-lowercase, no-dashes form. A camelCase `[name]` binding therefore\n// never resolves (silent \"[Ionicons Warning]: Could not load icon...\").\n// Verified empirically against\n// node_modules/ionicons/dist/collection/components/icon/utils.js. Mirrors\n// DataTug's local sibling of this component:\n// datatug-apps' libs/datatug/main/src/lib/menu/build-info/menu-build-info.component.ts.\n\n// Collapsible footer — a single collapsed-by-default row showing the app's\n// copyright line, expandable to reveal the app version, short git hash, and\n// UTC build timestamp. Founder ruling 2026-09-11: the previous\n// always-expanded \"App version\" card (an ion-item-divider + two rows, one of\n// them a readonly ion-input) took too much space in a side menu — collapse\n// it behind one tappable row, using Ionic's own chevron icon for the\n// expand/collapse control (no emoji), mirroring the requested\n// \"## copyright line [chevron] / --- Version / Build\" shape.\n//\n// No ion-input: a readonly ion-input breaks Playwright locators\n// (.inputValue() needs a native form control; ion-input is a Stencil\n// shadow-DOM component) — plain ion-item/ion-label/ion-note here so\n// build-info-version/build-info-hash are ordinary text nodes.\n@Component({\n selector: 'sneat-app-version',\n templateUrl: 'app-version.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [IonIcon, IonItem, IonLabel, IonNote],\n})\nexport class AppVersionComponent {\n // BUILD_INFO is optional so @sneat/components keeps working, unchanged,\n // for every consumer that has not called provideBuildInfo() yet — it\n // falls back to the never-restamped placeholders committed to\n // ./build-info.ts. See @sneat/core-public's build-info.ts for why this is\n // a runtime DI contract rather than a compile-time import.\n protected readonly buildInfo: IBuildInfo =\n inject(BUILD_INFO, { optional: true }) ?? placeholderBuildInfo;\n protected readonly shortGitHash = this.buildInfo.gitHash.substring(0, 7);\n\n /**\n * Name of the copyright holder shown as a link in the collapsed row.\n * Defaults to Sneat.Work — the copyright holder for every Sneat app, per\n * founder ruling 2026-09-11. Override only for a genuinely different\n * rights holder (e.g. a white-labeled deployment).\n */\n readonly copyrightHolder = input('Sneat.Work');\n\n /** URL the `copyrightHolder` link points to. */\n readonly copyrightUrl = input('https://sneat.work');\n\n /** First year of the copyright range shown in the collapsed row. */\n readonly startYear = input(2020);\n\n protected readonly expanded = signal(false);\n\n // The copyright range must end with the build year, never a hand-committed\n // one, so it can't go stale. buildTimestamp is an ISO-8601 UTC instant\n // once stamped (see @sneat/build-info's README), but stays the committed\n // placeholder \"timestamp t0be$et\" whenever this renders off an unstamped\n // build — new Date(placeholder) is Invalid Date, so fall back to the\n // current year rather than rendering \"NaN\".\n protected readonly buildYear = computed(() => {\n const stamped = new Date(this.buildInfo.buildTimestamp).getUTCFullYear();\n return Number.isFinite(stamped) ? stamped : new Date().getUTCFullYear();\n });\n\n protected toggle(): void {\n this.expanded.update((expanded) => !expanded);\n }\n\n // The copyright link must open its target without also toggling the row —\n // the ion-item's own (click) handler would otherwise also fire on bubble.\n protected onLinkClick(event: Event): void {\n event.stopPropagation();\n }\n}\n","<ion-item\n button=\"true\"\n detail=\"false\"\n lines=\"none\"\n data-testid=\"build-info-toggle\"\n [attr.aria-expanded]=\"expanded()\"\n (click)=\"toggle()\"\n>\n <ion-label>\n {{ startYear() }} - {{ buildYear() }} &copy;\n <a\n [href]=\"copyrightUrl()\"\n target=\"_blank\"\n rel=\"noopener\"\n (click)=\"onLinkClick($event)\"\n >{{ copyrightHolder() }}</a\n >\n </ion-label>\n <ion-icon\n slot=\"end\"\n data-testid=\"build-info-chevron\"\n [name]=\"expanded() ? 'chevron-up-outline' : 'chevron-down-outline'\"\n ></ion-icon>\n</ion-item>\n@if (expanded()) {\n <ion-item lines=\"none\">\n <ion-label>\n <ion-note data-testid=\"build-info-version\"\n >Version v{{ buildInfo.version }}</ion-note\n >\n </ion-label>\n </ion-item>\n <ion-item lines=\"none\">\n <ion-label>\n <ion-note data-testid=\"build-info-hash\" [title]=\"buildInfo.gitHash\"\n >Build {{ shortGitHash }} &commat;\n {{ buildInfo.buildTimestamp }}</ion-note\n >\n </ion-label>\n </ion-item>\n}\n"]}
@@ -3,6 +3,21 @@ import * as i0 from "@angular/core";
3
3
  export declare class AppVersionComponent {
4
4
  protected readonly buildInfo: IBuildInfo;
5
5
  protected readonly shortGitHash: string;
6
+ /**
7
+ * Name of the copyright holder shown as a link in the collapsed row.
8
+ * Defaults to Sneat.Work — the copyright holder for every Sneat app, per
9
+ * founder ruling 2026-09-11. Override only for a genuinely different
10
+ * rights holder (e.g. a white-labeled deployment).
11
+ */
12
+ readonly copyrightHolder: import("@angular/core").InputSignal<string>;
13
+ /** URL the `copyrightHolder` link points to. */
14
+ readonly copyrightUrl: import("@angular/core").InputSignal<string>;
15
+ /** First year of the copyright range shown in the collapsed row. */
16
+ readonly startYear: import("@angular/core").InputSignal<number>;
17
+ protected readonly expanded: import("@angular/core").WritableSignal<boolean>;
18
+ protected readonly buildYear: import("@angular/core").Signal<number>;
19
+ protected toggle(): void;
20
+ protected onLinkClick(event: Event): void;
6
21
  static ɵfac: i0.ɵɵFactoryDeclaration<AppVersionComponent, never>;
7
- static ɵcmp: i0.ɵɵComponentDeclaration<AppVersionComponent, "sneat-app-version", never, {}, {}, never, never, true, never>;
22
+ static ɵcmp: i0.ɵɵComponentDeclaration<AppVersionComponent, "sneat-app-version", never, { "copyrightHolder": { "alias": "copyrightHolder"; "required": false; "isSignal": true; }; "copyrightUrl": { "alias": "copyrightUrl"; "required": false; "isSignal": true; }; "startYear": { "alias": "startYear"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
8
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sneat/components",
3
- "version": "0.27.22",
3
+ "version": "0.27.23",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -24,6 +24,7 @@
24
24
  "@sneat/core-public": "^0.27.0"
25
25
  },
26
26
  "dependencies": {
27
+ "ionicons": "^8.0.13",
27
28
  "tslib": "2.8.1"
28
29
  },
29
30
  "module": "esm2022/sneat-components.js",