Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7881345
chore(timeslice): consistent file/folder names
bizarre May 8, 2025
8c792ce
feat(inlay): init
bizarre Aug 12, 2025
0214204
refactor(inlay): cleanup
bizarre Aug 12, 2025
d3d1ba2
refactor(inlay): lint
bizarre Aug 12, 2025
5c26a96
feat(structured-inlay): portal anchor
bizarre Aug 12, 2025
3d9aeb3
fix(inlay): IME composition
bizarre Aug 12, 2025
d48c109
feat(structured-inlay): stable token ids
bizarre Aug 12, 2025
123dadf
chore(inlay): playwright CT
bizarre Aug 13, 2025
70f2116
changes
bizarre Jan 17, 2026
fcd7006
fix: better clipboard handling
bizarre Jan 17, 2026
2cdea0d
feat: handle overlapping matches better
bizarre Jan 17, 2026
e10e618
tests: more tests
bizarre Jan 17, 2026
b8602d6
fix: empty state caret rendering
bizarre Jan 17, 2026
4fb5de0
tests: axe-core tests
bizarre Jan 17, 2026
b801fb3
feat: portal rework
bizarre Jan 17, 2026
bc245b0
fix: performance, flaky tests, etc
bizarre Jan 17, 2026
130dcac
fix(inlay): prevent crash and caret issues with many diverging tokens
bizarre Jan 17, 2026
0626207
feat: mobile
bizarre Jan 17, 2026
c3aee3d
refactor: backspace rework
bizarre Jan 18, 2026
bcab1c8
fix(inlay): autocomplete
bizarre Jan 18, 2026
435670d
refactor: ios rendering
bizarre Jan 18, 2026
b5ad6ce
feat(inlay): portal improvements
bizarre Jan 18, 2026
7d3d7fd
refactor(landing): visual rework
bizarre Jan 18, 2026
d55f302
pretty
bizarre Jan 18, 2026
2d524fd
refactor(timeslice): rename to chrono
bizarre Jan 18, 2026
be3b924
docs(inlay): archi docs
bizarre Jan 18, 2026
65fa72c
fix(vitest): exclude ct tests
bizarre Jan 18, 2026
37efaf8
fix(tests): update tests
bizarre Jan 18, 2026
ca477d0
feat(tests): run playwright tests in CI
bizarre Jan 18, 2026
1f6c9c9
fix: tests
bizarre Jan 18, 2026
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
53 changes: 50 additions & 3 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
- master

jobs:
test:
unit-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand All @@ -20,9 +20,31 @@ jobs:
with:
bun-version: latest

- name: Cache Bun dependencies
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-bun-

- name: Install dependencies
run: bun install

- name: Run unit tests
run: bun run test:run

component-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Cache Bun dependencies
uses: actions/cache@v4
with:
Expand All @@ -31,5 +53,30 @@ jobs:
restore-keys: |
${{ runner.os }}-bun-

- name: Run tests
run: bun run test:run
- name: Install dependencies
run: bun install

- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/bun.lock') }}

- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: bun run playwright:install

- name: Install Playwright system dependencies
run: bunx playwright install-deps

- name: Run component tests
run: bun run test:ct

- name: Upload test results
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 7
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ yarn-error.log*
*storybook.log

/dist

# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Or use `npm`, `yarn`, or `pnpm`. Whatever you like.

## 🧩 Components

### `TimeSlice`
### `Chrono`

A smart, headless time range picker that speaks human.

Expand All @@ -38,27 +38,27 @@ A smart, headless time range picker that speaks human.
#### 🛠 Basic Usage

