@fayz-ai/pos 0.27.6 → 0.27.8

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.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { haptic, createKeymap, directionOf, moveFocus } from '@fayz-ai/core/interaction';
2
2
  export { comboOf, createKeymap, haptic, setHapticsEnabled } from '@fayz-ai/core/interaction';
3
- import { forwardRef, memo, useState, useRef, useCallback, useEffect, useMemo, useReducer, Fragment as Fragment$1 } from 'react';
4
- import { UserPlus, X, Check, Phone, User, Receipt, RotateCcw, FileCheck, FileWarning, ChevronDown, Pencil, ChevronLeft, ChevronRight, Plus, ChevronUp, ChefHat, AlertTriangle, Printer, Percent, QrCode, CreditCard, Banknote, Trash2, Clock, Minus, Bike, History, PackageCheck, CheckCheck, Package, Loader2, MapPin, ShieldCheck, Keyboard, CalendarClock, Users, UserRound, Search, Utensils, BellRing, UserX, UserCheck, Delete, UtensilsCrossed, Info, ClipboardList, LayoutGrid, ShoppingBag, ConciergeBell, Map as Map$1, Settings2, Square, Circle, PenLine, MoreHorizontal, Store, Tag, Star, Ticket, Ban, ArrowLeftRight, Gift, UserMinus, CheckCircle2, Crosshair, Unlink, MousePointerClick, PencilRuler, RefreshCw, MapPinOff, Eye, EyeOff, Undo2, TriangleAlert, ArrowRight, Pizza, Wheat, Sandwich, IceCreamCone, CakeSlice, Coffee, Croissant, CupSoda, Salad, Beef, Wine, GlassWater, Soup, Fish, Wrench, CalendarCheck } from 'lucide-react';
3
+ import { forwardRef, memo, useRef, useState, useCallback, useLayoutEffect, useEffect, useMemo, useReducer, useDeferredValue, Fragment as Fragment$1 } from 'react';
4
+ import { Minus, Plus, Search, Pizza, Wheat, Sandwich, IceCreamCone, CakeSlice, Coffee, Croissant, CupSoda, Salad, Beef, Utensils, Wine, GlassWater, Soup, Fish, UserPlus, X, Check, Phone, User, Receipt, RotateCcw, FileCheck, FileWarning, ChevronDown, Pencil, ChevronLeft, ChevronRight, ChevronUp, ChefHat, AlertTriangle, Printer, Percent, QrCode, CreditCard, Banknote, Trash2, Clock, Bike, History, PackageCheck, CheckCheck, Package, Loader2, MapPin, ShieldCheck, Keyboard, CalendarClock, Users, UserRound, BellRing, UserX, UserCheck, Delete, UtensilsCrossed, Info, ClipboardList, LayoutGrid, ShoppingBag, ConciergeBell, Map as Map$1, Settings2, Square, Circle, PenLine, MoreHorizontal, Store, Tag, Star, Ticket, Ban, ArrowLeftRight, Gift, UserMinus, CheckCircle2, Crosshair, Unlink, MousePointerClick, PencilRuler, RefreshCw, MapPinOff, Eye, EyeOff, Undo2, TriangleAlert, ArrowRight, Wrench, CalendarCheck } from 'lucide-react';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
6
  import { describeError, channelOfOrder } from '@fayz-ai/core';
7
7
  import { createPortal } from 'react-dom';
@@ -981,8 +981,15 @@ function asksInstallments(transactionType, accountKind, typeName) {
981
981
  }
982
982
 
983
983
  // src/screen/money.ts
984
+ var formatters = /* @__PURE__ */ new Map();
984
985
  function formatCents(cents, currency = "BRL", locale = "pt-BR") {
985
- return new Intl.NumberFormat(locale, { style: "currency", currency }).format(cents / 100);
986
+ const key = `${locale}|${currency}`;
987
+ let f = formatters.get(key);
988
+ if (!f) {
989
+ f = new Intl.NumberFormat(locale, { style: "currency", currency });
990
+ formatters.set(key, f);
991
+ }
992
+ return f.format(cents / 100);
986
993
  }
