feat: add Image widget + goView Image converter
This commit is contained in:
parent
8ba32270c7
commit
f78d2a7fe7
@ -495,9 +495,27 @@ function NodeView(props: {
|
||||
{node.props.text}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div>{node.type}</div>
|
||||
)}
|
||||
) : node.type === 'image' ? (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
borderRadius: `${node.props.borderRadius ?? 0}px`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={node.props.src}
|
||||
alt=""
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: node.props.fit ?? 'contain',
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{props.selected && <ResizeHandles onPointerDown={props.onResizePointerDown} />}
|
||||
</div>
|
||||
|
||||
@ -79,6 +79,7 @@ export function EditorApp() {
|
||||
<Inspector
|
||||
selected={state.doc.screen.nodes.find((n) => n.id === state.selection.ids[0])}
|
||||
onUpdateTextProps={(id, props) => dispatch({ type: 'updateTextProps', id, props })}
|
||||
onUpdateImageProps={(id, props) => dispatch({ type: 'updateImageProps', id, props })}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { Input, InputNumber, Select, Space, Typography } from 'antd';
|
||||
import type { TextWidgetNode, WidgetNode } from '@astralview/sdk';
|
||||
import type { WidgetNode, TextWidgetNode } from '@astralview/sdk';
|
||||
|
||||
type ImageWidgetNode = Extract<WidgetNode, { type: 'image' }>;
|
||||
|
||||
export function Inspector(props: {
|
||||
selected?: WidgetNode;
|
||||
onUpdateTextProps: (id: string, patch: Partial<TextWidgetNode['props']>) => void;
|
||||
onUpdateImageProps: (id: string, patch: Partial<ImageWidgetNode['props']>) => void;
|
||||
}) {
|
||||
const node = props.selected;
|
||||
|
||||
@ -11,8 +14,50 @@ export function Inspector(props: {
|
||||
return <Typography.Paragraph style={{ color: '#666' }}>No selection.</Typography.Paragraph>;
|
||||
}
|
||||
|
||||
if (node.type === 'image') {
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={5} style={{ marginTop: 0 }}>
|
||||
Image
|
||||
</Typography.Title>
|
||||
|
||||
<Typography.Text type="secondary">Source</Typography.Text>
|
||||
<Input
|
||||
value={node.props.src}
|
||||
onChange={(e) => props.onUpdateImageProps(node.id, { src: e.target.value })}
|
||||
placeholder="https://..."
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
|
||||
<Typography.Text type="secondary">Fit</Typography.Text>
|
||||
<Select
|
||||
value={node.props.fit ?? 'contain'}
|
||||
onChange={(v) => props.onUpdateImageProps(node.id, { fit: v })}
|
||||
style={{ width: '100%', marginBottom: 12 }}
|
||||
options={[
|
||||
{ value: 'contain', label: 'contain' },
|
||||
{ value: 'cover', label: 'cover' },
|
||||
{ value: 'fill', label: 'fill' },
|
||||
{ value: 'none', label: 'none' },
|
||||
{ value: 'scale-down', label: 'scale-down' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Typography.Text type="secondary">Border radius</Typography.Text>
|
||||
<InputNumber
|
||||
value={node.props.borderRadius ?? 0}
|
||||
min={0}
|
||||
onChange={(v) => props.onUpdateImageProps(node.id, { borderRadius: typeof v === 'number' ? v : 0 })}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If more widget types are added, handle them above.
|
||||
// For now, we only support text/image.
|
||||
if (node.type !== 'text') {
|
||||
return <Typography.Paragraph style={{ color: '#666' }}>Unsupported widget type: {node.type}</Typography.Paragraph>;
|
||||
return <Typography.Paragraph style={{ color: '#666' }}>Unsupported widget type.</Typography.Paragraph>;
|
||||
}
|
||||
|
||||
const fontWeight = node.props.fontWeight ?? 400;
|
||||
@ -144,7 +189,7 @@ export function Inspector(props: {
|
||||
style={{ width: '100%', marginBottom: 12 }}
|
||||
options={[
|
||||
{ value: 'horizontal-tb', label: 'Horizontal' },
|
||||
{ value: 'vertical-rl', label: 'Vertical' }
|
||||
{ value: 'vertical-rl', label: 'Vertical' },
|
||||
]}
|
||||
/>
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
createEmptyScreen,
|
||||
migrateScreen,
|
||||
type Rect,
|
||||
type ImageWidgetNode,
|
||||
type Screen,
|
||||
type TextWidgetNode,
|
||||
type WidgetNode,
|
||||
@ -39,7 +40,8 @@ export type EditorAction =
|
||||
| { type: 'deleteSelected' }
|
||||
| { type: 'nudgeSelected'; dx: number; dy: number }
|
||||
| { type: 'duplicateSelected' }
|
||||
| { type: 'updateTextProps'; id: string; props: Partial<TextWidgetNode['props']> };
|
||||
| { type: 'updateTextProps'; id: string; props: Partial<TextWidgetNode['props']> }
|
||||
| { type: 'updateImageProps'; id: string; props: Partial<ImageWidgetNode['props']> };
|
||||
|
||||
interface DragSession {
|
||||
kind: 'move' | 'resize';
|
||||
@ -198,6 +200,23 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
|
||||
};
|
||||
}
|
||||
|
||||
case 'updateImageProps': {
|
||||
const node = state.doc.screen.nodes.find((n) => n.id === action.id);
|
||||
if (!node || node.type !== 'image') return state;
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: {
|
||||
screen: {
|
||||
...state.doc.screen,
|
||||
nodes: state.doc.screen.nodes.map((n) => {
|
||||
if (n.id !== action.id || n.type !== 'image') return n;
|
||||
return { ...n, props: { ...n.props, ...action.props } };
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'deleteSelected': {
|
||||
if (!state.selection.ids.length) return state;
|
||||
const ids = new Set(state.selection.ids);
|
||||
|
||||
@ -4,4 +4,19 @@ import react from '@vitejs/plugin-react'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// Keep bundle sizes manageable (and make caching better) by splitting common deps.
|
||||
manualChunks(id) {
|
||||
if (!id.includes('node_modules')) return
|
||||
|
||||
// Keep this match tight: many packages contain "react" in their name.
|
||||
if (/node_modules[\\/](react|react-dom|scheduler)[\\/]/.test(id)) return 'react'
|
||||
|
||||
return 'vendor'
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user