@lyeve-labs/ui-kit 0.39.1 → 0.41.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.
@@ -0,0 +1,61 @@
1
+ /**
2
+ * What a split pane remembers per viewer: its width and whether the width is
3
+ * locked. Browser storage can be absent or throw (a private window, cleared or
4
+ * blocked site data, a thumbnail capture), so every read and write is guarded
5
+ * and the pane falls back to its default. A convenience, not state a server
6
+ * holds.
7
+ *
8
+ * The stored value is `{"width": <px>, "locked": <bool>}` under the key the
9
+ * consumer names, so a reader keeps the width an earlier copy of this pane
10
+ * wrote under the same key.
11
+ *
12
+ * Not exported from the package entry point: this is an implementation detail.
13
+ */
14
+ /**
15
+ * The page's local storage, or nothing. Reading the global can itself throw
16
+ * when site data is blocked. Outside a browser it is never read: a server
17
+ * runtime may define a localStorage of its own, warning on first access or
18
+ * holding one store that every request would share.
19
+ */
20
+ export function viewerStorage() {
21
+ if (typeof window === 'undefined')
22
+ return null;
23
+ try {
24
+ return typeof localStorage === 'undefined' ? null : localStorage;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ export function clampWidth(bounds, width) {
31
+ return Math.round(Math.min(bounds.max, Math.max(bounds.min, width)));
32
+ }
33
+ export function readAside(storage, key, bounds) {
34
+ const fallback = { width: bounds.width, locked: false };
35
+ try {
36
+ const raw = storage?.getItem(key);
37
+ if (!raw)
38
+ return fallback;
39
+ const parsed = JSON.parse(raw);
40
+ if (typeof parsed !== 'object' || parsed === null)
41
+ return fallback;
42
+ const { width, locked } = parsed;
43
+ return {
44
+ width: typeof width === 'number' && Number.isFinite(width)
45
+ ? clampWidth(bounds, width)
46
+ : fallback.width,
47
+ locked: locked === true,
48
+ };
49
+ }
50
+ catch {
51
+ return fallback;
52
+ }
53
+ }
54
+ export function writeAside(storage, key, bounds, prefs) {
55
+ try {
56
+ storage?.setItem(key, JSON.stringify({ width: clampWidth(bounds, prefs.width), locked: prefs.locked }));
57
+ }
58
+ catch {
59
+ // Storage refused: the width lasts the page and nothing else.
60
+ }
61
+ }
@@ -14,6 +14,7 @@
14
14
  *
15
15
  * Not exported from the package entry point: this is an implementation detail.
16
16
  */
17
+ import type { IconPx } from './icon.js';
17
18
  /** The scale of the painted box: sm 14px, md 16px, lg 20px. */
18
19
  export type ChoiceSize = 'sm' | 'md' | 'lg';
19
20
  /** Inline is a box beside a label. Card is a bordered option surface. */
@@ -100,9 +101,10 @@ export declare function choiceIcon(size: ChoiceSize): string;
100
101
  *
101
102
  * A lucide icon takes a number, not a class, so the slot above cannot size it.
102
103
  * These are the same 14, 16 and 20 the slot reserves. An icon rendered at any
103
- * other size overflows the slot or floats inside it.
104
+ * other size overflows the slot or floats inside it. Each is a rung of the
105
+ * icon ladder, which the type holds.
104
106
  */
105
- export declare const CHOICE_ICON_PX: Record<ChoiceSize, number>;
107
+ export declare const CHOICE_ICON_PX: Record<ChoiceSize, IconPx>;
106
108
  /**
107
109
  * The fieldset a group renders, so the set is announced as a group rather than
108
110
  * as loose controls.
@@ -142,7 +142,8 @@ export function choiceIcon(size) {
142
142
  *
143
143
  * A lucide icon takes a number, not a class, so the slot above cannot size it.
144
144
  * These are the same 14, 16 and 20 the slot reserves. An icon rendered at any
145
- * other size overflows the slot or floats inside it.
145
+ * other size overflows the slot or floats inside it. Each is a rung of the
146
+ * icon ladder, which the type holds.
146
147
  */
147
148
  export const CHOICE_ICON_PX = {
148
149
  sm: 14,
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Why a read a page needed did not fill it, as the page learned it from the
3
+ * server's answer. A page never decides how a feature is enabled, which role a
4
+ * route needs or whether the database is up. It maps the answer to one of
5
+ * these states and hands it to GateNotice with its own words.
6
+ */
7
+ export type Gate = {
8
+ state: 'ok';
9
+ }
10
+ /** The route is not there. */
11
+ | {
12
+ state: 'absent';
13
+ }
14
+ /** Not enabled here. The link forward is where it is turned on, when the server names one. */
15
+ | {
16
+ state: 'locked';
17
+ forwardHref: string;
18
+ }
19
+ /**
20
+ * At a capacity ceiling: as many are in use as the server allows, so it
21
+ * refuses one more. The notice draws the page's words for it and offers no
22
+ * link, since only the page knows a way past the ceiling. The numbers are
23
+ * there for the page to put into those words, and either is null when the
24
+ * server did not send it.
25
+ */
26
+ | {
27
+ state: 'full';
28
+ limit: number | null;
29
+ current: number | null;
30
+ }
31
+ /** The reader's role may not read it, which no retry changes. */
32
+ | {
33
+ state: 'forbidden';
34
+ }
35
+ /** Refused for the request rate. The same read succeeds once the window passes. */
36
+ | {
37
+ state: 'busy';
38
+ }
39
+ /** The server or its database did not answer. The same read may answer in a moment. */
40
+ | {
41
+ state: 'unavailable';
42
+ }
43
+ /** The read failed. The message says so in words that never read as an empty result. */
44
+ | {
45
+ state: 'error';
46
+ message: string;
47
+ };
48
+ /** A heading and the sentence under it. */
49
+ export interface GateText {
50
+ title: string;
51
+ body: string;
52
+ }
53
+ /**
54
+ * Every sentence a GateNotice can draw, in the page's own words. The kit holds
55
+ * none of them, so each console says a refusal its own way and in its own
56
+ * language.
57
+ */
58
+ export interface GateWords {
59
+ /** What a missing route means for this page. */
60
+ absent: string;
61
+ /** A capacity ceiling, with the numbers the gate carried already in the body. */
62
+ full: GateText;
63
+ /** A role that may not read it, and who to ask. */
64
+ forbidden: string;
65
+ /** A refusal for the request rate. */
66
+ busy: GateText;
67
+ /** A server or database that did not answer. */
68
+ unavailable: GateText;
69
+ /** The label of the link that asks for the page again. */
70
+ retry: string;
71
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -11,3 +11,11 @@
11
11
  */
12
12
  /** The href as given, or nothing when its scheme is one no component will emit. */
13
13
  export declare function safeHref(href: string | undefined | null): string | undefined;
14
+ /**
15
+ * Whether an href leaves the page's own origin: an absolute URL with a scheme
16
+ * and a host, or a protocol-relative one, whose origin is not the page's. A
17
+ * link that leaves opens in a tab of its own, so the page it opens cannot
18
+ * reach back through window.opener. A server render has no location to
19
+ * compare with, so there every absolute URL counts as leaving.
20
+ */
21
+ export declare function isExternalHref(href: string): boolean;
@@ -25,3 +25,21 @@ export function safeHref(href) {
25
25
  return undefined;
26
26
  return DENIED.test(href.replace(IGNORED, '')) ? undefined : href;
27
27
  }
28
+ /**
29
+ * Whether an href leaves the page's own origin: an absolute URL with a scheme
30
+ * and a host, or a protocol-relative one, whose origin is not the page's. A
31
+ * link that leaves opens in a tab of its own, so the page it opens cannot
32
+ * reach back through window.opener. A server render has no location to
33
+ * compare with, so there every absolute URL counts as leaving.
34
+ */
35
+ export function isExternalHref(href) {
36
+ const absolute = /^[a-z][a-z0-9+.-]*:\/\//i.test(href) || href.startsWith('//');
37
+ if (!absolute || typeof location === 'undefined')
38
+ return absolute;
39
+ try {
40
+ return new URL(href, location.href).origin !== location.origin;
41
+ }
42
+ catch {
43
+ return true;
44
+ }
45
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The five sizes an icon may take. Each console names the same ladder in its
3
+ * own icon.ts, and the consistency suite holds every literal size in the kit
4
+ * to this list.
5
+ */
6
+ export declare const ICON_LADDER: readonly [12, 14, 16, 20, 40];
7
+ /**
8
+ * A rung of the icon ladder. A size a component looks up in a table is typed
9
+ * with it, so an entry off the ladder fails the type check where the suite
10
+ * cannot read a number held in a variable.
11
+ */
12
+ export type IconPx = (typeof ICON_LADDER)[number];
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The five sizes an icon may take. Each console names the same ladder in its
3
+ * own icon.ts, and the consistency suite holds every literal size in the kit
4
+ * to this list.
5
+ */
6
+ export const ICON_LADDER = [12, 14, 16, 20, 40];
@@ -93,6 +93,15 @@ export declare const PAGE_STACK = "flex flex-col gap-section";
93
93
  * crops the ring of every button inside it down to whichever edge fits.
94
94
  */
95
95
  export declare const CARD_SURFACE = "bg-surface border border-line rounded-xl";
96
+ /**
97
+ * The card surface in the danger tone, for a group of destructive actions.
98
+ *
99
+ * The fill is danger mixed into surface rather than a translucent tint, so it
100
+ * is opaque and paints the same on the page ground and inside a card. Five
101
+ * percent keeps the controls of a form inside it on a ground they were drawn
102
+ * for. The contrast suite reads the share from this string.
103
+ */
104
+ export declare const PANEL_DANGER_SURFACE = "bg-[color-mix(in_srgb,var(--color-danger)_5%,var(--color-surface))] border border-danger/40 rounded-xl";
96
105
  /**
97
106
  * Card padding by name.
98
107
  *
@@ -104,6 +104,15 @@ export const PAGE_STACK = 'flex flex-col gap-section';
104
104
  * crops the ring of every button inside it down to whichever edge fits.
105
105
  */
106
106
  export const CARD_SURFACE = 'bg-surface border border-line rounded-xl';
107
+ /**
108
+ * The card surface in the danger tone, for a group of destructive actions.
109
+ *
110
+ * The fill is danger mixed into surface rather than a translucent tint, so it
111
+ * is opaque and paints the same on the page ground and inside a card. Five
112
+ * percent keeps the controls of a form inside it on a ground they were drawn
113
+ * for. The contrast suite reads the share from this string.
114
+ */
115
+ export const PANEL_DANGER_SURFACE = 'bg-[color-mix(in_srgb,var(--color-danger)_5%,var(--color-surface))] border border-danger/40 rounded-xl';
107
116
  /**
108
117
  * Card padding by name.
109
118
  *
@@ -524,6 +524,12 @@ html[data-theme='light'] {
524
524
  *
525
525
  * Durations are not zeroed. An animation that finishes in 1ms still fires its
526
526
  * animationend, so components that wait on one keep working.
527
+ *
528
+ * The View Transitions API animates pseudo-elements on the root, and `*`
529
+ * matches none of them, so a page transition still cross-faded. They take a
530
+ * rule of their own because a browser that does not know a pseudo-element
531
+ * drops the whole rule it appears in, and the element rule has to survive
532
+ * there.
527
533
  */
528
534
  @media (prefers-reduced-motion: reduce) {
529
535
  *,
@@ -534,6 +540,14 @@ html[data-theme='light'] {
534
540
  transition-duration: 1ms !important;
535
541
  scroll-behavior: auto !important;
536
542
  }
543
+
544
+ ::view-transition-group(*),
545
+ ::view-transition-image-pair(*),
546
+ ::view-transition-old(*),
547
+ ::view-transition-new(*) {
548
+ animation-duration: 1ms !important;
549
+ animation-iteration-count: 1 !important;
550
+ }
537
551
  }
538
552
  }
539
553
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyeve-labs/ui-kit",
3
- "version": "0.39.1",
3
+ "version": "0.41.0",
4
4
  "description": "A clean, accessible, palette-aware Svelte 5 component library. The design system behind LyEve.",
5
5
  "license": "MIT",
6
6
  "author": "LyEve Labs <hello@lyeve.com>",
@@ -22,7 +22,9 @@
22
22
  "lyeve"
23
23
  ],
24
24
  "type": "module",
25
- "sideEffects": false,
25
+ "sideEffects": [
26
+ "**/*.css"
27
+ ],
26
28
  "engines": {
27
29
  "node": ">=24"
28
30
  },
@@ -52,7 +54,9 @@
52
54
  "access": "public"
53
55
  },
54
56
  "devDependencies": {
57
+ "@axe-core/playwright": "^4.10.1",
55
58
  "@lucide/svelte": "^0.511.0",
59
+ "@playwright/experimental-ct-svelte": "1.58.2",
56
60
  "@sveltejs/package": "^2.5.7",
57
61
  "@sveltejs/vite-plugin-svelte": "^4.0.4",
58
62
  "@testing-library/svelte": "^5.3.1",
@@ -524,6 +524,12 @@ html[data-theme='light'] {
524
524
  *
525
525
  * Durations are not zeroed. An animation that finishes in 1ms still fires its
526
526
  * animationend, so components that wait on one keep working.
527
+ *
528
+ * The View Transitions API animates pseudo-elements on the root, and `*`
529
+ * matches none of them, so a page transition still cross-faded. They take a
530
+ * rule of their own because a browser that does not know a pseudo-element
531
+ * drops the whole rule it appears in, and the element rule has to survive
532
+ * there.
527
533
  */
528
534
  @media (prefers-reduced-motion: reduce) {
529
535
  *,
@@ -534,6 +540,14 @@ html[data-theme='light'] {
534
540
  transition-duration: 1ms !important;
535
541
  scroll-behavior: auto !important;
536
542
  }
543
+
544
+ ::view-transition-group(*),
545
+ ::view-transition-image-pair(*),
546
+ ::view-transition-old(*),
547
+ ::view-transition-new(*) {
548
+ animation-duration: 1ms !important;
549
+ animation-iteration-count: 1 !important;
550
+ }
537
551
  }
538
552
  }
539
553