@podoba/react 0.0.32 → 0.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/package.json +3 -3
  2. package/src/components/asset-masonry-grid.examples.tsx +9 -0
  3. package/src/components/asset-masonry-grid.tsx +62 -0
  4. package/src/components/asset-selection-surface.examples.tsx +32 -0
  5. package/src/components/asset-selection-surface.tsx +184 -0
  6. package/src/components/brand-page-header.tsx +297 -67
  7. package/src/components/button.tsx +27 -4
  8. package/src/components/combobox.tsx +1 -1
  9. package/src/components/compact-action-button.examples.tsx +7 -0
  10. package/src/components/compact-action-button.tsx +21 -0
  11. package/src/components/compact-settings-dialog.examples.tsx +15 -0
  12. package/src/components/compact-settings-dialog.tsx +48 -0
  13. package/src/components/context-action-glyph.examples.tsx +9 -0
  14. package/src/components/context-action-glyph.tsx +61 -0
  15. package/src/components/context-menu.tsx +191 -107
  16. package/src/components/context-search-panel.examples.tsx +9 -0
  17. package/src/components/context-search-panel.tsx +50 -0
  18. package/src/components/csv-binding-presentation.examples.tsx +14 -0
  19. package/src/components/csv-binding-presentation.tsx +40 -0
  20. package/src/components/cta-pill.tsx +19 -4
  21. package/src/components/dashboard-grid.tsx +1 -1
  22. package/src/components/date-picker.tsx +5 -3
  23. package/src/components/date-selection-calendar.examples.tsx +8 -0
  24. package/src/components/date-selection-calendar.tsx +47 -0
  25. package/src/components/delivery/send-to-print-modal.tsx +351 -84
  26. package/src/components/dialog-action-button.examples.tsx +7 -0
  27. package/src/components/dialog-action-button.tsx +22 -0
  28. package/src/components/dialog.tsx +32 -12
  29. package/src/components/document-upload-panel.examples.tsx +12 -0
  30. package/src/components/document-upload-panel.tsx +48 -0
  31. package/src/components/dropdown-menu.tsx +5 -3
  32. package/src/components/empty-panel-action.examples.tsx +7 -0
  33. package/src/components/empty-panel-action.tsx +8 -0
  34. package/src/components/field-appearance.ts +14 -0
  35. package/src/components/focus-field.tsx +3 -3
  36. package/src/components/icons.tsx +30 -0
  37. package/src/components/input.examples.tsx +8 -0
  38. package/src/components/input.tsx +13 -5
  39. package/src/components/media-asset-panel.examples.tsx +11 -0
  40. package/src/components/media-asset-panel.tsx +58 -0
  41. package/src/components/media-gallery.examples.tsx +12 -0
  42. package/src/components/media-gallery.tsx +53 -0
  43. package/src/components/media-settings-dialog.examples.tsx +21 -0
  44. package/src/components/media-settings-dialog.tsx +83 -0
  45. package/src/components/preview-info-card.examples.tsx +18 -0
  46. package/src/components/preview-info-card.tsx +27 -0
  47. package/src/components/reload-icon.examples.tsx +7 -0
  48. package/src/components/reload-icon.tsx +6 -0
  49. package/src/components/request-changes-modal.examples.tsx +26 -0
  50. package/src/components/request-changes-modal.tsx +36 -19
  51. package/src/components/rich-text-editor.tsx +1 -1
  52. package/src/components/select.examples.tsx +17 -0
  53. package/src/components/select.tsx +78 -22
  54. package/src/components/settings-dialog-surface.examples.tsx +44 -0
  55. package/src/components/settings-dialog-surface.tsx +240 -0
  56. package/src/components/side-panel.examples.tsx +80 -0
  57. package/src/components/side-panel.tsx +140 -0
  58. package/src/components/subtle.tsx +64 -8
  59. package/src/components/table.examples.tsx +9 -0
  60. package/src/components/table.tsx +29 -12
  61. package/src/components/template-catalog-card.examples.tsx +25 -0
  62. package/src/components/template-catalog-card.tsx +178 -0
  63. package/src/components/text.tsx +14 -1
  64. package/src/components/textarea.examples.tsx +8 -0
  65. package/src/components/textarea.tsx +16 -6
  66. package/src/components/tile.tsx +16 -17
  67. package/src/editor/block-editor.tsx +56 -14
  68. package/src/index.ts +20 -0
  69. package/src/layout/app-shell.tsx +13 -9
  70. package/src/layout/topbar.tsx +9 -12
