PadPicker.svelte 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. <script lang="ts">
  2. import type { Pad } from "./api";
  3. import { transfer, transferKind } from "./pads";
  4. import { BANKS, PER_BANK } from "./slots";
  5. import type { Picker } from "./store.svelte";
  6. // "Move to pad" / "Copy to pad": the keyboard-friendly route to what
  7. // dragging does.
  8. let { picker, pads, onclose }: { picker: Picker; pads: Pad[]; onclose: () => void } = $props();
  9. const src = $derived(pads[picker.from]);
  10. // Start on the first empty pad after the source, else the next pad.
  11. function firstTarget(): number {
  12. for (let i = 1; i < pads.length; i++) {
  13. const s = (picker.from + i) % pads.length;
  14. if (!pads[s].file) return s;
  15. }
  16. return (picker.from + 1) % pads.length;
  17. }
  18. const start = firstTarget();
  19. let bank = $state(Math.floor(start / PER_BANK));
  20. let pad = $state(start % PER_BANK);
  21. const target = $derived(bank * PER_BANK + pad);
  22. const kind = $derived(transferKind(src, pads[target], picker.kind === "copy"));
  23. const action = $derived(
  24. target === picker.from
  25. ? ""
  26. : kind === "move"
  27. ? "Move"
  28. : kind === "swap"
  29. ? "Swap"
  30. : pads[target].file
  31. ? "Copy and replace"
  32. : "Copy",
  33. );
  34. let firstField: HTMLSelectElement | undefined = $state();
  35. $effect(() => {
  36. firstField?.focus();
  37. });
  38. async function go() {
  39. if (!action) return;
  40. // Read everything before closing: closing clears ui.picker, and props
  41. // are live, so `picker` is null from then on.
  42. const what = kind;
  43. const from = picker.from;
  44. const to = target;
  45. onclose();
  46. await transfer(what, from, to);
  47. }
  48. function keydown(e: KeyboardEvent) {
  49. if (e.key === "Escape") onclose();
  50. }
  51. </script>
  52. <svelte:window onkeydown={keydown} />
  53. <div class="backdrop">
  54. <div class="dialog" role="dialog" aria-modal="true" aria-labelledby="picker-title">
  55. <h2 id="picker-title">{picker.kind === "move" ? "Move" : "Copy"} {src.label} to pad</h2>
  56. <p class="from">{src.name}</p>
  57. <div class="fields">
  58. <div class="control">
  59. <label class="label" for="picker-bank">Bank</label>
  60. <select id="picker-bank" class="field" bind:value={bank} bind:this={firstField}>
  61. {#each BANKS.split("") as letter, i (letter)}
  62. <option value={i}>{letter}</option>
  63. {/each}
  64. </select>
  65. </div>
  66. <div class="control">
  67. <label class="label" for="picker-pad">Pad</label>
  68. <select id="picker-pad" class="field" bind:value={pad}>
  69. {#each Array.from({ length: PER_BANK }, (_, i) => i) as i (i)}
  70. {@const p = pads[bank * PER_BANK + i]}
  71. <option value={i}>{p.label}{p.file ? " · " + p.name : " · empty"}</option>
  72. {/each}
  73. </select>
  74. </div>
  75. </div>
  76. <p class="hint">
  77. {#if !action}
  78. Choose a different pad.
  79. {:else if kind === "swap"}
  80. {pads[target].label} has a sample: the two pads will swap.
  81. {:else if kind === "copy" && pads[target].file}
  82. {pads[target].label} will be replaced.
  83. {:else}
  84. &nbsp;
  85. {/if}
  86. </p>
  87. <div class="buttons">
  88. <button type="button" class="btn secondary" onclick={onclose}>Cancel</button>
  89. <button type="button" class="btn primary" disabled={!action} onclick={go}
  90. >{action || (picker.kind === "move" ? "Move" : "Copy")}</button
  91. >
  92. </div>
  93. </div>
  94. </div>
  95. <style>
  96. .backdrop {
  97. position: fixed;
  98. inset: 0;
  99. padding: 24px;
  100. display: flex;
  101. align-items: center;
  102. justify-content: center;
  103. background: rgba(10, 11, 13, 0.78);
  104. }
  105. .dialog {
  106. width: 100%;
  107. max-width: 440px;
  108. padding: 24px;
  109. display: flex;
  110. flex-direction: column;
  111. gap: 12px;
  112. background: var(--surface);
  113. border: 1px solid var(--edge);
  114. border-radius: 12px;
  115. }
  116. h2 {
  117. margin: 0;
  118. font-family: var(--display);
  119. font-size: 20px;
  120. font-weight: 700;
  121. }
  122. .from {
  123. margin: 0;
  124. color: var(--soft);
  125. overflow-wrap: anywhere;
  126. }
  127. .fields {
  128. display: grid;
  129. grid-template-columns: 1fr 2fr;
  130. gap: 10px;
  131. }
  132. .buttons {
  133. display: flex;
  134. justify-content: flex-end;
  135. gap: 8px;
  136. }
  137. </style>