@godxjp/ui 28.10.0 → 28.13.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.
Files changed (51) hide show
  1. package/agent/START-HERE.md +2 -2
  2. package/agent/components/AppLauncher.json +10 -0
  3. package/agent/components/Attachments.json +27 -1
  4. package/agent/components/BranchScopePicker.json +10 -0
  5. package/agent/components/Cascader.json +5 -0
  6. package/agent/components/Checkbox.json +6 -0
  7. package/agent/components/CredentialReveal.json +21 -0
  8. package/agent/components/InputOTP.json +35 -0
  9. package/agent/components/PermissionMatrix.json +5 -0
  10. package/agent/components/SearchInput.json +5 -0
  11. package/agent/components/ServiceLauncherCard.json +10 -3
  12. package/agent/components/ServiceRolePanel.json +5 -0
  13. package/agent/components/Switch.json +6 -0
  14. package/agent/components/Tabs.json +10 -0
  15. package/agent/components/TimeRangePicker.json +5 -0
  16. package/agent/components/Transfer.json +5 -0
  17. package/agent/components/Upload.json +5 -0
  18. package/agent/components-index.json +1 -1
  19. package/agent/components.json +170 -4
  20. package/agent/index.json +3 -3
  21. package/agent/llms.txt +4 -4
  22. package/agent/tokens.json +30 -25
  23. package/dist/components/data-display/service-launcher-card.d.ts +19 -0
  24. package/dist/components/data-display/service-launcher-card.js +14 -1
  25. package/dist/components/data-entry/attachments.js +77 -33
  26. package/dist/components/ui/avatar.d.ts +1 -18
  27. package/dist/components/ui/avatar.js +1 -36
  28. package/dist/contracts/measurement.json +1 -1
  29. package/dist/i18n/messages/en.json +0 -530
  30. package/dist/i18n/messages/ja.json +0 -524
  31. package/dist/i18n/messages/vi.json +0 -524
  32. package/dist/lib/image-loading-status.d.ts +25 -0
  33. package/dist/lib/image-loading-status.js +41 -0
  34. package/dist/props/registry.d.ts +5 -1
  35. package/dist/props/registry.js +12 -1
  36. package/dist/styles/card-layout.css +6 -0
  37. package/dist/styles/control.css +2 -2
  38. package/dist/styles/data-entry-layout.css +245 -2
  39. package/dist/tokens/components/attachments.css +18 -9
  40. package/dist/tokens/components/segmented.css +2 -2
  41. package/docs/DESIGN-AUTHORITY.md +51 -0
  42. package/docs/assets/service-mark-rose.svg +6 -0
  43. package/docs/assets/service-mark-teal.svg +5 -0
  44. package/docs/data-display/service-launcher-card.tsx +232 -92
  45. package/docs/i18n/messages/en.json +699 -0
  46. package/docs/i18n/messages/ja.json +693 -0
  47. package/docs/i18n/messages/vi.json +693 -0
  48. package/docs/showcase/marketing-page.tsx +3 -2
  49. package/docs/showcase/table-pagination.tsx +18 -7
  50. package/docs/showcase/theme-customization.tsx +2 -1
  51. package/package.json +4 -3
@@ -441,6 +441,11 @@
441
441
  "name": "allowEmpty",
442
442
  "type": "[boolean,boolean]"
443
443
  },
444
+ {
445
+ "description": "A PAIR, one per endpoint — TimePicker's single-string `placeholder` is omitted from this type on purpose, because a range has two empty fields and one string would label both of them the same. Route both through t().",
446
+ "name": "placeholder",
447
+ "type": "[string,string]"
448
+ },
444
449
  {
445
450
  "description": "Native names are name_from and name_to.",
446
451
  "name": "name",
@@ -3796,17 +3801,22 @@
3796
3801
  ]
3797
3802
  },
