@heyramzi/ui 1.53.0 → 1.54.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/COMPONENTS.md +6 -0
- package/package.json +2 -2
- package/scripts/lib/sync-core.mjs +1 -1
- package/scripts/sync-shadcn.mjs +11 -11
- package/scripts/sync-transitions.mjs +7 -7
- package/src/agents/agent-avatar.svelte +14 -15
- package/src/agents/agent-cast.svelte +2 -2
- package/src/agents/demo.svelte +1 -1
- package/src/agents/helpers.ts +10 -1
- package/src/agents/index.ts +2 -2
- package/src/agents/page-state.ts +1 -1
- package/src/agents/types.ts +3 -3
- package/src/components/copy/copy-button.test.ts +3 -1
- package/src/components/data-table/infinite-scroll.svelte +1 -1
- package/src/components/data-table/pagination.svelte +1 -1
- package/src/components/data-table/row-arrow.svelte +1 -1
- package/src/components/data-table/row-context-menu.svelte +1 -1
- package/src/components/data-table/sortable-head.svelte +2 -2
- package/src/components/data-table/table-skeleton.svelte +1 -1
- package/src/components/docs/doc-page.svelte +161 -0
- package/src/components/docs/index.ts +12 -0
- package/src/components/docs/on-this-page.svelte +217 -0
- package/src/components/docs/scroll-spy.svelte.ts +97 -0
- package/src/components/field-row/field-row.svelte +57 -0
- package/src/components/field-row/index.ts +1 -0
- package/src/components/hold-to-confirm/hold-to-confirm.svelte +2 -2
- package/src/components/icon/icon.svelte +1 -1
- package/src/components/list-page/detail-empty.svelte +1 -1
- package/src/components/list-page/detail-header.svelte +1 -1
- package/src/components/list-page/detail-panel.svelte +1 -1
- package/src/components/list-page/detail-row.svelte +1 -1
- package/src/components/list-page/detail-stack.svelte +1 -1
- package/src/components/list-page/filter-item.svelte +1 -1
- package/src/components/list-page/filter-popover.svelte +1 -1
- package/src/components/list-page/grouped-list.svelte +1 -1
- package/src/components/list-page/list-page-shell.svelte +1 -1
- package/src/components/list-page/master-detail.svelte +1 -1
- package/src/components/list-page/prev-next.svelte +1 -1
- package/src/components/list-page/rail-body.svelte +1 -1
- package/src/components/list-page/rail-head.svelte +1 -1
- package/src/components/list-page/rail-row.svelte +1 -1
- package/src/components/list-page/section-label.svelte +1 -1
- package/src/components/list-page/toolbar-search.svelte +2 -2
- package/src/components/list-page/toolbar-trigger.ts +1 -1
- package/src/components/locale-switch/locale-switch.svelte +1 -1
- package/src/components/mark/mark.svelte +1 -1
- package/src/components/picker/mark-picker.svelte +1 -1
- package/src/components/picker/picker.svelte +1 -1
- package/src/components/picker/trigger.ts +2 -2
- package/src/components/section-card/index.ts +1 -0
- package/src/components/section-card/section-card.svelte +43 -0
- package/src/components/section-header/index.ts +1 -0
- package/src/components/section-header/section-header.svelte +54 -0
- package/src/components/section-header/section-header.test.ts +25 -0
- package/src/components/segmented/animated-tabs.svelte +2 -2
- package/src/components/segmented/collapse.test.ts +10 -0
- package/src/components/segmented/collapse.ts +5 -0
- package/src/components/segmented/segment.svelte +2 -2
- package/src/components/segmented/segmented-filter.svelte +6 -5
- package/src/components/segmented/segmented-nav.svelte +1 -1
- package/src/components/segmented/segmented-overflow.svelte +1 -1
- package/src/components/segmented/segmented-track.svelte +29 -4
- package/src/components/segmented/segmented.test.ts +1 -1
- package/src/components/segmented/segmented.ts +2 -2
- package/src/components/star-rating-input/star-rating-input.svelte +11 -4
- package/src/components/table-view/index.ts +3 -0
- package/src/components/table-view/table-view.svelte +103 -0
- package/src/components/table-view/table-view.test.ts +53 -0
- package/src/components/table-view/types.ts +14 -0
- package/src/components/theme/index.ts +17 -0
- package/src/components/theme/theme-provider.svelte +47 -0
- package/src/components/theme/theme-provider.test.ts +61 -0
- package/src/effects/animated-beam/diagrams/BeamSvg.svelte +1 -1
- package/src/effects/animated-beam/path-calculator.svelte.ts +1 -1
- package/src/effects/border-beam/types.ts +1 -1
- package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +1 -1
- package/src/effects/shader-gradient/shaders.ts +1 -1
- package/src/effects/spotlight/Spotlight.svelte +1 -2
- package/src/icons/trades.ts +3 -3
- package/src/lib/sliding-pill.ts +3 -3
- package/src/lib/tone.test.ts +10 -0
- package/src/lib/tone.ts +1 -1
- package/src/shadcn/badge/badge.svelte +1 -1
- package/src/shadcn/button/button.svelte +10 -1
- package/src/shadcn/button/button.test.ts +19 -0
- package/src/shadcn/checkbox/checkbox.svelte +1 -1
- package/src/shadcn/context-menu/context-menu-separator.svelte +4 -1
- package/src/shadcn/dropdown-menu/dropdown-menu-separator.svelte +4 -1
- package/src/shadcn/input/input.svelte +2 -2
- package/src/shadcn/progress/progress.svelte +8 -2
- package/src/shadcn/progress/tone.test.ts +13 -0
- package/src/shadcn/progress/tone.ts +15 -0
- package/src/shadcn/sidebar/sidebar-group-label.svelte +1 -1
- package/src/shadcn/table/table-cell.svelte +21 -1
- package/src/shadcn/table/table-cell.test.ts +16 -0
- package/src/tokens/base.css +183 -0
- package/src/tokens/craft.css +179 -0
- package/src/tokens/index.css +19 -0
- package/src/tokens/palette.css +410 -0
- package/src/tokens/primitives.css +223 -0
- package/scripts/relative-imports.mjs +0 -39
- package/src/tokens.css +0 -993
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { flushSync, mount, unmount } from "svelte";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
|
|
3
|
+
import { setMode, toggleMode } from "mode-watcher";
|
|
4
|
+
import ThemeProvider from "./theme-provider.svelte";
|
|
5
|
+
|
|
6
|
+
function render(props: Record<string, unknown>) {
|
|
7
|
+
const target = document.createElement("div");
|
|
8
|
+
document.body.appendChild(target);
|
|
9
|
+
const app = mount(ThemeProvider, { target, props });
|
|
10
|
+
flushSync();
|
|
11
|
+
return () => {
|
|
12
|
+
void unmount(app);
|
|
13
|
+
target.remove();
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("ThemeProvider", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
localStorage.clear();
|
|
20
|
+
// WHY: jsdom has no matchMedia, and the watcher asks it for the system mode on mount.
|
|
21
|
+
Object.defineProperty(window, "matchMedia", {
|
|
22
|
+
configurable: true,
|
|
23
|
+
value: (query: string) => ({
|
|
24
|
+
matches: false,
|
|
25
|
+
media: query,
|
|
26
|
+
addEventListener() {},
|
|
27
|
+
removeEventListener() {},
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => document.documentElement.classList.remove("dark"));
|
|
32
|
+
|
|
33
|
+
it("takes the server's choice when nothing is stored", () => {
|
|
34
|
+
const done = render({ initial: "dark", disableHeadScriptInjection: true });
|
|
35
|
+
flushSync();
|
|
36
|
+
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
|
37
|
+
done();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("lets a stored choice beat the server's", () => {
|
|
41
|
+
localStorage.setItem("mode-watcher-mode", "light");
|
|
42
|
+
const done = render({ initial: "dark", disableHeadScriptInjection: true });
|
|
43
|
+
flushSync();
|
|
44
|
+
expect(document.documentElement.classList.contains("dark")).toBe(false);
|
|
45
|
+
done();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("reports a person's choice and not the load", () => {
|
|
49
|
+
const onChange = vi.fn();
|
|
50
|
+
const done = render({ initial: "light", onChange, disableHeadScriptInjection: true });
|
|
51
|
+
flushSync();
|
|
52
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
53
|
+
setMode("dark");
|
|
54
|
+
flushSync();
|
|
55
|
+
expect(onChange).toHaveBeenLastCalledWith("dark");
|
|
56
|
+
toggleMode();
|
|
57
|
+
flushSync();
|
|
58
|
+
expect(onChange).toHaveBeenLastCalledWith("light");
|
|
59
|
+
done();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -43,7 +43,7 @@ export function usePathCalculator() {
|
|
|
43
43
|
|
|
44
44
|
// A cubic with horizontal handles, not a quadratic through a raised midpoint.
|
|
45
45
|
|
|
46
|
-
// The quadratic put its single control point at `midY
|
|
46
|
+
// The quadratic put its single control point at `midY` minus `curvature`, which for a fan into one node lands past the far end:
|
|
47
47
|
|
|
48
48
|
// the top beam dove below the hub and crossed the middle beam before arriving, and three beams bundled into a lens for the last stretch.
|
|
49
49
|
|
|
@@ -24,7 +24,7 @@ export interface BorderBeamProps extends Omit<HTMLAttributes<HTMLDivElement>, "c
|
|
|
24
24
|
// preset stop is mapped by index (cycling if shorter) so layout/timing
|
|
25
25
|
// stay identical, only hues swap. Accepts hex (#rrggbb), rgb(), rgba().
|
|
26
26
|
colors?: string[];
|
|
27
|
-
// WHY: the bloom layer is the single most expensive part of the beam
|
|
27
|
+
// WHY: the bloom layer is the single most expensive part of the beam: a blurred conic-gradient re-rasterized every frame.
|
|
28
28
|
|
|
29
29
|
// Opt-out gives a 2-3x perf win for callers who only need the stroke and inner shimmer.
|
|
30
30
|
bloom?: boolean;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/*
|
|
14
|
-
* irradiance * BRDF_Lambert(diffuse * (1
|
|
14
|
+
* irradiance * BRDF_Lambert(diffuse * (1 minus metalness)) = (brightness * PI) * (1 / PI) * (base * 0.8) = brightness * 0.8 * base
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/*
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// =========================
|
|
9
9
|
// TYPES
|
|
10
10
|
// =========================
|
|
11
|
-
|
|
11
|
+
interface Props {
|
|
12
12
|
class?: string;
|
|
13
13
|
fill?: string;
|
|
14
14
|
};
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
<!-- =========================== -->
|
|
23
23
|
<!-- MARKUP -->
|
|
24
24
|
<!-- =========================== -->
|
|
25
|
-
<!-- no-inline-svg-ignore: static decorative blob, animated via the spotlight keyframe -->
|
|
26
25
|
<svg
|
|
27
26
|
class={cn(
|
|
28
27
|
"hr-spotlight pointer-events-none absolute z-[1] h-[169%] w-[138%] animate-spotlight opacity-0 lg:w-[84%]",
|
package/src/icons/trades.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Trade icon map
|
|
2
|
+
* Trade icon map. Tabler covers the domain Lucide and Phosphor miss.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
|
-
* Lucide/Phosphor gap audit (Sept 2026) against the 14 trade pills in the "NOT SURE? TAP WHAT YOU DO" strip
|
|
6
|
+
* Lucide/Phosphor gap audit (Sept 2026) against the 14 trade pills in the "NOT SURE? TAP WHAT YOU DO" strip. Lucide has `hammer`, `wrench`, `hard-hat`, `bolt`, `brick-wall`,
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
|
-
* `building`, `car
|
|
10
|
+
* `building`, `car`, but no `trowel`, no tiling grid, no `window` cleaning `spray/squeegee`, no `crane`, no dedicated `handyman` toolset. Phosphor has `hammer`, `wrench`, `hard-hat`, `paint-roller`, and also no `trowel`.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/*
|
package/src/lib/sliding-pill.ts
CHANGED
|
@@ -18,8 +18,8 @@ export const slidingPill: Attachment<HTMLElement> = (pill) => {
|
|
|
18
18
|
|
|
19
19
|
let settled = false;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
const active = list
|
|
21
|
+
const move = (animate: boolean) => {
|
|
22
|
+
const active = list.querySelector<HTMLElement>(ACTIVE);
|
|
23
23
|
if (!active) {
|
|
24
24
|
pill.style.opacity = "0";
|
|
25
25
|
return;
|
|
@@ -37,7 +37,7 @@ export const slidingPill: Attachment<HTMLElement> = (pill) => {
|
|
|
37
37
|
pill.style.width = `${active.offsetWidth}px`;
|
|
38
38
|
void pill.offsetWidth;
|
|
39
39
|
pill.style.transition = "";
|
|
40
|
-
}
|
|
40
|
+
};
|
|
41
41
|
|
|
42
42
|
// Position first, THEN hand the highlight over. Swapped, one frame draws both
|
|
43
43
|
// the trigger's own background and a zero-width pill: a flicker on hydration.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
2
|
+
import { TONE_CLASS } from "./tone.js";
|
|
3
|
+
|
|
4
|
+
describe("TONE_CLASS", () => {
|
|
5
|
+
// Plain destructive text on its own 10% tint is 3.6:1. The ink mix clears 4.5:1.
|
|
6
|
+
it("sets destructive text in the ink, not the raw colour", () => {
|
|
7
|
+
expect(TONE_CLASS.destructive).toContain("text-destructive-ink");
|
|
8
|
+
expect(TONE_CLASS.destructive.split(" ")).not.toContain("text-destructive");
|
|
9
|
+
});
|
|
10
|
+
});
|
package/src/lib/tone.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const TONE_CLASS: Record<Tone, string> = {
|
|
|
14
14
|
info: "border-info/30 bg-info/10 text-info-800 dark:text-info-400",
|
|
15
15
|
success: "border-success/30 bg-success/10 text-success-800 dark:text-success-400",
|
|
16
16
|
warning: "border-warning/30 bg-warning/10 text-warning-800 dark:text-warning-400",
|
|
17
|
-
destructive: "border-destructive/30 bg-destructive/10 text-destructive",
|
|
17
|
+
destructive: "border-destructive/30 bg-destructive/10 text-destructive-ink",
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
/** The same scale as text, for a verdict inside a line of muted facts. */
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
12
12
|
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
13
13
|
// For a consumer that wants one "this is wrong" badge without a tone prop.
|
|
14
|
-
destructive: "bg-destructive/10 text-destructive [a]:hover:bg-destructive/20",
|
|
14
|
+
destructive: "bg-destructive/10 text-destructive-ink [a]:hover:bg-destructive/20",
|
|
15
15
|
},
|
|
16
16
|
},
|
|
17
17
|
defaultVariants: {
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
19
19
|
destructive:
|
|
20
20
|
"bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive dark:text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
|
21
|
+
// Quiet text that opens an editor or a menu: a ghost that wraps to as many lines as its
|
|
22
|
+
// label needs, aligned left, never a fixed-height square.
|
|
23
|
+
text: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
21
24
|
// WHY a variant and not a class every call site writes: "add another
|
|
22
25
|
|
|
23
26
|
// row", "add a step", "attach a call" is one affordance, and it was hand-drawn eleven times across the app with four radii, three text sizes and three different hover hues.
|
|
@@ -34,7 +37,7 @@
|
|
|
34
37
|
|
|
35
38
|
// WHY sm carries a hit area: 28px is under the 32px desktop floor and the 40px coarse height is under 44. The ::after box stays the same 32px (44 on coarse) square whatever the visual size, so a dense toolbar keeps its look.
|
|
36
39
|
|
|
37
|
-
// Icon buttons grow both axes because they have no label to widen them. any-pointer, not pointer: see the note in tokens.css.
|
|
40
|
+
// Icon buttons grow both axes because they have no label to widen them. any-pointer, not pointer: see the note in tokens/base.css.
|
|
38
41
|
size: {
|
|
39
42
|
sm: "relative after:absolute after:top-1/2 after:left-1/2 after:h-[max(100%,2rem)] after:w-[max(100%,2rem)] after:-translate-x-1/2 after:-translate-y-1/2 any-pointer-coarse:after:h-[max(100%,2.75rem)] any-pointer-coarse:after:w-[max(100%,2.75rem)] h-7 gap-1 px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 any-pointer-coarse:h-10 any-pointer-coarse:px-3",
|
|
40
43
|
default:
|
|
@@ -45,6 +48,12 @@
|
|
|
45
48
|
icon: { true: "", false: "" },
|
|
46
49
|
},
|
|
47
50
|
compoundVariants: [
|
|
51
|
+
// After `size` so its heights lose: a text button is as tall as its lines.
|
|
52
|
+
{
|
|
53
|
+
variant: "text",
|
|
54
|
+
class:
|
|
55
|
+
"h-auto min-h-7 justify-start py-1 text-left whitespace-normal any-pointer-coarse:h-auto any-pointer-coarse:min-h-11",
|
|
56
|
+
},
|
|
48
57
|
{
|
|
49
58
|
size: "sm",
|
|
50
59
|
icon: true,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
2
|
+
import { buttonVariants } from "./button.svelte";
|
|
3
|
+
|
|
4
|
+
describe("buttonVariants text", () => {
|
|
5
|
+
for (const size of ["sm", "default", "lg"] as const) {
|
|
6
|
+
it(`wraps and grows with its lines at size ${size}`, () => {
|
|
7
|
+
const classes = buttonVariants({ variant: "text", size }).split(" ");
|
|
8
|
+
expect(classes).toContain("h-auto");
|
|
9
|
+
expect(classes).toContain("whitespace-normal");
|
|
10
|
+
expect(classes).toContain("any-pointer-coarse:min-h-11");
|
|
11
|
+
expect(classes).not.toContain("whitespace-nowrap");
|
|
12
|
+
expect(classes).not.toContain("h-7");
|
|
13
|
+
expect(classes).not.toContain("h-8");
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
it("leaves the other variants fixed-height", () => {
|
|
17
|
+
expect(buttonVariants({ variant: "ghost" }).split(" ")).toContain("h-8");
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
bind:ref
|
|
17
17
|
data-slot="checkbox"
|
|
18
18
|
class={cn(
|
|
19
|
-
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-sm border shadow-btn transition-[background-color,border-color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 any-pointer-coarse:after:-inset-
|
|
19
|
+
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-sm border shadow-btn transition-[background-color,border-color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 any-pointer-coarse:after:-inset-[15px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
20
20
|
className,
|
|
21
21
|
)}
|
|
22
22
|
bind:checked
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
<ContextMenuPrimitive.Separator
|
|
13
13
|
bind:ref
|
|
14
14
|
data-slot="context-menu-separator"
|
|
15
|
-
class={cn(
|
|
15
|
+
class={cn(
|
|
16
|
+
"bg-border -mx-1 my-1 h-px first:hidden last:hidden [[data-slot=context-menu-separator]+&]:hidden",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
16
19
|
{...restProps}
|
|
17
20
|
/>
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
<DropdownMenuPrimitive.Separator
|
|
13
13
|
bind:ref
|
|
14
14
|
data-slot="dropdown-menu-separator"
|
|
15
|
-
class={cn(
|
|
15
|
+
class={cn(
|
|
16
|
+
"bg-border -mx-1 my-1 h-px first:hidden last:hidden [[data-slot=dropdown-menu-separator]+&]:hidden",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
16
19
|
{...restProps}
|
|
17
20
|
/>
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
bind:this={ref}
|
|
26
26
|
data-slot={dataSlot}
|
|
27
27
|
class={cn(
|
|
28
|
-
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base shadow-field transition-[color,background-color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
28
|
+
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 any-pointer-coarse:h-11 rounded-lg border bg-transparent px-2.5 py-1 text-base shadow-field transition-[color,background-color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
29
29
|
className,
|
|
30
30
|
)}
|
|
31
31
|
type="file"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
bind:this={ref}
|
|
39
39
|
data-slot={dataSlot}
|
|
40
40
|
class={cn(
|
|
41
|
-
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base shadow-field transition-[color,background-color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
41
|
+
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 any-pointer-coarse:h-11 rounded-lg border bg-transparent px-2.5 py-1 text-base shadow-field transition-[color,background-color,box-shadow] duration-[var(--duration-quick)] ease-[var(--ease-smooth-out)] file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
42
42
|
className,
|
|
43
43
|
)}
|
|
44
44
|
{type}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Progress as ProgressPrimitive } from "bits-ui";
|
|
3
3
|
import { cn, type WithoutChildrenOrChild } from "@heyramzi/utils";
|
|
4
|
+
import { progressToneClass } from "./tone.js";
|
|
5
|
+
import type { Tone } from "../../lib/tone.js";
|
|
4
6
|
|
|
5
7
|
let {
|
|
6
8
|
ref = $bindable(null),
|
|
7
9
|
class: className,
|
|
8
10
|
max = 100,
|
|
9
11
|
value,
|
|
12
|
+
tone,
|
|
10
13
|
...restProps
|
|
11
|
-
}: WithoutChildrenOrChild<ProgressPrimitive.RootProps>
|
|
14
|
+
}: WithoutChildrenOrChild<ProgressPrimitive.RootProps> & {
|
|
15
|
+
/** The fill's colour on the severity scale. Left out, the primary colour. */
|
|
16
|
+
tone?: Tone;
|
|
17
|
+
} = $props();
|
|
12
18
|
</script>
|
|
13
19
|
|
|
14
20
|
<ProgressPrimitive.Root
|
|
@@ -24,7 +30,7 @@
|
|
|
24
30
|
>
|
|
25
31
|
<div
|
|
26
32
|
data-slot="progress-indicator"
|
|
27
|
-
class="
|
|
33
|
+
class="{progressToneClass(tone)} size-full flex-1 transition-transform duration-[var(--duration-slow)] ease-[var(--ease-smooth-out)] motion-reduce:transition-none"
|
|
28
34
|
style="transform: translateX(-{100 - (100 * (value ?? 0)) / (max ?? 1)}%)"
|
|
29
35
|
></div>
|
|
30
36
|
</ProgressPrimitive.Root>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
2
|
+
import { progressToneClass } from "./tone.js";
|
|
3
|
+
|
|
4
|
+
describe("progressToneClass", () => {
|
|
5
|
+
it("keeps the primary fill when no tone is given", () => {
|
|
6
|
+
expect(progressToneClass()).toBe("bg-primary");
|
|
7
|
+
});
|
|
8
|
+
it("gives each tone its own fill", () => {
|
|
9
|
+
expect(progressToneClass("success")).toBe("bg-success");
|
|
10
|
+
expect(progressToneClass("destructive")).toBe("bg-destructive");
|
|
11
|
+
expect(progressToneClass("accent")).toContain("--tone-accent");
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Tone } from "../../lib/tone.js";
|
|
2
|
+
|
|
3
|
+
/** The fill colour for each tone. No tone is the primary colour, as before. */
|
|
4
|
+
const FILL: Record<Tone, string> = {
|
|
5
|
+
neutral: "bg-muted-foreground",
|
|
6
|
+
accent: "bg-[color:var(--tone-accent,var(--color-primary))]",
|
|
7
|
+
info: "bg-info",
|
|
8
|
+
success: "bg-success",
|
|
9
|
+
warning: "bg-warning",
|
|
10
|
+
destructive: "bg-destructive",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function progressToneClass(tone?: Tone): string {
|
|
14
|
+
return tone ? FILL[tone] : "bg-primary";
|
|
15
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
const mergedProps = $derived({
|
|
17
17
|
class: cn(
|
|
18
|
-
"text-sidebar-foreground/
|
|
18
|
+
"text-sidebar-foreground/90 ring-sidebar-ring h-8 rounded-md px-2 text-xs font-medium transition-[margin,opacity] duration-[var(--duration-fast)] ease-[var(--ease-smooth-out)] group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 group-data-[collapsible=icon]:pointer-events-none focus-visible:ring-2 [&>svg]:size-4 flex shrink-0 items-center outline-hidden [&>svg]:shrink-0",
|
|
19
19
|
className,
|
|
20
20
|
),
|
|
21
21
|
"data-slot": "sidebar-group-label",
|
|
@@ -13,8 +13,28 @@
|
|
|
13
13
|
<td
|
|
14
14
|
bind:this={ref}
|
|
15
15
|
data-slot="table-cell"
|
|
16
|
-
class={cn("p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)}
|
|
16
|
+
class={cn("p-2 align-middle whitespace-nowrap tabular-nums [&:has([role=checkbox])]:pr-0", className)}
|
|
17
17
|
{...restProps}
|
|
18
18
|
>
|
|
19
19
|
{@render children?.()}
|
|
20
20
|
</td>
|
|
21
|
+
|
|
22
|
+
<!-- WHY here: a link in a cell is 20px tall, and a cell is the only place that knows it sits in a
|
|
23
|
+
row of them. On a coarse pointer the link grows a 44px box centred on it, wider when the link
|
|
24
|
+
is. A link that positions itself (a stretched row link) keeps its own box. -->
|
|
25
|
+
<style>
|
|
26
|
+
@media (any-pointer: coarse) {
|
|
27
|
+
td :global(a[href]:not([class*="absolute"], [class*="fixed"], [class*="sticky"])) {
|
|
28
|
+
position: relative;
|
|
29
|
+
}
|
|
30
|
+
td :global(a[href]:not([class*="absolute"], [class*="fixed"], [class*="sticky"]))::after {
|
|
31
|
+
content: "";
|
|
32
|
+
position: absolute;
|
|
33
|
+
top: 50%;
|
|
34
|
+
left: 50%;
|
|
35
|
+
width: max(100%, 2.75rem);
|
|
36
|
+
height: max(100%, 2.75rem);
|
|
37
|
+
transform: translate(-50%, -50%);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { flushSync, mount, unmount } from "svelte";
|
|
2
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
3
|
+
import TableCell from "./table-cell.svelte";
|
|
4
|
+
|
|
5
|
+
describe("TableCell", () => {
|
|
6
|
+
// A column of dates or prices has to line up whoever writes the cell.
|
|
7
|
+
it("keeps digits one width", () => {
|
|
8
|
+
const target = document.createElement("tr");
|
|
9
|
+
document.body.appendChild(target);
|
|
10
|
+
const app = mount(TableCell, { target, props: {} });
|
|
11
|
+
flushSync();
|
|
12
|
+
expect(target.querySelector("td")?.className).toContain("tabular-nums");
|
|
13
|
+
void unmount(app);
|
|
14
|
+
target.remove();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/* The shadow family, now valued by @heyramzi/ui/design. Names stay because
|
|
2
|
+
170-odd call sites use them. Shadow only, since these elements draw their own border. */
|
|
3
|
+
|
|
4
|
+
/* New work: use `shadow-btn`, `shadow-card`, `shadow-raised` and `shadow-overlay`. */
|
|
5
|
+
@layer utilities {
|
|
6
|
+
.shadow-soft-sm {
|
|
7
|
+
box-shadow: var(--elev-btn);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.shadow-soft {
|
|
11
|
+
box-shadow: var(--elev-card);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.shadow-soft-md {
|
|
15
|
+
box-shadow: var(--elev-raised);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* The one step that answers a pointer rather than describing a resting
|
|
19
|
+
height, so it borrows the step above whatever it was at rest. */
|
|
20
|
+
.shadow-soft-hover {
|
|
21
|
+
box-shadow: var(--elev-raised);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* The nine bits-ui state variants moved into `@heyramzi/ui/design` on 14 Sep
|
|
26
|
+
2026. app.css imports that before this file. */
|
|
27
|
+
|
|
28
|
+
@utility no-scrollbar {
|
|
29
|
+
-ms-overflow-style: none;
|
|
30
|
+
scrollbar-width: none;
|
|
31
|
+
&::-webkit-scrollbar {
|
|
32
|
+
display: none;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@utility thin-scrollbar {
|
|
37
|
+
scrollbar-width: thin;
|
|
38
|
+
scrollbar-color: oklch(0.84 0 0) transparent;
|
|
39
|
+
&::-webkit-scrollbar {
|
|
40
|
+
width: 4px;
|
|
41
|
+
}
|
|
42
|
+
&::-webkit-scrollbar-track {
|
|
43
|
+
background: transparent;
|
|
44
|
+
}
|
|
45
|
+
&::-webkit-scrollbar-thumb {
|
|
46
|
+
background: oklch(0.84 0 0);
|
|
47
|
+
border-radius: 9999px;
|
|
48
|
+
}
|
|
49
|
+
.dark & {
|
|
50
|
+
scrollbar-color: oklch(0.45 0 0) transparent;
|
|
51
|
+
&::-webkit-scrollbar-thumb {
|
|
52
|
+
background: oklch(0.45 0 0);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* WHY: below sm the dialog docks as a bottom sheet. The .t-modal scale would lift
|
|
58
|
+
it off the edge. A sheet enters from the edge it rests on. */
|
|
59
|
+
@media (max-width: 639px) {
|
|
60
|
+
[data-slot="dialog-content"]:not([data-command-dialog]).t-modal {
|
|
61
|
+
transform: translateY(100%);
|
|
62
|
+
transition:
|
|
63
|
+
transform var(--modal-open-dur) var(--modal-ease),
|
|
64
|
+
opacity var(--modal-open-dur) var(--modal-ease);
|
|
65
|
+
}
|
|
66
|
+
[data-slot="dialog-content"]:not([data-command-dialog]).t-modal[data-state="open"] {
|
|
67
|
+
transform: translateY(0);
|
|
68
|
+
}
|
|
69
|
+
[data-slot="dialog-content"]:not([data-command-dialog]).t-modal[data-state="closed"] {
|
|
70
|
+
transform: translateY(100%);
|
|
71
|
+
transition:
|
|
72
|
+
transform var(--modal-close-dur) var(--modal-ease),
|
|
73
|
+
opacity var(--modal-close-dur) var(--modal-ease);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ── The palette does not animate ──────────────────────
|
|
78
|
+
WHY: it's opened by shortcut dozens of times a day, so an entrance feels like
|
|
79
|
+
lag. Raycast and Spotlight don't animate either. */
|
|
80
|
+
|
|
81
|
+
/* The backdrop is a sibling in the portal, so it's named via `overlayClass`.
|
|
82
|
+
Reaching across the portal would also hit a dialog open underneath. */
|
|
83
|
+
[data-slot="dialog-content"][data-command-dialog].t-modal,
|
|
84
|
+
[data-slot="dialog-overlay"].t-modal-backdrop.t-instant {
|
|
85
|
+
transition: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@layer base {
|
|
89
|
+
* {
|
|
90
|
+
@apply border-border outline-ring/50;
|
|
91
|
+
}
|
|
92
|
+
body {
|
|
93
|
+
@apply bg-background text-foreground;
|
|
94
|
+
}
|
|
95
|
+
html {
|
|
96
|
+
@apply font-sans;
|
|
97
|
+
scrollbar-color: var(--gray-300) transparent;
|
|
98
|
+
/* WHY: iOS Safari inflates text in landscape unless this is pinned. */
|
|
99
|
+
-webkit-text-size-adjust: 100%;
|
|
100
|
+
text-size-adjust: 100%;
|
|
101
|
+
}
|
|
102
|
+
.dark html,
|
|
103
|
+
html.dark {
|
|
104
|
+
scrollbar-color: var(--brand-veil) transparent;
|
|
105
|
+
}
|
|
106
|
+
h1,
|
|
107
|
+
h2,
|
|
108
|
+
h3,
|
|
109
|
+
h4,
|
|
110
|
+
h5,
|
|
111
|
+
h6 {
|
|
112
|
+
font-family: var(--font-heading);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* WHY: the browser's default highlight is a system blue from no theme. Indigo
|
|
116
|
+
at 18% keeps text at its own contrast in both modes. */
|
|
117
|
+
::selection {
|
|
118
|
+
background-color: color-mix(in oklab, var(--brand-indigo) 18%, transparent);
|
|
119
|
+
}
|
|
120
|
+
.dark ::selection,
|
|
121
|
+
html.dark ::selection {
|
|
122
|
+
background-color: color-mix(in oklab, var(--brand-indigo-light) 26%, transparent);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ── Touch input ───────────────────────────────────────
|
|
126
|
+
WHY any-pointer: an iPad with a Magic Keyboard reports a fine pointer, and
|
|
127
|
+
`pointer: coarse` would drop touch sizing while the screen's still tapped. */
|
|
128
|
+
|
|
129
|
+
/* iOS paints a gray flash on every tap, which clashes with our own :active
|
|
130
|
+
feedback, so it's turned off. */
|
|
131
|
+
@media (any-pointer: coarse) {
|
|
132
|
+
a,
|
|
133
|
+
button,
|
|
134
|
+
label,
|
|
135
|
+
summary,
|
|
136
|
+
select,
|
|
137
|
+
[role="button"],
|
|
138
|
+
[role="link"],
|
|
139
|
+
[role="menuitem"],
|
|
140
|
+
[role="option"],
|
|
141
|
+
[role="tab"] {
|
|
142
|
+
-webkit-tap-highlight-color: transparent;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* WHY: rows are built at desk density (28-32px). A thumb needs 44px, and rows
|
|
146
|
+
stack, so growing them costs only scroll. */
|
|
147
|
+
[data-slot="dropdown-menu-item"],
|
|
148
|
+
[data-slot="dropdown-menu-checkbox-item"],
|
|
149
|
+
[data-slot="dropdown-menu-radio-item"],
|
|
150
|
+
[data-slot="select-item"],
|
|
151
|
+
[data-slot="command-item"],
|
|
152
|
+
[data-slot="context-menu-item"],
|
|
153
|
+
[data-slot="context-menu-checkbox-item"],
|
|
154
|
+
[data-slot="context-menu-radio-item"] {
|
|
155
|
+
min-height: 44px;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ── Reduced motion, once ──────────────────────────────
|
|
161
|
+
WHY here: 22 files had no guard and 32 wrote their own. Both app.css files
|
|
162
|
+
import this, so it's stated once. */
|
|
163
|
+
|
|
164
|
+
/* A floor, not a kill switch: transitions still run at the micro step, so hover
|
|
165
|
+
and focus still answer. Entrance delays, loops and long travel go. */
|
|
166
|
+
|
|
167
|
+
/* Unlayered so it beats utilities and scoped rules. A component that wants
|
|
168
|
+
different behaviour writes its own reduced-motion block with `!important`. */
|
|
169
|
+
@media (prefers-reduced-motion: reduce) {
|
|
170
|
+
*,
|
|
171
|
+
*::before,
|
|
172
|
+
*::after {
|
|
173
|
+
animation-delay: 0ms !important;
|
|
174
|
+
animation-duration: 1ms !important;
|
|
175
|
+
animation-iteration-count: 1 !important;
|
|
176
|
+
transition-delay: 0ms !important;
|
|
177
|
+
transition-duration: var(--duration-micro) !important;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
html {
|
|
181
|
+
scroll-behavior: auto !important;
|
|
182
|
+
}
|
|
183
|
+
}
|