@@ -0,0 +1,40 @@
1
+ import { useEffect, useState, type ReactNode } from 'react'
2
+ import { uic } from '../utils/uic'
3
+ import { Table } from './table'
4
+ import { Select, type SelectProps } from './select'
5
+ import { DialogActionButton } from './dialog-action-button'
6
+
7
+ const PreviewScroll = uic('div', { displayName: 'CsvPreviewScroll', baseClass: 'w-full min-w-0 overflow-x-auto' })
8
+
9
+ /** CSV preview has one header per original column; a mapping row precedes data. */
10
+ export function CsvPreviewTable({ headers, rows, mapping, label, emptyMessage, ...rest }: {
11
+ headers: string[]
12
+ rows: Readonly<Record<string, string>>[]
13
+ mapping?: ReactNode[]
14
+ label: string
15
+ emptyMessage: string
16
+ 'data-testid'?: string
17
+ }) {
18
+ const [compact, setCompact] = useState(() => typeof window !== 'undefined' && window.matchMedia('(max-width: 900px)').matches)
19
+ useEffect(() => {
20
+ const media = window.matchMedia('(max-width: 900px)')
21
+ const update = () => setCompact(media.matches)
22
+ update()
23
+ media.addEventListener('change', update)
24
+ return () => media.removeEventListener('change', update)
25
+ }, [])
26
+ const data: ReactNode[][] = rows.map(row => headers.map(header => row[header] ?? ''))
27
+ if (mapping) data.unshift(mapping)
28
+ return <PreviewScroll {...rest}>
29
+ <Table<ReactNode[]> appearance="worksheet" className={compact ? 'min-w-168' : 'min-w-256'}
30
+ aria-label={label} emptyMessage={emptyMessage} data={data}
31
+ columns={headers.map((header, index) => ({ key: String(index), header, render: row => row[index] }))} />
32
+ </PreviewScroll>
33
+ }
34
+
35
+ export function CsvMappingSelect<T extends object>(props: SelectProps<T>) {
36
+ return <Select {...props} appearance="filled" isLabelHidden rootClassName="w-full min-w-48" />
37
+ }
38
+
39
+ /** Original UI Button md (16/20 medium, 12x24 padding), without global button changes. */
40
+ export const CsvActionButton = DialogActionButton
@@ -5,8 +5,11 @@ import type { ReactNode } from 'react'
5
5
  * something" (the middle word emphasised) with an action control on the right.
6
6
  *
7
7
  * The copy is three fragments (`lead` · `emphasis` · `tail`) so a consumer can
