@iblai/iblai-js 2.27.1 → 2.28.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.
@@ -1815,7 +1815,11 @@ var advancedCustomDomain$3 = {
1815
1815
  noProjects: "You have not deployed any apps yet. Deploy one first, then come back to give it a domain.",
1816
1816
  customDomainLabel: "Domain",
1817
1817
  domainHelpPlatform: "Without the scheme or a leading “www.” — for example, app.example.com.",
1818
- domainHelpApp: "Leave this empty and a subdomain will be assigned to the app for you, with no DNS records to set.",
1818
+ domainHelpApp: "A domain you own, without the scheme or a leading “www.” — for example, app.example.com. Leave it empty to use the subdomain above.",
1819
+ subdomainLabel: "Subdomain",
1820
+ subdomainHelp: "Lowercase letters, digits and hyphens, like a username. The app is served at <subdomain>.<the platform’s shared domain>, with no DNS records to set.",
1821
+ oneOfSubdomainOrDomain: "Fill in either the subdomain or a domain you own, not both.",
1822
+ subdomainInvalid: "A subdomain is 1–63 lowercase letters, digits and hyphens, and cannot start or end with a hyphen.",
1819
1823
  cancel: "Cancel",
1820
1824
  save: "Save",
1821
1825
  expandAriaLabel: "Expand domains",
@@ -1837,6 +1841,9 @@ var advancedCustomDomain$3 = {
1837
1841
  copyNameHostAriaLabel: "Copy name/host: {domain}",
1838
1842
  copyValueAriaLabel: "Copy value/target",
1839
1843
  noRecordsYet: "No records yet. Check the domain again to fetch them.",
1844
+ sharedDomainLine: "Runs on our shared domain — nothing to set up.",
1845
+ openSite: "Open {domain}",
1846
+ servesApp: "Serves {app}",
1840
1847
  confirmRemoveTitle: "Remove this domain?",
1841
1848
  confirmDeleteBody: "{domain} will stop signing users in to this organization.",
1842
1849
  confirmDetachBody: "{domain} will stop serving the app, here and at the hosting provider.",
@@ -9360,7 +9367,11 @@ var advancedCustomDomain$2 = {
9360
9367
  noProjects: "Vous n’avez encore déployé aucune application. Déployez-en une, puis revenez lui donner un domaine.",
9361
9368
  customDomainLabel: "Domaine",
9362
9369
  domainHelpPlatform: "Sans le schéma ni un « www. » en tête — par exemple, app.example.com.",
9363
- domainHelpApp: "Laissez ce champ vide et un sous-domaine sera attribué à l’application, sans aucun enregistrement DNS à créer.",
9370
+ domainHelpApp: "Un domaine qui vous appartient, sans le schéma ni un « www. » en tête — par exemple, app.example.com. Laissez ce champ vide pour utiliser le sous-domaine ci-dessus.",
9371
+ subdomainLabel: "Sous-domaine",
9372
+ subdomainHelp: "Lettres minuscules, chiffres et tirets, comme un nom d’utilisateur. L’application est servie à <sous-domaine>.<le domaine partagé de la plateforme>, sans aucun enregistrement DNS à créer.",
9373
+ oneOfSubdomainOrDomain: "Renseignez soit le sous-domaine, soit un domaine qui vous appartient, pas les deux.",
9374
+ subdomainInvalid: "Un sous-domaine compte de 1 à 63 lettres minuscules, chiffres et tirets, et ne peut ni commencer ni finir par un tiret.",
9364
9375
  cancel: "Annuler",
9365
9376
  save: "Enregistrer",
9366
9377
  expandAriaLabel: "Déplier les domaines",
@@ -9382,6 +9393,9 @@ var advancedCustomDomain$2 = {
9382
9393
  copyNameHostAriaLabel: "Copier le nom/hôte : {domain}",
9383
9394
  copyValueAriaLabel: "Copier la valeur/cible",
9384
9395
  noRecordsYet: "Aucun enregistrement pour l’instant. Vérifiez le domaine à nouveau pour les obtenir.",
9396
+ sharedDomainLine: "Fonctionne sur notre domaine partagé — rien à configurer.",
9397
+ openSite: "Ouvrir {domain}",
9398
+ servesApp: "Dessert {app}",
9385
9399
  confirmRemoveTitle: "Supprimer ce domaine ?",
9386
9400
  confirmDeleteBody: "{domain} ne permettra plus de se connecter à cette organisation.",
9387
9401
  confirmDetachBody: "{domain} cessera de servir l’application, ici comme chez l’hébergeur.",
@@ -16905,7 +16919,11 @@ var advancedCustomDomain$1 = {
16905
16919
  noProjects: "Todavía no has desplegado ninguna aplicación. Despliega una primero y vuelve para darle un dominio.",
16906
16920
  customDomainLabel: "Dominio",
16907
16921
  domainHelpPlatform: "Sin el esquema ni un “www.” inicial — por ejemplo, app.example.com.",
16908
- domainHelpApp: "Déjalo vacío y se asignará un subdominio a la aplicación, sin registros DNS que configurar.",
16922
+ domainHelpApp: "Un dominio tuyo, sin el esquema ni un “www.” inicial — por ejemplo, app.example.com. Déjalo vacío para usar el subdominio de arriba.",
16923
+ subdomainLabel: "Subdominio",
16924
+ subdomainHelp: "Letras minúsculas, dígitos y guiones, como un nombre de usuario. La aplicación se sirve en <subdominio>.<el dominio compartido de la plataforma>, sin registros DNS que configurar.",
16925
+ oneOfSubdomainOrDomain: "Rellena el subdominio o un dominio tuyo, no ambos.",
16926
+ subdomainInvalid: "Un subdominio tiene entre 1 y 63 letras minúsculas, dígitos y guiones, y no puede empezar ni terminar con un guion.",
16909
16927
  cancel: "Cancelar",
16910
16928
  save: "Guardar",
16911
16929
  expandAriaLabel: "Expandir dominios",
@@ -16927,6 +16945,9 @@ var advancedCustomDomain$1 = {
16927
16945
  copyNameHostAriaLabel: "Copiar nombre/host: {domain}",
16928
16946
  copyValueAriaLabel: "Copiar valor/destino",
16929
16947
  noRecordsYet: "Aún no hay registros. Comprueba el dominio de nuevo para obtenerlos.",
16948
+ sharedDomainLine: "Funciona en nuestro dominio compartido — no hay nada que configurar.",
16949
+ openSite: "Abrir {domain}",
16950
+ servesApp: "Sirve {app}",
16930
16951
  confirmRemoveTitle: "¿Eliminar este dominio?",
16931
16952
  confirmDeleteBody: "{domain} dejará de permitir el inicio de sesión en esta organización.",
16932
16953
  confirmDetachBody: "{domain} dejará de servir la aplicación, aquí y en el proveedor de alojamiento.",
@@ -24450,7 +24471,11 @@ var advancedCustomDomain = {
24450
24471
  noProjects: "你还没有部署任何应用。请先部署一个,然后回来为它设置域名。",
24451
24472
  customDomainLabel: "域名",
24452
24473
  domainHelpPlatform: "不要带协议前缀或开头的“www.”,例如 app.example.com。",
24453
- domainHelpApp: "留空即可自动为该应用分配一个子域名,无需设置任何 DNS 记录。",
24474
+ domainHelpApp: "你拥有的域名,不要带协议前缀或开头的“www.”,例如 app.example.com。留空则使用上方的子域名。",
24475
+ subdomainLabel: "子域名",
24476
+ subdomainHelp: "小写字母、数字和连字符,像用户名一样。应用将通过 <子域名>.<平台的共享域名> 提供服务,无需设置任何 DNS 记录。",
24477
+ oneOfSubdomainOrDomain: "请填写子域名或你拥有的域名,二者只填其一。",
24478
+ subdomainInvalid: "子域名为 1 到 63 个小写字母、数字和连字符,且不能以连字符开头或结尾。",
24454
24479
  cancel: "取消",
24455
24480
  save: "保存",
24456
24481
  expandAriaLabel: "展开域名",
@@ -24472,6 +24497,9 @@ var advancedCustomDomain = {
24472
24497
  copyNameHostAriaLabel: "复制名称/主机:{domain}",
24473
24498
  copyValueAriaLabel: "复制值/目标",
24474
24499
  noRecordsYet: "暂无记录。请重新检查该域名以获取记录。",
24500
+ sharedDomainLine: "运行在我们的共享域名上——无需任何设置。",
24501
+ openSite: "打开 {domain}",
24502
+ servesApp: "服务于 {app}",
24475
24503
  confirmRemoveTitle: "要移除这个域名吗?",
24476
24504
  confirmDeleteBody: "{domain} 将不再用于登录该组织。",
24477
24505
  confirmDetachBody: "{domain} 将不再为该应用提供服务,本处和托管服务商处都会解除。",
@@ -198286,15 +198314,23 @@ const sanitizeDomain = (domain) => domain
198286
198314
  .replace(/^https?:\/\//, '')
198287
198315
  .replace(/\/.*$/, '')
198288
198316
  .replace(/^www\./, '');
198289
- const AddDomainDialog = ({ open, onOpenChange, canAddPlatformDomain, projects, isLoadingProjects, isSubmitting, onSubmitPlatformDomain, onSubmitAppDomain, }) => {
198317
+ /** One label under the platform's shared domain — the backend's own rule. */
198318
+ const SUBDOMAIN_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
198319
+ /** Typed like a username: only case and surrounding whitespace are forgiven. */
198320
+ const sanitizeSubdomain = (subdomain) => subdomain.trim().toLowerCase();
198321
+ const AddDomainDialog = ({ open, onOpenChange, canAddPlatformDomain, defaultMode = 'platform', projects, isLoadingProjects, isSubmitting, onSubmitPlatformDomain, onSubmitAppDomain, }) => {
198322
+ var _a;
198290
198323
  const t = useT();
198291
- const [mode, setMode] = useState(canAddPlatformDomain ? 'platform' : 'app');
198324
+ const initialMode = canAddPlatformDomain ? defaultMode : 'app';
198325
+ const [mode, setMode] = useState(initialMode);
198292
198326
  const [domainValue, setDomainValue] = useState('');
198327
+ const [subdomainValue, setSubdomainValue] = useState('');
198293
198328
  const [projectId, setProjectId] = useState('');
198294
198329
  const reset = () => {
198295
198330
  setDomainValue('');
198331
+ setSubdomainValue('');
198296
198332
  setProjectId('');
198297
- setMode(canAddPlatformDomain ? 'platform' : 'app');
198333
+ setMode(initialMode);
198298
198334
  };
198299
198335
  const close = () => {
198300
198336
  reset();
@@ -198306,10 +198342,27 @@ const AddDomainDialog = ({ open, onOpenChange, canAddPlatformDomain, projects, i
198306
198342
  await onSubmitPlatformDomain(sanitizeDomain(domainValue));
198307
198343
  }
198308
198344
  else {
198309
- await onSubmitAppDomain(Number(projectId), sanitizeDomain(domainValue));
198345
+ await onSubmitAppDomain(Number(projectId), {
198346
+ domain: sanitizeDomain(domainValue),
198347
+ subdomain: sanitizeSubdomain(subdomainValue),
198348
+ });
198310
198349
  }
198311
198350
  };
198312
- const canSubmit = mode === 'platform' ? domainValue.trim().length > 0 : projectId.trim().length > 0;
198351
+ const hasDomain = domainValue.trim().length > 0;
198352
+ const subdomain = sanitizeSubdomain(subdomainValue);
198353
+ const hasSubdomain = subdomain.length > 0;
198354
+ // An app is served at exactly one address, and a subdomain has to be one the
198355
+ // backend will take — say which rule is unmet, and only once something was
198356
+ // typed, so an untouched form stays quiet.
198357
+ const appAddressProblem = mode === 'app' && hasDomain && hasSubdomain
198358
+ ? 'both'
198359
+ : mode === 'app' && hasSubdomain && !SUBDOMAIN_RE.test(subdomain)
198360
+ ? 'invalid'
198361
+ : null;
198362
+ const canSubmit = mode === 'platform'
198363
+ ? hasDomain
198364
+ : projectId.trim().length > 0 && (hasDomain || hasSubdomain) && appAddressProblem === null;
198365
+ const selectedProject = projects.find((project) => String(project.id) === projectId);
198313
198366
  return (jsx(Dialog, { open: open, onOpenChange: (next) => {
198314
198367
  if (!next) {
198315
198368
  reset();
@@ -198318,9 +198371,11 @@ const AddDomainDialog = ({ open, onOpenChange, canAddPlatformDomain, projects, i
198318
198371
  }, children: jsxs(DialogContent, { className: "sm:max-w-[480px]", children: [jsxs(DialogHeader, { children: [jsx(DialogTitle, { children: t('advancedCustomDomain.dialogTitle') }), jsx(DialogDescription, { children: t('advancedCustomDomain.dialogDescription') })] }), jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [jsxs("div", { className: "space-y-2", children: [jsx("label", { htmlFor: "add-domain-kind", className: "text-sm", children: t('advancedCustomDomain.kindLabel') }), jsxs(Select$1, { value: mode, onValueChange: (next) => setMode(next), children: [jsx(SelectTrigger, { id: "add-domain-kind", children: jsx(SelectValue, {}) }), jsxs(SelectContent, { children: [canAddPlatformDomain && (jsx(SelectItem, { value: "platform", children: t('advancedCustomDomain.kindPlatform') })), jsx(SelectItem, { value: "app", children: t('advancedCustomDomain.kindApp') })] })] })] }), mode === 'app' && (jsxs("div", { className: "space-y-2", children: [jsx("label", { htmlFor: "add-domain-project", className: "text-sm", children: t('advancedCustomDomain.projectLabel') }), isLoadingProjects ? (jsx("p", { className: "text-sm text-[#646464]", children: t('advancedCustomDomain.loading') })) : projects.length === 0 ? (
198319
198372
  // The backend lists only the caller's own projects, so this is
198320
198373
  // "you have not deployed anything", not "the platform has not".
198321
- jsx("p", { className: "text-sm text-[#646464]", children: t('advancedCustomDomain.noProjects') })) : (jsxs(Select$1, { value: projectId, onValueChange: setProjectId, children: [jsx(SelectTrigger, { id: "add-domain-project", children: jsx(SelectValue, { placeholder: t('advancedCustomDomain.projectPlaceholder') }) }), jsx(SelectContent, { children: projects.map((project) => (jsx(SelectItem, { value: String(project.id), children: project.name }, project.id))) })] }))] })), jsxs("div", { className: "space-y-2", children: [jsx("label", { htmlFor: "add-domain-name", className: "text-sm", children: t('advancedCustomDomain.customDomainLabel') }), jsx(Input, { id: "add-domain-name", value: domainValue, onChange: (event) => setDomainValue(event.target.value), placeholder: "example.com", className: "w-full", required: mode === 'platform' }), jsx("p", { className: "text-xs text-[#646464]", children: mode === 'app'
198374
+ jsx("p", { className: "text-sm text-[#646464]", children: t('advancedCustomDomain.noProjects') })) : (jsxs(Select$1, { value: projectId, onValueChange: setProjectId, children: [jsx(SelectTrigger, { id: "add-domain-project", children: jsx(SelectValue, { placeholder: t('advancedCustomDomain.projectPlaceholder') }) }), jsx(SelectContent, { children: projects.map((project) => (jsx(SelectItem, { value: String(project.id), children: project.name }, project.id))) })] }))] })), mode === 'app' && (jsxs("div", { className: "space-y-2", children: [jsx("label", { htmlFor: "add-domain-subdomain", className: "text-sm", children: t('advancedCustomDomain.subdomainLabel') }), jsx(Input, { id: "add-domain-subdomain", value: subdomainValue, onChange: (event) => setSubdomainValue(event.target.value),
198375
+ // The app's own name is the suggestion the deploy skill makes.
198376
+ placeholder: (_a = selectedProject === null || selectedProject === void 0 ? void 0 : selectedProject.name) !== null && _a !== void 0 ? _a : '', className: "w-full" }), jsx("p", { className: "text-xs text-[#646464]", children: t('advancedCustomDomain.subdomainHelp') })] })), jsxs("div", { className: "space-y-2", children: [jsx("label", { htmlFor: "add-domain-name", className: "text-sm", children: t('advancedCustomDomain.customDomainLabel') }), jsx(Input, { id: "add-domain-name", value: domainValue, onChange: (event) => setDomainValue(event.target.value), placeholder: "example.com", className: "w-full", required: mode === 'platform' }), jsx("p", { className: "text-xs text-[#646464]", children: mode === 'app'
198322
198377
  ? t('advancedCustomDomain.domainHelpApp')
198323
- : t('advancedCustomDomain.domainHelpPlatform') })] }), jsxs("div", { className: "flex justify-end gap-2", children: [jsx(Button$1, { type: "button", variant: "outline", onClick: close, children: t('advancedCustomDomain.cancel') }), jsxs(Button$1, { type: "submit", disabled: isSubmitting || !canSubmit, children: [isSubmitting && jsx(LoaderCircle, { className: "mr-2 h-4 w-4 animate-spin" }), t('advancedCustomDomain.save')] })] })] })] }) }));
198378
+ : t('advancedCustomDomain.domainHelpPlatform') }), appAddressProblem === 'both' && (jsx("p", { className: "text-xs text-[#646464]", role: "status", children: t('advancedCustomDomain.oneOfSubdomainOrDomain') })), appAddressProblem === 'invalid' && (jsx("p", { className: "text-xs text-[#646464]", role: "status", children: t('advancedCustomDomain.subdomainInvalid') }))] }), jsxs("div", { className: "flex justify-end gap-2", children: [jsx(Button$1, { type: "button", variant: "outline", onClick: close, children: t('advancedCustomDomain.cancel') }), jsxs(Button$1, { type: "submit", disabled: isSubmitting || !canSubmit, children: [isSubmitting && jsx(LoaderCircle, { className: "mr-2 h-4 w-4 animate-spin" }), t('advancedCustomDomain.save')] })] })] })] }) }));
198324
198379
  };
