@godxjp/ui 31.2.0 → 31.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/agent/START-HERE.md +6 -6
  2. package/agent/components/AuthExpiryProvider.json +40 -0
  3. package/agent/components/DataState.json +3 -1
  4. package/agent/components/InfiniteQueryState.json +1 -1
  5. package/agent/components/Tabs.json +5 -0
  6. package/agent/components-index.json +5 -0
  7. package/agent/components.json +49 -2
  8. package/agent/index.json +7 -7
  9. package/agent/llms.txt +6 -6
  10. package/agent/patterns/async-data-state.json +1 -1
  11. package/agent/patterns.json +1 -1
  12. package/agent/tokens.json +39 -3
  13. package/dist/components/admin/index.d.ts +2 -0
  14. package/dist/components/admin/index.js +2 -0
  15. package/dist/components/data-entry/upload.js +12 -1
  16. package/dist/components/feedback/alert.d.ts +4 -0
  17. package/dist/components/feedback/alert.js +29 -8
  18. package/dist/components/feedback/auth-expiry.d.ts +30 -0
  19. package/dist/components/feedback/auth-expiry.js +64 -0
  20. package/dist/components/feedback/index.d.ts +2 -0
  21. package/dist/components/feedback/index.js +2 -0
  22. package/dist/components/navigation/tabs.d.ts +2 -2
  23. package/dist/components/navigation/tabs.js +57 -10
  24. package/dist/components/query/data-state.js +9 -0
  25. package/dist/components/query/index.d.ts +2 -0
  26. package/dist/components/query/index.js +2 -0
  27. package/dist/components/query/infinite-query-state.js +9 -0
  28. package/dist/contracts/measurement.json +1 -1
  29. package/dist/i18n/messages/en.json +3 -0
  30. package/dist/i18n/messages/ja.json +3 -0
  31. package/dist/i18n/messages/vi.json +3 -0
  32. package/dist/props/components/feedback.prop.d.ts +13 -0
  33. package/dist/props/components/index.d.ts +1 -1
  34. package/dist/props/components/navigation.prop.d.ts +19 -1
  35. package/dist/props/registry.d.ts +11 -2
  36. package/dist/props/registry.js +13 -0
  37. package/dist/styles/alert-layout.css +16 -12
  38. package/dist/styles/control.css +18 -2
  39. package/dist/styles/data-entry-layout.css +4 -0
  40. package/dist/styles/form-layout.css +47 -5
  41. package/dist/styles/layers.json +35 -1
  42. package/dist/styles/layout.css +2 -1
  43. package/dist/tokens/components/feedback.css +2 -0
  44. package/dist/tokens/components/segmented.css +4 -0
  45. package/dist/tokens/semantic/layout.css +2 -0
  46. package/docs/FRAME-COVERAGE-REPORT.md +4 -2
  47. package/docs/data-entry/segmented.tsx +2 -0
  48. package/docs/feedback/auth-expiry.tsx +104 -0
  49. package/docs/i18n/messages/en.json +6 -0
  50. package/docs/i18n/messages/ja.json +6 -0
  51. package/docs/i18n/messages/vi.json +6 -0
  52. package/docs/navigation/tabs.tsx +43 -0
  53. package/docs/query/data-state.tsx +84 -12
  54. package/docs/showcase/table-view-tabs.tsx +15 -13
  55. package/docs/themes/flat.css +4 -0
  56. package/docs/themes/glassmorphism.css +2 -0
  57. package/docs/themes/neubrutalism.css +6 -0
  58. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
- import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
1
+ import { useState } from "react";
2
+ import { QueryClient, QueryClientProvider, useQuery, useQueryClient } from "@tanstack/react-query";
2
3
 
