a lightweight, interval-based utility to combat digital strain through "Ma" (intentional pauses) for the eyes and body.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(ui): stylize ui, first pass

+1034 -172
+1 -1
src/tray.rs
··· 38 38 menu.append(&item_resume)?; 39 39 menu.append(&PredefinedMenuItem::separator())?; 40 40 41 - let pause_sub = Submenu::new("Pause for", true); 41 + let pause_sub = Submenu::new("Pause breaks for", true); 42 42 let mut pause_items = Vec::new(); 43 43 for (label, secs) in PAUSE_DURATIONS { 44 44 let item = MenuItem::new(*label, true, None);
+1033 -171
ui/settings.slint
··· 1 - import { Button, ComboBox, CheckBox, Slider, TabWidget, LineEdit, SpinBox } from "std-widgets.slint"; 1 + import { SpinBox, Slider, LineEdit } from "std-widgets.slint"; 2 + 3 + // ── Design tokens ──────────────────────────────────────────────────────────── 4 + // bg: #F3EFE7 warm cream 5 + // ink: #232028 near-black with warm cast 6 + // muted: #23202888 (55 % ink) 7 + // line: #2320281A (10 % ink) — hair dividers 8 + // accent: #8A9A82 sage green 9 + // chip: #FFFFFF white chips on profile rows 10 + 11 + // ── Primitives ─────────────────────────────────────────────────────────────── 12 + component PaperToggle { 13 + in-out property <bool> checked: false; 14 + callback toggled(bool); 15 + 16 + width: 38px; 17 + height: 22px; 18 + 19 + TouchArea { 20 + clicked => { 21 + root.checked = !root.checked; 22 + root.toggled(root.checked); 23 + } 24 + } 25 + 26 + Rectangle { 27 + border-radius: 11px; 28 + background: root.checked ? #8A9A82 : #23202820; 29 + animate background { duration: 180ms; } 30 + 31 + Rectangle { 32 + x: root.checked ? 18px : 2px; 33 + y: 2px; 34 + width: 18px; 35 + height: 18px; 36 + border-radius: 9px; 37 + background: white; 38 + drop-shadow-color: #00000026; 39 + drop-shadow-blur: 3px; 40 + drop-shadow-offset-y: 1px; 41 + animate x { 42 + duration: 180ms; 43 + easing: ease-out; 44 + } 45 + } 46 + } 47 + } 48 + 49 + component PaperDivider { 50 + height: 1px; 51 + Rectangle { 52 + background: #2320281A; 53 + } 54 + } 55 + 56 + // A ghost button — paper white, thin line border 57 + component PaperButton { 58 + in property <string> text; 59 + callback clicked; 60 + 61 + height: 28px; 62 + min-width: 100px; 63 + preferred-width: 160px; 64 + 65 + ta := TouchArea { 66 + clicked => { 67 + root.clicked(); 68 + } 69 + } 70 + 71 + Rectangle { 72 + border-radius: 6px; 73 + background: #FFFFFF80; 74 + border-width: 1px; 75 + border-color: #2320281A; 76 + 77 + Text { 78 + text: root.text; 79 + font-size: 12px; 80 + color: #232028; 81 + horizontal-alignment: center; 82 + vertical-alignment: center; 83 + } 84 + } 85 + } 86 + 87 + // Primary filled button (ink bg, cream text) 88 + component PaperPrimaryButton { 89 + in property <string> text; 90 + callback clicked; 91 + 92 + height: 30px; 93 + min-width: 72px; 94 + 95 + ta := TouchArea { 96 + clicked => { 97 + root.clicked(); 98 + } 99 + } 100 + 101 + Rectangle { 102 + border-radius: 6px; 103 + background: ta.has-hover ? #3a3542 : #232028; 104 + animate background { duration: 120ms; } 105 + 106 + Text { 107 + text: root.text; 108 + font-size: 12px; 109 + font-weight: 600; 110 + color: #F3EFE7; 111 + horizontal-alignment: center; 112 + vertical-alignment: center; 113 + } 114 + } 115 + } 116 + 117 + // Cancel-style secondary button 118 + component PaperSecondaryButton { 119 + in property <string> text; 120 + callback clicked; 121 + 122 + height: 30px; 123 + min-width: 72px; 124 + 125 + ta := TouchArea { 126 + clicked => { 127 + root.clicked(); 128 + } 129 + } 130 + 131 + Rectangle { 132 + border-radius: 6px; 133 + background: transparent; 134 + border-width: 1px; 135 + border-color: #2320281A; 136 + 137 + Text { 138 + text: root.text; 139 + font-size: 12px; 140 + color: #232028; 141 + horizontal-alignment: center; 142 + vertical-alignment: center; 143 + } 144 + } 145 + } 146 + 147 + 148 + // ── Interval card (Profile tab) ─────────────────────────────────────────────── 149 + component IntervalCard { 150 + in property <int> index; 151 + in property <int> work-mins; 152 + in property <int> break-mins; 153 + in property <int> break-secs; 154 + in property <string> label; 155 + 156 + callback remove-clicked; 157 + callback work-mins-changed(int); 158 + callback break-mins-changed(int); 159 + callback break-secs-changed(int); 160 + callback label-changed(string); 161 + 162 + height: 58px; 163 + 164 + Rectangle { 165 + border-radius: 8px; 166 + background: #FFFFFF80; 167 + border-width: 1px; 168 + border-color: #2320281A; 169 + 170 + HorizontalLayout { 171 + padding: 12px; 172 + padding-left: 16px; 173 + padding-right: 16px; 174 + spacing: 12px; 175 + alignment: center; 176 + 177 + Text { 178 + text: root.index; 179 + font-size: 18px; 180 + color: #8A9A82; 181 + width: 22px; 182 + horizontal-alignment: center; 183 + vertical-alignment: center; 184 + } 185 + 186 + Text { 187 + text: "every"; 188 + font-size: 12px; 189 + color: #23202880; 190 + vertical-alignment: center; 191 + } 192 + 193 + SpinBox { 194 + width: 72px; 195 + height: 28px; 196 + value: root.work-mins; 197 + minimum: 1; 198 + maximum: 240; 199 + edited(v) => { 200 + root.work-mins-changed(v); 201 + } 202 + } 203 + 204 + Text { 205 + text: "min · pause"; 206 + font-size: 12px; 207 + color: #23202880; 208 + vertical-alignment: center; 209 + } 2 210 3 - component SettingsSpinBox inherits SpinBox { 4 - width: 90px; 211 + SpinBox { 212 + width: 60px; 213 + height: 28px; 214 + value: root.break-mins; 215 + minimum: 0; 216 + maximum: 120; 217 + edited(v) => { 218 + root.break-mins-changed(v); 219 + } 220 + } 221 + 222 + Text { 223 + text: "m"; 224 + font-size: 12px; 225 + color: #23202880; 226 + vertical-alignment: center; 227 + } 228 + 229 + SpinBox { 230 + width: 60px; 231 + height: 28px; 232 + value: root.break-secs; 233 + minimum: 0; 234 + maximum: 59; 235 + edited(v) => { 236 + root.break-secs-changed(v); 237 + } 238 + } 239 + 240 + Text { 241 + text: "s"; 242 + font-size: 12px; 243 + color: #23202880; 244 + vertical-alignment: center; 245 + } 246 + 247 + LineEdit { 248 + width: 120px; 249 + height: 28px; 250 + text: root.label; 251 + edited(v) => { 252 + root.label-changed(v); 253 + } 254 + } 255 + 256 + // Spacer 257 + Rectangle { 258 + horizontal-stretch: 1; 259 + } 260 + 261 + // Remove button 262 + ta-rm := TouchArea { 263 + width: 20px; 264 + height: 20px; 265 + clicked => { 266 + root.remove-clicked(); 267 + } 268 + 269 + Text { 270 + text: "×"; 271 + font-size: 16px; 272 + color: ta-rm.has-hover ? #232028 : #23202860; 273 + horizontal-alignment: center; 274 + vertical-alignment: center; 275 + animate color { duration: 120ms; } 276 + } 277 + } 278 + } 279 + } 280 + } 281 + 282 + // ── Tab button ──────────────────────────────────────────────────────────────── 283 + component TabButton { 284 + in property <string> text; 285 + in property <bool> active: false; 286 + callback clicked; 287 + 288 + height: 40px; 289 + min-width: 64px; 290 + 291 + TouchArea { 292 + clicked => { 293 + root.clicked(); 294 + } 295 + } 296 + 297 + VerticalLayout { 298 + padding-bottom: 0px; 299 + 300 + Text { 301 + text: root.text; 302 + font-size: 13px; 303 + font-weight: root.active ? 600 : 400; 304 + color: root.active ? #232028 : #23202888; 305 + horizontal-alignment: left; 306 + vertical-alignment: center; 307 + } 308 + 309 + // Active indicator 310 + Rectangle { 311 + height: 2px; 312 + background: root.active ? #232028 : transparent; 313 + } 314 + } 5 315 } 6 316 7 317 struct LevelEntry { ··· 11 321 label: string, 12 322 } 13 323 324 + // ═══════════════════════════════════════════════════════════════════════════════ 325 + // Main window 326 + // ═══════════════════════════════════════════════════════════════════════════════ 14 327 export component SettingsWindow inherits Window { 15 328 title: "ioma — Settings"; 16 - width: 620px; 329 + width: 760px; 17 330 height: 540px; 331 + background: #F3EFE7; 18 332 333 + // ── Properties (same contract as before) ───────────────────────────────── 19 334 in-out property <bool> enforced-mode: false; 20 335 in-out property <bool> sound-enabled: true; 21 336 in-out property <float> sound-volume: 0.7; 22 337 in-out property <bool> autostart: false; 23 338 in-out property <bool> idle-detection-enabled: true; 24 339 in-out property <int> idle-threshold-mins: 5; 25 - in-out property <string> active-profile: "Pomodoro"; 26 - in-out property <[string]> profile-names: ["Pomodoro", "52/17", "20-20-20", "Focus+Micro"]; 340 + in-out property <string> active-profile: "20·20·20"; 341 + in-out property <[string]> profile-names: ["20·20·20", "Pomodoro", "52/17", "Focus+Micro"]; 27 342 28 343 in-out property <[LevelEntry]> levels: []; 29 344 in-out property <bool> long-break-enabled: false; 30 - in-out property <int> long-break-after-cycles: 4; 31 - in-out property <int> long-break-duration-mins: 15; 345 + in-out property <int> long-break-after-cycles: 3; 346 + in-out property <int> long-break-duration-mins: 30; 32 347 in-out property <int> long-break-gap-mins: 30; 33 - in-out property <string> long-break-label: "Long break"; 348 + in-out property <string> long-break-label: "Long rest"; 34 349 350 + // ── Callbacks (same contract as before) ────────────────────────────────── 35 351 callback save-clicked(); 36 352 callback cancel-clicked(); 37 353 callback set-password-clicked(); ··· 41 357 callback level-removed(int); 42 358 callback level-added(); 43 359 360 + // ── Internal state ─────────────────────────────────────────────────────── 361 + private property <string> active-tab: "rhythm"; 362 + 44 363 VerticalLayout { 45 - padding: 16px; 46 - spacing: 12px; 364 + // ── Header ────────────────────────────────────────────────────────── 365 + HorizontalLayout { 366 + padding-left: 28px; 367 + padding-right: 28px; 368 + padding-top: 18px; 369 + padding-bottom: 0px; 370 + spacing: 0px; 371 + 372 + HorizontalLayout { 373 + spacing: 10px; 374 + alignment: center; 375 + 376 + Text { 377 + text: "ioma"; 378 + font-size: 22px; 379 + color: #232028; 380 + vertical-alignment: center; 381 + } 382 + 383 + Text { 384 + text: "間"; 385 + font-size: 11px; 386 + color: #23202888; 387 + vertical-alignment: bottom; 388 + } 389 + } 390 + 391 + // Spacer 392 + Rectangle { 393 + horizontal-stretch: 1; 394 + } 395 + 396 + // Window controls (decorative — actual close handled by WM) 397 + HorizontalLayout { 398 + spacing: 6px; 399 + alignment: center; 400 + 401 + for glyph in ["−", "□", "×"]: Rectangle { 402 + width: 22px; 403 + height: 22px; 404 + border-radius: 11px; 405 + background: #00000009; 406 + 407 + Text { 408 + text: glyph; 409 + font-size: 11px; 410 + color: #23202888; 411 + horizontal-alignment: center; 412 + vertical-alignment: center; 413 + } 414 + } 415 + } 416 + } 417 + 418 + // ── Tab bar ────────────────────────────────────────────────────────── 419 + HorizontalLayout { 420 + padding-left: 28px; 421 + padding-right: 28px; 422 + padding-top: 14px; 423 + spacing: 28px; 424 + 425 + TabButton { 426 + text: "Rhythm"; 427 + active: root.active-tab == "rhythm"; 428 + clicked => { 429 + root.active-tab = "rhythm"; 430 + } 431 + } 432 + 433 + TabButton { 434 + text: "Profile"; 435 + active: root.active-tab == "profile"; 436 + clicked => { 437 + root.active-tab = "profile"; 438 + } 439 + } 440 + 441 + TabButton { 442 + text: "About"; 443 + active: root.active-tab == "about"; 444 + clicked => { 445 + root.active-tab = "about"; 446 + } 447 + } 448 + 449 + Rectangle { 450 + horizontal-stretch: 1; 451 + } 452 + } 453 + 454 + // Tab underline 455 + Rectangle { 456 + height: 1px; 457 + background: #2320281A; 458 + } 459 + 460 + // ── Tab content ────────────────────────────────────────────────────── 461 + if root.active-tab == "rhythm": VerticalLayout { 462 + padding-left: 28px; 463 + padding-right: 28px; 464 + padding-top: 20px; 465 + padding-bottom: 16px; 466 + spacing: 0px; 467 + vertical-stretch: 1; 47 468 48 - TabWidget { 469 + // Active profile 470 + HorizontalLayout { 471 + padding-top: 10px; 472 + padding-bottom: 14px; 473 + spacing: 24px; 49 474 50 - // ── Merged general settings tab ────────────────────────────── 51 - Tab { 52 - title: "Settings"; 53 475 VerticalLayout { 54 - alignment: start; 55 - padding: 16px; 56 - spacing: 10px; 476 + alignment: center; 477 + spacing: 3px; 478 + 479 + Text { 480 + text: "Active profile"; 481 + font-size: 13px; 482 + font-weight: 500; 483 + color: #232028; 484 + } 57 485 58 - // Active profile selector 486 + Text { 487 + text: "The cadence ioma follows today."; 488 + font-size: 11.5px; 489 + color: #23202880; 490 + } 491 + } 492 + 493 + Rectangle { 494 + horizontal-stretch: 1; 495 + } 496 + 497 + // Profile chip 498 + Rectangle { 499 + height: 32px; 500 + min-width: 160px; 501 + border-radius: 6px; 502 + background: #FFFFFF80; 503 + border-width: 1px; 504 + border-color: #2320281A; 505 + 59 506 HorizontalLayout { 60 - alignment: start; 507 + padding-left: 12px; 508 + padding-right: 12px; 61 509 spacing: 10px; 62 - Text { text: "Active profile:"; vertical-alignment: center; } 63 - ComboBox { 64 - model: profile-names; 65 - current-value <=> active-profile; 66 - selected(name) => { root.profile-changed(name); } 510 + alignment: center; 511 + 512 + Text { 513 + text: root.active-profile; 514 + font-size: 12.5px; 515 + color: #232028; 516 + vertical-alignment: center; 517 + } 518 + 519 + Text { 520 + text: "every 20 min, look 20 ft for 20 s"; 521 + font-size: 11px; 522 + color: #23202880; 523 + vertical-alignment: center; 67 524 } 68 525 } 526 + } 527 + } 69 528 70 - Rectangle { height: 1px; background: #555555; } 529 + PaperDivider { } 71 530 72 - // Mode 73 - CheckBox { 74 - text: "Enforced mode (full-screen, requires password to unlock early)"; 75 - checked <=> enforced-mode; 531 + // Enforced mode 532 + HorizontalLayout { 533 + padding-top: 12px; 534 + padding-bottom: 4px; 535 + spacing: 24px; 536 + 537 + VerticalLayout { 538 + alignment: center; 539 + spacing: 3px; 540 + Text { 541 + text: "Enforced mode"; 542 + font-size: 13px; 543 + font-weight: 500; 544 + color: #232028; 76 545 } 77 - if enforced-mode : Button { 78 - text: "Set emergency unlock password…"; 79 - clicked => { root.set-password-clicked(); } 546 + 547 + Text { 548 + text: "Full-screen break. Emergency unlock requires a password."; 549 + font-size: 11.5px; 550 + color: #23202880; 80 551 } 552 + } 553 + 554 + Rectangle { 555 + horizontal-stretch: 1; 556 + } 557 + 558 + PaperToggle { 559 + checked <=> root.enforced-mode; 560 + } 561 + } 562 + 563 + HorizontalLayout { 564 + padding-bottom: 12px; 565 + PaperButton { 566 + text: "Set emergency unlock password…"; 567 + preferred-width: 220px; 568 + clicked => { 569 + root.set-password-clicked(); 570 + } 571 + } 572 + } 573 + 574 + PaperDivider { } 575 + 576 + // Pause on idle 577 + HorizontalLayout { 578 + padding-top: 12px; 579 + padding-bottom: 4px; 580 + spacing: 24px; 581 + 582 + VerticalLayout { 583 + alignment: center; 584 + spacing: 3px; 81 585 Text { 82 - text: "In reminder mode a single snooze is allowed per break interval."; 83 - wrap: word-wrap; 84 - color: #888888; 586 + text: "Pause on idle"; 85 587 font-size: 13px; 588 + font-weight: 500; 589 + color: #232028; 86 590 } 87 591 88 - Rectangle { height: 1px; background: #555555; } 592 + Text { 593 + text: "If you've stepped away, ioma waits."; 594 + font-size: 11.5px; 595 + color: #23202880; 596 + } 597 + } 598 + 599 + Rectangle { 600 + horizontal-stretch: 1; 601 + } 89 602 90 - // Idle 91 - CheckBox { 92 - text: "Pause timer when idle"; 93 - checked <=> idle-detection-enabled; 603 + PaperToggle { 604 + checked <=> root.idle-detection-enabled; 605 + } 606 + } 607 + 608 + HorizontalLayout { 609 + padding-bottom: 12px; 610 + spacing: 10px; 611 + alignment: start; 612 + 613 + Text { 614 + text: "threshold"; 615 + font-size: 12px; 616 + color: #23202880; 617 + vertical-alignment: center; 618 + } 619 + 620 + SpinBox { 621 + width: 80px; 622 + height: 26px; 623 + value <=> root.idle-threshold-mins; 624 + minimum: 1; 625 + maximum: 60; 626 + } 627 + 628 + Text { 629 + text: "minutes"; 630 + font-size: 12px; 631 + color: #23202880; 632 + vertical-alignment: center; 633 + } 634 + } 635 + 636 + PaperDivider { } 637 + 638 + // Chime on break start 639 + HorizontalLayout { 640 + padding-top: 12px; 641 + padding-bottom: 4px; 642 + spacing: 24px; 643 + 644 + VerticalLayout { 645 + alignment: center; 646 + spacing: 3px; 647 + Text { 648 + text: "Chime on break start"; 649 + font-size: 13px; 650 + font-weight: 500; 651 + color: #232028; 94 652 } 95 - HorizontalLayout { 96 - alignment: start; 97 - spacing: 8px; 98 - Text { text: "Idle threshold:"; vertical-alignment: center; } 99 - SettingsSpinBox { 100 - value <=> idle-threshold-mins; 101 - minimum: 1; 102 - maximum: 60; 103 - } 104 - Text { text: "min"; vertical-alignment: center; } 653 + 654 + Text { 655 + text: "A single, gentle tone."; 656 + font-size: 11.5px; 657 + color: #23202880; 105 658 } 659 + } 106 660 107 - Rectangle { height: 1px; background: #555555; } 661 + Rectangle { 662 + horizontal-stretch: 1; 663 + } 664 + 665 + PaperToggle { 666 + checked <=> root.sound-enabled; 667 + } 668 + } 669 + 670 + HorizontalLayout { 671 + padding-bottom: 12px; 672 + spacing: 12px; 673 + alignment: start; 674 + 675 + Text { 676 + text: "volume"; 677 + font-size: 12px; 678 + color: #23202880; 679 + vertical-alignment: center; 680 + } 681 + 682 + // Custom slim slider track 683 + slider-area := Rectangle { 684 + width: 200px; 685 + height: 16px; 108 686 109 - // Sound 110 - CheckBox { 111 - text: "Play sound on break start"; 112 - checked <=> sound-enabled; 687 + // Track 688 + Rectangle { 689 + y: (parent.height - 2px) / 2; 690 + height: 2px; 691 + background: #23202820; 692 + border-radius: 1px; 113 693 } 114 - HorizontalLayout { 115 - alignment: start; 116 - spacing: 8px; 117 - Text { text: "Volume:"; vertical-alignment: center; } 118 - Slider { 119 - width: 200px; 120 - value <=> sound-volume; 121 - minimum: 0.0; 122 - maximum: 1.0; 123 - } 694 + 695 + // Fill 696 + Rectangle { 697 + y: (parent.height - 2px) / 2; 698 + width: parent.width * root.sound-volume; 699 + height: 2px; 700 + background: #8A9A82; 701 + border-radius: 1px; 702 + animate width { duration: 80ms; } 124 703 } 125 704 126 - Rectangle { height: 1px; background: #555555; } 705 + // Thumb 706 + Rectangle { 707 + x: parent.width * root.sound-volume - 6px; 708 + y: (parent.height - 12px) / 2; 709 + width: 12px; 710 + height: 12px; 711 + border-radius: 6px; 712 + background: white; 713 + border-width: 1.5px; 714 + border-color: #8A9A82; 715 + drop-shadow-blur: 2px; 716 + drop-shadow-color: #00000020; 127 717 128 - // System 129 - CheckBox { 130 - text: "Start ioma automatically at login"; 131 - checked <=> autostart; 718 + animate x { duration: 80ms; } 132 719 } 133 - Button { 134 - text: "Open config file location"; 135 - clicked => { root.open-config-dir(); } 720 + 721 + // Interaction 722 + TouchArea { 723 + moved => { 724 + if self.pressed { 725 + root.sound-volume = max(0.0, min(1.0, self.mouse-x / parent.width)); 726 + } 727 + } 728 + pointer-event(e) => { 729 + if e.kind == PointerEventKind.down { 730 + root.sound-volume = max(0.0, min(1.0, self.mouse-x / parent.width)); 731 + } 732 + } 136 733 } 137 734 } 138 735 } 139 736 140 - // ── Profile level editor ───────────────────────────────────── 141 - Tab { 142 - title: "Profile"; 737 + PaperDivider { } 738 + 739 + // Launch with system 740 + HorizontalLayout { 741 + padding-top: 12px; 742 + padding-bottom: 10px; 743 + spacing: 24px; 744 + 143 745 VerticalLayout { 144 - alignment: start; 145 - padding: 16px; 146 - spacing: 10px; 746 + alignment: center; 747 + spacing: 3px; 748 + Text { 749 + text: "Launch with system"; 750 + font-size: 13px; 751 + font-weight: 500; 752 + color: #232028; 753 + } 147 754 148 - Text { text: "Break levels:"; font-weight: 600; } 755 + Text { 756 + text: "ioma starts quietly at login."; 757 + font-size: 11.5px; 758 + color: #23202880; 759 + } 760 + } 149 761 150 - for level[i] in levels : HorizontalLayout { 151 - spacing: 8px; 152 - alignment: start; 153 - SettingsSpinBox { 154 - width: 104px; 155 - value: level.work-mins; 156 - minimum: 1; 157 - maximum: 240; 158 - edited(v) => { root.level-changed(i, { work-mins: v, break-mins: level.break-mins, break-extra-secs: level.break-extra-secs, label: level.label }); } 159 - } 160 - Text { text: "min →"; vertical-alignment: center; } 161 - SettingsSpinBox { 162 - value: level.break-mins; 163 - minimum: 0; 164 - maximum: 120; 165 - edited(v) => { root.level-changed(i, { work-mins: level.work-mins, break-mins: v, break-extra-secs: level.break-extra-secs, label: level.label }); } 166 - } 167 - Text { text: "m"; vertical-alignment: center; } 168 - SettingsSpinBox { 169 - value: level.break-extra-secs; 170 - minimum: 0; 171 - maximum: 59; 172 - edited(v) => { root.level-changed(i, { work-mins: level.work-mins, break-mins: level.break-mins, break-extra-secs: v, label: level.label }); } 173 - } 174 - Text { text: "s break"; vertical-alignment: center; } 175 - LineEdit { 176 - width: 130px; 177 - text: level.label; 178 - edited(v) => { root.level-changed(i, { work-mins: level.work-mins, break-mins: level.break-mins, break-extra-secs: level.break-extra-secs, label: v }); } 179 - } 180 - Button { 181 - width: 32px; 182 - text: "✕"; 183 - clicked => { root.level-removed(i); } 184 - } 762 + Rectangle { 763 + horizontal-stretch: 1; 764 + } 765 + 766 + PaperToggle { 767 + checked <=> root.autostart; 768 + } 769 + } 770 + 771 + HorizontalLayout { 772 + padding-bottom: 4px; 773 + PaperButton { 774 + text: "Open config file location"; 775 + preferred-width: 180px; 776 + clicked => { 777 + root.open-config-dir(); 185 778 } 779 + } 780 + } 186 781 187 - Button { 188 - text: "+ Add level"; 189 - clicked => { root.level-added(); } 782 + // Spacer 783 + Rectangle { 784 + vertical-stretch: 1; 785 + } 786 + } 787 + 788 + if root.active-tab == "profile": VerticalLayout { 789 + padding-left: 28px; 790 + padding-right: 28px; 791 + padding-top: 24px; 792 + padding-bottom: 16px; 793 + spacing: 0px; 794 + vertical-stretch: 1; 795 + 796 + Text { 797 + text: "Break cadence"; 798 + font-size: 13px; 799 + font-weight: 500; 800 + color: #232028; 801 + } 802 + 803 + Text { 804 + text: "Each row is an interval. ioma cycles through them in order."; 805 + font-size: 11.5px; 806 + color: #23202880; 807 + } 808 + 809 + Rectangle { 810 + height: 14px; 811 + } 812 + 813 + // Interval rows 814 + for level[i] in root.levels: VerticalLayout { 815 + spacing: 0px; 816 + 817 + IntervalCard { 818 + index: i + 1; 819 + work-mins: level.work-mins; 820 + break-mins: level.break-mins; 821 + break-secs: level.break-extra-secs; 822 + label: level.label; 823 + remove-clicked => { 824 + root.level-removed(i); 825 + } 826 + work-mins-changed(v) => { 827 + root.level-changed(i, { 828 + work-mins: v, 829 + break-mins: level.break-mins, 830 + break-extra-secs: level.break-extra-secs, 831 + label: level.label 832 + }); 833 + } 834 + break-mins-changed(v) => { 835 + root.level-changed(i, { 836 + work-mins: level.work-mins, 837 + break-mins: v, 838 + break-extra-secs: level.break-extra-secs, 839 + label: level.label 840 + }); 190 841 } 842 + break-secs-changed(v) => { 843 + root.level-changed(i, { 844 + work-mins: level.work-mins, 845 + break-mins: level.break-mins, 846 + break-extra-secs: v, 847 + label: level.label 848 + }); 849 + } 850 + label-changed(v) => { 851 + root.level-changed(i, { 852 + work-mins: level.work-mins, 853 + break-mins: level.break-mins, 854 + break-extra-secs: level.break-extra-secs, 855 + label: v 856 + }); 857 + } 858 + } 191 859 192 - Rectangle { height: 1px; background: #555555; } 860 + Rectangle { 861 + height: 10px; 862 + } 863 + } 864 + 865 + // Add interval 866 + add-ta := TouchArea { 867 + height: 42px; 868 + clicked => { 869 + root.level-added(); 870 + } 871 + 872 + Rectangle { 873 + border-radius: 8px; 874 + background: add-ta.has-hover ? #FFFFFF60 : transparent; 875 + border-width: 1px; 876 + border-color: #23202828; 877 + // dashed look via inner text (Slint has no native dashed borders) 878 + 879 + Text { 880 + text: "+ Add interval"; 881 + font-size: 12px; 882 + color: #23202880; 883 + horizontal-alignment: center; 884 + vertical-alignment: center; 885 + animate color { duration: 120ms; } 886 + } 887 + } 888 + } 889 + 890 + Rectangle { 891 + height: 20px; 892 + } 893 + 894 + Rectangle { 895 + height: 1px; 896 + background: #2320281A; 897 + } 898 + 899 + Rectangle { 900 + height: 20px; 901 + } 902 + 903 + // Long rest section 904 + Text { 905 + text: "Long rest"; 906 + font-size: 13px; 907 + font-weight: 500; 908 + color: #232028; 909 + } 910 + 911 + Rectangle { 912 + height: 10px; 913 + } 914 + 915 + HorizontalLayout { 916 + spacing: 8px; 917 + alignment: start; 918 + 919 + Text { 920 + text: "after"; 921 + font-size: 12.5px; 922 + color: #23202880; 923 + vertical-alignment: center; 924 + } 925 + 926 + SpinBox { 927 + width: 72px; 928 + height: 28px; 929 + value <=> root.long-break-after-cycles; 930 + minimum: 2; 931 + maximum: 20; 932 + } 933 + 934 + Text { 935 + text: "cycles, take"; 936 + font-size: 12.5px; 937 + color: #23202880; 938 + vertical-alignment: center; 939 + } 940 + 941 + SpinBox { 942 + width: 72px; 943 + height: 28px; 944 + value <=> root.long-break-duration-mins; 945 + minimum: 1; 946 + maximum: 120; 947 + } 948 + 949 + Text { 950 + text: "min"; 951 + font-size: 12.5px; 952 + color: #23202880; 953 + vertical-alignment: center; 954 + } 955 + 956 + Text { 957 + text: "—"; 958 + font-size: 12.5px; 959 + color: #23202860; 960 + vertical-alignment: center; 961 + } 962 + 963 + Text { 964 + text: root.long-break-label; 965 + font-size: 12.5px; 966 + color: #23202880; 967 + font-italic: true; 968 + vertical-alignment: center; 969 + } 970 + } 971 + 972 + Rectangle { 973 + height: 10px; 974 + } 975 + 976 + Text { 977 + text: "Reset the counter if you've been away longer than \{root.long-break-gap-mins} minutes."; 978 + font-size: 11.5px; 979 + color: #23202880; 980 + wrap: word-wrap; 981 + } 982 + 983 + // Spacer 984 + Rectangle { 985 + vertical-stretch: 1; 986 + } 987 + } 988 + 989 + if root.active-tab == "about": VerticalLayout { 990 + alignment: center; 991 + padding: 40px; 992 + spacing: 0px; 993 + vertical-stretch: 1; 193 994 194 - CheckBox { 195 - text: "Long break (after multiple cycles)"; 196 - checked <=> long-break-enabled; 995 + // Logo (larger, centered) 996 + HorizontalLayout { 997 + alignment: center; 998 + Rectangle { 999 + width: 56px; 1000 + height: 56px; 1001 + 1002 + // Left dot 1003 + Rectangle { 1004 + x: (9px - 5px) * 56 / 64; 1005 + y: (32px - 5px) * 56 / 64; 1006 + width: 10px * 56 / 64; 1007 + height: 10px * 56 / 64; 1008 + border-radius: 5px * 56 / 64; 1009 + background: #232028; 197 1010 } 198 1011 199 - if long-break-enabled : VerticalLayout { 200 - spacing: 8px; 201 - padding-left: 16px; 1012 + Rectangle { 1013 + x: (32px - 8px) * 56 / 64; 1014 + y: (32px - 8px) * 56 / 64; 1015 + width: 16px * 56 / 64; 1016 + height: 16px * 56 / 64; 1017 + border-radius: 8px * 56 / 64; 1018 + background: #232028; 1019 + } 202 1020 203 - HorizontalLayout { 204 - alignment: start; 205 - spacing: 6px; 206 - Text { text: "After"; vertical-alignment: center; } 207 - SettingsSpinBox { 208 - value <=> long-break-after-cycles; 209 - minimum: 2; 210 - maximum: 20; 211 - } 212 - Text { text: "cycles, take a"; vertical-alignment: center; } 213 - SettingsSpinBox { 214 - value <=> long-break-duration-mins; 215 - minimum: 1; 216 - maximum: 120; 217 - } 218 - Text { text: "min break"; vertical-alignment: center; } 219 - } 1021 + Rectangle { 1022 + x: (55px - 5px) * 56 / 64; 1023 + y: (32px - 5px) * 56 / 64; 1024 + width: 10px * 56 / 64; 1025 + height: 10px * 56 / 64; 1026 + border-radius: 5px * 56 / 64; 1027 + background: #232028; 1028 + } 220 1029 221 - HorizontalLayout { 222 - alignment: start; 223 - spacing: 6px; 224 - Text { text: "Reset cycle count if gap >"; vertical-alignment: center; } 225 - SettingsSpinBox { 226 - value <=> long-break-gap-mins; 227 - minimum: 5; 228 - maximum: 240; 229 - } 230 - Text { text: "min"; vertical-alignment: center; } 231 - } 1030 + Rectangle { 1031 + x: 17px * 56 / 64; 1032 + y: (32px - 1px) * 56 / 64; 1033 + width: 6px * 56 / 64; 1034 + height: 2px * 56 / 64; 1035 + border-radius: 1px; 1036 + background: #232028; 1037 + } 232 1038 233 - HorizontalLayout { 234 - alignment: start; 235 - spacing: 6px; 236 - Text { text: "Label:"; vertical-alignment: center; } 237 - LineEdit { 238 - width: 200px; 239 - text <=> long-break-label; 240 - } 241 - } 1039 + Rectangle { 1040 + x: 41px * 56 / 64; 1041 + y: (32px - 1px) * 56 / 64; 1042 + width: 6px * 56 / 64; 1043 + height: 2px * 56 / 64; 1044 + border-radius: 1px; 1045 + background: #232028; 242 1046 } 243 1047 } 244 1048 } 1049 + 1050 + Rectangle { 1051 + height: 12px; 1052 + } 1053 + 1054 + Text { 1055 + text: "ioma"; 1056 + font-size: 34px; 1057 + color: #232028; 1058 + horizontal-alignment: center; 1059 + } 1060 + 1061 + Rectangle { 1062 + height: 4px; 1063 + } 1064 + 1065 + Text { 1066 + text: "v0.4.2 · the space between"; 1067 + font-size: 12px; 1068 + color: #23202880; 1069 + horizontal-alignment: center; 1070 + } 1071 + 1072 + Rectangle { 1073 + height: 28px; 1074 + } 1075 + 1076 + Text { 1077 + text: "間 (ma) is the Japanese notion of the pause between —\nthe rest in music, the silence between words.\nioma gives that pause back to your eyes and body."; 1078 + font-size: 12.5px; 1079 + color: #23202880; 1080 + horizontal-alignment: center; 1081 + vertical-alignment: center; 1082 + wrap: word-wrap; 1083 + // line-height not available in Slint — spacing comes from font metrics 1084 + } 1085 + } 1086 + 1087 + // ── Footer ─────────────────────────────────────────────────────────── 1088 + Rectangle { 1089 + height: 1px; 1090 + background: #2320281A; 245 1091 } 246 1092 247 1093 HorizontalLayout { 248 - alignment: end; 249 - spacing: 8px; 1094 + padding-left: 24px; 1095 + padding-right: 24px; 1096 + padding-top: 12px; 1097 + padding-bottom: 14px; 1098 + spacing: 0px; 250 1099 251 - Button { 252 - text: "Cancel"; 253 - clicked => { root.cancel-clicked(); } 1100 + Rectangle { 1101 + horizontal-stretch: 1; 254 1102 } 255 - Button { 256 - text: "Save"; 257 - clicked => { root.save-clicked(); } 1103 + 1104 + HorizontalLayout { 1105 + spacing: 8px; 1106 + 1107 + PaperSecondaryButton { 1108 + text: "Cancel"; 1109 + clicked => { 1110 + root.cancel-clicked(); 1111 + } 1112 + } 1113 + 1114 + PaperPrimaryButton { 1115 + text: "Save"; 1116 + clicked => { 1117 + root.save-clicked(); 1118 + } 1119 + } 258 1120 } 259 1121 } 260 1122 }