@retro-antlitz-kartei/react-editor 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -18,11 +18,11 @@ import { AvatarEditor } from "@retro-antlitz-kartei/react-editor";
18
18
  <AvatarEditor seed="Ada" onChange={(cfg) => save(cfg)} />;
19
19
 
20
20
  // Controlled
21
- const [cfg, setCfg] = useState(() => normalizeConfig({ hut: 2 }));
21
+ const [cfg, setCfg] = useState(() => normalizeConfig({ hat: "crown" }));
22
22
  <AvatarEditor value={cfg} onChange={setCfg} defaultLayout="kompakt" />;
23
23
  ```
24
24
 
25
- The editor renders a live preview, part cyclers, skin/clothing/background
25
+ The editor renders a live preview, part cyclers, skin/top/trousers/background
26
26
  swatches, a view switcher (left/front/right), a layout switcher
27
27
  (arcade/compact/wanted), a shareable code box, a seed input, and a tap-to-open
28
28
  **combat modal** with pose buttons.
package/dist/index.cjs CHANGED
@@ -178,7 +178,7 @@ function theme(layout) {
178
178
  mono: MONO,
179
179
  body: BODY,
180
180
  radius: "14px",
181
- titleText: "AVATAR EDITOR",
181
+ titleText: "@retro-antlitz-kartei AVATAR EDITOR",
182
182
  glow: false,
183
183
  paper: false
184
184
  };
@@ -233,20 +233,37 @@ var FONT_HREFS = [
233
233
  "https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap"
234
234
  ];
235
235
  var PART_CONTROLS = [
236
- { key: "gender", label: "BUILD" },
237
- { key: "hut", label: "HAT" },
238
- { key: "haare", label: "HAIR" },
239
- { key: "ohren", label: "EARS" },
240
- { key: "nase", label: "NOSE" },
241
- { key: "acc", label: "ACCESSORY" },
242
- { key: "torso", label: "TOP" },
243
- { key: "hose", label: "TROUSERS" }
236
+ { key: "build", label: "BUILD" },
237
+ { key: "hat", label: "HAT" },
238
+ { key: "hair", label: "HAIR" },
239
+ { key: "ears", label: "EARS" },
240
+ { key: "nose", label: "NOSE" },
241
+ { key: "accessory", label: "ACCESSORY" },
242
+ { key: "top", label: "TOP" },
243
+ { key: "trousers", label: "TROUSERS" },
244
+ { key: "shoes", label: "SHOES" }
244
245
  ];
245
246
  var VIEW_OPTIONS = [
246
247
  { key: "left", name: "LEFT" },
247
248
  { key: "front", name: "FRONT" },
248
249
  { key: "right", name: "RIGHT" }
249
250
  ];
251
+ function useNarrow(ref, breakpoint = 640) {
252
+ const [narrow, setNarrow] = react.useState(
253
+ () => typeof window !== "undefined" && window.innerWidth < breakpoint
254
+ );
255
+ react.useEffect(() => {
256
+ const el = ref.current;
257
+ if (!el || typeof ResizeObserver === "undefined") return;
258
+ const ro = new ResizeObserver((entries) => {
259
+ const w = entries[0]?.contentRect.width ?? 0;
260
+ if (w > 0) setNarrow(w < breakpoint);
261
+ });
262
+ ro.observe(el);
263
+ return () => ro.disconnect();
264
+ }, [ref, breakpoint]);
265
+ return narrow;
266
+ }
250
267
  function useControllable(controlled, initial, onChange) {
251
268
  const [internal, setInternal] = react.useState(initial);
252
269
  const value = controlled !== void 0 ? controlled : internal;
@@ -272,6 +289,7 @@ function AvatarEditor(props) {
272
289
  showCombat = true,
273
290
  showCode = true,
274
291
  showSeed = true,
292
+ onMagic,
275
293
  loadFonts = true,
276
294
  className,
277
295
  style
@@ -288,7 +306,10 @@ function AvatarEditor(props) {
288
306
  const [codeInput, setCodeInput] = react.useState("");
289
307
  const [codeMsg, setCodeMsg] = react.useState("");
290
308
  const [seedInput, setSeedInput] = react.useState(seed ?? "");
309
+ const stageRef = react.useRef(null);
310
+ const narrow = useNarrow(stageRef);
291
311
  const t = theme(layout);
312
+ const stacked = narrow || t.mainDir === "column";
292
313
  react.useEffect(() => {
293
314
  if (!loadFonts || typeof document === "undefined") return;
294
315
  for (const href of FONT_HREFS) {
@@ -298,18 +319,13 @@ function AvatarEditor(props) {
298
319
  link.href = href;
299
320
  document.head.appendChild(link);
300
321
  }
301
- if (!document.getElementById("rak-keyframes")) {
302
- const s = document.createElement("style");
303
- s.id = "rak-keyframes";
304
- s.textContent = "@keyframes rak-coinpulse{0%,100%{transform:translateY(0)}50%{transform:translateY(-2px)}}";
305
- document.head.appendChild(s);
306
- }
307
322
  }, [loadFonts]);
308
323
  const patch = react.useCallback((p) => setConfig({ ...config, ...p }), [config, setConfig]);
309
324
  const cycle = react.useCallback(
310
325
  (key, dir) => {
311
- const len = generator.PART_NAMES[key].length;
312
- patch({ [key]: (config[key] + dir + len) % len });
326
+ const ids = generator.PARTS[key];
327
+ const cur = ids.indexOf(config[key]);
328
+ patch({ [key]: ids[(cur + dir + ids.length) % ids.length] });
313
329
  },
314
330
  [config, patch]
315
331
  );
@@ -400,24 +416,25 @@ function AvatarEditor(props) {
400
416
  border: "2px solid " + (active ? t.accent : t.paper ? "#00000033" : "#ffffff44"),
401
417
  boxShadow: active ? "0 0 0 2px " + t.accent + "66" : "none"
402
418
  });
403
- const nameDisplay = generator.PART_NAMES.torso[config.torso].toUpperCase();
419
+ const nameDisplay = generator.partLabel("top", config.top).toUpperCase();
404
420
  const partRow = (key, label) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: rowStyle, children: [
405
421
  /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => cycle(key, -1), style: arrowStyle, children: "\u25C0" }),
406
422
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1, minWidth: 0, textAlign: "center" }, children: [
407
423
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: partLabelStyle, children: label }),
408
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: partValueStyle, children: generator.PART_NAMES[key][config[key]] })
424
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: partValueStyle, children: generator.partLabel(key, config[key]) })
409
425
  ] }),
410
426
  /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => cycle(key, 1), style: arrowStyle, children: "\u25B6" })
411
427
  ] }, key);
412
428
  return /* @__PURE__ */ jsxRuntime.jsxs(
413
429
  "div",
414
430
  {
431
+ ref: stageRef,
415
432
  className,
416
433
  style: {
417
434
  minHeight: "100%",
418
435
  width: "100%",
419
436
  background: t.stageBg,
420
- padding: "24px",
437
+ padding: narrow ? "10px" : "24px",
421
438
  display: "flex",
422
439
  flexDirection: "column",
423
440
  alignItems: "center",
@@ -454,11 +471,11 @@ function AvatarEditor(props) {
454
471
  {
455
472
  style: {
456
473
  width: "100%",
457
- maxWidth: t.mainDir === "row" ? "860px" : "540px",
474
+ maxWidth: narrow ? "100%" : t.mainDir === "row" ? "860px" : "540px",
458
475
  background: t.cabinet,
459
- border: "4px solid " + t.cabinetBorder,
460
- borderRadius: t.radius,
461
- boxShadow: "0 18px 50px rgba(0,0,0,.5)" + (t.glow ? ", 0 0 30px " + t.accent + "33" : ""),
476
+ border: narrow ? "none" : "4px solid " + t.cabinetBorder,
477
+ borderRadius: narrow ? "10px" : t.radius,
478
+ boxShadow: narrow ? "none" : "0 18px 50px rgba(0,0,0,.5)" + (t.glow ? ", 0 0 30px " + t.accent + "33" : ""),
462
479
  overflow: "hidden"
463
480
  },
464
481
  children: [
@@ -467,29 +484,32 @@ function AvatarEditor(props) {
467
484
  {
468
485
  style: {
469
486
  background: t.marqueeBg,
470
- padding: "14px",
487
+ padding: narrow ? "10px" : "14px",
471
488
  display: "flex",
472
- gap: "12px",
489
+ gap: narrow ? "8px" : "12px",
473
490
  alignItems: "center",
474
491
  justifyContent: "center",
475
- borderBottom: "3px solid " + t.cabinetBorder
492
+ flexWrap: "wrap",
493
+ borderBottom: narrow ? "none" : "3px solid " + t.cabinetBorder
476
494
  },
477
495
  children: [
478
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { width: "10px", height: "10px", borderRadius: "50%", background: t.accent, boxShadow: "0 0 8px " + t.accent } }),
496
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { width: "10px", height: "10px", borderRadius: "50%", background: t.accent, boxShadow: "0 0 8px " + t.accent, flex: "0 0 auto" } }),
479
497
  /* @__PURE__ */ jsxRuntime.jsx(
480
498
  "span",
481
499
  {
482
500
  style: {
483
501
  fontFamily: t.mono,
484
- fontSize: t.titleText === "WANTED" ? "20px" : "15px",
502
+ fontSize: t.titleText === "WANTED" ? "20px" : narrow ? "10px" : "15px",
485
503
  color: t.title,
486
- letterSpacing: "2px",
487
- textShadow: t.glow ? "0 0 10px " + t.accent : "2px 2px 0 #00000044"
504
+ letterSpacing: narrow ? "1px" : "2px",
505
+ textShadow: t.glow ? "0 0 10px " + t.accent : "2px 2px 0 #00000044",
506
+ textAlign: "center",
507
+ lineHeight: 1.5
488
508
  },
489
509
  children: t.titleText
490
510
  }
491
511
  ),
492
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { width: "10px", height: "10px", borderRadius: "50%", background: t.accent, boxShadow: "0 0 8px " + t.accent } })
512
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { width: "10px", height: "10px", borderRadius: "50%", background: t.accent, boxShadow: "0 0 8px " + t.accent, flex: "0 0 auto" } })
493
513
  ]
494
514
  }
495
515
  ),
@@ -498,21 +518,23 @@ function AvatarEditor(props) {
498
518
  {
499
519
  style: {
500
520
  display: "flex",
501
- flexDirection: t.mainDir,
521
+ flexDirection: stacked ? "column" : "row",
502
522
  gap: "16px",
503
- padding: "18px",
504
- alignItems: t.mainDir === "row" ? "stretch" : "center"
523
+ padding: narrow ? "10px" : "18px",
524
+ alignItems: stacked ? "center" : "stretch"
505
525
  },
506
526
  children: [
507
527
  /* @__PURE__ */ jsxRuntime.jsxs(
508
528
  "div",
509
529
  {
510
530
  style: {
511
- flex: t.mainDir === "row" ? "0 0 300px" : "0 0 auto",
531
+ flex: stacked ? "0 0 auto" : "0 0 300px",
512
532
  display: "flex",
513
533
  flexDirection: "column",
514
534
  gap: "8px",
515
- alignItems: "center"
535
+ alignItems: "center",
536
+ width: stacked ? "100%" : void 0,
537
+ maxWidth: "100%"
516
538
  },
517
539
  children: [
518
540
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -529,7 +551,15 @@ function AvatarEditor(props) {
529
551
  },
530
552
  onClick: showCombat ? () => setModalOpen(true) : void 0,
531
553
  children: [
532
- /* @__PURE__ */ jsxRuntime.jsx(AvatarPreview, { config, width: 320, height: 400, style: { width: "264px", height: "330px", borderRadius: "3px" } }),
554
+ /* @__PURE__ */ jsxRuntime.jsx(
555
+ AvatarPreview,
556
+ {
557
+ config,
558
+ width: 320,
559
+ height: 400,
560
+ style: { width: "264px", maxWidth: "100%", height: "auto", aspectRatio: "320 / 400", borderRadius: "3px" }
561
+ }
562
+ ),
533
563
  /* @__PURE__ */ jsxRuntime.jsx(
534
564
  "div",
535
565
  {
@@ -604,10 +634,10 @@ function AvatarEditor(props) {
604
634
  },
605
635
  o.key
606
636
  )) }),
607
- partRow("mund", "MOUTH"),
637
+ partRow("mouth", "MOUTH"),
608
638
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: swatchRowStyle, children: [
609
639
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: swatchLabelStyle, children: "BACKGROUND" }),
610
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: "6px", flexWrap: "wrap" }, children: generator.BG.map((c, i) => /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => patch({ bg: i }), style: swatchBtn(c, config.bg === i) }, c)) })
640
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: "6px", flexWrap: "wrap" }, children: generator.BG.map((c) => /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => patch({ background: c }), style: swatchBtn(c, config.background === c) }, c)) })
611
641
  ] }),
612
642
  showCode && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
613
643
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: dividerStyle }),
@@ -666,7 +696,7 @@ function AvatarEditor(props) {
666
696
  style: {
667
697
  flex: 1,
668
698
  minWidth: 0,
669
- width: t.mainDir === "column" ? "100%" : "auto",
699
+ width: stacked ? "100%" : "auto",
670
700
  display: "flex",
671
701
  flexDirection: "column",
672
702
  gap: "9px",
@@ -680,56 +710,66 @@ function AvatarEditor(props) {
680
710
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: dividerStyle }),
681
711
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: swatchRowStyle, children: [
682
712
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: swatchLabelStyle, children: "SKIN" }),
683
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: "6px", flexWrap: "wrap" }, children: generator.SKIN.map((c, i) => /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => patch({ skin: i }), style: swatchBtn(c, config.skin === i) }, c)) })
713
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: "6px", flexWrap: "wrap" }, children: generator.SKIN.map((c) => /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => patch({ skin: c }), style: swatchBtn(c, config.skin === c) }, c)) })
714
+ ] }),
715
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: swatchRowStyle, children: [
716
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: swatchLabelStyle, children: "HAIR" }),
717
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: "6px", flexWrap: "wrap" }, children: generator.HAIRS.map((c) => /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => patch({ hairColor: c }), style: swatchBtn(c, config.hairColor === c) }, c)) })
684
718
  ] }),
685
719
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: swatchRowStyle, children: [
686
720
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: swatchLabelStyle, children: "CLOTHING" }),
687
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: "6px", flexWrap: "wrap" }, children: generator.CLOTH.map((c, i) => /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => patch({ cloth: i }), style: swatchBtn(c, config.cloth === i) }, c)) })
721
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: "6px", flexWrap: "wrap" }, children: generator.CLOTH.map((c) => /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => patch({ topColor: c }), style: swatchBtn(c, config.topColor === c) }, c)) })
722
+ ] }),
723
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: swatchRowStyle, children: [
724
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: swatchLabelStyle, children: "TROUSERS" }),
725
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: "6px", flexWrap: "wrap" }, children: generator.PANTS.map((c) => /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => patch({ trousersColor: c }), style: swatchBtn(c, config.trousersColor === c) }, c)) })
688
726
  ] }),
689
727
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: dividerStyle }),
690
- /* @__PURE__ */ jsxRuntime.jsx(
691
- "button",
692
- {
693
- onClick: onRandom,
694
- style: {
695
- fontFamily: t.mono,
696
- fontSize: "10px",
697
- letterSpacing: "1px",
698
- cursor: "pointer",
699
- padding: "12px",
700
- borderRadius: t.paper ? "3px" : "9px",
701
- marginTop: "2px",
702
- border: "3px solid " + t.title,
703
- background: t.accent,
704
- color: "#10131c"
705
- },
706
- children: "\u{1F3B2} RANDOM"
707
- }
708
- )
728
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "8px", marginTop: "2px" }, children: [
729
+ onMagic && /* @__PURE__ */ jsxRuntime.jsx(
730
+ "button",
731
+ {
732
+ onClick: onMagic,
733
+ style: {
734
+ fontFamily: t.mono,
735
+ fontSize: "10px",
736
+ letterSpacing: "1px",
737
+ cursor: "pointer",
738
+ padding: "12px",
739
+ borderRadius: t.paper ? "3px" : "9px",
740
+ border: "3px solid " + t.title,
741
+ background: "#9b5de5",
742
+ color: "#fff",
743
+ flex: "0 0 auto"
744
+ },
745
+ children: "\u2728 MAGIC"
746
+ }
747
+ ),
748
+ /* @__PURE__ */ jsxRuntime.jsx(
749
+ "button",
750
+ {
751
+ onClick: onRandom,
752
+ style: {
753
+ fontFamily: t.mono,
754
+ fontSize: "10px",
755
+ letterSpacing: "1px",
756
+ cursor: "pointer",
757
+ padding: "12px",
758
+ borderRadius: t.paper ? "3px" : "9px",
759
+ border: "3px solid " + t.title,
760
+ background: t.accent,
761
+ color: "#10131c",
762
+ flex: 1
763
+ },
764
+ children: "\u{1F3B2} RANDOM"
765
+ }
766
+ )
767
+ ] })
709
768
  ]
710
769
  }
711
770
  )
712
771
  ]
713
772
  }
714
- ),
715
- /* @__PURE__ */ jsxRuntime.jsxs(
716
- "div",
717
- {
718
- style: {
719
- background: t.marqueeBg,
720
- padding: "10px",
721
- display: "flex",
722
- gap: "10px",
723
- alignItems: "center",
724
- justifyContent: "center",
725
- color: t.title,
726
- borderTop: "3px solid " + t.cabinetBorder
727
- },
728
- children: [
729
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: t.accent, fontSize: "14px", animation: "rak-coinpulse 1.2s ease-in-out infinite" }, children: "\u25CF" }),
730
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontFamily: t.mono, fontSize: "8px", letterSpacing: "1px" }, children: "INSERT COIN \xB7 SATIRE EDITION" })
731
- ]
732
- }
733
773
  )
734
774
  ]
735
775
  }