@peers-app/peers-ui 0.18.3 → 0.18.4

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.
@@ -106,7 +106,7 @@ const PackageInfo = (props) => {
106
106
  peers_sdk_1.rpcServerCalls.openPackage(localPath || peers_sdk_1.packagesRootDir);
107
107
  }, children: "Open Local" })] })] }), (0, jsx_runtime_1.jsx)("input", { type: "text", className: "form-control mb-3 p-0 ps-2", placeholder: "~/peers/packages/my-package", value: localPath || "", onChange: (e) => setLocalPath(e.target.value) })] }), remoteRepoUrl ? ((0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Source URL:", (0, jsx_runtime_1.jsxs)("small", { children: [(0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: `The remote source of this package. Often a github repository URL or a link to the Peers page for the package.` }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-link", onClick: () => {
108
108
  peers_sdk_1.rpcServerCalls.openLinkInBrowser(remoteRepoUrl);
109
- }, children: "Open Remote" })] })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: pkg.qs.remoteRepo, className: "form-control mb-3 p-0 ps-2", disabled: true })] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsx)("hr", {}), (0, jsx_runtime_1.jsxs)("small", { children: ["Version:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "The currently active version of this package. Manage versions in the Versions tab." })] }), activeVersion ? ((0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center mt-1 mb-3", children: [(0, jsx_runtime_1.jsxs)("strong", { className: "me-2", children: ["v", activeVersion.version] }), activeVersion.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ${activeVersion.versionTag.startsWith("beta") ? "text-bg-warning" : "text-bg-info"} me-2`, children: activeVersion.versionTag })), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small me-2", children: activeVersion.packageVersionHash?.substring(0, 8) }), newerLevel === "uptodate" ? ((0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-success", children: "Up to date" })) : newerLevel ? ((0, jsx_runtime_1.jsxs)("span", { className: `badge text-bg-${newerLevel === "major" ? "danger" : newerLevel === "minor" ? "warning" : "info"}`, children: ["Newer ", newerLevel, " version available"] })) : null] })) : ((0, jsx_runtime_1.jsx)("div", { className: "text-muted small mt-1 mb-3", children: "No active version" })), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Auto-Update Range:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Controls which new versions are auto-activated. **Pinned** = never auto-update. **Patch** = same major.minor (e.g. 1.2.x). **Minor** = same major (e.g. 1.x.x). **Latest** = always auto-update to newest." })] }), (0, jsx_runtime_1.jsxs)("select", { className: "form-select form-select-sm", value: versionFollowRange || "latest", onChange: (e) => {
109
+ }, children: "Open Remote" })] })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: pkg.qs.remoteRepo, className: "form-control mb-3 p-0 ps-2", disabled: true })] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsx)("hr", {}), (0, jsx_runtime_1.jsxs)("small", { children: ["Version:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "The currently active version of this package. Manage versions in the Versions tab." })] }), activeVersion ? ((0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center mt-1 mb-3", children: [(0, jsx_runtime_1.jsxs)("strong", { className: "me-2", children: ["v", activeVersion.version] }), activeVersion.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ${activeVersion.versionTag === "dev" ? "text-bg-danger" : activeVersion.versionTag.startsWith("beta") ? "text-bg-warning" : "text-bg-success"} me-2`, children: activeVersion.versionTag })), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small me-2", children: activeVersion.packageVersionHash?.substring(0, 8) }), newerLevel === "uptodate" ? ((0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-success", children: "Up to date" })) : newerLevel ? ((0, jsx_runtime_1.jsxs)("span", { className: `badge text-bg-${newerLevel === "major" ? "danger" : newerLevel === "minor" ? "warning" : "info"}`, children: ["Newer ", newerLevel, " version available"] })) : null] })) : ((0, jsx_runtime_1.jsx)("div", { className: "text-muted small mt-1 mb-3", children: "No active version" })), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Auto-Update Range:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Controls which new versions are auto-activated. **Pinned** = never auto-update. **Patch** = same major.minor (e.g. 1.2.x). **Minor** = same major (e.g. 1.x.x). **Latest** = always auto-update to newest." })] }), (0, jsx_runtime_1.jsxs)("select", { className: "form-select form-select-sm", value: versionFollowRange || "latest", onChange: (e) => {
110
110
  const val = e.target.value;
111
111
  pkg.versionFollowRange = val === "latest" ? undefined : val;
