@api-client/ui 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,43 @@
1
+ import type { Exception } from '@api-client/core/exceptions/exception.js';
2
+ /**
3
+ * Defines the options for the retry mechanism.
4
+ */
5
+ export interface RetryOptions {
6
+ /**
7
+ * The maximum number of retry attempts.
8
+ * Defaults to 3.
9
+ */
10
+ retries?: number;
11
+ /**
12
+ * The delay in milliseconds before the next retry.
13
+ * Can be a fixed number or a function that returns a number (e.g., for exponential backoff).
14
+ * Defaults to 1000ms.
15
+ */
16
+ delayMs?: number | ((attempt: number) => number);
17
+ /**
18
+ * An optional function that determines if a retry should be attempted based on the error.
19
+ * If not provided, all errors will trigger a retry up to the maximum number of retries.
20
+ * @param error The error that occurred.
21
+ * @returns True if a retry should be attempted, false otherwise.
22
+ */
23
+ shouldRetry?: (error: Error) => boolean;
24
+ }
25
+ /**
26
+ * Calculates an exponential backoff delay for retry attempts.
27
+ * The delay doubles with each attempt.
28
+ *
29
+ * @param attempt The current retry attempt number (should be 1-indexed).
30
+ * @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.
31
+ * @returns The calculated delay in milliseconds.
32
+ */
33
+ export declare function exponentialBackoffDelay(attempt: number, initialDelay?: number): number;
34
+ /**
35
+ * Checks if the API error should be retried based on its status code.
36
+ * If the status code is 404, it returns false (do not retry).
37
+ * For other status codes, it returns true (retry).
38
+ * @param error The error to check.
39
+ */
40
+ export declare function shouldRetryApiError(error: Exception): boolean;
1
41
  /**
2
42
  * The base class for models.
3
43
  *
@@ -36,6 +76,10 @@ export declare abstract class Model<T> extends EventTarget {
36
76
  * @type A promise resolved when the debounced task finished.
37
77
  */
38
78
  get taskComplete(): Promise<void> | undefined;
79
+ /**
80
+ * A getter for the raw value.
81
+ */
82
+ get value(): T | undefined;
39
83
  /**
40
84
  * @param obj The source object to use.
41
85
  */
@@ -53,5 +97,28 @@ export declare abstract class Model<T> extends EventTarget {
53
97
  debounce(callback: (...args: unknown[]) => unknown): void;
54
98
  notifyError(error: Error): void;
55
99
  protected resolveTaskPromise(): void;
100
+ /**
101
+ * Executes an asynchronous operation with a retry mechanism.
102
+ * @param operation A function that returns a Promise to be executed.
103
+ * @param options Optional retry configurations.
104
+ * @returns A Promise that resolves with the result of the operation or rejects if all retries fail.
105
+ * @example
106
+ * ```typescript
107
+ * override async create(data: Partial<IUser>): Promise<IUser> {
108
+ * const operation = async (): Promise<UserData> => {
109
+ * // ... perform API call to create user
110
+ * }
111
+ * const retryOptions: RetryOptions = {
112
+ * retries: 5,
113
+ * delayMs: exponentialBackoffDelay,
114
+ * shouldRetry: shouldRetryApiError,
115
+ * }
116
+ * this.raw = await this.retry(operation)
117
+ * this.dispatchEvent(new CustomEvent('data-changed', { detail: this.raw }))
118
+ * return this.raw;
119
+ * }
120
+ * ```
121
+ */
122
+ protected retry<R>(operation: () => Promise<R>, options?: RetryOptions): Promise<R>;
56
123
  }
57
124
  //# sourceMappingURL=Model.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../../../src/core/Model.ts"],"names":[],"mappings":"AACA;;;;;;;;;GASG;AACH,8BAAsB,KAAK,CAAC,CAAC,CAAE,SAAQ,WAAW;;IAChD;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,CAAC,CAAA;IAEP;;;;OAIG;IACH,eAAe,SAAM;IAErB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO,CAAA;IAExC;;OAEG;IACH,KAAK,SAAK;IAiBV;;OAEG;IACH,IAAI,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAE5C;IAED;;OAEG;gBACS,GAAG,CAAC,EAAE,CAAC;IAMnB;;;;;;;;OAQG;IACH,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIpC,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,GAAG,IAAI;IAqBzD,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAe/B,SAAS,CAAC,kBAAkB,IAAI,IAAI;CAUrC"}
1
+ {"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../../../src/core/Model.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0CAA0C,CAAA;AAEzE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,CAAA;IAChD;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAA;CACxC;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,SAAO,GAAG,MAAM,CAIpF;AACD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAO7D;AACD;;;;;;;;;GASG;AACH,8BAAsB,KAAK,CAAC,CAAC,CAAE,SAAQ,WAAW;;IAChD;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,CAAC,CAAA;IAEP;;;;OAIG;IACH,eAAe,SAAM;IAErB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO,CAAA;IAExC;;OAEG;IACH,KAAK,SAAK;IAiBV;;OAEG;IACH,IAAI,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAE5C;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,CAAC,GAAG,SAAS,CAEzB;IAED;;OAEG;gBACS,GAAG,CAAC,EAAE,CAAC;IAMnB;;;;;;;;OAQG;IACH,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIpC,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,GAAG,IAAI;IAqBzD,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAe/B,SAAS,CAAC,kBAAkB,IAAI,IAAI;IAWpC;;;;;;;;;;;;;;;;;;;;;OAqBG;cACa,KAAK,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,CAAC,CAAC;CAmC9F"}
@@ -1,4 +1,32 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ /**
3
+ * Calculates an exponential backoff delay for retry attempts.
4
+ * The delay doubles with each attempt.
5
+ *
6
+ * @param attempt The current retry attempt number (should be 1-indexed).
7
+ * @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.
8
+ * @returns The calculated delay in milliseconds.
9
+ */
10
+ export function exponentialBackoffDelay(attempt, initialDelay = 1000) {
11
+ if (attempt <= 0)
12
+ return initialDelay;
13
+ // For attempt 1, delay is initialDelay * 2^0 = initialDelay
14
+ return initialDelay * Math.pow(2, attempt - 1);
15
+ }
16
+ /**
17
+ * Checks if the API error should be retried based on its status code.
18
+ * If the status code is 404, it returns false (do not retry).
19
+ * For other status codes, it returns true (retry).
20
+ * @param error The error to check.
21
+ */
22
+ export function shouldRetryApiError(error) {
23
+ const { status } = error;
24
+ if (status === 404) {
25
+ // Do not retry on 404 errors
26
+ return false;
27
+ }
28
+ return true;
29
+ }
2
30
  /**
3
31
  * The base class for models.
4
32
  *
@@ -50,6 +78,12 @@ export class Model extends EventTarget {
50
78
  get taskComplete() {
51
79
  return this.#taskComplete;
52
80
  }
81
+ /**
82
+ * A getter for the raw value.
83
+ */
84
+ get value() {
85
+ return this.raw;
86
+ }
53
87
  /**
54
88
  * @param obj The source object to use.
55
89
  */
@@ -113,5 +147,60 @@ export class Model extends EventTarget {
113
147
  resolver();
114
148
  }
115
149
  }
150
+ /**
151
+ * Executes an asynchronous operation with a retry mechanism.
152
+ * @param operation A function that returns a Promise to be executed.
153
+ * @param options Optional retry configurations.
154
+ * @returns A Promise that resolves with the result of the operation or rejects if all retries fail.
155
+ * @example
156
+ * ```typescript
157
+ * override async create(data: Partial<IUser>): Promise<IUser> {
158
+ * const operation = async (): Promise<UserData> => {
159
+ * // ... perform API call to create user
160
+ * }
161
+ * const retryOptions: RetryOptions = {
162
+ * retries: 5,
163
+ * delayMs: exponentialBackoffDelay,
164
+ * shouldRetry: shouldRetryApiError,
165
+ * }
166
+ * this.raw = await this.retry(operation)
167
+ * this.dispatchEvent(new CustomEvent('data-changed', { detail: this.raw }))
168
+ * return this.raw;
169
+ * }
170
+ * ```
171
+ */
172
+ async retry(operation, options = {}) {
173
+ const { retries = 3, delayMs = 1000, shouldRetry = () => true } = options;
174
+ let lastError;
175
+ for (let attempt = 0; attempt < retries; attempt++) {
176
+ try {
177
+ return await operation();
178
+ }
179
+ catch (e) {
180
+ lastError = e;
181
+ if (!shouldRetry(lastError) || attempt === retries - 1) {
182
+ // Do not retry if shouldRetry returns false or if it's the last attempt
183
+ this.notifyError(lastError);
184
+ throw lastError;
185
+ }
186
+ const delay = typeof delayMs === 'function' ? delayMs(attempt + 1) : delayMs;
187
+ if (delay > 0) {
188
+ await new Promise((resolve) => setTimeout(resolve, delay));
189
+ }
190
+ }
191
+ }
192
+ // This part should ideally not be reached if retries > 0,
193
+ // as the loop would throw on the last attempt.
194
+ // However, to satisfy TypeScript and guard against retries = 0:
195
+ if (lastError) {
196
+ this.notifyError(lastError);
197
+ throw lastError;
198
+ }
199
+ // Should not happen if operation is called at least once.
200
+ // Adding a generic error for completeness.
201
+ const err = new Error('Retry operation failed without a specific error.');
202
+ this.notifyError(err);
203
+ throw err;
204
+ }
116
205
  }
