@ethlete/core 5.0.0-next.59 → 5.0.0-next.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @ethlete/core
2
2
 
3
+ ## 5.0.0-next.60
4
+
5
+ ### Minor Changes
6
+
7
+ - `provideSurfaceThemesWithTailwind4` now warns in dev mode when the page leaves the root font size at the browser default instead of setting `html { font-size: 62.5%; }`.
8
+
9
+ ### Patch Changes
10
+
11
+ - Element signals such as `signalElementIntersection` now accept a signal of a read-only array, so a `viewChildren` signal can be passed directly.
12
+ - `htmlToMarkdown` keeps block alignment as an `et-rte-align-*` class instead of a `style` attribute a strict CSP blocks, and structured-data scripts carry the `CSP_NONCE`. Other renderers need CSS for that class.
13
+ - The `provideX` functions from `defineProvider`, `defineRootProvider` and `defineStaticProvider` return `StaticProvider[]` instead of `Provider[]`. You can now spread them into a `StaticProvider[]`, for example into `Injector.create` or an overlay's `providers`.
14
+ - `defineStaticProvider` and `defineStaticRootProvider` now replace an array or other non-object default with the override, instead of spreading the array into an object. Only two plain objects are merged.
15
+ - `migrate-to-v5` lists every file that still uses the removed `@Memo` decorator or props module (`createProps`, `[etProps]`, …), which have no replacement.
16
+
3
17
  ## 5.0.0-next.59
4
18
 
5
19
  ### Patch Changes
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { assertInInjectionContext, inject, DestroyRef, ElementRef, TemplateRef, InjectionToken, QueryList, RendererFactory2, RendererStyleFlags2, isSignal, DOCUMENT, signal, linkedSignal, isDevMode, computed, afterNextRender, effect, untracked, ApplicationRef, runInInjectionContext, NgZone, Injector, booleanAttribute, numberAttribute, PLATFORM_ID, EnvironmentInjector, createComponent, ViewEncapsulation, Component, Directive, model, ViewContainerRef, input, provideEnvironmentInitializer, output, createEnvironmentInjector, Pipe, afterEveryRender } from '@angular/core';
2
+ import { assertInInjectionContext, inject, DestroyRef, ElementRef, TemplateRef, InjectionToken, QueryList, RendererFactory2, RendererStyleFlags2, isSignal, DOCUMENT, signal, linkedSignal, isDevMode, computed, afterNextRender, effect, untracked, ApplicationRef, runInInjectionContext, NgZone, Injector, booleanAttribute, numberAttribute, PLATFORM_ID, EnvironmentInjector, createComponent, ViewEncapsulation, Component, Directive, model, ViewContainerRef, input, CSP_NONCE, provideEnvironmentInitializer, output, createEnvironmentInjector, Pipe, afterEveryRender } from '@angular/core';
3
3
  import { takeUntilDestroyed, toSignal, toObservable, outputFromObservable } from '@angular/core/rxjs-interop';
4
4
  import { Subject, fromEvent, switchMap, startWith, of, defer, from, map, catchError, timer, takeUntil, pairwise, debounceTime, merge, distinctUntilChanged, EMPTY, expand, skip, tap, take, filter, BehaviorSubject, combineLatest, Observable, isObservable, firstValueFrom, defaultIfEmpty, concatMap, concat, finalize, exhaustMap, share, shareReplay } from 'rxjs';
5
5
  import { FormGroup, FormArray, FormControl } from '@angular/forms';
@@ -74,6 +74,36 @@ const createDestroy = () => {
74
74
  return destroy$.asObservable();
75
75
  };
76
76
 
77
+ const isObject = (value) => {
78
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
79
+ };
80
+ const isArray = (value) => {
81
+ return Array.isArray(value);
82
+ };
83
+ const getObjectProperty = (obj, prop) => {
84
+ const hasDotNotation = prop.includes('.');
85
+ const hasBracketNotation = prop.includes('[');
86
+ if (!hasDotNotation && !hasBracketNotation)
87
+ return obj[prop];
88
+ const props = prop.split('.');
89
+ let value = obj;
90
+ for (const prop of props) {
91
+ if (!isObject(value))
92
+ return undefined;
93
+ if (prop.includes('[')) {
94
+ const [key, index] = prop.split('[').map((part) => part.replace(']', ''));
95
+ const arr = value[key];
96
+ if (!Array.isArray(arr))
97
+ return undefined;
98
+ value = arr[+index];
99
+ }
100
+ else {
101
+ value = value[prop];
102
+ }
103
+ }
104
+ return value;
105
+ };
106
+
77
107
  const injectHostElement = () => inject(ElementRef).nativeElement;
78
108
  const injectTemplateRef = () => inject(TemplateRef);