3798
3803
  {
3799
- "example": "import { Clock } from \"lucide-react\";\nimport {\n ServiceCatalogCta,\n ServiceLauncherCard,\n ServiceLauncherCardSkeleton,\n} from \"@godxjp/ui/data-display\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { ResponsiveGrid } from \"@godxjp/ui/layout\";\n\n// ResponsiveGrid owns the canonical 3 → 2 → 1 ladder; the page writes no tracks.\n<ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }}>\n {loading\n ? services.map((s) => <ServiceLauncherCardSkeleton key={s.id} label={t(\"loadingService\")} />)\n : services.map((s) => (\n <ServiceLauncherCard\n key={s.id}\n icon={Clock}\n title={s.name}\n statusLabel={s.accessLabel}\n statusTone={s.accessTone}\n description={s.description}\n metadata={s.hostnameAndPlan}\n disabledReason={s.blockedReason}\n action={\n <Button asChild={s.canLaunch} disabled={!s.canLaunch}>\n {s.canLaunch ? <a href={s.launchUrl}>{t(\"launch\")}</a> : t(\"launch\")}\n </Button>\n }\n />\n ))}\n <ServiceCatalogCta\n title={t(\"addFromCatalog\")}\n action={<Button variant=\"outline\">{t(\"viewCatalog\")}</Button>}\n />\n</ResponsiveGrid>",
3804
+ "example": "import { Clock } from \"lucide-react\";\nimport {\n ServiceCatalogCta,\n ServiceLauncherCard,\n ServiceLauncherCardSkeleton,\n} from \"@godxjp/ui/data-display\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { ResponsiveGrid } from \"@godxjp/ui/layout\";\n\n// ResponsiveGrid owns the canonical 3 → 2 → 1 ladder; the page writes no tracks.\n<ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }}>\n {loading\n ? services.map((s) => <ServiceLauncherCardSkeleton key={s.id} label={t(\"loadingService\")} />)\n : services.map((s) => (\n <ServiceLauncherCard\n key={s.id}\n icon={Clock}\n // The uploaded mark when there is one; the tile falls back to `icon` when there is not,\n // and again if the file behind the URL has gone away.\n logo={s.logoUrl ?? undefined}\n title={s.name}\n statusLabel={s.accessLabel}\n statusTone={s.accessTone}\n description={s.description}\n metadata={s.hostnameAndPlan}\n disabledReason={s.blockedReason}\n action={\n <Button asChild={s.canLaunch} disabled={!s.canLaunch}>\n {s.canLaunch ? <a href={s.launchUrl}>{t(\"launch\")}</a> : t(\"launch\")}\n </Button>\n }\n />\n ))}\n <ServiceCatalogCta\n title={t(\"addFromCatalog\")}\n action={<Button variant=\"outline\">{t(\"viewCatalog\")}</Button>}\n />\n</ResponsiveGrid>",
3800
3805
  "group": "data-display",
3801
3806
  "importPath": "@godxjp/ui/data-display",
3802
3807
  "name": "ServiceLauncherCard",
3803
3808
  "props": [
3804
3809
  {
3805
- "description": "Decorative service glyph rendered in the canonical semantic icon surface.",
3810
+ "description": "Decorative glyph for the KIND of service, rendered in the canonical semantic icon surface. Stays required because it is also what `logo` falls back to when an upload is missing or broken.",
3806
3811
  "name": "icon",
3807
3812
  "required": true,
3808
3813
  "type": "LucideIcon"
3809
3814
  },
3815
+ {
3816
+ "description": "URL of the service's OWN uploaded mark (the PNG/WebP an administrator uploaded). When it loads it replaces `icon` in the medallion, in the glyph's exact box (--card-service-launcher-icon-glyph-size, object-fit: contain). It is a fallback chain, not a switch: the tile renders `icon` while the URL is in flight and KEEPS rendering `icon` if the URL 404s or is empty — a broken logo never reaches the DOM as an <img>. Decorative (alt=\"\", medallion aria-hidden), so there is no logoAlt prop: the service name is already beside it.",
3817
+ "name": "logo",
3818
+ "type": "string"
3819
+ },
3810
3820
  {
3811
3821
  "description": "Real downstream service display name.",
3812
3822
  "name": "title",
@@ -3865,13 +3875,15 @@
3865
3875
  "subParts": [
3866
3876
  "ServiceLauncherCardSkeleton"
3867
3877
  ],
3868
- "tagline": "Token-owned downstream-service launcher tile with semantic icon, status, metadata, action, disabled reason, matching skeleton, and companion catalog CTA.",
3878
+ "tagline": "Token-owned downstream-service launcher tile with an uploaded service logo (falling back to a semantic icon), status, metadata, action, disabled reason, matching skeleton, and companion catalog CTA.",
3869
3879
  "usage": [
3870
3880
  "DO provide status, hostname, plan, access state and action from the product's real API contract. ServiceLauncherCard deliberately performs no entitlement or URL inference — it has no href/entitlement/available prop at all.",
3871
3881
  "DO own the layout with ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }} — the canonical 3→2→1 launcher grid. ResponsiveGrid queries its OWN container (40/48/64rem), so never hand-write grid-template-columns or a media query in the page. The shorthand columns={3} also works but widens to 2 columns earlier (40rem).",
3872
3882
  "DO render ServiceLauncherCard directly as a grid child; it already owns its Card shell, its 36px medallion (--control-height-lg tier) and the canonical internal rhythm.",
3873
3883
  "DO replace it with ServiceLauncherCardSkeleton while loading (it carries a required `label` and aria-busy, and deliberately opens no live region). Use ServiceCatalogCta as the peer tile only when a real catalog/add route exists.",
3874
3884
  "DO keep `metadata` to machine identifiers (hostname · plan) — it is the only mono line. Sentences belong in `description` / `disabledReason`.",
3885
+ "DO pass BOTH `logo` and `icon` when a service may have an uploaded mark: `logo={s.logoUrl ?? undefined}` with `icon` as the kind glyph. `icon` is not optional and must not be treated as redundant — it is the tile a freshly created service, and a service whose logo file has been deleted, actually shows.",
3886
+ "DON'T wrap an <img> in a component to squeeze it through `icon`, and DON'T style the medallion to fit a picture. `logo` sizes the image to the glyph's own token, so a grid mixing logo tiles and glyph tiles keeps one optical weight.",
3875
3887
  "DON'T recreate launcher geometry with page-local CSS, utility padding, grid tracks, or a hand-built Card hierarchy. Retune it with the --card-service-launcher-* tokens instead.",
3876
3888
  "DON'T show LIVE, a hostname, subscribed plan, or launch action merely because a service is active in the global catalog."
3877
3889
  ],
@@ -4359,12 +4371,23 @@
4359
4371
  "name": "onAcknowledge",
4360
4372
  "type": "() => void"
4361
4373
  },
4374
+ {
4375
+ "description": "Copy on the button `onAcknowledge` creates. Defaults to a localized \"I've saved it\" — override it when the confirmation claims something more specific than having read the secret (\"保管しました\", \"Stored in 1Password\"). Consumer-owned wording: route it through t().",
4376
+ "name": "acknowledgeLabel",
4377
+ "type": "React.ReactNode"
4378
+ },
4362
4379
  {
4363
4380
  "defaultValue": "false",
4364
4381
  "description": "Offer a download-as-file button.",
4365
4382
  "name": "downloadable",
4366
4383
  "type": "boolean"
4367
4384
  },
4385
+ {
4386
+ "defaultValue": "\"credential.txt\"",
4387
+ "description": "Name of the file `downloadable` writes. The default is deliberately anonymous; set it when the user will hold several at once (`api-key-prod.txt`) so the saved files are still telling apart in a downloads folder.",
4388
+ "name": "downloadFileName",
4389
+ "type": "string"
4390
+ },
4368
4391
  {
4369
4392
  "defaultValue": "\"md\"",
4370
4393
  "description": "Action button size tier.",
@@ -4376,6 +4399,16 @@
4376
4399
  "description": "Caution banner severity.",
4377
4400
  "name": "tone",
4378
4401
  "type": "\"warning\" | \"destructive\" | \"info\""
4402
+ },
4403
+ {
4404
+ "description": "DOM id on the root. Useful here because the surface is usually inside a Dialog: it is what a `aria-describedby` on the dialog, or a deep link back to the issued credential, can point at.",
4405
+ "name": "id",
4406
+ "type": "string"
4407
+ },
4408
+ {
4409
+ "description": "Accessible name for the credential region when `label` is not set or is a non-string node. `label` is the VISIBLE caption and already names the region when it is a plain string, so reach for this only when the caption is rich content or when the surrounding dialog title is the only thing saying which secret this is.",
4410
+ "name": "aria-label",
4411
+ "type": "string"
4379
4412
  }
4380
4413
  ],
4381
4414
  "related": [
@@ -5828,6 +5861,11 @@
5828
5861
  "description": "Disable search input and clearing.",
5829
5862
  "name": "disabled",
5830
5863
  "type": "boolean"
5864
+ },
5865
+ {
5866
+ "description": "Class on the `<input>` itself. SearchInput renders a WRAPPER (label, icon, clear button, input), so `className` lands on that wrapper and never reaches the field — this is the second handle, for the case where the field and its chrome need different treatment. Layout and colour still belong to tokens; use it for the rare geometry a token cannot reach.",
5867
+ "name": "inputClassName",
5868
+ "type": "string"
5831
5869
  }
5832
5870
  ],
