@treelocator/runtime 0.1.8 → 0.3.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 (58) hide show
  1. package/dist/adapters/createTreeNode.js +41 -4
  2. package/dist/adapters/nextjs/parseNextjsDataAttributes.d.ts +31 -0
  3. package/dist/adapters/nextjs/parseNextjsDataAttributes.js +106 -0
  4. package/dist/adapters/phoenix/__tests__/parsePhoenixComments.test.d.ts +4 -0
  5. package/dist/adapters/phoenix/__tests__/parsePhoenixComments.test.js +218 -0
  6. package/dist/adapters/phoenix/detectPhoenix.d.ts +11 -0
  7. package/dist/adapters/phoenix/detectPhoenix.js +38 -0
  8. package/dist/adapters/phoenix/index.d.ts +10 -0
  9. package/dist/adapters/phoenix/index.js +9 -0
  10. package/dist/adapters/phoenix/parsePhoenixComments.d.ts +35 -0
  11. package/dist/adapters/phoenix/parsePhoenixComments.js +131 -0
  12. package/dist/adapters/phoenix/types.d.ts +16 -0
  13. package/dist/adapters/phoenix/types.js +1 -0
  14. package/dist/adapters/react/findDebugSource.d.ts +13 -0
  15. package/dist/adapters/react/findDebugSource.js +37 -0
  16. package/dist/adapters/react/findFiberByHtmlElement.js +23 -1
  17. package/dist/adapters/react/getFiberLabel.js +2 -1
  18. package/dist/adapters/react/resolveSourceMap.d.ts +29 -0
  19. package/dist/adapters/react/resolveSourceMap.js +236 -0
  20. package/dist/browserApi.d.ts +4 -4
  21. package/dist/browserApi.js +13 -15
  22. package/dist/components/MaybeOutline.js +65 -3
  23. package/dist/components/Runtime.js +13 -0
  24. package/dist/functions/enrichAncestrySourceMaps.d.ts +7 -0
  25. package/dist/functions/enrichAncestrySourceMaps.js +80 -0
  26. package/dist/functions/formatAncestryChain.d.ts +3 -0
  27. package/dist/functions/formatAncestryChain.js +104 -15
  28. package/dist/functions/formatAncestryChain.test.js +26 -20
  29. package/dist/functions/normalizeFilePath.d.ts +14 -0
  30. package/dist/functions/normalizeFilePath.js +40 -0
  31. package/dist/output.css +87 -15
  32. package/dist/types/ServerComponentInfo.d.ts +14 -0
  33. package/dist/types/ServerComponentInfo.js +1 -0
  34. package/package.json +4 -3
  35. package/src/adapters/createTreeNode.ts +44 -3
  36. package/src/adapters/nextjs/parseNextjsDataAttributes.ts +112 -0
  37. package/src/adapters/phoenix/__tests__/parsePhoenixComments.test.ts +264 -0
  38. package/src/adapters/phoenix/detectPhoenix.ts +44 -0
  39. package/src/adapters/phoenix/index.ts +11 -0
  40. package/src/adapters/phoenix/parsePhoenixComments.ts +140 -0
  41. package/src/adapters/phoenix/types.ts +16 -0
  42. package/src/adapters/react/findDebugSource.ts +40 -0
  43. package/src/adapters/react/findFiberByHtmlElement.ts +26 -1
  44. package/src/adapters/react/getFiberLabel.ts +2 -1
  45. package/src/adapters/react/reactAdapter.ts +2 -1
  46. package/src/adapters/react/resolveSourceMap.ts +316 -0
  47. package/src/browserApi.ts +27 -25
  48. package/src/components/MaybeOutline.tsx +63 -4
  49. package/src/components/Runtime.tsx +15 -0
  50. package/src/functions/enrichAncestrySourceMaps.ts +103 -0
  51. package/src/functions/formatAncestryChain.test.ts +26 -20
  52. package/src/functions/formatAncestryChain.ts +121 -15
  53. package/src/functions/normalizeFilePath.ts +41 -0
  54. package/src/types/ServerComponentInfo.ts +14 -0
  55. package/.turbo/turbo-build.log +0 -30
  56. package/.turbo/turbo-test.log +0 -19
  57. package/.turbo/turbo-ts.log +0 -4
  58. package/LICENSE +0 -22
