@twoux/ui 0.0.0-stage → 0.1.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/LICENSE.md +61 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/NOTICE.md +64 -0
- package/SHADCN-DIFF.md +228 -0
- package/agent/AGENTS.md +358 -0
- package/agent/docs-md/accessibility.md +49 -0
- package/agent/docs-md/action-catalogue.md +81 -0
- package/agent/docs-md/actions.md +74 -0
- package/agent/docs-md/agent-activity-log.md +70 -0
- package/agent/docs-md/agent-permissions.md +82 -0
- package/agent/docs-md/agent-run.md +90 -0
- package/agent/docs-md/agents-on-behalf.md +74 -0
- package/agent/docs-md/ai-column.md +85 -0
- package/agent/docs-md/ai-origin-label.md +70 -0
- package/agent/docs-md/announcing-features.md +73 -0
- package/agent/docs-md/answer-actions.md +81 -0
- package/agent/docs-md/app-shell.md +114 -0
- package/agent/docs-md/artifact-card.md +89 -0
- package/agent/docs-md/ask-on-screen.md +72 -0
- package/agent/docs-md/assistant-page.md +100 -0
- package/agent/docs-md/assistant-panel.md +142 -0
- package/agent/docs-md/avatar.md +82 -0
- package/agent/docs-md/banner.md +106 -0
- package/agent/docs-md/board.md +163 -0
- package/agent/docs-md/breadcrumb.md +89 -0
- package/agent/docs-md/breakdown-table.md +97 -0
- package/agent/docs-md/bulk-action-bar.md +112 -0
- package/agent/docs-md/button.md +293 -0
- package/agent/docs-md/card.md +115 -0
- package/agent/docs-md/change-list.md +99 -0
- package/agent/docs-md/change-request.md +166 -0
- package/agent/docs-md/chart-area.md +87 -0
- package/agent/docs-md/chart-bar.md +100 -0
- package/agent/docs-md/chart-box-plot.md +88 -0
- package/agent/docs-md/chart-bullet.md +89 -0
- package/agent/docs-md/chart-candlestick.md +112 -0
- package/agent/docs-md/chart-card-line.md +120 -0
- package/agent/docs-md/chart-donut.md +90 -0
- package/agent/docs-md/chart-drill-down.md +72 -0
- package/agent/docs-md/chart-funnel.md +71 -0
- package/agent/docs-md/chart-heatmap.md +76 -0
- package/agent/docs-md/chart-histogram.md +81 -0
- package/agent/docs-md/chart-map.md +165 -0
- package/agent/docs-md/chart-mixed.md +88 -0
- package/agent/docs-md/chart-price.md +92 -0
- package/agent/docs-md/chart-radial-bars.md +109 -0
- package/agent/docs-md/chart-radial-lines.md +111 -0
- package/agent/docs-md/chart-sankey.md +104 -0
- package/agent/docs-md/chart-scatter.md +87 -0
- package/agent/docs-md/chart-share-bar.md +89 -0
- package/agent/docs-md/chart-treemap.md +98 -0
- package/agent/docs-md/chart-waterfall.md +93 -0
- package/agent/docs-md/checkbox.md +131 -0
- package/agent/docs-md/choosing-selection-control.md +71 -0
- package/agent/docs-md/citations-sources.md +79 -0
- package/agent/docs-md/citing-sources.md +63 -0
- package/agent/docs-md/clarify-before-acting.md +69 -0
- package/agent/docs-md/clarifying-question.md +92 -0
- package/agent/docs-md/code-block.md +174 -0
- package/agent/docs-md/colours.md +87 -0
- package/agent/docs-md/columns-menu.md +140 -0
- package/agent/docs-md/combobox.md +161 -0
- package/agent/docs-md/command-palette.md +126 -0
- package/agent/docs-md/confidence-note.md +87 -0
- package/agent/docs-md/configurable-dashboard.md +76 -0
- package/agent/docs-md/confirmation-dialog.md +93 -0
- package/agent/docs-md/connected-tools.md +80 -0
- package/agent/docs-md/connector-picker.md +127 -0
- package/agent/docs-md/consistency.md +53 -0
- package/agent/docs-md/content-density.md +61 -0
- package/agent/docs-md/conversation-history.md +78 -0
- package/agent/docs-md/create-record.md +80 -0
- package/agent/docs-md/csv-import.md +88 -0
- package/agent/docs-md/dashboard-items.md +78 -0
- package/agent/docs-md/data-export.md +61 -0
- package/agent/docs-md/data-grid.md +137 -0
- package/agent/docs-md/data-vis-colours.md +76 -0
- package/agent/docs-md/data-visualisation.md +76 -0
- package/agent/docs-md/date-picker.md +178 -0
- package/agent/docs-md/delete-record.md +75 -0
- package/agent/docs-md/dense-table.md +495 -0
- package/agent/docs-md/design-tokens.md +53 -0
- package/agent/docs-md/disabled-read-only.md +75 -0
- package/agent/docs-md/drag-and-drop.md +74 -0
- package/agent/docs-md/dropdown-menu.md +137 -0
- package/agent/docs-md/edit-record.md +73 -0
- package/agent/docs-md/empty-loading-error.md +78 -0
- package/agent/docs-md/empty-state.md +128 -0
- package/agent/docs-md/error-messages.md +75 -0
- package/agent/docs-md/error-state.md +123 -0
- package/agent/docs-md/export-menu.md +86 -0
- package/agent/docs-md/feedback-prompt.md +88 -0
- package/agent/docs-md/field-mapping.md +78 -0
- package/agent/docs-md/file-drop.md +110 -0
- package/agent/docs-md/filter-bar.md +138 -0
- package/agent/docs-md/filter-panel.md +103 -0
- package/agent/docs-md/filtering-search.md +79 -0
- package/agent/docs-md/follow-ups-starters.md +79 -0
- package/agent/docs-md/form-validation.md +95 -0
- package/agent/docs-md/generated-columns.md +65 -0
- package/agent/docs-md/generated-drafts-files.md +71 -0
- package/agent/docs-md/grouped-list.md +89 -0
- package/agent/docs-md/guide-cue.md +170 -0
- package/agent/docs-md/help-panel.md +103 -0
- package/agent/docs-md/help-system.md +74 -0
- package/agent/docs-md/iconography.md +49 -0
- package/agent/docs-md/inbox.md +108 -0
- package/agent/docs-md/inline-editing-validation.md +80 -0
- package/agent/docs-md/inline-input-in-answer.md +70 -0
- package/agent/docs-md/items-palette.md +116 -0
- package/agent/docs-md/keyboard-model.md +47 -0
- package/agent/docs-md/keyboard-navigation.md +68 -0
- package/agent/docs-md/keyboard-shortcut-hint.md +87 -0
- package/agent/docs-md/keyboard-shortcut-sheet.md +90 -0
- package/agent/docs-md/labelling-ai-output.md +66 -0
- package/agent/docs-md/layout.md +47 -0
- package/agent/docs-md/list-table.md +254 -0
- package/agent/docs-md/loading-skeleton.md +91 -0
- package/agent/docs-md/managing-agents.md +71 -0
- package/agent/docs-md/message-composer.md +105 -0
- package/agent/docs-md/migrating.md +49 -0
- package/agent/docs-md/motion.md +93 -0
- package/agent/docs-md/navigation.md +76 -0
- package/agent/docs-md/notification-channels.md +65 -0
- package/agent/docs-md/notifications-decision.md +100 -0
- package/agent/docs-md/onboarding-step.md +114 -0
- package/agent/docs-md/order-book.md +108 -0
- package/agent/docs-md/page-header.md +96 -0
- package/agent/docs-md/pagination.md +88 -0
- package/agent/docs-md/pane-layout.md +105 -0
- package/agent/docs-md/period-comparison.md +66 -0
- package/agent/docs-md/plan-card.md +120 -0
- package/agent/docs-md/popover-rules.md +88 -0
- package/agent/docs-md/prompt-editor.md +90 -0
- package/agent/docs-md/propose-apply.md +65 -0
- package/agent/docs-md/provider-logo.md +97 -0
- package/agent/docs-md/radio.md +83 -0
- package/agent/docs-md/ranked-list.md +102 -0
- package/agent/docs-md/reasoning-progress.md +72 -0
- package/agent/docs-md/record-detail.md +71 -0
- package/agent/docs-md/regenerating-answers.md +70 -0
- package/agent/docs-md/run-status-bar.md +94 -0
- package/agent/docs-md/saved-tasks-schedules.md +63 -0
- package/agent/docs-md/saved-views.md +149 -0
- package/agent/docs-md/saving-views.md +68 -0
- package/agent/docs-md/scheduler.md +258 -0
- package/agent/docs-md/secondary-panels.md +72 -0
- package/agent/docs-md/segmented-control.md +88 -0
- package/agent/docs-md/selection-bulk-actions.md +70 -0
- package/agent/docs-md/separator.md +75 -0
- package/agent/docs-md/setup-checklist.md +92 -0
- package/agent/docs-md/shape-elevation.md +55 -0
- package/agent/docs-md/sheet-grid.md +211 -0
- package/agent/docs-md/side-panel.md +134 -0
- package/agent/docs-md/sign-in.md +114 -0
- package/agent/docs-md/spacing.md +55 -0
- package/agent/docs-md/split-button.md +100 -0
- package/agent/docs-md/stat-compare.md +82 -0
- package/agent/docs-md/static-dashboard.md +75 -0
- package/agent/docs-md/status-badge.md +140 -0
- package/agent/docs-md/step-card.md +75 -0
- package/agent/docs-md/stepper.md +92 -0
- package/agent/docs-md/suggested-changes.md +91 -0
- package/agent/docs-md/switch.md +78 -0
- package/agent/docs-md/table-settings.md +81 -0
- package/agent/docs-md/tabs.md +85 -0
- package/agent/docs-md/tag-input.md +108 -0
- package/agent/docs-md/tag.md +79 -0
- package/agent/docs-md/task-builder.md +87 -0
- package/agent/docs-md/team-instructions.md +72 -0
- package/agent/docs-md/templates-gallery.md +85 -0
- package/agent/docs-md/text-input.md +160 -0
- package/agent/docs-md/thinking-steps.md +89 -0
- package/agent/docs-md/timeline.md +189 -0
- package/agent/docs-md/timestamp.md +84 -0
- package/agent/docs-md/timestamps.md +70 -0
- package/agent/docs-md/toast.md +117 -0
- package/agent/docs-md/tooltip.md +90 -0
- package/agent/docs-md/typography.md +62 -0
- package/agent/docs-md/undo-destructive.md +66 -0
- package/agent/docs-md/unsaved-changes.md +71 -0
- package/agent/docs-md/usage-meter.md +80 -0
- package/agent/docs-md/user-feedback.md +71 -0
- package/agent/docs-md/voice-and-tone.md +60 -0
- package/agent/docs-md/why-choice.md +93 -0
- package/agent/docs-md/workspace-onboarding.md +74 -0
- package/agent/docs-md/writing.md +48 -0
- package/agent/llms-full.txt +18369 -0
- package/agent/llms.txt +198 -0
- package/agent/skills/twoux-audit/SKILL.md +49 -0
- package/agent/skills/twoux-build/SKILL.md +39 -0
- package/agent/skills/twoux-design/SKILL.md +54 -0
- package/agent/skills/twoux-write/SKILL.md +32 -0
- package/bin/check.mjs +123 -0
- package/bin/docs.mjs +24 -0
- package/bin/guard.mjs +27 -0
- package/bin/init.mjs +113 -0
- package/bin/knowledge.mjs +87 -0
- package/bin/mcp.mjs +116 -0
- package/bin/theme.mjs +168 -0
- package/bin/twoux.mjs +25 -0
- package/package.json +80 -3
- package/registry/jobs.json +1028 -0
- package/registry/palette.json +770 -0
- package/registry/rules.json +913 -0
- package/src/components/agent-review.tsx +364 -0
- package/src/components/ai-basics.tsx +212 -0
- package/src/components/ai-work.tsx +304 -0
- package/src/components/ai.tsx +227 -0
- package/src/components/alert-dialog.tsx +203 -0
- package/src/components/alert.tsx +86 -0
- package/src/components/avatar.tsx +114 -0
- package/src/components/badge.tsx +59 -0
- package/src/components/board.tsx +397 -0
- package/src/components/breadcrumb.tsx +114 -0
- package/src/components/button-group.tsx +90 -0
- package/src/components/button.tsx +109 -0
- package/src/components/calendar.tsx +247 -0
- package/src/components/card.tsx +102 -0
- package/src/components/chart-map.tsx +336 -0
- package/src/components/charts-more.tsx +1331 -0
- package/src/components/charts-radial.tsx +328 -0
- package/src/components/charts.tsx +390 -0
- package/src/components/checkbox.tsx +40 -0
- package/src/components/code.tsx +139 -0
- package/src/components/collapsible.tsx +37 -0
- package/src/components/combobox.tsx +339 -0
- package/src/components/command-extras.tsx +11 -0
- package/src/components/command.tsx +196 -0
- package/src/components/data-grid.tsx +213 -0
- package/src/components/data-table.tsx +334 -0
- package/src/components/data-views.tsx +740 -0
- package/src/components/date-picker.tsx +686 -0
- package/src/components/dense-table.tsx +207 -0
- package/src/components/dialog.tsx +162 -0
- package/src/components/dropdown-menu.tsx +264 -0
- package/src/components/empty.tsx +114 -0
- package/src/components/feedback-prompt.tsx +80 -0
- package/src/components/field.tsx +254 -0
- package/src/components/fields.tsx +112 -0
- package/src/components/flow.tsx +840 -0
- package/src/components/guide-cue.tsx +273 -0
- package/src/components/inbox.tsx +113 -0
- package/src/components/input-group.tsx +176 -0
- package/src/components/input.tsx +26 -0
- package/src/components/kbd.tsx +34 -0
- package/src/components/label.tsx +27 -0
- package/src/components/labels.tsx +176 -0
- package/src/components/motion.tsx +136 -0
- package/src/components/onboarding.tsx +454 -0
- package/src/components/order-book.tsx +185 -0
- package/src/components/pane-layout.tsx +417 -0
- package/src/components/popover.tsx +96 -0
- package/src/components/provider-logos.data.ts +49 -0
- package/src/components/radio-group.tsx +52 -0
- package/src/components/scheduler.tsx +997 -0
- package/src/components/segmented-control.tsx +28 -0
- package/src/components/select.tsx +195 -0
- package/src/components/separator.tsx +31 -0
- package/src/components/sheet.tsx +148 -0
- package/src/components/shell.tsx +426 -0
- package/src/components/shortcut-sheet.tsx +83 -0
- package/src/components/skeleton.tsx +18 -0
- package/src/components/sonner.tsx +107 -0
- package/src/components/spinner.tsx +13 -0
- package/src/components/split-button.tsx +58 -0
- package/src/components/stepper.tsx +78 -0
- package/src/components/switch.tsx +43 -0
- package/src/components/table.tsx +126 -0
- package/src/components/tabs.tsx +118 -0
- package/src/components/tag-input.tsx +253 -0
- package/src/components/textarea.tsx +21 -0
- package/src/components/theme.tsx +80 -0
- package/src/components/timeline.tsx +408 -0
- package/src/components/toggle-group.tsx +87 -0
- package/src/components/toggle.tsx +54 -0
- package/src/components/tooltip.tsx +65 -0
- package/src/components/workspace-actions.tsx +294 -0
- package/src/components/workspace-import.tsx +465 -0
- package/src/components/workspace.tsx +1884 -0
- package/src/index.ts +80 -0
- package/src/lib/active-indicator.tsx +53 -0
- package/src/lib/chart-kit.tsx +72 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/shortcuts.tsx +61 -0
- package/src/lib/workspace.tsx +10 -0
- package/src/shadcn/alert-dialog.tsx +196 -0
- package/src/shadcn/alert.tsx +65 -0
- package/src/shadcn/avatar.tsx +108 -0
- package/src/shadcn/badge.tsx +47 -0
- package/src/shadcn/breadcrumb.tsx +108 -0
- package/src/shadcn/button-group.tsx +83 -0
- package/src/shadcn/button.tsx +63 -0
- package/src/shadcn/calendar.tsx +220 -0
- package/src/shadcn/card.tsx +91 -0
- package/src/shadcn/checkbox.tsx +31 -0
- package/src/shadcn/collapsible.tsx +33 -0
- package/src/shadcn/command.tsx +184 -0
- package/src/shadcn/dialog.tsx +158 -0
- package/src/shadcn/dropdown-menu.tsx +256 -0
- package/src/shadcn/empty.tsx +103 -0
- package/src/shadcn/field.tsx +248 -0
- package/src/shadcn/input-group.tsx +170 -0
- package/src/shadcn/input.tsx +20 -0
- package/src/shadcn/kbd.tsx +28 -0
- package/src/shadcn/label.tsx +23 -0
- package/src/shadcn/popover.tsx +88 -0
- package/src/shadcn/radio-group.tsx +44 -0
- package/src/shadcn/select.tsx +189 -0
- package/src/shadcn/separator.tsx +27 -0
- package/src/shadcn/sheet.tsx +142 -0
- package/src/shadcn/skeleton.tsx +13 -0
- package/src/shadcn/sonner.tsx +40 -0
- package/src/shadcn/switch.tsx +34 -0
- package/src/shadcn/table.tsx +115 -0
- package/src/shadcn/tabs.tsx +90 -0
- package/src/shadcn/textarea.tsx +17 -0
- package/src/shadcn/toggle-group.tsx +83 -0
- package/src/shadcn/toggle.tsx +46 -0
- package/src/shadcn/tooltip.tsx +56 -0
- package/src/styles/palette.css +127 -0
- package/src/styles/theme.css +35 -0
- package/src/styles/twoux.css +236 -0
- package/theme.json +14 -0
- package/README.md +0 -3
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GuideCue: a short guide of one to three popovers, each pointing at a real control. Pulled, never pushed.
|
|
3
|
+
* TwoUX composition on the @twoux/ui Popover (Radix Popover.Anchor with the target element as its virtual ref).
|
|
4
|
+
* - Opens only when the person asks: a SetupChecklist Start, a Help menu entry, a HelpPanel link or a FeatureLabel
|
|
5
|
+
* popover's "Show me". Never on page load, on a timer or on hover. This is the only in-product guide TwoUX allows.
|
|
6
|
+
* - One to three steps, counted as text ("Step 2 of 3"). A step whose target is missing or hidden is skipped, so the
|
|
7
|
+
* cue never points at nothing; if no step has a target, the guide does not open.
|
|
8
|
+
* - The target gets the focus ring (outline in the focus colour) drawn above it. No dimmed overlay, no spotlight, no
|
|
9
|
+
* beacon: the page stays usable, and nothing pulses (WCAG 2.3.3).
|
|
10
|
+
* - Not modal: role="dialog" aria-modal="false", named by its heading, described by its body. Focus moves to the
|
|
11
|
+
* heading on open, step changes are announced, Esc or Close ends it and focus returns to what started it.
|
|
12
|
+
* - Back (outline), Next or Done (the single primary inside the cue) and Close (ghost icon button).
|
|
13
|
+
*/
|
|
14
|
+
"use client"
|
|
15
|
+
|
|
16
|
+
import { X } from "lucide-react"
|
|
17
|
+
import * as React from "react"
|
|
18
|
+
import { createPortal } from "react-dom"
|
|
19
|
+
import { cn } from "../lib/cn"
|
|
20
|
+
import { Button } from "./button"
|
|
21
|
+
import { Kbd } from "./kbd"
|
|
22
|
+
import { Popover, PopoverAnchor, PopoverContent } from "./popover"
|
|
23
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
|
|
24
|
+
|
|
25
|
+
/* ---------------------------------------------------------------- types */
|
|
26
|
+
|
|
27
|
+
/** Where a step points: a ref, a CSS selector (prefer a data attribute, e.g. '[data-guide="filters"]') or a function. */
|
|
28
|
+
export type GuideTarget = React.RefObject<HTMLElement | null> | string | (() => HTMLElement | null | undefined)
|
|
29
|
+
|
|
30
|
+
export type GuideStep = {
|
|
31
|
+
target: GuideTarget
|
|
32
|
+
/** Names what the person does here, sentence case: "Filter the list" */
|
|
33
|
+
title: string
|
|
34
|
+
/** One or two sentences: what the control does for them. */
|
|
35
|
+
body: React.ReactNode
|
|
36
|
+
/** Preferred side of the target. The cue flips when there is no room. */
|
|
37
|
+
side?: "top" | "right" | "bottom" | "left"
|
|
38
|
+
align?: "start" | "center" | "end"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type GuideCueProps = {
|
|
42
|
+
/** One to three steps. More are dropped (with a warning in development): split the guide or write a help article. */
|
|
43
|
+
steps: GuideStep[]
|
|
44
|
+
open: boolean
|
|
45
|
+
onOpenChange: (open: boolean) => void
|
|
46
|
+
/** Called when the person presses Done on the last step (not on Close or Esc). */
|
|
47
|
+
onFinish?: () => void
|
|
48
|
+
/** Accessible name prefix for the step count and the live announcement, e.g. "Saved views guide". */
|
|
49
|
+
label?: string
|
|
50
|
+
/**
|
|
51
|
+
* Docs and design previews only: the cue is shown open without taking focus or scrolling, because nobody started it.
|
|
52
|
+
* In a product a guide opens only from something the person pressed; never set this there.
|
|
53
|
+
*/
|
|
54
|
+
preview?: boolean
|
|
55
|
+
className?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const GUIDE_MAX_STEPS = 3
|
|
59
|
+
|
|
60
|
+
/* ---------------------------------------------------------------- helpers */
|
|
61
|
+
|
|
62
|
+
function resolve(target: GuideTarget): HTMLElement | null {
|
|
63
|
+
if (typeof document === "undefined") return null
|
|
64
|
+
if (typeof target === "string") return document.querySelector<HTMLElement>(target)
|
|
65
|
+
if (typeof target === "function") return target() ?? null
|
|
66
|
+
return target.current
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Present, laid out and not hidden from people: a cue never points at nothing. */
|
|
70
|
+
export function isGuideTargetAvailable(el: HTMLElement | null): el is HTMLElement {
|
|
71
|
+
if (!el || !el.isConnected) return false
|
|
72
|
+
if (el.getClientRects().length === 0) return false
|
|
73
|
+
if (el.closest("[hidden],[inert],[aria-hidden='true']")) return false
|
|
74
|
+
const style = window.getComputedStyle(el)
|
|
75
|
+
return style.visibility !== "hidden"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const reducedMotion = () => typeof window !== "undefined" && !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
|
|
79
|
+
|
|
80
|
+
/** The focus ring drawn over the target, following it on scroll and resize. Never takes pointer events. */
|
|
81
|
+
function TargetRing({ el }: { el: HTMLElement }) {
|
|
82
|
+
const [rect, setRect] = React.useState<DOMRect | null>(null)
|
|
83
|
+
React.useLayoutEffect(() => {
|
|
84
|
+
let frame = 0
|
|
85
|
+
const update = () => {
|
|
86
|
+
cancelAnimationFrame(frame)
|
|
87
|
+
frame = requestAnimationFrame(() => setRect(el.getBoundingClientRect()))
|
|
88
|
+
}
|
|
89
|
+
setRect(el.getBoundingClientRect())
|
|
90
|
+
const ro = new ResizeObserver(update)
|
|
91
|
+
ro.observe(el)
|
|
92
|
+
window.addEventListener("scroll", update, true)
|
|
93
|
+
window.addEventListener("resize", update)
|
|
94
|
+
return () => {
|
|
95
|
+
cancelAnimationFrame(frame)
|
|
96
|
+
ro.disconnect()
|
|
97
|
+
window.removeEventListener("scroll", update, true)
|
|
98
|
+
window.removeEventListener("resize", update)
|
|
99
|
+
}
|
|
100
|
+
}, [el])
|
|
101
|
+
if (!rect) return null
|
|
102
|
+
const radius = window.getComputedStyle(el).borderRadius
|
|
103
|
+
return createPortal(
|
|
104
|
+
<span
|
|
105
|
+
aria-hidden="true"
|
|
106
|
+
data-slot="guide-cue-ring"
|
|
107
|
+
className="pointer-events-none fixed top-(--gy) left-(--gx) z-40 h-(--gh) w-(--gw) rounded-(--gr) outline-2 outline-offset-2 outline-ring"
|
|
108
|
+
style={{ "--gx": `${rect.left}px`, "--gy": `${rect.top}px`, "--gw": `${rect.width}px`, "--gh": `${rect.height}px`, "--gr": radius || "0px" } as React.CSSProperties}
|
|
109
|
+
/>,
|
|
110
|
+
document.body,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ---------------------------------------------------------------- component */
|
|
115
|
+
|
|
116
|
+
export function GuideCue({ steps, open, onOpenChange, onFinish, label = "Guide", preview = false, className }: GuideCueProps) {
|
|
117
|
+
const id = React.useId()
|
|
118
|
+
const list = steps.slice(0, GUIDE_MAX_STEPS)
|
|
119
|
+
|
|
120
|
+
// Resolved once per opening: the steps whose target is there, with their elements.
|
|
121
|
+
const [live, setLive] = React.useState<{ step: GuideStep; el: HTMLElement }[]>([])
|
|
122
|
+
const [index, setIndex] = React.useState(0)
|
|
123
|
+
const [announce, setAnnounce] = React.useState("")
|
|
124
|
+
const returnTo = React.useRef<HTMLElement | null>(null)
|
|
125
|
+
const headingRef = React.useRef<HTMLHeadingElement>(null)
|
|
126
|
+
const nextRef = React.useRef<HTMLButtonElement>(null)
|
|
127
|
+
const moved = React.useRef(false)
|
|
128
|
+
|
|
129
|
+
const stepsKey = list.map((s) => s.title).join("|")
|
|
130
|
+
React.useLayoutEffect(() => {
|
|
131
|
+
if (!open) {
|
|
132
|
+
setLive([])
|
|
133
|
+
// Back to what started the guide, unless the person has already moved focus somewhere else on the page.
|
|
134
|
+
const back = returnTo.current
|
|
135
|
+
returnTo.current = null
|
|
136
|
+
if (back?.isConnected) {
|
|
137
|
+
requestAnimationFrame(() => {
|
|
138
|
+
const now = document.activeElement
|
|
139
|
+
if (!now || now === document.body) back.focus()
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
// Remember what started the guide before anything moves focus. A preview was started by nobody.
|
|
145
|
+
returnTo.current = !preview && document.activeElement instanceof HTMLElement ? document.activeElement : null
|
|
146
|
+
const found = list.flatMap((step) => { const el = resolve(step.target); return isGuideTargetAvailable(el) ? [{ step, el }] : [] })
|
|
147
|
+
if (found.length === 0) {
|
|
148
|
+
console.warn("GuideCue: no step has a visible target, so the guide did not open.")
|
|
149
|
+
onOpenChange(false)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
setLive(found)
|
|
153
|
+
setIndex(0)
|
|
154
|
+
setAnnounce("")
|
|
155
|
+
moved.current = false
|
|
156
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
157
|
+
}, [open, stepsKey])
|
|
158
|
+
|
|
159
|
+
const current = live[index]
|
|
160
|
+
const total = live.length
|
|
161
|
+
const last = index === total - 1
|
|
162
|
+
|
|
163
|
+
// Keep the target in view; respect reduced motion.
|
|
164
|
+
React.useEffect(() => {
|
|
165
|
+
if (!open || !current) return
|
|
166
|
+
// A preview opens in place: no scrolling until the person moves through the steps.
|
|
167
|
+
if (!preview || moved.current) current.el.scrollIntoView({ block: "nearest", inline: "nearest", behavior: reducedMotion() ? "auto" : "smooth" })
|
|
168
|
+
// Announce step changes only; on open, focus on the heading already reads the first step.
|
|
169
|
+
if (moved.current) setAnnounce(`${label}, step ${index + 1} of ${total}: ${current.step.title}`)
|
|
170
|
+
// On a step change, keep focus inside the cue: if the pressed button went away (Back on step 1), go to Next.
|
|
171
|
+
if (moved.current) {
|
|
172
|
+
requestAnimationFrame(() => {
|
|
173
|
+
const content = headingRef.current?.closest("[data-slot='guide-cue']")
|
|
174
|
+
if (!content?.contains(document.activeElement)) nextRef.current?.focus()
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
}, [open, current, index, total, label])
|
|
178
|
+
|
|
179
|
+
// A guide given more than three steps keeps the first three: split it, or write a help article instead.
|
|
180
|
+
React.useEffect(() => {
|
|
181
|
+
if (steps.length > GUIDE_MAX_STEPS) console.warn(`GuideCue: ${steps.length} steps given; only the first ${GUIDE_MAX_STEPS} are shown.`)
|
|
182
|
+
}, [steps.length])
|
|
183
|
+
|
|
184
|
+
const close = React.useCallback(() => onOpenChange(false), [onOpenChange])
|
|
185
|
+
const go = (to: number) => { moved.current = true; setIndex(to) }
|
|
186
|
+
|
|
187
|
+
if (!open || !current) return null
|
|
188
|
+
const { step, el } = current
|
|
189
|
+
const virtualRef = { current: el }
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<>
|
|
193
|
+
<TargetRing el={el} />
|
|
194
|
+
<Popover open onOpenChange={(o) => { if (!o) close() }}>
|
|
195
|
+
<PopoverAnchor virtualRef={virtualRef} />
|
|
196
|
+
<PopoverContent
|
|
197
|
+
data-slot="guide-cue"
|
|
198
|
+
side={step.side ?? "bottom"}
|
|
199
|
+
align={step.align ?? "start"}
|
|
200
|
+
sideOffset={8}
|
|
201
|
+
updatePositionStrategy="always"
|
|
202
|
+
aria-modal="false"
|
|
203
|
+
aria-labelledby={`${id}-title`}
|
|
204
|
+
aria-describedby={`${id}-body`}
|
|
205
|
+
// Focus goes to the heading, so the title is read first; it returns to what started the guide.
|
|
206
|
+
onOpenAutoFocus={(e) => { e.preventDefault(); if (!preview) headingRef.current?.focus() }}
|
|
207
|
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
208
|
+
// Not modal: people can try the highlighted control without the guide closing.
|
|
209
|
+
onInteractOutside={(e) => e.preventDefault()}
|
|
210
|
+
className={cn("grid w-72 max-w-(--radix-popover-content-available-width) gap-2 text-base", className)}
|
|
211
|
+
>
|
|
212
|
+
<div className="flex items-start gap-2">
|
|
213
|
+
<h2 ref={headingRef} id={`${id}-title`} tabIndex={-1} className="min-w-0 flex-1 pt-0.5 text-base font-semibold text-ink outline-none">
|
|
214
|
+
{step.title}
|
|
215
|
+
</h2>
|
|
216
|
+
<Tooltip>
|
|
217
|
+
<TooltipTrigger asChild>
|
|
218
|
+
<Button variant="ghost" size="icon-xs" aria-label="Close guide" onClick={close} className="-mt-0.5 -mr-1 text-ink-3">
|
|
219
|
+
<X aria-hidden="true" />
|
|
220
|
+
</Button>
|
|
221
|
+
</TooltipTrigger>
|
|
222
|
+
<TooltipContent>Close guide <Kbd>Esc</Kbd></TooltipContent>
|
|
223
|
+
</Tooltip>
|
|
224
|
+
</div>
|
|
225
|
+
<div id={`${id}-body`} className="text-ink-2">{step.body}</div>
|
|
226
|
+
<div className="flex flex-wrap items-center gap-2 pt-1">
|
|
227
|
+
{total > 1 && <span className="tabular flex-1 text-sm text-ink-2">Step {index + 1} of {total}</span>}
|
|
228
|
+
{total === 1 && <span className="flex-1" />}
|
|
229
|
+
{index > 0 && <Button variant="outline" size="sm" onClick={() => go(index - 1)}>Back</Button>}
|
|
230
|
+
<Button
|
|
231
|
+
ref={nextRef}
|
|
232
|
+
size="sm"
|
|
233
|
+
onClick={() => { if (last) { onFinish?.(); close() } else go(index + 1) }}
|
|
234
|
+
>
|
|
235
|
+
{last ? "Done" : "Next"}
|
|
236
|
+
</Button>
|
|
237
|
+
</div>
|
|
238
|
+
<span className="sr-only" aria-live="polite" aria-atomic="true">{announce}</span>
|
|
239
|
+
</PopoverContent>
|
|
240
|
+
</Popover>
|
|
241
|
+
</>
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* ---------------------------------------------------------------- hook */
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* State for one guide. `start` is what a "Show me" or Start button calls; spread `cueProps` on GuideCue.
|
|
249
|
+
* `finished` turns true once Done is pressed on the last step, so the starting point can say so.
|
|
250
|
+
*/
|
|
251
|
+
export function useGuide(steps: GuideStep[], options: { onFinish?: () => void; label?: string; /** Docs and design previews only: start open, without taking focus. See GuideCue `preview`. */ preview?: boolean } = {}) {
|
|
252
|
+
const [open, setOpen] = React.useState(!!options.preview)
|
|
253
|
+
// Quiet only for the first, unrequested opening; once the person starts or closes it, it behaves normally.
|
|
254
|
+
const [quiet, setQuiet] = React.useState(!!options.preview)
|
|
255
|
+
const [finished, setFinished] = React.useState(false)
|
|
256
|
+
const start = React.useCallback(() => { setQuiet(false); setOpen(true) }, [])
|
|
257
|
+
// A guide given more than three steps keeps the first three: split it, or write a help article instead.
|
|
258
|
+
React.useEffect(() => {
|
|
259
|
+
if (steps.length > GUIDE_MAX_STEPS) console.warn(`GuideCue: ${steps.length} steps given; only the first ${GUIDE_MAX_STEPS} are shown.`)
|
|
260
|
+
}, [steps.length])
|
|
261
|
+
|
|
262
|
+
const close = React.useCallback(() => { setQuiet(false); setOpen(false) }, [])
|
|
263
|
+
const { onFinish, label } = options
|
|
264
|
+
const cueProps: GuideCueProps = {
|
|
265
|
+
steps,
|
|
266
|
+
open,
|
|
267
|
+
onOpenChange: (o) => { if (!o) setQuiet(false); setOpen(o) },
|
|
268
|
+
preview: quiet,
|
|
269
|
+
onFinish: () => { setFinished(true); onFinish?.() },
|
|
270
|
+
label,
|
|
271
|
+
}
|
|
272
|
+
return { open, finished, start, close, cueProps }
|
|
273
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TwoUX composition: the one place for things that need the person. List and detail side by side,
|
|
3
|
+
* Inbox / Snoozed / Done tabs with counts, items that block an agent first ("Needs you"), J/K to move,
|
|
4
|
+
* E to mark done, ⇧H to snooze. Acting never leaves the Inbox; the next item opens after Done.
|
|
5
|
+
*/
|
|
6
|
+
import { Check, Clock } from "lucide-react"
|
|
7
|
+
import * as React from "react"
|
|
8
|
+
import { cn } from "../lib/cn"
|
|
9
|
+
import { Badge } from "./badge"
|
|
10
|
+
import { Button } from "./button"
|
|
11
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "./dropdown-menu"
|
|
12
|
+
import { Tabs, TabsList, TabsTrigger } from "./tabs"
|
|
13
|
+
|
|
14
|
+
export type InboxItem = { id: string; title: string; source: string; time: string; needsYou?: boolean; state: "inbox" | "snoozed" | "done"; snoozedUntil?: string }
|
|
15
|
+
export const SNOOZE_TIMES = ["In an hour", "Tomorrow, 9:00", "Next week", "In a month"] as const
|
|
16
|
+
|
|
17
|
+
export function Inbox({
|
|
18
|
+
items,
|
|
19
|
+
onDone,
|
|
20
|
+
onSnooze,
|
|
21
|
+
renderDetail,
|
|
22
|
+
className,
|
|
23
|
+
}: {
|
|
24
|
+
items: InboxItem[]
|
|
25
|
+
onDone: (id: string) => void
|
|
26
|
+
onSnooze: (id: string, until: string) => void
|
|
27
|
+
renderDetail: (item: InboxItem) => React.ReactNode
|
|
28
|
+
className?: string
|
|
29
|
+
}) {
|
|
30
|
+
const [tab, setTab] = React.useState<InboxItem["state"]>("inbox")
|
|
31
|
+
const list = items.filter((i) => i.state === tab).sort((a, b) => Number(!!b.needsYou) - Number(!!a.needsYou))
|
|
32
|
+
const [activeId, setActiveId] = React.useState<string | null>(list[0]?.id ?? null)
|
|
33
|
+
const [snoozeOpen, setSnoozeOpen] = React.useState(false)
|
|
34
|
+
const active = list.find((i) => i.id === activeId) ?? list[0] ?? null
|
|
35
|
+
const idx = active ? list.indexOf(active) : -1
|
|
36
|
+
const count = (s: InboxItem["state"]) => items.filter((i) => i.state === s).length
|
|
37
|
+
const listRef = React.useRef<HTMLUListElement>(null)
|
|
38
|
+
|
|
39
|
+
const next = () => list[idx + 1]?.id ?? list[idx - 1]?.id ?? null
|
|
40
|
+
const done = () => { if (!active) return; const n = next(); onDone(active.id); setActiveId(n) }
|
|
41
|
+
const snooze = (until: string) => { if (!active) return; const n = next(); onSnooze(active.id, until); setActiveId(n); setSnoozeOpen(false) }
|
|
42
|
+
|
|
43
|
+
const onKey = (e: React.KeyboardEvent) => {
|
|
44
|
+
if (e.metaKey || e.ctrlKey || e.altKey || (e.target as HTMLElement).closest("input, textarea")) return
|
|
45
|
+
const k = e.key.toLowerCase()
|
|
46
|
+
if (k === "j" || e.key === "ArrowDown") { e.preventDefault(); setActiveId(list[Math.min(list.length - 1, idx + 1)]?.id ?? null) }
|
|
47
|
+
else if (k === "k" || e.key === "ArrowUp") { e.preventDefault(); setActiveId(list[Math.max(0, idx - 1)]?.id ?? null) }
|
|
48
|
+
else if (k === "e" && tab !== "done") { e.preventDefault(); done() }
|
|
49
|
+
else if (k === "h" && e.shiftKey && tab === "inbox") { e.preventDefault(); setSnoozeOpen(true) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div data-slot="inbox" className={cn("grid min-h-0 grid-cols-[minmax(0,320px)_minmax(0,1fr)] overflow-hidden rounded-sm border border-line bg-surface max-md:grid-cols-1", className)}>
|
|
54
|
+
<div className="flex min-h-0 flex-col border-r border-line max-md:border-r-0 max-md:border-b">
|
|
55
|
+
<Tabs value={tab} onValueChange={(v) => { setTab(v as InboxItem["state"]); setActiveId(null) }} className="px-3 pt-2">
|
|
56
|
+
<TabsList aria-label="Inbox views">
|
|
57
|
+
<TabsTrigger value="inbox" count={count("inbox")}>Inbox</TabsTrigger>
|
|
58
|
+
<TabsTrigger value="snoozed" count={count("snoozed")}>Snoozed</TabsTrigger>
|
|
59
|
+
<TabsTrigger value="done" count={count("done")}>Done</TabsTrigger>
|
|
60
|
+
</TabsList>
|
|
61
|
+
</Tabs>
|
|
62
|
+
{list.length === 0 ? (
|
|
63
|
+
<p className="px-4 py-8 text-center text-base text-ink-2">{tab === "inbox" ? "You're all caught up." : tab === "snoozed" ? "Nothing snoozed." : "Nothing done yet."}</p>
|
|
64
|
+
) : (
|
|
65
|
+
<ul ref={listRef} role="listbox" aria-label="Items" aria-activedescendant={active ? `inbox-${active.id}` : undefined} tabIndex={0} onKeyDown={onKey} className="min-h-0 flex-1 overflow-y-auto py-1 outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset">
|
|
66
|
+
{list.map((it) => (
|
|
67
|
+
<li
|
|
68
|
+
key={it.id}
|
|
69
|
+
id={`inbox-${it.id}`}
|
|
70
|
+
role="option"
|
|
71
|
+
aria-selected={active?.id === it.id}
|
|
72
|
+
onClick={() => setActiveId(it.id)}
|
|
73
|
+
className={cn("mx-1 grid cursor-default gap-0.5 rounded-xs px-2 py-2", active?.id === it.id ? "bg-selected" : "hover:bg-hover")}
|
|
74
|
+
>
|
|
75
|
+
<span className="flex items-center gap-2">
|
|
76
|
+
<span className="min-w-0 flex-1 truncate text-base font-medium text-ink">{it.title}</span>
|
|
77
|
+
<span className="tabular shrink-0 text-sm text-ink-3">{it.state === "snoozed" && it.snoozedUntil ? it.snoozedUntil : it.time}</span>
|
|
78
|
+
</span>
|
|
79
|
+
<span className="flex items-center gap-2">
|
|
80
|
+
<span className="min-w-0 flex-1 truncate text-sm text-ink-2">{it.source}</span>
|
|
81
|
+
{it.needsYou && it.state === "inbox" && <Badge variant="warning" dot>Needs you</Badge>}
|
|
82
|
+
</span>
|
|
83
|
+
</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
)}
|
|
87
|
+
<p className="border-t border-line px-3 py-2 text-xs text-ink-3">J K to move · E done · ⇧H snooze</p>
|
|
88
|
+
</div>
|
|
89
|
+
<div className="flex min-h-0 flex-col">
|
|
90
|
+
{active ? (
|
|
91
|
+
<>
|
|
92
|
+
<div className="flex items-start gap-2 border-b border-line px-4 py-2">
|
|
93
|
+
<h2 className="min-w-0 flex-1 pt-1 text-md font-semibold text-ink">{active.title}</h2>
|
|
94
|
+
{tab === "inbox" && (
|
|
95
|
+
<DropdownMenu open={snoozeOpen} onOpenChange={setSnoozeOpen}>
|
|
96
|
+
<DropdownMenuTrigger asChild><Button variant="ghost" size="sm" shortcut="⇧H"><Clock />Snooze</Button></DropdownMenuTrigger>
|
|
97
|
+
<DropdownMenuContent align="end">
|
|
98
|
+
<DropdownMenuLabel>Snooze until</DropdownMenuLabel>
|
|
99
|
+
{SNOOZE_TIMES.map((t) => <DropdownMenuItem key={t} onSelect={() => snooze(t)}>{t}</DropdownMenuItem>)}
|
|
100
|
+
</DropdownMenuContent>
|
|
101
|
+
</DropdownMenu>
|
|
102
|
+
)}
|
|
103
|
+
{tab !== "done" && <Button variant="outline" size="sm" shortcut="E" onClick={done}><Check />Done</Button>}
|
|
104
|
+
</div>
|
|
105
|
+
<div className="min-h-0 flex-1 overflow-y-auto p-4">{renderDetail(active)}</div>
|
|
106
|
+
</>
|
|
107
|
+
) : (
|
|
108
|
+
<div className="grid flex-1 place-items-center p-8 text-base text-ink-3">Select an item to see it here.</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated from shadcn/ui new-york-v4 "input-group" by scripts/tailor-shadcn.mjs. Do not edit by hand.
|
|
3
|
+
* TwoUX changes:
|
|
4
|
+
* - Compact 32px
|
|
5
|
+
* - Light edge darkens on hover
|
|
6
|
+
*/
|
|
7
|
+
"use client"
|
|
8
|
+
|
|
9
|
+
import * as React from "react"
|
|
10
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
11
|
+
import { cn } from "../lib/cn"
|
|
12
|
+
|
|
13
|
+
import { Button } from "./button"
|
|
14
|
+
import { Input } from "./input"
|
|
15
|
+
import { Textarea } from "./textarea"
|
|
16
|
+
|
|
17
|
+
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
data-slot="input-group"
|
|
21
|
+
role="group"
|
|
22
|
+
className={cn(
|
|
23
|
+
"group/input-group relative flex w-full items-center rounded-sm border border-input bg-background shadow-xs hover:border-input-hover transition-[color,background-color,border-color,box-shadow] duration-(--duration-fast) ease-(--ease-out) outline-none",
|
|
24
|
+
"h-8 min-w-0 has-[>textarea]:h-auto",
|
|
25
|
+
|
|
26
|
+
// Variants based on alignment.
|
|
27
|
+
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
|
28
|
+
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
|
29
|
+
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
|
30
|
+
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
|
31
|
+
|
|
32
|
+
// Focus state.
|
|
33
|
+
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
|
|
34
|
+
|
|
35
|
+
// Error state.
|
|
36
|
+
"has-[[data-slot][aria-invalid=true]:not(:focus-visible)]:border-destructive-edge",
|
|
37
|
+
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const inputGroupAddonVariants = cva(
|
|
46
|
+
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-base font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-xs [&>svg:not([class*='size-'])]:size-4",
|
|
47
|
+
{
|
|
48
|
+
variants: {
|
|
49
|
+
align: {
|
|
50
|
+
"inline-start":
|
|
51
|
+
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
|
52
|
+
"inline-end":
|
|
53
|
+
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
|
54
|
+
"block-start":
|
|
55
|
+
"order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
|
|
56
|
+
"block-end":
|
|
57
|
+
"order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
defaultVariants: {
|
|
61
|
+
align: "inline-start",
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
function InputGroupAddon({
|
|
67
|
+
className,
|
|
68
|
+
align = "inline-start",
|
|
69
|
+
...props
|
|
70
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
role="group"
|
|
74
|
+
data-slot="input-group-addon"
|
|
75
|
+
data-align={align}
|
|
76
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
77
|
+
onClick={(e) => {
|
|
78
|
+
if ((e.target as HTMLElement).closest("button")) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
|
82
|
+
}}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const inputGroupButtonVariants = cva(
|
|
89
|
+
"flex items-center gap-2 text-base shadow-none",
|
|
90
|
+
{
|
|
91
|
+
variants: {
|
|
92
|
+
size: {
|
|
93
|
+
xs: "h-6 gap-1 rounded-xs px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
|
94
|
+
sm: "h-8 gap-1.5 rounded-sm px-2.5 has-[>svg]:px-2.5",
|
|
95
|
+
"icon-xs":
|
|
96
|
+
"size-6 rounded-xs p-0 has-[>svg]:p-0",
|
|
97
|
+
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
defaultVariants: {
|
|
101
|
+
size: "xs",
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
function InputGroupButton({
|
|
107
|
+
className,
|
|
108
|
+
type = "button",
|
|
109
|
+
variant = "ghost",
|
|
110
|
+
size = "xs",
|
|
111
|
+
...props
|
|
112
|
+
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
|
113
|
+
VariantProps<typeof inputGroupButtonVariants>) {
|
|
114
|
+
return (
|
|
115
|
+
<Button
|
|
116
|
+
type={type}
|
|
117
|
+
data-size={size}
|
|
118
|
+
variant={variant}
|
|
119
|
+
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
120
|
+
{...props}
|
|
121
|
+
/>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|
126
|
+
return (
|
|
127
|
+
<span
|
|
128
|
+
className={cn(
|
|
129
|
+
"flex items-center gap-2 text-base text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
130
|
+
className
|
|
131
|
+
)}
|
|
132
|
+
{...props}
|
|
133
|
+
/>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function InputGroupInput({
|
|
138
|
+
className,
|
|
139
|
+
...props
|
|
140
|
+
}: React.ComponentProps<"input">) {
|
|
141
|
+
return (
|
|
142
|
+
<Input
|
|
143
|
+
data-slot="input-group-control"
|
|
144
|
+
className={cn(
|
|
145
|
+
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0",
|
|
146
|
+
className
|
|
147
|
+
)}
|
|
148
|
+
{...props}
|
|
149
|
+
/>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function InputGroupTextarea({
|
|
154
|
+
className,
|
|
155
|
+
...props
|
|
156
|
+
}: React.ComponentProps<"textarea">) {
|
|
157
|
+
return (
|
|
158
|
+
<Textarea
|
|
159
|
+
data-slot="input-group-control"
|
|
160
|
+
className={cn(
|
|
161
|
+
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0",
|
|
162
|
+
className
|
|
163
|
+
)}
|
|
164
|
+
{...props}
|
|
165
|
+
/>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export {
|
|
170
|
+
InputGroup,
|
|
171
|
+
InputGroupAddon,
|
|
172
|
+
InputGroupButton,
|
|
173
|
+
InputGroupText,
|
|
174
|
+
InputGroupInput,
|
|
175
|
+
InputGroupTextarea,
|
|
176
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated from shadcn/ui new-york-v4 "input" by scripts/tailor-shadcn.mjs. Do not edit by hand.
|
|
3
|
+
* TwoUX changes:
|
|
4
|
+
* - Compact 32px, light edge that darkens on hover
|
|
5
|
+
* - One size on every screen
|
|
6
|
+
*/
|
|
7
|
+
import * as React from "react"
|
|
8
|
+
import { cn } from "../lib/cn"
|
|
9
|
+
|
|
10
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
11
|
+
return (
|
|
12
|
+
<input
|
|
13
|
+
type={type}
|
|
14
|
+
data-slot="input"
|
|
15
|
+
className={cn(
|
|
16
|
+
"h-8 w-full min-w-0 rounded-sm border border-input bg-background px-2.5 py-1 text-base shadow-xs hover:border-input-hover transition-[color,background-color,border-color,box-shadow] duration-(--duration-fast) ease-(--ease-out) outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-base file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
17
|
+
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
|
18
|
+
"aria-invalid:not-focus-visible:border-destructive-edge",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { Input }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated from shadcn/ui new-york-v4 "kbd" by scripts/tailor-shadcn.mjs. Do not edit by hand.
|
|
3
|
+
* TwoUX changes:
|
|
4
|
+
* - Inside a primary or destructive button, the key sits on a darker tint so white text keeps 4.5:1
|
|
5
|
+
*/
|
|
6
|
+
import { cn } from "../lib/cn"
|
|
7
|
+
|
|
8
|
+
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
|
9
|
+
return (
|
|
10
|
+
<kbd
|
|
11
|
+
data-slot="kbd"
|
|
12
|
+
className={cn(
|
|
13
|
+
"pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-xs bg-muted px-1 font-sans text-sm font-medium text-muted-foreground select-none",
|
|
14
|
+
"[&_svg:not([class*='size-'])]:size-3",
|
|
15
|
+
"[[data-slot=button][data-variant=default]_&]:bg-black/20 [[data-slot=button][data-variant=default]_&]:text-primary-foreground [[data-slot=button][data-variant=destructive]_&]:bg-black/20 [[data-slot=button][data-variant=destructive]_&]:text-white",
|
|
16
|
+
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background",
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
25
|
+
return (
|
|
26
|
+
<kbd
|
|
27
|
+
data-slot="kbd-group"
|
|
28
|
+
className={cn("inline-flex items-center gap-1", className)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { Kbd, KbdGroup }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated from shadcn/ui new-york-v4 "label" by scripts/tailor-shadcn.mjs. Do not edit by hand.
|
|
3
|
+
* TwoUX changes: shared changes only (imports, role tokens for light and dark, type scale).
|
|
4
|
+
*/
|
|
5
|
+
"use client"
|
|
6
|
+
|
|
7
|
+
import * as React from "react"
|
|
8
|
+
import { cn } from "../lib/cn"
|
|
9
|
+
import { Label as LabelPrimitive } from "radix-ui"
|
|
10
|
+
|
|
11
|
+
function Label({
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
15
|
+
return (
|
|
16
|
+
<LabelPrimitive.Root
|
|
17
|
+
data-slot="label"
|
|
18
|
+
className={cn(
|
|
19
|
+
"flex items-center gap-2 text-base leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { Label }
|