@startlibs/utils 1.1.1 → 1.2.0-rc.1

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 (87) hide show
  1. package/lib/animationKeyframes.mjs +11 -0
  2. package/lib/animationKeyframes.mjs.map +1 -0
  3. package/lib/fixMediaQuery.mjs +43 -17
  4. package/lib/fixMediaQuery.mjs.map +1 -1
  5. package/lib/fixMediaText.mjs +16 -0
  6. package/lib/fixMediaText.mjs.map +1 -0
  7. package/lib/formatDate.mjs +1 -1
  8. package/lib/formatDate.mjs.map +1 -1
  9. package/lib/index.cjs +212 -100
  10. package/lib/index.cjs.map +1 -1
  11. package/lib/index.mjs +3 -1
  12. package/lib/index.mjs.map +1 -1
  13. package/lib/insertKeyframes.mjs +68 -0
  14. package/lib/insertKeyframes.mjs.map +1 -0
  15. package/lib/styled.mjs +22 -141
  16. package/lib/styled.mjs.map +1 -1
  17. package/lib/styledShared.mjs +125 -0
  18. package/lib/styledShared.mjs.map +1 -0
  19. package/lib/timerUtils.mjs +0 -1
  20. package/lib/timerUtils.mjs.map +1 -1
  21. package/lib/yakDomTag.mjs +44 -0
  22. package/lib/yakDomTag.mjs.map +1 -0
  23. package/lib-yak/ActionUtils.mjs +117 -0
  24. package/lib-yak/ActionUtils.mjs.map +1 -0
  25. package/lib-yak/FetchUtils.mjs +100 -0
  26. package/lib-yak/FetchUtils.mjs.map +1 -0
  27. package/lib-yak/animationKeyframes.mjs +31 -0
  28. package/lib-yak/animationKeyframes.mjs.map +1 -0
  29. package/lib-yak/createPressEvent.mjs +74 -0
  30. package/lib-yak/createPressEvent.mjs.map +1 -0
  31. package/lib-yak/fixMediaQuery.mjs +90 -0
  32. package/lib-yak/fixMediaQuery.mjs.map +1 -0
  33. package/lib-yak/fixMediaText.mjs +16 -0
  34. package/lib-yak/fixMediaText.mjs.map +1 -0
  35. package/lib-yak/formatDate.mjs +110 -0
  36. package/lib-yak/formatDate.mjs.map +1 -0
  37. package/lib-yak/formatUtils.mjs +64 -0
  38. package/lib-yak/formatUtils.mjs.map +1 -0
  39. package/lib-yak/getClosestOffsetScroll.mjs +15 -0
  40. package/lib-yak/getClosestOffsetScroll.mjs.map +1 -0
  41. package/lib-yak/hooks/useEventListener.mjs +27 -0
  42. package/lib-yak/hooks/useEventListener.mjs.map +1 -0
  43. package/lib-yak/index.mjs +21 -0
  44. package/lib-yak/index.mjs.map +1 -0
  45. package/lib-yak/insertKeyframes.mjs +68 -0
  46. package/lib-yak/insertKeyframes.mjs.map +1 -0
  47. package/lib-yak/lodashUtils.mjs +131 -0
  48. package/lib-yak/lodashUtils.mjs.map +1 -0
  49. package/lib-yak/promiseUtils.mjs +92 -0
  50. package/lib-yak/promiseUtils.mjs.map +1 -0
  51. package/lib-yak/reactUtils.mjs +12 -0
  52. package/lib-yak/reactUtils.mjs.map +1 -0
  53. package/lib-yak/reduceUtils.mjs +14 -0
  54. package/lib-yak/reduceUtils.mjs.map +1 -0
  55. package/lib-yak/smoothScroll.mjs +59 -0
  56. package/lib-yak/smoothScroll.mjs.map +1 -0
  57. package/lib-yak/styled.mjs +23 -0
  58. package/lib-yak/styled.mjs.map +1 -0
  59. package/lib-yak/styledShared.mjs +129 -0
  60. package/lib-yak/styledShared.mjs.map +1 -0
  61. package/lib-yak/timeUtils.mjs +81 -0
  62. package/lib-yak/timeUtils.mjs.map +1 -0
  63. package/lib-yak/timerUtils.mjs +10 -0
  64. package/lib-yak/timerUtils.mjs.map +1 -0
  65. package/lib-yak/utils.mjs +386 -0
  66. package/lib-yak/utils.mjs.map +1 -0
  67. package/lib-yak/wrapLazy.mjs +132 -0
  68. package/lib-yak/wrapLazy.mjs.map +1 -0
  69. package/lib-yak/yakDomTag.mjs +44 -0
  70. package/lib-yak/yakDomTag.mjs.map +1 -0
  71. package/package.json +14 -3
  72. package/src/animationKeyframes.js +10 -0
  73. package/src/animationKeyframes.test.mjs +75 -0
  74. package/src/animationKeyframes.yak.js +30 -0
  75. package/src/fixMediaQuery.js +40 -19
  76. package/src/fixMediaText.js +18 -0
  77. package/src/fixMediaText.test.mjs +60 -0
  78. package/src/formatDate.js +1 -1
  79. package/src/index.js +1 -0
  80. package/src/insertKeyframes.js +73 -0
  81. package/src/insertKeyframes.test.mjs +144 -0
  82. package/src/styled.js +5 -97
  83. package/src/styled.yak.js +30 -0
  84. package/src/styledShared.js +96 -0
  85. package/src/timerUtils.jsx +0 -2
  86. package/src/yakDomTag.jsx +42 -0
  87. package/src/yakDomTag.test.mjs +49 -0
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import isPropValid from '@emotion/is-prop-valid';
3
+
4
+ // Task 13c: @yak/react só filtra props `$`-prefixadas e `undefined` antes de repassar pro
5
+ // elemento real; qualquer prop custom (string, número, objeto) lida por um `styled.<tag>` no yak
6
+ // vaza como atributo no DOM. styled-components v5 evita isso sozinho pra tags string, usando
7
+ // @emotion/is-prop-valid. yakDomTag(tag) devolve um componente equivalente: filtra pelas mesmas
8
+ // regras antes de renderizar `tag`, pra o DOM do yak ficar idêntico ao do sc.
9
+ //
10
+ // Resolvido via `@startlibs/utils/yakDomTag` (exports["./*"] do package.json) — não passa pelo
11
+ // index: só existe nessa saída porque o babel-plugin-startlibs-to-yak importa explicitamente
12
+ // (ver tools/babel-plugin-startlibs-to-yak/domTag.cjs), nunca no fonte sc.
13
+ //
14
+ // Memoizado por nome de tag: `styled(yakDomTag('div'))` precisa devolver sempre o MESMO
15
+ // componente de DOM final pra identidade ficar estável entre `${X}` e composições
16
+ // (`styled(X)`/`styledExtend(X)`) de um mesmo `styled.div` — ver @yak/react runtime, que carrega
17
+ // esse componente até o fim da cadeia de composição.
18
+ //
19
+ // `as` (styled-components troca a tag renderizada com essa prop, ex.: components/src/containers/
20
+ // usePositioning.jsx's Placeholder as={placeholderAs}, form/src/TextInput.jsx's Input
21
+ // as={inputTag}) não é filtrada pelo @emotion/is-prop-valid (`isPropValid('as')` é false) — sem
22
+ // tratar à parte, o yak renderizaria sempre a tag original, ignorando a troca.
23
+ const cache = new Map();
24
+ const yakDomTag = tag => {
25
+ let Component = cache.get(tag);
26
+ if (!Component) {
27
+ Component = /*#__PURE__*/React.forwardRef((props, ref) => {
28
+ const domProps = {};
29
+ for (const key in props) {
30
+ // `as` troca a tag renderizada (como styled-components faz); não é uma prop do DOM.
31
+ if (key === 'as') continue;
32
+ if (key === 'className' || isPropValid(key)) domProps[key] = props[key];
33
+ }
34
+ domProps.ref = ref;
35
+ return /*#__PURE__*/React.createElement(props.as || tag, domProps);
36
+ });
37
+ Component.displayName = `yakDomTag(${tag})`;
38
+ cache.set(tag, Component);
39
+ }
40
+ return Component;
41
+ };
42
+
43
+ export { yakDomTag };
44
+ //# sourceMappingURL=yakDomTag.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yakDomTag.mjs","sources":["../src/yakDomTag.jsx"],"sourcesContent":["import React from 'react'\nimport isPropValid from '@emotion/is-prop-valid'\n\n// Task 13c: @yak/react só filtra props `$`-prefixadas e `undefined` antes de repassar pro\n// elemento real; qualquer prop custom (string, número, objeto) lida por um `styled.<tag>` no yak\n// vaza como atributo no DOM. styled-components v5 evita isso sozinho pra tags string, usando\n// @emotion/is-prop-valid. yakDomTag(tag) devolve um componente equivalente: filtra pelas mesmas\n// regras antes de renderizar `tag`, pra o DOM do yak ficar idêntico ao do sc.\n//\n// Resolvido via `@startlibs/utils/yakDomTag` (exports[\"./*\"] do package.json) — não passa pelo\n// index: só existe nessa saída porque o babel-plugin-startlibs-to-yak importa explicitamente\n// (ver tools/babel-plugin-startlibs-to-yak/domTag.cjs), nunca no fonte sc.\n//\n// Memoizado por nome de tag: `styled(yakDomTag('div'))` precisa devolver sempre o MESMO\n// componente de DOM final pra identidade ficar estável entre `${X}` e composições\n// (`styled(X)`/`styledExtend(X)`) de um mesmo `styled.div` — ver @yak/react runtime, que carrega\n// esse componente até o fim da cadeia de composição.\n//\n// `as` (styled-components troca a tag renderizada com essa prop, ex.: components/src/containers/\n// usePositioning.jsx's Placeholder as={placeholderAs}, form/src/TextInput.jsx's Input\n// as={inputTag}) não é filtrada pelo @emotion/is-prop-valid (`isPropValid('as')` é false) — sem\n// tratar à parte, o yak renderizaria sempre a tag original, ignorando a troca.\nconst cache = new Map()\n\nexport const yakDomTag = (tag) => {\n let Component = cache.get(tag)\n if (!Component) {\n Component = React.forwardRef((props, ref) => {\n const domProps = {}\n for (const key in props) {\n // `as` troca a tag renderizada (como styled-components faz); não é uma prop do DOM.\n if (key === 'as') continue\n if (key === 'className' || isPropValid(key)) domProps[key] = props[key]\n }\n domProps.ref = ref\n return React.createElement(props.as || tag, domProps)\n })\n Component.displayName = `yakDomTag(${tag})`\n cache.set(tag, Component)\n }\n return Component\n}\n"],"names":["cache","Map","yakDomTag","tag","Component","get","React","forwardRef","props","ref","domProps","key","isPropValid","createElement","as","displayName","set"],"mappings":";;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMA,KAAK,GAAG,IAAIC,GAAG,EAAE,CAAA;AAEVC,MAAAA,SAAS,GAAIC,GAAG,IAAK;AAChC,EAAA,IAAIC,SAAS,GAAGJ,KAAK,CAACK,GAAG,CAACF,GAAG,CAAC,CAAA;EAC9B,IAAI,CAACC,SAAS,EAAE;IACdA,SAAS,gBAAGE,KAAK,CAACC,UAAU,CAAC,CAACC,KAAK,EAAEC,GAAG,KAAK;MAC3C,MAAMC,QAAQ,GAAG,EAAE,CAAA;AACnB,MAAA,KAAK,MAAMC,GAAG,IAAIH,KAAK,EAAE;AACvB;QACA,IAAIG,GAAG,KAAK,IAAI,EAAE,SAAA;AAClB,QAAA,IAAIA,GAAG,KAAK,WAAW,IAAIC,WAAW,CAACD,GAAG,CAAC,EAAED,QAAQ,CAACC,GAAG,CAAC,GAAGH,KAAK,CAACG,GAAG,CAAC,CAAA;AACzE,OAAA;MACAD,QAAQ,CAACD,GAAG,GAAGA,GAAG,CAAA;MAClB,oBAAOH,KAAK,CAACO,aAAa,CAACL,KAAK,CAACM,EAAE,IAAIX,GAAG,EAAEO,QAAQ,CAAC,CAAA;AACvD,KAAC,CAAC,CAAA;AACFN,IAAAA,SAAS,CAACW,WAAW,GAAG,CAAA,UAAA,EAAaZ,GAAG,CAAG,CAAA,CAAA,CAAA;AAC3CH,IAAAA,KAAK,CAACgB,GAAG,CAACb,GAAG,EAAEC,SAAS,CAAC,CAAA;AAC3B,GAAA;AACA,EAAA,OAAOA,SAAS,CAAA;AAClB;;;;"}
package/package.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "@startlibs/utils",
3
- "version": "1.1.1",
3
+ "version": "1.2.0-rc.1",
4
4
  "description": "Startlibs Utils",
