@vielzeug/refine 3.2.0 → 3.3.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 (132) hide show
  1. package/dist/chip.cjs +9 -9
  2. package/dist/chip.cjs.map +1 -1
  3. package/dist/chip.js +38 -35
  4. package/dist/chip.js.map +1 -1
  5. package/dist/command-palette.cjs +5 -5
  6. package/dist/command-palette.cjs.map +1 -1
  7. package/dist/command-palette.js +5 -5
  8. package/dist/command-palette.js.map +1 -1
  9. package/dist/content/card/card.css.cjs +1 -1
  10. package/dist/content/card/card.css.cjs.map +1 -1
  11. package/dist/content/card/card.css.js +1 -1
  12. package/dist/content/card/card.css.js.map +1 -1
  13. package/dist/content/index.d.ts +4 -0
  14. package/dist/content/index.d.ts.map +1 -1
  15. package/dist/content/keyboard-key/keyboard-key.css.cjs +2 -0
  16. package/dist/content/keyboard-key/keyboard-key.css.cjs.map +1 -0
  17. package/dist/content/keyboard-key/keyboard-key.css.js +6 -0
  18. package/dist/content/keyboard-key/keyboard-key.css.js.map +1 -0
  19. package/dist/content/keyboard-key/keyboard-key.d.ts +12 -0
  20. package/dist/content/keyboard-key/keyboard-key.d.ts.map +1 -0
  21. package/dist/content/keyboard-key/keyboard-shortcut.css.cjs +2 -0
  22. package/dist/content/keyboard-key/keyboard-shortcut.css.cjs.map +1 -0
  23. package/dist/content/keyboard-key/keyboard-shortcut.css.js +6 -0
  24. package/dist/content/keyboard-key/keyboard-shortcut.css.js.map +1 -0
  25. package/dist/content/qr-code/qr-code.css.cjs +2 -0
  26. package/dist/content/qr-code/qr-code.css.cjs.map +1 -0
  27. package/dist/content/qr-code/qr-code.css.js +6 -0
  28. package/dist/content/qr-code/qr-code.css.js.map +1 -0
  29. package/dist/content/qr-code/qr-code.d.ts +74 -0
  30. package/dist/content/qr-code/qr-code.d.ts.map +1 -0
  31. package/dist/content/stepper/step.css.cjs +1 -1
  32. package/dist/content/stepper/step.css.cjs.map +1 -1
  33. package/dist/content/stepper/step.css.js +1 -1
  34. package/dist/content/stepper/step.css.js.map +1 -1
  35. package/dist/counter.cjs +91 -0
  36. package/dist/counter.cjs.map +1 -0
  37. package/dist/counter.js +180 -0
  38. package/dist/counter.js.map +1 -0
  39. package/dist/custom-elements.json +3838 -3117
  40. package/dist/disclosure/tabs/tabs.css.cjs +1 -1
  41. package/dist/disclosure/tabs/tabs.css.cjs.map +1 -1
  42. package/dist/disclosure/tabs/tabs.css.js +1 -1
  43. package/dist/disclosure/tabs/tabs.css.js.map +1 -1
  44. package/dist/feedback/chip/chip.css.cjs +1 -1
  45. package/dist/feedback/chip/chip.css.cjs.map +1 -1
  46. package/dist/feedback/chip/chip.css.js +1 -1
  47. package/dist/feedback/chip/chip.css.js.map +1 -1
  48. package/dist/feedback/chip/chip.d.ts +6 -2
  49. package/dist/feedback/chip/chip.d.ts.map +1 -1
  50. package/dist/feedback/progress/progress.css.cjs +1 -1
  51. package/dist/feedback/progress/progress.css.cjs.map +1 -1
  52. package/dist/feedback/progress/progress.css.js +1 -1
  53. package/dist/feedback/progress/progress.css.js.map +1 -1
  54. package/dist/feedback/progress/progress.d.ts +6 -0
  55. package/dist/feedback/progress/progress.d.ts.map +1 -1
  56. package/dist/frameworks/elements.d.ts +1 -1
  57. package/dist/inputs/button/button.css.cjs +1 -1
  58. package/dist/inputs/button/button.css.cjs.map +1 -1
  59. package/dist/inputs/button/button.css.js +1 -1
  60. package/dist/inputs/button/button.css.js.map +1 -1
  61. package/dist/inputs/checkbox/checkbox.css.cjs +1 -1
  62. package/dist/inputs/checkbox/checkbox.css.cjs.map +1 -1
  63. package/dist/inputs/checkbox/checkbox.css.js +1 -1
  64. package/dist/inputs/checkbox/checkbox.css.js.map +1 -1
  65. package/dist/inputs/counter/counter.css.cjs +2 -0
  66. package/dist/inputs/counter/counter.css.cjs.map +1 -0
  67. package/dist/inputs/counter/counter.css.js +6 -0
  68. package/dist/inputs/counter/counter.css.js.map +1 -0
  69. package/dist/inputs/counter/counter.d.ts +100 -0
  70. package/dist/inputs/counter/counter.d.ts.map +1 -0
  71. package/dist/inputs/index.d.ts +4 -0
  72. package/dist/inputs/index.d.ts.map +1 -1
  73. package/dist/inputs/message-composer/message-composer.css.cjs +1 -1
  74. package/dist/inputs/message-composer/message-composer.css.cjs.map +1 -1
  75. package/dist/inputs/message-composer/message-composer.css.js +1 -1
  76. package/dist/inputs/message-composer/message-composer.css.js.map +1 -1
  77. package/dist/inputs/qr-scanner/qr-scanner.css.cjs +2 -0
  78. package/dist/inputs/qr-scanner/qr-scanner.css.cjs.map +1 -0
  79. package/dist/inputs/qr-scanner/qr-scanner.css.js +6 -0
  80. package/dist/inputs/qr-scanner/qr-scanner.css.js.map +1 -0
  81. package/dist/inputs/qr-scanner/qr-scanner.d.ts +87 -0
  82. package/dist/inputs/qr-scanner/qr-scanner.d.ts.map +1 -0
  83. package/dist/inputs/radio/radio.css.cjs +1 -1
  84. package/dist/inputs/radio/radio.css.cjs.map +1 -1
  85. package/dist/inputs/radio/radio.css.js +1 -1
  86. package/dist/inputs/radio/radio.css.js.map +1 -1
  87. package/dist/inputs/switch/switch.css.cjs +1 -1
  88. package/dist/inputs/switch/switch.css.cjs.map +1 -1
  89. package/dist/inputs/switch/switch.css.js +1 -1
  90. package/dist/inputs/switch/switch.css.js.map +1 -1
  91. package/dist/keyboard-key.cjs +9 -0
  92. package/dist/keyboard-key.cjs.map +1 -0
  93. package/dist/keyboard-key.js +36 -0
  94. package/dist/keyboard-key.js.map +1 -0
  95. package/dist/layout/navbar/navbar.css.cjs +1 -1
  96. package/dist/layout/navbar/navbar.css.cjs.map +1 -1
  97. package/dist/layout/navbar/navbar.css.js +1 -1
  98. package/dist/layout/navbar/navbar.css.js.map +1 -1
  99. package/dist/menu.cjs +1 -1
  100. package/dist/menu.cjs.map +1 -1
  101. package/dist/menu.js +2 -1
  102. package/dist/menu.js.map +1 -1
  103. package/dist/overlay/command-palette/command-palette.css.cjs +1 -1
  104. package/dist/overlay/command-palette/command-palette.css.cjs.map +1 -1
  105. package/dist/overlay/command-palette/command-palette.css.js +1 -1
  106. package/dist/overlay/command-palette/command-palette.css.js.map +1 -1
  107. package/dist/overlay/command-palette/command-palette.d.ts +1 -0
  108. package/dist/overlay/command-palette/command-palette.d.ts.map +1 -1
  109. package/dist/progress.cjs +14 -6
  110. package/dist/progress.cjs.map +1 -1
  111. package/dist/progress.js +19 -7
  112. package/dist/progress.js.map +1 -1
  113. package/dist/qr-code.cjs +15 -0
  114. package/dist/qr-code.cjs.map +1 -0
  115. package/dist/qr-code.js +70 -0
  116. package/dist/qr-code.js.map +1 -0
  117. package/dist/qr-scanner.cjs +46 -0
  118. package/dist/qr-scanner.cjs.map +1 -0
  119. package/dist/qr-scanner.js +129 -0
  120. package/dist/qr-scanner.js.map +1 -0
  121. package/dist/refine.cjs +327 -166
  122. package/dist/refine.cjs.map +1 -1
  123. package/dist/refine.iife.js +355 -194
  124. package/dist/refine.iife.js.map +1 -1
  125. package/dist/refine.js +298 -137
  126. package/dist/refine.js.map +1 -1
  127. package/dist/tabs.cjs +2 -2
  128. package/dist/tabs.cjs.map +1 -1
  129. package/dist/tabs.js +37 -34
  130. package/dist/tabs.js.map +1 -1
  131. package/dist/types/elements.d.ts +9 -0
  132. package/package.json +35 -2
