@tailng-ui/primitives 0.56.0 → 0.58.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.
Files changed (25) hide show
  1. package/package.json +2 -2
  2. package/src/lib/utility/file-upload/__tests__/tng-file-upload.test-helpers.d.ts +42 -0
  3. package/src/lib/utility/file-upload/__tests__/tng-file-upload.test-helpers.d.ts.map +1 -0
  4. package/src/lib/utility/file-upload/__tests__/tng-file-upload.test-helpers.js +118 -0
  5. package/src/lib/utility/file-upload/__tests__/tng-file-upload.test-helpers.js.map +1 -0
  6. package/src/lib/utility/file-upload/index.d.ts +3 -0
  7. package/src/lib/utility/file-upload/index.d.ts.map +1 -0
  8. package/src/lib/utility/file-upload/index.js +3 -0
  9. package/src/lib/utility/file-upload/index.js.map +1 -0
  10. package/src/lib/utility/file-upload/tng-file-upload.d.ts +64 -0
  11. package/src/lib/utility/file-upload/tng-file-upload.d.ts.map +1 -0
  12. package/src/lib/utility/file-upload/tng-file-upload.js +163 -0
  13. package/src/lib/utility/file-upload/tng-file-upload.js.map +1 -0
  14. package/src/lib/utility/file-upload/tng-file-upload.types.d.ts +60 -0
  15. package/src/lib/utility/file-upload/tng-file-upload.types.d.ts.map +1 -0
  16. package/src/lib/utility/file-upload/tng-file-upload.types.js +9 -0
  17. package/src/lib/utility/file-upload/tng-file-upload.types.js.map +1 -0
  18. package/src/lib/utility/file-upload/tng-file-upload.utils.d.ts +66 -0
  19. package/src/lib/utility/file-upload/tng-file-upload.utils.d.ts.map +1 -0
  20. package/src/lib/utility/file-upload/tng-file-upload.utils.js +143 -0
  21. package/src/lib/utility/file-upload/tng-file-upload.utils.js.map +1 -0
  22. package/src/lib/utility/index.d.ts +1 -0
  23. package/src/lib/utility/index.d.ts.map +1 -1
  24. package/src/lib/utility/index.js +1 -0
  25. package/src/lib/utility/index.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailng-ui/primitives",
3
- "version": "0.56.0",
3
+ "version": "0.58.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "peerDependencies": {
19
19
  "@angular/core": "^21.1.0",
20
- "@tailng-ui/cdk": "^0.42.0",
20
+ "@tailng-ui/cdk": "^0.43.0",
21
21
  "tslib": "^2.3.0"
22
22
  },
23
23
  "sideEffects": false