112
112
  }, children: [(0, jsx_runtime_1.jsx)("option", { value: "latest", children: "Latest (auto-update to newest)" }), (0, jsx_runtime_1.jsx)("option", { value: "minor", children: "Minor (same major version)" }), (0, jsx_runtime_1.jsx)("option", { value: "patch", children: "Patch (same major.minor version)" }), (0, jsx_runtime_1.jsx)("option", { value: "pinned", children: "Pinned (no auto-updates)" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `mb-3 ${isPinned ? "opacity-50" : ""}`, children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Follow Version Tags:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Which version tags to auto-activate. Leave empty for **current** (follows the active version's tag). Use `*` for any tag, or a comma-separated list like `stable,prod`." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: pkg.qs.followVersionTags, className: "form-control form-control-sm p-0 ps-2", placeholder: `Following "${activeVersion?.versionTag || "stable"}"`, disabled: isPinned })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-2", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Device-Specific Version Tag:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Override the tag policy for this device only. For example, set to `beta` to test pre-release versions on this device without affecting other devices or group members. This is not synced." })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-2", children: [(0, jsx_runtime_1.jsx)("input", { type: "text", className: "form-control form-control-sm p-0 ps-2", placeholder: "e.g. beta", value: deviceTagDraft, onChange: (e) => setDeviceTagDraft(e.target.value) }), deviceTagDraft && ((0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-secondary", title: "Clear device tag override", onClick: () => setDeviceTagDraft(""), children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-x-lg" }) }))] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsx)("hr", {}), (0, jsx_runtime_1.jsxs)("small", { children: ["Description:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: `This should be edited in the package's README.md. It will automatically update when you restart Peers or reload the package.` })] }), (0, jsx_runtime_1.jsx)(markdown_with_mentions_1.MarkdownWithMentions, { content: pkg.description || "" })] })] }));
@@ -55,7 +55,7 @@ const PackageVersionBadge = ({ activePackageVersionId, packageId, followVersionT
55
55
  if (!data)
56
56
  return null;
57
57
  const { pv, newerLevel } = data;
58
- return ((0, jsx_runtime_1.jsxs)("span", { className: "ms-2", children: [(0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: ["v", pv.version] }), pv.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ms-1 ${pv.versionTag.startsWith("beta") ? "text-bg-warning" : "text-bg-info"}`, style: { fontSize: "0.65em" }, children: pv.versionTag })), newerLevel ? ((0, jsx_runtime_1.jsxs)("span", { className: `badge ms-1 text-bg-${newerLevel === "major" ? "danger" : newerLevel === "minor" ? "warning" : "info"}`, style: { fontSize: "0.65em" }, children: [newerLevel, " update"] })) : ((0, jsx_runtime_1.jsx)("span", { className: "badge ms-1 text-bg-success", style: { fontSize: "0.65em" }, children: "up to date" }))] }));
58
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "ms-2", children: [(0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: ["v", pv.version] }), pv.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ms-1 ${pv.versionTag === "dev" ? "text-bg-danger" : pv.versionTag.startsWith("beta") ? "text-bg-warning" : "text-bg-success"}`, style: { fontSize: "0.65em" }, children: pv.versionTag })), newerLevel ? ((0, jsx_runtime_1.jsxs)("span", { className: `badge ms-1 text-bg-${newerLevel === "major" ? "danger" : newerLevel === "minor" ? "warning" : "info"}`, style: { fontSize: "0.65em" }, children: [newerLevel, " update"] })) : ((0, jsx_runtime_1.jsx)("span", { className: "badge ms-1 text-bg-success", style: { fontSize: "0.65em" }, children: "up to date" }))] }));
59
59
  };
60
60
  const PackageList = () => {
61
61
  const [searchTextObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)(""));
@@ -30,10 +30,12 @@ function formatDate(iso) {
30
30
  function tagBadgeClass(tag) {
31
31
  if (!tag)
32
32
  return "text-bg-secondary";
33
+ if (tag === "dev")
34
+ return "text-bg-danger";
33
35
  if (tag.startsWith("beta"))
34
36
  return "text-bg-warning";
35
37
  if (tag === "stable")
36
- return "text-bg-info";
38
+ return "text-bg-success";
37
39
  return "text-bg-secondary";
38
40
  }
39
41
  const PackageVersionsList = (props) => {
@@ -106,8 +108,16 @@ const PackageVersionsList = (props) => {
106
108
  }
107
109
  async function promoteVersion(pv, newTag) {
108
110
  try {
111
+ const me = await (0, peers_sdk_1.getMe)();
112
+ const history = [...(pv.history ?? [])];
113
+ history.push({
114
+ action: `promoted:${newTag}`,
115
+ by: me.userId,
116
+ at: new Date().toISOString(),
117
+ signature: "",
118
+ });
109
119
  const pvHash = (0, peers_sdk_1.computePackageVersionHash)(pv.version, newTag, pv.packageBundleFileHash, pv.routesBundleFileHash, pv.uiBundleFileHash);
110
- const updated = { ...pv, versionTag: newTag, packageVersionHash: pvHash };
120
+ const updated = { ...pv, versionTag: newTag, packageVersionHash: pvHash, history };
111
121
  await (0, peers_sdk_1.PackageVersions)().signAndSave(updated, { saveAsSnapshot: true });
112
122
  refreshKey(refreshKey() + 1);
113
123
  }
@@ -117,7 +127,7 @@ const PackageVersionsList = (props) => {
117
127
  }
118
128
  async function updateVersion(pv, newSemver) {
119
129
  try {
120
- const pvHash = (0, peers_sdk_1.computePackageVersionHash)(newSemver, pv.versionTag || "beta", pv.packageBundleFileHash, pv.routesBundleFileHash, pv.uiBundleFileHash);
130
+ const pvHash = (0, peers_sdk_1.computePackageVersionHash)(newSemver, pv.versionTag || "dev", pv.packageBundleFileHash, pv.routesBundleFileHash, pv.uiBundleFileHash);
121
131
  const updated = { ...pv, version: newSemver, packageVersionHash: pvHash };
122
132
  await (0, peers_sdk_1.PackageVersions)().signAndSave(updated, { saveAsSnapshot: true });
123
133
  refreshKey(refreshKey() + 1);
@@ -153,7 +163,20 @@ const PackageVersionsList = (props) => {
153
163
  return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: [sorted.length, " version", sorted.length !== 1 ? "s" : ""] }), (0, jsx_runtime_1.jsx)("div", { className: "list-group mt-2", children: sorted.map((pv) => ((0, jsx_runtime_1.jsx)(VersionRow, { pv: pv, isActive: pv.packageVersionId === activeVersionId, isPinned: isPinned, pinning: pinning, activating: activating, deleting: deleting, userMap: userMap, onActivate: activateVersion, onDelete: deleteVersion, onPin: pinVersion, onPromote: promoteVersion, onUpdateVersion: updateVersion }, pv.packageVersionId))) })] }));
154
164
  };
155
165
  exports.PackageVersionsList = PackageVersionsList;
156
- const TAG_OPTIONS = ["beta", "stable"];
166
+ /**
167
+ * Valid promotion targets for each version tag. Promotion only moves forward:
168
+ * dev -> beta -> stable. No demotions allowed.
169
+ */
170
+ function getPromoteTargets(currentTag) {
171
+ switch (currentTag) {
172
+ case "dev":
173
+ return ["beta", "stable"];
174
+ case "beta":
175
+ return ["stable"];
176
+ default:
177
+ return [];
178
+ }
179
+ }
157
180
  function VersionRow(props) {
158
181
  const { pv, isActive, isPinned, pinning, activating, deleting, userMap } = props;
159
182
  const creatorName = userMap.get(pv.createdBy);
@@ -170,12 +193,12 @@ function VersionRow(props) {
170
193
  }
171
194
  setEditingVersion(false);
172
195
  }
173
- const currentTag = pv.versionTag || "beta";
174
- const promoteTargets = TAG_OPTIONS.filter((t) => t !== currentTag);
196
+ const currentTag = pv.versionTag || "dev";
197
+ const promoteTargets = getPromoteTargets(currentTag);
175
198
  return ((0, jsx_runtime_1.jsx)("div", { className: `list-group-item ${isActive ? "list-group-item-success" : ""}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center justify-content-between", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center flex-wrap", children: [editingVersion ? ((0, jsx_runtime_1.jsx)("input", { type: "text", className: "form-control form-control-sm me-2", style: { width: "7em" }, value: versionDraft, onChange: (e) => setVersionDraft(e.target.value), onBlur: commitVersion, onKeyDown: (e) => {
176
199
  if (e.key === "Enter")
177
200
  commitVersion();
178
201
  if (e.key === "Escape")
179
202
  setEditingVersion(false);
180
- } })) : ((0, jsx_runtime_1.jsxs)("strong", { className: "me-2", style: { cursor: "pointer" }, onClick: startEditing, title: "Click to edit version", children: ["v", pv.version] })), pv.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ${tagBadgeClass(pv.versionTag)} me-2`, children: pv.versionTag })), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small me-2", children: pv.packageVersionHash?.substring(0, 8) }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted me-2", children: formatDate(pv.createdAt) }), creatorName && (0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: ["by ", creatorName] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-1 align-items-center", children: [promoteTargets.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "dropdown", children: [(0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-info dropdown-toggle", "data-bs-toggle": "dropdown", title: "Change version tag", children: "Promote" }), (0, jsx_runtime_1.jsx)("ul", { className: "dropdown-menu dropdown-menu-end", children: promoteTargets.map((tag) => ((0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsxs)("button", { className: "dropdown-item", onClick: () => props.onPromote(pv, tag), children: [currentTag, " \u2192 ", tag] }) }, tag))) })] })), isActive ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-secondary", disabled: isPinned || pinning, onClick: () => props.onPin(), title: isPinned ? "Already pinned" : "Pin to this version (disable auto-updates)", children: [pinning ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-pin-fill" })), isPinned ? " Pinned" : " Pin"] }), (0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-success align-self-center", children: "active" })] })) : ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-primary", disabled: activating === pv.packageVersionId, onClick: () => props.onActivate(pv), children: [activating === pv.packageVersionId ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-check2-circle me-1" })), "Activate"] })), !isActive && ((0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-danger", disabled: deleting === pv.packageVersionId, onClick: () => props.onDelete(pv), children: deleting === pv.packageVersionId ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-trash" })) }))] })] }) }));
203
+ } })) : ((0, jsx_runtime_1.jsxs)("strong", { className: "me-2", style: { cursor: "pointer" }, onClick: startEditing, title: "Click to edit version", children: ["v", pv.version] })), pv.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ${tagBadgeClass(pv.versionTag)} me-2`, children: pv.versionTag })), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small me-2", children: pv.packageVersionHash?.substring(0, 8) }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted me-2", children: formatDate(pv.createdAt) }), creatorName && (0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: ["by ", creatorName] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-1 align-items-center", children: [promoteTargets.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "dropdown", children: [(0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-info dropdown-toggle", "data-bs-toggle": "dropdown", title: "Change version tag", children: "Promote" }), (0, jsx_runtime_1.jsx)("ul", { className: "dropdown-menu dropdown-menu-end", children: promoteTargets.map((tag) => ((0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsxs)("button", { className: "dropdown-item", onClick: () => props.onPromote(pv, tag), children: [currentTag, " \u2192 ", tag] }) }, tag))) })] })), isActive ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-secondary", disabled: isPinned || pinning, onClick: () => props.onPin(), title: isPinned ? "Already pinned" : "Pin to this version (disable auto-updates)", children: [pinning ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-pin-fill" })), isPinned ? " Pinned" : " Pin"] }), (0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-info align-self-center", children: "active" })] })) : ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-primary", disabled: activating === pv.packageVersionId, onClick: () => props.onActivate(pv), children: [activating === pv.packageVersionId ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-check2-circle me-1" })), "Activate"] })), !isActive && ((0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-danger", disabled: deleting === pv.packageVersionId, onClick: () => props.onDelete(pv), children: deleting === pv.packageVersionId ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-trash" })) }))] })] }) }));
181
204
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.18.3",
3
+ "version": "0.18.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -28,7 +28,7 @@
28
28
  "lint:fix": "biome check --write ."
29
29
  },
30
30
  "peerDependencies": {
31
- "@peers-app/peers-sdk": "^0.18.3",
31
+ "@peers-app/peers-sdk": "^0.18.4",
32
32
  "bootstrap": "^5.3.3",
33
33
  "react": "^18.0.0",
34
34
  "react-dom": "^18.0.0"
@@ -39,7 +39,7 @@
39
39
  "@babel/preset-env": "^7.24.5",
40
40
  "@babel/preset-react": "^7.24.1",
41
41
  "@babel/preset-typescript": "^7.27.1",
42
- "@peers-app/peers-sdk": "0.18.3",
42
+ "@peers-app/peers-sdk": "0.18.4",
43
43
  "@testing-library/dom": "^10.4.0",
44
44
  "@testing-library/jest-dom": "^6.6.3",
45
45
  "@testing-library/react": "^16.3.0",
@@ -189,7 +189,7 @@ export const PackageInfo = (props: {
189
189
  <strong className="me-2">v{activeVersion.version}</strong>
190
190
  {activeVersion.versionTag && (
191
191
  <span
192
- className={`badge ${activeVersion.versionTag.startsWith("beta") ? "text-bg-warning" : "text-bg-info"} me-2`}
192
+ className={`badge ${activeVersion.versionTag === "dev" ? "text-bg-danger" : activeVersion.versionTag.startsWith("beta") ? "text-bg-warning" : "text-bg-success"} me-2`}
193
193
  >
194
194
  {activeVersion.versionTag}
195
195
  </span>
@@ -80,7 +80,7 @@ const PackageVersionBadge = ({
80
80
  <small className="text-muted">v{pv.version}</small>
81
81
  {pv.versionTag && (
82
82
  <span
83
- className={`badge ms-1 ${pv.versionTag.startsWith("beta") ? "text-bg-warning" : "text-bg-info"}`}
83
+ className={`badge ms-1 ${pv.versionTag === "dev" ? "text-bg-danger" : pv.versionTag.startsWith("beta") ? "text-bg-warning" : "text-bg-success"}`}
84
84
  style={{ fontSize: "0.65em" }}
85
85
  >
86
86
  {pv.versionTag}
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  computePackageVersionHash,
3
+ getMe,
3
4
  type IDoc,
4
5
  type IPackage,
5
6
  type IPackageVersion,
@@ -30,8 +31,9 @@ function formatDate(iso: string): string {
30
31
 
31
32
  function tagBadgeClass(tag?: string): string {
32
33
  if (!tag) return "text-bg-secondary";
34
+ if (tag === "dev") return "text-bg-danger";
33
35
  if (tag.startsWith("beta")) return "text-bg-warning";
34
- if (tag === "stable") return "text-bg-info";
36
+ if (tag === "stable") return "text-bg-success";
35
37
  return "text-bg-secondary";
36
38
  }
37
39
 
@@ -106,6 +108,14 @@ export const PackageVersionsList = (props: { pkg: IDoc<IPackage> }) => {
106
108
 
107
109
  async function promoteVersion(pv: IPackageVersion, newTag: string) {
108
110
  try {
111
+ const me = await getMe();
112
+ const history = [...(pv.history ?? [])];
113
+ history.push({
114
+ action: `promoted:${newTag}`,
115
+ by: me.userId,
116
+ at: new Date().toISOString(),
117
+ signature: "",
118
+ });
109
119
  const pvHash = computePackageVersionHash(
110
120
  pv.version,
111
121
  newTag,
@@ -113,7 +123,7 @@ export const PackageVersionsList = (props: { pkg: IDoc<IPackage> }) => {
113
123
  pv.routesBundleFileHash,
114
124
  pv.uiBundleFileHash,
115
125
  );
116
- const updated = { ...pv, versionTag: newTag, packageVersionHash: pvHash };
126
+ const updated = { ...pv, versionTag: newTag, packageVersionHash: pvHash, history };
117
127
  await PackageVersions().signAndSave(updated, { saveAsSnapshot: true });
118
128
  refreshKey(refreshKey() + 1);
119
129
  } catch (err) {
@@ -125,7 +135,7 @@ export const PackageVersionsList = (props: { pkg: IDoc<IPackage> }) => {
125
135
  try {
126
136
  const pvHash = computePackageVersionHash(
127
137
  newSemver,
128
- pv.versionTag || "beta",
138
+ pv.versionTag || "dev",
129
139
  pv.packageBundleFileHash,
130
140
  pv.routesBundleFileHash,
131
141
  pv.uiBundleFileHash,
@@ -201,7 +211,20 @@ export const PackageVersionsList = (props: { pkg: IDoc<IPackage> }) => {
201
211
  );
202
212
  };
203
213
 
204
- const TAG_OPTIONS = ["beta", "stable"];
214
+ /**
215
+ * Valid promotion targets for each version tag. Promotion only moves forward:
216
+ * dev -> beta -> stable. No demotions allowed.
217
+ */
218
+ function getPromoteTargets(currentTag?: string): string[] {
219
+ switch (currentTag) {
220
+ case "dev":
221
+ return ["beta", "stable"];
222
+ case "beta":
223
+ return ["stable"];
224
+ default:
225
+ return [];
226
+ }
227
+ }
205
228
 
206
229
  function VersionRow(props: {
207
230
  pv: IPackageVersion;
@@ -235,8 +258,8 @@ function VersionRow(props: {
235
258
  setEditingVersion(false);
236
259
  }
237
260
 
238
- const currentTag = pv.versionTag || "beta";
239
- const promoteTargets = TAG_OPTIONS.filter((t) => t !== currentTag);
261
+ const currentTag = pv.versionTag || "dev";
262
+ const promoteTargets = getPromoteTargets(currentTag);
240
263
 
241
264
  return (
242
265
  <div className={`list-group-item ${isActive ? "list-group-item-success" : ""}`}>
@@ -308,7 +331,7 @@ function VersionRow(props: {
308
331
  )}
309
332
  {isPinned ? " Pinned" : " Pin"}
310
333
  </button>
311
- <span className="badge text-bg-success align-self-center">active</span>
334
+ <span className="badge text-bg-info align-self-center">active</span>
312
335
  </>
313
336
  ) : (
314
337
  <button