package/dist/progress.js CHANGED
@@ -14,6 +14,7 @@ o(p, {
14
14
  indeterminate: l.bool(!1),
15
15
  label: l.string(),
16
16
  max: l.number(100),
17
+ segments: l.number(0),
17
18
  title: l.string(),
18
19
  type: l.oneOf([
19
20
  "linear",
@@ -27,14 +28,17 @@ o(p, {
27
28
  let t = s(), n = d, r = 2 * Math.PI * 45, i = f(() => `${Math.max(0, Math.min(Number(e.value.value), Number(e.max.value))) / Math.max(1, Number(e.max.value)) * 100}%`), a = f(() => {
28
29
  let t = Math.max(0, Math.min(Number(e.value.value), Number(e.max.value))), n = Math.max(1, Number(e.max.value));
29
30
  return r - t / n * r;
30
- }), o = () => e.indeterminate.value ? null : String(e.value.value), l = () => e.label.value ?? e.title.value ?? "Progress", p = u({ "--_circ": `${r}px` }), m = () => e.indeterminate.value ? void 0 : `${r}px`, h = () => e.indeterminate.value ? void 0 : `${a.value}px`, g = f(() => e.type.value === "vertical"), _ = u({
31
+ }), o = () => e.indeterminate.value ? null : String(e.value.value), l = () => e.label.value ?? e.title.value ?? "Progress", p = u({ "--_circ": `${r}px` }), m = () => e.indeterminate.value ? void 0 : `${r}px`, h = () => e.indeterminate.value ? void 0 : `${a.value}px`, g = f(() => e.type.value === "vertical"), _ = f(() => Math.max(0, Math.floor(Number(e.segments.value) || 0))), v = f(() => _.value > 0 && !e.indeterminate.value && !g.value), y = f(() => {
32
+ let t = Math.max(0, Math.min(Number(e.value.value), Number(e.max.value))), n = Math.max(1, Number(e.max.value));
33
+ return Math.round(t / n * _.value);
34
+ }), b = u({ "--_segments": () => String(_.value) }), x = u({
31
35
  height: () => !e.indeterminate.value && g.value ? i.value : null,
32
36
  width: () => !e.indeterminate.value && !g.value ? i.value : null
33
37
  });
34
38
  n(() => {
35
39
  t.style.setProperty("--_percent", e.indeterminate.value ? "0%" : i.value);
36
40
  });
37
- let v = () => c`
41
+ let S = () => c`
38
42
  <div
39
43
  class="circular-track"
40
44
  role="progressbar"
@@ -59,7 +63,7 @@ o(p, {
59
63
  <span class="circular-title">${() => e.title.value ?? ""}</span>
60
64
  </div>
61
65
  </div>
62
- `, y = () => c`
66
+ `, C = () => c`
63
67
  <div class="wrapper">
64
68
  <div class="header">
65
69
  <span class="progress-title">${() => e.title.value ?? ""}</span>
@@ -68,14 +72,22 @@ o(p, {
68
72
  <div class=${() => `bar-row${g.value ? " bar-row-vertical" : ""}`}>
69
73
  <div class="track-outer">
70
74
  <div
71
- class="track"
75
+ class=${() => `track${v.value ? " track-segmented" : ""}`}
72
76
  role="progressbar"
73
77
  aria-valuenow="${o}"
74
78
  aria-valuemin="0"
75
79
  aria-valuemax="${e.max}"
76
80
  aria-label="${l}"
77
- aria-valuetext="${e["value-text"]}">
78
- <div class="fill" part="fill" style="${_}"></div>
81
+ aria-valuetext="${e["value-text"]}"
82
+ style="${b}">
83
+ ${() => v.value ? Array.from({ length: _.value }, (e, t) => c`
84
+ <span
85
+ class="segment"
86
+ part="segment"
87
+ data-filled="${() => t < y.value ? "" : null}"></span>
88
+ `) : c`
89
+ <div class="fill" part="fill" style="${x}"></div>
90
+ `}
79
91
  </div>
80
92
  ${() => g.value ? null : c`
81
93
  <span class="floating-label">${e["floating-label"]}</span>
@@ -86,7 +98,7 @@ o(p, {
86
98
  </div>
87
99
  `;
88
100
  return c`
89
- ${() => e.type.value === "circular" ? v() : y()}
101
+ ${() => e.type.value === "circular" ? S() : C()}
90
102
  `;
91
103
  },
92
104
  styles: [
@@ -1 +1 @@
1
- {"version":3,"file":"progress.js","names":[],"sources":["../src/feedback/progress/progress.ts"],"sourcesContent":["import { define, getHost, html, prop, styleMap, watchEffect } from '@vielzeug/ore';\nimport { computed } from '@vielzeug/ripple';\nimport { sizableBundle, themableBundle } from '../../shared';\nimport { colorThemeMixin, forcedColorsMixin, reducedMotionMixin } from '../../styles';\nimport type { ComponentSize, ThemeColor } from '../../types';\nimport componentStyles from './progress.css?inline';\n\n/** Progress bar component properties */\nexport type OreProgressProps = {\n /** Theme color for the fill bar */\n color?: ThemeColor;\n /** Floating chip centered above the fill endpoint (linear only). Hidden in indeterminate mode. Position formula: left = fill% − half chip width (CSS: left:X%; transform:translateX(−50%)). */\n 'floating-label'?: string;\n /** When true, shows an infinite animation — use when progress is unknown. */\n indeterminate?: boolean;\n /** Accessible name AND visible text label.\n * - Linear without `title`: rendered at the end of the bar.\n * - Linear with `title`: moved into the header row above the bar.\n * - Circular: large text centered inside the ring. */\n label?: string;\n /** Maximum value. Defaults to 100. */\n max?: number;\n /** Size variant controlling bar height */\n size?: ComponentSize;\n /** Title text.\n * - Linear: displayed as a header above the bar; moves `label` into the header row.\n * - Circular: smaller text displayed below the `label` inside the ring. */\n title?: string;\n /** 'linear' (default), 'circular', or 'vertical' */\n type?: 'linear' | 'circular' | 'vertical';\n /** Current progress value (0 to `max`). Ignored when `indeterminate`. */\n value?: number;\n /** Human-readable value text for screen readers (e.g. \"Step 2 of 5\", \"75%\"). Overrides the raw aria-valuenow when set. */\n 'value-text'?: string;\n};\n\n/**\n * A linear progress bar for conveying operation progress.\n * Supports determinate (known value) and indeterminate (unknown duration) modes.\n *\n * @element ore-progress\n *\n * @attr {number} value - Current value (0–max). Defaults to 0.\n * @attr {number} max - Maximum value. Defaults to 100.\n * @attr {boolean} indeterminate - Show infinite animation (ignores value/max).\n * @attr {string} color - Theme color: 'primary' | 'success' | 'warning' | 'error' | …\n * @attr {string} size - Bar height/width: 'sm' | 'md' | 'lg'\n * @attr {string} type - 'linear' (default) | 'circular' | 'vertical'\n *\n * @cssprop --progress-width - Bar width override (vertical only)\n * @attr {string} label - Visible text label + accessible name. Linear: at bar end (or header row with title). Circular: large text centered inside the ring.\n * @attr {string} title - Title text. Linear: header above the bar (moves label to header row). Circular: smaller text below the label inside the ring.\n * @attr {string} floating-label - Floating chip centered above the fill endpoint (linear only); hidden when indeterminate.\n *\n * @cssprop --progress-height - Bar height override\n * @cssprop --progress-track-bg - Track background color\n * @cssprop --progress-fill - Fill bar color\n * @cssprop --progress-radius - Border radius\n * @cssprop --progress-label-gap - Gap between header/bar row and between bar and trailing label (default 0.25 rem)\n * @cssprop --progress-title-color - Title text color (defaults to currentColor)\n * @cssprop --progress-label-color - Label text color (defaults to currentColor)\n * @cssprop --progress-circle-size - Circular indicator diameter (default 6rem)\n * @cssprop --progress-circular-label-size - Font size of the label inside the ring (default --text-xl)\n * @cssprop --progress-circular-title-size - Font size of the title inside the ring (default --text-xs)\n *\n * @part fill - Progress fill element.\n * @example\n * ```html\n * <ore-progress value=\"45\"></ore-progress>\n * <ore-progress value=\"75\" max=\"100\" color=\"success\" size=\"lg\"></ore-progress>\n * <ore-progress indeterminate color=\"primary\" label=\"Loading…\"></ore-progress>\n * ```\n */\nexport const PROGRESS_TAG = 'ore-progress' as const;\ndefine<OreProgressProps>(PROGRESS_TAG, {\n props: {\n ...themableBundle,\n ...sizableBundle,\n 'floating-label': prop.string(),\n indeterminate: prop.bool(false),\n label: prop.string(),\n max: prop.number(100),\n title: prop.string(),\n type: prop.oneOf(['linear', 'circular', 'vertical'] as const, 'linear'),\n value: prop.number(0),\n 'value-text': prop.string(),\n },\n\n setup(props) {\n const el = getHost();\n const watch = watchEffect;\n\n // The SVG circle circumference for a radius of 45 (viewBox 0 0 100 100)\n const RADIUS = 45;\n const CIRC = 2 * Math.PI * RADIUS; // ~282.7\n\n const percent = computed(() => {\n const v = Math.max(0, Math.min(Number(props.value.value), Number(props.max.value)));\n const m = Math.max(1, Number(props.max.value));\n\n return `${(v / m) * 100}%`;\n });\n\n const dashoffset = computed(() => {\n const v = Math.max(0, Math.min(Number(props.value.value), Number(props.max.value)));\n const m = Math.max(1, Number(props.max.value));\n\n return CIRC - (v / m) * CIRC;\n });\n\n const ariaValueNow = () => (props.indeterminate.value ? null : String(props.value.value));\n const ariaLabel = () => props.label.value ?? props.title.value ?? 'Progress';\n const circularStyle = styleMap({ '--_circ': `${CIRC}px` });\n const strokeDasharray = () => (props.indeterminate.value ? undefined : `${CIRC}px`);\n const strokeDashoffset = () => (props.indeterminate.value ? undefined : `${dashoffset.value}px`);\n const isVertical = computed(() => props.type.value === 'vertical');\n const linearFillStyle = styleMap({\n height: () => (!props.indeterminate.value && isVertical.value ? percent.value : null),\n width: () => (!props.indeterminate.value && !isVertical.value ? percent.value : null),\n });\n\n watch(() => {\n el.style.setProperty('--_percent', props.indeterminate.value ? '0%' : percent.value);\n });\n\n const circularTemplate = () => html`\n <div\n class=\"circular-track\"\n role=\"progressbar\"\n aria-valuenow=\"${ariaValueNow}\"\n aria-valuemin=\"0\"\n aria-valuemax=\"${props.max}\"\n aria-label=\"${ariaLabel}\"\n aria-valuetext=\"${props['value-text']}\"\n style=\"${circularStyle}\">\n <svg viewBox=\"0 0 100 100\" xmlns=\"http://www.w3.org/2000/svg\">\n <circle class=\"circle-bg\" cx=\"50\" cy=\"50\" r=\"${RADIUS}\"></circle>\n <circle\n class=\"circle-fill\"\n cx=\"50\"\n cy=\"50\"\n r=\"${RADIUS}\"\n stroke-dasharray=\"${strokeDasharray}\"\n stroke-dashoffset=\"${strokeDashoffset}\"></circle>\n </svg>\n <div class=\"circular-inner\">\n <span class=\"circular-label\">${() => props.label.value ?? ''}</span>\n <span class=\"circular-title\">${() => props.title.value ?? ''}</span>\n </div>\n </div>\n `;\n\n const barTemplate = () => html`\n <div class=\"wrapper\">\n <div class=\"header\">\n <span class=\"progress-title\">${() => props.title.value ?? ''}</span>\n <span class=\"end-label header-label\">${() => props.label.value ?? ''}</span>\n </div>\n <div class=${() => `bar-row${isVertical.value ? ' bar-row-vertical' : ''}`}>\n <div class=\"track-outer\">\n <div\n class=\"track\"\n role=\"progressbar\"\n aria-valuenow=\"${ariaValueNow}\"\n aria-valuemin=\"0\"\n aria-valuemax=\"${props.max}\"\n aria-label=\"${ariaLabel}\"\n aria-valuetext=\"${props['value-text']}\">\n <div class=\"fill\" part=\"fill\" style=\"${linearFillStyle}\"></div>\n </div>\n ${() =>\n !isVertical.value\n ? html`\n <span class=\"floating-label\">${props['floating-label']}</span>\n `\n : null}\n </div>\n <span class=\"end-label row-label\">${() => props.label.value ?? ''}</span>\n </div>\n </div>\n `;\n\n return html`\n ${() => (props.type.value === 'circular' ? circularTemplate() : barTemplate())}\n `;\n },\n\n styles: [colorThemeMixin, forcedColorsMixin, reducedMotionMixin, componentStyles],\n});\n"],"mappings":";;;;;;;AAyEA,IAAa,IAAe;AAC5B,EAAyB,GAAc;CACrC,OAAO;EACL,GAAG;EACH,GAAG;EACH,kBAAkB,EAAK,OAAO;EAC9B,eAAe,EAAK,KAAK,EAAK;EAC9B,OAAO,EAAK,OAAO;EACnB,KAAK,EAAK,OAAO,GAAG;EACpB,OAAO,EAAK,OAAO;EACnB,MAAM,EAAK,MAAM;GAAC;GAAU;GAAY;EAAU,GAAY,QAAQ;EACtE,OAAO,EAAK,OAAO,CAAC;EACpB,cAAc,EAAK,OAAO;CAC5B;CAEA,MAAM,GAAO;EACX,IAAM,IAAK,EAAQ,GACb,IAAQ,GAIR,IAAO,IAAI,KAAK,KAAK,IAErB,IAAU,QAIP,GAHG,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,EAAM,MAAM,KAAK,GAAG,OAAO,EAAM,IAAI,KAAK,CAAC,CAGtE,IAFD,KAAK,IAAI,GAAG,OAAO,EAAM,IAAI,KAAK,CAE7B,IAAK,IAAI,EACzB,GAEK,IAAa,QAAe;GAChC,IAAM,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,EAAM,MAAM,KAAK,GAAG,OAAO,EAAM,IAAI,KAAK,CAAC,CAAC,GAC5E,IAAI,KAAK,IAAI,GAAG,OAAO,EAAM,IAAI,KAAK,CAAC;GAE7C,OAAO,IAAQ,IAAI,IAAK;EAC1B,CAAC,GAEK,UAAsB,EAAM,cAAc,QAAQ,OAAO,OAAO,EAAM,MAAM,KAAK,GACjF,UAAkB,EAAM,MAAM,SAAS,EAAM,MAAM,SAAS,YAC5D,IAAgB,EAAS,EAAE,WAAW,GAAG,EAAK,IAAI,CAAC,GACnD,UAAyB,EAAM,cAAc,QAAQ,KAAA,IAAY,GAAG,EAAK,KACzE,UAA0B,EAAM,cAAc,QAAQ,KAAA,IAAY,GAAG,EAAW,MAAM,KACtF,IAAa,QAAe,EAAM,KAAK,UAAU,UAAU,GAC3D,IAAkB,EAAS;GAC/B,cAAe,CAAC,EAAM,cAAc,SAAS,EAAW,QAAQ,EAAQ,QAAQ;GAChF,aAAc,CAAC,EAAM,cAAc,SAAS,CAAC,EAAW,QAAQ,EAAQ,QAAQ;EAClF,CAAC;EAED,QAAY;GACV,EAAG,MAAM,YAAY,cAAc,EAAM,cAAc,QAAQ,OAAO,EAAQ,KAAK;EACrF,CAAC;EAED,IAAM,UAAyB,CAAI;;;;yBAId,EAAa;;yBAEb,EAAM,IAAI;sBACb,EAAU;0BACN,EAAM,cAAc;iBAC7B,EAAc;;yDAE0B,GAAO;;;;;iBAK/C,GAAO;gCACQ,EAAgB;iCACf,EAAiB;;;+CAGH,EAAM,MAAM,SAAS,GAAG;+CACxB,EAAM,MAAM,SAAS,GAAG;;;OAK7D,UAAoB,CAAI;;;+CAGa,EAAM,MAAM,SAAS,GAAG;uDAChB,EAAM,MAAM,SAAS,GAAG;;2BAEpD,UAAU,EAAW,QAAQ,sBAAsB,KAAK;;;;;+BAKpD,EAAa;;+BAEb,EAAM,IAAI;4BACb,EAAU;gCACN,EAAM,cAAc;qDACC,EAAgB;;oBAGtD,EAAW,QAIR,OAHA,CAAI;mDAC6B,EAAM,kBAAkB;oBAEpD;;oDAE6B,EAAM,MAAM,SAAS,GAAG;;;;EAKxE,OAAO,CAAI;cACA,EAAM,KAAK,UAAU,aAAa,EAAiB,IAAI,EAAY,EAAG;;CAEnF;CAEA,QAAQ;EAAC;EAAiB;EAAmB;EAAoB;CAAe;AAClF,CAAC"}
1
+ {"version":3,"file":"progress.js","names":[],"sources":["../src/feedback/progress/progress.ts"],"sourcesContent":["import { define, getHost, html, prop, styleMap, watchEffect } from '@vielzeug/ore';\nimport { computed } from '@vielzeug/ripple';\nimport { sizableBundle, themableBundle } from '../../shared';\nimport { colorThemeMixin, forcedColorsMixin, reducedMotionMixin } from '../../styles';\nimport type { ComponentSize, ThemeColor } from '../../types';\nimport componentStyles from './progress.css?inline';\n\n/** Progress bar component properties */\nexport type OreProgressProps = {\n /** Theme color for the fill bar */\n color?: ThemeColor;\n /** Floating chip centered above the fill endpoint (linear only). Hidden in indeterminate mode. Position formula: left = fill% − half chip width (CSS: left:X%; transform:translateX(−50%)). */\n 'floating-label'?: string;\n /** When true, shows an infinite animation — use when progress is unknown. */\n indeterminate?: boolean;\n /** Accessible name AND visible text label.\n * - Linear without `title`: rendered at the end of the bar.\n * - Linear with `title`: moved into the header row above the bar.\n * - Circular: large text centered inside the ring. */\n label?: string;\n /** Maximum value. Defaults to 100. */\n max?: number;\n /** Split the linear bar into this many discrete blocks; blocks up to `value / max` are filled. Ignored when `indeterminate` or circular. */\n segments?: number;\n /** Size variant controlling bar height */\n size?: ComponentSize;\n /** Title text.\n * - Linear: displayed as a header above the bar; moves `label` into the header row.\n * - Circular: smaller text displayed below the `label` inside the ring. */\n title?: string;\n /** 'linear' (default), 'circular', or 'vertical' */\n type?: 'linear' | 'circular' | 'vertical';\n /** Current progress value (0 to `max`). Ignored when `indeterminate`. */\n value?: number;\n /** Human-readable value text for screen readers (e.g. \"Step 2 of 5\", \"75%\"). Overrides the raw aria-valuenow when set. */\n 'value-text'?: string;\n};\n\n/**\n * A linear progress bar for conveying operation progress.\n * Supports determinate (known value) and indeterminate (unknown duration) modes.\n *\n * @element ore-progress\n *\n * @attr {number} value - Current value (0–max). Defaults to 0.\n * @attr {number} max - Maximum value. Defaults to 100.\n * @attr {boolean} indeterminate - Show infinite animation (ignores value/max).\n * @attr {string} color - Theme color: 'primary' | 'success' | 'warning' | 'error' | …\n * @attr {string} size - Bar height/width: 'sm' | 'md' | 'lg'\n * @attr {string} type - 'linear' (default) | 'circular' | 'vertical'\n * @attr {number} segments - Split the linear bar into discrete blocks (tracks, steps, stages)\n *\n * @cssprop --progress-width - Bar width override (vertical only)\n * @attr {string} label - Visible text label + accessible name. Linear: at bar end (or header row with title). Circular: large text centered inside the ring.\n * @attr {string} title - Title text. Linear: header above the bar (moves label to header row). Circular: smaller text below the label inside the ring.\n * @attr {string} floating-label - Floating chip centered above the fill endpoint (linear only); hidden when indeterminate.\n *\n * @cssprop --progress-height - Bar height override\n * @cssprop --progress-track-bg - Track background color\n * @cssprop --progress-fill - Fill bar color\n * @cssprop --progress-radius - Border radius\n * @cssprop --progress-label-gap - Gap between header/bar row and between bar and trailing label (default 0.25 rem)\n * @cssprop --progress-title-color - Title text color (defaults to currentColor)\n * @cssprop --progress-label-color - Label text color (defaults to currentColor)\n * @cssprop --progress-circle-size - Circular indicator diameter (default 6rem)\n * @cssprop --progress-circular-label-size - Font size of the label inside the ring (default --text-xl)\n * @cssprop --progress-circular-title-size - Font size of the title inside the ring (default --text-xs)\n * @cssprop --progress-segment-gap - Gap between blocks when `segments` is set (default --size-1)\n *\n * @part fill - Progress fill element.\n * @part segment - One block when `segments` is set; carries `data-filled` once reached.\n * @example\n * ```html\n * <ore-progress value=\"45\"></ore-progress>\n * <ore-progress value=\"75\" max=\"100\" color=\"success\" size=\"lg\"></ore-progress>\n * <ore-progress indeterminate color=\"primary\" label=\"Loading…\"></ore-progress>\n * <ore-progress value=\"2\" max=\"6\" segments=\"6\" label=\"Struggle\"></ore-progress>\n * ```\n */\nexport const PROGRESS_TAG = 'ore-progress' as const;\ndefine<OreProgressProps>(PROGRESS_TAG, {\n props: {\n ...themableBundle,\n ...sizableBundle,\n 'floating-label': prop.string(),\n indeterminate: prop.bool(false),\n label: prop.string(),\n max: prop.number(100),\n segments: prop.number(0),\n title: prop.string(),\n type: prop.oneOf(['linear', 'circular', 'vertical'] as const, 'linear'),\n value: prop.number(0),\n 'value-text': prop.string(),\n },\n\n setup(props) {\n const el = getHost();\n const watch = watchEffect;\n\n // The SVG circle circumference for a radius of 45 (viewBox 0 0 100 100)\n const RADIUS = 45;\n const CIRC = 2 * Math.PI * RADIUS; // ~282.7\n\n const percent = computed(() => {\n const v = Math.max(0, Math.min(Number(props.value.value), Number(props.max.value)));\n const m = Math.max(1, Number(props.max.value));\n\n return `${(v / m) * 100}%`;\n });\n\n const dashoffset = computed(() => {\n const v = Math.max(0, Math.min(Number(props.value.value), Number(props.max.value)));\n const m = Math.max(1, Number(props.max.value));\n\n return CIRC - (v / m) * CIRC;\n });\n\n const ariaValueNow = () => (props.indeterminate.value ? null : String(props.value.value));\n const ariaLabel = () => props.label.value ?? props.title.value ?? 'Progress';\n const circularStyle = styleMap({ '--_circ': `${CIRC}px` });\n const strokeDasharray = () => (props.indeterminate.value ? undefined : `${CIRC}px`);\n const strokeDashoffset = () => (props.indeterminate.value ? undefined : `${dashoffset.value}px`);\n const isVertical = computed(() => props.type.value === 'vertical');\n const segmentCount = computed(() => Math.max(0, Math.floor(Number(props.segments.value) || 0)));\n const isSegmented = computed(() => segmentCount.value > 0 && !props.indeterminate.value && !isVertical.value);\n const filledSegments = computed(() => {\n const v = Math.max(0, Math.min(Number(props.value.value), Number(props.max.value)));\n const m = Math.max(1, Number(props.max.value));\n\n return Math.round((v / m) * segmentCount.value);\n });\n const segmentStyle = styleMap({ '--_segments': () => String(segmentCount.value) });\n const linearFillStyle = styleMap({\n height: () => (!props.indeterminate.value && isVertical.value ? percent.value : null),\n width: () => (!props.indeterminate.value && !isVertical.value ? percent.value : null),\n });\n\n watch(() => {\n el.style.setProperty('--_percent', props.indeterminate.value ? '0%' : percent.value);\n });\n\n const circularTemplate = () => html`\n <div\n class=\"circular-track\"\n role=\"progressbar\"\n aria-valuenow=\"${ariaValueNow}\"\n aria-valuemin=\"0\"\n aria-valuemax=\"${props.max}\"\n aria-label=\"${ariaLabel}\"\n aria-valuetext=\"${props['value-text']}\"\n style=\"${circularStyle}\">\n <svg viewBox=\"0 0 100 100\" xmlns=\"http://www.w3.org/2000/svg\">\n <circle class=\"circle-bg\" cx=\"50\" cy=\"50\" r=\"${RADIUS}\"></circle>\n <circle\n class=\"circle-fill\"\n cx=\"50\"\n cy=\"50\"\n r=\"${RADIUS}\"\n stroke-dasharray=\"${strokeDasharray}\"\n stroke-dashoffset=\"${strokeDashoffset}\"></circle>\n </svg>\n <div class=\"circular-inner\">\n <span class=\"circular-label\">${() => props.label.value ?? ''}</span>\n <span class=\"circular-title\">${() => props.title.value ?? ''}</span>\n </div>\n </div>\n `;\n\n const barTemplate = () => html`\n <div class=\"wrapper\">\n <div class=\"header\">\n <span class=\"progress-title\">${() => props.title.value ?? ''}</span>\n <span class=\"end-label header-label\">${() => props.label.value ?? ''}</span>\n </div>\n <div class=${() => `bar-row${isVertical.value ? ' bar-row-vertical' : ''}`}>\n <div class=\"track-outer\">\n <div\n class=${() => `track${isSegmented.value ? ' track-segmented' : ''}`}\n role=\"progressbar\"\n aria-valuenow=\"${ariaValueNow}\"\n aria-valuemin=\"0\"\n aria-valuemax=\"${props.max}\"\n aria-label=\"${ariaLabel}\"\n aria-valuetext=\"${props['value-text']}\"\n style=\"${segmentStyle}\">\n ${() =>\n isSegmented.value\n ? Array.from(\n { length: segmentCount.value },\n (_, i) => html`\n <span\n class=\"segment\"\n part=\"segment\"\n data-filled=\"${() => (i < filledSegments.value ? '' : null)}\"></span>\n `,\n )\n : html`\n <div class=\"fill\" part=\"fill\" style=\"${linearFillStyle}\"></div>\n `}\n </div>\n ${() =>\n !isVertical.value\n ? html`\n <span class=\"floating-label\">${props['floating-label']}</span>\n `\n : null}\n </div>\n <span class=\"end-label row-label\">${() => props.label.value ?? ''}</span>\n </div>\n </div>\n `;\n\n return html`\n ${() => (props.type.value === 'circular' ? circularTemplate() : barTemplate())}\n `;\n },\n\n styles: [colorThemeMixin, forcedColorsMixin, reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./styles/mixins/shape.css.cjs"),t=require("./shared/prop-bundles.cjs"),n=require("./content/qr-code/qr-code.css.cjs");let r=require("@vielzeug/ore"),i=require("@vielzeug/ripple"),a=require("@vielzeug/sigil");var o={lg:256,md:192,sm:128},s=e=>typeof e==`number`?e:o[e]??o.md,c=`ore-qr-code`;(0,r.define)(c,{props:{"error-correction":r.prop.string(`M`),label:r.prop.string(`QR code`),margin:r.prop.number(4),...t.roundableBundle,size:{default:`md`,parse:e=>e!==null&&/^\d+$/.test(e)?Number(e):e??`md`},value:r.prop.string(``),variant:r.prop.string(`flat`)},setup(e){let t=(0,r.useEmit)(),n=(0,r.useSlots)(),o=(0,i.computed)(()=>{let t=e.value.value??``;if(!t)return{error:new a.SigilError(`No value to encode`)};try{let n=(0,a.encodeQr)(t,{errorCorrection:e[`error-correction`].value??`M`});return{svg:(0,a.toSvg)(n,{dark:`var(--_dark)`,label:e.label.value??`QR code`,light:`var(--_light)`,margin:e.margin.value??4}),version:n.version}}catch(e){return{error:e instanceof a.SigilError?e:new a.SigilError(`QR encoding failed`,{cause:e})}}}),c=(0,i.computed)(()=>`error`in o.value),l=(0,i.computed)(()=>n.has(`caption`).value);return(0,i.watch)(o,n=>{`error`in n?t(`error`,{error:n.error}):t(`render`,{size:s(e.size.value??`md`),version:n.version})}),r.html`
2
+ <div class="wrapper" part="wrapper" style=${()=>`--_size-px: ${s(e.size.value??`md`)}px`}>
3
+ ${(0,r.when)(()=>c.value,()=>r.html`
4
+ <div class="error" part="error" role="alert">
5
+ <span class="error-text">${()=>`error`in o.value?o.value.error.message:``}</span>
6
+ </div>
7
+ `,()=>r.html`
8
+ <span class="svg" part="svg">${(0,r.unsafeHtml)(()=>`error`in o.value?``:o.value.svg)}</span>
9
+ `)}
10
+ <span class="caption" part="caption" ?hidden=${()=>!l.value}>
11
+ <slot name="caption"></slot>
12
+ </span>
13
+ </div>
14
+ `},styles:[e.roundedVariantMixin,n.default]}),exports.QR_CODE_TAG=c;
15
+ //# sourceMappingURL=qr-code.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qr-code.cjs","names":[],"sources":["../src/content/qr-code/qr-code.ts"],"sourcesContent":["import { define, html, prop, unsafeHtml, useEmit, useSlots, when } from '@vielzeug/ore';\nimport { computed, watch } from '@vielzeug/ripple';\nimport { encodeQr, type QrErrorCorrection, SigilError, toSvg } from '@vielzeug/sigil';\nimport { roundableBundle } from '../../shared';\nimport { roundedVariantMixin } from '../../styles';\nimport type { ComponentSize, RoundedSize } from '../../types';\nimport componentStyles from './qr-code.css?inline';\n\n/** QR code component properties */\nexport type OreQrCodeProps = {\n /** Error-correction level */\n 'error-correction'?: QrErrorCorrection;\n /** Accessible label for the rendered code */\n label?: string;\n /** Quiet-zone width in modules */\n margin?: number;\n /** Border radius size */\n rounded?: RoundedSize;\n /** Rendered size: named scale or pixel number */\n size?: ComponentSize | number;\n /** The payload to encode */\n value?: string;\n /** Surface variant: 'flat' (transparent) or 'card' (themed background) */\n variant?: 'flat' | 'card';\n};\n\n/** QR code component events */\nexport type OreQrCodeEvents = {\n /** Emitted whenever the code is (re)rendered. detail: `{ version, size }` */\n render: { version: number; size: number };\n /** Emitted when encoding fails (e.g. payload exceeds capacity). detail: `{ error }` */\n error: { error: SigilError };\n};\n\nconst SIZES: Record<ComponentSize, number> = { lg: 256, md: 192, sm: 128 };\n\nconst cssSize = (size: ComponentSize | number): number => (typeof size === 'number' ? size : (SIZES[size] ?? SIZES.md));\n\n/**\n * Renders a QR code for `value` as inline SVG — themeable via `currentColor`,\n * crisp at any scale, no canvas. On capacity errors the component shows an\n * error state and emits `error` rather than throwing.\n *\n * @element ore-qr-code\n *\n * @attr {string} value - The payload to encode\n * @attr {string} error-correction - 'L' | 'M' | 'Q' | 'H' (default 'M')\n * @attr {number} margin - Quiet-zone modules (default 4)\n * @attr {string} size - 'sm' | 'md' | 'lg', or a pixel value\n * @attr {string} label - Accessible label (default \"QR code\")\n * @attr {string} rounded - Border radius for the card variant\n *\n * @fires render - detail: `{ version, size }` after each successful render\n * @fires error - detail: `{ error }` when encoding fails\n *\n * @slot caption - Optional caption rendered below the code\n *\n * @cssprop --qr-code-dark - Module color (default `currentColor`)\n * @cssprop --qr-code-light - Background color (default `transparent`)\n * @cssprop --qr-code-size - Rendered size in px (overrides `size`)\n * @cssprop --qr-code-radius - Border radius\n * @cssprop --qr-code-padding - Inner padding\n * @cssprop --qr-code-bg - Card background (variant=\"card\")\n * @cssprop --qr-code-border - Card border width (variant=\"card\")\n * @cssprop --qr-code-border-color - Card border color (variant=\"card\")\n * @cssprop --qr-code-shadow - Card box shadow (variant=\"card\")\n * @cssprop --qr-code-error-color - Error-state text color\n *\n * @part wrapper - Outer container\n * @part svg - The rendered `<svg>` element\n * @part caption - Caption slot container\n * @part error - Error-state container\n *\n * @example\n * ```html\n * <ore-qr-code value=\"https://example.com\"></ore-qr-code>\n * <ore-qr-code value=\"otpauth://…\" error-correction=\"H\" size=\"256\"></ore-qr-code>\n * ```\n */\nexport const QR_CODE_TAG = 'ore-qr-code' as const;\n\ndefine<OreQrCodeProps>(QR_CODE_TAG, {\n props: {\n 'error-correction': prop.string<QrErrorCorrection>('M'),\n label: prop.string('QR code'),\n margin: prop.number(4),\n ...roundableBundle,\n size: {\n default: 'md' as ComponentSize | number,\n parse: (v) => (v !== null && /^\\d+$/.test(v) ? Number(v) : ((v ?? 'md') as ComponentSize)),\n },\n value: prop.string(''),\n variant: prop.string<'flat' | 'card'>('flat'),\n },\n\n setup(props) {\n const emit = useEmit<OreQrCodeEvents>();\n const slots = useSlots();\n\n // Encode → SVG string in one computed; SigilError flips the error state.\n const encoded = computed<{ svg: string; version: number } | { error: SigilError }>(() => {\n const value = props.value.value ?? '';\n if (!value) return { error: new SigilError('No value to encode') };\n try {\n const matrix = encodeQr(value, { errorCorrection: props['error-correction'].value ?? 'M' });\n const svg = toSvg(matrix, {\n dark: 'var(--_dark)',\n label: props.label.value ?? 'QR code',\n light: 'var(--_light)',\n margin: props.margin.value ?? 4,\n });\n return { svg, version: matrix.version };\n } catch (error) {\n return { error: error instanceof SigilError ? error : new SigilError('QR encoding failed', { cause: error }) };\n }\n });\n\n const failed = computed(() => 'error' in encoded.value);\n const hasCaption = computed(() => slots.has('caption').value);\n\n // Emit once per encode outcome, not per reactive read.\n watch(encoded, (result) => {\n if ('error' in result) emit('error', { error: result.error });\n else emit('render', { size: cssSize(props.size.value ?? 'md'), version: result.version });\n });\n\n return html`\n <div class=\"wrapper\" part=\"wrapper\" style=${() => `--_size-px: ${cssSize(props.size.value ?? 'md')}px`}>\n ${when(\n () => failed.value,\n () => html`\n <div class=\"error\" part=\"error\" role=\"alert\">\n <span class=\"error-text\">${() => ('error' in encoded.value ? encoded.value.error.message : '')}</span>\n </div>\n `,\n () => html`\n <span class=\"svg\" part=\"svg\">${unsafeHtml(() => ('error' in encoded.value ? '' : encoded.value.svg))}</span>\n `,\n )}\n <span class=\"caption\" part=\"caption\" ?hidden=${() => !hasCaption.value}>\n <slot name=\"caption\"></slot>\n </span>\n </div>\n `;\n },\n styles: [roundedVariantMixin, componentStyles],\n});\n"],"mappings":"oSAkCA,IAAM,EAAuC,CAAE,GAAI,IAAK,GAAI,IAAK,GAAI,GAAI,EAEnE,EAAW,GAA0C,OAAO,GAAS,SAAW,EAAQ,EAAM,IAAS,EAAM,GA2CtG,EAAc,eAE3B,EAAA,EAAA,OAAA,CAAuB,EAAa,CAClC,MAAO,CACL,mBAAoB,EAAA,KAAK,OAA0B,GAAG,EACtD,MAAO,EAAA,KAAK,OAAO,SAAS,EAC5B,OAAQ,EAAA,KAAK,OAAO,CAAC,EACrB,GAAG,EAAA,gBACH,KAAM,CACJ,QAAS,KACT,MAAQ,GAAO,IAAM,MAAQ,QAAQ,KAAK,CAAC,EAAI,OAAO,CAAC,EAAM,GAAK,IACpE,EACA,MAAO,EAAA,KAAK,OAAO,EAAE,EACrB,QAAS,EAAA,KAAK,OAAwB,MAAM,CAC9C,EAEA,MAAM,EAAO,CACX,IAAM,GAAA,EAAO,EAAA,QAAA,CAAyB,EAChC,GAAA,EAAQ,EAAA,SAAA,CAAS,EAGjB,GAAA,EAAU,EAAA,SAAA,KAAyE,CACvF,IAAM,EAAQ,EAAM,MAAM,OAAS,GACnC,GAAI,CAAC,EAAO,MAAO,CAAE,MAAO,IAAI,EAAA,WAAW,oBAAoB,CAAE,EACjE,GAAI,CACF,IAAM,GAAA,EAAS,EAAA,SAAA,CAAS,EAAO,CAAE,gBAAiB,EAAM,mBAAmB,CAAC,OAAS,GAAI,CAAC,EAO1F,MAAO,CAAE,KAAA,EANG,EAAA,MAAA,CAAM,EAAQ,CACxB,KAAM,eACN,MAAO,EAAM,MAAM,OAAS,UAC5B,MAAO,gBACP,OAAQ,EAAM,OAAO,OAAS,CAChC,CACS,EAAK,QAAS,EAAO,OAAQ,CACxC,OAAS,EAAO,CACd,MAAO,CAAE,MAAO,aAAiB,EAAA,WAAa,EAAQ,IAAI,EAAA,WAAW,qBAAsB,CAAE,MAAO,CAAM,CAAC,CAAE,CAC/G,CACF,CAAC,EAEK,GAAA,EAAS,EAAA,SAAA,KAAe,UAAW,EAAQ,KAAK,EAChD,GAAA,EAAa,EAAA,SAAA,KAAe,EAAM,IAAI,SAAS,CAAC,CAAC,KAAK,EAQ5D,OALA,EAAA,EAAA,MAAA,CAAM,EAAU,GAAW,CACrB,UAAW,EAAQ,EAAK,QAAS,CAAE,MAAO,EAAO,KAAM,CAAC,EACvD,EAAK,SAAU,CAAE,KAAM,EAAQ,EAAM,KAAK,OAAS,IAAI,EAAG,QAAS,EAAO,OAAQ,CAAC,CAC1F,CAAC,EAEM,EAAA,IAAI;sDACyC,eAAe,EAAQ,EAAM,KAAK,OAAS,IAAI,EAAE,IAAI;WACnG,EAAA,EAAA,KAAA,KACM,EAAO,UACP,EAAA,IAAI;;6CAE4B,UAAW,EAAQ,MAAQ,EAAQ,MAAM,MAAM,QAAU,GAAI;;gBAG7F,EAAA,IAAI;4CACuB,EAAA,EAAA,WAAA,KAAkB,UAAW,EAAQ,MAAQ,GAAK,EAAQ,MAAM,GAAI,EAAE;WAEzG,EAAE;2DACmD,CAAC,EAAW,MAAM;;;;KAK7E,EACA,OAAQ,CAAC,EAAA,oBAAqB,EAAA,OAAe,CAC/C,CAAC"}
@@ -0,0 +1,70 @@
1
+ import { roundedVariantMixin as e } from "./styles/mixins/shape.css.js";
2
+ import { roundableBundle as t } from "./shared/prop-bundles.js";
3
+ import n from "./content/qr-code/qr-code.css.js";
4
+ import { define as r, html as i, prop as a, unsafeHtml as o, useEmit as s, useSlots as c, when as l } from "@vielzeug/ore";
5
+ import { computed as u, watch as d } from "@vielzeug/ripple";
6
+ import { SigilError as f, encodeQr as p, toSvg as m } from "@vielzeug/sigil";
7
+ //#region src/content/qr-code/qr-code.ts
8
+ var h = {
9
+ lg: 256,
10
+ md: 192,
11
+ sm: 128
12
+ }, g = (e) => typeof e == "number" ? e : h[e] ?? h.md, _ = "ore-qr-code";
13
+ r(_, {
14
+ props: {
15
+ "error-correction": a.string("M"),
16
+ label: a.string("QR code"),
17
+ margin: a.number(4),
18
+ ...t,
19
+ size: {
20
+ default: "md",
21
+ parse: (e) => e !== null && /^\d+$/.test(e) ? Number(e) : e ?? "md"
22
+ },
23
+ value: a.string(""),
24
+ variant: a.string("flat")
25
+ },
26
+ setup(e) {
27
+ let t = s(), n = c(), r = u(() => {
28
+ let t = e.value.value ?? "";
29
+ if (!t) return { error: new f("No value to encode") };
30
+ try {
31
+ let n = p(t, { errorCorrection: e["error-correction"].value ?? "M" });
32
+ return {
33
+ svg: m(n, {
34
+ dark: "var(--_dark)",
35
+ label: e.label.value ?? "QR code",
36
+ light: "var(--_light)",
37
+ margin: e.margin.value ?? 4
38
+ }),
39
+ version: n.version
40
+ };
41
+ } catch (e) {
42
+ return { error: e instanceof f ? e : new f("QR encoding failed", { cause: e }) };
43
+ }
44
+ }), a = u(() => "error" in r.value), h = u(() => n.has("caption").value);
45
+ return d(r, (n) => {
46
+ "error" in n ? t("error", { error: n.error }) : t("render", {
47
+ size: g(e.size.value ?? "md"),
48
+ version: n.version
49
+ });
50
+ }), i`
51
+ <div class="wrapper" part="wrapper" style=${() => `--_size-px: ${g(e.size.value ?? "md")}px`}>
52
+ ${l(() => a.value, () => i`
53
+ <div class="error" part="error" role="alert">
54
+ <span class="error-text">${() => "error" in r.value ? r.value.error.message : ""}</span>
55
+ </div>
56
+ `, () => i`
57
+ <span class="svg" part="svg">${o(() => "error" in r.value ? "" : r.value.svg)}</span>
58
+ `)}
59
+ <span class="caption" part="caption" ?hidden=${() => !h.value}>
60
+ <slot name="caption"></slot>
61
+ </span>
62
+ </div>
63
+ `;
64
+ },
65
+ styles: [e, n]
66
+ });
67
+ //#endregion
68
+ export { _ as QR_CODE_TAG };
69
+
70
+ //# sourceMappingURL=qr-code.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qr-code.js","names":[],"sources":["../src/content/qr-code/qr-code.ts"],"sourcesContent":["import { define, html, prop, unsafeHtml, useEmit, useSlots, when } from '@vielzeug/ore';\nimport { computed, watch } from '@vielzeug/ripple';\nimport { encodeQr, type QrErrorCorrection, SigilError, toSvg } from '@vielzeug/sigil';\nimport { roundableBundle } from '../../shared';\nimport { roundedVariantMixin } from '../../styles';\nimport type { ComponentSize, RoundedSize } from '../../types';\nimport componentStyles from './qr-code.css?inline';\n\n/** QR code component properties */\nexport type OreQrCodeProps = {\n /** Error-correction level */\n 'error-correction'?: QrErrorCorrection;\n /** Accessible label for the rendered code */\n label?: string;\n /** Quiet-zone width in modules */\n margin?: number;\n /** Border radius size */\n rounded?: RoundedSize;\n /** Rendered size: named scale or pixel number */\n size?: ComponentSize | number;\n /** The payload to encode */\n value?: string;\n /** Surface variant: 'flat' (transparent) or 'card' (themed background) */\n variant?: 'flat' | 'card';\n};\n\n/** QR code component events */\nexport type OreQrCodeEvents = {\n /** Emitted whenever the code is (re)rendered. detail: `{ version, size }` */\n render: { version: number; size: number };\n /** Emitted when encoding fails (e.g. payload exceeds capacity). detail: `{ error }` */\n error: { error: SigilError };\n};\n\nconst SIZES: Record<ComponentSize, number> = { lg: 256, md: 192, sm: 128 };\n\nconst cssSize = (size: ComponentSize | number): number => (typeof size === 'number' ? size : (SIZES[size] ?? SIZES.md));\n\n/**\n * Renders a QR code for `value` as inline SVG — themeable via `currentColor`,\n * crisp at any scale, no canvas. On capacity errors the component shows an\n * error state and emits `error` rather than throwing.\n *\n * @element ore-qr-code\n *\n * @attr {string} value - The payload to encode\n * @attr {string} error-correction - 'L' | 'M' | 'Q' | 'H' (default 'M')\n * @attr {number} margin - Quiet-zone modules (default 4)\n * @attr {string} size - 'sm' | 'md' | 'lg', or a pixel value\n * @attr {string} label - Accessible label (default \"QR code\")\n * @attr {string} rounded - Border radius for the card variant\n *\n * @fires render - detail: `{ version, size }` after each successful render\n * @fires error - detail: `{ error }` when encoding fails\n *\n * @slot caption - Optional caption rendered below the code\n *\n * @cssprop --qr-code-dark - Module color (default `currentColor`)\n * @cssprop --qr-code-light - Background color (default `transparent`)\n * @cssprop --qr-code-size - Rendered size in px (overrides `size`)\n * @cssprop --qr-code-radius - Border radius\n * @cssprop --qr-code-padding - Inner padding\n * @cssprop --qr-code-bg - Card background (variant=\"card\")\n * @cssprop --qr-code-border - Card border width (variant=\"card\")\n * @cssprop --qr-code-border-color - Card border color (variant=\"card\")\n * @cssprop --qr-code-shadow - Card box shadow (variant=\"card\")\n * @cssprop --qr-code-error-color - Error-state text color\n *\n * @part wrapper - Outer container\n * @part svg - The rendered `<svg>` element\n * @part caption - Caption slot container\n * @part error - Error-state container\n *\n * @example\n * ```html\n * <ore-qr-code value=\"https://example.com\"></ore-qr-code>\n * <ore-qr-code value=\"otpauth://…\" error-correction=\"H\" size=\"256\"></ore-qr-code>\n * ```\n */\nexport const QR_CODE_TAG = 'ore-qr-code' as const;\n\ndefine<OreQrCodeProps>(QR_CODE_TAG, {\n props: {\n 'error-correction': prop.string<QrErrorCorrection>('M'),\n label: prop.string('QR code'),\n margin: prop.number(4),\n ...roundableBundle,\n size: {\n default: 'md' as ComponentSize | number,\n parse: (v) => (v !== null && /^\\d+$/.test(v) ? Number(v) : ((v ?? 'md') as ComponentSize)),\n },\n value: prop.string(''),\n variant: prop.string<'flat' | 'card'>('flat'),\n },\n\n setup(props) {\n const emit = useEmit<OreQrCodeEvents>();\n const slots = useSlots();\n\n // Encode → SVG string in one computed; SigilError flips the error state.\n const encoded = computed<{ svg: string; version: number } | { error: SigilError }>(() => {\n const value = props.value.value ?? '';\n if (!value) return { error: new SigilError('No value to encode') };\n try {\n const matrix = encodeQr(value, { errorCorrection: props['error-correction'].value ?? 'M' });\n const svg = toSvg(matrix, {\n dark: 'var(--_dark)',\n label: props.label.value ?? 'QR code',\n light: 'var(--_light)',\n margin: props.margin.value ?? 4,\n });\n return { svg, version: matrix.version };\n } catch (error) {\n return { error: error instanceof SigilError ? error : new SigilError('QR encoding failed', { cause: error }) };\n }\n });\n\n const failed = computed(() => 'error' in encoded.value);\n const hasCaption = computed(() => slots.has('caption').value);\n\n // Emit once per encode outcome, not per reactive read.\n watch(encoded, (result) => {\n if ('error' in result) emit('error', { error: result.error });\n else emit('render', { size: cssSize(props.size.value ?? 'md'), version: result.version });\n });\n\n return html`\n <div class=\"wrapper\" part=\"wrapper\" style=${() => `--_size-px: ${cssSize(props.size.value ?? 'md')}px`}>\n ${when(\n () => failed.value,\n () => html`\n <div class=\"error\" part=\"error\" role=\"alert\">\n <span class=\"error-text\">${() => ('error' in encoded.value ? encoded.value.error.message : '')}</span>\n </div>\n `,\n () => html`\n <span class=\"svg\" part=\"svg\">${unsafeHtml(() => ('error' in encoded.value ? '' : encoded.value.svg))}</span>\n `,\n )}\n <span class=\"caption\" part=\"caption\" ?hidden=${() => !hasCaption.value}>\n <slot name=\"caption\"></slot>\n </span>\n </div>\n `;\n },\n styles: [roundedVariantMixin, componentStyles],\n});\n"],"mappings":";;;;;;;AAkCA,IAAM,IAAuC;CAAE,IAAI;CAAK,IAAI;CAAK,IAAI;AAAI,GAEnE,KAAW,MAA0C,OAAO,KAAS,WAAW,IAAQ,EAAM,MAAS,EAAM,IA2CtG,IAAc;AAE3B,EAAuB,GAAa;CAClC,OAAO;EACL,oBAAoB,EAAK,OAA0B,GAAG;EACtD,OAAO,EAAK,OAAO,SAAS;EAC5B,QAAQ,EAAK,OAAO,CAAC;EACrB,GAAG;EACH,MAAM;GACJ,SAAS;GACT,QAAQ,MAAO,MAAM,QAAQ,QAAQ,KAAK,CAAC,IAAI,OAAO,CAAC,IAAM,KAAK;EACpE;EACA,OAAO,EAAK,OAAO,EAAE;EACrB,SAAS,EAAK,OAAwB,MAAM;CAC9C;CAEA,MAAM,GAAO;EACX,IAAM,IAAO,EAAyB,GAChC,IAAQ,EAAS,GAGjB,IAAU,QAAyE;GACvF,IAAM,IAAQ,EAAM,MAAM,SAAS;GACnC,IAAI,CAAC,GAAO,OAAO,EAAE,OAAO,IAAI,EAAW,oBAAoB,EAAE;GACjE,IAAI;IACF,IAAM,IAAS,EAAS,GAAO,EAAE,iBAAiB,EAAM,mBAAmB,CAAC,SAAS,IAAI,CAAC;IAO1F,OAAO;KAAE,KANG,EAAM,GAAQ;MACxB,MAAM;MACN,OAAO,EAAM,MAAM,SAAS;MAC5B,OAAO;MACP,QAAQ,EAAM,OAAO,SAAS;KAChC,CACS;KAAK,SAAS,EAAO;IAAQ;GACxC,SAAS,GAAO;IACd,OAAO,EAAE,OAAO,aAAiB,IAAa,IAAQ,IAAI,EAAW,sBAAsB,EAAE,OAAO,EAAM,CAAC,EAAE;GAC/G;EACF,CAAC,GAEK,IAAS,QAAe,WAAW,EAAQ,KAAK,GAChD,IAAa,QAAe,EAAM,IAAI,SAAS,CAAC,CAAC,KAAK;EAQ5D,OALA,EAAM,IAAU,MAAW;GACzB,AAAI,WAAW,IAAQ,EAAK,SAAS,EAAE,OAAO,EAAO,MAAM,CAAC,IACvD,EAAK,UAAU;IAAE,MAAM,EAAQ,EAAM,KAAK,SAAS,IAAI;IAAG,SAAS,EAAO;GAAQ,CAAC;EAC1F,CAAC,GAEM,CAAI;wDACyC,eAAe,EAAQ,EAAM,KAAK,SAAS,IAAI,EAAE,IAAI;UACnG,QACM,EAAO,aACP,CAAI;;+CAE4B,WAAW,EAAQ,QAAQ,EAAQ,MAAM,MAAM,UAAU,GAAI;;mBAG7F,CAAI;2CACuB,QAAkB,WAAW,EAAQ,QAAQ,KAAK,EAAQ,MAAM,GAAI,EAAE;WAEzG,EAAE;6DACmD,CAAC,EAAW,MAAM;;;;;CAK7E;CACA,QAAQ,CAAC,GAAqB,CAAe;AAC/C,CAAC"}
@@ -0,0 +1,46 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./styles/mixins/shape.css.cjs"),t=require("./shared/prop-bundles.cjs"),n=require("./inputs/qr-scanner/qr-scanner.css.cjs");let r=require("@vielzeug/ore"),i=require("@vielzeug/ripple"),a=require("@vielzeug/sigil");var o={lg:360,md:280,sm:200},s=e=>typeof e==`number`?e:o[e]??o.md,c={denied:`Camera access denied. Allow camera permission and retry.`,error:`Scanning failed.`,idle:`Camera is off.`,result:`Code scanned.`,scanning:`Point the camera at a QR code.`,starting:`Starting camera…`,unsupported:`QR scanning is not supported in this browser.`},l=`ore-qr-scanner`;(0,r.define)(l,{props:{active:r.prop.bool(!1),"facing-mode":r.prop.oneOf([`environment`,`user`],`environment`),interval:r.prop.number(200),label:r.prop.string(`QR code scanner`),once:r.prop.bool(!0),...t.roundableBundle,scannerFactory:r.prop.data(),size:{default:`md`,parse:e=>e!==null&&/^\d+$/.test(e)?Number(e):e??`md`}},setup(e){let t=(0,r.useEmit)(),n=(0,r.ref)(),o=(0,i.signal)(`idle`),l=(0,i.signal)(``),u=(0,i.signal)(``),d=(0,i.signal)(!1);(0,i.watch)(e.active,e=>{d.value=e===!0},{immediate:!0});let f=(e,n=``)=>{l.value=n,o.value!==e&&(o.value=e,t(`status`,{status:e}))},p=()=>o.value===`result`&&u.value?`Code scanned: ${u.value}`:c[o.value];!e.scannerFactory.value&&!(0,a.isQrScanSupported)()&&f(`unsupported`,c.unsupported);let m=async e=>{try{await e.start(),d.value||e.stop()}catch(e){d.value=!1;let n=e instanceof a.SigilError?e:new a.SigilError(`QR scanner failed to start`,{cause:e});n instanceof a.SigilUnsupportedError?f(`unsupported`,n.message):n instanceof a.SigilPermissionError?f(`denied`,n.message):f(`error`,n.message),t(`error`,{error:n})}},h=n=>{n.onResult(n=>{u.value=n.value,t(`scan`,{value:n.value}),e.once.value&&f(`result`)}),n.tap(e=>{e.type===`status-change`?e.status===`starting`?f(`starting`):e.status===`scanning`?f(`scanning`):(e.status===`stopped`&&o.value!==`result`||e.status===`idle`)&&f(`idle`):e.type===`error`&&(t(`error`,{error:e.error}),(o.value===`scanning`||o.value===`starting`)&&f(`error`,e.error.message))})};(0,r.onElement)(n,t=>{let n=(0,i.signal)(null);(0,r.watchEffect)(()=>{let r=(e.scannerFactory.value??a.createQrScanner)({constraints:{facingMode:e[`facing-mode`].value??`environment`},intervalMs:e.interval.value??200,once:e.once.value??!0,video:t});return n.value=r,h(r),()=>r.dispose()}),(0,i.watch)(()=>[d.value,n.value],([e,t])=>{t&&!t.disposed&&(e?m(t):t.stop())},{immediate:!0})});let g=()=>{d.value=!0},_=()=>{d.value=!1},v=()=>o.value===`starting`||o.value===`scanning`;return r.html`
2
+ <div
3
+ class="wrapper"
4
+ part="wrapper"
5
+ role="region"
6
+ aria-label=${()=>e.label.value}
7
+ style=${()=>`--_size-px: ${s(e.size.value??`md`)}px`}>
8
+ <div class="stage" part="stage">
9
+ <video
10
+ ref=${n}
11
+ part="video"
12
+ autoplay
13
+ playsinline
14
+ muted
15
+ aria-hidden="true"
16
+ ?hidden=${()=>!v()}></video>
17
+ <div class="viewfinder" part="viewfinder" aria-hidden="true" ?hidden=${()=>o.value!==`scanning`}></div>
18
+ <div class="overlay"><slot name="overlay"></slot></div>
19
+ ${(0,r.when)(()=>!v(),()=>r.html`
20
+ <div class="state" part="state" data-state=${()=>o.value}>
21
+ ${(0,r.when)(()=>o.value===`unsupported`,()=>r.html`<slot name="unsupported"></slot>`,()=>r.html`
22
+ ${(0,r.when)(()=>o.value===`result`,()=>r.html`<ore-icon name="check" size="24" aria-hidden="true"></ore-icon>`,()=>r.html`<ore-icon name="camera" size="24" aria-hidden="true"></ore-icon>`)}
23
+ <span class="state-text">${()=>l.value||c[o.value]}</span>
24
+ `)}
25
+ </div>
26
+ `)}
27
+ </div>
28
+ <div class="status" part="status" role="status">${p}</div>
29
+ ${(0,r.when)(()=>o.value!==`unsupported`,()=>r.html`
30
+ <div class="controls" part="controls">
31
+ ${(0,r.when)(()=>o.value===`starting`||o.value===`scanning`,()=>r.html`
32
+ <ore-button variant="ghost" size="sm" @click=${_}>Stop</ore-button>
33
+ `,()=>r.html`
34
+ <ore-button
35
+ variant="ghost"
36
+ size="sm"
37
+ @click=${g}>
38
+ ${()=>o.value===`result`?`Scan again`:o.value===`denied`||o.value===`error`?`Retry`:`Start camera`}
39
+ </ore-button>
40
+ `)}
41
+ </div>
42
+ `)}
43
+ <slot name="footer"></slot>
44
+ </div>
45
+ `},styles:[e.roundedVariantMixin,n.default]}),exports.QR_SCANNER_TAG=l;
46
+ //# sourceMappingURL=qr-scanner.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qr-scanner.cjs","names":[],"sources":["../src/inputs/qr-scanner/qr-scanner.ts"],"sourcesContent":["import { define, html, onElement, prop, ref, useEmit, watchEffect, when } from '@vielzeug/ore';\nimport { signal, watch } from '@vielzeug/ripple';\nimport {\n createQrScanner,\n isQrScanSupported,\n type QrScanner,\n type QrScannerOptions,\n type QrScanResult,\n SigilError,\n SigilPermissionError,\n SigilUnsupportedError,\n} from '@vielzeug/sigil';\nimport { roundableBundle } from '../../shared';\nimport { roundedVariantMixin } from '../../styles';\nimport type { ComponentSize, RoundedSize } from '../../types';\nimport '../../content/icon/icon';\nimport '../button/button';\nimport componentStyles from './qr-scanner.css?inline';\n\n/** Testing hook — inject a fake `createQrScanner` without touching globals. */\nexport type QrScannerFactory = (options: QrScannerOptions) => QrScanner;\n\n/** Component-level view states; sigil's transport states are mapped onto these. */\nexport type OreQrScannerStatus = 'idle' | 'starting' | 'scanning' | 'result' | 'unsupported' | 'denied' | 'error';\n\n/** QR scanner component properties */\nexport type OreQrScannerProps = {\n /** Start/stop the camera externally; mirrors into internal intent on change */\n active?: boolean;\n /** Preferred camera: 'environment' (rear) or 'user' (front) */\n 'facing-mode'?: 'environment' | 'user';\n /** Minimum ms between detection passes */\n interval?: number;\n /** Accessible label for the scanner region */\n label?: string;\n /** Stop after the first successful scan (default true) */\n once?: boolean;\n /** Border radius size */\n rounded?: RoundedSize;\n /** JS-only testing hook: custom scanner factory (defaults to `createQrScanner`) */\n scannerFactory?: QrScannerFactory;\n /** Preview size: named scale or pixel number */\n size?: ComponentSize | number;\n};\n\n/** QR scanner component events */\nexport type OreQrScannerEvents = {\n /** Emitted on every successful decode. detail: `{ value }` */\n scan: { value: string };\n /** Emitted on permission, support, or detection failure. detail: `{ error }` */\n error: { error: SigilError };\n /** Emitted on every view-state transition. detail: `{ status }` */\n status: { status: OreQrScannerStatus };\n};\n\nconst SIZES: Record<ComponentSize, number> = { lg: 360, md: 280, sm: 200 };\n\nconst cssSize = (size: ComponentSize | number): number => (typeof size === 'number' ? size : (SIZES[size] ?? SIZES.md));\n\nconst STATUS_TEXT: Record<OreQrScannerStatus, string> = {\n denied: 'Camera access denied. Allow camera permission and retry.',\n error: 'Scanning failed.',\n idle: 'Camera is off.',\n result: 'Code scanned.',\n scanning: 'Point the camera at a QR code.',\n starting: 'Starting camera…',\n unsupported: 'QR scanning is not supported in this browser.',\n};\n\n/**\n * Camera QR scanner built on `createQrScanner` (native `BarcodeDetector`).\n * `active` starts/stops the camera externally; internal Start/Stop controls\n * drive the same intent. Unsupported browsers render the `unsupported` slot\n * so apps can substitute a paste fallback.\n *\n * @element ore-qr-scanner\n *\n * @attr {boolean} active - Start/stop the camera\n * @attr {string} facing-mode - 'environment' | 'user' (default 'environment')\n * @attr {number} interval - Ms between detection passes (default 200)\n * @attr {boolean} once - Stop after first scan (default true)\n * @attr {string} size - 'sm' | 'md' | 'lg', or a pixel value\n * @attr {string} label - Region label (default \"QR code scanner\")\n * @attr {string} rounded - Border radius\n *\n * @fires scan - detail: `{ value }` on each decode\n * @fires error - detail: `{ error }` on permission/support/detection failure\n * @fires status - detail: `{ status }` on view-state transitions\n *\n * @slot overlay - Content layered over the camera preview\n * @slot unsupported - Fallback UI when scanning is unsupported (e.g. paste input)\n * @slot footer - Extra content below the status row\n *\n * @cssprop --qr-scanner-size - Preview edge length in px (overrides `size`)\n * @cssprop --qr-scanner-radius - Border radius\n * @cssprop --qr-scanner-bg - Preview background while idle\n * @cssprop --qr-scanner-viewfinder-color - Viewfinder frame color\n * @cssprop --qr-scanner-status-color - Status text color\n *\n * @part wrapper - Outer container\n * @part stage - Square preview area (video + overlays)\n * @part video - The `<video>` preview element\n * @part viewfinder - Decorative scan frame over the preview\n * @part state - Idle/error/denied/unsupported/result panel\n * @part status - Live status line\n * @part controls - Button row\n *\n * @example\n * ```html\n * <ore-qr-scanner active @scan=${(e) => pair(e.detail.value)}></ore-qr-scanner>\n * ```\n */\nexport const QR_SCANNER_TAG = 'ore-qr-scanner' as const;\n\ndefine<OreQrScannerProps>(QR_SCANNER_TAG, {\n props: {\n active: prop.bool(false),\n 'facing-mode': prop.oneOf(['environment', 'user'] as const, 'environment'),\n interval: prop.number(200),\n label: prop.string('QR code scanner'),\n once: prop.bool(true),\n ...roundableBundle,\n scannerFactory: prop.data<QrScannerFactory>(),\n size: {\n default: 'md' as ComponentSize | number,\n parse: (v) => (v !== null && /^\\d+$/.test(v) ? Number(v) : ((v ?? 'md') as ComponentSize)),\n },\n },\n\n setup(props) {\n const emit = useEmit<OreQrScannerEvents>();\n const videoRef = ref<HTMLVideoElement>();\n\n const status = signal<OreQrScannerStatus>('idle');\n const failure = signal('');\n const lastValue = signal('');\n\n // `active` is an imperative hint mirrored into internal intent; the\n // internal Start/Stop buttons drive the same signal.\n const requested = signal(false);\n watch(\n props.active,\n (active) => {\n requested.value = active === true;\n },\n { immediate: true },\n );\n\n const setStatus = (next: OreQrScannerStatus, message = ''): void => {\n failure.value = message;\n if (status.value === next) return;\n status.value = next;\n emit('status', { status: next });\n };\n\n const statusText = (): string =>\n status.value === 'result' && lastValue.value ? `Code scanned: ${lastValue.value}` : STATUS_TEXT[status.value];\n\n // Eager support check so VitePress/jsdom (no BarcodeDetector) land on the\n // unsupported state immediately; an injected factory skips the gate.\n if (!props.scannerFactory.value && !isQrScanSupported()) {\n setStatus('unsupported', STATUS_TEXT.unsupported);\n }\n\n const startScanner = async (scanner: QrScanner): Promise<void> => {\n try {\n await scanner.start();\n // Intent may have been withdrawn while getUserMedia was pending.\n if (!requested.value) scanner.stop();\n } catch (error) {\n requested.value = false;\n const sigilError =\n error instanceof SigilError ? error : new SigilError('QR scanner failed to start', { cause: error });\n if (sigilError instanceof SigilUnsupportedError) setStatus('unsupported', sigilError.message);\n else if (sigilError instanceof SigilPermissionError) setStatus('denied', sigilError.message);\n else setStatus('error', sigilError.message);\n emit('error', { error: sigilError });\n }\n };\n\n const wire = (scanner: QrScanner): void => {\n scanner.onResult((result: QrScanResult) => {\n lastValue.value = result.value;\n emit('scan', { value: result.value });\n if (props.once.value) setStatus('result');\n });\n scanner.tap((event) => {\n if (event.type === 'status-change') {\n if (event.status === 'starting') setStatus('starting');\n else if (event.status === 'scanning') setStatus('scanning');\n // 'stopped' after a once-result must not clobber the result view.\n else if (event.status === 'stopped' && status.value !== 'result') setStatus('idle');\n else if (event.status === 'idle') setStatus('idle');\n } else if (event.type === 'error') {\n emit('error', { error: event.error });\n if (status.value === 'scanning' || status.value === 'starting') setStatus('error', event.error.message);\n }\n });\n };\n\n onElement(videoRef, (video) => {\n const current = signal<QrScanner | null>(null);\n\n // Rebuild when scan-affecting props change; cleanup disposes the previous scanner.\n watchEffect(() => {\n const factory = props.scannerFactory.value ?? createQrScanner;\n const scanner = factory({\n constraints: { facingMode: props['facing-mode'].value ?? 'environment' },\n intervalMs: props.interval.value ?? 200,\n once: props.once.value ?? true,\n video,\n });\n current.value = scanner;\n wire(scanner);\n return () => scanner.dispose();\n });\n\n // Drive start/stop from intent + the current scanner instance. `watch`\n // (untracked callback) is required over `watchEffect`: `scanner.stop()`\n // synchronously re-enters the tap handler, and tracking its `status`\n // reads would retrigger this effect on every transition — an infinite loop.\n watch(\n () => [requested.value, current.value] as const,\n ([want, scanner]) => {\n if (!scanner || scanner.disposed) return;\n if (want) void startScanner(scanner);\n else scanner.stop();\n },\n { immediate: true },\n );\n });\n\n const resume = (): void => {\n requested.value = true;\n };\n const pause = (): void => {\n requested.value = false;\n };\n\n const showVideo = (): boolean => status.value === 'starting' || status.value === 'scanning';\n const showControls = (): boolean => status.value !== 'unsupported';\n\n return html`\n <div\n class=\"wrapper\"\n part=\"wrapper\"\n role=\"region\"\n aria-label=${() => props.label.value}\n style=${() => `--_size-px: ${cssSize(props.size.value ?? 'md')}px`}>\n <div class=\"stage\" part=\"stage\">\n <video\n ref=${videoRef}\n part=\"video\"\n autoplay\n playsinline\n muted\n aria-hidden=\"true\"\n ?hidden=${() => !showVideo()}></video>\n <div class=\"viewfinder\" part=\"viewfinder\" aria-hidden=\"true\" ?hidden=${() => status.value !== 'scanning'}></div>\n <div class=\"overlay\"><slot name=\"overlay\"></slot></div>\n ${when(\n () => !showVideo(),\n () => html`\n <div class=\"state\" part=\"state\" data-state=${() => status.value}>\n ${when(\n () => status.value === 'unsupported',\n () => html`<slot name=\"unsupported\"></slot>`,\n () => html`\n ${when(\n () => status.value === 'result',\n () => html`<ore-icon name=\"check\" size=\"24\" aria-hidden=\"true\"></ore-icon>`,\n () => html`<ore-icon name=\"camera\" size=\"24\" aria-hidden=\"true\"></ore-icon>`,\n )}\n <span class=\"state-text\">${() => failure.value || STATUS_TEXT[status.value]}</span>\n `,\n )}\n </div>\n `,\n )}\n </div>\n <div class=\"status\" part=\"status\" role=\"status\">${statusText}</div>\n ${when(\n showControls,\n () => html`\n <div class=\"controls\" part=\"controls\">\n ${when(\n () => status.value === 'starting' || status.value === 'scanning',\n () => html`\n <ore-button variant=\"ghost\" size=\"sm\" @click=${pause}>Stop</ore-button>\n `,\n () => html`\n <ore-button\n variant=\"ghost\"\n size=\"sm\"\n @click=${resume}>\n ${() =>\n status.value === 'result'\n ? 'Scan again'\n : status.value === 'denied' || status.value === 'error'\n ? 'Retry'\n : 'Start camera'}\n </ore-button>\n `,\n )}\n </div>\n `,\n )}\n <slot name=\"footer\"></slot>\n </div>\n `;\n },\n styles: [roundedVariantMixin, componentStyles],\n});\n"],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ import { roundedVariantMixin as e } from "./styles/mixins/shape.css.js";
2
+ import { roundableBundle as t } from "./shared/prop-bundles.js";
3
+ import n from "./inputs/qr-scanner/qr-scanner.css.js";
4
+ import { define as r, html as i, onElement as a, prop as o, ref as s, useEmit as c, watchEffect as l, when as u } from "@vielzeug/ore";
5
+ import { signal as d, watch as f } from "@vielzeug/ripple";
6
+ import { SigilError as p, SigilPermissionError as m, SigilUnsupportedError as h, createQrScanner as g, isQrScanSupported as _ } from "@vielzeug/sigil";
7
+ //#region src/inputs/qr-scanner/qr-scanner.ts
8
+ var v = {
9
+ lg: 360,
10
+ md: 280,
11
+ sm: 200
12
+ }, y = (e) => typeof e == "number" ? e : v[e] ?? v.md, b = {
13
+ denied: "Camera access denied. Allow camera permission and retry.",
14
+ error: "Scanning failed.",
15
+ idle: "Camera is off.",
16
+ result: "Code scanned.",
17
+ scanning: "Point the camera at a QR code.",
18
+ starting: "Starting camera…",
19
+ unsupported: "QR scanning is not supported in this browser."
20
+ }, x = "ore-qr-scanner";
21
+ r(x, {
22
+ props: {
23
+ active: o.bool(!1),
24
+ "facing-mode": o.oneOf(["environment", "user"], "environment"),
25
+ interval: o.number(200),
26
+ label: o.string("QR code scanner"),
27
+ once: o.bool(!0),
28
+ ...t,
29
+ scannerFactory: o.data(),
30
+ size: {
31
+ default: "md",
32
+ parse: (e) => e !== null && /^\d+$/.test(e) ? Number(e) : e ?? "md"
33
+ }
34
+ },
35
+ setup(e) {
36
+ let t = c(), n = s(), r = d("idle"), o = d(""), v = d(""), x = d(!1);
37
+ f(e.active, (e) => {
38
+ x.value = e === !0;
39
+ }, { immediate: !0 });
40
+ let S = (e, n = "") => {
41
+ o.value = n, r.value !== e && (r.value = e, t("status", { status: e }));
42
+ }, C = () => r.value === "result" && v.value ? `Code scanned: ${v.value}` : b[r.value];
43
+ !e.scannerFactory.value && !_() && S("unsupported", b.unsupported);
44
+ let w = async (e) => {
45
+ try {
46
+ await e.start(), x.value || e.stop();
47
+ } catch (e) {
48
+ x.value = !1;
49
+ let n = e instanceof p ? e : new p("QR scanner failed to start", { cause: e });
50
+ n instanceof h ? S("unsupported", n.message) : n instanceof m ? S("denied", n.message) : S("error", n.message), t("error", { error: n });
51
+ }
52
+ }, T = (n) => {
53
+ n.onResult((n) => {
54
+ v.value = n.value, t("scan", { value: n.value }), e.once.value && S("result");
55
+ }), n.tap((e) => {
56
+ e.type === "status-change" ? e.status === "starting" ? S("starting") : e.status === "scanning" ? S("scanning") : (e.status === "stopped" && r.value !== "result" || e.status === "idle") && S("idle") : e.type === "error" && (t("error", { error: e.error }), (r.value === "scanning" || r.value === "starting") && S("error", e.error.message));
57
+ });
58
+ };
59
+ a(n, (t) => {
60
+ let n = d(null);
61
+ l(() => {
62
+ let r = (e.scannerFactory.value ?? g)({
63
+ constraints: { facingMode: e["facing-mode"].value ?? "environment" },
64
+ intervalMs: e.interval.value ?? 200,
65
+ once: e.once.value ?? !0,
66
+ video: t
67
+ });
68
+ return n.value = r, T(r), () => r.dispose();
69
+ }), f(() => [x.value, n.value], ([e, t]) => {
70
+ t && !t.disposed && (e ? w(t) : t.stop());
71
+ }, { immediate: !0 });
72
+ });
73
+ let E = () => {
74
+ x.value = !0;
75
+ }, D = () => {
76
+ x.value = !1;
77
+ }, O = () => r.value === "starting" || r.value === "scanning";
78
+ return i`
79
+ <div
80
+ class="wrapper"
81
+ part="wrapper"
82
+ role="region"
83
+ aria-label=${() => e.label.value}
84
+ style=${() => `--_size-px: ${y(e.size.value ?? "md")}px`}>
85
+ <div class="stage" part="stage">
86
+ <video
87
+ ref=${n}
88
+ part="video"
89
+ autoplay
90
+ playsinline
91
+ muted
92
+ aria-hidden="true"
93
+ ?hidden=${() => !O()}></video>
94
+ <div class="viewfinder" part="viewfinder" aria-hidden="true" ?hidden=${() => r.value !== "scanning"}></div>
95
+ <div class="overlay"><slot name="overlay"></slot></div>
96
+ ${u(() => !O(), () => i`
97
+ <div class="state" part="state" data-state=${() => r.value}>
98
+ ${u(() => r.value === "unsupported", () => i`<slot name="unsupported"></slot>`, () => i`
99
+ ${u(() => r.value === "result", () => i`<ore-icon name="check" size="24" aria-hidden="true"></ore-icon>`, () => i`<ore-icon name="camera" size="24" aria-hidden="true"></ore-icon>`)}
100
+ <span class="state-text">${() => o.value || b[r.value]}</span>
101
+ `)}
102
+ </div>
103
+ `)}
104
+ </div>
105
+ <div class="status" part="status" role="status">${C}</div>
106
+ ${u(() => r.value !== "unsupported", () => i`
107
+ <div class="controls" part="controls">
108
+ ${u(() => r.value === "starting" || r.value === "scanning", () => i`
109
+ <ore-button variant="ghost" size="sm" @click=${D}>Stop</ore-button>
110
+ `, () => i`
111
+ <ore-button
112
+ variant="ghost"
113
+ size="sm"
114
+ @click=${E}>
115
+ ${() => r.value === "result" ? "Scan again" : r.value === "denied" || r.value === "error" ? "Retry" : "Start camera"}
116
+ </ore-button>
117
+ `)}
118
+ </div>
119
+ `)}
120
+ <slot name="footer"></slot>
121
+ </div>
122
+ `;
123
+ },
124
+ styles: [e, n]
125
+ });
126
+ //#endregion
127
+ export { x as QR_SCANNER_TAG };
128
+
129
+ //# sourceMappingURL=qr-scanner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qr-scanner.js","names":[],"sources":["../src/inputs/qr-scanner/qr-scanner.ts"],"sourcesContent":["import { define, html, onElement, prop, ref, useEmit, watchEffect, when } from '@vielzeug/ore';\nimport { signal, watch } from '@vielzeug/ripple';\nimport {\n createQrScanner,\n isQrScanSupported,\n type QrScanner,\n type QrScannerOptions,\n type QrScanResult,\n SigilError,\n SigilPermissionError,\n SigilUnsupportedError,\n} from '@vielzeug/sigil';\nimport { roundableBundle } from '../../shared';\nimport { roundedVariantMixin } from '../../styles';\nimport type { ComponentSize, RoundedSize } from '../../types';\nimport '../../content/icon/icon';\nimport '../button/button';\nimport componentStyles from './qr-scanner.css?inline';\n\n/** Testing hook — inject a fake `createQrScanner` without touching globals. */\nexport type QrScannerFactory = (options: QrScannerOptions) => QrScanner;\n\n/** Component-level view states; sigil's transport states are mapped onto these. */\nexport type OreQrScannerStatus = 'idle' | 'starting' | 'scanning' | 'result' | 'unsupported' | 'denied' | 'error';\n\n/** QR scanner component properties */\nexport type OreQrScannerProps = {\n /** Start/stop the camera externally; mirrors into internal intent on change */\n active?: boolean;\n /** Preferred camera: 'environment' (rear) or 'user' (front) */\n 'facing-mode'?: 'environment' | 'user';\n /** Minimum ms between detection passes */\n interval?: number;\n /** Accessible label for the scanner region */\n label?: string;\n /** Stop after the first successful scan (default true) */\n once?: boolean;\n /** Border radius size */\n rounded?: RoundedSize;\n /** JS-only testing hook: custom scanner factory (defaults to `createQrScanner`) */\n scannerFactory?: QrScannerFactory;\n /** Preview size: named scale or pixel number */\n size?: ComponentSize | number;\n};\n\n/** QR scanner component events */\nexport type OreQrScannerEvents = {\n /** Emitted on every successful decode. detail: `{ value }` */\n scan: { value: string };\n /** Emitted on permission, support, or detection failure. detail: `{ error }` */\n error: { error: SigilError };\n /** Emitted on every view-state transition. detail: `{ status }` */\n status: { status: OreQrScannerStatus };\n};\n\nconst SIZES: Record<ComponentSize, number> = { lg: 360, md: 280, sm: 200 };\n\nconst cssSize = (size: ComponentSize | number): number => (typeof size === 'number' ? size : (SIZES[size] ?? SIZES.md));\n\nconst STATUS_TEXT: Record<OreQrScannerStatus, string> = {\n denied: 'Camera access denied. Allow camera permission and retry.',\n error: 'Scanning failed.',\n idle: 'Camera is off.',\n result: 'Code scanned.',\n scanning: 'Point the camera at a QR code.',\n starting: 'Starting camera…',\n unsupported: 'QR scanning is not supported in this browser.',\n};\n\n/**\n * Camera QR scanner built on `createQrScanner` (native `BarcodeDetector`).\n * `active` starts/stops the camera externally; internal Start/Stop controls\n * drive the same intent. Unsupported browsers render the `unsupported` slot\n * so apps can substitute a paste fallback.\n *\n * @element ore-qr-scanner\n *\n * @attr {boolean} active - Start/stop the camera\n * @attr {string} facing-mode - 'environment' | 'user' (default 'environment')\n * @attr {number} interval - Ms between detection passes (default 200)\n * @attr {boolean} once - Stop after first scan (default true)\n * @attr {string} size - 'sm' | 'md' | 'lg', or a pixel value\n * @attr {string} label - Region label (default \"QR code scanner\")\n * @attr {string} rounded - Border radius\n *\n * @fires scan - detail: `{ value }` on each decode\n * @fires error - detail: `{ error }` on permission/support/detection failure\n * @fires status - detail: `{ status }` on view-state transitions\n *\n * @slot overlay - Content layered over the camera preview\n * @slot unsupported - Fallback UI when scanning is unsupported (e.g. paste input)\n * @slot footer - Extra content below the status row\n *\n * @cssprop --qr-scanner-size - Preview edge length in px (overrides `size`)\n * @cssprop --qr-scanner-radius - Border radius\n * @cssprop --qr-scanner-bg - Preview background while idle\n * @cssprop --qr-scanner-viewfinder-color - Viewfinder frame color\n * @cssprop --qr-scanner-status-color - Status text color\n *\n * @part wrapper - Outer container\n * @part stage - Square preview area (video + overlays)\n * @part video - The `<video>` preview element\n * @part viewfinder - Decorative scan frame over the preview\n * @part state - Idle/error/denied/unsupported/result panel\n * @part status - Live status line\n * @part controls - Button row\n *\n * @example\n * ```html\n * <ore-qr-scanner active @scan=${(e) => pair(e.detail.value)}></ore-qr-scanner>\n * ```\n */\nexport const QR_SCANNER_TAG = 'ore-qr-scanner' as const;\n\ndefine<OreQrScannerProps>(QR_SCANNER_TAG, {\n props: {\n active: prop.bool(false),\n 'facing-mode': prop.oneOf(['environment', 'user'] as const, 'environment'),\n interval: prop.number(200),\n label: prop.string('QR code scanner'),\n once: prop.bool(true),\n ...roundableBundle,\n scannerFactory: prop.data<QrScannerFactory>(),\n size: {\n default: 'md' as ComponentSize | number,\n parse: (v) => (v !== null && /^\\d+$/.test(v) ? Number(v) : ((v ?? 'md') as ComponentSize)),\n },\n },\n\n setup(props) {\n const emit = useEmit<OreQrScannerEvents>();\n const videoRef = ref<HTMLVideoElement>();\n\n const status = signal<OreQrScannerStatus>('idle');\n const failure = signal('');\n const lastValue = signal('');\n\n // `active` is an imperative hint mirrored into internal intent; the\n // internal Start/Stop buttons drive the same signal.\n const requested = signal(false);\n watch(\n props.active,\n (active) => {\n requested.value = active === true;\n },\n { immediate: true },\n );\n\n const setStatus = (next: OreQrScannerStatus, message = ''): void => {\n failure.value = message;\n if (status.value === next) return;\n status.value = next;\n emit('status', { status: next });\n };\n\n const statusText = (): string =>\n status.value === 'result' && lastValue.value ? `Code scanned: ${lastValue.value}` : STATUS_TEXT[status.value];\n\n // Eager support check so VitePress/jsdom (no BarcodeDetector) land on the\n // unsupported state immediately; an injected factory skips the gate.\n if (!props.scannerFactory.value && !isQrScanSupported()) {\n setStatus('unsupported', STATUS_TEXT.unsupported);\n }\n\n const startScanner = async (scanner: QrScanner): Promise<void> => {\n try {\n await scanner.start();\n // Intent may have been withdrawn while getUserMedia was pending.\n if (!requested.value) scanner.stop();\n } catch (error) {\n requested.value = false;\n const sigilError =\n error instanceof SigilError ? error : new SigilError('QR scanner failed to start', { cause: error });\n if (sigilError instanceof SigilUnsupportedError) setStatus('unsupported', sigilError.message);\n else if (sigilError instanceof SigilPermissionError) setStatus('denied', sigilError.message);\n else setStatus('error', sigilError.message);\n emit('error', { error: sigilError });\n }\n };\n\n const wire = (scanner: QrScanner): void => {\n scanner.onResult((result: QrScanResult) => {\n lastValue.value = result.value;\n emit('scan', { value: result.value });\n if (props.once.value) setStatus('result');\n });\n scanner.tap((event) => {\n if (event.type === 'status-change') {\n if (event.status === 'starting') setStatus('starting');\n else if (event.status === 'scanning') setStatus('scanning');\n // 'stopped' after a once-result must not clobber the result view.\n else if (event.status === 'stopped' && status.value !== 'result') setStatus('idle');\n else if (event.status === 'idle') setStatus('idle');\n } else if (event.type === 'error') {\n emit('error', { error: event.error });\n if (status.value === 'scanning' || status.value === 'starting') setStatus('error', event.error.message);\n }\n });\n };\n\n onElement(videoRef, (video) => {\n const current = signal<QrScanner | null>(null);\n\n // Rebuild when scan-affecting props change; cleanup disposes the previous scanner.\n watchEffect(() => {\n const factory = props.scannerFactory.value ?? createQrScanner;\n const scanner = factory({\n constraints: { facingMode: props['facing-mode'].value ?? 'environment' },\n intervalMs: props.interval.value ?? 200,\n once: props.once.value ?? true,\n video,\n });\n current.value = scanner;\n wire(scanner);\n return () => scanner.dispose();\n });\n\n // Drive start/stop from intent + the current scanner instance. `watch`\n // (untracked callback) is required over `watchEffect`: `scanner.stop()`\n // synchronously re-enters the tap handler, and tracking its `status`\n // reads would retrigger this effect on every transition — an infinite loop.\n watch(\n () => [requested.value, current.value] as const,\n ([want, scanner]) => {\n if (!scanner || scanner.disposed) return;\n if (want) void startScanner(scanner);\n else scanner.stop();\n },\n { immediate: true },\n );\n });\n\n const resume = (): void => {\n requested.value = true;\n };\n const pause = (): void => {\n requested.value = false;\n };\n\n const showVideo = (): boolean => status.value === 'starting' || status.value === 'scanning';\n const showControls = (): boolean => status.value !== 'unsupported';\n\n return html`\n <div\n class=\"wrapper\"\n part=\"wrapper\"\n role=\"region\"\n aria-label=${() => props.label.value}\n style=${() => `--_size-px: ${cssSize(props.size.value ?? 'md')}px`}>\n <div class=\"stage\" part=\"stage\">\n <video\n ref=${videoRef}\n part=\"video\"\n autoplay\n playsinline\n muted\n aria-hidden=\"true\"\n ?hidden=${() => !showVideo()}></video>\n <div class=\"viewfinder\" part=\"viewfinder\" aria-hidden=\"true\" ?hidden=${() => status.value !== 'scanning'}></div>\n <div class=\"overlay\"><slot name=\"overlay\"></slot></div>\n ${when(\n () => !showVideo(),\n () => html`\n <div class=\"state\" part=\"state\" data-state=${() => status.value}>\n ${when(\n () => status.value === 'unsupported',\n () => html`<slot name=\"unsupported\"></slot>`,\n () => html`\n ${when(\n () => status.value === 'result',\n () => html`<ore-icon name=\"check\" size=\"24\" aria-hidden=\"true\"></ore-icon>`,\n () => html`<ore-icon name=\"camera\" size=\"24\" aria-hidden=\"true\"></ore-icon>`,\n )}\n <span class=\"state-text\">${() => failure.value || STATUS_TEXT[status.value]}</span>\n `,\n )}\n </div>\n `,\n )}\n </div>\n <div class=\"status\" part=\"status\" role=\"status\">${statusText}</div>\n ${when(\n showControls,\n () => html`\n <div class=\"controls\" part=\"controls\">\n ${when(\n () => status.value === 'starting' || status.value === 'scanning',\n () => html`\n <ore-button variant=\"ghost\" size=\"sm\" @click=${pause}>Stop</ore-button>\n `,\n () => html`\n <ore-button\n variant=\"ghost\"\n size=\"sm\"\n @click=${resume}>\n ${() =>\n status.value === 'result'\n ? 'Scan again'\n : status.value === 'denied' || status.value === 'error'\n ? 'Retry'\n : 'Start camera'}\n </ore-button>\n `,\n )}\n </div>\n `,\n )}\n <slot name=\"footer\"></slot>\n </div>\n `;\n },\n styles: [roundedVariantMixin, componentStyles],\n});\n"],"mappings":"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"}