5833
5871
  "related": [
@@ -6303,6 +6341,12 @@
6303
6341
  "name": "id",
6304
6342
  "type": "string"
6305
6343
  },
6344
+ {
6345
+ "defaultValue": "false",
6346
+ "description": "ANNOUNCES the requirement; it does not enforce it. react-aria's Switch omits `isRequired`, and a switch is never the target of native constraint validation in this library, so this writes `aria-required=\"true\"` onto the real input and stops there. The form layer (FormField / your schema) still owns whether an unflipped switch blocks submit — pairing this with nothing that validates is how a screen reader ends up promising a check the form never makes.",
6347
+ "name": "required",
6348
+ "type": "boolean"
6349
+ },
6306
6350
  {
6307
6351
  "defaultValue": "false",
6308
6352
  "description": "Disable the toggle.",
@@ -6535,6 +6579,12 @@
6535
6579
  "name": "id",
6536
6580
  "type": "string"
6537
6581
  },
6582
+ {
6583
+ "defaultValue": "false",
6584
+ "description": "Keeps its HTML spelling here and becomes react-aria's `isRequired`, so unlike Switch — where the same prop only ANNOUNCES the requirement — this is real constraint validation on the underlying input: an unchecked box blocks native form submission. The consent checkbox is the case it exists for. It does not render an asterisk; the required MARK belongs to FormField's label.",
6585
+ "name": "required",
6586
+ "type": "boolean"
6587
+ },
6538
6588
  {
6539
6589
  "description": "antd `<Checkbox>label</Checkbox>` — the INLINE label: box first, label at inline-end on the same line, the same markup as a `Checkbox.Group` option. A real `<label for>`: clicking the text toggles the box and the text is its accessible name. `className` then styles the labelled row.",
6540
6590
  "name": "children",
@@ -7675,6 +7725,16 @@
7675
7725
  "description": "Ant Design `onTabScroll`, fired whenever the trigger strip's own scrollport moves — a swipe, a wheel, or the component re-pinning the active trigger (antd reports its own re-pins too). LOGICAL VALUES instead of antd's `left | right | top | bottom`: two of those four are just the other axis of the same event, and upstream's pair is read off the sign of an inner transform, so in an RTL strip its `left` means the opposite of what it means in LTR. `start`/`end` say the same thing on whichever axis and in whichever direction the strip is written. Only fires for the `items` API, which is the path that owns the strip element.",
7676
7726
  "name": "onTabScroll",
7677
7727
  "type": "(info: { direction: \"start\" | \"end\" }) => void"
7728
+ },
7729
+ {
7730
+ "description": "Class on the TRIGGER STRIP (`TabsList`) under the `items` API — the handle that composing the tree manually gives you as `<TabsList className>`. `className` reaches only the root, which holds the strip AND the panels, so anything meant for the bar alone belongs here. Almost always unnecessary: placement, size, centring and the card rail are props and `--tabs-*` tokens.",
7731
+ "name": "listClassName",
7732
+ "type": "string"
7733
+ },
7734
+ {
7735
+ "description": "Class on EVERY panel (`TabsContent`) under the `items` API. It is written so it can WIN: the joined card body travels to CSS as `data-bodied` on the root rather than as a class, precisely so a consumer class on the panel is not fighting a utility the component already claimed (gh#762). Reach for the `bodied` prop and the `--tabs-panel-*` tokens first — this is for the geometry no token exposes.",
7736
+ "name": "contentClassName",
7737
+ "type": "string"
7678
7738
  }
7679
7739
  ],
7680
7740
  "related": [
@@ -8734,6 +8794,11 @@
8734
8794
  "description": "Search query change (antd `showSearch.onSearch`).",
8735
8795
  "name": "onSearchChange",
8736
8796
  "type": "(query: string) => void"
8797
+ },
8798
+ {
8799
+ "description": "Accessible name for the COMBOBOX TRIGGER — the element a keyboard user lands on, not the panel. Inside a FormField it is injected for you and you do not pass it; on a bare Cascader (a toolbar scope filter, a compact drilldown with no label row) it is the only name the control has, and cardinal rule 227 requires one. Route it through t(). The rest of the field-a11y contract — aria-labelledby / describedby / errormessage / invalid / required — is accepted on every form-capable component here and is FormField's to wire.",
8800
+ "name": "aria-label",
8801
+ "type": "string"
8737
8802
  }
8738
8803
  ],
