@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.
- package/agent/START-HERE.md +2 -2
- package/agent/components/Attachments.json +2 -1
- package/agent/components/ServiceLauncherCard.json +10 -3
- package/agent/components-index.json +1 -1
- package/agent/components.json +12 -4
- package/agent/index.json +3 -3
- package/agent/llms.txt +4 -4
- package/agent/tokens.json +30 -25
- package/dist/components/data-display/service-launcher-card.d.ts +19 -0
- package/dist/components/data-display/service-launcher-card.js +14 -1
- package/dist/components/data-entry/attachments.js +77 -33
- package/dist/components/ui/avatar.d.ts +1 -18
- package/dist/components/ui/avatar.js +1 -36
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +167 -0
- package/dist/i18n/messages/ja.json +167 -0
- package/dist/i18n/messages/vi.json +167 -0
- package/dist/lib/image-loading-status.d.ts +25 -0
- package/dist/lib/image-loading-status.js +41 -0
- package/dist/props/registry.d.ts +5 -1
- package/dist/props/registry.js +12 -1
- package/dist/styles/card-layout.css +6 -0
- package/dist/styles/control.css +2 -2
- package/dist/styles/data-entry-layout.css +245 -2
- package/dist/tokens/components/attachments.css +18 -9
- package/dist/tokens/components/segmented.css +2 -2
- package/docs/DESIGN-AUTHORITY.md +38 -0
- package/docs/assets/service-mark-rose.svg +6 -0
- package/docs/assets/service-mark-teal.svg +5 -0
- package/docs/data-display/service-launcher-card.tsx +232 -92
- package/docs/showcase/table-pagination.tsx +18 -7
- package/package.json +4 -3
package/agent/START-HERE.md
CHANGED
|
@@ -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.
|
|
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` —
|
|
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
|
|
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",
|
package/agent/components.json
CHANGED
|
@@ -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
|
|
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":
|
|
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.
|
|
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.
|
|
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,
|
|
4
|
-
> 47 cardinal rules. This file is the entry point for AI agents. Catalog version 28.
|
|
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.
|
|
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.
|
|
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": "
|
|
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": "
|
|
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": "
|
|
165
|
+
"value": "16.75rem"
|
|
166
166
|
},
|
|
167
167
|
{
|
|
168
|
-
"description": "
|
|
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(--
|
|
175
|
+
"value": "var(--radius-lg)"
|
|
171
176
|
},
|
|
172
177
|
{
|
|
173
|
-
"description": "
|
|
178
|
+
"description": "Ant X `marginSM` — glyph to name",
|
|
174
179
|
"name": "--attachments-card-gap",
|
|
175
|
-
"value": "var(--
|
|
180
|
+
"value": "var(--space-3)"
|
|
176
181
|
},
|
|
177
182
|
{
|
|
178
|
-
"description": "
|
|
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(--
|
|
185
|
+
"value": "var(--space-4)"
|
|
181
186
|
},
|
|
182
187
|
{
|
|
183
|
-
"description": "
|
|
188
|
+
"description": "Ant X `token.borderRadius` = 6 ≡ --radius",
|
|
184
189
|
"name": "--attachments-placeholder-radius",
|
|
185
|
-
"value": "var(--
|
|
190
|
+
"value": "var( --radius-lg )"
|
|
186
191
|
},
|
|
187
192
|
{
|
|
188
|
-
"description": "
|
|
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(--
|
|
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-
|
|
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(--
|
|
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-
|
|
210
|
+
"value": "var(--space-1)"
|
|
206
211
|
},
|
|
207
212
|
{
|
|
208
|
-
"description": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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:
|
|
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(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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
|
-
|
|
349
|
+
/* @__PURE__ */ jsx(
|
|
301
350
|
"div",
|
|
302
351
|
{
|
|
303
352
|
className: "ui-attachments-drop-layer",
|
|
304
|
-
"data-
|
|
305
|
-
"
|
|
306
|
-
|
|
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
|
-
)
|
|
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 {};
|