@@ -0,0 +1,42 @@
1
+ import type { ElementRef } from '@angular/core';
2
+ import { TngFileUploadDirective } from '../tng-file-upload';
3
+ import type { TngFileUploadDragState, TngFileUploadRejectedEvent, TngFileUploadSelectedEvent } from '../tng-file-upload.types';
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * Host component exercising the directive with bindable inputs and output
7
+ * collectors. Provides existing class / attribute / a11y metadata so tests can
8
+ * confirm the directive preserves consumer state.
9
+ */
10
+ export declare class FileUploadHostComponent {
11
+ readonly accept: import("@angular/core").WritableSignal<string | readonly string[] | null | undefined>;
12
+ readonly multiple: import("@angular/core").WritableSignal<string | boolean>;
13
+ readonly maxSize: import("@angular/core").WritableSignal<number | null | undefined>;
14
+ readonly disabled: import("@angular/core").WritableSignal<string | boolean>;
15
+ readonly selectedEvents: TngFileUploadSelectedEvent[];
16
+ readonly rejectedEvents: TngFileUploadRejectedEvent[];
17
+ readonly dragStates: TngFileUploadDragState[];
18
+ zone: ElementRef<HTMLDivElement>;
19
+ directive: TngFileUploadDirective;
20
+ static ɵfac: i0.ɵɵFactoryDeclaration<FileUploadHostComponent, never>;
21
+ static ɵcmp: i0.ɵɵComponentDeclaration<FileUploadHostComponent, "ng-component", never, {}, {}, never, never, true, never>;
22
+ }
23
+ export type FileUploadFixture = {
24
+ readonly component: FileUploadHostComponent;
25
+ readonly element: HTMLDivElement;
26
+ readonly detectChanges: () => void;
27
+ };
28
+ export declare function createFileUploadFixture(): FileUploadFixture;
29
+ /** Build a `File` of an exact byte size with the given name and MIME type. */
30
+ export declare function makeFile(name: string, type?: string, size?: number): File;
31
+ /**
32
+ * Dispatch a drag-family event on the element with an optional list of files.
33
+ * Uses a plain `Event` with a structurally-shaped `dataTransfer` so it works in
34
+ * jsdom (which lacks `DragEvent`/`DataTransfer`). Returns the dispatched event
35
+ * so callers can assert on `defaultPrevented`.
36
+ *
37
+ * Pass `files` as `null` to omit `dataTransfer` entirely.
38
+ */
39
+ export declare function dispatchDrag(element: HTMLElement, type: 'dragenter' | 'dragover' | 'dragleave' | 'drop', files?: readonly File[] | null): Event;
40
+ /** Dispatch a drop event whose `dataTransfer.files` contains a non-file entry. */
41
+ export declare function dispatchDropWithNonFile(element: HTMLElement): Event;
42
+ //# sourceMappingURL=tng-file-upload.test-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tng-file-upload.test-helpers.d.ts","sourceRoot":"","sources":["../../../../../../../../../libs/tailng-ui/primitives/src/lib/utility/file-upload/__tests__/tng-file-upload.test-helpers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EACV,sBAAsB,EACtB,0BAA0B,EAC1B,0BAA0B,EAC3B,MAAM,0BAA0B,CAAC;;AAElC;;;;GAIG;AACH,qBAuBa,uBAAuB;IAClC,SAAgB,MAAM,wFAAoE;IAC1F,SAAgB,QAAQ,2DAAmC;IAC3D,SAAgB,OAAO,oEAA2C;IAClE,SAAgB,QAAQ,2DAAmC;IAE3D,SAAgB,cAAc,EAAE,0BAA0B,EAAE,CAAM;IAClE,SAAgB,cAAc,EAAE,0BAA0B,EAAE,CAAM;IAClE,SAAgB,UAAU,EAAE,sBAAsB,EAAE,CAAM;IAGnD,IAAI,EAAG,UAAU,CAAC,cAAc,CAAC,CAAC;IAGlC,SAAS,EAAG,sBAAsB,CAAC;yCAd/B,uBAAuB;2CAAvB,uBAAuB;CAenC;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,SAAS,EAAE,uBAAuB,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,aAAa,EAAE,MAAM,IAAI,CAAC;CACpC,CAAC;AAEF,wBAAgB,uBAAuB,IAAI,iBAAiB,CAY3D;AAED,8EAA8E;AAC9E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,SAAK,EAAE,IAAI,SAAI,GAAG,IAAI,CAEhE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,WAAW,EACpB,IAAI,EAAE,WAAW,GAAG,UAAU,GAAG,WAAW,GAAG,MAAM,EACrD,KAAK,CAAC,EAAE,SAAS,IAAI,EAAE,GAAG,IAAI,GAC7B,KAAK,CAYP;AAED,kFAAkF;AAClF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,WAAW,GAAG,KAAK,CAQnE"}
@@ -0,0 +1,118 @@
1
+ import { Component, ViewChild, signal } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { TngFileUploadDirective } from '../tng-file-upload';
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * Host component exercising the directive with bindable inputs and output
7
+ * collectors. Provides existing class / attribute / a11y metadata so tests can
8
+ * confirm the directive preserves consumer state.
9
+ */
10
+ export class FileUploadHostComponent {
11
+ accept = signal(undefined, ...(ngDevMode ? [{ debugName: "accept" }] : []));
12
+ multiple = signal(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
13
+ maxSize = signal(null, ...(ngDevMode ? [{ debugName: "maxSize" }] : []));
14
+ disabled = signal(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
15
+ selectedEvents = [];
16
+ rejectedEvents = [];
17
+ dragStates = [];
18
+ zone;
19
+ directive;
20
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: FileUploadHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
21
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.1", type: FileUploadHostComponent, isStandalone: true, selector: "ng-component", viewQueries: [{ propertyName: "zone", first: true, predicate: ["zone"], descendants: true, static: true }, { propertyName: "directive", first: true, predicate: TngFileUploadDirective, descendants: true, static: true }], ngImport: i0, template: `
22
+ <div
23
+ #zone
24
+ tngFileUpload
25
+ [accept]="accept()"
26
+ [multiple]="multiple()"
27
+ [maxSize]="maxSize()"
28
+ [disabled]="disabled()"
29
+ (filesSelected)="selectedEvents.push($event)"
30
+ (filesRejected)="rejectedEvents.push($event)"
31
+ (dragStateChange)="dragStates.push($event)"
32
+ class="existing-class extra-class"
33
+ data-consumer="keep-me"
34
+ role="button"
35
+ tabindex="3"
36
+ aria-label="Upload files"
37
+ >
38
+ Drop files here
39
+ </div>
40
+ `, isInline: true, dependencies: [{ kind: "directive", type: TngFileUploadDirective, selector: "[tngFileUpload]", inputs: ["accept", "multiple", "maxSize", "disabled"], outputs: ["filesSelected", "filesRejected", "dragStateChange"], exportAs: ["tngFileUpload"] }] });
41
+ }
42
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: FileUploadHostComponent, decorators: [{
43
+ type: Component,
44
+ args: [{
45
+ imports: [TngFileUploadDirective],
46
+ template: `
47
+ <div
48
+ #zone
49
+ tngFileUpload
50
+ [accept]="accept()"
51
+ [multiple]="multiple()"
52
+ [maxSize]="maxSize()"
53
+ [disabled]="disabled()"
54
+ (filesSelected)="selectedEvents.push($event)"
55
+ (filesRejected)="rejectedEvents.push($event)"
56
+ (dragStateChange)="dragStates.push($event)"
57
+ class="existing-class extra-class"
58
+ data-consumer="keep-me"
59
+ role="button"
60
+ tabindex="3"
61
+ aria-label="Upload files"
62
+ >
63
+ Drop files here
64
+ </div>
65
+ `,
66
+ }]
67
+ }], propDecorators: { zone: [{
68
+ type: ViewChild,
69
+ args: ['zone', { static: true }]
70
+ }], directive: [{
71
+ type: ViewChild,
72
+ args: [TngFileUploadDirective, { static: true }]
73
+ }] } });
74
+ export function createFileUploadFixture() {
75
+ const fixture = TestBed.configureTestingModule({
76
+ imports: [FileUploadHostComponent],
77
+ }).createComponent(FileUploadHostComponent);
78
+ fixture.detectChanges();
79
+ return {
80
+ component: fixture.componentInstance,
81
+ element: fixture.componentInstance.zone.nativeElement,
82
+ detectChanges: () => fixture.detectChanges(),
83
+ };
84
+ }
85
+ /** Build a `File` of an exact byte size with the given name and MIME type. */
86
+ export function makeFile(name, type = '', size = 8) {
87
+ return new File([new Uint8Array(Math.max(0, size))], name, { type });
88
+ }
89
+ /**
90
+ * Dispatch a drag-family event on the element with an optional list of files.
91
+ * Uses a plain `Event` with a structurally-shaped `dataTransfer` so it works in
92
+ * jsdom (which lacks `DragEvent`/`DataTransfer`). Returns the dispatched event
93
+ * so callers can assert on `defaultPrevented`.
94
+ *
95
+ * Pass `files` as `null` to omit `dataTransfer` entirely.
96
+ */
97
+ export function dispatchDrag(element, type, files) {
98
+ const event = new Event(type, { bubbles: true, cancelable: true });
99
+ if (files !== null) {
100
+ Object.defineProperty(event, 'dataTransfer', {
101
+ configurable: true,
102
+ value: { files: files ?? [], types: ['Files'] },
103
+ });
104
+ }
105
+ element.dispatchEvent(event);
106
+ return event;
107
+ }
108
+ /** Dispatch a drop event whose `dataTransfer.files` contains a non-file entry. */
109
+ export function dispatchDropWithNonFile(element) {
110
+ const event = new Event('drop', { bubbles: true, cancelable: true });
111
+ Object.defineProperty(event, 'dataTransfer', {
112
+ configurable: true,
113
+ value: { files: [{ notAFile: true }], types: ['Files'] },
114
+ });
115
+ element.dispatchEvent(event);
116
+ return event;
117
+ }
118
+ //# sourceMappingURL=tng-file-upload.test-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tng-file-upload.test-helpers.js","sourceRoot":"","sources":["../../../../../../../../../libs/tailng-ui/primitives/src/lib/utility/file-upload/__tests__/tng-file-upload.test-helpers.ts"],"names":[],"mappings":"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","sourcesContent":["import { Component, ViewChild, signal } from '@angular/core';\nimport type { ElementRef } from '@angular/core';\nimport { TestBed } from '@angular/core/testing';\n\nimport { TngFileUploadDirective } from '../tng-file-upload';\nimport type {\n TngFileUploadDragState,\n TngFileUploadRejectedEvent,\n TngFileUploadSelectedEvent,\n} from '../tng-file-upload.types';\n\n/**\n * Host component exercising the directive with bindable inputs and output\n * collectors. Provides existing class / attribute / a11y metadata so tests can\n * confirm the directive preserves consumer state.\n */\n@Component({\n imports: [TngFileUploadDirective],\n template: `\n <div\n #zone\n tngFileUpload\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [maxSize]=\"maxSize()\"\n [disabled]=\"disabled()\"\n (filesSelected)=\"selectedEvents.push($event)\"\n (filesRejected)=\"rejectedEvents.push($event)\"\n (dragStateChange)=\"dragStates.push($event)\"\n class=\"existing-class extra-class\"\n data-consumer=\"keep-me\"\n role=\"button\"\n tabindex=\"3\"\n aria-label=\"Upload files\"\n >\n Drop files here\n </div>\n `,\n})\nexport class FileUploadHostComponent {\n public readonly accept = signal<string | readonly string[] | null | undefined>(undefined);\n public readonly multiple = signal<boolean | string>(false);\n public readonly maxSize = signal<number | null | undefined>(null);\n public readonly disabled = signal<boolean | string>(false);\n\n public readonly selectedEvents: TngFileUploadSelectedEvent[] = [];\n public readonly rejectedEvents: TngFileUploadRejectedEvent[] = [];\n public readonly dragStates: TngFileUploadDragState[] = [];\n\n @ViewChild('zone', { static: true })\n public zone!: ElementRef<HTMLDivElement>;\n\n @ViewChild(TngFileUploadDirective, { static: true })\n public directive!: TngFileUploadDirective;\n}\n\nexport type FileUploadFixture = {\n readonly component: FileUploadHostComponent;\n readonly element: HTMLDivElement;\n readonly detectChanges: () => void;\n};\n\nexport function createFileUploadFixture(): FileUploadFixture {\n const fixture = TestBed.configureTestingModule({\n imports: [FileUploadHostComponent],\n }).createComponent(FileUploadHostComponent);\n\n fixture.detectChanges();\n\n return {\n component: fixture.componentInstance,\n element: fixture.componentInstance.zone.nativeElement,\n detectChanges: () => fixture.detectChanges(),\n };\n}\n\n/** Build a `File` of an exact byte size with the given name and MIME type. */\nexport function makeFile(name: string, type = '', size = 8): File {\n return new File([new Uint8Array(Math.max(0, size))], name, { type });\n}\n\n/**\n * Dispatch a drag-family event on the element with an optional list of files.\n * Uses a plain `Event` with a structurally-shaped `dataTransfer` so it works in\n * jsdom (which lacks `DragEvent`/`DataTransfer`). Returns the dispatched event\n * so callers can assert on `defaultPrevented`.\n *\n * Pass `files` as `null` to omit `dataTransfer` entirely.\n */\nexport function dispatchDrag(\n element: HTMLElement,\n type: 'dragenter' | 'dragover' | 'dragleave' | 'drop',\n files?: readonly File[] | null,\n): Event {\n const event = new Event(type, { bubbles: true, cancelable: true });\n\n if (files !== null) {\n Object.defineProperty(event, 'dataTransfer', {\n configurable: true,\n value: { files: files ?? [], types: ['Files'] },\n });\n }\n\n element.dispatchEvent(event);\n return event;\n}\n\n/** Dispatch a drop event whose `dataTransfer.files` contains a non-file entry. */\nexport function dispatchDropWithNonFile(element: HTMLElement): Event {\n const event = new Event('drop', { bubbles: true, cancelable: true });\n Object.defineProperty(event, 'dataTransfer', {\n configurable: true,\n value: { files: [{ notAFile: true }], types: ['Files'] },\n });\n element.dispatchEvent(event);\n return event;\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export * from './tng-file-upload';
2
+ export * from './tng-file-upload.types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../../../libs/tailng-ui/primitives/src/lib/utility/file-upload/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './tng-file-upload';
2
+ export * from './tng-file-upload.types';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../../../libs/tailng-ui/primitives/src/lib/utility/file-upload/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC","sourcesContent":["export * from './tng-file-upload';\nexport * from './tng-file-upload.types';\n"]}
@@ -0,0 +1,64 @@
1
+ import type { TngFileUploadDragState, TngFileUploadRejectedEvent, TngFileUploadSelectedEvent } from './tng-file-upload.types';
2
+ import { type TngFileUploadAcceptInput, type TngFileUploadMaxSizeInput } from './tng-file-upload.utils';
3
+ import * as i0 from "@angular/core";
4
+ /**
5
+ * Headless drag-and-drop file selection directive.
6
+ *
7
+ * Attach `tngFileUpload` to any section-like host element (`div`, `section`,
8
+ * `label`, ...) to turn it into a drop zone. The directive is unstyled and only
9
+ * concerns itself with drag/drop handling, validation, and state reflection via
10
+ * data attributes and outputs. It never sets a `role`, `tabindex`, or ARIA
11
+ * attributes, so any consumer-provided accessibility metadata is preserved.
12
+ *
13
+ * @example
14
+ * ```html
15
+ * <div
16
+ * tngFileUpload
17
+ * [accept]="'.png,.jpg,.jpeg,.pdf'"
18
+ * [multiple]="true"
19
+ * [maxSize]="5 * 1024 * 1024"
20
+ * (filesSelected)="onFilesSelected($event)"
21
+ * (filesRejected)="onFilesRejected($event)"
22
+ * (dragStateChange)="onDragStateChange($event)"
23
+ * >
24
+ * Drop files here
25
+ * </div>
26
+ * ```
27
+ */
28
+ export declare class TngFileUploadDirective {
29
+ /** Accepted file types (extensions, exact MIME types, or wildcard MIME groups). */
30
+ readonly accept: import("@angular/core").InputSignalWithTransform<readonly string[], TngFileUploadAcceptInput>;
31
+ /** Whether more than one file may be selected at once. */
32
+ readonly multiple: import("@angular/core").InputSignalWithTransform<boolean, string | boolean>;
33
+ /** Maximum allowed file size in bytes. `null` (the default) means no limit. */
34
+ readonly maxSize: import("@angular/core").InputSignalWithTransform<number | null, TngFileUploadMaxSizeInput>;
35
+ /** Whether the drop zone is disabled. Disabled drop zones are fully passive. */
36
+ readonly disabled: import("@angular/core").InputSignalWithTransform<boolean, string | boolean>;
37
+ /** Emits the accepted files (and their source) when valid files are selected. */
38
+ readonly filesSelected: import("@angular/core").OutputEmitterRef<TngFileUploadSelectedEvent>;
39
+ /** Emits the rejected files (with reasons) when files fail validation. */
40
+ readonly filesRejected: import("@angular/core").OutputEmitterRef<TngFileUploadRejectedEvent>;
41
+ /** Emits whenever the drag interaction state changes. */
42
+ readonly dragStateChange: import("@angular/core").OutputEmitterRef<TngFileUploadDragState>;
43
+ /** Reactive drag state used to drive host attribute reflection. */
44
+ readonly dragState: import("@angular/core").WritableSignal<TngFileUploadDragState>;
45
+ /**
46
+ * Tracks nested drag enter/leave pairs so dragging over child elements does
47
+ * not prematurely reset the drag state.
48
+ */
49
+ private dragDepth;
50
+ protected readonly dataFileUploadAttr = "";
51
+ protected get dataDraggingAttr(): '' | null;
52
+ protected get dataRejectedAttr(): '' | null;
53
+ protected get dataDisabledAttr(): '' | null;
54
+ protected onDragEnter(...args: readonly unknown[]): void;
55
+ protected onDragOver(...args: readonly unknown[]): void;
56
+ protected onDragLeave(...args: readonly unknown[]): void;
57
+ protected onDrop(...args: readonly unknown[]): void;
58
+ private toDomEvent;
59
+ private setDragState;
60
+ private handleFiles;
61
+ static ɵfac: i0.ɵɵFactoryDeclaration<TngFileUploadDirective, never>;
62
+ static ɵdir: i0.ɵɵDirectiveDeclaration<TngFileUploadDirective, "[tngFileUpload]", ["tngFileUpload"], { "accept": { "alias": "accept"; "required": false; "isSignal": true; }; "multiple": { "alias": "multiple"; "required": false; "isSignal": true; }; "maxSize": { "alias": "maxSize"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; }, { "filesSelected": "filesSelected"; "filesRejected": "filesRejected"; "dragStateChange": "dragStateChange"; }, never, never, true, never>;
63
+ }
64
+ //# sourceMappingURL=tng-file-upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tng-file-upload.d.ts","sourceRoot":"","sources":["../../../../../../../../libs/tailng-ui/primitives/src/lib/utility/file-upload/tng-file-upload.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,sBAAsB,EACtB,0BAA0B,EAC1B,0BAA0B,EAE3B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAKL,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC/B,MAAM,yBAAyB,CAAC;;AAEjC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAIa,sBAAsB;IACjC,mFAAmF;IACnF,SAAgB,MAAM,gGAEnB;IAEH,0DAA0D;IAC1D,SAAgB,QAAQ,8EAErB;IAEH,+EAA+E;IAC/E,SAAgB,OAAO,6FAEpB;IAEH,gFAAgF;IAChF,SAAgB,QAAQ,8EAErB;IAEH,iFAAiF;IACjF,SAAgB,aAAa,uEAAwC;IAErE,0EAA0E;IAC1E,SAAgB,aAAa,uEAAwC;IAErE,yDAAyD;IACzD,SAAgB,eAAe,mEAAoC;IAEnE,mEAAmE;IACnE,SAAgB,SAAS,iEAA0C;IAEnE;;;OAGG;IACH,OAAO,CAAC,SAAS,CAAK;IAGtB,SAAS,CAAC,QAAQ,CAAC,kBAAkB,MAAM;IAG3C,SAAS,KAAK,gBAAgB,IAAI,EAAE,GAAG,IAAI,CAE1C;IAGD,SAAS,KAAK,gBAAgB,IAAI,EAAE,GAAG,IAAI,CAE1C;IAGD,SAAS,KAAK,gBAAgB,IAAI,EAAE,GAAG,IAAI,CAE1C;IAGD,SAAS,CAAC,WAAW,CAAC,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI;IAYxD,SAAS,CAAC,UAAU,CAAC,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI;IAcvD,SAAS,CAAC,WAAW,CAAC,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI;IAexD,SAAS,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI;IAYnD,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,WAAW;yCA7HR,sBAAsB;2CAAtB,sBAAsB;CAgJlC"}
@@ -0,0 +1,163 @@
1
+ import { Directive, HostBinding, HostListener, booleanAttribute, input, output, signal, } from '@angular/core';
2
+ import { coerceTngFileUploadMaxSize, extractTngFileUploadFiles, normalizeTngFileUploadAccept, validateTngFileUploadFiles, } from './tng-file-upload.utils';
3
+ import * as i0 from "@angular/core";
4
+ /**
5
+ * Headless drag-and-drop file selection directive.
6
+ *
7
+ * Attach `tngFileUpload` to any section-like host element (`div`, `section`,
8
+ * `label`, ...) to turn it into a drop zone. The directive is unstyled and only
9
+ * concerns itself with drag/drop handling, validation, and state reflection via
10
+ * data attributes and outputs. It never sets a `role`, `tabindex`, or ARIA
11
+ * attributes, so any consumer-provided accessibility metadata is preserved.
12
+ *
13
+ * @example
14
+ * ```html
15
+ * <div
16
+ * tngFileUpload
17
+ * [accept]="'.png,.jpg,.jpeg,.pdf'"
18
+ * [multiple]="true"
19
+ * [maxSize]="5 * 1024 * 1024"
20
+ * (filesSelected)="onFilesSelected($event)"
21
+ * (filesRejected)="onFilesRejected($event)"
22
+ * (dragStateChange)="onDragStateChange($event)"
23
+ * >
24
+ * Drop files here
25
+ * </div>
26
+ * ```
27
+ */
28
+ export class TngFileUploadDirective {
29
+ /** Accepted file types (extensions, exact MIME types, or wildcard MIME groups). */
30
+ accept = input([], { ...(ngDevMode ? { debugName: "accept" } : {}), transform: normalizeTngFileUploadAccept });
31
+ /** Whether more than one file may be selected at once. */
32
+ multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : {}), transform: booleanAttribute });
33
+ /** Maximum allowed file size in bytes. `null` (the default) means no limit. */
34
+ maxSize = input(null, { ...(ngDevMode ? { debugName: "maxSize" } : {}), transform: coerceTngFileUploadMaxSize });
35
+ /** Whether the drop zone is disabled. Disabled drop zones are fully passive. */
36
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : {}), transform: booleanAttribute });
37
+ /** Emits the accepted files (and their source) when valid files are selected. */
38
+ filesSelected = output();
39
+ /** Emits the rejected files (with reasons) when files fail validation. */
40
+ filesRejected = output();
41
+ /** Emits whenever the drag interaction state changes. */
42
+ dragStateChange = output();
43
+ /** Reactive drag state used to drive host attribute reflection. */
44
+ dragState = signal('idle', ...(ngDevMode ? [{ debugName: "dragState" }] : []));
45
+ /**
46
+ * Tracks nested drag enter/leave pairs so dragging over child elements does
47
+ * not prematurely reset the drag state.
48
+ */
49
+ dragDepth = 0;
50
+ dataFileUploadAttr = '';
51
+ get dataDraggingAttr() {
52
+ return this.dragState() === 'dragging' ? '' : null;
53
+ }
54
+ get dataRejectedAttr() {
55
+ return this.dragState() === 'rejected' ? '' : null;
56
+ }
57
+ get dataDisabledAttr() {
58
+ return this.disabled() ? '' : null;
59
+ }
60
+ onDragEnter(...args) {
61
+ const event = this.toDomEvent(args);
62
+ if (event === null || this.disabled()) {
63
+ return;
64
+ }
65
+ event.preventDefault();
66
+ this.dragDepth += 1;
67
+ this.setDragState('dragging');
68
+ }
69
+ onDragOver(...args) {
70
+ const event = this.toDomEvent(args);
71
+ if (event === null || this.disabled()) {
72
+ return;
73
+ }
74
+ event.preventDefault();
75
+ if (this.dragDepth === 0) {
76
+ this.dragDepth = 1;
77
+ }
78
+ this.setDragState('dragging');
79
+ }
80
+ onDragLeave(...args) {
81
+ const event = this.toDomEvent(args);
82
+ if (event === null || this.disabled()) {
83
+ return;
84
+ }
85
+ if (this.dragDepth > 0) {
86
+ this.dragDepth -= 1;
87
+ }
88
+ if (this.dragDepth === 0) {
89
+ this.setDragState('idle');
90
+ }
91
+ }
92
+ onDrop(...args) {
93
+ const event = this.toDomEvent(args);
94
+ if (event === null || this.disabled()) {
95
+ return;
96
+ }
97
+ event.preventDefault();
98
+ this.dragDepth = 0;
99
+ this.setDragState('idle');
100
+ this.handleFiles(extractTngFileUploadFiles(event), 'drop');
101
+ }
102
+ toDomEvent(args) {
103
+ const [event] = args;
104
+ return event instanceof Event ? event : null;
105
+ }
106
+ setDragState(next) {
107
+ if (this.dragState() === next) {
108
+ return;
109
+ }
110
+ this.dragState.set(next);
111
+ this.dragStateChange.emit(next);
112
+ }
113
+ handleFiles(files, source) {
114
+ if (files.length === 0) {
115
+ return;
116
+ }
117
+ const { accepted, rejected } = validateTngFileUploadFiles(files, {
118
+ accept: this.accept(),
119
+ maxSize: this.maxSize(),
120
+ multiple: this.multiple(),
121
+ });
122
+ if (accepted.length > 0) {
123
+ this.filesSelected.emit({ files: accepted, source });
124
+ }
125
+ if (rejected.length > 0) {
126
+ this.filesRejected.emit({ rejected, accepted, source });
127
+ }
128
+ }
129
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: TngFileUploadDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
130
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.1", type: TngFileUploadDirective, isStandalone: true, selector: "[tngFileUpload]", inputs: { accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filesSelected: "filesSelected", filesRejected: "filesRejected", dragStateChange: "dragStateChange" }, host: { listeners: { "dragenter": "onDragEnter($event)", "dragover": "onDragOver($event)", "dragleave": "onDragLeave($event)", "drop": "onDrop($event)" }, properties: { "attr.data-file-upload": "this.dataFileUploadAttr", "attr.data-dragging": "this.dataDraggingAttr", "attr.data-rejected": "this.dataRejectedAttr", "attr.data-disabled": "this.dataDisabledAttr" } }, exportAs: ["tngFileUpload"], ngImport: i0 });
131
+ }
132
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: TngFileUploadDirective, decorators: [{
133
+ type: Directive,
134
+ args: [{
135
+ selector: '[tngFileUpload]',
136
+ exportAs: 'tngFileUpload',
137
+ }]
138
+ }], propDecorators: { accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], filesSelected: [{ type: i0.Output, args: ["filesSelected"] }], filesRejected: [{ type: i0.Output, args: ["filesRejected"] }], dragStateChange: [{ type: i0.Output, args: ["dragStateChange"] }], dataFileUploadAttr: [{
139
+ type: HostBinding,
140
+ args: ['attr.data-file-upload']
141
+ }], dataDraggingAttr: [{
142
+ type: HostBinding,
143
+ args: ['attr.data-dragging']
144
+ }], dataRejectedAttr: [{
145
+ type: HostBinding,
146
+ args: ['attr.data-rejected']
147
+ }], dataDisabledAttr: [{
148
+ type: HostBinding,
149
+ args: ['attr.data-disabled']
150
+ }], onDragEnter: [{
151
+ type: HostListener,
152
+ args: ['dragenter', ['$event']]
153
+ }], onDragOver: [{
154
+ type: HostListener,
155
+ args: ['dragover', ['$event']]
156
+ }], onDragLeave: [{
157
+ type: HostListener,
158
+ args: ['dragleave', ['$event']]
159
+ }], onDrop: [{
160
+ type: HostListener,
161
+ args: ['drop', ['$event']]
162
+ }] } });
163
+ //# sourceMappingURL=tng-file-upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tng-file-upload.js","sourceRoot":"","sources":["../../../../../../../../libs/tailng-ui/primitives/src/lib/utility/file-upload/tng-file-upload.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Directive,\n HostBinding,\n HostListener,\n booleanAttribute,\n input,\n output,\n signal,\n} from '@angular/core';\n\nimport type {\n TngFileUploadDragState,\n TngFileUploadRejectedEvent,\n TngFileUploadSelectedEvent,\n TngFileUploadSource,\n} from './tng-file-upload.types';\nimport {\n coerceTngFileUploadMaxSize,\n extractTngFileUploadFiles,\n normalizeTngFileUploadAccept,\n validateTngFileUploadFiles,\n type TngFileUploadAcceptInput,\n type TngFileUploadMaxSizeInput,\n} from './tng-file-upload.utils';\n\n/**\n * Headless drag-and-drop file selection directive.\n *\n * Attach `tngFileUpload` to any section-like host element (`div`, `section`,\n * `label`, ...) to turn it into a drop zone. The directive is unstyled and only\n * concerns itself with drag/drop handling, validation, and state reflection via\n * data attributes and outputs. It never sets a `role`, `tabindex`, or ARIA\n * attributes, so any consumer-provided accessibility metadata is preserved.\n *\n * @example\n * ```html\n * <div\n * tngFileUpload\n * [accept]=\"'.png,.jpg,.jpeg,.pdf'\"\n * [multiple]=\"true\"\n * [maxSize]=\"5 * 1024 * 1024\"\n * (filesSelected)=\"onFilesSelected($event)\"\n * (filesRejected)=\"onFilesRejected($event)\"\n * (dragStateChange)=\"onDragStateChange($event)\"\n * >\n * Drop files here\n * </div>\n * ```\n */\n@Directive({\n selector: '[tngFileUpload]',\n exportAs: 'tngFileUpload',\n})\nexport class TngFileUploadDirective {\n /** Accepted file types (extensions, exact MIME types, or wildcard MIME groups). */\n public readonly accept = input<readonly string[], TngFileUploadAcceptInput>([], {\n transform: normalizeTngFileUploadAccept,\n });\n\n /** Whether more than one file may be selected at once. */\n public readonly multiple = input<boolean, boolean | string>(false, {\n transform: booleanAttribute,\n });\n\n /** Maximum allowed file size in bytes. `null` (the default) means no limit. */\n public readonly maxSize = input<number | null, TngFileUploadMaxSizeInput>(null, {\n transform: coerceTngFileUploadMaxSize,\n });\n\n /** Whether the drop zone is disabled. Disabled drop zones are fully passive. */\n public readonly disabled = input<boolean, boolean | string>(false, {\n transform: booleanAttribute,\n });\n\n /** Emits the accepted files (and their source) when valid files are selected. */\n public readonly filesSelected = output<TngFileUploadSelectedEvent>();\n\n /** Emits the rejected files (with reasons) when files fail validation. */\n public readonly filesRejected = output<TngFileUploadRejectedEvent>();\n\n /** Emits whenever the drag interaction state changes. */\n public readonly dragStateChange = output<TngFileUploadDragState>();\n\n /** Reactive drag state used to drive host attribute reflection. */\n public readonly dragState = signal<TngFileUploadDragState>('idle');\n\n /**\n * Tracks nested drag enter/leave pairs so dragging over child elements does\n * not prematurely reset the drag state.\n */\n private dragDepth = 0;\n\n @HostBinding('attr.data-file-upload')\n protected readonly dataFileUploadAttr = '';\n\n @HostBinding('attr.data-dragging')\n protected get dataDraggingAttr(): '' | null {\n return this.dragState() === 'dragging' ? '' : null;\n }\n\n @HostBinding('attr.data-rejected')\n protected get dataRejectedAttr(): '' | null {\n return this.dragState() === 'rejected' ? '' : null;\n }\n\n @HostBinding('attr.data-disabled')\n protected get dataDisabledAttr(): '' | null {\n return this.disabled() ? '' : null;\n }\n\n @HostListener('dragenter', ['$event'])\n protected onDragEnter(...args: readonly unknown[]): void {\n const event = this.toDomEvent(args);\n if (event === null || this.disabled()) {\n return;\n }\n\n event.preventDefault();\n this.dragDepth += 1;\n this.setDragState('dragging');\n }\n\n @HostListener('dragover', ['$event'])\n protected onDragOver(...args: readonly unknown[]): void {\n const event = this.toDomEvent(args);\n if (event === null || this.disabled()) {\n return;\n }\n\n event.preventDefault();\n if (this.dragDepth === 0) {\n this.dragDepth = 1;\n }\n this.setDragState('dragging');\n }\n\n @HostListener('dragleave', ['$event'])\n protected onDragLeave(...args: readonly unknown[]): void {\n const event = this.toDomEvent(args);\n if (event === null || this.disabled()) {\n return;\n }\n\n if (this.dragDepth > 0) {\n this.dragDepth -= 1;\n }\n if (this.dragDepth === 0) {\n this.setDragState('idle');\n }\n }\n\n @HostListener('drop', ['$event'])\n protected onDrop(...args: readonly unknown[]): void {\n const event = this.toDomEvent(args);\n if (event === null || this.disabled()) {\n return;\n }\n\n event.preventDefault();\n this.dragDepth = 0;\n this.setDragState('idle');\n this.handleFiles(extractTngFileUploadFiles(event), 'drop');\n }\n\n private toDomEvent(args: readonly unknown[]): Event | null {\n const [event] = args;\n return event instanceof Event ? event : null;\n }\n\n private setDragState(next: TngFileUploadDragState): void {\n if (this.dragState() === next) {\n return;\n }\n\n this.dragState.set(next);\n this.dragStateChange.emit(next);\n }\n\n private handleFiles(files: readonly File[], source: TngFileUploadSource): void {\n if (files.length === 0) {\n return;\n }\n\n const { accepted, rejected } = validateTngFileUploadFiles(files, {\n accept: this.accept(),\n maxSize: this.maxSize(),\n multiple: this.multiple(),\n });\n\n if (accepted.length > 0) {\n this.filesSelected.emit({ files: accepted, source });\n }\n\n if (rejected.length > 0) {\n this.filesRejected.emit({ rejected, accepted, source });\n }\n }\n}\n"]}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Public type contracts for the `tngFileUpload` primitive directive.
3
+ *
4
+ * These types describe the data emitted by the directive's outputs and the
5
+ * drag state it reflects on the host element. They are intentionally framework
6
+ * agnostic so consumers can react to selection/rejection without depending on
7
+ * any internal helper types.
8
+ */
9
+ /**
10
+ * Drag interaction state reflected on the host while a drag/drop gesture is in
11
+ * progress.
12
+ *
13
+ * - `idle`: nothing is being dragged over the host.
14
+ * - `dragging`: a drag gesture is currently over the host.
15
+ * - `rejected`: reserved for implementations that surface a rejected drag state.
16
+ */
17
+ export type TngFileUploadDragState = 'idle' | 'dragging' | 'rejected';
18
+ /**
19
+ * Origin of a selection/rejection event. Currently only `drop` is emitted by
20
+ * the headless directive, but `input` is reserved for a future click-to-upload
21
+ * file picker so the event shape stays stable.
22
+ */
23
+ export type TngFileUploadSource = 'drop' | 'input';
24
+ /**
25
+ * Reason a file was rejected during validation.
26
+ *
27
+ * - `disabled`: the directive was disabled when the gesture happened.
28
+ * - `multiple`: extra files were dropped while `multiple` is false.
29
+ * - `type`: the file did not match the `accept` rules.
30
+ * - `size`: the file exceeded `maxSize`.
31
+ * - `empty`: reserved for empty/invalid file entries.
32
+ */
33
+ export type TngFileUploadRejectReason = 'disabled' | 'multiple' | 'type' | 'size' | 'empty';
34
+ /**
35
+ * A single rejected file paired with the reason it failed validation and a
36
+ * human-readable message describing the failure.
37
+ */
38
+ export type TngFileUploadRejectedFile = {
39
+ readonly file: File;
40
+ readonly reason: TngFileUploadRejectReason;
41
+ readonly message: string;
42
+ };
43
+ /**
44
+ * Payload emitted by `filesSelected` when one or more valid files are chosen.
45
+ */
46
+ export type TngFileUploadSelectedEvent = {
47
+ readonly files: readonly File[];
48
+ readonly source: TngFileUploadSource;
49
+ };
50
+ /**
51
+ * Payload emitted by `filesRejected` when one or more files fail validation.
52
+ * The accepted files from the same gesture are included for convenience so
53
+ * consumers can correlate the two outputs from a single drop.
54
+ */
55
+ export type TngFileUploadRejectedEvent = {
56
+ readonly rejected: readonly TngFileUploadRejectedFile[];
57
+ readonly accepted: readonly File[];
58
+ readonly source: TngFileUploadSource;
59
+ };
60
+ //# sourceMappingURL=tng-file-upload.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tng-file-upload.types.d.ts","sourceRoot":"","sources":["../../../../../../../../libs/tailng-ui/primitives/src/lib/utility/file-upload/tng-file-upload.types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;AAEtE;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE5F;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,yBAAyB,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;CACtC,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,yBAAyB,EAAE,CAAC;IACxD,QAAQ,CAAC,QAAQ,EAAE,SAAS,IAAI,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;CACtC,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Public type contracts for the `tngFileUpload` primitive directive.
3
+ *
4
+ * These types describe the data emitted by the directive's outputs and the
5
+ * drag state it reflects on the host element. They are intentionally framework
6
+ * agnostic so consumers can react to selection/rejection without depending on
7
+ * any internal helper types.
8
+ */
9
+ //# sourceMappingURL=tng-file-upload.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tng-file-upload.types.js","sourceRoot":"","sources":["../../../../../../../../libs/tailng-ui/primitives/src/lib/utility/file-upload/tng-file-upload.types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG","sourcesContent":["/**\n * Public type contracts for the `tngFileUpload` primitive directive.\n *\n * These types describe the data emitted by the directive's outputs and the\n * drag state it reflects on the host element. They are intentionally framework\n * agnostic so consumers can react to selection/rejection without depending on\n * any internal helper types.\n */\n\n/**\n * Drag interaction state reflected on the host while a drag/drop gesture is in\n * progress.\n *\n * - `idle`: nothing is being dragged over the host.\n * - `dragging`: a drag gesture is currently over the host.\n * - `rejected`: reserved for implementations that surface a rejected drag state.\n */\nexport type TngFileUploadDragState = 'idle' | 'dragging' | 'rejected';\n\n/**\n * Origin of a selection/rejection event. Currently only `drop` is emitted by\n * the headless directive, but `input` is reserved for a future click-to-upload\n * file picker so the event shape stays stable.\n */\nexport type TngFileUploadSource = 'drop' | 'input';\n\n/**\n * Reason a file was rejected during validation.\n *\n * - `disabled`: the directive was disabled when the gesture happened.\n * - `multiple`: extra files were dropped while `multiple` is false.\n * - `type`: the file did not match the `accept` rules.\n * - `size`: the file exceeded `maxSize`.\n * - `empty`: reserved for empty/invalid file entries.\n */\nexport type TngFileUploadRejectReason = 'disabled' | 'multiple' | 'type' | 'size' | 'empty';\n\n/**\n * A single rejected file paired with the reason it failed validation and a\n * human-readable message describing the failure.\n */\nexport type TngFileUploadRejectedFile = {\n readonly file: File;\n readonly reason: TngFileUploadRejectReason;\n readonly message: string;\n};\n\n/**\n * Payload emitted by `filesSelected` when one or more valid files are chosen.\n */\nexport type TngFileUploadSelectedEvent = {\n readonly files: readonly File[];\n readonly source: TngFileUploadSource;\n};\n\n/**\n * Payload emitted by `filesRejected` when one or more files fail validation.\n * The accepted files from the same gesture are included for convenience so\n * consumers can correlate the two outputs from a single drop.\n */\nexport type TngFileUploadRejectedEvent = {\n readonly rejected: readonly TngFileUploadRejectedFile[];\n readonly accepted: readonly File[];\n readonly source: TngFileUploadSource;\n};\n"]}
@@ -0,0 +1,66 @@
1
+ import type { TngFileUploadRejectedFile } from './tng-file-upload.types';
2
+ /** Accepted shapes for the `accept` input before normalization. */
3
+ export type TngFileUploadAcceptInput = string | readonly string[] | null | undefined;
4
+ /** Accepted shapes for the `maxSize` input before coercion. */
5
+ export type TngFileUploadMaxSizeInput = number | string | null | undefined;
6
+ /** Options consumed by {@link validateTngFileUploadFiles}. */
7
+ export type TngFileUploadValidationOptions = {
8
+ readonly accept: readonly string[];
9
+ readonly maxSize: number | null;
10
+ readonly multiple: boolean;
11
+ };
12
+ /** Result of running validation over a list of files. */
13
+ export type TngFileUploadValidationResult = {
14
+ readonly accepted: File[];
15
+ readonly rejected: TngFileUploadRejectedFile[];
16
+ };
17
+ /**
18
+ * Normalize the `accept` input into a deduplicated list of lower-cased,
19
+ * trimmed tokens. Accepts a single comma-separated string or an array of
20
+ * tokens (each of which may itself be comma-separated). Empty / whitespace
21
+ * only values produce an empty list, which means "no restriction".
22
+ */
23
+ export declare function normalizeTngFileUploadAccept(value: TngFileUploadAcceptInput): readonly string[];
24
+ /**
25
+ * Coerce the `maxSize` input into a positive byte count, or `null` when no
26
+ * size restriction should be applied. Zero, negative, and non-finite values
27
+ * are treated as "no restriction".
28
+ */
29
+ export declare function coerceTngFileUploadMaxSize(value: TngFileUploadMaxSizeInput): number | null;
30
+ /**
31
+ * Extract the lower-cased file extension (including the leading dot) from a
32
+ * file name, or an empty string when the name has no usable extension.
33
+ * Handles names with multiple dots by returning only the final segment, and
34
+ * treats dot-prefixed names without a real extension (e.g. `.gitignore`) as
35
+ * having no extension.
36
+ */
37
+ export declare function getTngFileUploadExtension(fileName: string): string;
38
+ /**
39
+ * Determine whether a file satisfies any of the normalized accept tokens.
40
+ * An empty token list means "no restriction" and always matches.
41
+ */
42
+ export declare function matchesTngFileUploadAccept(file: File, accept: readonly string[]): boolean;
43
+ /**
44
+ * Convert an unknown array-like value (typically a `FileList`) into a real
45
+ * array of `File` instances, ignoring any non-file entries. Returns an empty
46
+ * array for `null`/`undefined` or non array-like values.
47
+ */
48
+ export declare function normalizeTngFileUploadFiles(list: unknown): File[];
49
+ /**
50
+ * Pull the dropped files out of a DOM event's `dataTransfer`, defensively
51
+ * handling events that lack a `dataTransfer` or `files` collection. Uses
52
+ * structural access rather than `instanceof DragEvent`/`DataTransfer` so it
53
+ * works in environments (such as jsdom) where those globals are absent.
54
+ */
55
+ export declare function extractTngFileUploadFiles(event: Event): File[];
56
+ /**
57
+ * Validate a list of files against the accept, size, and multiplicity rules.
58
+ *
59
+ * Validation priority is deterministic: each file is checked for `type`, then
60
+ * `size`. Only files that pass both are considered for acceptance. When
61
+ * `multiple` is false, the first otherwise-valid file is accepted and any
62
+ * further otherwise-valid files are rejected with reason `multiple`. Original
63
+ * input order is preserved within both the accepted and rejected lists.
64
+ */
65
+ export declare function validateTngFileUploadFiles(files: readonly File[], options: TngFileUploadValidationOptions): TngFileUploadValidationResult;
66
+ //# sourceMappingURL=tng-file-upload.utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tng-file-upload.utils.d.ts","sourceRoot":"","sources":["../../../../../../../../libs/tailng-ui/primitives/src/lib/utility/file-upload/tng-file-upload.utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EAE1B,MAAM,yBAAyB,CAAC;AAEjC,mEAAmE;AACnE,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;AAErF,+DAA+D;AAC/D,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAE3E,8DAA8D;AAC9D,MAAM,MAAM,8BAA8B,GAAG;IAC3C,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,yDAAyD;AACzD,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,yBAAyB,EAAE,CAAC;CAChD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,wBAAwB,GAAG,SAAS,MAAM,EAAE,CAY/F;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,yBAAyB,GAAG,MAAM,GAAG,IAAI,CAW1F;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAOlE;AAwBD;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAMzF;AAMD;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,EAAE,CAiBjE;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,CAO9D;AAUD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,OAAO,EAAE,8BAA8B,GACtC,6BAA6B,CAoC/B"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Normalize the `accept` input into a deduplicated list of lower-cased,
3
+ * trimmed tokens. Accepts a single comma-separated string or an array of
4
+ * tokens (each of which may itself be comma-separated). Empty / whitespace
5
+ * only values produce an empty list, which means "no restriction".
6
+ */
7
+ export function normalizeTngFileUploadAccept(value) {
8
+ if (value === null || value === undefined) {
9
+ return [];
10
+ }
11
+ const rawTokens = typeof value === 'string' ? [value] : value;
12
+ const tokens = rawTokens
13
+ .flatMap((entry) => entry.split(','))
14
+ .map((token) => token.trim().toLowerCase())
15
+ .filter((token) => token.length > 0);
16
+ return [...new Set(tokens)];
17
+ }
18
+ /**
19
+ * Coerce the `maxSize` input into a positive byte count, or `null` when no
20
+ * size restriction should be applied. Zero, negative, and non-finite values
21
+ * are treated as "no restriction".
22
+ */
23
+ export function coerceTngFileUploadMaxSize(value) {
24
+ if (value === null || value === undefined) {
25
+ return null;
26
+ }
27
+ const numeric = typeof value === 'number' ? value : Number(value);
28
+ if (!Number.isFinite(numeric) || numeric <= 0) {
29
+ return null;
30
+ }
31
+ return numeric;
32
+ }
33
+ /**
34
+ * Extract the lower-cased file extension (including the leading dot) from a
35
+ * file name, or an empty string when the name has no usable extension.
36
+ * Handles names with multiple dots by returning only the final segment, and
37
+ * treats dot-prefixed names without a real extension (e.g. `.gitignore`) as
38
+ * having no extension.
39
+ */
40
+ export function getTngFileUploadExtension(fileName) {
41
+ const lastDot = fileName.lastIndexOf('.');
42
+ if (lastDot <= 0) {
43
+ return '';
44
+ }
45
+ return fileName.slice(lastDot).toLowerCase();
46
+ }
47
+ function tokenMatchesFile(token, file) {
48
+ const fileType = file.type.toLowerCase();
49
+ if (token.startsWith('.')) {
50
+ return getTngFileUploadExtension(file.name) === token;
51
+ }
52
+ if (token.endsWith('/*')) {
53
+ if (fileType.length === 0) {
54
+ return false;
55
+ }
56
+ const group = token.slice(0, token.length - 1);
57
+ return fileType.startsWith(group);
58
+ }
59
+ if (token.includes('/')) {
60
+ return fileType.length > 0 && fileType === token;
61
+ }
62
+ return false;
63
+ }
64
+ /**
65
+ * Determine whether a file satisfies any of the normalized accept tokens.
66
+ * An empty token list means "no restriction" and always matches.
67
+ */
68
+ export function matchesTngFileUploadAccept(file, accept) {
69
+ if (accept.length === 0) {
70
+ return true;
71
+ }
72
+ return accept.some((token) => tokenMatchesFile(token, file));
73
+ }
74
+ function isFile(value) {
75
+ return typeof File !== 'undefined' && value instanceof File;
76
+ }
77
+ /**
78
+ * Convert an unknown array-like value (typically a `FileList`) into a real
79
+ * array of `File` instances, ignoring any non-file entries. Returns an empty
80
+ * array for `null`/`undefined` or non array-like values.
81
+ */
82
+ export function normalizeTngFileUploadFiles(list) {
83
+ if (list === null || list === undefined) {
84
+ return [];
85
+ }
86
+ const arrayLike = list;
87
+ const length = typeof arrayLike.length === 'number' ? arrayLike.length : 0;
88
+ const files = [];
89
+ for (let index = 0; index < length; index += 1) {
90
+ const item = arrayLike[index];
91
+ if (isFile(item)) {
92
+ files.push(item);
93
+ }
94
+ }
95
+ return files;
96
+ }
97
+ /**
98
+ * Pull the dropped files out of a DOM event's `dataTransfer`, defensively
99
+ * handling events that lack a `dataTransfer` or `files` collection. Uses
100
+ * structural access rather than `instanceof DragEvent`/`DataTransfer` so it
101
+ * works in environments (such as jsdom) where those globals are absent.
102
+ */
103
+ export function extractTngFileUploadFiles(event) {
104
+ const dataTransfer = event.dataTransfer;
105
+ if (dataTransfer === null || dataTransfer === undefined) {
106
+ return [];
107
+ }
108
+ return normalizeTngFileUploadFiles(dataTransfer.files);
109
+ }
110
+ function rejectedFile(file, reason, message) {
111
+ return { file, reason, message };
112
+ }
113
+ /**
114
+ * Validate a list of files against the accept, size, and multiplicity rules.
115
+ *
116
+ * Validation priority is deterministic: each file is checked for `type`, then
117
+ * `size`. Only files that pass both are considered for acceptance. When
118
+ * `multiple` is false, the first otherwise-valid file is accepted and any
119
+ * further otherwise-valid files are rejected with reason `multiple`. Original
120
+ * input order is preserved within both the accepted and rejected lists.
121
+ */
122
+ export function validateTngFileUploadFiles(files, options) {
123
+ const { accept, maxSize, multiple } = options;
124
+ const accepted = [];
125
+ const rejected = [];
126
+ for (const file of files) {
127
+ if (!matchesTngFileUploadAccept(file, accept)) {
128
+ rejected.push(rejectedFile(file, 'type', `File "${file.name}" does not match the accepted file types.`));
129
+ continue;
130
+ }
131
+ if (maxSize !== null && file.size > maxSize) {
132
+ rejected.push(rejectedFile(file, 'size', `File "${file.name}" exceeds the maximum allowed size of ${maxSize} bytes.`));
133
+ continue;
134
+ }
135
+ if (!multiple && accepted.length >= 1) {
136
+ rejected.push(rejectedFile(file, 'multiple', `File "${file.name}" was rejected because only one file is allowed.`));
137
+ continue;
138
+ }
139
+ accepted.push(file);
140
+ }
141
+ return { accepted, rejected };
142
+ }
143
+ //# sourceMappingURL=tng-file-upload.utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tng-file-upload.utils.js","sourceRoot":"","sources":["../../../../../../../../libs/tailng-ui/primitives/src/lib/utility/file-upload/tng-file-upload.utils.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n TngFileUploadRejectedFile,\n TngFileUploadRejectReason,\n} from './tng-file-upload.types';\n\n/** Accepted shapes for the `accept` input before normalization. */\nexport type TngFileUploadAcceptInput = string | readonly string[] | null | undefined;\n\n/** Accepted shapes for the `maxSize` input before coercion. */\nexport type TngFileUploadMaxSizeInput = number | string | null | undefined;\n\n/** Options consumed by {@link validateTngFileUploadFiles}. */\nexport type TngFileUploadValidationOptions = {\n readonly accept: readonly string[];\n readonly maxSize: number | null;\n readonly multiple: boolean;\n};\n\n/** Result of running validation over a list of files. */\nexport type TngFileUploadValidationResult = {\n readonly accepted: File[];\n readonly rejected: TngFileUploadRejectedFile[];\n};\n\n/**\n * Normalize the `accept` input into a deduplicated list of lower-cased,\n * trimmed tokens. Accepts a single comma-separated string or an array of\n * tokens (each of which may itself be comma-separated). Empty / whitespace\n * only values produce an empty list, which means \"no restriction\".\n */\nexport function normalizeTngFileUploadAccept(value: TngFileUploadAcceptInput): readonly string[] {\n if (value === null || value === undefined) {\n return [];\n }\n\n const rawTokens = typeof value === 'string' ? [value] : value;\n const tokens = rawTokens\n .flatMap((entry) => entry.split(','))\n .map((token) => token.trim().toLowerCase())\n .filter((token) => token.length > 0);\n\n return [...new Set(tokens)];\n}\n\n/**\n * Coerce the `maxSize` input into a positive byte count, or `null` when no\n * size restriction should be applied. Zero, negative, and non-finite values\n * are treated as \"no restriction\".\n */\nexport function coerceTngFileUploadMaxSize(value: TngFileUploadMaxSizeInput): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n const numeric = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(numeric) || numeric <= 0) {\n return null;\n }\n\n return numeric;\n}\n\n/**\n * Extract the lower-cased file extension (including the leading dot) from a\n * file name, or an empty string when the name has no usable extension.\n * Handles names with multiple dots by returning only the final segment, and\n * treats dot-prefixed names without a real extension (e.g. `.gitignore`) as\n * having no extension.\n */\nexport function getTngFileUploadExtension(fileName: string): string {\n const lastDot = fileName.lastIndexOf('.');\n if (lastDot <= 0) {\n return '';\n }\n\n return fileName.slice(lastDot).toLowerCase();\n}\n\nfunction tokenMatchesFile(token: string, file: File): boolean {\n const fileType = file.type.toLowerCase();\n\n if (token.startsWith('.')) {\n return getTngFileUploadExtension(file.name) === token;\n }\n\n if (token.endsWith('/*')) {\n if (fileType.length === 0) {\n return false;\n }\n const group = token.slice(0, token.length - 1);\n return fileType.startsWith(group);\n }\n\n if (token.includes('/')) {\n return fileType.length > 0 && fileType === token;\n }\n\n return false;\n}\n\n/**\n * Determine whether a file satisfies any of the normalized accept tokens.\n * An empty token list means \"no restriction\" and always matches.\n */\nexport function matchesTngFileUploadAccept(file: File, accept: readonly string[]): boolean {\n if (accept.length === 0) {\n return true;\n }\n\n return accept.some((token) => tokenMatchesFile(token, file));\n}\n\nfunction isFile(value: unknown): value is File {\n return typeof File !== 'undefined' && value instanceof File;\n}\n\n/**\n * Convert an unknown array-like value (typically a `FileList`) into a real\n * array of `File` instances, ignoring any non-file entries. Returns an empty\n * array for `null`/`undefined` or non array-like values.\n */\nexport function normalizeTngFileUploadFiles(list: unknown): File[] {\n if (list === null || list === undefined) {\n return [];\n }\n\n const arrayLike = list as ArrayLike<unknown>;\n const length = typeof arrayLike.length === 'number' ? arrayLike.length : 0;\n\n const files: File[] = [];\n for (let index = 0; index < length; index += 1) {\n const item = arrayLike[index];\n if (isFile(item)) {\n files.push(item);\n }\n }\n\n return files;\n}\n\n/**\n * Pull the dropped files out of a DOM event's `dataTransfer`, defensively\n * handling events that lack a `dataTransfer` or `files` collection. Uses\n * structural access rather than `instanceof DragEvent`/`DataTransfer` so it\n * works in environments (such as jsdom) where those globals are absent.\n */\nexport function extractTngFileUploadFiles(event: Event): File[] {\n const dataTransfer = (event as { dataTransfer?: { files?: unknown } | null }).dataTransfer;\n if (dataTransfer === null || dataTransfer === undefined) {\n return [];\n }\n\n return normalizeTngFileUploadFiles(dataTransfer.files);\n}\n\nfunction rejectedFile(\n file: File,\n reason: TngFileUploadRejectReason,\n message: string,\n): TngFileUploadRejectedFile {\n return { file, reason, message };\n}\n\n/**\n * Validate a list of files against the accept, size, and multiplicity rules.\n *\n * Validation priority is deterministic: each file is checked for `type`, then\n * `size`. Only files that pass both are considered for acceptance. When\n * `multiple` is false, the first otherwise-valid file is accepted and any\n * further otherwise-valid files are rejected with reason `multiple`. Original\n * input order is preserved within both the accepted and rejected lists.\n */\nexport function validateTngFileUploadFiles(\n files: readonly File[],\n options: TngFileUploadValidationOptions,\n): TngFileUploadValidationResult {\n const { accept, maxSize, multiple } = options;\n\n const accepted: File[] = [];\n const rejected: TngFileUploadRejectedFile[] = [];\n\n for (const file of files) {\n if (!matchesTngFileUploadAccept(file, accept)) {\n rejected.push(\n rejectedFile(file, 'type', `File \"${file.name}\" does not match the accepted file types.`),\n );\n continue;\n }\n\n if (maxSize !== null && file.size > maxSize) {\n rejected.push(\n rejectedFile(\n file,\n 'size',\n `File \"${file.name}\" exceeds the maximum allowed size of ${maxSize} bytes.`,\n ),\n );\n continue;\n }\n\n if (!multiple && accepted.length >= 1) {\n rejected.push(\n rejectedFile(file, 'multiple', `File \"${file.name}\" was rejected because only one file is allowed.`),\n );\n continue;\n }\n\n accepted.push(file);\n }\n\n return { accepted, rejected };\n}\n"]}
@@ -3,5 +3,6 @@ export * from './badge/tng-badge';
3
3
  export * from './press/tng-press';
