scenri 0.12.0 → 0.13.1

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,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.1](https://github.com/tonygorb/Scenri/compare/v0.13.0...v0.13.1) (2026-09-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **create:** the feed keeps the tile you were looking at when its columns change ([8908270](https://github.com/tonygorb/Scenri/commit/8908270c37d479659ba7793c0c4fc08a8dbfb430))
9
+ * **studio:** a lesson step pushed out of view by a resize comes back into view ([5f5fefb](https://github.com/tonygorb/Scenri/commit/5f5fefbbe9d98f7241f9d34b057d96fe8d65da92))
10
+ * **studio:** a lesson step written as the page reloads is not lost ([22d04ca](https://github.com/tonygorb/Scenri/commit/22d04ca8006295ab9421470394c0a4e1c64d42d4))
11
+ * **studio:** alerts above the tutor and dialogs, a feed that keeps its place, and three small repairs ([ca63798](https://github.com/tonygorb/Scenri/commit/ca63798df7d6d3db85cef641b977de05e4d8c4be))
12
+ * **studio:** alerts are drawn above the tutor and dialogs, and can be pressed there ([3c5e75c](https://github.com/tonygorb/Scenri/commit/3c5e75c8bf041575c06f80ac78b45f68e9679191))
13
+ * **studio:** the brief has a name a screen reader can say ([22c6b6a](https://github.com/tonygorb/Scenri/commit/22c6b6a66a696cb558d4989dd81d93d1838e86ea))
14
+
15
+ ## [0.13.0](https://github.com/tonygorb/Scenri/compare/v0.12.0...v0.13.0) (2026-09-21)
16
+
17
+
18
+ ### Features
19
+
20
+ * **studio:** rebuild transient alerts as a quiet toast stack ([7b44141](https://github.com/tonygorb/Scenri/commit/7b44141b57cc6748942d97bc6acc27a022260207))
21
+ * **studio:** turn first-use on, with a tutor that can go back ([df6b799](https://github.com/tonygorb/Scenri/commit/df6b799708d65cf31130233dabe2fd0ee1095a28))
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * **cli:** an install that has made a shot is not offered the first-use welcome ([4dae292](https://github.com/tonygorb/Scenri/commit/4dae292f3e833180448b2310a81573b641408bcc))
27
+ * **create:** keep the feed still when a shot archives or the rail toggles ([b3276cd](https://github.com/tonygorb/Scenri/commit/b3276cd1f68315455e8b29d7dfdf13c768a0438a))
28
+ * **studio:** a lesson on its first step has not begun, and Learn cannot take the app down ([cf0540b](https://github.com/tonygorb/Scenri/commit/cf0540b1ecd1e8fbdde2e3d203538f9e13f46d8c))
29
+ * **studio:** a lesson owns its progress, and only the screen is shared ([8c58bdb](https://github.com/tonygorb/Scenri/commit/8c58bdbc06124d2f9fda0775a41f84fa156e10ea))
30
+ * **studio:** a library lesson keeps its button in sight on narrow screens ([93869f4](https://github.com/tonygorb/Scenri/commit/93869f4f1f1fa8791c15ff893ef8ac536deda721))
31
+ * **studio:** a step scrolled out of sight is not gone ([49ceccb](https://github.com/tonygorb/Scenri/commit/49ceccb8d7dc41daf8327ecefb8353f98ed94e70))
32
+ * **studio:** alerts stand above a docked composer instead of over its buttons ([b6756c7](https://github.com/tonygorb/Scenri/commit/b6756c79c63419ca2ed146fc2e3c6d868eeb6f79))
33
+ * **studio:** Learn gives the keyboard back to whichever control opened it ([e9c5945](https://github.com/tonygorb/Scenri/commit/e9c5945d60e6ceb55704da1130fe23a988c6044f))
34
+ * **studio:** no empty alert on every page, and a held toast stays held ([3305f75](https://github.com/tonygorb/Scenri/commit/3305f7542fb38f604c9b4087c59aa7cfe0d4cf6b))
35
+ * **studio:** refining teaches how to find a shot, instead of picking one for you ([228938f](https://github.com/tonygorb/Scenri/commit/228938f2440e17b48d9bab836e7c4aeeb0a74cef))
36
+
3
37
  ## [0.12.0](https://github.com/tonygorb/Scenri/compare/v0.11.1...v0.12.0) (2026-09-20)
4
38
 
5
39
 
package/dist/serve.js CHANGED
@@ -11716,6 +11716,43 @@ function registerImageRoutes(app, deps) {
11716
11716
 
11717
11717
  // src/release/notes.data.ts
11718
11718
  var RELEASES = [
11719
+ {
11720
+ version: "0.13.1",
11721
+ date: "2026-09-22",
11722
+ sections: [
11723
+ {
11724
+ heading: "Alerts",
11725
+ body: "An alert that arrives during a lesson now shows above it, and an alert over an open dialog can be closed or used without closing the dialog, popover or picker beneath it."
11726
+ },
11727
+ {
11728
+ heading: "Create",
11729
+ body: "Closing the side panel or resizing the window while scrolled keeps the shot you were looking at in place. The brief field now has a name that screen readers announce."
11730
+ },
11731
+ {
11732
+ heading: "Learn",
11733
+ body: "A lesson step pushed out of view by resizing the window comes back into view, and a step reached just as the page reloads is no longer lost."
11734
+ }
11735
+ ]
11736
+ },
11737
+ {
11738
+ version: "0.13.0",
11739
+ date: "2026-09-21",
11740
+ title: "Learn teaches Scenri one lesson at a time, alerts are quieter, and the Create feed holds still.",
11741
+ sections: [
11742
+ {
11743
+ heading: "Learn",
11744
+ body: "Learn sits in the top bar, or under Help on a smaller screen, and teaches Scenri in six short lessons: your first shot, adding a product, a presenter, a scene, using a product again, and refining a shot. Each lesson keeps its own place, so you can set one down, take up another and come back to it. A new install is offered a guided first shot, and the guide can step back when you change your mind."
11745
+ },
11746
+ {
11747
+ heading: "Alerts",
11748
+ body: "Alerts are smaller and stack at the bottom of the screen, above the composer rather than over it. The same event twice shares one card, an error stays until you close it, and an alert holds still while you point at it or tab into it."
11749
+ },
11750
+ {
11751
+ heading: "Create",
11752
+ body: "Archiving a shot no longer makes the pictures around it blink, and opening or closing the side panel rearranges the feed in one step."
11753
+ }
11754
+ ]
11755
+ },
11719
11756
  {
11720
11757
  version: "0.12.0",
11721
11758
  date: "2026-09-20",
@@ -12835,14 +12872,24 @@ function registerSystemRoutes(app, deps) {
12835
12872
  }
12836
12873
 
12837
12874
  // src/routes/guide.ts
12838
- var TASKS = ["first-shot", "refine", "product", "presenter", "scene"];
12875
+ var TASKS = ["first-shot", "refine", "product", "presenter", "scene", "reuse"];
12839
12876
  var MILESTONES = ["shot", "refine", "product", "presenter", "scene"];
12840
12877
  var KEY = "guide";
12841
12878
  var isTask = (t) => TASKS.includes(t);
12842
12879
  var isMilestone = (m) => MILESTONES.includes(m);
12843
12880
  var num2 = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : 0;
12844
12881
  function blank(eligible) {
12845
- return { v: 2, eligible, welcome: null, hidden: null, done: {}, dismissed: [], active: null };
12882
+ return {
12883
+ v: 2,
12884
+ eligible,
12885
+ welcome: null,
12886
+ hidden: null,
12887
+ done: {},
12888
+ lessons: {},
12889
+ dismissed: [],
12890
+ active: null,
12891
+ progress: {}
12892
+ };
12846
12893
  }
12847
12894
  function parse2(raw) {
12848
12895
  if (!raw) return null;
@@ -12854,9 +12901,47 @@ function parse2(raw) {
12854
12901
  if (j.done && typeof j.done === "object") {
12855
12902
  for (const [k, at] of Object.entries(j.done)) if (isMilestone(k) && typeof at === "string") done[k] = at;
12856
12903
  }
12904
+ const lessons = {};
12905
+ if (j.lessons && typeof j.lessons === "object") {
12906
+ for (const [k, at] of Object.entries(j.lessons)) if (isTask(k) && typeof at === "string") lessons[k] = at;
12907
+ }
12908
+ const asTask = (a2) => {
12909
+ const b2 = a2?.baseline;
12910
+ return a2 && isTask(a2.task) && typeof a2.brandId === "string" && typeof a2.since === "string" ? {
12911
+ task: a2.task,
12912
+ brandId: a2.brandId,
12913
+ since: a2.since,
12914
+ baseline: { products: num2(b2?.products), presenters: num2(b2?.presenters), scenes: num2(b2?.scenes) },
12915
+ ...a2.paused === true ? { paused: true } : {}
12916
+ } : null;
12917
+ };
12918
+ const asProgress = (v) => {
12919
+ const p = v;
12920
+ const b2 = p?.baseline;
12921
+ return p && typeof p.brandId === "string" && typeof p.since === "string" ? {
12922
+ brandId: p.brandId,
12923
+ since: p.since,
12924
+ baseline: { products: num2(b2?.products), presenters: num2(b2?.presenters), scenes: num2(b2?.scenes) },
12925
+ reached: Array.isArray(p.reached) ? p.reached.filter((m) => typeof m === "string") : [],
12926
+ ...p.paused === true ? { paused: true } : {}
12927
+ } : null;
12928
+ };
12929
+ const progress = {};
12930
+ if (j.progress && typeof j.progress === "object") {
12931
+ for (const [k, v] of Object.entries(j.progress)) {
12932
+ const p = asProgress(v);
12933
+ if (isTask(k) && p) progress[k] = p;
12934
+ }
12935
+ }
12936
+ if (j.parked && typeof j.parked === "object") {
12937
+ for (const [k, v] of Object.entries(j.parked)) {
12938
+ const t = asTask(v);
12939
+ if (isTask(k) && t && !progress[k]) progress[k] = { ...t, reached: [], paused: true };
12940
+ }
12941
+ }
12857
12942
  const a = j.active;
12858
12943
  const b = a?.baseline;
12859
- const active = a && isTask(a.task) && typeof a.brandId === "string" && typeof a.since === "string" ? {
12944
+ const held = a && isTask(a.task) && typeof a.brandId === "string" && typeof a.since === "string" ? {
12860
12945
  task: a.task,
12861
12946
  brandId: a.brandId,
12862
12947
  since: a.since,
@@ -12869,8 +12954,11 @@ function parse2(raw) {
12869
12954
  welcome: j.welcome === "taken" || j.welcome === "declined" ? j.welcome : null,
12870
12955
  hidden: typeof j.hidden === "boolean" ? j.hidden : null,
12871
12956
  done,
12957
+ lessons,
12872
12958
  dismissed: Array.isArray(j.dismissed) ? j.dismissed.filter(isTask) : [],
12873
- active
12959
+ // an old record's one task in hand is that lesson's progress now
12960
+ active: typeof j.active === "string" && isTask(j.active) ? j.active : held?.task ?? null,
12961
+ progress: held && !progress[held.task] ? { ...progress, [held.task]: { ...held, reached: [] } } : progress
12874
12962
  };
12875
12963
  } catch {
12876
12964
  return null;
@@ -12929,21 +13017,33 @@ function readGuide(core, env) {
12929
13017
  changed = true;
12930
13018
  }
12931
13019
  }
12932
- if (r.active && !core.store.getBrand(r.active.brandId)) {
12933
- r.active = null;
13020
+ for (const [t, p] of Object.entries(r.progress)) {
13021
+ if (core.store.getBrand(p.brandId)) continue;
13022
+ delete r.progress[t];
13023
+ if (r.active === t) r.active = null;
12934
13024
  changed = true;
12935
13025
  }
12936
13026
  if (changed) write(core.store, r);
12937
- const a = r.active;
12938
- const nodeKind = a?.task === "first-shot" ? "generation" : a?.task === "refine" ? "edit" : null;
12939
- const eligible = r.eligible && env.SCENRI_NO_GUIDE !== "1";
13027
+ const on = r.active ? r.progress[r.active] : null;
13028
+ const a = r.active && on ? {
13029
+ task: r.active,
13030
+ brandId: on.brandId,
13031
+ since: on.since,
13032
+ baseline: on.baseline,
13033
+ ...on.paused ? { paused: true } : {}
13034
+ } : null;
13035
+ const nodeKind = a?.task === "first-shot" || a?.task === "reuse" ? "generation" : a?.task === "refine" ? "edit" : null;
13036
+ const settled = r.welcome === null && !!r.done.shot;
13037
+ const eligible = r.eligible && !settled && env.SCENRI_NO_GUIDE !== "1";
12940
13038
  return {
12941
13039
  eligible,
12942
13040
  welcome: r.welcome,
12943
13041
  hidden: r.hidden ?? !eligible,
12944
13042
  done: r.done,
13043
+ lessons: r.lessons,
12945
13044
  dismissed: r.dismissed,
12946
13045
  active: a,
13046
+ progress: r.progress,
12947
13047
  activeNodes: a && nodeKind ? core.store.nodesSince(a.brandId, nodeKind, a.since) : [],
12948
13048
  activeDraftId: a?.task === "presenter" ? core.store.presenterDraftSince(a.brandId, a.since) : null,
12949
13049
  counts: a ? countsOf(core, a.brandId) : null
@@ -12961,19 +13061,34 @@ function applyIntent(core, body) {
12961
13061
  if (typeof s.brandId !== "string") return "brandId required";
12962
13062
  const baseline = countsOf(core, s.brandId);
12963
13063
  if (!baseline) return "brand not found";
12964
- const held = r.active;
12965
- if (held && held.paused && held.task === s.task && held.brandId === s.brandId) {
12966
- const { paused: _, ...going } = held;
12967
- r.active = going;
12968
- } else r.active = { task: s.task, brandId: s.brandId, since: core.store.now(), baseline };
13064
+ const guiding = r.active ? r.progress[r.active] : null;
13065
+ if (r.active && r.active !== s.task && guiding) r.progress[r.active] = { ...guiding, paused: true };
13066
+ const mine = r.progress[s.task];
13067
+ if (mine && mine.brandId === s.brandId) {
13068
+ const { paused: _, ...going } = mine;
13069
+ r.progress[s.task] = going;
13070
+ } else r.progress[s.task] = { brandId: s.brandId, since: core.store.now(), baseline, reached: [] };
13071
+ r.active = s.task;
12969
13072
  r.dismissed = r.dismissed.filter((t) => t !== s.task);
12970
13073
  } else if ("finish" in b) {
12971
13074
  if (!isTask(b.finish)) return "unknown task";
12972
- if (r.active?.task === b.finish) r.active = null;
13075
+ if (!r.lessons[b.finish]) r.lessons[b.finish] = (/* @__PURE__ */ new Date()).toISOString();
13076
+ if (r.active === b.finish) r.active = null;
13077
+ delete r.progress[b.finish];
12973
13078
  } else if ("dismiss" in b) {
12974
13079
  if (!isTask(b.dismiss)) return "unknown task";
12975
13080
  if (!r.dismissed.includes(b.dismiss)) r.dismissed.push(b.dismiss);
12976
- if (r.active?.task === b.dismiss) r.active = { ...r.active, paused: true };
13081
+ const shut = r.progress[b.dismiss];
13082
+ if (shut) r.progress[b.dismiss] = { ...shut, paused: true };
13083
+ if (r.active === b.dismiss) r.active = null;
13084
+ } else if ("reached" in b) {
13085
+ const w = b.reached ?? {};
13086
+ if (!isTask(w.task)) return "unknown task";
13087
+ if (typeof w.moment !== "string" || !w.moment) return "moment required";
13088
+ const p = r.progress[w.task];
13089
+ if (!p) return null;
13090
+ if (p.reached.includes(w.moment)) return null;
13091
+ r.progress[w.task] = { ...p, reached: [...p.reached, w.moment].slice(-24) };
12977
13092
  } else if ("hidden" in b) {
12978
13093
  if (typeof b.hidden !== "boolean") return "hidden is true or false";
12979
13094
  r.hidden = b.hidden;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenri",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",