```tsx
import { TimeSlice } from '@bizarre/ui'
import { Chrono } from '@bizarre/ui'

function MyComponent() {
const handleConfirm = (range) => {
console.log('Selected range:', range)
}

return (
<TimeSlice.Root onDateRangeConfirm={handleConfirm}>
<TimeSlice.Input />
<TimeSlice.Portal>
<TimeSlice.Shortcut duration={{ minutes: 15 }}>
<Chrono.Root onDateRangeConfirm={handleConfirm}>
<Chrono.Input />
<Chrono.Portal>
<Chrono.Shortcut duration={{ minutes: 15 }}>
15 minutes
</TimeSlice.Shortcut>
<TimeSlice.Shortcut duration={{ hours: 1 }}>1 hour</TimeSlice.Shortcut>
<TimeSlice.Shortcut duration={{ days: 1 }}>1 day</TimeSlice.Shortcut>
<TimeSlice.Shortcut duration={{ months: 1 }}>
</Chrono.Shortcut>
<Chrono.Shortcut duration={{ hours: 1 }}>1 hour</Chrono.Shortcut>
<Chrono.Shortcut duration={{ days: 1 }}>1 day</Chrono.Shortcut>
<Chrono.Shortcut duration={{ months: 1 }}>
1 month
</TimeSlice.Shortcut>
</TimeSlice.Portal>
</TimeSlice.Root>
</Chrono.Shortcut>
</Chrono.Portal>
</Chrono.Root>
)
}
```
Expand Down
305 changes: 272 additions & 33 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default [
rules: {
'prettier/prettier': 'error',
'react/prop-types': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
Expand Down
10 changes: 10 additions & 0 deletions landing/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

188 changes: 188 additions & 0 deletions landing/components/chrono-example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import React from 'react'
import { Chrono } from '@lib'
import type { DateRange } from '@lib/chrono'
import { ChevronDown } from 'lucide-react'
import {
differenceInYears,
differenceInMonths,
differenceInWeeks,
differenceInDays,
differenceInHours,
differenceInMinutes,
differenceInSeconds
} from 'date-fns'

const colors = {
bg: '#0A0A0A',
surface: '#111111',
border: '#222222',
text: '#FFFFFF',
textMuted: '#888888',
cyan: '#00F0FF'
}

export default function ChronoExample() {
const [dateRange, setDateRange] = React.useState<DateRange>({
startDate: new Date(Date.now() - 1000 * 60 * 15), // 15 minutes ago
endDate: new Date()
})
const [isOpen, setIsOpen] = React.useState(false)

const onDateRangeChange = (range: DateRange) => {
setDateRange(range)
}

const getDurationLabel = (start: Date, end: Date) => {
const diffSeconds = differenceInSeconds(end, start)
const diffMinutes = differenceInMinutes(end, start)
const diffHours = differenceInHours(end, start)
const diffDays = differenceInDays(end, start)
const diffWeeks = differenceInWeeks(end, start)
const diffMonths = differenceInMonths(end, start)
const diffYears = differenceInYears(end, start)

if (diffYears > 0) return `${diffYears}y`
if (diffMonths > 0) return `${diffMonths}mo`
if (diffWeeks > 0) return `${diffWeeks}w`
if (diffDays > 0) return `${diffDays}d`
if (diffHours > 0) return `${diffHours}h`
if (diffMinutes > 0) return `${diffMinutes}m`

return `${diffSeconds}s`
}

const activeDurationLabel = React.useMemo(() => {
if (!dateRange.startDate || !dateRange.endDate) return '-'
return getDurationLabel(dateRange.startDate, dateRange.endDate)
}, [dateRange])

return (
<Chrono.Root
onDateRangeChange={onDateRangeChange}
defaultDateRange={dateRange}
onOpenChange={setIsOpen}
>
<Chrono.Trigger asChild>
<div className="flex items-center space-x-2 w-full">
<div
className="flex-1 flex items-center rounded-lg px-4 py-3 transition-all duration-200 cursor-pointer group"
style={{
border: `1px solid ${colors.border}`,
backgroundColor: colors.surface
}}
>
<div className="flex items-center space-x-3 w-full">
<div
className="h-6 min-w-[48px] rounded text-xs text-center leading-6 font-mono font-medium"
style={{
backgroundColor: `${colors.cyan}20`,
color: colors.cyan
}}
>
{activeDurationLabel}
</div>
<Chrono.Input
className="bg-transparent text-sm w-full outline-none border-none cursor-pointer overflow-hidden truncate"
style={{ color: colors.textMuted }}
/>
<div
className={`transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
style={{ color: colors.textMuted }}
>
<ChevronDown className="h-4 w-4" />
</div>
</div>
</div>
</div>
</Chrono.Trigger>

<Chrono.Portal
className="relative rounded-lg mt-2 p-1.5 flex flex-col gap-0.5 text-sm shadow-2xl z-50 w-full transition-all duration-100 animate-in fade-in-0 zoom-in-95"
style={{
backgroundColor: colors.surface,
border: `1px solid ${colors.border}`
}}
>
<div
className="pb-2 px-2 mb-1"
style={{ borderBottom: `1px solid ${colors.border}` }}
>
<div
className="text-[10px] font-mono font-medium uppercase tracking-widest"
style={{ color: colors.textMuted }}
>
Quick select
</div>
</div>
<Chrono.Shortcut duration={{ minutes: 15 }} asChild>
<div
className="p-2.5 rounded-md cursor-pointer transition-colors duration-100 flex items-center justify-between"
style={{ color: colors.text }}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = `${colors.cyan}10`)
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = 'transparent')
}
>
<span className="text-sm">15 minutes</span>
<span className="text-xs font-mono" style={{ color: colors.cyan }}>
15m
</span>
</div>
</Chrono.Shortcut>
<Chrono.Shortcut duration={{ hours: 1 }} asChild>
<div
className="p-2.5 rounded-md cursor-pointer transition-colors duration-100 flex items-center justify-between"
style={{ color: colors.text }}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = `${colors.cyan}10`)
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = 'transparent')
}
>
<span className="text-sm">1 hour</span>
<span className="text-xs font-mono" style={{ color: colors.cyan }}>
1h
</span>
</div>
</Chrono.Shortcut>
<Chrono.Shortcut duration={{ days: 1 }} asChild>
<div
className="p-2.5 rounded-md cursor-pointer transition-colors duration-100 flex items-center justify-between"
style={{ color: colors.text }}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = `${colors.cyan}10`)
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = 'transparent')
}
>
<span className="text-sm">1 day</span>
<span className="text-xs font-mono" style={{ color: colors.cyan }}>
1d
</span>
</div>
</Chrono.Shortcut>
<Chrono.Shortcut duration={{ months: 1 }} asChild>
<div
className="p-2.5 rounded-md cursor-pointer transition-colors duration-100 flex items-center justify-between"
style={{ color: colors.text }}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = `${colors.cyan}10`)
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = 'transparent')
}
>
<span className="text-sm">1 month</span>
<span className="text-xs font-mono" style={{ color: colors.cyan }}>
1mo
</span>
</div>
</Chrono.Shortcut>
</Chrono.Portal>
</Chrono.Root>
)
}
Loading
Loading