@@ -0,0 +1,80 @@
1
+ import { resolveSourceLocation, parseDebugStack } from "../adapters/react/resolveSourceMap";
2
+ import { normalizeFilePath } from "./normalizeFilePath";
3
+
4
+ /**
5
+ * Check if any DOM element has React 19 fibers (with _debugStack instead of _debugSource).
6
+ */
7
+ function isReact19Environment() {
8
+ const el = document.querySelector("[class]") || document.body;
9
+ if (!el) return false;
10
+ const fiberKey = Object.keys(el).find(k => k.startsWith("__reactFiber$"));
11
+ if (!fiberKey) return false;
12
+ const fiber = el[fiberKey];
13
+ return !fiber?._debugSource && !!fiber?._debugStack;
14
+ }
15
+
16
+ /**
17
+ * Walk a fiber's _debugOwner chain and collect _debugStack stack traces for each component.
18
+ * Returns a map from component name to its parsed stack location.
19
+ */
20
+ function collectFiberStacks(element) {
21
+ const stacks = new Map();
22
+ const fiberKey = Object.keys(element).find(k => k.startsWith("__reactFiber$"));
23
+ if (!fiberKey) return stacks;
24
+ let fiber = element[fiberKey];
25
+
26
+ // Collect stacks from the fiber itself and its _debugOwner chain
27
+ while (fiber) {
28
+ const debugStack = fiber._debugStack;
29
+ if (debugStack?.stack) {
30
+ const parsed = parseDebugStack(debugStack.stack);
31
+ if (parsed) {
32
+ const name = fiber.type?.name || fiber.type?.displayName || fiber.type;
33
+ if (typeof name === "string") {
34
+ stacks.set(name, parsed);
35
+ }
36
+ }
37
+ }
38
+ fiber = fiber._debugOwner || null;
39
+ }
40
+ return stacks;
41
+ }
42
+
43
+ /**
44
+ * Enrich ancestry items that are missing filePath by resolving via source maps.
45
+ * This is an async operation that fetches source maps for React 19 environments.
46
+ * For React 18 (where _debugSource exists), this is a no-op.
47
+ */
48
+ export async function enrichAncestryWithSourceMaps(items, element) {
49
+ // Skip if all items already have file paths, or not React 19
50
+ const needsEnrichment = items.some(item => item.componentName && !item.filePath);
51
+ if (!needsEnrichment || !isReact19Environment()) {
52
+ return items;
53
+ }
54
+
55
+ // Collect _debugStack info from the DOM element's fiber chain
56
+ const stacks = element ? collectFiberStacks(element) : new Map();
57
+
58
+ // Resolve source maps in parallel for items missing filePath
59
+ const enriched = await Promise.all(items.map(async item => {
60
+ if (item.filePath || !item.componentName) return item;
61
+
62
+ // Find the stack trace for this component
63
+ const stack = stacks.get(item.componentName);
64
+ if (!stack) return item;
65
+ try {
66
+ const source = await resolveSourceLocation(stack.url, stack.line, stack.column);
67
+ if (source) {
68
+ return {
69
+ ...item,
70
+ filePath: normalizeFilePath(source.fileName),
71
+ line: source.lineNumber
72
+ };
73
+ }
74
+ } catch {
75
+ // Source map resolution failed — keep item as-is
76
+ }
77
+ return item;
78
+ }));
79
+ return enriched;
80
+ }
@@ -1,4 +1,5 @@
1
1
  import { TreeNode } from "../types/TreeNode";