8739
8804
  "related": [
@@ -9094,6 +9159,11 @@
9094
9159
  "name": "className",
9095
9160
  "type": "string"
9096
9161
  },
9162
+ {
9163
+ "description": "Lands on the `role=\"group\"` shuttle container, not on any one input — a two-pane shuttle has no single labelable control, so this is what a FormField label points at. FormField injects it; pass it yourself only for a bare Transfer.",
9164
+ "name": "id",
9165
+ "type": "string"
9166
+ },
9097
9167
  {
9098
9168
  "description": "Controlled selection state as a tuple: index 0 = keys checked in the source panel, index 1 = keys checked in the target panel. Omit to use internal (uncontrolled) selection state. Must be paired with `onSelectChange` when provided.",
9099
9169
  "name": "selectedKeys",
@@ -9208,6 +9278,11 @@
9208
9278
  "name": "className",
9209
9279
  "type": "string"
9210
9280
  },
9281
+ {
9282
+ "description": "Lands on the native `<input type=\"file\">`, NOT on the wrapper — the hidden input is the semantic focus target, so this is what makes a `<label htmlFor>` (or FormField, which injects it) actually focus the picker. Putting it on the visible dropzone instead is the usual reason a label click does nothing.",
9283
+ "name": "id",
9284
+ "type": "string"
9285
+ },
9211
9286
  {
9212
9287
  "description": "Custom button label for variant='button'. Falls back to the i18n 'Upload file' string.",
9213
9288
  "name": "children",
@@ -12075,6 +12150,41 @@
12075
12150
  "description": "Main-axis alignment of the whole code row (groups + separators) inside the field. `center` is the canonical auth challenge. Before this existed, every consumer wrapped the OTP in their own flex-centring div — do not. A service that wants all code fields centred sets `--otp-container-align` once instead.",
12076
12151
  "name": "align",
12077
12152
  "type": "\"start\" | \"center\" | \"end\""
12153
+ },
12154
+ {
12155
+ "description": "Fires ONCE the last slot fills, whether the user typed it or pasted the whole code. This is the auto-submit hook: a 2FA challenge with a visible submit button is a step nobody wants, and the alternative — watching `value.length === maxLength` in an effect — re-fires on every re-render. Keep the submit button anyway for the paste-then-correct case.",
12156
+ "name": "onComplete",
12157
+ "type": "(value: string) => void"
12158
+ },
12159
+ {
12160
+ "description": "Rewrites CLIPBOARD text before it reaches the field. Distinct from `formatter`, which normalises every value: this one only sees a paste, which is where the junk arrives — `\"123 456\"`, `\"code: 123456\"`, a copied SMS line. Note the order the field applies them: `pattern` is matched against the RAW keystroke first, so a pattern must accept what a user actually types, not only what these two produce.",
12161
+ "name": "pasteTransformer",
12162
+ "type": "(pasted: string) => string"
12163
+ },
12164
+ {
12165
+ "description": "Class on the ROW container `input-otp` renders (the slots' flex parent), which `className` cannot reach — `className` lands on the hidden input, because that is the real field. Prefer `align` and the `--otp-*` tokens; this is the vendored escape hatch underneath them.",
12166
+ "name": "containerClassName",
12167
+ "type": "string"
12168
+ },
12169
+ {
12170
+ "description": "`input-otp`'s answer to the 1Password / LastPass badge that browsers float over a code field and that covers the last slot. `increase-width` (its default) reserves room so the badge sits beside the row; `none` turns the accommodation off, which is what a row already centred by `align` usually wants. Pure layout — it changes no value and no keyboard behaviour.",
12171
+ "name": "pushPasswordManagerStrategy",
12172
+ "type": "\"increase-width\" | \"none\""
12173
+ },
12174
+ {
12175
+ "description": "The `<noscript>` stylesheet `input-otp` injects so the slots are still visible with JS disabled. Pass `null` to suppress it — the one real reason being a CSP that forbids inline styles and that `nonce` cannot satisfy. Leave it alone otherwise.",
12176
+ "name": "noScriptCSSFallback",
12177
+ "type": "string | null"
12178
+ },
12179
+ {
12180
+ "description": "CSP nonce stamped on the stylesheet `input-otp` injects. Required only under a `style-src 'nonce-…'` policy, where the field otherwise renders unstyled and the console reports a blocked inline style. Pass the same nonce the document was served with.",
12181
+ "name": "nonce",
12182
+ "type": "string"
12183
+ },
12184
+ {
12185
+ "description": "`input-otp`'s headless mode: you draw the entire row from the slot state instead of composing InputOTPGroup / InputOTPSlot. It is mutually exclusive with `children` — the vendor types the two as a union and this component keeps that union. Reaching for it means giving up the slot styling, the group outline and the separator this package owns, so it is the last resort, not a customisation point.",
12186
+ "name": "render",
12187
+ "type": "(props: InputOTPRenderProps) => React.ReactNode"
12078
12188
  }
12079
12189
  ],
12080
12190
  "related": [
@@ -13842,6 +13952,16 @@
13842
13952
  "name": "appearance",
13843
13953
  "type": "\"bar\" | \"icon\""
13844
13954
  },
