@spree/dashboard-ui 1.0.0-rc.2 → 1.0.0-rc.4

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.
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from 'react'
2
+ import { Item, ItemActions, ItemContent, ItemDescription, ItemTitle } from '../ui/item'
2
3
  import { Thumbnail } from '../ui/thumbnail'
3
4
  import { PackageIcon } from './icons'
4
5
 
@@ -30,17 +31,17 @@ export type FulfillmentItemRowData = {
30
31
  */
31
32
  function ItemRow({ row }: { row: FulfillmentItemRowData }) {
32
33
  return (
33
- <div className="flex items-center gap-3 p-3">
34
+ <Item className="flex-nowrap gap-3 rounded-none border-0 p-3">
34
35
  <Thumbnail src={row.thumbnailUrl} fallback={<PackageIcon />} />
35
36
 
36
- <div className="min-w-0 flex-1">
37
- <div className="truncate text-sm font-medium">{row.name}</div>
37
+ <ItemContent className="min-w-0 gap-0">
38
+ <ItemTitle className="block truncate">{row.name}</ItemTitle>
38
39
  {row.optionsText && (
39
- <div className="truncate text-xs text-muted-foreground">{row.optionsText}</div>
40
+ <ItemDescription className="line-clamp-1 text-xs">{row.optionsText}</ItemDescription>
40
41
  )}
41
- </div>
42
+ </ItemContent>
42
43
 
43
- <div className="min-w-0 shrink text-right text-muted-foreground text-sm">
44
+ <ItemActions className="min-w-0 shrink flex-col items-end gap-0 text-right text-muted-foreground text-sm">
44
45
  {row.displayPrice ? (
45
46
  <span className="whitespace-nowrap">
46
47
  {row.displayPrice} × {row.quantity}
@@ -48,9 +49,9 @@ function ItemRow({ row }: { row: FulfillmentItemRowData }) {
48
49
  ) : (
49
50
  <span className="whitespace-nowrap">× {row.quantity}</span>
50
51
  )}
51
- {row.priceSource && <div className="truncate text-xs">{row.priceSource}</div>}
52
- </div>
53
- </div>
52
+ {row.priceSource && <div className="max-w-full truncate text-xs">{row.priceSource}</div>}
53
+ </ItemActions>
54
+ </Item>
54
55
  )
55
56
  }
56
57
 
@@ -18,11 +18,14 @@ export {
18
18
  BanIcon,
19
19
  BanknoteIcon,
20
20
  BarChart3Icon as AnalyticsIcon,
21
+ BellIcon,
21
22
  BoldIcon,
22
23
  BookOpenIcon,
24
+ BotIcon,
23
25
  BoxesIcon,
24
26
  BracesIcon,
25
27
  Building2Icon,
28
+ CalculatorIcon,
26
29
  CalendarIcon,
27
30
  CalendarOffIcon,
28
31
  ChartColumnIcon,
@@ -35,6 +38,7 @@ export {
35
38
  ChevronsLeftIcon,
36
39
  ChevronsRightIcon,
37
40
  ChevronsUpDownIcon,
41
+ CircleAlertIcon,
38
42
  CircleCheckIcon,
39
43
  CircleDotIcon,
40
44
  CircleIcon,
@@ -127,7 +127,7 @@ export function LabelUploadDialog({
127
127
  <InputGroupInput
128
128
  id="label-cost"
129
129
  type="number"
130
- step="0.01"
130
+ step="any"
131
131
  min="0"
132
132
  {...form.register('cost')}
133
133
  />
@@ -100,18 +100,11 @@ export function CreateReturnDialog({
100
100
 
101
101
  /** A line the claim can name, with what the customer paid for it. */
102
102
  export type ClaimableLine = PostSaleUnit & {
103
- /** The whole line after discounts, used to default the refund to what was actually paid. */
104
- discountedAmount?: string | null
105
- /** Tax charged on top of the line's price, which a claim refunds with it. */
106
- additionalTaxTotal?: string | null
107
- }
108
-
109
- /** What the customer paid for `quantity` units of the line, or blank when that is unknown. */
110
- function paidForUnits(line: ClaimableLine, quantity: number): string {
111
- const paid = Number(line.discountedAmount) + Number(line.additionalTaxTotal ?? 0)
112
- if (quantity <= 0 || line.discountedAmount == null || !Number.isFinite(paid)) return ''
113
-
114
- return ((paid * quantity) / line.quantity).toFixed(2)
103
+ /**
104
+ * What the customer paid, tax included, for that many units of the line, as
105
+ * a decimal string — the default refund. Blank when it is unknown.
106
+ */
107
+ paidFor?: (quantity: number) => string
115
108
  }
116
109
 
117
110
  /**
@@ -189,7 +182,7 @@ export function CreateClaimDialog({
189
182
  if (!typedAmounts[line.id]) {
190
183
  setAmounts((current) => ({
191
184
  ...current,
192
- [line.id]: paidForUnits(line, quantity),
185
+ [line.id]: quantity > 0 ? (line.paidFor?.(quantity) ?? '') : '',
193
186
  }))
194
187
  }
195
188
  }}
@@ -207,7 +200,7 @@ export function CreateClaimDialog({
207
200
  <InputGroupInput
208
201
  id={`claim-amount-${line.id}`}
209
202
  type="number"
210
- step="0.01"
203
+ step="any"
211
204
  min="0"
212
205
  value={amounts[line.id] ?? ''}
213
206
  onChange={(event) => {
@@ -1,6 +1,7 @@
1
1
  import { useTranslation } from 'react-i18next'
2
2
  import { Field, FieldLabel } from '../ui/field'
3
3
  import { Input } from '../ui/input'
4
+ import { Item, ItemActions, ItemContent, ItemTitle } from '../ui/item'
4
5
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
5
6
 
6
7
  /** A unit on the order that a return, exchange or claim can name. */
@@ -31,26 +32,27 @@ export function QuantityPicker({
31
32
  return (
32
33
  <div className="flex flex-col gap-2">
33
34
  {units.map((unit) => (
34
- <div
35
- key={unit.id}
36
- className="flex items-center justify-between gap-4 rounded-lg border border-border-subtle p-3"
37
- >
38
- <span className="min-w-0 truncate text-sm">{unit.label}</span>
39
- <Input
40
- type="number"
41
- min={0}
42
- max={unit.quantity}
43
- className="w-20"
44
- value={selection[unit.id] ?? 0}
45
- aria-label={unit.label}
46
- onChange={(event) =>
47
- onChange({
48
- ...selection,
49
- [unit.id]: Math.max(0, Math.min(Number(event.target.value), unit.quantity)),
50
- })
51
- }
52
- />
53
- </div>
35
+ <Item key={unit.id} variant="outline" className="flex-nowrap border-border-subtle">
36
+ <ItemContent className="min-w-0">
37
+ <ItemTitle className="block truncate font-normal">{unit.label}</ItemTitle>
38
+ </ItemContent>
39
+ <ItemActions>
40
+ <Input
41
+ type="number"
42
+ min={0}
43
+ max={unit.quantity}
44
+ className="w-20"
45
+ value={selection[unit.id] ?? 0}
46
+ aria-label={unit.label}
47
+ onChange={(event) =>
48
+ onChange({
49
+ ...selection,
50
+ [unit.id]: Math.max(0, Math.min(Number(event.target.value), unit.quantity)),
51
+ })
52
+ }
53
+ />
54
+ </ItemActions>
55
+ </Item>
54
56
  ))}
55
57
  </div>
56
58
  )
@@ -106,7 +106,7 @@ export function QuantityTierEditor({
106
106
  )}
107
107
  <InputGroupInput
108
108
  type="number"
109
- step="0.01"
109
+ step="any"
110
110
  value={row.value}
111
111
  disabled={disabled}
112
112
  aria-label={labels.value}
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react'
1
+ import { type ReactNode, useState } from 'react'
2
2
  import {
3
3
  Combobox,
4
4
  ComboboxContent,
@@ -84,14 +84,30 @@ export function ResourceCombobox<T extends ComboboxOption>({
84
84
  disabled,
85
85
  id,
86
86
  }: ResourceComboboxProps<T>) {
87
- const selected = items.find((r) => r.id === value) ?? null
87
+ // The picked record is remembered so it stays selected while `items` is
88
+ // between fetches. Without it, the selection drops out of the list for a
89
+ // moment, Base UI reads that as the value being cleared, empties the input,
90
+ // and the field flickers between labels until the queries settle.
91
+ const [picked, setPicked] = useState<T | null>(null)
92
+ const selected =
93
+ items.find((r) => r.id === value) ?? (picked && picked.id === value ? picked : null)
88
94
 
89
95
  return (
90
96
  <Combobox
91
97
  items={items}
92
98
  value={selected}
93
- onValueChange={(record: T | null) => onChange(record?.id, record)}
94
- onInputValueChange={onInputChange}
99
+ // Compared by id: a refetch hands back equal records as new objects.
100
+ isItemEqualToValue={(item: T, current: T) => item.id === current.id}
101
+ onValueChange={(record: T | null) => {
102
+ setPicked(record)
103
+ onChange(record?.id, record)
104
+ }}
105
+ // Picking an option writes its label into the input. That is not a
106
+ // search, so the query goes back to the empty list instead of searching
107
+ // for the full label of what was just chosen.
108
+ onInputValueChange={(next, details) =>
109
+ onInputChange?.(details.reason === 'item-press' ? '' : next)
110
+ }
95
111
  itemToStringLabel={(record: T | null) => (record ? getOptionLabel(record) : '')}
96
112
  itemToStringValue={(record: T | null) => record?.id ?? ''}
97
113
  // Filtering is server-side: the caller's `onInputChange` already narrows
@@ -139,49 +139,6 @@ export function ReturnReceiveDialog({
139
139
  )
140
140
  }
141
141
 
142
- /** True when a received return is owed nothing, so completing it moves no money. */
143
- export function returnOwesNothing(refundableTotal: string): boolean {
144
- return Number(refundableTotal) === 0
145
- }
146
-
147
- export type ReturnRefundFigures = {
148
- status: string
149
- refund_total: string
150
- display_refund_total: string
151
- refunded_total: string
152
- display_refunded_total: string
153
- }
154
-
155
- /**
156
- * What a return's card reports: what it is owed until money goes back, then
157
- * what actually went back — which a merchant keeping a restocking fee makes
158
- * less than it was owed, so that case also names the full amount. Money that
159
- * went back counts even before the return is marked refunded: a refund that
160
- * succeeded on one payment and was declined on the next leaves it received.
161
- */
162
- export function returnRefundSummary(
163
- returnRecord: ReturnRefundFigures,
164
- ):
165
- | { kind: 'owed'; amount: string }
166
- | { kind: 'refunded'; amount: string }
167
- | { kind: 'refunded_short'; amount: string; total: string } {
168
- const refunded = Number(returnRecord.refunded_total)
169
-
170
- if (returnRecord.status !== 'refunded' && refunded === 0) {
171
- return { kind: 'owed', amount: returnRecord.display_refund_total }
172
- }
173
-
174
- if (refunded < Number(returnRecord.refund_total)) {
175
- return {
176
- kind: 'refunded_short',
177
- amount: returnRecord.display_refunded_total,
178
- total: returnRecord.display_refund_total,
179
- }
180
- }
181
-
182
- return { kind: 'refunded', amount: returnRecord.display_refunded_total }
183
- }
184
-
185
142
  /**
186
143
  * Gives the money back: how much, and by what means. A return owed nothing
187
144
  * (a free gift sent back) is completed instead, with nothing to choose.
@@ -193,6 +150,7 @@ export function returnRefundSummary(
193
150
  */
194
151
  export function ReturnRefundDialog({
195
152
  refundableTotal,
153
+ owesNothing,
196
154
  refundTaxTotal,
197
155
  currencySymbol,
198
156
  onClose,
@@ -200,6 +158,8 @@ export function ReturnRefundDialog({
200
158
  pending = false,
201
159
  }: {
202
160
  refundableTotal: string
161
+ /** True when the return is owed nothing, so it is completed rather than refunded. */
162
+ owesNothing: boolean
203
163
  refundTaxTotal?: string
204
164
  currencySymbol: string
205
165
  onClose: () => void
@@ -209,7 +169,6 @@ export function ReturnRefundDialog({
209
169
  const { t } = useTranslation()
210
170
  const [refundMethod, setRefundMethod] = useState<RefundMethod>('original_payment')
211
171
  const [amount, setAmount] = useState(refundableTotal)
212
- const owesNothing = returnOwesNothing(refundableTotal)
213
172
 
214
173
  const methodOptions = [
215
174
  {
@@ -250,7 +209,7 @@ export function ReturnRefundDialog({
250
209
  <InputGroupInput
251
210
  id="refund-amount"
252
211
  type="number"
253
- step="0.01"
212
+ step="any"
254
213
  min="0"
255
214
  value={amount}
256
215
  onChange={(event) => setAmount(event.target.value)}
@@ -2,7 +2,8 @@ import { useState } from 'react'
2
2
  import { useTranslation } from 'react-i18next'
3
3
  import { Button } from '../ui/button'
4
4
  import { Field, FieldLabel } from '../ui/field'
5
- import { Input } from '../ui/input'
5
+ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '../ui/input-group'
6
+ import { EyeIcon, EyeOffIcon } from './icons'
6
7
 
7
8
  // Server masks stored secrets as `••••…` on read so admins can see "yes,
8
9
  // something is stored" without exposing the actual value. Detecting that
@@ -35,7 +36,9 @@ interface SecretInputProps {
35
36
  * mask (`••••3K9z`) and renders as a read-only badge with a "Replace"
36
37
  * button — never as an input, and never with a reveal toggle, because the
37
38
  * server does not send the secret and there is nothing to unhide. Replace
38
- * switches to an empty password input so the admin opts in to rotation;
39
+ * switches to an empty password input so the admin opts in to rotation; that
40
+ * input does carry a reveal toggle, since what it hides is what the admin is
41
+ * typing right now;
39
42
  * Cancel reverts to the badge so the next save preserves the existing
40
43
  * secret via the backend's masked round-trip guard.
41
44
  *
@@ -57,6 +60,7 @@ export function SecretInput({
57
60
  // after `onChange('')` has cleared the parent value.
58
61
  const [pendingMask, setPendingMask] = useState<string | null>(null)
59
62
  const replacing = pendingMask !== null
63
+ const [revealed, setRevealed] = useState(false)
60
64
 
61
65
  // A stored secret always renders as the badge: the server sent a mask,
62
66
  // not the value, so there is nothing a reveal toggle could show.
@@ -93,17 +97,29 @@ export function SecretInput({
93
97
  <Field>
94
98
  <FieldLabel htmlFor={id}>{label}</FieldLabel>
95
99
  <div className="flex items-center gap-2">
96
- <Input
97
- id={id}
98
- type="password"
99
- autoComplete="new-password"
100
- placeholder={
101
- replacing ? t('admin.components.secret_input.new_value_placeholder') : placeholder
102
- }
103
- value={(value as string) ?? ''}
104
- onChange={(e) => onChange(e.target.value)}
105
- className="flex-1"
106
- />
100
+ <InputGroup className="flex-1">
101
+ <InputGroupInput
102
+ id={id}
103
+ type={revealed ? 'text' : 'password'}
104
+ autoComplete="new-password"
105
+ spellCheck={false}
106
+ placeholder={
107
+ replacing ? t('admin.components.secret_input.new_value_placeholder') : placeholder
108
+ }
109
+ value={(value as string) ?? ''}
110
+ onChange={(e) => onChange(e.target.value)}
111
+ />
112
+ <InputGroupAddon align="inline-end">
113
+ <InputGroupButton
114
+ size="icon-xs"
115
+ aria-label={revealed ? t('admin.actions.hide') : t('admin.actions.show')}
116
+ aria-pressed={revealed}
117
+ onClick={() => setRevealed((visible) => !visible)}
118
+ >
119
+ {revealed ? <EyeOffIcon /> : <EyeIcon />}
120
+ </InputGroupButton>
121
+ </InputGroupAddon>
122
+ </InputGroup>
107
123
  {replacing && (
108
124
  <Button
109
125
  type="button"
@@ -15,17 +15,20 @@ import {
15
15
  ComboboxInput,
16
16
  ComboboxItem,
17
17
  ComboboxList,
18
- DropdownMenu,
19
- DropdownMenuContent,
20
- DropdownMenuItem,
21
- DropdownMenuTrigger,
22
18
  Field,
23
19
  FieldDescription,
24
20
  FieldError,
25
21
  FieldGroup,
26
22
  FieldLabel,
27
23
  Input,
24
+ Item,
25
+ ItemActions,
26
+ ItemContent,
27
+ ItemDescription,
28
+ ItemGroup,
29
+ ItemTitle,
28
30
  RelativeTime,
31
+ RowActions,
29
32
  Sheet,
30
33
  SheetContent,
31
34
  SheetDescription,
@@ -35,7 +38,7 @@ import {
35
38
  StatusBadge,
36
39
  useConfirm,
37
40
  } from '../index'
38
- import { BadgeCheckIcon, EllipsisVerticalIcon, PencilIcon, PlusIcon, TrashIcon } from './icons'
41
+ import { BadgeCheckIcon, PlusIcon } from './icons'
39
42
 
40
43
  /** One selectable registration kind. */
41
44
  interface KindOption {
@@ -153,81 +156,71 @@ export function TaxIdentifiersCard({
153
156
  <p className="text-muted-foreground text-sm">{t('admin.tax_identifiers.empty')}</p>
154
157
  </CardContent>
155
158
  ) : (
156
- <CardContent className="flex flex-col gap-3">
157
- {identifiers.map((identifier) => (
158
- <div
159
- key={identifier.id}
160
- className="flex items-start justify-between gap-3 rounded-md border p-3"
161
- >
162
- <div className="flex min-w-0 flex-col gap-1">
163
- <div className="flex flex-wrap items-center gap-2">
164
- <span className="font-medium text-sm">{identifier.value}</span>
165
- <Badge variant="outline">
166
- {t(`admin.tax_identifiers.kinds.${identifier.kind}`, {
167
- defaultValue: identifier.kind,
168
- })}
169
- </Badge>
170
- {identifier.validation_status ? (
171
- <StatusBadge
172
- status={identifier.validation_status}
173
- label={t(
174
- `admin.tax_identifiers.validation_status.${identifier.validation_status}`,
175
- { defaultValue: identifier.validation_status },
176
- )}
177
- />
178
- ) : (
179
- <Badge variant="secondary">
180
- {t('admin.tax_identifiers.validation_status.not_checked')}
159
+ <CardContent>
160
+ <ItemGroup className="gap-3">
161
+ {identifiers.map((identifier) => (
162
+ <Item key={identifier.id} role="listitem" variant="outline" className="items-start">
163
+ <ItemContent className="min-w-0">
164
+ <ItemTitle className="flex-wrap">
165
+ {identifier.value}
166
+ <Badge variant="outline">
167
+ {t(`admin.tax_identifiers.kinds.${identifier.kind}`, {
168
+ defaultValue: identifier.kind,
169
+ })}
181
170
  </Badge>
171
+ {identifier.validation_status ? (
172
+ <StatusBadge
173
+ status={identifier.validation_status}
174
+ label={t(
175
+ `admin.tax_identifiers.validation_status.${identifier.validation_status}`,
176
+ { defaultValue: identifier.validation_status },
177
+ )}
178
+ />
179
+ ) : (
180
+ <Badge variant="secondary">
181
+ {t('admin.tax_identifiers.validation_status.not_checked')}
182
+ </Badge>
183
+ )}
184
+ </ItemTitle>
185
+ {identifier.validated_at && (
186
+ <ItemDescription className="text-xs">
187
+ <RelativeTime
188
+ iso={identifier.validated_at}
189
+ prefix={t('admin.tax_identifiers.checked_prefix')}
190
+ />
191
+ </ItemDescription>
192
+ )}
193
+ {!identifier.validatable && (
194
+ <ItemDescription className="text-xs">
195
+ {t('admin.tax_identifiers.not_validatable')}
196
+ </ItemDescription>
182
197
  )}
183
- </div>
184
- {identifier.validated_at && (
185
- <span className="text-muted-foreground text-xs">
186
- <RelativeTime
187
- iso={identifier.validated_at}
188
- prefix={t('admin.tax_identifiers.checked_prefix')}
198
+ </ItemContent>
199
+
200
+ {canEdit && (
201
+ <ItemActions>
202
+ <RowActions
203
+ actions={[
204
+ {
205
+ key: 'validate',
206
+ label: t('admin.tax_identifiers.validate_action'),
207
+ icon: <BadgeCheckIcon className="size-4" />,
208
+ disabled: !identifier.validatable || mutations.isValidating,
209
+ onSelect: () => mutations.validate(identifier.id).catch(() => undefined),
210
+ },
211
+ { key: 'edit', onSelect: () => setEditing(identifier) },
212
+ {
213
+ key: 'delete',
214
+ destructive: true,
215
+ onSelect: () => handleDelete(identifier),
216
+ },
217
+ ]}
189
218
  />
190
- </span>
191
- )}
192
- {!identifier.validatable && (
193
- <span className="text-muted-foreground text-xs">
194
- {t('admin.tax_identifiers.not_validatable')}
195
- </span>
219
+ </ItemActions>
196
220
  )}
197
- </div>
198
-
199
- {canEdit && (
200
- <DropdownMenu>
201
- <DropdownMenuTrigger asChild>
202
- <Button variant="ghost" size="icon-xs">
203
- <EllipsisVerticalIcon className="size-4" />
204
- <span className="sr-only">{t('admin.actions.actions_menu')}</span>
205
- </Button>
206
- </DropdownMenuTrigger>
207
- <DropdownMenuContent align="end">
208
- <DropdownMenuItem
209
- disabled={!identifier.validatable || mutations.isValidating}
210
- onClick={() => mutations.validate(identifier.id).catch(() => undefined)}
211
- >
212
- <BadgeCheckIcon className="size-4" />
213
- {t('admin.tax_identifiers.validate_action')}
214
- </DropdownMenuItem>
215
- <DropdownMenuItem onClick={() => setEditing(identifier)}>
216
- <PencilIcon className="size-4" />
217
- {t('admin.actions.edit')}
218
- </DropdownMenuItem>
219
- <DropdownMenuItem
220
- className="text-destructive focus:text-destructive"
221
- onClick={() => handleDelete(identifier)}
222
- >
223
- <TrashIcon className="size-4" />
224
- {t('admin.actions.delete')}
225
- </DropdownMenuItem>
226
- </DropdownMenuContent>
227
- </DropdownMenu>
228
- )}
229
- </div>
230
- ))}
221
+ </Item>
222
+ ))}
223
+ </ItemGroup>
231
224
  </CardContent>
232
225
  )}
233
226
 
@@ -188,13 +188,7 @@ export function Wizard({
188
188
  }}
189
189
  modal
190
190
  >
191
- <DialogContent
192
- // Edge-to-edge minus a gutter, like the import wizard and the bulk
193
- // price editor — every inset/translate/max is overridden.
194
- className="!inset-3 !w-auto !max-w-none !translate-x-0 !translate-y-0 flex flex-col p-0"
195
- style={{ maxHeight: 'none' }}
196
- showCloseButton={false}
197
- >
191
+ <DialogContent fullScreen showCloseButton={false}>
198
192
  <DialogHeader className="h-14 shrink-0 flex-row items-center gap-3 space-y-0 border-b border-border-subtle bg-background px-4 py-0">
199
193
  <div className="flex min-w-0 items-center gap-3">
200
194
  <DialogTitle className="truncate text-base">{title}</DialogTitle>
package/src/ui/dialog.tsx CHANGED
@@ -95,20 +95,29 @@ function DialogContent({
95
95
  className,
96
96
  children,
97
97
  showCloseButton = true,
98
+ fullScreen = false,
98
99
  ...props
99
100
  }: React.ComponentProps<typeof DialogPrimitive.Popup> & {
100
101
  showCloseButton?: boolean
102
+ /**
103
+ * Fills the window minus a small gutter, over an opaque overlay — for
104
+ * wizards and editors that replace the page rather than sit on top of it.
105
+ * A see-through overlay would let the page's own panel edges show around
106
+ * the dialog as a second border.
107
+ */
108
+ fullScreen?: boolean
101
109
  }) {
102
110
  return (
103
111
  <DialogPortal>
104
- <DialogOverlay />
112
+ <DialogOverlay className={cn(fullScreen && 'bg-background dark:bg-background')} />
105
113
  <DialogPrimitive.Popup
106
114
  data-slot="dialog-content"
107
115
  className={cn(
108
116
  'fixed top-1/2 left-1/2 z-50 flex w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl border border-border bg-card text-foreground text-sm shadow-lg duration-100 outline-none sm:max-w-[min(500px,calc(100%-2rem))] data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 transition-[opacity,transform,translate,scale]',
117
+ fullScreen && 'inset-3 w-auto max-w-none translate-x-0 translate-y-0 sm:max-w-none',
109
118
  className,
110
119
  )}
111
- style={{ maxHeight: '90vh' }}
120
+ style={fullScreen ? undefined : { maxHeight: '90vh' }}
112
121
  {...props}
113
122
  >
114
123
  {children}