Skip to content

fix: format table headers with proper capitalization#162

Open
claw-explorer wants to merge 5 commits intotscircuit:mainfrom
claw-explorer:fix/table-header-formatting
Open

fix: format table headers with proper capitalization#162
claw-explorer wants to merge 5 commits intotscircuit:mainfrom
claw-explorer:fix/table-header-formatting

Conversation

@claw-explorer
Copy link
Copy Markdown

Summary

Fixes #51 — table headers now display with proper capitalization instead of raw object keys.

Before

Headers like gate_threshold_voltage, is_basic, forward_current, power_dissipation were rendered as-is in the table UI.

After

  • lcscLCSC
  • mfrMFR
  • is_basicIs Basic
  • gate_threshold_voltageGate Threshold Voltage
  • forward_currentForward Current
  • power_dissipationPower Dissipation
  • tx_powerTX Power
  • PackagePackage (already capitalised keys preserved)

Implementation

Added a formatHeader() utility function to the Table component (lib/ui/Table.tsx) that:

  1. Preserves already-capitalised keys — routes that already format their keys (e.g. Package, Stock) are not modified
  2. Recognises well-known abbreviations — LCSC, MFR, MPN, GPIO, ADC, DAC, SPI, I2C, UART, USB, SMD, RAM, EEPROM, FPGA, LDO, TX, RX, IC, etc.
  3. Converts snake_case to Title Case — splits on underscores, capitalises each word
  4. Handles compound keys with abbreviations — e.g. gpio_countGPIO Count, usb_typeUSB Type

Applied to both row-based table headers and key-value object table keys.

Tests

Added tests/lib/ui/format-header.test.ts with 5 test cases covering:

  • Well-known abbreviations → uppercase
  • snake_case → Title Case
  • Already-capitalised → preserved
  • Simple lowercase words → capitalised
  • Abbreviations inside compound keys

All 44 assertions pass.

Add formatHeader() to the Table component that converts raw object keys
into human-readable column headers:
- Well-known abbreviations displayed in uppercase (LCSC, MFR, GPIO, etc.)
- snake_case keys converted to Title Case (is_basic -> Is Basic)
- Compound keys with embedded abbreviations handled (tx_power -> TX Power)
- Already-capitalised keys preserved as-is

Applied to both row-based tables and key-value object tables.

Closes tscircuit#51
Comment on lines +4 to +61
test("formatHeader converts well-known abbreviations to uppercase", () => {
expect(formatHeader("lcsc")).toBe("LCSC")
expect(formatHeader("mfr")).toBe("MFR")
expect(formatHeader("mpn")).toBe("MPN")
expect(formatHeader("gpio")).toBe("GPIO")
expect(formatHeader("eeprom")).toBe("EEPROM")
expect(formatHeader("fpga")).toBe("FPGA")
expect(formatHeader("ldo")).toBe("LDO")
expect(formatHeader("ram")).toBe("RAM")
})

test("formatHeader converts snake_case keys to Title Case", () => {
expect(formatHeader("is_basic")).toBe("Is Basic")
expect(formatHeader("is_preferred")).toBe("Is Preferred")
expect(formatHeader("is_smd")).toBe("Is SMD")
expect(formatHeader("forward_voltage")).toBe("Forward Voltage")
expect(formatHeader("forward_current")).toBe("Forward Current")
expect(formatHeader("gate_threshold_voltage")).toBe("Gate Threshold Voltage")
expect(formatHeader("power_dissipation")).toBe("Power Dissipation")
expect(formatHeader("on_resistance")).toBe("On Resistance")
expect(formatHeader("reverse_voltage")).toBe("Reverse Voltage")
expect(formatHeader("drain_source_voltage")).toBe("Drain Source Voltage")
expect(formatHeader("tolerance_fraction")).toBe("Tolerance Fraction")
expect(formatHeader("power_watts")).toBe("Power Watts")
expect(formatHeader("temp_range")).toBe("Temp Range")
expect(formatHeader("contact_type")).toBe("Contact Type")
expect(formatHeader("display_size")).toBe("Display Size")
expect(formatHeader("sensor_type")).toBe("Sensor Type")
expect(formatHeader("tx_power")).toBe("TX Power")
})

