Setting the file. One moment.
Board Template · Daily Focus Board · github/awesome-copilot · Skills Docs
ContentsBack to the top of the page (opens in a new tab)
assets/ board.template.html
HTML · 487 lines · 43 KB
8 SKILL: daily-focus-board (Ember) — executive-function-friendly daily board.
9 Personalize by setting window.__BOARD__ below to a config object:
10 { name:"Alex", dateKey:"2026-07-27",
11 mantra:"Small, real, done — one block at a time.", // optional: today's intention
12 checkin:"above", // optional: "below" | "mid" | "above"
13 tasks:[
14 { id:"pages", emoji:"📖", title:"Read 30 pages",
15 goal:30, start:0, inc:5, unit:"pages", tag:"mind", tagc:"new" },
16 { id:"doc", emoji:"⚙️", title:"Finish the design doc",
17 sub:"the anchor", due:"2026-07-27T17:00", tag:"deadline", tagc:"deadline" },
18 { id:"read", emoji:"📖", title:"Read a chapter", tag:"mind" } ] }
19 - numeric `goal` → counter card (progress bar). otherwise a status card
20 (to-do → in progress → done) with progress notes.
21 - optional `due` (ISO local datetime) → gentle live countdown, never red-shaming.
22 - optional `mantra` / `checkin` → today's intention + an above/below-the-line arrival
23 check-in (both editable on the board; the partner can also set them from your chat).
24 Built-in features (no config needed): a "how are you arriving?" check-in + daily mantra,
25 ➕ add-a-task, Focus mode (dim all but one), "not today" kind carryover, a 🧠 brain-dump
26 capture box, a reduced-motion toggle, and always-visible time. State persists in localStorage.
27 -->
28 < script >window.__BOARD__ = null ; /* SKILL: replace null with the config object above */ </ script >
29 < style >
30 :root { --bg : #0f1117 ; --card : #191d29 ; --line : #2b3150 ; --ink : #eef1fa ; --sub : #98a2bd ;
31 --ember : #ff7a3c ; --ember2 : #ffb23c ; --good : #37d39a ; --urgent : #ff5d73 ;}
32 * { box-sizing : border-box } html , body { margin : 0 }
33 body { font-family : "Segoe UI" , system-ui , -apple-system , sans-serif ;
34 background : radial-gradient ( 1100 px 560 px at 82 % -12 % , #34204a 0 % , var ( --bg ) 55 % );
35 color : var ( --ink ); min-height : 100 vh ; padding : 34 px 18 px 90 px ;}
36 body .rm * { transition : none !important ; animation : none !important }
37 .wrap { max-width : 800 px ; margin : 0 auto }
38 header { display : flex ; align-items : flex-start ; gap : 22 px ; margin-bottom : 4 px }
39 .ring { width : 98 px ; height : 98 px ; flex : none ; border-radius : 50 % ; position : relative ; background : conic-gradient ( var ( --ember ) 0 deg , var ( --line ) 0 deg )}
40 .ring b { position : absolute ; inset : 12 px ; border-radius : 50 % ; background : #12141c ; display : grid ; place-items : center ; font-size : 22 px ; font-weight : 800 }
41 h1 { font-size : 27 px ; margin : 0 0 3 px }
42 .date { color : var ( --sub ); font-size : 14 px ; margin : 0 }
43 .date .clock { color : var ( --ember2 ); font-variant-numeric : tabular-nums ; font-weight : 600 }
44 .tally { color : var ( --ember2 ); font-size : 13.5 px ; margin : 6 px 0 0 ; font-weight : 600 }
45 .spark { color : var ( --sub ); font-size : 13.5 px ; margin : 8 px 0 0 ; min-height : 18 px ; font-style : italic }
46 .controls { display : flex ; gap : 8 px ; margin-top : 10 px ; flex-wrap : wrap }
47 .chip { font-size : 12 px ; padding : 5 px 11 px ; border-radius : 999 px ; background : #232a42 ; border : 1 px solid var ( --line ); color : #aab4d6 ; cursor : pointer ; user-select : none }
48 .chip:hover { border-color : var ( --ember )}
49 .chip.on { background : #3a2f18 ; border-color : #5c471f ; color : #ffce7a }
50 .focusbar { display : none ; align-items : center ; gap : 10 px ; margin : 16 px 0 0 ; padding : 10 px 14 px ; background : #20182c ; border : 1 px solid #4a3a6b ; border-radius : 12 px ; font-size : 14 px }
51 body .focusing .focusbar { display : flex }
52 .focusbar .x { margin-left : auto ; color : #c3a9ff ; cursor : pointer ; font-weight : 600 }
53 .cards { margin-top : 16 px ; display : flex ; flex-direction : column ; gap : 12 px }
54 .card { background : var ( --card ); border : 1 px solid var ( --line ); border-radius : 16 px ; padding : 14 px 16 px ; transition :border-color .2 s , background .2 s ,opacity .2 s }
55 .card.done { background : #141826 ; border-color : #243a30 }
56 .card.doing { border-color : #4a3a1e }
57 body .focusing .card { opacity : .24 ; filter : saturate ( .5 )}
58 body .focusing .card.focused { opacity : 1 ; filter : none ; border-color : #6b52a0 }
59 .card.carried { opacity : .5 }
60 .top { display : flex ; align-items : center ; gap : 14 px }
61 .box { width : 30 px ; height : 30 px ; flex : none ; border-radius : 9 px ; border : 2 px solid #3b4472 ; display : grid ; place-items : center ; font-size : 16 px ; color : #12141c ; cursor : pointer ; transition : .2 s ; user-select : none }
62 .box.doing { background : var ( --ember2 ); border-color : var ( --ember2 )}
63 .box.done { background : var ( --good ); border-color : var ( --good )}
64 .emoji { font-size : 22 px ; flex : none ; width : 26 px ; text-align : center }
65 .body { flex : 1 ; min-width : 0 }
66 .title { font-size : 16 px ; font-weight : 600 }
67 .card.done .title { color : var ( --sub )}
68 .sub { font-size : 12.5 px ; color : var ( --sub ); margin-top : 2 px }
69 .due { font-size : 11.5 px ; margin-top : 3 px ; color : var ( --ember2 ); font-weight : 600 }
70 .due.over { color : #ffab6b }
71 .due.done { color : var ( --good )}
72 .pill { font-size : 10.5 px ; font-weight : 700 ; letter-spacing : .4 px ; text-transform : uppercase ; padding : 4 px 9 px ; border-radius : 999 px ; cursor : pointer ; flex : none ; user-select : none ; border : 1 px solid transparent }
73 .pill.todo { background : #232a42 ; color : #aab4d6 }
74 .pill.doing { background : #3a2f18 ; color : #ffce7a ; border-color : #5c471f }
75 .pill.done { background : #16311f ; color : #7fe3bb ; border-color : #204d33 }
76 .pill.carried { background : #241f30 ; color : #b9a6dd ; border-color : #3d2f57 }
77 .fbtn { background : none ; border : none ; color : #6b7599 ; cursor : pointer ; font-size : 15 px ; flex : none ; padding : 2 px }
78 .fbtn:hover { color : #c3a9ff }
79 .tag { font-size : 10 px ; font-weight : 700 ; letter-spacing : .4 px ; text-transform : uppercase ; padding : 3 px 8 px ; border-radius : 999 px ; background : #232a42 ; color : #8b96b8 ; flex : none }
80 .tag.deadline { background : #3a1d26 ; color : #ff9caa }
81 .tag.new { background : #22322c ; color : #7fe3bb }
82 .tag.career { background : #2c2540 ; color : #c3a9ff }
83 .cardfoot { margin : 9 px 0 0 44 px ; display : flex ; align-items : center ; gap : 12 px }
84 .soft { font-size : 11.5 px ; color : var ( --sub ); cursor : pointer ; background : none ; border : none ; padding : 0 }
85 .soft:hover { color : var ( --ember2 )}
86 .notes { margin : 11 px 0 0 44 px ; display : flex ; flex-direction : column ; gap : 6 px }
87 .note { display : flex ; align-items : flex-start ; gap : 8 px ; font-size : 13 px ; background : #12141c ; border : 1 px solid var ( --line ); border-radius : 9 px ; padding : 6 px 10 px }
88 .note .nt { color : var ( --ember2 ); font-variant-numeric : tabular-nums ; font-size : 11.5 px ; flex : none ; padding-top : 1 px }
89 .note .nx { margin-left : auto ; color : #556079 ; cursor : pointer ; flex : none ; font-size : 14 px ; line-height : 1 }
90 .note .nx:hover { color : var ( --urgent )}
91 .addrow { margin : 9 px 0 0 44 px ; display : flex ; gap : 7 px }
92 .addrow input { flex : 1 ; background : #12141c ; border : 1 px solid var ( --line ); color : var ( --ink ); border-radius : 9 px ; padding : 7 px 10 px ; font-size : 13 px }
93 .addrow input ::placeholder { color : #566079 }
94 .addrow input :focus { outline : none ; border-color : var ( --ember )}
95 .addrow button { background : #232a42 ; border : 1 px solid var ( --line ); color : var ( --ink ); border-radius : 9 px ; padding : 0 13 px ; cursor : pointer ; font-size : 15 px }
96 .addrow button :hover { border-color : var ( --ember ); color : var ( --ember2 )}
97 .bar { height : 12 px ; border-radius : 999 px ; background : #232a42 ; overflow : hidden ; margin : 12 px 0 0 0 }
98 .fill { height : 100 % ; width : 0 ; border-radius : 999 px ; background : linear-gradient ( 90 deg , var ( --ember ), var ( --ember2 )); transition :width .7 s cubic-bezier ( .2 , .8 , .2 , 1 )}
99 .stepctl { display : flex ; align-items : center ; gap : 8 px ; font-size : 12.5 px ; color : var ( --sub ); margin-top : 10 px }
100 .stepctl input { width : 92 px ; background : #12141c ; border : 1 px solid var ( --line ); color : var ( --ink ); border-radius : 8 px ; padding : 6 px 8 px ; font-size : 14 px }
101 .stepctl button { background : #232a42 ; border : 1 px solid var ( --line ); color : var ( --ink ); border-radius : 8 px ; padding : 6 px 12 px ; cursor : pointer ; font-size : 13 px }
102 .stepctl button :hover { border-color : var ( --ember )}
103 .panel { margin-top : 18 px ; background : var ( --card ); border : 1 px solid var ( --line ); border-radius : 16 px ; padding : 16 px 18 px }
104 .panel h2 { font-size : 16 px ; margin : 0 0 4 px ; display : flex ; align-items : center ; gap : 8 px }
105 .panel .hint { color : var ( --sub ); font-size : 12 px ; margin : 0 0 12 px }
106 .chips { display : flex ; flex-direction : column ; gap : 8 px }
107 .fitem { display : flex ; gap : 10 px ; align-items : flex-start ; font-size : 13.5 px ; padding : 8 px 11 px ; background : #12141c ; border : 1 px solid var ( --line ); border-radius : 10 px ; border-left : 3 px solid var ( --ember )}
108 .fitem.think { border-left-color : #c3a9ff }
109 .fitem .ft { color : var ( --ember2 ); font-variant-numeric : tabular-nums ; font-size : 11.5 px ; flex : none ; padding-top : 1 px ; min-width : 52 px }
110 .fitem .fx { margin-left : auto ; color : #556079 ; cursor : pointer ; flex : none }
111 .fitem .fx:hover { color : var ( --urgent )}
112 .empty { color : var ( --sub ); font-size : 13 px ; font-style : italic ; padding : 6 px 2 px }
113 .foot { text-align : center ; color : var ( --sub ); font-size : 12 px ; margin-top : 24 px }
114 #cc { position : fixed ; inset : 0 ; pointer-events : none ; z-index : 50 }
115 .checkin { margin : 16 px 0 0 ; padding : 12 px 14 px ; background : #161a26 ; border : 1 px solid var ( --line ); border-radius : 14 px }
116 .ci-row { display : flex ; align-items : center ; gap : 8 px ; flex-wrap : wrap }
117 .ci-label { color : var ( --sub ); font-size : 13 px ; margin-right : 2 px }
118 .ci-opt { font-size : 13 px ; padding : 5 px 12 px ; border-radius : 999 px ; background : #232a42 ; border : 1 px solid var ( --line ); color : #aab4d6 ; cursor : pointer ; user-select : none }
119 .ci-opt:hover { border-color : var ( --ember )}
120 .ci-opt.on { background : #20182c ; border-color : #6b52a0 ; color : #d9c7ff }
121 .ci-hint { color : var ( --sub ); font-size : 12.5 px ; margin : 8 px 0 0 ; font-style : italic ; min-height : 16 px }
122 .mantra { display : flex ; align-items : center ; gap : 10 px ; margin-top : 12 px ; padding-top : 12 px ; border-top : 1 px solid var ( --line )}
123 .mantra-tag { font-size : 10.5 px ; font-weight : 700 ; letter-spacing : .5 px ; text-transform : uppercase ; color : var ( --ember2 ); flex : none }
124 .mantra-in { flex : 1 ; min-width : 0 ; background : transparent ; border : none ; border-bottom : 1 px dashed #3b4472 ; color : var ( --ink ); font-size : 15.5 px ; font-style : italic ; font-weight : 600 ; padding : 4 px 2 px }
125 .mantra-in:focus { outline : none ; border-bottom-color : var ( --ember )}
126 .mantra-in::placeholder { color : #5b6480 ; font-weight : 400 }
127 .mantra-btn { flex : none ; background : #232a42 ; border : 1 px solid var ( --line ); color : #aab4d6 ; border-radius : 9 px ; cursor : pointer ; font-size : 14 px ; padding : 5 px 9 px }
128 .mantra-btn:hover { border-color : var ( --ember )}
129 .addtask { margin-top : 12 px }
130 .addtask-toggle { background : #191d29 ; border : 1 px dashed #3b4472 ; color : var ( --sub ); border-radius : 12 px ; padding : 10 px 14 px ; width : 100 % ; cursor : pointer ; font-size : 14 px ; text-align : left }
131 .addtask-toggle:hover { border-color : var ( --ember ); color : var ( --ink )}
132 .addtask-form { display : flex ; align-items : center ; gap : 8 px ; flex-wrap : wrap ; background : var ( --card ); border : 1 px solid var ( --line ); border-radius : 12 px ; padding : 12 px }
133 .addtask-form #ntitle , .addtask-form #nunit { background : #12141c ; border : 1 px solid var ( --line ); color : var ( --ink ); border-radius : 8 px ; padding : 8 px 10 px ; font-size : 14 px }
134 .addtask-form #ntitle { flex : 1 ; min-width : 160 px }
135 .addtask-count { display : flex ; align-items : center ; gap : 6 px ; color : var ( --sub ); font-size : 13 px ; cursor : pointer }
136 .ncount-fields { align-items : center ; gap : 6 px }
137 .ncount-fields input { width : 96 px ; background : #12141c ; border : 1 px solid var ( --line ); color : var ( --ink ); border-radius : 8 px ; padding : 8 px 10 px ; font-size : 14 px }
138 .addtask-form #naddbtn { background : var ( --ember ); border : none ; color : #12141c ; font-weight : 700 ; border-radius : 8 px ; padding : 8 px 14 px ; cursor : pointer }
139 .rmbtn { flex : none ; background : none ; border : none ; color : #556079 ; cursor : pointer ; font-size : 14 px ; padding : 2 px }
140 .rmbtn:hover { color : var ( --urgent )}
141 .overload { margin : 16 px 0 0 ; padding : 11 px 14 px ; background : #2a2213 ; border : 1 px solid #5c471f ; border-radius : 12 px ; color : #ffce7a ; font-size : 13.5 px ; line-height : 1.5 }
142 .prioline { display : flex ; gap : 8 px ; margin : 14 px 0 0 ; flex-wrap : wrap }
143 .legend { margin : 10 px 0 0 ; padding : 12 px 14 px ; background : #161a26 ; border : 1 px solid var ( --line ); border-radius : 12 px ; font-size : 13 px ; color : var ( --sub ); line-height : 1.5 }
144 .legend-row { display : flex ; align-items : center ; gap : 8 px ; padding : 4 px 0 }
145 .qbadge { font-size : 10.5 px ; font-weight : 700 ; text-transform : uppercase ; letter-spacing : .3 px ; padding : 3 px 8 px ; border-radius : 6 px ; flex : none ; color : #12141c }
146 .qbadge.q-iu { background : var ( --urgent )}
147 .qbadge.q-ins { background : #5aa9ff }
148 .qbadge.q-niu { background : var ( --ember2 )}
149 .qbadge.q-ninu { background : #8892b0 }
150 .card.q-iu { border-left : 4 px solid var ( --urgent )}
151 .card.q-ins { border-left : 4 px solid #5aa9ff }
152 .card.q-niu { border-left : 4 px solid var ( --ember2 )}
153 .card.q-ninu { border-left : 4 px solid #8892b0 }
154 .card.dragover { border-color : var ( --ember ); box-shadow : 0 0 0 2 px rgba ( 255 , 122 , 60 , .4 )}
155 .cardmeta { display : flex ; align-items : center ; gap : 8 px ; margin-top : 10 px ; padding-top : 10 px ; border-top : 1 px solid var ( --line ); flex-wrap : wrap }
156 .grip { cursor : grab ; color : #556079 ; font-size : 15 px ; user-select : none ; flex : none }
157 .grip:active { cursor : grabbing }
158 .quadsel { background : #12141c ; border : 1 px solid var ( --line ); color : var ( --ink ); border-radius : 8 px ; padding : 5 px 8 px ; font-size : 12 px }
159 .tagedit { flex : 1 ; min-width : 80 px ; max-width : 170 px ; background : #12141c ; border : 1 px solid var ( --line ); color : var ( --sub ); border-radius : 999 px ; padding : 4 px 11 px ; font-size : 11 px ; text-transform : uppercase ; letter-spacing : .4 px }
160 .tagedit:focus { outline : none ; border-color : var ( --ember ); color : var ( --ink )}
161 #eoddownload { background : var ( --ember ); border : none ; color : #12141c ; font-weight : 700 ; border-radius : 9 px ; padding : 8 px 14 px ; cursor : pointer ; font-size : 14 px }
162 button { font-family : inherit }
163 button .box { padding : 0 }
164 button .x , button .nx , button .fx , button .mv { background : none ; border : none ; padding : 0 ; cursor : pointer ; font-size : inherit ; line-height : 1 }
165 .mv { color : #556079 ; font-size : 11 px ; padding : 0 1 px }
166 .mv:hover { color : var ( --ember )}
167 .tagedit.new { border-color : #2f6b4f ; color : #8fe3bb }
168 .tagedit.deadline { border-color : #6b2f3f ; color : #ff9fb0 }
169 .tagedit.career { border-color : #4a3a6b ; color : #c9b6ff }
170 </ style >
171 </ head >
172 < body >
173 < canvas id = "cc" ></ canvas >
174 < div class = "wrap" >
175 < header >
176 < div class = "ring" id = "ring" >< b id = "ringtxt" >0/0</ b ></ div >
177 < div style = "flex:1" >
178 < h1 id = "h1" >Let's go 🔥</ h1 >
179 < p class = "date" id = "date" ></ p >
180 < p class = "tally" id = "tally" ></ p >
181 < p class = "spark" id = "spark" ></ p >
182 < div class = "controls" >
183 < button type = "button" class = "chip" id = "focuschip" aria-pressed = "false" >🎯 Focus mode</ button >
184 < button type = "button" class = "chip" id = "rmchip" aria-pressed = "false" >🌙 Reduce motion</ button >
185 </ div >
186 </ div >
187 </ header >
188
189 < div class = "checkin" id = "checkin" >
190 < div class = "ci-row" >
191 < span class = "ci-label" >Arriving today:</ span >
192 < button type = "button" class = "ci-opt" data-checkin = "below" aria-pressed = "false" >🌧️ below the line</ button >
193 < button type = "button" class = "ci-opt" data-checkin = "mid" aria-pressed = "false" >⛅ in between</ button >
194 < button type = "button" class = "ci-opt" data-checkin = "above" aria-pressed = "false" >☀️ above the line</ button >
195 </ div >
196 < p class = "ci-hint" id = "checkinhint" >Just notice where you're arriving — no wrong answer.</ p >
197 < div class = "mantra" >
198 < span class = "mantra-tag" >today's mantra</ span >
199 < input id = "mantrain" class = "mantra-in" placeholder = "set a mantra for today — or tap 🔄, or ask Ember for one" />
200 < button id = "mantrasuggest" class = "mantra-btn" title = "suggest a mantra" aria-label = "suggest a mantra" >🔄</ button >
201 </ div >
202 </ div >
203
204 < div class = "focusbar" id = "focusbar" >< span id = "focustxt" ></ span >< button type = "button" class = "x" id = "focusexit" >show all ✕</ button ></ div >
205
206 < div class = "overload" id = "overload" style = "display:none" ></ div >
207
208 < div class = "prioline" >
209 < button type = "button" class = "chip" id = "prioguide" aria-expanded = "false" >🧭 priority guide</ button >
210 < button type = "button" class = "chip" id = "priosort" >⬍ sort by priority</ button >
211 </ div >
212 < div class = "legend" id = "legend" style = "display:none" >
213 < div class = "legend-row" >< span class = "qbadge q-iu" >Do first</ span > important & urgent — do it now.</ div >
214 < div class = "legend-row" >< span class = "qbadge q-ins" >Schedule</ span > important, not urgent — plan a time. (The good, non-frantic work lives here.)</ div >
215 < div class = "legend-row" >< span class = "qbadge q-niu" >Delegate</ span > urgent, not important — hand it off, automate, or shrink it.</ div >
216 < div class = "legend-row" >< span class = "qbadge q-ninu" >Later</ span > not urgent, not important — park it, or kindly let it go.</ div >
217 </ div >
218
219 < div class = "cards" id = "cards" ></ div >
220
221 < div class = "addtask" id = "addtask" >
222 < button class = "addtask-toggle" id = "addtaskbtn" >➕ add a task</ button >
223 < div class = "addtask-form" id = "addtaskform" style = "display:none" >
224 < input id = "ntitle" placeholder = "what needs doing?" />
225 < label class = "addtask-count" >< input type = "checkbox" id = "niscount" /> count toward a number</ label >
226 < span id = "ncountfields" class = "ncount-fields" style = "display:none" >< input type = "number" id = "ngoal" min = "1" step = "1" placeholder = "goal" />< input id = "nunit" placeholder = "unit (pages, reps…)" /></ span >
227 < button id = "naddbtn" >add</ button >
228 < button id = "ncancel" class = "soft" >cancel</ button >
229 </ div >
230 </ div >
231
232 < div class = "panel" >
233 < h2 >🧠 Parked thoughts</ h2 >
234 < p class = "hint" >Something pulling at your attention? Park it here so it's out of your head — deal with it later, not now.</ p >
235 < div class = "addrow" style = "margin-left:0" >< input id = "brainin" placeholder = "get it out of your head…" />< button id = "brainbtn" title = "park it" aria-label = "park this thought" >+</ button ></ div >
236 < div class = "chips" id = "brain" style = "margin-top:12px" ></ div >
237 </ div >
238
239 < div class = "panel" >
240 < h2 >🔥 Today's momentum</ h2 >
241 < p class = "hint" >Every step you log lands here, newest first. Starting counts. Small wins count.</ p >
242 < div class = "addrow" style = "margin-left:0" >< input id = "dayin" placeholder = "log a win / milestone for the day…" />< button id = "daybtn" title = "add" aria-label = "log a win for the day" >+</ button ></ div >
243 < div class = "chips" id = "feed" style = "margin-top:12px" ></ div >
244 </ div >
245
246 < div class = "panel" >
247 < h2 >🌙 End of day</ h2 >
248 < p class = "hint" >Save the day's progress — keep the recap, or paste it to Ember to journal today and plan tomorrow.</ p >
249 < div class = "addrow" style = "margin-left:0" >
250 < button id = "eoddownload" >💾 download recap</ button >
251 < button id = "eodcopy" class = "soft" >📋 copy to share with Ember</ button >
252 </ div >
253 < p class = "ci-hint" id = "eodmsg" role = "status" aria-live = "polite" ></ p >
254 </ div >
255
256 < p class = "foot" >progress saves automatically in this browser · 🔥 built with Ember</ p >
257 </ div >
258 < script >
259 const DEMO = {name: "" ,mantra: "" ,checkin: null ,tasks:[
260 {id: "pages" ,emoji: "📖" ,title: "Read 30 pages" ,goal: 30 ,start: 0 ,inc: 5 ,unit: "pages" ,tag: "mind" ,tagc: "new" },
261 {id: "deep" ,emoji: "⚙️" ,title: "Two hours of deep work" ,sub: "the thing that moves the needle" ,due: new Date (Date. now () + 3 * 3600e3 ). toISOString (),tag: "anchor" ,tagc: "deadline" },
262 {id: "move" ,emoji: "🌿" ,title: "Move a little — whatever fits your body" ,tag: "body" }]};
263 const CFG = (window.__BOARD__ && window.__BOARD__.tasks) ? window.__BOARD__ : DEMO ;
264 const NAME = CFG .name || "" ; const baseTasks = CFG .tasks || []; let tasks = baseTasks. slice ();
265 const MANTRAS = {
266 above:[ "Ride the momentum — one block at a time." , "You're in it. Keep the thread." , "Open, curious, moving." , "Great start — let it carry you." ],
267 mid:[ "Progress over perfect." , "One thing at a time." , "Small, real, done. Repeat." , "Just the next step — that's enough." ],
268 below:[ "Small and kind. Just the next tiny step." , "You don't have to feel ready — start gently." , "Lower the bar. Starting counts today." , "Be gentle with yourself; one small thing." ]
269 };
270 const sparks = [ "One block at a time." , "Starting is the win. The rest follows." , "You don't have to feel ready — just start the next one." , "Small, real, done. Repeat." , "Progress over perfect." , "Pick one thing. Just one." ];
271 const KEY = "focus-board-" + ( CFG .dateKey || todayLocal ());
272 const raw = (() => { try { return JSON . parse ((window.localStorage && localStorage. getItem ( KEY )) || "{}" ) || {};} catch (e){ return {};}})();
273 const prefersRM = window.matchMedia && window. matchMedia ( "(prefers-reduced-motion: reduce)" ).matches;
274 let state = {counters:(raw.counters || {}),t:(raw.t || {}),day:(raw.day || []),brain:(raw.brain || []),focus:(raw.focus || null ),rm:(raw.rm !== undefined ? raw.rm : prefersRM),added:(raw.added || []),checkin:(raw.checkin !== undefined ? raw.checkin : ( CFG .checkin || null )),mantra:(raw.mantra !== undefined ? raw.mantra : ( CFG .mantra || "" )),order:(raw.order || []),quad:(raw.quad || {}),tag:(raw.tag || {})};
275 let dragId = null , renderedIds = [];
276 function ensureTaskState (){tasks. forEach ( t => { if ( typeof t.goal === "number" ){ if (state.counters[t.id] === undefined )state.counters[t.id] = (t.start || 0 );}
277 else if ( ! state.t[t.id])state.t[t.id] = {status: "todo" ,notes:[],carried: false }; else if (state.t[t.id].carried === undefined )state.t[t.id].carried = false ; });}
278 function rebuildTasks (){ const seen = {};tasks = baseTasks. concat (state.added || []). filter ( t => { const id = t && t.id; if ( ! validId (id) || seen[id]) return false ;seen[id] = 1 ; return true ;}); ensureTaskState (); if (state.focus &&! tasks. some ( t => t.id === state.focus))state.focus = null ;}
279 rebuildTasks ();
280
281 const cardsEl = document. getElementById ( "cards" ), feedEl = document. getElementById ( "feed" ), brainEl = document. getElementById ( "brain" );
282 document. getElementById ( "h1" ).textContent = NAME ? `Let's go, ${ NAME } 🔥` : "Let's go 🔥" ;
283 document. getElementById ( "spark" ).textContent = sparks[Math. floor (Math. random () * sparks. length )];
284 if (state.rm)document.body.classList. add ( "rm" );
285 document. getElementById ( "rmchip" ).classList. toggle ( "on" , !! state.rm);document. getElementById ( "rmchip" ). setAttribute ( "aria-pressed" ,state.rm ? "true" : "false" );
286 document. getElementById ( "mantrain" ).value = state.mantra || "" ;
287 document. getElementById ( "mantrain" ). addEventListener ( "input" , e => {state.mantra = e.target.value; save ();});
288 document. getElementById ( "mantrasuggest" ). onclick = () => suggestMantra ();
289 document. querySelectorAll ( "[data-checkin]" ). forEach ( b => b. onclick = () => setCheckin (b.dataset.checkin));
290 renderCheckin ();
291 document. getElementById ( "addtaskbtn" ). onclick = () => toggleAddForm ();
292 document. getElementById ( "ncancel" ). onclick = () => toggleAddForm ( false );
293 document. getElementById ( "naddbtn" ). onclick = () => addTask ();
294 document. getElementById ( "ntitle" ). addEventListener ( "keydown" , e => { if (e.key === "Enter" ) addTask ();});
295 document. getElementById ( "niscount" ). addEventListener ( "change" , e => {document. getElementById ( "ncountfields" ).style.display = e.target.checked ? "inline-flex" : "none" ;});
296 document. getElementById ( "prioguide" ). onclick = () => { const l = document. getElementById ( "legend" ); const vis = (l.style.display === "none" ||! l.style.display);l.style.display = vis ? "block" : "none" ;document. getElementById ( "prioguide" ). setAttribute ( "aria-expanded" ,vis ? "true" : "false" );};
297 document. getElementById ( "priosort" ). onclick = () => sortByPriority ();
298 document. getElementById ( "eoddownload" ). onclick = () => downloadRecap ();
299 document. getElementById ( "eodcopy" ). onclick = () => copyRecap ();
300
301 function save (){ try { if (window.localStorage)localStorage. setItem ( KEY , JSON . stringify (state));} catch (e){}}
302 function fmt ( ms ){ return new Date (ms). toLocaleTimeString ( undefined ,{hour: "numeric" ,minute: "2-digit" }). toLowerCase (). replace ( " " , "" );}
303 function esc ( s ){ return (s || "" ). replace ( / [&<>] / g , m => ({ "&" : "&" , "<" : "<" , ">" : ">" }[m]));}
304 const LABEL = {todo: "to do" ,doing: "in progress" ,done: "done" };
305 function isCounter ( t ){ return typeof t.goal === "number" ;}
306 function statusOf ( t ){ if ( isCounter (t)){ const v = state.counters[t.id]; return v >= t.goal ? "done" : (v > 0 ? "doing" : "todo" );} return state.t[t.id].status;}
307 function carriedOf ( t ){ return ! isCounter (t) && state.t[t.id].carried;}
308
309 function dueStr ( t ){
310 if ( ! t.due) return null ;
311 const ms =new Date (t.due). getTime () - Date. now ();
312 if ( statusOf (t) === "done" ) return {cls: "done" ,txt: "⏰ done — nice" };
313 if (ms <= 0 ){ const tm = Math. round ( - ms / 60e3 ), h = Math. floor (tm / 60 ), m = tm % 60 ; return {cls: "over" ,txt: `⏰ was due ${ fmt ( new Date ( t . due )) }${ h || m ? ` · ${ h ? h + "h " : ""}${ m }m ago` : ""} — still worth doing` };}
314 const tm = Math. round (ms / 60e3 ), h = Math. floor (tm / 60 ), m = tm % 60 ;
315 return {cls: "" ,txt: `⏰ ${ h ? h + "h " : ""}${ m }m left (due ${ fmt ( new Date ( t . due )) })` };
316 }
317
318 function render (){
319 cardsEl.innerHTML = "" ;
320 const ordered = [ ... tasks]. sort (( a , b ) => { const ca = carriedOf (a) ? 1 : 0 , cb = carriedOf (b) ? 1 : 0 ; if (ca !== cb) return ca - cb; return orderIndex (a.id) - orderIndex (b.id);});
321 renderedIds = ordered. map ( t => t.id);
322 ordered. forEach ( t => {
323 const st = statusOf (t), carried = carriedOf (t);
324 const c = document. createElement ( "div" );
325 const q = quadOf (t), tag = tagOf (t);
326 c.className = "card " + st + (carried ? " carried" : "" ) + (state.focus === t.id ? " focused" : "" ) + (q ? " q-" + q : "" );
327 c. setAttribute ( "role" , "group" );c. setAttribute ( "aria-label" ,t.title || "task" );
328 c.dataset.cardid = t.id;
329 const dh = dueStr (t);
330 const rmHtml = t.added ? `<button class="rmbtn" data-rmtask="${ t . id }" title="remove this task" aria-label="remove ${ escAttr ( t . title ) }">🗑</button>` : "" ;
331 const metaHtml = `<div class="cardmeta"><span class="grip" draggable="true" data-grip="${ t . id }" title="drag to reorder">⠿</span><button type="button" class="mv" data-move="${ t . id }:-1" title="move up" aria-label="move up">▲</button><button type="button" class="mv" data-move="${ t . id }:1" title="move down" aria-label="move down">▼</button>`
332 + `<select class="quadsel" data-quad="${ t . id }" aria-label="priority for ${ escAttr ( t . title ) }"><option value=""${ q === "" ? " selected" : ""}>priority…</option>`
333 + `<option value="iu"${ q === "iu" ? " selected" : ""}>🔴 Do first</option><option value="ins"${ q === "ins" ? " selected" : ""}>🔵 Schedule</option>`
334 + `<option value="niu"${ q === "niu" ? " selected" : ""}>🟡 Delegate</option><option value="ninu"${ q === "ninu" ? " selected" : ""}>⚪ Later</option></select>`
335 + `<input class="tagedit ${ safeCls ( t . tagc ) }" data-tagedit="${ t . id }" value="${ escAttr ( tag ) }" placeholder="label" aria-label="label for ${ escAttr ( t . title ) }"/></div>` ;
336 if ( isCounter (t)){
337 const v = state.counters[t.id], g = t.goal > 0 ? t.goal : 1 , pct = Math. min ( 100 ,Math. round (v / g * 100 )), inc = t.inc || Math. max ( 1 ,Math. round (g / 10 )), unit = t.unit || "" ;
338 c.innerHTML = `<div class="top"><div class="emoji">${ esc ( t . emoji || "🎯" ) }</div>
339 <div class="body"><div class="title">${ esc ( t . title ) }</div>
340 <div class="sub"><b style="color:var(--ink)">${ v . toLocaleString () }</b> / ${ t . goal . toLocaleString () } ${ esc ( unit ) } · ${ pct }%${ st === "done" ? " — done 🎉" : ""}</div>
341 ${ dh ? `<div class="due ${ dh . cls }" data-duefor="${ t . id }">${ dh . txt }</div>` : ""}</div>
342 <span class="pill ${ st }">${ LABEL [ st ] }</span>
343 <button class="fbtn" data-focus="${ t . id }" title="focus on this" aria-label="focus on ${ escAttr ( t . title ) }">🎯</button>${ rmHtml }</div>
344 <div class="bar"><div class="fill" data-fill="${ t . id }"></div></div>
345 <div class="stepctl">update: <input type="number" data-cin="${ t . id }" value="${ v }" min="0" step="1"/>
346 <button data-cset="${ t . id }">set</button><button data-cinc="${ t . id }">+${ inc . toLocaleString () }</button></div>${ metaHtml }` ;
347 cardsEl. appendChild (c);
348 requestAnimationFrame (() => { const f = cardsEl. querySelector ( `[data-fill="${ t . id }"]` ); if (f)f.style.width = pct + "%" ;});
349 } else {
350 const o = state.t[t.id], notes = o.notes || [];
351 const pillCls = carried ? "carried" : st, pillTxt = carried ? "→ tomorrow" : LABEL [st];
352 c.innerHTML = `<div class="top">
353 <button type="button" class="box ${ st }" data-cyc="${ t . id }" aria-label="cycle status">${ st === "done" ? "✓" : ( st === "doing" ? "…" : "" ) }</button>
354 <div class="emoji">${ esc ( t . emoji || "•" ) }</div>
355 <div class="body"><div class="title">${ esc ( t . title ) }</div>${ t . sub ? `<div class="sub">${ esc ( t . sub ) }</div>` : ""}${ dh ? `<div class="due ${ dh . cls }" data-duefor="${ t . id }">${ dh . txt }</div>` : ""}</div>
356 <button type="button" class="pill ${ pillCls }" data-cyc="${ t . id }">${ pillTxt }</button>
357 <button class="fbtn" data-focus="${ t . id }" title="focus on this" aria-label="focus on ${ escAttr ( t . title ) }">🎯</button>${ rmHtml }</div>
358 ${ notes . length ? `<div class="notes">${ notes . map (( n , i ) => `<div class="note"><span class="nt">${ fmt ( n . t ) }</span><span>${ esc ( n . txt ) }</span><button type="button" class="nx" data-del="${ t . id }:${ i }" aria-label="delete note">×</button></div>` ). join ( "" ) }</div>` : ""}
359 <div class="cardfoot">${ st !== "done" ? `<button class="soft" data-carry="${ t . id }">${ carried ? "↩ bring back to today" : "⤳ not today"}</button>` : ""}</div>
360 <div class="addrow"><input placeholder="log a step…" data-in="${ t . id }"/><button data-add="${ t . id }" title="add note" aria-label="add a note to ${ escAttr ( t . title ) }">+</button></div>${ metaHtml }` ;
361 cardsEl. appendChild (c);
362 }
363 });
364 cardsEl. querySelectorAll ( "[data-cyc]" ). forEach ( el => el. onclick = () => cycle (el.dataset.cyc));
365 cardsEl. querySelectorAll ( "[data-add]" ). forEach ( b => b. onclick = () => { const inp = cardsEl. querySelector ( `[data-in="${ b . dataset . add }"]` ); addNote (b.dataset.add,inp.value);});
366 cardsEl. querySelectorAll ( "[data-in]" ). forEach ( inp => inp. addEventListener ( "keydown" , e => { if (e.key === "Enter" ) addNote (inp.dataset.in,inp.value);}));
367 cardsEl. querySelectorAll ( "[data-del]" ). forEach ( x => x. onclick = () => { const [ id , i ] = x.dataset.del. split ( ":" );state.t[id].notes. splice ( + i, 1 ); save (); render ();});
368 cardsEl. querySelectorAll ( "[data-cset]" ). forEach ( b => b. onclick = () => { const inp = cardsEl. querySelector ( `[data-cin="${ b . dataset . cset }"]` ); setCounter (b.dataset.cset, + inp.value);});
369 cardsEl. querySelectorAll ( "[data-cinc]" ). forEach ( b => b. onclick = () => { const t = tasks. find ( x => x.id === b.dataset.cinc); setCounter (b.dataset.cinc,state.counters[b.dataset.cinc] + (t.inc || Math. max ( 1 ,Math. round (t.goal / 10 ))));});
370 cardsEl. querySelectorAll ( "[data-focus]" ). forEach ( b => b. onclick = () => setFocus (b.dataset.focus));
371 cardsEl. querySelectorAll ( "[data-carry]" ). forEach ( b => b. onclick = () => toggleCarry (b.dataset.carry));
372 cardsEl. querySelectorAll ( "[data-rmtask]" ). forEach ( b => b. onclick = () => removeTask (b.dataset.rmtask));
373 cardsEl. querySelectorAll ( "[data-quad]" ). forEach ( s => s. onchange = () => setQuad (s.dataset.quad,s.value));
374 cardsEl. querySelectorAll ( "[data-tagedit]" ). forEach ( i => i. addEventListener ( "change" ,() => setTag (i.dataset.tagedit,i.value)));
375 cardsEl. querySelectorAll ( ".card" ). forEach ( c => {c. addEventListener ( "dragover" , e => {e. preventDefault ();c.classList. add ( "dragover" );});c. addEventListener ( "dragleave" ,() => c.classList. remove ( "dragover" ));c. addEventListener ( "drop" , e => {e. preventDefault ();c.classList. remove ( "dragover" ); if (dragId) reorder (dragId,c.dataset.cardid);});});
376 cardsEl. querySelectorAll ( "[data-grip]" ). forEach ( g => g. addEventListener ( "dragstart" , e => {dragId = g.dataset.grip; if (e.dataTransfer){e.dataTransfer.effectAllowed = "move" ;e.dataTransfer. setData ( "text/plain" ,dragId);}}));
377 cardsEl. querySelectorAll ( "[data-move]" ). forEach ( b => b. onclick = () => { const p = b.dataset.move. split ( ":" ); moveTask (p[ 0 ], + p[ 1 ]);});
378 renderBrain (); renderFeed (); updateRing (); updateFocusBar (); updateOverload (); tick ();
379 }
380 function renderFeed (){
381 const items = [];
382 tasks. forEach ( t => { if ( isCounter (t)) return ;(state.t[t.id].notes || []). forEach (( n , i ) => items. push ({t:n.t,txt:n.txt,emoji:t.emoji || "•" ,src:t.id,idx:i}));});
383 state.day. forEach (( n , i ) => items. push ({t:n.t,txt:n.txt,emoji: "📌" ,src: "day" ,idx:i}));
384 items. sort (( a , b ) => b.t - a.t);
385 if ( ! items. length ){feedEl.innerHTML = `<div class="empty">No steps logged yet — starting counts. Log your first one 👆</div>` ; return ;}
386 feedEl.innerHTML = items. map ( it => `<div class="fitem"><span class="ft">${ fmt ( it . t ) }</span><span>${ esc ( it . emoji ) }</span><span>${ esc ( it . txt ) }</span><button type="button" class="fx" data-fdel="${ it . src }:${ it . idx }" aria-label="delete">×</button></div>` ). join ( "" );
387 feedEl. querySelectorAll ( "[data-fdel]" ). forEach ( x => x. onclick = () => { const [ src , i ] = x.dataset.fdel. split ( ":" ); if (src === "day" )state.day. splice ( + i, 1 ); else state.t[src].notes. splice ( + i, 1 ); save (); render ();});
388 }
389 function renderBrain (){
390 if ( ! state.brain. length ){brainEl.innerHTML = `<div class="empty">Nothing parked. When a stray thought hits, drop it here and keep going.</div>` ; return ;}
391 brainEl.innerHTML = state.brain. map (( n , i ) => `<div class="fitem think"><span class="ft">${ fmt ( n . t ) }</span><span>💭</span><span>${ esc ( n . txt ) }</span><button type="button" class="fx" data-bdel="${ i }" aria-label="delete">×</button></div>` ). join ( "" );
392 brainEl. querySelectorAll ( "[data-bdel]" ). forEach ( x => x. onclick = () => {state.brain. splice ( + x.dataset.bdel, 1 ); save (); render ();});
393 }
394 function addNote ( id , txt ){txt = (txt || "" ). trim (); if ( ! txt) return ;state.t[id].notes. push ({t:Date. now (),txt}); if (state.t[id].status === "todo" )state.t[id].status = "doing" ; if (state.t[id].carried)state.t[id].carried = false ; save (); render ();}
395 function addDayNote ( txt ){txt = (txt || "" ). trim (); if ( ! txt) return ;state.day. unshift ({t:Date. now (),txt}); save (); render ();}
396 function addBrain ( txt ){txt = (txt || "" ). trim (); if ( ! txt) return ;state.brain. unshift ({t:Date. now (),txt}); save (); render ();}
397 document. getElementById ( "daybtn" ). onclick = () => { const i = document. getElementById ( "dayin" ); addDayNote (i.value);i.value = "" ;};
398 document. getElementById ( "dayin" ). addEventListener ( "keydown" , e => { if (e.key === "Enter" ){ addDayNote (e.target.value);e.target.value = "" ;}});
399 document. getElementById ( "brainbtn" ). onclick = () => { const i = document. getElementById ( "brainin" ); addBrain (i.value);i.value = "" ;};
400 document. getElementById ( "brainin" ). addEventListener ( "keydown" , e => { if (e.key === "Enter" ){ addBrain (e.target.value);e.target.value = "" ;}});
401 function cycle ( id ){ const o = [ "todo" , "doing" , "done" ], nx = o[(o. indexOf (state.t[id].status) + 1 ) % 3 ];state.t[id].status = nx; if (state.t[id].carried)state.t[id].carried = false ; save (); if (nx === "done" ){ burst ( 70 ); checkAll ();} render ();}
402 function setCounter ( id , v ){v = Math. max ( 0 ,Math. round (v || 0 )); const t = tasks. find ( x => x.id === id); const was = state.counters[id] >= t.goal;state.counters[id] = v; save (); if ( ! was && v >= t.goal){ burst ( 160 ); checkAll ();} render ();}
403 function toggleCarry ( id ){state.t[id].carried =! state.t[id].carried; save (); render ();}
404 function setFocus ( id ){state.focus = (state.focus === id ? null : id); save (); render ();}
405 function setCheckin ( v ){state.checkin = (state.checkin === v ? null : v); save (); renderCheckin (); if (state.checkin &&! (state.mantra || "" ). trim ()) suggestMantra ();}
406 function renderCheckin (){document. querySelectorAll ( "[data-checkin]" ). forEach ( b => { const on = b.dataset.checkin === state.checkin;b.classList. toggle ( "on" ,on);b. setAttribute ( "aria-pressed" ,on ? "true" : "false" );}); const h = document. getElementById ( "checkinhint" ); const msg = {below: "Below the line — and that's ok. Be gentle; shrink the first step." ,mid: "Somewhere in between. Just noticing, no judgment." ,above: "Above the line — open and ready. Ride it." }; if (h)h.textContent = state.checkin ? msg[state.checkin] : "Just notice where you're arriving — no wrong answer." ;}
407 function suggestMantra (){ const zone = (state.checkin === "above" || state.checkin === "below" ) ? state.checkin : "mid" ; const list = MANTRAS [zone] || MANTRAS .mid; setMantra (list[Math. floor (Math. random () * list. length )]);}
408 function setMantra ( m ){state.mantra = m; const i = document. getElementById ( "mantrain" ); if (i)i.value = m; save ();}
409 function toggleAddForm ( show ){ const f = document. getElementById ( "addtaskform" ), b = document. getElementById ( "addtaskbtn" ); const on = (show === undefined ) ? (f.style.display === "none" ||! f.style.display) : show;f.style.display = on ? "flex" : "none" ;b.style.display = on ? "none" : "inline-flex" ; if (on)document. getElementById ( "ntitle" ). focus ();}
410 function addTask (){ const ti = document. getElementById ( "ntitle" ), gi = document. getElementById ( "ngoal" ), ui = document. getElementById ( "nunit" ), isc = document. getElementById ( "niscount" ); const title = (ti.value || "" ). trim (); if ( ! title) return ; const id = "u" + Date. now (). toString ( 36 ); const t = {id,title,emoji: "📝" ,added: true }; if (isc.checked &&+ gi.value > 0 ){t.goal =+ gi.value;t.start = 0 ;t.unit = (ui.value || "" ). trim ();t.inc = Math. max ( 1 ,Math. round ( + gi.value / 10 ));}state.added. push (t); save (); rebuildTasks ();ti.value = "" ;gi.value = "" ;ui.value = "" ;isc.checked = false ;document. getElementById ( "ncountfields" ).style.display = "none" ; toggleAddForm ( false ); render ();}
411 function removeTask ( id ){state.added = (state.added || []). filter ( x => x.id !== id); delete state.t[id]; delete state.counters[id]; if (state.focus === id)state.focus = null ; save (); rebuildTasks (); render ();}
412 function todayLocal (){ const d =new Date (); return d. getFullYear () + "-" + String (d. getMonth () + 1 ). padStart ( 2 , "0" ) + "-" + String (d. getDate ()). padStart ( 2 , "0" );}
413 function recapDate (){ if ( CFG .dateKey && / ^ \d {4} - \d {2} - \d {2}$ / . test ( CFG .dateKey)){ const p = CFG .dateKey. split ( "-" ); return new Date ( + p[ 0 ], + p[ 1 ] - 1 , + p[ 2 ]);} return new Date ();}
414 function moveTask ( id , dir ){ let ids = renderedIds. slice (); const i = ids. indexOf (id); if (i < 0 ) return ; const j = i + dir; if (j < 0 || j >= ids. length ) return ; const tmp = ids[i];ids[i] = ids[j];ids[j] = tmp;state.order = ids; save (); render ();}
415 function stopConfetti (){ if (rafId !== null ){ cancelAnimationFrame (rafId);rafId = null ;}parts = []; if (cx)cx. clearRect ( 0 , 0 ,cv.width,cv.height);}
416 function orderIndex ( id ){ const i = (state.order || []). indexOf (id); return i < 0 ? 9999 : i;}
417 function quadOf ( t ){ return state.quad[t.id] !== undefined ? state.quad[t.id] : (t.quad || "" );}
418 function tagOf ( t ){ return state.tag[t.id] !== undefined ? state.tag[t.id] : (t.tag || "" );}
419 function setQuad ( id , v ){state.quad[id] = v; save (); render ();}
420 function setTag ( id , v ){state.tag[id] = v; save (); render ();}
421 function escAttr ( s ){ return esc (s). replace ( /"/ g , """ );}
422 function safeCls ( s ){ return (s || "" ). replace ( / [ ^ a-zA-Z0-9_-] / g , "" );}
423 function validId ( id ){ return typeof id === "string" && / ^ [A-Za-z0-9_-] +$ / . test (id);}
424 function reorder ( from , targetId ){ if ( ! from || from === targetId) return ; let ids = renderedIds. slice (); const fromI = ids. indexOf (from), tgtI = ids. indexOf (targetId); if (fromI < 0 || tgtI < 0 ) return ;ids. splice (fromI, 1 ); let ins = ids. indexOf (targetId); if (fromI < tgtI)ins += 1 ;ids. splice (ins, 0 ,from);state.order = ids; save (); render ();}
425 function sortByPriority (){ const rank = {iu: 0 ,ins: 1 ,niu: 2 ,ninu: 3 , "" : 4 }; const ids = [ ... tasks]. sort (( a , b ) => { const ca = carriedOf (a) ? 1 : 0 , cb = carriedOf (b) ? 1 : 0 ; if (ca !== cb) return ca - cb; const ra = (rank[ quadOf (a)] === undefined ? 4 : rank[ quadOf (a)]), rb = (rank[ quadOf (b)] === undefined ? 4 : rank[ quadOf (b)]); if (ra !== rb) return ra - rb; return orderIndex (a.id) - orderIndex (b.id);}). map ( t => t.id);state.order = ids; save (); render ();}
426 function updateOverload (){ const el = document. getElementById ( "overload" ); if ( ! el) return ; const live = tasks. filter ( t =>! carriedOf (t) && statusOf (t) !== "done" ). length ; if (live > 9 ){el.style.display = "block" ;el.textContent = `That's ${ live } active tasks for today — more than a focus board loves. Consider carrying a few to tomorrow (⤳ not today), or use Focus mode to take one at a time. No pressure — this is a nudge, not a rule.` ;} else {el.style.display = "none" ;}}
427 function dayRecap (){
428 const dstr = recapDate (). toLocaleDateString ( undefined ,{weekday: "long" ,month: "long" ,day: "numeric" ,year: "numeric" });
429 const zoneTxt = {below: "below the line 🌧️" ,mid: "in between ⛅" ,above: "above the line ☀️" };
430 const L = [ `# Focus board — ${ dstr }` , "" ];
431 const head = []; if (state.checkin && zoneTxt[state.checkin])head. push ( `**Arrived:** ${ zoneTxt [ state . checkin ] }` ); if ((state.mantra || "" ). trim ())head. push ( `**Mantra:** "${ state . mantra . trim () }"` );
432 if (head. length ) L . push (head. join ( " · " ), "" );
433 const live = tasks. filter ( t =>! carriedOf (t));
434 const done = live. filter ( t => statusOf (t) === "done" ), doing = live. filter ( t => statusOf (t) === "doing" ), todo = live. filter ( t => statusOf (t) === "todo" ), carried = tasks. filter ( t => carriedOf (t));
435 const line = t => { let extra = "" ; if ( isCounter (t))extra = ` (${ state . counters [ t . id ]. toLocaleString () }/${ t . goal . toLocaleString () }${ t . unit ? " " + t . unit : ""})` ; const lbl = tagOf (t) ? `[${ tagOf ( t ) }] ` : "" ; return `- ${ t . emoji || "•"} ${ lbl }${ t . title }${ extra }` ;};
436 L . push ( `## Done (${ done . length }/${ live . length })` );done. length ? done. forEach ( t => L . push ( line (t))) : L . push ( "- (nothing marked done — and starting still counts)" ); L . push ( "" );
437 if (doing. length ){ L . push ( "## In progress" );doing. forEach ( t => L . push ( line (t))); L . push ( "" );}
438 if (todo. length ){ L . push ( "## Still to do" );todo. forEach ( t => L . push ( line (t))); L . push ( "" );}
439 if (carried. length ){ L . push ( "## Carried to tomorrow (a valid, healthy choice)" );carried. forEach ( t => L . push ( line (t))); L . push ( "" );}
440 const feed = [];tasks. forEach ( t => { if ( isCounter (t)) return ;(state.t[t.id].notes || []). forEach ( n => feed. push ({t:n.t,txt:n.txt,e:t.emoji || "•" }));});state.day. forEach ( n => feed. push ({t:n.t,txt:n.txt,e: "📌" }));feed. sort (( a , b ) => a.t - b.t);
441 if (feed. length ){ L . push ( "## Momentum" );feed. forEach ( f => L . push ( `- ${ fmt ( f . t ) } — ${ f . e } ${ f . txt }` )); L . push ( "" );}
442 if ((state.brain || []). length ){ L . push ( "## Parked thoughts" );state.brain. forEach ( n => L . push ( `- 💭 ${ n . txt }` )); L . push ( "" );}
443 L . push ( `_saved ${ fmt ( Date . now ()) } · 🔥 built with Ember_` );
444 return L . join ( " \n " );
445 }
446 function eodMsg ( m ){ const el = document. getElementById ( "eodmsg" ); if (el)el.textContent = m;}
447 function downloadRecap (){ const md = dayRecap (); try { const blob =new Blob ([md],{type: "text/markdown" }); const url = URL . createObjectURL (blob); const a = document. createElement ( "a" );a.href = url;a.download = "focus-" + ( CFG .dateKey || todayLocal ()) + ".md" ;document.body. appendChild (a);a. click ();a. remove (); setTimeout (() => URL . revokeObjectURL (url), 1000 ); eodMsg ( "saved a recap to your downloads 💾" );} catch (e){ eodMsg ( "couldn't auto-download here — use copy instead 📋" );}}
448 function fallbackCopy ( md , cb ){ try { const ta = document. createElement ( "textarea" );ta.value = md;document.body. appendChild (ta);ta. select (); const ok = document. execCommand ( "copy" );ta. remove (); if (ok){cb && cb ();} else { eodMsg ( "couldn't copy here — try download 💾" );}} catch (e){ eodMsg ( "couldn't copy here — try download 💾" );}}
449 function copyRecap (){ const md = dayRecap (); const ok = () => eodMsg ( "copied — paste it to Ember to journal your day 📋" ); if (navigator.clipboard && navigator.clipboard.writeText){navigator.clipboard. writeText (md). then (ok,() => fallbackCopy (md,ok));} else { fallbackCopy (md,ok);}}
450 document. getElementById ( "focuschip" ). onclick = () => {
451 if (state.focus){state.focus = null ;}
452 else { const cand = tasks. find ( t => statusOf (t) === "doing" &&! carriedOf (t)) || tasks. find ( t => statusOf (t) === "todo" &&! carriedOf (t)) || tasks[ 0 ];state.focus = cand ? cand.id : null ;}
453 save (); render ();
454 };
455 document. getElementById ( "focusexit" ). onclick = () => {state.focus = null ; save (); render ();};
456 document. getElementById ( "rmchip" ). onclick = () => {state.rm =! state.rm;document.body.classList. toggle ( "rm" ,state.rm);document. getElementById ( "rmchip" ).classList. toggle ( "on" ,state.rm);document. getElementById ( "rmchip" ). setAttribute ( "aria-pressed" ,state.rm ? "true" : "false" ); if (state.rm) stopConfetti (); save ();};
457 function checkAll (){ const live = tasks. filter ( t =>! carriedOf (t)); if (live. length && live. every ( t => statusOf (t) === "done" )) setTimeout (() => { burst ( 320 );document. getElementById ( "spark" ).textContent = "Everything you kept for today — done. 🔥" ;}, 250 );}
458 function updateFocusBar (){
459 document.body.classList. toggle ( "focusing" , !! state.focus);
460 { const fc = document. getElementById ( "focuschip" ); if (fc)fc. setAttribute ( "aria-pressed" ,state.focus ? "true" : "false" );}
461 if (state.focus){ const t = tasks. find ( x => x.id === state.focus);document. getElementById ( "focustxt" ).innerHTML = `🎯 Just this one right now: <b>${ esc ( t ? t . title : "" ) }</b>` ;}
462 }
463 function updateRing (){
464 const live = tasks. filter ( t =>! carriedOf (t));
465 const done = live. filter ( t => statusOf (t) === "done" ). length , doing = live. filter ( t => statusOf (t) === "doing" ). length , carried = tasks. length - live. length ;
466 const n = live. length ;
467 document. getElementById ( "ring" ).style.background = `conic-gradient(var(--ember) ${ n ? done / n * 360 : 0 }deg,var(--line) 0deg)` ;
468 document. getElementById ( "ringtxt" ).textContent = done + "/" + n;
469 document. getElementById ( "tally" ).textContent = `${ done } done · ${ doing } in progress · ${ n - done - doing } to go` + (carried ? ` · ${ carried } for tomorrow` : "" );
470 }
471 function tick (){
472 const now =new Date ();
473 document. getElementById ( "date" ).innerHTML = now. toLocaleDateString ( undefined ,{weekday: "long" ,month: "long" ,day: "numeric" }) + ` · <span class="clock">${ fmt ( now . getTime ()) }</span>` ;
474 tasks. forEach ( t => { const el = document. querySelector ( `[data-duefor="${ t . id }"]` ); const dh = dueStr (t); if (el && dh){el.className = "due " + dh.cls;el.textContent = dh.txt;}});
475 }
476 setInterval (tick, 30000 );
477
478 const cv = document. getElementById ( "cc" ), cx = cv. getContext ( "2d" ); let parts = [];
479 function size (){cv.width = innerWidth;cv.height = innerHeight;} addEventListener ( "resize" ,size); size ();
480 const COL = [ "#ff7a3c" , "#ffb23c" , "#37d39a" , "#c3a9ff" , "#ff5d73" , "#eef1fa" ];
481 let rafId = null ;
482 function burst ( n ){ if (state.rm ||! cx) return ; for ( let i = 0 ;i < n;i ++ )parts. push ({x:innerWidth / 2 + (Math. random () - .5 ) * 220 ,y:innerHeight * 0.28 ,vx:(Math. random () - .5 ) * 11 ,vy:Math. random () *- 13 - 4 ,g: .42 ,life: 70 + Math. random () * 40 ,c: COL [i % COL . length ],s: 5 + Math. random () * 6 ,rot:Math. random () * 6 }); if (rafId === null )rafId = requestAnimationFrame (loop);}
483 function loop (){ if ( ! cx){rafId = null ; return ;}cx. clearRect ( 0 , 0 ,cv.width,cv.height);parts. forEach ( p => {p.vy += p.g;p.x += p.vx;p.y += p.vy;p.life -- ;p.rot += .2 ;cx. save ();cx. translate (p.x,p.y);cx. rotate (p.rot);cx.fillStyle = p.c;cx. fillRect ( - p.s / 2 , - p.s / 2 ,p.s,p.s * 1.6 );cx. restore ();});parts = parts. filter ( p => p.life > 0 && p.y < cv.height + 40 );rafId = parts. length ? requestAnimationFrame (loop) : null ;}
484 render ();
485 </ script >
486 </ body >
487 </ html >