yummies 7.12.0 → 7.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +5 -87
  2. package/async.cjs +17 -0
  3. package/async.cjs.map +1 -1
  4. package/async.d.ts +17 -0
  5. package/async.js +17 -0
  6. package/async.js.map +1 -1
  7. package/common.cjs.map +1 -1
  8. package/common.d.ts +18 -0
  9. package/common.js.map +1 -1
  10. package/complex.cjs.map +1 -1
  11. package/complex.d.ts +66 -0
  12. package/complex.js.map +1 -1
  13. package/cookie.cjs.map +1 -1
  14. package/cookie.d.ts +18 -0
  15. package/cookie.js.map +1 -1
  16. package/css.cjs +16 -0
  17. package/css.cjs.map +1 -1
  18. package/css.d.ts +17 -0
  19. package/css.js +16 -0
  20. package/css.js.map +1 -1
  21. package/data.cjs.map +1 -1
  22. package/data.d.ts +18 -0
  23. package/data.js.map +1 -1
  24. package/date-time.cjs +16 -0
  25. package/date-time.cjs.map +1 -1
  26. package/date-time.d.ts +17 -0
  27. package/date-time.js +16 -0
  28. package/date-time.js.map +1 -1
  29. package/device.cjs +17 -0
  30. package/device.cjs.map +1 -1
  31. package/device.d.ts +17 -0
  32. package/device.js +17 -0
  33. package/device.js.map +1 -1
  34. package/encodings.cjs.map +1 -1
  35. package/encodings.d.ts +17 -0
  36. package/encodings.js.map +1 -1
  37. package/errors.cjs +16 -0
  38. package/errors.cjs.map +1 -1
  39. package/errors.d.ts +17 -0
  40. package/errors.js +16 -0
  41. package/errors.js.map +1 -1
  42. package/file.cjs +16 -0
  43. package/file.cjs.map +1 -1
  44. package/file.d.ts +16 -0
  45. package/file.js +16 -0
  46. package/file.js.map +1 -1
  47. package/format.cjs.map +1 -1
  48. package/format.d.ts +18 -0
  49. package/format.js.map +1 -1
  50. package/html.cjs +16 -0
  51. package/html.cjs.map +1 -1
  52. package/html.d.ts +17 -0
  53. package/html.js +16 -0
  54. package/html.js.map +1 -1
  55. package/id.cjs +16 -0
  56. package/id.cjs.map +1 -1
  57. package/id.d.ts +16 -0
  58. package/id.js +16 -0
  59. package/id.js.map +1 -1
  60. package/imports.cjs +16 -0
  61. package/imports.cjs.map +1 -1
  62. package/imports.d.ts +16 -0
  63. package/imports.js +16 -0
  64. package/imports.js.map +1 -1
  65. package/math.cjs.map +1 -1
  66. package/math.d.ts +17 -0
  67. package/math.js.map +1 -1
  68. package/media.cjs +16 -0
  69. package/media.cjs.map +1 -1
  70. package/media.d.ts +16 -0
  71. package/media.js +16 -0
  72. package/media.js.map +1 -1
  73. package/mobx.cjs +96 -0
  74. package/mobx.cjs.map +1 -1
  75. package/mobx.d.ts +101 -0
  76. package/mobx.js +96 -0
  77. package/mobx.js.map +1 -1
  78. package/ms.cjs +16 -0
  79. package/ms.cjs.map +1 -1
  80. package/ms.d.ts +16 -0
  81. package/ms.js +16 -0
  82. package/ms.js.map +1 -1
  83. package/number.cjs +16 -0
  84. package/number.cjs.map +1 -1
  85. package/number.d.ts +16 -0
  86. package/number.js +16 -0
  87. package/number.js.map +1 -1
  88. package/package.json +2 -2
  89. package/parser.cjs.map +1 -1
  90. package/parser.d.ts +17 -0
  91. package/parser.js.map +1 -1
  92. package/price.cjs.map +1 -1
  93. package/price.d.ts +16 -0
  94. package/price.js.map +1 -1
  95. package/random.cjs +16 -0
  96. package/random.cjs.map +1 -1
  97. package/random.d.ts +16 -0
  98. package/random.js +16 -0
  99. package/random.js.map +1 -1
  100. package/sound.cjs +16 -0
  101. package/sound.cjs.map +1 -1
  102. package/sound.d.ts +16 -0
  103. package/sound.js +16 -0
  104. package/sound.js.map +1 -1
  105. package/storage.cjs.map +1 -1
  106. package/storage.d.ts +16 -0
  107. package/storage.js.map +1 -1
  108. package/text.cjs +16 -0
  109. package/text.cjs.map +1 -1
  110. package/text.d.ts +16 -0
  111. package/text.js +16 -0
  112. package/text.js.map +1 -1
  113. package/type-guard.cjs.map +1 -1
  114. package/type-guard.d.ts +18 -0
  115. package/type-guard.js.map +1 -1
  116. package/types.d.ts +41 -0
  117. package/types.global.d.ts +41 -0
  118. package/vibrate.cjs +16 -0
  119. package/vibrate.cjs.map +1 -1
  120. package/vibrate.d.ts +16 -0
  121. package/vibrate.js +16 -0
  122. package/vibrate.js.map +1 -1