13955
+ {
13956
+ "description": "Which way the panel opens. DERIVED from `appearance` when unset — `bottom` in a bar (the panel drops below the trigger, the only direction that does not cover the bar itself), `right` otherwise, because a rail is vertical and its panel goes beside it. State it when the chrome can be RE-DOCKED: `appearance` says the trigger is NOT in a bar, and it cannot say which way is out — a rail pinned to the top edge is not a bar and still opens downward. Measured without it, an embedded bar trigger at (2,50) put its panel at (12,90), lying over the host application's sidebar. Not used by `responsive=\"fullscreen\"`, which has no side.",
13957
+ "name": "side",
13958
+ "type": "\"top\" | \"right\" | \"bottom\" | \"left\""
13959
+ },
13960
+ {
13961
+ "description": "Where the panel sits along the `side` edge — the cross-axis half of the same decision, and derived the same way: `end` in a bar (the Workspace shape, flush with the bar's end), `start` otherwise (aligned to the rail trigger's own start). State it alongside `side` when you state either.",
13962
+ "name": "align",
13963
+ "type": "\"start\" | \"center\" | \"end\""
13964
+ },
13845
13965
  {
13846
13966
  "description": "Controlled open state.",
13847
13967
  "name": "open",
@@ -14030,6 +14150,11 @@
14030
14150
  "description": "Accessible table name (localized default).",
14031
14151
  "name": "label",
14032
14152
  "type": "string"
14153
+ },
14154
+ {
14155
+ "description": "DOM id on the grid root. Worth setting on a permissions page that also renders a summary or a legend elsewhere: it is the anchor those can point at, and the stable handle for an E2E selector that must not depend on the localized `label`.",
14156
+ "name": "id",
14157
+ "type": "string"
14033
14158
  }
14034
14159
  ],
14035
14160
  "related": [
@@ -14120,6 +14245,16 @@
14120
14245
  "description": "Override the localized radio labels (e.g. domain wording like 全店舗).",
14121
14246
  "name": "allLabel / selectedLabel",
14122
14247
  "type": "ReactNode"
14248
+ },
14249
+ {
14250
+ "description": "Native form name, forwarded to the MODE radio group — the all / selected choice is what submits under it. The checked branch ids are not native fields; they live in the single `{ mode, branchIds }` value and are yours to serialise.",
14251
+ "name": "name",
14252
+ "type": "string"
14253
+ },
14254
+ {
14255
+ "description": "DOM id on the picker root, and the SEED for the ids beneath it — the validation message is `${id}-error`, which is what `aria-errormessage` points at. Left out, a `useId`-based id is generated, so the association still holds; set it when a server-rendered page needs those ids to be stable.",
14256
+ "name": "id",
14257
+ "type": "string"
14123
14258
  }
14124
14259
  ],
14125
14260
  "related": [
@@ -14202,6 +14337,11 @@
14202
14337
  "description": "Forwarded to MasterDetail (localized region labels by default). Never re-derive tracks or breakpoints in the app.",
14203
14338
  "name": "railWidth / masterViewport / collapseBelow / masterLabel / detailLabel",
14204
14339
  "type": "MasterDetail geometry + region labels"
14340
+ },
14341
+ {
14342
+ "description": "DOM id on the panel root — the two-region MasterDetail wrapper, not the rail or the detail. It is the handle for a deep link onto the roles panel of a settings page, and for an E2E selector that must survive `masterLabel` being localized.",
14343
+ "name": "id",
14344
+ "type": "string"
14205
14345
  }
14206
14346
  ],
14207
14347
  "related": [
@@ -15110,6 +15250,31 @@
15110
15250
  "description": "Child mode: visible trigger; upload runs through a hidden input beside it.",
15111
15251
  "name": "children",
15112
15252
  "type": "ReactElement"
15253
+ },
15254
+ {
15255
+ "description": "antd Upload `onRemove`, narrowed to the attachment row. RETURNING `false` (or a promise of it) VETOES the removal and the card stays — anything else, including `undefined`, lets it go. That is how you gate a removal behind a confirm dialog without owning `items` yourself. It is awaited, so an async guard works.",
15256
+ "name": "onRemove",
15257
+ "type": "(item: AttachmentsItemProp) => boolean | void | Promise<boolean | void>"
15258
+ },
15259
+ {
15260
+ "description": "Ant Design X `classNames` — per-part classes (root, list, card, file, upload, placeholder). DECLARED AND FORWARDED, and the only semantic part map in this package: docs/DESIGN-AUTHORITY.md rules that antd's `classNames`/`styles` maps are NOT adopted because this library answers that layer with tokens (cardinal rule #45), and Attachments is the one component that carries them anyway. Recorded there as a contradiction, not a pattern — do not copy it onto another component, and retune through `--attachments-*` instead.",
15261
+ "name": "classNames",
15262
+ "type": "Partial<Record<AttachmentsSemanticProp, string>>"
15263
+ },
15264
+ {
15265
+ "description": "Ant Design X `styles` — the same part map as `classNames`, as inline styles, and under the same standing ruling against it. Inline styles beat every stylesheet rule, so this is the one handle in the package that can take a part off the design system entirely. The `--attachments-*` tokens are the supported route.",
15266
+ "name": "styles",
15267
+ "type": "Partial<Record<AttachmentsSemanticProp, React.CSSProperties>>"
15268
+ },
15269
+ {
15270
+ "description": "Ant Design X `rootClassName` — the outermost node. It is not a duplicate of `className`: in the full-screen-drop mode (`getDropContainer`) the outermost node is the OVERLAY rather than the inline tray, which is why antd separates the two. Both are applied.",
15271
+ "name": "rootClassName",
15272
+ "type": "string"
15273
+ },
15274
+ {
15275
+ "description": "ACCEPTED AND INERT. Ant Design X forwards it to its own Image preview; this package has no Image primitive yet, so the prop exists only so an Ant X call site type-checks, and passing it changes nothing on screen. Do not reach for it expecting a preview knob.",
15276
+ "name": "imageProps",
15277
+ "type": "Record<string, unknown>"
15113
15278
  }
15114
15279
  ],
