@cosmicdrift/kumiko-renderer-web 0.4.0 → 0.5.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,81 @@
1
1
  # @cosmicdrift/kumiko-renderer-web
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7ff69ab: feat(es-ops): Phase 1 — file-based seed-migrations
8
+
9
+ Neues first-class Operations-Pattern fürs Framework. Liefert `seed-migrations`
10
+ als drizzle-migrate-equivalent für Event-Sourcing-Aggregate-Updates die
11
+ idempotent-Seeder nicht erfassen können (z.B. „Member hat schon eine
12
+ Rolle, aber jetzt soll noch eine dazukommen").
13
+
14
+ Public-API:
15
+
16
+ - `runProdApp({ seedsDir })` — Auto-apply pending Migrations beim Boot
17
+ - `SeedMigration`-Interface (default-Export einer `seeds/<id>.ts`-File)
18
+ - `SeedMigrationContext` mit `systemWriteAs` (ruft existing write-handler
19
+ als System-User) + Read-Helpers (`findUserByEmail`,
20
+ `findMembershipsOfUser`, `findTenants`)
21
+ - CLI: `bunx kumiko ops seed:new|status|apply`
22
+ - Tracking-Table `kumiko_es_operations` mit `operation_type`-Discriminator
23
+ (vorbereitet auf Phase 2+ Operations: projection-rebuild, event-replay,
24
+ stream-migration, ...)
25
+ - Env-Flags: `KUMIKO_SKIP_ES_OPS=1` (alle skippen für Recovery),
26
+ `KUMIKO_SKIP_ES_OPS_<ID>=1` (einzelne kaputte skippen)
27
+
28
+ Garantien: single-run via tracking, atomic via per-migration-Tx,
29
+ chronological order via filename-prefix, fail-stop bei Failure (kein
30
+ Partial-Apply), ES-konform via Handler-Dispatch.
31
+
32
+ Sub-path-Export: `@cosmicdrift/kumiko-framework/es-ops`
33
+
34
+ Plan-Doc: `kumiko-platform/docs/plans/features/es-ops.md`
35
+ Recipe: `samples/recipes/seed-migration/`
36
+ Driver-Use-Case: publicstatus admin-roles-drift (parallel-Branch
37
+ `feat/es-ops-driver-admin-roles`).
38
+
39
+ Phase 2+ skizziert + offen markiert — Implementation pro Use-Case.
40
+
41
+ ### Patch Changes
42
+
43
+ - Updated dependencies [7ff69ab]
44
+ - @cosmicdrift/kumiko-dispatcher-live@0.5.0
45
+ - @cosmicdrift/kumiko-headless@0.5.0
46
+ - @cosmicdrift/kumiko-renderer@0.5.0
47
+
48
+ ## 0.4.1
49
+
50
+ ### Patch Changes
51
+
52
+ - 010b410: feat(auth-email-password): "Bestätigungs-Mail erneut senden" im LoginScreen
53
+
54
+ LoginScreen bietet bei reason=email_not_verified jetzt einen Resend-Link
55
+ im Fehler-Banner — der existierende `requestEmailVerification`-Endpoint
56
+ wird direkt aufgerufen, der Banner wechselt nach Erfolg zum Info-Variant
57
+ ("Wir haben dir eine neue Bestätigungs-Mail geschickt.").
58
+
59
+ UX-Details:
60
+
61
+ - Bei 429 → inline-Hint "Bitte warte kurz und versuche es erneut."
62
+ - Bei Netzwerk/sonstigen Fehlern → inline-Hint "Konnte nicht senden."
63
+ - Anti-Typo-Gate: ändert der User die Email-Eingabe nach dem Login-Fail,
64
+ verschwindet der Resend-Link — sonst würde Resend silent-success an die
65
+ geänderte (potentiell typoed) Adresse gehen ohne User-Feedback.
66
+ - Andere Failure-Codes (invalid_credentials etc.) zeigen weiterhin keinen
67
+ Resend-Link.
68
+
69
+ i18n: 4 neue Keys (DE+EN) im `auth.login.resend*`-Namespace, additive.
70
+ Apps die ihre Translations override-en müssen nichts ändern.
71
+
72
+ Additive UI-Feature — keine API-Breaks, keine Schema-Migration.
73
+
74
+ - Updated dependencies [010b410]
75
+ - @cosmicdrift/kumiko-dispatcher-live@0.4.1
76
+ - @cosmicdrift/kumiko-headless@0.4.1
77
+ - @cosmicdrift/kumiko-renderer@0.4.1
78
+
3
79
  ## 0.4.0
4
80
 
5
81
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer-web",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -16,9 +16,9 @@
16
16
  "./styles.css": "./src/styles.css"
17
17
  },
