Initial commit of Descrybe v2 without local scratch artifacts.

Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
export {
ACTIVATION_STEPS,
ACTIVATION_STEP_DEFS,
visibleActivationSteps,
activationStepIndexById,
activationIndexFromTutorialStep,
activationIndexFromWorkspace,
type ActivationStep,
type ActivationWorkspaceEvidence
} from "./steps";
export {
ACTIVATION_STORAGE_KEY,
ACTIVATION_PROGRESS_VERSION,
readActivationProgress,
writeActivationProgress,
resolveActivationCursor,
type ActivationProgress,
type ActivationStatus
} from "./storage";
+134
View File
@@ -0,0 +1,134 @@
import { i18n } from "$lib/i18n";
import {
activationIndexFromWorkspace as indexFromWorkspace,
visibleActivationSteps as filterVisible,
type ActivationWorkspaceEvidence
} from "./workspace";
export type { ActivationWorkspaceEvidence } from "./workspace";
/** Checklist step shown on the dashboard (titles/bodies from i18n). */
export type ActivationStep = {
id: string;
title: string;
body: string;
href: string;
optional?: boolean;
feature?: string;
tutorialDoneIds: string[];
};
/**
* Destination / plan-gate definition without resolved copy.
* Titles/bodies resolve via i18n (`activation.step.<id>.title|body`).
*/
export type ActivationStepDef = {
id: string;
href: string;
optional?: boolean;
feature?: string;
tutorialDoneIds: string[];
};
/**
* First-value path on the dashboard checklist.
* Value path (plain language): catalog in → enrich/process → export/push to stores.
* Checklist sequence: enable-fields → Feeds → Map → Sync → Process → optional store-connect → export.
* Aligned with tutorial/steps.ts core path (stores-hub sits with export after process).
* tutorialDoneIds = tour steps that mean this checklist step is already past.
* Deep links: add=1 opens the connect dialog; focus=map|sync highlights the next action on Feeds.
* store-connect Continue opens the guided wizard when stores.hub is allowed.
* ACTIVATION_STEP_DEFS (no i18n) is for destination / plan-gate unit tests.
*/
export const ACTIVATION_STEP_DEFS: ActivationStepDef[] = [
{
id: "enable-fields",
href: "/standard-fields",
tutorialDoneIds: ["connect-source", "map", "sync-sample", "process", "store-connect", "export", "tour-done", "done"]
},
{
id: "connect-source",
href: "/feeds?add=1",
tutorialDoneIds: ["map", "sync-sample", "process", "store-connect", "export", "tour-done", "done"]
},
{
id: "map",
href: "/feeds?focus=map",
tutorialDoneIds: ["sync-sample", "process", "store-connect", "export", "tour-done", "done"]
},
{
id: "sync-sample",
href: "/feeds?focus=sync",
tutorialDoneIds: ["process", "store-connect", "export", "tour-done", "done"]
},
{
id: "process",
href: "/products?type=raw&status=unprocessed",
tutorialDoneIds: ["store-connect", "stores-hub", "export", "tour-done", "done"]
},
{
id: "store-connect",
href: "/stores/wizard",
optional: true,
feature: "stores.hub",
tutorialDoneIds: ["export", "tour-done", "done"]
},
{
id: "export",
href: "/export-feeds",
tutorialDoneIds: ["tour-done", "done"]
}
];
function activationStep(def: ActivationStepDef): ActivationStep {
return {
...def,
title: i18n.t(`activation.step.${def.id}.title`),
body: i18n.t(`activation.step.${def.id}.body`)
};
}
export const ACTIVATION_STEPS: ActivationStep[] = ACTIVATION_STEP_DEFS.map(activationStep);
/** Steps visible for the current plan (omit feature-gated steps the plan denies). */
export function visibleActivationSteps(
can: (featureKey: string) => boolean = () => true
): ActivationStep[] {
return filterVisible(ACTIVATION_STEPS, can);
}
export function activationStepIndexById(
id: string | null | undefined,
steps: ActivationStep[] = ACTIVATION_STEPS
): number {
if (!id) return 0;
const idx = steps.findIndex((s) => s.id === id);
return idx >= 0 ? idx : 0;
}
/** Furthest activation index completed given a tutorial step id (exclusive of current tour focus). */
export function activationIndexFromTutorialStep(
tutorialStepId: string | null | undefined,
steps: ActivationStep[] = ACTIVATION_STEPS
): number {
if (!tutorialStepId) return 0;
// Current tour focus (aligned id) is not completed yet.
if (steps.some((s) => s.id === tutorialStepId)) {
return activationStepIndexById(tutorialStepId, steps);
}
let furthest = 0;
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
if (step.tutorialDoneIds.includes(tutorialStepId)) {
furthest = i + 1;
}
}
return Math.min(furthest, steps.length);
}
export function activationIndexFromWorkspace(
evidence: ActivationWorkspaceEvidence | null | undefined,
steps: ActivationStep[] = ACTIVATION_STEPS
): number {
return indexFromWorkspace(evidence, steps);
}
+134
View File
@@ -0,0 +1,134 @@
import type { TutorialProgress, TutorialStatus } from "$lib/tutorial/types";
import {
ACTIVATION_STEPS,
activationIndexFromWorkspace,
type ActivationStep,
type ActivationWorkspaceEvidence
} from "./steps";
/** Same progress shape as tutorial/storage.ts (`TutorialProgress`). */
export type ActivationProgress = TutorialProgress;
export type ActivationStatus = TutorialStatus;
export const ACTIVATION_STORAGE_KEY = "descrybe.activation.v1";
export const ACTIVATION_PROGRESS_VERSION = 1;
const idleProgress = (): ActivationProgress => ({
version: ACTIVATION_PROGRESS_VERSION,
status: "idle",
stepId: null,
updatedAt: new Date().toISOString()
});
export function readActivationProgress(): ActivationProgress {
if (typeof localStorage === "undefined") return idleProgress();
try {
const raw = localStorage.getItem(ACTIVATION_STORAGE_KEY);
if (!raw) return idleProgress();
const parsed = JSON.parse(raw) as Partial<ActivationProgress>;
if (parsed.version !== ACTIVATION_PROGRESS_VERSION) return idleProgress();
const status = parsed.status;
if (
status !== "idle" &&
status !== "in_progress" &&
status !== "completed" &&
status !== "skipped"
) {
return idleProgress();
}
return {
version: ACTIVATION_PROGRESS_VERSION,
status,
stepId: typeof parsed.stepId === "string" ? parsed.stepId : null,
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : new Date().toISOString()
};
} catch {
return idleProgress();
}
}
export function writeActivationProgress(
status: ActivationStatus,
stepId: string | null
): ActivationProgress {
const next: ActivationProgress = {
version: ACTIVATION_PROGRESS_VERSION,
status,
stepId,
updatedAt: new Date().toISOString()
};
if (typeof localStorage !== "undefined") {
try {
localStorage.setItem(ACTIVATION_STORAGE_KEY, JSON.stringify(next));
} catch {
/* ignore quota / private mode */
}
}
return next;
}
/**
* Resolve the checklist cursor, preferring activation storage and advancing from
* live workspace evidence when further along. Pass `steps` from
* `visibleActivationSteps` so plan-gated optional steps stay out of the cursor.
*/
export function resolveActivationCursor(
progress: ActivationProgress = readActivationProgress(),
workspace?: ActivationWorkspaceEvidence | null,
steps: ActivationStep[] = ACTIVATION_STEPS
): {
progress: ActivationProgress;
currentIndex: number;
completedCount: number;
} {
if (progress.status === "completed") {
return {
progress,
currentIndex: steps.length,
completedCount: steps.length
};
}
if (progress.status === "skipped") {
const storedIdx = progress.stepId ? steps.findIndex((s) => s.id === progress.stepId) : -1;
const index = storedIdx >= 0 ? storedIdx : 0;
const fromWorkspace = activationIndexFromWorkspace(workspace, steps);
const completedCount = Math.max(index, fromWorkspace);
return { progress, currentIndex: index, completedCount };
}
const storedIdx =
progress.status === "idle" || !progress.stepId
? -1
: steps.findIndex((s) => s.id === progress.stepId);
let index = storedIdx >= 0 ? storedIdx : 0;
// Demo tour progress must not fake checklist completion — workspace evidence only.
const fromWorkspace = activationIndexFromWorkspace(workspace, steps);
if (fromWorkspace > index) index = fromWorkspace;
// Gated-out step id (e.g. store-connect when stores.hub denied) → use workspace cursor.
if (progress.stepId && storedIdx < 0 && fromWorkspace > 0) {
index = fromWorkspace;
}
if (index >= steps.length) {
const completed = writeActivationProgress("completed", steps.at(-1)?.id ?? "export");
return {
progress: completed,
currentIndex: steps.length,
completedCount: steps.length
};
}
const stepId = steps[index]?.id ?? steps[0]?.id ?? null;
if (
stepId &&
(progress.status === "idle" ||
(progress.status === "in_progress" && progress.stepId !== stepId))
) {
const next = writeActivationProgress("in_progress", stepId);
return { progress: next, currentIndex: index, completedCount: index };
}
return { progress, currentIndex: index, completedCount: index };
}
+73
View File
@@ -0,0 +1,73 @@
/** Live workspace signals used to advance the checklist without relying only on localStorage. */
export type ActivationWorkspaceEvidence = {
hasEnabledFields?: boolean;
hasSource?: boolean;
hasMapping?: boolean;
hasSyncedSample?: boolean;
hasProcessed?: boolean;
hasStoreConnect?: boolean;
hasExport?: boolean;
};
/** Minimal step shape for gating / workspace cursor (no i18n). */
export type ActivationStepRef = {
id: string;
optional?: boolean;
feature?: string;
};
/** Steps visible for the current plan (omit feature-gated steps the plan denies). */
export function visibleActivationSteps<T extends ActivationStepRef>(
steps: T[],
can: (featureKey: string) => boolean = () => true
): T[] {
return steps.filter((s) => !s.feature || can(s.feature));
}
const evidenceForStep: Record<
string,
(evidence: ActivationWorkspaceEvidence) => boolean | undefined
> = {
"enable-fields": (e) => e.hasEnabledFields,
"connect-source": (e) => e.hasSource,
map: (e) => e.hasMapping,
"sync-sample": (e) => e.hasSyncedSample,
process: (e) => e.hasProcessed,
"store-connect": (e) => e.hasStoreConnect,
export: (e) => e.hasExport
};
function stepEvidenceDone(step: ActivationStepRef, evidence: ActivationWorkspaceEvidence): boolean {
const getter = evidenceForStep[step.id];
return getter ? Boolean(getter(evidence)) : false;
}
/**
* Contiguous completed-step count from workspace state (stop at first incomplete).
* Optional steps do not block later required evidence (e.g. export without a store).
*/
export function activationIndexFromWorkspace(
evidence: ActivationWorkspaceEvidence | null | undefined,
steps: ActivationStepRef[]
): number {
if (!evidence) return 0;
let count = 0;
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
if (stepEvidenceDone(step, evidence)) {
count += 1;
continue;
}
if (step.optional) {
const laterRequiredDone = steps
.slice(i + 1)
.some((s) => !s.optional && stepEvidenceDone(s, evidence));
if (laterRequiredDone) {
count += 1;
continue;
}
}
break;
}
return Math.min(count, steps.length);
}