package/format.d.ts CHANGED
@@ -144,6 +144,24 @@ declare const percent: (value: Maybe<number | string>, settings?: PercentFormatS
144
144
  */
145
145
  declare const skipSpaces: (value: string) => string;
146
146
 
147
+ /**
148
+ * ---header-docs-section---
149
+ * # yummies/format
150
+ *
151
+ * ## Description
152
+ *
153
+ * Namespace-style **number, percent, and string** formatting for UI tables, inputs, and charts.
154
+ * Submodules cover locale-aware numbers, percent parsing/formatting, trimming helpers, and shared
155
+ * constants so presentation rules stay centralized and tree-shakable behind the package
156
+ * aggregated **format** export from this entry point.
157
+ *
158
+ * ## Usage
159
+ *
160
+ * ```ts
161
+ * import { format } from "yummies/format";
162
+ * ```
163
+ */
164
+
147
165
  declare const _exports_HYPHEN: typeof HYPHEN;
148
166
  declare const _exports_INFINITY: typeof INFINITY;
149
167
  declare const _exports_NO_VALUE: typeof NO_VALUE;
package/format.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"format.js","names":[],"sources":["../src/format/number.ts","../src/format/percent.ts","../src/format/skip-spaces.ts","../src/format/_exports.ts"],"sourcesContent":["import { parser } from 'yummies/parser';\nimport { typeGuard } from 'yummies/type-guard';\nimport type { Maybe } from 'yummies/types';\nimport { NO_VALUE } from './constants.js';\n\nexport interface NumberFormatSettings {\n delimiter?: string;\n /**\n * digitsOnlyForFloat - Show digits after decimal point only if they are not zeros after converting to number.\n * Example: \"0.00\" -> \"0\", \"0.10\" -> \"0.1\", but \"0.003\" -> \"0.003\"\n *\n * @default true\n */\n digitsOnlyForFloat?: boolean;\n /**\n * Text which will be returned if the value is undefined, null, NaN, Infinity or empty string.\n * Example: \"–\" will be returned if the value is undefined and emptyText is \"–\".\n */\n emptyText?: string;\n /**\n * Text to append to the end of the formatted number.\n * Example: if value is 1000 and postfix is \"₽\", result will be \"1 000₽\".\n */\n postfix?: string;\n /**\n * Fixed number of digits after the decimal point (number.toFixed() method)\n * If set to false, the truncation is ignored!\n */\n digits?: number | false;\n /**\n * Remove trailing zeros from the end of the number\n * Example: 0.010000000000000000000000000000000000000000000 -> 0.01\n */\n cutZeros?: boolean;\n cropDigitsOnly?: boolean;\n}\n\n/**\n * Formats a numeric value with thousands separators, fractional digit control\n * and optional postfix text.\n *\n * Invalid, empty or unsupported values fall back to `emptyText`.\n *\n * @param rawValue Number or numeric string to format.\n * @param userSettings Formatting overrides merged with `number.defaultSettings`.\n * @returns Formatted number string or fallback text.\n *\n * @example\n * ```ts\n * number(12000); // '12 000'\n * ```\n *\n * @example\n * ```ts\n * number(12.5, { digits: 1, postfix: '%' }); // '12.5%'\n * ```\n */\nexport const number = (\n rawValue: Maybe<string | number>,\n userSettings?: Maybe<NumberFormatSettings>,\n): string => {\n const settings = {\n ...number.defaultSettings,\n ...userSettings,\n };\n\n const digits = settings.digits ?? 0;\n const cutZeros = settings?.cutZeros ?? false;\n const delimiter = settings.delimiter ?? ' ';\n const postfix = settings.postfix ?? '';\n const emptyText = settings.emptyText ?? NO_VALUE;\n const digitsOnlyForFloat = settings.digitsOnlyForFloat ?? true;\n\n let value: Maybe<number>;\n\n if (typeGuard.isString(rawValue)) {\n value = parser.number(value, { fallback: undefined });\n } else {\n value = rawValue;\n }\n\n if (typeGuard.isNumber(value)) {\n let raw: string = `${value}`;\n\n if (digits !== false) {\n if (settings.cropDigitsOnly) {\n const [integerPart, decimalPart] = `${raw}`.split('.');\n const leftPart = integerPart;\n const rightPart = (decimalPart || '')\n .slice(0, digits)\n .padEnd(digits, '0');\n\n if (rightPart) {\n raw = `${leftPart}.${rightPart}`;\n } else {\n raw = leftPart;\n }\n } else {\n raw = value.toFixed(digits);\n }\n }\n\n if (cutZeros) {\n raw = `${+raw}`;\n }\n\n const [integerPart, decimalPart] = raw.split('.', 2);\n\n let formattedIntegerPart = integerPart;\n let formattedDecimalPart = '';\n\n if (decimalPart && (!digitsOnlyForFloat || !/^0+$/.test(decimalPart))) {\n formattedDecimalPart = `.${decimalPart}`;\n }\n\n const rgx = /(\\d+)(\\d{3})/;\n\n while (rgx.test(formattedIntegerPart) && delimiter) {\n formattedIntegerPart = formattedIntegerPart.replace(\n rgx,\n `$1${delimiter}$2`,\n );\n }\n\n return formattedIntegerPart + formattedDecimalPart + postfix;\n }\n\n return emptyText;\n};\n\nnumber.defaultSettings = {} as NumberFormatSettings;\n","import { parser } from 'yummies/parser';\nimport { typeGuard } from 'yummies/type-guard';\nimport type { Maybe } from 'yummies/types';\n\nimport { NO_VALUE } from './constants.js';\n\nexport interface PercentFormatSettings\n extends Omit<parser.NumberParserSettings, 'fallback'> {\n divider?: string;\n delimiter?: string;\n symbol?: string;\n emptyText?: string;\n}\n\n/**\n * Formats a value as a percent string with configurable decimal precision,\n * decimal divider and suffix symbol.\n *\n * @param value Number or numeric string to format.\n * @param settings Parser and formatting options.\n * @returns Formatted percent string or fallback text for invalid values.\n *\n * @example\n * ```ts\n * percent(12.345); // '12.35%'\n * ```\n *\n * @example\n * ```ts\n * percent(12.345, { divider: ',', symbol: ' pct' }); // '12,35 pct'\n * ```\n */\nexport const percent = (\n value: Maybe<number | string>,\n settings?: PercentFormatSettings,\n) => {\n const numericValue = parser.number(value, {\n ...settings,\n digits: settings?.digits ?? 2,\n fallback: Number.NaN,\n });\n\n if (typeGuard.isNumber(numericValue)) {\n const divider = settings?.divider ?? '.';\n\n const formattedPercent =\n divider === '.' ? numericValue : `${numericValue}`.replace('.', divider);\n\n return `${formattedPercent}${settings?.delimiter ?? ''}${settings?.symbol ?? '%'}`;\n } else {\n return settings?.emptyText ?? NO_VALUE;\n }\n};\n","/**\n * Removes all whitespace characters from a string.\n *\n * @param value Source string.\n * @returns String without spaces, tabs and line breaks.\n *\n * @example\n * ```ts\n * skipSpaces('1 000 000'); // '1000000'\n * ```\n *\n * @example\n * ```ts\n * skipSpaces('a\\tb\\nc'); // 'abc'\n * ```\n */\nexport const skipSpaces = (value: string) => value.replaceAll(/\\s/g, '');\n","export * from './constants.js';\nexport * from './number.js';\nexport * from './percent.js';\nexport * from './skip-spaces.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyDA,IAAa,UACX,UACA,iBACW;CACX,MAAM,WAAW;EACf,GAAG,OAAO;EACV,GAAG;EACJ;CAED,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,WAAW,UAAU,YAAY;CACvC,MAAM,YAAY,SAAS,aAAa;CACxC,MAAM,UAAU,SAAS,WAAW;CACpC,MAAM,YAAY,SAAS,aAAA;CAC3B,MAAM,qBAAqB,SAAS,sBAAsB;CAE1D,IAAI;AAEJ,KAAI,UAAU,SAAS,SAAS,CAC9B,SAAQ,OAAO,OAAO,OAAO,EAAE,UAAU,KAAA,GAAW,CAAC;KAErD,SAAQ;AAGV,KAAI,UAAU,SAAS,MAAM,EAAE;EAC7B,IAAI,MAAc,GAAG;AAErB,MAAI,WAAW,MACb,KAAI,SAAS,gBAAgB;GAC3B,MAAM,CAAC,aAAa,eAAe,GAAG,MAAM,MAAM,IAAI;GACtD,MAAM,WAAW;GACjB,MAAM,aAAa,eAAe,IAC/B,MAAM,GAAG,OAAO,CAChB,OAAO,QAAQ,IAAI;AAEtB,OAAI,UACF,OAAM,GAAG,SAAS,GAAG;OAErB,OAAM;QAGR,OAAM,MAAM,QAAQ,OAAO;AAI/B,MAAI,SACF,OAAM,GAAG,CAAC;EAGZ,MAAM,CAAC,aAAa,eAAe,IAAI,MAAM,KAAK,EAAE;EAEpD,IAAI,uBAAuB;EAC3B,IAAI,uBAAuB;AAE3B,MAAI,gBAAgB,CAAC,sBAAsB,CAAC,OAAO,KAAK,YAAY,EAClE,wBAAuB,IAAI;EAG7B,MAAM,MAAM;AAEZ,SAAO,IAAI,KAAK,qBAAqB,IAAI,UACvC,wBAAuB,qBAAqB,QAC1C,KACA,KAAK,UAAU,IAChB;AAGH,SAAO,uBAAuB,uBAAuB;;AAGvD,QAAO;;AAGT,OAAO,kBAAkB,EAAE;;;;;;;;;;;;;;;;;;;;;AClG3B,IAAa,WACX,OACA,aACG;CACH,MAAM,eAAe,OAAO,OAAO,OAAO;EACxC,GAAG;EACH,QAAQ,UAAU,UAAU;EAC5B,UAAU;EACX,CAAC;AAEF,KAAI,UAAU,SAAS,aAAa,EAAE;EACpC,MAAM,UAAU,UAAU,WAAW;AAKrC,SAAO,GAFL,YAAY,MAAM,eAAe,GAAG,eAAe,QAAQ,KAAK,QAAQ,GAE7C,UAAU,aAAa,KAAK,UAAU,UAAU;OAE7E,QAAO,UAAU,aAAA;;;;;;;;;;;;;;;;;;;;AClCrB,IAAa,cAAc,UAAkB,MAAM,WAAW,OAAO,GAAG"}
1
+ {"version":3,"file":"format.js","names":[],"sources":["../src/format/number.ts","../src/format/percent.ts","../src/format/skip-spaces.ts","../src/format/_exports.ts"],"sourcesContent":["import { parser } from 'yummies/parser';\nimport { typeGuard } from 'yummies/type-guard';\nimport type { Maybe } from 'yummies/types';\nimport { NO_VALUE } from './constants.js';\n\nexport interface NumberFormatSettings {\n delimiter?: string;\n /**\n * digitsOnlyForFloat - Show digits after decimal point only if they are not zeros after converting to number.\n * Example: \"0.00\" -> \"0\", \"0.10\" -> \"0.1\", but \"0.003\" -> \"0.003\"\n *\n * @default true\n */\n digitsOnlyForFloat?: boolean;\n /**\n * Text which will be returned if the value is undefined, null, NaN, Infinity or empty string.\n * Example: \"–\" will be returned if the value is undefined and emptyText is \"–\".\n */\n emptyText?: string;\n /**\n * Text to append to the end of the formatted number.\n * Example: if value is 1000 and postfix is \"₽\", result will be \"1 000₽\".\n */\n postfix?: string;\n /**\n * Fixed number of digits after the decimal point (number.toFixed() method)\n * If set to false, the truncation is ignored!\n */\n digits?: number | false;\n /**\n * Remove trailing zeros from the end of the number\n * Example: 0.010000000000000000000000000000000000000000000 -> 0.01\n */\n cutZeros?: boolean;\n cropDigitsOnly?: boolean;\n}\n\n/**\n * Formats a numeric value with thousands separators, fractional digit control\n * and optional postfix text.\n *\n * Invalid, empty or unsupported values fall back to `emptyText`.\n *\n * @param rawValue Number or numeric string to format.\n * @param userSettings Formatting overrides merged with `number.defaultSettings`.\n * @returns Formatted number string or fallback text.\n *\n * @example\n * ```ts\n * number(12000); // '12 000'\n * ```\n *\n * @example\n * ```ts\n * number(12.5, { digits: 1, postfix: '%' }); // '12.5%'\n * ```\n */\nexport const number = (\n rawValue: Maybe<string | number>,\n userSettings?: Maybe<NumberFormatSettings>,\n): string => {\n const settings = {\n ...number.defaultSettings,\n ...userSettings,\n };\n\n const digits = settings.digits ?? 0;\n const cutZeros = settings?.cutZeros ?? false;\n const delimiter = settings.delimiter ?? ' ';\n const postfix = settings.postfix ?? '';\n const emptyText = settings.emptyText ?? NO_VALUE;\n const digitsOnlyForFloat = settings.digitsOnlyForFloat ?? true;\n\n let value: Maybe<number>;\n\n if (typeGuard.isString(rawValue)) {\n value = parser.number(value, { fallback: undefined });\n } else {\n value = rawValue;\n }\n\n if (typeGuard.isNumber(value)) {\n let raw: string = `${value}`;\n\n if (digits !== false) {\n if (settings.cropDigitsOnly) {\n const [integerPart, decimalPart] = `${raw}`.split('.');\n const leftPart = integerPart;\n const rightPart = (decimalPart || '')\n .slice(0, digits)\n .padEnd(digits, '0');\n\n if (rightPart) {\n raw = `${leftPart}.${rightPart}`;\n } else {\n raw = leftPart;\n }\n } else {\n raw = value.toFixed(digits);\n }\n }\n\n if (cutZeros) {\n raw = `${+raw}`;\n }\n\n const [integerPart, decimalPart] = raw.split('.', 2);\n\n let formattedIntegerPart = integerPart;\n let formattedDecimalPart = '';\n\n if (decimalPart && (!digitsOnlyForFloat || !/^0+$/.test(decimalPart))) {\n formattedDecimalPart = `.${decimalPart}`;\n }\n\n const rgx = /(\\d+)(\\d{3})/;\n\n while (rgx.test(formattedIntegerPart) && delimiter) {\n formattedIntegerPart = formattedIntegerPart.replace(\n rgx,\n `$1${delimiter}$2`,\n );\n }\n\n return formattedIntegerPart + formattedDecimalPart + postfix;\n }\n\n return emptyText;\n};\n\nnumber.defaultSettings = {} as NumberFormatSettings;\n","import { parser } from 'yummies/parser';\nimport { typeGuard } from 'yummies/type-guard';\nimport type { Maybe } from 'yummies/types';\n\nimport { NO_VALUE } from './constants.js';\n\nexport interface PercentFormatSettings\n extends Omit<parser.NumberParserSettings, 'fallback'> {\n divider?: string;\n delimiter?: string;\n symbol?: string;\n emptyText?: string;\n}\n\n/**\n * Formats a value as a percent string with configurable decimal precision,\n * decimal divider and suffix symbol.\n *\n * @param value Number or numeric string to format.\n * @param settings Parser and formatting options.\n * @returns Formatted percent string or fallback text for invalid values.\n *\n * @example\n * ```ts\n * percent(12.345); // '12.35%'\n * ```\n *\n * @example\n * ```ts\n * percent(12.345, { divider: ',', symbol: ' pct' }); // '12,35 pct'\n * ```\n */\nexport const percent = (\n value: Maybe<number | string>,\n settings?: PercentFormatSettings,\n) => {\n const numericValue = parser.number(value, {\n ...settings,\n digits: settings?.digits ?? 2,\n fallback: Number.NaN,\n });\n\n if (typeGuard.isNumber(numericValue)) {\n const divider = settings?.divider ?? '.';\n\n const formattedPercent =\n divider === '.' ? numericValue : `${numericValue}`.replace('.', divider);\n\n return `${formattedPercent}${settings?.delimiter ?? ''}${settings?.symbol ?? '%'}`;\n } else {\n return settings?.emptyText ?? NO_VALUE;\n }\n};\n","/**\n * Removes all whitespace characters from a string.\n *\n * @param value Source string.\n * @returns String without spaces, tabs and line breaks.\n *\n * @example\n * ```ts\n * skipSpaces('1 000 000'); // '1000000'\n * ```\n *\n * @example\n * ```ts\n * skipSpaces('a\\tb\\nc'); // 'abc'\n * ```\n */\nexport const skipSpaces = (value: string) => value.replaceAll(/\\s/g, '');\n","/**\n * ---header-docs-section---\n * # yummies/format\n *\n * ## Description\n *\n * Namespace-style **number, percent, and string** formatting for UI tables, inputs, and charts.\n * Submodules cover locale-aware numbers, percent parsing/formatting, trimming helpers, and shared\n * constants so presentation rules stay centralized and tree-shakable behind the package\n * aggregated **format** export from this entry point.\n *\n * ## Usage\n *\n * ```ts\n * import { format } from \"yummies/format\";\n * ```\n */\n\nexport * from './constants.js';\nexport * from './number.js';\nexport * from './percent.js';\nexport * from './skip-spaces.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyDA,IAAa,UACX,UACA,iBACW;CACX,MAAM,WAAW;EACf,GAAG,OAAO;EACV,GAAG;EACJ;CAED,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,WAAW,UAAU,YAAY;CACvC,MAAM,YAAY,SAAS,aAAa;CACxC,MAAM,UAAU,SAAS,WAAW;CACpC,MAAM,YAAY,SAAS,aAAA;CAC3B,MAAM,qBAAqB,SAAS,sBAAsB;CAE1D,IAAI;AAEJ,KAAI,UAAU,SAAS,SAAS,CAC9B,SAAQ,OAAO,OAAO,OAAO,EAAE,UAAU,KAAA,GAAW,CAAC;KAErD,SAAQ;AAGV,KAAI,UAAU,SAAS,MAAM,EAAE;EAC7B,IAAI,MAAc,GAAG;AAErB,MAAI,WAAW,MACb,KAAI,SAAS,gBAAgB;GAC3B,MAAM,CAAC,aAAa,eAAe,GAAG,MAAM,MAAM,IAAI;GACtD,MAAM,WAAW;GACjB,MAAM,aAAa,eAAe,IAC/B,MAAM,GAAG,OAAO,CAChB,OAAO,QAAQ,IAAI;AAEtB,OAAI,UACF,OAAM,GAAG,SAAS,GAAG;OAErB,OAAM;QAGR,OAAM,MAAM,QAAQ,OAAO;AAI/B,MAAI,SACF,OAAM,GAAG,CAAC;EAGZ,MAAM,CAAC,aAAa,eAAe,IAAI,MAAM,KAAK,EAAE;EAEpD,IAAI,uBAAuB;EAC3B,IAAI,uBAAuB;AAE3B,MAAI,gBAAgB,CAAC,sBAAsB,CAAC,OAAO,KAAK,YAAY,EAClE,wBAAuB,IAAI;EAG7B,MAAM,MAAM;AAEZ,SAAO,IAAI,KAAK,qBAAqB,IAAI,UACvC,wBAAuB,qBAAqB,QAC1C,KACA,KAAK,UAAU,IAChB;AAGH,SAAO,uBAAuB,uBAAuB;;AAGvD,QAAO;;AAGT,OAAO,kBAAkB,EAAE;;;;;;;;;;;;;;;;;;;;;AClG3B,IAAa,WACX,OACA,aACG;CACH,MAAM,eAAe,OAAO,OAAO,OAAO;EACxC,GAAG;EACH,QAAQ,UAAU,UAAU;EAC5B,UAAU;EACX,CAAC;AAEF,KAAI,UAAU,SAAS,aAAa,EAAE;EACpC,MAAM,UAAU,UAAU,WAAW;AAKrC,SAAO,GAFL,YAAY,MAAM,eAAe,GAAG,eAAe,QAAQ,KAAK,QAAQ,GAE7C,UAAU,aAAa,KAAK,UAAU,UAAU;OAE7E,QAAO,UAAU,aAAA;;;;;;;;;;;;;;;;;;;;AClCrB,IAAa,cAAc,UAAkB,MAAM,WAAW,OAAO,GAAG"}
package/html.cjs CHANGED
@@ -5,6 +5,22 @@ dompurify = require_chunk.__toESM(dompurify);
5
5
  let yummies_media = require("yummies/media");
6
6
  //#region src/html.ts
7
7
  /**
8
+ * ---header-docs-section---
9
+ * # yummies/html
10
+ *
11
+ * ## Description
12
+ *
13
+ * DOM-centric utilities: sanitizing HTML with **DOMPurify**, computed style probes, downloads via
14
+ * temporary anchors, and small string helpers for safe markup. Prefer these over `innerHTML` with
15
+ * raw user input; keep CSP and server-side validation as the real security boundary.
16
+ *
17
+ * ## Usage
18
+ *
19
+ * ```ts
20
+ * import { getComputedColor } from "yummies/html";
21
+ * ```
22
+ */
23
+ /**
8
24
  * Extracts an RGB value from any valid CSS color.
9
25
  *
10
26
  * Not recommended for frequent use because it triggers a reflow.
package/html.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"html.cjs","names":[],"sources":["../src/html.ts"],"sourcesContent":["import DOMPurify, { type Config as DOMPurifyConfig } from 'dompurify';\nimport { blobToUrl } from 'yummies/media';\nimport type { Maybe } from 'yummies/types';\n\n/**\n * Extracts an RGB value from any valid CSS color.\n *\n * Not recommended for frequent use because it triggers a reflow.\n */\nexport const getComputedColor = (color?: string): string | null => {\n if (!color) return null;\n\n const d = document.createElement('div');\n d.style.color = color;\n document.body.append(d);\n const rgbcolor = globalThis.getComputedStyle(d).color;\n const match =\n /rgba?\\((\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(,\\s*\\d+[.d+]*)*\\)/g.exec(rgbcolor);\n\n d.remove();\n\n if (!match) return null;\n\n return `${match[1]}, ${match[2]}, ${match[3]}`;\n};\n\n/**\n * Triggers a file download by creating and clicking a temporary anchor element.\n *\n * @example\n * ```ts\n * downloadUsingAnchor('/report.pdf', 'report.pdf');\n * ```\n */\nexport const downloadUsingAnchor = (\n urlOrBlob: string | Blob,\n fileName?: string,\n) => {\n const url = blobToUrl(urlOrBlob);\n\n const a = document.createElement('a');\n a.href = url;\n\n a.download = fileName ?? 'file';\n\n a.target = '_blank';\n\n document.body.append(a);\n\n a.click();\n\n a.remove();\n};\n\n/**\n * Surrounds string in an anchor tag\n */\nexport function wrapTextToTagLink(link: string) {\n const descr = String(link).replace(/^(https?:\\/{0,2})?(w{3}\\.)?/, 'www.');\n if (!/^https?:\\/{2}/.test(link)) link = `http://${link}`;\n return `<a href=${link} target=\"_blank\">${descr}</a>`;\n}\n\n/**\n * Collects the cumulative `offsetTop` value through the element parent chain.\n *\n * @example\n * ```ts\n * const offsetTop = collectOffsetTop(document.getElementById('section'));\n * ```\n */\nexport const collectOffsetTop = (element: HTMLElement | null) => {\n let offsetTop = 0;\n let node = element;\n\n while (node != null) {\n offsetTop += node.offsetTop;\n node = node.parentElement;\n }\n\n return offsetTop;\n};\n\n/**\n * Prevents the default browser action and stops event propagation.\n *\n * @example\n * ```ts\n * button.addEventListener('click', (event) => skipEvent(event));\n * ```\n */\nexport const skipEvent = (e: Event) => {\n e.preventDefault();\n e.stopPropagation();\n\n return false;\n};\n\n/**\n * Scrolls the page vertically to the viewport section containing the target element.\n *\n * @example\n * ```ts\n * globalScrollIntoViewForY(document.getElementById('footer')!);\n * ```\n */\nexport const globalScrollIntoViewForY = (node: HTMLElement) => {\n const scrollContainer = document.body;\n const pageHeight = window.innerHeight;\n const nodeBounding = node.getBoundingClientRect();\n const scrollPagesCount = scrollContainer.scrollHeight / pageHeight;\n\n const scrollPageNumber = Math.min(\n Math.max(nodeBounding.top / pageHeight, 1),\n scrollPagesCount,\n );\n\n window.scroll({\n top: scrollPageNumber * pageHeight,\n behavior: 'smooth',\n });\n};\n\nconst sanitizeDefaults: DOMPurifyConfig = {\n ALLOWED_TAGS: [\n 'a',\n 'article',\n 'b',\n 'blockquote',\n 'br',\n 'caption',\n 'code',\n 'del',\n 'details',\n 'div',\n 'em',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'hr',\n 'i',\n 'img',\n 'ins',\n 'kbd',\n 'li',\n 'main',\n 'ol',\n 'p',\n 'pre',\n 'section',\n 'span',\n 'strong',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'th',\n 'thead',\n 'tr',\n 'u',\n 'ul',\n ],\n ALLOWED_ATTR: ['href', 'target', 'name', 'src', 'class'],\n};\n\n/**\n * Sanitizes HTML using the default allowlist merged with custom DOMPurify config.\n *\n * @example\n * ```ts\n * sanitizeHtml('<img src=x onerror=alert(1) />');\n * ```\n */\nexport const sanitizeHtml = (html: Maybe<string>, config?: DOMPurifyConfig) => {\n return DOMPurify.sanitize(html || '', {\n ...sanitizeDefaults,\n ...config,\n });\n};\n\n/**\n * Checks whether the element is nested inside the provided parent element.\n *\n * @example\n * ```ts\n * checkElementHasParent(childElement, modalElement);\n * ```\n */\nexport const checkElementHasParent = (\n element: HTMLElement | null,\n parent: Maybe<HTMLElement>,\n) => {\n let node = element;\n\n if (!parent) return false;\n\n while (node != null) {\n if (node === parent) {\n return true;\n } else {\n node = node.parentElement;\n }\n }\n\n return false;\n};\n\n/**\n * Executes a function within a view transition if supported by the browser.\n *\n * @param {VoidFunction} fn - The function to be executed.\n * @returns {ViewTransition} - The result of the executed function.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition | MDN: Document.startViewTransition}\n */\nexport const startViewTransitionSafety = (\n fn: VoidFunction,\n params?: { disabled?: boolean },\n) => {\n if (\n typeof document !== 'undefined' &&\n document.startViewTransition &&\n !params?.disabled\n ) {\n return document.startViewTransition(fn);\n }\n fn();\n};\n\n/**\n * Calculates the scrollbar width.\n */\nexport const calcScrollbarWidth = (elementToAppend = document.body) => {\n const outer = document.createElement('div');\n\n outer.style.visibility = 'hidden';\n outer.style.width = '100px';\n outer.style.overflow = 'scroll';\n\n elementToAppend.append(outer);\n\n const inner = document.createElement('div');\n inner.style.width = '100%';\n\n outer.append(inner);\n\n const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;\n\n outer.parentNode?.removeChild(outer);\n\n return scrollbarWidth;\n};\n\n/**\n * Calculates the inner height of an HTML element, accounting for padding.\n */\nexport function getElementInnerHeight(element: HTMLElement) {\n const { clientHeight } = element;\n const { paddingTop, paddingBottom } = getComputedStyle(element);\n return (\n clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n );\n}\n\n/**\n * Calculates the inner width of an HTML element, accounting for padding.\n */\nexport function getElementInnerWidth(el: HTMLElement) {\n const { clientWidth } = el;\n const { paddingLeft, paddingRight } = getComputedStyle(el);\n return (\n clientWidth -\n Number.parseFloat(paddingLeft) -\n Number.parseFloat(paddingRight)\n );\n}\n\n/**\n * Checks whether the user prefers a dark color scheme.\n *\n * @example\n * ```ts\n * const prefersDark = isPrefersDarkTheme();\n * ```\n */\nexport const isPrefersDarkTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: dark)')?.matches;\n\n/**\n * Checks whether the user prefers a light color scheme.\n *\n * @example\n * ```ts\n * const prefersLight = isPrefersLightTheme();\n * ```\n */\nexport const isPrefersLightTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: light)')?.matches;\n"],"mappings":"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"}
1
+ {"version":3,"file":"html.cjs","names":[],"sources":["../src/html.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/html\n *\n * ## Description\n *\n * DOM-centric utilities: sanitizing HTML with **DOMPurify**, computed style probes, downloads via\n * temporary anchors, and small string helpers for safe markup. Prefer these over `innerHTML` with\n * raw user input; keep CSP and server-side validation as the real security boundary.\n *\n * ## Usage\n *\n * ```ts\n * import { getComputedColor } from \"yummies/html\";\n * ```\n */\n\nimport DOMPurify, { type Config as DOMPurifyConfig } from 'dompurify';\nimport { blobToUrl } from 'yummies/media';\nimport type { Maybe } from 'yummies/types';\n\n/**\n * Extracts an RGB value from any valid CSS color.\n *\n * Not recommended for frequent use because it triggers a reflow.\n */\nexport const getComputedColor = (color?: string): string | null => {\n if (!color) return null;\n\n const d = document.createElement('div');\n d.style.color = color;\n document.body.append(d);\n const rgbcolor = globalThis.getComputedStyle(d).color;\n const match =\n /rgba?\\((\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(,\\s*\\d+[.d+]*)*\\)/g.exec(rgbcolor);\n\n d.remove();\n\n if (!match) return null;\n\n return `${match[1]}, ${match[2]}, ${match[3]}`;\n};\n\n/**\n * Triggers a file download by creating and clicking a temporary anchor element.\n *\n * @example\n * ```ts\n * downloadUsingAnchor('/report.pdf', 'report.pdf');\n * ```\n */\nexport const downloadUsingAnchor = (\n urlOrBlob: string | Blob,\n fileName?: string,\n) => {\n const url = blobToUrl(urlOrBlob);\n\n const a = document.createElement('a');\n a.href = url;\n\n a.download = fileName ?? 'file';\n\n a.target = '_blank';\n\n document.body.append(a);\n\n a.click();\n\n a.remove();\n};\n\n/**\n * Surrounds string in an anchor tag\n */\nexport function wrapTextToTagLink(link: string) {\n const descr = String(link).replace(/^(https?:\\/{0,2})?(w{3}\\.)?/, 'www.');\n if (!/^https?:\\/{2}/.test(link)) link = `http://${link}`;\n return `<a href=${link} target=\"_blank\">${descr}</a>`;\n}\n\n/**\n * Collects the cumulative `offsetTop` value through the element parent chain.\n *\n * @example\n * ```ts\n * const offsetTop = collectOffsetTop(document.getElementById('section'));\n * ```\n */\nexport const collectOffsetTop = (element: HTMLElement | null) => {\n let offsetTop = 0;\n let node = element;\n\n while (node != null) {\n offsetTop += node.offsetTop;\n node = node.parentElement;\n }\n\n return offsetTop;\n};\n\n/**\n * Prevents the default browser action and stops event propagation.\n *\n * @example\n * ```ts\n * button.addEventListener('click', (event) => skipEvent(event));\n * ```\n */\nexport const skipEvent = (e: Event) => {\n e.preventDefault();\n e.stopPropagation();\n\n return false;\n};\n\n/**\n * Scrolls the page vertically to the viewport section containing the target element.\n *\n * @example\n * ```ts\n * globalScrollIntoViewForY(document.getElementById('footer')!);\n * ```\n */\nexport const globalScrollIntoViewForY = (node: HTMLElement) => {\n const scrollContainer = document.body;\n const pageHeight = window.innerHeight;\n const nodeBounding = node.getBoundingClientRect();\n const scrollPagesCount = scrollContainer.scrollHeight / pageHeight;\n\n const scrollPageNumber = Math.min(\n Math.max(nodeBounding.top / pageHeight, 1),\n scrollPagesCount,\n );\n\n window.scroll({\n top: scrollPageNumber * pageHeight,\n behavior: 'smooth',\n });\n};\n\nconst sanitizeDefaults: DOMPurifyConfig = {\n ALLOWED_TAGS: [\n 'a',\n 'article',\n 'b',\n 'blockquote',\n 'br',\n 'caption',\n 'code',\n 'del',\n 'details',\n 'div',\n 'em',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'hr',\n 'i',\n 'img',\n 'ins',\n 'kbd',\n 'li',\n 'main',\n 'ol',\n 'p',\n 'pre',\n 'section',\n 'span',\n 'strong',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'th',\n 'thead',\n 'tr',\n 'u',\n 'ul',\n ],\n ALLOWED_ATTR: ['href', 'target', 'name', 'src', 'class'],\n};\n\n/**\n * Sanitizes HTML using the default allowlist merged with custom DOMPurify config.\n *\n * @example\n * ```ts\n * sanitizeHtml('<img src=x onerror=alert(1) />');\n * ```\n */\nexport const sanitizeHtml = (html: Maybe<string>, config?: DOMPurifyConfig) => {\n return DOMPurify.sanitize(html || '', {\n ...sanitizeDefaults,\n ...config,\n });\n};\n\n/**\n * Checks whether the element is nested inside the provided parent element.\n *\n * @example\n * ```ts\n * checkElementHasParent(childElement, modalElement);\n * ```\n */\nexport const checkElementHasParent = (\n element: HTMLElement | null,\n parent: Maybe<HTMLElement>,\n) => {\n let node = element;\n\n if (!parent) return false;\n\n while (node != null) {\n if (node === parent) {\n return true;\n } else {\n node = node.parentElement;\n }\n }\n\n return false;\n};\n\n/**\n * Executes a function within a view transition if supported by the browser.\n *\n * @param {VoidFunction} fn - The function to be executed.\n * @returns {ViewTransition} - The result of the executed function.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition | MDN: Document.startViewTransition}\n */\nexport const startViewTransitionSafety = (\n fn: VoidFunction,\n params?: { disabled?: boolean },\n) => {\n if (\n typeof document !== 'undefined' &&\n document.startViewTransition &&\n !params?.disabled\n ) {\n return document.startViewTransition(fn);\n }\n fn();\n};\n\n/**\n * Calculates the scrollbar width.\n */\nexport const calcScrollbarWidth = (elementToAppend = document.body) => {\n const outer = document.createElement('div');\n\n outer.style.visibility = 'hidden';\n outer.style.width = '100px';\n outer.style.overflow = 'scroll';\n\n elementToAppend.append(outer);\n\n const inner = document.createElement('div');\n inner.style.width = '100%';\n\n outer.append(inner);\n\n const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;\n\n outer.parentNode?.removeChild(outer);\n\n return scrollbarWidth;\n};\n\n/**\n * Calculates the inner height of an HTML element, accounting for padding.\n */\nexport function getElementInnerHeight(element: HTMLElement) {\n const { clientHeight } = element;\n const { paddingTop, paddingBottom } = getComputedStyle(element);\n return (\n clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n );\n}\n\n/**\n * Calculates the inner width of an HTML element, accounting for padding.\n */\nexport function getElementInnerWidth(el: HTMLElement) {\n const { clientWidth } = el;\n const { paddingLeft, paddingRight } = getComputedStyle(el);\n return (\n clientWidth -\n Number.parseFloat(paddingLeft) -\n Number.parseFloat(paddingRight)\n );\n}\n\n/**\n * Checks whether the user prefers a dark color scheme.\n *\n * @example\n * ```ts\n * const prefersDark = isPrefersDarkTheme();\n * ```\n */\nexport const isPrefersDarkTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: dark)')?.matches;\n\n/**\n * Checks whether the user prefers a light color scheme.\n *\n * @example\n * ```ts\n * const prefersLight = isPrefersLightTheme();\n * ```\n */\nexport const isPrefersLightTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: light)')?.matches;\n"],"mappings":"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"}
package/html.d.ts CHANGED
@@ -1,6 +1,23 @@
1
1
  import { Config } from 'dompurify';
2
2
  import { Maybe } from 'yummies/types';
3
3
 
4
+ /**
5
+ * ---header-docs-section---
6
+ * # yummies/html
7
+ *
8
+ * ## Description
9
+ *
10
+ * DOM-centric utilities: sanitizing HTML with **DOMPurify**, computed style probes, downloads via
11
+ * temporary anchors, and small string helpers for safe markup. Prefer these over `innerHTML` with
12
+ * raw user input; keep CSP and server-side validation as the real security boundary.
13
+ *
14
+ * ## Usage
15
+ *
16
+ * ```ts
17
+ * import { getComputedColor } from "yummies/html";
18
+ * ```
19
+ */
20
+
4
21
  /**
5
22
  * Extracts an RGB value from any valid CSS color.
6
23
  *
package/html.js CHANGED
@@ -2,6 +2,22 @@ import DOMPurify from "dompurify";
2
2
  import { blobToUrl } from "yummies/media";
3
3
  //#region src/html.ts
4
4
  /**
5
+ * ---header-docs-section---
6
+ * # yummies/html
7
+ *
8
+ * ## Description
9
+ *
10
+ * DOM-centric utilities: sanitizing HTML with **DOMPurify**, computed style probes, downloads via
11
+ * temporary anchors, and small string helpers for safe markup. Prefer these over `innerHTML` with
12
+ * raw user input; keep CSP and server-side validation as the real security boundary.
13
+ *
14
+ * ## Usage
15
+ *
16
+ * ```ts
17
+ * import { getComputedColor } from "yummies/html";
18
+ * ```
19
+ */
20
+ /**
5
21
  * Extracts an RGB value from any valid CSS color.
6
22
  *
7
23
  * Not recommended for frequent use because it triggers a reflow.
package/html.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"html.js","names":[],"sources":["../src/html.ts"],"sourcesContent":["import DOMPurify, { type Config as DOMPurifyConfig } from 'dompurify';\nimport { blobToUrl } from 'yummies/media';\nimport type { Maybe } from 'yummies/types';\n\n/**\n * Extracts an RGB value from any valid CSS color.\n *\n * Not recommended for frequent use because it triggers a reflow.\n */\nexport const getComputedColor = (color?: string): string | null => {\n if (!color) return null;\n\n const d = document.createElement('div');\n d.style.color = color;\n document.body.append(d);\n const rgbcolor = globalThis.getComputedStyle(d).color;\n const match =\n /rgba?\\((\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(,\\s*\\d+[.d+]*)*\\)/g.exec(rgbcolor);\n\n d.remove();\n\n if (!match) return null;\n\n return `${match[1]}, ${match[2]}, ${match[3]}`;\n};\n\n/**\n * Triggers a file download by creating and clicking a temporary anchor element.\n *\n * @example\n * ```ts\n * downloadUsingAnchor('/report.pdf', 'report.pdf');\n * ```\n */\nexport const downloadUsingAnchor = (\n urlOrBlob: string | Blob,\n fileName?: string,\n) => {\n const url = blobToUrl(urlOrBlob);\n\n const a = document.createElement('a');\n a.href = url;\n\n a.download = fileName ?? 'file';\n\n a.target = '_blank';\n\n document.body.append(a);\n\n a.click();\n\n a.remove();\n};\n\n/**\n * Surrounds string in an anchor tag\n */\nexport function wrapTextToTagLink(link: string) {\n const descr = String(link).replace(/^(https?:\\/{0,2})?(w{3}\\.)?/, 'www.');\n if (!/^https?:\\/{2}/.test(link)) link = `http://${link}`;\n return `<a href=${link} target=\"_blank\">${descr}</a>`;\n}\n\n/**\n * Collects the cumulative `offsetTop` value through the element parent chain.\n *\n * @example\n * ```ts\n * const offsetTop = collectOffsetTop(document.getElementById('section'));\n * ```\n */\nexport const collectOffsetTop = (element: HTMLElement | null) => {\n let offsetTop = 0;\n let node = element;\n\n while (node != null) {\n offsetTop += node.offsetTop;\n node = node.parentElement;\n }\n\n return offsetTop;\n};\n\n/**\n * Prevents the default browser action and stops event propagation.\n *\n * @example\n * ```ts\n * button.addEventListener('click', (event) => skipEvent(event));\n * ```\n */\nexport const skipEvent = (e: Event) => {\n e.preventDefault();\n e.stopPropagation();\n\n return false;\n};\n\n/**\n * Scrolls the page vertically to the viewport section containing the target element.\n *\n * @example\n * ```ts\n * globalScrollIntoViewForY(document.getElementById('footer')!);\n * ```\n */\nexport const globalScrollIntoViewForY = (node: HTMLElement) => {\n const scrollContainer = document.body;\n const pageHeight = window.innerHeight;\n const nodeBounding = node.getBoundingClientRect();\n const scrollPagesCount = scrollContainer.scrollHeight / pageHeight;\n\n const scrollPageNumber = Math.min(\n Math.max(nodeBounding.top / pageHeight, 1),\n scrollPagesCount,\n );\n\n window.scroll({\n top: scrollPageNumber * pageHeight,\n behavior: 'smooth',\n });\n};\n\nconst sanitizeDefaults: DOMPurifyConfig = {\n ALLOWED_TAGS: [\n 'a',\n 'article',\n 'b',\n 'blockquote',\n 'br',\n 'caption',\n 'code',\n 'del',\n 'details',\n 'div',\n 'em',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'hr',\n 'i',\n 'img',\n 'ins',\n 'kbd',\n 'li',\n 'main',\n 'ol',\n 'p',\n 'pre',\n 'section',\n 'span',\n 'strong',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'th',\n 'thead',\n 'tr',\n 'u',\n 'ul',\n ],\n ALLOWED_ATTR: ['href', 'target', 'name', 'src', 'class'],\n};\n\n/**\n * Sanitizes HTML using the default allowlist merged with custom DOMPurify config.\n *\n * @example\n * ```ts\n * sanitizeHtml('<img src=x onerror=alert(1) />');\n * ```\n */\nexport const sanitizeHtml = (html: Maybe<string>, config?: DOMPurifyConfig) => {\n return DOMPurify.sanitize(html || '', {\n ...sanitizeDefaults,\n ...config,\n });\n};\n\n/**\n * Checks whether the element is nested inside the provided parent element.\n *\n * @example\n * ```ts\n * checkElementHasParent(childElement, modalElement);\n * ```\n */\nexport const checkElementHasParent = (\n element: HTMLElement | null,\n parent: Maybe<HTMLElement>,\n) => {\n let node = element;\n\n if (!parent) return false;\n\n while (node != null) {\n if (node === parent) {\n return true;\n } else {\n node = node.parentElement;\n }\n }\n\n return false;\n};\n\n/**\n * Executes a function within a view transition if supported by the browser.\n *\n * @param {VoidFunction} fn - The function to be executed.\n * @returns {ViewTransition} - The result of the executed function.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition | MDN: Document.startViewTransition}\n */\nexport const startViewTransitionSafety = (\n fn: VoidFunction,\n params?: { disabled?: boolean },\n) => {\n if (\n typeof document !== 'undefined' &&\n document.startViewTransition &&\n !params?.disabled\n ) {\n return document.startViewTransition(fn);\n }\n fn();\n};\n\n/**\n * Calculates the scrollbar width.\n */\nexport const calcScrollbarWidth = (elementToAppend = document.body) => {\n const outer = document.createElement('div');\n\n outer.style.visibility = 'hidden';\n outer.style.width = '100px';\n outer.style.overflow = 'scroll';\n\n elementToAppend.append(outer);\n\n const inner = document.createElement('div');\n inner.style.width = '100%';\n\n outer.append(inner);\n\n const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;\n\n outer.parentNode?.removeChild(outer);\n\n return scrollbarWidth;\n};\n\n/**\n * Calculates the inner height of an HTML element, accounting for padding.\n */\nexport function getElementInnerHeight(element: HTMLElement) {\n const { clientHeight } = element;\n const { paddingTop, paddingBottom } = getComputedStyle(element);\n return (\n clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n );\n}\n\n/**\n * Calculates the inner width of an HTML element, accounting for padding.\n */\nexport function getElementInnerWidth(el: HTMLElement) {\n const { clientWidth } = el;\n const { paddingLeft, paddingRight } = getComputedStyle(el);\n return (\n clientWidth -\n Number.parseFloat(paddingLeft) -\n Number.parseFloat(paddingRight)\n );\n}\n\n/**\n * Checks whether the user prefers a dark color scheme.\n *\n * @example\n * ```ts\n * const prefersDark = isPrefersDarkTheme();\n * ```\n */\nexport const isPrefersDarkTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: dark)')?.matches;\n\n/**\n * Checks whether the user prefers a light color scheme.\n *\n * @example\n * ```ts\n * const prefersLight = isPrefersLightTheme();\n * ```\n */\nexport const isPrefersLightTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: light)')?.matches;\n"],"mappings":"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"}
1
+ {"version":3,"file":"html.js","names":[],"sources":["../src/html.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/html\n *\n * ## Description\n *\n * DOM-centric utilities: sanitizing HTML with **DOMPurify**, computed style probes, downloads via\n * temporary anchors, and small string helpers for safe markup. Prefer these over `innerHTML` with\n * raw user input; keep CSP and server-side validation as the real security boundary.\n *\n * ## Usage\n *\n * ```ts\n * import { getComputedColor } from \"yummies/html\";\n * ```\n */\n\nimport DOMPurify, { type Config as DOMPurifyConfig } from 'dompurify';\nimport { blobToUrl } from 'yummies/media';\nimport type { Maybe } from 'yummies/types';\n\n/**\n * Extracts an RGB value from any valid CSS color.\n *\n * Not recommended for frequent use because it triggers a reflow.\n */\nexport const getComputedColor = (color?: string): string | null => {\n if (!color) return null;\n\n const d = document.createElement('div');\n d.style.color = color;\n document.body.append(d);\n const rgbcolor = globalThis.getComputedStyle(d).color;\n const match =\n /rgba?\\((\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(,\\s*\\d+[.d+]*)*\\)/g.exec(rgbcolor);\n\n d.remove();\n\n if (!match) return null;\n\n return `${match[1]}, ${match[2]}, ${match[3]}`;\n};\n\n/**\n * Triggers a file download by creating and clicking a temporary anchor element.\n *\n * @example\n * ```ts\n * downloadUsingAnchor('/report.pdf', 'report.pdf');\n * ```\n */\nexport const downloadUsingAnchor = (\n urlOrBlob: string | Blob,\n fileName?: string,\n) => {\n const url = blobToUrl(urlOrBlob);\n\n const a = document.createElement('a');\n a.href = url;\n\n a.download = fileName ?? 'file';\n\n a.target = '_blank';\n\n document.body.append(a);\n\n a.click();\n\n a.remove();\n};\n\n/**\n * Surrounds string in an anchor tag\n */\nexport function wrapTextToTagLink(link: string) {\n const descr = String(link).replace(/^(https?:\\/{0,2})?(w{3}\\.)?/, 'www.');\n if (!/^https?:\\/{2}/.test(link)) link = `http://${link}`;\n return `<a href=${link} target=\"_blank\">${descr}</a>`;\n}\n\n/**\n * Collects the cumulative `offsetTop` value through the element parent chain.\n *\n * @example\n * ```ts\n * const offsetTop = collectOffsetTop(document.getElementById('section'));\n * ```\n */\nexport const collectOffsetTop = (element: HTMLElement | null) => {\n let offsetTop = 0;\n let node = element;\n\n while (node != null) {\n offsetTop += node.offsetTop;\n node = node.parentElement;\n }\n\n return offsetTop;\n};\n\n/**\n * Prevents the default browser action and stops event propagation.\n *\n * @example\n * ```ts\n * button.addEventListener('click', (event) => skipEvent(event));\n * ```\n */\nexport const skipEvent = (e: Event) => {\n e.preventDefault();\n e.stopPropagation();\n\n return false;\n};\n\n/**\n * Scrolls the page vertically to the viewport section containing the target element.\n *\n * @example\n * ```ts\n * globalScrollIntoViewForY(document.getElementById('footer')!);\n * ```\n */\nexport const globalScrollIntoViewForY = (node: HTMLElement) => {\n const scrollContainer = document.body;\n const pageHeight = window.innerHeight;\n const nodeBounding = node.getBoundingClientRect();\n const scrollPagesCount = scrollContainer.scrollHeight / pageHeight;\n\n const scrollPageNumber = Math.min(\n Math.max(nodeBounding.top / pageHeight, 1),\n scrollPagesCount,\n );\n\n window.scroll({\n top: scrollPageNumber * pageHeight,\n behavior: 'smooth',\n });\n};\n\nconst sanitizeDefaults: DOMPurifyConfig = {\n ALLOWED_TAGS: [\n 'a',\n 'article',\n 'b',\n 'blockquote',\n 'br',\n 'caption',\n 'code',\n 'del',\n 'details',\n 'div',\n 'em',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'hr',\n 'i',\n 'img',\n 'ins',\n 'kbd',\n 'li',\n 'main',\n 'ol',\n 'p',\n 'pre',\n 'section',\n 'span',\n 'strong',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'th',\n 'thead',\n 'tr',\n 'u',\n 'ul',\n ],\n ALLOWED_ATTR: ['href', 'target', 'name', 'src', 'class'],\n};\n\n/**\n * Sanitizes HTML using the default allowlist merged with custom DOMPurify config.\n *\n * @example\n * ```ts\n * sanitizeHtml('<img src=x onerror=alert(1) />');\n * ```\n */\nexport const sanitizeHtml = (html: Maybe<string>, config?: DOMPurifyConfig) => {\n return DOMPurify.sanitize(html || '', {\n ...sanitizeDefaults,\n ...config,\n });\n};\n\n/**\n * Checks whether the element is nested inside the provided parent element.\n *\n * @example\n * ```ts\n * checkElementHasParent(childElement, modalElement);\n * ```\n */\nexport const checkElementHasParent = (\n element: HTMLElement | null,\n parent: Maybe<HTMLElement>,\n) => {\n let node = element;\n\n if (!parent) return false;\n\n while (node != null) {\n if (node === parent) {\n return true;\n } else {\n node = node.parentElement;\n }\n }\n\n return false;\n};\n\n/**\n * Executes a function within a view transition if supported by the browser.\n *\n * @param {VoidFunction} fn - The function to be executed.\n * @returns {ViewTransition} - The result of the executed function.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition | MDN: Document.startViewTransition}\n */\nexport const startViewTransitionSafety = (\n fn: VoidFunction,\n params?: { disabled?: boolean },\n) => {\n if (\n typeof document !== 'undefined' &&\n document.startViewTransition &&\n !params?.disabled\n ) {\n return document.startViewTransition(fn);\n }\n fn();\n};\n\n/**\n * Calculates the scrollbar width.\n */\nexport const calcScrollbarWidth = (elementToAppend = document.body) => {\n const outer = document.createElement('div');\n\n outer.style.visibility = 'hidden';\n outer.style.width = '100px';\n outer.style.overflow = 'scroll';\n\n elementToAppend.append(outer);\n\n const inner = document.createElement('div');\n inner.style.width = '100%';\n\n outer.append(inner);\n\n const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;\n\n outer.parentNode?.removeChild(outer);\n\n return scrollbarWidth;\n};\n\n/**\n * Calculates the inner height of an HTML element, accounting for padding.\n */\nexport function getElementInnerHeight(element: HTMLElement) {\n const { clientHeight } = element;\n const { paddingTop, paddingBottom } = getComputedStyle(element);\n return (\n clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n );\n}\n\n/**\n * Calculates the inner width of an HTML element, accounting for padding.\n */\nexport function getElementInnerWidth(el: HTMLElement) {\n const { clientWidth } = el;\n const { paddingLeft, paddingRight } = getComputedStyle(el);\n return (\n clientWidth -\n Number.parseFloat(paddingLeft) -\n Number.parseFloat(paddingRight)\n );\n}\n\n/**\n * Checks whether the user prefers a dark color scheme.\n *\n * @example\n * ```ts\n * const prefersDark = isPrefersDarkTheme();\n * ```\n */\nexport const isPrefersDarkTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: dark)')?.matches;\n\n/**\n * Checks whether the user prefers a light color scheme.\n *\n * @example\n * ```ts\n * const prefersLight = isPrefersLightTheme();\n * ```\n */\nexport const isPrefersLightTheme = () =>\n !!globalThis.matchMedia?.('(prefers-color-scheme: light)')?.matches;\n"],"mappings":"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"}
package/id.cjs CHANGED
@@ -2,6 +2,22 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("./chunk-CVq3Gv4J.cjs");
3
3
  let nanoid = require("nanoid");