5
5
  "main": "lib/index.cjs",
6
6
  "exports": {
7
7
  ".": {
8
8
  "source": "./src/index.js",
9
+ "yak": "./lib-yak/index.mjs",
9
10
  "require": "./lib/index.cjs",
10
11
  "import": "./lib/index.mjs",
11
12
  "default": "./lib/index.mjs"
12
13
  },
13
14
  "./package.json": "./package.json",
14
15
  "./*": {
16
+ "yak": "./lib-yak/*.mjs",
15
17
  "import": "./lib/*.mjs"
16
18
  }
17
19
  },
@@ -19,11 +21,17 @@
19
21
  "source": "./src/index.js",
20
22
  "peerDependencies": {
21
23
  "@startlibs/core": "*",
24
+ "@yak/react": "10.0.0",
22
25
  "lodash": "^4.17.21",
23
26
  "polished": "^2.0.0 || ^3.0.0 || ^4.0.0",
24
27
  "react": ">=16.8",
25
28
  "styled-components": ">=5"
26
29
  },
30
+ "peerDependenciesMeta": {
31
+ "@yak/react": {
32
+ "optional": true
33
+ }
34
+ },
27
35
  "devDependencies": {
28
36
  "@babel/core": "^7.26.0",
29
37
  "@babel/plugin-transform-runtime": "^7.26.0",
@@ -35,6 +43,7 @@
35
43
  "babel-plugin-styled-components": "^1.10.0",
36
44
  "lodash": "^4.17.21",
37
45
  "polished": "^2.0.0 || ^3.0.0 || ^4.0.0",
46
+ "react": "^18.3.1",
38
47
  "rollup": "^2.79.2",
39
48
  "rollup-plugin-auto-external": "^2.0.0",
40
49
  "rollup-plugin-babel": "^4.0.3",
@@ -43,8 +52,8 @@
43
52
  "styled-components": "^5.3.11"
44
53
  },
