@tailor-platform/app-shell 1.18.0 → 1.20.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 (55) hide show
  1. package/dist/app-shell-core.css +2 -2
  2. package/dist/app-shell.d.ts +1108 -190
  3. package/dist/app-shell.js +13090 -10783
  4. package/dist/{appshell-Y1AYCuVJ.js → appshell-B4QkQvXF.js} +519 -392
  5. package/dist/fonts/inter.css +7 -7
  6. package/dist/fonts/noto-sans-jp.css +116 -116
  7. package/dist/style-entry.d.ts +1 -656
  8. package/dist/testing.d.ts +132 -97
  9. package/dist/testing.js +1 -1
  10. package/dist/vite-plugin.d.ts +132 -97
  11. package/package.json +15 -10
  12. package/skills/app-shell-patterns/SKILL.md +38 -27
  13. package/skills/app-shell-patterns/references/api/aggregations.md +131 -0
  14. package/skills/app-shell-patterns/references/api/create-ai-gateway-client.md +32 -2
  15. package/skills/app-shell-patterns/references/api/define-module.md +5 -5
  16. package/skills/app-shell-patterns/references/api/router/use-params.md +2 -2
  17. package/skills/app-shell-patterns/references/api/use-ai-chat.md +46 -8
  18. package/skills/app-shell-patterns/references/api/use-app-shell-config.md +4 -4
  19. package/skills/app-shell-patterns/references/api/use-app-shell-data.md +5 -5
  20. package/skills/app-shell-patterns/references/api/use-app-shell.md +2 -2
  21. package/skills/app-shell-patterns/references/api/use-auth.md +3 -3
  22. package/skills/app-shell-patterns/references/api/use-route-error.md +12 -12
  23. package/skills/app-shell-patterns/references/components/action-panel.md +57 -23
  24. package/skills/app-shell-patterns/references/components/activity-card.md +3 -3
  25. package/skills/app-shell-patterns/references/components/ai-chat.md +37 -3
  26. package/skills/app-shell-patterns/references/components/app-shell.md +6 -5
  27. package/skills/app-shell-patterns/references/components/badge.md +7 -3
  28. package/skills/app-shell-patterns/references/components/card.md +2 -2
  29. package/skills/app-shell-patterns/references/components/data-table.md +514 -77
  30. package/skills/app-shell-patterns/references/components/date-picker.md +2 -2
  31. package/skills/app-shell-patterns/references/components/default-sidebar.md +2 -2
  32. package/skills/app-shell-patterns/references/components/dialog.md +8 -0
  33. package/skills/app-shell-patterns/references/components/filter-rail.md +10 -3
  34. package/skills/app-shell-patterns/references/components/form.md +19 -9
  35. package/skills/app-shell-patterns/references/components/global-header-layout.md +31 -8
  36. package/skills/app-shell-patterns/references/components/item-list.md +191 -0
  37. package/skills/app-shell-patterns/references/components/layout.md +24 -17
  38. package/skills/app-shell-patterns/references/components/select.md +18 -0
  39. package/skills/app-shell-patterns/references/components/sidebar-group.md +9 -7
  40. package/skills/app-shell-patterns/references/components/sidebar-layout.md +74 -11
  41. package/skills/app-shell-patterns/references/components/sidebar-section.md +8 -8
  42. package/skills/app-shell-patterns/references/components/tabs.md +32 -2
  43. package/skills/app-shell-patterns/references/components/text.md +75 -38
  44. package/skills/app-shell-patterns/references/components/tree.md +342 -0
  45. package/skills/app-shell-patterns/references/components/with-guard.md +5 -5
  46. package/skills/app-shell-patterns/references/concepts/authentication.md +1 -1
  47. package/skills/app-shell-patterns/references/concepts/routing-navigation.md +3 -3
  48. package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +16 -14
  49. package/skills/app-shell-patterns/references/concepts/styling-theming.md +28 -20
  50. package/skills/app-shell-patterns/references/migrations.md +22 -2
  51. package/skills/app-shell-patterns/references/pages/document-detail.md +32 -25
  52. package/skills/app-shell-patterns/references/patterns/form-modal.md +4 -1
  53. package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +1 -1
  54. package/dist/semantic-contrast.test.ts +0 -227
  55. package/dist/theme-bridge.test.ts +0 -182