3
4
  import {
4
5
  Badge,
@@ -12,8 +13,9 @@ import {
12
13
  type ColumnDef,
13
14
  } from "@godxjp/ui/data-display";
14
15
  import { SkeletonTable } from "@godxjp/ui/feedback";
16
+ import { Button, Text } from "@godxjp/ui/general";
15
17
  import { Flex, PageContainer } from "@godxjp/ui/layout";
16
- import { DataState } from "@godxjp/ui/query";
18
+ import { AuthExpiryProvider, DataState } from "@godxjp/ui/query";
17
19
 
18
20
  /**
19
21
  * DataState · drives skeleton / error / empty / success for ONE useQuery block.
@@ -49,15 +51,36 @@ function SuccessBlock() {
49
51
  );
50
52
  }
51
53
 
54
+ /** Loads for 2s and comes back empty — the whole lifecycle: skeleton → EmptyState. */
55
+ const LOAD_MS = 2000;
56
+
52
57
  function LoadingBlock() {
58
+ const client = useQueryClient();
53
59
  const query = useQuery({
54
60
  queryKey: ["ds-loading"],
55
- queryFn: () => new Promise<Invoice[]>(() => {}),
61
+ queryFn: () => new Promise<Invoice[]>((resolve) => setTimeout(() => resolve([]), LOAD_MS)),
56
62
  });
57
63
  return (
58
- <DataState query={query} skeleton={<SkeletonTable />}>
59
- {(d) => <DataTable data={d} columns={columns} getRowId={(r) => r.id} />}
60
- </DataState>
64
+ <Flex direction="col" gap="sm">
65
+ <DataState
66
+ query={query}
67
+ skeleton={<SkeletonTable rows={4} columns={3} />}
68
+ isEmpty={(d) => d.length === 0}
69
+ empty={
70
+ <EmptyState title="データがありません" description="条件に合う請求書はありません。" />
71
+ }
72
+ >
73
+ {(d) => <DataTable data={d} columns={columns} getRowId={(r) => r.id} />}
74
+ </DataState>
75
+ {/* resetQueries puts the query back to pending, so the skeleton plays again. */}
76
+ <Button
77
+ variant="outline"
78
+ size="sm"
79
+ onClick={() => void client.resetQueries({ queryKey: ["ds-loading"] })}
80
+ >
81
+ もう一度読み込む
82
+ </Button>
83
+ </Flex>
61
84
  );
62
85
  }
63
86
 
@@ -99,8 +122,40 @@ function PrerequisiteBlock() {
99
122
  );
100
123
  }
101
124
 
