@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.
- package/CHANGELOG.md +577 -0
- package/NOTICE +11 -0
- package/README.md +52 -0
- package/package.json +33 -7
- package/src/fonts.css +20 -0
- package/src/index.ts +8 -25
- package/src/lib/cn.ts +23 -6
- package/src/styles.css +17 -0
- package/src/theme.css +132 -0
- package/src/tokens/colors.css +112 -0
- package/src/tokens/compat.css +36 -0
- package/src/tokens/elevation.css +14 -0
- package/src/tokens/motion.css +42 -0
- package/src/tokens/spacing.css +55 -0
- package/src/tokens/typography-cjk.css +42 -0
- package/src/tokens/typography.css +81 -0
- package/src/actionbar/ActionBar.tsx +0 -837
- package/src/actionbar/ActionBarButton.tsx +0 -111
- package/src/actionbar/ActionBarMenuGroup.tsx +0 -116
- package/src/actionbar/ActionBarModeHandle.tsx +0 -171
- package/src/actionbar/ActionBarProvider.tsx +0 -91
- package/src/actionbar/actionbar-context.ts +0 -45
- package/src/actionbar/index.ts +0 -28
- package/src/actionbar/types.ts +0 -147
- package/src/actionbar/useActionBarItems.ts +0 -60
- package/src/actionbar/useActionBarMode.ts +0 -144
- package/src/actionbar/useActionBarPanel.ts +0 -56
- package/src/actionbar/useActionBarStatusDot.ts +0 -49
- package/src/actionbar/useActionBarToast.ts +0 -33
- package/src/ambient/AmbientBackdrop.tsx +0 -74
- package/src/ambient/CommandInput.tsx +0 -107
- package/src/ambient/SuperbarStrip.tsx +0 -36
- package/src/ambient/index.ts +0 -6
- package/src/bento/BentoCell.tsx +0 -66
- package/src/bento/BentoGrid.tsx +0 -42
- package/src/bento/index.ts +0 -2
- package/src/command/CommandPalette.tsx +0 -329
- package/src/command/CommandProvider.tsx +0 -57
- package/src/command/command-context.ts +0 -12
- package/src/command/index.ts +0 -6
- package/src/command/rank.ts +0 -50
- package/src/command/types.ts +0 -37
- package/src/command/useCommandSource.ts +0 -37
- package/src/dialogs/DialogsProvider.tsx +0 -216
- package/src/dialogs/Modal.tsx +0 -260
- package/src/dialogs/Toast.tsx +0 -85
- package/src/dialogs/dialogs-context.ts +0 -6
- package/src/dialogs/index.ts +0 -10
- package/src/dialogs/types.ts +0 -37
- package/src/dialogs/useDialogs.ts +0 -8
- package/src/editorial/EditorialSpotlight.tsx +0 -63
- package/src/editorial/Folio.tsx +0 -52
- package/src/editorial/PlateMarker.tsx +0 -33
- package/src/editorial/RomanSection.tsx +0 -65
- package/src/editorial/RunningMarginalia.tsx +0 -65
- package/src/editorial/index.ts +0 -10
- package/src/hooks/index.ts +0 -5
- package/src/hooks/useKeyboardHotkey.ts +0 -93
- package/src/hooks/useReducedMotion.ts +0 -20
- package/src/hooks/useViewport.ts +0 -61
- package/src/layout/Breadcrumb.tsx +0 -74
- package/src/layout/LeftNavRail.tsx +0 -139
- package/src/layout/MobileTabBar.tsx +0 -101
- package/src/layout/NavItem.tsx +0 -128
- package/src/layout/NavSearchTrigger.tsx +0 -88
- package/src/layout/NavSection.tsx +0 -40
- package/src/layout/RightSidebarPanel.tsx +0 -111
- package/src/layout/Shell.tsx +0 -91
- package/src/layout/SidePanel.tsx +0 -316
- package/src/layout/StickyBanner.tsx +0 -83
- package/src/layout/Topbar.tsx +0 -68
- package/src/layout/index.ts +0 -24
- package/src/layout/useNavRailState.ts +0 -69
- package/src/lib/time-utils.ts +0 -44
- package/src/neobrutalism/Marquee.tsx +0 -81
- package/src/neobrutalism/Sticker.tsx +0 -71
- package/src/neobrutalism/index.ts +0 -4
- package/src/primitives/Avatar.tsx +0 -53
- package/src/primitives/Button.tsx +0 -67
- package/src/primitives/Card.tsx +0 -41
- package/src/primitives/Checkbox.tsx +0 -87
- package/src/primitives/Chip.tsx +0 -191
- package/src/primitives/Combobox.tsx +0 -507
- package/src/primitives/CopyField.tsx +0 -139
- package/src/primitives/CountBadge.tsx +0 -50
- package/src/primitives/Input.tsx +0 -204
- package/src/primitives/Kbd.tsx +0 -45
- package/src/primitives/MentionCombobox.tsx +0 -612
- package/src/primitives/MultiCombobox.tsx +0 -327
- package/src/primitives/PageHeader.tsx +0 -77
- package/src/primitives/Radio.tsx +0 -131
- package/src/primitives/SegmentedControl.tsx +0 -135
- package/src/primitives/Select.tsx +0 -779
- package/src/primitives/SignaturePad.tsx +0 -461
- package/src/primitives/Tag.tsx +0 -56
- package/src/primitives/Textarea.tsx +0 -62
- package/src/primitives/ToggleSwitch.tsx +0 -79
- package/src/primitives/Tooltip.tsx +0 -213
- package/src/primitives/index.ts +0 -53
- package/src/pwa/InstallPromptBanner.tsx +0 -132
- package/src/pwa/index.ts +0 -4
- package/src/pwa/manifest.template.json +0 -20
- package/src/pwa/registerSW.ts +0 -55
- package/src/riso/Halftone.tsx +0 -85
- package/src/riso/Misregister.tsx +0 -63
- package/src/riso/RisoStamp.tsx +0 -76
- package/src/riso/index.ts +0 -3
- package/src/sketch/HandUnderline.tsx +0 -53
- package/src/sketch/RoughArrow.tsx +0 -91
- package/src/sketch/RoughBox.tsx +0 -73
- package/src/sketch/StickyNote.tsx +0 -56
- package/src/sketch/index.ts +0 -4
- package/src/styles/base.css +0 -186
- package/src/styles/chrome-ambient.css +0 -235
- package/src/styles/chrome-bento.css +0 -192
- package/src/styles/chrome-editorial.css +0 -168
- package/src/styles/chrome-neobrutalism.css +0 -326
- package/src/styles/chrome-riso.css +0 -339
- package/src/styles/chrome-sketch.css +0 -362
- package/src/styles/chrome-swiss.css +0 -265
- package/src/styles/chrome-terminal.css +0 -272
- package/src/styles/fonts.css +0 -1034
- package/src/styles/index.css +0 -343
- package/src/styles/tokens.css +0 -941
- package/src/terminal/AsciiBox.tsx +0 -65
- package/src/terminal/BrailleSpinner.tsx +0 -46
- package/src/terminal/index.ts +0 -4
- package/src/theme/ThemeProvider.tsx +0 -99
- package/src/theme/index.ts +0 -6
- package/src/theme/inline-script.ts +0 -40
- package/src/theme/theme-context.ts +0 -7
- package/src/theme/types.ts +0 -39
- 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
|
-
}
|
package/src/primitives/index.ts
DELETED
|
@@ -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,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
|
-
}
|
package/src/pwa/registerSW.ts
DELETED
|
@@ -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
|
-
}
|
package/src/riso/Halftone.tsx
DELETED
|
@@ -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
|
-
}
|
package/src/riso/Misregister.tsx
DELETED
|
@@ -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
|
-
}
|