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}
|
{node.props.text}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : node.type === 'image' ? (
|
||||||
<div>{node.type}</div>
|
<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} />}
|
{props.selected && <ResizeHandles onPointerDown={props.onResizePointerDown} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -79,6 +79,7 @@ export function EditorApp() {
|
|||||||
<Inspector
|
<Inspector
|
||||||
selected={state.doc.screen.nodes.find((n) => n.id === state.selection.ids[0])}
|
selected={state.doc.screen.nodes.find((n) => n.id === state.selection.ids[0])}
|
||||||
onUpdateTextProps={(id, props) => dispatch({ type: 'updateTextProps', id, props })}
|
onUpdateTextProps={(id, props) => dispatch({ type: 'updateTextProps', id, props })}
|
||||||
|
onUpdateImageProps={(id, props) => dispatch({ type: 'updateImageProps', id, props })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { Input, InputNumber, Select, Space, Typography } from 'antd';
|
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: {
|
export function Inspector(props: {
|
||||||
selected?: WidgetNode;
|
selected?: WidgetNode;
|
||||||
onUpdateTextProps: (id: string, patch: Partial<TextWidgetNode['props']>) => void;
|
onUpdateTextProps: (id: string, patch: Partial<TextWidgetNode['props']>) => void;
|
||||||
|
onUpdateImageProps: (id: string, patch: Partial<ImageWidgetNode['props']>) => void;
|
||||||
}) {
|
}) {
|
||||||
const node = props.selected;
|
const node = props.selected;
|
||||||
|
|
||||||
@ -11,8 +14,50 @@ export function Inspector(props: {
|
|||||||
return <Typography.Paragraph style={{ color: '#666' }}>No selection.</Typography.Paragraph>;
|
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') {
|
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;
|
const fontWeight = node.props.fontWeight ?? 400;
|
||||||
@ -144,7 +189,7 @@ export function Inspector(props: {
|
|||||||
style={{ width: '100%', marginBottom: 12 }}
|
style={{ width: '100%', marginBottom: 12 }}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'horizontal-tb', label: 'Horizontal' },
|
{ value: 'horizontal-tb', label: 'Horizontal' },
|
||||||
{ value: 'vertical-rl', label: 'Vertical' }
|
{ value: 'vertical-rl', label: 'Vertical' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
createEmptyScreen,
|
createEmptyScreen,
|
||||||
migrateScreen,
|
migrateScreen,
|
||||||
type Rect,
|
type Rect,
|
||||||
|
type ImageWidgetNode,
|
||||||
type Screen,
|
type Screen,
|
||||||
type TextWidgetNode,
|
type TextWidgetNode,
|
||||||
type WidgetNode,
|
type WidgetNode,
|
||||||
@ -39,7 +40,8 @@ export type EditorAction =
|
|||||||
| { type: 'deleteSelected' }
|
| { type: 'deleteSelected' }
|
||||||
| { type: 'nudgeSelected'; dx: number; dy: number }
|
| { type: 'nudgeSelected'; dx: number; dy: number }
|
||||||
| { type: 'duplicateSelected' }
|
| { 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 {
|
interface DragSession {
|
||||||
kind: 'move' | 'resize';
|
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': {
|
case 'deleteSelected': {
|
||||||
if (!state.selection.ids.length) return state;
|
if (!state.selection.ids.length) return state;
|
||||||
const ids = new Set(state.selection.ids);
|
const ids = new Set(state.selection.ids);
|
||||||
|
|||||||
@ -4,4 +4,19 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
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