45
54
  "scripts": {
46
- "test": "echo \"Error: no test specified\" && exit 1",
47
- "build": "rollup -c"
55
+ "test": "node --import ./test/register.mjs --test src/**/*.test.mjs",
56
+ "build": "rollup -c && rollup -c --environment YAK"
48
57
  },
49
58
  "repository": {
50
59
  "type": "git",
@@ -62,10 +71,12 @@
62
71
  "homepage": "https://github.com/felipeko/startlibs#readme",
63
72
  "dependencies": {
64
73
  "@babel/runtime": "^7.20.0",
74
+ "@emotion/is-prop-valid": "^1.1.0",
65
75
  "create-emotion": "10.0.27"
66
76
  },
67
77
  "files": [
68
78
  "lib",
79
+ "lib-yak",
69
80
  "src"
70
81
  ],
71
82
  "module": "lib/index.mjs",
@@ -0,0 +1,10 @@
1
+ import createEmotion from 'create-emotion'
2
+
3
+ // Saída sc (lib/): o keyframes do emotion, como sempre foi. A saída yak (lib-yak/) usa
4
+ // animationKeyframes.yak.js no lugar deste arquivo (ver tools/rollup.base.mjs).
5
+ const emotion = createEmotion({
6
+ nonce: (typeof document !== 'undefined' &&
7
+ (document.querySelector('meta[property="csp-nonce"]') || {}).content) || 'animation_nonce'
8
+ })
9
+
10
+ export const animationKeyframes = emotion.keyframes
@@ -0,0 +1,75 @@
1
+ import {test, before, after} from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ // oneWayAnimation/twoWayAnimation na saída yak. `styled.js?yak` carrega o grafo como a
5
+ // lib-yak (animationKeyframes.yak.js no lugar de animationKeyframes.js, ver test/jsxLoader.mjs).
6
+ // A saída sc (emotion) não roda aqui: o default do create-emotion é CJS; ela fica igual à de
7
+ // antes, o que tools/compare-lib.sh e as goldens do compat conferem.
8
+ // fixMediaQuery lê `window` no import; sem document, ele não mede nada.
9
+ const adopted = []
10
+ before(() => {
11
+ globalThis.window = {}
12
+ globalThis.CSSStyleSheet = class {
13
+ constructor() {
14
+ this.cssRules = []
15
+ adopted.push(this)
16
+ }
17
+ insertRule(rule, index) {
18
+ this.cssRules.splice(index, 0, rule)
19
+ }
20
+ replaceSync() {}
21
+ }
22
+ })
23
+ after(() => {
24
+ delete globalThis.window
25
+ delete globalThis.document
26
+ delete globalThis.CSSStyleSheet
27
+ })
28
+
29
+ const withDocument = () => {
30
+ globalThis.document = {
31
+ adoptedStyleSheets: [],
32
+ styleSheets: [],
33
+ querySelector: () => null
34
+ }
35
+ }
36
+
37
+ const rules = () => adopted.flatMap((sheet) => sheet.cssRules)
38
+ const squash = (text) => text.replace(/\s+/g, ' ').trim()
39
+
40
+ test('yak: mesmos offsets de antes (0..100%, frame vazio fica de fora), registrado pelo CSSOM', async () => {
41
+ const {oneWayAnimation} = await import('./styled.js?yak')
42
+ withDocument()
43
+ const name = oneWayAnimation('opacity: 0;', '', 'opacity: 1;')
44
+ assert.match(name, /^startlibs-animation-[0-9a-z]+$/)
45
+ const rule = rules().find((r) => r.startsWith(`@keyframes ${name} `))
46
+ assert.equal(squash(rule), `@keyframes ${name} { 0% { opacity: 0; }100% { opacity: 1; }}`)
47
+
48
+ const three = oneWayAnimation('a: 1;', 'a: 2;', 'a: 3;', 'a: 4;')
49
+ assert.equal(squash(rules().find((r) => r.startsWith(`@keyframes ${three} `))),
50
+ `@keyframes ${three} { 0% { a: 1; }33% { a: 2; }67% { a: 3; }100% { a: 4; }}`)
51
+ })
52
+
53
+ test('yak: mesmos frames, mesmo nome e uma regra só; frames diferentes, nomes diferentes', async () => {
54
+ const {oneWayAnimation, twoWayAnimation} = await import('./styled.js?yak')
55
+ withDocument()
56
+ const before = rules().length
57
+ const a = oneWayAnimation('opacity: 0.25;', 'opacity: 0.5;')
58
+ assert.equal(oneWayAnimation('opacity: 0.25;', 'opacity: 0.5;'), a)
59
+ assert.equal(rules().length, before + 1)
60
+
61
+ const [enter, leave] = twoWayAnimation('opacity: 0.25;', 'opacity: 0.75;')
62
+ assert.notEqual(enter, leave)
63
+ assert.notEqual(enter, a)
64
+ assert.equal(squash(rules().find((r) => r.startsWith(`@keyframes ${leave} `))),
65
+ `@keyframes ${leave} { 0% { opacity: 0.75; }100% { opacity: 0.25; }}`)
66
+ })
67
+
68
+ test('yak: no import (sem document) devolve um nome e registra quando o frame é pedido de novo', async () => {
69
+ delete globalThis.document
70
+ const {identityOpacityFrames, oneWayAnimation} = await import('./styled.js?yak')
71
+ assert.match(identityOpacityFrames, /^startlibs-animation-/)
72
+ withDocument()
73
+ assert.equal(oneWayAnimation('opacity: 1;', 'opacity:1;'), identityOpacityFrames)
74
+ assert.ok(rules().some((r) => r.startsWith(`@keyframes ${identityOpacityFrames} `)))
75
+ })
@@ -0,0 +1,30 @@
1
+ import {insertKeyframes} from './insertKeyframes'
2
+
3
+ // Saída yak (lib-yak/): mesmo contrato do keyframes do emotion (tag de template que devolve o
4
+ // nome da animação), mas registrado pelo CSSOM, sem <style> e sem nonce.
5
+ const interpolate = (value) =>
6
+ Array.isArray(value) ? value.map(interpolate).join('')
7
+ : value === null || value === undefined || typeof value === 'boolean' ? ''
8
+ : String(value)
9
+
10
+ // djb2: só precisa ser estável e curto; colisão é tratada abaixo.
11
+ const hash = (text) => {
12
+ let h = 5381
13
+ for (let i = 0; i < text.length; i++) h = ((h * 33) ^ text.charCodeAt(i)) >>> 0
14
+ return h.toString(36)
15
+ }
16
+
17
+ const nameByFrames = new Map()
18
+ const usedNames = new Set()
19
+
20
+ export const animationKeyframes = (strings, ...values) => {
21
+ const frames = strings.reduce((text, part, i) => text + part + (i < values.length ? interpolate(values[i]) : ''), '')
22
+ if (!nameByFrames.has(frames)) {
23
+ const base = 'startlibs-animation-' + hash(frames)
24
+ let name = base
25
+ for (let i = 2; usedNames.has(name); i++) name = base + '-' + i
26
+ usedNames.add(name)
27
+ nameByFrames.set(frames, name)
28
+ }
29
+ return insertKeyframes(nameByFrames.get(frames), frames)
30
+ }
@@ -1,4 +1,9 @@
1
+ import {fixMediaText} from './fixMediaText'
2
+
1
3
  let fixed = false
4
+ // Each rule may only get the scrollbar width added once, even if its sheet is seen again.
5
+ const patched = new WeakSet()
6
+
2
7
  export const scrollWidth = (function() {
3
8
  if (!window.document) return 0
4
9
  const html = document.documentElement
@@ -15,28 +20,44 @@ const fixMediaQueryFn = function() {
15
20
 
16
21
  if (scrollWidth > 0) {
17
22
 
18
- [].slice.call(document.styleSheets).filter(styleSheet => isValidExternal(styleSheet) || isInline(styleSheet)).forEach(stylesheet => {
19
- try {
20
- [].slice.call(stylesheet.cssRules || stylesheet.media).filter(rule => rule.constructor === window.CSSMediaRule).forEach(rule => {
21
- rule.media.mediaText = rule.media.mediaText.replace(/m(in|ax)-width:\s*(\d|\.)+(px|em)/gi, function(strA) {
22
- if (strA.match('px')) {
23
- return strA.replace(/\d+px/gi, function(strB) {
24
- return parseInt(strB, 10) + scrollWidth + 'px'
25
- })
26
- }
27
- else {
28
- return strA.replace(/\d.+?em/gi, function(strB) {
29
- return ((parseFloat(strB) * fontSize) + scrollWidth) / fontSize + 'em'
30
- })
31
- }
32
- })
33
- })
34
- } catch (e) {}
35
- })
23
+ [].slice.call(document.styleSheets).forEach(fixStylesheet)
24
+ watchLateStylesheets()
36
25
  }
37
26
 
38
27
  fixed = true
39
28
 
29
+ function fixStylesheet(stylesheet) {
30
+ if (!stylesheet || patched.has(stylesheet) || !(isValidExternal(stylesheet) || isInline(stylesheet))) {
31
+ return
32
+ }
33
+ try {
34
+ [].slice.call(stylesheet.cssRules).filter(rule => rule.constructor === window.CSSMediaRule).forEach(rule => {
35
+ rule.media.mediaText = fixMediaText(rule.media.mediaText, scrollWidth, fontSize)
36
+ })
37
+ patched.add(stylesheet)
38
+ } catch (e) {}
39
+ }
40
+
41
+ // Lazy routes add <link>s, and Vite dev and HMR add or rewrite <style>s, after the first pass.
42
+ function watchLateStylesheets() {
43
+ new MutationObserver(mutations => mutations.forEach(({target, addedNodes}) => {
44
+ const owner = target.nodeType === Node.TEXT_NODE ? target.parentNode : target
45
+ if (owner && owner.nodeName === 'STYLE') {
46
+ fixStylesheet(owner.sheet)
47
+ }
48
+ [].slice.call(addedNodes).forEach(node => {
49
+ if (node.nodeName === 'STYLE') {
50
+ fixStylesheet(node.sheet)
51
+ }
52
+ if (node.nodeName === 'LINK' && node.rel === 'stylesheet') {
53
+ // Not parsed yet when added; `load` also fires again if its href changes.
54
+ node.addEventListener('load', () => fixStylesheet(node.sheet))
55
+ fixStylesheet(node.sheet)
56
+ }
57
+ })
58
+ })).observe(document.head, {childList: true, subtree: true, characterData: true})
59
+ }
60
+
40
61
  function sameOrigin(url) {
41
62
  const loc = window.location,
42
63
  a = document.createElement('a')
@@ -68,4 +89,4 @@ export function fixMediaQuery() {
68
89
  } else {
69
90
  document.addEventListener('DOMContentLoaded', fixMediaQueryFn)
70
91
  }
71
- }
92
+ }
@@ -0,0 +1,18 @@
1
+ // Soma a largura da scrollbar aos limites de largura de um mediaText.
2
+ // Reconhece a forma antiga (`max-width: 600px`, `min-width: 37.5em`, que o sc emite) e a
3
+ // sintaxe de intervalo (`width <= 600px`, `width < 600px`, `600px <= width`,
4
+ // `400px < width < 600px`), que o Chromium devolve para o CSS estático do yak.
5
+ // Sem lookbehind de propósito: é erro de sintaxe no Safari < 16.4, e o browserslist do pacote
6
+ // ainda promete Safari 11. O caractere antes de `width` é capturado e devolvido igual, só pra
7
+ // não casar o `width` de `device-width`.
8
+ const LEGACY_OR_WIDTH_FIRST = /(m(?:in|ax)-width\s*:\s*|(?:^|[^-\w])width\s*[<>]=?\s*)([\d.]+)(px|em)/gi
9
+ const VALUE_FIRST = /([\d.]+)(px|em)(\s*[<>]=?\s*width)(?![-\w])/gi
10
+
11
+ export function fixMediaText(mediaText, scrollWidth, fontSize) {
12
+ const grow = (value, unit) => unit.toLowerCase() === 'px'
13
+ ? parseFloat(value) + scrollWidth + unit
14
+ : ((parseFloat(value) * fontSize) + scrollWidth) / fontSize + unit
15
+ return mediaText
16
+ .replace(LEGACY_OR_WIDTH_FIRST, (match, prefix, value, unit) => prefix + grow(value, unit))
17
+ .replace(VALUE_FIRST, (match, value, unit, suffix) => grow(value, unit) + suffix)
18
+ }
@@ -0,0 +1,60 @@
1
+ import {test} from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {readFileSync} from 'node:fs'
4
+ import {fixMediaText} from './fixMediaText.js'
5
+
6
+ const fix = (mediaText) => fixMediaText(mediaText, 15, 16)
7
+
8
+ test('forma antiga do sc: max-width/min-width em px', () => {
9
+ assert.equal(fix('(max-width: 600px)'), '(max-width: 615px)')
10
+ assert.equal(fix('screen and (min-width: 601px)'), 'screen and (min-width: 616px)')
11
+ assert.equal(fix('(max-width:600px)'), '(max-width:615px)')
12
+ })
13
+
14
+ test('forma antiga do sc: em é convertido via font-size', () => {
15
+ assert.equal(fix('(max-width: 37.5em)'), `(max-width: ${(37.5 * 16 + 15) / 16}em)`)
16
+ })
17
+
18
+ test('sintaxe de intervalo do yak: <= e >=', () => {
19
+ assert.equal(fix('(width <= 600px)'), '(width <= 615px)')
20
+ assert.equal(fix('(width>=601px)'), '(width>=616px)')
21
+ })
22
+
23
+ test('sintaxe de intervalo estrita: < e >', () => {
24
+ assert.equal(fix('(width < 600px)'), '(width < 615px)')
25
+ assert.equal(fix('(width>601px)'), '(width>616px)')
26
+ })
27
+
28
+ test('sintaxe de intervalo com o valor antes de width', () => {
29
+ assert.equal(fix('(600px <= width)'), '(615px <= width)')
30
+ assert.equal(fix('(600px<width)'), '(615px<width)')
31
+ assert.equal(fix('(400px < width < 600px)'), '(415px < width < 615px)')
32
+ })
33
+
34
+ test('mantém o caractere antes de width', () => {
35
+ assert.equal(fix('(width <= 600px)'), '(width <= 615px)')
36
+ assert.equal(fix('width <= 600px'), 'width <= 615px')
37
+ assert.equal(fix('not all and (width<600px)'), 'not all and (width<615px)')
38
+ })
39
+
40
+ test('não mexe em device-width nem em outros media features', () => {
41
+ assert.equal(fix('(device-width >= 600px)'), '(device-width >= 600px)')
42
+ assert.equal(fix('(600px <= device-width)'), '(600px <= device-width)')
43
+ assert.equal(fix('(max-device-width: 600px)'), '(max-device-width: 600px)')
44
+ assert.equal(fix('(height <= 600px)'), '(height <= 600px)')
45
+ assert.equal(fix('print'), 'print')
46
+ })
47
+
48
+ test('várias condições na mesma regra', () => {
49
+ assert.equal(
50
+ fix('(min-width: 400px) and (max-width: 600px), (width < 300px)'),
51
+ '(min-width: 415px) and (max-width: 615px), (width < 315px)'
52
+ )
53
+ })
54
+
55
+ test('o fonte não usa lookbehind (quebra Safari < 16.4)', () => {
56
+ for (const file of ['fixMediaText.js', 'fixMediaQuery.js']) {
57
+ const src = readFileSync(new URL(file, import.meta.url), 'utf8')
58
+ assert.doesNotMatch(src, /\(\?<[=!]/, file)
59
+ }
60
+ })
package/src/formatDate.js CHANGED
@@ -77,7 +77,7 @@ export function formatDateToPattern(format) {
77
77
  }
78
78
 
79
79
  export function formatDate(date, format, messages,noUTC) {
80
- if (!date)
80
+ if (!date && date !== 0)
81
81
  return ''
82
82
  const separators = format.replace(escapedValidParts, '\0').split('\0')
83
83
  const parts = format.match(escapedValidParts)
package/src/index.js CHANGED
@@ -4,6 +4,7 @@ export * from './smoothScroll'
4
4
  export * from './fixMediaQuery'
5
5
  export * from './FetchUtils'
6
6
  export * from './styled'
7
+ export * from './insertKeyframes'
7
8
  export * from './formatDate'
8
9
  export * from './lodashUtils'
9
10
  export * from './ActionUtils'
@@ -0,0 +1,73 @@
1
+ // Registra um @keyframes pelo CSSOM, sem criar <style> na página: funciona com CSP
2
+ // `style-src 'self'` sem nonce. Ordem de tentativa:
3
+ // 1. uma folha construída (new CSSStyleSheet) em document.adoptedStyleSheets;
4
+ // 2. browser sem folha construída: insertRule numa folha do próprio site já carregada
5
+ // (de preferência um <link>, que nenhum runtime de CSS-in-JS gerencia);
6
+ // 3. nenhuma das duas: <style> com o nonce do <meta property="csp-nonce">, se houver.
7
+ // Cada nome entra uma vez só; quem chama garante que nome igual é conteúdo igual.
8
+ const inserted = new Set()
9
+ let adoptedSheet = null
10
+ let pageSheet = null
11
+
12
+ const canAdoptSheets = () =>
13
+ typeof CSSStyleSheet !== 'undefined' &&
14
+ 'replaceSync' in CSSStyleSheet.prototype &&
15
+ 'adoptedStyleSheets' in document
16
+
17
+ // Outro código pode reatribuir document.adoptedStyleSheets e tirar a folha da lista: recoloca.
18
+ const getAdoptedSheet = () => {
19
+ if (!adoptedSheet) adoptedSheet = new CSSStyleSheet()
20
+ if (document.adoptedStyleSheets.indexOf(adoptedSheet) < 0) {
21
+ document.adoptedStyleSheets = document.adoptedStyleSheets.concat(adoptedSheet)
22
+ }
23
+ return adoptedSheet
24
+ }
25
+
26
+ // Folha que vale pra tela inteira e cujas regras dá pra ler (folha de outra origem lança erro).
27
+ const isWritable = (sheet) => {
28
+ if (sheet.disabled || !sheet.ownerNode || !sheet.ownerNode.isConnected) return false
29
+ if (sheet.media && sheet.media.length && sheet.media.mediaText !== 'all') return false
30
+ try {
31
+ return !!sheet.cssRules
32
+ } catch (e) {
33
+ return false
34
+ }
35
+ }
36
+
37
+ const getPageSheet = () => {
38
+ if (pageSheet && isWritable(pageSheet)) return pageSheet
39
+ const sheets = Array.prototype.slice.call(document.styleSheets).filter(isWritable)
40
+ pageSheet = sheets.filter((sheet) => sheet.ownerNode.tagName === 'LINK')[0] || sheets[0] || null
41
+ return pageSheet
42
+ }
43
+
44
+ const appendRule = (sheet, rule) => {
45
+ try {
46
+ sheet.insertRule(rule, sheet.cssRules.length)
47
+ } catch (e) {
48
+ // regra que o browser não entende: igual a um <style> com ela, só não faz nada
49
+ }
50
+ }
51
+
52
+ const appendStyleElement = (name, rule) => {
53
+ const style = document.createElement('style')
54
+ style.id = 'startlibs-keyframes-' + name
55
+ const nonce = (document.querySelector('meta[property="csp-nonce"]') || {}).content
56
+ if (nonce) style.nonce = nonce
57
+ style.textContent = rule
58
+ document.head.appendChild(style)
59
+ }
60
+
61
+ export const insertKeyframes = (name, frames) => {
62
+ if (typeof document === 'undefined' || inserted.has(name)) return name
63
+ inserted.add(name)
64
+ const rule = '@keyframes ' + name + ' {' + frames + '}'
65
+ if (canAdoptSheets()) {
66
+ appendRule(getAdoptedSheet(), rule)
67
+ } else if (getPageSheet()) {
68
+ appendRule(pageSheet, rule)
69
+ } else {
70
+ appendStyleElement(name, rule)
71
+ }
72
+ return name
73
+ }
@@ -0,0 +1,144 @@
1
+ import {test, afterEach} from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ // DOM falso, só o que insertKeyframes lê. Cada teste importa o módulo com uma query diferente
5
+ // pra começar sem estado (folha adotada, nomes já inseridos).
6
+ let instance = 0
7
+ const freshInsertKeyframes = async () => (await import(`./insertKeyframes.js?t${++instance}`)).insertKeyframes
8
+
9
+ const fakeSheet = ({tagName = 'LINK', media = '', readable = true} = {}) => {
10
+ const rules = []
11
+ return {
12
+ ownerNode: {tagName, isConnected: true},
13
+ media: {length: media ? 1 : 0, mediaText: media},
14
+ get cssRules() {
15
+ if (!readable) throw new Error('SecurityError')
16
+ return rules
17
+ },
18
+ rules,
19
+ insertRule(rule, index) {
20
+ rules.splice(index, 0, rule)
21
+ return index
22
+ }
23
+ }
24
+ }
25
+
26
+ const fakeDocument = ({styleSheets = [], adopted = false, nonce} = {}) => {
27
+ const appended = []
28
+ const doc = {
29
+ styleSheets,
30
+ appended,
31
+ head: {appendChild: (el) => appended.push(el)},
32
+ createElement: (tag) => ({tag}),
33
+ querySelector: (selector) => selector === 'meta[property="csp-nonce"]' && nonce ? {content: nonce} : null
34
+ }
35
+ if (adopted) doc.adoptedStyleSheets = []
36
+ return doc
37
+ }
38
+
39
+ class FakeConstructedSheet {
40
+ constructor() {
41
+ this.rules = []
42
+ this.cssRules = this.rules
43
+ }
44
+ insertRule(rule, index) {
45
+ this.rules.splice(index, 0, rule)
46
+ return index
47
+ }
48
+ replaceSync() {}
49
+ }
50
+
51
+ afterEach(() => {
52
+ delete globalThis.document
53
+ delete globalThis.CSSStyleSheet
54
+ })
55
+
56
+ test('sem document (SSR): devolve o nome e não faz nada', async () => {
57
+ const insertKeyframes = await freshInsertKeyframes()
58
+ assert.equal(insertKeyframes('spin', '100% { opacity: 0 }'), 'spin')
59
+ })
60
+
61
+ test('folha construída em adoptedStyleSheets, uma só, sem repetir o mesmo nome', async () => {
62
+ const insertKeyframes = await freshInsertKeyframes()
63
+ const link = fakeSheet()
64
+ globalThis.document = fakeDocument({adopted: true, styleSheets: [link]})
65
+ globalThis.CSSStyleSheet = FakeConstructedSheet
66
+
67
+ assert.equal(insertKeyframes('a', 'from { opacity: 0 } to { opacity: 1 }'), 'a')
68
+ insertKeyframes('a', 'from { opacity: 0 } to { opacity: 1 }')
69
+ insertKeyframes('b', '100% { opacity: 0 }')
70
+
71
+ assert.equal(document.adoptedStyleSheets.length, 1)
72
+ assert.deepEqual(document.adoptedStyleSheets[0].rules, [
73
+ '@keyframes a {from { opacity: 0 } to { opacity: 1 }}',
74
+ '@keyframes b {100% { opacity: 0 }}'
75
+ ])
76
+ assert.deepEqual(link.rules, [])
77
+ assert.deepEqual(document.appended, [])
78
+ })
79
+
80
+ test('folha adotada some da lista (outro código reatribuiu): volta pra lista', async () => {
81
+ const insertKeyframes = await freshInsertKeyframes()
82
+ globalThis.document = fakeDocument({adopted: true})
83
+ globalThis.CSSStyleSheet = FakeConstructedSheet
84
+ insertKeyframes('a', '100% { opacity: 0 }')
85
+ const sheet = document.adoptedStyleSheets[0]
86
+ const other = new FakeConstructedSheet()
87
+ document.adoptedStyleSheets = [other]
88
+
89
+ insertKeyframes('b', '100% { opacity: 1 }')
90
+
91
+ assert.deepEqual(document.adoptedStyleSheets, [other, sheet])
92
+ assert.equal(sheet.rules.length, 2)
93
+ })
94
+
95
+ test('sem folha construída: insertRule numa folha do site, preferindo <link>', async () => {
96
+ const insertKeyframes = await freshInsertKeyframes()
97
+ const crossOrigin = fakeSheet({readable: false})
98
+ const print = fakeSheet({media: 'print'})
99
+ const style = fakeSheet({tagName: 'STYLE'})
100
+ const link = fakeSheet()
101
+ globalThis.document = fakeDocument({styleSheets: [crossOrigin, print, style, link], nonce: 'n'})
102
+
103
+ insertKeyframes('a', '100% { opacity: 0 }')
104
+ insertKeyframes('a', '100% { opacity: 0 }')
105
+
106
+ assert.deepEqual(link.rules, ['@keyframes a {100% { opacity: 0 }}'])
107
+ assert.deepEqual(style.rules, [])
108
+ assert.deepEqual(print.rules, [])
109
+ assert.deepEqual(document.appended, [])
110
+ })
111
+
112
+ test('sem folha construída e sem <link>: usa um <style> do site que dê pra ler', async () => {
113
+ const insertKeyframes = await freshInsertKeyframes()
114
+ const style = fakeSheet({tagName: 'STYLE'})
115
+ globalThis.document = fakeDocument({styleSheets: [fakeSheet({readable: false}), style]})
116
+
117
+ insertKeyframes('a', '100% { opacity: 0 }')
118
+
119
+ assert.deepEqual(style.rules, ['@keyframes a {100% { opacity: 0 }}'])
120
+ })
121
+
122
+ test('último recurso: <style> com o nonce do <meta>, uma vez por nome', async () => {
123
+ const insertKeyframes = await freshInsertKeyframes()
124
+ globalThis.document = fakeDocument({styleSheets: [fakeSheet({readable: false})], nonce: 'abc'})
125
+
126
+ insertKeyframes('a', '100% { opacity: 0 }')
127
+ insertKeyframes('a', '100% { opacity: 0 }')
128
+
129
+ assert.deepEqual(document.appended, [{
130
+ tag: 'style',
131
+ id: 'startlibs-keyframes-a',
132
+ nonce: 'abc',
133
+ textContent: '@keyframes a {100% { opacity: 0 }}'
134
+ }])
135
+ })
136
+
137
+ test('último recurso sem <meta> de nonce: <style> sem nonce', async () => {
138
+ const insertKeyframes = await freshInsertKeyframes()
139
+ globalThis.document = fakeDocument()
140
+
141
+ insertKeyframes('a', '100% { opacity: 0 }')
142
+
143
+ assert.equal('nonce' in document.appended[0], false)
144
+ })