@duro-app/ui 0.34.3 → 0.34.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/ui",
3
- "version": "0.34.3",
3
+ "version": "0.34.5",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -53,7 +53,7 @@
53
53
  }
54
54
  },
55
55
  "dependencies": {
56
- "@duro-app/tokens": "^0.34.3"
56
+ "@duro-app/tokens": "^0.34.5"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@babel/preset-typescript": "^7.28.0",
@@ -107,8 +107,13 @@ function unwrapElementType(t: unknown): unknown {
107
107
  return current
108
108
  }
109
109
 
110
- function extractColumnMeta(children: ReactNode): {template: string; labels: string[]} {
110
+ function extractColumnMeta(children: ReactNode): {
111
+ template: string
112
+ compactTemplate: string
113
+ labels: string[]
114
+ } {
111
115
  const widths: string[] = []
116
+ const compactWidths: string[] = []
112
117
  const labels: string[] = []
113
118
 
114
119
  function walk(node: ReactNode) {
@@ -128,7 +133,13 @@ function extractColumnMeta(children: ReactNode): {template: string; labels: stri
128
133
  // cell's text is wider than 1/N of the container. With minmax(0,
129
134
  // 1fr), the cell's compact-mode `min-width: 0` + `overflow:
130
135
  // hidden` + `text-overflow: ellipsis` can actually truncate.
131
- widths.push(props.width || 'minmax(0, 1fr)')
136
+ const width = props.width || 'minmax(0, 1fr)'
137
+ widths.push(width)
138
+ // `compactWidth` lets the consumer switch to a content-aware
139
+ // layout in the compact band while staying evenly distributed
140
+ // at desktop. Falls back to `width` so columns that don't opt
141
+ // in keep one template across both breakpoints.
142
+ compactWidths.push(props.compactWidth || width)
132
143
  const explicit = typeof props.label === 'string' ? props.label : undefined
133
144
  const fallback = extractText(props.children).trim()
134
145
  const label = explicit ?? fallback
@@ -148,7 +159,11 @@ function extractColumnMeta(children: ReactNode): {template: string; labels: stri
148
159
  }
149
160
 
150
161
  walk(children)
151
- return {template: widths.join(' '), labels}
162
+ return {
163
+ template: widths.join(' '),
164
+ compactTemplate: compactWidths.join(' '),
165
+ labels,
166
+ }
152
167
  }
153
168
 
154
169
  // --- Root ---
@@ -181,7 +196,7 @@ export function Root({
181
196
  pagination,
182
197
  }: RootProps) {
183
198
  const inferredTemplateRef = useRef<string | null>(null)
184
- const {template, labels} = extractColumnMeta(children)
199
+ const {template, compactTemplate, labels} = extractColumnMeta(children)
185
200
  if (template) {
186
201
  inferredTemplateRef.current = template
187
202
  }
@@ -199,7 +214,7 @@ export function Root({
199
214
  // always wins as the later entry in the array.
200
215
  template
201
216
  ? responsive
202
- ? styles.gridColumnsResponsive(template)
217
+ ? styles.gridColumnsResponsive(template, compactTemplate)
203
218
  : styles.gridColumns(template)
204
219
  : undefined,
205
220
  responsive && styles.rootResponsive,
@@ -362,13 +377,24 @@ const CellIndexContext = createContext<{index: number; total: number}>({index: 0
362
377
  export function HeaderCell({
363
378
  children,
364
379
  width: _width,
380
+ compactWidth: _compactWidth,
365
381
  label: _label,
366
382
  isActions,
367
383
  'aria-label': ariaLabel,
368
384
  }: {
369
385
  children?: ReactNode
370
- /** Column width: CSS value like '40px', '2fr', 'max-content'. Defaults to '1fr'. */
386
+ /** Column width at default (wide) viewport. CSS grid-template-columns
387
+ * track value — e.g. '40px', '2fr', 'max-content'. Defaults to
388
+ * 'minmax(0, 1fr)' so each column shares the row evenly and is free
389
+ * to shrink below its content's intrinsic size. */
371
390
  width?: string
391
+ /** Column width when the table's container drops into compact mode
392
+ * (≤720px container). Use this to switch to a content-aware layout
393
+ * at narrow widths — e.g. give a status badge `max-content` and the
394
+ * action column `minmax(0, 2fr)` so it absorbs the slack — while
395
+ * keeping evenly-distributed columns on wide screens. Defaults to the
396
+ * same value as `width`. */
397
+ compactWidth?: string
372
398
  /** Stack-mode label string. Optional when children is a plain string — the
373
399
  * text content is used as the label automatically. Required when children
374
400
  * contain JSX (icon + text, sort indicator, etc.). */
@@ -159,8 +159,15 @@ export const styles = css.create({
159
159
  },
160
160
  },
161
161
 
162
- // Body cell. In compact mode: nowrap + truncate. In stack mode:
163
- // becomes a 2-column grid for label / value.
162
+ // Body cell. Text wraps by default — preferring "wrap to a 2nd line" over
163
+ // "truncate with ellipsis" matches BI/data-reporting conventions where
164
+ // hiding content (even with a visual hint) is treated as data loss.
165
+ // Consumers that explicitly want single-line truncation use Text's
166
+ // `truncate` prop, which sets its own nowrap + overflow + ellipsis chain
167
+ // on the Text element itself. Buttons and badges already enforce
168
+ // `white-space: nowrap` internally, so they're unaffected by this
169
+ // default. In stack mode the cell becomes a 2-column grid for
170
+ // label / value.
164
171
  cell: {
165
172
  color: colors.text,
166
173
  display: {
@@ -169,22 +176,20 @@ export const styles = css.create({
169
176
  },
170
177
  alignItems: 'center',
171
178
  minWidth: {
179
+ // Allow the cell to shrink inside its grid track. In default (wide)
180
+ // mode there's enough room so `auto` is fine; in compact and stack
181
+ // we explicitly drop to 0 so wrappable content can size to the
182
+ // track instead of forcing the track to grow.
172
183
  default: 'auto',
173
184
  [`@container (max-width: ${COMPACT_BP})`]: 0,
174
185
  },
175
186
  overflow: {
187
+ // Safety against pathological unbreakable content (long URLs,
188
+ // dataless ids) at narrow widths.
176
189
  default: 'visible',
177
190
  [`@container (max-width: ${COMPACT_BP})`]: 'hidden',
178
191
  },
179
- textOverflow: {
180
- default: 'clip',
181
- [`@container (max-width: ${COMPACT_BP})`]: 'ellipsis',
182
- },
183
- whiteSpace: {
184
- default: 'normal',
185
- [`@container (max-width: ${COMPACT_BP})`]: 'nowrap',
186
- [`@container (max-width: ${STACK_BP})`]: 'normal',
187
- },
192
+ whiteSpace: 'normal',
188
193
  gridTemplateColumns: {
189
194
  default: null,
190
195
  [`@container (max-width: ${STACK_BP})`]: '1fr 2fr',
@@ -303,15 +308,22 @@ export const styles = css.create({
303
308
  gridTemplateColumns: template,
304
309
  }),
305
310
 
306
- // Dynamic: grid columns + stack-mode collapse in one style.
307
- // Used instead of gridColumns() when responsive=true so that both the
308
- // explicit column template and the @container override live in the same
309
- // StyleX style object. If they were in separate styles (gridColumns +
310
- // rootResponsive), StyleX conflict resolution would keep only the LAST
311
- // style's gridTemplateColumns class and silently discard the earlier one.
312
- gridColumnsResponsive: (template: string) => ({
311
+ // Dynamic: grid columns at three tiers (default / compact / stack) in
312
+ // one style. Used instead of gridColumns() when responsive=true so that
313
+ // every gridTemplateColumns rule lives in the same StyleX style object —
314
+ // splitting them would let StyleX's conflict resolution drop the
315
+ // earlier classes (the LAST style wins per property key).
316
+ //
317
+ // - default: balanced multi-column layout for desktop widths
318
+ // - @container (max-width: COMPACT_BP): consumer-supplied "compact"
319
+ // template, e.g. give a badge column max-content and let the action
320
+ // column absorb the slack so its hint text fits on one line
321
+ // - @container (max-width: STACK_BP): collapse to a single column so
322
+ // each row becomes a card
323
+ gridColumnsResponsive: (template: string, compactTemplate: string) => ({
313
324
  gridTemplateColumns: {
314
325
  default: template,
326
+ [`@container (max-width: ${COMPACT_BP})`]: compactTemplate,
315
327
  [`@container (max-width: ${STACK_BP})`]: '1fr',
316
328
  },
317
329
  }),