@k2b/cloud 0.39.0 → 0.40.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/README.md +8 -0
- package/package.json +8 -4
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +216 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +36 -15
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +2 -2
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/background-review.ts +72 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +398 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +669 -0
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action.tsx +481 -0
- package/src/ai/chat/turn-view.tsx +10 -160
- package/src/ai/chat/view-block.tsx +15 -5
- package/src/ai/chat-task-contracts.ts +4 -1
- package/src/ai/chat-task-reasons-migrate.ts +7 -0
- package/src/ai/chat-task-reasons.ts +1 -1
- package/src/ai/chat-tasks.ts +55 -25
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +190 -39
- package/src/ai/index.ts +2 -0
- package/src/ai/migrate.ts +11 -2
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/routes.ts +145 -1
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +227 -24
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-wording.ts +24 -2
- package/src/ai/types.ts +28 -3
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +2 -0
- package/src/api/app-approval.ts +46 -5
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +2 -0
- package/src/api/auth.ts +25 -6
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +6 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +3 -0
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +80 -33
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +180 -238
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/app-approval.ts +54 -5
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +45 -3
- package/src/browser/composer-insert.ts +10 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +11 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +412 -10
- package/src/browser/panel.ts +37 -0
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +120 -13
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/seen-hints.ts +35 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/services/app-approval.ts +382 -59
- package/src/services/approvals/device.ts +7 -5
- package/src/services/approvals/index.ts +6 -5
- package/src/services/approvals/list.ts +26 -5
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +11 -0
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +97 -16
- package/src/services/session/recent.ts +13 -0
- package/src/services/webauthn.ts +12 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +10 -2
- package/src/ssr/Layout.tsx +25 -2
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +435 -274
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +9 -1
- package/src/ssr/PanelRuntime.island.tsx +7 -0
- package/src/ssr/PanelSlot.tsx +53 -3
- package/src/ssr/ShellRuntime.island.tsx +11 -2
- package/src/ssr/app-color.ts +14 -9
- package/src/ssr/app-navigation.ts +16 -3
- package/src/ssr/compact-shell.browser-harness.ts +30 -22
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +20 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +2 -0
- package/src/ssr/islands/index.ts +4 -3
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-slot.browser-harness.tsx +53 -4
- package/src/ssr/platform-messages.ts +18 -0
- package/src/ssr/shell-data.tsx +2 -1
- package/src/styles/effects.css +198 -9
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +49 -12
- package/src/styles/panel.css +47 -4
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +17 -11
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +17 -22
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { dates, i18n } from "@k2b/stdlib";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
CapabilityActionCallReview,
|
|
4
|
+
CapabilityActionOutcome,
|
|
5
|
+
CapabilityActionReceipt,
|
|
6
|
+
CapabilityActionSentences,
|
|
7
|
+
CapabilityAskReason,
|
|
8
|
+
CapabilityPerson,
|
|
9
|
+
CapabilityPresentationCatalog,
|
|
10
|
+
CapabilitySlot,
|
|
11
|
+
} from "../contracts/capabilities";
|
|
3
12
|
import { canonicalLocale, localeFallbackChain, normalizeLocale } from "../shared/locale";
|
|
4
13
|
import { normalizeTimeZone } from "../shared/time";
|
|
5
14
|
|
|
@@ -78,12 +87,38 @@ const words = i18n.define({
|
|
|
78
87
|
baseLocale: "en",
|
|
79
88
|
messages: {
|
|
80
89
|
en: {
|
|
90
|
+
question: ({ sentence }: { sentence: string }) => `${sentence}?`,
|
|
91
|
+
rejected: ({ action }: { action: string }) => `Not done: ${action}`,
|
|
92
|
+
unknown: ({ action }: { action: string }) => `Not known yet whether this happened: ${action}`,
|
|
93
|
+
unknownStill: ({ action }: { action: string }) => `Still not known whether this happened: ${action}`,
|
|
94
|
+
people: ({ count }: { count: number }) => `${count} ${new Intl.PluralRules("en").select(count) === "one" ? "person" : "people"}`,
|
|
95
|
+
sends: "I'm asking because this leaves Cloud.",
|
|
96
|
+
deletes: "I'm asking because you can't bring it back afterwards.",
|
|
97
|
+
cannot_undo: "I'm asking because you can't set this back.",
|
|
98
|
+
involves_people: "I'm asking because this involves or notifies other people.",
|
|
99
|
+
changes_access: "I'm asking because this changes who can see it.",
|
|
100
|
+
keeps_working: "I'm asking because this keeps acting without you.",
|
|
101
|
+
sensitive: "I'm asking because this concerns payments, invoices, addresses, or sign-in data.",
|
|
102
|
+
app_asks: ({ app }: { app: string }) => `I'm asking because ${app} always asks for this.`,
|
|
81
103
|
yes: "yes",
|
|
82
104
|
no: "no",
|
|
83
105
|
more: ({ count }: { count: number }) => `${count} more`,
|
|
84
106
|
why: "Why",
|
|
85
107
|
},
|
|
86
108
|
de: {
|
|
109
|
+
question: ({ sentence }) => `${sentence}?`,
|
|
110
|
+
rejected: ({ action }) => `Nicht ausgeführt: ${action}`,
|
|
111
|
+
unknown: ({ action }) => `Ob ${action} passiert ist, weiß ich noch nicht.`,
|
|
112
|
+
unknownStill: ({ action }) => `Ob ${action} passiert ist, weiß ich weiterhin nicht.`,
|
|
113
|
+
people: ({ count }) => `${count} ${new Intl.PluralRules("de").select(count) === "one" ? "Person" : "Personen"}`,
|
|
114
|
+
sends: "Ich frage, weil das Cloud verlässt.",
|
|
115
|
+
deletes: "Ich frage, weil du das danach nicht zurückholen kannst.",
|
|
116
|
+
cannot_undo: "Ich frage, weil du das nicht zurückstellen kannst.",
|
|
117
|
+
involves_people: "Ich frage, weil das andere Personen einbindet oder benachrichtigt.",
|
|
118
|
+
changes_access: "Ich frage, weil sich ändert, wer das sehen kann.",
|
|
119
|
+
keeps_working: "Ich frage, weil das ohne dich weiterwirkt.",
|
|
120
|
+
sensitive: "Ich frage, weil das Zahlungen, Rechnungen, Adressen oder Zugangsdaten betrifft.",
|
|
121
|
+
app_asks: ({ app }) => `Ich frage, weil ${app} hier jedes Mal fragt.`,
|
|
87
122
|
yes: "ja",
|
|
88
123
|
no: "nein",
|
|
89
124
|
more: ({ count }) => `${count} weitere`,
|
|
@@ -411,3 +446,183 @@ export const capabilitySentenceFields = (input: {
|
|
|
411
446
|
}
|
|
412
447
|
return [...fields.values()];
|
|
413
448
|
};
|
|
449
|
+
|
|
450
|
+
export type CapabilitySlotSentenceContext = { locale: string; timeZone: string };
|
|
451
|
+
export type CapabilitySentencePart =
|
|
452
|
+
| { kind: "text"; text: string }
|
|
453
|
+
| { kind: "chip"; name: string; slot: CapabilitySlot; label: string; full: string };
|
|
454
|
+
export const capabilityPersonText = (person: CapabilityPerson): string =>
|
|
455
|
+
person.name && person.address ? `${person.name} <${person.address}>` : (person.name ?? person.address ?? "");
|
|
456
|
+
|
|
457
|
+
const slotText = (slot: CapabilitySlot, context: CapabilitySlotSentenceContext, mode: "chip" | "plain" | "button" | "full"): string => {
|
|
458
|
+
const t = words.resolve([context.locale]).t;
|
|
459
|
+
switch (slot.kind) {
|
|
460
|
+
case "text":
|
|
461
|
+
return slot.value;
|
|
462
|
+
case "count": {
|
|
463
|
+
const plural = new Intl.PluralRules(context.locale).select(slot.value);
|
|
464
|
+
return `${new Intl.NumberFormat(context.locale).format(slot.value)} ${slot.noun[plural] ?? slot.noun.other}`;
|
|
465
|
+
}
|
|
466
|
+
case "person": {
|
|
467
|
+
const total = slot.total ?? slot.people.length;
|
|
468
|
+
if (mode === "button") return total === 1 ? (slot.people[0]!.name ?? slot.people[0]!.address ?? "") : t.people({ count: total });
|
|
469
|
+
const shown = mode === "full" ? slot.people : slot.people.slice(0, 3);
|
|
470
|
+
const list = shown.map((person) =>
|
|
471
|
+
mode === "chip" ? plain(person.name ?? person.address ?? "", 40, "middle") : capabilityPersonText(person),
|
|
472
|
+
);
|
|
473
|
+
const rest = total - shown.length;
|
|
474
|
+
if (rest > 0) list.push(t.more({ count: rest }));
|
|
475
|
+
return new Intl.ListFormat(context.locale, { type: "conjunction" }).format(list);
|
|
476
|
+
}
|
|
477
|
+
case "object":
|
|
478
|
+
case "place": {
|
|
479
|
+
if (mode === "button") return plain(slot.title, 24, "middle");
|
|
480
|
+
if (mode === "chip" || mode === "full") return slot.title;
|
|
481
|
+
return context.locale.toLowerCase().startsWith("de") ? `„${slot.title}“` : `“${slot.title}”`;
|
|
482
|
+
}
|
|
483
|
+
case "time": {
|
|
484
|
+
const date = (value: string) => {
|
|
485
|
+
const options = { locale: context.locale, timeZone: slot.format === "date" ? "UTC" : normalizeTimeZone(context.timeZone) };
|
|
486
|
+
return slot.format === "date" ? dates.formatDate(`${value}T12:00:00.000Z`, options) : dates.formatDateTime(value, options);
|
|
487
|
+
};
|
|
488
|
+
return slot.end ? `${date(slot.start)} – ${date(slot.end)}` : date(slot.start);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
/** Typed slots are data, never expressions. All formatting is explicit and independent of the process zone. */
|
|
494
|
+
export const capabilitySentenceParts = (
|
|
495
|
+
text: string,
|
|
496
|
+
slots: Readonly<Record<string, CapabilitySlot>>,
|
|
497
|
+
context: CapabilitySlotSentenceContext,
|
|
498
|
+
): CapabilitySentencePart[] => {
|
|
499
|
+
const parts: CapabilitySentencePart[] = [];
|
|
500
|
+
let index = 0;
|
|
501
|
+
for (const match of text.matchAll(/\{([a-z][A-Za-z0-9]{0,31})\}/g)) {
|
|
502
|
+
if (match.index > index) parts.push({ kind: "text", text: text.slice(index, match.index) });
|
|
503
|
+
const name = match[1]!;
|
|
504
|
+
const slot = Object.hasOwn(slots, name) ? slots[name] : undefined;
|
|
505
|
+
if (!slot) throw new Error(`Unknown sentence slot ${name}`);
|
|
506
|
+
const label = slotText(slot, context, "chip");
|
|
507
|
+
const full = slotText(slot, context, "full");
|
|
508
|
+
parts.push(
|
|
509
|
+
slot.kind === "text" || slot.kind === "count"
|
|
510
|
+
? { kind: "text", text: label }
|
|
511
|
+
: { kind: "chip", name, slot, label: slot.kind === "person" ? label : plain(label, 40, "middle"), full },
|
|
512
|
+
);
|
|
513
|
+
index = match.index + match[0].length;
|
|
514
|
+
}
|
|
515
|
+
if (index < text.length) parts.push({ kind: "text", text: text.slice(index) });
|
|
516
|
+
return parts;
|
|
517
|
+
};
|
|
518
|
+
const sentenceText = (
|
|
519
|
+
text: string,
|
|
520
|
+
slots: Readonly<Record<string, CapabilitySlot>>,
|
|
521
|
+
context: CapabilitySlotSentenceContext,
|
|
522
|
+
mode: "plain" | "button",
|
|
523
|
+
max = 400,
|
|
524
|
+
): string => {
|
|
525
|
+
const render = (slotMax: number) =>
|
|
526
|
+
text.replace(/\{([a-z][A-Za-z0-9]{0,31})\}/g, (_, name: string) => {
|
|
527
|
+
const slot = Object.hasOwn(slots, name) ? slots[name] : undefined;
|
|
528
|
+
if (!slot) throw new Error(`Unknown sentence slot ${name}`);
|
|
529
|
+
return plain(slotText(slot, context, mode), slotMax, "middle");
|
|
530
|
+
});
|
|
531
|
+
let result = render(Number.MAX_SAFE_INTEGER);
|
|
532
|
+
if (Array.from(result).length > max) result = render(40);
|
|
533
|
+
return plain(result, max);
|
|
534
|
+
};
|
|
535
|
+
export const capabilitySentenceText = (
|
|
536
|
+
text: string,
|
|
537
|
+
slots: Readonly<Record<string, CapabilitySlot>>,
|
|
538
|
+
context: CapabilitySlotSentenceContext,
|
|
539
|
+
): string => sentenceText(text, slots, context, "plain");
|
|
540
|
+
export type CapabilityReviewSentenceForm =
|
|
541
|
+
| "question"
|
|
542
|
+
| "unknownStill"
|
|
543
|
+
| "running"
|
|
544
|
+
| "done"
|
|
545
|
+
| "rejected"
|
|
546
|
+
| "notRun"
|
|
547
|
+
| "unknown"
|
|
548
|
+
| "short"
|
|
549
|
+
| "confirm"
|
|
550
|
+
| "decline";
|
|
551
|
+
export const capabilityReviewSentence = (
|
|
552
|
+
review: CapabilityActionCallReview,
|
|
553
|
+
form: CapabilityReviewSentenceForm,
|
|
554
|
+
context: { timeZone: string },
|
|
555
|
+
): string => {
|
|
556
|
+
const rendering = { locale: review.locale, timeZone: context.timeZone };
|
|
557
|
+
const t = words.resolve([review.locale]).t;
|
|
558
|
+
const action = () => capabilitySentenceText(review.sentence.action, review.slots, rendering);
|
|
559
|
+
if (form === "question") return plain(t.question({ sentence: action() }), 400);
|
|
560
|
+
if (form === "unknown") return plain(t.unknown({ action: action() }), 400);
|
|
561
|
+
if (form === "unknownStill") return plain(t.unknownStill({ action: action() }), 400);
|
|
562
|
+
if (form === "confirm" || form === "decline")
|
|
563
|
+
return sentenceText(review.sentence[form], review.slots, rendering, "button", Number.MAX_SAFE_INTEGER);
|
|
564
|
+
const template = form === "notRun" ? (review.sentence.notRun ?? review.sentence.rejected) : review.sentence[form];
|
|
565
|
+
if (template) return capabilitySentenceText(template, review.slots, rendering);
|
|
566
|
+
if (form === "short") return action();
|
|
567
|
+
return plain(t.rejected({ action: action() }), 400);
|
|
568
|
+
};
|
|
569
|
+
export const capabilityOutcomeSentence = (
|
|
570
|
+
review: CapabilityActionCallReview,
|
|
571
|
+
outcome: CapabilityActionOutcome,
|
|
572
|
+
receipt: CapabilityActionReceipt | undefined,
|
|
573
|
+
context: { timeZone: string },
|
|
574
|
+
): string =>
|
|
575
|
+
outcome === "done" && receipt
|
|
576
|
+
? capabilitySentenceText(receipt.text, receipt.slots ?? {}, { locale: review.locale, timeZone: context.timeZone })
|
|
577
|
+
: capabilityReviewSentence(review, outcome === "not_run" ? "notRun" : outcome, context);
|
|
578
|
+
export const capabilityAskReasonText = (reason: CapabilityAskReason, context: { locale: string; appName: string }): string => {
|
|
579
|
+
const t = words.resolve([context.locale]).t;
|
|
580
|
+
return reason === "app_asks" ? t.app_asks({ app: context.appName }) : t[reason];
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
/** Splits a Cloud frame such as "Ob {action} passiert ist, weiß ich noch nicht." around the inserted sentence. */
|
|
584
|
+
const framed = (frame: (input: { action: string }) => string, inner: CapabilitySentencePart[]): CapabilitySentencePart[] => {
|
|
585
|
+
const marker = "\u0000";
|
|
586
|
+
const [before = "", after = ""] = frame({ action: marker }).split(marker);
|
|
587
|
+
return [
|
|
588
|
+
...(before ? [{ kind: "text" as const, text: before }] : []),
|
|
589
|
+
...inner,
|
|
590
|
+
...(after ? [{ kind: "text" as const, text: after }] : []),
|
|
591
|
+
];
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* One form of a review as parts for chips: the same words as `capabilityReviewSentence`, with Cloud's frames (the
|
|
596
|
+
* question mark, "not known yet", the default rejection) around the app's sentence. Buttons and `short` stay text.
|
|
597
|
+
*/
|
|
598
|
+
export const capabilityReviewSentenceParts = (
|
|
599
|
+
review: CapabilityActionCallReview,
|
|
600
|
+
form: Exclude<CapabilityReviewSentenceForm, "short" | "confirm" | "decline">,
|
|
601
|
+
context: { timeZone: string },
|
|
602
|
+
): CapabilitySentencePart[] => {
|
|
603
|
+
const rendering = { locale: review.locale, timeZone: context.timeZone };
|
|
604
|
+
const t = words.resolve([review.locale]).t;
|
|
605
|
+
const action = () => capabilitySentenceParts(review.sentence.action, review.slots, rendering);
|
|
606
|
+
if (form === "question") return framed(({ action: sentence }) => t.question({ sentence }), action());
|
|
607
|
+
if (form === "unknown") return framed(t.unknown, action());
|
|
608
|
+
if (form === "unknownStill") return framed(t.unknownStill, action());
|
|
609
|
+
const template = form === "notRun" ? (review.sentence.notRun ?? review.sentence.rejected) : review.sentence[form];
|
|
610
|
+
if (template) return capabilitySentenceParts(template, review.slots, rendering);
|
|
611
|
+
return framed(t.rejected, action());
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
/** The parts of a receipt the app returned, in the review's language. */
|
|
615
|
+
export const capabilityReceiptParts = (
|
|
616
|
+
receipt: CapabilityActionReceipt,
|
|
617
|
+
context: { locale: string; timeZone: string },
|
|
618
|
+
): CapabilitySentencePart[] => capabilitySentenceParts(receipt.text, receipt.slots ?? {}, context);
|
|
619
|
+
|
|
620
|
+
/** Cloud's frame for an outcome it could not see, around any plain sentence. */
|
|
621
|
+
export const capabilityUnknownText = (action: string, locale: string, still = false): string => {
|
|
622
|
+
const t = words.resolve([locale]).t;
|
|
623
|
+
return plain(still ? t.unknownStill({ action }) : t.unknown({ action }), 400);
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
/** Cloud's question frame around a plain sentence, such as Cloud's own sentence for a call without a review. */
|
|
627
|
+
export const capabilityQuestionText = (sentence: string, locale: string): string =>
|
|
628
|
+
plain(words.resolve([locale]).t.question({ sentence }), 400);
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
capabilityResultSchema,
|
|
9
9
|
} from "../contracts/capabilities";
|
|
10
10
|
import type { CompiledCapabilities } from "./capabilities";
|
|
11
|
+
import { capabilityActionReceiptIssues } from "./capability-reviews";
|
|
11
12
|
|
|
12
13
|
const failure = (status: number, code: string, message: string) => Response.json({ code, message }, { status });
|
|
13
14
|
export async function invokeCapabilityStream(params: {
|
|
@@ -26,6 +27,12 @@ export async function invokeCapabilityStream(params: {
|
|
|
26
27
|
const receipt = (value: unknown) => {
|
|
27
28
|
const parsed = capabilityResultSchema(operation.definition.data).parse(value);
|
|
28
29
|
if (parsed.stream) throw new Error("Stream receipts cannot issue another stream");
|
|
30
|
+
// The same receipt rules as an ordinary result, so a stream completion cannot skip them.
|
|
31
|
+
if (parsed.receipt !== undefined) {
|
|
32
|
+
const issues =
|
|
33
|
+
params.kind === "queries" ? ["Queries cannot return a receipt"] : capabilityActionReceiptIssues(parsed.receipt, parsed.links);
|
|
34
|
+
if (issues.length > 0) throw new Error(`Stream result receipt is invalid: ${issues.join("; ")}`);
|
|
35
|
+
}
|
|
29
36
|
return parsed;
|
|
30
37
|
};
|
|
31
38
|
try {
|
|
@@ -2,6 +2,7 @@ import { type AppSkillDefinition, appSkillManifestHash, registerAppSkills, valid
|
|
|
2
2
|
import { startLiveOutbox } from "../events/live";
|
|
3
3
|
import { registerHelp } from "../services/help";
|
|
4
4
|
import { preloadLayoutHelp } from "../ssr/help";
|
|
5
|
+
import { preloadPanelConsent } from "../ssr/panel-consent";
|
|
5
6
|
import { invokeCapabilityStream } from "./capability-streams";
|
|
6
7
|
import { bindProcessApplicationId, clearProcessApplicationId } from "./process-identity";
|
|
7
8
|
/**
|
|
@@ -75,10 +76,11 @@ import { capabilityMessages } from "../shared/capability-messages";
|
|
|
75
76
|
import { normalizeLocale } from "../shared/locale";
|
|
76
77
|
import { escapeHtml } from "../shared/markdown/shared";
|
|
77
78
|
import { type CloudThemePreference, readThemePreferenceFromCookieHeader, themeBootstrapScript } from "../shared/theme";
|
|
79
|
+
import { appAccentStyle } from "../ssr/app-appearance";
|
|
78
80
|
import { appFaviconHref } from "./app-favicon";
|
|
79
81
|
import { compileAppPresentation } from "./app-presentation";
|
|
80
82
|
import { readBoundedJson } from "./bounded-json";
|
|
81
|
-
import { appRuntimeMetadata } from "./build-metadata";
|
|
83
|
+
import { appRuntimeMetadata, buildMetadata } from "./build-metadata";
|
|
82
84
|
import { compileCapabilities, invokeCompiledCapability, reviewCompiledCapability, serializeCapabilityProviderResult } from "./capabilities";
|
|
83
85
|
import { cliPluginRoutePrefixes, createCliPluginRoutes, validateAppCliModules } from "./cli-plugins";
|
|
84
86
|
import { FONT_PRELOAD_LINKS } from "./font-preloads";
|
|
@@ -90,14 +92,14 @@ import { getProcessSync, startProcessSync } from "./process-sync";
|
|
|
90
92
|
import { APP_READINESS_PATH, appReadinessResponse } from "./readiness";
|
|
91
93
|
import { appRegistry, type CapabilityRegistryRecord, capabilityRegistry } from "./registry";
|
|
92
94
|
import { canonicalRoute, isOwnAppPath } from "./registry-validation";
|
|
93
|
-
import { ensureRuntimeWatcher, getCurrentRuntime, stopRuntimeWatcher } from "./runtime-watcher";
|
|
94
|
-
import { servePublicAsset } from "./static-assets";
|
|
95
|
+
import { ensureRuntimeWatcher, getCurrentRuntime, registeredCoreRelease, stopRuntimeWatcher } from "./runtime-watcher";
|
|
96
|
+
import { publicFileVersion, servePublicAsset, sharedStylesheetVersion, versionedPublicHref } from "./static-assets";
|
|
95
97
|
import { createStatusPreservingSsrHandler } from "./status-preserving-ssr";
|
|
96
98
|
import { WEB_VITALS_ASSET_HREF } from "./web-vitals-asset";
|
|
97
99
|
import { compileWidgetDeclarations } from "./widget-declarations";
|
|
98
100
|
|
|
99
|
-
/**
|
|
100
|
-
const
|
|
101
|
+
/** Only development links use this start stamp to pick up rebuilds. */
|
|
102
|
+
const developmentStamp = Date.now();
|
|
101
103
|
|
|
102
104
|
export type PageOptions = {
|
|
103
105
|
title?: string;
|
|
@@ -110,7 +112,7 @@ export type PageOptions = {
|
|
|
110
112
|
pwa?: { name: string };
|
|
111
113
|
};
|
|
112
114
|
|
|
113
|
-
type ResolvedPageOptions = PageOptions & { performanceRoute?: string; themePreference?: CloudThemePreference };
|
|
115
|
+
type ResolvedPageOptions = PageOptions & { performanceRoute?: string; themePreference?: CloudThemePreference; appAccent?: string };
|
|
114
116
|
|
|
115
117
|
// ── Public types ────────────────────────────────────────────────────────────
|
|
116
118
|
|
|
@@ -377,13 +379,20 @@ const validateAppNavBadge = (opts: Pick<AppOptions, "id" | "routes" | "nav">): v
|
|
|
377
379
|
}
|
|
378
380
|
};
|
|
379
381
|
|
|
380
|
-
const validateAppPanel = (opts: Pick<AppOptions, "id" | "routes" | "panel">): AppMeta["panel"] => {
|
|
382
|
+
const validateAppPanel = (opts: Pick<AppOptions, "id" | "routes" | "basePath" | "panel">): AppMeta["panel"] => {
|
|
381
383
|
const panel = opts.panel;
|
|
382
384
|
if (panel === undefined) return undefined;
|
|
383
385
|
if (!panel || typeof panel !== "object" || Array.isArray(panel)) throw new Error(`App "${opts.id}" declares panel; it must be an object`);
|
|
384
386
|
if (typeof panel.href !== "string" || !isOwnAppPath(panel.href, opts.routes)) {
|
|
385
387
|
throw new Error(`App "${opts.id}" declares panel.href "${panel.href}"; it must be a same-origin path below one of its routes`);
|
|
386
388
|
}
|
|
389
|
+
const pageBase = `${opts.basePath?.replace(/\/$/, "") ?? ""}/help`;
|
|
390
|
+
const path = new URL(panel.href, "https://cloud.invalid").pathname;
|
|
391
|
+
if (path === pageBase || path.startsWith(`${pageBase}/`)) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`App "${opts.id}" declares panel.href "${panel.href}"; it must not be at or below its Help path "${pageBase}", which the framework uses for Help`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
387
396
|
for (const field of ["label", "icon"] as const) {
|
|
388
397
|
if (typeof panel[field] !== "string" || !panel[field].trim())
|
|
389
398
|
throw new Error(`App "${opts.id}" declares panel.${field}; it must be a non-empty string`);
|
|
@@ -434,7 +443,18 @@ export const defineApp = <
|
|
|
434
443
|
// Every island instance already mounts inside its own error boundary; this
|
|
435
444
|
// only gives the fallback Cloud's look and the reader's language.
|
|
436
445
|
errorFallback: fileURLToPath(new URL("../ssr/IslandError.tsx", import.meta.url)),
|
|
437
|
-
template: ({ body, scripts, title, description, theme, themePreference, lang, performanceRoute, pwa }) => {
|
|
446
|
+
template: ({ body, scripts, title, description, theme, themePreference, lang, performanceRoute, pwa, appAccent }) => {
|
|
447
|
+
// A URL changes only when what it names changes: the app's own files by their content, Core's shared sheets by
|
|
448
|
+
// the release of the Core build in the registry, so every app links them under one URL per Core build.
|
|
449
|
+
const sharedVersion = isDevelopment ? String(developmentStamp) : sharedStylesheetVersion(registeredCoreRelease());
|
|
450
|
+
const appStylesheet = `/public/${opts.id}/app.css`;
|
|
451
|
+
const appVersion = isDevelopment ? String(developmentStamp) : publicFileVersion(appStylesheet);
|
|
452
|
+
const faviconVersion = isDevelopment
|
|
453
|
+
? developmentStamp
|
|
454
|
+
: opts.id === "core"
|
|
455
|
+
? undefined
|
|
456
|
+
: publicFileVersion(`/public/${opts.id}/favicon.svg`);
|
|
457
|
+
const accentStyle = appAccentStyle(appAccent);
|
|
438
458
|
const fixed = theme !== undefined;
|
|
439
459
|
const resolved = theme ?? (themePreference === "light" || themePreference === "dark" ? themePreference : undefined);
|
|
440
460
|
// App pages paint the canvas before any stylesheet and give it to the
|
|
@@ -478,17 +498,17 @@ export const defineApp = <
|
|
|
478
498
|
<title>${escapeHtml(title ?? "Cloud")}</title>
|
|
479
499
|
<meta name="description" content="${escapeHtml(description ?? "Cloud workspace")}">
|
|
480
500
|
${head.app}
|
|
481
|
-
<link rel="icon" href="${appFaviconHref(opts.id,
|
|
501
|
+
<link rel="icon" href="${appFaviconHref(opts.id, faviconVersion)}">
|
|
482
502
|
<style data-cloud-css-layers>@layer properties, theme, base, components, utilities;</style>
|
|
483
503
|
<link rel="preload" href="/public/tabler-icons.woff2" as="font" type="font/woff2" crossorigin>
|
|
484
504
|
${FONT_PRELOAD_LINKS}
|
|
485
|
-
<link rel="stylesheet" href="/public/fonts.css
|
|
486
|
-
<link rel="stylesheet" href="/public/tabler-icons.css
|
|
487
|
-
<link rel="stylesheet" href="
|
|
488
|
-
<link rel="stylesheet" href="/public/global.css
|
|
505
|
+
<link rel="stylesheet" href="${versionedPublicHref("/public/fonts.css", sharedVersion)}">
|
|
506
|
+
<link rel="stylesheet" href="${versionedPublicHref("/public/tabler-icons.css", sharedVersion)}">
|
|
507
|
+
<link rel="stylesheet" href="${versionedPublicHref(appStylesheet, appVersion)}">
|
|
508
|
+
<link rel="stylesheet" href="${versionedPublicHref("/public/global.css", sharedVersion)}">${head.styles}
|
|
489
509
|
<script>${themeBootstrapScript}</script>
|
|
490
510
|
</head>
|
|
491
|
-
<body class="k2b-ui" data-k2b-app-workspace-controller="global">
|
|
511
|
+
<body class="k2b-ui${accentStyle ? " app-accent-scope" : ""}"${accentStyle ? ` style="${escapeHtml(accentStyle)}"` : ""} data-k2b-app-workspace-controller="global">
|
|
492
512
|
${body}
|
|
493
513
|
</body>
|
|
494
514
|
${scripts}
|
|
@@ -520,6 +540,7 @@ export const defineApp = <
|
|
|
520
540
|
await Promise.all([
|
|
521
541
|
preloadLayoutAnnouncements(c),
|
|
522
542
|
preloadLayoutHelp(c),
|
|
543
|
+
user ? preloadPanelConsent(c) : undefined,
|
|
523
544
|
user ? readRailSnapshot(user).then((snapshot) => c.set("railPreferences", snapshot)) : undefined,
|
|
524
545
|
]);
|
|
525
546
|
}),
|
|
@@ -741,7 +762,7 @@ export const defineApp = <
|
|
|
741
762
|
.get(APP_READINESS_PATH, () => appReadinessResponse(meta.id))
|
|
742
763
|
.route(ssrMountPath, routes(config))
|
|
743
764
|
.all(`${ssrMountPath}/*`, (c) => c.notFound())
|
|
744
|
-
.all("/public/*", servePublicAsset(isDevelopment))
|
|
765
|
+
.all("/public/*", servePublicAsset(isDevelopment, sharedStylesheetVersion(buildMetadata.release)))
|
|
745
766
|
.use(
|
|
746
767
|
"/_internal/sync/*",
|
|
747
768
|
requireInvocation((c) => ({
|
|
@@ -3,7 +3,8 @@ import plexPreset from "@k2b/ui/fonts/plex.css" with { type: "text" };
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Preload tags for the IBM Plex Sans faces that every page sets in its first
|
|
6
|
-
* frame: Latin text at 400, 500 (
|
|
6
|
+
* frame: Latin text at 400, 500 (list titles, medium text) and 600 (headings,
|
|
7
|
+
* buttons, labels).
|
|
7
8
|
*
|
|
8
9
|
* The preset declares its faces with `font-display: swap`. Without a preload
|
|
9
10
|
* the browser requests a face only when layout needs it, paints the fallback
|
|
@@ -19,6 +19,7 @@ import { buildRuntimeFromRegistry } from "./runtime-context";
|
|
|
19
19
|
const log = logger("runtime-watcher");
|
|
20
20
|
|
|
21
21
|
let current: CloudRuntime | undefined;
|
|
22
|
+
let coreRelease: string | undefined;
|
|
22
23
|
let initPromise: Promise<void> | undefined;
|
|
23
24
|
let watcherTask: Promise<void> | undefined;
|
|
24
25
|
let abort: AbortController | undefined;
|
|
@@ -27,8 +28,12 @@ const refresh = async () => {
|
|
|
27
28
|
const apps = await listApps();
|
|
28
29
|
const capabilities = await listCapabilities(apps);
|
|
29
30
|
current = buildRuntimeFromRegistry(apps, capabilities);
|
|
31
|
+
coreRelease = apps.find((app) => app.id === "core")?.runtime?.release;
|
|
30
32
|
};
|
|
31
33
|
|
|
34
|
+
/** Core's release from the live registry, absent before initialization and after shutdown. */
|
|
35
|
+
export const registeredCoreRelease = (): string | undefined => coreRelease;
|
|
36
|
+
|
|
32
37
|
export const ensureRuntimeWatcher = (): Promise<void> => {
|
|
33
38
|
// Concurrent callers (start() + first request) wait on the same init —
|
|
34
39
|
// returning before `current` is populated would race getCurrentRuntime().
|
|
@@ -68,6 +73,7 @@ export const stopRuntimeWatcher = async (): Promise<void> => {
|
|
|
68
73
|
watcherTask = undefined;
|
|
69
74
|
initPromise = undefined;
|
|
70
75
|
current = undefined;
|
|
76
|
+
coreRelease = undefined;
|
|
71
77
|
};
|
|
72
78
|
|
|
73
79
|
export const getCurrentRuntime = (): CloudRuntime => {
|
|
@@ -1,7 +1,43 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
1
3
|
import { resolve, sep } from "node:path";
|
|
2
4
|
import type { Context } from "hono";
|
|
3
5
|
|
|
4
6
|
const publicRoot = resolve(process.cwd(), "public");
|
|
7
|
+
const versions = new Map<string, { size: number; mtimeMs: number; version: string }>();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Version of Core's shared stylesheets: the release, which names one build. `local` and `development` are the
|
|
11
|
+
* defaults of unnamed builds and name many, so those sheets get no version and are revalidated instead.
|
|
12
|
+
*/
|
|
13
|
+
export function sharedStylesheetVersion(release: string | undefined): string | undefined {
|
|
14
|
+
return !release || release === "local" || release === "development" ? undefined : release;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Content version of a file below `public/`: hashed on first use and re-hashed only when the file's size or
|
|
19
|
+
* modification time changes; a missing or unreadable file has no version.
|
|
20
|
+
*/
|
|
21
|
+
export function publicFileVersion(pathname: string): string | undefined {
|
|
22
|
+
const path = resolvePublicAsset(pathname);
|
|
23
|
+
if (!path) return undefined;
|
|
24
|
+
try {
|
|
25
|
+
const stat = statSync(path, { throwIfNoEntry: false });
|
|
26
|
+
if (!stat?.isFile()) return undefined;
|
|
27
|
+
const cached = versions.get(path);
|
|
28
|
+
if (cached && cached.size === stat.size && cached.mtimeMs === stat.mtimeMs) return cached.version;
|
|
29
|
+
const version = createHash("sha256").update(readFileSync(path)).digest("hex").slice(0, 16);
|
|
30
|
+
versions.set(path, { size: stat.size, mtimeMs: stat.mtimeMs, version });
|
|
31
|
+
return version;
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Append a URL-encoded version only when the asset has one. */
|
|
38
|
+
export function versionedPublicHref(pathname: string, version: string | undefined): string {
|
|
39
|
+
return version === undefined ? pathname : `${pathname}?v=${encodeURIComponent(version)}`;
|
|
40
|
+
}
|
|
5
41
|
|
|
6
42
|
function decodePathname(pathname: string): string | null {
|
|
7
43
|
try {
|
|
@@ -48,14 +84,15 @@ async function encodedFile(path: string, encoding: "br" | "gzip"): Promise<Bun.B
|
|
|
48
84
|
return (await file.exists()) ? file : null;
|
|
49
85
|
}
|
|
50
86
|
|
|
51
|
-
export function servePublicAsset(isDevelopment: boolean) {
|
|
87
|
+
export function servePublicAsset(isDevelopment: boolean, sharedVersion?: string) {
|
|
52
88
|
return async (c: Context) => {
|
|
53
89
|
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
54
90
|
c.header("Allow", "GET, HEAD");
|
|
55
91
|
return c.text("Method Not Allowed", 405);
|
|
56
92
|
}
|
|
57
93
|
|
|
58
|
-
const
|
|
94
|
+
const url = new URL(c.req.url);
|
|
95
|
+
const path = resolvePublicAsset(url.pathname);
|
|
59
96
|
if (!path) return c.notFound();
|
|
60
97
|
|
|
61
98
|
const sourceFile = Bun.file(path);
|
|
@@ -84,8 +121,20 @@ export function servePublicAsset(isDevelopment: boolean) {
|
|
|
84
121
|
// Development rebuilds files in place under the same address, so the browser asks again on every use; an
|
|
85
122
|
// unchanged file then costs one round trip instead of its whole size on every page.
|
|
86
123
|
const etag = `"${selectedEncoding ?? "identity"}-${selected.size.toString(36)}-${selected.lastModified.toString(36)}"`;
|
|
124
|
+
// The page head links these files with the version this process names for them. Any other version may name
|
|
125
|
+
// other content (a rolling deploy, an app started before a Core redeploy), so it is revalidated, never kept.
|
|
126
|
+
let cacheControl = isDevelopment ? "no-cache" : "public, max-age=31536000, immutable";
|
|
127
|
+
if (!isDevelopment) {
|
|
128
|
+
const pathname = decodePathname(url.pathname)!;
|
|
129
|
+
const shared = /^\/public\/(fonts|tabler-icons|global)\.css$/.test(pathname);
|
|
130
|
+
const own = /^\/public\/[^/]+\/(app\.css|favicon\.svg)$/.test(pathname);
|
|
131
|
+
if (shared || own) {
|
|
132
|
+
const expected = shared ? sharedVersion : publicFileVersion(url.pathname);
|
|
133
|
+
if (expected === undefined || url.searchParams.get("v") !== expected) cacheControl = "no-cache";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
87
136
|
const headers = new Headers({
|
|
88
|
-
"Cache-Control":
|
|
137
|
+
"Cache-Control": cacheControl,
|
|
89
138
|
ETag: etag,
|
|
90
139
|
Vary: "Accept-Encoding",
|
|
91
140
|
});
|
|
@@ -2,6 +2,7 @@ import type { HtmlFn, RenderFn } from "@k2b/ssr";
|
|
|
2
2
|
import { createSSRHandler } from "@k2b/ssr/hono";
|
|
3
3
|
import type { Context, Env, MiddlewareHandler, TypedResponse } from "hono";
|
|
4
4
|
import type { StatusCode } from "hono/utils/http-status";
|
|
5
|
+
import type { PanelConsentSnapshot } from "../contracts/panel-context";
|
|
5
6
|
import type { RailSnapshot } from "../contracts/rail-preferences";
|
|
6
7
|
import type { SettingsFlat, Unflatten } from "../contracts/settings-types";
|
|
7
8
|
import type { User } from "../contracts/shared";
|
|
@@ -13,6 +14,7 @@ type PageEnv<T extends object> = {
|
|
|
13
14
|
page: Partial<T>;
|
|
14
15
|
user?: User;
|
|
15
16
|
railPreferences?: RailSnapshot;
|
|
17
|
+
panelConsent?: PanelConsentSnapshot;
|
|
16
18
|
settings?: Unflatten<SettingsFlat<typeof CORE_SETTINGS>>;
|
|
17
19
|
};
|
|
18
20
|
};
|
|
@@ -425,7 +425,7 @@ function AccessEntryRow(props: {
|
|
|
425
425
|
<Show
|
|
426
426
|
when={props.entry.principal.type === "user"}
|
|
427
427
|
fallback={
|
|
428
|
-
<div class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-
|
|
428
|
+
<div class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-secondary dark:bg-zinc-700">
|
|
429
429
|
<i class={`ti ${getPrincipalIcon(props.entry, t())} text-sm`} />
|
|
430
430
|
</div>
|
|
431
431
|
}
|
|
@@ -509,7 +509,7 @@ function AccessEntryRow(props: {
|
|
|
509
509
|
aria-label={t().remove({ name: displayName() })}
|
|
510
510
|
// A disabled button takes no focus and touch has no hover, so the reason also reaches assistive technology.
|
|
511
511
|
aria-description={props.lastManager ? removeLabel() : undefined}
|
|
512
|
-
class="focus-ui flex h-7 w-7 items-center justify-center rounded text-
|
|
512
|
+
class="focus-ui flex h-7 w-7 items-center justify-center rounded text-dimmed transition-colors focus:opacity-100 enabled:hover:bg-red-500/[0.08] enabled:hover:text-red-600 dark:enabled:hover:text-red-400"
|
|
513
513
|
>
|
|
514
514
|
<i class="ti ti-x text-sm" />
|
|
515
515
|
</IconButton>
|
|
@@ -350,7 +350,7 @@ const ResultRow = (props: {
|
|
|
350
350
|
<Show
|
|
351
351
|
when={props.avatar}
|
|
352
352
|
fallback={
|
|
353
|
-
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-xs text-
|
|
353
|
+
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-xs text-secondary dark:bg-zinc-700">
|
|
354
354
|
<i class={`ti ${props.icon} text-sm`} />
|
|
355
355
|
</div>
|
|
356
356
|
}
|