@xsynaptic/sonic-ui 0.26.0 → 0.28.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 (127) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -0
  3. package/dist/default/define/ticker.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/{display-element.js → analyser-element.js} +10 -12
  6. package/dist/default/elements/button.js +12 -9
  7. package/dist/default/elements/dial.js +9 -13
  8. package/dist/default/elements/envelope.js +26 -23
  9. package/dist/default/elements/field-gesture.js +49 -8
  10. package/dist/default/elements/meter.js +6 -8
  11. package/dist/default/elements/number.js +5 -14
  12. package/dist/default/elements/position-group.js +4 -4
  13. package/dist/default/elements/radio-group.js +6 -9
  14. package/dist/default/elements/segmented.js +7 -4
  15. package/dist/default/elements/slider.js +12 -13
  16. package/dist/default/elements/sonic-element.js +7 -5
  17. package/dist/default/elements/spectrum.js +3 -3
  18. package/dist/default/elements/switch.js +11 -6
  19. package/dist/default/elements/ticker.js +313 -0
  20. package/dist/default/elements/toggle.js +14 -8
  21. package/dist/default/elements/value-element.js +11 -3
  22. package/dist/default/elements/wave-element.js +11 -12
  23. package/dist/default/elements/waveform.js +1 -4
  24. package/dist/default/elements/wavestrip.js +3 -6
  25. package/dist/default/elements/xy.js +34 -47
  26. package/dist/default/index.js +2 -1
  27. package/dist/default/lib/cancel-zone.js +12 -0
  28. package/dist/default/lib/canvas-surface.js +13 -1
  29. package/dist/default/lib/distribute.js +2 -3
  30. package/dist/default/lib/mirror-children.js +1 -1
  31. package/dist/default/lib/pointer-drag.js +8 -2
  32. package/dist/default/lib/render.js +2 -2
  33. package/dist/default/lib/ticker-leg.js +54 -0
  34. package/dist/dev/define/ticker.d.ts +2 -0
  35. package/dist/dev/define/ticker.js +8 -0
  36. package/dist/dev/define/ticker.js.map +1 -0
  37. package/dist/dev/define.d.ts +3 -1
  38. package/dist/dev/define.js +1 -0
  39. package/dist/dev/elements/{display-element.d.ts → analyser-element.d.ts} +3 -3
  40. package/dist/dev/elements/{display-element.js → analyser-element.js} +11 -14
  41. package/dist/dev/elements/analyser-element.js.map +1 -0
  42. package/dist/dev/elements/button.d.ts +4 -1
  43. package/dist/dev/elements/button.js +12 -14
  44. package/dist/dev/elements/button.js.map +1 -1
  45. package/dist/dev/elements/dial.d.ts +4 -2
  46. package/dist/dev/elements/dial.js +9 -17
  47. package/dist/dev/elements/dial.js.map +1 -1
  48. package/dist/dev/elements/envelope.d.ts +5 -1
  49. package/dist/dev/elements/envelope.js +26 -27
  50. package/dist/dev/elements/envelope.js.map +1 -1
  51. package/dist/dev/elements/field-gesture.js +49 -8
  52. package/dist/dev/elements/field-gesture.js.map +1 -1
  53. package/dist/dev/elements/meter.d.ts +2 -1
  54. package/dist/dev/elements/meter.js +6 -9
  55. package/dist/dev/elements/meter.js.map +1 -1
  56. package/dist/dev/elements/number.d.ts +2 -2
  57. package/dist/dev/elements/number.js +5 -15
  58. package/dist/dev/elements/number.js.map +1 -1
  59. package/dist/dev/elements/position-group.js +4 -4
  60. package/dist/dev/elements/position-group.js.map +1 -1
  61. package/dist/dev/elements/radio-group.d.ts +1 -2
  62. package/dist/dev/elements/radio-group.js +6 -9
  63. package/dist/dev/elements/radio-group.js.map +1 -1
  64. package/dist/dev/elements/segmented.d.ts +4 -2
  65. package/dist/dev/elements/segmented.js +7 -9
  66. package/dist/dev/elements/segmented.js.map +1 -1
  67. package/dist/dev/elements/slider.d.ts +4 -2
  68. package/dist/dev/elements/slider.js +12 -17
  69. package/dist/dev/elements/slider.js.map +1 -1
  70. package/dist/dev/elements/sonic-element.d.ts +6 -7
  71. package/dist/dev/elements/sonic-element.js +14 -83
  72. package/dist/dev/elements/sonic-element.js.map +1 -1
  73. package/dist/dev/elements/spectrum.d.ts +2 -2
  74. package/dist/dev/elements/spectrum.js +3 -3
  75. package/dist/dev/elements/spectrum.js.map +1 -1
  76. package/dist/dev/elements/switch.d.ts +4 -1
  77. package/dist/dev/elements/switch.js +11 -10
  78. package/dist/dev/elements/switch.js.map +1 -1
  79. package/dist/dev/elements/ticker.d.ts +25 -0
  80. package/dist/dev/elements/ticker.js +314 -0
  81. package/dist/dev/elements/ticker.js.map +1 -0
  82. package/dist/dev/elements/toggle.d.ts +4 -1
  83. package/dist/dev/elements/toggle.js +14 -13
  84. package/dist/dev/elements/toggle.js.map +1 -1
  85. package/dist/dev/elements/value-element.d.ts +4 -3
  86. package/dist/dev/elements/value-element.js +11 -3
  87. package/dist/dev/elements/value-element.js.map +1 -1
  88. package/dist/dev/elements/wave-element.d.ts +2 -2
  89. package/dist/dev/elements/wave-element.js +11 -16
  90. package/dist/dev/elements/wave-element.js.map +1 -1
  91. package/dist/dev/elements/waveform.d.ts +0 -1
  92. package/dist/dev/elements/waveform.js +1 -4
  93. package/dist/dev/elements/waveform.js.map +1 -1
  94. package/dist/dev/elements/wavestrip.js +3 -6
  95. package/dist/dev/elements/wavestrip.js.map +1 -1
  96. package/dist/dev/elements/xy.d.ts +4 -1
  97. package/dist/dev/elements/xy.js +34 -51
  98. package/dist/dev/elements/xy.js.map +1 -1
  99. package/dist/dev/index.d.ts +4 -1
  100. package/dist/dev/index.js +2 -1
  101. package/dist/dev/lib/cancel-zone.js +13 -0
  102. package/dist/dev/lib/cancel-zone.js.map +1 -0
  103. package/dist/dev/lib/canvas-surface.js +13 -1
  104. package/dist/dev/lib/canvas-surface.js.map +1 -1
  105. package/dist/dev/lib/check-styles.d.ts +8 -0
  106. package/dist/dev/lib/check-styles.js +95 -0
  107. package/dist/dev/lib/check-styles.js.map +1 -0
  108. package/dist/dev/lib/distribute.js +2 -3
  109. package/dist/dev/lib/distribute.js.map +1 -1
  110. package/dist/dev/lib/mirror-children.js +9 -7
  111. package/dist/dev/lib/mirror-children.js.map +1 -1
  112. package/dist/dev/lib/pointer-drag.js +8 -2
  113. package/dist/dev/lib/pointer-drag.js.map +1 -1
  114. package/dist/dev/lib/render.js +2 -2
  115. package/dist/dev/lib/render.js.map +1 -1
  116. package/dist/dev/lib/ticker-leg.js +55 -0
  117. package/dist/dev/lib/ticker-leg.js.map +1 -0
  118. package/dist/styles/controls.css +1 -0
  119. package/dist/styles/dial.css +28 -5
  120. package/dist/styles/material/cap.css +16 -5
  121. package/dist/styles/material/core.css +1 -0
  122. package/dist/styles/slider.css +2 -1
  123. package/dist/styles/switch.css +3 -1
  124. package/dist/styles/ticker.css +115 -0
  125. package/dist/styles/toggle.css +7 -5
  126. package/package.json +2 -1
  127. package/dist/dev/elements/display-element.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"distribute.js","names":[],"sources":["../../../src/lib/distribute.ts"],"sourcesContent":["import { clamp, trimFloat } from '#lib/math.ts';\n\nexport interface SplitMember {\n\tisFree: boolean;\n\tmax: number;\n\tmin: number;\n\tsnap: (value: number) => number;\n\tstepFrom: (value: number, direction: -1 | 1) => number;\n\tvalue: number;\n}\n\nexport type SplitMode = 'cascade' | 'equal' | 'proportional';\n\ninterface SplitMove {\n\tdirection?: -1 | 0 | 1;\n\tindex: number;\n\ttarget: number;\n}\n\ninterface SplitRule {\n\tmode: SplitMode;\n\ttotal: number;\n}\n\ninterface Pooled {\n\tindex: number;\n\troom: number;\n\tweight: number;\n}\n\nconst epsilon = 1e-9;\n\nfunction sumOf(values: ReadonlyArray<number>): number {\n\treturn values.reduce((total, value) => total + value, 0);\n}\n\nfunction others(members: ReadonlyArray<SplitMember>, index: number): Array<SplitMember> {\n\treturn members.filter((_member, at) => at !== index);\n}\n\nexport function splitLimits(\n\tmembers: ReadonlyArray<SplitMember>,\n\tindex: number,\n\ttotal: number,\n): [number, number] {\n\tconst member = members[index];\n\tif (!member) return [NaN, NaN];\n\n\tconst rest = others(members, index);\n\tconst free = rest.filter((other) => other.isFree);\n\tif (free.length === 0) return [member.value, member.value];\n\n\tconst fixed = sumOf(rest.filter((other) => !other.isFree).map((other) => other.value));\n\tconst room = total - fixed;\n\n\treturn [\n\t\tMath.max(member.min, room - sumOf(free.map((other) => other.max))),\n\t\tMath.min(member.max, room - sumOf(free.map((other) => other.min))),\n\t];\n}\n\nfunction spreadWeighted(owed: number, pool: ReadonlyArray<Pooled>): Map<number, number> {\n\tconst shares = new Map<number, number>();\n\tlet remaining = owed;\n\tlet active = [...pool];\n\n\twhile (active.length > 0 && Math.abs(remaining) > epsilon) {\n\t\tconst weights = sumOf(active.map(({ weight }) => weight));\n\t\tconst count = active.length;\n\t\tconst shareOf = (entry: Pooled): number =>\n\t\t\tremaining * (weights > 0 ? entry.weight / weights : 1 / count);\n\t\tconst full = active.filter((entry) => Math.abs(shareOf(entry)) >= entry.room);\n\n\t\tif (full.length === 0) {\n\t\t\tfor (const entry of active) shares.set(entry.index, shareOf(entry));\n\t\t\tbreak;\n\t\t}\n\n\t\tconst sign = Math.sign(remaining);\n\n\t\tfor (const entry of full) {\n\t\t\tshares.set(entry.index, sign * entry.room);\n\t\t\tremaining -= sign * entry.room;\n\t\t}\n\t\tactive = active.filter((entry) => !full.includes(entry));\n\t}\n\n\treturn shares;\n}\n\nfunction spreadInOrder(owed: number, pool: ReadonlyArray<Pooled>): Map<number, number> {\n\tconst shares = new Map<number, number>();\n\tlet remaining = owed;\n\n\tfor (const entry of pool) {\n\t\tif (Math.abs(remaining) <= epsilon) break;\n\n\t\tconst share = Math.sign(remaining) * Math.min(entry.room, Math.abs(remaining));\n\n\t\tshares.set(entry.index, share);\n\t\tremaining -= share;\n\t}\n\n\treturn shares;\n}\n\nfunction poolOrder(members: ReadonlyArray<SplitMember>, index: number): Array<number> {\n\tconst count = members.length;\n\tconst start = index + 1;\n\n\treturn Array.from({ length: count }, (_entry, offset) => (start + offset) % count).filter(\n\t\t(at) => at !== index && members[at]?.isFree === true,\n\t);\n}\n\nfunction stepWithin(member: SplitMember, from: number, owed: number): number | undefined {\n\tconst next = member.stepFrom(from, owed > 0 ? 1 : -1);\n\tconst change = trimFloat(next - from);\n\tconst isInside = next >= member.min && next <= member.max;\n\n\treturn change !== 0 && isInside && Math.abs(change) <= Math.abs(owed) + epsilon\n\t\t? next\n\t\t: undefined;\n}\n\nfunction stepToward(member: SplitMember, from: number, owed: number): number {\n\tlet value = from;\n\tlet remainder = owed;\n\n\twhile (Math.abs(remainder) > epsilon) {\n\t\tconst next = stepWithin(member, value, remainder);\n\t\tif (next === undefined) break;\n\n\t\tremainder = trimFloat(remainder - (next - value));\n\t\tvalue = next;\n\t}\n\n\treturn value;\n}\n\ninterface Settling {\n\texact: ReadonlyMap<number, number>;\n\torder: ReadonlyArray<number>;\n\ttotal: number;\n}\n\n// Furthest from its exact share first; a tie goes to the last in pool order\nfunction byShortfall(\n\tvalues: ReadonlyArray<number>,\n\t{ exact, order }: Settling,\n\tdirection: number,\n): Array<number> {\n\tconst shortfall = (index: number): number => {\n\t\tconst value = values[index] ?? NaN;\n\n\t\treturn trimFloat(direction * ((exact.get(index) ?? value) - value));\n\t};\n\n\treturn order.toReversed().toSorted((first, second) => shortfall(second) - shortfall(first));\n}\n\nfunction stepEach(\n\tmembers: ReadonlyArray<SplitMember>,\n\tvalues: Array<number>,\n\tpass: { neediest: ReadonlyArray<number>; owed: number },\n): number {\n\tlet remainder = pass.owed;\n\n\tfor (const index of pass.neediest) {\n\t\tconst member = members[index];\n\t\tconst value = values[index];\n\t\tconst next = member && value !== undefined ? stepWithin(member, value, remainder) : undefined;\n\t\tif (value === undefined || next === undefined) continue;\n\n\t\tvalues[index] = next;\n\t\tremainder = trimFloat(remainder - (next - value));\n\t}\n\n\treturn remainder;\n}\n\nfunction settle(\n\tmembers: ReadonlyArray<SplitMember>,\n\tvalues: Array<number>,\n\tsettling: Settling,\n): number {\n\tlet remainder = trimFloat(settling.total - sumOf(values));\n\tlet before = NaN;\n\n\twhile (remainder !== before && Math.abs(remainder) > epsilon) {\n\t\tbefore = remainder;\n\t\tremainder = stepEach(members, values, {\n\t\t\tneediest: byShortfall(values, settling, Math.sign(remainder)),\n\t\t\towed: remainder,\n\t\t});\n\t}\n\n\treturn remainder;\n}\n\nfunction pooled(\n\tmembers: ReadonlyArray<SplitMember>,\n\torder: ReadonlyArray<number>,\n\towed: number,\n): Array<Pooled> {\n\treturn order.flatMap((at): Array<Pooled> => {\n\t\tconst member = members[at];\n\t\tif (!member) return [];\n\n\t\tconst room = owed > 0 ? member.max - member.value : member.value - member.min;\n\n\t\treturn [{ index: at, room: Math.max(0, room), weight: member.value - member.min }];\n\t});\n}\n\nfunction shares(pool: Array<Pooled>, owed: number, mode: SplitMode): Map<number, number> {\n\tif (mode === 'cascade') return spreadInOrder(owed, pool);\n\tif (mode === 'equal')\n\t\treturn spreadWeighted(\n\t\t\towed,\n\t\t\tpool.map((entry) => ({ ...entry, weight: 1 })),\n\t\t);\n\n\treturn spreadWeighted(owed, pool);\n}\n\nfunction landMover(\n\tmembers: ReadonlyArray<SplitMember>,\n\tvalues: Array<number>,\n\tmove: SplitMove & { total: number },\n): number | undefined {\n\tconst { index, target, total } = move;\n\tconst mover = members[index];\n\tif (!mover?.isFree || !Number.isFinite(target)) return undefined;\n\n\tconst next = mover.snap(clamp(target, ...splitLimits(members, index, total)));\n\n\tvalues[index] = next;\n\n\treturn mover.value - next;\n}\n\nfunction landShares(\n\tmembers: ReadonlyArray<SplitMember>,\n\tvalues: Array<number>,\n\tspread: ReadonlyMap<number, number>,\n): Map<number, number> {\n\tconst exact = new Map<number, number>();\n\n\tfor (const [at, share] of spread) {\n\t\tconst member = members[at];\n\t\tif (!member) continue;\n\n\t\texact.set(at, member.value + share);\n\t\tvalues[at] = clamp(member.snap(member.value + share), member.min, member.max);\n\t}\n\n\treturn exact;\n}\n\ninterface Spread {\n\tleft: number;\n\torder: ReadonlyArray<number>;\n\tvalues: Array<number>;\n}\n\nfunction spread(\n\tmembers: ReadonlyArray<SplitMember>,\n\trule: SplitRule,\n\tmove?: SplitMove,\n): Spread | undefined {\n\tconst values = members.map(({ value }) => value);\n\tconst owed = move\n\t\t? landMover(members, values, { ...move, total: rule.total })\n\t\t: rule.total - sumOf(values);\n\tif (owed === undefined) return undefined;\n\n\tconst order = poolOrder(members, move?.index ?? -1);\n\tconst exact = landShares(members, values, shares(pooled(members, order, owed), owed, rule.mode));\n\n\treturn { left: settle(members, values, { exact, order, total: rule.total }), order, values };\n}\n\nfunction smallestStep(\n\tmembers: ReadonlyArray<SplitMember>,\n\t{ left, order, values }: Spread,\n): number | undefined {\n\tconst steps = order.flatMap((at) => {\n\t\tconst member = members[at];\n\t\tconst value = values[at];\n\t\tif (!member || value === undefined) return [];\n\n\t\tconst next = member.stepFrom(value, left > 0 ? 1 : -1);\n\t\tconst isInside = next >= member.min && next <= member.max;\n\n\t\treturn isInside && next !== value ? [trimFloat(next - value)] : [];\n\t});\n\n\treturn steps.toSorted((one, other) => Math.abs(one) - Math.abs(other))[0];\n}\n\n// The mover takes what is left on its own grid, or a sibling steps past the total and the mover gives the difference back\nfunction moverTargets(\n\tmembers: ReadonlyArray<SplitMember>,\n\tlanding: Spread,\n\tindex: number,\n): Array<number> {\n\tconst mover = members[index];\n\tconst landed = landing.values[index];\n\tif (!mover || landed === undefined) return [];\n\n\tconst step = smallestStep(members, landing);\n\tconst held = stepToward(mover, landed, landing.left);\n\n\treturn step === undefined\n\t\t? [held]\n\t\t: [held, stepToward(mover, landed, trimFloat(landing.left - step))];\n}\n\nfunction nearest(\n\tcandidates: Array<Array<number>>,\n\tmove: SplitMove,\n\tfrom: { landed: number; start: number },\n): Array<number> | undefined {\n\tconst { direction = 0, index } = move;\n\tconst moverOf = (candidate: Array<number>): number => candidate[index] ?? NaN;\n\tconst [first, ...rest] = candidates.toSorted(\n\t\t(one, other) => Math.abs(moverOf(one) - from.landed) - Math.abs(moverOf(other) - from.landed),\n\t);\n\tif (!first || direction === 0 || moverOf(first) !== from.start) return first;\n\n\treturn (\n\t\trest.find((candidate) => Math.sign(moverOf(candidate) - from.start) === direction) ?? first\n\t);\n}\n\nfunction close(\n\tmembers: ReadonlyArray<SplitMember>,\n\trule: SplitRule,\n\theld: { landing: Spread; move: SplitMove; mover: SplitMember },\n): Array<number> {\n\tconst { landing, move, mover } = held;\n\tconst { index } = move;\n\tconst landed = landing.values[index] ?? mover.value;\n\tconst targets = moverTargets(members, landing, index);\n\tconst closed = targets.flatMap((target) => {\n\t\tconst attempt = spread(members, rule, { index, target });\n\n\t\treturn attempt && Math.abs(attempt.left) <= epsilon ? [attempt.values] : [];\n\t});\n\n\treturn (\n\t\tnearest(closed, move, { landed, start: mover.value }) ??\n\t\tlanding.values.with(index, targets[0] ?? landed)\n\t);\n}\n\nexport function distribute(\n\tmembers: ReadonlyArray<SplitMember>,\n\trule: SplitRule,\n\tmove?: SplitMove,\n): Array<number> {\n\tconst landing = spread(members, rule, move);\n\tif (!landing) return members.map(({ value }) => value);\n\n\tconst mover = members[move?.index ?? -1];\n\tif (!move || !mover || Math.abs(landing.left) <= epsilon) return landing.values;\n\n\treturn close(members, rule, { landing, move, mover });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"distribute.js","names":[],"sources":["../../../src/lib/distribute.ts"],"sourcesContent":["import { clamp, trimFloat } from '#lib/math.ts';\n\nexport interface SplitMember {\n\tisFree: boolean;\n\tmax: number;\n\tmin: number;\n\tsnap: (value: number) => number;\n\tstepFrom: (value: number, direction: -1 | 1) => number;\n\tvalue: number;\n}\n\nexport type SplitMode = 'cascade' | 'equal' | 'proportional';\n\ninterface SplitMove {\n\tdirection?: -1 | 0 | 1;\n\tindex: number;\n\ttarget: number;\n}\n\ninterface SplitRule {\n\tmode: SplitMode;\n\ttotal: number;\n}\n\ninterface Pooled {\n\tindex: number;\n\troom: number;\n\tweight: number;\n}\n\nconst epsilon = 1e-9;\n\nfunction sumOf(values: ReadonlyArray<number>): number {\n\treturn values.reduce((total, value) => total + value, 0);\n}\n\nfunction others(members: ReadonlyArray<SplitMember>, index: number): Array<SplitMember> {\n\treturn members.filter((_member, at) => at !== index);\n}\n\nexport function splitLimits(\n\tmembers: ReadonlyArray<SplitMember>,\n\tindex: number,\n\ttotal: number,\n): [number, number] {\n\tconst member = members[index];\n\tif (!member) return [NaN, NaN];\n\n\tconst rest = others(members, index);\n\tconst free = rest.filter((other) => other.isFree);\n\tif (free.length === 0) return [member.value, member.value];\n\n\tconst fixed = sumOf(rest.filter((other) => !other.isFree).map((other) => other.value));\n\tconst room = total - fixed;\n\n\treturn [\n\t\tMath.max(member.min, room - sumOf(free.map((other) => other.max))),\n\t\tMath.min(member.max, room - sumOf(free.map((other) => other.min))),\n\t];\n}\n\nfunction spreadWeighted(owed: number, pool: ReadonlyArray<Pooled>): Map<number, number> {\n\tconst shares = new Map<number, number>();\n\tlet remaining = owed;\n\tlet active = [...pool];\n\n\twhile (active.length > 0 && Math.abs(remaining) > epsilon) {\n\t\tconst weights = sumOf(active.map(({ weight }) => weight));\n\t\tconst count = active.length;\n\t\tconst shareOf = (entry: Pooled): number =>\n\t\t\tremaining * (weights > 0 ? entry.weight / weights : 1 / count);\n\t\tconst full = active.filter((entry) => Math.abs(shareOf(entry)) >= entry.room);\n\n\t\tif (full.length === 0) {\n\t\t\tfor (const entry of active) shares.set(entry.index, shareOf(entry));\n\t\t\tbreak;\n\t\t}\n\n\t\tconst sign = Math.sign(remaining);\n\n\t\tfor (const entry of full) {\n\t\t\tshares.set(entry.index, sign * entry.room);\n\t\t\tremaining -= sign * entry.room;\n\t\t}\n\t\tactive = active.filter((entry) => !full.includes(entry));\n\t}\n\n\treturn shares;\n}\n\nfunction spreadInOrder(owed: number, pool: ReadonlyArray<Pooled>): Map<number, number> {\n\tconst shares = new Map<number, number>();\n\tlet remaining = owed;\n\n\tfor (const entry of pool) {\n\t\tif (Math.abs(remaining) <= epsilon) break;\n\n\t\tconst share = Math.sign(remaining) * Math.min(entry.room, Math.abs(remaining));\n\n\t\tshares.set(entry.index, share);\n\t\tremaining -= share;\n\t}\n\n\treturn shares;\n}\n\nfunction poolOrder(members: ReadonlyArray<SplitMember>, index: number): Array<number> {\n\tconst count = members.length;\n\tconst start = index + 1;\n\n\treturn Array.from({ length: count }, (_entry, offset) => (start + offset) % count).filter(\n\t\t(at) => at !== index && members[at]?.isFree === true,\n\t);\n}\n\nfunction stepWithin(member: SplitMember, from: number, owed: number): number | undefined {\n\tconst next = member.stepFrom(from, owed > 0 ? 1 : -1);\n\tconst change = trimFloat(next - from);\n\tconst isInside = next >= member.min && next <= member.max;\n\n\treturn change !== 0 && isInside && Math.abs(change) <= Math.abs(owed) + epsilon\n\t\t? next\n\t\t: undefined;\n}\n\nfunction stepToward(member: SplitMember, from: number, owed: number): number {\n\tlet value = from;\n\tlet remainder = owed;\n\n\twhile (Math.abs(remainder) > epsilon) {\n\t\tconst next = stepWithin(member, value, remainder);\n\t\tif (next === undefined) break;\n\n\t\tremainder = trimFloat(remainder - (next - value));\n\t\tvalue = next;\n\t}\n\n\treturn value;\n}\n\ninterface Settling {\n\texact: ReadonlyMap<number, number>;\n\torder: ReadonlyArray<number>;\n\ttotal: number;\n}\n\n// Furthest from its exact share first; a tie goes to the last in pool order\nfunction byShortfall(\n\tvalues: ReadonlyArray<number>,\n\t{ exact, order }: Settling,\n\tdirection: number,\n): Array<number> {\n\tconst shortfall = (index: number): number => {\n\t\tconst value = values[index] ?? NaN;\n\n\t\treturn trimFloat(direction * ((exact.get(index) ?? value) - value));\n\t};\n\n\treturn order.toReversed().toSorted((first, second) => shortfall(second) - shortfall(first));\n}\n\nfunction stepEach(\n\tmembers: ReadonlyArray<SplitMember>,\n\tvalues: Array<number>,\n\tpass: { neediest: ReadonlyArray<number>; owed: number },\n): number {\n\tlet remainder = pass.owed;\n\n\tfor (const index of pass.neediest) {\n\t\tconst member = members[index];\n\t\tconst value = values[index];\n\t\tconst next = member && value !== undefined ? stepWithin(member, value, remainder) : undefined;\n\t\tif (value === undefined || next === undefined) continue;\n\n\t\tvalues[index] = next;\n\t\tremainder = trimFloat(remainder - (next - value));\n\t}\n\n\treturn remainder;\n}\n\nfunction settle(\n\tmembers: ReadonlyArray<SplitMember>,\n\tvalues: Array<number>,\n\tsettling: Settling,\n): number {\n\tlet remainder = trimFloat(settling.total - sumOf(values));\n\tlet before = NaN;\n\n\twhile (remainder !== before && Math.abs(remainder) > epsilon) {\n\t\tbefore = remainder;\n\t\tremainder = stepEach(members, values, {\n\t\t\tneediest: byShortfall(values, settling, Math.sign(remainder)),\n\t\t\towed: remainder,\n\t\t});\n\t}\n\n\treturn remainder;\n}\n\nfunction pooled(\n\tmembers: ReadonlyArray<SplitMember>,\n\torder: ReadonlyArray<number>,\n\towed: number,\n): Array<Pooled> {\n\treturn order.flatMap((at): Array<Pooled> => {\n\t\tconst member = members[at];\n\t\tif (!member) return [];\n\n\t\tconst room = owed > 0 ? member.max - member.value : member.value - member.min;\n\n\t\treturn [{ index: at, room: Math.max(0, room), weight: member.value - member.min }];\n\t});\n}\n\nfunction shares(pool: Array<Pooled>, owed: number, mode: SplitMode): Map<number, number> {\n\tif (mode === 'cascade') return spreadInOrder(owed, pool);\n\tif (mode === 'equal')\n\t\treturn spreadWeighted(\n\t\t\towed,\n\t\t\tpool.map((entry) => ({ ...entry, weight: 1 })),\n\t\t);\n\n\treturn spreadWeighted(owed, pool);\n}\n\nfunction landMover(\n\tmembers: ReadonlyArray<SplitMember>,\n\tvalues: Array<number>,\n\tmove: SplitMove & { total: number },\n): number | undefined {\n\tconst { index, target, total } = move;\n\tconst mover = members[index];\n\tif (!mover?.isFree || !Number.isFinite(target)) return undefined;\n\n\tconst next = mover.snap(clamp(target, ...splitLimits(members, index, total)));\n\n\tvalues[index] = next;\n\n\treturn mover.value - next;\n}\n\nfunction landShares(\n\tmembers: ReadonlyArray<SplitMember>,\n\tvalues: Array<number>,\n\tspread: ReadonlyMap<number, number>,\n): Map<number, number> {\n\tconst exact = new Map<number, number>();\n\n\tfor (const [at, share] of spread) {\n\t\tconst member = members[at];\n\t\tif (!member) continue;\n\n\t\texact.set(at, member.value + share);\n\t\tvalues[at] = clamp(member.snap(member.value + share), member.min, member.max);\n\t}\n\n\treturn exact;\n}\n\ninterface Spread {\n\tleft: number;\n\torder: ReadonlyArray<number>;\n\tvalues: Array<number>;\n}\n\nfunction spread(\n\tmembers: ReadonlyArray<SplitMember>,\n\trule: SplitRule,\n\tmove?: SplitMove,\n): Spread | undefined {\n\tconst values = members.map(({ value }) => value);\n\tconst owed = move\n\t\t? landMover(members, values, { ...move, total: rule.total })\n\t\t: rule.total - sumOf(values);\n\tif (owed === undefined) return undefined;\n\n\tconst order = poolOrder(members, move?.index ?? -1);\n\tconst exact = landShares(members, values, shares(pooled(members, order, owed), owed, rule.mode));\n\n\treturn { left: settle(members, values, { exact, order, total: rule.total }), order, values };\n}\n\nfunction smallestStep(\n\tmembers: ReadonlyArray<SplitMember>,\n\t{ left, order, values }: Spread,\n): number | undefined {\n\tconst steps = order.flatMap((at) => {\n\t\tconst member = members[at];\n\t\tconst value = values[at];\n\t\tif (!member || value === undefined) return [];\n\n\t\tconst next = member.stepFrom(value, left > 0 ? 1 : -1);\n\t\tconst isInside = next >= member.min && next <= member.max;\n\n\t\treturn isInside && next !== value ? [trimFloat(next - value)] : [];\n\t});\n\n\treturn steps.toSorted((one, other) => Math.abs(one) - Math.abs(other))[0];\n}\n\n// The mover takes what is left on its own grid, or a sibling steps past the total and the mover gives the difference back\nfunction moverTargets(\n\tmembers: ReadonlyArray<SplitMember>,\n\tlanding: Spread,\n\tindex: number,\n): Array<number> {\n\tconst mover = members[index];\n\tconst landed = landing.values[index];\n\tif (!mover || landed === undefined) return [];\n\n\tconst step = smallestStep(members, landing);\n\tconst held = stepToward(mover, landed, landing.left);\n\n\treturn step === undefined\n\t\t? [held]\n\t\t: [held, stepToward(mover, landed, trimFloat(landing.left - step))];\n}\n\nfunction nearest(\n\tcandidates: Array<Array<number>>,\n\tmove: SplitMove,\n\tfrom: { landed: number; start: number },\n): Array<number> | undefined {\n\tconst { direction = 0, index } = move;\n\tconst moverOf = (candidate: Array<number>): number => candidate[index] ?? NaN;\n\tconst [first, ...rest] = candidates.toSorted(\n\t\t(one, other) => Math.abs(moverOf(one) - from.landed) - Math.abs(moverOf(other) - from.landed),\n\t);\n\tif (!first || direction === 0 || moverOf(first) !== from.start) return first;\n\n\treturn (\n\t\trest.find((candidate) => Math.sign(moverOf(candidate) - from.start) === direction) ?? first\n\t);\n}\n\nfunction close(\n\tmembers: ReadonlyArray<SplitMember>,\n\trule: SplitRule,\n\theld: { landing: Spread; move: SplitMove; mover: SplitMember },\n): Array<number> {\n\tconst { landing, move, mover } = held;\n\tconst { index } = move;\n\tconst landed = landing.values[index] ?? mover.value;\n\tconst targets = moverTargets(members, landing, index);\n\tconst closed = targets.flatMap((target) => {\n\t\tconst attempt = spread(members, rule, { index, target });\n\n\t\treturn attempt && Math.abs(attempt.left) <= epsilon ? [attempt.values] : [];\n\t});\n\n\treturn nearest(closed, move, { landed, start: mover.value }) ?? members.map(({ value }) => value);\n}\n\nexport function distribute(\n\tmembers: ReadonlyArray<SplitMember>,\n\trule: SplitRule,\n\tmove?: SplitMove,\n): Array<number> {\n\tconst landing = spread(members, rule, move);\n\tif (!landing) return members.map(({ value }) => value);\n\n\tconst mover = members[move?.index ?? -1];\n\tif (!move || !mover || Math.abs(landing.left) <= epsilon) return landing.values;\n\n\treturn close(members, rule, { landing, move, mover });\n}\n"],"mappings":"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"}
@@ -4,14 +4,16 @@ import { attachSlots } from "./render.js";
4
4
  //#region src/lib/mirror-children.ts
5
5
  const checkedMirrors = /* @__PURE__ */ new WeakSet();
6
6
  const hostSlots = /* @__PURE__ */ new WeakMap();
7
- const interactive = "a[href], button, input, select, textarea, [tabindex]";
8
- function unmirrorable(original) {
7
+ const interactive = "button, input, select, textarea, [tabindex]";
8
+ const link = "a[href]";
9
+ function unmirrorable(original, selector) {
9
10
  if (!(original instanceof Element)) return void 0;
10
- return [original, ...original.querySelectorAll("*")].find((element) => element.matches(interactive) || element.localName.includes("-"));
11
+ return [original, ...original.querySelectorAll("*")].find((element) => element.matches(selector) || element.localName.includes("-"));
11
12
  }
12
- function checkMirrored(host, originals) {
13
+ function checkMirrored(host, originals, canCopyLinks) {
13
14
  if (checkedMirrors.has(host.constructor)) return;
14
- const [found] = originals.flatMap((original) => unmirrorable(original) ?? []);
15
+ const selector = canCopyLinks ? interactive : `${link}, ${interactive}`;
16
+ const [found] = originals.flatMap((original) => unmirrorable(original, selector) ?? []);
15
17
  if (!found) return;
16
18
  checkedMirrors.add(host.constructor);
17
19
  console.warn(`<${host.localName}> copies its children into the control, so the <${found.localName}> inside it loses its listeners and state; keep mirrored children static`);
@@ -27,7 +29,7 @@ function isCopiedChange(host, record, { control, isCopied }) {
27
29
  if (child === void 0) return false;
28
30
  return isCopied(child) || record.type === "attributes" && record.target === child;
29
31
  }
30
- function mirrorChildren(host, { control, copy, isCopied = () => true, isPassed = () => false, place }, signal) {
32
+ function mirrorChildren(host, { canCopyLinks = false, control, copy, isCopied = () => true, isPassed = () => false, place }, signal) {
31
33
  const slots = hostSlots.get(host) ?? attachSlots(host);
32
34
  let copies = /* @__PURE__ */ new Map();
33
35
  hostSlots.set(host, slots);
@@ -37,7 +39,7 @@ function mirrorChildren(host, { control, copy, isCopied = () => true, isPassed =
37
39
  const slotted = children.filter((child) => child instanceof Element || child instanceof Text);
38
40
  const kept = copies;
39
41
  const originals = children.filter((child) => isCopied(child));
40
- checkMirrored(host, originals);
42
+ checkMirrored(host, originals, canCopyLinks);
41
43
  checkChildren(host, children.filter((child) => !isCopied(child) && !isPassed(child)));
42
44
  copies = new Map(originals.map((original) => [original, touched && !touched.has(original) && kept.has(original) ? kept.get(original) : copy(original)]));
43
45
  place([...copies.values()].filter((made) => made !== void 0));
@@ -1 +1 @@
1
- {"version":3,"file":"mirror-children.js","names":[],"sources":["../../../src/lib/mirror-children.ts"],"sourcesContent":["import type { MirrorSlots } from '#lib/render.ts';\n\nimport { appendOnce, checkChildren } from '#lib/owned-control.ts';\nimport { attachSlots } from '#lib/render.ts';\n\ndeclare const __DEV__: boolean;\n\nconst checkedMirrors = new WeakSet<object>();\n\nconst hostSlots = new WeakMap<HTMLElement, MirrorSlots>();\n\nconst interactive = 'a[href], button, input, select, textarea, [tabindex]';\n\ninterface Mirror {\n\tcontrol: Element;\n\tcopy: (original: ChildNode) => Node | undefined;\n\tisCopied?: (child: Node) => boolean;\n\tisPassed?: (child: Node) => boolean;\n\tplace: (copies: Array<Node>) => void;\n}\n\nfunction unmirrorable(original: Node): Element | undefined {\n\tif (!(original instanceof Element)) return undefined;\n\n\treturn [original, ...original.querySelectorAll('*')].find(\n\t\t(element) => element.matches(interactive) || element.localName.includes('-'),\n\t);\n}\n\nfunction checkMirrored(host: HTMLElement, originals: Array<Node>): void {\n\tif (!__DEV__ || checkedMirrors.has(host.constructor)) return;\n\n\tconst [found] = originals.flatMap((original) => unmirrorable(original) ?? []);\n\tif (!found) return;\n\n\tcheckedMirrors.add(host.constructor);\n\tconsole.warn(\n\t\t`<${host.localName}> copies its children into the control, so the <${found.localName}> inside it loses its listeners and state; keep mirrored children static`,\n\t);\n}\n\nfunction childHolding(host: HTMLElement, node: Node): Node | undefined {\n\tlet current: Node | null = node;\n\n\twhile (current && current.parentNode !== host) current = current.parentNode;\n\n\treturn current ?? undefined;\n}\n\nfunction isCopiedChange(\n\thost: HTMLElement,\n\trecord: MutationRecord,\n\t{ control, isCopied }: Required<Pick<Mirror, 'control' | 'isCopied'>>,\n): boolean {\n\tif (record.target === host) {\n\t\treturn (\n\t\t\trecord.removedNodes.length > 0 || [...record.addedNodes].some((node) => node !== control)\n\t\t);\n\t}\n\n\tconst child = childHolding(host, record.target);\n\n\tif (child === undefined) return false;\n\n\treturn isCopied(child) || (record.type === 'attributes' && record.target === child);\n}\n\nexport function mirrorChildren(\n\thost: HTMLElement,\n\t{ control, copy, isCopied = () => true, isPassed = () => false, place }: Mirror,\n\tsignal: AbortSignal,\n): void {\n\tconst slots = hostSlots.get(host) ?? attachSlots(host);\n\tlet copies = new Map<ChildNode, Node | undefined>();\n\n\thostSlots.set(host, slots);\n\tconst mirror = (touched?: Set<Node | undefined>): void => {\n\t\tappendOnce(host, control);\n\n\t\tconst children = [...host.childNodes].filter((child) => child !== control);\n\t\tconst slotted = children.filter(\n\t\t\t(child): child is Element | Text => child instanceof Element || child instanceof Text,\n\t\t);\n\t\tconst kept = copies;\n\t\tconst originals = children.filter((child) => isCopied(child));\n\n\t\tif (__DEV__) {\n\t\t\tcheckMirrored(host, originals);\n\t\t\tcheckChildren(\n\t\t\t\thost,\n\t\t\t\tchildren.filter((child) => !isCopied(child) && !isPassed(child)),\n\t\t\t);\n\t\t}\n\t\tcopies = new Map(\n\t\t\toriginals.map((original) => [\n\t\t\t\toriginal,\n\t\t\t\ttouched && !touched.has(original) && kept.has(original)\n\t\t\t\t\t? kept.get(original)\n\t\t\t\t\t: copy(original),\n\t\t\t]),\n\t\t);\n\t\tplace([...copies.values()].filter((made) => made !== undefined));\n\t\tslots.shown.assign(...slotted.filter((child) => !isCopied(child)), control);\n\t\tslots.kept.assign(...slotted.filter((child) => isCopied(child)));\n\t};\n\t// Each child is watched, never the control, so a value write wakes nothing\n\tconst watch = (): void => {\n\t\tobserver.disconnect();\n\t\tobserver.observe(host, { childList: true });\n\t\tfor (const child of host.childNodes) {\n\t\t\tif (child === control) continue;\n\n\t\t\tobserver.observe(child, {\n\t\t\t\tattributes: true,\n\t\t\t\tcharacterData: true,\n\t\t\t\tchildList: true,\n\t\t\t\tsubtree: true,\n\t\t\t});\n\t\t}\n\t};\n\tconst observer = new MutationObserver((records) => {\n\t\tconst changes = records.filter((record) => isCopiedChange(host, record, { control, isCopied }));\n\t\tif (changes.length === 0) return;\n\n\t\tif (changes.some((record) => record.target === host)) {\n\t\t\tmirror();\n\t\t\twatch();\n\t\t} else mirror(new Set(changes.map((record) => childHolding(host, record.target))));\n\t});\n\n\tmirror();\n\twatch();\n\tsignal.addEventListener(\n\t\t'abort',\n\t\t() => {\n\t\t\tobserver.disconnect();\n\t\t},\n\t\t{ once: true },\n\t);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mirror-children.js","names":[],"sources":["../../../src/lib/mirror-children.ts"],"sourcesContent":["import type { MirrorSlots } from '#lib/render.ts';\n\nimport { appendOnce, checkChildren } from '#lib/owned-control.ts';\nimport { attachSlots } from '#lib/render.ts';\n\ndeclare const __DEV__: boolean;\n\nconst checkedMirrors = new WeakSet<object>();\n\nconst hostSlots = new WeakMap<HTMLElement, MirrorSlots>();\n\nconst interactive = 'button, input, select, textarea, [tabindex]';\n\nconst link = 'a[href]';\n\ninterface Mirror {\n\tcanCopyLinks?: boolean;\n\tcontrol: Element;\n\tcopy: (original: ChildNode) => Node | undefined;\n\tisCopied?: (child: Node) => boolean;\n\tisPassed?: (child: Node) => boolean;\n\tplace: (copies: Array<Node>) => void;\n}\n\nfunction unmirrorable(original: Node, selector: string): Element | undefined {\n\tif (!(original instanceof Element)) return undefined;\n\n\treturn [original, ...original.querySelectorAll('*')].find(\n\t\t(element) => element.matches(selector) || element.localName.includes('-'),\n\t);\n}\n\nfunction checkMirrored(host: HTMLElement, originals: Array<Node>, canCopyLinks: boolean): void {\n\tif (!__DEV__ || checkedMirrors.has(host.constructor)) return;\n\n\tconst selector = canCopyLinks ? interactive : `${link}, ${interactive}`;\n\tconst [found] = originals.flatMap((original) => unmirrorable(original, selector) ?? []);\n\tif (!found) return;\n\n\tcheckedMirrors.add(host.constructor);\n\tconsole.warn(\n\t\t`<${host.localName}> copies its children into the control, so the <${found.localName}> inside it loses its listeners and state; keep mirrored children static`,\n\t);\n}\n\nfunction childHolding(host: HTMLElement, node: Node): Node | undefined {\n\tlet current: Node | null = node;\n\n\twhile (current && current.parentNode !== host) current = current.parentNode;\n\n\treturn current ?? undefined;\n}\n\nfunction isCopiedChange(\n\thost: HTMLElement,\n\trecord: MutationRecord,\n\t{ control, isCopied }: Required<Pick<Mirror, 'control' | 'isCopied'>>,\n): boolean {\n\tif (record.target === host) {\n\t\treturn (\n\t\t\trecord.removedNodes.length > 0 || [...record.addedNodes].some((node) => node !== control)\n\t\t);\n\t}\n\n\tconst child = childHolding(host, record.target);\n\n\tif (child === undefined) return false;\n\n\treturn isCopied(child) || (record.type === 'attributes' && record.target === child);\n}\n\nexport function mirrorChildren(\n\thost: HTMLElement,\n\t{\n\t\tcanCopyLinks = false,\n\t\tcontrol,\n\t\tcopy,\n\t\tisCopied = () => true,\n\t\tisPassed = () => false,\n\t\tplace,\n\t}: Mirror,\n\tsignal: AbortSignal,\n): void {\n\tconst slots = hostSlots.get(host) ?? attachSlots(host);\n\tlet copies = new Map<ChildNode, Node | undefined>();\n\n\thostSlots.set(host, slots);\n\tconst mirror = (touched?: Set<Node | undefined>): void => {\n\t\tappendOnce(host, control);\n\n\t\tconst children = [...host.childNodes].filter((child) => child !== control);\n\t\tconst slotted = children.filter(\n\t\t\t(child): child is Element | Text => child instanceof Element || child instanceof Text,\n\t\t);\n\t\tconst kept = copies;\n\t\tconst originals = children.filter((child) => isCopied(child));\n\n\t\tif (__DEV__) {\n\t\t\tcheckMirrored(host, originals, canCopyLinks);\n\t\t\tcheckChildren(\n\t\t\t\thost,\n\t\t\t\tchildren.filter((child) => !isCopied(child) && !isPassed(child)),\n\t\t\t);\n\t\t}\n\t\tcopies = new Map(\n\t\t\toriginals.map((original) => [\n\t\t\t\toriginal,\n\t\t\t\ttouched && !touched.has(original) && kept.has(original)\n\t\t\t\t\t? kept.get(original)\n\t\t\t\t\t: copy(original),\n\t\t\t]),\n\t\t);\n\t\tplace([...copies.values()].filter((made) => made !== undefined));\n\t\tslots.shown.assign(...slotted.filter((child) => !isCopied(child)), control);\n\t\tslots.kept.assign(...slotted.filter((child) => isCopied(child)));\n\t};\n\t// Each child is watched, never the control, so a value write wakes nothing\n\tconst watch = (): void => {\n\t\tobserver.disconnect();\n\t\tobserver.observe(host, { childList: true });\n\t\tfor (const child of host.childNodes) {\n\t\t\tif (child === control) continue;\n\n\t\t\tobserver.observe(child, {\n\t\t\t\tattributes: true,\n\t\t\t\tcharacterData: true,\n\t\t\t\tchildList: true,\n\t\t\t\tsubtree: true,\n\t\t\t});\n\t\t}\n\t};\n\tconst observer = new MutationObserver((records) => {\n\t\tconst changes = records.filter((record) => isCopiedChange(host, record, { control, isCopied }));\n\t\tif (changes.length === 0) return;\n\n\t\tif (changes.some((record) => record.target === host)) {\n\t\t\tmirror();\n\t\t\twatch();\n\t\t} else mirror(new Set(changes.map((record) => childHolding(host, record.target))));\n\t});\n\n\tmirror();\n\twatch();\n\tsignal.addEventListener(\n\t\t'abort',\n\t\t() => {\n\t\t\tobserver.disconnect();\n\t\t},\n\t\t{ once: true },\n\t);\n}\n"],"mappings":"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"}
@@ -5,6 +5,7 @@ function capturePointer(target, pointerId) {
5
5
  } catch {}
6
6
  }
7
7
  function bindDrag(target, handlers, signal) {
8
+ const isCaptureLate = handlers.isCaptureLate === true;
8
9
  let held;
9
10
  const end = () => {
10
11
  if (!held) return;
@@ -28,14 +29,19 @@ function bindDrag(target, handlers, signal) {
28
29
  pointerId: event.pointerId
29
30
  };
30
31
  handlers.toggle?.(true);
31
- capturePointer(target, event.pointerId);
32
+ if (!isCaptureLate) capturePointer(target, event.pointerId);
32
33
  }, { signal });
33
34
  whileHeld("pointermove", handlers.move);
34
35
  whileHeld("pointercancel", (drag) => {
35
36
  handlers.cancel?.(drag);
36
37
  end();
37
38
  });
38
- whileHeld("lostpointercapture", end);
39
+ whileHeld("lostpointercapture", (_drag, event) => {
40
+ if (!isCaptureLate || event.target === target) end();
41
+ });
42
+ if (isCaptureLate) whileHeld("pointerleave", (_drag, event) => {
43
+ if (!target.hasPointerCapture(event.pointerId)) end();
44
+ });
39
45
  whileHeld("pointerup", (drag, event) => {
40
46
  end();
41
47
  handlers.lift?.(drag, event);
@@ -1 +1 @@
1
- {"version":3,"file":"pointer-drag.js","names":[],"sources":["../../../src/lib/pointer-drag.ts"],"sourcesContent":["interface DragHandlers<Drag> {\n\tcancel?: (drag: Drag) => void;\n\tgrab: (event: PointerEvent) => Drag | undefined;\n\tlift?: (drag: Drag, event: PointerEvent) => void;\n\tmove: (drag: Drag, event: PointerEvent) => void;\n\trelease?: (drag: Drag) => void;\n\ttoggle?: (isDragging: boolean) => void;\n}\n\nexport interface PointerDrag<Drag> {\n\tcurrent: () => Drag | undefined;\n\tend: () => void;\n}\n\nexport function capturePointer(target: HTMLElement, pointerId: number): void {\n\ttry {\n\t\ttarget.setPointerCapture(pointerId);\n\t} catch {\n\t\t// A synthetic pointer has no id to capture; the gesture carries on uncaptured\n\t}\n}\n\nexport function bindDrag<Drag>(\n\ttarget: HTMLElement,\n\thandlers: DragHandlers<Drag>,\n\tsignal: AbortSignal,\n): PointerDrag<Drag> {\n\tlet held: undefined | { drag: Drag; pointerId: number };\n\n\tconst end = (): void => {\n\t\tif (!held) return;\n\n\t\tconst { drag } = held;\n\n\t\theld = undefined;\n\t\thandlers.toggle?.(false);\n\t\thandlers.release?.(drag);\n\t};\n\tconst whileHeld = (\n\t\ttype: 'lostpointercapture' | 'pointercancel' | 'pointermove' | 'pointerup',\n\t\thandle: (drag: Drag, event: PointerEvent) => void,\n\t): void => {\n\t\ttarget.addEventListener(\n\t\t\ttype,\n\t\t\t(event) => {\n\t\t\t\tif (held?.pointerId === event.pointerId) handle(held.drag, event);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t};\n\n\ttarget.addEventListener(\n\t\t'pointerdown',\n\t\t(event) => {\n\t\t\tif (event.button !== 0 || (held && held.pointerId !== event.pointerId)) return;\n\n\t\t\tend();\n\n\t\t\tconst drag = handlers.grab(event);\n\t\t\tif (drag === undefined) return;\n\n\t\t\theld = { drag, pointerId: event.pointerId };\n\t\t\thandlers.toggle?.(true);\n\t\t\tcapturePointer(target, event.pointerId);\n\t\t},\n\t\t{ signal },\n\t);\n\twhileHeld('pointermove', handlers.move);\n\twhileHeld('pointercancel', (drag) => {\n\t\thandlers.cancel?.(drag);\n\t\tend();\n\t});\n\twhileHeld('lostpointercapture', end);\n\twhileHeld('pointerup', (drag, event) => {\n\t\tend();\n\t\thandlers.lift?.(drag, event);\n\t});\n\tsignal.addEventListener('abort', end, { once: true });\n\n\treturn { current: () => held?.drag, end };\n}\n"],"mappings":";AAcA,SAAgB,eAAe,QAAqB,WAAyB;CAC5E,IAAI;EACH,OAAO,kBAAkB,SAAS;CACnC,QAAQ,CAER;AACD;AAEA,SAAgB,SACf,QACA,UACA,QACoB;CACpB,IAAI;CAEJ,MAAM,YAAkB;EACvB,IAAI,CAAC,MAAM;EAEX,MAAM,EAAE,SAAS;EAEjB,OAAO;EACP,SAAS,SAAS,KAAK;EACvB,SAAS,UAAU,IAAI;CACxB;CACA,MAAM,aACL,MACA,WACU;EACV,OAAO,iBACN,OACC,UAAU;GACV,IAAI,MAAM,cAAc,MAAM,WAAW,OAAO,KAAK,MAAM,KAAK;EACjE,GACA,EAAE,OAAO,CACV;CACD;CAEA,OAAO,iBACN,gBACC,UAAU;EACV,IAAI,MAAM,WAAW,KAAM,QAAQ,KAAK,cAAc,MAAM,WAAY;EAExE,IAAI;EAEJ,MAAM,OAAO,SAAS,KAAK,KAAK;EAChC,IAAI,SAAS,QAAW;EAExB,OAAO;GAAE;GAAM,WAAW,MAAM;EAAU;EAC1C,SAAS,SAAS,IAAI;EACtB,eAAe,QAAQ,MAAM,SAAS;CACvC,GACA,EAAE,OAAO,CACV;CACA,UAAU,eAAe,SAAS,IAAI;CACtC,UAAU,kBAAkB,SAAS;EACpC,SAAS,SAAS,IAAI;EACtB,IAAI;CACL,CAAC;CACD,UAAU,sBAAsB,GAAG;CACnC,UAAU,cAAc,MAAM,UAAU;EACvC,IAAI;EACJ,SAAS,OAAO,MAAM,KAAK;CAC5B,CAAC;CACD,OAAO,iBAAiB,SAAS,KAAK,EAAE,MAAM,KAAK,CAAC;CAEpD,OAAO;EAAE,eAAe,MAAM;EAAM;CAAI;AACzC"}
1
+ {"version":3,"file":"pointer-drag.js","names":[],"sources":["../../../src/lib/pointer-drag.ts"],"sourcesContent":["interface DragHandlers<Drag> {\n\tcancel?: (drag: Drag) => void;\n\tgrab: (event: PointerEvent) => Drag | undefined;\n\t// Capturing on press would swallow a click on a link inside the target; the caller captures once the press is a drag\n\tisCaptureLate?: boolean;\n\tlift?: (drag: Drag, event: PointerEvent) => void;\n\tmove: (drag: Drag, event: PointerEvent) => void;\n\trelease?: (drag: Drag) => void;\n\ttoggle?: (isDragging: boolean) => void;\n}\n\nexport interface PointerDrag<Drag> {\n\tcurrent: () => Drag | undefined;\n\tend: () => void;\n}\n\nexport function capturePointer(target: HTMLElement, pointerId: number): void {\n\ttry {\n\t\ttarget.setPointerCapture(pointerId);\n\t} catch {\n\t\t// A synthetic pointer has no id to capture; the gesture carries on uncaptured\n\t}\n}\n\nexport function bindDrag<Drag>(\n\ttarget: HTMLElement,\n\thandlers: DragHandlers<Drag>,\n\tsignal: AbortSignal,\n): PointerDrag<Drag> {\n\tconst isCaptureLate = handlers.isCaptureLate === true;\n\tlet held: undefined | { drag: Drag; pointerId: number };\n\n\tconst end = (): void => {\n\t\tif (!held) return;\n\n\t\tconst { drag } = held;\n\n\t\theld = undefined;\n\t\thandlers.toggle?.(false);\n\t\thandlers.release?.(drag);\n\t};\n\tconst whileHeld = (\n\t\ttype: 'lostpointercapture' | 'pointercancel' | 'pointerleave' | 'pointermove' | 'pointerup',\n\t\thandle: (drag: Drag, event: PointerEvent) => void,\n\t): void => {\n\t\ttarget.addEventListener(\n\t\t\ttype,\n\t\t\t(event) => {\n\t\t\t\tif (held?.pointerId === event.pointerId) handle(held.drag, event);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t};\n\n\ttarget.addEventListener(\n\t\t'pointerdown',\n\t\t(event) => {\n\t\t\tif (event.button !== 0 || (held && held.pointerId !== event.pointerId)) return;\n\n\t\t\tend();\n\n\t\t\tconst drag = handlers.grab(event);\n\t\t\tif (drag === undefined) return;\n\n\t\t\theld = { drag, pointerId: event.pointerId };\n\t\t\thandlers.toggle?.(true);\n\t\t\tif (!isCaptureLate) capturePointer(target, event.pointerId);\n\t\t},\n\t\t{ signal },\n\t);\n\twhileHeld('pointermove', handlers.move);\n\twhileHeld('pointercancel', (drag) => {\n\t\thandlers.cancel?.(drag);\n\t\tend();\n\t});\n\t// Capturing late takes a touch away from the child it landed on, and that child's `lostpointercapture` bubbles up here\n\twhileHeld('lostpointercapture', (_drag, event) => {\n\t\tif (!isCaptureLate || event.target === target) end();\n\t});\n\tif (isCaptureLate) {\n\t\twhileHeld('pointerleave', (_drag, event) => {\n\t\t\tif (!target.hasPointerCapture(event.pointerId)) end();\n\t\t});\n\t}\n\twhileHeld('pointerup', (drag, event) => {\n\t\tend();\n\t\thandlers.lift?.(drag, event);\n\t});\n\tsignal.addEventListener('abort', end, { once: true });\n\n\treturn { current: () => held?.drag, end };\n}\n"],"mappings":";AAgBA,SAAgB,eAAe,QAAqB,WAAyB;CAC5E,IAAI;EACH,OAAO,kBAAkB,SAAS;CACnC,QAAQ,CAER;AACD;AAEA,SAAgB,SACf,QACA,UACA,QACoB;CACpB,MAAM,gBAAgB,SAAS,kBAAkB;CACjD,IAAI;CAEJ,MAAM,YAAkB;EACvB,IAAI,CAAC,MAAM;EAEX,MAAM,EAAE,SAAS;EAEjB,OAAO;EACP,SAAS,SAAS,KAAK;EACvB,SAAS,UAAU,IAAI;CACxB;CACA,MAAM,aACL,MACA,WACU;EACV,OAAO,iBACN,OACC,UAAU;GACV,IAAI,MAAM,cAAc,MAAM,WAAW,OAAO,KAAK,MAAM,KAAK;EACjE,GACA,EAAE,OAAO,CACV;CACD;CAEA,OAAO,iBACN,gBACC,UAAU;EACV,IAAI,MAAM,WAAW,KAAM,QAAQ,KAAK,cAAc,MAAM,WAAY;EAExE,IAAI;EAEJ,MAAM,OAAO,SAAS,KAAK,KAAK;EAChC,IAAI,SAAS,QAAW;EAExB,OAAO;GAAE;GAAM,WAAW,MAAM;EAAU;EAC1C,SAAS,SAAS,IAAI;EACtB,IAAI,CAAC,eAAe,eAAe,QAAQ,MAAM,SAAS;CAC3D,GACA,EAAE,OAAO,CACV;CACA,UAAU,eAAe,SAAS,IAAI;CACtC,UAAU,kBAAkB,SAAS;EACpC,SAAS,SAAS,IAAI;EACtB,IAAI;CACL,CAAC;CAED,UAAU,uBAAuB,OAAO,UAAU;EACjD,IAAI,CAAC,iBAAiB,MAAM,WAAW,QAAQ,IAAI;CACpD,CAAC;CACD,IAAI,eACH,UAAU,iBAAiB,OAAO,UAAU;EAC3C,IAAI,CAAC,OAAO,kBAAkB,MAAM,SAAS,GAAG,IAAI;CACrD,CAAC;CAEF,UAAU,cAAc,MAAM,UAAU;EACvC,IAAI;EACJ,SAAS,OAAO,MAAM,KAAK;CAC5B,CAAC;CACD,OAAO,iBAAiB,SAAS,KAAK,EAAE,MAAM,KAAK,CAAC;CAEpD,OAAO;EAAE,eAAe,MAAM;EAAM;CAAI;AACzC"}
@@ -1,7 +1,7 @@
1
1
  //#region src/lib/render.ts
2
2
  function requireChild(root, selector, child) {
3
3
  const found = root.querySelector(selector);
4
- if (!(found instanceof child)) throw new Error(`A template is missing its ${selector}`);
4
+ if (!(found instanceof child)) throw new TypeError(`A template is missing its ${selector}`);
5
5
  return found;
6
6
  }
7
7
  function template(html, root) {
@@ -9,7 +9,7 @@ function template(html, root) {
9
9
  parsed.innerHTML = html;
10
10
  return () => {
11
11
  const clone = document.importNode(parsed.content, true).firstElementChild;
12
- if (!(clone instanceof root)) throw new Error(`A template's root is not a ${root.name}`);
12
+ if (!(clone instanceof root)) throw new TypeError(`A template's root is not a ${root.name}`);
13
13
  return clone;
14
14
  };
15
15
  }
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","names":[],"sources":["../../../src/lib/render.ts"],"sourcesContent":["export function requireChild<Child extends Element>(\n\troot: Element,\n\tselector: string,\n\tchild: new () => Child,\n): Child {\n\tconst found = root.querySelector(selector);\n\tif (!(found instanceof child)) throw new Error(`A template is missing its ${selector}`);\n\n\treturn found;\n}\n\nexport function template<Root extends Element>(html: string, root: new () => Root): () => Root {\n\tconst parsed = document.createElement('template');\n\n\tparsed.innerHTML = html;\n\n\treturn () => {\n\t\tconst clone = document.importNode(parsed.content, true).firstElementChild;\n\t\tif (!(clone instanceof root)) throw new Error(`A template's root is not a ${root.name}`);\n\n\t\treturn clone;\n\t};\n}\n\nexport function placeChildren(parent: Element, children: Array<Node>): void {\n\tconst dropped = [...parent.childNodes].filter((child) => !children.includes(child));\n\n\tfor (const child of dropped) child.remove();\n\tfor (const [index, child] of children.entries()) {\n\t\tconst current = parent.childNodes[index];\n\n\t\t// A node taken out and put back restarts its animations\n\t\tif (current === child) continue;\n\n\t\tif (current) current.before(child);\n\t\telse parent.append(child);\n\t}\n}\n\nexport interface MirrorSlots {\n\tkept: HTMLSlotElement;\n\tshown: HTMLSlotElement;\n}\n\n// An id resolves to the first element in the tree that carries it, and paints nothing when that one has no box\nexport function attachSlots(host: HTMLElement): MirrorSlots {\n\tconst shown = document.createElement('slot');\n\tconst kept = document.createElement('slot');\n\n\tkept.style.cssText =\n\t\t'position: absolute; display: block; inline-size: 0; block-size: 0; overflow: hidden; visibility: hidden';\n\thost.attachShadow({ mode: 'open', slotAssignment: 'manual' }).append(shown, kept);\n\n\treturn { kept, shown };\n}\n"],"mappings":";AAAA,SAAgB,aACf,MACA,UACA,OACQ;CACR,MAAM,QAAQ,KAAK,cAAc,QAAQ;CACzC,IAAI,EAAE,iBAAiB,QAAQ,MAAM,IAAI,MAAM,6BAA6B,UAAU;CAEtF,OAAO;AACR;AAEA,SAAgB,SAA+B,MAAc,MAAkC;CAC9F,MAAM,SAAS,SAAS,cAAc,UAAU;CAEhD,OAAO,YAAY;CAEnB,aAAa;EACZ,MAAM,QAAQ,SAAS,WAAW,OAAO,SAAS,IAAI,CAAC,CAAC;EACxD,IAAI,EAAE,iBAAiB,OAAO,MAAM,IAAI,MAAM,8BAA8B,KAAK,MAAM;EAEvF,OAAO;CACR;AACD;AAEA,SAAgB,cAAc,QAAiB,UAA6B;CAC3E,MAAM,UAAU,CAAC,GAAG,OAAO,UAAU,CAAC,CAAC,QAAQ,UAAU,CAAC,SAAS,SAAS,KAAK,CAAC;CAElF,KAAK,MAAM,SAAS,SAAS,MAAM,OAAO;CAC1C,KAAK,MAAM,CAAC,OAAO,UAAU,SAAS,QAAQ,GAAG;EAChD,MAAM,UAAU,OAAO,WAAW;EAGlC,IAAI,YAAY,OAAO;EAEvB,IAAI,SAAS,QAAQ,OAAO,KAAK;OAC5B,OAAO,OAAO,KAAK;CACzB;AACD;AAQA,SAAgB,YAAY,MAAgC;CAC3D,MAAM,QAAQ,SAAS,cAAc,MAAM;CAC3C,MAAM,OAAO,SAAS,cAAc,MAAM;CAE1C,KAAK,MAAM,UACV;CACD,KAAK,aAAa;EAAE,MAAM;EAAQ,gBAAgB;CAAS,CAAC,CAAC,CAAC,OAAO,OAAO,IAAI;CAEhF,OAAO;EAAE;EAAM;CAAM;AACtB"}
1
+ {"version":3,"file":"render.js","names":[],"sources":["../../../src/lib/render.ts"],"sourcesContent":["export function requireChild<Child extends Element>(\n\troot: Element,\n\tselector: string,\n\tchild: new () => Child,\n): Child {\n\tconst found = root.querySelector(selector);\n\tif (!(found instanceof child)) throw new TypeError(`A template is missing its ${selector}`);\n\n\treturn found;\n}\n\nexport function template<Root extends Element>(html: string, root: new () => Root): () => Root {\n\tconst parsed = document.createElement('template');\n\n\tparsed.innerHTML = html;\n\n\treturn () => {\n\t\tconst clone = document.importNode(parsed.content, true).firstElementChild;\n\t\tif (!(clone instanceof root)) throw new TypeError(`A template's root is not a ${root.name}`);\n\n\t\treturn clone;\n\t};\n}\n\nexport function placeChildren(parent: Element, children: Array<Node>): void {\n\tconst dropped = [...parent.childNodes].filter((child) => !children.includes(child));\n\n\tfor (const child of dropped) child.remove();\n\tfor (const [index, child] of children.entries()) {\n\t\tconst current = parent.childNodes[index];\n\n\t\t// A node taken out and put back restarts its animations\n\t\tif (current === child) continue;\n\n\t\tif (current) current.before(child);\n\t\telse parent.append(child);\n\t}\n}\n\nexport interface MirrorSlots {\n\tkept: HTMLSlotElement;\n\tshown: HTMLSlotElement;\n}\n\n// An id resolves to the first element in the tree that carries it, and paints nothing when that one has no box\nexport function attachSlots(host: HTMLElement): MirrorSlots {\n\tconst shown = document.createElement('slot');\n\tconst kept = document.createElement('slot');\n\n\tkept.style.cssText =\n\t\t'position: absolute; display: block; inline-size: 0; block-size: 0; overflow: hidden; visibility: hidden';\n\thost.attachShadow({ mode: 'open', slotAssignment: 'manual' }).append(shown, kept);\n\n\treturn { kept, shown };\n}\n"],"mappings":";AAAA,SAAgB,aACf,MACA,UACA,OACQ;CACR,MAAM,QAAQ,KAAK,cAAc,QAAQ;CACzC,IAAI,EAAE,iBAAiB,QAAQ,MAAM,IAAI,UAAU,6BAA6B,UAAU;CAE1F,OAAO;AACR;AAEA,SAAgB,SAA+B,MAAc,MAAkC;CAC9F,MAAM,SAAS,SAAS,cAAc,UAAU;CAEhD,OAAO,YAAY;CAEnB,aAAa;EACZ,MAAM,QAAQ,SAAS,WAAW,OAAO,SAAS,IAAI,CAAC,CAAC;EACxD,IAAI,EAAE,iBAAiB,OAAO,MAAM,IAAI,UAAU,8BAA8B,KAAK,MAAM;EAE3F,OAAO;CACR;AACD;AAEA,SAAgB,cAAc,QAAiB,UAA6B;CAC3E,MAAM,UAAU,CAAC,GAAG,OAAO,UAAU,CAAC,CAAC,QAAQ,UAAU,CAAC,SAAS,SAAS,KAAK,CAAC;CAElF,KAAK,MAAM,SAAS,SAAS,MAAM,OAAO;CAC1C,KAAK,MAAM,CAAC,OAAO,UAAU,SAAS,QAAQ,GAAG;EAChD,MAAM,UAAU,OAAO,WAAW;EAGlC,IAAI,YAAY,OAAO;EAEvB,IAAI,SAAS,QAAQ,OAAO,KAAK;OAC5B,OAAO,OAAO,KAAK;CACzB;AACD;AAQA,SAAgB,YAAY,MAAgC;CAC3D,MAAM,QAAQ,SAAS,cAAc,MAAM;CAC3C,MAAM,OAAO,SAAS,cAAc,MAAM;CAE1C,KAAK,MAAM,UACV;CACD,KAAK,aAAa;EAAE,MAAM;EAAQ,gBAAgB;CAAS,CAAC,CAAC,CAAC,OAAO,OAAO,IAAI;CAEhF,OAAO;EAAE;EAAM;CAAM;AACtB"}
@@ -0,0 +1,55 @@
1
+ import { clamp, clampProportion } from "./math.js";
2
+
3
+ //#region src/lib/ticker-leg.ts
4
+ const fadeApproach = 3;
5
+ function tickerLeg({ distance, hold: fullHold, rate }, { from, heading, hold = fullHold }) {
6
+ if (!(distance > 0) || !(rate > 0)) return void 0;
7
+ const start = clamp(from, 0, distance);
8
+ const isOutbound = start === (heading > 0 ? distance : 0) ? heading < 0 : heading > 0;
9
+ const to = isOutbound ? distance : 0;
10
+ return {
11
+ duration: Math.abs(to - start) / rate * 1e3,
12
+ from: start,
13
+ heading: isOutbound ? 1 : -1,
14
+ hold: Math.max(0, hold),
15
+ to
16
+ };
17
+ }
18
+ function legPosition(leg, elapsed) {
19
+ return leg.from + (leg.to - leg.from) * clampProportion((elapsed - leg.hold) / leg.duration);
20
+ }
21
+ function legDeparture(leg, elapsed) {
22
+ return {
23
+ from: legPosition(leg, elapsed),
24
+ heading: leg.heading,
25
+ hold: Math.max(0, leg.hold - elapsed)
26
+ };
27
+ }
28
+ function clippedEnds(position, distance) {
29
+ const ends = [...position > 0 ? ["start"] : [], ...position < distance ? ["end"] : []];
30
+ return ends.length > 0 ? ends.join(" ") : void 0;
31
+ }
32
+ function fadeWidth(past, fade) {
33
+ return clamp(past / fadeApproach, 0, Math.max(0, fade));
34
+ }
35
+ function legFade(leg, [pastFrom, pastTo], fade) {
36
+ const reach = Math.max(0, fade) * fadeApproach;
37
+ const msPerPx = leg.duration / Math.abs(leg.to - leg.from);
38
+ const from = Math.min(pastFrom, reach);
39
+ const to = Math.min(pastTo, reach);
40
+ return {
41
+ delay: leg.hold + (pastFrom - from) * msPerPx,
42
+ duration: Math.abs(to - from) * msPerPx,
43
+ widths: [from / fadeApproach, to / fadeApproach]
44
+ };
45
+ }
46
+ function legFades(leg, distance, fade) {
47
+ return {
48
+ end: legFade(leg, [distance - leg.from, distance - leg.to], fade),
49
+ start: legFade(leg, [leg.from, leg.to], fade)
50
+ };
51
+ }
52
+
53
+ //#endregion
54
+ export { clippedEnds, fadeWidth, legDeparture, legFades, legPosition, tickerLeg };
55
+ //# sourceMappingURL=ticker-leg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ticker-leg.js","names":[],"sources":["../../../src/lib/ticker-leg.ts"],"sourcesContent":["import { clamp, clampProportion } from '#lib/math.ts';\n\nexport type TickerHeading = -1 | 1;\n\nexport interface TickerTravel {\n\tdistance: number;\n\thold: number;\n\trate: number;\n}\n\nexport interface TickerDeparture {\n\tfrom: number;\n\theading: TickerHeading;\n\thold?: number;\n}\n\nexport interface TickerLeg {\n\tduration: number;\n\tfrom: number;\n\theading: TickerHeading;\n\thold: number;\n\tto: number;\n}\n\nexport interface TickerFade {\n\tdelay: number;\n\tduration: number;\n\twidths: [number, number];\n}\n\nconst fadeApproach = 3;\n\nexport function tickerLeg(\n\t{ distance, hold: fullHold, rate }: TickerTravel,\n\t{ from, heading, hold = fullHold }: TickerDeparture,\n): TickerLeg | undefined {\n\tif (!(distance > 0) || !(rate > 0)) return undefined;\n\n\tconst start = clamp(from, 0, distance);\n\tconst isAtEnd = start === (heading > 0 ? distance : 0);\n\tconst isOutbound = isAtEnd ? heading < 0 : heading > 0;\n\tconst to = isOutbound ? distance : 0;\n\n\treturn {\n\t\tduration: (Math.abs(to - start) / rate) * 1000,\n\t\tfrom: start,\n\t\theading: isOutbound ? 1 : -1,\n\t\thold: Math.max(0, hold),\n\t\tto,\n\t};\n}\n\nexport function legPosition(leg: TickerLeg, elapsed: number): number {\n\treturn leg.from + (leg.to - leg.from) * clampProportion((elapsed - leg.hold) / leg.duration);\n}\n\nexport function legDeparture(leg: TickerLeg, elapsed: number): Required<TickerDeparture> {\n\treturn {\n\t\tfrom: legPosition(leg, elapsed),\n\t\theading: leg.heading,\n\t\thold: Math.max(0, leg.hold - elapsed),\n\t};\n}\n\nexport function clippedEnds(position: number, distance: number): string | undefined {\n\tconst ends = [...(position > 0 ? ['start'] : []), ...(position < distance ? ['end'] : [])];\n\n\treturn ends.length > 0 ? ends.join(' ') : undefined;\n}\n\nexport function fadeWidth(past: number, fade: number): number {\n\treturn clamp(past / fadeApproach, 0, Math.max(0, fade));\n}\n\nfunction legFade(leg: TickerLeg, [pastFrom, pastTo]: [number, number], fade: number): TickerFade {\n\tconst reach = Math.max(0, fade) * fadeApproach;\n\tconst msPerPx = leg.duration / Math.abs(leg.to - leg.from);\n\tconst from = Math.min(pastFrom, reach);\n\tconst to = Math.min(pastTo, reach);\n\n\treturn {\n\t\tdelay: leg.hold + (pastFrom - from) * msPerPx,\n\t\tduration: Math.abs(to - from) * msPerPx,\n\t\twidths: [from / fadeApproach, to / fadeApproach],\n\t};\n}\n\nexport function legFades(\n\tleg: TickerLeg,\n\tdistance: number,\n\tfade: number,\n): Record<'end' | 'start', TickerFade> {\n\treturn {\n\t\tend: legFade(leg, [distance - leg.from, distance - leg.to], fade),\n\t\tstart: legFade(leg, [leg.from, leg.to], fade),\n\t};\n}\n"],"mappings":";;;AA8BA,MAAM,eAAe;AAErB,SAAgB,UACf,EAAE,UAAU,MAAM,UAAU,QAC5B,EAAE,MAAM,SAAS,OAAO,YACA;CACxB,IAAI,EAAE,WAAW,MAAM,EAAE,OAAO,IAAI,OAAO;CAE3C,MAAM,QAAQ,MAAM,MAAM,GAAG,QAAQ;CAErC,MAAM,aADU,WAAW,UAAU,IAAI,WAAW,KACvB,UAAU,IAAI,UAAU;CACrD,MAAM,KAAK,aAAa,WAAW;CAEnC,OAAO;EACN,UAAW,KAAK,IAAI,KAAK,KAAK,IAAI,OAAQ;EAC1C,MAAM;EACN,SAAS,aAAa,IAAI;EAC1B,MAAM,KAAK,IAAI,GAAG,IAAI;EACtB;CACD;AACD;AAEA,SAAgB,YAAY,KAAgB,SAAyB;CACpE,OAAO,IAAI,QAAQ,IAAI,KAAK,IAAI,QAAQ,iBAAiB,UAAU,IAAI,QAAQ,IAAI,QAAQ;AAC5F;AAEA,SAAgB,aAAa,KAAgB,SAA4C;CACxF,OAAO;EACN,MAAM,YAAY,KAAK,OAAO;EAC9B,SAAS,IAAI;EACb,MAAM,KAAK,IAAI,GAAG,IAAI,OAAO,OAAO;CACrC;AACD;AAEA,SAAgB,YAAY,UAAkB,UAAsC;CACnF,MAAM,OAAO,CAAC,GAAI,WAAW,IAAI,CAAC,OAAO,IAAI,CAAC,GAAI,GAAI,WAAW,WAAW,CAAC,KAAK,IAAI,CAAC,CAAE;CAEzF,OAAO,KAAK,SAAS,IAAI,KAAK,KAAK,GAAG,IAAI;AAC3C;AAEA,SAAgB,UAAU,MAAc,MAAsB;CAC7D,OAAO,MAAM,OAAO,cAAc,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC;AACvD;AAEA,SAAS,QAAQ,KAAgB,CAAC,UAAU,SAA2B,MAA0B;CAChG,MAAM,QAAQ,KAAK,IAAI,GAAG,IAAI,IAAI;CAClC,MAAM,UAAU,IAAI,WAAW,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI;CACzD,MAAM,OAAO,KAAK,IAAI,UAAU,KAAK;CACrC,MAAM,KAAK,KAAK,IAAI,QAAQ,KAAK;CAEjC,OAAO;EACN,OAAO,IAAI,QAAQ,WAAW,QAAQ;EACtC,UAAU,KAAK,IAAI,KAAK,IAAI,IAAI;EAChC,QAAQ,CAAC,OAAO,cAAc,KAAK,YAAY;CAChD;AACD;AAEA,SAAgB,SACf,KACA,UACA,MACsC;CACtC,OAAO;EACN,KAAK,QAAQ,KAAK,CAAC,WAAW,IAAI,MAAM,WAAW,IAAI,EAAE,GAAG,IAAI;EAChE,OAAO,QAAQ,KAAK,CAAC,IAAI,MAAM,IAAI,EAAE,GAAG,IAAI;CAC7C;AACD"}
@@ -4,6 +4,7 @@
4
4
  @import './button.css';
5
5
  @import './led.css';
6
6
  @import './switch.css';
7
+ @import './ticker.css';
7
8
  @import './toggle.css';
8
9
  @import './meter.css';
9
10
  @import './number.css';
@@ -346,15 +346,38 @@
346
346
  calc(
347
347
  var(--_sonic-dial-face-drift) * var(--_sonic-dial-angle) - var(--_sonic-light-tilt)
348
348
  ),
349
- color-mix(in srgb, var(--_sonic-shine) calc(16% * var(--_sonic-relief)), transparent),
349
+ color-mix(
350
+ in srgb,
351
+ var(--_sonic-shine) calc(16% * var(--_sonic-relief) * var(--_sonic-gloss)),
352
+ transparent
353
+ ),
350
354
  transparent 12%,
351
- color-mix(in srgb, var(--_sonic-shade) calc(10% * var(--_sonic-relief)), transparent) 25%,
355
+ color-mix(
356
+ in srgb,
357
+ var(--_sonic-shade) calc(10% * var(--_sonic-relief) * var(--_sonic-gloss)),
358
+ transparent
359
+ )
360
+ 25%,
352
361
  transparent 38%,
353
- color-mix(in srgb, var(--_sonic-shine) calc(12% * var(--_sonic-relief)), transparent) 50%,
362
+ color-mix(
363
+ in srgb,
364
+ var(--_sonic-shine) calc(12% * var(--_sonic-relief) * var(--_sonic-gloss)),
365
+ transparent
366
+ )
367
+ 50%,
354
368
  transparent 62%,
355
- color-mix(in srgb, var(--_sonic-shade) calc(10% * var(--_sonic-relief)), transparent) 75%,
369
+ color-mix(
370
+ in srgb,
371
+ var(--_sonic-shade) calc(10% * var(--_sonic-relief) * var(--_sonic-gloss)),
372
+ transparent
373
+ )
374
+ 75%,
356
375
  transparent 88%,
357
- color-mix(in srgb, var(--_sonic-shine) calc(16% * var(--_sonic-relief)), transparent)
376
+ color-mix(
377
+ in srgb,
378
+ var(--_sonic-shine) calc(16% * var(--_sonic-relief) * var(--_sonic-gloss)),
379
+ transparent
380
+ )
358
381
  ),
359
382
  var(--_sonic-face);
360
383
  box-shadow: var(--_sonic-face-shadow);
@@ -22,6 +22,7 @@
22
22
  --_sonic-focus: var(--sonic-focus, oklch(90% 0.23 128deg));
23
23
  --_sonic-focus-strength: var(--sonic-focus-glow, 1);
24
24
  --_sonic-disabled-opacity: var(--sonic-disabled-opacity, 1);
25
+ --_sonic-gloss: var(--sonic-gloss, 1);
25
26
  --_sonic-ink: var(--sonic-ink, currentcolor);
26
27
  --_sonic-etch: color-mix(in oklab, var(--_sonic-ink) 30%, transparent);
27
28
  --_sonic-ink-disabled: var(--sonic-ink-disabled, var(--_sonic-etch));
@@ -56,14 +57,24 @@
56
57
  --_sonic-sheen: linear-gradient(
57
58
  calc(var(--_sonic-light-fall) - 58deg),
58
59
  transparent 20%,
59
- color-mix(in srgb, var(--_sonic-shine) calc(10% * var(--_sonic-relief)), transparent) 45%,
60
+ color-mix(
61
+ in srgb,
62
+ var(--_sonic-shine) calc(10% * var(--_sonic-relief) * var(--_sonic-gloss)),
63
+ transparent
64
+ )
65
+ 45%,
60
66
  transparent 55%,
61
- color-mix(in srgb, var(--_sonic-shade) calc(6% * var(--_sonic-relief)), transparent) 80%
67
+ color-mix(
68
+ in srgb,
69
+ var(--_sonic-shade) calc(6% * var(--_sonic-relief) * var(--_sonic-gloss)),
70
+ transparent
71
+ )
72
+ 80%
62
73
  );
63
74
  --_sonic-press-transition:
64
- scale var(--_sonic-press-duration) cubic-bezier(0.4, 0, 0.2, 1),
65
- translate var(--_sonic-press-duration) cubic-bezier(0.4, 0, 0.2, 1),
66
- box-shadow var(--_sonic-press-duration) cubic-bezier(0.4, 0, 0.2, 1);
75
+ scale var(--_sonic-press-duration) var(--_sonic-ease),
76
+ translate var(--_sonic-press-duration) var(--_sonic-ease),
77
+ box-shadow var(--_sonic-press-duration) var(--_sonic-ease);
67
78
  --_sonic-face-shadow:
68
79
  inset var(--_sonic-cast-x) var(--_sonic-cast-y) calc(1.5 * var(--_sonic-unit))
69
80
  color-mix(in srgb, var(--_sonic-shade) calc(35% * var(--_sonic-relief)), transparent),
@@ -44,6 +44,7 @@
44
44
  --_sonic-shine: var(--sonic-shine, #fff);
45
45
  --_sonic-press-duration: var(--sonic-press-duration, 60ms);
46
46
  --_sonic-fade-duration: var(--sonic-fade-duration, 120ms);
47
+ --_sonic-ease: cubic-bezier(0.4, 0, 0.2, 1);
47
48
  --_sonic-light-tilt: var(--sonic-light-tilt, 17deg);
48
49
  --_sonic-light-fall: calc(180deg - var(--_sonic-light-tilt));
49
50
  --_sonic-cast-x: calc(sin(var(--_sonic-light-tilt)) * var(--_sonic-unit));
@@ -61,6 +61,7 @@
61
61
  }
62
62
 
63
63
  .sonic-slider {
64
+ --_sonic-gloss: var(--sonic-gloss, 0);
64
65
  --_sonic-slider-groove-rest: var(--sonic-slider-groove-ratio, 0.15);
65
66
  --_sonic-slider-groove-hover-delay: 0s;
66
67
  --_sonic-slider-groove-ratio: calc(
@@ -482,7 +483,7 @@
482
483
 
483
484
  .sonic-slider-cap::before {
484
485
  inset: var(--_sonic-slider-bevel);
485
- background: var(--_sonic-face);
486
+ background: var(--_sonic-sheen), var(--_sonic-face);
486
487
  border-radius: max(0px, var(--_sonic-slider-corner) - var(--_sonic-slider-bevel));
487
488
  box-shadow: var(--_sonic-face-shadow);
488
489
  }
@@ -60,6 +60,8 @@
60
60
  var(--_sonic-switch-cap) calc(100% - 20% * var(--_sonic-relief)),
61
61
  var(--_sonic-shade)
62
62
  );
63
+ --_sonic-switch-ease: cubic-bezier(0.34, 1.3, 0.64, 1);
64
+ --_sonic-switch-duration: 120ms;
63
65
 
64
66
  display: inline-grid;
65
67
  flex-shrink: 0;
@@ -68,7 +70,7 @@
68
70
  line-height: 1;
69
71
  -webkit-user-select: none;
70
72
  user-select: none;
71
- transition: --_sonic-switch-at 90ms cubic-bezier(0.4, 0, 0.2, 1);
73
+ transition: --_sonic-switch-at var(--_sonic-switch-duration) var(--_sonic-switch-ease);
72
74
  }
73
75
 
74
76
  @media (prefers-reduced-motion: reduce) {
@@ -0,0 +1,115 @@
1
+ @layer sonic.material, sonic.control;
2
+
3
+ @property --_sonic-ticker-rate {
4
+ syntax: '<length>';
5
+ inherits: false;
6
+ initial-value: 40px;
7
+ }
8
+
9
+ @property --_sonic-ticker-hold {
10
+ syntax: '<time>';
11
+ inherits: false;
12
+ initial-value: 2s;
13
+ }
14
+
15
+ @property --_sonic-ticker-fade {
16
+ syntax: '<length>';
17
+ inherits: false;
18
+ initial-value: 12px;
19
+ }
20
+
21
+ @property --_sonic-ticker-fade-start {
22
+ syntax: '<length>';
23
+ inherits: false;
24
+ initial-value: 0;
25
+ }
26
+
27
+ @property --_sonic-ticker-fade-end {
28
+ syntax: '<length>';
29
+ inherits: false;
30
+ initial-value: 0;
31
+ }
32
+
33
+ @property --_sonic-ticker-cancel-zone {
34
+ syntax: '<length>';
35
+ inherits: false;
36
+ initial-value: 48px;
37
+ }
38
+
39
+ @layer sonic.control {
40
+ :has(> .sonic-ticker):not([hidden]) {
41
+ display: contents;
42
+ }
43
+
44
+ :is(sonic-ticker:not(:defined, [hidden]), .sonic-ticker) {
45
+ display: block;
46
+ min-inline-size: 0;
47
+ max-inline-size: 100%;
48
+ overflow: hidden;
49
+ text-overflow: ellipsis;
50
+ white-space: nowrap;
51
+ }
52
+
53
+ .sonic-ticker {
54
+ --_sonic-ticker-rate: var(--sonic-ticker-rate, 40px);
55
+ --_sonic-ticker-hold: var(--sonic-ticker-hold, 2s);
56
+ --_sonic-ticker-fade: var(--sonic-ticker-fade, 0.75rem);
57
+ --_sonic-ticker-cancel-zone: var(--sonic-cancel-zone, 48px);
58
+
59
+ -webkit-user-select: none;
60
+
61
+ user-select: none;
62
+ }
63
+
64
+ .sonic-ticker[data-sonic-overflowing] {
65
+ text-overflow: clip;
66
+ touch-action: var(--sonic-touch-action, pan-y);
67
+ -webkit-mask: linear-gradient(
68
+ to right,
69
+ transparent,
70
+ #000 var(--_sonic-ticker-fade-start),
71
+ #000 calc(100% - var(--_sonic-ticker-fade-end)),
72
+ transparent
73
+ );
74
+ mask: linear-gradient(
75
+ to right,
76
+ transparent,
77
+ #000 var(--_sonic-ticker-fade-start),
78
+ #000 calc(100% - var(--_sonic-ticker-fade-end)),
79
+ transparent
80
+ );
81
+ }
82
+
83
+ .sonic-ticker[data-sonic-overflowing] > .sonic-ticker-tape {
84
+ display: inline-block;
85
+ }
86
+
87
+ .sonic-ticker[data-sonic-overflowing]:not(:any-link *) {
88
+ cursor: grab;
89
+ }
90
+
91
+ .sonic-ticker[data-sonic-overflowing][data-sonic-dragging],
92
+ .sonic-ticker[data-sonic-dragging] :any-link {
93
+ cursor: grabbing;
94
+ }
95
+
96
+ @media (prefers-reduced-motion: reduce) {
97
+ .sonic-ticker[data-sonic-overflowing]:not(
98
+ [data-sonic-clipped~='start'],
99
+ [data-sonic-dragging]
100
+ ) {
101
+ text-overflow: ellipsis;
102
+ -webkit-mask: none;
103
+ mask: none;
104
+ }
105
+
106
+ .sonic-ticker[data-sonic-overflowing]:not([data-sonic-clipped~='start'],[data-sonic-dragging]) > .sonic-ticker-tape {
107
+ display: inline;
108
+ }
109
+ }
110
+
111
+ .sonic-ticker:focus-within {
112
+ -webkit-mask: none;
113
+ mask: none;
114
+ }
115
+ }
@@ -96,6 +96,8 @@
96
96
  --_sonic-keycap-corner: max(0px, var(--_sonic-toggle-corner) - var(--_sonic-toggle-gap));
97
97
  --_sonic-keycap-bevel: calc(var(--_sonic-toggle-size) * var(--_sonic-toggle-bevel-ratio));
98
98
  --_sonic-keycap-face-corner: max(0px, var(--_sonic-keycap-corner) - var(--_sonic-keycap-bevel));
99
+ --_sonic-toggle-ease: cubic-bezier(0.34, 1.3, 0.64, 1);
100
+ --_sonic-toggle-duration: 120ms;
99
101
 
100
102
  position: relative;
101
103
  display: inline-grid;
@@ -108,8 +110,8 @@
108
110
  -webkit-user-select: none;
109
111
  user-select: none;
110
112
  transition:
111
- --_sonic-toggle-at 120ms cubic-bezier(0.4, 0, 0.2, 1),
112
- --_sonic-toggle-sink var(--_sonic-press-duration) cubic-bezier(0.4, 0, 0.2, 1);
113
+ --_sonic-toggle-at var(--_sonic-toggle-duration) var(--_sonic-toggle-ease),
114
+ --_sonic-toggle-sink var(--_sonic-press-duration) var(--_sonic-ease);
113
115
  }
114
116
 
115
117
  [orientation='vertical'] > .sonic-toggle {
@@ -124,7 +126,7 @@
124
126
  }
125
127
 
126
128
  .sonic-toggle[data-sonic-dragging] {
127
- transition: --_sonic-toggle-sink var(--_sonic-press-duration) cubic-bezier(0.4, 0, 0.2, 1);
129
+ transition: --_sonic-toggle-sink var(--_sonic-press-duration) var(--_sonic-ease);
128
130
  }
129
131
 
130
132
  [data-sonic-lit='ok'] > .sonic-toggle:has(> [role='switch']),
@@ -221,8 +223,8 @@
221
223
  translate: calc(var(--_sonic-toggle-along-x) * var(--_sonic-toggle-throw))
222
224
  calc(var(--_sonic-toggle-along-y) * var(--_sonic-toggle-throw) + var(--_sonic-toggle-sink));
223
225
  transition:
224
- scale var(--_sonic-press-duration) cubic-bezier(0.4, 0, 0.2, 1),
225
- box-shadow var(--_sonic-press-duration) cubic-bezier(0.4, 0, 0.2, 1);
226
+ scale var(--_sonic-press-duration) var(--_sonic-ease),
227
+ box-shadow var(--_sonic-press-duration) var(--_sonic-ease);
226
228
  }
227
229
 
228
230
  .sonic-toggle-cap > .sonic-led {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xsynaptic/sonic-ui",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Native custom elements for audio interfaces, skinned with CSS custom properties",
5
5
  "keywords": [
6
6
  "custom-elements",
@@ -65,6 +65,7 @@
65
65
  "./slider.css": "./dist/styles/slider.css",
66
66
  "./spectrum.css": "./dist/styles/spectrum.css",
67
67
  "./switch.css": "./dist/styles/switch.css",
68
+ "./ticker.css": "./dist/styles/ticker.css",
68
69
  "./toggle.css": "./dist/styles/toggle.css",
69
70
  "./waveform.css": "./dist/styles/waveform.css",
70
71
  "./wavestrip.css": "./dist/styles/wavestrip.css",