18
18
  "dependencies": {
19
- "@cosmicdrift/kumiko-dispatcher-live": "0.4.0",
20
- "@cosmicdrift/kumiko-headless": "0.4.0",
21
- "@cosmicdrift/kumiko-renderer": "0.4.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.5.0",
20
+ "@cosmicdrift/kumiko-headless": "0.5.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.5.0",
22
22
  "@radix-ui/react-dialog": "^1.1.15",
23
23
  "@radix-ui/react-dropdown-menu": "^2.1.16",
24
24
  "@radix-ui/react-label": "^2.1.8",
@@ -0,0 +1,132 @@
1
+ import type {
2
+ ConfigCascade,
3
+ ConfigCascadeLevel,
4
+ ConfigScope,
5
+ ConfigValueSource,
6
+ } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { useTranslation } from "@cosmicdrift/kumiko-renderer";
8
+ import type { ReactNode } from "react";
9
+ import { useState } from "react";
10
+
11
+ const SOURCE_I18N_KEY: Record<ConfigValueSource, string> = {
12
+ "user-row": "config.source.user",
13
+ "tenant-row": "config.source.tenant",
14
+ "system-row": "config.source.system",
15
+ "app-override": "config.source.appOverride",
16
+ computed: "config.source.computed",
17
+ default: "config.source.default",
18
+ missing: "config.source.missing",
19
+ };
20
+
21
+ const SOURCE_COLORS: Record<ConfigValueSource, string> = {
22
+ "user-row": "text-blue-600 bg-blue-50 border-blue-200",
23
+ "tenant-row": "text-green-600 bg-green-50 border-green-200",
24
+ "system-row": "text-purple-600 bg-purple-50 border-purple-200",
25
+ "app-override": "text-orange-600 bg-orange-50 border-orange-200",
26
+ computed: "text-teal-600 bg-teal-50 border-teal-200",
27
+ default: "text-gray-500 bg-gray-50 border-gray-200",
28
+ missing: "text-red-500 bg-red-50 border-red-200",
29
+ };
30
+
31
+ function SourceBadge({ source }: { source: ConfigValueSource }): ReactNode {
32
+ const t = useTranslation();
33
+ return (
34
+ <span
35
+ className={`inline-flex items-center rounded border px-1.5 py-0.5 text-xs font-medium ${SOURCE_COLORS[source]}`}
36
+ >
37
+ {t(SOURCE_I18N_KEY[source])}
38
+ </span>
39
+ );
40
+ }
41
+
42
+ function formatValue(value: string | number | boolean | undefined, hasValue: boolean): string {
43
+ if (!hasValue || value === undefined) return "—";
44
+ return String(value);
45
+ }
46
+
47
+ function scopeToSource(scope: ConfigScope): ConfigValueSource {
48
+ if (scope === "user") return "user-row";
49
+ if (scope === "tenant") return "tenant-row";
50
+ return "system-row";
51
+ }
52
+
53
+ type ConfigCascadeViewProps = {
54
+ readonly cascade: ConfigCascade;
55
+ readonly screenScope: ConfigScope;
56
+ readonly onReset?: (key: string, scope: ConfigScope) => void;
57
+ readonly qualifiedKey?: string;
58
+ };
59
+
60
+ export function ConfigCascadeView({
61
+ cascade,
62
+ screenScope,
63
+ onReset,
64
+ qualifiedKey,
65
+ }: ConfigCascadeViewProps): ReactNode {
66
+ const t = useTranslation();
67
+ const [expanded, setExpanded] = useState(false);
68
+
69
+ // Safety net: callers should already filter malformed cascades, but
70
+ // a missing levels-array (e.g. from a partial mock) shouldn't crash
71
+ // the screen.
72
+ if (!Array.isArray(cascade?.levels)) return null;
73
+
74
+ const activeLevel = cascade.levels.find((l) => l.isActive);
75
+ const screenScopeSource = scopeToSource(screenScope);
76
+ const hasOverride = activeLevel?.source === screenScopeSource;
77
+
78
+ return (
79
+ <div className="mt-1 text-xs">
80
+ <button
81
+ type="button"
82
+ onClick={() => setExpanded(!expanded)}
83
+ className="flex items-center gap-1 text-gray-500 hover:text-gray-700 cursor-pointer"
84
+ >
85
+ <span className="text-[10px]">{expanded ? "▼" : "▶"}</span>
86
+ {activeLevel ? (
87
+ <>
88
+ <SourceBadge source={activeLevel.source} />
89
+ <span className="text-gray-400">
90
+ {formatValue(activeLevel.value, activeLevel.hasValue)}
91
+ </span>
92
+ </>
93
+ ) : (
94
+ <span className="text-gray-400">{t("config.cascade.noValue")}</span>
95
+ )}
96
+ </button>
97
+
98
+ {expanded ? (
99
+ <div className="mt-1 flex flex-col gap-0.5 pl-3 border-l-2 border-gray-100">
100
+ {cascade.levels.map((level) => (
101
+ <CascadeLevelRow key={level.source} level={level} />
102
+ ))}
103
+
104
+ {hasOverride && onReset && qualifiedKey ? (
105
+ <button
106
+ type="button"
107
+ onClick={() => onReset(qualifiedKey, screenScope)}
108
+ className="mt-1 self-start text-[10px] text-orange-500 hover:text-orange-700 cursor-pointer underline"
109
+ >
110
+ {t("config.cascade.resetTo", { scope: t(SOURCE_I18N_KEY[screenScopeSource]) })}
111
+ </button>
112
+ ) : null}
113
+ </div>
114
+ ) : null}
115
+ </div>
116
+ );
117
+ }
118
+
119
+ function CascadeLevelRow({ level }: { level: ConfigCascadeLevel }): ReactNode {
120
+ const t = useTranslation();
121
+ return (
122
+ <div
123
+ className={`flex items-center gap-1.5 ${level.isActive ? "font-medium" : "text-gray-400"}`}
124
+ >
125
+ <SourceBadge source={level.source} />
126
+ <span>{formatValue(level.value, level.hasValue)}</span>
127
+ {level.isActive ? (
128
+ <span className="text-[10px] text-gray-400">{t("config.cascade.activeMarker")}</span>
129
+ ) : null}
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,36 @@
1
+ import type { ConfigValueSource } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { ReactNode } from "react";
3
+
4
+ const SOURCE_CONFIG: Record<ConfigValueSource, { label: string; bg: string; text: string }> = {
5
+ "user-row": { label: "User", bg: "#dbeafe", text: "#1e40af" },
6
+ "tenant-row": { label: "Tenant", bg: "#dcfce7", text: "#166534" },
7
+ "system-row": { label: "System", bg: "#f3e8ff", text: "#6b21a8" },
8
+ "app-override": { label: "Override", bg: "#ffedd5", text: "#9a3412" },
9
+ computed: { label: "Computed", bg: "#ccfbf1", text: "#115e59" },
10
+ default: { label: "Default", bg: "#f3f4f6", text: "#4b5563" },
11
+ missing: { label: "Missing", bg: "#fee2e2", text: "#991b1b" },
12
+ };
13
+
14
+ export function ConfigSourceBadge({ source }: { readonly source: ConfigValueSource }): ReactNode {
15
+ const cfg = SOURCE_CONFIG[source];
16
+
17
+ return (
18
+ <span
19
+ style={{
20
+ display: "inline-flex",
21
+ alignItems: "center",
22
+ padding: "0 6px",
23
+ fontSize: "11px",
24
+ fontWeight: 500,
25
+ lineHeight: "18px",
26
+ borderRadius: "4px",
27
+ backgroundColor: cfg.bg,
28
+ color: cfg.text,
29
+ marginLeft: "6px",
30
+ whiteSpace: "nowrap",
31
+ }}
32
+ >
33
+ {cfg.label}
34
+ </span>
35
+ );
36
+ }
@@ -130,7 +130,16 @@ function DefaultBanner({
130
130
 
131
131
  // ---- Field (Label + Error) ----
132
132
 
133
- function DefaultField({ id, label, required, issues, children, testId }: FieldProps): ReactNode {
133
+ function DefaultField({
134
+ id,
135
+ label,
136
+ required,
137
+ issues,
138
+ labelAppendix,
139
+ fieldAppendix,
140
+ children,
141
+ testId,
142
+ }: FieldProps): ReactNode {
134
143
  const t = useTranslation();
135
144
  const hasError = issues !== undefined && issues.length > 0;
136
145
  return (
@@ -148,9 +157,11 @@ function DefaultField({ id, label, required, issues, children, testId }: FieldPr
148
157
  )}
149
158
  >
150
159
  {label}
160
+ {labelAppendix !== undefined && <>{labelAppendix}</>}
151
161
  {required === true && <span className="ml-0.5 text-destructive">*</span>}
152
162
  </LabelPrimitive.Root>
153
163
  {children}
164
+ {fieldAppendix !== undefined && <div className="mt-1">{fieldAppendix}</div>}
154
165
  {hasError && (
155
166
  <div
156
167
  role="alert"
@@ -1255,6 +1266,9 @@ function DefaultHeading({ variant = "page", children, testId }: HeadingProps): R
1255
1266
  );
1256
1267
  }
1257
1268
 
1269
+ import { ConfigCascadeView as DefaultConfigCascadeView } from "../components/config-cascade";
1270
+ import { ConfigSourceBadge as DefaultConfigSourceBadge } from "../components/config-source-badge";
1271
+
1258
1272
  export const defaultPrimitives: CorePrimitives = {
1259
1273
  Button: DefaultButton,
1260
1274
  Banner: DefaultBanner,
@@ -1268,4 +1282,6 @@ export const defaultPrimitives: CorePrimitives = {
1268
1282
  Text: DefaultText,
1269
1283
  Heading: DefaultHeading,
1270
1284
  Dialog: DefaultDialog,
1285
+ ConfigSourceBadge: DefaultConfigSourceBadge,
1286
+ ConfigCascadeView: DefaultConfigCascadeView,
1271
1287
  };