987
994
  function splitEvenly(totalCents, ways) {
988
995
  if (ways <= 0) throw new Error("splitEvenly: ways must be positive");
@@ -2770,6 +2777,39 @@ function categoryIcon(label) {
2770
2777
  for (const [palavra, Icone] of POR_PALAVRA) if (nome.includes(palavra)) return Icone;
2771
2778
  return Utensils;
2772
2779
  }
2780
+
2781
+ // src/screen/catalogWindow.ts
2782
+ var CATALOG_OVERSCAN_PX = 480;
2783
+ var CATALOG_FIRST_PASS = 36;
2784
+ function visibleRows(sections, cols, stride, viewTop, viewBottom) {
2785
+ const c = Math.max(1, cols);
2786
+ return sections.map(({ top, count }) => {
2787
+ const rows = Math.ceil(count / c);
2788
+ if (rows === 0 || stride <= 0) return [0, rows];
2789
+ const first = clamp(Math.floor((viewTop - top) / stride), 0, rows);
2790
+ const end = clamp(Math.ceil((viewBottom - top) / stride), first, rows);
2791
+ return [first, end];
2792
+ });
2793
+ }
2794
+ function firstPassRows(counts, cols, budget = CATALOG_FIRST_PASS) {
2795
+ const c = Math.max(1, cols);
2796
+ let left = budget;
2797
+ return counts.map((count) => {
2798
+ const rows = Math.ceil(count / c);
2799
+ const take = clamp(Math.ceil(left / c), 0, rows);
2800
+ left -= take * c;
2801
+ return [0, take];
2802
+ });
2803
+ }
2804
+ function spacerHeight(rows, stride, gap) {
2805
+ return rows > 0 ? rows * stride - gap : 0;
2806
+ }
2807
+ function sameRows(a, b) {
2808
+ return a.length === b.length && a.every((r, i) => r[0] === b[i][0] && r[1] === b[i][1]);
2809
+ }
2810
+ function clamp(n, lo, hi) {
2811
+ return Math.min(hi, Math.max(lo, n));
2812
+ }
2773
2813
  var STATUS = {
2774
2814
  free: { label: "Livre", Icon: CheckCircle2 },
2775
2815
  occupied: { label: "Ocupada", Icon: Utensils },
@@ -3180,7 +3220,8 @@ function seatOffsets(seats, w, h) {
3180
3220
  place(perSide, (i) => ({ x: w + 4, y: step(perSide, h, i) }));
3181
3221
  return out;
3182
3222
  }
3183
- function CatalogView({
3223
+ var CatalogView = memo(CatalogViewImpl, (a, b) => !!a.hidden && !!b.hidden);
3224
+ function CatalogViewImpl({
3184
3225
  categories,
3185
3226
  items,
3186
3227
  activeCategoryId,
@@ -3192,7 +3233,8 @@ function CatalogView({
3192
3233
  query,
3193
3234
  menuLabel,
3194
3235
  onRefreshMenu,
3195
- menuRefreshing
3236
+ menuRefreshing,
3237
+ hidden
3196
3238
  }) {
3197
3239
  const searching = menuSearchActive(query);
3198
3240
  const shown = items.filter(
@@ -3205,155 +3247,251 @@ function CatalogView({
3205
3247
  }
3206
3248
  const orfaos = shown.filter((i) => !categories.some((c) => c.id === i.categoryId));
3207
3249
  if (orfaos.length > 0) secoes.push([void 0, orfaos]);
3208
- return /* @__PURE__ */ jsxs("div", { className: "pos-scroll", style: { flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }, children: [
3209
- !searching && /* @__PURE__ */ jsx("div", { className: "pos-cats", children: categories.map((c, index) => /* @__PURE__ */ jsxs(
3210
- PosButton,
3211
- {
3212
- className: "pos-cat",
3213
- style: {
3214
- background: `var(--pos-tone-${c.tone})`,
3215
- ["--pos-stagger"]: `${Math.min(index, 11) * 20}ms`
3216
- },
3217
- "aria-pressed": c.id === activeCategoryId,
3218
- onClick: () => {
3219
- haptic("tap");
3220
- onCategoryChange(c.id);
3221
- },
3222
- children: [
3223
- /* @__PURE__ */ jsxs("span", { children: [
3224
- /* @__PURE__ */ jsx("span", { className: "pos-cat__name", children: c.label }),
3225
- c.itemCount != null && /* @__PURE__ */ jsxs("span", { className: "pos-cat__n pos-num", style: { display: "block" }, children: [
3226
- c.itemCount,
3227
- " itens"
3228
- ] })
3229
- ] }),
3230
- /* @__PURE__ */ jsx("span", { className: "pos-cat__icon", children: c.icon ?? /* @__PURE__ */ jsx(CategoryIcon, { label: c.label }) })
3231
- ]
3232
- },
3233
- c.id
3234
- )) }),
3235
- !searching && /* @__PURE__ */ jsx("hr", { className: "pos-cats-rule" }),
3236
- onRefreshMenu && /* @__PURE__ */ jsxs("div", { style: {
3237
- display: "flex",
3238
- alignItems: "center",
3239
- justifyContent: "flex-end",
3240
- gap: "0.5rem",
3241
- padding: "0 0.75rem 0.375rem",
3242
- fontSize: 11,
3243
- opacity: 0.55
3244
- }, children: [
3245
- /* @__PURE__ */ jsx("span", { children: menuLabel }),
3246
- /* @__PURE__ */ jsx(
3247
- PosButton,
3248
- {
3249
- type: "button",
3250
- className: "fz-tap",
3251
- onClick: () => {
3252
- haptic("tap");
3253
- onRefreshMenu();
3254
- },
3255
- disabled: menuRefreshing,
3256
- style: {
3257
- background: "transparent",
3258
- border: 0,
3259
- padding: "0.125rem 0.25rem",
3260
- color: "inherit",
3261
- font: "inherit",
3262
- textDecoration: "underline",
3263
- cursor: menuRefreshing ? "default" : "pointer"
3250
+ const scrollRef = useRef(null);
3251
+ const gridRefs = useRef([]);
3252
+ gridRefs.current.length = secoes.length;
3253
+ const countsRef = useRef([]);
3254
+ countsRef.current = secoes.map(([, itens]) => itens.length);
3255
+ const [win, setWin] = useState({ kind: "first" });
3256
+ const measure = useCallback(() => {
3257
+ const el = scrollRef.current;
3258
+ if (!el || el.style.display === "none") return;
3259
+ if (el.clientHeight === 0) {
3260
+ setWin((w) => w.kind === "all" ? w : { kind: "all" });
3261
+ return;
3262
+ }
3263
+ const grids = gridRefs.current;
3264
+ const g0 = grids.find(Boolean);
3265
+ if (!g0) return;
3266
+ const cs = getComputedStyle(g0);
3267
+ const cols2 = Math.max(1, cs.gridTemplateColumns.split(" ").filter(Boolean).length);
3268
+ const gap2 = parseFloat(cs.rowGap) || 0;
3269
+ const padTop = parseFloat(cs.paddingTop) || 0;
3270
+ const card = el.querySelector(".pos-items > .pos-item");
3271
+ const stride2 = (card?.offsetHeight || 128) + gap2;
3272
+ const base = el.getBoundingClientRect().top - el.scrollTop;
3273
+ const boxes = countsRef.current.map((count, i) => ({
3274
+ top: (grids[i]?.getBoundingClientRect().top ?? 0) - base + padTop,
3275
+ count
3276
+ }));
3277
+ const rows = visibleRows(
3278
+ boxes,
3279
+ cols2,
3280
+ stride2,
3281
+ el.scrollTop - CATALOG_OVERSCAN_PX,
3282
+ el.scrollTop + el.clientHeight + CATALOG_OVERSCAN_PX
3283
+ );
3284
+ setWin((w) => w.kind === "rows" && w.cols === cols2 && w.stride === stride2 && w.gap === gap2 && sameRows(w.rows, rows) ? w : { kind: "rows", cols: cols2, stride: stride2, gap: gap2, rows });
3285
+ }, []);
3286
+ const shape = countsRef.current.join(",");
3287
+ const wasHidden = useRef(hidden);
3288
+ useLayoutEffect(() => {
3289
+ if (wasHidden.current && !hidden && scrollRef.current) scrollRef.current.scrollTop = 0;
3290
+ wasHidden.current = hidden;
3291
+ }, [hidden]);
3292
+ const geometry = win.kind === "rows" ? `${win.cols}|${win.stride}|${win.gap}` : win.kind;
3293
+ useLayoutEffect(() => {
3294
+ if (!hidden) measure();
3295
+ }, [measure, hidden, shape, activeCategoryId, query, geometry]);
3296
+ useEffect(() => {
3297
+ const el = scrollRef.current;
3298
+ if (!el) return;
3299
+ let frame = 0;
3300
+ const onScroll = () => {
3301
+ if (frame) return;
3302
+ frame = requestAnimationFrame(() => {
3303
+ frame = 0;
3304
+ measure();
3305
+ });
3306
+ };
3307
+ el.addEventListener("scroll", onScroll, { passive: true });
3308
+ const ro = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(onScroll);
3309
+ ro?.observe(el);
3310
+ return () => {
3311
+ el.removeEventListener("scroll", onScroll);
3312
+ ro?.disconnect();
3313
+ if (frame) cancelAnimationFrame(frame);
3314
+ };
3315
+ }, [measure]);
3316
+ const dealRef = useRef({ key: "", at: 0 });
3317
+ const dealKey = `${hidden ? 1 : 0}|${activeCategoryId ?? ""}|${query}`;
3318
+ if (dealRef.current.key !== dealKey) dealRef.current = { key: dealKey, at: Date.now() };
3319
+ const late = win.kind === "rows" && Date.now() - dealRef.current.at > 400;
3320
+ const ranges = win.kind === "rows" ? win.rows.length === secoes.length ? win.rows : firstPassRows(countsRef.current, win.cols) : win.kind === "first" ? firstPassRows(countsRef.current, 4) : void 0;
3321
+ const cols = win.kind === "rows" ? win.cols : 4;
3322
+ const stride = win.kind === "rows" ? win.stride : 128 + 12;
3323
+ const gap = win.kind === "rows" ? win.gap : 12;
3324
+ return /* @__PURE__ */ jsxs(
3325
+ "div",
3326
+ {
3327
+ ref: scrollRef,
3328
+ className: "pos-scroll",
3329
+ style: { flex: 1, minHeight: 0, display: hidden ? "none" : "flex", flexDirection: "column" },
3330
+ children: [
3331
+ !searching && /* @__PURE__ */ jsx("div", { className: "pos-cats", children: categories.map((c, index) => /* @__PURE__ */ jsxs(
3332
+ PosButton,
3333
+ {
3334
+ className: "pos-cat",
3335
+ style: {
3336
+ background: `var(--pos-tone-${c.tone})`,
3337
+ ["--pos-stagger"]: `${Math.min(index, 11) * 20}ms`
3338
+ },
3339
+ "aria-pressed": c.id === activeCategoryId,
3340
+ onClick: () => {
3341
+ haptic("tap");
3342
+ onCategoryChange(c.id);
3343
+ },
3344
+ children: [
3345
+ /* @__PURE__ */ jsxs("span", { children: [
3346
+ /* @__PURE__ */ jsx("span", { className: "pos-cat__name", children: c.label }),
3347
+ c.itemCount != null && /* @__PURE__ */ jsxs("span", { className: "pos-cat__n pos-num", style: { display: "block" }, children: [
3348
+ c.itemCount,
3349
+ " itens"
3350
+ ] })
3351
+ ] }),
3352
+ /* @__PURE__ */ jsx("span", { className: "pos-cat__icon", children: c.icon ?? /* @__PURE__ */ jsx(CategoryIcon, { label: c.label }) })
3353
+ ]
3264
3354
  },
3265
- children: "Atualizar"
3266
- }
3267
- )
3268
- ] }),
3269
- shown.length === 0 ? (
3270
- /* A FRASE SEGUE A CAUSA. Sem busca, `query` é vazio e o título saía
3271
- «Nada encontrado para “”» — aspas vazias sobre uma busca que
3272
- ninguém fez. Ver `catalogEmpty.ts`. */
3273
- (() => {
3274
- const v = catalogEmpty(query);
3275
- return /* @__PURE__ */ jsx(EmptyState, { title: v.title, hint: v.hint });
3276
- })()
3277
- ) : (
3278
- /* UMA SEÇÃO POR CATEGORIA, com o nome acima da grade — é assim que o
3279
- V1 desenha o cardápio, e a razão aparece com "Todas": dezenove
3280
- categorias numa grade plana são duzentos e cinquenta cartões sem
3281
- nenhuma divisa, e achar "Coca Cola" vira rolagem cega.
3282
-
3283
- A ORDEM É A DO CARDÁPIO, não a alfabética — o V1 é explícito: é o
3284
- `menu_order` das categorias, a mesma ordem das fichas coloridas do
3285
- topo. Ordenar por nome faria a faixa de cima e a lista de baixo
3286
- discordarem, e é a faixa que o operador usa para se orientar.
3287
-
3288
- Com uma categoria escolhida sobra uma seção só, e o rótulo continua
3289
- pagando o espaço: ele confirma onde se está depois de rolar. */
3290
- secoes.map(([cat, itens]) => /* @__PURE__ */ jsxs("div", { children: [
3291
- /* @__PURE__ */ jsx("h3", { className: "pos-sect", children: cat?.label ?? "Sem categoria" }),
3292
- /* @__PURE__ */ jsx("div", { className: "pos-items", children: itens.map((item, index) => {
3293
- const quantity = quantities[item.id] ?? 0;
3294
- const category = categories.find((c) => c.id === item.categoryId);
3295
- const filled = quantity > 0;
3296
- return /* @__PURE__ */ jsxs(
3297
- ItemCard,
3355
+ c.id
3356
+ )) }),
3357
+ !searching && /* @__PURE__ */ jsx("hr", { className: "pos-cats-rule" }),
3358
+ onRefreshMenu && /* @__PURE__ */ jsxs("div", { style: {
3359
+ display: "flex",
3360
+ alignItems: "center",
3361
+ justifyContent: "flex-end",
3362
+ gap: "0.5rem",
3363
+ padding: "0 0.75rem 0.375rem",
3364
+ fontSize: 11,
3365
+ opacity: 0.55
3366
+ }, children: [
3367
+ /* @__PURE__ */ jsx("span", { children: menuLabel }),
3368
+ /* @__PURE__ */ jsx(
3369
+ PosButton,
3298
3370
  {
3299
- quantity,
3300
- filled,
3301
- onQuickAdd: () => {
3371
+ type: "button",
3372
+ className: "fz-tap",
3373
+ onClick: () => {
3302
3374
  haptic("tap");
3303
- onAdd(item);
3375
+ onRefreshMenu();
3304
3376
  },
3305
- label: item.name,
3306
- delayMs: Math.min(index, 11) * 24,
3307
- tone: category?.tone,
3308
- children: [
3309
- /* @__PURE__ */ jsxs("span", { className: "pos-item__top", children: [
3310
- item.imageUrl ? /* @__PURE__ */ jsx("span", { className: "pos-item__img", children: /* @__PURE__ */ jsx("img", { src: item.imageUrl, alt: "", loading: "lazy", decoding: "async" }) }) : null,
3311
- /* @__PURE__ */ jsxs("span", { className: "pos-item__id", children: [
3312
- item.route && /* @__PURE__ */ jsx("span", { className: "pos-item__route", children: item.route }),
3313
- /* @__PURE__ */ jsx("span", { className: "pos-item__name", children: item.name }),
3314
- /* @__PURE__ */ jsxs("span", { className: "pos-item__price pos-num", children: [
3315
- formatCents(item.priceCents),
3316
- item.code && ` \xB7 ${item.code}`
3317
- ] })
3318
- ] })
3319
- ] }),
3320
- /* @__PURE__ */ jsx("span", { className: "pos-item__foot", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxs("span", { className: "pos-step", children: [
3321
- /* @__PURE__ */ jsx(
3322
- PosButton,
3323
- {
3324
- className: "pos-step__b",
3325
- onClick: () => {
3326
- haptic("tap");
3327
- onRemove(item);
3328
- },
3329
- disabled: (pending?.[item.id] ?? 0) === 0,
3330
- title: quantity > 0 && (pending?.[item.id] ?? 0) === 0 ? "J\xE1 foi para a cozinha \u2014 use Cancelamento" : void 0,
3331
- "aria-label": `Tirar um ${item.name}`,
3332
- children: /* @__PURE__ */ jsx(Minus, { size: 16, strokeWidth: 2.6, "aria-hidden": true })
3333
- }
3334
- ),
3335
- /* @__PURE__ */ jsx("span", { className: "pos-step__n", "data-zero": quantity === 0 ? "" : void 0, children: quantity }),
3336
- /* @__PURE__ */ jsx(
3337
- PosButton,
3377
+ disabled: menuRefreshing,
3378
+ style: {
3379
+ background: "transparent",
3380
+ border: 0,
3381
+ padding: "0.125rem 0.25rem",
3382
+ color: "inherit",
3383
+ font: "inherit",
3384
+ textDecoration: "underline",
3385
+ cursor: menuRefreshing ? "default" : "pointer"
3386
+ },
3387
+ children: "Atualizar"
3388
+ }
3389
+ )
3390
+ ] }),
3391
+ shown.length === 0 ? (
3392
+ /* A FRASE SEGUE A CAUSA. Sem busca, `query` é vazio e o título saía
3393
+ «Nada encontrado para “”» — aspas vazias sobre uma busca que
3394
+ ninguém fez. Ver `catalogEmpty.ts`. */
3395
+ (() => {
3396
+ const v = catalogEmpty(query);
3397
+ return /* @__PURE__ */ jsx(EmptyState, { title: v.title, hint: v.hint });
3398
+ })()
3399
+ ) : (
3400
+ /* UMA SEÇÃO POR CATEGORIA, com o nome acima da grade — é assim que o
3401
+ V1 desenha o cardápio, e a razão aparece com "Todas": dezenove
3402
+ categorias numa grade plana são duzentos e cinquenta cartões sem
3403
+ nenhuma divisa, e achar "Coca Cola" vira rolagem cega.
3404
+
3405
+ A ORDEM É A DO CARDÁPIO, não a alfabética — o V1 é explícito: é o
3406
+ `menu_order` das categorias, a mesma ordem das fichas coloridas do
3407
+ topo. Ordenar por nome faria a faixa de cima e a lista de baixo
3408
+ discordarem, e é a faixa que o operador usa para se orientar.
3409
+
3410
+ Com uma categoria escolhida sobra uma seção só, e o rótulo continua
3411
+ pagando o espaço: ele confirma onde se está depois de rolar. */
3412
+ secoes.map(([cat, itens], sectionIndex) => {
3413
+ const rows = Math.ceil(itens.length / cols);
3414
+ const [first, end] = ranges?.[sectionIndex] ?? [0, rows];
3415
+ const windowed = ranges ? itens.slice(first * cols, end * cols) : itens;
3416
+ const before = ranges ? first : 0;
3417
+ const after = ranges ? rows - Math.max(end, first) : 0;
3418
+ return /* @__PURE__ */ jsxs("div", { children: [
3419
+ /* @__PURE__ */ jsx("h3", { className: "pos-sect", children: cat?.label ?? "Sem categoria" }),
3420
+ /* @__PURE__ */ jsxs("div", { className: "pos-items", ref: (n) => {
3421
+ gridRefs.current[sectionIndex] = n;
3422
+ }, children: [
3423
+ windowed.length === 0 ? /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "pos-items__gap", style: { height: spacerHeight(rows, stride, gap) } }) : before > 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "pos-items__gap", style: { height: spacerHeight(before, stride, gap) } }),
3424
+ windowed.map((item, index) => {
3425
+ const quantity = quantities[item.id] ?? 0;
3426
+ const category = categories.find((c) => c.id === item.categoryId);
3427
+ const filled = quantity > 0;
3428
+ return /* @__PURE__ */ jsxs(
3429
+ ItemCard,
3338
3430
  {
3339
- className: "pos-step__b",
3340
- onClick: () => {
3431
+ quantity,
3432
+ filled,
3433
+ onQuickAdd: () => {
3341
3434
  haptic("tap");
3342
3435
  onAdd(item);
3343
3436
  },
3344
- "aria-label": `Mais um ${item.name}`,
3345
- children: /* @__PURE__ */ jsx(Plus, { size: 16, strokeWidth: 2.6, "aria-hidden": true })
3346
- }
3347
- )
3348
- ] }) })
3349
- ]
3350
- },
3351
- item.id
3352
- );
3353
- }) })
3354
- ] }, cat?.id ?? "__sem"))
3355
- )
3356
- ] });
3437
+ label: item.name,
3438
+ delayMs: Math.min(index, 11) * 24,
3439
+ late,
3440
+ tone: category?.tone,
3441
+ children: [
3442
+ /* @__PURE__ */ jsxs("span", { className: "pos-item__top", children: [
3443
+ item.imageUrl ? /* @__PURE__ */ jsx("span", { className: "pos-item__img", children: /* @__PURE__ */ jsx("img", { src: item.imageUrl, alt: "", loading: "lazy", decoding: "async" }) }) : null,
3444
+ /* @__PURE__ */ jsxs("span", { className: "pos-item__id", children: [
3445
+ item.route && /* @__PURE__ */ jsx("span", { className: "pos-item__route", children: item.route }),
3446
+ /* @__PURE__ */ jsx("span", { className: "pos-item__name", children: item.name }),
3447
+ /* @__PURE__ */ jsxs("span", { className: "pos-item__price pos-num", children: [
3448
+ formatCents(item.priceCents),
3449
+ item.code && ` \xB7 ${item.code}`
3450
+ ] })
3451
+ ] })
3452
+ ] }),
3453
+ /* @__PURE__ */ jsx("span", { className: "pos-item__foot", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxs("span", { className: "pos-step", children: [
3454
+ /* @__PURE__ */ jsx(
3455
+ PosButton,
3456
+ {
3457
+ className: "pos-step__b",
3458
+ onClick: () => {
3459
+ haptic("tap");
3460
+ onRemove(item);
3461
+ },
3462
+ disabled: (pending?.[item.id] ?? 0) === 0,
3463
+ title: quantity > 0 && (pending?.[item.id] ?? 0) === 0 ? "J\xE1 foi para a cozinha \u2014 use Cancelamento" : void 0,
3464
+ "aria-label": `Tirar um ${item.name}`,
3465
+ children: /* @__PURE__ */ jsx(Minus, { size: 16, strokeWidth: 2.6, "aria-hidden": true })
3466
+ }
3467
+ ),
3468
+ /* @__PURE__ */ jsx("span", { className: "pos-step__n", "data-zero": quantity === 0 ? "" : void 0, children: quantity }),
3469
+ /* @__PURE__ */ jsx(
3470
+ PosButton,
3471
+ {
3472
+ className: "pos-step__b",
3473
+ onClick: () => {
3474
+ haptic("tap");
3475
+ onAdd(item);
3476
+ },
3477
+ "aria-label": `Mais um ${item.name}`,
3478
+ children: /* @__PURE__ */ jsx(Plus, { size: 16, strokeWidth: 2.6, "aria-hidden": true })
3479
+ }
3480
+ )
3481
+ ] }) })
3482
+ ]
3483
+ },
3484
+ item.id
3485
+ );
3486
+ }),
3487
+ windowed.length > 0 && after > 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "pos-items__gap", style: { height: spacerHeight(after, stride, gap) } })
3488
+ ] })
3489
+ ] }, cat?.id ?? "__sem");
3490
+ })
3491
+ )
3492
+ ]
3493
+ }
3494
+ );
3357
3495
  }
3358
3496
  function ShiftView({ positions, staff }) {
3359
3497
  const open = positions.filter((p) => p.status !== "free");
@@ -3378,7 +3516,7 @@ function Stat({ label, value }) {
3378
3516
  /* @__PURE__ */ jsx("div", { className: "pos-t-display pos-num", children: value })
3379
3517
  ] });
3380
3518
  }
3381
- function ItemCard({ quantity, filled, delayMs, tone, onQuickAdd, label, children }) {
3519
+ function ItemCard({ quantity, filled, delayMs, late, tone, onQuickAdd, label, children }) {
3382
3520
  return /* @__PURE__ */ jsx(
3383
3521
  "div",
3384
3522
  {
@@ -3387,6 +3525,7 @@ function ItemCard({ quantity, filled, delayMs, tone, onQuickAdd, label, children
3387
3525
  tabIndex: 0,
3388
3526
  "aria-label": `${label}, adicionar \xE0 comanda`,
3389
3527
  "data-filled": filled,
3528
+ "data-late": late || void 0,
3390
3529
  onClick: onQuickAdd,
3391
3530
  onKeyDown: (e) => {
3392
3531
  if (e.key === "Enter" || e.key === " ") {
@@ -8760,6 +8899,10 @@ function PosScreen({
8760
8899
  const positionsDoSalao = query.trim() && !plantaNaTela ? positionsDoFiltro.filter((p) => tableMatches({ label: p.label, name: p.customerName }, query)) : positionsDoFiltro;
8761
8900
  const wideRail = useWideRail();
8762
8901
  const headHasContent = menu?.view === "floor" || !wideRail;
8902
+ const onCatalog = menu?.view === "catalog";
8903
+ const catalogShown = useDeferredValue(onCatalog);
8904
+ const [catalogKept, setCatalogKept] = useState(false);
8905
+ if (catalogShown && !catalogKept) setCatalogKept(true);
8763
8906
  const visibleMenus = config.menus.filter((m) => !m.hidden);
8764
8907
  const phoneOrder = [...visibleMenus].sort((a, b) => {
8765
8908
  const peso = (m) => m.id === "delivery" ? 1 : m.id === "retirada" ? 2 : 0;
@@ -9095,10 +9238,11 @@ function PosScreen({
9095
9238
  onToggleTableInZone: pracaNaFicha && onTogglaMesaNaPraca ? (tableId, how) => onTogglaMesaNaPraca(pracaNaFicha, tableId, how) : void 0
9096
9239
  }
9097
9240
  ),
9098
- menu?.view === "catalog" && loading && /* @__PURE__ */ jsx(CatalogSkeleton, {}),
9099
- menu?.view === "catalog" && !loading && /* @__PURE__ */ jsx(
9241
+ onCatalog && (loading || !catalogShown) && /* @__PURE__ */ jsx(CatalogSkeleton, {}),
9242
+ !loading && catalogKept && /* @__PURE__ */ jsx(
9100
9243
  CatalogView,
9101
9244
  {
9245
+ hidden: !(onCatalog && catalogShown),
9102
9246
  categories,
9103
9247
  items,
9104
9248
  activeCategoryId,
@@ -9690,12 +9834,26 @@ function Dock({ positions, activeId, onSelect, now, onDiscard, paidByPosition })
9690
9834
  useEffect(() => {
9691
9835
  const el = activeRef.current;
9692
9836
  if (!el) return;
9693
- const still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
9694
- el.scrollIntoView?.({
9695
- behavior: still ? "auto" : "smooth",
9696
- block: "nearest",
9697
- inline: "nearest"
9837
+ const go = () => {
9838
+ const still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
9839
+ el.scrollIntoView?.({
9840
+ behavior: still ? "auto" : "smooth",
9841
+ block: "nearest",
9842
+ inline: "nearest"
9843
+ });
9844
+ };
9845
+ if (document.visibilityState !== "visible") {
9846
+ go();
9847
+ return;
9848
+ }
9849
+ let timer = 0;
9850
+ const frame = requestAnimationFrame(() => {
9851
+ timer = window.setTimeout(go, 0);
9698
9852
  });
9853
+ return () => {
9854
+ cancelAnimationFrame(frame);
9855
+ clearTimeout(timer);
9856
+ };
9699
9857
  }, [activeId]);
9700
9858
  if (positions.length === 0) return null;
9701
9859
  const rotulo = `${positions.length} comanda${positions.length === 1 ? "" : "s"} aberta${positions.length === 1 ? "" : "s"}`;
@@ -9874,7 +10032,7 @@ function DiscountSheet({
9874
10032
  const [raw, setRaw] = useState("");
9875
10033
  const [reasonId, setReasonId] = useState("");
9876
10034
  const [authorizerId, setAuthorizerId] = useState("");
9877
- const [memo2, setMemo] = useState("");
10035
+ const [memo3, setMemo] = useState("");
9878
10036
  const comAbatimento = useMemo(
9879
10037
  () => ticket.lines.filter((l) => (l.discountCents ?? 0) > 0),
9880
10038
  [ticket.lines]
@@ -9888,7 +10046,7 @@ function DiscountSheet({
9888
10046
  const reason = reasons.find((r) => r.id === reasonId);
9889
10047
  const authorizer = authorizers.find((a) => a.id === authorizerId);
9890
10048
  const percentEquivalente = base > 0 ? cents / base * 100 : 0;
9891
- const impedimento = picked.size === 0 ? "Escolha ao menos um item." : value <= 0 ? "Informe o desconto." : mode === "percent" && value > 100 ? "Percentual n\xE3o pode ser maior que 100%." : cents > base ? "O desconto \xE9 maior que o valor dos itens escolhidos." : !authorizerId ? "Selecione o usu\xE1rio autorizador." : !reasonId && !memo2.trim() ? "Selecione um motivo ou informe no memorando." : capRefusal(percentEquivalente, {
10049
+ const impedimento = picked.size === 0 ? "Escolha ao menos um item." : value <= 0 ? "Informe o desconto." : mode === "percent" && value > 100 ? "Percentual n\xE3o pode ser maior que 100%." : cents > base ? "O desconto \xE9 maior que o valor dos itens escolhidos." : !authorizerId ? "Selecione o usu\xE1rio autorizador." : !reasonId && !memo3.trim() ? "Selecione um motivo ou informe no memorando." : capRefusal(percentEquivalente, {
9892
10050
  reasonMax: reason?.maxPercent,
9893
10051
  authorizerMax: authorizer?.maxPercent,
9894
10052
  authorizerName: authorizer?.name
@@ -10074,7 +10232,7 @@ function DiscountSheet({
10074
10232
  "textarea",
10075
10233
  {
10076
10234
  className: "pos-dsc__memo",
10077
- value: memo2,
10235
+ value: memo3,
10078
10236
  onChange: (e) => setMemo(e.target.value),
10079
10237
  placeholder: "Memorando",
10080
10238
  rows: 2,
@@ -10096,7 +10254,7 @@ function DiscountSheet({
10096
10254
  value,
10097
10255
  authorizerId: authorizerId || void 0,
10098
10256
  reasonId: reasonId || void 0,
10099
- memo: memo2.trim() || void 0
10257
+ memo: memo3.trim() || void 0
10100
10258
  }),
10101
10259
  children: [
10102
10260
  /* @__PURE__ */ jsx(Tag, { size: 17, strokeWidth: 2.2, "aria-hidden": true }),
@@ -10566,7 +10724,7 @@ function CancelSheet({ ticket, party = [], reasons, error, onCancel, onConfirm }
10566
10724
  const [qty, setQty] = useState({});
10567
10725
  const [tudo, setTudo] = useState(false);
10568
10726
  const [reasonId, setReasonId] = useState("");
10569
- const [memo2, setMemo] = useState("");
10727
+ const [memo3, setMemo] = useState("");
10570
10728
  const escolhidas = tudo ? enviados.map((l) => ({ lineId: l.id, quantity: l.quantity })) : enviados.map((l) => ({ lineId: l.id, quantity: qty[l.id] ?? 0 })).filter((x) => x.quantity > 0);
10571
10729
  const totalCents = escolhidas.reduce((sum, x) => {
10572
10730
  const linha = enviados.find((l) => l.id === x.lineId);
@@ -10577,7 +10735,7 @@ function CancelSheet({ ticket, party = [], reasons, error, onCancel, onConfirm }
10577
10735
  [enviados, party]
10578
10736
  );
10579
10737
  const temGente = grupos.length > 1 || grupos.length === 1 && grupos[0].guestId !== null;
10580
- const impedimento = enviados.length === 0 ? "Nenhum item enviado para cancelar." : escolhidas.length === 0 ? "Escolha quantas unidades cancelar." : !reasonId && !memo2.trim() ? "Selecione um motivo ou informe no memorando." : void 0;
10738
+ const impedimento = enviados.length === 0 ? "Nenhum item enviado para cancelar." : escolhidas.length === 0 ? "Escolha quantas unidades cancelar." : !reasonId && !memo3.trim() ? "Selecione um motivo ou informe no memorando." : void 0;
10581
10739
  const linhaDoItem = (l) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("div", { className: "pos-svc__guest", children: [
10582
10740
  /* @__PURE__ */ jsxs("span", { className: "pos-dsc__name", children: [
10583
10741
  /* @__PURE__ */ jsxs("span", { className: "pos-dsc__n", children: [
@@ -10654,7 +10812,7 @@ function CancelSheet({ ticket, party = [], reasons, error, onCancel, onConfirm }
10654
10812
  "textarea",
10655
10813
  {
10656
10814
  className: "pos-dsc__memo",
10657
- value: memo2,
10815
+ value: memo3,
10658
10816
  onChange: (e) => setMemo(e.target.value),
10659
10817
  placeholder: "Memorando",
10660
10818
  rows: 2,
@@ -10680,7 +10838,7 @@ function CancelSheet({ ticket, party = [], reasons, error, onCancel, onConfirm }
10680
10838
  onClick: () => onConfirm({
10681
10839
  quantities: escolhidas,
10682
10840
  reasonId: reasonId || void 0,
10683
- memo: memo2.trim() || void 0
10841
+ memo: memo3.trim() || void 0
10684
10842
  }),
10685
10843
  children: "Cancelar itens"
10686
10844
  }