@weft-js/warp 0.0.0-stage → 0.1.0
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/LICENSE +16 -0
- package/README.md +55 -2
- package/package.json +43 -4
- package/plugin.d.mts +30 -0
- package/plugin.mjs +358 -0
- package/src/Accordion.tsx +58 -0
- package/src/Alert.tsx +25 -0
- package/src/Badge.tsx +17 -0
- package/src/Button.tsx +100 -0
- package/src/Calendar.tsx +162 -0
- package/src/Canvas.tsx +110 -0
- package/src/Card.tsx +29 -0
- package/src/Chart.tsx +133 -0
- package/src/Charts.tsx +69 -0
- package/src/Chip.tsx +27 -0
- package/src/Choice.tsx +105 -0
- package/src/CodeBlock.tsx +65 -0
- package/src/CodeInput.tsx +130 -0
- package/src/Combobox.tsx +113 -0
- package/src/CommandPalette.tsx +348 -0
- package/src/DataGrid.tsx +561 -0
- package/src/DateField.tsx +221 -0
- package/src/Dither.tsx +129 -0
- package/src/Drawer.tsx +41 -0
- package/src/DurationField.tsx +98 -0
- package/src/Feed.tsx +74 -0
- package/src/Field.tsx +147 -0
- package/src/Icon.tsx +8 -0
- package/src/Kbd.tsx +6 -0
- package/src/KeyValueEditor.tsx +114 -0
- package/src/Layout.tsx +109 -0
- package/src/Markdown.tsx +63 -0
- package/src/MentionsInput.tsx +150 -0
- package/src/Menu.tsx +111 -0
- package/src/Modal.tsx +40 -0
- package/src/MonthPicker.tsx +190 -0
- package/src/Nav.tsx +20 -0
- package/src/NumberField.tsx +78 -0
- package/src/Pagination.tsx +122 -0
- package/src/PaymentCard.tsx +63 -0
- package/src/Picker.tsx +120 -0
- package/src/PlaceField.tsx +126 -0
- package/src/Popover.tsx +34 -0
- package/src/Progress.tsx +26 -0
- package/src/RecurrenceField.tsx +126 -0
- package/src/RichSelect.tsx +243 -0
- package/src/RichTextEditor.tsx +207 -0
- package/src/ScrollGrid.tsx +190 -0
- package/src/Segments.tsx +70 -0
- package/src/Select.tsx +192 -0
- package/src/Shape.tsx +20 -0
- package/src/Shell.tsx +308 -0
- package/src/SideNav.tsx +147 -0
- package/src/SignaturePad.tsx +120 -0
- package/src/Skeleton.tsx +14 -0
- package/src/Slider.tsx +42 -0
- package/src/Spinner.tsx +7 -0
- package/src/Table.tsx +528 -0
- package/src/Tabs.tsx +67 -0
- package/src/Tag.tsx +15 -0
- package/src/Ticker.tsx +36 -0
- package/src/TimeField.tsx +237 -0
- package/src/Toast.tsx +56 -0
- package/src/Toggle.tsx +42 -0
- package/src/Tooltip.tsx +32 -0
- package/src/Type.tsx +46 -0
- package/src/VirtualList.tsx +386 -0
- package/src/admin/ApiKeys.tsx +136 -0
- package/src/admin/Audit.tsx +93 -0
- package/src/admin/Billing.tsx +123 -0
- package/src/admin/Cart.tsx +96 -0
- package/src/admin/Checkout.tsx +300 -0
- package/src/admin/CsvImport.tsx +174 -0
- package/src/admin/Danger.tsx +110 -0
- package/src/admin/Integrations.tsx +200 -0
- package/src/admin/InviteDialog.tsx +64 -0
- package/src/admin/Members.tsx +186 -0
- package/src/admin/Order.tsx +63 -0
- package/src/admin/PlainTable.tsx +22 -0
- package/src/admin/Plans.tsx +167 -0
- package/src/admin/SaveBar.tsx +112 -0
- package/src/admin/Secret.tsx +37 -0
- package/src/admin/Settings.tsx +76 -0
- package/src/admin/StepBar.tsx +35 -0
- package/src/admin/TagsInput.tsx +79 -0
- package/src/admin/atoms.tsx +33 -0
- package/src/admin/index.ts +16 -0
- package/src/agent/Actions.tsx +85 -0
- package/src/agent/AgentPlan.tsx +49 -0
- package/src/agent/ArtifactPanel.tsx +80 -0
- package/src/agent/Assistant.tsx +96 -0
- package/src/agent/Checkpoint.tsx +14 -0
- package/src/agent/Cite.tsx +39 -0
- package/src/agent/ContextMeter.tsx +31 -0
- package/src/agent/Conversation.tsx +34 -0
- package/src/agent/Message.tsx +53 -0
- package/src/agent/ModelPicker.tsx +65 -0
- package/src/agent/Prompt.tsx +197 -0
- package/src/agent/Prose.tsx +54 -0
- package/src/agent/Question.tsx +52 -0
- package/src/agent/Reasoning.tsx +19 -0
- package/src/agent/Shimmer.tsx +7 -0
- package/src/agent/Sources.tsx +23 -0
- package/src/agent/SpeechInput.tsx +102 -0
- package/src/agent/Suggestions.tsx +22 -0
- package/src/agent/ToolCall.tsx +91 -0
- package/src/agent/TraceViewer.tsx +55 -0
- package/src/agent/bits.tsx +40 -0
- package/src/agent/index.ts +22 -0
- package/src/bind.ts +42 -0
- package/src/collab/AutoTrack.tsx +42 -0
- package/src/collab/Comments.tsx +94 -0
- package/src/collab/Composer.tsx +173 -0
- package/src/collab/ConversationList.tsx +73 -0
- package/src/collab/Cursors.tsx +35 -0
- package/src/collab/DataIntegrity.tsx +45 -0
- package/src/collab/Kanban.tsx +185 -0
- package/src/collab/Lock.tsx +23 -0
- package/src/collab/PresenceStack.tsx +67 -0
- package/src/collab/ShareDialog.tsx +118 -0
- package/src/collab/Status.tsx +42 -0
- package/src/collab/SyncedForm.tsx +19 -0
- package/src/collab/Thread.tsx +273 -0
- package/src/collab/VersionHistory.tsx +49 -0
- package/src/collab/ViewportRail.tsx +49 -0
- package/src/collab/index.ts +16 -0
- package/src/collab/peer.tsx +16 -0
- package/src/fields.ts +5 -0
- package/src/flow/AuthScreen.tsx +40 -0
- package/src/flow/AuthSteps.tsx +120 -0
- package/src/flow/Checklist.tsx +84 -0
- package/src/flow/ConnBanner.tsx +147 -0
- package/src/flow/Flow.tsx +63 -0
- package/src/flow/MfaSetup.tsx +103 -0
- package/src/flow/Otp.tsx +114 -0
- package/src/flow/ReauthDialog.tsx +47 -0
- package/src/flow/ShortcutsOverlay.tsx +60 -0
- package/src/flow/SignGate.tsx +98 -0
- package/src/flow/SignIn.tsx +80 -0
- package/src/flow/SignUp.tsx +71 -0
- package/src/flow/Sso.tsx +38 -0
- package/src/flow/SystemPage.tsx +247 -0
- package/src/flow/Tour.tsx +108 -0
- package/src/flow/index.ts +17 -0
- package/src/flow/kit.tsx +170 -0
- package/src/index.ts +74 -0
- package/src/layout.css +4 -0
- package/src/links.css +4 -0
- package/src/live/ActivityFeed.tsx +49 -0
- package/src/live/Bell.tsx +62 -0
- package/src/live/Connection.tsx +49 -0
- package/src/live/Inbox.tsx +220 -0
- package/src/live/MeetingRoom.tsx +298 -0
- package/src/live/NotificationPreferences.tsx +193 -0
- package/src/live/Prejoin.tsx +140 -0
- package/src/live/PushSoftAsk.tsx +37 -0
- package/src/live/TransportPicker.tsx +163 -0
- package/src/live/index.ts +12 -0
- package/src/live/notify.tsx +16 -0
- package/src/live/shared.tsx +39 -0
- package/src/parts.tsx +93 -0
- package/src/schedule/Agenda.tsx +173 -0
- package/src/schedule/AvailabilityEditor.tsx +164 -0
- package/src/schedule/BookingCalendar.tsx +132 -0
- package/src/schedule/Calendar.tsx +746 -0
- package/src/schedule/EventEditor.tsx +87 -0
- package/src/schedule/EventPopover.tsx +65 -0
- package/src/schedule/MeetingPlanner.tsx +214 -0
- package/src/schedule/MoveSheet.tsx +78 -0
- package/src/schedule/YearView.tsx +163 -0
- package/src/schedule/bits.tsx +51 -0
- package/src/schedule/index.ts +10 -0
- package/src/skeleton-css.ts +4 -0
- package/src/styles.ts +10 -0
- package/src/tokens.d.ts +2 -0
- package/src/type.css +15 -0
- package/src/ui.ts +26 -0
- package/src/use-ui.ts +10 -0
- package/tailwind.css +38 -0
- package/tokens/color.mjs +66 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// What the schedule pack's components share: sweep parts, glyphs, the clock.
|
|
2
|
+
import { RepeatIcon } from '@weft-js/warp-icons'
|
|
3
|
+
import { installSweep, nowIn } from '@weft-js/warp-core'
|
|
4
|
+
import { onCleanup, onMount, state, untrack, watch } from '@weft-js/core'
|
|
5
|
+
import '../styles'
|
|
6
|
+
import '@weft-js/warp-core/styles/app/tokens.css'
|
|
7
|
+
import '@weft-js/warp-core/styles/app/base.css'
|
|
8
|
+
import '@weft-js/warp-core/styles/rich/helpers.css'
|
|
9
|
+
|
|
10
|
+
export const SweepParts = () => <><span data-part="sweep-base" aria-hidden="true"></span><span data-part="sweep" aria-hidden="true"></span></>
|
|
11
|
+
|
|
12
|
+
/** calendar.css and agenda.css default `--_c` on the same element as `data-cal`, so the source's colour is set inline too. */
|
|
13
|
+
export const tint = (cal?: number) => `--_c:var(--warp-color-data-${cal ?? 1})`
|
|
14
|
+
export const RepeatGlyph = () => <RepeatIcon />
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The wall clock for the now line (07 §2.2): the prop when there is one, else null through hydration and the browser's
|
|
18
|
+
* clock a microtask after mount, then on each minute. It pauses while the page is hidden.
|
|
19
|
+
*/
|
|
20
|
+
export function useNow(now: () => string | undefined, zone: () => string | undefined) {
|
|
21
|
+
const value = state<string | null>(untrack(now) ?? null)
|
|
22
|
+
onMount(() => {
|
|
23
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
24
|
+
const tick = () => {
|
|
25
|
+
clearTimeout(timer)
|
|
26
|
+
if (untrack(now) || document.hidden) return
|
|
27
|
+
value.value = nowIn(untrack(zone))
|
|
28
|
+
timer = setTimeout(tick, 60000 - (Date.now() % 60000))
|
|
29
|
+
}
|
|
30
|
+
queueMicrotask(tick)
|
|
31
|
+
document.addEventListener('visibilitychange', tick)
|
|
32
|
+
watch(now, n => { if (n) value.value = n })
|
|
33
|
+
onCleanup(() => { clearTimeout(timer); document.removeEventListener('visibilitychange', tick) })
|
|
34
|
+
})
|
|
35
|
+
return value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The container's width, 0 until it has been measured after mount. */
|
|
39
|
+
export function useWidth(el: () => Element | undefined) {
|
|
40
|
+
const width = state(0)
|
|
41
|
+
onMount(() => {
|
|
42
|
+
const node = el()
|
|
43
|
+
if (!node || typeof ResizeObserver === 'undefined') return
|
|
44
|
+
const ro = new ResizeObserver(r => { width.value = r[0]!.contentRect.width })
|
|
45
|
+
ro.observe(node)
|
|
46
|
+
onCleanup(() => ro.disconnect())
|
|
47
|
+
})
|
|
48
|
+
return width
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const confirmSweep = (el: Element | null | undefined) => { if (el) installSweep(el.ownerDocument).confirm(el) }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The schedule pack (07 §2, §6.4): calendar, agenda, year, booking, planner, availability. One file per family; each imports only its own CSS.
|
|
2
|
+
export { Calendar, type CalendarProps, type EventChange, type RecurrenceScope, type Resource } from './Calendar'
|
|
3
|
+
export { EventPopover, type EventPopoverProps } from './EventPopover'
|
|
4
|
+
export { EventEditor, type EventEditorProps, type CalendarSource } from './EventEditor'
|
|
5
|
+
export { MoveSheet, type MoveSheetProps } from './MoveSheet'
|
|
6
|
+
export { Agenda, AgendaDay, type AgendaProps } from './Agenda'
|
|
7
|
+
export { YearView, type YearViewProps } from './YearView'
|
|
8
|
+
export { BookingCalendar, type BookingCalendarProps, type BookingSlot, type BookingDetails } from './BookingCalendar'
|
|
9
|
+
export { MeetingPlanner, type MeetingPlannerProps, type MeetingDetails } from './MeetingPlanner'
|
|
10
|
+
export { AvailabilityEditor, type AvailabilityEditorProps, type Availability } from './AvailabilityEditor'
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// warp's skeleton CSS for a page that uses `<Boundary pending="shape">` without a <Shape> or <Skeleton> (research 31 §5):
|
|
2
|
+
// `import '@weft-js/warp/skeleton'` once. A shape variant is core markup, so nothing else pulls the CSS in.
|
|
3
|
+
import './styles'
|
|
4
|
+
import '@weft-js/warp-core/styles/components/skeleton.css'
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The shared base every warp component loads first: the spec tokens, base and signature layers (warp-core/styles/
|
|
2
|
+
// warp.css), an app's seed and field defaults from warp() (weft:tokens/warp, empty without the plugin), and the
|
|
3
|
+
// components' plumbing (tone channels, icon strokes, kbd, panels). `links.css` is warp's colour for a bare `<a>`.
|
|
4
|
+
import { flow } from '@weft-js/warp-core'
|
|
5
|
+
import '@weft-js/warp-core/styles/warp.css'
|
|
6
|
+
import 'weft:tokens/warp'
|
|
7
|
+
import '@weft-js/warp-core/styles/components/base.css'
|
|
8
|
+
import './links.css'
|
|
9
|
+
|
|
10
|
+
flow()
|
package/src/tokens.d.ts
ADDED
package/src/type.css
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* Type roles: each role reads the spec's own text tokens (warp.css §Type), in warp.layout so app CSS still wins. */
|
|
2
|
+
@layer warp.layout {
|
|
3
|
+
:where([data-scope="text"]) { margin: 0; }
|
|
4
|
+
:where([data-scope="text"][data-role="display"]) { font-family: var(--warp-text-display-font); font-size: var(--warp-text-display-size); line-height: var(--warp-text-display-leading); font-weight: var(--warp-text-display-weight); letter-spacing: var(--warp-text-display-tracking); }
|
|
5
|
+
:where([data-scope="text"][data-role="title"]) { font-family: var(--warp-text-title-font); font-size: var(--warp-text-title-size); line-height: var(--warp-text-title-leading); font-weight: var(--warp-text-title-weight); letter-spacing: var(--warp-text-title-tracking); }
|
|
6
|
+
:where([data-scope="text"][data-role="heading"]) { font-family: var(--warp-text-heading-font); font-size: var(--warp-text-heading-size); line-height: var(--warp-text-heading-leading); font-weight: var(--warp-text-heading-weight); letter-spacing: var(--warp-text-heading-tracking); }
|
|
7
|
+
:where([data-scope="text"][data-role="body"]) { font-family: var(--warp-text-body-font); font-size: var(--warp-text-body-size); line-height: var(--warp-text-body-leading); }
|
|
8
|
+
:where([data-scope="text"][data-role="label"]) { font-family: var(--warp-text-label-font); font-size: var(--warp-text-label-size); line-height: var(--warp-text-label-leading); font-weight: var(--warp-text-label-weight); letter-spacing: var(--warp-text-label-tracking); text-transform: var(--warp-text-label-case); }
|
|
9
|
+
:where([data-scope="text"][data-role="caption"]) { font-family: var(--warp-text-caption-font); font-size: var(--warp-text-caption-size); line-height: var(--warp-text-caption-leading); }
|
|
10
|
+
:where([data-scope="text"][data-role="eyebrow"]) { font-family: var(--warp-text-eyebrow-font); font-size: var(--warp-text-eyebrow-size); line-height: var(--warp-text-eyebrow-leading); font-weight: var(--warp-text-eyebrow-weight); letter-spacing: var(--warp-text-eyebrow-tracking); text-transform: var(--warp-text-eyebrow-case); }
|
|
11
|
+
:where([data-scope="text"][data-role="code"]) { font-family: var(--warp-text-code-font); font-size: var(--warp-text-code-size); }
|
|
12
|
+
:where([data-scope="text"][data-ink="subtle"]) { color: var(--warp-color-text-subtle); }
|
|
13
|
+
:where([data-scope="text"][data-ink="faint"]) { color: var(--warp-color-text-faint); }
|
|
14
|
+
:where([data-scope="text"][data-tone]) { color: var(--_text); }
|
|
15
|
+
}
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// How warp draws `ui`, from any source (research 45). The `weft()` plugin imports this where `ui=` is used, so a
|
|
2
|
+
// component that never gets a `ui` carries none of it.
|
|
3
|
+
//
|
|
4
|
+
// A lit row is `data-hold`, warp's existing word for "drawn active with no pointer on it", so no component needs a new
|
|
5
|
+
// rule. `data-remote` says only someone else has it, for whoever wants to tint that. `data-press` is a press from
|
|
6
|
+
// anywhere, drawn as `:active` is, so a press shows in the other pane, tab or client and not just as a hover.
|
|
7
|
+
import { interact } from '@weft-js/warp-core'
|
|
8
|
+
import { createRoot, effect } from '@weft-js/core'
|
|
9
|
+
import { seams } from '@weft-js/core/interaction'
|
|
10
|
+
|
|
11
|
+
seams.attach = (el, ui) => {
|
|
12
|
+
const real = interact(el, ui)
|
|
13
|
+
const stop = createRoot(dispose => {
|
|
14
|
+
effect(() => {
|
|
15
|
+
const r = ui.remote.value
|
|
16
|
+
const mine = ui.hover.value || ui.focus.value || ui.pressed.value
|
|
17
|
+
return mine ? 2 : r.hover || r.focus || r.pressed ? 1 : 0
|
|
18
|
+
}, lit => {
|
|
19
|
+
el.toggleAttribute('data-hold', lit > 0)
|
|
20
|
+
el.toggleAttribute('data-remote', lit === 1)
|
|
21
|
+
})
|
|
22
|
+
effect(() => ui.pressed.value || ui.remote.value.pressed > 0, on => el.toggleAttribute('data-press', on))
|
|
23
|
+
return dispose
|
|
24
|
+
}, null)
|
|
25
|
+
return { update() {}, destroy() { real.destroy(); stop(); el.removeAttribute('data-hold'); el.removeAttribute('data-remote'); el.removeAttribute('data-press') } }
|
|
26
|
+
}
|
package/src/use-ui.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { use } from '@weft-js/warp-core/weft'
|
|
2
|
+
import { untrack } from '@weft-js/core'
|
|
3
|
+
import type { Interaction } from '@weft-js/core/interaction'
|
|
4
|
+
|
|
5
|
+
/** `ui={row}`: a row's `attach` on the host. Read once, like every prop that picks what a component is. */
|
|
6
|
+
export function useUi(ui: () => Interaction | undefined, host: () => HTMLElement | undefined) {
|
|
7
|
+
const row = untrack(ui)
|
|
8
|
+
// oxlint-disable-next-line typescript/unbound-method -- interaction() closes over its row: attach reads no `this`
|
|
9
|
+
if (row) use(row.attach, host)
|
|
10
|
+
}
|
package/tailwind.css
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* warp's tokens as Tailwind v4 theme values: `bg-surface`, `text-accent`, `rounded-control`, `font-display`. `inline`
|
|
2
|
+
keeps each utility on the var(), so dials and scopes re-theme utilities the same way they re-theme components. */
|
|
3
|
+
@theme inline {
|
|
4
|
+
--color-page: var(--warp-color-page);
|
|
5
|
+
--color-surface: var(--warp-color-surface);
|
|
6
|
+
--color-surface-raised: var(--warp-color-surface-raised);
|
|
7
|
+
--color-surface-sunken: var(--warp-color-surface-sunken);
|
|
8
|
+
--color-text: var(--warp-color-text);
|
|
9
|
+
--color-text-subtle: var(--warp-color-text-subtle);
|
|
10
|
+
--color-text-faint: var(--warp-color-text-faint);
|
|
11
|
+
--color-border: var(--warp-color-border);
|
|
12
|
+
--color-border-strong: var(--warp-color-border-strong);
|
|
13
|
+
--color-focus: var(--warp-color-focus);
|
|
14
|
+
--color-accent: var(--warp-color-accent-solid);
|
|
15
|
+
--color-accent-soft: var(--warp-color-accent-soft);
|
|
16
|
+
--color-accent-text: var(--warp-color-accent-text);
|
|
17
|
+
--color-on-accent: var(--warp-color-accent-on-solid);
|
|
18
|
+
--color-positive: var(--warp-color-positive-solid);
|
|
19
|
+
--color-caution: var(--warp-color-caution-solid);
|
|
20
|
+
--color-critical: var(--warp-color-critical-solid);
|
|
21
|
+
--color-info: var(--warp-color-info-solid);
|
|
22
|
+
|
|
23
|
+
--font-sans: var(--warp-font-sans);
|
|
24
|
+
--font-display: var(--warp-font-display);
|
|
25
|
+
--font-mono: var(--warp-font-mono);
|
|
26
|
+
|
|
27
|
+
/* one scale for gap, padding and margin: `p-related`, `gap-group`. A numeric `p-3` that isn't on it is D002. */
|
|
28
|
+
--spacing-hair: var(--warp-space-hair);
|
|
29
|
+
--spacing-tight: var(--warp-space-tight);
|
|
30
|
+
--spacing-related: var(--warp-space-related);
|
|
31
|
+
--spacing-group: var(--warp-space-group);
|
|
32
|
+
--spacing-section: var(--warp-space-section);
|
|
33
|
+
--spacing-page: var(--warp-space-page);
|
|
34
|
+
|
|
35
|
+
--radius-mark: var(--warp-radius-mark);
|
|
36
|
+
--radius-control: var(--warp-radius-control);
|
|
37
|
+
--radius-surface: var(--warp-radius-surface);
|
|
38
|
+
}
|
package/tokens/color.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// OKLCH → sRGB with CSS Color 4 gamut mapping (chroma reduced until it fits), WCAG 2.x luminance and contrast.
|
|
2
|
+
// Plain Node, no dependencies: the token build and the contrast gate share it.
|
|
3
|
+
|
|
4
|
+
function oklchToLinear(L, C, H) {
|
|
5
|
+
const h = (H * Math.PI) / 180, a = C * Math.cos(h), b = C * Math.sin(h)
|
|
6
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3
|
|
7
|
+
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3
|
|
8
|
+
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3
|
|
9
|
+
return [
|
|
10
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
11
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
12
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const inGamut = rgb => rgb.every(c => c >= -1e-4 && c <= 1 + 1e-4)
|
|
17
|
+
|
|
18
|
+
/** `{ L, C, H }` → linear sRGB, with chroma reduced (never lightness or hue) until it is inside sRGB. */
|
|
19
|
+
export function toLinear({ L, C, H }) {
|
|
20
|
+
const rgb = oklchToLinear(L, C, H)
|
|
21
|
+
if (inGamut(rgb)) return rgb
|
|
22
|
+
let lo = 0, hi = C
|
|
23
|
+
for (let k = 0; k < 24; k++) {
|
|
24
|
+
const mid = (lo + hi) / 2
|
|
25
|
+
if (inGamut(oklchToLinear(L, mid, H))) lo = mid
|
|
26
|
+
else hi = mid
|
|
27
|
+
}
|
|
28
|
+
return oklchToLinear(L, lo, H).map(v => Math.min(1, Math.max(0, v)))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const encode = v => (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055)
|
|
32
|
+
const decode = v => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4)
|
|
33
|
+
|
|
34
|
+
export const hex = lin => '#' + lin.map(v => Math.round(Math.min(1, Math.max(0, encode(v))) * 255).toString(16).padStart(2, '0')).join('')
|
|
35
|
+
|
|
36
|
+
/** The 8-bit sRGB the browser will actually paint, back in linear light (so checks match the shipped hex). */
|
|
37
|
+
export const quantize = lin => lin.map(v => decode(Math.round(Math.min(1, Math.max(0, encode(v))) * 255) / 255))
|
|
38
|
+
|
|
39
|
+
export const luminance = ([r, g, b]) => 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
40
|
+
export const contrast = (a, b) => {
|
|
41
|
+
const y1 = luminance(a), y2 = luminance(b)
|
|
42
|
+
return (Math.max(y1, y2) + 0.05) / (Math.min(y1, y2) + 0.05)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Linear sRGB `fg` at `alpha` over `bg`. */
|
|
46
|
+
export const over = (fg, bg, alpha) => fg.map((v, i) => v * alpha + bg[i] * (1 - alpha))
|
|
47
|
+
|
|
48
|
+
const toLab = ({ L, C, H }) => [L, C * Math.cos((H * Math.PI) / 180), C * Math.sin((H * Math.PI) / 180)]
|
|
49
|
+
const fromLab = ([L, a, b]) => ({ L, C: Math.hypot(a, b), H: ((Math.atan2(b, a) * 180) / Math.PI + 360) % 360 })
|
|
50
|
+
|
|
51
|
+
/** color-mix(in oklab, a p, b): what the browser does, returned as OKLCH. */
|
|
52
|
+
export const mix = (a, b, p) => fromLab(toLab(a).map((v, i) => v * p + toLab(b)[i] * (1 - p)))
|
|
53
|
+
|
|
54
|
+
/** `#rrggbb` → `{ L, C, H }`, so a brand hex can seed the accent. */
|
|
55
|
+
export function fromHex(text) {
|
|
56
|
+
const n = parseInt(text.replace('#', ''), 16)
|
|
57
|
+
const [r, g, b] = [(n >> 16) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255].map(decode)
|
|
58
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b)
|
|
59
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b)
|
|
60
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b)
|
|
61
|
+
return fromLab([
|
|
62
|
+
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
63
|
+
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
64
|
+
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
|
65
|
+
])
|
|
66
|
+
}
|