79
109
  const createInjectFunction = (token) => {
@@ -156,7 +186,7 @@ const createValueProviders = (token, defaultValue, valueOverride, options) => [
156
186
  ...(options?.extraInjectionToken ? [{ provide: options.extraInjectionToken, useExisting: token }] : []),
157
187
  ];
158
188
  const maybeMergeValues = (defaultValue, valueOverride) => {
159
- if (valueOverride && defaultValue && typeof defaultValue === 'object') {
189
+ if (isObject(valueOverride) && isObject(defaultValue)) {
160
190
  return { ...defaultValue, ...valueOverride };
161
191
  }
162
192
  return valueOverride ?? defaultValue;
@@ -3803,13 +3833,13 @@ const htmlToMarkdown = (html) => {
3803
3833
  if (!html)
3804
3834
  return '';
3805
3835
  let md = html;
3806
- // Aligned blocks: text-align has no Markdown form, so preserve them verbatim as native HTML (their
3807
- // inner markup stays HTML) and round-trip via a placeholder - extracted before the block passes
3808
- // below rewrite them, restored after the final tag-strip.
3836
+ // Aligned blocks: text-align has no Markdown form, so preserve them as HTML (their inner markup
3837
+ // stays HTML) and round-trip via a placeholder - extracted before the block passes below rewrite
3838
+ // them, restored after the final tag-strip.
3809
3839
  const alignedBlocks = [];
3810
3840
  md = md.replace(/<(p|h[1-6]|div)\b([^>]*\b(?:style="[^"]*text-align|class="[^"]*\bet-rte-align-)[^>]*)>([\s\S]*?)<\/\1>/gi, (_, tag, attrs, inner) => {
3811
3841
  const name = tag.toLowerCase();
3812
- return makePlaceholder('ALIGN', alignedBlocks.push(`<${name} style="text-align: ${alignOf(attrs)}">${inner}</${name}>`) - 1);
3842
+ return makePlaceholder('ALIGN', alignedBlocks.push(`<${name}${alignClass(alignOf(attrs))}>${inner}</${name}>`) - 1);
3813
3843
  });
3814
3844
  // Code blocks - process before inline code
3815
3845
  md = md.replace(/<pre[^>]*><code[^>]*class="language-([^"]+)"[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, lang, code) => `\n\`\`\`${lang}\n${unescapeHtml(code.trim())}\n\`\`\`\n`);
@@ -3907,36 +3937,6 @@ const round = (value, precision = 0) => {
3907
3937
  return Math.round(value * multiplier) / multiplier;
3908
3938
  };
3909
3939
 
3910
- const isObject = (value) => {
3911
- return typeof value === 'object' && value !== null && !Array.isArray(value);
3912
- };
3913
- const isArray = (value) => {
3914
- return Array.isArray(value);
3915
- };
3916
- const getObjectProperty = (obj, prop) => {
3917
- const hasDotNotation = prop.includes('.');
3918
- const hasBracketNotation = prop.includes('[');
3919
- if (!hasDotNotation && !hasBracketNotation)
3920
- return obj[prop];
3921
- const props = prop.split('.');
3922
- let value = obj;
3923
- for (const prop of props) {
3924
- if (!isObject(value))
3925
- return undefined;
3926
- if (prop.includes('[')) {
3927
- const [key, index] = prop.split('[').map((part) => part.replace(']', ''));
3928
- const arr = value[key];
3929
- if (!Array.isArray(arr))
3930
- return undefined;
3931
- value = arr[+index];
3932
- }
3933
- else {
3934
- value = value[prop];
3935
- }
3936
- }
3937
- return value;
3938
- };
3939
-
3940
3940
  /**
3941
3941
  * Creates an object URL for a `Blob`/`File` and hands back a handle that owns it. Keep the handle
3942
3942
  * for as long as the URL is on screen and call `revoke()` when it is not - the browser holds the
@@ -6182,6 +6182,7 @@ const STRUCTURED_DATA_STORE_DEF = /* @__PURE__ */ defineRootProvider(() => {
6182
6182
  const document = inject(DOCUMENT$1);
6183
6183
  const renderer = injectRenderer();
6184
6184
  const config = injectStructuredDataConfig();
6185
+ const nonce = inject(CSP_NONCE, { optional: true });
6185
6186
  const scripts = signal(new Map(), /* @ts-ignore */
6186
6187
  ...(ngDevMode ? [{ debugName: "scripts" }] : /* istanbul ignore next */ []));
6187
6188
  const getTargetElement = () => {
@@ -6196,6 +6197,8 @@ const STRUCTURED_DATA_STORE_DEF = /* @__PURE__ */ defineRootProvider(() => {
6196
6197
  }
6197
6198
  const script = renderer.createElement('script');
6198
6199
  renderer.setAttributes(script, { type: 'application/ld+json' });
6200
+ if (nonce)
6201
+ renderer.setAttribute(script, 'nonce', nonce);
6199
6202
  renderer.setTextContent(script, JSON.stringify(data));
6200
6203
  const target = getTargetElement();
6201
6204
  renderer.appendChild(target, script);
@@ -9295,6 +9298,68 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
9295
9298
  }]
9296
9299
  }], ctorParameters: () => [] });
9297
9300
 
