diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af681eaa..1ee69404 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -158,9 +158,21 @@ jobs: # enough for every assertion here. package-release: name: release packaging (macos) - runs-on: macos-latest + # GitHub-hosted preview image, not a self-hosted runner label: + # https://github.com/actions/runner-images/issues/14404 + # The saved Composer document enables Xcode 27's refractivity feature. + runs-on: xcode-27 steps: - uses: actions/checkout@v4 + - name: Verify release Xcode toolchain + run: | + version="$(xcodebuild -version | awk '$1 == "Xcode" {print $2}')" || version= + major="${version%%.*}" + if [[ ! "$major" =~ ^[0-9]+$ ]] || ((major < 27)); then + echo "release packaging requires Xcode 27 or later (selected: ${version:-unknown})" >&2 + exit 1 + fi + xcodebuild -version - name: Install pinned toolchain run: rustup show - uses: Swatinem/rust-cache@v2 diff --git a/README.md b/README.md index b31fdb9b..3810d7d1 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,19 @@ Two entry points, both in `scripts/`: Follow [Distributing OnetimePad through TestFlight](docs/development/testflight-distribution.md) for account setup, upload, and tester qualification. +Release packaging compiles [artwork/OnetimePad-Glass.icon](artwork/OnetimePad-Glass.icon) +with Xcode's `actool` and bundles both `Assets.car` and the generated `.icns`. +The document enables refractivity; Apple's +[Xcode 27 release notes](https://developer.apple.com/documentation/xcode-release-notes/xcode-27-release-notes) +introduce Icon Composer 2.0 with “support for refractivity”. Release packaging +requires Xcode 27 or later; set `DEVELOPER_DIR` or `xcode-select` to select it. +Edit the document in Icon Composer, then run +`scripts/build-icons.sh --glass` to compile just the icon, or use the release +commands above to package it. Debug builds keep the black development icon. +Ad-hoc icons rendered with `build-icons.sh` no longer select the release icon. +See the [exported glass icon preview](artwork/OnetimePad-Glass-CastShadow.png) +for the current composition. + Signing values stay outside the checkout, one environment directory per lane, so every worktree reads the same ones: `dev/.env` for the dev lane, `local/.env` for the local lane, and `staging/.env` for the App Store lane, diff --git a/artwork/OnetimePad-Glass-CastShadow.png b/artwork/OnetimePad-Glass-CastShadow.png new file mode 100644 index 00000000..76b40ba7 Binary files /dev/null and b/artwork/OnetimePad-Glass-CastShadow.png differ diff --git a/artwork/OnetimePad-Glass.icon/Assets/00-logo-cast-shadow.png b/artwork/OnetimePad-Glass.icon/Assets/00-logo-cast-shadow.png new file mode 100644 index 00000000..c286bf5b Binary files /dev/null and b/artwork/OnetimePad-Glass.icon/Assets/00-logo-cast-shadow.png differ diff --git a/artwork/OnetimePad-Glass.icon/Assets/01-logo-foreground.png b/artwork/OnetimePad-Glass.icon/Assets/01-logo-foreground.png new file mode 100644 index 00000000..e246e7c4 Binary files /dev/null and b/artwork/OnetimePad-Glass.icon/Assets/01-logo-foreground.png differ diff --git a/artwork/OnetimePad-Glass.icon/icon.json b/artwork/OnetimePad-Glass.icon/icon.json new file mode 100644 index 00000000..cf6f81f2 --- /dev/null +++ b/artwork/OnetimePad-Glass.icon/icon.json @@ -0,0 +1,43 @@ +{ + "features" : [ + "refractivity" + ], + "fill" : { + "automatic-gradient" : "extended-srgb:0.86275,0.29020,0.13333,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "image-name" : "01-logo-foreground.png", + "name" : "01-logo-foreground" + }, + { + "glass" : false, + "image-name" : "00-logo-cast-shadow.png", + "name" : "Cast shadow" + } + ], + "name" : "Group", + "refractivity" : { + "depth" : 0, + "enabled" : true, + "strength" : 0.15 + }, + "shadow" : { + "kind" : "neutral", + "opacity" : 0.18 + }, + "translucency" : { + "enabled" : true, + "value" : 0.18 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} diff --git a/crates/core/src/sheet.rs b/crates/core/src/sheet.rs index d6910d78..8d4e00c2 100644 --- a/crates/core/src/sheet.rs +++ b/crates/core/src/sheet.rs @@ -19,7 +19,7 @@ //! The excerpt on a chip is mechanical; counts are counts; detection //! never returns. -use std::time::{Duration, Instant}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use crate::blocks::{BlockIndex, BlockMeta}; use crate::document::{DocRun, SheetDocument}; @@ -27,15 +27,20 @@ use crate::ledger::SizeClass; use crate::secret::SecretBuffer; use crate::ttl::{self, Ttl}; -/// A random 128-bit item identifier (a version 4 UUID), minted at -/// creation. The sequential [`SheetId`] and [`ChipId`] counters stay for +/// A stable 128-bit item identifier. Newly minted values are version 7 UUIDs; +/// restored legacy version 4 values retain their original bytes. The +/// sequential [`SheetId`] and [`ChipId`] counters stay for /// internal ordering; this is the only identifier that may appear in the -/// ledger or in any persisted artifact (ADR-0012). +/// ledger or in any persisted artifact (ADR-0012; `UUIDv7` follow-up in ADR-0039). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ItemId([u8; 16]); impl ItemId { - /// Mint a fresh identity from the operating system CSPRNG. + /// Mint a `UUIDv7` from Unix milliseconds and the operating system CSPRNG. + /// The timestamp is visible in the ID; the remaining 74 bits are random. + /// Same-millisecond order and clock-rollback monotonicity are not promised. + /// A pre-epoch clock uses timestamp zero; a clock beyond the 48-bit field + /// uses its maximum value. Neither clock condition aborts identity minting. /// /// Panicking when the CSPRNG is unavailable is deliberate: a fallback /// identifier would be predictable, and an unpredictable identity is @@ -46,9 +51,23 @@ impl ItemId { pub fn random() -> Self { let mut bytes = [0u8; 16]; getrandom::getrandom(&mut bytes).expect("the OS CSPRNG must be available"); - // Version 4 in the high nibble of byte 6, RFC 4122 variant in the - // top two bits of byte 8. - bytes[6] = (bytes[6] & 0x0F) | 0x40; + Self::version_seven_at(SystemTime::now(), bytes) + } + + fn version_seven_at(time: SystemTime, bytes: [u8; 16]) -> Self { + let millis = time + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + Self::version_seven(millis, bytes) + } + + fn version_seven(millis: u128, mut bytes: [u8; 16]) -> Self { + let millis = millis.min((1_u128 << 48) - 1); + // RFC 9562 §5.7: 48-bit big-endian timestamp, version 7 and + // RFC variant, with all other bits drawn from the CSPRNG. + bytes[..6].copy_from_slice(&millis.to_be_bytes()[10..]); + bytes[6] = (bytes[6] & 0x0F) | 0x70; bytes[8] = (bytes[8] & 0x3F) | 0x80; Self(bytes) } @@ -256,7 +275,7 @@ impl SealedChip { self.id } - /// The random item identity, minted when this chip was sealed. The + /// The item identity, minted when this chip was sealed. The /// only identifier of this chip that may leave the process. #[must_use] pub fn uuid(&self) -> ItemId { @@ -401,7 +420,7 @@ impl Sheet { self.id } - /// The random item identity, minted when this page was created. The + /// The item identity, minted when this page was created. The /// only identifier of this page that may leave the process. #[must_use] pub fn uuid(&self) -> ItemId { @@ -730,7 +749,7 @@ impl Tab { self.id } - /// The random item identity, minted when this tab was opened. + /// The item identity, minted when this tab was opened. #[must_use] pub fn uuid(&self) -> ItemId { self.uuid @@ -1170,17 +1189,44 @@ mod tests { } #[test] - fn item_ids_are_random_version_four() { + fn item_ids_are_version_seven_with_current_unix_milliseconds() { let mut seen = std::collections::HashSet::new(); for _ in 0..1000 { + let before = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(); let id = ItemId::random(); + let after = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(); let bytes = *id.as_bytes(); - assert_eq!(bytes[6] >> 4, 0x4, "version nibble"); + let mut timestamp = [0u8; 8]; + timestamp[2..].copy_from_slice(&bytes[..6]); + let millis = u128::from(u64::from_be_bytes(timestamp)); + assert!((before..=after).contains(&millis), "Unix milliseconds"); + assert_eq!(bytes[6] >> 4, 0x7, "version nibble"); assert_eq!(bytes[8] >> 6, 0b10, "variant bits"); assert!(seen.insert(bytes), "a minted identity repeated"); } } + #[test] + fn item_id_v7_matches_rfc_9562_vector_and_orders_distinct_milliseconds() { + let random = [ + 0, 0, 0, 0, 0, 0, 0xcc, 0xc3, 0x18, 0xc4, 0xdc, 0x0c, 0x0c, 0x07, 0x39, 0x8f, + ]; + let id = ItemId::version_seven(0x017f_22e2_79b0, random); + assert_eq!(id.to_string(), "017f22e2-79b0-7cc3-98c4-dc0c0c07398f"); + let later = ItemId::version_seven(0x017f_22e2_79b1, [0; 16]); + assert!(id < later); + // Restoring an older UUIDv4 retains every original bit. + let mut legacy = [0x44; 16]; + legacy[8] = 0x84; + assert_eq!(ItemId::from_bytes(legacy).as_bytes(), &legacy); + } + #[test] fn item_id_renders_as_hyphenated_lowercase_hex() { let id = ItemId::random(); @@ -1198,6 +1244,37 @@ mod tests { assert_eq!(ItemId::from_bytes(*id.as_bytes()), id); } + #[test] + fn item_id_clock_boundaries_clamp_without_changing_random_bits() { + let random = [0xAB; 16]; + let earliest = ItemId::version_seven_at(UNIX_EPOCH - Duration::from_millis(1), random); + let epoch = ItemId::version_seven_at(UNIX_EPOCH, random); + assert_eq!(earliest, epoch); + assert_eq!(&earliest.as_bytes()[..6], &[0; 6]); + let submillisecond = + ItemId::version_seven_at(UNIX_EPOCH + Duration::from_nanos(999_999), random); + assert_eq!(submillisecond, epoch); + let one_millisecond = + ItemId::version_seven_at(UNIX_EPOCH + Duration::from_millis(1), random); + assert_eq!(&one_millisecond.as_bytes()[..6], &[0, 0, 0, 0, 0, 1]); + + let maximum_millis = (1_u64 << 48) - 1; + let maximum = + ItemId::version_seven_at(UNIX_EPOCH + Duration::from_millis(maximum_millis), random); + let beyond = ItemId::version_seven_at( + UNIX_EPOCH + Duration::from_millis(maximum_millis + 1), + random, + ); + assert_eq!(beyond, maximum); + assert_eq!(ItemId::version_seven(u128::MAX, random), maximum); + assert_eq!(&maximum.as_bytes()[..6], &[0xFF; 6]); + // Clamping changes only the timestamp; version/variant masking and + // the supplied random bits remain identical at both bounds. + assert_eq!(&earliest.as_bytes()[6..], &maximum.as_bytes()[6..]); + assert_eq!(earliest.as_bytes()[6] >> 4, 7); + assert_eq!(earliest.as_bytes()[8] >> 6, 2); + } + #[test] fn a_sealed_chip_carries_its_own_uuid() { // Identity is minted, not derived from content: identical text diff --git a/crates/ffi/include/companion_ffi.h b/crates/ffi/include/companion_ffi.h index 22740f27..5372eea0 100644 --- a/crates/ffi/include/companion_ffi.h +++ b/crates/ffi/include/companion_ffi.h @@ -263,6 +263,12 @@ bool companion_tab_set_title(CompanionHandle *handle, uint64_t tab, * entry per slot, whether or not it holds a page. Free with * companion_string_free(). Fields per tab: * id (the TAB's id, what the selection and the keyboard address), + * uuid (string: the TAB's stable ItemId in lowercase hyphenated UUID + * form. It survives restore unchanged while the dense numeric id is + * re-minted. Newly minted identities are UUIDv7: the first 48 bits + * encode Unix milliseconds at creation, clamped to zero for a + * pre-epoch clock or to 2^48-1 beyond that range. Restored legacy + * UUIDv4 identities coexist unchanged and carry no such timestamp), * has_page (bool, false is a slot whose page expired or was never * opened; every clock field below is meaningless then, and the * strip draws the dashed empty treatment instead of a gauge), diff --git a/crates/ffi/src/lib.rs b/crates/ffi/src/lib.rs index 781f8044..16208aa7 100644 --- a/crates/ffi/src/lib.rs +++ b/crates/ffi/src/lib.rs @@ -3631,6 +3631,7 @@ fn summary_json( let remaining = page.map_or(std::time::Duration::ZERO, |sheet| sheet.remaining(now)); serde_json::json!({ "id": tab.id().raw(), + "uuid": tab.uuid().to_string(), "has_page": page.is_some(), "page_id": page.map(|sheet| sheet.id().raw()), "title": tab.label(utc_offset_seconds), @@ -4022,6 +4023,30 @@ mod tests { serde_json::from_str(&unsafe { take_json(companion_tabs_json(handle)) }).unwrap() } + #[test] + fn tab_summary_uuid_survives_reorder_and_dense_restore_ids() { + unsafe { + let handle = handle(); + let first = companion_tab_new(handle); + let second = companion_tab_new(handle); + let third = companion_tab_new(handle); + let before = strip(handle); + let second_uuid = before[1]["uuid"].clone(); + let third_uuid = before[2]["uuid"].clone(); + assert!(second_uuid.as_str().is_some_and(|uuid| uuid.len() == 36)); + assert!(companion_tab_close(handle, first)); + assert!(companion_tab_move(handle, third, 0)); + age_by(handle, 0); + let restored = strip(handle); + assert_eq!(restored[0]["id"], 1); + assert_eq!(restored[0]["uuid"], third_uuid); + assert_eq!(restored[1]["id"], 2); + assert_eq!(restored[1]["uuid"], second_uuid); + assert_ne!(second, 0); + companion_free(handle); + } + } + /// The page a tab holds, read back through the summaries, or `None` /// for an empty slot. unsafe fn page_of(handle: *mut CompanionHandle, tab: u64) -> Option { @@ -5533,7 +5558,7 @@ mod tests { /// summary is decoded by name on the far side, so a rename here is /// an empty window there. #[test] - fn the_fifteen_existing_summary_keys_are_unchanged() { + fn existing_summary_fields_and_additive_stable_uuid_are_preserved() { let handle = handle(); unsafe { let (_tab, page) = new_page(handle); @@ -5548,6 +5573,7 @@ mod tests { keys.sort_unstable(); let mut expected = [ "id", + "uuid", "has_page", "page_id", "title", diff --git a/docs/README.md b/docs/README.md index 5ef447c0..484eb5b6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -74,6 +74,12 @@ browser prototype, folder and application association proposals, and approaches removed during review. Browser behavior and simulated native actions are identified separately; a mockup is not evidence of a shipped contract. +The opt-in native attempt is defined in the [pad-context feature proposal](spec/feature/pad-context/README.md), +with [implementation notes](development/pad-context-experiment.md) and a +[native verification runbook](qa/pad-context-experiment.md). The +[PR #232 follow-up checklist](qa/pr232-review-followup.md) records each review +observation and its implementation resolution. + ## plans/ Routes to a milestone: what has to happen, in what order, to get diff --git a/docs/adr/0039-pad-selection-and-associations.md b/docs/adr/0039-pad-selection-and-associations.md index 1a0df68b..5605059b 100644 --- a/docs/adr/0039-pad-selection-and-associations.md +++ b/docs/adr/0039-pad-selection-and-associations.md @@ -113,6 +113,37 @@ Storage choice, migration, and metadata removal still require review. - Stable ownership must survive native tab restoration. A restore-reminted numeric handle is insufficient as the persistent catalog identity; the implementation must use stable identity and verify restart behavior. +- The maintainer's follow-up instruction is: “re: UUIDs, use uuidv7.” Newly + minted pad IDs and core `ItemId` identities (tabs, pages, chips, and block + records) use UUIDv7 in this experiment. + Existing stored IDs and Scratch's nil sentinel are preserved. This departs + from the **proposed** [ADR-0012](0012-framing-threat-boundary-and-persistence-model.md), + whose item-identity wording is “Every sheet/chip gets a random 128-bit + identifier (UUIDv4) at creation, minted in the Rust core.” UUIDv7 encodes + Unix milliseconds plus 74 random bits ([RFC 9562 §5.7](https://www.rfc-editor.org/rfc/rfc9562.html#section-5.7)); + the RFC defines `unix_ts_ms` as a “48-bit big-endian unsigned number of the + Unix Epoch timestamp in milliseconds”. + The earlier fully-random-identity rationale cannot be carried forward as + an unchanged privacy claim. This proposal records that scope explicitly. + UUIDv7 timestamps are not trusted evidence of real creation time: the + proposed generators clamp clocks before the Unix epoch to zero and clocks + beyond the 48-bit field to `2^48 - 1` milliseconds. The Swift generator also + treats NaN dates as zero and infinite dates as the corresponding bound. + These rules keep malformed wall clocks from aborting identity minting while + retaining OS CSPRNG randomness; they do not promise monotonic IDs across a + clock rollback. The FFI tab-summary `uuid` is the stable identity, while + `id` is a restore-reminted numeric handle. Restored UUIDv4 values coexist + with newly minted UUIDv7 values, so consumers must not interpret every + summary UUID as containing a creation timestamp. +- The revised implementation proposal compares explicitly supplied directory + aliases using resolved symlinks and volume/inode metadata when available, + and folds path spelling only when the volume reports case-insensitive + matching. It does not infer a universal case-folding rule. Inaccessible + roots fall back to resolved lexical comparison; that fallback cannot + establish that two unavailable spellings refer to different physical + directories. Thus one-owner-by-physical-identity remains a proposed + requirement with an explicit unavailable-metadata limitation. No directory + tree scan, watcher, or file-content read is part of this comparison. - Path equality, nested roots, inaccessible associations, activation failures, association editing, and bounded recency need tests and documented failure behavior. Sample chooser exclusivity does not prove any of them. diff --git a/docs/development/pad-context-experiment.md b/docs/development/pad-context-experiment.md new file mode 100644 index 00000000..3ad71521 --- /dev/null +++ b/docs/development/pad-context-experiment.md @@ -0,0 +1,53 @@ +--- +documentation_status: needs-review +--- + +# Native pad context experiment + +This opt-in implementation attempts the [one-pad-picker proposal](../mockups/multiple-pads/one-pad-picker-design.md). The behavior below describes implementation observations and proposals from this attempt; it is not an accepted security or compatibility guarantee. Settings → General → Multiple pads and context associations · experiment enables it; disabling it exposes the existing shared store through the legacy navigation and numbered shortcuts. + +## Model and persistence + +One `PageModel`, one core `SheetStore`, and the existing encrypted page state remain. Pads are local navigation groups over durable tab UUIDs; numeric tab IDs are re-minted on restore and are unsuitable for ownership. The FFI summary now exposes the already-persisted UUID, without changing the encrypted snapshot format. Existing tabs and unassigned restored/synced tabs belong to Scratch. New pages belong to the selected pad. Selecting an empty pad creates no page. All-pad liveness still drives expiry, storage pruning, and core emptiness/key rotation; inactive pads do not extend page lifetime. + +New pad IDs and all newly generated core `ItemId` identities (tabs, pages, chips, and block records) use UUIDv7 following [RFC 9562 §5.7](https://www.rfc-editor.org/rfc/rfc9562.html#section-5.7): 48-bit Unix milliseconds and 74 random bits. IDs therefore expose their minting timestamp and carry fewer random bits than UUIDv4. They are identities, not authorization tokens. Same-millisecond ordering and monotonicity across clock rollback are not promised; catalog order and explicit timestamps still govern navigation. Stored UUIDv4 identities are retained rather than migrated, and Scratch keeps its fixed nil UUID. The core's existing `ItemId::random` entry point now generates UUIDv7; its restore path continues to accept stored bytes unchanged. + +The catalog is stored as `experimentalPadCatalogV1` in the build's UserDefaults domain. **Pad names, folder paths, application bundle IDs, ownership UUIDs, remembered selections, sorting choices, and pad recency are local but unencrypted metadata.** No note text, clipboard data, window titles, or app-switch journal is written to this catalog. This is an explicit experimental storage choice, not an assertion that metadata cannot be sensitive. Settings states this limitation. Unreadable, malformed, and unknown-version catalogs are preserved and cannot enable the experiment or overwrite themselves with fallback state. Catalog and encrypted content writes are separate transactions; a crash can leave ownership metadata without its newly minted page. Such orphan metadata contains no page content. After a successful content restore, refresh reconciles tab ownership and remembered tabs against the complete live roster and drops checkpoint-order preferences for dates with no live pages. Empty surviving tabs keep ownership. File-roster updates prune closed-file ownership and remembered file selections only after a successful draft restore or confirmation that no draft file exists; unreadable restore input does not authorize pruning. Repeated selection/assignment of the same state and relocation of an implicitly owned Scratch file avoid catalog writes. Recency metadata is capped at the named provisional recent-set limit. Pad-level sort preferences remain bounded by the existing pads. + +Pads can be renamed; empty names and names longer than 80 characters are rejected rather than silently truncated. Removing a named pad removes its bindings, remembered selections, sorting preferences, and recency metadata, and rehomes its tabs and open files to Scratch. It does not call core content-deletion operations. Scratch cannot be renamed or removed. Pad switches remember the selected open file as well as the underlying tab; switching back restores that file if it remains open, otherwise the remembered tab is used. Successful file relocation also transfers the remembered file path. + +Catalog ownership is device-local. Sync continues to carry the existing page/tab store; it does not synchronize the pad catalog or association definitions. Remote tabs without local ownership appear in Scratch. Names and bindings do not change note TTL. + +## Context and associations + +Each normalized absolute directory root belongs to one pad; one pad can have multiple roots. Scratch has no folder associations. Adding a folder uses an explicit system folder panel. The selected path is an association identity, not permission to scan or read its contents. No directory bookmark is created and no filesystem watch runs. Only supplied paths and stored roots are resolved and probed for filesystem metadata. Symlink aliases are resolved; volume-and-inode identity rejects a second binding to an accessible physical directory. Paths are folded only when the volume reports case-insensitive names; distinct directories on a case-sensitive volume remain distinct. Missing or inaccessible paths fall back to resolved lexical comparison, which cannot establish physical uniqueness. Older catalogs with ambiguous aliases are preserved, but an equally specific ambiguous match does not select an arbitrary pad. A directory rename requires removing the old association and choosing the new directory. + +Opening a new file through an existing explicit file operation supplies its URL. A component-boundary path match selects the associated pad, with the longest root winning nested bindings. Reopening an already-open core file ID retains its current pad owner, including implicit Scratch ownership, even if the supplied path is an alias or a folder binding was added later. Matching resolves the supplied URL and stored roots without enumerating directory contents; file ownership is then recorded under the core's canonical file path. The normal file coordinator remains responsible for file access and security scope. Files can remain open in different pads; changing pads does not close or discard them. Successful Save As, Locate, and bookmark relocation transfer already-recorded ownership and remembered file selection when the roster path changes, including while pad navigation is disabled. Supplied missing path components are reattached after resolving the nearest existing prefix, so a new file beneath a symlink alias can still match its root. + +Application associations store bundle IDs. Their editable list consists of regular applications already running. While both the experiment and app associations are enabled, the app observer retains only the previous regular application's bundle ID in memory; it reads no window contents, clipboard, or Accessibility data. On arrival, enabled app associations can select the most recently manually visited matching pad among the active pad and the last nine visited pads (`PadCatalog.recentPadLimit`). This provisional recent-set limit is named separately from the numbered-shortcut count; pads outside it remain manually selectable but do not participate in app hints until visited again. Automatic app routing does not update manual recency. Manual visits use bounded logical ranks rather than wall-clock timestamps, so a clock step does not demote the newly visited pad. Older stored timestamp values keep their relative order on read and are normalized on the next recorded manual visit; explicitly selecting an automatically selected older pad promotes it once. Equal recency uses catalog order. A selected file, modal operation, pending file-close decision, or conceal confirmation blocks this soft route. A supplied file operation therefore takes precedence whichever notification arrives first. + +An associated app icon activates a running `NSRunningApplication`; it does not launch an absent app or identify a particular project window. Selecting a pad manually and explicitly opening another file also wait for an unfinished file-close decision. An actual pad switch, pad creation/removal that changes the active pad, or experiment enable/disable transition dismisses the old inline conceal offer; it does not cancel a network request that was already confirmed. Experiment toggles retain a pending dirty-close file as the selected visible file, aligning its pad owner when enabling. Actual Command-Tab notification ordering, cross-Space activation, and sandboxed panel behavior require hardware verification. + +## Native surface + +A shared `PadPickerView` appears in the primary editor and ambient panel only while the experiment is enabled. Its closed header shows the pad name and folder tally; Scratch omits the folder tally. Associated app icons align at the trailing edge. Running apps supply their actual icons; a stopped app has a disabled generic icon. + +The popover highlights the entire pad cell. Folder labels are inert; add and × remove controls operate on the pad whose row contains them. Each row's Applications menu adds or removes regular running apps; its pad menu also renames or removes that pad after a confirmation explaining the rehome to Scratch. The footer contains the global App associations and Full paths toggles, plus New pad creation. Native controls show only association choices and state, never clipboard contents. + +The experimental sidebar groups checkpoints from `model.timeUnits`, followed by Files. Independent controls change day ordering and each day's checkpoint ordering. The paper reads the same projection, starts at the first checkpoint rule, and locates Today by bucket zero even when ascending order places it last. It uses native implementation geometry, preserving the mockup's intentional timeline approximation rather than reproducing its sample layout. + +## Sorting and shortcuts + +The experiment sorts a read-only day projection per pad and checkpoint order per calendar date. Equal creation timestamps use stable tab UUID order, reversed with the direction. Reversing one day's checkpoints does not reverse other days or mutate tab order/content. Date keys use the displayed local calendar day, derived from a validated roster-read reference and the projected day offset. Future-born pages folded into Today after a clock rollback share Today’s key, independently of roster order; obsolete-preference pruning uses the same mapping. The shell brackets each roster read with calendar and wall-clock readings. A read spanning midnight, a timezone/offset change, or a backwards wall-clock step defers checkpoint-preference pruning and toggles until the next consistent refresh; tab-ownership reconciliation still runs. During that ambiguous refresh, checkpoints use the chronological default and the independent day direction remains applied. A consistent read supplies the same cached reference to lookup, sorting and toggling, so a later control lookup does not reinterpret old relative offsets against a new day. A relative day moving at midnight keeps its calendar-date preference. Changing the system timezone can regroup dates; this experiment does not define timezone migration of those preferences. + +While enabled, application-local Command-0 selects Scratch and Command-1…9 select the first nine named pads, unless those chords are explicitly remapped or disabled by the user keymap. The owning surface installs an application-local monitor, so shortcuts also work with the picker closed; modal panels and sheets in any app window suspend that route, including a sheet attached to a window that is not key. The UI shows available hints while Command is held and resets them on release, app deactivation, or editor ownership transfer. A claimed digit without a corresponding pad is consumed rather than falling through to select a page. No global numbered hotkey is installed. Legacy numbered page shortcuts remain the default when the experiment is disabled. Explicit user keymap remappings and null bindings take precedence; native shortcut tests cover this behavior. The experiment claims only still-bundled page-digit bindings and the unbound Scratch zero; changed bindings and explicit null bindings remain with the user keymap. Release compatibility still requires a scoped decision. + +## Verification and remaining work + +Automated tests cover catalog reload and refusal, exclusive/nested folder routing, soft app recency, independent date sorting, no-mint switching, new-page ownership, inactive editor-storage preservation, legacy navigation on disable, UUID ownership after relaunch, file routing, soft-route modal refusal and precedence, and file ownership through Save As. Native UI tests cover shared timeline/roll projections and shortcut routing. The 17 targeted catalog, model, shortcut, and mounted-timeline tests passed on 2026-10-01. The final automated gate on 2026-10-01 passed: Rust formatting; workspace Clippy with and without `test-util` with warnings denied; 767 Rust tests without the feature and 769 with it (one existing ignored test in each run); the universal test-util core build; Swift build and all 1,475 Swift tests (288 executable-target tests and 1,187 shared-target tests). The 17 new tests are included in those counts. ADR structural lint passed for 38 records. These are execution observations on this development host, not release compatibility or runtime privacy guarantees. The [native verification runbook](../qa/pad-context-experiment.md) covers the outstanding signed-app and hardware checks; their inclusion does not mean they passed. + +The UUIDv7 follow-up on 2026-10-01 passed all 1,478 Swift tests and 770 Rust workspace tests with `test-util` (one existing ignored test), plus workspace Clippy with warnings denied, Rust formatting, and ADR structural lint. Both generators match the RFC 9562 Appendix A.6 reference vector; tests cover version/variant bits, current Unix milliseconds, distinct-millisecond ordering, and retained legacy UUIDv4 identities. Focused read-only review found no code defect. + +The [PR #232 review follow-up](../qa/pr232-review-followup.md) records each finding and its resolution. Its final 2026-10-01 gate passed 1,518 Swift tests, 771 Rust workspace tests with `test-util` (one existing ignored test), Clippy, formatting, 39 ADR checks, localization lint and eight icon-script tests. This later gate includes clock clamping, catalog pruning/refusal protection, pad lifecycle, remembered files and directory alias regressions. + +This experiment does not implement directory scanning, automatic copying, clipboard collection, project-window identity, pad catalog sync, encrypted association metadata, directory relocation tracking, or Scratch clear-on-close. Its native timeline follows the implementation's existing geometry; the mockup's timeline remains an intentional approximation. diff --git a/docs/qa/pad-context-experiment.md b/docs/qa/pad-context-experiment.md new file mode 100644 index 00000000..befe0cf3 --- /dev/null +++ b/docs/qa/pad-context-experiment.md @@ -0,0 +1,122 @@ +# Pad context experiment: native verification + +This runbook exercises the opt-in implementation attempt of the +[reviewed mockup](../mockups/multiple-pads/one-pad-picker-design.md). +It is a verification procedure, not a record that these checks passed. +Record the exact OS/build, application commit, signing lane, and sandbox status +with each manual run. The browser prototype cannot establish these outcomes. + +## Isolated setup + +Use a debug bundle and disposable sample notes and folders. Do not point a test +runner at installed application state. Begin with the experiment disabled; +record the existing slot shortcuts and opened files before enabling it. + +## Pads and editor ownership + +1. Enable the multiple-pad experiment. Confirm existing notes appear in Scratch. +2. Create two named pads. Switch to an empty pad without typing; verify it does + not silently create a checkpoint or show another pad's ink. +3. Write distinct sample text in each pad. Switch via picker and Command digits, + with both the ordinary editor and ambient panel. Confirm the same active pad + appears on both surfaces and there is still one live editor owner. +4. Restart the debug bundle. Confirm pad names, ownership, and writing survive. + Repeat after reordering and closing slots so numeric handles cannot accidentally + masquerade as stable identities. +5. Disable the experiment. Verify legacy navigation and shortcuts return and all + notes remain reachable. Re-enable and verify the same pad memberships. +6. Exercise the existing page-expiry test seams with inactive pads. Confirm + expiration follows existing page rules and navigation does not revive content. + +7. Create a typo, rename it, and reject empty or over-80-character names. Remove a + named pad after reading the confirmation. Verify its content and open files + appear in Scratch, its associations are removed, and Scratch cannot be removed. +8. Open a page/chip conceal offer without confirming. Switch pads, create a pad, + remove the active pad, and toggle the experiment in separate trials. Verify + the outgoing offer closes. Rename the same pad or toggle full paths; its offer + should remain. With a controlled delayed response, dismiss and reopen an offer + for the same target; the earlier result must not populate the new offer. + +## Folder bindings and file context + +1. Add two directories to one named pad through the explicit chooser. Confirm + Scratch cannot bind a folder and already-owned roots cannot bind to another pad. +2. Add a nested directory to a different pad. Open a deliberately supplied file + beneath each root and check the most-specific matching root selects the pad. + A similarly named sibling directory must not match by string prefix alone. +3. Cancel the chooser, remove an association, and restart. Confirm each operation + has only its named effect; no sample directory or file is created or scanned. +4. Open an unrelated file, then switch A → B → A. Expect A's selected file to + return if still open, with its edits retained. Close the file and repeat; + expect A's remembered tab instead. Check Save As and Locate also update the + remembered path. Disable the experiment, Save As an associated file, and re-enable; + expect its existing pad ownership and selected file to survive the path change. + A pending close confirmation must prevent switching pads. + Disable/re-enable the experiment during that decision; it must remain visible. + Also close an inactive dirty file while pads are off, then enable pads and + choose Keep Editing; verify the previous file returns in its owning pad. +5. Try missing, renamed, inaccessible, symlinked, and case-varied paths. Record + the actual matching behavior and any unsupported case; do not infer permission + or document provenance from a path association. Reject an accessible symlink + alias bound to another pad. On case-insensitive volumes reject alternate-case + aliases; on case-sensitive volumes allow distinct existing directories. Verify + inaccessible paths report only the fallback behavior, not physical uniqueness. +6. Reopen an already-open file from a different pad, using its original path and + a symlink alias. Add a folder binding to another pad and repeat. Verify the + existing file keeps its owner; an already-open Scratch file stays in Scratch. + +## App associations + +1. Associate a running ordinary application with one pad, then with a second. + Check add/remove controls are attached to the correct pad. +2. With app associations disabled, leave and return from that app. Confirm no + app-based pad change. Enable them and exercise the recent-pad tie-breaker. +3. Explicitly supply a matching folder/file path while an app hint is pending. + Check that folder context wins and does not redirect a pending paste or dirty + file confirmation. +4. Click each header icon. Check the named running application becomes active; + quit it and retry to check unavailable-app handling. Two windows belonging to + that application must not be described as distinct project context. +5. Inspect the implementation's activation path and instrument general-pasteboard + content reads/writes if available. Activation, hover, pad selection, shortcut + hints, sorting, and association edits must not acquire clipboard content. + A source audit alone is not runtime clipboard/privacy validation. + +## Picker and timeline + +1. Hover/focus a pad cell: highlight includes its folder rows and add action. + Folder labels remain inert; only add and remove change bindings. +2. Toggle full paths and app associations independently in the menu footer. + Confirm the closed header stays compact and shows a tally rather than paths. +3. Hold Command to reveal assignments, release it, and move focus to another + app. Check hints clear and Command-0 always selects Scratch while the + experiment is enabled. Test user keymap overrides and the disabled mode. + Leave a pad-name sheet open in one app window, focus another app window, + and press a Command digit. Verify the active pad stays unchanged until the + sheet is dismissed, then verify the shortcut works again. +4. Change overall day direction, then change checkpoints within just one day. + Check the rail and paper agree while another day's order stays unchanged. + Switch pads and restart to check preference scoping. + With a controlled clock, span local midnight during a roster refresh. Verify + the saved checkpoint direction survives the ambiguous refresh and applies to + the same calendar date after the next consistent refresh. +5. Verify Timeline stays above Files as files are opened and closed, and test + narrow window layout, keyboard reachability, and menu closing. With VoiceOver, + verify each sort button announces its subject and current order as its value, + while its hint describes the next order that activation will select. + +## Persistence failures and release limits + +Using injected disposable preferences, present an invalid catalog and confirm +it is not overwritten with a new empty catalog. Test duplicate owners and stale +UUID mappings explicitly. After successful restore, close tabs/files and expire +pages; inspect that ownership, remembered selections, and obsolete date-sort +keys are pruned across every pad. Repeat with a refused content/draft restore; +its absent temporary roster must not prune saved ownership. Repeatedly select +an already-selected tab/pad and verify no catalog write is owed. Move an unowned +Scratch file and verify it does not gain an explicit ownership entry. The experiment's context metadata storage is distinct +from note ciphertext; record its actual contents and limitations. + +Clipboard compatibility claims require the independent release matrix in +[ADR-0038](../adr/0038-explicit-clipboard-operations.md). No broad macOS release +promise follows from a successful local build or this runbook. diff --git a/docs/qa/pr232-review-followup.md b/docs/qa/pr232-review-followup.md new file mode 100644 index 00000000..8ff87ce9 --- /dev/null +++ b/docs/qa/pr232-review-followup.md @@ -0,0 +1,122 @@ +# PR #232 review follow-up + +This checklist tracks the observations in [Claude's review](https://github.com/onetimesecret/macos/pull/232#issuecomment-5945850036). It records implementation changes and verification, not accepted product guarantees. ADR-0039 remains proposed; the manual runbook remains owed on a signed native build. + +| Observation | Resolution | +| --- | --- | +| 1. Missing FFI UUID contract | Document stable tab identity, restore-reminted numeric handles, newly minted UUIDv7 and unchanged legacy UUIDv4 values in the C header. | +| 2. Wall-clock panic | Clamp pre-epoch and overflowing timestamps in both generators; retain secure random bits. Test timestamp bounds and non-finite Swift dates. | +| 3. Growing catalog and repeated writes | Reconcile against complete live rosters after restoration, prune closed-item references and expired-date sorting, bound recency, and suppress no-op mutations. Preserve catalog ownership when content restoration refuses. | +| 4. Permanent pad names/entries | Add rename and remove controls within each named pad cell. Removal drops associations and moves existing pages/files to implicit Scratch ownership. Scratch cannot be removed. Reject invalid names visibly rather than silently truncate. | +| 5. Forgotten file selection | Remember each pad's selected open file alongside its tab; follow path transfers even while the experiment is off, and discard closed-file references. | +| 6. Directory aliases | Compare explicitly supplied roots using symlink resolution and available physical identity; resolve existing prefixes for missing file leaves and use volume case sensitivity for matching. Unavailable metadata retains a documented fallback limitation. | +| 7. Magic recency limit | Name the nine-pad recent-set constant and document its provisional policy separately from numbered shortcuts. | +| Implicit Scratch ownership during file transfer | Transfer recorded ownership only; Scratch stays implicit. | +| Extra roster fetch for new tabs | Assign queued ownership from the next normal refresh's decoded summaries. | +| Duplicate creation activation / silent invalid names | Create activates once. Name editor shows empty/overlong-name validation and a failure message. | +| Callback reassignment | Use an explicit catalog-change reentrancy guard. | +| Undiscoverable experimental rail width | Expose legacy and experimental widths through one width query used by the view and settings caption. | +| Opaque clock-backwards date key | Explain Today grouping and the persisted creation-date preference key in the model. | +| Sort VoiceOver ambiguity | Separate current order (accessibility value) from the next action (hint and help). | +| Mutable defaulted UUID property | Explain memberwise initializer compatibility next to the declaration. | +| Hard-coded user-facing copy | Route new picker, association, disclosure, error and sorting copy through the localization bundle. | +| Missing `--shadows` usage option | Include it in every usage branch; icon script tests and shell syntax check cover the script. | +| Xcode 27 preview runner | Verified existing packaging job passed on this PR. GitHub's main branch protection endpoint returned “Branch not protected”; the repository's sole ruleset was disabled and contained no required status-check rule. No workflow change made. This is a point-in-time observation, not an availability promise. | +| Greptile missing-label notice | Add the available `greptile-review` label. This notice is not a code defect. | + +The preview label is documented by [GitHub's runner-images announcement](https://github.com/actions/runner-images/issues/14404), which also calls out possible queueing issues. The successful [packaging run](https://github.com/onetimesecret/macos/actions/runs/36966679988/job/110711840501) predates these follow-up changes. + +## Verification + +On 2026-10-01 the final follow-up gate passed on the development host: + +- 1,518 Swift tests: 296 executable-target and 1,222 shared-target tests. +- 771 Rust workspace tests with `test-util`; one existing ignored test. +- Workspace/all-target Clippy with `test-util` and warnings denied, Rust formatting, + 39 ADR structural checks, localization resource lint and shell syntax checking. +- Eight icon-script tests. + +Independent read-only reviews covered identity/FFI separately from catalog/model/UI. +QA's disabled-Save-As ownership finding was fixed and covered before the final gate. +Regression coverage also exercises separate refused content/draft restores, +closed-file pruning, symlinked roots with missing leaves, renaming/removal and +selected-file restoration. Native VoiceOver, chooser, activation and signed-app +checks remain in [the manual runbook](pad-context-experiment.md). + +## Greptile follow-up and focused self-review + +The checkout was verified against PR #232 before editing: repository +`onetimesecret/macos`, branch `codex/pad-context-implementation`, base `main`, +and reviewed head `349dd4193d033509378ff2387e94bc94d2ba70a0`. + +### Summary + +Three attached comments identified remaining state/routing defects; two were +already addressed by the preceding follow-up. The focused review found two +clock-rollback issues and a return-navigation edge. All are addressed below. +These are implementation observations, not accepted security or storage claims. + +### Primary findings + +| Attached feedback | Assessment and resolution | +| --- | --- | +| 1. Conceal confirmation survives pad switch | Fixed. An actual pad/context transition dismisses the outgoing offer, including create, active-pad removal and experiment toggles. Same-pad metadata edits preserve it. This does not cancel an already-confirmed request. | +| 2. Dirty-close decision disappears on disable | Fixed. Experiment toggles keep the pending file selected and align its owner when enabling, so the inline decision retains its subject. Keep Editing returns through owner-aware selection if its previous file belongs to another pad. | +| 3. Reopening moves an already-open file | Fixed. The returned core ID is compared with the pre-open roster; an existing file retains its canonical-path owner, including implicit Scratch. Only newly opened files acquire folder/active-pad ownership. | +| 4. Disabled Save As loses ownership | Already addressed in `1f05bed`: transfers run regardless of the navigation gate. The existing disabled-Save-As regression passed again. | +| 5. Closed file paths remain stored | Already addressed in `1f05bed`: authoritative roster updates prune closed-file ownership and remembered selections. The closed-file and refused-draft-restore regressions passed again. | +| Self-review: wall-clock recency | Fixed. Bounded logical ranks represent manual visit order without depending on the wall clock; legacy timestamp order is normalized on the next recorded visit. Manual reselection after automatic routing promotes the pad once. | +| Self-review: folded Today sort key | Fixed. Lookup and pruning use the displayed local calendar day, so future-born pages folded into Today do not change its preference when tabs reorder. | + +### Pre-existing issues + +The conceal completion guard previously distinguished only targets. A response +could populate a newly opened offer for the same page/chip. This is also fixed: +each offer has an immutable transient UUIDv7 identity, captured by confirmation +and checked with the target on completion. That identity is not persisted or +sent to the core/network. Successful earlier requests still arm their existing +clipboard-clear lifecycle; this change isolates UI results rather than cancelling +an authorized operation. + +### Speculative issues + +None identified as actionable in this focused pass. Timezone regrouping remains +the documented experimental behavior; native activation and accessibility still +require the manual runbook. + +### Clean files and final verification + +Independent review found no remaining actionable issue in the changed catalog, +model, and regression tests. The final full Swift run passed **1,528 tests** +(296 executable-target and 1,232 shared-target tests), including ten new tests. +The initial new date fixture used the wrong wire-key spelling; correcting it and +asserting the decoded offsets/timestamps preceded the final passing run. +No Rust or packaging implementation changed in this round, so their preceding +results above were not rerun. `git diff --check` and ADR structural lint passed. + +## Last Greptile check + +The last check started from a clean checkout of `codex/pad-context-implementation` +in `onetimesecret/macos`, matching PR #232 head +`a7210c0514cecc98c31f2120b7263d1a7c0189c8` and base `main`. +Greptile's summary was updated at **2026-10-02 06:13:14 UTC** +(2026-10-01 locally). GitHub marked the earlier five threads resolved; the +summary and inline threads identified two remaining findings: + +| Feedback | Assessment and resolution | +| --- | --- | +| [Shortcut bypasses another window's sheet](https://github.com/onetimesecret/macos/pull/232#discussion_r4163209132) | Valid and fixed. The shortcut monitor inspects all app windows for an attached sheet or a sheet parent, matching the application-context route. A real AppKit multiwindow regression verifies suspension for either sheet representation and resumption after dismissal. | +| [Midnight resets checkpoint order](https://github.com/onetimesecret/macos/pull/232#discussion_r4163300085) | Valid and fixed. The shell brackets the core roster read and validates one calendar day, timezone/offset and forward wall-clock interval. A consistent read provides a shared reference for date lookup, sorting and toggling. An ambiguous read defers date-preference pruning and checkpoint toggles until a consistent refresh; tab ownership still reconciles. Tests cover crossing midnight, preference retention, later normal pruning, timezone changes and backwards clock steps. | + +These are observations of the changes and test results, not newly accepted +persistence or compatibility guarantees. The full Swift gate passed **1,530 tests** +(296 executable-target and 1,234 shared-target tests). This includes two new model +tests and replacement of the earlier boolean-only shortcut test with the AppKit +window regression. ADR structural lint passed for 39 records and +`git diff --check` passed. No Rust or packaging implementation changed. + +Independent review caught and corrected an initial fallback that also bypassed +the independent day-sort direction. The final fallback keeps that direction and +uses chronological checkpoint order while no date reference is available. +The full Swift suite passed again after that correction. Final independent review +passed both fixes and found no remaining actionable issue in this patch. diff --git a/docs/spec/feature/pad-context/README.md b/docs/spec/feature/pad-context/README.md new file mode 100644 index 00000000..4351dc56 --- /dev/null +++ b/docs/spec/feature/pad-context/README.md @@ -0,0 +1,57 @@ +--- +documentation_status: needs-review +--- + +# Feature proposal: pad contexts + +Status: experimental proposal, default off. The maintainer requested a native +implementation attempt after reviewing the 2026-10-01 +[one-pad-picker design](../../../mockups/multiple-pads/one-pad-picker-design.md). +That design owns the interaction detail and rejected alternatives; +[ADR-0039](../../../adr/0039-pad-selection-and-associations.md) owns the proposed +association model and shortcut decision. Neither establishes release acceptance. + +## Experimental contract + +Enable **Multiple pads and context associations · experiment** in General +settings to expose a shared picker on both editing surfaces. Existing tabs +start in Scratch. Creating or selecting a pad does not itself mint a page; +deliberate writing/new-page gestures use the existing page lifecycle. + +One directory root belongs to one pad; several roots can belong to that pad. +Scratch has no directory bindings. Explicit file-open context uses the longest +component-boundary root match. No directory scan or permission grant follows +from a binding. Application associations use regular running app identities, +select among the active/recent pads when enabled, and yield to explicit file +context and unresolved modal/confirmation operations. Header icons deliberately +activate an associated running app. + +The picker contains per-pad binding controls, rename/removal actions, and global +application/full-path options. Removing a named pad rehomes its existing pages +and files to Scratch; it does not delete their content. The closed header uses +a folder tally. Command-0 selects Scratch and +Command-1…9 select named pads in roster order while enabled, preserving explicit +user keymap overrides. Timeline precedes Files; day direction and checkpoint +direction within each date are independent display preferences. + +Accepted [ADR-0017](../../../adr/0017-durable-tabs-expiring-pages.md) states: +“When a Page expires, it is dropped whole and its Tab remains empty and reusable.” +This proposal groups those tabs; it introduces no pad lifetime or expiring link. +The existing slot shortcut and newest-first day-policy conflicts are recorded +in the linked design and ADR and require successor acceptance before becoming +release defaults. + +## Implementation and evidence + +[Native implementation notes](../../../development/pad-context-experiment.md) +record the actual storage/routing choices, test observations, and limitations. +The catalog is unencrypted local context metadata, separate from note content. +It is not synced. Explicitly supplied paths use symlink resolution and available +filesystem metadata to compare directory aliases, with a documented fallback +when that metadata is unavailable. Folder relocation tracking and project +window recognition are outside this attempt. Clipboard transfers remain the +separate [ADR-0038](../../../adr/0038-explicit-clipboard-operations.md) proposal. + +[Native verification](../../../qa/pad-context-experiment.md) lists signed-app, +activation, accessibility, and release checks still owed. Passing unit tests +does not establish those outcomes or a supported macOS release promise. diff --git a/docs/spec/icon/about-longshadow.md b/docs/spec/icon/about-longshadow.md index 9ad992ad..d51dc4a2 100644 --- a/docs/spec/icon/about-longshadow.md +++ b/docs/spec/icon/about-longshadow.md @@ -1,4 +1,17 @@ -# docs/spec/icon/about-longshadow.md +# Long-shadow renderer study + +This document records an earlier study of the standalone Swift renderer's +long-shadow styles. It is design exploration, not an accepted specification +or an Apple design guideline. The historical discussion and visual +interpretations below describe that study. + +The current release packaging implementation instead compiles the saved +[Icon Composer document](../../../artwork/OnetimePad-Glass.icon), which combines +a white foreground, a separate fading southeast cast-shadow layer, and +Composer's glass effects. See its [exported preview](../../../artwork/OnetimePad-Glass-CastShadow.png). +`scripts/build-icons.sh --composer` exports source layers for manual import +into that document; it does not update the saved composition. The renderer +matrix below remains available for experiments. --- longshadow is now a generated matrix instead of one hardcoded look. diff --git a/scripts/build-icons.sh b/scripts/build-icons.sh index b31cc8d9..dbd5ba56 100755 --- a/scripts/build-icons.sh +++ b/scripts/build-icons.sh @@ -18,6 +18,13 @@ # scripts/build-icons.sh