@underlayerdev/ui 0.11.0 → 0.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@underlayerdev/ui",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "peerDependencies": {
5
5
  "@angular/cdk": "^22.0.0",
6
6
  "@angular/common": "^22.0.0",
@@ -298,6 +298,33 @@ a:active {
298
298
  color: #a781ff;
299
299
  }
300
300
 
301
+ .ul-image {
302
+ height: 100%;
303
+ }
304
+ .ul-image--not-found {
305
+ position: relative;
306
+ }
307
+ .ul-image--not-found::after {
308
+ display: flex;
309
+ place-items: center;
310
+ place-content: center;
311
+ position: absolute;
312
+ z-index: 2;
313
+ top: 0;
314
+ left: 0;
315
+ width: 100%;
316
+ height: 100%;
317
+ font-family: "Ultra Icons";
318
+ font-size: 48px;
319
+ line-height: 48px;
320
+ color: rgba(255, 255, 255, 0.3);
321
+ content: "image_dashed";
322
+ visibility: visible;
323
+ }
324
+ .ul-image--not-found::after {
325
+ background-color: #232323;
326
+ }
327
+
301
328
  @font-face {
302
329
  font-family: "Roboto Mono";
303
330
  src: url("fonts/RobotoMono.ttf") format("truetype");
@@ -4,6 +4,7 @@ import * as i1 from '@angular/cdk/accordion';
4
4
  import { CdkAccordionItem } from '@angular/cdk/accordion';
5
5
  import { FormValueControl, WithOptionalFieldTree, ValidationError, FormCheckboxControl } from '@angular/forms/signals';
6
6
  import Splide, { Options } from '@splidejs/splide';
7
+ import { ImageTransform, Dimensions } from 'ngx-image-cropper';
7
8
  import { CurrencyPipe } from '@angular/common';
8
9
  import * as _underlayerdev_ui from '@underlayerdev/ui';
9
10
 
@@ -79,7 +80,7 @@ declare class AreaCodeComponent {
79
80
 
80
81
  type UiSize = 'sm' | 'md' | 'lg' | 'xl';
81
82
  type UiCheckboxSize = 'default' | 'sm' | 'lg';
82
- type UiTheme = 'fill-purple' | 'fill-white' | 'fill-yellow' | 'fill-red' | 'transparent-purple' | 'transparent-white' | 'transparent-red' | 'transparent-black' | 'ghost-purple' | 'ghost-white' | 'ghost-red' | 'ghost-green' | 'outline-white' | 'outline-purple' | 'outline-red' | 'outline-green';
83
+ type UiTheme = 'fill-purple' | 'fill-white' | 'fill-yellow' | 'fill-red' | 'fill-green' | 'transparent-purple' | 'transparent-white' | 'transparent-red' | 'transparent-black' | 'ghost-purple' | 'ghost-white' | 'ghost-red' | 'ghost-green' | 'outline-white' | 'outline-purple' | 'outline-red' | 'outline-green';
83
84
 
84
85
  type IconSize$1 = '5' | '6' | '7' | '8' | '16';
85
86
  /**
@@ -108,15 +109,25 @@ declare class AvatarComponent {
108
109
  alt: _angular_core.InputSignal<string>;
109
110
  /** Show a skeleton placeholder instead of content while loading. */
110
111
  loading: _angular_core.InputSignal<boolean>;
112
+ /**
113
+ * Marks this avatar as a photo picker: when there's no photo to show, a
114
+ * default person icon fills in for initials/icon, and a dimmed overlay
115
+ * with a photo icon is layered on top to signal it's clickable — shown
116
+ * even once a photo is set, so the user can tell they can still change
117
+ * it, not just add one the first time. Purely visual — wrap the avatar
118
+ * in your own button/link to actually handle the click.
119
+ */
120
+ editable: _angular_core.InputSignal<boolean>;
111
121
  /** Internal signal to track image loading errors */
112
122
  protected readonly imageError: _angular_core.WritableSignal<boolean>;
123
+ protected readonly hasPhoto: _angular_core.Signal<boolean>;
113
124
  /** Computed icon size based on avatar size */
114
125
  readonly iconSize: _angular_core.Signal<IconSize$1>;
115
126
  constructor();
116
127
  /** Handles image loading errors */
117
128
  protected handleImageError(): void;
118
129
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<AvatarComponent, never>;
119
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<AvatarComponent, "ul-avatar", never, { "size": { "alias": "size"; "required": false; "isSignal": true; }; "src": { "alias": "src"; "required": false; "isSignal": true; }; "initials": { "alias": "initials"; "required": false; "isSignal": true; }; "icon": { "alias": "icon"; "required": false; "isSignal": true; }; "alt": { "alias": "alt"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
130
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<AvatarComponent, "ul-avatar", never, { "size": { "alias": "size"; "required": false; "isSignal": true; }; "src": { "alias": "src"; "required": false; "isSignal": true; }; "initials": { "alias": "initials"; "required": false; "isSignal": true; }; "icon": { "alias": "icon"; "required": false; "isSignal": true; }; "alt": { "alias": "alt"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "editable": { "alias": "editable"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
120
131
  }
121
132
 
122
133
  /** Icon size keys from design tokens (iconography.font.size). */
@@ -885,14 +896,19 @@ type ImageCropperShape = 'rectangle' | 'round';
885
896
  * third-party crop mechanics never leak outside the design system —
886
897
  * consumers only ever see `ul-image-cropper`.
887
898
  *
888
- * The frame is always what moves/resizes, never the photo: ngx-image-cropper
889
- * only bounds-checks that path (`checkCropperWithinMaxSizeBounds`) — its
890
- * alternative "drag the photo within a fixed frame" mode applies pan
891
- * offsets with no clamping at all, so the photo could be dragged clean out
892
- * of the frame. `aspectRatio` just decides whether that frame is locked to
893
- * a ratio (e.g. `1` for an avatar) or freely resizable (the default — for
894
- * listing photos, which keep their own shape today, see `um-image-upload`'s
895
- * auto-width carousel).
899
+ * Two interaction modes, chosen by whether `aspectRatio` is set:
900
+ * - **Locked** (`aspectRatio` given, e.g. `1` for an avatar): a fixed-size
901
+ * frame; the user drags/zooms the photo underneath it. ngx-image-cropper
902
+ * applies that pan with *no bounds checking at all* (unlike its
903
+ * frame-resize path, which does call `checkCropperWithinMaxSizeBounds`),
904
+ * so this component clamps translateH/translateV itself — see
905
+ * `clampPan()` — snapping the photo back to fully cover the frame the
906
+ * moment a drag would otherwise leave a gap.
907
+ * - **Free-form** (`aspectRatio` omitted, the default): a resizable frame
908
+ * over a static photo; the user drags its corners to trim/straighten
909
+ * with no ratio imposed. This path is already bounds-checked by the
910
+ * library itself. Best for listing photos, which keep their own shape
911
+ * today (see `um-image-upload`'s auto-width carousel).
896
912
  *
897
913
  * Emits a resized `File`, ready to hand to any upload call that accepts
898
914
  * one — this component only produces the file, it never uploads it.
@@ -922,7 +938,11 @@ declare class ImageCropperComponent {
922
938
  readonly imageFile: _angular_core.InputSignal<File | null>;
923
939
  /** Visual shape of the crop mask. */
924
940
  readonly shape: _angular_core.InputSignal<ImageCropperShape>;
925
- /** Locks the crop frame to this width/height ratio. Omit (or pass null) for a free-form frame. */
941
+ /**
942
+ * Locks the crop frame to this width/height ratio and switches to
943
+ * pan-and-zoom mode. Omit (or pass null) for a free-form resizable frame
944
+ * with no ratio imposed.
945
+ */
926
946
  readonly aspectRatio: _angular_core.InputSignal<number | null>;
927
947
  /** Longest side (px) of the final output image; scales down without distorting. */
928
948
  readonly maxOutputSize: _angular_core.InputSignal<number>;
@@ -930,6 +950,8 @@ declare class ImageCropperComponent {
930
950
  readonly title: _angular_core.InputSignal<string>;
931
951
  /** Label for the confirm button. */
932
952
  readonly confirmLabel: _angular_core.InputSignal<string>;
953
+ /** Label next to the zoom slider (locked-aspect mode only). */
954
+ readonly zoomLabel: _angular_core.InputSignal<string>;
933
955
  /** Accessible label for the crop frame. */
934
956
  readonly frameAriaLabel: _angular_core.InputSignal<string>;
935
957
  /** Emitted with the cropped, resized photo once the user confirms. */
@@ -944,13 +966,29 @@ declare class ImageCropperComponent {
944
966
  protected readonly isLocked: _angular_core.Signal<boolean>;
945
967
  /** ngx-image-cropper always wants a number here; unused while isLocked() is false. */
946
968
  protected readonly effectiveAspectRatio: _angular_core.Signal<number>;
947
- protected readonly minFrameDimension = 40;
969
+ protected readonly frameWidth: _angular_core.Signal<number | undefined>;
970
+ protected readonly frameHeight: _angular_core.Signal<number | undefined>;
971
+ protected readonly minZoom = 1;
972
+ protected readonly maxZoom = 3;
973
+ protected readonly zoomStep = 0.01;
974
+ protected readonly zoom: _angular_core.WritableSignal<number>;
948
975
  protected readonly cropperReady: _angular_core.WritableSignal<boolean>;
976
+ private readonly baseImageSize;
977
+ private readonly pan;
978
+ protected readonly clampedPan: _angular_core.Signal<{
979
+ h: number;
980
+ v: number;
981
+ }>;
982
+ protected readonly transform: _angular_core.Signal<ImageTransform>;
949
983
  constructor();
984
+ protected onTransformChange(event: ImageTransform): void;
985
+ protected onZoomInput(event: Event): void;
986
+ protected onCropperReady(size: Dimensions): void;
950
987
  protected onConfirm(): Promise<void>;
951
988
  protected onClosed(): void;
989
+ private clampPan;
952
990
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<ImageCropperComponent, never>;
953
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<ImageCropperComponent, "ul-image-cropper", never, { "open": { "alias": "open"; "required": false; "isSignal": true; }; "imageFile": { "alias": "imageFile"; "required": false; "isSignal": true; }; "shape": { "alias": "shape"; "required": false; "isSignal": true; }; "aspectRatio": { "alias": "aspectRatio"; "required": false; "isSignal": true; }; "maxOutputSize": { "alias": "maxOutputSize"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "confirmLabel": { "alias": "confirmLabel"; "required": false; "isSignal": true; }; "frameAriaLabel": { "alias": "frameAriaLabel"; "required": false; "isSignal": true; }; }, { "open": "openChange"; "cropped": "cropped"; "cancelled": "cancelled"; "loadFailed": "loadFailed"; }, never, never, true, never>;
991
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<ImageCropperComponent, "ul-image-cropper", never, { "open": { "alias": "open"; "required": false; "isSignal": true; }; "imageFile": { "alias": "imageFile"; "required": false; "isSignal": true; }; "shape": { "alias": "shape"; "required": false; "isSignal": true; }; "aspectRatio": { "alias": "aspectRatio"; "required": false; "isSignal": true; }; "maxOutputSize": { "alias": "maxOutputSize"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "confirmLabel": { "alias": "confirmLabel"; "required": false; "isSignal": true; }; "zoomLabel": { "alias": "zoomLabel"; "required": false; "isSignal": true; }; "frameAriaLabel": { "alias": "frameAriaLabel"; "required": false; "isSignal": true; }; }, { "open": "openChange"; "cropped": "cropped"; "cancelled": "cancelled"; "loadFailed": "loadFailed"; }, never, never, true, never>;
954
992
  }
955
993
 
956
994
  type InputType = 'text' | 'password' | 'email' | 'number' | 'tel' | 'url';