198325
198380
 
198326
198381
  var SLOTTABLE_IDENTIFIER = Symbol("radix.slottable");
@@ -198512,7 +198567,7 @@ const resolveCname = async (domain, target) => {
198512
198567
  const body = await response.json();
198513
198568
  return Boolean((_a = body === null || body === void 0 ? void 0 : body.Answer) === null || _a === void 0 ? void 0 : _a.some((answer) => { var _a; return ((_a = answer.data) !== null && _a !== void 0 ? _a : '').replace(/\.$/, '') === target; }));
198514
198569
  };
198515
- const DomainRow = ({ row, sslValue, onReverify, onRemove, isBusy, onStatusChange, }) => {
198570
+ const DomainRow = ({ row, sharedApp, sslValue, onReverify, onRemove, isBusy, onStatusChange, }) => {
198516
198571
  var _a;
198517
198572
  const t = useT();
198518
198573
  const isHosted = row.spa === VERCEL_SPA;
@@ -198584,7 +198639,7 @@ const DomainRow = ({ row, sslValue, onReverify, onRemove, isBusy, onStatusChange
198584
198639
  const statusBadge = (jsx("span", { className: status === 'verified'
198585
198640
  ? 'rounded bg-green-100 px-2 py-1 text-xs text-green-800'
198586
198641
  : 'rounded bg-gray-100 px-2 py-1 text-xs text-gray-600', children: status === 'checking' ? (jsxs("span", { className: "flex items-center gap-1", children: [jsx(LoaderCircle, { className: "h-3 w-3 animate-spin" }), t('advancedCustomDomain.verifying')] })) : (statusLabel()) }));
198587
- return (jsxs("div", { className: "space-y-3 rounded-lg border border-gray-200 p-3 dark:border-gray-700", children: [jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm", children: row.custom_domain }), jsx("span", { className: "text-xs text-[#646464]", children: isHosted ? t('advancedCustomDomain.kindApp') : t('advancedCustomDomain.kindPlatform') }), statusBadge] }), jsxs("div", { className: "flex items-center gap-2", children: [jsx(Button$1, { variant: "outline", size: "sm", onClick: () => (isHosted ? void onReverify(row) : void checkSpaDomain()), disabled: isBusy || status === 'checking' || (!isHosted && !row.registered_with_dns_pro), "aria-label": t('advancedCustomDomain.reverifyAriaLabel', { domain: row.custom_domain }), children: isBusy || status === 'checking' ? (jsx(LoaderCircle, { className: "h-4 w-4 animate-spin" })) : (jsxs(Fragment$1, { children: [jsx(RefreshCw, { className: "mr-1 h-4 w-4", "aria-hidden": "true" }), t('advancedCustomDomain.reverify')] })) }), jsx(Button$1, { variant: "outline", size: "sm", className: "text-red-600 hover:text-red-700", onClick: () => setConfirmOpen(true), disabled: isBusy, "aria-label": t('advancedCustomDomain.removeAriaLabel', { domain: row.custom_domain }), children: isHosted ? t('advancedCustomDomain.detach') : t('advancedCustomDomain.delete') })] })] }), records.length > 0 ? (jsxs(Table, { className: "table-fixed", children: [jsx(TableHeader, { children: jsxs(TableRow, { children: [jsx(TableHead, { className: "w-[90px]", children: t('advancedCustomDomain.tableType') }), jsx(TableHead, { className: "w-1/3", children: t('advancedCustomDomain.tableNameHost') }), jsx(TableHead, { children: t('advancedCustomDomain.tableValue') })] }) }), jsx(TableBody, { children: records.map((record, index) => (jsxs(TableRow, { children: [jsx(TableCell, { className: "text-sm", children: record.type }), jsx(TableCell, { children: jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm break-all", children: record.name }), jsx(Button$1, { type: "button", variant: "ghost", size: "icon", className: "h-6 w-6 shrink-0", onClick: () => handleCopy(record.name, `name-${index}`), "aria-label": t('advancedCustomDomain.copyNameHostAriaLabel', {
198642
+ return (jsxs("div", { className: "space-y-3 rounded-lg border border-gray-200 p-3 dark:border-gray-700", children: [jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm", children: row.custom_domain }), jsx("span", { className: "text-xs text-[#646464]", children: isHosted ? t('advancedCustomDomain.kindApp') : t('advancedCustomDomain.kindPlatform') }), statusBadge] }), jsxs("div", { className: "flex items-center gap-2", children: [jsx(Button$1, { variant: "outline", size: "sm", onClick: () => (isHosted ? void onReverify(row) : void checkSpaDomain()), disabled: isBusy || status === 'checking' || (!isHosted && !row.registered_with_dns_pro), "aria-label": t('advancedCustomDomain.reverifyAriaLabel', { domain: row.custom_domain }), children: isBusy || status === 'checking' ? (jsx(LoaderCircle, { className: "h-4 w-4 animate-spin" })) : (jsxs(Fragment$1, { children: [jsx(RefreshCw, { className: "mr-1 h-4 w-4", "aria-hidden": "true" }), t('advancedCustomDomain.reverify')] })) }), jsx(Button$1, { variant: "outline", size: "sm", className: "text-red-600 hover:text-red-700", onClick: () => setConfirmOpen(true), disabled: isBusy, "aria-label": t('advancedCustomDomain.removeAriaLabel', { domain: row.custom_domain }), children: isHosted ? t('advancedCustomDomain.detach') : t('advancedCustomDomain.delete') })] })] }), sharedApp ? (jsxs("div", { className: "space-y-1 text-sm text-[#646464]", children: [jsxs("p", { children: [t('advancedCustomDomain.sharedDomainLine'), ' ', jsx("a", { href: `https://${row.custom_domain}`, target: "_blank", rel: "noreferrer", className: "underline", children: t('advancedCustomDomain.openSite', { domain: row.custom_domain }) })] }), jsx("p", { className: "text-xs", children: t('advancedCustomDomain.servesApp', { app: sharedApp.name }) })] })) : records.length > 0 ? (jsxs(Table, { className: "table-fixed", children: [jsx(TableHeader, { children: jsxs(TableRow, { children: [jsx(TableHead, { className: "w-[90px]", children: t('advancedCustomDomain.tableType') }), jsx(TableHead, { className: "w-1/3", children: t('advancedCustomDomain.tableNameHost') }), jsx(TableHead, { children: t('advancedCustomDomain.tableValue') })] }) }), jsx(TableBody, { children: records.map((record, index) => (jsxs(TableRow, { children: [jsx(TableCell, { className: "text-sm", children: record.type }), jsx(TableCell, { children: jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm break-all", children: record.name }), jsx(Button$1, { type: "button", variant: "ghost", size: "icon", className: "h-6 w-6 shrink-0", onClick: () => handleCopy(record.name, `name-${index}`), "aria-label": t('advancedCustomDomain.copyNameHostAriaLabel', {
198588
198643
  domain: record.name,
198589
198644
  }), children: copiedKey === `name-${index}` ? (jsx(Check, { className: "h-4 w-4 text-blue-600", "aria-hidden": "true" })) : (jsx(Copy, { className: "h-4 w-4", "aria-hidden": "true" })) })] }) }), jsxs(TableCell, { children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm break-all", children: record.value }), jsx(Button$1, { type: "button", variant: "ghost", size: "icon", className: "h-6 w-6 shrink-0", onClick: () => handleCopy(record.value, `value-${index}`), "aria-label": t('advancedCustomDomain.copyValueAriaLabel'), children: copiedKey === `value-${index}` ? (jsx(Check, { className: "h-4 w-4 text-blue-600", "aria-hidden": "true" })) : (jsx(Copy, { className: "h-4 w-4", "aria-hidden": "true" })) })] }), record.reason && jsx("p", { className: "mt-1 text-xs text-[#646464]", children: record.reason })] })] }, `${record.type}-${record.name}-${record.value}`))) })] })) : (jsx("p", { className: "text-sm text-[#646464]", children: t('advancedCustomDomain.noRecordsYet') })), jsx(AlertDialog, { open: confirmOpen, onOpenChange: setConfirmOpen, children: jsxs(AlertDialogContent, { children: [jsxs(AlertDialogHeader, { children: [jsx(AlertDialogTitle, { children: t('advancedCustomDomain.confirmRemoveTitle') }), jsx(AlertDialogDescription, { children: isHosted
198590
198645
  ? t('advancedCustomDomain.confirmDetachBody', { domain: row.custom_domain })
@@ -198642,9 +198697,20 @@ const CustomDomainsContent = ({ platformKey, username, currentSPA, currentPlatfo
198642
198697
  const { data, isLoading, isFetching, refetch } = useGetCustomDomainsQuery({
198643
198698
  params: { platform_key: platformKey },
198644
198699
  });
198645
- // Only the caller's own deployed apps are listable, and only the Add dialog
198646
- // needs them — so this stays idle until it opens.
198647
- const { data: projectData, isFetching: isLoadingProjects } = useGetHostingProjectsQuery({ org: platformKey, username }, { skip: !isModalOpen || !username });
198700
+ // Only the caller's own deployed apps are listable. The Add dialog offers
198701
+ // them, and a row on the platform's shared domain names the app it serves
198702
+ // (`shared_domain` is that address), so the list is wanted from the start.
198703
+ const { data: projectData, isFetching: isLoadingProjects } = useGetHostingProjectsQuery({ org: platformKey, username }, { skip: !username });
198704
+ // The caller's app served at a shared-domain address, by that address. A
198705
+ // row without a match is an own domain (or another user's app): it keeps
198706
+ // its records.
198707
+ const sharedAppFor = (row) => {
198708
+ var _a;
198709
+ return row.spa === VERCEL_SPA
198710
+ ? ((_a = projectData === null || projectData === void 0 ? void 0 : projectData.projects) !== null && _a !== void 0 ? _a : []).find((project) => !!project.shared_domain &&
198711
+ project.shared_domain.toLowerCase() === row.custom_domain.toLowerCase())
198712
+ : undefined;
198713
+ };
198648
198714
  const [createCustomDomain, { isLoading: isCreating }] = useCreateCustomDomainMutation();
198649
198715
  const [deleteCustomDomain] = useDeleteCustomDomainMutation();
198650
198716
  const [createHostingProjectDomain, { isLoading: isAttaching }] = useCreateHostingProjectDomainMutation();
@@ -198673,10 +198739,15 @@ const CustomDomainsContent = ({ platformKey, username, currentSPA, currentPlatfo
198673
198739
  : typeof window !== 'undefined'
198674
198740
  ? window.location.hostname.split('.').slice(-2).join('.')
198675
198741
  : 'iblai.app'}`;
198676
- // A platform gets one sign-in domain per SPA, so the Add dialog offers that
198677
- // kind only while this SPA has none. Deployed apps have no such limit.
198678
- const canAddPlatformDomain = useMemo(() => Boolean(currentSPA) &&
198679
- !customDomains.some((domain) => { var _a; return domain.spa !== VERCEL_SPA && ((_a = domain.spa) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(currentSPA.toLowerCase())); }), [customDomains, currentSPA]);
198742
+ // The Add dialog offers the sign-in kind whenever it knows which app to bind
198743
+ // the domain to. How many sign-in domains an app may have is the platform's
198744
+ // decision, not the panel's: a refusal comes back as the platform's own
198745
+ // message (`reportError` shows it), so no count is enforced here.
198746
+ const canAddPlatformDomain = Boolean(currentSPA);
198747
+ // The kind the dialog opens on: an app that already has a sign-in domain is
198748
+ // more often adding one for a deployed app; the sign-in kind stays one
198749
+ // click away either way.
198750
+ const hasPlatformDomain = useMemo(() => customDomains.some((domain) => { var _a; return domain.spa !== VERCEL_SPA && ((_a = domain.spa) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(currentSPA.toLowerCase())); }), [customDomains, currentSPA]);
198680
198751
  /**
198681
198752
  * The status decides what the user should do next, so it gets its own
198682
198753
  * message. 409 shares 400's: both mean "not this domain, not here", and the
@@ -198701,10 +198772,10 @@ const CustomDomainsContent = ({ platformKey, username, currentSPA, currentPlatfo
198701
198772
  };
198702
198773
  const reportError = (error, fallback) => {
198703
198774
  const status = error === null || error === void 0 ? void 0 : error.status;
198704
- // The server's own words go underneath the headline — except on 409, the
198705
- // one status whose body names the organization holding the domain.
198706
- const description = status === 409 ? undefined : serverDetail(error);
198707
- toast.error(headlineForStatus(status, fallback), { description });
198775
+ // The server's own words go underneath the headline. A 409 body never
198776
+ // names the organization holding the domain — the backend keeps that out
198777
+ // on purpose — and for a taken subdomain it carries the way out.
198778
+ toast.error(headlineForStatus(status, fallback), { description: serverDetail(error) });
198708
198779
  };
198709
198780
  const handleAddPlatformDomain = async (domain) => {
198710
198781
  if (!domain) {
@@ -198728,15 +198799,16 @@ const CustomDomainsContent = ({ platformKey, username, currentSPA, currentPlatfo
198728
198799
  reportError(error, t('advancedCustomDomain.createError'));
198729
198800
  }
198730
198801
  };
198731
- const handleAddAppDomain = async (project, domain) => {
198802
+ const handleAddAppDomain = async (project, address) => {
198732
198803
  try {
198733
- // An empty domain is the "assign me a subdomain" path, which the backend
198734
- // reads as the absent key — not as an empty string.
198804
+ // Only the address that was filled in is sent; the dialog guarantees
198805
+ // exactly one. The backend reads an absent key, never an empty string.
198735
198806
  await createHostingProjectDomain({
198736
198807
  org: platformKey,
198737
198808
  username,
198738
198809
  project,
198739
- domain: domain || undefined,
198810
+ domain: address.domain || undefined,
198811
+ subdomain: address.subdomain || undefined,
198740
198812
  }).unwrap();
198741
198813
  await refetch();
198742
198814
  toast.success(t('advancedCustomDomain.createSuccess'));
@@ -198801,7 +198873,7 @@ const CustomDomainsContent = ({ platformKey, username, currentSPA, currentPlatfo
198801
198873
  setIsModalOpen(true);
198802
198874
  }, children: [jsx(Plus, { className: "h-4 w-4", "aria-hidden": "true" }), t('advancedCustomDomain.add')] }), jsx("button", { onClick: () => setIsCollapsed((previous) => !previous), className: "rounded p-1 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 focus:outline-none dark:hover:bg-gray-800", "aria-label": isCollapsed
198803
198875
  ? t('advancedCustomDomain.expandAriaLabel')
198804
- : t('advancedCustomDomain.collapseAriaLabel'), type: "button", children: isCollapsed ? (jsx(ChevronDown, { className: "h-5 w-5 text-gray-400", "aria-hidden": "true" })) : (jsx(ChevronUp, { className: "h-5 w-5 text-gray-400", "aria-hidden": "true" })) })] })] }), !isCollapsed && (jsx("div", { className: "mt-4", children: isLoading ? (jsx("div", { className: "p-4 text-gray-500", children: t('advancedCustomDomain.loading') })) : customDomains.length > 0 ? (jsx("div", { className: "space-y-3", children: sortedDomains.map((row) => (jsx(DomainRow, { row: row, sslValue: sslValue, onReverify: handleReverify, onRemove: handleRemove, isBusy: busyDomainId === row.id, onStatusChange: handleStatusChange }, row.id))) })) : (jsx("div", { className: "p-4 text-center text-sm text-gray-500", children: t('advancedCustomDomain.noDomainConfigured') })) })), jsx(AddDomainDialog, { open: isModalOpen, onOpenChange: setIsModalOpen, canAddPlatformDomain: canAddPlatformDomain, projects: (_a = projectData === null || projectData === void 0 ? void 0 : projectData.projects) !== null && _a !== void 0 ? _a : [], isLoadingProjects: isLoadingProjects, isSubmitting: isCreating || isAttaching, onSubmitPlatformDomain: handleAddPlatformDomain, onSubmitAppDomain: handleAddAppDomain })] }));
198876
+ : t('advancedCustomDomain.collapseAriaLabel'), type: "button", children: isCollapsed ? (jsx(ChevronDown, { className: "h-5 w-5 text-gray-400", "aria-hidden": "true" })) : (jsx(ChevronUp, { className: "h-5 w-5 text-gray-400", "aria-hidden": "true" })) })] })] }), !isCollapsed && (jsx("div", { className: "mt-4", children: isLoading ? (jsx("div", { className: "p-4 text-gray-500", children: t('advancedCustomDomain.loading') })) : customDomains.length > 0 ? (jsx("div", { className: "space-y-3", children: sortedDomains.map((row) => (jsx(DomainRow, { row: row, sharedApp: sharedAppFor(row), sslValue: sslValue, onReverify: handleReverify, onRemove: handleRemove, isBusy: busyDomainId === row.id, onStatusChange: handleStatusChange }, row.id))) })) : (jsx("div", { className: "p-4 text-center text-sm text-gray-500", children: t('advancedCustomDomain.noDomainConfigured') })) })), jsx(AddDomainDialog, { open: isModalOpen, onOpenChange: setIsModalOpen, canAddPlatformDomain: canAddPlatformDomain, defaultMode: hasPlatformDomain ? 'app' : 'platform', projects: (_a = projectData === null || projectData === void 0 ? void 0 : projectData.projects) !== null && _a !== void 0 ? _a : [], isLoadingProjects: isLoadingProjects, isSubmitting: isCreating || isAttaching, onSubmitPlatformDomain: handleAddPlatformDomain, onSubmitAppDomain: handleAddAppDomain })] }));
198805
198877
  };
198806
198878
 
198807
198879
  const StudentMentorCreationContent = ({ platformKey, }) => {
@@ -229871,7 +229943,9 @@ function SummerNoteTextEditor({ value, onChange, exportFormat = 'markdown', disa
229871
229943
  isApplyingValueRef.current = false;
229872
229944
  }
229873
229945
  }, [value, isReady, toEditorHtml]);
229874
- React__default.useEffect(() => {
229946
+ // A layout effect: the loading node leaves in this same commit, so the editor
229947
+ // is never painted editable while disabled. The test helper waits on that node.
229948
+ React__default.useLayoutEffect(() => {
229875
229949
  const instance = instanceRef.current;
229876
229950
  if (!instance || !isReady) {
229877
229951
  return;
@@ -229899,8 +229973,9 @@ function SummerNoteTextEditor({ value, onChange, exportFormat = 'markdown', disa
229899
229973
  editable.style.maxHeight = `${Math.max(EDITOR_MAX_HEIGHT, minHeightPx)}px`;
229900
229974
  }, [minHeightPx, isReady]);
229901
229975
  // Summernote's editable region is a plain contenteditable div; describe it the
229902
- // same way `RichTextEditor` describes its own.
229903
- React__default.useEffect(() => {
229976
+ // same way `RichTextEditor` describes its own. A layout effect, so the region
229977
+ // is never painted without its name.
229978
+ React__default.useLayoutEffect(() => {
229904
229979
  const editable = editableRef.current;
229905
229980
  if (!editable || !isReady) {
229906
229981
  return;
@@ -96359,7 +96359,11 @@ var advancedCustomDomain$3 = {
96359
96359
  noProjects: "You have not deployed any apps yet. Deploy one first, then come back to give it a domain.",
96360
96360
  customDomainLabel: "Domain",
96361
96361
  domainHelpPlatform: "Without the scheme or a leading “www.” — for example, app.example.com.",
96362
- domainHelpApp: "Leave this empty and a subdomain will be assigned to the app for you, with no DNS records to set.",
96362
+ domainHelpApp: "A domain you own, without the scheme or a leading “www.” — for example, app.example.com. Leave it empty to use the subdomain above.",
96363
+ subdomainLabel: "Subdomain",
96364
+ subdomainHelp: "Lowercase letters, digits and hyphens, like a username. The app is served at <subdomain>.<the platform’s shared domain>, with no DNS records to set.",
96365
+ oneOfSubdomainOrDomain: "Fill in either the subdomain or a domain you own, not both.",
96366
+ subdomainInvalid: "A subdomain is 1–63 lowercase letters, digits and hyphens, and cannot start or end with a hyphen.",
96363
96367
  cancel: "Cancel",
96364
96368
  save: "Save",
96365
96369
  expandAriaLabel: "Expand domains",
@@ -96381,6 +96385,9 @@ var advancedCustomDomain$3 = {
96381
96385
  copyNameHostAriaLabel: "Copy name/host: {domain}",
96382
96386
  copyValueAriaLabel: "Copy value/target",
96383
96387
  noRecordsYet: "No records yet. Check the domain again to fetch them.",
96388
+ sharedDomainLine: "Runs on our shared domain — nothing to set up.",
96389
+ openSite: "Open {domain}",
96390
+ servesApp: "Serves {app}",
96384
96391
  confirmRemoveTitle: "Remove this domain?",
96385
96392
  confirmDeleteBody: "{domain} will stop signing users in to this organization.",
96386
96393
  confirmDetachBody: "{domain} will stop serving the app, here and at the hosting provider.",
@@ -103904,7 +103911,11 @@ var advancedCustomDomain$2 = {
103904
103911
  noProjects: "Vous n’avez encore déployé aucune application. Déployez-en une, puis revenez lui donner un domaine.",
103905
103912
  customDomainLabel: "Domaine",
103906
103913
  domainHelpPlatform: "Sans le schéma ni un « www. » en tête — par exemple, app.example.com.",
103907
- domainHelpApp: "Laissez ce champ vide et un sous-domaine sera attribué à l’application, sans aucun enregistrement DNS à créer.",
103914
+ domainHelpApp: "Un domaine qui vous appartient, sans le schéma ni un « www. » en tête — par exemple, app.example.com. Laissez ce champ vide pour utiliser le sous-domaine ci-dessus.",
103915
+ subdomainLabel: "Sous-domaine",
103916
+ subdomainHelp: "Lettres minuscules, chiffres et tirets, comme un nom d’utilisateur. L’application est servie à <sous-domaine>.<le domaine partagé de la plateforme>, sans aucun enregistrement DNS à créer.",
103917
+ oneOfSubdomainOrDomain: "Renseignez soit le sous-domaine, soit un domaine qui vous appartient, pas les deux.",
103918
+ subdomainInvalid: "Un sous-domaine compte de 1 à 63 lettres minuscules, chiffres et tirets, et ne peut ni commencer ni finir par un tiret.",
103908
103919
  cancel: "Annuler",
103909
103920
  save: "Enregistrer",
103910
103921
  expandAriaLabel: "Déplier les domaines",
@@ -103926,6 +103937,9 @@ var advancedCustomDomain$2 = {
103926
103937
  copyNameHostAriaLabel: "Copier le nom/hôte : {domain}",
103927
103938
  copyValueAriaLabel: "Copier la valeur/cible",
103928
103939
  noRecordsYet: "Aucun enregistrement pour l’instant. Vérifiez le domaine à nouveau pour les obtenir.",
103940
+ sharedDomainLine: "Fonctionne sur notre domaine partagé — rien à configurer.",
103941
+ openSite: "Ouvrir {domain}",
103942
+ servesApp: "Dessert {app}",
103929
103943
  confirmRemoveTitle: "Supprimer ce domaine ?",
103930
103944
  confirmDeleteBody: "{domain} ne permettra plus de se connecter à cette organisation.",
103931
103945
  confirmDetachBody: "{domain} cessera de servir l’application, ici comme chez l’hébergeur.",
@@ -111449,7 +111463,11 @@ var advancedCustomDomain$1 = {
111449
111463
  noProjects: "Todavía no has desplegado ninguna aplicación. Despliega una primero y vuelve para darle un dominio.",
111450
111464
  customDomainLabel: "Dominio",
111451
111465
  domainHelpPlatform: "Sin el esquema ni un “www.” inicial — por ejemplo, app.example.com.",
111452
- domainHelpApp: "Déjalo vacío y se asignará un subdominio a la aplicación, sin registros DNS que configurar.",
111466
+ domainHelpApp: "Un dominio tuyo, sin el esquema ni un “www.” inicial — por ejemplo, app.example.com. Déjalo vacío para usar el subdominio de arriba.",
111467
+ subdomainLabel: "Subdominio",
111468
+ subdomainHelp: "Letras minúsculas, dígitos y guiones, como un nombre de usuario. La aplicación se sirve en <subdominio>.<el dominio compartido de la plataforma>, sin registros DNS que configurar.",
111469
+ oneOfSubdomainOrDomain: "Rellena el subdominio o un dominio tuyo, no ambos.",
111470
+ subdomainInvalid: "Un subdominio tiene entre 1 y 63 letras minúsculas, dígitos y guiones, y no puede empezar ni terminar con un guion.",
111453
111471
  cancel: "Cancelar",
111454
111472
  save: "Guardar",
111455
111473
  expandAriaLabel: "Expandir dominios",
@@ -111471,6 +111489,9 @@ var advancedCustomDomain$1 = {
111471
111489
  copyNameHostAriaLabel: "Copiar nombre/host: {domain}",
111472
111490
  copyValueAriaLabel: "Copiar valor/destino",
111473
111491
  noRecordsYet: "Aún no hay registros. Comprueba el dominio de nuevo para obtenerlos.",
111492
+ sharedDomainLine: "Funciona en nuestro dominio compartido — no hay nada que configurar.",
111493
+ openSite: "Abrir {domain}",
111494
+ servesApp: "Sirve {app}",
111474
111495
  confirmRemoveTitle: "¿Eliminar este dominio?",
111475
111496
  confirmDeleteBody: "{domain} dejará de permitir el inicio de sesión en esta organización.",
111476
111497
  confirmDetachBody: "{domain} dejará de servir la aplicación, aquí y en el proveedor de alojamiento.",
@@ -118994,7 +119015,11 @@ var advancedCustomDomain = {
118994
119015
  noProjects: "你还没有部署任何应用。请先部署一个,然后回来为它设置域名。",
118995
119016
  customDomainLabel: "域名",
118996
119017
  domainHelpPlatform: "不要带协议前缀或开头的“www.”,例如 app.example.com。",
118997
- domainHelpApp: "留空即可自动为该应用分配一个子域名,无需设置任何 DNS 记录。",
119018
+ domainHelpApp: "你拥有的域名,不要带协议前缀或开头的“www.”,例如 app.example.com。留空则使用上方的子域名。",
119019
+ subdomainLabel: "子域名",
119020
+ subdomainHelp: "小写字母、数字和连字符,像用户名一样。应用将通过 <子域名>.<平台的共享域名> 提供服务,无需设置任何 DNS 记录。",
119021
+ oneOfSubdomainOrDomain: "请填写子域名或你拥有的域名,二者只填其一。",
119022
+ subdomainInvalid: "子域名为 1 到 63 个小写字母、数字和连字符,且不能以连字符开头或结尾。",
118998
119023
  cancel: "取消",
118999
119024
  save: "保存",
119000
119025
  expandAriaLabel: "展开域名",
@@ -119016,6 +119041,9 @@ var advancedCustomDomain = {
119016
119041
  copyNameHostAriaLabel: "复制名称/主机:{domain}",
119017
119042
  copyValueAriaLabel: "复制值/目标",
119018
119043
  noRecordsYet: "暂无记录。请重新检查该域名以获取记录。",
119044
+ sharedDomainLine: "运行在我们的共享域名上——无需任何设置。",
119045
+ openSite: "打开 {domain}",
119046
+ servesApp: "服务于 {app}",
119019
119047
  confirmRemoveTitle: "要移除这个域名吗?",
119020
119048
  confirmDeleteBody: "{domain} 将不再用于登录该组织。",
119021
119049
  confirmDetachBody: "{domain} 将不再为该应用提供服务,本处和托管服务商处都会解除。",
@@ -230705,15 +230733,23 @@ const sanitizeDomain = (domain) => domain
230705
230733
  .replace(/^https?:\/\//, '')
230706
230734
  .replace(/\/.*$/, '')
230707
230735
  .replace(/^www\./, '');
230708
- const AddDomainDialog = ({ open, onOpenChange, canAddPlatformDomain, projects, isLoadingProjects, isSubmitting, onSubmitPlatformDomain, onSubmitAppDomain, }) => {
230736
+ /** One label under the platform's shared domain — the backend's own rule. */
230737
+ const SUBDOMAIN_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
230738
+ /** Typed like a username: only case and surrounding whitespace are forgiven. */
230739
+ const sanitizeSubdomain = (subdomain) => subdomain.trim().toLowerCase();
230740
+ const AddDomainDialog = ({ open, onOpenChange, canAddPlatformDomain, defaultMode = 'platform', projects, isLoadingProjects, isSubmitting, onSubmitPlatformDomain, onSubmitAppDomain, }) => {
230741
+ var _a;
230709
230742
  const t = useT();
230710
- const [mode, setMode] = useState(canAddPlatformDomain ? 'platform' : 'app');
230743
+ const initialMode = canAddPlatformDomain ? defaultMode : 'app';
230744
+ const [mode, setMode] = useState(initialMode);
230711
230745
  const [domainValue, setDomainValue] = useState('');
230746
+ const [subdomainValue, setSubdomainValue] = useState('');
230712
230747
  const [projectId, setProjectId] = useState('');
230713
230748
  const reset = () => {
230714
230749
  setDomainValue('');
230750
+ setSubdomainValue('');
230715
230751
  setProjectId('');
230716
- setMode(canAddPlatformDomain ? 'platform' : 'app');
230752
+ setMode(initialMode);
230717
230753
  };
230718
230754
  const close = () => {
230719
230755
  reset();
@@ -230725,10 +230761,27 @@ const AddDomainDialog = ({ open, onOpenChange, canAddPlatformDomain, projects, i
230725
230761
  await onSubmitPlatformDomain(sanitizeDomain(domainValue));
230726
230762
  }
230727
230763
  else {
230728
- await onSubmitAppDomain(Number(projectId), sanitizeDomain(domainValue));
230764
+ await onSubmitAppDomain(Number(projectId), {
230765
+ domain: sanitizeDomain(domainValue),
230766
+ subdomain: sanitizeSubdomain(subdomainValue),
230767
+ });
230729
230768
  }
230730
230769
  };
230731
- const canSubmit = mode === 'platform' ? domainValue.trim().length > 0 : projectId.trim().length > 0;
230770
+ const hasDomain = domainValue.trim().length > 0;
230771
+ const subdomain = sanitizeSubdomain(subdomainValue);
230772
+ const hasSubdomain = subdomain.length > 0;
230773
+ // An app is served at exactly one address, and a subdomain has to be one the
230774
+ // backend will take — say which rule is unmet, and only once something was
230775
+ // typed, so an untouched form stays quiet.
230776
+ const appAddressProblem = mode === 'app' && hasDomain && hasSubdomain
230777
+ ? 'both'
230778
+ : mode === 'app' && hasSubdomain && !SUBDOMAIN_RE.test(subdomain)
230779
+ ? 'invalid'
230780
+ : null;
230781
+ const canSubmit = mode === 'platform'
230782
+ ? hasDomain
230783
+ : projectId.trim().length > 0 && (hasDomain || hasSubdomain) && appAddressProblem === null;
230784
+ const selectedProject = projects.find((project) => String(project.id) === projectId);
230732
230785
  return (jsx(Dialog, { open: open, onOpenChange: (next) => {
230733
230786
  if (!next) {
230734
230787
  reset();
@@ -230737,9 +230790,11 @@ const AddDomainDialog = ({ open, onOpenChange, canAddPlatformDomain, projects, i
230737
230790
  }, children: jsxs(DialogContent, { className: "sm:max-w-[480px]", children: [jsxs(DialogHeader, { children: [jsx(DialogTitle, { children: t('advancedCustomDomain.dialogTitle') }), jsx(DialogDescription, { children: t('advancedCustomDomain.dialogDescription') })] }), jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [jsxs("div", { className: "space-y-2", children: [jsx("label", { htmlFor: "add-domain-kind", className: "text-sm", children: t('advancedCustomDomain.kindLabel') }), jsxs(Select$1, { value: mode, onValueChange: (next) => setMode(next), children: [jsx(SelectTrigger, { id: "add-domain-kind", children: jsx(SelectValue, {}) }), jsxs(SelectContent, { children: [canAddPlatformDomain && (jsx(SelectItem, { value: "platform", children: t('advancedCustomDomain.kindPlatform') })), jsx(SelectItem, { value: "app", children: t('advancedCustomDomain.kindApp') })] })] })] }), mode === 'app' && (jsxs("div", { className: "space-y-2", children: [jsx("label", { htmlFor: "add-domain-project", className: "text-sm", children: t('advancedCustomDomain.projectLabel') }), isLoadingProjects ? (jsx("p", { className: "text-sm text-[#646464]", children: t('advancedCustomDomain.loading') })) : projects.length === 0 ? (
230738
230791
  // The backend lists only the caller's own projects, so this is
230739
230792
  // "you have not deployed anything", not "the platform has not".
230740
- jsx("p", { className: "text-sm text-[#646464]", children: t('advancedCustomDomain.noProjects') })) : (jsxs(Select$1, { value: projectId, onValueChange: setProjectId, children: [jsx(SelectTrigger, { id: "add-domain-project", children: jsx(SelectValue, { placeholder: t('advancedCustomDomain.projectPlaceholder') }) }), jsx(SelectContent, { children: projects.map((project) => (jsx(SelectItem, { value: String(project.id), children: project.name }, project.id))) })] }))] })), jsxs("div", { className: "space-y-2", children: [jsx("label", { htmlFor: "add-domain-name", className: "text-sm", children: t('advancedCustomDomain.customDomainLabel') }), jsx(Input, { id: "add-domain-name", value: domainValue, onChange: (event) => setDomainValue(event.target.value), placeholder: "example.com", className: "w-full", required: mode === 'platform' }), jsx("p", { className: "text-xs text-[#646464]", children: mode === 'app'
230793
+ jsx("p", { className: "text-sm text-[#646464]", children: t('advancedCustomDomain.noProjects') })) : (jsxs(Select$1, { value: projectId, onValueChange: setProjectId, children: [jsx(SelectTrigger, { id: "add-domain-project", children: jsx(SelectValue, { placeholder: t('advancedCustomDomain.projectPlaceholder') }) }), jsx(SelectContent, { children: projects.map((project) => (jsx(SelectItem, { value: String(project.id), children: project.name }, project.id))) })] }))] })), mode === 'app' && (jsxs("div", { className: "space-y-2", children: [jsx("label", { htmlFor: "add-domain-subdomain", className: "text-sm", children: t('advancedCustomDomain.subdomainLabel') }), jsx(Input, { id: "add-domain-subdomain", value: subdomainValue, onChange: (event) => setSubdomainValue(event.target.value),
230794
+ // The app's own name is the suggestion the deploy skill makes.
230795
+ placeholder: (_a = selectedProject === null || selectedProject === void 0 ? void 0 : selectedProject.name) !== null && _a !== void 0 ? _a : '', className: "w-full" }), jsx("p", { className: "text-xs text-[#646464]", children: t('advancedCustomDomain.subdomainHelp') })] })), jsxs("div", { className: "space-y-2", children: [jsx("label", { htmlFor: "add-domain-name", className: "text-sm", children: t('advancedCustomDomain.customDomainLabel') }), jsx(Input, { id: "add-domain-name", value: domainValue, onChange: (event) => setDomainValue(event.target.value), placeholder: "example.com", className: "w-full", required: mode === 'platform' }), jsx("p", { className: "text-xs text-[#646464]", children: mode === 'app'
230741
230796
  ? t('advancedCustomDomain.domainHelpApp')
230742
- : t('advancedCustomDomain.domainHelpPlatform') })] }), jsxs("div", { className: "flex justify-end gap-2", children: [jsx(Button$1, { type: "button", variant: "outline", onClick: close, children: t('advancedCustomDomain.cancel') }), jsxs(Button$1, { type: "submit", disabled: isSubmitting || !canSubmit, children: [isSubmitting && jsx(LoaderCircle, { className: "mr-2 h-4 w-4 animate-spin" }), t('advancedCustomDomain.save')] })] })] })] }) }));
230797
+ : t('advancedCustomDomain.domainHelpPlatform') }), appAddressProblem === 'both' && (jsx("p", { className: "text-xs text-[#646464]", role: "status", children: t('advancedCustomDomain.oneOfSubdomainOrDomain') })), appAddressProblem === 'invalid' && (jsx("p", { className: "text-xs text-[#646464]", role: "status", children: t('advancedCustomDomain.subdomainInvalid') }))] }), jsxs("div", { className: "flex justify-end gap-2", children: [jsx(Button$1, { type: "button", variant: "outline", onClick: close, children: t('advancedCustomDomain.cancel') }), jsxs(Button$1, { type: "submit", disabled: isSubmitting || !canSubmit, children: [isSubmitting && jsx(LoaderCircle, { className: "mr-2 h-4 w-4 animate-spin" }), t('advancedCustomDomain.save')] })] })] })] }) }));
230743
230798
  };
230744
230799
 
230745
230800
  var SLOTTABLE_IDENTIFIER = Symbol("radix.slottable");
@@ -230931,7 +230986,7 @@ const resolveCname = async (domain, target) => {
230931
230986
  const body = await response.json();
230932
230987
  return Boolean((_a = body === null || body === void 0 ? void 0 : body.Answer) === null || _a === void 0 ? void 0 : _a.some((answer) => { var _a; return ((_a = answer.data) !== null && _a !== void 0 ? _a : '').replace(/\.$/, '') === target; }));
230933
230988
  };
230934
- const DomainRow = ({ row, sslValue, onReverify, onRemove, isBusy, onStatusChange, }) => {
230989
+ const DomainRow = ({ row, sharedApp, sslValue, onReverify, onRemove, isBusy, onStatusChange, }) => {
230935
230990
  var _a;
230936
230991
  const t = useT();
230937
230992
  const isHosted = row.spa === VERCEL_SPA;
@@ -231003,7 +231058,7 @@ const DomainRow = ({ row, sslValue, onReverify, onRemove, isBusy, onStatusChange
231003
231058
  const statusBadge = (jsx("span", { className: status === 'verified'
231004
231059
  ? 'rounded bg-green-100 px-2 py-1 text-xs text-green-800'
231005
231060
  : 'rounded bg-gray-100 px-2 py-1 text-xs text-gray-600', children: status === 'checking' ? (jsxs("span", { className: "flex items-center gap-1", children: [jsx(LoaderCircle, { className: "h-3 w-3 animate-spin" }), t('advancedCustomDomain.verifying')] })) : (statusLabel()) }));
231006
- return (jsxs("div", { className: "space-y-3 rounded-lg border border-gray-200 p-3 dark:border-gray-700", children: [jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm", children: row.custom_domain }), jsx("span", { className: "text-xs text-[#646464]", children: isHosted ? t('advancedCustomDomain.kindApp') : t('advancedCustomDomain.kindPlatform') }), statusBadge] }), jsxs("div", { className: "flex items-center gap-2", children: [jsx(Button$1, { variant: "outline", size: "sm", onClick: () => (isHosted ? void onReverify(row) : void checkSpaDomain()), disabled: isBusy || status === 'checking' || (!isHosted && !row.registered_with_dns_pro), "aria-label": t('advancedCustomDomain.reverifyAriaLabel', { domain: row.custom_domain }), children: isBusy || status === 'checking' ? (jsx(LoaderCircle, { className: "h-4 w-4 animate-spin" })) : (jsxs(Fragment$1, { children: [jsx(RefreshCw, { className: "mr-1 h-4 w-4", "aria-hidden": "true" }), t('advancedCustomDomain.reverify')] })) }), jsx(Button$1, { variant: "outline", size: "sm", className: "text-red-600 hover:text-red-700", onClick: () => setConfirmOpen(true), disabled: isBusy, "aria-label": t('advancedCustomDomain.removeAriaLabel', { domain: row.custom_domain }), children: isHosted ? t('advancedCustomDomain.detach') : t('advancedCustomDomain.delete') })] })] }), records.length > 0 ? (jsxs(Table$1, { className: "table-fixed", children: [jsx(TableHeader$1, { children: jsxs(TableRow$1, { children: [jsx(TableHead, { className: "w-[90px]", children: t('advancedCustomDomain.tableType') }), jsx(TableHead, { className: "w-1/3", children: t('advancedCustomDomain.tableNameHost') }), jsx(TableHead, { children: t('advancedCustomDomain.tableValue') })] }) }), jsx(TableBody, { children: records.map((record, index) => (jsxs(TableRow$1, { children: [jsx(TableCell$1, { className: "text-sm", children: record.type }), jsx(TableCell$1, { children: jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm break-all", children: record.name }), jsx(Button$1, { type: "button", variant: "ghost", size: "icon", className: "h-6 w-6 shrink-0", onClick: () => handleCopy(record.name, `name-${index}`), "aria-label": t('advancedCustomDomain.copyNameHostAriaLabel', {
231061
+ return (jsxs("div", { className: "space-y-3 rounded-lg border border-gray-200 p-3 dark:border-gray-700", children: [jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm", children: row.custom_domain }), jsx("span", { className: "text-xs text-[#646464]", children: isHosted ? t('advancedCustomDomain.kindApp') : t('advancedCustomDomain.kindPlatform') }), statusBadge] }), jsxs("div", { className: "flex items-center gap-2", children: [jsx(Button$1, { variant: "outline", size: "sm", onClick: () => (isHosted ? void onReverify(row) : void checkSpaDomain()), disabled: isBusy || status === 'checking' || (!isHosted && !row.registered_with_dns_pro), "aria-label": t('advancedCustomDomain.reverifyAriaLabel', { domain: row.custom_domain }), children: isBusy || status === 'checking' ? (jsx(LoaderCircle, { className: "h-4 w-4 animate-spin" })) : (jsxs(Fragment$1, { children: [jsx(RefreshCw, { className: "mr-1 h-4 w-4", "aria-hidden": "true" }), t('advancedCustomDomain.reverify')] })) }), jsx(Button$1, { variant: "outline", size: "sm", className: "text-red-600 hover:text-red-700", onClick: () => setConfirmOpen(true), disabled: isBusy, "aria-label": t('advancedCustomDomain.removeAriaLabel', { domain: row.custom_domain }), children: isHosted ? t('advancedCustomDomain.detach') : t('advancedCustomDomain.delete') })] })] }), sharedApp ? (jsxs("div", { className: "space-y-1 text-sm text-[#646464]", children: [jsxs("p", { children: [t('advancedCustomDomain.sharedDomainLine'), ' ', jsx("a", { href: `https://${row.custom_domain}`, target: "_blank", rel: "noreferrer", className: "underline", children: t('advancedCustomDomain.openSite', { domain: row.custom_domain }) })] }), jsx("p", { className: "text-xs", children: t('advancedCustomDomain.servesApp', { app: sharedApp.name }) })] })) : records.length > 0 ? (jsxs(Table$1, { className: "table-fixed", children: [jsx(TableHeader$1, { children: jsxs(TableRow$1, { children: [jsx(TableHead, { className: "w-[90px]", children: t('advancedCustomDomain.tableType') }), jsx(TableHead, { className: "w-1/3", children: t('advancedCustomDomain.tableNameHost') }), jsx(TableHead, { children: t('advancedCustomDomain.tableValue') })] }) }), jsx(TableBody, { children: records.map((record, index) => (jsxs(TableRow$1, { children: [jsx(TableCell$1, { className: "text-sm", children: record.type }), jsx(TableCell$1, { children: jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm break-all", children: record.name }), jsx(Button$1, { type: "button", variant: "ghost", size: "icon", className: "h-6 w-6 shrink-0", onClick: () => handleCopy(record.name, `name-${index}`), "aria-label": t('advancedCustomDomain.copyNameHostAriaLabel', {
231007
231062
  domain: record.name,
231008
231063
  }), children: copiedKey === `name-${index}` ? (jsx(Check, { className: "h-4 w-4 text-blue-600", "aria-hidden": "true" })) : (jsx(Copy, { className: "h-4 w-4", "aria-hidden": "true" })) })] }) }), jsxs(TableCell$1, { children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm break-all", children: record.value }), jsx(Button$1, { type: "button", variant: "ghost", size: "icon", className: "h-6 w-6 shrink-0", onClick: () => handleCopy(record.value, `value-${index}`), "aria-label": t('advancedCustomDomain.copyValueAriaLabel'), children: copiedKey === `value-${index}` ? (jsx(Check, { className: "h-4 w-4 text-blue-600", "aria-hidden": "true" })) : (jsx(Copy, { className: "h-4 w-4", "aria-hidden": "true" })) })] }), record.reason && jsx("p", { className: "mt-1 text-xs text-[#646464]", children: record.reason })] })] }, `${record.type}-${record.name}-${record.value}`))) })] })) : (jsx("p", { className: "text-sm text-[#646464]", children: t('advancedCustomDomain.noRecordsYet') })), jsx(AlertDialog, { open: confirmOpen, onOpenChange: setConfirmOpen, children: jsxs(AlertDialogContent, { children: [jsxs(AlertDialogHeader, { children: [jsx(AlertDialogTitle, { children: t('advancedCustomDomain.confirmRemoveTitle') }), jsx(AlertDialogDescription, { children: isHosted
231009
231064
  ? t('advancedCustomDomain.confirmDetachBody', { domain: row.custom_domain })
@@ -231061,9 +231116,20 @@ const CustomDomainsContent = ({ platformKey, username, currentSPA, currentPlatfo
231061
231116
  const { data, isLoading, isFetching, refetch } = useGetCustomDomainsQuery({
231062
231117
  params: { platform_key: platformKey },
231063
231118
  });
231064
- // Only the caller's own deployed apps are listable, and only the Add dialog
231065
- // needs them — so this stays idle until it opens.
231066
- const { data: projectData, isFetching: isLoadingProjects } = useGetHostingProjectsQuery({ org: platformKey, username }, { skip: !isModalOpen || !username });
231119
+ // Only the caller's own deployed apps are listable. The Add dialog offers
231120
+ // them, and a row on the platform's shared domain names the app it serves
231121
+ // (`shared_domain` is that address), so the list is wanted from the start.
231122
+ const { data: projectData, isFetching: isLoadingProjects } = useGetHostingProjectsQuery({ org: platformKey, username }, { skip: !username });
231123
+ // The caller's app served at a shared-domain address, by that address. A
231124
+ // row without a match is an own domain (or another user's app): it keeps
231125
+ // its records.
231126
+ const sharedAppFor = (row) => {
231127
+ var _a;
231128
+ return row.spa === VERCEL_SPA
231129
+ ? ((_a = projectData === null || projectData === void 0 ? void 0 : projectData.projects) !== null && _a !== void 0 ? _a : []).find((project) => !!project.shared_domain &&
231130
+ project.shared_domain.toLowerCase() === row.custom_domain.toLowerCase())
231131
+ : undefined;
231132
+ };
231067
231133
  const [createCustomDomain, { isLoading: isCreating }] = useCreateCustomDomainMutation();
231068
231134
  const [deleteCustomDomain] = useDeleteCustomDomainMutation();
231069
231135
  const [createHostingProjectDomain, { isLoading: isAttaching }] = useCreateHostingProjectDomainMutation();
@@ -231092,10 +231158,15 @@ const CustomDomainsContent = ({ platformKey, username, currentSPA, currentPlatfo
231092
231158
  : typeof window !== 'undefined'
231093
231159
  ? window.location.hostname.split('.').slice(-2).join('.')
231094
231160
  : 'iblai.app'}`;
231095
- // A platform gets one sign-in domain per SPA, so the Add dialog offers that
231096
- // kind only while this SPA has none. Deployed apps have no such limit.
231097
- const canAddPlatformDomain = useMemo(() => Boolean(currentSPA) &&
231098
- !customDomains.some((domain) => { var _a; return domain.spa !== VERCEL_SPA && ((_a = domain.spa) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(currentSPA.toLowerCase())); }), [customDomains, currentSPA]);
231161
+ // The Add dialog offers the sign-in kind whenever it knows which app to bind
231162
+ // the domain to. How many sign-in domains an app may have is the platform's
231163
+ // decision, not the panel's: a refusal comes back as the platform's own
231164
+ // message (`reportError` shows it), so no count is enforced here.
231165
+ const canAddPlatformDomain = Boolean(currentSPA);
231166
+ // The kind the dialog opens on: an app that already has a sign-in domain is
231167
+ // more often adding one for a deployed app; the sign-in kind stays one
231168
+ // click away either way.
231169
+ const hasPlatformDomain = useMemo(() => customDomains.some((domain) => { var _a; return domain.spa !== VERCEL_SPA && ((_a = domain.spa) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(currentSPA.toLowerCase())); }), [customDomains, currentSPA]);
231099
231170
  /**
231100
231171
  * The status decides what the user should do next, so it gets its own
231101
231172
  * message. 409 shares 400's: both mean "not this domain, not here", and the
@@ -231120,10 +231191,10 @@ const CustomDomainsContent = ({ platformKey, username, currentSPA, currentPlatfo
231120
231191
  };
231121
231192
  const reportError = (error, fallback) => {
231122
231193
  const status = error === null || error === void 0 ? void 0 : error.status;
231123
- // The server's own words go underneath the headline — except on 409, the
231124
- // one status whose body names the organization holding the domain.
231125
- const description = status === 409 ? undefined : serverDetail(error);
231126
- toast.error(headlineForStatus(status, fallback), { description });
231194
+ // The server's own words go underneath the headline. A 409 body never
231195
+ // names the organization holding the domain — the backend keeps that out
231196
+ // on purpose — and for a taken subdomain it carries the way out.
231197
+ toast.error(headlineForStatus(status, fallback), { description: serverDetail(error) });
231127
231198
  };
231128
231199
  const handleAddPlatformDomain = async (domain) => {
231129
231200
  if (!domain) {
@@ -231147,15 +231218,16 @@ const CustomDomainsContent = ({ platformKey, username, currentSPA, currentPlatfo
231147
231218
  reportError(error, t('advancedCustomDomain.createError'));
231148
231219
  }
231149
231220
  };
231150
- const handleAddAppDomain = async (project, domain) => {
231221
+ const handleAddAppDomain = async (project, address) => {
231151
231222
  try {
231152
- // An empty domain is the "assign me a subdomain" path, which the backend
231153
- // reads as the absent key — not as an empty string.
231223
+ // Only the address that was filled in is sent; the dialog guarantees
231224
+ // exactly one. The backend reads an absent key, never an empty string.
231154
231225
  await createHostingProjectDomain({
231155
231226
  org: platformKey,
231156
231227
  username,
231157
231228
  project,
231158
- domain: domain || undefined,
231229
+ domain: address.domain || undefined,
231230
+ subdomain: address.subdomain || undefined,
231159
231231
  }).unwrap();
231160
231232
  await refetch();
231161
231233
  toast.success(t('advancedCustomDomain.createSuccess'));
@@ -231220,7 +231292,7 @@ const CustomDomainsContent = ({ platformKey, username, currentSPA, currentPlatfo
231220
231292
  setIsModalOpen(true);
231221
231293
  }, children: [jsx(Plus, { className: "h-4 w-4", "aria-hidden": "true" }), t('advancedCustomDomain.add')] }), jsx("button", { onClick: () => setIsCollapsed((previous) => !previous), className: "rounded p-1 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 focus:outline-none dark:hover:bg-gray-800", "aria-label": isCollapsed
231222
231294
  ? t('advancedCustomDomain.expandAriaLabel')
231223
- : t('advancedCustomDomain.collapseAriaLabel'), type: "button", children: isCollapsed ? (jsx(ChevronDown, { className: "h-5 w-5 text-gray-400", "aria-hidden": "true" })) : (jsx(ChevronUp, { className: "h-5 w-5 text-gray-400", "aria-hidden": "true" })) })] })] }), !isCollapsed && (jsx("div", { className: "mt-4", children: isLoading ? (jsx("div", { className: "p-4 text-gray-500", children: t('advancedCustomDomain.loading') })) : customDomains.length > 0 ? (jsx("div", { className: "space-y-3", children: sortedDomains.map((row) => (jsx(DomainRow, { row: row, sslValue: sslValue, onReverify: handleReverify, onRemove: handleRemove, isBusy: busyDomainId === row.id, onStatusChange: handleStatusChange }, row.id))) })) : (jsx("div", { className: "p-4 text-center text-sm text-gray-500", children: t('advancedCustomDomain.noDomainConfigured') })) })), jsx(AddDomainDialog, { open: isModalOpen, onOpenChange: setIsModalOpen, canAddPlatformDomain: canAddPlatformDomain, projects: (_a = projectData === null || projectData === void 0 ? void 0 : projectData.projects) !== null && _a !== void 0 ? _a : [], isLoadingProjects: isLoadingProjects, isSubmitting: isCreating || isAttaching, onSubmitPlatformDomain: handleAddPlatformDomain, onSubmitAppDomain: handleAddAppDomain })] }));
231295
+ : t('advancedCustomDomain.collapseAriaLabel'), type: "button", children: isCollapsed ? (jsx(ChevronDown, { className: "h-5 w-5 text-gray-400", "aria-hidden": "true" })) : (jsx(ChevronUp, { className: "h-5 w-5 text-gray-400", "aria-hidden": "true" })) })] })] }), !isCollapsed && (jsx("div", { className: "mt-4", children: isLoading ? (jsx("div", { className: "p-4 text-gray-500", children: t('advancedCustomDomain.loading') })) : customDomains.length > 0 ? (jsx("div", { className: "space-y-3", children: sortedDomains.map((row) => (jsx(DomainRow, { row: row, sharedApp: sharedAppFor(row), sslValue: sslValue, onReverify: handleReverify, onRemove: handleRemove, isBusy: busyDomainId === row.id, onStatusChange: handleStatusChange }, row.id))) })) : (jsx("div", { className: "p-4 text-center text-sm text-gray-500", children: t('advancedCustomDomain.noDomainConfigured') })) })), jsx(AddDomainDialog, { open: isModalOpen, onOpenChange: setIsModalOpen, canAddPlatformDomain: canAddPlatformDomain, defaultMode: hasPlatformDomain ? 'app' : 'platform', projects: (_a = projectData === null || projectData === void 0 ? void 0 : projectData.projects) !== null && _a !== void 0 ? _a : [], isLoadingProjects: isLoadingProjects, isSubmitting: isCreating || isAttaching, onSubmitPlatformDomain: handleAddPlatformDomain, onSubmitAppDomain: handleAddAppDomain })] }));
231224
231296
  };
231225
231297
 
231226
231298
  const StudentMentorCreationContent = ({ platformKey, }) => {
@@ -275743,7 +275815,7 @@ function LocalModelRow({ name, size, logo, providerLabel, status, progress, disa
275743
275815
  style: { width: `${status === 'starting' ? 6 : Math.max(pct, 6)}%` } })), jsx("span", { "aria-hidden": "true", className: "relative z-10 flex h-8 w-8 flex-shrink-0 items-center justify-center overflow-hidden rounded-lg", children: jsx(LLMProviderLogo, { logo: logo, label: providerLabel !== null && providerLabel !== void 0 ? providerLabel : name, alt: "", className: cn({ grayscale: isBusyDisabled }) }) }), jsxs("span", { className: "relative z-10 flex min-w-0 flex-1 flex-col gap-1.5", children: [jsx("span", { className: "truncate text-sm font-medium text-[#646464] lowercase", children: name }), jsxs("span", { className: "flex min-w-0 items-center gap-1.5 text-sm text-gray-500", children: [jsx("span", { className: "flex-shrink-0 rounded bg-gray-100 px-2 py-0.5 text-sm font-medium text-gray-500", children: labels.onDevice }), status === 'not-installed' && (jsxs(Fragment$1, { children: [jsx("span", { "aria-hidden": "true", children: "\u00B7" }), jsx("span", { children: size }), jsx(Download, { className: "h-4 w-4", "aria-hidden": "true" })] })), status === 'starting' && (jsxs(Fragment$1, { children: [jsx("span", { "aria-hidden": "true", children: "\u00B7" }), jsx("span", { children: labels.starting })] })), status === 'downloading' && (jsxs(Fragment$1, { children: [jsx("span", { "aria-hidden": "true", children: "\u00B7" }), jsxs("span", { className: "tabular-nums group-hover:hidden group-focus-visible:hidden", children: [pct, "%"] }), jsxs("span", { className: "hidden items-center gap-1 text-blue-600 group-hover:flex group-focus-visible:flex", children: [jsx(X$2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), labels.cancel] })] })), status === 'selected' && (jsxs("span", { className: "flex items-center gap-1 text-blue-600", children: [jsx(Check, { className: "h-4 w-4", "aria-hidden": "true" }), labels.inUse] })), status === 'error' && (jsxs(Fragment$1, { children: [jsx("span", { "aria-hidden": "true", children: "\u00B7" }), jsx("span", { className: "truncate text-red-500", children: errorMessage || labels.downloadFailedRetry }), jsx(RotateCcw, { className: "h-4 w-4 flex-shrink-0 text-red-500", "aria-hidden": "true" })] }))] })] })] }));
275744
275816
  }
275745
275817
 
275746
- function LLMProviderModal({ isOpen, onClose, onSelect, llmProvider, isSelecting, mentorSettings, llms, labels: labelsProp, }) {
275818
+ function LLMProviderModal({ isOpen, onClose, onSelect, llmProvider, isSelecting, mentorSettings, llms, labels: labelsProp, cloudOnly = false, }) {
275747
275819
  var _a;
275748
275820
  const t = useT();
275749
275821
  const labels = React__default.useMemo(() => labelsProp !== null && labelsProp !== void 0 ? labelsProp : buildLLMTabLabels(t).providerModal, [labelsProp, t]);
@@ -275811,13 +275883,13 @@ function LLMProviderModal({ isOpen, onClose, onSelect, llmProvider, isSelecting,
275811
275883
  }, [searchQuery, llmProvider]);
275812
275884
  // Local models belonging to THIS provider (merged into the same list).
275813
275885
  const localModels = React__default.useMemo(() => {
275814
- if (!isAvailable)
275886
+ if (cloudOnly || !isAvailable)
275815
275887
  return [];
275816
275888
  const key = getProviderName(llmProvider.name);
275817
275889
  // Per-device catalog: Tauri iOS lists only models with phone-sized weights.
275818
275890
  return localModelsForThisDevice().filter((m) => getProviderName(m.provider) === key &&
275819
275891
  m.name.toLowerCase().includes(searchQuery.toLowerCase()));
275820
- }, [isAvailable, llmProvider.name, searchQuery]);
275892
+ }, [cloudOnly, isAvailable, llmProvider.name, searchQuery]);
275821
275893
  const switchLLMAllowed = canSwitchLLm(llmProvider);
275822
275894
  const switchProviderAllowed = canSwitchProvider(llms, llmProvider.name);
275823
275895
  // Only an actual pull counts as "busy". A status refresh ('checking') must NOT
@@ -275843,7 +275915,7 @@ function LLMProviderModal({ isOpen, onClose, onSelect, llmProvider, isSelecting,
275843
275915
  // is off.
275844
275916
  const isCloudActive = (llm) => (mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_name) === llm.llm_name &&
275845
275917
  (mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.llm_provider) === llmProvider.name &&
275846
- !localSel.enabled;
275918
+ (cloudOnly || !localSel.enabled);
275847
275919
  // Order the WHOLE list — cloud + local — available (usable now) first,
275848
275920
  // unavailable last, with the in-use model leading. "Available" for a cloud
275849
275921
  // model means the provider is switchable (has credentials + models); for a
@@ -275912,7 +275984,7 @@ function LLMProviderModal({ isOpen, onClose, onSelect, llmProvider, isSelecting,
275912
275984
  };
275913
275985
  const selectCloud = (llmName) => {
275914
275986
  // Picking a cloud model turns local mode off so routing uses the cloud LLM.
275915
- if (localSel.enabled) {
275987
+ if (!cloudOnly && localSel.enabled) {
275916
275988
  setLocalLLMEnabled(false);
275917
275989
  setLocalSel((prev) => ({ ...prev, enabled: false }));
275918
275990
  window.dispatchEvent(new Event(LOCAL_LLM_CHANGED_EVENT));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iblai/iblai-js",
3
- "version": "2.27.1",
3
+ "version": "2.28.0",
4
4
  "description": "Unified JavaScript SDK for IBL.ai — re-exports data-layer, web-containers, and web-utils under a single package",
5
5
  "type": "module",
6
6
  "engines": {
@@ -76,10 +76,10 @@
76
76
  "axios": "1.13.6",
77
77
  "dotenv": "16.6.1",
78
78
  "winston": "3.19.0",
79
- "@iblai/data-layer": "1.24.0",
80
- "@iblai/mcp": "1.23.1",
81
- "@iblai/web-containers": "1.33.1",
82
- "@iblai/web-utils": "2.6.0"
79
+ "@iblai/mcp": "1.24.0",
80
+ "@iblai/web-utils": "2.6.0",
81
+ "@iblai/data-layer": "1.25.0",
82
+ "@iblai/web-containers": "1.34.0"
83
83
  },
84
84
  "peerDependencies": {
85
85
  "@iblai/iblai-api": "4.166.0-ai",