From 8a0e43b7eef89978c2f776b1c7bbf708872b8859 Mon Sep 17 00:00:00 2001 From: Raunak Bhagat Date: Thu, 10 Sep 2026 01:05:52 +0000 Subject: [PATCH] refactor(opal): Move the dependency-free raw inputs into Opal (#14617) --- web/bun.lock | 1 + web/lib/opal/package.json | 3 +- web/lib/opal/src/components/README.md | 20 +++++---- web/lib/opal/src/components/index.ts | 21 ++++++++++ .../DateRangePicker.stories.tsx | 2 +- .../DateRangePicker.test.tsx | 2 +- .../inputs/date-range-picker/README.md | 18 ++++++++ .../inputs/date-range-picker/components.tsx} | 41 ++++++++----------- .../inputs/input-file}/InputFile.stories.tsx | 4 +- .../components/inputs/input-file/README.md | 24 +++++++++++ .../inputs/input-file/components.tsx} | 25 +++++++---- .../ListFieldInput.stories.tsx | 4 +- .../inputs/list-field-input/README.md | 18 ++++++++ .../inputs/list-field-input/components.tsx} | 16 +++++--- web/lib/opal/src/strings.tsx | 20 +++++++++ .../admin/billing/LicenseActivationCard.tsx | 2 +- .../[connector]/pages/gdrive/Credential.tsx | 2 +- .../[connector]/pages/gmail/Credential.tsx | 2 +- .../app/app/settings/usage/UsageSettings.tsx | 2 +- web/src/app/app/settings/usage/lib.test.tsx | 2 +- .../v1/apps/admin/CreateCustomAppModal.tsx | 2 +- .../query-history/KickoffCSVExport.tsx | 2 +- .../query-history/QueryHistoryTable.tsx | 2 +- .../admin/performance/query-history/page.tsx | 2 +- .../admin/performance/query-history/utils.ts | 2 +- .../app/ee/admin/performance/usage/page.tsx | 2 +- .../app/ee/agents/stats/[id]/AgentStats.tsx | 6 +-- .../federated/FederatedConnectorForm.tsx | 2 +- .../SearchDateRangeSelector.tsx | 2 +- web/src/components/filters/SourceSelector.tsx | 2 +- web/src/i18n/OpalStringsBridge.tsx | 11 +++++ web/src/i18n/messages/ar.json | 40 +++++++----------- web/src/i18n/messages/de.json | 40 +++++++----------- web/src/i18n/messages/en.json | 40 +++++++----------- web/src/i18n/messages/es.json | 40 +++++++----------- web/src/i18n/messages/fr.json | 40 +++++++----------- web/src/i18n/messages/ja.json | 40 +++++++----------- web/src/i18n/messages/ko.json | 40 +++++++----------- web/src/i18n/messages/pt.json | 40 +++++++----------- web/src/i18n/messages/zh.json | 40 +++++++----------- web/src/lib/hooks.ts | 2 +- web/src/lib/search/interfaces.ts | 2 +- web/src/lib/searchFilters/hooks.ts | 2 +- web/src/lib/searchFilters/types.ts | 2 +- web/src/lib/searchFilters/utils.ts | 2 +- web/src/lib/usage/hooks.ts | 2 +- web/src/sections/usage/AnalyticsChart.tsx | 2 +- .../forms/VertexImageGenForm.tsx | 2 +- web/src/views/admin/PerUserUsagePanel.tsx | 2 +- .../AnalyticsCharts.tsx | 2 +- .../PersonaMessagesChart.tsx | 2 +- .../admin/WorkspaceAnalyticsPage/index.tsx | 2 +- 52 files changed, 333 insertions(+), 315 deletions(-) rename web/{src/refresh-components => lib/opal/src/components/inputs/date-range-picker}/DateRangePicker.stories.tsx (95%) rename web/{src/refresh-components => lib/opal/src/components/inputs/date-range-picker}/DateRangePicker.test.tsx (98%) create mode 100644 web/lib/opal/src/components/inputs/date-range-picker/README.md rename web/{src/refresh-components/DateRangePicker.tsx => lib/opal/src/components/inputs/date-range-picker/components.tsx} (87%) rename web/{src/refresh-components/inputs => lib/opal/src/components/inputs/input-file}/InputFile.stories.tsx (94%) create mode 100644 web/lib/opal/src/components/inputs/input-file/README.md rename web/{src/refresh-components/inputs/InputFile.tsx => lib/opal/src/components/inputs/input-file/components.tsx} (89%) rename web/{src/refresh-components/inputs => lib/opal/src/components/inputs/list-field-input}/ListFieldInput.stories.tsx (94%) create mode 100644 web/lib/opal/src/components/inputs/list-field-input/README.md rename web/{src/refresh-components/inputs/ListFieldInput.tsx => lib/opal/src/components/inputs/list-field-input/components.tsx} (86%) diff --git a/web/bun.lock b/web/bun.lock index f089d62eac..732cae6650 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -151,6 +151,7 @@ "@radix-ui/react-tabs": "^1.0.0", "@radix-ui/react-tooltip": "^1.0.0", "@tanstack/react-table": "^8.0.0", + "date-fns": "^4.0.0", "formik": "^2.0.0", "next": ">=14.0.0", "react": "^18.0.0 || ^19.0.0", diff --git a/web/lib/opal/package.json b/web/lib/opal/package.json index 7bc685e310..19597e1e23 100644 --- a/web/lib/opal/package.json +++ b/web/lib/opal/package.json @@ -1,7 +1,7 @@ { "name": "@onyx-ai/opal", "version": "0.0.0", - "description": "Opal — Onyx's Typescript component library and design system.", + "description": "Opal \u2014 Onyx's Typescript component library and design system.", "license": "MIT", "repository": { "type": "git", @@ -94,6 +94,7 @@ "@radix-ui/react-slot": "^1.0.0", "@radix-ui/react-tooltip": "^1.0.0", "@tanstack/react-table": "^8.0.0", + "date-fns": "^4.0.0", "formik": "^2.0.0", "next": ">=14.0.0", "react": "^18.0.0 || ^19.0.0", diff --git a/web/lib/opal/src/components/README.md b/web/lib/opal/src/components/README.md index 7c323fb64b..eebbe606b2 100644 --- a/web/lib/opal/src/components/README.md +++ b/web/lib/opal/src/components/README.md @@ -14,12 +14,15 @@ The barrel file at `index.ts` re-exports each component and its prop types. Each ## Components -| Component | Description | Docs | -|-----------|-------------|------| -| [Button](./buttons/button/) | Label and/or icon-only stateless button | [README](./buttons/button/README.md) | -| [SelectButton](./buttons/select-button/) | Stateful toggle button with optional foldable content | [README](./buttons/select-button/README.md) | -| [OpenButton](./buttons/open-button/) | Trigger button with rotating chevron for popovers | [README](./buttons/open-button/README.md) | -| [Tag](./tag/) | Small colored label for status/category metadata | [README](./tag/README.md) | +| Component | Description | Docs | +| ---------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------- | +| [Button](./buttons/button/) | Label and/or icon-only stateless button | [README](./buttons/button/README.md) | +| [SelectButton](./buttons/select-button/) | Stateful toggle button with optional foldable content | [README](./buttons/select-button/README.md) | +| [OpenButton](./buttons/open-button/) | Trigger button with rotating chevron for popovers | [README](./buttons/open-button/README.md) | +| [Tag](./tag/) | Small colored label for status/category metadata | [README](./tag/README.md) | +| [DateRangePicker](./inputs/date-range-picker/) | Preset date-range strip with a custom-range calendar popover | [README](./inputs/date-range-picker/README.md) | +| [InputFile](./inputs/input-file/) | Text field that doubles as a file picker | [README](./inputs/input-file/README.md) | +| [ListFieldInput](./inputs/list-field-input/) | Type-and-Enter list builder with removable chips below | [README](./inputs/list-field-input/README.md) | ## Adding new components @@ -30,5 +33,8 @@ The barrel file at `index.ts` re-exports each component and its prop types. Each 5. Add a `README.md` inside the component directory with architecture, props, and usage examples 6. In `components/index.ts`, re-export the component: ```ts - export { TextInput, type TextInputProps } from "@opal/components/inputs/text-input/components"; + export { + TextInput, + type TextInputProps, + } from "@opal/components/inputs/text-input/components"; ``` diff --git a/web/lib/opal/src/components/index.ts b/web/lib/opal/src/components/index.ts index 4f5d79c023..e12a8c6802 100644 --- a/web/lib/opal/src/components/index.ts +++ b/web/lib/opal/src/components/index.ts @@ -54,6 +54,27 @@ export { type TextButtonProps, } from "@opal/components/buttons/text-button/components"; +/* DateRangePicker */ +export { + DateRangePicker, + rangeForInclusiveDays, + THIRTY_DAYS, + type DateRange, + type DateRangePickerValue, +} from "@opal/components/inputs/date-range-picker/components"; + +/* InputFile */ +export { + default as InputFile, + type InputFileProps, +} from "@opal/components/inputs/input-file/components"; + +/* ListFieldInput */ +export { + ListFieldInput, + type ListFieldInputProps, +} from "@opal/components/inputs/list-field-input/components"; + /* Text */ export { Text, diff --git a/web/src/refresh-components/DateRangePicker.stories.tsx b/web/lib/opal/src/components/inputs/date-range-picker/DateRangePicker.stories.tsx similarity index 95% rename from web/src/refresh-components/DateRangePicker.stories.tsx rename to web/lib/opal/src/components/inputs/date-range-picker/DateRangePicker.stories.tsx index 88234754cc..3c92418d30 100644 --- a/web/src/refresh-components/DateRangePicker.stories.tsx +++ b/web/lib/opal/src/components/inputs/date-range-picker/DateRangePicker.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { DateRangePicker, type DateRange, -} from "@/refresh-components/DateRangePicker"; +} from "@opal/components/inputs/date-range-picker/components"; const meta: Meta = { title: "components/DateRangePicker", diff --git a/web/src/refresh-components/DateRangePicker.test.tsx b/web/lib/opal/src/components/inputs/date-range-picker/DateRangePicker.test.tsx similarity index 98% rename from web/src/refresh-components/DateRangePicker.test.tsx rename to web/lib/opal/src/components/inputs/date-range-picker/DateRangePicker.test.tsx index 6a2cae88da..24575797dc 100644 --- a/web/src/refresh-components/DateRangePicker.test.tsx +++ b/web/lib/opal/src/components/inputs/date-range-picker/DateRangePicker.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from "@tests/setup/test-utils"; import userEvent from "@testing-library/user-event"; -import { DateRangePicker } from "@/refresh-components/DateRangePicker"; +import { DateRangePicker } from "@opal/components"; describe("DateRangePicker", () => { beforeEach(() => { diff --git a/web/lib/opal/src/components/inputs/date-range-picker/README.md b/web/lib/opal/src/components/inputs/date-range-picker/README.md new file mode 100644 index 0000000000..d22a4ea4bf --- /dev/null +++ b/web/lib/opal/src/components/inputs/date-range-picker/README.md @@ -0,0 +1,18 @@ +# DateRangePicker + +**Import:** `import { DateRangePicker, rangeForInclusiveDays, type DateRange } from "@opal/components";` + +A preset-strip date-range control: 1D / 7D / 1M / 3M buttons plus a "Custom" trigger that opens +the Opal `Calendar` in a popover for an arbitrary range. The active custom range renders as its +formatted span (e.g. "Mar 3 – Apr 1"). Built on `SelectButton`, `Popover`, and `Calendar`. + +## Props + +| Prop | Type | Default | Description | +| --------------- | ---------------------------- | ------- | ------------------------------------------- | +| `value` | `DateRange` | — | Controlled range (or unset) | +| `onValueChange` | `(value: DateRange) => void` | — | Fires on preset pick or custom-range commit | +| `size` | `"md" \| "sm"` | `"md"` | Button size | + +All labels — preset names, the group and custom-range aria labels — come from `OpalStrings` +(`dateRange*` keys), bridged from the `opal.dateRange` catalog namespace. diff --git a/web/src/refresh-components/DateRangePicker.tsx b/web/lib/opal/src/components/inputs/date-range-picker/components.tsx similarity index 87% rename from web/src/refresh-components/DateRangePicker.tsx rename to web/lib/opal/src/components/inputs/date-range-picker/components.tsx index c7f3b62555..88c2e73d8c 100644 --- a/web/src/refresh-components/DateRangePicker.tsx +++ b/web/lib/opal/src/components/inputs/date-range-picker/components.tsx @@ -1,8 +1,10 @@ import { memo, useState } from "react"; -import { useTranslations } from "next-intl"; import { endOfDay, format, isSameDay, startOfDay, subDays } from "date-fns"; -import { Calendar, Popover, SelectButton } from "@opal/components"; +import { Calendar } from "@opal/components/calendar/components"; +import { Popover } from "@opal/components/popover/components"; +import { SelectButton } from "@opal/components/buttons/select-button/components"; import { SvgCalendar } from "@opal/icons"; +import { useOpalStrings, type OpalStrings } from "@opal/strings"; export const THIRTY_DAYS = "1M"; @@ -66,25 +68,18 @@ function rangesMatch(left: DateRange, right: DateRange): boolean { type SelectorSize = "md" | "sm"; -type DateRangeTranslator = ReturnType< - typeof useTranslations<"common.dateRange"> ->; - // Display labels are looked up per preset so the stable `label` ids used for // range matching stay untranslated. -function presetDisplayLabel( - t: DateRangeTranslator, - preset: DatePreset -): string { +function presetDisplayLabel(strings: OpalStrings, preset: DatePreset): string { switch (preset.inclusiveDays) { case 1: - return t("preset.oneDay"); + return strings.dateRangeOneDay; case 7: - return t("preset.sevenDays"); + return strings.dateRangeSevenDays; case 30: - return t("preset.oneMonth"); + return strings.dateRangeOneMonth; case 90: - return t("preset.threeMonths"); + return strings.dateRangeThreeMonths; default: return preset.label; } @@ -99,7 +94,7 @@ export const DateRangePicker = memo(function DateRangePicker({ onValueChange: (value: DateRange) => void; size?: SelectorSize; }) { - const t = useTranslations("common.dateRange"); + const strings = useOpalStrings(); const buttonSize = size === "sm" ? "sm" : "md"; const [isOpen, setIsOpen] = useState(false); const [draftRange, setDraftRange] = useState(value); @@ -114,7 +109,7 @@ export const DateRangePicker = memo(function DateRangePicker({ const customLabel = customActive && value ? `${format(value.from, value.from.getFullYear() === value.to.getFullYear() ? "MMM d" : "MMM d, y")} – ${format(value.to, value.from.getFullYear() === value.to.getFullYear() ? "MMM d" : "MMM d, y")}` - : t("custom.label"); + : strings.dateRangeCustom; function selectPreset(preset: DatePreset) { const range = rangeForPreset(preset); @@ -129,7 +124,7 @@ export const DateRangePicker = memo(function DateRangePicker({
{PRESETS.map((preset) => { @@ -143,7 +138,7 @@ export const DateRangePicker = memo(function DateRangePicker({ aria-pressed={active} onClick={() => selectPreset(preset)} > - {presetDisplayLabel(t, preset)} + {presetDisplayLabel(strings, preset)} ); })} @@ -164,11 +159,11 @@ export const DateRangePicker = memo(function DateRangePicker({ rightIcon={hasCustomRange ? undefined : SvgCalendar} aria-label={ value - ? t("customRange.ariaLabel", { - from: format(value.from, "MMM d, y"), - to: format(value.to, "MMM d, y"), - }) - : t("chooseCustom.ariaLabel") + ? strings.dateRangeCustomRange( + format(value.from, "MMM d, y"), + format(value.to, "MMM d, y") + ) + : strings.dateRangeChooseCustom } aria-pressed={customActive} aria-haspopup="dialog" diff --git a/web/src/refresh-components/inputs/InputFile.stories.tsx b/web/lib/opal/src/components/inputs/input-file/InputFile.stories.tsx similarity index 94% rename from web/src/refresh-components/inputs/InputFile.stories.tsx rename to web/lib/opal/src/components/inputs/input-file/InputFile.stories.tsx index 88b7ba9f0e..5f9a22f1b6 100644 --- a/web/src/refresh-components/inputs/InputFile.stories.tsx +++ b/web/lib/opal/src/components/inputs/input-file/InputFile.stories.tsx @@ -1,10 +1,10 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import React from "react"; -import InputFile from "./InputFile"; +import InputFile from "@opal/components/inputs/input-file/components"; import * as TooltipPrimitive from "@radix-ui/react-tooltip"; const meta: Meta = { - title: "refresh-components/inputs/InputFile", + title: "opal/components/InputFile", component: InputFile, tags: ["autodocs"], decorators: [ diff --git a/web/lib/opal/src/components/inputs/input-file/README.md b/web/lib/opal/src/components/inputs/input-file/README.md new file mode 100644 index 0000000000..60783c3efa --- /dev/null +++ b/web/lib/opal/src/components/inputs/input-file/README.md @@ -0,0 +1,24 @@ +# InputFile + +**Import:** `import { InputFile, type InputFileProps } from "@opal/components";` + +A text field that doubles as a file picker: type a value directly, or attach a file via the +paperclip action and the field displays the file's name and carries its text content. Clearing +returns to typed mode. Built on `InputTypeIn`; the attach/clear actions are `Button`s in the +right slot. + +## Props + +Extends `InputTypeInProps` minus the props the component owns itself — +`type`, `rightChildren`, `value`, `onChange`, `readOnly`, `clearButton` — +plus: + +| Prop | Type | Description | +| -------------------- | ------------------------------- | ------------------------------------------------------ | +| `setValue` | `(value: string) => void` | Receives the typed text or the selected file's content | +| `onValueSet` | callback | Fires after a value lands | +| `accept` | `string` | File-picker accept filter | +| `maxSizeKb` | `number` | Reject files above this size | +| `onFileSizeExceeded` | `({ file, maxSizeKb }) => void` | Called when a file is rejected for size | + +The attach/clear aria labels come from `OpalStrings` (`attachFile` / `clearFile`). diff --git a/web/src/refresh-components/inputs/InputFile.tsx b/web/lib/opal/src/components/inputs/input-file/components.tsx similarity index 89% rename from web/src/refresh-components/inputs/InputFile.tsx rename to web/lib/opal/src/components/inputs/input-file/components.tsx index 13f2823f45..d67c0c765b 100644 --- a/web/src/refresh-components/inputs/InputFile.tsx +++ b/web/lib/opal/src/components/inputs/input-file/components.tsx @@ -1,11 +1,22 @@ "use client"; import React, { useRef, useState } from "react"; -import { useTranslations } from "next-intl"; -import { InputTypeIn, type InputTypeInProps } from "@opal/components"; -import { Button } from "@opal/components"; -import { noProp } from "@/lib/utils"; +import InputTypeIn, { + type InputTypeInProps, +} from "@opal/components/inputs/input-type-in/components"; +import { Button } from "@opal/components/buttons/button/components"; import { SvgPaperclip, SvgX } from "@opal/icons"; +import { useOpalStrings } from "@opal/strings"; + +// Stops the click reaching the input chrome, which would re-open the picker. +function noProp( + f?: (event: React.MouseEvent) => void +): React.MouseEventHandler { + return (event) => { + event.stopPropagation(); + f?.(event); + }; +} export interface InputFileProps extends Omit< InputTypeInProps, @@ -43,7 +54,7 @@ export default function InputFile({ placeholder, ...rest }: InputFileProps) { - const t = useTranslations("common.inputFile"); + const strings = useOpalStrings(); const [displayValue, setDisplayValue] = useState(""); const [selectedFileName, setSelectedFileName] = useState(null); const [isFileMode, setIsFileMode] = useState(false); @@ -131,7 +142,7 @@ export default function InputFile({ type="button" prominence="tertiary" size="sm" - aria-label={t("clearButton.ariaLabel")} + aria-label={strings.clearFile} /> ) : (