@cosxai/ui 0.25.0 → 1.0.0-alpha.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.
Files changed (133) hide show
  1. package/CHANGELOG.md +577 -0
  2. package/NOTICE +11 -0
  3. package/README.md +52 -0
  4. package/package.json +33 -7
  5. package/src/fonts.css +20 -0
  6. package/src/index.ts +8 -25
  7. package/src/lib/cn.ts +23 -6
  8. package/src/styles.css +17 -0
  9. package/src/theme.css +132 -0
  10. package/src/tokens/colors.css +112 -0
  11. package/src/tokens/compat.css +36 -0
  12. package/src/tokens/elevation.css +14 -0
  13. package/src/tokens/motion.css +42 -0
  14. package/src/tokens/spacing.css +55 -0
  15. package/src/tokens/typography-cjk.css +42 -0
  16. package/src/tokens/typography.css +81 -0
  17. package/src/actionbar/ActionBar.tsx +0 -837
  18. package/src/actionbar/ActionBarButton.tsx +0 -111
  19. package/src/actionbar/ActionBarMenuGroup.tsx +0 -116
  20. package/src/actionbar/ActionBarModeHandle.tsx +0 -171
  21. package/src/actionbar/ActionBarProvider.tsx +0 -91
  22. package/src/actionbar/actionbar-context.ts +0 -45
  23. package/src/actionbar/index.ts +0 -28
  24. package/src/actionbar/types.ts +0 -147
  25. package/src/actionbar/useActionBarItems.ts +0 -60
  26. package/src/actionbar/useActionBarMode.ts +0 -144
  27. package/src/actionbar/useActionBarPanel.ts +0 -56
  28. package/src/actionbar/useActionBarStatusDot.ts +0 -49
  29. package/src/actionbar/useActionBarToast.ts +0 -33
  30. package/src/ambient/AmbientBackdrop.tsx +0 -74
  31. package/src/ambient/CommandInput.tsx +0 -107
  32. package/src/ambient/SuperbarStrip.tsx +0 -36
  33. package/src/ambient/index.ts +0 -6
  34. package/src/bento/BentoCell.tsx +0 -66
  35. package/src/bento/BentoGrid.tsx +0 -42
  36. package/src/bento/index.ts +0 -2
  37. package/src/command/CommandPalette.tsx +0 -329
  38. package/src/command/CommandProvider.tsx +0 -57
  39. package/src/command/command-context.ts +0 -12
  40. package/src/command/index.ts +0 -6
  41. package/src/command/rank.ts +0 -50
  42. package/src/command/types.ts +0 -37
  43. package/src/command/useCommandSource.ts +0 -37
  44. package/src/dialogs/DialogsProvider.tsx +0 -216
  45. package/src/dialogs/Modal.tsx +0 -260
  46. package/src/dialogs/Toast.tsx +0 -85
  47. package/src/dialogs/dialogs-context.ts +0 -6
  48. package/src/dialogs/index.ts +0 -10
  49. package/src/dialogs/types.ts +0 -37
  50. package/src/dialogs/useDialogs.ts +0 -8
  51. package/src/editorial/EditorialSpotlight.tsx +0 -63
  52. package/src/editorial/Folio.tsx +0 -52
  53. package/src/editorial/PlateMarker.tsx +0 -33
  54. package/src/editorial/RomanSection.tsx +0 -65
  55. package/src/editorial/RunningMarginalia.tsx +0 -65
  56. package/src/editorial/index.ts +0 -10
  57. package/src/hooks/index.ts +0 -5
  58. package/src/hooks/useKeyboardHotkey.ts +0 -93
  59. package/src/hooks/useReducedMotion.ts +0 -20
  60. package/src/hooks/useViewport.ts +0 -61
  61. package/src/layout/Breadcrumb.tsx +0 -74
  62. package/src/layout/LeftNavRail.tsx +0 -139
  63. package/src/layout/MobileTabBar.tsx +0 -101
  64. package/src/layout/NavItem.tsx +0 -128
  65. package/src/layout/NavSearchTrigger.tsx +0 -88
  66. package/src/layout/NavSection.tsx +0 -40
  67. package/src/layout/RightSidebarPanel.tsx +0 -111
  68. package/src/layout/Shell.tsx +0 -91
  69. package/src/layout/SidePanel.tsx +0 -316
  70. package/src/layout/StickyBanner.tsx +0 -83
  71. package/src/layout/Topbar.tsx +0 -68
  72. package/src/layout/index.ts +0 -24
  73. package/src/layout/useNavRailState.ts +0 -69
  74. package/src/lib/time-utils.ts +0 -44
  75. package/src/neobrutalism/Marquee.tsx +0 -81
  76. package/src/neobrutalism/Sticker.tsx +0 -71
  77. package/src/neobrutalism/index.ts +0 -4
  78. package/src/primitives/Avatar.tsx +0 -53
  79. package/src/primitives/Button.tsx +0 -67
  80. package/src/primitives/Card.tsx +0 -41
  81. package/src/primitives/Checkbox.tsx +0 -87
  82. package/src/primitives/Chip.tsx +0 -191
  83. package/src/primitives/Combobox.tsx +0 -507
  84. package/src/primitives/CopyField.tsx +0 -139
  85. package/src/primitives/CountBadge.tsx +0 -50
  86. package/src/primitives/Input.tsx +0 -204
  87. package/src/primitives/Kbd.tsx +0 -45
  88. package/src/primitives/MentionCombobox.tsx +0 -612
  89. package/src/primitives/MultiCombobox.tsx +0 -327
  90. package/src/primitives/PageHeader.tsx +0 -77
  91. package/src/primitives/Radio.tsx +0 -131
  92. package/src/primitives/SegmentedControl.tsx +0 -135
  93. package/src/primitives/Select.tsx +0 -779
  94. package/src/primitives/SignaturePad.tsx +0 -461
  95. package/src/primitives/Tag.tsx +0 -56
  96. package/src/primitives/Textarea.tsx +0 -62
  97. package/src/primitives/ToggleSwitch.tsx +0 -79
  98. package/src/primitives/Tooltip.tsx +0 -213
  99. package/src/primitives/index.ts +0 -53
  100. package/src/pwa/InstallPromptBanner.tsx +0 -132
  101. package/src/pwa/index.ts +0 -4
  102. package/src/pwa/manifest.template.json +0 -20
  103. package/src/pwa/registerSW.ts +0 -55
  104. package/src/riso/Halftone.tsx +0 -85
  105. package/src/riso/Misregister.tsx +0 -63
  106. package/src/riso/RisoStamp.tsx +0 -76
  107. package/src/riso/index.ts +0 -3
  108. package/src/sketch/HandUnderline.tsx +0 -53
  109. package/src/sketch/RoughArrow.tsx +0 -91
  110. package/src/sketch/RoughBox.tsx +0 -73
  111. package/src/sketch/StickyNote.tsx +0 -56
  112. package/src/sketch/index.ts +0 -4
  113. package/src/styles/base.css +0 -186
  114. package/src/styles/chrome-ambient.css +0 -235
  115. package/src/styles/chrome-bento.css +0 -192
  116. package/src/styles/chrome-editorial.css +0 -168
  117. package/src/styles/chrome-neobrutalism.css +0 -326
  118. package/src/styles/chrome-riso.css +0 -339
  119. package/src/styles/chrome-sketch.css +0 -362
  120. package/src/styles/chrome-swiss.css +0 -265
  121. package/src/styles/chrome-terminal.css +0 -272
  122. package/src/styles/fonts.css +0 -1034
  123. package/src/styles/index.css +0 -343
  124. package/src/styles/tokens.css +0 -941
  125. package/src/terminal/AsciiBox.tsx +0 -65
  126. package/src/terminal/BrailleSpinner.tsx +0 -46
  127. package/src/terminal/index.ts +0 -4
  128. package/src/theme/ThemeProvider.tsx +0 -99
  129. package/src/theme/index.ts +0 -6
  130. package/src/theme/inline-script.ts +0 -40
  131. package/src/theme/theme-context.ts +0 -7
  132. package/src/theme/types.ts +0 -39
  133. package/src/theme/useTheme.ts +0 -8