4
4
  //#region src/id.ts
5
+ /**
6
+ * ---header-docs-section---
7
+ * # yummies/id
8
+ *
9
+ * ## Description
10
+ *
11
+ * Fast, URL-friendly identifiers based on **nanoid** with curated alphabets and lengths. Use for
12
+ * client-generated keys, trace ids, or UI instance ids where UUID size is unnecessary. Collisions are
13
+ * unlikely at these lengths but still assume server-side uniqueness for persisted entities.
14
+ *
15
+ * ## Usage
16
+ *
17
+ * ```ts
18
+ * import { generateId } from "yummies/id";
19
+ * ```
20
+ */
5
21
  var DIGITS = "0123456789";
6
22
  var ALPHABET = `abcdefghijklmnopqrstuvwxyz${DIGITS}`;
7
23
  /**
package/id.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"id.cjs","names":[],"sources":["../src/id.ts"],"sourcesContent":["import { customAlphabet } from 'nanoid';\n\nconst DIGITS = '0123456789';\nconst LATIN_CHARS = 'abcdefghijklmnopqrstuvwxyz';\n\nconst ALPHABET = `${LATIN_CHARS}${DIGITS}`;\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 6.\n */\nexport const generateId = customAlphabet(ALPHABET, 6);\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 4.\n */\nexport const generateShortId = customAlphabet(ALPHABET, 4);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 6.\n */\nexport const generateNumericId = customAlphabet(DIGITS, 6);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 4.\n */\nexport const generateNumericShortId = customAlphabet(DIGITS, 4);\n\n/**\n * Creates a function that generates unique strings based on call order.\n *\n * @example\n * ```ts\n * generateLinearNumericId = createLinearNumericIdGenerator(6);\n * generateLinearNumericId() // '000000'\n * generateLinearNumericId() // '000001'\n * ...\n * generateLinearNumericId() // '999999'\n * generateLinearNumericId() // '1000000'\n * ...\n * generateLinearNumericId() // '9999999'\n * generateLinearNumericId() // '10000000'\n * ```\n *\n * @param size Minimum string length.\n * @returns {()=>string}\n */\nexport const createLinearNumericIdGenerator = (size: number = 9) => {\n let lastCount = 0;\n return () => {\n return (lastCount++).toString().padStart(size, '0');\n };\n};\n\n/**\n *\n * @example\n * ```ts\n * generateLinearNumericId() // '000000000'\n * generateLinearNumericId() // '000000001'\n * ...\n * generateLinearNumericId() // '999999999'\n * generateLinearNumericId() // '1000000000'\n * ...\n * generateLinearNumericId() // '9999999999'\n * generateLinearNumericId() // '10000000000'\n * ```\n *\n */\nexport const generateLinearNumericId = createLinearNumericIdGenerator();\n\n/**\n * Is not recommended to use.\n *\n * Generates execution stack based pseudo-id (just sliced string from error stack)\n */\nexport const generateStackBasedId = () =>\n new Error().stack!.split('\\n').slice(1, 4).join('');\n"],"mappings":";;;;AAEA,IAAM,SAAS;AAGf,IAAM,WAAW,6BAAiB;;;;;AAMlC,IAAa,cAAA,GAAA,OAAA,gBAA4B,UAAU,EAAE;;;;;AAMrD,IAAa,mBAAA,GAAA,OAAA,gBAAiC,UAAU,EAAE;;;;;AAM1D,IAAa,qBAAA,GAAA,OAAA,gBAAmC,QAAQ,EAAE;;;;;AAM1D,IAAa,0BAAA,GAAA,OAAA,gBAAwC,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;AAqB/D,IAAa,kCAAkC,OAAe,MAAM;CAClE,IAAI,YAAY;AAChB,cAAa;AACX,UAAQ,aAAa,UAAU,CAAC,SAAS,MAAM,IAAI;;;;;;;;;;;;;;;;;;AAmBvD,IAAa,0BAA0B,gCAAgC;;;;;;AAOvE,IAAa,8CACX,IAAI,OAAO,EAAC,MAAO,MAAM,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG"}
1
+ {"version":3,"file":"id.cjs","names":[],"sources":["../src/id.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/id\n *\n * ## Description\n *\n * Fast, URL-friendly identifiers based on **nanoid** with curated alphabets and lengths. Use for\n * client-generated keys, trace ids, or UI instance ids where UUID size is unnecessary. Collisions are\n * unlikely at these lengths but still assume server-side uniqueness for persisted entities.\n *\n * ## Usage\n *\n * ```ts\n * import { generateId } from \"yummies/id\";\n * ```\n */\n\nimport { customAlphabet } from 'nanoid';\n\nconst DIGITS = '0123456789';\nconst LATIN_CHARS = 'abcdefghijklmnopqrstuvwxyz';\n\nconst ALPHABET = `${LATIN_CHARS}${DIGITS}`;\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 6.\n */\nexport const generateId = customAlphabet(ALPHABET, 6);\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 4.\n */\nexport const generateShortId = customAlphabet(ALPHABET, 4);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 6.\n */\nexport const generateNumericId = customAlphabet(DIGITS, 6);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 4.\n */\nexport const generateNumericShortId = customAlphabet(DIGITS, 4);\n\n/**\n * Creates a function that generates unique strings based on call order.\n *\n * @example\n * ```ts\n * generateLinearNumericId = createLinearNumericIdGenerator(6);\n * generateLinearNumericId() // '000000'\n * generateLinearNumericId() // '000001'\n * ...\n * generateLinearNumericId() // '999999'\n * generateLinearNumericId() // '1000000'\n * ...\n * generateLinearNumericId() // '9999999'\n * generateLinearNumericId() // '10000000'\n * ```\n *\n * @param size Minimum string length.\n * @returns {()=>string}\n */\nexport const createLinearNumericIdGenerator = (size: number = 9) => {\n let lastCount = 0;\n return () => {\n return (lastCount++).toString().padStart(size, '0');\n };\n};\n\n/**\n *\n * @example\n * ```ts\n * generateLinearNumericId() // '000000000'\n * generateLinearNumericId() // '000000001'\n * ...\n * generateLinearNumericId() // '999999999'\n * generateLinearNumericId() // '1000000000'\n * ...\n * generateLinearNumericId() // '9999999999'\n * generateLinearNumericId() // '10000000000'\n * ```\n *\n */\nexport const generateLinearNumericId = createLinearNumericIdGenerator();\n\n/**\n * Is not recommended to use.\n *\n * Generates execution stack based pseudo-id (just sliced string from error stack)\n */\nexport const generateStackBasedId = () =>\n new Error().stack!.split('\\n').slice(1, 4).join('');\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,IAAM,SAAS;AAGf,IAAM,WAAW,6BAAiB;;;;;AAMlC,IAAa,cAAA,GAAA,OAAA,gBAA4B,UAAU,EAAE;;;;;AAMrD,IAAa,mBAAA,GAAA,OAAA,gBAAiC,UAAU,EAAE;;;;;AAM1D,IAAa,qBAAA,GAAA,OAAA,gBAAmC,QAAQ,EAAE;;;;;AAM1D,IAAa,0BAAA,GAAA,OAAA,gBAAwC,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;AAqB/D,IAAa,kCAAkC,OAAe,MAAM;CAClE,IAAI,YAAY;AAChB,cAAa;AACX,UAAQ,aAAa,UAAU,CAAC,SAAS,MAAM,IAAI;;;;;;;;;;;;;;;;;;AAmBvD,IAAa,0BAA0B,gCAAgC;;;;;;AAOvE,IAAa,8CACX,IAAI,OAAO,EAAC,MAAO,MAAM,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG"}
package/id.d.ts CHANGED
@@ -1,3 +1,19 @@
1
+ /**
2
+ * ---header-docs-section---
3
+ * # yummies/id
4
+ *
5
+ * ## Description
6
+ *
7
+ * Fast, URL-friendly identifiers based on **nanoid** with curated alphabets and lengths. Use for
8
+ * client-generated keys, trace ids, or UI instance ids where UUID size is unnecessary. Collisions are
9
+ * unlikely at these lengths but still assume server-side uniqueness for persisted entities.
10
+ *
11
+ * ## Usage
12
+ *
13
+ * ```ts
14
+ * import { generateId } from "yummies/id";
15
+ * ```
16
+ */
1
17
  /**
2
18
  * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.
3
19
  * Length: 6.
package/id.js CHANGED
@@ -1,5 +1,21 @@
1
1
  import { customAlphabet } from "nanoid";
2
2
  //#region src/id.ts
3
+ /**
4
+ * ---header-docs-section---
5
+ * # yummies/id
6
+ *
7
+ * ## Description
8
+ *
9
+ * Fast, URL-friendly identifiers based on **nanoid** with curated alphabets and lengths. Use for
10
+ * client-generated keys, trace ids, or UI instance ids where UUID size is unnecessary. Collisions are
11
+ * unlikely at these lengths but still assume server-side uniqueness for persisted entities.
12
+ *
13
+ * ## Usage
14
+ *
15
+ * ```ts
16
+ * import { generateId } from "yummies/id";
17
+ * ```
18
+ */
3
19
  var DIGITS = "0123456789";
4
20
  var ALPHABET = `abcdefghijklmnopqrstuvwxyz${DIGITS}`;
5
21
  /**
package/id.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"id.js","names":[],"sources":["../src/id.ts"],"sourcesContent":["import { customAlphabet } from 'nanoid';\n\nconst DIGITS = '0123456789';\nconst LATIN_CHARS = 'abcdefghijklmnopqrstuvwxyz';\n\nconst ALPHABET = `${LATIN_CHARS}${DIGITS}`;\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 6.\n */\nexport const generateId = customAlphabet(ALPHABET, 6);\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 4.\n */\nexport const generateShortId = customAlphabet(ALPHABET, 4);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 6.\n */\nexport const generateNumericId = customAlphabet(DIGITS, 6);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 4.\n */\nexport const generateNumericShortId = customAlphabet(DIGITS, 4);\n\n/**\n * Creates a function that generates unique strings based on call order.\n *\n * @example\n * ```ts\n * generateLinearNumericId = createLinearNumericIdGenerator(6);\n * generateLinearNumericId() // '000000'\n * generateLinearNumericId() // '000001'\n * ...\n * generateLinearNumericId() // '999999'\n * generateLinearNumericId() // '1000000'\n * ...\n * generateLinearNumericId() // '9999999'\n * generateLinearNumericId() // '10000000'\n * ```\n *\n * @param size Minimum string length.\n * @returns {()=>string}\n */\nexport const createLinearNumericIdGenerator = (size: number = 9) => {\n let lastCount = 0;\n return () => {\n return (lastCount++).toString().padStart(size, '0');\n };\n};\n\n/**\n *\n * @example\n * ```ts\n * generateLinearNumericId() // '000000000'\n * generateLinearNumericId() // '000000001'\n * ...\n * generateLinearNumericId() // '999999999'\n * generateLinearNumericId() // '1000000000'\n * ...\n * generateLinearNumericId() // '9999999999'\n * generateLinearNumericId() // '10000000000'\n * ```\n *\n */\nexport const generateLinearNumericId = createLinearNumericIdGenerator();\n\n/**\n * Is not recommended to use.\n *\n * Generates execution stack based pseudo-id (just sliced string from error stack)\n */\nexport const generateStackBasedId = () =>\n new Error().stack!.split('\\n').slice(1, 4).join('');\n"],"mappings":";;AAEA,IAAM,SAAS;AAGf,IAAM,WAAW,6BAAiB;;;;;AAMlC,IAAa,aAAa,eAAe,UAAU,EAAE;;;;;AAMrD,IAAa,kBAAkB,eAAe,UAAU,EAAE;;;;;AAM1D,IAAa,oBAAoB,eAAe,QAAQ,EAAE;;;;;AAM1D,IAAa,yBAAyB,eAAe,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;AAqB/D,IAAa,kCAAkC,OAAe,MAAM;CAClE,IAAI,YAAY;AAChB,cAAa;AACX,UAAQ,aAAa,UAAU,CAAC,SAAS,MAAM,IAAI;;;;;;;;;;;;;;;;;;AAmBvD,IAAa,0BAA0B,gCAAgC;;;;;;AAOvE,IAAa,8CACX,IAAI,OAAO,EAAC,MAAO,MAAM,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG"}
1
+ {"version":3,"file":"id.js","names":[],"sources":["../src/id.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/id\n *\n * ## Description\n *\n * Fast, URL-friendly identifiers based on **nanoid** with curated alphabets and lengths. Use for\n * client-generated keys, trace ids, or UI instance ids where UUID size is unnecessary. Collisions are\n * unlikely at these lengths but still assume server-side uniqueness for persisted entities.\n *\n * ## Usage\n *\n * ```ts\n * import { generateId } from \"yummies/id\";\n * ```\n */\n\nimport { customAlphabet } from 'nanoid';\n\nconst DIGITS = '0123456789';\nconst LATIN_CHARS = 'abcdefghijklmnopqrstuvwxyz';\n\nconst ALPHABET = `${LATIN_CHARS}${DIGITS}`;\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 6.\n */\nexport const generateId = customAlphabet(ALPHABET, 6);\n\n/**\n * Uses the alphabet `abcdefghijklmnopqrstuvwxyz0123456789`.\n * Length: 4.\n */\nexport const generateShortId = customAlphabet(ALPHABET, 4);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 6.\n */\nexport const generateNumericId = customAlphabet(DIGITS, 6);\n\n/**\n * Uses the alphabet `0123456789`.\n * Length: 4.\n */\nexport const generateNumericShortId = customAlphabet(DIGITS, 4);\n\n/**\n * Creates a function that generates unique strings based on call order.\n *\n * @example\n * ```ts\n * generateLinearNumericId = createLinearNumericIdGenerator(6);\n * generateLinearNumericId() // '000000'\n * generateLinearNumericId() // '000001'\n * ...\n * generateLinearNumericId() // '999999'\n * generateLinearNumericId() // '1000000'\n * ...\n * generateLinearNumericId() // '9999999'\n * generateLinearNumericId() // '10000000'\n * ```\n *\n * @param size Minimum string length.\n * @returns {()=>string}\n */\nexport const createLinearNumericIdGenerator = (size: number = 9) => {\n let lastCount = 0;\n return () => {\n return (lastCount++).toString().padStart(size, '0');\n };\n};\n\n/**\n *\n * @example\n * ```ts\n * generateLinearNumericId() // '000000000'\n * generateLinearNumericId() // '000000001'\n * ...\n * generateLinearNumericId() // '999999999'\n * generateLinearNumericId() // '1000000000'\n * ...\n * generateLinearNumericId() // '9999999999'\n * generateLinearNumericId() // '10000000000'\n * ```\n *\n */\nexport const generateLinearNumericId = createLinearNumericIdGenerator();\n\n/**\n * Is not recommended to use.\n *\n * Generates execution stack based pseudo-id (just sliced string from error stack)\n */\nexport const generateStackBasedId = () =>\n new Error().stack!.split('\\n').slice(1, 4).join('');\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,IAAM,SAAS;AAGf,IAAM,WAAW,6BAAiB;;;;;AAMlC,IAAa,aAAa,eAAe,UAAU,EAAE;;;;;AAMrD,IAAa,kBAAkB,eAAe,UAAU,EAAE;;;;;AAM1D,IAAa,oBAAoB,eAAe,QAAQ,EAAE;;;;;AAM1D,IAAa,yBAAyB,eAAe,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;AAqB/D,IAAa,kCAAkC,OAAe,MAAM;CAClE,IAAI,YAAY;AAChB,cAAa;AACX,UAAQ,aAAa,UAAU,CAAC,SAAS,MAAM,IAAI;;;;;;;;;;;;;;;;;;AAmBvD,IAAa,0BAA0B,gCAAgC;;;;;;AAOvE,IAAa,8CACX,IAAI,OAAO,EAAC,MAAO,MAAM,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG"}
package/imports.cjs CHANGED
@@ -3,6 +3,22 @@ require("./chunk-CVq3Gv4J.cjs");
3
3
  let yummies_async = require("yummies/async");
4
4
  //#region src/imports.ts
5
5
  /**
6
+ * ---header-docs-section---
7
+ * # yummies/imports
8
+ *
9
+ * ## Description
10
+ *
11
+ * Patterns for **dynamic `import()`**: retry with backoff, unpacking `{ default }` from mixed ESM
12
+ * shapes, and typed helpers around lazy-loaded modules. Ideal for code-split routes, optional panels,
13
+ * and resilient loading when the network or CDN flakes without freezing the whole app.
14
+ *
15
+ * ## Usage
16
+ *
17
+ * ```ts
18
+ * import { fetchLazyModule } from "yummies/imports";
19
+ * ```
20
+ */
21
+ /**
6
22
  * Lazily loads a module with retry support.
7
23
  *
8
24
  * @example
package/imports.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"imports.cjs","names":[],"sources":["../src/imports.ts"],"sourcesContent":["import { sleep } from 'yummies/async';\n\n/**\n * Lazily loads a module with retry support.\n *\n * @example\n * ```ts\n * fetchLazyModule(() => import('./test.ts'), 3) // starts loading test.ts\n * // If loading test.ts fails, fetchLazyModule retries by calling fetchModule() again\n * // It retries as many times as specified by attempts (3 by default)\n * ```\n */\nexport const fetchLazyModule = async <T>(\n fetchModule: () => Promise<T>,\n attempts = 3,\n delay = 1000,\n): Promise<T> => {\n const attemptsArray = Array.from<typeof fetchModule>({\n length: attempts,\n }).fill(fetchModule);\n\n let lastError: null | Error = null;\n\n for await (const attempt of attemptsArray) {\n try {\n if (lastError !== null) {\n await sleep(delay);\n }\n return await attempt();\n } catch (error) {\n lastError = error as Error;\n }\n }\n throw lastError;\n};\n\nexport type PackedAsyncModule<T> = Promise<T | { default: T }>;\n\n/**\n * Resolves either a direct value or an asynchronously imported module and unwraps its `default` export.\n *\n * @example\n * ```ts\n * const component = await unpackAsyncModule(import('./Component.ts'));\n * ```\n */\nexport const unpackAsyncModule = async <T>(\n maybeModule: T | PackedAsyncModule<T>,\n): Promise<T> => {\n if (maybeModule instanceof Promise) {\n const data = (await maybeModule) as any;\n\n if ((data as any).default) {\n return (data as any).default;\n } else {\n return data;\n }\n }\n\n return maybeModule;\n};\n"],"mappings":";;;;;;;;;;;;;;AAYA,IAAa,kBAAkB,OAC7B,aACA,WAAW,GACX,QAAQ,QACO;CACf,MAAM,gBAAgB,MAAM,KAAyB,EACnD,QAAQ,UACT,CAAC,CAAC,KAAK,YAAY;CAEpB,IAAI,YAA0B;AAE9B,YAAW,MAAM,WAAW,cAC1B,KAAI;AACF,MAAI,cAAc,KAChB,QAAA,GAAA,cAAA,OAAY,MAAM;AAEpB,SAAO,MAAM,SAAS;UACf,OAAO;AACd,cAAY;;AAGhB,OAAM;;;;;;;;;;AAaR,IAAa,oBAAoB,OAC/B,gBACe;AACf,KAAI,uBAAuB,SAAS;EAClC,MAAM,OAAQ,MAAM;AAEpB,MAAK,KAAa,QAChB,QAAQ,KAAa;MAErB,QAAO;;AAIX,QAAO"}
1
+ {"version":3,"file":"imports.cjs","names":[],"sources":["../src/imports.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/imports\n *\n * ## Description\n *\n * Patterns for **dynamic `import()`**: retry with backoff, unpacking `{ default }` from mixed ESM\n * shapes, and typed helpers around lazy-loaded modules. Ideal for code-split routes, optional panels,\n * and resilient loading when the network or CDN flakes without freezing the whole app.\n *\n * ## Usage\n *\n * ```ts\n * import { fetchLazyModule } from \"yummies/imports\";\n * ```\n */\n\nimport { sleep } from 'yummies/async';\n\n/**\n * Lazily loads a module with retry support.\n *\n * @example\n * ```ts\n * fetchLazyModule(() => import('./test.ts'), 3) // starts loading test.ts\n * // If loading test.ts fails, fetchLazyModule retries by calling fetchModule() again\n * // It retries as many times as specified by attempts (3 by default)\n * ```\n */\nexport const fetchLazyModule = async <T>(\n fetchModule: () => Promise<T>,\n attempts = 3,\n delay = 1000,\n): Promise<T> => {\n const attemptsArray = Array.from<typeof fetchModule>({\n length: attempts,\n }).fill(fetchModule);\n\n let lastError: null | Error = null;\n\n for await (const attempt of attemptsArray) {\n try {\n if (lastError !== null) {\n await sleep(delay);\n }\n return await attempt();\n } catch (error) {\n lastError = error as Error;\n }\n }\n throw lastError;\n};\n\nexport type PackedAsyncModule<T> = Promise<T | { default: T }>;\n\n/**\n * Resolves either a direct value or an asynchronously imported module and unwraps its `default` export.\n *\n * @example\n * ```ts\n * const component = await unpackAsyncModule(import('./Component.ts'));\n * ```\n */\nexport const unpackAsyncModule = async <T>(\n maybeModule: T | PackedAsyncModule<T>,\n): Promise<T> => {\n if (maybeModule instanceof Promise) {\n const data = (await maybeModule) as any;\n\n if ((data as any).default) {\n return (data as any).default;\n } else {\n return data;\n }\n }\n\n return maybeModule;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAa,kBAAkB,OAC7B,aACA,WAAW,GACX,QAAQ,QACO;CACf,MAAM,gBAAgB,MAAM,KAAyB,EACnD,QAAQ,UACT,CAAC,CAAC,KAAK,YAAY;CAEpB,IAAI,YAA0B;AAE9B,YAAW,MAAM,WAAW,cAC1B,KAAI;AACF,MAAI,cAAc,KAChB,QAAA,GAAA,cAAA,OAAY,MAAM;AAEpB,SAAO,MAAM,SAAS;UACf,OAAO;AACd,cAAY;;AAGhB,OAAM;;;;;;;;;;AAaR,IAAa,oBAAoB,OAC/B,gBACe;AACf,KAAI,uBAAuB,SAAS;EAClC,MAAM,OAAQ,MAAM;AAEpB,MAAK,KAAa,QAChB,QAAQ,KAAa;MAErB,QAAO;;AAIX,QAAO"}
package/imports.d.ts CHANGED
@@ -1,3 +1,19 @@
1
+ /**
2
+ * ---header-docs-section---
3
+ * # yummies/imports
4
+ *
5
+ * ## Description
6
+ *
7
+ * Patterns for **dynamic `import()`**: retry with backoff, unpacking `{ default }` from mixed ESM
8
+ * shapes, and typed helpers around lazy-loaded modules. Ideal for code-split routes, optional panels,
9
+ * and resilient loading when the network or CDN flakes without freezing the whole app.
10
+ *
11
+ * ## Usage
12
+ *
13
+ * ```ts
14
+ * import { fetchLazyModule } from "yummies/imports";
15
+ * ```
16
+ */
1
17
  /**
2
18
  * Lazily loads a module with retry support.
3
19
  *
package/imports.js CHANGED
@@ -1,6 +1,22 @@
1
1
  import { sleep } from "yummies/async";
2
2
  //#region src/imports.ts
3
3
  /**
4
+ * ---header-docs-section---
5
+ * # yummies/imports
6
+ *
7
+ * ## Description
8
+ *
9
+ * Patterns for **dynamic `import()`**: retry with backoff, unpacking `{ default }` from mixed ESM
10
+ * shapes, and typed helpers around lazy-loaded modules. Ideal for code-split routes, optional panels,
11
+ * and resilient loading when the network or CDN flakes without freezing the whole app.
12
+ *
13
+ * ## Usage
14
+ *
15
+ * ```ts
16
+ * import { fetchLazyModule } from "yummies/imports";
17
+ * ```
18
+ */
19
+ /**
4
20
  * Lazily loads a module with retry support.
5
21
  *
6
22
  * @example
package/imports.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"imports.js","names":[],"sources":["../src/imports.ts"],"sourcesContent":["import { sleep } from 'yummies/async';\n\n/**\n * Lazily loads a module with retry support.\n *\n * @example\n * ```ts\n * fetchLazyModule(() => import('./test.ts'), 3) // starts loading test.ts\n * // If loading test.ts fails, fetchLazyModule retries by calling fetchModule() again\n * // It retries as many times as specified by attempts (3 by default)\n * ```\n */\nexport const fetchLazyModule = async <T>(\n fetchModule: () => Promise<T>,\n attempts = 3,\n delay = 1000,\n): Promise<T> => {\n const attemptsArray = Array.from<typeof fetchModule>({\n length: attempts,\n }).fill(fetchModule);\n\n let lastError: null | Error = null;\n\n for await (const attempt of attemptsArray) {\n try {\n if (lastError !== null) {\n await sleep(delay);\n }\n return await attempt();\n } catch (error) {\n lastError = error as Error;\n }\n }\n throw lastError;\n};\n\nexport type PackedAsyncModule<T> = Promise<T | { default: T }>;\n\n/**\n * Resolves either a direct value or an asynchronously imported module and unwraps its `default` export.\n *\n * @example\n * ```ts\n * const component = await unpackAsyncModule(import('./Component.ts'));\n * ```\n */\nexport const unpackAsyncModule = async <T>(\n maybeModule: T | PackedAsyncModule<T>,\n): Promise<T> => {\n if (maybeModule instanceof Promise) {\n const data = (await maybeModule) as any;\n\n if ((data as any).default) {\n return (data as any).default;\n } else {\n return data;\n }\n }\n\n return maybeModule;\n};\n"],"mappings":";;;;;;;;;;;;AAYA,IAAa,kBAAkB,OAC7B,aACA,WAAW,GACX,QAAQ,QACO;CACf,MAAM,gBAAgB,MAAM,KAAyB,EACnD,QAAQ,UACT,CAAC,CAAC,KAAK,YAAY;CAEpB,IAAI,YAA0B;AAE9B,YAAW,MAAM,WAAW,cAC1B,KAAI;AACF,MAAI,cAAc,KAChB,OAAM,MAAM,MAAM;AAEpB,SAAO,MAAM,SAAS;UACf,OAAO;AACd,cAAY;;AAGhB,OAAM;;;;;;;;;;AAaR,IAAa,oBAAoB,OAC/B,gBACe;AACf,KAAI,uBAAuB,SAAS;EAClC,MAAM,OAAQ,MAAM;AAEpB,MAAK,KAAa,QAChB,QAAQ,KAAa;MAErB,QAAO;;AAIX,QAAO"}
1
+ {"version":3,"file":"imports.js","names":[],"sources":["../src/imports.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/imports\n *\n * ## Description\n *\n * Patterns for **dynamic `import()`**: retry with backoff, unpacking `{ default }` from mixed ESM\n * shapes, and typed helpers around lazy-loaded modules. Ideal for code-split routes, optional panels,\n * and resilient loading when the network or CDN flakes without freezing the whole app.\n *\n * ## Usage\n *\n * ```ts\n * import { fetchLazyModule } from \"yummies/imports\";\n * ```\n */\n\nimport { sleep } from 'yummies/async';\n\n/**\n * Lazily loads a module with retry support.\n *\n * @example\n * ```ts\n * fetchLazyModule(() => import('./test.ts'), 3) // starts loading test.ts\n * // If loading test.ts fails, fetchLazyModule retries by calling fetchModule() again\n * // It retries as many times as specified by attempts (3 by default)\n * ```\n */\nexport const fetchLazyModule = async <T>(\n fetchModule: () => Promise<T>,\n attempts = 3,\n delay = 1000,\n): Promise<T> => {\n const attemptsArray = Array.from<typeof fetchModule>({\n length: attempts,\n }).fill(fetchModule);\n\n let lastError: null | Error = null;\n\n for await (const attempt of attemptsArray) {\n try {\n if (lastError !== null) {\n await sleep(delay);\n }\n return await attempt();\n } catch (error) {\n lastError = error as Error;\n }\n }\n throw lastError;\n};\n\nexport type PackedAsyncModule<T> = Promise<T | { default: T }>;\n\n/**\n * Resolves either a direct value or an asynchronously imported module and unwraps its `default` export.\n *\n * @example\n * ```ts\n * const component = await unpackAsyncModule(import('./Component.ts'));\n * ```\n */\nexport const unpackAsyncModule = async <T>(\n maybeModule: T | PackedAsyncModule<T>,\n): Promise<T> => {\n if (maybeModule instanceof Promise) {\n const data = (await maybeModule) as any;\n\n if ((data as any).default) {\n return (data as any).default;\n } else {\n return data;\n }\n }\n\n return maybeModule;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAa,kBAAkB,OAC7B,aACA,WAAW,GACX,QAAQ,QACO;CACf,MAAM,gBAAgB,MAAM,KAAyB,EACnD,QAAQ,UACT,CAAC,CAAC,KAAK,YAAY;CAEpB,IAAI,YAA0B;AAE9B,YAAW,MAAM,WAAW,cAC1B,KAAI;AACF,MAAI,cAAc,KAChB,OAAM,MAAM,MAAM;AAEpB,SAAO,MAAM,SAAS;UACf,OAAO;AACd,cAAY;;AAGhB,OAAM;;;;;;;;;;AAaR,IAAa,oBAAoB,OAC/B,gBACe;AACf,KAAI,uBAAuB,SAAS;EAClC,MAAM,OAAQ,MAAM;AAEpB,MAAK,KAAa,QAChB,QAAQ,KAAa;MAErB,QAAO;;AAIX,QAAO"}
package/math.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"math.cjs","names":[],"sources":["../src/math.ts"],"sourcesContent":["import type { Maybe } from 'yummies/types';\n\n/**\n * Converts degrees to radians.\n *\n * @example\n * ```ts\n * degToRad(180); // Math.PI\n * ```\n */\nexport function degToRad(deg: number) {\n return deg * (Math.PI / 180);\n}\n\n/**\n * Converts radians to degrees.\n *\n * @example\n * ```ts\n * radToDeg(Math.PI); // 180\n * ```\n */\nexport function radToDeg(rad: number) {\n return rad * (180 / Math.PI);\n}\n\n/**\n * Returns what percentage `value` is of `from`.\n * @example\n * ```ts\n * percentFrom(500, 2000) // 25\n * percentFrom(1000, 2000) // 50\n * ```\n */\nexport const percentFrom = (value: Maybe<number>, from: Maybe<number>) => {\n return ((value ?? 0) / (from ?? 0)) * 100 || 0;\n};\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,SAAS,KAAa;AACpC,QAAO,OAAO,KAAK,KAAK;;;;;;;;;;AAW1B,SAAgB,SAAS,KAAa;AACpC,QAAO,OAAO,MAAM,KAAK;;;;;;;;;;AAW3B,IAAa,eAAe,OAAsB,SAAwB;AACxE,SAAS,SAAS,MAAM,QAAQ,KAAM,OAAO"}
1
+ {"version":3,"file":"math.cjs","names":[],"sources":["../src/math.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/math\n *\n * ## Description\n *\n * Tiny math helpers: degree/radian conversion and nullable-safe percentage calculations. They wrap\n * common formulas so components do not repeat magic numbers or null checks when deriving layout or\n * charts from partially loaded data.\n *\n * ## Usage\n *\n * ```ts\n * import { degToRad, percentFrom } from \"yummies/math\";\n * ```\n */\n\nimport type { Maybe } from 'yummies/types';\n\n/**\n * Converts degrees to radians.\n *\n * @example\n * ```ts\n * degToRad(180); // Math.PI\n * ```\n */\nexport function degToRad(deg: number) {\n return deg * (Math.PI / 180);\n}\n\n/**\n * Converts radians to degrees.\n *\n * @example\n * ```ts\n * radToDeg(Math.PI); // 180\n * ```\n */\nexport function radToDeg(rad: number) {\n return rad * (180 / Math.PI);\n}\n\n/**\n * Returns what percentage `value` is of `from`.\n * @example\n * ```ts\n * percentFrom(500, 2000) // 25\n * percentFrom(1000, 2000) // 50\n * ```\n */\nexport const percentFrom = (value: Maybe<number>, from: Maybe<number>) => {\n return ((value ?? 0) / (from ?? 0)) * 100 || 0;\n};\n"],"mappings":";;;;;;;;;;AA2BA,SAAgB,SAAS,KAAa;AACpC,QAAO,OAAO,KAAK,KAAK;;;;;;;;;;AAW1B,SAAgB,SAAS,KAAa;AACpC,QAAO,OAAO,MAAM,KAAK;;;;;;;;;;AAW3B,IAAa,eAAe,OAAsB,SAAwB;AACxE,SAAS,SAAS,MAAM,QAAQ,KAAM,OAAO"}
package/math.d.ts CHANGED
@@ -1,5 +1,22 @@
1
1
  import { Maybe } from 'yummies/types';
2
2
 
3
+ /**
4
+ * ---header-docs-section---
5
+ * # yummies/math
6
+ *
7
+ * ## Description
8
+ *
9
+ * Tiny math helpers: degree/radian conversion and nullable-safe percentage calculations. They wrap
10
+ * common formulas so components do not repeat magic numbers or null checks when deriving layout or
11
+ * charts from partially loaded data.
12
+ *
13
+ * ## Usage
14
+ *
15
+ * ```ts
16
+ * import { degToRad, percentFrom } from "yummies/math";
17
+ * ```
18
+ */
19
+
3
20
  /**
4
21
  * Converts degrees to radians.
5
22
  *
package/math.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"math.js","names":[],"sources":["../src/math.ts"],"sourcesContent":["import type { Maybe } from 'yummies/types';\n\n/**\n * Converts degrees to radians.\n *\n * @example\n * ```ts\n * degToRad(180); // Math.PI\n * ```\n */\nexport function degToRad(deg: number) {\n return deg * (Math.PI / 180);\n}\n\n/**\n * Converts radians to degrees.\n *\n * @example\n * ```ts\n * radToDeg(Math.PI); // 180\n * ```\n */\nexport function radToDeg(rad: number) {\n return rad * (180 / Math.PI);\n}\n\n/**\n * Returns what percentage `value` is of `from`.\n * @example\n * ```ts\n * percentFrom(500, 2000) // 25\n * percentFrom(1000, 2000) // 50\n * ```\n */\nexport const percentFrom = (value: Maybe<number>, from: Maybe<number>) => {\n return ((value ?? 0) / (from ?? 0)) * 100 || 0;\n};\n"],"mappings":";;;;;;;;;AAUA,SAAgB,SAAS,KAAa;AACpC,QAAO,OAAO,KAAK,KAAK;;;;;;;;;;AAW1B,SAAgB,SAAS,KAAa;AACpC,QAAO,OAAO,MAAM,KAAK;;;;;;;;;;AAW3B,IAAa,eAAe,OAAsB,SAAwB;AACxE,SAAS,SAAS,MAAM,QAAQ,KAAM,OAAO"}
1
+ {"version":3,"file":"math.js","names":[],"sources":["../src/math.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/math\n *\n * ## Description\n *\n * Tiny math helpers: degree/radian conversion and nullable-safe percentage calculations. They wrap\n * common formulas so components do not repeat magic numbers or null checks when deriving layout or\n * charts from partially loaded data.\n *\n * ## Usage\n *\n * ```ts\n * import { degToRad, percentFrom } from \"yummies/math\";\n * ```\n */\n\nimport type { Maybe } from 'yummies/types';\n\n/**\n * Converts degrees to radians.\n *\n * @example\n * ```ts\n * degToRad(180); // Math.PI\n * ```\n */\nexport function degToRad(deg: number) {\n return deg * (Math.PI / 180);\n}\n\n/**\n * Converts radians to degrees.\n *\n * @example\n * ```ts\n * radToDeg(Math.PI); // 180\n * ```\n */\nexport function radToDeg(rad: number) {\n return rad * (180 / Math.PI);\n}\n\n/**\n * Returns what percentage `value` is of `from`.\n * @example\n * ```ts\n * percentFrom(500, 2000) // 25\n * percentFrom(1000, 2000) // 50\n * ```\n */\nexport const percentFrom = (value: Maybe<number>, from: Maybe<number>) => {\n return ((value ?? 0) / (from ?? 0)) * 100 || 0;\n};\n"],"mappings":";;;;;;;;;AA2BA,SAAgB,SAAS,KAAa;AACpC,QAAO,OAAO,KAAK,KAAK;;;;;;;;;;AAW1B,SAAgB,SAAS,KAAa;AACpC,QAAO,OAAO,MAAM,KAAK;;;;;;;;;;AAW3B,IAAa,eAAe,OAAsB,SAAwB;AACxE,SAAS,SAAS,MAAM,QAAQ,KAAM,OAAO"}
package/media.cjs CHANGED
@@ -3,6 +3,22 @@ require("./chunk-CVq3Gv4J.cjs");
3
3
  let yummies_math = require("yummies/math");
4
4
  //#region src/media.ts
5
5
  /**
6
+ * ---header-docs-section---
7
+ * # yummies/media
8
+ *
9
+ * ## Description
10
+ *
11
+ * Binary and media helpers: Base64 data URLs, object URLs, image orientation fixes, and related
12
+ * browser APIs wrapped in promises. Centralizes `FileReader`/`URL.createObjectURL` usage so image
13
+ * pipelines and uploads stay consistent and easier to test or mock.
14
+ *
15
+ * ## Usage
16
+ *
17
+ * ```ts
18
+ * import { blobToBase64 } from "yummies/media";
19
+ * ```
20
+ */
21
+ /**
6
22
  * Reads a {@link Blob} as a **data URL** string (`data:<mime>;base64,...`) using {@link FileReader#readAsDataURL}.
7
23
  *
8
24
  * Useful for previewing uploads, embedding small assets inline, or serializing binary for APIs that
package/media.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"media.cjs","names":[],"sources":["../src/media.ts"],"sourcesContent":["import { degToRad } from 'yummies/math';\n\n/**\n * Reads a {@link Blob} as a **data URL** string (`data:<mime>;base64,...`) using {@link FileReader#readAsDataURL}.\n *\n * Useful for previewing uploads, embedding small assets inline, or serializing binary for APIs that\n * expect Base64-in-JSON. The result includes the MIME prefix, not raw Base64 alone — use\n * {@link decodeDataUrl} if you need the payload and type separately.\n *\n * @param blob - Any `Blob` or `File` (files are blobs).\n * @returns Resolves to the data URL string; rejects if reading fails.\n *\n * @example\n * ```ts\n * const dataUrl = await blobToBase64(file);\n * previewImg.src = dataUrl;\n * ```\n *\n * @example\n * ```ts\n * const fromFetch = await fetch('/api/export').then((r) => r.blob());\n * const dataUrl = await blobToBase64(fromFetch);\n * ```\n */\nexport function blobToBase64(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onloadend = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\n/**\n * If `urlOrBlob` is already a string, returns it unchanged. If it is a {@link Blob}, returns\n * `URL.createObjectURL(blob)` — a short-lived `blob:` URL valid in this document until\n * {@link URL.revokeObjectURL} is called.\n *\n * Pair with {@link renderImage} or `<img src>` without re-fetching binary data. **Remember to\n * `revokeObjectURL`** when the URL is no longer needed to avoid retaining blob memory.\n *\n * @param urlOrBlob - Remote/http(s) URL string, data URL, or a `Blob` / `File`.\n * @returns A string suitable for `HTMLImageElement.src` or similar.\n *\n * @example\n * ```ts\n * const src = blobToUrl(uploadedFile);\n * img.src = src;\n * // when done:\n * URL.revokeObjectURL(src);\n * ```\n *\n * @example\n * ```ts\n * blobToUrl('https://cdn.example.com/logo.png'); // passed through as-is\n * ```\n */\nexport const blobToUrl = (urlOrBlob: string | Blob) =>\n urlOrBlob instanceof Blob ? URL.createObjectURL(urlOrBlob) : urlOrBlob;\n\n/**\n * Creates a new {@link Blob} from a {@link File}, copying the bytes and keeping `file.type` as\n * the blob’s MIME type. Handy when an API accepts `Blob` but you only have `File`, or you want a\n * plain blob without the `File` name/lastModified metadata.\n *\n * @param file - Source file from an `<input type=\"file\">` or drag-and-drop.\n * @returns A `Blob` with the same content and `type` as the file.\n *\n * @example\n * ```ts\n * const blob = fileToBlob(fileFromInput);\n * await uploadEndpoint(blob);\n * ```\n *\n * @example\n * ```ts\n * const blob = fileToBlob(imageFile);\n * const url = URL.createObjectURL(blob);\n * ```\n */\nexport const fileToBlob = (file: File) => {\n return new Blob([file], { type: file.type });\n};\n\n/**\n * Draws an {@link HTMLImageElement} onto an offscreen canvas, then builds a {@link Blob} from the\n * raster (via `canvas.toDataURL` + binary decode). Dimensions use `naturalWidth` / `naturalHeight`,\n * falling back to `300×300` if those are zero (e.g. not yet decoded).\n *\n * **CORS:** if the image is cross-origin without proper CORS headers, the canvas may be\n * *tainted* and `toDataURL` can throw — same browser rules as any canvas export.\n *\n * @param imageElement - Loaded image (`complete` / decoded recommended).\n * @param mimeType - Output MIME type, e.g. `'image/png'` (default) or `'image/jpeg'`. Encoder support is browser-dependent.\n * @returns Encoded image as a `Blob` with a matching `type` when possible.\n *\n * @example\n * ```ts\n * const img = await renderImage('/photo.jpg');\n * const jpegBlob = imageToBlob(img, 'image/jpeg');\n * ```\n *\n * @example\n * ```ts\n * const pngBlob = imageToBlob(cachedImg); // default image/png\n * ```\n */\nexport const imageToBlob = (\n imageElement: HTMLImageElement,\n mimeType: string = 'image/png',\n) => {\n const canvas = document.createElement('canvas');\n\n canvas.width = imageElement.naturalWidth || 300;\n canvas.height = imageElement.naturalHeight || 300;\n\n canvas.getContext('2d')!.drawImage(imageElement, 0, 0);\n\n const dataUri = canvas.toDataURL(mimeType, 1);\n const base64data = dataUri.split(',')[1];\n const base64MimeType = dataUri.split(';')[0].slice(5);\n\n const bytes = globalThis.atob(base64data);\n const buf = new ArrayBuffer(bytes.length);\n const array = new Uint8Array(buf);\n\n for (let index = 0; index < bytes.length; index++) {\n array[index] = bytes.charCodeAt(index);\n }\n\n const blob = new Blob([array], { type: base64MimeType });\n\n return blob;\n};\n\n/**\n * Loads a resource into a new `HTMLImageElement`: `src` is set via {@link blobToUrl} (so blobs get\n * object URLs, strings are used directly). Resolves on `load` with the same element; rejects on\n * `error` (e.g. bad URL, network failure, corrupt image) **with no rejection value**.\n *\n * Does not add the node to the DOM — use the returned element for canvas, measuring, or\n * {@link imageToBlob}.\n *\n * @param urlOrBlob - Remote URL, data URL, or `Blob` / `File`.\n * @returns Promise that fulfills with the loaded `HTMLImageElement`.\n *\n * @example\n * ```ts\n * const img = await renderImage('https://example.com/pic.png');\n * document.body.appendChild(img);\n * ```\n *\n * @example\n * ```ts\n * const img = await renderImage(pickedFile);\n * const blob = imageToBlob(img, 'image/webp');\n * ```\n */\nexport const renderImage = (urlOrBlob: Blob | string) =>\n new Promise<HTMLImageElement>((resolve, reject) => {\n const image = new Image();\n image.src = blobToUrl(urlOrBlob);\n image.onload = () => resolve(image);\n image.onerror = () => reject();\n });\n\nfunction cropImageFromCanvas(context: CanvasRenderingContext2D) {\n const canvas = context.canvas;\n let w = canvas.width;\n let h = canvas.height;\n const pix: { x: number[]; y: number[] } = { x: [], y: [] };\n const imageData = context.getImageData(0, 0, canvas.width, canvas.height);\n let x: number;\n let y: number;\n let index: number;\n\n for (y = 0; y < h; y++) {\n for (x = 0; x < w; x++) {\n index = (y * w + x) * 4;\n if (imageData.data[index + 3] > 0) {\n pix.x.push(x);\n pix.y.push(y);\n }\n }\n }\n pix.x.sort((a, b) => a - b);\n pix.y.sort((a, b) => a - b);\n const n = pix.x.length - 1;\n\n w = 1 + pix.x[n] - pix.x[0];\n h = 1 + pix.y[n] - pix.y[0];\n const cut = context.getImageData(pix.x[0], pix.y[0], w, h);\n\n canvas.width = w;\n canvas.height = h;\n context.putImageData(cut, 0, 0);\n return canvas;\n}\n\n// TODO: ломает iphone с огромными изображениями\n/**\n * Rotates `image` around its center on a square canvas (side = max(width, height)), then **crops**\n * transparent margins by trimming to the bounding box of pixels with non-zero alpha.\n * Returns a **new** loaded `HTMLImageElement` (PNG data URL under the hood via {@link renderImage}).\n *\n * `angle` is in **degrees**; converted with {@link degToRad} from `yummies/math`.\n *\n * Very large sources can stress memory on some mobile browsers (known iPhone issues — see TODO in source).\n *\n * @param image - Source image (should be decoded; uses `width` / `height`).\n * @param angle - Rotation in degrees (e.g. `90` for quarter turn).\n * @returns Promise of a new `HTMLImageElement` showing the rotated, cropped result.\n *\n * @example\n * ```ts\n * const upright = await rotateImage(landscapeImg, 90);\n * ```\n *\n * @example\n * ```ts\n * const fixed = await rotateImage(await renderImage(file), -15);\n * ```\n */\nexport const rotateImage = (image: HTMLImageElement, angle: number) => {\n const maxSize = Math.max(image.width, image.height);\n const canvas = document.createElement('canvas');\n canvas.width = maxSize;\n canvas.height = maxSize;\n const context = canvas.getContext('2d')!;\n context.save();\n context.translate(canvas.width / 2, canvas.height / 2);\n context.rotate(degToRad(angle));\n context.drawImage(image, -image.width / 2, -image.height / 2);\n context.restore();\n cropImageFromCanvas(context);\n return renderImage(canvas.toDataURL('image/png'));\n};\n\ninterface DecodedDataUrl {\n mimeType?: string;\n data?: string;\n}\n\n/**\n * Parses a `data:` URL of the form `data:<mime>;base64,<payload>` into its MIME type and raw\n * Base64 body (without the `data:...;base64,` prefix). Non-matching strings yield `undefined`\n * fields in the result object.\n *\n * @param url - Full data URL string.\n * @returns `{ mimeType, data }` — both optional when the regex does not match.\n *\n * @example\n * ```ts\n * const { mimeType, data } = decodeDataUrl('data:image/png;base64,iVBORw0KGgo=');\n * // mimeType === 'image/png', data === 'iVBORw0KGgo='\n * ```\n *\n * @example\n * ```ts\n * decodeDataUrl('not-a-data-url'); // { mimeType: undefined, data: undefined }\n * ```\n */\nexport function decodeDataUrl(url: string): DecodedDataUrl {\n const regex = /^data:(.*);base64,\\s?(.*)$/;\n const matches = new RegExp(regex).exec(url);\n\n return {\n mimeType: matches?.[1],\n data: matches?.[2],\n };\n}\n\n/**\n * Returns `true` if `url` starts with `https://` or `http://` (case-sensitive, as in\n * `String#startsWith`). Does not validate that the rest is a well-formed URL — only the scheme\n * prefix. `blob:`, `data:`, and relative paths return `false`.\n *\n * @param url - String to test (often a user-provided or configured href).\n *\n * @example\n * ```ts\n * isHttpUrl('https://example.com/path'); // true\n * isHttpUrl('http://localhost:3000'); // true\n * ```\n *\n * @example\n * ```ts\n * isHttpUrl('//cdn.example.com'); // false — no explicit http(s) prefix\n * isHttpUrl('/assets/logo.png'); // false — relative path\n * ```\n */\nexport const isHttpUrl = (url: string): boolean => {\n return url.startsWith('https://') || url.startsWith('http://');\n};\n\n/**\n * Returns `true` when `str` is a data URL that {@link decodeDataUrl} can parse **and** the MIME\n * type starts with `image/` (e.g. `image/png`, `image/jpeg`). Requires both a non-empty Base64\n * payload and an image MIME — arbitrary `data:text/plain;base64,...` is `false`.\n *\n * @param str - Candidate string (often `img.src` or API payload).\n *\n * @example\n * ```ts\n * isBase64Image('data:image/png;base64,iVBORw0KGgo='); // true\n * ```\n *\n * @example\n * ```ts\n * isBase64Image('https://example.com/x.png'); // false\n * isBase64Image('data:text/plain;base64,SGk='); // false\n * ```\n */\nexport const isBase64Image = (str: string): boolean => {\n const { mimeType, data } = decodeDataUrl(str);\n return !!(data && mimeType?.startsWith('image/'));\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"media.cjs","names":[],"sources":["../src/media.ts"],"sourcesContent":["/**\n * ---header-docs-section---\n * # yummies/media\n *\n * ## Description\n *\n * Binary and media helpers: Base64 data URLs, object URLs, image orientation fixes, and related\n * browser APIs wrapped in promises. Centralizes `FileReader`/`URL.createObjectURL` usage so image\n * pipelines and uploads stay consistent and easier to test or mock.\n *\n * ## Usage\n *\n * ```ts\n * import { blobToBase64 } from \"yummies/media\";\n * ```\n */\n\nimport { degToRad } from 'yummies/math';\n\n/**\n * Reads a {@link Blob} as a **data URL** string (`data:<mime>;base64,...`) using {@link FileReader#readAsDataURL}.\n *\n * Useful for previewing uploads, embedding small assets inline, or serializing binary for APIs that\n * expect Base64-in-JSON. The result includes the MIME prefix, not raw Base64 alone — use\n * {@link decodeDataUrl} if you need the payload and type separately.\n *\n * @param blob - Any `Blob` or `File` (files are blobs).\n * @returns Resolves to the data URL string; rejects if reading fails.\n *\n * @example\n * ```ts\n * const dataUrl = await blobToBase64(file);\n * previewImg.src = dataUrl;\n * ```\n *\n * @example\n * ```ts\n * const fromFetch = await fetch('/api/export').then((r) => r.blob());\n * const dataUrl = await blobToBase64(fromFetch);\n * ```\n */\nexport function blobToBase64(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onloadend = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\n/**\n * If `urlOrBlob` is already a string, returns it unchanged. If it is a {@link Blob}, returns\n * `URL.createObjectURL(blob)` — a short-lived `blob:` URL valid in this document until\n * {@link URL.revokeObjectURL} is called.\n *\n * Pair with {@link renderImage} or `<img src>` without re-fetching binary data. **Remember to\n * `revokeObjectURL`** when the URL is no longer needed to avoid retaining blob memory.\n *\n * @param urlOrBlob - Remote/http(s) URL string, data URL, or a `Blob` / `File`.\n * @returns A string suitable for `HTMLImageElement.src` or similar.\n *\n * @example\n * ```ts\n * const src = blobToUrl(uploadedFile);\n * img.src = src;\n * // when done:\n * URL.revokeObjectURL(src);\n * ```\n *\n * @example\n * ```ts\n * blobToUrl('https://cdn.example.com/logo.png'); // passed through as-is\n * ```\n */\nexport const blobToUrl = (urlOrBlob: string | Blob) =>\n urlOrBlob instanceof Blob ? URL.createObjectURL(urlOrBlob) : urlOrBlob;\n\n/**\n * Creates a new {@link Blob} from a {@link File}, copying the bytes and keeping `file.type` as\n * the blob’s MIME type. Handy when an API accepts `Blob` but you only have `File`, or you want a\n * plain blob without the `File` name/lastModified metadata.\n *\n * @param file - Source file from an `<input type=\"file\">` or drag-and-drop.\n * @returns A `Blob` with the same content and `type` as the file.\n *\n * @example\n * ```ts\n * const blob = fileToBlob(fileFromInput);\n * await uploadEndpoint(blob);\n * ```\n *\n * @example\n * ```ts\n * const blob = fileToBlob(imageFile);\n * const url = URL.createObjectURL(blob);\n * ```\n */\nexport const fileToBlob = (file: File) => {\n return new Blob([file], { type: file.type });\n};\n\n/**\n * Draws an {@link HTMLImageElement} onto an offscreen canvas, then builds a {@link Blob} from the\n * raster (via `canvas.toDataURL` + binary decode). Dimensions use `naturalWidth` / `naturalHeight`,\n * falling back to `300×300` if those are zero (e.g. not yet decoded).\n *\n * **CORS:** if the image is cross-origin without proper CORS headers, the canvas may be\n * *tainted* and `toDataURL` can throw — same browser rules as any canvas export.\n *\n * @param imageElement - Loaded image (`complete` / decoded recommended).\n * @param mimeType - Output MIME type, e.g. `'image/png'` (default) or `'image/jpeg'`. Encoder support is browser-dependent.\n * @returns Encoded image as a `Blob` with a matching `type` when possible.\n *\n * @example\n * ```ts\n * const img = await renderImage('/photo.jpg');\n * const jpegBlob = imageToBlob(img, 'image/jpeg');\n * ```\n *\n * @example\n * ```ts\n * const pngBlob = imageToBlob(cachedImg); // default image/png\n * ```\n */\nexport const imageToBlob = (\n imageElement: HTMLImageElement,\n mimeType: string = 'image/png',\n) => {\n const canvas = document.createElement('canvas');\n\n canvas.width = imageElement.naturalWidth || 300;\n canvas.height = imageElement.naturalHeight || 300;\n\n canvas.getContext('2d')!.drawImage(imageElement, 0, 0);\n\n const dataUri = canvas.toDataURL(mimeType, 1);\n const base64data = dataUri.split(',')[1];\n const base64MimeType = dataUri.split(';')[0].slice(5);\n\n const bytes = globalThis.atob(base64data);\n const buf = new ArrayBuffer(bytes.length);\n const array = new Uint8Array(buf);\n\n for (let index = 0; index < bytes.length; index++) {\n array[index] = bytes.charCodeAt(index);\n }\n\n const blob = new Blob([array], { type: base64MimeType });\n\n return blob;\n};\n\n/**\n * Loads a resource into a new `HTMLImageElement`: `src` is set via {@link blobToUrl} (so blobs get\n * object URLs, strings are used directly). Resolves on `load` with the same element; rejects on\n * `error` (e.g. bad URL, network failure, corrupt image) **with no rejection value**.\n *\n * Does not add the node to the DOM — use the returned element for canvas, measuring, or\n * {@link imageToBlob}.\n *\n * @param urlOrBlob - Remote URL, data URL, or `Blob` / `File`.\n * @returns Promise that fulfills with the loaded `HTMLImageElement`.\n *\n * @example\n * ```ts\n * const img = await renderImage('https://example.com/pic.png');\n * document.body.appendChild(img);\n * ```\n *\n * @example\n * ```ts\n * const img = await renderImage(pickedFile);\n * const blob = imageToBlob(img, 'image/webp');\n * ```\n */\nexport const renderImage = (urlOrBlob: Blob | string) =>\n new Promise<HTMLImageElement>((resolve, reject) => {\n const image = new Image();\n image.src = blobToUrl(urlOrBlob);\n image.onload = () => resolve(image);\n image.onerror = () => reject();\n });\n\nfunction cropImageFromCanvas(context: CanvasRenderingContext2D) {\n const canvas = context.canvas;\n let w = canvas.width;\n let h = canvas.height;\n const pix: { x: number[]; y: number[] } = { x: [], y: [] };\n const imageData = context.getImageData(0, 0, canvas.width, canvas.height);\n let x: number;\n let y: number;\n let index: number;\n\n for (y = 0; y < h; y++) {\n for (x = 0; x < w; x++) {\n index = (y * w + x) * 4;\n if (imageData.data[index + 3] > 0) {\n pix.x.push(x);\n pix.y.push(y);\n }\n }\n }\n pix.x.sort((a, b) => a - b);\n pix.y.sort((a, b) => a - b);\n const n = pix.x.length - 1;\n\n w = 1 + pix.x[n] - pix.x[0];\n h = 1 + pix.y[n] - pix.y[0];\n const cut = context.getImageData(pix.x[0], pix.y[0], w, h);\n\n canvas.width = w;\n canvas.height = h;\n context.putImageData(cut, 0, 0);\n return canvas;\n}\n\n// TODO: ломает iphone с огромными изображениями\n/**\n * Rotates `image` around its center on a square canvas (side = max(width, height)), then **crops**\n * transparent margins by trimming to the bounding box of pixels with non-zero alpha.\n * Returns a **new** loaded `HTMLImageElement` (PNG data URL under the hood via {@link renderImage}).\n *\n * `angle` is in **degrees**; converted with {@link degToRad} from `yummies/math`.\n *\n * Very large sources can stress memory on some mobile browsers (known iPhone issues — see TODO in source).\n *\n * @param image - Source image (should be decoded; uses `width` / `height`).\n * @param angle - Rotation in degrees (e.g. `90` for quarter turn).\n * @returns Promise of a new `HTMLImageElement` showing the rotated, cropped result.\n *\n * @example\n * ```ts\n * const upright = await rotateImage(landscapeImg, 90);\n * ```\n *\n * @example\n * ```ts\n * const fixed = await rotateImage(await renderImage(file), -15);\n * ```\n */\nexport const rotateImage = (image: HTMLImageElement, angle: number) => {\n const maxSize = Math.max(image.width, image.height);\n const canvas = document.createElement('canvas');\n canvas.width = maxSize;\n canvas.height = maxSize;\n const context = canvas.getContext('2d')!;\n context.save();\n context.translate(canvas.width / 2, canvas.height / 2);\n context.rotate(degToRad(angle));\n context.drawImage(image, -image.width / 2, -image.height / 2);\n context.restore();\n cropImageFromCanvas(context);\n return renderImage(canvas.toDataURL('image/png'));\n};\n\ninterface DecodedDataUrl {\n mimeType?: string;\n data?: string;\n}\n\n/**\n * Parses a `data:` URL of the form `data:<mime>;base64,<payload>` into its MIME type and raw\n * Base64 body (without the `data:...;base64,` prefix). Non-matching strings yield `undefined`\n * fields in the result object.\n *\n * @param url - Full data URL string.\n * @returns `{ mimeType, data }` — both optional when the regex does not match.\n *\n * @example\n * ```ts\n * const { mimeType, data } = decodeDataUrl('data:image/png;base64,iVBORw0KGgo=');\n * // mimeType === 'image/png', data === 'iVBORw0KGgo='\n * ```\n *\n * @example\n * ```ts\n * decodeDataUrl('not-a-data-url'); // { mimeType: undefined, data: undefined }\n * ```\n */\nexport function decodeDataUrl(url: string): DecodedDataUrl {\n const regex = /^data:(.*);base64,\\s?(.*)$/;\n const matches = new RegExp(regex).exec(url);\n\n return {\n mimeType: matches?.[1],\n data: matches?.[2],\n };\n}\n\n/**\n * Returns `true` if `url` starts with `https://` or `http://` (case-sensitive, as in\n * `String#startsWith`). Does not validate that the rest is a well-formed URL — only the scheme\n * prefix. `blob:`, `data:`, and relative paths return `false`.\n *\n * @param url - String to test (often a user-provided or configured href).\n *\n * @example\n * ```ts\n * isHttpUrl('https://example.com/path'); // true\n * isHttpUrl('http://localhost:3000'); // true\n * ```\n *\n * @example\n * ```ts\n * isHttpUrl('//cdn.example.com'); // false — no explicit http(s) prefix\n * isHttpUrl('/assets/logo.png'); // false — relative path\n * ```\n */\nexport const isHttpUrl = (url: string): boolean => {\n return url.startsWith('https://') || url.startsWith('http://');\n};\n\n/**\n * Returns `true` when `str` is a data URL that {@link decodeDataUrl} can parse **and** the MIME\n * type starts with `image/` (e.g. `image/png`, `image/jpeg`). Requires both a non-empty Base64\n * payload and an image MIME — arbitrary `data:text/plain;base64,...` is `false`.\n *\n * @param str - Candidate string (often `img.src` or API payload).\n *\n * @example\n * ```ts\n * isBase64Image('data:image/png;base64,iVBORw0KGgo='); // true\n * ```\n *\n * @example\n * ```ts\n * isBase64Image('https://example.com/x.png'); // false\n * isBase64Image('data:text/plain;base64,SGk='); // false\n * ```\n */\nexport const isBase64Image = (str: string): boolean => {\n const { mimeType, data } = decodeDataUrl(str);\n return !!(data && mimeType?.startsWith('image/'));\n};\n"],"mappings":"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"}
package/media.d.ts CHANGED
@@ -1,3 +1,19 @@
1
+ /**
2
+ * ---header-docs-section---
3
+ * # yummies/media
4
+ *
5
+ * ## Description
6
+ *
7
+ * Binary and media helpers: Base64 data URLs, object URLs, image orientation fixes, and related
8
+ * browser APIs wrapped in promises. Centralizes `FileReader`/`URL.createObjectURL` usage so image
9
+ * pipelines and uploads stay consistent and easier to test or mock.
10
+ *
11
+ * ## Usage
12
+ *
13
+ * ```ts
14
+ * import { blobToBase64 } from "yummies/media";
15
+ * ```
16
+ */
1
17
  /**
2
18
  * Reads a {@link Blob} as a **data URL** string (`data:<mime>;base64,...`) using {@link FileReader#readAsDataURL}.
3
19
  *