@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
|
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffectEvent, useLayoutEffect } from "@react-aria/utils";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
|
+
import { useImageLoadingStatus } from "../../lib/image-loading-status.js";
|
|
6
7
|
import { Slot } from "../../lib/slot.js";
|
|
7
8
|
import { cn } from "../../lib/utils.js";
|
|
8
9
|
const AvatarContext = React.createContext(null);
|
|
@@ -13,42 +14,6 @@ function useAvatarContext(part) {
|
|
|
13
14
|
}
|
|
14
15
|
return context;
|
|
15
16
|
}
|
|
16
|
-
function getImageLoadingStatus(image) {
|
|
17
|
-
if (!image.complete) {
|
|
18
|
-
return "loading";
|
|
19
|
-
}
|
|
20
|
-
return image.naturalWidth > 0 ? "loaded" : "error";
|
|
21
|
-
}
|
|
22
|
-
function useImageLoadingStatus(src, {
|
|
23
|
-
loadingStatus,
|
|
24
|
-
setLoadingStatus,
|
|
25
|
-
referrerPolicy,
|
|
26
|
-
crossOrigin
|
|
27
|
-
}) {
|
|
28
|
-
useLayoutEffect(() => {
|
|
29
|
-
if (!src) {
|
|
30
|
-
setLoadingStatus("error");
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
const image = new window.Image();
|
|
34
|
-
const handleLoad = (event) => setLoadingStatus(getImageLoadingStatus(event.currentTarget));
|
|
35
|
-
const handleError = () => setLoadingStatus("error");
|
|
36
|
-
image.addEventListener("load", handleLoad);
|
|
37
|
-
image.addEventListener("error", handleError);
|
|
38
|
-
if (referrerPolicy) {
|
|
39
|
-
image.referrerPolicy = referrerPolicy;
|
|
40
|
-
}
|
|
41
|
-
image.crossOrigin = crossOrigin ?? null;
|
|
42
|
-
image.src = src;
|
|
43
|
-
setLoadingStatus(getImageLoadingStatus(image));
|
|
44
|
-
return () => {
|
|
45
|
-
image.removeEventListener("load", handleLoad);
|
|
46
|
-
image.removeEventListener("error", handleError);
|
|
47
|
-
setLoadingStatus("idle");
|
|
48
|
-
};
|
|
49
|
-
}, [src, crossOrigin, referrerPolicy, setLoadingStatus]);
|
|
50
|
-
return loadingStatus;
|
|
51
|
-
}
|
|
52
17
|
const Avatar = React.forwardRef(
|
|
53
18
|
({
|
|
54
19
|
className,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "28.
|
|
3
|
+
"version": "28.12.0",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -1195,5 +1195,172 @@
|
|
|
1195
1195
|
"ledgerLabel": "Ledger ID",
|
|
1196
1196
|
"ledgerHelper": "A 71-character identifier still stays inside the control"
|
|
1197
1197
|
}
|
|
1198
|
+
},
|
|
1199
|
+
"serviceLauncherShowcase": {
|
|
1200
|
+
"page": {
|
|
1201
|
+
"title": "ServiceLauncherCard",
|
|
1202
|
+
"subtitle": "Tiles for an organization's internal application list — available, restricted, suspended, archived, loading, and catalog entry."
|
|
1203
|
+
},
|
|
1204
|
+
"status": {
|
|
1205
|
+
"available": "Available",
|
|
1206
|
+
"active": "Active",
|
|
1207
|
+
"maintenance": "Under maintenance",
|
|
1208
|
+
"stopped": "Stopped",
|
|
1209
|
+
"adminOnly": "Admin only",
|
|
1210
|
+
"preparing": "Preparing",
|
|
1211
|
+
"archived": "Archived",
|
|
1212
|
+
"unapplied": "Not requested",
|
|
1213
|
+
"noPermission": "No permission",
|
|
1214
|
+
"attentionRequired": "Attention required"
|
|
1215
|
+
},
|
|
1216
|
+
"action": {
|
|
1217
|
+
"openService": "Open service",
|
|
1218
|
+
"open": "Open",
|
|
1219
|
+
"viewStatus": "View status",
|
|
1220
|
+
"viewHistory": "View history",
|
|
1221
|
+
"checkPermissions": "Check permissions",
|
|
1222
|
+
"openSettings": "Open settings",
|
|
1223
|
+
"viewArchive": "View archive",
|
|
1224
|
+
"openRequestForm": "Open request form",
|
|
1225
|
+
"openList": "Open the list",
|
|
1226
|
+
"openServiceEnglish": "Open service"
|
|
1227
|
+
},
|
|
1228
|
+
"sections": {
|
|
1229
|
+
"basics": {
|
|
1230
|
+
"heading": "1 · Basics · services you can use",
|
|
1231
|
+
"why": "Icon, name, status, description, identifier, action. The status copy always belongs to the consumer — the component only decides what the Badge's tone means.",
|
|
1232
|
+
"attendance": {
|
|
1233
|
+
"title": "Attendance management",
|
|
1234
|
+
"description": "Manage clock-in/out, shifts, and leave requests in one workspace."
|
|
1235
|
+
},
|
|
1236
|
+
"expense": {
|
|
1237
|
+
"title": "Expense reimbursement",
|
|
1238
|
+
"description": "Handles everything from receipt submission and approval to journal-entry sync."
|
|
1239
|
+
},
|
|
1240
|
+
"meetingRoom": {
|
|
1241
|
+
"title": "Meeting room booking",
|
|
1242
|
+
"description": "Book meeting rooms and equipment for each site."
|
|
1243
|
+
}
|
|
1244
|
+
},
|
|
1245
|
+
"statusTones": {
|
|
1246
|
+
"heading": "2 · All 6 statusTone values",
|
|
1247
|
+
"why": "success / warning / destructive / info / neutral / muted. The tone only decides what the Badge means — the copy, the judgment, and whether the action is allowed all come from the consumer.",
|
|
1248
|
+
"mail": {
|
|
1249
|
+
"title": "Internal mail",
|
|
1250
|
+
"description": "Manage mailboxes, distribution lists, and shared signatures."
|
|
1251
|
+
},
|
|
1252
|
+
"fileShare": {
|
|
1253
|
+
"title": "File sharing",
|
|
1254
|
+
"description": "Handles department folders and external share links."
|
|
1255
|
+
},
|
|
1256
|
+
"dataSync": {
|
|
1257
|
+
"title": "Data integration platform",
|
|
1258
|
+
"description": "Manages data-sync jobs with external systems."
|
|
1259
|
+
},
|
|
1260
|
+
"iam": {
|
|
1261
|
+
"title": "Access management",
|
|
1262
|
+
"description": "Review roles, two-factor authentication policy, and audit events."
|
|
1263
|
+
},
|
|
1264
|
+
"integrations": {
|
|
1265
|
+
"title": "Connected apps",
|
|
1266
|
+
"description": "Configure third-party integration connections and scopes."
|
|
1267
|
+
},
|
|
1268
|
+
"legacyKb": {
|
|
1269
|
+
"title": "Legacy knowledge base",
|
|
1270
|
+
"description": "Keeps operating procedures through fiscal year 2024 in read-only storage."
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
"disabledReason": {
|
|
1274
|
+
"heading": "3 · disabledReason · the reason an action doesn't work comes before the action",
|
|
1275
|
+
"why": "A disabled button conveys no 'why' at all. That's why the reason is placed before the action in DOM order (WCAG 2.2 · 1.3.2), and the tile carries data-unavailable so its medallion recedes. disabledReason does not disable the button — that stays the consumer's own Button prop.",
|
|
1276
|
+
"inventory": {
|
|
1277
|
+
"title": "Inventory management",
|
|
1278
|
+
"description": "Manage inventory, stock movements, and physical counts by site.",
|
|
1279
|
+
"reason": "This department's usage request has not been approved yet."
|
|
1280
|
+
},
|
|
1281
|
+
"contracts": {
|
|
1282
|
+
"title": "Contract management",
|
|
1283
|
+
"description": "Handles contract storage, deadline notifications, and e-signature requests.",
|
|
1284
|
+
"reason": "Viewing requires a Legal department role. Apply through the IT help desk."
|
|
1285
|
+
},
|
|
1286
|
+
"analytics": {
|
|
1287
|
+
"title": "Analytics platform",
|
|
1288
|
+
"description": "Provides company-wide dashboards and data marts.",
|
|
1289
|
+
"reason": "This service has been suspended by an organization administrator."
|
|
1290
|
+
}
|
|
1291
|
+
},
|
|
1292
|
+
"loadingCatalog": {
|
|
1293
|
+
"heading": "4 · Loading state and catalog entry",
|
|
1294
|
+
"why": "ServiceLauncherCardSkeleton is the initial placeholder with the same skeleton, requires a label, carries aria-busy, and deliberately never opens a live region. ServiceCatalogCta is the companion tile you place alongside it only when a real additional entry point exists, and it accepts a glyph other than the default Plus.",
|
|
1295
|
+
"skeletonLabel": "Loading service",
|
|
1296
|
+
"addApp": {
|
|
1297
|
+
"title": "Add an app"
|
|
1298
|
+
},
|
|
1299
|
+
"allApps": {
|
|
1300
|
+
"title": "View all internal apps"
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
"edgeCases": {
|
|
1304
|
+
"heading": "5 · Touching the edges · a 3-line name, no description, a long hostname that won't break",
|
|
1305
|
+
"why": "A tidy example never shows where a tile breaks. This is the side that breaks: a name that wraps to 3 lines sits next to a 2-word name in the same row, a tile with no description sits beside one that has one, and a hostname with no whitespace tries to stretch the column. If it looks unsightly, this section is doing its job — what has to hold up is the component, not the sample data.",
|
|
1306
|
+
"hrPortal": {
|
|
1307
|
+
"title": "Global HR information platform · Employee self-service portal (Asia-Pacific)",
|
|
1308
|
+
"description": "An integrated workspace that centrally manages HR master data, attendance, payroll, and performance reviews."
|
|
1309
|
+
},
|
|
1310
|
+
"dailyReport": {
|
|
1311
|
+
"title": "Daily report"
|
|
1312
|
+
},
|
|
1313
|
+
"warningConsole": {
|
|
1314
|
+
"title": "Global Workforce Identity & Entitlement Administration Console",
|
|
1315
|
+
"description": "Review pending entitlement requests before the next access certification.",
|
|
1316
|
+
"reason": "Your organization has no approved request for this service yet."
|
|
1317
|
+
}
|
|
1318
|
+
},
|
|
1319
|
+
"minimalTiles": {
|
|
1320
|
+
"heading": "6 · Minimal tiles",
|
|
1321
|
+
"why": "Only icon, title, and action are required. Omit statusLabel and the whole Badge row disappears; omit description and metadata too and the skeleton still holds — compare its height with the fully loaded tile next to it.",
|
|
1322
|
+
"wiki": {
|
|
1323
|
+
"title": "Internal wiki"
|
|
1324
|
+
},
|
|
1325
|
+
"contact": {
|
|
1326
|
+
"title": "Support contact",
|
|
1327
|
+
"description": "Internal system inquiries are accepted here."
|
|
1328
|
+
},
|
|
1329
|
+
"calendar": {
|
|
1330
|
+
"title": "Company-wide calendar",
|
|
1331
|
+
"description": "Shares company-wide events, holidays, and site-specific schedules.",
|
|
1332
|
+
"reason": "Published as read-only."
|
|
1333
|
+
}
|
|
1334
|
+
},
|
|
1335
|
+
"logo": {
|
|
1336
|
+
"heading": "7 · logo · an uploaded logo, and the services that have none",
|
|
1337
|
+
"why": "logo is a URL to an image the administrator uploaded, and it only replaces icon when it actually loads. icon stays required — a freshly created service has no logo, and a service whose URL now 404s has none either. A broken image never enters the DOM. The image box is the same 20px as the glyph (--card-service-launcher-icon-glyph-size), and its aspect ratio is preserved with object-fit: contain.",
|
|
1338
|
+
"uploaded": {
|
|
1339
|
+
"title": "Attendance management",
|
|
1340
|
+
"description": "A service with an uploaded logo. The medallion shows the logo instead of icon."
|
|
1341
|
+
},
|
|
1342
|
+
"missing": {
|
|
1343
|
+
"title": "Attendance management (no logo set)",
|
|
1344
|
+
"description": "The same tile without a logo passed. icon stays as-is, and the medallion size doesn't change either."
|
|
1345
|
+
},
|
|
1346
|
+
"broken": {
|
|
1347
|
+
"title": "Data integration platform (logo 404)",
|
|
1348
|
+
"description": "logo_url is set but the file doesn't exist. The tile doesn't go blank — it falls back to icon."
|
|
1349
|
+
},
|
|
1350
|
+
"wideMark": {
|
|
1351
|
+
"title": "Expense reimbursement",
|
|
1352
|
+
"description": "A wide 240×96 wordmark logo. Because of contain it's never cropped, and the box width stays the same as the glyph's."
|
|
1353
|
+
},
|
|
1354
|
+
"withReason": {
|
|
1355
|
+
"title": "Access management",
|
|
1356
|
+
"description": "logo and disabledReason are orthogonal. State is conveyed by the Badge and the reason text — we never let another company's brand color carry that meaning on our own.",
|
|
1357
|
+
"reason": "Viewing requires an administrator role."
|
|
1358
|
+
},
|
|
1359
|
+
"emptyString": {
|
|
1360
|
+
"title": "Internal mail",
|
|
1361
|
+
"description": "An empty-string logo. It's treated the same as not set, and issues no wasted request either."
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1198
1365
|
}
|
|
1199
1366
|
}
|
|
@@ -1175,5 +1175,172 @@
|
|
|
1175
1175
|
"ledgerLabel": "台帳ID",
|
|
1176
1176
|
"ledgerHelper": "71文字の識別子でも行からはみ出さない"
|
|
1177
1177
|
}
|
|
1178
|
+
},
|
|
1179
|
+
"serviceLauncherShowcase": {
|
|
1180
|
+
"page": {
|
|
1181
|
+
"title": "ServiceLauncherCard",
|
|
1182
|
+
"subtitle": "社内アプリケーション一覧のタイル — 利用可能・制限・停止・アーカイブ・読み込み中・カタログ導線"
|
|
1183
|
+
},
|
|
1184
|
+
"status": {
|
|
1185
|
+
"available": "利用可能",
|
|
1186
|
+
"active": "稼働中",
|
|
1187
|
+
"maintenance": "メンテナンス中",
|
|
1188
|
+
"stopped": "停止中",
|
|
1189
|
+
"adminOnly": "管理者限定",
|
|
1190
|
+
"preparing": "準備中",
|
|
1191
|
+
"archived": "アーカイブ",
|
|
1192
|
+
"unapplied": "未申請",
|
|
1193
|
+
"noPermission": "権限なし",
|
|
1194
|
+
"attentionRequired": "Attention required"
|
|
1195
|
+
},
|
|
1196
|
+
"action": {
|
|
1197
|
+
"openService": "サービスを開く",
|
|
1198
|
+
"open": "開く",
|
|
1199
|
+
"viewStatus": "状況を見る",
|
|
1200
|
+
"viewHistory": "履歴を見る",
|
|
1201
|
+
"checkPermissions": "権限を確認",
|
|
1202
|
+
"openSettings": "設定を開く",
|
|
1203
|
+
"viewArchive": "アーカイブを見る",
|
|
1204
|
+
"openRequestForm": "申請フォームを開く",
|
|
1205
|
+
"openList": "一覧を開く",
|
|
1206
|
+
"openServiceEnglish": "Open service"
|
|
1207
|
+
},
|
|
1208
|
+
"sections": {
|
|
1209
|
+
"basics": {
|
|
1210
|
+
"heading": "1 · 基本 · 利用できるサービス",
|
|
1211
|
+
"why": "アイコン・名称・ステータス・説明・識別子・操作。ステータスの文言は常に利用側のコピーで、コンポーネントが決めるのは tone に対応する Badge の意味づけだけです。",
|
|
1212
|
+
"attendance": {
|
|
1213
|
+
"title": "勤怠管理",
|
|
1214
|
+
"description": "打刻、シフト、休暇申請を一つのワークスペースで管理します。"
|
|
1215
|
+
},
|
|
1216
|
+
"expense": {
|
|
1217
|
+
"title": "経費精算",
|
|
1218
|
+
"description": "領収書の提出から承認、仕訳連携までを扱います。"
|
|
1219
|
+
},
|
|
1220
|
+
"meetingRoom": {
|
|
1221
|
+
"title": "会議室予約",
|
|
1222
|
+
"description": "拠点ごとの会議室と備品を予約します。"
|
|
1223
|
+
}
|
|
1224
|
+
},
|
|
1225
|
+
"statusTones": {
|
|
1226
|
+
"heading": "2 · statusTone の 6 値すべて",
|
|
1227
|
+
"why": "success / warning / destructive / info / neutral / muted。tone は Badge の意味づけだけを決め、文言・判定・操作の可否はすべて利用側が渡します。",
|
|
1228
|
+
"mail": {
|
|
1229
|
+
"title": "社内メール",
|
|
1230
|
+
"description": "メールボックス、配布リスト、共有署名を管理します。"
|
|
1231
|
+
},
|
|
1232
|
+
"fileShare": {
|
|
1233
|
+
"title": "ファイル共有",
|
|
1234
|
+
"description": "部門フォルダと外部共有リンクを扱います。"
|
|
1235
|
+
},
|
|
1236
|
+
"dataSync": {
|
|
1237
|
+
"title": "データ連携基盤",
|
|
1238
|
+
"description": "外部システムとのデータ同期ジョブを管理します。"
|
|
1239
|
+
},
|
|
1240
|
+
"iam": {
|
|
1241
|
+
"title": "権限管理",
|
|
1242
|
+
"description": "ロール、二要素認証ポリシー、監査イベントを確認します。"
|
|
1243
|
+
},
|
|
1244
|
+
"integrations": {
|
|
1245
|
+
"title": "連携アプリ",
|
|
1246
|
+
"description": "サードパーティ連携の接続とスコープを設定します。"
|
|
1247
|
+
},
|
|
1248
|
+
"legacyKb": {
|
|
1249
|
+
"title": "旧ナレッジベース",
|
|
1250
|
+
"description": "2024 年度までの運用手順を読み取り専用で保管しています。"
|
|
1251
|
+
}
|
|
1252
|
+
},
|
|
1253
|
+
"disabledReason": {
|
|
1254
|
+
"heading": "3 · disabledReason · 操作が効かない理由は操作より先に置かれる",
|
|
1255
|
+
"why": "無効化されたボタンは「なぜ」を一切伝えません。だから理由は DOM 順で操作より先に置かれ(WCAG 2.2 · 1.3.2)、タイルは data-unavailable を帯びてメダリオンが沈みます。disabledReason はボタンを無効化しません — それは利用側の Button prop のままです。",
|
|
1256
|
+
"inventory": {
|
|
1257
|
+
"title": "在庫管理",
|
|
1258
|
+
"description": "拠点別の在庫、入出庫、棚卸を管理します。",
|
|
1259
|
+
"reason": "この部門ではまだ利用申請が承認されていません。"
|
|
1260
|
+
},
|
|
1261
|
+
"contracts": {
|
|
1262
|
+
"title": "契約書管理",
|
|
1263
|
+
"description": "契約書の保管、期限通知、電子署名の依頼を扱います。",
|
|
1264
|
+
"reason": "閲覧には法務部門のロールが必要です。IT ヘルプデスクへ申請してください。"
|
|
1265
|
+
},
|
|
1266
|
+
"analytics": {
|
|
1267
|
+
"title": "分析基盤",
|
|
1268
|
+
"description": "全社ダッシュボードとデータマートを提供します。",
|
|
1269
|
+
"reason": "このサービスは組織の管理者によって停止されています。"
|
|
1270
|
+
}
|
|
1271
|
+
},
|
|
1272
|
+
"loadingCatalog": {
|
|
1273
|
+
"heading": "4 · 読み込み中とカタログ導線",
|
|
1274
|
+
"why": "ServiceLauncherCardSkeleton は同じ骨格の初期プレースホルダーで、label が必須・aria-busy を持ち、ライブリージョンは意図的に開きません。ServiceCatalogCta は実在する追加導線がある場合にだけ並べる相棒タイルで、既定の Plus 以外のグリフも受け取ります。",
|
|
1275
|
+
"skeletonLabel": "サービスを読み込み中",
|
|
1276
|
+
"addApp": {
|
|
1277
|
+
"title": "アプリを追加"
|
|
1278
|
+
},
|
|
1279
|
+
"allApps": {
|
|
1280
|
+
"title": "すべての社内アプリを見る"
|
|
1281
|
+
}
|
|
1282
|
+
},
|
|
1283
|
+
"edgeCases": {
|
|
1284
|
+
"heading": "5 · 端に触れる · 3 行の名称、説明なし、切れない長いホスト名",
|
|
1285
|
+
"why": "整った例はタイルが崩れる場所を何も見せません。ここは崩れる側です:3 行になる名称と 2 語の名称が同じ行に並び、説明のないタイルが説明のあるタイルの隣に置かれ、空白のないホスト名が列幅を広げようとします。見苦しく見えるなら、このセクションは仕事をしています — 耐えるのはサンプルデータではなくコンポーネントの側です。",
|
|
1286
|
+
"hrPortal": {
|
|
1287
|
+
"title": "グローバル人事情報基盤・従業員セルフサービスポータル(アジア太平洋地域)",
|
|
1288
|
+
"description": "人事マスタ、勤怠、給与、評価を一元管理する統合ワークスペースです。"
|
|
1289
|
+
},
|
|
1290
|
+
"dailyReport": {
|
|
1291
|
+
"title": "日報"
|
|
1292
|
+
},
|
|
1293
|
+
"warningConsole": {
|
|
1294
|
+
"title": "Global Workforce Identity & Entitlement Administration Console",
|
|
1295
|
+
"description": "Review pending entitlement requests before the next access certification.",
|
|
1296
|
+
"reason": "Your organization has no approved request for this service yet."
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1299
|
+
"minimalTiles": {
|
|
1300
|
+
"heading": "6 · 最小のタイル",
|
|
1301
|
+
"why": "必須は icon・title・action の 3 つだけです。statusLabel を省けば Badge の行ごと消え、description と metadata を省いても骨格は保たれます — 隣の満載のタイルと高さを比べてください。",
|
|
1302
|
+
"wiki": {
|
|
1303
|
+
"title": "社内 Wiki"
|
|
1304
|
+
},
|
|
1305
|
+
"contact": {
|
|
1306
|
+
"title": "問い合わせ窓口",
|
|
1307
|
+
"description": "社内のシステム相談はこちらで受け付けています。"
|
|
1308
|
+
},
|
|
1309
|
+
"calendar": {
|
|
1310
|
+
"title": "全社カレンダー",
|
|
1311
|
+
"description": "全社行事、休業日、拠点別の予定を共有します。",
|
|
1312
|
+
"reason": "読み取り専用で公開されています。"
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
"logo": {
|
|
1316
|
+
"heading": "7 · logo · アップロードされたロゴと、それがないサービス",
|
|
1317
|
+
"why": "logo は管理者がアップロードした画像の URL で、読み込めたときだけ icon を置き換えます。icon は必須のままです — 作ったばかりのサービスにはロゴがなく、URL が 404 になったサービスにもロゴはありません。壊れた画像は DOM に入りません。画像の箱はグリフと同じ 20px(--card-service-launcher-icon-glyph-size)で、縦横比は object-fit: contain で保たれます。",
|
|
1318
|
+
"uploaded": {
|
|
1319
|
+
"title": "勤怠管理",
|
|
1320
|
+
"description": "ロゴをアップロード済みのサービス。メダリオンには icon ではなくロゴが出ます。"
|
|
1321
|
+
},
|
|
1322
|
+
"missing": {
|
|
1323
|
+
"title": "勤怠管理(ロゴ未設定)",
|
|
1324
|
+
"description": "同じタイルで logo を渡さない場合。icon がそのまま残り、メダリオンの大きさも変わりません。"
|
|
1325
|
+
},
|
|
1326
|
+
"broken": {
|
|
1327
|
+
"title": "データ連携基盤(ロゴ 404)",
|
|
1328
|
+
"description": "logo_url は入っているのにファイルがない状態。タイルは空にならず icon に戻ります。"
|
|
1329
|
+
},
|
|
1330
|
+
"wideMark": {
|
|
1331
|
+
"title": "経費精算",
|
|
1332
|
+
"description": "240×96 の横長ロゴ。contain なので切り抜かれず、箱の幅もグリフと同じままです。"
|
|
1333
|
+
},
|
|
1334
|
+
"withReason": {
|
|
1335
|
+
"title": "権限管理",
|
|
1336
|
+
"description": "ロゴと disabledReason は直交します。状態を伝えるのは Badge と理由文であって、他社のブランド色をこちらで勝手に転ばしはしません。",
|
|
1337
|
+
"reason": "閲覧には管理者ロールが必要です。"
|
|
1338
|
+
},
|
|
1339
|
+
"emptyString": {
|
|
1340
|
+
"title": "社内メール",
|
|
1341
|
+
"description": "空文字列の logo。未設定と同じ扱いで、無駄なリクエストも発行しません。"
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1178
1345
|
}
|
|
1179
1346
|
}
|
|
@@ -1176,5 +1176,172 @@
|
|
|
1176
1176
|
"ledgerLabel": "Mã sổ cái",
|
|
1177
1177
|
"ledgerHelper": "Định danh 71 ký tự vẫn nằm trong khung"
|
|
1178
1178
|
}
|
|
1179
|
+
},
|
|
1180
|
+
"serviceLauncherShowcase": {
|
|
1181
|
+
"page": {
|
|
1182
|
+
"title": "ServiceLauncherCard",
|
|
1183
|
+
"subtitle": "Các ô cho danh sách ứng dụng nội bộ của tổ chức — khả dụng, hạn chế, tạm dừng, lưu trữ, đang tải và lối vào danh mục."
|
|
1184
|
+
},
|
|
1185
|
+
"status": {
|
|
1186
|
+
"available": "Khả dụng",
|
|
1187
|
+
"active": "Đang hoạt động",
|
|
1188
|
+
"maintenance": "Đang bảo trì",
|
|
1189
|
+
"stopped": "Đã dừng",
|
|
1190
|
+
"adminOnly": "Chỉ dành cho quản trị viên",
|
|
1191
|
+
"preparing": "Đang chuẩn bị",
|
|
1192
|
+
"archived": "Đã lưu trữ",
|
|
1193
|
+
"unapplied": "Chưa đăng ký",
|
|
1194
|
+
"noPermission": "Không có quyền",
|
|
1195
|
+
"attentionRequired": "Attention required"
|
|
1196
|
+
},
|
|
1197
|
+
"action": {
|
|
1198
|
+
"openService": "Mở dịch vụ",
|
|
1199
|
+
"open": "Mở",
|
|
1200
|
+
"viewStatus": "Xem tình trạng",
|
|
1201
|
+
"viewHistory": "Xem lịch sử",
|
|
1202
|
+
"checkPermissions": "Kiểm tra quyền",
|
|
1203
|
+
"openSettings": "Mở cài đặt",
|
|
1204
|
+
"viewArchive": "Xem lưu trữ",
|
|
1205
|
+
"openRequestForm": "Mở biểu mẫu yêu cầu",
|
|
1206
|
+
"openList": "Mở danh sách",
|
|
1207
|
+
"openServiceEnglish": "Open service"
|
|
1208
|
+
},
|
|
1209
|
+
"sections": {
|
|
1210
|
+
"basics": {
|
|
1211
|
+
"heading": "1 · Cơ bản · các dịch vụ có thể sử dụng",
|
|
1212
|
+
"why": "Biểu tượng, tên, trạng thái, mô tả, định danh, thao tác. Nội dung trạng thái luôn do phía sử dụng cung cấp — component chỉ quyết định ý nghĩa của tone trên Badge.",
|
|
1213
|
+
"attendance": {
|
|
1214
|
+
"title": "Quản lý chấm công",
|
|
1215
|
+
"description": "Quản lý chấm công, ca làm việc và đơn xin nghỉ phép trong một không gian làm việc duy nhất."
|
|
1216
|
+
},
|
|
1217
|
+
"expense": {
|
|
1218
|
+
"title": "Quyết toán chi phí",
|
|
1219
|
+
"description": "Xử lý từ nộp hóa đơn, phê duyệt cho đến đồng bộ với sổ kế toán."
|
|
1220
|
+
},
|
|
1221
|
+
"meetingRoom": {
|
|
1222
|
+
"title": "Đặt phòng họp",
|
|
1223
|
+
"description": "Đặt phòng họp và thiết bị theo từng cơ sở."
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
"statusTones": {
|
|
1227
|
+
"heading": "2 · Toàn bộ 6 giá trị statusTone",
|
|
1228
|
+
"why": "success / warning / destructive / info / neutral / muted. Tone chỉ quyết định ý nghĩa của Badge — nội dung, phán đoán và việc thao tác có được phép hay không đều do phía sử dụng cung cấp.",
|
|
1229
|
+
"mail": {
|
|
1230
|
+
"title": "Email nội bộ",
|
|
1231
|
+
"description": "Quản lý hộp thư, danh sách phân phối và chữ ký dùng chung."
|
|
1232
|
+
},
|
|
1233
|
+
"fileShare": {
|
|
1234
|
+
"title": "Chia sẻ tệp",
|
|
1235
|
+
"description": "Xử lý thư mục phòng ban và liên kết chia sẻ ra bên ngoài."
|
|
1236
|
+
},
|
|
1237
|
+
"dataSync": {
|
|
1238
|
+
"title": "Nền tảng tích hợp dữ liệu",
|
|
1239
|
+
"description": "Quản lý các tác vụ đồng bộ dữ liệu với hệ thống bên ngoài."
|
|
1240
|
+
},
|
|
1241
|
+
"iam": {
|
|
1242
|
+
"title": "Quản lý quyền truy cập",
|
|
1243
|
+
"description": "Xem vai trò, chính sách xác thực hai yếu tố và các sự kiện kiểm toán."
|
|
1244
|
+
},
|
|
1245
|
+
"integrations": {
|
|
1246
|
+
"title": "Ứng dụng liên kết",
|
|
1247
|
+
"description": "Cấu hình kết nối và phạm vi cho các tích hợp bên thứ ba."
|
|
1248
|
+
},
|
|
1249
|
+
"legacyKb": {
|
|
1250
|
+
"title": "Cơ sở tri thức cũ",
|
|
1251
|
+
"description": "Lưu trữ ở chế độ chỉ đọc các quy trình vận hành đến hết năm tài chính 2024."
|
|
1252
|
+
}
|
|
1253
|
+
},
|
|
1254
|
+
"disabledReason": {
|
|
1255
|
+
"heading": "3 · disabledReason · lý do thao tác không thực hiện được được đặt trước thao tác",
|
|
1256
|
+
"why": "Một nút bị vô hiệu hóa không truyền tải bất kỳ lý do 'tại sao' nào. Vì vậy lý do được đặt trước thao tác theo thứ tự DOM (WCAG 2.2 · 1.3.2), và ô mang thuộc tính data-unavailable khiến huy hiệu mờ đi. disabledReason không vô hiệu hóa nút — đó vẫn là prop Button do phía sử dụng kiểm soát.",
|
|
1257
|
+
"inventory": {
|
|
1258
|
+
"title": "Quản lý tồn kho",
|
|
1259
|
+
"description": "Quản lý tồn kho, nhập xuất kho và kiểm kê theo từng cơ sở.",
|
|
1260
|
+
"reason": "Yêu cầu sử dụng của phòng ban này vẫn chưa được phê duyệt."
|
|
1261
|
+
},
|
|
1262
|
+
"contracts": {
|
|
1263
|
+
"title": "Quản lý hợp đồng",
|
|
1264
|
+
"description": "Xử lý lưu trữ hợp đồng, thông báo hạn và yêu cầu ký điện tử.",
|
|
1265
|
+
"reason": "Cần có vai trò của bộ phận pháp chế để xem. Vui lòng đăng ký qua bộ phận hỗ trợ IT."
|
|
1266
|
+
},
|
|
1267
|
+
"analytics": {
|
|
1268
|
+
"title": "Nền tảng phân tích",
|
|
1269
|
+
"description": "Cung cấp bảng điều khiển và kho dữ liệu cho toàn công ty.",
|
|
1270
|
+
"reason": "Dịch vụ này đã bị quản trị viên của tổ chức tạm dừng."
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
"loadingCatalog": {
|
|
1274
|
+
"heading": "4 · Trạng thái đang tải và lối vào danh mục",
|
|
1275
|
+
"why": "ServiceLauncherCardSkeleton là placeholder ban đầu có cùng khung xương, bắt buộc phải có label, mang aria-busy, và cố ý không mở live region. ServiceCatalogCta là ô đồng hành chỉ đặt cạnh khi thực sự có thêm lối vào, và nó nhận một glyph khác ngoài Plus mặc định.",
|
|
1276
|
+
"skeletonLabel": "Đang tải dịch vụ",
|
|
1277
|
+
"addApp": {
|
|
1278
|
+
"title": "Thêm ứng dụng"
|
|
1279
|
+
},
|
|
1280
|
+
"allApps": {
|
|
1281
|
+
"title": "Xem tất cả ứng dụng nội bộ"
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
"edgeCases": {
|
|
1285
|
+
"heading": "5 · Chạm tới giới hạn · tên 3 dòng, không có mô tả, tên máy chủ dài không thể ngắt",
|
|
1286
|
+
"why": "Một ví dụ gọn gàng không bao giờ cho thấy nơi ô bị vỡ. Đây là mặt vỡ đó: một tên phải xuống 3 dòng nằm cạnh một tên chỉ 2 từ trên cùng một hàng, một ô không có mô tả đặt cạnh một ô có mô tả, và một tên máy chủ không có khoảng trắng cố kéo giãn cột. Nếu trông có vẻ xấu xí, tức là phần này đang làm đúng việc của nó — thứ phải chịu được áp lực là component, chứ không phải dữ liệu mẫu.",
|
|
1287
|
+
"hrPortal": {
|
|
1288
|
+
"title": "Nền tảng thông tin nhân sự toàn cầu · Cổng tự phục vụ nhân viên (khu vực châu Á - Thái Bình Dương)",
|
|
1289
|
+
"description": "Không gian làm việc tích hợp quản lý tập trung dữ liệu nhân sự gốc, chấm công, lương và đánh giá."
|
|
1290
|
+
},
|
|
1291
|
+
"dailyReport": {
|
|
1292
|
+
"title": "Báo cáo hằng ngày"
|
|
1293
|
+
},
|
|
1294
|
+
"warningConsole": {
|
|
1295
|
+
"title": "Global Workforce Identity & Entitlement Administration Console",
|
|
1296
|
+
"description": "Review pending entitlement requests before the next access certification.",
|
|
1297
|
+
"reason": "Your organization has no approved request for this service yet."
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
"minimalTiles": {
|
|
1301
|
+
"heading": "6 · Ô tối giản",
|
|
1302
|
+
"why": "Chỉ có icon, title và action là bắt buộc. Bỏ statusLabel thì cả hàng Badge biến mất; bỏ cả description và metadata thì khung vẫn được giữ nguyên — hãy so sánh chiều cao với ô đầy đủ bên cạnh.",
|
|
1303
|
+
"wiki": {
|
|
1304
|
+
"title": "Wiki nội bộ"
|
|
1305
|
+
},
|
|
1306
|
+
"contact": {
|
|
1307
|
+
"title": "Đầu mối liên hệ hỗ trợ",
|
|
1308
|
+
"description": "Nơi tiếp nhận các thắc mắc về hệ thống nội bộ."
|
|
1309
|
+
},
|
|
1310
|
+
"calendar": {
|
|
1311
|
+
"title": "Lịch toàn công ty",
|
|
1312
|
+
"description": "Chia sẻ sự kiện toàn công ty, ngày nghỉ và lịch theo từng cơ sở.",
|
|
1313
|
+
"reason": "Được công khai ở chế độ chỉ đọc."
|
|
1314
|
+
}
|
|
1315
|
+
},
|
|
1316
|
+
"logo": {
|
|
1317
|
+
"heading": "7 · logo · logo đã tải lên, và những dịch vụ chưa có logo",
|
|
1318
|
+
"why": "logo là URL của một hình ảnh do quản trị viên tải lên, và nó chỉ thay thế icon khi tải thành công. icon vẫn là bắt buộc — một dịch vụ vừa tạo chưa có logo, và một dịch vụ có URL trả về 404 cũng không có logo. Một hình ảnh bị lỗi sẽ không bao giờ vào DOM. Khung hình ảnh có cùng kích thước 20px với glyph (--card-service-launcher-icon-glyph-size), và tỉ lệ khung hình được giữ nguyên nhờ object-fit: contain.",
|
|
1319
|
+
"uploaded": {
|
|
1320
|
+
"title": "Quản lý chấm công",
|
|
1321
|
+
"description": "Dịch vụ đã tải lên logo. Huy hiệu hiển thị logo thay vì icon."
|
|
1322
|
+
},
|
|
1323
|
+
"missing": {
|
|
1324
|
+
"title": "Quản lý chấm công (chưa đặt logo)",
|
|
1325
|
+
"description": "Cùng một ô nhưng không truyền logo. icon vẫn giữ nguyên, và kích thước huy hiệu cũng không đổi."
|
|
1326
|
+
},
|
|
1327
|
+
"broken": {
|
|
1328
|
+
"title": "Nền tảng tích hợp dữ liệu (logo lỗi 404)",
|
|
1329
|
+
"description": "logo_url đã được đặt nhưng tệp không tồn tại. Ô không bị trống mà quay về dùng icon."
|
|
1330
|
+
},
|
|
1331
|
+
"wideMark": {
|
|
1332
|
+
"title": "Quyết toán chi phí",
|
|
1333
|
+
"description": "Logo dạng chữ nằm ngang 240×96. Vì dùng contain nên không bị cắt xén, và chiều rộng khung vẫn giữ nguyên như glyph."
|
|
1334
|
+
},
|
|
1335
|
+
"withReason": {
|
|
1336
|
+
"title": "Quản lý quyền truy cập",
|
|
1337
|
+
"description": "logo và disabledReason là hai trục độc lập. Trạng thái được truyền tải qua Badge và văn bản lý do — chúng ta không tự ý để màu thương hiệu của bên khác gánh vai trò đó.",
|
|
1338
|
+
"reason": "Cần có vai trò quản trị viên để xem."
|
|
1339
|
+
},
|
|
1340
|
+
"emptyString": {
|
|
1341
|
+
"title": "Email nội bộ",
|
|
1342
|
+
"description": "logo là chuỗi rỗng. Được xử lý giống như chưa đặt, và cũng không phát sinh yêu cầu thừa."
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1179
1346
|
}
|
|
1180
1347
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* THE ONE image-load probe in this library — do not write a second one.
|
|
4
|
+
*
|
|
5
|
+
* It was born inside `Avatar` (a mark with an initials fallback) and was lifted out whole when
|
|
6
|
+
* `ServiceLauncherCard` needed exactly the same question answered for an uploaded service logo
|
|
7
|
+
* (gh#850). Two components asking "did this URL actually produce a picture?" must not answer it
|
|
8
|
+
* two different ways, or the failure path diverges where nobody looks: on the 404.
|
|
9
|
+
*
|
|
10
|
+
* React Aria has no avatar, so this is hand-built — but the part worth building is not a `<span>`
|
|
11
|
+
* wrapper, it is the rule `@radix-ui/react-avatar` holds: the image is drawn ONLY once it has
|
|
12
|
+
* finished loading, and the fallback is drawn while it has not, or has failed.
|
|
13
|
+
*
|
|
14
|
+
* The probe is a DETACHED `new Image()`, not `onLoad`/`onError` on the element being rendered: that
|
|
15
|
+
* element only exists once the status is already `loaded`, so a handler on it can never report
|
|
16
|
+
* "loading" or "error". It is also why a broken `<img>` is never in the DOM for the browser to
|
|
17
|
+
* paint its torn-page glyph over the fallback.
|
|
18
|
+
*/
|
|
19
|
+
export type ImageLoadingStatus = "idle" | "loading" | "loaded" | "error";
|
|
20
|
+
export declare function useImageLoadingStatus(src: string | undefined, { loadingStatus, setLoadingStatus, referrerPolicy, crossOrigin, }: {
|
|
21
|
+
loadingStatus: ImageLoadingStatus;
|
|
22
|
+
setLoadingStatus: React.Dispatch<React.SetStateAction<ImageLoadingStatus>>;
|
|
23
|
+
referrerPolicy?: React.HTMLAttributeReferrerPolicy;
|
|
24
|
+
crossOrigin?: "anonymous" | "use-credentials" | "";
|
|
25
|
+
}): ImageLoadingStatus;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useLayoutEffect } from "@react-aria/utils";
|
|
3
|
+
function getImageLoadingStatus(image) {
|
|
4
|
+
if (!image.complete) {
|
|
5
|
+
return "loading";
|
|
6
|
+
}
|
|
7
|
+
return image.naturalWidth > 0 ? "loaded" : "error";
|
|
8
|
+
}
|
|
9
|
+
function useImageLoadingStatus(src, {
|
|
10
|
+
loadingStatus,
|
|
11
|
+
setLoadingStatus,
|
|
12
|
+
referrerPolicy,
|
|
13
|
+
crossOrigin
|
|
14
|
+
}) {
|
|
15
|
+
useLayoutEffect(() => {
|
|
16
|
+
if (!src) {
|
|
17
|
+
setLoadingStatus("error");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const image = new window.Image();
|
|
21
|
+
const handleLoad = (event) => setLoadingStatus(getImageLoadingStatus(event.currentTarget));
|
|
22
|
+
const handleError = () => setLoadingStatus("error");
|
|
23
|
+
image.addEventListener("load", handleLoad);
|
|
24
|
+
image.addEventListener("error", handleError);
|
|
25
|
+
if (referrerPolicy) {
|
|
26
|
+
image.referrerPolicy = referrerPolicy;
|
|
27
|
+
}
|
|
28
|
+
image.crossOrigin = crossOrigin ?? null;
|
|
29
|
+
image.src = src;
|
|
30
|
+
setLoadingStatus(getImageLoadingStatus(image));
|
|
31
|
+
return () => {
|
|
32
|
+
image.removeEventListener("load", handleLoad);
|
|
33
|
+
image.removeEventListener("error", handleError);
|
|
34
|
+
setLoadingStatus("idle");
|
|
35
|
+
};
|
|
36
|
+
}, [src, crossOrigin, referrerPolicy, setLoadingStatus]);
|
|
37
|
+
return loadingStatus;
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
useImageLoadingStatus
|
|
41
|
+
};
|