117
206
  //# sourceMappingURL=Model.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Model.js","sourceRoot":"","sources":["../../../src/core/Model.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD;;;;;;;;;GASG;AACH,MAAM,OAAgB,KAAS,SAAQ,WAAW;IAChD;;;;;;OAMG;IACH,GAAG,CAAI;IAEP;;;;OAIG;IACH,eAAe,GAAG,GAAG,CAAA;IAErB;;OAEG;IACH,cAAc,CAA0B;IAExC;;OAEG;IACH,KAAK,GAAG,EAAE,CAAA;IAEV;;OAEG;IACH,sBAAsB,GAAG,KAAK,CAAA;IAE9B;;OAEG;IACH,aAAa,CAAgB;IAE7B;;OAEG;IACH,aAAa,CAAa;IAE1B;;OAEG;IACH,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,aAAa,CAAA;IAC3B,CAAC;IAED;;OAEG;IACH,YAAY,GAAO;QACjB,KAAK,EAAE,CAAA;QACP,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,iBAAiB,EAAE,CAAA;IAC1B,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,IAAgB;QACrB,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAA;IACrC,CAAC;IAED,QAAQ,CAAC,QAAyC;QAChD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QACnC,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,CAAC;YACjC,IAAI,CAAC,iBAAiB,EAAE,CAAA;QAC1B,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,UAAU,CAAC,KAAK,IAAI,EAAE;YAC1C,IAAI,CAAC,cAAc,GAAG,SAAS,CAAA;YAC/B,IAAI,CAAC;gBACH,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC3B,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,MAAM,GAAG,GAAG,CAAU,CAAA;gBACtB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;gBACrB,MAAM,GAAG,CAAA;YACX,CAAC;oBAAS,CAAC;gBACT,IAAI,CAAC,kBAAkB,EAAE,CAAA;YAC3B,CAAC;QACH,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,CAAA;IAC1B,CAAC;IAED,WAAW,CAAC,KAAY;QACtB,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAQ,OAAO,EAAE;YAC9B,MAAM,EAAE,KAAK;SACd,CAAC,CACH,CAAA;IACH,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,aAAa,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YACjD,IAAI,CAAC,aAAa,GAAG,OAAO,CAAA;YAC5B,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAA;QACpC,CAAC,CAAC,CAAA;IACJ,CAAC;IAES,kBAAkB;QAC1B,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,CAAC;YACjC,OAAM;QACR,CAAC;QACD,IAAI,CAAC,sBAAsB,GAAG,KAAK,CAAA;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAA;QACnC,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,EAAE,CAAA;QACZ,CAAC;IACH,CAAC;CACF","sourcesContent":["/* eslint-disable @typescript-eslint/no-unused-vars */\n/**\n * The base class for models.\n *\n * Models wrap a logic related to manipulating model data, querying for data from the API,\n * retry logic, cashing, refreshing of data, offline storage, pooling endpoints for new data, etc.\n *\n * The application should use these models in order to centralize object manipulation.\n *\n * @template T The underlying domain model.\n */\nexport abstract class Model<T> extends EventTarget {\n /**\n * The underlying raw object.\n * Do not modify properties of the object directly.\n * Use one of the methods provided by the model.\n * Direct changes will not be reflected in the UI\n * and may cause issues when processing the object.\n */\n raw?: T\n\n /**\n * The timeout for the query debouncer.\n * When any property change this is the time the element will wait\n * until the actual query is made.\n */\n debounceTimeout = 100\n\n /**\n * A reference to the current debouncer.\n */\n debouncerValue?: number | NodeJS.Timeout\n\n /**\n * Default limit for list queries.\n */\n limit = 30\n\n /**\n * A flag that helps to determine whether the `taskComplete` is setup.\n */\n #hasPendingTaskPromise = false\n\n /**\n * A hidden value for the `taskComplete` getter.\n */\n #taskComplete?: Promise<void>\n\n /**\n * The resolver to call when the debounced task completes.\n */\n #taskResolver?: () => void\n\n /**\n * @type A promise resolved when the debounced task finished.\n */\n get taskComplete(): Promise<void> | undefined {\n return this.#taskComplete\n }\n\n /**\n * @param obj The source object to use.\n */\n constructor(obj?: T) {\n super()\n this.raw = obj\n this.#setUpdatePromise()\n }\n\n /**\n * Creates a new instance of the data. It often invokes API call.\n * This is what happens after the user triggers \"create new\" flow.\n *\n * After this method is calls, the `raw` object is set.\n *\n * @param data The partial data to create.\n * @returns The created object.\n */\n create(data: Partial<T>): Promise<T> {\n throw new Error(`Not implemented.`)\n }\n\n debounce(callback: (...args: unknown[]) => unknown): void {\n if (this.debouncerValue) {\n clearTimeout(this.debouncerValue)\n }\n if (!this.#hasPendingTaskPromise) {\n this.#setUpdatePromise()\n }\n this.debouncerValue = setTimeout(async () => {\n this.debouncerValue = undefined\n try {\n await callback.call(this)\n } catch (e) {\n const err = e as Error\n this.notifyError(err)\n throw err\n } finally {\n this.resolveTaskPromise()\n }\n }, this.debounceTimeout)\n }\n\n notifyError(error: Error): void {\n this.dispatchEvent(\n new CustomEvent<Error>('error', {\n detail: error,\n })\n )\n }\n\n #setUpdatePromise(): void {\n this.#taskComplete = new Promise<void>((resolve) => {\n this.#taskResolver = resolve\n this.#hasPendingTaskPromise = true\n })\n }\n\n protected resolveTaskPromise(): void {\n if (!this.#hasPendingTaskPromise) {\n return\n }\n this.#hasPendingTaskPromise = false\n const resolver = this.#taskResolver\n if (resolver) {\n resolver()\n }\n }\n}\n"]}
