@preset.nz/ux-kit 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.
- package/LICENSE +21 -0
- package/README.md +117 -2
- package/THIRD_PARTY_NOTICES.md +253 -0
- package/package.json +50 -3
- package/src/chat/index.ts +9 -0
- package/src/chat/message.tsx +80 -0
- package/src/chat/prompt-input.tsx +121 -0
- package/src/chat/reasoning.tsx +182 -0
- package/src/chat/scroller.tsx +147 -0
- package/src/chat/suggestion.tsx +65 -0
- package/src/chat/tool.tsx +143 -0
- package/src/chat/types.ts +8 -0
- package/src/components/avatar.tsx +35 -0
- package/src/components/badge.tsx +40 -0
- package/src/components/breadcrumb.tsx +100 -0
- package/src/components/button.tsx +56 -0
- package/src/components/card.tsx +75 -0
- package/src/components/checkbox.tsx +25 -0
- package/src/components/color-field.tsx +261 -0
- package/src/components/color-swatch.tsx +52 -0
- package/src/components/context-menu.tsx +249 -0
- package/src/components/dialog.tsx +133 -0
- package/src/components/dropdown-menu.tsx +249 -0
- package/src/components/empty-state.tsx +35 -0
- package/src/components/input.tsx +20 -0
- package/src/components/label.tsx +19 -0
- package/src/components/navigation-menu.tsx +150 -0
- package/src/components/number-field.tsx +432 -0
- package/src/components/popover.tsx +75 -0
- package/src/components/select.tsx +189 -0
- package/src/components/separator.tsx +21 -0
- package/src/components/sheet.tsx +132 -0
- package/src/components/side-panel.tsx +189 -0
- package/src/components/sidebar.tsx +698 -0
- package/src/components/skeleton.tsx +14 -0
- package/src/components/slider.tsx +71 -0
- package/src/components/snackbar.tsx +143 -0
- package/src/components/status-bar.tsx +45 -0
- package/src/components/tabs.tsx +75 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/toggle-group.tsx +86 -0
- package/src/components/toggle.tsx +43 -0
- package/src/components/toolbar.tsx +304 -0
- package/src/components/tooltip.tsx +54 -0
- package/src/graphics/adsr.ts +78 -0
- package/src/graphics/curve-editor.tsx +567 -0
- package/src/graphics/curve-edits.ts +146 -0
- package/src/graphics/index.ts +16 -0
- package/src/graphics/scale.ts +25 -0
- package/src/graphics/use-canvas.ts +56 -0
- package/src/graphics/use-tokens.ts +35 -0
- package/src/icons.ts +3 -0
- package/src/index.css +199 -0
- package/src/index.ts +35 -0
- package/src/lib/use-mobile.ts +18 -0
- package/src/lib/utils.ts +6 -0
- package/src/shadcn-tailwind.css +665 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Basis,
|
|
3
|
+
type Curve,
|
|
4
|
+
type CurvePoint,
|
|
5
|
+
clamp,
|
|
6
|
+
invlerp,
|
|
7
|
+
TENSION_STRENGTH,
|
|
8
|
+
} from "@preset.nz/math"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Limits on a curve, applied to every proposed edit: a drag, a nudge, an added or removed
|
|
12
|
+
* point, a basis or sustain change. Return the curve to show (adjusted if need be), or `null`
|
|
13
|
+
* to refuse the edit. `prev` is the curve before the edit, or the curve at the start of a
|
|
14
|
+
* drag. An ADSR envelope is a curve with limits (`constrainAdsr`), not a different editor.
|
|
15
|
+
*/
|
|
16
|
+
export type CurveConstraint = (next: Curve, prev: Curve) => Curve | null
|
|
17
|
+
|
|
18
|
+
export interface Domain {
|
|
19
|
+
x: readonly [number, number]
|
|
20
|
+
y: readonly [number, number]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `next` through the constraint, or `next` itself without one. */
|
|
24
|
+
export function propose(next: Curve, prev: Curve, constrain?: CurveConstraint): Curve | null {
|
|
25
|
+
return constrain ? constrain(next, prev) : next
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Point `i` of `start` moved to `(x, y)`, kept inside the domain. After the constraint, the
|
|
30
|
+
* moved point is held between its neighbours, so the points stay sorted while indices stay put.
|
|
31
|
+
*/
|
|
32
|
+
export function movePoint(
|
|
33
|
+
start: Curve,
|
|
34
|
+
i: number,
|
|
35
|
+
x: number,
|
|
36
|
+
y: number,
|
|
37
|
+
domain: Domain,
|
|
38
|
+
constrain?: CurveConstraint,
|
|
39
|
+
): Curve | null {
|
|
40
|
+
const points = start.points.map((p) => ({ ...p }))
|
|
41
|
+
const p = points[i]
|
|
42
|
+
if (!p) return null
|
|
43
|
+
p.x = clamp(x, domain.x[0], domain.x[1])
|
|
44
|
+
p.y = clamp(y, domain.y[0], domain.y[1])
|
|
45
|
+
const next = propose({ ...start, points }, start, constrain)
|
|
46
|
+
return next && keepOrder(next, i)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Point `i` held between its neighbours. Equal x is allowed: it makes a step. */
|
|
50
|
+
function keepOrder(curve: Curve, i: number): Curve {
|
|
51
|
+
const pts = curve.points
|
|
52
|
+
const p = pts[i]
|
|
53
|
+
if (!p) return curve
|
|
54
|
+
const lo = pts[i - 1]?.x ?? -Infinity
|
|
55
|
+
const hi = pts[i + 1]?.x ?? Infinity
|
|
56
|
+
if (p.x >= lo && p.x <= hi) return curve
|
|
57
|
+
const points = pts.map((q, j) => (j === i ? { ...q, x: clamp(q.x, lo, hi) } : q))
|
|
58
|
+
return { ...curve, points }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A point added at `(x, y)`, in order. It takes the basis and tension of the segment it
|
|
63
|
+
* splits, so the curve around it keeps its character. Returns the new curve and the new
|
|
64
|
+
* point's index.
|
|
65
|
+
*/
|
|
66
|
+
export function addPoint(
|
|
67
|
+
curve: Curve,
|
|
68
|
+
x: number,
|
|
69
|
+
y: number,
|
|
70
|
+
domain: Domain,
|
|
71
|
+
constrain?: CurveConstraint,
|
|
72
|
+
): { curve: Curve; index: number } | null {
|
|
73
|
+
const px = clamp(x, domain.x[0], domain.x[1])
|
|
74
|
+
const py = clamp(y, domain.y[0], domain.y[1])
|
|
75
|
+
let index = curve.points.findIndex((p) => p.x > px)
|
|
76
|
+
if (index < 0) index = curve.points.length
|
|
77
|
+
const before = curve.points[index - 1] ?? curve.points[index]
|
|
78
|
+
const point: CurvePoint = { x: px, y: py, basis: before?.basis ?? "linear" }
|
|
79
|
+
if (before?.tension !== undefined) point.tension = before.tension
|
|
80
|
+
const points = [...curve.points.slice(0, index), point, ...curve.points.slice(index)]
|
|
81
|
+
const next: Curve = { ...curve, points }
|
|
82
|
+
if (curve.sustain !== undefined && index <= curve.sustain) next.sustain = curve.sustain + 1
|
|
83
|
+
const out = propose(next, curve, constrain)
|
|
84
|
+
return out && { curve: out, index }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Point `i` removed. The last point can't be. Removing the sustain point clears sustain. */
|
|
88
|
+
export function removePoint(curve: Curve, i: number, constrain?: CurveConstraint): Curve | null {
|
|
89
|
+
if (curve.points.length <= 1 || !curve.points[i]) return null
|
|
90
|
+
const next: Curve = { points: curve.points.filter((_, j) => j !== i) }
|
|
91
|
+
const s = curve.sustain
|
|
92
|
+
if (s !== undefined && s !== i) next.sustain = s > i ? s - 1 : s
|
|
93
|
+
return propose(next, curve, constrain)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Point `i`'s basis. Tension only means something on a linear segment, so others drop it. */
|
|
97
|
+
export function setBasis(
|
|
98
|
+
curve: Curve,
|
|
99
|
+
i: number,
|
|
100
|
+
basis: Basis,
|
|
101
|
+
constrain?: CurveConstraint,
|
|
102
|
+
): Curve | null {
|
|
103
|
+
const points = curve.points.map((p, j) => {
|
|
104
|
+
if (j !== i) return p
|
|
105
|
+
const q: CurvePoint = { ...p, basis }
|
|
106
|
+
if (basis !== "linear") delete q.tension
|
|
107
|
+
return q
|
|
108
|
+
})
|
|
109
|
+
return propose({ ...curve, points }, curve, constrain)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Sustain on point `i`, or off when it is already there. */
|
|
113
|
+
export function toggleSustain(curve: Curve, i: number, constrain?: CurveConstraint): Curve | null {
|
|
114
|
+
const next: Curve = { points: curve.points }
|
|
115
|
+
if (curve.sustain !== i) next.sustain = i
|
|
116
|
+
return propose(next, curve, constrain)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The tension of the segment from point `i`, -1..1. Zero removes it. */
|
|
120
|
+
export function setTension(
|
|
121
|
+
curve: Curve,
|
|
122
|
+
i: number,
|
|
123
|
+
tension: number,
|
|
124
|
+
constrain?: CurveConstraint,
|
|
125
|
+
): Curve | null {
|
|
126
|
+
const t = clamp(tension, -1, 1)
|
|
127
|
+
const points = curve.points.map((p, j) => {
|
|
128
|
+
if (j !== i) return p
|
|
129
|
+
const q: CurvePoint = { ...p }
|
|
130
|
+
if (Math.abs(t) < 1e-3) delete q.tension
|
|
131
|
+
else q.tension = t
|
|
132
|
+
return q
|
|
133
|
+
})
|
|
134
|
+
return propose({ ...curve, points }, curve, constrain)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The tension that puts a linear segment's midpoint at `y`. The warp at t = 0.5 is
|
|
139
|
+
* `1 / (e^(k/2) + 1)`, so `k = 2 ln(1/w − 1)` where `w` is how far up the segment `y` sits.
|
|
140
|
+
* `null` on a flat segment, where the midpoint can't move.
|
|
141
|
+
*/
|
|
142
|
+
export function tensionForMidpoint(y0: number, y1: number, y: number): number | null {
|
|
143
|
+
if (Math.abs(y1 - y0) < 1e-9) return null
|
|
144
|
+
const w = clamp(invlerp(y, y0, y1), 0.005, 0.995)
|
|
145
|
+
return clamp((2 * Math.log(1 / w - 1)) / TENSION_STRENGTH, -1, 1)
|
|
146
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { type Adsr, adsrCurve, adsrOf, constrainAdsr } from "./adsr"
|
|
2
|
+
export { type CurveEditKind, CurveEditor, type CurveEditorProps } from "./curve-editor"
|
|
3
|
+
export {
|
|
4
|
+
addPoint,
|
|
5
|
+
type CurveConstraint,
|
|
6
|
+
type Domain,
|
|
7
|
+
movePoint,
|
|
8
|
+
removePoint,
|
|
9
|
+
setBasis,
|
|
10
|
+
setTension,
|
|
11
|
+
tensionForMidpoint,
|
|
12
|
+
toggleSustain,
|
|
13
|
+
} from "./curve-edits"
|
|
14
|
+
export { type LinearScale, linearScale } from "./scale"
|
|
15
|
+
export { type Size, useCanvasDraw, useSize } from "./use-canvas"
|
|
16
|
+
export { useTokenColours } from "./use-tokens"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { fitUnclamped } from "@preset.nz/math"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One axis's mapping between data and pixels. Both layers of a view draw through it, and
|
|
5
|
+
* pointer events come back through `invert`, so a handle can't drift off its line.
|
|
6
|
+
*/
|
|
7
|
+
export interface LinearScale {
|
|
8
|
+
(v: number): number
|
|
9
|
+
invert: (px: number) => number
|
|
10
|
+
domain: readonly [number, number]
|
|
11
|
+
range: readonly [number, number]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function linearScale(
|
|
15
|
+
domain: readonly [number, number],
|
|
16
|
+
range: readonly [number, number],
|
|
17
|
+
): LinearScale {
|
|
18
|
+
const [d0, d1] = domain
|
|
19
|
+
const [r0, r1] = range
|
|
20
|
+
const scale = ((v: number) => fitUnclamped(v, d0, d1, r0, r1)) as LinearScale
|
|
21
|
+
scale.invert = (px: number) => fitUnclamped(px, r0, r1, d0, d1)
|
|
22
|
+
scale.domain = domain
|
|
23
|
+
scale.range = range
|
|
24
|
+
return scale
|
|
25
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type RefObject, useEffect, useLayoutEffect, useRef, useState } from "react"
|
|
2
|
+
|
|
3
|
+
export interface Size {
|
|
4
|
+
width: number
|
|
5
|
+
height: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** An element's size in CSS pixels, kept current by a `ResizeObserver`. */
|
|
9
|
+
export function useSize(ref: RefObject<HTMLElement | null>): Size {
|
|
10
|
+
const [size, setSize] = useState<Size>({ width: 0, height: 0 })
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const el = ref.current
|
|
13
|
+
if (!el) return
|
|
14
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
15
|
+
if (!entry) return
|
|
16
|
+
const { width, height } = entry.contentRect
|
|
17
|
+
setSize((s) => (s.width === width && s.height === height ? s : { width, height }))
|
|
18
|
+
})
|
|
19
|
+
observer.observe(el)
|
|
20
|
+
return () => observer.disconnect()
|
|
21
|
+
}, [ref])
|
|
22
|
+
return size
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Draws on a canvas at the device's pixel ratio, at most once per animation frame. `draw`
|
|
27
|
+
* gets a context already scaled to CSS pixels. It runs again whenever `deps` change.
|
|
28
|
+
*/
|
|
29
|
+
export function useCanvasDraw(
|
|
30
|
+
canvas: RefObject<HTMLCanvasElement | null>,
|
|
31
|
+
size: Size,
|
|
32
|
+
draw: (ctx: CanvasRenderingContext2D, size: Size) => void,
|
|
33
|
+
deps: readonly unknown[],
|
|
34
|
+
) {
|
|
35
|
+
const latest = useRef(draw)
|
|
36
|
+
useLayoutEffect(() => {
|
|
37
|
+
latest.current = draw
|
|
38
|
+
})
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const el = canvas.current
|
|
41
|
+
if (!el || size.width === 0 || size.height === 0) return
|
|
42
|
+
const frame = requestAnimationFrame(() => {
|
|
43
|
+
const dpr = window.devicePixelRatio || 1
|
|
44
|
+
const w = Math.round(size.width * dpr)
|
|
45
|
+
const h = Math.round(size.height * dpr)
|
|
46
|
+
if (el.width !== w) el.width = w
|
|
47
|
+
if (el.height !== h) el.height = h
|
|
48
|
+
const ctx = el.getContext("2d")
|
|
49
|
+
if (!ctx) return
|
|
50
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
51
|
+
ctx.clearRect(0, 0, size.width, size.height)
|
|
52
|
+
latest.current(ctx, size)
|
|
53
|
+
})
|
|
54
|
+
return () => cancelAnimationFrame(frame)
|
|
55
|
+
}, [canvas, size, ...deps]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
56
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The current values of some colour tokens, for drawing on a canvas, which can't read CSS
|
|
5
|
+
* variables. Read again when the theme changes: the `.dark` class or a style on `<html>`, or
|
|
6
|
+
* the system appearance.
|
|
7
|
+
*/
|
|
8
|
+
export function useTokenColours<K extends string>(names: readonly K[]): Record<K, string> {
|
|
9
|
+
const key = names.join(",")
|
|
10
|
+
const [colours, setColours] = useState(() => read(names))
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const list = key.split(",") as K[]
|
|
13
|
+
const update = () => setColours(read(list))
|
|
14
|
+
update()
|
|
15
|
+
const observer = new MutationObserver(update)
|
|
16
|
+
observer.observe(document.documentElement, {
|
|
17
|
+
attributes: true,
|
|
18
|
+
attributeFilter: ["class", "style", "data-theme"],
|
|
19
|
+
})
|
|
20
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)")
|
|
21
|
+
media.addEventListener("change", update)
|
|
22
|
+
return () => {
|
|
23
|
+
observer.disconnect()
|
|
24
|
+
media.removeEventListener("change", update)
|
|
25
|
+
}
|
|
26
|
+
}, [key])
|
|
27
|
+
return colours
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function read<K extends string>(names: readonly K[]): Record<K, string> {
|
|
31
|
+
const style = getComputedStyle(document.documentElement)
|
|
32
|
+
const out = {} as Record<K, string>
|
|
33
|
+
for (const name of names) out[name] = style.getPropertyValue(`--${name}`).trim() || "currentColor"
|
|
34
|
+
return out
|
|
35
|
+
}
|
package/src/icons.ts
ADDED
package/src/index.css
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/* @preset.nz/ux-kit: tokens, palettes and fonts.
|
|
2
|
+
Import this after tailwindcss, and add an @source line for the kit's src:
|
|
3
|
+
|
|
4
|
+
@import "tailwindcss";
|
|
5
|
+
@source "../node_modules/@preset.nz/ux-kit/src";
|
|
6
|
+
@import "@preset.nz/ux-kit/index.css";
|
|
7
|
+
|
|
8
|
+
Seeded from Strata's index.css. Dark mode is a .dark class on <html>. */
|
|
9
|
+
@import "tw-animate-css";
|
|
10
|
+
@import "./shadcn-tailwind.css";
|
|
11
|
+
|
|
12
|
+
/* Two families only (Georg 2026-10-03): Geist for the interface, JetBrains Mono for values and code. */
|
|
13
|
+
@import "@fontsource-variable/geist";
|
|
14
|
+
@import "@fontsource-variable/jetbrains-mono";
|
|
15
|
+
|
|
16
|
+
@custom-variant dark (&:is(.dark *));
|
|
17
|
+
|
|
18
|
+
@theme inline {
|
|
19
|
+
/* Headings use the interface face; hierarchy comes from size and weight. */
|
|
20
|
+
--font-heading: 'Geist Variable', ui-sans-serif, system-ui, sans-serif;
|
|
21
|
+
/* Body text face. */
|
|
22
|
+
--font-sans: 'Geist Variable', ui-sans-serif, system-ui, sans-serif;
|
|
23
|
+
--font-mono: 'JetBrains Mono Variable', monospace;
|
|
24
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
25
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
26
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
27
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
28
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
29
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
30
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
31
|
+
--color-sidebar: var(--sidebar);
|
|
32
|
+
--color-chart-5: var(--chart-5);
|
|
33
|
+
--color-chart-4: var(--chart-4);
|
|
34
|
+
--color-chart-3: var(--chart-3);
|
|
35
|
+
--color-chart-2: var(--chart-2);
|
|
36
|
+
--color-chart-1: var(--chart-1);
|
|
37
|
+
--color-ring: var(--ring);
|
|
38
|
+
--color-input: var(--input);
|
|
39
|
+
--color-border: var(--border);
|
|
40
|
+
--color-destructive: var(--destructive);
|
|
41
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
42
|
+
--color-warning: var(--warning);
|
|
43
|
+
--color-selection: var(--selection);
|
|
44
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
45
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
46
|
+
--color-accent: var(--accent);
|
|
47
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
48
|
+
--color-muted: var(--muted);
|
|
49
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
50
|
+
--color-secondary: var(--secondary);
|
|
51
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
52
|
+
--color-primary: var(--primary);
|
|
53
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
54
|
+
--color-popover: var(--popover);
|
|
55
|
+
--color-card-foreground: var(--card-foreground);
|
|
56
|
+
--color-card: var(--card);
|
|
57
|
+
--color-foreground: var(--foreground);
|
|
58
|
+
--color-background: var(--background);
|
|
59
|
+
--radius-sm: calc(var(--radius) * 0.6);
|
|
60
|
+
--radius-md: calc(var(--radius) * 0.8);
|
|
61
|
+
--radius-lg: var(--radius);
|
|
62
|
+
--radius-xl: calc(var(--radius) * 1.4);
|
|
63
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
64
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
65
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
:root {
|
|
69
|
+
/* Petrol-tinted neutrals; no background or fill is pure white or black (Georg 2026-10-03). */
|
|
70
|
+
--background: oklch(0.972 0.006 215);
|
|
71
|
+
--foreground: oklch(0.2 0.012 215);
|
|
72
|
+
--card: oklch(0.985 0.004 215);
|
|
73
|
+
--card-foreground: oklch(0.2 0.012 215);
|
|
74
|
+
--popover: oklch(0.985 0.004 215);
|
|
75
|
+
--popover-foreground: oklch(0.2 0.012 215);
|
|
76
|
+
--primary: oklch(0.5 0.085 210);
|
|
77
|
+
--primary-foreground: oklch(0.97 0.008 215);
|
|
78
|
+
--secondary: oklch(0.935 0.006 215);
|
|
79
|
+
--secondary-foreground: oklch(0.25 0.012 215);
|
|
80
|
+
--muted: oklch(0.935 0.006 215);
|
|
81
|
+
--muted-foreground: oklch(0.5 0.012 215);
|
|
82
|
+
--accent: oklch(0.93 0.025 210);
|
|
83
|
+
--accent-foreground: oklch(0.3 0.05 210);
|
|
84
|
+
--destructive: oklch(0.55 0.14 27);
|
|
85
|
+
--destructive-foreground: oklch(0.97 0.008 215);
|
|
86
|
+
--warning: oklch(0.7 0.12 70);
|
|
87
|
+
--warning-foreground: oklch(0.25 0.04 70);
|
|
88
|
+
--border: oklch(0.87 0.008 215);
|
|
89
|
+
--input: oklch(0.87 0.008 215);
|
|
90
|
+
--selection: oklch(0.66 0.04 210); /* quieter than primary: selected things, not actions */
|
|
91
|
+
--ring: oklch(0.5 0.085 210);
|
|
92
|
+
--chart-1: oklch(0.5 0.085 210);
|
|
93
|
+
--chart-2: oklch(0.7 0.12 70);
|
|
94
|
+
--chart-3: oklch(0.62 0.07 180);
|
|
95
|
+
--chart-4: oklch(0.42 0.05 240);
|
|
96
|
+
--chart-5: oklch(0.6 0.01 215);
|
|
97
|
+
--sidebar: oklch(0.955 0.006 215);
|
|
98
|
+
--sidebar-foreground: oklch(0.2 0.012 215);
|
|
99
|
+
--sidebar-primary: oklch(0.5 0.085 210);
|
|
100
|
+
--sidebar-primary-foreground: oklch(0.97 0.008 215);
|
|
101
|
+
--sidebar-accent: oklch(0.92 0.025 210);
|
|
102
|
+
--sidebar-accent-foreground: oklch(0.3 0.05 210);
|
|
103
|
+
--sidebar-border: oklch(0.87 0.008 215);
|
|
104
|
+
--sidebar-ring: oklch(0.5 0.085 210);
|
|
105
|
+
--radius: 0.125rem; /* 2px: industrial, not soft SaaS (Georg 2026-10-03; iterating) */
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.dark {
|
|
109
|
+
/* Petrol-tinted neutrals; no background or fill is pure white or black (Georg 2026-10-03). */
|
|
110
|
+
--background: oklch(0.19 0.01 215);
|
|
111
|
+
--foreground: oklch(0.93 0.006 215);
|
|
112
|
+
--card: oklch(0.23 0.012 215);
|
|
113
|
+
--card-foreground: oklch(0.93 0.006 215);
|
|
114
|
+
--popover: oklch(0.23 0.012 215);
|
|
115
|
+
--popover-foreground: oklch(0.93 0.006 215);
|
|
116
|
+
--primary: oklch(0.68 0.09 205);
|
|
117
|
+
--primary-foreground: oklch(0.18 0.02 215);
|
|
118
|
+
--secondary: oklch(0.28 0.012 215);
|
|
119
|
+
--secondary-foreground: oklch(0.93 0.006 215);
|
|
120
|
+
--muted: oklch(0.28 0.012 215);
|
|
121
|
+
--muted-foreground: oklch(0.68 0.012 215);
|
|
122
|
+
--accent: oklch(0.32 0.04 210);
|
|
123
|
+
--accent-foreground: oklch(0.93 0.006 215);
|
|
124
|
+
--destructive: oklch(0.66 0.13 25);
|
|
125
|
+
--destructive-foreground: oklch(0.18 0.02 215);
|
|
126
|
+
--warning: oklch(0.76 0.11 70);
|
|
127
|
+
--warning-foreground: oklch(0.22 0.04 70);
|
|
128
|
+
--border: oklch(0.32 0.012 215);
|
|
129
|
+
--input: oklch(0.35 0.012 215);
|
|
130
|
+
--selection: oklch(0.56 0.045 205);
|
|
131
|
+
--ring: oklch(0.68 0.09 205);
|
|
132
|
+
--chart-1: oklch(0.68 0.09 205);
|
|
133
|
+
--chart-2: oklch(0.76 0.11 70);
|
|
134
|
+
--chart-3: oklch(0.7 0.07 175);
|
|
135
|
+
--chart-4: oklch(0.6 0.06 245);
|
|
136
|
+
--chart-5: oklch(0.55 0.01 215);
|
|
137
|
+
--sidebar: oklch(0.215 0.012 215);
|
|
138
|
+
--sidebar-foreground: oklch(0.93 0.006 215);
|
|
139
|
+
--sidebar-primary: oklch(0.68 0.09 205);
|
|
140
|
+
--sidebar-primary-foreground: oklch(0.18 0.02 215);
|
|
141
|
+
--sidebar-accent: oklch(0.3 0.035 210);
|
|
142
|
+
--sidebar-accent-foreground: oklch(0.93 0.006 215);
|
|
143
|
+
--sidebar-border: oklch(0.32 0.012 215);
|
|
144
|
+
--sidebar-ring: oklch(0.68 0.09 205);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@layer base {
|
|
148
|
+
* {
|
|
149
|
+
@apply border-border outline-ring/50;
|
|
150
|
+
}
|
|
151
|
+
body {
|
|
152
|
+
@apply bg-background text-foreground;
|
|
153
|
+
}
|
|
154
|
+
html {
|
|
155
|
+
@apply font-sans;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Make it feel like a native desktop app, not a webpage. */
|
|
159
|
+
html,
|
|
160
|
+
body {
|
|
161
|
+
user-select: none;
|
|
162
|
+
-webkit-user-select: none;
|
|
163
|
+
cursor: default;
|
|
164
|
+
overscroll-behavior: none;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Restore text selection where typing happens. */
|
|
168
|
+
input,
|
|
169
|
+
textarea,
|
|
170
|
+
[contenteditable] {
|
|
171
|
+
user-select: text;
|
|
172
|
+
-webkit-user-select: text;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Opt-in class for selectable content (filenames, error messages, etc). */
|
|
176
|
+
.select-text {
|
|
177
|
+
user-select: text;
|
|
178
|
+
-webkit-user-select: text;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* No drag-image ghost when grabbing an <img>. */
|
|
182
|
+
img {
|
|
183
|
+
-webkit-user-drag: none;
|
|
184
|
+
user-drag: none;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Selection is a faint tint with a 1px edge; focus is the ring. Never two rings (ux-patterns.md,
|
|
189
|
+
Georg 2026-10-03). Put `selectable` on a row, card or item and set `data-selected` (or
|
|
190
|
+
aria-selected="true") on the selected one. The edge is a background gradient, not a border or a
|
|
191
|
+
shadow, so it never shifts the layout and never competes with a focus ring's box-shadow. */
|
|
192
|
+
@utility selectable {
|
|
193
|
+
&:is([data-selected], [aria-selected="true"]) {
|
|
194
|
+
background-color: color-mix(in oklab, var(--selection) 12%, transparent);
|
|
195
|
+
background-image: linear-gradient(var(--selection), var(--selection));
|
|
196
|
+
background-size: 1px 100%;
|
|
197
|
+
background-repeat: no-repeat;
|
|
198
|
+
}
|
|
199
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export * from "./components/avatar"
|
|
2
|
+
export * from "./components/badge"
|
|
3
|
+
export * from "./components/breadcrumb"
|
|
4
|
+
export * from "./components/button"
|
|
5
|
+
export * from "./components/card"
|
|
6
|
+
export * from "./components/checkbox"
|
|
7
|
+
export * from "./components/color-field"
|
|
8
|
+
export * from "./components/color-swatch"
|
|
9
|
+
export * from "./components/context-menu"
|
|
10
|
+
export * from "./components/dialog"
|
|
11
|
+
export * from "./components/dropdown-menu"
|
|
12
|
+
export * from "./components/empty-state"
|
|
13
|
+
export * from "./components/input"
|
|
14
|
+
export * from "./components/label"
|
|
15
|
+
export * from "./components/navigation-menu"
|
|
16
|
+
export * from "./components/number-field"
|
|
17
|
+
export * from "./components/popover"
|
|
18
|
+
export * from "./components/select"
|
|
19
|
+
export * from "./components/separator"
|
|
20
|
+
export * from "./components/sheet"
|
|
21
|
+
export * from "./components/side-panel"
|
|
22
|
+
export * from "./components/sidebar"
|
|
23
|
+
export * from "./components/skeleton"
|
|
24
|
+
export * from "./components/slider"
|
|
25
|
+
export * from "./components/snackbar"
|
|
26
|
+
export * from "./components/status-bar"
|
|
27
|
+
export * from "./components/tabs"
|
|
28
|
+
export * from "./components/textarea"
|
|
29
|
+
export * from "./components/toggle"
|
|
30
|
+
export * from "./components/toggle-group"
|
|
31
|
+
export * from "./components/toolbar"
|
|
32
|
+
export * from "./components/tooltip"
|
|
33
|
+
export * as Icons from "./icons"
|
|
34
|
+
export { useIsMobile } from "./lib/use-mobile"
|
|
35
|
+
export { cn } from "./lib/utils"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
const QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`
|
|
5
|
+
|
|
6
|
+
function subscribe(onChange: () => void) {
|
|
7
|
+
const mql = window.matchMedia(QUERY)
|
|
8
|
+
mql.addEventListener("change", onChange)
|
|
9
|
+
return () => mql.removeEventListener("change", onChange)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useIsMobile() {
|
|
13
|
+
return React.useSyncExternalStore(
|
|
14
|
+
subscribe,
|
|
15
|
+
() => window.matchMedia(QUERY).matches,
|
|
16
|
+
() => false,
|
|
17
|
+
)
|
|
18
|
+
}
|