wip
This commit is contained in:
parent
7161858a0d
commit
12cde6f378
7 changed files with 642 additions and 100 deletions
|
|
@ -84,7 +84,13 @@ export class GridCommand {
|
|||
};
|
||||
const dir = dirMap[e.key];
|
||||
if (dir) {
|
||||
const next = getNextMatrixItem(rows, row, col, dir);
|
||||
const next = getNextMatrixItem({
|
||||
matrix: rows,
|
||||
currentRow: row,
|
||||
currentCol: col,
|
||||
direction: dir,
|
||||
isAvailable: (item) => item.dataset.disabled === undefined,
|
||||
});
|
||||
if (next) {
|
||||
this.highlighted = next.dataset.value;
|
||||
this.scrollToHighlighted();
|
||||
|
|
@ -170,18 +176,23 @@ export class GridCommand {
|
|||
}
|
||||
|
||||
getItems() {
|
||||
return this.getRows().flatMap((row) => row);
|
||||
return this.getRows()
|
||||
.flatMap((row) => row)
|
||||
.filter((item) => !item.dataset.disabled);
|
||||
}
|
||||
|
||||
getItem(value: string) {
|
||||
getItem(value: string, args: { disabled?: boolean } = {}) {
|
||||
return {
|
||||
'data-thom-grid-command-item': '',
|
||||
'data-highlighted': dataAttr(value === this.highlighted),
|
||||
'data-value': dataAttr(value),
|
||||
'data-disabled': dataAttr(args?.disabled),
|
||||
onmouseover: () => {
|
||||
if (args?.disabled) return;
|
||||
this.highlighted = value;
|
||||
},
|
||||
onclick: () => {
|
||||
if (args?.disabled) return;
|
||||
this.onSelect(value);
|
||||
},
|
||||
[createAttachmentKey()]: () => {
|
||||
|
|
|
|||
486
src/lib/utils/array.spec.ts
Normal file
486
src/lib/utils/array.spec.ts
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { getNextMatrixItem } from './array';
|
||||
|
||||
// --- Test Data ---
|
||||
|
||||
// Simple matrix for basic tests
|
||||
const simpleMatrix = [
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
[7, 8, 9],
|
||||
];
|
||||
|
||||
// Jagged matrix to test snapping behavior
|
||||
const jaggedMatrix = [
|
||||
[10, 11, 12], // Row 0 (length 3)
|
||||
[13, 14], // Row 1 (length 2)
|
||||
[15, 16, 17, 18], // Row 2 (length 4)
|
||||
[19], // Row 3 (length 1)
|
||||
[], // Row 4 (length 0)
|
||||
];
|
||||
|
||||
// Item interface for availability tests
|
||||
interface TestItem {
|
||||
id: number;
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
// Matrix with availability for testing isAvailable predicate
|
||||
const matrixWithAvailability: TestItem[][] = [
|
||||
[
|
||||
{ id: 1, available: true },
|
||||
{ id: 2, available: false },
|
||||
{ id: 3, available: true },
|
||||
], // Row 0
|
||||
[
|
||||
{ id: 4, available: true },
|
||||
{ id: 5, available: false },
|
||||
], // Row 1
|
||||
[
|
||||
{ id: 6, available: true },
|
||||
{ id: 7, available: true },
|
||||
{ id: 8, available: false },
|
||||
{ id: 9, available: true },
|
||||
], // Row 2
|
||||
[{ id: 10, available: false }], // Row 3
|
||||
[
|
||||
{ id: 11, available: false },
|
||||
{ id: 12, available: false },
|
||||
], // Row 4 (all unavailable)
|
||||
[{ id: 13, available: true }], // Row 5
|
||||
];
|
||||
|
||||
// isAvailable predicate for TestItem
|
||||
const isTestItemAvailable = (item: TestItem) => item.available;
|
||||
|
||||
// --- Test Suite ---
|
||||
|
||||
describe('getNextMatrixItem', () => {
|
||||
// --- Input Validation Tests ---
|
||||
|
||||
describe('Input Validation', () => {
|
||||
it('should return undefined for empty matrix', () => {
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: [], currentRow: 0, currentCol: 0, direction: 'down' })
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for invalid currentRow (negative)', () => {
|
||||
expect(
|
||||
getNextMatrixItem({
|
||||
matrix: simpleMatrix,
|
||||
currentRow: -1,
|
||||
currentCol: 0,
|
||||
direction: 'down',
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for invalid currentRow (out of bounds)', () => {
|
||||
expect(
|
||||
getNextMatrixItem({
|
||||
matrix: simpleMatrix,
|
||||
currentRow: 99,
|
||||
currentCol: 0,
|
||||
direction: 'down',
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for invalid currentCol (negative)', () => {
|
||||
expect(
|
||||
getNextMatrixItem({
|
||||
matrix: simpleMatrix,
|
||||
currentRow: 0,
|
||||
currentCol: -1,
|
||||
direction: 'right',
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for invalid currentCol (out of bounds for current row)', () => {
|
||||
expect(
|
||||
getNextMatrixItem({
|
||||
matrix: simpleMatrix,
|
||||
currentRow: 0,
|
||||
currentCol: 99,
|
||||
direction: 'right',
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// --- Basic Movement Tests (without isAvailable, no jagged snapping scenarios) ---
|
||||
|
||||
describe('Basic Movement (standard grid)', () => {
|
||||
it('should move right correctly', () => {
|
||||
expect(
|
||||
getNextMatrixItem({
|
||||
matrix: simpleMatrix,
|
||||
currentRow: 0,
|
||||
currentCol: 0,
|
||||
direction: 'right',
|
||||
})
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it('should move left correctly', () => {
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: simpleMatrix, currentRow: 0, currentCol: 2, direction: 'left' })
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it('should move down correctly', () => {
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: simpleMatrix, currentRow: 0, currentCol: 0, direction: 'down' })
|
||||
).toBe(4);
|
||||
});
|
||||
|
||||
it('should move up correctly', () => {
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: simpleMatrix, currentRow: 2, currentCol: 0, direction: 'up' })
|
||||
).toBe(4);
|
||||
});
|
||||
|
||||
it('should return undefined when moving right off edge', () => {
|
||||
expect(
|
||||
getNextMatrixItem({
|
||||
matrix: simpleMatrix,
|
||||
currentRow: 0,
|
||||
currentCol: 2,
|
||||
direction: 'right',
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when moving left off edge', () => {
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: simpleMatrix, currentRow: 0, currentCol: 0, direction: 'left' })
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when moving down off edge', () => {
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: simpleMatrix, currentRow: 2, currentCol: 0, direction: 'down' })
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when moving up off edge', () => {
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: simpleMatrix, currentRow: 0, currentCol: 0, direction: 'up' })
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// --- Jagged Array & Snapping Tests ---
|
||||
|
||||
describe('Jagged Array & Snapping', () => {
|
||||
it('should snap right when moving down to a shorter row', () => {
|
||||
// From (0,2) [12] -> down. Row 1 is [13, 14]. Column 2 is out of bounds for row 1.
|
||||
// Should snap to (1,1) [14]
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: jaggedMatrix, currentRow: 0, currentCol: 2, direction: 'down' })
|
||||
).toBe(14);
|
||||
});
|
||||
|
||||
it('should snap right when moving up to a shorter row', () => {
|
||||
// From (2,3) [18] -> up. Row 1 is [13, 14]. Column 3 is out of bounds for row 1.
|
||||
// Should snap to (1,1) [14]
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: jaggedMatrix, currentRow: 2, currentCol: 3, direction: 'up' })
|
||||
).toBe(14);
|
||||
});
|
||||
|
||||
it('should return undefined when snapping results in an invalid column (row is too short/empty)', () => {
|
||||
// From (0,0) [10] -> down. Row 4 is []. Column 0 is out of bounds.
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: jaggedMatrix, currentRow: 0, currentCol: 0, direction: 'down' })
|
||||
).toBe(13); // First down
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: jaggedMatrix, currentRow: 1, currentCol: 0, direction: 'down' })
|
||||
).toBe(15); // Second down
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: jaggedMatrix, currentRow: 2, currentCol: 0, direction: 'down' })
|
||||
).toBe(19); // Third down
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: jaggedMatrix, currentRow: 3, currentCol: 0, direction: 'down' })
|
||||
).toBeUndefined(); // Fourth down to empty row 4
|
||||
});
|
||||
|
||||
it('should not snap if target row is longer or same length', () => {
|
||||
// From (1,0) [13] -> down. Row 2 is [15,16,17,18]. Column 0 is valid.
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: jaggedMatrix, currentRow: 1, currentCol: 0, direction: 'down' })
|
||||
).toBe(15);
|
||||
// From (1,1) [14] -> down. Row 2 is [15,16,17,18]. Column 1 is valid.
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: jaggedMatrix, currentRow: 1, currentCol: 1, direction: 'down' })
|
||||
).toBe(16);
|
||||
});
|
||||
|
||||
it('should handle moving to a row with only one element when current column is greater', () => {
|
||||
// From (0,2) [12] -> down. Row 3 is [19]. Column 2 is out of bounds.
|
||||
// Should snap to (3,0) [19]
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: jaggedMatrix, currentRow: 0, currentCol: 2, direction: 'down' })
|
||||
).toBe(14); // Down to row 1
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: jaggedMatrix, currentRow: 1, currentCol: 1, direction: 'down' })
|
||||
).toBe(16); // Down to row 2
|
||||
expect(
|
||||
getNextMatrixItem({ matrix: jaggedMatrix, currentRow: 2, currentCol: 3, direction: 'down' })
|
||||
).toBe(19); // Down to row 3 (snaps from col 3 to col 0)
|
||||
});
|
||||
});
|
||||
|
||||
// --- Availability Tests ---
|
||||
|
||||
describe('Availability (isAvailable predicate)', () => {
|
||||
it('should return the item if it is available and no isAvailable is provided', () => {
|
||||
// Default behavior, all items are available
|
||||
expect(
|
||||
getNextMatrixItem({
|
||||
matrix: simpleMatrix,
|
||||
currentRow: 0,
|
||||
currentCol: 0,
|
||||
direction: 'right',
|
||||
})
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it('should return the item if it is available and isAvailable returns true', () => {
|
||||
// (0,0) is {id:1, available:true}. Move right to (0,1) {id:2, available:false}
|
||||
// This test is for the *first* item, not skipping.
|
||||
expect(
|
||||
getNextMatrixItem({
|
||||
matrix: matrixWithAvailability,
|
||||
currentRow: 0,
|
||||
currentCol: 0,
|
||||
direction: 'down',
|
||||
isAvailable: isTestItemAvailable,
|
||||
})
|
||||
).toEqual({ id: 4, available: true });
|
||||
});
|
||||
|
||||
// --- Horizontal Scan (right) ---
|
||||
it('should scan right to find the next available item', () => {
|
||||
// From (0,0) {id:1, true} -> right. Next is (0,1) {id:2, false}. Scan right to (0,2) {id:3, true}.
|
||||
const result = getNextMatrixItem({
|
||||
matrix: matrixWithAvailability,
|
||||
currentRow: 0,
|
||||
currentCol: 0,
|
||||
direction: 'right',
|
||||
isAvailable: isTestItemAvailable,
|
||||
});
|
||||
expect(result).toEqual({ id: 3, available: true });
|
||||
});
|
||||
|
||||
it('should scan right multiple times if needed', () => {
|
||||
// From (2,1) {id:7, true} -> right. Next is (2,2) {id:8, false}. Scan right to (2,3) {id:9, true}.
|
||||
const result = getNextMatrixItem({
|
||||
matrix: matrixWithAvailability,
|
||||
currentRow: 2,
|
||||
currentCol: 1,
|
||||
direction: 'right',
|
||||
isAvailable: isTestItemAvailable,
|
||||
});
|
||||
expect(result).toEqual({ id: 9, available: true });
|
||||
});
|
||||
|
||||
it('should return undefined if scanning right hits end of row and no available item is found', () => {
|
||||
// From (0,1) {id:2, false} -> right. Next is (0,2) {id:3, true}.
|
||||
// If we start from (0,1) and item 3 becomes unavailable, we should get undefined.
|
||||
const modifiedMatrix = JSON.parse(JSON.stringify(matrixWithAvailability)); // Deep copy
|
||||
modifiedMatrix[0][2].available = false; // Make id:3 unavailable
|
||||
const result = getNextMatrixItem({
|
||||
matrix: modifiedMatrix,
|
||||
currentRow: 0,
|
||||
currentCol: 1,
|
||||
direction: 'right',
|
||||
isAvailable: isTestItemAvailable,
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
// --- Horizontal Scan (left) ---
|
||||
it('should scan left to find the next available item', () => {
|
||||
// From (0,2) {id:3, true} -> left. Next is (0,1) {id:2, false}. Scan left to (0,0) {id:1, true}.
|
||||
const result = getNextMatrixItem({
|
||||
matrix: matrixWithAvailability,
|
||||
currentRow: 0,
|
||||
currentCol: 2,
|
||||
direction: 'left',
|
||||
isAvailable: isTestItemAvailable,
|
||||
});
|
||||
expect(result).toEqual({ id: 1, available: true });
|
||||
});
|
||||
|
||||
it('should scan left multiple times if needed', () => {
|
||||
// From (2,3) {id:9, true} -> left. Next is (2,2) {id:8, false}. Scan left to (2,1) {id:7, true}.
|
||||
const result = getNextMatrixItem({
|
||||
matrix: matrixWithAvailability,
|
||||
currentRow: 2,
|
||||
currentCol: 3,
|
||||
direction: 'left',
|
||||
isAvailable: isTestItemAvailable,
|
||||
});
|
||||
expect(result).toEqual({ id: 7, available: true });
|
||||
});
|
||||
|
||||
it('should return undefined if scanning left hits start of row and no available item is found', () => {
|
||||
// From (0,1) {id:2, false} -> left. Next is (0,0) {id:1, true}.
|
||||
// If we start from (0,1) and item 1 becomes unavailable, we should get undefined.
|
||||
const modifiedMatrix = JSON.parse(JSON.stringify(matrixWithAvailability)); // Deep copy
|
||||
modifiedMatrix[0][0].available = false; // Make id:1 unavailable
|
||||
const result = getNextMatrixItem({
|
||||
matrix: modifiedMatrix,
|
||||
currentRow: 0,
|
||||
currentCol: 1,
|
||||
direction: 'left',
|
||||
isAvailable: isTestItemAvailable,
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
// --- Vertical Scan (up/down - scans left) ---
|
||||
it('should scan left for available item after snapping down', () => {
|
||||
// From (0,2) {id:3, true} -> down. Target row 1. Snaps to (1,1) {id:5, false}.
|
||||
// Scan left: (1,0) {id:4, true}.
|
||||
const result = getNextMatrixItem({
|
||||
matrix: matrixWithAvailability,
|
||||
currentRow: 0,
|
||||
currentCol: 2,
|
||||
direction: 'down',
|
||||
isAvailable: isTestItemAvailable,
|
||||
});
|
||||
expect(result).toEqual({ id: 4, available: true });
|
||||
});
|
||||
|
||||
it('should scan left for available item after snapping up', () => {
|
||||
// From (2,3) {id:9, true} -> up. Target row 1. Snaps to (1,1) {id:5, false}.
|
||||
// Scan left: (1,0) {id:4, true}.
|
||||
const result = getNextMatrixItem({
|
||||
matrix: matrixWithAvailability,
|
||||
currentRow: 2,
|
||||
currentCol: 3,
|
||||
direction: 'up',
|
||||
isAvailable: isTestItemAvailable,
|
||||
});
|
||||
expect(result).toEqual({ id: 4, available: true });
|
||||
});
|
||||
|
||||
it('should return undefined if no available item found after vertical scan (all unavailable in target row)', () => {
|
||||
// From (5,0) {id:13, true} -> up. Target row 4 {id:11, false}, {id:12, false}.
|
||||
// Snaps to (4,0) {id:11, false}. Scan left to (4,0) {id:11, false}. No more to left.
|
||||
const result = getNextMatrixItem({
|
||||
matrix: matrixWithAvailability,
|
||||
currentRow: 5,
|
||||
currentCol: 0,
|
||||
direction: 'up',
|
||||
isAvailable: isTestItemAvailable,
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if no available item found after vertical scan (empty target row)', () => {
|
||||
// From (3,0) {id:10, false} -> down. Target row 4 is [].
|
||||
const result = getNextMatrixItem({
|
||||
matrix: matrixWithAvailability,
|
||||
currentRow: 3,
|
||||
currentCol: 0,
|
||||
direction: 'down',
|
||||
isAvailable: isTestItemAvailable,
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if the initial candidate for vertical move is out of bounds after clamping', () => {
|
||||
// From (0,0) which is {id:1, true}
|
||||
// Consider a matrix like:
|
||||
// [ [A] ]
|
||||
// [ [] ] <- row 1 is empty
|
||||
// [ [B] ]
|
||||
// From A down, it goes to row 1. Clamps to col -1. Returns undefined. Then next down to B.
|
||||
const customMatrix = [
|
||||
[{ id: 1, available: true }],
|
||||
[], // Empty row
|
||||
[{ id: 2, available: true }],
|
||||
];
|
||||
// Move from (0,0) down. Target row 1 is empty. nextCol becomes -1 after clamping.
|
||||
expect(
|
||||
getNextMatrixItem({
|
||||
matrix: customMatrix,
|
||||
currentRow: 0,
|
||||
currentCol: 0,
|
||||
direction: 'down',
|
||||
isAvailable: isTestItemAvailable,
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// --- Combined Scenarios ---
|
||||
|
||||
describe('Combined Scenarios', () => {
|
||||
it('should handle snapping and then scanning for availability correctly (down)', () => {
|
||||
// Matrix:
|
||||
// [ T, F, F ]
|
||||
// [ F, F ]
|
||||
// [ T, F, T, T ]
|
||||
// From (0,0) (T) down. Target row 1. Initial nextCol is 0. Item (1,0) is F.
|
||||
// Snaps to (1,0) (F). Scan left. No more left. Should return undefined.
|
||||
const matrix = [
|
||||
[
|
||||
{ id: 1, available: true },
|
||||
{ id: 2, available: false },
|
||||
{ id: 3, available: false },
|
||||
],
|
||||
[
|
||||
{ id: 4, available: false },
|
||||
{ id: 5, available: false },
|
||||
],
|
||||
[
|
||||
{ id: 6, available: true },
|
||||
{ id: 7, available: false },
|
||||
{ id: 8, available: true },
|
||||
{ id: 9, available: true },
|
||||
],
|
||||
];
|
||||
const result = getNextMatrixItem({
|
||||
matrix,
|
||||
currentRow: 0,
|
||||
currentCol: 0,
|
||||
direction: 'down',
|
||||
isAvailable: isTestItemAvailable,
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle snapping and then scanning for availability correctly (up)', () => {
|
||||
// Matrix:
|
||||
// [ F, F ]
|
||||
// [ T, F, F ]
|
||||
// From (1,0) (T) up. Target row 0. Initial nextCol is 0. Item (0,0) is F.
|
||||
// Snaps to (0,0) (F). Scan left. No more left. Should return undefined.
|
||||
const matrix = [
|
||||
[
|
||||
{ id: 1, available: false },
|
||||
{ id: 2, available: false },
|
||||
],
|
||||
[
|
||||
{ id: 3, available: true },
|
||||
{ id: 4, available: false },
|
||||
{ id: 5, available: false },
|
||||
],
|
||||
];
|
||||
const result = getNextMatrixItem({
|
||||
matrix,
|
||||
currentRow: 1,
|
||||
currentCol: 0,
|
||||
direction: 'up',
|
||||
isAvailable: isTestItemAvailable,
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -132,40 +132,61 @@ export function last<T>(arr: T[]): T | undefined {
|
|||
*/
|
||||
export type Direction = 'up' | 'down' | 'left' | 'right';
|
||||
|
||||
/**
|
||||
* Options for the getNextMatrixItem function.
|
||||
* @template T The type of items stored in the matrix.
|
||||
*/
|
||||
export interface GetNextMatrixItemOptions<T> {
|
||||
/** The matrix (an array of arrays) where rows can have varying lengths. */
|
||||
matrix: T[][];
|
||||
/** The 0-based index of the current row. */
|
||||
currentRow: number;
|
||||
/** The 0-based index of the current column. */
|
||||
currentCol: number;
|
||||
/** The direction to move ('up', 'down', 'left', 'right'). */
|
||||
direction: Direction;
|
||||
/**
|
||||
* An optional predicate function that determines if an item is "available".
|
||||
* If an item is not available, the function will attempt to find the next available candidate.
|
||||
* Defaults to always returning true (all items are available).
|
||||
* @param item The item to check.
|
||||
* @returns True if the item is available, false otherwise.
|
||||
*/
|
||||
isAvailable?: (item: T) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the next item in a matrix based on a current position and a direction.
|
||||
* This function is designed to work with "jagged" arrays (rows can have different lengths).
|
||||
*
|
||||
* Special behavior for 'up'/'down': If the target row is shorter than the current column,
|
||||
* the column index will snap to the rightmost valid column of the target row.
|
||||
*
|
||||
* If an `isAvailable` function is provided and the initially calculated next item is
|
||||
* not available, the function will attempt to find the next available candidate
|
||||
* within the target row:
|
||||
* - For 'up'/'down': It will scan left from the snapped column.
|
||||
* - For 'left': It will continue scanning left.
|
||||
* - For 'right': It will continue scanning right.
|
||||
*
|
||||
* @template T The type of items stored in the matrix.
|
||||
* @param matrix The matrix (an array of arrays) where rows can have varying lengths.
|
||||
* @param currentRow The 0-based index of the current row.
|
||||
* @param currentCol The 0-based index of the current column.
|
||||
* @param direction The direction to move ('up', 'down', 'left', 'right').
|
||||
* @returns The item at the next position, or `undefined` if the current position is invalid,
|
||||
* the next position is out of bounds, or the matrix itself is invalid.
|
||||
* @param options The options object containing matrix, current position, direction, and optional isAvailable predicate.
|
||||
* @returns The item at the next valid and available position, or `undefined` if no such item is found.
|
||||
*/
|
||||
export function getNextMatrixItem<T>(
|
||||
matrix: T[][],
|
||||
currentRow: number,
|
||||
currentCol: number,
|
||||
direction: Direction
|
||||
): T | undefined {
|
||||
export function getNextMatrixItem<T>(options: GetNextMatrixItemOptions<T>): T | undefined {
|
||||
const { matrix, currentRow, currentCol, direction, isAvailable = (_i) => true } = options;
|
||||
|
||||
// --- 1. Input Validation: Matrix and Current Position ---
|
||||
|
||||
// Ensure the matrix is a valid array and not empty
|
||||
if (!matrix || !Array.isArray(matrix) || matrix.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Validate the current row index
|
||||
if (currentRow < 0 || currentRow >= matrix.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Get the current row array to validate the current column
|
||||
const currentRowArray = matrix[currentRow];
|
||||
|
||||
// Ensure the current row is a valid array and validate current column index
|
||||
if (
|
||||
!currentRowArray ||
|
||||
!Array.isArray(currentRowArray) ||
|
||||
|
|
@ -178,7 +199,7 @@ export function getNextMatrixItem<T>(
|
|||
// --- 2. Calculate Tentative Next Coordinates ---
|
||||
|
||||
let nextRow = currentRow;
|
||||
let nextCol = currentCol; // Start with the same column
|
||||
let nextCol = currentCol;
|
||||
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
|
|
@ -188,45 +209,70 @@ export function getNextMatrixItem<T>(
|
|||
nextRow++;
|
||||
break;
|
||||
case 'left':
|
||||
nextCol--; // Column changes for horizontal movement
|
||||
nextCol--;
|
||||
break;
|
||||
case 'right':
|
||||
nextCol++; // Column changes for horizontal movement
|
||||
nextCol++;
|
||||
break;
|
||||
}
|
||||
|
||||
// --- 3. Validate and Adjust Next Coordinates Against Matrix Bounds ---
|
||||
|
||||
// Check if the calculated next row is within the matrix's vertical bounds
|
||||
if (nextRow < 0 || nextRow >= matrix.length) {
|
||||
return undefined; // Out of vertical bounds
|
||||
}
|
||||
|
||||
// Get the array for the target row. This is crucial for jagged arrays.
|
||||
const nextRowArray = matrix[nextRow];
|
||||
|
||||
// Ensure the target row is a valid array before checking its length
|
||||
if (!nextRowArray || !Array.isArray(nextRowArray)) {
|
||||
return undefined; // The row itself is malformed or non-existent
|
||||
}
|
||||
|
||||
// --- NEW LOGIC: Adjust nextCol for vertical movements if it's out of bounds ---
|
||||
if (direction === 'up' || direction === 'down') {
|
||||
// If the tentative nextCol is beyond the length of the target row,
|
||||
// clamp it to the last valid index of that row.
|
||||
// If nextRowArray.length is 0, then nextRowArray.length - 1 is -1.
|
||||
// Math.min(currentCol, -1) would result in -1, which will then correctly
|
||||
// be caught by the next `if (nextCol < 0)` check.
|
||||
// Clamp nextCol to the last valid index of the target row if it's too far right
|
||||
nextCol = Math.min(nextCol, nextRowArray.length - 1);
|
||||
}
|
||||
|
||||
// Check if the calculated next column is within the target row's horizontal bounds
|
||||
// This applies to ALL directions, including after potential clamping for up/down.
|
||||
// --- 4. Find the Next Available Item ---
|
||||
|
||||
// Initial check for bounds after clamping/calculation
|
||||
if (nextCol < 0 || nextCol >= nextRowArray.length) {
|
||||
return undefined; // Out of horizontal bounds for the specific nextRow
|
||||
return undefined; // No valid column to start searching from in the target row
|
||||
}
|
||||
|
||||
// --- 4. Return the Item ---
|
||||
// Loop to find the next available item
|
||||
let candidateCol = nextCol;
|
||||
|
||||
return nextRowArray[nextCol];
|
||||
if (direction === 'up' || direction === 'down') {
|
||||
// For vertical moves, try the calculated/clamped 'candidateCol', then scan left
|
||||
while (candidateCol >= 0) {
|
||||
const candidateItem = nextRowArray[candidateCol]!;
|
||||
if (isAvailable(candidateItem)) {
|
||||
return candidateItem;
|
||||
}
|
||||
candidateCol--; // Move left to find an available item
|
||||
}
|
||||
} else if (direction === 'left') {
|
||||
// For 'left' moves, keep scanning left from the calculated 'candidateCol'
|
||||
while (candidateCol >= 0) {
|
||||
const candidateItem = nextRowArray[candidateCol]!;
|
||||
if (isAvailable(candidateItem)) {
|
||||
return candidateItem;
|
||||
}
|
||||
candidateCol--; // Continue moving left
|
||||
}
|
||||
} else {
|
||||
// direction === 'right'
|
||||
// For 'right' moves, keep scanning right from the calculated 'candidateCol'
|
||||
while (candidateCol < nextRowArray.length) {
|
||||
const candidateItem = nextRowArray[candidateCol]!;
|
||||
if (isAvailable(candidateItem)) {
|
||||
return candidateItem;
|
||||
}
|
||||
candidateCol++; // Continue moving right
|
||||
}
|
||||
}
|
||||
|
||||
// If no available item was found in the search path
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ html.dark .shiki span {
|
|||
}
|
||||
|
||||
.prose {
|
||||
@apply text-foreground max-w-[65ch];
|
||||
@apply text-foreground;
|
||||
|
||||
& h1:not(.not-prose h1) {
|
||||
@apply scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl;
|
||||
|
|
|
|||
|
|
@ -558,7 +558,7 @@
|
|||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 pr-2 sm:flex-row sm:items-center">
|
||||
<ModelPicker />
|
||||
<ModelPicker onlyImageModels={selectedImages.length > 0} />
|
||||
{#if currentModelSupportsImages}
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -40,7 +40,12 @@
|
|||
</script>
|
||||
|
||||
{#if message.role !== 'system' && !(message.role === 'assistant' && message.content.length === 0)}
|
||||
<div class={cn('group flex max-w-[80%] flex-col gap-1', { 'self-end': message.role === 'user' })}>
|
||||
<div
|
||||
class={cn('group flex flex-col gap-1', {
|
||||
'self-end': message.role === 'user',
|
||||
'max-w-[80%]': message.role === 'user',
|
||||
})}
|
||||
>
|
||||
{#if message.images && message.images.length > 0}
|
||||
<div class="mb-2 flex flex-wrap gap-2">
|
||||
{#each message.images as image (image.storage_id)}
|
||||
|
|
|
|||
|
|
@ -35,9 +35,11 @@
|
|||
|
||||
type Props = {
|
||||
class?: string;
|
||||
/* When images are attached, we should not select models that don't support images */
|
||||
onlyImageModels?: boolean;
|
||||
};
|
||||
|
||||
let { class: className }: Props = $props();
|
||||
let { class: className, onlyImageModels }: Props = $props();
|
||||
|
||||
const enabledModelsQuery = useCachedQuery(api.user_enabled_models.get_enabled, {
|
||||
session_token: session.current?.session.token ?? '',
|
||||
|
|
@ -280,74 +282,66 @@
|
|||
{@const openRouterModel = modelsState
|
||||
.from(Provider.OpenRouter)
|
||||
.find((m) => m.id === model.model_id)}
|
||||
{@const disabled =
|
||||
onlyImageModels && openRouterModel && !supportsImages(openRouterModel)}
|
||||
|
||||
{#if isMobile.current}
|
||||
<div
|
||||
{...gridCommand.getItem(model.model_id)}
|
||||
class={cn(
|
||||
'border-border flex h-10 items-center justify-between rounded-lg border p-2',
|
||||
'relative scroll-m-2 select-none',
|
||||
'data-highlighted:bg-accent/50 data-highlighted:text-accent-foreground',
|
||||
isSelected && 'border-reflect border-none'
|
||||
)}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if getModelIcon(model.model_id)}
|
||||
{@const ModelIcon = getModelIcon(model.model_id)}
|
||||
<ModelIcon class="size-6 shrink-0" />
|
||||
{/if}
|
||||
<p class="font-fake-proxima text-center leading-tight font-bold">
|
||||
{formatted.full}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if openRouterModel && supportsImages(openRouterModel)}
|
||||
<Tooltip>
|
||||
{#snippet trigger(tooltip)}
|
||||
<div class="" {...tooltip.trigger}>
|
||||
<EyeIcon class="size-3" />
|
||||
</div>
|
||||
{/snippet}
|
||||
Supports image anaylsis
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
{...gridCommand.getItem(model.model_id)}
|
||||
class={cn(
|
||||
'border-border flex h-40 w-32 scroll-m-2 flex-col items-center justify-center rounded-lg border p-2',
|
||||
'relative select-none',
|
||||
'data-highlighted:bg-accent/50 data-highlighted:text-accent-foreground',
|
||||
isSelected && 'border-reflect border-none'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
{...gridCommand.getItem(model.model_id, {
|
||||
disabled,
|
||||
})}
|
||||
class={cn(
|
||||
'border-border flex rounded-lg border p-2',
|
||||
'relative scroll-m-2 select-none',
|
||||
'data-highlighted:bg-accent/50 data-highlighted:text-accent-foreground',
|
||||
isSelected && 'border-reflect border-none',
|
||||
isMobile.current
|
||||
? 'h-10 items-center justify-between'
|
||||
: 'h-40 w-32 flex-col items-center justify-center',
|
||||
disabled && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{onlyImageModels}
|
||||
{disabled}
|
||||
<div class={cn('flex items-center', isMobile.current ? 'gap-2' : 'flex-col')}>
|
||||
{#if getModelIcon(model.model_id)}
|
||||
{@const ModelIcon = getModelIcon(model.model_id)}
|
||||
<ModelIcon class="size-6 shrink-0" />
|
||||
{/if}
|
||||
<p class="font-fake-proxima mt-2 text-center leading-tight font-bold">
|
||||
{formatted.primary}
|
||||
</p>
|
||||
<p class="mt-0 text-center text-xs leading-tight font-medium">
|
||||
{formatted.secondary}
|
||||
|
||||
<p
|
||||
class={cn(
|
||||
'font-fake-proxima text-center leading-tight font-bold',
|
||||
!isMobile.current && 'mt-2'
|
||||
)}
|
||||
>
|
||||
{isMobile.current ? formatted.full : formatted.primary}
|
||||
</p>
|
||||
|
||||
{#if openRouterModel && supportsImages(openRouterModel)}
|
||||
<Tooltip>
|
||||
{#snippet trigger(tooltip)}
|
||||
<div
|
||||
class="abs-x-center text-muted-foreground absolute bottom-3 flex items-center gap-1 text-xs"
|
||||
{...tooltip.trigger}
|
||||
>
|
||||
<EyeIcon class="size-3" />
|
||||
</div>
|
||||
{/snippet}
|
||||
Supports image anaylsis
|
||||
</Tooltip>
|
||||
{#if !isMobile.current}
|
||||
<p class="mt-0 text-center text-xs leading-tight font-medium">
|
||||
{formatted.secondary}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if openRouterModel && supportsImages(openRouterModel)}
|
||||
<Tooltip>
|
||||
{#snippet trigger(tooltip)}
|
||||
<div
|
||||
class={cn(
|
||||
isMobile.current
|
||||
? ''
|
||||
: 'abs-x-center text-muted-foreground absolute bottom-3 flex items-center gap-1 text-xs'
|
||||
)}
|
||||
{...tooltip.trigger}
|
||||
>
|
||||
<EyeIcon class="size-3" />
|
||||
</div>
|
||||
{/snippet}
|
||||
Supports image anaylsis
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue