@ahrowe/ui 0.25.1 → 0.26.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.
Files changed (82) hide show
  1. package/dist/esm/common/floorPlan/floorPlan.faces.mjs +2 -0
  2. package/dist/esm/common/floorPlan/floorPlan.faces.mjs.map +1 -0
  3. package/dist/esm/common/floorPlan/floorPlan.geometry.mjs +2 -0
  4. package/dist/esm/common/floorPlan/floorPlan.geometry.mjs.map +1 -0
  5. package/dist/esm/common/floorPlan/floorPlan.graph.mjs +2 -0
  6. package/dist/esm/common/floorPlan/floorPlan.graph.mjs.map +1 -0
  7. package/dist/esm/common/floorPlan/floorPlan.hit.mjs +2 -0
  8. package/dist/esm/common/floorPlan/floorPlan.hit.mjs.map +1 -0
  9. package/dist/esm/common/floorPlan/floorPlan.json.mjs +2 -0
  10. package/dist/esm/common/floorPlan/floorPlan.json.mjs.map +1 -0
  11. package/dist/esm/common/floorPlan/floorPlan.path.mjs +2 -0
  12. package/dist/esm/common/floorPlan/floorPlan.path.mjs.map +1 -0
  13. package/dist/esm/common/floorPlan/floorPlan.plan.mjs +2 -0
  14. package/dist/esm/common/floorPlan/floorPlan.plan.mjs.map +1 -0
  15. package/dist/esm/common/floorPlan/floorPlan.rooms.mjs +2 -0
  16. package/dist/esm/common/floorPlan/floorPlan.rooms.mjs.map +1 -0
  17. package/dist/esm/common/floorPlan/floorPlan.snap.mjs +2 -0
  18. package/dist/esm/common/floorPlan/floorPlan.snap.mjs.map +1 -0
  19. package/dist/esm/common/floorPlan/floorPlan.types.mjs +2 -0
  20. package/dist/esm/common/floorPlan/floorPlan.types.mjs.map +1 -0
  21. package/dist/esm/common/floorPlan/legacySvg.mjs +2 -0
  22. package/dist/esm/common/floorPlan/legacySvg.mjs.map +1 -0
  23. package/dist/esm/common/hooks/useSheet.mjs +1 -1
  24. package/dist/esm/common/hooks/useSheet.mjs.map +1 -1
  25. package/dist/esm/common/planCanvas/planCanvas.mjs +2 -0
  26. package/dist/esm/common/planCanvas/planCanvas.mjs.map +1 -0
  27. package/dist/esm/common/planCanvas/planCanvas.module.mjs +2 -0
  28. package/dist/esm/common/planCanvas/planCanvas.module.mjs.map +1 -0
  29. package/dist/esm/common/roomDrawer/roomDrawer.mjs +1 -1
  30. package/dist/esm/common/roomDrawer/roomDrawer.mjs.map +1 -1
  31. package/dist/esm/common/roomDrawer/roomDrawer.module.mjs +1 -1
  32. package/dist/esm/common/roomDrawer/roomDrawer.module.mjs.map +1 -1
  33. package/dist/esm/common/roomDrawer/usePlanHistory.mjs +2 -0
  34. package/dist/esm/common/roomDrawer/usePlanHistory.mjs.map +1 -0
  35. package/dist/esm/common/roomViewer/roomViewer.mjs +1 -1
  36. package/dist/esm/common/roomViewer/roomViewer.mjs.map +1 -1
  37. package/dist/esm/common/roomViewer/roomViewer.module.mjs +1 -1
  38. package/dist/esm/common/roomViewer/roomViewer.module.mjs.map +1 -1
  39. package/dist/esm/common/styles/sheet.module.mjs.map +1 -1
  40. package/dist/esm/index.mjs +1 -1
  41. package/dist/index.cjs +3 -7
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/style.css +1 -1
  44. package/dist/types/package/common/configProvider/configProvider.types.d.ts +4 -0
  45. package/dist/types/package/common/floorPlan/floorPlan.faces.d.ts +33 -0
  46. package/dist/types/package/common/floorPlan/floorPlan.fixtures.d.ts +35 -0
  47. package/dist/types/package/common/floorPlan/floorPlan.geometry.d.ts +111 -0
  48. package/dist/types/package/common/floorPlan/floorPlan.graph.d.ts +139 -0
  49. package/dist/types/package/common/floorPlan/floorPlan.hit.d.ts +26 -0
  50. package/dist/types/package/common/floorPlan/floorPlan.json.d.ts +23 -0
  51. package/dist/types/package/common/floorPlan/floorPlan.path.d.ts +59 -0
  52. package/dist/types/package/common/floorPlan/floorPlan.plan.d.ts +98 -0
  53. package/dist/types/package/common/floorPlan/floorPlan.rooms.d.ts +47 -0
  54. package/dist/types/package/common/floorPlan/floorPlan.snap.d.ts +101 -0
  55. package/dist/types/package/common/floorPlan/floorPlan.types.d.ts +276 -0
  56. package/dist/types/package/common/floorPlan/index.d.ts +28 -0
  57. package/dist/types/package/common/floorPlan/legacySvg.d.ts +27 -0
  58. package/dist/types/package/common/hooks/useSheet.d.ts +4 -1
  59. package/dist/types/package/common/planCanvas/index.d.ts +2 -0
  60. package/dist/types/package/common/planCanvas/planCanvas.d.ts +7 -0
  61. package/dist/types/package/common/planCanvas/planCanvas.types.d.ts +62 -0
  62. package/dist/types/package/common/roomDrawer/roomDrawer.d.ts +3 -3
  63. package/dist/types/package/common/roomDrawer/roomDrawer.types.d.ts +295 -15
  64. package/dist/types/package/common/roomDrawer/usePlanHistory.d.ts +24 -0
  65. package/dist/types/package/common/roomViewer/index.d.ts +1 -0
  66. package/dist/types/package/common/roomViewer/roomViewer.d.ts +9 -2
  67. package/dist/types/package/common/roomViewer/roomViewer.types.d.ts +88 -9
  68. package/dist/types/package/index.d.ts +1 -0
  69. package/docs/CLAUDE.md +1 -0
  70. package/docs/ConfigProvider.md +1 -0
  71. package/docs/Dropdown.md +1 -1
  72. package/docs/FloatingMenu.md +1 -1
  73. package/docs/FloorPlan.md +214 -0
  74. package/docs/Modal.md +2 -2
  75. package/docs/Popover.md +1 -1
  76. package/docs/RoomDrawer.md +330 -30
  77. package/docs/RoomViewer.md +80 -27
  78. package/package.json +1 -1
  79. package/dist/esm/common/roomDrawer/roomDrawer.utils.mjs +0 -6
  80. package/dist/esm/common/roomDrawer/roomDrawer.utils.mjs.map +0 -1
  81. package/dist/types/package/common/roomDrawer/roomDrawer.utils.d.ts +0 -32
  82. package/docs/room-drawing-analysis.md +0 -337
