@godxjp/ui 33.1.1 → 33.3.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/README.md +5 -5
- package/agent/START-HERE.md +7 -7
- package/agent/components/Command.json +9 -1
- package/agent/components/ResponsiveGrid.json +2 -2
- package/agent/components/SearchForm.json +115 -0
- package/agent/components/Toolbar.json +6 -3
- package/agent/components/TranslatableInput.json +92 -0
- package/agent/components/TranslatableTextarea.json +84 -0
- package/agent/components-index.json +20 -1
- package/agent/components.json +308 -6
- package/agent/index.json +8 -8
- package/agent/llms.txt +8 -8
- package/agent/patterns/condition-builder.json +31 -0
- package/agent/patterns/data-table-page.json +1 -1
- package/agent/patterns/filter-bar-layouts.json +30 -0
- package/agent/patterns/inertia-list-page.json +1 -1
- package/agent/patterns/list-filters.json +1 -1
- package/agent/patterns/search-form.json +27 -0
- package/agent/patterns-index.json +85 -0
- package/agent/patterns.json +91 -3
- package/agent/rules.json +1 -1
- package/agent/tokens.json +12 -0
- package/dist/components/data-entry/command.d.ts +22 -1
- package/dist/components/data-entry/command.js +14 -1
- package/dist/components/data-entry/form-field.js +10 -4
- package/dist/components/data-entry/index.d.ts +4 -2
- package/dist/components/data-entry/index.js +6 -1
- package/dist/components/data-entry/translatable-input.d.ts +29 -0
- package/dist/components/data-entry/translatable-input.js +184 -0
- package/dist/components/navigation/index.d.ts +2 -0
- package/dist/components/navigation/index.js +2 -0
- package/dist/components/navigation/search-form.d.ts +31 -0
- package/dist/components/navigation/search-form.js +112 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +13 -0
- package/dist/i18n/messages/ja.json +13 -0
- package/dist/i18n/messages/vi.json +13 -0
- package/dist/lib/field-a11y.d.ts +15 -0
- package/dist/lib/field-a11y.js +13 -1
- package/dist/lib/intl-cache.d.ts +1 -0
- package/dist/lib/intl-cache.js +4 -0
- package/dist/props/components/data-entry.prop.d.ts +46 -0
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/layout.prop.d.ts +3 -1
- package/dist/props/components/navigation.prop.d.ts +43 -3
- package/dist/props/registry.d.ts +56 -0
- package/dist/props/registry.js +91 -0
- package/dist/styles/control.css +15 -0
- package/dist/styles/data-entry-layout.css +25 -0
- package/dist/styles/layers.json +29 -1
- package/dist/styles/layout.css +41 -0
- package/dist/styles.css +1 -1
- package/dist/tokens/components/data-entry.css +4 -0
- package/docs/CONSUMER-RULES.md +1 -1
- package/package.json +2 -2
- package/scripts/ui-audit.mjs +148 -3
package/agent/components.json
CHANGED
|
@@ -1123,9 +1123,9 @@
|
|
|
1123
1123
|
"type": "\"rows\" | \"columns\""
|
|
1124
1124
|
},
|
|
1125
1125
|
{
|
|
1126
|
-
"description": "antd `Row align`, spelled with Flex's FlexAlignProp values (antd `top` → \"start\"). \"stretch\" makes every cell as tall as the tallest one. Omitted, flow=\"columns\" keeps \"start\" and flow=\"rows\" keeps the grid's natural stretch.",
|
|
1126
|
+
"description": "antd `Row align`, spelled with Flex's FlexAlignProp values (antd `top` → \"start\", `middle` → \"center\"). \"center\" puts cells of different heights on one centre line (a condition row's number and +/− buttons beside taller controls). \"stretch\" makes every cell as tall as the tallest one. Omitted, flow=\"columns\" keeps \"start\" and flow=\"rows\" keeps the grid's natural stretch.",
|
|
1127
1127
|
"name": "align",
|
|
1128
|
-
"type": "\"start\" | \"stretch\""
|
|
1128
|
+
"type": "\"start\" | \"center\" | \"stretch\""
|
|
1129
1129
|
},
|
|
1130
1130
|
{
|
|
1131
1131
|
"defaultValue": "4",
|
|
@@ -7126,6 +7126,182 @@
|
|
|
7126
7126
|
"Audit log annotation — allowing an accountant to attach a plain-text explanation to a manual journal entry or adjustment record."
|
|
7127
7127
|
]
|
|
7128
7128
|
},
|
|
7129
|
+
{
|
|
7130
|
+
"example": "import { FormField, TranslatableInput, TranslatableTextarea, type TranslatableValue } from \"@godxjp/ui/data-entry\";\n\nconst [productName, setProductName] = React.useState<TranslatableValue>({ ja: \"醤油ラーメン\" });\nconst [description, setDescription] = React.useState<TranslatableValue>({});\n\n<FormField label=\"商品名\" required>\n <TranslatableInput\n locales={[\"ja\", \"en\", \"vi\"]}\n fallbackLocale=\"ja\"\n value={productName}\n onValueChange={setProductName}\n errors={{ en: \"英語名を入力してください\" }}\n />\n</FormField>\n\n<FormField label=\"説明\">\n <TranslatableTextarea locales={[\"ja\", \"en\", \"vi\"]} value={description} onValueChange={setDescription} rows={3} />\n</FormField>",
|
|
7131
|
+
"group": "data-entry",
|
|
7132
|
+
"importPath": "@godxjp/ui/data-entry",
|
|
7133
|
+
"name": "TranslatableInput",
|
|
7134
|
+
"props": [
|
|
7135
|
+
{
|
|
7136
|
+
"description": "Controlled per-locale values — `Partial<Record<string, string>>` keyed by BCP-47 tag. A missing key or `\"\"` is an empty locale.",
|
|
7137
|
+
"name": "value",
|
|
7138
|
+
"type": "TranslatableValue"
|
|
7139
|
+
},
|
|
7140
|
+
{
|
|
7141
|
+
"description": "Uncontrolled initial per-locale values.",
|
|
7142
|
+
"name": "defaultValue",
|
|
7143
|
+
"type": "TranslatableValue"
|
|
7144
|
+
},
|
|
7145
|
+
{
|
|
7146
|
+
"description": "The WHOLE map after an edit: only the active locale's key changes, every other locale is carried over untouched. The ✕ of `allowClear` empties the active locale only.",
|
|
7147
|
+
"name": "onValueChange",
|
|
7148
|
+
"type": "(value: TranslatableValue) => void"
|
|
7149
|
+
},
|
|
7150
|
+
{
|
|
7151
|
+
"description": "The BCP-47 tags the field edits, in tab order. Default: every locale registered with the app (`getRegisteredLocales()` — built-in `vi`/`en`/`ja` plus `registerLocale`). Tabs are named by `Intl.DisplayNames` in the reader's UI language; the tags stay the keys.",
|
|
7152
|
+
"name": "locales",
|
|
7153
|
+
"type": "readonly string[]"
|
|
7154
|
+
},
|
|
7155
|
+
{
|
|
7156
|
+
"description": "The locale tab selected first. Default: the AppProvider's active locale when it is in `locales`, else the fallback locale.",
|
|
7157
|
+
"name": "defaultLocale",
|
|
7158
|
+
"type": "string"
|
|
7159
|
+
},
|
|
7160
|
+
{
|
|
7161
|
+
"description": "The locale whose text an EMPTY locale shows as its placeholder. Default: the AppProvider's `fallbackLocale`, or the first of `locales` when that is not one of them.",
|
|
7162
|
+
"name": "fallbackLocale",
|
|
7163
|
+
"type": "string"
|
|
7164
|
+
},
|
|
7165
|
+
{
|
|
7166
|
+
"description": "Per-locale validation messages. Each locale that has one gets an error mark on its tab — a glyph plus screen-reader text («Vietnamese, has an error»), never colour alone. The ACTIVE locale's message sets `aria-invalid` and is shown in the enclosing FormField's own error line (wired by `aria-errormessage`); without a FormField it is shown under the field.",
|
|
7167
|
+
"name": "errors",
|
|
7168
|
+
"type": "Partial<Record<string, ReactNode>>"
|
|
7169
|
+
},
|
|
7170
|
+
{
|
|
7171
|
+
"defaultValue": "\"md\"",
|
|
7172
|
+
"description": "The field's control tier, as on Input. The tab strip uses the Tabs `sm` tier (`md` for an `lg` field).",
|
|
7173
|
+
"name": "size",
|
|
7174
|
+
"type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
|
|
7175
|
+
},
|
|
7176
|
+
{
|
|
7177
|
+
"description": "Shown while the active locale is empty and there is no fallback text to show (the fallback locale itself, or an empty fallback).",
|
|
7178
|
+
"name": "placeholder",
|
|
7179
|
+
"type": "string"
|
|
7180
|
+
},
|
|
7181
|
+
{
|
|
7182
|
+
"description": "Disables the control; the locale tabs stay operable so every locale can still be read.",
|
|
7183
|
+
"name": "disabled",
|
|
7184
|
+
"type": "boolean"
|
|
7185
|
+
},
|
|
7186
|
+
{
|
|
7187
|
+
"description": "Lands on the control, so a FormField label click focuses it.",
|
|
7188
|
+
"name": "id",
|
|
7189
|
+
"type": "string"
|
|
7190
|
+
},
|
|
7191
|
+
{
|
|
7192
|
+
"description": "Root (strip + field) class override.",
|
|
7193
|
+
"name": "className",
|
|
7194
|
+
"type": "string"
|
|
7195
|
+
}
|
|
7196
|
+
],
|
|
7197
|
+
"related": [
|
|
7198
|
+
"TranslatableTextarea — the same contract over a Textarea.",
|
|
7199
|
+
"Input — the control it composes; every Input prop applies.",
|
|
7200
|
+
"FormField — the label / helper / error wiring it reports into.",
|
|
7201
|
+
"Tabs — the switcher it composes (strip-only, `controls` → the field's panel).",
|
|
7202
|
+
"AppSettingPicker kind=\"locale\" — chooses the UI language, not per-locale content."
|
|
7203
|
+
],
|
|
7204
|
+
"rules": [],
|
|
7205
|
+
"storyPath": "data-entry/translatable-input.tsx",
|
|
7206
|
+
"tagline": "ONE text field holding a value per locale (`{ ja, en, vi }`), switched by locale tabs above it (gh#1336). For user content stored per locale — a product name, a category label, a menu description (Astrotomic / Spatie `*_translations`). Locales, first tab and fallback come from AppProvider; an empty locale shows the fallback locale's text as its placeholder; a locale with an error is marked on its tab. Every other `Input` prop applies to the one control.",
|
|
7207
|
+
"usage": [
|
|
7208
|
+
"DO wrap it in FormField: the label names the control together with the active locale («商品名 English») and names the tab strip («商品名 Language»), `required`/`helper` reach the control, and the active locale's error renders in FormField's error line.",
|
|
7209
|
+
"DO pass the per-locale error map from your validator as `errors` (e.g. Laravel `name.ja` → `errors.ja`). Don't pass the same message to FormField `error` as well: FormField's own `error` wins over the reported one, and it would mark the field invalid on every locale.",
|
|
7210
|
+
"DO set `fallbackLocale` to the content's SOURCE language (often `ja`) when it differs from the AppProvider's UI fallback — the placeholder of an untranslated locale then shows the source text to translate from.",
|
|
7211
|
+
"DON'T build a local wrapper of Tabs + Input for this: the tab↔panel ARIA, the lang attribute, the error marks and the FormField wiring are what this component exists for.",
|
|
7212
|
+
"DON'T use it for a choice of language that IS the form's value (a user's preferred language): that is `AppSettingPicker kind=\"locale\"` or a Select.",
|
|
7213
|
+
"The locale switcher is the WAI-ARIA APG tabs pattern (tablist → one tabpanel holding the field), not a Segmented radio group: the locale is not form state — every locale is submitted together — the tab only chooses which locale the one field shows."
|
|
7214
|
+
],
|
|
7215
|
+
"useCases": [
|
|
7216
|
+
"Product / menu item name in ja, en and vi on an admin edit form",
|
|
7217
|
+
"Category or section label stored per locale (Astrotomic `*_translations`)",
|
|
7218
|
+
"Translated description or recipe text (TranslatableTextarea)"
|
|
7219
|
+
]
|
|
7220
|
+
},
|
|
7221
|
+
{
|
|
7222
|
+
"example": "import { FormField, TranslatableTextarea } from \"@godxjp/ui/data-entry\";\n\n<FormField label=\"説明\">\n <TranslatableTextarea locales={[\"ja\", \"en\", \"vi\"]} value={description} onValueChange={setDescription} autoGrow minRows={3} />\n</FormField>",
|
|
7223
|
+
"group": "data-entry",
|
|
7224
|
+
"importPath": "@godxjp/ui/data-entry",
|
|
7225
|
+
"name": "TranslatableTextarea",
|
|
7226
|
+
"props": [
|
|
7227
|
+
{
|
|
7228
|
+
"description": "Controlled per-locale values, keyed by BCP-47 tag.",
|
|
7229
|
+
"name": "value",
|
|
7230
|
+
"type": "TranslatableValue"
|
|
7231
|
+
},
|
|
7232
|
+
{
|
|
7233
|
+
"description": "Uncontrolled initial per-locale values.",
|
|
7234
|
+
"name": "defaultValue",
|
|
7235
|
+
"type": "TranslatableValue"
|
|
7236
|
+
},
|
|
7237
|
+
{
|
|
7238
|
+
"description": "The whole map after an edit; only the active locale's key changes.",
|
|
7239
|
+
"name": "onValueChange",
|
|
7240
|
+
"type": "(value: TranslatableValue) => void"
|
|
7241
|
+
},
|
|
7242
|
+
{
|
|
7243
|
+
"description": "The BCP-47 tags the field edits, in tab order. Default: the app's registered locales.",
|
|
7244
|
+
"name": "locales",
|
|
7245
|
+
"type": "readonly string[]"
|
|
7246
|
+
},
|
|
7247
|
+
{
|
|
7248
|
+
"description": "The locale tab selected first. Default: the AppProvider's active locale.",
|
|
7249
|
+
"name": "defaultLocale",
|
|
7250
|
+
"type": "string"
|
|
7251
|
+
},
|
|
7252
|
+
{
|
|
7253
|
+
"description": "The locale whose text an empty locale shows as its placeholder. Default: the AppProvider's `fallbackLocale`.",
|
|
7254
|
+
"name": "fallbackLocale",
|
|
7255
|
+
"type": "string"
|
|
7256
|
+
},
|
|
7257
|
+
{
|
|
7258
|
+
"description": "Per-locale messages: tab marks for each, `aria-invalid` + FormField's error line for the active one.",
|
|
7259
|
+
"name": "errors",
|
|
7260
|
+
"type": "Partial<Record<string, ReactNode>>"
|
|
7261
|
+
},
|
|
7262
|
+
{
|
|
7263
|
+
"defaultValue": "\"md\"",
|
|
7264
|
+
"description": "The field's control tier, as on Textarea.",
|
|
7265
|
+
"name": "size",
|
|
7266
|
+
"type": "\"sm\" | \"md\" | \"lg\""
|
|
7267
|
+
},
|
|
7268
|
+
{
|
|
7269
|
+
"description": "Shown while the active locale is empty and there is no fallback text.",
|
|
7270
|
+
"name": "placeholder",
|
|
7271
|
+
"type": "string"
|
|
7272
|
+
},
|
|
7273
|
+
{
|
|
7274
|
+
"description": "Disables the control; the locale tabs stay operable.",
|
|
7275
|
+
"name": "disabled",
|
|
7276
|
+
"type": "boolean"
|
|
7277
|
+
},
|
|
7278
|
+
{
|
|
7279
|
+
"description": "Lands on the control.",
|
|
7280
|
+
"name": "id",
|
|
7281
|
+
"type": "string"
|
|
7282
|
+
},
|
|
7283
|
+
{
|
|
7284
|
+
"description": "Root class override.",
|
|
7285
|
+
"name": "className",
|
|
7286
|
+
"type": "string"
|
|
7287
|
+
}
|
|
7288
|
+
],
|
|
7289
|
+
"related": [
|
|
7290
|
+
"TranslatableInput — the single-line form and the full contract.",
|
|
7291
|
+
"Textarea — the control it composes."
|
|
7292
|
+
],
|
|
7293
|
+
"rules": [],
|
|
7294
|
+
"storyPath": "data-entry/translatable-input.tsx",
|
|
7295
|
+
"tagline": "TranslatableInput over a Textarea (gh#1336): one multi-line field holding a value per locale, switched by locale tabs. Same locale, fallback-placeholder, error-mark and FormField contract; every Textarea prop (rows, autoGrow, count…) applies to the one control.",
|
|
7296
|
+
"usage": [
|
|
7297
|
+
"DO use it for per-locale multi-line content (a description, a recipe); for a single line use TranslatableInput.",
|
|
7298
|
+
"DO wrap it in FormField, exactly as TranslatableInput."
|
|
7299
|
+
],
|
|
7300
|
+
"useCases": [
|
|
7301
|
+
"Menu item description in ja, en and vi",
|
|
7302
|
+
"Recipe or notice body stored per locale"
|
|
7303
|
+
]
|
|
7304
|
+
},
|
|
7129
7305
|
{
|
|
7130
7306
|
"example": "import { Label } from \"@godxjp/ui/data-entry\";\n\n<Label htmlFor=\"stackable\">併用を許可</Label>",
|
|
7131
7307
|
"group": "data-entry",
|
|
@@ -8659,7 +8835,8 @@
|
|
|
8659
8835
|
"DataTable.Toolbar — the in-table strip for column/density/bulk-action controls; Toolbar (this) is the page-level filter strip that sits ABOVE the table.",
|
|
8660
8836
|
"SearchInput — the debounced free-text control placed as the first child of Toolbar.",
|
|
8661
8837
|
"Badge — compose Badge + a sibling icon Button to render each active-filter chip; Badge itself is a non-interactive leaf.",
|
|
8662
|
-
"Sheet — holds the filters beyond the 2–4 inline ones, opened by ONE 'More filters' Button inside the bar (pattern `list-filters`, rule #51). Not a ScrollArea: a filter strip never scrolls sideways."
|
|
8838
|
+
"Sheet — holds the filters beyond the 2–4 inline ones, opened by ONE 'More filters' Button inside the bar (pattern `list-filters`, rule #51). Not a ScrollArea: a filter strip never scrolls sideways.",
|
|
8839
|
+
"SearchForm — the submit-driven 「検索条件フォーム」 for many conditions or an explicit 「検索」 button (rule #51 decision table); FilterBar is the live strip."
|
|
8663
8840
|
],
|
|
8664
8841
|
"rules": [
|
|
8665
8842
|
23,
|
|
@@ -8674,7 +8851,7 @@
|
|
|
8674
8851
|
"FilterBar",
|
|
8675
8852
|
"FilterBarGroup"
|
|
8676
8853
|
],
|
|
8677
|
-
"tagline": "List-page filter strip — SearchInput + labelled ToolbarGroup filter slots (or the typed `search`/`filters`/`chips` model) + a clear-all affordance, optionally sticky. It WRAPS and never scrolls sideways; many filters → 2–4 inline + ONE 'More filters' Sheet (rule #51, pattern `list-filters`). `FilterBar`/`FilterBarGroup` are the same components under their alias names.",
|
|
8854
|
+
"tagline": "List-page filter strip — SearchInput + labelled ToolbarGroup filter slots (or the typed `search`/`filters`/`chips` model) + a clear-all affordance, optionally sticky. It WRAPS and never scrolls sideways; many filters → 2–4 inline + ONE 'More filters' Sheet (rule #51, pattern `list-filters`). `FilterBar`/`FilterBarGroup` are the same components under their alias names. Eleven layouts (inline, Filter menu + chips, More-filters Sheet, saved views, two rows, actions + sticky, faceted sidebar, compact in a card, date range, phone, docked panel) → pattern `filter-bar-layouts`.",
|
|
8678
8855
|
"usage": [
|
|
8679
8856
|
"DO prefer the typed model on a canonical list page — `search`/`filters`/`chips`/`onChipRemove`/`actions`/`resultCount` (+ `loading`/`disabled`/`error`) make the bar own layout, widths, chip lifecycle and keyboard order through --filter-bar-* tokens while ALL state stays consumer data. Children composition remains fully supported and unchanged when no model prop is passed.",
|
|
8680
8857
|
"DO place Toolbar ABOVE the table Card, never inside a `CardContent flush`. Put SearchInput as a direct child (it self-labels) and wrap every other control in a ToolbarGroup with a `label`.",
|
|
@@ -8682,6 +8859,7 @@
|
|
|
8682
8859
|
"DO set `sticky` for long list pages so the filters stay reachable while scrolling; if a topbar sits above the list, raise `--filter-bar-sticky-offset` so the strip parks below it.",
|
|
8683
8860
|
"DO pass `controlId` on a ToolbarGroup that wraps ONE control and give that control the same `id` — the visible caption then becomes the control's real `<label htmlFor>` (WCAG 2.5.3 label-in-name). Without it the caption only names the group wrapper and the control itself is NAMELESS (axe: select-name / button-name), so you must fall back to an `aria-label` that repeats the caption.",
|
|
8684
8861
|
"DO let the strip WRAP — cardinal rule #51 (`get_rule 51`). It stacks below 640px and wraps onto extra rows above; it never scrolls sideways, and neither may anything you wrap it in (`ScrollArea orientation=\"horizontal\"`, an `overflow-x-auto` box): the `overflow` prop and its `scroll` strategy were removed in 33.0.0 because a sideways strip hid filters off-screen and clipped the first label. When the filters are many, keep the 2–4 most-used ones inline and put the rest behind ONE 'More filters' Button (`count` = hidden filters applied) that opens a Sheet holding them as FilterBarGroup + control; every applied filter, inline or hidden, is a chip in `chips` (the chips row wraps too). Copy-paste composition: pattern `list-filters` (`get_pattern list-filters`); there is no dedicated component. Never re-implement the geometry in the page: no page-local flex/grid/width/overflow rules on a filter strip.",
|
|
8862
|
+
"DO keep filters PACKED at the start of the bar with its one gap. Never lay them out yourself in a Flex/div/Card/ResponsiveGrid of FormFields above the table, and never spread them with `justify=\"between\"|\"around\"|\"evenly\"` — the only far-end element of a filter bar is its `actions` slot (with the clear button). ui-audit `hand-rolled-filter-bar` flags both. For a layout other than one strip (saved views, a Filter menu with chips, a faceted sidebar, a docked side panel, a compact bar inside a table card, a date range with quick ranges, a phone Sheet), copy pattern `filter-bar-layouts` (docs page navigation/filter-bar-layouts).",
|
|
8685
8863
|
"DON'T build active-filter chips by nesting a Button inside a Badge (invalid markup + broken focus). Render each chip as a Badge label with a SIBLING icon Button (`aria-label` = 'clear <filter>'); a ghost `size='sm'` Button clears all.",
|
|
8686
8864
|
"DON'T hand-roll a debounced search box or a raw `<select>` — compose SearchInput and Select. Toolbar is layout + clear-all only; the controls own their own state and a11y.",
|
|
8687
8865
|
"Prefer the typed model for a canonical list page: pass `search` + `filters` + `chips` + `resultCount` and the bar owns the layout, widths, chip lifecycle, keyboard order (search → filters → children → reset → actions → chip removes) and responsive stacking through --filter-bar-* tokens. Everything stays consumer data — the bar renders state, never owns it.",
|
|
@@ -8692,7 +8870,123 @@
|
|
|
8692
8870
|
"useCases": [
|
|
8693
8871
|
"Master list screens (members, organizations, subscriptions, invoices) that need free-text search plus a few dropdown filters above a DataTable.",
|
|
8694
8872
|
"Sticky filter strip over a tall list where the filters must remain visible as the user scrolls the results.",
|
|
8695
|
-
"Filtered views that surface applied conditions as removable chips (clear-one via each chip's × Button, clear-all via `onClear`)."
|
|
8873
|
+
"Filtered views that surface applied conditions as removable chips (clear-one via each chip's × Button, clear-all via `onClear`).",
|
|
8874
|
+
"Filter layout choices — saved views filter tabs above the bar, a search-first bar with one Filter menu, a faceted filter sidebar with per-facet counts, a docked filter panel beside the table, a compact table toolbar, a date range filter with quick ranges, a phone filter Sheet: pattern `filter-bar-layouts`."
|
|
8875
|
+
]
|
|
8876
|
+
},
|
|
8877
|
+
{
|
|
8878
|
+
"absorbed": [
|
|
8879
|
+
"QueryFilter",
|
|
8880
|
+
"SearchPanel"
|
|
8881
|
+
],
|
|
8882
|
+
"docPath": "navigation/search-form",
|
|
8883
|
+
"example": "import { SearchForm } from \"@godxjp/ui/navigation\";\nimport { FormField, Input, Select, DatePicker } from \"@godxjp/ui/data-entry\";\n\n<SearchForm\n controlWidth=\"100%\"\n onSubmit={(e) => { e.preventDefault(); setApplied(draft); }}\n onReset={() => { setDraft(EMPTY); setApplied(EMPTY); }}\n summary=\"国籍: ベトナム / 状態: 実習中\"\n resultCount={rows.length}\n>\n <FormField label=\"氏名\" id=\"q-name\"><Input id=\"q-name\" value={draft.name} onChange={(e) => set({ name: e.target.value })} /></FormField>\n <FormField label=\"国籍\" id=\"q-nat\"><Select id=\"q-nat\" options={nationalities} value={draft.nationality} onValueChange={(v) => set({ nationality: v })} /></FormField>\n <FormField label=\"入国日\" id=\"q-entry\"><DatePicker id=\"q-entry\" range /></FormField>\n <FormField label=\"職種\" id=\"q-occ\"><Select id=\"q-occ\" options={occupations} value={draft.occupation} onValueChange={(v) => set({ occupation: v })} /></FormField>\n</SearchForm>",
|
|
8884
|
+
"group": "navigation",
|
|
8885
|
+
"importPath": "@godxjp/ui/navigation",
|
|
8886
|
+
"name": "SearchForm",
|
|
8887
|
+
"props": [
|
|
8888
|
+
{
|
|
8889
|
+
"description": "One FormField per condition, in reading order. Each direct child is one grid cell; a range (「日付」 From〜To, 「金額」 「下限〜上限」) is ONE FormField holding both controls.",
|
|
8890
|
+
"name": "children",
|
|
8891
|
+
"required": true,
|
|
8892
|
+
"type": "ReactNode"
|
|
8893
|
+
},
|
|
8894
|
+
{
|
|
8895
|
+
"defaultValue": "{ base: 1, md: 2, lg: 3 }",
|
|
8896
|
+
"description": "Grid columns per container breakpoint — one column on a phone, aligned columns above. Conditions never spread to the edges.",
|
|
8897
|
+
"name": "columns",
|
|
8898
|
+
"type": "ResponsiveGridColumnsProp"
|
|
8899
|
+
},
|
|
8900
|
+
{
|
|
8901
|
+
"description": "Conditions shown while collapsed (antd `defaultFormItemsNumber`). Default: the widest `columns` step, i.e. the first row.",
|
|
8902
|
+
"name": "visibleFields",
|
|
8903
|
+
"type": "number"
|
|
8904
|
+
},
|
|
8905
|
+
{
|
|
8906
|
+
"description": "Controlled: whether the conditions past `visibleFields` are shown.",
|
|
8907
|
+
"name": "open",
|
|
8908
|
+
"type": "boolean"
|
|
8909
|
+
},
|
|
8910
|
+
{
|
|
8911
|
+
"defaultValue": "false",
|
|
8912
|
+
"description": "Uncontrolled initial state — collapsed to the first row by default.",
|
|
8913
|
+
"name": "defaultOpen",
|
|
8914
|
+
"type": "boolean"
|
|
8915
|
+
},
|
|
8916
|
+
{
|
|
8917
|
+
"description": "Fires when the 「詳細条件を開く」 / 「閉じる」 toggle is pressed.",
|
|
8918
|
+
"name": "onOpenChange",
|
|
8919
|
+
"type": "(open: boolean) => void"
|
|
8920
|
+
},
|
|
8921
|
+
{
|
|
8922
|
+
"description": "Search (「検索」) — the native form submit. Call `preventDefault()` and copy the draft conditions into the applied query; the form never searches on each keystroke.",
|
|
8923
|
+
"name": "onSubmit",
|
|
8924
|
+
"type": "FormEventHandler<HTMLFormElement>"
|
|
8925
|
+
},
|
|
8926
|
+
{
|
|
8927
|
+
"description": "Clear (「クリア」). The button renders only when this is set.",
|
|
8928
|
+
"name": "onReset",
|
|
8929
|
+
"type": "() => void"
|
|
8930
|
+
},
|
|
8931
|
+
{
|
|
8932
|
+
"description": "The conditions the CURRENT results were searched with, shown after the localized 「検索条件」 label in the status line (e.g. 「国「籍」: 「ベトナム」 / 「状態」: 「実習」中」).",
|
|
8933
|
+
"name": "summary",
|
|
8934
|
+
"type": "ReactNode"
|
|
8935
|
+
},
|
|
8936
|
+
{
|
|
8937
|
+
"description": "Localized, pluralized result count in the same `role=\"status\"` line.",
|
|
8938
|
+
"name": "resultCount",
|
|
8939
|
+
"type": "number"
|
|
8940
|
+
},
|
|
8941
|
+
{
|
|
8942
|
+
"description": "Puts the Search button in its loading state while the query runs.",
|
|
8943
|
+
"name": "loading",
|
|
8944
|
+
"type": "boolean"
|
|
8945
|
+
},
|
|
8946
|
+
{
|
|
8947
|
+
"description": "Overrides the localized Search (「検索」) text.",
|
|
8948
|
+
"name": "searchLabel",
|
|
8949
|
+
"type": "ReactNode"
|
|
8950
|
+
},
|
|
8951
|
+
{
|
|
8952
|
+
"description": "Overrides the localized Clear (「クリア」) text.",
|
|
8953
|
+
"name": "resetLabel",
|
|
8954
|
+
"type": "ReactNode"
|
|
8955
|
+
},
|
|
8956
|
+
{
|
|
8957
|
+
"description": "Accessible name of the `search` landmark (default: localized 'Search conditions').",
|
|
8958
|
+
"name": "label",
|
|
8959
|
+
"type": "string"
|
|
8960
|
+
},
|
|
8961
|
+
{
|
|
8962
|
+
"description": "Passed to the underlying Form: `layout=\"vertical\"` (default) is label-top; `layout=\"horizontal\"` + `labelWidth` is the label-left table style of Japanese business systems. Set `controlWidth` once here so Selects fill their column.",
|
|
8963
|
+
"name": "layout / labelWidth / controlWidth / labelAlign",
|
|
8964
|
+
"type": "Form props"
|
|
8965
|
+
}
|
|
8966
|
+
],
|
|
8967
|
+
"related": [
|
|
8968
|
+
"Toolbar — FilterBar, the live filter strip; use it for a few filters applied as the user changes them.",
|
|
8969
|
+
"Form — the layout container SearchForm renders (`role=\"search\"`); its `layout`, `labelWidth` and `controlWidth` pass through.",
|
|
8970
|
+
"FormField — one per condition; a range is one FormField holding two controls.",
|
|
8971
|
+
"DataTable — the results under the form."
|
|
8972
|
+
],
|
|
8973
|
+
"rules": [
|
|
8974
|
+
23,
|
|
8975
|
+
51
|
|
8976
|
+
],
|
|
8977
|
+
"storyPath": "navigation/search-form.tsx",
|
|
8978
|
+
"tagline": "Submit-driven 「検索条件フォーム」 (Ant Design ProComponents QueryFilter): a `search` landmark holding a grid of labelled conditions aligned in columns, Search 「検索」 + Clear 「クリア」, the conditions past the first row behind ONE 「詳細条件を開く」 toggle that names how many are hidden, and a 「検索条件」 summary + result count in a role=\"status\" line after the search. Use it instead of a live FilterBar when the conditions are many, the query is heavy, or users expect an explicit 「検索」 button (rule #51 decision table).",
|
|
8979
|
+
"usage": [
|
|
8980
|
+
"DO pick the shape from rule #51's decision table: a few filters applied instantly → FilterBar; many conditions / a heavy query / users who expect an explicit 「検索」 button → SearchForm; arbitrary field × operator conditions saved as views → pattern `condition-builder`.",
|
|
8981
|
+
"DO put one FormField per condition as a direct child. Ranges (「入国日」 From〜To, 「金額」 「下限〜上限」) are ONE FormField with both controls; 「部分一致」 / 「完全一致」 is a RadioGroup inside the name field.",
|
|
8982
|
+
"DO keep a draft and an applied copy of the conditions: inputs edit the draft, onSubmit copies it to the applied query, onReset clears both. Pass the applied conditions as `summary` and the hit count as `resultCount`.",
|
|
8983
|
+
"DO NOT wrap it in another Form, and do not rebuild it as a Form + ResponsiveGrid + Collapsible by hand — the collapse, the hidden-count toggle, the search landmark and the status line are what this component owns. Hidden conditions stay mounted, so a typed value survives collapsing.",
|
|
8984
|
+
"A SearchForm (or any `<Form role=\"search\" onSubmit>`) above a table is NOT flagged by ui-audit `form-on-list-page` or `hand-rolled-filter-bar`; a submitting create form without role=\"search\" still is."
|
|
8985
|
+
],
|
|
8986
|
+
"useCases": [
|
|
8987
|
+
"「検索条件フォーム」 on a Japanese business-system list (「基幹系」 / 「業務システム」): 6–12 conditions, 「検索」 and 「クリア」 buttons, 「詳細条件を開く」.",
|
|
8988
|
+
"Search screens over a heavy query where searching on every keystroke would be too expensive.",
|
|
8989
|
+
"Label-left (「表形式」) or label-top search panels above a DataTable, with the applied conditions summarized after the search."
|
|
8696
8990
|
]
|
|
8697
8991
|
},
|
|
8698
8992
|
{
|
|
@@ -10669,6 +10963,12 @@
|
|
|
10669
10963
|
"description": "Renders automatically only when there are no matching results. Place inside CommandList.",
|
|
10670
10964
|
"name": "CommandEmpty",
|
|
10671
10965
|
"type": "React.ReactNode (children)"
|
|
10966
|
+
},
|
|
10967
|
+
{
|
|
10968
|
+
"defaultValue": "true",
|
|
10969
|
+
"description": "CommandSeparator is cmdk's Command.Separator (data-slot=\"command-separator\", ref forwarded, className merged): a hairline between two groups of one list. Default true — the OPPOSITE of cmdk, which drops the separator once the search is non-empty; a `+ Create \"<q>\"` group only exists while the user types, so the rule above it must stay. Pass false for cmdk's palette behaviour. Hidden automatically beside a group cmdk has hidden (no matching rows) and under `split` (which already rules group boundaries). aria-hidden: decorative, since a separator is not an allowed listbox child. Tokens: --command-item-divider-width / --command-item-divider-color (→ --stroke-hairline / --border), margin-block --command-group-padding.",
|
|
10970
|
+
"name": "CommandSeparator.alwaysRender",
|
|
10971
|
+
"type": "boolean"
|
|
10672
10972
|
}
|
|
10673
10973
|
],
|
|
10674
10974
|
"related": [
|
|
@@ -10689,7 +10989,8 @@
|
|
|
10689
10989
|
"CommandGroup",
|
|
10690
10990
|
"CommandInput",
|
|
10691
10991
|
"CommandItem",
|
|
10692
|
-
"CommandList"
|
|
10992
|
+
"CommandList",
|
|
10993
|
+
"CommandSeparator"
|
|
10693
10994
|
],
|
|
10694
10995
|
"tagline": "Accessible, keyboard-navigable command palette / combobox list built on cmdk — always pair CommandInput inside its own wrapper div, never render items outside CommandList.",
|
|
10695
10996
|
"usage": [
|
|
@@ -10698,6 +10999,7 @@
|
|
|
10698
10999
|
"DO always provide a stable explicit value prop on CommandItem when the item's text content can change between renders — relying on inferred textContent with dynamic labels causes selection bugs.",
|
|
10699
11000
|
"DO include CommandEmpty inside CommandList to show a no-results message. It renders automatically only when the filtered count is zero; do not conditionally render it yourself.",
|
|
10700
11001
|
"DO use split for checkbox/option lists (a filter facet, a picker: Popover + PopoverContent flush + Command split) — rows get a divider between them and 0 list padding, so they read as one box. Keep the default (unsplit) for a command palette. Never re-create the look with page CSS (border on items, padding 0 on the group): retune --command-item-divider-color / --command-item-divider-width instead.",
|
|
11002
|
+
"DO divide a result group from an action group with CommandSeparator — matches, then `<CommandSeparator />`, then `<CommandGroup forceMount><CommandItem forceMount value={`__create__${q}`}>+ Create \"{q}\"</CommandItem></CommandGroup>` rendered only while the query has no exact match. Give the create item an explicit value (never inferred from its changing text) and forceMount both it and its group so the filter cannot hide it. Never keep a local copy of the Command family for the divider — a compound must come from one cmdk instance.",
|
|
10701
11003
|
"DON'T use CommandInput as a standalone search input — it is only meaningful inside a Command root (the root manages shared filter state). For a standalone search field use SearchInput instead.",
|
|
10702
11004
|
"DON'T hand-roll keyboard navigation on a list of items; Command handles arrow keys, Enter, Escape, Home/End, and vim bindings. Adding your own keyDown handlers on top creates conflicts — use onSelect on CommandItem for selection logic."
|
|
10703
11005
|
],
|
package/agent/index.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"counts": {
|
|
3
3
|
"anti-ai-tells": 35,
|
|
4
|
-
"components":
|
|
5
|
-
"patterns":
|
|
4
|
+
"components": 166,
|
|
5
|
+
"patterns": 28,
|
|
6
6
|
"rules": 44,
|
|
7
|
-
"tokens":
|
|
7
|
+
"tokens": 2230,
|
|
8
8
|
"vocabulary": 14
|
|
9
9
|
},
|
|
10
10
|
"design": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/DESIGN.md",
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"file": "components-index.json",
|
|
19
|
-
"note": "
|
|
19
|
+
"note": "50 KB — name + group + tagline for all 166. FETCH THIS FIRST, then fetch only the components you chose.",
|
|
20
20
|
"url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"file": "components/<Name>.json",
|
|
24
|
-
"note": "One file per component (1 KB–37 KB, median 6 KB), each carrying its importPath. This is the selective route: read the index, then fetch only what you need instead of the 1.
|
|
24
|
+
"note": "One file per component (1 KB–37 KB, median 6 KB), each carrying its importPath. This is the selective route: read the index, then fetch only what you need instead of the 1.3 MB blob.",
|
|
25
25
|
"url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/<Name>.json"
|
|
26
26
|
},
|
|
27
27
|
{
|
|
@@ -54,19 +54,19 @@
|
|
|
54
54
|
"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.",
|
|
55
55
|
"read": {
|
|
56
56
|
"live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
|
|
57
|
-
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v33.
|
|
57
|
+
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v33.3.0/agent/index.json"
|
|
58
58
|
},
|
|
59
59
|
"source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
|
|
60
60
|
"start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
|
|
61
61
|
"tokenTiers": {
|
|
62
62
|
"component": "per-part knobs, --{component}-{part}-{property}; usually leave these alone",
|
|
63
63
|
"counts": {
|
|
64
|
-
"component":
|
|
64
|
+
"component": 1901,
|
|
65
65
|
"foundation": 216,
|
|
66
66
|
"semantic": 113
|
|
67
67
|
},
|
|
68
68
|
"foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
|
|
69
69
|
"semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
|
|
70
70
|
},
|
|
71
|
-
"version": "33.
|
|
71
|
+
"version": "33.3.0"
|
|
72
72
|
}
|
package/agent/llms.txt
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @godxjp/ui
|
|
2
2
|
|
|
3
|
-
> A Japanese-enterprise React design system:
|
|
4
|
-
> 44 cardinal rules. This file is the entry point for AI agents. Catalog version 33.
|
|
3
|
+
> A Japanese-enterprise React design system: 166 components, 2230 design tokens,
|
|
4
|
+
> 44 cardinal rules. This file is the entry point for AI agents. Catalog version 33.3.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@33.
|
|
7
|
+
(`npx @godxjp/ui-mcp@33.3.0`). It is searchable and version-locked. These files exist for agents
|
|
8
8
|
that can only fetch URLs.
|
|
9
9
|
|
|
10
10
|
## Start
|
|
@@ -15,11 +15,11 @@ that can only fetch URLs.
|
|
|
15
15
|
|
|
16
16
|
## Catalog
|
|
17
17
|
|
|
18
|
-
- [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json):
|
|
19
|
-
- [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json):
|
|
18
|
+
- [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json): 28 whole-task patterns (name, tagline, tags). Start here when the task is a TASK — "build a settings page" — then fetch `patterns/<name>.json` for complete code.
|
|
19
|
+
- [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 50 KB — all 166 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
|
|
20
20
|
- [components/<Name>.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–37 KB, median 6 KB). Read the index, then fetch only the ones you chose — this is the selective route, and the reason you do not need the blob.
|
|
21
|
-
- [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.
|
|
22
|
-
- [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 216 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 113 `semantic` roles,
|
|
21
|
+
- [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.3 MB — every entry in one file. Most URL fetchers truncate a response this size without saying so; prefer the per-component files.
|
|
22
|
+
- [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 216 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 113 `semantic` roles, 1901 `component` knobs.
|
|
23
23
|
- [vocabulary.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json): the controlled prop vocabulary — which prop name means what, across every component.
|
|
24
24
|
- [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 44 cardinal rules.
|
|
25
25
|
- [anti-ai-tells.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json): 35 shapes that make generated UI look generated, each with its fix.
|
|
@@ -27,7 +27,7 @@ that can only fetch URLs.
|
|
|
27
27
|
## Pinning
|
|
28
28
|
|
|
29
29
|
Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
|
|
30
|
-
the tag: `.../godx-jp/godxjp-ui/v33.
|
|
30
|
+
the tag: `.../godx-jp/godxjp-ui/v33.3.0/agent/...`. A catalog that does not match the installed
|
|
31
31
|
package describes props that are absent, or hides props that are present, and says nothing either way.
|
|
32
32
|
|
|
33
33
|
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.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"aliases": [
|
|
3
|
+
"絞り込み条件",
|
|
4
|
+
"条件ビルダー",
|
|
5
|
+
"filter-builder",
|
|
6
|
+
"query-builder",
|
|
7
|
+
"kintone filter",
|
|
8
|
+
"kintone-filter",
|
|
9
|
+
"salesforce list view filter",
|
|
10
|
+
"salesforce-filter",
|
|
11
|
+
"filter logic",
|
|
12
|
+
"list-view-filter"
|
|
13
|
+
],
|
|
14
|
+
"code": "// Live examples: docs page navigation/search-form (m = builder, o = list view with the builder in a\n// docked panel). Research: kintone's filter rows are field / operator / value (「次のいずれかを\n// 含む」, ≦ …) combined with 「すべての条件を満たす」, conditions added with 「絞り込み条件を\n// 追加」; the kintone query language joins conditions with and / or and groups them with\n// parentheses, sorted by order by. Salesforce numbers each filter (Filter 1, 2, …) and combines\n// them with AND / OR / NOT, e.g. (1 AND 2) OR 3; list views are switched from a picker, pinned\n// as the default, and edited in a Filters panel (Add Filter → field / operator / value).\n//\n// A COMPOSITION, not a component: only one of the design systems the library follows names a\n// condition builder, the operators are domain policy, and every piece is an existing primitive.\nimport { useRef, useState } from \"react\";\nimport { Minus, Plus } from \"lucide-react\";\nimport { Badge } from \"@godxjp/ui/data-display\";\nimport { Form, FormField, Input, NumberInput, RadioGroup, Select } from \"@godxjp/ui/data-entry\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Flex, ResponsiveGrid } from \"@godxjp/ui/layout\";\n\ntype Kind = \"text\" | \"choice\" | \"number\";\ntype Field = { value: string; label: string; kind: Kind; options?: { value: string; label: string }[] };\ntype Row = { id: number; field: string; operator: string; value: string };\nexport type Conditions = { match: \"all\" | \"any\" | \"custom\"; logic: string; rows: Row[] };\n\nconst OPERATORS: Record<Kind, { value: string; label: string }[]> = {\n text: [{ value: \"contains\", label: \"次のキーワードを含む\" }, { value: \"notContains\", label: \"次のキーワードを含まない\" }, { value: \"eq\", label: \"=\" }, { value: \"neq\", label: \"≠\" }],\n choice: [{ value: \"in\", label: \"次のいずれかを含む\" }, { value: \"notIn\", label: \"次のいずれも含まない\" }],\n number: [{ value: \"eq\", label: \"=\" }, { value: \"neq\", label: \"≠\" }, { value: \"gte\", label: \"≥(以上)\" }, { value: \"lte\", label: \"≤(以下)\" }],\n};\n\n/** Numbered-row logic: \"1 AND (2 OR 3)\", \"NOT 2\". null = malformed → show the field error. */\nexport function compileLogic(expr: string, count: number): ((hits: boolean[]) => boolean) | null {\n const tokens = expr.toUpperCase().match(/\\d+|AND|OR|NOT|\\(|\\)|\\S/g) ?? [];\n let pos = 0;\n type Node = (h: boolean[]) => boolean;\n const or = (): Node | null => { let l = and(); while (l && tokens[pos] === \"OR\") { pos++; const r = and(); if (!r) return null; const a = l; l = (h) => a(h) || r(h); } return l; };\n const and = (): Node | null => { let l = not(); while (l && tokens[pos] === \"AND\") { pos++; const r = not(); if (!r) return null; const a = l; l = (h) => a(h) && r(h); } return l; };\n const not = (): Node | null => {\n if (tokens[pos] === \"NOT\") { pos++; const i = not(); return i ? (h) => !i(h) : null; }\n if (tokens[pos] === \"(\") { pos++; const i = or(); if (!i || tokens[pos] !== \")\") return null; pos++; return i; }\n const n = Number(tokens[pos]);\n if (!Number.isInteger(n) || n < 1 || n > count) return null;\n pos++; return (h) => h[n - 1] ?? false;\n };\n const tree = tokens.length ? or() : null;\n return tree && pos === tokens.length ? tree : null;\n}\n\nexport function ConditionBuilder({ fields, value, onApply }: {\n fields: Field[]; value: Conditions; onApply: (c: Conditions) => void;\n}) {\n const [draft, setDraft] = useState(value);\n const nextId = useRef(Math.max(0, ...value.rows.map((r) => r.id)) + 1);\n const valid = draft.match !== \"custom\" || compileLogic(draft.logic, draft.rows.length) !== null;\n const patch = (id: number, p: Partial<Row>) => setDraft((c) => ({ ...c, rows: c.rows.map((r) => (r.id === id ? { ...r, ...p } : r)) }));\n const kindOf = (f: string) => fields.find((x) => x.value === f)?.kind ?? \"text\";\n /** kintone's + adds a blank condition right after its own row; null appends one. */\n const insertAfter = (rows: Row[], afterId: number | null): Row[] => {\n const blank: Row = { id: nextId.current++, field: fields[0].value, operator: OPERATORS[fields[0].kind][0].value, value: \"\" };\n const at = afterId === null ? rows.length : rows.findIndex((r) => r.id === afterId) + 1;\n return [...rows.slice(0, at), blank, ...rows.slice(at)];\n };\n\n // role=\"search\" + a real submit: neither form-on-list-page nor hand-rolled-filter-bar flags it.\n return (\n <Form role=\"search\" aria-label=\"絞り込み条件\" controlWidth=\"100%\"\n onSubmit={(e) => { e.preventDefault(); if (valid) onApply(draft); }}>\n <Flex direction=\"col\" gap=\"md\">\n <FormField label=\"条件の組み合わせ\">\n <RadioGroup aria-label=\"条件の組み合わせ\" orientation=\"horizontal\" value={draft.match}\n options={[{ value: \"all\", label: \"すべての条件を満たす\" }, { value: \"any\", label: \"いずれかの条件を満たす\" }, { value: \"custom\", label: \"条件式を指定\" }]}\n onValueChange={(v) => setDraft((c) => ({ ...c, match: v as Conditions[\"match\"] }))} />\n </FormField>\n {draft.rows.map((row, i) => {\n const field = fields.find((f) => f.value === row.field);\n const n = i + 1;\n return (\n // ONE line per condition, kintone-style: [No.] [field] [operator] [value] [+][−].\n // Container grid: 24 tracks from lg (field 6, operator 5 — narrower but wide enough for\n // 「次のキーワードを含まない」 — value 10), 12 at md (the operator gets the extra track so its\n // longest label is not cut). Below md (a docked panel, a phone) it is 4 tracks and\n // `dense` backfills the +/− cell beside the number: No. and buttons on the first line,\n // field / operator / value stacked below. align=\"center\" = antd Row align=\"middle\".\n <Flex key={row.id} direction=\"col\" role=\"group\" aria-label={\"条件 \" + n}>\n <ResponsiveGrid columns={{ base: 4, md: 12, lg: 24 }} gap=\"sm\" align=\"center\" dense>\n <ResponsiveGrid.Item span={{ base: 1, md: 1, lg: 1 }}>\n <Badge variant=\"outline\">{n}</Badge>\n </ResponsiveGrid.Item>\n <ResponsiveGrid.Item span={{ base: 4, md: 3, lg: 6 }}>\n <Select aria-label={\"条件 \" + n + \" · フィールド\"} options={fields} value={row.field}\n onValueChange={(f: string) => patch(row.id, { field: f, operator: OPERATORS[kindOf(f)][0].value, value: \"\" })} />\n </ResponsiveGrid.Item>\n <ResponsiveGrid.Item span={{ base: 4, md: 4, lg: 5 }}>\n <Select aria-label={\"条件 \" + n + \" · 演算子\"} options={OPERATORS[kindOf(row.field)]} value={row.operator}\n onValueChange={(op: string) => patch(row.id, { operator: op })} />\n </ResponsiveGrid.Item>\n <ResponsiveGrid.Item span={{ base: 4, md: 3, lg: 10 }}>\n {field?.kind === \"choice\" ? (\n <Select aria-label={\"条件 \" + n + \" · 値\"} options={field.options ?? []} value={row.value}\n onValueChange={(v: string) => patch(row.id, { value: v })} />\n ) : field?.kind === \"number\" ? (\n <NumberInput aria-label={\"条件 \" + n + \" · 値\"} value={row.value === \"\" ? null : Number(row.value)}\n onValueChange={(v) => patch(row.id, { value: v == null ? \"\" : String(v) })} />\n ) : (\n <Input aria-label={\"条件 \" + n + \" · 値\"} value={row.value} onChange={(e) => patch(row.id, { value: e.target.value })} />\n )}\n </ResponsiveGrid.Item>\n <ResponsiveGrid.Item span={{ base: 3, md: 1, lg: 2 }}>\n <Flex gap=\"xs\" justify=\"end\">\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" aria-label={\"条件 \" + n + \" の後に条件を追加\"}\n onClick={() => setDraft((c) => ({ ...c, rows: insertAfter(c.rows, row.id) }))}>\n <Plus aria-hidden=\"true\" />\n </Button>\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" aria-label={\"条件 \" + n + \" を削除\"}\n onClick={() => setDraft((c) => ({ ...c, rows: c.rows.filter((r) => r.id !== row.id) }))}>\n <Minus aria-hidden=\"true\" />\n </Button>\n </Flex>\n </ResponsiveGrid.Item>\n </ResponsiveGrid>\n </Flex>\n );\n })}\n {/* With every row removed there is no + left to press: offer one. */}\n {draft.rows.length === 0 && (\n <Flex>\n <Button type=\"button\" variant=\"outline\" size=\"sm\"\n onClick={() => setDraft((c) => ({ ...c, rows: insertAfter(c.rows, null) }))}>\n <Plus aria-hidden=\"true\" />条件を追加\n </Button>\n </Flex>\n )}\n {draft.match === \"custom\" && (\n <FormField label=\"条件式\" id=\"cb-logic\" helper=\"番号付きの条件を AND・OR・NOT と括弧で組み合わせます(例: 1 AND (2 OR 3))。\"\n error={valid ? undefined : \"存在する条件の番号、AND・OR・NOT、対応の取れた括弧だけを使ってください。\"}>\n <Input id=\"cb-logic\" value={draft.logic} onChange={(e) => setDraft((c) => ({ ...c, logic: e.target.value }))} />\n </FormField>\n )}\n <Flex gap=\"sm\" justify=\"end\" wrap>\n <Button type=\"button\" variant=\"ghost\" onClick={() => setDraft(value)}>変更を元に戻す</Button>\n <Button type=\"submit\" disabled={!valid}>適用</Button>\n </Flex>\n </Flex>\n </Form>\n );\n}\n// Add 並び順 (Select + Segmented 昇順/降順) and \"save as view\" (Input + Button) as two more\n// FormFields when the screen keeps saved views. Docked beside the list (Salesforce list view):\n// <SplitPane asideWidth=\"lg\" aside={open ? <Card>…<ConditionBuilder/>…</Card> : null}> with a\n// counted Filters toggle in FilterBar's actions; below 1024px the same builder opens in a Sheet.",
|
|
15
|
+
"name": "condition-builder",
|
|
16
|
+
"tagline": "Condition builder — numbered rows of field / operator / value, 「すべての条件を満たす」 / 「いずれかの条件を満たす」 / custom logic `1 AND (2 OR 3)`, add/remove rows, 「並び順」, save as a named view; applied on SUBMIT inside a role=\"search\" Form. Composition, not a component. Docked beside the list it is the Salesforce list-view filter panel.",
|
|
17
|
+
"tags": [
|
|
18
|
+
"filter",
|
|
19
|
+
"condition",
|
|
20
|
+
"builder",
|
|
21
|
+
"query",
|
|
22
|
+
"logic",
|
|
23
|
+
"and",
|
|
24
|
+
"or",
|
|
25
|
+
"kintone",
|
|
26
|
+
"salesforce",
|
|
27
|
+
"saved view",
|
|
28
|
+
"絞り込み",
|
|
29
|
+
"list"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"table-state",
|
|
5
5
|
"list-page"
|
|
6
6
|
],
|
|
7
|
-
"code": "import { useState } from \"react\";\nimport { DataState } from \"@godxjp/ui/query\";\nimport { Card, CardContent, DataTable, EmptyState, type ColumnDef } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { useQuery, keepPreviousData } from \"@tanstack/react-query\";\n\ntype Row = { id: string; name: string; status: string };\n\nexport function MembersTablePage({ orgId }: { orgId?: string }) {\n const [q, setQ] = useState(\"\");\n const [status, setStatus] = useState(\"all\");\n const [page, setPage] = useState(1);\n const pageSize = 20;\n\n // placeholderData: keepPreviousData → on page/filter change the PREVIOUS page stays visible\n // (a stale refresh) instead of flashing a skeleton. enabled gates on the prerequisite (orgId).\n const query = useQuery({\n queryKey: [\"members\", orgId, q, status, page],\n queryFn: () => fetchMembers({ orgId: orgId!, q, status, page, pageSize }),\n enabled: Boolean(orgId),\n placeholderData: keepPreviousData,\n });\n\n const columns: ColumnDef<Row>[] = [\n { key: \"name\", header: \"氏名\", render: (r) => <span className=\"font-medium\">{r.name}</span> },\n { key: \"status\", header: \"ステータス\" },\n ];\n\n return (\n <Flex direction=\"col\" gap=\"lg\">\n {/* Toolbar (filter bar) is STANDALONE and always visible so the user can change filters even\n while a query is idle/loading/empty. Changing a filter resets to page 1. It WRAPS, never\n scrolls sideways (rule #51); more than 2–4 filters → the \"list-filters\" pattern. */}\n <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"}\n onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1); }}>\n <SearchInput placeholder=\"氏名で検索\" value={q} onSearch={(v) => { setQ(v); setPage(1); }} />\n <ToolbarGroup label=\"ステータス\">\n <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1); }}>\n <SelectTrigger><SelectValue /></SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">すべて</SelectItem>\n <SelectItem value=\"active\">有効</SelectItem>\n <SelectItem value=\"invited\">招待中</SelectItem>\n </SelectContent>\n </Select>\n </ToolbarGroup>\n </Toolbar>\n\n {/* The table surface owns exactly ONE card. Pagination lives INSIDE the success branch, so it\n is hidden during prerequisite / loading / empty / error and for single-page results. */}\n <DataState\n query={query}\n prerequisite={<EmptyState variant=\"section\" title=\"組織を選択してください\" />}\n skeleton={<Card><CardContent flush><SkeletonTable rows={8} columns={2} /></CardContent></Card>}\n empty={<EmptyState variant=\"section\" title=\"該当するメンバーがいません\" description=\"検索条件を変更してください。\" />}\n isEmpty={(d) => d.items.length === 0}\n >\n {(d) => {\n const totalPages = Math.ceil(d.total / pageSize);\n return (\n <>\n <Card><CardContent flush><DataTable data={d.items} columns={columns} getRowId={(r) => r.id} /></CardContent></Card>\n {totalPages > 1 && (\n <Pagination value={page} total={d.total} pageSize={pageSize} showTotal onValueChange={setPage} />\n )}\n </>\n );\n }}\n </DataState>\n </Flex>\n );\n}\n// Responsive: Pagination's showTotal range + controls stack at narrow widths automatically.\n// A transient retry keeps q/status/page (they live in component state, not the query) — the\n// same page reloads in place; the user never loses their filters.",
|
|
7
|
+
"code": "import { useState } from \"react\";\nimport { DataState } from \"@godxjp/ui/query\";\nimport { Card, CardContent, DataTable, EmptyState, type ColumnDef } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { useQuery, keepPreviousData } from \"@tanstack/react-query\";\n\ntype Row = { id: string; name: string; status: string };\n\nexport function MembersTablePage({ orgId }: { orgId?: string }) {\n const [q, setQ] = useState(\"\");\n const [status, setStatus] = useState(\"all\");\n const [page, setPage] = useState(1);\n const pageSize = 20;\n\n // placeholderData: keepPreviousData → on page/filter change the PREVIOUS page stays visible\n // (a stale refresh) instead of flashing a skeleton. enabled gates on the prerequisite (orgId).\n const query = useQuery({\n queryKey: [\"members\", orgId, q, status, page],\n queryFn: () => fetchMembers({ orgId: orgId!, q, status, page, pageSize }),\n enabled: Boolean(orgId),\n placeholderData: keepPreviousData,\n });\n\n const columns: ColumnDef<Row>[] = [\n { key: \"name\", header: \"氏名\", render: (r) => <span className=\"font-medium\">{r.name}</span> },\n { key: \"status\", header: \"ステータス\" },\n ];\n\n return (\n <Flex direction=\"col\" gap=\"lg\">\n {/* Toolbar (filter bar) is STANDALONE and always visible so the user can change filters even\n while a query is idle/loading/empty. Changing a filter resets to page 1. It WRAPS, never\n scrolls sideways (rule #51); more than 2–4 filters → the \"list-filters\" pattern. Never a\n hand-built Flex/Card of FormFields (ui-audit hand-rolled-filter-bar). */}\n <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"}\n onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1); }}>\n <SearchInput placeholder=\"氏名で検索\" value={q} onSearch={(v) => { setQ(v); setPage(1); }} />\n <ToolbarGroup label=\"ステータス\">\n <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1); }}>\n <SelectTrigger><SelectValue /></SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">すべて</SelectItem>\n <SelectItem value=\"active\">有効</SelectItem>\n <SelectItem value=\"invited\">招待中</SelectItem>\n </SelectContent>\n </Select>\n </ToolbarGroup>\n </Toolbar>\n\n {/* The table surface owns exactly ONE card. Pagination lives INSIDE the success branch, so it\n is hidden during prerequisite / loading / empty / error and for single-page results. */}\n <DataState\n query={query}\n prerequisite={<EmptyState variant=\"section\" title=\"組織を選択してください\" />}\n skeleton={<Card><CardContent flush><SkeletonTable rows={8} columns={2} /></CardContent></Card>}\n empty={<EmptyState variant=\"section\" title=\"該当するメンバーがいません\" description=\"検索条件を変更してください。\" />}\n isEmpty={(d) => d.items.length === 0}\n >\n {(d) => {\n const totalPages = Math.ceil(d.total / pageSize);\n return (\n <>\n <Card><CardContent flush><DataTable data={d.items} columns={columns} getRowId={(r) => r.id} /></CardContent></Card>\n {totalPages > 1 && (\n <Pagination value={page} total={d.total} pageSize={pageSize} showTotal onValueChange={setPage} />\n )}\n </>\n );\n }}\n </DataState>\n </Flex>\n );\n}\n// Responsive: Pagination's showTotal range + controls stack at narrow widths automatically.\n// A transient retry keeps q/status/page (they live in component state, not the query) — the\n// same page reloads in place; the user never loses their filters.",
|
|
8
8
|
"name": "data-table-page",
|
|
9
9
|
"tagline": "Filter (Toolbar) + table surface + bottom pagination driven by a query state machine — pagination shows ONLY for successful multi-page data; filters/page survive a transient retry.",
|
|
10
10
|
"tags": [
|