15115
15280
  "related": [
@@ -15127,7 +15292,8 @@
15127
15292
  "usage": [
15128
15293
  "DO keep antd field names on each item (`thumbUrl`, `originFileObj`, `uid`) — an Ant X call site should compile unchanged.",
15129
15294
  "DO use `ref.select({ accept, multiple })` to open the picker programmatically (Ant X 2.0).",
15130
- "DON'T expect `styles`/`classNames` from Ant X — retune through `--attachments-*` tokens."
15295
+ "Ant X's `classNames` / `styles` / `rootClassName` ARE declared and forwarded, although docs/DESIGN-AUTHORITY.md rules that antd's semantic part maps are not adopted here. The older note in this slot said not to expect them at all; the type has never agreed with it, so an agent reading only the catalog was told the opposite of what autocomplete offered. Retune through the `--attachments-*` tokens, and treat the maps as a recorded contradiction on this one component rather than a pattern to reuse.",
15296
+ "The card is a FIXED box — 268x68 (Ant X's own), from `--attachments-card-size` (inline) and `--attachments-card-block-size`. The block size is also the `+` tile's square and the `overflow=\"scrollY\"` one-row viewport, so retune it once and all three follow. The file input is `sr-only`: never style it visible."
15131
15297
  ],
15132
15298
  "useCases": [
15133
15299
  "The attachment tray above a ChatComposer in an assistant surface.",
package/agent/index.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "components": 170,
5
5
  "patterns": 19,
6
6
  "rules": 47,
7
- "tokens": 1684,
7
+ "tokens": 1685,
8
8
  "vocabulary": 14
9
9
  },
10
10
  "files": [
@@ -48,9 +48,9 @@
48
48
  "note": "Pin to the tag that matches the @godxjp/ui version you installed. A catalog newer than your package describes props you do not have; older, and it hides props you do.",
49
49
  "read": {
50
50
  "live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
51
- "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v28.10.0/agent/index.json"
51
+ "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v28.13.0/agent/index.json"
52
52
  },
53
53
  "source": "mcp/src/data — the same data @godxjp/ui-mcp serves",
54
54
  "start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
55
- "version": "28.10.0"
55
+ "version": "28.13.0"
56
56
  }
package/agent/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @godxjp/ui
2
2
 
3
- > A Japanese-enterprise React design system: 170 components, 1684 design tokens,
4
- > 47 cardinal rules. This file is the entry point for AI agents. Catalog version 28.10.0.
3
+ > A Japanese-enterprise React design system: 170 components, 1685 design tokens,
4
+ > 47 cardinal rules. This file is the entry point for AI agents. Catalog version 28.13.0.
5
5
 
6
6
  If your client can run a process, do not read these files — run the MCP server instead
7
- (`npx @godxjp/ui-mcp@28.10.0`). It is searchable and version-locked. These files exist for agents
7
+ (`npx @godxjp/ui-mcp@28.13.0`). It is searchable and version-locked. These files exist for agents
8
8
  that can only fetch URLs.
9
9
 
10
10
  ## Start
@@ -26,7 +26,7 @@ that can only fetch URLs.
26
26
  ## Pinning
27
27
 
28
28
  Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
29
- the tag: `.../godx-jp/godxjp-ui/v28.10.0/agent/...`. A catalog that does not match the installed
29
+ the tag: `.../godx-jp/godxjp-ui/v28.13.0/agent/...`. A catalog that does not match the installed
30
30
  package describes props that are absent, or hides props that are present, and says nothing either way.
31
31
 
32
32
  Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.
package/agent/tokens.json CHANGED
@@ -155,62 +155,67 @@
155
155
  "value": "var(--space-inline-lg)"
156
156
  },
157
157
  {
158
- "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css.",
158
+ "description": "Gap between cards in the list. Ant X: `marginXS` = 8px.",
159
159
  "name": "--attachments-gap",
160
160
  "value": "var(--space-2)"
161
161
  },
162
162
  {
163
- "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css.",
163
+ "description": "THE CARD BOX. Ant X's file card is a fixed 268x68 chip: wide enough for a name plus one line * of description, short enough that a row of them reads as a tray and not as a list. Both axes * are fixed there and fixed here — the block size is what the `+` tile and the `scrollY` * one-row viewport are measured against, so it has three readers, not one. Ant X `width: 268`",
164
164
  "name": "--attachments-card-size",
165
- "value": "var(--upload-tile-size)"
165
+ "value": "16.75rem"
166
166
  },
167
167
  {
168
- "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css.",
168
+ "description": "Ant X `cardHeight` = 14 * 1.5714 * 2 + 12 + 12 = 68",
169
+ "name": "--attachments-card-block-size",
170
+ "value": "4.25rem"
171
+ },
172
+ {
173
+ "description": "Ant X `token.borderRadius` = 6 ≡ --radius",
169
174
  "name": "--attachments-card-radius",
170
- "value": "var(--upload-tile-radius)"
175
+ "value": "var(--radius-lg)"
171
176
  },
172
177
  {
173
- "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css.",
178
+ "description": "Ant X `marginSM` — glyph to name",
174
179
  "name": "--attachments-card-gap",
175
- "value": "var(--upload-tile-space-gap)"
180
+ "value": "var(--space-3)"
176
181
  },
177
182
  {
178
- "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css.",
183
+ "description": "THE PLACEHOLDER — the whole control while the list is empty, and the drop target. Ant X `token.padding` = 16",
179
184
  "name": "--attachments-placeholder-space-inset",
180
- "value": "var(--upload-dropzone-space-inset)"
185
+ "value": "var(--space-4)"
181
186
  },
182
187
  {
183
- "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css.",
188
+ "description": "Ant X `token.borderRadius` = 6 ≡ --radius",
184
189
  "name": "--attachments-placeholder-radius",
185
- "value": "var(--upload-dropzone-radius)"
190
+ "value": "var( --radius-lg )"
186
191
  },
187
192
  {
188
- "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css.",
193
+ "description": "Ant X's placeholder mark is `fontSizeHeading2` = 30px, which is not a step of this repo's * nine-value icon scale (gh#326) and whose neighbours are 24 and 36. It stays on the step the * frozen table in `src/tokens/__tests__/icon-size-scale.test.ts` already records for it — the * same mark Upload's dropzone draws, which is the surface a reader meets this one beside.",
189
194
  "name": "--attachments-placeholder-icon-size",
190
- "value": "var(--upload-dropzone-icon-size)"
195
+ "value": "var(--icon-size-3xl)"
191
196
  },
