recipe-planner-ui 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/cjs/loader.cjs.js +1 -1
  2. package/dist/cjs/recipe-planner-ui.cjs.js +1 -1
  3. package/dist/cjs/rp-day-slot.cjs.entry.js +407 -17
  4. package/dist/cjs/rp-filter-chips_6.cjs.entry.js +132 -8
  5. package/dist/collection/components/rp-day-slot/rp-day-slot.css +43 -3
  6. package/dist/collection/components/rp-day-slot/rp-day-slot.js +408 -17
  7. package/dist/collection/components/rp-modal/rp-modal.css +1 -1
  8. package/dist/collection/components/rp-modal/rp-modal.js +43 -0
  9. package/dist/collection/components/rp-recipe-card/rp-recipe-card.css +13 -5
  10. package/dist/collection/components/rp-select/rp-select.css +5 -0
  11. package/dist/collection/components/rp-select/rp-select.js +86 -5
  12. package/dist/components/p-DE3tFqRZ.js +1 -0
  13. package/dist/components/rp-day-slot.js +1 -1
  14. package/dist/components/rp-modal.js +1 -1
  15. package/dist/components/rp-recipe-card.js +1 -1
  16. package/dist/components/rp-select.js +1 -1
  17. package/dist/esm/loader.js +1 -1
  18. package/dist/esm/recipe-planner-ui.js +1 -1
  19. package/dist/esm/rp-day-slot.entry.js +407 -17
  20. package/dist/esm/rp-filter-chips_6.entry.js +132 -8
  21. package/dist/recipe-planner-ui/p-22441648.entry.js +1 -0
  22. package/dist/recipe-planner-ui/p-b3485d04.entry.js +1 -0
  23. package/dist/recipe-planner-ui/recipe-planner-ui.css +2 -2
  24. package/dist/recipe-planner-ui/recipe-planner-ui.esm.js +1 -1
  25. package/dist/types/components/rp-day-slot/rp-day-slot.d.ts +115 -2
  26. package/dist/types/components/rp-modal/rp-modal.d.ts +18 -0
  27. package/dist/types/components/rp-select/rp-select.d.ts +23 -0
  28. package/package.json +1 -1
  29. package/dist/components/p-q8kKM7W4.js +0 -1
  30. package/dist/recipe-planner-ui/p-7f595239.entry.js +0 -1
  31. package/dist/recipe-planner-ui/p-dcb370be.entry.js +0 -1
@@ -6,7 +6,7 @@ var appGlobals = require('./app-globals-V2Kpy_OQ.js');
6
6
  const defineCustomElements = async (win, options) => {
7
7
  if (typeof window === 'undefined') return undefined;
8
8
  await appGlobals.globalScripts();
9
- return index.bootstrapLazy([["rp-day-slot.cjs",[[774,"rp-day-slot",{"day":[513],"dayLabel":[1,"day-label"],"meals":[16],"days":[16],"dayLabels":[16],"normalized":[32],"dropActive":[32],"draggingId":[32]}]]],["rp-filter-menu.cjs",[[514,"rp-filter-menu",{"label":[1],"options":[16],"selected":[1],"searchPlaceholder":[1,"search-placeholder"],"searchable":[4],"open":[1540],"filterText":[32],"activeIndex":[32]},[[5,"pointerdown","onDocumentPointerDown"],[4,"keydown","onDocumentKeydown"]],{"open":[{"onOpenChange":0}]}]]],["rp-filter-chips_6.cjs",[[514,"rp-filter-chips",{"options":[16],"selected":[1],"label":[1]}],[774,"rp-ingredient-list",{"items":[16]}],[774,"rp-modal",{"open":[516],"heading":[1],"focusFirstField":[64]},null,{"open":[{"onOpenChange":0}]}],[774,"rp-recipe-card",{"recipeId":[1,"recipe-id"],"recipeTitle":[1,"recipe-title"],"image":[1],"category":[1],"href":[1],"hrefLabel":[1,"href-label"],"area":[1],"minutes":[2],"favorite":[516],"imageLoading":[32],"imageFailed":[32],"pulsing":[32]}],[514,"rp-search-bar",{"value":[1],"placeholder":[1],"label":[1],"draft":[32]},null,{"value":[{"onValueChange":0}]}],[514,"rp-select",{"options":[16],"value":[1025],"label":[1],"placeholder":[1],"open":[1540],"disabled":[516],"compact":[516],"dropUp":[1540,"drop-up"],"activeIndex":[32],"focusControl":[64]},[[5,"pointerdown","onDocumentPointerDown"]],{"open":[{"onOpenChange":0}],"options":[{"onOptionsChange":0}]}]]]], options);
9
+ return index.bootstrapLazy([["rp-day-slot.cjs",[[774,"rp-day-slot",{"day":[513],"dayLabel":[1,"day-label"],"meals":[16],"days":[16],"dayLabels":[16],"normalized":[32],"dropActive":[32],"draggingId":[32],"pointerKind":[32]}]]],["rp-filter-menu.cjs",[[514,"rp-filter-menu",{"label":[1],"options":[16],"selected":[1],"searchPlaceholder":[1,"search-placeholder"],"searchable":[4],"open":[1540],"filterText":[32],"activeIndex":[32]},[[5,"pointerdown","onDocumentPointerDown"],[4,"keydown","onDocumentKeydown"]],{"open":[{"onOpenChange":0}]}]]],["rp-filter-chips_6.cjs",[[514,"rp-filter-chips",{"options":[16],"selected":[1],"label":[1]}],[774,"rp-ingredient-list",{"items":[16]}],[774,"rp-modal",{"open":[516],"heading":[1],"focusFirstField":[64]},null,{"open":[{"onOpenChange":0}]}],[774,"rp-recipe-card",{"recipeId":[1,"recipe-id"],"recipeTitle":[1,"recipe-title"],"image":[1],"category":[1],"href":[1],"hrefLabel":[1,"href-label"],"area":[1],"minutes":[2],"favorite":[516],"imageLoading":[32],"imageFailed":[32],"pulsing":[32]}],[514,"rp-search-bar",{"value":[1],"placeholder":[1],"label":[1],"draft":[32]},null,{"value":[{"onValueChange":0}]}],[514,"rp-select",{"options":[16],"value":[1025],"label":[1],"placeholder":[1],"open":[1540],"disabled":[516],"compact":[516],"dropUp":[1540,"drop-up"],"activeIndex":[32],"focusControl":[64]},[[5,"pointerdown","onDocumentPointerDown"]],{"open":[{"onOpenChange":0}],"options":[{"onOptionsChange":0}]}]]]], options);
10
10
  };
11
11
 
12
12
  exports.setNonce = index.setNonce;
