@supersuit/suggest-a-change 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2 (2026-10-26)
4
+
5
+ - The pixel ratio in a report's context is rounded to two places (it read `1.600000023841858x` on a real Mac).
6
+
7
+ ## 0.2.1 (2026-10-26)
8
+
9
+ - **The sheet says when a relay opens the issue.** `Destination` takes `via: 'direct' | 'relay'` and `relayApp` (the relay's app name as people see it). With `via: 'relay'`, `destinationWords` says "Opened by the Supersuit Suggestions app on GitHub, which says it came from <name>." instead of naming a GitHub account that does not open it ("Filed from <account>'s GitHub account."); without a name it says "came from you", without an app name "a suggestions app". The Helm is the first user: its own Mac's account usually cannot push to the repo, so Supersuit files through the relay.
10
+ - **The contract export is typed.** `"."` is now `{ types: "./dist/contract.d.mts", default: "./src/contract.mjs" }`, so a TypeScript host (the Helm's server, which checks a report's context with `validContext`) imports it with its types instead of an implicit any. Plain ESM imports are unchanged.
11
+
3
12
  ## 0.2.0 (2026-10-26)
4
13
 
5
14
  A suggestion now becomes one ordinary GitHub issue, with everything it was sent with, in the repo that owns the app. A minor version because the statuses changed: apps on `^0.1` stay on 0.1 until they take this one.
package/README.md CHANGED
@@ -97,7 +97,7 @@ The trail (pages, taps, errors) is kept from the moment any of the module's scre
97
97
 
98
98
  The issue carries it all in a "Context (shared so this can be fixed)" table of what, value and why (in place of the About line), the trail in collapsible blocks, and a "Reach them" section with `mailto:` and `tel:` links "To thank them or ask a follow-up question". When the body would pass GitHub's limit, the trail is cut first (oldest taps, then pages, then errors), then the transcript.
99
99
 
100
- **Where it files.** With the optional `destination()` adapter, answering `{ repo, public, account, attaches, name? }`, that line becomes the list of exactly what the issue will carry ("your words · your voice note and its transcript · 2 screenshots, saved to the suggestion-attachments branch of acme/notes · this screen (Photos, /photos) · notes 1.2.3 · iPhone, 390x844 · the time · your name, Alex Kim"), the repo it files on, who can read it there (a public repo says so in `--sac-danger`), and whose GitHub account files it. The list follows the draft as things are added or removed, and it is built by `whatGoes` from `./github`, the same pieces `composeIssue` writes the body from, so the sheet and the issue cannot drift. `attaches: false` says files wait until the repo can take them. Sending is the consent. Without the adapter (or when it answers null) the sheet keeps the one line. My suggestions and the detail view link the issue a report became ("Issue #7"); a member who cannot read the repo still sees the number and the state.
100
+ **Where it files.** With the optional `destination()` adapter, answering `{ repo, public, account, attaches, name? }`, that line becomes the list of exactly what the issue will carry ("your words · your voice note and its transcript · 2 screenshots, saved to the suggestion-attachments branch of acme/notes · this screen (Photos, /photos) · notes 1.2.3 · iPhone, 390x844 · the time · your name, Alex Kim"), the repo it files on, who can read it there (a public repo says so in `--sac-danger`), and whose GitHub account files it. The list follows the draft as things are added or removed, and it is built by `whatGoes` from `./github`, the same pieces `composeIssue` writes the body from, so the sheet and the issue cannot drift. `attaches: false` says files wait until the repo can take them. With `via: "relay"` (and `relayApp`, the relay's app name as people see it) the account line says the issue is opened by that app and whose it is ("Opened by the Supersuit Suggestions app on GitHub, which says it came from Alex Kim."), since the filer's own account is not what opens it. Sending is the consent. Without the adapter (or when it answers null) the sheet keeps the one line. My suggestions and the detail view link the issue a report became ("Issue #7"); a member who cannot read the repo still sees the number and the state.
101
101
 
102
102
  **Identity.** Every screen asks `adapters.identity()` when it mounts, whenever it is handed a different `adapters` object, and whenever the host calls `refreshIdentity()` (exported from `"./client"`; call it after a sign-in, sign-out or account switch). When the account differs, the old account's rows leave the screen and its waiting reports stay on the phone, to be sent only while that account is signed in; every send re-confirms the account first. Keep `adapters` stable where you can; a parent that rebuilds it every render is safe, only slower (one `identity()` call per render).
103
103
 
@@ -239,7 +239,7 @@ export async function collectContext(o = {}) {
239
239
  add('device', 'Your device', await deviceWords(o.userAgent), 'so the fixer can try it on the same kind of device and browser');
240
240
  if (typeof window !== 'undefined') {
241
241
  const s = typeof screen === 'undefined' ? null : screen;
242
- add('screen', 'Screen and window', `${s ? `${s.width}x${s.height} screen, ` : ''}${window.innerWidth}x${window.innerHeight} window, ${window.devicePixelRatio || 1}x pixels`, 'how things are laid out depends on the size of the screen and window');
242
+ add('screen', 'Screen and window', `${s ? `${s.width}x${s.height} screen, ` : ''}${window.innerWidth}x${window.innerHeight} window, ${Math.round((window.devicePixelRatio || 1) * 100) / 100}x pixels`, 'how things are laid out depends on the size of the screen and window');
243
243
  const look = [media('(prefers-color-scheme: dark)') ? 'dark mode' : 'light mode', ...(media('(prefers-reduced-motion: reduce)') ? ['reduced motion'] : [])];
244
244
  add('appearance', 'Appearance', look.join(', '), 'some problems only show in dark mode or with animations turned down');
245
245
  }
@@ -50,6 +50,11 @@ export interface Destination {
50
50
  transcribes?: boolean;
51
51
  /** The largest file this destination takes; a larger one is named in the list as staying on the phone. */
52
52
  maxInlineBytes?: number;
53
+ /** How the issue is opened: `direct` from `account`, or `relay` by a relay's GitHub App (createSuggestionRelay), which names the
54
+ * sender in the issue. With `relay` the sheet's account line says the app opens it and whose it is, not `account`. */
55
+ via?: 'direct' | 'relay';
56
+ /** The relay's app name as people see it ("Acme Suggestions"), for that line. */
57
+ relayApp?: string;
53
58
  }
54
59
  /** What composeIssue and whatGoes read: a report in the contract's shape, plus two optional names for the list. */
55
60
  export interface Draft {
@@ -137,7 +142,9 @@ export declare const PUBLIC_CONTACT_REASON = "this repo is public, so anyone cou
137
142
  * of context. Anything but what the person added, the app's version and their name can be left out (`optional`). */
138
143
  export declare function whatGoes(draft: Draft, destination: Destination): GoesItem[];
139
144
  /** The lines around the list: where it files, who can read it there, and whose account files it. */
140
- export declare function destinationWords(d: Destination): {
145
+ export declare function destinationWords(d: Destination & {
146
+ name?: string;
147
+ }): {
141
148
  headline: string;
142
149
  privacy: string;
143
150
  danger: boolean;
@@ -115,11 +115,14 @@ function basicGoes(draft, destination) {
115
115
  }
116
116
  /** The lines around the list: where it files, who can read it there, and whose account files it. */
117
117
  export function destinationWords(d) {
118
+ // Through a relay the issue is opened by the relay's GitHub App, not by the sender's account: say so, and say whose it is.
119
+ const sender = typeof d.name === 'string' && d.name.trim() ? d.name.trim() : 'you';
120
+ const app = typeof d.relayApp === 'string' && d.relayApp.trim() ? `the ${d.relayApp.trim()} app` : 'a suggestions app';
118
121
  return {
119
122
  headline: `Sending this files an issue on ${d.repo}`,
120
123
  privacy: d.public ? 'public: anyone on the internet can read this' : `private: people with access to ${d.repo}'s code can read it`,
121
124
  danger: Boolean(d.public),
122
- account: `Filed from ${d.account}'s GitHub account.`,
125
+ account: d.via === 'relay' ? `Opened by ${app} on GitHub, which says it came from ${sender}.` : `Filed from ${d.account}'s GitHub account.`,
123
126
  };
124
127
  }
125
128
  /** "2026-10-26T17:04:12Z" -> "2026-10-26 17:04 UTC"; anything unreadable as it is. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supersuit/suggest-a-change",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "The report format and flow behind a Suggest a change button: what a report is, who acts at each step, the screens, and the check every app runs. The contract has zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,10 @@
8
8
  "suggest-a-change-check": "bin/check.mjs"
9
9
  },
10
10
  "exports": {
11
- ".": "./src/contract.mjs",
11
+ ".": {
12
+ "types": "./dist/contract.d.mts",
13
+ "default": "./src/contract.mjs"
14
+ },
12
15
  "./client": {
13
16
  "types": "./dist/client/index.d.ts",
14
17
  "import": "./dist/client/index.js"
@@ -211,7 +211,7 @@ export async function collectContext(o: CollectOptions = {}): Promise<ReportCont
211
211
  add('device', 'Your device', await deviceWords(o.userAgent), 'so the fixer can try it on the same kind of device and browser')
212
212
  if (typeof window !== 'undefined') {
213
213
  const s = typeof screen === 'undefined' ? null : screen
214
- add('screen', 'Screen and window', `${s ? `${s.width}x${s.height} screen, ` : ''}${window.innerWidth}x${window.innerHeight} window, ${window.devicePixelRatio || 1}x pixels`, 'how things are laid out depends on the size of the screen and window')
214
+ add('screen', 'Screen and window', `${s ? `${s.width}x${s.height} screen, ` : ''}${window.innerWidth}x${window.innerHeight} window, ${Math.round((window.devicePixelRatio || 1) * 100) / 100}x pixels`, 'how things are laid out depends on the size of the screen and window')
215
215
  const look = [media('(prefers-color-scheme: dark)') ? 'dark mode' : 'light mode', ...(media('(prefers-reduced-motion: reduce)') ? ['reduced motion'] : [])]
216
216
  add('appearance', 'Appearance', look.join(', '), 'some problems only show in dark mode or with animations turned down')
217
217
  }
@@ -28,6 +28,11 @@ export interface Destination {
28
28
  transcribes?: boolean
29
29
  /** The largest file this destination takes; a larger one is named in the list as staying on the phone. */
30
30
  maxInlineBytes?: number
31
+ /** How the issue is opened: `direct` from `account`, or `relay` by a relay's GitHub App (createSuggestionRelay), which names the
32
+ * sender in the issue. With `relay` the sheet's account line says the app opens it and whose it is, not `account`. */
33
+ via?: 'direct' | 'relay'
34
+ /** The relay's app name as people see it ("Acme Suggestions"), for that line. */
35
+ relayApp?: string
31
36
  }
32
37
  /** What composeIssue and whatGoes read: a report in the contract's shape, plus two optional names for the list. */
33
38
  export interface Draft {
@@ -167,12 +172,15 @@ function basicGoes(draft: Draft, destination: Destination): GoesItem[] {
167
172
  }
168
173
 
169
174
  /** The lines around the list: where it files, who can read it there, and whose account files it. */
170
- export function destinationWords(d: Destination): { headline: string; privacy: string; danger: boolean; account: string } {
175
+ export function destinationWords(d: Destination & { name?: string }): { headline: string; privacy: string; danger: boolean; account: string } {
176
+ // Through a relay the issue is opened by the relay's GitHub App, not by the sender's account: say so, and say whose it is.
177
+ const sender = typeof d.name === 'string' && d.name.trim() ? d.name.trim() : 'you'
178
+ const app = typeof d.relayApp === 'string' && d.relayApp.trim() ? `the ${d.relayApp.trim()} app` : 'a suggestions app'
171
179
  return {
172
180
  headline: `Sending this files an issue on ${d.repo}`,
173
181
  privacy: d.public ? 'public: anyone on the internet can read this' : `private: people with access to ${d.repo}'s code can read it`,
174
182
  danger: Boolean(d.public),
175
- account: `Filed from ${d.account}'s GitHub account.`,
183
+ account: d.via === 'relay' ? `Opened by ${app} on GitHub, which says it came from ${sender}.` : `Filed from ${d.account}'s GitHub account.`,
176
184
  }
177
185
  }
178
186