Skip to content

Commit a9bf467

Browse files
add palette support to color from source/uri helpers
1 parent 2bacc1f commit a9bf467

4 files changed

Lines changed: 115 additions & 3 deletions

File tree

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@molstar/molstar-components",
3-
"version": "0.6.0-experimental.20",
3+
"version": "0.6.0-experimental.21",
44
"license": "MIT",
55
"exports": {
66
".": "./src/mod.ts",
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { ChevronRightIcon } from 'lucide-react';
5+
import { Input } from '../base/input.tsx';
6+
import { Label } from '../base/label.tsx';
7+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../base/select.tsx';
8+
import { cn } from '../lib/utils.ts';
9+
10+
export type PaletteKind = 'categorical' | 'discrete' | 'continuous';
11+
12+
export interface PaletteValue {
13+
kind: PaletteKind;
14+
colors: string;
15+
missing_color: string;
16+
}
17+
18+
interface PaletteSectionProps {
19+
value: PaletteValue | null;
20+
onChange: (value: PaletteValue | null) => void;
21+
}
22+
23+
const DEFAULT_PALETTE: PaletteValue = { kind: 'categorical', colors: '', missing_color: '' };
24+
25+
export function paletteFromParams(params: Record<string, unknown>): PaletteValue | null {
26+
const p = params.palette as Record<string, unknown> | null | undefined;
27+
if (!p || typeof p !== 'object') return null;
28+
return {
29+
kind: (p.kind as PaletteKind) ?? 'categorical',
30+
colors: typeof p.colors === 'string' ? p.colors : '',
31+
missing_color: typeof p.missing_color === 'string' ? p.missing_color : '',
32+
};
33+
}
34+
35+
export function paletteToParams(value: PaletteValue | null): Record<string, unknown> | undefined {
36+
if (!value) return undefined;
37+
const result: Record<string, unknown> = { kind: value.kind };
38+
if (value.colors) result.colors = value.colors;
39+
if (value.missing_color && value.kind === 'categorical') result.missing_color = value.missing_color;
40+
return result;
41+
}
42+
43+
export function PaletteSection({ value, onChange }: PaletteSectionProps) {
44+
const [open, setOpen] = useState(false);
45+
46+
const palette = value ?? DEFAULT_PALETTE;
47+
48+
const handleEnable = () => {
49+
if (!value) onChange(DEFAULT_PALETTE);
50+
setOpen(true);
51+
};
52+
53+
const update = (patch: Partial<PaletteValue>) => {
54+
onChange({ ...palette, ...patch });
55+
};
56+
57+
return (
58+
<div className='flex flex-col gap-1'>
59+
<button type='button' className='flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground w-fit' onClick={() => { if (!value) handleEnable(); else setOpen(o => !o); }}>
60+
<ChevronRightIcon className={cn('size-3 transition-transform', open && value && 'rotate-90')} />
61+
Palette
62+
{value && !open && <span className='text-foreground'>({value.kind})</span>}
63+
</button>
64+
{open && (
65+
<div className='flex flex-col gap-2 pl-4 pt-1'>
66+
<div className='flex gap-2 items-end'>
67+
<div className='flex flex-col gap-1'>
68+
<Label className='text-xs'>Kind</Label>
69+
<Select value={palette.kind} onValueChange={(v) => update({ kind: v as PaletteKind })}>
70+
<SelectTrigger size='sm' className='w-32'><SelectValue /></SelectTrigger>
71+
<SelectContent>
72+
<SelectItem value='categorical'>categorical</SelectItem>
73+
<SelectItem value='discrete'>discrete</SelectItem>
74+
<SelectItem value='continuous'>continuous</SelectItem>
75+
</SelectContent>
76+
</Select>
77+
</div>
78+
<div className='flex flex-col gap-1 flex-1'>
79+
<Label className='text-xs'>Colors <span className='text-muted-foreground font-normal'>(name or leave blank)</span></Label>
80+
<Input className='h-7 text-xs font-mono' placeholder='ResidueName, Viridis, …' value={palette.colors} onChange={(e) => update({ colors: e.target.value })} />
81+
</div>
82+
</div>
83+
{palette.kind === 'categorical' && (
84+
<div className='flex flex-col gap-1 w-36'>
85+
<Label className='text-xs'>Missing color <span className='text-muted-foreground font-normal'>(optional)</span></Label>
86+
<Input className='h-7 text-xs font-mono' placeholder='yellow' value={palette.missing_color} onChange={(e) => update({ missing_color: e.target.value })} />
87+
</div>
88+
)}
89+
<button type='button' className='text-xs text-muted-foreground hover:text-destructive w-fit' onClick={() => { onChange(null); setOpen(false); }}>
90+
Remove palette
91+
</button>
92+
</div>
93+
)}
94+
</div>
95+
);
96+
}

src/state-builder-ui/helpers/ColorFromSourceHelper.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Label } from '../base/label.tsx';
77
import type { UINode } from '../../state-builder/index.ts';
88
import { NodeHelperBase } from './NodeHelperBase.tsx';
99
import { FieldRemappingSection, type RemapEntry } from '../components/FieldRemappingSection.tsx';
10+
import { PaletteSection, paletteFromParams, paletteToParams, type PaletteValue } from '../components/PaletteSection.tsx';
1011