4
4
  export * from './code-block/tng-code-block';
5
5
  export * from './copy/tng-copy';
6
+ export * from './file-upload';
6
7
  export * from './tag/tng-tag';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../../libs/tailng-ui/primitives/src/lib/utility/index.ts"],"names":[],"mappings":"AACA,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../../libs/tailng-ui/primitives/src/lib/utility/index.ts"],"names":[],"mappings":"AACA,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC"}
@@ -4,5 +4,6 @@ export * from './badge/tng-badge';
4
4
  export * from './press/tng-press';
5
5
  export * from './code-block/tng-code-block';
6
6
  export * from './copy/tng-copy';
7
+ export * from './file-upload';
7
8
  export * from './tag/tng-tag';
8
9
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../../libs/tailng-ui/primitives/src/lib/utility/index.ts"],"names":[],"mappings":"AAAA,qBAAqB;AACrB,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC","sourcesContent":["// Utility primitives\nexport * from './avatar/tng-avatar';\nexport * from './badge/tng-badge';\nexport * from './press/tng-press';\nexport * from './code-block/tng-code-block';\nexport * from './copy/tng-copy';\nexport * from './tag/tng-tag';\n\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../../libs/tailng-ui/primitives/src/lib/utility/index.ts"],"names":[],"mappings":"AAAA,qBAAqB;AACrB,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC","sourcesContent":["// Utility primitives\nexport * from './avatar/tng-avatar';\nexport * from './badge/tng-badge';\nexport * from './press/tng-press';\nexport * from './code-block/tng-code-block';\nexport * from './copy/tng-copy';\nexport * from './file-upload';\nexport * from './tag/tng-tag';\n\n"]}