@godxjp/ui 28.10.0 → 28.12.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.
@@ -3,7 +3,7 @@
3
3
  You are about to write code against a design system you did not author. This file is the whole
4
4
  contract. Read it before you write JSX.
5
5
 
6
- **This catalog describes `@godxjp/ui` 28.10.0.** If the project you are editing has a different
6
+ **This catalog describes `@godxjp/ui` 28.12.0.** If the project you are editing has a different
7
7
  version in its `package.json`, read the pinned catalog for THAT version instead
8
8
  (`…/v<their-version>/agent/…`). A catalog newer than the installed package describes props that do
9
9
  not exist yet; older, and it hides props that do. Neither failure announces itself.
@@ -56,7 +56,7 @@ Then ask it: `search_components`, `get_component`, `get_tokens`, `get_rule`, `li
56
56
  its `importPath`, and its examples. Fetch only the handful you picked in step 1.
57
57
  3. `rules.json` — 47 cardinal rules. The ones about raw HTML and hardcoded colour are not
58
58
  style advice.
59
- 4. `tokens.json` — 1684 design tokens. Only when you need a specific knob's name.
59
+ 4. `tokens.json` — 1685 design tokens. Only when you need a specific knob's name.
60
60
  5. `anti-ai-tells.json` — 26 shapes that make generated UI look generated, each with the
61
61
  fix. Read before you reach for a gradient hero or a wall of coloured chips.
62
62
 
@@ -67,7 +67,8 @@
67
67
  "usage": [
68
68
  "DO keep antd field names on each item (`thumbUrl`, `originFileObj`, `uid`) — an Ant X call site should compile unchanged.",
69
69
  "DO use `ref.select({ accept, multiple })` to open the picker programmatically (Ant X 2.0).",
70
- "DON'T expect `styles`/`classNames` from Ant X — retune through `--attachments-*` tokens."
70
+ "DON'T expect `styles`/`classNames` from Ant X — retune through `--attachments-*` tokens.",
71
+ "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."
71
72
  ],
72
73
  "useCases": [
73
74
  "The attachment tray above a ChatComposer in an assistant surface.",
@@ -1,15 +1,20 @@
1
1
  {
2
- "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>",
2
+ "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>",
3
3
  "group": "data-display",
4
4
  "importPath": "@godxjp/ui/data-display",
5
5
  "name": "ServiceLauncherCard",
6
6
  "props": [
7
7
  {
8
- "description": "Decorative service glyph rendered in the canonical semantic icon surface.",
8
+ "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.",
9
9
  "name": "icon",
10
10
  "required": true,
11
11
  "type": "LucideIcon"
12
12
  },
13
+ {
14
+ "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.",
15
+ "name": "logo",
16
+ "type": "string"
17
+ },
13
18
  {
14
19
  "description": "Real downstream service display name.",
15
20
  "name": "title",
@@ -68,13 +73,15 @@
68
73
  "subParts": [
69
74
  "ServiceLauncherCardSkeleton"
70
75
  ],
71
- "tagline": "Token-owned downstream-service launcher tile with semantic icon, status, metadata, action, disabled reason, matching skeleton, and companion catalog CTA.",
76
+ "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.",
72
77
  "usage": [
73
78
  "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.",
74
79
  "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).",
75
80
  "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.",
76
81
  "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.",
77
82
  "DO keep `metadata` to machine identifiers (hostname · plan) — it is the only mono line. Sentences belong in `description` / `disabledReason`.",
83
+ "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.",
84
+ "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.",
78
85
  "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.",
79
86
  "DON'T show LIVE, a hostname, subscribed plan, or launch action merely because a service is active in the global catalog."
80
87
  ],
@@ -224,7 +224,7 @@
224
224
  {
225
225
  "group": "data-display",
226
226
  "name": "ServiceLauncherCard",
227
- "tagline": "Token-owned downstream-service launcher tile with semantic icon, status, metadata, action, disabled reason, matching skeleton, and companion catalog CTA."
227
+ "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."
228
228
  },
229
229
  {
230
230
  "group": "data-display",
@@ -3796,17 +3796,22 @@
3796
3796
  ]
3797
3797
  },
3798
3798
  {
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>",
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 // 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
3800
  "group": "data-display",
3801
3801
  "importPath": "@godxjp/ui/data-display",
3802
3802
  "name": "ServiceLauncherCard",
3803
3803
  "props": [
3804
3804
  {
3805
- "description": "Decorative service glyph rendered in the canonical semantic icon surface.",
3805
+ "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
3806
  "name": "icon",
3807
3807
  "required": true,
3808
3808
  "type": "LucideIcon"
3809
3809
  },
3810
+ {
3811
+ "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.",
3812
+ "name": "logo",
3813
+ "type": "string"
3814
+ },
3810
3815
  {
3811
3816
  "description": "Real downstream service display name.",
3812
3817
  "name": "title",
@@ -3865,13 +3870,15 @@
3865
3870
  "subParts": [
3866
3871
  "ServiceLauncherCardSkeleton"
3867
3872
  ],
3868
- "tagline": "Token-owned downstream-service launcher tile with semantic icon, status, metadata, action, disabled reason, matching skeleton, and companion catalog CTA.",
3873
+ "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
3874
  "usage": [
3870
3875
  "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
3876
  "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
3877
  "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
3878
  "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
3879
  "DO keep `metadata` to machine identifiers (hostname · plan) — it is the only mono line. Sentences belong in `description` / `disabledReason`.",
3880
+ "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.",
3881
+ "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
3882
  "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
3883
  "DON'T show LIVE, a hostname, subscribed plan, or launch action merely because a service is active in the global catalog."
3877
3884
  ],
@@ -15127,7 +15134,8 @@
15127
15134
  "usage": [
15128
15135
  "DO keep antd field names on each item (`thumbUrl`, `originFileObj`, `uid`) — an Ant X call site should compile unchanged.",
15129
15136
  "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."
15137
+ "DON'T expect `styles`/`classNames` from Ant X — retune through `--attachments-*` tokens.",
15138
+ "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
15139
  ],
15132
15140
  "useCases": [
15133
15141
  "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.12.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.12.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.12.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.12.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.12.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
@@ -6,7 +6,6 @@ import { useTranslation } from "../../i18n/use-translation.js";
6
6
  import { formatBytes } from "../../lib/format.js";
7
7
  import { cn } from "../../lib/utils.js";
8
8
  import { Button } from "../general/button.js";
9
- import { Text } from "../general/typography.js";
10
9
  import { Progress } from "../data-display/progress.js";
11
10
  import { createUploadItem } from "./upload-types.js";
12
11
  import { readDroppedFiles } from "./upload-files.js";
@@ -30,6 +29,10 @@ function resolvePlaceholder(placeholder, type) {
30
29
  if (!placeholder) return {};
31
30
  return typeof placeholder === "function" ? placeholder(type) : placeholder;
32
31
  }
32
+ function splitFileName(name) {
33
+ const match = /^(.*)\.[^.]+$/.exec(name ?? "");
34
+ return match ? [match[1], (name ?? "").slice(match[1].length)] : [name ?? "", ""];
35
+ }
33
36
  function fileMatchesAccept(file, accept) {
34
37
  if (!accept) return true;
35
38
  return accept.split(",").some((rule) => {
@@ -60,6 +63,7 @@ function PlaceholderUploader({
60
63
  className: cn("ui-attachments-placeholder", className),
61
64
  style,
62
65
  "data-drag-active": dragActive ? "true" : void 0,
66
+ "data-disabled": disabled ? "true" : void 0,
63
67
  "aria-hidden": disabled ? true : void 0,
64
68
  onDragEnter: (event) => {
65
69
  event.preventDefault();
@@ -71,6 +75,7 @@ function PlaceholderUploader({
71
75
  },
72
76
  onDrop: async (event) => {
73
77
  event.preventDefault();
78
+ event.stopPropagation();
74
79
  setDragActive(false);
75
80
  if (disabled) return;
76
81
  const files = await readDroppedFiles(event.dataTransfer, false);
@@ -107,27 +112,55 @@ function AttachmentCard({
107
112
  const { t } = useTranslation();
108
113
  const { disabled } = React.useContext(AttachmentContext);
109
114
  const preview = item.thumbUrl ?? item.url;
115
+ const [namePrefix, nameSuffix] = splitFileName(item.name);
110
116
  const desc = description ?? item.description ?? (item.status === "uploading" ? `${item.percent ?? 0}%` : item.status === "error" ? typeof item.response === "string" ? item.response : t("dataEntry.attachments.error") : item.size ? formatBytes(item.size) : "");
111
- return /* @__PURE__ */ jsxs("li", { className: cn("ui-attachments-card", classNames?.card, classNames?.file), children: [
112
- preview ? /* @__PURE__ */ jsx("img", { src: preview, alt: "", className: "ui-attachments-card-preview", "aria-hidden": "true" }) : /* @__PURE__ */ jsx("span", { className: "ui-attachments-card-placeholder", "aria-hidden": "true", children: /* @__PURE__ */ jsx(FileIcon, {}) }),
113
- (item.status === "uploading" || item.status === "error") && /* @__PURE__ */ jsx("div", { className: "ui-attachments-card-overlay", children: item.status === "uploading" && item.percent != null ? /* @__PURE__ */ jsx(Progress, { value: item.percent, "aria-label": t("dataEntry.attachments.uploading") }) : /* @__PURE__ */ jsx(Text, { size: "xs", children: String(desc) }) }),
114
- desc && item.status !== "uploading" && item.status !== "error" ? /* @__PURE__ */ jsx("div", { className: "ui-attachments-card-meta", children: desc }) : null,
115
- !disabled && onRemove ? /* @__PURE__ */ jsx(
116
- Button,
117
- {
118
- type: "button",
119
- variant: "ghost",
120
- size: "icon-xs",
121
- className: "ui-attachments-card-remove",
122
- "aria-label": t("dataEntry.attachments.remove", { name: item.name }),
123
- onClick: (event) => {
124
- event.stopPropagation();
125
- onRemove();
126
- },
127
- children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
128
- }
129
- ) : null
130
- ] });
117
+ return /* @__PURE__ */ jsxs(
118
+ "li",
119
+ {
120
+ className: cn("ui-attachments-card", classNames?.card, classNames?.file),
121
+ "data-status": item.status,
122
+ children: [
123
+ preview ? /* @__PURE__ */ jsx("img", { src: preview, alt: "", className: "ui-attachments-card-preview", "aria-hidden": "true" }) : /* @__PURE__ */ jsx("span", { className: "ui-attachments-card-placeholder", "aria-hidden": "true", children: /* @__PURE__ */ jsx(FileIcon, {}) }),
124
+ /* @__PURE__ */ jsxs("span", { className: "ui-attachments-card-content", children: [
125
+ /* @__PURE__ */ jsxs("span", { className: "ui-attachments-card-name", title: item.name, children: [
126
+ /* @__PURE__ */ jsx("span", { className: "ui-attachments-card-name-prefix", children: namePrefix }),
127
+ /* @__PURE__ */ jsx("span", { className: "ui-attachments-card-name-suffix", children: nameSuffix })
128
+ ] }),
129
+ desc ? /* @__PURE__ */ jsx(
130
+ "span",
131
+ {
132
+ className: "ui-attachments-card-meta",
133
+ title: typeof desc === "string" ? desc : void 0,
134
+ children: desc
135
+ }
136
+ ) : null
137
+ ] }),
138
+ item.status === "uploading" && item.percent != null ? /* @__PURE__ */ jsx("span", { className: "ui-attachments-card-progress", children: /* @__PURE__ */ jsx(
139
+ Progress,
140
+ {
141
+ size: "sm",
142
+ value: item.percent,
143
+ "aria-label": t("dataEntry.attachments.uploading")
144
+ }
145
+ ) }) : null,
146
+ !disabled && onRemove ? /* @__PURE__ */ jsx(
147
+ Button,
148
+ {
149
+ type: "button",
150
+ variant: "ghost",
151
+ size: "icon-xs",
152
+ className: "ui-attachments-card-remove",
153
+ "aria-label": t("dataEntry.attachments.remove", { name: item.name }),
154
+ onClick: (event) => {
155
+ event.stopPropagation();
156
+ onRemove();
157
+ },
158
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
159
+ }
160
+ ) : null
161
+ ]
162
+ }
163
+ );
131
164
  }
132
165
  const Attachments = React.forwardRef(
133
166
  ({
@@ -151,6 +184,7 @@ const Attachments = React.forwardRef(
151
184
  const containerRef = React.useRef(null);
152
185
  const inputRef = React.useRef(null);
153
186
  const [internalItems, setInternalItems] = React.useState([]);
187
+ const [dropActive, setDropActive] = React.useState(false);
154
188
  const items = controlledItems ?? internalItems;
155
189
  const setItems = React.useCallback(
156
190
  (next, file) => {
@@ -207,7 +241,7 @@ const Attachments = React.forwardRef(
207
241
  {
208
242
  ref: inputRef,
209
243
  type: "file",
210
- className: "ui-attachments-input",
244
+ className: cn("ui-attachments-input", "sr-only"),
211
245
  accept,
212
246
  multiple: multiple ?? (maxCount == null || maxCount > 1),
213
247
  disabled,
@@ -244,7 +278,7 @@ const Attachments = React.forwardRef(
244
278
  )
245
279
  ] });
246
280
  }
247
- const dropHost = getDropContainer?.() ?? containerRef.current;
281
+ const dropScope = getDropContainer?.() ? "viewport" : "control";
248
282
  return /* @__PURE__ */ jsx(AttachmentContext.Provider, { value: { disabled }, children: /* @__PURE__ */ jsxs(
249
283
  "div",
250
284
  {
@@ -252,6 +286,21 @@ const Attachments = React.forwardRef(
252
286
  className: cn("ui-attachments", className, rootClassName, classNames.root),
253
287
  style: styles.root,
254
288
  dir: "auto",
289
+ onDragEnter: (event) => {
290
+ event.preventDefault();
291
+ if (!disabled) setDropActive(true);
292
+ },
293
+ onDragOver: (event) => event.preventDefault(),
294
+ onDragLeave: (event) => {
295
+ if (!event.currentTarget.contains(event.relatedTarget)) setDropActive(false);
296
+ },
297
+ onDrop: async (event) => {
298
+ event.preventDefault();
299
+ setDropActive(false);
300
+ if (disabled) return;
301
+ const files = await readDroppedFiles(event.dataTransfer, false);
302
+ if (files.length) dispatchFiles(files);
303
+ },
255
304
  children: [
256
305
  fileInput,
257
306
  hasFiles ? /* @__PURE__ */ jsxs(
@@ -297,20 +346,15 @@ const Attachments = React.forwardRef(
297
346
  hidden: hasFiles
298
347
  }
299
348
  ),
300
- dropHost ? /* @__PURE__ */ jsx(
349
+ /* @__PURE__ */ jsx(
301
350
  "div",
302
351
  {
303
352
  className: "ui-attachments-drop-layer",
304
- "data-active": void 0,
305
- "aria-hidden": "true",
306
- onDragOver: (event) => event.preventDefault(),
307
- onDrop: async (event) => {
308
- event.preventDefault();
309
- const files = await readDroppedFiles(event.dataTransfer, false);
310
- dispatchFiles(files);
311
- }
353
+ "data-scope": dropScope,
354
+ "data-active": dropActive ? "true" : void 0,
355
+ "aria-hidden": "true"
312
356
  }
313
- ) : null
357
+ )
314
358
  ]
315
359
  }
316
360
  ) });
@@ -1,22 +1,6 @@
1
1
  import * as React from "react";
2
+ import { type ImageLoadingStatus } from "../../lib/image-loading-status.js";
2
3
  import type { AvatarProp } from "../../props/components/data-display.prop.js";
3
- /**
4
- * Trạng thái tải của ảnh trong avatar — chia sẻ giữa `AvatarImage` và
5
- * `AvatarFallback` qua context, đúng như Radix làm.
6
- *
7
- * React Aria KHÔNG có avatar, nên đây là việc TỰ DỰNG. Nhưng phần cần dựng lại
8
- * không phải cái vỏ `<span>` — mà là logic thật mà `@radix-ui/react-avatar` cầm:
9
- * ảnh chỉ được vẽ khi đã tải XONG, fallback chỉ được vẽ khi ảnh CHƯA xong hoặc
10
- * HỎNG, và `delayMs` giữ fallback im lặng đủ lâu để mạng nhanh không kịp nháy
11
- * một khung initials trước khi ảnh về.
12
- *
13
- * Phép dò là một `new Image()` RỜI, không phải `onLoad`/`onError` trên chính
14
- * thẻ được vẽ: thẻ chỉ tồn tại khi đã `loaded`, nên handler trên nó không bao
15
- * giờ có cơ hội báo "đang tải" hay "hỏng". Đây cũng là lý do một `<img>` hỏng
16
- * không bao giờ nằm trong DOM để trình duyệt vẽ biểu tượng ảnh vỡ lên trên
17
- * fallback.
18
- */
19
- type ImageLoadingStatus = "idle" | "loading" | "loaded" | "error";
20
4
  export declare const Avatar: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & {
21
5
  asChild?: boolean;
22
6
  } & Pick<AvatarProp, "appearance" | "presence" | "presenceLabel" | "shape" | "size"> & React.RefAttributes<HTMLSpanElement>>;
@@ -28,4 +12,3 @@ export declare const AvatarFallback: React.ForwardRefExoticComponent<Omit<React.
28
12
  asChild?: boolean;
29
13
  delayMs?: number;
30
14
  } & React.RefAttributes<HTMLSpanElement>>;
31
- export {};