1112
interface ColorFromSourceHelperProps {
1213
node: UINode;
@@ -18,12 +19,13 @@ interface ColorFromSourceHelperProps {
1819
}
1920

2021
function initFromNode(node: UINode) {
21-
const p = node.params;
22+
const p = node.params as Record<string, unknown>;
2223
return {
2324
categoryName: (p.category_name as string) ?? '',
2425
fieldName: (p.field_name as string) ?? '',
2526
blockIndex: p.block_index as number | undefined,
2627
fieldRemapping: Object.entries((p.field_remapping as Record<string, string>) ?? {}).map(([key, value]) => ({ key, value })),
28+
palette: paletteFromParams(p),
2729
};
2830
}
2931

@@ -33,13 +35,15 @@ export function ColorFromSourceHelper({ node, onUpdate, open, onOpenChange, trig
3335
const [fieldName, setFieldName] = useState(init.fieldName);
3436
const [blockIndex, setBlockIndex] = useState<number | undefined>(init.blockIndex);
3537
const [fieldRemapping, setFieldRemapping] = useState<RemapEntry[]>(init.fieldRemapping);
38+
const [palette, setPalette] = useState<PaletteValue | null>(init.palette);
3639

3740
const handleDialogOpen = () => {
3841
const s = initFromNode(node);
3942
setCategoryName(s.categoryName);
4043
setFieldName(s.fieldName);
4144
setBlockIndex(s.blockIndex);
4245
setFieldRemapping(s.fieldRemapping);
46+
setPalette(s.palette);
4347
};
4448

4549
const handleApply = (ref: string) => {
@@ -49,6 +53,9 @@ export function ColorFromSourceHelper({ node, onUpdate, open, onOpenChange, trig
4953
const remapping = Object.fromEntries(fieldRemapping.filter(e => e.key).map(e => [e.key, e.value]));
5054
if (Object.keys(remapping).length > 0) params.field_remapping = remapping;
5155
else delete params.field_remapping;
56+
const paletteParams = paletteToParams(palette);
57+
if (paletteParams) params.palette = paletteParams;
58+
else delete params.palette;
5259
onUpdate({ params, ...(ref ? { ref } : {}) });
5360
};
5461

@@ -82,6 +89,7 @@ export function ColorFromSourceHelper({ node, onUpdate, open, onOpenChange, trig
8289
<Label className='text-xs'>Block index</Label>
8390
<Input className='h-7 text-xs font-mono' type='number' min='0' placeholder='optional' value={blockIndex ?? ''} onChange={(e) => setBlockIndex(e.target.value === '' ? undefined : parseInt(e.target.value))} />
8491
</div>
92+
<PaletteSection value={palette} onChange={setPalette} />
8593
<FieldRemappingSection entries={fieldRemapping} onChange={setFieldRemapping} />
8694
</div>
8795
),

src/state-builder-ui/helpers/ColorFromUriHelper.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Label } from '../base/label.tsx';
77
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../base/select.tsx';
88
import type { UINode } from '../../state-builder/index.ts';
99
import { NodeHelperBase } from './NodeHelperBase.tsx';
10+
import { PaletteSection, paletteFromParams, paletteToParams, type PaletteValue } from '../components/PaletteSection.tsx';
1011

1112
interface ColorFromUriHelperProps {
1213
node: UINode;
@@ -25,13 +26,14 @@ const SCHEMAS = [
2526
] as const;
2627

2728
function initFromNode(node: UINode) {
28-
const p = node.params;
29+
const p = node.params as Record<string, unknown>;
2930
return {
3031
uri: (p.uri as string) ?? '',
3132
format: (p.format as string) ?? 'cif',
3233
schema: (p.schema as string) ?? 'all_atomic',
3334
categoryName: (p.category_name as string) ?? '',
3435
fieldName: (p.field_name as string) ?? '',
36+
palette: paletteFromParams(p),
3537
};
3638
}
3739

@@ -42,6 +44,7 @@ export function ColorFromUriHelper({ node, onUpdate, open, onOpenChange, trigger
4244
const [schema, setSchema] = useState(init.schema);
4345
const [categoryName, setCategoryName] = useState(init.categoryName);
4446
const [fieldName, setFieldName] = useState(init.fieldName);
47+
const [palette, setPalette] = useState<PaletteValue | null>(init.palette);
4548

4649
const handleDialogOpen = () => {
4750
const s = initFromNode(node);
@@ -50,6 +53,7 @@ export function ColorFromUriHelper({ node, onUpdate, open, onOpenChange, trigger
5053
setSchema(s.schema);
5154
setCategoryName(s.categoryName);
5255
setFieldName(s.fieldName);
56+
setPalette(s.palette);
5357
};
5458

5559
const handleApply = (ref: string) => {
@@ -61,6 +65,9 @@ export function ColorFromUriHelper({ node, onUpdate, open, onOpenChange, trigger
6165
category_name: categoryName || undefined,
6266
field_name: fieldName || undefined,
6367
};
68+
const paletteParams = paletteToParams(palette);
69+
if (paletteParams) params.palette = paletteParams;
70+
else delete params.palette;
6471
onUpdate({ params, ...(ref ? { ref } : {}) });
6572
};
6673

@@ -112,6 +119,7 @@ export function ColorFromUriHelper({ node, onUpdate, open, onOpenChange, trigger
112119
<Input className='h-7 text-xs font-mono' placeholder='color' value={fieldName} onChange={(e) => setFieldName(e.target.value)} />
113120
</div>
114121
</div>
122+
<PaletteSection value={palette} onChange={setPalette} />
115123
</div>
116124
),
117125
}]}

0 commit comments

Comments
 (0)