2
+ import { ServerComponentInfo } from "../types/ServerComponentInfo";
2
3
  export interface OwnerComponentInfo {
3
4
  name: string;
4
5
  filePath?: string;
@@ -13,6 +14,8 @@ export interface AncestryItem {
13
14
  nthChild?: number;
14
15
  /** All owner components from outermost (Sidebar) to innermost (GlassPanel) */
15
16
  ownerComponents?: OwnerComponentInfo[];
17
+ /** Server-side components (Phoenix LiveView, Rails, Next.js RSC, etc.) */
18
+ serverComponents?: ServerComponentInfo[];
16
19
  }
17
20
  export declare function collectAncestry(node: TreeNode): AncestryItem[];
18
21
  export declare function formatAncestryChain(items: AncestryItem[]): string;
@@ -1,3 +1,6 @@
1
+ import { parsePhoenixServerComponents } from "../adapters/phoenix/parsePhoenixComments";
2
+ import { parseNextjsServerComponents } from "../adapters/nextjs/parseNextjsDataAttributes";
3
+ import { normalizeFilePath } from "./normalizeFilePath";
1
4
  // Elements to exclude from ancestry (not useful for debugging)
2
5
  const EXCLUDED_ELEMENTS = new Set(["html", "body", "head"]);
3
6
  function isTreeNodeElement(node) {
@@ -22,7 +25,7 @@ function getNthChildIfAmbiguous(element) {
22
25
  function treeNodeComponentToOwnerInfo(comp) {
23
26
  return {
24
27
  name: comp.label,
25
- filePath: comp.callLink?.fileName,
28
+ filePath: comp.callLink?.fileName ? normalizeFilePath(comp.callLink.fileName) : undefined,
26
29
  line: comp.callLink?.lineNumber
27
30
  };
28
31
  }
@@ -51,6 +54,19 @@ export function collectAncestry(node) {
51
54
  if (nthChild !== undefined) {
52
55
  item.nthChild = nthChild;
53
56
  }
57
+
58
+ // Parse server components from various sources
59
+ // 1. Phoenix LiveView (HTML comments)
60
+ const phoenixComponents = parsePhoenixServerComponents(element);
61
+
62
+ // 2. Next.js Server Components (data-locatorjs attributes)
63
+ const nextjsComponents = parseNextjsServerComponents(element);
64
+
65
+ // Combine all server components
66
+ const allServerComponents = [...(phoenixComponents || []), ...(nextjsComponents || [])];
67
+ if (allServerComponents.length > 0) {
68
+ item.serverComponents = allServerComponents;
69
+ }
54
70
  }
55
71
  }
56
72
 
@@ -62,7 +78,7 @@ export function collectAncestry(node) {
62
78
  // Use outermost component as the primary component name
63
79
  item.componentName = outermost.label;
64
80
  if (outermost.callLink) {
65
- item.filePath = outermost.callLink.fileName;
81
+ item.filePath = normalizeFilePath(outermost.callLink.fileName);
66
82
  item.line = outermost.callLink.lineNumber;
67
83
  }
68
84
  } else {
@@ -71,18 +87,18 @@ export function collectAncestry(node) {
71
87
  if (component) {
72
88
  item.componentName = component.label;
73
89
  if (component.callLink) {
74
- item.filePath = component.callLink.fileName;
90
+ item.filePath = normalizeFilePath(component.callLink.fileName);
75
91
  item.line = component.callLink.lineNumber;
76
92
  }
77
93
  }
78
94
  }
79
95
  if (!item.filePath && source) {
80
- item.filePath = source.fileName;
96
+ item.filePath = normalizeFilePath(source.fileName);
81
97
  item.line = source.lineNumber;
82
98
  }
83
99
 
84
- // Only include items that have useful info (component name or file path)
85
- if (item.componentName || item.filePath) {
100
+ // Only include items that have useful info (component name, file path, or server components)
101
+ if (item.componentName || item.filePath || item.serverComponents) {
86
102
  items.push(item);
87
103
  }
88
104
  current = current.getParent();
@@ -101,8 +117,35 @@ export function formatAncestryChain(items) {
101
117
  const indent = " ".repeat(index);
102
118
  const prefix = index === 0 ? "" : "└─ ";
103
119
 
104
- // Build element selector: elementName:nth-child(n)#id
105
- let selector = item.elementName;
120
+ // Get the previous item's component to detect component boundaries
121
+ const prevItem = index > 0 ? reversed[index - 1] : null;
122
+ const prevComponentName = prevItem?.componentName || prevItem?.ownerComponents?.[prevItem.ownerComponents.length - 1]?.name;
123
+
124
+ // Get current item's innermost component
125
+ const currentComponentName = item.ownerComponents?.[item.ownerComponents.length - 1]?.name || item.componentName;
126
+
127
+ // Determine the display name for the element
128
+ // Use component name ONLY when crossing a component boundary (root element of a component)
129
+ // This prevents "App -> App:nth-child(5)" when both are just elements inside App
130
+ let displayName = item.elementName;
131
+ let outerComponents = [];
132
+ const isComponentBoundary = currentComponentName && currentComponentName !== prevComponentName;
133
+ if (isComponentBoundary) {
134
+ if (item.ownerComponents && item.ownerComponents.length > 0) {
135
+ // Use innermost component as display name, show outer ones in "in X > Y"
136
+ const innermost = item.ownerComponents[item.ownerComponents.length - 1];
137
+ if (innermost) {
138
+ displayName = innermost.name;
139
+ // Outer components (excluding innermost)
140
+ outerComponents = item.ownerComponents.slice(0, -1).map(c => c.name);
141
+ }
142
+ } else if (item.componentName) {
143
+ displayName = item.componentName;
144
+ }
145
+ }
146
+
147
+ // Build element selector: displayName:nth-child(n)#id
148
+ let selector = displayName;
106
149
  if (item.nthChild !== undefined) {
107
150
  selector += `:nth-child(${item.nthChild})`;
108
151
  }
@@ -111,14 +154,60 @@ export function formatAncestryChain(items) {
111
154
  }
112
155
  let description = selector;
113
156
 
114
- // Show all owner components if available (Sidebar > GlassPanel)
115
- if (item.ownerComponents && item.ownerComponents.length > 0) {
116
- const componentChain = item.ownerComponents.map(c => c.name).join(" > ");
117
- description = `${selector} in ${componentChain}`;
118
- } else if (item.componentName) {
119
- description = `${selector} in ${item.componentName}`;
157
+ // Build component description parts
158
+ const parts = [];
159
+
160
+ // Server components (Phoenix/Next.js/Rails/etc.)
161
+ if (item.serverComponents && item.serverComponents.length > 0) {
162
+ // Group server components by framework (detected by file extension)
163
+ const phoenixComponents = item.serverComponents.filter(sc => sc.filePath.match(/\.(ex|exs|heex)$/));
164
+ const nextjsComponents = item.serverComponents.filter(sc => sc.filePath.match(/\.(tsx?|jsx?)$/));
165
+
166
+ // Format Phoenix components
167
+ if (phoenixComponents.length > 0) {
168
+ const names = phoenixComponents.filter(sc => sc.type === "component").map(sc => sc.name);
169
+ if (names.length > 0) {
170
+ parts.push(`[Phoenix: ${names.join(" > ")}]`);
171
+ }
172
+ }
173
+
174
+ // Format Next.js components
175
+ if (nextjsComponents.length > 0) {
176
+ const names = nextjsComponents.filter(sc => sc.type === "component").map(sc => sc.name);
177
+ if (names.length > 0) {
178
+ parts.push(`[Next.js: ${names.join(" > ")}]`);
179
+ }
180
+ }
181
+ }
182
+
183
+ // Client components - show outer components for context (if any)
184
+ if (outerComponents.length > 0) {
185
+ parts.push(`in ${outerComponents.join(" > ")}`);
186
+ }
187
+ if (parts.length > 0) {
188
+ description = `${selector} ${parts.join(" ")}`;
189
+ }
190
+
191
+ // Build location string
192
+ const locationParts = [];
193
+
194
+ // Server component locations
195
+ if (item.serverComponents && item.serverComponents.length > 0) {
196
+ item.serverComponents.forEach(sc => {
197
+ const prefix = sc.type === "caller" ? " (called from)" : "";
198
+ locationParts.push(`${sc.filePath}:${sc.line}${prefix}`);
199
+ });
200
+ }
201
+
202
+ // Client component location (only add if different from server components)
203
+ if (item.filePath) {
204
+ const clientLocation = `${item.filePath}:${item.line}`;
205
+ // Only add if this location isn't already in the list
206
+ if (!locationParts.includes(clientLocation)) {
207
+ locationParts.push(clientLocation);
208
+ }
120
209
  }
121
- const location = item.filePath ? ` at ${item.filePath}:${item.line}` : "";
210
+ const location = locationParts.length > 0 ? ` at ${locationParts.join(", ")}` : "";
122
211
  lines.push(`${indent}${prefix}${description}${location}`);
123
212
  });
124
213
  return lines.join("\n");
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { formatAncestryChain } from "./formatAncestryChain";
3
3
  describe("formatAncestryChain", () => {
4
- it("formats basic ancestry without ID or nth-child", () => {
4
+ it("uses component name only at component boundaries", () => {
5
5
  const items = [{
6
6
  elementName: "button",
7
7
  componentName: "Button"
@@ -10,23 +10,25 @@ describe("formatAncestryChain", () => {
10
10
  componentName: "App"
11
11
  }];
12
12
  const result = formatAncestryChain(items);
13
- expect(result).toBe(`div in App
14
- └─ button in Button`);
13
+ // App is root (component boundary), Button is different component (boundary)
14
+ expect(result).toBe(`App
15
+ └─ Button`);
15
16
  });
16
- it("includes ID when present", () => {
17
+ it("shows element name when same component as parent", () => {
17
18
  const items = [{
18
19
  elementName: "button",
19
- componentName: "Button",
20
+ componentName: "App",
20
21
  id: "submit-btn"
21
22
  }, {
22
23
  elementName: "div",
23
24
  componentName: "App"
24
25
  }];
25
26
  const result = formatAncestryChain(items);
26
- expect(result).toBe(`div in App
27
- └─ button#submit-btn in Button`);
27
+ // Both are in App, so second item shows element name not component name
28
+ expect(result).toBe(`App
29
+ └─ button#submit-btn`);
28
30
  });
29
- it("includes nth-child when present", () => {
31
+ it("includes nth-child with component name at boundary", () => {
30
32
  const items = [{
31
33
  elementName: "li",
32
34
  componentName: "ListItem",
@@ -36,10 +38,11 @@ describe("formatAncestryChain", () => {
36
38
  componentName: "List"
37
39
  }];
38
40
  const result = formatAncestryChain(items);
39
- expect(result).toBe(`ul in List
40
- └─ li:nth-child(3) in ListItem`);
41
+ // Different components = boundaries
42
+ expect(result).toBe(`List
43
+ └─ ListItem:nth-child(3)`);
41
44
  });
42
- it("includes both nth-child and ID when present", () => {
45
+ it("includes both nth-child and ID at component boundary", () => {
43
46
  const items = [{
44
47
  elementName: "li",
45
48
  componentName: "ListItem",
@@ -50,10 +53,10 @@ describe("formatAncestryChain", () => {
50
53
  componentName: "List"
51
54
  }];
52
55
  const result = formatAncestryChain(items);
53
- expect(result).toBe(`ul in List
54
- └─ li:nth-child(2)#special-item in ListItem`);
56
+ expect(result).toBe(`List
57
+ └─ ListItem:nth-child(2)#special-item`);
55
58
  });
56
- it("includes file location when present", () => {
59
+ it("includes file location at component boundary", () => {
57
60
  const items = [{
58
61
  elementName: "button",
59
62
  componentName: "Button",
@@ -62,7 +65,8 @@ describe("formatAncestryChain", () => {
62
65
  line: 42
63
66
  }];
64
67
  const result = formatAncestryChain(items);
65
- expect(result).toBe("button#save in Button at src/Button.tsx:42");
68
+ // First item is always a boundary (no previous item)
69
+ expect(result).toBe("Button#save at src/Button.tsx:42");
66
70
  });
67
71
  it("formats element without component name", () => {
68
72
  const items = [{
@@ -78,7 +82,7 @@ describe("formatAncestryChain", () => {
78
82
  const result = formatAncestryChain([]);
79
83
  expect(result).toBe("");
80
84
  });
81
- it("shows all owner components when ownerComponents is provided", () => {
85
+ it("uses innermost component as display name with outer components in chain", () => {
82
86
  const items = [{
83
87
  elementName: "div",
84
88
  id: "sidebar-panel",
@@ -101,10 +105,11 @@ describe("formatAncestryChain", () => {
101
105
  line: 104
102
106
  }];
103
107
  const result = formatAncestryChain(items);
104
- expect(result).toBe(`div in App at src/App.jsx:104
105
- └─ div#sidebar-panel in Sidebar > GlassPanel at src/components/game/Sidebar.jsx:78`);
108
+ // GlassPanel (innermost) is the display name, Sidebar (outer) shown in "in"
109
+ expect(result).toBe(`App at src/App.jsx:104
110
+ └─ GlassPanel#sidebar-panel in Sidebar at src/components/game/Sidebar.jsx:78`);
106
111
  });
107
- it("shows single owner component without arrow when only one in chain", () => {
112
+ it("uses component name as display name when only one in chain", () => {
108
113
  const items = [{
109
114
  elementName: "button",
110
115
  componentName: "Button",
@@ -117,6 +122,7 @@ describe("formatAncestryChain", () => {
117
122
  line: 10
118
123
  }];
119
124
  const result = formatAncestryChain(items);
120
- expect(result).toBe("button in Button at src/Button.tsx:10");
125
+ // Single component becomes the display name, no "in X" needed
126
+ expect(result).toBe("Button at src/Button.tsx:10");
121
127
  });
122
128
  });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Convert absolute file paths to relative paths for display.
3
+ *
4
+ * This ensures consistent path display across different sources:
5
+ * - React _debugSource may provide absolute paths
6
+ * - Next.js data-locatorjs attributes may have absolute paths
7
+ * - Phoenix comments use relative paths
8
+ *
9
+ * Examples:
10
+ * - "/Users/name/project/src/App.tsx" → "src/App.tsx"
11
+ * - "/workspace/apps/next-16/app/page.tsx" → "app/page.tsx"
12
+ * - "src/components/Button.tsx" → "src/components/Button.tsx" (unchanged)
13
+ */
14
+ export declare function normalizeFilePath(filePath: string): string;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Convert absolute file paths to relative paths for display.
3
+ *
4
+ * This ensures consistent path display across different sources:
5
+ * - React _debugSource may provide absolute paths
6
+ * - Next.js data-locatorjs attributes may have absolute paths
7
+ * - Phoenix comments use relative paths
8
+ *
9
+ * Examples:
10
+ * - "/Users/name/project/src/App.tsx" → "src/App.tsx"
11
+ * - "/workspace/apps/next-16/app/page.tsx" → "app/page.tsx"
12
+ * - "src/components/Button.tsx" → "src/components/Button.tsx" (unchanged)
13
+ */
14
+ export function normalizeFilePath(filePath) {
15
+ // If it's already relative (doesn't start with /), return as-is
16
+ if (!filePath.startsWith("/")) {
17
+ return filePath;
18
+ }
19
+
20
+ // Find common project indicators to trim the path
21
+ const indicators = ["/app/", "/src/", "/pages/", "/components/", "/lib/"];
22
+ for (const indicator of indicators) {
23
+ const index = filePath.indexOf(indicator);
24
+ if (index !== -1) {
25
+ // Return from the indicator onwards (remove leading slash)
26
+ return filePath.substring(index + 1);
27
+ }
28
+ }
29
+
30
+ // If no indicator found, try to get just the last few path segments
31
+ const parts = filePath.split("/");
32
+
33
+ // Return last 3-4 segments if available (e.g., "apps/next-16/app/page.tsx")
34
+ if (parts.length > 3) {
35
+ return parts.slice(-4).join("/");
36
+ }
37
+
38
+ // Last resort: return as-is
39
+ return filePath;
40
+ }
package/dist/output.css CHANGED
@@ -554,7 +554,7 @@ video {
554
554
  display: none;
555
555
  }
556
556
 
557
- [type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
557
+ input:where([type='text']),input:where(:not([type])),input:where([type='email']),input:where([type='url']),input:where([type='password']),input:where([type='number']),input:where([type='date']),input:where([type='datetime-local']),input:where([type='month']),input:where([type='search']),input:where([type='tel']),input:where([type='time']),input:where([type='week']),select:where([multiple]),textarea,select {
558
558
  -webkit-appearance: none;
559
559
  -moz-appearance: none;
560
560
  appearance: none;
@@ -571,7 +571,7 @@ video {
571
571
  --tw-shadow: 0 0 #0000;
572
572
  }
573
573
 
574
- [type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
574
+ input:where([type='text']):focus, input:where(:not([type])):focus, input:where([type='email']):focus, input:where([type='url']):focus, input:where([type='password']):focus, input:where([type='number']):focus, input:where([type='date']):focus, input:where([type='datetime-local']):focus, input:where([type='month']):focus, input:where([type='search']):focus, input:where([type='tel']):focus, input:where([type='time']):focus, input:where([type='week']):focus, select:where([multiple]):focus, textarea:focus, select:focus {
575
575
  outline: 2px solid transparent;
576
576
  outline-offset: 2px;
577
577
  --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
@@ -600,6 +600,11 @@ input::placeholder,textarea::placeholder {
600
600
 
601
601
  ::-webkit-date-and-time-value {
602
602
  min-height: 1.5em;
603
+ text-align: inherit;
604
+ }
605
+
606
+ ::-webkit-datetime-edit {
607
+ display: inline-flex;
603
608
  }
604
609
 
605
610
  ::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
@@ -617,7 +622,7 @@ select {
617
622
  print-color-adjust: exact;
618
623
  }
619
624
 
620
- [multiple],[size]:where(select:not([size="1"])) {
625
+ select:where([multiple]),select:where([size]:not([size="1"])) {
621
626
  background-image: initial;
622
627
  background-position: initial;
623
628
  background-repeat: unset;
@@ -627,7 +632,7 @@ select {
627
632
  print-color-adjust: unset;
628
633
  }
629
634
 
630
- [type='checkbox'],[type='radio'] {
635
+ input:where([type='checkbox']),input:where([type='radio']) {
631
636
  -webkit-appearance: none;
632
637
  -moz-appearance: none;
633
638
  appearance: none;
@@ -650,15 +655,15 @@ select {
650
655
  --tw-shadow: 0 0 #0000;
651
656
  }
652
657
 
653
- [type='checkbox'] {
658
+ input:where([type='checkbox']) {
654
659
  border-radius: 0px;
655
660
  }
656
661
 
657
- [type='radio'] {
662
+ input:where([type='radio']) {
658
663
  border-radius: 100%;
659
664
  }
660
665
 
661
- [type='checkbox']:focus,[type='radio']:focus {
666
+ input:where([type='checkbox']):focus,input:where([type='radio']):focus {
662
667
  outline: 2px solid transparent;
663
668
  outline-offset: 2px;
664
669
  --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
@@ -670,7 +675,7 @@ select {
670
675
  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
671
676
  }
672
677
 
673
- [type='checkbox']:checked,[type='radio']:checked {
678
+ input:where([type='checkbox']):checked,input:where([type='radio']):checked {
674
679
  border-color: transparent;
675
680
  background-color: currentColor;
676
681
  background-size: 100% 100%;
@@ -678,20 +683,36 @@ select {
678
683
  background-repeat: no-repeat;
679
684
  }
680
685
 
681
- [type='checkbox']:checked {
686
+ input:where([type='checkbox']):checked {
682
687
  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
683
688
  }
684
689
 
685
- [type='radio']:checked {
690
+ @media (forced-colors: active) {
691
+ input:where([type='checkbox']):checked {
692
+ -webkit-appearance: auto;
693
+ -moz-appearance: auto;
694
+ appearance: auto;
695
+ }
696
+ }
697
+
698
+ input:where([type='radio']):checked {
686
699
  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
687
700
  }
688
701
 
689
- [type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
702
+ @media (forced-colors: active) {
703
+ input:where([type='radio']):checked {
704
+ -webkit-appearance: auto;
705
+ -moz-appearance: auto;
706
+ appearance: auto;
707
+ }
708
+ }
709
+
710
+ input:where([type='checkbox']):checked:hover,input:where([type='checkbox']):checked:focus,input:where([type='radio']):checked:hover,input:where([type='radio']):checked:focus {
690
711
  border-color: transparent;
691
712
  background-color: currentColor;
692
713
  }
693
714
 
694
- [type='checkbox']:indeterminate {
715
+ input:where([type='checkbox']):indeterminate {
695
716
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
696
717
  border-color: transparent;
697
718
  background-color: currentColor;
@@ -700,12 +721,20 @@ select {
700
721
  background-repeat: no-repeat;
701
722
  }
702
723
 
703
- [type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
724
+ @media (forced-colors: active) {
725
+ input:where([type='checkbox']):indeterminate {
726
+ -webkit-appearance: auto;
727
+ -moz-appearance: auto;
728
+ appearance: auto;
729
+ }
730
+ }
731
+
732
+ input:where([type='checkbox']):indeterminate:hover,input:where([type='checkbox']):indeterminate:focus {
704
733
  border-color: transparent;
705
734
  background-color: currentColor;
706
735
  }
707
736
 
708
- [type='file'] {
737
+ input:where([type='file']) {
709
738
  background: unset;
710
739
  border-color: inherit;
711
740
  border-width: 0;
@@ -715,11 +744,45 @@ select {
715
744
  line-height: inherit;
716
745
  }
717
746
 
718
- [type='file']:focus {
747
+ input:where([type='file']):focus {
719
748
  outline: 1px solid ButtonText;
720
749
  outline: 1px auto -webkit-focus-ring-color;
721
750
  }
722
751
 
752
+ .container {
753
+ width: 100%;
754
+ }
755
+
756
+ @media (min-width: 640px) {
757
+ .container {
758
+ max-width: 640px;
759
+ }
760
+ }
761
+
762
+ @media (min-width: 768px) {
763
+ .container {
764
+ max-width: 768px;
765
+ }
766
+ }
767
+
768
+ @media (min-width: 1024px) {
769
+ .container {
770
+ max-width: 1024px;
771
+ }
772
+ }
773
+
774
+ @media (min-width: 1280px) {
775
+ .container {
776
+ max-width: 1280px;
777
+ }
778
+ }
779
+
780
+ @media (min-width: 1536px) {
781
+ .container {
782
+ max-width: 1536px;
783
+ }
784
+ }
785
+
723
786
  .sr-only {
724
787
  position: absolute;
725
788
  width: 1px;
@@ -1184,6 +1247,11 @@ select {
1184
1247
  border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
1185
1248
  }
1186
1249
 
1250
+ .border-gray-500 {
1251
+ --tw-border-opacity: 1;
1252
+ border-color: rgb(107 114 128 / var(--tw-border-opacity, 1));
1253
+ }
1254
+
1187
1255
  .border-gray-600 {
1188
1256
  --tw-border-opacity: 1;
1189
1257
  border-color: rgb(75 85 99 / var(--tw-border-opacity, 1));
@@ -1351,6 +1419,10 @@ select {
1351
1419
  padding: 1rem;
1352
1420
  }
1353
1421
 
1422
+ .p-5 {
1423
+ padding: 1.25rem;
1424
+ }
1425
+
1354
1426
  .p-6 {
1355
1427
  padding: 1.5rem;
1356
1428
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Represents a server-side component in the ancestry chain.
3
+ * Used for Phoenix LiveView, Rails ViewComponents, Next.js RSC, etc.
4
+ */
5
+ export interface ServerComponentInfo {
6
+ /** Component name (e.g., "AppWeb.CoreComponents.button") or "@caller" for call site */
7
+ name: string;
8
+ /** File path (e.g., "lib/app_web/core_components.ex") */
9
+ filePath: string;
10
+ /** Line number in the source file */
11
+ line: number;
12
+ /** Type of server component annotation */
13
+ type: "component" | "caller";
14
+ }
@@ -0,0 +1 @@
1
+ export {};