@@ -1,213 +0,0 @@
1
- import {
2
- cloneElement,
3
- isValidElement,
4
- useCallback,
5
- useEffect,
6
- useRef,
7
- useState,
8
- type CSSProperties,
9
- type ReactElement,
10
- type ReactNode,
11
- } from "react";
12
- import { createPortal } from "react-dom";
13
-
14
- // Lightweight tooltip — replaces the native `title=` attribute when
15
- // you want a short reveal delay (default 120 ms vs the OS' ~500 ms)
16
- // and styling that matches the kit's chrome. Portals to body so it
17
- // floats above transformed parents / fixed bars.
18
- //
19
- // Wraps a single React element child; mouse + focus listeners are
20
- // attached via clone so the trigger keeps its own handlers.
21
-
22
- export interface TooltipProps {
23
- content: ReactNode;
24
- children: ReactElement<{
25
- onMouseEnter?: React.MouseEventHandler;
26
- onMouseLeave?: React.MouseEventHandler;
27
- onFocus?: React.FocusEventHandler;
28
- onBlur?: React.FocusEventHandler;
29
- }>;
30
- // ms before showing. Default 120.
31
- delay?: number;
32
- // "top" hovers above the trigger, "bottom" below, "auto" prefers
33
- // top but flips to bottom when the trigger sits near the viewport
34
- // top edge (would otherwise clip against the browser chrome).
35
- // Default "auto".
36
- placement?: "top" | "bottom" | "auto";
37
- // Render even if `content` is falsy. Default false — callers can
38
- // wrap unconditionally without branching JSX.
39
- alwaysRender?: boolean;
40
- // When true, force the tooltip to a single line and truncate
41
- // overflow with an ellipsis. Default false — content wraps to
42
- // multiple lines within `maxWidth`. Existing single-line labels
43
- // (scan-status pill, date row) render identically because they
44
- // fit within maxWidth without wrapping; longer hints (explanatory
45
- // text under form fields, etc.) now display in full.
46
- truncate?: boolean;
47
- }
48
-
49
- // AUTO_FLIP_THRESHOLD_PX = the vertical space "top" placement needs
50
- // above the trigger (tooltip height + gap + a bit of breathing room).
51
- // Trigger positioned closer than this to the viewport top gets a
52
- // bottom-flip.
53
- const AUTO_FLIP_THRESHOLD_PX = 48;
54
-
55
- export function Tooltip({
56
- content,
57
- children,
58
- delay = 120,
59
- placement = "auto",
60
- alwaysRender = false,
61
- truncate = false,
62
- }: TooltipProps) {
63
- const triggerRef = useRef<HTMLElement | null>(null);
64
- const [open, setOpen] = useState(false);
65
- const [pos, setPos] = useState<{ x: number; y: number } | null>(null);
66
- const [resolvedPlacement, setResolvedPlacement] = useState<"top" | "bottom">(
67
- placement === "bottom" ? "bottom" : "top",
68
- );
69
- const timerRef = useRef<number | null>(null);
70
-
71
- const cancelTimer = useCallback(() => {
72
- if (timerRef.current !== null) {
73
- window.clearTimeout(timerRef.current);
74
- timerRef.current = null;
75
- }
76
- }, []);
77
-
78
- const computePos = useCallback(() => {
79
- const el = triggerRef.current;
80
- if (!el) return;
81
- const r = el.getBoundingClientRect();
82
- // "auto" resolves to top when there's enough room above, else
83
- // bottom. Explicit "top" / "bottom" are honoured verbatim.
84
- let effective: "top" | "bottom";
85
- if (placement === "auto") {
86
- effective = r.top < AUTO_FLIP_THRESHOLD_PX ? "bottom" : "top";
87
- } else {
88
- effective = placement;
89
- }
90
- setResolvedPlacement(effective);
91
- setPos({
92
- x: r.left + r.width / 2,
93
- y: effective === "top" ? r.top : r.bottom,
94
- });
95
- }, [placement]);
96
-
97
- const show = useCallback(() => {
98
- cancelTimer();
99
- timerRef.current = window.setTimeout(() => {
100
- computePos();
101
- setOpen(true);
102
- timerRef.current = null;
103
- }, delay);
104
- }, [cancelTimer, computePos, delay]);
105
-
106
- const hide = useCallback(() => {
107
- cancelTimer();
108
- setOpen(false);
109
- }, [cancelTimer]);
110
-
111
- useEffect(() => {
112
- if (!open) return;
113
- const onScrollResize = () => computePos();
114
- window.addEventListener("scroll", onScrollResize, { passive: true, capture: true });
115
- window.addEventListener("resize", onScrollResize);
116
- return () => {
117
- window.removeEventListener("scroll", onScrollResize, { capture: true });
118
- window.removeEventListener("resize", onScrollResize);
119
- };
120
- }, [open, computePos]);
121
-
122
- useEffect(() => cancelTimer, [cancelTimer]);
123
-
124
- if (!isValidElement(children)) return children;
125
- if (!content && !alwaysRender) return children;
126
-
127
- const child = children;
128
- // cloneElement's `Partial<P>` signature isn't compatible with the
129
- // `T | undefined` event-handler types in HTMLAttributes once
130
- // `exactOptionalPropertyTypes: true` is on. The runtime behaviour is fine —
131
- // React happily accepts these props on any host element. We narrow via
132
- // unknown to silence the type system on a known React-types limitation.
133
- const clonedProps = {
134
- ref: (el: HTMLElement | null) => {
135
- triggerRef.current = el;
136
- const childRef = (child as unknown as { ref?: React.Ref<HTMLElement> }).ref;
137
- if (typeof childRef === "function") childRef(el);
138
- else if (childRef && typeof childRef === "object") {
139
- (childRef as { current: HTMLElement | null }).current = el;
140
- }
141
- },
142
- onMouseEnter: (e: React.MouseEvent) => {
143
- child.props.onMouseEnter?.(e);
144
- show();
145
- },
146
- onMouseLeave: (e: React.MouseEvent) => {
147
- child.props.onMouseLeave?.(e);
148
- hide();
149
- },
150
- onFocus: (e: React.FocusEvent) => {
151
- child.props.onFocus?.(e);
152
- show();
153
- },
154
- onBlur: (e: React.FocusEvent) => {
155
- child.props.onBlur?.(e);
156
- hide();
157
- },
158
- };
159
- const clonedTrigger = cloneElement(
160
- child,
161
- clonedProps as unknown as Parameters<typeof cloneElement>[1],
162
- );
163
-
164
- const tooltipStyle: CSSProperties = {
165
- position: "fixed",
166
- left: pos?.x ?? -9999,
167
- top: pos?.y ?? -9999,
168
- transform:
169
- resolvedPlacement === "top"
170
- ? "translate(-50%, calc(-100% - 8px))"
171
- : "translate(-50%, 8px)",
172
- background: "var(--ck-bg-surface)",
173
- color: "var(--ck-text-primary)",
174
- border: "1px solid var(--ck-border-strong)",
175
- borderRadius: "var(--ck-radius-sm)",
176
- boxShadow: "var(--ck-shadow-2)",
177
- padding: "6px 10px",
178
- fontSize: 11,
179
- fontFamily: "var(--ck-font-mono)",
180
- lineHeight: 1.45,
181
- letterSpacing: "0.02em",
182
- pointerEvents: "none",
183
- zIndex: 9999,
184
- maxWidth: 320,
185
- // Default: wrap long hints. Opt-in `truncate` restores the old
186
- // single-line-with-ellipsis behaviour for compact labels where
187
- // horizontal overflow is preferable to a taller tooltip (e.g.
188
- // dense toolbars). Single-line labels shorter than maxWidth
189
- // render identically in both modes — the branch only matters
190
- // when content would need multiple lines.
191
- ...(truncate
192
- ? {
193
- whiteSpace: "nowrap" as const,
194
- overflow: "hidden" as const,
195
- textOverflow: "ellipsis" as const,
196
- }
197
- : {
198
- whiteSpace: "normal" as const,
199
- wordBreak: "break-word" as const,
200
- }),
201
- opacity: open && pos ? 1 : 0,
202
- transition: "opacity 120ms ease",
203
- };
204
-
205
- return (
206
- <>
207
- {clonedTrigger}
208
- {(open || alwaysRender) &&
209
- pos &&
210
- createPortal(<div style={tooltipStyle}>{content}</div>, document.body)}
211
- </>
212
- );
213
- }
@@ -1,53 +0,0 @@
1
- export { Button } from "./Button";
2
- export type { ButtonProps, ButtonVariant } from "./Button";
3
- export { Card } from "./Card";
4
- export type { CardProps } from "./Card";
5
- export { Tag } from "./Tag";
6
- export type { TagProps, TagTone } from "./Tag";
7
- export { CountBadge } from "./CountBadge";
8
- export type { CountBadgeProps } from "./CountBadge";
9
- export { Kbd } from "./Kbd";
10
- export type { KbdProps } from "./Kbd";
11
- export { Avatar } from "./Avatar";
12
- export type { AvatarProps } from "./Avatar";
13
- export { Input } from "./Input";
14
- export type { InputProps } from "./Input";
15
- export { Select } from "./Select";
16
- export type { SelectProps, SelectOption } from "./Select";
17
- export { Combobox } from "./Combobox";
18
- export type {
19
- ComboboxProps,
20
- ComboboxOption,
21
- ComboboxCommit,
22
- ComboboxCommitted,
23
- ComboboxHandle,
24
- } from "./Combobox";
25
- export { Chip } from "./Chip";
26
- export type { ChipProps, ChipTone } from "./Chip";
27
- export { MultiCombobox } from "./MultiCombobox";
28
- export type {
29
- MultiComboboxProps,
30
- MultiComboboxEntry,
31
- MultiComboboxHandle,
32
- MultiComboboxLayout,
33
- } from "./MultiCombobox";
34
- export { Textarea } from "./Textarea";
35
- export type { TextareaProps } from "./Textarea";
36
- export { Checkbox } from "./Checkbox";
37
- export type { CheckboxProps } from "./Checkbox";
38
- export { Radio } from "./Radio";
39
- export type { RadioProps } from "./Radio";
40
- export { ToggleSwitch } from "./ToggleSwitch";
41
- export type { ToggleSwitchProps } from "./ToggleSwitch";
42
- export { Tooltip } from "./Tooltip";
43
- export type { TooltipProps } from "./Tooltip";
44
- export { PageHeader } from "./PageHeader";
45
- export type { PageHeaderProps } from "./PageHeader";
46
- export { MentionCombobox, findActiveMentionStart, extractQuery } from "./MentionCombobox";
47
- export type { MentionComboboxProps, MentionComboboxHandle } from "./MentionCombobox";
48
- export { SignaturePad } from "./SignaturePad";
49
- export type { SignaturePadProps, SignaturePadHandle } from "./SignaturePad";
50
- export { CopyField } from "./CopyField";
51
- export type { CopyFieldProps } from "./CopyField";
52
- export { SegmentedControl } from "./SegmentedControl";
53
- export type { SegmentedControlProps, SegmentedControlOption } from "./SegmentedControl";
@@ -1,132 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { Button } from "../primitives/Button";
3
-
4
- // Surfaces the browser's PWA install affordance — `beforeinstallprompt`
5
- // is the Chromium-side hook that fires when a site qualifies for
6
- // install. Without intervention browsers eventually suppress it; we
7
- // catch the event, render a dismissable banner, and trigger the
8
- // prompt only when the user clicks Install.
9
- //
10
- // Dismissal persists in localStorage so the banner doesn't reappear
11
- // on every page load after the user said no thanks.
12
- //
13
- // iOS Safari doesn't fire beforeinstallprompt at all — for that
14
- // platform you need to show your own "Tap share → Add to Home
15
- // Screen" instructions. Detect via `iosSafari` prop or your own
16
- // heuristic and render a different banner if needed.
17
-
18
- interface BeforeInstallPromptEvent extends Event {
19
- prompt: () => Promise<void>;
20
- userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
21
- }
22
-
23
- export interface InstallPromptBannerProps {
24
- // Heading text.
25
- title?: string;
26
- // Body text below the heading.
27
- message?: string;
28
- installLabel?: string;
29
- dismissLabel?: string;
30
- // localStorage key for the dismissal flag.
31
- storageKey?: string;
32
- // Called after the user installs (accepted) or dismisses.
33
- onResolve?: (outcome: "accepted" | "dismissed") => void;
34
- }
35
-
36
- export function InstallPromptBanner({
37
- title = "Install this app",
38
- message = "Add it to your home screen for a faster, full-screen experience.",
39
- installLabel = "Install",
40
- dismissLabel = "Not now",
41
- storageKey = "ck-install-prompt-dismissed",
42
- onResolve,
43
- }: InstallPromptBannerProps) {
44
- const [evt, setEvt] = useState<BeforeInstallPromptEvent | null>(null);
45
- const [dismissed, setDismissed] = useState(() => {
46
- if (typeof window === "undefined") return true;
47
- return window.localStorage.getItem(storageKey) === "1";
48
- });
49
-
50
- useEffect(() => {
51
- if (dismissed) return;
52
- const onBefore = (e: Event) => {
53
- e.preventDefault();
54
- setEvt(e as BeforeInstallPromptEvent);
55
- };
56
- window.addEventListener("beforeinstallprompt", onBefore);
57
- return () => window.removeEventListener("beforeinstallprompt", onBefore);
58
- }, [dismissed]);
59
-
60
- const dismiss = () => {
61
- window.localStorage.setItem(storageKey, "1");
62
- setDismissed(true);
63
- setEvt(null);
64
- onResolve?.("dismissed");
65
- };
66
-
67
- const install = async () => {
68
- if (!evt) return;
69
- await evt.prompt();
70
- const choice = await evt.userChoice;
71
- setEvt(null);
72
- if (choice.outcome === "accepted") {
73
- window.localStorage.setItem(storageKey, "1");
74
- setDismissed(true);
75
- }
76
- onResolve?.(choice.outcome);
77
- };
78
-
79
- if (dismissed || !evt) return null;
80
-
81
- return (
82
- <div
83
- role="region"
84
- aria-label="Install app"
85
- style={{
86
- position: "fixed",
87
- left: 16,
88
- right: 16,
89
- bottom: `calc(16px + var(--ck-tabbar-height, 0px))`,
90
- zIndex: 90,
91
- maxWidth: 480,
92
- marginLeft: "auto",
93
- marginRight: "auto",
94
- background: "var(--ck-bg-surface)",
95
- border: "1px solid var(--ck-border-subtle)",
96
- borderRadius: "var(--ck-radius-md)",
97
- boxShadow: "var(--ck-shadow-2)",
98
- padding: "14px 16px",
99
- display: "flex",
100
- gap: 12,
101
- alignItems: "center",
102
- fontFamily: "var(--ck-font-sans)",
103
- }}
104
- >
105
- <div style={{ flex: 1, minWidth: 0 }}>
106
- <div
107
- style={{
108
- font: "500 14px/1.3 var(--ck-font-sans)",
109
- color: "var(--ck-text-primary)",
110
- marginBottom: 2,
111
- }}
112
- >
113
- {title}
114
- </div>
115
- <div
116
- style={{
117
- font: "400 12px/1.4 var(--ck-font-sans)",
118
- color: "var(--ck-text-tertiary)",
119
- }}
120
- >
121
- {message}
122
- </div>
123
- </div>
124
- <Button variant="ghost" onClick={dismiss}>
125
- {dismissLabel}
126
- </Button>
127
- <Button variant="primary" onClick={install}>
128
- {installLabel}
129
- </Button>
130
- </div>
131
- );
132
- }
package/src/pwa/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export { InstallPromptBanner } from "./InstallPromptBanner";
2
- export type { InstallPromptBannerProps } from "./InstallPromptBanner";
3
- export { registerSW } from "./registerSW";
4
- export type { RegisterSWOptions } from "./registerSW";
@@ -1,20 +0,0 @@
1
- {
2
- "name": "Your App Name",
3
- "short_name": "AppName",
4
- "description": "One-line app description.",
5
- "start_url": "/",
6
- "display": "standalone",
7
- "orientation": "portrait",
8
- "theme_color": "#0B0D12",
9
- "background_color": "#0B0D12",
10
- "icons": [
11
- { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
12
- { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
13
- {
14
- "src": "/icon-512-maskable.png",
15
- "sizes": "512x512",
16
- "type": "image/png",
17
- "purpose": "maskable"
18
- }
19
- ]
20
- }
@@ -1,55 +0,0 @@
1
- // Thin wrapper around vite-plugin-pwa's `virtual:pwa-register`. Use
2
- // from your app's entry point (main.tsx) so the service worker is
3
- // registered on first paint. Behaviour:
4
- //
5
- // - new SW detected → optional onNeedRefresh callback (typically
6
- // toast prompting reload)
7
- // - SW activated for the first time → onOfflineReady callback
8
- // (typically toast announcing "Ready offline")
9
- //
10
- // The kit doesn't pull in vite-plugin-pwa as a dep — the consumer
11
- // app installs and configures it. This wrapper just centralises
12
- // the registration boilerplate so consumers don't repeat it.
13
-
14
- export interface RegisterSWOptions {
15
- onNeedRefresh?: () => void;
16
- onOfflineReady?: () => void;
17
- onRegistered?: (registration: ServiceWorkerRegistration | undefined) => void;
18
- onRegisterError?: (error: unknown) => void;
19
- }
20
-
21
- // Specifier is constructed from a runtime expression so Vite's
22
- // static import analyser can't see "virtual:pwa-register" as a
23
- // literal — without this, every consumer app would fail to boot
24
- // unless they had vite-plugin-pwa installed AND configured (since
25
- // transform happens before the try/catch runs at runtime).
26
- //
27
- // The eval is the simplest indirection that's also obviously a
28
- // no-op to readers. /* @vite-ignore */ on its own isn't enough
29
- // because Vite still attempts resolution when the specifier is a
30
- // constant string literal.
31
- const PWA_REGISTER_SPECIFIER = ["virtual", "pwa-register"].join(":");
32
-
33
- export async function registerSW(opts: RegisterSWOptions = {}) {
34
- if (typeof window === "undefined") return;
35
- try {
36
- const mod = await import(/* @vite-ignore */ PWA_REGISTER_SPECIFIER);
37
- if (typeof mod.registerSW !== "function") {
38
- console.warn("virtual:pwa-register found but registerSW() is not a function");
39
- return;
40
- }
41
- mod.registerSW({
42
- immediate: true,
43
- onNeedRefresh: opts.onNeedRefresh,
44
- onOfflineReady: opts.onOfflineReady,
45
- onRegisteredSW: (_url: string, reg: ServiceWorkerRegistration | undefined) => {
46
- opts.onRegistered?.(reg);
47
- },
48
- onRegisterError: opts.onRegisterError,
49
- });
50
- } catch (err) {
51
- // vite-plugin-pwa not installed / configured. Silent in dev,
52
- // surface to caller if they care.
53
- opts.onRegisterError?.(err);
54
- }
55
- }
@@ -1,85 +0,0 @@
1
- import type { CSSProperties, ReactNode } from "react";
2
-
3
- // Halftone-tinted image / fill block. Renders a solid ink-color
4
- // background with a halftone dot pattern overlay; if `src` is
5
- // provided, the image sits underneath in `mix-blend-mode: multiply`
6
- // for a duotone-on-paper feel.
7
- //
8
- // Use for hero blocks, empty-state illustrations, photo treatments.
9
-
10
- export interface HalftoneProps {
11
- children?: ReactNode;
12
- // Optional image source. The image is duotoned via blend modes.
13
- src?: string;
14
- alt?: string;
15
- // Ink color of the dot fill. Defaults to riso pink.
16
- ink?: string;
17
- // Dot size in px. Defaults to 1.6.
18
- dotSize?: number;
19
- // Dot grid spacing in px. Defaults to 7.
20
- dotSpacing?: number;
21
- width?: number | string;
22
- height?: number | string;
23
- className?: string;
24
- style?: CSSProperties;
25
- }
26
-
27
- export function Halftone({
28
- children,
29
- src,
30
- alt = "",
31
- ink = "var(--ck-riso-pink, #FF48B0)",
32
- dotSize = 1.6,
33
- dotSpacing = 7,
34
- width = "100%",
35
- height = 200,
36
- className,
37
- style,
38
- }: HalftoneProps) {
39
- return (
40
- <div
41
- className={className}
42
- style={{
43
- position: "relative",
44
- width,
45
- height,
46
- background: ink,
47
- overflow: "hidden",
48
- borderRadius: 4,
49
- ...style,
50
- }}
51
- >
52
- {src && (
53
- <img
54
- src={src}
55
- alt={alt}
56
- style={{
57
- position: "absolute",
58
- inset: 0,
59
- width: "100%",
60
- height: "100%",
61
- objectFit: "cover",
62
- mixBlendMode: "multiply",
63
- filter: "grayscale(1) contrast(1.15)",
64
- pointerEvents: "none",
65
- }}
66
- />
67
- )}
68
- <div
69
- aria-hidden
70
- style={{
71
- position: "absolute",
72
- inset: 0,
73
- pointerEvents: "none",
74
- backgroundImage: `radial-gradient(circle, rgba(26,26,26,0.85) ${dotSize}px, transparent ${dotSize + 0.2}px)`,
75
- backgroundSize: `${dotSpacing}px ${dotSpacing}px`,
76
- mixBlendMode: "multiply",
77
- opacity: 0.55,
78
- }}
79
- />
80
- {children && (
81
- <div style={{ position: "relative", zIndex: 2, color: "#1A1A1A" }}>{children}</div>
82
- )}
83
- </div>
84
- );
85
- }
@@ -1,63 +0,0 @@
1
- import type { CSSProperties, ReactNode } from "react";
2
-
3
- // Misregistration — render text (or any child) twice, with a small
4
- // offset between an "under" duplicate in a second ink color and the
5
- // "top" pass in the primary ink. The signature riso effect.
6
- //
7
- // The under copy is set aria-hidden so screen readers don't double-
8
- // read the content.
9
-
10
- export interface MisregisterProps {
11
- children?: ReactNode;
12
- // Ink color of the foreground (top) pass.
13
- ink?: string;
14
- // Ink color of the misregistered duplicate. Defaults to riso pink.
15
- offsetInk?: string;
16
- // Offset of the duplicate, in px. Defaults to {x:3, y:3}.
17
- offsetX?: number;
18
- offsetY?: number;
19
- // Wrap in inline-block? Defaults true. Pass false for block use.
20
- inline?: boolean;
21
- className?: string;
22
- style?: CSSProperties;
23
- }
24
-
25
- export function Misregister({
26
- children,
27
- ink,
28
- offsetInk = "var(--ck-riso-pink, #FF48B0)",
29
- offsetX = 3,
30
- offsetY = 3,
31
- inline = true,
32
- className,
33
- style,
34
- }: MisregisterProps) {
35
- return (
36
- <span
37
- className={className}
38
- style={{
39
- display: inline ? "inline-block" : "block",
40
- position: "relative",
41
- color: ink ?? "var(--ck-text-primary)",
42
- ...style,
43
- }}
44
- >
45
- <span
46
- aria-hidden
47
- style={{
48
- position: "absolute",
49
- top: offsetY,
50
- left: offsetX,
51
- color: offsetInk,
52
- mixBlendMode: "multiply",
53
- pointerEvents: "none",
54
- width: "100%",
55
- height: "100%",
56
- }}
57
- >
58
- {children}
59
- </span>
60
- <span style={{ position: "relative" }}>{children}</span>
61
- </span>
62
- );
63
- }