@@ -19,7 +19,7 @@ var patchBrowser = () => {
19
19
 
20
20
  patchBrowser().then(async (options) => {
21
21
  await appGlobals.globalScripts();
22
- return index.bootstrapLazy([["rp-day-slot.cjs",[[774,"rp-day-slot",{"day":[513],"dayLabel":[1,"day-label"],"meals":[16],"days":[16],"dayLabels":[16],"normalized":[32],"dropActive":[32],"draggingId":[32]}]]],["rp-filter-menu.cjs",[[514,"rp-filter-menu",{"label":[1],"options":[16],"selected":[1],"searchPlaceholder":[1,"search-placeholder"],"searchable":[4],"open":[1540],"filterText":[32],"activeIndex":[32]},[[5,"pointerdown","onDocumentPointerDown"],[4,"keydown","onDocumentKeydown"]],{"open":[{"onOpenChange":0}]}]]],["rp-filter-chips_6.cjs",[[514,"rp-filter-chips",{"options":[16],"selected":[1],"label":[1]}],[774,"rp-ingredient-list",{"items":[16]}],[774,"rp-modal",{"open":[516],"heading":[1],"focusFirstField":[64]},null,{"open":[{"onOpenChange":0}]}],[774,"rp-recipe-card",{"recipeId":[1,"recipe-id"],"recipeTitle":[1,"recipe-title"],"image":[1],"category":[1],"href":[1],"hrefLabel":[1,"href-label"],"area":[1],"minutes":[2],"favorite":[516],"imageLoading":[32],"imageFailed":[32],"pulsing":[32]}],[514,"rp-search-bar",{"value":[1],"placeholder":[1],"label":[1],"draft":[32]},null,{"value":[{"onValueChange":0}]}],[514,"rp-select",{"options":[16],"value":[1025],"label":[1],"placeholder":[1],"open":[1540],"disabled":[516],"compact":[516],"dropUp":[1540,"drop-up"],"activeIndex":[32],"focusControl":[64]},[[5,"pointerdown","onDocumentPointerDown"]],{"open":[{"onOpenChange":0}],"options":[{"onOptionsChange":0}]}]]]], options);
22
+ return index.bootstrapLazy([["rp-day-slot.cjs",[[774,"rp-day-slot",{"day":[513],"dayLabel":[1,"day-label"],"meals":[16],"days":[16],"dayLabels":[16],"normalized":[32],"dropActive":[32],"draggingId":[32],"pointerKind":[32]}]]],["rp-filter-menu.cjs",[[514,"rp-filter-menu",{"label":[1],"options":[16],"selected":[1],"searchPlaceholder":[1,"search-placeholder"],"searchable":[4],"open":[1540],"filterText":[32],"activeIndex":[32]},[[5,"pointerdown","onDocumentPointerDown"],[4,"keydown","onDocumentKeydown"]],{"open":[{"onOpenChange":0}]}]]],["rp-filter-chips_6.cjs",[[514,"rp-filter-chips",{"options":[16],"selected":[1],"label":[1]}],[774,"rp-ingredient-list",{"items":[16]}],[774,"rp-modal",{"open":[516],"heading":[1],"focusFirstField":[64]},null,{"open":[{"onOpenChange":0}]}],[774,"rp-recipe-card",{"recipeId":[1,"recipe-id"],"recipeTitle":[1,"recipe-title"],"image":[1],"category":[1],"href":[1],"hrefLabel":[1,"href-label"],"area":[1],"minutes":[2],"favorite":[516],"imageLoading":[32],"imageFailed":[32],"pulsing":[32]}],[514,"rp-search-bar",{"value":[1],"placeholder":[1],"label":[1],"draft":[32]},null,{"value":[{"onValueChange":0}]}],[514,"rp-select",{"options":[16],"value":[1025],"label":[1],"placeholder":[1],"open":[1540],"disabled":[516],"compact":[516],"dropUp":[1540,"drop-up"],"activeIndex":[32],"focusControl":[64]},[[5,"pointerdown","onDocumentPointerDown"]],{"open":[{"onOpenChange":0}],"options":[{"onOptionsChange":0}]}]]]], options);
23
23
  });
24
24
 
25
25
  exports.setNonce = index.setNonce;
@@ -2,7 +2,7 @@
2
2
 
3
3
  var index = require('./index-CrJlwqe_.js');
4
4
 
5
- const rpDaySlotCss = () => `.sc-rp-day-slot-h{display:block;font-family:var(--rp-font-sans);color:var(--rp-color-text)}.slot.sc-rp-day-slot{display:flex;flex-direction:column;height:100%;min-height:200px;overflow:hidden;background:var(--rp-color-surface);border:1px solid var(--rp-color-border);border-radius:var(--rp-radius-lg);box-shadow:var(--rp-shadow-xs);transition:border-color var(--rp-duration) var(--rp-ease), box-shadow var(--rp-duration) var(--rp-ease), background-color var(--rp-duration) var(--rp-ease)}.slot.is-empty.sc-rp-day-slot{background:var(--rp-color-surface-sunken);border-style:dashed}.slot.is-drop-active.sc-rp-day-slot{background:var(--rp-color-highlight-soft);border-style:solid;border-color:var(--rp-color-highlight);box-shadow:var(--rp-shadow-md), inset 0 0 0 1px var(--rp-color-highlight)}.head.sc-rp-day-slot{display:flex;gap:var(--rp-space-1);align-items:center;justify-content:space-between;padding:var(--rp-space-3) var(--rp-space-2) var(--rp-space-3) var(--rp-space-3);border-bottom:1px solid var(--rp-color-border)}.day.sc-rp-day-slot{min-width:0;margin:0;overflow:hidden;font-size:var(--rp-font-size-xs);font-weight:700;letter-spacing:0.08em;text-transform:uppercase;text-overflow:ellipsis;white-space:nowrap;color:var(--rp-color-text-muted)}.head-right.sc-rp-day-slot{display:flex;flex-shrink:0;gap:var(--rp-space-2);align-items:center}.count.sc-rp-day-slot{display:grid;place-items:center;min-width:20px;height:20px;padding:0 6px;font-size:0.6875rem;font-weight:700;font-variant-numeric:tabular-nums;color:var(--rp-caramel-600);background:var(--rp-color-highlight-soft);border-radius:var(--rp-radius-pill)}.add.sc-rp-day-slot{display:grid;place-items:center;width:26px;height:26px;padding:0;cursor:pointer;color:var(--rp-color-accent-contrast);background:var(--rp-color-accent);border:none;border-radius:50%;transition:transform var(--rp-duration-fast) var(--rp-ease), background-color var(--rp-duration-fast) var(--rp-ease)}.add.sc-rp-day-slot svg.sc-rp-day-slot{fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round}.add.sc-rp-day-slot:hover{background:var(--rp-color-highlight);transform:rotate(90deg) scale(1.1)}.add.sc-rp-day-slot:active{transform:rotate(90deg) scale(0.92)}.meals.sc-rp-day-slot{flex:1;margin:0;padding:var(--rp-space-2);list-style:none;display:flex;flex-direction:column;gap:var(--rp-space-2)}.meal.sc-rp-day-slot{position:relative;padding:var(--rp-space-3) var(--rp-space-3) var(--rp-space-2);cursor:grab;touch-action:pan-y;background:var(--rp-color-surface-sunken);border:1px solid var(--rp-color-border);border-radius:var(--rp-radius-md);transition:transform var(--rp-duration-fast) var(--rp-ease), box-shadow var(--rp-duration-fast) var(--rp-ease), opacity var(--rp-duration-fast) var(--rp-ease)}.meal.sc-rp-day-slot:hover{background:var(--rp-color-surface);box-shadow:var(--rp-shadow-sm);transform:translateY(-1px)}.meal.sc-rp-day-slot:active{cursor:grabbing}.meal.is-dragging.sc-rp-day-slot{opacity:0.4;box-shadow:none;transform:none}.grip.sc-rp-day-slot{position:absolute;inset-block-start:var(--rp-space-2);inset-inline-end:var(--rp-space-2);display:block;opacity:0;transition:opacity var(--rp-duration-fast) var(--rp-ease)}.grip.sc-rp-day-slot svg.sc-rp-day-slot{display:block;fill:var(--rp-color-text-subtle)}.meal.sc-rp-day-slot:hover .grip.sc-rp-day-slot{opacity:1}.meal-title.sc-rp-day-slot{display:block;padding-inline-end:var(--rp-space-4);font-size:var(--rp-font-size-sm);font-weight:500;line-height:1.35;color:var(--rp-color-text)}.meal-actions.sc-rp-day-slot{display:flex;gap:var(--rp-space-1);align-items:center;justify-content:space-between;margin-top:var(--rp-space-2)}.move-select.sc-rp-day-slot{min-width:0;position:relative;z-index:1}.meal.sc-rp-day-slot:has(rp-select[open]),.meals.sc-rp-day-slot:has(rp-select[open]){overflow:visible}.slot.sc-rp-day-slot:has(rp-select[open]){overflow:visible}.meal.sc-rp-day-slot:has(rp-select[open]){z-index:3}.remove.sc-rp-day-slot{display:grid;place-items:center;width:24px;height:24px;padding:0;color:var(--rp-color-text-subtle);cursor:pointer;background:none;border:none;border-radius:var(--rp-radius-sm);transition:color var(--rp-duration-fast) var(--rp-ease), background-color var(--rp-duration-fast) var(--rp-ease)}.remove.sc-rp-day-slot svg.sc-rp-day-slot{fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round}.remove.sc-rp-day-slot:hover{color:var(--rp-color-danger);background:var(--rp-color-danger-soft)}.empty.sc-rp-day-slot{display:flex;flex:1;flex-direction:column;gap:var(--rp-space-2);align-items:center;justify-content:center;padding:var(--rp-space-4) var(--rp-space-3);text-align:center}.empty-icon.sc-rp-day-slot{fill:none;stroke:var(--rp-color-text-subtle);stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}.empty-text.sc-rp-day-slot{margin:0;font-size:var(--rp-font-size-xs);color:var(--rp-color-text-subtle)}.add.sc-rp-day-slot:focus-visible,.remove.sc-rp-day-slot:focus-visible,.move-select.sc-rp-day-slot:focus-visible{outline:var(--rp-focus-ring);outline-offset:var(--rp-focus-offset)}.sr-only.sc-rp-day-slot{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}@media (prefers-reduced-motion: reduce){.slot.sc-rp-day-slot,.meal.sc-rp-day-slot,.add.sc-rp-day-slot,.grip.sc-rp-day-slot,.remove.sc-rp-day-slot,.move-select.sc-rp-day-slot{transition:none}.meal.sc-rp-day-slot:hover,.add.sc-rp-day-slot:hover,.add.sc-rp-day-slot:active{transform:none}.grip.sc-rp-day-slot{opacity:1}}`;
5
+ const rpDaySlotCss = () => `.sc-rp-day-slot-h{display:block;font-family:var(--rp-font-sans);color:var(--rp-color-text)}.slot.sc-rp-day-slot{display:flex;flex-direction:column;height:100%;min-height:200px;overflow:hidden;background:var(--rp-color-surface);border:1px solid var(--rp-color-border);border-radius:var(--rp-radius-lg);box-shadow:var(--rp-shadow-xs);transition:border-color var(--rp-duration) var(--rp-ease), box-shadow var(--rp-duration) var(--rp-ease), background-color var(--rp-duration) var(--rp-ease)}.slot.is-empty.sc-rp-day-slot{background:var(--rp-color-surface-sunken);border-style:dashed}.slot.is-drop-active.sc-rp-day-slot{background:var(--rp-color-highlight-soft);border-style:solid;border-color:var(--rp-color-highlight);box-shadow:var(--rp-shadow-md), inset 0 0 0 1px var(--rp-color-highlight)}.head.sc-rp-day-slot{display:flex;gap:var(--rp-space-1);align-items:center;justify-content:space-between;padding:var(--rp-space-3) var(--rp-space-2) var(--rp-space-3) var(--rp-space-3);border-bottom:1px solid var(--rp-color-border)}.day.sc-rp-day-slot{min-width:0;margin:0;overflow:hidden;font-size:var(--rp-font-size-xs);font-weight:700;letter-spacing:0.08em;text-transform:uppercase;text-overflow:ellipsis;white-space:nowrap;color:var(--rp-color-text-muted)}.head-right.sc-rp-day-slot{display:flex;flex-shrink:0;gap:var(--rp-space-2);align-items:center}.count.sc-rp-day-slot{display:grid;place-items:center;min-width:20px;height:20px;padding:0 6px;font-size:0.6875rem;font-weight:700;font-variant-numeric:tabular-nums;color:var(--rp-caramel-600);background:var(--rp-color-highlight-soft);border-radius:var(--rp-radius-pill)}.add.sc-rp-day-slot{display:grid;place-items:center;width:26px;height:26px;padding:0;cursor:pointer;color:var(--rp-color-accent-contrast);background:var(--rp-color-accent);border:none;border-radius:50%;transition:transform var(--rp-duration-fast) var(--rp-ease), background-color var(--rp-duration-fast) var(--rp-ease)}.add.sc-rp-day-slot svg.sc-rp-day-slot{fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round}.add.sc-rp-day-slot:hover{background:var(--rp-color-highlight);transform:rotate(90deg) scale(1.1)}.add.sc-rp-day-slot:active{transform:rotate(90deg) scale(0.92)}.meals.sc-rp-day-slot{flex:1;margin:0;padding:var(--rp-space-2);list-style:none;display:flex;flex-direction:column;gap:var(--rp-space-2)}.meal.sc-rp-day-slot{position:relative;padding:var(--rp-space-3) var(--rp-space-3) var(--rp-space-2);cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;background:var(--rp-color-surface-sunken);border:1px solid var(--rp-color-border);border-radius:var(--rp-radius-md);transition:transform var(--rp-duration-fast) var(--rp-ease), box-shadow var(--rp-duration-fast) var(--rp-ease), opacity var(--rp-duration-fast) var(--rp-ease)}.meal.sc-rp-day-slot:hover{background:var(--rp-color-surface);box-shadow:var(--rp-shadow-sm);transform:translateY(-1px)}.meal.sc-rp-day-slot:active{cursor:grabbing}.meal.is-dragging.sc-rp-day-slot{opacity:0.4;box-shadow:none;transform:none}.grip.sc-rp-day-slot{position:absolute;inset-block-start:var(--rp-space-2);inset-inline-end:var(--rp-space-2);display:block;opacity:0;transition:opacity var(--rp-duration-fast) var(--rp-ease)}.grip.sc-rp-day-slot svg.sc-rp-day-slot{display:block;fill:var(--rp-color-text-subtle)}.meal.sc-rp-day-slot:hover .grip.sc-rp-day-slot{opacity:1}.meal-title.sc-rp-day-slot{display:block;padding-inline-end:var(--rp-space-4);font-size:var(--rp-font-size-sm);font-weight:500;line-height:1.35;color:var(--rp-color-text)}.meal-actions.sc-rp-day-slot{display:flex;gap:var(--rp-space-1);align-items:center;justify-content:space-between;margin-top:var(--rp-space-2)}.move-select.sc-rp-day-slot{min-width:0;position:relative;z-index:1;touch-action:manipulation}.remove.sc-rp-day-slot::before{content:'';position:absolute;inset:-9px}.meal.sc-rp-day-slot:has(rp-select[open]),.meals.sc-rp-day-slot:has(rp-select[open]){overflow:visible}.slot.sc-rp-day-slot:has(rp-select[open]){overflow:visible}.meal.sc-rp-day-slot:has(rp-select[open]){z-index:3}.remove.sc-rp-day-slot{display:grid;place-items:center;touch-action:manipulation;position:relative;width:24px;height:24px;padding:0;color:var(--rp-color-text-subtle);cursor:pointer;background:none;border:none;border-radius:var(--rp-radius-sm);transition:color var(--rp-duration-fast) var(--rp-ease), background-color var(--rp-duration-fast) var(--rp-ease)}.remove.sc-rp-day-slot svg.sc-rp-day-slot{fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round}.remove.sc-rp-day-slot:hover{color:var(--rp-color-danger);background:var(--rp-color-danger-soft)}.empty.sc-rp-day-slot{display:flex;flex:1;flex-direction:column;gap:var(--rp-space-2);align-items:center;justify-content:center;padding:var(--rp-space-4) var(--rp-space-3);text-align:center}.empty-icon.sc-rp-day-slot{fill:none;stroke:var(--rp-color-text-subtle);stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}.empty-text.sc-rp-day-slot{margin:0;font-size:var(--rp-font-size-xs);color:var(--rp-color-text-subtle)}.add.sc-rp-day-slot:focus-visible,.remove.sc-rp-day-slot:focus-visible,.move-select.sc-rp-day-slot:focus-visible{outline:var(--rp-focus-ring);outline-offset:var(--rp-focus-offset)}.sr-only.sc-rp-day-slot{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}@media (prefers-reduced-motion: reduce){.slot.sc-rp-day-slot,.meal.sc-rp-day-slot,.add.sc-rp-day-slot,.grip.sc-rp-day-slot,.remove.sc-rp-day-slot,.move-select.sc-rp-day-slot{transition:none}.meal.sc-rp-day-slot:hover,.add.sc-rp-day-slot:hover,.add.sc-rp-day-slot:active{transform:none}.grip.sc-rp-day-slot{opacity:1}}`;
6
6
 
7
7
  const DaySlot = class {
8
8
  constructor(hostRef) {
@@ -33,6 +33,19 @@ const DaySlot = class {
33
33
  dropActive = false;
34
34
  /** Id of the meal being dragged out of this column, dimmed at its origin. */
35
35
  draggingId = null;
36
+ /**
37
+ * Which input the user is actually using, which decides whether native HTML5
38
+ * drag-and-drop is armed at all.
39
+ *
40
+ * It starts as `'unknown'` and is set by the first pointer event seen, rather than being
41
+ * guessed from `navigator.maxTouchPoints` or a media query: a laptop with a touchscreen
42
+ * answers yes to both while the person is using a mouse, and a tablet with a keyboard
43
+ * case can be either from one moment to the next. Only an actual event knows.
44
+ *
45
+ * Until something is known, native dragging stays on — that is the mouse behaviour, and
46
+ * a mouse cannot be detected any earlier than its first event either.
47
+ */
48
+ pointerKind = 'unknown';
36
49
  /**
37
50
  * Nested children fire dragleave as the pointer crosses them, so a boolean flag would
38
51
  * flicker off mid-column. Counting enter and leave pairs is what makes the state stable.
@@ -54,6 +67,36 @@ const DaySlot = class {
54
67
  hoveredPanel = null;
55
68
  /** Movement in CSS pixels before a press becomes a drag rather than a tap or a scroll. */
56
69
  static DRAG_THRESHOLD = 8;
70
+ /**
71
+ * Movement tolerated during the long press before it is abandoned as a scroll.
72
+ *
73
+ * A finger resting on a card is never still: holding a phone one-handed produces several
74
+ * pixels of drift, and a real device reports it. With only `DRAG_THRESHOLD` to go on,
75
+ * that drift was read as a deliberate vertical swipe and the pending drag was discarded
76
+ * — the user held, and nothing happened. Every platform's long press allows a slop
77
+ * radius for exactly this reason; this is ours.
78
+ */
79
+ static HOLD_SLOP = 16;
80
+ /**
81
+ * How long a finger must rest on a meal before the press becomes a drag.
82
+ *
83
+ * A phone lays the week out as a single column, so the day you are dragging to is above
84
+ * or below — the same axis as scrolling. Distance alone cannot separate the two
85
+ * intentions on that axis, so time does: hold to drag, swipe to scroll. This is the
86
+ * long-press convention every mobile reorder UI uses, and it is why the desktop path
87
+ * needs no equivalent — there the axes already differ.
88
+ */
89
+ static LONG_PRESS_MS = 220;
90
+ /** Distance from a viewport edge, in CSS pixels, within which a drag scrolls the page. */
91
+ static EDGE_ZONE = 72;
92
+ /** Peak scroll speed in CSS pixels per frame, reached at the very edge. */
93
+ static EDGE_SPEED = 14;
94
+ /** Pending long-press timer, cleared if the finger moves or lifts first. */
95
+ holdTimer = null;
96
+ /** Auto-scroll frame handle, active only while a pointer drag is near an edge. */
97
+ edgeScrollFrame = null;
98
+ /** Latest pointer position, read by the auto-scroll loop between pointer events. */
99
+ lastPoint = { x: 0, y: 0 };
57
100
  /** Fired when the user asks to add a meal to this day. */
58
101
  rpAddMeal;
59
102
  /** Fired when a meal is removed from this day. */
@@ -97,6 +140,18 @@ const DaySlot = class {
97
140
  this.rpMoveMeal.emit({ id, from, to });
98
141
  }
99
142
  onDragStart = (event, meal) => {
143
+ /**
144
+ * Refuses a native drag that a touch produced.
145
+ *
146
+ * Belt and braces alongside clearing `draggable` on pointerdown: some Android builds
147
+ * evaluate the long press before that clearing is observed. A native drag started by a
148
+ * touch is unusable here — `dragover` and `drop` are not reliably delivered for one —
149
+ * and letting it run would cancel the pointer drag that *does* work.
150
+ */
151
+ if (this.pointerKind === 'touch') {
152
+ event.preventDefault();
153
+ return;
154
+ }
100
155
  this.draggingId = meal.id;
101
156
  // A custom MIME type is what lets dragover distinguish a meal from a dragged file or
102
157
  // a text selection, so unrelated drags never light up a column as a drop target.
@@ -139,8 +194,9 @@ const DaySlot = class {
139
194
  /**
140
195
  * Begins tracking a possible pointer drag.
141
196
  *
142
- * Nothing happens until the pointer has moved past the threshold, so a tap still reaches
143
- * the select and the remove button, and a vertical swipe still scrolls the page.
197
+ * Nothing happens until the finger has either held still for `LONG_PRESS_MS` or moved
198
+ * past the threshold, so a tap still reaches the select and the remove button, and a
199
+ * swipe still scrolls the page.
144
200
  */
145
201
  onPointerDown = (event, meal) => {
146
202
  /**
@@ -150,8 +206,22 @@ const DaySlot = class {
150
206
  * so the card never dimmed and no drag image appeared even though the drop still
151
207
  * worked. The native path is left to own the mouse entirely.
152
208
  */
153
- if (event.pointerType === 'mouse')
209
+ if (event.pointerType === 'mouse') {
210
+ this.pointerKind = 'mouse';
154
211
  return;
212
+ }
213
+ this.pointerKind = 'touch';
214
+ /**
215
+ * Disarms the native drag for *this* gesture, not merely the next render.
216
+ *
217
+ * Android Chrome decides whether to start an HTML5 drag during the long press that is
218
+ * about to happen, and a `@State` change does not reach the DOM until Stencil's next
219
+ * render — which lands too late to matter. Clearing the attribute directly is what
220
+ * stops the two drag systems arming from the same press; the state above keeps it off
221
+ * for every card once the render does catch up.
222
+ */
223
+ const card = event.currentTarget;
224
+ card?.removeAttribute('draggable');
155
225
  // A press on the select or a button is that control's, not the start of a drag.
156
226
  if (event.target.closest('button, select, label, a, rp-select'))
157
227
  return;
@@ -163,26 +233,108 @@ const DaySlot = class {
163
233
  startY: event.clientY,
164
234
  started: false,
165
235
  };
236
+ this.lastPoint = { x: event.clientX, y: event.clientY };
237
+ // Holding still is enough to start; `onPointerMove` cancels this if the finger
238
+ // travels first, which is what keeps a scroll a scroll.
239
+ this.holdTimer = window.setTimeout(() => {
240
+ this.holdTimer = null;
241
+ if (this.pointerDrag)
242
+ this.beginPointerDrag();
243
+ }, DaySlot.LONG_PRESS_MS);
166
244
  window.addEventListener('pointermove', this.onPointerMove, { passive: false });
167
245
  window.addEventListener('pointerup', this.onPointerUp);
168
246
  window.addEventListener('pointercancel', this.onPointerCancel);
169
247
  };
248
+ /**
249
+ * Swallows a context menu raised anywhere while a touch drag is running.
250
+ *
251
+ * The element's own handler covers the press that starts the drag, but Android can also
252
+ * raise the menu from a descendant or once the finger has travelled onto another
253
+ * element, and either would hijack the touch stream mid-drag.
254
+ */
255
+ onContextMenuDuringDrag = (event) => {
256
+ if (this.pointerDrag?.started)
257
+ event.preventDefault();
258
+ };
259
+ /** Promotes the tracked press into an actual drag. */
260
+ beginPointerDrag() {
261
+ const drag = this.pointerDrag;
262
+ if (!drag || drag.started)
263
+ return;
264
+ drag.started = true;
265
+ this.draggingId = drag.id;
266
+ /**
267
+ * Cloned before `draggingId` is applied to the DOM.
268
+ *
269
+ * Stencil renders on its own schedule, so at this point the original card has not yet
270
+ * gained `.is-dragging` — which is what makes it a clean copy rather than a dimmed one.
271
+ * `.is-dragging` is stripped from the clone regardless, since that ordering is a
272
+ * detail of the framework rather than a guarantee.
273
+ */
274
+ this.createGhost(this.cardFor(drag.id), drag.title);
275
+ this.moveGhost(this.lastPoint.x, this.lastPoint.y);
276
+ this.highlightPanelAt(this.lastPoint.x, this.lastPoint.y);
277
+ this.startEdgeScroll();
278
+ document.addEventListener('contextmenu', this.onContextMenuDuringDrag, true);
279
+ }
170
280
  onPointerMove = (event) => {
171
281
  const drag = this.pointerDrag;
172
282
  if (!drag || event.pointerId !== drag.pointerId)
173
283
  return;
284
+ /**
285
+ * Clamped into the viewport.
286
+ *
287
+ * Pointer capture keeps delivering coordinates once a finger passes the edge of the
288
+ * screen, and on a phone that happens constantly — the days below the fold are reached
289
+ * by dragging *downwards* past the bottom. Those coordinates hit-test to nothing, so
290
+ * the highlight would drop out and the release would land on no column at all.
291
+ *
292
+ * Clamping keeps the effective point on the last visible row, which is also the row
293
+ * auto-scrolling is bringing new days into.
294
+ */
295
+ this.lastPoint = {
296
+ x: Math.min(Math.max(event.clientX, 1), window.innerWidth - 1),
297
+ y: Math.min(Math.max(event.clientY, 1), window.innerHeight - 1),
298
+ };
174
299
  if (!drag.started) {
175
- const moved = Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY);
300
+ const dx = Math.abs(event.clientX - drag.startX);
301
+ const dy = Math.abs(event.clientY - drag.startY);
302
+ const moved = Math.hypot(dx, dy);
303
+ /**
304
+ * Below the threshold the press is still undecided, and must stay that way.
305
+ *
306
+ * A finger resting on a card is never perfectly still — it jitters a few pixels
307
+ * while the long press runs. Reacting to that jitter is what broke the gesture on a
308
+ * real device: a wobble of a pixel or two is mostly vertical simply because of how a
309
+ * phone is held, so the branch below read it as a scroll and tore down the pending
310
+ * drag. The user held, and nothing ever happened.
311
+ */
176
312
  if (moved < DaySlot.DRAG_THRESHOLD)
177
313
  return;
178
- drag.started = true;
179
- this.draggingId = drag.id;
180
- this.createGhost(drag.title);
314
+ /**
315
+ * Past the threshold with the hold still pending, a mainly vertical movement is a
316
+ * scroll — the axis the page scrolls on. A sideways swipe cannot be a scroll here,
317
+ * so it starts a drag immediately, which keeps a multi-column layout responsive.
318
+ *
319
+ * The slop radius is what distinguishes a swipe from the drift of a finger that is
320
+ * simply resting. Inside it the hold keeps running; only travel beyond it is a
321
+ * decision to scroll.
322
+ */
323
+ if (dy > dx) {
324
+ if (moved < DaySlot.HOLD_SLOP)
325
+ return;
326
+ this.scrollFromTouch(event);
327
+ return;
328
+ }
329
+ this.cancelHold();
330
+ this.beginPointerDrag();
181
331
  }
182
332
  // Suppresses the page scroll that a touch-move would otherwise produce.
183
333
  event.preventDefault();
184
- this.moveGhost(event.clientX, event.clientY);
185
- this.highlightPanelAt(event.clientX, event.clientY);
334
+ // The clamped point, not the raw one: a finger past the edge of the screen still has
335
+ // to highlight the row it is pressing against.
336
+ this.moveGhost(this.lastPoint.x, this.lastPoint.y);
337
+ this.highlightPanelAt(this.lastPoint.x, this.lastPoint.y);
186
338
  };
187
339
  onPointerUp = (event) => {
188
340
  const drag = this.pointerDrag;
@@ -190,25 +342,132 @@ const DaySlot = class {
190
342
  return;
191
343
  const started = drag.started;
192
344
  const id = drag.id;
345
+ /**
346
+ * Resolved from the last clamped point rather than the release coordinates.
347
+ *
348
+ * A finger lifted past the bottom of the screen reports a position outside the
349
+ * viewport, which hit-tests to nothing — the drop would be discarded even though the
350
+ * column the user was holding against is obvious. `lastPoint` is that column.
351
+ */
352
+ const panel = started ? this.panelAt(this.lastPoint.x, this.lastPoint.y) : null;
193
353
  this.endPointerDrag();
194
354
  if (!started)
195
355
  return;
196
- const panel = this.panelAt(event.clientX, event.clientY);
197
356
  const to = panel?.getAttribute('day');
198
357
  if (to)
199
358
  this.emitMove(id, this.day, to);
200
359
  };
201
- onPointerCancel = () => this.endPointerDrag();
360
+ /**
361
+ * A cancelled pointer sequence.
362
+ *
363
+ * Once a drag is under way this deliberately does *not* tear it down. On a phone
364
+ * `pointercancel` is delivered whenever the platform decides some other gesture owns the
365
+ * touch — a native drag arming, an edge-swipe, the browser's own scroll heuristics — and
366
+ * treating that as "the user gave up" is what made a drag vanish mid-gesture with the
367
+ * card left behind. The drag stays live and `pointerup` still resolves it; only a press
368
+ * that had not yet become a drag is discarded here.
369
+ */
370
+ onPointerCancel = () => {
371
+ if (this.pointerDrag?.started)
372
+ return;
373
+ this.endPointerDrag();
374
+ };
375
+ /**
376
+ * Scrolls the page for a swipe that turned out not to be a drag.
377
+ *
378
+ * The card sets `touch-action: none`, which it must in order to be draggable at all on
379
+ * the vertical axis — but that also means the browser will not scroll for this gesture,
380
+ * so the component has to. The drag is abandoned first, then each subsequent move is
381
+ * translated into scrolling until the finger lifts, which keeps the swipe continuous
382
+ * rather than dropping the rest of it.
383
+ */
384
+ scrollFromTouch(event) {
385
+ const startY = this.pointerDrag?.startY ?? event.clientY;
386
+ let lastY = event.clientY;
387
+ this.endPointerDrag();
388
+ window.scrollBy(0, startY - event.clientY);
389
+ const onMove = (move) => {
390
+ if (move.pointerId !== event.pointerId)
391
+ return;
392
+ window.scrollBy(0, lastY - move.clientY);
393
+ lastY = move.clientY;
394
+ move.preventDefault();
395
+ };
396
+ const stop = () => {
397
+ window.removeEventListener('pointermove', onMove);
398
+ window.removeEventListener('pointerup', stop);
399
+ window.removeEventListener('pointercancel', stop);
400
+ };
401
+ window.addEventListener('pointermove', onMove, { passive: false });
402
+ window.addEventListener('pointerup', stop);
403
+ window.addEventListener('pointercancel', stop);
404
+ }
405
+ cancelHold() {
406
+ if (this.holdTimer !== null) {
407
+ clearTimeout(this.holdTimer);
408
+ this.holdTimer = null;
409
+ }
410
+ }
202
411
  endPointerDrag() {
412
+ this.cancelHold();
413
+ this.stopEdgeScroll();
414
+ document.removeEventListener('contextmenu', this.onContextMenuDuringDrag, true);
203
415
  this.pointerDrag = null;
204
416
  this.draggingId = null;
205
417
  this.clearHighlight();
206
418
  this.ghost?.remove();
207
419
  this.ghost = null;
420
+ this.el.style.removeProperty('touch-action');
208
421
  window.removeEventListener('pointermove', this.onPointerMove);
209
422
  window.removeEventListener('pointerup', this.onPointerUp);
210
423
  window.removeEventListener('pointercancel', this.onPointerCancel);
211
424
  }
425
+ /**
426
+ * Scrolls the page while the finger rests near the top or bottom of the viewport.
427
+ *
428
+ * On a phone the week is one column tall, so the day being dragged to is usually off
429
+ * screen — and a finger that is already touching the screen cannot scroll to it, because
430
+ * that same touch is the drag. Without this the drop target is simply unreachable, which
431
+ * is exactly the failure that a desktop browser's device emulation hides: its window is
432
+ * tall enough that every day is visible at once.
433
+ */
434
+ startEdgeScroll() {
435
+ if (this.edgeScrollFrame !== null)
436
+ return;
437
+ const step = () => {
438
+ if (!this.pointerDrag?.started) {
439
+ this.edgeScrollFrame = null;
440
+ return;
441
+ }
442
+ const y = this.lastPoint.y;
443
+ const height = window.innerHeight;
444
+ let delta = 0;
445
+ if (y < DaySlot.EDGE_ZONE) {
446
+ delta = -DaySlot.EDGE_SPEED * (1 - y / DaySlot.EDGE_ZONE);
447
+ }
448
+ else if (y > height - DaySlot.EDGE_ZONE) {
449
+ delta = DaySlot.EDGE_SPEED * (1 - (height - y) / DaySlot.EDGE_ZONE);
450
+ }
451
+ if (delta !== 0) {
452
+ const before = window.scrollY;
453
+ window.scrollBy(0, delta);
454
+ // Nothing moved, so the page is already at the end — keep the highlight where it
455
+ // is rather than re-running a hit test that cannot produce a new answer.
456
+ if (window.scrollY !== before) {
457
+ // The page moved under a stationary finger, so what is beneath it has changed.
458
+ this.highlightPanelAt(this.lastPoint.x, this.lastPoint.y);
459
+ }
460
+ }
461
+ this.edgeScrollFrame = requestAnimationFrame(step);
462
+ };
463
+ this.edgeScrollFrame = requestAnimationFrame(step);
464
+ }
465
+ stopEdgeScroll() {
466
+ if (this.edgeScrollFrame !== null) {
467
+ cancelAnimationFrame(this.edgeScrollFrame);
468
+ this.edgeScrollFrame = null;
469
+ }
470
+ }
212
471
  /** The `rp-day-slot` under a point, found by hit-testing rather than by geometry. */
213
472
  panelAt(x, y) {
214
473
  // The ghost follows the pointer and would otherwise be the topmost element.
@@ -217,7 +476,36 @@ const DaySlot = class {
217
476
  const target = document.elementFromPoint(x, y);
218
477
  if (this.ghost)
219
478
  this.ghost.style.display = '';
220
- return target?.closest('rp-day-slot') ?? null;
479
+ const hit = target?.closest('rp-day-slot');
480
+ if (hit)
481
+ return hit;
482
+ /**
483
+ * Hit-testing returns nothing when the point is outside the viewport or lands in the
484
+ * gap between columns — a finger dragged to the very edge of the screen does both.
485
+ * Falling back to the nearest panel by geometry means a drop at the edge still lands
486
+ * somewhere sensible instead of being silently discarded.
487
+ */
488
+ return this.nearestPanel(x, y);
489
+ }
490
+ nearestPanel(x, y) {
491
+ let best = null;
492
+ let bestDistance = Infinity;
493
+ for (const panel of Array.from(document.querySelectorAll('rp-day-slot'))) {
494
+ const box = panel.getBoundingClientRect();
495
+ if (box.width === 0 || box.height === 0)
496
+ continue;
497
+ // Zero on either axis whenever the point is already within the panel's span there,
498
+ // so this measures the gap to the box rather than to its centre.
499
+ const dx = Math.max(box.left - x, 0, x - box.right);
500
+ const dy = Math.max(box.top - y, 0, y - box.bottom);
501
+ const distance = Math.hypot(dx, dy);
502
+ if (distance < bestDistance) {
503
+ bestDistance = distance;
504
+ best = panel;
505
+ }
506
+ }
507
+ // Beyond this the pointer is nowhere near the planner and the drop is not meant for it.
508
+ return bestDistance <= 64 ? best : null;
221
509
  }
222
510
  highlightPanelAt(x, y) {
223
511
  const panel = this.panelAt(x, y);
@@ -234,18 +522,89 @@ const DaySlot = class {
234
522
  this.hoveredPanel?.querySelector('.slot')?.classList.remove('is-drop-active');
235
523
  this.hoveredPanel = null;
236
524
  }
237
- createGhost(title) {
525
+ /**
526
+ * The rendered card for one meal id.
527
+ *
528
+ * Found by index rather than by a selector: the `<li>` carries no id attribute, and the
529
+ * order of `.meal` elements is the order of `normalized`, which is what the render walks.
530
+ */
531
+ cardFor(id) {
532
+ const index = this.normalized.findIndex((meal) => meal.id === id);
533
+ if (index < 0)
534
+ return null;
535
+ return this.el.querySelectorAll('.meal')[index] ?? null;
536
+ }
537
+ /**
538
+ * Builds the copy of the card that follows the finger.
539
+ *
540
+ * A clone of the real card rather than a label bearing its title. The native drag on a
541
+ * desktop hands the browser a snapshot of the element itself, so the thing being dragged
542
+ * looks exactly like the thing that was picked up; a touch drag has to construct that
543
+ * likeness, and a bare title made the two platforms look like different features.
544
+ *
545
+ * The clone is sized to the original because it is positioned out of the flow, where a
546
+ * percentage width would resolve against the viewport instead of the column.
547
+ */
548
+ createGhost(source, title) {
238
549
  const ghost = document.createElement('div');
239
- ghost.textContent = title;
240
550
  ghost.setAttribute('aria-hidden', 'true');
241
551
  ghost.className = 'rp-drag-ghost';
552
+ if (source) {
553
+ const box = source.getBoundingClientRect();
554
+ ghost.style.width = `${box.width}px`;
555
+ const copy = source.cloneNode(true);
556
+ // The original is dimmed by `.is-dragging`; the copy is the thing being dragged and
557
+ // must not inherit that, nor the id-based state that belongs to the live card.
558
+ copy.classList.remove('is-dragging');
559
+ copy.removeAttribute('draggable');
560
+ /**
561
+ * Every cloned custom element is flattened to inert markup.
562
+ *
563
+ * A cloned `rp-select` is still an undefined `rp-select` in the registry's eyes, so
564
+ * appending it to the document upgrades it — Stencil then renders a *second* trigger
565
+ * inside the one already copied, and the ghost shows two dropdowns. Replacing the
566
+ * element with a plain `div` carrying the same classes keeps the rendered appearance
567
+ * while removing anything that can come alive.
568
+ */
569
+ for (const custom of Array.from(copy.querySelectorAll('rp-select'))) {
570
+ const flat = document.createElement('div');
571
+ flat.className = custom.className;
572
+ // The trigger is what was visible; the list is closed and contributes nothing.
573
+ const trigger = custom.querySelector('.trigger');
574
+ if (trigger)
575
+ flat.append(trigger.cloneNode(true));
576
+ custom.replaceWith(flat);
577
+ }
578
+ for (const node of Array.from(copy.querySelectorAll('button, input, select, a'))) {
579
+ node.style.pointerEvents = 'none';
580
+ node.setAttribute('tabindex', '-1');
581
+ }
582
+ ghost.append(copy);
583
+ }
584
+ else {
585
+ // No element to copy — fall back to the title, which is better than nothing at all.
586
+ ghost.textContent = title;
587
+ }
242
588
  document.body.append(ghost);
243
589
  this.ghost = ghost;
244
590
  }
591
+ /**
592
+ * Positions the floating copy, keeping it fully on screen.
593
+ *
594
+ * The copy is as wide as the card it came from, so following the pointer literally pushes
595
+ * it off the edge whenever a drag reaches one — and on a phone a drag reaches the bottom
596
+ * edge on almost every move, because that is how auto-scroll is triggered. Clamping to
597
+ * the viewport keeps the thing being dragged visible, which is the entire point of it.
598
+ */
245
599
  moveGhost(x, y) {
246
600
  if (!this.ghost)
247
601
  return;
248
- this.ghost.style.transform = `translate(${x}px, ${y}px)`;
602
+ const box = this.ghost.getBoundingClientRect();
603
+ // The rule's negative margin offsets the copy up and left of the fingertip; these are
604
+ // the bounds that keep the result inside the viewport once it is applied.
605
+ const maxX = window.innerWidth - box.width + 12;
606
+ const maxY = window.innerHeight - box.height + 44;
607
+ this.ghost.style.transform = `translate(${Math.min(Math.max(x, 12), Math.max(maxX, 12))}px, ${Math.min(Math.max(y, 44), Math.max(maxY, 44))}px)`;
249
608
  }
250
609
  onDrop = (event) => {
251
610
  const payload = event.dataTransfer?.getData('application/x-rp-meal');
@@ -267,7 +626,38 @@ const DaySlot = class {
267
626
  const days = Array.isArray(this.days) ? this.days : [];
268
627
  const labels = this.dayLabels ?? {};
269
628
  const meals = this.normalized;
270
- return (index.h(index.Host, { key: '47e4ac959151a9e43b8398bcbed2df2af827e9f8' }, index.h("section", { key: 'b08414f465303f8015bc3035e3f12994c1868ac8', class: { slot: true, 'is-drop-active': this.dropActive, 'is-empty': meals.length === 0 }, onDragEnter: this.onDragEnter, onDragOver: this.onDragOver, onDragLeave: this.onDragLeave, onDrop: this.onDrop }, index.h("header", { key: '1b3ab376369ae3339e60740693bcf349ef14a981', class: "head" }, index.h("h3", { key: '077f5818cc37c85110ec831482f6f700ada577dc', class: "day" }, this.dayLabel || this.day), index.h("div", { key: '1fabc2004094d0787d6d734b05d95d825c41c55f', class: "head-right" }, meals.length > 0 && index.h("span", { key: 'af3875556f999563482e8e9ecc833e768515f001', class: "count" }, meals.length), index.h("button", { key: '56f11a2a992f8467d4c876d7ab0771f0f89f31e3', type: "button", class: "add", onClick: () => this.rpAddMeal.emit(this.day), "aria-label": `Add a meal to ${(this.dayLabels ?? {})[this.day] || this.dayLabel || this.day}` }, index.h("svg", { key: 'c27fc88bd1ef85ddac15016e21ca140ffd7d0f82', viewBox: "0 0 24 24", width: "15", height: "15", "aria-hidden": "true" }, index.h("path", { key: 'ecde1e8d37b1c6d2abc9a7edc81ec39d09f79d84', d: "M12 5v14M5 12h14" }))))), meals.length === 0 ? (index.h("div", { class: "empty" }, index.h("slot", null, index.h("svg", { class: "empty-icon", viewBox: "0 0 24 24", width: "26", height: "26", "aria-hidden": "true" }, index.h("path", { d: "M4 4h16v16H4z", opacity: "0.35" }), index.h("path", { d: "M8 2v4M16 2v4M4 10h16" })), index.h("p", { class: "empty-text" }, "No meals planned")))) : (index.h("ul", { class: "meals" }, meals.map((meal) => (index.h("li", { class: { meal: true, 'is-dragging': this.draggingId === meal.id }, key: meal.id, draggable: true, onDragStart: (event) => this.onDragStart(event, meal), onPointerDown: (event) => this.onPointerDown(event, meal) }, index.h("span", { class: "grip", "aria-hidden": "true" }, index.h("svg", { viewBox: "0 0 24 24", width: "12", height: "12" }, index.h("circle", { cx: "9", cy: "6", r: "1.4" }), index.h("circle", { cx: "15", cy: "6", r: "1.4" }), index.h("circle", { cx: "9", cy: "12", r: "1.4" }), index.h("circle", { cx: "15", cy: "12", r: "1.4" }), index.h("circle", { cx: "9", cy: "18", r: "1.4" }), index.h("circle", { cx: "15", cy: "18", r: "1.4" }))), index.h("span", { class: "meal-title" }, meal.title), index.h("div", { class: "meal-actions" }, index.h("rp-select", { class: "move-select", compact: true, label: `Move ${meal.title} to another day`, placeholder: "Move\u2026", options: days
629
+ return (index.h(index.Host, { key: '05ee7eff0f1ff19f77363539f3f5ccf2d1d96efb' }, index.h("section", { key: 'dce7ecf1d85d7d8cd8b988fb6dc06f93810f532a', class: { slot: true, 'is-drop-active': this.dropActive, 'is-empty': meals.length === 0 }, onDragEnter: this.onDragEnter, onDragOver: this.onDragOver, onDragLeave: this.onDragLeave, onDrop: this.onDrop }, index.h("header", { key: '086dc84dce8943ae814505878fee4dbc1e3acc1a', class: "head" }, index.h("h3", { key: 'a37cb66c77bc30ed80e7eb5d9bbe38c2b2c207b6', class: "day" }, this.dayLabel || this.day), index.h("div", { key: '67e62ed1a5754842f8ffd828ac1ce7cd63a5a666', class: "head-right" }, meals.length > 0 && index.h("span", { key: 'c4460980f7998c355e00ea7d0035297b47a8ecb0', class: "count" }, meals.length), index.h("button", { key: '5d4a5c5fafa9e63cf6cd323b0e23cd6354a18d3b', type: "button", class: "add", onClick: () => this.rpAddMeal.emit(this.day), "aria-label": `Add a meal to ${(this.dayLabels ?? {})[this.day] || this.dayLabel || this.day}` }, index.h("svg", { key: '684fc27b7da23ce41a656bc04524b600dec89ee3', viewBox: "0 0 24 24", width: "15", height: "15", "aria-hidden": "true" }, index.h("path", { key: 'c22a9bdbccc3a2fee2f59106371afbfd96c9946f', d: "M12 5v14M5 12h14" }))))), meals.length === 0 ? (index.h("div", { class: "empty" }, index.h("slot", null, index.h("svg", { class: "empty-icon", viewBox: "0 0 24 24", width: "26", height: "26", "aria-hidden": "true" }, index.h("path", { d: "M4 4h16v16H4z", opacity: "0.35" }), index.h("path", { d: "M8 2v4M16 2v4M4 10h16" })), index.h("p", { class: "empty-text" }, "No meals planned")))) : (index.h("ul", { class: "meals" }, meals.map((meal) => (index.h("li", { class: { meal: true, 'is-dragging': this.draggingId === meal.id }, key: meal.id,
630
+ /**
631
+ * Only where HTML5 drag-and-drop is the right mechanism — a mouse.
632
+ *
633
+ * Android Chrome fires a native `dragstart` on a long press, so leaving
634
+ * this on unconditionally armed *both* systems from one touch: the
635
+ * pointer path began its ghost, the native drag then seized the touch
636
+ * stream and delivered `pointercancel`, which tore that ghost and the
637
+ * target highlight straight back down. The native drag it left running
638
+ * has no usable drop target, because a touch-initiated drag does not
639
+ * reliably deliver `dragover`/`drop` on Android.
640
+ *
641
+ * The visible result was a card that could be picked up, showed no
642
+ * highlight over any day, and refused to drop.
643
+ */
644
+ draggable: this.pointerKind !== 'touch', onDragStart: (event) => this.onDragStart(event, meal), onPointerDown: (event) => this.onPointerDown(event, meal),
645
+ /**
646
+ * Suppresses Android's long-press context menu.
647
+ *
648
+ * The gesture that starts a drag here is the same one the platform uses
649
+ * to raise its text-selection and link menu. On a card containing a title
650
+ * and controls that menu appears over the drag, takes the touch stream
651
+ * with it, and leaves the next tap dismissing the menu rather than
652
+ * pressing the button it landed on — which is why dragging and the remove
653
+ * button appeared broken together.
654
+ *
655
+ * A desktop right-click is a different gesture and is left alone.
656
+ */
657
+ onContextMenu: (event) => {
658
+ if (this.pointerKind === 'touch')
659
+ event.preventDefault();
660
+ } }, index.h("span", { class: "grip", "aria-hidden": "true" }, index.h("svg", { viewBox: "0 0 24 24", width: "12", height: "12" }, index.h("circle", { cx: "9", cy: "6", r: "1.4" }), index.h("circle", { cx: "15", cy: "6", r: "1.4" }), index.h("circle", { cx: "9", cy: "12", r: "1.4" }), index.h("circle", { cx: "15", cy: "12", r: "1.4" }), index.h("circle", { cx: "9", cy: "18", r: "1.4" }), index.h("circle", { cx: "15", cy: "18", r: "1.4" }))), index.h("span", { class: "meal-title" }, meal.title), index.h("div", { class: "meal-actions" }, index.h("rp-select", { class: "move-select", compact: true, label: `Move ${meal.title} to another day`, placeholder: "Move\u2026", options: days
271
661
  .filter((day) => day !== this.day)
272
662
  .map((day) => ({ value: day, label: labels[day] ?? day })), onRpSelectChange: (event) => this.emitMove(meal.id, this.day, event.detail) }), index.h("button", { type: "button", class: "remove", onClick: () => this.rpRemoveMeal.emit({ id: meal.id, day: this.day }), "aria-label": `Remove ${meal.title}` }, index.h("svg", { viewBox: "0 0 24 24", width: "13", height: "13", "aria-hidden": "true" }, index.h("path", { d: "M6 6l12 12M18 6L6 18" }))))))))))));
273
663
  }