diff --git a/README.md b/README.md index f8b97416..fd7f1841 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ import { Dialog } from '@signozhq/ui'; import { Drawer } from '@signozhq/ui'; import { DropdownMenu } from '@signozhq/ui'; import { Input } from '@signozhq/ui'; +import { InputNumber } from '@signozhq/ui'; import { Kbd } from '@signozhq/ui'; import { Pagination } from '@signozhq/ui'; import { Progress } from '@signozhq/ui'; @@ -85,6 +86,7 @@ import { Switch } from '@signozhq/ui'; import { Table } from '@signozhq/ui'; import { Tabs } from '@signozhq/ui'; import { TextEllipsis } from '@signozhq/ui'; +import { Textarea } from '@signozhq/ui'; import { Toaster } from '@signozhq/ui'; import { Toggle } from '@signozhq/ui'; import { ToggleGroup } from '@signozhq/ui'; diff --git a/apps/docs/stories/input-number.stories.tsx b/apps/docs/stories/input-number.stories.tsx new file mode 100644 index 00000000..94d5a608 --- /dev/null +++ b/apps/docs/stories/input-number.stories.tsx @@ -0,0 +1,750 @@ +import { + InputNumber, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@signozhq/ui'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useState } from 'react'; + +const meta: Meta = { + title: 'Components/InputNumber', + component: InputNumber, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + "Numeric input with prefix/suffix, addonBefore/addonAfter, optional spinner controls, formatter/parser, sizes, variants, and status. API mirrors Ant Design's `InputNumber` for a drop-in migration path.", + }, + }, + }, + argTypes: { + value: { control: 'number', table: { category: 'Form' } }, + defaultValue: { control: 'number', table: { category: 'Form' } }, + min: { control: 'number', table: { category: 'Behavior' } }, + max: { control: 'number', table: { category: 'Behavior' } }, + step: { control: 'number', table: { category: 'Behavior' } }, + precision: { control: 'number', table: { category: 'Behavior' } }, + placeholder: { control: 'text', table: { category: 'Form' } }, + disabled: { control: 'boolean', table: { category: 'Behavior' } }, + readOnly: { control: 'boolean', table: { category: 'Behavior' } }, + controls: { control: 'boolean', table: { category: 'Behavior' } }, + mode: { + control: 'inline-radio', + options: ['input', 'spinner'], + table: { category: 'Behavior' }, + }, + variant: { + control: 'inline-radio', + options: ['outlined', 'filled', 'borderless', 'underlined'], + table: { category: 'Appearance' }, + }, + size: { + control: 'inline-radio', + options: ['small', 'middle', 'large'], + table: { category: 'Appearance' }, + }, + status: { + control: 'inline-radio', + options: [undefined, 'error', 'warning'], + table: { category: 'Appearance' }, + }, + keyboard: { control: 'boolean', table: { category: 'Behavior' } }, + changeOnWheel: { control: 'boolean', table: { category: 'Behavior' } }, + changeOnBlur: { control: 'boolean', table: { category: 'Behavior' } }, + prefix: { control: false, table: { category: 'Appearance' } }, + suffix: { control: false, table: { category: 'Appearance' } }, + addonBefore: { control: false, table: { category: 'Appearance' } }, + addonAfter: { control: false, table: { category: 'Appearance' } }, + onChange: { control: false, table: { category: 'Events' } }, + onStep: { control: false, table: { category: 'Events' } }, + onPressEnter: { control: false, table: { category: 'Events' } }, + }, +}; + +export default meta; + +type Story = StoryObj; + +/* -------------------------------------------------------------------------- */ +/* Shared layout helpers */ +/* -------------------------------------------------------------------------- */ + +function Section({ + title, + description, + children, +}: { + title: string; + description?: string; + children: React.ReactNode; +}) { + return ( +
+
+

{title}

+ {description && ( +

{description}

+ )} +
+
{children}
+
+ ); +} + +function Field({ + label, + hint, + children, +}: { + label: string; + hint?: string; + children: React.ReactNode; +}) { + return ( + + ); +} + +function Output({ value }: { value: number | null }) { + return ( +

+ onChange: {value === null ? 'null' : value} +

+ ); +} + +/* -------------------------------------------------------------------------- */ +/* Primary */ +/* -------------------------------------------------------------------------- */ + +/** Playground — explore every prop. */ +export const Playground: Story = { + args: { + defaultValue: 3, + min: 0, + max: 100, + step: 1, + placeholder: 'Enter a number', + }, + render: (args) => ( +
+ +
+ ), +}; + +/* -------------------------------------------------------------------------- */ +/* Core props */ +/* -------------------------------------------------------------------------- */ + +/** States — basic, disabled, readonly. */ +export const States: Story = { + parameters: { + docs: { + description: { + story: 'Three interactive states stacked for comparison.', + }, + }, + }, + render: () => ( +
+
+ + + + + + + + + +
+
+ ), +}; + +/** Spinner controls — opt in via `controls`, choose layout via `mode`. */ +export const Spinner: Story = { + parameters: { + docs: { + description: { + story: + '`controls` toggles the up/down buttons; `mode` chooses their layout. The buttons disable automatically at `min` / `max`.', + }, + }, + }, + render: () => ( +
+
+ + + + + + + + + +
+
+ ), +}; + +/** Formatter / parser — custom display formatting. */ +export const Formatter: Story = { + parameters: { + docs: { + description: { + story: + '`formatter` controls how the value renders; `parser` extracts the numeric part of what the user types. Common cases: currency, percentage, units.', + }, + }, + }, + render: () => { + const [currency, setCurrency] = useState(1000); + const [percent, setPercent] = useState(80); + const [bytes, setBytes] = useState(1024); + return ( +
+
+ + `$ ${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')} + parser={(v) => v.replace(/\$\s?|(,*)/g, '')} + /> + + + + `${v}%`} + parser={(v) => v.replace('%', '')} + /> + + + + `${v} B`} + parser={(v) => v.replace(/\s?B/g, '')} + /> + + +
+
+ ); + }, +}; + +/** Precision — round to a fixed number of decimals. */ +export const Precision: Story = { + parameters: { + docs: { + description: { + story: + '`precision` rounds the emitted value AND pads the display with trailing zeros so the field always shows a consistent shape.', + }, + }, + }, + render: () => { + const [value, setValue] = useState(1.234); + return ( +
+ + + + +
+ ); + }, +}; + +/* -------------------------------------------------------------------------- */ +/* Slots */ +/* -------------------------------------------------------------------------- */ + +/** Prefix / suffix — inline content inside the input border. */ +export const PrefixAndSuffix: Story = { + parameters: { + docs: { + description: { + story: + 'Use for short labels, units, or icons. Renders inline inside the bordered input — no extra outer box.', + }, + }, + }, + render: () => ( +
+
+ + + + + + + + + +
+
+ ), +}; + +/** Addons — bordered outer slots that merge into the input. */ +export const Addons: Story = { + parameters: { + docs: { + description: { + story: + '`addonBefore` / `addonAfter` are bordered outer slots that visually merge with the input wrapper. Common pattern: text labels on either side, or an embedded `Select` for unit choice.', + }, + }, + }, + render: () => { + const [unit, setUnit] = useState('GiB'); + return ( +
+
+ + + + + setUnit(v as string)}> + + + + + MiB + GiB + TiB + + + } + /> + + + + + + + + GiB + + + } + /> + +
+
+ ); + }, +}; + +/* -------------------------------------------------------------------------- */ +/* Validation */ +/* -------------------------------------------------------------------------- */ + +/** Status & out-of-range. */ +export const Status: Story = { + parameters: { + docs: { + description: { + story: + '`status` paints an error or warning border. When the value drifts outside `[min, max]`, the wrapper picks up a warning border automatically (unless `status` is set explicitly).', + }, + }, + }, + render: () => ( +
+
+ + + + + + + + + +
+
+ ), +}; + +/** Clamping — value rounds back into range on blur. */ +export const ClampOnBlur: Story = { + parameters: { + docs: { + description: { + story: + '`changeOnBlur` (default `true`) re-emits a clamped, precision-rounded value when the input loses focus. Useful when you want users to be able to type freely but settle on a valid value.', + }, + }, + }, + render: () => { + const [value, setValue] = useState(5); + return ( +
+ + + + +
+ ); + }, +}; + +/* -------------------------------------------------------------------------- */ +/* Appearance */ +/* -------------------------------------------------------------------------- */ + +/** Sizes — small (24px), middle (32px), large (40px). */ +export const Sizes: Story = { + render: () => ( +
+
+ + + + + + + + + +
+
+ ), +}; + +/** Variants — outlined, filled, borderless, underlined. */ +export const Variants: Story = { + render: () => ( +
+
+ + + + + + + + + + +g + +
+
+ ), +}; + +/* -------------------------------------------------------------------------- */ +/* Interaction */ +/* -------------------------------------------------------------------------- */ + +/** Keyboard & wheel. */ +export const KeyboardAndWheel: Story = { + parameters: { + docs: { + description: { + story: + '`keyboard` controls ↑/↓ stepping. `changeOnWheel` enables mouse-wheel stepping when the input is focused (focus required to avoid accidental edits while scrolling the page).', + }, + }, + }, + render: () => { + const [keyboardEnabled, setKeyboardEnabled] = useState(true); + const [wheelEnabled, setWheelEnabled] = useState(true); + return ( +
+
+ + +
+ + + +
+ ); + }, +}; + +/** onPressEnter — submit on Enter. */ +export const PressEnter: Story = { + parameters: { + docs: { + description: { + story: + '`onPressEnter` fires when the user hits Enter while the input is focused. Pair with a controlled value to build "type-and-submit" flows.', + }, + }, + }, + render: () => { + const [submitted, setSubmitted] = useState(null); + const [value, setValue] = useState(null); + return ( +
+ + setSubmitted(value)} + placeholder="e.g. 42" + /> + +
+ ); + }, +}; + +/* -------------------------------------------------------------------------- */ +/* Real-world examples — mirrored from SigNoz PR #11378 */ +/* -------------------------------------------------------------------------- */ + +/** Threshold field — alert rule threshold with text prefix. */ +export const ThresholdField: Story = { + parameters: { + docs: { + description: { + story: + 'Adapted from `FormAlertRules/RuleOptions.tsx`. A labelled threshold field — the label sits in the prefix slot so it stays right next to the value.', + }, + }, + }, + render: () => { + const [value, setValue] = useState(null); + return ( +
+ + + + +
+ ); + }, +}; + +/** Ingestion limits — value + unit dropdown. */ +export const IngestionLimits: Story = { + parameters: { + docs: { + description: { + story: + 'Adapted from `IngestionSettings/MultiIngestionSettings.tsx`. A daily-limit field paired with a unit `Select` in the `addonAfter` slot.', + }, + }, + }, + render: () => { + const [dailyLimit, setDailyLimit] = useState(null); + const [unit, setUnit] = useState('GiB'); + const [samples, setSamples] = useState(null); + const [samplesUnit, setSamplesUnit] = useState('million'); + return ( +
+
+ + setUnit(v as string)}> + + + + + MiB + GiB + TiB + + + } + /> + + + setSamplesUnit(v as string)} + > + + + + + thousand + million + billion + + + } + /> + +
+
+ ); + }, +}; + +/** Histogram buckets — integer count + decimal width. */ +export const HistogramBuckets: Story = { + parameters: { + docs: { + description: { + story: + 'Adapted from `NewWidget/HistogramBucketsSection.tsx`. A pair of bucket-config inputs with different precisions and steps.', + }, + }, + }, + render: () => { + const [count, setCount] = useState(null); + const [width, setWidth] = useState(null); + return ( +
+
+ + + + + + +
+
+ ); + }, +}; + +/** Soft axes — paired min/max range. */ +export const SoftAxesRange: Story = { + parameters: { + docs: { + description: { + story: + 'Adapted from `NewWidget/AxesSection.tsx`. Paired soft-min / soft-max inputs in a horizontal field layout.', + }, + }, + }, + render: () => { + const [softMin, setSoftMin] = useState(null); + const [softMax, setSoftMax] = useState(null); + return ( +
+
+
+ Soft Min + + Soft Max + +
+
+
+ ); + }, +}; + +/** Query limit — min=1, optionally disabled. */ +export const QueryLimit: Story = { + parameters: { + docs: { + description: { + story: + 'Adapted from `QueryBuilder/filters/LimitFilter`. A row-limit field that is min-clamped to 1 and can be disabled when the query isn\'t configured to use a limit.', + }, + }, + }, + render: () => { + const [limit, setLimit] = useState(10); + const [disabled, setDisabled] = useState(false); + return ( +
+
+ + + + + +
+
+ ); + }, +}; diff --git a/apps/docs/stories/intro.mdx b/apps/docs/stories/intro.mdx index df8970eb..3fc38a21 100644 --- a/apps/docs/stories/intro.mdx +++ b/apps/docs/stories/intro.mdx @@ -76,6 +76,7 @@ import { Dialog } from '@signozhq/ui'; import { Drawer } from '@signozhq/ui'; import { DropdownMenu } from '@signozhq/ui'; import { Input } from '@signozhq/ui'; +import { InputNumber } from '@signozhq/ui'; import { Kbd } from '@signozhq/ui'; import { Pagination } from '@signozhq/ui'; import { Progress } from '@signozhq/ui'; @@ -89,6 +90,7 @@ import { Switch } from '@signozhq/ui'; import { Table } from '@signozhq/ui'; import { Tabs } from '@signozhq/ui'; import { TextEllipsis } from '@signozhq/ui'; +import { Textarea } from '@signozhq/ui'; import { Toaster } from '@signozhq/ui'; import { Toggle } from '@signozhq/ui'; import { ToggleGroup } from '@signozhq/ui'; diff --git a/apps/docs/stories/textarea.mdx b/apps/docs/stories/textarea.mdx new file mode 100644 index 00000000..d524b7df --- /dev/null +++ b/apps/docs/stories/textarea.mdx @@ -0,0 +1,72 @@ +import { Meta, Controls, Primary } from '@storybook/addon-docs/blocks'; +import * as TextareaStories from './textarea.stories'; + + + +# Textarea + +Multi-line text input. Mirrors `Input` styling via shared tokens and exposes an `autoSize` mode that grows the field with content between optional `minRows`/`maxRows` bounds. Also available as `Input.TextArea` for Ant Design drop-in compatibility. + +## How to use + +### Basic usage + +```tsx +import { Textarea } from '@signozhq/ui'; + +export default function MyComponent() { + return