@weft-js/warp 0.0.0-stage → 0.1.1

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.
Files changed (180) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +55 -2
  3. package/package.json +43 -4
  4. package/plugin.d.mts +30 -0
  5. package/plugin.mjs +358 -0
  6. package/src/Accordion.tsx +58 -0
  7. package/src/Alert.tsx +25 -0
  8. package/src/Badge.tsx +17 -0
  9. package/src/Button.tsx +100 -0
  10. package/src/Calendar.tsx +162 -0
  11. package/src/Canvas.tsx +110 -0
  12. package/src/Card.tsx +29 -0
  13. package/src/Chart.tsx +133 -0
  14. package/src/Charts.tsx +69 -0
  15. package/src/Chip.tsx +27 -0
  16. package/src/Choice.tsx +105 -0
  17. package/src/CodeBlock.tsx +65 -0
  18. package/src/CodeInput.tsx +130 -0
  19. package/src/Combobox.tsx +113 -0
  20. package/src/CommandPalette.tsx +348 -0
  21. package/src/DataGrid.tsx +561 -0
  22. package/src/DateField.tsx +221 -0
  23. package/src/Dither.tsx +129 -0
  24. package/src/Drawer.tsx +41 -0
  25. package/src/DurationField.tsx +98 -0
  26. package/src/Feed.tsx +74 -0
  27. package/src/Field.tsx +147 -0
  28. package/src/Icon.tsx +8 -0
  29. package/src/Kbd.tsx +6 -0
  30. package/src/KeyValueEditor.tsx +114 -0
  31. package/src/Layout.tsx +109 -0
  32. package/src/Markdown.tsx +63 -0
  33. package/src/MentionsInput.tsx +150 -0
  34. package/src/Menu.tsx +111 -0
  35. package/src/Modal.tsx +40 -0
  36. package/src/MonthPicker.tsx +190 -0
  37. package/src/Nav.tsx +20 -0
  38. package/src/NumberField.tsx +78 -0
  39. package/src/Pagination.tsx +122 -0
  40. package/src/PaymentCard.tsx +63 -0
  41. package/src/Picker.tsx +120 -0
  42. package/src/PlaceField.tsx +126 -0
  43. package/src/Popover.tsx +34 -0
  44. package/src/Progress.tsx +26 -0
  45. package/src/RecurrenceField.tsx +126 -0
  46. package/src/RichSelect.tsx +243 -0
  47. package/src/RichTextEditor.tsx +207 -0
  48. package/src/ScrollGrid.tsx +190 -0
  49. package/src/Segments.tsx +70 -0
  50. package/src/Select.tsx +192 -0
  51. package/src/Shape.tsx +20 -0
  52. package/src/Shell.tsx +308 -0
  53. package/src/SideNav.tsx +147 -0
  54. package/src/SignaturePad.tsx +120 -0
  55. package/src/Skeleton.tsx +14 -0
  56. package/src/Slider.tsx +42 -0
  57. package/src/Spinner.tsx +7 -0
  58. package/src/Table.tsx +528 -0
  59. package/src/Tabs.tsx +67 -0
  60. package/src/Tag.tsx +15 -0
  61. package/src/Ticker.tsx +36 -0
  62. package/src/TimeField.tsx +237 -0
  63. package/src/Toast.tsx +56 -0
  64. package/src/Toggle.tsx +42 -0
  65. package/src/Tooltip.tsx +32 -0
  66. package/src/Type.tsx +46 -0
  67. package/src/VirtualList.tsx +386 -0
  68. package/src/admin/ApiKeys.tsx +136 -0
  69. package/src/admin/Audit.tsx +93 -0
  70. package/src/admin/Billing.tsx +123 -0
  71. package/src/admin/Cart.tsx +96 -0
  72. package/src/admin/Checkout.tsx +300 -0
  73. package/src/admin/CsvImport.tsx +174 -0
  74. package/src/admin/Danger.tsx +110 -0
  75. package/src/admin/Integrations.tsx +200 -0
  76. package/src/admin/InviteDialog.tsx +64 -0
  77. package/src/admin/Members.tsx +186 -0
  78. package/src/admin/Order.tsx +63 -0
  79. package/src/admin/PlainTable.tsx +22 -0
  80. package/src/admin/Plans.tsx +167 -0
  81. package/src/admin/SaveBar.tsx +112 -0
  82. package/src/admin/Secret.tsx +37 -0
  83. package/src/admin/Settings.tsx +76 -0
  84. package/src/admin/StepBar.tsx +35 -0
  85. package/src/admin/TagsInput.tsx +79 -0
  86. package/src/admin/atoms.tsx +33 -0
  87. package/src/admin/index.ts +16 -0
  88. package/src/agent/Actions.tsx +85 -0
  89. package/src/agent/AgentPlan.tsx +49 -0
  90. package/src/agent/ArtifactPanel.tsx +80 -0
  91. package/src/agent/Assistant.tsx +96 -0
  92. package/src/agent/Checkpoint.tsx +14 -0
  93. package/src/agent/Cite.tsx +39 -0
  94. package/src/agent/ContextMeter.tsx +31 -0
  95. package/src/agent/Conversation.tsx +34 -0
  96. package/src/agent/Message.tsx +53 -0
  97. package/src/agent/ModelPicker.tsx +65 -0
  98. package/src/agent/Prompt.tsx +197 -0
  99. package/src/agent/Prose.tsx +54 -0
  100. package/src/agent/Question.tsx +52 -0
  101. package/src/agent/Reasoning.tsx +19 -0
  102. package/src/agent/Shimmer.tsx +7 -0
  103. package/src/agent/Sources.tsx +23 -0
  104. package/src/agent/SpeechInput.tsx +102 -0
  105. package/src/agent/Suggestions.tsx +22 -0
  106. package/src/agent/ToolCall.tsx +91 -0
  107. package/src/agent/TraceViewer.tsx +55 -0
  108. package/src/agent/bits.tsx +40 -0
  109. package/src/agent/index.ts +22 -0
  110. package/src/bind.ts +42 -0
  111. package/src/collab/AutoTrack.tsx +42 -0
  112. package/src/collab/Comments.tsx +94 -0
  113. package/src/collab/Composer.tsx +173 -0
  114. package/src/collab/ConversationList.tsx +73 -0
  115. package/src/collab/Cursors.tsx +35 -0
  116. package/src/collab/DataIntegrity.tsx +45 -0
  117. package/src/collab/Kanban.tsx +185 -0
  118. package/src/collab/Lock.tsx +23 -0
  119. package/src/collab/PresenceStack.tsx +67 -0
  120. package/src/collab/ShareDialog.tsx +118 -0
  121. package/src/collab/Status.tsx +42 -0
  122. package/src/collab/SyncedForm.tsx +19 -0
  123. package/src/collab/Thread.tsx +273 -0
  124. package/src/collab/VersionHistory.tsx +49 -0
  125. package/src/collab/ViewportRail.tsx +49 -0
  126. package/src/collab/index.ts +16 -0
  127. package/src/collab/peer.tsx +16 -0
  128. package/src/fields.ts +5 -0
  129. package/src/flow/AuthScreen.tsx +40 -0
  130. package/src/flow/AuthSteps.tsx +120 -0
  131. package/src/flow/Checklist.tsx +84 -0
  132. package/src/flow/ConnBanner.tsx +147 -0
  133. package/src/flow/Flow.tsx +63 -0
  134. package/src/flow/MfaSetup.tsx +103 -0
  135. package/src/flow/Otp.tsx +114 -0
  136. package/src/flow/ReauthDialog.tsx +47 -0
  137. package/src/flow/ShortcutsOverlay.tsx +60 -0
  138. package/src/flow/SignGate.tsx +98 -0
  139. package/src/flow/SignIn.tsx +80 -0
  140. package/src/flow/SignUp.tsx +71 -0
  141. package/src/flow/Sso.tsx +38 -0
  142. package/src/flow/SystemPage.tsx +247 -0
  143. package/src/flow/Tour.tsx +108 -0
  144. package/src/flow/index.ts +17 -0
  145. package/src/flow/kit.tsx +170 -0
  146. package/src/index.ts +74 -0
  147. package/src/layout.css +4 -0
  148. package/src/links.css +4 -0
  149. package/src/live/ActivityFeed.tsx +49 -0
  150. package/src/live/Bell.tsx +62 -0
  151. package/src/live/Connection.tsx +49 -0
  152. package/src/live/Inbox.tsx +220 -0
  153. package/src/live/MeetingRoom.tsx +298 -0
  154. package/src/live/NotificationPreferences.tsx +193 -0
  155. package/src/live/Prejoin.tsx +140 -0
  156. package/src/live/PushSoftAsk.tsx +37 -0
  157. package/src/live/TransportPicker.tsx +163 -0
  158. package/src/live/index.ts +12 -0
  159. package/src/live/notify.tsx +16 -0
  160. package/src/live/shared.tsx +39 -0
  161. package/src/parts.tsx +93 -0
  162. package/src/schedule/Agenda.tsx +173 -0
  163. package/src/schedule/AvailabilityEditor.tsx +164 -0
  164. package/src/schedule/BookingCalendar.tsx +132 -0
  165. package/src/schedule/Calendar.tsx +746 -0
  166. package/src/schedule/EventEditor.tsx +87 -0
  167. package/src/schedule/EventPopover.tsx +65 -0
  168. package/src/schedule/MeetingPlanner.tsx +214 -0
  169. package/src/schedule/MoveSheet.tsx +78 -0
  170. package/src/schedule/YearView.tsx +163 -0
  171. package/src/schedule/bits.tsx +51 -0
  172. package/src/schedule/index.ts +10 -0
  173. package/src/skeleton-css.ts +4 -0
  174. package/src/styles.ts +10 -0
  175. package/src/tokens.d.ts +2 -0
  176. package/src/type.css +15 -0
  177. package/src/ui.ts +26 -0
  178. package/src/use-ui.ts +10 -0
  179. package/tailwind.css +38 -0
  180. 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()
@@ -0,0 +1,2 @@
1
+ // warp()'s virtual stylesheet: an app's seed and field defaults, and empty without the plugin.
2
+ declare module 'weft:tokens/warp'
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
+ }
@@ -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
+ }