192
197
  {
193
- "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css.",
198
+ "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css. * * EVERY KNOB BELOW IS READ BY A RULE IN `src/styles/data-entry-layout.css` (gh#855). It was not * always so: all twelve of these resolved, were generated into the MCP catalog, were described to * every consumer — and nothing anywhere read one of them, because the component had no stylesheet * at all. The defaults are Ant X's own numbers, taken from `@ant-design/x@2.9.0` * (`es/attachments/style/index.js` + `es/file-card/style/index.js`) rather than invented, and * mapped onto this repo's scales; `docs/DESIGN-AUTHORITY.md` carries the divergences.",
194
199
  "name": "--attachments-placeholder-title-font-size",
195
- "value": "var(--font-size-sm, var(--font-size-base))"
200
+ "value": "var(--font-size-base)"
196
201
  },
197
202
  {
198
- "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css.",
203
+ "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css. * * EVERY KNOB BELOW IS READ BY A RULE IN `src/styles/data-entry-layout.css` (gh#855). It was not * always so: all twelve of these resolved, were generated into the MCP catalog, were described to * every consumer — and nothing anywhere read one of them, because the component had no stylesheet * at all. The defaults are Ant X's own numbers, taken from `@ant-design/x@2.9.0` * (`es/attachments/style/index.js` + `es/file-card/style/index.js`) rather than invented, and * mapped onto this repo's scales; `docs/DESIGN-AUTHORITY.md` carries the divergences.",
199
204
  "name": "--attachments-placeholder-description-font-size",
200
- "value": "var(--upload-dropzone-meta-font-size)"
205
+ "value": "var(--font-size-sm, var(--font-size-base))"
201
206
  },
202
207
  {
203
- "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css.",
208
+ "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css. * * EVERY KNOB BELOW IS READ BY A RULE IN `src/styles/data-entry-layout.css` (gh#855). It was not * always so: all twelve of these resolved, were generated into the MCP catalog, were described to * every consumer — and nothing anywhere read one of them, because the component had no stylesheet * at all. The defaults are Ant X's own numbers, taken from `@ant-design/x@2.9.0` * (`es/attachments/style/index.js` + `es/file-card/style/index.js`) rather than invented, and * mapped onto this repo's scales; `docs/DESIGN-AUTHORITY.md` carries the divergences.",
204
209
  "name": "--attachments-placeholder-gap",
205
- "value": "var(--space-2)"
210
+ "value": "var(--space-1)"
206
211
  },
207
212
  {
208
- "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css.",
213
+ "description": "The scrim the drop layer paints over the whole control while a file is dragged across it. * Role-mirror knob, so `initial` and the role resolved at the CALL SITE (docs/TOKENS.md) — * a `[data-tenant]`/`.dark` scope that re-seeds `--background` must reach this. Documented * default = `color-mix(in srgb, hsl(var(--background)) 85%, transparent)`, which is Ant X's * `colorBgPlaceholderHover` (`colorBgContainer` at alpha .85).",
209
214
  "name": "--attachments-drop-overlay-background",
210
215
  "value": "initial"
211
216
  },
212
217
  {
213
- "description": "Attachments — Ant Design X chat-surface file collection. * * Ant X retunes this through `styles={{ list, placeholder, upload, … }}`. Here every length and * tint is a named knob (rules #44/#45) so a service sets the rail once in theme.css.",
218
+ "description": "The ✕ at the end of a card's row. Ant X draws it at `fontSizeLG` = 16; it stays at the 14px * step the frozen icon table records, which is also `--upload-remove-icon-size`. Two remove * affordances in one group drawing two different sizes is the defect, not the saving.",
214
219
  "name": "--attachments-remove-icon-size",
215
220
  "value": "var(--upload-remove-icon-size)"
216
221
  },
@@ -5885,9 +5890,9 @@
5885
5890
  "value": "calc(var(--space-1) / 2)"
5886
5891
  },
5887
5892
  {
5888
- "description": "track background — the recessed ground a control group sits in.",
5893
+ "description": "track background — the recessed ground a control group sits in. default = var(--muted), resolved at the call site",
5889
5894
  "name": "--segmented-track-background",
5890
- "value": "var(--muted)"
5895
+ "value": "initial"
5891
5896
  },
5892
5897
  {
5893
5898
  "description": "track radius = the base radius; item radius = one step DOWN the radius scale, not * `radius − trackPadding` (the two coincide only because the source scale happens to step by 2). * This scale is φ-spaced, so \"one step down\" from --radius is --radius-md (6px → 3.71px, against * the source's 6 → 4); --radius-sm is two steps and reads visibly squarer. The same pair is * already what TabsList (`rounded-lg`) and TabsTrigger (`rounded-md`) use — this library's other * track-and-slab control.",
@@ -5945,9 +5950,9 @@
5945
5950
  "value": "var(--secondary)"
5946
5951
  },
5947
5952
  {
5948
- "description": "selected fill = the elevated surface + a soft shadow — the selected slab reads as lifted * off the recessed track, which is the whole affordance.",
5953
+ "description": "selected fill = the elevated surface + a soft shadow — the selected slab reads as lifted * off the recessed track, which is the whole affordance. default = var(--background), resolved at the call site",
5949
5954
  "name": "--segmented-item-selected-background",
5950
- "value": "var(--background)"
5955
+ "value": "initial"
5951
5956
  },