1
+ {"version":3,"file":"Model.js","sourceRoot":"","sources":["../../../src/core/Model.ts"],"names":[],"mappings":"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","sourcesContent":["/* eslint-disable @typescript-eslint/no-unused-vars */\n\nimport type { Exception } from '@api-client/core/exceptions/exception.js'\n\n/**\n * Defines the options for the retry mechanism.\n */\nexport interface RetryOptions {\n /**\n * The maximum number of retry attempts.\n * Defaults to 3.\n */\n retries?: number\n /**\n * The delay in milliseconds before the next retry.\n * Can be a fixed number or a function that returns a number (e.g., for exponential backoff).\n * Defaults to 1000ms.\n */\n delayMs?: number | ((attempt: number) => number)\n /**\n * An optional function that determines if a retry should be attempted based on the error.\n * If not provided, all errors will trigger a retry up to the maximum number of retries.\n * @param error The error that occurred.\n * @returns True if a retry should be attempted, false otherwise.\n */\n shouldRetry?: (error: Error) => boolean\n}\n\n/**\n * Calculates an exponential backoff delay for retry attempts.\n * The delay doubles with each attempt.\n *\n * @param attempt The current retry attempt number (should be 1-indexed).\n * @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.\n * @returns The calculated delay in milliseconds.\n */\nexport function exponentialBackoffDelay(attempt: number, initialDelay = 1000): number {\n if (attempt <= 0) return initialDelay\n // For attempt 1, delay is initialDelay * 2^0 = initialDelay\n return initialDelay * Math.pow(2, attempt - 1)\n}\n/**\n * Checks if the API error should be retried based on its status code.\n * If the status code is 404, it returns false (do not retry).\n * For other status codes, it returns true (retry).\n * @param error The error to check.\n */\nexport function shouldRetryApiError(error: Exception): boolean {\n const { status } = error\n if (status === 404) {\n // Do not retry on 404 errors\n return false\n }\n return true\n}\n/**\n * The base class for models.\n *\n * Models wrap a logic related to manipulating model data, querying for data from the API,\n * retry logic, cashing, refreshing of data, offline storage, pooling endpoints for new data, etc.\n *\n * The application should use these models in order to centralize object manipulation.\n *\n * @template T The underlying domain model.\n */\nexport abstract class Model<T> extends EventTarget {\n /**\n * The underlying raw object.\n * Do not modify properties of the object directly.\n * Use one of the methods provided by the model.\n * Direct changes will not be reflected in the UI\n * and may cause issues when processing the object.\n */\n raw?: T\n\n /**\n * The timeout for the query debouncer.\n * When any property change this is the time the element will wait\n * until the actual query is made.\n */\n debounceTimeout = 100\n\n /**\n * A reference to the current debouncer.\n */\n debouncerValue?: number | NodeJS.Timeout\n\n /**\n * Default limit for list queries.\n */\n limit = 30\n\n /**\n * A flag that helps to determine whether the `taskComplete` is setup.\n */\n #hasPendingTaskPromise = false\n\n /**\n * A hidden value for the `taskComplete` getter.\n */\n #taskComplete?: Promise<void>\n\n /**\n * The resolver to call when the debounced task completes.\n */\n #taskResolver?: () => void\n\n /**\n * @type A promise resolved when the debounced task finished.\n */\n get taskComplete(): Promise<void> | undefined {\n return this.#taskComplete\n }\n\n /**\n * A getter for the raw value.\n */\n get value(): T | undefined {\n return this.raw\n }\n\n /**\n * @param obj The source object to use.\n */\n constructor(obj?: T) {\n super()\n this.raw = obj\n this.#setUpdatePromise()\n }\n\n /**\n * Creates a new instance of the data. It often invokes API call.\n * This is what happens after the user triggers \"create new\" flow.\n *\n * After this method is calls, the `raw` object is set.\n *\n * @param data The partial data to create.\n * @returns The created object.\n */\n create(data: Partial<T>): Promise<T> {\n throw new Error(`Not implemented.`)\n }\n\n debounce(callback: (...args: unknown[]) => unknown): void {\n if (this.debouncerValue) {\n clearTimeout(this.debouncerValue)\n }\n if (!this.#hasPendingTaskPromise) {\n this.#setUpdatePromise()\n }\n this.debouncerValue = setTimeout(async () => {\n this.debouncerValue = undefined\n try {\n await callback.call(this)\n } catch (e) {\n const err = e as Error\n this.notifyError(err)\n throw err\n } finally {\n this.resolveTaskPromise()\n }\n }, this.debounceTimeout)\n }\n\n notifyError(error: Error): void {\n this.dispatchEvent(\n new CustomEvent<Error>('error', {\n detail: error,\n })\n )\n }\n\n #setUpdatePromise(): void {\n this.#taskComplete = new Promise<void>((resolve) => {\n this.#taskResolver = resolve\n this.#hasPendingTaskPromise = true\n })\n }\n\n protected resolveTaskPromise(): void {\n if (!this.#hasPendingTaskPromise) {\n return\n }\n this.#hasPendingTaskPromise = false\n const resolver = this.#taskResolver\n if (resolver) {\n resolver()\n }\n }\n\n /**\n * Executes an asynchronous operation with a retry mechanism.\n * @param operation A function that returns a Promise to be executed.\n * @param options Optional retry configurations.\n * @returns A Promise that resolves with the result of the operation or rejects if all retries fail.\n * @example\n * ```typescript\n * override async create(data: Partial<IUser>): Promise<IUser> {\n * const operation = async (): Promise<UserData> => {\n * // ... perform API call to create user\n * }\n * const retryOptions: RetryOptions = {\n * retries: 5,\n * delayMs: exponentialBackoffDelay,\n * shouldRetry: shouldRetryApiError,\n * }\n * this.raw = await this.retry(operation)\n * this.dispatchEvent(new CustomEvent('data-changed', { detail: this.raw }))\n * return this.raw;\n * }\n * ```\n */\n protected async retry<R>(operation: () => Promise<R>, options: RetryOptions = {}): Promise<R> {\n const { retries = 3, delayMs = 1000, shouldRetry = () => true } = options\n\n let lastError: Error | undefined\n\n for (let attempt = 0; attempt < retries; attempt++) {\n try {\n return await operation()\n } catch (e) {\n lastError = e as Error\n if (!shouldRetry(lastError) || attempt === retries - 1) {\n // Do not retry if shouldRetry returns false or if it's the last attempt\n this.notifyError(lastError)\n throw lastError\n }\n\n const delay = typeof delayMs === 'function' ? delayMs(attempt + 1) : delayMs\n if (delay > 0) {\n await new Promise((resolve) => setTimeout(resolve, delay))\n }\n }\n }\n // This part should ideally not be reached if retries > 0,\n // as the loop would throw on the last attempt.\n // However, to satisfy TypeScript and guard against retries = 0:\n if (lastError) {\n this.notifyError(lastError)\n throw lastError\n }\n // Should not happen if operation is called at least once.\n // Adding a generic error for completeness.\n const err = new Error('Retry operation failed without a specific error.')\n this.notifyError(err)\n throw err\n }\n}\n"]}
@@ -1,9 +1,9 @@
1
- import { TemplateResult, LitElement } from 'lit';
1
+ import { TemplateResult, LitElement, PropertyValues, SVGTemplateResult } from 'lit';
2
2
  import type { IUser } from '@api-client/core/models/store/User.js';
3
3
  import '@material/web/focus/md-focus-ring.js';
4
4
  import '@material/web/ripple/ripple.js';