@@ -1,20 +1,300 @@
1
- export type Tool = 'select' | 'rect' | 'polygon';
2
- export interface Point {
3
- x: number;
4
- y: number;
1
+ import { CSSProperties, ReactNode } from 'react';
2
+ import { FloorPlan, OpeningKind, PlanParseResult, PlanPoint, PlanRoom, PlanSelection, PlanView } from '../floorPlan';
3
+ import { IconContent } from '../types/icon.types';
4
+ import { HtmlProps, SlotClassNames, SlotStyles } from '../types/slots.types';
5
+ /**
6
+ * The drawing tools.
7
+ *
8
+ * Each opening kind is its own tool rather than one opening tool with a kind sub-mode:
9
+ * that is one less piece of state, one less prop to wire up, and one less way to be in
10
+ * a mode you did not realise you were in.
11
+ *
12
+ * There is deliberately no "room" tool. Rooms are the enclosed faces the walls form, so
13
+ * drawing a closed shape with `Rect` or `Wall` already produces one. A mode that
14
+ * declared which walls belonged to which room would reintroduce exactly the ownership
15
+ * relation that made a door unable to cut a shared wall in the first place.
16
+ */
17
+ export type RoomDrawerTool = 'select' | 'wall' | 'rect' | 'door' | 'window' | 'archway' | 'measure';
18
+ export type RoomDrawerChangeReason = 'draw-wall' | 'draw-rect' | 'move-node' | 'move-wall' | 'move-room' | 'nudge' | 'add-opening' | 'move-opening' | 'edit-opening' | 'edit-room' | 'edit-wall' | 'delete' | 'add-floor' | 'duplicate-floor' | 'remove-floor' | 'rename-floor' | 'move-floor' | 'edit-floor' | 'clear' | 'import' | 'undo' | 'redo' | 'external';
19
+ export interface RoomDrawerChangeMeta {
20
+ reason: RoomDrawerChangeReason;
21
+ /**
22
+ * `false` for the intermediate values emitted while a pointer gesture is still in
23
+ * flight, `true` for the one that is also pushed onto the undo stack. `onChangeEnd`
24
+ * only ever fires with `true`.
25
+ */
26
+ committed: boolean;
27
+ /** Ids of whatever the change touched, for syncing an external selection. */
28
+ targetIds: string[];
5
29
  }
6
- export interface RoomData {
7
- id: string;
8
- name: string;
9
- points: Point[];
30
+ /** Every string the drawer's own chrome renders, for localisation. */
31
+ export interface RoomDrawerLabels {
32
+ select?: string;
33
+ wall?: string;
34
+ rect?: string;
35
+ measure?: string;
36
+ door?: string;
37
+ window?: string;
38
+ archway?: string;
39
+ undo?: string;
40
+ redo?: string;
41
+ download?: string;
42
+ upload?: string;
43
+ underlay?: string;
44
+ addFloor?: string;
45
+ floorName?: string;
46
+ duplicateFloor?: string;
47
+ deleteFloor?: string;
48
+ floorHeight?: string;
49
+ moveFloorUp?: string;
50
+ moveFloorDown?: string;
51
+ /** The inspector's noun for a floor, shown when nothing is selected. */
52
+ selectedFloor?: string;
53
+ /** Shown in the hint line when an opened file could not be read at all. */
54
+ importFailed?: string;
55
+ snap?: string;
56
+ angleLock?: string;
57
+ grid?: string;
58
+ zoomIn?: string;
59
+ zoomOut?: string;
60
+ fit?: string;
61
+ roomName?: string;
62
+ addCorner?: string;
63
+ thickness?: string;
64
+ width?: string;
65
+ sill?: string;
66
+ head?: string;
67
+ deleteSelection?: string;
68
+ /** Shown when several things are selected. `{count}` is replaced with the number. */
69
+ selectedCount?: string;
70
+ /** What the inspector calls the current selection. */
71
+ selectedNothing?: string;
72
+ selectedRoom?: string;
73
+ selectedWall?: string;
74
+ selectedCorner?: string;
75
+ /** Overrides the opening's own kind, which is used by default. */
76
+ selectedOpening?: string;
77
+ hintSelect?: string;
78
+ hintWall?: string;
79
+ hintRect?: string;
80
+ hintMeasure?: string;
81
+ hintOpening?: string;
10
82
  }
11
- export interface DoorData {
12
- id: string;
13
- roomId: string;
14
- wallIndex: number;
15
- t: number;
16
- width: number;
83
+ /** Every icon in the chrome is overridable. */
84
+ export interface RoomDrawerIcons {
85
+ select?: IconContent;
86
+ wall?: IconContent;
87
+ rect?: IconContent;
88
+ measure?: IconContent;
89
+ door?: IconContent;
90
+ window?: IconContent;
91
+ archway?: IconContent;
92
+ undo?: IconContent;
93
+ redo?: IconContent;
94
+ download?: IconContent;
95
+ upload?: IconContent;
96
+ underlay?: IconContent;
97
+ addFloor?: IconContent;
98
+ duplicateFloor?: IconContent;
99
+ moveFloorUp?: IconContent;
100
+ moveFloorDown?: IconContent;
101
+ snap?: IconContent;
102
+ angleLock?: IconContent;
103
+ grid?: IconContent;
104
+ zoomIn?: IconContent;
105
+ zoomOut?: IconContent;
106
+ fit?: IconContent;
107
+ delete?: IconContent;
17
108
  }
18
- export interface RoomDrawerProps {
109
+ export type RoomDrawerSlots = 'root' | 'toolbar' | 'toolButton' | 'toolIcon' | 'viewControls' | 'viewButton' | 'zoomLabel' | 'canvasWrapper' | 'canvas' | 'grid' | 'rooms' | 'roomFill' | 'roomLabel' | 'walls' | 'wallBody' | 'openings' | 'opening' | 'overlay' | 'measureLine' | 'floorBar' | 'floorTab' | 'inspector' | 'inspectorLabel' | 'inspectorField' | 'hint';
110
+ export interface RoomDrawerHandle {
111
+ /** The current plan, read synchronously. Useful in uncontrolled mode. */
112
+ getPlan: () => FloorPlan;
113
+ /** Replaces the plan. Pushes one history entry unless `resetHistory` is set. */
114
+ setPlan: (plan: FloorPlan, options?: {
115
+ resetHistory?: boolean;
116
+ }) => void;
117
+ /** Empties the plan, as one history entry. */
118
+ clear: () => void;
119
+ undo: () => void;
120
+ redo: () => void;
121
+ canUndo: () => boolean;
122
+ canRedo: () => boolean;
123
+ /** Drops past and future without touching the plan. */
124
+ clearHistory: () => void;
125
+ /** The floor being edited, and switching to another. */
126
+ getFloorId: () => string;
127
+ setFloorId: (floorId: string) => void;
128
+ /** Adds an empty storey above the current one and switches to it. Returns its id. */
129
+ addFloor: (options?: {
130
+ label?: string;
131
+ }) => string;
132
+ /** Copies the current storey, geometry and all, and switches to the copy. */
133
+ duplicateFloor: () => string | null;
134
+ /** Removes a storey. Refuses to remove the last one. */
135
+ removeFloor: (floorId?: string) => void;
136
+ renameFloor: (floorId: string, label: string) => void;
137
+ /** Moves a storey up or down the stack, re-stacking the elevations behind it. */
138
+ moveFloor: (floorId: string, toIndex: number) => void;
139
+ /** Storey height in millimetres. Everything above moves to suit. */
140
+ setFloorHeight: (floorId: string, heightMm: number) => void;
141
+ /**
142
+ * Removes every ruler measurement. They are view state, not plan data, so they never
143
+ * reach `onChange`, a file, or the undo stack.
144
+ */
145
+ clearMeasurements: () => void;
146
+ /** Writes the current plan to a JSON file, as the download button does. */
147
+ download: (fileName?: string) => void;
148
+ /**
149
+ * Reads JSON text as a plan and loads it, as one undoable entry. Returns what it made
150
+ * of the text: a `null` plan means nothing was loaded. Use it to accept a file from
151
+ * your own UI, a drop target, or a server.
152
+ */
153
+ loadJson: (text: string) => PlanParseResult;
154
+ /** Everything currently selected, in selection order. Empty when nothing is. */
155
+ getSelection: () => PlanSelection[];
156
+ /** Replaces the selection. A single entry or `null` is accepted for convenience. */
157
+ select: (selection: readonly PlanSelection[] | PlanSelection | null) => void;
158
+ /** Adds to, removes from, or toggles entries without replacing the rest. */
159
+ addToSelection: (selection: readonly PlanSelection[] | PlanSelection) => void;
160
+ toggleSelection: (entry: PlanSelection) => void;
161
+ selectAll: () => void;
162
+ /** Deletes everything selected, with the same rules as the Delete key. */
163
+ deleteSelection: () => void;
164
+ getView: () => PlanView;
165
+ setView: (view: PlanView) => void;
166
+ zoomBy: (factor: number) => void;
167
+ /** Fits the whole plan into the viewport. */
168
+ fitToContent: (options?: {
169
+ padding?: number;
170
+ }) => void;
171
+ /** Screen (client) coordinates to world, and back. */
172
+ toWorld: (clientX: number, clientY: number) => PlanPoint;
173
+ toScreen: (point: PlanPoint) => {
174
+ x: number;
175
+ y: number;
176
+ };
177
+ getSvgElement: () => SVGSVGElement | null;
178
+ }
179
+ export interface RoomDrawerProps extends Omit<HtmlProps<HTMLDivElement>, 'onChange' | 'onSelect' | 'defaultValue'> {
180
+ /** Controlled plan. Omit for uncontrolled. */
181
+ value?: FloorPlan;
182
+ /** Initial plan when uncontrolled. Defaults to an empty single-floor plan. */
183
+ defaultValue?: FloorPlan;
184
+ /**
185
+ * Fires for every mutation, including the intermediate values emitted during a drag
186
+ * (`meta.committed === false`), so a controlled parent renders live feedback. Mirrors
187
+ * `Slider`'s `onChange` / `onChangeEnd` pair.
188
+ */
189
+ onChange?: (plan: FloorPlan, meta: RoomDrawerChangeMeta) => void;
190
+ /** Fires once per completed gesture or discrete edit, always with `committed: true`. */
191
+ onChangeEnd?: (plan: FloorPlan, meta: RoomDrawerChangeMeta) => void;
192
+ /** Id of the floor being edited. Controlled; pair it with `onFloorChange`. */
193
+ floorId?: string;
194
+ /** Id of the floor to start on. Uncontrolled. Defaults to the lowest. */
195
+ defaultFloorId?: string;
196
+ onFloorChange?: (floorId: string) => void;
197
+ /** The floor tabs and their add button. Default `true`. */
198
+ showFloorBar?: boolean;
199
+ /**
200
+ * Trace the storey below behind the one being edited, so walls can be lined up between
201
+ * floors. Default `true`; the toolbar toggle can still turn it off.
202
+ */
203
+ showUnderlay?: boolean;
204
+ tool?: RoomDrawerTool;
205
+ defaultTool?: RoomDrawerTool;
206
+ onToolChange?: (tool: RoomDrawerTool) => void;
207
+ /** Restricts and orders the toolbar. Defaults to all six tools. */
208
+ tools?: RoomDrawerTool[];
209
+ /**
210
+ * Controlled selection. An array, because the drawer supports selecting several things
211
+ * at once; a single selection is a one-element array.
212
+ */
213
+ selection?: readonly PlanSelection[];
214
+ defaultSelection?: readonly PlanSelection[];
215
+ onSelectionChange?: (selection: PlanSelection[]) => void;
216
+ /**
217
+ * Allow selecting more than one thing, by Ctrl/Cmd-clicking or by dragging a marquee
218
+ * across empty canvas with the select tool. Default `true`.
219
+ */
220
+ multiSelect?: boolean;
221
+ onHoverChange?: (target: PlanSelection | null) => void;
222
+ view?: PlanView;
223
+ defaultView?: PlanView;
224
+ onViewChange?: (view: PlanView) => void;
225
+ /** Screen pixels per millimetre. */
226
+ minZoom?: number;
227
+ maxZoom?: number;
228
+ /** Fit the plan into the viewport on mount, and when it first becomes non-empty. */
229
+ fitOnMount?: boolean;
230
+ /** Grid spacing in millimetres (default 100, i.e. 10cm). */
231
+ gridSize?: number;
232
+ showGrid?: boolean;
233
+ /** Snap to the grid and to existing geometry. Alt inverts it for one gesture. */
234
+ snap?: boolean;
235
+ /**
236
+ * Latch angle lock on, so drawing is constrained to multiples of `constrainAngle`
237
+ * without holding Shift. Shift then momentarily RELEASES the lock. Default `false`.
238
+ */
239
+ angleLock?: boolean;
240
+ /** The angle step in degrees that angle lock snaps to (default 45). */
241
+ constrainAngle?: number;
242
+ /** Draw the construction lines explaining what the pointer snapped to (default `true`). */
243
+ showSnapGuides?: boolean;
244
+ /** Millimetres. Applied to newly drawn walls. */
245
+ wallThickness?: number;
246
+ wallHeight?: number;
247
+ /** Defaults for newly placed openings, per kind. */
248
+ openingDefaults?: Partial<Record<OpeningKind, {
249
+ width?: number;
250
+ sill?: number;
251
+ head?: number;
252
+ }>>;
253
+ showMeasurements?: boolean;
254
+ formatLength?: (lengthMm: number) => string;
255
+ formatArea?: (areaMm2: number) => string;
256
+ showToolbar?: boolean;
257
+ showInspector?: boolean;
258
+ showHint?: boolean;
259
+ showHistoryControls?: boolean;
260
+ /** Download and open buttons in the toolbar. Default `true`. */
261
+ showFileControls?: boolean;
262
+ /**
263
+ * Name for the downloaded file, with `.json` appended if it is missing. Defaults to the
264
+ * plan's own `name`, or `floor-plan.json`.
265
+ */
266
+ fileName?: string;
267
+ /**
268
+ * Fires after every file the user opens, whether it worked or not. `issues` is empty
269
+ * only for a file that needed no repair; see `planFromJson`. Without this the drawer
270
+ * still reports a bad file in its hint line.
271
+ */
272
+ onImport?: (result: PlanParseResult) => void;
273
+ /**
274
+ * `'internal'` keeps an undo stack inside the component. `'none'` disables it and
275
+ * hides the buttons, for a consumer that owns its own history.
276
+ */
277
+ historyMode?: 'internal' | 'none';
278
+ historyLimit?: number;
279
+ onHistoryChange?: (state: {
280
+ canUndo: boolean;
281
+ canRedo: boolean;
282
+ }) => void;
283
+ /** View and select, but no edits. */
284
+ readOnly?: boolean;
285
+ disabled?: boolean;
286
+ /** Height of the canvas, any CSS length. */
287
+ canvasHeight?: string;
288
+ getRoomColor?: (room: PlanRoom, area: number) => string | undefined;
289
+ renderRoomLabel?: (room: PlanRoom | null, context: {
290
+ area: number;
291
+ faceId: string;
292
+ }) => ReactNode;
293
+ labels?: RoomDrawerLabels;
294
+ icons?: RoomDrawerIcons;
295
+ 'aria-label'?: string;
19
296
  className?: string;
297
+ style?: CSSProperties;
298
+ classNames?: SlotClassNames<RoomDrawerSlots>;
299
+ styles?: SlotStyles<RoomDrawerSlots>;
20
300
  }
@@ -0,0 +1,24 @@
1
+ import { FloorPlan } from '../floorPlan';
2
+ import { RoomDrawerChangeReason } from './roomDrawer.types';
3
+ export interface PlanHistory {
4
+ present: FloorPlan;
5
+ canUndo: boolean;
6
+ canRedo: boolean;
7
+ /** Stashes the current plan as the snapshot a later `endGesture` would restore to. */
8
+ beginGesture: () => void;
9
+ /** Replaces the present mid-gesture. Pushes nothing. */
10
+ updateGesture: (plan: FloorPlan) => void;
11
+ /** Pushes the stashed snapshot, if the gesture actually changed anything. */
12
+ endGesture: () => boolean;
13
+ /** Discards the gesture and restores the stashed snapshot. */
14
+ cancelGesture: () => FloorPlan | null;
15
+ /** A discrete edit: pushes and replaces atomically. */
16
+ commit: (plan: FloorPlan, reason: RoomDrawerChangeReason, targetIds?: string[]) => void;
17
+ /** Replaces the present without touching history, for an externally driven value. */
18
+ replace: (plan: FloorPlan) => void;
19
+ undo: () => FloorPlan | null;
20
+ redo: () => FloorPlan | null;
21
+ clearHistory: () => void;
22
+ isGestureActive: () => boolean;
23
+ }
24
+ export declare function usePlanHistory(initial: FloorPlan, limit?: number): PlanHistory;
@@ -1 +1,2 @@
1
1
  export { default } from './roomViewer';
2
+ export * from './roomViewer.types';
@@ -1,3 +1,10 @@
1
- import { RoomViewerProps } from './roomViewer.types';
2
- declare function RoomViewer({ data }: RoomViewerProps): import("react/jsx-runtime").JSX.Element;
1
+ import { RoomViewerHandle, RoomViewerProps } from './roomViewer.types';
2
+ /**
3
+ * Read-only view of a floor plan.
4
+ *
5
+ * It renders the same `planCanvas` the drawer does, from the same `FloorPlan`, so the
6
+ * two cannot drift apart. Selection and hover are React state rather than classes pushed
7
+ * onto DOM nodes, which is what lets them be controlled from outside.
8
+ */
9
+ declare const RoomViewer: import('react').ForwardRefExoticComponent<RoomViewerProps & import('react').RefAttributes<RoomViewerHandle>>;
3
10
  export default RoomViewer;
@@ -1,10 +1,89 @@
1
- import { default as React } from 'react';
2
- export interface RoomViewerFloor {
3
- /** Display label shown above the floor plan (e.g. "Ground Floor") */
4
- label: string;
5
- /** The SVG floor plan to render for this floor */
6
- data: React.ReactNode;
7
- }
8
- export interface RoomViewerProps {
9
- data?: RoomViewerFloor[];
1
+ import { CSSProperties, ReactNode } from 'react';
2
+ import { DerivedRoom, FloorPlan, PlanRoom, PlanView } from '../floorPlan';
3
+ import { IconContent } from '../types/icon.types';
4
+ import { HtmlProps, SlotClassNames, SlotStyles } from '../types/slots.types';
5
+ export type RoomViewerSlots = 'root' | 'floorBar' | 'floorTab' | 'canvasWrapper' | 'canvas' | 'grid' | 'rooms' | 'roomFill' | 'roomLabel' | 'walls' | 'wallBody' | 'openings' | 'opening' | 'viewControls' | 'viewButton' | 'empty';
6
+ export interface RoomViewerLabels {
7
+ /** Accessible name for the floor strip. */
8
+ floors?: string;
9
+ zoomIn?: string;
10
+ zoomOut?: string;
11
+ fit?: string;
12
+ empty?: string;
13
+ /** Announced when a room is selected. `{name}` and `{area}` are replaced. */
14
+ selectedRoom?: string;
15
+ /** Announced when the selection is cleared. */
16
+ noRoomSelected?: string;
17
+ }
18
+ export interface RoomViewerIcons {
19
+ zoomIn?: IconContent;
20
+ zoomOut?: IconContent;
21
+ fit?: IconContent;
22
+ }
23
+ export interface RoomViewerHandle {
24
+ /** Fits the active floor into the viewport. */
25
+ fitToContent: (options?: {
26
+ padding?: number;
27
+ }) => void;
28
+ getView: () => PlanView;
29
+ setView: (view: PlanView) => void;
30
+ zoomBy: (factor: number) => void;
31
+ /** Selects a room, or clears the selection with `null`. */
32
+ select: (roomId: string | null) => void;
33
+ getFloorId: () => string | null;
34
+ setFloorId: (floorId: string) => void;
35
+ /** The live `<svg>`, for measuring or screenshotting. */
36
+ getSvgElement: () => SVGSVGElement | null;
37
+ }
38
+ export interface RoomViewerProps extends Omit<HtmlProps<HTMLDivElement>, 'onSelect'> {
39
+ /**
40
+ * The plan to display, floors and all. This replaces the old `data` prop, which took
41
+ * pre-rendered SVG: that could not be selected, measured or re-themed, and nothing
42
+ * could be shared with the drawer.
43
+ */
44
+ plan?: FloorPlan;
45
+ /** Which storey is showing. Controlled; pair with `onFloorChange`. */
46
+ activeFloorId?: string;
47
+ defaultActiveFloorId?: string;
48
+ onFloorChange?: (floorId: string) => void;
49
+ /** Controlled selection. `null` is a real value here: nothing selected. */
50
+ selectedRoomId?: string | null;
51
+ defaultSelectedRoomId?: string | null;
52
+ onSelect?: (roomId: string | null, room: DerivedRoom | null, floorId: string) => void;
53
+ hoveredRoomId?: string | null;
54
+ onHoverChange?: (roomId: string | null) => void;
55
+ view?: PlanView;
56
+ defaultView?: PlanView;
57
+ onViewChange?: (view: PlanView) => void;
58
+ /** Rooms respond to pointer and keyboard. Default `true`. */
59
+ interactive?: boolean;
60
+ /** Drag to pan, wheel and pinch to zoom. Default `true`. */
61
+ pannable?: boolean;
62
+ /** Fit the plan into view once it is measured. Default `true`. */
63
+ fitOnMount?: boolean;
64
+ showFloorBar?: boolean;
65
+ showViewControls?: boolean;
66
+ showGrid?: boolean;
67
+ showRoomLabels?: boolean;
68
+ /** Door leaves, swing arcs and window symbols. Default `true`. */
69
+ showOpeningSymbols?: boolean;
70
+ getRoomColor?: (room: PlanRoom, area: number) => string | undefined;
71
+ renderRoomLabel?: (room: PlanRoom | null, context: {
72
+ area: number;
73
+ faceId: string;
74
+ }) => ReactNode;
75
+ getRoomLabel?: (room: PlanRoom | null, context: {
76
+ area: number;
77
+ }) => string;
78
+ formatArea?: (areaMm2: number) => string;
79
+ /** Canvas height. A number is pixels. Default 360. */
80
+ height?: number | string;
81
+ /** Shown when there is no plan, or the plan has nothing in it. */
82
+ emptyState?: ReactNode;
83
+ labels?: RoomViewerLabels;
84
+ icons?: RoomViewerIcons;
85
+ className?: string;
86
+ style?: CSSProperties;
87
+ classNames?: SlotClassNames<RoomViewerSlots>;
88
+ styles?: SlotStyles<RoomViewerSlots>;
10
89
  }
@@ -55,6 +55,7 @@ export { default as Flip } from './common/flip';
55
55
  export * from './common/flip';
56
56
  export { default as FloatingMenu } from './common/floatingMenu';
57
57
  export * from './common/floatingMenu';
58
+ export * from './common/floorPlan';
58
59
  export { default as IconPicker } from './common/iconPicker';
59
60
  export * from './common/iconPicker';
60
61
  export { default as IdleManager } from './common/idleManager';
package/docs/CLAUDE.md CHANGED
@@ -143,6 +143,7 @@ FontAwesome internals, so they still take an `IconDefinition` only.
143
143
  @Fab.md
144
144
  @Flip.md
145
145
  @FloatingMenu.md
146
+ @FloorPlan.md
146
147
  @FormValidator.md
147
148
  @FormValidatorGroup.md
148
149
  @IconPicker.md
@@ -94,6 +94,7 @@ Each entry is a `Partial<...Props>`, so any of that component's props can be def
94
94
  - **Form inputs:** `Input` · `Textarea` · `NumberInput` · `Dropdown` · `InputDropdown` · `Checkbox` · `Switch` · `RadioGroup` · `DatePicker` · `OptionPicker` · `OtpInput`
95
95
  - **Display:** `Button` · `ActionButtons` · `Badge` · `Chip` · `Card` · `SectionHeader` · `Skeleton` · `Accordion` · `Divider` · `Timer`
96
96
  - **Overlays:** `ConfirmModal` · `Modal` · `FloatingMenu` · `Popover`
97
+ - **Editors:** `RoomDrawer`, `RoomViewer`
97
98
 
98
99
  ```tsx
99
100
  <ConfigProvider
package/docs/Dropdown.md CHANGED
@@ -64,4 +64,4 @@ const statusValidator = new FormValidator('', [Validators.required()]);
64
64
 
65
65
  The list is rendered through a portal and tracks the trigger across scroll, so it always escapes clipping ancestors (cards, scroll containers, virtualized lists). It flips to open upward automatically when there's no room below, and shifts horizontally to stay within the viewport when the trigger sits near the left or right edge of the screen. It hides (without closing) if the trigger itself scrolls behind a clipping ancestor, reappearing once it's back in view. It also re-measures when its own content changes size while open, so a list that shrinks (e.g. filtered down to fewer entries) stays anchored to the trigger instead of hanging above it.
66
66
 
67
- **Bottom sheet:** `presentation={Presentation.Sheet}` drops the anchoring entirely and pins the list to the bottom of the screen instead, full width up to 560px, behind a backdrop, scrolling internally past 70% of the viewport height and lifting itself above an open on-screen keyboard. Tapping the backdrop closes the list without also pressing whatever sits behind it. It is the same list with the same keyboard handling, so selection, highlighting and infinite scroll are unchanged. `Presentation.Auto` does that only when the pointer is coarse *and* the screen is under 768px wide. The default is `Presentation.Default`. A whole app can opt in with `<ConfigProvider defaultProps={{ Dropdown: { presentation: Presentation.Auto } }}>`, or for every sheet-capable component at once with `<ConfigProvider presentation={Presentation.Auto}>` — see [ConfigProvider.md](ConfigProvider.md).
67
+ **Bottom sheet:** `presentation={Presentation.Sheet}` drops the anchoring entirely and pins the list to the bottom of the screen instead, full width up to 560px, behind a backdrop, scrolling internally past 70% of the viewport height and lifting itself above an open on-screen keyboard, shrinking to fit the space that leaves rather than running off the top of the screen. Tapping the backdrop closes the list without also pressing whatever sits behind it. It is the same list with the same keyboard handling, so selection, highlighting and infinite scroll are unchanged. `Presentation.Auto` does that only when the pointer is coarse *and* the screen is under 768px wide. The default is `Presentation.Default`. A whole app can opt in with `<ConfigProvider defaultProps={{ Dropdown: { presentation: Presentation.Auto } }}>`, or for every sheet-capable component at once with `<ConfigProvider presentation={Presentation.Auto}>` — see [ConfigProvider.md](ConfigProvider.md).
@@ -67,7 +67,7 @@ import { FloatingMenu, Presentation } from '@ahrowe/ui';
67
67
 
68
68
  **Slots:** `root` `trigger` `menu` `menuContainer` `backdrop` (`backdrop` only exists while presenting as a sheet)
69
69
 
70
- **Bottom sheet:** with `Presentation.Sheet` the menu leaves its trigger entirely: it pins to the bottom of the screen, spans the full width up to 560px, scrolls internally past 70% of the viewport height, and dims the page behind a backdrop. Scrolling on the page behind it is locked while it's open, Escape closes it even when focus never entered it, and it lifts itself above an open on-screen keyboard. Tapping the backdrop closes the sheet and nothing else: the tap is consumed by the backdrop rather than falling through to whatever sits behind it. `Presentation.Auto` applies that only when the pointer is coarse *and* the screen is under 768px wide: pointer alone would catch touchscreen laptops, width alone would catch a narrow desktop window. Set it once for a whole app via `ConfigProvider`:
70
+ **Bottom sheet:** with `Presentation.Sheet` the menu leaves its trigger entirely: it pins to the bottom of the screen, spans the full width up to 560px, scrolls internally past 70% of the viewport height, and dims the page behind a backdrop. Scrolling on the page behind it is locked while it's open, Escape closes it even when focus never entered it, and it lifts itself above an open on-screen keyboard, shrinking to fit the space that leaves rather than running off the top of the screen. Tapping the backdrop closes the sheet and nothing else: the tap is consumed by the backdrop rather than falling through to whatever sits behind it. `Presentation.Auto` applies that only when the pointer is coarse *and* the screen is under 768px wide: pointer alone would catch touchscreen laptops, width alone would catch a narrow desktop window. Set it once for a whole app via `ConfigProvider`:
71
71
 
72
72
  ```tsx
73
73
  <ConfigProvider defaultProps={{ FloatingMenu: { presentation: Presentation.Auto } }}>