125
+ function ExpiredSessionQuery({ queryKey }: { queryKey: string }) {
126
+ const query = useQuery<Invoice[]>({
127
+ queryKey: [queryKey],
128
+ queryFn: async () => {
129
+ throw Object.assign(new Error("Access token expired"), { status: 401 });
130
+ },
131
+ });
132
+ return (
133
+ <DataState query={query} skeleton={<SkeletonTable rows={3} />}>
134
+ {(d) => <DataTable data={d} columns={columns} getRowId={(r) => r.id} />}
135
+ </DataState>
136
+ );
137
+ }
138
+
139
+ function AuthExpiryBlock() {
140
+ // The app root owns session expiry (gh#1022). A real app redirects to the IdP here:
141
+ // window.location.assign(`/auth/login?return_to=${encodeURIComponent(location.href)}`)
142
+ // Two queries 401 at once — the handler still runs exactly once, and no alert is painted.
143
+ const [calls, setCalls] = useState(0);
144
+ return (
145
+ <AuthExpiryProvider onAuthExpired={() => setCalls((n) => n + 1)}>
146
+ <Flex direction="col" gap="md">
147
+ <Text size="sm" tone="muted">
148
+ onAuthExpired calls: {calls}
149
+ </Text>
150
+ <ExpiredSessionQuery queryKey="ds-auth-expiry-a" />
151
+ <ExpiredSessionQuery queryKey="ds-auth-expiry-b" />
152
+ </Flex>
153
+ </AuthExpiryProvider>
154
+ );
155
+ }
156
+
102
157
  function AuthErrorBlock() {
103
- // A 401 / expired token never resolves by retrying — DataState offers session renewal instead.
158
+ // Without an AuthExpiryProvider: the proportionate fallback — a neutral sign-in prompt.
104
159
  const query = useQuery<Invoice[]>({
105
160
  queryKey: ["ds-auth-error"],
106
161
  queryFn: async () => {
@@ -140,8 +195,11 @@ export default function Demo() {
140
195
 
141
196
  <Card>
142
197
  <CardHeader>
143
- <CardTitle level={2}>Loading (skeleton)</CardTitle>
144
- <CardDescription>The skeleton prop renders during the pending phase.</CardDescription>
198
+ <CardTitle level={2}>Loading → empty</CardTitle>
199
+ <CardDescription>
200
+ The skeleton (animated) renders while pending; when the data arrives empty the
201
+ skeleton goes away and the `empty` slot renders.
202
+ </CardDescription>
145
203
  </CardHeader>
146
204
  <CardContent flush>
147
205
  <LoadingBlock />
@@ -175,10 +233,24 @@ export default function Demo() {
175
233
 
176
234
  <Card>
177
235
  <CardHeader>
178
- <CardTitle level={2}>Error · auth (session renewal)</CardTitle>
236
+ <CardTitle level={2}>Session expired · AuthExpiryProvider (recommended)</CardTitle>
237
+ <CardDescription>
238
+ Mount `AuthExpiryProvider` once at the app root. A 401 calls `onAuthExpired`
239
+ automatically, once for any number of simultaneous 401s, while each DataState keeps
240
+ its skeleton and announces the redirect to sign-in through a live region.
241
+ </CardDescription>
242
+ </CardHeader>
243
+ <CardContent>
244
+ <AuthExpiryBlock />
245
+ </CardContent>
246
+ </Card>
247
+
248
+ <Card>
249
+ <CardHeader>
250
+ <CardTitle level={2}>Session expired · fallback without a provider</CardTitle>
179
251
  <CardDescription>
180
- A 401 / expired token routes to session renewal via `onAuthError`, never a blind
181
- retry, and never surfaces the raw token message.
252
+ Without a provider a 401 renders a neutral, width-capped sign-in prompt whose button
253
+ calls `onAuthError`. It never offers a blind retry or shows the raw token message.
182
254
  </CardDescription>
183
255
  </CardHeader>
184
256
  <CardContent>
@@ -367,19 +367,14 @@ const DOT_CLASS: Record<SavedView["dot"], string> = {
367
367
  neutral: "bg-muted-foreground",
368
368
  };
369
369
 
370
- // TabsTrigger renders a <button>, whose content model is phrasing content only — so the row and
371
- // the count chip take `as="span"`. Before Flex/Badge had that seam the only valid shape here was a
372
- // raw `<span className="flex …">`, i.e. utility layout.
373
- function ViewTrigger({ view, count }: { view: SavedView; count: number }) {
370
+ // TabsTrigger renders a <button>, whose content model is phrasing content only — so the row takes
371
+ // `as="span"`. The count is the trigger's own `count` slot (gh#1021), drawn exactly as the `items`
372
+ // API draws it, rather than a hand-placed Badge.
373
+ function ViewTrigger({ view }: { view: SavedView }) {
374
374
  return (
375
375
  <Flex as="span" align="center" gap="sm">
376
376
  <span aria-hidden="true" className={`size-2 rounded-full ${DOT_CLASS[view.dot]}`} />
377
377
  <span>{view.label}</span>
378
- <Badge as="span" tone="neutral" shape="pill">
379
- <Text as="span" size="2xs" weight="medium" tabular>
380
- {count}
381
- </Text>
382
- </Badge>
383
378
  </Flex>
384
379
  );
385
380
  }
@@ -432,23 +427,30 @@ export default function Demo() {
432
427
  }
433
428
  >
434
429
  <Flex direction="col" gap="md">
435
- {/* Saved-view ribbon — controlled Tabs; switching swaps the preset below. */}
436
- <Tabs value={view} onValueChange={(v) => setView(v as ViewId)}>
430
+ {/* Saved-view ribbon — controlled, strip-only Tabs (gh#1021): `controls` points the
431
+ selected tab at the grid below, which lives outside the Tabs and is never remounted. */}
432
+ <Tabs
433
+ aria-label="保存ビュー"
434
+ controls="attendance-grid"
435
+ value={view}
436
+ onValueChange={(v) => setView(v as ViewId)}
437
+ >
437
438
  <TabsList variant="line" className="h-auto w-full flex-wrap justify-start border-b">
438
439
  {VIEWS.map((v) => (
439
440
  <TabsTrigger
440
441
  key={v.id}
441
442
  value={v.id}
443
+ count={counts[v.id]}
442
444
  className="rounded-[var(--tabs-trigger-line-radius)] px-[var(--tabs-trigger-line-padding-x)] py-[var(--tabs-trigger-line-padding-y)]"
443
445
  >
444
- <ViewTrigger view={v} count={counts[v.id]} />
446
+ <ViewTrigger view={v} />
445
447
  </TabsTrigger>
446
448
  ))}
447
449
  </TabsList>
448
450
  </Tabs>
449
451
 
450
452
  {/* The grid — columns + data both come from the active saved view. */}
451
- <Card>
453
+ <Card id="attendance-grid" role="region" aria-label={active.label}>
452
454
  <CardContent flush>
453
455
  <DataTable
454
456
  data={rows}
@@ -235,7 +235,9 @@
235
235
  --conversations-item-active-background: hsl(220 20% 90%);
236
236
  --conversations-item-active-color: hsl(220 26% 12%);
237
237
  --tabs-count-active-background: hsl(220 20% 86%);
238
+ --segmented-count-selected-background: hsl(220 20% 86%);
238
239
  --tabs-count-active-color: hsl(220 26% 12%);
240
+ --segmented-count-selected-color: hsl(220 26% 12%);
239
241
  --toggle-soft-hover-background: hsl(220 16% 90%);
240
242
  --toggle-pressed-count-background: hsl(220 20% 84%);
241
243
  --toggle-pressed-count-color: hsl(220 26% 12%);
@@ -419,7 +421,9 @@
419
421
  --conversations-item-active-background: hsl(220 16% 22%);
420
422
  --conversations-item-active-color: hsl(220 20% 94%);
421
423
  --tabs-count-active-background: hsl(220 16% 26%);
424
+ --segmented-count-selected-background: hsl(220 16% 26%);
422
425
  --tabs-count-active-color: hsl(220 20% 94%);
426
+ --segmented-count-selected-color: hsl(220 20% 94%);
423
427
  --toggle-soft-hover-background: hsl(220 14% 22%);
424
428
  --toggle-pressed-count-background: hsl(220 16% 26%);
425
429
  --toggle-pressed-count-color: hsl(220 20% 94%);
@@ -613,7 +613,9 @@
613
613
  --conversations-item-active-background-alpha: 18%;
614
614
  --conversations-item-active-color: hsl(var(--glass-ink));
615
615
  --tabs-count-active-background-alpha: 24%;
616
+ --segmented-count-selected-background-alpha: 24%;
616
617
  --tabs-count-active-color: hsl(var(--glass-ink));
618
+ --segmented-count-selected-color: hsl(var(--glass-ink));
617
619
  --toggle-pressed-count-background-alpha: 26%;
618
620
  --toggle-pressed-count-color: hsl(var(--glass-ink));
619
621
  --toggle-pressed-border-color-alpha: 45%;
@@ -279,7 +279,9 @@
279
279
  --conversations-item-active-background: hsl(48 90% 78%);
280
280
  --conversations-item-active-color: hsl(0 0% 0%);
281
281
  --tabs-count-active-background: hsl(48 90% 74%);
282
+ --segmented-count-selected-background: hsl(48 90% 74%);
282
283
  --tabs-count-active-color: hsl(0 0% 0%);
284
+ --segmented-count-selected-color: hsl(0 0% 0%);
283
285
  --toggle-soft-hover-background: hsl(48 90% 84%);
284
286
  --toggle-pressed-count-background: hsl(48 90% 74%);
285
287
  --toggle-pressed-count-color: hsl(0 0% 0%);
@@ -331,6 +333,7 @@
331
333
  --mega-menu-link-hover-background: hsl(from hsl(var(--primary)) h 90% 82%);
332
334
  --conversations-item-active-background: hsl(from hsl(var(--primary)) h 90% 78%);
333
335
  --tabs-count-active-background: hsl(from hsl(var(--primary)) h 90% 74%);
336
+ --segmented-count-selected-background: hsl(from hsl(var(--primary)) h 90% 74%);
334
337
  --toggle-soft-hover-background: hsl(from hsl(var(--primary)) h 90% 84%);
335
338
  --toggle-pressed-count-background: hsl(from hsl(var(--primary)) h 90% 74%);
336
339
  }
@@ -423,7 +426,9 @@
423
426
  --conversations-item-active-background: hsl(48 90% 26%);
424
427
  --conversations-item-active-color: hsl(0 0% 100%);
425
428
  --tabs-count-active-background: hsl(48 90% 26%);
429
+ --segmented-count-selected-background: hsl(48 90% 26%);
426
430
  --tabs-count-active-color: hsl(0 0% 100%);
431
+ --segmented-count-selected-color: hsl(0 0% 100%);
427
432
  --toggle-soft-hover-background: hsl(48 90% 24%);
428
433
  --toggle-pressed-count-background: hsl(48 90% 26%);
429
434
  --toggle-pressed-count-color: hsl(0 0% 100%);
@@ -469,6 +474,7 @@
469
474
  --mega-menu-link-hover-background: hsl(from hsl(var(--primary)) h 90% 26%);
470
475
  --conversations-item-active-background: hsl(from hsl(var(--primary)) h 90% 26%);
471
476
  --tabs-count-active-background: hsl(from hsl(var(--primary)) h 90% 26%);
477
+ --segmented-count-selected-background: hsl(from hsl(var(--primary)) h 90% 26%);
472
478
  --toggle-soft-hover-background: hsl(from hsl(var(--primary)) h 90% 24%);
473
479
  --toggle-pressed-count-background: hsl(from hsl(var(--primary)) h 90% 26%);
474
480
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "31.2.0",
4
- "godxUiMcp": "31.2.0",
3
+ "version": "31.4.0",
4
+ "godxUiMcp": "31.4.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",