@@ -26,43 +26,70 @@ const AVAILABLE_WIDGETS = [
2626
2727const DEFAULT_WIDGETS = [ "dhall" , "weather" , "today" ] ;
2828const ALLOWED_WIDGET_IDS = new Set ( AVAILABLE_WIDGETS . map ( ( widget ) => widget . id ) ) ;
29- type WidgetColumn = "left" | "right" ;
29+ const ALL_WIDGET_COLUMNS = [ "left" , "middle" , "right" ] as const ;
30+ const MIN_WIDGET_COLUMNS = 2 ;
31+ const MAX_WIDGET_COLUMNS = 3 ;
32+
33+ type WidgetColumn = ( typeof ALL_WIDGET_COLUMNS ) [ number ] ;
3034type WidgetLayout = Record < WidgetColumn , string [ ] > ;
3135
32- const WIDGET_COLUMNS : WidgetColumn [ ] = [ "left" , "right" ] ;
36+ const getActiveColumns = ( columnCount : number ) : WidgetColumn [ ] => {
37+ const safeCount = Math . max ( MIN_WIDGET_COLUMNS , Math . min ( MAX_WIDGET_COLUMNS , columnCount ) ) ;
38+ return safeCount === 2 ? [ "left" , "right" ] : [ "left" , "middle" , "right" ] ;
39+ } ;
40+
41+ const createEmptyLayout = ( ) : WidgetLayout => ( {
42+ left : [ ] ,
43+ middle : [ ] ,
44+ right : [ ] ,
45+ } ) ;
3346
34- const buildDefaultLayout = ( widgets : string [ ] ) : WidgetLayout => {
35- const layout : WidgetLayout = { left : [ ] , right : [ ] } ;
47+ const buildDefaultLayout = ( widgets : string [ ] , columnCount : number ) : WidgetLayout => {
48+ const layout = createEmptyLayout ( ) ;
49+ const activeColumns = getActiveColumns ( columnCount ) ;
3650 widgets . forEach ( ( widgetId , index ) => {
37- const targetColumn : WidgetColumn = index % 2 === 0 ? "left" : "right" ;
51+ const targetColumn = activeColumns [ index % activeColumns . length ] ;
3852 layout [ targetColumn ] . push ( widgetId ) ;
3953 } ) ;
4054 return layout ;
4155} ;
4256
4357const normalizeWidgetLayout = (
4458 layout : Partial < WidgetLayout > | null ,
45- activeWidgets : string [ ]
59+ activeWidgets : string [ ] ,
60+ columnCount : number
4661) : WidgetLayout => {
62+ const activeColumns = getActiveColumns ( columnCount ) ;
4763 const activeSet = new Set ( activeWidgets ) ;
4864 const seen = new Set < string > ( ) ;
4965
50- const sanitize = ( ids ?: string [ ] ) =>
51- ( ids || [ ] ) . filter ( ( id ) => {
66+ const normalized = createEmptyLayout ( ) ;
67+
68+ // Keep explicit user ordering for currently active columns.
69+ activeColumns . forEach ( ( column ) => {
70+ normalized [ column ] = ( layout ?. [ column ] || [ ] ) . filter ( ( id ) => {
5271 if ( ! activeSet . has ( id ) || seen . has ( id ) ) return false ;
5372 seen . add ( id ) ;
5473 return true ;
5574 } ) ;
75+ } ) ;
5676
57- const normalized : WidgetLayout = {
58- left : sanitize ( layout ?. left ) ,
59- right : sanitize ( layout ?. right ) ,
60- } ;
77+ // Re-home widgets from inactive columns + any new/missing widgets.
78+ const extras = ALL_WIDGET_COLUMNS . filter ( ( column ) => ! activeColumns . includes ( column ) )
79+ . flatMap ( ( column ) => layout ?. [ column ] || [ ] )
80+ . filter ( ( id ) => activeSet . has ( id ) && ! seen . has ( id ) ) ;
6181
6282 activeWidgets . forEach ( ( id ) => {
83+ if ( ! seen . has ( id ) ) {
84+ extras . push ( id ) ;
85+ }
86+ } ) ;
87+
88+ extras . forEach ( ( id ) => {
6389 if ( seen . has ( id ) ) return ;
64- const targetColumn =
65- normalized . left . length <= normalized . right . length ? "left" : "right" ;
90+ const targetColumn = activeColumns . reduce ( ( shortest , candidate ) =>
91+ normalized [ candidate ] . length < normalized [ shortest ] . length ? candidate : shortest
92+ ) ;
6693 normalized [ targetColumn ] . push ( id ) ;
6794 seen . add ( id ) ;
6895 } ) ;
@@ -79,6 +106,10 @@ function App() {
79106 const [ draggingWidgetId , setDraggingWidgetId ] = useState < string | null > ( null ) ;
80107 const [ dropTargetKey , setDropTargetKey ] = useState < string | null > ( null ) ;
81108 const [ snappedWidgetId , setSnappedWidgetId ] = useState < string | null > ( null ) ;
109+ const [ columnCount , setColumnCount ] = useState < 2 | 3 > ( ( ) => {
110+ const saved = storage . getLocalStorage ( StorageKeys . WIDGET_COLUMN_COUNT ) ;
111+ return saved === "2" ? 2 : 3 ;
112+ } ) ;
82113
83114 const [ activeWidgets , setActiveWidgets ] = useState < string [ ] > ( ( ) => {
84115 const saved = storage . getLocalStorage ( StorageKeys . ACTIVE_WIDGETS ) ;
@@ -95,13 +126,13 @@ function App() {
95126 } ) ;
96127 const [ widgetLayout , setWidgetLayout ] = useState < WidgetLayout > ( ( ) => {
97128 const saved = storage . getLocalStorage ( StorageKeys . WIDGET_LAYOUT ) ;
98- if ( ! saved ) return buildDefaultLayout ( activeWidgets ) ;
129+ if ( ! saved ) return buildDefaultLayout ( activeWidgets , columnCount ) ;
99130
100131 try {
101132 const parsed = JSON . parse ( saved ) as Partial < WidgetLayout > ;
102- return normalizeWidgetLayout ( parsed , activeWidgets ) ;
133+ return normalizeWidgetLayout ( parsed , activeWidgets , columnCount ) ;
103134 } catch {
104- return buildDefaultLayout ( activeWidgets ) ;
135+ return buildDefaultLayout ( activeWidgets , columnCount ) ;
105136 }
106137 } ) ;
107138
@@ -130,13 +161,17 @@ function App() {
130161 } , [ activeWidgets ] ) ;
131162
132163 useEffect ( ( ) => {
133- setWidgetLayout ( ( prev ) => normalizeWidgetLayout ( prev , activeWidgets ) ) ;
134- } , [ activeWidgets ] ) ;
164+ setWidgetLayout ( ( prev ) => normalizeWidgetLayout ( prev , activeWidgets , columnCount ) ) ;
165+ } , [ activeWidgets , columnCount ] ) ;
135166
136167 useEffect ( ( ) => {
137168 storage . setLocalStorage ( StorageKeys . WIDGET_LAYOUT , JSON . stringify ( widgetLayout ) ) ;
138169 } , [ widgetLayout ] ) ;
139170
171+ useEffect ( ( ) => {
172+ storage . setLocalStorage ( StorageKeys . WIDGET_COLUMN_COUNT , String ( columnCount ) ) ;
173+ } , [ columnCount ] ) ;
174+
140175 const toggleWidget = ( id : string ) => {
141176 if ( ! ALLOWED_WIDGET_IDS . has ( id ) ) return ;
142177 setActiveWidgets ( prev =>
@@ -163,14 +198,26 @@ function App() {
163198 if ( ! fromId ) return ;
164199
165200 setWidgetLayout ( ( prev ) => {
166- const next = normalizeWidgetLayout ( prev , activeWidgets ) ;
167- next . left = next . left . filter ( ( id ) => id !== fromId ) ;
168- next . right = next . right . filter ( ( id ) => id !== fromId ) ;
169-
170- const insertionIndex = Math . max (
171- 0 ,
172- Math . min ( toIndex , next [ toColumn ] . length )
173- ) ;
201+ const next = normalizeWidgetLayout ( prev , activeWidgets , columnCount ) ;
202+ let fromColumn : WidgetColumn | null = null ;
203+ let fromIndex = - 1 ;
204+
205+ for ( const column of ALL_WIDGET_COLUMNS ) {
206+ const index = next [ column ] . indexOf ( fromId ) ;
207+ if ( index === - 1 || fromColumn ) continue ;
208+ fromColumn = column ;
209+ fromIndex = index ;
210+ }
211+
212+ if ( ! fromColumn ) return next ;
213+
214+ next [ fromColumn ] = next [ fromColumn ] . filter ( ( id ) => id !== fromId ) ;
215+
216+ let insertionIndex = Math . max ( 0 , Math . min ( toIndex , next [ toColumn ] . length ) ) ;
217+ if ( fromColumn === toColumn && fromIndex < toIndex ) {
218+ insertionIndex = Math . max ( 0 , insertionIndex - 1 ) ;
219+ }
220+
174221 next [ toColumn ] . splice ( insertionIndex , 0 , fromId ) ;
175222 return next ;
176223 } ) ;
@@ -180,6 +227,7 @@ function App() {
180227
181228 const feedbackMailto =
182229 "mailto:aj5828@princeton.edu?subject=TodayCustom%20Feedback%20%28Free%20Coffee%29&body=Hey%20Khan%2C%0A%0AHere%20is%20my%20feedback%20for%20TodayCustom%3A%0A-%20%0A-%20%0A%0AThanks!" ;
230+ const activeColumns = getActiveColumns ( columnCount ) ;
183231
184232 return (
185233 < div className = "App" >
@@ -216,8 +264,10 @@ function App() {
216264 </ div >
217265 ) }
218266
219- < div className = { `command-center-widgets ${ isArrangeMode ? "is-arranging" : "" } ` } >
220- { WIDGET_COLUMNS . map ( ( column ) => (
267+ < div
268+ className = { `command-center-widgets columns-${ columnCount } ${ isArrangeMode ? "is-arranging" : "" } ` }
269+ >
270+ { activeColumns . map ( ( column ) => (
221271 < div
222272 key = { column }
223273 className = { [
@@ -264,16 +314,20 @@ function App() {
264314 onDragOver = { ( e ) => {
265315 if ( ! isArrangeMode ) return ;
266316 e . preventDefault ( ) ;
317+ e . stopPropagation ( ) ;
267318 if ( dropTargetKey !== `${ column } :${ widgetId } ` ) {
268319 setDropTargetKey ( `${ column } :${ widgetId } ` ) ;
269320 }
270321 } }
271322 onDrop = { ( e ) => {
272323 if ( ! isArrangeMode ) return ;
273324 e . preventDefault ( ) ;
325+ e . stopPropagation ( ) ;
274326 const draggedId = e . dataTransfer . getData ( "text/plain" ) ;
275327 const targetIndex = widgetLayout [ column ] . indexOf ( widgetId ) ;
276- moveWidget ( draggedId , column , targetIndex ) ;
328+ const slotRect = ( e . currentTarget as HTMLDivElement ) . getBoundingClientRect ( ) ;
329+ const isDropAfter = e . clientY >= slotRect . top + slotRect . height / 2 ;
330+ moveWidget ( draggedId , column , targetIndex + ( isDropAfter ? 1 : 0 ) ) ;
277331 setDraggingWidgetId ( null ) ;
278332 setDropTargetKey ( null ) ;
279333 } }
@@ -305,6 +359,8 @@ function App() {
305359 activeWidgets = { activeWidgets }
306360 toggleWidget = { toggleWidget }
307361 availableWidgets = { AVAILABLE_WIDGETS }
362+ columnCount = { columnCount }
363+ onColumnCountChange = { setColumnCount }
308364 />
309365
310366 < a
0 commit comments