scenri 0.12.0 → 0.13.0

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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.0](https://github.com/tonygorb/Scenri/compare/v0.12.0...v0.13.0) (2026-09-21)
4
+
5
+
6
+ ### Features
7
+
8
+ * **studio:** rebuild transient alerts as a quiet toast stack ([7b44141](https://github.com/tonygorb/Scenri/commit/7b44141b57cc6748942d97bc6acc27a022260207))
9
+ * **studio:** turn first-use on, with a tutor that can go back ([df6b799](https://github.com/tonygorb/Scenri/commit/df6b799708d65cf31130233dabe2fd0ee1095a28))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **cli:** an install that has made a shot is not offered the first-use welcome ([4dae292](https://github.com/tonygorb/Scenri/commit/4dae292f3e833180448b2310a81573b641408bcc))
15
+ * **create:** keep the feed still when a shot archives or the rail toggles ([b3276cd](https://github.com/tonygorb/Scenri/commit/b3276cd1f68315455e8b29d7dfdf13c768a0438a))
16
+ * **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))
17
+ * **studio:** a lesson owns its progress, and only the screen is shared ([8c58bdb](https://github.com/tonygorb/Scenri/commit/8c58bdbc06124d2f9fda0775a41f84fa156e10ea))
18
+ * **studio:** a library lesson keeps its button in sight on narrow screens ([93869f4](https://github.com/tonygorb/Scenri/commit/93869f4f1f1fa8791c15ff893ef8ac536deda721))
19
+ * **studio:** a step scrolled out of sight is not gone ([49ceccb](https://github.com/tonygorb/Scenri/commit/49ceccb8d7dc41daf8327ecefb8353f98ed94e70))
20
+ * **studio:** alerts stand above a docked composer instead of over its buttons ([b6756c7](https://github.com/tonygorb/Scenri/commit/b6756c79c63419ca2ed146fc2e3c6d868eeb6f79))
21
+ * **studio:** Learn gives the keyboard back to whichever control opened it ([e9c5945](https://github.com/tonygorb/Scenri/commit/e9c5945d60e6ceb55704da1130fe23a988c6044f))
22
+ * **studio:** no empty alert on every page, and a held toast stays held ([3305f75](https://github.com/tonygorb/Scenri/commit/3305f7542fb38f604c9b4087c59aa7cfe0d4cf6b))
23
+ * **studio:** refining teaches how to find a shot, instead of picking one for you ([228938f](https://github.com/tonygorb/Scenri/commit/228938f2440e17b48d9bab836e7c4aeeb0a74cef))
24
+
3
25
  ## [0.12.0](https://github.com/tonygorb/Scenri/compare/v0.11.1...v0.12.0) (2026-09-20)
4
26
 
5
27
 
package/dist/serve.js CHANGED
@@ -11716,6 +11716,25 @@ function registerImageRoutes(app, deps) {
11716
11716
 
11717
11717
  // src/release/notes.data.ts
11718
11718
  var RELEASES = [
11719
+ {
11720
+ version: "0.13.0",
11721
+ date: "2026-09-21",
11722
+ title: "Learn teaches Scenri one lesson at a time, alerts are quieter, and the Create feed holds still.",
11723
+ sections: [
11724
+ {
11725
+ heading: "Learn",
11726
+ 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."
11727
+ },
11728
+ {
11729
+ heading: "Alerts",
11730
+ 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."
11731
+ },
11732
+ {
11733
+ heading: "Create",
11734
+ 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."
11735
+ }
11736
+ ]
11737
+ },
11719
11738
  {
11720
11739
  version: "0.12.0",
11721
11740
  date: "2026-09-20",
@@ -12835,14 +12854,24 @@ function registerSystemRoutes(app, deps) {
12835
12854
  }
12836
12855
 
12837
12856
  // src/routes/guide.ts
12838
- var TASKS = ["first-shot", "refine", "product", "presenter", "scene"];
12857
+ var TASKS = ["first-shot", "refine", "product", "presenter", "scene", "reuse"];
12839
12858
  var MILESTONES = ["shot", "refine", "product", "presenter", "scene"];
12840
12859
  var KEY = "guide";
12841
12860
  var isTask = (t) => TASKS.includes(t);
12842
12861
  var isMilestone = (m) => MILESTONES.includes(m);
12843
12862
  var num2 = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : 0;
12844
12863
  function blank(eligible) {
12845
- return { v: 2, eligible, welcome: null, hidden: null, done: {}, dismissed: [], active: null };
12864
+ return {
12865
+ v: 2,
12866
+ eligible,
12867
+ welcome: null,
12868
+ hidden: null,
12869
+ done: {},
12870
+ lessons: {},
12871
+ dismissed: [],
12872
+ active: null,
12873
+ progress: {}
12874
+ };
12846
12875
  }
12847
12876
  function parse2(raw) {
12848
12877
  if (!raw) return null;
@@ -12854,9 +12883,47 @@ function parse2(raw) {
12854
12883
  if (j.done && typeof j.done === "object") {
12855
12884
  for (const [k, at] of Object.entries(j.done)) if (isMilestone(k) && typeof at === "string") done[k] = at;
12856
12885
  }
12886
+ const lessons = {};
12887
+ if (j.lessons && typeof j.lessons === "object") {
12888
+ for (const [k, at] of Object.entries(j.lessons)) if (isTask(k) && typeof at === "string") lessons[k] = at;
12889
+ }
12890
+ const asTask = (a2) => {
12891
+ const b2 = a2?.baseline;
12892
+ return a2 && isTask(a2.task) && typeof a2.brandId === "string" && typeof a2.since === "string" ? {
12893
+ task: a2.task,
12894
+ brandId: a2.brandId,
12895
+ since: a2.since,
12896
+ baseline: { products: num2(b2?.products), presenters: num2(b2?.presenters), scenes: num2(b2?.scenes) },
12897
+ ...a2.paused === true ? { paused: true } : {}
12898
+ } : null;
12899
+ };
12900
+ const asProgress = (v) => {
12901
+ const p = v;
12902
+ const b2 = p?.baseline;
12903
+ return p && typeof p.brandId === "string" && typeof p.since === "string" ? {
12904
+ brandId: p.brandId,
12905
+ since: p.since,
12906
+ baseline: { products: num2(b2?.products), presenters: num2(b2?.presenters), scenes: num2(b2?.scenes) },
12907
+ reached: Array.isArray(p.reached) ? p.reached.filter((m) => typeof m === "string") : [],
12908
+ ...p.paused === true ? { paused: true } : {}
12909
+ } : null;
12910
+ };
12911
+ const progress = {};
12912
+ if (j.progress && typeof j.progress === "object") {
12913
+ for (const [k, v] of Object.entries(j.progress)) {
12914
+ const p = asProgress(v);
12915
+ if (isTask(k) && p) progress[k] = p;
12916
+ }
12917
+ }
12918
+ if (j.parked && typeof j.parked === "object") {
12919
+ for (const [k, v] of Object.entries(j.parked)) {
12920
+ const t = asTask(v);
12921
+ if (isTask(k) && t && !progress[k]) progress[k] = { ...t, reached: [], paused: true };
12922
+ }
12923
+ }
12857
12924
  const a = j.active;
12858
12925
  const b = a?.baseline;
12859
- const active = a && isTask(a.task) && typeof a.brandId === "string" && typeof a.since === "string" ? {
12926
+ const held = a && isTask(a.task) && typeof a.brandId === "string" && typeof a.since === "string" ? {
12860
12927
  task: a.task,
12861
12928
  brandId: a.brandId,
12862
12929
  since: a.since,
@@ -12869,8 +12936,11 @@ function parse2(raw) {
12869
12936
  welcome: j.welcome === "taken" || j.welcome === "declined" ? j.welcome : null,
12870
12937
  hidden: typeof j.hidden === "boolean" ? j.hidden : null,
12871
12938
  done,
12939
+ lessons,
12872
12940
  dismissed: Array.isArray(j.dismissed) ? j.dismissed.filter(isTask) : [],
12873
- active
12941
+ // an old record's one task in hand is that lesson's progress now
12942
+ active: typeof j.active === "string" && isTask(j.active) ? j.active : held?.task ?? null,
12943
+ progress: held && !progress[held.task] ? { ...progress, [held.task]: { ...held, reached: [] } } : progress
12874
12944
  };
12875
12945
  } catch {
12876
12946
  return null;
@@ -12929,21 +12999,33 @@ function readGuide(core, env) {
12929
12999
  changed = true;
12930
13000
  }
12931
13001
  }
12932
- if (r.active && !core.store.getBrand(r.active.brandId)) {
12933
- r.active = null;
13002
+ for (const [t, p] of Object.entries(r.progress)) {
13003
+ if (core.store.getBrand(p.brandId)) continue;
13004
+ delete r.progress[t];
13005
+ if (r.active === t) r.active = null;
12934
13006
  changed = true;
12935
13007
  }
12936
13008
  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";
13009
+ const on = r.active ? r.progress[r.active] : null;
13010
+ const a = r.active && on ? {
13011
+ task: r.active,
13012
+ brandId: on.brandId,
13013
+ since: on.since,
13014
+ baseline: on.baseline,
13015
+ ...on.paused ? { paused: true } : {}
13016
+ } : null;
13017
+ const nodeKind = a?.task === "first-shot" || a?.task === "reuse" ? "generation" : a?.task === "refine" ? "edit" : null;
13018
+ const settled = r.welcome === null && !!r.done.shot;
13019
+ const eligible = r.eligible && !settled && env.SCENRI_NO_GUIDE !== "1";
12940
13020
  return {
12941
13021
  eligible,
12942
13022
  welcome: r.welcome,
12943
13023
  hidden: r.hidden ?? !eligible,
12944
13024
  done: r.done,
13025
+ lessons: r.lessons,
12945
13026
  dismissed: r.dismissed,
12946
13027
  active: a,
13028
+ progress: r.progress,
12947
13029
  activeNodes: a && nodeKind ? core.store.nodesSince(a.brandId, nodeKind, a.since) : [],
12948
13030
  activeDraftId: a?.task === "presenter" ? core.store.presenterDraftSince(a.brandId, a.since) : null,
12949
13031
  counts: a ? countsOf(core, a.brandId) : null
@@ -12961,19 +13043,34 @@ function applyIntent(core, body) {
12961
13043
  if (typeof s.brandId !== "string") return "brandId required";
12962
13044
  const baseline = countsOf(core, s.brandId);
12963
13045
  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 };
13046
+ const guiding = r.active ? r.progress[r.active] : null;
13047
+ if (r.active && r.active !== s.task && guiding) r.progress[r.active] = { ...guiding, paused: true };
13048
+ const mine = r.progress[s.task];
13049
+ if (mine && mine.brandId === s.brandId) {
13050
+ const { paused: _, ...going } = mine;
13051
+ r.progress[s.task] = going;
13052
+ } else r.progress[s.task] = { brandId: s.brandId, since: core.store.now(), baseline, reached: [] };
13053
+ r.active = s.task;
12969
13054
  r.dismissed = r.dismissed.filter((t) => t !== s.task);
12970
13055
  } else if ("finish" in b) {
12971
13056
  if (!isTask(b.finish)) return "unknown task";
12972
- if (r.active?.task === b.finish) r.active = null;
13057
+ if (!r.lessons[b.finish]) r.lessons[b.finish] = (/* @__PURE__ */ new Date()).toISOString();
13058
+ if (r.active === b.finish) r.active = null;
13059
+ delete r.progress[b.finish];
12973
13060
  } else if ("dismiss" in b) {
12974
13061
  if (!isTask(b.dismiss)) return "unknown task";
12975
13062
  if (!r.dismissed.includes(b.dismiss)) r.dismissed.push(b.dismiss);
12976
- if (r.active?.task === b.dismiss) r.active = { ...r.active, paused: true };
13063
+ const shut = r.progress[b.dismiss];
13064
+ if (shut) r.progress[b.dismiss] = { ...shut, paused: true };
13065
+ if (r.active === b.dismiss) r.active = null;
13066
+ } else if ("reached" in b) {
13067
+ const w = b.reached ?? {};
13068
+ if (!isTask(w.task)) return "unknown task";
13069
+ if (typeof w.moment !== "string" || !w.moment) return "moment required";
13070
+ const p = r.progress[w.task];
13071
+ if (!p) return null;
13072
+ if (p.reached.includes(w.moment)) return null;
13073
+ r.progress[w.task] = { ...p, reached: [...p.reached, w.moment].slice(-24) };
12977
13074
  } else if ("hidden" in b) {
12978
13075
  if (typeof b.hidden !== "boolean") return "hidden is true or false";
12979
13076
  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.0",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",