Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dull-meals-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@layerstack/utils': patch
---

feat: Add string utils `toCamelCase()`, `toSnakeCase()`, `toKebabCase()`, and `toPascalCase()`
5 changes: 5 additions & 0 deletions .changeset/four-signs-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@layerstack/tailwind': patch
---

fix: Change `culori` dep to devDependencies (only used for generation and not runtime)
5 changes: 5 additions & 0 deletions .changeset/lazy-bottles-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@layerstack/tailwind': patch
---

fix: Change `tailwindcss` dep to devDependecies
10 changes: 5 additions & 5 deletions packages/svelte-stores/src/lib/localStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { writable } from 'svelte/store';
import { isFunction } from 'lodash-es';

import { parse, stringify } from '@layerstack/utils';
import { browser } from '@layerstack/utils/env';
Expand Down Expand Up @@ -42,10 +41,11 @@ function localStore<Value>(key: string, initialValue: Value, options?: LocalStor
? expireObject(previousExpiry, previousExpiry)
: previousExpiry;

const expiry = isFunction(options?.expiry)
? // @ts-expect-error
options?.expiry(prunedPreviousExpiry) // Update expiry on write
: options?.expiry;
const expiry =
typeof options?.expiry === 'function'
? // @ts-expect-error
options?.expiry(prunedPreviousExpiry) // Update expiry on write
: options?.expiry;
previousExpiry = expiry;

localStorage.setItem(key, stringify({ value: val, expiry }));
Expand Down
3 changes: 1 addition & 2 deletions packages/svelte-table/src/lib/stores.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { writable } from 'svelte/store';
import type { ComponentEvents } from 'svelte';
import { isFunction } from 'lodash-es';
import { index } from 'd3-array';

import { sortFunc } from '@layerstack/utils';
Expand Down Expand Up @@ -78,7 +77,7 @@ function createState(column: ColumnDef, props?: TableOrderProps, prevState?: Tab
: 'asc';

let handler: SortFunc | undefined = undefined;
if (isFunction(column.orderBy)) {
if (typeof column.orderBy === 'function') {
handler = column.orderBy;
} else if (typeof column.orderBy === 'string') {
handler = sortFunc(column.orderBy, direction);
Expand Down
6 changes: 3 additions & 3 deletions packages/svelte-table/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isFunction, get } from 'lodash-es';
import { get } from 'lodash-es';

import { PeriodType, parseDate } from '@layerstack/utils';

Expand Down Expand Up @@ -99,7 +99,7 @@ export function getCellHeader(column: ColumnDef) {

export function getCellValue(column: ColumnDef, rowData: any, rowIndex?: number) {
let value = undefined;
if (isFunction(column.value)) {
if (typeof column.value === 'function') {
value = column.value?.(rowData, rowIndex);
}

Expand All @@ -109,7 +109,7 @@ export function getCellValue(column: ColumnDef, rowData: any, rowIndex?: number)

if (
typeof value === 'string' &&
!isFunction(column.format) &&
typeof column.format !== 'function' &&
(column.format ?? 'none') in PeriodType
) {
// Convert date string to Date instance
Expand Down
6 changes: 3 additions & 3 deletions packages/tailwind/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
"@types/d3-array": "^3.2.1",
"@types/lodash-es": "^4.17.12",
"daisyui": "^4.12.24",
"culori": "^4.0.1",
"prettier": "^3.5.3",
"rimraf": "6.0.1",
"tslib": "^2.8.1",
"tsx": "^4.19.4",
"tailwindcss": "^4.1.5",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vitest": "^3.1.3"
Expand All @@ -37,11 +39,9 @@
"dependencies": {
"@layerstack/utils": "workspace:^",
"clsx": "^2.1.1",
"culori": "^4.0.1",
"d3-array": "^3.2.4",
"lodash-es": "^4.17.21",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.5"
"tailwind-merge": "^3.2.0"
},
"main": "./dist/index.js",
"exports": {
Expand Down
5 changes: 3 additions & 2 deletions packages/utils/src/lib/object.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { get, camelCase, mergeWith } from 'lodash-es';
import { get, mergeWith } from 'lodash-es';
import { entries, fromEntries, keys } from './typeHelpers.js';
import { toCamelCase } from './string.js';

export function isLiteralObject(obj: any): obj is object {
return obj && typeof obj === 'object' && obj.constructor === Object;
Expand All @@ -11,7 +12,7 @@ export function isEmptyObject(obj: any) {

export function camelCaseKeys(obj: any) {
return keys(obj).reduce(
(acc, key) => ((acc[camelCase(key ? String(key) : undefined)] = obj[key]), acc),
(acc, key) => ((acc[toCamelCase(key ? String(key) : '')] = obj[key]), acc),
{} as any
);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/lib/rollup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { rollup } from 'd3-array';
import { get, isFunction } from 'lodash-es';
import { get } from 'lodash-es';

export default function <T = any>(
data: T[],
Expand All @@ -13,7 +13,7 @@ export default function <T = any>(
// }

const keyFuncs = keys.map((key) => {
if (isFunction(key)) {
if (typeof key === 'function') {
return key;
} else if (typeof key === 'string') {
return (d: any) => get(d, key) || emptyKey;
Expand Down
150 changes: 141 additions & 9 deletions packages/utils/src/lib/string.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,149 @@
import { describe, it, expect } from 'vitest';
import { describe, test, expect } from 'vitest';

import { toTitleCase } from './string.js';
import {
isUpperCase,
romanize,
toCamelCase,
toKebabCase,
toPascalCase,
toSnakeCase,
toTitleCase,
truncate,
} from './string.js';

describe('isUpperCase()', () => {
test.each([
['A', true],
['a', false],
['THE QUICK BROWN FOX', true],
['the quick brown fox', false],
['The Quick Brown Fox', false],
['The quick brown fox', false],
])('isUpperCase(%s) => %s', (original, expected) => {
expect(isUpperCase(original)).equal(expected);
});
});

describe('toTitleCase()', () => {
it('basic', () => {
const original = 'this is a test';
const expected = 'This is a Test';
test.each([
['A long time ago', 'A Long Time Ago'], // sentence
['the quick brown fox', 'The Quick Brown Fox'], // lower case
['THE QUICK BROWN FOX', 'The Quick Brown Fox'], // upper case
['the_quick_brown_fox', 'The Quick Brown Fox'], // snake case
['the-quick-brown-fox', 'The Quick Brown Fox'], // kebab case
['theQuickBrownFox', 'The Quick Brown Fox'], // pascal case
['the - quick * brown# fox', 'The Quick Brown Fox'], // punctuation
])('toTitleCase(%s) => %s', (original, expected) => {
expect(toTitleCase(original)).equal(expected);
});
});

it('basic', () => {
const original = 'A long time ago';
const expected = 'A Long Time Ago';
expect(toTitleCase(original)).equal(expected);
describe('toCamelCase()', () => {
test.each([
['the quick brown fox', 'theQuickBrownFox'], // lower case
['the_quick_brown_fox', 'theQuickBrownFox'], // snake case
['the-quick-brown-fox', 'theQuickBrownFox'], // kebab case
['THE-QUICK-BROWN-FOX', 'theQuickBrownFox'], // snake case (all caps)
['theQuickBrownFox', 'theQuickBrownFox'], // pascal case
['thequickbrownfox', 'thequickbrownfox'], // lowercase
['the - quick * brown# fox', 'theQuickBrownFox'], // punctuation
['behold theQuickBrownFox', 'beholdTheQuickBrownFox'],
['Behold theQuickBrownFox', 'beholdTheQuickBrownFox'],
['The quick brown FOX', 'theQuickBrownFox'], // all caps words are camel-cased
['theQUickBrownFox', 'theQUickBrownFox'], // all caps substrings >= 4 chars are camel-cased
['theQUIckBrownFox', 'theQUIckBrownFox'],
])('toCamelCase(%s) => %s', (original, expected) => {
expect(toCamelCase(original)).equal(expected);
});
});

describe('toSnakeCase()', () => {
test.each([
['the quick brown fox', 'the_quick_brown_fox'], // lower case
['the-quick-brown-fox', 'the_quick_brown_fox'], // kebab case
['the_quick_brown_fox', 'the_quick_brown_fox'], // snake case
['theQuickBrownFox', 'the_quick_brown_fox'], // pascal case
['theQuickBrown Fox', 'the_quick_brown_fox'], // space separated words
['thequickbrownfox', 'thequickbrownfox'], // no spaces
['the - quick * brown# fox', 'the_quick_brown_fox'], // punctuation
['theQUICKBrownFox', 'the_q_u_i_c_k_brown_fox'], // all caps words are snake-cased
])('toSnakeCase(%s) => %s', (original, expected) => {
expect(toSnakeCase(original)).equal(expected);
});
});

describe('toKebabCase()', () => {
test.each([
['the quick brown fox', 'the-quick-brown-fox'], // lower case
['the-quick-brown-fox', 'the-quick-brown-fox'], // kebab case
['the_quick_brown_fox', 'the-quick-brown-fox'], // snake case
['theQuickBrownFox', 'the-quick-brown-fox'], // pascal case
['theQuickBrown Fox', 'the-quick-brown-fox'], // space separated words
['thequickbrownfox', 'thequickbrownfox'], // no spaces
['the - quick * brown# fox', 'the-quick-brown-fox'], // punctuation
['theQUICKBrownFox', 'the-q-u-i-c-k-brown-fox'], // all caps words are snake-cased
])('toKebabCase(%s) => %s', (original, expected) => {
expect(toKebabCase(original)).equal(expected);
});
});

describe('toPascalCase()', () => {
test.each([
['the quick brown fox', 'TheQuickBrownFox'], // lower case
['the_quick_brown_fox', 'TheQuickBrownFox'], // snake case
['the-quick-brown-fox', 'TheQuickBrownFox'], // kebab case
['theQuickBrownFox', 'TheQuickBrownFox'], // pascal case
['thequickbrownfox', 'Thequickbrownfox'], // lowercase
['the - quick * brown# fox', 'TheQuickBrownFox'], // punctuation
['theQUICKBrownFox', 'TheQUICKBrownFox'], // all caps words are pascal-cased
])('toPascalCase(%s) => %s', (original, expected) => {
expect(toPascalCase(original)).equal(expected);
});
});

describe('romanize()', () => {
test.each([
[1, 'I'],
[2, 'II'],
[3, 'III'],
[4, 'IV'],
[5, 'V'],
[6, 'VI'],
[7, 'VII'],
[8, 'VIII'],
[9, 'IX'],
[10, 'X'],
[11, 'XI'],
[12, 'XII'],
[13, 'XIII'],
[14, 'XIV'],
[15, 'XV'],
[16, 'XVI'],
[17, 'XVII'],
[18, 'XVIII'],
[19, 'XIX'],
[20, 'XX'],
[40, 'XL'],
[49, 'XLIX'],
[50, 'L'],
[90, 'XC'],
[100, 'C'],
[400, 'CD'],
[500, 'D'],
[900, 'CM'],
[1000, 'M'],
])('romanize(%s) => %s', (original, expected) => {
expect(romanize(original)).equal(expected);
});
});

describe('truncate()', () => {
test.each([
['the quick brown fox', 9, undefined, 'the quick…'],
['the quick brown fox', 15, undefined, 'the quick brown…'],
['the quick brown fox', 15, 3, 'the quick br…fox'],
['the quick brown fox', 9, Infinity, '…brown fox'],
])('truncate(%s, %s) => %s', (original, totalChars, endChars, expected) => {
expect(truncate(original, totalChars, endChars)).equal(expected);
});
});
Loading