tosijs-ui 1.16.17 → 1.16.18

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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.16.18
4
+
5
+ - **A `defineIcons` name for a stacked composition draws the stack.** `defineIcons({
6
+ cloudSync: 'spin120Loader40s_30x$cloud' })` made `icons.cloudSync()` draw the placeholder
7
+ square and log "icon cloudSync does not exist", although the same string used directly as
8
+ an icon name worked. The icons page documents exactly this pattern for building an app's
9
+ icon vocabulary, so every such name was silently a square, in every version since
10
+ composites shipped. A redirect's target now goes through the whole icon pipeline, stacking
11
+ included. closes #224, reported by tosijs-virta.
12
+ - **A named composite answers to its name.** Its `data-icon` is the name you defined
13
+ (`cloudSync`), not the composition it stands for, and suffixes and rules apply to it as
14
+ to any icon (`cloudSync50o`, `spin90CloudSync`).
15
+ - **A redirect cycle says so.** `a: 'b'` with `b: 'a'` now logs "icon a redirects in a
16
+ cycle" and draws the placeholder, instead of claiming the icon does not exist; a
17
+ composite that names itself (`a: 'a$check'`) draws the placeholder in place of the
18
+ cycling layer. A long redirect chain that ends in a drawing is no longer cut off at ten
19
+ hops.
20
+ - The named-composition example on the icons page is now a live example with a test, so
21
+ the documented pattern is checked on every build.
22
+
3
23
  ## 1.16.17
4
24
 
5
25
  - **The pinned tjs-lang is 0.14.0-rc.5; it was 0.13.13.** That is the devDependency this
package/dist/icons.js CHANGED
@@ -485,16 +485,36 @@ Now `removeUser`, `removeFile`, `removeProject` all just work — and
485
485
  they're consistent. If you redesign the trash icon or change
486
486
  `--action-color`, every "remove" icon updates automatically.
487
487
 
488
- You can also use `defineIcons` for one-off named compositions:
488
+ You can also use `defineIcons` for one-off named compositions. Then just
489
+ use `icons.cloudSync()` or `<tosi-icon icon="cloudSync">`: the composition
490
+ is invisible to consumers of the icon.
489
491
 
490
- defineIcons({
491
- cloudSync: 'spin120Loader40s_30x$cloud',
492
- secureShield: 'lock50s75o_10y$shield',
493
- newFile: 'plus75o60s25x25y$file',
494
- })
492
+ ```js
493
+ import { defineIcons, icons } from 'tosijs-ui'
495
494
 
496
- Then just use `icons.cloudSync()` or `<tosi-icon icon="cloudSync">` —
497
- the composition is invisible to consumers of the icon.
495
+ defineIcons({
496
+ cloudSync: 'spin120Loader40s_30x$cloud',
497
+ secureShield: 'lock50s75o_10y$shield',
498
+ newFile: 'plus75o60s25x25y$file',
499
+ })
500
+
501
+ preview.style.setProperty('--tosi-icon-size', '48px')
502
+ preview.append(icons.cloudSync(), icons.secureShield(), icons.newFile())
503
+ ```
504
+ ```test
505
+ test('named compositions draw their composites, not the placeholder', () => {
506
+ // A spin layer is a composite of its own, so count only the outer ones.
507
+ const composites = preview.querySelectorAll(':scope > .tosi-icon-composite')
508
+ expect([...composites].map((icon) => icon.dataset.icon)).toEqual([
509
+ 'cloudSync',
510
+ 'secureShield',
511
+ 'newFile',
512
+ ])
513
+ for (const composite of composites) {
514
+ expect(composite.querySelectorAll('svg').length).toBeGreaterThan(1)
515
+ }
516
+ })
517
+ ```
498
518
 
499
519
  ### Composites and `svg2DataUrl`
500
520
 
@@ -767,7 +787,8 @@ function composeIcon(prop, parts) {
767
787
  }
768
788
  return null;
769
789
  }
770
- const MAX_REDIRECTS = 10;
790
+ // Redirect names being resolved right now, so a redirect cycle ends instead of recursing.
791
+ const expanding = new Set();
771
792
  // Style suffixes — value then letter code:
772
793
  // 50o (opacity), 75s (scale), 20x (translateX%), _10y (translateY%)
773
794
  // 90r (rotate 90°), _45r (rotate -45°), 0f (flipH), 1f (flipV)
@@ -857,24 +878,41 @@ export function resolveIcon(prop, parts) {
857
878
  const data = iconData;
858
879
  // Direct match or redirect chain
859
880
  let name = prop;
860
- for (let i = 0; i < MAX_REDIRECTS; i++) {
861
- const spec = data[name];
862
- if (!spec)
863
- break;
881
+ const seen = new Set();
882
+ for (let spec = data[name]; spec; spec = data[name]) {
864
883
  if (spec.startsWith('<'))
865
884
  return makeIcon(spec, parts);
885
+ seen.add(name);
866
886
  name = spec;
887
+ if (seen.has(name)) {
888
+ console.warn(`icon ${prop} redirects in a cycle`);
889
+ return makeIcon(iconData.square, parts);
890
+ }
867
891
  }
868
892
  if (name !== prop) {
869
- const composed = composeIcon(name, parts);
870
- if (composed)
871
- return composed;
872
- const parsed = parseStyleSuffixes(name);
873
- if (parsed) {
874
- const icon = resolveIcon(parsed.baseName, parts);
875
- Object.assign(icon.style, parsed.style);
876
- return icon;
893
+ /*
894
+ A redirect's target goes through the WHOLE pipeline, stacking included. Handling rules
895
+ and suffixes here and leaving `$` to the checks below tested `prop`, which has no `$`, so
896
+ every named composite drew the placeholder (#224). `expanding` stops a redirect that
897
+ names itself (`a: 'a$b'`) from recursing forever.
898
+ */
899
+ if (!expanding.has(prop)) {
900
+ expanding.add(prop);
901
+ try {
902
+ const icon = resolveIcon(name, parts);
903
+ // A named composite answers to its name, not to the composition it stands for.
904
+ if (icon.classList.contains('tosi-icon-composite')) {
905
+ ;
906
+ icon.dataset.icon = prop;
907
+ }
908
+ return icon;
909
+ }
910
+ finally {
911
+ expanding.delete(prop);
912
+ }
877
913
  }
914
+ console.warn(`icon ${prop} redirects in a cycle`);
915
+ return makeIcon(iconData.square, parts);
878
916
  }
879
917
  // Stack: foo$bar$baz → baz on bottom, bar on top, foo on top
880
918
  if (prop.includes('$')) {