5
+ export type AvatarType = 'button' | 'icon';
5
6
  export default class UserAvatar extends LitElement {
6
- private _user?;
7
7
  /**
8
8
  * Set with the user. The computed user initials.
9
9
  */
@@ -12,23 +12,36 @@ export default class UserAvatar extends LitElement {
12
12
  * The URL to the user picture.
13
13
  */
14
14
  protected accessor userPicture: string | undefined;
15
- get user(): IUser | undefined;
16
15
  /**
16
+ * The user object to display the avatar for.
17
+ */
18
+ accessor user: IUser | undefined;
19
+ /**
20
+ * The type of avatar to render, either 'button' or 'icon'.
17
21
  * @attribute
18
22
  */
19
- set user(value: IUser | undefined);
20
- protected _processUser(user?: IUser): void;
21
- protected _readUserInitials(user: IUser): string | undefined;
22
- protected _pictureError(): void;
23
+ accessor type: AvatarType;
24
+ protected willUpdate(cp: PropertyValues<this>): void;
25
+ /**
26
+ * Handles changes to the user property.
27
+ * @param user The user object
28
+ */
29
+ protected handleUserChange(user?: IUser): void;
30
+ protected readUserInitials(user: IUser): string | undefined;
31
+ protected handlePictureError(): void;
23
32
  /**
24
33
  * @return Template result for an icon
25
34
  */
26
35
  render(): TemplateResult;
36
+ protected renderButton(content: TemplateResult): TemplateResult;
37
+ protected renderIcon(content: TemplateResult): TemplateResult;
27
38
  protected pictureTemplate(url: string): TemplateResult;
28
39
  /**
29
40
  * Renders a bubble with user initials
30
41
  * @param initials The user initials
31
42
  */
32
43
  protected nameTemplate(initials: string): TemplateResult;
44
+ protected renderDefaultAvatar(): TemplateResult;
45
+ protected defaultIcon(): SVGTemplateResult;
33
46
  }
34
47
  //# sourceMappingURL=UserAvatar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"UserAvatar.d.ts","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,cAAc,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAEtD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uCAAuC,CAAA;AAClE,OAAO,sCAAsC,CAAA;AAC7C,OAAO,gCAAgC,CAAA;AAEvC,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,UAAU;IAChD,OAAO,CAAC,KAAK,CAAC,CAAO;IAErB;;OAEG;IACM,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAE5D;;OAEG;IACM,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IAE3D,IAAI,IAAI,IAAI,KAAK,GAAG,SAAS,CAE5B;IAED;;OAEG;IACH,IACI,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,EAWhC;IAED,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,KAAK,GAAG,IAAI;IAU1C,SAAS,CAAC,iBAAiB,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS;IAc5D,SAAS,CAAC,aAAa,IAAI,IAAI;IAI/B;;OAEG;IACM,MAAM,IAAI,cAAc;IAuBjC,SAAS,CAAC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc;IAWtD;;;OAGG;IACH,SAAS,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc;CAGzD"}
1
+ {"version":3,"file":"UserAvatar.d.ts","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,cAAc,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,EAAO,MAAM,KAAK,CAAA;AAE9F,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uCAAuC,CAAA;AAClE,OAAO,sCAAsC,CAAA;AAC7C,OAAO,gCAAgC,CAAA;AAEvC,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAA;AAE1C,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,UAAU;IAChD;;OAEG;IACM,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAE5D;;OAEG;IACM,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IAE3D;;OAEG;IACyB,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,CAAA;IAC5D;;;OAGG;IACyB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAW;cAE7C,UAAU,CAAC,EAAE,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,IAAI;IAO7D;;;OAGG;IACH,SAAS,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,KAAK,GAAG,IAAI;IAU9C,SAAS,CAAC,gBAAgB,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS;IAc3D,SAAS,CAAC,kBAAkB,IAAI,IAAI;IAIpC;;OAEG;IACM,MAAM,IAAI,cAAc;IAgBjC,SAAS,CAAC,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc;IAW/D,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc;IAI7D,SAAS,CAAC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc;IAWtD;;;OAGG;IACH,SAAS,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc;IAIxD,SAAS,CAAC,mBAAmB,IAAI,cAAc;IAI/C,SAAS,CAAC,WAAW,IAAI,iBAAiB;CAiB3C"}
@@ -1,30 +1,35 @@
1
1
  import { __esDecorate, __runInitializers } from "tslib";
2
- import { html, LitElement } from 'lit';
2
+ import { html, LitElement, svg } from 'lit';
3
3
  import { property, state } from 'lit/decorators.js';
4
4
  import '@material/web/focus/md-focus-ring.js';
5
5
  import '@material/web/ripple/ripple.js';
6
6
  let UserAvatar = (() => {
7
7
  let _classSuper = LitElement;
8
- let _instanceExtraInitializers = [];
9
8
  let _userInitials_decorators;
10
9
  let _userInitials_initializers = [];
11
10
  let _userInitials_extraInitializers = [];
12
11
  let _userPicture_decorators;
13
12
  let _userPicture_initializers = [];
14
13
  let _userPicture_extraInitializers = [];
15
- let _set_user_decorators;
14
+ let _user_decorators;
15
+ let _user_initializers = [];
16
+ let _user_extraInitializers = [];
17
+ let _type_decorators;
18
+ let _type_initializers = [];
19
+ let _type_extraInitializers = [];
16
20
  return class UserAvatar extends _classSuper {
17
21
  static {
18
22
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
19
23
  _userInitials_decorators = [state()];
20
24
  _userPicture_decorators = [state()];
21
- _set_user_decorators = [property({ type: Object })];
25
+ _user_decorators = [property({ type: Object })];
26
+ _type_decorators = [property({ type: String })];
22
27
  __esDecorate(this, null, _userInitials_decorators, { kind: "accessor", name: "userInitials", static: false, private: false, access: { has: obj => "userInitials" in obj, get: obj => obj.userInitials, set: (obj, value) => { obj.userInitials = value; } }, metadata: _metadata }, _userInitials_initializers, _userInitials_extraInitializers);
23
28
  __esDecorate(this, null, _userPicture_decorators, { kind: "accessor", name: "userPicture", static: false, private: false, access: { has: obj => "userPicture" in obj, get: obj => obj.userPicture, set: (obj, value) => { obj.userPicture = value; } }, metadata: _metadata }, _userPicture_initializers, _userPicture_extraInitializers);
24
- __esDecorate(this, null, _set_user_decorators, { kind: "setter", name: "user", static: false, private: false, access: { has: obj => "user" in obj, set: (obj, value) => { obj.user = value; } }, metadata: _metadata }, null, _instanceExtraInitializers);
29
+ __esDecorate(this, null, _user_decorators, { kind: "accessor", name: "user", static: false, private: false, access: { has: obj => "user" in obj, get: obj => obj.user, set: (obj, value) => { obj.user = value; } }, metadata: _metadata }, _user_initializers, _user_extraInitializers);
30
+ __esDecorate(this, null, _type_decorators, { kind: "accessor", name: "type", static: false, private: false, access: { has: obj => "type" in obj, get: obj => obj.type, set: (obj, value) => { obj.type = value; } }, metadata: _metadata }, _type_initializers, _type_extraInitializers);
25
31
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
26
32
  }
27
- _user = __runInitializers(this, _instanceExtraInitializers);
28
33
  #userInitials_accessor_storage = __runInitializers(this, _userInitials_initializers, void 0);
29
34
  /**
30
35
  * Set with the user. The computed user initials.
@@ -37,27 +42,32 @@ let UserAvatar = (() => {
37
42
  */
38
43
  get userPicture() { return this.#userPicture_accessor_storage; }
39
44
  set userPicture(value) { this.#userPicture_accessor_storage = value; }
40
- get user() {
41
- return this._user;
42
- }
45
+ #user_accessor_storage = (__runInitializers(this, _userPicture_extraInitializers), __runInitializers(this, _user_initializers, void 0));
46
+ /**
47
+ * The user object to display the avatar for.
48
+ */
49
+ get user() { return this.#user_accessor_storage; }
50
+ set user(value) { this.#user_accessor_storage = value; }
51
+ #type_accessor_storage = (__runInitializers(this, _user_extraInitializers), __runInitializers(this, _type_initializers, 'button'));
43
52
  /**
53
+ * The type of avatar to render, either 'button' or 'icon'.
44
54
  * @attribute
45
55
  */
46
- set user(value) {
47
- const old = this._user;
48
- if (old === value) {
49
- return;
50
- }
51
- if (old && value && old.key === value.key) {
52
- return;
56
+ get type() { return this.#type_accessor_storage; }
57
+ set type(value) { this.#type_accessor_storage = value; }
58
+ willUpdate(cp) {
59
+ if (cp.has('user')) {
60
+ this.handleUserChange(this.user);
53
61
  }
54
- this._user = value;
55
- this.requestUpdate('user', old);
56
- this._processUser(value);
62
+ super.willUpdate(cp);
57
63
  }
58
- _processUser(user) {
64
+ /**
65
+ * Handles changes to the user property.
66
+ * @param user The user object
67
+ */
68
+ handleUserChange(user) {
59
69
  if (user) {
60
- this.userInitials = this._readUserInitials(user);
70
+ this.userInitials = this.readUserInitials(user);
61
71
  this.userPicture = user.picture && user.picture.url;
62
72
  }
63
73
  else {
@@ -65,7 +75,7 @@ let UserAvatar = (() => {
65
75
  this.userPicture = undefined;
66
76
  }
67
77
  }
68
- _readUserInitials(user) {
78
+ readUserInitials(user) {
69
79
  const { name } = user;
70
80
  if (!name) {
71
81
  return undefined;
@@ -78,17 +88,14 @@ let UserAvatar = (() => {
78
88
  .map((i) => i[0]);
79
89
  return parts.join('');
80
90
  }
81
- _pictureError() {
91
+ handlePictureError() {
82
92
  this.userPicture = undefined;
83
93
  }
84
94
  /**
85
95
  * @return Template result for an icon
86
96
  */
87
97
  render() {
88
- const { user, userPicture } = this;
89
- if (!user || user.key === 'default') {
90
- return html ``;
91
- }
98
+ const { userPicture } = this;
92
99
  let content;
93
100
  if (userPicture) {
94
101
  content = this.pictureTemplate(userPicture);
@@ -97,8 +104,14 @@ let UserAvatar = (() => {
97
104
  content = this.nameTemplate(this.userInitials);
98
105
  }
99
106
  else {
100
- content = this.nameTemplate('');
107
+ content = this.renderDefaultAvatar();
108
+ }
109
+ if (this.type === 'icon') {
110
+ return this.renderIcon(content);
101
111
  }
112
+ return this.renderButton(content);
113
+ }
114
+ renderButton(content) {
102
115
  return html `
103
116
  <button id="button" class="icon-button">
104
117
  <md-focus-ring part="focus-ring" for="button"></md-focus-ring>
@@ -108,13 +121,16 @@ let UserAvatar = (() => {
108
121
  </button>
109
122
  `;
110
123
  }
124
+ renderIcon(content) {
125
+ return html `<span role="presentation" class="icon">${content}</span>`;
126
+ }
111
127
  pictureTemplate(url) {
112
128
  return html `
113
129
  <img
114
130
  src="${url}"
115
131
  alt="${this.userInitials || 'Thumb'}"
116
132
  class="user-picture user-icon"
117
- @error="${this._pictureError}"
133
+ @error="${this.handlePictureError}"
118
134
  />
119
135
  `;
120
136
  }
@@ -125,9 +141,29 @@ let UserAvatar = (() => {
125
141
  nameTemplate(initials) {
126
142
  return html ` <span class="avatar-initials user-icon">${initials}</span> `;
127
143
  }
144
+ renderDefaultAvatar() {
145
+ return html `<span class="avatar-default icon"> ${this.defaultIcon()} </span>`;
146
+ }
147
+ defaultIcon() {
148
+ return svg `
149
+ <svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
150
+ <rect width="120" height="120" rx="60" class="avatar-background" />
151
+ <path
152
+ fill-rule="evenodd"
153
+ clip-rule="evenodd"
154
+ d="M78.0007 48C78.0007 57.9411 69.9419 66 60.0007 66C50.0596 66 42.0007 57.9411 42.0007 48C42.0007 38.0589 50.0596 30 60.0007 30C69.9419 30 78.0007 38.0589 78.0007 48ZM72.0007 48C72.0007 54.6274 66.6282 60 60.0007 60C53.3733 60 48.0007 54.6274 48.0007 48C48.0007 41.3726 53.3733 36 60.0007 36C66.6282 36 72.0007 41.3726 72.0007 48Z"
155
+ class="avatar-lines"
156
+ />
157
+ <path
158
+ d="M60.0007 75C40.5776 75 24.0286 86.4852 17.7246 102.576C19.2603 104.101 20.878 105.543 22.5706 106.896C27.2648 92.1231 41.9909 81 60.0007 81C78.0106 81 92.7367 92.1231 97.4309 106.896C99.1235 105.544 100.741 104.101 102.277 102.576C95.973 86.4853 79.4239 75 60.0007 75Z"
159
+ class="avatar-lines"
160
+ />
161
+ </svg>
162
+ `;
163
+ }
128
164
  constructor() {
129
165
  super(...arguments);
130
- __runInitializers(this, _userPicture_extraInitializers);
166
+ __runInitializers(this, _type_extraInitializers);
131
167
  }
132
168
  };
133
169
  })();
@@ -1 +1 @@
1
- {"version":3,"file":"UserAvatar.js","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, TemplateResult, LitElement } from 'lit'\nimport { property, state } from 'lit/decorators.js'\nimport type { IUser } from '@api-client/core/models/store/User.js'\nimport '@material/web/focus/md-focus-ring.js'\nimport '@material/web/ripple/ripple.js'\n\nexport default class UserAvatar extends LitElement {\n private _user?: IUser\n\n /**\n * Set with the user. The computed user initials.\n */\n @state() protected accessor userInitials: string | undefined\n\n /**\n * The URL to the user picture.\n */\n @state() protected accessor userPicture: string | undefined\n\n get user(): IUser | undefined {\n return this._user\n }\n\n /**\n * @attribute\n */\n @property({ type: Object })\n set user(value: IUser | undefined) {\n const old = this._user\n if (old === value) {\n return\n }\n if (old && value && old.key === value.key) {\n return\n }\n this._user = value\n this.requestUpdate('user', old)\n this._processUser(value)\n }\n\n protected _processUser(user?: IUser): void {\n if (user) {\n this.userInitials = this._readUserInitials(user)\n this.userPicture = user.picture && user.picture.url\n } else {\n this.userInitials = undefined\n this.userPicture = undefined\n }\n }\n\n protected _readUserInitials(user: IUser): string | undefined {\n const { name } = user\n if (!name) {\n return undefined\n }\n const max = 2\n const parts = name\n .split(/[\\s-]/)\n .slice(0, max)\n .filter((i) => !!i)\n .map((i) => i[0])\n return parts.join('')\n }\n\n protected _pictureError(): void {\n this.userPicture = undefined\n }\n\n /**\n * @return Template result for an icon\n */\n override render(): TemplateResult {\n const { user, userPicture } = this\n if (!user || user.key === 'default') {\n return html``\n }\n let content: TemplateResult\n if (userPicture) {\n content = this.pictureTemplate(userPicture)\n } else if (this.userInitials) {\n content = this.nameTemplate(this.userInitials)\n } else {\n content = this.nameTemplate('')\n }\n return html`\n <button id=\"button\" class=\"icon-button\">\n <md-focus-ring part=\"focus-ring\" for=\"button\"></md-focus-ring>\n <md-ripple></md-ripple>\n <span role=\"presentation\" class=\"icon\">${content}</span>\n <span class=\"touch\"></span>\n </button>\n `\n }\n\n protected pictureTemplate(url: string): TemplateResult {\n return html`\n <img\n src=\"${url}\"\n alt=\"${this.userInitials || 'Thumb'}\"\n class=\"user-picture user-icon\"\n @error=\"${this._pictureError}\"\n />\n `\n }\n\n /**\n * Renders a bubble with user initials\n * @param initials The user initials\n */\n protected nameTemplate(initials: string): TemplateResult {\n return html` <span class=\"avatar-initials user-icon\">${initials}</span> `\n }\n}\n"]}
1
+ {"version":3,"file":"UserAvatar.js","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, TemplateResult, LitElement, PropertyValues, SVGTemplateResult, svg } from 'lit'\nimport { property, state } from 'lit/decorators.js'\nimport type { IUser } from '@api-client/core/models/store/User.js'\nimport '@material/web/focus/md-focus-ring.js'\nimport '@material/web/ripple/ripple.js'\n\nexport type AvatarType = 'button' | 'icon'\n\nexport default class UserAvatar extends LitElement {\n /**\n * Set with the user. The computed user initials.\n */\n @state() protected accessor userInitials: string | undefined\n\n /**\n * The URL to the user picture.\n */\n @state() protected accessor userPicture: string | undefined\n\n /**\n * The user object to display the avatar for.\n */\n @property({ type: Object }) accessor user: IUser | undefined\n /**\n * The type of avatar to render, either 'button' or 'icon'.\n * @attribute\n */\n @property({ type: String }) accessor type: AvatarType = 'button'\n\n protected override willUpdate(cp: PropertyValues<this>): void {\n if (cp.has('user')) {\n this.handleUserChange(this.user)\n }\n super.willUpdate(cp)\n }\n\n /**\n * Handles changes to the user property.\n * @param user The user object\n */\n protected handleUserChange(user?: IUser): void {\n if (user) {\n this.userInitials = this.readUserInitials(user)\n this.userPicture = user.picture && user.picture.url\n } else {\n this.userInitials = undefined\n this.userPicture = undefined\n }\n }\n\n protected readUserInitials(user: IUser): string | undefined {\n const { name } = user\n if (!name) {\n return undefined\n }\n const max = 2\n const parts = name\n .split(/[\\s-]/)\n .slice(0, max)\n .filter((i) => !!i)\n .map((i) => i[0])\n return parts.join('')\n }\n\n protected handlePictureError(): void {\n this.userPicture = undefined\n }\n\n /**\n * @return Template result for an icon\n */\n override render(): TemplateResult {\n const { userPicture } = this\n let content: TemplateResult\n if (userPicture) {\n content = this.pictureTemplate(userPicture)\n } else if (this.userInitials) {\n content = this.nameTemplate(this.userInitials)\n } else {\n content = this.renderDefaultAvatar()\n }\n if (this.type === 'icon') {\n return this.renderIcon(content)\n }\n return this.renderButton(content)\n }\n\n protected renderButton(content: TemplateResult): TemplateResult {\n return html`\n <button id=\"button\" class=\"icon-button\">\n <md-focus-ring part=\"focus-ring\" for=\"button\"></md-focus-ring>\n <md-ripple></md-ripple>\n <span role=\"presentation\" class=\"icon\">${content}</span>\n <span class=\"touch\"></span>\n </button>\n `\n }\n\n protected renderIcon(content: TemplateResult): TemplateResult {\n return html`<span role=\"presentation\" class=\"icon\">${content}</span>`\n }\n\n protected pictureTemplate(url: string): TemplateResult {\n return html`\n <img\n src=\"${url}\"\n alt=\"${this.userInitials || 'Thumb'}\"\n class=\"user-picture user-icon\"\n @error=\"${this.handlePictureError}\"\n />\n `\n }\n\n /**\n * Renders a bubble with user initials\n * @param initials The user initials\n */\n protected nameTemplate(initials: string): TemplateResult {\n return html` <span class=\"avatar-initials user-icon\">${initials}</span> `\n }\n\n protected renderDefaultAvatar(): TemplateResult {\n return html`<span class=\"avatar-default icon\"> ${this.defaultIcon()} </span>`\n }\n\n protected defaultIcon(): SVGTemplateResult {\n return svg`\n <svg viewBox=\"0 0 120 120\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\">\n <rect width=\"120\" height=\"120\" rx=\"60\" class=\"avatar-background\" />\n <path\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M78.0007 48C78.0007 57.9411 69.9419 66 60.0007 66C50.0596 66 42.0007 57.9411 42.0007 48C42.0007 38.0589 50.0596 30 60.0007 30C69.9419 30 78.0007 38.0589 78.0007 48ZM72.0007 48C72.0007 54.6274 66.6282 60 60.0007 60C53.3733 60 48.0007 54.6274 48.0007 48C48.0007 41.3726 53.3733 36 60.0007 36C66.6282 36 72.0007 41.3726 72.0007 48Z\"\n class=\"avatar-lines\"\n />\n <path\n d=\"M60.0007 75C40.5776 75 24.0286 86.4852 17.7246 102.576C19.2603 104.101 20.878 105.543 22.5706 106.896C27.2648 92.1231 41.9909 81 60.0007 81C78.0106 81 92.7367 92.1231 97.4309 106.896C99.1235 105.544 100.741 104.101 102.277 102.576C95.973 86.4853 79.4239 75 60.0007 75Z\"\n class=\"avatar-lines\"\n />\n </svg>\n `\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"UserAvatar.styles.d.ts","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.styles.ts"],"names":[],"mappings":";AAEA,wBA6GC"}
1
+ {"version":3,"file":"UserAvatar.styles.d.ts","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.styles.ts"],"names":[],"mappings":";AAEA,wBAmHC"}
@@ -5,8 +5,8 @@ export default css `
5
5
  outline: none;
6
6
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
7
7
  justify-content: center;
8
- height: var(--_state-layer-height);
9
- width: var(--_state-layer-width);
8
+ height: 40px;
9
+ width: 40px;
10
10
 
11
11
  --_focus-icon-color: var(--md-icon-button-focus-icon-color, var(--md-sys-color-on-surface-variant, #49454f));
12
12
  --_hover-icon-color: var(--md-icon-button-hover-icon-color, var(--md-sys-color-on-surface-variant, #49454f));
@@ -23,8 +23,6 @@ export default css `
23
23
  );
24
24
  --_pressed-state-layer-opacity: var(--md-icon-button-pressed-state-layer-opacity, 0.12);
25
25
  --_state-layer-shape: var(--md-icon-button-state-layer-shape, var(--md-sys-shape-corner-full, 9999px));
26
- --_state-layer-height: 40px;
27
- --_state-layer-width: 40px;
28
26
  }
29
27
 
30
28
  .icon-button {
@@ -88,15 +86,13 @@ export default css `
88
86
 
89
87
  .user-icon,
90
88
  .avatar-initials {
91
- background-color: var(--user-avatar-initials-background-color, #0540f2);
92
- color: var(--user-avatar-initials-color, #fff);
89
+ background-color: var(--md-sys-color-primary-container, #9eeffe);
90
+ color: var(--md-sys-color-on-primary-container, #001f24);
93
91
  }
94
92
 
95
93
  .avatar-initials {
96
- /* border-radius: 50%; */
97
94
  text-transform: uppercase;
98
95
  font-size: large;
99
- border: 1px #0d47a1 solid;
100
96
  width: 100%;
101
97
  height: 100%;
102
98
  display: flex;
@@ -104,9 +100,19 @@ export default css `
104
100
  justify-content: center;
105
101
  }
106
102
 
107
- .user-icon {
108
- height: var(--_state-layer-height);
109
- width: var(--_state-layer-width);
103
+ .user-icon,
104
+ .icon-button,
105
+ .icon {
106
+ height: inherit;
107
+ width: inherit;
108
+ }
109
+
110
+ .avatar-background {
111
+ fill: var(--md-sys-color-primary-container, #9eeffe);
112
+ }
113
+
114
+ .avatar-lines {
115
+ fill: var(--md-sys-color-on-primary-container, #001f24);
110
116
  }
111
117
  `;
112
118
  //# sourceMappingURL=UserAvatar.styles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"UserAvatar.styles.js","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,eAAe,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6GjB,CAAA","sourcesContent":["import { css } from 'lit'\n\nexport default css`\n :host {\n display: inline-flex;\n outline: none;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n justify-content: center;\n height: var(--_state-layer-height);\n width: var(--_state-layer-width);\n\n --_focus-icon-color: var(--md-icon-button-focus-icon-color, var(--md-sys-color-on-surface-variant, #49454f));\n --_hover-icon-color: var(--md-icon-button-hover-icon-color, var(--md-sys-color-on-surface-variant, #49454f));\n --_hover-state-layer-color: var(\n --md-icon-button-hover-state-layer-color,\n var(--md-sys-color-on-surface-variant, #49454f)\n );\n --_hover-state-layer-opacity: var(--md-icon-button-hover-state-layer-opacity, 0.08);\n --_icon-color: var(--md-icon-button-icon-color, var(--md-sys-color-on-surface-variant, #49454f));\n --_pressed-icon-color: var(--md-icon-button-pressed-icon-color, var(--md-sys-color-on-surface-variant, #49454f));\n --_pressed-state-layer-color: var(\n --md-icon-button-pressed-state-layer-color,\n var(--md-sys-color-on-surface-variant, #49454f)\n );\n --_pressed-state-layer-opacity: var(--md-icon-button-pressed-state-layer-opacity, 0.12);\n --_state-layer-shape: var(--md-icon-button-state-layer-shape, var(--md-sys-shape-corner-full, 9999px));\n --_state-layer-height: 40px;\n --_state-layer-width: 40px;\n }\n\n .icon-button {\n background-color: rgba(0, 0, 0, 0);\n color: var(--_icon-color);\n place-items: center;\n background: none;\n border: none;\n box-sizing: border-box;\n cursor: pointer;\n display: flex;\n place-content: center;\n outline: none;\n padding: 0;\n position: relative;\n text-decoration: none;\n user-select: none;\n z-index: 0;\n flex: 1;\n border-radius: var(--_state-layer-shape);\n --md-ripple-hover-color: var(--_hover-state-layer-color);\n --md-ripple-hover-opacity: var(--_hover-state-layer-opacity);\n --md-ripple-pressed-color: var(--_pressed-state-layer-color);\n --md-ripple-pressed-opacity: var(--_pressed-state-layer-opacity);\n }\n\n .icon-button:hover {\n color: var(--_hover-icon-color);\n }\n\n .icon-button:focus {\n color: var(--_focus-icon-color);\n }\n\n .icon-button:active {\n color: var(--_pressed-icon-color);\n }\n\n md-focus-ring {\n --md-focus-ring-shape-start-start: var(--_state-layer-shape);\n --md-focus-ring-shape-start-end: var(--_state-layer-shape);\n --md-focus-ring-shape-end-end: var(--_state-layer-shape);\n --md-focus-ring-shape-end-start: var(--_state-layer-shape);\n }\n\n md-ripple {\n border-radius: var(--_state-layer-shape);\n }\n\n .icon {\n display: inline-flex;\n border-radius: var(--_state-layer-shape);\n overflow: hidden;\n }\n\n .touch {\n position: absolute;\n height: max(48px, 100%);\n width: max(48px, 100%);\n }\n\n .user-icon,\n .avatar-initials {\n background-color: var(--user-avatar-initials-background-color, #0540f2);\n color: var(--user-avatar-initials-color, #fff);\n }\n\n .avatar-initials {\n /* border-radius: 50%; */\n text-transform: uppercase;\n font-size: large;\n border: 1px #0d47a1 solid;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .user-icon {\n height: var(--_state-layer-height);\n width: var(--_state-layer-width);\n }\n`\n"]}
1
+ {"version":3,"file":"UserAvatar.styles.js","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,eAAe,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmHjB,CAAA","sourcesContent":["import { css } from 'lit'\n\nexport default css`\n :host {\n display: inline-flex;\n outline: none;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n justify-content: center;\n height: 40px;\n width: 40px;\n\n --_focus-icon-color: var(--md-icon-button-focus-icon-color, var(--md-sys-color-on-surface-variant, #49454f));\n --_hover-icon-color: var(--md-icon-button-hover-icon-color, var(--md-sys-color-on-surface-variant, #49454f));\n --_hover-state-layer-color: var(\n --md-icon-button-hover-state-layer-color,\n var(--md-sys-color-on-surface-variant, #49454f)\n );\n --_hover-state-layer-opacity: var(--md-icon-button-hover-state-layer-opacity, 0.08);\n --_icon-color: var(--md-icon-button-icon-color, var(--md-sys-color-on-surface-variant, #49454f));\n --_pressed-icon-color: var(--md-icon-button-pressed-icon-color, var(--md-sys-color-on-surface-variant, #49454f));\n --_pressed-state-layer-color: var(\n --md-icon-button-pressed-state-layer-color,\n var(--md-sys-color-on-surface-variant, #49454f)\n );\n --_pressed-state-layer-opacity: var(--md-icon-button-pressed-state-layer-opacity, 0.12);\n --_state-layer-shape: var(--md-icon-button-state-layer-shape, var(--md-sys-shape-corner-full, 9999px));\n }\n\n .icon-button {\n background-color: rgba(0, 0, 0, 0);\n color: var(--_icon-color);\n place-items: center;\n background: none;\n border: none;\n box-sizing: border-box;\n cursor: pointer;\n display: flex;\n place-content: center;\n outline: none;\n padding: 0;\n position: relative;\n text-decoration: none;\n user-select: none;\n z-index: 0;\n flex: 1;\n border-radius: var(--_state-layer-shape);\n --md-ripple-hover-color: var(--_hover-state-layer-color);\n --md-ripple-hover-opacity: var(--_hover-state-layer-opacity);\n --md-ripple-pressed-color: var(--_pressed-state-layer-color);\n --md-ripple-pressed-opacity: var(--_pressed-state-layer-opacity);\n }\n\n .icon-button:hover {\n color: var(--_hover-icon-color);\n }\n\n .icon-button:focus {\n color: var(--_focus-icon-color);\n }\n\n .icon-button:active {\n color: var(--_pressed-icon-color);\n }\n\n md-focus-ring {\n --md-focus-ring-shape-start-start: var(--_state-layer-shape);\n --md-focus-ring-shape-start-end: var(--_state-layer-shape);\n --md-focus-ring-shape-end-end: var(--_state-layer-shape);\n --md-focus-ring-shape-end-start: var(--_state-layer-shape);\n }\n\n md-ripple {\n border-radius: var(--_state-layer-shape);\n }\n\n .icon {\n display: inline-flex;\n border-radius: var(--_state-layer-shape);\n overflow: hidden;\n }\n\n .touch {\n position: absolute;\n height: max(48px, 100%);\n width: max(48px, 100%);\n }\n\n .user-icon,\n .avatar-initials {\n background-color: var(--md-sys-color-primary-container, #9eeffe);\n color: var(--md-sys-color-on-primary-container, #001f24);\n }\n\n .avatar-initials {\n text-transform: uppercase;\n font-size: large;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .user-icon,\n .icon-button,\n .icon {\n height: inherit;\n width: inherit;\n }\n\n .avatar-background {\n fill: var(--md-sys-color-primary-container, #9eeffe);\n }\n\n .avatar-lines {\n fill: var(--md-sys-color-on-primary-container, #001f24);\n }\n`\n"]}
@@ -48,8 +48,8 @@
48
48
  <dt><a href="authorization/index.html">Authorization element</a></dt>
49
49
  <dd>Elements to define HTTP authorization.</dd>
50
50
 
51
- <dt><a href="store/index.html">Net store UI</a></dt>
52
- <dd>UIs related to the net-store library.</dd>
51
+ <dt><a href="user/user-avatar.html">User avatar</a></dt>
52
+ <dd>User avatar element.</dd>
53
53
  </dl>
54
54
  </nav>
55
55
  </main>
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">
7
+ <title>User Avatar</title>
8
+ <link href="../../../src/styles/m3/tokens.css" rel="stylesheet" type="text/css" />
9
+ <link href="../../../src/styles/m3/theme.css" rel="stylesheet" type="text/css" />
10
+ </head>
11
+
12
+ <body class="demo">
13
+ <div id="app"></div>
14
+ <script type="module" src="../../../.tmp/demo/demo/elements/user/user-avatar.js"></script>
15
+ </body>
16
+
17
+ </html>
@@ -0,0 +1,56 @@
1
+ import { IUser } from '@api-client/core/models/store/User.js'
2
+ import { DemoPage } from '../../../src/demo/DemoPage.js'
3
+ import { reactive } from '../../../src/decorators/index.js'
4
+ import { UserKind } from '@api-client/core/models/kinds.js'
5
+ import { html, TemplateResult } from 'lit'
6
+ import '../../../src/elements/user/ui-user-avatar.js'
7
+
8
+ class ComponentDemoPage extends DemoPage {
9
+ @reactive() accessor user1: IUser
10
+ @reactive() accessor user2: IUser
11
+
12
+ constructor() {
13
+ super()
14
+ this.componentName = 'User Avatar'
15
+ this.user1 = {
16
+ key: '1',
17
+ name: 'John Doe',
18
+ email: [{ email: 'john.doe @example.com', verified: true }],
19
+ picture: {
20
+ url: 'https://i.pravatar.cc/300',
21
+ },
22
+ kind: UserKind,
23
+ status: 'active',
24
+ grantType: 'editor',
25
+ }
26
+ this.user2 = {
27
+ key: '2',
28
+ name: 'John Doe',
29
+ email: [{ email: 'john.doe @example.com', verified: true }],
30
+ kind: UserKind,
31
+ status: 'active',
32
+ grantType: 'editor',
33
+ }
34
+ }
35
+
36
+ contentTemplate(): TemplateResult {
37
+ return html`
38
+ <a href="./">Back</a>
39
+ <section class="centered">
40
+ <h3>Full data</h3>
41
+ <ui-user-avatar .user="${this.user1}"></ui-user-avatar>
42
+ <h3>No image</h3>
43
+ <ui-user-avatar .user="${this.user2}"></ui-user-avatar>
44
+ <h3>No user</h3>
45
+ <ui-user-avatar></ui-user-avatar>
46
+ <h3>Custom size</h3>
47
+ <ui-user-avatar style="width: 128px; height: 128px" .user="${this.user1}"></ui-user-avatar>
48
+ <h3>Icon type</h3>
49
+ <ui-user-avatar .user="${this.user1}" type="icon"></ui-user-avatar>
50
+ </section>
51
+ `
52
+ }
53
+ }
54
+
55
+ const instance = new ComponentDemoPage()
56
+ instance.render()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@api-client/ui",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Internal UI component library for the API Client ecosystem.",
5
5
  "license": "UNLICENSED",
6
6
  "main": "build/src/index.js",
@@ -67,7 +67,7 @@
67
67
  "tsc:watch": "wireit",
68
68
  "tsc": "wireit",
69
69
  "gen:apis": "node demo/model.js",
70
- "dev": "node --import ts-node-maintained/register/esm --enable-source-maps bin/dev.ts --watch --config=wds-demo.config.js",
70
+ "dev": "wireit",
71
71
  "tsc:tests": "wireit",
72
72
  "tsc:demo": "wireit"
73
73
  },
@@ -153,6 +153,25 @@
153
153
  ".tmp/tests/**",
154
154
  ".tsbuildinfo"
155
155
  ]
156
+ },
157
+ "dev": {
158
+ "command": "wds --watch --config=wds-demo.config.js",
159
+ "files": [
160
+ "demo/**"
161
+ ],
162
+ "service": true,
163
+ "dependencies": [
164
+ "build:ts:watch"
165
+ ]
166
+ },
167
+ "build:ts:watch": {
168
+ "command": "tsc --watch --project tsconfig.browser.json",
169
+ "files": [
170
+ "src/**",
171
+ "demo/**",
172
+ "tsconfig.browser.json"
173
+ ],
174
+ "service": true
156
175
  }
157
176
  },
158
177
  "dependencies": {
package/src/core/Model.ts CHANGED
@@ -1,4 +1,58 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
+
3
+ import type { Exception } from '@api-client/core/exceptions/exception.js'
4
+
5
+ /**
6
+ * Defines the options for the retry mechanism.
7
+ */
8
+ export interface RetryOptions {
9
+ /**
10
+ * The maximum number of retry attempts.
11
+ * Defaults to 3.
12
+ */
13
+ retries?: number
14
+ /**
15
+ * The delay in milliseconds before the next retry.
16
+ * Can be a fixed number or a function that returns a number (e.g., for exponential backoff).
17
+ * Defaults to 1000ms.
18
+ */
19
+ delayMs?: number | ((attempt: number) => number)
20
+ /**
21
+ * An optional function that determines if a retry should be attempted based on the error.
22
+ * If not provided, all errors will trigger a retry up to the maximum number of retries.
23
+ * @param error The error that occurred.
24
+ * @returns True if a retry should be attempted, false otherwise.
25
+ */
26
+ shouldRetry?: (error: Error) => boolean
27
+ }
28
+
29
+ /**
30
+ * Calculates an exponential backoff delay for retry attempts.
31
+ * The delay doubles with each attempt.
32
+ *
33
+ * @param attempt The current retry attempt number (should be 1-indexed).
34
+ * @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.
35
+ * @returns The calculated delay in milliseconds.
36
+ */
37
+ export function exponentialBackoffDelay(attempt: number, initialDelay = 1000): number {
38
+ if (attempt <= 0) return initialDelay
39
+ // For attempt 1, delay is initialDelay * 2^0 = initialDelay
40
+ return initialDelay * Math.pow(2, attempt - 1)
41
+ }
42
+ /**
43
+ * Checks if the API error should be retried based on its status code.
44
+ * If the status code is 404, it returns false (do not retry).
45
+ * For other status codes, it returns true (retry).
46
+ * @param error The error to check.
47
+ */
48
+ export function shouldRetryApiError(error: Exception): boolean {
49
+ const { status } = error
50
+ if (status === 404) {
51
+ // Do not retry on 404 errors
52
+ return false
53
+ }
54
+ return true
55
+ }
2
56
  /**
3
57
  * The base class for models.
4
58
  *
@@ -58,6 +112,13 @@ export abstract class Model<T> extends EventTarget {
58
112
  return this.#taskComplete
59
113
  }
60
114
 
115
+ /**
116
+ * A getter for the raw value.
117
+ */
118
+ get value(): T | undefined {
119
+ return this.raw
120
+ }
121
+
61
122
  /**
62
123
  * @param obj The source object to use.
63
124
  */
@@ -126,4 +187,62 @@ export abstract class Model<T> extends EventTarget {
126
187
  resolver()
127
188
  }
128
189
  }
190
+
191
+ /**
192
+ * Executes an asynchronous operation with a retry mechanism.
193
+ * @param operation A function that returns a Promise to be executed.
194
+ * @param options Optional retry configurations.
195
+ * @returns A Promise that resolves with the result of the operation or rejects if all retries fail.
196
+ * @example
197
+ * ```typescript
198
+ * override async create(data: Partial<IUser>): Promise<IUser> {
199
+ * const operation = async (): Promise<UserData> => {
200
+ * // ... perform API call to create user
201
+ * }
202
+ * const retryOptions: RetryOptions = {
203
+ * retries: 5,
204
+ * delayMs: exponentialBackoffDelay,
205
+ * shouldRetry: shouldRetryApiError,
206
+ * }
207
+ * this.raw = await this.retry(operation)
208
+ * this.dispatchEvent(new CustomEvent('data-changed', { detail: this.raw }))
209
+ * return this.raw;
210
+ * }
211
+ * ```
212
+ */
213
+ protected async retry<R>(operation: () => Promise<R>, options: RetryOptions = {}): Promise<R> {
214
+ const { retries = 3, delayMs = 1000, shouldRetry = () => true } = options
215
+
216
+ let lastError: Error | undefined
217
+
218
+ for (let attempt = 0; attempt < retries; attempt++) {
219
+ try {
220
+ return await operation()
221
+ } catch (e) {
222
+ lastError = e as Error
223
+ if (!shouldRetry(lastError) || attempt === retries - 1) {
224
+ // Do not retry if shouldRetry returns false or if it's the last attempt
225
+ this.notifyError(lastError)
226
+ throw lastError
227
+ }
228
+
229
+ const delay = typeof delayMs === 'function' ? delayMs(attempt + 1) : delayMs
230
+ if (delay > 0) {
231
+ await new Promise((resolve) => setTimeout(resolve, delay))
232
+ }
233
+ }
234
+ }
235
+ // This part should ideally not be reached if retries > 0,
236
+ // as the loop would throw on the last attempt.
237
+ // However, to satisfy TypeScript and guard against retries = 0:
238
+ if (lastError) {
239
+ this.notifyError(lastError)
240
+ throw lastError
241
+ }
242
+ // Should not happen if operation is called at least once.
243
+ // Adding a generic error for completeness.
244
+ const err = new Error('Retry operation failed without a specific error.')
245
+ this.notifyError(err)
246
+ throw err
247
+ }
129
248
  }
@@ -6,8 +6,8 @@ export default css`
6
6
  outline: none;
7
7
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
8
8
  justify-content: center;
9
- height: var(--_state-layer-height);
10
- width: var(--_state-layer-width);
9
+ height: 40px;
10
+ width: 40px;
11
11
 
12
12
  --_focus-icon-color: var(--md-icon-button-focus-icon-color, var(--md-sys-color-on-surface-variant, #49454f));
13
13
  --_hover-icon-color: var(--md-icon-button-hover-icon-color, var(--md-sys-color-on-surface-variant, #49454f));
@@ -24,8 +24,6 @@ export default css`
24
24
  );
25
25
  --_pressed-state-layer-opacity: var(--md-icon-button-pressed-state-layer-opacity, 0.12);
26
26
  --_state-layer-shape: var(--md-icon-button-state-layer-shape, var(--md-sys-shape-corner-full, 9999px));
27
- --_state-layer-height: 40px;
28
- --_state-layer-width: 40px;
29
27
  }
30
28
 
31
29
  .icon-button {
@@ -89,15 +87,13 @@ export default css`
89
87
 
90
88
  .user-icon,
91
89
  .avatar-initials {
92
- background-color: var(--user-avatar-initials-background-color, #0540f2);
93
- color: var(--user-avatar-initials-color, #fff);
90
+ background-color: var(--md-sys-color-primary-container, #9eeffe);
91
+ color: var(--md-sys-color-on-primary-container, #001f24);
94
92
  }
95
93
 
96
94
  .avatar-initials {
97
- /* border-radius: 50%; */
98
95
  text-transform: uppercase;
99
96
  font-size: large;
100
- border: 1px #0d47a1 solid;
101
97
  width: 100%;
102
98
  height: 100%;
103
99
  display: flex;
@@ -105,8 +101,18 @@ export default css`
105
101
  justify-content: center;
106
102
  }
107
103
 
108
- .user-icon {
109
- height: var(--_state-layer-height);
110
- width: var(--_state-layer-width);
104
+ .user-icon,
105
+ .icon-button,
106
+ .icon {
107
+ height: inherit;
108
+ width: inherit;
109
+ }
110
+
111
+ .avatar-background {
112
+ fill: var(--md-sys-color-primary-container, #9eeffe);
113
+ }
114
+
115
+ .avatar-lines {
116
+ fill: var(--md-sys-color-on-primary-container, #001f24);
111
117
  }
112
118
  `
@@ -1,12 +1,12 @@
1
- import { html, TemplateResult, LitElement } from 'lit'
1
+ import { html, TemplateResult, LitElement, PropertyValues, SVGTemplateResult, svg } from 'lit'
2
2
  import { property, state } from 'lit/decorators.js'
3
3
  import type { IUser } from '@api-client/core/models/store/User.js'
4
4
  import '@material/web/focus/md-focus-ring.js'
5
5
  import '@material/web/ripple/ripple.js'
6
6
 
7
- export default class UserAvatar extends LitElement {
8
- private _user?: IUser
7
+ export type AvatarType = 'button' | 'icon'
9
8
 
9
+ export default class UserAvatar extends LitElement {
10
10
  /**
11
11
  * Set with the user. The computed user initials.
12
12
  */
@@ -17,30 +17,30 @@ export default class UserAvatar extends LitElement {
17
17
  */
18
18
  @state() protected accessor userPicture: string | undefined
19
19
 
20
- get user(): IUser | undefined {
21
- return this._user
22
- }
23
-
24
20
  /**
21
+ * The user object to display the avatar for.
22
+ */
23
+ @property({ type: Object }) accessor user: IUser | undefined
24
+ /**
25
+ * The type of avatar to render, either 'button' or 'icon'.
25
26
  * @attribute
26
27
  */
27
- @property({ type: Object })
28
- set user(value: IUser | undefined) {
29
- const old = this._user
30
- if (old === value) {
31
- return
32
- }
33
- if (old && value && old.key === value.key) {
34
- return
28
+ @property({ type: String }) accessor type: AvatarType = 'button'
29
+
30
+ protected override willUpdate(cp: PropertyValues<this>): void {
31
+ if (cp.has('user')) {
32
+ this.handleUserChange(this.user)
35
33
  }
36
- this._user = value
37
- this.requestUpdate('user', old)
38
- this._processUser(value)
34
+ super.willUpdate(cp)
39
35
  }
40
36
 
41
- protected _processUser(user?: IUser): void {
37
+ /**
38
+ * Handles changes to the user property.
39
+ * @param user The user object
40
+ */
41
+ protected handleUserChange(user?: IUser): void {
42
42
  if (user) {
43
- this.userInitials = this._readUserInitials(user)
43
+ this.userInitials = this.readUserInitials(user)
44
44
  this.userPicture = user.picture && user.picture.url
45
45
  } else {
46
46
  this.userInitials = undefined
@@ -48,7 +48,7 @@ export default class UserAvatar extends LitElement {
48
48
  }
49
49
  }
50
50
 
51
- protected _readUserInitials(user: IUser): string | undefined {
51
+ protected readUserInitials(user: IUser): string | undefined {
52
52
  const { name } = user
53
53
  if (!name) {
54
54
  return undefined
@@ -62,7 +62,7 @@ export default class UserAvatar extends LitElement {
62
62
  return parts.join('')
63
63
  }
64
64
 
65
- protected _pictureError(): void {
65
+ protected handlePictureError(): void {
66
66
  this.userPicture = undefined
67
67
  }
68
68
 
@@ -70,18 +70,22 @@ export default class UserAvatar extends LitElement {
70
70
  * @return Template result for an icon
71
71
  */
72
72
  override render(): TemplateResult {
73
- const { user, userPicture } = this
74
- if (!user || user.key === 'default') {
75
- return html``
76
- }
73
+ const { userPicture } = this
77
74
  let content: TemplateResult
78
75
  if (userPicture) {
79
76
  content = this.pictureTemplate(userPicture)
80
77
  } else if (this.userInitials) {
81
78
  content = this.nameTemplate(this.userInitials)
82
79
  } else {
83
- content = this.nameTemplate('')
80
+ content = this.renderDefaultAvatar()
81
+ }
82
+ if (this.type === 'icon') {
83
+ return this.renderIcon(content)
84
84
  }
85
+ return this.renderButton(content)
86
+ }
87
+
88
+ protected renderButton(content: TemplateResult): TemplateResult {
85
89
  return html`
86
90
  <button id="button" class="icon-button">
87
91
  <md-focus-ring part="focus-ring" for="button"></md-focus-ring>
@@ -92,13 +96,17 @@ export default class UserAvatar extends LitElement {
92
96
  `
93
97
  }
94
98
 
99
+ protected renderIcon(content: TemplateResult): TemplateResult {
100
+ return html`<span role="presentation" class="icon">${content}</span>`
101
+ }
102
+
95
103
  protected pictureTemplate(url: string): TemplateResult {
96
104
  return html`
97
105
  <img
98
106
  src="${url}"
99
107
  alt="${this.userInitials || 'Thumb'}"
100
108
  class="user-picture user-icon"
101
- @error="${this._pictureError}"
109
+ @error="${this.handlePictureError}"
102
110
  />
103
111
  `
104
112
  }
@@ -110,4 +118,26 @@ export default class UserAvatar extends LitElement {
110
118
  protected nameTemplate(initials: string): TemplateResult {
111
119
  return html` <span class="avatar-initials user-icon">${initials}</span> `
112
120
  }
121
+
122
+ protected renderDefaultAvatar(): TemplateResult {
123
+ return html`<span class="avatar-default icon"> ${this.defaultIcon()} </span>`
124
+ }
125
+
126
+ protected defaultIcon(): SVGTemplateResult {
127
+ return svg`
128
+ <svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
129
+ <rect width="120" height="120" rx="60" class="avatar-background" />
130
+ <path
131
+ fill-rule="evenodd"
132
+ clip-rule="evenodd"
133
+ d="M78.0007 48C78.0007 57.9411 69.9419 66 60.0007 66C50.0596 66 42.0007 57.9411 42.0007 48C42.0007 38.0589 50.0596 30 60.0007 30C69.9419 30 78.0007 38.0589 78.0007 48ZM72.0007 48C72.0007 54.6274 66.6282 60 60.0007 60C53.3733 60 48.0007 54.6274 48.0007 48C48.0007 41.3726 53.3733 36 60.0007 36C66.6282 36 72.0007 41.3726 72.0007 48Z"
134
+ class="avatar-lines"
135
+ />
136
+ <path
137
+ d="M60.0007 75C40.5776 75 24.0286 86.4852 17.7246 102.576C19.2603 104.101 20.878 105.543 22.5706 106.896C27.2648 92.1231 41.9909 81 60.0007 81C78.0106 81 92.7367 92.1231 97.4309 106.896C99.1235 105.544 100.741 104.101 102.277 102.576C95.973 86.4853 79.4239 75 60.0007 75Z"
138
+ class="avatar-lines"
139
+ />
140
+ </svg>
141
+ `
142
+ }
113
143
  }
@@ -17,6 +17,7 @@
17
17
  "rootDir": "./",
18
18
  "baseUrl": ".",
19
19
  "sourceMap": true,
20
+ "declarationMap": true,
20
21
  "declaration": true,
21
22
  "noImplicitThis": true,
22
23
  "noImplicitAny": true,