5952
5957
  {
5953
5958
  "description": "Segmented (one-of-N control) component tokens. * * EVERY VALUE BELOW IS A PORT OF AN ESTABLISHED SEGMENTED SPEC, NOT A CHOICE. The geometry and the * role assignments come from the widely-implemented enterprise Segmented control, transcribed as * ratios rather than as pixels: * * track padding: the bold line width → 2px * track background: the page's recessed neutral * item colour: the label ink · hover / selected colour: the body ink * item hover fill: the lighter neutral · item active fill: the heavier neutral * item selected fill: the elevated surface * label height = control height − track padding × 2 → 32 − 4 = 28 * label padding-inline = control padding-x − border width → 12 − 1 = 11 * icon gap = the small margin step / 2 → 6 * * THE ROLE PORTS ARE THE ONES `Tabs` ALREADY MADE. TabsList is `bg-muted` and an active * TabsTrigger is `bg-background` + `shadow-sm` — the same track-and-slab pair, so a Segmented and a * default Tabs strip read as one control family in both themes rather than two near-misses. That * matters more than reproducing the source dark ramp literally: that ramp's dark track is BLACK * with a lighter slab on it, while this palette's dark `--muted` sits above `--background`, so a * literal port would invert the pairing relative to every Tabs strip on the same page. * * NOT ONE OF THEM READS THE BRAND SEED. The spec's own derivation produces identical Segmented * values for its default seed and for this system's `#0071bd` — the control is neutral by * construction, and the only brand ink it can carry is the focus mark, which styles/focus-ring.css * owns. So the ports below are ROLE references (the same neutrals this system already names), * never copied hex. * * The disabled state is the ONE place this departs. The source recolours to a disabled ink and * changes nothing else; this system disables every control with the one `--disabled-opacity` knob, * and a single library-wide answer outranks a per-component one. * * The other thing NOT ported is the sliding thumb: it exists to animate between items, and the * implementations that have one remove the `-item-selected` class while it runs, which means the * selected state lives in two places at once. A static selected slab reads identically at rest and * cannot desynchronise.",
@@ -28,7 +28,26 @@ import type { LucideIcon } from "lucide-react";
28
28
  import type { HeadingLevelProp, ToneProp } from "../../props/vocabulary/index.js";
29
29
  export type ServiceLauncherStatusTone = Extract<ToneProp, "success" | "warning" | "destructive" | "info" | "neutral" | "muted">;
30
30
  export interface ServiceLauncherCardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
31
+ /**
32
+ * The generic glyph for this KIND of service. Stays REQUIRED and stays a `LucideIcon`: every
33
+ * service has one, including a service created ten seconds ago that has no logo yet, and it is
34
+ * the thing `logo` falls back to. See the `logo` note for why this was not widened to a union.
35
+ */
31
36
  icon: LucideIcon;
37
+ /**
38
+ * URL of the service's OWN uploaded mark — the PNG/WebP an administrator uploaded (gh#850).
39
+ * When it loads, it replaces `icon` in the medallion, in the glyph's exact box.
40
+ *
41
+ * It is a fallback chain, not a switch: the tile shows `icon` while the URL is in flight, and
42
+ * KEEPS showing `icon` if the URL 404s, is empty, or decodes to nothing. A launcher never
43
+ * degrades to an empty medallion or a torn-page glyph because a logo went missing — which is the
44
+ * only state that reaches production, since a logo path outlives the file it points at.
45
+ *
46
+ * Decorative, and deliberately has no `logoAlt` companion: the medallion is `aria-hidden` and the
47
+ * service NAME sits beside it. A screen reader must not hear "Attendance" twice (WCAG 2.2 · 1.1.1
48
+ * — an image adjacent to text conveying the same thing is decorative).
49
+ */
50
+ logo?: string;
32
51
  title: React.ReactNode;
33
52
  titleLevel?: HeadingLevelProp;
34
53
  statusLabel?: React.ReactNode;
@@ -2,6 +2,7 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Plus } from "lucide-react";
5
+ import { useImageLoadingStatus } from "../../lib/image-loading-status.js";
5
6
  import { cn } from "../../lib/utils.js";
6
7
  import { Badge } from "./badge.js";
7
8
  import { Card, CardContent } from "./card.js";
@@ -9,6 +10,7 @@ const ServiceLauncherCard = React.forwardRef(
9
10
  ({
10
11
  className,
11
12
  icon: Icon,
13
+ logo,
12
14
  title,
13
15
  titleLevel = 2,
14
16
  statusLabel,
@@ -20,6 +22,11 @@ const ServiceLauncherCard = React.forwardRef(
20
22
  ...props
21
23
  }, ref) => {
22
24
  const Heading = `h${titleLevel}`;
25
+ const [logoStatus, setLogoStatus] = React.useState("idle");
26
+ const logoLoadingStatus = useImageLoadingStatus(logo || void 0, {
27
+ loadingStatus: logoStatus,
28
+ setLoadingStatus: setLogoStatus
29
+ });
23
30
  return /* @__PURE__ */ jsx(
24
31
  Card,
25
32
  {
@@ -32,7 +39,13 @@ const ServiceLauncherCard = React.forwardRef(
32
39
  children: /* @__PURE__ */ jsxs(CardContent, { solo: true, children: [
33
40
  /* @__PURE__ */ jsxs("div", { "data-slot": "service-launcher-heading", children: [
34
41
  /* @__PURE__ */ jsxs("div", { "data-slot": "service-launcher-identity", children: [
35
- /* @__PURE__ */ jsx("span", { "data-slot": "service-launcher-icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(Icon, {}) }),
42
+ /* @__PURE__ */ jsx("span", { "data-slot": "service-launcher-icon", "aria-hidden": "true", children: logoLoadingStatus === "loaded" ? (
43
+ /* No `loading="lazy"`: the probe above has ALREADY fetched this URL by the time
44
+ * this element exists, so a lazy hint would describe a request that is over.
45
+ * `decoding="async"` is the hint that still means something here — it keeps the
46
+ * decode of a grid of logos off the main thread. */
47
+ /* @__PURE__ */ jsx("img", { "data-slot": "service-launcher-logo", src: logo, alt: "", decoding: "async" })
48
+ ) : /* @__PURE__ */ jsx(Icon, {}) }),
36
49
  /* @__PURE__ */ jsx(Heading, { "data-slot": "service-launcher-title", children: title })
37
50
  ] }),
38
51
  statusLabel != null ? /* @__PURE__ */ jsx(Badge, { tone: statusTone, "data-slot": "service-launcher-status", children: statusLabel }) : null