docs: remove references to the source app and recording
Prepare for open-sourcing: scrub the prior-art attribution from the spec, agent guide, and code comments so the project documents its own behavior. - Reword RULES_FEATURE_SPEC.md to describe OpenAppLock's rules feature directly (drop the screen-recording source line and "as observed"/ "not demoed"/"as shown" framing) - Replace "the reference app/UI/style/defaults" comments and test names with direct descriptions of the behavior - No behavior change; comments, docs, and @Test display names only Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
12
AGENTS.md
12
AGENTS.md
@@ -3,8 +3,8 @@
|
|||||||
OpenAppLock is an iOS Screen Time app: recurring **rules** that block selected
|
OpenAppLock is an iOS Screen Time app: recurring **rules** that block selected
|
||||||
apps (Schedule windows, Time Limits, Open Limits), with a **Hard Mode** that
|
apps (Schedule windows, Time Limits, Open Limits), with a **Hard Mode** that
|
||||||
makes an active block impossible to lift, edit, or delete until it ends. The
|
makes an active block impossible to lift, edit, or delete until it ends. The
|
||||||
feature set is a clone of Opal's "Rules"; the presentation is bare native iOS
|
presentation is bare native iOS (List/Form/NavigationStack, default color
|
||||||
(List/Form/NavigationStack, default color scheme).
|
scheme).
|
||||||
|
|
||||||
## Repo layout
|
## Repo layout
|
||||||
|
|
||||||
@@ -39,9 +39,9 @@ OpenAppLockShieldAction/ ShieldAction extension: Open press spends an open,
|
|||||||
OpenAppLockTests/ Swift Testing unit suites (@MainActor — the app
|
OpenAppLockTests/ Swift Testing unit suites (@MainActor — the app
|
||||||
target defaults to MainActor isolation)
|
target defaults to MainActor isolation)
|
||||||
OpenAppLockUITests/ XCUITest flows (see harness below)
|
OpenAppLockUITests/ XCUITest flows (see harness below)
|
||||||
docs/RULES_FEATURE_SPEC.md Feature spec derived from the Opal reference
|
docs/RULES_FEATURE_SPEC.md Feature spec for the rules behavior; §6 maps it
|
||||||
recording; §6 maps it to the native presentation.
|
to the native presentation. Review/update this
|
||||||
Review/update this BEFORE behavior changes.
|
BEFORE behavior changes.
|
||||||
docs/SWIFT_GUIDELINES.md Swift coding/testing/patterns/security standards
|
docs/SWIFT_GUIDELINES.md Swift coding/testing/patterns/security standards
|
||||||
agents must follow on this project.
|
agents must follow on this project.
|
||||||
```
|
```
|
||||||
@@ -53,7 +53,7 @@ docs/SWIFT_GUIDELINES.md Swift coding/testing/patterns/security standards
|
|||||||
*starts* on. `start == end` = 24h window.
|
*starts* on. `start == end` = 24h window.
|
||||||
- Status is always **derived** (`rule.status(at:calendar:)`), never stored:
|
- Status is always **derived** (`rule.status(at:calendar:)`), never stored:
|
||||||
`disabled / dormant / active(until:) / paused(until:) / upcoming(startsAt:)`.
|
`disabled / dormant / active(until:) / paused(until:) / upcoming(startsAt:)`.
|
||||||
Labels match the reference app ("6h left" rounds hours **up**).
|
Countdown labels round hours **up** (e.g. "6h left").
|
||||||
- **Hard Mode**: `RulePolicy` is the single gate — while a hard-mode rule is
|
- **Hard Mode**: `RulePolicy` is the single gate — while a hard-mode rule is
|
||||||
actively blocking, canEdit/canDisable/canDelete/canUnblock are all false.
|
actively blocking, canEdit/canDisable/canDelete/canUnblock are all false.
|
||||||
Soft rules can be "unblocked", which sets `pausedUntil` = window end (the
|
Soft rules can be "unblocked", which sets `pausedUntil` = window end (the
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ enum RuleStatus: Equatable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compact countdown matching the reference app: minutes under an hour,
|
/// Compact countdown: minutes under an hour,
|
||||||
/// hours (rounded up) under two days, then days.
|
/// hours (rounded up) under two days, then days.
|
||||||
static func countdown(from now: Date, to target: Date) -> String {
|
static func countdown(from now: Date, to target: Date) -> String {
|
||||||
let minutes = max(1, Int(ceil(target.timeIntervalSince(now) / 60)))
|
let minutes = max(1, Int(ceil(target.timeIntervalSince(now) / 60)))
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ final class BlockingRule {
|
|||||||
self.dayNumbers = days.map(\.rawValue).sorted()
|
self.dayNumbers = days.map(\.rawValue).sorted()
|
||||||
self.pausedUntil = pausedUntil
|
self.pausedUntil = pausedUntil
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
// Raw per-kind columns start at the reference defaults, then the
|
// Raw per-kind columns start at the default values, then the
|
||||||
// configuration overwrites the ones that apply to its kind.
|
// configuration overwrites the ones that apply to its kind.
|
||||||
self.kindRaw = configuration.kind.rawValue
|
self.kindRaw = configuration.kind.rawValue
|
||||||
self.startMinutes = 9 * 60
|
self.startMinutes = 9 * 60
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ struct RuleDraft: Hashable {
|
|||||||
|
|
||||||
var kind: RuleKind { configuration.kind }
|
var kind: RuleKind { configuration.kind }
|
||||||
|
|
||||||
/// A fresh draft for a new rule of the given kind, using the reference
|
/// A fresh draft for a new rule of the given kind, using the default
|
||||||
/// app's defaults (9–5 weekdays schedule, 45m/day, 5 opens/day).
|
/// values (9–5 weekdays schedule, 45m/day, 5 opens/day).
|
||||||
init(kind: RuleKind) {
|
init(kind: RuleKind) {
|
||||||
self.name = kind.defaultRuleName
|
self.name = kind.defaultRuleName
|
||||||
self.days = Weekday.weekdays
|
self.days = Weekday.weekdays
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// A suggested schedule rule shown in the New Rule sheet's preset gallery,
|
/// A suggested schedule rule shown in the New Rule sheet's preset gallery.
|
||||||
/// mirroring the reference app's sections and timings.
|
|
||||||
struct RulePreset: Identifiable, Hashable, Sendable {
|
struct RulePreset: Identifiable, Hashable, Sendable {
|
||||||
let id: String
|
let id: String
|
||||||
let name: String
|
let name: String
|
||||||
@@ -14,7 +13,7 @@ struct RulePreset: Identifiable, Hashable, Sendable {
|
|||||||
let endMinutes: Int
|
let endMinutes: Int
|
||||||
let days: Set<Weekday>
|
let days: Set<Weekday>
|
||||||
let symbolName: String
|
let symbolName: String
|
||||||
/// Gradient stand-in for the reference app's photo backgrounds.
|
/// Gradient background shown behind each preset card.
|
||||||
let gradientTop: Color
|
let gradientTop: Color
|
||||||
let gradientBottom: Color
|
let gradientBottom: Color
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ struct RuleConfigurationTests {
|
|||||||
#expect(RuleConfiguration.openLimit(OpenLimitConfig()).kind == .openLimit)
|
#expect(RuleConfiguration.openLimit(OpenLimitConfig()).kind == .openLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Defaults match the reference app's new-rule defaults")
|
@Test("Defaults match the documented new-rule defaults")
|
||||||
func defaults() {
|
func defaults() {
|
||||||
let schedule = RuleConfiguration.default(for: .schedule).scheduleConfig
|
let schedule = RuleConfiguration.default(for: .schedule).scheduleConfig
|
||||||
#expect(schedule?.startMinutes == 9 * 60)
|
#expect(schedule?.startMinutes == 9 * 60)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Testing
|
|||||||
@MainActor
|
@MainActor
|
||||||
@Suite("BlockingRule model & persistence")
|
@Suite("BlockingRule model & persistence")
|
||||||
struct RuleModelTests {
|
struct RuleModelTests {
|
||||||
@Test("Defaults match the reference app's new-rule defaults")
|
@Test("Defaults match the documented new-rule defaults")
|
||||||
func defaults() {
|
func defaults() {
|
||||||
let rule = BlockingRule(name: "In the Zone")
|
let rule = BlockingRule(name: "In the Zone")
|
||||||
#expect(rule.kind == .schedule)
|
#expect(rule.kind == .schedule)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Testing
|
|||||||
@MainActor
|
@MainActor
|
||||||
@Suite("Rule status derivation and labels")
|
@Suite("Rule status derivation and labels")
|
||||||
struct RuleStatusTests {
|
struct RuleStatusTests {
|
||||||
/// 09:00–17:00 weekdays schedule rule, like the video's "Work Time".
|
/// 09:00–17:00 weekdays schedule rule (the "Work Time" preset).
|
||||||
func workTime(hardMode: Bool = false) -> BlockingRule {
|
func workTime(hardMode: Bool = false) -> BlockingRule {
|
||||||
BlockingRule(name: "Work Time", hardMode: hardMode)
|
BlockingRule(name: "Work Time", hardMode: hardMode)
|
||||||
}
|
}
|
||||||
@@ -71,16 +71,16 @@ struct RuleStatusTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Active label matches the reference style: hours round up")
|
@Test("Active label rounds hours up")
|
||||||
func activeLabel() {
|
func activeLabel() {
|
||||||
// 11:28 → 17:00 is 5h32m; the reference app shows "6h left".
|
// 11:28 → 17:00 is 5h32m; rounds up to "6h left".
|
||||||
let status = workTime().status(at: date(2025, 1, 6, 11, 28), calendar: utc)
|
let status = workTime().status(at: date(2025, 1, 6, 11, 28), calendar: utc)
|
||||||
#expect(status.label(relativeTo: date(2025, 1, 6, 11, 28)) == "6h left")
|
#expect(status.label(relativeTo: date(2025, 1, 6, 11, 28)) == "6h left")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Upcoming label matches the reference style")
|
@Test("Upcoming label formats hours until start")
|
||||||
func upcomingLabel() {
|
func upcomingLabel() {
|
||||||
// Friday 11:28 → Saturday 09:00 is 21h32m; the reference shows "Starts in 22h".
|
// Friday 11:28 → Saturday 09:00 is 21h32m; rounds up to "Starts in 22h".
|
||||||
let weekend = BlockingRule(name: "Weekend Zen", days: Weekday.weekends)
|
let weekend = BlockingRule(name: "Weekend Zen", days: Weekday.weekends)
|
||||||
let friday = date(2025, 1, 10, 11, 28)
|
let friday = date(2025, 1, 10, 11, 28)
|
||||||
#expect(weekend.status(at: friday, calendar: utc).label(relativeTo: friday) == "Starts in 22h")
|
#expect(weekend.status(at: friday, calendar: utc).label(relativeTo: friday) == "Starts in 22h")
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ enum RuleConfiguration: Hashable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The default configuration for a brand-new rule of the given kind, using
|
/// The default configuration for a brand-new rule of the given kind
|
||||||
/// the reference app's defaults (9–5 schedule, 45m/day, 5 opens/day).
|
/// (9–5 schedule, 45m/day, 5 opens/day).
|
||||||
static func `default`(for kind: RuleKind) -> RuleConfiguration {
|
static func `default`(for kind: RuleKind) -> RuleConfiguration {
|
||||||
switch kind {
|
switch kind {
|
||||||
case .schedule: .schedule(ScheduleConfig())
|
case .schedule: .schedule(ScheduleConfig())
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// The three kinds of blocking rules, mirroring the reference app's "New Rule" sheet.
|
/// The three kinds of blocking rules offered on the "New Rule" sheet.
|
||||||
enum RuleKind: String, Codable, CaseIterable, Sendable {
|
enum RuleKind: String, Codable, CaseIterable, Sendable {
|
||||||
/// Block selected apps during a recurring time window.
|
/// Block selected apps during a recurring time window.
|
||||||
case schedule
|
case schedule
|
||||||
@@ -38,7 +38,7 @@ enum RuleKind: String, Codable, CaseIterable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default name given to a brand-new rule of this kind (Opal: "In the Zone", "Time Keeper").
|
/// Default name given to a brand-new rule of this kind (e.g. "In the Zone", "Time Keeper").
|
||||||
var defaultRuleName: String {
|
var defaultRuleName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .schedule: "In the Zone"
|
case .schedule: "In the Zone"
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ struct RuleSchedule: Hashable, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension RuleSchedule {
|
extension RuleSchedule {
|
||||||
/// "09:00" style label for a minutes-from-midnight value, matching the reference UI.
|
/// "09:00" style label for a minutes-from-midnight value.
|
||||||
static func timeLabel(forMinutes minutes: Int) -> String {
|
static func timeLabel(forMinutes minutes: Int) -> String {
|
||||||
let clamped = ((minutes % (24 * 60)) + 24 * 60) % (24 * 60)
|
let clamped = ((minutes % (24 * 60)) + 24 * 60) % (24 * 60)
|
||||||
return String(format: "%02d:%02d", clamped / 60, clamped % 60)
|
return String(format: "%02d:%02d", clamped / 60, clamped % 60)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ enum Weekday: Int, CaseIterable, Codable, Hashable, Sendable {
|
|||||||
static let weekends: Set<Weekday> = [.saturday, .sunday]
|
static let weekends: Set<Weekday> = [.saturday, .sunday]
|
||||||
static let everyDay: Set<Weekday> = Set(Weekday.allCases)
|
static let everyDay: Set<Weekday> = Set(Weekday.allCases)
|
||||||
|
|
||||||
/// Display order used by the day picker, matching the reference UI: S M T W T F S.
|
/// Display order used by the day picker: S M T W T F S.
|
||||||
static let displayOrder: [Weekday] = [
|
static let displayOrder: [Weekday] = [
|
||||||
.sunday, .monday, .tuesday, .wednesday, .thursday, .friday, .saturday,
|
.sunday, .monday, .tuesday, .wednesday, .thursday, .friday, .saturday,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# OpenAppLock — "Rules" Feature Spec (derived from Opal screen recording)
|
# OpenAppLock — "Rules" Feature Spec
|
||||||
|
|
||||||
Source: `ScreenRecording_06-12-2026 11-26-19_1.MP4` (iPhone, 4:29). The rules feature
|
This spec describes OpenAppLock's recurring app-blocking **rules** feature: the
|
||||||
appears roughly between 1:00–3:00 of the recording, on Opal's "My Apps" tab.
|
behavior the app implements, then how it maps onto the native iOS presentation
|
||||||
This spec describes what was observed, then maps it onto an implementation plan
|
(see §6).
|
||||||
for OpenAppLock.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,13 +10,13 @@ for OpenAppLock.
|
|||||||
|
|
||||||
A **Rule** is a recurring, automated app-blocking policy. Unlike a one-off
|
A **Rule** is a recurring, automated app-blocking policy. Unlike a one-off
|
||||||
block/timer session, a rule re-arms itself on a schedule. Three rule types are
|
block/timer session, a rule re-arms itself on a schedule. Three rule types are
|
||||||
offered (Opal's "New Rule" sheet):
|
offered, presented on the "New Rule" sheet:
|
||||||
|
|
||||||
| Type | Icon | Example shown | Semantics |
|
| Type | Icon | Example shown | Semantics |
|
||||||
|------|------|---------------|-----------|
|
|------|------|---------------|-----------|
|
||||||
| **Schedule** | calendar grid | "e.g. 9-5, Daily" | Block selected apps during a daily time window on chosen days |
|
| **Schedule** | calendar grid | "e.g. 9-5, Daily" | Block selected apps during a daily time window on chosen days |
|
||||||
| **Time Limit** | hourglass | "e.g. 45m/day" | After N minutes of cumulative use of selected apps per day, block them until a reset point |
|
| **Time Limit** | hourglass | "e.g. 45m/day" | After N minutes of cumulative use of selected apps per day, block them until a reset point |
|
||||||
| **Open Limit** | padlock | "e.g. 5 opens/day" | After N opens of selected apps per day, block them (not demoed in video; inferred from card) |
|
| **Open Limit** | padlock | "e.g. 5 opens/day" | After N opens of selected apps per day, block them |
|
||||||
|
|
||||||
**Common attributes** — present for *every* rule kind:
|
**Common attributes** — present for *every* rule kind:
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ offered (Opal's "New Rule" sheet):
|
|||||||
independently of any rule. When editing a rule the user picks an existing
|
independently of any rule. When editing a rule the user picks an existing
|
||||||
list or creates a new one; editing a list affects every rule that uses it.
|
list or creates a new one; editing a list affects every rule that uses it.
|
||||||
Deleting a list detaches it from its rules (they fall back to "no apps").
|
Deleting a list detaches it from its rules (they fall back to "no apps").
|
||||||
- **Hard Mode** — boolean, PRO-gated in Opal; subtitle "No unblocks allowed". When off, the rule detail shows "Unblocks allowed: Yes"
|
- **Hard Mode** — boolean; subtitle "No unblocks allowed". When off, the rule detail shows "Unblocks allowed: Yes"
|
||||||
- **Enabled/disabled** — a rule can be disabled without deleting ("Disable Rule")
|
- **Enabled/disabled** — a rule can be disabled without deleting ("Disable Rule")
|
||||||
|
|
||||||
**Per-kind options** — each kind carries only the options that make sense for
|
**Per-kind options** — each kind carries only the options that make sense for
|
||||||
@@ -87,10 +86,10 @@ Dark theme throughout (near-black background, very dark green tint).
|
|||||||
2. **Blocked Apps** — section header; horizontal row of currently-blocked app
|
2. **Blocked Apps** — section header; horizontal row of currently-blocked app
|
||||||
icons. Each icon has a lock badge overlay and a teal/green rounded-rect
|
icons. Each icon has a lock badge overlay and a teal/green rounded-rect
|
||||||
outline; caption "Unblock" under the icon. Tapping unblocks (with friction
|
outline; caption "Unblock" under the icon. Tapping unblocks (with friction
|
||||||
if hard mode — not demoed).
|
if hard mode).
|
||||||
*(OpenAppLock)* Time/Open Limit rules whose budget is spent for the day
|
*(OpenAppLock)* Time/Open Limit rules whose budget is spent for the day
|
||||||
also appear here, blocked until midnight.
|
also appear here, blocked until midnight.
|
||||||
3. **Usage** *(OpenAppLock addition — not in the Opal video)* — a section
|
3. **Usage** *(OpenAppLock addition)* — a section
|
||||||
showing live tracking for every enabled Time/Open Limit rule scheduled today
|
showing live tracking for every enabled Time/Open Limit rule scheduled today
|
||||||
**that is not currently blocking**. Each row leads its subtitle with the rule
|
**that is not currently blocking**. Each row leads its subtitle with the rule
|
||||||
**type** so the kind is clear without relying on the icon:
|
**type** so the kind is clear without relying on the icon:
|
||||||
@@ -175,8 +174,7 @@ Sections (each an inset rounded group with a small icon + caption header):
|
|||||||
1. **📅 During this time**
|
1. **📅 During this time**
|
||||||
- Rows `From` / `To` with right-aligned time + stepper chevrons (`09:00 ⌃⌄`).
|
- Rows `From` / `To` with right-aligned time + stepper chevrons (`09:00 ⌃⌄`).
|
||||||
- A dotted vertical line with ●/○ endpoints visually links From → To.
|
- A dotted vertical line with ●/○ endpoints visually links From → To.
|
||||||
- Tapping a row expands an inline wheel time picker (24h, demoed changing
|
- Tapping a row expands an inline wheel time picker (24h).
|
||||||
From 22:00 → 23:00).
|
|
||||||
2. **On these days:** — trailing summary label ("Weekdays"/"Weekends"/custom);
|
2. **On these days:** — trailing summary label ("Weekdays"/"Weekends"/custom);
|
||||||
row of 7 circular toggles `S M T W T F S`; selected = filled white circle
|
row of 7 circular toggles `S M T W T F S`; selected = filled white circle
|
||||||
with black letter, unselected = dark circle.
|
with black letter, unselected = dark circle.
|
||||||
@@ -188,8 +186,8 @@ Sections (each an inset rounded group with a small icon + caption header):
|
|||||||
blocked" / "Only these apps are allowed" accordingly.
|
blocked" / "Only these apps are allowed" accordingly.
|
||||||
4. **Hard Mode** `⚡PRO` badge — subtitle "No unblocks allowed"; trailing
|
4. **Hard Mode** `⚡PRO` badge — subtitle "No unblocks allowed"; trailing
|
||||||
toggle.
|
toggle.
|
||||||
5. **Block Adult Content** *(OpenAppLock addition — not in the Opal video;
|
5. **Block Adult Content** *(OpenAppLock addition; **Schedule rules only**)* —
|
||||||
**Schedule rules only**)* — subtitle "Filter adult websites while this rule
|
subtitle "Filter adult websites while this rule
|
||||||
is active"; trailing toggle. Maps to Screen Time's web-content filter
|
is active"; trailing toggle. Maps to Screen Time's web-content filter
|
||||||
(`ManagedSettingsStore.webContent.blockedByFilter = .auto(...)`), applied
|
(`ManagedSettingsStore.webContent.blockedByFilter = .auto(...)`), applied
|
||||||
and cleared together with the rule's shield. Surfaces in the rule detail
|
and cleared together with the rule's shield. Surfaces in the rule detail
|
||||||
@@ -217,7 +215,7 @@ Same chrome (back / title / rename). Sections:
|
|||||||
|
|
||||||
### 3.6 Rule Editor — Open Limit type
|
### 3.6 Rule Editor — Open Limit type
|
||||||
|
|
||||||
Not demoed beyond its card. Spec by analogy: "When I open [apps]" /
|
Spec by analogy with the other editors: "When I open [apps]" /
|
||||||
"More than `N opens ⌃⌄` (Daily)" / day picker / "Then block until …" /
|
"More than `N opens ⌃⌄` (Daily)" / day picker / "Then block until …" /
|
||||||
Hard Mode / Hold to Commit. *(No Block Adult Content toggle — Schedule-only.)*
|
Hard Mode / Hold to Commit. *(No Block Adult Content toggle — Schedule-only.)*
|
||||||
|
|
||||||
@@ -243,11 +241,10 @@ Full-height sheet:
|
|||||||
letting users add arbitrary domains.
|
letting users add arbitrary domains.
|
||||||
- Footer: "**N Apps Selected**" caption + white pill "**Save**" (+ "Cancel").
|
- Footer: "**N Apps Selected**" caption + white pill "**Save**" (+ "Cancel").
|
||||||
|
|
||||||
> Implementation note: Opal ships its own app categorization. On iOS, third
|
> Implementation note: On iOS, third parties cannot enumerate installed apps;
|
||||||
> parties cannot enumerate installed apps; the system-sanctioned route is
|
> the system-sanctioned route is `FamilyActivityPicker` (FamilyControls), which
|
||||||
> `FamilyActivityPicker` (FamilyControls), which provides its own
|
> provides its own category/app/website UI and returns opaque tokens. **v1 of
|
||||||
> category/app/website UI and returns opaque tokens. **v1 of OpenAppLock should
|
> OpenAppLock embeds `FamilyActivityPicker`** rather than a custom app picker.
|
||||||
> embed `FamilyActivityPicker`** instead of cloning Opal's custom picker.
|
|
||||||
>
|
>
|
||||||
> **App Lists (OpenAppLock):** the selection itself lives on a reusable
|
> **App Lists (OpenAppLock):** the selection itself lives on a reusable
|
||||||
> **App List** (`@Model AppList`: name + encoded `FamilyActivitySelection`).
|
> **App List** (`@Model AppList`: name + encoded `FamilyActivitySelection`).
|
||||||
@@ -305,11 +302,11 @@ Full-height sheet:
|
|||||||
(which lists only rules whose budget is exhausted); it shows under "Usage"
|
(which lists only rules whose budget is exhausted); it shows under "Usage"
|
||||||
with its remaining opens.
|
with its remaining opens.
|
||||||
5. **Disable vs delete** — "Disable Rule" pauses scheduling but keeps the
|
5. **Disable vs delete** — "Disable Rule" pauses scheduling but keeps the
|
||||||
rule (card presumably shows disabled state). No delete flow was shown;
|
rule (the card shows a disabled state). Delete is offered from the rule
|
||||||
add delete via swipe/long-press or a button in the editor.
|
editor's actions menu.
|
||||||
6. **Commit friction** — creating/committing a rule uses press-and-hold
|
6. **Commit friction** — creating/committing a rule uses press-and-hold
|
||||||
("Hold to Commit"), echoing Opal's philosophy that *starting* a commitment
|
("Hold to Commit"), making the *start* of a commitment deliberate. Editing
|
||||||
should be deliberate. Editing uses a plain "Done".
|
uses a plain "Done".
|
||||||
7. **Live countdowns** — "Starts in 22h" / "6h left" update over time
|
7. **Live countdowns** — "Starts in 22h" / "6h left" update over time
|
||||||
(minute granularity is fine).
|
(minute granularity is fine).
|
||||||
8. **Overlapping rules — strictest enforcement wins.** When several rules
|
8. **Overlapping rules — strictest enforcement wins.** When several rules
|
||||||
@@ -503,20 +500,20 @@ Xh", "Xh left") is **derived**, not stored.
|
|||||||
- All shared logic lives in `Shared/` (notably `LimitEnforcement`), unit
|
- All shared logic lives in `Shared/` (notably `LimitEnforcement`), unit
|
||||||
tested from the app test target.
|
tested from the app test target.
|
||||||
|
|
||||||
### 5.6 Out of scope (seen in video, not part of "rules")
|
### 5.6 Out of scope (not part of "rules")
|
||||||
|
|
||||||
- Onboarding flow, paywall ("You know Opal works. Make it permanent."),
|
- Onboarding flow, paywall, Home tab gem/score UI, Timer tab (one-off focus
|
||||||
Home tab gem/score UI, Timer tab (one-off focus sessions, "Leave Early?"
|
sessions, "Leave Early?" friction screen), notification nudges ("Complete
|
||||||
friction screen), notification nudges ("Complete Your Setup").
|
Your Setup").
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Native UI re-skin (current presentation)
|
## 6. Native UI re-skin (current presentation)
|
||||||
|
|
||||||
OpenAppLock has since replaced the Opal-style custom presentation with the bare
|
OpenAppLock has since replaced its custom themed presentation with the bare
|
||||||
iOS design language, keeping the backend (models, logic, services), the
|
iOS design language, keeping the backend (models, logic, services), the
|
||||||
flows, and the accessibility identifiers intact. Sections 1–5 remain as the
|
flows, and the accessibility identifiers intact. Sections 1–5 remain as the
|
||||||
reference for *what* the feature does; presentation now maps as follows.
|
spec for *what* the feature does; presentation now maps as follows.
|
||||||
|
|
||||||
After onboarding the app is a three-tab `TabView` (`MainTabView`), each tab its
|
After onboarding the app is a three-tab `TabView` (`MainTabView`), each tab its
|
||||||
own `NavigationStack`:
|
own `NavigationStack`:
|
||||||
|
|||||||
Reference in New Issue
Block a user