Functional but settings modals position and size badly
This commit is contained in:
+11
-51
@@ -26,6 +26,7 @@ body {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Styles for the close button */
|
||||
@@ -82,18 +83,12 @@ body {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
/* Ensure container contents don't overlap the buttons */
|
||||
.container > *:not(.close-button):not(.settings-button) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Styles for ClockWidget time display */
|
||||
.clock-time {
|
||||
color: #ffffff;
|
||||
font-size: 48px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* SearchWidget form */
|
||||
@@ -107,51 +102,11 @@ body {
|
||||
padding: 5px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 1);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Widget settings modal */
|
||||
.widget-settings-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.widget-modal-content {
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
max-width: 300px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.widget-modal-content label {
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.widget-modal-content select,
|
||||
.widget-modal-content input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
margin-top: 5px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.widget-modal-content button {
|
||||
.accept-button {
|
||||
margin-top: 10px;
|
||||
padding: 5px 10px;
|
||||
background: #007bff;
|
||||
@@ -161,7 +116,7 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.widget-modal-content button:hover {
|
||||
.accept-button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
@@ -204,7 +159,6 @@ body {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -305,3 +259,9 @@ body {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.timezone-display{
|
||||
font-size: small;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
@@ -27,7 +27,10 @@ export const ClockWidgetComponent: React.FC<ClockWidgetProps> = ({ widget, updat
|
||||
return new Intl.DateTimeFormat('en-US', options).format(time);
|
||||
};
|
||||
|
||||
return <div className="clock-time">{formatTime()}</div>;
|
||||
return <>
|
||||
<div className="clock-time">{formatTime()}</div>
|
||||
<div className="timezone-display">{widget.timezone}</div>
|
||||
</>;
|
||||
};
|
||||
|
||||
export const ClockWidgetSettings = ({ widget, updateWidget }: ClockWidgetProps) => {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import React, { MouseEvent } from "react"
|
||||
|
||||
type ButtonProps = {
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>
|
||||
}
|
||||
|
||||
export function CloseButton({ onClick }: ButtonProps) {
|
||||
return (
|
||||
<button className="close-button" onClick={onClick}>×</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ReactElement, ReactNode, useRef, useState } from "react";
|
||||
|
||||
interface ModalOverlayProps {
|
||||
visible: boolean;
|
||||
setVisible: (b: boolean) => void;
|
||||
children: ReactElement | ReactElement[];
|
||||
}
|
||||
|
||||
export function ModalOverlay({visible, setVisible, children}: ModalOverlayProps) {
|
||||
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return visible && <div className="settings-modal" ref={modalRef}>
|
||||
<div className="modal-content">
|
||||
{children}
|
||||
<button className="close-modal" onClick={() => setVisible(false)}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { showGridState } from '../state/showGrid.state';
|
||||
import { WidgetData } from '../types';
|
||||
import { loadableBackgroundImageState } from '../state/backgroundImage.state';
|
||||
import { settingsService } from '../services/settingsService';
|
||||
import { ModalOverlay } from './ModalOverlay';
|
||||
|
||||
const GRID_COLS = 36;
|
||||
const GRID_ROWS = 18;
|
||||
@@ -41,14 +42,21 @@ export const NewTab: React.FC = () => {
|
||||
);
|
||||
setWidgets(updatedWidgets);
|
||||
settingsService.saveSetting('widgets', updatedWidgets)
|
||||
console.log('Updated widget:', { id, updates })
|
||||
};
|
||||
|
||||
const addWidget = (data: WidgetData) => {
|
||||
const updatedWidgets = [...widgets, data]
|
||||
setWidgets([...widgets, data])
|
||||
settingsService.saveSetting('widgets', updatedWidgets)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalOverlay />
|
||||
<SettingsPanel
|
||||
backgroundImage={backgroundImage}
|
||||
setBackgroundImage={setBackgroundImage}
|
||||
addWidget={addWidget}
|
||||
/>
|
||||
{showGrid && (
|
||||
<div className="grid-overlay">
|
||||
|
||||
@@ -1,76 +1,70 @@
|
||||
import React, { useState } from 'react';
|
||||
import { CogwheelIcon } from './CogwheelIcon';
|
||||
import { useAtom } from 'jotai';
|
||||
import { draggableState } from '../state/draggable.state';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { editState } from '../state/edit.state';
|
||||
import { showGridState } from '../state/showGrid.state';
|
||||
import { settingsService } from '../services/settingsService';
|
||||
import { intitialWidgetData } from '../defaults';
|
||||
import { WidgetData, WidgetTypes } from '../types';
|
||||
import { modalContent, modalVisible } from '../state/modalOverlay.state';
|
||||
import { ModalOverlay } from './ModalOverlay';
|
||||
|
||||
interface SettingsPanelProps {
|
||||
backgroundImage: string,
|
||||
setBackgroundImage: (url: string) => void
|
||||
setBackgroundImage: (url: string) => void,
|
||||
addWidget: (widget: WidgetData) => void,
|
||||
}
|
||||
|
||||
export const SettingsPanel: React.FC<SettingsPanelProps> = ({ backgroundImage, setBackgroundImage }) => {
|
||||
//const [backgroundUrl, setBackgroundUrl] = useAtom(backgroundImageState);
|
||||
const [dragEnabled, setDragEnabled] = useAtom(draggableState);
|
||||
export const SettingsPanel: React.FC<SettingsPanelProps> = ({ backgroundImage, setBackgroundImage, addWidget }) => {
|
||||
const [editEnabled, setEditEnabled] = useAtom(editState);
|
||||
const [showGrid, setShowGrid] = useAtom(showGridState);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isModalShowing, setModalShowing] = useState(false);
|
||||
|
||||
const handleSave = () => {
|
||||
//settingsService.saveSetting('backgroundUrl', backgroundUrl)
|
||||
settingsService.saveSetting('dragEnabled', dragEnabled)
|
||||
settingsService.saveSetting('showGrid', showGrid)
|
||||
|
||||
setIsOpen(false);
|
||||
const createWidget = (type: WidgetTypes) => {
|
||||
const newWidgetData = intitialWidgetData(type);
|
||||
addWidget(newWidgetData)
|
||||
};
|
||||
|
||||
const addWidget = (type: string) => {
|
||||
//const newWidget = createBaseWidget(type, Math.floor(gridCols / 2), Math.floor(gridRows / 2), dragEnabled);
|
||||
//saveSettings({ widgets: [...settings.widgets, newWidget] });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button className="settings-toggle" onClick={() => setIsOpen(!isOpen)}>
|
||||
<button className="settings-toggle" onClick={() => setModalShowing(true)}>
|
||||
<CogwheelIcon size={24} />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="settings-modal">
|
||||
<div className="modal-content">
|
||||
<h2>Settings</h2>
|
||||
<label>
|
||||
Background URL:
|
||||
<input
|
||||
type="text"
|
||||
value={backgroundImage}
|
||||
onChange={(e) => setBackgroundImage(e.target.value)}
|
||||
placeholder="Enter image URL"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Drag Enabled:
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={dragEnabled}
|
||||
onChange={(e) => setDragEnabled(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Show Grid:
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showGrid}
|
||||
onChange={(e) => setShowGrid(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
<div className="widget-buttons">
|
||||
<button onClick={() => addWidget('clock')}>Add Clock</button>
|
||||
<button onClick={() => addWidget('search')}>Add Search</button>
|
||||
</div>
|
||||
<button className="close-modal" onClick={handleSave}>Save & Close</button>
|
||||
</div>
|
||||
<ModalOverlay
|
||||
setVisible={setModalShowing}
|
||||
visible={isModalShowing}
|
||||
>
|
||||
<h2>Settings</h2>
|
||||
<label>
|
||||
Background URL:
|
||||
<input
|
||||
type="text"
|
||||
value={backgroundImage}
|
||||
onChange={(e) => setBackgroundImage(e.target.value)}
|
||||
placeholder="Enter image URL"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Drag Enabled:
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editEnabled}
|
||||
onChange={() => setEditEnabled(b => !b)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Show Grid:
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showGrid}
|
||||
onChange={() => setShowGrid(b => !b)}
|
||||
/>
|
||||
</label>
|
||||
<div className="widget-buttons">
|
||||
<button onClick={() => createWidget('clock')}>Add Clock</button>
|
||||
<button onClick={() => createWidget('search')}>Add Search</button>
|
||||
</div>
|
||||
)}
|
||||
</ModalOverlay >
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import React, { useRef, useState, useEffect, ReactElement } from 'react';
|
||||
import { WidgetData } from '../types';
|
||||
import { CogwheelIcon } from './CogwheelIcon';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { draggableState } from '../state/draggable.state';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { editState } from '../state/edit.state';
|
||||
import { CloseButton } from './Closebutton';
|
||||
import { ModalOverlay } from './ModalOverlay';
|
||||
|
||||
interface WidgetProps {
|
||||
widget: WidgetData;
|
||||
gridSize: { width: number; height: number };
|
||||
children: React.ReactNode;
|
||||
settingsContent?: React.ReactNode;
|
||||
settingsContent: ReactElement;
|
||||
updateWidget: (id: string, data: Partial<WidgetData>) => void
|
||||
}
|
||||
|
||||
@@ -17,19 +19,21 @@ export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, sett
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [isSettingsOpen, setSettingsOpen] = useState(false);
|
||||
const [widthInput, setWidthInput] = useState(widget.gridWidth);
|
||||
const [heightInput, setHeightInput] = useState(widget.gridHeight);
|
||||
|
||||
const isDraggable = useAtomValue(draggableState)
|
||||
const isDraggable = useAtomValue(editState)
|
||||
const snapToGrid = (pos: number, grid: number) => Math.round(pos / grid);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if(!isDraggable || isSettingsOpen){
|
||||
if (!isDraggable || isSettingsOpen) {
|
||||
return
|
||||
}
|
||||
|
||||
if (widgetRef.current && e.target === widgetRef.current) {
|
||||
e.stopPropagation()
|
||||
|
||||
if (!!widgetRef.current) {
|
||||
setIsDragging(true);
|
||||
const rect = widgetRef.current.getBoundingClientRect();
|
||||
setDragOffset({
|
||||
@@ -62,22 +66,16 @@ export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, sett
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
||||
setIsSettingsOpen(false);
|
||||
setSettingsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const stopPropagation = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleSizeChange = () => {
|
||||
useEffect(() => {
|
||||
const newWidth = Math.max(1, widthInput);
|
||||
const newHeight = Math.max(1, heightInput);
|
||||
updateWidget(widget.id, { gridWidth: newWidth, gridHeight: newHeight });
|
||||
setWidthInput(newWidth);
|
||||
setHeightInput(newHeight);
|
||||
setIsSettingsOpen(false);
|
||||
};
|
||||
}, [heightInput, widthInput])
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
@@ -110,55 +108,70 @@ export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, sett
|
||||
}
|
||||
}, [widget.gridX, widget.gridY, widget.gridWidth, widget.gridHeight, gridSize, isDragging]);
|
||||
|
||||
const genericSettingsContent = (
|
||||
<>
|
||||
<label>
|
||||
Width (grid cells):
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={widthInput}
|
||||
onChange={(e) => setWidthInput(parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Height (grid cells):
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={heightInput}
|
||||
onChange={(e) => setHeightInput(parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
</label>
|
||||
<button onClick={handleSizeChange}>Apply</button>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
<div
|
||||
ref={widgetRef}
|
||||
className="container"
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{ cursor: isDraggable && !isSettingsOpen ? 'grab' : 'default' }}
|
||||
>
|
||||
<button className="close-button" onClick={() => undefined /*removeWidget(widget.id)*/} onMouseDown={stopPropagation}>×</button>
|
||||
<button
|
||||
className="settings-button"
|
||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||
onMouseDown={stopPropagation}
|
||||
<ModalOverlay
|
||||
setVisible={setSettingsOpen}
|
||||
visible={isSettingsOpen}
|
||||
>
|
||||
<CogwheelIcon size={24} />
|
||||
</button>
|
||||
<SettingsComponent
|
||||
heightInput={heightInput}
|
||||
setHeightInput={setHeightInput}
|
||||
setWidthInput={setWidthInput}
|
||||
widthInput={widthInput}
|
||||
>
|
||||
{settingsContent}
|
||||
</SettingsComponent>
|
||||
</ModalOverlay>
|
||||
{isDraggable && <>
|
||||
<CloseButton onClick={() => undefined /*removeWidget(widget.id)*/} />
|
||||
<button
|
||||
className="settings-button"
|
||||
onClick={() => setSettingsOpen(true)}
|
||||
>
|
||||
<CogwheelIcon size={24} />
|
||||
</button>
|
||||
</>}
|
||||
{children}
|
||||
{isSettingsOpen && (
|
||||
<div className="widget-settings-modal" onMouseDown={stopPropagation}>
|
||||
<div ref={modalRef} className="widget-modal-content">
|
||||
<button className="close-button" onClick={() => setIsSettingsOpen(false)} onMouseDown={stopPropagation}>×</button>
|
||||
{settingsContent}
|
||||
{genericSettingsContent}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
type GenericSettingsProps = {
|
||||
widthInput: number;
|
||||
setWidthInput: (n: number) => void;
|
||||
heightInput: number;
|
||||
setHeightInput: (n: number) => void;
|
||||
children: ReactElement | ReactElement[]
|
||||
}
|
||||
|
||||
function SettingsComponent({ widthInput, setWidthInput, heightInput, setHeightInput, children }: GenericSettingsProps) {
|
||||
return <>
|
||||
{children}
|
||||
<label>
|
||||
Width (grid cells):
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={widthInput}
|
||||
onChange={(e) => { setWidthInput(parseInt(e.target.value)); }}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Height (grid cells):
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={heightInput}
|
||||
onChange={(e) => { setHeightInput(parseInt(e.target.value)); }}
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { WidgetData, SearchWidgetData, ClockWidgetData, WidgetExport, WidgetTypes } from '../types';
|
||||
import { WidgetData, WidgetExport, WidgetTypes } from '../types';
|
||||
import { Widget } from './Widget';
|
||||
import { SearchWidget } from './SearchWidget';
|
||||
import { ClockWidget } from './ClockWidget';
|
||||
|
||||
+31
-27
@@ -1,31 +1,35 @@
|
||||
import { GlobalSettings, SearchWidgetData, ClockWidgetData } from './types';
|
||||
import { GlobalSettings, SearchWidgetData, ClockWidgetData, WidgetTypes, WidgetData } from './types';
|
||||
|
||||
export const DEFAULT_SETTINGS: GlobalSettings = {
|
||||
backgroundUrl: '',
|
||||
widgets: [
|
||||
{
|
||||
id: 'clock-1',
|
||||
type: 'clock',
|
||||
gridX: 15,
|
||||
gridY: 7,
|
||||
gridWidth: 6, // Clock: 6x3
|
||||
gridHeight: 3,
|
||||
dragEnabled: true,
|
||||
showSeconds: true,
|
||||
use24Hour: false,
|
||||
timezone: 'UTC' // Default timezone
|
||||
} as ClockWidgetData,
|
||||
{
|
||||
id: 'search-1',
|
||||
type: 'search',
|
||||
gridX: 13,
|
||||
gridY: 10,
|
||||
gridWidth: 10, // Search: 10x2
|
||||
gridHeight: 2,
|
||||
dragEnabled: true,
|
||||
engine: 'Google'
|
||||
} as SearchWidgetData
|
||||
],
|
||||
dragEnabled: true,
|
||||
showGrid: false
|
||||
widgets: [],
|
||||
editEnabled: true,
|
||||
showGrid: true
|
||||
};
|
||||
|
||||
export const intitialWidgetData = (type: WidgetTypes): WidgetData => {
|
||||
switch (type) {
|
||||
case 'clock':
|
||||
return {
|
||||
gridHeight: 2,
|
||||
gridWidth: 6,
|
||||
gridX: 15,
|
||||
gridY: 6,
|
||||
timezone: 'UTC',
|
||||
showSeconds: false,
|
||||
use24Hour: true,
|
||||
type: 'clock',
|
||||
id: `${type}-${Date.now()}`
|
||||
} as ClockWidgetData
|
||||
case 'search':
|
||||
return {
|
||||
gridHeight: 2,
|
||||
gridWidth: 10,
|
||||
gridX: 13,
|
||||
gridY: 6,
|
||||
id: `${type}-${Date.now()}`,
|
||||
type: 'search',
|
||||
engine: 'Google'
|
||||
} as SearchWidgetData
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
export const draggableState = atom<boolean>(false)
|
||||
@@ -0,0 +1,4 @@
|
||||
import { atom } from "jotai";
|
||||
import { DEFAULT_SETTINGS } from "../defaults";
|
||||
|
||||
export const editState = atom<boolean>(DEFAULT_SETTINGS.editEnabled)
|
||||
@@ -0,0 +1,5 @@
|
||||
import { atom } from "jotai";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export const modalVisible = atom<boolean>(false)
|
||||
export const modalContent = atom<ReactNode>(undefined)
|
||||
@@ -1,3 +1,4 @@
|
||||
import { atom } from "jotai";
|
||||
import { DEFAULT_SETTINGS } from "../defaults";
|
||||
|
||||
export const showGridState = atom<boolean>(false)
|
||||
export const showGridState = atom<boolean>(DEFAULT_SETTINGS.showGrid)
|
||||
@@ -25,7 +25,7 @@ export type WidgetTypes = (SearchWidgetData | ClockWidgetData)['type']
|
||||
export interface GlobalSettings {
|
||||
backgroundUrl: string;
|
||||
widgets: WidgetData[];
|
||||
dragEnabled: boolean;
|
||||
editEnabled: boolean;
|
||||
showGrid: boolean;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user