@@ -0,0 +1,131 @@
1
+ ---
2
+ title: Value aggregations
3
+ description: Reusable selector-based sum, average, minimum, maximum, and non-null count over row collections, with optional result-only transformation
4
+ ---
5
+
6
+ <!-- GENERATED by docs-kit from docs-src/hooks/aggregations.docs.outline.md — do not edit by hand. -->
7
+
8
+ # Value aggregations
9
+
10
+ `aggregations` creates reusable callbacks that select values from a row collection and aggregate them. Each callback accepts `{ rows: readonly TRow[] }`; the module has no dependency on React, DataTable, or column definitions.
11
+
12
+ ```tsx
13
+ import { aggregations, type Aggregation } from "@tailor-platform/app-shell";
14
+
15
+ type Line = { quantity: number | null };
16
+
17
+ const sum = aggregations.sum((row: Line) => row.quantity);
18
+ sum({ rows: [{ quantity: 1 }, { quantity: 2 }, { quantity: null }] }); // 3
19
+ sum({ rows: [{ quantity: 4 }, { quantity: 5 }] }); // 9: no previous aggregate state
20
+
21
+ const summary: Aggregation<Line, string> = aggregations.sum(
22
+ (row: Line) => row.quantity,
23
+ (value) => (value === null ? "Unavailable" : `${value} units`),
24
+ );
25
+ summary({ rows: [{ quantity: 1 }, { quantity: 2 }] }); // "3 units"
26
+ ```
27
+
28
+ ## Operations
29
+
30
+ | Factory | Result | Empty or all-nullish selected values |
31
+ | ------------------------------ | --------------------------------------- | ------------------------------------ |
32
+ | `aggregations.sum(selector)` | Sum of finite numbers | `0` |
33
+ | `aggregations.avg(selector)` | Average of finite numbers | `null` |
34
+ | `aggregations.min(selector)` | Minimum finite number | `null` |
35
+ | `aggregations.max(selector)` | Maximum finite number | `null` |
36
+ | `aggregations.count(selector)` | Count of non-nullish values of any type | `0` |
37
+
38
+ A selector is required. It receives one row at a time and determines the values to aggregate. Numeric selectors must return `number | null | undefined`; count selectors may return any type. There is no automatic field lookup or connection to a column's `id`, `accessor`, or rendered content.
39
+
40
+ Each factory returns an `Aggregation<TRow, TResult>` callback. Pass a read-only or mutable row array through `{ rows }`; aggregation does not mutate it or retain state between invocations. Selectors and transformers must be pure and must not mutate rows. Exceptions from either callback propagate to the caller.
41
+
42
+ All operations skip selected `null` and `undefined` values. Numeric operations also validate selected values at runtime and return `null` for the entire result if any other value is not a finite JavaScript number, including numeric strings, booleans, dates, `NaN`, and infinities. They do not coerce values, compare dates or strings, or return a partial result. Average divides by the number of non-nullish numeric values, not the row count.
43
+
44
+ Calculations use ordinary JavaScript numbers. A non-finite calculated result returns `null`; for average, this includes an intermediate sum overflow even when the mathematical average could be finite. These functions do not provide arbitrary-precision arithmetic.
45
+
46
+ `count(selector)` counts selected `0`, `false`, and `""` as present values. It counts non-nullish values, not truthy values; use `rows.length` when every row should count.
47
+
48
+ ## Optional transformer
49
+
50
+ Pass a second function to transform the final result:
51
+
52
+ ```tsx
53
+ const averageText = aggregations.avg(
54
+ (row: Line) => row.quantity,
55
+ (value) => value?.toFixed(2) ?? "–",
56
+ );
57
+ averageText({ rows: [{ quantity: 1 }, { quantity: null }, { quantity: 2 }] }); // "1.50"
58
+
59
+ const detailed = aggregations.sum(
60
+ (row: Line) => row.quantity,
61
+ (total) => ({ total, valid: total !== null }),
62
+ );
63
+ detailed({ rows: [{ quantity: 1 }, { quantity: 2 }] }); // { total: 3, valid: true }
64
+ ```
65
+
66
+ The transformer is called exactly once per aggregation invocation, including for empty or invalid input. Numeric transformers receive `number | null`; count transformers receive `number`. Its return type becomes the callback's result type, and may be text, JSX, an object, or another value. The aggregation library does not interpret the transformed result.
67
+
68
+ A transformer receives only the result, not rows, selected values, columns, or UI context. Filter `rows` before invoking the callback when the calculation needs a different input set.
69
+
70
+ For detached callbacks, annotate the selector's row parameter as above so the row and transformed result types can both be inferred. Row types are not restricted to objects; for example, `aggregations.sum<number>((value) => value)({ rows: [1, 2] })` returns `3`.
71
+
72
+ ## Composing with business validation
73
+
74
+ An aggregation is an ordinary callback, so a wrapper can validate the supplied rows before calling it. Keep business rules in the wrapper, numeric calculation in the aggregation, and result formatting in the transformer. The transformer is not an error handler: it receives `null` for invalid numeric results but has no row context or business-error details; selector exceptions still propagate.
75
+
76
+ For example, reject mixed currencies before adding monetary amounts:
77
+
78
+ ```tsx
79
+ type MoneyLine = { amount: number | null; currency: string };
80
+
81
+ const sumAmount = aggregations.sum(
82
+ (row: MoneyLine) => row.amount,
83
+ (total) => (total === null ? "Invalid amount" : total.toLocaleString("en-US")),
84
+ );
85
+
86
+ const moneyFooter = (ctx: { rows: readonly MoneyLine[] }) => {
87
+ const currencies = new Set(
88
+ ctx.rows.filter((row) => row.amount != null).map((row) => row.currency),
89
+ );
90
+ if (currencies.size > 1) {
91
+ return `Mixed currencies: ${[...currencies].join(", ")}`;
92
+ }
93
+ return sumAmount(ctx);
94
+ };
95
+
96
+ moneyFooter({
97
+ rows: [
98
+ { amount: 1000, currency: "JPY" },
99
+ { amount: 500, currency: "JPY" },
100
+ ],
101
+ }); // "1,500"
102
+ moneyFooter({
103
+ rows: [
104
+ { amount: 1000, currency: "JPY" },
105
+ { amount: 500, currency: "USD" },
106
+ ],
107
+ }); // "Mixed currencies: JPY, USD"
108
+ ```
109
+
110
+ The guard ignores nullish amounts because they do not contribute to the sum. A rejected currency combination returns directly without evaluating the selector or transformer. Neither the wrapper nor the helper converts currencies or provides arbitrary-precision arithmetic.
111
+
112
+ Pass `moneyFooter` directly as a column's `footer`. See the [runnable DataTable example](../components/data-table.md#composing-with-business-validation) to toggle mixed currencies and observe the composed behavior.
113
+
114
+ ## DataTable integration
115
+
116
+ Pass an aggregation callback directly to a column footer. It is an ordinary footer renderer that reads the current `ctx.rows`; DataTable needs no aggregation-specific handling:
117
+
118
+ ```tsx
119
+ import { aggregations, createColumnHelper } from "@tailor-platform/app-shell";
120
+
121
+ const { column } = createColumnHelper<{ quantity: number | null }>();
122
+ column({
123
+ id: "quantity",
124
+ type: "number",
125
+ footer: aggregations.sum((row) => row.quantity),
126
+ });
127
+ ```
128
+
129
+ Inline selectors infer the row type from the surrounding footer definition. The selector is the source of truth even when the column has an `accessor`; write the same calculation in the selector if the footer should aggregate a computed cell value.
130
+
131
+ Use a transformer for simple footer text or JSX. DataTable only accepts callback results it can render; object-producing aggregations remain available for non-UI use. For the current-page scope, loading behavior, and custom renderers, see [DataTable column footers](../components/data-table.md#column-footers).
@@ -7,7 +7,7 @@ description: Create a low-level AI Gateway client that reuses AppShell authentic
7
7
 
8
8
  # createAIGatewayClient
9
9
 
10
- Creates a small AI Gateway transport client for text-only chat completions.
10
+ Creates a small AI Gateway transport client for chat completions and local function tool calls.
11
11
 
12
12
  ## Signature
13
13
 
@@ -43,6 +43,7 @@ interface AIGatewayClient {
43
43
  The iterable yields completion events:
44
44
 
45
45
  - `text-delta` — append `event.text` to build the assistant response
46
+ - `tool-call` — a local function tool call requested by the model
46
47
  - `done` — terminal event with an optional `finishReason`
47
48
 
48
49
  ## Related Types
@@ -56,11 +57,33 @@ type AIGatewayChatMessage =
56
57
  | {
57
58
  role: "assistant";
58
59
  content?: string;
60
+ toolCalls?: AIGatewayToolCall[];
61
+ }
62
+ | {
63
+ role: "tool";
64
+ toolCallId: string;
65
+ content: string;
59
66
  };
60
67
 
68
+ interface AIGatewayToolCall {
69
+ id: string;
70
+ name: string;
71
+ argumentsText: string;
72
+ }
73
+
74
+ interface AIGatewayFunctionTool {
75
+ type: "function";
76
+ function: {
77
+ name: string;
78
+ description?: string;
79
+ parameters: Record<string, unknown>;
80
+ };
81
+ }
82
+
61
83
  interface AIGatewayChatRequest {
62
84
  model: string;
63
85
  messages: AIGatewayChatMessage[];
86
+ tools?: AIGatewayFunctionTool[];
64
87
  signal?: AbortSignal;
65
88
  }
66
89
 
@@ -69,6 +92,12 @@ type AIChatCompletionEvent =
69
92
  type: "text-delta";
70
93
  text: string;
71
94
  }
95
+ | {
96
+ type: "tool-call";
97
+ toolCallId: string;
98
+ toolName: string;
99
+ argumentsText: string;
100
+ }
72
101
  | {
73
102
  type: "done";
74
103
  finishReason?: string;
@@ -106,7 +135,8 @@ console.log(text);
106
135
  ## Notes
107
136
 
108
137
  - AppShell chooses the appropriate AI Gateway transport automatically
109
- - The low-level API is intentionally narrow: text deltas plus completion metadata
138
+ - Local tools are sent as normalized `type: "function"` definitions; provider-native tools are not supported
139
+ - The low-level API stays event-based so hooks can layer streaming UI and tool loops on top
110
140
  - `request.signal` is passed through so callers can abort in-flight work
111
141
 
112
142
  ## Related
@@ -100,11 +100,11 @@ A module object that can be passed to the `modules` prop of `AppShell`.
100
100
  ### Basic Module
101
101
 
102
102
  ```typescript
103
- import { defineModule } from "@tailor-platform/app-shell";
103
+ import { defineModule, Text } from "@tailor-platform/app-shell";
104
104
 
105
105
  const dashboardModule = defineModule({
106
106
  path: "dashboard",
107
- component: () => <h1>Dashboard</h1>,
107
+ component: () => <Text.h1>Dashboard</Text.h1>,
108
108
  resources: [],
109
109
  });
110
110
  ```
@@ -183,14 +183,14 @@ const adminModule = defineModule({
183
183
  ### Module with Error Boundary
184
184
 
185
185
  ```typescript
186
- import { useRouteError } from "@tailor-platform/app-shell";
186
+ import { Text, useRouteError } from "@tailor-platform/app-shell";
187
187
 
188
188
  const ErrorBoundary = () => {
189
189
  const error = useRouteError() as Error;
190
190
  return (
191
191
  <div>
192
- <h1>Module Error</h1>
193
- <p>{error.message}</p>
192
+ <Text.h1>Module Error</Text.h1>
193
+ <Text.p>{error.message}</Text.p>
194
194
  </div>
195
195
  );
196
196
  };
@@ -182,7 +182,7 @@ function ProductEdit() {
182
182
 
183
183
  return (
184
184
  <form>
185
- <h1>Edit Product {id}</h1>
185
+ <Text.h1>Edit Product {id}</Text.h1>
186
186
  {/* form fields */}
187
187
  </form>
188
188
  );
@@ -203,7 +203,7 @@ function DeleteConfirmation() {
203
203
 
204
204
  return (
205
205
  <div>
206
- <p>Delete product {id}?</p>
206
+ <Text.p>Delete product {id}?</Text.p>
207
207
  <button onClick={handleDelete}>Confirm</button>
208
208
  </div>
209
209
  );
@@ -1,19 +1,24 @@
1
1
  ---
2
2
  title: useAIChat
3
- description: Simple text-only chat hook for AI Gateway
3
+ description: AI Gateway chat hook with optional local tools
4
4
  ---
5
5
 
6
6
  <!-- GENERATED by docs-kit from docs-src/hooks/use-ai-chat.docs.outline.md — do not edit by hand. -->
7
7
 
8
8
  # useAIChat
9
9
 
10
- React hook for simple text-only chat on top of `createAIGatewayClient`.
10
+ React hook for AI Gateway chat on top of `createAIGatewayClient`, with optional local tool support.
11
11
 
12
12
  ## Signature
13
13
 
14
14
  ```typescript
15
- const useAIChat: (config: { client: AIGatewayClient; model: string }) => {
15
+ const useAIChat: (config: {
16
+ client: AIGatewayClient;
17
+ model: string;
18
+ tools?: Record<string, AILocalTool>;
19
+ }) => {
16
20
  messages: AIChatMessage[];
21
+ toolCalls: { id: string; name: string }[];
17
22
  status: "ready" | "submitted" | "streaming" | "error";
18
23
  error?: Error;
19
24
  sendMessage: (message: string) => Promise<boolean>;
@@ -33,10 +38,17 @@ interface AIChatMessage {
33
38
  }
34
39
  ```
35
40
 
41
+ ### `toolCalls`
42
+
43
+ - **Type:** `{ id: string; name: string }[]`
44
+ - **Description:** Tool calls requested by the model in the active flow, including earlier tool rounds of the current request. This transient list is cleared when the response finishes, is stopped, or fails; it is never added to `messages` or conversation history.
45
+
46
+ Calls become available before local tool execution. They record requested tool names, not successful execution. Arguments and results remain internal.
47
+
36
48
  ### `status`
37
49
 
38
50
  - **Type:** `"ready" | "submitted" | "streaming" | "error"`
39
- - **Description:** Current request state
51
+ - **Description:** Current request state. `submitted` covers waiting for the first text, local tool execution, and waiting for the model's next reply after a tool result. `streaming` begins when text arrives. `AIChat.Conversation` automatically shows a waiting indicator while `submitted`.
40
52
 
41
53
  ### `error`
42
54
 
@@ -54,10 +66,20 @@ interface AIChatMessage {
54
66
  - **Type:** `() => void`
55
67
  - **Description:** Aborts the current request if one is in progress
56
68
 
69
+ ### `tools`
70
+
71
+ Register local tools under a single object using `defineAIChatTool(...)`. Their schemas must implement both [Standard Schema](https://standardschema.dev/) validation and Standard JSON Schema generation (for example, Zod 4). Provider-native tools are not supported.
72
+
57
73
  ## Usage
58
74
 
59
75
  ```tsx
60
- import { createAuthClient, createAIGatewayClient, useAIChat } from "@tailor-platform/app-shell";
76
+ import {
77
+ createAuthClient,
78
+ createAIGatewayClient,
79
+ defineAIChatTool,
80
+ useAIChat,
81
+ } from "@tailor-platform/app-shell";
82
+ import { z } from "zod/v4";
61
83
 
62
84
  const authClient = createAuthClient({
63
85
  clientId: "your-client-id",
@@ -69,10 +91,23 @@ const aiClient = createAIGatewayClient({
69
91
  authClient,
70
92
  });
71
93
 
94
+ const lookupCustomer = defineAIChatTool({
95
+ description: "Look up a customer in the current workspace",
96
+ schema: z.object({
97
+ customerId: z.string(),
98
+ }),
99
+ async execute({ customerId }) {
100
+ return { customerId, name: "Acme Corp" };
101
+ },
102
+ });
103
+
72
104
  export function ChatScreen() {
73
- const { messages, sendMessage, status, stop, error } = useAIChat({
105
+ const { messages, toolCalls, sendMessage, status, stop, error } = useAIChat({
74
106
  client: aiClient,
75
107
  model: "gpt-5-mini",
108
+ tools: {
109
+ lookupCustomer,
110
+ },
76
111
  });
77
112
 
78
113
  return (
@@ -82,6 +117,9 @@ export function ChatScreen() {
82
117
  {message.role}: {message.content}
83
118
  </div>
84
119
  ))}
120
+ {toolCalls.map((call) => (
121
+ <div key={call.id}>Tool call: {call.name}</div>
122
+ ))}
85
123
 
86
124
  <button
87
125
  onClick={() => void sendMessage("Hello")}
@@ -101,9 +139,9 @@ export function ChatScreen() {
101
139
  ## Notes
102
140
 
103
141
  - AppShell chooses the appropriate AI Gateway transport automatically
104
- - The hook is intentionally text-only
142
+ - Public messages stay user/assistant text-first; tool calls and results never become conversation messages
105
143
  - System prompts and custom history shaping should use the low-level client directly
106
- - `stop()` keeps any already-streamed assistant text and ignores late chunks from the stopped request
144
+ - `stop()` keeps already-streamed assistant text, clears the active `toolCalls`, and ignores late events from the stopped request
107
145
 
108
146
  ## Related
109
147
 
@@ -56,15 +56,15 @@ const useAppShellConfig: () => {
56
56
  ### Display App Info
57
57
 
58
58
  ```typescript
59
- import { useAppShellConfig } from "@tailor-platform/app-shell";
59
+ import { Text, useAppShellConfig } from "@tailor-platform/app-shell";
60
60
 
61
61
  function AppInfo() {
62
62
  const { title, configurations } = useAppShellConfig();
63
63
 
64
64
  return (
65
65
  <div>
66
- <h2>{title}</h2>
67
- <p>Base: {configurations.basePath}</p>
66
+ <Text.h2>{title}</Text.h2>
67
+ <Text.p>Base: {configurations.basePath}</Text.p>
68
68
  </div>
69
69
  );
70
70
  }
@@ -96,7 +96,7 @@ function SettingsMenu() {
96
96
 
97
97
  return (
98
98
  <div>
99
- <h3>Settings</h3>
99
+ <Text.h3>Settings</Text.h3>
100
100
  {configurations.settingsResources.map((resource) => (
101
101
  <button key={resource.path}>{resource.path}</button>
102
102
  ))}
@@ -28,15 +28,15 @@ const useAppShellData: () => {
28
28
  ### Basic Access
29
29
 
30
30
  ```typescript
31
- import { useAppShellData } from "@tailor-platform/app-shell";
31
+ import { Text, useAppShellData } from "@tailor-platform/app-shell";
32
32
 
33
33
  function UserProfile() {
34
34
  const { contextData } = useAppShellData();
35
35
 
36
36
  return (
37
37
  <div>
38
- <p>Welcome, {contextData.currentUser?.name}</p>
39
- <p>Role: {contextData.currentUser?.role}</p>
38
+ <Text.p>Welcome, {contextData.currentUser?.name}</Text.p>
39
+ <Text.p>Role: {contextData.currentUser?.role}</Text.p>
40
40
  </div>
41
41
  );
42
42
  }
@@ -54,8 +54,8 @@ function UserInfo() {
54
54
 
55
55
  return (
56
56
  <div>
57
- <h2>{contextData.currentUser.name}</h2>
58
- <p>{contextData.currentUser.email}</p>
57
+ <Text.h2>{contextData.currentUser.name}</Text.h2>
58
+ <Text.p>{contextData.currentUser.email}</Text.p>
59
59
  </div>
60
60
  );
61
61
  }
@@ -120,8 +120,8 @@ function UserInfo() {
120
120
 
121
121
  return (
122
122
  <div>
123
- <h2>{contextData.currentUser.name}</h2>
124
- <p>{contextData.currentUser.email}</p>
123
+ <Text.h2>{contextData.currentUser.name}</Text.h2>
124
+ <Text.p>{contextData.currentUser.email}</Text.p>
125
125
  </div>
126
126
  );
127
127
  }
@@ -59,7 +59,7 @@ const useAuth: () => {
59
59
  ### Basic Authentication Flow
60
60
 
61
61
  ```typescript
62
- import { useAuth } from "@tailor-platform/app-shell";
62
+ import { Text, useAuth } from "@tailor-platform/app-shell";
63
63
 
64
64
  function MyComponent() {
65
65
  const { isAuthenticated, isReady, login, logout } = useAuth();
@@ -74,7 +74,7 @@ function MyComponent() {
74
74
 
75
75
  return (
76
76
  <div>
77
- <p>You are logged in!</p>
77
+ <Text.p>You are logged in!</Text.p>
78
78
  <button onClick={logout}>Log Out</button>
79
79
  </div>
80
80
  );
@@ -108,7 +108,7 @@ function AuthStatus() {
108
108
  if (error) {
109
109
  return (
110
110
  <div>
111
- <p>Authentication failed: {error}</p>
111
+ <Text.p>Authentication failed: {error}</Text.p>
112
112
  <button onClick={login}>Retry</button>
113
113
  </div>
114
114
  );
@@ -26,7 +26,7 @@ const useRouteError: () => unknown;
26
26
  ### Basic Error Boundary
27
27
 
28
28
  ```typescript
29
- import { useRouteError } from "@tailor-platform/app-shell";
29
+ import { Text, useRouteError } from "@tailor-platform/app-shell";
30
30
 
31
31
  function ErrorBoundary() {
32
32
  const error = useRouteError();
@@ -37,8 +37,8 @@ function ErrorBoundary() {
37
37
 
38
38
  return (
39
39
  <div>
40
- <h1>Something went wrong</h1>
41
- <p>{message}</p>
40
+ <Text.h1>Something went wrong</Text.h1>
41
+ <Text.p>{message}</Text.p>
42
42
  <button onClick={() => window.location.reload()}>
43
43
  Try Again
44
44
  </button>
@@ -79,7 +79,7 @@ const productsModule = defineModule({
79
79
  ### Advanced Error Handling
80
80
 
81
81
  ```typescript
82
- import { isRouteErrorResponse, useRouteError } from "@tailor-platform/app-shell";
82
+ import { isRouteErrorResponse, Text, useRouteError } from "@tailor-platform/app-shell";
83
83
 
84
84
  function ErrorBoundary() {
85
85
  const error = useRouteError();
@@ -88,8 +88,8 @@ function ErrorBoundary() {
88
88
  if (isRouteErrorResponse(error)) {
89
89
  return (
90
90
  <div>
91
- <h1>{error.status} {error.statusText}</h1>
92
- <p>{error.data}</p>
91
+ <Text.h1>{error.status} {error.statusText}</Text.h1>
92
+ <Text.p>{error.data}</Text.p>
93
93
  </div>
94
94
  );
95
95
  }
@@ -98,8 +98,8 @@ function ErrorBoundary() {
98
98
  if (error instanceof Error) {
99
99
  return (
100
100
  <div>
101
- <h1>Error</h1>
102
- <p>{error.message}</p>
101
+ <Text.h1>Error</Text.h1>
102
+ <Text.p>{error.message}</Text.p>
103
103
  <pre>{error.stack}</pre>
104
104
  </div>
105
105
  );
@@ -108,8 +108,8 @@ function ErrorBoundary() {
108
108
  // Handle unknown errors
109
109
  return (
110
110
  <div>
111
- <h1>Unknown Error</h1>
112
- <p>Something went wrong</p>
111
+ <Text.h1>Unknown Error</Text.h1>
112
+ <Text.p>Something went wrong</Text.p>
113
113
  </div>
114
114
  );
115
115
  }
@@ -129,8 +129,8 @@ function ErrorBoundary() {
129
129
 
130
130
  return (
131
131
  <div>
132
- <h1>Error</h1>
133
- <p>We've been notified and will fix this soon.</p>
132
+ <Text.h1>Error</Text.h1>
133
+ <Text.p>We've been notified and will fix this soon.</Text.p>
134
134
  </div>
135
135
  );
136
136
  }
@@ -17,24 +17,30 @@ import { ActionPanel } from "@tailor-platform/app-shell";
17
17
 
18
18
  ## Basic Usage
19
19
 
20
+ <example-preview name="basic-usage"></example-preview>
21
+
20
22
  ```tsx
21
- <ActionPanel
22
- title="Actions"
23
- actions={[
24
- {
25
- key: "create-invoice",
26
- label: "Create new sales invoice",
27
- icon: <ReceiptIcon />,
28
- onClick: () => openCreateInvoiceModal(),
29
- },
30
- {
31
- key: "view-docs",
32
- label: "View documentation",
33
- icon: <ExternalLinkIcon />,
34
- onClick: () => window.open("/docs", "_blank", "noopener,noreferrer"),
35
- },
36
- ]}
37
- />
23
+ function BasicUsage() {
24
+ return (
25
+ <ActionPanel
26
+ title="Actions"
27
+ actions={[
28
+ {
29
+ key: "create-invoice",
30
+ label: "Create new sales invoice",
31
+ icon: <Receipt />,
32
+ onClick: () => {},
33
+ },
34
+ {
35
+ key: "view-docs",
36
+ label: "View documentation",
37
+ icon: <ExternalLink />,
38
+ onClick: () => {},
39
+ },
40
+ ]}
41
+ />
42
+ );
43
+ }
38
44
  ```
39
45
 
40
46
  ## Props
@@ -43,11 +49,12 @@ _Generated from the type surface — first-party props only._
43
49
 
44
50
  ### ActionPanelProps
45
51
 
46
- | Prop | Type | Default | Description |
47
- | ------------ | -------------- | ------- | --------------------------------------------- |
48
- | `actions` | `ActionItem[]` | - | List of actions to render |
49
- | `className?` | `string` | - | Additional CSS classes for the card container |
50
- | `title` | `string` | - | Card title |
52
+ | Prop | Type | Default | Description |
53
+ | --------------- | -------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
+ | `actions` | `ActionItem[]` | - | List of actions to render |
55
+ | `className?` | `string` | - | Additional CSS classes for the card container |
56
+ | `emptyContent?` | `ReactNode` | - | Content shown with consistent padding when actions is empty. Defaults to a localized "No actions available" message. Strings use the default body text styling; other React nodes render unchanged. Pass null to hide the empty content and its additional vertical spacing while keeping the card and title. |
57
+ | `title` | `string` | - | Card title |
51
58
 
52
59
  ## Exports
53
60
 
@@ -71,7 +78,34 @@ _Generated from the type surface — first-party props only._
71
78
  - Rows are rendered as `<button type="button">` elements.
72
79
  - `disabled` or `loading` makes the row non-interactive.
73
80
  - When `loading` is `true`, the icon slot renders a spinner without shifting layout.
74
- - Empty actions show a built-in fallback message: `No actions available`.
81
+ - Empty actions show `emptyContent`, which defaults to a styled message localized using the AppShell locale: `No actions available` in English or `利用可能なアクションはありません` in Japanese.
82
+
83
+ ## Custom Empty Content
84
+
85
+ Pass a string or any `ReactNode` to `emptyContent` to replace the default content when `actions` is empty. Strings use the same muted `Text` body styling as the default message. Other React nodes render directly inside `Card.Content` without a paragraph wrapper and control their own typography. `Card.Content` owns the spacing, and the title, empty content, and action icons share the same 32px horizontal inset. Action rows retain horizontal padding around their hover backgrounds and focus rings. Custom empty content is ignored when actions are present.
86
+
87
+ <example-preview name="custom-empty-content"></example-preview>
88
+
89
+ ```tsx
90
+ function CustomEmptyContent() {
91
+ return (
92
+ <ActionPanel
93
+ title="操作"
94
+ actions={[]}
95
+ emptyContent={
96
+ <div>
97
+ <Text.p className="text-muted-foreground">利用可能な操作はありません</Text.p>
98
+ <Text.p className="text-muted-foreground">
99
+ ステータスが変わると、操作が表示されます。
100
+ </Text.p>
101
+ </div>
102
+ }
103
+ />
104
+ );
105
+ }
106
+ ```
107
+
108
+ Pass `null` to hide the empty content and its additional vertical spacing while keeping the card and its title. To hide the entire panel when no actions are available, render it conditionally with `actions.length > 0`.
75
109
 
76
110
  ## With Loading State
77
111
 
@@ -105,7 +105,7 @@ import { ActivityCard, type ActivityCardBaseItem } from "@tailor-platform/app-sh
105
105
  ### Example
106
106
 
107
107
  ```tsx
108
- import { ActivityCard, type ActivityCardBaseItem } from "@tailor-platform/app-shell";
108
+ import { ActivityCard, Text, type ActivityCardBaseItem } from "@tailor-platform/app-shell";
109
109
 
110
110
  interface MyItem extends ActivityCardBaseItem {
111
111
  kind: "approval" | "update";
@@ -123,11 +123,11 @@ const items: MyItem[] = [
123
123
  {(item) =>
124
124
  item.kind === "approval" ? (
125
125
  <ActivityCard.Item indicator={<ApprovedIcon />}>
126
- <p>{item.label}</p>
126
+ <Text.p>{item.label}</Text.p>
127
127
  </ActivityCard.Item>
128
128
  ) : (
129
129
  <ActivityCard.Item>
130
- <p>{item.message}</p>
130
+ <Text.p>{item.message}</Text.p>
131
131
  </ActivityCard.Item>
132
132
  )
133
133
  }