@ntmats/hc-ui 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ButtonHTMLAttributes, type HTMLAttributes, type InputHTMLAttributes, type KeyboardEvent, type MouseEvent, type ReactElement, type ReactNode, type Ref } from 'react';
1
+ import { type ButtonHTMLAttributes, type Dispatch, type HTMLAttributes, type InputHTMLAttributes, type KeyboardEvent, type MouseEvent, type ReactElement, type ReactNode, type Ref, type SetStateAction } from 'react';
2
2
  export type SurfaceTone = 'page' | 'main' | 'content' | 'elevated' | 'overlay' | 'card';
3
3
  export interface SurfaceProps extends HTMLAttributes<HTMLDivElement> {
4
4
  tone?: SurfaceTone;
@@ -639,4 +639,960 @@ export interface CommandPaletteProps extends Omit<HTMLAttributes<HTMLDialogEleme
639
639
  * item under it. Texts, results and actions come from the caller.
640
640
  */
641
641
  export declare const CommandPalette: import("react").ForwardRefExoticComponent<CommandPaletteProps & import("react").RefAttributes<HTMLDialogElement>>;
642
+ /**
643
+ * Walks a list of image addresses: starts at the first and moves to the next on
644
+ * each load error. Starts over when the list changes. `src` is `undefined` once
645
+ * every address failed.
646
+ */
647
+ export declare function useImageChain(sources: readonly string[]): {
648
+ src: string | undefined;
649
+ index: number;
650
+ /** Call from `onError`: moves to the next address. */
651
+ next: () => void;
652
+ };
653
+ /** Rows of the mini dial, top to bottom. */
654
+ export declare const MINI_DIAL_STATS: readonly ["speed", "attack", "defense", "damage"];
655
+ export type MiniDialStat = (typeof MINI_DIAL_STATS)[number];
656
+ /** One resolved cell: the power color (or none), whether it is round and whether it is a special power. */
657
+ export interface MiniDialCell {
658
+ /** Standard power color. Without it the cell is the dial paper (no power). */
659
+ color?: string;
660
+ /** Round power (e.g. WWE). */
661
+ round?: boolean;
662
+ /** Special power: paper with a dark outline. */
663
+ special?: boolean;
664
+ }
665
+ /** One click of the dial, with its 4 cells. */
666
+ export interface MiniDialColumn {
667
+ /** Click number; also decides what has passed relative to `currentClick`. */
668
+ click: number;
669
+ cells: Record<MiniDialStat, MiniDialCell>;
670
+ }
671
+ export interface MiniDialViewProps {
672
+ columns: readonly MiniDialColumn[];
673
+ /** In play: the current click gets an outline and earlier ones fade. */
674
+ currentClick?: number;
675
+ /** Accessible label, already localized (e.g. "Dial: 6 clicks"). */
676
+ label: string;
677
+ className?: string;
678
+ }
679
+ /**
680
+ * The dial summarized in small squares: one column per click, one row per value
681
+ * (speed, attack, defense, damage). The cell size is `--hc-mini-dial-cell` (7px).
682
+ */
683
+ export declare const MiniDialView: import("react").ForwardRefExoticComponent<MiniDialViewProps & import("react").RefAttributes<HTMLSpanElement>>;
684
+ export interface PowerIconViewProps {
685
+ /** Power color (the circle's fill). */
686
+ color: string;
687
+ /** Address of the value symbol (boot, fist, shield, starburst), already built. */
688
+ glyphSrc: string;
689
+ /** The symbol is black; `true` turns it white, for contrast on dark colors. */
690
+ invertGlyph?: boolean;
691
+ size?: number;
692
+ }
693
+ /** Power icon: a circle in the power color with the value symbol on top. */
694
+ export declare const PowerIconView: import("react").ForwardRefExoticComponent<PowerIconViewProps & import("react").RefAttributes<HTMLSpanElement>>;
695
+ export interface StatGlyphViewProps {
696
+ /** Address of the symbol, already built. */
697
+ src: string;
698
+ /** Alternative text, as the caller wants it read. */
699
+ alt: string;
700
+ size?: number;
701
+ }
702
+ /** The value symbol alone (no circle), in neutral gray. Used in the dial legend. */
703
+ export declare const StatGlyphView: import("react").ForwardRefExoticComponent<StatGlyphViewProps & import("react").RefAttributes<HTMLImageElement>>;
704
+ export interface SpecialIconViewProps {
705
+ /** Round white background (the image server's `sp/white.svg`), already built. */
706
+ backgroundSrc: string;
707
+ /** Symbol over the background (the trait star or a value symbol), already built. */
708
+ glyphSrc: string;
709
+ size?: number;
710
+ }
711
+ /** Special power and trait icon: a white circle with a symbol in the middle. */
712
+ export declare const SpecialIconView: import("react").ForwardRefExoticComponent<SpecialIconViewProps & import("react").RefAttributes<HTMLSpanElement>>;
713
+ export interface EquipmentIconProps {
714
+ size?: number;
715
+ }
716
+ /** Equipment icon: a white diamond on a purple circle. Decorative. */
717
+ export declare function EquipmentIcon({ size }: EquipmentIconProps): import("react/jsx-runtime").JSX.Element;
718
+ export type StatIconType = 'speed' | 'attack' | 'defense' | 'damage';
719
+ export interface StatIconProps {
720
+ type: StatIconType;
721
+ size?: number;
722
+ className?: string;
723
+ }
724
+ /** Arrow, shield or diamond drawn in SVG in the text color (`currentColor`); not the official symbols. */
725
+ export declare function StatIcon({ type, size, className }: StatIconProps): import("react/jsx-runtime").JSX.Element;
726
+ /**
727
+ * Avatar shape:
728
+ * - `round`: the default (the caller's class crops it);
729
+ * - `card`: one-shot, a landscape card (width ÷ 1.42, proportional corners);
730
+ * - `square`: maps, a square with slightly rounded corners;
731
+ * - `square-fit`: terrain marker, a square with the whole image fitted in.
732
+ */
733
+ export type AvatarShape = 'round' | 'card' | 'square' | 'square-fit';
734
+ export interface AvatarViewProps {
735
+ /** Image addresses, in the order to try (e.g. a thumbnail, then the original). */
736
+ sources: readonly string[];
737
+ /** Image drawn when there is no address or all of them failed. */
738
+ fallbackSrc: string;
739
+ /** Alternative text, in the app's language. */
740
+ alt: string;
741
+ shape?: AvatarShape;
742
+ className?: string;
743
+ /** Reports that every attempt failed and the fallback is showing. */
744
+ onFallback?: () => void;
745
+ /** Width and height in px (the height follows the shape for `card`). Without it the caller's class sizes it. */
746
+ size?: number;
747
+ }
748
+ /**
749
+ * The piece's portrait. Tries each address in order and, when none loads, shows
750
+ * `fallbackSrc` (with `data-fallback` and the muted fill). The image cannot be
751
+ * dragged, selected or opened in a context menu.
752
+ */
753
+ export declare const AvatarView: import("react").ForwardRefExoticComponent<AvatarViewProps & import("react").RefAttributes<HTMLImageElement>>;
754
+ export interface SetIconViewProps {
755
+ /** Addresses of the set icon, in the order to try. Empty: renders nothing. */
756
+ sources: readonly string[];
757
+ className?: string;
758
+ /** Tooltip (e.g. the set name), in the app's language. */
759
+ title?: string;
760
+ }
761
+ /** Set icon. Tries each address in order and disappears when none loads. Decorative: the set name is next to it. */
762
+ export declare const SetIconView: import("react").ForwardRefExoticComponent<SetIconViewProps & import("react").RefAttributes<HTMLImageElement>>;
763
+ /** Value rows of the dial, top to bottom (the top row is the click number). */
764
+ export declare const COMBAT_DIAL_STATS: readonly ["speed", "attack", "defense", "damage"];
765
+ export type CombatDialStat = (typeof COMBAT_DIAL_STATS)[number];
766
+ /** Content of a power's tip, already in the app's language. */
767
+ export interface CombatDialCellTip {
768
+ title: string;
769
+ description: string;
770
+ /** Standard power color; none for a special power. */
771
+ accent?: string;
772
+ /** The color mark is a circle (round power, e.g. WWE). */
773
+ accentRound?: boolean;
774
+ }
775
+ /** One resolved value cell. */
776
+ export interface CombatDialCell {
777
+ value: number;
778
+ /** Accessible label, already localized (e.g. "Click 1: Speed 8 — Charge"). */
779
+ label: string;
780
+ /** Cell fill: the power color, or the dial paper (`var(--hc-color-dial-paper)`) for no power. */
781
+ background: string;
782
+ /** Number color, chosen for contrast with the fill. */
783
+ color: string;
784
+ /** Round power (e.g. WWE). */
785
+ round?: boolean;
786
+ /** Special power: dark outline. */
787
+ outlined?: boolean;
788
+ /** Without a tip the cell opens nothing; on a static dial it is not even a button. */
789
+ tip?: CombatDialCellTip;
790
+ }
791
+ /** One click of the dial. */
792
+ export interface CombatDialColumn {
793
+ click: number;
794
+ /** Group label (e.g. "Click 3"). */
795
+ label: string;
796
+ /** Label of the number button when the dial changes the click (e.g. "Select click 3"). */
797
+ selectLabel: string;
798
+ /** Outside the selected point value: faded. */
799
+ dimmed?: boolean;
800
+ /** Starting line of a point value: color of the bar left of the column. */
801
+ startColor?: string;
802
+ cells: Record<CombatDialStat, CombatDialCell>;
803
+ }
804
+ /** One legend symbol, with its address already built. */
805
+ export interface CombatDialLegendIcon {
806
+ src: string;
807
+ alt: string;
808
+ title?: string;
809
+ /** `true`: the neutral value symbol (`StatGlyphView`); otherwise the piece's combat symbol. */
810
+ glyph?: boolean;
811
+ }
812
+ export interface CombatDialLegend {
813
+ range: number;
814
+ targets: number;
815
+ /** Address of the target bolt. */
816
+ boltSrc: string;
817
+ icons: Record<CombatDialStat, CombatDialLegendIcon>;
818
+ }
819
+ /** One point value, with the chip texts ready. */
820
+ export interface CombatDialPoint {
821
+ index: number;
822
+ startClick: number;
823
+ color: string;
824
+ /** E.g. "100 pts". Without it the chip shows only the clicks; with none at all the row is hidden. */
825
+ valueLabel?: ReactNode;
826
+ /** E.g. "6 clicks". */
827
+ clicksLabel: ReactNode;
828
+ /** Native tooltip of the chip, shown only when there is more than one point value. */
829
+ title?: string;
830
+ }
831
+ export interface CombatDialLabels {
832
+ /** Header title (e.g. "Dial"). */
833
+ heading: string;
834
+ /** KO mark in the header (e.g. "K.O"). */
835
+ koMark: string;
836
+ /** What follows the KO mark (e.g. "at 7"), already localized. A node, so the number can stay in its own text node. */
837
+ koAt: ReactNode;
838
+ /** Text of the KO column cells (e.g. "KO"). */
839
+ koCell: string;
840
+ }
841
+ /** The open tip: which cell, how (passive hover or the touch dialog), where and its content. */
842
+ export interface CombatDialTipState {
843
+ key: string;
844
+ click: number;
845
+ stat: CombatDialStat;
846
+ content: CombatDialCellTip;
847
+ mode: 'hover' | 'dialog';
848
+ anchor: {
849
+ x: number;
850
+ y: number;
851
+ preferAbove?: boolean;
852
+ };
853
+ trigger: HTMLElement;
854
+ }
855
+ export interface CombatDialViewProps {
856
+ columns: readonly CombatDialColumn[];
857
+ legend: CombatDialLegend;
858
+ points: readonly CombatDialPoint[];
859
+ /** `index` of the selected point value; -1 for none. */
860
+ selectedPointIndex: number;
861
+ /** Click centered in the track when the point value changes (its starting line). */
862
+ focusClick?: number;
863
+ currentClick?: number;
864
+ labels: CombatDialLabels;
865
+ /** `printed`: the physical card's dial, without the header, in rows of 12 clicks. */
866
+ presentation?: 'default' | 'printed';
867
+ /** Without it the dial is static: it neither marks nor changes the click; only cells with a tip react. */
868
+ onClickChange?: (click: number) => void;
869
+ onSelectPoint: (point: CombatDialPoint) => void;
870
+ /** The open tip, controlled by the caller (which closes it when the piece changes). */
871
+ tip: CombatDialTipState | null;
872
+ onTipChange: Dispatch<SetStateAction<CombatDialTipState | null>>;
873
+ /** `id` of the tip element, for the hover's `aria-describedby`. */
874
+ tipId: string;
875
+ /** The tip drawn by the caller (callout, dialog), shown while `tip` is set. */
876
+ tipSlot?: ReactNode;
877
+ }
878
+ /**
879
+ * Combat dial: the legend, one column per click, the KO column and, outside the
880
+ * printed card, the header with the point values. Hover, touch and keyboard on
881
+ * the cells open the tip through `onTipChange`; the caller draws the tip.
882
+ */
883
+ export declare const CombatDialView: import("react").ForwardRefExoticComponent<CombatDialViewProps & import("react").RefAttributes<HTMLElement>>;
884
+ export declare const CARD_WIDTH = 375;
885
+ export declare const CARD_HEIGHT = 525;
886
+ export declare const BACK_TABLE_HEIGHT = 440;
887
+ /** Position of each value of the bystander's dial: every row goes 24px down and 7px right. */
888
+ export declare const BYSTANDER_DIAL_STEP: {
889
+ x: number;
890
+ y: number;
891
+ };
892
+ /**
893
+ * Splits the text blocks between the front (table 0) and the backs (1, 2, …), as
894
+ * HCUnits: a block that does not fit moves to the next column. The front may end
895
+ * up empty (the portrait and keywords already take room); a back column always
896
+ * keeps at least one block, even if it overflows.
897
+ *
898
+ * `overflowing(table)` tells whether that column overflows with the current split.
899
+ * Returns the next split, or `null` once it is stable.
900
+ */
901
+ export declare function nextDistribution(pages: number[], overflowing: (table: number) => boolean): number[] | null;
902
+ /** How many backs the split needs (each back has two columns). There is always one. */
903
+ export declare function backCount(pages: number[]): number;
904
+ /** Cursor position on the card (0 to 1) → tilt, parallax (-1 to 1) and shine (%). */
905
+ export declare function tiltAt(x: number, y: number, damping?: number): {
906
+ rotateX: number;
907
+ rotateY: number;
908
+ parallaxX: number;
909
+ parallaxY: number;
910
+ shineX: number;
911
+ shineY: number;
912
+ };
913
+ type CardTilt = ReturnType<typeof tiltAt>;
914
+ /** The finished values each moving part of the card takes, ready for `style`. */
915
+ export declare function tiltStyles(tilt: CardTilt): {
916
+ card: string;
917
+ art: string;
918
+ header: string;
919
+ shine: string;
920
+ };
921
+ /** Sensor reading relative to the reference pose → position on the card (0 to 1). */
922
+ export declare function devicePosition(beta: number, gamma: number, baseline: {
923
+ beta: number;
924
+ gamma: number;
925
+ }, screenAngle?: number): {
926
+ x: number;
927
+ y: number;
928
+ };
929
+ /**
930
+ * The iPhone only releases the motion sensor after a touch. Called at the end of
931
+ * the touches while the focus view is open, starting with the finger that opened
932
+ * it; outside iOS it does nothing. A touch the browser refuses leaves the request
933
+ * for the next one.
934
+ */
935
+ export declare function requestMotionAccess(): void;
936
+ /** Value rows of the dial, top to bottom. */
937
+ export declare const PHYSICAL_CARD_STATS: readonly ["speed", "attack", "defense", "damage"];
938
+ export type PhysicalCardStat = (typeof PHYSICAL_CARD_STATS)[number];
939
+ /** A tip's content (team ability, improved ability, bystander power), in the app's language. */
940
+ export interface PhysicalCardTip {
941
+ title: string;
942
+ /** Original description; the app may translate it before drawing the tip. */
943
+ description: string;
944
+ iconSrc?: string;
945
+ /** Standard power color; without one it is a special power. */
946
+ accent?: string;
947
+ badgeType?: PhysicalCardStat;
948
+ }
949
+ /** An open tip: which control, how (passive hover or the touch dialog), where, and the content. */
950
+ export interface PhysicalCardTipState extends PhysicalCardTip {
951
+ /** The control's key (e.g. `ta:<id>`, `imp:<type>:<id>`, `dial:<value>`). */
952
+ key: string;
953
+ mode: 'hover' | 'dialog';
954
+ anchor: {
955
+ x: number;
956
+ y: number;
957
+ preferAbove?: boolean;
958
+ };
959
+ trigger: HTMLElement;
960
+ }
961
+ /** A control that opens a tip: its key and content. */
962
+ export interface PhysicalCardTipTarget {
963
+ key: string;
964
+ tip: PhysicalCardTip;
965
+ }
966
+ /** Team ability button in the title (or on the bystander token's stripe). */
967
+ export interface PhysicalCardTeamAbility extends PhysicalCardTipTarget {
968
+ /** Accessible label (e.g. "Team ability: Avengers"). */
969
+ label: string;
970
+ iconSrc: string;
971
+ }
972
+ /** A group of improved abilities (movement or targeting) on the strip above the dial. */
973
+ export interface PhysicalCardImprovedGroup {
974
+ kind: 'movement' | 'targeting';
975
+ kindIconSrc: string;
976
+ kindLabel: string;
977
+ abilities: (PhysicalCardTipTarget & {
978
+ label: string;
979
+ iconSrc: string;
980
+ })[];
981
+ }
982
+ /** A text block (special power, trait, effect), translated and assembled. */
983
+ export interface PhysicalCardRule {
984
+ name?: string;
985
+ /** Drawn text (the game symbols already swapped in by the app). */
986
+ text: ReactNode;
987
+ /** Text copied when the paragraph is tapped. */
988
+ copyText: string;
989
+ icon?: ReactNode;
990
+ }
991
+ /** The front's portrait: the character's art, the terrain's art or an object token. */
992
+ export type PhysicalCardToken =
993
+ /** The character's portrait, drawn by the app with the class the card asks for. */
994
+ {
995
+ kind: 'portrait';
996
+ render: (className: string) => ReactNode;
997
+ } | {
998
+ kind: 'terrain';
999
+ src: string;
1000
+ alt: string;
1001
+ } | {
1002
+ kind: 'object';
1003
+ src?: string;
1004
+ alt: string;
1005
+ color?: string | null;
1006
+ };
1007
+ /** A cell of the bystander's dial (a single click). */
1008
+ export interface PhysicalCardBystanderCell {
1009
+ stat: PhysicalCardStat;
1010
+ value: number;
1011
+ label: string;
1012
+ background?: string;
1013
+ color?: string;
1014
+ special: boolean;
1015
+ /** Without a tip the cell is not a button. */
1016
+ tip?: PhysicalCardTipTarget;
1017
+ }
1018
+ /** Replica of the bystander's physical token (HCUnits' `BystanderView`). */
1019
+ export interface PhysicalCardBystander {
1020
+ art?: {
1021
+ src: string;
1022
+ alt: string;
1023
+ };
1024
+ range?: number | null;
1025
+ rangeLabel: string;
1026
+ targets: number;
1027
+ boltSrc: string;
1028
+ symbols: Partial<Record<PhysicalCardStat, string>>;
1029
+ /** In an arc on the left: the kind's icon (without `ability`) and each ability. */
1030
+ improved: {
1031
+ kind: 'movement' | 'targeting';
1032
+ iconSrc: string;
1033
+ label: string;
1034
+ ability?: PhysicalCardTipTarget & {
1035
+ label: string;
1036
+ iconSrc: string;
1037
+ };
1038
+ }[];
1039
+ trait?: {
1040
+ src: string;
1041
+ alt: string;
1042
+ };
1043
+ extra?: {
1044
+ kind: 'construct' | 'stack' | 'passengers';
1045
+ src: string;
1046
+ alt: string;
1047
+ text?: ReactNode;
1048
+ };
1049
+ name: string;
1050
+ dial?: {
1051
+ label: string;
1052
+ cells: PhysicalCardBystanderCell[];
1053
+ };
1054
+ }
1055
+ export interface PhysicalCardPoint {
1056
+ index: number;
1057
+ value: number;
1058
+ color: string;
1059
+ /** Accessible label (e.g. "100 pts, starts at click 1"). */
1060
+ label: string;
1061
+ }
1062
+ /** Measures, backgrounds and seals of the front (the app's layout rules, already applied). */
1063
+ export interface PhysicalCardFrontLayout {
1064
+ frontTable: 'normal' | 'long' | 'short' | 'none';
1065
+ frontTableHeight: number;
1066
+ rarityImage?: string | null;
1067
+ dialImage?: string | null;
1068
+ teamUp: boolean;
1069
+ /** Smaller portrait when the dial takes two rows. */
1070
+ smallToken: boolean;
1071
+ improvedTop: number;
1072
+ }
1073
+ export interface PhysicalCardViewProps {
1074
+ /** The card's backgrounds (front, back and the legacy overlay, when there is one), as addresses the app serves. */
1075
+ images: {
1076
+ front: string;
1077
+ back: string;
1078
+ legacy?: string | null;
1079
+ };
1080
+ layout: PhysicalCardFrontLayout;
1081
+ /** Name in capitals; the type shrinks until it fits the title line. */
1082
+ title: string;
1083
+ gearSrc?: string;
1084
+ /** Lines under the title, in order (object or bystander type, keywords). */
1085
+ headerLines: {
1086
+ kind: 'type' | 'keywords';
1087
+ text: string;
1088
+ }[];
1089
+ teamAbilities: PhysicalCardTeamAbility[];
1090
+ /** Shield drawn in every team ability button. */
1091
+ teamAbilityIcon?: ReactNode;
1092
+ collection: {
1093
+ number: ReactNode;
1094
+ setSources: readonly string[];
1095
+ setTitle?: string;
1096
+ };
1097
+ dimensions?: {
1098
+ text: string;
1099
+ oval: boolean;
1100
+ };
1101
+ token?: PhysicalCardToken;
1102
+ bystander?: PhysicalCardBystander;
1103
+ /** Keyphrase lines of the object, in order, before the effects. */
1104
+ keyphrases?: {
1105
+ key: string;
1106
+ content: ReactNode;
1107
+ }[];
1108
+ relic?: {
1109
+ label: string;
1110
+ value: string;
1111
+ };
1112
+ rules: PhysicalCardRule[];
1113
+ /**
1114
+ * Restarts the split of the text between front and backs when it changes
1115
+ * (piece, language and translated texts).
1116
+ */
1117
+ contentKey: string;
1118
+ improved: PhysicalCardImprovedGroup[];
1119
+ /** The printed dial (a `CombatDialView` with `presentation="printed"`, built by the app) and how many rows it takes. */
1120
+ dial?: {
1121
+ tables: 1 | 2;
1122
+ node: ReactNode;
1123
+ };
1124
+ terrainDial?: {
1125
+ icons: {
1126
+ range: {
1127
+ src: string;
1128
+ alt: string;
1129
+ };
1130
+ giantReach: {
1131
+ src: string;
1132
+ alt: string;
1133
+ };
1134
+ damage: {
1135
+ src: string;
1136
+ alt: string;
1137
+ };
1138
+ destroy: {
1139
+ src: string;
1140
+ alt: string;
1141
+ };
1142
+ };
1143
+ values: [ReactNode, ReactNode, ReactNode];
1144
+ };
1145
+ /** Points on the front's bar: the point value buttons, an object's line, or nothing. */
1146
+ points: {
1147
+ kind: 'none';
1148
+ } | {
1149
+ kind: 'object';
1150
+ values: string;
1151
+ suffix: string;
1152
+ } | {
1153
+ kind: 'normal' | 'long' | 'short';
1154
+ label: string;
1155
+ items: PhysicalCardPoint[];
1156
+ selectedIndex: number;
1157
+ };
1158
+ onSelectPoint: (point: PhysicalCardPoint) => void;
1159
+ /** The last back's black bar: real name and source, already in capitals. */
1160
+ backBar: {
1161
+ realName?: {
1162
+ label: string;
1163
+ value: string;
1164
+ };
1165
+ source?: {
1166
+ label: string;
1167
+ value: string;
1168
+ };
1169
+ };
1170
+ labels: {
1171
+ /** The front's label (e.g. "Batman · card front"). */
1172
+ front: string;
1173
+ /** Each back's label (`index` from 0). */
1174
+ back: (index: number, count: number) => string;
1175
+ };
1176
+ onCopy: (text: string) => void;
1177
+ /** Desktop: the front fits whole in the visible height of the scrolling area (`fitHeightWithin`). */
1178
+ fitHeight?: boolean;
1179
+ /** Selector of the scrolling area, looked up from the card, for `fitHeight`. */
1180
+ fitHeightWithin?: string;
1181
+ /** Focus view: one side at a time, with the `focusActions` buttons below. */
1182
+ focus?: boolean;
1183
+ /** Holding the card with a finger (outside the focus view) asks for the focus view. */
1184
+ onRequestFocus?: () => void;
1185
+ /** The focus view's buttons; `flip` turns to the next side (`nextSide`: 0 is the front). */
1186
+ focusActions?: (state: {
1187
+ nextSide: number;
1188
+ backs: number;
1189
+ flip: () => void;
1190
+ }) => ReactNode;
1191
+ /** The open tip, controlled by the caller. */
1192
+ tip: PhysicalCardTipState | null;
1193
+ onTipChange: Dispatch<SetStateAction<PhysicalCardTipState | null>>;
1194
+ /** `id` of the tip element, for the hover's `aria-describedby`. */
1195
+ tipId: string;
1196
+ /** The tip drawn by the app, while `tip` is set. */
1197
+ tipSlot?: ReactNode;
1198
+ }
1199
+ export interface PhysicalCardFocusViewProps {
1200
+ /** The dialog's accessible name (the piece's name). */
1201
+ label: string;
1202
+ onClose: () => void;
1203
+ /** A second card with `focus`, usually the app's adapter again. */
1204
+ children: ReactNode;
1205
+ }
1206
+ /**
1207
+ * The card's focus view: a full-screen modal dialog over a dimmed, blurred
1208
+ * background. Inside goes a second card with `focus`; tapping outside it (after
1209
+ * the finger that opened it is lifted) or Esc calls `onClose`.
1210
+ */
1211
+ export declare function PhysicalCardFocusView({ label, onClose, children }: PhysicalCardFocusViewProps): import("react/jsx-runtime").JSX.Element;
1212
+ /**
1213
+ * The physical card (front and backs in HCUnits' format), presentation only. It
1214
+ * gets everything resolved: backgrounds, seals, texts already translated and
1215
+ * assembled, symbol addresses, the printed dial ready and the points. It handles
1216
+ * the scale, the split of the text between front and backs, the tilt and shine,
1217
+ * holding to open the focus view and flipping in it, and opens the tips through
1218
+ * `onTipChange`; drawing the tip, translating and copying are the caller's.
1219
+ */
1220
+ export declare const PhysicalCardView: import("react").ForwardRefExoticComponent<PhysicalCardViewProps & import("react").RefAttributes<HTMLDivElement>>;
1221
+ /**
1222
+ * A faithful port of hcmaps.net's map drawing engine (map.js / tile.js / edge.js /
1223
+ * wall.js), the same engine HCUnits uses to draw the map's canvas. It reads the
1224
+ * same JSON (the piece's map) and draws it 1:1 locally, with no runtime
1225
+ * dependency on their scripts. Credit: hcmaps.net (original engine in JS).
1226
+ */
1227
+ export declare const TILE_SIZE = 64;
1228
+ export declare const ELEVATION_TILE_COLORS: string[];
1229
+ export declare const TERRAIN_TILE_COLORS: Record<string, string>;
1230
+ export declare const TERRAIN_EDGE_COLORS: Record<string, string>;
1231
+ export declare const ELEVATION_EDGE_COLOR = "#FF0000";
1232
+ export declare const INDOOR_EDGE_COLOR = "#FFFF00";
1233
+ export declare const WALL_COLORS: Record<string, string>;
1234
+ interface JsonLabel {
1235
+ text: string;
1236
+ color?: string;
1237
+ rotate?: number;
1238
+ }
1239
+ interface JsonTile {
1240
+ terrain?: string;
1241
+ type?: string;
1242
+ elevation?: number | string;
1243
+ isStartingZone?: boolean;
1244
+ isStartingZone4p?: boolean;
1245
+ group?: number;
1246
+ label?: JsonLabel;
1247
+ }
1248
+ interface JsonWall {
1249
+ x0: number;
1250
+ y0: number;
1251
+ x1: number;
1252
+ y1: number;
1253
+ type?: string;
1254
+ }
1255
+ interface JsonRamp {
1256
+ x0: number;
1257
+ y0: number;
1258
+ x1: number;
1259
+ y1: number;
1260
+ }
1261
+ export interface HCMapJson {
1262
+ name?: string;
1263
+ width: number;
1264
+ height: number;
1265
+ type?: string;
1266
+ defaultTerrain?: string;
1267
+ defaultElevation?: number;
1268
+ defaultType?: string;
1269
+ defaultWallType?: string;
1270
+ hideLabels?: boolean;
1271
+ rows: {
1272
+ tiles: JsonTile[];
1273
+ }[];
1274
+ walls?: JsonWall[];
1275
+ ramps?: JsonRamp[];
1276
+ ziplines?: JsonWall[];
1277
+ }
1278
+ export declare class HCMap {
1279
+ width: number;
1280
+ height: number;
1281
+ type: string;
1282
+ private tiles;
1283
+ private walls;
1284
+ private ziplines;
1285
+ constructor(json: HCMapJson);
1286
+ private getTile;
1287
+ private tryAddEdge;
1288
+ draw(ctx: CanvasRenderingContext2D): void;
1289
+ private drawWall;
1290
+ }
1291
+ export declare function mapSquare(map: HCMapJson, x: number, y: number): {
1292
+ x: number;
1293
+ y: number;
1294
+ terrain: string;
1295
+ elevation: string | number;
1296
+ environment: string;
1297
+ starting: boolean;
1298
+ label: string | undefined;
1299
+ };
1300
+ export type MapSquare = ReturnType<typeof mapSquare>;
1301
+ /** A text elevation exists in the hcmaps format; the 2D drawing treats it as an elevated level. */
1302
+ export declare function elevationLevel(elevation: number | string): number;
1303
+ export declare function mapMaxLevel(map: HCMapJson): number;
1304
+ export interface MapInspection {
1305
+ square: MapSquare;
1306
+ adjacent?: MapSquare;
1307
+ line?: {
1308
+ x0: number;
1309
+ y0: number;
1310
+ x1: number;
1311
+ y1: number;
1312
+ };
1313
+ wall?: string;
1314
+ }
1315
+ /** Coordinates are in squares; wall tolerance is based on screen pixels by the caller. */
1316
+ export declare function inspectMap(map: HCMapJson, x: number, y: number, tolerance?: number): MapInspection | null;
1317
+ export declare function squareCoordinate(square: Pick<MapSquare, 'x' | 'y'>): string;
1318
+ /**
1319
+ * The 3D model's scene, with no drawing. It uses only what the map JSON has:
1320
+ * terrain, elevation, environment, walls, ramps and ziplines. Blocking follows
1321
+ * the game's rule: indoor goes up to the map's highest elevation, outdoor takes
1322
+ * one elevation. Walls, water and obscuring have no height in the data: those
1323
+ * are conventions.
1324
+ */
1325
+ export declare const LEVEL_HEIGHT = 0.62;
1326
+ export declare const BASE_Z = -0.34;
1327
+ export interface SceneCell extends MapSquare {
1328
+ level: number;
1329
+ /** The square's floor, without the terrain's volume. */
1330
+ floor: number;
1331
+ top: number;
1332
+ }
1333
+ export interface SceneWall {
1334
+ x0: number;
1335
+ y0: number;
1336
+ x1: number;
1337
+ y1: number;
1338
+ type: string;
1339
+ z0: number;
1340
+ z1: number;
1341
+ }
1342
+ export interface SceneRamp {
1343
+ x: number;
1344
+ y: number;
1345
+ dx: number;
1346
+ dy: number;
1347
+ from: number;
1348
+ to: number;
1349
+ }
1350
+ export interface SceneLine {
1351
+ x0: number;
1352
+ y0: number;
1353
+ x1: number;
1354
+ y1: number;
1355
+ z: number;
1356
+ }
1357
+ export interface MapScene {
1358
+ width: number;
1359
+ height: number;
1360
+ cells: SceneCell[];
1361
+ walls: SceneWall[];
1362
+ ramps: SceneRamp[];
1363
+ ziplines: SceneLine[];
1364
+ maxTop: number;
1365
+ }
1366
+ export declare function sceneCell(scene: Pick<MapScene, 'width' | 'height' | 'cells'>, x: number, y: number): SceneCell | null;
1367
+ export declare function buildMapScene(map: HCMapJson): MapScene;
1368
+ export interface MapCamera {
1369
+ azimuth: number;
1370
+ tilt: number;
1371
+ zoom: number;
1372
+ panX: number;
1373
+ panY: number;
1374
+ }
1375
+ export interface Viewport {
1376
+ width: number;
1377
+ height: number;
1378
+ }
1379
+ export declare const DEFAULT_CAMERA: MapCamera;
1380
+ /** 90 is the flat view from above. Below the minimum the board becomes a line. */
1381
+ export declare const TILT_RANGE: readonly [6, 90];
1382
+ export declare const ZOOM_RANGE: readonly [1, 6];
1383
+ export declare const clamp: (value: number, min: number, max: number) => number;
1384
+ export interface Projection {
1385
+ scale: number;
1386
+ sin: number;
1387
+ cos: number;
1388
+ point(x: number, y: number, z: number): [number, number];
1389
+ /** Larger = closer to the viewer. */
1390
+ depth(x: number, y: number): number;
1391
+ /** From the screen to the map, on a plane at height `z`. */
1392
+ ground(px: number, py: number, z: number): [number, number];
1393
+ }
1394
+ export declare function makeProjection(scene: MapScene, camera: MapCamera, viewport: Viewport): Projection;
1395
+ /** The map's center may go as far as any of its corners reaches the middle of the screen. */
1396
+ export declare function clampPan(scene: MapScene, camera: MapCamera, viewport: Viewport): MapCamera;
1397
+ /** Zooms keeping the point at `anchor` (measured from the screen's center) still. */
1398
+ export declare function zoomCamera(scene: MapScene, camera: MapCamera, viewport: Viewport, factor: number, anchor?: {
1399
+ x: number;
1400
+ y: number;
1401
+ }): MapCamera;
1402
+ /** A square's four sides: the edge's points, the neighbor and the normal. */
1403
+ export declare const CELL_SIDES: readonly [{
1404
+ readonly ax: 0;
1405
+ readonly ay: 1;
1406
+ readonly bx: 0;
1407
+ readonly by: 0;
1408
+ readonly dx: -1;
1409
+ readonly dy: 0;
1410
+ }, {
1411
+ readonly ax: 1;
1412
+ readonly ay: 0;
1413
+ readonly bx: 1;
1414
+ readonly by: 1;
1415
+ readonly dx: 1;
1416
+ readonly dy: 0;
1417
+ }, {
1418
+ readonly ax: 0;
1419
+ readonly ay: 0;
1420
+ readonly bx: 1;
1421
+ readonly by: 0;
1422
+ readonly dx: 0;
1423
+ readonly dy: -1;
1424
+ }, {
1425
+ readonly ax: 1;
1426
+ readonly ay: 1;
1427
+ readonly bx: 0;
1428
+ readonly by: 1;
1429
+ readonly dx: 0;
1430
+ readonly dy: 1;
1431
+ }];
1432
+ /**
1433
+ * From the screen to map coordinates, testing from front to back. Returns the
1434
+ * point in `inspectMap`'s terms, so the tooltip and selection match the 2D board.
1435
+ */
1436
+ export declare function pickScene(scene: MapScene, view: Projection, px: number, py: number): {
1437
+ x: number;
1438
+ y: number;
1439
+ } | null;
1440
+ type Point = readonly [number, number];
1441
+ /**
1442
+ * The target's squares inside the board and their outer outline, in map coordinates.
1443
+ * Corners are where the outline turns or ends: the volume's edges rise there.
1444
+ */
1445
+ export declare function footprintOutline({ x, y, width, height, cells }: {
1446
+ x: number;
1447
+ y: number;
1448
+ width: number;
1449
+ height: number;
1450
+ cells?: readonly (readonly boolean[])[] | null;
1451
+ }, boardWidth: number, boardHeight: number): {
1452
+ squares: Point[];
1453
+ edges: [Point, Point][];
1454
+ corners: Point[];
1455
+ };
1456
+ export interface SceneFootprint {
1457
+ x: number;
1458
+ y: number;
1459
+ width: number;
1460
+ height: number;
1461
+ top: number;
1462
+ valid: boolean;
1463
+ /** Occupied squares (`cells[y][x]`, from the corner); without it, the whole rectangle. */
1464
+ cells?: readonly (readonly boolean[])[] | null;
1465
+ }
1466
+ export interface SceneHighlight {
1467
+ square?: {
1468
+ x: number;
1469
+ y: number;
1470
+ };
1471
+ line?: {
1472
+ x0: number;
1473
+ y0: number;
1474
+ x1: number;
1475
+ y1: number;
1476
+ };
1477
+ /** The target of a piece being moved: floor and volume, cut to the board. */
1478
+ footprint?: SceneFootprint;
1479
+ }
1480
+ export declare function drawMapScene(ctx: CanvasRenderingContext2D, scene: MapScene, view: Projection, texture: HTMLCanvasElement, highlight?: SceneHighlight | null, { tufts }?: {
1481
+ tufts?: boolean;
1482
+ }): void;
1483
+ /** The thumbnail's default height, in pixels. */
1484
+ export declare const MAP_THUMBNAIL_HEIGHT = 192;
1485
+ /**
1486
+ * The map's thumbnail as a PNG data URL: draws with the engine at full resolution
1487
+ * and scales down to `height` pixels. Gets the JSON already fetched; `null` when it
1488
+ * has no dimensions or the browser gives no 2D context.
1489
+ */
1490
+ export declare function renderMapThumbnail(json: HCMapJson, height?: number): string | null;
1491
+ export interface MapBoardViewProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onPointerDown' | 'onPointerMove' | 'onPointerLeave'> {
1492
+ /** The hcmaps JSON, already fetched and checked. */
1493
+ map: HCMapJson;
1494
+ /** The drawing's accessible name, in the app's language (the map's name or a generic one). */
1495
+ canvasLabel: string;
1496
+ /** What is inspected: the square is shaded; an edge becomes a line. */
1497
+ selection?: Pick<MapInspection, 'square' | 'line'> | null;
1498
+ /** Pointer over the map, in squares (0..width, 0..height), and the screen point. Touch counts only on press; mouse and pen also while passing over. */
1499
+ onInspect?(x: number, y: number, clientX: number, clientY: number): void;
1500
+ /** The mouse or pen left the map. */
1501
+ onLeave?(): void;
1502
+ /** Drawing failed (a JSON outside the expected format). */
1503
+ onDrawError?(): void;
1504
+ }
1505
+ /**
1506
+ * The map's 2D board: the hcmaps drawing in a canvas and, over it, the inspection's
1507
+ * highlight. Gets the map already fetched; fetching, translating and deciding what
1508
+ * the tooltip shows are the caller's. Other attributes (role, focus, keyboard,
1509
+ * `aria-*`, `data-*`) go to the container.
1510
+ */
1511
+ export declare const MapBoardView: import("react").ForwardRefExoticComponent<MapBoardViewProps & import("react").RefAttributes<HTMLDivElement>>;
1512
+ export interface MapBoardPlaceholderProps {
1513
+ /** Read by screen readers while the map loads (e.g. "Loading map"). */
1514
+ label: string;
1515
+ }
1516
+ /** The map's place while it loads: the empty grid in a map's proportion. The label is visually hidden. */
1517
+ export declare function MapBoardPlaceholder({ label }: MapBoardPlaceholderProps): import("react/jsx-runtime").JSX.Element;
1518
+ export interface MapSwatchProps {
1519
+ /** The square's fill, as on the board. */
1520
+ fill: string;
1521
+ /** The square's border (the terrain's or the elevation's color). */
1522
+ edge: string;
1523
+ /** Number inside the square (the elevation's level). */
1524
+ children?: number;
1525
+ /** White text, for dark fills. */
1526
+ lightText?: boolean;
1527
+ }
1528
+ /** A legend square with a map square's fill and border. Decorative. */
1529
+ export declare function MapSwatch({ fill, edge, children, lightText }: MapSwatchProps): import("react/jsx-runtime").JSX.Element;
1530
+ /** One inspected square, resolved: texts in the app's language and the board's colors. */
1531
+ export interface MapTooltipSquare {
1532
+ /** Coordinate (e.g. "H8"); shown only when the title is not the coordinate itself. */
1533
+ coordinate?: string;
1534
+ terrain: {
1535
+ label: string;
1536
+ fill: string;
1537
+ edge: string;
1538
+ };
1539
+ elevation: {
1540
+ level: number;
1541
+ label: string;
1542
+ fill: string;
1543
+ edge: string;
1544
+ lightText?: boolean;
1545
+ };
1546
+ /** The square's environment, e.g. a `MapEnvironmentView`. */
1547
+ environment?: ReactNode;
1548
+ /** Text in place of the environment, when the map does not say (e.g. "Environment unspecified"). */
1549
+ environmentNote?: string;
1550
+ /** Notes, in order (what fills a blocking square, starting area, the square's label). */
1551
+ notes?: readonly string[];
1552
+ }
1553
+ export interface MapTooltipViewProps {
1554
+ id?: string;
1555
+ /** Screen point (viewport coordinates) the tooltip opens next to. A new object moves it. */
1556
+ anchor: {
1557
+ x: number;
1558
+ y: number;
1559
+ };
1560
+ title: string;
1561
+ /** The wall's color, when the inspected point is a wall. */
1562
+ wallColor?: string;
1563
+ /** One square, or two on an edge between squares. */
1564
+ squares: readonly MapTooltipSquare[];
1565
+ }
1566
+ /**
1567
+ * The map inspection's tooltip: the title (wall, edge or coordinate) and what is in
1568
+ * each square. It is `position: fixed`, next to `anchor`, without leaving the
1569
+ * screen; to take it out of a container with overflow, the caller portals it.
1570
+ */
1571
+ export declare function MapTooltipView({ id, anchor, title, wallColor, squares }: MapTooltipViewProps): import("react/jsx-runtime").JSX.Element;
1572
+ export interface MapLegendItem {
1573
+ fill: string;
1574
+ edge: string;
1575
+ /** Visible name, in the app's language; also the item's key. */
1576
+ label: string;
1577
+ }
1578
+ export interface MapLegendViewProps {
1579
+ items: readonly MapLegendItem[];
1580
+ /** After the items, e.g. the map's environments (`MapEnvironmentView`). */
1581
+ children?: ReactNode;
1582
+ }
1583
+ /** The 3D model's legend: a small square and the name of each terrain on the map. */
1584
+ export declare function MapLegendView({ items, children }: MapLegendViewProps): import("react/jsx-runtime").JSX.Element;
1585
+ export type MapEnvironmentKind = 'indoor' | 'outdoor';
1586
+ /** The map's types (`indoor`, `outdoor`, `indoorOutdoor`, in any spelling) as environments, without repeats. */
1587
+ export declare function mapEnvironments(types: readonly string[]): MapEnvironmentKind[];
1588
+ export interface MapEnvironmentViewProps {
1589
+ types: readonly string[];
1590
+ /** Each environment's icon address (black line icons; the dark theme turns them light). */
1591
+ icons: Record<MapEnvironmentKind, string>;
1592
+ /** Each environment's visible name. A game term: HCUI shows it in English in all three languages. */
1593
+ labels: Record<MapEnvironmentKind, string>;
1594
+ className?: string;
1595
+ }
1596
+ /** The map's environments (indoor / outdoor), each with its icon and name. With no known environment it draws nothing. */
1597
+ export declare function MapEnvironmentView({ types, icons, labels, className }: MapEnvironmentViewProps): import("react/jsx-runtime").JSX.Element | null;
642
1598
  export {};