9301
+ const TOLERANCE_PX = 0.5;
9302
+ const readFontSizePx = (view, element) => {
9303
+ const match = /^(\d+(?:\.\d+)?)px$/.exec(view.getComputedStyle(element).fontSize ?? '');
9304
+ return match ? Number(match[1]) : null;
9305
+ };
9306
+ const measureProbe = (document, view, fontSize) => {
9307
+ const probe = document.createElement('div');
9308
+ probe.style.setProperty('display', 'none', 'important');
9309
+ probe.style.setProperty('font-size', fontSize, 'important');
9310
+ document.documentElement.appendChild(probe);
9311
+ try {
9312
+ return readFontSizePx(view, probe);
9313
+ }
9314
+ finally {
9315
+ probe.remove();
9316
+ }
9317
+ };
9318
+ const measureUnsetRootFontSize = (document) => {
9319
+ const view = document.defaultView;
9320
+ if (!view)
9321
+ return null;
9322
+ const root = readFontSizePx(view, document.documentElement);
9323
+ const browserDefault = measureProbe(document, view, 'medium');
9324
+ const smallest = measureProbe(document, view, '1px');
9325
+ if (root === null || browserDefault === null || smallest === null)
9326
+ return null;
9327
+ // A browser minimum font size at or above the default clamps any root to the default.
9328
+ if (smallest >= browserDefault - TOLERANCE_PX)
9329
+ return null;
9330
+ return Math.abs(root - browserDefault) <= TOLERANCE_PX ? root : null;
9331
+ };
9332
+ const warnIfRootFontSizeUnset = (document) => {
9333
+ const root = measureUnsetRootFontSize(document);
9334
+ if (root === null)
9335
+ return;
9336
+ console.warn(`[provideSurfaceThemesWithTailwind4] The root font size is ${root}px, the browser default, so every SDK control renders 1.6x too large. The SDK sizes everything in rem against a 10px root (${root * 0.625}px here). Add \`html { font-size: 62.5%; }\` to your global styles. See https://ethlete-sdk-docs-next.web.app/components/setup`);
9337
+ };
9338
+ const provideRootFontSizeCheck = () => provideEnvironmentInitializer(() => {
9339
+ if (!isDevMode() || !isPlatformBrowser(inject(PLATFORM_ID)))
9340
+ return;
9341
+ const document = inject(DOCUMENT$1);
9342
+ const destroyRef = inject(DestroyRef);
9343
+ const injector = inject(EnvironmentInjector);
9344
+ afterNextRender(() => {
9345
+ const view = document.defaultView;
9346
+ if (!view)
9347
+ return;
9348
+ if (document.readyState === 'complete') {
9349
+ warnIfRootFontSizeUnset(document);
9350
+ return;
9351
+ }
9352
+ const removeListener = () => view.removeEventListener('load', onLoad);
9353
+ const unregisterOnDestroy = destroyRef.onDestroy(removeListener);
9354
+ const onLoad = () => {
9355
+ removeListener();
9356
+ unregisterOnDestroy();
9357
+ warnIfRootFontSizeUnset(document);
9358
+ };
9359
+ view.addEventListener('load', onLoad);
9360
+ }, { injector });
9361
+ });
9362
+
9298
9363
  const SURFACE_TYPE = {
9299
9364
  LIGHT: 'light',
9300
9365
  DARK: 'dark',
@@ -9311,9 +9376,14 @@ const SURFACE_THEMES_PREFIX_DEF = /* @__PURE__ */ defineStaticProvider('et', {
9311
9376
  });
9312
9377
  const ɵProvideSurfaceThemesPrefix = /* @__PURE__ */ toProvideFn(SURFACE_THEMES_PREFIX_DEF);
9313
9378
  const injectSurfaceThemesPrefix = /* @__PURE__ */ toInjectFn(SURFACE_THEMES_PREFIX_DEF);
9379
+ /**
9380
+ * Registers the app's surface themes. In dev mode it also warns once the page has loaded if the root
9381
+ * font size is still the browser default, i.e. the app is missing `html { font-size: 62.5%; }`.
9382
+ */
9314
9383
  const provideSurfaceThemesWithTailwind4 = (themes, prefix = 'et') => [
9315
9384
  ɵProvideSurfaceThemes(themes),
9316
9385
  ɵProvideSurfaceThemesPrefix(prefix),
9386
+ ngDevMode ? provideRootFontSizeCheck() : [],
9317
9387
  ];
9318
9388
  /**
9319
9389
  * Inject the registered `isDefault` surface theme of a `type` - the surface an app renders on
@@ -10074,7 +10144,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
10074
10144
 
10075
10145
  // Generated from package.json by tools/scripts/generate-version.js - do not edit.
10076
10146
  /** The version of `@ethlete/core` this build was cut from. */
10077
- const CORE_VERSION = '5.0.0-next.59';
10147
+ const CORE_VERSION = '5.0.0-next.60';
10078
10148
 
10079
10149
  /**
10080
10150
  * Generated bundle index. Do not edit.