@almadar/ui 5.32.1 → 5.32.3

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.
@@ -7253,6 +7253,13 @@ var init_Textarea = __esm({
7253
7253
  Textarea.displayName = "Textarea";
7254
7254
  }
7255
7255
  });
7256
+ function dispatchValueChange(onValueChange, eventBus, value) {
7257
+ if (typeof onValueChange === "string") {
7258
+ eventBus.emit(`UI:${onValueChange}`, { value });
7259
+ } else {
7260
+ onValueChange?.(value);
7261
+ }
7262
+ }
7256
7263
  function flatOptions(opts, groups) {
7257
7264
  const flat = opts ?? [];
7258
7265
  const grp = (groups ?? []).flatMap((g) => g.options);
@@ -7276,7 +7283,7 @@ function NativeSelect({
7276
7283
  } else {
7277
7284
  onChange?.(e);
7278
7285
  }
7279
- onValueChange?.(e.target.value);
7286
+ dispatchValueChange(onValueChange, eventBus, e.target.value);
7280
7287
  };
7281
7288
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
7282
7289
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -7336,7 +7343,7 @@ function RichSelect({
7336
7343
  if (typeof onChange === "string") {
7337
7344
  eventBus.emit(`UI:${onChange}`, { value: next });
7338
7345
  }
7339
- onValueChange?.(next);
7346
+ dispatchValueChange(onValueChange, eventBus, next);
7340
7347
  };
7341
7348
  const clear = (e) => {
7342
7349
  e.stopPropagation();
@@ -7344,7 +7351,7 @@ function RichSelect({
7344
7351
  if (typeof onChange === "string") {
7345
7352
  eventBus.emit(`UI:${onChange}`, { value: next });
7346
7353
  }
7347
- onValueChange?.(next);
7354
+ dispatchValueChange(onValueChange, eventBus, next);
7348
7355
  };
7349
7356
  React88.useEffect(() => {
7350
7357
  const handler = (e) => {
package/dist/avl/index.js CHANGED
@@ -7204,6 +7204,13 @@ var init_Textarea = __esm({
7204
7204
  Textarea.displayName = "Textarea";
7205
7205
  }
7206
7206
  });
7207
+ function dispatchValueChange(onValueChange, eventBus, value) {
7208
+ if (typeof onValueChange === "string") {
7209
+ eventBus.emit(`UI:${onValueChange}`, { value });
7210
+ } else {
7211
+ onValueChange?.(value);
7212
+ }
7213
+ }
7207
7214
  function flatOptions(opts, groups) {
7208
7215
  const flat = opts ?? [];
7209
7216
  const grp = (groups ?? []).flatMap((g) => g.options);
@@ -7227,7 +7234,7 @@ function NativeSelect({
7227
7234
  } else {
7228
7235
  onChange?.(e);
7229
7236
  }
7230
- onValueChange?.(e.target.value);
7237
+ dispatchValueChange(onValueChange, eventBus, e.target.value);
7231
7238
  };
7232
7239
  return /* @__PURE__ */ jsxs("div", { className: "relative", children: [
7233
7240
  /* @__PURE__ */ jsxs(
@@ -7287,7 +7294,7 @@ function RichSelect({
7287
7294
  if (typeof onChange === "string") {
7288
7295
  eventBus.emit(`UI:${onChange}`, { value: next });
7289
7296
  }
7290
- onValueChange?.(next);
7297
+ dispatchValueChange(onValueChange, eventBus, next);
7291
7298
  };
7292
7299
  const clear = (e) => {
7293
7300
  e.stopPropagation();
@@ -7295,7 +7302,7 @@ function RichSelect({
7295
7302
  if (typeof onChange === "string") {
7296
7303
  eventBus.emit(`UI:${onChange}`, { value: next });
7297
7304
  }
7298
- onValueChange?.(next);
7305
+ dispatchValueChange(onValueChange, eventBus, next);
7299
7306
  };
7300
7307
  useEffect(() => {
7301
7308
  const handler = (e) => {
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import type { EventKey } from "@almadar/core";
2
+ import type { EventKey, EventEmit } from "@almadar/core";
3
3
  export interface SelectOption {
4
4
  value: string;
5
5
  label: string;
@@ -38,7 +38,12 @@ export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectE
38
38
  clearable?: boolean;
39
39
  /** onChange handler (native ChangeEvent) or declarative event key for trait dispatch */
40
40
  onChange?: React.ChangeEventHandler<HTMLSelectElement> | EventKey;
41
- /** Value-based callback receives the selected string (or string[] for multiple). */
42
- onValueChange?: (value: string | string[]) => void;
41
+ /** Value-based change: a React callback (internal use) OR a declarative event
42
+ * key that emits `{ value }` on the bus (render-ui / lolo authoring). Mirrors
43
+ * the `onChange` handler|event convention so it's an event-emitting prop, not a
44
+ * bare callback. */
45
+ onValueChange?: ((value: string | string[]) => void) | EventEmit<{
46
+ value: string | string[];
47
+ }>;
43
48
  }
44
49
  export declare const Select: React.ForwardRefExoticComponent<SelectProps & React.RefAttributes<HTMLSelectElement>>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ActivationBlock Molecule Component
3
+ *
4
+ * Pre-lesson activation prompt. Invites learners to surface prior knowledge
5
+ * before content begins. Emits `UI:SAVE_ACTIVATION { response }`.
6
+ *
7
+ * Event Contract:
8
+ * - Emits: UI:SAVE_ACTIVATION { response }
9
+ * - entityAware: false
10
+ */
11
+ import React from 'react';
12
+ export interface ActivationBlockProps {
13
+ /** The prior-knowledge question */
14
+ question: string;
15
+ /** Pre-filled response from saved state */
16
+ savedResponse?: string;
17
+ /** Event name emitted on save/skip (as `UI:<saveEvent>`). Defaults to 'SAVE_ACTIVATION'. */
18
+ saveEvent?: string;
19
+ /** Additional CSS classes */
20
+ className?: string;
21
+ }
22
+ export declare const ActivationBlock: React.FC<ActivationBlockProps>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * BloomQuizBlock Molecule Component
3
+ *
4
+ * Practice Q&A with Bloom's Taxonomy level badge. Emits `UI:ANSWER_BLOOM { index, level }`.
5
+ *
6
+ * Event Contract:
7
+ * - Emits: UI:ANSWER_BLOOM { index, level }
8
+ * - entityAware: false
9
+ */
10
+ import React from 'react';
11
+ export type BloomLevel = 'remember' | 'understand' | 'apply' | 'analyze' | 'evaluate' | 'create';
12
+ export interface BloomQuizBlockProps {
13
+ level: BloomLevel;
14
+ question: string;
15
+ answer: string;
16
+ /** Zero-based index (used in the emitted event payload) */
17
+ index?: number;
18
+ /** Whether the learner has already answered */
19
+ isAnswered?: boolean;
20
+ /** Event name emitted on first reveal (as `UI:<answerEvent>`). Defaults to 'ANSWER_BLOOM'. */
21
+ answerEvent?: string;
22
+ /** Additional CSS classes */
23
+ className?: string;
24
+ }
25
+ export declare const BloomQuizBlock: React.FC<BloomQuizBlockProps>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ConnectionBlock Molecule Component
3
+ *
4
+ * Surfaces prior-knowledge connections before new content.
5
+ * Renders arbitrary markdown via MarkdownContent.
6
+ *
7
+ * Event Contract:
8
+ * - No events emitted (display-only)
9
+ * - entityAware: false
10
+ */
11
+ import React from 'react';
12
+ export interface ConnectionBlockProps {
13
+ /** Markdown content summarising what the learner already knows */
14
+ content: string;
15
+ /** Additional CSS classes */
16
+ className?: string;
17
+ }
18
+ export declare const ConnectionBlock: React.FC<ConnectionBlockProps>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * ReflectionBlock Molecule Component
3
+ *
4
+ * Inline reflection prompt for deep processing. Emits `UI:SAVE_REFLECTION { index, note }`.
5
+ *
6
+ * Event Contract:
7
+ * - Emits: UI:SAVE_REFLECTION { index, note }
8
+ * - entityAware: false
9
+ */
10
+ import React from 'react';
11
+ export interface ReflectionBlockProps {
12
+ /** The reflection prompt */
13
+ prompt: string;
14
+ /** Zero-based index of this block (used in the emitted event payload) */
15
+ index: number;
16
+ /** Pre-filled note from saved state */
17
+ savedNote?: string;
18
+ /** Event name emitted on save (as `UI:<saveEvent>`). Defaults to 'SAVE_REFLECTION'. */
19
+ saveEvent?: string;
20
+ /** Additional CSS classes */
21
+ className?: string;
22
+ }
23
+ export declare const ReflectionBlock: React.FC<ReflectionBlockProps>;
@@ -125,3 +125,9 @@ export { Chart, type ChartProps, type ChartType, type ChartSeries, } from "./Cha
125
125
  export { SignaturePad, type SignaturePadProps, } from "./SignaturePad";
126
126
  export { DocumentViewer, type DocumentViewerProps, type DocumentType, } from "./DocumentViewer";
127
127
  export { GraphCanvas, type GraphCanvasProps, type GraphNode, type GraphEdge, } from "./GraphCanvas";
128
+ export { ActivationBlock, type ActivationBlockProps } from './ActivationBlock';
129
+ export { ReflectionBlock, type ReflectionBlockProps } from './ReflectionBlock';
130
+ export { ConnectionBlock, type ConnectionBlockProps } from './ConnectionBlock';
131
+ export { BloomQuizBlock, type BloomQuizBlockProps, type BloomLevel } from './BloomQuizBlock';
132
+ export { parseLessonSegments, type LessonSegment, type LessonUserProgress } from './parseLessonSegments';
133
+ export { parseMarkdownWithCodeBlocks, type MixedSegment } from './lessonSegmentUtils';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Utility for lesson-segment parsing: splits a markdown string into alternating
3
+ * markdown and fenced-code-block segments. Exported so SegmentRenderer and
4
+ * BloomQuizBlock can reuse without circular imports.
5
+ */
6
+ export type MarkdownSegment = {
7
+ type: 'markdown';
8
+ content: string;
9
+ };
10
+ export type CodeSegment = {
11
+ type: 'code';
12
+ language: string;
13
+ content: string;
14
+ runnable?: boolean;
15
+ };
16
+ export type MixedSegment = MarkdownSegment | CodeSegment;
17
+ /** Splits markdown content into markdown and fenced-code-block segments. */
18
+ export declare function parseMarkdownWithCodeBlocks(content: string): MixedSegment[];
@@ -0,0 +1,43 @@
1
+ /**
2
+ * parseLessonSegments — parses lesson markdown with embedded learning-science
3
+ * XML-style tags into a typed Segment array.
4
+ *
5
+ * Supported tags: <activate>, <connect>, <reflect>, <bloom level="...">,
6
+ * <question>/<answer>, <visualize type="..." description="..." />.
7
+ *
8
+ * Tags may be open (unclosed) — the parser falls back to consuming content
9
+ * until the next recognised tag, a section heading (`\n\n#`), or end-of-input.
10
+ */
11
+ import type { BloomLevel } from './BloomQuizBlock';
12
+ import { type MixedSegment } from './lessonSegmentUtils';
13
+ export type LessonSegment = MixedSegment | {
14
+ type: 'quiz';
15
+ question: string;
16
+ answer: string;
17
+ } | {
18
+ type: 'activate';
19
+ question: string;
20
+ } | {
21
+ type: 'connect';
22
+ content: string;
23
+ } | {
24
+ type: 'reflect';
25
+ prompt: string;
26
+ } | {
27
+ type: 'bloom';
28
+ level: BloomLevel;
29
+ question: string;
30
+ answer: string;
31
+ } | {
32
+ type: 'visualization';
33
+ visualizationType: 'chart' | 'simulation';
34
+ description: string;
35
+ };
36
+ /** User progress state passed into SegmentRenderer. */
37
+ export interface LessonUserProgress {
38
+ activationResponse?: string;
39
+ reflectionNotes?: string[];
40
+ bloomAnswered?: Record<number, boolean>;
41
+ }
42
+ /** Parse a lesson string into typed segments. Returns `[]` when input is empty. */
43
+ export declare function parseLessonSegments(lesson: string | undefined): LessonSegment[];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * CodeRunnerPanel Organism Component
3
+ *
4
+ * Editable code block with Run/Reset buttons and simulated terminal output.
5
+ * Real execution is a future concern; callers supply a simulation function via
6
+ * `onRun`. Emits `UI:RUN_CODE { language, exitCode }` on every run attempt.
7
+ *
8
+ * Event Contract:
9
+ * - Emits: UI:RUN_CODE { language, exitCode, error? }
10
+ * - entityAware: false
11
+ */
12
+ import React from 'react';
13
+ export interface CodeSimulationOutput {
14
+ stdout: string;
15
+ stderr: string;
16
+ exitCode: number;
17
+ testResults: Array<{
18
+ input: string;
19
+ expectedOutput: string;
20
+ actualOutput: string;
21
+ passed: boolean;
22
+ }>;
23
+ }
24
+ export interface CodeRunnerPanelProps {
25
+ /** Initial code content */
26
+ code: string;
27
+ /** Programming language for syntax highlighting */
28
+ language: string;
29
+ /** Whether the panel allows running (false = read-only code block) */
30
+ runnable?: boolean;
31
+ /**
32
+ * Simulate executing the code. Omit to render a read-only block.
33
+ * Real execution is a separate future track — this callback supplies
34
+ * deterministic simulated output for UI feedback.
35
+ */
36
+ onRun?: (code: string) => Promise<CodeSimulationOutput>;
37
+ /** Event name to emit on run (emitted as `UI:<runEvent>`). Defaults to 'RUN_CODE'. */
38
+ runEvent?: string;
39
+ /** Additional CSS classes */
40
+ className?: string;
41
+ }
42
+ export declare const CodeRunnerPanel: React.FC<CodeRunnerPanelProps>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * SegmentRenderer Organism Component
3
+ *
4
+ * Renders a parsed array of LessonSegments — markdown, code, quizzes,
5
+ * activation prompts, connections, reflections, Bloom questions.
6
+ * The `visualization` segment type is passed through as a no-op unless the
7
+ * caller provides `onRenderVisualization` (future extensibility hook).
8
+ *
9
+ * Event Contract:
10
+ * - Delegates to child molecules: UI:SAVE_ACTIVATION, UI:SAVE_REFLECTION, UI:ANSWER_BLOOM
11
+ * - entityAware: false
12
+ */
13
+ import React from 'react';
14
+ import { type CodeSimulationOutput } from './CodeRunnerPanel';
15
+ import type { LessonSegment, LessonUserProgress } from '../molecules/parseLessonSegments';
16
+ export type { LessonSegment, LessonUserProgress, CodeSimulationOutput };
17
+ export interface SegmentRendererProps {
18
+ /** Parsed lesson segments (see `parseLessonSegments`) */
19
+ segments: LessonSegment[];
20
+ /** Additional CSS classes for the root container */
21
+ className?: string;
22
+ /** CSS classes for the outer wrapping div */
23
+ containerClassName?: string;
24
+ /** User progress for restoring activation/reflection state */
25
+ userProgress?: LessonUserProgress;
26
+ /**
27
+ * Simulate executing runnable code blocks. Omit to render runnable blocks
28
+ * as read-only. Real execution is a future track.
29
+ */
30
+ onRunCodeSimulation?: (code: string, language: string) => Promise<CodeSimulationOutput>;
31
+ /**
32
+ * Optional render slot for `visualization` segment types. When not provided,
33
+ * visualization segments are silently skipped. Callers can wire this to any
34
+ * custom component or orbital generator.
35
+ */
36
+ onRenderVisualization?: (type: 'chart' | 'simulation', description: string, index: number) => React.ReactNode;
37
+ }
38
+ export declare const SegmentRenderer: React.FC<SegmentRendererProps>;
@@ -26,3 +26,5 @@ export { StepFlowOrganism, type StepFlowOrganismProps, } from "../../marketing/o
26
26
  export { ShowcaseOrganism, type ShowcaseOrganismProps, } from "../../marketing/organisms/ShowcaseOrganism";
27
27
  export { TeamOrganism, type TeamOrganismProps, } from "../../marketing/organisms/TeamOrganism";
28
28
  export { CaseStudyOrganism, type CaseStudyOrganismProps, } from "../../marketing/organisms/CaseStudyOrganism";
29
+ export { CodeRunnerPanel, type CodeRunnerPanelProps, type CodeSimulationOutput, } from './CodeRunnerPanel';
30
+ export { SegmentRenderer, type SegmentRendererProps, } from './SegmentRenderer';
@@ -17,6 +17,7 @@ import React from 'react';
17
17
  import type { EventEmit } from '@almadar/core';
18
18
  import * as THREE from 'three';
19
19
  import { AssetLoader } from './three/loaders/AssetLoader';
20
+ import { type MinimalMouseEvent } from './three/hooks/useGameCanvas3DEvents';
20
21
  import type { IsometricTile, IsometricUnit, IsometricFeature } from '../organisms/types/isometric';
21
22
  import './GameCanvas3D.css';
22
23
  export type { IsometricTile, IsometricUnit, IsometricFeature };
@@ -74,15 +75,15 @@ export interface GameCanvas3DProps {
74
75
  /** Background color */
75
76
  backgroundColor?: string;
76
77
  /** Callback when a tile is clicked */
77
- onTileClick?: (tile: IsometricTile, event: React.MouseEvent) => void;
78
+ onTileClick?: (tile: IsometricTile, event: MinimalMouseEvent) => void;
78
79
  /** Callback when a unit is clicked */
79
- onUnitClick?: (unit: IsometricUnit, event: React.MouseEvent) => void;
80
+ onUnitClick?: (unit: IsometricUnit, event: MinimalMouseEvent) => void;
80
81
  /** Callback when a feature is clicked */
81
- onFeatureClick?: (feature: IsometricFeature, event: React.MouseEvent) => void;
82
+ onFeatureClick?: (feature: IsometricFeature, event: MinimalMouseEvent) => void;
82
83
  /** Callback when canvas is clicked (background) */
83
- onCanvasClick?: (event: React.MouseEvent) => void;
84
+ onCanvasClick?: (event: MinimalMouseEvent) => void;
84
85
  /** Callback when mouse moves over a tile */
85
- onTileHover?: (tile: IsometricTile | null, event: React.MouseEvent) => void;
86
+ onTileHover?: (tile: IsometricTile | null, event: MinimalMouseEvent) => void;
86
87
  /** Callback for unit animation state change */
87
88
  onUnitAnimation?: (unitId: string, state: string) => void;
88
89
  /** Asset loader instance (uses global singleton if not provided) */
@@ -69,29 +69,29 @@ export interface MinimalMouseEvent {
69
69
  }
70
70
  export interface UseGameCanvas3DEventsOptions extends GameCanvas3DEventConfig {
71
71
  /** Callback for tile clicks (direct) */
72
- onTileClick?: (tile: IsometricTile, event: React.MouseEvent) => void;
72
+ onTileClick?: (tile: IsometricTile, event: MinimalMouseEvent) => void;
73
73
  /** Callback for unit clicks (direct) */
74
- onUnitClick?: (unit: IsometricUnit, event: React.MouseEvent) => void;
74
+ onUnitClick?: (unit: IsometricUnit, event: MinimalMouseEvent) => void;
75
75
  /** Callback for feature clicks (direct) */
76
- onFeatureClick?: (feature: IsometricFeature, event: React.MouseEvent) => void;
76
+ onFeatureClick?: (feature: IsometricFeature, event: MinimalMouseEvent) => void;
77
77
  /** Callback for canvas clicks (direct) */
78
- onCanvasClick?: (event: React.MouseEvent) => void;
78
+ onCanvasClick?: (event: MinimalMouseEvent) => void;
79
79
  /** Callback for tile hover (direct) */
80
- onTileHover?: (tile: IsometricTile | null, event: React.MouseEvent) => void;
80
+ onTileHover?: (tile: IsometricTile | null, event: MinimalMouseEvent) => void;
81
81
  /** Callback for unit animation changes (direct) */
82
82
  onUnitAnimation?: (unitId: string, state: string) => void;
83
83
  }
84
84
  export interface UseGameCanvas3DEventsReturn {
85
85
  /** Handle tile click - emits event and calls callback */
86
- handleTileClick: (tile: IsometricTile, event: React.MouseEvent) => void;
86
+ handleTileClick: (tile: IsometricTile, event: MinimalMouseEvent) => void;
87
87
  /** Handle unit click - emits event and calls callback */
88
- handleUnitClick: (unit: IsometricUnit, event: React.MouseEvent) => void;
88
+ handleUnitClick: (unit: IsometricUnit, event: MinimalMouseEvent) => void;
89
89
  /** Handle feature click - emits event and calls callback */
90
- handleFeatureClick: (feature: IsometricFeature, event: React.MouseEvent) => void;
90
+ handleFeatureClick: (feature: IsometricFeature, event: MinimalMouseEvent) => void;
91
91
  /** Handle canvas click - emits event and calls callback */
92
92
  handleCanvasClick: (event: MinimalMouseEvent) => void;
93
93
  /** Handle tile hover - emits event and calls callback */
94
- handleTileHover: (tile: IsometricTile | null, event: React.MouseEvent) => void;
94
+ handleTileHover: (tile: IsometricTile | null, event: MinimalMouseEvent) => void;
95
95
  /** Handle unit animation - emits event and calls callback */
96
96
  handleUnitAnimation: (unitId: string, state: string) => void;
97
97
  /** Handle camera change - emits event */
@@ -2518,6 +2518,28 @@ var GameCanvas3D = React11.forwardRef(
2518
2518
  else if (isAttackTarget) emissive = 4456448;
2519
2519
  else if (isValidMove) emissive = 17408;
2520
2520
  else if (isHovered) emissive = 2236962;
2521
+ if (tile.modelUrl) {
2522
+ return /* @__PURE__ */ jsxRuntime.jsx(
2523
+ "group",
2524
+ {
2525
+ position,
2526
+ onClick: (e) => handleTileClick(tile, e),
2527
+ onPointerEnter: (e) => handleTileHover(tile, e),
2528
+ onPointerLeave: (e) => handleTileHover(null, e),
2529
+ userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
2530
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2531
+ ModelLoader,
2532
+ {
2533
+ url: tile.modelUrl,
2534
+ scale: 0.95,
2535
+ fallbackGeometry: "box",
2536
+ castShadow: true,
2537
+ receiveShadow: true
2538
+ }
2539
+ )
2540
+ }
2541
+ );
2542
+ }
2521
2543
  return /* @__PURE__ */ jsxRuntime.jsxs(
2522
2544
  "mesh",
2523
2545
  {
@@ -2550,17 +2572,30 @@ var GameCanvas3D = React11.forwardRef(
2550
2572
  /* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
2551
2573
  /* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
2552
2574
  ] }),
2553
- /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.3, 0], children: [
2554
- /* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.3, 0.3, 0.1, 8] }),
2555
- /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
2556
- ] }),
2557
- /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.6, 0], children: [
2558
- /* @__PURE__ */ jsxRuntime.jsx("capsuleGeometry", { args: [0.2, 0.4, 4, 8] }),
2559
- /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
2560
- ] }),
2561
- /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.9, 0], children: [
2562
- /* @__PURE__ */ jsxRuntime.jsx("sphereGeometry", { args: [0.12, 8, 8] }),
2563
- /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
2575
+ unit.modelUrl ? (
2576
+ /* GLB unit model (box fallback while loading / on error) */
2577
+ /* @__PURE__ */ jsxRuntime.jsx(
2578
+ ModelLoader,
2579
+ {
2580
+ url: unit.modelUrl,
2581
+ scale: 0.5,
2582
+ fallbackGeometry: "box",
2583
+ castShadow: true
2584
+ }
2585
+ )
2586
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2587
+ /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.3, 0], children: [
2588
+ /* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.3, 0.3, 0.1, 8] }),
2589
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
2590
+ ] }),
2591
+ /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.6, 0], children: [
2592
+ /* @__PURE__ */ jsxRuntime.jsx("capsuleGeometry", { args: [0.2, 0.4, 4, 8] }),
2593
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
2594
+ ] }),
2595
+ /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.9, 0], children: [
2596
+ /* @__PURE__ */ jsxRuntime.jsx("sphereGeometry", { args: [0.12, 8, 8] }),
2597
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
2598
+ ] })
2564
2599
  ] }),
2565
2600
  unit.health !== void 0 && unit.maxHealth !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs("group", { position: [0, 1.2, 0], children: [
2566
2601
  /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [-0.25, 0, 0], children: [
@@ -2494,6 +2494,28 @@ var GameCanvas3D = forwardRef(
2494
2494
  else if (isAttackTarget) emissive = 4456448;
2495
2495
  else if (isValidMove) emissive = 17408;
2496
2496
  else if (isHovered) emissive = 2236962;
2497
+ if (tile.modelUrl) {
2498
+ return /* @__PURE__ */ jsx(
2499
+ "group",
2500
+ {
2501
+ position,
2502
+ onClick: (e) => handleTileClick(tile, e),
2503
+ onPointerEnter: (e) => handleTileHover(tile, e),
2504
+ onPointerLeave: (e) => handleTileHover(null, e),
2505
+ userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
2506
+ children: /* @__PURE__ */ jsx(
2507
+ ModelLoader,
2508
+ {
2509
+ url: tile.modelUrl,
2510
+ scale: 0.95,
2511
+ fallbackGeometry: "box",
2512
+ castShadow: true,
2513
+ receiveShadow: true
2514
+ }
2515
+ )
2516
+ }
2517
+ );
2518
+ }
2497
2519
  return /* @__PURE__ */ jsxs(
2498
2520
  "mesh",
2499
2521
  {
@@ -2526,17 +2548,30 @@ var GameCanvas3D = forwardRef(
2526
2548
  /* @__PURE__ */ jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
2527
2549
  /* @__PURE__ */ jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
2528
2550
  ] }),
2529
- /* @__PURE__ */ jsxs("mesh", { position: [0, 0.3, 0], children: [
2530
- /* @__PURE__ */ jsx("cylinderGeometry", { args: [0.3, 0.3, 0.1, 8] }),
2531
- /* @__PURE__ */ jsx("meshStandardMaterial", { color })
2532
- ] }),
2533
- /* @__PURE__ */ jsxs("mesh", { position: [0, 0.6, 0], children: [
2534
- /* @__PURE__ */ jsx("capsuleGeometry", { args: [0.2, 0.4, 4, 8] }),
2535
- /* @__PURE__ */ jsx("meshStandardMaterial", { color })
2536
- ] }),
2537
- /* @__PURE__ */ jsxs("mesh", { position: [0, 0.9, 0], children: [
2538
- /* @__PURE__ */ jsx("sphereGeometry", { args: [0.12, 8, 8] }),
2539
- /* @__PURE__ */ jsx("meshStandardMaterial", { color })
2551
+ unit.modelUrl ? (
2552
+ /* GLB unit model (box fallback while loading / on error) */
2553
+ /* @__PURE__ */ jsx(
2554
+ ModelLoader,
2555
+ {
2556
+ url: unit.modelUrl,
2557
+ scale: 0.5,
2558
+ fallbackGeometry: "box",
2559
+ castShadow: true
2560
+ }
2561
+ )
2562
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
2563
+ /* @__PURE__ */ jsxs("mesh", { position: [0, 0.3, 0], children: [
2564
+ /* @__PURE__ */ jsx("cylinderGeometry", { args: [0.3, 0.3, 0.1, 8] }),
2565
+ /* @__PURE__ */ jsx("meshStandardMaterial", { color })
2566
+ ] }),
2567
+ /* @__PURE__ */ jsxs("mesh", { position: [0, 0.6, 0], children: [
2568
+ /* @__PURE__ */ jsx("capsuleGeometry", { args: [0.2, 0.4, 4, 8] }),
2569
+ /* @__PURE__ */ jsx("meshStandardMaterial", { color })
2570
+ ] }),
2571
+ /* @__PURE__ */ jsxs("mesh", { position: [0, 0.9, 0], children: [
2572
+ /* @__PURE__ */ jsx("sphereGeometry", { args: [0.12, 8, 8] }),
2573
+ /* @__PURE__ */ jsx("meshStandardMaterial", { color })
2574
+ ] })
2540
2575
  ] }),
2541
2576
  unit.health !== void 0 && unit.maxHealth !== void 0 && /* @__PURE__ */ jsxs("group", { position: [0, 1.2, 0], children: [
2542
2577
  /* @__PURE__ */ jsxs("mesh", { position: [-0.25, 0, 0], children: [
@@ -51,6 +51,8 @@ export type IsometricUnit = {
51
51
  z?: number;
52
52
  /** Static sprite URL (used when no sprite sheet animation) */
53
53
  sprite?: AssetUrl;
54
+ /** 3D model URL (GLB format) for GameCanvas3D — rendered via ModelLoader with box fallback */
55
+ modelUrl?: AssetUrl;
54
56
  /** Unit archetype key for sprite resolution */
55
57
  unitType?: string;
56
58
  /** Hero identifier for sprite sheet lookup */