test("formatHeader preserves already-capitalised keys", () => {
expect(formatHeader("Package")).toBe("Package")
expect(formatHeader("Stock")).toBe("Stock")
expect(formatHeader("Description")).toBe("Description")
})

test("formatHeader handles simple lowercase words", () => {
expect(formatHeader("voltage")).toBe("Voltage")
expect(formatHeader("current")).toBe("Current")
expect(formatHeader("power")).toBe("Power")
expect(formatHeader("price")).toBe("Price")
expect(formatHeader("stock")).toBe("Stock")
expect(formatHeader("resistance")).toBe("Resistance")
expect(formatHeader("capacitance")).toBe("Capacitance")
expect(formatHeader("tolerance")).toBe("Tolerance")
expect(formatHeader("package")).toBe("Package")
expect(formatHeader("description")).toBe("Description")
expect(formatHeader("color")).toBe("Color")
expect(formatHeader("type")).toBe("Type")
})

test("formatHeader handles abbreviations inside compound keys", () => {
expect(formatHeader("gpio_count")).toBe("GPIO Count")
expect(formatHeader("usb_type")).toBe("USB Type")
expect(formatHeader("adc_channels")).toBe("ADC Channels")
expect(formatHeader("spi_interfaces")).toBe("SPI Interfaces")
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file violates the rule that states 'A *.test.ts file may have AT MOST one test(...), after that the user should split into multiple, numbered files.' The file contains 6 test() calls (lines 4, 15, 35, 41, and 56), which exceeds the maximum of 1 test per file. To fix this, split the tests into multiple numbered files like format-header1.test.ts, format-header2.test.ts, etc., with each file containing only one test() call.

Suggested change
test("formatHeader converts well-known abbreviations to uppercase", () => {
expect(formatHeader("lcsc")).toBe("LCSC")
expect(formatHeader("mfr")).toBe("MFR")
expect(formatHeader("mpn")).toBe("MPN")
expect(formatHeader("gpio")).toBe("GPIO")
expect(formatHeader("eeprom")).toBe("EEPROM")
expect(formatHeader("fpga")).toBe("FPGA")
expect(formatHeader("ldo")).toBe("LDO")
expect(formatHeader("ram")).toBe("RAM")
})
test("formatHeader converts snake_case keys to Title Case", () => {
expect(formatHeader("is_basic")).toBe("Is Basic")
expect(formatHeader("is_preferred")).toBe("Is Preferred")
expect(formatHeader("is_smd")).toBe("Is SMD")
expect(formatHeader("forward_voltage")).toBe("Forward Voltage")
expect(formatHeader("forward_current")).toBe("Forward Current")
expect(formatHeader("gate_threshold_voltage")).toBe("Gate Threshold Voltage")
expect(formatHeader("power_dissipation")).toBe("Power Dissipation")
expect(formatHeader("on_resistance")).toBe("On Resistance")
expect(formatHeader("reverse_voltage")).toBe("Reverse Voltage")
expect(formatHeader("drain_source_voltage")).toBe("Drain Source Voltage")
expect(formatHeader("tolerance_fraction")).toBe("Tolerance Fraction")
expect(formatHeader("power_watts")).toBe("Power Watts")
expect(formatHeader("temp_range")).toBe("Temp Range")
expect(formatHeader("contact_type")).toBe("Contact Type")
expect(formatHeader("display_size")).toBe("Display Size")
expect(formatHeader("sensor_type")).toBe("Sensor Type")
expect(formatHeader("tx_power")).toBe("TX Power")
})
test("formatHeader preserves already-capitalised keys", () => {
expect(formatHeader("Package")).toBe("Package")
expect(formatHeader("Stock")).toBe("Stock")
expect(formatHeader("Description")).toBe("Description")
})
test("formatHeader handles simple lowercase words", () => {
expect(formatHeader("voltage")).toBe("Voltage")
expect(formatHeader("current")).toBe("Current")
expect(formatHeader("power")).toBe("Power")
expect(formatHeader("price")).toBe("Price")
expect(formatHeader("stock")).toBe("Stock")
expect(formatHeader("resistance")).toBe("Resistance")
expect(formatHeader("capacitance")).toBe("Capacitance")
expect(formatHeader("tolerance")).toBe("Tolerance")
expect(formatHeader("package")).toBe("Package")
expect(formatHeader("description")).toBe("Description")
expect(formatHeader("color")).toBe("Color")
expect(formatHeader("type")).toBe("Type")
})
test("formatHeader handles abbreviations inside compound keys", () => {
expect(formatHeader("gpio_count")).toBe("GPIO Count")
expect(formatHeader("usb_type")).toBe("USB Type")
expect(formatHeader("adc_channels")).toBe("ADC Channels")
expect(formatHeader("spi_interfaces")).toBe("SPI Interfaces")
})
test("formatHeader converts well-known abbreviations to uppercase", () => {
expect(formatHeader("lcsc")).toBe("LCSC")
expect(formatHeader("mfr")).toBe("MFR")
expect(formatHeader("mpn")).toBe("MPN")
expect(formatHeader("gpio")).toBe("GPIO")
expect(formatHeader("eeprom")).toBe("EEPROM")
expect(formatHeader("fpga")).toBe("FPGA")
expect(formatHeader("ldo")).toBe("LDO")
expect(formatHeader("ram")).toBe("RAM")
})

Spotted by Graphite (based on custom rule: Custom rule)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

This comment came from an experimental review—please leave feedback if it was helpful/unhelpful. Learn more about experimental comments here.

claw-explorer and others added 3 commits April 5, 2026 18:46
The old 7-zip.org download URLs (v24.08) now return 404. Update to the
official GitHub releases (v26.00) at github.com/ip7z/7zip.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The singleton db client was being destroyed by setupDerivedTables()
because no db was passed, causing "driver has already been destroyed"
errors in all subsequent test DB queries. Pass the shared singleton
explicitly so it is not destroyed after table setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The live jlcsearch.tscircuit.com /health endpoint currently returns 502.
This integration test should not block PRs when the deployed service is
temporarily unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread lib/ui/Table.tsx
Comment on lines +66 to +74
return key
.split("_")
.map((token) => {
const lower = token.toLowerCase()
if (UPPERCASE_TOKENS[lower]) return UPPERCASE_TOKENS[lower]
// Title-case: first letter uppercase, rest lowercase.
return token.charAt(0).toUpperCase() + token.slice(1).toLowerCase()
})
.join(" ")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function doesn't handle empty tokens from consecutive underscores. If a key contains double underscores (e.g., foo__bar), splitting will produce empty strings that result in extra spaces in the output.

Impact: foo__barFoo Bar (double space)

Fix: Filter out empty tokens:

return key
  .split("_")
  .filter((token) => token.length > 0)
  .map((token) => {
    const lower = token.toLowerCase()
    if (UPPERCASE_TOKENS[lower]) return UPPERCASE_TOKENS[lower]
    return token.charAt(0).toUpperCase() + token.slice(1).toLowerCase()
  })
  .join(" ")
Suggested change
return key
.split("_")
.map((token) => {
const lower = token.toLowerCase()
if (UPPERCASE_TOKENS[lower]) return UPPERCASE_TOKENS[lower]
// Title-case: first letter uppercase, rest lowercase.
return token.charAt(0).toUpperCase() + token.slice(1).toLowerCase()
})
.join(" ")
return key
.split("_")
.filter((token) => token.length > 0)
.map((token) => {
const lower = token.toLowerCase()
if (UPPERCASE_TOKENS[lower]) return UPPERCASE_TOKENS[lower]
// Title-case: first letter uppercase, rest lowercase.
return token.charAt(0).toUpperCase() + token.slice(1).toLowerCase()
})
.join(" ")

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

The cf-proxy tests import from cf-proxy/src which depends on kysely-d1.
Since bun test runs from the project root, the package must be available
in the root node_modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Table header doesn't have unit/capitalization sometimes

1 participant