fold-ng 0.3.0 → 0.4.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/CHANGELOG.md +67 -1
- package/README.md +127 -50
- package/fesm2022/fold-ng.mjs +202 -51
- package/fesm2022/fold-ng.mjs.map +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/tokens/primitives.css +19 -0
- package/tokens/scales.css +11 -0
- package/tokens/semantic.css +238 -0
- package/types/fold-ng.d.ts +108 -27
- package/types/fold-ng.d.ts.map +1 -1
package/fesm2022/fold-ng.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DOCUMENT, NgTemplateOutlet, NgClass, DatePipe } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { input, inject, ElementRef, effect, Directive, Injectable, signal, DestroyRef, model, computed, HostListener, Component, InjectionToken, Service, booleanAttribute, numberAttribute, ViewEncapsulation, output, viewChild, afterNextRender, contentChildren, TemplateRef, isDevMode, contentChild, viewChildren, ViewContainerRef, Injector } from '@angular/core';
|
|
3
|
+
import { input, inject, ElementRef, effect, Directive, Injectable, signal, DestroyRef, model, computed, HostListener, Component, InjectionToken, Service, booleanAttribute, linkedSignal, numberAttribute, ViewEncapsulation, output, viewChild, afterNextRender, contentChildren, TemplateRef, isDevMode, contentChild, viewChildren, ViewContainerRef, Injector } from '@angular/core';
|
|
4
4
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -638,32 +638,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
638
638
|
type: Service
|
|
639
639
|
}] });
|
|
640
640
|
|
|
641
|
-
/** Ink for text on a light vs dark categorical fill. */
|
|
642
|
-
const DARK_INK = "#1a202c";
|
|
643
|
-
const LIGHT_INK = "#ffffff";
|
|
644
641
|
/**
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
642
|
+
* Small WCAG colour-contrast helpers — used to pick a legible ink for text on a
|
|
643
|
+
* categorical fill (avatar initials), and lockable by a contrast test.
|
|
644
|
+
*
|
|
645
|
+
* These operate on plain hex strings (`#rgb` / `#rrggbb`); a categorical palette
|
|
646
|
+
* is qualitative *data* (hex is allowed there — rule 5.3), so contrast can't be
|
|
647
|
+
* a token concern and lives here.
|
|
648
648
|
*/
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const
|
|
652
|
-
|
|
649
|
+
/** Parse `#rgb` / `#rrggbb` to `[r, g, b]` (0–255), or `null` if not a hex. */
|
|
650
|
+
function parseHex(hex) {
|
|
651
|
+
const h = hex.trim().replace(/^#/, "");
|
|
652
|
+
const full = h.length === 3
|
|
653
|
+
? h
|
|
653
654
|
.split("")
|
|
654
655
|
.map((c) => c + c)
|
|
655
656
|
.join("")
|
|
656
|
-
:
|
|
657
|
+
: h;
|
|
657
658
|
if (!/^[0-9a-fA-F]{6}$/.test(full)) {
|
|
658
|
-
return
|
|
659
|
+
return null;
|
|
659
660
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
661
|
+
return [
|
|
662
|
+
parseInt(full.slice(0, 2), 16),
|
|
663
|
+
parseInt(full.slice(2, 4), 16),
|
|
664
|
+
parseInt(full.slice(4, 6), 16),
|
|
665
|
+
];
|
|
666
|
+
}
|
|
667
|
+
/** WCAG relative luminance of a hex colour, or `null` if it isn't a hex. */
|
|
668
|
+
function foldLuminance(hex) {
|
|
669
|
+
const rgb = parseHex(hex);
|
|
670
|
+
if (!rgb) {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
const lin = (c) => {
|
|
674
|
+
const s = c / 255;
|
|
675
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
663
676
|
};
|
|
664
|
-
|
|
665
|
-
|
|
677
|
+
return 0.2126 * lin(rgb[0]) + 0.7152 * lin(rgb[1]) + 0.0722 * lin(rgb[2]);
|
|
678
|
+
}
|
|
679
|
+
/** WCAG contrast ratio between two hex colours (1–21), or `null` if either
|
|
680
|
+
* isn't a hex. */
|
|
681
|
+
function foldContrast(a, b) {
|
|
682
|
+
const la = foldLuminance(a);
|
|
683
|
+
const lb = foldLuminance(b);
|
|
684
|
+
if (la === null || lb === null) {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
|
|
688
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
666
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* The **more legible** of two inks for text drawn on `fill` — whichever has the
|
|
692
|
+
* higher WCAG contrast. Picking the max (rather than thresholding luminance)
|
|
693
|
+
* guarantees the best of the pair for *any* fill: at the black/white crossover
|
|
694
|
+
* both inks already clear ~4.5:1, so every fill lands at or above it. A non-hex
|
|
695
|
+
* `fill` falls back to `dark`.
|
|
696
|
+
*/
|
|
697
|
+
function foldReadableInk(fill, dark, light) {
|
|
698
|
+
const withDark = foldContrast(fill, dark);
|
|
699
|
+
const withLight = foldContrast(fill, light);
|
|
700
|
+
if (withDark === null || withLight === null) {
|
|
701
|
+
return dark;
|
|
702
|
+
}
|
|
703
|
+
return withDark >= withLight ? dark : light;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Ink for text on a light vs dark categorical fill. */
|
|
707
|
+
const DARK_INK = "#1a202c";
|
|
708
|
+
const LIGHT_INK = "#ffffff";
|
|
667
709
|
/**
|
|
668
710
|
* `<fold-avatar>` — a user/entity avatar with initials or an image.
|
|
669
711
|
*
|
|
@@ -672,11 +714,17 @@ function readableInk(fill) {
|
|
|
672
714
|
* (so the same seed is the same colour everywhere, and one `registry.use(...)`
|
|
673
715
|
* recolours every avatar).
|
|
674
716
|
*
|
|
717
|
+
* A broken `imageUrl` falls back to the initials rather than the browser's
|
|
718
|
+
* broken-image glyph (and retries when the URL changes).
|
|
719
|
+
*
|
|
675
720
|
* Three orthogonal state axes compose: `variant` (fill), `muted` (presence —
|
|
676
721
|
* dim for an absence/inactive person), and `ring` + `ringStyle` (a status
|
|
677
722
|
* outline). The component stays domain-agnostic: a screen maps its own states
|
|
678
723
|
* (e.g. absent → `[muted]`, a scheduled arrival → `ring="accent"`
|
|
679
|
-
* `ringStyle="dotted"`), the avatar just draws the primitives.
|
|
724
|
+
* `ringStyle="dotted"`), the avatar just draws the primitives. The ring is a
|
|
725
|
+
* redundant emphasis cue — never the sole carrier of a state — because on a
|
|
726
|
+
* light page a soft status hue (amber/green) can't clear WCAG 3:1 as a thin
|
|
727
|
+
* line (status-ring-contrast.spec.ts).
|
|
680
728
|
*
|
|
681
729
|
* ## Inputs
|
|
682
730
|
*
|
|
@@ -687,12 +735,19 @@ function readableInk(fill) {
|
|
|
687
735
|
* | `colorSeed` | `string` | `name` | String used to pick the deterministic colour. |
|
|
688
736
|
* | `variant` | `'solid' \| 'ghost'` | `'solid'` | `'ghost'` renders a dashed border (for guests). |
|
|
689
737
|
* | `square` | `boolean` | `false` | Square shape with a small radius (for orgs). |
|
|
690
|
-
* | `imageUrl` | `string` | — | Image/logo URL. Replaces initials when set.
|
|
738
|
+
* | `imageUrl` | `string` | — | Image/logo URL. Replaces initials when set; falls back to initials if it fails to load. |
|
|
691
739
|
* | `muted` | `boolean` | `false` | Dim the avatar (same hue, less intense) — absence / inactive. |
|
|
692
740
|
* | `ring` | `FoldAvatarRing` | `'none'` | A status outline (`accent`/`info`/`warning`/`alert`/`success`). |
|
|
693
741
|
* | `ringStyle` | `'solid' \| 'dotted'` | `'solid'` | Ring line style — `dotted` for scheduled/tentative states. |
|
|
694
742
|
*
|
|
695
743
|
* @selector `fold-avatar`
|
|
744
|
+
*
|
|
745
|
+
* @example
|
|
746
|
+
* ```html
|
|
747
|
+
* <fold-avatar name="Clément Aubry" size="lg" />
|
|
748
|
+
* <fold-avatar name="Léa Petit" ring="accent" ringStyle="dotted" />
|
|
749
|
+
* <fold-avatar name="Acme Corp" square imageUrl="/logo.svg" />
|
|
750
|
+
* ```
|
|
696
751
|
*/
|
|
697
752
|
class FoldAvatarComponent {
|
|
698
753
|
palette = inject(FoldPaletteRegistry);
|
|
@@ -715,8 +770,20 @@ class FoldAvatarComponent {
|
|
|
715
770
|
/** Ring line style — `'dotted'` for scheduled / tentative states. */
|
|
716
771
|
ringStyle = input("solid", /* @ts-ignore */
|
|
717
772
|
...(ngDevMode ? [{ debugName: "ringStyle" }] : /* istanbul ignore next */ []));
|
|
718
|
-
/**
|
|
719
|
-
|
|
773
|
+
/** True once the `<img>` fails to load. Resets whenever `imageUrl` changes,
|
|
774
|
+
* so a corrected URL is retried rather than staying on the fallback. */
|
|
775
|
+
imageFailed = linkedSignal(() => {
|
|
776
|
+
this.imageUrl();
|
|
777
|
+
return false;
|
|
778
|
+
}, /* @ts-ignore */
|
|
779
|
+
...(ngDevMode ? [{ debugName: "imageFailed" }] : /* istanbul ignore next */ []));
|
|
780
|
+
/** Show the image only when a URL is set AND it has not failed — a broken URL
|
|
781
|
+
* falls back to the initials instead of the browser's broken-image glyph. */
|
|
782
|
+
showImage = computed(() => Boolean(this.imageUrl()) && !this.imageFailed(), /* @ts-ignore */
|
|
783
|
+
...(ngDevMode ? [{ debugName: "showImage" }] : /* istanbul ignore next */ []));
|
|
784
|
+
/** Readable ink for the initials — the higher-contrast of dark/light ink on
|
|
785
|
+
* the fill, so a custom palette stays legible. */
|
|
786
|
+
onColor = computed(() => foldReadableInk(this.color(), DARK_INK, LIGHT_INK), /* @ts-ignore */
|
|
720
787
|
...(ngDevMode ? [{ debugName: "onColor" }] : /* istanbul ignore next */ []));
|
|
721
788
|
initials = computed(() => {
|
|
722
789
|
const n = this.name().trim();
|
|
@@ -734,11 +801,11 @@ class FoldAvatarComponent {
|
|
|
734
801
|
color = computed(() => this.palette.colorFor(this.colorSeed() ?? this.name()), /* @ts-ignore */
|
|
735
802
|
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
736
803
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldAvatarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
737
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldAvatarComponent, isStandalone: true, selector: "fold-avatar", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, colorSeed: { classPropertyName: "colorSeed", publicName: "colorSeed", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, square: { classPropertyName: "square", publicName: "square", isSignal: true, isRequired: false, transformFunction: null }, imageUrl: { classPropertyName: "imageUrl", publicName: "imageUrl", isSignal: true, isRequired: false, transformFunction: null }, muted: { classPropertyName: "muted", publicName: "muted", isSignal: true, isRequired: false, transformFunction: null }, ring: { classPropertyName: "ring", publicName: "ring", isSignal: true, isRequired: false, transformFunction: null }, ringStyle: { classPropertyName: "ringStyle", publicName: "ringStyle", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (
|
|
804
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldAvatarComponent, isStandalone: true, selector: "fold-avatar", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, colorSeed: { classPropertyName: "colorSeed", publicName: "colorSeed", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, square: { classPropertyName: "square", publicName: "square", isSignal: true, isRequired: false, transformFunction: null }, imageUrl: { classPropertyName: "imageUrl", publicName: "imageUrl", isSignal: true, isRequired: false, transformFunction: null }, muted: { classPropertyName: "muted", publicName: "muted", isSignal: true, isRequired: false, transformFunction: null }, ring: { classPropertyName: "ring", publicName: "ring", isSignal: true, isRequired: false, transformFunction: null }, ringStyle: { classPropertyName: "ringStyle", publicName: "ringStyle", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (showImage()) {\n <div\n class=\"avatar has-image\"\n [class.size-sm]=\"size() === 'sm'\"\n [class.size-md]=\"size() === 'md'\"\n [class.size-lg]=\"size() === 'lg'\"\n [class.variant-ghost]=\"variant() === 'ghost'\"\n [class.shape-square]=\"square()\"\n [class.is-muted]=\"muted()\"\n [attr.data-ring]=\"ring() === 'none' ? null : ring()\"\n [attr.data-ring-style]=\"ringStyle()\"\n [attr.title]=\"name()\"\n >\n <img\n [src]=\"imageUrl()\"\n [alt]=\"name()\"\n class=\"avatar-img\"\n (error)=\"imageFailed.set(true)\"\n />\n </div>\n} @else {\n <div\n class=\"avatar\"\n [class.size-sm]=\"size() === 'sm'\"\n [class.size-md]=\"size() === 'md'\"\n [class.size-lg]=\"size() === 'lg'\"\n [class.variant-ghost]=\"variant() === 'ghost'\"\n [class.shape-square]=\"square()\"\n [class.is-muted]=\"muted()\"\n [attr.data-ring]=\"ring() === 'none' ? null : ring()\"\n [attr.data-ring-style]=\"ringStyle()\"\n [style.background]=\"variant() === 'solid' ? color() : ''\"\n [style.color]=\"variant() === 'solid' ? onColor() : ''\"\n [attr.title]=\"name()\"\n >\n {{ initials() }}\n </div>\n}\n", styles: ["@charset \"UTF-8\";.avatar{display:flex;align-items:center;justify-content:center;flex-shrink:0;font-weight:700;letter-spacing:.02em;-webkit-user-select:none;user-select:none;border-radius:var(--fold-radius-round)}.avatar.size-sm{width:20px;height:20px;font-size:7px}.avatar.size-md{width:32px;height:32px;font-size:11px}.avatar.size-lg{width:44px;height:44px;font-size:15px}.avatar.variant-ghost{background:var(--fold-color-surface-raised);color:var(--fold-color-text-secondary);border:1px dashed var(--fold-color-border)}.avatar.shape-square{border-radius:var(--fold-radius-sm)}.avatar.has-image{overflow:hidden;background:var(--fold-color-surface-raised);border:1px solid var(--fold-color-border)}.avatar.has-image.variant-ghost{border-style:dashed}.avatar-img{width:100%;height:100%;object-fit:cover}.avatar.is-muted{opacity:.45}.avatar[data-ring]{outline:2px solid var(--avatar-ring, transparent);outline-offset:2px}.avatar[data-ring-style=dotted]{outline-style:dotted}.avatar[data-ring=accent]{--avatar-ring: var(--fold-color-primary)}.avatar[data-ring=info]{--avatar-ring: var(--fold-color-info)}.avatar[data-ring=warning]{--avatar-ring: var(--fold-color-warning)}.avatar[data-ring=alert]{--avatar-ring: var(--fold-color-alert)}.avatar[data-ring=success]{--avatar-ring: var(--fold-color-success)}\n"] });
|
|
738
805
|
}
|
|
739
806
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldAvatarComponent, decorators: [{
|
|
740
807
|
type: Component,
|
|
741
|
-
args: [{ selector: "fold-avatar", standalone: true, template: "@if (
|
|
808
|
+
args: [{ selector: "fold-avatar", standalone: true, template: "@if (showImage()) {\n <div\n class=\"avatar has-image\"\n [class.size-sm]=\"size() === 'sm'\"\n [class.size-md]=\"size() === 'md'\"\n [class.size-lg]=\"size() === 'lg'\"\n [class.variant-ghost]=\"variant() === 'ghost'\"\n [class.shape-square]=\"square()\"\n [class.is-muted]=\"muted()\"\n [attr.data-ring]=\"ring() === 'none' ? null : ring()\"\n [attr.data-ring-style]=\"ringStyle()\"\n [attr.title]=\"name()\"\n >\n <img\n [src]=\"imageUrl()\"\n [alt]=\"name()\"\n class=\"avatar-img\"\n (error)=\"imageFailed.set(true)\"\n />\n </div>\n} @else {\n <div\n class=\"avatar\"\n [class.size-sm]=\"size() === 'sm'\"\n [class.size-md]=\"size() === 'md'\"\n [class.size-lg]=\"size() === 'lg'\"\n [class.variant-ghost]=\"variant() === 'ghost'\"\n [class.shape-square]=\"square()\"\n [class.is-muted]=\"muted()\"\n [attr.data-ring]=\"ring() === 'none' ? null : ring()\"\n [attr.data-ring-style]=\"ringStyle()\"\n [style.background]=\"variant() === 'solid' ? color() : ''\"\n [style.color]=\"variant() === 'solid' ? onColor() : ''\"\n [attr.title]=\"name()\"\n >\n {{ initials() }}\n </div>\n}\n", styles: ["@charset \"UTF-8\";.avatar{display:flex;align-items:center;justify-content:center;flex-shrink:0;font-weight:700;letter-spacing:.02em;-webkit-user-select:none;user-select:none;border-radius:var(--fold-radius-round)}.avatar.size-sm{width:20px;height:20px;font-size:7px}.avatar.size-md{width:32px;height:32px;font-size:11px}.avatar.size-lg{width:44px;height:44px;font-size:15px}.avatar.variant-ghost{background:var(--fold-color-surface-raised);color:var(--fold-color-text-secondary);border:1px dashed var(--fold-color-border)}.avatar.shape-square{border-radius:var(--fold-radius-sm)}.avatar.has-image{overflow:hidden;background:var(--fold-color-surface-raised);border:1px solid var(--fold-color-border)}.avatar.has-image.variant-ghost{border-style:dashed}.avatar-img{width:100%;height:100%;object-fit:cover}.avatar.is-muted{opacity:.45}.avatar[data-ring]{outline:2px solid var(--avatar-ring, transparent);outline-offset:2px}.avatar[data-ring-style=dotted]{outline-style:dotted}.avatar[data-ring=accent]{--avatar-ring: var(--fold-color-primary)}.avatar[data-ring=info]{--avatar-ring: var(--fold-color-info)}.avatar[data-ring=warning]{--avatar-ring: var(--fold-color-warning)}.avatar[data-ring=alert]{--avatar-ring: var(--fold-color-alert)}.avatar[data-ring=success]{--avatar-ring: var(--fold-color-success)}\n"] }]
|
|
742
809
|
}], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], colorSeed: [{ type: i0.Input, args: [{ isSignal: true, alias: "colorSeed", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], square: [{ type: i0.Input, args: [{ isSignal: true, alias: "square", required: false }] }], imageUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "imageUrl", required: false }] }], muted: [{ type: i0.Input, args: [{ isSignal: true, alias: "muted", required: false }] }], ring: [{ type: i0.Input, args: [{ isSignal: true, alias: "ring", required: false }] }], ringStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "ringStyle", required: false }] }] } });
|
|
743
810
|
|
|
744
811
|
/**
|
|
@@ -766,6 +833,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
766
833
|
* | `ringStyle` | `'solid' \| 'dotted'` | `'solid'` | Ring line style (`dotted` = scheduled). |
|
|
767
834
|
*
|
|
768
835
|
* @selector `fold-avatar-detail`
|
|
836
|
+
*
|
|
837
|
+
* @example
|
|
838
|
+
* ```html
|
|
839
|
+
* <fold-avatar-detail primary="Alex Rivers" secondary="alex@sh3pherd.dev" />
|
|
840
|
+
* <fold-avatar-detail primary="Acme Corp" secondary="Organisation" square />
|
|
841
|
+
* ```
|
|
769
842
|
*/
|
|
770
843
|
class FoldAvatarDetailComponent {
|
|
771
844
|
primary = input.required(/* @ts-ignore */
|
|
@@ -1868,14 +1941,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
1868
1941
|
*
|
|
1869
1942
|
* - `surface` — `card` (the raised card tint, default) · `sunken` (a deeper
|
|
1870
1943
|
* surface for large containers, below the card — the ref's second card tint;
|
|
1871
|
-
* pairs with a fainter `border-subtle` hairline)
|
|
1944
|
+
* pairs with a fainter `border-subtle` hairline) · `accent` (an accent-filled
|
|
1945
|
+
* card whose content sub-tree auto-inverts to a compatible on-accent palette).
|
|
1872
1946
|
* - `radius` — `lg` (14px, default) · `md` · `sm`.
|
|
1873
1947
|
* - `padding` — `md` (16px, default) · `none` · `sm` · `lg`. Sets the *body*
|
|
1874
1948
|
* padding; override with a custom value via `--fold-card-padding`.
|
|
1875
|
-
* - `interactive` —
|
|
1876
|
-
* - `separators` —
|
|
1877
|
-
*
|
|
1878
|
-
*
|
|
1949
|
+
* - `interactive` — makes the whole card a clickable control (see below).
|
|
1950
|
+
* - `separators` — which bands get a hairline against the body:
|
|
1951
|
+
* `none` (default) · `header` · `footer` · `both`.
|
|
1952
|
+
* - `raisedBands` — which bands are tinted a step above the surface (fainter on
|
|
1953
|
+
* `sunken`): `none` (default) · `header` · `footer` · `both`.
|
|
1954
|
+
*
|
|
1955
|
+
* `separators` and `raisedBands` are per-band and independent axes, so a card can
|
|
1956
|
+
* have (say) a raised, un-separated header over a flush footer.
|
|
1879
1957
|
*
|
|
1880
1958
|
* Content projection:
|
|
1881
1959
|
* - default slot → the card body.
|
|
@@ -1887,10 +1965,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
1887
1965
|
* never pads. So the content padding is identical whether or not the bands show —
|
|
1888
1966
|
* toggling a header/footer never shifts the body.
|
|
1889
1967
|
*
|
|
1968
|
+
* **Interactive cards.** `interactive` turns the whole card into a real control:
|
|
1969
|
+
* the host gains `role="button"`, `tabindex="0"`, a focus ring and a hover lift,
|
|
1970
|
+
* and Enter / Space (or a click) emit `(activated)`. Give it an accessible name
|
|
1971
|
+
* with `ariaLabel` when the card's text isn't a sufficient label.
|
|
1972
|
+
* A `role="button"` must not wrap other interactive controls — so an interactive
|
|
1973
|
+
* card must NOT contain buttons/links. For a card with its own inner actions,
|
|
1974
|
+
* keep it non-interactive and put a single primary `<a>`/`<button>` inside.
|
|
1975
|
+
*
|
|
1890
1976
|
* ```html
|
|
1891
1977
|
* <fold-card>Static content</fold-card>
|
|
1892
1978
|
* <fold-card surface="sunken" padding="lg">Deep container</fold-card>
|
|
1893
|
-
* <fold-card separators>
|
|
1979
|
+
* <fold-card separators="both">
|
|
1894
1980
|
* <h3 cardHeader>Title</h3>
|
|
1895
1981
|
* Body content
|
|
1896
1982
|
* <div cardFooter>Actions</div>
|
|
@@ -1902,9 +1988,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
1902
1988
|
* (default `clip`) lets content escape the rounded corners.
|
|
1903
1989
|
*
|
|
1904
1990
|
* @selector `fold-card`
|
|
1991
|
+
*
|
|
1992
|
+
* @example
|
|
1993
|
+
* ```html
|
|
1994
|
+
* <fold-card interactive ariaLabel="Open Acme Records" (activated)="open()">
|
|
1995
|
+
* <h3>Acme Records</h3>
|
|
1996
|
+
* <p>128 contracts · 96 active</p>
|
|
1997
|
+
* </fold-card>
|
|
1998
|
+
* ```
|
|
1905
1999
|
*/
|
|
1906
2000
|
class FoldCardComponent {
|
|
1907
|
-
/** Surface tint — `card` (raised, default)
|
|
2001
|
+
/** Surface tint — `card` (raised, default), `sunken` (deeper container), or
|
|
2002
|
+
* `accent` (an **accent-filled** card: the accent colour as ground, with the
|
|
2003
|
+
* whole content sub-tree auto-inverted to a compatible on-accent role-set —
|
|
2004
|
+
* text, borders, band gradation, and even nested buttons/links/icon-tiles
|
|
2005
|
+
* convert automatically). Use it to make a card in a grid stand out. */
|
|
1908
2006
|
surface = input("card", /* @ts-ignore */
|
|
1909
2007
|
...(ngDevMode ? [{ debugName: "surface" }] : /* istanbul ignore next */ []));
|
|
1910
2008
|
/** Corner radius — `lg` (default), `md`, or `sm`. */
|
|
@@ -1914,31 +2012,67 @@ class FoldCardComponent {
|
|
|
1914
2012
|
* set with the `--fold-card-padding` CSS variable. */
|
|
1915
2013
|
padding = input("md", /* @ts-ignore */
|
|
1916
2014
|
...(ngDevMode ? [{ debugName: "padding" }] : /* istanbul ignore next */ []));
|
|
1917
|
-
/**
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2015
|
+
/** Make the whole card a clickable control: `role="button"` + `tabindex`, a
|
|
2016
|
+
* focus ring, a hover lift, and Enter/Space/click → `(activated)`. Must not
|
|
2017
|
+
* wrap other interactive controls (see the class docs). */
|
|
2018
|
+
interactive = input(false, { ...(ngDevMode ? { debugName: "interactive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2019
|
+
/** Accessible name for the interactive card, when its content isn't enough. */
|
|
2020
|
+
ariaLabel = input(/* @ts-ignore */
|
|
2021
|
+
...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
2022
|
+
/** Which bands get a hairline against the body — `none` (default), `header`,
|
|
2023
|
+
* `footer`, or `both`. */
|
|
2024
|
+
separators = input("none", /* @ts-ignore */
|
|
2025
|
+
...(ngDevMode ? [{ debugName: "separators" }] : /* istanbul ignore next */ []));
|
|
2026
|
+
/** Which bands are lifted with a subtle raised tint over the card surface
|
|
2027
|
+
* (fainter on `sunken`) — `none` (default), `header`, `footer`, or `both`. */
|
|
2028
|
+
raisedBands = input("none", /* @ts-ignore */
|
|
2029
|
+
...(ngDevMode ? [{ debugName: "raisedBands" }] : /* istanbul ignore next */ []));
|
|
2030
|
+
/** Fires when an `interactive` card is activated (click, Enter, or Space). */
|
|
2031
|
+
activated = output();
|
|
2032
|
+
/** Whether a per-band chrome value applies to the given band. */
|
|
2033
|
+
hasBand(value, band) {
|
|
2034
|
+
return value === band || value === "both";
|
|
2035
|
+
}
|
|
2036
|
+
onActivate(event) {
|
|
2037
|
+
if (this.interactive()) {
|
|
2038
|
+
this.activated.emit(event);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
/** Enter/Space activate the card, matching the native button keyboard contract
|
|
2042
|
+
* (Space is prevented from scrolling the page). */
|
|
2043
|
+
onKeydown(event) {
|
|
2044
|
+
if (!this.interactive() || (event.key !== "Enter" && event.key !== " ")) {
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
event.preventDefault();
|
|
2048
|
+
this.activated.emit(event);
|
|
2049
|
+
}
|
|
1925
2050
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1926
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.8", type: FoldCardComponent, isStandalone: true, selector: "fold-card", inputs: { surface: { classPropertyName: "surface", publicName: "surface", isSignal: true, isRequired: false, transformFunction: null }, radius: { classPropertyName: "radius", publicName: "radius", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, separators: { classPropertyName: "separators", publicName: "separators", isSignal: true, isRequired: false, transformFunction: null }, raisedBands: { classPropertyName: "raisedBands", publicName: "raisedBands", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.s-sunken": "surface() === 'sunken'", "class.r-sm": "radius() === 'sm'", "class.r-md": "radius() === 'md'", "class.p-none": "padding() === 'none'", "class.p-sm": "padding() === 'sm'", "class.p-lg": "padding() === 'lg'", "class.is-interactive": "interactive()", "class.
|
|
2051
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.8", type: FoldCardComponent, isStandalone: true, selector: "fold-card", inputs: { surface: { classPropertyName: "surface", publicName: "surface", isSignal: true, isRequired: false, transformFunction: null }, radius: { classPropertyName: "radius", publicName: "radius", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, separators: { classPropertyName: "separators", publicName: "separators", isSignal: true, isRequired: false, transformFunction: null }, raisedBands: { classPropertyName: "raisedBands", publicName: "raisedBands", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activated: "activated" }, host: { listeners: { "click": "onActivate($event)", "keydown": "onKeydown($event)" }, properties: { "class.s-sunken": "surface() === 'sunken'", "class.s-accent": "surface() === 'accent'", "attr.data-surface": "surface() === 'accent' ? 'accent' : null", "class.r-sm": "radius() === 'sm'", "class.r-md": "radius() === 'md'", "class.p-none": "padding() === 'none'", "class.p-sm": "padding() === 'sm'", "class.p-lg": "padding() === 'lg'", "class.is-interactive": "interactive()", "class.sep-header": "hasBand(separators(), 'header')", "class.sep-footer": "hasBand(separators(), 'footer')", "class.raise-header": "hasBand(raisedBands(), 'header')", "class.raise-footer": "hasBand(raisedBands(), 'footer')", "attr.role": "interactive() ? 'button' : null", "attr.tabindex": "interactive() ? 0 : null", "attr.aria-label": "interactive() ? (ariaLabel() ?? null) : null" } }, ngImport: i0, template: "<div class=\"card-header\"><ng-content select=\"[cardHeader]\" /></div>\n<div class=\"card-body\"><ng-content /></div>\n<div class=\"card-footer\"><ng-content select=\"[cardFooter]\" /></div>\n", styles: ["@charset \"UTF-8\";:host{--_pad: 16px;--_chrome: 12px 16px;--_radius: var(--fold-radius-lg);--_band-raise: var(--fold-color-surface-hover);--_sep-line: var(--fold-color-border-subtle);display:flex;flex-direction:column;background:var(--fold-color-surface-card);border:1px solid var(--fold-color-border);border-radius:var(--_radius);overflow:var(--fold-card-overflow, clip)}:host(.s-sunken){background:var(--fold-color-surface-sunken);border-color:var(--fold-color-border-subtle);--_band-raise: var(--fold-color-surface-card)}:host(.r-md){--_radius: var(--fold-radius-md)}:host(.r-sm){--_radius: var(--fold-radius-sm)}:host(.p-none){--_pad: 0}:host(.p-sm){--_pad: 10px}:host(.p-lg){--_pad: 20px}.card-header,.card-footer{position:relative;padding:var(--_chrome)}.card-header:empty,.card-footer:empty{display:none}.card-header{border-radius:var(--_radius) var(--_radius) 0 0}.card-footer{border-radius:0 0 var(--_radius) var(--_radius)}.card-body{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;padding:var(--fold-card-padding, var(--_pad))}:host(.raise-header) .card-header:not(:empty),:host(.raise-footer) .card-footer:not(:empty){background-color:var(--_band-raise)}:host(.sep-header) .card-header:not(:empty){border-bottom:1px solid var(--_sep-line)}:host(.sep-footer) .card-footer:not(:empty){border-top:1px solid var(--_sep-line)}:host(.is-interactive){cursor:pointer;transition:transform .18s ease,box-shadow .18s ease}:host(.is-interactive:hover){transform:translateY(-2px);box-shadow:var(--fold-shadow-md)}:host(.is-interactive:focus-visible){outline:2px solid var(--fold-color-primary);outline-offset:2px}@media(prefers-reduced-motion:reduce){:host(.is-interactive){transition:none}}:host(.s-accent){background:var(--fold-color-primary);border-color:var(--fold-color-primary);color:var(--fold-color-on-primary);--_band-raise: color-mix( in srgb, var(--_accent-ink) 16%, var(--_accent-fill) );--_sep-line: color-mix(in srgb, var(--_accent-ink) 24%, transparent)}:host(.s-accent.is-interactive:focus-visible){outline-color:var(--fold-color-on-primary)}\n"] });
|
|
1927
2052
|
}
|
|
1928
2053
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldCardComponent, decorators: [{
|
|
1929
2054
|
type: Component,
|
|
1930
2055
|
args: [{ selector: "fold-card", standalone: true, host: {
|
|
1931
2056
|
"[class.s-sunken]": "surface() === 'sunken'",
|
|
2057
|
+
"[class.s-accent]": "surface() === 'accent'",
|
|
2058
|
+
"[attr.data-surface]": "surface() === 'accent' ? 'accent' : null",
|
|
1932
2059
|
"[class.r-sm]": "radius() === 'sm'",
|
|
1933
2060
|
"[class.r-md]": "radius() === 'md'",
|
|
1934
2061
|
"[class.p-none]": "padding() === 'none'",
|
|
1935
2062
|
"[class.p-sm]": "padding() === 'sm'",
|
|
1936
2063
|
"[class.p-lg]": "padding() === 'lg'",
|
|
1937
2064
|
"[class.is-interactive]": "interactive()",
|
|
1938
|
-
"[class.
|
|
1939
|
-
"[class.
|
|
1940
|
-
|
|
1941
|
-
|
|
2065
|
+
"[class.sep-header]": "hasBand(separators(), 'header')",
|
|
2066
|
+
"[class.sep-footer]": "hasBand(separators(), 'footer')",
|
|
2067
|
+
"[class.raise-header]": "hasBand(raisedBands(), 'header')",
|
|
2068
|
+
"[class.raise-footer]": "hasBand(raisedBands(), 'footer')",
|
|
2069
|
+
"[attr.role]": "interactive() ? 'button' : null",
|
|
2070
|
+
"[attr.tabindex]": "interactive() ? 0 : null",
|
|
2071
|
+
"[attr.aria-label]": "interactive() ? (ariaLabel() ?? null) : null",
|
|
2072
|
+
"(click)": "onActivate($event)",
|
|
2073
|
+
"(keydown)": "onKeydown($event)",
|
|
2074
|
+
}, template: "<div class=\"card-header\"><ng-content select=\"[cardHeader]\" /></div>\n<div class=\"card-body\"><ng-content /></div>\n<div class=\"card-footer\"><ng-content select=\"[cardFooter]\" /></div>\n", styles: ["@charset \"UTF-8\";:host{--_pad: 16px;--_chrome: 12px 16px;--_radius: var(--fold-radius-lg);--_band-raise: var(--fold-color-surface-hover);--_sep-line: var(--fold-color-border-subtle);display:flex;flex-direction:column;background:var(--fold-color-surface-card);border:1px solid var(--fold-color-border);border-radius:var(--_radius);overflow:var(--fold-card-overflow, clip)}:host(.s-sunken){background:var(--fold-color-surface-sunken);border-color:var(--fold-color-border-subtle);--_band-raise: var(--fold-color-surface-card)}:host(.r-md){--_radius: var(--fold-radius-md)}:host(.r-sm){--_radius: var(--fold-radius-sm)}:host(.p-none){--_pad: 0}:host(.p-sm){--_pad: 10px}:host(.p-lg){--_pad: 20px}.card-header,.card-footer{position:relative;padding:var(--_chrome)}.card-header:empty,.card-footer:empty{display:none}.card-header{border-radius:var(--_radius) var(--_radius) 0 0}.card-footer{border-radius:0 0 var(--_radius) var(--_radius)}.card-body{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;padding:var(--fold-card-padding, var(--_pad))}:host(.raise-header) .card-header:not(:empty),:host(.raise-footer) .card-footer:not(:empty){background-color:var(--_band-raise)}:host(.sep-header) .card-header:not(:empty){border-bottom:1px solid var(--_sep-line)}:host(.sep-footer) .card-footer:not(:empty){border-top:1px solid var(--_sep-line)}:host(.is-interactive){cursor:pointer;transition:transform .18s ease,box-shadow .18s ease}:host(.is-interactive:hover){transform:translateY(-2px);box-shadow:var(--fold-shadow-md)}:host(.is-interactive:focus-visible){outline:2px solid var(--fold-color-primary);outline-offset:2px}@media(prefers-reduced-motion:reduce){:host(.is-interactive){transition:none}}:host(.s-accent){background:var(--fold-color-primary);border-color:var(--fold-color-primary);color:var(--fold-color-on-primary);--_band-raise: color-mix( in srgb, var(--_accent-ink) 16%, var(--_accent-fill) );--_sep-line: color-mix(in srgb, var(--_accent-ink) 24%, transparent)}:host(.s-accent.is-interactive:focus-visible){outline-color:var(--fold-color-on-primary)}\n"] }]
|
|
2075
|
+
}], propDecorators: { surface: [{ type: i0.Input, args: [{ isSignal: true, alias: "surface", required: false }] }], radius: [{ type: i0.Input, args: [{ isSignal: true, alias: "radius", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], separators: [{ type: i0.Input, args: [{ isSignal: true, alias: "separators", required: false }] }], raisedBands: [{ type: i0.Input, args: [{ isSignal: true, alias: "raisedBands", required: false }] }], activated: [{ type: i0.Output, args: ["activated"] }] } });
|
|
1942
2076
|
|
|
1943
2077
|
/**
|
|
1944
2078
|
* `<fold-element-title>` — the label that heads a section, card or panel. Fully
|
|
@@ -2039,7 +2173,7 @@ class FoldContextCardComponent {
|
|
|
2039
2173
|
subtitle = input(/* @ts-ignore */
|
|
2040
2174
|
...(ngDevMode ? [undefined, { debugName: "subtitle" }] : /* istanbul ignore next */ []));
|
|
2041
2175
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldContextCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2042
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.8", type: FoldContextCardComponent, isStandalone: true, selector: "fold-context-card", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, iconTone: { classPropertyName: "iconTone", publicName: "iconTone", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<fold-card padding=\"none\">\n <div class=\"cc-head\">\n <fold-element-title\n variant=\"title\"\n [level]=\"3\"\n [icon]=\"icon()\"\n [iconTone]=\"iconTone()\"\n [title]=\"title()\"\n [subtitle]=\"subtitle()\"\n />\n </div>\n <div class=\"cc-body\"><ng-content /></div>\n <div class=\"cc-foot\"><ng-content select=\"[footer]\" /></div>\n</fold-card>\n", styles: ["@charset \"UTF-8\";:host{display:block}.cc-head{padding:16px;border-bottom:1px solid var(--fold-color-border-subtle)}.cc-body{padding:2px 16px}.cc-foot{display:flex;justify-content:center;padding:14px 16px;border-top:1px solid var(--fold-color-border-subtle)}.cc-foot:empty{display:none}\n"], dependencies: [{ kind: "component", type: FoldCardComponent, selector: "fold-card", inputs: ["surface", "radius", "padding", "interactive", "separators", "raisedBands"] }, { kind: "component", type: FoldElementTitleComponent, selector: "fold-element-title", inputs: ["icon", "iconTone", "title", "subtitle", "variant", "level", "headingId"] }] });
|
|
2176
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.8", type: FoldContextCardComponent, isStandalone: true, selector: "fold-context-card", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, iconTone: { classPropertyName: "iconTone", publicName: "iconTone", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<fold-card padding=\"none\">\n <div class=\"cc-head\">\n <fold-element-title\n variant=\"title\"\n [level]=\"3\"\n [icon]=\"icon()\"\n [iconTone]=\"iconTone()\"\n [title]=\"title()\"\n [subtitle]=\"subtitle()\"\n />\n </div>\n <div class=\"cc-body\"><ng-content /></div>\n <div class=\"cc-foot\"><ng-content select=\"[footer]\" /></div>\n</fold-card>\n", styles: ["@charset \"UTF-8\";:host{display:block}.cc-head{padding:16px;border-bottom:1px solid var(--fold-color-border-subtle)}.cc-body{padding:2px 16px}.cc-foot{display:flex;justify-content:center;padding:14px 16px;border-top:1px solid var(--fold-color-border-subtle)}.cc-foot:empty{display:none}\n"], dependencies: [{ kind: "component", type: FoldCardComponent, selector: "fold-card", inputs: ["surface", "radius", "padding", "interactive", "ariaLabel", "separators", "raisedBands"], outputs: ["activated"] }, { kind: "component", type: FoldElementTitleComponent, selector: "fold-element-title", inputs: ["icon", "iconTone", "title", "subtitle", "variant", "level", "headingId"] }] });
|
|
2043
2177
|
}
|
|
2044
2178
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldContextCardComponent, decorators: [{
|
|
2045
2179
|
type: Component,
|
|
@@ -2288,6 +2422,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
2288
2422
|
* - `tone` — `accent` (default, brand link) · `muted` (secondary).
|
|
2289
2423
|
* - `icon` / `trailingIcon` — optional glyphs around the label.
|
|
2290
2424
|
* - `disabled` — button mode only.
|
|
2425
|
+
* - `target` / `rel` — anchor mode only. Setting `target="_blank"` auto-applies
|
|
2426
|
+
* `rel="noopener noreferrer"` (safe by default); override with an explicit
|
|
2427
|
+
* `rel`.
|
|
2291
2428
|
*
|
|
2292
2429
|
* @selector `fold-link`
|
|
2293
2430
|
*
|
|
@@ -2296,7 +2433,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
2296
2433
|
* <fold-link icon="company" trailingIcon="chevron-right" (clicked)="openOrg()">
|
|
2297
2434
|
* Voir l'organigramme
|
|
2298
2435
|
* </fold-link>
|
|
2299
|
-
* <fold-link href="https://sh3pherd.dev/docs" tone="muted">
|
|
2436
|
+
* <fold-link href="https://sh3pherd.dev/docs" target="_blank" tone="muted">
|
|
2437
|
+
* Documentation
|
|
2438
|
+
* </fold-link>
|
|
2300
2439
|
* ```
|
|
2301
2440
|
*/
|
|
2302
2441
|
class FoldLinkComponent {
|
|
@@ -2312,17 +2451,27 @@ class FoldLinkComponent {
|
|
|
2312
2451
|
/** When set, the link renders as an `<a href>` instead of a button. */
|
|
2313
2452
|
href = input(/* @ts-ignore */
|
|
2314
2453
|
...(ngDevMode ? [undefined, { debugName: "href" }] : /* istanbul ignore next */ []));
|
|
2454
|
+
/** Anchor target (e.g. `_blank`). Anchor mode only. */
|
|
2455
|
+
target = input(/* @ts-ignore */
|
|
2456
|
+
...(ngDevMode ? [undefined, { debugName: "target" }] : /* istanbul ignore next */ []));
|
|
2457
|
+
/** Anchor `rel`. Anchor mode only. Defaults to `noopener noreferrer` when
|
|
2458
|
+
* `target="_blank"`; set explicitly to override. */
|
|
2459
|
+
rel = input(/* @ts-ignore */
|
|
2460
|
+
...(ngDevMode ? [undefined, { debugName: "rel" }] : /* istanbul ignore next */ []));
|
|
2315
2461
|
/** Disable the button form (no effect on the `href` form). */
|
|
2316
2462
|
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2317
|
-
/** Fires on click in the button form
|
|
2463
|
+
/** Fires on click in the button form; carries the `MouseEvent`. */
|
|
2318
2464
|
clicked = output();
|
|
2465
|
+
/** Resolved `rel` — the explicit value, or a safe default for a new tab. */
|
|
2466
|
+
resolvedRel = computed(() => this.rel() ?? (this.target() === "_blank" ? "noopener noreferrer" : null), /* @ts-ignore */
|
|
2467
|
+
...(ngDevMode ? [{ debugName: "resolvedRel" }] : /* istanbul ignore next */ []));
|
|
2319
2468
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldLinkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2320
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldLinkComponent, isStandalone: true, selector: "fold-link", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, trailingIcon: { classPropertyName: "trailingIcon", publicName: "trailingIcon", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, host: { properties: { "class.tone-muted": "tone() === 'muted'" } }, ngImport: i0, template: "<ng-template #inner>\n @if (icon(); as i) {\n <fold-icon [name]=\"i\" size=\"sm\" />\n }\n <span class=\"lnk-label\"><ng-content /></span>\n @if (trailingIcon(); as t) {\n <fold-icon [name]=\"t\" size=\"sm\" />\n }\n</ng-template>\n@if (href(); as h) {\n <a
|
|
2469
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldLinkComponent, isStandalone: true, selector: "fold-link", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, trailingIcon: { classPropertyName: "trailingIcon", publicName: "trailingIcon", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, rel: { classPropertyName: "rel", publicName: "rel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, host: { properties: { "class.tone-muted": "tone() === 'muted'" } }, ngImport: i0, template: "<ng-template #inner>\n @if (icon(); as i) {\n <fold-icon [name]=\"i\" size=\"sm\" />\n }\n <span class=\"lnk-label\"><ng-content /></span>\n @if (trailingIcon(); as t) {\n <fold-icon [name]=\"t\" size=\"sm\" />\n }\n</ng-template>\n@if (href(); as h) {\n <a\n class=\"lnk\"\n [href]=\"h\"\n [attr.target]=\"target() ?? null\"\n [attr.rel]=\"resolvedRel()\"\n >\n <ng-container [ngTemplateOutlet]=\"inner\" />\n </a>\n} @else {\n <button\n type=\"button\"\n class=\"lnk\"\n [disabled]=\"disabled()\"\n (click)=\"clicked.emit($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"inner\" />\n </button>\n}\n", styles: [":host{display:inline-flex;min-width:0}.lnk{display:inline-flex;align-items:center;gap:6px;min-width:0;padding:0;border:0;background:none;font-family:inherit;font-size:var(--fold-text-xs);font-weight:600;color:var(--fold-color-primary);text-decoration:none;cursor:pointer;transition:color var(--fold-motion-fast)}:host(.tone-muted) .lnk{color:var(--fold-color-text-secondary)}.lnk:hover .lnk-label{text-decoration:underline}.lnk:disabled{color:var(--fold-color-text-muted);cursor:not-allowed}.lnk:disabled .lnk-label{text-decoration:none}.lnk-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: FoldIconComponent, selector: "fold-icon", inputs: ["name", "size", "title"] }] });
|
|
2321
2470
|
}
|
|
2322
2471
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldLinkComponent, decorators: [{
|
|
2323
2472
|
type: Component,
|
|
2324
|
-
args: [{ selector: "fold-link", standalone: true, imports: [NgTemplateOutlet, FoldIconComponent], host: { "[class.tone-muted]": "tone() === 'muted'" }, template: "<ng-template #inner>\n @if (icon(); as i) {\n <fold-icon [name]=\"i\" size=\"sm\" />\n }\n <span class=\"lnk-label\"><ng-content /></span>\n @if (trailingIcon(); as t) {\n <fold-icon [name]=\"t\" size=\"sm\" />\n }\n</ng-template>\n@if (href(); as h) {\n <a
|
|
2325
|
-
}], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], trailingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingIcon", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clicked: [{ type: i0.Output, args: ["clicked"] }] } });
|
|
2473
|
+
args: [{ selector: "fold-link", standalone: true, imports: [NgTemplateOutlet, FoldIconComponent], host: { "[class.tone-muted]": "tone() === 'muted'" }, template: "<ng-template #inner>\n @if (icon(); as i) {\n <fold-icon [name]=\"i\" size=\"sm\" />\n }\n <span class=\"lnk-label\"><ng-content /></span>\n @if (trailingIcon(); as t) {\n <fold-icon [name]=\"t\" size=\"sm\" />\n }\n</ng-template>\n@if (href(); as h) {\n <a\n class=\"lnk\"\n [href]=\"h\"\n [attr.target]=\"target() ?? null\"\n [attr.rel]=\"resolvedRel()\"\n >\n <ng-container [ngTemplateOutlet]=\"inner\" />\n </a>\n} @else {\n <button\n type=\"button\"\n class=\"lnk\"\n [disabled]=\"disabled()\"\n (click)=\"clicked.emit($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"inner\" />\n </button>\n}\n", styles: [":host{display:inline-flex;min-width:0}.lnk{display:inline-flex;align-items:center;gap:6px;min-width:0;padding:0;border:0;background:none;font-family:inherit;font-size:var(--fold-text-xs);font-weight:600;color:var(--fold-color-primary);text-decoration:none;cursor:pointer;transition:color var(--fold-motion-fast)}:host(.tone-muted) .lnk{color:var(--fold-color-text-secondary)}.lnk:hover .lnk-label{text-decoration:underline}.lnk:disabled{color:var(--fold-color-text-muted);cursor:not-allowed}.lnk:disabled .lnk-label{text-decoration:none}.lnk-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
|
|
2474
|
+
}], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], trailingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingIcon", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], target: [{ type: i0.Input, args: [{ isSignal: true, alias: "target", required: false }] }], rel: [{ type: i0.Input, args: [{ isSignal: true, alias: "rel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clicked: [{ type: i0.Output, args: ["clicked"] }] } });
|
|
2326
2475
|
|
|
2327
2476
|
/**
|
|
2328
2477
|
* `<fold-menu>` — a vertical icon navigation rail (the app's primary menu shell).
|
|
@@ -2866,13 +3015,15 @@ class FoldHeroCardComponent {
|
|
|
2866
3015
|
/** Add a primary accent bar down the left edge (composable with any surface). */
|
|
2867
3016
|
accentBar = input(false, { ...(ngDevMode ? { debugName: "accentBar" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2868
3017
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldHeroCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2869
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.8", type: FoldHeroCardComponent, isStandalone: true, selector: "fold-hero-card", inputs: { surface: { classPropertyName: "surface", publicName: "surface", isSignal: true, isRequired: false, transformFunction: null }, accent: { classPropertyName: "accent", publicName: "accent", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, accentBar: { classPropertyName: "accentBar", publicName: "accentBar", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.s-sunken": "surface() === 'sunken'", "class.s-primary": "surface() === 'primary'", "class.a-subtle": "accent() === 'subtle'", "class.a-gradient": "accent() === 'gradient'", "class.has-bar": "accentBar()", "class.p-sm": "padding() === 'sm'", "class.p-md": "padding() === 'md'" } }, ngImport: i0, template: "<ng-content />\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;overflow:hidden;isolation:isolate;background:var(--fold-color-surface-card);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-lg);padding:20px}:host(.p-sm){padding:10px}:host(.p-md){padding:16px}:host(.s-sunken){background:var(--fold-color-surface-sunken);border-color:var(--fold-color-border-subtle)}:host(.s-primary){background:var(--fold-color-primary);border-color:var(--fold-color-primary);color:var(--fold-color-on-primary)}:host(.a-subtle):after,:host(.a-gradient):after{content:\"\";position:absolute;inset:0;z-index:-1;pointer-events:none}:host(.a-subtle):after{background:radial-gradient(120% 140% at 100% 0%,color-mix(in srgb,var(--fold-color-primary) 10%,transparent),transparent 45%),linear-gradient(160deg,transparent 30%,color-mix(in srgb,var(--fold-color-surface-sunken) 50%,transparent))}:host(.a-gradient){border-color:var(--fold-color-primary-border)}:host(.a-gradient):after{background:radial-gradient(260px circle at 90% -10%,color-mix(in srgb,var(--fold-color-primary) 22%,transparent),transparent 68%),linear-gradient(180deg,color-mix(in srgb,var(--fold-color-primary) 8%,transparent),transparent 60%)}:host(.has-bar):before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(var(--fold-color-primary),color-mix(in srgb,var(--fold-color-primary) 30%,transparent))}\n"] });
|
|
3018
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.8", type: FoldHeroCardComponent, isStandalone: true, selector: "fold-hero-card", inputs: { surface: { classPropertyName: "surface", publicName: "surface", isSignal: true, isRequired: false, transformFunction: null }, accent: { classPropertyName: "accent", publicName: "accent", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, accentBar: { classPropertyName: "accentBar", publicName: "accentBar", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.s-sunken": "surface() === 'sunken'", "class.s-primary": "surface() === 'primary'", "attr.data-surface": "surface() === 'primary' ? 'accent' : null", "class.a-subtle": "accent() === 'subtle'", "class.a-gradient": "accent() === 'gradient'", "class.has-bar": "accentBar()", "class.p-sm": "padding() === 'sm'", "class.p-md": "padding() === 'md'" } }, ngImport: i0, template: "<ng-content />\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;overflow:hidden;isolation:isolate;background:var(--fold-color-surface-card);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-lg);padding:20px}:host(.p-sm){padding:10px}:host(.p-md){padding:16px}:host(.s-sunken){background:var(--fold-color-surface-sunken);border-color:var(--fold-color-border-subtle)}:host(.s-primary){background:var(--fold-color-primary);border-color:var(--fold-color-primary);color:var(--fold-color-on-primary)}:host(.a-subtle):after,:host(.a-gradient):after{content:\"\";position:absolute;inset:0;z-index:-1;pointer-events:none}:host(.a-subtle):after{background:radial-gradient(120% 140% at 100% 0%,color-mix(in srgb,var(--fold-color-primary) 10%,transparent),transparent 45%),linear-gradient(160deg,transparent 30%,color-mix(in srgb,var(--fold-color-surface-sunken) 50%,transparent))}:host(.a-gradient){border-color:var(--fold-color-primary-border)}:host(.a-gradient):after{background:radial-gradient(260px circle at 90% -10%,color-mix(in srgb,var(--fold-color-primary) 22%,transparent),transparent 68%),linear-gradient(180deg,color-mix(in srgb,var(--fold-color-primary) 8%,transparent),transparent 60%)}:host(.has-bar):before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:3px;background:linear-gradient(var(--fold-color-primary),color-mix(in srgb,var(--fold-color-primary) 30%,transparent))}\n"] });
|
|
2870
3019
|
}
|
|
2871
3020
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldHeroCardComponent, decorators: [{
|
|
2872
3021
|
type: Component,
|
|
2873
3022
|
args: [{ selector: "fold-hero-card", standalone: true, host: {
|
|
2874
3023
|
"[class.s-sunken]": "surface() === 'sunken'",
|
|
2875
3024
|
"[class.s-primary]": "surface() === 'primary'",
|
|
3025
|
+
// a solid primary ground is an accent surface: auto-invert the content too.
|
|
3026
|
+
"[attr.data-surface]": "surface() === 'primary' ? 'accent' : null",
|
|
2876
3027
|
"[class.a-subtle]": "accent() === 'subtle'",
|
|
2877
3028
|
"[class.a-gradient]": "accent() === 'gradient'",
|
|
2878
3029
|
"[class.has-bar]": "accentBar()",
|