8
- * translate each per-locale while keeping the middle-word highlight. Presentational
9
- * only (hard rule #1): every string arrives via props — no i18n, no domain data.
8
+ * translate each per-locale while keeping the middle-word highlight. With
9
+ * `mobileHeader`, it adopts the source ExpandableCTA's 99px mobile touch surface;
10
+ * `BrandPageHeader` owns the safe-bottom positioning and returns it to the one-third
11
+ * desktop grid at the `md` breakpoint. Presentational only (hard rule #1): every
12
+ * string arrives via props — no i18n, no domain data.
10
13
  *
11
14
  * a11y NOTE: the background is the FIXED light brand-secondary (#6eddb1) — it does
12
15
  * not flip with the theme, so every sentence fragment sits on `fg-on-brand` (stable
@@ -23,11 +26,23 @@ export interface CtaPillProps {
23
26
  tail: ReactNode
24
27
  /** Right-side action control (e.g. a Create button). */
25
28
  children: ReactNode
29
+ /**
30
+ * Use the source mobile header density (99px minimum height and doubled
31
+ * horizontal content padding). Positioning stays with `BrandPageHeader`.
32
+ */
33
+ mobileHeader?: boolean
26
34
  }
27
35
 
28
- export function CtaPill({ lead, emphasis, tail, children }: CtaPillProps) {
36
+ export function CtaPill({ lead, emphasis, tail, children, mobileHeader = false }: CtaPillProps) {
29
37
  return (
30
- <div className="flex h-full min-h-16 w-full items-center justify-between gap-nav-x rounded-lg bg-brand-green py-2.5 pr-3 pl-4.5">
38
+ <div
39
+ className={[
40
+ 'flex h-full w-full items-center justify-between gap-nav-x rounded-lg bg-brand-green',
41
+ mobileHeader
42
+ ? 'min-h-mobile-cta px-12 py-5 shadow-mobile-cta md:min-h-16 md:py-2.5 md:pr-3 md:pl-4.5 md:shadow-none'
43
+ : 'min-h-16 py-2.5 pr-3 pl-4.5',
44
+ ].join(' ')}
45
+ >
31
46
  <p className="min-w-0 text-heading4 font-medium leading-5 tracking-tight text-fg-on-brand">
32
47
  {lead} <span className="font-semibold">{emphasis}</span> {tail}
33
48
  </p>
@@ -34,7 +34,7 @@ export type DashboardGridProps = {
34
34
 
35
35
  export function DashboardGrid({ children, className }: DashboardGridProps) {
36
36
  return (
37
- <div className={['grid grid-cols-12 gap-4', className].filter(Boolean).join(' ')}>{children}</div>
37
+ <div className={['grid grid-cols-12 gap-3', className].filter(Boolean).join(' ')}>{children}</div>
38
38
  )
39
39
  }
40
40
 
@@ -42,9 +42,11 @@ const Chevron = ({ dir }: { dir: 'left' | 'right' }) => (
42
42
 
43
43
  const calendarCellClass =
44
44
  'flex h-9 w-9 cursor-pointer items-center justify-center rounded-md text-small text-fg outline-none transition-colors ' +
45
- 'data-[outside-month]:text-fg-subtle data-[hovered]:bg-surface-muted ' +
45
+ // #25: out-of-month and unavailable days stay de-emphasised, but they are dates a
46
+ // user reads and (outside-month) can click — `fg-muted`, not the 2.10:1 decorative grey.
47
+ 'data-[outside-month]:text-fg-muted data-[hovered]:bg-surface-muted ' +
46
48
  'data-[selected]:bg-fg data-[selected]:text-fg-inverted data-[selected]:font-medium ' +
47
- 'data-[unavailable]:text-fg-subtle data-[unavailable]:line-through ' +
49
+ 'data-[unavailable]:text-fg-muted data-[unavailable]:line-through ' +
48
50
  'data-[disabled]:opacity-40 data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring'
49
51
 
50
52
  function CalendarBody() {
@@ -62,7 +64,7 @@ function CalendarBody() {
62
64
  <CalendarGrid className="w-full border-collapse">
63
65
  <CalendarGridHeader>
64
66
  {(day) => (
65
- <CalendarHeaderCell className="pb-1 text-micro font-medium uppercase tracking-wide text-fg-subtle">
67
+ <CalendarHeaderCell className="pb-1 text-micro font-medium uppercase tracking-wide text-fg-muted">
66
68
  {day}
67
69
  </CalendarHeaderCell>
68
70
  )}
@@ -0,0 +1,8 @@
1
+ import { DateSelectionCalendar } from './date-selection-calendar'
2
+ const props = { monthLabel: 'September 2026', weekdays: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], previousLabel: 'Previous month', nextLabel: 'Next month', onPrevious: () => {}, onNext: () => {}, onSelect: () => {}, weeks: [[null, ...Array.from({ length: 6 }, (_, i) => ({ id: String(i + 1), label: String(i + 1), accessibleLabel: 'September ' + (i + 1) + ', 2026', selected: i === 2, today: i === 4 }))]] }
3
+ export const examples = {
4
+ default: () => <DateSelectionCalendar {...props} />,
5
+ emptySelection: () => <DateSelectionCalendar {...props} weeks={props.weeks.map(week => week.map(day => day ? { ...day, selected: false } : null))} />,
6
+ states: () => <DateSelectionCalendar {...props} isDisabled />,
7
+ }
8
+ export const meta = { category: 'Form', description: 'Month navigation and date cells with selected, today, disabled and keyboard focus states.' }
@@ -0,0 +1,47 @@
1
+ import { Button as AriaButton } from 'react-aria-components'
2
+ import { uic } from '../utils/uic'
3
+
4
+ export const CalendarDialogContent = uic('div', { displayName: 'CalendarDialogContent', baseClass: 'flex flex-col gap-8' })
5
+ export const CalendarDateHint = uic('p', { displayName: 'CalendarDateHint', baseClass: 'm-0 min-h-9 text-small font-normal text-fg-workflow-muted', style: { maxWidth: '48ch' } })
6
+ const Frame = uic('div', { displayName: 'DateSelectionCalendar', baseClass: 'mt-6 overflow-hidden border border-border bg-surface' })
7
+ const Header = uic('div', { displayName: 'DateSelectionCalendarHeader', baseClass: 'flex min-h-15 items-center border-b border-border px-5' })
8
+ const Navigation = uic(AriaButton, { displayName: 'DateSelectionCalendarNavigation', baseClass: 'mx-1.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-sm bg-transparent text-2xl font-normal leading-none text-fg outline-none data-[hovered]:bg-surface-card data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring' })
9
+ const Month = uic('div', { displayName: 'DateSelectionCalendarMonth', baseClass: 'flex-1 text-center text-small font-normal text-fg' })
10
+ const Week = uic('div', { displayName: 'DateSelectionCalendarWeek', baseClass: 'grid grid-cols-7 border-b border-border last:border-b-0' })
11
+ const Weekday = uic('div', { displayName: 'DateSelectionCalendarWeekday', baseClass: 'border-e border-border px-3 pb-2 pt-4 text-center font-mono text-small font-normal tracking-normal text-fg-workflow-muted last:border-e-0' })
12
+ const Day = uic(AriaButton, { displayName: 'DateSelectionCalendarDay', baseClass: 'inline-flex min-h-16 w-full items-center justify-center rounded-none border-e border-border bg-transparent text-small font-normal text-fg outline-none last:border-e-0 data-[hovered]:bg-surface-card data-[focus-visible]:ring-2 data-[focus-visible]:ring-inset data-[focus-visible]:ring-ring data-[disabled]:cursor-not-allowed' })
13
+ const Placeholder = uic('div', { displayName: 'DateSelectionCalendarPlaceholder', baseClass: 'min-h-16 border-e border-border last:border-e-0' })
14
+
15
+ export interface DateSelectionCell {
16
+ id: string
17
+ label: string
18
+ accessibleLabel: string
19
+ selected: boolean
20
+ today: boolean
21
+ }
22
+ export function DateSelectionCalendar({ monthLabel, weekdays, weeks, previousLabel, nextLabel, onPrevious, onNext, onSelect, isDisabled = false, autoFocus = false }: {
23
+ monthLabel: string
24
+ weekdays: string[]
25
+ weeks: Array<Array<DateSelectionCell | null>>
26
+ previousLabel: string
27
+ nextLabel: string
28
+ onPrevious: () => void
29
+ onNext: () => void
30
+ onSelect: (id: string) => void
31
+ isDisabled?: boolean
32
+ autoFocus?: boolean
33
+ }) {
34
+ return <Frame>
35
+ <Header>
36
+ <Navigation aria-label={previousLabel} onPress={onPrevious} isDisabled={isDisabled} autoFocus={autoFocus}>‹</Navigation>
37
+ <Month aria-live="polite">{monthLabel}</Month>
38
+ <Navigation aria-label={nextLabel} onPress={onNext} isDisabled={isDisabled}>›</Navigation>
39
+ </Header>
40
+ <Week>{weekdays.map((label, index) => <Weekday key={index}>{label}</Weekday>)}</Week>
41
+ {weeks.map((week, index) => <Week key={index}>{week.map((day, column) => day
42
+ ? <Day key={day.id} aria-label={day.accessibleLabel} aria-pressed={day.selected} aria-current={day.today ? 'date' : undefined} isDisabled={isDisabled} onPress={() => onSelect(day.id)}
43
+ style={day.selected ? { boxShadow: 'inset 0 calc(var(--spacing) * -0.5) 0 var(--color-brand-green)' } : undefined}
44
+ className={day.selected ? 'bg-surface-card' : day.today ? 'bg-brand-green/14 ring-1 ring-inset ring-brand-green data-[hovered]:bg-brand-green/18' : undefined}>{day.label}</Day>
45
+ : <Placeholder key={`blank-${column}`} />)}</Week>)}
46
+ </Frame>
47
+ }