@designfever/web-review-kit 0.2.0 → 0.4.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.
@@ -6,7 +6,7 @@ type ReviewMode = 'idle' | 'note' | 'element' | 'area';
6
6
  type ReviewSource = 'local' | 'supabase' | (string & {});
7
7
  type ReviewSubmitStatus = 'idle' | 'submitting' | 'submitted' | 'failed';
8
8
  type ReviewViewportScope = Exclude<ReviewItemScope, 'dom'>;
9
- type DomAnchorStrategy = 'configured-attribute' | 'id' | 'class' | 'dom-path';
9
+ type DomAnchorStrategy = 'configured-attribute' | 'attribute' | 'id' | 'class' | 'dom-path';
10
10
  interface ReviewRulerConfig {
11
11
  enabled?: boolean;
12
12
  unit?: string;
@@ -20,6 +20,8 @@ interface DomAnchorCandidate {
20
20
  interface DomSourceHint {
21
21
  component?: string;
22
22
  file?: string;
23
+ line?: string;
24
+ column?: string;
23
25
  sectionId?: string;
24
26
  sectionIndex?: string;
25
27
  }
@@ -117,6 +119,8 @@ interface ReviewViewportPreset {
117
119
  width: number;
118
120
  height: number;
119
121
  scope?: Exclude<ReviewItemScope, 'dom'>;
122
+ designWidth?: number;
123
+ designHeight?: number;
120
124
  }
121
125
  interface NumberedReviewItem {
122
126
  item: ReviewItem;
@@ -130,6 +134,7 @@ interface WebReviewKitOptions {
130
134
  userId?: string;
131
135
  adapter?: WebReviewKitAdapter;
132
136
  target?: WebReviewKitTarget | (() => WebReviewKitTarget | undefined);
137
+ adjustmentLabel?: string;
133
138
  viewports?: {
134
139
  presets?: ReviewViewportPreset[];
135
140
  };
@@ -6,7 +6,7 @@ type ReviewMode = 'idle' | 'note' | 'element' | 'area';
6
6
  type ReviewSource = 'local' | 'supabase' | (string & {});
7
7
  type ReviewSubmitStatus = 'idle' | 'submitting' | 'submitted' | 'failed';
8
8
  type ReviewViewportScope = Exclude<ReviewItemScope, 'dom'>;
9
- type DomAnchorStrategy = 'configured-attribute' | 'id' | 'class' | 'dom-path';
9
+ type DomAnchorStrategy = 'configured-attribute' | 'attribute' | 'id' | 'class' | 'dom-path';
10
10
  interface ReviewRulerConfig {
11
11
  enabled?: boolean;
12
12
  unit?: string;
@@ -20,6 +20,8 @@ interface DomAnchorCandidate {
20
20
  interface DomSourceHint {
21
21
  component?: string;
22
22
  file?: string;
23
+ line?: string;
24
+ column?: string;
23
25
  sectionId?: string;
24
26
  sectionIndex?: string;
25
27
  }
@@ -117,6 +119,8 @@ interface ReviewViewportPreset {
117
119
  width: number;
118
120
  height: number;
119
121
  scope?: Exclude<ReviewItemScope, 'dom'>;
122
+ designWidth?: number;
123
+ designHeight?: number;
120
124
  }
121
125
  interface NumberedReviewItem {
122
126
  item: ReviewItem;
@@ -130,6 +134,7 @@ interface WebReviewKitOptions {
130
134
  userId?: string;
131
135
  adapter?: WebReviewKitAdapter;
132
136
  target?: WebReviewKitTarget | (() => WebReviewKitTarget | undefined);
137
+ adjustmentLabel?: string;
133
138
  viewports?: {
134
139
  presets?: ReviewViewportPreset[];
135
140
  };
package/dist/vite.cjs ADDED
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/vite.ts
21
+ var vite_exports = {};
22
+ __export(vite_exports, {
23
+ reviewSourceLocator: () => reviewSourceLocator
24
+ });
25
+ module.exports = __toCommonJS(vite_exports);
26
+ var VIRTUAL_JSX_DEV_RUNTIME_ID = "\0@designfever/web-review-kit/source-locator/jsx-dev-runtime";
27
+ var reviewSourceLocator = (options = {}) => {
28
+ let runtimeOptions = createRuntimeOptions(options);
29
+ return {
30
+ name: "df-web-review-kit-source-locator",
31
+ enforce: "pre",
32
+ configResolved(config) {
33
+ runtimeOptions = createRuntimeOptions(options, config);
34
+ },
35
+ resolveId(id, importer) {
36
+ if (!runtimeOptions.enabled) return null;
37
+ if (id !== "react/jsx-dev-runtime") return null;
38
+ if (importer === VIRTUAL_JSX_DEV_RUNTIME_ID) return null;
39
+ return VIRTUAL_JSX_DEV_RUNTIME_ID;
40
+ },
41
+ load(id) {
42
+ if (id !== VIRTUAL_JSX_DEV_RUNTIME_ID) return null;
43
+ return createJsxDevRuntime(runtimeOptions);
44
+ }
45
+ };
46
+ };
47
+ function createRuntimeOptions(options, config) {
48
+ const attributePrefix = (options.attributePrefix ?? "data-wrk-source").replace(
49
+ /-+$/,
50
+ ""
51
+ );
52
+ const root = normalizePath(options.root ?? config?.root ?? "");
53
+ const enabled = options.enabled ?? config?.command === "serve";
54
+ return {
55
+ enabled,
56
+ root,
57
+ include: (options.include ?? []).map(createRuntimeMatcher),
58
+ exclude: (options.exclude ?? ["node_modules", "dist"]).map(
59
+ createRuntimeMatcher
60
+ ),
61
+ filePath: options.filePath ?? "relative",
62
+ line: options.line ?? true,
63
+ column: options.column ?? true,
64
+ fileAttribute: `${attributePrefix}-file`,
65
+ lineAttribute: `${attributePrefix}-line`,
66
+ columnAttribute: `${attributePrefix}-column`
67
+ };
68
+ }
69
+ function createRuntimeMatcher(pattern) {
70
+ if (pattern instanceof RegExp) {
71
+ return { type: "regex", value: pattern.source, flags: pattern.flags };
72
+ }
73
+ return { type: "path", value: normalizePath(pattern).replace(/^\.\//, "") };
74
+ }
75
+ function normalizePath(value) {
76
+ return value.replace(/\\/g, "/").replace(/\/+$/, "");
77
+ }
78
+ function createJsxDevRuntime(options) {
79
+ return `
80
+ import { Fragment, jsxDEV as baseJsxDEV } from 'react/jsx-dev-runtime';
81
+
82
+ const OPTIONS = ${JSON.stringify(options)};
83
+
84
+ export { Fragment };
85
+
86
+ export function jsxDEV(type, props, key, isStaticChildren, source, self) {
87
+ return baseJsxDEV(
88
+ type,
89
+ injectSourceProps(type, props, source),
90
+ key,
91
+ isStaticChildren,
92
+ source,
93
+ self
94
+ );
95
+ }
96
+
97
+ function injectSourceProps(type, props, source) {
98
+ if (typeof type !== 'string') return props;
99
+ if (!source || typeof source.fileName !== 'string') return props;
100
+
101
+ const sourceFile = getSourceFile(source.fileName);
102
+ if (!sourceFile) return props;
103
+
104
+ const nextProps = props ? { ...props } : {};
105
+ if (nextProps[OPTIONS.fileAttribute] == null) {
106
+ nextProps[OPTIONS.fileAttribute] = sourceFile;
107
+ }
108
+ if (OPTIONS.line && source.lineNumber != null && nextProps[OPTIONS.lineAttribute] == null) {
109
+ nextProps[OPTIONS.lineAttribute] = String(source.lineNumber);
110
+ }
111
+ if (OPTIONS.column && source.columnNumber != null && nextProps[OPTIONS.columnAttribute] == null) {
112
+ nextProps[OPTIONS.columnAttribute] = String(source.columnNumber);
113
+ }
114
+
115
+ return nextProps;
116
+ }
117
+
118
+ function getSourceFile(fileName) {
119
+ const absoluteFile = normalizePath(fileName);
120
+ const relativeFile = getRelativeFile(absoluteFile);
121
+
122
+ if (OPTIONS.include.length > 0 && !matchesAny(OPTIONS.include, absoluteFile, relativeFile)) {
123
+ return null;
124
+ }
125
+ if (matchesAny(OPTIONS.exclude, absoluteFile, relativeFile)) return null;
126
+
127
+ return OPTIONS.filePath === 'absolute' ? absoluteFile : relativeFile;
128
+ }
129
+
130
+ function getRelativeFile(absoluteFile) {
131
+ if (!OPTIONS.root) return absoluteFile;
132
+ if (absoluteFile === OPTIONS.root) return '';
133
+ if (absoluteFile.startsWith(OPTIONS.root + '/')) {
134
+ return absoluteFile.slice(OPTIONS.root.length + 1);
135
+ }
136
+
137
+ return absoluteFile;
138
+ }
139
+
140
+ function matchesAny(patterns, absoluteFile, relativeFile) {
141
+ return patterns.some((pattern) =>
142
+ matchesPattern(pattern, absoluteFile, relativeFile)
143
+ );
144
+ }
145
+
146
+ function matchesPattern(pattern, absoluteFile, relativeFile) {
147
+ if (pattern.type === 'regex') {
148
+ const regex = new RegExp(pattern.value, pattern.flags);
149
+ return regex.test(absoluteFile) || regex.test(relativeFile);
150
+ }
151
+
152
+ const value = pattern.value;
153
+ const target = isAbsolutePattern(value) ? absoluteFile : relativeFile;
154
+ if (!value.includes('*')) {
155
+ return target === value || target.startsWith(value + '/');
156
+ }
157
+
158
+ return globToRegExp(value).test(target);
159
+ }
160
+
161
+ function isAbsolutePattern(value) {
162
+ return value.startsWith('/') || /^[a-zA-Z]:\\//.test(value);
163
+ }
164
+
165
+ function globToRegExp(value) {
166
+ const source = escapeRegExp(value)
167
+ .replace(/\\\\\\*\\\\\\*/g, '.*')
168
+ .replace(/\\\\\\*/g, '[^/]*');
169
+
170
+ return new RegExp('^' + source + '$');
171
+ }
172
+
173
+ function escapeRegExp(value) {
174
+ return value.replace(/[|\\\\{}()[\\]^$+*?.]/g, '\\\\$&');
175
+ }
176
+
177
+ function normalizePath(value) {
178
+ return value.replace(/\\\\/g, '/').replace(/\\/+$/, '');
179
+ }
180
+ `;
181
+ }
182
+ // Annotate the CommonJS export names for ESM import in node:
183
+ 0 && (module.exports = {
184
+ reviewSourceLocator
185
+ });
186
+ //# sourceMappingURL=vite.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/vite.ts"],"sourcesContent":["import type { Plugin, ResolvedConfig } from 'vite';\n\ntype SourceLocatorPattern = string | RegExp;\n\nexport interface ReviewSourceLocatorOptions {\n enabled?: boolean;\n root?: string;\n include?: readonly SourceLocatorPattern[];\n exclude?: readonly SourceLocatorPattern[];\n filePath?: 'relative' | 'absolute';\n line?: boolean;\n column?: boolean;\n attributePrefix?: string;\n}\n\ntype RuntimeMatcher =\n | { type: 'path'; value: string }\n | { type: 'regex'; value: string; flags: string };\n\ntype RuntimeOptions = {\n enabled: boolean;\n root: string;\n include: RuntimeMatcher[];\n exclude: RuntimeMatcher[];\n filePath: 'relative' | 'absolute';\n line: boolean;\n column: boolean;\n fileAttribute: string;\n lineAttribute: string;\n columnAttribute: string;\n};\n\nconst VIRTUAL_JSX_DEV_RUNTIME_ID =\n '\\0@designfever/web-review-kit/source-locator/jsx-dev-runtime';\n\nexport const reviewSourceLocator = (\n options: ReviewSourceLocatorOptions = {}\n): Plugin => {\n let runtimeOptions = createRuntimeOptions(options);\n\n return {\n name: 'df-web-review-kit-source-locator',\n enforce: 'pre',\n configResolved(config) {\n runtimeOptions = createRuntimeOptions(options, config);\n },\n resolveId(id, importer) {\n if (!runtimeOptions.enabled) return null;\n if (id !== 'react/jsx-dev-runtime') return null;\n if (importer === VIRTUAL_JSX_DEV_RUNTIME_ID) return null;\n\n return VIRTUAL_JSX_DEV_RUNTIME_ID;\n },\n load(id) {\n if (id !== VIRTUAL_JSX_DEV_RUNTIME_ID) return null;\n return createJsxDevRuntime(runtimeOptions);\n },\n };\n};\n\nfunction createRuntimeOptions(\n options: ReviewSourceLocatorOptions,\n config?: ResolvedConfig\n): RuntimeOptions {\n const attributePrefix = (options.attributePrefix ?? 'data-wrk-source').replace(\n /-+$/,\n ''\n );\n const root = normalizePath(options.root ?? config?.root ?? '');\n const enabled = options.enabled ?? (config?.command === 'serve');\n\n return {\n enabled,\n root,\n include: (options.include ?? []).map(createRuntimeMatcher),\n exclude: (options.exclude ?? ['node_modules', 'dist']).map(\n createRuntimeMatcher\n ),\n filePath: options.filePath ?? 'relative',\n line: options.line ?? true,\n column: options.column ?? true,\n fileAttribute: `${attributePrefix}-file`,\n lineAttribute: `${attributePrefix}-line`,\n columnAttribute: `${attributePrefix}-column`,\n };\n}\n\nfunction createRuntimeMatcher(pattern: SourceLocatorPattern): RuntimeMatcher {\n if (pattern instanceof RegExp) {\n return { type: 'regex', value: pattern.source, flags: pattern.flags };\n }\n\n return { type: 'path', value: normalizePath(pattern).replace(/^\\.\\//, '') };\n}\n\nfunction normalizePath(value: string) {\n return value.replace(/\\\\/g, '/').replace(/\\/+$/, '');\n}\n\nfunction createJsxDevRuntime(options: RuntimeOptions) {\n return `\nimport { Fragment, jsxDEV as baseJsxDEV } from 'react/jsx-dev-runtime';\n\nconst OPTIONS = ${JSON.stringify(options)};\n\nexport { Fragment };\n\nexport function jsxDEV(type, props, key, isStaticChildren, source, self) {\n return baseJsxDEV(\n type,\n injectSourceProps(type, props, source),\n key,\n isStaticChildren,\n source,\n self\n );\n}\n\nfunction injectSourceProps(type, props, source) {\n if (typeof type !== 'string') return props;\n if (!source || typeof source.fileName !== 'string') return props;\n\n const sourceFile = getSourceFile(source.fileName);\n if (!sourceFile) return props;\n\n const nextProps = props ? { ...props } : {};\n if (nextProps[OPTIONS.fileAttribute] == null) {\n nextProps[OPTIONS.fileAttribute] = sourceFile;\n }\n if (OPTIONS.line && source.lineNumber != null && nextProps[OPTIONS.lineAttribute] == null) {\n nextProps[OPTIONS.lineAttribute] = String(source.lineNumber);\n }\n if (OPTIONS.column && source.columnNumber != null && nextProps[OPTIONS.columnAttribute] == null) {\n nextProps[OPTIONS.columnAttribute] = String(source.columnNumber);\n }\n\n return nextProps;\n}\n\nfunction getSourceFile(fileName) {\n const absoluteFile = normalizePath(fileName);\n const relativeFile = getRelativeFile(absoluteFile);\n\n if (OPTIONS.include.length > 0 && !matchesAny(OPTIONS.include, absoluteFile, relativeFile)) {\n return null;\n }\n if (matchesAny(OPTIONS.exclude, absoluteFile, relativeFile)) return null;\n\n return OPTIONS.filePath === 'absolute' ? absoluteFile : relativeFile;\n}\n\nfunction getRelativeFile(absoluteFile) {\n if (!OPTIONS.root) return absoluteFile;\n if (absoluteFile === OPTIONS.root) return '';\n if (absoluteFile.startsWith(OPTIONS.root + '/')) {\n return absoluteFile.slice(OPTIONS.root.length + 1);\n }\n\n return absoluteFile;\n}\n\nfunction matchesAny(patterns, absoluteFile, relativeFile) {\n return patterns.some((pattern) =>\n matchesPattern(pattern, absoluteFile, relativeFile)\n );\n}\n\nfunction matchesPattern(pattern, absoluteFile, relativeFile) {\n if (pattern.type === 'regex') {\n const regex = new RegExp(pattern.value, pattern.flags);\n return regex.test(absoluteFile) || regex.test(relativeFile);\n }\n\n const value = pattern.value;\n const target = isAbsolutePattern(value) ? absoluteFile : relativeFile;\n if (!value.includes('*')) {\n return target === value || target.startsWith(value + '/');\n }\n\n return globToRegExp(value).test(target);\n}\n\nfunction isAbsolutePattern(value) {\n return value.startsWith('/') || /^[a-zA-Z]:\\\\//.test(value);\n}\n\nfunction globToRegExp(value) {\n const source = escapeRegExp(value)\n .replace(/\\\\\\\\\\\\*\\\\\\\\\\\\*/g, '.*')\n .replace(/\\\\\\\\\\\\*/g, '[^/]*');\n\n return new RegExp('^' + source + '$');\n}\n\nfunction escapeRegExp(value) {\n return value.replace(/[|\\\\\\\\{}()[\\\\]^$+*?.]/g, '\\\\\\\\$&');\n}\n\nfunction normalizePath(value) {\n return value.replace(/\\\\\\\\/g, '/').replace(/\\\\/+$/, '');\n}\n`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCA,IAAM,6BACJ;AAEK,IAAM,sBAAsB,CACjC,UAAsC,CAAC,MAC5B;AACX,MAAI,iBAAiB,qBAAqB,OAAO;AAEjD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,eAAe,QAAQ;AACrB,uBAAiB,qBAAqB,SAAS,MAAM;AAAA,IACvD;AAAA,IACA,UAAU,IAAI,UAAU;AACtB,UAAI,CAAC,eAAe,QAAS,QAAO;AACpC,UAAI,OAAO,wBAAyB,QAAO;AAC3C,UAAI,aAAa,2BAA4B,QAAO;AAEpD,aAAO;AAAA,IACT;AAAA,IACA,KAAK,IAAI;AACP,UAAI,OAAO,2BAA4B,QAAO;AAC9C,aAAO,oBAAoB,cAAc;AAAA,IAC3C;AAAA,EACF;AACF;AAEA,SAAS,qBACP,SACA,QACgB;AAChB,QAAM,mBAAmB,QAAQ,mBAAmB,mBAAmB;AAAA,IACrE;AAAA,IACA;AAAA,EACF;AACA,QAAM,OAAO,cAAc,QAAQ,QAAQ,QAAQ,QAAQ,EAAE;AAC7D,QAAM,UAAU,QAAQ,WAAY,QAAQ,YAAY;AAExD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU,QAAQ,WAAW,CAAC,GAAG,IAAI,oBAAoB;AAAA,IACzD,UAAU,QAAQ,WAAW,CAAC,gBAAgB,MAAM,GAAG;AAAA,MACrD;AAAA,IACF;AAAA,IACA,UAAU,QAAQ,YAAY;AAAA,IAC9B,MAAM,QAAQ,QAAQ;AAAA,IACtB,QAAQ,QAAQ,UAAU;AAAA,IAC1B,eAAe,GAAG,eAAe;AAAA,IACjC,eAAe,GAAG,eAAe;AAAA,IACjC,iBAAiB,GAAG,eAAe;AAAA,EACrC;AACF;AAEA,SAAS,qBAAqB,SAA+C;AAC3E,MAAI,mBAAmB,QAAQ;AAC7B,WAAO,EAAE,MAAM,SAAS,OAAO,QAAQ,QAAQ,OAAO,QAAQ,MAAM;AAAA,EACtE;AAEA,SAAO,EAAE,MAAM,QAAQ,OAAO,cAAc,OAAO,EAAE,QAAQ,SAAS,EAAE,EAAE;AAC5E;AAEA,SAAS,cAAc,OAAe;AACpC,SAAO,MAAM,QAAQ,OAAO,GAAG,EAAE,QAAQ,QAAQ,EAAE;AACrD;AAEA,SAAS,oBAAoB,SAAyB;AACpD,SAAO;AAAA;AAAA;AAAA,kBAGS,KAAK,UAAU,OAAO,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmGzC;","names":[]}
@@ -0,0 +1,16 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ type SourceLocatorPattern = string | RegExp;
4
+ interface ReviewSourceLocatorOptions {
5
+ enabled?: boolean;
6
+ root?: string;
7
+ include?: readonly SourceLocatorPattern[];
8
+ exclude?: readonly SourceLocatorPattern[];
9
+ filePath?: 'relative' | 'absolute';
10
+ line?: boolean;
11
+ column?: boolean;
12
+ attributePrefix?: string;
13
+ }
14
+ declare const reviewSourceLocator: (options?: ReviewSourceLocatorOptions) => Plugin;
15
+
16
+ export { type ReviewSourceLocatorOptions, reviewSourceLocator };
package/dist/vite.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ type SourceLocatorPattern = string | RegExp;
4
+ interface ReviewSourceLocatorOptions {
5
+ enabled?: boolean;
6
+ root?: string;
7
+ include?: readonly SourceLocatorPattern[];
8
+ exclude?: readonly SourceLocatorPattern[];
9
+ filePath?: 'relative' | 'absolute';
10
+ line?: boolean;
11
+ column?: boolean;
12
+ attributePrefix?: string;
13
+ }
14
+ declare const reviewSourceLocator: (options?: ReviewSourceLocatorOptions) => Plugin;
15
+
16
+ export { type ReviewSourceLocatorOptions, reviewSourceLocator };
package/dist/vite.js ADDED
@@ -0,0 +1,161 @@
1
+ // src/vite.ts
2
+ var VIRTUAL_JSX_DEV_RUNTIME_ID = "\0@designfever/web-review-kit/source-locator/jsx-dev-runtime";
3
+ var reviewSourceLocator = (options = {}) => {
4
+ let runtimeOptions = createRuntimeOptions(options);
5
+ return {
6
+ name: "df-web-review-kit-source-locator",
7
+ enforce: "pre",
8
+ configResolved(config) {
9
+ runtimeOptions = createRuntimeOptions(options, config);
10
+ },
11
+ resolveId(id, importer) {
12
+ if (!runtimeOptions.enabled) return null;
13
+ if (id !== "react/jsx-dev-runtime") return null;
14
+ if (importer === VIRTUAL_JSX_DEV_RUNTIME_ID) return null;
15
+ return VIRTUAL_JSX_DEV_RUNTIME_ID;
16
+ },
17
+ load(id) {
18
+ if (id !== VIRTUAL_JSX_DEV_RUNTIME_ID) return null;
19
+ return createJsxDevRuntime(runtimeOptions);
20
+ }
21
+ };
22
+ };
23
+ function createRuntimeOptions(options, config) {
24
+ const attributePrefix = (options.attributePrefix ?? "data-wrk-source").replace(
25
+ /-+$/,
26
+ ""
27
+ );
28
+ const root = normalizePath(options.root ?? config?.root ?? "");
29
+ const enabled = options.enabled ?? config?.command === "serve";
30
+ return {
31
+ enabled,
32
+ root,
33
+ include: (options.include ?? []).map(createRuntimeMatcher),
34
+ exclude: (options.exclude ?? ["node_modules", "dist"]).map(
35
+ createRuntimeMatcher
36
+ ),
37
+ filePath: options.filePath ?? "relative",
38
+ line: options.line ?? true,
39
+ column: options.column ?? true,
40
+ fileAttribute: `${attributePrefix}-file`,
41
+ lineAttribute: `${attributePrefix}-line`,
42
+ columnAttribute: `${attributePrefix}-column`
43
+ };
44
+ }
45
+ function createRuntimeMatcher(pattern) {
46
+ if (pattern instanceof RegExp) {
47
+ return { type: "regex", value: pattern.source, flags: pattern.flags };
48
+ }
49
+ return { type: "path", value: normalizePath(pattern).replace(/^\.\//, "") };
50
+ }
51
+ function normalizePath(value) {
52
+ return value.replace(/\\/g, "/").replace(/\/+$/, "");
53
+ }
54
+ function createJsxDevRuntime(options) {
55
+ return `
56
+ import { Fragment, jsxDEV as baseJsxDEV } from 'react/jsx-dev-runtime';
57
+
58
+ const OPTIONS = ${JSON.stringify(options)};
59
+
60
+ export { Fragment };
61
+
62
+ export function jsxDEV(type, props, key, isStaticChildren, source, self) {
63
+ return baseJsxDEV(
64
+ type,
65
+ injectSourceProps(type, props, source),
66
+ key,
67
+ isStaticChildren,
68
+ source,
69
+ self
70
+ );
71
+ }
72
+
73
+ function injectSourceProps(type, props, source) {
74
+ if (typeof type !== 'string') return props;
75
+ if (!source || typeof source.fileName !== 'string') return props;
76
+
77
+ const sourceFile = getSourceFile(source.fileName);
78
+ if (!sourceFile) return props;
79
+
80
+ const nextProps = props ? { ...props } : {};
81
+ if (nextProps[OPTIONS.fileAttribute] == null) {
82
+ nextProps[OPTIONS.fileAttribute] = sourceFile;
83
+ }
84
+ if (OPTIONS.line && source.lineNumber != null && nextProps[OPTIONS.lineAttribute] == null) {
85
+ nextProps[OPTIONS.lineAttribute] = String(source.lineNumber);
86
+ }
87
+ if (OPTIONS.column && source.columnNumber != null && nextProps[OPTIONS.columnAttribute] == null) {
88
+ nextProps[OPTIONS.columnAttribute] = String(source.columnNumber);
89
+ }
90
+
91
+ return nextProps;
92
+ }
93
+
94
+ function getSourceFile(fileName) {
95
+ const absoluteFile = normalizePath(fileName);
96
+ const relativeFile = getRelativeFile(absoluteFile);
97
+
98
+ if (OPTIONS.include.length > 0 && !matchesAny(OPTIONS.include, absoluteFile, relativeFile)) {
99
+ return null;
100
+ }
101
+ if (matchesAny(OPTIONS.exclude, absoluteFile, relativeFile)) return null;
102
+
103
+ return OPTIONS.filePath === 'absolute' ? absoluteFile : relativeFile;
104
+ }
105
+
106
+ function getRelativeFile(absoluteFile) {
107
+ if (!OPTIONS.root) return absoluteFile;
108
+ if (absoluteFile === OPTIONS.root) return '';
109
+ if (absoluteFile.startsWith(OPTIONS.root + '/')) {
110
+ return absoluteFile.slice(OPTIONS.root.length + 1);
111
+ }
112
+
113
+ return absoluteFile;
114
+ }
115
+
116
+ function matchesAny(patterns, absoluteFile, relativeFile) {
117
+ return patterns.some((pattern) =>
118
+ matchesPattern(pattern, absoluteFile, relativeFile)
119
+ );
120
+ }
121
+
122
+ function matchesPattern(pattern, absoluteFile, relativeFile) {
123
+ if (pattern.type === 'regex') {
124
+ const regex = new RegExp(pattern.value, pattern.flags);
125
+ return regex.test(absoluteFile) || regex.test(relativeFile);
126
+ }
127
+
128
+ const value = pattern.value;
129
+ const target = isAbsolutePattern(value) ? absoluteFile : relativeFile;
130
+ if (!value.includes('*')) {
131
+ return target === value || target.startsWith(value + '/');
132
+ }
133
+
134
+ return globToRegExp(value).test(target);
135
+ }
136
+
137
+ function isAbsolutePattern(value) {
138
+ return value.startsWith('/') || /^[a-zA-Z]:\\//.test(value);
139
+ }
140
+
141
+ function globToRegExp(value) {
142
+ const source = escapeRegExp(value)
143
+ .replace(/\\\\\\*\\\\\\*/g, '.*')
144
+ .replace(/\\\\\\*/g, '[^/]*');
145
+
146
+ return new RegExp('^' + source + '$');
147
+ }
148
+
149
+ function escapeRegExp(value) {
150
+ return value.replace(/[|\\\\{}()[\\]^$+*?.]/g, '\\\\$&');
151
+ }
152
+
153
+ function normalizePath(value) {
154
+ return value.replace(/\\\\/g, '/').replace(/\\/+$/, '');
155
+ }
156
+ `;
157
+ }
158
+ export {
159
+ reviewSourceLocator
160
+ };
161
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/vite.ts"],"sourcesContent":["import type { Plugin, ResolvedConfig } from 'vite';\n\ntype SourceLocatorPattern = string | RegExp;\n\nexport interface ReviewSourceLocatorOptions {\n enabled?: boolean;\n root?: string;\n include?: readonly SourceLocatorPattern[];\n exclude?: readonly SourceLocatorPattern[];\n filePath?: 'relative' | 'absolute';\n line?: boolean;\n column?: boolean;\n attributePrefix?: string;\n}\n\ntype RuntimeMatcher =\n | { type: 'path'; value: string }\n | { type: 'regex'; value: string; flags: string };\n\ntype RuntimeOptions = {\n enabled: boolean;\n root: string;\n include: RuntimeMatcher[];\n exclude: RuntimeMatcher[];\n filePath: 'relative' | 'absolute';\n line: boolean;\n column: boolean;\n fileAttribute: string;\n lineAttribute: string;\n columnAttribute: string;\n};\n\nconst VIRTUAL_JSX_DEV_RUNTIME_ID =\n '\\0@designfever/web-review-kit/source-locator/jsx-dev-runtime';\n\nexport const reviewSourceLocator = (\n options: ReviewSourceLocatorOptions = {}\n): Plugin => {\n let runtimeOptions = createRuntimeOptions(options);\n\n return {\n name: 'df-web-review-kit-source-locator',\n enforce: 'pre',\n configResolved(config) {\n runtimeOptions = createRuntimeOptions(options, config);\n },\n resolveId(id, importer) {\n if (!runtimeOptions.enabled) return null;\n if (id !== 'react/jsx-dev-runtime') return null;\n if (importer === VIRTUAL_JSX_DEV_RUNTIME_ID) return null;\n\n return VIRTUAL_JSX_DEV_RUNTIME_ID;\n },\n load(id) {\n if (id !== VIRTUAL_JSX_DEV_RUNTIME_ID) return null;\n return createJsxDevRuntime(runtimeOptions);\n },\n };\n};\n\nfunction createRuntimeOptions(\n options: ReviewSourceLocatorOptions,\n config?: ResolvedConfig\n): RuntimeOptions {\n const attributePrefix = (options.attributePrefix ?? 'data-wrk-source').replace(\n /-+$/,\n ''\n );\n const root = normalizePath(options.root ?? config?.root ?? '');\n const enabled = options.enabled ?? (config?.command === 'serve');\n\n return {\n enabled,\n root,\n include: (options.include ?? []).map(createRuntimeMatcher),\n exclude: (options.exclude ?? ['node_modules', 'dist']).map(\n createRuntimeMatcher\n ),\n filePath: options.filePath ?? 'relative',\n line: options.line ?? true,\n column: options.column ?? true,\n fileAttribute: `${attributePrefix}-file`,\n lineAttribute: `${attributePrefix}-line`,\n columnAttribute: `${attributePrefix}-column`,\n };\n}\n\nfunction createRuntimeMatcher(pattern: SourceLocatorPattern): RuntimeMatcher {\n if (pattern instanceof RegExp) {\n return { type: 'regex', value: pattern.source, flags: pattern.flags };\n }\n\n return { type: 'path', value: normalizePath(pattern).replace(/^\\.\\//, '') };\n}\n\nfunction normalizePath(value: string) {\n return value.replace(/\\\\/g, '/').replace(/\\/+$/, '');\n}\n\nfunction createJsxDevRuntime(options: RuntimeOptions) {\n return `\nimport { Fragment, jsxDEV as baseJsxDEV } from 'react/jsx-dev-runtime';\n\nconst OPTIONS = ${JSON.stringify(options)};\n\nexport { Fragment };\n\nexport function jsxDEV(type, props, key, isStaticChildren, source, self) {\n return baseJsxDEV(\n type,\n injectSourceProps(type, props, source),\n key,\n isStaticChildren,\n source,\n self\n );\n}\n\nfunction injectSourceProps(type, props, source) {\n if (typeof type !== 'string') return props;\n if (!source || typeof source.fileName !== 'string') return props;\n\n const sourceFile = getSourceFile(source.fileName);\n if (!sourceFile) return props;\n\n const nextProps = props ? { ...props } : {};\n if (nextProps[OPTIONS.fileAttribute] == null) {\n nextProps[OPTIONS.fileAttribute] = sourceFile;\n }\n if (OPTIONS.line && source.lineNumber != null && nextProps[OPTIONS.lineAttribute] == null) {\n nextProps[OPTIONS.lineAttribute] = String(source.lineNumber);\n }\n if (OPTIONS.column && source.columnNumber != null && nextProps[OPTIONS.columnAttribute] == null) {\n nextProps[OPTIONS.columnAttribute] = String(source.columnNumber);\n }\n\n return nextProps;\n}\n\nfunction getSourceFile(fileName) {\n const absoluteFile = normalizePath(fileName);\n const relativeFile = getRelativeFile(absoluteFile);\n\n if (OPTIONS.include.length > 0 && !matchesAny(OPTIONS.include, absoluteFile, relativeFile)) {\n return null;\n }\n if (matchesAny(OPTIONS.exclude, absoluteFile, relativeFile)) return null;\n\n return OPTIONS.filePath === 'absolute' ? absoluteFile : relativeFile;\n}\n\nfunction getRelativeFile(absoluteFile) {\n if (!OPTIONS.root) return absoluteFile;\n if (absoluteFile === OPTIONS.root) return '';\n if (absoluteFile.startsWith(OPTIONS.root + '/')) {\n return absoluteFile.slice(OPTIONS.root.length + 1);\n }\n\n return absoluteFile;\n}\n\nfunction matchesAny(patterns, absoluteFile, relativeFile) {\n return patterns.some((pattern) =>\n matchesPattern(pattern, absoluteFile, relativeFile)\n );\n}\n\nfunction matchesPattern(pattern, absoluteFile, relativeFile) {\n if (pattern.type === 'regex') {\n const regex = new RegExp(pattern.value, pattern.flags);\n return regex.test(absoluteFile) || regex.test(relativeFile);\n }\n\n const value = pattern.value;\n const target = isAbsolutePattern(value) ? absoluteFile : relativeFile;\n if (!value.includes('*')) {\n return target === value || target.startsWith(value + '/');\n }\n\n return globToRegExp(value).test(target);\n}\n\nfunction isAbsolutePattern(value) {\n return value.startsWith('/') || /^[a-zA-Z]:\\\\//.test(value);\n}\n\nfunction globToRegExp(value) {\n const source = escapeRegExp(value)\n .replace(/\\\\\\\\\\\\*\\\\\\\\\\\\*/g, '.*')\n .replace(/\\\\\\\\\\\\*/g, '[^/]*');\n\n return new RegExp('^' + source + '$');\n}\n\nfunction escapeRegExp(value) {\n return value.replace(/[|\\\\\\\\{}()[\\\\]^$+*?.]/g, '\\\\\\\\$&');\n}\n\nfunction normalizePath(value) {\n return value.replace(/\\\\\\\\/g, '/').replace(/\\\\/+$/, '');\n}\n`;\n}\n"],"mappings":";AAgCA,IAAM,6BACJ;AAEK,IAAM,sBAAsB,CACjC,UAAsC,CAAC,MAC5B;AACX,MAAI,iBAAiB,qBAAqB,OAAO;AAEjD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,eAAe,QAAQ;AACrB,uBAAiB,qBAAqB,SAAS,MAAM;AAAA,IACvD;AAAA,IACA,UAAU,IAAI,UAAU;AACtB,UAAI,CAAC,eAAe,QAAS,QAAO;AACpC,UAAI,OAAO,wBAAyB,QAAO;AAC3C,UAAI,aAAa,2BAA4B,QAAO;AAEpD,aAAO;AAAA,IACT;AAAA,IACA,KAAK,IAAI;AACP,UAAI,OAAO,2BAA4B,QAAO;AAC9C,aAAO,oBAAoB,cAAc;AAAA,IAC3C;AAAA,EACF;AACF;AAEA,SAAS,qBACP,SACA,QACgB;AAChB,QAAM,mBAAmB,QAAQ,mBAAmB,mBAAmB;AAAA,IACrE;AAAA,IACA;AAAA,EACF;AACA,QAAM,OAAO,cAAc,QAAQ,QAAQ,QAAQ,QAAQ,EAAE;AAC7D,QAAM,UAAU,QAAQ,WAAY,QAAQ,YAAY;AAExD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU,QAAQ,WAAW,CAAC,GAAG,IAAI,oBAAoB;AAAA,IACzD,UAAU,QAAQ,WAAW,CAAC,gBAAgB,MAAM,GAAG;AAAA,MACrD;AAAA,IACF;AAAA,IACA,UAAU,QAAQ,YAAY;AAAA,IAC9B,MAAM,QAAQ,QAAQ;AAAA,IACtB,QAAQ,QAAQ,UAAU;AAAA,IAC1B,eAAe,GAAG,eAAe;AAAA,IACjC,eAAe,GAAG,eAAe;AAAA,IACjC,iBAAiB,GAAG,eAAe;AAAA,EACrC;AACF;AAEA,SAAS,qBAAqB,SAA+C;AAC3E,MAAI,mBAAmB,QAAQ;AAC7B,WAAO,EAAE,MAAM,SAAS,OAAO,QAAQ,QAAQ,OAAO,QAAQ,MAAM;AAAA,EACtE;AAEA,SAAO,EAAE,MAAM,QAAQ,OAAO,cAAc,OAAO,EAAE,QAAQ,SAAS,EAAE,EAAE;AAC5E;AAEA,SAAS,cAAc,OAAe;AACpC,SAAO,MAAM,QAAQ,OAAO,GAAG,EAAE,QAAQ,QAAQ,EAAE;AACrD;AAEA,SAAS,oBAAoB,SAAyB;AACpD,SAAO;AAAA;AAAA;AAAA,kBAGS,KAAK,UAAU,OAAO,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmGzC;","names":[]}
@@ -61,8 +61,11 @@ Persisted review data uses target-space viewport values plus optional anchor-rel
61
61
  When a user creates a DOM or area item, core tries to capture:
62
62
 
63
63
  - an explicit configured anchor such as `data-qa-id`
64
+ - common test/section attributes such as `data-testid`, `data-cy`, or `data-section-id`
64
65
  - a meaningful `id`
66
+ - semantic attributes such as `aria-label`, `title`, `name`, or `href`
65
67
  - a meaningful class
68
+ - a scoped DOM path from the nearest stable ancestor
66
69
  - a DOM path fallback
67
70
  - a short text fingerprint
68
71
 
@@ -170,6 +170,45 @@ mountReviewShell({
170
170
 
171
171
  See [DB setup](db-setup.md) before enabling Supabase in a shared environment.
172
172
 
173
+ ## Source Locator
174
+
175
+ Host projects can opt into source file hints for local QA. The Vite plugin wraps React's dev JSX runtime and writes source attributes to rendered DOM nodes.
176
+
177
+ ```ts
178
+ import { defineConfig } from 'vite';
179
+ import { reviewSourceLocator } from '@designfever/web-review-kit/vite';
180
+
181
+ export default defineConfig({
182
+ plugins: [
183
+ reviewSourceLocator({
184
+ enabled: true,
185
+ include: ['src'],
186
+ filePath: 'absolute',
187
+ }),
188
+ ],
189
+ });
190
+ ```
191
+
192
+ Captured DOM nodes will include `data-wrk-source-file`, `data-wrk-source-line`, and `data-wrk-source-column`. In the review shell, hold `Option` over the target iframe to show source candidates from the DOM ancestry. Click the target to pin the candidate list, then choose which file to open. If the file path is absolute, it opens directly. If the plugin stores relative paths, pass `sourceRoot` when mounting the shell.
193
+
194
+ ```tsx
195
+ mountReviewShell({
196
+ projectId: REVIEW_PROJECT_ID,
197
+ pages,
198
+ adapters,
199
+ reviewPathPrefix: REVIEW_PATH_PREFIX,
200
+ sourceRoot: import.meta.env.VITE_REVIEW_SOURCE_ROOT,
201
+ sourceInspector: {
202
+ editor: 'vscode', // 'vscode' | 'cursor' | 'webstorm' | 'custom'
203
+ // urlTemplate: 'my-editor://open?file={encodedPath}&line={line}&column={column}',
204
+ },
205
+ });
206
+ ```
207
+
208
+ Set `sourceInspector.enabled` to `false` when source code opening should be unavailable. The `data-font` overlay still belongs to the target project markup and is not required for source opening.
209
+
210
+ Use this only in dev/review builds. Source paths are written into the browser DOM and can be persisted with QA items.
211
+
173
212
  ## Custom Adapter
174
213
 
175
214
  If a team or host project owns its own QA backend, keep that adapter in the host project or in a separate package. Start from [adaptor.sample.ts](adaptor.sample.ts) and map its `WebReviewKitAdapter` methods to your backend API.
@@ -0,0 +1,94 @@
1
+ # 릴리즈 노트: 0.3.0
2
+
3
+ 0.2.0 mainline merge 이후 변경 사항 정리.
4
+
5
+ 비교 기준: `49f2665` (`Merge pull request #7 ... feat: release web review kit 0.2.0`)
6
+ 검토 기준 HEAD: `a82b204` (`release: 0.3.0`)
7
+
8
+ ## 주요 변경
9
+
10
+ - 개발용 Vite source locator export `@designfever/web-review-kit/vite`를 추가했다.
11
+ - DOM 리뷰 대상에 source file hint를 주입하고, 리뷰 shell에서 VS Code로 바로 여는 동작을 추가했다.
12
+ - QA item card에서 comment를 수정할 수 있게 했다.
13
+ - ruler 가독성, 측정 label, light theme 대비를 개선했다.
14
+ - `data-font`를 가진 요소에 source-select font hint overlay를 표시한다.
15
+ - 입력 필드에서 글을 쓰는 중 review hotkey가 실행되지 않도록 수정했다.
16
+
17
+ ## 추가
18
+
19
+ ### 리뷰 대상 Source Locator
20
+
21
+ Host project가 Vite에서 `reviewSourceLocator()`를 선택적으로 사용할 수 있다. 이 plugin은 React dev JSX runtime을 감싸서 렌더링된 DOM node에 source attribute를 주입한다.
22
+
23
+ - `data-wrk-source-file`
24
+ - `data-wrk-source-line`
25
+ - `data-wrk-source-column`
26
+
27
+ Source hint가 있으면 review shell에서 해당 source 위치를 VS Code로 열 수 있다. 절대 경로 source path와 `sourceRoot` 기준 상대 경로를 모두 지원한다.
28
+
29
+ Public package 변경:
30
+
31
+ - `./vite` package export 추가
32
+ - `src/vite.ts` 추가
33
+ - `ReviewShellProps`에 optional `sourceRoot` 추가
34
+ - `DomSourceHint`에 `line`, `column` 추가
35
+
36
+ ### Review Shell Source Action
37
+
38
+ Review shell에서 다음 동작을 지원한다.
39
+
40
+ - Target iframe 안에서 `Option` + click으로 클릭한 source hint 열기
41
+ - Option key 활성화 중 source hover 표시
42
+ - `data-font` metadata가 있는 요소의 source-select font hint overlay 표시
43
+ - 저장된 source hint가 있는 QA card에 source open action 표시
44
+ - Source hint가 없거나 source root가 필요한 경우 toast feedback 표시
45
+
46
+ ### QA Comment 수정
47
+
48
+ Active adapter가 update를 지원하면 QA item card에 edit action을 표시한다. Edit modal은 빈 comment를 막고, active adapter로 저장한 뒤 review data를 새로고침하고 toast feedback을 보여준다.
49
+
50
+ Adapter normalization에 `canUpdate`를 추가해서 read-only source에서는 edit action을 숨길 수 있게 했다.
51
+
52
+ ## 변경
53
+
54
+ ### Ruler UI
55
+
56
+ - 측정 label을 Figma 전용 문구가 아니라 중립적인 `width x height unit` 형식으로 바꿨다.
57
+ - Dark/light theme 모두에서 ruler label, coordinate chip, guide line, popover 가독성을 개선했다.
58
+ - 후속 fix에서 light theme ruler 대비를 한 번 더 개선했다.
59
+
60
+ ### Review Shell Workflow
61
+
62
+ - QA item list 정렬 기준을 `updatedAt`에서 `createdAt`으로 바꿨다. 이제 기존 item을 수정해도 목록 최상단으로 튀지 않는다.
63
+ - Supabase list ordering도 remote source에서 같은 동작을 하도록 `created_at` 기준으로 바꿨다.
64
+ - Quick add toolbar는 DOM element capture와 area capture 중심으로 정리했다.
65
+ - Edit comment modal은 settings modal의 visual system을 재사용한다.
66
+
67
+ ### Prompt Context
68
+
69
+ DOM source hint가 있으면 review item prompt에 source line context를 포함한다.
70
+
71
+ ## 수정
72
+
73
+ - Focus가 `input`, `textarea`, `select`, `contenteditable` 안에 있을 때 review hotkey를 무시한다.
74
+ - DOM source 추출이 새 `data-wrk-source-*` attribute와 기존 `data-file` / `data-component` 계열 attribute를 모두 읽도록 했다.
75
+
76
+ ## 문서
77
+
78
+ - README에 public import `@designfever/web-review-kit/vite`를 문서화했다.
79
+ - Installation docs에 source locator 설정, `sourceRoot`, source path가 DOM에 기록되는 dev/review build 주의사항을 추가했다.
80
+
81
+ ## 검증
82
+
83
+ 검토한 main branch 기준으로 아래 명령을 확인했다.
84
+
85
+ - `pnpm typecheck`
86
+ - `pnpm typecheck:dev`
87
+ - `pnpm build`
88
+ - `pnpm build:dev`
89
+
90
+ ## 메모
91
+
92
+ - `package.json` version은 `0.3.0`이다.
93
+ - `0.3.0` release commit은 `a82b204`다.
94
+ - 이 노트를 작성한 시점에는 local/remote 모두 `0.3.0` git tag가 없었다.