@blorkfield/blork-tabs 0.1.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.
@@ -0,0 +1,669 @@
1
+ /**
2
+ * @blorkfield/blork-tabs - Type Definitions
3
+ * A framework-agnostic tab/panel management system with snapping and docking
4
+ */
5
+ /**
6
+ * Configuration for initializing the TabManager
7
+ */
8
+ interface TabManagerConfig {
9
+ /** Distance threshold for panel-to-panel snapping (default: 50) */
10
+ snapThreshold?: number;
11
+ /** Gap between snapped panels (default: 0) */
12
+ panelGap?: number;
13
+ /** Margin from window edges (default: 16) */
14
+ panelMargin?: number;
15
+ /** Distance threshold for anchor snapping (default: 80) */
16
+ anchorThreshold?: number;
17
+ /** Default panel width for anchor calculations (default: 300) */
18
+ defaultPanelWidth?: number;
19
+ /** Container element for panels (default: document.body) */
20
+ container?: HTMLElement;
21
+ /** Whether to automatically initialize default anchors (default: true) */
22
+ initializeDefaultAnchors?: boolean;
23
+ /** Custom CSS class prefix (default: 'blork-tabs') */
24
+ classPrefix?: string;
25
+ }
26
+ /**
27
+ * Resolved configuration with all defaults applied
28
+ */
29
+ interface ResolvedTabManagerConfig {
30
+ snapThreshold: number;
31
+ panelGap: number;
32
+ panelMargin: number;
33
+ anchorThreshold: number;
34
+ defaultPanelWidth: number;
35
+ container: HTMLElement;
36
+ initializeDefaultAnchors: boolean;
37
+ classPrefix: string;
38
+ }
39
+ /**
40
+ * Configuration for creating a new panel
41
+ */
42
+ interface PanelConfig {
43
+ /** Unique identifier for the panel */
44
+ id: string;
45
+ /** Panel title displayed in header */
46
+ title?: string;
47
+ /** Initial width (default: 300) */
48
+ width?: number;
49
+ /** Initial collapsed state (default: true) */
50
+ startCollapsed?: boolean;
51
+ /** Initial position (optional - will be auto-positioned if not provided) */
52
+ initialPosition?: {
53
+ x: number;
54
+ y: number;
55
+ };
56
+ /** Content element or HTML string */
57
+ content?: HTMLElement | string;
58
+ /** Custom panel element (for existing DOM panels) */
59
+ element?: HTMLDivElement;
60
+ /** Custom drag handle element */
61
+ dragHandle?: HTMLDivElement;
62
+ /** Custom collapse button element */
63
+ collapseButton?: HTMLButtonElement;
64
+ /** Custom content wrapper element */
65
+ contentWrapper?: HTMLDivElement;
66
+ /** Custom detach grip element */
67
+ detachGrip?: HTMLDivElement;
68
+ /** Whether panel can be collapsed (default: true) */
69
+ collapsible?: boolean;
70
+ /** Whether panel can be detached from group (default: true) */
71
+ detachable?: boolean;
72
+ /** Whether panel can be dragged (default: true) */
73
+ draggable?: boolean;
74
+ /** Z-index for panel (default: 1000) */
75
+ zIndex?: number;
76
+ /** Z-index when dragging (default: 1002) */
77
+ dragZIndex?: number;
78
+ }
79
+ /**
80
+ * Configuration for anchor points
81
+ */
82
+ interface AnchorConfig {
83
+ /** Unique identifier for the anchor */
84
+ id: string;
85
+ /** Function that returns the anchor position (allows dynamic positioning) */
86
+ getPosition: () => Position;
87
+ /** Whether to show visual indicator (default: true) */
88
+ showIndicator?: boolean;
89
+ }
90
+ /**
91
+ * Preset anchor positions
92
+ */
93
+ type AnchorPreset = 'top-left' | 'top-right' | 'top-center' | 'bottom-left' | 'bottom-right' | 'bottom-center' | 'center-left' | 'center-right';
94
+ /**
95
+ * Current state of a panel
96
+ */
97
+ interface PanelState {
98
+ /** Unique panel identifier */
99
+ id: string;
100
+ /** DOM element for the panel */
101
+ element: HTMLDivElement;
102
+ /** Drag handle element */
103
+ dragHandle: HTMLDivElement;
104
+ /** Collapse button element (if collapsible) */
105
+ collapseButton: HTMLButtonElement | null;
106
+ /** Content wrapper element */
107
+ contentWrapper: HTMLDivElement;
108
+ /** Detach grip element (if detachable) */
109
+ detachGrip: HTMLDivElement | null;
110
+ /** Whether panel is currently collapsed */
111
+ isCollapsed: boolean;
112
+ /** ID of panel this is snapped to on its right (outgoing link) */
113
+ snappedTo: string | null;
114
+ /** ID of panel snapped to this on its left (incoming link) */
115
+ snappedFrom: string | null;
116
+ /** Panel configuration */
117
+ config: PanelConfig;
118
+ }
119
+ /**
120
+ * Current drag operation state
121
+ */
122
+ interface DragState {
123
+ /** Panel that initiated the drag */
124
+ grabbedPanel: PanelState;
125
+ /** Mouse offset from panel left edge */
126
+ offsetX: number;
127
+ /** Mouse offset from panel top edge */
128
+ offsetY: number;
129
+ /** Initial positions of all connected panels */
130
+ initialGroupPositions: Map<string, Position>;
131
+ /** Panels being moved in this drag operation */
132
+ movingPanels: PanelState[];
133
+ /** Drag mode - single panel or entire group */
134
+ mode: DragMode;
135
+ }
136
+ /**
137
+ * Drag mode determines what gets moved
138
+ */
139
+ type DragMode = 'single' | 'group';
140
+ /**
141
+ * Anchor state including visual indicator
142
+ */
143
+ interface AnchorState {
144
+ /** Anchor configuration */
145
+ config: AnchorConfig;
146
+ /** Visual indicator element */
147
+ indicator: HTMLDivElement | null;
148
+ }
149
+ /**
150
+ * Result of snap target detection
151
+ */
152
+ interface SnapTarget {
153
+ /** ID of the target panel */
154
+ targetId: string;
155
+ /** Which side of target to snap to */
156
+ side: SnapSide;
157
+ /** X position for snap */
158
+ x: number;
159
+ /** Y position for snap */
160
+ y: number;
161
+ }
162
+ /**
163
+ * Side of panel to snap to
164
+ */
165
+ type SnapSide = 'left' | 'right';
166
+ /**
167
+ * Result of anchor snap detection
168
+ */
169
+ interface AnchorSnapResult {
170
+ /** The matched anchor */
171
+ anchor: AnchorState;
172
+ /** Index of panel in group that docks to anchor */
173
+ dockPanelIndex: number;
174
+ /** Final positions for all panels in the group */
175
+ positions: Position[];
176
+ }
177
+ /**
178
+ * Simple x,y position
179
+ */
180
+ interface Position {
181
+ x: number;
182
+ y: number;
183
+ }
184
+ /**
185
+ * Bounding rectangle
186
+ */
187
+ interface Bounds {
188
+ left: number;
189
+ right: number;
190
+ top: number;
191
+ bottom: number;
192
+ width: number;
193
+ height: number;
194
+ }
195
+ /**
196
+ * Events emitted by TabManager
197
+ */
198
+ interface TabManagerEvents {
199
+ /** Fired when a panel is added */
200
+ 'panel:added': PanelAddedEvent;
201
+ /** Fired when a panel is removed */
202
+ 'panel:removed': PanelRemovedEvent;
203
+ /** Fired when a panel starts being dragged */
204
+ 'drag:start': DragStartEvent;
205
+ /** Fired during drag movement */
206
+ 'drag:move': DragMoveEvent;
207
+ /** Fired when drag ends */
208
+ 'drag:end': DragEndEvent;
209
+ /** Fired when panels snap together */
210
+ 'snap:panel': PanelSnapEvent;
211
+ /** Fired when panels snap to anchor */
212
+ 'snap:anchor': AnchorSnapEvent;
213
+ /** Fired when a panel is detached from group */
214
+ 'panel:detached': PanelDetachedEvent;
215
+ /** Fired when panel collapse state changes */
216
+ 'panel:collapse': PanelCollapseEvent;
217
+ }
218
+ interface PanelAddedEvent {
219
+ panel: PanelState;
220
+ }
221
+ interface PanelRemovedEvent {
222
+ panelId: string;
223
+ }
224
+ interface DragStartEvent {
225
+ panel: PanelState;
226
+ mode: DragMode;
227
+ movingPanels: PanelState[];
228
+ }
229
+ interface DragMoveEvent {
230
+ panel: PanelState;
231
+ position: Position;
232
+ snapTarget: SnapTarget | null;
233
+ anchorTarget: AnchorSnapResult | null;
234
+ }
235
+ interface DragEndEvent {
236
+ panel: PanelState;
237
+ finalPosition: Position;
238
+ snappedToPanel: boolean;
239
+ snappedToAnchor: boolean;
240
+ }
241
+ interface PanelSnapEvent {
242
+ movingPanels: PanelState[];
243
+ targetPanel: PanelState;
244
+ side: SnapSide;
245
+ }
246
+ interface AnchorSnapEvent {
247
+ movingPanels: PanelState[];
248
+ anchor: AnchorState;
249
+ }
250
+ interface PanelDetachedEvent {
251
+ panel: PanelState;
252
+ previousGroup: PanelState[];
253
+ }
254
+ interface PanelCollapseEvent {
255
+ panel: PanelState;
256
+ isCollapsed: boolean;
257
+ }
258
+ /**
259
+ * Event listener function type
260
+ */
261
+ type EventListener<T> = (event: T) => void;
262
+ /**
263
+ * CSS class names used by the library
264
+ */
265
+ interface CSSClasses {
266
+ panel: string;
267
+ panelHeader: string;
268
+ panelTitle: string;
269
+ panelContent: string;
270
+ panelContentCollapsed: string;
271
+ detachGrip: string;
272
+ collapseButton: string;
273
+ snapPreview: string;
274
+ snapPreviewVisible: string;
275
+ anchorIndicator: string;
276
+ anchorIndicatorVisible: string;
277
+ anchorIndicatorActive: string;
278
+ dragging: string;
279
+ }
280
+
281
+ /**
282
+ * @blorkfield/blork-tabs - TabManager
283
+ * Main orchestrator for the tab/panel management system
284
+ */
285
+
286
+ /**
287
+ * Main TabManager class - orchestrates all panel, snap, and anchor functionality
288
+ */
289
+ declare class TabManager {
290
+ private config;
291
+ private classes;
292
+ private panels;
293
+ private dragManager;
294
+ private anchorManager;
295
+ private snapPreview;
296
+ private eventListeners;
297
+ constructor(userConfig?: TabManagerConfig);
298
+ /**
299
+ * Add a new panel
300
+ */
301
+ addPanel(panelConfig: PanelConfig): PanelState;
302
+ /**
303
+ * Register an existing panel element
304
+ */
305
+ registerPanel(id: string, element: HTMLDivElement, options?: {
306
+ dragHandle?: HTMLDivElement;
307
+ collapseButton?: HTMLButtonElement;
308
+ contentWrapper?: HTMLDivElement;
309
+ detachGrip?: HTMLDivElement;
310
+ startCollapsed?: boolean;
311
+ }): PanelState;
312
+ /**
313
+ * Remove a panel
314
+ */
315
+ removePanel(id: string): boolean;
316
+ /**
317
+ * Get a panel by ID
318
+ */
319
+ getPanel(id: string): PanelState | undefined;
320
+ /**
321
+ * Get all panels
322
+ */
323
+ getAllPanels(): PanelState[];
324
+ /**
325
+ * Set up event handlers for a panel
326
+ */
327
+ private setupPanelEvents;
328
+ /**
329
+ * Get all panels in the same snap chain as the given panel
330
+ */
331
+ getSnapChain(panelId: string): PanelState[];
332
+ /**
333
+ * Manually snap two panels together
334
+ */
335
+ snap(leftPanelId: string, rightPanelId: string): boolean;
336
+ /**
337
+ * Detach a panel from its snap chain
338
+ */
339
+ detach(panelId: string): boolean;
340
+ /**
341
+ * Update snapped positions (call after collapse/expand or resize)
342
+ */
343
+ updatePositions(): void;
344
+ /**
345
+ * Add a custom anchor
346
+ */
347
+ addAnchor(config: AnchorConfig): AnchorState;
348
+ /**
349
+ * Add a preset anchor
350
+ */
351
+ addPresetAnchor(preset: AnchorPreset): AnchorState;
352
+ /**
353
+ * Remove an anchor
354
+ */
355
+ removeAnchor(id: string): boolean;
356
+ /**
357
+ * Get all anchors
358
+ */
359
+ getAnchors(): AnchorState[];
360
+ private handleDragStart;
361
+ private handleDragMove;
362
+ private handleDragEnd;
363
+ /**
364
+ * Subscribe to an event
365
+ */
366
+ on<K extends keyof TabManagerEvents>(event: K, listener: EventListener<TabManagerEvents[K]>): () => void;
367
+ /**
368
+ * Unsubscribe from an event
369
+ */
370
+ off<K extends keyof TabManagerEvents>(event: K, listener: EventListener<TabManagerEvents[K]>): void;
371
+ /**
372
+ * Emit an event
373
+ */
374
+ private emit;
375
+ /**
376
+ * Position panels in a row from right edge
377
+ */
378
+ positionPanelsFromRight(panelIds: string[], gap?: number): void;
379
+ /**
380
+ * Position panels in a row from left edge
381
+ */
382
+ positionPanelsFromLeft(panelIds: string[], gap?: number): void;
383
+ /**
384
+ * Set up initial snap chain for a list of panels (left to right)
385
+ */
386
+ createSnapChain(panelIds: string[]): void;
387
+ /**
388
+ * Get the current configuration
389
+ */
390
+ getConfig(): ResolvedTabManagerConfig;
391
+ /**
392
+ * Get the CSS classes used
393
+ */
394
+ getClasses(): CSSClasses;
395
+ /**
396
+ * Check if dragging is currently active
397
+ */
398
+ isDragging(): boolean;
399
+ /**
400
+ * Destroy the TabManager and clean up resources
401
+ */
402
+ destroy(): void;
403
+ }
404
+
405
+ /**
406
+ * @blorkfield/blork-tabs - AnchorManager
407
+ * Manages screen anchor points for panel docking
408
+ */
409
+
410
+ /**
411
+ * Creates the default anchor positions
412
+ */
413
+ declare function getDefaultAnchorConfigs(config: ResolvedTabManagerConfig): AnchorConfig[];
414
+ /**
415
+ * Create an anchor preset config
416
+ */
417
+ declare function createPresetAnchor(preset: AnchorPreset, config: ResolvedTabManagerConfig): AnchorConfig;
418
+ /**
419
+ * Manages anchor points and docking behavior
420
+ */
421
+ declare class AnchorManager {
422
+ private anchors;
423
+ private config;
424
+ private classes;
425
+ private container;
426
+ constructor(config: ResolvedTabManagerConfig, classes: CSSClasses);
427
+ /**
428
+ * Create an anchor indicator element
429
+ */
430
+ private createIndicator;
431
+ /**
432
+ * Add an anchor point
433
+ */
434
+ addAnchor(anchorConfig: AnchorConfig): AnchorState;
435
+ /**
436
+ * Add multiple default anchors
437
+ */
438
+ addDefaultAnchors(): void;
439
+ /**
440
+ * Add anchor from preset
441
+ */
442
+ addPresetAnchor(preset: AnchorPreset): AnchorState;
443
+ /**
444
+ * Remove an anchor
445
+ */
446
+ removeAnchor(id: string): boolean;
447
+ /**
448
+ * Update position of a single indicator
449
+ */
450
+ private updateIndicatorPosition;
451
+ /**
452
+ * Update all indicator positions (call on window resize)
453
+ */
454
+ updateIndicatorPositions(): void;
455
+ /**
456
+ * Find the nearest anchor for a group of moving panels
457
+ */
458
+ findNearestAnchor(movingPanels: PanelState[]): AnchorSnapResult | null;
459
+ /**
460
+ * Show anchor indicators during drag
461
+ */
462
+ showIndicators(activeAnchor: AnchorState | null): void;
463
+ /**
464
+ * Hide all anchor indicators
465
+ */
466
+ hideIndicators(): void;
467
+ /**
468
+ * Get all anchors
469
+ */
470
+ getAnchors(): AnchorState[];
471
+ /**
472
+ * Get anchor by ID
473
+ */
474
+ getAnchor(id: string): AnchorState | undefined;
475
+ /**
476
+ * Clean up
477
+ */
478
+ destroy(): void;
479
+ }
480
+
481
+ /**
482
+ * @blorkfield/blork-tabs - DragManager
483
+ * Handles drag operations for panels including single and group modes
484
+ */
485
+
486
+ interface DragCallbacks {
487
+ onDragStart?: (state: DragState) => void;
488
+ onDragMove?: (state: DragState, position: Position, snapTarget: SnapTarget | null, anchorResult: AnchorSnapResult | null) => void;
489
+ onDragEnd?: (state: DragState, snapTarget: SnapTarget | null, anchorResult: AnchorSnapResult | null) => void;
490
+ findAnchorTarget?: (movingPanels: PanelState[]) => AnchorSnapResult | null;
491
+ }
492
+ /**
493
+ * Creates and manages drag operations
494
+ */
495
+ declare class DragManager {
496
+ private panels;
497
+ private config;
498
+ private callbacks;
499
+ private activeDrag;
500
+ private boundMouseMove;
501
+ private boundMouseUp;
502
+ constructor(panels: Map<string, PanelState>, config: ResolvedTabManagerConfig, callbacks: DragCallbacks);
503
+ /**
504
+ * Start a drag operation
505
+ */
506
+ startDrag(e: MouseEvent, panel: PanelState, mode: DragMode): void;
507
+ /**
508
+ * Handle mouse movement during drag
509
+ */
510
+ private handleMouseMove;
511
+ /**
512
+ * Handle mouse up - finalize drag
513
+ */
514
+ private handleMouseUp;
515
+ /**
516
+ * Check if a drag is currently in progress
517
+ */
518
+ isActive(): boolean;
519
+ /**
520
+ * Get the current drag state
521
+ */
522
+ getState(): DragState | null;
523
+ /**
524
+ * Clean up event listeners
525
+ */
526
+ destroy(): void;
527
+ }
528
+
529
+ /**
530
+ * @blorkfield/blork-tabs - SnapPreview
531
+ * Visual feedback for snap targets during drag operations
532
+ */
533
+
534
+ /**
535
+ * Manages the visual snap preview indicator
536
+ */
537
+ declare class SnapPreview {
538
+ private element;
539
+ private classes;
540
+ private container;
541
+ private panels;
542
+ constructor(container: HTMLElement, classes: CSSClasses, panels: Map<string, PanelState>);
543
+ /**
544
+ * Create the preview element lazily
545
+ */
546
+ private ensureElement;
547
+ /**
548
+ * Update the preview to show a snap target
549
+ */
550
+ show(snapTarget: SnapTarget): void;
551
+ /**
552
+ * Hide the preview
553
+ */
554
+ hide(): void;
555
+ /**
556
+ * Update based on current snap target (convenience method)
557
+ */
558
+ update(snapTarget: SnapTarget | null): void;
559
+ /**
560
+ * Clean up
561
+ */
562
+ destroy(): void;
563
+ }
564
+
565
+ /**
566
+ * @blorkfield/blork-tabs - Panel
567
+ * Individual panel component with collapse/expand functionality
568
+ */
569
+
570
+ /**
571
+ * Creates the default panel DOM structure
572
+ */
573
+ declare function createPanelElement(config: PanelConfig, classes: CSSClasses): {
574
+ element: HTMLDivElement;
575
+ dragHandle: HTMLDivElement;
576
+ collapseButton: HTMLButtonElement | null;
577
+ contentWrapper: HTMLDivElement;
578
+ detachGrip: HTMLDivElement | null;
579
+ };
580
+ /**
581
+ * Creates a PanelState from config, using existing elements or creating new ones
582
+ */
583
+ declare function createPanelState(config: PanelConfig, classes: CSSClasses): PanelState;
584
+ /**
585
+ * Toggle panel collapse state
586
+ */
587
+ declare function toggleCollapse(state: PanelState, classes: CSSClasses, collapsed?: boolean): boolean;
588
+ /**
589
+ * Set panel position
590
+ */
591
+ declare function setPanelPosition(state: PanelState, x: number, y: number): void;
592
+ /**
593
+ * Get panel position from DOM
594
+ */
595
+ declare function getPanelPosition(state: PanelState): {
596
+ x: number;
597
+ y: number;
598
+ };
599
+ /**
600
+ * Get panel dimensions
601
+ */
602
+ declare function getPanelDimensions(state: PanelState): {
603
+ width: number;
604
+ height: number;
605
+ };
606
+ /**
607
+ * Set panel z-index
608
+ */
609
+ declare function setPanelZIndex(state: PanelState, zIndex: number): void;
610
+ /**
611
+ * Get default z-index for panel
612
+ */
613
+ declare function getDefaultZIndex(state: PanelState): number;
614
+ /**
615
+ * Get drag z-index for panel
616
+ */
617
+ declare function getDragZIndex(state: PanelState): number;
618
+
619
+ /**
620
+ * @blorkfield/blork-tabs - SnapChain
621
+ * Manages snap relationships between panels forming linked chains
622
+ */
623
+
624
+ /**
625
+ * Get all panels connected to a given panel (the entire snap chain)
626
+ * Returns panels ordered from left to right
627
+ */
628
+ declare function getConnectedGroup(startPanel: PanelState, panels: Map<string, PanelState>): PanelState[];
629
+ /**
630
+ * Detach a panel from its snap chain
631
+ * Clears both incoming and outgoing snap relationships
632
+ */
633
+ declare function detachFromGroup(panel: PanelState, panels: Map<string, PanelState>): void;
634
+ /**
635
+ * Find a snap target for the moving panels
636
+ * Checks if moving panels are close enough to snap to a stationary panel
637
+ */
638
+ declare function findSnapTarget(movingPanels: PanelState[], panels: Map<string, PanelState>, config: ResolvedTabManagerConfig): SnapTarget | null;
639
+ /**
640
+ * Execute a snap operation - position panels and establish relationships
641
+ */
642
+ declare function snapPanelsToTarget(movingPanels: PanelState[], targetId: string, side: SnapSide, x: number, y: number, panels: Map<string, PanelState>, config: ResolvedTabManagerConfig): void;
643
+ /**
644
+ * Update positions of all snapped panels when one changes size
645
+ * Traverses from rightmost panel leftward, repositioning as needed
646
+ */
647
+ declare function updateSnappedPositions(panels: Map<string, PanelState>, config: ResolvedTabManagerConfig): void;
648
+ /**
649
+ * Get the leftmost panel in a chain
650
+ */
651
+ declare function getLeftmostPanel(panel: PanelState, panels: Map<string, PanelState>): PanelState;
652
+ /**
653
+ * Get the rightmost panel in a chain
654
+ */
655
+ declare function getRightmostPanel(panel: PanelState, panels: Map<string, PanelState>): PanelState;
656
+ /**
657
+ * Check if two panels are in the same snap chain
658
+ */
659
+ declare function areInSameChain(panel1: PanelState, panel2: PanelState, panels: Map<string, PanelState>): boolean;
660
+ /**
661
+ * Establish a snap relationship between two panels
662
+ */
663
+ declare function snapPanels(leftPanel: PanelState, rightPanel: PanelState): void;
664
+ /**
665
+ * Break the snap relationship between two specific panels
666
+ */
667
+ declare function unsnap(leftPanel: PanelState, rightPanel: PanelState): void;
668
+
669
+ export { type AnchorConfig, AnchorManager, type AnchorPreset, type AnchorSnapEvent, type AnchorSnapResult, type AnchorState, type Bounds, type CSSClasses, type DragEndEvent, DragManager, type DragMode, type DragMoveEvent, type DragStartEvent, type DragState, type EventListener, type PanelAddedEvent, type PanelCollapseEvent, type PanelConfig, type PanelDetachedEvent, type PanelRemovedEvent, type PanelSnapEvent, type PanelState, type Position, type ResolvedTabManagerConfig, SnapPreview, type SnapSide, type SnapTarget, TabManager, type TabManagerConfig, type TabManagerEvents, areInSameChain, createPanelElement, createPanelState, createPresetAnchor, detachFromGroup, findSnapTarget, getConnectedGroup, getDefaultAnchorConfigs, getDefaultZIndex, getDragZIndex, getLeftmostPanel, getPanelDimensions, getPanelPosition, getRightmostPanel, setPanelPosition, setPanelZIndex, snapPanels, snapPanelsToTarget, toggleCollapse, unsnap, updateSnappedPositions };